@asteby/metacore-runtime-react 49.3.0 → 49.3.1

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
+ ## 49.3.1
4
+
5
+ ### Patch Changes
6
+
7
+ - 6e6d443: `DocumentFormDialog` reinicia tipo y paso al abrirse durante el render, no en un efecto: el primer frame ya es el DocumentEditor del tipo pedido. Antes, si el host lo dejaba montado mientras cambiaban sus formularios (misma ruta para otra vista) o el `initialType`, al abrir se pintaba un instante el wizard «Nuevo documento» o el editor del tipo anterior (QA Pitsline r6, Nota de crédito).
8
+
3
9
  ## 49.3.0
4
10
 
5
11
  ### Minor Changes
@@ -1 +1 @@
1
- {"version":3,"file":"document-form-dialog.d.ts","sourceRoot":"","sources":["../src/document-form-dialog.tsx"],"names":[],"mappings":"AA6BA,OAAO,EAEH,KAAK,sBAAsB,EAE9B,MAAM,8BAA8B,CAAA;AAerC,OAAO,KAAK,EAER,gBAAgB,EAChB,qBAAqB,EACrB,aAAa,EAChB,MAAM,SAAS,CAAA;AAEhB,MAAM,WAAW,uBAAuB;IACpC,IAAI,EAAE,OAAO,CAAA;IACb,YAAY,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAA;IACrC,kEAAkE;IAClE,KAAK,EAAE,MAAM,CAAA;IACb,oDAAoD;IACpD,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,KAAK,EAAE,qBAAqB,CAAA;IAC5B,mDAAmD;IACnD,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,uCAAuC;IACvC,OAAO,CAAC,EAAE,CAAC,MAAM,CAAC,EAAE,OAAO,KAAK,IAAI,CAAA;IACpC;;;OAGG;IACH,cAAc,CAAC,EAAE,sBAAsB,CAAC,QAAQ,CAAC,CAAA;IACjD,+EAA+E;IAC/E,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,0GAA0G;IAC1G,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,sEAAsE;IACtE,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB;;;OAGG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;IAC5B,gFAAgF;IAChF,aAAa,CAAC,EAAE;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,EAAE,EAAE,MAAM,CAAA;KAAE,CAAA;IAC3C;;;;OAIG;IACH,gBAAgB,CAAC,EAAE,CAAC,MAAM,EAAE,eAAe,KAAK,IAAI,CAAA;CACvD;AAED,2EAA2E;AAC3E,MAAM,WAAW,eAAe;IAC5B,sFAAsF;IACtF,KAAK,EAAE,MAAM,CAAA;IACb,2BAA2B;IAC3B,IAAI,EAAE,gBAAgB,CAAA;CACzB;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,qBAAqB,GAAG,SAAS,GAAG,eAAe,GAAG,SAAS,CAIrG;AAED,8EAA8E;AAC9E,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,qBAAqB,GAAG,SAAS,GAAG,qBAAqB,GAAG,SAAS,CAKjH;AAED,uFAAuF;AACvF,wBAAgB,oBAAoB,CAChC,QAAQ,CAAC,EAAE,IAAI,CAAC,aAAa,EAAE,gBAAgB,CAAC,GAAG,IAAI,EACvD,QAAQ,CAAC,EAAE,qBAAqB,GAAG,IAAI,GACxC,qBAAqB,GAAG,SAAS,CAGnC;AAED;;;;;;;GAOG;AACH,wBAAgB,0BAA0B,CACtC,KAAK,EAAE,qBAAqB,GAAG,SAAS,EACxC,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,GACxC;IAAE,KAAK,EAAE,qBAAqB,GAAG,SAAS,CAAC;IAAC,UAAU,EAAE,OAAO,CAAA;CAAE,CAQnE;AAID,wBAAgB,kBAAkB,CAAC,EAC/B,IAAI,EACJ,YAAY,EACZ,KAAK,EACL,QAAQ,EACR,KAAK,EACL,WAAW,EACX,OAAO,EACP,cAAc,EACd,YAAY,EACZ,cAAc,EACd,QAAQ,EACR,MAAM,EACN,aAAa,EACb,gBAAgB,GACnB,EAAE,uBAAuB,sCAiVzB"}
1
+ {"version":3,"file":"document-form-dialog.d.ts","sourceRoot":"","sources":["../src/document-form-dialog.tsx"],"names":[],"mappings":"AA6BA,OAAO,EAEH,KAAK,sBAAsB,EAE9B,MAAM,8BAA8B,CAAA;AAerC,OAAO,KAAK,EAER,gBAAgB,EAChB,qBAAqB,EACrB,aAAa,EAChB,MAAM,SAAS,CAAA;AAEhB,MAAM,WAAW,uBAAuB;IACpC,IAAI,EAAE,OAAO,CAAA;IACb,YAAY,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAA;IACrC,kEAAkE;IAClE,KAAK,EAAE,MAAM,CAAA;IACb,oDAAoD;IACpD,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,KAAK,EAAE,qBAAqB,CAAA;IAC5B,mDAAmD;IACnD,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,uCAAuC;IACvC,OAAO,CAAC,EAAE,CAAC,MAAM,CAAC,EAAE,OAAO,KAAK,IAAI,CAAA;IACpC;;;OAGG;IACH,cAAc,CAAC,EAAE,sBAAsB,CAAC,QAAQ,CAAC,CAAA;IACjD,+EAA+E;IAC/E,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,0GAA0G;IAC1G,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,sEAAsE;IACtE,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB;;;OAGG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;IAC5B,gFAAgF;IAChF,aAAa,CAAC,EAAE;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,EAAE,EAAE,MAAM,CAAA;KAAE,CAAA;IAC3C;;;;OAIG;IACH,gBAAgB,CAAC,EAAE,CAAC,MAAM,EAAE,eAAe,KAAK,IAAI,CAAA;CACvD;AAED,2EAA2E;AAC3E,MAAM,WAAW,eAAe;IAC5B,sFAAsF;IACtF,KAAK,EAAE,MAAM,CAAA;IACb,2BAA2B;IAC3B,IAAI,EAAE,gBAAgB,CAAA;CACzB;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,qBAAqB,GAAG,SAAS,GAAG,eAAe,GAAG,SAAS,CAIrG;AAED,8EAA8E;AAC9E,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,qBAAqB,GAAG,SAAS,GAAG,qBAAqB,GAAG,SAAS,CAKjH;AAED,uFAAuF;AACvF,wBAAgB,oBAAoB,CAChC,QAAQ,CAAC,EAAE,IAAI,CAAC,aAAa,EAAE,gBAAgB,CAAC,GAAG,IAAI,EACvD,QAAQ,CAAC,EAAE,qBAAqB,GAAG,IAAI,GACxC,qBAAqB,GAAG,SAAS,CAGnC;AAED;;;;;;;GAOG;AACH,wBAAgB,0BAA0B,CACtC,KAAK,EAAE,qBAAqB,GAAG,SAAS,EACxC,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,GACxC;IAAE,KAAK,EAAE,qBAAqB,GAAG,SAAS,CAAC;IAAC,UAAU,EAAE,OAAO,CAAA;CAAE,CAQnE;AAID,wBAAgB,kBAAkB,CAAC,EAC/B,IAAI,EACJ,YAAY,EACZ,KAAK,EACL,QAAQ,EACR,KAAK,EACL,WAAW,EACX,OAAO,EACP,cAAc,EACd,YAAY,EACZ,cAAc,EACd,QAAQ,EACR,MAAM,EACN,aAAa,EACb,gBAAgB,GACnB,EAAE,uBAAuB,sCA0VzB"}
@@ -111,18 +111,26 @@ export function DocumentFormDialog({ open, onOpenChange, model, endpoint, forms,
111
111
  ? withDefaultTaxRate(searchProducts, taxRate)
112
112
  : createCatalogProductSearch(api, { model: productModel, defaultTaxRate: taxRate }), [searchProducts, api, productModel, taxRate]);
113
113
  const lineCfg = type ? editorLinesConfig(type, forms) : undefined;
114
- // Reset every time the dialog (re)opens.
115
- useEffect(() => {
116
- if (!open)
117
- return;
118
- const start = initialType ?? single?.key ?? null;
119
- setTypeKey(start);
120
- setStep(start ? 'fields' : 'type');
121
- setLines([]);
122
- setFieldErrors({});
123
- setServerLineErrors({});
124
- setFormError(undefined);
125
- }, [open, initialType, single?.key]);
114
+ // Reset every time the dialog (re)opens — DURING render, not in an effect.
115
+ // The host keeps this dialog mounted while closed (and across views whose
116
+ // forms change, e.g. the same /m/$model route going Facturas → Notas de
117
+ // crédito); an effect reset ran after the first open frame was already
118
+ // painted with the previous type/step, so the wizard (or the previous
119
+ // type's editor) flashed before the right DocumentEditor (QA Pitsline r6).
120
+ const openKey = open ? `${initialType ?? ''}\u0000${single?.key ?? ''}` : null;
121
+ const [resetFor, setResetFor] = useState(null);
122
+ if (openKey !== resetFor) {
123
+ setResetFor(openKey);
124
+ if (openKey !== null) {
125
+ const start = initialType ?? single?.key ?? null;
126
+ setTypeKey(start);
127
+ setStep(start ? 'fields' : 'type');
128
+ setLines([]);
129
+ setFieldErrors({});
130
+ setServerLineErrors({});
131
+ setFormError(undefined);
132
+ }
133
+ }
126
134
  // Seed field defaults for the chosen type.
127
135
  useEffect(() => {
128
136
  if (!type)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@asteby/metacore-runtime-react",
3
- "version": "49.3.0",
3
+ "version": "49.3.1",
4
4
  "description": "React runtime for metacore hosts — renders addon contributions dynamically",
5
5
  "repository": {
6
6
  "type": "git",
@@ -0,0 +1,169 @@
1
+ // @vitest-environment happy-dom
2
+ // QA Pitsline r6: «al abrir una Nota de crédito se ve un instante el modal viejo»
3
+ // antes del DocumentEditor. El host reutiliza el diálogo montado (misma ruta
4
+ // /m/$model para Facturas → Notas de crédito, o formularios que cambian de la
5
+ // caché a la metadata viva mientras está cerrado) y el primer render al abrir
6
+ // pintaba el wizard con el tipo de la apertura anterior: el reinicio vivía en un
7
+ // efecto, que corre DESPUÉS de que ese frame ya se pintó.
8
+ import { afterEach, describe, expect, it, vi } from 'vitest'
9
+ import { cleanup, render } from '@testing-library/react'
10
+
11
+ vi.mock('@tanstack/react-router', () => ({ useNavigate: () => () => {} }))
12
+ vi.mock('react-i18next', () => {
13
+ const i18n = { language: 'es' }
14
+ const t = (k: string, o?: { defaultValue?: string }) => o?.defaultValue ?? k
15
+ const value = { t, i18n }
16
+ return { useTranslation: () => value }
17
+ })
18
+ // Dialog sin portal ni Presence: cada commit de React llega tal cual al DOM. Con
19
+ // el portal de Radix el primer commit al abrir queda vacío y, en act(), el efecto
20
+ // de reinicio alcanza a correr antes del siguiente; en el navegador ese reinicio
21
+ // va en otra prioridad y el frame intermedio sí se pinta.
22
+ vi.mock('@asteby/metacore-ui/primitives', async (importOriginal) => {
23
+ const actual = await importOriginal<Record<string, unknown>>()
24
+ const React = await import('react')
25
+ return {
26
+ ...actual,
27
+ Dialog: ({ open, children }: { open?: boolean; children?: React.ReactNode }) =>
28
+ open ? React.createElement(React.Fragment, null, children) : null,
29
+ DialogContent: ({ children, ...rest }: Record<string, unknown> & { children?: React.ReactNode }) => {
30
+ const attrs: Record<string, unknown> = {}
31
+ for (const [k, v] of Object.entries(rest)) if (k.startsWith('data-')) attrs[k] = v
32
+ return React.createElement('div', { role: 'dialog', ...attrs }, children)
33
+ },
34
+ DialogHeader: ({ children }: { children?: React.ReactNode }) => React.createElement('div', null, children),
35
+ DialogFooter: ({ children }: { children?: React.ReactNode }) => React.createElement('div', null, children),
36
+ DialogTitle: ({ children }: { children?: React.ReactNode }) => React.createElement('h2', null, children),
37
+ DialogDescription: ({ children }: { children?: React.ReactNode }) => React.createElement('p', null, children),
38
+ }
39
+ })
40
+
41
+ import { DocumentFormDialog } from '../document-form-dialog'
42
+ import { ApiProvider, type ApiClient } from '../api-context'
43
+ import type { DocumentFormsManifest } from '../types'
44
+
45
+ afterEach(cleanup)
46
+
47
+ const client = {
48
+ get: vi.fn().mockResolvedValue({ data: { data: [] } }),
49
+ post: vi.fn(),
50
+ put: vi.fn(),
51
+ delete: vi.fn(),
52
+ } as unknown as ApiClient
53
+
54
+ // Formularios genéricos con layout editor (cualquier document_form, no sólo fiscal).
55
+ const formsA: DocumentFormsManifest = {
56
+ types: [
57
+ {
58
+ key: 'doc_a',
59
+ label: 'Documento A',
60
+ layout: 'editor',
61
+ fields: [{ key: 'party', label: 'Contraparte', type: 'text' }],
62
+ },
63
+ ],
64
+ }
65
+ const formsB: DocumentFormsManifest = {
66
+ types: [
67
+ {
68
+ key: 'doc_b',
69
+ label: 'Documento B',
70
+ layout: 'editor',
71
+ submit_action: 'create_doc_b',
72
+ fields: [{ key: 'origin', label: 'Origen', type: 'text' }],
73
+ },
74
+ ],
75
+ }
76
+ const formsAB: DocumentFormsManifest = { types: [...formsA.types, ...formsB.types] }
77
+
78
+ /** Registra si el wizard llegó a montarse en el DOM, aunque sea un frame. */
79
+ function watchLegacyDialog(forbiddenText?: string) {
80
+ let seen = false
81
+ let sawText = false
82
+ const hasText = (v: string | null | undefined) => !!forbiddenText && !!v && v.includes(forbiddenText)
83
+ const hit = (n: Node) =>
84
+ n instanceof Element &&
85
+ (n.matches('[data-slot="document-form-dialog"]') || !!n.querySelector('[data-slot="document-form-dialog"]'))
86
+ const check = () => {
87
+ if (document.querySelector('[data-slot="document-form-dialog"]')) seen = true
88
+ }
89
+ // React reutiliza el mismo nodo del Dialog entre ramas y sólo cambia
90
+ // `data-slot`: se mira también el valor anterior del atributo.
91
+ const scan = (records: MutationRecord[]) => {
92
+ for (const r of records) {
93
+ if (r.type === 'attributes' && r.oldValue === 'document-form-dialog') seen = true
94
+ if (r.type === 'characterData' && hasText(r.oldValue)) sawText = true
95
+ r.addedNodes.forEach((n) => {
96
+ if (hit(n)) seen = true
97
+ if (hasText(n.textContent)) sawText = true
98
+ })
99
+ }
100
+ }
101
+ const obs = new MutationObserver(scan)
102
+ obs.observe(document.body, {
103
+ childList: true,
104
+ subtree: true,
105
+ attributes: true,
106
+ attributeOldValue: true,
107
+ attributeFilter: ['data-slot'],
108
+ characterData: true,
109
+ characterDataOldValue: true,
110
+ })
111
+ return {
112
+ check,
113
+ seen: () => {
114
+ scan(obs.takeRecords())
115
+ obs.disconnect()
116
+ return seen
117
+ },
118
+ sawText: () => sawText,
119
+ }
120
+ }
121
+
122
+ function dialog(props: { open: boolean; forms: DocumentFormsManifest; initialType?: string }) {
123
+ return (
124
+ <ApiProvider client={client}>
125
+ <DocumentFormDialog
126
+ open={props.open}
127
+ onOpenChange={() => {}}
128
+ model="docs"
129
+ endpoint="/data/docs/me"
130
+ forms={props.forms}
131
+ initialType={props.initialType}
132
+ />
133
+ </ApiProvider>
134
+ )
135
+ }
136
+
137
+ describe('DocumentFormDialog no pinta el wizard antes del DocumentEditor', () => {
138
+ it('reutilizado con otros formularios (otra vista del mismo host): abre directo el editor', () => {
139
+ const { rerender } = render(dialog({ open: false, forms: formsA }))
140
+ rerender(dialog({ open: false, forms: formsB }))
141
+ const watch = watchLegacyDialog()
142
+ rerender(dialog({ open: true, forms: formsB, initialType: 'doc_b' }))
143
+ watch.check()
144
+ expect(watch.seen()).toBe(false)
145
+ expect(document.querySelector('[data-slot="document-editor"]')).toBeTruthy()
146
+ })
147
+
148
+ it('«Crear» sin tipo inicial en la vista nueva: tampoco hay frame del wizard', () => {
149
+ const { rerender } = render(dialog({ open: false, forms: formsA }))
150
+ rerender(dialog({ open: false, forms: formsB }))
151
+ const watch = watchLegacyDialog()
152
+ rerender(dialog({ open: true, forms: formsB }))
153
+ watch.check()
154
+ expect(watch.seen()).toBe(false)
155
+ expect(document.querySelector('[data-slot="document-editor"]')).toBeTruthy()
156
+ })
157
+
158
+ it('reabierto con otro initialType: el primer frame ya es el editor del tipo pedido', () => {
159
+ const { rerender } = render(dialog({ open: true, forms: formsAB, initialType: 'doc_a' }))
160
+ rerender(dialog({ open: false, forms: formsAB, initialType: 'doc_a' }))
161
+ const watch = watchLegacyDialog('Documento A')
162
+ rerender(dialog({ open: true, forms: formsAB, initialType: 'doc_b' }))
163
+ watch.check()
164
+ expect(watch.seen()).toBe(false)
165
+ // Ni un frame del editor del tipo de la apertura anterior.
166
+ expect(watch.sawText()).toBe(false)
167
+ expect(document.querySelector('[data-slot="document-editor"]')).toBeTruthy()
168
+ })
169
+ })
@@ -211,17 +211,26 @@ export function DocumentFormDialog({
211
211
  )
212
212
  const lineCfg = type ? editorLinesConfig(type, forms) : undefined
213
213
 
214
- // Reset every time the dialog (re)opens.
215
- useEffect(() => {
216
- if (!open) return
217
- const start = initialType ?? single?.key ?? null
218
- setTypeKey(start)
219
- setStep(start ? 'fields' : 'type')
220
- setLines([])
221
- setFieldErrors({})
222
- setServerLineErrors({})
223
- setFormError(undefined)
224
- }, [open, initialType, single?.key])
214
+ // Reset every time the dialog (re)opens — DURING render, not in an effect.
215
+ // The host keeps this dialog mounted while closed (and across views whose
216
+ // forms change, e.g. the same /m/$model route going Facturas → Notas de
217
+ // crédito); an effect reset ran after the first open frame was already
218
+ // painted with the previous type/step, so the wizard (or the previous
219
+ // type's editor) flashed before the right DocumentEditor (QA Pitsline r6).
220
+ const openKey = open ? `${initialType ?? ''}\u0000${single?.key ?? ''}` : null
221
+ const [resetFor, setResetFor] = useState<string | null>(null)
222
+ if (openKey !== resetFor) {
223
+ setResetFor(openKey)
224
+ if (openKey !== null) {
225
+ const start = initialType ?? single?.key ?? null
226
+ setTypeKey(start)
227
+ setStep(start ? 'fields' : 'type')
228
+ setLines([])
229
+ setFieldErrors({})
230
+ setServerLineErrors({})
231
+ setFormError(undefined)
232
+ }
233
+ }
225
234
 
226
235
  // Seed field defaults for the chosen type.
227
236
  useEffect(() => {