@bigbinary/neeto-pdf-frontend
v1.0.20
Published
A repo acts as the source of truth for the new nano's structure, configs, data etc.
Downloads
3,284
Readme
neeto-pdf-nano
Add reusable PDF templates, field mapping, preview, download, and email attachments to a Rails and React application.
neeto-pdf-engine: storage, APIs, and server-side rendering.@bigbinary/neeto-pdf-frontend: documents, editor, preview, and email attachment UI.
The host supplies an owner for reusable templates and a data record whose values fill each PDF. It also owns authorization, routes, field normalization, email settings, and value formatting.
Required host stack: Rails, Active Record, Active Storage, PostgreSQL,
NeetoCommonsBackend::Api::BaseController, React Query, Axios, i18next,
neetoUI, neetoMolecules, Formik, and Tailwind CSS.
Backend setup
1. Install the engine
source "NEETO_GEM_SERVER_URL" do
gem "neeto-pdf-engine"
endbundle install2. Copy the document migration
db/migrate/20260706120000_create_neeto_pdf_engine_documents.rbGive it a free host timestamp, then run:
bundle exec rails db:migrateDo not run the general engine migration installer; this repository contains
unrelated template migrations. owner_id is a UUID. Adapt and test the copied
migration first if the owner uses another primary-key type.
3. Configure the owner and render context
Create config/initializers/neeto_pdf_engine.rb:
# frozen_string_literal: true
NeetoPdfEngine.owner_class = "DocumentOwner"
NeetoPdfEngine.render_context_resolver = lambda do |owner:, record_id:|
PdfDocuments::RenderContextResolver.call(owner:, record_id:)
endReplace DocumentOwner with the real model. It must be the current organization
or support where(organization: current_organization). Add:
class DocumentOwner < ApplicationRecord
belongs_to :organization
has_many :neeto_pdf_documents,
as: :owner,
class_name: "NeetoPdfEngine::Document",
dependent: :destroy
endImplement the render context:
module PdfDocuments
class RenderContextResolver
def self.call(owner:, record_id:)
record = owner.data_records.find(record_id) # Use the real association.
{
value_resolver: ValueResolver.new(owner:, record:),
filename_context: { id: record.id }
}
end
end
endAlways find the data record through the owner. The value resolver must respond
to call(mapping):
module PdfDocuments
class ValueResolver
def initialize(owner:, record:)
@owner = owner
@record = record
end
def call(mapping)
return system_value(mapping["field_slug"]) if mapping["source"] == "system"
value_for(mapping["field_slug"])
end
private
attr_reader :owner, :record
def value_for(field_slug)
# Return the host-formatted value.
end
def system_value(field_slug)
case field_slug
when "form_name" then owner.name
when "submission_id" then record.id
when "submission_date" then I18n.l(record.created_at, format: :long)
else ""
end
end
end
endThe built-in system keys are form_name, submission_id, and
submission_date; the host defines their meaning. For an image, return:
{
type: "image",
data: attachment.download,
content_type: attachment.blob.content_type
}Use image/svg+xml for SVG data. Return text, a number, or "" for other
mappings.
4. Mount and protect the engine
Mount the engine:
mount NeetoPdfEngine::Engine, at: "/neeto_pdf"const PDF_API_BASE_URL = "/neeto_pdf/api/v1";The engine authenticates and scopes owners to the current organization. The host
must add server authorization to NeetoPdfEngine::Api::V1::DocumentsController,
including the render action. canManage only changes the UI.
5. Handle host lifecycle rules
When applicable, add NeetoPdfEngine::Document to host incineration, clone PDF
documents with a cloned owner, and remap saved record_id values to cloned
fields.
Frontend setup
1. Install the package and PDF runtimes
yarn add @bigbinary/neeto-pdf-frontend [email protected] [email protected]The package uses the host's Axios case transforms, React Query provider, and i18next instance.
2. Include styles and translations
There is no PDF stylesheet. Keep neetoUI and neetoMolecules styles enabled, and
make Tailwind scan @bigbinary packages. For Tailwind CSS 4, adjust this path
relative to the host stylesheet:
@import "tailwindcss" important;
@source "../../../../node_modules/@bigbinary";The standard neeto-commons build loads package translations. Other builds must
merge @bigbinary/neeto-pdf-frontend/app/javascript/src/translations/en.json
into their i18next resources.
3. Serve the PDF.js worker
Add:
{
"scripts": {
"sync:pdf-worker": "mkdir -p public && cp -f node_modules/pdfjs-dist/build/pdf.worker.min.mjs public/pdf.worker.min.js"
}
}Run it before dev and build. Serve the result at:
/pdf.worker.min.jsThe generated worker may be ignored in Git.
Use the documents page
import { PdfDocumentsPage } from "@bigbinary/neeto-pdf-frontend";
const PdfDocuments = ({ ownerId, selectedPdfDocumentIds }) => (
<PdfDocumentsPage
apiBaseUrl="/neeto_pdf/api/v1"
canManage={canManagePdfDocuments}
ownerId={ownerId}
selectedPdfDocumentIds={selectedPdfDocumentIds}
onOpenEditor={document => openEditor(document.id)}
/>
);onOpenEditorreceives the complete document object.selectedPdfDocumentIdsonly marks documents already used by email settings.renderHeaderandrenderContentcan fit the page into a host layout.emailRoutesoptionally accepts{ submitter, receiver }links.- Pass
canManageexplicitly. It only changes the UI.
Uploads use neetoMolecules FileUpload. Active Storage must return signedId.
Templates must be PDFs of 10 MB or less.
Use the editor
import { PdfEditorPage } from "@bigbinary/neeto-pdf-frontend";
<PdfEditorPage
apiBaseUrl="/neeto_pdf/api/v1"
canManage={canManagePdfDocuments}
documentId={documentId}
fields={pdfFields}
homeUrl={pdfDocumentsUrl}
isPreviewLoading={isPreviewLoading}
mode={mode}
ownerId={ownerId}
previewPage={previewPage}
previewRecord={previewRecord}
previewTotal={previewTotal}
onBack={goToPdfDocuments}
onModeChange={setMode}
onPreviewPageChange={setPreviewPage}
/>;Use a real homeUrl. mode and onModeChange are only needed for
host-controlled mode state.
Field shape
const pdfField = {
id: "field-id",
key: "stable-field-key",
recordId: "field-id",
slug: "stable-field-key",
label: "Display label",
kind: "email",
renderType: "text",
icon: EmailIcon,
typeLabel: "Email",
};Use a stable recordId, plain-text labels, and renderType: "image" only for
image data. Exclude fields that cannot contain values.
Preview record shape
const previewRecord = { id: "data-record-id" };The editor sends this ID to the render endpoint. The engine then calls
render_context_resolver, keeping preview and attachment rendering on the same
value path.
Add email attachment controls
import {
PdfAttachmentSelector,
PdfAttachmentsPreview,
} from "@bigbinary/neeto-pdf-frontend";
import { Form, Formik } from "formik";
<Formik
initialValues={{ pdfDocumentIds: persistedPdfDocumentIds }}
onSubmit={saveEmailSettings}
>
<Form>
<PdfAttachmentSelector
documents={pdfDocuments}
maxTotalSize={25 * 1024 * 1024}
/>
<PdfAttachmentsPreview documents={pdfDocuments} />
</Form>
</Formik>;Formik: pdfDocumentIds
Frontend API: customFields.pdfDocumentIds
Rails: custom_fields["pdf_document_ids"]Permit custom_fields[:pdf_document_ids] as an array. Remove deleted or
inaccessible IDs before saving. Fetch documents with the same owner ID used by
the editor. Pass maxTotalSize in bytes to prevent selections whose combined
template size would exceed the email attachment limit. The selector displays the
used size, disables PDFs that no longer fit, and explains the remaining space
when a disabled PDF is clicked.
Render attachments on the server
render_context = PdfDocuments::RenderContextResolver.call(
owner:,
record_id: record.id
)
owner.neeto_pdf_documents.where(id: selected_pdf_document_ids).map do |document|
NeetoPdfEngine::Documents::RendererService.new(
document,
**render_context
).process
endEach result contains:
{
filename: "document-record-id.pdf",
mime_type: "application/pdf",
content: "<binary PDF>",
truncated_mapping_ids: []
}The host attaches or encodes content. Encrypted PDFs raise
NeetoPdfEngine::EncryptedDocumentError. Built-in fonts support ASCII and
Latin-1; other characters are removed during server rendering.
API
With the engine mounted at /neeto_pdf:
GET /neeto_pdf/api/v1/documents
POST /neeto_pdf/api/v1/documents
GET /neeto_pdf/api/v1/documents/:id
PATCH /neeto_pdf/api/v1/documents/:id
PUT /neeto_pdf/api/v1/documents/:id
PATCH /neeto_pdf/api/v1/documents/:id/template_file
POST /neeto_pdf/api/v1/documents/:id/clone
POST /neeto_pdf/api/v1/documents/:id/render
DELETE /neeto_pdf/api/v1/documents/:idEvery request requires owner_id; clients cannot choose owner_type. Render
also requires record_id and accepts temporary mappings without persisting
them. Uploads accept an Active Storage signed blob ID or multipart
template_file.
The default output filename is:
<parameterized-document-name>-{{id}}.pdffilename_context[:id] replaces {{id}}.
Local development
gem "neeto-pdf-engine", path: "../neeto-pdf-nano"For local frontend work, follow the frontend package development guide. To publish, follow the building and releasing packages guide.
