@asteby/metacore-runtime-react 47.0.0 → 48.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 +42 -0
- package/dist/action-modal-dispatcher.d.ts.map +1 -1
- package/dist/action-modal-dispatcher.js +24 -4
- package/dist/audit-info.d.ts +54 -0
- package/dist/audit-info.d.ts.map +1 -0
- package/dist/audit-info.js +142 -0
- package/dist/business/catalog-product-search.d.ts +25 -0
- package/dist/business/catalog-product-search.d.ts.map +1 -0
- package/dist/business/catalog-product-search.js +57 -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 +107 -0
- package/dist/business/document-editor-model.d.ts.map +1 -0
- package/dist/business/document-editor-model.js +279 -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 +445 -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 +7 -0
- package/dist/business/line-items.d.ts.map +1 -1
- package/dist/business/line-items.js +6 -1
- 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 +19 -8
- 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 +43 -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 +8 -2
- package/dist/dynamic-columns.d.ts.map +1 -1
- package/dist/dynamic-columns.js +21 -5
- package/dist/dynamic-crud-page.d.ts +6 -0
- package/dist/dynamic-crud-page.d.ts.map +1 -1
- package/dist/dynamic-crud-page.js +14 -5
- 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 +81 -20
- package/dist/dynamic-kanban.d.ts +9 -3
- package/dist/dynamic-kanban.d.ts.map +1 -1
- package/dist/dynamic-kanban.js +39 -13
- package/dist/dynamic-line-items.d.ts.map +1 -1
- package/dist/dynamic-line-items.js +3 -0
- package/dist/dynamic-multi-select-field.d.ts.map +1 -1
- package/dist/dynamic-multi-select-field.js +2 -0
- 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-select-field.d.ts.map +1 -1
- package/dist/dynamic-select-field.js +4 -0
- package/dist/index.d.ts +8 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +17 -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/option-filter.d.ts +31 -0
- package/dist/option-filter.d.ts.map +1 -0
- package/dist/option-filter.js +49 -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 +82 -0
- package/dist/primitives/contributions.d.ts.map +1 -0
- package/dist/primitives/contributions.js +126 -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-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/types.d.ts +142 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/use-options-resolver.d.ts +8 -0
- package/dist/use-options-resolver.d.ts.map +1 -1
- package/dist/use-options-resolver.js +8 -2
- package/package.json +5 -5
- package/src/__tests__/action-modal-grid-layout.test.tsx +1 -1
- package/src/__tests__/apply-sidebar-layout.test.ts +1 -1
- package/src/__tests__/audit-info.test.tsx +103 -0
- package/src/__tests__/catalog-product-search.test.ts +61 -0
- package/src/__tests__/dependent-options.test.tsx +12 -5
- package/src/__tests__/document-editor-model.test.ts +167 -0
- package/src/__tests__/document-editor.test.tsx +261 -0
- package/src/__tests__/document-form-dialog.test.tsx +118 -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__/dynamic-table-infinite-scroll.test.tsx +4 -0
- package/src/__tests__/installed-addons-context.test.tsx +50 -0
- package/src/__tests__/line-item-totals.test.ts +28 -0
- package/src/__tests__/line-items-lock-rows.test.tsx +2 -1
- package/src/__tests__/option-filter.test.tsx +115 -0
- 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__/structured-view-value.test.tsx +5 -1
- package/src/__tests__/wizard-action-modal.test.tsx +1 -1
- package/src/action-modal-dispatcher.tsx +47 -4
- package/src/audit-info.tsx +219 -0
- package/src/business/catalog-product-search.ts +87 -0
- package/src/business/catalog.ts +2 -0
- package/src/business/document-editor-model.ts +325 -0
- package/src/business/document-editor-panels.tsx +164 -0
- package/src/business/document-editor.tsx +727 -0
- package/src/business/document-lines.ts +5 -1
- package/src/business/index.ts +5 -0
- package/src/business/line-items.ts +15 -1
- package/src/business/payment-allocator.tsx +110 -0
- package/src/business/product-search.ts +4 -0
- package/src/dialogs/dynamic-record.tsx +42 -6
- package/src/document-form-dialog.tsx +92 -12
- package/src/dropdown-menu-entry.test.ts +23 -0
- package/src/dynamic-columns-helpers.ts +22 -0
- package/src/dynamic-columns.tsx +23 -6
- package/src/dynamic-crud-page.tsx +22 -3
- package/src/dynamic-form-schema.ts +15 -2
- package/src/dynamic-form.tsx +120 -39
- package/src/dynamic-kanban.tsx +41 -10
- package/src/dynamic-line-items.tsx +3 -0
- package/src/dynamic-multi-select-field.tsx +2 -0
- package/src/dynamic-relation-helpers.ts +9 -0
- package/src/dynamic-relation.tsx +7 -2
- package/src/dynamic-select-field.tsx +4 -0
- package/src/index.ts +34 -1
- package/src/installed-addons-context.tsx +58 -0
- package/src/option-filter.ts +88 -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 +208 -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-prefill-bar.tsx +62 -0
- package/src/row-action-stage-gate.test.ts +33 -0
- package/src/types.ts +142 -0
- package/src/use-options-resolver.ts +19 -1
- package/vitest.config.ts +1 -0
- package/vitest.setup.ts +7 -0
|
@@ -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,12 @@ 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>
|
|
99
105
|
}
|
|
100
106
|
|
|
101
107
|
/**
|
|
@@ -119,6 +125,7 @@ export function DynamicCRUDPage(props: DynamicCRUDPageProps) {
|
|
|
119
125
|
classes,
|
|
120
126
|
onChange,
|
|
121
127
|
documentForms,
|
|
128
|
+
filter,
|
|
122
129
|
} = props
|
|
123
130
|
|
|
124
131
|
const strings = { ...defaultStrings, ...(i18n ?? {}) }
|
|
@@ -189,11 +196,22 @@ export function DynamicCRUDPage(props: DynamicCRUDPageProps) {
|
|
|
189
196
|
// (useCan defaults to always-true), in which case create/export/import
|
|
190
197
|
// require `lowercase(model).create|export|import`.
|
|
191
198
|
const can = useCan()
|
|
192
|
-
const
|
|
199
|
+
const scoped = scopeDocumentFormsToFilter(resolveDocumentForms(metadata, documentForms), filter)
|
|
200
|
+
const guidedForms = scoped.forms
|
|
201
|
+
const showCreate = primary.showGenericCreate && !scoped.hideCreate && can(modelCapability(model, 'create'))
|
|
193
202
|
const showImport = enableCRUD && !effectiveHideImport && can(modelCapability(model, 'import'))
|
|
194
203
|
const showExport = !effectiveHideExport && can(modelCapability(model, 'export'))
|
|
195
204
|
const showRefresh = !effectiveHideRefresh
|
|
196
|
-
|
|
205
|
+
|
|
206
|
+
// El mismo filtro acota la tabla (forma `eq:<valor>` que espera DynamicTable).
|
|
207
|
+
const filterKey = filter ? JSON.stringify(filter) : ''
|
|
208
|
+
const tableFilters = useMemo(() => {
|
|
209
|
+
if (!filter || Object.keys(filter).length === 0) return undefined
|
|
210
|
+
return Object.fromEntries(
|
|
211
|
+
Object.entries(filter).map(([k, v]) => [k, String(v).includes(':') ? String(v) : `eq:${v}`]),
|
|
212
|
+
)
|
|
213
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
214
|
+
}, [filterKey])
|
|
197
215
|
|
|
198
216
|
const handleRefresh = useCallback(() => {
|
|
199
217
|
setRefreshKey((k) => k + 1)
|
|
@@ -278,6 +296,7 @@ export function DynamicCRUDPage(props: DynamicCRUDPageProps) {
|
|
|
278
296
|
model={model}
|
|
279
297
|
endpoint={dataEndpoint}
|
|
280
298
|
refreshTrigger={refreshKey}
|
|
299
|
+
defaultFilters={tableFilters}
|
|
281
300
|
/>
|
|
282
301
|
</div>
|
|
283
302
|
</div>
|
|
@@ -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
|
package/src/dynamic-form.tsx
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Minimal standalone DynamicForm. Factored from the dynamic-record-dialog
|
|
2
2
|
// pattern + ActionFieldDef renderer so callers can reuse the form layout
|
|
3
3
|
// outside the full record-edit modal.
|
|
4
|
-
import { useEffect, useMemo, useState } from 'react'
|
|
4
|
+
import { useEffect, useMemo, useRef, useState } from 'react'
|
|
5
5
|
import { groupFieldsBySection, type FormLayout } from './form-layout'
|
|
6
6
|
import { FieldSection, WizardProgress } from './form-layout-ui'
|
|
7
7
|
import { AssistInterview } from './assist-interview'
|
|
@@ -30,6 +30,7 @@ import {
|
|
|
30
30
|
} from './dynamic-form-schema'
|
|
31
31
|
import { ScanLine } from 'lucide-react'
|
|
32
32
|
import { BarcodeScanner } from './barcode-scanner'
|
|
33
|
+
import { getOptionFilter } from './option-filter'
|
|
33
34
|
import { useOptionsResolver, type ResolvedOption } from './use-options-resolver'
|
|
34
35
|
import { DynamicLineItems } from './dynamic-line-items'
|
|
35
36
|
import { DynamicSelectField } from './dynamic-select-field'
|
|
@@ -64,8 +65,35 @@ export interface DynamicFormProps {
|
|
|
64
65
|
* legacy flat list (unchanged).
|
|
65
66
|
*/
|
|
66
67
|
formLayout?: FormLayout
|
|
68
|
+
/**
|
|
69
|
+
* Para formularios dentro de un modal con alto acotado: el cuerpo de campos
|
|
70
|
+
* hace scroll propio y la barra de botones (Guardar/Cancelar) queda fija
|
|
71
|
+
* abajo, así nunca queda fuera de pantalla con muchos campos. El contenedor
|
|
72
|
+
* debe ser flex-col con alto máximo (p.ej. `max-h-[90dvh]`).
|
|
73
|
+
*/
|
|
74
|
+
scrollableBody?: boolean
|
|
67
75
|
}
|
|
68
76
|
|
|
77
|
+
// Valores iniciales del formulario: el registro (edición) gana sobre el default
|
|
78
|
+
// del campo. Se calcula de forma síncrona en el primer render para que ningún
|
|
79
|
+
// widget (p.ej. un Select de Radix) monte con '' y emita un cambio espurio que
|
|
80
|
+
// pise el valor real del registro.
|
|
81
|
+
function seedValues(fields: ActionFieldDef[], initialValues?: Record<string, any>): Record<string, any> {
|
|
82
|
+
const defaults: Record<string, any> = {}
|
|
83
|
+
for (const f of fields) {
|
|
84
|
+
if (isLineItemsField(f)) {
|
|
85
|
+
defaults[f.key] = initialValues?.[f.key] ?? f.defaultValue ?? []
|
|
86
|
+
continue
|
|
87
|
+
}
|
|
88
|
+
defaults[f.key] = initialValues?.[f.key] ?? f.defaultValue ?? (f.type === 'boolean' ? false : '')
|
|
89
|
+
}
|
|
90
|
+
return defaults
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const isReadonlyField = (f: ActionFieldDef) =>
|
|
94
|
+
!!(f as { readonly?: boolean; readOnly?: boolean }).readonly ||
|
|
95
|
+
!!(f as { readonly?: boolean; readOnly?: boolean }).readOnly
|
|
96
|
+
|
|
69
97
|
export function DynamicForm({
|
|
70
98
|
fields,
|
|
71
99
|
initialValues,
|
|
@@ -75,8 +103,8 @@ export function DynamicForm({
|
|
|
75
103
|
cancelLabel = 'Cancelar',
|
|
76
104
|
disabled = false,
|
|
77
105
|
formLayout,
|
|
106
|
+
scrollableBody = false,
|
|
78
107
|
}: DynamicFormProps) {
|
|
79
|
-
const [values, setValues] = useState<Record<string, any>>({})
|
|
80
108
|
const [errors, setErrors] = useState<Record<string, string>>({})
|
|
81
109
|
const [submitting, setSubmitting] = useState(false)
|
|
82
110
|
|
|
@@ -86,10 +114,15 @@ export function DynamicForm({
|
|
|
86
114
|
// (ColumnDef.Readonly). They are also excluded from defaults/submit so a
|
|
87
115
|
// create never posts e.g. `status: ""` over the column default.
|
|
88
116
|
const editableFields = useMemo(
|
|
89
|
-
() => fields.filter((f) => !(f
|
|
117
|
+
() => fields.filter((f) => !isReadonlyField(f)),
|
|
90
118
|
[fields],
|
|
91
119
|
)
|
|
92
120
|
|
|
121
|
+
const [values, setValues] = useState<Record<string, any>>(() => seedValues(editableFields, initialValues))
|
|
122
|
+
// Campos que el usuario (o un reset dependiente) cambió desde la siembra.
|
|
123
|
+
// En edición, un select que sigue vacío y nadie tocó no viaja en el PATCH.
|
|
124
|
+
const touched = useRef<Set<string>>(new Set())
|
|
125
|
+
|
|
93
126
|
// Conditional visibility: a field carrying `visible_when` is rendered — and
|
|
94
127
|
// validated — only while the referenced sibling field's current value
|
|
95
128
|
// matches the predicate. Driving BOTH the schema and the render off the same
|
|
@@ -132,21 +165,22 @@ export function DynamicForm({
|
|
|
132
165
|
setStepIndex((i) => Math.min(i, Math.max(groups.length - 1, 0)))
|
|
133
166
|
}, [groups.length])
|
|
134
167
|
|
|
168
|
+
// Re-siembra cuando cambia el registro o los campos DESPUÉS del montaje (el
|
|
169
|
+
// primer render ya quedó sembrado por el inicializador de useState).
|
|
170
|
+
const seededFrom = useRef({ editableFields, initialValues })
|
|
135
171
|
useEffect(() => {
|
|
136
|
-
const
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
}
|
|
142
|
-
defaults[f.key] = initialValues?.[f.key] ?? f.defaultValue ?? (f.type === 'boolean' ? false : '')
|
|
143
|
-
}
|
|
144
|
-
setValues(defaults)
|
|
172
|
+
const prev = seededFrom.current
|
|
173
|
+
if (prev.editableFields === editableFields && prev.initialValues === initialValues) return
|
|
174
|
+
seededFrom.current = { editableFields, initialValues }
|
|
175
|
+
touched.current = new Set()
|
|
176
|
+
setValues(seedValues(editableFields, initialValues))
|
|
145
177
|
setErrors({})
|
|
146
178
|
}, [editableFields, initialValues])
|
|
147
179
|
|
|
148
|
-
const update = (k: string, v: any) =>
|
|
180
|
+
const update = (k: string, v: any) => {
|
|
181
|
+
touched.current.add(k)
|
|
149
182
|
setValues((prev: Record<string, any>) => ({ ...prev, [k]: v }))
|
|
183
|
+
}
|
|
150
184
|
|
|
151
185
|
const handleSubmit = async (e: React.FormEvent) => {
|
|
152
186
|
e.preventDefault()
|
|
@@ -162,8 +196,19 @@ export function DynamicForm({
|
|
|
162
196
|
return
|
|
163
197
|
}
|
|
164
198
|
setErrors({})
|
|
199
|
+
const data = { ...(result.data as Record<string, any>) }
|
|
200
|
+
// Edición: un select vacío que el usuario no tocó no se manda — el update
|
|
201
|
+
// es parcial, así que omitirlo conserva lo que tenga el registro en vez de
|
|
202
|
+
// pisarlo con ''.
|
|
203
|
+
if (initialValues) {
|
|
204
|
+
for (const f of visibleFields) {
|
|
205
|
+
if (resolveWidget(f) !== 'select') continue
|
|
206
|
+
if (touched.current.has(f.key)) continue
|
|
207
|
+
if (data[f.key] === '' || data[f.key] == null) delete data[f.key]
|
|
208
|
+
}
|
|
209
|
+
}
|
|
165
210
|
setSubmitting(true)
|
|
166
|
-
try { await onSubmit(
|
|
211
|
+
try { await onSubmit(data) } finally { setSubmitting(false) }
|
|
167
212
|
}
|
|
168
213
|
|
|
169
214
|
// Renders one group's fields into the responsive 2-column grid: scalar
|
|
@@ -185,6 +230,11 @@ export function DynamicForm({
|
|
|
185
230
|
</div>
|
|
186
231
|
)
|
|
187
232
|
|
|
233
|
+
// scrollableBody: el cuerpo hace scroll y el footer de botones queda fijo.
|
|
234
|
+
const formClass = scrollableBody ? 'flex min-h-0 flex-1 flex-col gap-4' : 'grid gap-4'
|
|
235
|
+
const bodyClass = scrollableBody ? '-mx-1 grid min-h-0 flex-1 gap-4 overflow-y-auto px-1' : 'grid gap-4'
|
|
236
|
+
const footerExtra = scrollableBody ? ' shrink-0 border-t' : ''
|
|
237
|
+
|
|
188
238
|
// ── Steps (wizard) mode ────────────────────────────────────────────────
|
|
189
239
|
// One step per section; Anterior/Siguiente navigate, submit only on the
|
|
190
240
|
// last step. Advancing validates just the current step's fields so a later
|
|
@@ -211,20 +261,22 @@ export function DynamicForm({
|
|
|
211
261
|
const goBack = () => setStepIndex((i) => Math.max(i - 1, 0))
|
|
212
262
|
|
|
213
263
|
return (
|
|
214
|
-
<form onSubmit={handleSubmit} className=
|
|
215
|
-
<
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
264
|
+
<form onSubmit={handleSubmit} className={formClass}>
|
|
265
|
+
<div className={bodyClass}>
|
|
266
|
+
<WizardProgress groups={groups} stepIndex={stepIndex} onStepClick={i => setStepIndex(i)} />
|
|
267
|
+
{step.assist ? (
|
|
268
|
+
<AssistInterview
|
|
269
|
+
assist={step.assist}
|
|
270
|
+
values={values}
|
|
271
|
+
eyebrow={step.title}
|
|
272
|
+
autoStart={step.assist.trigger !== 'button'}
|
|
273
|
+
onApply={fields => setValues(prev => ({ ...prev, ...fields }))}
|
|
274
|
+
/>
|
|
275
|
+
) : (
|
|
276
|
+
renderGrid(step.fields)
|
|
277
|
+
)}
|
|
278
|
+
</div>
|
|
279
|
+
<div className={'flex justify-between gap-2 pt-2' + footerExtra}>
|
|
228
280
|
{stepIndex > 0 ? (
|
|
229
281
|
<Button type="button" variant="outline" onClick={goBack} disabled={submitting || disabled}>
|
|
230
282
|
Anterior
|
|
@@ -257,13 +309,15 @@ export function DynamicForm({
|
|
|
257
309
|
// stays fully declarative — driven only by field shape. With no layout this
|
|
258
310
|
// is a single default group rendered without section chrome (unchanged).
|
|
259
311
|
return (
|
|
260
|
-
<form onSubmit={handleSubmit} className=
|
|
261
|
-
{
|
|
262
|
-
|
|
263
|
-
{
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
312
|
+
<form onSubmit={handleSubmit} className={formClass}>
|
|
313
|
+
<div className={bodyClass}>
|
|
314
|
+
{groups.map((group) => (
|
|
315
|
+
<FieldSection key={group.key} group={group}>
|
|
316
|
+
{renderGrid(group.fields)}
|
|
317
|
+
</FieldSection>
|
|
318
|
+
))}
|
|
319
|
+
</div>
|
|
320
|
+
<div className={'flex justify-end gap-2 pt-2' + footerExtra}>
|
|
267
321
|
{onCancel && (
|
|
268
322
|
<Button type="button" variant="outline" onClick={onCancel} disabled={submitting || disabled}>
|
|
269
323
|
{cancelLabel}
|
|
@@ -427,15 +481,17 @@ function FieldRenderer({
|
|
|
427
481
|
onChange={onChange}
|
|
428
482
|
/>
|
|
429
483
|
)
|
|
430
|
-
case 'select':
|
|
484
|
+
case 'select': {
|
|
485
|
+
const opts = effectiveOptions ?? field.options
|
|
431
486
|
return (
|
|
432
|
-
<Select value={value || ''} onValueChange={onChange}>
|
|
487
|
+
<Select value={value || ''} onValueChange={guardEmptySelect(value, onChange, (opts ?? []).map((o) => o.value))}>
|
|
433
488
|
<SelectTrigger className="w-full"><SelectValue placeholder={field.placeholder || 'Seleccionar...'} /></SelectTrigger>
|
|
434
489
|
<SelectContent>
|
|
435
|
-
{
|
|
490
|
+
{opts?.map((opt) => <SelectItem key={opt.value} value={opt.value}>{opt.label}</SelectItem>)}
|
|
436
491
|
</SelectContent>
|
|
437
492
|
</Select>
|
|
438
493
|
)
|
|
494
|
+
}
|
|
439
495
|
case 'switch':
|
|
440
496
|
return <Switch id={field.key} checked={!!value} onCheckedChange={onChange} />
|
|
441
497
|
case 'number':
|
|
@@ -525,14 +581,39 @@ function ScannableInput({
|
|
|
525
581
|
)
|
|
526
582
|
}
|
|
527
583
|
|
|
584
|
+
/**
|
|
585
|
+
* Radix Select puede emitir `onValueChange('')` sin interacción del usuario (su
|
|
586
|
+
* <select> nativo oculto colapsa a '' si recibe el valor antes que sus
|
|
587
|
+
* <option>s, p.ej. dentro de un <form> en el primer render). Ese '' pisaría el
|
|
588
|
+
* valor real del registro y viajaría en el PATCH. Se ignora cuando ya hay un
|
|
589
|
+
* valor y '' no es una opción válida — el usuario nunca puede elegir '' así.
|
|
590
|
+
*/
|
|
591
|
+
export function guardEmptySelect(
|
|
592
|
+
current: unknown,
|
|
593
|
+
onChange: (v: any) => void,
|
|
594
|
+
optionValues: readonly unknown[],
|
|
595
|
+
): (v: string) => void {
|
|
596
|
+
return (v: string) => {
|
|
597
|
+
const hasValue = current !== undefined && current !== null && current !== ''
|
|
598
|
+
if (v === '' && hasValue && !optionValues.some((o) => String(o) === '')) return
|
|
599
|
+
onChange(v)
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
|
|
528
603
|
function RefSelect({ field, value, onChange }: FieldRendererProps) {
|
|
529
604
|
const { options, loading } = useOptionsResolver({
|
|
530
605
|
modelKey: '', // unused — `ref` drives the URL
|
|
531
606
|
fieldKey: 'id',
|
|
532
607
|
ref: field.ref,
|
|
608
|
+
optionFilter: getOptionFilter(field),
|
|
609
|
+
keepValue: value,
|
|
533
610
|
})
|
|
534
611
|
return (
|
|
535
|
-
<Select
|
|
612
|
+
<Select
|
|
613
|
+
value={value || ''}
|
|
614
|
+
onValueChange={guardEmptySelect(value, onChange, options.map((o: ResolvedOption) => String(o.id)))}
|
|
615
|
+
disabled={loading}
|
|
616
|
+
>
|
|
536
617
|
<SelectTrigger className="w-full">
|
|
537
618
|
<SelectValue placeholder={loading ? 'Cargando…' : (field.placeholder || 'Seleccionar...')} />
|
|
538
619
|
</SelectTrigger>
|
package/src/dynamic-kanban.tsx
CHANGED
|
@@ -94,9 +94,6 @@ import {
|
|
|
94
94
|
Button,
|
|
95
95
|
Card,
|
|
96
96
|
CardContent,
|
|
97
|
-
DropdownMenu,
|
|
98
|
-
DropdownMenuContent,
|
|
99
|
-
DropdownMenuTrigger,
|
|
100
97
|
Input,
|
|
101
98
|
Popover,
|
|
102
99
|
PopoverContent,
|
|
@@ -113,6 +110,13 @@ import {
|
|
|
113
110
|
SheetTrigger,
|
|
114
111
|
Skeleton,
|
|
115
112
|
} from '@asteby/metacore-ui/primitives'
|
|
113
|
+
// The card "…" menu renders RowActionMenuItem (dynamic-columns), whose
|
|
114
|
+
// DropdownMenuItem comes from the ROOT '@asteby/metacore-ui' entry. The menu
|
|
115
|
+
// shell MUST come from that same entry: hosts share the root entry as a Module
|
|
116
|
+
// Federation singleton while '/primitives' is bundled locally, so mixing them
|
|
117
|
+
// yields two Radix Menu contexts and the click crashes the app with
|
|
118
|
+
// "`MenuItem` must be used within `Menu`" (Pitsline taller kanban, 2026-10-05).
|
|
119
|
+
import { DropdownMenu, DropdownMenuContent, DropdownMenuTrigger } from '@asteby/metacore-ui'
|
|
116
120
|
import { ColumnFilterControl, FilterValueCombobox, type ColumnFilterType } from '@asteby/metacore-ui/data-table'
|
|
117
121
|
import { generateBadgeStyles, optionColor } from '@asteby/metacore-ui/lib'
|
|
118
122
|
import { useApi } from './api-context'
|
|
@@ -161,6 +165,7 @@ import {
|
|
|
161
165
|
aggregateOf,
|
|
162
166
|
formatAggregateTotal,
|
|
163
167
|
isRowActionVisible,
|
|
168
|
+
lifecycleStageField,
|
|
164
169
|
relationKeyFor,
|
|
165
170
|
RowActionMenuItem,
|
|
166
171
|
} from './dynamic-columns'
|
|
@@ -420,18 +425,39 @@ export function isCardOverdue(card: any, col: ColumnDefinition, now: number = Da
|
|
|
420
425
|
* The stage machine (stages, transitions, smart lanes) belongs to the model's
|
|
421
426
|
* stage column (`stage_field`; older hosts that do not serve it: the served
|
|
422
427
|
* group_by): grouping by it keeps the machine, grouping by anything else drops
|
|
423
|
-
* it and the lanes come from the new column.
|
|
424
|
-
*
|
|
425
|
-
*
|
|
428
|
+
* it and the lanes come from the new column.
|
|
429
|
+
*
|
|
430
|
+
* The served group_by is the LAST kanban nav entry's, which need not be the
|
|
431
|
+
* stage column: a board opened without `?group_by=` (the list's view toggle)
|
|
432
|
+
* groups by the stage column, and a board grouped by the served column keeps
|
|
433
|
+
* the machine only when that column IS the stage column. Otherwise the stage
|
|
434
|
+
* lanes were painted over another column (by bay: every card under «Sin etapa»
|
|
435
|
+
* and a drop wrote `service_bay_id = 'diagnosis'`, 422). Returns the SAME
|
|
436
|
+
* object when there is nothing to change. Pure — exported for unit tests.
|
|
426
437
|
*/
|
|
427
438
|
export function withGroupBy(
|
|
428
439
|
metadata: TableMetadata | null,
|
|
429
440
|
groupBy: string | undefined,
|
|
430
441
|
): TableMetadata | null {
|
|
431
|
-
if (!metadata
|
|
442
|
+
if (!metadata) return metadata
|
|
443
|
+
const hasMachine = (metadata.stages?.length ?? 0) > 0
|
|
444
|
+
const stageColumn = metadata.stage_field ?? (hasMachine ? metadata.group_by : undefined)
|
|
445
|
+
if (!groupBy) {
|
|
446
|
+
if (!hasMachine || !stageColumn || stageColumn === metadata.group_by) return metadata
|
|
447
|
+
return { ...metadata, group_by: stageColumn }
|
|
448
|
+
}
|
|
449
|
+
if (groupBy === stageColumn) {
|
|
450
|
+
return groupBy === metadata.group_by ? metadata : { ...metadata, group_by: groupBy }
|
|
451
|
+
}
|
|
432
452
|
if (!metadata.columns.some((c) => c.key === groupBy)) return metadata
|
|
433
|
-
|
|
434
|
-
|
|
453
|
+
if (
|
|
454
|
+
groupBy === metadata.group_by &&
|
|
455
|
+
!hasMachine &&
|
|
456
|
+
!metadata.transitions &&
|
|
457
|
+
!metadata.smart_lanes
|
|
458
|
+
) {
|
|
459
|
+
return metadata
|
|
460
|
+
}
|
|
435
461
|
return {
|
|
436
462
|
...metadata,
|
|
437
463
|
group_by: groupBy,
|
|
@@ -2115,6 +2141,7 @@ function DynamicKanbanBoard({
|
|
|
2115
2141
|
fieldCols={fieldCols}
|
|
2116
2142
|
showAllFields={explicitCard}
|
|
2117
2143
|
actions={rowActions}
|
|
2144
|
+
stageField={lifecycleStageField(metadata)}
|
|
2118
2145
|
locale={i18n.language}
|
|
2119
2146
|
timeZone={timeZone}
|
|
2120
2147
|
currency={currency}
|
|
@@ -2147,6 +2174,7 @@ function DynamicKanbanBoard({
|
|
|
2147
2174
|
fieldCols={fieldCols}
|
|
2148
2175
|
showAllFields={explicitCard}
|
|
2149
2176
|
actions={rowActions}
|
|
2177
|
+
stageField={lifecycleStageField(metadata)}
|
|
2150
2178
|
locale={i18n.language}
|
|
2151
2179
|
timeZone={timeZone}
|
|
2152
2180
|
currency={currency}
|
|
@@ -3179,6 +3207,8 @@ interface KanbanCardProps {
|
|
|
3179
3207
|
/** The model laid its card out explicitly: show every field with a value. */
|
|
3180
3208
|
showAllFields?: boolean
|
|
3181
3209
|
actions: ActionDefinition[]
|
|
3210
|
+
/** Column the actions' requiresState reads (the model's stage_field). */
|
|
3211
|
+
stageField?: string
|
|
3182
3212
|
locale: string
|
|
3183
3213
|
timeZone?: string
|
|
3184
3214
|
currency?: string
|
|
@@ -3256,6 +3286,7 @@ function KanbanCard({
|
|
|
3256
3286
|
fieldCols,
|
|
3257
3287
|
showAllFields,
|
|
3258
3288
|
actions,
|
|
3289
|
+
stageField,
|
|
3259
3290
|
locale,
|
|
3260
3291
|
timeZone,
|
|
3261
3292
|
currency,
|
|
@@ -3270,7 +3301,7 @@ function KanbanCard({
|
|
|
3270
3301
|
disabled: !draggable,
|
|
3271
3302
|
})
|
|
3272
3303
|
|
|
3273
|
-
const visibleActions = actions.filter((a) => isRowActionVisible(a, card))
|
|
3304
|
+
const visibleActions = actions.filter((a) => isRowActionVisible(a, card, stageField))
|
|
3274
3305
|
|
|
3275
3306
|
const menu =
|
|
3276
3307
|
visibleActions.length > 0 ? (
|
|
@@ -36,6 +36,7 @@ import {
|
|
|
36
36
|
applyOptionWhen,
|
|
37
37
|
} from './dynamic-form-schema'
|
|
38
38
|
import { DynamicSelectField, DEFAULT_DEPENDS_HINT } from './dynamic-select-field'
|
|
39
|
+
import { getOptionFilter } from './option-filter'
|
|
39
40
|
import { useOptionsResolver, type ResolvedOption } from './use-options-resolver'
|
|
40
41
|
|
|
41
42
|
export interface DynamicLineItemsProps {
|
|
@@ -575,6 +576,8 @@ function RefCell({ field, value, onChange, onPick, disabled, formValues, rowValu
|
|
|
575
576
|
ref: optSource.ref,
|
|
576
577
|
endpoint: optSource.endpoint,
|
|
577
578
|
filterValue: dependsOn ? scope : undefined,
|
|
579
|
+
optionFilter: getOptionFilter(field),
|
|
580
|
+
keepValue: value,
|
|
578
581
|
enabled: !blockedByDependency,
|
|
579
582
|
})
|
|
580
583
|
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
// value instead.
|
|
20
20
|
import { useMemo } from 'react'
|
|
21
21
|
import { MultiSelect } from '@asteby/metacore-ui/primitives'
|
|
22
|
+
import { getOptionFilter } from './option-filter'
|
|
22
23
|
import { useOptionsResolver } from './use-options-resolver'
|
|
23
24
|
import { getFieldRef } from './dynamic-form-schema'
|
|
24
25
|
import type { ActionFieldDef } from './types'
|
|
@@ -38,6 +39,7 @@ export function DynamicMultiSelectField({ field, value, onChange }: DynamicMulti
|
|
|
38
39
|
ref,
|
|
39
40
|
endpoint: !ref && field.searchEndpoint ? field.searchEndpoint : undefined,
|
|
40
41
|
limit: 200,
|
|
42
|
+
optionFilter: getOptionFilter(field),
|
|
41
43
|
})
|
|
42
44
|
|
|
43
45
|
const selected = useMemo(() => (Array.isArray(value) ? value.map(String) : []), [value])
|
|
@@ -175,11 +175,20 @@ export function deriveRelationFormFields(
|
|
|
175
175
|
// than a raw uuid text input. Without this the column lost its `ref`
|
|
176
176
|
// crossing the column→field boundary and degraded to plain text.
|
|
177
177
|
ref: col.ref,
|
|
178
|
+
// Columnas de solo lectura (escritas por el servidor/sistema): el
|
|
179
|
+
// DynamicForm las oculta y no las manda en el submit. Sin copiarla,
|
|
180
|
+
// el renglón las exponía editables y el PATCH las pisaba.
|
|
181
|
+
...(isReadonlyColumn(col) ? { readonly: true } : {}),
|
|
178
182
|
})
|
|
179
183
|
}
|
|
180
184
|
return out
|
|
181
185
|
}
|
|
182
186
|
|
|
187
|
+
function isReadonlyColumn(col: ColumnDefinition): boolean {
|
|
188
|
+
const c = col as ColumnDefinition & { readonly?: boolean; readOnly?: boolean; read_only?: boolean }
|
|
189
|
+
return c.readonly === true || c.readOnly === true || c.read_only === true
|
|
190
|
+
}
|
|
191
|
+
|
|
183
192
|
function columnTypeToFieldType(col: ColumnDefinition): string {
|
|
184
193
|
switch (col.type) {
|
|
185
194
|
case 'number': return 'number'
|
package/src/dynamic-relation.tsx
CHANGED
|
@@ -636,11 +636,16 @@ function OneToManyRelation({
|
|
|
636
636
|
)}
|
|
637
637
|
|
|
638
638
|
<Dialog open={formOpen} onOpenChange={(open: boolean) => { setFormOpen(open); if (!open) setEditingRow(null) }}>
|
|
639
|
-
|
|
640
|
-
|
|
639
|
+
{/* Alto acotado al viewport: el cuerpo del form hace scroll y el
|
|
640
|
+
footer (Guardar) queda fijo, para que un renglón con muchos
|
|
641
|
+
campos no deje el botón fuera de pantalla. */}
|
|
642
|
+
<DialogContent className="flex max-h-[90dvh] flex-col overflow-hidden">
|
|
643
|
+
<DialogHeader className="shrink-0">
|
|
641
644
|
<DialogTitle>{editingRow ? labels.editLabel : labels.addLabel}</DialogTitle>
|
|
642
645
|
</DialogHeader>
|
|
643
646
|
<DynamicForm
|
|
647
|
+
key={editingRow ? `edit-${editingRow.id ?? ''}` : 'new'}
|
|
648
|
+
scrollableBody
|
|
644
649
|
fields={formFields}
|
|
645
650
|
initialValues={editingRow || undefined}
|
|
646
651
|
onSubmit={handleSubmit}
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
// value). A dedicated `?ids=` lookup is a follow-up; create flows — the common
|
|
23
23
|
// case — start empty and never hit this.
|
|
24
24
|
import { useEffect, useRef, useState } from 'react'
|
|
25
|
+
import { getOptionFilter } from './option-filter'
|
|
25
26
|
import { useTranslation } from 'react-i18next'
|
|
26
27
|
import {
|
|
27
28
|
Badge,
|
|
@@ -266,6 +267,7 @@ export function DynamicSelectField({
|
|
|
266
267
|
const blockedByDependency = !!dependsOn && scope === ''
|
|
267
268
|
|
|
268
269
|
const useStatic = Array.isArray(staticOptions)
|
|
270
|
+
const optionFilter = getOptionFilter(field)
|
|
269
271
|
|
|
270
272
|
const { options: fetchedOptions, loading: fetchLoading } = useOptionsResolver({
|
|
271
273
|
modelKey: '',
|
|
@@ -280,6 +282,8 @@ export function DynamicSelectField({
|
|
|
280
282
|
// Cascade scope forwarded as filter_value (only when this field
|
|
281
283
|
// declares a dependency). Re-fetches when the parent value changes.
|
|
282
284
|
filterValue: dependsOn ? scope : undefined,
|
|
285
|
+
optionFilter,
|
|
286
|
+
keepValue: value,
|
|
283
287
|
// Don't fetch until the popover opens (and keep fetching as the query
|
|
284
288
|
// changes while open). A picker blocked by an unset dependency never
|
|
285
289
|
// fetches. A readonly cell fetches eagerly so its value's label resolves
|
package/src/index.ts
CHANGED
|
@@ -206,6 +206,7 @@ export {
|
|
|
206
206
|
export {
|
|
207
207
|
DocumentFormDialog,
|
|
208
208
|
resolveDocumentForms,
|
|
209
|
+
scopeDocumentFormsToFilter,
|
|
209
210
|
type DocumentFormDialogProps,
|
|
210
211
|
} from './document-form-dialog'
|
|
211
212
|
export {
|
|
@@ -359,6 +360,10 @@ export {
|
|
|
359
360
|
} from './sidebar-layout-manager'
|
|
360
361
|
export * from './org-runtime-context'
|
|
361
362
|
export * from './business'
|
|
363
|
+
// Primitivos de negocio (PaymentAllocator, AgingTable, ApprovalInbox,
|
|
364
|
+
// contribuciones/modales federados, tipos de documento y contratos de paneles).
|
|
365
|
+
// Ver primitives/index.ts para el mapa completo.
|
|
366
|
+
export * from './primitives'
|
|
362
367
|
export * from './org-runtime-provider'
|
|
363
368
|
export * from './navigation-builder'
|
|
364
369
|
export * from './i18n-provider'
|
|
@@ -422,7 +427,7 @@ export {
|
|
|
422
427
|
resolveRelationLabel,
|
|
423
428
|
type DynamicColumnsHelpers,
|
|
424
429
|
} from './dynamic-columns'
|
|
425
|
-
export { humanizeToken, translateMetadataLabel } from './dynamic-columns-helpers'
|
|
430
|
+
export { humanizeToken, localizeFieldLabel, translateMetadataLabel } from './dynamic-columns-helpers'
|
|
426
431
|
export type { MetadataTranslator } from './dynamic-columns-helpers'
|
|
427
432
|
export {
|
|
428
433
|
UrlChip,
|
|
@@ -584,6 +589,14 @@ export {
|
|
|
584
589
|
type ActivityEvent,
|
|
585
590
|
type ActivityDiffProps,
|
|
586
591
|
} from './activity-diff'
|
|
592
|
+
export {
|
|
593
|
+
AuditInfo,
|
|
594
|
+
readAuditMeta,
|
|
595
|
+
resolveAuditActor,
|
|
596
|
+
SYSTEM_ACTOR_ID,
|
|
597
|
+
type AuditInfoProps,
|
|
598
|
+
type AuditActor,
|
|
599
|
+
} from './audit-info'
|
|
587
600
|
export {
|
|
588
601
|
RecordHistory,
|
|
589
602
|
type RecordHistoryProps,
|
|
@@ -661,3 +674,23 @@ export {
|
|
|
661
674
|
|
|
662
675
|
// Página de documento (benchmark §5/§6): cabecera, badges, acciones por estado, botones inteligentes.
|
|
663
676
|
export * from './document'
|
|
677
|
+
|
|
678
|
+
// Option filter — hide options of a relation / dynamic picker from the manifest
|
|
679
|
+
// (`option_filter` / `optionFilter` on the field).
|
|
680
|
+
export { getOptionFilter, applyOptionFilter, optionPassesRule } from './option-filter'
|
|
681
|
+
export type { OptionFilter, OptionFilterRule } from './option-filter'
|
|
682
|
+
|
|
683
|
+
// Addons instalados / capacidades provistas — primitivo único de «¿qué hay
|
|
684
|
+
// instalado?» para hosts y remotes federados. Sin provider los hooks devuelven
|
|
685
|
+
// `undefined` (desconocido): el consumidor NUNCA debe asumir ausente.
|
|
686
|
+
export {
|
|
687
|
+
InstalledAddonsProvider,
|
|
688
|
+
useAddonInstalled,
|
|
689
|
+
useCapabilityProvided,
|
|
690
|
+
useInstalledAddons,
|
|
691
|
+
type InstalledAddonsValue,
|
|
692
|
+
} from './installed-addons-context'
|
|
693
|
+
|
|
694
|
+
// Ayudantes de captura (registerRecordPrefill de @asteby/metacore-sdk) arriba
|
|
695
|
+
// del formulario genérico de un modelo; DynamicRecordDialog ya lo monta.
|
|
696
|
+
export { RecordPrefillBar, type RecordPrefillBarProps } from './record-prefill-bar'
|