@asteby/metacore-runtime-react 49.0.0 → 49.2.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 +45 -0
- package/dist/action-modal-dispatcher.d.ts +18 -1
- package/dist/action-modal-dispatcher.d.ts.map +1 -1
- package/dist/action-modal-dispatcher.js +66 -11
- package/dist/business/document-editor-model.d.ts +6 -1
- package/dist/business/document-editor-model.d.ts.map +1 -1
- package/dist/business/document-editor-model.js +48 -7
- package/dist/business/document-editor-panels.d.ts +18 -0
- package/dist/business/document-editor-panels.d.ts.map +1 -1
- package/dist/business/document-editor-panels.js +9 -0
- package/dist/business/document-editor.d.ts +7 -1
- package/dist/business/document-editor.d.ts.map +1 -1
- package/dist/business/document-editor.js +72 -26
- package/dist/calendar-date.d.ts +12 -0
- package/dist/calendar-date.d.ts.map +1 -1
- package/dist/calendar-date.js +36 -0
- package/dist/document-form-dialog.d.ts +23 -2
- package/dist/document-form-dialog.d.ts.map +1 -1
- package/dist/document-form-dialog.js +49 -8
- package/dist/dynamic-crud-page.d.ts +7 -0
- package/dist/dynamic-crud-page.d.ts.map +1 -1
- package/dist/dynamic-crud-page.js +5 -4
- package/dist/index.d.ts +4 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -3
- package/dist/record-mutation-events.d.ts +12 -0
- package/dist/record-mutation-events.d.ts.map +1 -1
- package/dist/record-mutation-events.js +18 -0
- package/dist/types.d.ts +7 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/__tests__/document-editor-model.test.ts +27 -0
- package/src/__tests__/document-editor.test.tsx +60 -0
- package/src/__tests__/document-form-create-model.test.tsx +143 -0
- package/src/__tests__/field-default-today.test.tsx +116 -0
- package/src/__tests__/invoice-document-editor.test.tsx +267 -0
- package/src/__tests__/r5-self-label-settled.test.ts +57 -0
- package/src/action-modal-dispatcher.tsx +78 -11
- package/src/business/document-editor-model.ts +50 -7
- package/src/business/document-editor-panels.tsx +49 -0
- package/src/business/document-editor.tsx +249 -166
- package/src/calendar-date.ts +36 -0
- package/src/document-form-dialog.tsx +72 -8
- package/src/dynamic-crud-page.tsx +20 -2
- package/src/index.ts +8 -0
- package/src/record-mutation-events.ts +23 -0
- package/src/types.ts +7 -0
|
@@ -23,7 +23,7 @@ import {
|
|
|
23
23
|
import { Loader2 } from 'lucide-react'
|
|
24
24
|
import { toast } from 'sonner'
|
|
25
25
|
import { useApi } from './api-context'
|
|
26
|
-
import { useOrgTaxRate } from './org-runtime-context'
|
|
26
|
+
import { useOrgTaxRate, useTimeZone } from './org-runtime-context'
|
|
27
27
|
import { DynamicIcon } from './dynamic-icon'
|
|
28
28
|
import { FieldCell, FieldGrid, FieldLabel } from './field-grid'
|
|
29
29
|
import { FormErrorBanner } from './business/feedback'
|
|
@@ -45,9 +45,10 @@ import { emitRecordMutation } from './record-mutation-events'
|
|
|
45
45
|
import { isLineItemsField, resolveWidget, scopeValueFromFilterToken } from './dynamic-form-schema'
|
|
46
46
|
import { createCatalogProductSearch, withDefaultTaxRate } from './business/catalog-product-search'
|
|
47
47
|
import { DocumentEditor } from './business/document-editor'
|
|
48
|
-
import { editorLinesConfig, isEditorLayout } from './business/document-editor-model'
|
|
48
|
+
import { editorLinesConfig, isEditorLayout, isFullscreenEditor } from './business/document-editor-model'
|
|
49
49
|
import type {
|
|
50
50
|
ActionFieldDef,
|
|
51
|
+
DocumentFormType,
|
|
51
52
|
DocumentFormsManifest,
|
|
52
53
|
TableMetadata,
|
|
53
54
|
} from './types'
|
|
@@ -82,6 +83,39 @@ export interface DocumentFormDialogProps {
|
|
|
82
83
|
record?: Record<string, any>
|
|
83
84
|
/** Documento origen a precargar en el DocumentEditor (`sources[].key` + id). */
|
|
84
85
|
initialSource?: { key: string; id: string }
|
|
86
|
+
/**
|
|
87
|
+
* Tipo con `create_model`: el alta vive en otro modelo. El host navega a la
|
|
88
|
+
* página de ese modelo con su alta abierta; el diálogo se cierra sin pintar
|
|
89
|
+
* formulario. Sin este callback un tipo delegado no se ofrece.
|
|
90
|
+
*/
|
|
91
|
+
onDelegateCreate?: (target: DelegatedCreate) => void
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Alta delegada a otro modelo (`document_forms.types[].create_model`). */
|
|
95
|
+
export interface DelegatedCreate {
|
|
96
|
+
/** Clave del modelo destino tal como la declara el manifest (`customers.Invoice`). */
|
|
97
|
+
model: string
|
|
98
|
+
/** Tipo que la declaró. */
|
|
99
|
+
type: DocumentFormType
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Si «Crear» de estos formularios es un alta delegada: un único tipo (p. ej.
|
|
104
|
+
* ya acotado por el filtro de la vista) con `create_model`. Con varios tipos el
|
|
105
|
+
* selector la ofrece como una tarjeta más.
|
|
106
|
+
*/
|
|
107
|
+
export function delegatedCreate(forms: DocumentFormsManifest | undefined): DelegatedCreate | undefined {
|
|
108
|
+
const only = forms?.types.length === 1 ? forms.types[0] : undefined
|
|
109
|
+
const model = only?.create_model?.trim()
|
|
110
|
+
return only && model ? { model, type: only } : undefined
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** Quita los tipos delegados cuando el host no sabe navegar a otro modelo. */
|
|
114
|
+
export function withoutDelegatedTypes(forms: DocumentFormsManifest | undefined): DocumentFormsManifest | undefined {
|
|
115
|
+
if (!forms) return forms
|
|
116
|
+
const types = forms.types.filter((x) => !x.create_model?.trim())
|
|
117
|
+
if (types.length === forms.types.length) return forms
|
|
118
|
+
return types.length > 0 ? { ...forms, types } : undefined
|
|
85
119
|
}
|
|
86
120
|
|
|
87
121
|
/** Manifest efectivo: la prop gana sobre el metadata; sin tipos devuelve undefined. */
|
|
@@ -130,11 +164,17 @@ export function DocumentFormDialog({
|
|
|
130
164
|
currency,
|
|
131
165
|
record,
|
|
132
166
|
initialSource,
|
|
167
|
+
onDelegateCreate,
|
|
133
168
|
}: DocumentFormDialogProps) {
|
|
134
169
|
const { t, i18n } = useTranslation()
|
|
135
170
|
const api = useApi()
|
|
136
171
|
const orgTaxRate = useOrgTaxRate()
|
|
137
|
-
const
|
|
172
|
+
const orgTimeZone = useTimeZone()
|
|
173
|
+
// Sin onDelegateCreate el host no sabe abrir otro modelo: sus tipos no se ofrecen.
|
|
174
|
+
const types = useMemo(
|
|
175
|
+
() => (onDelegateCreate ? forms.types : forms.types.filter((x) => !x.create_model?.trim())),
|
|
176
|
+
[forms.types, onDelegateCreate],
|
|
177
|
+
)
|
|
138
178
|
const single = types.length === 1 ? types[0] : undefined
|
|
139
179
|
|
|
140
180
|
const [typeKey, setTypeKey] = useState<string | null>(initialType ?? single?.key ?? null)
|
|
@@ -147,6 +187,16 @@ export function DocumentFormDialog({
|
|
|
147
187
|
const [saving, setSaving] = useState(false)
|
|
148
188
|
|
|
149
189
|
const type = useMemo(() => types.find((x) => x.key === typeKey) ?? null, [types, typeKey])
|
|
190
|
+
const delegatedModel = type?.create_model?.trim() || undefined
|
|
191
|
+
|
|
192
|
+
// Tipo con alta en otro modelo: no hay formulario aquí; el host navega.
|
|
193
|
+
useEffect(() => {
|
|
194
|
+
if (!open || !type || !delegatedModel) return
|
|
195
|
+
onDelegateCreate?.({ model: delegatedModel, type })
|
|
196
|
+
onOpenChange(false)
|
|
197
|
+
// onDelegateCreate/onOpenChange: callbacks del host, no disparan de nuevo.
|
|
198
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
199
|
+
}, [open, type, delegatedModel])
|
|
150
200
|
// Sin buscador inyectado, el paso de renglones solo ofrecía «Renglón libre»
|
|
151
201
|
// y la línea entraba sin precio (DynamicCRUDPage no pasa searchProducts).
|
|
152
202
|
// Default: el catálogo de productos de la org. El buscador del host también
|
|
@@ -176,10 +226,13 @@ export function DocumentFormDialog({
|
|
|
176
226
|
// Seed field defaults for the chosen type.
|
|
177
227
|
useEffect(() => {
|
|
178
228
|
if (!type) return
|
|
179
|
-
setFormData({
|
|
229
|
+
setFormData({
|
|
230
|
+
...buildFieldDefaults(type.fields, record, undefined, { timeZone: orgTimeZone }),
|
|
231
|
+
...(type.defaults ?? {}),
|
|
232
|
+
})
|
|
180
233
|
setFieldErrors({})
|
|
181
234
|
setFormError(undefined)
|
|
182
|
-
}, [type, record])
|
|
235
|
+
}, [type, record, orgTimeZone])
|
|
183
236
|
|
|
184
237
|
const updateField = (key: string, value: any) => {
|
|
185
238
|
setFormData((prev) => ({ ...prev, [key]: value }))
|
|
@@ -280,15 +333,25 @@ export function DocumentFormDialog({
|
|
|
280
333
|
? t('documentForm.step_data', { defaultValue: 'Datos' })
|
|
281
334
|
: tl(lineCfg?.title ?? 'Renglones')
|
|
282
335
|
|
|
336
|
+
if (open && type && delegatedModel) return null
|
|
337
|
+
|
|
283
338
|
// layout "editor": una sola pantalla por secciones (DocumentEditor). Sin él
|
|
284
|
-
// sigue el wizard de siempre (retrocompatible).
|
|
339
|
+
// sigue el wizard de siempre (retrocompatible). Un documento con renglones
|
|
340
|
+
// (factura, cotización, pedido, OC) ocupa toda la pantalla; NC y cobro
|
|
341
|
+
// siguen en el diálogo grande.
|
|
285
342
|
if (open && type && step !== 'type' && (isEditorLayout(type) || !!initialSource)) {
|
|
343
|
+
const fullscreen = isFullscreenEditor(type, forms)
|
|
286
344
|
return (
|
|
287
345
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
288
346
|
<DialogContent
|
|
289
|
-
className=
|
|
290
|
-
style={
|
|
347
|
+
className={fullscreen ? 'flex flex-col overflow-hidden sm:max-w-none' : 'flex max-h-[92dvh] flex-col overflow-hidden'}
|
|
348
|
+
style={
|
|
349
|
+
fullscreen
|
|
350
|
+
? { width: 'calc(100vw - 2rem)', maxWidth: '1440px', height: 'calc(100dvh - 2rem)', maxHeight: 'calc(100dvh - 2rem)' }
|
|
351
|
+
: { maxHeight: '92dvh', maxWidth: '960px', width: '95vw' }
|
|
352
|
+
}
|
|
291
353
|
data-slot="document-editor-dialog"
|
|
354
|
+
data-fullscreen={fullscreen ? 'true' : undefined}
|
|
292
355
|
>
|
|
293
356
|
<DialogHeader className="shrink-0">
|
|
294
357
|
<DialogTitle>{tl(type.label)}</DialogTitle>
|
|
@@ -304,6 +367,7 @@ export function DocumentFormDialog({
|
|
|
304
367
|
initialSource={initialSource}
|
|
305
368
|
searchProducts={lineSearch}
|
|
306
369
|
currency={currency}
|
|
370
|
+
fullscreen={fullscreen}
|
|
307
371
|
onCancel={() => (canGoBackToTypes ? setStep('type') : onOpenChange(false))}
|
|
308
372
|
onSaved={(rec) => {
|
|
309
373
|
onSaved?.(rec)
|
|
@@ -33,7 +33,13 @@ import { useApi } from './api-context'
|
|
|
33
33
|
import { useMetadataCache } from './metadata-cache'
|
|
34
34
|
import { DynamicTable } from './dynamic-table'
|
|
35
35
|
import { DynamicRecordDialog } from './dialogs/dynamic-record'
|
|
36
|
-
import {
|
|
36
|
+
import {
|
|
37
|
+
DocumentFormDialog,
|
|
38
|
+
resolveDocumentForms,
|
|
39
|
+
scopeDocumentFormsToFilter,
|
|
40
|
+
withoutDelegatedTypes,
|
|
41
|
+
type DelegatedCreate,
|
|
42
|
+
} from './document-form-dialog'
|
|
37
43
|
import { ExportDialog } from './dialogs/export'
|
|
38
44
|
import { ImportDialog } from './dialogs/import'
|
|
39
45
|
import { getModelExtension } from './model-extension-registry'
|
|
@@ -108,6 +114,12 @@ export interface DynamicCRUDPageProps {
|
|
|
108
114
|
* `?create=<tipo>&from=<fuente>&from_id=<id>`. Solo con document_forms.
|
|
109
115
|
*/
|
|
110
116
|
initialCreate?: { type?: string; source?: { key: string; id: string } }
|
|
117
|
+
/**
|
|
118
|
+
* Alta delegada (`document_forms.types[].create_model`): el host abre la
|
|
119
|
+
* página del modelo destino con su alta abierta. Sin él, esos tipos no se
|
|
120
|
+
* ofrecen (y si la vista queda acotada a uno, no hay «Crear»).
|
|
121
|
+
*/
|
|
122
|
+
onDelegateCreate?: (target: DelegatedCreate) => void
|
|
111
123
|
}
|
|
112
124
|
|
|
113
125
|
/**
|
|
@@ -133,6 +145,7 @@ export function DynamicCRUDPage(props: DynamicCRUDPageProps) {
|
|
|
133
145
|
documentForms,
|
|
134
146
|
filter,
|
|
135
147
|
initialCreate,
|
|
148
|
+
onDelegateCreate,
|
|
136
149
|
} = props
|
|
137
150
|
|
|
138
151
|
const strings = { ...defaultStrings, ...(i18n ?? {}) }
|
|
@@ -203,7 +216,11 @@ export function DynamicCRUDPage(props: DynamicCRUDPageProps) {
|
|
|
203
216
|
// (useCan defaults to always-true), in which case create/export/import
|
|
204
217
|
// require `lowercase(model).create|export|import`.
|
|
205
218
|
const can = useCan()
|
|
206
|
-
const
|
|
219
|
+
const declaredForms = resolveDocumentForms(metadata, documentForms)
|
|
220
|
+
const scoped = scopeDocumentFormsToFilter(
|
|
221
|
+
onDelegateCreate ? declaredForms : withoutDelegatedTypes(declaredForms),
|
|
222
|
+
filter,
|
|
223
|
+
)
|
|
207
224
|
const guidedForms = scoped.forms
|
|
208
225
|
const showCreate = primary.showGenericCreate && !scoped.hideCreate && can(modelCapability(model, 'create'))
|
|
209
226
|
const showImport = enableCRUD && !effectiveHideImport && can(modelCapability(model, 'import'))
|
|
@@ -318,6 +335,7 @@ export function DynamicCRUDPage(props: DynamicCRUDPageProps) {
|
|
|
318
335
|
initialType={initialCreate?.type}
|
|
319
336
|
initialSource={initialCreate?.source}
|
|
320
337
|
onSaved={handleRefresh}
|
|
338
|
+
onDelegateCreate={onDelegateCreate}
|
|
321
339
|
/>
|
|
322
340
|
)}
|
|
323
341
|
|
package/src/index.ts
CHANGED
|
@@ -179,8 +179,11 @@ export { FieldGrid, FieldCell, FieldLabel } from './field-grid'
|
|
|
179
179
|
export {
|
|
180
180
|
ActionModalDispatcher,
|
|
181
181
|
FEDERATED_ACTION_MODAL_TIMEOUT_MS,
|
|
182
|
+
resolveFieldDefault,
|
|
182
183
|
type ActionModalProps,
|
|
184
|
+
type FieldDefaultContext,
|
|
183
185
|
} from './action-modal-dispatcher'
|
|
186
|
+
export { todayInZone, isTodayToken } from './calendar-date'
|
|
184
187
|
export { setFederatedActionLoader, type FederatedActionLoader } from './federated-action-loader'
|
|
185
188
|
export {
|
|
186
189
|
ModelActionToolbar,
|
|
@@ -198,8 +201,10 @@ export {
|
|
|
198
201
|
} from './list-primary-action'
|
|
199
202
|
export {
|
|
200
203
|
emitRecordMutation,
|
|
204
|
+
emitRecordMutationSettled,
|
|
201
205
|
subscribeRecordMutations,
|
|
202
206
|
RECORD_MUTATION_EVENT,
|
|
207
|
+
RECORD_MUTATION_SETTLE_DELAYS,
|
|
203
208
|
type RecordMutationKind,
|
|
204
209
|
type RecordMutationDetail,
|
|
205
210
|
} from './record-mutation-events'
|
|
@@ -207,6 +212,9 @@ export {
|
|
|
207
212
|
DocumentFormDialog,
|
|
208
213
|
resolveDocumentForms,
|
|
209
214
|
scopeDocumentFormsToFilter,
|
|
215
|
+
delegatedCreate,
|
|
216
|
+
withoutDelegatedTypes,
|
|
217
|
+
type DelegatedCreate,
|
|
210
218
|
type DocumentFormDialogProps,
|
|
211
219
|
} from './document-form-dialog'
|
|
212
220
|
export {
|
|
@@ -31,6 +31,29 @@ export function emitRecordMutation(model: string | undefined | null, kind: Recor
|
|
|
31
31
|
}
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
+
/**
|
|
35
|
+
* Delays (ms) at which a mutation is announced again. The backend's event
|
|
36
|
+
* subscribers write AFTER the action answered (stamping → the invoice's state,
|
|
37
|
+
* a payment → its auto-stamped REP), so a list or tab that reloaded at once kept
|
|
38
|
+
* showing the previous state until a manual reload (retest Pitsline r5).
|
|
39
|
+
*/
|
|
40
|
+
export const RECORD_MUTATION_SETTLE_DELAYS: readonly number[] = [1500, 5000]
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Announce a mutation now and again after `delays`, so what the server derives
|
|
44
|
+
* asynchronously from it reaches mounted lists too. Returns a cancel function.
|
|
45
|
+
*/
|
|
46
|
+
export function emitRecordMutationSettled(
|
|
47
|
+
model: string | undefined | null,
|
|
48
|
+
kind: RecordMutationKind,
|
|
49
|
+
delays: readonly number[] = RECORD_MUTATION_SETTLE_DELAYS,
|
|
50
|
+
): () => void {
|
|
51
|
+
emitRecordMutation(model, kind)
|
|
52
|
+
if (!model || typeof window === 'undefined') return () => {}
|
|
53
|
+
const timers = delays.map((ms) => window.setTimeout(() => emitRecordMutation(model, kind), ms))
|
|
54
|
+
return () => timers.forEach((t) => window.clearTimeout(t))
|
|
55
|
+
}
|
|
56
|
+
|
|
34
57
|
/**
|
|
35
58
|
* Listen for mutations of `model` (case-insensitive). Returns an unsubscribe.
|
|
36
59
|
*/
|
package/src/types.ts
CHANGED
|
@@ -107,6 +107,13 @@ export interface DocumentFormType {
|
|
|
107
107
|
* reparte el pago en PaymentAllocation dentro de una transacción.
|
|
108
108
|
*/
|
|
109
109
|
submit_action?: string
|
|
110
|
+
/**
|
|
111
|
+
* El alta de este tipo vive en OTRO modelo (`customers.Invoice`): «Crear»
|
|
112
|
+
* no pinta formulario, el host abre la página de ese modelo con su alta
|
|
113
|
+
* abierta (`onDelegateCreate`). P. ej. Documentos fiscales → Facturas: la
|
|
114
|
+
* factura nace en customers y se timbra después. Sin campos ni renglones.
|
|
115
|
+
*/
|
|
116
|
+
create_model?: string
|
|
110
117
|
}
|
|
111
118
|
|
|
112
119
|
/** Contraparte: qué campo del encabezado la elige y qué muestra su tarjeta. */
|