@asteby/metacore-runtime-react 47.1.0 → 49.0.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 +51 -0
- package/dist/action-modal-dispatcher.d.ts +17 -3
- package/dist/action-modal-dispatcher.d.ts.map +1 -1
- package/dist/action-modal-dispatcher.js +80 -13
- package/dist/action-priority.d.ts +34 -0
- package/dist/action-priority.d.ts.map +1 -0
- package/dist/action-priority.js +60 -0
- package/dist/business/catalog-product-search.d.ts +32 -0
- package/dist/business/catalog-product-search.d.ts.map +1 -0
- package/dist/business/catalog-product-search.js +71 -0
- package/dist/business/catalog.d.ts.map +1 -1
- package/dist/business/catalog.js +2 -0
- package/dist/business/document-editor-model.d.ts +119 -0
- package/dist/business/document-editor-model.d.ts.map +1 -0
- package/dist/business/document-editor-model.js +317 -0
- package/dist/business/document-editor-panels.d.ts +64 -0
- package/dist/business/document-editor-panels.d.ts.map +1 -0
- package/dist/business/document-editor-panels.js +58 -0
- package/dist/business/document-editor.d.ts +27 -0
- package/dist/business/document-editor.d.ts.map +1 -0
- package/dist/business/document-editor.js +493 -0
- package/dist/business/document-lines.d.ts.map +1 -1
- package/dist/business/document-lines.js +5 -1
- package/dist/business/index.d.ts +5 -0
- package/dist/business/index.d.ts.map +1 -1
- package/dist/business/index.js +5 -0
- package/dist/business/line-items.d.ts +16 -1
- package/dist/business/line-items.d.ts.map +1 -1
- package/dist/business/line-items.js +14 -2
- package/dist/business/payment-allocator.d.ts +19 -0
- package/dist/business/payment-allocator.d.ts.map +1 -0
- package/dist/business/payment-allocator.js +31 -0
- package/dist/business/product-search.d.ts +4 -0
- package/dist/business/product-search.d.ts.map +1 -1
- package/dist/dialogs/dynamic-record.d.ts.map +1 -1
- package/dist/dialogs/dynamic-record.js +14 -7
- package/dist/document/document-page.d.ts.map +1 -1
- package/dist/document/document-page.js +8 -1
- package/dist/document/evaluate.d.ts.map +1 -1
- package/dist/document/evaluate.js +17 -1
- package/dist/document/index.d.ts +1 -0
- package/dist/document/index.d.ts.map +1 -1
- package/dist/document/index.js +1 -0
- package/dist/document/secondary-bar.d.ts +20 -0
- package/dist/document/secondary-bar.d.ts.map +1 -0
- package/dist/document/secondary-bar.js +44 -0
- package/dist/document/types.d.ts +8 -0
- package/dist/document/types.d.ts.map +1 -1
- package/dist/document-form-dialog.d.ts +31 -2
- package/dist/document-form-dialog.d.ts.map +1 -1
- package/dist/document-form-dialog.js +46 -11
- package/dist/dynamic-columns-helpers.d.ts +9 -0
- package/dist/dynamic-columns-helpers.d.ts.map +1 -1
- package/dist/dynamic-columns-helpers.js +19 -0
- package/dist/dynamic-columns.d.ts +9 -13
- package/dist/dynamic-columns.d.ts.map +1 -1
- package/dist/dynamic-columns.js +27 -21
- package/dist/dynamic-crud-page.d.ts +18 -0
- package/dist/dynamic-crud-page.d.ts.map +1 -1
- package/dist/dynamic-crud-page.js +15 -6
- package/dist/dynamic-form-schema.d.ts.map +1 -1
- package/dist/dynamic-form-schema.js +15 -3
- package/dist/dynamic-form.d.ts +16 -1
- package/dist/dynamic-form.d.ts.map +1 -1
- package/dist/dynamic-form.js +78 -20
- package/dist/dynamic-kanban.d.ts +9 -3
- package/dist/dynamic-kanban.d.ts.map +1 -1
- package/dist/dynamic-kanban.js +31 -12
- package/dist/dynamic-relation-helpers.d.ts.map +1 -1
- package/dist/dynamic-relation-helpers.js +8 -0
- package/dist/dynamic-relation.js +1 -1
- package/dist/dynamic-row-actions.d.ts +8 -0
- package/dist/dynamic-row-actions.d.ts.map +1 -1
- package/dist/dynamic-row-actions.js +12 -2
- package/dist/dynamic-table.d.ts.map +1 -1
- package/dist/dynamic-table.js +64 -63
- package/dist/index.d.ts +7 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +19 -2
- package/dist/installed-addons-context.d.ts +15 -0
- package/dist/installed-addons-context.d.ts.map +1 -0
- package/dist/installed-addons-context.js +39 -0
- package/dist/org-runtime-context.d.ts +8 -0
- package/dist/org-runtime-context.d.ts.map +1 -1
- package/dist/org-runtime-context.js +8 -0
- package/dist/org-runtime-provider.d.ts +3 -1
- package/dist/org-runtime-provider.d.ts.map +1 -1
- package/dist/org-runtime-provider.js +3 -3
- package/dist/primitives/aging.d.ts +34 -0
- package/dist/primitives/aging.d.ts.map +1 -0
- package/dist/primitives/aging.js +48 -0
- package/dist/primitives/allocation.d.ts +42 -0
- package/dist/primitives/allocation.d.ts.map +1 -0
- package/dist/primitives/allocation.js +59 -0
- package/dist/primitives/approvals.d.ts +71 -0
- package/dist/primitives/approvals.d.ts.map +1 -0
- package/dist/primitives/approvals.js +48 -0
- package/dist/primitives/contributions.d.ts +93 -0
- package/dist/primitives/contributions.d.ts.map +1 -0
- package/dist/primitives/contributions.js +163 -0
- package/dist/primitives/document-kinds.d.ts +25 -0
- package/dist/primitives/document-kinds.d.ts.map +1 -0
- package/dist/primitives/document-kinds.js +26 -0
- package/dist/primitives/index.d.ts +8 -0
- package/dist/primitives/index.d.ts.map +1 -0
- package/dist/primitives/index.js +23 -0
- package/dist/primitives/panels.d.ts +68 -0
- package/dist/primitives/panels.d.ts.map +1 -0
- package/dist/primitives/panels.js +3 -0
- package/dist/primitives/use-contributions.d.ts +15 -0
- package/dist/primitives/use-contributions.d.ts.map +1 -0
- package/dist/primitives/use-contributions.js +30 -0
- package/dist/record-actions.d.ts +21 -0
- package/dist/record-actions.d.ts.map +1 -0
- package/dist/record-actions.js +76 -0
- package/dist/record-prefill-bar.d.ts +11 -0
- package/dist/record-prefill-bar.d.ts.map +1 -0
- package/dist/record-prefill-bar.js +33 -0
- package/dist/row-actions-menu.d.ts +32 -0
- package/dist/row-actions-menu.d.ts.map +1 -0
- package/dist/row-actions-menu.js +59 -0
- package/dist/slot.d.ts +4 -21
- package/dist/slot.d.ts.map +1 -1
- package/dist/slot.js +14 -36
- package/dist/types.d.ts +134 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/__tests__/catalog-product-search.test.ts +61 -0
- package/src/__tests__/document-editor-model.test.ts +221 -0
- package/src/__tests__/document-editor.test.tsx +340 -0
- package/src/__tests__/document-form-dialog.test.tsx +158 -3
- package/src/__tests__/dynamic-form-select-edit.test.tsx +155 -0
- package/src/__tests__/dynamic-kanban.test.tsx +24 -3
- package/src/__tests__/dynamic-record-extension-label.test.tsx +88 -0
- package/src/__tests__/installed-addons-context.test.tsx +50 -0
- package/src/__tests__/line-item-totals.test.ts +28 -0
- package/src/__tests__/prefill-scalar-from-record.test.ts +52 -1
- package/src/__tests__/primitives-federated.test.tsx +156 -0
- package/src/__tests__/primitives.test.ts +224 -0
- package/src/__tests__/record-prefill-bar.test.tsx +50 -0
- package/src/__tests__/row-action-link-navigation.test.ts +17 -0
- package/src/__tests__/secondary-actions.test.tsx +261 -0
- package/src/action-modal-dispatcher.tsx +97 -12
- package/src/action-priority.ts +82 -0
- package/src/business/business.test.ts +1 -1
- package/src/business/catalog-product-search.ts +105 -0
- package/src/business/catalog.ts +2 -0
- package/src/business/document-editor-model.ts +367 -0
- package/src/business/document-editor-panels.tsx +164 -0
- package/src/business/document-editor.tsx +781 -0
- package/src/business/document-lines.ts +5 -1
- package/src/business/index.ts +5 -0
- package/src/business/line-items.ts +31 -2
- package/src/business/payment-allocator.tsx +110 -0
- package/src/business/product-search.ts +4 -0
- package/src/dialogs/dynamic-record.tsx +29 -6
- package/src/document/document-page.tsx +15 -0
- package/src/document/evaluate.ts +17 -1
- package/src/document/index.ts +1 -0
- package/src/document/secondary-bar.tsx +92 -0
- package/src/document/types.ts +8 -0
- package/src/document-form-dialog.tsx +96 -12
- package/src/dynamic-columns-helpers.ts +22 -0
- package/src/dynamic-columns.tsx +35 -62
- package/src/dynamic-crud-page.tsx +33 -5
- package/src/dynamic-form-schema.ts +15 -2
- package/src/dynamic-form.tsx +117 -39
- package/src/dynamic-kanban.tsx +34 -7
- package/src/dynamic-relation-helpers.ts +9 -0
- package/src/dynamic-relation.tsx +7 -2
- package/src/dynamic-row-actions.tsx +12 -2
- package/src/dynamic-table.tsx +3 -0
- package/src/index.ts +46 -1
- package/src/installed-addons-context.tsx +58 -0
- package/src/org-runtime-context.ts +10 -0
- package/src/org-runtime-provider.tsx +7 -2
- package/src/primitives/aging.ts +76 -0
- package/src/primitives/allocation.ts +104 -0
- package/src/primitives/approvals.ts +103 -0
- package/src/primitives/contributions.ts +255 -0
- package/src/primitives/document-kinds.ts +62 -0
- package/src/primitives/index.ts +23 -0
- package/src/primitives/panels.ts +59 -0
- package/src/primitives/use-contributions.ts +42 -0
- package/src/record-actions.ts +101 -0
- package/src/record-prefill-bar.tsx +62 -0
- package/src/row-action-stage-gate.test.ts +33 -0
- package/src/row-actions-menu.tsx +159 -0
- package/src/slot.tsx +18 -53
- package/src/types.ts +129 -0
|
@@ -23,6 +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
27
|
import { DynamicIcon } from './dynamic-icon'
|
|
27
28
|
import { FieldCell, FieldGrid, FieldLabel } from './field-grid'
|
|
28
29
|
import { FormErrorBanner } from './business/feedback'
|
|
@@ -41,12 +42,13 @@ import { extractFieldErrors, localizeFieldErrorMap } from './server-error'
|
|
|
41
42
|
import { validateValues, bagHasErrors } from './validator'
|
|
42
43
|
import { clearFieldErrorTree } from './field-validation-ui'
|
|
43
44
|
import { emitRecordMutation } from './record-mutation-events'
|
|
44
|
-
import { isLineItemsField, resolveWidget } from './dynamic-form-schema'
|
|
45
|
+
import { isLineItemsField, resolveWidget, scopeValueFromFilterToken } from './dynamic-form-schema'
|
|
46
|
+
import { createCatalogProductSearch, withDefaultTaxRate } from './business/catalog-product-search'
|
|
47
|
+
import { DocumentEditor } from './business/document-editor'
|
|
48
|
+
import { editorLinesConfig, isEditorLayout } from './business/document-editor-model'
|
|
45
49
|
import type {
|
|
46
50
|
ActionFieldDef,
|
|
47
|
-
DocumentFormLines,
|
|
48
51
|
DocumentFormsManifest,
|
|
49
|
-
DocumentFormType,
|
|
50
52
|
TableMetadata,
|
|
51
53
|
} from './types'
|
|
52
54
|
|
|
@@ -62,10 +64,24 @@ export interface DocumentFormDialogProps {
|
|
|
62
64
|
initialType?: string
|
|
63
65
|
/** Se llama con el registro creado. */
|
|
64
66
|
onSaved?: (record?: unknown) => void
|
|
65
|
-
/**
|
|
67
|
+
/**
|
|
68
|
+
* Buscador de productos para el editor de renglones (mismo contrato que
|
|
69
|
+
* ProductPicker). Sin él se busca en el catálogo (`productModel`) vía /data.
|
|
70
|
+
*/
|
|
66
71
|
searchProducts?: DocumentLinesGridProps['search']
|
|
72
|
+
/** Modelo de catálogo del buscador por defecto. Default `products.Product`. */
|
|
73
|
+
productModel?: string
|
|
74
|
+
/** Tasa de impuesto (0.16) si el producto no trae la suya. Default: la de la org (OrgRuntimeProvider). */
|
|
75
|
+
defaultTaxRate?: number
|
|
67
76
|
/** Moneda ISO para el editor de renglones (default: la de la org). */
|
|
68
77
|
currency?: string
|
|
78
|
+
/**
|
|
79
|
+
* Registro desde el que se abre (acción de fila «Crear NC», «Registrar
|
|
80
|
+
* cobro»…): siembra los campos con `default_from_record`.
|
|
81
|
+
*/
|
|
82
|
+
record?: Record<string, any>
|
|
83
|
+
/** Documento origen a precargar en el DocumentEditor (`sources[].key` + id). */
|
|
84
|
+
initialSource?: { key: string; id: string }
|
|
69
85
|
}
|
|
70
86
|
|
|
71
87
|
/** Manifest efectivo: la prop gana sobre el metadata; sin tipos devuelve undefined. */
|
|
@@ -77,10 +93,25 @@ export function resolveDocumentForms(
|
|
|
77
93
|
return m && Array.isArray(m.types) && m.types.length > 0 ? m : undefined
|
|
78
94
|
}
|
|
79
95
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
96
|
+
/**
|
|
97
|
+
* Acota el alta guiada a la vista filtrada. Un nav que filtra por la columna
|
|
98
|
+
* discriminadora (`type_field`) lista UN tipo de documento: «Crear» debe abrir
|
|
99
|
+
* ese tipo directo y, si el manifest no trae formulario para él, no ofrecerse
|
|
100
|
+
* (retest r3: «Documentos fiscales → Facturas → Crear» abría el REP, el único
|
|
101
|
+
* tipo declarado). Sin manifest, sin `type_field` o sin filtro sobre él, todo
|
|
102
|
+
* queda igual.
|
|
103
|
+
*/
|
|
104
|
+
export function scopeDocumentFormsToFilter(
|
|
105
|
+
forms: DocumentFormsManifest | undefined,
|
|
106
|
+
filter?: Record<string, unknown> | null,
|
|
107
|
+
): { forms: DocumentFormsManifest | undefined; hideCreate: boolean } {
|
|
108
|
+
const field = forms?.type_field
|
|
109
|
+
if (!forms || !field || !filter || !(field in filter)) return { forms, hideCreate: false }
|
|
110
|
+
const want = scopeValueFromFilterToken(filter[field]).trim().toLowerCase()
|
|
111
|
+
if (want === '') return { forms, hideCreate: false }
|
|
112
|
+
const types = forms.types.filter((x) => String(x.value ?? x.key).trim().toLowerCase() === want)
|
|
113
|
+
if (types.length === 0) return { forms: undefined, hideCreate: true }
|
|
114
|
+
return { forms: types.length === forms.types.length ? forms : { ...forms, types }, hideCreate: false }
|
|
84
115
|
}
|
|
85
116
|
|
|
86
117
|
type Step = 'type' | 'fields' | 'lines'
|
|
@@ -94,10 +125,15 @@ export function DocumentFormDialog({
|
|
|
94
125
|
initialType,
|
|
95
126
|
onSaved,
|
|
96
127
|
searchProducts,
|
|
128
|
+
productModel,
|
|
129
|
+
defaultTaxRate,
|
|
97
130
|
currency,
|
|
131
|
+
record,
|
|
132
|
+
initialSource,
|
|
98
133
|
}: DocumentFormDialogProps) {
|
|
99
134
|
const { t, i18n } = useTranslation()
|
|
100
135
|
const api = useApi()
|
|
136
|
+
const orgTaxRate = useOrgTaxRate()
|
|
101
137
|
const types = forms.types
|
|
102
138
|
const single = types.length === 1 ? types[0] : undefined
|
|
103
139
|
|
|
@@ -111,7 +147,19 @@ export function DocumentFormDialog({
|
|
|
111
147
|
const [saving, setSaving] = useState(false)
|
|
112
148
|
|
|
113
149
|
const type = useMemo(() => types.find((x) => x.key === typeKey) ?? null, [types, typeKey])
|
|
114
|
-
|
|
150
|
+
// Sin buscador inyectado, el paso de renglones solo ofrecía «Renglón libre»
|
|
151
|
+
// y la línea entraba sin precio (DynamicCRUDPage no pasa searchProducts).
|
|
152
|
+
// Default: el catálogo de productos de la org. El buscador del host también
|
|
153
|
+
// recibe el IVA de la org para los productos que no traen su tasa.
|
|
154
|
+
const taxRate = defaultTaxRate ?? orgTaxRate
|
|
155
|
+
const lineSearch = useMemo(
|
|
156
|
+
() =>
|
|
157
|
+
searchProducts
|
|
158
|
+
? withDefaultTaxRate(searchProducts, taxRate)
|
|
159
|
+
: createCatalogProductSearch(api, { model: productModel, defaultTaxRate: taxRate }),
|
|
160
|
+
[searchProducts, api, productModel, taxRate],
|
|
161
|
+
)
|
|
162
|
+
const lineCfg = type ? editorLinesConfig(type, forms) : undefined
|
|
115
163
|
|
|
116
164
|
// Reset every time the dialog (re)opens.
|
|
117
165
|
useEffect(() => {
|
|
@@ -128,10 +176,10 @@ export function DocumentFormDialog({
|
|
|
128
176
|
// Seed field defaults for the chosen type.
|
|
129
177
|
useEffect(() => {
|
|
130
178
|
if (!type) return
|
|
131
|
-
setFormData({ ...buildFieldDefaults(type.fields,
|
|
179
|
+
setFormData({ ...buildFieldDefaults(type.fields, record), ...(type.defaults ?? {}) })
|
|
132
180
|
setFieldErrors({})
|
|
133
181
|
setFormError(undefined)
|
|
134
|
-
}, [type])
|
|
182
|
+
}, [type, record])
|
|
135
183
|
|
|
136
184
|
const updateField = (key: string, value: any) => {
|
|
137
185
|
setFormData((prev) => ({ ...prev, [key]: value }))
|
|
@@ -232,6 +280,41 @@ export function DocumentFormDialog({
|
|
|
232
280
|
? t('documentForm.step_data', { defaultValue: 'Datos' })
|
|
233
281
|
: tl(lineCfg?.title ?? 'Renglones')
|
|
234
282
|
|
|
283
|
+
// layout "editor": una sola pantalla por secciones (DocumentEditor). Sin él
|
|
284
|
+
// sigue el wizard de siempre (retrocompatible).
|
|
285
|
+
if (open && type && step !== 'type' && (isEditorLayout(type) || !!initialSource)) {
|
|
286
|
+
return (
|
|
287
|
+
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
288
|
+
<DialogContent
|
|
289
|
+
className="flex max-h-[92dvh] flex-col overflow-hidden"
|
|
290
|
+
style={{ maxHeight: '92dvh', maxWidth: '960px', width: '95vw' }}
|
|
291
|
+
data-slot="document-editor-dialog"
|
|
292
|
+
>
|
|
293
|
+
<DialogHeader className="shrink-0">
|
|
294
|
+
<DialogTitle>{tl(type.label)}</DialogTitle>
|
|
295
|
+
{type.description && <DialogDescription>{tl(type.description)}</DialogDescription>}
|
|
296
|
+
</DialogHeader>
|
|
297
|
+
<DocumentEditor
|
|
298
|
+
key={type.key}
|
|
299
|
+
model={model}
|
|
300
|
+
endpoint={endpoint}
|
|
301
|
+
forms={forms}
|
|
302
|
+
type={type}
|
|
303
|
+
record={record}
|
|
304
|
+
initialSource={initialSource}
|
|
305
|
+
searchProducts={lineSearch}
|
|
306
|
+
currency={currency}
|
|
307
|
+
onCancel={() => (canGoBackToTypes ? setStep('type') : onOpenChange(false))}
|
|
308
|
+
onSaved={(rec) => {
|
|
309
|
+
onSaved?.(rec)
|
|
310
|
+
onOpenChange(false)
|
|
311
|
+
}}
|
|
312
|
+
/>
|
|
313
|
+
</DialogContent>
|
|
314
|
+
</Dialog>
|
|
315
|
+
)
|
|
316
|
+
}
|
|
317
|
+
|
|
235
318
|
return (
|
|
236
319
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
237
320
|
<DialogContent
|
|
@@ -311,8 +394,9 @@ export function DocumentFormDialog({
|
|
|
311
394
|
}}
|
|
312
395
|
columns={(lineCfg.columns as LineItemsColumn[] | undefined) ?? ['discount', 'tax']}
|
|
313
396
|
priceSource={lineCfg.price_source ?? 'sale'}
|
|
397
|
+
discountMode={lineCfg.discount_mode}
|
|
314
398
|
serverErrors={serverLineErrors}
|
|
315
|
-
search={
|
|
399
|
+
search={lineSearch}
|
|
316
400
|
currency={currency}
|
|
317
401
|
/>
|
|
318
402
|
)}
|
|
@@ -78,3 +78,25 @@ export function translateMetadataLabel(
|
|
|
78
78
|
const translated = translate(value, { defaultValue: fallback })
|
|
79
79
|
return translated && translated !== value ? translated : fallback
|
|
80
80
|
}
|
|
81
|
+
|
|
82
|
+
// A catalog key as addons write them: lowercase dotted segments
|
|
83
|
+
// ("fiscal_mexico.ext.customer.rfc_receptor"). Human labels have spaces,
|
|
84
|
+
// capitals or accents and never match.
|
|
85
|
+
const CATALOG_KEY_RE = /^[a-z0-9_]+(\.[a-z0-9_]+)+$/
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Form-field label as the user should read it. The host usually serves labels
|
|
89
|
+
* already localized; when it could not (an extension field whose addon catalog
|
|
90
|
+
* the server did not resolve), the label arrives as the raw catalog key and the
|
|
91
|
+
* form printed `FISCAL_MEXICO.EXT.CUSTOMER.RFC_RECEPTOR`. Such a key is resolved
|
|
92
|
+
* with the client catalog and, failing that, humanized. Any other label is
|
|
93
|
+
* returned untouched.
|
|
94
|
+
*/
|
|
95
|
+
export function localizeFieldLabel(
|
|
96
|
+
label: string | undefined,
|
|
97
|
+
translate?: MetadataTranslator,
|
|
98
|
+
): string {
|
|
99
|
+
if (!label) return ''
|
|
100
|
+
if (!CATALOG_KEY_RE.test(label)) return label
|
|
101
|
+
return translateMetadataLabel(label, translate)
|
|
102
|
+
}
|
package/src/dynamic-columns.tsx
CHANGED
|
@@ -17,7 +17,7 @@ import { ColumnDef } from '@tanstack/react-table'
|
|
|
17
17
|
import { format, type Locale } from 'date-fns'
|
|
18
18
|
import { isCalendarDayValue, parseCalendarDate } from './calendar-date'
|
|
19
19
|
import { es, enUS } from 'date-fns/locale'
|
|
20
|
-
import { Calendar, Check, Copy, Mail, Minus
|
|
20
|
+
import { Calendar, Check, Copy, Mail, Minus } from 'lucide-react'
|
|
21
21
|
import {
|
|
22
22
|
Avatar,
|
|
23
23
|
AvatarFallback,
|
|
@@ -25,10 +25,6 @@ import {
|
|
|
25
25
|
Badge,
|
|
26
26
|
Button,
|
|
27
27
|
Checkbox,
|
|
28
|
-
DropdownMenu,
|
|
29
|
-
DropdownMenuContent,
|
|
30
|
-
DropdownMenuItem,
|
|
31
|
-
DropdownMenuTrigger,
|
|
32
28
|
InitialsAvatar,
|
|
33
29
|
} from '@asteby/metacore-ui'
|
|
34
30
|
import {
|
|
@@ -57,7 +53,7 @@ import { DynamicIcon, isLucideIconName } from './dynamic-icon'
|
|
|
57
53
|
import { CollectionCell } from './collection-cell'
|
|
58
54
|
import { isNilUuid, normalizeNilUuid } from './nil-uuid'
|
|
59
55
|
import { useOptionsResolver } from './use-options-resolver'
|
|
60
|
-
import {
|
|
56
|
+
import { RowActionsMenu } from './row-actions-menu'
|
|
61
57
|
import type { TableMetadata, ColumnDefinition } from './types'
|
|
62
58
|
import { isColumnVisibleInTable } from './column-visibility'
|
|
63
59
|
import type {
|
|
@@ -208,42 +204,25 @@ const CodeCell: React.FC<{ text: string; maxLength?: number }> = ({ text, maxLen
|
|
|
208
204
|
)
|
|
209
205
|
}
|
|
210
206
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
* `onAction`, whose shared handler (useDynamicRowActions) opens the
|
|
215
|
-
* requires-addon dialog instead of executing.
|
|
216
|
-
*/
|
|
217
|
-
export function RowActionMenuItem({
|
|
218
|
-
action,
|
|
219
|
-
label,
|
|
220
|
-
onSelect,
|
|
221
|
-
}: {
|
|
222
|
-
action: any
|
|
223
|
-
label: React.ReactNode
|
|
224
|
-
onSelect: (e: React.MouseEvent) => void
|
|
225
|
-
}) {
|
|
226
|
-
const requirement = resolveRequiresAddon(action)
|
|
227
|
-
const requiresAddonLabel = useRequiresAddonLabel()
|
|
228
|
-
return (
|
|
229
|
-
<DropdownMenuItem
|
|
230
|
-
onClick={onSelect}
|
|
231
|
-
title={requirement ? requiresAddonLabel(requirement) : undefined}
|
|
232
|
-
data-requires-addon={requirement?.key}
|
|
233
|
-
>
|
|
234
|
-
<DynamicIcon name={action.icon || 'Zap'} className="mr-2 h-4 w-4" />
|
|
235
|
-
{label}
|
|
236
|
-
{requirement && <RequiresAddonLock requirement={requirement} />}
|
|
237
|
-
</DropdownMenuItem>
|
|
238
|
-
)
|
|
239
|
-
}
|
|
207
|
+
// RowActionMenuItem vive en row-actions-menu.tsx (junto al menú que lo usa);
|
|
208
|
+
// se re-exporta aquí por compatibilidad (dynamic-kanban, hosts).
|
|
209
|
+
export { RowActionMenuItem } from './row-actions-menu'
|
|
240
210
|
|
|
241
211
|
/**
|
|
242
212
|
* Lifecycle column used by `requiresState`: prefer `status` (workshop, vehicles,
|
|
243
213
|
* …) and fall back to `state` (purchases, inventory transfers, …). Empty string
|
|
244
214
|
* is treated as missing so a blank `status` does not hide a populated `state`.
|
|
245
215
|
*/
|
|
246
|
-
const rowLifecycleState = (row: any): unknown => {
|
|
216
|
+
const rowLifecycleState = (row: any, stageField?: string): unknown => {
|
|
217
|
+
// A model with a stage machine gates its actions on the stage column, the
|
|
218
|
+
// same field the kernel's checkRequiresState reads (dynamic/action.go).
|
|
219
|
+
// Without this, WorkOrder actions declared with stage keys
|
|
220
|
+
// (requiresState: ['reception', …]) were compared to `status` ('open') and
|
|
221
|
+
// hidden in every row, so an OT could never be advanced from the UI.
|
|
222
|
+
if (stageField) {
|
|
223
|
+
const stage = row?.[stageField]
|
|
224
|
+
if (stage !== undefined && stage !== null && stage !== '') return stage
|
|
225
|
+
}
|
|
247
226
|
const status = row?.status
|
|
248
227
|
if (status !== undefined && status !== null && status !== '') return status
|
|
249
228
|
const state = row?.state
|
|
@@ -264,10 +243,10 @@ const rowLifecycleState = (row: any): unknown => {
|
|
|
264
243
|
* - action without requiresState (or empty array) → always shown.
|
|
265
244
|
* - row with neither `status` nor `state` → all actions shown.
|
|
266
245
|
*/
|
|
267
|
-
export const isActionAllowedForRowState = (action: any, row: any): boolean => {
|
|
246
|
+
export const isActionAllowedForRowState = (action: any, row: any, stageField?: string): boolean => {
|
|
268
247
|
const requires: unknown = action?.requiresState ?? action?.requires_state
|
|
269
248
|
if (!Array.isArray(requires) || requires.length === 0) return true
|
|
270
|
-
const status = rowLifecycleState(row)
|
|
249
|
+
const status = rowLifecycleState(row, stageField)
|
|
271
250
|
if (status === undefined) return true
|
|
272
251
|
return requires.map(String).includes(String(status))
|
|
273
252
|
}
|
|
@@ -341,8 +320,16 @@ export const isActionConditionMet = (action: any, row: any): boolean => {
|
|
|
341
320
|
* (`requiresState`) AND the declarative `condition` must pass. Shared by the
|
|
342
321
|
* table's action column and the kanban card menu so they hide/show identically.
|
|
343
322
|
*/
|
|
344
|
-
export const isRowActionVisible = (action: any, row: any): boolean =>
|
|
345
|
-
isActionAllowedForRowState(action, row) && isActionConditionMet(action, row)
|
|
323
|
+
export const isRowActionVisible = (action: any, row: any, stageField?: string): boolean =>
|
|
324
|
+
isActionAllowedForRowState(action, row, stageField) && isActionConditionMet(action, row)
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* The column a model's `requiresState` gates read: the served `stage_field`
|
|
328
|
+
* (or, for hosts that predate it, the `group_by` of a model with stages).
|
|
329
|
+
* Undefined → the historical `status` / `state` lookup.
|
|
330
|
+
*/
|
|
331
|
+
export const lifecycleStageField = (metadata: any): string | undefined =>
|
|
332
|
+
metadata?.stage_field ?? metadata?.stageField ?? (metadata?.stages?.length ? metadata?.group_by : undefined)
|
|
346
333
|
|
|
347
334
|
const lowerFirst = (value?: string) => {
|
|
348
335
|
if (!value) return value
|
|
@@ -1554,28 +1541,14 @@ export function makeDefaultGetDynamicColumns(
|
|
|
1554
1541
|
maxSize: 80,
|
|
1555
1542
|
meta: {},
|
|
1556
1543
|
cell: ({ row }) => (
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
<DropdownMenuContent align="end">
|
|
1566
|
-
{resolvedActions
|
|
1567
|
-
.filter((action) => isRowActionVisible(action, row.original))
|
|
1568
|
-
.map((action) => (
|
|
1569
|
-
<RowActionMenuItem
|
|
1570
|
-
key={action.key}
|
|
1571
|
-
action={action}
|
|
1572
|
-
label={translateMetadataLabel(action.label, t)}
|
|
1573
|
-
onSelect={() => onAction && onAction(action.key, row.original)}
|
|
1574
|
-
/>
|
|
1575
|
-
))}
|
|
1576
|
-
</DropdownMenuContent>
|
|
1577
|
-
</DropdownMenu>
|
|
1578
|
-
</div>
|
|
1544
|
+
// Primaria destacada, principales y un «Más…» con las
|
|
1545
|
+
// secundarias (compartir/imprimir/correo/chat + las que
|
|
1546
|
+
// aporten addons instalados). Ver row-actions-menu.tsx.
|
|
1547
|
+
<RowActionsMenu
|
|
1548
|
+
actions={resolvedActions.filter((action) => isRowActionVisible(action, row.original, lifecycleStageField(metadata)))}
|
|
1549
|
+
row={row.original}
|
|
1550
|
+
onAction={onAction}
|
|
1551
|
+
/>
|
|
1579
1552
|
),
|
|
1580
1553
|
})
|
|
1581
1554
|
}
|
|
@@ -33,7 +33,7 @@ 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 { DocumentFormDialog, resolveDocumentForms } from './document-form-dialog'
|
|
36
|
+
import { DocumentFormDialog, resolveDocumentForms, scopeDocumentFormsToFilter } from './document-form-dialog'
|
|
37
37
|
import { ExportDialog } from './dialogs/export'
|
|
38
38
|
import { ImportDialog } from './dialogs/import'
|
|
39
39
|
import { getModelExtension } from './model-extension-registry'
|
|
@@ -96,6 +96,18 @@ export interface DynamicCRUDPageProps {
|
|
|
96
96
|
* with neither, the generic record dialog is used as before.
|
|
97
97
|
*/
|
|
98
98
|
documentForms?: DocumentFormsManifest
|
|
99
|
+
/**
|
|
100
|
+
* Filtro fijo de la vista (el `filter` del nav). Si va sobre el `type_field`
|
|
101
|
+
* del alta guiada, «Crear» abre ese tipo directo o se oculta cuando no hay
|
|
102
|
+
* formulario para él.
|
|
103
|
+
*/
|
|
104
|
+
filter?: Record<string, string>
|
|
105
|
+
/**
|
|
106
|
+
* Abre el alta guiada al montar («crear desde»): tipo de documento y origen
|
|
107
|
+
* a precargar (`sources[].key` + id), p. ej. leídos de la URL
|
|
108
|
+
* `?create=<tipo>&from=<fuente>&from_id=<id>`. Solo con document_forms.
|
|
109
|
+
*/
|
|
110
|
+
initialCreate?: { type?: string; source?: { key: string; id: string } }
|
|
99
111
|
}
|
|
100
112
|
|
|
101
113
|
/**
|
|
@@ -119,6 +131,8 @@ export function DynamicCRUDPage(props: DynamicCRUDPageProps) {
|
|
|
119
131
|
classes,
|
|
120
132
|
onChange,
|
|
121
133
|
documentForms,
|
|
134
|
+
filter,
|
|
135
|
+
initialCreate,
|
|
122
136
|
} = props
|
|
123
137
|
|
|
124
138
|
const strings = { ...defaultStrings, ...(i18n ?? {}) }
|
|
@@ -130,7 +144,7 @@ export function DynamicCRUDPage(props: DynamicCRUDPageProps) {
|
|
|
130
144
|
|
|
131
145
|
const [metadata, setMetadata] = useState<TableMetadata | null>(cachedMeta ?? null)
|
|
132
146
|
const [refreshKey, setRefreshKey] = useState(0)
|
|
133
|
-
const [openCreate, setOpenCreate] = useState(
|
|
147
|
+
const [openCreate, setOpenCreate] = useState(!!initialCreate)
|
|
134
148
|
const [openExport, setOpenExport] = useState(false)
|
|
135
149
|
const [openImport, setOpenImport] = useState(false)
|
|
136
150
|
|
|
@@ -189,11 +203,22 @@ export function DynamicCRUDPage(props: DynamicCRUDPageProps) {
|
|
|
189
203
|
// (useCan defaults to always-true), in which case create/export/import
|
|
190
204
|
// require `lowercase(model).create|export|import`.
|
|
191
205
|
const can = useCan()
|
|
192
|
-
const
|
|
206
|
+
const scoped = scopeDocumentFormsToFilter(resolveDocumentForms(metadata, documentForms), filter)
|
|
207
|
+
const guidedForms = scoped.forms
|
|
208
|
+
const showCreate = primary.showGenericCreate && !scoped.hideCreate && can(modelCapability(model, 'create'))
|
|
193
209
|
const showImport = enableCRUD && !effectiveHideImport && can(modelCapability(model, 'import'))
|
|
194
210
|
const showExport = !effectiveHideExport && can(modelCapability(model, 'export'))
|
|
195
211
|
const showRefresh = !effectiveHideRefresh
|
|
196
|
-
|
|
212
|
+
|
|
213
|
+
// El mismo filtro acota la tabla (forma `eq:<valor>` que espera DynamicTable).
|
|
214
|
+
const filterKey = filter ? JSON.stringify(filter) : ''
|
|
215
|
+
const tableFilters = useMemo(() => {
|
|
216
|
+
if (!filter || Object.keys(filter).length === 0) return undefined
|
|
217
|
+
return Object.fromEntries(
|
|
218
|
+
Object.entries(filter).map(([k, v]) => [k, String(v).includes(':') ? String(v) : `eq:${v}`]),
|
|
219
|
+
)
|
|
220
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
221
|
+
}, [filterKey])
|
|
197
222
|
|
|
198
223
|
const handleRefresh = useCallback(() => {
|
|
199
224
|
setRefreshKey((k) => k + 1)
|
|
@@ -278,6 +303,7 @@ export function DynamicCRUDPage(props: DynamicCRUDPageProps) {
|
|
|
278
303
|
model={model}
|
|
279
304
|
endpoint={dataEndpoint}
|
|
280
305
|
refreshTrigger={refreshKey}
|
|
306
|
+
defaultFilters={tableFilters}
|
|
281
307
|
/>
|
|
282
308
|
</div>
|
|
283
309
|
</div>
|
|
@@ -289,13 +315,15 @@ export function DynamicCRUDPage(props: DynamicCRUDPageProps) {
|
|
|
289
315
|
model={model}
|
|
290
316
|
endpoint={dataEndpoint}
|
|
291
317
|
forms={guidedForms}
|
|
318
|
+
initialType={initialCreate?.type}
|
|
319
|
+
initialSource={initialCreate?.source}
|
|
292
320
|
onSaved={handleRefresh}
|
|
293
321
|
/>
|
|
294
322
|
)}
|
|
295
323
|
|
|
296
324
|
{showCreate && !guidedForms && (
|
|
297
325
|
<DynamicRecordDialog
|
|
298
|
-
open={openCreate}
|
|
326
|
+
open={openCreate && !initialCreate?.source}
|
|
299
327
|
onOpenChange={setOpenCreate}
|
|
300
328
|
mode='create'
|
|
301
329
|
model={model}
|
|
@@ -107,12 +107,25 @@ export function computeLineItemTotals(
|
|
|
107
107
|
field: ActionFieldDef,
|
|
108
108
|
rows: any[] | undefined,
|
|
109
109
|
): Record<string, number> {
|
|
110
|
-
const
|
|
110
|
+
const itemFields = getItemFields(field)
|
|
111
|
+
const cols = itemFields.filter((c) => c.total)
|
|
112
|
+
// A `total` UNIT PRICE column on a grid with a quantity and no line amount
|
|
113
|
+
// column sums the line amounts (qty × price − discount): adding unit prices
|
|
114
|
+
// is meaningless once a quantity exists (2 × 750 footed 750, not 1500).
|
|
115
|
+
const qtyKey = firstKey(itemFields, LINE_QTY_KEYS)
|
|
116
|
+
const lineValued = !!qtyKey && !firstKey(itemFields, LINE_AMOUNT_KEYS)
|
|
117
|
+
const discountKey = firstKey(itemFields, LINE_DISCOUNT_KEYS)
|
|
111
118
|
const totals: Record<string, number> = {}
|
|
112
119
|
for (const c of cols) totals[c.key] = 0
|
|
113
120
|
if (Array.isArray(rows)) {
|
|
114
121
|
for (const row of rows) {
|
|
115
|
-
for (const c of cols)
|
|
122
|
+
for (const c of cols) {
|
|
123
|
+
const v = toNumber(row?.[c.key])
|
|
124
|
+
totals[c.key] +=
|
|
125
|
+
lineValued && LINE_PRICE_KEYS.has(c.key)
|
|
126
|
+
? toNumber(row?.[qtyKey!]) * v - (discountKey ? toNumber(row?.[discountKey]) : 0)
|
|
127
|
+
: v
|
|
128
|
+
}
|
|
116
129
|
}
|
|
117
130
|
}
|
|
118
131
|
for (const k of Object.keys(totals)) totals[k] = Math.round(totals[k] * 100) / 100
|