Merge pull request #2060 from OpenSignLabs/staging

This commit is contained in:
Amol
2025-12-22 15:29:21 +05:30
committed by GitHub
64 changed files with 16808 additions and 988 deletions
+22 -23
View File
@@ -1,58 +1,57 @@
{
"name": "open_sign",
"version": "2.32.0",
"version": "2.34.0",
"private": true,
"dependencies": {
"@formkit/auto-animate": "^0.9.0",
"@imgly/background-removal": "^1.7.0",
"@lottiefiles/dotlottie-react": "^0.17.8",
"@lottiefiles/dotlottie-react": "^0.17.10",
"@pdf-lib/fontkit": "^1.1.1",
"@radix-ui/themes": "^3.2.1",
"@reduxjs/toolkit": "^2.11.0",
"@reactour/tour": "^3.8.0",
"@reduxjs/toolkit": "^2.11.2",
"axios": "^1.13.2",
"body-scroll-lock": "^4.0.0-beta.0",
"date-fns-tz": "^3.2.0",
"file-saver": "^2.0.5",
"i18next": "^25.7.1",
"i18next": "^25.7.3",
"i18next-browser-languagedetector": "^8.2.0",
"i18next-http-backend": "^3.0.2",
"jszip": "^3.10.1",
"jwt-decode": "^4.0.0",
"moment": "^2.30.1",
"parse": "^7.1.2",
"parse": "^8.0.0",
"pdf-lib": "^1.17.1",
"pkijs": "^3.3.3",
"print-js": "^1.6.0",
"prismjs": "^1.30.0",
"quill-html-edit-button": "^3.0.0",
"radix-ui": "^1.4.3",
"react": "^18.3.1",
"react": "^19.2.3",
"react-bootstrap": "^2.10.10",
"react-confetti": "^6.4.0",
"react-datepicker": "^8.10.0",
"react-datepicker": "^9.0.0",
"react-dnd": "^16.0.1",
"react-dnd-html5-backend": "^16.0.1",
"react-dnd-multi-backend": "^9.0.0",
"react-dom": "^18.3.1",
"react-dom": "^19.2.3",
"react-gtm-module": "^2.0.11",
"react-helmet": "^6.1.0",
"react-i18next": "^16.3.5",
"react-konva": "^18.2.12",
"react-pdf": "^9.2.1",
"react-i18next": "^16.5.0",
"react-konva": "^19.2.1",
"react-pdf": "^10.2.0",
"react-quill-new": "^3.6.0",
"react-redux": "^9.2.0",
"react-rnd": "^10.5.2",
"react-router": "^7.10.0",
"react-router": "^7.10.1",
"react-scrollbars-custom": "^4.1.1",
"react-select": "^5.10.2",
"react-signature-canvas": "^1.1.0-alpha.2",
"react-timezone-select": "^3.2.8",
"react-to-print": "^3.2.0",
"react-tooltip": "^5.30.0",
"reactour": "^1.19.4",
"redux": "^5.0.1",
"regex-parser": "^2.3.1",
"serve": "^14.2.5",
"styled-components": "^5.3.11",
"web-vitals": "^5.1.0",
"xlsx": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz"
},
@@ -99,31 +98,31 @@
"@babel/preset-react": "^7.28.5",
"@babel/runtime-corejs2": "^7.28.3",
"@testing-library/jest-dom": "^6.8.0",
"@testing-library/react": "^16.3.0",
"@testing-library/react": "^16.3.1",
"@testing-library/user-event": "^14.6.1",
"@types/react": "^19.2.7",
"@vitejs/plugin-react": "^5.1.1",
"@vitejs/plugin-react": "^5.1.2",
"@vitest/ui": "^4.0.15",
"autoprefixer": "^10.4.22",
"autoprefixer": "^10.4.23",
"babel-loader": "^10.0.0",
"commitizen": "^4.3.1",
"concurrently": "^9.2.1",
"css-loader": "^7.1.2",
"daisyui": "^4.12.24",
"eslint": "^9.39.1",
"eslint": "^9.39.2",
"eslint-plugin-prettier": "^5.5.4",
"eslint-plugin-react": "^7.37.5",
"jsdom": "^27.2.0",
"jsdom": "^27.3.0",
"lint-staged": "^16.2.7",
"postcss": "^8.5.6",
"prettier": "^3.7.4",
"pretty-quick": "^4.2.2",
"rollup-plugin-node-polyfills": "^0.2.1",
"tailwindcss": "^3.4.17",
"tailwindcss": "^3.4.19",
"tmp": "^0.2.5",
"vite": "^7.2.6",
"vite": "^7.3.0",
"vite-plugin-svgr": "^4.5.0",
"vite-tsconfig-paths": "^5.1.4",
"vite-tsconfig-paths": "^6.0.1",
"vitest": "^4.0.15"
},
"overrides": {
@@ -270,7 +270,7 @@
"signature": "Unterschrift",
"upload-image": "Bild hochladen",
"clear": "Löschen",
"upload-signature/Image": "Bitte Unterschrift/Bild hochladen",
"upload-signature/Image": "Bitte Unterschrift/Bild hochladen/Stempel",
"initials": "Initialen",
"API": "API",
"api-token": "API-Token",
@@ -588,7 +588,7 @@
"provide-your-details": "Bitte geben Sie Ihre Daten an",
"provide-email": "Bitte E-Mail angeben.",
"additional-info": "Zusätzliche Informationen",
"signature-saved-alert": "Unterschrift erfolgreich gespeichert.",
"signature-saved-alert": "erfolgreich gespeichert.",
"saved-successfully": "Erfolgreich gespeichert.",
"fill-field": "Bitte dieses Feld ausfüllen",
"error-template": "Fehler: Vorlage nicht gefunden!",
@@ -1308,5 +1308,25 @@
"your-credits-usage": "Ihr Kreditverbrauch",
"remaining": "verbleibend",
"used": "verwendet",
"your-credits-usage-help": "Dies ist die Anzahl der Credits, die mit Ihrem API-Schlüssel verbraucht wurden."
"your-credits-usage-help": "Dies ist die Anzahl der Credits, die mit Ihrem API-Schlüssel verbraucht wurden.",
"api-credits-updated": "API-Credits wurden erfolgreich zugewiesen.",
"min-date": "Mindestdatum",
"max-date": "Höchstdatum",
"invalid-min-date": "Ungültiges Mindestdatum.",
"min-less-max": "Das Mindestdatum muss kleiner oder gleich dem Höchstdatum sein.",
"invalid-max-date": "Ungültiges Höchstdatum.",
"max-less-min": "Das Höchstdatum muss größer oder gleich dem Mindestdatum sein.",
"date-range": "Datumsbereich",
"date-range-help": "Legen Sie ein Mindest- und/oder Höchstdatum fest, um die Auswahl einzuschränken. Das gewählte Datum muss innerhalb dieses Bereichs liegen.",
"option-disabled-by-owner": "Der Dokumentersteller hat diese Option deaktiviert",
"pen-colors": "Stiftfarben",
"pen-colors-help": "Wählen Sie aus, welche Stiftfarben für Signatur- und Initialen-Widgets verfügbar sind. Aktivieren Sie ein Kontrollkästchen, um eine Farbe anzuzeigen, und deaktivieren Sie es, um sie auszublenden.",
"security-key": "Sicherheitsschlüssel",
"remove-limit": "Limit entfernen",
"removed-api-credits": "Das API-Credit-Limit wurde entfernt.",
"enable-authentication": "Authentifizierung aktivieren",
"working": "In Bearbeitung",
"test-security-key": "Sicherheitsschlüssel testen",
"generate": "Generieren",
"regenerate": "Neu generieren"
}
@@ -270,7 +270,7 @@
"signature": "Signature",
"upload-image": " Upload image",
"clear": "Clear",
"upload-signature/Image": "Please upload signature/Image",
"upload-signature/Image": "Please upload signature/Image/stamp",
"initials": "Initials",
"API": "API",
"api-token": "API token",
@@ -589,7 +589,7 @@
"provide-your-details": "Provide your details",
"provide-email": "Please provide email.",
"additional-info": "Additional info",
"signature-saved-alert": "Signature saved successfully.",
"signature-saved-alert": "Saved successfully.",
"saved-successfully": "Saved successfully.",
"fill-field": "Please fill out this field",
"error-template": "Error: Template not found!",
@@ -1309,5 +1309,25 @@
"your-credits-usage": "Your credits usage",
"remaining": "remaining",
"used": "used",
"your-credits-usage-help": "This is the number of credits consumed with your API key."
"your-credits-usage-help": "This is the number of credits consumed with your API key.",
"api-credits-updated": "API credits have been allocated successfully.",
"min-date": "Min Date",
"max-date": "Max Date",
"invalid-min-date": "Invalid min date.",
"min-less-max": "Min date must be less than or equal to max date.",
"invalid-max-date": "Invalid max date.",
"max-less-min": "Max date must be greater than or equal to min date.",
"date-range": "Date range",
"date-range-help": "Set Min and/or Max to restrict selection. Chosen date must fall within the range.",
"option-disabled-by-owner": "Document creator has disabled this option",
"pen-colors": "Pen colors",
"pen-colors-help": "Choose which pen colors are available for Signature and Initials widgets. Check to show a color, uncheck to hide it.",
"security-key": "Security key",
"remove-limit": "Remove limit",
"removed-api-credits": "The API credit limit has been removed.",
"enable-authentication": "Enable authentication",
"working": "Working",
"test-security-key": "Test security key",
"generate": "Generate",
"regenerate": "Regenerate"
}
@@ -270,7 +270,7 @@
"signature": "Firma",
"upload-image": " Subir imagen",
"clear": "Limpiar",
"upload-signature/Image": "Por favor, sube la firma/imagen",
"upload-signature/Image": "Por favor, sube la firma/imagen/estampilla",
"initials": "Iniciales",
"API": "API",
"api-token": "Token API",
@@ -589,7 +589,7 @@
"provide-your-details": "Proporciona tus detalles",
"provide-email": "Por favor, proporciona un correo.",
"additional-info": "Información adicional",
"signature-saved-alert": "Firma guardada exitosamente.",
"signature-saved-alert": "guardada exitosamente.",
"saved-successfully": "Guardado con éxito.",
"fill-field": "Por favor, rellena este campo",
"error-template": "Error: ¡Plantilla no encontrada!",
@@ -1309,5 +1309,25 @@
"your-credits-usage": "Uso de sus créditos",
"remaining": "restantes",
"used": "usados",
"your-credits-usage-help": "Esta es la cantidad de créditos consumidos con su clave API."
"your-credits-usage-help": "Esta es la cantidad de créditos consumidos con su clave API.",
"api-credits-updated": "Los créditos de API se han asignado correctamente.",
"min-date": "Fecha mínima",
"max-date": "Fecha máxima",
"invalid-min-date": "Fecha mínima no válida.",
"min-less-max": "La fecha mínima debe ser menor o igual a la fecha máxima.",
"invalid-max-date": "Fecha máxima no válida.",
"max-less-min": "La fecha máxima debe ser mayor o igual a la fecha mínima.",
"date-range": "Rango de fechas",
"date-range-help": "Establezca una fecha mínima y/o máxima para restringir la selección. La fecha elegida debe estar dentro del rango.",
"option-disabled-by-owner": "El creador del documento ha deshabilitado esta opción",
"pen-colors": "Colores del bolígrafo",
"pen-colors-help": "Elija qué colores de bolígrafo están disponibles para los widgets de Firma e Iniciales. Marque una casilla para mostrar un color y desmárquela para ocultarlo.",
"security-key": "Clave de seguridad",
"remove-limit": "Eliminar límite",
"removed-api-credits": "El límite de créditos API ha sido eliminado.",
"enable-authentication": "Habilitar autenticación",
"working": "En funcionamiento",
"test-security-key": "Probar clave de seguridad",
"generate": "Generar",
"regenerate": "Regenerar"
}
@@ -270,7 +270,7 @@
"signature": "Signature",
"upload-image": "Télécharger une image",
"clear": "Vider",
"upload-signature/Image": "télécharger-signature/Image",
"upload-signature/Image": "télécharger-signature/Image/timbre",
"initials": "Initiales",
"API": "API",
"api-token": "Jeton API",
@@ -588,7 +588,7 @@
"provide-your-details": "Fournissez vos coordonnées",
"provide-email": "Veuillez fournir un e-mail.",
"additional-info": "Information additionnelle",
"signature-saved-alert": "Signature enregistrée avec succès.",
"signature-saved-alert": "enregistrée avec succès.",
"saved-successfully": "Enregistré avec succès.",
"fill-field": "Veuillez remplir ce champ",
"error-template": "Erreur : modèle introuvable ! ",
@@ -1308,5 +1308,25 @@
"your-credits-usage": "Votre utilisation des crédits",
"remaining": "restants",
"used": "utilisés",
"your-credits-usage-help": "Ceci est le nombre de crédits consommés avec votre clé API."
"your-credits-usage-help": "Ceci est le nombre de crédits consommés avec votre clé API.",
"api-credits-updated": "Les crédits API ont été attribués avec succès.",
"min-date": "Date minimale",
"max-date": "Date maximale",
"invalid-min-date": "Date minimale invalide.",
"min-less-max": "La date minimale doit être inférieure ou égale à la date maximale.",
"invalid-max-date": "Date maximale invalide.",
"max-less-min": "La date maximale doit être supérieure ou égale à la date minimale.",
"date-range": "Plage de dates",
"date-range-help": "Définissez une date minimale et/ou maximale pour restreindre la sélection. La date choisie doit se situer dans cette plage.",
"option-disabled-by-owner": "Le créateur du document a désactivé cette option",
"pen-colors": "Couleurs du stylo",
"pen-colors-help": "Choisissez quelles couleurs de stylo sont disponibles pour les widgets Signature et Initiales. Cochez pour afficher une couleur, décochez pour la masquer.",
"security-key": "Clé de sécurité",
"remove-limit": "Supprimer la limite",
"removed-api-credits": "La limite de crédits API a été supprimée.",
"enable-authentication": "Activer l'authentification",
"working": "En cours",
"test-security-key": "Tester la clé de sécurité",
"generate": "Générer",
"regenerate": "Régénérer"
}
@@ -270,7 +270,7 @@
"signature": "हस्ताक्षर",
"upload-image": " छवि अपलोड करें",
"clear": "स्पष्ट",
"upload-signature/Image": "कृपया हस्ताक्षर/छवि अपलोड करें",
"upload-signature/Image": "कृपया हस्ताक्षर/छवि अपलोड करें/मोहर",
"initials": "हस्ताक्षर",
"API": "एपीआई",
"api-token": "एपीआई टोकन",
@@ -588,7 +588,7 @@
"provide-your-details": "अपना विवरण प्रदान करें",
"provide-email": "कृपया ईमेल प्रदान करें।",
"additional-info": "अतिरिक्त जानकारी",
"signature-saved-alert": "हस्ताक्षर सफलतापूर्वक सहेजा गया।",
"signature-saved-alert": "सफलतापूर्वक सहेजा गया।",
"saved-successfully": "सफलतापूर्वक सहेजा गया।",
"fill-field": "कृपया इस फ़ील्ड को भरें",
"error-template": "त्रुटि: टेम्पलेट नहीं मिला!",
@@ -1308,5 +1308,25 @@
"your-credits-usage": "आपके क्रेडिट उपयोग",
"remaining": "शेष",
"used": "उपयोग किए गए",
"your-credits-usage-help": "यह आपके API कुंजी द्वारा उपभोग किए गए क्रेडिट की संख्या है।"
"your-credits-usage-help": "यह आपके API कुंजी द्वारा उपभोग किए गए क्रेडिट की संख्या है।",
"api-credits-updated": "API क्रेडिट सफलतापूर्वक आवंटित कर दिए गए हैं।",
"min-date": "न्यूनतम तिथि",
"max-date": "अधिकतम तिथि",
"invalid-min-date": "अमान्य न्यूनतम तिथि।",
"min-less-max": "न्यूनतम तिथि अधिकतम तिथि से कम या उसके बराबर होनी चाहिए।",
"invalid-max-date": "अमान्य अधिकतम तिथि।",
"max-less-min": "अधिकतम तिथि न्यूनतम तिथि से अधिक या उसके बराबर होनी चाहिए।",
"date-range": "तिथि सीमा",
"date-range-help": "चयन को सीमित करने के लिए न्यूनतम और/या अधिकतम तिथि सेट करें। चयनित तिथि इस सीमा के भीतर होनी चाहिए।",
"option-disabled-by-owner": "दस्तावेज़ निर्माता ने इस विकल्प को अक्षम कर दिया है",
"pen-colors": "पेन के रंग",
"pen-colors-help": "हस्ताक्षर और प्रारंभिक विजेट्स के लिए उपलब्ध पेन रंग चुनें। किसी रंग को दिखाने के लिए चेक करें और छिपाने के लिए अनचेक करें।",
"security-key": "सुरक्षा कुंजी",
"remove-limit": "सीमा हटाएँ",
"removed-api-credits": "API क्रेडिट सीमा हटा दी गई है।",
"enable-authentication": "प्रमाणीकरण सक्षम करें",
"working": "कार्यरत",
"test-security-key": "सुरक्षा कुंजी का परीक्षण करें",
"generate": "जनरेट करें",
"regenerate": "पुनः जनरेट करें"
}
@@ -270,7 +270,7 @@
"signature": "Firma",
"upload-image": "Carica immagine",
"clear": "Cancella",
"upload-signature/Image": "Carica firma/immagine",
"upload-signature/Image": "Carica firma/immagine/timbro",
"initials": "Iniziali",
"API": "API",
"api-token": "Token API",
@@ -588,7 +588,7 @@
"provide-your-details": "Fornisci i tuoi dettagli",
"provide-email": "Fornisci l'email.",
"additional-info": "Informazioni aggiuntive",
"signature-saved-alert": "Firma salvata con successo.",
"signature-saved-alert": "salvata con successo.",
"saved-successfully": "Salvato con successo.",
"fill-field": "Compila questo campo",
"error-template": "Errore: Modello non trovato!",
@@ -1308,5 +1308,25 @@
"your-credits-usage": "Utilizzo dei tuoi crediti",
"remaining": "rimanenti",
"used": "usati",
"your-credits-usage-help": "Questo è il numero di crediti consumati con la tua chiave API."
"your-credits-usage-help": "Questo è il numero di crediti consumati con la tua chiave API.",
"api-credits-updated": "I crediti API sono stati assegnati con successo.",
"min-date": "Data minima",
"max-date": "Data massima",
"invalid-min-date": "Data minima non valida.",
"min-less-max": "La data minima deve essere minore o uguale alla data massima.",
"invalid-max-date": "Data massima non valida.",
"max-less-min": "La data massima deve essere maggiore o uguale alla data minima.",
"date-range": "Intervallo di date",
"date-range-help": "Imposta una data minima e/o massima per limitare la selezione. La data scelta deve rientrare nell'intervallo.",
"option-disabled-by-owner": "Il creatore del documento ha disabilitato questa opzione.",
"pen-colors": "Colori della penna",
"pen-colors-help": "Scegli quali colori della penna sono disponibili per i widget Firma e Iniziali. Seleziona una casella per mostrare un colore e deselezionala per nasconderlo.",
"security-key": "Chiave di sicurezza",
"remove-limit": "Rimuovi limite",
"removed-api-credits": "Il limite dei crediti API è stato rimosso.",
"enable-authentication": "Abilita autenticazione",
"working": "In funzione",
"test-security-key": "Testa la chiave di sicurezza",
"generate": "Genera",
"regenerate": "Rigenera"
}
+5 -6
View File
@@ -190,9 +190,7 @@ const BulkSendUi = (props) => {
const batchQuery = async (Documents) => {
const token =
{ "X-Parse-Session-Token": localStorage.getItem("accesstoken") };
const functionsUrl = `${localStorage.getItem(
"baseUrl"
)}functions/batchdocuments`;
const functionsUrl = `${localStorage.getItem("baseUrl")}functions/batchdocuments`;
const headers = {
"Content-Type": "application/json",
"X-Parse-Application-Id": localStorage.getItem("parseAppId"),
@@ -201,13 +199,14 @@ const BulkSendUi = (props) => {
const params = { Documents: JSON.stringify(Documents) };
try {
const res = await axios.post(functionsUrl, params, { headers: headers });
// console.log("res ", res);
if (res.data && res.data.result) {
props.handleClose("success", Documents?.length);
}
} catch (err) {
console.log("Err ", err);
props.handleClose("error", 0);
const message =
err?.response?.data?.error || err?.message || "something went wrong.";
console.error("Error sending documents:", message);
props.handleClose("error", 0, message);
} finally {
setIsSubmit(false);
}
+1 -1
View File
@@ -197,7 +197,7 @@ const Header = ({ isConsole, setIsLoggingOut }) => {
</div>
<ul
tabIndex={0}
className={`mt-3 z-[1] p-2 shadow op-dropdown-open op-menu op-menu-sm op-dropdown-content text-base-content bg-base-100 rounded-box w-56 ${
className={`mt-4 z-[1] p-2 shadow op-dropdown-open op-menu op-menu-sm op-dropdown-content text-base-content bg-base-100 rounded-box w-56 ${
isOpen ? "" : "hidden"
}`}
>
+3 -4
View File
@@ -1,4 +1,3 @@
import { Helmet } from "react-helmet";
import { useLocation, matchPath } from "react-router";
import { useTranslation } from "react-i18next";
import { useMemo } from "react";
@@ -70,13 +69,13 @@ export default function Title() {
[t, prefix, appName]
);
const manifestUrl = useManifestUrl(appName, logo);
return (
<Helmet>
<>
<title>{title}</title>
<meta name="description" content={title} />
{logo && <link rel="icon" type="image/png" href={logo} />}
<link rel="manifest" href={manifestUrl} />
</Helmet>
</>
);
}
@@ -56,9 +56,7 @@ function DriveBody(props) {
setRename("");
const trimmedValue = renameValue.trim();
if (trimmedValue.length > 0) {
const updateName = {
Name: renameValue
};
const updateName = { Name: renameValue };
const docId = data.objectId;
const docData = props.pdfData;
const updatedData = docData.map((item) => {
@@ -84,7 +82,7 @@ function DriveBody(props) {
}
);
} catch (err) {
console.log("Err ", err);
console.error("Error in rename doc", err);
props.setIsAlert({
isShow: true,
alertMessage: t("something-went-wrong-mssg")
@@ -160,9 +158,7 @@ function DriveBody(props) {
const handleDeleteDocument = async (docData) => {
setIsDeleteDoc({});
const docId = docData.objectId;
const data = {
IsArchive: true
};
const data = { IsArchive: true };
await axios
.put(
@@ -184,7 +180,7 @@ function DriveBody(props) {
}
})
.catch((err) => {
console.log("Err ", err);
console.error("Err in delete doc", err);
props.setIsAlert({
isShow: true,
alertMessage: t("something-went-wrong-mssg")
@@ -218,16 +214,12 @@ function DriveBody(props) {
}
};
} else {
updateData = {
Folder: { __op: "Delete" }
};
updateData = { Folder: { __op: "Delete" } };
}
await axios
.put(
`${localStorage.getItem(
"baseUrl"
)}classes/contracts_Document/${updateDocId}`,
`${localStorage.getItem("baseUrl")}classes/contracts_Document/${updateDocId}`,
updateData,
{
headers: {
@@ -248,7 +240,7 @@ function DriveBody(props) {
}
})
.catch((err) => {
console.log("err", err);
console.error("err in move folder", err);
});
setIsOpenMoveModal(false);
@@ -296,7 +288,7 @@ function DriveBody(props) {
props.setPdfData(updatedData);
}
} catch (err) {
console.log("Err ", err);
console.error("Err in delete folder", err);
props.setIsAlert({
isShow: true,
alertMessage: t("something-went-wrong-mssg")
@@ -1,4 +1,4 @@
import React, { useState } from "react";
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { useSelector } from "react-redux";
function DefaultSignature(props) {
@@ -105,9 +105,7 @@ function DefaultSignature(props) {
</button>
{!props.isDefault && (
<div className="absolute bg-black/70 text-white w-full h-full flex items-center justify-center text-[11px] cursor-default">
<span className="-rotate-45">
Document creator has disabled this option
</span>
<span className="-rotate-45">{t("option-disabled-by-owner")}</span>
</div>
)}
</div>
@@ -54,6 +54,10 @@ const EditTemplate = ({
AllowModifications: template?.AllowModifications || false,
TimeToCompleteDays: template?.TimeToCompleteDays || 15,
});
const pensList = ["blue", "red", "black"];
const [selectedColors, setSelectedColors] = useState(
template?.PenColors || pensList
);
const [isUpdate, setIsUpdate] = useState(false);
const [showConfirm, setShowConfirm] = useState(false);
const [uploadPdf, setUploadPdf] = useState({
@@ -211,6 +215,7 @@ const EditTemplate = ({
IsEnableOTP: IsEnableOTP,
IsTourEnabled: isTourEnabled,
AllowModifications: allowModify,
PenColors: selectedColors,
...reminderDate
};
onSuccess(data);
@@ -245,6 +250,19 @@ const EditTemplate = ({
handleClose();
};
const handleColorsChange = (color) => {
setSelectedColors((prev) => {
// If user tries to uncheck the last remaining color → block it
if (prev.length === 1 && prev.includes(color)) {
return prev;
}
// Normal toggle behavior
return prev.includes(color)
? prev.filter((c) => c !== color) // remove
: [...prev, color]; // add
});
};
return (
<ModalUi
isOpen
@@ -485,6 +503,41 @@ const EditTemplate = ({
</div>
</div>
</div>
<div className="text-xs mt-3 mb-4">
<label htmlFor="penColors">
{t("pen-colors")}
<a data-tooltip-id="pen-colors-tooltip" className="ml-1">
<sup>
<i className="fa-light fa-question rounded-full border-[#33bbff] text-[#33bbff] text-[13px] border-[1px] py-[1.5px] px-[4px]"></i>
</sup>
</a>
<Tooltip id="pen-colors-tooltip" className="z-[999]">
<div className="max-w-[200px] md:max-w-[450px]">
<p className="font-bold">{t("pen-colors")}</p>
<div>{t("pen-colors-help")}</div>
</div>
</Tooltip>
</label>
<div className="ml-[7px] flex flex-col md:flex-row gap-[10px] mb-[0.7rem]">
{pensList.map((color) => (
<div
key={color}
className="flex flex-row gap-[5px] items-center"
>
<input
className="mr-[2px] op-checkbox op-checkbox-xs"
type="checkbox"
name="penColors"
checked={selectedColors.includes(color)}
onChange={() => handleColorsChange(color)}
/>
<div className="hover:underline underline-offset-2 cursor-default capitalize">
{color}
</div>
</div>
))}
</div>
</div>
<div className="text-xs mt-3">
<SignersInput
label={t("Bcc")}
@@ -136,7 +136,7 @@ function Placeholder(props) {
data?.format,
props.fontSize || props.pos?.options?.fontSize || 12,
props.fontColor || props.pos?.options?.fontColor || "black",
formdata
formdata,
);
};
@@ -620,12 +620,12 @@ function Placeholder(props) {
};
const ExampleCustomInput = forwardRef(({ value, onClick }, ref) => (
<div
className="border-gray-400 rounded-[50px] border-[1px] px-3 md:ml-2 text-xs py-2 focus:outline-none hover:border-base-content "
className="border-gray-400 rounded-[50px] border-[1px] px-3 text-xs py-2 focus:outline-none hover:border-base-content "
onClick={onClick}
ref={ref}
>
{value}
<i className="fa-light fa-calendar ml-[5px]"></i>
<i className={`${value ? "ml-[5px]" : ""} fa-light fa-calendar `}></i>
</div>
));
ExampleCustomInput.displayName = "ExampleCustomInput";
@@ -655,12 +655,14 @@ function Placeholder(props) {
{props.pos?.options?.response &&
props.pos.key !== props?.currWidgetsDetails?.key &&
props.pos.type === textWidget ? (
<span
<textarea
readOnly
onClick={() => {
if (props?.ispublicTemplate) return;
props.setCurrWidgetsDetails &&
props.setCurrWidgetsDetails(props.pos);
props?.setRoleName("prefill");
props.setUniqueId(props?.data?.Id);
props?.setRoleName && props?.setRoleName("prefill");
props.setUniqueId && props.setUniqueId(props?.data?.Id);
}}
style={{
fontFamily: "Arial, sans-serif",
@@ -671,13 +673,21 @@ function Placeholder(props) {
...(props.pos?.Height ? { height: props.pos?.Height } : {}),
fontSize: fontSize,
color: fontColor,
zIndex: 99
zIndex: 99,
cursor: getCursor(),
resize: "none",
background: "transparent",
width: props.posWidth(props.pos, props.isSignYourself),
whiteSpace: "pre-wrap",
overflow: "hidden",
outline: "none",
border: "none"
}}
>
{
props.pos?.options?.response
}
</span>
</textarea>
) : (
<Rnd
id={props.pos.key}
@@ -737,7 +747,7 @@ function Placeholder(props) {
props?.data,
props?.isNeedSign,
props?.uniqueId
) //handle block color of widget for
)
}}
onDrag={(_, d) => {
props?.handleTabDrag?.(props.pos.key);
@@ -938,7 +948,7 @@ function Placeholder(props) {
<span>{t("set-date")} :</span>
<DatePicker
renderCustomHeader={({ date, changeYear, changeMonth }) => (
<div className="flex justify-start md:ml-2 ">
<div className="flex justify-start md:ml-2">
<select
className="bg-transparent outline-none"
value={months[getMonth(date)]}
@@ -992,7 +1002,7 @@ function Placeholder(props) {
}
portalId="root-portal"
/>
<label className=" flex items-center gap-1 cursor-pointer">
<label className="flex items-center gap-1 cursor-pointer mb-0">
<input
checked={isToday}
type="checkbox"
@@ -1006,7 +1016,7 @@ function Placeholder(props) {
</label>
<span
onClick={() => handleClearDate()}
className="underline text-blue-500 cursor-pointer md:ml-2"
className="underline text-blue-500 cursor-pointer ml-2"
>
{t("clear")}
</span>
@@ -1,5 +1,11 @@
import { useEffect, useMemo, useState, forwardRef } from "react";
import {
useEffect,
useState,
forwardRef,
useRef
} from "react";
import {
onChangeInput,
getMonth,
getYear,
radioButtonWidget,
@@ -9,9 +15,7 @@ import {
months,
years,
selectCheckbox,
checkRegularExpress,
isBase64,
getSignerPages
isBase64
} from "../../constant/Utils";
import DatePicker from "react-datepicker";
import "react-datepicker/dist/react-datepicker.css";
@@ -28,6 +32,7 @@ const widgetCls =
function PlaceholderType(props) {
const selectWidgetCls = `w-full h-full absolute left-0 top-0 focus:outline-none text-base-content`;
const { t } = useTranslation();
const textRef = useRef();
const prefillImg = useSelector((state) => state.widget.prefillImg);
const prefillImgLoad = useSelector((state) => state.widget.prefillImgLoad);
const type = props?.pos?.type;
@@ -42,10 +47,8 @@ function PlaceholderType(props) {
// prefer the latest response value over any default value
const widgetData =
props.pos?.options?.response ?? props.pos?.options?.defaultValue ?? "";
// const widgetTypeTranslation = t(`widgets-name.${props?.pos?.type}`);
const [widgetValue, setwidgetValue] = useState();
const [selectedCheckbox, setSelectedCheckbox] = useState([]);
// const [hint, setHint] = useState("");
const [imgUrl, setImgUrl] = useState("");
const fontSize = props.calculateFont(props.pos.options?.fontSize);
const fontColor = props.pos.options?.fontColor || "black";
@@ -72,11 +75,6 @@ function PlaceholderType(props) {
// keep displayed value in sync with the stored response
setwidgetValue(widgetData);
}
// if (props.pos?.options?.hint) {
// setHint(props.pos?.options.hint);
// } else if (props.pos?.options?.validation?.type) {
// checkRegularExpress(props.pos?.options?.validation?.type, setHint);
// }
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [props.pos, widgetData, type]);
@@ -138,6 +136,39 @@ function PlaceholderType(props) {
props?.pos?.type === name[0] ? `${name[0]}${lastWord}` : widgetName;
return props?.pos?.options?.hint || title;
};
//useEffect is used to increase auto height of text/textInput widget when user using multiline option and enter value in next line
useEffect(() => {
if (!textRef.current) return;
if (
(type === "text" || type === textInputWidget) &&
textRef.current &&
widgetValue
) {
const el = textRef.current;
// Count actual number of lines
const lines = widgetValue?.split("\n")?.length || props.pos?.Height;
// Get line height from computed style
const lineHeight = parseInt(window.getComputedStyle(el).lineHeight);
const textWidgetHeight = lines * lineHeight;
// Set widget box height (your logic)
onChangeInput(
null,
props.pos,
props?.xyPosition,
props.index,
props?.setXyPosition,
props.data && props.data.Id,
null,
null,
null,
null,
null,
textWidgetHeight
);
}
}, [widgetValue]);
switch (type) {
case "signature":
return props.pos.SignUrl ? (
@@ -230,17 +261,19 @@ function PlaceholderType(props) {
return props.isSignYourself || iswidgetEnable ? (
<textarea
placeholder={formatWidgetName()}
rows={1}
ref={textRef}
rows={50}
value={
widgetValue
}
className={`${textWidgetCls} ${isReadOnly ? "select-none" : ""}`}
className={`w-full resize-none overflow-hidden text-base-content item-center outline-none ${isReadOnly ? "select-none" : ""}`}
style={{
fontSize: fontSize,
color: fontColor,
background: isReadOnly ? props.data?.blockColor : "white",
pointerEvents: "none"
}}
name="text"
readOnly
disabled={props.isNeedSign && isReadOnly}
cols="50"
@@ -530,20 +563,22 @@ function PlaceholderType(props) {
case textWidget:
return (
<textarea
name="text"
ref={textRef}
readOnly
placeholder={t("widgets-name.text")}
rows={1}
value={
rows={50}
cols={50}
defaultValue={
widgetValue
}
className={textWidgetCls}
style={{
fontFamily: "Arial, sans-serif",
fontSize: fontSize,
color: fontColor,
background: "white"
}}
cols="50"
className="w-full resize-none overflow-hidden text-base-content item-center outline-none"
/>
);
default:
@@ -72,7 +72,7 @@ const ImageComponent = (props) => {
<div className="cursor-pointer op-card border-[1px] border-gray-400 flex flex-col w-full h-full justify-center items-center ">
<img
alt="print img"
ref={(el) => (imageRefs.current[props?.position.key] = el)} // Assign ref dynamicallys
ref={(el) => (imageRefs.current[props?.id] = el)} // Assign ref dynamicallys
src={imgUrl}
draggable="false"
className="object-contain h-full w-full aspect-[5/2]"
@@ -90,19 +90,14 @@ const ImageComponent = (props) => {
) : (
<div
className="cursor-pointer op-card border-[1px] op-border-hover flex flex-col overflow-hidden w-full h-full aspect-[5/2] justify-center items-center"
onClick={() => imageRefs.current[props?.position.key]?.click()}
onClick={() => imageRefs.current[props?.id]?.click()}
>
{props?.imageLoaders[props?.position?.key] && (
<div className="absolute w-full h-full inset-0 flex justify-center items-center bg-white/30 z-50">
<Loader />
</div>
)}
<input
type="file"
onChange={(e) => props?.onImageChange?.(e, props?.position)}
className="filetype"
accept="image/png,image/jpeg"
ref={(el) => (imageRefs.current[props?.position.key] = el)} // Assign ref dynamically
ref={(el) => (imageRefs.current[props?.id] = el)} // Assign ref dynamically
hidden
/>
<i className="fa-light text-base-content fa-cloud-upload-alt text-[25px]"></i>
@@ -120,16 +115,12 @@ function PrefillWidgetModal(props) {
const loadedSet = useRef(new Set());
const initializedRef = useRef(false); // prevent rerun on state updates
const [image, setImage] = useState(null);
const [imageLoaders, setImageLoaders] = useState({});
const [currentWidget, setCurrentWidget] = useState("");
const [userList, setUserList] = useState([]);
const [totalImages, setTotalImages] = useState(0);
const [loadedImages, setLoadedImages] = useState(0);
const [loading, setLoading] = useState(false);
const years = range(1950, getYear(new Date()) + 16, 1);
const isAnyLoaderActive = Object.values(imageLoaders).some(
(val) => val === true
);
// useMemo to memoize the calculation of unique widgets
const uniqueWidget = useMemo(() => {
@@ -179,7 +170,7 @@ function PrefillWidgetModal(props) {
if (Array.isArray(prefillImg)) {
prefillImg.forEach((img) => dispatch(setPrefillImg(img)));
}
setImageLoaders({});
setLoading(false);
};
savePrefillImg();
}, [props.xyPosition]);
@@ -271,7 +262,7 @@ function PrefillWidgetModal(props) {
};
const handleSavePrefillImg = async (widgetDetails) => {
setImageLoaders((prev) => ({ ...prev, [widgetDetails?.key]: true }));
setLoading(true);
try {
const imageName = generatePdfName(16);
const imageUrl = await convertBase64ToFile(
@@ -419,9 +410,9 @@ function PrefillWidgetModal(props) {
// (tracks how many images have finished loading)
setLoadedImages((prev) => prev + 1);
}
setImageLoaders({});
setLoading(false);
};
const handleWidgetType = (position) => {
const handleWidgetType = (position, id) => {
switch (position?.type) {
case "checkbox":
return (
@@ -533,10 +524,10 @@ function PrefillWidgetModal(props) {
return (
<ImageComponent
position={position}
imageLoaders={imageLoaders}
onImageChange={onImageChange}
handleImageLoaded={handleImageLoaded}
handleClearImage={handleClearImage}
id={id}
/>
);
case radioButtonWidget:
@@ -658,7 +649,7 @@ function PrefillWidgetModal(props) {
<div className="grid grid-cols-1 md:grid-cols-2 gap-x-10 gap-y-4 w-full">
{uniqueWidget.map((x, id) => (
<div key={id} className="flex flex-col gap-2 w-full">
{handleWidgetType(x.widget)}
{handleWidgetType(x.widget, id)}
</div>
))}
</div>
@@ -727,7 +718,7 @@ function PrefillWidgetModal(props) {
</div>
<div className="flex gap-2 mx-4 mb-3">
<button
disabled={isAnyLoaderActive || props?.isSubmit}
disabled={props?.isSubmit}
className="op-btn op-btn-primary op-btn-sm w-[80px]"
onClick={() => handleEmbedPrefill(props?.item)}
>
@@ -222,6 +222,7 @@ const WidgetNameModal = (props) => {
)
);
};
return (
<ModalUi
isOpen={props.isOpen}
@@ -373,7 +374,7 @@ const WidgetNameModal = (props) => {
<label htmlFor="signaturetype" className="text-[14px] mb-[0.7rem]">
{t("allowed-signature-types")}
</label>
<div className=" ml-[7px] flex flex-col md:flex-row gap-[10px] mb-[0.7rem]">
<div className="ml-[7px] flex flex-col md:flex-row gap-[10px] mb-[0.7rem]">
{signatureType.map((type, i) => {
return (
<div key={i} className="flex flex-row gap-[5px] items-center">
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,17 @@
function DefaultSignature(props) {
return (
<div className="flex justify-center">
<div
className={`${props?.currWidgetsDetails?.type === "initials" ? "intialSignatureCanvas" : "signatureCanvas"} border-[1.3px] border-gray-300 rounded-[4px]`}
>
<img
src={props?.defaultSignImg}
draggable="false"
className="w-full h-full object-contain"
/>
</div>
</div>
);
}
export default DefaultSignature;
@@ -0,0 +1,21 @@
import SignatureCanvas from "react-signature-canvas";
function Draw(props) {
return (
<div className="flex justify-center">
<SignatureCanvas
ref={props?.canvasRef}
penColor={props?.penColor}
canvasProps={{
className: `${props?.currWidgetsDetails?.type === "initials" ? "intialSignatureCanvas" : "signatureCanvas"} border-[1.3px] border-gray-300 rounded-[4px]`
}}
onEnd={() =>
props?.handleSignatureChange(props?.canvasRef.current?.toDataURL())
}
dotSize={1}
/>
</div>
);
}
export default Draw;
@@ -0,0 +1,57 @@
import { useMemo } from "react";
import { useTranslation } from "react-i18next";
const ALLOWED_COLORS = ["blue", "red", "black"];
const COLOR_CLASS = {
blue: "text-blue-600",
red: "text-red-600",
black: "text-black",
white: "text-white"
};
function PenColorComponent({
providedColors,
penColor,
convertToImg,
fontSelect,
typedSignature,
setPenColor
}) {
const { t } = useTranslation();
const pensList = useMemo(() => {
const allowed = new Set(ALLOWED_COLORS);
const filtered = Array.isArray(providedColors)
? providedColors
.map((c) => String(c).toLowerCase())
.filter((c) => allowed.has(c))
: [];
// remove duplicates while preserving order
return filtered.length ? [...new Set(filtered)] : ALLOWED_COLORS;
}, [providedColors]);
return (
<div className="flex flex-row items-center m-[5px] gap-3">
<span className="text-base-content">{t("options")}</span>
{pensList.map((color) => {
const selected = penColor === color;
return (
<i
key={color}
role="button"
tabIndex={0}
aria-label={`Select ${color} pen`}
onClick={() => {
setPenColor?.(color);
convertToImg?.(fontSelect, typedSignature, color);
}}
className={`${COLOR_CLASS[color] || "text-base-content"} ${selected ? "border-current" : "border-white"} border-b-[2px] pb-0.5 cursor-pointer text-[16px] fa-light fa-pen-nib`}
></i>
);
})}
</div>
);
}
export default PenColorComponent;
@@ -0,0 +1,65 @@
import { useTranslation } from "react-i18next";
function TypeSignature(props) {
const { t } = useTranslation();
return (
<div>
<div className="flex justify-between items-center tabWidth rounded-[4px]">
<span className="ml-[5px] text-[12px] text-base-content">
{props?.currWidgetsDetails?.type === "initials"
? t("initial-teb")
: t("signature-tab")}
:
</span>
<input
maxLength={props?.currWidgetsDetails?.type === "initials" ? 3 : 30}
style={{ fontFamily: props?.fontSelect, color: props?.penColor }}
type="text"
className="bg-transparent ml-1 op-input op-input-bordered op-input-sm focus:outline-none hover:border-base-content w-full text-[20px]"
placeholder={
props?.currWidgetsDetails?.type === "initials"
? t("initial-type")
: t("signature-type")
}
value={props?.typedSignature}
onChange={(e) => {
props?.setTypedSignature(e.target.value);
if (e.target.value?.trim()?.length > 0) {
props?.convertToImg(props?.fontSelect, e.target.value);
}
}}
/>
</div>
<div className="border-[1px] border-[#d6d3d3] mt-[10px] rounded-[4px] tabWidth">
{props?.fontOptions.map((font, ind) => {
return (
<div
key={ind}
style={{
cursor: "pointer",
fontFamily: font.value,
backgroundColor:
props?.fontSelect === font.value && "rgb(206 225 247)"
}}
onClick={() => {
props?.setFontSelect(font.value);
props?.convertToImg(font.value, props?.typedSignature);
}}
>
<div
className="py-[5px] px-[10px] text-[20px]"
style={{ color: props?.penColor }}
>
{props?.typedSignature
? props?.typedSignature
: t("Your-Signature")}
</div>
</div>
);
})}
</div>
</div>
);
}
export default TypeSignature;
@@ -0,0 +1,58 @@
import React, { useRef } from "react";
import { useTranslation } from "react-i18next";
function UploadImage(props) {
const { t } = useTranslation();
const imageRefs = useRef({});
const getImageRef = (key) => {
if (!imageRefs.current[key]) {
imageRefs.current[key] = React.createRef();
}
return imageRefs.current[key];
};
return (
<div>
{(props?.isImageSelect || props?.isStampOrImage) && !props?.image ? (
<div className="flex justify-center">
<div
className={`${props?.currWidgetsDetails?.type === "initials" ? "intialSignatureCanvas" : "signatureCanvas"} bg-white border-[1.3px] border-gray-300 flex flex-col justify-center items-center mb-[6px] cursor-pointer`}
onClick={() =>
getImageRef(props?.currWidgetsDetails?.key).current.click()
}
>
<input
type="file"
onChange={props?.onImageChange}
className="filetype"
accept="image/png,image/jpeg"
ref={getImageRef(props?.currWidgetsDetails?.key)}
hidden
/>
<i className="fa-light fa-cloud-upload-alt uploadImgLogo text-base-content"></i>
<div className="text-[10px] text-base-content">{t("upload")}</div>
</div>
</div>
) : (
<>
<div className="flex justify-center">
<div
className={`${props?.currWidgetsDetails?.type === "initials" ? "intialSignatureCanvas" : "signatureCanvas"} bg-white border-[1.3px] border-gray-300 mb-[6px] overflow-hidden`}
>
<img
alt="print img"
ref={getImageRef(props?.currWidgetsDetails?.key)}
src={props?.image?.src}
draggable="false"
className="object-contain h-full w-full"
/>
</div>
</div>
</>
)}
</div>
);
}
export default UploadImage;
@@ -0,0 +1,82 @@
import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { Tooltip } from "react-tooltip";
//This file is for both Text and textInput widgets
export default function TextInput(props) {
const [isMultiline, setIsMultiline] = useState();
const { t } = useTranslation();
const handleKeyDown = (e) => {
if (!isMultiline && e.key === "Enter") {
e.preventDefault(); // disable new line in single-line mode
}
};
useEffect(() => {
if (props?.widgetValue) {
const isNextLine = props?.widgetValue.includes("\n");
if (isNextLine) {
setIsMultiline(true);
}
}
}, [props?.widgetValue]);
return (
<div style={{ position: "relative", width: "100%" }}>
{isMultiline ? (
<textarea
onBlur={props?.handleInputBlur}
ref={props?.widgetRef}
value={props?.widgetValue ?? ""} //use an empty string "" as a default
onChange={(e) => props?.handleOnchangeTextBox(e)}
onKeyDown={handleKeyDown}
rows={3}
placeholder={t("widgets-name.text")}
// className={`${textInputcls} resize-none outline-none max-h-[100px] `}
style={{
width: "100%",
padding: "12px 40px 12px 12px",
borderRadius: "20px",
border: "1px solid #dcdcdc",
fontSize: "16px",
resize: "none",
outline: "none"
}}
/>
) : (
<input
name="text"
ref={props?.widgetRef}
type="text"
placeholder={t("widgets-name.text")}
value={props?.widgetValue ?? ""} //use an empty string "" as a default
onChange={(e) => props?.handleOnchangeTextBox(e)}
className={`${props?.textInputcls} pr-4`}
onBlur={props?.handleInputBlur}
/>
)}
{!isMultiline && (
<>
<i
data-tooltip-id="toggle-multiline"
className="fa-light fa-file-lines"
size={20}
style={{
position: "absolute",
right: "12px",
top: "50%",
transform: "translateY(-50%)",
color: "#444",
cursor: "pointer"
}}
onClick={() => setIsMultiline((prev) => !prev)}
></i>
<Tooltip id={"toggle-multiline"}>
<span>Toggle Multiline Text</span>
</Tooltip>
</>
)}
</div>
);
}
@@ -1,5 +1,5 @@
import React from "react";
import { useTranslation } from "react-i18next";
import { useSelector } from "react-redux";
import { NavLink } from "react-router";
const Menu = ({ item, isOpen, closeSidebar }) => {
@@ -7,6 +7,8 @@ const Menu = ({ item, isOpen, closeSidebar }) => {
"OpenSign™";
const drivename = appName === "OpenSign™" ? "OpenSign™" : "";
const { t } = useTranslation();
const { selectedMenu } = useSelector((state) => state.sidebar);
return (
<li key={item.title} role="none" className="my-0.5">
<NavLink
@@ -16,11 +18,9 @@ const Menu = ({ item, isOpen, closeSidebar }) => {
: `/${item.objectId}`
}
className={({ isActive }) =>
`${
isActive ? " bg-base-300 text-base-content" : ""
} flex gap-x-5 items-center justify-start text-left p-3 text-base-content hover:text-base-content focus:bg-base-300 hover:bg-base-300 hover:no-underline focus:outline-none`
`${isActive && selectedMenu ? "bg-base-300 text-base-content" : ""} flex gap-x-5 items-center justify-start text-left p-3 text-base-content hover:text-base-content focus:bg-base-300 hover:bg-base-300 hover:no-underline focus:outline-none`
}
onClick={closeSidebar}
onClick={() => closeSidebar(item.title)}
tabIndex={isOpen ? 0 : -1}
role="menuitem"
>
@@ -7,7 +7,10 @@ import sidebarList, { subSetting } from "../../json/menuJson";
import { useNavigate } from "react-router";
import { useDispatch, useSelector } from "react-redux";
import { useWindowSize } from "../../hook/useWindowSize";
import { toggleSidebar } from "../../redux/reducers/sidebarReducer";
import {
setSelectedMenu,
toggleSidebar
} from "../../redux/reducers/sidebarReducer";
const Sidebar = () => {
const { width } = useWindowSize();
@@ -29,6 +32,7 @@ const Sidebar = () => {
}, []);
const closeSidebar = () => {
dispatch(setSelectedMenu(true));
if (width <= 1023) {
dispatch(toggleSidebar(false));
}
@@ -58,10 +62,12 @@ const Sidebar = () => {
};
const toggleSubmenu = (title) => {
dispatch(setSelectedMenu(false));
setSubmenuOpen({ [title]: !submenuOpen[title] });
};
const handleMenuItem = () => {
dispatch(setSelectedMenu(true));
closeSidebar();
setSubmenuOpen({});
};
@@ -1,5 +1,5 @@
import React from "react";
import { useTranslation } from "react-i18next";
import { useSelector } from "react-redux";
import { NavLink } from "react-router";
const Submenu = ({ item, closeSidebar, toggleSubmenu, submenuOpen }) => {
@@ -8,6 +8,8 @@ const Submenu = ({ item, closeSidebar, toggleSubmenu, submenuOpen }) => {
const drivename = appName === "OpenSign™" ? "OpenSign™" : "";
const { t } = useTranslation();
const { title, icon, children } = item;
const { selectedMenu } = useSelector((state) => state.sidebar);
return (
<li role="none" className="my-0.5">
<button
@@ -45,11 +47,9 @@ const Submenu = ({ item, closeSidebar, toggleSubmenu, submenuOpen }) => {
: `/${childItem.objectId}`
}
className={({ isActive }) =>
`${
isActive ? "bg-base-300 text-base-content " : ""
} pl-4 flex items-center gap-x-5 py-2 text-sm cursor-pointer text-base-content hover:text-base-content focus:bg-base-300 hover:bg-base-300 hover:no-underline focus:outline-none`
`${isActive && selectedMenu ? "bg-base-300 text-base-content" : ""} pl-4 flex items-center gap-x-5 py-2 text-sm cursor-pointer text-base-content hover:text-base-content focus:bg-base-300 hover:bg-base-300 hover:no-underline focus:outline-none`
}
onClick={closeSidebar}
onClick={() => closeSidebar(childItem.title)}
role="menuitem"
tabIndex={submenuOpen ? 0 : -1}
>
+174 -91
View File
@@ -28,6 +28,11 @@ const appName = "OpenSign™";
export const defaultMailBody = `<p>Hi {{receiver_name}},</p><br><p>We hope this email finds you well. {{sender_name}}&nbsp;has requested you to review and sign&nbsp;{{document_title}}.</p><p>Your signature is crucial to proceed with the next steps as it signifies your agreement and authorization.</p><br><p><a href='{{signing_url}}' rel='noopener noreferrer' target='_blank'>Sign here</a></p><br><br><p>If you have any questions or need further clarification regarding the document or the signing process, please contact the sender.</p><br><p>Thanks</p><p> Team ${appName}</p><br>`;
export const defaultMailSubject = `{{sender_name}} has requested you to sign {{document_title}}`;
export const nonPresentMaskCss = (base) => ({
...base,
width: "0px",
height: "0px"
});
export const randomId = (digit = 8) => {
// 1. Grab a cryptographically-secure 32-bit random value
@@ -849,6 +854,7 @@ export const signPdfFun = async (
}
}
};
export const createDocument = async (
template,
placeholders,
@@ -900,6 +906,8 @@ export const createDocument = async (
const RedirectUrl = Doc?.RedirectUrl
? { RedirectUrl: Doc?.RedirectUrl }
: {};
const PenColors =
Doc?.PenColors?.length > 0 ? { PenColors: Doc?.PenColors } : {};
const TemplateId = Doc?.objectId
? {
TemplateId: {
@@ -922,11 +930,7 @@ export const createDocument = async (
className: "contracts_Users",
objectId: extUserId
},
CreatedBy: {
__type: "Pointer",
className: "_User",
objectId: creatorId
},
CreatedBy: { __type: "Pointer", className: "_User", objectId: creatorId },
Signers: signers,
SendinOrder: Doc?.SendinOrder || false,
AutomaticReminders: Doc?.AutomaticReminders || false,
@@ -940,7 +944,8 @@ export const createDocument = async (
...NotifyOnSignatures,
...Bcc,
...RedirectUrl,
...TemplateId
...TemplateId,
...PenColors
};
const remindOnceInEvery = Doc?.RemindOnceInEvery;
const TimeToCompleteDays = parseInt(Doc?.TimeToCompleteDays);
@@ -949,23 +954,27 @@ export const createDocument = async (
if (AutomaticReminders && reminderCount > 15) {
return { status: "error", id: "only-15-reminder-allowed" };
}
const url = `${localStorage.getItem("baseUrl")}classes/contracts_Document`;
const token = {
"X-Parse-Session-Token": localStorage.getItem("accesstoken")
};
try {
const res = await axios.post(
`${localStorage.getItem("baseUrl")}classes/contracts_Document`,
data,
{
headers: {
"Content-Type": "application/json",
"X-Parse-Application-Id": localStorage.getItem("parseAppId"),
"X-Parse-Session-Token": localStorage.getItem("accesstoken")
}
const res = await axios.post(url, data, {
headers: {
"Content-Type": "application/json",
"X-Parse-Application-Id": localStorage.getItem("parseAppId"),
...token
}
);
});
if (res) {
return { status: "success", id: res.data.objectId, data: res.data };
const result = res.data;
return { status: "success", id: result.objectId, data: result };
}
} catch (err) {
console.log("axois err ", err);
const message =
err?.response?.data?.error || err?.message || "something went wrong.";
console.error("error in create document:", message);
return { status: "error", id: "something-went-wrong-mssg" };
}
}
@@ -1037,7 +1046,8 @@ export const onChangeInput = (
dateFormat,
fontSize,
fontColor,
dateDetails
dateDetails,
textWidgetHeight
) => {
const isSigners = xyPosition.some(
(data) => data.signerPtr || data.Role === "prefill"
@@ -1087,6 +1097,16 @@ export const onChangeInput = (
}
}
};
} else if (
(currentPosition?.type === "text" ||
currentPosition?.type === textInputWidget) &&
textWidgetHeight &&
!value
) {
return {
...position,
Height: textWidgetHeight
};
} else {
return {
...position,
@@ -1137,6 +1157,15 @@ export const onChangeInput = (
}
}
};
} else if (
currentPosition?.type === "text" &&
textWidgetHeight &&
!value
) {
return {
...positionData,
Height: textWidgetHeight
};
} else {
return {
...positionData,
@@ -1605,7 +1634,9 @@ export function onSaveImage(
isAutoSign,
widgetsType,
isApplyAll,
imgUrl
imgUrl,
defaultStampImg,
defaultStampType
) {
// let getIMGWH;
const isSignOrInitials =
@@ -1616,12 +1647,12 @@ export function onSaveImage(
if (position.key === signKey) {
return {
...position,
SignUrl: imgUrl || image.src,
ImageType: image.imgType,
SignUrl: imgUrl || image?.src || defaultStampImg,
ImageType: image?.imgType || defaultStampType,
...(isSignOrInitials && { signatureType: signatureType || "" }),
options: {
...position.options,
response: imgUrl || image.src
response: imgUrl || image?.src || defaultStampImg
}
};
}
@@ -1643,16 +1674,17 @@ export function onSaveImage(
item.type === widgetsType && item.type !== "image"
? {
...item,
SignUrl: image.src,
ImageType: image.imgType,
SignUrl: image?.src || defaultStampImg,
ImageType: image?.imgType || defaultStampType,
options: {
...item.options,
response: image.src
response: image?.src || defaultStampImg
}
}
: item // Otherwise, keep it unchanged
)
}));
return updatedArray;
} else {
return updateXYposition;
@@ -1792,6 +1824,26 @@ export const isBase64 = (str) => {
/^data:([a-zA-Z0-9]+\/[a-zA-Z0-9-.+]+)?;base64,[A-Za-z0-9+/=]+$/;
return base64Pattern.test(str);
};
// Break a long word into smaller parts character-by-character
const forceBreakLongWord = (word, width, font, fontSize) => {
const parts = [];
let current = "";
for (const char of word) {
const lineWidth = font.widthOfTextAtSize(current + char, fontSize);
if (lineWidth <= width) {
current += char;
} else {
parts.push(current);
current = char;
}
}
if (current) parts.push(current);
return parts;
};
//function for embed all type widgets in document using pdf-lib
export const embedWidgetsToDoc = async (
widgets,
@@ -2080,41 +2132,59 @@ export const embedWidgetsToDoc = async (
const lines = [];
let currentLine = "";
for (const word of textContent?.split(" ")) {
//get text line width
const lineWidth = font.widthOfTextAtSize(
`${currentLine} ${word}`,
fontSize
);
//check text content line width is less or equal to container width
if (lineWidth <= width) {
currentLine += ` ${word}`;
const words = textContent?.split(" ");
for (let word of words) {
const testLine = currentLine ? currentLine + " " + word : word;
const testWidth = font.widthOfTextAtSize(testLine, fontSize);
if (testWidth <= width) {
currentLine = testLine;
} else {
lines.push(currentLine.trim());
currentLine = `${word}`;
// If single long word
if (font.widthOfTextAtSize(word, fontSize) > width) {
// Break a long word into smaller parts character-by-character
const brokenParts = forceBreakLongWord(
word,
width,
font,
fontSize
);
// push currentLine before breaking word
if (currentLine.trim()) lines.push(currentLine.trim());
// push ALL broken parts in order
brokenParts.forEach((p) => lines.push(p));
currentLine = "";
} else {
// push current line and start new
if (currentLine.trim()) lines.push(currentLine.trim());
currentLine = word;
}
}
}
lines.push(currentLine.trim());
if (currentLine.trim()) lines.push(currentLine.trim());
return lines;
};
// Function to break text into lines based on when user go next line on press enter button
const breakTextIntoLines = (textContent, width) => {
const lines = [];
for (const word of textContent.split("\n")) {
const lineWidth = font.widthOfTextAtSize(`${word}`, fontSize);
const finalLines = [];
const paragraphs = textContent.split("\n"); // preserve user order
for (const para of paragraphs) {
const lineWidth = font.widthOfTextAtSize(para, fontSize);
//checking string length to container width
//if string length is less then container width it means user press enter button
if (lineWidth <= width) {
lines.push(word);
}
//else adjust text content according to width and send it in new line
else {
const newLine = NewbreakTextIntoLines(word, width);
lines.push(...newLine);
// user forced newline
finalLines.push(para);
} else {
// auto wrap paragraph
finalLines.push(...NewbreakTextIntoLines(para, width));
}
}
return lines;
return finalLines;
};
//check if text content have `\n` string it means user press enter to go next line and handle condition
//else auto adjust text content according to container width
@@ -3022,13 +3092,17 @@ export const getDefaultSignature = async (objectId) => {
const defaultInitial = res?.Initials
? await getBase64FromUrl(res?.Initials, true)
: "";
const defaultStamp = res?.Stamp
? await getBase64FromUrl(res?.Stamp, true)
: "";
return {
status: "success",
res: {
id: result?.id,
defaultSignature: defaultSignature,
defaultInitial: defaultInitial
defaultInitial: defaultInitial,
defaultStamp: defaultStamp
}
};
}
@@ -3927,51 +4001,55 @@ export const sendEmailToSigners = async (
* @returns {Promise<File>} - A promise that resolves with a new PNG File.
*/
export function convertJpegToPng(base64Image, filename) {
const arr = base64Image.split(",");
const mimeMatch = arr[0].match(/:(.*?);/);
if (!mimeMatch) throw new Error("Invalid dataURL");
const mime = mimeMatch[1];
const type = mime.split("/")[1];
if (type === "png") {
return base64Image;
} else {
const bstr = atob(arr[1]);
let n = bstr.length;
const u8arr = new Uint8Array(n);
while (n--) u8arr[n] = bstr.charCodeAt(n);
const inputFile = new File([u8arr], `${filename}.${type}`, { type: mime });
return new Promise((resolve, reject) => {
// ensure its JPEG/JPG (you can remove this check if not needed)
if (!/image\/jpe?g/.test(inputFile.type)) {
return reject(new Error("Input must be a JPEG or JPG image"));
}
if (base64Image) {
const arr = base64Image.split(",");
const mimeMatch = arr[0].match(/:(.*?);/);
if (!mimeMatch) throw new Error("Invalid dataURL");
const mime = mimeMatch[1];
const type = mime.split("/")[1];
if (type === "png") {
return base64Image;
} else {
const bstr = atob(arr[1]);
let n = bstr.length;
const u8arr = new Uint8Array(n);
while (n--) u8arr[n] = bstr.charCodeAt(n);
const inputFile = new File([u8arr], `${filename}.${type}`, {
type: mime
});
return new Promise((resolve, reject) => {
// ensure its JPEG/JPG (you can remove this check if not needed)
if (!/image\/jpe?g/.test(inputFile.type)) {
return reject(new Error("Input must be a JPEG or JPG image"));
}
// Read the file as a data URL
const reader = new FileReader();
reader.onerror = () => reject(new Error("Failed to read the file"));
reader.onload = () => {
const img = new Image();
img.onerror = () => reject(new Error("Failed to load image"));
img.onload = () => {
// draw image onto a canvas
const canvas = document.createElement("canvas");
canvas.width = img.width;
canvas.height = img.height;
const ctx = canvas.getContext("2d");
ctx.drawImage(img, 0, 0);
// Read the file as a data URL
const reader = new FileReader();
reader.onerror = () => reject(new Error("Failed to read the file"));
reader.onload = () => {
const img = new Image();
img.onerror = () => reject(new Error("Failed to load image"));
img.onload = () => {
// draw image onto a canvas
const canvas = document.createElement("canvas");
canvas.width = img.width;
canvas.height = img.height;
const ctx = canvas.getContext("2d");
ctx.drawImage(img, 0, 0);
// directly get PNG as base64 data URL
try {
const pngDataUrl = canvas.toDataURL("image/png");
resolve(pngDataUrl);
} catch (err) {
reject(new Error("Failed to convert canvas to PNG"));
}
// directly get PNG as base64 data URL
try {
const pngDataUrl = canvas.toDataURL("image/png");
resolve(pngDataUrl);
} catch (err) {
reject(new Error("Failed to convert canvas to PNG"));
}
};
img.src = reader.result;
};
img.src = reader.result;
};
reader.readAsDataURL(inputFile);
});
reader.readAsDataURL(inputFile);
});
}
}
}
//function is used to get assigned signer's email
@@ -4080,3 +4158,8 @@ export const handleBackground = (data, isNeedSign, uniqueId) => {
return "rgba(203, 233, 237, 0.69)";
}
};
export function getBase64MimeType(base64) {
const match = base64.match(/^data:(image\/[a-zA-Z0-9+.-]+);base64,/);
return match ? match[1] : null;
}
+4 -4
View File
@@ -5,19 +5,19 @@ export const templateReportTour = [
selector: "[data-tut=reactourFirst]",
content: <Trans i18nKey="tour-mssg.report-1" />,
position: "top",
style: { fontSize: "13px" }
styles: { fontSize: "11px" }
},
{
selector: "[data-tut=reactourSecond]",
content: <Trans i18nKey="tour-mssg.redirect" />,
position: "top",
style: { fontSize: "13px" }
styles: { fontSize: "13px" }
},
{
selector: "[data-tut=tourbulksend]",
content: <Trans i18nKey="tour-mssg.bulksend" />,
position: "top",
style: { fontSize: "13px" }
styles: { fontSize: "13px" }
},
{
selector: "[data-tut=reactourThird]",
@@ -42,6 +42,6 @@ export const templateReportTour = [
</Trans>
),
position: "top",
style: { fontSize: "13px" }
styles: { fontSize: "13px" }
}
];
+30 -31
View File
@@ -1,4 +1,7 @@
import { useState, useEffect } from "react";
import {
nonPresentMaskCss
} from "../constant/Utils";
import Header from "../components/Header";
import Footer from "../components/Footer";
import Sidebar from "../components/sidebar/Sidebar";
@@ -6,7 +9,9 @@ import Tour from "../primitives/Tour";
import axios from "axios";
import { useDispatch, useSelector } from "react-redux";
import Parse from "parse";
import { useNavigate, Outlet } from "react-router";
import {
Outlet
} from "react-router";
import Loader from "../primitives/Loader";
import { useTranslation } from "react-i18next";
import { sessionStatus } from "../redux/reducers/userReducer";
@@ -16,9 +21,8 @@ const HomeLayout = () => {
const appName =
"OpenSign™";
const { t, i18n } = useTranslation();
const navigate = useNavigate();
const dispatch = useDispatch();
const arr = useSelector((state) => state.TourSteps);
const tourArr = useSelector((state) => state.TourSteps);
const isValidSession = useSelector((state) => state.user.isValidSession);
const [isLoader, setIsLoader] = useState(true);
const [isCloseBtn, setIsCloseBtn] = useState(true);
@@ -55,7 +59,7 @@ const HomeLayout = () => {
dispatch(sessionStatus(true));
}
} catch (error) {
console.log("catch");
console.error("error in authentication:", error?.message);
// Session token is invalid or there was an error
dispatch(sessionStatus(false));
}
@@ -68,40 +72,36 @@ const HomeLayout = () => {
useEffect(() => {
if (arr && arr.length > 0) {
if (tourArr && tourArr.length > 0) {
handleDynamicSteps();
} else {
setIsTour(false);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [arr]);
}, [tourArr]);
const handleDynamicSteps = () => {
const github = "https://github.com/OpenSignLabs/OpenSign";
if (arr && arr.length > 0) {
// const resArr = arr;
const resArr = arr.map((obj, index) => {
if (arr.length - 1 === index) {
return { ...obj };
if (tourArr && tourArr.length > 0) {
const resArr = tourArr.map((obj, index) => {
if (tourArr.length - 1 === index) {
return obj;
} else {
return {
...obj,
actions: () => {
setIsCloseBtn(false);
}
};
return { ...obj, actions: () => setIsCloseBtn(false) };
}
});
setTourConfigs([
{
selector: '[data-tut="nonpresentmask"]',
content: t("tour-mssg.home-layout-1"),
position: "center"
position: "center",
styles: { fontSize: "13px", maskArea: nonPresentMaskCss }
},
{
selector: '[data-tut="tourbutton"]',
content: t("tour-mssg.home-layout-2"),
position: "top"
position: "top",
styles: { fontSize: "13px" }
},
...resArr,
{
@@ -122,20 +122,19 @@ const HomeLayout = () => {
</p>
</div>
),
position: "center"
position: "center",
styles: { fontSize: "13px", maskArea: nonPresentMaskCss }
}
]);
checkTourStatus();
}
};
const closeTour = async () => {
// console.log("closeTour");
setIsTour(false);
const serverUrl = localStorage.getItem("baseUrl");
const appId = localStorage.getItem("parseAppId");
const json = JSON.parse(localStorage.getItem("Extand_Class"));
const extUserId = json && json.length > 0 && json[0].objectId;
// console.log("extUserId ", extUserId)
let updatedTourStatus = [];
if (tourStatusArr.length > 0) {
@@ -209,15 +208,15 @@ const HomeLayout = () => {
</div>
</main>
</div>
<Tour
onRequestClose={closeTour}
steps={tourConfigs}
isOpen={isTour}
disableKeyboardNavigation={["esc"]}
// disableInteraction={true}
scrollOffset={-100}
showCloseButton={isCloseBtn}
/>
{isTour && (
<Tour
onRequestClose={closeTour}
steps={tourConfigs}
isOpen={isTour}
// scrollOffset={-100}
showCloseButton={isCloseBtn}
/>
)}
</>
)}
</div>
+2 -1
View File
@@ -42,7 +42,8 @@ const Dashboard = () => {
return {
selector: `[data-tut=${col.widget.data.tourSection}]`,
content: t(`tour-mssg.${col.widget.label}`),
position: "top"
position: "top",
styles: { fontSize: "13px" }
// style: { backgroundColor: "#abd4d2" },
};
});
+60
View File
@@ -84,6 +84,8 @@ const Forms = (props) => {
const [isCorrectPass, setIsCorrectPass] = useState(true);
const [isAdvanceOpt, setIsAdvanceOpt] = useState(false);
const [bcc, setBcc] = useState([]);
const pensList = ["blue", "red", "black"];
const [selectedColors, setSelectedColors] = useState(pensList);
const handleStrInput = (e) => {
setIsCorrectPass(true);
@@ -417,6 +419,8 @@ const Forms = (props) => {
object.set("RemindOnceInEvery", remindOnceInEvery);
object.set("IsTourEnabled", isTourEnabled);
object.set("TimeToCompleteDays", TimeToCompleteDays);
object.set("PenColors", selectedColors);
object.set("AllowModifications", false);
object.set("IsEnableOTP", false);
if (formData.NotifyOnSignatures !== undefined) {
@@ -456,6 +460,7 @@ const Forms = (props) => {
if (res) {
setSigners([]);
setBcc([]);
setSelectedColors(pensList);
setFolder({ ObjectId: "", Name: "" });
const notifySign =
extUserData?.NotifyOnSignatures !== undefined
@@ -538,6 +543,7 @@ const Forms = (props) => {
setIsReset(true);
setSigners([]);
setBcc([]);
setSelectedColors(pensList);
setFolder({ ObjectId: "", Name: "" });
const notifySign =
extUserData?.NotifyOnSignatures !== undefined
@@ -651,6 +657,20 @@ const Forms = (props) => {
setFormData((obj) => ({ ...obj, NotifyOnSignatures: value }));
};
const handleColorsChange = (color) => {
setSelectedColors((prev) => {
// If user tries to uncheck the last remaining color → block it
if (prev.length === 1 && prev.includes(color)) {
return prev;
}
// Normal toggle behavior
return prev.includes(color)
? prev.filter((c) => c !== color) // remove
: [...prev, color]; // add
});
};
return (
<div
className={`${isSubmit ? "" : "shadow-md rounded-box my-[2px] p-3 bg-base-100 text-base-content"}`}
@@ -1214,6 +1234,46 @@ const Forms = (props) => {
onInput={(e) => e.target.setCustomValidity("")}
/>
</div>
{props.title !== "Sign Yourself" && (
<div className="text-xs mt-3">
<label htmlFor="penColors">
{t("pen-colors")}
<a
data-tooltip-id="pen-colors-tooltip"
className="ml-1"
>
<sup>
<i className="fa-light fa-question rounded-full border-[#33bbff] text-[#33bbff] text-[13px] border-[1px] py-[1.5px] px-[4px]"></i>
</sup>
</a>
<Tooltip id="pen-colors-tooltip" className="z-[999]">
<div className="max-w-[200px] md:max-w-[450px]">
<p className="font-bold">{t("pen-colors")}</p>
<div>{t("pen-colors-help")}</div>
</div>
</Tooltip>
</label>
<div className="ml-[7px] flex flex-col md:flex-row gap-[10px] mb-[0.7rem]">
{pensList.map((color) => (
<div
key={color}
className="flex flex-row gap-[5px] items-center"
>
<input
className="mr-[2px] op-checkbox op-checkbox-xs"
type="checkbox"
name="penColors"
checked={selectedColors.includes(color)}
onChange={() => handleColorsChange(color)}
/>
<div className="hover:underline underline-offset-2 cursor-default capitalize">
{color}
</div>
</div>
))}
</div>
</div>
)}
</div>
)}
</div>
+116 -20
View File
@@ -13,6 +13,14 @@ import Alert from "../primitives/Alert";
import Loader from "../primitives/Loader";
import { useTranslation } from "react-i18next";
import { sanitizeFileName } from "../utils";
const COLOR_CLASS = {
blue: "text-blue-600",
red: "text-red-600",
black: "text-black",
white: "text-white"
};
const ManageSign = () => {
const { t } = useTranslation();
const [penColor, setPenColor] = useState("blue");
@@ -29,10 +37,12 @@ const ManageSign = () => {
const [isInitials, setIsInitials] = useState(false);
const [id, setId] = useState("");
const [imgInitials, setImgInitials] = useState("");
const [stamp, setStamp] = useState();
const canvasRef = useRef(null);
const imageRef = useRef(null);
const initailsRef = useRef(null);
const imgInitialsRef = useRef(null);
const stampRef = useRef(null);
useEffect(() => {
fetchUserSign();
// eslint-disable-next-line
@@ -63,6 +73,9 @@ const ManageSign = () => {
setIsInitials(true);
setImgInitials(res?.Initials);
}
if (res && res?.Stamp) {
setStamp(res?.Stamp);
}
} else {
if (User?.get("name")) {
const sanitizename = generateTitleFromFilename(User?.get("name"));
@@ -171,11 +184,24 @@ const ManageSign = () => {
} else {
initialsUrl = imgInitials;
}
const isStampUrl = stamp?.includes("https") || stamp?.includes("http");
let stampFile;
if (stamp && !isStampUrl) {
stampFile = base64StringtoFile(stamp, `${replaceSpace}_stamp`);
}
let stampUrl;
if (stampFile && !isStampUrl) {
stampUrl = await uploadFile(stampFile);
} else {
stampUrl = stamp;
}
if (imgUrl) {
await saveEntry({
name: signName,
url: imgUrl,
initialsUrl: initialsUrl
initialsUrl: initialsUrl,
stampUrl: stampUrl
});
}
}
@@ -232,7 +258,8 @@ const ManageSign = () => {
userId: User,
initials: obj.initialsUrl,
id: id,
title: obj.name
title: obj.name,
stamp: obj?.stampUrl
});
setIsAlert({ type: "success", message: t("signature-saved-alert") });
return res;
@@ -274,6 +301,28 @@ const ManageSign = () => {
setIsValue(false);
}
};
const handleClearStamp = () => {
if (stampRef.current) {
stampRef.current.clear();
}
if (stampRef.current) {
stampRef.current.value = "";
}
setStamp("");
if (image) {
setIsValue(true);
}
};
const onStampChange = async (event) => {
if (event.target.files && event.target.files[0]) {
const file = event.target.files[0];
const base64Img = await toDataUrl(file);
setStamp(base64Img);
setIsValue(true);
} else {
setStamp("");
}
};
return (
<div className="relative h-full bg-base-100 text-base-content flex shadow-md rounded-box overflow-auto">
{isLoader && (
@@ -287,9 +336,9 @@ const ManageSign = () => {
<div className="text-[20px] font-semibold m-[10px] md:m-0 mb-2">
{t("my-signature")}
</div>
<div className="flex flex-col md:flex-row gap-0 md:gap-[12px]">
<div className="flex flex-col md:flex-row gap-2 md:gap-5">
<div className="relative">
<span className="font-medium select-none flex mb-[10px] pl-[10px]">
<span className="font-medium select-none flex mb-[10px]">
{t("signature")}
</span>
<input
@@ -300,7 +349,7 @@ const ManageSign = () => {
ref={imageRef}
hidden
/>
<div className="relative">
<div>
{image ? (
<div className="mysignatureCanvas relative border-[2px] border-[#888] rounded-box overflow-hidden">
<img
@@ -323,23 +372,24 @@ const ManageSign = () => {
dotSize={1}
/>
)}
<div className="penContainerDefault flex flex-row justify-between">
<div className="flex flex-row justify-between w-full">
<div>
{!image && (
<div className="flex flex-row gap-1.5 m-[5px]">
{allColor.map((data, key) => {
<div className="flex flex-row gap-1 m-[5px]">
{allColor.map((color, key) => {
const selected = penColor === color;
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`}
onClick={() => setPenColor(color)}
className={`${COLOR_CLASS[color] ?? ""} ${selected ? "border-current" : "border-white"} cursor-pointer border-b-[2px] pb-[2px] 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 className="flex flex-row gap-2 text-sm pr-2">
<div
type="button"
className="op-link"
@@ -364,7 +414,8 @@ const ManageSign = () => {
)}
</div>
</div>
<div className="relative">
<div>
<span className="font-medium select-none flex mb-[10px] pl-[10px]">
{t("initials")}
</span>
@@ -396,26 +447,29 @@ const ManageSign = () => {
dotSize={1}
/>
)}
<div className="flex flex-row justify-between w-[183px]">
<div
className={`${isInitials ? "justify-between" : ""} flex flex-row gap-1`}
>
<div>
{!isInitials && (
<div className="flex flex-row gap-1.5 m-[5px]">
{allColor.map((data, key) => {
<div className="flex flex-row gap-1 m-[5px]">
{allColor.map((color, key) => {
const selected = initialPen === color;
return (
<i
key={key}
onClick={() => setInitialPen(allColor[key])}
className={`border-b-[2px] ${key === 0 && initialPen === "blue" ? "border-blue-600" : key === 1 && initialPen === "red" ? "border-red-500" : key === 2 && initialPen === "black" ? "border-black" : "border-white"} text-[${data}] text-[16px] fa-light fa-pen-nib`}
onClick={() => setInitialPen(color)}
className={`${COLOR_CLASS[color] ?? ""} ${selected ? "border-current" : "border-white"} cursor-pointer border-b-[2px] pb-[2px] text-[16px] fa-light fa-pen-nib`}
></i>
);
})}
</div>
)}
</div>
<div className="flex flex-row gap-1">
<div className="flex flex-row gap-1.5 text-sm">
<div
type="button"
className="op-link text-sm md:text-base mr-1"
className="op-link"
onClick={() => handleUploadInitials()}
>
{t("upload")}
@@ -423,7 +477,7 @@ const ManageSign = () => {
<div>
<div
type="button"
className="op-link text-sm md:text-base mr-1"
className="op-link"
onClick={() => handleClearInitials()}
>
{t("clear")}
@@ -433,6 +487,48 @@ const ManageSign = () => {
</div>
</div>
</div>
<div>
<span className="font-medium select-none flex mb-[10px] pl-[10px]">
Stamp
</span>
<div
onClick={() => stampRef.current.click()}
className="mystampCanvas opensigndark:bg-[#1f2937] cursor-pointer border-[2px] opensigncss:border-[#888] opensigndark:border-[#4b5563] rounded-box flex flex-col overflow-hidden w-full h-full aspect-[5/2] justify-center items-center mr-2"
>
{stamp ? (
<img
alt="signature"
src={stamp}
className="w-full h-full object-contain"
/>
) : (
<>
<input
onChange={onStampChange}
type="file"
className="filetype"
accept="image/png,image/jpeg"
hidden
ref={stampRef}
/>
<i className="fa-light text-base-content fa-cloud-upload-alt text-[28px]"></i>
<div className="text-[15px] text-base-content">
{t("upload")}
</div>
</>
)}
</div>
<div className="flex justify-end pr-3">
<div
type="button"
className="op-link text-sm"
onClick={() => handleClearStamp()}
>
{t("clear")}
</div>
</div>
</div>
</div>
<div className="pt-3 ml-2 md:ml-0">
<button
+14 -16
View File
@@ -20,7 +20,7 @@ const DriveBody = lazyWithRetry(
() => import("../components/opensigndrive/DriveBody")
);
const dropdowncss =
"absolute right-0 py-2 text-[1rem] text-left bg-white border-[1px] border-gray-300 min-w-1 hover:bg-gray-300 rounded-[0.25rem] z-[800]";
"absolute right-0 py-2 text-[1rem] text-left bg-base-100 border-[1px] border-gray-300 min-w-1 rounded-[0.25rem] z-[800]";
const AppLoader = () => {
return (
<div className="h-[100vh] flex justify-center items-center">
@@ -98,7 +98,7 @@ function Opensigndrive() {
/>
),
position: "top",
style: { fontSize: "13px" }
styles: { fontSize: "13px" }
},
{
selector: '[data-tut="reactourSecond"]',
@@ -109,7 +109,7 @@ function Opensigndrive() {
/>
),
position: "top",
style: { fontSize: "13px" }
styles: { fontSize: "13px" }
},
{
selector: '[data-tut="reactourThird"]',
@@ -120,7 +120,7 @@ function Opensigndrive() {
/>
),
position: "top",
style: { fontSize: "13px" }
styles: { fontSize: "13px" }
},
{
selector: '[data-tut="reactourForth"]',
@@ -131,7 +131,7 @@ function Opensigndrive() {
/>
),
position: "top",
style: { fontSize: "13px" }
styles: { fontSize: "13px" }
}
];
const getDetails = async () => {
@@ -161,7 +161,7 @@ function Opensigndrive() {
/>
),
position: "bottom",
style: { fontSize: "13px" }
styles: { fontSize: "13px" }
},
{
selector: '[data-tut="reactourSixth"]',
@@ -172,7 +172,7 @@ function Opensigndrive() {
/>
),
position: "bottom",
style: { fontSize: "13px" }
styles: { fontSize: "13px" }
},
{
selector: '[data-tut="reactourSeventh"]',
@@ -183,7 +183,7 @@ function Opensigndrive() {
/>
),
position: "bottom",
style: { fontSize: "13px" }
styles: { fontSize: "13px" }
}
];
let newTour = [...tourConfigs, ...addMoreTour];
@@ -688,7 +688,7 @@ function Opensigndrive() {
</div>
{/* Mobile search toggle */}
<button
className="md:hidden p-2 flex justify-center items-center focus:outline-none rounded-md hover:bg-gray-200 text-[18px]"
className="md:hidden p-2 flex justify-center items-center focus:outline-none rounded-md hover:bg-base-300 text-[18px]"
aria-label="Search"
onClick={() => setMobileSearchOpen((open) => !open)}
>
@@ -699,7 +699,7 @@ function Opensigndrive() {
</button>
<div
id="folder-menu"
className={`${isOptions ? "dropdown show dropDownStyle" : "dropdown"} hidden md:block cursor-pointer hover:bg-gray-200 p-2 rounded-md`}
className={`${isOptions ? "dropdown show dropDownStyle" : "dropdown"} hidden md:block cursor-pointer hover:bg-base-300 p-2 rounded-md`}
onClick={handleFolderOptions}
>
<div data-tut="reactourSecond">
@@ -749,7 +749,7 @@ function Opensigndrive() {
>
<div
data-tut="reactourThird"
className="cursor-pointer flex flex-row items-center justify-center p-2 hover:bg-gray-200 rounded-md"
className="cursor-pointer flex flex-row items-center justify-center p-2 hover:bg-base-300 rounded-md"
data-toggle="dropdown"
>
<i
@@ -767,9 +767,7 @@ function Opensigndrive() {
</span>
</div>
<div
className={
isShowSort ? "dropdown-menu show" : "dropdown-menu"
}
className={`${isShowSort ? "dropdown-menu show" : "dropdown-menu"} bg-base-100 border-[1px] border-gray-300`}
aria-labelledby="dropdownMenuButton"
aria-expanded={isShowSort ? "true" : "false"}
>
@@ -817,7 +815,7 @@ function Opensigndrive() {
</div>
</div>
<div
className="cursor-pointer p-2 hover:bg-gray-200 rounded-md flex justify-center items-center"
className="cursor-pointer p-2 hover:bg-base-300 rounded-md flex justify-center items-center"
data-tut="reactourForth"
onClick={handleViewOption}
>
@@ -833,7 +831,7 @@ function Opensigndrive() {
onClick={() => setIsOptions(!isOptions)}
>
<div
className="p-3 flex items-center justify-center cursor-pointer rounded-md hover:bg-gray-200"
className="p-3 flex items-center justify-center cursor-pointer rounded-md hover:bg-base-300"
data-tut="reactourSecond"
>
<i
+14 -13
View File
@@ -14,7 +14,8 @@ import {
setDefaultSignImg,
resetWidgetState,
setPrefillImg,
setTypedSignFont
setTypedSignFont,
setMyStamp
} from "../redux/reducers/widgetSlice";
import {
contractDocument,
@@ -155,12 +156,10 @@ function PdfRequestFiles(
const [assignedWidgetId, setAssignedWidgetId] = useState([]);
const [showSignPagenumber, setShowSignPagenumber] = useState([]);
const [owner, setOwner] = useState({});
const [tenant, setTenant] = useState({});
const [tenantMailTemplate, setTenantMailTemplate] = useState({
body: "",
subject: ""
});
const [isOptionalDetails, setIsOptionalDetails] = useState(false);
const isMobile = window.innerWidth < 767;
let isGuestSignFlow = false;
let sendmail;
@@ -245,7 +244,6 @@ function PdfRequestFiles(
const filterSignTypes = signatureType?.filter(
(x) => x.enabled === true
);
setTenant(tenantDetails || {});
if (tenantDetails?.RequestBody) {
setTenantMailTemplate({
body: tenantDetails?.RequestBody,
@@ -532,6 +530,7 @@ function PdfRequestFiles(
const initials = defaultSignRes?.res?.defaultInitial || "";
dispatch(setDefaultSignImg(sign));
dispatch(setMyInitial(initials));
dispatch(setMyStamp(defaultSignRes?.res?.defaultStamp));
} else {
dispatch(setSaveSignCheckbox({ isVisible: true }));
}
@@ -1103,7 +1102,7 @@ function PdfRequestFiles(
? t("signature-validate-alert", { minRequiredCount })
: t("signature-validate-alert-2"),
position: "top",
style: { fontSize: "13px" }
styles: { fontSize: "13px" }
}
];
//function for get pdf page details
@@ -1260,7 +1259,7 @@ function PdfRequestFiles(
/>
),
position: "top",
style: { fontSize: "13px" }
styles: { fontSize: "13px" }
},
{
selector: '[data-tut="reactourFirst"]',
@@ -1272,7 +1271,7 @@ function PdfRequestFiles(
/>
),
position: "top",
style: { fontSize: "13px" }
styles: { fontSize: "13px" }
},
{
selector: '[data-tut="pdfArea"]',
@@ -1284,7 +1283,7 @@ function PdfRequestFiles(
/>
),
position: "top",
style: { fontSize: "13px" }
styles: { fontSize: "13px" }
},
{
selector: '[data-tut="pdftools"]',
@@ -1296,7 +1295,7 @@ function PdfRequestFiles(
/>
),
position: "top",
style: { fontSize: "13px" }
styles: { fontSize: "13px" }
},
{
selector: '[data-tut="reactourFifth"]',
@@ -1308,7 +1307,7 @@ function PdfRequestFiles(
/>
),
position: "top",
style: { fontSize: "13px" }
styles: { fontSize: "13px" }
}
];
const signedByStep = {
@@ -1321,7 +1320,7 @@ function PdfRequestFiles(
/>
),
position: "top",
style: { fontSize: "13px" }
styles: { fontSize: "13px" }
};
//checking if signed by user component exist then add signed by tour step
const signedBy =
@@ -1340,7 +1339,7 @@ function PdfRequestFiles(
/>
),
position: "top",
style: { fontSize: "13px" }
styles: { fontSize: "13px" }
};
//checking if AllowModifications is true then add allow widgets panel tour step
const allModifyWidgets = {
@@ -1353,7 +1352,7 @@ function PdfRequestFiles(
/>
),
position: "top",
style: { fontSize: "13px" }
styles: { fontSize: "13px" }
};
//handle signed by panel index if signed by exist then 2 else 1 to add tour step
@@ -1823,6 +1822,7 @@ function PdfRequestFiles(
!alreadySign &&
requestSignTourFunction()}
<Tour
key={`tour-min-required-${minRequiredCount || 0}`}
showNumber={false}
showNavigation={false}
showNavigationNumber={false}
@@ -2224,6 +2224,7 @@ function PdfRequestFiles(
setUniqueId={setUniqueId}
signatureTypes={signatureType}
allowCellResize={false}
penColors={pdfDetails?.[0]?.PenColors}
/>
)}
<DownloadPdfZip
+23 -17
View File
@@ -75,7 +75,6 @@ function PlaceHolderSign() {
const prefillImg = useSelector((state) => state.widget.prefillImg);
const isShowModal = useSelector((state) => state.widget.isShowModal);
const editorRef = useRef();
const { state } = useLocation();
const navigate = useNavigate();
const [defaultBody, setDefaultBody] = useState("");
const [defaultSubject, setDefaultSubject] = useState("");
@@ -1152,14 +1151,14 @@ function PlaceHolderSign() {
className="flex flex-row items-center op-link op-link-primary"
>
<i className="fa-light fa-copy" />
<span className=" hidden md:block ml-1 ">{t("copy-link")}</span>
<span className="hidden md:block ml-1">{t("copy-link")}</span>
</button>
<ShareButton
title={t("sign-url")}
text={t("sign-url")}
url={data.url}
>
<i className="fa-light fa-share-from-square op-link data-[theme=opensigndark]:op-link-primary data-[theme=opensigncss]:op-link-secondary no-underline"></i>
<i className="fa-light fa-share-from-square op-link opensigncss:op-link-secondary opensigndark:op-link-primary no-underline"></i>
</ShareButton>
</div>
</div>
@@ -1372,7 +1371,7 @@ function PlaceHolderSign() {
/>
),
position: "top",
style: { fontSize: "13px" }
styles: { fontSize: "13px" }
},
{
selector: '[data-tut="recipientArea"]',
@@ -1384,7 +1383,7 @@ function PlaceHolderSign() {
/>
),
position: "top",
style: { fontSize: "13px" }
styles: { fontSize: "13px" }
},
{
selector: '[data-tut="addRecipient"]',
@@ -1396,7 +1395,7 @@ function PlaceHolderSign() {
/>
),
position: "top",
style: { fontSize: "13px" }
styles: { fontSize: "13px" }
},
{
selector: '[data-tut="addWidgets"]',
@@ -1408,7 +1407,7 @@ function PlaceHolderSign() {
/>
),
position: "top",
style: { fontSize: "13px" }
styles: { fontSize: "13px" }
},
{
selector: '[data-tut="pdfArea"]',
@@ -1420,7 +1419,7 @@ function PlaceHolderSign() {
/>
),
position: "top",
style: { fontSize: "13px" }
styles: { fontSize: "13px" }
},
{
selector: '[data-tut="pdftools"]',
@@ -1432,7 +1431,7 @@ function PlaceHolderSign() {
/>
),
position: "top",
style: { fontSize: "13px" }
styles: { fontSize: "13px" }
},
{
selector: '[data-tut="headerArea"]',
@@ -1444,7 +1443,7 @@ function PlaceHolderSign() {
/>
),
position: "top",
style: { fontSize: "13px" }
styles: { fontSize: "13px" }
}
];
@@ -1641,13 +1640,16 @@ function PlaceHolderSign() {
}
};
}
else if (["signature"].includes(position.type)) {
else if (["signature", "initials"].includes(position.type)) {
return {
...position,
options: {
...position.options,
name: defaultdata.name,
hint: defaultdata?.hint || ""
hint: defaultdata?.hint || "",
...(defaultdata?.penColors?.length > 0 && {
penColors: defaultdata?.penColors
})
}
};
} else {
@@ -1844,7 +1846,7 @@ function PlaceHolderSign() {
selector: '[data-tut="assignSigner"]',
content: t("attach-signer-tour"),
position: "top",
style: { fontSize: "13px" }
styles: { fontSize: "13px" }
}
];
const prefillWidgetTour = [
@@ -1852,7 +1854,7 @@ function PlaceHolderSign() {
selector: '[data-tut="IsSigned"]',
content: t("text-field-tour"),
position: "top",
style: { fontSize: "13px" }
styles: { fontSize: "13px" }
}
];
const signatureWidgetTour = [
@@ -1860,7 +1862,7 @@ function PlaceHolderSign() {
selector: '[data-tut="isSignatureWidget"]',
content: t("signature-field-widget", { signersName }),
position: "top",
style: { fontSize: "13px" }
styles: { fontSize: "13px" }
}
];
@@ -1963,6 +1965,10 @@ function PlaceHolderSign() {
const RedirectUrl = updateDocument?.[0]?.RedirectUrl
? { RedirectUrl: updateDocument?.[0]?.RedirectUrl }
: {};
const penColors =
updateDocument?.[0]?.PenColors?.length > 0
? { PenColors: updateDocument?.[0]?.PenColors }
: {};
const data = {
...(updateDocument?.[0]?.URL ? { URL: updateDocument?.[0]?.URL } : {}),
Name: updateDocument?.[0]?.Name || "",
@@ -1979,6 +1985,7 @@ function PlaceHolderSign() {
: false,
TimeToCompleteDays:
parseInt(updateDocument?.[0]?.TimeToCompleteDays) || 15,
...penColors,
...Bcc,
...RedirectUrl
};
@@ -2017,8 +2024,6 @@ function PlaceHolderSign() {
onRequestClose={closeTour}
steps={tourConfig}
isOpen={placeholderTour}
rounded={5}
closeWithMask={false}
/>
<Tour
onRequestClose={() => setSignerExistModal(false)}
@@ -2489,6 +2494,7 @@ function PlaceHolderSign() {
isSave={true}
setUniqueId={setUniqueId}
signatureTypes={signatureType}
penColors={pdfDetails?.[0]?.PenColors}
/>
)}
<ModalUi
-4
View File
@@ -22,8 +22,6 @@ const Preferences = () => {
const appName =
"OpenSign™";
const { t } = useTranslation();
const Extand_Class = localStorage.getItem("Extand_Class");
const extClass = Extand_Class && JSON.parse(Extand_Class);
const [isalert, setIsAlert] = useState({ type: "success", msg: "" });
const [isTopLoader, setIsTopLoader] = useState(false);
const [isLoader, setIsLoader] = useState(false);
@@ -44,7 +42,6 @@ const Preferences = () => {
const [is12HourTime, setIs12HourTime] = useState(false);
const [isLTVEnabled, setIsLTVEnabled] = useState(false);
const [fileNameFormat, setFileNameFormat] = useState("DOCNAME");
const [userInfo, setUserInfo] = useState({});
const [tenantInfo, setTenantInfo] = useState({});
useEffect(() => {
@@ -87,7 +84,6 @@ const Preferences = () => {
const getUser = extUser.data?.result;
if (getUser) {
const _getUser = JSON.parse(JSON.stringify(getUser));
setUserInfo(_getUser);
setIsNotifyOnSignatures(
_getUser?.NotifyOnSignatures !== undefined
? _getUser?.NotifyOnSignatures
+7 -6
View File
@@ -63,7 +63,8 @@ import {
setDefaultSignImg,
setIsShowModal,
resetWidgetState,
setTypedSignFont
setTypedSignFont,
setMyStamp
} from "../redux/reducers/widgetSlice";
import WidgetsValueModal from "../components/pdf/WidgetsValueModal";
import WidgetNameModal from "../components/pdf/WidgetNameModal";
@@ -192,7 +193,6 @@ function SignYourSelf() {
return () => clearTimeout(timer);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [divRef.current, isSidebar]);
//function for get document details for perticular signer with signer'object id
const getDocumentDetails = async (showComplete) => {
try {
@@ -267,6 +267,7 @@ function SignYourSelf() {
);
dispatch(setDefaultSignImg(defaultSignRes?.res?.defaultSignature));
dispatch(setMyInitial(defaultSignRes?.res?.defaultInitial));
dispatch(setMyStamp(defaultSignRes?.res?.defaultStamp));
} else {
dispatch(setSaveSignCheckbox({ isVisible: true }));
}
@@ -892,7 +893,7 @@ function SignYourSelf() {
/>
),
position: "top",
style: { fontSize: "13px" }
styles: { fontSize: "13px" }
},
{
selector: '[data-tut="reactourSecond"]',
@@ -904,7 +905,7 @@ function SignYourSelf() {
/>
),
position: "top",
style: { fontSize: "13px" }
styles: { fontSize: "13px" }
},
{
selector: '[data-tut="pdftools"]',
@@ -916,7 +917,7 @@ function SignYourSelf() {
/>
),
position: "top",
style: { fontSize: "13px" }
styles: { fontSize: "13px" }
}
];
@@ -1215,7 +1216,7 @@ function SignYourSelf() {
selector: '[data-tut="IsSigned"]',
content: t("signature-validate-alert-2"),
position: "top",
style: { fontSize: "13px" }
styles: { fontSize: "13px" }
}
];
return (
+45 -32
View File
@@ -38,7 +38,8 @@ import {
getOriginalWH,
defaultMailBody,
defaultMailSubject,
handleDeleteWidget
handleDeleteWidget,
nonPresentMaskCss
} from "../constant/Utils";
import RenderPdf from "../components/pdf/RenderPdf";
import "../styles/AddUser.css";
@@ -71,7 +72,6 @@ import ShareButton from "../primitives/ShareButton";
const TemplatePlaceholder = () => {
const { t } = useTranslation();
const journey = "Use Template";
const { templateId } = useParams();
const dispatch = useDispatch();
const prefillImg = useSelector((state) => state.widget.prefillImg);
@@ -629,7 +629,7 @@ const TemplatePlaceholder = () => {
selector: '[data-tut="reactourAddbtn"]',
content: t("add-user-template"),
position: "top",
style: { fontSize: "13px" }
styles: { fontSize: "13px" }
}
];
@@ -958,24 +958,16 @@ const TemplatePlaceholder = () => {
);
if (res?.status === "unfilled") {
setIsUiLoading(false);
const emptyWidget = res?.emptyResponseObjects.map((item, index) => (
<span className="font-medium" key={index}>
{item.options.name}
</span>
));
const emptyWidget = res?.emptyResponseObjects
?.map((item) => item.options.name)
?.join(", ");
const timeInMiliSec = 6000;
showAlert(
"danger",
<>
The following required field(s) cannot be left empty:{" "}
{emptyWidget.map((item, index) => (
<span key={index}>
{index > 0 && ", "}
{item}
</span>
))}
. Please fill them out to proceed.
</>,
6000
t("prefill-unfilled-widget", {
emptyWidget: emptyWidget ? `[${emptyWidget}]` : ""
}),
timeInMiliSec
);
} else if (res?.status === "unattach signer") {
setIsUiLoading(false);
@@ -996,7 +988,7 @@ const TemplatePlaceholder = () => {
pdfDetails,
signerPos,
signersdata,
updatedPdfUrl
updatedPdfUrl,
);
if (res.status === "success") {
navigate(`/placeHolderSign/${res.id}`, {
@@ -1025,7 +1017,7 @@ const TemplatePlaceholder = () => {
/>
),
position: "top",
style: { fontSize: "13px" }
styles: { fontSize: "13px" }
},
{
selector: '[data-tut="reactourAddbtn"]',
@@ -1038,19 +1030,31 @@ const TemplatePlaceholder = () => {
),
position: "top",
observe: '[data-tut="reactourAddbtn--observe"]',
style: { fontSize: "13px" }
styles: { fontSize: "13px" }
},
// {
// selector: '[data-tut="addWidgets"]',
// content: () => (
// <TourContentWithBtn
// message={t("tour-mssg.template-placeholder-2")}
// isDontShowCheckbox={!checkTourStatus}
// isChecked={handleDontShow}
// />
// ),
// position: "top",
// styles: { fontSize: "13px" }
// },
{
selector: '[data-tut="addWidgets"]',
selector: '[data-tut="nonpresentmask"]',
content: () => (
<TourContentWithBtn
message={t("tour-mssg.template-placeholder-2")}
isDontShowCheckbox={!checkTourStatus}
isChecked={handleDontShow}
/>
),
position: "top",
style: { fontSize: "13px" }
position: "center",
styles: { fontSize: "13px", maskArea: nonPresentMaskCss },
action: () => handleCloseRoleModal()
},
{
selector: '[data-tut="pdftools"]',
@@ -1062,7 +1066,7 @@ const TemplatePlaceholder = () => {
/>
),
position: "top",
style: { fontSize: "13px" }
styles: { fontSize: "13px" }
},
{
selector: '[data-tut="headerArea"]',
@@ -1074,7 +1078,7 @@ const TemplatePlaceholder = () => {
/>
),
position: "top",
style: { fontSize: "13px" }
styles: { fontSize: "13px" }
}
];
@@ -1277,6 +1281,10 @@ const TemplatePlaceholder = () => {
const RedirectUrl = updateTemplate?.[0]?.RedirectUrl
? { RedirectUrl: updateTemplate?.[0]?.RedirectUrl }
: {};
const penColors =
updateTemplate?.[0]?.PenColors?.length > 0
? { PenColors: updateTemplate?.[0]?.PenColors }
: {};
const data = {
...(updateTemplate?.[0]?.URL ? { URL: updateTemplate?.[0]?.URL } : {}),
Name: updateTemplate?.[0]?.Name || "",
@@ -1293,6 +1301,7 @@ const TemplatePlaceholder = () => {
: false,
TimeToCompleteDays:
parseInt(updateTemplate?.[0]?.TimeToCompleteDays) || 15,
...penColors,
...Bcc,
...RedirectUrl
};
@@ -1507,13 +1516,16 @@ const TemplatePlaceholder = () => {
}
};
}
else if (["signature"].includes(position.type)) {
else if (["signature", "initials"].includes(position.type)) {
return {
...position,
options: {
...position.options,
name: defaultdata.name,
hint: defaultdata?.hint || ""
hint: defaultdata?.hint || "",
...(defaultdata?.penColors?.length > 0 && {
penColors: defaultdata?.penColors
})
}
};
} else {
@@ -1621,7 +1633,7 @@ const TemplatePlaceholder = () => {
selector: '[data-tut="isSignatureWidget"]',
content: t("signature-field-widget", { signersName }),
position: "top",
style: { fontSize: "13px" }
styles: { fontSize: "13px" }
}
];
const showAlert = (type, message, duration = 1500) => {
@@ -1633,7 +1645,7 @@ const TemplatePlaceholder = () => {
selector: '[data-tut="IsSigned"]',
content: t("text-field-tour"),
position: "top",
style: { fontSize: "13px" }
styles: { fontSize: "13px" }
}
];
const copytoclipboard = (text) => {
@@ -2150,6 +2162,7 @@ const TemplatePlaceholder = () => {
isSave={true}
setUniqueId={setUniqueId}
signatureTypes={signatureType}
penColors={pdfDetails?.[0]?.PenColors}
/>
)}
<RotateAlert
+1 -1
View File
@@ -11,7 +11,7 @@ import axios from "axios";
import Tooltip from "../primitives/Tooltip";
import {
getSecureUrl,
handleSendOTP,
handleSendOTP
} from "../constant/Utils";
import ModalUi from "../primitives/ModalUi";
import Loader from "../primitives/Loader";
+1 -1
View File
@@ -1,4 +1,4 @@
// below code is used to handle undefined Promise.withResolvers for safari
// below code is used to handle undefined Promise.withResolvers for safari for react-pdf
if (typeof Promise.withResolvers === "undefined") {
Promise.withResolvers = function () {
let resolve, reject;
+9 -2
View File
@@ -1,11 +1,15 @@
import React, { useState, useEffect } from "react";
import { useState, useEffect } from "react";
import Loader from "./Loader";
import { useTranslation } from "react-i18next";
import axios from "axios";
import { getTenantDetails } from "../constant/Utils";
import { emailRegex } from "../constant/const";
import { useDispatch } from "react-redux";
import { sessionStatus } from "../redux/reducers/userReducer";
const AddContact = (props) => {
const { t } = useTranslation();
const dispatch = useDispatch();
const [name, setName] = useState("");
const [phone, setPhone] = useState("");
const [email, setEmail] = useState("");
@@ -108,6 +112,7 @@ const AddContact = (props) => {
}
} else {
setIsLoader(false);
dispatch(sessionStatus(false));
alert(t("something-went-wrong-mssg"));
}
}
@@ -139,7 +144,9 @@ const AddContact = (props) => {
)}
<div className="w-full mx-auto p-[8px]">
{!props?.isDisableTitle && (
<div className="text-[14px] font-[700] text-base-content mb-1">{t("add-contact")}</div>
<div className="text-[14px] font-[700] text-base-content mb-1">
{t("add-contact")}
</div>
)}
{isUserExist && props?.isAddYourSelfCheckbox && (
<div className="mb-[0.75rem] flex items-center mt-1">
+68 -17
View File
@@ -1,33 +1,84 @@
import ReactTour from "reactour";
import { TourProvider, useTour } from "@reactour/tour";
import { useEffect, useRef } from "react";
import {
clearAllBodyScrollLocks,
disableBodyScroll,
enableBodyScroll
} from "body-scroll-lock";
function TourStateSync({ isOpen, onRequestClose }) {
const { isOpen: contextIsOpen, setIsOpen } = useTour();
const previousIsOpen = useRef(contextIsOpen);
useEffect(() => {
setIsOpen(isOpen);
}, [isOpen, setIsOpen]);
useEffect(() => {
if (previousIsOpen.current && !contextIsOpen && onRequestClose) {
onRequestClose();
}
previousIsOpen.current = contextIsOpen;
}, [contextIsOpen, onRequestClose]);
return null;
}
export default function Tour({
steps,
isOpen,
rounded,
rounded = 5,
className,
showNumber,
closeWithMask = false,
onRequestClose,
showNavigation,
showCloseButton,
showNavigationNumber,
disableKeyboardNavigation
showNavigationNumber
}) {
const radius = rounded || 5;
function disableBody() {
if (typeof window !== "undefined") {
disableBodyScroll(document.body);
}
}
function enableBody() {
if (typeof window !== "undefined") {
enableBodyScroll(document.body);
clearAllBodyScrollLocks();
}
}
return (
<ReactTour
<TourProvider
className={className}
steps={steps}
isOpen={isOpen}
rounded={radius}
scrollOffset={-100}
closeWithMask={closeWithMask}
disableKeyboardNavigation={disableKeyboardNavigation}
showCloseButton={showCloseButton}
onRequestClose={onRequestClose}
showNumber={showNumber}
defaultOpen={isOpen}
// scrollOffset={-100} // available only for v1
inViewThreshold={{ y: 150 }}
onClickMask={() => {}}
disableKeyboardNavigation={["esc"]}
showBadge={showNumber}
showNavigation={showNavigation}
showNavigationNumber={showNavigationNumber}
/>
showPrevNextButtons={showNavigation}
showDots={showNavigationNumber ?? showNavigation}
showCloseButton={showCloseButton}
padding={rounded}
onClickClose={(clickProps) => {
clickProps.setIsOpen(false);
}}
styles={{
popover: (base) => ({
...base,
borderRadius: rounded,
padding: "18px",
fontSize: "13px"
}),
maskArea: (base) => ({ ...base, rx: rounded }),
close: (base) => ({ ...base, right: 10, top: 10, outline: "none" })
}}
afterOpen={disableBody}
beforeClose={enableBody}
scrollSmooth
>
<TourStateSync isOpen={isOpen} onRequestClose={onRequestClose} />
</TourProvider>
);
}
@@ -2,7 +2,7 @@ import { createSlice } from "@reduxjs/toolkit";
const sidebarSlice = createSlice({
name: "sidebar",
initialState: { isOpen: true },
initialState: { isOpen: true, selectedMenu: true },
reducers: {
toggleSidebar: (state, action) => {
if (typeof action.payload === "undefined") {
@@ -12,9 +12,12 @@ const sidebarSlice = createSlice({
// payload provided → use it directly
state.isOpen = action.payload;
}
},
setSelectedMenu: (state, action) => {
state.selectedMenu = action.payload;
}
}
});
export const { toggleSidebar } = sidebarSlice.actions;
export const { toggleSidebar, setSelectedMenu } = sidebarSlice.actions;
export default sidebarSlice.reducer;
@@ -10,6 +10,7 @@ const initialState = {
signatureTypes: null,
defaultSignImg: null,
myInitial: null,
myStamp: null,
lastIndex: "",
scrollTriggerId: "",
prefillImg: [],
@@ -63,6 +64,9 @@ const widgetSlice = createSlice({
setPrefillImgLoad: (state, action) => {
state.prefillImgLoad = action.payload;
},
setMyStamp: (state, action) => {
state.myStamp = action.payload;
},
resetWidgetState: () => initialState
}
});
@@ -78,7 +82,8 @@ export const {
setScrollTriggerId,
setPrefillImg,
setPrefillImgLoad,
setTypedSignFont
setTypedSignFont,
setMyStamp
} = widgetSlice.actions;
export default widgetSlice.reducer;
@@ -2,8 +2,12 @@ import { useEffect, useState } from "react";
import Loader from "../../primitives/Loader";
import { useTranslation } from "react-i18next";
import Parse from "parse";
import { useDispatch } from "react-redux";
import { sessionStatus } from "../../redux/reducers/userReducer";
const EditContactForm = (props) => {
const { t } = useTranslation();
const dispatch = useDispatch();
const [isLoader, setIsLoader] = useState(false);
const [formData, setFormData] = useState({
Name: "",
@@ -30,39 +34,49 @@ const EditContactForm = (props) => {
const handleSubmit = async (e) => {
e.preventDefault();
e.stopPropagation();
if (props.handleEditContact) {
try {
setIsLoader(true);
const params = {
contactId: props.contact.objectId,
name: formData.Name,
email: formData.Email,
phone: formData?.Phone,
company: formData?.Company,
jobTitle: formData?.JobTitle,
tenantId: localStorage.getItem("TenantId")
};
const res = await Parse.Cloud.run("editcontact", params);
const updateContact = {
...res,
Name: formData.Name,
Email: formData.Email,
Phone: formData?.Phone,
Company: formData?.Company,
JobTitle: formData?.JobTitle
};
props.handleEditContact(updateContact);
} catch (err) {
console.log("err in edit contact ", err);
if (err.code === 137) {
alert(t("contact-already-exists"));
} else {
alert(t("something-went-wrong-mssg"));
try {
const sessionToken = Parse.User?.current()?.getSessionToken();
if (localStorage.getItem("TenantId") && sessionToken) {
if (props.handleEditContact) {
try {
setIsLoader(true);
const params = {
contactId: props.contact.objectId,
name: formData.Name,
email: formData.Email,
phone: formData?.Phone,
company: formData?.Company,
jobTitle: formData?.JobTitle,
tenantId: localStorage.getItem("TenantId")
};
const res = await Parse.Cloud.run("editcontact", params);
const updateContact = {
...res,
Name: formData.Name,
Email: formData.Email,
Phone: formData?.Phone,
Company: formData?.Company,
JobTitle: formData?.JobTitle
};
props.handleEditContact(updateContact);
} catch (err) {
console.log("err in edit contact ", err);
if (err.code === 137) {
alert(t("contact-already-exists"));
} else {
alert(t("something-went-wrong-mssg"));
}
} finally {
setIsLoader(false);
props.handleClose && props.handleClose();
}
}
} finally {
setIsLoader(false);
props.handleClose && props.handleClose();
} else {
dispatch(sessionStatus(false));
}
} catch (err) {
console.error("invalid session or missing tenantId ", err);
dispatch(sessionStatus(false));
}
};
return (
@@ -1,11 +1,14 @@
import { useState, useEffect, useRef } from "react";
import { useState } from "react";
import { useTranslation } from "react-i18next";
import * as XLSX from "xlsx";
import Parse from "parse";
import { emailRegex } from "../../constant/const";
import { useDispatch } from "react-redux";
import { sessionStatus } from "../../redux/reducers/userReducer";
const ImportContact = ({ setLoader, onImport, showAlert }) => {
const { t } = useTranslation();
const dispatch = useDispatch();
const [currentImportPage, setCurrentImportPage] = useState(1);
const [importedData, setImportedData] = useState([]);
const [invalidRecords, setInvalidRecords] = useState(0);
@@ -183,40 +186,50 @@ const ImportContact = ({ setLoader, onImport, showAlert }) => {
e.stopPropagation();
setLoader(true);
try {
const filterdata = importedData.map((x) => ({
Name: x.Name,
Email: x.Email,
Phone: x.Phone,
Company: x.Company,
JobTitle: x.JobTitle,
TenantId: localStorage.getItem("TenantId")
}));
const contacts = JSON.stringify(filterdata);
const res = await Parse.Cloud.run("createbatchcontact", { contacts });
if (res) {
showAlert(
"info",
t("contact-imported", {
imported: res?.success || 0,
failed: res?.failed || 0
})
);
if (res?.success > 0) {
setTimeout(() => window.location.reload(), 1500);
const sessionToken = Parse.User?.current()?.getSessionToken();
if (localStorage.getItem("TenantId") && sessionToken) {
try {
const filterdata = importedData.map((x) => ({
Name: x.Name,
Email: x.Email,
Phone: x.Phone,
Company: x.Company,
JobTitle: x.JobTitle,
TenantId: localStorage.getItem("TenantId")
}));
const contacts = JSON.stringify(filterdata);
const res = await Parse.Cloud.run("createbatchcontact", { contacts });
if (res) {
showAlert(
"info",
t("contact-imported", {
imported: res?.success || 0,
failed: res?.failed || 0
})
);
if (res?.success > 0) {
setTimeout(() => window.location.reload(), 1500);
}
}
} catch (err) {
console.log("err while creating batch contact", err);
showAlert("danger", t("something-went-wrong-mssg"));
} finally {
onImport && onImport();
setImportedData([]);
setInvalidRecords(0);
}
} else {
dispatch(sessionStatus(false));
}
} catch (err) {
console.log("err while creating batch contact", err);
showAlert("danger", t("something-went-wrong-mssg"));
} finally {
onImport && onImport();
setImportedData([]);
setInvalidRecords(0);
console.error("invalid session or missing tenantId ", err);
dispatch(sessionStatus(false));
}
};
return (
<form onSubmit={handleImportData} className="p-[20px] h-full ">
<form onSubmit={handleImportData} className="p-[20px] h-full">
<div className="text-xs">
<label className="block ml-2">
{t("contacts-file")}
@@ -11,7 +11,6 @@ import Parse from "parse";
import {
copytoData,
fetchUrl,
formatDate,
getSignedUrl,
getTenantDetails,
handleSignatureType,
@@ -46,7 +45,6 @@ const DocumentsReport = (props) => {
const copyUrlRef = useRef(null);
const titleRef = useRef(null);
const titleElement = useElSize(titleRef);
const journey = "Use Template";
const appName =
"OpenSign™";
const { t } = useTranslation();
@@ -267,7 +265,7 @@ const DocumentsReport = (props) => {
[templateRes || templateDeatils],
placeholder || xyPosition,
signer || signerList,
templateRes?.URL || templateDeatils?.URL
templateRes?.URL || templateDeatils?.URL,
);
if (res.status === "success") {
navigate(`/placeHolderSign/${res.id}`, {
@@ -944,12 +942,14 @@ const DocumentsReport = (props) => {
);
if (res?.status === "unfilled") {
const emptyWidget = res?.emptyResponseObjects
.map((item) => `[ ${item.options.name}]`)
.join(", ");
?.map((item) => item.options.name)
?.join(", ");
const timeInMiliSec = 6000;
showAlert(
"danger",
t("prefill-unfilled-widget", { emptyWidget: emptyWidget }),
t("prefill-unfilled-widget", {
emptyWidget: emptyWidget ? `[${emptyWidget}]` : ""
}),
timeInMiliSec
);
} else if (res?.status === "unattach signer") {
@@ -16,7 +16,6 @@ import {
handleSignatureType,
replaceMailVaribles,
signatureTypes,
openInNewTab,
createDocument,
defaultMailBody,
defaultMailSubject
@@ -30,7 +29,9 @@ import "../../styles/quill.css";
import BulkSendUi from "../../components/BulkSendUi";
import Loader from "../../primitives/Loader";
import { serverUrl_fn } from "../../constant/appinfo";
import { Trans, useTranslation } from "react-i18next";
import {
useTranslation
} from "react-i18next";
import { useElSize } from "../../hook/useElSize";
import LottieWithLoader from "../../primitives/DotLottieReact";
import PrefillWidgetModal from "../../components/pdf/PrefillWidgetsModal";
@@ -42,7 +43,6 @@ import CustomizeMail from "../../components/pdf/CustomizeMail";
const TemplatesReport = (props) => {
const copyUrlRef = useRef(null);
const titleRef = useRef(null);
const journey = "Use Template";
const titleElement = useElSize(titleRef);
const prefillImg = useSelector((state) => state.widget.prefillImg);
const appName =
@@ -309,7 +309,7 @@ const TemplatesReport = (props) => {
[templateRes || templateDetails],
placeholder || xyPosition,
signer || signerList,
templateRes?.URL || templateDetails?.URL
templateRes?.URL || templateDetails?.URL,
);
if (res.status === "success") {
navigate(`/placeHolderSign/${res.id}`, {
@@ -900,12 +900,14 @@ const TemplatesReport = (props) => {
);
if (res?.status === "unfilled") {
const emptyWidget = res?.emptyResponseObjects
.map((item) => `[ ${item.options.name}]`)
.join(", ");
?.map((item) => item.options.name)
?.join(", ");
const timeInMiliSec = 6000;
showAlert(
"danger",
t("prefill-unfilled-widget", { emptyWidget: emptyWidget }),
t("prefill-unfilled-widget", {
emptyWidget: emptyWidget ? `[${emptyWidget}]` : ""
}),
timeInMiliSec
);
} else if (res?.status === "unattach signer") {
+32 -25
View File
@@ -44,8 +44,26 @@
}
.mysignatureCanvas {
width: 456px;
height: 180px;
/* width: 456px;
height: 180px; */
width: 370px;
height: 146px;
}
.mystampCanvas {
/* width: 446px;
height: 180px; */
width: 370px;
height: 146px;
}
.intialSignature {
border: 2px solid #888;
background-color: rgb(255, 255, 255);
/* width: 180px;
height: 180px; */
width: 150px;
height: 150px;
}
[data-theme="opensigndark"] .tabWidth {
@@ -70,13 +88,6 @@
/* blue-500 */
}
.intialSignature {
border: 2px solid #888;
background-color: rgb(255, 255, 255);
width: 180px;
height: 180px;
}
/* Dark mode support for initials box in /managesign */
[data-theme="opensigndark"] .intialSignature {
background-color: #1f2937 !important;
@@ -117,10 +128,6 @@
border-color: #4b5563 !important;
}
.penContainerDefault {
width: 460px;
}
.icon {
position: absolute;
font-size: 14px;
@@ -450,7 +457,7 @@ option {
to prevent sudden quick movement (as the
navigation bar gets a new position at the top of the
page (position:fixed and top:0) */
.stickyHead+.content {
.stickyHead + .content {
padding-top: 60px;
}
}
@@ -466,11 +473,12 @@ option {
height: 118px;
}
.tabWidth {
.mystampCanvas {
width: 300px;
height: 118px;
}
.penContainerDefault {
.tabWidth {
width: 300px;
}
@@ -498,18 +506,17 @@ option {
width: 280px;
height: 111px;
}
.mystampCanvas {
width: 280px;
height: 111px;
}
.tabWidth {
width: 280px;
}
.penContainerDefault {
width: 280px;
}
.signTab {
font-weight: 500 !important;
font-size: 10px !important;
}
@@ -528,12 +535,12 @@ option {
width: 230px;
height: 91px;
}
.tabWidth {
.mystampCanvas {
width: 230px;
height: 91px;
}
.penContainerDefault {
.tabWidth {
width: 230px;
}
@@ -545,4 +552,4 @@ option {
.uploadImgLogo {
font-size: 20px;
}
}
}
+2
View File
@@ -161,6 +161,8 @@ export const handleCheckPrefillCreateDoc = async (
);
if (res.status === "success") {
return res;
} else if (res.status === "error") {
return res;
}
} else {
const res = { status: "unattach signer" };
+23 -1
View File
@@ -13,7 +13,9 @@ export const saveToMySign = async (widget) => {
const fileName =
widget?.type === "signature"
? `${replaceSpace}__sign`
: `${replaceSpace}__initials`;
: widget?.type === "initials"
? `${replaceSpace}__initials`
: `${replaceSpace}_stamp`;
const file = base64StringtoFile(base64, fileName);
const fileUrl = await uploadFile(file, User?.id);
// below code is used to save or update default signature, initials, stamp
@@ -25,6 +27,8 @@ export const saveToMySign = async (widget) => {
params.initials = fileUrl; // save initials image url
} else if (widget?.type === "signature") {
params.signature = fileUrl; // save signature image url
} else if (widget?.type === "stamp") {
params.stamp = fileUrl; // save stamp image url
}
const signRes = await Parse.Cloud.run("savesignature", params);
return { base64File: base64, id: signRes?.id };
@@ -113,3 +117,21 @@ export const applyNumberFormulasToPages = (pages = []) => {
// Return the cloned, updated pages
return clonedPages;
};
export const getInitials = (name) => {
if (!name) return "";
// Trim and split by any amount of whitespace
const parts = name.trim().split(/\s+/);
// Only one word → return first letter of that word
if (parts.length === 1) {
return parts[0][0].toUpperCase();
}
// More than one word → take first and second words
const first = parts[0];
const second = parts[1];
return `${first[0].toUpperCase()} ${second[0].toUpperCase()}`;
};
+5 -1
View File
@@ -6,7 +6,11 @@ module.exports = {
},
plugins: [
require("daisyui"),
function ({ addUtilities, theme }) {
function ({ addUtilities, addVariant }) {
// ✅ Variants that match html[data-theme="..."] (or any ancestor with data-theme)
addVariant("opensigncss", '[data-theme="opensigncss"] &');
addVariant("opensigndark", '[data-theme="opensigndark"] &');
addUtilities({
// Prevent iOS long-press popup
".touch-callout-none": {
+11
View File
@@ -160,6 +160,17 @@ export const smtpsecure = process.env.SMTP_PORT && process.env.SMTP_PORT !== '46
export const smtpenable =
process.env.SMTP_ENABLE && process.env.SMTP_ENABLE.toLowerCase() === 'true' ? true : false;
export const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
export function signPayload(payload, secret) {
if (payload && secret) {
const signature = crypto
.createHmac('sha256', secret)
.update(JSON.stringify(payload))
.digest('hex');
return { 'x-webhook-signature': signature };
} else {
return {};
}
}
// `generateId` is used to unique Id for fileAdapter
export function generateId(length) {
@@ -8,12 +8,16 @@ async function SignatureAfterFind(request) {
const obj = request.objects[0];
const ImageURL = obj?.get('ImageURL') && obj?.get('ImageURL');
const Initials = obj?.get('Initials') && obj?.get('Initials');
const Stamp = obj?.get('Stamp') && obj?.get('Stamp');
if (ImageURL) {
obj.set('ImageURL', getPresignedUrl(ImageURL));
}
if (Initials) {
obj.set('Initials', getPresignedUrl(Initials));
}
if (Stamp) {
obj.set('Stamp', getPresignedUrl(Stamp));
}
return [obj];
}
}
@@ -23,12 +27,16 @@ async function SignatureAfterFind(request) {
const obj = request.objects[0];
const ImageURL = obj?.get('ImageURL') && obj?.get('ImageURL');
const Initials = obj?.get('Initials') && obj?.get('Initials');
const Stamp = obj?.get('Stamp') && obj?.get('Stamp');
if (ImageURL) {
obj.set('ImageURL', presignedlocalUrl(ImageURL));
}
if (Initials) {
obj.set('Initials', presignedlocalUrl(Initials));
}
if (Stamp) {
obj.set('Stamp', presignedlocalUrl(Stamp));
}
return [obj];
}
}
@@ -101,11 +101,7 @@ async function sendMail(document, publicUrl) {
}
async function batchQuery(userId, Documents, Ip, parseConfig, type, publicUrl) {
const extCls = new Parse.Query('contracts_Users');
extCls.equalTo('UserId', {
__type: 'Pointer',
className: '_User',
objectId: userId,
});
extCls.equalTo('UserId', { __type: 'Pointer', className: '_User', objectId: userId });
const resExt = await extCls.first({ useMasterKey: true });
if (resExt) {
const _resExt = JSON.parse(JSON.stringify(resExt));
@@ -189,6 +185,7 @@ async function batchQuery(userId, Documents, Ip, parseConfig, type, publicUrl) {
},
}
: {}),
...(x?.PenColors?.length > 0 ? { PenColors: x?.PenColors } : {}),
},
};
});
@@ -1,5 +1,5 @@
export default async function manageSign(request) {
const { signature, userId, initials, id, title } = request.params;
const { signature, userId, initials, id, title, stamp } = request.params;
if (!userId) {
throw new Parse.Error(Parse.Error.INVALID_QUERY, 'Missing userId parameter.');
@@ -16,6 +16,7 @@ export default async function manageSign(request) {
signatureCls.set('Initials', initials ? initials : '');
signatureCls.set('ImageURL', signature ? signature : '');
signatureCls.set('SignatureName', title ? title : '');
signatureCls.set('Stamp', stamp ? stamp : '');
if (userPtr) {
signatureCls.set('UserId', userPtr);
}
@@ -114,6 +114,9 @@ export default async function saveAsTemplate(request) {
if (_docRes?.Bcc?.length > 0) {
templateCls.set('Bcc', _docRes?.Bcc);
}
if (_docRes?.PenColors?.length > 0) {
templateCls.set('PenColors', _docRes?.PenColors);
}
const res = await templateCls.save(null, { useMasterKey: true });
return res;
} else {
@@ -1,5 +1,5 @@
export default async function saveSignature(request) {
const { signature, userId, initials, id, title } = request.params;
const { signature, userId, initials, id, title, stamp } = request.params;
if (!userId) {
throw new Parse.Error(Parse.Error.INVALID_QUERY, 'Missing userId parameter.');
@@ -19,6 +19,9 @@ export default async function saveSignature(request) {
if (signature) {
signatureCls.set('ImageURL', signature);
}
if (stamp) {
signatureCls.set('Stamp', stamp);
}
if (title) {
signatureCls.set('SignatureName', title);
}
@@ -8,6 +8,8 @@ export default async function triggerEvent(request) {
const contactId = request.params.contactId;
const serverUrl = cloudServerUrl; //process.env.SERVER_URL;
const appId = serverAppId;
const sessiontoken = request.headers?.sessiontoken;
try {
const docQuery = new Parse.Query('contracts_Document');
docQuery.select(['Name', 'IsEnableOTP', 'SignedUrl', 'AuditTrail']);
@@ -17,13 +19,16 @@ export default async function triggerEvent(request) {
let userId;
if (isEnableOTP) {
const userRes = await axios.get(serverUrl + '/users/me', {
headers: {
'X-Parse-Application-Id': appId,
'X-Parse-Session-Token': request.headers['sessiontoken'],
},
});
userId = userRes.data && userRes.data.objectId;
let userId;
if (sessiontoken) {
const userRes = await axios.get(serverUrl + '/users/me', {
headers: {
'X-Parse-Application-Id': appId,
'X-Parse-Session-Token': sessiontoken,
},
});
userId = userRes.data && userRes.data.objectId;
}
if (!userId) {
return { message: 'User not found!' };
}
@@ -58,7 +63,10 @@ export default async function triggerEvent(request) {
return { message: 'event called!' };
} catch (err) {
console.log(`Err in triggerEvent: ${err}`);
console.log(
`triggerEvent error: `,
err?.response?.data?.error || err?.message || 'Something went wrong!'
);
return { message: 'Something went wrong!' };
}
}
@@ -0,0 +1,19 @@
/**
*
* @param {Parse} Parse
*/
exports.up = async Parse => {
const schema = new Parse.Schema('contracts_Signature');
schema.addString('Stamp');
return schema.update();
};
/**
*
* @param {Parse} Parse
*/
exports.down = async Parse => {
const schema = new Parse.Schema('contracts_Signature');
schema.deleteField('Stamp');
return schema.update();
};
@@ -0,0 +1,27 @@
/**
*
* @param {Parse} Parse
*/
exports.up = async Parse => {
const docSchema = new Parse.Schema('contracts_Document');
docSchema.addArray('PenColors');
await docSchema.update();
const templateSchema = new Parse.Schema('contracts_Template');
templateSchema.addArray('PenColors');
await templateSchema.update();
};
/**
*
* @param {Parse} Parse
*/
exports.down = async Parse => {
const docSchema = new Parse.Schema('contracts_Document');
docSchema.deleteField('PenColors');
await docSchema.update();
const templateSchema = new Parse.Schema('contracts_Template');
templateSchema.deleteField('PenColors');
await templateSchema.update();
};
+14894
View File
File diff suppressed because it is too large Load Diff
+15 -14
View File
@@ -1,6 +1,6 @@
{
"name": "open_sign_server",
"version": "2.32.0",
"version": "2.34.0",
"description": "opensign server using the parse-server module",
"repository": {
"type": "git",
@@ -18,16 +18,16 @@
"watch": "nodemon index.js"
},
"dependencies": {
"@aws-sdk/client-s3": "^3.943.0",
"@aws-sdk/s3-request-presigner": "^3.943.0",
"@aws-sdk/client-s3": "^3.952.0",
"@aws-sdk/s3-request-presigner": "^3.952.0",
"@parse/fs-files-adapter": "^3.0.0",
"@parse/push-adapter": "^8.0.2",
"@parse/push-adapter": "^8.1.0",
"@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",
"aws-sdk": "^2.1693.0",
"axios": "^1.13.2",
"coherentpdf": "^2.5.5",
"cors": "^2.8.5",
@@ -36,23 +36,24 @@
"express": "^5.2.1",
"form-data": "^4.0.5",
"generate-api-key": "^1.0.2",
"googleapis": "^167.0.0",
"googleapis": "^169.0.0",
"libreoffice-convert": "^1.7.0",
"mailgun.js": "^12.2.0",
"mongodb": "^5.9.2",
"mailgun.js": "^12.4.0",
"moment": "^2.30.1",
"mongodb": "^7.0.0",
"multer": "^2.0.2",
"multer-s3": "^3.0.1",
"node-forge": "^1.3.3",
"nodemailer": "^7.0.11",
"p-limit": "^7.2.0",
"parse": "^7.1.2",
"parse": "^8.0.0",
"parse-dbtool": "^1.2.0",
"parse-server": "^8.5.0",
"parse-server": "^8.6.2",
"parse-server-api-mail-adapter": "^5.0.2",
"pdf-lib": "^1.17.1",
"posthog-node": "^5.14.0",
"posthog-node": "^5.17.2",
"qrcode": "^1.5.4",
"rate-limiter-flexible": "^8.3.0",
"rate-limiter-flexible": "^9.0.1",
"sharp": "^0.34.5",
"speakeasy": "^2.0.0",
"ws": "^8.18.3"
@@ -60,9 +61,9 @@
"type": "module",
"devDependencies": {
"@babel/eslint-parser": "^7.28.5",
"eslint": "^9.39.1",
"eslint": "^9.39.2",
"jasmine": "^5.12.0",
"mongodb-runner": "^5.11.1",
"mongodb-runner": "^6.4.0",
"nodemon": "^3.1.11",
"nyc": "^17.1.0",
"prettier": "^3.7.4"