# Related List Editor Duplicate Labels Analysis

## Status
Investigation complete

## Date
2026-04-02

## Scope
This document records the current bug on the Account Layout Editor related-lists tab, describes the problem clearly, and summarizes the code-level investigation results.

## Problem Summary
On the Account Layout Editor page:

- URL: `http://localhost:5173/setup/objects/account/layout/3bb07418-c752-4aaf-b0b9-59e99d10a114`
- Tab: `Related Lists`

the editor shows multiple entries with the same visible label, for example:

- `Quotes`
- `Quotes`
- `Revenues`
- `Revenues`
- `Tasks`
- `Tasks`
- `Assets`
- `Assets`
- `Assets`

At first glance this looks like duplicate related lists were stored into the layout multiple times.

## User-Facing Symptom
From an admin or ops perspective, the problem is not "the system internally distinguishes lookup fields."

The actual visible problem is:

- the page shows too many related lists
- several rows appear to be the same thing
- names like `Quotes`, `Tasks`, and `Assets` appear multiple times
- the UI does not explain why they are different
- the page does not resemble the cleaner, business-oriented related-list experience that users expect from Salesforce

For a user, this is naturally interpreted as:

- duplicate rows
- messy configuration
- wrong related-list discovery
- a broken layout editor

That user interpretation is valid at the product level, even if the underlying rows are technically distinct relationships.

## What Users Actually Need
Users do not need exposure to raw relationship mechanics.

Users need the layout editor to answer these practical questions clearly:

- Which related lists will actually show on the record page?
- Why are there multiple entries with the same business label?
- Which one should I reorder?
- Which one should I remove?
- Which one matches the Salesforce layout or business expectation?

In other words, users need:

- a small, business-meaningful set of related lists
- labels that distinguish otherwise similar relationships
- a layout editor that matches Salesforce-style admin expectations
- enough context to manage the page confidently without guessing

If the system chooses to keep multiple relationships for the same child object, it must explain them in the UI.

## Why This Is A Real Product Bug
Even if the underlying data is technically valid, the current UI is still wrong from an admin workflow perspective.

An admin looking at this page cannot tell:

- whether two rows are true duplicates or two distinct relationships
- which row should be moved up or down
- which row should be removed
- which relationship is currently configured on the page

That makes the Related Lists tab effectively unmanageable for objects such as `Account`, where many child objects relate back through multiple lookup fields.

## Expected Behavior
Each configured related list should be distinguishable in the editor.

If two related lists point to the same child object through different lookup fields, the editor should make that relationship source visible, for example:

- `Quotes via Primary Account`
- `Quotes via Billing Account`
- `Tasks via WhatId`
- `Tasks via AccountId`

The user should never have to guess which row is which.

## Actual Behavior
The editor renders only the related list display label and hides the relationship source.

That causes multiple distinct relationships to collapse into visually identical rows.

## Investigation Result

### 1. These are not necessarily duplicate rows in storage
The backend identifies a related list by:

- `ObjectAPIName`
- `LookupField`

not by label alone.

Code evidence:

- `backend/internal/features/metadata/metadata_layouts.go`
  - duplicate check compares `rl.ObjectAPIName == res.ChildObjectAPI && rl.LookupField == res.LookupFieldAPI`
  - related-list ID is generated as `fmt.Sprintf("%s-%s", res.ChildObjectAPI, res.LookupFieldAPI)`

This means:

- `Quotes` via one lookup field
- `Quotes` via another lookup field

are treated as two different related lists by design.

### 2. The backend display label is too coarse
When the backend builds a related list, it sets:

- `Label = ChildPluralLabel`

Code evidence:

- `backend/internal/features/metadata/metadata_layouts.go`
- `backend/internal/infrastructure/persistence/repo_ui.go`

So the label is derived from the child object's plural label only, such as:

- `Quotes`
- `Tasks`
- `Assets`

It does not include:

- lookup field API name
- lookup field label
- relationship name

As a result, multiple distinct relationships can legitimately end up with the exact same visible label.

### 2b. Salesforce uses a dedicated related-list label
This is also a real product-model gap versus Salesforce.

In Salesforce, the visible related-list name is not just derived from the child object's plural label.
It is backed by a dedicated relationship-level "Related List Label" that admins can set when defining the relationship.

That means two lookups from the same child object to the same parent can have intentionally different list names in the UI.

For NexusCRM, this means:

- `via <fieldLabel>` is a reasonable MVP fix
- but the longer-term parity target should be a dedicated relationship-level related-list label, rather than relying only on child plural labels

### 3. The frontend editor renders only the coarse label
The selected related-list rows in the editor render only:

- `list.label`

Code evidence:

- `frontend/src/features/admin/components/layout/RelatedListEditor.tsx`

The current row UI does not display:

- `list.lookupField`
- any field label
- any secondary relationship metadata

That is the direct reason the page looks broken.

### 4. The editor already knows how to show relationship context
In the same component, the "Available Relationships" section already renders:

- relationship label
- `via {fieldLabel}`

Code evidence:

- `frontend/src/features/admin/components/layout/RelatedListEditor.tsx`

So the system already understands that relationship source matters. The bug is that the editor shows this context when adding a relationship, but discards it when rendering the selected list rows.

## Root Cause
The bug is a UI design and rendering bug, not a storage duplication bug.

More precisely:

1. The backend correctly keeps distinct related lists separate by `child object + lookup field`.
2. The backend exposes only the child plural label as the human-facing label.
3. The frontend editor renders only that coarse label for configured related lists.

This combination causes distinct relationships to appear as duplicate rows in the UI.

## Related Bug Family: Account Quick Links
This Layout Editor bug is related to, but not identical with, the Account detail-page Quick Links bugs discussed in external issue reports such as:

- duplicate-looking `Quotes`, `Tasks`, `Assets`, `Leads`, and other related-list entries
- exposure of too many system/internal related lists
- all quick-link counts displaying as `0`

These should be understood as one related problem family with multiple symptoms.

### Shared upstream problem
The shared upstream design problem is that the system currently exposes too much of the raw relationship model.

Instead of presenting a Salesforce-style, business-curated set of related lists, the system currently:

- discovers many child relationships automatically
- keeps multiple relationships for the same child object when they use different lookup fields
- uses a coarse display label based on the child object's plural label
- does not sufficiently explain relationship source in the UI
- does not sufficiently filter out low-value or system-oriented relationships

That shared upstream behavior causes both of these user-facing problems:

1. **Layout Editor symptom**
   - admins see multiple identical-looking related-list rows
   - they cannot tell which one to reorder or remove

2. **Quick Links symptom**
   - business users see many repeated-looking related-list links on the record detail page
   - the result does not resemble the smaller, more business-meaningful related-list set users expect from Salesforce

### Why the Quick Links bug is related
Quick Links and the Layout Editor are two different surfaces, but both consume the same related-list model.

That means both surfaces are affected by the same underlying issues:

- over-discovery
- coarse labels
- lack of relationship-source explanation

So when users complain that Account Quick Links shows too many `Quotes` or `Tasks`, that complaint is valid and closely related to the editor bug documented here.

### Important distinction: Quick Links has an additional independent bug
The Quick Links page also has a separate count bug that is not part of the Layout Editor rendering problem.

When too many related lists are present:

- the frontend sends one batch count request covering all visible quick links
- the backend count endpoint has a hard limit of 50 requests per batch
- if the request contains more than 50 items, the backend rejects the entire batch with HTTP 400
- the frontend then receives no count data for any quick link in that batch
- the frontend fallback renders missing counts as `0`

So the Quick Links issue is actually the combination of:

1. **duplicate-looking / over-discovered related lists**
2. **all counts shown as `0` when batch count size exceeds backend limits**

This second problem is specific to the Quick Links count path and does not apply to the Layout Editor UI itself.

### Root cause: Quick Links is not layout-controlled
There is also a deeper architectural issue behind the Quick Links experience.

Today, the Quick Links surface is effectively fed by related-list discovery, not by the layout's intentionally curated related-list configuration.

That is a fundamental mismatch with Salesforce-style behavior.

In a Salesforce-like model:

- Quick Links should reflect the related lists configured on the active page layout
- not every discovered child relationship that references the parent object

This matters because over-discovery is not just a filtering bug. It is also a data-source bug.

### Product interpretation
From a product perspective, these issues should not be treated as isolated bugs that happen to share labels.

They are better understood as:

- one shared related-list design problem
- exposed in multiple surfaces
- plus one additional Quick Links count-failure bug

This distinction matters because:

- fixing only the editor labels will not make Quick Links business-friendly
- fixing only the batch count limit will still leave repeated-looking `Quotes` and `Tasks`
- fixing only system-object filtering will still leave multiple same-label rows unexplained

The full user-facing problem is therefore broader than a single rendering defect.

## Historical Analysis

### Initial introduction
The issue traces back to the initial Related List Editor implementation:

- Commit: `3de7c0dd61d88f56c6a39bb0955a2089baaf8b44`
- Author: `Qi Liu`
- Date: `2025-12-29`
- Message: `Initial commit: NexusCRM modular backend and UI`

Two important pieces were already present in that initial implementation:

1. The backend modeled related-list identity using `ObjectAPIName + LookupField`.
2. The frontend editor rendered configured related lists using only `list.label`.

So the core bug existed from day one.

### Was it made worse later?
No clear later regression commit was found.

What changed later was mostly:

- file moves / refactors
- repository extraction
- type renames
- unrelated improvements such as UUID generation

The core behavior stayed the same:

- distinct relationships remained distinct in backend data
- selected rows in the editor continued to display only the coarse label

The problem appears more severe on `Account` because that object has many child relationships that share the same child plural label.

## Impact
This bug affects layout administration directly:

- admins cannot safely reorder repeated related-list rows
- admins cannot know which repeated row to remove
- admins cannot verify whether the layout contains the intended relationship
- the Related Lists editor becomes misleading for standard objects with many relationships

## What This Bug Is Not
This investigation did not find evidence that:

- the layout editor is blindly inserting the exact same related-list record multiple times
- a recent PR introduced a regression from a previously correct editor
- the deduplication key on the backend is wrong

The problem is presentation and usability, not row identity.

## Recommended Fix Direction
The minimal correct fix is in the editor UI.

### Recommended UI change
For each configured related list row, display:

- primary text: `label`
- secondary text: `via <fieldLabel>` if available
- fallback secondary text: `via <lookupField>`

For example:

- `Quotes`
  - `via Primary Account`
- `Quotes`
  - `via Billing Account`

### Optional backend improvement
If the frontend does not currently have enough relationship display metadata for saved rows, extend the related-list payload so the editor can render:

- `fieldLabel`
- or a prebuilt display string such as `Quotes via Billing Account`

Longer term, the better parity target is:

- a dedicated relationship-level related-list label

rather than deriving all display names from the child object's plural label.

### Non-goal
Do not collapse these rows by label alone.

That would destroy legitimate configurations where the same child object appears multiple times through different relationships.

## Product Behavior View
From a product and UI perspective, the problem should not be framed as:

- "the backend stores duplicate rows"

The real user-facing problem is:

- the system exposes too much of the raw relationship model
- the UI does not organize that relationship model into a Salesforce-style admin experience
- users see repeated-looking labels, too many low-value related lists, and unclear configuration choices

### What users expect
Users expect a Salesforce-like experience where:

- only a manageable set of business-relevant related lists is shown
- repeated names are either avoided or clearly distinguished
- system/internal relationships do not dominate the main experience
- layout configuration is understandable without technical knowledge of lookup fields
- counts in Quick Links are trustworthy

### What the system currently does
The current behavior is closer to:

- discover all child relationships that reference the parent object
- preserve multiple relationships for the same child object
- label them with only the child plural label
- show them directly in admin and runtime surfaces

That is why the current UI feels technical, noisy, and unlike Salesforce.

## Proposal Design
The solution should be designed as a product behavior change, not just a rendering patch.

### Product Principle 1: Related lists are page elements, not raw relationship dumps
Only layout-relevant related lists should shape the end-user experience.

Quick Links should reflect the related lists intentionally shown on the page, not every discovered relationship that happens to reference the parent object.

This is not just a presentation preference. It is a data-source requirement.

If Quick Links is fed from schema auto-discovery instead of the layout's stored related-list configuration, the product will keep over-exposing internal or low-value relationships no matter how much label cleanup is done later.

### Product Principle 2: Same-label items must be distinguishable
If multiple relationships for the same child object are kept, the UI must explain the difference.

That means each related list row should display:

- a primary business label, such as `Quotes`
- a secondary qualifier, such as `via Billing Account`

The user should never need to guess which `Quotes` row they are editing or clicking.

### Product Principle 3: System noise should be hidden by default
System-oriented and low-value relationships should not dominate the default layout experience.

Examples include:

- share objects
- history objects
- territory association objects
- internal event/task relation tables

These may still exist technically, but should be suppressed or moved behind an advanced workflow.

### Product Principle 4: Failures must not masquerade as zero
If Quick Links counts fail to load, the UI must not render all counts as `0`.

`0` should mean a real zero, not:

- request failed
- batch limit exceeded
- backend returned an error

In the current failure mode, the backend rejects the entire oversized batch with an explicit HTTP 400 response. The frontend then has no count data and its fallback makes every count look like a genuine zero. That is the behavior that must be removed.

## Proposed UX Changes

### 1. Layout Editor: configured related-list rows
For each selected related list row, show:

- primary label: `Quotes`
- secondary label: `via Billing Account`
- optional technical fallback: `Quote.AccountId`

This directly fixes the current "multiple identical labels" problem.

### 2. Layout Editor: available relationships picker
The available relationship list should be grouped into:

- `Recommended`
- `Additional Relationships`
- `System / Advanced`

This prevents the picker from feeling like an unfiltered relationship dump.

### 3. Layout Editor: optional display-label override
Allow the admin to override the display label for a related list in the layout if needed.

This is not required for correctness, but it would improve parity with real admin workflows and give teams a way to align the page with business terminology.

### 4. Record Detail: Quick Links should consume curated layout intent
Quick Links should reflect the related lists that the layout intentionally exposes.

If multiple same-object relationships are present, Quick Links should either:

- display the qualifier, or
- display only the explicitly curated set chosen in layout configuration

### 5. Quick Links count loading
Counts should be requested in safe chunks, and failures should be represented honestly.

Preferred behavior:

- chunk requests to respect backend limits
- show loading state while counts are pending
- show `—` or omit the number on failure
- never substitute failure with fake `0`

## MVP Scope
The minimum useful fix should include:

1. Layout Editor selected rows display `via <fieldLabel>` or `via <lookupField>`
2. Quick Links batch count requests are chunked below backend limits
3. Quick Links no longer render failed counts as `0`
4. System/internal related lists are filtered from the default Quick Links experience
5. Available relationships in the editor are visually grouped so admins are not overwhelmed

This MVP would make both admin and runtime experiences materially more understandable without requiring a full redesign of the metadata model.

## Non-Goals For The First Fix
The first fix does not need to:

- redesign the entire relationship metadata schema
- eliminate all support for multiple relationships to the same parent
- achieve full Salesforce parity for every related-list property
- solve all business curation through hardcoded object-specific allowlists

The goal is to make the system understandable and usable first.

## Acceptance Criteria
The proposal should be considered successful only if all of the following become true:

1. An admin can look at the Account Layout Editor and immediately tell why two `Quotes` rows are different.
2. An admin can reorder or remove repeated-looking related lists confidently without guessing.
3. Account Quick Links no longer show dozens of unexplained same-label entries by default.
4. Quick Links counts do not all collapse to `0` when too many related lists are present.
5. The resulting UX feels closer to a Salesforce page-layout workflow than to a raw relationship browser.

## Additional Bug: Removed Related Lists Do Not Return To Available Relationships
This is a separate but closely related bug in the same editor surface.

### User-facing symptom
On the Account Layout Editor page:

- URL: `http://localhost:5173/setup/objects/account/layout/3bb07418-c752-4aaf-b0b9-59e99d10a114`
- Tab: `Related Lists`

when a user removes a related list from the selected list, that relationship does not correctly reappear in the `Available Relationships` area below.

From the user's perspective, this feels wrong because the product expectation is simple:

- if a relationship is currently selected, it appears in the selected list
- if a relationship is removed, it should become available to add again

When that does not happen, the editor feels inconsistent and unreliable.

### Why this matters
This is not a cosmetic issue.

It breaks the expected admin workflow:

- add a related list
- review placement
- remove it if not needed
- re-add it later if desired

If removed items do not re-enter the available pool, the editor stops behaving like a reversible configuration tool.

### Problem root cause
The current editor does not model `selected` and `available` relationships as two views over a single canonical source.

Instead:

- `layout.relatedLists` is one state source
- `availableRelatedLists` is a separate local state source

That is the design flaw.

### Code-level root causes

#### 1. Available relationships are stored separately instead of derived
In:

- `frontend/src/features/admin/components/layout/RelatedListEditor.tsx`

the component stores:

- `availableRelatedLists`

in local React state.

This list is independently populated from schema discovery, rather than computed as:

- `allCandidates - currentlySelected`

That means the UI has two partially independent sources of truth.

#### 2. Available relationships do not react to removal of selected items
The discovery effect depends on:

- `metadata`
- `allSchemas`

but not on:

- `layout.relatedLists`

So when a related list is removed from the layout, the available list is not recalculated as a function of that new selected state.

#### 3. Remove only mutates the selected list
In:

- `frontend/src/features/admin/components/layout/useLayoutState.ts`

the removal flow only filters the entry out of:

- `layout.relatedLists`

It does not restore anything into an available pool, and it does not trigger a canonical recomputation model.

#### 4. Added related lists are stored with generated UUIDs instead of stable relationship identity
In:

- `frontend/src/features/admin/components/layout/RelatedListEditor.tsx`

new related lists are added with a random UUID, instead of using a canonical identity such as:

- `objectApiName + lookupField`

This makes it harder to reason about the same relationship across:

- discovered candidates
- selected layout rows
- removed rows that should become addable again

#### 5. Relationship display metadata is lost when saving into the layout
The available candidate model contains:

- `fieldApiName`
- `fieldLabel`

but the saved layout item only keeps:

- `label`
- `objectApiName`
- `lookupField`
- `fields`

So the saved layout item still preserves the field identity via `lookupField`, but it throws away the human-readable relationship display context such as `fieldLabel`.

That means even if the system can identify the same relationship again, it has already lost part of the metadata needed to present it clearly in the UI.

## Correct Design
The correct design is to treat selected and available relationships as two views over one canonical relationship set.

### Canonical model
The editor should first build or fetch one complete set of candidate relationships for the current object.

For each candidate, it should keep stable identity and sufficient display information, for example:

- `id`
- `objectApiName`
- `lookupField`
- `label`
- `fieldLabel`
- optional relationship metadata

### Derived views
From that canonical candidate set, the UI should derive:

- `selectedRelationships`
- `availableRelationships`

using the layout's current related-list selection.

Conceptually:

- `selected = allCandidates where id is present in layout.relatedLists`
- `available = allCandidates where id is not present in layout.relatedLists`

That means:

- add removes an item from available automatically
- remove returns an item to available automatically
- no manual synchronization is needed

### Stable identity requirement
The same relationship must use the same identifier in:

- discovered candidates
- layout.relatedLists
- Quick Links / runtime consumption

The correct identifier is not a random UUID.

It should be a canonical related-list key, such as:

- `normalizeRelatedListId(objectApiName, lookupField)`

### UI expectation
Once selected/available are derived from one source of truth:

- removing a selected related list immediately makes it available again
- adding it again uses the same stable identity
- no duplicate drift occurs between the two lists

## Proposed Solution

### MVP implementation
The simplest correct fix is:

1. Keep one canonical `allCandidates` list
2. Stop storing `availableRelatedLists` as independent mutable state
3. Compute `availableRelationships` from `allCandidates - selectedIds`
4. Use canonical related-list IDs instead of generated UUIDs
5. Preserve enough relationship display metadata for both selected and available rendering

### Better long-term design
The long-term fix should move relationship candidate discovery and curation into a backend-backed contract, so the frontend is not responsible for reconstructing relationship semantics from raw schemas.

That backend contract should eventually support:

- recommended relationships
- additional relationships
- system/internal relationships
- display label
- secondary relationship qualifier

The frontend editor would then simply render:

- selected items
- available items

from one stable source of truth.

## Acceptance Criteria For This Bug
This bug is resolved only if all of the following are true:

1. Removing a related list immediately makes that relationship appear again in `Available Relationships`.
2. Re-adding the same relationship uses the same stable identity, not a brand-new logical item.
3. The available list is always derived from current layout state, not maintained as stale parallel state.
4. The user can repeatedly add, remove, and re-add relationships without the editor drifting into an inconsistent state.

## Final Conclusion
The repeated `Quotes`, `Tasks`, `Revenues`, and `Assets` rows on the Account Layout Editor are not proof of duplicated stored config.

They are distinct related-list relationships that become visually indistinguishable because:

- backend label generation is too coarse
- frontend selected-row rendering hides the relationship source

This is a longstanding bug in the Related List Editor UX, present since the initial implementation.
