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.
Direction (decided)
Section titled “Direction (decided)”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, indirect-send/shared/) owns recipients + undo, subject, dirty/revert, in-flight/sent state, and renders header → addresses → subject → body slot → footer. Bothemail-order-panelandpo-composerconsume it. OrderSheetis 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.
Gap analysis
Section titled “Gap analysis”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
OrderSheetis 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
OrderSheetcapabilities and this lands on top.
What is ready now vs. blocked on refactor
Section titled “What is ready now vs. blocked on refactor”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— supersedesemail-order-panel/recipient-chips.tsx(DSTokenChip, 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:
- Decouple
OrderSheetfrom 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. - Wrap
OrderSheetas anOrderSheetNodeTipTap Node View, mounted viaComposerBody’sextensionsprop, so Email Order’s body is one editable canvas of prose + the order table inline. - 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-htmlutil (not the interactive React markup). - 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.
- Adopt the ready shared primitives in Email Order — swap in
direct-send/shared/{mailto,recipient-field,composer-footer}, delete theemail-order-panel/duplicates. Pure consolidation, no behavior change. (Small, safe — this is the “do it now” slice.) - Make
OrderSheetprop-driven — decouple from Redux; PO wired through an adapter and verified behavior-identical. (Medium; the load-bearing refactor.) OrderSheetNodeNode View + serialization — the new embedding capability, withcompose-email-htmlround-tripping. (The new surface.)- Migrate Email Order onto
ComposerBody+OrderSheetNode— retireemail-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.
Open questions
Section titled “Open questions”- 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 inemail-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), andOrderSheethas no per-line taxable column. AdoptingOrderSheetis the moment to decide: drop per-line taxable (recommended) and keep tax order-level like PO, or add a taxable column toOrderSheet. 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-htmlserver step as the PO cover email (DQ-010), while the order node stays structured. - Hosting shells (out of scope, noted): PO uses
ResizableSidebarwith a two-view flow; Email Order is afixed inset-0overlay. 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”).
Related
Section titled “Related”- Shared-Component Audit — the overlap inventory this plan executes against.
- PO direct-send UI — the reference flow.
- Email Order UI — the flow being migrated.
- PDEV-1019 — Harmonize Email Order UX with PO-Lite (this plan).
- PDEV-970 — PO direct email send (the base this stacks on).
Copyright: © Arda Systems 2025-2026, All rights reserved