@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,
|
|
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
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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
|
@@ -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
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
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(() => {
|