Skip to content

feat(forms): separate logical fields from visual widgets - #164

Open
lfals wants to merge 1 commit into
LibreSign:mainfrom
lfals:feat/84-form-field-widget-model
Open

lfals wants to merge 1 commit into
LibreSign:mainfrom
lfals:feat/84-form-field-widget-model

Conversation

@lfals

@lfals lfals commented Nov 9, 2026 •

Copy link
Copy Markdown

Resolves: #84

📝 Summary

A single overlay rectangle cannot represent radio groups or the same form field appearing on several pages. Introduces a stable public model separating logical fields from visual widgets, providing the foundation for generic form-field editing in #85.

Model Responsibility
PDFFormField Stable ID, type, name, value/default, required/read-only flags, choice options, widget references and metadata
PDFFormWidget Stable ID, parent field, page/document placement, geometry, export value and optional overlay link
PDFFormModel Plain serializable collection of fields and widgets

One field can reference multiple widgets across pages. Unknown field types retain their identity. The existing PDFElementObject API remains compatible; form-aware consumers can adopt the model alongside their overlay store.

Also adds:

  • factories, relationship validation, JSON persistence and overlay bridge helpers
  • runtime validation of malformed properties, inconsistent references, non-JSON data and sparse arrays
  • public API documentation and gradual migration guidance in docs/FORM_MODEL.md

AcroForm parsing/writing, PDF mutation, signing and host workflow concepts remain outside scope.

🧪 How to test

  1. Check public types and lint:
    npm run typecheck
    npm run lint
  2. Run focused relationship and serialization coverage:
    npm run test -- tests/utils/formModel.spec.ts
    Expected: 22 tests pass, including multi-page radio widgets, JSON round-trips, invalid references and sparse-array rejection.
  3. Run the full unit suite and build:
    npm run test
    npm run build
    npm run validate:dist
    Expected: 104 unit tests pass; build and PDF.js distribution validation succeed.
  4. Optional demo smoke check:
    npm run dev
    Existing overlay editing remains available. This issue adds the model layer; form editing UI belongs to Add generic form-field editing support #85.

Validation evidence

  • Focused regression/acceptance tests
  • Relevant lint and type checks
  • Full unit suite: 104 tests passed
  • Production build and PDF.js distribution validation
  • CI reviewed

⚙️ Public API changes

  • Separate logical field and visual widget types exported publicly
  • One-to-many relationships support radio groups and multiple pages
  • Unknown types, shared field properties and extensible metadata supported
  • JSON persistence and relationship validation helpers added
  • Existing generic overlay API preserved
  • Unit tests added for relationships, serialization and invalid input
  • Public types, compatibility and migration documented
  • Authorization/capabilities: not applicable to this data-model change.

🚧 Tasks

  • Define stable field/widget identifiers and relationships
  • Keep shared properties on fields and placement on widgets
  • Add persistence and overlay coexistence helpers
  • Document serialization expectations and migration
  • Complete two council review rounds and address blocking findings
  • CI review on the draft PR

✅ Checklist

  • I have read and followed the contribution guide.
  • The diff does not contain unrelated refactors or generated changes.
  • Commits follow Conventional Commits.
  • New source files include SPDX headers.
  • All commits include DCO sign-off (current commit has no sign-off trailer).

🤖 AI (if applicable)

  • The content of this PR was partially or fully generated using AI.

@lfals
lfals marked this pull request as ready for review November 9, 2026 09:31
@lfals
lfals requested a review from a team as a code owner November 9, 2026 09:31
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Nov 9, 2026 •

Copy link
Copy Markdown
Codex Review Summary: ✅ Completed

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-11-09T09:58:12.070498Z 5467f17 Draft marked ready
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 5467f171dd

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/utils/formModel.ts
})
}
references.add(widgetId)
const widget = getFormWidgetById(model, widgetId)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Index fields and widgets before validating relationships

When a consumer restores a large valid model on the main thread, each getFormWidgetById() call rescans the widget array, while the preceding widget loop similarly rescans fields with getFormFieldById(). This makes default-strict parseFormModel() quadratic and synchronous: a direct Node 24 check took approximately 0.5 seconds for 5,000 one-widget fields and 2.2 seconds for 10,000, blocking interaction during restoration. Build field/widget lookup maps once and reuse them, with per-field reference sets for reverse-membership checks.

Useful? React with 👍 / 👎.

@welcome

welcome Bot commented Nov 9, 2026

Copy link
Copy Markdown

Thanks for opening your first pull request in this repository! ✌️

@vitormattos vitormattos left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Requesting changes before merge to address two data-integrity issues documented in the inline comments: lossless model serialization and protection of widget-owned overlay identity and geometry. Each finding includes focused regression cases and positive controls.

Please also address the existing unresolved relationship-validation performance discussion without duplicating that thread, and add the missing DCO Signed-off-by trailer to the commit.

Comment thread src/utils/formModel.ts
* Suitable for persistence and transport.
*/
export function serializeFormModel(model: PDFFormModel): PDFFormModel {
return JSON.parse(JSON.stringify(model)) as PDFFormModel

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

serializeFormModel() can silently corrupt data before persistence because it calls JSON.stringify() without validating the input model.

For example, Infinity and NaN become null, while function-valued metadata is silently removed. The resulting snapshot may subsequently pass parseFormModel() even though information has already been lost.

Please validate the model's shape and JSON constraints before serialization, and add focused regression tests in tests/utils/formModel.spec.ts.

Required test cases:

  1. Non-finite field values: With an otherwise valid model, set field.value to Infinity and NaN in named parameterized cases. serializeFormModel() must throw instead of returning null.
  2. Invalid metadata: Test metadata: { fn: () => 42 } and metadata: { date: new Date() }. Serialization must reject these inputs rather than dropping or converting their contents.
  3. Invalid widget geometry: Set widget.x = Infinity on an otherwise valid model. Serialization must reject it.
  4. Positive control: Serialize a model containing value: 42 and metadata: { source: 'import' }. Verify that the snapshot preserves both values and that a subsequent parseFormModel() round-trip produces an equivalent model. Reuse the existing valid round-trip test where possible.

The negative tests must call serializeFormModel() directly. Existing tests that only reject these inputs through parseFormModel() do not exercise the affected path.

The expected behavior is lossless serialization of valid models and explicit rejection of invalid data.

Comment thread src/utils/formModel.ts
}
if (options.resizable !== undefined) element.resizable = options.resizable
if (options.extra) {
Object.assign(element, options.extra)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

formWidgetToElementObject() merges options.extra after assigning widget-owned properties. As a result, extra properties can overwrite the widget's overlay identity and geometry.

For example, extra: { id: 'fake', x: 999 } replaces values derived from widget.elementId and widget.x. This can break the documented widget-to-overlay relationship and cause later operations to target an incorrect overlay.

Please protect the widget-owned id, x, y, width, and height properties while continuing to support additional visual properties.

Required regression tests in tests/utils/formModel.spec.ts:

  1. Conflicting properties: Create a widget with elementId: 'overlay-1', x: 12, y: 34, width: 160, and height: 48. Call formWidgetToElementObject() with extra: { id: 'fake', x: 999, y: 999, width: 1, height: 1, icon: 'pen' }. The result must retain id: 'overlay-1' and all four original geometry values, while preserving icon: 'pen'.
  2. Positive control: Call the same helper with extra: { icon: 'pen' }. Verify that the additional property is retained and the widget-owned properties remain unchanged.
  3. Existing behavior: Preserve the current conversion test without conflicting extra properties to ensure that the normal field/widget bridge does not regress.

The conflicting-input test should fail against the current implementation and pass after the fix.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Define a stable form field and widget data model

2 participants