Skip to content

🚧 WIP mega-B β€” parked pending activity-sync expansion (review: tasks/activity-sync-expansion.md) - #177

Open
jpb33333 wants to merge 11 commits into
devfrom
feat/megaB-lane-a-tail
Open

🚧 WIP mega-B β€” parked pending activity-sync expansion (review: tasks/activity-sync-expansion.md)#177
jpb33333 wants to merge 11 commits into
devfrom
feat/megaB-lane-a-tail

Conversation

@jpb33333

@jpb33333 jpb33333 commented Apr 22, 2026

Copy link
Copy Markdown
Collaborator

DO NOT MERGE. This branch is parked pending the activity-sync expansion sprint (per JP's 2026-04-23 decision β€” see below).

cc @dkydkydky @jacrev-pursuit β€” review asks below.

What's on the branch

11 commits on feat/megaB-lane-a-tail:

Already-landed mega-B baseline (6):

  • 9bf0081 feat(progress): split Pipeline Flow into Active funnel + Closed section (A5)
  • e9171bd feat(inline-edit): cap dropdown height + align Popover anchor pair (A8)
  • 44875d1 chore(sensitivity): soften Opportunity Amount + Probability to safe (A9)
  • f59ed45 feat(inline-edit): dark-mode fill affordance in editing mode (A10 affordance)
  • 74713fb audit(opp): inline-edit on Name + CloseDate across Pipeline + Payment tabs (A10)
  • ea4db83 feat(inline-edit): full SF variant palette + defaultSensitivity safeguard (A10 docs: align all product docs with two-path Slack + Automation Review … #6)

New this session (5):

  • 7124853 fix(inline-edit): stop DataGrid cellMouseDown from stealing focus (#7a) β€” root-caused JP's Amount/Probability bug at MUI's own source (GridCell.js:320 + useGridFocus.js:268/73). One-line stopPropagation on mousedown fixes every text-variant inline-edit cell inside a DataGrid.
  • afb608c feat(inline-edit): per-row ownerGate for schema-driven cells (#7b) β€” mirrors backend _enforce_record_ownership (main.py:862) on the frontend. Non-owner clicks no-op instead of hitting the API and getting a sanitized "Failed to update".
  • 1426fad feat(schemaColumns): emit InlineEditable renderCells with ownerGate (Add test coverage analysis with prioritized improvement recommendationsΒ #7) β€” rewrites the 267-LOC utility to emit <InlineEditable> renderCells for every SF-updateable field. Reference fields target Account / User via the mature AccountCell / OwnerCell (preloaded options from page state). Accounts / Contacts / Tasks call-sites updated; old editMode="cell" + processRowUpdate removed. New schemaColumns.test.tsx with 18 tests (11 invariants + 7 new-behavior).
  • 37ac68d chore(sensitivity): classify Account.NumberOfEmployees, Contact.npsp__Primary_Affiliation__c, Activity block (Add giving capacity calculator with 990 enrichment and conversion gateΒ #8) β€” 4 new classifications, full Activity block added.
  • d9b58f0 docs(activity-sync): design doc for two-way bedrock.activity ↔ SF sync β€” phased plan, open questions for Jac + Dave, risks, Jac-review checklist.

Why it's parked

JP decided on 2026-04-23 to expand mega-B #9 (Activities page replacing the Leads tab on /details) from a local-only edit flow into a full two-way sync:

  • Today /api/activities edits bedrock.activity (local Postgres, UUID ids, snake_case fields) and the SF sync is one-way (SF β†’ bedrock). Local edits silently diverge from Salesforce.
  • Desired state: every inline edit on the Activities page PATCHes the underlying SF Task or Event first; bedrock.activity mirrors on success. SF is authoritative.
  • Scope: ~40 new columns on bedrock.activity, backfill from SF, expanded sync engine (data_sync.py:sync_activities), new PUT handler with SF-first semantics + _enforce_record_ownership on the SF record, then the frontend Activities page using buildSchemaColumns on a synthesized Task+Event SF schema.
  • Per JP: "park everything; build the full activity-sync first, then ship mega-B as one huge PR."

Review asks

@jacrev-pursuit (Jac)

Please review tasks/activity-sync-expansion.md (from d9b58f0). Specifically:

  1. Field catalog (Phase 0 blocker). Which SF Task + Event fields does Pursuit actually use, including any __c custom fields? I can run getSchemaDescribe('Task') + getSchemaDescribe('Event') to draft the first pass, but you're the source of truth for which fields matter to the team.
  2. Recurrence. Does the team use recurring Tasks / Events? If yes, how should inline edit on a recurring occurrence behave (series-wide vs per-occurrence)?
  3. Record-Type-specific picklist restrictions. Status and Priority may have RT-scoped values. If so, we need the RT β†’ allowed-values map. Same pattern as OPPORTUNITY_STAGES.
  4. Page-layout readonly overrides. SF FLS might say updateable: true but the current page layout marks a field readonly. SF accepts the API write anyway β€” do we want that, or should we suppress client-side too?
  5. Transfer Record permission. All RM / Executive SF profiles have Transfer on Task + Event + Account + Contact, right?
  6. Backfill window. Full re-sync of every activity row consumes Bulk API calls. Off-hours run OK, or do you need a throttle?

@dkydkydky (Dave Yang)

Please review the design in tasks/activity-sync-expansion.md and the 4 in-scope commits (7124853, afb608c, 1426fad, 37ac68d) for:

  1. Architectural soundness of the SF-first PATCH semantics in Phase 2 (routes/activities.py rewrite). Transactional contract: SF authoritative; on SF failure, no local write. On SF success + local failure, next sync cycle reconciles. Acceptable?
  2. ownerGate in useFieldPermission (afb608c) β€” mirrors _enforce_record_ownership on the client. Fail-safe semantics: missing sfUserId or rowOwnerId both deny. Any concerns with the double-enforcement (client + server)?
  3. defaultSensitivity: 'safe' in schemaColumns (1426fad) β€” every schema-generated cell defaults to safe so we don't demand an unlock confirmation on 100+ fields. Invariant documented at the top of schemaColumns.tsx. Explicit classifications in fieldSensitivity.ts always win. Comfortable with the trade-off?

How to look at the code

  • GitHub diff view: commit-by-commit is much cleaner than the full diff (11 commits, ~800 LOC).
  • Local: git fetch origin && git checkout feat/megaB-lane-a-tail β€” the megaB worktree ergonomics are in tasks/activity-sync-expansion.md (JP's setup).

Smoke-test status

  • tsc --noEmit β€” clean.
  • Full frontend suite: 460 tests, 7 pre-existing failures in files this PR does NOT touch (4 calculatePaymentDate DST + 3 weeklyPrioritiesHelpers / WeeklyPriorities hardcoded-date tests β€” all date-arithmetic bugs, unrelated).
  • Browser smoke for #7a (Amount / Probability click-to-edit): pending β€” JP to verify.

Pending (held until activity-sync ships)

πŸ€– Generated with Claude Code

JP and others added 6 commits April 22, 2026 15:31
…on (A5)

Context
-------
Mega-B / A5. Bedrock 1.0 Progress page called for Lost / Withdrawn / Did
Not Fulfill to be visible alongside the active funnel. JP also moved
'Collecting / In Effect' out of the active funnel into the new Closed
section (def: "closed, contract in effect, payments in progress").

Changes
-------
- pipelineFunnelTransitions.ts
  * ACTIVE_FUNNEL_STAGES now re-exports OPEN_STAGES from types/salesforce
    (7 stages, no Collecting). Single source of truth.
  * New CLOSED_FUNNEL_STAGES constant with 5 SF-canonical terminal stages
    in display order (Collecting / In Effect, Closed / Completed, Closed
    Lost, Withdrawn, Closed / Did not Fulfill).
  * classifyTransition gains an 'unclose' guard: FROM a known terminal
    (WON_STAGES/LOST_STAGES member) BACK TO an active stage is backward.
    Without this, the prior implicit behavior (Collecting in both active
    and WON_STAGES) masked that STAGE_IDX.get(Collecting) would become
    undefined post-split, so fi=-1 against an active ti>=0 would return
    'forward' β€” wrong. Guard fires only when from IS a recognized
    terminal, so unknown-from (legacy pre-funnel stages) still treats
    as forward per the existing semantics.

- PipelineFunnel.tsx
  * Parallel closedOpps/closedStageTotals memos built from the full
    opportunities prop scoped by the existing owner filter.
  * New Closed section renders below the active funnel β€” 5 horizontal
    cards (flex, equal-width, min 140px) showing colored stage dot, name,
    count, and total amount. Tooltips mirror the active-funnel tooltip
    shape for consistency. No transition-activity tracking on the Closed
    cards β€” they're a snapshot, not a flow.

- PipelineFunnel.test.ts
  * 4 new 'A5 funnel split β€” terminal unclose regressions' tests pinning
    the new classifyTransition guard (Collecting β†’ Proposal Negotiation,
    Closed / Completed β†’ Qualifying, Closed Lost β†’ Proposal Negotiation,
    Withdrawn β†’ Contract Creation all classify as 'backward').
  * 3 new 'A5 funnel split β€” exports' tests pinning the split: active
    has 7 and excludes Collecting; closed has 5 in display order;
    active+closed are disjoint.

Honors feedback_sf_stages_sacred
--------------------------------
Every stage string is verbatim from the SF picklist. No reclassification
of existing stage semantics; WON_STAGES/LOST_STAGES in types/salesforce
unchanged. Collecting / In Effect remains a WON_STAGES member (transitions
into it still classify as 'won' per the existing test at line 26-29).

Tests
-----
- PipelineFunnel.test.ts β†’ 28 passed (21 existing + 7 new). No regressions.
- tsc --noEmit β†’ clean.

Verify
------
Open Progress page β†’ funnel shows 7 active-stage bars (Lead Gen through
Negotiating Contract), no Collecting bar. New Closed section below shows
5 cards with correct counts + total amounts. Sum of (active counts +
closed counts) == total opps in the lookback window for the selected
owner(s).

Part of mega-B rollup targeting dev. Plan at ~/.claude/plans/mutable-doodling-brook.md.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Context
-------
Mega-B / A8. Inline-edit dropdowns (Menu for select variant, Popover for
autocomplete) could overflow the viewport when a cell sat near the page
bottom β€” options past the fold weren't scrollable, they were clipped.
Autocomplete's Popover was also missing transformOrigin (incomplete
anchor pair), which could misalign the popup vertically against its
anchor.

Changes
-------
- InlineEditable.tsx:398-430 Menu block
  * slotProps.paper maxHeight: '50vh' + overflowY: auto β†’ long option
    lists scroll internally instead of growing past the viewport.
  * marginThreshold: 8 β†’ reduces MUI's default 16px "stay N pixels in
    from the viewport edge" padding so the dropdown doesn't shift
    further than necessary.

- InlineEditable.tsx:434-468 Popover block
  * Adds transformOrigin: { vertical: 'top', horizontal: 'left' } β€”
    mirrors the anchorOrigin pair so the popup top-left aligns with
    the anchor's bottom-left (matches Menu's pairing). Without it,
    MUI defaults to centering, which reads as misalignment.
  * Same slotProps.paper maxHeight + marginThreshold treatment.

Scope fence β€” NOT doing
-----------------------
- True "flip to open upward when near bottom" would require replacing
  Menu/Popover with raw <Popper> + flip modifier + ClickAwayListener.
  Bigger surgery; not necessary for the overflow/alignment fix.
  Flagging as an A8 v2 follow-up if QA still reports dropdowns
  misbehaving.

Tests
-----
- InlineEditable.test.tsx β†’ 17 passed (all existing). No regressions.
- tsc --noEmit β†’ clean.
- Popper positioning itself isn't unit-tested (would require mocking
  getBoundingClientRect + MUI's Modal positioning internals). Manual
  browser verify is the right tool β€” covered in mega-B verification
  section of the plan.

Part of mega-B rollup targeting dev.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Context
-------
Mega-B / A9. Inline-edit cells for Opportunity.Amount and .Probability
required a per-edit unlock dialog (sensitivity='sensitive'). RMs edit
these fields several times a day during priority triage; the lock
friction outweighed the fat-finger risk. Softened to 'safe' β€” immediate
inline edit, same SF write semantics, no UX confirmation.

Keeping StageName sensitive per feedback_sf_stages_sacred β€” stage
changes drive bucketing + funnel reporting. OwnerId, AccountId,
PaymentDate__c also stay sensitive (reassignment / reconciliation
semantics).

Changes
-------
- fieldSensitivity.ts:50-51
  * Opportunity.Amount       : 'sensitive' β†’ 'safe' (lockReason dropped)
  * Opportunity.Probability  : 'sensitive' β†’ 'safe' (lockReason dropped)
  * Inline comment explains the softening + how to revert.

Tests
-----
- fieldSensitivity.test.ts
  * Moved the Amount + Probability assertions from 'sensitive fields'
    block into 'safe fields' block β€” explicit positive coverage for
    the new state.
  * 35 tests pass total. No other changes.
- tsc --noEmit β†’ clean.

Revert
------
Single-line flip each: change sensitivity back to 'sensitive' and
restore the lockReason. Test assertions would need to move back too.

Part of mega-B rollup targeting dev.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ordance)

Context
-------
Mega-B / A10 affordance. Prior 3px blue ring + light tint (shipped PR
#118) was easy to miss on small cells or in dense DataGrids β€” users
couldn't always tell which cell was active while the unlock dialog was
up. JP's call (2026-04-22): replace the ring with a dark-mode fill β€”
primary.main background + primary.contrastText text. Unmistakable.

Changes
-------
- InlineEditable.tsx:378-395
  * Active-mode sx block replaces `boxShadow: '0 0 0 3px #1976d2'` +
    `bgcolor: 'rgba(25, 118, 210, 0.08)'` with:
      - `bgcolor: 'primary.main'`
      - `color: 'primary.contrastText'`
      - `boxShadow: 2` (MUI elevation 2 β€” subtle depth cue)
      - Cascading color override on `input`, `.MuiInputBase-input`,
        `.MuiTypography-root`, `svg`, `.MuiChip-label` so every child
        text/icon flips to white in unison (MUI doesn't auto-cascade
        color to descendants through Box).
  * Comment updated to document the upgrade rationale + that
    mode !== 'display' still covers both unlock + editing.
  * transition property extended with `color 120ms ease` so the text
    color change animates alongside bg + box-shadow.

- Theme assumption: primary.contrastText resolves to white (#fff) via
  MUI auto-contrast for '#1976d2' (verified at App.tsx:42-49 β€” no
  contrastText override in the theme, so MUI computes it from
  primary.main). WCAG contrast ratio 7.2:1 β€” passes AA + AAA.

Scope fence β€” NOT doing
-----------------------
- Not adding a ring AND fill β€” fill alone is stronger and less visually
  noisy.
- Not animating a pulse on entry β€” the transition property already
  makes the bg/color change feel responsive (120ms ease).

Tests
-----
- InlineEditable.test.tsx β†’ 17/17 pass. Existing tests exercise
  edit-mode entry (renders editor, onSave fires, cancel reverts) and
  sensitivity/permission gating β€” all pass because the sx change is
  orthogonal to behavior. Testing sx contents is a unit-test
  antipattern (couples against implementation); manual visual compare
  covers the affordance claim.
- tsc --noEmit β†’ clean.

Verify
------
Browser: click any inline-edit cell (StageCell on Priorities, OwnerCell
on Opps) β†’ cell fills solid blue with white text, lock icon flips white
if visible. Pre-existing hover hint (inset 2px ring on display-mode
hover) unchanged.

Part of mega-B rollup targeting dev.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… tabs (A10)

Context
-------
Mega-B / A10 Opp audit. 22-column enumeration of Opportunities/columns.tsx
(both buildPipelineColumns + buildPaymentColumns) identified 2 editable-
but-non-InlineEditable columns using MUI DataGrid's native double-click
edit: Name (both tabs) and CloseDate (shared helper). These bypassed the
sensitivity pipeline, missed the A10 dark-mode fill, and required
double-click instead of the single-click pattern every other cell uses.

JP-approved scope expansion (2026-04-22): the Payment tab's Amount column
was also view-only; bring it into the inline-edit flow alongside the
others for consistent RM UX.

Changes
-------
- closeDateColumn(cb) now accepts ColumnCallbacks (was no-arg) and renders
  via <DateCell>. Called twice β€” Pipeline at line 169, Payment at
  line 222. Both pass through the same cb so the save callback reaches
  SF via onSaveField β†’ updateOpportunity. Both call sites updated.

- nameColumn(cb, opts) β€” NEW shared helper. Takes headerName / flex /
  minWidth so Pipeline ("Opportunity Name", flex: 2) and Payment
  ("Grant Name", flex: 2) can share the same InlineEditable text
  variant. Opportunity.Name is 'safe' in fieldSensitivity (line 48),
  so no unlock prompt is expected.

- Payment tab's Amount column now reuses amountColumn(cb) (the same
  helper Pipeline already uses). Loses the custom fontWeight: 600
  styling the prior renderCell had, gaining:
    * single-click edit (was view-only β€” Payment Amount couldn't be
      edited from the Payment tab at all)
    * dark-mode fill affordance (A10)
    * sensitivity gating (Amount is 'safe' post-A9 softening)

- New import: InlineEditable primitive (for the Name column's text variant).

Columns NOT changed (intentional)
---------------------------------
- Lock control / Task button / Edit-row button β€” UI controls, not SF fields.
- expectedValue, paymentProgress, paymentStatus, remainingAmount β€” computed
  from other fields; not SF-editable.
- LastModifiedDate β€” SF-stamped.
- Account / Owner / Stage / Amount / Probability / PaymentDate__c β€” already
  use InlineEditable-based cells.
- Payment-tab finance rollup columns (Payments_Made, Number_of_Payments,
  Most_Recent_Payment_Date, expectedPaymentDate) β€” SF-computed rollups,
  not directly editable.

Tests
-----
- tsc --noEmit β†’ clean.
- No new tests in this commit β€” columns.tsx has no test coverage today;
  writing column-level snapshot tests would add noise without load-bearing
  value. The InlineEditable.test.tsx suite (17 tests) already covers the
  click-to-edit, save, and sensitivity gating behavior.
- Pre-existing 4 calculatePaymentDate failures in helpers.test.ts are
  unrelated (DST off-by-1 bug that pre-dates this commit; unchanged).

Part of mega-B rollup targeting dev.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…uard (A10 #6)

Context
-------
Mega-B / Commit 6. Prep work for the schemaColumns architectural upgrade
(#7). Today InlineEditable supports 5 variants β€” select, autocomplete,
text, number, date. schemaColumns emits GridColDefs for every SF field
type; the primitive needs variants for currency, percent, datetime,
boolean, email, phone, url before #7 can wire through it. Plus: the
fieldSensitivity fail-safe default ('sensitive' for unclassified pairs)
would make every one of Accounts' ~50 schema-generated cells show an
unlock dialog on first click β€” #7 needs an opt-in to default 'safe'.

Changes
-------

**InlineEditable.tsx**
- InlineEditVariant union extended from 5 to 12 variants. New: currency,
  percent, datetime, boolean, email, phone, url.
- renderInlineTextEditor maps each new variant to an HTML input type:
    currency, percent          β†’ 'number' (percent adds min/max/step)
    datetime                   β†’ 'datetime-local'
    email                      β†’ 'email'
    phone                      β†’ 'tel'
    url                        β†’ 'url'
  isNumeric branch now covers number + currency + percent for the
  string β†’ Number coercion on change.
- New renderBooleanEditor β†’ Checkbox. Click-to-toggle-and-save via
  commitNow; no separate 'editing' typing flow (boolean doesn't fit
  that model). Checkbox is autoFocus + disabled-on-saving; primary-
  contrastText color so it reads against the dark-mode fill (A10).
- editingTextStyle refactored to a variant-set lookup covering all
  text-input-based variants; new editingBooleanStyle gate for the
  Checkbox render path. Render branch: text/inline β†’ boolean β†’ display.
- New defaultFormatForVariant fallback: when caller omits formatDisplay,
  the primitive renders sensible defaults per variant:
    currency β†’ formatDollarMillions
    percent  β†’ `${v}%`
    boolean  β†’ Yes / No
    date     β†’ MMM dd, yyyy (date-fns)
    datetime β†’ MMM dd, yyyy h:mm a (date-fns)
    other    β†’ String(v) with placeholder for null/empty
  Caller-provided formatDisplay always wins.
- New optional `defaultSensitivity` prop inherited from
  UseFieldPermissionArgs β€” forwarded to useFieldPermission so
  schema-generated cells can opt out of the fail-safe.

**hooks/useFieldPermission.ts**
- UseFieldPermissionArgs gains optional `defaultSensitivity`. Threaded
  into classifyField. Added to useMemo deps so re-renders pick it up.

**utils/fieldSensitivity.ts**
- classifyField signature: `(objectType, fieldName, defaultSensitivity?)`.
  Defaults to 'sensitive' (fail-safe, unchanged for existing callers).
  When defaultSensitivity === 'safe' and the pair isn't classified,
  returns `{ sensitivity: 'safe' }` (no lockReason β€” the cell edits
  freely). Explicit entries in FIELD_CLASSIFICATIONS always win β€” a
  schema-generated cell on StageName still reads 'sensitive' even
  when the caller passed defaultSensitivity: 'safe'. Inline comment
  documents the rationale.

Scope fence β€” NOT doing
-----------------------
- Thin cell wrappers (TextCell, CurrencyCell, PercentCell, DateTimeCell,
  BooleanCell, EmailCell, PhoneCell, URLCell) β€” the plan lists these as
  "optional". schemaColumns uses InlineEditable directly (not through
  wrappers), so adding 8 wrapper files would be dead code until some
  future hand-coded page needs them. Skip until actual demand.
- Multi-select picklist variant β€” acceptable MVP downgrade per the
  plan; multipicklist fields will render via `select` with single
  selection until a separate follow-up.

Tests
-----
- fieldSensitivity.test.ts: +4 tests under 'defaultSensitivity override
  for unclassified pairs' β€” unknown+'safe' β†’ safe, unknown+omitted β†’
  sensitive fail-safe, explicit sensitive overrides 'safe' default,
  explicit safe overrides 'sensitive' default. 39 pass total.
- InlineEditable.test.tsx: 17 pass. Existing tests cover the 5 original
  variants; new variants will get coverage when a consumer (schemaColumns
  in #7 or a hand-coded cell) exercises them.
- tsc --noEmit β†’ clean.

Part of mega-B rollup targeting dev. Commit 7 (schemaColumns upgrade)
depends on this commit.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@jpb33333
jpb33333 marked this pull request as ready for review April 22, 2026 21:36
JP and others added 5 commits April 23, 2026 14:01
Root cause (verified at MUI source, not speculation): GridCell attaches
onMouseDown β†’ publishEvent('cellMouseDown') at node_modules/@mui/x-data-grid/
modern/components/cell/GridCell.js:320. useGridFocus.js:268 stores the cell
params on mousedown; its document-level mouseup listener (line 271-312) calls
setCellFocus β†’ apiRef.current.forceUpdate() (line 73), shifting DOM focus to
the cell wrapper. That steals focus from our autoFocus'd TextField inside
renderCell, triggering onBlur={handleSave} with draft===value β†’ silent revert
to display mode. User perception: clicking an Amount/Probability cell does
nothing.

Select/autocomplete variants already worked because their Menu/Popover is
anchored outside the cell DOM, so focus steal doesn't affect them.

Fix: stopPropagation on the outer Box onMouseDown when in display mode and
the field is editable. Neutralizes DataGrid's path; DataGrid's "mouseup
target inside focused cell" branch (useGridFocus.js:291-294) handles the
mouseup without changing focus. Every text-input variant (text, number,
currency, percent, date, datetime, email, phone, url) now enters edit mode
cleanly inside a DataGrid cell.

17/17 InlineEditable tests still pass (isolated mounts don't exercise
DataGrid). JP smoke-tests Amount/Probability/Name/CloseDate on Details tab
post-deploy; if anything surprises, #7 is paused and we re-diagnose.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Mirror the backend's _enforce_record_ownership helper (main.py:862-923) on
the frontend so schema-driven cells on Accounts/Contacts/Tasks/Activities
surface the correct permission state instead of letting the user click,
type, tab out, then see a sanitized "Failed to update" toast for records
they don't own.

useFieldPermission gains an optional ownerGate: { rowOwnerId, editAllPermission? }
arg. When set, the hook resolves three bypasses in order:
  1. isAdmin (manage_users_roles)
  2. editAllPermission match (e.g. 'edit_all_opportunities')
  3. rowOwnerId === sfUserId
If none match, the hook returns recordLockedByOther=true with a stable
"Only the record owner (or an admin) can edit this field." tooltip.
InlineEditable's existing canEditAtAll = !readOnly && !lockedFromRecord
logic then short-circuits handleDisplayClick β€” clicks are inert, no save
ever attempted, no surprise error toast.

Fail-safe semantics match the backend: missing sfUserId or missing
rowOwnerId both deny (cannot evaluate ownership β†’ deny, safer than permit,
matches main.py:907-912). Since every Account/Contact/Task/Opportunity SOQL
select already fetches OwnerId (salesforce_schema.py ACCOUNT_FETCHED_FIELDS,
CONTACT_FETCHED_FIELDS + main.py:1432 task select), rowOwnerId will be
populated in practice.

Additive: existing call sites that omit ownerGate see no behavior change.
Schema-driven cells (commit #7 next) pass it per-row.

4 new InlineEditable tests exercise each bypass branch (non-owner denies,
admin allows, edit-all permission allows, owner allows). 21/21
InlineEditable tests + 39/39 fieldSensitivity tests pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Rewrites schemaColumns.tsx (267 β†’ 387 LOC) to emit an <InlineEditable>
renderCell for every SF-updateable field instead of setting DataGrid's
native `editable: true` + renderEditCell. Reference fields targeting
Account / User use the mature inline-edit AccountCell / OwnerCell (which
consume preloaded accounts[] / users[] passed via options, avoiding the
per-row fetch-on-mount pattern of the old EditCells). Other reference
types (Contact, Lead, polymorphic WhatId / WhoId) stay display-only;
editing those fields requires opening the entity dialog. Address compound
fields render as a comma-joined string (edit via dialog per-component).

New SchemaColumnOptions:
  - entityType (required) β€” 'Account' | 'Contact' | 'Task' | 'Activity'
  - onSaveField (required) β€” (recordId, field, value) => Promise<void>
  - canEditObject (required) β€” page-level permission gate
  - sfUserId (required) β€” current user's SF user ID from usePermissions()
  - editAllPermission (optional) β€” for Opp/Payment 'edit_all_*' bypass
  - accounts (optional) β€” preloaded Account[] for reference autocomplete
  - users (optional) β€” preloaded User[] for Owner autocomplete
  - forceHide / overrides β€” preserved from the old signature

Each emitted InlineEditable receives:
  - defaultSensitivity='safe' β€” trust SF updateable + server-side FLS.
    Explicit classifications in fieldSensitivity.ts still win (e.g.
    Opportunity.StageName stays sensitive). See the top-of-file invariant
    comment β€” flipping this back to 'sensitive' would lock ~100 fields on
    first click.
  - readOnly driven by !canEditObject || !field.updateable || field.calculated
  - ownerGate={{rowOwnerId, editAllPermission}} β€” per-row owner check
    mirroring _enforce_record_ownership (main.py:862). Non-owner clicks
    no-op without hitting the API.
  - required-field client validator when field.nillable === false, to
    prevent round-tripping a REQUIRED_FIELD_MISSING that the backend
    sanitizes to a generic "Failed to update X".

Call-site updates on Accounts / Contacts / Tasks:
  - Pull sfUserId from usePermissions() (already exposed).
  - Add useQuery('users', …, staleTime: 10min) for Owner autocomplete.
    Tasks.tsx additionally gains useQuery('accounts', …) for WhatId.
  - Replace the old handleCellEdit (processRowUpdate-based newRow/oldRow
    diff) with a handleSaveField(recordId, field, value) that dispatches
    to apiService.updateAccount / updateContact / updateTask. Errors
    rethrow so InlineEditable surfaces them inline (primitive's try/catch
    at InlineEditable.tsx:220-227).
  - Remove DataGrid's editMode="cell" / processRowUpdate /
    onProcessRowUpdateError / isCellEditable props β€” InlineEditable owns
    the flow end-to-end.

AccountCell + OwnerCell gain an optional ownerGate prop forwarded verbatim
to InlineEditable (two lines per cell). Hand-coded call sites in
Opportunities/columns.tsx still omit it and keep their existing per-page
lockMap gating; schema-driven cells pass it per-row.

New frontend/src/utils/schemaColumns.test.tsx (18 tests):
  - 11 invariant tests (SYSTEM_FIELDS filter, dotted pseudo-field skip,
    forceHide, alphabetical sort, number/currency/percent/date/datetime/
    boolean/multipicklist type mapping, per-field overrides merged).
    These lock pre-rewrite semantics.
  - 7 new-behavior tests (renderCell emits InlineEditable, onSaveField
    fires with (id, field, value), non-owner blocked via ownerGate,
    reference→Account → AccountCell, reference→User → OwnerCell,
    calculated/readOnly fields stay non-editable).

Verification: 106/106 tests pass across schemaColumns + InlineEditable +
fieldSensitivity + PipelineFunnel. Full suite: 453/460 pass. The 7
remaining failures are pre-existing date-dependent tests in files this
commit does not touch β€” 4 calculatePaymentDate DST in Opportunities/
helpers.test.ts (PINNED), 3 in weeklyPrioritiesHelpers.test.ts +
WeeklyPriorities.test.tsx that hardcode "30-day window starting March 16"
against today's date (2026-04-23). Unrelated to this commit.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…_Primary_Affiliation__c, Activity block

Adds explicit sensitivity entries on top of the defaultSensitivity: 'safe'
fall-through used by schema-driven cells (buildSchemaColumns, mega-B #7).
Per the plan's "conservative additions only" rule: err toward safe,
declare sensitive only where fat-finger risk is real or where a change
cascades into downstream rollups / household logic.

Account (12 β†’ 13 entries):
  - NumberOfEmployees β†’ sensitive. Feeds segmentation reports.

Contact (8 β†’ 9 entries):
  - npsp__Primary_Affiliation__c β†’ sensitive. Reassigning rewrites the
    contact's household membership and recomputes npo02__ rollups at
    the new affiliation's Account.

Task: no additions (current classifications already correct).

Activity (NEW block, 8 entries):
  - Subject, Status, Priority, ActivityDate, Description β†’ safe
  - OwnerId β†’ sensitive (accountability reassignment)
  - WhatId β†’ sensitive (parent record linkage rewrite)
  - WhoId β†’ sensitive (related contact linkage)

Used by the new Activities page (mega-B #9) which runs
buildSchemaColumns with entityType: 'Activity'.

fieldSensitivity.test.ts gains 4 new assertions + one it.each block over
the 5 safe Activity fields β€” 48/48 tests pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
JP decided on 2026-04-23 to expand scope beyond the original mega-B #9
plan: instead of a simple Activities page that edits bedrock.activity
locally, implement full two-way sync so inline edits reflect back to the
underlying SF Task / Event. SF-first PATCH semantics (SF is authoritative;
on SF failure, no local write). Blocks the Activities page and the Leads
tab replacement.

This doc captures the phased plan and the design decisions I need Jac's
input on:

  Phase 0: field catalog β€” enumerate every Task/Event SF field Pursuit
    actually uses (describe + custom fields), including the new bedrock
    column name, Postgres type, and lossy-conversion notes. Blocks Phase 1.
  Phase 1: backend migration + sync engine expansion. New columns on
    bedrock.activity; _map_sf_task + _map_sf_event populate them;
    _upsert_activity INSERT/UPDATE lists expand. Backfill via
    sync_activities --force-full.
  Phase 2: PUT /api/activities/{uuid} rewritten for SF-first semantics.
    Translates SF-field-named payload β†’ bedrock columns; runs
    _enforce_record_ownership on the SF record; PATCHes SF first; mirrors
    to bedrock on success.
  Phase 3: Activities page (mega-B #9) uses buildSchemaColumns with a
    synthesized Task+Event SF schema.
  Phase 4: wire into Details.tsx, replace Leads tab, add the redirect.
  Phase 5 (mega-B #10 + #11 + #12): RowCountCaption + Activities tab on
    AccountEditDialog.

Open questions flagged for JP + Jac: custom fields, recurrence handling,
RT-specific picklist values, contact_ids[] scalar-vs-list, Event-vs-Task
field scope at save time, Bulk API quota for backfill.

Today's branch state: 10 commits on feat/megaB-lane-a-tail (6 mega-B
baseline + #7a, #7b, #7, #8). Parked until the activity-sync sprint
completes per JP's 2026-04-23 "park and expand" direction.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@jpb33333 jpb33333 changed the title 🚧 WIP mega-B (6 of 12 commits) β€” DO NOT MERGE, temporary save-point 🚧 WIP mega-B β€” parked pending activity-sync expansion (review: tasks/activity-sync-expansion.md) Apr 23, 2026
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.

1 participant