MemberJunction / MemberJunction/MJ

mj-entity-data-grid renders ZERO columns when a stale cross-entity grid state matches no fields — rows load, count is correct, nothing displays

Open Beginner friendly
#4,244 0 comments 0 reactions 0 assignees View on GitHub
Dominant language
TSQL
Stars
29
Forks
6
Avg merge
2d 1h
Merged PRs (30d)
323

Description

## Summary

`mj-entity-data-grid` accepts a **zero-column result** from a stale, cross-entity grid state and
renders it — producing a grid that loads its rows, reports the correct row count, and displays
**no columns and therefore nothing at all**. There is no error and no console warning.

The correctly-built column list is sitting in the very next branch of the same `if`, unused.

## Repro

1. Host **one** `` and rebind `[EntityName]` between two entities that share **no**
`DefaultInView` field names. (We used `MJ: Animals` → `MJ: Care Logs`.)
2. Load entity A and let its grid render — this populates the grid state's `columnSettings`.
3. Rebind to entity B.
4. Entity B's toolbar shows its **correct row count**; the grid shows **no header row and no rows**.

## Measured, live in the browser

Read off the component with `ng.getComponent()` on the failing page:

```
entity: "MJ: Care Logs"
agRowsInDom: 21 ← the rows ARE loaded and in the DOM
columnsBuilt: 6 ← _columns was built correctly from metadata
agColumnDefs: 0 ← …and then discarded
agHeaderCells: 0
defaultInViewTrue: 6 ← metadata is correct
truthyButNotTrue: 0 ← and they are real booleans, not 1/truthy
gridStateColumnNames: ["Name","Species","IntakeDate","Status","Breed","Housing"] ← ANIMAL's columns
ownFieldNames: ["ID","AnimalID","CareDate","CareType","Description","PerformedBy",
"IsComplete","FollowUpDate","Notes","__mj_CreatedAt","__mj_UpdatedAt","Animal"]
```

The server side was fully eliminated first: rows returned, entity permissions, `DefaultInView`
metadata, and the **serialized `GetDatasetByName('MJ_Metadata')` payload** the browser actually
receives (six `DefaultInView: true`, `typeof boolean`). All correct.

## Root cause — two spots, both in `packages/Angular/Generic/entity-viewer/src/lib/entity-data-grid/entity-data-grid.component.ts`

**1. `buildAgColumnDefsFromGridState()` (~line 2526)** drops any setting whose field the current
entity does not have, with no floor when *every* setting is dropped:

```ts
for (const colConfig of sortedColumns) {
const field = this._entityInfo.Fields.find(f =>
f.Name.toLowerCase() === colConfig.Name.toLowerCase()
);
if (!field) continue; // all 6 of Animal's settings miss → cols === []
...
}
```

**2. `buildAgColumnDefs()` (~line 2390)** takes that empty array as the answer:

```ts
if (this._gridState?.columnSettings?.length && this._entityInfo) {
this.agColumnDefs = this.buildAgColumnDefsFromGridState(this._gridState.columnSettings);
} else if (this._columns.length > 0) { // ← the correctly-built 6, never reached
this.agColumnDefs = this._columns.map(col => this.mapColumnConfigToColDef(col));
} else if (this._entityInfo) {
this.agColumnDefs = this.generateAgColumnDefs(this._entityInfo);
} else {
this.agColumnDefs = [];
}
```

## Suggested fix

Treat an empty result from the grid-state branch as **no usable state** and fall through:

```ts
const fromState = (this._gridState?.columnSettings?.length && this._entityInfo)
? this.buildAgColumnDefsFromGridState(this._gridState.columnSettings)
: [];

if (fromState.length) this.agColumnDefs = fromState;
else if (this._columns.length > 0) this.agColumnDefs = this._columns.map(c => this.mapColumnConfigToColDef(c));
else if (this._entityInfo) this.agColumnDefs = this.generateAgColumnDefs(this._entityInfo);
else this.agColumnDefs = [];
```

**This is consistent with code already in the same file.** `generateAgColumnDefs()` (~line 2620)
has exactly this kind of floor — *"Fallback: if no DefaultInView fields are defined, show first 10
non-system fields"*. The grid-state branch is the one path missing one.

## Two reasons this is worth more than the empty grid we hit

**1. The partial case is worse than the total one, because nobody notices.** The failure degrades by
field-name **overlap**, not by erroring. In the same app, Animals → **Breeds** looked fine because
those two share `Name` and `Species`, so two of four columns survived — a silently truncated grid
that reads as normal. Only Animals → **Care Logs**, which shares nothing, collapsed to zero and
became visible. Every partial-overlap pair in every app is currently showing a subset of its columns
with no indication.

**2. Rebinding `[EntityName]` is a supported pattern, not misuse.** `EntityName` is a rebindable
`@Input`, and the `Entity` setter already has explicit `entityChanged` handling that clears
`viewTypeConfigById`, resets `InternalSortState`, and nulls `dynamicRendererRef` — with a comment
naming this exact symptom:

> *"Keeping the old entity's config applies its columnSettings to the new entity, so only fields
> common to both survive (e.g. just Name/Description) — the 'no/too-few columns' symptom."*

So the intent to handle entity switching is clearly there and was implemented in several places; the
child grid's `_gridState` just isn't covered by it. A secondary question for whoever picks this up:
should `EntityViewerComponent` also be clearing the child grid's `_gridState` on entity change, in
addition to the grid defending itself?

## Workaround for anyone hitting this now

Force the viewer to be destroyed and recreated when the entity changes, so no state crosses
entities — e.g. in Angular 17+ control flow, key the block on the page identity:

```html
@for (page of activePageAsList; track page.id) {

}
```

That prevents state crossing entities but does not address the underlying acceptance of an empty
column set, which still bites any host that legitimately reuses one viewer.

## Environment

MJ **v6.1.0-edge.4** (commit `43217fa139`). Found while building MJ Academy (a MemberJunction
teaching course) — a left-rail category whose sub-pages are separate entity grids sharing one viewer.

Contributor guide

Open the contributing guide

Research direction

Start in packages/Angular/Generic/entity-viewer/src/lib/entity-data-grid/entity-data-grid.component.ts, reading buildAgColumnDefs() and buildAgColumnDefsFromGridState() around the mentioned lines. Verify that stale state with no matching fields falls through to the existing _columns or entity fallback, then confirm rebinding between disjoint entities renders headers and rows instead of an empty grid.

Written by the indexing model from the issue text.

Assessment

Tech stack
angular, typescript
Domain
frontend
Issue type
Bug
Difficulty
2/5
Estimated time
1-3 hours
Activity status
Active
Clarity
Clearly specified
Newbie friendliness
84/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.