From a57ae3439b847ca80a9a81ca673d042df7598e2e Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Wed, 19 Mar 2025 16:14:50 +0000 Subject: [PATCH 01/69] Add UI for selecting PDF template --- .../design/ScreenDetailsModal.svelte | 1 + packages/builder/src/constants/index.ts | 1 + .../NewScreen/CreateScreenModal.svelte | 2 +- .../_components/NewScreen/images/pdf.svg | 38 +++++++++++++++ .../design/_components/NewScreen/index.svelte | 47 ++++++++++++++----- 5 files changed, 76 insertions(+), 13 deletions(-) create mode 100644 packages/builder/src/pages/builder/app/[application]/design/_components/NewScreen/images/pdf.svg diff --git a/packages/builder/src/components/design/ScreenDetailsModal.svelte b/packages/builder/src/components/design/ScreenDetailsModal.svelte index 3f8e08d031..410ddee8a9 100644 --- a/packages/builder/src/components/design/ScreenDetailsModal.svelte +++ b/packages/builder/src/components/design/ScreenDetailsModal.svelte @@ -80,5 +80,6 @@ white-space: nowrap; overflow: hidden; text-overflow: ellipsis; + margin-top: 4px; } diff --git a/packages/builder/src/constants/index.ts b/packages/builder/src/constants/index.ts index 3c3a6888ad..7068a1bb96 100644 --- a/packages/builder/src/constants/index.ts +++ b/packages/builder/src/constants/index.ts @@ -71,4 +71,5 @@ export const AutoScreenTypes = { BLANK: "blank", TABLE: "table", FORM: "form", + PDF: "pdf", } diff --git a/packages/builder/src/pages/builder/app/[application]/design/_components/NewScreen/CreateScreenModal.svelte b/packages/builder/src/pages/builder/app/[application]/design/_components/NewScreen/CreateScreenModal.svelte index edc502bbb4..2dedec96b4 100644 --- a/packages/builder/src/pages/builder/app/[application]/design/_components/NewScreen/CreateScreenModal.svelte +++ b/packages/builder/src/pages/builder/app/[application]/design/_components/NewScreen/CreateScreenModal.svelte @@ -53,7 +53,7 @@ // Otherwise choose a datasource datasourceModal.show() } - } else if (mode === AutoScreenTypes.BLANK) { + } else if (mode === AutoScreenTypes.BLANK || mode === AutoScreenTypes.PDF) { screenDetailsModal.show() } else { throw new Error("Invalid mode provided") diff --git a/packages/builder/src/pages/builder/app/[application]/design/_components/NewScreen/images/pdf.svg b/packages/builder/src/pages/builder/app/[application]/design/_components/NewScreen/images/pdf.svg new file mode 100644 index 0000000000..16ac02e2d7 --- /dev/null +++ b/packages/builder/src/pages/builder/app/[application]/design/_components/NewScreen/images/pdf.svg @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/packages/builder/src/pages/builder/app/[application]/design/_components/NewScreen/index.svelte b/packages/builder/src/pages/builder/app/[application]/design/_components/NewScreen/index.svelte index bbd6fda256..1797af4be3 100644 --- a/packages/builder/src/pages/builder/app/[application]/design/_components/NewScreen/index.svelte +++ b/packages/builder/src/pages/builder/app/[application]/design/_components/NewScreen/index.svelte @@ -4,8 +4,10 @@ import blank from "./images/blank.svg" import table from "./images/tableInline.svg" import form from "./images/formUpdate.svg" + import pdf from "./images/pdf.svg" import CreateScreenModal from "./CreateScreenModal.svelte" import { screenStore } from "@/stores/builder" + import { AutoScreenTypes } from "@/constants" export let onClose = null @@ -27,32 +29,54 @@
-
createScreenModal.show("blank")}> +
createScreenModal.show(AutoScreenTypes.BLANK)} + >
A blank screen
- Blank + Blank Add an empty blank screen
-
createScreenModal.show("table")}> +
createScreenModal.show(AutoScreenTypes.TABLE)} + >
A table of data
- Table + Table List rows in a table
-
createScreenModal.show("form")}> +
createScreenModal.show(AutoScreenTypes.PDF)} + > +
+ A form containing data +
+
+ PDF Editor + Create, edit and export your PDF +
+
+ +
createScreenModal.show(AutoScreenTypes.FORM)} + >
A form containing data
- Form + Form Capture data from your users
@@ -111,14 +135,13 @@ .text { border: 1px solid var(--grey-4); border-radius: 0 0 4px 4px; - padding: 8px 16px 13px 16px; - } - - .text :global(p:nth-child(1)) { - margin-bottom: 6px; + padding: 12px 16px 12px 16px; + display: flex; + flex-direction: column; + gap: 2px; } .text :global(p:nth-child(2)) { - color: var(--grey-6); + color: var(--spectrum-global-color-gray-600); } From 76cde11eb0ef83b7e514398f4612e03bd98c37d7 Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Wed, 19 Mar 2025 16:21:37 +0000 Subject: [PATCH 02/69] Convert PDF plugin to native component --- .../new/_components/componentStructure.json | 3 +- packages/client/manifest.json | 37 +++ packages/client/package.json | 1 + packages/client/src/components/app/index.js | 1 + .../client/src/components/app/pdf/PDF.svelte | 143 ++++++++++++ packages/client/src/components/app/pdf/pdf.js | 156 +++++++++++++ yarn.lock | 216 +++++++++++++++++- 7 files changed, 546 insertions(+), 11 deletions(-) create mode 100644 packages/client/src/components/app/pdf/PDF.svelte create mode 100644 packages/client/src/components/app/pdf/pdf.js diff --git a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/[componentId]/new/_components/componentStructure.json b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/[componentId]/new/_components/componentStructure.json index d809095dc0..0d6eb876aa 100644 --- a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/[componentId]/new/_components/componentStructure.json +++ b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/[componentId]/new/_components/componentStructure.json @@ -8,7 +8,8 @@ "formblock", "multistepformblock", "chartblock", - "rowexplorer" + "rowexplorer", + "pdf" ] }, { diff --git a/packages/client/manifest.json b/packages/client/manifest.json index fc0d5a4614..c3bef81dd4 100644 --- a/packages/client/manifest.json +++ b/packages/client/manifest.json @@ -8058,5 +8058,42 @@ "showInBar": true } ] + }, + "pdf": { + "name": "PDF Generator", + "icon": "Document", + "hasChildren": true, + "showEmptyState": false, + "grid": { + "hAlign": "center", + "vAlign": "start" + }, + "size": { + "width": 800, + "height": 1200 + }, + "description": "A component to render PDFs from other Budibase components", + "settings": [ + { + "type": "text", + "label": "File name", + "key": "fileName" + }, + { + "type": "boolean", + "label": "Landscape", + "key": "landscape" + }, + { + "type": "boolean", + "label": "Footer", + "key": "footer" + }, + { + "type": "number", + "label": "Pages", + "key": "pageCount" + } + ] } } diff --git a/packages/client/package.json b/packages/client/package.json index daaf0ca433..00e7b8538c 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -29,6 +29,7 @@ "apexcharts": "^3.48.0", "dayjs": "^1.10.8", "downloadjs": "1.4.7", + "html2pdf.js": "^0.9.3", "html5-qrcode": "^2.3.8", "leaflet": "^1.7.1", "sanitize-html": "^2.13.0", diff --git a/packages/client/src/components/app/index.js b/packages/client/src/components/app/index.js index 78a3657024..547cb50230 100644 --- a/packages/client/src/components/app/index.js +++ b/packages/client/src/components/app/index.js @@ -36,6 +36,7 @@ export { default as sidepanel } from "./SidePanel.svelte" export { default as modal } from "./Modal.svelte" export { default as gridblock } from "./GridBlock.svelte" export { default as textv2 } from "./Text.svelte" +export { default as pdf } from "./pdf/PDF.svelte" export * from "./charts" export * from "./forms" export * from "./blocks" diff --git a/packages/client/src/components/app/pdf/PDF.svelte b/packages/client/src/components/app/pdf/PDF.svelte new file mode 100644 index 0000000000..f66c4d9df9 --- /dev/null +++ b/packages/client/src/components/app/pdf/PDF.svelte @@ -0,0 +1,143 @@ + + + +
+
+ {safeName} + +
+ {#each pageRefs as ref, pageNumber} +
+
+ + + +
+ {#if footer} + + {/if} +
+ {/each} +
+
+ + diff --git a/packages/client/src/components/app/pdf/pdf.js b/packages/client/src/components/app/pdf/pdf.js new file mode 100644 index 0000000000..06639f082d --- /dev/null +++ b/packages/client/src/components/app/pdf/pdf.js @@ -0,0 +1,156 @@ +import html2pdf from "html2pdf.js" + +// Orientation options for generated PDFs +export const Orientations = { + PORTRAIT: "portrait", + LANDSCAPE: "landscape", +} + +// Function to manipulate the pageSize prop of a html2pdf worker +function manipulatePageSize(pageSize, factor) { + pageSize.width *= factor + pageSize.height *= factor + pageSize.inner.width *= factor + pageSize.inner.height *= factor + pageSize.inner.px.width *= factor + pageSize.inner.px.height *= factor +} + +// Converts an HTML string or an array of HTML pages into a PDF document and +// downloads it +export function htmlToPdf(pages = [], opts = {}) { + const defaultOpts = { + fileName: "file.pdf", + margin: 60, + orientation: Orientations.PORTRAIT, + htmlScale: 1, + progressCallback: () => {}, + footer: true, + } + opts = { + ...defaultOpts, + ...opts, + } + + return new Promise(resolve => { + // Sanity check title + let fileName = opts.fileName + if (!fileName.endsWith(".pdf")) { + fileName += ".pdf" + } + + // Config + const options = { + margin: opts.margin, + filename: fileName, + image: { type: "jpeg", quality: 0.95 }, + html2canvas: { dpi: 192, scale: 2, useCORS: true }, + jsPDF: { + orientation: opts.orientation, + unit: "pt", + format: "a4", + }, + pagebreak: { avoid: ".no-break" }, + + // Custom params + htmlScale: opts.htmlScale, + progressCallback: opts.progressCallback, + } + + // Function to add a PDF page to a html2pdf worker chain + const addPage = (page, worker) => { + return worker + .set(options) + .from(page) + .then(() => { + manipulatePageSize(worker.prop.pageSize, options.htmlScale) + }) + .to("canvas") + .get("canvas") + .then(canvas => { + // Original dimensions of content + const originalWidth = Math.round( + parseInt(canvas.style.width) / options.htmlScale + ) + const originalHeight = Math.round( + parseInt(canvas.style.height) / options.htmlScale + ) + + // Create new canvas, sized exactly for 1 page + const newWidth = originalWidth + const newHeight = Math.floor( + newWidth * worker.prop.pageSize.inner.ratio + ) + const newCanvas = document.createElement("canvas") + newCanvas.width = newWidth * options.html2canvas.scale + newCanvas.height = newHeight * options.html2canvas.scale + newCanvas.style.width = `${newWidth}px` + newCanvas.style.height = `${newHeight}px` + + // Draw original canvas, scaled appropriately + const drawnWidth = Math.min( + originalWidth * options.html2canvas.scale, + newCanvas.width + ) + const drawnHeight = Math.min( + originalHeight * options.html2canvas.scale, + newCanvas.height + ) + + // Draw new canvas image if valid + if (drawnHeight > 0) { + const ctx = newCanvas.getContext("2d") + ctx.drawImage(canvas, 0, 0, drawnWidth, drawnHeight) + } + + // Replace existing canvas with new canvas + worker.prop.canvas = newCanvas + + // Revert pageSize prop manipulation + manipulatePageSize(worker.prop.pageSize, 1 / options.htmlScale) + }) + .to("pdf") + } + + // Generate each page individually + options.progressCallback(1, pages.length) + + // Create html2pdf worker + let worker = addPage(pages[0], html2pdf()) + + // Add other pages + pages.slice(1).forEach((page, pageIdx) => { + worker = worker.get("pdf").then(pdf => { + pdf.addPage() + options.progressCallback(pageIdx + 2, pages.length) + }) + worker = addPage(page, worker) + }) + + // Add footer + if (opts.footer) { + worker = worker.get("pdf").then(pdf => { + const totalPages = pdf.internal.getNumberOfPages() + for (let i = 1; i <= totalPages; i++) { + pdf.setPage(i) + pdf.setFontSize(10) + pdf.setTextColor(150) + pdf.text( + `Page ${i} of ${totalPages}`, + pdf.internal.pageSize.getWidth() - options.margin, + pdf.internal.pageSize.getHeight() - 30, + "right" + ) + pdf.text( + options.filename.replace(".pdf", ""), + options.margin, + pdf.internal.pageSize.getHeight() - 30 + ) + } + }) + } + + // Save PDF + worker.save().then(resolve) + }) +} diff --git a/yarn.lock b/yarn.lock index 7b855ecf15..4a526a566a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7556,6 +7556,11 @@ a-sync-waterfall@^1.0.0: resolved "https://registry.yarnpkg.com/a-sync-waterfall/-/a-sync-waterfall-1.0.1.tgz#75b6b6aa72598b497a125e7a2770f14f4c8a1fa7" integrity sha512-RYTOHHdWipFUliRFMCS4X2Yn2X8M87V/OpSqWzKKOGhzqyUxzyVmhHDH9sAvG+ZuQf/TAOFsLCpMw09I1ufUnA== +abab@^1.0.0: + version "1.0.4" + resolved "https://registry.yarnpkg.com/abab/-/abab-1.0.4.tgz#5faad9c2c07f60dd76770f71cf025b62a63cfd4e" + integrity sha512-I+Wi+qiE2kUXyrRhNsWv6XsjUTBJjSoVSctKNBfLG5zG/Xe7Rjbxf13+vqYHNTwHaFU+FtSlVxOCTiMEVtPv0A== + abab@^2.0.3, abab@^2.0.5, abab@^2.0.6: version "2.0.6" resolved "https://registry.yarnpkg.com/abab/-/abab-2.0.6.tgz#41b80f2c871d19686216b82309231cfd3cb3d291" @@ -7627,6 +7632,13 @@ accepts@^1.3.5, accepts@^1.3.7, accepts@~1.3.4, accepts@~1.3.8: mime-types "~2.1.34" negotiator "0.6.3" +acorn-globals@^1.0.4: + version "1.0.9" + resolved "https://registry.yarnpkg.com/acorn-globals/-/acorn-globals-1.0.9.tgz#55bb5e98691507b74579d0513413217c380c54cf" + integrity sha512-j3/4pkfih8W4NK22gxVSXcEonTpAHOHh0hu5BoZrKcOsW/4oBPxTi4Yk3SAj+FhC1f3+bRTkXdm4019gw1vg9g== + dependencies: + acorn "^2.1.0" + acorn-globals@^6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/acorn-globals/-/acorn-globals-6.0.0.tgz#46cdd39f0f8ff08a876619b55f5ac8a6dc770b45" @@ -7665,6 +7677,11 @@ acorn-walk@^8.0.2, acorn-walk@^8.1.1, acorn-walk@^8.2.0: dependencies: acorn "^8.11.0" +acorn@^2.1.0, acorn@^2.4.0: + version "2.7.0" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-2.7.0.tgz#ab6e7d9d886aaca8b085bc3312b79a198433f0e7" + integrity sha512-pXK8ez/pVjqFdAgBkF1YPVRacuLQ9EXBKaKWaeh58WNfMkCmZhOZzu+NtKSPD5PHmCCHheQ5cD29qM1K4QTxIg== + acorn@^5.2.1: version "5.7.4" resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.7.4.tgz#3e8d8a9947d0599a1796d10225d7432f4a4acf5e" @@ -7980,6 +7997,11 @@ array-differ@^3.0.0: resolved "https://registry.yarnpkg.com/array-differ/-/array-differ-3.0.0.tgz#3cbb3d0f316810eafcc47624734237d6aee4ae6b" integrity sha512-THtfYS6KtME/yIAhKjZ2ul7XI96lQGHRputJQHO80LAWQnuGP4iCIN8vdMRboGbIEYBwU33q8Tch1os2+X0kMg== +array-equal@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/array-equal/-/array-equal-1.0.2.tgz#a8572e64e822358271250b9156d20d96ef5dec04" + integrity sha512-gUHx76KtnhEgB3HOuFYiCm3FIdEs6ocM2asHvNTkfu/Y09qQVrrVVaOKENmS2KkSaGoxgXNqC+ZVtR/n0MOkSA== + array-flatten@1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2" @@ -8400,6 +8422,11 @@ base62@^1.1.0: resolved "https://registry.yarnpkg.com/base62/-/base62-1.2.8.tgz#1264cb0fb848d875792877479dbe8bae6bae3428" integrity sha512-V6YHUbjLxN1ymqNLb1DPHoU1CpfdL7d2YTIp5W3U4hhoG4hhxNmsFDs66M9EXxBiSEke5Bt5dwdfMwwZF70iLA== +base64-arraybuffer@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz#1c37589a7c4b0746e34bd1feb951da2df01c1bdc" + integrity sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ== + base64-js@^1.0.2, base64-js@^1.3.0, base64-js@^1.3.1: version "1.5.1" resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" @@ -8882,6 +8909,16 @@ caniuse-lite@^1.0.30001449: resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001460.tgz#31d2e26f0a2309860ed3eff154e03890d9d851a7" integrity sha512-Bud7abqjvEjipUkpLs4D7gR0l8hBYBHoa+tGtKJHvT2AYzLp1z7EmVkUT4ERpVUfca8S2HGIVs883D8pUH1ZzQ== +canvg@^1.0: + version "1.5.3" + resolved "https://registry.yarnpkg.com/canvg/-/canvg-1.5.3.tgz#aad17915f33368bf8eb80b25d129e3ae922ddc5f" + integrity sha512-7Gn2IuQzvUQWPIuZuFHrzsTM0gkPz2RRT9OcbdmA03jeKk8kltrD8gqUzNX15ghY/4PV5bbe5lmD6yDLDY6Ybg== + dependencies: + jsdom "^8.1.0" + rgbcolor "^1.0.1" + stackblur-canvas "^1.4.1" + xmldom "^0.1.22" + caseless@~0.12.0: version "0.12.0" resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" @@ -8892,6 +8929,11 @@ catering@^2.0.0, catering@^2.1.0: resolved "https://registry.yarnpkg.com/catering/-/catering-2.1.1.tgz#66acba06ed5ee28d5286133982a927de9a04b510" integrity sha512-K7Qy8O9p76sL3/3m7/zLKbRkyOlSZAgzEaLhyj2mXS8PsCud2Eo4hAb8aLtZqHh0QGqLcb9dlJSu6lHRVENm1w== +cf-blob.js@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/cf-blob.js/-/cf-blob.js-0.0.1.tgz#f5ab7e12e798caf08ccf828c69aba0f063d83f99" + integrity sha512-KkUmNT/rgVK+KehG7cSvbLwMb+OS5Qby6ADB4LP12jtx6rfVvHCdyqFUjAeQnDpGpQNNwvpi0R/tluT2J6P99Q== + chai@^4.3.7: version "4.5.0" resolved "https://registry.yarnpkg.com/chai/-/chai-4.5.0.tgz#707e49923afdd9b13a8b0b47d33d732d13812fd8" @@ -9730,6 +9772,13 @@ crypto-randomuuid@^1.0.0: resolved "https://registry.yarnpkg.com/crypto-randomuuid/-/crypto-randomuuid-1.0.0.tgz#acf583e5e085e867ae23e107ff70279024f9e9e7" integrity sha512-/RC5F4l1SCqD/jazwUF6+t34Cd8zTSAGZ7rvvZu1whZUhD2a5MOGKjSGowoGcpj/cbVZk1ZODIooJEQQq3nNAA== +css-line-break@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/css-line-break/-/css-line-break-2.1.0.tgz#bfef660dfa6f5397ea54116bb3cb4873edbc4fa0" + integrity sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w== + dependencies: + utrie "^1.0.2" + css-tree@^2.3.1: version "2.3.1" resolved "https://registry.yarnpkg.com/css-tree/-/css-tree-2.3.1.tgz#10264ce1e5442e8572fc82fbe490644ff54b5c20" @@ -9748,15 +9797,22 @@ cssesc@^3.0.0: resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-3.0.0.tgz#37741919903b868565e1c09ea747445cd18983ee" integrity sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg== +cssom@0.3.x, "cssom@>= 0.3.0 < 0.4.0", cssom@~0.3.6: + version "0.3.8" + resolved "https://registry.yarnpkg.com/cssom/-/cssom-0.3.8.tgz#9f1276f5b2b463f2114d3f2c75250af8c1a36f4a" + integrity sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg== + cssom@^0.4.4: version "0.4.4" resolved "https://registry.yarnpkg.com/cssom/-/cssom-0.4.4.tgz#5a66cf93d2d0b661d80bf6a44fb65f5c2e4e0a10" integrity sha512-p3pvU7r1MyyqbTk+WbNJIgJjG2VmTIaB10rI93LzVPrmDJKkzKYMtxxyAvQXR/NS6otuzveI7+7BBq3SjBS2mw== -cssom@~0.3.6: - version "0.3.8" - resolved "https://registry.yarnpkg.com/cssom/-/cssom-0.3.8.tgz#9f1276f5b2b463f2114d3f2c75250af8c1a36f4a" - integrity sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg== +"cssstyle@>= 0.2.34 < 0.3.0": + version "0.2.37" + resolved "https://registry.yarnpkg.com/cssstyle/-/cssstyle-0.2.37.tgz#541097234cb2513c83ceed3acddc27ff27987d54" + integrity sha512-FUpKc+1FNBsHUr9IsfSGCovr8VuGOiiuzlgCyppKBjJi2jYTOFLN3oiiNRMIvYqbFzF38mqKj4BgcevzU5/kIA== + dependencies: + cssom "0.3.x" cssstyle@^2.3.0: version "2.3.0" @@ -11068,6 +11124,11 @@ es6-error@^4.0.1, es6-error@^4.1.1: resolved "https://registry.yarnpkg.com/es6-error/-/es6-error-4.1.1.tgz#9e3af407459deed47e9a91f9b885a84eb05c561d" integrity sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg== +es6-promise@^4.2.5: + version "4.2.8" + resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-4.2.8.tgz#4eb21594c972bc40553d276e510539143db53e0a" + integrity sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w== + esbuild-node-externals@^1.14.0: version "1.14.0" resolved "https://registry.yarnpkg.com/esbuild-node-externals/-/esbuild-node-externals-1.14.0.tgz#fc2950c67a068dc2b538fd1381ad7d8e20a6f54d" @@ -11134,6 +11195,18 @@ escape-string-regexp@^4.0.0: resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== +escodegen@^1.6.1: + version "1.14.3" + resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-1.14.3.tgz#4e7b81fba61581dc97582ed78cab7f0e8d63f503" + integrity sha512-qFcX0XJkdg+PB3xjZZG/wKSuT1PnQWx57+TVSjIMmILd2yC/6ByYElPwJnslDsuWuSAp4AwJGumarAAmJch5Kw== + dependencies: + esprima "^4.0.1" + estraverse "^4.2.0" + esutils "^2.0.2" + optionator "^0.8.1" + optionalDependencies: + source-map "~0.6.1" + escodegen@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-2.0.0.tgz#5e32b12833e8aa8fa35e1bf0befa89380484c7dd" @@ -11411,7 +11484,7 @@ esrecurse@^4.3.0: dependencies: estraverse "^5.2.0" -estraverse@^4.1.1: +estraverse@^4.1.1, estraverse@^4.2.0: version "4.3.0" resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.3.0.tgz#398ad3f3c5a24948be7725e83d11a7de28cdbd1d" integrity sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw== @@ -11806,6 +11879,11 @@ file-entry-cache@^8.0.0: dependencies: flat-cache "^4.0.0" +file-saver@1.3.8: + version "1.3.8" + resolved "https://registry.yarnpkg.com/file-saver/-/file-saver-1.3.8.tgz#e68a30c7cb044e2fb362b428469feb291c2e09d8" + integrity sha512-spKHSBQIxxS81N/O21WmuXA2F6wppUCsutpzenOeZzOCCJ5gEfcbqJP983IrpLXzYmXnMUa6J03SubcNPdKrlg== + file-type@^11.1.0: version "11.1.0" resolved "https://registry.yarnpkg.com/file-type/-/file-type-11.1.0.tgz#93780f3fed98b599755d846b99a1617a2ad063b8" @@ -13013,6 +13091,23 @@ html-tag@^2.0.0: is-self-closing "^1.0.1" kind-of "^6.0.0" +html2canvas@^1.0.0-alpha.12: + version "1.4.1" + resolved "https://registry.yarnpkg.com/html2canvas/-/html2canvas-1.4.1.tgz#7cef1888311b5011d507794a066041b14669a543" + integrity sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA== + dependencies: + css-line-break "^2.1.0" + text-segmentation "^1.0.3" + +html2pdf.js@^0.9.3: + version "0.9.3" + resolved "https://registry.yarnpkg.com/html2pdf.js/-/html2pdf.js-0.9.3.tgz#e7fc6143f748ce253670eaae403987342b66b15c" + integrity sha512-M254g3Z+ZsjtQFDxJlU6E8Zgb8xOpCBQQM1lFPn4Lq+myAdWoYtMFnwlVo/eOI9R1cG75+YmMSDQofkugwOV/Q== + dependencies: + es6-promise "^4.2.5" + html2canvas "^1.0.0-alpha.12" + jspdf "1.4.1" + html5-qrcode@^2.3.8: version "2.3.8" resolved "https://registry.yarnpkg.com/html5-qrcode/-/html5-qrcode-2.3.8.tgz#0b0cdf7a9926cfd4be530e13a51db47592adfa0d" @@ -13163,7 +13258,7 @@ ical-generator@4.1.0: dependencies: uuid-random "^1.3.2" -iconv-lite@0.4.24, iconv-lite@^0.4.24, iconv-lite@^0.4.5: +iconv-lite@0.4.24, iconv-lite@^0.4.13, iconv-lite@^0.4.24, iconv-lite@^0.4.5: version "0.4.24" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== @@ -14657,6 +14752,29 @@ jsdom@^24.1.1: ws "^8.18.0" xml-name-validator "^5.0.0" +jsdom@^8.1.0: + version "8.5.0" + resolved "https://registry.yarnpkg.com/jsdom/-/jsdom-8.5.0.tgz#d4d8f5dbf2768635b62a62823b947cf7071ebc98" + integrity sha512-rvWfcn2O8SrXPaX5fTYIfPVwvnbU8DnZkjAXK305wfP67csyaJBhgg0F2aU6imqJ+lZmj9EmrBAXy6rWHf2/9Q== + dependencies: + abab "^1.0.0" + acorn "^2.4.0" + acorn-globals "^1.0.4" + array-equal "^1.0.0" + cssom ">= 0.3.0 < 0.4.0" + cssstyle ">= 0.2.34 < 0.3.0" + escodegen "^1.6.1" + iconv-lite "^0.4.13" + nwmatcher ">= 1.3.7 < 2.0.0" + parse5 "^1.5.1" + request "^2.55.0" + sax "^1.1.4" + symbol-tree ">= 3.1.0 < 4.0.0" + tough-cookie "^2.2.0" + webidl-conversions "^3.0.1" + whatwg-url "^2.0.1" + xml-name-validator ">= 2.0.1 < 3.0.0" + jsesc@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-3.0.2.tgz#bb8b09a6597ba426425f2e4a07245c3d00b9343e" @@ -14776,6 +14894,17 @@ jsonwebtoken@9.0.2, jsonwebtoken@^9.0.0: ms "^2.1.1" semver "^7.5.4" +jspdf@1.4.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/jspdf/-/jspdf-1.4.1.tgz#8dbd437986346d65efe20ede5361927666b8e4ca" + integrity sha512-2vYVdrvrQUdKKPyWHw81t1jEYYAJ6uFJ/HtTcGbI4qXIQEdl18dLEuL2wTeSv2GzeQLSgUvEvwsXsszuHK+PTw== + dependencies: + canvg "^1.0" + cf-blob.js "0.0.1" + file-saver "1.3.8" + omggif "1.0.7" + stackblur "^1.0.0" + jsprim@^1.2.2: version "1.4.2" resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.2.tgz#712c65533a15c878ba59e9ed5f0e26d5b77c5feb" @@ -16832,6 +16961,11 @@ nunjucks@^3.2.3: asap "^2.0.3" commander "^5.1.0" +"nwmatcher@>= 1.3.7 < 2.0.0": + version "1.4.4" + resolved "https://registry.yarnpkg.com/nwmatcher/-/nwmatcher-1.4.4.tgz#2285631f34a95f0d0395cd900c96ed39b58f346e" + integrity sha512-3iuY4N5dhgMpCUrOVnuAdGrgxVqV2cJpM+XNccjR2DKOB1RUP0aA+wGXEiNziG/UKboFyGBIoKOaNlJxx8bciQ== + nwsapi@^2.2.0, nwsapi@^2.2.4: version "2.2.12" resolved "https://registry.yarnpkg.com/nwsapi/-/nwsapi-2.2.12.tgz#fb6af5c0ec35b27b4581eb3bbad34ec9e5c696f8" @@ -17013,6 +17147,11 @@ obliterator@^1.6.1: resolved "https://registry.yarnpkg.com/obliterator/-/obliterator-1.6.1.tgz#dea03e8ab821f6c4d96a299e17aef6a3af994ef3" integrity sha512-9WXswnqINnnhOG/5SLimUlzuU1hFJUc8zkwyD59Sd+dPOMf05PmnYG/d6Q7HZ+KmgkZJa1PxRso6QdM3sTNHig== +omggif@1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/omggif/-/omggif-1.0.7.tgz#59d2eecb0263de84635b3feb887c0c9973f1e49d" + integrity sha512-KVVUF85EHKUB9kxxT2D8CksGgfayZKxWtH/+i34zbyDdxFHvsqQs+O756usW7uri2YBD8jE/8GgAsA6wVA1tjg== + omggif@^1.0.10: version "1.0.10" resolved "https://registry.yarnpkg.com/omggif/-/omggif-1.0.10.tgz#ddaaf90d4a42f532e9e7cb3a95ecdd47f17c7b19" @@ -17483,6 +17622,11 @@ parse5@6.0.1: resolved "https://registry.yarnpkg.com/parse5/-/parse5-6.0.1.tgz#e1a1c085c569b3dc08321184f19a39cc27f7c30b" integrity sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw== +parse5@^1.5.1: + version "1.5.1" + resolved "https://registry.yarnpkg.com/parse5/-/parse5-1.5.1.tgz#9b7f3b0de32be78dc2401b17573ccaf0f6f59d94" + integrity sha512-w2jx/0tJzvgKwZa58sj2vAYq/S/K1QJfIB3cWYea/Iu1scFPDQQ3IQiVZTHWtRBwAjv2Yd7S/xeZf3XqLDb3bA== + parse5@^7.1.2: version "7.1.2" resolved "https://registry.yarnpkg.com/parse5/-/parse5-7.1.2.tgz#0736bebbfd77793823240a23b7fc5e010b7f8e32" @@ -19046,7 +19190,7 @@ remixicon@2.5.0: resolved "https://registry.yarnpkg.com/remixicon/-/remixicon-2.5.0.tgz#b5e245894a1550aa23793f95daceadbf96ad1a41" integrity sha512-q54ra2QutYDZpuSnFjmeagmEiN9IMo56/zz5dDNitzKD23oFRw77cWo4TsrAdmdkPiEn8mxlrTqxnkujDbEGww== -request@^2.88.0: +request@^2.55.0, request@^2.88.0: version "2.88.2" resolved "https://registry.yarnpkg.com/request/-/request-2.88.2.tgz#d73c918731cb5a87da047e207234146f664d12b3" integrity sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw== @@ -19211,6 +19355,11 @@ rfdc@^1.3.0, rfdc@^1.3.1: resolved "https://registry.yarnpkg.com/rfdc/-/rfdc-1.4.1.tgz#778f76c4fb731d93414e8f925fbecf64cce7f6ca" integrity sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA== +rgbcolor@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/rgbcolor/-/rgbcolor-1.0.1.tgz#d6505ecdb304a6595da26fa4b43307306775945d" + integrity sha512-9aZLIrhRaD97sgVhtJOW6ckOEh6/GnvQtdVNfdZ6s67+3/XwLS9lBcQYzEEhYVeUowN7pRzMLsyGhK2i/xvWbw== + rimraf@3.0.2, rimraf@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a" @@ -19436,6 +19585,11 @@ sax@>=0.6.0: resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9" integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw== +sax@^1.1.4: + version "1.4.1" + resolved "https://registry.yarnpkg.com/sax/-/sax-1.4.1.tgz#44cc8988377f126304d3b3fc1010c733b929ef0f" + integrity sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg== + saxes@^5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/saxes/-/saxes-5.0.1.tgz#eebab953fa3b7608dbe94e5dadb15c888fa6696d" @@ -20093,6 +20247,16 @@ stackback@0.0.2: resolved "https://registry.yarnpkg.com/stackback/-/stackback-0.0.2.tgz#1ac8a0d9483848d1695e418b6d031a3c3ce68e3b" integrity sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw== +stackblur-canvas@^1.4.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/stackblur-canvas/-/stackblur-canvas-1.4.1.tgz#849aa6f94b272ff26f6471fa4130ed1f7e47955b" + integrity sha512-TfbTympL5C1K+F/RizDkMBqH18EkUKU8V+4PphIXR+fWhZwwRi3bekP04gy2TOwOT3R6rJQJXAXFrbcZde7wow== + +stackblur@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/stackblur/-/stackblur-1.0.0.tgz#b407a7e05c93b08d66883bb808d7cba3a503f12f" + integrity sha512-K92JX8alrs0pTox5U2arVBqB8tJmak9dh9i4Xausy94TnnGMdLfTn7P2Dp/NOzlmxvEs7lDzeryo8YqOy0BHRQ== + standard-as-callback@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/standard-as-callback/-/standard-as-callback-2.1.0.tgz#8953fc05359868a77b5b9739a665c5977bb7df45" @@ -20652,7 +20816,7 @@ swagger-parser@10.0.2: dependencies: "@apidevtools/swagger-parser" "10.0.2" -symbol-tree@^3.2.4: +"symbol-tree@>= 3.1.0 < 4.0.0", symbol-tree@^3.2.4: version "3.2.4" resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.4.tgz#430637d248ba77e078883951fb9aa0eed7c63fa2" integrity sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw== @@ -20837,6 +21001,13 @@ text-hex@1.0.x: resolved "https://registry.yarnpkg.com/text-hex/-/text-hex-1.0.0.tgz#69dc9c1b17446ee79a92bf5b884bb4b9127506f5" integrity sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg== +text-segmentation@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/text-segmentation/-/text-segmentation-1.0.3.tgz#52a388159efffe746b24a63ba311b6ac9f2d7943" + integrity sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw== + dependencies: + utrie "^1.0.2" + text-table@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" @@ -21030,7 +21201,7 @@ touch@^3.1.0: dependencies: nopt "~1.0.10" -tough-cookie@4.1.3, "tough-cookie@^2.3.3 || ^3.0.1 || ^4.0.0", tough-cookie@^4.0.0, tough-cookie@^4.1.2, tough-cookie@^4.1.4, tough-cookie@~2.5.0: +tough-cookie@4.1.3, tough-cookie@^2.2.0, "tough-cookie@^2.3.3 || ^3.0.1 || ^4.0.0", tough-cookie@^4.0.0, tough-cookie@^4.1.2, tough-cookie@^4.1.4, tough-cookie@~2.5.0: version "4.1.3" resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-4.1.3.tgz#97b9adb0728b42280aa3d814b6b999b2ff0318bf" integrity sha512-aX/y5pVRkfRnfmuX+OdbSdXvPe6ieKX/G2s7e98f4poJHnqH3281gDPm/metm6E/WRamfx7WC4HUqkWHfQHprw== @@ -21635,6 +21806,13 @@ utils-merge@1.0.1, utils-merge@1.x.x, utils-merge@^1.0.1: resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" integrity sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA== +utrie@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/utrie/-/utrie-1.0.2.tgz#d42fe44de9bc0119c25de7f564a6ed1b2c87a645" + integrity sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw== + dependencies: + base64-arraybuffer "^1.0.2" + uue@3.1.2: version "3.1.2" resolved "https://registry.yarnpkg.com/uue/-/uue-3.1.2.tgz#e99368414e87200012eb37de4dbaebaa1c742ad2" @@ -21886,7 +22064,7 @@ webfinger@^0.4.2: step "0.0.x" xml2js "0.1.x" -webidl-conversions@^3.0.0: +webidl-conversions@^3.0.0, webidl-conversions@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871" integrity sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ== @@ -21966,6 +22144,14 @@ whatwg-url@^14.0.0: tr46 "^5.0.0" webidl-conversions "^7.0.0" +whatwg-url@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-2.0.1.tgz#5396b2043f020ee6f704d9c45ea8519e724de659" + integrity sha512-sX+FT4N6iR0ZiqGqyDEKklyfMGR99zvxZD+LQ8IGae5uVGswQ7DOeLPB5KgJY8FzkwSzwqOXLQeVQvtOTSQU9Q== + dependencies: + tr46 "~0.0.3" + webidl-conversions "^3.0.0" + whatwg-url@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-5.0.0.tgz#966454e8765462e37644d3626f6742ce8b70965d" @@ -22255,6 +22441,11 @@ xhr@^2.4.1: parse-headers "^2.0.0" xtend "^4.0.0" +"xml-name-validator@>= 2.0.1 < 3.0.0": + version "2.0.1" + resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-2.0.1.tgz#4d8b8f1eccd3419aa362061becef515e1e559635" + integrity sha512-jRKe/iQYMyVJpzPH+3HL97Lgu5HrCfii+qSo+TfjKHtOnvbnvdVfMYrn9Q34YV81M2e5sviJlI6Ko9y+nByzvA== + xml-name-validator@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-3.0.0.tgz#6ae73e06de4d8c6e47f9fb181f78d648ad457c6a" @@ -22293,6 +22484,11 @@ xmlchars@^2.2.0: resolved "https://registry.yarnpkg.com/xmlchars/-/xmlchars-2.2.0.tgz#060fe1bcb7f9c76fe2a17db86a9bc3ab894210cb" integrity sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw== +xmldom@^0.1.22: + version "0.1.31" + resolved "https://registry.yarnpkg.com/xmldom/-/xmldom-0.1.31.tgz#b76c9a1bd9f0a9737e5a72dc37231cf38375e2ff" + integrity sha512-yS2uJflVQs6n+CyjHoaBmVSqIDevTAWrzMmjG1Gc7h1qQ7uVozNhEPJAwZXWyGQ/Gafo3fCwrcaokezLPupVyQ== + xmlhttprequest-ssl@~2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.0.0.tgz#91360c86b914e67f44dce769180027c0da618c67" From 508e051954c6ee77c4c24cc8bb8ee9607aef0ce1 Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Wed, 19 Mar 2025 16:27:32 +0000 Subject: [PATCH 03/69] Fix grid layout for PDFs --- packages/client/src/components/Component.svelte | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/packages/client/src/components/Component.svelte b/packages/client/src/components/Component.svelte index 77414ed37f..99fd9e8737 100644 --- a/packages/client/src/components/Component.svelte +++ b/packages/client/src/components/Component.svelte @@ -196,8 +196,6 @@ } // Metadata to pass into grid action to apply CSS - const checkGrid = x => - x?._component?.endsWith("/container") && x?.layout === "grid" $: insideGrid = checkGrid(parent) $: isGrid = checkGrid(instance) $: gridMetadata = { @@ -601,6 +599,18 @@ } } + const checkGrid = x => { + // Check for a grid container + if (x?._component?.endsWith("/container") && x?.layout === "grid") { + return true + } + // Check for a PDF (always grid) + if (x?._component?.endsWith("/pdf")) { + return true + } + return false + } + onMount(() => { // Register this component instance for external access if ($appStore.isDevApp) { From 814fb723e583b3efb8b75254738a76a58da07043 Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Thu, 20 Mar 2025 09:29:26 +0000 Subject: [PATCH 04/69] Add functional screen template for PDFs --- .../_components/Screen/GeneralPanel.svelte | 68 +++++++---- .../NewScreen/CreateScreenModal.svelte | 2 +- .../src/templates/screenTemplating/Screen.js | 22 ++++ .../src/templates/screenTemplating/index.js | 1 + .../src/templates/screenTemplating/pdf.js | 24 ++++ packages/client/manifest.json | 22 ++-- .../src/components/BlockComponent.svelte | 8 +- .../client/src/components/app/Layout.svelte | 2 + .../client/src/components/app/pdf/PDF.svelte | 107 ++++++++++-------- packages/client/src/index.ts | 4 + 10 files changed, 170 insertions(+), 90 deletions(-) create mode 100644 packages/builder/src/templates/screenTemplating/pdf.js diff --git a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/[componentId]/_components/Screen/GeneralPanel.svelte b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/[componentId]/_components/Screen/GeneralPanel.svelte index 31479bc820..0bf45b16b8 100644 --- a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/[componentId]/_components/Screen/GeneralPanel.svelte +++ b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/[componentId]/_components/Screen/GeneralPanel.svelte @@ -10,12 +10,13 @@ } from "@budibase/bbui" import PropertyControl from "@/components/design/settings/controls/PropertyControl.svelte" import RoleSelect from "@/components/design/settings/controls/RoleSelect.svelte" - import { selectedScreen, screenStore } from "@/stores/builder" + import { selectedScreen, screenStore, componentStore } from "@/stores/builder" import sanitizeUrl from "@/helpers/sanitizeUrl" import ButtonActionEditor from "@/components/design/settings/controls/ButtonActionEditor/ButtonActionEditor.svelte" import { getBindableProperties } from "@/dataBinding" import BarButtonList from "@/components/design/settings/controls/BarButtonList.svelte" import URLVariableTestInput from "@/components/design/settings/controls/URLVariableTestInput.svelte" + import { DrawerBindableInput } from "@/components/common/bindings" $: bindings = getBindableProperties($selectedScreen, null) $: screenSettings = getScreenSettings($selectedScreen) @@ -23,7 +24,49 @@ let errors = {} const getScreenSettings = screen => { - let settings = [ + // Determine correct screen settings for the top level component + let screenComponentSettings = [] + switch ($selectedScreen.props._component) { + case "@budibase/standard-components/pdf": + screenComponentSettings = [ + { + key: "props.fileName", + label: "PDF title", + defaultValue: "Report", + control: DrawerBindableInput, + }, + { + key: "props.buttonText", + label: "Button text", + defaultValue: "Download PDF", + control: DrawerBindableInput, + }, + ] + break + default: + screenComponentSettings = [ + { + key: "props.layout", + label: "Layout", + defaultValue: "flex", + control: BarButtonList, + props: { + options: [ + { + barIcon: "ModernGridView", + value: "flex", + }, + { + barIcon: "ViewGrid", + value: "grid", + }, + ], + }, + }, + ] + } + + return [ { key: "routing.homeScreen", control: Checkbox, @@ -76,24 +119,7 @@ disabled: !!screen.layoutId, }, }, - { - key: "props.layout", - label: "Layout", - defaultValue: "flex", - control: BarButtonList, - props: { - options: [ - { - barIcon: "ModernGridView", - value: "flex", - }, - { - barIcon: "ViewGrid", - value: "grid", - }, - ], - }, - }, + ...screenComponentSettings, { key: "urlTest", control: URLVariableTestInput, @@ -102,8 +128,6 @@ }, }, ] - - return settings } const routeTaken = url => { diff --git a/packages/builder/src/pages/builder/app/[application]/design/_components/NewScreen/CreateScreenModal.svelte b/packages/builder/src/pages/builder/app/[application]/design/_components/NewScreen/CreateScreenModal.svelte index 2dedec96b4..c717900aea 100644 --- a/packages/builder/src/pages/builder/app/[application]/design/_components/NewScreen/CreateScreenModal.svelte +++ b/packages/builder/src/pages/builder/app/[application]/design/_components/NewScreen/CreateScreenModal.svelte @@ -102,7 +102,7 @@ } const createBlankScreen = async ({ route }) => { - const screenTemplates = screenTemplating.blank({ route, screens }) + const screenTemplates = screenTemplating.pdf({ route, screens }) const newScreens = await createScreens(screenTemplates) loadNewScreen(newScreens[0]) } diff --git a/packages/builder/src/templates/screenTemplating/Screen.js b/packages/builder/src/templates/screenTemplating/Screen.js index da3c83820e..397dfe6db3 100644 --- a/packages/builder/src/templates/screenTemplating/Screen.js +++ b/packages/builder/src/templates/screenTemplating/Screen.js @@ -81,3 +81,25 @@ export class Screen extends BaseStructure { return this } } + +export class PDFScreen extends Screen { + constructor() { + super() + this._json.variant = "pdf" + this._json.width = "Max" + this._json.showNavigation = false + this._json.props = { + _id: Helpers.uuid(), + _component: "@budibase/standard-components/pdf", + _styles: { + normal: {}, + hover: {}, + active: {}, + selected: {}, + }, + _children: [], + _instanceName: "", + title: "PDF Editor", + } + } +} diff --git a/packages/builder/src/templates/screenTemplating/index.js b/packages/builder/src/templates/screenTemplating/index.js index ff15687776..855f255a3a 100644 --- a/packages/builder/src/templates/screenTemplating/index.js +++ b/packages/builder/src/templates/screenTemplating/index.js @@ -1,3 +1,4 @@ export { default as blank } from "./blank" export { default as form } from "./form" export { default as table } from "./table" +export { default as pdf } from "./pdf" diff --git a/packages/builder/src/templates/screenTemplating/pdf.js b/packages/builder/src/templates/screenTemplating/pdf.js new file mode 100644 index 0000000000..b5cde8f536 --- /dev/null +++ b/packages/builder/src/templates/screenTemplating/pdf.js @@ -0,0 +1,24 @@ +import { PDFScreen } from "./Screen" +import { capitalise } from "@/helpers" +import getValidRoute from "./getValidRoute" +import { Roles } from "@/constants/backend" + +const pdf = ({ route, screens }) => { + const validRoute = getValidRoute(screens, route, Roles.BASIC) + + const template = new PDFScreen() + .instanceName("PDF Editor") + .role(Roles.BASIC) + .route(validRoute) + .json() + + return [ + { + data: template, + navigationLinkLabel: + validRoute === "/" ? null : capitalise(validRoute.split("/")[1]), + }, + ] +} + +export default pdf diff --git a/packages/client/manifest.json b/packages/client/manifest.json index c3bef81dd4..8b1ed79973 100644 --- a/packages/client/manifest.json +++ b/packages/client/manifest.json @@ -8076,23 +8076,15 @@ "settings": [ { "type": "text", - "label": "File name", - "key": "fileName" + "label": "PDF title", + "key": "fileName", + "defaultValue": "Report" }, { - "type": "boolean", - "label": "Landscape", - "key": "landscape" - }, - { - "type": "boolean", - "label": "Footer", - "key": "footer" - }, - { - "type": "number", - "label": "Pages", - "key": "pageCount" + "type": "text", + "label": "Button text", + "key": "buttonText", + "defaultValue": "Download PDF" } ] } diff --git a/packages/client/src/components/BlockComponent.svelte b/packages/client/src/components/BlockComponent.svelte index cacb4f64c1..39b4075e46 100644 --- a/packages/client/src/components/BlockComponent.svelte +++ b/packages/client/src/components/BlockComponent.svelte @@ -1,20 +1,20 @@ -
-
- {safeName} - -
- {#each pageRefs as ref, pageNumber} -
-
- - - -
- {#if footer} - - {/if} +
+
+
+ {safeName} +
- {/each} + {#each pageRefs as ref, pageNumber} +
+
+ + + +
+ {#if footer} + + {/if} +
+ {/each} +
diff --git a/packages/client/src/components/app/pdf/pdf.js b/packages/client/src/components/app/pdf/pdf.js index 06639f082d..c236e27c9a 100644 --- a/packages/client/src/components/app/pdf/pdf.js +++ b/packages/client/src/components/app/pdf/pdf.js @@ -1,28 +1,14 @@ import html2pdf from "html2pdf.js" -// Orientation options for generated PDFs -export const Orientations = { - PORTRAIT: "portrait", - LANDSCAPE: "landscape", -} +export const pxToPt = px => (px / 4) * 3 +export const ptToPx = pt => (pt / 3) * 4 +export const A4HeightPx = ptToPx(841.92) + 1 -// Function to manipulate the pageSize prop of a html2pdf worker -function manipulatePageSize(pageSize, factor) { - pageSize.width *= factor - pageSize.height *= factor - pageSize.inner.width *= factor - pageSize.inner.height *= factor - pageSize.inner.px.width *= factor - pageSize.inner.px.height *= factor -} - -// Converts an HTML string or an array of HTML pages into a PDF document and -// downloads it -export function htmlToPdf(pages = [], opts = {}) { +export async function htmlToPdf(el, opts = {}) { const defaultOpts = { fileName: "file.pdf", margin: 60, - orientation: Orientations.PORTRAIT, + orientation: "portrait", htmlScale: 1, progressCallback: () => {}, footer: true, @@ -57,100 +43,6 @@ export function htmlToPdf(pages = [], opts = {}) { progressCallback: opts.progressCallback, } - // Function to add a PDF page to a html2pdf worker chain - const addPage = (page, worker) => { - return worker - .set(options) - .from(page) - .then(() => { - manipulatePageSize(worker.prop.pageSize, options.htmlScale) - }) - .to("canvas") - .get("canvas") - .then(canvas => { - // Original dimensions of content - const originalWidth = Math.round( - parseInt(canvas.style.width) / options.htmlScale - ) - const originalHeight = Math.round( - parseInt(canvas.style.height) / options.htmlScale - ) - - // Create new canvas, sized exactly for 1 page - const newWidth = originalWidth - const newHeight = Math.floor( - newWidth * worker.prop.pageSize.inner.ratio - ) - const newCanvas = document.createElement("canvas") - newCanvas.width = newWidth * options.html2canvas.scale - newCanvas.height = newHeight * options.html2canvas.scale - newCanvas.style.width = `${newWidth}px` - newCanvas.style.height = `${newHeight}px` - - // Draw original canvas, scaled appropriately - const drawnWidth = Math.min( - originalWidth * options.html2canvas.scale, - newCanvas.width - ) - const drawnHeight = Math.min( - originalHeight * options.html2canvas.scale, - newCanvas.height - ) - - // Draw new canvas image if valid - if (drawnHeight > 0) { - const ctx = newCanvas.getContext("2d") - ctx.drawImage(canvas, 0, 0, drawnWidth, drawnHeight) - } - - // Replace existing canvas with new canvas - worker.prop.canvas = newCanvas - - // Revert pageSize prop manipulation - manipulatePageSize(worker.prop.pageSize, 1 / options.htmlScale) - }) - .to("pdf") - } - - // Generate each page individually - options.progressCallback(1, pages.length) - - // Create html2pdf worker - let worker = addPage(pages[0], html2pdf()) - - // Add other pages - pages.slice(1).forEach((page, pageIdx) => { - worker = worker.get("pdf").then(pdf => { - pdf.addPage() - options.progressCallback(pageIdx + 2, pages.length) - }) - worker = addPage(page, worker) - }) - - // Add footer - if (opts.footer) { - worker = worker.get("pdf").then(pdf => { - const totalPages = pdf.internal.getNumberOfPages() - for (let i = 1; i <= totalPages; i++) { - pdf.setPage(i) - pdf.setFontSize(10) - pdf.setTextColor(150) - pdf.text( - `Page ${i} of ${totalPages}`, - pdf.internal.pageSize.getWidth() - options.margin, - pdf.internal.pageSize.getHeight() - 30, - "right" - ) - pdf.text( - options.filename.replace(".pdf", ""), - options.margin, - pdf.internal.pageSize.getHeight() - 30 - ) - } - }) - } - - // Save PDF - worker.save().then(resolve) + html2pdf().set(options).from(el).save().then(resolve) }) } From db4af20ded208205d06e827e79777384afb0f8d6 Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Fri, 21 Mar 2025 09:25:53 +0000 Subject: [PATCH 12/69] Clean up and add footer --- .../client/src/components/app/pdf/PDF.svelte | 35 ++++++++++++------- packages/client/src/components/app/pdf/pdf.js | 28 +++++++++++++-- 2 files changed, 48 insertions(+), 15 deletions(-) diff --git a/packages/client/src/components/app/pdf/PDF.svelte b/packages/client/src/components/app/pdf/PDF.svelte index c3686aa568..9a777edee3 100644 --- a/packages/client/src/components/app/pdf/PDF.svelte +++ b/packages/client/src/components/app/pdf/PDF.svelte @@ -1,5 +1,5 @@ -
+
{safeName} @@ -68,10 +63,11 @@
{#if pageCount > 1} - {#each new Array(pageCount - 1) as _, idx} + {#each new Array(pageCount) as _, idx}
{/each} @@ -142,10 +138,23 @@ } .divider { width: 100%; - height: 1px; + height: 2px; background: var(--spectrum-global-color-static-gray-400); position: absolute; left: 0; top: var(--top); + transform: translateY(-50%); + } + .divider.last { + top: calc(var(--top) + var(--margin)); + background: transparent; + } + .divider::after { + position: absolute; + top: -32px; + right: 24px; + content: var(--idx); + color: var(--spectrum-global-color-static-gray-400); + text-align: right; } diff --git a/packages/client/src/components/app/pdf/pdf.js b/packages/client/src/components/app/pdf/pdf.js index c236e27c9a..f3579e5f82 100644 --- a/packages/client/src/components/app/pdf/pdf.js +++ b/packages/client/src/components/app/pdf/pdf.js @@ -40,9 +40,33 @@ export async function htmlToPdf(el, opts = {}) { // Custom params htmlScale: opts.htmlScale, - progressCallback: opts.progressCallback, } - html2pdf().set(options).from(el).save().then(resolve) + let worker = html2pdf().set(options).from(el).toPdf() + + // Add footer if required + if (opts.footer) { + worker = worker.get("pdf").then(pdf => { + const totalPages = pdf.internal.getNumberOfPages() + for (let i = 1; i <= totalPages; i++) { + pdf.setPage(i) + pdf.setFontSize(10) + pdf.setTextColor(200) + pdf.text( + `Page ${i} of ${totalPages}`, + pdf.internal.pageSize.getWidth() - options.margin, + pdf.internal.pageSize.getHeight() - options.margin / 2, + "right" + ) + pdf.text( + options.filename.replace(".pdf", ""), + options.margin, + pdf.internal.pageSize.getHeight() - options.margin / 2 + ) + } + }) + } + + worker.save().then(resolve) }) } From 646b91e285a35e60b80513b130c21e1eabeb8f56 Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Fri, 21 Mar 2025 09:37:25 +0000 Subject: [PATCH 13/69] Convert to TS and tidy up code --- .../client/src/components/app/pdf/PDF.svelte | 54 +++++++++---------- .../src/components/app/pdf/{pdf.js => pdf.ts} | 34 +++++++----- 2 files changed, 46 insertions(+), 42 deletions(-) rename packages/client/src/components/app/pdf/{pdf.js => pdf.ts} (69%) diff --git a/packages/client/src/components/app/pdf/PDF.svelte b/packages/client/src/components/app/pdf/PDF.svelte index 9a777edee3..88e5df3711 100644 --- a/packages/client/src/components/app/pdf/PDF.svelte +++ b/packages/client/src/components/app/pdf/PDF.svelte @@ -1,7 +1,7 @@ -
+
{safeName} @@ -61,28 +65,22 @@ {safeButtonText}
-
+
{#if pageCount > 1} {#each new Array(pageCount) as _, idx}
{/each} {/if}
- +
diff --git a/packages/client/src/components/app/pdf/pdf.js b/packages/client/src/components/app/pdf/pdf.ts similarity index 69% rename from packages/client/src/components/app/pdf/pdf.js rename to packages/client/src/components/app/pdf/pdf.ts index f3579e5f82..ed47dc9831 100644 --- a/packages/client/src/components/app/pdf/pdf.js +++ b/packages/client/src/components/app/pdf/pdf.ts @@ -1,52 +1,58 @@ +// @ts-ignore import html2pdf from "html2pdf.js" -export const pxToPt = px => (px / 4) * 3 -export const ptToPx = pt => (pt / 3) * 4 +export const pxToPt = (px: number) => (px / 4) * 3 +export const ptToPx = (pt: number) => (pt / 3) * 4 + export const A4HeightPx = ptToPx(841.92) + 1 -export async function htmlToPdf(el, opts = {}) { - const defaultOpts = { +export interface PDFOptions { + fileName?: string + marginPt?: number + orientation?: "portrait" | "landscape" + htmlScale?: number + footer?: boolean +} + +export async function htmlToPdf(el: HTMLElement, opts: PDFOptions = {}) { + const userOpts: Required = { fileName: "file.pdf", - margin: 60, + marginPt: 60, orientation: "portrait", htmlScale: 1, - progressCallback: () => {}, footer: true, - } - opts = { - ...defaultOpts, ...opts, } return new Promise(resolve => { // Sanity check title - let fileName = opts.fileName + let fileName = userOpts.fileName if (!fileName.endsWith(".pdf")) { fileName += ".pdf" } // Config const options = { - margin: opts.margin, + margin: userOpts.marginPt, filename: fileName, image: { type: "jpeg", quality: 0.95 }, html2canvas: { dpi: 192, scale: 2, useCORS: true }, jsPDF: { - orientation: opts.orientation, + orientation: userOpts.orientation, unit: "pt", format: "a4", }, pagebreak: { avoid: ".no-break" }, // Custom params - htmlScale: opts.htmlScale, + htmlScale: userOpts.htmlScale, } let worker = html2pdf().set(options).from(el).toPdf() // Add footer if required if (opts.footer) { - worker = worker.get("pdf").then(pdf => { + worker = worker.get("pdf").then((pdf: any) => { const totalPages = pdf.internal.getNumberOfPages() for (let i = 1; i <= totalPages; i++) { pdf.setPage(i) From 0c29cdf520556bd7a445313523441610ae5bf2a6 Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Fri, 21 Mar 2025 12:27:01 +0000 Subject: [PATCH 14/69] Massively increase grid DND performance by reducing the cost of style recomputation by reducing the quantity of placeholder elements required for guide lines --- .../_components/Screen/GeneralPanel.svelte | 2 +- packages/client/manifest.json | 1 + .../app/container/GridContainer.svelte | 47 +++++++++++------- .../client/src/components/app/pdf/PDF.svelte | 48 +++++++++++++------ 4 files changed, 66 insertions(+), 32 deletions(-) diff --git a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/[componentId]/_components/Screen/GeneralPanel.svelte b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/[componentId]/_components/Screen/GeneralPanel.svelte index 0bf45b16b8..ea7e548bbd 100644 --- a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/[componentId]/_components/Screen/GeneralPanel.svelte +++ b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/[componentId]/_components/Screen/GeneralPanel.svelte @@ -10,7 +10,7 @@ } from "@budibase/bbui" import PropertyControl from "@/components/design/settings/controls/PropertyControl.svelte" import RoleSelect from "@/components/design/settings/controls/RoleSelect.svelte" - import { selectedScreen, screenStore, componentStore } from "@/stores/builder" + import { selectedScreen, screenStore } from "@/stores/builder" import sanitizeUrl from "@/helpers/sanitizeUrl" import ButtonActionEditor from "@/components/design/settings/controls/ButtonActionEditor/ButtonActionEditor.svelte" import { getBindableProperties } from "@/dataBinding" diff --git a/packages/client/manifest.json b/packages/client/manifest.json index 8b1ed79973..efd392f54d 100644 --- a/packages/client/manifest.json +++ b/packages/client/manifest.json @@ -8064,6 +8064,7 @@ "icon": "Document", "hasChildren": true, "showEmptyState": false, + "illegalChildren": ["sidepanel", "modal", "gridblock"], "grid": { "hAlign": "center", "vAlign": "start" diff --git a/packages/client/src/components/app/container/GridContainer.svelte b/packages/client/src/components/app/container/GridContainer.svelte index 03598d53bd..66b7e14bac 100644 --- a/packages/client/src/components/app/container/GridContainer.svelte +++ b/packages/client/src/components/app/container/GridContainer.svelte @@ -135,12 +135,18 @@ use:styleable={$styles} data-cols={GridColumns} data-col-size={colSize} + data-required-rows={requiredRows} on:click={onClick} > {#if inBuilder} -
- {#each { length: GridColumns * rows } as _, idx} -
+
+ {#each { length: rows } as _} +
+ {/each} +
+
+ {#each { length: GridColumns } as _} +
{/each}
{/if} @@ -151,7 +157,8 @@ From 4563e9a13f8c8610e67b989a037cfe80224fede5 Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Fri, 21 Mar 2025 14:05:34 +0000 Subject: [PATCH 15/69] Improve real time updates of page counts to avoid jumping --- .../client/src/components/app/pdf/PDF.svelte | 28 ++++++++++++++----- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/packages/client/src/components/app/pdf/PDF.svelte b/packages/client/src/components/app/pdf/PDF.svelte index ad6d6dbf03..842baf329d 100644 --- a/packages/client/src/components/app/pdf/PDF.svelte +++ b/packages/client/src/components/app/pdf/PDF.svelte @@ -18,7 +18,9 @@ let rendering = false let pageCount = 1 + let nextPageCount = 1 let ref: HTMLElement + let gridRef: HTMLElement $: safeName = fileName || "Report" $: safeButtonText = buttonText || "Download PDF" @@ -47,17 +49,29 @@ return `--idx:"${idx + 1}"; --top:${top}px;` } + const handleGridMutation = () => { + if (gridRef.classList.contains("highlight")) { + // If we're actively dragging then we can grow but not shrink the page + // count, to avoid jumping + const rows = parseInt(gridRef.dataset.requiredRows || "1") + nextPageCount = Math.max(1, Math.ceil(rows / DesiredRows)) + if (nextPageCount > pageCount) { + pageCount = nextPageCount + } + } else { + // Once we stop dragging, apply this new page count + pageCount = nextPageCount + } + } + onMount(() => { // Observe required content rows and use this to determine required pages const gridDOMID = `${$component.id}-grid-dom` - const grid = document.getElementsByClassName(gridDOMID)[0] as HTMLElement - const mutationObserver = new MutationObserver(() => { - const rows = parseInt(grid.dataset.requiredRows || "1") - pageCount = Math.max(1, Math.ceil(rows / DesiredRows)) - }) - mutationObserver.observe(grid, { + gridRef = document.getElementsByClassName(gridDOMID)[0] as HTMLElement + const mutationObserver = new MutationObserver(handleGridMutation) + mutationObserver.observe(gridRef, { attributes: true, - attributeFilter: ["data-required-rows"], + attributeFilter: ["data-required-rows", "class"], }) return () => { mutationObserver.disconnect() From b4763f6430921eeb4f495011b8d4f9eef457bd05 Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Fri, 21 Mar 2025 14:55:06 +0000 Subject: [PATCH 16/69] Ensure pages are properly removed when deleting components --- packages/client/src/components/app/pdf/PDF.svelte | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/packages/client/src/components/app/pdf/PDF.svelte b/packages/client/src/components/app/pdf/PDF.svelte index 842baf329d..c7f43e824b 100644 --- a/packages/client/src/components/app/pdf/PDF.svelte +++ b/packages/client/src/components/app/pdf/PDF.svelte @@ -18,7 +18,6 @@ let rendering = false let pageCount = 1 - let nextPageCount = 1 let ref: HTMLElement let gridRef: HTMLElement @@ -50,16 +49,9 @@ } const handleGridMutation = () => { - if (gridRef.classList.contains("highlight")) { - // If we're actively dragging then we can grow but not shrink the page - // count, to avoid jumping - const rows = parseInt(gridRef.dataset.requiredRows || "1") - nextPageCount = Math.max(1, Math.ceil(rows / DesiredRows)) - if (nextPageCount > pageCount) { - pageCount = nextPageCount - } - } else { - // Once we stop dragging, apply this new page count + const rows = parseInt(gridRef.dataset.requiredRows || "1") + const nextPageCount = Math.max(1, Math.ceil(rows / DesiredRows)) + if (nextPageCount > pageCount || !gridRef.classList.contains("highlight")) { pageCount = nextPageCount } } From 04f5dede3cbc8c22070de89789d1fb21c492563d Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Fri, 21 Mar 2025 14:57:56 +0000 Subject: [PATCH 17/69] Remove PDF component from list --- .../[componentId]/new/_components/componentStructure.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/[componentId]/new/_components/componentStructure.json b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/[componentId]/new/_components/componentStructure.json index 0d6eb876aa..d809095dc0 100644 --- a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/[componentId]/new/_components/componentStructure.json +++ b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/[componentId]/new/_components/componentStructure.json @@ -8,8 +8,7 @@ "formblock", "multistepformblock", "chartblock", - "rowexplorer", - "pdf" + "rowexplorer" ] }, { From 1bb7059cd40f0e0bc89c8931c2026e33a8ec2aae Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Fri, 21 Mar 2025 15:01:26 +0000 Subject: [PATCH 18/69] Restore ability to create all autoscreens --- .../_components/NewScreen/CreateScreenModal.svelte | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/packages/builder/src/pages/builder/app/[application]/design/_components/NewScreen/CreateScreenModal.svelte b/packages/builder/src/pages/builder/app/[application]/design/_components/NewScreen/CreateScreenModal.svelte index c717900aea..12186a7055 100644 --- a/packages/builder/src/pages/builder/app/[application]/design/_components/NewScreen/CreateScreenModal.svelte +++ b/packages/builder/src/pages/builder/app/[application]/design/_components/NewScreen/CreateScreenModal.svelte @@ -101,8 +101,11 @@ } } - const createBlankScreen = async ({ route }) => { - const screenTemplates = screenTemplating.pdf({ route, screens }) + const createBasicScreen = async ({ route }) => { + const screenTemplates = + mode === AutoScreenTypes.BLANK + ? screenTemplating.blank({ route, screens }) + : screenTemplating.pdf({ route, screens }) const newScreens = await createScreens(screenTemplates) loadNewScreen(newScreens[0]) } @@ -243,7 +246,7 @@ - + From 09d92ed1de09cf0aeec53b954f3bf00fbeb499b1 Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Fri, 21 Mar 2025 15:04:14 +0000 Subject: [PATCH 19/69] Ensure desktop view is active when swapping to a PDF screen --- .../builder/src/stores/builder/screens.ts | 20 ++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/packages/builder/src/stores/builder/screens.ts b/packages/builder/src/stores/builder/screens.ts index 51072adbb8..a67490badb 100644 --- a/packages/builder/src/stores/builder/screens.ts +++ b/packages/builder/src/stores/builder/screens.ts @@ -4,22 +4,24 @@ import { Helpers } from "@budibase/bbui" import { RoleUtils, Utils } from "@budibase/frontend-core" import { findAllMatchingComponents } from "@/helpers/components" import { - layoutStore, appStore, componentStore, + layoutStore, navigationStore, + previewStore, selectedComponent, } from "@/stores/builder" import { createHistoryStore, HistoryStore } from "@/stores/builder/history" import { API } from "@/api" import { BudiStore } from "../BudiStore" import { - FetchAppPackageResponse, - DeleteScreenResponse, - Screen, Component, - SaveScreenResponse, ComponentDefinition, + DeleteScreenResponse, + FetchAppPackageResponse, + SaveScreenResponse, + Screen, + ScreenVariant, } from "@budibase/types" interface ScreenState { @@ -115,6 +117,14 @@ export class ScreenStore extends BudiStore { state.selectedScreenId = screen._id return state }) + + // If this is a PDF screen, ensure we're on desktop + if ( + screen.variant === ScreenVariant.PDF && + get(previewStore).previewDevice !== "desktop" + ) { + previewStore.setDevice("desktop") + } } /** From 588ad5f907eed88d2a8fd628be67aaf4670ca86a Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Thu, 27 Mar 2025 09:36:31 +0000 Subject: [PATCH 20/69] Add PDF specific table component --- .../new/_components/componentStructure.json | 1 + packages/client/manifest.json | 56 ++++++++ packages/client/src/components/app/index.js | 2 +- .../src/components/app/pdf/PDFTable.svelte | 133 ++++++++++++++++++ .../client/src/components/app/pdf/index.ts | 2 + 5 files changed, 193 insertions(+), 1 deletion(-) create mode 100644 packages/client/src/components/app/pdf/PDFTable.svelte create mode 100644 packages/client/src/components/app/pdf/index.ts diff --git a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/[componentId]/new/_components/componentStructure.json b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/[componentId]/new/_components/componentStructure.json index d809095dc0..21cb25f74f 100644 --- a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/[componentId]/new/_components/componentStructure.json +++ b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/[componentId]/new/_components/componentStructure.json @@ -23,6 +23,7 @@ "dataprovider", "repeater", "gridblock", + "pdftable", "spreadsheet", "dynamicfilter", "daterangepicker" diff --git a/packages/client/manifest.json b/packages/client/manifest.json index efd392f54d..17e25347ad 100644 --- a/packages/client/manifest.json +++ b/packages/client/manifest.json @@ -8088,5 +8088,61 @@ "defaultValue": "Download PDF" } ] + }, + "pdftable": { + "name": "Plain Table", + "icon": "Table", + "styles": ["size"], + "size": { + "width": 600, + "height": 400 + }, + "grid": { + "hAlign": "stretch", + "vAlign": "stretch" + }, + "settings": [ + { + "type": "dataSource", + "label": "Data", + "key": "datasource", + "required": true + }, + { + "type": "filter", + "label": "Filtering", + "key": "filter", + "dependsOn": { + "setting": "datasource.type", + "value": "custom", + "invert": true + } + }, + { + "type": "field/sortable", + "label": "Sort column", + "key": "sortColumn", + "placeholder": "Default", + "dependsOn": { + "setting": "datasource.type", + "value": "custom", + "invert": true + } + }, + { + "type": "select", + "label": "Sort order", + "key": "sortOrder", + "options": ["Ascending", "Descending"], + "defaultValue": "Ascending", + "dependsOn": "sortColumn" + }, + { + "type": "columns/basic", + "label": "Columns", + "key": "columns", + "resetOn": "datasource" + } + ] } } diff --git a/packages/client/src/components/app/index.js b/packages/client/src/components/app/index.js index 547cb50230..1cad2019fb 100644 --- a/packages/client/src/components/app/index.js +++ b/packages/client/src/components/app/index.js @@ -36,11 +36,11 @@ export { default as sidepanel } from "./SidePanel.svelte" export { default as modal } from "./Modal.svelte" export { default as gridblock } from "./GridBlock.svelte" export { default as textv2 } from "./Text.svelte" -export { default as pdf } from "./pdf/PDF.svelte" export * from "./charts" export * from "./forms" export * from "./blocks" export * from "./dynamic-filter" +export * from "./pdf" // Deprecated component left for compatibility in old apps export * from "./deprecated/table" diff --git a/packages/client/src/components/app/pdf/PDFTable.svelte b/packages/client/src/components/app/pdf/PDFTable.svelte new file mode 100644 index 0000000000..7335a548fe --- /dev/null +++ b/packages/client/src/components/app/pdf/PDFTable.svelte @@ -0,0 +1,133 @@ + + +
+
+ {#if schema} + {#each Object.keys(schema) as col} +
{schema[col].displayName}
+ {/each} + {#each $fetch.rows as row} + {#each Object.keys(schema) as col} +
{row[col]}
+ {/each} + {/each} + {/if} +
+
+ + diff --git a/packages/client/src/components/app/pdf/index.ts b/packages/client/src/components/app/pdf/index.ts new file mode 100644 index 0000000000..ae100c894b --- /dev/null +++ b/packages/client/src/components/app/pdf/index.ts @@ -0,0 +1,2 @@ +export { default as pdf } from "./PDF.svelte" +export { default as pdftable } from "./PDFTable.svelte" From 7a5b311a7ee6a27c57e4d24dcb2c8499360a8764 Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Thu, 27 Mar 2025 16:25:38 +0000 Subject: [PATCH 21/69] Support string values when formatting dates --- packages/bbui/src/helpers.ts | 5 +- .../frontend-core/src/utils/formatting.ts | 120 ++++++++++++++++++ 2 files changed, 124 insertions(+), 1 deletion(-) create mode 100644 packages/frontend-core/src/utils/formatting.ts diff --git a/packages/bbui/src/helpers.ts b/packages/bbui/src/helpers.ts index 330f381d53..10ccf4683d 100644 --- a/packages/bbui/src/helpers.ts +++ b/packages/bbui/src/helpers.ts @@ -211,9 +211,12 @@ const localeDateFormat = new Intl.DateTimeFormat() // Formats a dayjs date according to schema flags export const getDateDisplayValue = ( - value: dayjs.Dayjs | null, + value: dayjs.Dayjs | string | null, { enableTime = true, timeOnly = false } = {} ): string => { + if (typeof value === "string") { + value = dayjs(value) + } if (!value?.isValid()) { return "" } diff --git a/packages/frontend-core/src/utils/formatting.ts b/packages/frontend-core/src/utils/formatting.ts new file mode 100644 index 0000000000..2fa1a59adf --- /dev/null +++ b/packages/frontend-core/src/utils/formatting.ts @@ -0,0 +1,120 @@ +import { + BBReferenceFieldMetadata, + BBReferenceFieldSubType, + BBReferenceSingleFieldMetadata, + DateFieldMetadata, + FieldSchema, + FieldType, + Row, + TableSchema, +} from "@budibase/types" +import { Helpers } from "@budibase/bbui" + +// Singleton formatter to save us creating one every time +const NumberFormatter = Intl.NumberFormat() + +export type StringifiedRow = { [key: string]: string } + +// Formats a number according to the locale +export const formatNumber = (value: any): string => { + const type = typeof value + if (type !== "string" && type !== "number") { + return "" + } + if (type === "string" && !value.trim().length) { + return "" + } + const res = NumberFormatter.format(value) + return res === "NaN" ? stringifyValue(value) : res +} + +// Attempts to stringify any type of value +const stringifyValue = (value: any): string => { + if (value == null) { + return "" + } + if (typeof value === "string") { + return value + } + if (typeof value.toString === "function") { + return stringifyValue(value.toString()) + } + try { + return JSON.stringify(value) + } catch (e) { + return "" + } +} + +const stringifyField = (value: any, schema: FieldSchema): string => { + switch (schema.type) { + // TODO + case FieldType.ATTACHMENT_SINGLE: + case FieldType.ATTACHMENTS: + case FieldType.AUTO: + case FieldType.JSON: + case FieldType.LINK: + case FieldType.SIGNATURE_SINGLE: + return "" + + // User is the only BB reference subtype right now + case FieldType.BB_REFERENCE: + case FieldType.BB_REFERENCE_SINGLE: { + if ( + schema.subtype !== BBReferenceFieldSubType.USERS && + schema.subtype !== BBReferenceFieldSubType.USER + ) { + return "" + } + if (!value) { + return "" + } + const arrayVal = Array.isArray(value) ? value : [value] + return arrayVal?.map((user: any) => user.primaryDisplay).join(", ") || "" + } + + // Join arrays with commas + case FieldType.ARRAY: + return value?.join(", ") || "" + + // Just capitalise booleans + case FieldType.BOOLEAN: + return Helpers.capitalise(value?.toString() || "false") + + // Format dates into something readable + case FieldType.DATETIME: { + return Helpers.getDateDisplayValue(value, { + enableTime: !schema.dateOnly, + timeOnly: schema.timeOnly, + }) + } + + // Format numbers using a locale string + case FieldType.NUMBER: + return formatNumber(value) + + // Simple string types + case FieldType.STRING: + case FieldType.LONGFORM: + case FieldType.BIGINT: + case FieldType.OPTIONS: + case FieldType.AI: + case FieldType.BARCODEQR: + return value || "" + + // Fallback for unknown types or future column types that we forget to add + case FieldType.FORMULA: + default: + return stringifyValue(value) + } +} + +// Stringifies every property of a row, ensuring they are all human-readable +// strings for display +export const stringifyRow = (row: Row, schema: TableSchema): StringifiedRow => { + let stringified: StringifiedRow = {} + Object.entries(schema).forEach(([field, fieldSchema]) => { + stringified[field] = stringifyField(row[field], fieldSchema) + }) + return stringified +} From 776f1cf61f60db5e5c92c14a3c98b162cd2883d6 Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Thu, 27 Mar 2025 16:26:00 +0000 Subject: [PATCH 22/69] Add WIP on core formatting utils for row values --- .../src/components/app/pdf/PDFTable.svelte | 7 +++++-- .../src/components/grid/cells/NumberCell.svelte | 17 +---------------- packages/frontend-core/src/utils/index.ts | 1 + 3 files changed, 7 insertions(+), 18 deletions(-) diff --git a/packages/client/src/components/app/pdf/PDFTable.svelte b/packages/client/src/components/app/pdf/PDFTable.svelte index 7335a548fe..3c65e6f707 100644 --- a/packages/client/src/components/app/pdf/PDFTable.svelte +++ b/packages/client/src/components/app/pdf/PDFTable.svelte @@ -8,7 +8,7 @@ UISearchFilter, UserDatasource, } from "@budibase/types" - import { fetchData, QueryUtils } from "@budibase/frontend-core/src" + import { fetchData, QueryUtils, stringifyRow } from "@budibase/frontend-core" import { getContext } from "svelte" type ProviderDatasource = Exclude< @@ -39,6 +39,9 @@ $: schema = sanitizeSchema($fetch.schema, columns) $: columnCount = Object.keys(schema).length $: rowCount = $fetch.rows?.length || 0 + $: stringifiedRows = ($fetch?.rows || []).map(row => + stringifyRow(row, schema) + ) const createFetch = (datasource: ProviderDatasource) => { return fetchData({ @@ -95,7 +98,7 @@ {#each Object.keys(schema) as col}
{schema[col].displayName}
{/each} - {#each $fetch.rows as row} + {#each stringifiedRows as row} {#each Object.keys(schema) as col}
{row[col]}
{/each} diff --git a/packages/frontend-core/src/components/grid/cells/NumberCell.svelte b/packages/frontend-core/src/components/grid/cells/NumberCell.svelte index c8ae96ef21..5ac6e14b6e 100644 --- a/packages/frontend-core/src/components/grid/cells/NumberCell.svelte +++ b/packages/frontend-core/src/components/grid/cells/NumberCell.svelte @@ -1,8 +1,5 @@ - - Date: Fri, 28 Mar 2025 10:58:54 +0000 Subject: [PATCH 23/69] Add nested JSON fields to plain table and create core util for nested JSON fields --- packages/builder/src/dataBinding.js | 22 ++---------------- .../src/components/app/pdf/PDFTable.svelte | 12 +++++++--- .../frontend-core/src/utils/formatting.ts | 10 ++++++-- packages/frontend-core/src/utils/schema.js | 23 +++++++++++++++++++ 4 files changed, 42 insertions(+), 25 deletions(-) diff --git a/packages/builder/src/dataBinding.js b/packages/builder/src/dataBinding.js index 1011774ac5..ada1ee274d 100644 --- a/packages/builder/src/dataBinding.js +++ b/packages/builder/src/dataBinding.js @@ -26,7 +26,7 @@ import { getJsHelperList, } from "@budibase/string-templates" import { TableNames } from "./constants" -import { JSONUtils, Constants } from "@budibase/frontend-core" +import { JSONUtils, Constants, SchemaUtils } from "@budibase/frontend-core" import ActionDefinitions from "@/components/design/settings/controls/ButtonActionEditor/manifest.json" import { environment, licensing } from "@/stores/portal" import { convertOldFieldFormat } from "@/components/design/settings/controls/FieldConfiguration/utils" @@ -1026,25 +1026,7 @@ export const getSchemaForDatasource = (asset, datasource, options) => { // Check for any JSON fields so we can add any top level properties if (schema) { - let jsonAdditions = {} - Object.keys(schema).forEach(fieldKey => { - const fieldSchema = schema[fieldKey] - if (fieldSchema?.type === "json") { - const jsonSchema = JSONUtils.convertJSONSchemaToTableSchema( - fieldSchema, - { - squashObjects: true, - } - ) - Object.keys(jsonSchema).forEach(jsonKey => { - jsonAdditions[`${fieldKey}.${jsonKey}`] = { - type: jsonSchema[jsonKey].type, - nestedJSON: true, - } - }) - } - }) - schema = { ...schema, ...jsonAdditions } + schema = SchemaUtils.addNestedJSONSchemaFields(schema) } // Determine if we should add ID and rev to the schema diff --git a/packages/client/src/components/app/pdf/PDFTable.svelte b/packages/client/src/components/app/pdf/PDFTable.svelte index 3c65e6f707..a9376460df 100644 --- a/packages/client/src/components/app/pdf/PDFTable.svelte +++ b/packages/client/src/components/app/pdf/PDFTable.svelte @@ -8,7 +8,12 @@ UISearchFilter, UserDatasource, } from "@budibase/types" - import { fetchData, QueryUtils, stringifyRow } from "@budibase/frontend-core" + import { + fetchData, + QueryUtils, + stringifyRow, + SchemaUtils, + } from "@budibase/frontend-core" import { getContext } from "svelte" type ProviderDatasource = Exclude< @@ -88,7 +93,8 @@ sanitized = pruned } - return sanitized + // Add nested JSON fields + return SchemaUtils.addNestedJSONSchemaFields(sanitized) } @@ -114,7 +120,7 @@ } .table { display: grid; - grid-template-columns: repeat(var(--cols), minmax(20px, auto)); + grid-template-columns: repeat(var(--cols), minmax(40px, auto)); grid-template-rows: repeat(var(--rows), max-content); overflow: hidden; background: white; diff --git a/packages/frontend-core/src/utils/formatting.ts b/packages/frontend-core/src/utils/formatting.ts index 2fa1a59adf..66c10de0d8 100644 --- a/packages/frontend-core/src/utils/formatting.ts +++ b/packages/frontend-core/src/utils/formatting.ts @@ -52,11 +52,14 @@ const stringifyField = (value: any, schema: FieldSchema): string => { case FieldType.ATTACHMENT_SINGLE: case FieldType.ATTACHMENTS: case FieldType.AUTO: - case FieldType.JSON: case FieldType.LINK: case FieldType.SIGNATURE_SINGLE: return "" + // Stringify JSON blobs + case FieldType.JSON: + return value ? JSON.stringify(value) : "" + // User is the only BB reference subtype right now case FieldType.BB_REFERENCE: case FieldType.BB_REFERENCE_SINGLE: { @@ -114,7 +117,10 @@ const stringifyField = (value: any, schema: FieldSchema): string => { export const stringifyRow = (row: Row, schema: TableSchema): StringifiedRow => { let stringified: StringifiedRow = {} Object.entries(schema).forEach(([field, fieldSchema]) => { - stringified[field] = stringifyField(row[field], fieldSchema) + stringified[field] = stringifyField( + Helpers.deepGet(row, field), + fieldSchema + ) }) return stringified } diff --git a/packages/frontend-core/src/utils/schema.js b/packages/frontend-core/src/utils/schema.js index cd55d4983d..135dbd3e35 100644 --- a/packages/frontend-core/src/utils/schema.js +++ b/packages/frontend-core/src/utils/schema.js @@ -1,5 +1,6 @@ import { helpers } from "@budibase/shared-core" import { TypeIconMap } from "../constants" +import { convertJSONSchemaToTableSchema } from "./json" export const getColumnIcon = column => { // For some reason we have remix icons saved under this property sometimes, @@ -24,3 +25,25 @@ export const getColumnIcon = column => { return result || "Text" } + +export const addNestedJSONSchemaFields = schema => { + if (!schema) { + return schema + } + let jsonAdditions = {} + Object.keys(schema).forEach(fieldKey => { + const fieldSchema = schema[fieldKey] + if (fieldSchema?.type === "json") { + const jsonSchema = convertJSONSchemaToTableSchema(fieldSchema, { + squashObjects: true, + }) + Object.keys(jsonSchema).forEach(jsonKey => { + jsonAdditions[`${fieldKey}.${jsonKey}`] = { + type: jsonSchema[jsonKey].type, + nestedJSON: true, + } + }) + } + }) + return { ...schema, ...jsonAdditions } +} From 4dfe10a5ad63675376fefa07de79f7888668b656 Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Fri, 28 Mar 2025 11:30:09 +0000 Subject: [PATCH 24/69] Add new top level column editor and ensure all types can be stringified in PDF tables --- .../design/settings/componentSettings.js | 4 ++ .../controls/ColumnEditor/ColumnEditor.svelte | 17 ++++---- .../ColumnEditor/TopLevelColumnEditor.svelte | 15 +++++++ packages/client/manifest.json | 2 +- .../src/components/app/pdf/PDFTable.svelte | 3 +- .../frontend-core/src/utils/formatting.ts | 39 +++++++++++++++---- .../types/src/documents/app/table/schema.ts | 2 + 7 files changed, 63 insertions(+), 19 deletions(-) create mode 100644 packages/builder/src/components/design/settings/controls/ColumnEditor/TopLevelColumnEditor.svelte diff --git a/packages/builder/src/components/design/settings/componentSettings.js b/packages/builder/src/components/design/settings/componentSettings.js index 1cf4b0211c..86014f152c 100644 --- a/packages/builder/src/components/design/settings/componentSettings.js +++ b/packages/builder/src/components/design/settings/componentSettings.js @@ -22,6 +22,7 @@ import ValidationEditor from "./controls/ValidationEditor/ValidationEditor.svelt import DrawerBindableInput from "@/components/common/bindings/DrawerBindableInput.svelte" import ColumnEditor from "./controls/ColumnEditor/ColumnEditor.svelte" import BasicColumnEditor from "./controls/ColumnEditor/BasicColumnEditor.svelte" +import TopLevelColumnEditor from "./controls/ColumnEditor/TopLevelColumnEditor.svelte" import GridColumnEditor from "./controls/GridColumnConfiguration/GridColumnConfiguration.svelte" import BarButtonList from "./controls/BarButtonList.svelte" import FieldConfiguration from "./controls/FieldConfiguration/FieldConfiguration.svelte" @@ -62,7 +63,10 @@ const componentMap = { stepConfiguration: FormStepConfiguration, formStepControls: FormStepControls, columns: ColumnEditor, + // "Basic" actually includes nested JSON and relationship fields "columns/basic": BasicColumnEditor, + // "Top level" is only the top level schema fields + "columns/toplevel": TopLevelColumnEditor, "columns/grid": GridColumnEditor, tableConditions: TableConditionEditor, "field/sortable": SortableFieldSelect, diff --git a/packages/builder/src/components/design/settings/controls/ColumnEditor/ColumnEditor.svelte b/packages/builder/src/components/design/settings/controls/ColumnEditor/ColumnEditor.svelte index 5e27b591f8..d4190068c1 100644 --- a/packages/builder/src/components/design/settings/controls/ColumnEditor/ColumnEditor.svelte +++ b/packages/builder/src/components/design/settings/controls/ColumnEditor/ColumnEditor.svelte @@ -10,10 +10,17 @@ } from "@/dataBinding" import { selectedScreen, tables } from "@/stores/builder" - export let componentInstance + const getSearchableFields = (schema, tableList) => { + return search.getFields(tableList, Object.values(schema || {}), { + allowLinks: true, + }) + } + + export let componentInstance = undefined export let value = [] export let allowCellEditing = true export let allowReorder = true + export let getSchemaFields = getSearchableFields const dispatch = createEventDispatcher() @@ -28,13 +35,7 @@ : enrichedSchemaFields?.map(field => field.name) $: sanitisedValue = getValidColumns(value, options) $: updateBoundValue(sanitisedValue) - $: enrichedSchemaFields = search.getFields( - $tables.list, - Object.values(schema || {}), - { - allowLinks: true, - } - ) + $: enrichedSchemaFields = getSchemaFields(schema, $tables.list) $: { value = (value || []).filter( diff --git a/packages/builder/src/components/design/settings/controls/ColumnEditor/TopLevelColumnEditor.svelte b/packages/builder/src/components/design/settings/controls/ColumnEditor/TopLevelColumnEditor.svelte new file mode 100644 index 0000000000..69a80a85da --- /dev/null +++ b/packages/builder/src/components/design/settings/controls/ColumnEditor/TopLevelColumnEditor.svelte @@ -0,0 +1,15 @@ + + + diff --git a/packages/client/manifest.json b/packages/client/manifest.json index 17e25347ad..055086baf7 100644 --- a/packages/client/manifest.json +++ b/packages/client/manifest.json @@ -8138,7 +8138,7 @@ "dependsOn": "sortColumn" }, { - "type": "columns/basic", + "type": "columns/toplevel", "label": "Columns", "key": "columns", "resetOn": "datasource" diff --git a/packages/client/src/components/app/pdf/PDFTable.svelte b/packages/client/src/components/app/pdf/PDFTable.svelte index a9376460df..793134984c 100644 --- a/packages/client/src/components/app/pdf/PDFTable.svelte +++ b/packages/client/src/components/app/pdf/PDFTable.svelte @@ -93,8 +93,7 @@ sanitized = pruned } - // Add nested JSON fields - return SchemaUtils.addNestedJSONSchemaFields(sanitized) + return sanitized } diff --git a/packages/frontend-core/src/utils/formatting.ts b/packages/frontend-core/src/utils/formatting.ts index 66c10de0d8..af0fe5ebd2 100644 --- a/packages/frontend-core/src/utils/formatting.ts +++ b/packages/frontend-core/src/utils/formatting.ts @@ -1,8 +1,5 @@ import { - BBReferenceFieldMetadata, BBReferenceFieldSubType, - BBReferenceSingleFieldMetadata, - DateFieldMetadata, FieldSchema, FieldType, Row, @@ -48,14 +45,40 @@ const stringifyValue = (value: any): string => { const stringifyField = (value: any, schema: FieldSchema): string => { switch (schema.type) { - // TODO - case FieldType.ATTACHMENT_SINGLE: - case FieldType.ATTACHMENTS: + // Auto should not exist as it should always be typed by its underlying + // real type, like date or user case FieldType.AUTO: - case FieldType.LINK: - case FieldType.SIGNATURE_SINGLE: return "" + // Just state whether signatures exist or not + case FieldType.SIGNATURE_SINGLE: + return value ? "Yes" : "No" + + // Extract attachment names + case FieldType.ATTACHMENT_SINGLE: + case FieldType.ATTACHMENTS: { + if (!value) { + return "" + } + const arrayValue = Array.isArray(value) ? value : [value] + return arrayValue + .map(x => x.name) + .filter(x => !!x) + .join(", ") + } + + // Extract primary displays from relationships + case FieldType.LINK: { + if (!value) { + return "" + } + const arrayValue = Array.isArray(value) ? value : [value] + return arrayValue + .map(x => x.primaryDisplay) + .filter(x => !!x) + .join(", ") + } + // Stringify JSON blobs case FieldType.JSON: return value ? JSON.stringify(value) : "" diff --git a/packages/types/src/documents/app/table/schema.ts b/packages/types/src/documents/app/table/schema.ts index 86e15e4974..e3f9d3af3f 100644 --- a/packages/types/src/documents/app/table/schema.ts +++ b/packages/types/src/documents/app/table/schema.ts @@ -207,6 +207,8 @@ export interface BaseFieldSchema extends UIFieldMetadata { autocolumn?: boolean autoReason?: AutoReason.FOREIGN_KEY subtype?: never + // added when enriching nested JSON fields into schema + nestedJSON?: boolean } interface OtherFieldMetadata extends BaseFieldSchema { From b8b8828b13d5cfd087ec4c3a6f2bb59f7f164401 Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Fri, 28 Mar 2025 12:38:24 +0000 Subject: [PATCH 25/69] Add limit setting to PDF tables --- packages/client/manifest.json | 6 ++++++ .../client/src/components/app/pdf/PDFTable.svelte | 13 +++++-------- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/packages/client/manifest.json b/packages/client/manifest.json index 055086baf7..b7d68264f7 100644 --- a/packages/client/manifest.json +++ b/packages/client/manifest.json @@ -8142,6 +8142,12 @@ "label": "Columns", "key": "columns", "resetOn": "datasource" + }, + { + "type": "number", + "label": "Limit", + "key": "limit", + "defaultValue": 20 } ] } diff --git a/packages/client/src/components/app/pdf/PDFTable.svelte b/packages/client/src/components/app/pdf/PDFTable.svelte index 793134984c..306756f00b 100644 --- a/packages/client/src/components/app/pdf/PDFTable.svelte +++ b/packages/client/src/components/app/pdf/PDFTable.svelte @@ -8,12 +8,7 @@ UISearchFilter, UserDatasource, } from "@budibase/types" - import { - fetchData, - QueryUtils, - stringifyRow, - SchemaUtils, - } from "@budibase/frontend-core" + import { fetchData, QueryUtils, stringifyRow } from "@budibase/frontend-core" import { getContext } from "svelte" type ProviderDatasource = Exclude< @@ -28,6 +23,7 @@ export let sortColumn: string | undefined = undefined export let sortOrder: SortOrder | undefined = undefined export let columns: ChosenColumns = undefined + export let limit: number = 20 const component = getContext("component") const { styleable, API } = getContext("sdk") @@ -38,8 +34,7 @@ query, sortColumn, sortOrder, - limit: 100, - paginate: false, + limit, }) $: schema = sanitizeSchema($fetch.schema, columns) $: columnCount = Object.keys(schema).length @@ -56,6 +51,8 @@ query, sortColumn, sortOrder, + limit, + paginate: false, }, }) } From 4b8a5a7f7977bf4838716f638f34b0d6630f8d08 Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Fri, 28 Mar 2025 14:32:12 +0000 Subject: [PATCH 26/69] Add size setting to text component --- packages/client/manifest.json | 26 +++++++++++++++++++ .../client/src/components/app/Text.svelte | 7 +++-- 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/packages/client/manifest.json b/packages/client/manifest.json index b7d68264f7..f360a837d4 100644 --- a/packages/client/manifest.json +++ b/packages/client/manifest.json @@ -8017,6 +8017,32 @@ "key": "text", "wide": true }, + { + "type": "select", + "label": "Size", + "key": "size", + "defaultValue": "14px", + "showInBar": true, + "placeholder": "Default", + "options": [ + { + "label": "Small", + "value": "12px" + }, + { + "label": "Medium", + "value": "14px" + }, + { + "label": "Large", + "value": "18px" + }, + { + "label": "Extra Large", + "value": "24px" + } + ] + }, { "type": "select", "label": "Alignment", diff --git a/packages/client/src/components/app/Text.svelte b/packages/client/src/components/app/Text.svelte index 1880669c15..086c0c66e3 100644 --- a/packages/client/src/components/app/Text.svelte +++ b/packages/client/src/components/app/Text.svelte @@ -5,20 +5,23 @@ export let text: string = "" export let color: string | undefined = undefined export let align: "left" | "center" | "right" | "justify" = "left" + export let size: string | undefined = "14px" const component = getContext("component") const { styleable } = getContext("sdk") // Add in certain settings to styles - $: styles = enrichStyles($component.styles, color, align) + $: styles = enrichStyles($component.styles, color, align, size) const enrichStyles = ( styles: any, colorStyle: typeof color, - alignStyle: typeof align + alignStyle: typeof align, + size: string | undefined ) => { let additions: Record = { "text-align": alignStyle, + "font-size": size || "14px", } if (colorStyle) { additions.color = colorStyle From 3cade98428d97e7627f83d54a0142eaf14e8a0c3 Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Fri, 28 Mar 2025 15:57:15 +0000 Subject: [PATCH 27/69] Only show first 3 columns by default in PDF table, and line clamp all cells to 3 lines --- .../controls/ColumnEditor/ColumnDrawer.svelte | 2 +- .../controls/ColumnEditor/ColumnEditor.svelte | 3 ++- packages/client/manifest.json | 5 ++-- .../src/components/app/pdf/PDFTable.svelte | 25 +++++++++++-------- 4 files changed, 21 insertions(+), 14 deletions(-) diff --git a/packages/builder/src/components/design/settings/controls/ColumnEditor/ColumnDrawer.svelte b/packages/builder/src/components/design/settings/controls/ColumnEditor/ColumnDrawer.svelte index 09734c2ca4..df7932c74d 100644 --- a/packages/builder/src/components/design/settings/controls/ColumnEditor/ColumnDrawer.svelte +++ b/packages/builder/src/components/design/settings/controls/ColumnEditor/ColumnDrawer.svelte @@ -145,7 +145,7 @@
- By default, all columns will automatically be shown. + The default column configuration will automatically be shown.
You can manually control which columns are included by adding them below. diff --git a/packages/builder/src/components/design/settings/controls/ColumnEditor/ColumnEditor.svelte b/packages/builder/src/components/design/settings/controls/ColumnEditor/ColumnEditor.svelte index d4190068c1..fab905e8b7 100644 --- a/packages/builder/src/components/design/settings/controls/ColumnEditor/ColumnEditor.svelte +++ b/packages/builder/src/components/design/settings/controls/ColumnEditor/ColumnEditor.svelte @@ -21,6 +21,7 @@ export let allowCellEditing = true export let allowReorder = true export let getSchemaFields = getSearchableFields + export let placeholder = "All columns" const dispatch = createEventDispatcher() @@ -45,7 +46,7 @@ const getText = value => { if (!value?.length) { - return "All columns" + return placeholder } let text = `${value.length} column` if (value.length !== 1) { diff --git a/packages/client/manifest.json b/packages/client/manifest.json index f360a837d4..b47c4a63ab 100644 --- a/packages/client/manifest.json +++ b/packages/client/manifest.json @@ -8038,7 +8038,7 @@ "value": "18px" }, { - "label": "Extra Large", + "label": "Extra large", "value": "24px" } ] @@ -8167,7 +8167,8 @@ "type": "columns/toplevel", "label": "Columns", "key": "columns", - "resetOn": "datasource" + "resetOn": "datasource", + "placeholder": "First 3 columns" }, { "type": "number", diff --git a/packages/client/src/components/app/pdf/PDFTable.svelte b/packages/client/src/components/app/pdf/PDFTable.svelte index 306756f00b..c5d92aa59a 100644 --- a/packages/client/src/components/app/pdf/PDFTable.svelte +++ b/packages/client/src/components/app/pdf/PDFTable.svelte @@ -76,19 +76,21 @@ } }) - // Clean out unselected columns - if (columns?.length) { - let pruned: Schema = {} - for (let col of columns) { - if (sanitized[col.name]) { - pruned[col.name] = { - ...sanitized[col.name], - displayName: col.displayName || sanitized[col.name].displayName, - } + // Clean out unselected columns. + // Default to first 3 columns if none specified, as we are width contrained. + if (!columns?.length) { + columns = Object.values(sanitized).slice(0, 3) + } + let pruned: Schema = {} + for (let col of columns) { + if (sanitized[col.name]) { + pruned[col.name] = { + ...sanitized[col.name], + displayName: col.displayName || sanitized[col.name].displayName, } } - sanitized = pruned } + sanitized = pruned return sanitized } @@ -131,6 +133,9 @@ padding: var(--spacing-xs) var(--spacing-s); overflow: hidden; word-break: break-word; + display: -webkit-box; + -webkit-box-orient: vertical; + -webkit-line-clamp: 3; } .cell.header { font-weight: 600; From 7590534bb71d8cdcfd4af5c7ab9ae69f6c126f2f Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Mon, 31 Mar 2025 09:04:47 +0100 Subject: [PATCH 28/69] Update default sizing and props of PDF table --- packages/client/manifest.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/client/manifest.json b/packages/client/manifest.json index b47c4a63ab..a5557be5fb 100644 --- a/packages/client/manifest.json +++ b/packages/client/manifest.json @@ -8121,7 +8121,7 @@ "styles": ["size"], "size": { "width": 600, - "height": 400 + "height": 304 }, "grid": { "hAlign": "stretch", @@ -8174,7 +8174,7 @@ "type": "number", "label": "Limit", "key": "limit", - "defaultValue": 20 + "defaultValue": 10 } ] } From 959d49e32858d92fb2687e2e599007653cc492be Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Mon, 31 Mar 2025 09:05:06 +0100 Subject: [PATCH 29/69] Update name to PDF Table --- packages/client/manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/client/manifest.json b/packages/client/manifest.json index a5557be5fb..0afdbbc6de 100644 --- a/packages/client/manifest.json +++ b/packages/client/manifest.json @@ -8116,7 +8116,7 @@ ] }, "pdftable": { - "name": "Plain Table", + "name": "PDF Table", "icon": "Table", "styles": ["size"], "size": { From 8fae73f90071b1b08e39edd7f94dc8fe7a84fe7e Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Mon, 31 Mar 2025 09:51:05 +0100 Subject: [PATCH 30/69] Add a sensible error message when failing to find a row rather than an ugly error or a 500 --- .../components/app/SingleRowProvider.svelte | 28 +++++++++++++++++++ .../server/src/api/controllers/row/index.ts | 8 ++++-- 2 files changed, 33 insertions(+), 3 deletions(-) create mode 100644 packages/client/src/components/app/SingleRowProvider.svelte diff --git a/packages/client/src/components/app/SingleRowProvider.svelte b/packages/client/src/components/app/SingleRowProvider.svelte new file mode 100644 index 0000000000..d4ef950e88 --- /dev/null +++ b/packages/client/src/components/app/SingleRowProvider.svelte @@ -0,0 +1,28 @@ + + +
+ + + +
diff --git a/packages/server/src/api/controllers/row/index.ts b/packages/server/src/api/controllers/row/index.ts index 8f4629a5b0..49527419a8 100644 --- a/packages/server/src/api/controllers/row/index.ts +++ b/packages/server/src/api/controllers/row/index.ts @@ -144,9 +144,11 @@ export async function find(ctx: UserCtx) { const { tableId, viewId } = utils.getSourceId(ctx) const sourceId = viewId || tableId const rowId = ctx.params.rowId - - const response = await sdk.rows.find(sourceId, rowId) - ctx.body = response + try { + ctx.body = await sdk.rows.find(sourceId, rowId) + } catch (e) { + ctx.throw(400, "That row couldn't be found") + } } function isDeleteRows(input: any): input is DeleteRows { From 3ae20796a3dc3fabd34803a80d0a4927cd6f707f Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Mon, 31 Mar 2025 09:51:28 +0100 Subject: [PATCH 31/69] Fix bug with swapping order of components in new component panel --- .../[componentId]/new/_components/NewComponentPanel.svelte | 5 ----- 1 file changed, 5 deletions(-) diff --git a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/[componentId]/new/_components/NewComponentPanel.svelte b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/[componentId]/new/_components/NewComponentPanel.svelte index 1654ff5e06..767a32b626 100644 --- a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/[componentId]/new/_components/NewComponentPanel.svelte +++ b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/[componentId]/new/_components/NewComponentPanel.svelte @@ -144,11 +144,6 @@ } }) - // Swap blocks and plugins - let tmp = enrichedStructure[1] - enrichedStructure[1] = enrichedStructure[0] - enrichedStructure[0] = tmp - return enrichedStructure } From e8ff8b1abda44b72621644d72a0218c4979391ab Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Mon, 31 Mar 2025 09:51:40 +0100 Subject: [PATCH 32/69] Add single row provider component --- .../new/_components/componentStructure.json | 1 + packages/client/manifest.json | 30 +++++++++++++++++++ .../components/app/SingleRowProvider.svelte | 15 ++++++++-- packages/client/src/components/app/index.js | 1 + 4 files changed, 45 insertions(+), 2 deletions(-) diff --git a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/[componentId]/new/_components/componentStructure.json b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/[componentId]/new/_components/componentStructure.json index 21cb25f74f..c4ee0c9dd4 100644 --- a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/[componentId]/new/_components/componentStructure.json +++ b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/[componentId]/new/_components/componentStructure.json @@ -20,6 +20,7 @@ "name": "Data", "icon": "Data", "children": [ + "singlerowprovider", "dataprovider", "repeater", "gridblock", diff --git a/packages/client/manifest.json b/packages/client/manifest.json index 0afdbbc6de..3cd24ee0f8 100644 --- a/packages/client/manifest.json +++ b/packages/client/manifest.json @@ -8177,5 +8177,35 @@ "defaultValue": 10 } ] + }, + "singlerowprovider": { + "name": "Single Row Provider", + "icon": "SQLQuery", + "hasChildren": true, + "size": { + "width": 500, + "height": 200 + }, + "grid": { + "hAlign": "stretch", + "vAlign": "stretch" + }, + "settings": [ + { + "type": "table", + "label": "Datasource", + "key": "datasource", + "required": true + }, + { + "type": "text", + "label": "Row ID", + "key": "rowId", + "required": true + } + ], + "context": { + "type": "schema" + } } } diff --git a/packages/client/src/components/app/SingleRowProvider.svelte b/packages/client/src/components/app/SingleRowProvider.svelte index d4ef950e88..7271a1ed5e 100644 --- a/packages/client/src/components/app/SingleRowProvider.svelte +++ b/packages/client/src/components/app/SingleRowProvider.svelte @@ -1,8 +1,8 @@
- +
From 9390b21f63386bf2515b84276951db9bb54fae31 Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Mon, 31 Mar 2025 11:50:20 +0100 Subject: [PATCH 34/69] 400 > 404 --- packages/server/src/api/controllers/row/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/server/src/api/controllers/row/index.ts b/packages/server/src/api/controllers/row/index.ts index 49527419a8..1d0271a726 100644 --- a/packages/server/src/api/controllers/row/index.ts +++ b/packages/server/src/api/controllers/row/index.ts @@ -147,7 +147,7 @@ export async function find(ctx: UserCtx) { try { ctx.body = await sdk.rows.find(sourceId, rowId) } catch (e) { - ctx.throw(400, "That row couldn't be found") + ctx.throw(404, "That row couldn't be found") } } From 2d6dee717b13a27429ba6605a3f7f88d7c1d22de Mon Sep 17 00:00:00 2001 From: mike12345567 Date: Mon, 31 Mar 2025 17:50:46 +0100 Subject: [PATCH 35/69] Adding support for static formulas with a numeric response type when creating view calculations, this only applies to the internal DB but offers a way to transform say string columns to numbers and then calculate on these. --- .../grid/GridViewCalculationButton.svelte | 15 ++++++- .../src/api/routes/tests/viewV2.spec.ts | 45 ++++++++++++++++++- packages/server/src/sdk/app/views/index.ts | 7 ++- packages/types/src/documents/app/row.ts | 10 +++++ 4 files changed, 73 insertions(+), 4 deletions(-) diff --git a/packages/builder/src/components/backend/DataTable/buttons/grid/GridViewCalculationButton.svelte b/packages/builder/src/components/backend/DataTable/buttons/grid/GridViewCalculationButton.svelte index 7733a90bad..1e5ea1ebf6 100644 --- a/packages/builder/src/components/backend/DataTable/buttons/grid/GridViewCalculationButton.svelte +++ b/packages/builder/src/components/backend/DataTable/buttons/grid/GridViewCalculationButton.svelte @@ -6,7 +6,13 @@ Multiselect, Button, } from "@budibase/bbui" - import { CalculationType, canGroupBy, isNumeric } from "@budibase/types" + import { + CalculationType, + canGroupBy, + FieldType, + isNumeric, + isNumericStaticFormula, + } from "@budibase/types" import InfoDisplay from "@/pages/builder/app/[application]/design/[screenId]/[componentId]/_components/Component/InfoDisplay.svelte" import { getContext } from "svelte" import DetailPopover from "@/components/common/DetailPopover.svelte" @@ -94,10 +100,15 @@ if (fieldSchema.calculationType) { return false } + // static numeric formulas will work + if (isNumericStaticFormula(fieldSchema)) { + return true + } // Only allow numeric columns for most calculation types if ( self.type !== CalculationType.COUNT && - !isNumeric(fieldSchema.type) + !isNumeric(fieldSchema.type) && + fieldSchema.responseType !== FieldType.NUMBER ) { return false } diff --git a/packages/server/src/api/routes/tests/viewV2.spec.ts b/packages/server/src/api/routes/tests/viewV2.spec.ts index ad41aa618c..44605d7940 100644 --- a/packages/server/src/api/routes/tests/viewV2.spec.ts +++ b/packages/server/src/api/routes/tests/viewV2.spec.ts @@ -35,9 +35,13 @@ import { ViewV2, ViewV2Schema, ViewV2Type, + FormulaType, } from "@budibase/types" import { generator, mocks } from "@budibase/backend-core/tests" -import { datasourceDescribe } from "../../../integrations/tests/utils" +import { + datasourceDescribe, + DatabaseName, +} from "../../../integrations/tests/utils" import merge from "lodash/merge" import { quotas } from "@budibase/pro" import { context, db, events, roles, setEnv } from "@budibase/backend-core" @@ -3865,6 +3869,45 @@ if (descriptions.length) { expect(rows[0].count).toEqual(2) }) + isInternal && + it("should be able to max a static formula field", async () => { + const table = await config.api.table.save( + saveTableRequest({ + schema: { + string: { + type: FieldType.STRING, + name: "string", + }, + formula: { + type: FieldType.FORMULA, + name: "formula", + formulaType: FormulaType.STATIC, + responseType: FieldType.NUMBER, + formula: "{{ string }}", + }, + }, + }) + ) + await config.api.row.save(table._id!, { + string: "1", + }) + const view = await config.api.viewV2.create({ + tableId: table._id!, + name: generator.guid(), + type: ViewV2Type.CALCULATION, + schema: { + maxFormula: { + visible: true, + calculationType: CalculationType.MAX, + field: "formula", + }, + }, + }) + const { rows } = await config.api.row.search(view.id) + expect(rows.length).toEqual(1) + expect(rows[0].maxFormula).toEqual(1) + }) + it("should not be able to COUNT(DISTINCT ...) against a non-existent field", async () => { await config.api.viewV2.create( { diff --git a/packages/server/src/sdk/app/views/index.ts b/packages/server/src/sdk/app/views/index.ts index b3edc92e17..7f67e95e66 100644 --- a/packages/server/src/sdk/app/views/index.ts +++ b/packages/server/src/sdk/app/views/index.ts @@ -4,6 +4,7 @@ import { canGroupBy, FieldType, isNumeric, + isNumericStaticFormula, PermissionLevel, RelationSchemaField, RenameColumn, @@ -176,7 +177,11 @@ async function guardCalculationViewSchema( } const isCount = schema.calculationType === CalculationType.COUNT - if (!isCount && !isNumeric(targetSchema.type)) { + if ( + !isCount && + !isNumeric(targetSchema.type) && + !isNumericStaticFormula(targetSchema) + ) { throw new HTTPError( `Calculation field "${name}" references field "${schema.field}" which is not a numeric field`, 400 diff --git a/packages/types/src/documents/app/row.ts b/packages/types/src/documents/app/row.ts index 4c32e45a8c..e508c4e3c0 100644 --- a/packages/types/src/documents/app/row.ts +++ b/packages/types/src/documents/app/row.ts @@ -1,4 +1,5 @@ import { Document } from "../document" +import { FieldSchema, FormulaType } from "./table" export enum FieldType { /** @@ -147,6 +148,15 @@ export function isNumeric(type: FieldType) { return NumericTypes.includes(type) } +export function isNumericStaticFormula(schema: FieldSchema) { + return ( + schema.type === FieldType.FORMULA && + schema.formulaType === FormulaType.STATIC && + schema.responseType && + isNumeric(schema.responseType) + ) +} + export const GroupByTypes = [ FieldType.STRING, FieldType.LONGFORM, From 4de1d05a366c1776d4ecc84f498f949dd0563fd3 Mon Sep 17 00:00:00 2001 From: mike12345567 Date: Mon, 31 Mar 2025 18:03:55 +0100 Subject: [PATCH 36/69] Linting. --- packages/server/src/api/routes/tests/viewV2.spec.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/packages/server/src/api/routes/tests/viewV2.spec.ts b/packages/server/src/api/routes/tests/viewV2.spec.ts index 44605d7940..ddb3dd6a78 100644 --- a/packages/server/src/api/routes/tests/viewV2.spec.ts +++ b/packages/server/src/api/routes/tests/viewV2.spec.ts @@ -38,10 +38,7 @@ import { FormulaType, } from "@budibase/types" import { generator, mocks } from "@budibase/backend-core/tests" -import { - datasourceDescribe, - DatabaseName, -} from "../../../integrations/tests/utils" +import { datasourceDescribe } from "../../../integrations/tests/utils" import merge from "lodash/merge" import { quotas } from "@budibase/pro" import { context, db, events, roles, setEnv } from "@budibase/backend-core" From 5edeb37718cb7887ca62b1b3009e8e740f5f26dd Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Tue, 1 Apr 2025 09:04:03 +0100 Subject: [PATCH 37/69] Add CSS vars for grid positioning of boilerplate screen components on mobile --- packages/server/src/constants/screens.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/server/src/constants/screens.ts b/packages/server/src/constants/screens.ts index 41c1e74874..3a7413633d 100644 --- a/packages/server/src/constants/screens.ts +++ b/packages/server/src/constants/screens.ts @@ -365,7 +365,11 @@ export function createSampleDataTableScreen(): Screen { _component: "@budibase/standard-components/textv2", _styles: { normal: { + "--grid-desktop-col-start": 1, "--grid-desktop-col-end": 3, + "--grid-desktop-row-start": 1, + "--grid-desktop-row-end": 3, + "--grid-mobile-col-end": 7, }, hover: {}, active: {}, @@ -384,6 +388,7 @@ export function createSampleDataTableScreen(): Screen { "--grid-desktop-row-start": 1, "--grid-desktop-row-end": 3, "--grid-desktop-h-align": "end", + "--grid-mobile-col-start": 7, }, hover: {}, active: {}, From 35b9a4038bad5763a4f51084d8f89260e8e841b5 Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Tue, 1 Apr 2025 09:30:34 +0100 Subject: [PATCH 38/69] Expose refresh data provider action from single row provider and reset row ID setting when changing datasource --- packages/client/manifest.json | 4 +++- .../src/components/app/SingleRowProvider.svelte | 11 +++++++++-- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/packages/client/manifest.json b/packages/client/manifest.json index 3cd24ee0f8..9f30281831 100644 --- a/packages/client/manifest.json +++ b/packages/client/manifest.json @@ -8182,6 +8182,7 @@ "name": "Single Row Provider", "icon": "SQLQuery", "hasChildren": true, + "actions": ["RefreshDatasource"], "size": { "width": 500, "height": 200 @@ -8201,7 +8202,8 @@ "type": "text", "label": "Row ID", "key": "rowId", - "required": true + "required": true, + "resetOn": "datasource" } ], "context": { diff --git a/packages/client/src/components/app/SingleRowProvider.svelte b/packages/client/src/components/app/SingleRowProvider.svelte index 1905a4c772..b8a25a6005 100644 --- a/packages/client/src/components/app/SingleRowProvider.svelte +++ b/packages/client/src/components/app/SingleRowProvider.svelte @@ -6,13 +6,20 @@ export let rowId: string const component = getContext("component") - const { styleable, API, Provider } = getContext("sdk") + const { styleable, API, Provider, ActionTypes } = getContext("sdk") let row: Row | undefined $: datasourceId = datasource.type === "table" ? datasource.tableId : datasource.id $: fetchRow(datasourceId, rowId) + $: actions = [ + { + type: ActionTypes.RefreshDatasource, + callback: () => fetchRow(datasourceId, rowId), + metadata: { dataSource: datasource }, + }, + ] const fetchRow = async (datasourceId: string, rowId: string) => { try { @@ -24,7 +31,7 @@
- +
From 739c993588e1d510362d53a4bf7130991a9d6ca0 Mon Sep 17 00:00:00 2001 From: Sam Rose Date: Tue, 1 Apr 2025 11:01:32 +0100 Subject: [PATCH 39/69] Create new global DB for self-host quota. --- .../backend-core/src/cache/writethrough.ts | 25 +++ packages/backend-core/src/constants/db.ts | 5 + .../backend-core/src/context/mainContext.ts | 31 +++ packages/backend-core/src/context/types.ts | 1 + packages/pro | 2 +- .../server/src/api/routes/tests/ai.spec.ts | 186 ++++++++++-------- 6 files changed, 164 insertions(+), 86 deletions(-) diff --git a/packages/backend-core/src/cache/writethrough.ts b/packages/backend-core/src/cache/writethrough.ts index cd7409ca15..5a1d9f6a14 100644 --- a/packages/backend-core/src/cache/writethrough.ts +++ b/packages/backend-core/src/cache/writethrough.ts @@ -96,6 +96,24 @@ async function get(db: Database, id: string): Promise { return cacheItem.doc } +async function tryGet( + db: Database, + id: string +): Promise { + const cache = await getCache() + const cacheKey = makeCacheKey(db, id) + let cacheItem: CacheItem | null = await cache.get(cacheKey) + if (!cacheItem) { + const doc = await db.tryGet(id) + if (!doc) { + return null + } + cacheItem = makeCacheItem(doc) + await cache.store(cacheKey, cacheItem) + } + return cacheItem.doc +} + async function remove(db: Database, docOrId: any, rev?: any): Promise { const cache = await getCache() if (!docOrId) { @@ -123,10 +141,17 @@ export class Writethrough { return put(this.db, doc, writeRateMs) } + /** + * @deprecated use `tryGet` instead + */ async get(id: string) { return get(this.db, id) } + async tryGet(id: string) { + return tryGet(this.db, id) + } + async remove(docOrId: any, rev?: any) { return remove(this.db, docOrId, rev) } diff --git a/packages/backend-core/src/constants/db.ts b/packages/backend-core/src/constants/db.ts index 3085b91ef1..28d389e6ba 100644 --- a/packages/backend-core/src/constants/db.ts +++ b/packages/backend-core/src/constants/db.ts @@ -60,6 +60,11 @@ export const StaticDatabases = { SCIM_LOGS: { name: "scim-logs", }, + // Used by self-host users making use of Budicloud resources. Introduced when + // we started letting self-host users use Budibase AI in the cloud. + SELF_HOST_CLOUD: { + name: "self-host-cloud", + }, } export const APP_PREFIX = prefixed(DocumentType.APP) diff --git a/packages/backend-core/src/context/mainContext.ts b/packages/backend-core/src/context/mainContext.ts index 8e0c71ff18..ed0c56daaf 100644 --- a/packages/backend-core/src/context/mainContext.ts +++ b/packages/backend-core/src/context/mainContext.ts @@ -157,6 +157,27 @@ export async function doInTenant( return newContext(updates, task) } +export async function doInSelfHostTenantUsingCloud( + tenantId: string, + task: () => T +): Promise { + const updates = { tenantId, isSelfHostUsingCloud: true } + return newContext(updates, task) +} + +export function isSelfHostUsingCloud() { + const context = Context.get() + return !!context?.isSelfHostUsingCloud +} + +export function getSelfHostCloudDB() { + const context = Context.get() + if (!context || !context.isSelfHostUsingCloud) { + throw new Error("Self-host cloud DB not found") + } + return getDB(StaticDatabases.SELF_HOST_CLOUD.name) +} + export async function doInAppContext( appId: string, task: () => T @@ -325,6 +346,11 @@ export function getGlobalDB(): Database { if (!context || (env.MULTI_TENANCY && !context.tenantId)) { throw new Error("Global DB not found") } + if (context.isSelfHostUsingCloud) { + throw new Error( + "Global DB not found - self-host users using cloud don't have a global DB" + ) + } return getDB(baseGlobalDBName(context?.tenantId)) } @@ -344,6 +370,11 @@ export function getAppDB(opts?: any): Database { if (!appId) { throw new Error("Unable to retrieve app DB - no app ID.") } + if (isSelfHostUsingCloud()) { + throw new Error( + "App DB not found - self-host users using cloud don't have app DBs" + ) + } return getDB(appId, opts) } diff --git a/packages/backend-core/src/context/types.ts b/packages/backend-core/src/context/types.ts index 23598b951e..adee495e60 100644 --- a/packages/backend-core/src/context/types.ts +++ b/packages/backend-core/src/context/types.ts @@ -5,6 +5,7 @@ import { GoogleSpreadsheet } from "google-spreadsheet" // keep this out of Budibase types, don't want to expose context info export type ContextMap = { tenantId?: string + isSelfHostUsingCloud?: boolean appId?: string identity?: IdentityContext environmentVariables?: Record diff --git a/packages/pro b/packages/pro index 4417bceb24..0f46b458f3 160000 --- a/packages/pro +++ b/packages/pro @@ -1 +1 @@ -Subproject commit 4417bceb24eabdd9a8c1615fb83c4e6fe8c0c914 +Subproject commit 0f46b458f3fd1edd15fa7ff1d16eeb92caee27e1 diff --git a/packages/server/src/api/routes/tests/ai.spec.ts b/packages/server/src/api/routes/tests/ai.spec.ts index 288ab888fd..ad2ae7dc50 100644 --- a/packages/server/src/api/routes/tests/ai.spec.ts +++ b/packages/server/src/api/routes/tests/ai.spec.ts @@ -29,7 +29,6 @@ interface TestSetup { name: string setup: SetupFn mockLLMResponse: MockLLMResponseFn - selfHostOnly?: boolean } function budibaseAI(): SetupFn { @@ -80,7 +79,7 @@ function customAIConfig(providerConfig: Partial): SetupFn { } } -const providers: TestSetup[] = [ +const allProviders: TestSetup[] = [ { name: "OpenAI API key", setup: async () => { @@ -89,7 +88,6 @@ const providers: TestSetup[] = [ }) }, mockLLMResponse: mockChatGPTResponse, - selfHostOnly: true, }, { name: "OpenAI API key with custom config", @@ -126,9 +124,9 @@ describe("AI", () => { nock.cleanAll() }) - describe.each(providers)( + describe.each(allProviders)( "provider: $name", - ({ setup, mockLLMResponse, selfHostOnly }: TestSetup) => { + ({ setup, mockLLMResponse }: TestSetup) => { let cleanup: () => Promise | void beforeAll(async () => { cleanup = await setup(config) @@ -243,86 +241,104 @@ describe("AI", () => { ) }) }) - - !selfHostOnly && - describe("POST /api/ai/chat", () => { - let envCleanup: () => void - let featureCleanup: () => void - beforeAll(() => { - envCleanup = setEnv({ SELF_HOSTED: false }) - featureCleanup = features.testutils.setFeatureFlags("*", { - AI_JS_GENERATION: true, - }) - }) - - afterAll(() => { - featureCleanup() - envCleanup() - }) - - beforeEach(() => { - const license: License = { - plan: { - type: PlanType.FREE, - model: PlanModel.PER_USER, - usesInvoicing: false, - }, - features: [], - quotas: {} as any, - tenantId: config.tenantId, - } - nock(env.ACCOUNT_PORTAL_URL).get("/api/license").reply(200, license) - }) - - it("handles correct chat response", async () => { - mockLLMResponse("Hi there!") - const { message } = await config.api.ai.chat({ - messages: [{ role: "user", content: "Hello!" }], - licenseKey: "test-key", - }) - expect(message).toBe("Hi there!") - }) - - it("handles chat response error", async () => { - mockLLMResponse(() => { - throw new Error("LLM error") - }) - await config.api.ai.chat( - { - messages: [{ role: "user", content: "Hello!" }], - licenseKey: "test-key", - }, - { status: 500 } - ) - }) - - it("handles no license", async () => { - nock.cleanAll() - nock(env.ACCOUNT_PORTAL_URL).get("/api/license").reply(404) - await config.api.ai.chat( - { - messages: [{ role: "user", content: "Hello!" }], - licenseKey: "test-key", - }, - { - status: 403, - } - ) - }) - - it("handles no license key", async () => { - await config.api.ai.chat( - { - messages: [{ role: "user", content: "Hello!" }], - // @ts-expect-error - intentionally wrong - licenseKey: undefined, - }, - { - status: 403, - } - ) - }) - }) } ) }) + +describe("BudibaseAI", () => { + const config = new TestConfiguration() + let cleanup: () => void | Promise + beforeAll(async () => { + await config.init() + cleanup = await budibaseAI()(config) + }) + + afterAll(async () => { + if ("then" in cleanup) { + await cleanup() + } else { + cleanup() + } + config.end() + }) + + describe("POST /api/ai/chat", () => { + let envCleanup: () => void + let featureCleanup: () => void + beforeAll(() => { + envCleanup = setEnv({ SELF_HOSTED: false }) + featureCleanup = features.testutils.setFeatureFlags("*", { + AI_JS_GENERATION: true, + }) + }) + + afterAll(() => { + featureCleanup() + envCleanup() + }) + + beforeEach(() => { + nock.cleanAll() + const license: License = { + plan: { + type: PlanType.FREE, + model: PlanModel.PER_USER, + usesInvoicing: false, + }, + features: [], + quotas: {} as any, + tenantId: config.tenantId, + } + nock(env.ACCOUNT_PORTAL_URL).get("/api/license").reply(200, license) + }) + + it("handles correct chat response", async () => { + mockChatGPTResponse("Hi there!") + const { message } = await config.api.ai.chat({ + messages: [{ role: "user", content: "Hello!" }], + licenseKey: "test-key", + }) + expect(message).toBe("Hi there!") + }) + + it("handles chat response error", async () => { + mockChatGPTResponse(() => { + throw new Error("LLM error") + }) + await config.api.ai.chat( + { + messages: [{ role: "user", content: "Hello!" }], + licenseKey: "test-key", + }, + { status: 500 } + ) + }) + + it("handles no license", async () => { + nock.cleanAll() + nock(env.ACCOUNT_PORTAL_URL).get("/api/license").reply(404) + await config.api.ai.chat( + { + messages: [{ role: "user", content: "Hello!" }], + licenseKey: "test-key", + }, + { + status: 403, + } + ) + }) + + it("handles no license key", async () => { + await config.api.ai.chat( + { + messages: [{ role: "user", content: "Hello!" }], + // @ts-expect-error - intentionally wrong + licenseKey: undefined, + }, + { + status: 403, + } + ) + }) + }) +}) From 3896a60428c01e26d2003136c259f9af16273291 Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Tue, 1 Apr 2025 11:04:23 +0100 Subject: [PATCH 40/69] Reset filter and sort settings when datasource changes --- packages/client/manifest.json | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/client/manifest.json b/packages/client/manifest.json index 0afdbbc6de..de13a15fd5 100644 --- a/packages/client/manifest.json +++ b/packages/client/manifest.json @@ -8138,6 +8138,7 @@ "type": "filter", "label": "Filtering", "key": "filter", + "resetOn": "datasource", "dependsOn": { "setting": "datasource.type", "value": "custom", @@ -8149,6 +8150,7 @@ "label": "Sort column", "key": "sortColumn", "placeholder": "Default", + "resetOn": "datasource", "dependsOn": { "setting": "datasource.type", "value": "custom", @@ -8159,6 +8161,7 @@ "type": "select", "label": "Sort order", "key": "sortOrder", + "resetOn": "datasource", "options": ["Ascending", "Descending"], "defaultValue": "Ascending", "dependsOn": "sortColumn" From 400db88e2d0c41e93f46d5744f0491a8880274c0 Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Tue, 1 Apr 2025 11:07:04 +0100 Subject: [PATCH 41/69] Don't use static white background color as we allow this component to be used anywhere --- packages/client/src/components/app/pdf/PDFTable.svelte | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/client/src/components/app/pdf/PDFTable.svelte b/packages/client/src/components/app/pdf/PDFTable.svelte index c5d92aa59a..e4d415fa01 100644 --- a/packages/client/src/components/app/pdf/PDFTable.svelte +++ b/packages/client/src/components/app/pdf/PDFTable.svelte @@ -121,7 +121,7 @@ grid-template-columns: repeat(var(--cols), minmax(40px, auto)); grid-template-rows: repeat(var(--rows), max-content); overflow: hidden; - background: white; + background: var(--spectrum-global-color-gray-50); } .table.valid { border-left: 1px solid var(--border-color); From 853c00242f08db2d44232fca31f2eba3a1590f4f Mon Sep 17 00:00:00 2001 From: Sam Rose Date: Tue, 1 Apr 2025 11:28:13 +0100 Subject: [PATCH 42/69] Respond to PR comments. --- packages/backend-core/src/context/mainContext.ts | 6 ++++++ packages/pro | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/backend-core/src/context/mainContext.ts b/packages/backend-core/src/context/mainContext.ts index ed0c56daaf..e701f111aa 100644 --- a/packages/backend-core/src/context/mainContext.ts +++ b/packages/backend-core/src/context/mainContext.ts @@ -157,6 +157,12 @@ export async function doInTenant( return newContext(updates, task) } +// We allow self-host licensed users to make use of some Budicloud services +// (e.g. Budibase AI). When they do this, they use their license key as an API +// key. We use that license key to identify the tenant ID, and we set the +// context to be self-host using cloud. This affects things like where their +// quota documents get stored (because we want to avoid creating a new global +// DB for each self-host tenant). export async function doInSelfHostTenantUsingCloud( tenantId: string, task: () => T diff --git a/packages/pro b/packages/pro index 0f46b458f3..8eb981cf01 160000 --- a/packages/pro +++ b/packages/pro @@ -1 +1 @@ -Subproject commit 0f46b458f3fd1edd15fa7ff1d16eeb92caee27e1 +Subproject commit 8eb981cf01151261697a8f26c08c4c28f66b8e15 From 7e068cd2521d3315e132c72c3c74b503e65dd1a5 Mon Sep 17 00:00:00 2001 From: mike12345567 Date: Tue, 1 Apr 2025 11:35:21 +0100 Subject: [PATCH 43/69] PR comments. --- packages/server/src/api/routes/tests/viewV2.spec.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/server/src/api/routes/tests/viewV2.spec.ts b/packages/server/src/api/routes/tests/viewV2.spec.ts index 348187399b..261cd097d9 100644 --- a/packages/server/src/api/routes/tests/viewV2.spec.ts +++ b/packages/server/src/api/routes/tests/viewV2.spec.ts @@ -3888,6 +3888,9 @@ if (descriptions.length) { await config.api.row.save(table._id!, { string: "1", }) + await config.api.row.save(table._id!, { + string: "2", + }) const view = await config.api.viewV2.create({ tableId: table._id!, name: generator.guid(), @@ -3902,7 +3905,7 @@ if (descriptions.length) { }) const { rows } = await config.api.row.search(view.id) expect(rows.length).toEqual(1) - expect(rows[0].maxFormula).toEqual(1) + expect(rows[0].maxFormula).toEqual(2) }) it("should not be able to COUNT(DISTINCT ...) against a non-existent field", async () => { From cf5ffbe4c5bc3d4a6c5dc8486a277c36001c5c16 Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Tue, 1 Apr 2025 13:01:10 +0100 Subject: [PATCH 44/69] Rename PDF Editor to PDF --- .../[application]/design/_components/NewScreen/index.svelte | 2 +- packages/builder/src/templates/screenTemplating/Screen.js | 4 ++-- packages/builder/src/templates/screenTemplating/pdf.js | 6 +----- 3 files changed, 4 insertions(+), 8 deletions(-) diff --git a/packages/builder/src/pages/builder/app/[application]/design/_components/NewScreen/index.svelte b/packages/builder/src/pages/builder/app/[application]/design/_components/NewScreen/index.svelte index 1797af4be3..d50d284965 100644 --- a/packages/builder/src/pages/builder/app/[application]/design/_components/NewScreen/index.svelte +++ b/packages/builder/src/pages/builder/app/[application]/design/_components/NewScreen/index.svelte @@ -63,7 +63,7 @@ A form containing data
- PDF Editor + PDF Create, edit and export your PDF
diff --git a/packages/builder/src/templates/screenTemplating/Screen.js b/packages/builder/src/templates/screenTemplating/Screen.js index a9803dc5a4..4eb0e7a098 100644 --- a/packages/builder/src/templates/screenTemplating/Screen.js +++ b/packages/builder/src/templates/screenTemplating/Screen.js @@ -99,8 +99,8 @@ export class PDFScreen extends Screen { selected: {}, }, _children: [], - _instanceName: "", - title: "PDF Editor", + _instanceName: "PDF", + title: "PDF", } } } diff --git a/packages/builder/src/templates/screenTemplating/pdf.js b/packages/builder/src/templates/screenTemplating/pdf.js index b5cde8f536..5c10e5cec2 100644 --- a/packages/builder/src/templates/screenTemplating/pdf.js +++ b/packages/builder/src/templates/screenTemplating/pdf.js @@ -6,11 +6,7 @@ import { Roles } from "@/constants/backend" const pdf = ({ route, screens }) => { const validRoute = getValidRoute(screens, route, Roles.BASIC) - const template = new PDFScreen() - .instanceName("PDF Editor") - .role(Roles.BASIC) - .route(validRoute) - .json() + const template = new PDFScreen().role(Roles.BASIC).route(validRoute).json() return [ { From 80981982063ce435d23503b44f47da7e686e289b Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Tue, 1 Apr 2025 13:01:56 +0100 Subject: [PATCH 45/69] Remove width setting for PDF screens --- .../_components/Screen/GeneralPanel.svelte | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/[componentId]/_components/Screen/GeneralPanel.svelte b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/[componentId]/_components/Screen/GeneralPanel.svelte index ea7e548bbd..1da03377b5 100644 --- a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/[componentId]/_components/Screen/GeneralPanel.svelte +++ b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/[componentId]/_components/Screen/GeneralPanel.svelte @@ -45,6 +45,16 @@ break default: screenComponentSettings = [ + { + key: "width", + label: "Width", + control: Select, + props: { + options: ["Extra small", "Small", "Medium", "Large", "Max"], + placeholder: "Default", + disabled: !!screen.layoutId, + }, + }, { key: "props.layout", label: "Layout", @@ -109,16 +119,6 @@ label: "On screen load", control: ButtonActionEditor, }, - { - key: "width", - label: "Width", - control: Select, - props: { - options: ["Extra small", "Small", "Medium", "Large", "Max"], - placeholder: "Default", - disabled: !!screen.layoutId, - }, - }, ...screenComponentSettings, { key: "urlTest", From 189a8ee6943ccbafd654de13d3557cf76aee86c7 Mon Sep 17 00:00:00 2001 From: Dean Date: Tue, 1 Apr 2025 15:40:01 +0100 Subject: [PATCH 46/69] Readded the DrawerBindableSlot to the RowSelector as it was still needed several column types. Some UI changes to accomodate also --- .../automation/SetupPanel/RowSelector.svelte | 71 +++++++++++++++---- .../common/bindings/DrawerBindableSlot.svelte | 27 ++++--- 2 files changed, 75 insertions(+), 23 deletions(-) diff --git a/packages/builder/src/components/automation/SetupPanel/RowSelector.svelte b/packages/builder/src/components/automation/SetupPanel/RowSelector.svelte index 73b2aba18d..86e04a4f48 100644 --- a/packages/builder/src/components/automation/SetupPanel/RowSelector.svelte +++ b/packages/builder/src/components/automation/SetupPanel/RowSelector.svelte @@ -11,6 +11,10 @@ import { FieldType } from "@budibase/types" import RowSelectorTypes from "./RowSelectorTypes.svelte" + import { + DrawerBindableSlot, + ServerBindingPanel as AutomationBindingPanel, + } from "@/components/common/bindings" import { FIELDS } from "@/constants/backend" import { capitalise } from "@/helpers" import { memo } from "@budibase/frontend-core" @@ -234,6 +238,14 @@ ) dispatch("change", result) } + + /** + * Converts arrays into strings. The CodeEditor expects a string or encoded JS + * @param{object} fieldValue + */ + const drawerValue = fieldValue => { + return Array.isArray(fieldValue) ? fieldValue.join(",") : fieldValue + } {#each schemaFields || [] as [field, schema]} @@ -243,20 +255,55 @@ fullWidth={fullWidth || isFullWidth(schema.type)} {componentWidth} > -
- + +
+ {:else} + + onChange({ + row: { + [field]: e.detail, + }, + })} + {bindings} + allowJS={true} + updateOnChange={false} {context} - /> -
+ > +
+ +
+ + {/if} {/if} {/each} diff --git a/packages/builder/src/components/common/bindings/DrawerBindableSlot.svelte b/packages/builder/src/components/common/bindings/DrawerBindableSlot.svelte index 0982fd6329..f3009f05ee 100644 --- a/packages/builder/src/components/common/bindings/DrawerBindableSlot.svelte +++ b/packages/builder/src/components/common/bindings/DrawerBindableSlot.svelte @@ -150,7 +150,7 @@
- {#if !isValid(value) && !$$slots.default} + {#if !isValid(value)}
{ if (!isJS) { dispatch("change", "") @@ -212,22 +212,27 @@ } .slot-icon { - right: 31px; + right: 31px !important; border-right: 1px solid var(--spectrum-alias-border-color); - border-top-right-radius: 0px; - border-bottom-right-radius: 0px; + border-top-right-radius: 0px !important; + border-bottom-right-radius: 0px !important; } - .text-area-slot-icon { - border-bottom: 1px solid var(--spectrum-alias-border-color); - border-bottom-right-radius: 0px; - top: 1px; + .icon.close { + right: 1px !important; + border-right: none; + border-top-right-radius: 4px !important; + border-bottom-right-radius: 4px !important; } + + .text-area-slot-icon, .json-slot-icon { + right: 1px !important; border-bottom: 1px solid var(--spectrum-alias-border-color); - border-bottom-right-radius: 0px; + border-top-right-radius: 4px !important; + border-bottom-right-radius: 0px !important; + border-bottom-left-radius: 4px !important; top: 1px; - right: 0px; } .icon { From 585a85246d2abd068aa808a061fab50c88789e0a Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Tue, 1 Apr 2025 16:16:32 +0100 Subject: [PATCH 47/69] Update how the base client app spectrum theme class names are applied to fix issue with custom themes in PDFs --- .../[componentId]/_components/Screen/ThemePanel.svelte | 4 +++- packages/client/src/components/ClientApp.svelte | 9 ++++++++- packages/client/src/components/app/pdf/PDF.svelte | 6 +----- 3 files changed, 12 insertions(+), 7 deletions(-) diff --git a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/[componentId]/_components/Screen/ThemePanel.svelte b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/[componentId]/_components/Screen/ThemePanel.svelte index af693a872f..c1e68010ad 100644 --- a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/[componentId]/_components/Screen/ThemePanel.svelte +++ b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/[componentId]/_components/Screen/ThemePanel.svelte @@ -26,7 +26,9 @@
- These settings apply to all screens + + These settings apply to all screens. PDFs are always light theme. +
diff --git a/packages/client/src/components/ClientApp.svelte b/packages/client/src/components/ClientApp.svelte index d10ec2c997..d5bcde111d 100644 --- a/packages/client/src/components/ClientApp.svelte +++ b/packages/client/src/components/ClientApp.svelte @@ -46,6 +46,7 @@ import SnippetsProvider from "./context/SnippetsProvider.svelte" import EmbedProvider from "./context/EmbedProvider.svelte" import DNDSelectionIndicators from "./preview/DNDSelectionIndicators.svelte" + import { ScreenVariant } from "@budibase/types" // Provide contexts setContext("sdk", SDK) @@ -56,6 +57,12 @@ let permissionError = false let embedNoScreens = false + // Get theme class names, which is always lightest for LDFs + $: isPDFScreen = $screenStore.activeScreen?.variant === ScreenVariant.PDF + $: themeClassNames = isPDFScreen + ? "spectrum--light" + : getThemeClassNames($themeStore.theme) + // Determine if we should show devtools or not $: showDevTools = $devToolsEnabled && !$routeStore.queryParams?.peek @@ -157,7 +164,7 @@ id="spectrum-root" lang="en" dir="ltr" - class="spectrum spectrum--medium {getThemeClassNames($themeStore.theme)}" + class="spectrum spectrum--medium {themeClassNames}" class:builder={$builderStore.inBuilder} class:show={fontsLoaded && dataLoaded} > diff --git a/packages/client/src/components/app/pdf/PDF.svelte b/packages/client/src/components/app/pdf/PDF.svelte index c7f43e824b..75751ae93f 100644 --- a/packages/client/src/components/app/pdf/PDF.svelte +++ b/packages/client/src/components/app/pdf/PDF.svelte @@ -90,11 +90,7 @@ /> {/each} {/if} -
+
Date: Tue, 1 Apr 2025 16:39:18 +0100 Subject: [PATCH 48/69] Fix crash when a component is missing a definition --- .../[componentId]/new/_components/NewComponentPanel.svelte | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/[componentId]/new/_components/NewComponentPanel.svelte b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/[componentId]/new/_components/NewComponentPanel.svelte index 1654ff5e06..9603caad7b 100644 --- a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/[componentId]/new/_components/NewComponentPanel.svelte +++ b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/[componentId]/new/_components/NewComponentPanel.svelte @@ -58,7 +58,7 @@ // Get initial set of allowed components let allowedComponents = [] const definition = componentStore.getDefinition(component?._component) - if (definition.legalDirectChildren?.length) { + if (definition?.legalDirectChildren?.length) { allowedComponents = definition.legalDirectChildren.map(x => { return `@budibase/standard-components/${x}` }) @@ -67,7 +67,7 @@ } // Build up list of illegal children from ancestors - let illegalChildren = definition.illegalChildren || [] + let illegalChildren = definition?.illegalChildren || [] path.forEach(ancestor => { // Sidepanels and modals can be nested anywhere in the component tree, but really they are always rendered at the top level. // Because of this, it doesn't make sense to carry over any parent illegal children to them, so the array is reset here. From 916c480d0ec851935b812565928febc028259b71 Mon Sep 17 00:00:00 2001 From: Dean Date: Tue, 1 Apr 2025 17:12:31 +0100 Subject: [PATCH 49/69] Ensure the CodeEditorField always displays itself when inside a bindable slot --- .../components/automation/SetupPanel/ExecuteScriptV2.svelte | 6 ++++-- .../common/bindings/DrawerBindableCodeEditorField.svelte | 3 +++ .../components/common/bindings/DrawerBindableSlot.svelte | 3 ++- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/packages/builder/src/components/automation/SetupPanel/ExecuteScriptV2.svelte b/packages/builder/src/components/automation/SetupPanel/ExecuteScriptV2.svelte index a5ba264f60..97fffa9922 100644 --- a/packages/builder/src/components/automation/SetupPanel/ExecuteScriptV2.svelte +++ b/packages/builder/src/components/automation/SetupPanel/ExecuteScriptV2.svelte @@ -7,6 +7,7 @@ import { type EnrichedBinding, FieldType } from "@budibase/types" import CodeEditorField from "@/components/common/bindings/CodeEditorField.svelte" import { DropdownPosition } from "@/components/common/CodeEditor/CodeEditor.svelte" + import DrawerBindableCodeEditorField from "@/components/common/bindings/DrawerBindableCodeEditorField.svelte" export let value: string export let context: Record | undefined = undefined @@ -27,6 +28,7 @@ allowHBS={false} updateOnChange={false} {context} + showComponent >
@@ -32,6 +33,7 @@ value = e.detail dispatch("change", value) }} + showComponent >
- {#if !isValid(value)} + {#if !isValid(value) && !showComponent} Date: Tue, 1 Apr 2025 17:20:46 +0100 Subject: [PATCH 50/69] Lint --- .../src/components/automation/SetupPanel/ExecuteScriptV2.svelte | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/builder/src/components/automation/SetupPanel/ExecuteScriptV2.svelte b/packages/builder/src/components/automation/SetupPanel/ExecuteScriptV2.svelte index 97fffa9922..c0cdba458e 100644 --- a/packages/builder/src/components/automation/SetupPanel/ExecuteScriptV2.svelte +++ b/packages/builder/src/components/automation/SetupPanel/ExecuteScriptV2.svelte @@ -7,7 +7,6 @@ import { type EnrichedBinding, FieldType } from "@budibase/types" import CodeEditorField from "@/components/common/bindings/CodeEditorField.svelte" import { DropdownPosition } from "@/components/common/CodeEditor/CodeEditor.svelte" - import DrawerBindableCodeEditorField from "@/components/common/bindings/DrawerBindableCodeEditorField.svelte" export let value: string export let context: Record | undefined = undefined From 651ac10de31ddbceddf8e51ce6f1b29d13b50544 Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Tue, 1 Apr 2025 20:06:44 +0100 Subject: [PATCH 51/69] Preprocess certain CSS values before PDF rendering to fix html2canvas limitations --- .../client/src/components/ClientApp.svelte | 8 +--- .../src/components/CustomThemeWrapper.svelte | 12 ++++-- .../client/src/components/app/pdf/PDF.svelte | 43 +++++++++++++------ packages/client/src/utils/grid.ts | 3 ++ 4 files changed, 43 insertions(+), 23 deletions(-) diff --git a/packages/client/src/components/ClientApp.svelte b/packages/client/src/components/ClientApp.svelte index d5bcde111d..15fd2ad600 100644 --- a/packages/client/src/components/ClientApp.svelte +++ b/packages/client/src/components/ClientApp.svelte @@ -57,12 +57,6 @@ let permissionError = false let embedNoScreens = false - // Get theme class names, which is always lightest for LDFs - $: isPDFScreen = $screenStore.activeScreen?.variant === ScreenVariant.PDF - $: themeClassNames = isPDFScreen - ? "spectrum--light" - : getThemeClassNames($themeStore.theme) - // Determine if we should show devtools or not $: showDevTools = $devToolsEnabled && !$routeStore.queryParams?.peek @@ -164,7 +158,7 @@ id="spectrum-root" lang="en" dir="ltr" - class="spectrum spectrum--medium {themeClassNames}" + class="spectrum spectrum--medium {getThemeClassNames($themeStore.theme)}" class:builder={$builderStore.inBuilder} class:show={fontsLoaded && dataLoaded} > diff --git a/packages/client/src/components/CustomThemeWrapper.svelte b/packages/client/src/components/CustomThemeWrapper.svelte index 61f3b577a8..75ab1dfaf1 100644 --- a/packages/client/src/components/CustomThemeWrapper.svelte +++ b/packages/client/src/components/CustomThemeWrapper.svelte @@ -1,12 +1,18 @@ -
+
diff --git a/packages/client/src/components/app/pdf/PDF.svelte b/packages/client/src/components/app/pdf/PDF.svelte index 75751ae93f..071b399466 100644 --- a/packages/client/src/components/app/pdf/PDF.svelte +++ b/packages/client/src/components/app/pdf/PDF.svelte @@ -3,6 +3,7 @@ import { Heading, Button } from "@budibase/bbui" import { htmlToPdf, pxToPt, A4HeightPx, type PDFOptions } from "./pdf" import { GridRowHeight } from "@/constants" + import CustomThemeWrapper from "@/components/CustomThemeWrapper.svelte" const component = getContext("component") const { styleable, Block, BlockComponent } = getContext("sdk") @@ -30,6 +31,7 @@ const generatePDF = async () => { rendering = true await tick() + preprocessCSS() try { const opts: PDFOptions = { fileName: safeName, @@ -43,6 +45,16 @@ rendering = false } + const preprocessCSS = () => { + const els = document.getElementsByClassName( + "grid-child" + ) as unknown as HTMLElement[] + for (let el of els) { + const styles = window.getComputedStyle(el) + el.style.setProperty("grid-column-end", styles.gridColumnEnd, "important") + } + } + const getDividerStyle = (idx: number) => { const top = (idx + 1) * innerPageHeightPx + doubleMarginPx / 2 return `--idx:"${idx + 1}"; --top:${top}px;` @@ -90,19 +102,24 @@ /> {/each} {/if} -
- - - +
+ + + + +
diff --git a/packages/client/src/utils/grid.ts b/packages/client/src/utils/grid.ts index 3412e734c6..08520ff781 100644 --- a/packages/client/src/utils/grid.ts +++ b/packages/client/src/utils/grid.ts @@ -116,6 +116,9 @@ export const gridLayout = (node: HTMLDivElement, metadata: GridMetadata) => { return } + // Add a unique class to elements we mutate so we can easily find them later + node.classList.add("grid-child") + // Callback to select the component when clicking on the wrapper selectComponent = (e: Event) => { e.stopPropagation() From e836f08c53d614cfad8c7d5763db2488f328ca59 Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Tue, 1 Apr 2025 20:08:42 +0100 Subject: [PATCH 52/69] Swap PDF to light theme instead of lightest --- packages/client/src/components/app/pdf/PDF.svelte | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/client/src/components/app/pdf/PDF.svelte b/packages/client/src/components/app/pdf/PDF.svelte index 071b399466..5a2d4fab6d 100644 --- a/packages/client/src/components/app/pdf/PDF.svelte +++ b/packages/client/src/components/app/pdf/PDF.svelte @@ -103,7 +103,7 @@ {/each} {/if}
@@ -170,6 +170,7 @@ flex-direction: column; justify-content: flex-start; align-items: stretch; + background: white; } .divider { width: 100%; From 51ba8510a8b9e21873ab843ea3bc401b730300c8 Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Tue, 1 Apr 2025 20:11:27 +0100 Subject: [PATCH 53/69] Lint --- packages/client/src/components/ClientApp.svelte | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/client/src/components/ClientApp.svelte b/packages/client/src/components/ClientApp.svelte index 15fd2ad600..d10ec2c997 100644 --- a/packages/client/src/components/ClientApp.svelte +++ b/packages/client/src/components/ClientApp.svelte @@ -46,7 +46,6 @@ import SnippetsProvider from "./context/SnippetsProvider.svelte" import EmbedProvider from "./context/EmbedProvider.svelte" import DNDSelectionIndicators from "./preview/DNDSelectionIndicators.svelte" - import { ScreenVariant } from "@budibase/types" // Provide contexts setContext("sdk", SDK) From 66bff41b3191c33796da12e567a93a9202face15 Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Tue, 1 Apr 2025 20:19:11 +0100 Subject: [PATCH 54/69] Improve CSS preprocessing --- .../client/src/components/app/pdf/PDF.svelte | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/packages/client/src/components/app/pdf/PDF.svelte b/packages/client/src/components/app/pdf/PDF.svelte index 5a2d4fab6d..bfdb893106 100644 --- a/packages/client/src/components/app/pdf/PDF.svelte +++ b/packages/client/src/components/app/pdf/PDF.svelte @@ -46,10 +46,13 @@ } const preprocessCSS = () => { - const els = document.getElementsByClassName( - "grid-child" - ) as unknown as HTMLElement[] + const els = document.getElementsByClassName("grid-child") for (let el of els) { + if (!(el instanceof HTMLElement)) { + return + } + // Get the computed values and assign them back to the style, simplifying + // the CSS that gets handled by HTML2PDF const styles = window.getComputedStyle(el) el.style.setProperty("grid-column-end", styles.gridColumnEnd, "important") } @@ -185,12 +188,4 @@ top: calc(var(--top) + var(--margin)); background: transparent; } - /*.divider::after {*/ - /* position: absolute;*/ - /* top: -32px;*/ - /* right: 24px;*/ - /* content: var(--idx);*/ - /* color: var(--spectrum-global-color-static-gray-400);*/ - /* text-align: right;*/ - /*}*/ From 7074a4087abf4fed99c463ad76c23d75ffff5333 Mon Sep 17 00:00:00 2001 From: Dean Date: Wed, 2 Apr 2025 09:56:01 +0100 Subject: [PATCH 55/69] Add bigint to the row types handling their own binding drawer UX --- .../src/components/automation/SetupPanel/RowSelector.svelte | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/builder/src/components/automation/SetupPanel/RowSelector.svelte b/packages/builder/src/components/automation/SetupPanel/RowSelector.svelte index 86e04a4f48..77df862ac9 100644 --- a/packages/builder/src/components/automation/SetupPanel/RowSelector.svelte +++ b/packages/builder/src/components/automation/SetupPanel/RowSelector.svelte @@ -246,6 +246,9 @@ const drawerValue = fieldValue => { return Array.isArray(fieldValue) ? fieldValue.join(",") : fieldValue } + + // The element controls their own binding drawer + const customDrawer = ["string", "number", "barcodeqr", "bigint"] {#each schemaFields || [] as [field, schema]} @@ -255,7 +258,7 @@ fullWidth={fullWidth || isFullWidth(schema.type)} {componentWidth} > - {#if ["string", "number", "barcodeqr"].includes(schema.type)} + {#if customDrawer.includes(schema.type)}
Date: Wed, 2 Apr 2025 10:31:27 +0100 Subject: [PATCH 56/69] Add license feature for PDF and update UI to account for it --- .../design/_components/NewScreen/index.svelte | 67 ++++++++++++------- .../builder/src/stores/portal/licensing.ts | 4 ++ packages/types/src/sdk/licensing/feature.ts | 1 + 3 files changed, 49 insertions(+), 23 deletions(-) diff --git a/packages/builder/src/pages/builder/app/[application]/design/_components/NewScreen/index.svelte b/packages/builder/src/pages/builder/app/[application]/design/_components/NewScreen/index.svelte index d50d284965..416299e00d 100644 --- a/packages/builder/src/pages/builder/app/[application]/design/_components/NewScreen/index.svelte +++ b/packages/builder/src/pages/builder/app/[application]/design/_components/NewScreen/index.svelte @@ -1,5 +1,5 @@ From ab921ec6084c482cd54e9312155dfb3498216a9d Mon Sep 17 00:00:00 2001 From: Budibase Staging Release Bot <> Date: Thu, 3 Apr 2025 15:03:02 +0000 Subject: [PATCH 66/69] Bump version to 3.8.4 --- lerna.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lerna.json b/lerna.json index fd1ce425cf..34e020d48f 100644 --- a/lerna.json +++ b/lerna.json @@ -1,6 +1,6 @@ { "$schema": "node_modules/lerna/schemas/lerna-schema.json", - "version": "3.8.3", + "version": "3.8.4", "npmClient": "yarn", "concurrency": 20, "command": { From 47f24d42ddd361f3fe9960fb5dc7329c9265959f Mon Sep 17 00:00:00 2001 From: Dean Date: Fri, 4 Apr 2025 09:31:40 +0100 Subject: [PATCH 67/69] External data connector picker readded and the layout adjusted. --- .../SetupPanel/AutomationSchemaLayout.svelte | 6 +++- .../SetupPanel/QueryParamSelector.svelte | 31 +++++++++---------- packages/builder/src/types/automations.ts | 1 + 3 files changed, 20 insertions(+), 18 deletions(-) diff --git a/packages/builder/src/components/automation/SetupPanel/AutomationSchemaLayout.svelte b/packages/builder/src/components/automation/SetupPanel/AutomationSchemaLayout.svelte index 6011e2753d..762a416cf6 100644 --- a/packages/builder/src/components/automation/SetupPanel/AutomationSchemaLayout.svelte +++ b/packages/builder/src/components/automation/SetupPanel/AutomationSchemaLayout.svelte @@ -184,6 +184,8 @@ }, [SchemaFieldTypes.QUERY_PARAMS]: { comp: QueryParamSelector, + fullWidth: true, + title: "Query*", }, [SchemaFieldTypes.CODE]: { comp: ExecuteScript, @@ -281,7 +283,9 @@ } const type = getFieldType(field, block) const config = type ? SchemaTypes[type] : null - const title = getFieldLabel(key, field, requiredProperties?.includes(key)) + const title = + config?.title || + getFieldLabel(key, field, requiredProperties?.includes(key)) const value = getInputValue(inputData, key) const meta = getInputValue(inputData, "meta") diff --git a/packages/builder/src/components/automation/SetupPanel/QueryParamSelector.svelte b/packages/builder/src/components/automation/SetupPanel/QueryParamSelector.svelte index eb97f9b02f..f05d3941bd 100644 --- a/packages/builder/src/components/automation/SetupPanel/QueryParamSelector.svelte +++ b/packages/builder/src/components/automation/SetupPanel/QueryParamSelector.svelte @@ -1,9 +1,10 @@
-