# Issue #2150 Frontend Gating Execution Plan

## Status

- Type: `staged closure blueprint`
- Readiness: `execution-ready for in-scope buckets`
- Primary issue: `#2150 refactor(frontend): move business gating out of generic pages`

## 1. Problem To Close

`frontend/src/pages/**` still contains generic-page read paths that branch on
business objects, business actions, or profile literals. That keeps tenant or
business rules in public frontend runtime instead of in server-driven metadata,
effective permissions, or private overlay ownership.

This issue is not closed by deleting a few `if` statements. It closes only when:

1. generic page runtime stops deciding business action availability itself
2. the frontend reads one authority for each gate
3. tenant/business behavior lands in a destination that does not pollute the
   next tenant

## 2. Scope Classification

### In scope

1. `ObjectView` related-list ordering that still special-cases business objects
2. `ObjectView` detail-header action gating that still special-cases
   `Opportunity`
3. `RecycleBin` scope gating that still special-cases `system_admin`
4. the adjacent frontend read paths needed to keep those page fixes honest:
   `MetadataRecordDetail`, `ActionRenderer`, `ActionRegistry`,
   `TaskQuickCreateModal`, `useActivities`, and recycle-bin API helpers

### Out of scope

1. backend implementation changes
2. tenant-private overlay content itself
3. broader extraction of all business fields from shared activity components
   unrelated to the page-entry gating path
4. generic plugin system completion outside the minimal action-modal surface
   needed by this issue

### Current-stage closure claim

This document does **not** claim full frontend/business decoupling closure. It
freezes the execution contract for the gating hotspots currently still owned by
generic pages.

## 3. Current State

### 3.1 `ObjectView` still carries business object gating

Current repo evidence:

- `frontend/src/pages/ObjectView.tsx:116` sorts `PageLayout.relatedLists`
  only when `objectApiName` is `quote`, `order`, or `contract`, so runtime
  layout output is rewritten by a business-object branch.
- `frontend/src/pages/ObjectView.tsx:376` applies that sort during layout
  normalization, which means the page layout is not the final runtime
  authority.
- `frontend/src/pages/ObjectView.tsx:657` adds a `Weekly Updates` button
  only when `normalizedObjectApiName === 'opportunity'`.

### 3.2 `ObjectView` already shows the correct shape for one action path

Current repo evidence:

- `frontend/src/pages/ObjectView.tsx:211` fetches standard actions from the
  server, filters them by `targetObjects`, and treats action visibility as
  metadata-owned.
- `frontend/src/pages/ObjectView.tsx:705` passes the full action entry to
  `ConvertLeadModal`, keeping page-level visibility separate from action-local
  UI details.
- `frontend/src/infrastructure/api/actions.ts:87` documents the frontend
  contract for `targetObjects`, `endpoint`, and `inputGuards`.

This is the positive local baseline: generic pages can already be blind to
action-specific business rules when the server owns availability.

### 3.3 `RecycleBin` still branches on profile literal and ignores its own scope contract

Current repo evidence:

- `frontend/src/pages/RecycleBin.tsx:23` models scope as `'mine' | 'all'`.
- `frontend/src/pages/RecycleBin.tsx:100` derives admin status from
  `user?.ProfileId === 'system_admin'`.
- `frontend/src/pages/RecycleBin.tsx:158` disables the `all` option from
  that profile literal.
- `frontend/src/components/Router.tsx:133` already gates page access by
  `ViewSetup`, so page access and page-internal scope authority are split.
- `frontend/src/infrastructure/api/data/recycleBin.ts:19` accepts `scope`
  but explicitly ignores it.
- `frontend/src/infrastructure/api/data/recycleBin.ts:33` always issues
  `queryAll` against deleted rows, which means the client is not the right
  authority for org-wide recycle-bin exposure.

### 3.4 The current weekly-update flow is business-shaped, not generic

Current repo evidence:

- `frontend/src/features/activity/components/TaskQuickCreateModal.tsx:31`
  hardcodes a `weekly-update` mode with `Progress__c`, `Next_Milestone__c`, and
  `Risk_and_Next_Step__c`.
- `frontend/src/features/activity/components/TaskQuickCreateModal.tsx:143`
  adds weekly-update-only read-only context fields.
- `frontend/src/features/activity/components/TaskQuickCreateModal.tsx:175`
  auto-fills `Updated_By__c`-style data for that mode.
- `frontend/src/features/activity/hooks/useActivities.ts:15` treats
  weekly-update custom fields as part of the shared activity record shape.
- `frontend/src/features/activity/components/ActivityTimeline.tsx:207`
  renders `Weekly Updates` as a generic activity action button, not as a
  metadata-owned or overlay-owned capability.

### 3.5 There is already a generic record-detail action insertion point, but not a usable modal destination

Current repo evidence:

- `frontend/src/features/record/components/MetadataRecordDetail.tsx:385`
  has a generic header action area: `extraActions` plus metadata-driven
  `actions`.
- `frontend/src/features/record/components/ActionRenderer.tsx:77` executes
  actions through `ActionRegistry`.
- `frontend/src/registries/ActionRegistry.ts:62` defines modal/custom action
  handling, but it currently fails closed with "plugin system not available".

This means the public page already has an insertion point, but not yet a
finished generic destination for tenant-specific modal behavior.

## 4. Salesforce Baseline

The relevant Salesforce product boundary is:

1. record-detail related lists are layout-owned, not object-branch-owned
2. record actions are surfaced from layout/action metadata and permission
   evaluation, not from `if objectName == ...` inside the generic record page
3. admin-only or org-wide capabilities are permission-owned, not
   `ProfileId === ...` literals
4. tenant/org-specific business quick actions may exist, but they are authored
   as metadata/customization, not embedded in the generic page runtime

This repo already aligns with that shape in some places:

- `docs/design/related-list-curation-design.md:37` says runtime must
  read `PageLayout.RelatedLists` and that selected related lists are the only
  runtime definition of business relevance.
- `docs/design/lead-convert-phase1-metadata-driven-design.md:658` says
  action permissions and guards must flow through generic permission helpers and
  metadata, never profile literals or tenant names.

## 5. Candidate Paths

### Path A: delete the current `if` branches and accept behavior loss

Rejected.

Why rejected:

1. it would remove symptoms without naming the replacement authority
2. `Weekly Updates` and recycle-bin scope would just reappear in another page
   branch
3. it does not answer where the next tenant should author equivalent behavior

### Path B: move every remaining business gate into a private React override only

Rejected as the primary solution.

Why rejected:

1. it cleans the public page, but still leaves runtime authority duplicated on
   the client
2. page visibility would still not come from one metadata/permission read path
3. the next tenant would need more private code rather than metadata when they
   need a new action or scope rule

### Path C: split authority by concern

Chosen.

Authority split:

1. record-detail action availability: server-driven metadata or effective
   permission read path
2. record-detail tenant/business behavior UI: private overlay component only
   after a generic public action slot exists
3. related-list ordering/default prominence: persisted layout order in runtime;
   private overlay only seeds layout defaults
4. recycle-bin scope: server-enforced capability + effective-permission read
   path, never profile literal

Why chosen:

1. it reuses existing public substrates instead of inventing object-specific
   page branches
2. it keeps generic pages blind to tenant/business instances
3. it gives the next tenant either metadata authoring or private overlay
   authoring, but not public-core pollution

## 6. Target Closure State

### 6.1 Generic page runtime

`frontend/src/pages/ObjectView.tsx` and `frontend/src/pages/RecycleBin.tsx`
must be reduced to generic orchestration:

1. read layout
2. read action availability / permissions
3. hand off to generic renderer or overlay-registered component
4. never branch on `Opportunity`, `Quote`, `Order`, `Contract`, or
   `system_admin`

### 6.2 Runtime authority by hotspot

| Hotspot | Current authority | Required authority |
|---|---|---|
| Related-list file-first ordering for Quote / Order / Contract | `ObjectView` branch | persisted `PageLayout.RelatedLists` order at runtime; private overlay layout seeds for tenant/business defaults |
| `Weekly Updates` button visibility | `ObjectView` branch on `Opportunity` | server-driven action availability in the record-detail action path |
| `Weekly Updates` modal fields/defaults | public shared component hardcodes business fields | private overlay action behavior or private overlay modal component behind a generic action slot |
| Recycle-bin `all` scope visibility | `ProfileId === 'system_admin'` | effective system permission read path |
| Recycle-bin `all` scope data access | client-side convention only | server-enforced scope contract |

## 7. Landing Zone Decisions

### 7.1 Related-list curation: private overlay seed, not runtime sort

Chosen destination:

1. runtime authority: `PageLayout.RelatedLists` order only
2. authoring destination: layout editor / layout metadata
3. business-specific default ordering: private overlay-owned layout seeds

Why:

- `docs/design/related-list-curation-design.md:80` already says heuristics
  may help seed defaults but must not remain a runtime discovery path
- the current `quote/order/contract` sort in `ObjectView` is exactly the
  runtime patch that the related-list design is trying to eliminate

Execution implication:

- remove `prioritizeFilesRelatedList(...)` from the public runtime path
- if PingCAP or another tenant still needs "Files first" on certain objects,
  seed the affected layouts in the private overlay or through admin-edited
  layouts

### 7.2 Weekly Updates: server-driven availability plus private overlay behavior

Chosen destination:

1. runtime authority for button presence: record-detail action metadata
2. frontend read path: generic detail-action renderer
3. business-specific modal/body behavior: private overlay registration behind a
   generic modal action slot

Why this split:

- the public page must not own `Opportunity -> Weekly Updates`
- the current shared task modal still hardcodes tenant/business fields, so it
  is not a valid public-core destination
- `MetadataRecordDetail` already has a metadata-driven action area, which is a
  better public insertion point than `ObjectView.extraActions`
- `frontend/src/features/activity/components/ActivityTimeline.tsx:207` means
  Bucket B is not done until the shared activity CTA stops reintroducing the
  same weekly-update business gate through a second public read path

Required public-core substrate:

1. a usable modal-capable action path in `ActionRenderer` / `ActionRegistry`
   instead of the current "plugin system not available" stub
2. action placement on record detail driven from one action source, not from
   `ObjectView.extraActions`

Required private destination:

1. the weekly-update modal implementation
2. any custom-field handling such as `Progress__c` or `Updated_By__c`
3. any tenant-specific task defaults or copy

### 7.3 Recycle Bin: effective permission plus server-enforced scope

Chosen destination:

1. page visibility remains permission-gated by router/setup rules
2. `mine` vs `all` capability comes from effective system permissions, not
   `ProfileId`
3. actual `all` scope data access is enforced server-side

Preferred permission rule:

1. allow `all` listing only when the effective-permission payload shows a
   global data capability such as `ViewAllData` or `ModifyAllData`
2. require the server to reject `all` scope when the caller lacks that
   capability

Reasoning:

- org-wide recycle-bin visibility is a global capability, not a profile-name
  convention
- `frontend/src/infrastructure/api/data/recycleBin.ts:19` already proves the
  client `scope` parameter is not authoritative today

### 7.4 Recycle Bin restore/purge: deleted-entry authority, not live-record access parity

Declared delta:

1. restore/purge authorization does not reuse the active-record mutation gate
   of `CheckObjectPermissionOnRecord + CheckRecordAccess`
2. instead, restore/purge authority is owned by the recycle-bin entry plus the
   backing object's object-level capability:
   - caller must be allowed to see the recycle-bin entry
   - `restore` additionally requires object-level `edit`
   - `purge` additionally requires object-level `delete`
3. current active-record read/edit/delete visibility is not re-evaluated as a
   second gate for recycle-bin restore/purge

Surface classification:

1. this is a strict-parity surface because it maps to Salesforce's Recycle Bin
   visibility, `undelete`, and `emptyRecycleBin` product contract

Why this is the correct owner:

1. the deleted-entry authority fact lives on `_System_RecycleBin.DeletedBy`
   plus effective org-wide permissions, not on the live record's current
   sharing state
2. Salesforce's documented contract distinguishes "records you deleted" from
   current live-record access; restore/purge is controlled by recycle-bin
   visibility and org-wide authority, not by re-running the current sharing
   graph after delete
3. reusing active-record `CheckRecordAccess` here would create a product
   regression against the frozen contract: a user could see their own recycle
   bin entry in `mine`, yet lose restore/purge solely because live sharing
   drifted after deletion

Salesforce baseline:

1. standard users can view and restore records they personally deleted
2. org-wide recycle-bin actions require a global capability such as
   `ModifyAllData`
3. no repo evidence or Salesforce evidence in this execution line shows an
   additional requirement to hold current backing-record access at restore or
   purge time

Repo evidence checked:

1. `frontend/src/pages/RecycleBin.tsx` now renders scope/actions from
   server-returned capability and item visibility instead of `ProfileId`
2. `frontend/src/infrastructure/api/data/recycleBin.ts` treats the list API as
   the authority for scope and records
3. `backend/internal/features/records/recycle_bin.go` owns recycle-bin list,
   restore, and purge enforcement from the deleted-entry path
4. `backend/internal/features/records/persistence_delete.go` and
   `backend/internal/features/records/persistence_service.go` still keep the
   live-record object+record access pair for normal delete/update mutations;
   that gate remains the owner for active-record CRUD, not recycle-bin entry
   disposal

Next-tenant impact:

1. greenfield orgs inherit the same metadata-driven split:
   - live-record CRUD stays on the existing object+record access path
   - recycle-bin restore/purge stays on deleted-entry visibility plus object
     permission
2. no tenant-specific profile or object-name branch is introduced

Verification:

1. `tests/e2e-api/cases/recycle_bin/rb-001-restore-by-entry-id.json`
   exercises restore from the recycle-bin entry path
2. `tests/e2e-api/cases/recycle_bin/rb-002-list-mine-scope.json`
   verifies `mine` visibility is tied to deleted-entry ownership
3. `tests/e2e-api/cases/recycle_bin/rb-003-purge-by-entry-id.json`
   exercises purge through the recycle-bin entry path
4. backend unit/handler coverage in
   `backend/internal/features/records/recycle_bin_test.go` and
   `backend/internal/interfaces/rest/recycle_bin_handler_test.go`
   verifies permission-owned scope and mutation enforcement

## 8. Execution Buckets

### Bucket A: remove runtime related-list business ordering from `ObjectView`

Changes:

1. delete the `quote/order/contract` runtime sort path
2. keep layout normalization generic
3. verify the desired file-first order through layout metadata instead

Depends on:

1. no new public substrate
2. tenant-private or admin-authored layout order for any affected org

Verification:

1. fresh org: no special object-name branch needed for related lists
2. tenant overlay org: file-first order only when the layout metadata says so
3. grep: no `quote|order|contract` business ordering branch remains in
   `frontend/src/pages/ObjectView.tsx`

### Bucket B: cut `Weekly Updates` out of generic page action gating

Changes:

1. stop rendering `Weekly Updates` from the `Opportunity` branch in
   `ObjectView`
2. move the button to the generic detail-action surface
3. make button presence come from one action-availability read path
4. move weekly-update-specific modal logic out of public page/shared activity
   paths into the private destination

Depends on:

1. completing the modal-capable generic action surface
2. deciding whether the action source is the existing metadata action path or
   the standard-action path; do not leave both active

Verification:

1. `ObjectView` has no `normalizedObjectApiName === 'opportunity'` branch for
   detail-header actions
2. an org without overlay metadata renders no weekly-update business action
3. an overlay-enabled org gets the action from metadata/registration, not from
   page code
4. before reusing the current action path, recheck
   `frontend/src/registries/ActionRegistry.ts:62` on the implementation branch;
   Bucket B cannot assume the current modal stub has already become a usable
   destination

### Bucket C: replace `RecycleBin` profile gating with permission gating

Changes:

1. remove `user?.ProfileId === 'system_admin'`
2. derive scope capability from `usePermissions()` / effective-permission data
3. wire the recycle-bin data API to a server-enforced scope contract

Depends on:

1. backend scope endpoint or equivalent server-enforced contract
2. effective permission availability for the chosen global capability

Verification:

1. grep: no `ProfileId === 'system_admin'` remains in `frontend/src/pages`
2. two users with identical effective permissions but different profiles see
   the same scope options
3. forced client-side `scope=all` without permission fails on the server

## 9. Sub-issue Split

`#2150` should execute as at least three tracked buckets, even if they share one
PR line:

1. `#2150-A` remove runtime related-list business ordering from `ObjectView`
2. `#2150-B` move weekly-update action placement and business modal behavior out
   of generic page paths
3. `#2150-C` replace recycle-bin profile gating with permission-owned scope

If separate issues are opened, `#2150-B` is the one most likely to need a
design-linked dependency note because it touches the generic action slot.

## 10. Declared Delta From Salesforce

Declared delta:

1. NexusCRM may use a private overlay component for tenant/business-specific
   action UI instead of Salesforce's exact quick-action implementation
2. that mechanism delta is acceptable only if the public page still reads
   action visibility from one metadata/permission authority and the private
   behavior is not hardwired into public generic pages

Why justified:

1. the product-boundary requirement is metadata/permission-owned availability,
   not UI implementation identity
2. the repo already uses registries and overlay-style component destinations;
   extending that is cheaper than cloning Salesforce quick-action internals

Not allowed under this delta:

1. a public `ObjectView` branch on `Opportunity`
2. a public `RecycleBin` branch on `system_admin`
3. a second public runtime sort for quote/order/contract related lists

## 11. Next-Tenant Impact

If this plan is implemented as written:

1. the next tenant can add or omit a business detail action without changing
   public generic pages
2. the next tenant can choose different related-list prominence through layout
   metadata or private overlay seeds, not runtime code
3. the next tenant does not inherit PingCAP-shaped recycle-bin or weekly-update
   assumptions from public frontend pages
4. public-core extraction work stops needing to audit these three business gates
   repeatedly in `ObjectView` and `RecycleBin`

If this issue is "fixed" only by moving code around inside public frontend, the
next tenant still inherits the pollution.

## 12. Verification Plan

### Static checks

1. `rg -n "ProfileId === 'system_admin'|normalizedObjectApiName === 'opportunity'|\\['quote', 'order', 'contract'\\]|Weekly Updates" frontend/src/pages --glob '!**/*.test.tsx'`
   must not show the old page gating patterns after implementation, except for
   allowed standard/setup references unrelated to these buckets
2. no new `ProfileId ===` or business-object gating appears in public page code

### Behavioral checks

1. fresh org without private overlay:
   - no `Weekly Updates` business action on generic record detail unless public
     metadata explicitly provides it
   - related-list order follows layout metadata only
   - recycle-bin scope does not expose `all` without the chosen effective
     permission
2. overlay-enabled org:
   - weekly-update action appears through the chosen action source
   - related-list prominence comes from seeded layout rows
3. permission variation:
   - two users with the same effective permissions but different profiles get
     the same recycle-bin scope behavior

### Review gate checks

1. Greenfield Test: a fresh org with no tenant overlay must still render
   `ObjectView` and `RecycleBin` correctly
2. Admin Self-Service Test: changing related-list prominence or action presence
   must not require editing public generic pages
3. Two-Path Consistency Test: action availability and recycle-bin scope must
   each have one read authority, not one page-level branch plus one server path

## 13. Frozen Contract

Frozen for implementation:

1. public generic pages must not own business object gating for these hotspots
2. related-list runtime order is layout-owned
3. weekly-update button presence is action-authority-owned
4. weekly-update business UI lands outside public generic pages
5. recycle-bin org-scope capability is permission-owned, not profile-owned

Not frozen yet:

1. whether `#2150-B` reuses the existing metadata action path or standard-action
   path as the single action source
2. the exact private component registration mechanism, as long as it is not a
   public page branch

## 14. Deferred Follow-up

Deferred but explicitly related:

1. extracting weekly-update-specific fields out of shared activity hooks if they
   remain in public core after the page-gating cutover
2. broader cleanup of `frontend/src/features/users/**` or other non-page
   profile-literal usage outside `#2150` scope
3. finishing a richer plugin/action system beyond the minimal modal-capable
   action destination needed here

These deferred items do **not** block Bucket A or Bucket C, but they may block
the final implementation shape of Bucket B if the chosen action destination is
not ready.

## 15. Contract Update Protocol

This document must be re-reviewed before implementation if any of the following
changes:

1. the chosen single action source for record-detail business actions changes
2. the public runtime claims a new generic quick-action substrate
3. related-list runtime stops using `PageLayout.RelatedLists` as the authority
4. recycle-bin scope is tied to a new permission primitive instead of reusing
   current effective-permission capabilities

Required evidence for updates:

1. fresh repo line references from current `main`
2. the replacement read path named explicitly
3. updated next-tenant impact and verification sections
