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

Nobody has claimed this yet.

Assessment

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

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.

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 <mj-entity-viewer> and rebind [EntityName] between two entities that share no
    DefaultInView field names. (We used MJ: AnimalsMJ: 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:

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:

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:

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:

@for (page of activePageAsList; track page.id) {
  <mj-entity-viewer [EntityName]="page.entityName" …></mj-entity-viewer>
}

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.

Dominant language
TSQL
Stars
29
Forks
6
Avg merge
1d 1h
Merged PRs (30d)
295

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from MemberJunction/MJ

All issues in MemberJunction/MJ

Similar issues

More Web Dev issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.