# Record header renders ActivityTimeline quick-actions (Log a Call, Email)

## Summary

On a CRM record page (e.g. **Contact**), the header renders the activity
quick-actions **Log a Call** and **Email** as buttons, in addition to their
intended home in the **Activity** tab quick-create surface. These should live
only in the Activity tab, not in the record header.

## Where this is governed (upstream)

The record header's standard-action group is resolved in the frontend by
`resolveVisibleStandardActionsForRecord`:

`frontend/src/features/record/components/ActionRendererHost.tsx`

```ts
const resolved = options.layout?.headerActions && options.layout.headerActions.length > 0
    ? resolvePlacedStandardActions(options.layout.headerActions, options.standardActions, options.objectApiName)
    : getObjectScopedStandardActions(options.standardActions, options.objectApiName);
```

- When a layout authors `headerActions` (non-empty), the header shows exactly
  those placed actions.
- Otherwise it falls back to `getObjectScopedStandardActions`, which returns
  **every** standard action whose `targetObjects` includes the object and
  **ignores `category`**.

The activity actions `taskCall` ("Log a Call") and `taskEmail` ("Email") have
`category: "ActivityTimeline"` and target `Contact` (among others), so the
fallback surfaces them in the header.

The **Activity tab** (`app-packs/crm/activity/components/ActivityTimeline.tsx`)
discovers its actions independently by `category === 'ActivityTimeline'` +
`targetObjects`, so it is unaffected by the header path.

## Why this is upstream, not downstream

- The deployed Contact/Account/Case/Order layouts are **curated by upstream**
  layout maintenance and are deliberately excluded from the downstream final
  pack (see `docs/downstream-pack-compatibility-notes.md` §10). Those curated
  layouts leave `headerActions` empty, so the header uses the upstream fallback
  above. There is no downstream lever to curate the Contact header.
- The only downstream-owned lever is the action's `targetObjects`
  (`packs/Salesforce-PingCAP-L2/actions/invocable_standard_actions.json`), but
  removing `Contact` there also removes the buttons from the **Activity tab**
  quick-create — not the requested behavior.

## Expected behavior

`ActivityTimeline`-category actions appear in the record's Activity tab but
**not** in the record header's default (unauthored) action set. An explicit
`headerActions` placement may still pin one to the header on purpose.

## Actual behavior

`Log a Call` and `Email` render as record-header buttons on every object whose
layout does not author `headerActions` (Contact, Lead, Opportunity, Case,
Contract, Order).

## Suggested fix direction

Exclude `category === 'ActivityTimeline'` from the **unauthored header
fallback** only (the explicit-placement branch is left intact). One-line change
plus tests; full diff in
[`record-header-activity-actions.patch`](./record-header-activity-actions.patch):

```diff
         ? resolvePlacedStandardActions(options.layout.headerActions, options.standardActions, options.objectApiName)
-        : getObjectScopedStandardActions(options.standardActions, options.objectApiName);
+        : getObjectScopedStandardActions(options.standardActions, options.objectApiName)
+            // Activity-timeline quick-actions (Log a Call, Email, Weekly Update) live in
+            // the record's Activity tab, not the header. Keep them out of the unauthored
+            // header fallback; an explicit `headerActions` placement still pins one.
+            .filter(action => (action.category || '') !== 'ActivityTimeline');
     return filterVisibleActionsForRecord(resolved, options.record);
```

### Scope note

This is a **global** behavior change: the activity quick-actions leave **all**
record headers (Contact, Lead, Opportunity, Case, Contract, Order), staying in
each object's Activity tab. This matches the agreed intent ("activity
quick-actions belong in the Activity tab, not the record header"). If a
per-object policy is later required, add a per-action header-exclusion field the
fallback respects and set it from the downstream pack.

## Verification

- `getObjectScopedStandardActions` has a single consumer (this resolver), so the
  blast radius is the header fallback only.
- Backend serves `category` on standard actions
  (`standard_action_dispatcher.go`), so the frontend filter has the field.
- Added tests assert (1) the fallback drops an `ActivityTimeline` action while
  keeping a non-activity (`LeadActions`) action, and (2) an explicitly *placed*
  `ActivityTimeline` action is still rendered.
- Not yet run through `vitest` — the local NexusCRM checkout is a partial clone
  with missing git objects and a CRLF-dirty tree, which blocked a clean
  worktree/test run. Run `npx vitest run src/features/record/components/ActionRendererHost.test.tsx`
  in a healthy clone before merge.

## Origin

Surfaced from the downstream request to remove "Log a Call" and "Email" from the
Contact record header (header-only). Downstream PR
[#2339](https://github.com/pingcap-inc/nexuscrm-pingcap-pack/pull/2339) (the
`targetObjects` approach) was closed because it also removed the Activity-tab
quick-create.
