From d3cb8d575c29292308ff3a646db99edf722efc1f Mon Sep 17 00:00:00 2001 From: raktima-opensignlabs Date: Tue, 26 May 2026 04:33:08 +0000 Subject: [PATCH] Merge pull request #2474 from nxglabs/sync-to-public_repo-26403170706 Merge pull request #2473 from nxglabs/raktima-patch-main-6 --- apps/OpenSign/server.cjs | 160 ++++++++++++++++++ .../bulksend/components/PrefillWidgets.jsx | 7 +- .../components/pdf/PrefillWidgetsModal.jsx | 30 ++-- 3 files changed, 186 insertions(+), 11 deletions(-) create mode 100644 apps/OpenSign/server.cjs diff --git a/apps/OpenSign/server.cjs b/apps/OpenSign/server.cjs new file mode 100644 index 000000000..fa5c218ea --- /dev/null +++ b/apps/OpenSign/server.cjs @@ -0,0 +1,160 @@ +#!/usr/bin/env node +/* eslint-env node */ +/* eslint-disable no-console */ +// Tiny static file server for the production build. +// - Serves real files from ./build (including dotfile dirs like .well-known) +// - Falls back to /index.html only when the requested path does not exist +// (SPA client-side routing). +const http = require("node:http"); +const fs = require("node:fs"); +const path = require("node:path"); + +const root = path.join(__dirname, "build"); +const port = Number(process.env.PORT) || 3000; +const host = process.env.HOST || "0.0.0.0"; + +const mime = { + ".html": "text/html; charset=utf-8", + ".js": "application/javascript; charset=utf-8", + ".mjs": "application/javascript; charset=utf-8", + ".css": "text/css; charset=utf-8", + ".json": "application/json; charset=utf-8", + ".map": "application/json; charset=utf-8", + ".svg": "image/svg+xml", + ".png": "image/png", + ".jpg": "image/jpeg", + ".jpeg": "image/jpeg", + ".gif": "image/gif", + ".ico": "image/x-icon", + ".webp": "image/webp", + ".woff": "font/woff", + ".woff2": "font/woff2", + ".ttf": "font/ttf", + ".otf": "font/otf", + ".txt": "text/plain; charset=utf-8", + ".pdf": "application/pdf", + ".csv": "text/csv; charset=utf-8", + ".wasm": "application/wasm", + ".xml": "application/xml; charset=utf-8" +}; + +function contentType(filePath) { + return mime[path.extname(filePath).toLowerCase()] || "application/octet-stream"; +} + + +function safeJoin(reqPath) { + let decoded; + try { + decoded = decodeURIComponent(reqPath.split("?")[0].split("#")[0]); + } catch { + return null; + } + const resolved = path.normalize(path.join(root, decoded)); + if (resolved !== root && !resolved.startsWith(root + path.sep)) return null; + return resolved; +} + +function cacheControl(filePath) { + const ext = path.extname(filePath).toLowerCase(); + const relativePath = path.relative(root, filePath); + const isBuildAsset = + relativePath === "assets" || + relativePath.startsWith(`assets${path.sep}`); + + if (ext === ".html" || ext === "") { + return "no-cache"; + } + + return isBuildAsset + ? "public, max-age=31536000, immutable" + : "no-cache"; +} + +function streamFile(req, res, filePath, stats) { + const headers = { + "Content-Type": contentType(filePath), + "Content-Length": stats.size, + "Last-Modified": stats.mtime.toUTCString(), + "Cache-Control": cacheControl(filePath) + }; + if (req.method === "HEAD") { + res.writeHead(200, headers); + return res.end(); + } + const stream = fs.createReadStream(filePath); + stream.on("error", (err) => { + console.error("Stream error:", err); + if (!res.headersSent) { + res.writeHead(500, { "Content-Type": "text/plain" }); + res.end("Internal Server Error"); + } else { + res.destroy(); + } + }); + res.on("close", () => { + if (!stream.destroyed) stream.destroy(); + }); + res.on("error", (err) => { + console.error("Response error:", err); + if (!stream.destroyed) stream.destroy(); + }); + res.writeHead(200, headers); + stream.pipe(res); +} + +function sendIndex(req, res) { + const indexPath = path.join(root, "index.html"); + fs.stat(indexPath, (err, stats) => { + if (err || !stats || !stats.isFile()) { + res.writeHead(500, { "Content-Type": "text/plain" }); + return res.end("index.html not found"); + } + streamFile(req, res, indexPath, stats); + }); +} + +const server = http.createServer((req, res) => { + if (req.method === "OPTIONS") { + const filePath = safeJoin(req.url || "/"); + res.writeHead(204); + return res.end(); + } + + if (req.method !== "GET" && req.method !== "HEAD") { + res.writeHead(405, { Allow: "GET, HEAD, OPTIONS" }); + return res.end(); + } + + const reqUrl = req.url || "/"; + const filePath = safeJoin(reqUrl); + if (!filePath) { + res.writeHead(400); + return res.end("Bad Request"); + } + + fs.stat(filePath, (err, stats) => { + if (err) { + // No file at this path → SPA fallback to index.html + return sendIndex(req, res); + } + if (stats.isFile()) { + return streamFile(req, res, filePath, stats); + } + if (stats.isDirectory()) { + const indexInDir = path.join(filePath, "index.html"); + return fs.stat(indexInDir, (dirErr, dirStats) => { + if (!dirErr && dirStats && dirStats.isFile()) { + return streamFile(req, res, indexInDir, dirStats); + } + return sendIndex(req, res); + }); + } + // Path exists but is neither file nor directory → SPA fallback + return sendIndex(req, res); + }); +}); + +server.listen(port, host, () => { + console.log(`Serving ${root} on http://${host}:${port}`); +}); \ No newline at end of file diff --git a/apps/OpenSign/src/components/bulksend/components/PrefillWidgets.jsx b/apps/OpenSign/src/components/bulksend/components/PrefillWidgets.jsx index 3dea73c76..ac4cde54d 100644 --- a/apps/OpenSign/src/components/bulksend/components/PrefillWidgets.jsx +++ b/apps/OpenSign/src/components/bulksend/components/PrefillWidgets.jsx @@ -32,7 +32,12 @@ const PrefillWidgets = ({ prefills = [], setPrefills, onNext }) => { >
- {prefills.map((widget, index) => ( + {[...prefills] + .sort((a, b) => + a.pageNumber !== b.pageNumber + ? a.pageNumber - b.pageNumber + : (a.yPosition ?? 0) - (b.yPosition ?? 0) + ).map((widget, index) => ( - page.pos - .filter((widget) => !widget.options?.isReadOnly) - .map((widget) => ({ - widget, - pageNumber: page.pageNumber - })) - ); + // Flatten the filtered array, exclude read-only widgets, + // carry yPosition for sorting, then sort by pageNumber asc → yPosition asc + // (mirrors the newSignPos.sort in PdfRequestFiles so widgets appear in + // the same top-to-bottom, page-1-first order as they do in the document) + const flatArray = filteredArray + ?.flatMap((page) => + page.pos + .filter((widget) => !widget.options?.isReadOnly) + .map((widget) => ({ + widget, + pageNumber: page.pageNumber, + yPosition: widget.yPosition ?? 0 + })) + ) + ?.sort((a, b) => + a.pageNumber !== b.pageNumber + ? a.pageNumber - b.pageNumber // primary: page order (page 1 first) + : a.yPosition - b.yPosition // secondary: top-to-bottom within page + ); return flatArray || []; }, [props.prefillData]); @@ -896,4 +906,4 @@ function PrefillWidgetModal(props) { ); } -export default PrefillWidgetModal; +export default PrefillWidgetModal; \ No newline at end of file