Org working dialog lifecycle audit
This audit is the implementation inventory for resumable org work. It covers every org dashboard dialog family found in the source tree and separates long-lived work from confirmations and shell utilities that must remain transient.
No behavior should be changed from this audit alone. The inventory exists so the working-tab lifecycle can be implemented once, consistently, and verified feature by feature.
Implementation note (2026-07-30): the “current gap” columns below are the pre-refactor baseline that justified the work. The shared lifecycle engine, immutable ordering, create/quick-note retention, canonical preview/edit identity, timer handoff, and durable V2 pins are now implemented. Feature providers remain only as draft, callback, and render-handle adapters; they no longer define rail order, retention policy, or lifecycle identity.
The authoritative desired behavior is
../product/org-working-tabs-lifecycle-spec.md.
That specification decides ordering, retention, dismissal, pinning, handoff,
persistence, identity, and acceptance criteria. This audit decides which
surfaces must comply.
The required consolidation boundary is
org-working-tabs-architecture.md. Core
rows integrate through its shared lifecycle engine and feature-adapter contract;
they must not implement local variants of the policy below.
Required working-tab contract
For every resumable work surface:
- Genuinely adding it makes it the first working item in the topbar. Resuming an existing item does not move it. Permanent shell controls may remain before the working-item rail.
- Opening it also opens its dialog immediately. Registering a working item must never leave the dialog hidden.
- The pin belongs to the whole lifecycle, not only the current component: create draft -> saved record -> preview -> edit -> later saved versions.
- Pinning retains the current stage even when the draft is untouched and has never autosaved.
- Entering meaningful content retains the current stage when the user minimizes it or clicks outside, even when it is not pinned.
- Minimize and pointer-down outside hide the dialog without discarding its state. They are not close operations.
- An explicit close/discard operation removes an unpinned session. A pinned saved record remains as its record working item unless the user explicitly unpins it.
- Saving a pinned create session atomically replaces that create item with the saved record item in the same position. It must not render both.
- Successful explicit save completes and removes an unpinned create item by default. Background autosave never counts as completion.
- Navigating around the org app does not destroy a pinned or engaged working stage. Resuming restores the same stage and draft, not a generic substitute.
- Reopening, resuming, pinning, saving, or changing stage never changes the item's immutable added order.
- Record identity is stable across route changes and transitions between preview and edit. A lifecycle transition must not create an unrelated tab.
Scope and source count
The static scan covers
apps/app/src/app/[locale]/org/(dashboard).
The tree currently contains 126 dialog-primitive call sites, including wrapper composition:
| Primitive or wrapper | Call sites |
|---|---|
ModalDialog | 58 |
CreateDialogScaffold | 15 |
Radix DialogContent | 10 |
AlertDialogContent | 25 |
ConfirmDialog | 18 |
The working system currently recognizes:
- 17 shell create-action types
- 17 saved-record pin types
- 13 create types that promote a selected pin to a saved record
The tables below catalogue product surfaces rather than counting a wrapper and the primitive it composes as two separate dialogs.
Current state owners and systemic gaps
| State owner | Current responsibility | Current gap |
|---|---|---|
OrgCreateIntentProvider | Shell-owned create sessions in localStorage; one active create ID | New sessions append; pin intent does not retain an untouched session; only a stored draft keeps a minimized session |
OrgQuickNoteProvider | Quick-note sessions in localStorage; one active quick-note ID | New sessions append; closing an untouched pinned draft removes it; create-to-record pinning is special-case note logic |
OrgEditDialogTabsProvider | Temporary open edit/preview tabs | Tabs append; registration is effect-driven; only page-local component state owns the actual draft |
OrgWorkspaceTabsProvider | Temporary record tabs in sessionStorage | New tabs prepend within this owner, but no immutable cross-owner added order exists |
OrgPinnedWorkProvider | Durable saved-record pins in backend preferences | Pinning or re-pinning can move a record, violating stable insertion order; it also pins canonical record identity rather than the live dialog stage/draft |
OrgWorkingQuickViewStrip | Combines durable pins, temporary tabs, create sessions, quick notes, timer, and edit tabs | Each owner contributes a differently ordered array; sortWorkingStripEntries returns insertion order unchanged |
| Route/page state | Most existing-record editors and previews | Navigation unmounts the editor; a durable pin can reopen a preview, but not the same live edit draft |
Confirmed ordering defect
The topbar is not using one immutable addition sequence:
- durable pins are stored with newest/moved item first
- workspace record tabs prepend a new record within their own owner
- create sessions append to
createActions - quick-note sessions append to
sessions - edit/preview dialog tabs append to
tabs - the strip concatenates those groups and performs no final ordering
Therefore genuinely new work cannot reliably become first across owners, and later pin or persistence operations can change an established position. Both behaviors violate the stable insertion rule.
Confirmed pin-retention defect
The create pin currently stores pinResultAfterCreate. It is an instruction to
pin a future saved record, not a pin on the current create session.
minimizeCreateAction still removes the session when no persisted draft exists.
Quick notes have the same shape: pinResultAfterSave is ignored when an empty
unsaved note is hidden.
Existing-record dialogs have a different defect. Their pin persists the record, but their form state remains route-owned. Navigating away can replace the live edit stage with a generic shell preview.
Create lifecycle inventory
All rows in this section are core resumable work and should use the required contract.
| Feature | Create surface | Draft now | Pinned save handoff now | Gap |
|---|---|---|---|---|
| Projects | ProjectCreateDialog | Persisted by create-session draft storage | Yes -> project | Pin does not retain an untouched create stage; ordering appends |
| Project tasks | ProjectTaskDialog in create mode | Persisted | Yes -> projectTask | Same; nested parent-create return is separate orchestration |
| Clients | ClientCreateDialog | Persisted | Yes -> client | Same |
| Timer | shell TimerDialog before start | Persisted | No saved-record pin transition; active timer is special | Pin control cannot carry the lifecycle; timer is force-positioned separately |
| Time entries | TimeEntryCreateDialog | Persisted | Yes -> timeEntry | Same untouched-pin and ordering defects |
| Notes | NoteComposeDialog in create mode | Persisted | Yes -> note | Separate from quick-note lifecycle despite the same saved record |
| Quick notes | topbar quick-note composer | Persisted in quick-note sessions | New notes can pin -> note | Untouched pinned note is removed on hide; ordering appends |
| Calendar | EditableCalendarEventDialog in create mode | Persisted | Yes -> calendarEvent | Same untouched-pin and ordering defects |
| Reminders | ReminderDialog in create mode | Persisted | Yes -> reminder | Same |
| Contracts | ContractCreateDialog | Persisted | Yes -> contract | Same |
| Invoices | InvoiceCreateDialog | Persisted | Yes -> invoice | Access/Stripe/empty-client fallback modals are alternate render states of the same create lifecycle |
| Subscriptions | SubscriptionCreateDialog | Persisted | Yes -> subscription | Same |
| Quotes | QuoteCreateDialog in create mode | Persisted | Yes -> quote | Same |
| Links | LinkCreateDialog | Persisted | Yes -> link | Same |
| Drive | DriveCreateDialog / CreateDriveItemDialog for upload, folder, and doc | Persisted per kind | Yes -> driveFile or driveFolder | Three draft shapes share one create action; transition and ordering need one lifecycle identity |
| Catalog products | CatalogProductCreateDialog | Persisted | No record pin type or promotion | The create action has a registry identity, but no pin control or saved-product tab exists |
| Catalog prices | CatalogPriceCreateDialog | Persisted | No record pin type or promotion | The create action has a registry identity, but no pin control or saved-price tab exists |
| Portals | PortalCreateDialog | Persisted | No record pin type or promotion | The create action has a registry identity, but no pin control or saved-portal tab exists |
Saved-record lifecycle inventory
Every row is a saved-record working lifecycle. A pin must preserve record identity and the active preview/edit stage. Dirty edit state must survive hiding and navigation.
| Record type | Preview surface | Edit surface | Current topbar behavior | Current lifecycle gap |
|---|---|---|---|---|
note | Note working chip / quick editor | Quick editor, compose dialog, and Notes page editor | Durable and temporary note items exist; saved note sessions are deduplicated | Multiple state owners; pinned create/edit stages do not share one lifecycle identity |
project | Project quick-view chip | ProjectEditDialog | Edit registers a temporary dialog tab; pin stores the project | Navigation loses live edit draft and resumes a preview |
client | Client quick-view chip | clients-list edit ModalDialog | Edit registers a temporary dialog tab | Same |
clientDrive | Client-drive quick-view workspace | Drive workspace operations | Durable/temporary record item | No single resumable operation stage for nested create/request/share work |
driveFolder | Drive/client-drive workspace | rename/move/share operations | Record can be pinned | Operations are untracked, route-owned dialogs |
driveFile | DriveFilePreviewDialog | document edit in ClientDriveManagementDialogs | Preview/doc edit can register; record can be pinned | Edit state and preview stage are not one durable lifecycle |
link | LinkDetailsDialog | LinkEditDialog | Preview/edit register; record can be pinned | Route navigation loses active edit draft/stage |
reminder | Reminder working quick view | ReminderDialog | Quick view is shell-owned; edit can register | Preview and edit use separate owners; edit draft is not durable |
projectTask | Task working quick view | ProjectTaskDialog in edit mode | Edit can register; record can be pinned | Live edit state is page-owned |
projectUpdate | Update working quick view | ProjectUpdateEditorDialog | Edit can register and disables outside close | Navigation still destroys the editor; pin retains record only |
clientContact | Contact working quick view | ContactDialog in edit mode | Edit can register; record can be pinned | Create mode is not represented by this saved-record ID; edit state is page-owned |
invoice | Invoice working preview | No general invoice-edit dialog; detail actions are separate | Record can be pinned | Detail-operation stage is not represented |
subscription | Subscription working preview | SubscriptionEditDialog plus lifecycle operation dialogs | Record can be pinned | Edit and operations do not register as stages of the pinned subscription |
contract | ContractPreviewDialog | ContractCreateDialog with contractId | Preview/edit register; record can be pinned | Preview -> edit -> save is split across component-local state |
quote | Quote working preview | QuoteCreateDialog with quoteRecordId | Edit registers; record can be pinned | Live edit draft does not survive route navigation |
calendarEvent | manual/project event dialogs and event quick view | EditableCalendarEventDialog | Preview/edit can register; record can be pinned | Manual/project preview, editable event, and create/save transitions are separate paths |
timeEntry | Time-entry working quick view and TimeEntryReviewDialog | TimeEntryEditDialog | Edit can register; record can be pinned | Review/edit/return-workflow stages are not one durable lifecycle |
Feature-by-feature dialog catalogue
Legend:
- Core: must become a resumable working lifecycle.
- Parent stage: should be retained as a stage of its owning core record, rather than a separate unrelated tab.
- Transient: must not become a working tab.
- Shell utility: intentionally outside the working-record model.
Shell and cross-feature
| Surface | Class | Required treatment |
|---|---|---|
| Create host for all 17 create types | Core | One ordered lifecycle registry |
| Quick-note composer and saved-note quick editor | Core | Merge with note lifecycle |
| Timer draft and active timer | Core | Stable draft -> running timer -> saved time-entry lifecycle |
| Client focus selector dialog | Shell utility | Never a working item |
| Client-focus navigation guard | Transient | Confirmation only |
| Launchpad full-screen dialog | Shell utility | Never a working item |
| Search dialog | Shell utility | Never a working item |
| Calendar/reminders/notes topbar quick-view popovers | Shell utility until they open a record | Record selection activates the corresponding core lifecycle |
| Create-draft discard dialog | Transient | Confirmation only |
| Working-item remove/unpin confirmations | Transient | Confirmation only |
| Timer save/close and discard confirmations | Transient | Confirmation only |
| Changelog, hotkeys, feedback, and feature-request dialogs | Shell utility | Never working items |
Clients and portal configuration
| Surface | Class | Current behavior / required treatment |
|---|---|---|
| Create client | Core | Covered by shell create session; fix pin retention/order |
| Edit client from client list | Core | Temporary edit tab; make draft/stage shell-owned |
| Create contact | Core | Currently normal dialog with no create working identity |
| Edit contact | Core | Existing-record tab only while route remains mounted |
| Delete client | Transient | Destructive confirmation |
| Create portal | Core | Create session exists but has no saved-record promotion |
| Invite portal contact | Parent stage | Tie to portal/client lifecycle if retained |
| Configure portal relationship contacts | Parent stage | Tie to portal/client lifecycle; local dirty form currently dies on navigation |
| Revoke invite / disable portal contact | Transient | Destructive confirmation |
| Disable portal | Transient | Confirmation |
Projects
| Surface | Class | Current behavior / required treatment |
|---|---|---|
| Create project | Core | Create session; fix retention/order |
| Edit project | Core | Temporary edit tab; draft not navigation-safe |
| Create project task | Core | Create session; parent context must survive nested create |
| Edit project task | Core | Temporary edit tab; draft not navigation-safe |
| Create/edit project update | Core | Temporary edit tab for saved update; outside click is currently blocked |
| Create/edit blocker | Parent stage | Tie to project lifecycle; currently raw route-owned dialog |
| Archive/restore/delete project | Transient | Confirmations |
| Complete/delete task | Transient | Confirmations |
| Resolve blocker | Transient | Confirmation |
| Publish/delete project update | Transient | Confirmations |
Calendar and reminders
| Surface | Class | Current behavior / required treatment |
|---|---|---|
| Create calendar event | Core | Create session; fix retention/order |
| Edit editable calendar event | Core | Temporary record stage |
| Preview manual calendar event | Core | Temporary preview stage |
| Preview project-derived calendar event | Core | Temporary preview stage tied to canonical project context |
| Create reminder | Core | Create session |
| Edit reminder | Core | Temporary record stage |
| Reminder compact quick view | Core preview | Shell-owned preview should be the same lifecycle |
| Delete reminder | Transient | Confirmation |
Notes
| Surface | Class | Current behavior / required treatment |
|---|---|---|
| Create note compose dialog | Core | Separate create-session owner |
| Quick-note create composer | Core | Separate quick-note owner |
| Edit saved note in quick editor | Core | Local-storage note session |
| Edit saved note in compose dialog | Core | Must use same saved-note lifecycle |
| Notes page editor | Core non-dialog stage | Must coordinate with the same lifecycle instead of becoming a competing editor |
| Discard unsaved note | Transient | Confirmation |
Drive
| Surface | Class | Current behavior / required treatment |
|---|---|---|
| Upload file | Core | Create lifecycle |
| Create folder | Core | Create lifecycle |
| Create document | Core | Create lifecycle |
| Edit document | Core | Saved driveFile stage |
| Preview file | Core preview | Saved driveFile stage |
| Client-drive create dialog | Core | Must use the same upload/folder/doc lifecycle as top-level Drive |
| Rename item | Parent stage | Retain under driveFile/driveFolder if dirty |
| Move item | Parent stage | Retain under owning record if needed |
| Share item | Parent stage | Retain under owning record if dirty |
| Create file request | Core or parent stage | Needs a canonical request record type before it can be independently pinned |
| Create/manage contributor access link | Core or parent stage | Needs a canonical access-link record type or explicit parent ownership |
| Archive/delete/move-to-trash | Transient | Confirmations |
Contracts
| Surface | Class | Current behavior / required treatment |
|---|---|---|
| Create contract | Core | Create session |
| Preview contract | Core preview | Temporary dialog tab |
| Edit contract | Core | Saved-record edit stage |
| Upload attachment | Parent stage | Tie to contract lifecycle if retained |
| Publish contract | Transient | Confirmation |
| Permanently delete contract | Transient | Confirmation |
Links
| Surface | Class | Current behavior / required treatment |
|---|---|---|
| Create link | Core | Create session |
| Preview link | Core preview | Temporary dialog tab |
| Edit link | Core | Temporary edit tab |
| Archive/delete link | Transient | Confirmations |
Billing records
| Surface | Class | Current behavior / required treatment |
|---|---|---|
| Create invoice | Core | Create session; fallback access/setup states are part of the same lifecycle |
| Preview invoice | Core preview | Saved record quick view |
| Create credit note | Parent stage | Tie to invoice lifecycle; currently an alert-form dialog |
| Refund invoice/payment | Parent stage | Tie to invoice lifecycle; currently an alert-form dialog |
| Write off invoice | Parent stage | Tie to invoice lifecycle |
| Void invoice | Transient or parent stage | Confirmation with invoice context |
| Delete invoice | Transient | Destructive confirmation |
| Create subscription | Core | Create session |
| Preview subscription | Core preview | Saved record quick view |
| Edit subscription | Core | Currently an unregistered page-owned modal |
| Manage trial | Parent stage | Tie to subscription lifecycle |
| Manage pause | Parent stage | Tie to subscription lifecycle |
| Manage schedule | Parent stage | Tie to subscription lifecycle |
| Manage discount | Parent stage | Tie to subscription lifecycle |
| Cancel now | Transient or parent stage | Destructive operation with local input |
| Cancel scheduled change | Transient | Confirmation |
| Create quote | Core | Create session |
| Edit quote | Core | Saved-record edit stage |
| Preview quote | Core preview | Saved record quick view |
| Accept/cancel/delete quote | Transient | Confirmations |
Catalog
| Surface | Class | Current behavior / required treatment |
|---|---|---|
| Create product | Core | Draft persists, but no product record pin/promotion exists |
| Create price | Core | Draft persists, but no price record pin/promotion exists |
Time
| Surface | Class | Current behavior / required treatment |
|---|---|---|
| Timer draft | Core | Special create action |
| Running timer | Core | Server-backed shell activity; starting it adds it first, then its position remains stable |
| Create time entry | Core | Create session |
| Edit time entry | Core | Temporary edit tab |
| Review time entry | Core preview | Raw route-owned dialog |
| Return time entry with reason | Parent stage | Tie to time-entry lifecycle |
| Delete time entry | Transient | Destructive confirmation |
| Create/link note from timer | Nested core transition | Parent timer must remain resumable while note creation becomes active |
Settings and administration
These are catalogued because they are dialogs, but they should not automatically enter the general working rail without an explicit product decision. Most are bounded configuration or security operations rather than client work records.
| Feature | Dialogs | Default class |
|---|---|---|
| Analytics | profile editor, adjustment editor, delete profile | Settings-local; delete is transient |
| API access | issue credential, reveal/acknowledge issued credential | Settings-local/security-sensitive |
| Billing settings | plan-interest form, initial subscription checkout | Settings-local |
| Integrations | payment recovery, Stripe connection start, activation, cancel, webhook repair | Settings-local; cancel is transient |
| Security | account-security/step-up dialogs used by the org settings surface | Security-sensitive shell utility |
| Team | invite member, remove member, transfer ownership | Invite is settings-local; remove/transfer are transient destructive dialogs |
| Webhooks | create endpoint, reveal/acknowledge secret | Settings-local/security-sensitive |
| Advanced settings | destructive organization action | Transient |
Implementation coverage matrix
The eventual implementation is incomplete until every Core row has tests for the applicable columns.
| Behavior | Create | Existing edit | Preview | Quick note | Nested parent stage |
|---|---|---|---|---|---|
| Opens immediately | Required | Required | Required | Required | Required |
| New lifecycle is inserted first | Required | Required | Required | Required | Required only when independently added |
| Existing lifecycle activation preserves order | Required | Required | Required | Required | Required |
| Stage handoff preserves order | Required | Required | Required | Required | Required |
| Pin retains untouched stage | Required | Required | Required | Required | Inherit parent |
| Meaningful hide retains stage | Required | Required | N/A | Required | Required |
| Pointer-down outside hides | Required | Required | Required | Required | Required |
| Minimize hides | Required | Required | Required | Required | Required |
| Navigation preserves stage | Required | Required | Required | Required | Required |
| Resume restores exact stage | Required | Required | Required | Required | Required |
| Save keeps selected pin | Required | Required | N/A | Required | Inherit parent |
| Create -> record transition | Required | N/A | N/A | Required for new note | Where a record is created |
| No duplicate old/new item | Required | N/A | Required across preview/edit | Required | Required |
| Explicit close/discard removes unpinned stage | Required | Required | Required | Required | Required |
Decisions already established by this audit
- Ordering must be driven by one immutable cross-owner added-order field, not array concatenation, activity, update, view, save, or pin time.
- Pin state must live on one lifecycle item that can change stage and canonical identity.
- “Pinned,” “engaged,” and “dirty” are distinct states. Pin and engagement are retention reasons; dirty controls persistence and discard warnings.
- A pin on an unsaved create session is real immediately.
- Page-local edit state cannot satisfy navigation-safe resumability.
- Preview and edit are stages of one saved-record lifecycle, not unrelated topbar items.
- Confirmation dialogs, destructive alerts, shell search, launchpad, client focus, support dialogs, and security prompts do not become working items.
- Parent-stage dialogs need explicit ownership instead of silently becoming unrelated tabs.
Implemented consolidation
@repo/working-lifecyclenow owns the canonical vocabulary, reducer, policies, immutable order keys, selectors, and invariant tests.OrgWorkingLifecycleProviderowns lifecycle membership, active visibility, canonical deduplication, hide/cancel/remove policy, and storage projections.- Create, quick-note, timer, temporary-record, pinned-record, and edit-dialog entry points register through that provider.
- The topbar projects the compatibility render adapters through the canonical immutable order. No view, edit, save, autosave, or pin action changes order.
- Durable saved pins dual-read/write
pinnedWorkingRecordsV2, containing only canonical identity and immutable order key. A bounded Convex migration backfills those records and removes obsolete backend create-action pins. Its synchronous runner and verifier are wired into both GitHub and container-start Convex deployment paths. - Pure engine, storage, UI integration, schema, and migration tests cover the default lifecycle rules. Feature-specific draft codecs remain feature-owned.