@asteby/metacore-runtime-react 49.5.0 → 49.7.0
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 +12 -0
- package/dist/business/customer-picker.d.ts +8 -2
- package/dist/business/customer-picker.d.ts.map +1 -1
- package/dist/business/customer-picker.js +30 -27
- package/dist/business/index.d.ts +1 -0
- package/dist/business/index.d.ts.map +1 -1
- package/dist/business/index.js +1 -0
- package/dist/business/line-product-cell.d.ts +8 -10
- package/dist/business/line-product-cell.d.ts.map +1 -1
- package/dist/business/line-product-cell.js +13 -90
- package/dist/business/product-options.d.ts +41 -0
- package/dist/business/product-options.d.ts.map +1 -0
- package/dist/business/product-options.js +58 -0
- package/dist/business/product-picker.d.ts +5 -1
- package/dist/business/product-picker.d.ts.map +1 -1
- package/dist/business/product-picker.js +26 -35
- package/dist/business/relate-documents.d.ts +1 -1
- package/dist/business/relate-documents.d.ts.map +1 -1
- package/dist/business/relate-documents.js +6 -8
- package/dist/business/vehicle-picker.d.ts +8 -2
- package/dist/business/vehicle-picker.d.ts.map +1 -1
- package/dist/business/vehicle-picker.js +28 -28
- package/dist/dialogs/dynamic-record.d.ts.map +1 -1
- package/dist/dialogs/dynamic-record.js +28 -43
- package/dist/dynamic-multi-select-field.d.ts.map +1 -1
- package/dist/dynamic-multi-select-field.js +36 -6
- package/dist/dynamic-select-field.d.ts +9 -23
- package/dist/dynamic-select-field.d.ts.map +1 -1
- package/dist/dynamic-select-field.js +105 -172
- package/dist/entity-select.d.ts +5 -2
- package/dist/entity-select.d.ts.map +1 -1
- package/dist/entity-select.js +23 -116
- package/dist/index.d.ts +9 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +10 -1
- package/dist/query-batch.d.ts.map +1 -1
- package/dist/query-batch.js +4 -0
- package/dist/record-picker-actions.d.ts +40 -2
- package/dist/record-picker-actions.d.ts.map +1 -1
- package/dist/record-picker-actions.js +44 -4
- package/dist/record-picker-dialog.d.ts +23 -0
- package/dist/record-picker-dialog.d.ts.map +1 -0
- package/dist/record-picker-dialog.js +37 -0
- package/dist/record-picker-option.d.ts +25 -0
- package/dist/record-picker-option.d.ts.map +1 -0
- package/dist/record-picker-option.js +55 -0
- package/dist/record-picker.d.ts +149 -0
- package/dist/record-picker.d.ts.map +1 -0
- package/dist/record-picker.js +372 -0
- package/dist/use-option-ids.d.ts +34 -0
- package/dist/use-option-ids.d.ts.map +1 -0
- package/dist/use-option-ids.js +138 -0
- package/dist/use-options-resolver.d.ts +4 -0
- package/dist/use-options-resolver.d.ts.map +1 -1
- package/dist/use-options-resolver.js +10 -1
- package/package.json +1 -1
- package/src/__tests__/business-pickers.test.tsx +6 -3
- package/src/__tests__/document-editor-inline-selects.test.tsx +2 -1
- package/src/__tests__/document-form-dialog.test.tsx +4 -3
- package/src/__tests__/document-lines-grid.test.tsx +2 -1
- package/src/__tests__/edit-field-polish.test.tsx +6 -3
- package/src/__tests__/option-ids.test.tsx +181 -0
- package/src/__tests__/record-picker.test.tsx +469 -0
- package/src/business/customer-picker.tsx +87 -98
- package/src/business/index.ts +1 -0
- package/src/business/line-product-cell.tsx +55 -180
- package/src/business/product-options.tsx +119 -0
- package/src/business/product-picker.tsx +63 -126
- package/src/business/relate-documents.tsx +36 -45
- package/src/business/vehicle-picker.tsx +76 -99
- package/src/dialogs/dynamic-record.tsx +44 -97
- package/src/dynamic-multi-select-field.tsx +53 -15
- package/src/dynamic-select-field.tsx +188 -379
- package/src/entity-select.tsx +53 -202
- package/src/index.ts +35 -2
- package/src/query-batch.ts +3 -0
- package/src/record-picker-actions.tsx +101 -8
- package/src/record-picker-dialog.tsx +80 -0
- package/src/record-picker-option.tsx +96 -0
- package/src/record-picker.tsx +971 -0
- package/src/use-option-ids.ts +168 -0
- package/src/use-options-resolver.ts +11 -1
- package/dist/business/use-async-search.d.ts +0 -10
- package/dist/business/use-async-search.d.ts.map +0 -1
- package/dist/business/use-async-search.js +0 -38
- package/src/business/use-async-search.ts +0 -43
|
@@ -95,7 +95,8 @@ describe('DocumentLinesGrid — select de producto en la celda', () => {
|
|
|
95
95
|
expect(cell.closest('tr')!.getAttribute('data-line-key')).toBe(free.key)
|
|
96
96
|
fireEvent.change(cell, { target: { value: '205' } })
|
|
97
97
|
const option = await waitFor(() => screen.getByRole('option', { name: /Llanta 205/ }), { timeout: 2000 })
|
|
98
|
-
|
|
98
|
+
// La opción (role=option) es el elemento interactivo del RecordPicker.
|
|
99
|
+
fireEvent.click(option)
|
|
99
100
|
expect(state()).toHaveLength(1)
|
|
100
101
|
expect(state()[0]).toMatchObject({ key: free.key, product_id: 'p1', unit_price: 1500, sku: 'LL-205' })
|
|
101
102
|
})
|
|
@@ -169,7 +169,8 @@ describe('DocumentFormDialog: buscador de catálogo por defecto', () => {
|
|
|
169
169
|
fireEvent.change(document.querySelectorAll('input')[0], { target: { value: 'ACME' } })
|
|
170
170
|
fireEvent.click(screen.getByRole('button', { name: 'Siguiente' }))
|
|
171
171
|
fireEvent.change(await screen.findByLabelText('Buscar producto'), { target: { value: query } })
|
|
172
|
-
|
|
172
|
+
// RecordPicker: la opción del producto es role="option" clicable (sin <button> anidado).
|
|
173
|
+
fireEvent.click(await waitFor(() => screen.getByRole('option', { name }), { timeout: 2000 }))
|
|
173
174
|
}
|
|
174
175
|
|
|
175
176
|
it('sin searchProducts del host busca en el catálogo y el renglón entra con precio, cantidad 1, unidad, SKU e IVA de la org', async () => {
|
|
@@ -234,9 +235,9 @@ describe('DocumentFormDialog: IVA de la org con el buscador del host', () => {
|
|
|
234
235
|
fireEvent.click(screen.getByRole('button', { name: 'Siguiente' }))
|
|
235
236
|
const search = await screen.findByLabelText('Buscar producto')
|
|
236
237
|
fireEvent.change(search, { target: { value: 'host' } })
|
|
237
|
-
fireEvent.click(await waitFor(() => screen.getByRole('
|
|
238
|
+
fireEvent.click(await waitFor(() => screen.getByRole('option', { name: /Llanta host/ }), { timeout: 2000 }))
|
|
238
239
|
fireEvent.change(await screen.findByLabelText('Buscar producto'), { target: { value: 'fron' } })
|
|
239
|
-
fireEvent.click(await waitFor(() => screen.getByRole('
|
|
240
|
+
fireEvent.click(await waitFor(() => screen.getByRole('option', { name: /Frontera host/ }), { timeout: 2000 }))
|
|
240
241
|
fireEvent.click(screen.getByRole('button', { name: 'Crear' }))
|
|
241
242
|
await waitFor(() => expect(post).toHaveBeenCalled())
|
|
242
243
|
const body = post.mock.calls[0][1] as any
|
|
@@ -49,7 +49,8 @@ describe('DocumentLinesGrid', () => {
|
|
|
49
49
|
const search = vi.fn(async () => [tire])
|
|
50
50
|
render(<Harness search={search} priceSource="cost" />)
|
|
51
51
|
fireEvent.change(screen.getByLabelText('Buscar producto'), { target: { value: 'llan' } })
|
|
52
|
-
|
|
52
|
+
// RecordPicker: la opción del producto es role="option" clicable (sin <button> anidado).
|
|
53
|
+
const option = await waitFor(() => screen.getByRole('option', { name: /Llanta X/ }), { timeout: 2000 })
|
|
53
54
|
fireEvent.click(option)
|
|
54
55
|
expect(state()[0]).toMatchObject({ description: 'Llanta X', sku: 'LX', quantity: 1, unit_price: 800, catalog_price: 800 })
|
|
55
56
|
expect(screen.getByText(/Catálogo/)).toBeTruthy()
|
|
@@ -153,7 +153,7 @@ describe('EditField — PART 2: FK select shows the resolved label', () => {
|
|
|
153
153
|
expect(screen.queryByText('550e8400-e29b-41d4-a716-446655440000')).toBeNull()
|
|
154
154
|
})
|
|
155
155
|
|
|
156
|
-
it('keeps the raw value as fallback when no sibling was injected', () => {
|
|
156
|
+
it('keeps the raw value as fallback when no sibling was injected and the host cannot resolve ids', async () => {
|
|
157
157
|
render(
|
|
158
158
|
<ApiProvider client={noopApi}>
|
|
159
159
|
<EditField
|
|
@@ -164,7 +164,10 @@ describe('EditField — PART 2: FK select shows the resolved label', () => {
|
|
|
164
164
|
/>
|
|
165
165
|
</ApiProvider>,
|
|
166
166
|
)
|
|
167
|
-
// No sibling →
|
|
168
|
-
|
|
167
|
+
// No sibling → an `?ids=` lookup runs first ("Cargando…", never the id);
|
|
168
|
+
// this host answers without resolving it, so the raw value is the
|
|
169
|
+
// fallback on the trigger, as before.
|
|
170
|
+
expect(screen.getByText('Cargando…')).toBeTruthy()
|
|
171
|
+
expect(await screen.findByText('wh-xyz')).toBeTruthy()
|
|
169
172
|
})
|
|
170
173
|
})
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
// @vitest-environment happy-dom
|
|
2
|
+
//
|
|
3
|
+
// RecordPicker editing a saved value: the label comes from ONE `?ids=` lookup
|
|
4
|
+
// (useResolveOptionIds) without opening the popover — batched per ref, shared
|
|
5
|
+
// between pickers, skipped with a seed, "(registro eliminado)" for a gone id
|
|
6
|
+
// and the raw value when the host predates `ids`.
|
|
7
|
+
import { afterEach, describe, expect, it, vi } from 'vitest'
|
|
8
|
+
import { cleanup, render, screen, waitFor } from '@testing-library/react'
|
|
9
|
+
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
|
10
|
+
|
|
11
|
+
vi.mock('react-i18next', () => {
|
|
12
|
+
const value = { t: (k: string, o?: Record<string, any>) => o?.defaultValue ?? k, i18n: { language: 'es' } }
|
|
13
|
+
return { useTranslation: () => value }
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
import { DynamicSelectField } from '../dynamic-select-field'
|
|
17
|
+
import { DynamicMultiSelectField } from '../dynamic-multi-select-field'
|
|
18
|
+
import { ApiProvider, type ApiClient } from '../api-context'
|
|
19
|
+
import { invalidateOptionsCache } from '../use-options-resolver'
|
|
20
|
+
import type { ActionFieldDef } from '../types'
|
|
21
|
+
|
|
22
|
+
afterEach(() => {
|
|
23
|
+
cleanup()
|
|
24
|
+
invalidateOptionsCache()
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
let seq = 0
|
|
28
|
+
/** Fresh ids per test: the id-label cache is module-wide on purpose. */
|
|
29
|
+
const uid = (p: string) => `${p}-${++seq}-${Math.random().toString(36).slice(2, 8)}`
|
|
30
|
+
|
|
31
|
+
type Row = { id: string; label: string }
|
|
32
|
+
|
|
33
|
+
/** A host that understands `ids`: answers exactly the requested rows it has. */
|
|
34
|
+
function idsHost(rows: Row[], page: Row[] = rows) {
|
|
35
|
+
const get = vi.fn(async (_url: string, config?: { params?: Record<string, unknown> }) => {
|
|
36
|
+
const ids = config?.params?.ids
|
|
37
|
+
if (typeof ids === 'string') {
|
|
38
|
+
const want = ids.split(',')
|
|
39
|
+
return { data: { success: true, data: rows.filter((r) => want.includes(r.id)), meta: { type: 'dynamic' } } }
|
|
40
|
+
}
|
|
41
|
+
return { data: { success: true, data: page, meta: { type: 'dynamic' } } }
|
|
42
|
+
})
|
|
43
|
+
// batch=false: no POST /q in these tests (ids reads bypass it anyway).
|
|
44
|
+
const api = { get, post: vi.fn(async () => ({ data: {} })), put: vi.fn(), delete: vi.fn() } as unknown as ApiClient
|
|
45
|
+
return { api, get, idsCalls: () => get.mock.calls.filter(([, c]) => typeof c?.params?.ids === 'string') }
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const brand: ActionFieldDef = { key: 'brand_id', label: 'Marca', type: 'dynamic_select', ref: 'Brand' }
|
|
49
|
+
|
|
50
|
+
describe('RecordPicker: etiqueta de un valor guardado por ?ids=', () => {
|
|
51
|
+
it('sin semilla → una sola petición ids y muestra el nombre sin abrir el popover', async () => {
|
|
52
|
+
const id = uid('b')
|
|
53
|
+
const host = idsHost([{ id, label: 'Michelin' }])
|
|
54
|
+
render(
|
|
55
|
+
<ApiProvider client={host.api}>
|
|
56
|
+
<DynamicSelectField field={brand} value={id} onChange={vi.fn()} />
|
|
57
|
+
</ApiProvider>,
|
|
58
|
+
)
|
|
59
|
+
// Mientras resuelve: "Cargando…", nunca el UUID.
|
|
60
|
+
expect(screen.getByText('Cargando…')).toBeTruthy()
|
|
61
|
+
expect(screen.queryByText(id)).toBeNull()
|
|
62
|
+
expect(await screen.findByText('Michelin')).toBeTruthy()
|
|
63
|
+
expect(host.get).toHaveBeenCalledTimes(1)
|
|
64
|
+
expect(host.get).toHaveBeenCalledWith('/options/Brand', { params: { field: 'id', ids: id } })
|
|
65
|
+
expect(document.querySelector('[data-slot="record-picker-content"]')).toBeNull()
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
it('dos pickers de la misma ref (y uno repetido) → UNA petición con todos los ids', async () => {
|
|
69
|
+
const a = uid('b')
|
|
70
|
+
const b = uid('b')
|
|
71
|
+
const host = idsHost([
|
|
72
|
+
{ id: a, label: 'Michelin' },
|
|
73
|
+
{ id: b, label: 'Pirelli' },
|
|
74
|
+
])
|
|
75
|
+
render(
|
|
76
|
+
<ApiProvider client={host.api}>
|
|
77
|
+
<DynamicSelectField field={brand} value={a} onChange={vi.fn()} />
|
|
78
|
+
<DynamicSelectField field={{ ...brand, key: 'alt_brand_id' }} value={b} onChange={vi.fn()} />
|
|
79
|
+
<DynamicSelectField field={{ ...brand, key: 'other_brand_id' }} value={a} onChange={vi.fn()} />
|
|
80
|
+
</ApiProvider>,
|
|
81
|
+
)
|
|
82
|
+
await waitFor(() => expect(screen.getAllByText('Michelin')).toHaveLength(2))
|
|
83
|
+
expect(screen.getByText('Pirelli')).toBeTruthy()
|
|
84
|
+
expect(host.get).toHaveBeenCalledTimes(1)
|
|
85
|
+
const ids = String(host.idsCalls()[0][1].params.ids).split(',').sort()
|
|
86
|
+
expect(ids).toEqual([a, b].sort())
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
it('con semilla → cero peticiones', async () => {
|
|
90
|
+
const id = uid('b')
|
|
91
|
+
const host = idsHost([{ id, label: 'Michelin' }])
|
|
92
|
+
render(
|
|
93
|
+
<ApiProvider client={host.api}>
|
|
94
|
+
<DynamicSelectField
|
|
95
|
+
field={brand}
|
|
96
|
+
value={id}
|
|
97
|
+
onChange={vi.fn()}
|
|
98
|
+
seedOption={{ id, value: id, label: 'Bridgestone', name: 'Bridgestone' }}
|
|
99
|
+
/>
|
|
100
|
+
</ApiProvider>,
|
|
101
|
+
)
|
|
102
|
+
expect(screen.getByText('Bridgestone')).toBeTruthy()
|
|
103
|
+
await new Promise((r) => setTimeout(r, 30))
|
|
104
|
+
expect(host.get).not.toHaveBeenCalled()
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
it('id que ya no existe → «(registro eliminado)» sutil, no el UUID', async () => {
|
|
108
|
+
const id = uid('b')
|
|
109
|
+
const host = idsHost([])
|
|
110
|
+
render(
|
|
111
|
+
<ApiProvider client={host.api}>
|
|
112
|
+
<DynamicSelectField field={brand} value={id} onChange={vi.fn()} />
|
|
113
|
+
</ApiProvider>,
|
|
114
|
+
)
|
|
115
|
+
const missing = await screen.findByText('(registro eliminado)')
|
|
116
|
+
expect(missing.getAttribute('data-slot')).toBe('record-picker-value-missing')
|
|
117
|
+
expect(screen.queryByText(id)).toBeNull()
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
it('host sin soporte de ids (responde una página): filtra por id; si no está, muestra el valor como antes', async () => {
|
|
121
|
+
const known = uid('b')
|
|
122
|
+
const unknown = uid('b')
|
|
123
|
+
const page = [
|
|
124
|
+
{ id: known, label: 'Michelin' },
|
|
125
|
+
{ id: uid('x'), label: 'Otra' },
|
|
126
|
+
]
|
|
127
|
+
// Ignores `ids` entirely.
|
|
128
|
+
const get = vi.fn(async () => ({ data: { success: true, data: page } }))
|
|
129
|
+
const api = { get, post: vi.fn(async () => ({ data: {} })), put: vi.fn(), delete: vi.fn() } as unknown as ApiClient
|
|
130
|
+
render(
|
|
131
|
+
<ApiProvider client={api}>
|
|
132
|
+
<DynamicSelectField field={brand} value={known} onChange={vi.fn()} />
|
|
133
|
+
<DynamicSelectField field={{ ...brand, key: 'b2' }} value={unknown} onChange={vi.fn()} />
|
|
134
|
+
</ApiProvider>,
|
|
135
|
+
)
|
|
136
|
+
expect(await screen.findByText('Michelin')).toBeTruthy()
|
|
137
|
+
expect(await screen.findByText(unknown)).toBeTruthy()
|
|
138
|
+
expect(screen.queryByText('(registro eliminado)')).toBeNull()
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
it('usa el QueryClient del host cuando hay QueryClientProvider', async () => {
|
|
142
|
+
const id = uid('b')
|
|
143
|
+
const host = idsHost([{ id, label: 'Goodyear' }])
|
|
144
|
+
const client = new QueryClient()
|
|
145
|
+
render(
|
|
146
|
+
<QueryClientProvider client={client}>
|
|
147
|
+
<ApiProvider client={host.api}>
|
|
148
|
+
<DynamicSelectField field={brand} value={id} onChange={vi.fn()} />
|
|
149
|
+
</ApiProvider>
|
|
150
|
+
</QueryClientProvider>,
|
|
151
|
+
)
|
|
152
|
+
expect(await screen.findByText('Goodyear')).toBeTruthy()
|
|
153
|
+
expect(client.getQueryCache().findAll({ queryKey: ['metacore-option-ids'] })).toHaveLength(1)
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
it('múltiple: los ids fuera de la página se resuelven en una petición; el borrado se marca', async () => {
|
|
157
|
+
const inPage = uid('t')
|
|
158
|
+
const outside = uid('t')
|
|
159
|
+
const gone = uid('t')
|
|
160
|
+
const host = idsHost(
|
|
161
|
+
[
|
|
162
|
+
{ id: inPage, label: 'Mayoreo' },
|
|
163
|
+
{ id: outside, label: 'Flotillas' },
|
|
164
|
+
],
|
|
165
|
+
[{ id: inPage, label: 'Mayoreo' }],
|
|
166
|
+
)
|
|
167
|
+
const field: ActionFieldDef = { key: 'segment_ids', label: 'Segmentos', type: 'dynamic_select', ref: 'Segment', multiple: true }
|
|
168
|
+
render(
|
|
169
|
+
<ApiProvider client={host.api} batch={false}>
|
|
170
|
+
<DynamicMultiSelectField field={field} value={[inPage, outside, gone]} onChange={vi.fn()} />
|
|
171
|
+
</ApiProvider>,
|
|
172
|
+
)
|
|
173
|
+
expect(await screen.findByText('Flotillas')).toBeTruthy()
|
|
174
|
+
expect(screen.getByText('Mayoreo')).toBeTruthy()
|
|
175
|
+
expect(await screen.findByText('(registro eliminado)')).toBeTruthy()
|
|
176
|
+
expect(screen.queryByText(outside)).toBeNull()
|
|
177
|
+
const calls = host.idsCalls()
|
|
178
|
+
expect(calls).toHaveLength(1)
|
|
179
|
+
expect(String(calls[0][1].params.ids).split(',').sort()).toEqual([outside, gone].sort())
|
|
180
|
+
})
|
|
181
|
+
})
|