@asteby/metacore-runtime-react 29.0.1 → 29.0.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,27 @@
1
1
  # @asteby/metacore-runtime-react
2
2
 
3
+ ## 29.0.3
4
+
5
+ ### Patch Changes
6
+
7
+ - 14dcf25: Dashboard: el skeleton/empty del tablero (`DashboardEmptyMockup`) se veía
8
+ apretado y raro en móvil (3 columnas absolutas angostas en un teléfono). En
9
+ `<sm` ahora renderiza un stack vertical de cards a ancho completo; el reflow
10
+ animado de 3 columnas se conserva de `sm` hacia arriba. Toggle por CSS, sin JS.
11
+
12
+ ## 29.0.2
13
+
14
+ ### Patch Changes
15
+
16
+ - c0c8cb2: fix(hotswap): al recibir `ADDON_MANIFEST_CHANGED`, si la invalidación scopeada
17
+ no elimina ninguna entrada (el caso real: la metadata se cachea por nombre de
18
+ TABLA — `purchase_order_items`, `suppliers` — que no lleva el prefijo del
19
+ addon), se limpia TODO el cache de metadata como fallback. Antes, actualizar un
20
+ addon dejaba el label/ref viejo en el store persistido y el usuario tenía que
21
+ borrar datos del sitio a mano para ver la metadata fresca. Un cambio de manifest
22
+ es infrecuente (install/upgrade), así que el refetch perezoso en el próximo
23
+ montaje es barato y correcto.
24
+
3
25
  ## 29.0.1
4
26
 
5
27
  ### Patch Changes
@@ -1 +1 @@
1
- {"version":3,"file":"dashboard-empty-mockup.d.ts","sourceRoot":"","sources":["../src/dashboard-empty-mockup.tsx"],"names":[],"mappings":"AA8BA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAK9B,wEAAwE;AACxE,MAAM,WAAW,UAAU;IACvB,CAAC,EAAE,MAAM,CAAA;IACT,CAAC,EAAE,MAAM,CAAA;IACT,CAAC,EAAE,MAAM,CAAA;IACT,CAAC,EAAE,MAAM,CAAA;CACZ;AAKD,UAAU,UAAU;IAChB,MAAM,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAAA;IAChC,MAAM,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAAA;CACnC;AAID,eAAO,MAAM,cAAc,EAAE,UAAU,EAKtC,CAAA;AAED,yDAAyD;AACzD,eAAO,MAAM,UAAU,IAAI,CAAA;AAE3B;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,UAAU,EAAE,GAAG,SAAa,GAAG,UAAU,EAAE,CAerF;AAED,kFAAkF;AAClF,wBAAgB,iBAAiB,CAAC,GAAG,SAAa,GAAG,UAAU,EAAE,EAAE,CAElE;AA4HD;;;;;;;;;;GAUG;AACH,wBAAgB,oBAAoB,CAAC,EAAE,SAAS,EAAE,EAAE;IAAE,SAAS,CAAC,EAAE,MAAM,CAAA;CAAE,qBAuBzE"}
1
+ {"version":3,"file":"dashboard-empty-mockup.d.ts","sourceRoot":"","sources":["../src/dashboard-empty-mockup.tsx"],"names":[],"mappings":"AA8BA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAK9B,wEAAwE;AACxE,MAAM,WAAW,UAAU;IACvB,CAAC,EAAE,MAAM,CAAA;IACT,CAAC,EAAE,MAAM,CAAA;IACT,CAAC,EAAE,MAAM,CAAA;IACT,CAAC,EAAE,MAAM,CAAA;CACZ;AAKD,UAAU,UAAU;IAChB,MAAM,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAAA;IAChC,MAAM,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAAA;CACnC;AAID,eAAO,MAAM,cAAc,EAAE,UAAU,EAKtC,CAAA;AAED,yDAAyD;AACzD,eAAO,MAAM,UAAU,IAAI,CAAA;AAE3B;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,UAAU,EAAE,GAAG,SAAa,GAAG,UAAU,EAAE,CAerF;AAED,kFAAkF;AAClF,wBAAgB,iBAAiB,CAAC,GAAG,SAAa,GAAG,UAAU,EAAE,EAAE,CAElE;AA4HD;;;;;;;;;;GAUG;AACH,wBAAgB,oBAAoB,CAAC,EAAE,SAAS,EAAE,EAAE;IAAE,SAAS,CAAC,EAAE,MAAM,CAAA;CAAE,qBAsCzE"}
@@ -139,8 +139,11 @@ const TILE_KINDS = ['stat', 'chart', 'chart', 'list', 'stat', 'bar'];
139
139
  export function DashboardEmptyMockup({ className }) {
140
140
  useMockupStyles();
141
141
  const layoutA = React.useMemo(() => computeLayoutRects(MOCKUP_LAYOUTS[0]), []);
142
- return (_jsx("div", { "aria-hidden": "true", "data-testid": "dashboard-empty-mockup", className: cn('mc-demock pointer-events-none select-none', className), children: layoutA.map((r, i) => (_jsx("div", { "data-mockup-tile": "tile", className: cn(tileBase, `mc-demock-t${i}`),
143
- // Base position = layout A, so reduced-motion (animation off)
144
- // shows the full static composition.
145
- style: { left: `${r.x}%`, top: `${r.y}%`, width: `${r.w}%`, height: `${r.h}%` }, children: _jsx(TileGlyph, { kind: TILE_KINDS[i] }) }, i))) }));
142
+ return (_jsxs("div", { "aria-hidden": "true", "data-testid": "dashboard-empty-mockup", className: cn('pointer-events-none select-none', className), children: [_jsx("div", { className: "flex h-full flex-col gap-3 sm:hidden", children: MOBILE_TILE_KINDS.map((kind, i) => (_jsx("div", { className: cn(tileBase, 'mc-demock-shimmer relative flex-1'), children: _jsx(TileGlyph, { kind: kind }) }, i))) }), _jsx("div", { className: "mc-demock hidden h-full sm:block", children: layoutA.map((r, i) => (_jsx("div", { "data-mockup-tile": "tile", className: cn(tileBase, `mc-demock-t${i}`),
143
+ // Base position = layout A, so reduced-motion (animation off)
144
+ // shows the full static composition.
145
+ style: { left: `${r.x}%`, top: `${r.y}%`, width: `${r.w}%`, height: `${r.h}%` }, children: _jsx(TileGlyph, { kind: TILE_KINDS[i] }) }, i))) })] }));
146
146
  }
147
+ // En móvil mostramos 3 cards apiladas (stat + chart + list) a ancho completo —
148
+ // suficiente para comunicar "tablero cargando/ vacío" sin apretar 6 tiles.
149
+ const MOBILE_TILE_KINDS = ['stat', 'chart', 'list'];
@@ -45,7 +45,9 @@ export interface WireHotSwapInvalidationOptions {
45
45
  * Useful for hosts that want to log/observe hot-swaps or trigger a
46
46
  * `window.location.reload()` when the running addon's bundle hash
47
47
  * changes (see the module-level comment above for the trade-off).
48
- * `removed` is the number of cache entries flushed for this addon.
48
+ * `removed` is the number of cache entries flushed for this addon, or
49
+ * `-1` when the scoped matcher found nothing and the whole metadata
50
+ * cache was cleared as a fallback (see `wireHotSwapInvalidation`).
49
51
  */
50
52
  onSwap?: (msg: AddonManifestChangedMessage, removed: number) => void;
51
53
  }
@@ -1 +1 @@
1
- {"version":3,"file":"manifest-hotswap-subscriber.d.ts","sourceRoot":"","sources":["../src/manifest-hotswap-subscriber.ts"],"names":[],"mappings":"AA6CA,OAAO,EAAoB,KAAK,eAAe,EAAE,MAAM,kBAAkB,CAAA;AAEzE;;;;GAIG;AACH,eAAO,MAAM,2BAA2B,EAAG,wBAAiC,CAAA;AAE5E;;;;GAIG;AACH,MAAM,WAAW,2BAA2B;IACxC,IAAI,EAAE,OAAO,2BAA2B,CAAA;IACxC,OAAO,EAAE;QACL,KAAK,CAAC,EAAE,MAAM,CAAA;QACd,QAAQ,EAAE,MAAM,CAAA;QAChB,OAAO,CAAC,EAAE,MAAM,CAAA;QAChB,OAAO,CAAC,EAAE,MAAM,CAAA;QAChB,OAAO,CAAC,EAAE,MAAM,CAAA;QAChB,SAAS,CAAC,EAAE,MAAM,CAAA;KACrB,CAAA;CACJ;AAED;;;;;;;GAOG;AACH,MAAM,WAAW,qBAAqB;IAClC,SAAS,EAAE,CACP,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,CAAC,OAAO,EAAE,2BAA2B,KAAK,IAAI,KACtD,MAAM,IAAI,CAAA;CAClB;AAED,MAAM,WAAW,8BAA8B;IAC3C;;;;;OAKG;IACH,OAAO,CAAC,EAAE,eAAe,CAAA;IACzB;;;;;;OAMG;IACH,MAAM,CAAC,EAAE,CAAC,GAAG,EAAE,2BAA2B,EAAE,OAAO,EAAE,MAAM,KAAK,IAAI,CAAA;CACvE;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,uBAAuB,CACnC,MAAM,EAAE,qBAAqB,GAAG,SAAS,GAAG,IAAI,EAChD,OAAO,GAAE,8BAAmC,GAC7C,MAAM,IAAI,CAeZ;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,4BAA4B,CACxC,MAAM,EAAE,qBAAqB,GAAG,SAAS,GAAG,IAAI,EAChD,OAAO,GAAE,8BAAmC,GAC7C,IAAI,CAON"}
1
+ {"version":3,"file":"manifest-hotswap-subscriber.d.ts","sourceRoot":"","sources":["../src/manifest-hotswap-subscriber.ts"],"names":[],"mappings":"AA6CA,OAAO,EAAoB,KAAK,eAAe,EAAE,MAAM,kBAAkB,CAAA;AAEzE;;;;GAIG;AACH,eAAO,MAAM,2BAA2B,EAAG,wBAAiC,CAAA;AAE5E;;;;GAIG;AACH,MAAM,WAAW,2BAA2B;IACxC,IAAI,EAAE,OAAO,2BAA2B,CAAA;IACxC,OAAO,EAAE;QACL,KAAK,CAAC,EAAE,MAAM,CAAA;QACd,QAAQ,EAAE,MAAM,CAAA;QAChB,OAAO,CAAC,EAAE,MAAM,CAAA;QAChB,OAAO,CAAC,EAAE,MAAM,CAAA;QAChB,OAAO,CAAC,EAAE,MAAM,CAAA;QAChB,SAAS,CAAC,EAAE,MAAM,CAAA;KACrB,CAAA;CACJ;AAED;;;;;;;GAOG;AACH,MAAM,WAAW,qBAAqB;IAClC,SAAS,EAAE,CACP,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,CAAC,OAAO,EAAE,2BAA2B,KAAK,IAAI,KACtD,MAAM,IAAI,CAAA;CAClB;AAED,MAAM,WAAW,8BAA8B;IAC3C;;;;;OAKG;IACH,OAAO,CAAC,EAAE,eAAe,CAAA;IACzB;;;;;;;;OAQG;IACH,MAAM,CAAC,EAAE,CAAC,GAAG,EAAE,2BAA2B,EAAE,OAAO,EAAE,MAAM,KAAK,IAAI,CAAA;CACvE;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,uBAAuB,CACnC,MAAM,EAAE,qBAAqB,GAAG,SAAS,GAAG,IAAI,EAChD,OAAO,GAAE,8BAAmC,GAC7C,MAAM,IAAI,CA+BZ;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,4BAA4B,CACxC,MAAM,EAAE,qBAAqB,GAAG,SAAS,GAAG,IAAI,EAChD,OAAO,GAAE,8BAAmC,GAC7C,IAAI,CAON"}
@@ -72,9 +72,25 @@ export function wireHotSwapInvalidation(client, options = {}) {
72
72
  const addonKey = message?.payload?.addonKey;
73
73
  if (!addonKey)
74
74
  return;
75
- const removed = useMetadataCache
76
- .getState()
77
- .invalidateAddon(addonKey, matcher);
75
+ const store = useMetadataCache.getState();
76
+ let removed = store.invalidateAddon(addonKey, matcher);
77
+ // Matcher-miss fallback. `invalidateAddon`'s default matcher only
78
+ // recognises cache keys that carry the addon prefix
79
+ // (`purchases`, `purchases.Model`, `purchases:Model`). But most
80
+ // hosts cache metadata under the model's TABLE name
81
+ // (`purchase_order_items`, `suppliers`) — which has no `purchases.`
82
+ // prefix — so a scoped invalidation removes 0 entries and the
83
+ // stale label/ref survives (the "why did I have to clear site
84
+ // data" bug). A manifest change is rare (addon install/upgrade),
85
+ // so when scoping matched nothing we drop the WHOLE metadata cache:
86
+ // correctness over a micro-optimisation. Entries refetch lazily on
87
+ // the next DynamicTable/relation mount, picking up the new
88
+ // fields/refs/labels. Hosts that DO namespace their keys keep the
89
+ // cheap scoped path (removed > 0).
90
+ if (removed === 0) {
91
+ store.clearAll();
92
+ removed = -1; // sentinel: "cleared all" (distinct from 0 = no-op)
93
+ }
78
94
  onSwap?.(message, removed);
79
95
  });
80
96
  return unsubscribe;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@asteby/metacore-runtime-react",
3
- "version": "29.0.1",
3
+ "version": "29.0.3",
4
4
  "description": "React runtime for metacore hosts — renders addon contributions dynamically",
5
5
  "repository": {
6
6
  "type": "git",
@@ -118,6 +118,36 @@ describe('manifest-hotswap-subscriber', () => {
118
118
  expect(onSwap).toHaveBeenCalledWith(message, 2)
119
119
  })
120
120
 
121
+ it('clears the WHOLE cache when the scoped matcher finds nothing (table-named keys)', () => {
122
+ // Real-world case: metadata is cached under TABLE names, which carry no
123
+ // addon prefix. `invalidateAddon('purchases')` matches none of them, so
124
+ // without the fallback the stale label/ref would survive an upgrade.
125
+ const cache = useMetadataCache.getState()
126
+ cache.setMetadata('purchase_order_items', fakeMeta('items-old'))
127
+ cache.setMetadata('suppliers', fakeMeta('suppliers-old'))
128
+ cache.setModalMetadata('purchase_orders', fakeMeta('po-old'))
129
+
130
+ const onSwap = vi.fn()
131
+ const client = makeFakeClient()
132
+ wireHotSwapInvalidation(client, { onSwap })
133
+
134
+ const message: AddonManifestChangedMessage = {
135
+ type: 'ADDON_MANIFEST_CHANGED',
136
+ payload: { addonKey: 'purchases', newHash: 'sha256:new' },
137
+ }
138
+ client.emit(message)
139
+
140
+ const after = useMetadataCache.getState()
141
+ // Everything dropped — next mount refetches fresh metadata.
142
+ expect(after.cache).toEqual({})
143
+ expect(after.modalCache).toEqual({})
144
+ expect(after.prefetched).toBe(false)
145
+ // metadataVersion reset so prefetchAll re-seeds from the server.
146
+ expect(after.metadataVersion).toBe('')
147
+ // Sentinel -1 tells the host "cleared all" (vs 0 = genuine no-op).
148
+ expect(onSwap).toHaveBeenCalledWith(message, -1)
149
+ })
150
+
121
151
  it('honours a custom matcher when the host uses a different key convention', () => {
122
152
  const cache = useMetadataCache.getState()
123
153
  cache.setMetadata('addons/pos/cart', fakeMeta('cart'))
@@ -230,20 +230,39 @@ export function DashboardEmptyMockup({ className }: { className?: string }) {
230
230
  <div
231
231
  aria-hidden="true"
232
232
  data-testid="dashboard-empty-mockup"
233
- className={cn('mc-demock pointer-events-none select-none', className)}
233
+ className={cn('pointer-events-none select-none', className)}
234
234
  >
235
- {layoutA.map((r, i) => (
236
- <div
237
- key={i}
238
- data-mockup-tile="tile"
239
- className={cn(tileBase, `mc-demock-t${i}`)}
240
- // Base position = layout A, so reduced-motion (animation off)
241
- // shows the full static composition.
242
- style={{ left: `${r.x}%`, top: `${r.y}%`, width: `${r.w}%`, height: `${r.h}%` }}
243
- >
244
- <TileGlyph kind={TILE_KINDS[i]} />
245
- </div>
246
- ))}
235
+ {/* Móvil (<sm): las 3 columnas absolutas quedan angostas y apretadas
236
+ en un teléfono. Un STACK vertical de cards a ancho completo se lee
237
+ claro. Toggle por CSS (sin JS, SSR-safe): el reflow animado sólo
238
+ aparece de sm hacia arriba, donde hay ancho para las 3 columnas. */}
239
+ <div className="flex h-full flex-col gap-3 sm:hidden">
240
+ {MOBILE_TILE_KINDS.map((kind, i) => (
241
+ <div key={i} className={cn(tileBase, 'mc-demock-shimmer relative flex-1')}>
242
+ <TileGlyph kind={kind} />
243
+ </div>
244
+ ))}
245
+ </div>
246
+
247
+ {/* Tablet/escritorio: el mockup de 3 columnas que reorganiza en bucle. */}
248
+ <div className="mc-demock hidden h-full sm:block">
249
+ {layoutA.map((r, i) => (
250
+ <div
251
+ key={i}
252
+ data-mockup-tile="tile"
253
+ className={cn(tileBase, `mc-demock-t${i}`)}
254
+ // Base position = layout A, so reduced-motion (animation off)
255
+ // shows the full static composition.
256
+ style={{ left: `${r.x}%`, top: `${r.y}%`, width: `${r.w}%`, height: `${r.h}%` }}
257
+ >
258
+ <TileGlyph kind={TILE_KINDS[i]} />
259
+ </div>
260
+ ))}
261
+ </div>
247
262
  </div>
248
263
  )
249
264
  }
265
+
266
+ // En móvil mostramos 3 cards apiladas (stat + chart + list) a ancho completo —
267
+ // suficiente para comunicar "tablero cargando/ vacío" sin apretar 6 tiles.
268
+ const MOBILE_TILE_KINDS: Glyph[] = ['stat', 'chart', 'list']
@@ -97,7 +97,9 @@ export interface WireHotSwapInvalidationOptions {
97
97
  * Useful for hosts that want to log/observe hot-swaps or trigger a
98
98
  * `window.location.reload()` when the running addon's bundle hash
99
99
  * changes (see the module-level comment above for the trade-off).
100
- * `removed` is the number of cache entries flushed for this addon.
100
+ * `removed` is the number of cache entries flushed for this addon, or
101
+ * `-1` when the scoped matcher found nothing and the whole metadata
102
+ * cache was cleared as a fallback (see `wireHotSwapInvalidation`).
101
103
  */
102
104
  onSwap?: (msg: AddonManifestChangedMessage, removed: number) => void
103
105
  }
@@ -128,9 +130,25 @@ export function wireHotSwapInvalidation(
128
130
  (message) => {
129
131
  const addonKey = message?.payload?.addonKey
130
132
  if (!addonKey) return
131
- const removed = useMetadataCache
132
- .getState()
133
- .invalidateAddon(addonKey, matcher)
133
+ const store = useMetadataCache.getState()
134
+ let removed = store.invalidateAddon(addonKey, matcher)
135
+ // Matcher-miss fallback. `invalidateAddon`'s default matcher only
136
+ // recognises cache keys that carry the addon prefix
137
+ // (`purchases`, `purchases.Model`, `purchases:Model`). But most
138
+ // hosts cache metadata under the model's TABLE name
139
+ // (`purchase_order_items`, `suppliers`) — which has no `purchases.`
140
+ // prefix — so a scoped invalidation removes 0 entries and the
141
+ // stale label/ref survives (the "why did I have to clear site
142
+ // data" bug). A manifest change is rare (addon install/upgrade),
143
+ // so when scoping matched nothing we drop the WHOLE metadata cache:
144
+ // correctness over a micro-optimisation. Entries refetch lazily on
145
+ // the next DynamicTable/relation mount, picking up the new
146
+ // fields/refs/labels. Hosts that DO namespace their keys keep the
147
+ // cheap scoped path (removed > 0).
148
+ if (removed === 0) {
149
+ store.clearAll()
150
+ removed = -1 // sentinel: "cleared all" (distinct from 0 = no-op)
151
+ }
134
152
  onSwap?.(message, removed)
135
153
  },
136
154
  )