Compare commits

..
Author SHA1 Message Date
nxglabs 5561673fdc Merge pull request #866 from nxglabs/fix_main_issues 2025-06-19 09:31:17 +00:00
prafull-opensignlabsandGitHub 0df44062d6 Merge pull request #1803 from OpenSignLabs/updates-15736278136
fix: correct baseapi of docxtopdf and decryptpdf
2025-06-18 20:25:53 +05:30
nxglabs 8983440763 Merge pull request #864 from nxglabs/staging 2025-06-18 14:54:13 +00:00
prafull-opensignlabsandGitHub 5a28c0ed06 Merge pull request #1802 from OpenSignLabs/updates-15735030679
v2.25.0
2025-06-18 19:43:43 +05:30
nxglabs cbca22c308 Merge pull request #861 from nxglabs/staging
fix: handle password protected pdf
2025-06-18 14:03:05 +00:00
76 changed files with 3621 additions and 1417 deletions
+213
View File
@@ -0,0 +1,213 @@
# Tailwind Dark Mode Usage Guide
## Overview
This guide shows how to use the new Tailwind utilities for better dark mode visibility in OpenSign.
## Button Styling
### VS Code-style Disabled Buttons
```jsx
// Option 1: Direct VS Code styling
<button className="op-btn op-btn-primary op-btn-vscode-disabled" disabled>
Disabled Button
</button>
// Option 2: Themed disabled styling
<button className="op-btn btn-themed-disabled">
Themed Button
</button>
// Option 3: Conditional styling
<button
className={`op-btn op-btn-primary ${isDisabled ? 'op-btn-vscode-disabled' : ''}`}
disabled={isDisabled}
>
Dynamic Button
</button>
```
## Icon Styling
### Theme-aware Icons
```jsx
// Better visibility in dark mode
<i className="fa-light fa-folder icon-improved"></i>
// Muted but still visible
<i className="fa-light fa-plus icon-muted"></i>
// Disabled state
<i className="fa-light fa-trash icon-disabled"></i>
```
### CSS Variable Approach
```jsx
// Using CSS variables
<i className="fa-light fa-search icon-themed"></i>
<i className="fa-light fa-settings icon-themed-muted"></i>
// Inline styles with CSS variables
<i
className="fa-light fa-plus"
style={{ color: 'var(--icon-color)' }}
/>
```
### Legacy JavaScript Function (Still Supported)
```jsx
// Existing approach - still works
<i
className="fa-light fa-plus"
style={{ color: getThemeIconColor() }}
/>
```
## Text Styling
### Improved Gray Text
```jsx
// These automatically improve in dark mode
<span className="text-gray-500">More visible in dark mode</span>
<span className="text-gray-400">Muted but readable</span>
<span className="text-gray-600">Clear text</span>
```
## Complete Examples
### Toolbar with Better Visibility
```jsx
const Toolbar = () => (
<div className="flex space-x-2 p-2">
<button className="p-2 hover:bg-gray-200 rounded">
<i className="fa-light fa-plus icon-improved"></i>
</button>
<button className="p-2 hover:bg-gray-200 rounded" disabled>
<i className="fa-light fa-trash icon-disabled"></i>
</button>
<button className="p-2 hover:bg-gray-200 rounded">
<i className="fa-light fa-edit icon-improved"></i>
</button>
</div>
);
```
### Form with Disabled States
```jsx
const Form = ({ isSubmitting }) => (
<form>
<input className="op-input" />
<button
className={`op-btn op-btn-primary ${isSubmitting ? 'op-btn-vscode-disabled' : ''}`}
disabled={isSubmitting}
>
{isSubmitting ? 'Submitting...' : 'Submit'}
</button>
</form>
);
```
## React-Tour and Tooltip Dark Mode Support
### React-Tour Modals
The react-tour modals now automatically support dark mode with VS Code-inspired styling:
```jsx
// These components automatically get dark mode styling
<Tour
onRequestClose={closeTour}
steps={tourConfig}
isOpen={isOpen}
rounded={5}
/>
```
### ReactTooltip Components
All ReactTooltip instances now support dark mode:
```jsx
// Automatically styled for dark mode
<ReactTooltip id="my-tooltip" className="z-[999]">
<div className="max-w-[200px]">
<p>Tooltip content</p>
</div>
</ReactTooltip>
```
### HoverCard Balloon UI
The balloon tooltips in OpenSign Drive now properly support dark mode:
```jsx
// These automatically get dark styling in dark mode
<HoverCard>
<HoverCardContent>
Document information
</HoverCardContent>
</HoverCard>
```
## Dark Mode Features Added
### 1. **React-Tour Modal Styling**
- Background: `#1F2937` (VS Code modal background)
- Text: `#E5E7EB` (soft white for readability)
- Borders: `#374151` (subtle borders)
- Buttons: VS Code-style primary/secondary buttons
### 2. **ReactTooltip Styling**
- Background: `#1F2937` with proper contrast
- Border: `#374151` for definition
- Box shadow: Enhanced for dark backgrounds
- Text: `#E5E7EB` for optimal readability
### 3. **HoverCard Balloon UI**
- Background: `#1F2937` (matches VS Code)
- Text: `#E5E7EB` for readability
- Arrow: Automatically matches background color
- Enhanced shadows for dark backgrounds
### 4. **React-Datepicker Support**
- Calendar background: `#1F2937`
- Selected dates: VS Code blue (`#007ACC`)
- Hover states: Proper contrast ratios
- Navigation arrows: Themed appropriately
## CSS Classes Reference
| Class | Purpose | Dark Mode Color |
|-------|---------|----------------|
| `icon-improved` | Better icon visibility | `#CCCCCC` |
| `icon-muted` | Muted but visible icons | `#999999` |
| `icon-disabled` | Disabled icon state | `#858585` |
| `op-btn-vscode-disabled` | VS Code disabled button | Background: `#3C3C3C` |
| `btn-themed-disabled` | Themed disabled button | Uses CSS variables |
| `icon-themed` | Variable-based icon color | `var(--icon-color)` |
| `.reactour__helper` | `#1F2937` background |
| `.react-tooltip` | `#1F2937` background |
| `.HoverCardContent` | `#1F2937` background |
| `.react-datepicker` | `#1F2937` background |
## Migration Guide
### From JavaScript Function to Tailwind
```jsx
// Before
<i style={{ color: getThemeIconColor() }} className="fa-light fa-plus" />
// After
<i className="fa-light fa-plus icon-improved" />
```
### From Hardcoded Colors to Theme-aware
```jsx
// Before
<i className="fa-light fa-plus text-gray-500" />
// After (automatic improvement)
<i className="fa-light fa-plus text-gray-500" />
// OR explicitly
<i className="fa-light fa-plus icon-improved" />
```
## Migration Notes
All existing tooltip and tour components will automatically inherit the new dark mode styling when the theme is set to `opensigndark`. No code changes required for existing implementations.
@@ -0,0 +1,92 @@
/**
* Tailwind Dark Mode Usage Examples for OpenSign
*
* This file demonstrates how to use the new Tailwind utilities
* for better dark mode visibility of buttons and icons.
*/
// Example 1: VS Code-style disabled buttons
const DisabledButtonExamples = () => {
return (
<div>
{/* Option A: Using the VS Code disabled style */}
<button className="op-btn op-btn-primary op-btn-vscode-disabled" disabled>
VS Code Style Disabled Button
</button>
{/* Option B: Using themed disabled style */}
<button className="op-btn btn-themed-disabled">
Themed Disabled Button
</button>
{/* Option C: Conditional styling */}
<button
className={`op-btn op-btn-primary ${
isDisabled ? "op-btn-vscode-disabled" : ""
}`}
disabled={isDisabled}
>
Conditional Button
</button>
</div>
);
};
// Example 2: Icon visibility improvements
const IconExamples = () => {
return (
<div>
{/* Theme-aware icons with better visibility */}
<i className="fa-light fa-folder icon-improved"></i>
<i className="fa-light fa-plus icon-muted"></i>
<i className="fa-light fa-trash icon-disabled"></i>
{/* Using CSS variables */}
<i className="fa-light fa-search icon-themed"></i>
<i className="fa-light fa-settings icon-themed-muted"></i>
{/* Gray text that automatically improves in dark mode */}
<span className="text-gray-500">
This text is now more visible in dark mode
</span>
<span className="text-gray-400">Muted but still readable</span>
</div>
);
};
// Example 3: Using CSS variables in inline styles
const InlineStyleExamples = () => {
return (
<div>
{/* Using CSS variables directly */}
<i className="fa-light fa-plus" style={{ color: "var(--icon-color)" }} />
{/* Using the existing JavaScript function */}
<i className="fa-light fa-minus" style={{ color: getThemeIconColor() }} />
</div>
);
};
// Example 4: Toolbar with improved icons
const ToolbarExample = () => {
return (
<div className="flex space-x-2 p-2">
<button className="p-2 hover:bg-gray-200 rounded">
<i className="fa-light fa-plus icon-improved"></i>
</button>
<button className="p-2 hover:bg-gray-200 rounded" disabled>
<i className="fa-light fa-trash icon-disabled"></i>
</button>
<button className="p-2 hover:bg-gray-200 rounded">
<i className="fa-light fa-edit icon-improved"></i>
</button>
</div>
);
};
export {
DisabledButtonExamples,
IconExamples,
InlineStyleExamples,
ToolbarExample
};
@@ -0,0 +1,94 @@
/**
* Tailwind Dark Mode Usage Examples for OpenSign
*
* This file demonstrates how to use the new Tailwind utilities
* for better dark mode visibility of buttons and icons.
*/
// Example 1: VS Code-style disabled buttons
const DisabledButtonExamples = () => {
return (
<div>
{/* Option A: Using the VS Code disabled style */}
<button className="op-btn op-btn-primary op-btn-vscode-disabled" disabled>
VS Code Style Disabled Button
</button>
{/* Option B: Using themed disabled style */}
<button className="op-btn btn-themed-disabled">
Themed Disabled Button
</button>
{/* Option C: Conditional styling */}
<button
className={`op-btn op-btn-primary ${isDisabled ? 'op-btn-vscode-disabled' : ''}`}
disabled={isDisabled}
>
Conditional Button
</button>
</div>
);
};
// Example 2: Icon visibility improvements
const IconExamples = () => {
return (
<div>
{/* Theme-aware icons with better visibility */}
<i className="fa-light fa-folder icon-improved"></i>
<i className="fa-light fa-plus icon-muted"></i>
<i className="fa-light fa-trash icon-disabled"></i>
{/* Using CSS variables */}
<i className="fa-light fa-search icon-themed"></i>
<i className="fa-light fa-settings icon-themed-muted"></i>
{/* Gray text that automatically improves in dark mode */}
<span className="text-gray-500">This text is now more visible in dark mode</span>
<span className="text-gray-400">Muted but still readable</span>
</div>
);
};
// Example 3: Using CSS variables in inline styles
const InlineStyleExamples = () => {
return (
<div>
{/* Using CSS variables directly */}
<i
className="fa-light fa-plus"
style={{ color: 'var(--icon-color)' }}
/>
{/* Using the existing JavaScript function */}
<i
className="fa-light fa-minus"
style={{ color: getThemeIconColor() }}
/>
</div>
);
};
// Example 4: Toolbar with improved icons
const ToolbarExample = () => {
return (
<div className="flex space-x-2 p-2">
<button className="p-2 hover:bg-gray-200 rounded">
<i className="fa-light fa-plus icon-improved"></i>
</button>
<button className="p-2 hover:bg-gray-200 rounded" disabled>
<i className="fa-light fa-trash icon-disabled"></i>
</button>
<button className="p-2 hover:bg-gray-200 rounded">
<i className="fa-light fa-edit icon-improved"></i>
</button>
</div>
);
};
export {
DisabledButtonExamples,
IconExamples,
InlineStyleExamples,
ToolbarExample
};
+16 -6
View File
@@ -28,6 +28,7 @@
"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",
@@ -5995,9 +5996,9 @@
}
},
"node_modules/brace-expansion": {
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
"version": "1.1.12",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
"license": "MIT",
"dependencies": {
"balanced-match": "^1.0.0",
@@ -12462,6 +12463,15 @@
"node": ">= 12.0.0"
}
},
"node_modules/quill-html-edit-button": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/quill-html-edit-button/-/quill-html-edit-button-3.0.0.tgz",
"integrity": "sha512-ZUV3rYjEvXz+v/7f8N0Mkp2+0D8VGDj6yCYPfagSLmyshczeMawS7vZJzaJ18dJp35IfjiPnzqyVk3KCI8qN1w==",
"license": "MIT",
"peerDependencies": {
"quill": "^2.x"
}
},
"node_modules/radix-ui": {
"version": "1.4.2",
"resolved": "https://registry.npmjs.org/radix-ui/-/radix-ui-1.4.2.tgz",
@@ -15976,9 +15986,9 @@
}
},
"node_modules/sucrase/node_modules/brace-expansion": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
"integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
"integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
"dev": true,
"license": "MIT",
"dependencies": {
+1
View File
@@ -23,6 +23,7 @@
"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",
@@ -5,6 +5,7 @@
"create-account": "Konto erstellen",
"login": "Anmelden",
"language": "Sprache",
"dark-mode": "Dunkelmodus",
"name": "Name",
"phone": "Telefon",
"phone-optional": "optional",
@@ -753,6 +754,7 @@
"delete-page": "Seite löschen",
"merge-pdf": "PDFs zusammenführen",
"add-pages": "Seiten hinzufügen",
"reorder-pages": "Seiten neu anordnen",
"delete-alert": "Einzelne Seite kann nicht gelöscht werden.",
"delete-alert-2": "Sind Sie sicher, dass Sie diese Seite löschen möchten?",
"delete-note": "Hinweis: Sobald Sie diese Seite löschen, kann dies nicht rückgängig gemacht werden.",
@@ -985,6 +987,7 @@
"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",
@@ -1026,8 +1029,9 @@
"could-not-parse-signer-info": "Signaturinformationen konnten nicht analysiert werden",
"not-calculated": "Nicht berechnet",
"not-found-in-signature": "Nicht in Signatur gefunden",
"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"
"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…"
}
@@ -5,6 +5,7 @@
"create-account": "Create account",
"login": "Login",
"language": "Language",
"dark-mode": "Dark mode",
"name": "Name",
"phone": "Phone",
"phone-optional": "optional",
@@ -199,7 +200,7 @@
},
"file-type": "pdf, png, jpg, jpeg",
"docx": "docx",
"file-selected": "file selected",
"file-selected": "file(s) selected",
"template-title": "Template title",
"document-title": "Document title",
"description": "Description",
@@ -529,7 +530,7 @@
"new-password": "New password",
"confirm-password": "Confirm password",
"file-alert-1": "The selected file size is too large. Please select a file less than",
"file-alert-2": "Please select file.",
"file-alert-2": "Please select file(s).",
"file-alert-3": "Please wait while the document is being uploaded.",
"enter-pdf-password": "Enter Pdf password",
"correct-password": "Please provide correct password",
@@ -753,6 +754,7 @@
"delete-page": "Delete page",
"merge-pdf": "Merge pdf",
"add-pages": "Add pages",
"reorder-pages": "Reorder pages",
"delete-alert": "Can not delete single page",
"delete-alert-2": "Are you sure you want to delete this page?",
"delete-note": "Note: Once you delete this page, you cannot undo.",
@@ -888,7 +890,7 @@
"you-will-receive-email-shortly": "✅ That's it! You'll receive a confirmation email shortly.",
"please-provide-templateid": "Please provide templateid",
"this-template-is-not-public": "This template is not public",
"invalid-templateid": "Invaldi templateid",
"invalid-templateid": "Invalid templateid",
"contact-billing-at-opensign": "To add more seats, please contact OpenSign™ at <1>billing@opensignlabs.com</1> for assistance",
"title-length-alert": "Title must be at most 250 characters long.",
"note-length-alert": "Note must be at most 200 characters long.",
@@ -985,6 +987,7 @@
"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",
@@ -1026,8 +1029,9 @@
"could-not-parse-signer-info": "Could not parse signer info",
"not-calculated": "Not calculated",
"not-found-in-signature": "Not found in signature",
"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"
"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…"
}
@@ -5,6 +5,7 @@
"create-account": "Crear cuenta",
"login": "Iniciar sesión",
"language": "Idioma",
"dark-mode": "Modo oscuro",
"name": "Nombre",
"phone": "Teléfono",
"phone-optional": "opcional",
@@ -753,6 +754,7 @@
"delete-page": "eliminar página",
"merge-pdf": "fusionar pdf",
"add-pages": "Agregar páginas",
"reorder-pages": "Reordenar páginas",
"delete-alert": "No se puede eliminar una sola página",
"delete-alert-2": "¿Está seguro de que desea eliminar esta página?",
"delete-note": "Nota: una vez que elimines esta página, no podrás deshacerla",
@@ -985,6 +987,7 @@
"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",
@@ -1026,8 +1029,9 @@
"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-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"
"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…"
}
@@ -5,6 +5,7 @@
"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)",
@@ -753,6 +754,7 @@
"delete-page": "supprimer la page",
"merge-pdf": "Fusionner le pdf",
"add-pages": "Ajouter des pages",
"reorder-pages": "Réorganiser les pages",
"delete-alert": "Impossible de supprimer une seule page",
"delete-alert-2": "Etes-vous sûr de vouloir supprimer cette page ?",
"delete-note": "Remarque : une fois cette page supprimée, vous ne pouvez plus l'annuler",
@@ -985,6 +987,7 @@
"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é",
@@ -1026,8 +1029,9 @@
"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-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"
"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…"
}
@@ -5,6 +5,7 @@
"create-account": "खाता बनाएं",
"login": "लॉग इन करें",
"language": "भाषा",
"dark-mode": "डार्क मोड",
"name": "नाम",
"phone": "फ़ोन",
"phone-optional": "वैकल्पिक",
@@ -753,6 +754,7 @@
"delete-page": "पृष्ठ हटाएं",
"merge-pdf": "पीडीएफ मर्ज करें",
"add-pages": "पृष्ठ जोड़ें",
"reorder-pages": "पृष्ठ पुनः व्यवस्थित करें",
"delete-alert": "एकल पृष्ठ नहीं हटाया जा सकता",
"delete-alert-2": "क्या आप वाकई इस पृष्ठ को हटाना चाहते हैं?",
"delete-note": "ध्यान दें: एक बार जब आप इस पृष्ठ को हटा देते हैं, तो आप इसे पूर्ववत नहीं कर सकते।",
@@ -985,6 +987,7 @@
"review": "समीक्षा",
"next-field": "अगला फ़ील्ड",
"required-mssg": "{{totalWidget}} में से {{leftRequiredWidget}} फ़ील्ड शेष हैं",
"verify-document": "दस्तावेज़ सत्यापित करें",
"verify-document-signature": "दस्तावेज़ हस्ताक्षर सत्यापित करें",
"select-pdf-document": "पीडीएफ दस्तावेज़ चुनें",
"selected-file": "चयनित फ़ाइल",
@@ -1026,8 +1029,9 @@
"could-not-parse-signer-info": "हस्ताक्षरकर्ता जानकारी पार्स नहीं की जा सकी",
"not-calculated": "गणना नहीं की गई",
"not-found-in-signature": "हस्ताक्षर में नहीं मिला",
"readonly-textinput-error": "रीड-ओनली टेक्स्ट विजेट में एक डिफ़ॉल्ट मान होना चाहिए या आप इसे वैकल्पिक बना सकते हैं।",
"readonly-dropdown-error": "रीड-ओनली ड्रॉपडाउन विजेट में एक डिफ़ॉल्ट मान होना चाहिए या आप इसे वैकल्पिक बना सकते हैं।",
"readonly-radiobtn-error": "रीड-ओनली रेडियो बटन विजेट में एक डिफ़ॉल्ट मान होना चाहिए या आप इसे वैकल्पिक बना सकते हैं।",
"choose-one":"एक का चयन"
"readonly-error": "रीड-ओनली {{widgetName}} विजेट में एक डिफ़ॉल्ट मान होना चाहिए या आप इसे वैकल्पिक बना सकते हैं।",
"choose-one":"एक का चयन",
"search-templates": "टेम्पलेट खोजें…",
"search-documents": "दस्तावेज़ खोजें…",
"search-contacts": "संपर्क खोजें…"
}
@@ -5,6 +5,7 @@
"create-account": "Crea Account",
"login": "Accedi",
"language": "Lingua",
"dark-mode": "Modalità scura",
"name": "Nome",
"phone": "Telefono",
"phone-optional": "facoltativo",
@@ -753,6 +754,7 @@
"delete-page": "Elimina pagina",
"merge-pdf": "Unisci PDF",
"add-pages": "Aggiungi pagine",
"reorder-pages": "Riordina pagine",
"delete-alert": "Non è possibile eliminare una singola pagina",
"delete-alert-2": "Sei sicuro di voler eliminare questa pagina?",
"delete-note": "Nota: Una volta eliminata questa pagina, non potrai annullare l'operazione.",
@@ -985,6 +987,7 @@
"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",
@@ -1026,8 +1029,9 @@
"could-not-parse-signer-info": "Impossibile analizzare le informazioni del firmatario",
"not-calculated": "Non calcolato",
"not-found-in-signature": "Non trovato nella firma",
"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"
"readonly-error": "Il widget {{widgetName}} di sola lettura deve avere un valore predefinito oppure può essere reso facoltativo.",
"choose-one":"Scegline uno",
"search-templates": "Cerca modelli…",
"search-documents": "Cerca documenti…",
"search-contacts": "Cerca contatti…"
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 95 KiB

+1 -1
View File
@@ -142,7 +142,7 @@ function App() {
/>
{/* signyouself route with no rowlevel data using docId from url */}
<Route path="/signaturePdf/:docId" element={<SignYourSelf />} />
{/* draft document route to handle and navigate route page accordiing to document status */}
{/* draft document route to handle and navigate route page according to document status */}
<Route path="/draftDocument" element={<DraftDocument />} />
{/* recipient placeholder set route with no rowlevel data using docId from url*/}
<Route
+1 -1
View File
@@ -20,7 +20,7 @@ const BulkSendUi = (props) => {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
//function to check atleast one signature field exist
//function to check at least one signature field exist
const signatureExist = async () => {
setIsDisableBulkSend(false);
const getPlaceholder = props?.Placeholders;
+40 -1
View File
@@ -1,6 +1,7 @@
import { useState, useEffect } from "react";
import dp from "../assets/images/dp.png";
import FullScreenButton from "./FullScreenButton";
import ThemeToggle from "./ThemeToggle";
import { useNavigate } from "react-router";
import Parse from "parse";
import { useWindowSize } from "../hook/useWindowSize";
@@ -85,6 +86,34 @@ const Header = ({ showSidebar, setIsMenu, isConsole }) => {
};
}, [isOpen]);
useEffect(() => {
const updateLogoForTheme = () => {
const isDarkMode =
document.documentElement.getAttribute("data-theme") === "opensigndark";
const logo = isDarkMode
? "/static/js/assets/images/logo-dark.png" // Path to the dark mode logo
: appInfo.applogo; // Use current logo for light mode
if (applogo !== logo) {
setAppLogo(logo);
}
};
// Set the logo immediately based on the current theme
updateLogoForTheme();
const observer = new MutationObserver(() => {
updateLogoForTheme();
});
observer.observe(document.documentElement, {
attributes: true,
attributeFilter: ["data-theme"]
});
return () => observer.disconnect();
}, [applogo]);
return (
<div>
<div className="op-navbar bg-base-100 shadow">
@@ -147,7 +176,7 @@ const Header = ({ showSidebar, setIsMenu, isConsole }) => {
</div>
<ul
tabIndex={0}
className={`mt-3 z-[1] p-2 shadow op-dropdown-open op-menu op-menu-sm op-dropdown-content text-base-content bg-base-100 rounded-box w-52 ${
className={`mt-3 z-[1] p-2 shadow op-dropdown-open op-menu op-menu-sm op-dropdown-content text-base-content bg-base-100 rounded-box w-56 ${
isOpen ? "" : "hidden"
}`}
>
@@ -194,6 +223,16 @@ 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}>
@@ -0,0 +1,41 @@
import { useEffect, useState } from "react";
const ThemeToggle = () => {
const [isDark, setIsDark] = useState(false);
useEffect(() => {
const storedTheme = localStorage.getItem("theme");
if (storedTheme === "dark") {
setIsDark(true);
document.documentElement.setAttribute("data-theme", "opensigndark");
} else {
document.documentElement.setAttribute("data-theme", "opensigncss");
}
}, []);
const handleChange = () => {
const newTheme = !isDark;
setIsDark(newTheme);
if (newTheme) {
document.documentElement.setAttribute("data-theme", "opensigndark");
localStorage.setItem("theme", "dark");
} else {
document.documentElement.setAttribute("data-theme", "opensigncss");
localStorage.setItem("theme", "light");
}
};
return (
<>
<input
id="dark-mode-toggle"
type="checkbox"
className="op-toggle checked:[--tglbg:#3368ff] transition-all checked:bg-white"
checked={isDark}
onChange={handleChange}
/>
</>
);
};
export default ThemeToggle;
@@ -29,7 +29,7 @@ const DashboardButton = (props) => {
: "cursor-default"
} w-full shadow-md px-3 py-2 op-card bg-base-100`}
>
<div className="flex flex-row items-center">
<div className="flex flex-row items-center text-base-content">
<div className="flex flex-row items-center">
<span className="rounded-full bg-base-content bg-opacity-20 w-[60px] h-[60px] self-start flex justify-center items-center">
<i
@@ -39,7 +39,7 @@ const DashboardButton = (props) => {
></i>
</span>
</div>
<div className="text-lg ml-3 text-base-content">
<div className="text-lg ml-3">
{t(`sidebar.${props.Label}`)}
{props.Label === "Sign yourself" && (
<div className="text-gray-500 text-xs mt-1">
@@ -1,4 +1,4 @@
import React, { useEffect, useState } from "react";
import React, { useEffect, useState, useRef } from "react";
import Parse from "parse";
import ReportTable from "../../primitives/GetReportDisplay";
import reportJson from "../../json/ReportJson";
@@ -17,10 +17,16 @@ function DashboardReport(props) {
const [isMoreDocs, setIsMoreDocs] = useState(true);
const abortController = new AbortController();
const docPerPage = 5;
const [searchTerm, setSearchTerm] = useState("");
const [mobileSearchOpen, setMobileSearchOpen] = useState(false);
const [isSearchResult, setIsSearchResult] = useState(false);
const debounceTimer = useRef(null);
useEffect(() => {
setReportName("");
getReportData(props.Record.reportId);
setSearchTerm("");
setMobileSearchOpen(false);
getReportData(props.Record.reportId, 0, 20, "");
// Function returned from useEffect is called on unmount
return () => {
@@ -36,12 +42,69 @@ function DashboardReport(props) {
// below useEffect call when isNextRecord state is true and fetch next record
useEffect(() => {
if (isNextRecord) {
getReportData(props.Record.reportId, List.length, 20);
getReportData(props.Record.reportId, List.length, 20, searchTerm);
}
// eslint-disable-next-line
}, [isNextRecord]);
const getReportData = async (id, skipUserRecord = 0, limit = 20) => {
const handleSearchChange = async (e) => {
const term = e.target.value.toLowerCase();
setSearchTerm(term);
if (debounceTimer.current) {
clearTimeout(debounceTimer.current);
}
debounceTimer.current = setTimeout(async () => {
try {
const headers = {
"Content-Type": "application/json",
"X-Parse-Application-Id": localStorage.getItem("parseAppId"),
sessiontoken: localStorage.getItem("accesstoken")
};
const url = `${localStorage.getItem("baseUrl")}functions/getReport`;
const res = await axios.post(
url,
{
reportId: props.Record.reportId,
searchTerm: term,
skip: 0,
limit: docPerPage
},
{ headers }
);
const data = res.data?.result || [];
if (!data.error) {
setList(data);
setIsMoreDocs(data.length >= docPerPage);
setIsNextRecord(false);
setIsSearchResult(true);
}
} catch (err) {
console.error("Search error:", err);
}
}, 300);
setIsSearchResult(false);
};
const handleSearchPaste = (e) => {
setTimeout(() => {
handleSearchChange({ target: { value: e.target.value } });
}, 0);
};
useEffect(() => {
return () => {
if (debounceTimer.current) {
clearTimeout(debounceTimer.current);
}
};
}, []);
const getReportData = async (
id,
skipUserRecord = 0,
limit = 20,
term = searchTerm
) => {
setIsLoader(true);
const json = reportJson(id);
if (json) {
@@ -59,6 +122,9 @@ function DashboardReport(props) {
const skipRecord = id === "5Go51Q7T8r" ? 0 : skipUserRecord;
const limitRecord = id === "5Go51Q7T8r" ? 200 : limit;
const params = { reportId: id, skip: skipRecord, limit: limitRecord };
if (term) {
params.searchTerm = term;
}
const url = `${localStorage.getItem("baseUrl")}functions/getReport`;
const res = await axios.post(url, params, {
headers: headers,
@@ -141,11 +207,17 @@ function DashboardReport(props) {
setIsNextRecord={setIsNextRecord}
isMoreDocs={isMoreDocs}
docPerPage={docPerPage}
mobileSearchOpen={mobileSearchOpen}
setMobileSearchOpen={setMobileSearchOpen}
searchTerm={searchTerm}
handleSearchChange={handleSearchChange}
handleSearchPaste={handleSearchPaste}
isSearchResult={isSearchResult}
/>
) : (
<div className="flex items-center justify-center h-[100px] w-full bg-white rounded">
<div className="text-center">
<p className="text-xl text-black">{t("report-not-found")}</p>
<p className="text-xl text-base-content">{t("report-not-found")}</p>
</div>
</div>
)}
@@ -10,7 +10,7 @@ const AddRoleModal = (props) => {
isOpen={props.isModalRole}
handleClose={props.handleCloseRoleModal}
>
<div className="h-full py-[10px] px-[20px]">
<div className="text-base-content h-full py-[10px] px-[20px]">
<form className="flex flex-col" onSubmit={props.handleAddRole}>
<input
value={props.roleName}
@@ -38,7 +38,7 @@ function AgreementSign(props) {
}}
/>
</label>
<div className="text-[11px] md:text-base">
<div className="text-[11px] md:text-base text-base-content">
<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">
<div className="mt-2 text-base-content">
<span className="text-[11px]">{t("agreement-note")}</span>
</div>
</div>
@@ -12,12 +12,9 @@ function BorderResize(props) {
return (
<div
style={{
width: getHeight() || "14px",
height: getHeight() || "14px"
}}
style={{ width: getHeight() || "14px", height: getHeight() || "14px" }}
className={`${props.right ? `-right-[12px]` : "-right-[2px]"} ${
props.top ? `-bottom-[12px]` : "-bottom-[2px] "
props.top ? `-bottom-[12px]` : "-bottom-[2px]"
} absolute inline-block hover:cursor-sw-resize border-r-[3px] border-b-[3px] border-[#188ae2]`}
></div>
);
@@ -0,0 +1,104 @@
import React, { useState, useEffect } from "react";
import ModalUi from "../../primitives/ModalUi";
import { fontsizeArr, fontColorArr } from "../../constant/Utils";
import { useTranslation } from "react-i18next";
export default function CellsSettingModal({
isOpen,
handleClose,
defaultData,
handleSave
}) {
const { t } = useTranslation();
const [name, setName] = useState("");
const [cellCount, setCellCount] = useState(5);
const [fontSize, setFontSize] = useState(12);
const [fontColor, setFontColor] = useState("black");
useEffect(() => {
if (defaultData) {
setName(defaultData.options?.name || "Cells");
setCellCount(defaultData.options?.cellCount || 5);
setFontSize(defaultData.options?.fontSize || 12);
setFontColor(defaultData.options?.fontColor || "black");
}
}, [defaultData]);
const onSubmit = (e) => {
e.preventDefault();
handleSave &&
handleSave({
name,
cellCount: parseInt(cellCount, 10),
fontSize,
fontColor
});
};
return (
<ModalUi isOpen={isOpen} handleClose={handleClose} title={t("widget-info")}>
<form onSubmit={onSubmit} className="p-[20px] text-base-content flex flex-col gap-3">
<div>
<label htmlFor="name" className="text-[13px]">
{t("name")} <span className="text-[red]">*</span>
</label>
<input
required
name="name"
className="op-input op-input-bordered op-input-sm w-full text-xs focus:outline-none hover:border-base-content"
value={name}
onChange={(e) => setName(e.target.value)}
/>
</div>
<div>
<label htmlFor="cellCount" className="text-[13px]">
{t("cell-count")}
</label>
<input
type="number"
min="1"
name="cellCount"
className="op-input op-input-bordered op-input-sm w-full text-xs focus:outline-none hover:border-base-content"
value={cellCount}
onChange={(e) => setCellCount(e.target.value)}
/>
</div>
<div className="flex flex-col md:flex-row md:items-center gap-3">
<div className="flex items-center gap-2">
<span className="whitespace-nowrap">{t("font-size")}: </span>
<select
className="ml-[7px] w-[60%] op-select op-select-bordered op-select-sm focus:outline-none hover:border-base-content text-xs"
value={fontSize}
onChange={(e) => setFontSize(parseInt(e.target.value))}
>
{fontsizeArr.map((size, ind) => (
<option className="text-[13px]" value={size} key={ind}>
{size}
</option>
))}
</select>
</div>
<div className="flex items-center">
<span>{t("color")}: </span>
<select
className="ml-[33px] md:ml-4 w-[65%] md:w-full op-select op-select-bordered op-select-sm focus:outline-none hover:border-base-content text-xs"
value={fontColor}
onChange={(e) => setFontColor(e.target.value)}
>
{fontColorArr.map((color, ind) => (
<option value={color} key={ind}>
{t(`color-type.${color}`)}
</option>
))}
</select>
<span className="w-5 h-[19px] ml-1" style={{ background: fontColor }} />
</div>
</div>
<div className="h-[1px] w-full bg-[#b7b3b3] my-[16px]"></div>
<button type="submit" className="op-btn op-btn-primary">
{t("save")}
</button>
</form>
</ModalUi>
);
}
@@ -1,42 +1,56 @@
import { useState, useEffect, useRef } from "react";
const Cell = ({
isEnabled,
count,
h,
value,
editable,
onChange,
onKeyDown,
onBlur,
inputRef,
index
index,
fontSize,
fontColor,
hint
}) => (
<div
className="flex items-center justify-center border border-gray-800 bg-white"
className={`${isEnabled ? "bg-white border-gray-400" : "select-none-cls pointer-events-none border-gray-500"} flex items-center justify-center border-[1px]`}
style={{ flex: `0 0 ${100 / count}%`, height: h }}
>
<input
disabled={!isEnabled}
maxLength={1}
value={value}
readOnly={!editable}
ref={inputRef}
onChange={editable ? (e) => onChange && onChange(e, index) : undefined}
onKeyDown={editable ? (e) => onKeyDown && onKeyDown(e, index) : undefined}
className="w-full text-center uppercase focus:outline-none bg-transparent text-[12px]"
style={{ fontFamily: "Arial, sans-serif" }}
// trigger validation when leaving a cell
onBlur={editable ? (e) => onBlur && onBlur(e, index) : undefined}
className={`${isEnabled ? "placeholder-gray-300" : "placeholder-gray-500"} w-full text-center focus:outline-none bg-transparent`}
placeholder={hint}
style={{ fontFamily: "Arial, sans-serif", fontSize, color: fontColor }}
/>
</div>
);
export default function CellsWidget({
isEnabled,
count = 8,
height = 40,
value = "",
editable = false,
onChange,
onKeyDown,
onBlur,
onCellCountChange,
inputRefs,
resizable = false
resizable = false,
fontSize = "12px",
fontColor = "black",
hint = ""
}) {
const [cellCount, setCellCount] = useState(count);
@@ -79,6 +93,7 @@ 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
@@ -87,7 +102,7 @@ export default function CellsWidget({
>
{resizable && (
<div
className="cell-size-handle absolute left-1/2 -translate-x-1/2 -top-3 cursor-ew-resize touch-none"
className="cell-size-handle absolute left-1/2 -translate-x-1/2 -bottom-4 rotate-180 cursor-ew-resize touch-none"
onPointerDown={onTopHandlePointerDown}
>
<svg
@@ -108,14 +123,19 @@ 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,17 +111,22 @@ 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 its a dropdown and its read-only without a value (nor marked optional), stop here.
if (isDropdownOrRadio && readOnlyWithoutValue) {
alert(
props?.type === "dropdown"
? t("readonly-dropdown-error")
: t("readonly-radiobtn-error")
);
alert(t("readonly-error", { widgetName: props?.type }));
return;
} else if (
isCheckbox &&
isReadOnly &&
minCount > 0 &&
defaultCheckbox?.length === 0
) {
alert(t("readonly-error", { widgetName: props?.type }));
return;
}
@@ -267,7 +272,9 @@ 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>
@@ -334,7 +341,10 @@ 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>
@@ -349,7 +359,10 @@ 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>
@@ -1,5 +1,10 @@
import React from "react";
import { Quill } from "react-quill-new";
// Use a third-party plugin so editors can toggle raw HTML view.
import htmlEditButton from "quill-html-edit-button";
// Register the module which adds a "<>" button for HTML editing.
Quill.register("modules/htmlEditButton", htmlEditButton);
// Custom Undo button icon component for Quill editor. You can import it directly
// from 'quill/assets/icons/undo.svg' but I found that a number of loaders do not
@@ -56,7 +61,9 @@ export const module1 = {
container: "#toolbar1",
handlers: { undo: undoChange, redo: redoChange }
},
history: { delay: 500, maxStack: 100, userOnly: true }
history: { delay: 500, maxStack: 100, userOnly: true },
// Enable the "<>" button registered above
htmlEditButton: {}
};
// Modules object for setting up the Quill editor
@@ -65,7 +72,9 @@ export const module2 = {
container: "#toolbar2",
handlers: { undo: undoChange, redo: redoChange }
},
history: { delay: 500, maxStack: 100, userOnly: true }
history: { delay: 500, maxStack: 100, userOnly: true },
// Enable the "<>" button registered above
htmlEditButton: {}
};
// Formats objects for setting up the Quill editor
@@ -186,7 +186,7 @@ function EmailComponent({
<input
type="email"
value={emailValue}
className="p-[10px] pb-[20px] rounded-md w-full text-[15px] outline-none bg-transparent border-[1px] op-border-primary"
className="p-[10px] pb-[20px] text-base-content rounded-md w-full text-[15px] outline-none bg-transparent border-[1px] op-border-primary"
onChange={handleEmailValue}
onKeyDown={handleEnterPress}
placeholder={t("enter-email-plaholder")}
@@ -204,19 +204,6 @@ function EmailComponent({
{t("email-error-1")}
</p>
)}
{/* <button
className={`${
emailValue ? "cursor-pointer" : "cursor-default"
} op-btn op-btn-primary op-btn-sm m-2 shadow-md`}
onClick={() => emailValue && handleEnterPress("add")}
>
<i className="fa-light fa-plus" aria-hidden="true"></i>
</button>
<div className="bg-[#e3e2e1] mt-[10px] p-[5px] rounded">
<span className="font-bold">{t("report-heading.Note")}: </span>
<span className="text-[15px]">{t("email-error-2")}</span>
</div>
<hr className="w-full my-[15px] bg-base-content" /> */}
<div className="mt-2">
<button
type="button"
@@ -227,7 +214,7 @@ function EmailComponent({
</button>
<button
type="button"
className="op-btn op-btn-ghost ml-2"
className="op-btn op-btn-ghost text-base-content ml-2"
onClick={() => handleClose()}
>
{t("close")}
@@ -0,0 +1,94 @@
import React, { useEffect, useState, useRef } from "react";
import ModalUi from "../../primitives/ModalUi";
import { useTranslation } from "react-i18next";
export default function PageReorderModal({
isOpen,
handleClose,
totalPages = 0,
onSave
}) {
const { t } = useTranslation();
const [order, setOrder] = useState([]);
// Keeps track of the page order relative to the original PDF
const orderRef = useRef([]);
// Captures the order when the modal opens
const initialOrderRef = useRef([]);
// Initialize orderRef when total pages change (e.g. after upload)
useEffect(() => {
if (orderRef.current.length !== totalPages) {
orderRef.current = Array.from({ length: totalPages }, (_, i) => i + 1);
}
}, [totalPages]);
// When modal opens, display the last saved order and store it as initial
useEffect(() => {
if (isOpen) {
setOrder(orderRef.current);
initialOrderRef.current = [...orderRef.current];
}
}, [isOpen]);
const move = (index, dir) => {
const swapIndex = index + dir;
if (swapIndex < 0 || swapIndex >= order.length) return;
const newOrder = [...order];
[newOrder[index], newOrder[swapIndex]] = [newOrder[swapIndex], newOrder[index]];
setOrder(newOrder);
};
const handleSave = () => {
const saveOrder = order.map((num) =>
initialOrderRef.current.indexOf(num) + 1
);
// Persist the new display order for next time
orderRef.current = [...order];
onSave && onSave(saveOrder);
};
const isUnchanged =
order.length === initialOrderRef.current.length &&
order.every((n, i) => n === initialOrderRef.current[i]);
return (
<ModalUi isOpen={isOpen} handleClose={handleClose} title={t("reorder-pages")}>
<div className="p-[20px] flex flex-col gap-2 text-base-content">
{order.map((num, i) => (
<div key={num} className="flex items-center justify-between">
<span>
{t("page")} {num}
</span>
<div className="flex gap-1">
<button
className="op-btn op-btn-xs op-btn-ghost"
disabled={i === 0}
onClick={() => move(i, -1)}
>
<i className="fa-light fa-arrow-up"></i>
</button>
<button
className="op-btn op-btn-xs op-btn-ghost"
disabled={i === order.length - 1}
onClick={() => move(i, 1)}
>
<i className="fa-light fa-arrow-down"></i>
</button>
</div>
</div>
))}
<div className="h-[1px] bg-[#9f9f9f] w-full my-[15px]"></div>
<button onClick={handleSave} type="button" className="op-btn op-btn-primary" disabled={isUnchanged}>
{t("save")}
</button>
<button
onClick={handleClose}
type="button"
className="op-btn op-btn-ghost ml-1"
>
{t("close")}
</button>
</div>
</ModalUi>
);
}
+75 -2
View File
@@ -2,16 +2,21 @@ import React, { useRef, useState } from "react";
import PrevNext from "./PrevNext";
import {
base64ToArrayBuffer,
decryptPdf,
deletePdfPage,
flattenPdf,
getFileAsArrayBuffer,
handleDownloadCertificate,
handleDownloadPdf,
handleRemoveWidgets,
handleToPrint
handleToPrint,
reorderPdfPages
} from "../../constant/Utils";
import "../../styles/signature.css";
import { DropdownMenu } from "radix-ui";
import ModalUi from "../../primitives/ModalUi";
import Loader from "../../primitives/Loader";
import PageReorderModal from "./PageReorderModal";
import { useTranslation } from "react-i18next";
import { PDFDocument } from "pdf-lib";
import { maxFileSize } from "../../constant/const";
@@ -24,6 +29,7 @@ function Header(props) {
const isMobile = window.innerWidth < 767;
const [isDownloading, setIsDownloading] = useState("");
const [isDeletePage, setIsDeletePage] = useState(false);
const [isReorderModal, setIsReorderModal] = useState(false);
const mergePdfInputRef = useRef(null);
const enabledBackBtn = props?.disabledBackBtn === true ? false : true;
//function for show decline alert
@@ -80,7 +86,42 @@ function Header(props) {
return;
}
try {
const uploadedPdfBytes = await file.arrayBuffer();
let uploadedPdfBytes = await file.arrayBuffer();
try {
uploadedPdfBytes = await flattenPdf(uploadedPdfBytes);
} catch (err) {
if (err?.message?.includes("is encrypted")) {
try {
const pdfFile = await decryptPdf(file, "");
const pdfArrayBuffer = await getFileAsArrayBuffer(pdfFile);
uploadedPdfBytes = await flattenPdf(pdfArrayBuffer);
} catch (err) {
if (err?.response?.status === 401) {
const password = prompt(
`PDF "${file.name}" is password-protected. Enter password:`
);
if (password) {
try {
const pdfFile = await decryptPdf(file, password);
const pdfArrayBuffer = await getFileAsArrayBuffer(pdfFile);
uploadedPdfBytes = await flattenPdf(pdfArrayBuffer);
// Upload the file to Parse Server
} catch (err) {
console.error("Incorrect password or decryption failed", err);
alert("Incorrect password or decryption failed.");
}
} else {
alert("Please provided Password.");
}
} else {
console.log("Err ", err);
alert("error while uploading pdf.");
}
}
} else {
alert("error while uploading pdf.");
}
}
const uploadedPdfDoc = await PDFDocument.load(uploadedPdfBytes, {
ignoreEncryption: true
});
@@ -106,6 +147,21 @@ function Header(props) {
console.error("Error merging PDF:", error);
}
};
const handleReorderSave = async (order) => {
try {
const pdfupdatedData = await reorderPdfPages(props.pdfArrayBuffer, order);
if (pdfupdatedData) {
props.setPdfArrayBuffer(pdfupdatedData.arrayBuffer);
props.setPdfBase64Url(pdfupdatedData.base64);
props.setAllPages(pdfupdatedData.totalPages);
props.setPageNumber(1);
}
} catch (e) {
console.log("error in reorder pdf pages", e);
}
setIsReorderModal(false);
};
return (
<div className="flex py-[5px]">
{isMobile && props?.isShowHeader ? (
@@ -334,6 +390,17 @@ function Header(props) {
</span>
</div>
</DropdownMenu.Item>
<DropdownMenu.Item
className="DropdownMenuItem"
onClick={() => setIsReorderModal(true)}
>
<div className="flex flex-row">
<i className="fa-light fa-list-ol text-gray-500 2xl:text-[30px] mr-[3px]"></i>
<span className="font-[500]">
{t("reorder-pages")}
</span>
</div>
</DropdownMenu.Item>
<DropdownMenu.Item
className="DropdownMenuItem"
@@ -696,6 +763,12 @@ function Header(props) {
</button>
</div>
</ModalUi>
<PageReorderModal
isOpen={isReorderModal}
handleClose={() => setIsReorderModal(false)}
totalPages={props.allPages}
onSave={handleReorderSave}
/>
</div>
);
}
+72 -3
View File
@@ -1,18 +1,24 @@
import React, { useRef, useState } from "react";
import { useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import {
base64ToArrayBuffer,
decryptPdf,
deletePdfPage,
handleRemoveWidgets
flattenPdf,
getFileAsArrayBuffer,
handleRemoveWidgets,
reorderPdfPages
} from "../../constant/Utils";
import ModalUi from "../../primitives/ModalUi";
import { PDFDocument } from "pdf-lib";
import { maxFileSize } from "../../constant/const";
import PageReorderModal from "./PageReorderModal";
function PdfZoom(props) {
const { t } = useTranslation();
const mergePdfInputRef = useRef(null);
const [isDeletePage, setIsDeletePage] = useState(false);
const [isReorderModal, setIsReorderModal] = useState(false);
const handleDetelePage = async () => {
props.setIsUploadPdf && props.setIsUploadPdf(true);
try {
@@ -67,7 +73,42 @@ function PdfZoom(props) {
return;
}
try {
const uploadedPdfBytes = await file.arrayBuffer();
let uploadedPdfBytes = await file.arrayBuffer();
try {
uploadedPdfBytes = await flattenPdf(uploadedPdfBytes);
} catch (err) {
if (err?.message?.includes("is encrypted")) {
try {
const pdfFile = await decryptPdf(file, "");
const pdfArrayBuffer = await getFileAsArrayBuffer(pdfFile);
uploadedPdfBytes = await flattenPdf(pdfArrayBuffer);
} catch (err) {
if (err?.response?.status === 401) {
const password = prompt(
`PDF "${file.name}" is password-protected. Enter password:`
);
if (password) {
try {
const pdfFile = await decryptPdf(file, password);
const pdfArrayBuffer = await getFileAsArrayBuffer(pdfFile);
uploadedPdfBytes = await flattenPdf(pdfArrayBuffer);
// Upload the file to Parse Server
} catch (err) {
console.error("Incorrect password or decryption failed", err);
alert("Incorrect password or decryption failed.");
}
} else {
alert("Please provided Password.");
}
} else {
console.log("Err ", err);
alert("error while uploading pdf.");
}
}
} else {
alert("error while uploading pdf.");
}
}
const uploadedPdfDoc = await PDFDocument.load(uploadedPdfBytes, {
ignoreEncryption: true
});
@@ -94,6 +135,21 @@ function PdfZoom(props) {
}
};
const handleReorderSave = async (order) => {
try {
const pdfupdatedData = await reorderPdfPages(props.pdfArrayBuffer, order);
if (pdfupdatedData) {
props.setPdfArrayBuffer(pdfupdatedData.arrayBuffer);
props.setPdfBase64Url(pdfupdatedData.base64);
props.setAllPages(pdfupdatedData.totalPages);
props.setPageNumber(1);
}
} catch (e) {
console.log("error in reorder pdf pages", e);
}
setIsReorderModal(false);
};
return (
<>
<span className="hidden md:flex flex-col gap-1 text-center md:w-[5%] mt-[42px]">
@@ -120,6 +176,13 @@ function PdfZoom(props) {
>
<i className="fa-light fa-trash text-gray-500 2xl:text-[25px]"></i>
</span>
<span
className="bg-gray-50 px-[4px] 2xl:py-[10px] cursor-pointer"
onClick={() => setIsReorderModal(true)}
title={t("reorder-pages")}
>
<i className="fa-light fa-list-ol text-gray-500 2xl:text-[25px]"></i>
</span>
</>
)}
<span
@@ -185,6 +248,12 @@ function PdfZoom(props) {
</button>
</div>
</ModalUi>
<PageReorderModal
isOpen={isReorderModal}
handleClose={() => setIsReorderModal(false)}
totalPages={props.allPages}
onSave={handleReorderSave}
/>
</>
);
}
@@ -1,4 +1,4 @@
import { useState, useEffect, useRef } from "react";
import { useState, useEffect } from "react";
import BorderResize from "./BorderResize";
import { Rnd } from "react-rnd";
import {
@@ -81,8 +81,6 @@ 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({});
@@ -307,6 +305,13 @@ function Placeholder(props) {
} else if (props.pos.type === "checkbox") {
props?.setIsCheckbox(true);
}
// cells widget settings in sign yourself flow
else if (
props.pos.type === cellsWidget &&
(props.isSignYourself || props.isSelfSign)
) {
props.handleCellSettingModal && props.handleCellSettingModal();
}
//condition to handle setting icon for signyour-self flow for all type text widgets
else if (
[
@@ -417,41 +422,47 @@ function Placeholder(props) {
};
const setCellCount = (key, newCount) => {
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
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
);
return { ...ph, pos: newPos };
});
const newSignerPos = props.xyPosition.map((obj) =>
obj.Id === signerId
? { ...obj, placeHolder: updatedPlaceHolder }
: obj
}
} else {
const updatePos = prev[props.index].pos.map((p) =>
p.key === key
? { ...p, options: { ...p.options, cellCount: newCount } }
: p
);
return prev.map((obj, ind) =>
ind === props.index ? { ...obj, pos: updatePos } : obj
);
props.setXyPosition(newSignerPos);
}
} 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);
}
return prev;
});
};
const PlaceholderIcon = () => {
const isSettingForCells =
props?.isAlllowModify && !props?.assignedWidgetId.includes(props.pos.key)
? []
: [cellsWidget];
// 1- If props.isShowBorder is true, display border's icon for all widgets. OR
// 2- Use the combination of props?.isAlllowModify and !props?.assignedWidgetId.includes(props.pos.key) to determine when to show border's icon:
// 1- When isAlllowModify is true, show border's icon.
@@ -475,10 +486,15 @@ function Placeholder(props) {
"name",
"company",
"job title",
"email"
"email",
...isSettingForCells
].includes(props.pos.type) &&
(props.isSignYourself || props.isSelfSign) ? (
<i
onPointerDown={(e) => {
e.stopPropagation();
handleOnClickSettingIcon();
}}
onClick={(e) => {
e.stopPropagation();
handleOnClickSettingIcon();
@@ -488,7 +504,14 @@ function Placeholder(props) {
handleOnClickSettingIcon();
}}
className="fa-light fa-gear icon"
style={{ color: "#188ae2", right: "29px", top: "-19px" }}
style={{
color: "#188ae2",
right: "29px",
top: "-19px",
cursor: "pointer",
zIndex: 99,
pointerEvents: "auto"
}}
></i>
) : (
/* condition to add setting icon for placeholder & template flow for all widgets except signature and date */
@@ -498,6 +521,10 @@ function Placeholder(props) {
!props.isSignYourself &&
!props.isSelfSign)) && (
<i
onPointerDown={(e) => {
e.stopPropagation();
handleOnClickSettingIcon();
}}
onClick={(e) => {
e.stopPropagation();
handleOnClickSettingIcon();
@@ -510,7 +537,10 @@ function Placeholder(props) {
style={{
color: "#188ae2",
right: props?.pos?.type === textWidget ? "32px" : "51px",
top: "-19px"
top: "-19px",
cursor: "pointer",
zIndex: 99,
pointerEvents: "auto"
}}
></i>
)
@@ -548,6 +578,7 @@ 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);
@@ -580,7 +611,9 @@ function Placeholder(props) {
top: "-18px",
right: props.isPlaceholder ? "50px" : "30px",
color: "#188ae2",
fontSize: "14px"
fontSize: "14px",
cursor: "pointer",
pointerEvents: "auto"
}}
className="fa-light fa-gear icon"
></i>
@@ -707,7 +740,7 @@ function Placeholder(props) {
return "not-allowed";
}
} else {
return "all-scroll";
return "move";
}
};
@@ -727,21 +760,6 @@ 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";
@@ -792,13 +810,16 @@ function Placeholder(props) {
id={props.pos.key}
data-tut={props.pos.key === props.unSignedWidgetId ? "IsSigned" : ""}
key={props.pos.key}
cancel=".cell-size-handle"
cancel=".cell-size-handle, .icon"
lockAspectRatio={
!props.isFreeResize &&
(props.pos.Width
? props.pos.Width / props.pos.Height
: defaultWidthHeight(props.pos.type).width /
defaultWidthHeight(props.pos.type).height)
props?.isAlllowModify &&
!props?.assignedWidgetId.includes(props.pos.key)
? false
: !props.isFreeResize &&
(props.pos.Width
? props.pos.Width / props.pos.Height
: defaultWidthHeight(props.pos.type).width /
defaultWidthHeight(props.pos.type).height)
}
enableResizing={{
top: false,
@@ -925,23 +946,24 @@ function Placeholder(props) {
props.pos.key === props?.currWidgetsDetails?.key && <BorderResize />
)}
{/* 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).
{/* 1- Show a border if props.pos.key === props?.currWidgetsDetails?.key, indicating the current user's selected widget.
2- If props.isShowBorder is true, display border for all widgets.
3- Use the combination of props?.isAlllowModify and !props?.assignedWidgetId.includes(props.pos.key) to determine when to show border:
3.1- When isAlllowModify is true, show border.
3.2- Do not display border for widgets already assigned (props.assignedWidgetId.includes(props.pos.key) is true).
*/}
{props.pos.key === props?.currWidgetsDetails?.key &&
(props.isShowBorder ||
(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={`${
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`}
className="flex items-stretch justify-center"
style={{
outlineColor: themeColor,
left: xPos(props.pos, props.isSignYourself),
top: yPos(props.pos, props.isSignYourself),
width: "100%",
@@ -981,7 +1003,7 @@ function Placeholder(props) {
)}
<ModalUi isOpen={isDateModal} title={t("widget-info")} showClose={false}>
<div className="h-[100%] p-[20px]">
<div className="text-base-content h-[100%] p-[20px]">
<div className="flex flex-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>
@@ -217,16 +217,24 @@ 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={props.isPlaceholder}
resizable={props.isPlaceholder}
editable={isEditable}
resizable={isEditable}
fontSize={fontSize}
fontColor={fontColor}
hint={hint}
onCellCountChange={handleCellResize}
/>
);
@@ -31,7 +31,7 @@ const RecipientList = (props) => {
e.preventDefault();
};
//handle draggable element drop and also used in mobile view on up and down key to chnage sequence of recipient's list
//handle draggable element drop and also used in mobile view on up and down key to change sequence of recipient's list
const handleChangeSequence = (e, ind, isUp, isDown, obj) => {
e.preventDefault();
let draggedItemId;
@@ -3,7 +3,12 @@ import { useTranslation } from "react-i18next";
import { Document, Page } from "react-pdf";
import { useSelector } from "react-redux";
import { PDFDocument } from "pdf-lib";
import { base64ToArrayBuffer } from "../../constant/Utils";
import {
base64ToArrayBuffer,
decryptPdf,
flattenPdf,
getFileAsArrayBuffer
} from "../../constant/Utils";
import { maxFileSize } from "../../constant/const";
function RenderAllPdfPage(props) {
@@ -87,7 +92,42 @@ function RenderAllPdfPage(props) {
return;
}
try {
const uploadedPdfBytes = await file.arrayBuffer();
let uploadedPdfBytes = await file.arrayBuffer();
try {
uploadedPdfBytes = await flattenPdf(uploadedPdfBytes);
} catch (err) {
if (err?.message?.includes("is encrypted")) {
try {
const pdfFile = await decryptPdf(file, "");
const pdfArrayBuffer = await getFileAsArrayBuffer(pdfFile);
uploadedPdfBytes = await flattenPdf(pdfArrayBuffer);
} catch (err) {
if (err?.response?.status === 401) {
const password = prompt(
`PDF "${file.name}" is password-protected. Enter password:`
);
if (password) {
try {
const pdfFile = await decryptPdf(file, password);
const pdfArrayBuffer = await getFileAsArrayBuffer(pdfFile);
uploadedPdfBytes = await flattenPdf(pdfArrayBuffer);
// Upload the file to Parse Server
} catch (err) {
console.error("Incorrect password or decryption failed", err);
alert("Incorrect password or decryption failed.");
}
} else {
alert("Please provided Password.");
}
} else {
console.log("Err ", err);
alert("error while uploading pdf.");
}
}
} else {
alert("error while uploading pdf.");
}
}
const uploadedPdfDoc = await PDFDocument.load(uploadedPdfBytes, {
ignoreEncryption: true
});
@@ -136,8 +136,9 @@ function RenderPdf(props) {
handleDeleteSign={props.handleDeleteSign}
setIsPageCopy={props.setIsPageCopy}
handleTextSettingModal={props.handleTextSettingModal}
handleCellSettingModal={props.handleCellSettingModal}
setIsCheckbox={props.setIsCheckbox}
isFreeResize={false}
isFreeResize={props.isSelfSign ? true : false}
isOpenSignPad={true}
assignedWidgetId={props.assignedWidgetId}
isApplyAll={true}
@@ -270,6 +271,9 @@ function RenderPdf(props) {
handleTextSettingModal={
props.handleTextSettingModal
}
handleCellSettingModal={
props.handleCellSettingModal
}
scale={props.scale}
containerWH={props.containerWH}
pdfOriginalWH={props.pdfOriginalWH}
@@ -330,6 +334,9 @@ function RenderPdf(props) {
handleTextSettingModal={
props.handleTextSettingModal
}
handleCellSettingModal={
props.handleCellSettingModal
}
scale={props.scale}
pdfOriginalWH={props.pdfOriginalWH}
pageNumber={props.pageNumber}
@@ -339,7 +346,7 @@ function RenderPdf(props) {
setFontColor={props.setFontColor}
isResize={props.isResize}
setIsResize={props.setIsResize}
isFreeResize={false}
isFreeResize={true}
isOpenSignPad={true}
calculateFontsize={calculateFontsize}
currWidgetsDetails={
@@ -24,14 +24,14 @@ function SelectLanguage(props) {
<div
className={`${
!props.isProfile && " mt-[9px] pb-2 md:pb-0 "
} flex justify-center items-center `}
} flex justify-center items-center text-base-content`}
>
<select
value={lang}
onChange={handleChangeLang}
className={`${
!props.isProfile ? " md:w-[15%] w-[50%]" : "w-[180px]"
} op-select op-select-bordered bg-white op-select-sm `}
} op-select op-select-bordered op-select-sm `}
>
<option disabled>select</option>
{languages.map((item) => {
@@ -13,7 +13,7 @@ function TextFontSetting(props) {
title={t("text-field")}
handleClose={() => props.setIsTextSetting(false)}
>
<div className="h-full p-[20px]">
<div className="h-full p-[20px] text-base-content">
<div className="flex flex-col md:flex-row md:items-center gap-3">
{/* Font Size Selector */}
<div className="flex items-center gap-2">
@@ -96,9 +96,8 @@ const WidgetNameModal = (props) => {
if (isTextInput) {
const readOnlyWithoutValue =
isReadOnly && !defaultValue && status !== "optional";
if (readOnlyWithoutValue) {
alert(t("readonly-textinput-error"));
alert(t("readonly-error", { widgetName: props.defaultdata?.type }));
return;
}
}
@@ -124,6 +123,23 @@ const WidgetNameModal = (props) => {
}
};
const handleChangeValidateInput = (e) => {
if (e) {
if (e.target.value === "ssn") {
setFormdata({
...formdata,
[e.target.name]: e.target.value,
hint: "xxx-xx-xxxx",
cellCount: 11
});
} else {
setFormdata({ ...formdata, [e.target.name]: e.target.value });
}
} else {
setFormdata({ ...formdata, textvalidate: "" });
}
};
const handledefaultChange = (e) => {
if (formdata.textvalidate) {
const regexObject = RegexParser(handleValidation(formdata.textvalidate));
@@ -146,9 +162,10 @@ const WidgetNameModal = (props) => {
case "number":
return "/^\\d+$/";
case "text":
return "/^[a-zA-Zs]+$/";
//allow space in text regex
return "/^[a-zA-Z ]+$/";
case "ssn":
return "^\\d{3}-\\d{2}-\\d{4}$";
return "/^(?!000|666|9\\d{2})\\d{3}-(?!00)\\d{2}-(?!0000)\\d{4}$/";
default:
return type;
}
@@ -299,7 +316,10 @@ 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>
@@ -46,9 +46,9 @@ import RegexParser from "regex-parser";
//function to get default format
const getDefaultFormat = (dateFormat) => dateFormat || "MM/dd/yyyy";
//function to convert formated date to new Date() format
//function to convert formatted date to new Date() format
const getDefaultDate = (dateStr, format) => {
//get valid date format for moment to convert formated date to new Date() format
//get valid date format for moment to convert formatted date to new Date() format
const formats = changeDateToMomentFormat(format);
const parsedDate = moment(dateStr, formats);
let date;
@@ -87,7 +87,9 @@ function WidgetsValueModal(props) {
isSave,
setUniqueId,
tempSignerId,
signatureTypes
signatureTypes,
setCellCount,
allowCellResize = true
} = props;
const [penColor, setPenColor] = useState("blue");
const [isOptional, setIsOptional] = useState(true);
@@ -133,6 +135,15 @@ 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;
@@ -141,7 +152,7 @@ function WidgetsValueModal(props) {
currWidgetsDetails?.options?.defaultValue ||
"";
setCellsValue(Array.from({ length: count }, (_, i) => val[i] || ""));
}, [currWidgetsDetails?.key]);
}, [currWidgetsDetails?.key, currWidgetsDetails?.options?.cellCount]);
const [selectedCheckbox, setSelectedCheckbox] = useState(
currWidgetsDetails.type === "checkbox" &&
(currWidgetsDetails?.options?.response ||
@@ -158,7 +169,7 @@ function WidgetsValueModal(props) {
);
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";
"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]";
useEffect(() => {
if (
@@ -186,7 +197,7 @@ function WidgetsValueModal(props) {
setRemoveBgEnabled(false);
}, [currWidgetsDetails?.key]);
//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
//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
useEffect(() => {
if (currWidgetsDetails?.type === "date") {
const isDateChange = true;
@@ -760,7 +771,7 @@ function WidgetsValueModal(props) {
const PenColorComponent = (props) => {
return (
<div className="flex flex-row items-center m-[5px] gap-3">
<span>Options</span>
<span className="text-base-content">Options</span>
{allColor.map((data, key) => {
return (
<i
@@ -956,6 +967,7 @@ function WidgetsValueModal(props) {
setStartDate(date);
};
const handleOnchangeTextBox = (e) => {
// hide any prior validation error while typing
setIsShowValidation(false);
setWidgetValue(e.target.value);
onChangeInput(
@@ -967,13 +979,15 @@ function WidgetsValueModal(props) {
uniqueId
);
};
const handleCellsInput = (e, idx) => {
const val = e.target.value.slice(0, 1);
const updated = [...cellsValue];
updated[idx] = val;
const updateCells = (updated) => {
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,
@@ -982,49 +996,37 @@ function WidgetsValueModal(props) {
setXyPosition,
uniqueId
);
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 handleCellsInput = (e, idx) => {
setIsShowValidation(false);
const val = e.target.value.slice(0, 1);
const updated = [...cellsValue];
updated[idx] = val;
updateCells(updated);
};
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();
}
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
);
};
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);
}
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) => {
@@ -1044,7 +1046,7 @@ function WidgetsValueModal(props) {
<>
{currWidgetsDetails?.type !== "stamp" &&
currWidgetsDetails?.type !== "image" && (
<div className="text-base-content bg-gray-100 border-[1px] border-gray-200 rounded-[4px] tabWidth">
<div className="text-base-content rounded-[4px] tabWidth">
<div className="ml-3 flex justify-start gap-4 text-[11px] md:text-base my-[3px]">
<>
{currWidgetsDetails?.type !== "initials" &&
@@ -1341,7 +1343,7 @@ 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">
<div className="flex flex-row ml-1 mt-2 gap-x-3 text-base-content">
{/* Standalone autoSignAll for "Type" tab if conditions met */}
{setIsAutoSign && uniqueId && (
<div className="flex justify-start my-1">
@@ -1377,7 +1379,7 @@ 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">
<div className="flex flex-row ml-1 mt-1 gap-x-3 text-base-content">
{/* Standalone autoSignAll for "Draw" tab if conditions met */}
{setIsAutoSign && uniqueId && (
<div className="flex justify-start my-1">
@@ -1417,7 +1419,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=" select-none-cls">
<div key={ind} className="text-base-content select-none-cls">
<label
htmlFor={`checkbox-${currWidgetsDetails?.key + ind}`}
className="text-xs flex items-center gap-1"
@@ -1426,7 +1428,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) => {
@@ -1470,14 +1472,18 @@ function WidgetsValueModal(props) {
case cellsWidget:
return (
<CellsWidget
isEnabled={true}
count={cellsValue.length}
height="100%"
value={cellsValue.join("")}
editable={true}
resizable={false}
resizable={allowCellResize}
onChange={handleCellsInput}
onKeyDown={handleCellsKeyDown}
onBlur={handleCellsBlur}
onCellCountChange={allowCellResize ? handleCellResize : undefined}
inputRefs={cellRefs}
hint={hint}
/>
);
case "dropdown":
@@ -1540,7 +1546,7 @@ function WidgetsValueModal(props) {
case "date":
return (
<div
className={`border-[1px] border-gray-300 rounded-[2px] p-1 px-3`}
className={`border-[1px] text-base-content data-[theme=opensigndark]:border-base-content data-[theme=opensigncss]:border-gray-300 rounded-[2px] p-1 px-3`}
>
<DatePicker
renderCustomHeader={({ date, changeYear, changeMonth }) => (
@@ -1614,7 +1620,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="select-none-cls">
<div key={ind} className="text-base-content select-none-cls">
<label
htmlFor={`radio-${currWidgetsDetails?.key + ind}`}
className="cursor-pointer flex items-center text-sm gap-1"
@@ -1744,7 +1750,7 @@ function WidgetsValueModal(props) {
}
}
};
//funtion is used when user enter value any textbox then check validation
//function is used when user enter value in any textbox then check validation
const handleInputBlur = () => {
const validateType = currWidgetsDetails?.options?.validation?.type;
let regexValidation;
@@ -1757,6 +1763,10 @@ 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;
@@ -1768,7 +1778,7 @@ function WidgetsValueModal(props) {
break;
}
};
//funtion too uuse on click on next/finish button then update modal ui according to cuurent widgets
//function too use on click on next/finish button then update modal UI according to current widgets
const handleClickOnNext = (isFinishDoc) => {
if (
["signature", "stamp", "image", "initials"].includes(
@@ -1867,13 +1877,18 @@ 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 excecute on click finish button functionality
//function is used to execute the finish button functionality
const handleFinish = () => {
props?.finishDocument();
dispatch(setIsShowModal({}));
@@ -1884,7 +1899,7 @@ function WidgetsValueModal(props) {
handleFinishButton();
}
}, [isSave]);
//'handleFinishButton' function is used to show finish button click on any widge if all required widgtes have response
//'handleFinishButton' function is used to show finish button click on any widget if all required widgets have response
const handleFinishButton = () => {
const widgetsPosition = xyPosition?.find((data) => data.Id === uniqueId);
//using 'flatMap' create all nested array in one level
@@ -1931,7 +1946,7 @@ function WidgetsValueModal(props) {
<>
<ModalUi
isOpen={true}
handleClose={() => !isShowValidation && handleclose()}
handleClose={() => handleclose()}
position="bottom"
>
<div className="h-[100%] p-[18px]">
@@ -1963,7 +1978,7 @@ function WidgetsValueModal(props) {
<>
<div>
<div className="relative inline-block">
<span className="text-base">
<span className="text-base text-base-content">
{currWidgetsDetails?.options?.name || widgetTypeTranslation}
</span>
{!isOptional && (
@@ -1989,7 +2004,7 @@ function WidgetsValueModal(props) {
) ? (
<button
type="button"
className="op-btn op-btn-ghost op-btn-sm mr-1"
className="op-btn op-btn-ghost op-btn-sm text-base-content 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-black text-sm">
<div className="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 dcument signers list)
//compareArrays is a function where compare between two array (total signersList and document signers list)
//and filter signers from total signer's list which already present in document's signers list
// const compareArrays = (res, signerObj) => {
// return res.filter(
@@ -122,7 +122,7 @@ const SignersInput = (props) => {
);
if (contactRes) {
const res = JSON.parse(JSON.stringify(contactRes));
//compareArrays is a function where compare between two array (total signersList and dcument signers list)
//compareArrays is a function where compare between two array (total signersList and document signers list)
//and filter signers from total signer's list which already present in document's signers list
const compareArrays = (res, signerObj) => {
return res.filter(
+222 -1
View File
@@ -67,6 +67,17 @@ 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");
@@ -87,6 +98,27 @@ 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 didnt 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",
@@ -401,7 +433,7 @@ export const addWidgetOptions = (type, signer, widgetValue) => {
defaultValue: widgetValue ? widgetValue : ""
};
case "dropdown":
return { ...status, name: "Dropdown" };
return { ...status, name: "Choose one" };
case radioButtonWidget:
return {
...status,
@@ -3032,6 +3064,23 @@ 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 =
@@ -3285,3 +3334,175 @@ 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 files 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);
});
}
+5
View File
@@ -3,6 +3,11 @@ export const templateCls = "contracts_Template";
export const documentCls = "contracts_Document";
export const themeColor = "#47a3ad";
export const iconColor = "#686968";
// Dynamic icon color function for better dark mode visibility
export const getThemeIconColor = () => {
const theme = document.documentElement.getAttribute("data-theme");
return theme === "opensigndark" ? "#CCCCCC" : "#686968";
};
export const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
export const maxFileSize = 10; // 10MB
export const maxTitleLength = 250; // 250 characters
+38
View File
@@ -0,0 +1,38 @@
import { useState, useEffect } from 'react';
type Size = { width: number; height: number; };
/**
* useSize
* @param ref React ref object pointing to a DOM element
* @returns current size of that element: { width, height }
*/
export function useElSize(ref: React.RefObject<HTMLElement>): Size {
const [size, setSize] = useState<Size>({ width: 0, height: 0 });
useEffect(() => {
const el = ref.current;
if (!el) return;
// Initialize size
setSize({
width: el.offsetWidth,
height: el.offsetHeight,
});
// Watch for resize
const observer = new ResizeObserver(entries => {
for (let entry of entries) {
const { width, height } = entry.contentRect;
setSize({ width, height });
}
});
observer.observe(el);
return () => {
observer.disconnect();
};
}, [ref]);
return size;
}
+1 -2
View File
@@ -1,6 +1,6 @@
import { useEffect } from "react";
/**`useScript` hook is generated scripte for google sign in button */
/**`useScript` hook is generated script for google sign in button */
export const useScript = (url, onload) => {
useEffect(() => {
const script = document.createElement("script");
@@ -15,4 +15,3 @@ export const useScript = (url, onload) => {
};
}, [url, onload]);
};
+160
View File
@@ -98,4 +98,164 @@ body {
*::-webkit-scrollbar-thumb {
background-color: gray;
border-radius: 10px;
}
/* Note: Dark mode styling is now handled via Tailwind utilities in tailwind.config.js */
/* You can use classes like: icon-improved, icon-muted, icon-disabled, op-btn-vscode-disabled */
/* React-tour and ReactTooltip dark mode styling */
[data-theme="opensigndark"] {
/* React-tour modal styling */
.reactour__helper {
background-color: #1F2937 !important;
color: #E5E7EB !important;
border: 1px solid #374151 !important;
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.5) !important;
}
.reactour__close {
color: #E5E7EB !important;
background-color: #374151 !important;
border: 1px solid #4B5563 !important;
}
.reactour__close:hover {
background-color: #4B5563 !important;
color: #FFFFFF !important;
}
/* React-tour navigation buttons */
.reactour__controls {
background-color: #1F2937 !important;
border-top: 1px solid #374151 !important;
}
.reactour__controls button {
background-color: #374151 !important;
color: #E5E7EB !important;
border: 1px solid #4B5563 !important;
}
.reactour__controls button:hover {
background-color: #4B5563 !important;
color: #FFFFFF !important;
}
.reactour__controls button:disabled {
background-color: #3C3C3C !important;
color: #858585 !important;
border-color: #565656 !important;
cursor: not-allowed !important;
}
/* ReactTooltip styling */
.react-tooltip {
background-color: #1F2937 !important;
color: #E5E7EB !important;
border: 1px solid #374151 !important;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4) !important;
}
.react-tooltip.type-dark {
background-color: #1F2937 !important;
color: #E5E7EB !important;
}
.react-tooltip.place-top:after,
.react-tooltip.place-bottom:after,
.react-tooltip.place-left:after,
.react-tooltip.place-right:after {
border-color: #1F2937 !important;
}
/* Tour content improvements */
.reactour__badge {
background-color: #007ACC !important;
color: #FFFFFF !important;
}
.reactour__helper h1,
.reactour__helper h2,
.reactour__helper h3,
.reactour__helper h4,
.reactour__helper h5,
.reactour__helper h6 {
color: #FFFFFF !important;
}
.reactour__helper p {
color: #E5E7EB !important;
}
/* VS Code-style tour buttons */
.reactour__controls .op-btn {
background-color: #007ACC !important;
color: #FFFFFF !important;
border: 1px solid #007ACC !important;
}
.reactour__controls .op-btn:hover {
background-color: #0086D1 !important;
border-color: #0086D1 !important;
}
.reactour__controls .op-btn-secondary {
background-color: #374151 !important;
color: #E5E7EB !important;
border: 1px solid #4B5563 !important;
}
.reactour__controls .op-btn-secondary:hover {
background-color: #4B5563 !important;
color: #FFFFFF !important;
}
/* React-datepicker dark mode styling */
.react-datepicker {
background-color: #1F2937 !important;
border: 1px solid #374151 !important;
color: #E5E7EB !important;
}
.react-datepicker__header {
background-color: #374151 !important;
border-bottom: 1px solid #4B5563 !important;
color: #E5E7EB !important;
}
.react-datepicker__current-month,
.react-datepicker__day-name {
color: #E5E7EB !important;
}
.react-datepicker__day {
color: #E5E7EB !important;
}
.react-datepicker__day:hover {
background-color: #4B5563 !important;
color: #FFFFFF !important;
}
.react-datepicker__day--selected {
background-color: #007ACC !important;
color: #FFFFFF !important;
}
.react-datepicker__day--keyboard-selected {
background-color: #374151 !important;
color: #E5E7EB !important;
}
.react-datepicker__day--outside-month {
color: #6B7280 !important;
}
.react-datepicker__navigation {
color: #E5E7EB !important;
}
.react-datepicker__navigation:hover {
background-color: #4B5563 !important;
}
}
+6
View File
@@ -1,6 +1,7 @@
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";
@@ -24,6 +25,11 @@ 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: [
{
+202 -232
View File
@@ -1,4 +1,4 @@
import React, { useEffect, useRef, useState } from "react";
import { useEffect, useRef, useState } from "react";
import { useNavigate, useParams } from "react-router";
import { formJson } from "../json/FormJson";
import Parse from "parse";
@@ -12,9 +12,12 @@ import {
flattenPdf,
generatePdfName,
generateTitleFromFilename,
getFileName,
getSecureUrl,
toDataUrl
toDataUrl,
decryptPdf,
base64ToFile,
getFileAsArrayBuffer,
removeTrailingSegment
} from "../constant/Utils";
import { PDFDocument } from "pdf-lib";
import axios from "axios";
@@ -68,6 +71,7 @@ const Forms = (props) => {
AllowModifications: false
});
const [fileupload, setFileUpload] = useState("");
const [selectedFiles, setSelectedFiles] = useState([]);
const [fileload, setfileload] = useState(false);
const [percentage, setpercentage] = useState(0);
const [isReset, setIsReset] = useState(false);
@@ -114,15 +118,6 @@ const Forms = (props) => {
}));
};
function getFileAsArrayBuffer(file) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = (e) => resolve(e.target.result);
reader.onerror = (e) => reject(e.target.error);
reader.readAsArrayBuffer(file);
});
}
// `removeFile` is used to reset progress, percentage and remove file if exists
const removeFile = (e) => {
setfileload(false);
@@ -134,219 +129,212 @@ const Forms = (props) => {
const handleFileInput = async (e) => {
setpercentage(0);
try {
let files = e.target.files;
setFormData((prev) => ({ ...prev, file: e.target.files[0] }));
if (typeof files[0] !== "undefined") {
const mb = Math.round(files[0].size / Math.pow(1024, 2));
if (mb > maxFileSize) {
alert(`${t("file-alert-1")} ${maxFileSize} MB`);
setFileUpload("");
removeFile(e);
return;
} else {
if (files?.[0]?.type === "application/pdf") {
const size = files?.[0]?.size;
const name = generatePdfName(16);
const pdfName = `${name?.split(".")[0]}.pdf`;
setfileload(true);
try {
const res = await getFileAsArrayBuffer(files[0]);
const flatPdf = await flattenPdf(res);
const parseFile = new Parse.File(
pdfName,
[...flatPdf],
"application/pdf"
);
const files = Array.from(e.target.files);
const filesNameArr = files.map((f) => f.name);
setSelectedFiles(filesNameArr);
if (!files.length) {
alert(t("file-alert-2"));
return;
}
// setFormData((prev) => ({ ...prev, file: files[0] }));
const totalMb = Math.round(
files.reduce((sum, f) => sum + f.size, 0) / Math.pow(1024, 2)
);
if (totalMb > maxFileSize) {
alert(`${t("file-alert-1")} ${maxFileSize} MB`);
setFileUpload("");
setSelectedFiles([]);
removeFile(e);
return;
}
try {
const response = await parseFile.save({
progress: (progressValue, loaded, total, { type }) => {
if (type === "upload" && progressValue !== null) {
const percentCompleted = Math.round(
(loaded * 100) / total
);
setpercentage(percentCompleted);
}
}
});
// The response object will contain information about the uploaded file
// You can access the URL of the uploaded file using response.url()
if (response.url()) {
const fileRes = await getSecureUrl(response.url());
if (fileRes.url) {
setFileUpload(fileRes.url);
setfileload(false);
const tenantId = localStorage.getItem("TenantId");
const title = generateTitleFromFilename(files?.[0]?.name);
setFormData((obj) => ({ ...obj, Name: title }));
SaveFileSize(size, fileRes.url, tenantId);
return fileRes.url;
} else {
removeFile(e);
}
} else {
removeFile(e);
}
} catch (error) {
removeFile(e);
console.error("Error uploading file:", error);
}
const pdfBuffers = [];
for (const file of files) {
setFormData((prev) => ({ ...prev, file: file }));
if (file.type === "application/pdf") {
try {
const buffer = await getFileAsArrayBuffer(file);
const flat = await flattenPdf(buffer);
pdfBuffers.push(flat);
} catch (err) {
if (err?.message?.includes("is encrypted")) {
try {
setIsDecrypting(true);
const pdfFile = await decryptPdf(file, "");
setIsDecrypting(false);
setfileload(true);
const res = await getFileAsArrayBuffer(pdfFile);
const flatPdf = await flattenPdf(res);
// Upload the file to Parse Server
pdfBuffers.push(flatPdf);
} catch (err) {
if (err?.message?.includes("is encrypted")) {
try {
setIsDecrypting(true);
const size = files?.[0].size;
const name = generatePdfName(16);
const url = "https://ai.nxglabs.in/decryptpdf"; //
let formData = new FormData();
formData.append("file", files[0]);
formData.append("password", "");
const config = {
headers: { "content-type": "multipart/form-data" },
responseType: "blob"
};
const response = await axios.post(url, formData, config);
const pdfBlob = new Blob([response.data], {
type: "application/pdf"
});
const pdfFile = new File([pdfBlob], name, {
type: "application/pdf"
});
setIsDecrypting(false);
setfileload(true);
removeFile(e);
if (err?.response?.status === 401) {
// setIsPassword(true);
const password = prompt(
`PDF "${file.name}" is password-protected. Enter password:`
);
if (password) {
try {
const pdfFile = await decryptPdf(file, password);
setIsDecrypting(false);
setfileload(true);
const res = await getFileAsArrayBuffer(pdfFile);
const flatPdf = await flattenPdf(res);
// Upload the file to Parse Server
const parseFile = new Parse.File(
name,
[...flatPdf],
"application/pdf"
pdfBuffers.push(flatPdf);
} catch (err) {
console.error(
"Incorrect password or decryption failed",
err
);
setSelectedFiles(
filesNameArr.filter((f) => f !== file.name)
);
await parseFile.save({
progress: (progressValue, loaded, total, { type }) => {
if (type === "upload" && progressValue !== null) {
const percentCompleted = Math.round(
(loaded * 100) / total
);
setpercentage(percentCompleted);
}
}
});
// Retrieve the URL of the uploaded file
if (parseFile.url()) {
const fileRes = await getSecureUrl(parseFile.url());
if (fileRes.url) {
setFileUpload(fileRes.url);
removeFile();
const title = generateTitleFromFilename(
files?.[0]?.name
);
setFormData((obj) => ({ ...obj, Name: title }));
const tenantId = localStorage.getItem("TenantId");
SaveFileSize(size, fileRes.url, tenantId);
return fileRes.url;
} else {
removeFile(e);
}
} else {
removeFile(e);
}
} catch (err) {
removeFile();
if (err?.response?.status === 401) {
setIsPassword(true);
} else {
console.log("Error uploading file: ", err?.response);
setIsDecrypting(false);
e.target.value = "";
}
}
} else {
console.log("err ", err);
setFileUpload("");
removeFile(e);
}
}
} else {
const isImage = files?.[0]?.type.includes("image/");
if (isImage) {
const image = await toDataUrl(files[0]);
const pdfDoc = await PDFDocument.create();
let embedImg;
if (files?.[0]?.type === "image/png") {
embedImg = await pdfDoc.embedPng(image);
} else {
embedImg = await pdfDoc.embedJpg(image);
}
// Get image dimensions
const imageWidth = embedImg.width;
const imageHeight = embedImg.height;
const page = pdfDoc.addPage([imageWidth, imageHeight]);
page.drawImage(embedImg, {
x: 0,
y: 0,
width: imageWidth,
height: imageHeight
});
const size = files?.[0]?.size;
const name = generatePdfName(16);
const getFile = await pdfDoc.save({
useObjectStreams: false
});
setfileload(true);
const pdfName = `${name?.split(".")[0]}.pdf`;
const parseFile = new Parse.File(
pdfName,
[...getFile],
"application/pdf"
);
try {
const response = await parseFile.save({
progress: (progressValue, loaded, total, { type }) => {
if (type === "upload" && progressValue !== null) {
const percentCompleted = Math.round(
(loaded * 100) / total
);
setpercentage(percentCompleted);
}
}
});
// The response object will contain information about the uploaded file
// You can access the URL of the uploaded file using response.url()
if (response.url()) {
const fileRes = await getSecureUrl(response.url());
if (fileRes.url) {
setFileUpload(fileRes.url);
setfileload(false);
const tenantId = localStorage.getItem("TenantId");
const title = generateTitleFromFilename(files?.[0]?.name);
setFormData((obj) => ({ ...obj, Name: title }));
SaveFileSize(size, fileRes.url, tenantId);
return fileRes.url;
} else {
removeFile(e);
alert(`Incorrect password for file: ${file.name}`);
}
} else {
console.error("password not provided");
setSelectedFiles(
filesNameArr.filter((f) => f !== file.name)
);
setIsDecrypting(false);
setfileload(false);
removeFile(e);
}
} catch (error) {
} else {
console.log("Error uploading file: ", err?.response);
setIsDecrypting(false);
e.target.value = "";
removeFile(e);
console.error("Error uploading file:", error);
}
}
} else {
console.log("err ", err);
removeFile(e);
}
}
} else if (file.type.includes("image/")) {
const image = await toDataUrl(file);
const pdfDoc = await PDFDocument.create();
const embed =
file.type === "image/png"
? await pdfDoc.embedPng(image)
: await pdfDoc.embedJpg(image);
const page = pdfDoc.addPage([embed.width, embed.height]);
page.drawImage(embed, {
x: 0,
y: 0,
width: embed.width,
height: embed.height
});
const bytes = await pdfDoc.save({ useObjectStreams: false });
pdfBuffers.push(bytes);
} else if (
file.type ===
"application/vnd.openxmlformats-officedocument.wordprocessingml.document" ||
file.name.toLowerCase().endsWith(".docx")
) {
try {
const baseApi = localStorage.getItem("baseUrl") || "";
const url = removeTrailingSegment(baseApi) + "/docxtopdf";
let fd = new FormData();
fd.append("file", file);
setfileload(true);
setpercentage(0);
const config = {
headers: {
"content-type": "multipart/form-data",
sessiontoken: Parse.User.current().getSessionToken()
},
signal: abortController.signal,
onUploadProgress: (progressEvent) => {
if (progressEvent.total) {
const percentCompleted = Math.round(
(progressEvent.loaded * 100) / progressEvent.total
);
setpercentage(percentCompleted);
}
}
};
const res = await axios.post(url, fd, config);
if (res.data?.url) {
const pdfRes = await axios.get(res.data.url, {
responseType: "arraybuffer"
});
pdfBuffers.push(pdfRes.data);
}
setfileload(false);
} catch (err) {
setfileload(false);
removeFile(e);
console.log("err in docx to pdf ", err);
const error = isOpenSignDomain
? `${t("docx-error")} ${t("docx-error-contact")}`
: t("docx-error");
alert(error);
return;
}
}
} else {
}
if (!pdfBuffers.length) {
alert(t("file-alert-2"));
return false;
setSelectedFiles([]);
return;
}
setfileload(true);
const merged = await PDFDocument.create();
for (const bytes of pdfBuffers) {
const doc = await PDFDocument.load(bytes, { ignoreEncryption: true });
const pages = await merged.copyPages(doc, doc.getPageIndices());
pages.forEach((p) => merged.addPage(p));
}
const pdfBytes = await merged.save({ useObjectStreams: false });
const name = generatePdfName(16);
const pdfName = `${name}.pdf`;
let uploadedUrl = "";
const parseFile = new Parse.File(
pdfName,
[...pdfBytes],
"application/pdf"
);
const response = await parseFile.save({
progress: (progressValue, loaded, total, { type }) => {
if (type === "upload" && progressValue !== null) {
const percentCompleted = Math.round((loaded * 100) / total);
setpercentage(percentCompleted);
}
}
});
if (response.url()) {
const fileRes = await getSecureUrl(response.url());
if (fileRes.url) {
uploadedUrl = fileRes.url;
}
}
if (uploadedUrl) {
const tenantId = localStorage.getItem("TenantId");
SaveFileSize(pdfBytes.byteLength, uploadedUrl, tenantId);
setFileUpload(uploadedUrl);
setfileload(false);
const title = generateTitleFromFilename(filesNameArr?.[0]);
setFormData((obj) => ({ ...obj, Name: title }));
removeFile(e);
} else {
setfileload(false);
removeFile(e);
setSelectedFiles([]);
}
} catch (error) {
alert(error.message);
return false;
setSelectedFiles([]);
}
};
// `isValidURL` is used to check valid webhook url
@@ -446,9 +434,6 @@ 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([]);
@@ -478,6 +463,7 @@ const Forms = (props) => {
AllowModifications: false
});
setFileUpload("");
setSelectedFiles([]);
setpercentage(0);
navigate(`/${props?.redirectRoute}/${res.id}`);
}
@@ -559,6 +545,7 @@ const Forms = (props) => {
setFormData(obj);
removeFile();
setFileUpload("");
setSelectedFiles([]);
setTimeout(() => setIsReset(false), 50);
};
const handleCancel = () => {
@@ -571,24 +558,7 @@ const Forms = (props) => {
try {
const size = formData?.file?.size;
const name = generatePdfName(16);
const url = "https://ai.nxglabs.in/decryptpdf"; //
let Data = new FormData();
Data.append("file", formData?.file);
Data.append("password", formData.password);
const config = {
headers: {
"content-type": "multipart/form-data"
// sessiontoken: Parse.User.current().getSessionToken()
},
responseType: "blob"
};
const response = await axios.post(url, Data, config);
const pdfBlob = new Blob([response.data], {
type: "application/pdf"
});
const pdfFile = new File([pdfBlob], name, {
type: "application/pdf"
});
const pdfFile = await decryptPdf(formData?.file, formData?.password);
setIsDecrypting(false);
const res = await getFileAsArrayBuffer(pdfFile);
const flatPdf = await flattenPdf(res);
@@ -749,19 +719,20 @@ const Forms = (props) => {
)}
<div className="text-xs">
<label className="block">
{`${`${t("report-heading.File")} (${t("file-type")}`}${
")"
}`}
{`${`${t("report-heading.File")} (${t("file-type")}`}${", docx)"}`}
<span className="text-red-500 text-[13px]">*</span>
</label>
{fileupload.length > 0 ? (
<div className="flex gap-1 justify-center items-center">
<div className="flex justify-between items-center op-input op-input-bordered op-input-sm w-full h-full text-[13px]">
<div className="break-all cursor-default">
{t("file-selected")}: {getFileName(fileupload)}
{t("file-selected")}: {selectedFiles.join(", ")}
</div>
<div
onClick={() => setFileUpload("")}
onClick={() => {
setFileUpload("");
setSelectedFiles([]);
}}
className="cursor-pointer px-[10px] text-[20px] font-bold text-red-500"
>
<i className="fa-light fa-xmark"></i>
@@ -772,12 +743,11 @@ const Forms = (props) => {
<div className="flex gap-1 justify-center items-center">
<input
type="file"
multiple
className="op-file-input op-file-input-bordered op-file-input-sm focus:outline-none hover:border-base-content w-full text-xs"
onChange={(e) => handleFileInput(e)}
ref={inputFileRef}
accept={
"application/pdf,image/png,image/jpeg"
}
accept="application/pdf,application/vnd.openxmlformats-officedocument.wordprocessingml.document,image/png,image/jpeg"
onInvalid={(e) =>
e.target.setCustomValidity(t("input-required"))
}
+57 -56
View File
@@ -324,63 +324,65 @@ const ManageSign = () => {
hidden
/>
<div className="relative">
<div>
{image ? (
<div className="signatureCanvas relative border-[2px] border-[#888] rounded-box overflow-hidden">
<img
alt="signature"
src={image}
className="w-full h-full object-contain"
/>
</div>
) : (
<SignatureCanvas
ref={canvasRef}
penColor={penColor}
canvasProps={{
width: "456px",
height: "180px",
className:
"signatureCanvas border-[2px] border-[#888] rounded-box"
}}
// backgroundColor="rgb(255, 255, 255)"
onEnd={() =>
handleSignatureChange(canvasRef.current.toDataURL())
}
dotSize={1}
{isLoader && (
<div className="absolute bg-black bg-opacity-30 z-50 w-full h-full flex justify-center items-center">
<Loader />
</div>
)}
{image ? (
<div className="signatureCanvas relative border-[2px] border-[#888] rounded-box overflow-hidden">
<img
alt="signature"
src={image}
className="w-full h-full object-contain"
/>
)}
<div className="penContainerDefault flex flex-row justify-between">
<div>
{!image && (
<div className="flex flex-row gap-1.5 m-[5px]">
{allColor.map((data, key) => {
return (
<i
key={key}
onClick={() => setPenColor(allColor[key])}
className={`border-b-[2px] ${key === 0 && penColor === "blue" ? "border-blue-600" : key === 1 && penColor === "red" ? "border-red-500" : key === 2 && penColor === "black" ? "border-black" : "border-white"} text-[${data}] text-[16px] fa-light fa-pen-nib`}
></i>
);
})}
</div>
)}
</div>
) : (
<SignatureCanvas
ref={canvasRef}
penColor={penColor}
canvasProps={{
width: "456px",
height: "180px",
className:
"signatureCanvas border-[2px] border-[#888] rounded-box"
}}
onEnd={() =>
handleSignatureChange(canvasRef.current.toDataURL())
}
dotSize={1}
/>
)}
<div className="penContainerDefault flex flex-row justify-between">
<div>
{!image && (
<div className="flex flex-row gap-1.5 m-[5px]">
{allColor.map((data, key) => {
return (
<i
key={key}
onClick={() => setPenColor(allColor[key])}
className={`border-b-[2px] ${key === 0 && penColor === "blue" ? "border-blue-600" : key === 1 && penColor === "red" ? "border-red-500" : key === 2 && penColor === "black" ? "border-black" : "border-white"} text-[${data}] text-[16px] fa-light fa-pen-nib`}
></i>
);
})}
</div>
)}
</div>
<div className="flex flex-row gap-2 text-sm md:text-base mr-1">
<div
type="button"
className="op-link"
onClick={() => handleUploadBtn()}
>
{t("upload")}
</div>
<div className="flex flex-row gap-2 text-sm md:text-base mr-1">
<div
type="button"
className="op-link"
onClick={() => handleUploadBtn()}
>
{t("upload")}
</div>
<div
type="button"
className="op-link"
onClick={() => handleClear()}
>
{t("clear")}
</div>
<div
type="button"
className="op-link"
onClick={() => handleClear()}
>
{t("clear")}
</div>
</div>
</div>
@@ -420,7 +422,6 @@ const ManageSign = () => {
canvasProps={{
className: "intialSignature rounded-box"
}}
// backgroundColor="rgb(255, 255, 255)"
onEnd={() => handleInitialsChange()}
dotSize={1}
/>
+17 -9
View File
@@ -1,7 +1,7 @@
import React, { useEffect, useState, useRef } from "react";
import "../styles/opensigndrive.css";
import {
iconColor,
getThemeIconColor,
} from "../constant/const";
import {
getDrive
@@ -539,6 +539,12 @@ function Opensigndrive() {
}
}, 300);
};
const handleSearchPaste = (e) => {
setTimeout(() => {
handleSearchChange({ target: { value: e.target.value } });
}, 0);
};
// Cleanup on unmount
useEffect(() => {
return () => {
@@ -657,7 +663,8 @@ function Opensigndrive() {
type="search"
value={searchTerm}
onChange={handleSearchChange}
placeholder="Search documents"
placeholder={t("search-documents")}
onPaste={handleSearchPaste}
className="op-input op-input-bordered op-input-sm focus:outline-none hover:border-base-content w-64 text-xs"
/>
</div>
@@ -668,7 +675,7 @@ function Opensigndrive() {
onClick={() => setMobileSearchOpen((open) => !open)}
>
<i
style={{ color: `${iconColor}` }}
style={{ color: `${getThemeIconColor()}` }}
className="fa-solid fa-magnifying-glass"
></i>
</button>
@@ -681,7 +688,7 @@ function Opensigndrive() {
<i
className="fa-light fa-plus-square text-[24px]"
aria-hidden="true"
style={{ color: `${iconColor}` }}
style={{ color: `${getThemeIconColor()}` }}
></i>
</div>
<div
@@ -730,9 +737,9 @@ function Opensigndrive() {
<i
className="fa-light fa-sort-amount-asc mr-[5px] text-[19px]"
aria-hidden="true"
style={{ color: `${iconColor}` }}
style={{ color: `${getThemeIconColor()}` }}
></i>
<span style={{ fontSize: "15px", color: `${iconColor}` }}>
<span style={{ fontSize: "15px", color: `${getThemeIconColor()}` }}>
{selectedSort}
</span>
</div>
@@ -793,7 +800,7 @@ function Opensigndrive() {
>
<i
className={`${isList ? "fa-light fa-th-large" : "fa-light fa-list"} text-[20px]`}
style={{ color: `${iconColor}` }}
style={{ color: `${getThemeIconColor()}` }}
aria-hidden="true"
></i>
</div>
@@ -809,7 +816,7 @@ function Opensigndrive() {
<i
className="fa-light fa-ellipsis-vertical fa-lg"
aria-hidden="true"
style={{ color: `${iconColor}` }}
style={{ color: `${getThemeIconColor()}` }}
></i>
</div>
<div
@@ -854,7 +861,8 @@ function Opensigndrive() {
type="search"
value={searchTerm}
onChange={handleSearchChange}
placeholder="Search documents"
placeholder={t("search-documents")}
onPaste={handleSearchPaste}
className="op-input op-input-bordered op-input-sm focus:outline-none hover:border-base-content w-full text-xs"
/>
</div>
+28 -124
View File
@@ -23,7 +23,6 @@ import {
pdfNewWidthFun,
signPdfFun,
addDefaultSignatureImg,
radioButtonWidget,
replaceMailVaribles,
convertPdfArrayBuffer,
contractUsers,
@@ -48,6 +47,7 @@ import {
updateDateWidgetsRes,
widgetDataValue,
getOriginalWH,
handleCheckResponse,
} from "../constant/Utils";
import Header from "../components/pdf/PdfHeader";
import RenderPdf from "../components/pdf/RenderPdf";
@@ -159,9 +159,6 @@ 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),
@@ -280,6 +277,15 @@ 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;
@@ -637,111 +643,10 @@ function PdfRequestFiles(
(data) => data.signerObjId === signerObjectId
);
if (checkUser && checkUser.length > 0) {
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);
const status = handleCheckResponse(checkUser,setminRequiredCount)
if (status?.showAlert) {
setUnSignedWidgetId(status?.widgetKey);
setPageNumber(status?.tourPageNumber);
setWidgetsTour(true);
setIsUiLoading(false);
} else {
@@ -794,7 +699,6 @@ function PdfRequestFiles(
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;
@@ -1003,7 +907,6 @@ function PdfRequestFiles(
}
}
}
const handleSignPdf = async () => {
setIsUiLoading(true);
await embedWidgetsData();
@@ -1029,15 +932,15 @@ function PdfRequestFiles(
let filterSignerPos = [];
if (signerObjId) {
//get current signerObjId placeholder details
filterSignerPos = updateSignPos.filter(
filterSignerPos = updateSignPos?.filter(
(data) => data.Id === signerObjId
);
}
if (filterSignerPos.length > 0) {
const getPlaceHolder = filterSignerPos[0].placeHolder;
const getPlaceHolder = filterSignerPos[0]?.placeHolder;
//get position of current pagenumber
const getPageNumer = getPlaceHolder.filter(
const getPageNumer = getPlaceHolder?.filter(
(data) => data.pageNumber === pageNumber
);
if (getPageNumer.length > 0) {
@@ -1081,9 +984,9 @@ function PdfRequestFiles(
setIsTextSetting(value);
};
const handleSaveFontSize = () => {
const filterSignerPos = signerPos.filter((data) => data.Id === uniqueId);
const filterSignerPos = signerPos?.filter((data) => data.Id === uniqueId);
if (filterSignerPos) {
const placehoder = filterSignerPos[0].placeHolder;
const placehoder = filterSignerPos[0]?.placeHolder;
const getPageNumer = placehoder.filter(
(data) => data.pageNumber === pageNumber
);
@@ -1199,7 +1102,7 @@ function PdfRequestFiles(
const addDefaultSignature = () => {
const type = defaultSignAlert?.type;
//get current signers placeholder position data
const currentSignerPosition = signerPos.filter(
const currentSignerPosition = signerPos?.filter(
(data) => data.signerObjId === signerObjectId
);
const defaultSign = type === "signature" ? defaultSignImg : myInitial;
@@ -1590,11 +1493,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
);
}
@@ -1637,7 +1540,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(
@@ -1665,7 +1568,7 @@ function PdfRequestFiles(
});
setSignerPos(newUpdateSigner);
} else {
const getRemainPage = filterSignerPos[0].placeHolder.filter(
const getRemainPage = filterSignerPos[0]?.placeHolder?.filter(
(data) => data.pageNumber !== pageNumber
);
//condition to check placeholder length is greater than 1 do not need to remove whole placeholder
@@ -1678,11 +1581,11 @@ function PdfRequestFiles(
return obj;
});
let signerupdate = [];
signerupdate = signerPos.filter((data) => data.Id !== Id);
signerupdate = signerPos?.filter((data) => data.Id !== Id);
signerupdate.push(newUpdatePos[0]);
setSignerPos(signerupdate);
} else {
const updatedData = signerPos.map((item) => {
const updatedData = signerPos?.map((item) => {
if (item.Id === Id) {
// Create a copy of the item object and delete the placeHolder field
const updatedItem = { ...item };
@@ -1700,7 +1603,7 @@ function PdfRequestFiles(
//function to get first widget and page number to assign currect signer and tour message
const showFirstWidget = () => {
if (!requestSignTour) {
const getCurrentUserPlaceholder = signerPos.find(
const getCurrentUserPlaceholder = signerPos?.find(
(x) => x.Id === uniqueId
);
const placeholder = getCurrentUserPlaceholder.placeHolder;
@@ -2232,6 +2135,7 @@ function PdfRequestFiles(
setUniqueId={setUniqueId}
tempSignerId={tempSignerId}
signatureTypes={signatureType}
allowCellResize={pdfDetails[0]?.AllowModifications ?? false}
/>
)}
<DownloadPdfZip
+3 -9
View File
@@ -1608,13 +1608,7 @@ function PlaceHolderSign() {
parseInt(defaultdata?.cellCount || 5)
),
validation:
isSubscribe && inputype
? {
type: inputype,
pattern:
inputype === "regex" ? defaultdata.textvalidate : ""
}
: {},
{},
fontSize:
fontSize || currWidgetsDetails?.options?.fontSize || 12,
fontColor:
@@ -2184,7 +2178,7 @@ function PlaceHolderSign() {
navigate("/report/1MwEuxLEkF");
}}
>
<div className="h-[100%] p-[20px]">
<div className="h-[100%] p-[20px] text-base-content">
{mailStatus === "success" ? (
<div className="text-center mb-[10px]">
<LottieWithLoader />
@@ -2636,7 +2630,7 @@ function PlaceHolderSign() {
title={t("document-alert")}
showClose={false}
>
<div className="h-[100%] p-[20px]">
<div className="h-[100%] p-[20px] text-base-content">
<p>{isAlreadyPlace.message}</p>
<div className="h-[1px] w-full my-[15px] bg-[#9f9f9f]"></div>
<button
+1 -1
View File
@@ -301,7 +301,7 @@ const Preferences = () => {
JSON.parse(localStorage.getItem("Extand_Class"))?.[0];
if (extUser && extUser?.objectId) {
extUser.TenantId.RequestBody = updateRes?.RequestBody;
extUser.TenantId.RequestBody = updateRes?.RequestSubject;
extUser.TenantId.RequestSubject = updateRes?.RequestSubject;
const _extUser = JSON.parse(JSON.stringify(extUser));
localStorage.setItem("Extand_Class", JSON.stringify([_extUser]));
}
+17 -2
View File
@@ -29,6 +29,7 @@ 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
@@ -73,7 +74,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(
@@ -86,11 +87,19 @@ 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(() => {
@@ -100,7 +109,11 @@ 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) {
@@ -248,6 +261,8 @@ const Report = () => {
setMobileSearchOpen={setMobileSearchOpen}
searchTerm={searchTerm}
handleSearchChange={handleSearchChange}
handleSearchPaste={handleSearchPaste}
isSearchResult={isSearchResult}
isImport={isImport}
/>
) : (
+104 -6
View File
@@ -69,6 +69,8 @@ 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();
@@ -119,6 +121,10 @@ 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);
@@ -1040,16 +1046,89 @@ function SignYourSelf() {
};
const setCellCount = (key, newCount) => {
const getPageNumer = xyPosition.filter((data) => data.pageNumber === pageNumber);
setXyPosition((prev) => {
const getPageNumer = prev.filter((data) => data.pageNumber === pageNumber);
if (getPageNumer.length > 0) {
const updatePos = getPageNumer[0].pos.map((p) =>
p.key === key ? { ...p, options: { ...p.options, cellCount: newCount } } : p
);
return prev.map((obj, ind) => (ind === index ? { ...obj, pos: updatePos } : obj));
}
return prev;
});
};
const handleWidgetdefaultdata = (defaultdata) => {
const newFontSize =
defaultdata?.fontSize !== undefined ? defaultdata.fontSize : fontSize;
const newFontColor =
defaultdata?.fontColor !== undefined ? defaultdata.fontColor : fontColor;
const getPageNumer = xyPosition.filter(
(data) => data.pageNumber === pageNumber
);
if (getPageNumer.length > 0) {
const updatePos = getPageNumer[0].pos.map((p) =>
p.key === key ? { ...p, options: { ...p.options, cellCount: newCount } } : p
);
const updatePos = getPageNumer[0].pos.map((position) => {
if (position.key === currWidgetsDetails?.key) {
if (position.type === cellsWidget) {
const count = parseInt(
defaultdata?.cellCount ?? position.options?.cellCount ?? 5,
10
);
return {
...position,
options: {
...position.options,
name: defaultdata?.name || position.options?.name || "Cells",
cellCount: count,
defaultValue: (defaultdata?.defaultValue || "").slice(0, count),
fontSize: newFontSize || position.options?.fontSize || 12,
fontColor: newFontColor || position.options?.fontColor || "black"
}
};
} else {
return {
...position,
options: {
...position.options,
name: defaultdata?.name || position.options?.name,
fontSize: newFontSize || position.options?.fontSize || 12,
fontColor:
newFontColor || position.options?.fontColor || "black"
}
};
}
}
return position;
});
const updateXYposition = xyPosition.map((obj, ind) =>
ind === index ? { ...obj, pos: updatePos } : obj
);
setXyPosition(updateXYposition);
}
setFontSize();
setFontColor();
setCurrWidgetsDetails({});
setIsNameModal(false);
};
const handleNameModal = () => {
setIsNameModal(false);
setCurrWidgetsDetails({});
setIsCheckbox(false);
};
const handleCellsSettingSave = (data) => {
// ensure font and color are updated before applying widget changes
if (data?.fontSize !== undefined) setFontSize(data.fontSize);
if (data?.fontColor !== undefined) setFontColor(data.fontColor);
handleWidgetdefaultdata(data);
setIsCellsSetting(false);
};
const handleCellsSettingClose = () => {
setIsCellsSetting(false);
setCurrWidgetsDetails({});
};
const clickOnZoomIn = () => {
onClickZoomIn(scale, zoomPercent, setScale, setZoomPercent);
@@ -1206,9 +1285,8 @@ function SignYourSelf() {
title={t("document-signed")}
handleClose={() => setShowAlreadySignDoc({ status: false })}
>
<div className="p-[20px] h-full">
<div className="p-[20px] h-full text-base-content">
<p>{showAlreadySignDoc.mssg}</p>
<div className="h-[1px] w-full my-[15px] bg-[#9f9f9f]"></div>
<button
className="op-btn op-btn-ghost shadow-md"
@@ -1306,6 +1384,8 @@ function SignYourSelf() {
setIsPageCopy={setIsPageCopy}
setIsCheckbox={setIsCheckbox}
setCurrWidgetsDetails={setCurrWidgetsDetails}
handleNameModal={openNameModal}
handleCellSettingModal={openCellsSettingModal}
handleTextSettingModal={handleTextSettingModal}
setScale={setScale}
scale={scale}
@@ -1354,6 +1434,7 @@ function SignYourSelf() {
xyPosition={xyPosition} //placeholder details
pageNumber={pageNumber} //current page number
setXyPosition={setXyPosition} //placeholder details state
setCellCount={setCellCount}
setPageNumber={setPageNumber}
setCurrWidgetsDetails={setCurrWidgetsDetails}
currWidgetsDetails={currWidgetsDetails}
@@ -1362,6 +1443,23 @@ 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}
@@ -1721,7 +1721,7 @@ const TemplatePlaceholder = () => {
navigate("/report/6TeaPr321t");
}}
>
<div className="h-full p-[20px] mb-2">
<div className="h-full p-[20px] mb-2 text-base-content">
<p>{t("template-created-alert")}</p>
<div className="h-[1px] w-full my-[15px] bg-[#9f9f9f]"></div>
<div className="flex gap-1 flex-col md:flex-row">
+1 -1
View File
@@ -284,7 +284,7 @@ const UserList = () => {
handleClose={handleClose}
>
<div className="m-[20px]">
<div className="text-lg font-normal text-black">
<div className="text-lg font-normal text-base-content">
{t("are-you-sure")}{" "}
{item?.IsDisabled
? t("activate")
+1 -1
View File
@@ -286,7 +286,7 @@ function UserProfile() {
style={{ width: `${percentage}%` }}
></div>
</div>
<span className="text-black text-sm">{percentage}%</span>
<span className="text-base-contentk text-sm">{percentage}%</span>
</div>
)}
<div className="text-base font-semibold pt-4">
@@ -91,7 +91,7 @@ function DownloadPdfZip(props) {
title={t("download-files")}
handleClose={() => props.setIsDownloadModal(false)}
>
<div className="p-[20px] h-full">
<div className="p-[20px] h-full text-base-content">
{downloadType.map((data, ind) => {
return (
<label
@@ -39,9 +39,12 @@ import { useTranslation } from "react-i18next";
import DownloadPdfZip from "./DownloadPdfZip";
import * as XLSX from "xlsx";
import EditContactForm from "../components/EditContactForm";
import { useElSize } from "../hook/useElSize";
const ReportTable = (props) => {
const copyUrlRef = useRef(null);
const titleRef = useRef(null);
const titleElement = useElSize(titleRef);
const appName =
"OpenSign™";
const drivename = appName === "OpenSign™" ? "OpenSign™" : "";
@@ -93,6 +96,12 @@ const ReportTable = (props) => {
const startIndex = (currentPage - 1) * props.docPerPage;
const { isMoreDocs, setIsNextRecord } = props;
useEffect(() => {
if (props.isSearchResult) {
setCurrentPage(1);
}
}, [props.isSearchResult]);
const getPaginationRange = () => {
const totalPageNumbers = 7; // Adjust this value to show more/less page numbers
const pages = [];
@@ -999,7 +1008,7 @@ const ReportTable = (props) => {
? "op-border-primary op-text-primary"
: x.Activity === "VIEWED"
? "border-green-400 text-green-400"
: "border-black text-black"
: "border-base-content text-base-content"
} focus:outline-none border-2 w-[60px] h-[30px] text-[11px] rounded-full`}
>
{x?.Activity?.toUpperCase() || "-"}
@@ -1421,7 +1430,10 @@ const ReportTable = (props) => {
/>
</>
)}
<div className="flex flex-row items-center justify-between my-2 mx-3 text-[20px] md:text-[23px]">
<div
ref={titleRef}
className="flex flex-row items-center justify-between my-2 mx-3 text-[20px] md:text-[23px]"
>
<div className="font-light">
{t(`report-name.${props.ReportName}`)}{" "}
{props.report_help && (
@@ -1433,44 +1445,69 @@ const ReportTable = (props) => {
</span>
)}
</div>
<div className="flex flex-row justify-center items-center gap-3">
{props.isImport && (
<div className="cursor-pointer" onClick={() => handleImportBtn()}>
<i className="fa-light fa-upload op-text-secondary text-[23px] md:text-[30px]"></i>
<div className="flex flex-row justify-center items-center gap-3 mb-2">
{/* Search input for report bigger in width */}
{titleElement?.width > 500 && (
<div className="flex">
<input
type="search"
value={props.searchTerm}
onChange={props.handleSearchChange}
placeholder={
props.ReportName === "Contactbook"
? t("search-contacts")
: isTemplateReport
? t("search-templates")
: t("search-documents")
}
onPaste={props.handleSearchPaste}
className="op-input op-input-bordered op-input-sm focus:outline-none hover:border-base-content w-64 text-xs"
/>
</div>
)}
{/* contact import */}
{props.isImport && (
<div
className="cursor-pointer flex"
onClick={() => handleImportBtn()}
>
<i className="fa-light fa-upload op-text-secondary text-[23px] md:text-[25px]"></i>
</div>
)}
{/* add contact form */}
{props.form && (
<div
className="cursor-pointer"
className="cursor-pointer flex"
onClick={() => handleContactFormModal()}
>
<i className="fa-light fa-square-plus text-accent text-[30px] md:text-[35px]"></i>
<i className="fa-light fa-square-plus text-accent text-[30px] md:text-[32px]"></i>
</div>
)}
{/* create template form */}
{isTemplateReport && (
<i
<div
data-tut="reactourFirst"
className="cursor-pointer flex"
onClick={() => navigate("/form/template")}
className="cursor-pointer fa-light fa-square-plus text-accent text-[30px] md:text-[35px]"
></i>
>
<i className="cursor-pointer fa-light fa-square-plus text-accent text-[30px] md:text-[32px]"></i>
</div>
)}
{/* search icon/magnifer icon */}
{titleElement?.width < 500 && (
<button
className="flex justify-center items-center focus:outline-none rounded-md text-[18px]"
aria-label="Search"
onClick={() =>
props.setMobileSearchOpen(!props.mobileSearchOpen)
}
>
<i
style={{ color: `${iconColor}` }}
className="fa-solid fa-magnifying-glass"
></i>
</button>
)}
{/* 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")}
@@ -1558,13 +1595,15 @@ const ReportTable = (props) => {
</ModalUi>
</div>
</div>
{props.mobileSearchOpen && (
<div className="top-full left-0 w-full bg-white px-4 py-2 shadow-md md:hidden">
{/* Search input for report smalle in width */}
{titleElement?.width < 500 && props.mobileSearchOpen && (
<div className="top-full left-0 w-full bg-white px-3 pt-1 pb-3">
<input
type="search"
value={props.searchTerm}
onChange={props.handleSearchChange}
placeholder="Search documents"
placeholder={t("search-documents")}
onPaste={props.handleSearchPaste}
className="op-input op-input-bordered op-input-sm focus:outline-none hover:border-base-content w-full text-xs"
/>
</div>
@@ -1635,7 +1674,7 @@ const ReportTable = (props) => {
handleClose={handleClose}
>
<div className="m-[20px]">
<div className="text-lg font-normal text-black">
<div className="text-lg font-normal text-base-content">
{t("contact-delete-alert")}
</div>
<hr className="bg-[#ccc] mt-4 " />
@@ -2011,7 +2050,7 @@ const ReportTable = (props) => {
</div>
) : (
<div className="m-[20px]">
<div className="text-lg font-normal text-black">
<div className="text-lg font-normal text-base-content">
{t("save-as-template-?")}
</div>
<hr className="bg-[#ccc] mt-3" />
@@ -2049,7 +2088,7 @@ const ReportTable = (props) => {
</label>
<input
type="date"
className="rounded-full mb-2 bg-base-300 w-full px-4 py-2 text-black border-2 hover:border-spacing-2"
className="rounded-full mb-2 bg-base-300 w-full px-4 py-2 text-base-content border-2 hover:border-spacing-2"
defaultValue={
item?.ExpiryDate?.iso?.split("T")?.[0]
}
@@ -2159,7 +2198,7 @@ const ReportTable = (props) => {
handleClose={handleClose}
>
<div className="m-[20px]">
<div className="text-lg font-normal text-black">
<div className="text-lg font-normal text-base-content">
{t("delete-document-alert")}
</div>
<hr className="bg-[#ccc] mt-4" />
@@ -2216,7 +2255,7 @@ const ReportTable = (props) => {
{shareUrls.map((share, i) => (
<div
key={i}
className="text-sm font-normal text-black flex my-2 justify-between items-center"
className="text-sm font-normal text-base-content flex my-2 justify-between items-center"
>
<span className="w-[150px] mr-[5px] md:mr-0 md:w-[300px] whitespace-nowrap overflow-hidden text-ellipsis text-sm font-semibold">
{share.email}
@@ -2258,14 +2297,14 @@ const ReportTable = (props) => {
handleClose={handleClose}
>
<div className="m-[20px]">
<div className="text-sm md:text-lg font-normal text-black">
<div className="text-sm md:text-lg font-normal text-base-content">
{t("revoke-document-alert")}
</div>
<div className="mt-2">
<textarea
rows={3}
placeholder="Reason (optional)"
className="px-4 op-textarea op-textarea-bordered focus:outline-none hover:border-base-content w-full text-xs"
className="px-4 op-textarea op-textarea-bordered text-base-content focus:outline-none hover:border-base-content w-full text-xs"
value={reason}
onChange={(e) => setReason(e.target.value)}
></textarea>
@@ -2376,7 +2415,7 @@ const ReportTable = (props) => {
)}
{Object?.keys(isNextStep) <= 0 && (
<div className="flex justify-between items-center gap-2 my-2 px-3">
<div className="text-black">
<div className="text-base-content">
{user?.signerPtr?.Name || "-"}{" "}
{`<${
user?.email
@@ -43,7 +43,7 @@ function CustomModal(props) {
</h3>
{!isExtendExpiry && (
<div className="p-[10px] px-[20px]">
<p className="text-[15px]">{props.bodyMssg && props.bodyMssg}</p>
<p className="text-[15px] text-base-content">{props.bodyMssg && props.bodyMssg}</p>
</div>
)}
{!isExtendExpiry && (
@@ -68,7 +68,7 @@ function CustomModal(props) {
)}
{props.footerMessage && (
<>
<div className="mx-3">
<div className="mx-3 text-base-content">
<textarea
rows={3}
placeholder="Reason (optional)"
@@ -108,7 +108,7 @@ function CustomModal(props) {
</label>
<input
type="date"
className="rounded-full bg-base-300 w-full px-4 py-2 text-black border-2 hover:border-spacing-2"
className="rounded-full bg-base-300 w-full px-4 py-2 text-base-content border-2 hover:border-spacing-2"
defaultValue={props?.doc?.ExpiryDate?.iso?.split("T")?.[0]}
onChange={(e) => setExpiryDate(e.target.value)}
/>
+5 -1
View File
@@ -10,6 +10,10 @@
background-position: right 0.7rem top 50%;
background-size: 1rem auto;
}
[data-theme="opensigndark"] .validationlist{
background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABkAAAAZCAYAAADE6YVjAAAAAXNSR0IArs4c6QAAAQ1JREFUSEvtlDtKBTEARc/Fys8OXumndhl2ggt4jTuwsVJQK92DtY1Yuw3bp2jlDvxUcn2BDIQ4k2Sa1ziBNMmdHO4JGbGCoRUwmCCjLP8DXbYPgD3gVtLnkB/bm8AxsJD02Jfr1WX7EJjHD16BC0kf+QG2N4BzYDfu3Um6z3N/ILb3gbMs+AJcpo1igwDYybIh95Su9UFmwPVyrg+BCoBv4FTSexESNm1vRw1bGegNuAFOEkVdJABCi0VVVxewHS49aAve0/EDrGVrX8u1qz5AyBXfSaFRyhhs0IWqj7ECKjZohhTuqAlQ1ZU6yRo1A0ZBYqPw6I6AB0nPrX/J6p20HlTKTZBRFiddo3T9ArZOWBrGcf52AAAAAElFTkSuQmCC");
}
@media (max-width: 375px) {
.validationlist {
@@ -17,7 +21,7 @@
}
}
@media (min-width:375px) and (max-width: 767px) {
@media (min-width: 375px) and (max-width: 767px) {
.validationlist {
background-position: right 1rem top 50%;
}
@@ -0,0 +1,143 @@
/* VS Code Dark Theme Improvements for OpenSign */
/* Better disabled button styling for dark mode */
[data-theme="opensigndark"] {
/* Primary button disabled state */
.op-btn-primary:disabled {
background-color: #3C3C3C !important;
color: #CCCCCC !important;
border-color: #565656 !important;
opacity: 1 !important;
cursor: not-allowed !important;
}
.op-btn-primary:disabled:hover {
background-color: #3C3C3C !important;
color: #CCCCCC !important;
border-color: #565656 !important;
transform: none !important;
box-shadow: none !important;
}
/* Secondary button disabled state */
.op-btn-secondary:disabled {
background-color: #2A2A2A !important;
color: #999999 !important;
border-color: #444444 !important;
opacity: 1 !important;
cursor: not-allowed !important;
}
/* Ghost button disabled state */
.op-btn-ghost:disabled {
background-color: transparent !important;
color: #666666 !important;
border-color: #444444 !important;
opacity: 1 !important;
cursor: not-allowed !important;
}
/* Better icon visibility for various states */
.icon-disabled,
.fa-light.text-gray-400,
.fa-light.text-gray-500 {
color: #858585 !important;
}
.icon-visible,
.nav-icon,
.folder-icon {
color: #CCCCCC !important;
}
/* Muted/inactive icons with better visibility */
.muted-icon,
.inactive-icon {
color: #999999 !important;
}
/* Hover states for better interactivity */
.hover\\:bg-gray-200:hover {
background-color: #2A2A2A !important;
}
.hover\\:text-gray-600:hover {
color: #E5E7EB !important;
}
/* Form elements in disabled state */
.op-input:disabled,
.op-select:disabled,
.op-textarea:disabled {
background-color: #2A2A2A !important;
color: #999999 !important;
border-color: #444444 !important;
cursor: not-allowed !important;
}
/* Dropdown menu items */
.dropdown-item {
color: #E5E7EB !important;
}
.dropdown-item:hover {
background-color: #2A2A2A !important;
color: #FFFFFF !important;
}
/* Better text contrast for various elements */
.text-gray-600 {
color: #CCCCCC !important;
}
.text-gray-500 {
color: #999999 !important;
}
.text-gray-400 {
color: #858585 !important;
}
/* Status indicators with better visibility */
.status-badge {
box-shadow: 0 2px 4px rgba(255, 255, 255, 0.1) !important;
}
/* Tooltip improvements */
.op-tooltip {
background-color: #1F2937 !important;
color: #E5E7EB !important;
border-color: #4B5563 !important;
}
/* Card and panel borders */
.op-card,
.border-gray-300 {
border-color: #2C2C2C !important;
}
/* Loading states */
.opacity-50 {
opacity: 0.7 !important;
}
/* Focus states for better accessibility */
.op-btn:focus-visible,
.op-input:focus-visible,
.op-select:focus-visible {
outline: 2px solid #007ACC !important;
outline-offset: 2px !important;
}
}
/* Ensure these styles don't affect light mode */
[data-theme="opensigncss"] {
/* Keep original colors for light mode */
.icon-disabled {
color: #9CA3AF;
}
.icon-visible {
color: #6B7280;
}
}
+23
View File
@@ -1,3 +1,26 @@
/* Dark mode support for custom warning in Managesign */
[data-theme="opensigndark"] .customwarning {
background-color: #374151 !important;
color: #E5E7EB !important;
border-color: #4B5563 !important;
}
[data-theme="opensigndark"] .customwarning::before {
border-color: transparent transparent #4B5563 transparent !important;
}
/* Dark mode support for signature management warning */
[data-theme="opensigndark"] .signWarning {
background-color: #374151 !important;
color: #E5E7EB !important;
}
/* Ensure the Managesign page background is consistent */
[data-theme="opensigndark"] .managesign-container {
background-color: #121212 !important;
color: #F3F4F6 !important;
}
.customwarning {
position: absolute;
padding: 8px;
@@ -204,6 +204,15 @@ a {
align-items: center;
}
/* Dark mode support for HoverCard */
[data-theme="opensigndark"] .HoverCardContent {
background-color: #1F2937;
color: #E5E7EB;
box-shadow:
hsl(0 0% 0% / 50%) 0px 10px 38px -10px,
hsl(0 0% 0% / 30%) 0px 10px 20px -15px;
}
.HoverCardContent[data-side="top"] {
animation-name: slideDownAndFade;
}
@@ -224,6 +233,11 @@ a {
fill: white;
}
/* Dark mode support for HoverCard arrow */
[data-theme="opensigndark"] .HoverCardArrow {
fill: #1F2937;
}
@keyframes slideUpAndFade {
0% {
opacity: 0;
+46 -6
View File
@@ -6,7 +6,7 @@
.react-datepicker__input-container {
position: initial !important;
}
.select-none-cls{
.select-none-cls {
-webkit-user-select: none;
/* Disable text selection in WebKit browsers */
-moz-user-select: none;
@@ -33,10 +33,17 @@
width: 440px;
height: 167px;
}
.tabWidth{
.tabWidth {
border: 1px solid #f3f4f6;
background-color: #f3f4f6;
width: 440px;
}
[data-theme="opensigndark"] .tabWidth {
border: 1px solid #1f2937 !important;
background-color: #1f2937 !important;
}
.intialSignatureCanvas {
width: 150px;
height: 150px;
@@ -52,7 +59,6 @@
background-color: #111111; /* blue-500 */
}
.intialSignature {
border: 2px solid #888;
background-color: rgb(255, 255, 255);
@@ -60,6 +66,40 @@
height: 183px;
}
/* Dark mode support for initials box in /managesign */
[data-theme="opensigndark"] .intialSignature {
background-color: #1f2937 !important;
border-color: #4b5563 !important;
}
[data-theme="opensigndark"] .intialSignatureCanvas {
background-color: #1f2937 !important;
border-color: #4b5563 !important;
}
/* Also support signature canvas for consistency */
[data-theme="opensigndark"] .signatureCanvas {
background-color: #1f2937 !important;
border-color: #4b5563 !important;
}
/* Dark mode support for initials box in /managesign */
[data-theme="opensigndark"] .intialSignature {
background-color: #1f2937 !important;
border-color: #4b5563 !important;
}
[data-theme="opensigndark"] .intialSignatureCanvas {
background-color: #1f2937 !important;
border-color: #4b5563 !important;
}
/* Also support signature canvas for consistency */
[data-theme="opensigndark"] .signatureCanvas {
background-color: #1f2937 !important;
border-color: #4b5563 !important;
}
.penContainerDefault {
width: 460px;
}
@@ -401,7 +441,7 @@ option {
width: 300px;
height: 120px;
}
.tabWidth{
.tabWidth {
width: 300px;
}
@@ -427,7 +467,7 @@ option {
width: 280px;
height: 112px;
}
.tabWidth{
.tabWidth {
width: 280px;
}
@@ -451,7 +491,7 @@ option {
width: 230px;
height: 92px;
}
.tabWidth{
.tabWidth {
width: 230px;
}
+99 -2
View File
@@ -6,11 +6,67 @@ module.exports = {
},
plugins: [
require("daisyui"),
function ({ addUtilities }) {
function ({ addUtilities, theme }) {
addUtilities({
// Prevent iOS long-press popup
".touch-callout-none": {
"-webkit-touch-callout": "none"
},
// VS Code-style disabled button for all themes
".op-btn-vscode-disabled": {
"background-color": "#3C3C3C !important",
color: "#CCCCCC !important",
"border-color": "#565656 !important",
cursor: "not-allowed !important",
opacity: "1 !important",
"&:hover": {
"background-color": "#3C3C3C !important",
color: "#CCCCCC !important",
"border-color": "#565656 !important",
transform: "none !important"
}
},
// Dark mode icon improvements using DaisyUI theme detection
'[data-theme="opensigndark"] .icon-improved': {
color: "#CCCCCC !important"
},
'[data-theme="opensigndark"] .icon-muted': {
color: "#999999 !important"
},
'[data-theme="opensigndark"] .icon-disabled': {
color: "#858585 !important"
},
// Gray text improvements for dark mode
'[data-theme="opensigndark"] .text-gray-500': {
color: "#CCCCCC !important"
},
'[data-theme="opensigndark"] .text-gray-400': {
color: "#999999 !important"
},
'[data-theme="opensigndark"] .text-gray-600': {
color: "#CCCCCC !important"
},
// CSS variable utilities that work with arbitrary values
".icon-themed": {
color: "var(--icon-color)"
},
".icon-themed-muted": {
color: "var(--icon-color-muted)"
},
".icon-themed-disabled": {
color: "var(--icon-color-disabled)"
},
".btn-themed-disabled": {
"background-color": "var(--btn-disabled-bg)",
color: "var(--btn-disabled-color)",
"border-color": "var(--btn-disabled-border)",
cursor: "not-allowed",
"&:hover": {
"background-color": "var(--btn-disabled-bg)",
color: "var(--btn-disabled-color)",
"border-color": "var(--btn-disabled-border)",
transform: "none"
}
}
});
}
@@ -18,7 +74,48 @@ module.exports = {
daisyui: {
// themes: true,
themes: [
"dark",
{
opensigndark: {
primary: "#007ACC", // VS Code blue - CTA & highlight color
"primary-content": "#FFFFFF",
secondary: "#1F2937", // Sidebar background (darker slate)
"secondary-content": "#E5E7EB",
accent: "#4A9EFF", // Lighter VS Code blue for hover, minor CTA
"accent-content": "#FFFFFF",
neutral: "#3C3C3C", // VS Code inactive/disabled element background
"neutral-content": "#CCCCCC", // VS Code inactive text color
"base-100": "#121212", // App background
"base-200": "#181818", // Slight elevation (cards)
"base-300": "#1E1E1E", // Further elevated items (panels)
"base-content": "#F3F4F6", // Main text color (soft white)
info: "#2563EB", // For info panels like "Out for signature"
success: "#22C55E", // Optional: for completed docs or alerts
warning: "#FBBF24",
error: "#EF4444",
"--rounded-btn": "1.9rem",
"--tab-border": "2px",
"--tab-radius": "0.7rem",
// Custom CSS variables for icon and button states
"--icon-color": "#CCCCCC",
"--icon-color-muted": "#999999",
"--icon-color-disabled": "#858585",
"--btn-disabled-bg": "#3C3C3C",
"--btn-disabled-color": "#CCCCCC",
"--btn-disabled-border": "#565656",
// Optional polish
"--navbar-padding": "0.8rem",
"--border-color": "#2C2C2C", // Card/table separation
"--tooltip-color": "#1F2937"
}
},
{
opensigncss: {
primary: "#002864",
+5
View File
@@ -2,6 +2,11 @@
FROM node:22.14.0
# Install LibreOffice for DOCX to PDF conversions
RUN apt-get update \
&& apt-get install -y libreoffice \
&& rm -rf /var/lib/apt/lists/*
# Set the working directory inside the container
WORKDIR /usr/src/app
@@ -2,6 +2,8 @@ import express from 'express';
import cors from 'cors';
import dotenv from 'dotenv';
import uploadFile from './uploadFile.js';
import docxtopdf, { upload as docxUpload } from './docxtopdf.js';
import decryptpdf, { upload as decryptUpload } from './decryptpdf.js';
export const app = express();
@@ -11,4 +13,5 @@ app.use(express.json({ limit: '50mb' }));
app.use(express.urlencoded({ limit: '50mb', extended: true }));
app.post('/file_upload', uploadFile);
app.post('/docxtopdf', docxUpload.single('file'), docxtopdf);
app.post('/decryptpdf', decryptUpload.single('file'), decryptpdf);
@@ -0,0 +1,43 @@
import fs from 'node:fs';
import multer from 'multer';
import Coherentpdf from 'coherentpdf';
const storage = multer.diskStorage({
destination(req, file, cb) {
cb(null, 'exports');
},
filename(req, file, cb) {
cb(null, file.originalname);
},
});
export const upload = multer({ storage });
export default async function decryptpdf(req, res) {
const inputPath = req.file.path;
const password = req.body.password || '';
try {
const file = fs.readFileSync(inputPath);
const pdf = await Coherentpdf.fromMemory(file, password);
await Coherentpdf.decryptPdf(pdf, password);
// Get decrypted buffer directly from memory (no file I/O)
const buffer = await Coherentpdf.toMemory(pdf, false, false);
res.set({
'Content-Type': 'application/pdf',
'Content-Disposition': 'inline; filename="decrypted.pdf"',
'Content-Length': buffer.length,
});
res.send(buffer);
fs.unlink(inputPath, () => {});
} catch (err) {
fs.unlink(inputPath, () => {});
console.log('Error in decrypt file: ', err);
let code = err?.code ? err.code : 400;
let message = err?.[2]?.c ? err[2].c : 'Something went wrong.';
if (err?.[2]?.c?.includes('Bad password') || err?.[2]?.c?.includes('decrypt_pdf_inner')) {
code = 401;
message = 'Incorrect password.';
}
return res.status(code).json({ error: message });
}
}
@@ -0,0 +1,166 @@
import fs from 'node:fs';
import axios from 'axios';
import multer from 'multer';
import libre from 'libreoffice-convert';
import util from 'node:util';
import { cloudServerUrl, getSecureUrl } from '../../Utils.js';
libre.convertAsync = util.promisify(libre.convert);
const storage = multer.diskStorage({
destination(req, file, cb) {
cb(null, 'exports');
},
filename(req, file, cb) {
cb(null, file.originalname);
},
});
export const upload = multer({ storage });
function generatePdfName(length) {
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
let result = '';
for (let i = 0; i < length; i++) {
result += chars.charAt(Math.floor(Math.random() * chars.length));
}
return result;
}
export default async function docxtopdf(req, res) {
const serverUrl = cloudServerUrl;
const appId = process.env.APP_ID;
const masterKey = process.env.MASTER_KEY;
const inputPath = req.file.path;
const name = generatePdfName(16);
const fileName = `${name}.pdf`;
const outputPath = './exports/output.pdf';
try {
const userRes = await axios.get(serverUrl + '/users/me', {
headers: {
'X-Parse-Application-Id': appId,
'X-Parse-Session-Token': req.headers['sessiontoken'],
},
});
const userId = JSON.stringify({
UserId: {
__type: 'Pointer',
className: '_User',
objectId: userRes.data.objectId,
},
});
const resUser = await axios.get(
serverUrl + `/classes/contracts_Users?where=${userId}&limit=1&include=TenantId`,
{
headers: {
'X-Parse-Application-Id': appId,
'X-Parse-Master-Key': masterKey,
},
}
);
if (resUser?.data?.results?.length > 0) {
const tenantId = resUser.data.results[0].TenantId?.objectId;
const ext = '.pdf';
const outPath = `./exports/output${ext}`;
const docxBuf = fs.readFileSync(inputPath);
const pdfBuffer = await libre.convertAsync(docxBuf, ext, undefined);
fs.writeFileSync(outPath, pdfBuffer);
const file = fs.readFileSync(outPath);
const size = fs.statSync(outPath).size;
const PartnersTenant = JSON.stringify({
PartnersTenant: {
__type: 'Pointer',
className: 'partners_Tenant',
objectId: tenantId,
},
});
const resTenantCredit = await axios.get(
serverUrl + `/classes/partners_TenantCredits?where=${PartnersTenant}&limit=1`,
{
headers: {
'X-Parse-Application-Id': appId,
'X-Parse-Master-Key': masterKey,
},
}
);
if (resTenantCredit.data?.results?.length > 0) {
const tenantCreditsId = resTenantCredit.data.results[0].objectId;
const activeFileAdapter = resUser.data.results[0].TenantId?.ActiveFileAdapter;
let fileUrl;
if (activeFileAdapter) {
const params = {
fileBase64: file.toString('base64'),
fileName,
id: activeFileAdapter,
};
const url = serverUrl + '/functions/savetofileadapter';
const headers = {
'Content-Type': 'application/json',
'X-Parse-Application-Id': appId,
'X-Parse-Session-Token': req.headers['sessiontoken'],
};
try {
const savetos3 = await axios.post(url, params, { headers });
fileUrl = savetos3?.data?.result?.url;
} catch (err) {
console.log('err in save to customfile', err);
}
} else {
const parsefile = await axios.post(serverUrl + `/files/${fileName}`, file, {
headers: {
'X-Parse-Application-Id': appId,
'X-Parse-Master-Key': masterKey,
'Content-Type': 'application/pdf',
},
});
const fileRes = getSecureUrl(parsefile.data.url);
fileUrl = fileRes.url;
}
const usedStorage = resTenantCredit.data.results[0].usedStorage
? resTenantCredit.data.results[0].usedStorage + size
: size;
await axios.put(
serverUrl + `/classes/partners_TenantCredits/${tenantCreditsId}`,
{ usedStorage },
{
headers: {
'X-Parse-Application-Id': appId,
'X-Parse-Master-Key': masterKey,
},
}
);
await axios.post(
serverUrl + '/classes/partners_DataFiles',
{
FileSize: size,
FileUrl: fileUrl,
TenantPtr: {
__type: 'Pointer',
className: 'partners_Tenant',
objectId: tenantId,
},
},
{
headers: {
'X-Parse-Application-Id': appId,
'X-Parse-Master-Key': masterKey,
},
}
);
[inputPath, outPath].forEach(p => fs.existsSync(p) && fs.unlinkSync(p));
return res.status(200).json({ message: 'success.', url: fileUrl });
}
}
} catch (err) {
[inputPath, outputPath].forEach(p => fs.existsSync(p) && fs.unlinkSync(p));
const msg =
err?.response?.data?.error || err?.response?.data || err?.message || 'Something went wrong.';
console.log(`Error converting file: ${msg}`);
const message =
'We are currently experiencing some issues with processing DOCX files. Please upload the PDF version or contact us on support@opensignlabs.com';
return res.status(400).json({ error: message });
}
}
@@ -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 digitial sign in certificate
// `pdflibAddPlaceholder` is used to add code of only digital sign in certificate
pdflibAddPlaceholder({
pdfDoc: certificatePdf,
reason: `Digitally signed by ${eSignName}.`,
@@ -55,7 +55,7 @@ export default function reportJson(id, userId) {
'ExpiryDate',
],
};
// In progess report
// In progress report
case '1MwEuxLEkF':
return {
reportName: 'In-progress documents',
@@ -51,6 +51,17 @@ 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',
},
})),
}));
const placeHolders = {
signerObjId: '',
signerPtr: {},
@@ -58,7 +69,7 @@ export default async function saveAsTemplate(request) {
blockColor: '#93a3db',
Role: 'Role 1',
email: '',
placeHolder: _docRes?.Placeholders,
placeHolder: updatedPlaceholder,
};
templateCls.set('Placeholders', [placeHolders]);
} else {
+796 -665
View File
File diff suppressed because it is too large Load Diff
+12 -10
View File
@@ -18,23 +18,25 @@
"watch": "nodemon index.js"
},
"dependencies": {
"@aws-sdk/client-s3": "^3.824.0",
"@aws-sdk/s3-request-presigner": "^3.824.0",
"@aws-sdk/client-s3": "^3.828.0",
"@aws-sdk/s3-request-presigner": "^3.828.0",
"@parse/fs-files-adapter": "^3.0.0",
"@parse/s3-files-adapter": "^4.1.1",
"@parse/s3-files-adapter": "^4.2.0",
"@pdf-lib/fontkit": "^1.1.1",
"@signpdf/placeholder-pdf-lib": "^3.2.6",
"@signpdf/signer-p12": "^3.2.4",
"@signpdf/signpdf": "^3.2.5",
"aws-sdk": "^2.1692.0",
"axios": "^1.9.0",
"axios": "^1.10.0",
"coherentpdf": "^2.5.5",
"cors": "^2.8.5",
"date-fns-tz": "^3.2.0",
"dotenv": "^16.5.0",
"express": "^5.1.0",
"form-data": "^4.0.2",
"form-data": "^4.0.3",
"generate-api-key": "^1.0.2",
"googleapis": "^149.0.0",
"googleapis": "^150.0.1",
"libreoffice-convert": "^1.6.1",
"mailgun.js": "^12.0.2",
"mongodb": "^6.17.0",
"multer": "^2.0.1",
@@ -46,7 +48,7 @@
"parse-server": "^8.2.1",
"parse-server-api-mail-adapter": "^4.1.0",
"pdf-lib": "^1.17.1",
"posthog-node": "^4.18.0",
"posthog-node": "^5.1.0",
"qrcode": "^1.5.4",
"rate-limiter-flexible": "^7.1.1",
"speakeasy": "^2.0.0",
@@ -55,9 +57,9 @@
"type": "module",
"devDependencies": {
"@babel/eslint-parser": "^7.27.5",
"eslint": "^9.28.0",
"jasmine": "^5.7.1",
"mongodb-runner": "^5.8.3",
"eslint": "^9.29.0",
"jasmine": "^5.8.0",
"mongodb-runner": "^5.9.0",
"nodemon": "^3.1.10",
"nyc": "^17.1.0",
"prettier": "^3.5.3"