Merge pull request #1995 from OpenSignLabs/staging

v2.30.0
This commit is contained in:
prafull-opensignlabs
2025-10-16 14:24:55 +05:30
committed by GitHub
30 changed files with 3190 additions and 3106 deletions
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "open_sign",
"version": "2.26.0",
"version": "2.30.0",
"private": true,
"dependencies": {
"@formkit/auto-animate": "^0.9.0",
@@ -110,7 +110,7 @@
"concurrently": "^9.2.1",
"css-loader": "^7.1.2",
"daisyui": "^4.12.24",
"eslint": "^9.34.0",
"eslint": "^9.37.0",
"eslint-plugin-prettier": "^5.5.4",
"eslint-plugin-react": "^7.37.5",
"jsdom": "^27.0.0",
@@ -386,7 +386,8 @@
"dropdown": "Dropdown",
"radio button": "Radiobutton",
"image": "Bild",
"email": "E-Mail"
"email": "E-Mail",
"number": "Zahl"
},
"fields": "Felder",
"recipients": "Empfänger",
@@ -443,6 +444,19 @@
"maximum-check": "Maximale Anzahl",
"cell-count": "Zellzahl",
"default-value": "Standardwert",
"formula": "Formel",
"formula-placeholder": "Erstellen Sie eine Formel mit Zahlen-Widgets",
"formula-no-widgets": "Für diesen Unterzeichner sind noch keine weiteren Zahlen-Widgets vorhanden.",
"supported-operators": "Unterstützte Operatoren:",
"formula-helper": "Klicken Sie auf einen Zahlen-Widget-Chip, um ihn einzufügen (z. B. {{number-1}} + {{number-2}}).",
"formula-error-letter": "Widget-Namen müssen in doppelte geschweifte Klammern gesetzt werden (z. B. {{number-1}}).",
"formula-error-characters": "Außerhalb der Klammern sind nur Zahlen sowie +, -, *, /, (, ) erlaubt.",
"formula-error-braces": "Jede öffnende {{ muss eine passende }} haben.",
"formula-error-operators": "Verwenden Sie bei Operatoren oder Klammern mindestens zwei Werte.",
"formula-error-leading-operator": "Formeln dürfen nicht mit Operatoren beginnen.",
"decimal-places": "Dezimalstellen",
"number-of-decimal-places": "Anzahl der Dezimalstellen",
"decimal-places-helper": "Auf 0 setzen für ganze Zahlen",
"select": "Auswählen",
"read-only": "Ist schreibgeschützt",
"hide-labels": "Labels ausblenden",
@@ -386,7 +386,8 @@
"dropdown": "dropdown",
"radio button": "radio button",
"image": "image",
"email": "email"
"email": "email",
"number": "number"
},
"fields": "Fields",
"recipients": "Recipients",
@@ -444,6 +445,19 @@
"maximum-check": "Maximum check",
"cell-count": "Cell count",
"default-value": "Default value",
"formula": "Formula",
"formula-placeholder": "Build a formula using number widgets",
"formula-no-widgets": "No other number widgets are assigned to this signer yet.",
"supported-operators": "Supported operators:",
"formula-helper": "Click a number widget chip to insert it (e.g. {{number-1}} + {{number-2}}).",
"formula-error-letter": "Wrap widget names in double curly braces (e.g. {{number-1}}).",
"formula-error-characters": "Only numbers and +, -, *, /, (, ) are allowed outside of braces.",
"formula-error-braces": "Each opening {{ must have a matching }}.",
"formula-error-operators": "Add at least two values when using operators or parentheses.",
"formula-error-leading-operator": "Formulas cannot start with operators.",
"decimal-places": "Decimal Places",
"number-of-decimal-places": "Number of decimal places",
"decimal-places-helper": "Set to 0 for whole numbers",
"select": "Select",
"read-only": "read only",
"hide-labels": "Hide labels",
@@ -386,7 +386,8 @@
"dropdown": "desplegable",
"radio button": "botón de radio",
"image": "imagen",
"email": "correo"
"email": "correo",
"number": "número"
},
"fields": "Campos",
"recipients": "Destinatarios",
@@ -444,6 +445,19 @@
"maximum-check": "Chequeo máximo",
"cell-count": "recuento de células",
"default-value": "Valor por defecto",
"formula": "Fórmula",
"formula-placeholder": "Crea una fórmula utilizando widgets numéricos",
"formula-no-widgets": "Todavía no hay otros widgets numéricos asignados a este firmante.",
"supported-operators": "Operadores compatibles:",
"formula-helper": "Haz clic en una ficha de widget numérico para insertarla (p. ej. {{number-1}} + {{number-2}}).",
"formula-error-letter": "Envuelve los nombres de los widgets entre dobles llaves (p. ej. {{number-1}}).",
"formula-error-characters": "Fuera de las llaves solo se permiten números y +, -, *, /, (, ).",
"formula-error-braces": "Cada {{ de apertura debe tener una }} correspondiente.",
"formula-error-operators": "Cuando uses operadores o paréntesis, agrega al menos dos valores.",
"formula-error-leading-operator": "Las fórmulas no pueden comenzar con operadores.",
"decimal-places": "Decimales",
"number-of-decimal-places": "Número de decimales",
"decimal-places-helper": "Establecer en 0 para números enteros",
"select": "Seleccionar",
"read-only": "Es de solo lectura",
"hide-labels": "Esconder etiquetas",
@@ -386,7 +386,8 @@
"dropdown": "dérouler",
"radio button": "bouton radio",
"image": "image",
"email": "e-mail"
"email": "e-mail",
"number": "nombre"
},
"fields": "Des champs",
"recipients": "Destinataires",
@@ -443,6 +444,19 @@
"maximum-check": "Contrôle maximum",
"cell-count": "numération cellulaire",
"default-value": "Valeur par défaut",
"formula": "Formule",
"formula-placeholder": "Créez une formule à l'aide des widgets numériques",
"formula-no-widgets": "Aucun autre widget numérique n'est encore attribué à ce signataire.",
"supported-operators": "Opérateurs pris en charge :",
"formula-helper": "Cliquez sur une puce de widget numérique pour l'insérer (ex. {{number-1}} + {{number-2}}).",
"formula-error-letter": "Encadrez les noms de widget avec des doubles accolades (ex. {{number-1}}).",
"formula-error-characters": "En dehors des accolades, seuls les chiffres et +, -, *, /, (, ) sont autorisés.",
"formula-error-braces": "Chaque {{ ouvrante doit avoir une }} correspondante.",
"formula-error-operators": "Lorsque vous utilisez des opérateurs ou des parenthèses, ajoutez au moins deux valeurs.",
"formula-error-leading-operator": "Les formules ne peuvent pas commencer par des opérateurs.",
"decimal-places": "Décimales",
"number-of-decimal-places": "Nombre de décimales",
"decimal-places-helper": "Définir sur 0 pour les nombres entiers",
"select": "Sélectionner",
"read-only": "Est en lecture seule",
"hide-labels": "Masquer les étiquettes",
@@ -386,7 +386,8 @@
"dropdown": "ड्रॉपडाउन",
"radio button": "रेडियो बटन",
"image": "छवि",
"email": "ईमेल"
"email": "ईमेल",
"number": "संख्या"
},
"fields": "क्षेत्र",
"recipients": "प्राप्तकर्ता",
@@ -443,6 +444,19 @@
"maximum-check": "अधिकतम जाँच",
"cell-count": "सेल्स संख्या",
"default-value": "डिफ़ॉल्ट मान",
"formula": "सूत्र",
"formula-placeholder": "संख्या विजेट्स का उपयोग करके एक सूत्र बनाएं",
"formula-no-widgets": "इस हस्ताक्षरकर्ता को अभी तक कोई अन्य संख्या विजेट असाइन नहीं किया गया है।",
"supported-operators": "समर्थित ऑपरेटर:",
"formula-helper": "सूची में से संख्या विजेट चिप पर क्लिक करके उसे जोड़ें (उदा. {{number-1}} + {{number-2}})।",
"formula-error-letter": "विजेट नामों को दोहरी घुंघराली ब्रैकेट में रखें (उदा. {{number-1}})।",
"formula-error-characters": "ब्रैकेट के बाहर केवल संख्याएँ और +, -, *, /, (, ) की अनुमति है।",
"formula-error-braces": "हर खुलने वाली {{ के लिए एक मिलान करने वाली }} होना चाहिए।",
"formula-error-operators": "ऑपरेटर या कोष्ठक का उपयोग करते समय कम से कम दो मान जोड़ें।",
"formula-error-leading-operator": "सूत्र ऑपरेटर से शुरू नहीं हो सकते।",
"decimal-places": "दशमलव स्थान",
"number-of-decimal-places": "दशमलव स्थानों की संख्या",
"decimal-places-helper": "पूर्णांक के लिए 0 पर सेट करें",
"select": "चुनें",
"read-only": "सिर्फ पढ़ने के लिए है",
"hide-labels": "लेबल छिपाएँ",
@@ -386,7 +386,8 @@
"dropdown": "menu a tendina",
"radio button": "pulsante di opzione",
"image": "immagine",
"email": "email"
"email": "email",
"number": "numero"
},
"fields": "Campi",
"recipients": "Destinatari",
@@ -443,6 +444,19 @@
"maximum-check": "Controllo massimo",
"cell-count": "conteggio delle cellule",
"default-value": "Valore predefinita",
"formula": "Formula",
"formula-placeholder": "Crea una formula utilizzando i widget numerici",
"formula-no-widgets": "Non ci sono ancora altri widget numerici assegnati a questo firmatario.",
"supported-operators": "Operatori supportati:",
"formula-helper": "Fai clic su una chip di widget numerico per inserirla (es. {{number-1}} + {{number-2}}).",
"formula-error-letter": "Racchiudi i nomi dei widget tra doppie parentesi graffe (es. {{number-1}}).",
"formula-error-characters": "Al di fuori delle parentesi sono consentiti solo numeri e +, -, *, /, (, ).",
"formula-error-braces": "Ogni {{ di apertura deve avere una }} corrispondente.",
"formula-error-operators": "Quando usi operatori o parentesi, aggiungi almeno due valori.",
"formula-error-leading-operator": "Le formule non possono iniziare con operatori.",
"decimal-places": "Cifre decimali",
"number-of-decimal-places": "Numero di cifre decimali",
"decimal-places-helper": "Imposta su 0 per numeri interi",
"select": "Seleziona",
"read-only": "È di sola lettura",
"hide-labels": "Nascondi etichette",
@@ -71,11 +71,9 @@ function DriveBody(props) {
});
props.setPdfData(updatedData);
props.sortingData(null, null, updatedData);
await axios
.put(
`${localStorage.getItem(
"baseUrl"
)}classes/contracts_Document/${docId}`,
try {
await axios.put(
`${localStorage.getItem("baseUrl")}classes/contracts_Document/${docId}`,
updateName,
{
headers: {
@@ -84,18 +82,14 @@ function DriveBody(props) {
"X-Parse-Session-Token": localStorage.getItem("accesstoken")
}
}
)
.then(() => {
// const res = result.data;
// console.log("res", res);
})
.catch((err) => {
console.log("Err ", err);
props.setIsAlert({
isShow: true,
alertMessage: t("something-went-wrong-mssg")
});
);
} catch (err) {
console.log("Err ", err);
props.setIsAlert({
isShow: true,
alertMessage: t("something-went-wrong-mssg")
});
}
}
};
@@ -245,7 +239,6 @@ function DriveBody(props) {
)
.then((Listdata) => {
// console.log("Listdata ", Listdata);
const res = Listdata.data;
if (res) {
const updatedData = props.pdfData.filter(
@@ -31,7 +31,6 @@ function EmailComponent({
setIsLoading(true);
const params = { docId: pdfDetails?.[0]?.objectId, recipients: emailList };
const sendmail = await Parse.Cloud.run("forwarddoc", params);
console.log("sendmail ", sendmail);
if (sendmail?.status === "success") {
setSuccessEmail(true);
setIsEmail(false);
@@ -267,7 +267,8 @@ function Placeholder(props) {
(props.isSignYourself || props.isSelfSign)
) {
props.handleTextSettingModal(true);
} else {
}
else {
props?.handleNameModal && props?.handleNameModal(true);
}
@@ -305,13 +306,7 @@ function Placeholder(props) {
) {
props.setUniqueId(props?.data?.Id);
}
//checking widget's type and open widget copy modal for required widgets
if (
["signature", textInputWidget, textWidget, "stamp", "initials"].includes(
props.pos.type
)
) {
if (props?.data?.Role !== "prefill") {
props.setIsPageCopy(true);
props.setCurrWidgetsDetails(props.pos);
} else {
@@ -326,7 +321,7 @@ function Placeholder(props) {
props.data && props.data?.Id
);
//condiiton is used to store copied prefill image base64 url in redux for display image
if (props?.data?.Role === "prefill") {
if (props?.pos?.type === "image") {
const getPrefillImg = prefillImg?.find((x) => x.id === props.pos.key);
dispatch(
setPrefillImg({
@@ -1,4 +1,4 @@
import { useEffect, useState, forwardRef } from "react";
import { useEffect, useMemo, useState, forwardRef } from "react";
import {
getMonth,
getYear,
@@ -10,7 +10,8 @@ import {
years,
selectCheckbox,
checkRegularExpress,
isBase64
isBase64,
getSignerPages
} from "../../constant/Utils";
import DatePicker from "react-datepicker";
import "react-datepicker/dist/react-datepicker.css";
@@ -19,6 +20,7 @@ import { useTranslation } from "react-i18next";
import CellsWidget from "./CellsWidget";
import { useSelector } from "react-redux";
import Loader from "../../primitives/Loader";
const textWidgetCls =
"w-full h-full md:min-w-full md:min-h-full z-[999] text-[12px] overflow-hidden resize-none outline-none text-base-content item-center whitespace-pre-wrap";
const widgetCls =
@@ -65,18 +67,21 @@ function PlaceholderType(props) {
props?.pos?.options?.defaultValue ||
[]
);
} else {
}
else {
// keep displayed value in sync with the stored response
setwidgetValue(widgetData);
}
if (props.pos?.options?.hint) {
setHint(props.pos?.options.hint);
} else if (props.pos?.options?.validation?.type) {
checkRegularExpress(props.pos?.options?.validation?.type, setHint);
checkRegularExpress(props.pos?.options?.validation?.type, setHint);
}
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [props.pos]);
}, [props.pos, widgetData, type]);
const ExampleCustomInput = forwardRef(({ value, onClick }, ref) => (
<div
style={{
@@ -28,6 +28,8 @@ import {
} from "../../redux/reducers/widgetSlice";
import * as utils from "../../utils";
const widgetTitle = "font-medium";
const ShowTextWidget = ({ position, handleWidgetDetails }) => {
const inputRef = useRef(null);
const [inputValue, setInputValue] = useState(position.options.response || "");
@@ -46,13 +48,77 @@ const ShowTextWidget = ({ position, handleWidgetDetails }) => {
);
};
const ImageComponent = (props) => {
const { t } = useTranslation();
const prefillImg = useSelector((state) => state.widget.prefillImg);
const imageRefs = useRef({});
let imgUrl = "";
const isBase64Url = isBase64(props?.position?.SignUrl);
if (isBase64Url) {
imgUrl = props?.position?.SignUrl;
} else {
const getPrefillImg = prefillImg?.find(
(x) => x.id === props?.position?.key
);
imgUrl = getPrefillImg?.base64;
}
return (
<>
<span className={widgetTitle}>{props?.position.options?.name}</span>
{imgUrl ? (
<>
<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
src={imgUrl}
draggable="false"
className="object-contain h-full w-full aspect-[5/2]"
onLoad={() => props?.handleImageLoaded?.(props?.position.key)}
onError={() => props?.handleImageLoaded?.(props?.position.key)}
/>
</div>
<span
onClick={() => props?.handleClearImage(props?.position)}
className="flex justify-start text-blue-500 underline cursor-pointer"
>
{t("clear")}
</span>
</>
) : (
<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()}
>
{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
hidden
/>
<i className="fa-light text-base-content fa-cloud-upload-alt text-[25px]"></i>
<div className="text-[10px] text-base-content">{t("upload")}</div>
</div>
)}
</>
);
};
function PrefillWidgetModal(props) {
const { t } = useTranslation();
const dispatch = useDispatch();
// Track already loaded image keys so they don't increment multiple times
const loadedSet = useRef(new Set());
const initializedRef = useRef(false); // prevent rerun on state updates
const prefillImg = useSelector((state) => state.widget.prefillImg);
const [image, setImage] = useState(null);
const [imageLoaders, setImageLoaders] = useState({});
const [currentWidget, setCurrentWidget] = useState("");
@@ -61,7 +127,6 @@ function PrefillWidgetModal(props) {
const [loadedImages, setLoadedImages] = useState(0);
const [loading, setLoading] = useState(false);
const years = range(1950, getYear(new Date()) + 16, 1);
const widgetTitle = "font-medium";
const isAnyLoaderActive = Object.values(imageLoaders).some(
(val) => val === true
);
@@ -93,6 +158,7 @@ function PrefillWidgetModal(props) {
return flatArray || [];
}, [props.prefillData]);
useEffect(() => {
dispatch(resetWidgetState([]));
}, []);
@@ -163,67 +229,7 @@ function PrefillWidgetModal(props) {
}
return date;
};
const ImageComponent = (props) => {
const imageRefs = useRef([]);
let imgUrl = "";
const isBase64Url = isBase64(props?.position?.SignUrl);
if (isBase64Url) {
imgUrl = props?.position?.SignUrl;
} else {
const getPrefillImg = prefillImg?.find(
(x) => x.id === props?.position?.key
);
imgUrl = getPrefillImg?.base64;
}
return (
<>
<span className={widgetTitle}>{props?.position.options?.name}</span>
{imgUrl ? (
<>
<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
src={imgUrl}
draggable="false"
className="object-contain h-full w-full aspect-[5/2]"
onLoad={() => handleImageLoaded?.(props?.position.key)}
onError={() => handleImageLoaded?.(props?.position.key)}
/>
</div>
<span
onClick={() => handleClearImage(props?.position)}
className="flex justify-start text-blue-500 underline cursor-pointer"
>
{t("clear")}
</span>
</>
) : (
<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()}
>
{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) => onImageChange(e, props?.position)}
className="filetype"
accept="image/png,image/jpeg"
ref={(el) => (imageRefs.current[props?.position.key] = el)} // Assign ref dynamically
hidden
/>
<i className="fa-light text-base-content fa-cloud-upload-alt text-[25px]"></i>
<div className="text-[10px] text-base-content">{t("upload")}</div>
</div>
)}
</>
);
};
const ExampleCustomInput = forwardRef(({ value, onClick }, ref) => (
<div
style={{ fontFamily: "Arial, sans-serif" }}
@@ -273,7 +279,6 @@ function PrefillWidgetModal(props) {
image.src,
image.imgType
);
setImageLoaders({});
if (imageUrl) {
return imageUrl;
}
@@ -414,6 +419,7 @@ function PrefillWidgetModal(props) {
// (tracks how many images have finished loading)
setLoadedImages((prev) => prev + 1);
}
setImageLoaders({});
};
const handleWidgetType = (position) => {
switch (position?.type) {
@@ -432,9 +438,9 @@ function PrefillWidgetModal(props) {
className="mt-[2px] op-checkbox op-checkbox-xs"
type="checkbox"
checked={selectCheckbox(ind, position)}
onChange={(e) => {
handleCheckboxValue(e.target.checked, ind, position);
}}
onChange={(e) =>
handleCheckboxValue(e.target.checked, ind, position)
}
/>
<label
htmlFor={`checkbox-${position.key + ind}`}
@@ -467,9 +473,7 @@ function PrefillWidgetModal(props) {
value={
position?.options?.response || position?.options?.defaultValue
}
onChange={(e) => {
handleWidgetDetails(position, e.target.value);
}}
onChange={(e) => handleWidgetDetails(position, e.target.value)}
>
{/* Default/Title option */}
<option value="" disabled hidden>
@@ -519,9 +523,7 @@ function PrefillWidgetModal(props) {
)}
closeOnScroll={true}
selected={handleDate(position)}
onChange={(date) => {
handleOnDateChange(date, position);
}}
onChange={(date) => handleOnDateChange(date, position)}
customInput={<ExampleCustomInput />}
dateFormat={position?.options?.validation?.format || "MM/dd/yyyy"}
/>
@@ -531,7 +533,10 @@ function PrefillWidgetModal(props) {
return (
<ImageComponent
position={position}
docId={props?.docId}
imageLoaders={imageLoaders}
onImageChange={onImageChange}
handleImageLoaded={handleImageLoaded}
handleClearImage={handleClearImage}
/>
);
case radioButtonWidget:
@@ -549,9 +554,7 @@ function PrefillWidgetModal(props) {
className="mt-[2px] op-radio op-radio-xs"
type="radio"
checked={handleRadioCheck(data, position)}
onChange={() => {
handleWidgetDetails(position, data);
}}
onChange={() => handleWidgetDetails(position, data)}
/>
<label
htmlFor={`radio-${position.key + ind}`}
@@ -697,9 +700,9 @@ function PrefillWidgetModal(props) {
multiValueLabel: () => "mb-[2px]",
menu: () =>
"mt-1 shadow-md rounded-lg bg-base-200 text-base-content absolute z-9999",
menuList: () => "shadow-md rounded-lg ",
menuList: () => "shadow-md rounded-lg",
option: () =>
"bg-base-200 text-base-content rounded-lg m-1 hover:bg-base-300 p-2 ",
"bg-base-200 text-base-content rounded-lg m-1 hover:bg-base-300 p-2",
noOptionsMessage: () =>
"p-2 bg-base-200 rounded-lg m-1 p-2"
}}
@@ -710,7 +713,7 @@ function PrefillWidgetModal(props) {
</div>
<button
onClick={(e) => handleCreateNew(e, field.value)}
className="op-btn op-btn-accent op-btn-outline op-btn-sm "
className="op-btn op-btn-accent op-btn-outline op-btn-sm"
>
<i className="fa-light fa-plus"></i>
</button>
@@ -271,6 +271,7 @@ function RenderPdf(props) {
setIsReqSignTourDisabled={props.setIsReqSignTourDisabled}
calculateFontsize={calculateFontsize}
currWidgetsDetails={props?.currWidgetsDetails}
handleNameModal={props?.handleNameModal}
/>
</React.Fragment>
)
@@ -484,6 +485,8 @@ function RenderPdf(props) {
setIsReqSignTourDisabled={
props.setIsReqSignTourDisabled
}
unSignedWidgetId={props.unSignedWidgetId}
handleNameModal={props.handleNameModal}
/>
)
)}
@@ -91,14 +91,14 @@ function WidgetComponent(props) {
name,
jobTitle,
company,
email,
date,
text,
cells,
checkbox,
dropdown,
radioButton,
image,
email
image
];
const getWidgetArray = widgets;
const newUpdateSigner = getWidgetArray.map((obj, ind) => {
@@ -109,7 +109,7 @@ function WidgetComponent(props) {
// eslint-disable-next-line
}, []);
//allow only (signature,stamp,initials,text,name, job title, company,email) widget when isAllowModification true and user have session token
// allow only (signature, stamp, initials, text, name, job title, company, email, cells) widget when isAllowModification true and user have session token
const modifiedWidgets = widget.filter(
(data) =>
![
@@ -121,7 +121,7 @@ function WidgetComponent(props) {
"checkbox"
].includes(data.type)
);
//allow only (signature,stamp,initials,text) widget when isAllowModification true and user does not have session token
// allow only (signature, stamp, initials, text, cells) widget when isAllowModification true and user does not have session token
const unlogedInUserWidgets = widget.filter(
(data) =>
![
@@ -263,7 +263,12 @@ function WidgetComponent(props) {
)}
</span>
</div>
<div className="p-[15px] flex flex-col pt-4" data-tut="addWidgets">
<div
className="p-[12px] grid lg:grid-cols-2 gap-x-2 lg:gap-y-1.5 pt-3"
data-tut="addWidgets"
role="list"
aria-label="Add widgets"
>
<WidgetList
updateWidgets={handleWidgetType}
handleDivClick={props.handleDivClick}
@@ -1,4 +1,4 @@
import React, { useEffect, useState } from "react";
import { useEffect, useMemo, useState } from "react";
import ModalUi from "../../primitives/ModalUi";
import "../../styles/AddUser.css";
import RegexParser from "regex-parser";
@@ -21,11 +21,30 @@ const WidgetNameModal = (props) => {
hint: "",
textvalidate: "",
isReadOnly: false,
cellCount: 5
cellCount: 5,
});
const [isValid, setIsValid] = useState(true);
const statusArr = ["Required", "Optional"];
const [signatureType, setSignatureType] = useState([]);
const type = props?.defaultdata?.type;
const isCellWidget = useMemo(() => type === cellsWidget, [type]);
const isSignOrInitials = useMemo(
() => ["signature", "initials"].includes(type),
[type]
);
const showFontControls = useMemo(
() =>
[
textInputWidget,
textWidget,
cellsWidget,
"name",
"company",
"job title",
"email"
].includes(props.defaultdata?.type),
[type]
);
const handleHint = () => {
const type = props.defaultdata?.type;
@@ -54,13 +73,13 @@ const WidgetNameModal = (props) => {
? props.defaultdata?.options?.validation?.pattern
: props.defaultdata?.options?.validation?.type || "",
isReadOnly: props.defaultdata?.options?.isReadOnly || false,
cellCount: props.defaultdata?.options?.cellCount || 5
cellCount: props.defaultdata?.options?.cellCount || 5,
});
} else {
setFormdata({
...formdata,
name: props.defaultdata?.options?.name || "",
cellCount: props.defaultdata?.options?.cellCount || 5
cellCount: props.defaultdata?.options?.cellCount || 5,
});
}
@@ -68,12 +87,13 @@ const WidgetNameModal = (props) => {
const defaultSignatureType = signTypes || [];
setSignatureType(defaultSignatureType);
}
// eslint-disable-next-line
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [props.defaultdata]);
const handleSubmit = (e) => {
e.preventDefault();
if (props.handleData) {
if (["signature", "initials"].includes(props.defaultdata?.type)) {
if (isSignOrInitials) {
const enabledSignTypes = signatureType?.filter((x) => x.enabled);
const isDefaultSignTypeOnly =
enabledSignTypes?.length === 1 &&
@@ -87,9 +107,11 @@ const WidgetNameModal = (props) => {
props.handleData(data, props.defaultdata?.type);
}
} else {
const isTextInput = [textInputWidget, cellsWidget].includes(
props.defaultdata?.type
);
const isTextInput = [
textInputWidget,
cellsWidget,
].includes(props.defaultdata?.type);
const { isReadOnly, defaultValue, status } = formdata;
// If its a textinput widget, enforce that read-only fields have
// either a defaultValue or an "optional" status.
@@ -110,7 +132,7 @@ const WidgetNameModal = (props) => {
status: "required",
hint: "",
textvalidate: "",
cellCount: 5
cellCount: 5,
});
setSignatureType(signTypes);
}
@@ -148,10 +170,11 @@ const WidgetNameModal = (props) => {
} else {
setIsValid(true);
}
const val =
props.defaultdata?.type === cellsWidget
? e.target.value.slice(0, formdata.cellCount)
: e.target.value;
const val = isCellWidget
? e.target.value.slice(0, formdata.cellCount)
: e.target.value;
setFormdata({ ...formdata, [e.target.name]: val });
};
@@ -160,7 +183,7 @@ const WidgetNameModal = (props) => {
case "email":
return "/^[a-zA-Z0-9._-]+@[a-zA-Z0-9.-]+.[a-zA-Z]{2,}$/";
case "number":
return "/^\\d+$/";
return "^\\d+(?:\\.\\d+)?$"; // "/^\\d+$/";
case "text":
//allow space in text regex
return "/^[a-zA-Z ]+$/";
@@ -184,23 +207,19 @@ const WidgetNameModal = (props) => {
<ModalUi
isOpen={props.isOpen}
handleClose={props.handleClose && props.handleClose}
title={
["signature", "initials"].includes(props.defaultdata?.type)
? t("signature-setting")
: t("widget-info")
}
title={isSignOrInitials ? t("signature-setting") : t("widget-info")}
>
<form
onSubmit={handleSubmit}
className={`${
[textInputWidget, cellsWidget].includes(props.defaultdata?.type)
? "pt-0"
: ["signature", "initials"].includes(props.defaultdata?.type)
: isSignOrInitials
? "pt-2"
: ""
} p-[20px] text-base-content`}
>
{!["signature", "initials"].includes(props.defaultdata?.type) && (
{!isSignOrInitials && (
<div className="mb-[0.75rem] text-[13px]">
<label htmlFor="name">
{t("name")}
@@ -217,7 +236,7 @@ const WidgetNameModal = (props) => {
/>
</div>
)}
{props.defaultdata?.type === cellsWidget && (
{isCellWidget && (
<div className="mb-[0.75rem] text-[13px]">
<label htmlFor="cellCount">{t("cell-count")}</label>
<input
@@ -231,7 +250,10 @@ const WidgetNameModal = (props) => {
/>
</div>
)}
{[textInputWidget, cellsWidget].includes(props.defaultdata?.type) &&
{[
textInputWidget,
cellsWidget,
].includes(props.defaultdata?.type) &&
props?.roleName !== "prefill" && (
<>
<div className="mb-[0.75rem]">
@@ -244,11 +266,7 @@ const WidgetNameModal = (props) => {
value={formdata.defaultValue}
onChange={(e) => handledefaultChange(e)}
autoComplete="off"
maxLength={
props.defaultdata?.type === cellsWidget
? formdata.cellCount
: undefined
}
maxLength={isCellWidget ? formdata.cellCount : undefined}
onBlur={() => {
if (isValid === false) {
setFormdata({ ...formdata, defaultValue: "" });
@@ -262,7 +280,7 @@ const WidgetNameModal = (props) => {
style={{ fontSize: 12 }}
>
<i
className="fa-light fa-exclamation-circle text-[15px]"
className="fa-light fa-exclamation-circle text-[15px] mr-1"
style={{ color: "#fab005" }}
></i>
{t("invalid-default-value")}
@@ -271,7 +289,8 @@ const WidgetNameModal = (props) => {
</div>
</>
)}
{!["signature", "initials"].includes(props.defaultdata?.type) &&
{!props?.isSelfSign &&
!isSignOrInitials &&
props?.roleName !== "prefill" && (
<div className="mb-[0.75rem]">
<div className="flex flex-row gap-[10px] mb-[0.5rem]">
@@ -302,9 +321,10 @@ const WidgetNameModal = (props) => {
);
})}
</div>
{[textInputWidget, cellsWidget].includes(
props.defaultdata?.type
) &&
{[
textInputWidget,
cellsWidget,
].includes(props.defaultdata?.type) &&
props?.roleName !== "prefill" && (
<div className="flex items-center">
<input
@@ -330,7 +350,7 @@ const WidgetNameModal = (props) => {
)}
</div>
)}
{["signature", "initials"].includes(props.defaultdata?.type) && (
{isSignOrInitials && (
<div className="mb-[0.75rem]">
<label htmlFor="signaturetype" className="text-[14px] mb-[0.7rem]">
{t("allowed-signature-types")}
@@ -358,7 +378,7 @@ const WidgetNameModal = (props) => {
</div>
</div>
)}
{props?.roleName !== "prefill" && (
{!props?.isSelfSign && props?.roleName !== "prefill" && (
<div className="mb-[0.75rem]">
<label htmlFor="hint" className="text-[13px]">
{t("hint")}
@@ -372,15 +392,7 @@ const WidgetNameModal = (props) => {
/>
</div>
)}
{[
textInputWidget,
textWidget,
cellsWidget,
"name",
"company",
"job title",
"email"
].includes(props.defaultdata?.type) && (
{showFontControls && (
<div className="flex flex-col md:flex-row md:items-center gap-3 mb-3">
<div className="flex items-center gap-2 ">
<span className="whitespace-nowrap">{t("font-size")}: </span>
@@ -1,4 +1,4 @@
import { forwardRef, useEffect, useRef, useState } from "react";
import { forwardRef, useEffect, useMemo, useRef, useState } from "react";
import ModalUi from "../../primitives/ModalUi";
import { useTranslation } from "react-i18next";
import { removeBackground } from "@imgly/background-removal";
@@ -23,7 +23,8 @@ import {
convertBase64ToFile,
generatePdfName,
getDefaultDate,
getDefaultFormat
getDefaultFormat,
getSignerPages
} from "../../constant/Utils";
import CellsWidget from "./CellsWidget";
import DatePicker from "react-datepicker";
@@ -45,7 +46,9 @@ import { useDispatch, useSelector } from "react-redux";
import Loader from "../../primitives/Loader";
import { emailRegex } from "../../constant/const";
import RegexParser from "regex-parser";
import { saveToMySign } from "../../utils/widgetUtils";
import {
saveToMySign
} from "../../utils/widgetUtils";
const fontOptions = [
{ value: "Fasthand" },
@@ -111,12 +114,28 @@ function WidgetsValueModal(props) {
props?.kiosk_signer || JSON.parse(localStorage.getItem("kiosk_signer"));
const jsonSender = senderUser && JSON.parse(senderUser);
const currentUserName = jsonSender && jsonSender?.name;
const type = currWidgetsDetails?.type;
const widgetTypeTranslation = t(`widgets-name.${currWidgetsDetails?.type}`);
const [widgetValue, setWidgetValue] = useState(
currWidgetsDetails.type !== "checkbox" &&
(currWidgetsDetails?.options?.response ||
currWidgetsDetails?.options?.defaultValue)
);
const [widgetValue, setWidgetValue] = useState(() => {
if (currWidgetsDetails.type === "checkbox") {
return undefined;
}
return (
currWidgetsDetails?.options?.response ||
currWidgetsDetails?.options?.defaultValue
);
});
const blocked = [
"checkbox",
"dropdown",
"date",
"radio",
];
const showClearbtn =
!blocked.includes(type);
const [cellsValue, setCellsValue] = useState(() => {
const count = currWidgetsDetails?.options?.cellCount || 5;
const val =
@@ -192,7 +211,8 @@ function WidgetsValueModal(props) {
} else {
setHint(currWidgetsDetails?.type);
}
} else if (isSignOrInitials) {
}
else if (isSignOrInitials) {
setIsTab(currWidgetsDetails?.signatureType);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
@@ -877,9 +897,10 @@ function WidgetsValueModal(props) {
const handleOnchangeTextBox = (e) => {
// hide any prior validation error while typing
setIsShowValidation(false);
setWidgetValue(e.target.value);
let value = e.target.value;
setWidgetValue(value);
onChangeInput(
e.target.value,
value,
currWidgetsDetails,
xyPosition,
props.index,
@@ -1718,13 +1739,14 @@ function WidgetsValueModal(props) {
} else if (!isSave) {
const widgetsPosition = xyPosition?.find((data) => data.Id === uniqueId);
let nextWidgetDetails = null;
const editableWidgets = widgetsPosition?.placeHolder.flatMap((page) =>
page.pos
.filter((widget) => !widget.options?.isReadOnly)
.map((widget) => ({
widget,
pageNumber: page.pageNumber
}))
const editableWidgets = (widgetsPosition?.placeHolder ?? []).flatMap(
({ pos = [], pageNumber }) =>
pos
.filter(
({ options }) =>
!options?.isReadOnly
)
.map((widget) => ({ widget, pageNumber }))
);
//get current index of widget
const currentIndex = editableWidgets.findIndex(
@@ -1926,13 +1948,11 @@ function WidgetsValueModal(props) {
</div>
</div>
<div className="flex justify-between items-center text-center">
{!["checkbox", "dropdown", "date", "radio"].includes(
currWidgetsDetails?.type
) ? (
{showClearbtn ? (
<button
type="button"
className="op-btn op-btn-ghost op-btn-sm text-base-content mr-1"
onClick={() => handleClear()}
onClick={handleClear}
>
{t("clear")}
</button>
@@ -3,15 +3,20 @@ import { isMobile } from "../../constant/Utils";
// `getWidgetType` is used to load ui of widget in side list
const getWidgetType = (item, widgetName) => {
return (
<div className="op-btn w-fit md:w-[100%] op-btn-primary op-btn-outline op-btn-sm focus:outline-none outline outline-[1.5px] ml-[6px] md:ml-0 p-0 overflow-hidden">
<div
title={widgetName}
className="op-btn w-fit md:w-[100%] op-btn-primary op-btn-outline op-btn-sm focus:outline-none ml-[6px] outline outline-[1px] md:ml-0 p-0 overflow-hidden"
>
<div className="w-full h-full flex md:justify-between items-center">
<div className="flex justify-start items-center text-[13px] ml-1">
{!isMobile && <i className="fa-light fa-grip-vertical ml-[3px]"></i>}
<span className="md:inline-block text-center text-[15px] ml-[5px] font-semibold pr-1 md:pr-0">
<div className="flex justify-start items-center ml-1 overflow-hidden">
{!isMobile && (
<i className="fa-light fa-grip-vertical ml-[3px] text-[13px]"></i>
)}
<span className="md:inline-block text-center text-[11px] mx-[3px] truncate font-medium capitalize">
{widgetName}
</span>
</div>
<div className="text-[20px] op-btn op-btn-primary rounded-none w-[40px] h-full flex justify-center items-center">
<div className="text-[13px] op-btn op-btn-primary rounded-none w-[20px] h-full flex justify-center items-center">
<i className={item.icon}></i>
</div>
</div>
+68 -97
View File
@@ -9,7 +9,7 @@ import fontkit from "@pdf-lib/fontkit";
import { themeColor } from "./const";
import { format, toZonedTime } from "date-fns-tz";
import i18n from "../i18n";
import { buildDownloadFilename } from "../utils";
import { applyNumberFormulasToPages, buildDownloadFilename } from "../utils";
export const fontsizeArr = [2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 24, 26, 28];
export const fontColorArr = ["red", "black", "blue", "yellow"];
@@ -327,6 +327,7 @@ export const widgets = [
{ type: "name", icon: "fa-light fa-user", iconSize: "21px" },
{ type: "job title", icon: "fa-light fa-address-card", iconSize: "17px" },
{ type: "company", icon: "fa-light fa-building", iconSize: "25px" },
{ type: "email", icon: "fa-light fa-envelope", iconSize: "20px" },
{ type: "date", icon: "fa-light fa-calendar-days", iconSize: "20px" },
{ type: textWidget, icon: "fa-light fa-text-width", iconSize: "20px" },
{ type: cellsWidget, icon: "fa-light fa-table-cells", iconSize: "20px" },
@@ -337,8 +338,7 @@ export const widgets = [
iconSize: "19px"
},
{ type: radioButtonWidget, icon: "fa-light fa-circle-dot", iconSize: "20px" },
{ type: "image", icon: "fa-light fa-image", iconSize: "20px" },
{ type: "email", icon: "fa-light fa-envelope", iconSize: "20px" }
{ type: "image", icon: "fa-light fa-image", iconSize: "20px" }
];
export const getDate = (dateformat) => {
@@ -410,6 +410,35 @@ export const changeDateToMomentFormat = (format) => {
return "L";
}
};
export const getSignerPages = (xyPosition = [], currentKey, signerId) => {
if (!Array.isArray(xyPosition)) {
return [];
}
const hasSigners = xyPosition.some((item) =>
Array.isArray(item?.placeHolder)
);
if (hasSigners) {
const candidates =
signerId !== undefined && signerId !== null && signerId !== ""
? xyPosition.filter(
(item) => item.Id === signerId || item.signerObjId === signerId
)
: xyPosition;
for (const signer of candidates) {
const pages = signer?.placeHolder || [];
const ownsWidget = pages.some((page) =>
(page?.pos || []).some((widget) => widget?.key === currentKey)
);
if (ownsWidget) {
return pages;
}
}
return [];
}
return xyPosition;
};
export const addWidgetOptions = (
type,
signer,
@@ -423,15 +452,9 @@ export const addWidgetOptions = (
return count + page.pos.filter((item) => item.type === type).length;
}, 0);
const count = countSameWidget + 1;
defaultOpt = {
name: `${type}-${count}`,
status: "required"
};
defaultOpt = { name: `${type}-${count}`, status: "required" };
} else {
defaultOpt = {
name: `${type}-1`,
status: "required"
};
defaultOpt = { name: `${type}-1`, status: "required" };
}
switch (type) {
case "signature":
@@ -439,11 +462,7 @@ export const addWidgetOptions = (
case "stamp":
return defaultOpt;
case "checkbox":
return {
...defaultOpt,
isReadOnly: false,
isHideLabel: false
};
return { ...defaultOpt, isReadOnly: false, isHideLabel: false };
case textInputWidget:
return { ...defaultOpt, isReadOnly: false };
case cellsWidget:
@@ -457,20 +476,11 @@ export const addWidgetOptions = (
case "initials":
return defaultOpt;
case "name":
return {
...defaultOpt,
defaultValue: widgetValue ? widgetValue : ""
};
return { ...defaultOpt, defaultValue: widgetValue ? widgetValue : "" };
case "company":
return {
...defaultOpt,
defaultValue: widgetValue ? widgetValue : ""
};
return { ...defaultOpt, defaultValue: widgetValue ? widgetValue : "" };
case "job title":
return {
...defaultOpt,
defaultValue: widgetValue ? widgetValue : ""
};
return { ...defaultOpt, defaultValue: widgetValue ? widgetValue : "" };
case "date": {
const dateFormat = signer?.DateFormat
? selectFormat(signer?.DateFormat)
@@ -518,15 +528,9 @@ export const addWidgetSelfsignOptions = (
return count + page.pos.filter((item) => item.type === type).length;
}, 0);
const count = countSameWidget + 1;
defaultOpt = {
name: `${type}-${count}`,
status: "required"
};
defaultOpt = { name: `${type}-${count}`, status: "required" };
} else {
defaultOpt = {
name: `${type}-1`,
status: "required"
};
defaultOpt = { name: `${type}-1`, status: "required" };
}
switch (type) {
case "signature":
@@ -1077,9 +1081,11 @@ export const onChangeInput = (
return obj;
});
const recalculatedPages = applyNumberFormulasToPages(newUpdateSignPos);
const newUpdateSigner = xyPosition.map((obj) => {
if (obj.Id === userId) {
return { ...obj, placeHolder: newUpdateSignPos };
return { ...obj, placeHolder: recalculatedPages };
}
return obj;
});
@@ -1125,7 +1131,7 @@ export const onChangeInput = (
}
return obj;
});
setXyPosition(updatePlaceholder);
setXyPosition(applyNumberFormulasToPages(updatePlaceholder));
}
};
//function to increase height of text area on press enter
@@ -1220,13 +1226,13 @@ export const calculateInitialWidthHeight = (widgetData) => {
export const widgetDataValue = (type, value) => {
switch (type) {
case "name":
return value?.Name;
return value?.Name || value?.name;
case "company":
return value?.Company;
return value?.Company || value?.company;
case "job title":
return value?.JobTitle;
return value?.JobTitle || value?.jobTitle;
case "email":
return value?.Email;
return value?.Email || value?.email;
default:
return "";
}
@@ -1850,6 +1856,16 @@ export const embedWidgetsToDoc = async (
console.log("error in image", e);
}
for (let [id, position] of widgetsPositionArr.entries()) {
const isTextTypeWidget = [
textWidget,
textInputWidget,
cellsWidget,
"name",
"company",
"job title",
"date",
"email"
].includes(position.type);
if (hasError) break; // Stop the inner loop if an error occurred
try {
let img;
@@ -1898,33 +1914,13 @@ export const embedWidgetsToDoc = async (
return y;
}
} else {
const WidgetsTypeTextExist = [
textWidget,
textInputWidget,
cellsWidget,
"name",
"company",
"job title",
"date",
"email"
].includes(position.type);
const yPosition = WidgetsTypeTextExist ? resizePos + 6 : resizePos;
const yPosition = isTextTypeWidget ? resizePos + 6 : resizePos;
return yPosition;
}
};
const color = position?.options?.fontColor;
const updateColorInRgb = getWidgetsFontColor(color);
const fontSize = parseInt(position?.options?.fontSize || 12);
const isTextTypeWidget = [
textWidget,
textInputWidget,
cellsWidget,
"name",
"company",
"job title",
"date",
"email"
].includes(position.type);
if (position.type === "checkbox") {
// Determine layout mode: 'vertical' (default) or 'horizontal'
// const isHorizontal = position.layout === "horizontal";
@@ -2022,7 +2018,7 @@ export const embedWidgetsToDoc = async (
textContent = position.options?.response;
}
} else if (position?.options?.defaultValue) {
textContent = position?.options?.defaultValue;
textContent = position?.options?.defaultValue?.toString();
}
if (position.type === cellsWidget) {
const cellCount =
@@ -2049,15 +2045,16 @@ export const embedWidgetsToDoc = async (
if (ch) page.drawText(ch, textPosition);
}
} else {
const fixedWidth = widgetWidth; // Set your fixed width
const isNewOnEnterLineExist = textContent.includes("\n");
const fixedWidth = widgetWidth; // Set your fixed
textContent = textContent?.toString();
const isNewOnEnterLineExist = textContent?.includes("\n");
// Function to break text into lines based on the fixed width
const NewbreakTextIntoLines = (textContent, width) => {
const lines = [];
let currentLine = "";
for (const word of textContent.split(" ")) {
for (const word of textContent?.split(" ")) {
//get text line width
const lineWidth = font.widthOfTextAtSize(
`${currentLine} ${word}`,
@@ -2242,7 +2239,6 @@ export const embedWidgetsToDoc = async (
width: widgetWidth,
height: widgetHeight
};
const imageOptions = getWidgetPosition(page, signature, 1, getSize);
page.drawImage(img, imageOptions);
}
@@ -2370,56 +2366,34 @@ export const contractDocument = async (documentId, include) => {
//function for add default signature or image for all requested location
export const addDefaultSignatureImg = (xyPosition, defaultSignImg, type) => {
let imgWH = { width: "", height: "" };
const img = new Image();
img.src = defaultSignImg;
if (img.complete) {
imgWH = { width: img.width, height: img.height };
}
let xyDefaultPos = [];
for (let i = 0; i < xyPosition.length; i++) {
// let getIMGWH;
const getXYdata = xyPosition[i].pos;
const getPageNo = xyPosition[i].pageNumber;
const getPosData = getXYdata;
const addSign = getPosData.map((position) => {
// getIMGWH = calculateImgAspectRatio(imgWH, position);
if (position.type) {
if (position?.type === type) {
return {
...position,
SignUrl: defaultSignImg,
// Width: getIMGWH.newWidth,
// Height: getIMGWH.newHeight,
ImageType: "default",
options: {
...position.options,
response: defaultSignImg
}
options: { ...position.options, response: defaultSignImg }
};
}
} else if (position && !position.isStamp) {
return {
...position,
SignUrl: defaultSignImg,
// Width: getIMGWH.newWidth,
// Height: getIMGWH.newHeight,
ImageType: "default",
options: {
...position.options,
response: defaultSignImg
}
options: { ...position.options, response: defaultSignImg }
};
}
return position;
});
const newXypos = {
pageNumber: getPageNo,
pos: addSign
};
const newXypos = { pageNumber: getPageNo, pos: addSign };
xyDefaultPos.push(newXypos);
}
return xyDefaultPos;
@@ -3359,10 +3333,7 @@ export const handleHeighlightWidget = (
...data,
pos: data.pos.map((position) => {
if (position.key === key) {
return {
...position,
zIndex: highestZIndex + 1
};
return { ...position, zIndex: highestZIndex + 1 };
}
return position;
})
+7 -17
View File
@@ -4,6 +4,10 @@ const parseAppId = process.env.REACT_APP_APPID
? process.env.REACT_APP_APPID
: "opensign";
const serverUrl = serverUrl_fn();
const commonheader = {
"Content-Type": "application/json",
"X-Parse-Application-Id": parseAppId
};
export const SaveFileSize = async (size, imageUrl, tenantId, userId) => {
//checking server url and save file's size
const tenantPtr = {
@@ -29,7 +33,6 @@ export const SaveFileSize = async (size, imageUrl, tenantId, userId) => {
);
const response = res.data.results;
let data;
// console.log("response", response);
if (response && response.length > 0) {
data = {
usedStorage: response[0].usedStorage
@@ -39,20 +42,12 @@ export const SaveFileSize = async (size, imageUrl, tenantId, userId) => {
await axios.put(
`${serverUrl}/classes/partners_TenantCredits/${response[0].objectId}`,
data,
{
headers: {
"Content-Type": "application/json",
"X-Parse-Application-Id": parseAppId
}
}
{ headers: commonheader }
);
} else {
data = { usedStorage: size, PartnersTenant: tenantPtr };
await axios.post(`${serverUrl}/classes/partners_TenantCredits`, data, {
headers: {
"Content-Type": "application/json",
"X-Parse-Application-Id": parseAppId
}
headers: commonheader
});
}
} catch (err) {
@@ -69,14 +64,9 @@ const saveDataFile = async (size, imageUrl, tenantPtr, UserId) => {
TenantPtr: tenantPtr,
...(UserId ? { UserId: UserId } : {})
};
// console.log("data save",file, data)
try {
await axios.post(`${serverUrl}/classes/partners_DataFiles`, data, {
headers: {
"Content-Type": "application/json",
"X-Parse-Application-Id": parseAppId
}
headers: commonheader
});
} catch (err) {
console.log("err in save usage ", err);
+1 -1
View File
@@ -49,7 +49,7 @@ import {
widgetDataValue,
getOriginalWH,
handleCheckResponse,
convertJpegToPng
convertJpegToPng,
} from "../constant/Utils";
import Header from "../components/pdf/PdfHeader";
import RenderPdf from "../components/pdf/RenderPdf";
+26 -47
View File
@@ -722,9 +722,7 @@ function PlaceHolderSign() {
(data) => data.pageNumber === pageNumber
);
if (getPageNumer.length > 0) {
const getXYdata = getPageNumer[0].pos;
const getPosData = getXYdata;
const addSignPos = getPosData.map((url) => {
const addSignPos = getPageNumer?.[0]?.pos?.map((url) => {
if (url.key === keyValue) {
return {
...url,
@@ -1455,9 +1453,9 @@ function PlaceHolderSign() {
(data) => data.pageNumber === pageNumber
);
if (getPageNumer.length > 0) {
const getXYdata = getPageNumer[0].pos;
const getPosData = getXYdata;
const addSignPos = getPosData.map((position) => {
const textSize = fontSize || currWidgetsDetails?.options?.fontSize;
const textColor = fontColor || currWidgetsDetails?.options?.fontColor;
const addSignPos = getPageNumer?.[0]?.pos?.map((position) => {
if (position.key === currWidgetsDetails?.key) {
if (currWidgetsDetails?.type === radioButtonWidget) {
if (addOption) {
@@ -1486,12 +1484,8 @@ function PlaceHolderSign() {
isReadOnly: isReadOnly || false,
isHideLabel: isHideLabel || false,
defaultValue: defaultValue,
fontSize:
fontSize || currWidgetsDetails?.options?.fontSize || 12,
fontColor:
fontColor ||
currWidgetsDetails?.options?.fontColor ||
"black"
fontSize: textSize || 12,
fontColor: textColor || "black"
}
};
}
@@ -1525,12 +1519,8 @@ function PlaceHolderSign() {
layout: layout,
isReadOnly: isReadOnly || false,
isHideLabel: isHideLabel || false,
fontSize:
fontSize || currWidgetsDetails?.options?.fontSize || 12,
fontColor:
fontColor ||
currWidgetsDetails?.options?.fontColor ||
"black"
fontSize: textSize || 12,
fontColor: textColor || "black"
}
};
}
@@ -1543,12 +1533,8 @@ function PlaceHolderSign() {
status: status,
values: dropdownOptions,
defaultValue: defaultValue,
fontSize:
fontSize || currWidgetsDetails?.options?.fontSize || 12,
fontColor:
fontColor ||
currWidgetsDetails?.options?.fontColor ||
"black",
fontSize: textSize || 12,
fontColor: textColor || "black",
...(isReadOnly ? { isReadOnly: isReadOnly || false } : {})
}
};
@@ -1563,9 +1549,11 @@ function PlaceHolderSign() {
}
return obj;
});
const recalculatedPlaceholders =
utils.applyNumberFormulasToPages(newUpdateSignPos);
const newUpdateSigner = signerPos.map((obj) => {
if (obj.Id === uniqueId) {
return { ...obj, placeHolder: newUpdateSignPos };
return { ...obj, placeHolder: recalculatedPlaceholders };
}
return obj;
});
@@ -1597,9 +1585,9 @@ function PlaceHolderSign() {
);
if (getPageNumer.length > 0) {
const getXYdata = getPageNumer[0].pos;
const getPosData = getXYdata;
const addSignPos = getPosData.map((position) => {
const textSize = fontSize || currWidgetsDetails?.options?.fontSize;
const textColor = fontColor || currWidgetsDetails?.options?.fontColor;
const addSignPos = getPageNumer?.[0]?.pos?.map((position) => {
if (position.key === currWidgetsDetails?.key) {
if (position.type === textInputWidget) {
return {
@@ -1612,12 +1600,8 @@ function PlaceHolderSign() {
defaultValue: defaultdata?.defaultValue || "",
validation:
{},
fontSize:
fontSize || currWidgetsDetails?.options?.fontSize || 12,
fontColor:
fontColor ||
currWidgetsDetails?.options?.fontColor ||
"black",
fontSize: textSize || 12,
fontColor: textColor || "black",
isReadOnly: defaultdata?.isReadOnly || false
}
};
@@ -1636,16 +1620,13 @@ function PlaceHolderSign() {
),
validation:
{},
fontSize:
fontSize || currWidgetsDetails?.options?.fontSize || 12,
fontColor:
fontColor ||
currWidgetsDetails?.options?.fontColor ||
"black",
fontSize: textSize || 12,
fontColor: textColor || "black",
isReadOnly: defaultdata?.isReadOnly || false
}
};
} else if (["signature"].includes(position.type)) {
}
else if (["signature"].includes(position.type)) {
return {
...position,
options: {
@@ -1663,12 +1644,8 @@ function PlaceHolderSign() {
status: defaultdata.status,
defaultValue: defaultdata.defaultValue,
hint: defaultdata?.hint || "",
fontSize:
fontSize || currWidgetsDetails?.options?.fontSize || 12,
fontColor:
fontColor ||
currWidgetsDetails?.options?.fontColor ||
"black"
fontSize: textSize || 12,
fontColor: textColor || "black"
}
};
}
@@ -2535,6 +2512,8 @@ function PlaceHolderSign() {
fontColor={fontColor}
setFontColor={setFontColor}
roleName={roleName}
widgetsSource={signerPos}
activeSignerId={uniqueId}
/>
<RotateAlert
showRotateAlert={showRotateAlert.status}
+57 -36
View File
@@ -69,6 +69,7 @@ import {
import WidgetsValueModal from "../components/pdf/WidgetsValueModal";
import WidgetNameModal from "../components/pdf/WidgetNameModal";
import CellsSettingModal from "../components/pdf/CellsSettingModal";
import { applyNumberFormulasToPages } from "../utils";
//For signYourself inProgress section signer can add sign and complete doc sign.
function SignYourSelf() {
const { t } = useTranslation();
@@ -140,6 +141,8 @@ function SignYourSelf() {
const [isDownloadModal, setIsDownloadModal] = useState(false);
const [isResize, setIsResize] = useState(false);
const [isUploadPdf, setIsUploadPdf] = useState(false);
const [unSignedWidgetId, setUnSignedWidgetId] = useState("");
const [widgetsTour, setWidgetsTour] = useState(false);
const [owner, setOwner] = useState({});
const [, drop] = useDrop({
@@ -586,9 +589,23 @@ function SignYourSelf() {
}
if (!isEnableOTP || isEmailVerified) {
let showAlert = false,
isSignatureExist = false;
widgetKey,
tourPageNumber;
try {
const isSignatureExist = xyPosition?.some((p) =>
p?.pos?.some((x) => x?.type === "signature")
);
if (!isSignatureExist) {
setIsAlert({
header: t("fields-required"),
isShow: true,
alertMessage: t("signature-widget-alert-1")
});
return;
}
for (let i = 0; i < xyPosition?.length; i++) {
tourPageNumber = xyPosition[i]?.pageNumber;
const requiredWidgets = xyPosition[i].pos.filter(
(position) => position.type !== "checkbox"
);
@@ -597,34 +614,24 @@ function SignYourSelf() {
for (let i = 0; i < requiredWidgets?.length; i++) {
checkSigned = requiredWidgets[i]?.options?.response;
if (!checkSigned) {
const checkSignUrl = requiredWidgets[i]?.pos?.SignUrl;
const checkSignUrl = requiredWidgets[i]?.SignUrl;
let checkDefaultSigned =
requiredWidgets[i]?.options?.defaultValue;
if (!checkSignUrl && !checkDefaultSigned && !showAlert) {
showAlert = true;
widgetKey = requiredWidgets[i]?.key;
}
}
}
}
//condition to check exist signature widget or not
if (!isSignatureExist) {
isSignatureExist = xyPosition[i].pos.some(
(data) => data?.type === "signature"
);
if (showAlert) {
break;
}
}
if (xyPosition.length === 0 || !isSignatureExist) {
setIsAlert({
header: t("fields-required"),
isShow: true,
alertMessage: t("signature-widget-alert-1")
});
return;
} else if (showAlert) {
setIsAlert({
isShow: true,
alertMessage: t("signature-widget-alert-2")
});
if (showAlert) {
setUnSignedWidgetId(widgetKey);
setPageNumber(tourPageNumber);
setWidgetsTour(true);
return;
} else {
setIsUiLoading(true);
@@ -771,9 +778,7 @@ function SignYourSelf() {
(data) => data.pageNumber === pageNumber
);
if (filterDropPos?.length > 0) {
const getXYdata = xyPosition[index].pos;
const getPosData = getXYdata;
const addSign = getPosData.map((url) => {
const addSign = xyPosition[index]?.pos?.map((url) => {
if (url.key === dragKey) {
return {
...url,
@@ -958,11 +963,9 @@ function SignYourSelf() {
(data) => data.pageNumber === pageNumber
);
if (getPageNumer.length > 0) {
const getXYdata = getPageNumer[0].pos;
const getPosData = getXYdata;
const widgetLayout =
currWidgetsDetails?.type === "checkbox" ? { layout: layout } : {};
const addSignPos = getPosData.map((position) => {
const addSignPos = getPageNumer?.[0]?.pos?.map((position) => {
if (position.key === currWidgetsDetails?.key) {
if (addOption) {
return {
@@ -1024,9 +1027,7 @@ function SignYourSelf() {
(data) => data.pageNumber === pageNumber
);
if (getPageNumer.length > 0) {
const getXYdata = getPageNumer[0].pos;
const getPosData = getXYdata;
const addSignPos = getPosData.map((position) => {
const addSignPos = getPageNumer?.[0]?.pos?.map((position) => {
if (position.key === currWidgetsDetails?.key) {
return {
...position,
@@ -1083,6 +1084,8 @@ function SignYourSelf() {
);
if (getPageNumer.length > 0) {
const updatePos = getPageNumer[0].pos.map((position) => {
const textSize = newFontSize || position?.options?.fontSize;
const textColor = newFontColor || position?.options?.fontColor;
if (position.key === currWidgetsDetails?.key) {
if (position.type === cellsWidget) {
const count = parseInt(
@@ -1096,20 +1099,19 @@ function SignYourSelf() {
name: defaultdata?.name || position.options?.name || "Cells",
cellCount: count,
defaultValue: (defaultdata?.defaultValue || "").slice(0, count),
fontSize: newFontSize || position.options?.fontSize || 12,
fontColor:
newFontColor || position.options?.fontColor || "black"
fontSize: textSize || 12,
fontColor: textColor || "black"
}
};
} else {
}
else {
return {
...position,
options: {
...position.options,
name: defaultdata?.name || position.options?.name,
fontSize: newFontSize || position.options?.fontSize || 12,
fontColor:
newFontColor || position.options?.fontColor || "black"
fontSize: textSize || 12,
fontColor: textColor || "black"
}
};
}
@@ -1119,7 +1121,7 @@ function SignYourSelf() {
const updateXYposition = xyPosition.map((obj, ind) =>
ind === index ? { ...obj, pos: updatePos } : obj
);
setXyPosition(updateXYposition);
setXyPosition(applyNumberFormulasToPages(updateXYposition));
}
setFontSize();
setFontColor();
@@ -1196,6 +1198,14 @@ function SignYourSelf() {
setPdfBase64Url(urlDetails.base64);
setShowRotateAlert({ status: false, degree: 0 });
};
const WidgetTourConfig = [
{
selector: '[data-tut="IsSigned"]',
content: t("signature-validate-alert-2"),
position: "top",
style: { fontSize: "13px" }
}
];
return (
<>
{isLoading.isLoad ? (
@@ -1241,6 +1251,14 @@ function SignYourSelf() {
isOpen={signTour}
/>
)}
<Tour
showNumber={false}
showNavigation={false}
showNavigationNumber={false}
onRequestClose={() => setWidgetsTour(false)}
steps={WidgetTourConfig}
isOpen={widgetsTour}
/>
{/* this component used to render all pdf pages in left side */}
<RenderAllPdfPage
allPages={allPages}
@@ -1408,6 +1426,7 @@ function SignYourSelf() {
divRef={divRef}
currWidgetsDetails={currWidgetsDetails}
isShowModal={isShowModal}
unSignedWidgetId={unSignedWidgetId}
/>
)}
</div>
@@ -1459,6 +1478,8 @@ function SignYourSelf() {
setFontSize={setFontSize}
fontColor={fontColor}
setFontColor={setFontColor}
widgetsSource={xyPosition}
isSelfSign={true}
/>
<CellsSettingModal
isOpen={isCellsSetting}
+27 -50
View File
@@ -662,9 +662,7 @@ const TemplatePlaceholder = () => {
(data) => data.pageNumber === pageNumber
);
if (getPageNumer.length > 0) {
const getXYdata = getPageNumer[0].pos;
const getPosData = getXYdata;
const addSignPos = getPosData.map((url) => {
const addSignPos = getPageNumer?.[0]?.pos?.map((url) => {
if (url.key === keyValue) {
return {
...url,
@@ -756,7 +754,7 @@ const TemplatePlaceholder = () => {
}
};
useEffect(() => {
if (pdfDetails?.[0]?.CreatedBy?.objectId !== user?.objectId) return;
if (pdfDetails?.[0]?.CreatedBy?.objectId !== user?.objectId || isPrefillModal) return;
const timer = setTimeout(() => {
autosavedetails();
}, 2000);
@@ -1319,10 +1317,9 @@ const TemplatePlaceholder = () => {
);
if (getPageNumer.length > 0) {
const getXYdata = getPageNumer[0].pos;
const getPosData = getXYdata;
const addSignPos = getPosData.map((position) => {
const textSize = fontSize || currWidgetsDetails?.options?.fontSize;
const textColor = fontColor || currWidgetsDetails?.options?.fontColor;
const addSignPos = getPageNumer?.[0]?.pos?.map((position) => {
if (position.key === currWidgetsDetails?.key) {
if (currWidgetsDetails?.type === radioButtonWidget) {
if (addOption) {
@@ -1351,12 +1348,8 @@ const TemplatePlaceholder = () => {
defaultValue: defaultValue,
isReadOnly: isReadOnly || false,
isHideLabel: isHideLabel || false,
fontSize:
fontSize || currWidgetsDetails?.options?.fontSize || 12,
fontColor:
fontColor ||
currWidgetsDetails?.options?.fontColor ||
"black"
fontSize: textSize || 12,
fontColor: textColor || "black"
}
};
}
@@ -1390,12 +1383,8 @@ const TemplatePlaceholder = () => {
isReadOnly: isReadOnly || false,
defaultValue: defaultValue,
isHideLabel: isHideLabel || false,
fontSize:
fontSize || currWidgetsDetails?.options?.fontSize || 12,
fontColor:
fontColor ||
currWidgetsDetails?.options?.fontColor ||
"black"
fontSize: textSize || 12,
fontColor: textColor || "black"
}
};
}
@@ -1408,12 +1397,8 @@ const TemplatePlaceholder = () => {
status: status,
values: dropdownOptions,
defaultValue: defaultValue,
fontSize:
fontSize || currWidgetsDetails?.options?.fontSize || 12,
fontColor:
fontColor ||
currWidgetsDetails?.options?.fontColor ||
"black",
fontSize: textSize || 12,
fontColor: textColor || "black",
...(isReadOnly ? { isReadOnly: isReadOnly || false } : {})
}
};
@@ -1428,9 +1413,11 @@ const TemplatePlaceholder = () => {
}
return obj;
});
const recalculatedPlaceholders =
utils.applyNumberFormulasToPages(newUpdateSignPos);
const newUpdateSigner = signerPos.map((obj) => {
if (obj.Id === uniqueId) {
return { ...obj, placeHolder: newUpdateSignPos };
return { ...obj, placeHolder: recalculatedPlaceholders };
}
return obj;
});
@@ -1463,9 +1450,9 @@ const TemplatePlaceholder = () => {
);
if (getPageNumer.length > 0) {
const getXYdata = getPageNumer[0].pos;
const getPosData = getXYdata;
const addSignPos = getPosData.map((position) => {
const textSize = fontSize || currWidgetsDetails?.options?.fontSize;
const textColor = fontColor || currWidgetsDetails?.options?.fontColor;
const addSignPos = getPageNumer?.[0]?.pos?.map((position) => {
if (position.key === currWidgetsDetails?.key) {
if (position.type === textInputWidget) {
return {
@@ -1479,12 +1466,8 @@ const TemplatePlaceholder = () => {
isReadOnly: defaultdata?.isReadOnly || false,
validation:
{},
fontSize:
fontSize || currWidgetsDetails?.options?.fontSize || 12,
fontColor:
fontColor ||
currWidgetsDetails?.options?.fontColor ||
"black"
fontSize: textSize || 12,
fontColor: textColor || "black"
}
};
} else if (position.type === cellsWidget) {
@@ -1503,15 +1486,12 @@ const TemplatePlaceholder = () => {
validation:
{},
isReadOnly: defaultdata?.isReadOnly || false,
fontSize:
fontSize || currWidgetsDetails?.options?.fontSize || 12,
fontColor:
fontColor ||
currWidgetsDetails?.options?.fontColor ||
"black"
fontSize: textSize || 12,
fontColor: textColor || "black"
}
};
} else if (["signature"].includes(position.type)) {
}
else if (["signature"].includes(position.type)) {
return {
...position,
options: {
@@ -1529,12 +1509,8 @@ const TemplatePlaceholder = () => {
status: defaultdata.status,
defaultValue: defaultdata.defaultValue,
hint: defaultdata?.hint || "",
fontSize:
fontSize || currWidgetsDetails?.options?.fontSize || 12,
fontColor:
fontColor ||
currWidgetsDetails?.options?.fontColor ||
"black"
fontSize: textSize || 12,
fontColor: textColor || "black"
}
};
}
@@ -2125,6 +2101,8 @@ const TemplatePlaceholder = () => {
fontColor={fontColor}
setFontColor={setFontColor}
roleName={roleName}
widgetsSource={signerPos}
activeSignerId={uniqueId}
/>
{isPrefillModal && (
<PrefillWidgetModal
@@ -2140,7 +2118,6 @@ const TemplatePlaceholder = () => {
navigatePageToDoc={navigatePageToDoc}
setIsNewContact={setIsNewContact}
isNewContact={isNewContact}
documentFlow={"template"}
docId={pdfDetails[0]?.objectId}
isSubmit={isUseTemplate}
/>
+1 -1
View File
@@ -14,7 +14,7 @@ const Tooltip = ({
onClick={() =>
handleOnlickHelp ? handleOnlickHelp() : openInNewTab(url)
}
className={"text-center cursor-pointer"}
className="text-center cursor-pointer focus:outline-none"
>
<sup>
<i
+38 -1
View File
@@ -1,4 +1,4 @@
import { generateTitleFromFilename, getBase64FromUrl } from "../constant/Utils";
import { generateTitleFromFilename } from "../constant/Utils";
import { base64StringtoFile, uploadFile } from "./fileUtils";
import { sanitizeFileName } from "./sanitizeFileName";
import Parse from "parse";
@@ -41,3 +41,40 @@ export const hasSignatureWidget = (s) =>
(s.placeHolder ?? []).some((ph) =>
(ph?.pos ?? []).some((p) => p?.type === "signature")
);
/**
*
* @param {Array<object>} pages
* Array of page objects. Each page is expected to have:
* - pos: Array<Widget> where Widget has shape:
* {
* key: string,
* type: string,
* options?: {
* response?: number|string,
* defaultValue?: number|string,
* formula?: string
* }
* }
*
* @returns {Array<object>}
*/
export const applyNumberFormulasToPages = (pages = []) => {
// Guard: if not an array or empty, just return as-is
if (!Array.isArray(pages) || pages.length === 0) {
return pages;
}
// Clone pages and each widget.options so we do not mutate caller's data.
// (Shallow clone is sufficient for our current usage since we only touch `options`.)
const clonedPages = pages.map((page) => ({
...page,
pos: (page?.pos || []).map((widget) => ({
...widget,
options: { ...widget.options }
}))
}));
// Return the cloned, updated pages
return clonedPages;
};
+118 -138
View File
@@ -1,169 +1,149 @@
import fs from 'node:fs';
import axios from 'axios';
import multer from 'multer';
import libre from 'libreoffice-convert';
import util from 'node:util';
import { cloudServerUrl, getSecureUrl, serverAppId } from '../../Utils.js';
libre.convertAsync = util.promisify(libre.convert);
/** @returns {Promise<Buffer>} */
async function convertLibre(input, ext, opts) {
return await new Promise((resolve, reject) => {
try {
libre.convert(input, ext, opts, (err, out) => (err ? reject(err) : resolve(out)));
} catch (e) {
reject(e);
}
});
}
// -------------------- Concurrency limiter (no dependency) --------------------
const MAX_CONCURRENCY = Number(process.env.DOCX2PDF_CONCURRENCY || 2); // 1 for tiny droplets
let active = 0;
const queue = [];
function runWithLimit(task) {
return new Promise((resolve, reject) => {
const run = () => {
active++;
Promise.resolve()
.then(task)
.then(resolve, reject)
.finally(() => {
active--;
if (queue.length) queue.shift()();
});
};
if (active < MAX_CONCURRENCY) run();
else queue.push(run);
});
}
const storage = multer.diskStorage({
destination(req, file, cb) {
cb(null, 'exports');
},
filename(req, file, cb) {
cb(null, file.originalname);
// -------------------- Timeout helper (async version = no TS hint) --------------------
/**
* @template T
* @param {Promise<T>} promise
* @param {number} ms
* @param {string} [label='operation']
* @returns {Promise<T>}
*/
export async function withTimeout(promise, ms, label = 'operation') {
let timer;
try {
const timeout = new Promise((_, reject) => {
timer = setTimeout(() => reject(new Error(`${label} timed out after ${ms}ms`)), ms);
});
return await Promise.race([promise, timeout]);
} finally {
if (timer) clearTimeout(timer);
}
}
// -------------------- Multer: use memory storage --------------------
const storage = multer.memoryStorage();
export const upload = multer({
storage,
fileFilter: (req, file, cb) => {
const okExt = /\.docx$/i.test(file.originalname || '');
const okMime =
file.mimetype === 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' ||
file.mimetype === 'application/octet-stream';
if (okExt && okMime) return cb(null, true);
cb(new Error('Only .docx files are supported'));
},
});
export const upload = multer({ storage });
function generatePdfName(length) {
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
let result = '';
for (let i = 0; i < length; i++) {
result += chars.charAt(Math.floor(Math.random() * chars.length));
}
for (let i = 0; i < length; i++) result += chars.charAt(Math.floor(Math.random() * chars.length));
return result;
}
export default async function docxtopdf(req, res) {
const serverUrl = cloudServerUrl;
const appId = serverAppId;
const parseAppKey = { 'X-Parse-Application-Id': serverAppId };
const masterKey = process.env.MASTER_KEY;
const inputPath = req.file.path;
const name = generatePdfName(16);
const fileName = `${name}.pdf`;
const outputPath = './exports/output.pdf';
const masterHeader = { ...parseAppKey, 'X-Parse-Master-Key': masterKey };
const sessionHeader = { ...parseAppKey, 'X-Parse-Session-Token': req.headers['sessiontoken'] };
if (!req.file || !req.file.buffer) {
return res.status(400).json({ error: 'No file uploaded.' });
}
try {
const userRes = await axios.get(serverUrl + '/users/me', {
headers: {
'X-Parse-Application-Id': appId,
'X-Parse-Session-Token': req.headers['sessiontoken'],
},
});
const userId = JSON.stringify({
UserId: {
__type: 'Pointer',
className: '_User',
objectId: userRes.data.objectId,
},
// ---- Auth: current user ----
const userRes = await axios.get(`${serverUrl}/users/me`, { headers: sessionHeader });
// ---- contracts_Users ----
const whereUser = JSON.stringify({
UserId: { __type: 'Pointer', className: '_User', objectId: userRes.data.objectId },
});
const resUser = await axios.get(
serverUrl + `/classes/contracts_Users?where=${userId}&limit=1&include=TenantId`,
{
headers: {
'X-Parse-Application-Id': appId,
'X-Parse-Master-Key': masterKey,
},
}
`${serverUrl}/classes/contracts_Users?where=${whereUser}&limit=1&include=TenantId`,
{ headers: masterHeader }
);
if (resUser?.data?.results?.length > 0) {
const tenantId = resUser.data.results[0].TenantId?.objectId;
const ext = '.pdf';
const outPath = `./exports/output${ext}`;
const docxBuf = fs.readFileSync(inputPath);
const pdfBuffer = await libre.convertAsync(docxBuf, ext, undefined);
fs.writeFileSync(outPath, pdfBuffer);
const file = fs.readFileSync(outPath);
const size = fs.statSync(outPath).size;
const PartnersTenant = JSON.stringify({
PartnersTenant: {
__type: 'Pointer',
className: 'partners_Tenant',
objectId: tenantId,
},
});
const resTenantCredit = await axios.get(
serverUrl + `/classes/partners_TenantCredits?where=${PartnersTenant}&limit=1`,
{
headers: {
'X-Parse-Application-Id': appId,
'X-Parse-Master-Key': masterKey,
},
}
);
if (resTenantCredit.data?.results?.length > 0) {
const tenantCreditsId = resTenantCredit.data.results[0].objectId;
const activeFileAdapter = resUser.data.results[0].TenantId?.ActiveFileAdapter;
let fileUrl;
if (activeFileAdapter) {
const params = {
fileBase64: file.toString('base64'),
fileName,
id: activeFileAdapter,
};
const url = serverUrl + '/functions/savetofileadapter';
const headers = {
'Content-Type': 'application/json',
'X-Parse-Application-Id': appId,
'X-Parse-Session-Token': req.headers['sessiontoken'],
};
try {
const savetos3 = await axios.post(url, params, { headers });
fileUrl = savetos3?.data?.result?.url;
} catch (err) {
console.log('err in save to customfile', err);
}
} else {
const parsefile = await axios.post(serverUrl + `/files/${fileName}`, file, {
headers: {
'X-Parse-Application-Id': appId,
'X-Parse-Master-Key': masterKey,
'Content-Type': 'application/pdf',
},
});
const fileRes = getSecureUrl(parsefile.data.url);
fileUrl = fileRes.url;
}
const usedStorage = resTenantCredit.data.results[0].usedStorage
? resTenantCredit.data.results[0].usedStorage + size
: size;
await axios.put(
serverUrl + `/classes/partners_TenantCredits/${tenantCreditsId}`,
{ usedStorage },
{
headers: {
'X-Parse-Application-Id': appId,
'X-Parse-Master-Key': masterKey,
},
}
);
await axios.post(
serverUrl + '/classes/partners_DataFiles',
{
FileSize: size,
FileUrl: fileUrl,
TenantPtr: {
__type: 'Pointer',
className: 'partners_Tenant',
objectId: tenantId,
},
UserId: {
__type: 'Pointer',
className: '_User',
objectId: userRes.data.objectId,
},
},
{
headers: {
'X-Parse-Application-Id': appId,
'X-Parse-Master-Key': masterKey,
},
}
);
[inputPath, outPath].forEach(p => fs.existsSync(p) && fs.unlinkSync(p));
return res.status(200).json({ message: 'success.', url: fileUrl });
}
if (!resUser?.data?.results?.length) {
return res.status(403).json({ error: 'User not linked to tenant.' });
}
const tenantId = resUser.data.results[0]?.TenantId?.objectId;
if (!tenantId) {
return res.status(403).json({ error: 'Tenant not found for user.' });
}
// ---- DOCX -> PDF (buffer -> buffer), with concurrency + timeout ----
const fileName = `${generatePdfName(16)}.pdf`;
const pdfBuffer = await runWithLimit(() =>
withTimeout(convertLibre(req.file.buffer, '.pdf', undefined), 60_000, 'DOCX->PDF')
);
// ---- Upload PDF (no disk IO) ----
const activeFileAdapter = resUser.data.results[0]?.TenantId?.ActiveFileAdapter;
let fileUrl;
if (activeFileAdapter) {
const params = {
fileBase64: Buffer.from(pdfBuffer).toString('base64'),
fileName,
id: activeFileAdapter,
};
const url = serverUrl + '/functions/savetofileadapter';
const headers = { 'Content-Type': 'application/json', ...sessionHeader };
const savetos3 = await axios.post(url, params, { headers });
fileUrl = savetos3?.data?.result?.url;
if (!fileUrl) throw new Error('No URL returned from file adapter');
} else {
const parsefile = await axios.post(`${serverUrl}/files/${fileName}`, pdfBuffer, {
headers: { ...masterHeader, 'Content-Type': 'application/pdf' },
});
const fileRes = getSecureUrl(parsefile.data.url);
fileUrl = fileRes.url;
}
return res.status(200).json({ message: 'success.', url: fileUrl });
} catch (err) {
[inputPath, outputPath].forEach(p => fs.existsSync(p) && fs.unlinkSync(p));
const msg =
err?.response?.data?.error || err?.response?.data || err?.message || 'Something went wrong.';
console.log(`Error converting file: ${msg}`);
console.log(`Error in docxtopdf: ${msg}`);
// Friendly message to the client
const message =
'We are currently experiencing some issues with processing DOCX files. Please upload the PDF version or contact us on support@opensignlabs.com';
return res.status(400).json({ error: message });
@@ -59,6 +59,7 @@ export default async function saveAsTemplate(request) {
options: {
...p.options,
status: 'required',
response: '',
...(p?.options?.defaultValue ? { defaultValue: '' } : {}),
},
})),
+1
View File
@@ -110,6 +110,7 @@ export const config = {
appName: appName,
allowClientClassCreation: false,
allowExpiredAuthDataToken: false,
enableInsecureAuthAdapters: false,
encodeParseObjectInCloudFunction: true,
...(isMailAdapter === true
? {
+2511 -2508
View File
File diff suppressed because it is too large Load Diff
+14 -14
View File
@@ -1,10 +1,10 @@
{
"name": "open_sign_server",
"version": "2.26.0",
"description": "An example Parse API server using the parse-server module",
"version": "2.30.0",
"description": "opensign server using the parse-server module",
"repository": {
"type": "git",
"url": "https://github.com/ParsePlatform/parse-server-example"
"url": "https://github.com/OpenSignLabs/OpenSign"
},
"license": "MIT",
"main": "index.js",
@@ -18,8 +18,8 @@
"watch": "nodemon index.js"
},
"dependencies": {
"@aws-sdk/client-s3": "^3.896.0",
"@aws-sdk/s3-request-presigner": "^3.896.0",
"@aws-sdk/client-s3": "^3.901.0",
"@aws-sdk/s3-request-presigner": "^3.901.0",
"@parse/fs-files-adapter": "^3.0.0",
"@parse/s3-files-adapter": "^4.2.0",
"@pdf-lib/fontkit": "^1.1.1",
@@ -31,27 +31,27 @@
"coherentpdf": "^2.5.5",
"cors": "^2.8.5",
"date-fns-tz": "^3.2.0",
"dotenv": "^17.2.2",
"dotenv": "^17.2.3",
"express": "^5.1.0",
"form-data": "^4.0.4",
"generate-api-key": "^1.0.2",
"googleapis": "^159.0.0",
"googleapis": "^161.0.0",
"libreoffice-convert": "^1.7.0",
"mailgun.js": "^12.1.0",
"mongodb": "^6.20.0",
"multer": "^2.0.2",
"multer-s3": "^3.0.1",
"node-forge": "^1.3.1",
"nodemailer": "^7.0.6",
"nodemailer": "^7.0.9",
"p-limit": "^7.1.1",
"parse": "^6.1.1",
"parse-dbtool": "^1.2.0",
"parse-server": "^8.2.4",
"parse-server": "^8.2.5",
"parse-server-api-mail-adapter": "^4.1.0",
"pdf-lib": "^1.17.1",
"posthog-node": "^5.9.1",
"posthog-node": "^5.9.3",
"qrcode": "^1.5.4",
"rate-limiter-flexible": "^7.4.0",
"rate-limiter-flexible": "^8.0.1",
"sharp": "^0.34.4",
"speakeasy": "^2.0.0",
"ws": "^8.18.3"
@@ -59,8 +59,8 @@
"type": "module",
"devDependencies": {
"@babel/eslint-parser": "^7.28.4",
"eslint": "^9.36.0",
"jasmine": "^5.11.0",
"eslint": "^9.37.0",
"jasmine": "^5.12.0",
"mongodb-runner": "^5.10.0",
"nodemon": "^3.1.10",
"nyc": "^17.1.0",
@@ -74,4 +74,4 @@
"engines": {
"node": "18 || 20 || 22"
}
}
}