Skip to content

Direct-Send Composer — Harmonization Plan (Email Order adopts OrderSheet)

Execution plan for PDEV-1019 — Harmonize Email Order UX with PO-Lite, building on the Shared-Component Audit. Where the audit proposed a new shared shell abstraction, this plan takes the concrete direction set after PDEV-970 shipped and was tested against the dev backend: the PO flow is the reference, and the PO OrderSheet becomes the single canonical line-item editor that the Email Order flow adopts.

Guiding principle — the two composers are the same component. The Email Order and PO composer pages are one shared DirectSendComposer. The only difference between them is where the order sheet goes:

  • Email Order embeds the order sheet inline in the email body.
  • PO delivers the order sheet as the attached PDF (its prose cover-email body has no inline table).

Everything else is shared and identical — the addresses block (To / Cc / From, in the same order, with the same vendor To-typeahead), the Subject field (same control), the footer/actions (Copy · Send · Open-in-email), and the body chrome. No per-flow divergence in any of it. Where they differ today (e.g. Subject as EditableText vs a plain input) is a bug to converge, not a variation to preserve.

Concretely:

  • One shared shell (DirectSendComposer, in direct-send/shared/) owns recipients + undo, subject, dirty/revert, in-flight/sent state, and renders header → addresses → subject → body slot → footer. Both email-order-panel and po-composer consume it.
  • OrderSheet is the single line-item editor. Where it renders is the per-flow variant: Email mounts it inline as a TipTap node in the body; PO keeps it on its order-sheet page and feeds the generated PDF. The Email Order bespoke table is retired.
  • PO’s flow shape is unchanged (two views: order-sheet page → composer page; PDF as the delivered payload). What changes is that PO’s composer page is refactored to consume the shared shell — behavior-preserving, test-covered.
  • Attachments are a generic slot, not PO-only. PO’s PDF rides in it today; future: both order types can attach additional files (drag-in extra attachments alongside the order).

This realizes the audit’s DirectSendComposer shell in full. The line-item surface is the existing, battle-tested OrderSheet (not a re-abstraction); the composer chrome is the shared shell.

OrderSheet.tsx is the richer, correct implementation; the Email Order table lacks or mishandles functionality it provides (per-line notes, add/remove line, computed cost-per-each and line cost, subtotal footer, the shared cell-edit interaction). Adopting OrderSheet for Email Order closes those gaps by construction — that is the primary motivation, not cosmetic consistency.

Known gap in OrderSheet itself (net-new capability)

Section titled “Known gap in OrderSheet itself (net-new capability)”

OrderSheet currently only renders/edits lines that originate from the order queue. It cannot add an arbitrary line that isn’t already an order-queue item — an empty row the user fills in by hand (name, qty, price, etc.).

  • This capability is needed for both flows once OrderSheet is the shared editor.
  • Future extension: an arbitrary hand-entered line becomes the seed for generating a new item / kanban card, so the ad-hoc order also creates the catalog entry it implies.
  • Tracked separately (candidate ticket) so it doesn’t block the harmonization itself — the migration can land with the current OrderSheet capabilities and this lands on top.

The shared foundation already exists but is consumed only by the PO composer today; Email Order still runs a full parallel stack. Some of the adoption is safe to do immediately; the OrderSheet embedding needs a refactor first.

Ready now — shared primitives Email Order can adopt with no new work:

  • direct-send/shared/mailto.ts (buildMailtoHref) — Email Order’s copy is verbatim; delete the duplicate.
  • direct-send/shared/recipient-field.tsx — supersedes email-order-panel/recipient-chips.tsx (DS TokenChip, autofill guard).
  • direct-send/shared/composer-footer.tsx — generic slotted footer.
  • Downstream libs are already shared by both (validate-email-order, arda-badge, use-email-config-status, EmailJobProxy) — no change.

Blocked on refactor — the OrderSheet embedding:

  1. Decouple OrderSheet from Redux. It reads PO draft state directly (selectDraftLines / selectDraftSummary). It must become prop-driven (lines + summary + change callbacks in) so each flow supplies its own data. PO keeps identical behavior through a thin store-backed adapter. This is the load-bearing refactor.
  2. Wrap OrderSheet as an OrderSheetNode TipTap Node View, mounted via ComposerBody’s extensions prop, so Email Order’s body is one editable canvas of prose + the order table inline.
  3. Serialize the node to the email body. Email Order sends inline HTML, so the node’s structured line data composes to email-safe inline-styled HTML through the existing compose-email-html util (not the interactive React markup).
  4. Reconcile the line model and send model. OrderSheet’s columns (SKU, qty, unit, cost/ea, cost, subtotal, add/remove) vs. the email model’s (name, qty, taxable, sku, unit price, note) don’t fully overlap — unify on the superset the email send needs. Email Order today is a purely structured send; it becomes free-form prose + a structured order node, so the prose now needs the same server sanitize step the PO cover email uses.

Phased plan (stacked PRs on PDEV-970 / #981)

Section titled “Phased plan (stacked PRs on PDEV-970 / #981)”

Each phase is independently reviewable — the reason for stacking rather than one large PR.

  1. Adopt the ready shared primitives in Email Order — swap in direct-send/shared/{mailto,recipient-field,composer-footer}, delete the email-order-panel/ duplicates. Pure consolidation, no behavior change. (Small, safe — this is the “do it now” slice.)
  2. Make OrderSheet prop-driven — decouple from Redux; PO wired through an adapter and verified behavior-identical. (Medium; the load-bearing refactor.)
  3. OrderSheetNode Node View + serialization — the new embedding capability, with compose-email-html round-tripping. (The new surface.)
  4. Migrate Email Order onto ComposerBody + OrderSheetNode — retire email-body-preview.tsx / editable-text.tsx, reconcile the send model. (The payoff; closes PDEV-1019.)

Net-new add-arbitrary-line capability (the gap above) is a separate ticket that can land before or after phase 4.

  • Line/send model: the exact unified line shape and how cost/ea + subtotal (OrderSheet) map onto the email send model.
  • Per-line taxable — revisit / likely drop. The Email Order table has a per-line Taxable-status toggle (listed in email-order-ui.md + carried in the send model), but the documentation only lists it — there’s no rationale for surfacing an internal taxable flag to a supplier. The PO model handles tax at the order level (a Taxes/Fees/Discounts section + summary), and OrderSheet has no per-line taxable column. Adopting OrderSheet is the moment to decide: drop per-line taxable (recommended) and keep tax order-level like PO, or add a taxable column to OrderSheet. Decision belongs to the OrderSheet-embedding phase, not the shared-primitive adoption.
  • Sanitization: Email Order’s prose becomes free-form — confirm it routes through the same sanitize-email-html server step as the PO cover email (DQ-010), while the order node stays structured.
  • Hosting shells (out of scope, noted): PO uses ResizableSidebar with a two-view flow; Email Order is a fixed inset-0 overlay. Converging the outer shells is separate work and deliberately not in this plan.
  • Node View backing: use TipTap node attributes for the order data, not React context (the audit flags context “doesn’t scale to multi-table documents”).