Skip to content

feat(package): document-list field — compose/upload workflows, drafts, permissions - #165

Open
horner wants to merge 78 commits into
mainfrom
fix/touch-density-consistency
Open

feat(package): document-list field — compose/upload workflows, drafts, permissions#165
horner wants to merge 78 commits into
mainfrom
fix/touch-density-consistency

Conversation

@horner

@horner horner commented Aug 25, 2026

Copy link
Copy Markdown
Member

The document-list field line of work eCase runs on: the DataVis-backed field and host providers, compose/upload workflows (DockablePanel + RichEditor), shared drafts (ED.36–ED.41), tombstone/restore with reasons (ED.42), capability gating, revision history + host document view, drag-and-drop upload targets, rename-only file rows, activity log summarization, renderer page navigation (fixes #147), and touch-density/style fixes.

Developed and dogfooded in mieweb/eCase, which pins this branch as a submodule.

lattln and others added 30 commits August 18, 2026 19:55
Add a reusable `@esheet/document-list-field` package backed by DataVis, including document normalization, grid rendering, toolbar actions, detail rows, and provider-based host integration.

Add generic `FieldProviderStack` support to the builder, renderer, and Blaze renderer so field add-ons can receive host behavior without changing form response data.

Register the document-list field in the demo and demonstrate Compose, Upload, detail expansion, custom detail rows, and YAML schema loading. Add focused unit tests and workspace build configuration for the new package.
- Add package-owned Compose and Upload modals
- Support editable document metadata and note content
- Pass transient text and file content through the repository contract
- Add text/image detail previews with metadata fallback
- Move demo persistence to in-memory document/content maps
- Remove host-owned upload and compose handlers
- Add workflow tests, styles, and exports
New built-in fieldType 'notes' — an array of rich (markdown) note
entries, each optionally carrying attachments:
- NotesFieldDefinition (allowAttachments/accept/maxFileSize/
  maxAttachments/maxNotes/sortOrder/entryLabel) in the FieldDefinition
  union, strict zod schema, and normalizeFormDefinition property list.
- noteEntrySchema / NoteEntry: GUID id (stable across edits, the unit
  of merge), createdAt/updatedAt, optional author, raw markdown body,
  optional attachments reusing AttachmentAnswer (now with a zod schema).
- FieldResponse.notes, AnswerValue gains NoteEntry[], answerType
  'notes' flows through extractResponseValue and hydrateResponse.
- Registry meta (label Notes, category rich, defaults entryLabel=Note,
  sortOrder=newest); required means >=1 entry in validation.

420 core tests pass (12 new).
- mergeNotes(a, b): union by GUID id; same-id conflicts resolve
  last-writer-wins on updatedAt ?? createdAt; output sorted by
  createdAt. Ships in core so every host resolves conflicts the same
  way (CRDT hosts call it when both sides changed a notes response).
- collectAttachments / mapAttachments: traverse every attachment in a
  response — fileData and notes[].attachments — so externalize/
  rehydrate pipelines don't hardcode field shapes. mapAttachments
  returns the same reference when there is nothing to rewrite.

433 core tests pass (13 new).
pnpm v10 defaults linkWorkspacePackages to false, so @esheet/* deps
resolved to the last published registry tarballs — any PR that adds an
export to core or fields could not typecheck or bundle in dependent
packages. Also externalize @esheet/fields in the builder bundle for the
same shared-registry reason as the renderer (PR #152).
…ation

Journal-style notes field (fieldType 'notes'):
- Read-only card list sorted per sortOrder (newest default): author +
  created/edited timestamps in the header, body rendered through the
  shared markdown pipeline. That pipeline is DisplayField's renderer
  extracted verbatim to lib/markdown.tsx (DisplayField now imports it;
  no new renderer dependency).
- Composer: Add button -> textarea with Write/Preview tabs; Save stamps
  GUID/createdAt (author from host identity when present), Edit reuses
  the composer and stamps updatedAt, delete requires inline
  confirmation. maxNotes hides Add; canModify hook gates per-entry
  edit/delete (host policy — server-side enforcement is the host's).
- Attachments when allowAttachments: picker enforcing accept /
  maxFileSize / maxAttachments via lib/file-utils.ts (formatFileSize +
  accept matching extracted from FileField, which now shares them).
- NoteCardList is a separate presentational list so the upcoming
  read-only activity log can reuse it without copying.
- Builder mode edits question/entryLabel/sortOrder/maxNotes/attachment
  props in-component (same pattern as FileField); registered in both
  renderer and builder defaults.

13 new DOM tests; all 6 packages green (593 tests).
EsheetRenderer gains identity?: { name: string }, kept in new form-store
state (setIdentity). NotesField stamps new entries' author from it; no
identity -> notes save unstamped. No breaking change: the prop and the
store field are both optional.
…xport

- Builder palette needs no change (registry-driven); NotesField's
  builder mode already edits entryLabel/allowAttachments/accept/limits/
  sortOrder in-component like FileField.
- Logic: 'notes' answers support empty/notEmpty conditions (LogicEditor
  operator list + core condition evaluation: count for numeric access,
  entry ids for value access).
- FHIR export: notes map to a repeating text item; each entry flattens
  human-readably (author/timestamp header, raw markdown, attachment
  titles). Structured FHIR round-trip and SurveyJS/MCP mappings are
  deferred follow-ups to note in the PR description.
- Submission payload already carries NoteEntry[] via core
  hydrateResponse (covered in the phase-1 tests).
New built-in fieldType 'activity': a read-only page any eSheet can
include that renders a log of response changes over time.

- Core: ActivityEntry (GUID id, at, author, fieldId, question, display
  from/to) under the reserved _activity response key. The form store
  emits an entry on every setResponse when the form contains an
  activity field; consecutive changes to the same field within 2s
  collapse into the last entry (original 'from' preserved) so
  keystrokes don't flood the log. Author comes from the identity state;
  display values via extractResponseValue + formatActivityValue.
- mergeActivity reuses the notes merge, now generalized as mergeById
  (union by GUID, LWW on stamp) — no copied merge logic.
- Fields: ActivityField renders the log newest-first through the same
  NoteCardList as NotesField, with zero mutation affordances (asserted
  in DOM tests); registered in renderer and builder defaults.
- Validation skips activity fields (non-input); answerType 'display'
  keeps it out of submission payloads.

Core 440 and fields 33 tests pass (11 new); all packages green.
Field-type pages with YAML examples, answer formats, merge semantics,
authorship/edit-policy guidance, and the attachment traversal helpers;
sidebar + overview updated. Full build and test sweep green across all
packages (607 tests).
…mposer

NotesField's composer is now a registration point: hosts call
registerNotesComposer() from @esheet/fields to swap the default
textarea (Write/Preview tabs) for a rich editor. Markdown stays the
storage format either way, so composed notes render identically in the
read-only card list and the textarea default remains for hosts that
don't want the weight (~1.4 MB JS).

@esheet/field-kerebron ships KerebronNotesComposer: the same
CoreEditor + AdvancedEditorKit markdown load/save loop as
RichTextEditorField (debounced changed-event flush, final flush on
unmount, fresh mount node for destroy()). @esheet/fields is externalized
there for the same shared-registry reason as renderer/builder.

All packages green (627 tests).
…undle

@kerebron/* pin prosemirror-model 1.25.9 exactly while other prosemirror
libs float to 1.25.11, so the bundle carried both — at runtime typing
threw 'Can not convert to a Fragment (looks like multiple versions of
prosemirror-model were loaded)' because DOM changes parsed with one
model against a schema built with the other. Workspace override pins
1.25.9; the dist now contains a single copy.
…, serialize loads

Two runtime fixes found wiring KerebronNotesComposer into a host:
- loadDocumentText always runs, even for an empty note: without an
  initial document the editor logs 'NO NODE at 1' and swallows input.
- Initial loads are serialized through a module-level queue (the same
  pattern as echart-sim's DocumentComposeEditor): the markdown
  converter's tree-sitter WASM init is not concurrency-safe, so
  parallel mounts (React StrictMode, multiple notes fields) raced into
  'cannot construct a Parser before calling init()'. A disposed guard
  skips loads whose editor was destroyed while queued.

Browser-verified in eCase: compose, save, and edit round-trip through
markdown with the WASM served same-host.
The field wrapper was static, so the absolutely positioned dots flew to the page corner. Keying by name also collapsed two windows of one person into one dot, and the dot size came from a utility a host that only loads the fields stylesheet does not have.
Touch mode forced min-height/font-size on every button under the renderer
root, which oversized the DataVis chrome; exempt [data-slot] buttons and
size the document-list title actions directly, since the overlay never
inherits body.condensed.
eSheet carries whole files inline as base64 data URLs. That is fine for a
demo and ruinous for a host that persists the response — a CRDT-backed host
pays the +33% again on every edit, forever.

Adds an optional AttachmentManager (store/load/remove) in @esheet/core,
delivered through the existing fieldProviders stack. FileField stores on
pick and removes on delete; the NotesField composer stores on save, so a
cancelled composer leaves nothing behind. With no provider both fields
behave exactly as before, inline.
A host that stores document bytes outside the response needs to address
them from the row alone. eSheet never computes sha256/size; it carries
them through normalization untouched.
# Conflicts:
#	packages/field-kerebron/package.json
…e the noun

The compose and upload forms were @mieweb/ui modals: they dimmed the page
and shrank the form to a narrow dialog, so writing a note felt like
working through a magnifying glass. They are now a panel that covers the
document list it belongs to — full width, full height, header with a
close button, scrolling body, Escape to dismiss, first field focused on
open.

Two things the forms hard-coded are now the field's to say:

- `noun` names one row, so the same field reads as "Compose note",
  "Compose letter", or the default "Compose document" — in the toolbar
  labels, the panel title, and the save button.
- The compose form asks for the columns the list shows. A list without a
  `subject` column no longer asks for a subject, and its validation
  message names only the fields it rendered. Title is always asked for:
  it is the row's name.

DocumentListComposeModal and DocumentListUploadModal are renamed to
…ComposePanel and …UploadPanel, and are rendered only while open.
horner added 17 commits August 24, 2026 11:27
docTypes entries take template (a .mdyt source; hosts resolve file
references before it gets here) and mergeContext (template variable ->
host field id — the template may reference only what is declared).
The host supplies renderTemplate(template, mergeContext); this package
never depends on a template engine, and the host loads its own lazily.
Composing new renders once and the author edits from there — revising
starts from the saved revision instead, and a saved document never
re-renders itself. Note tier lands the body in the editor; the
definition tier holds its form until the template is ready, then
applies it as the body field's initial value.

Design record: eCase editor-plan.md Phase 6 (mieweb/eCase).
The author still gets an editor — just not a prefilled one — and the
panel's error line names the failure (it caught a missing engine peer
during eCase's browser verification).
…cell

datavis 1.6.0's table renderer does 'if (formatCell) return
formatCell(...)' with no fallback, and DocumentListGrid always installs
a wrapper once the field has actions or presence badges — so every
plain cell in a real browser rendered blank while the jsdom specs,
which mock the grid, asserted undefined was fine. The wrapper now
returns the cell's own value when neither the host formatter nor the
actions column produced anything; cells are already normalized display
strings, so the fallback is exact.
… says

ED.40's parse-failure rule said the legacy body loads as a note, but
the panel chose its tier from the docType alone — so a document saved
before its type gained a definition opened the definition form with
nothing in it and silently dropped the old body. The opener now records
'meta:tier: note' on the draft and the panel honors it, so joiners land
in the same tier the opener saw. The next save writes the type's
current serialization, which heals the document forward.
The runtime cached loadContent results and never invalidated them, so
revising a document you had just saved (or previewed) prefilled the
previous revision's body. The save's success path now drops the row's
contents entry; the next read refetches the new head.
…g history

The revision number joins the grid's columns ('rev' — payloads default
absent to 0 so pre-revision rows read as rev 0), and the detail row is
just the document again: the inline History section is gone, replaced
by a link to the host's full-page document view when the host supplies
documentHref on DocumentListFieldHost. Revision reading moves to that
page; the field keeps listRevisions for hosts that render their own.
…le accessors

The active page becomes host-addressable: initialPageId seeds the first
page shown (unknown or absent falls back to the first page, silently),
onPageChange(pageId, pageIndex) fires on user navigation only — tab
clicks and prev/next, never the initial seed and never programmatic
moves, so a host syncing the URL cannot loop — and the handle gains
getCurrentPageId() / setCurrentPage(pageIdOrIndex), the latter a silent
no-op for unknown targets. The page index stays in RendererBody; the
handle reaches it through a registration callback like goToPage before
it.

Fixes #147 (Redmine #158288).
…less

A field whose columns drop 'title' (case notes: the note is the
content) composes without a Title input; the note-tier save titles the
row after its document type, the same fallback the definition tier
already used. Presence badges ride the title cell when there is one and
the field's first column otherwise.
A field definition may say expandDetails: true — for lists read more
than scanned, every row mounts with its content open (the Detail toggle
still collapses them). The built-in Edit/Append/Remove render in a
nowrap inline-flex span and the actions column loses its fixed 48px, so
the buttons sit on one line instead of wrapping down the cell.
Edit / Append / Remove / Restore render as icon-only ghost buttons
(SquarePen, ListPlus, Trash2, ArchiveRestore) — the aria-labels keep
naming the row, tooltips carry the verb, and the actions cell shrinks
from ~155px of text to ~84px of icons.
The cell fallback wraps date-shaped values in a nowrap span, and the
presence wrapper is nowrap too — on a title-less list the badges ride
the date column.
Custom fields round-trip whole JSON documents as one string answer, so
the activity log printed raw JSON blobs. formatActivityValue now
summarizes JSON-shaped strings ('2 entries', from the first array
property), change detection moved to the raw values — two edits can
share a summary and still be a real change, shown as '2 entries
(edited)' — and the renderer drops the '— →' prefix on first fills so
they read as one statement.
The grid sorts reverse-chronologically by the date column (undated rows
sink, ties keep answer order), and opening the compose panel on the
note tier puts the caret in the rich text editor — composing is
writing. The definition tier keeps its own first-field focus.
Padding and gap drop to a quarter of what they were (the row shrinks
~97px to ~65px) and the revision link renders small and muted, taking
color only on hover — a footnote, not a call to action.
Two drop targets feed the uploader: the upload panel's file field is now
a drop zone, and the whole list accepts a dropped file, opening the
upload panel with it pre-selected (session-based uploads only — a host
that owns onUpload keeps its own flow). Dropping sets the stored
filename the same way picking does.
An uploaded screenshot rendered borderless was indistinguishable from
live UI. A muted mat with a border says 'this is an attachment'.
A file's bytes aren't editable prose: rows whose docType is a MIME type
(uploads) swap Edit for a Rename dialog that changes only the title
(a metadata-only rev bump, action 'edit'), and drop Append. Composed
rows keep the full Edit/Append flow.
@horner horner changed the title Document list field: compose/upload workflows, drafts, permissions, density feat(package): document-list field — compose/upload workflows, drafts, permissions Aug 25, 2026
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Aug 25, 2026

Copy link
Copy Markdown

Deploying esheet with  Cloudflare Pages  Cloudflare Pages

Latest commit: f5c139e
Status: ✅  Deploy successful!
Preview URL: https://26055377.esheet.pages.dev
Branch Preview URL: https://fix-touch-density-consistenc.esheet.pages.dev

View logs

@lattln

lattln commented Aug 25, 2026

Copy link
Copy Markdown
Collaborator

doing a indpeth review, testing, qa .. etc update : running ci fixes

@lattln lattln self-assigned this Aug 26, 2026
@lattln

lattln commented Aug 26, 2026

Copy link
Copy Markdown
Collaborator

All Popups in DocumentList Field not interacting as a Modal
Screenshot 2026-08-26 at 9 13 31 AM
Screenshot 2026-08-26 at 9 13 39 AM

@lattln

lattln commented Aug 26, 2026

Copy link
Copy Markdown
Collaborator
Screenshot 2026-08-26 at 9 19 55 AM Screenshot 2026-08-26 at 9 28 08 AM

Looks like my kerebron patch got removed & gutted

lattln added 4 commits August 26, 2026 11:55
+ Fixes Kerebron Toolbar
+ Fixes DocumentListField to use Kerebron from field-kerebron package
+ remove mieweb/ui kerebron usage
+ Patch kerebron with frozen version in repo
+ Fix Kerebron Style Issue in Toolbar
... etc
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.

Let the host control which page EsheetRenderer is showing

2 participants