@asteby/metacore-runtime-react 18.17.2 → 18.17.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # @asteby/metacore-runtime-react
2
2
 
3
+ ## 18.17.3
4
+
5
+ ### Patch Changes
6
+
7
+ - d2c92e1: Fix React #310: move flatten/order useMemo before the empty-state early return (conditional hook crashed on empty→populated)
8
+
3
9
  ## 18.17.2
4
10
 
5
11
  ### Patch Changes
@@ -1 +1 @@
1
- {"version":3,"file":"dashboard-grid.d.ts","sourceRoot":"","sources":["../src/dashboard-grid.tsx"],"names":[],"mappings":"AAMA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAK9B,OAAO,KAAK,EACR,kBAAkB,EAElB,oBAAoB,EACpB,mBAAmB,EACtB,MAAM,mBAAmB,CAAA;AAW1B,uEAAuE;AACvE,wBAAgB,eAAe,CAC3B,MAAM,CAAC,EAAE,oBAAoB,EAAE,EAC/B,OAAO,CAAC,EAAE,mBAAmB,EAAE,GAChC,oBAAoB,EAAE,CAgBxB;AASD,wBAAgB,aAAa,CAAC,EAC1B,MAAM,EACN,OAAO,EACP,QAAQ,EACR,OAAO,EACP,MAAM,EACN,QAAQ,EACR,SAAS,EACT,OAAO,GACV,EAAE,kBAAkB,qBAkJpB"}
1
+ {"version":3,"file":"dashboard-grid.d.ts","sourceRoot":"","sources":["../src/dashboard-grid.tsx"],"names":[],"mappings":"AAMA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAK9B,OAAO,KAAK,EACR,kBAAkB,EAElB,oBAAoB,EACpB,mBAAmB,EACtB,MAAM,mBAAmB,CAAA;AAW1B,uEAAuE;AACvE,wBAAgB,eAAe,CAC3B,MAAM,CAAC,EAAE,oBAAoB,EAAE,EAC/B,OAAO,CAAC,EAAE,mBAAmB,EAAE,GAChC,oBAAoB,EAAE,CAgBxB;AASD,wBAAgB,aAAa,CAAC,EAC1B,MAAM,EACN,OAAO,EACP,QAAQ,EACR,OAAO,EACP,MAAM,EACN,QAAQ,EACR,SAAS,EACT,OAAO,GACV,EAAE,kBAAkB,qBAiJpB"}
@@ -106,15 +106,10 @@ export function DashboardGrid({ groups, widgets, loadData, isAdmin, locale, curr
106
106
  };
107
107
  // eslint-disable-next-line react-hooks/exhaustive-deps
108
108
  }, [keySig, loadData]);
109
- // Global empty state (no widgets at all / none visible after gating).
110
- if (visibleGroups.length === 0) {
111
- return (_jsxs("div", { "data-testid": "dashboard-empty", className: cn('flex min-h-[40vh] flex-col items-center justify-center rounded-xl border border-dashed border-border/60 p-10 text-center', className), children: [_jsx("div", { className: "mb-4 flex size-14 items-center justify-center rounded-2xl bg-muted text-muted-foreground", children: _jsx(DynamicIcon, { name: "LayoutDashboard", className: "size-7" }) }), _jsx("h3", { className: "text-base font-semibold text-foreground", children: tr(undefined, s.emptyTitle) || s.emptyTitle }), _jsx("p", { className: "mt-1 max-w-sm text-sm text-muted-foreground", children: s.emptyDescription })] }));
112
- }
113
- // ONE unified dense grid across every group. Per-group sections used to
114
- // break the layout into rows, so a lone-widget group (e.g. a single KPI)
115
- // left the rest of its row blank. Flattening + `grid-flow-row-dense`
116
- // backfills those holes; ordering compact KPIs before charts makes the top
117
- // read as a metric band and the charts mosaic below it. No blank space.
109
+ // Flatten every group into ONE ordered list (compact KPIs before charts) for
110
+ // the masonry grid. MUST run before any early return — it is a hook, and a
111
+ // conditional hook (placed after the empty-state return) trips React #310
112
+ // when the dashboard transitions empty → populated.
118
113
  const ordered = React.useMemo(() => {
119
114
  const flat = visibleGroups.flatMap((g) => g.widgets);
120
115
  return flat
@@ -123,6 +118,10 @@ export function DashboardGrid({ groups, widgets, loadData, isAdmin, locale, curr
123
118
  a.i - b.i)
124
119
  .map((x) => x.w);
125
120
  }, [visibleGroups]);
121
+ // Global empty state (no widgets at all / none visible after gating).
122
+ if (visibleGroups.length === 0) {
123
+ return (_jsxs("div", { "data-testid": "dashboard-empty", className: cn('flex min-h-[40vh] flex-col items-center justify-center rounded-xl border border-dashed border-border/60 p-10 text-center', className), children: [_jsx("div", { className: "mb-4 flex size-14 items-center justify-center rounded-2xl bg-muted text-muted-foreground", children: _jsx(DynamicIcon, { name: "LayoutDashboard", className: "size-7" }) }), _jsx("h3", { className: "text-base font-semibold text-foreground", children: tr(undefined, s.emptyTitle) || s.emptyTitle }), _jsx("p", { className: "mt-1 max-w-sm text-sm text-muted-foreground", children: s.emptyDescription })] }));
124
+ }
126
125
  return (_jsx("div", { "data-testid": "dashboard-grid", className: cn(
127
126
  // Masonry: balanced CSS columns. Cards take their natural height
128
127
  // (compact stats, taller charts) and flow to equalize column
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@asteby/metacore-runtime-react",
3
- "version": "18.17.2",
3
+ "version": "18.17.3",
4
4
  "description": "React runtime for metacore hosts — renders addon contributions dynamically",
5
5
  "repository": {
6
6
  "type": "git",
@@ -64,8 +64,8 @@
64
64
  "typescript": "^6.0.0",
65
65
  "vitest": "^4.0.0",
66
66
  "zustand": "^5.0.0",
67
- "@asteby/metacore-ui": "2.5.2",
68
- "@asteby/metacore-sdk": "3.2.0"
67
+ "@asteby/metacore-sdk": "3.2.0",
68
+ "@asteby/metacore-ui": "2.5.2"
69
69
  },
70
70
  "scripts": {
71
71
  "build": "tsc -p tsconfig.json",
@@ -155,6 +155,24 @@ describe('DashboardGrid render', () => {
155
155
  render(<DashboardGrid widgets={[]} loadData={loaderOf({})} />)
156
156
  expect(screen.getByTestId('dashboard-empty')).toBeTruthy()
157
157
  })
158
+
159
+ it('survives an empty → populated transition (React #310 regression)', async () => {
160
+ // The flatten/order useMemo must run BEFORE the empty-state early return.
161
+ // When it sat after the return, an empty render called one fewer hook
162
+ // than the populated render → "Rendered more hooks" (React #310) crash.
163
+ const { rerender } = render(
164
+ <DashboardGrid widgets={[]} loadData={loaderOf({})} />,
165
+ )
166
+ expect(screen.getByTestId('dashboard-empty')).toBeTruthy()
167
+ rerender(
168
+ <DashboardGrid
169
+ widgets={[spec({ key: 'rev', kind: 'stat' })]}
170
+ loadData={loaderOf({ rev: { value: 5 } })}
171
+ />,
172
+ )
173
+ await waitFor(() => expect(screen.getByTestId('widget-rev')).toBeTruthy())
174
+ expect(screen.getByText('5')).toBeTruthy()
175
+ })
158
176
  })
159
177
 
160
178
  describe('DashboardGrid permission gating', () => {
@@ -132,6 +132,22 @@ export function DashboardGrid({
132
132
  // eslint-disable-next-line react-hooks/exhaustive-deps
133
133
  }, [keySig, loadData])
134
134
 
135
+ // Flatten every group into ONE ordered list (compact KPIs before charts) for
136
+ // the masonry grid. MUST run before any early return — it is a hook, and a
137
+ // conditional hook (placed after the empty-state return) trips React #310
138
+ // when the dashboard transitions empty → populated.
139
+ const ordered = React.useMemo(() => {
140
+ const flat = visibleGroups.flatMap((g) => g.widgets)
141
+ return flat
142
+ .map((w, i) => ({ w, i }))
143
+ .sort(
144
+ (a, b) =>
145
+ (isTallWidget(a.w) ? 1 : 0) - (isTallWidget(b.w) ? 1 : 0) ||
146
+ a.i - b.i,
147
+ )
148
+ .map((x) => x.w)
149
+ }, [visibleGroups])
150
+
135
151
  // Global empty state (no widgets at all / none visible after gating).
136
152
  if (visibleGroups.length === 0) {
137
153
  return (
@@ -155,23 +171,6 @@ export function DashboardGrid({
155
171
  )
156
172
  }
157
173
 
158
- // ONE unified dense grid across every group. Per-group sections used to
159
- // break the layout into rows, so a lone-widget group (e.g. a single KPI)
160
- // left the rest of its row blank. Flattening + `grid-flow-row-dense`
161
- // backfills those holes; ordering compact KPIs before charts makes the top
162
- // read as a metric band and the charts mosaic below it. No blank space.
163
- const ordered = React.useMemo(() => {
164
- const flat = visibleGroups.flatMap((g) => g.widgets)
165
- return flat
166
- .map((w, i) => ({ w, i }))
167
- .sort(
168
- (a, b) =>
169
- (isTallWidget(a.w) ? 1 : 0) - (isTallWidget(b.w) ? 1 : 0) ||
170
- a.i - b.i,
171
- )
172
- .map((x) => x.w)
173
- }, [visibleGroups])
174
-
175
174
  return (
176
175
  <div
177
176
  data-testid="dashboard-grid"