@asteby/metacore-runtime-react 10.0.0 → 12.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 +77 -0
- package/dist/action-modal-dispatcher.d.ts.map +1 -1
- package/dist/action-modal-dispatcher.js +21 -1
- package/dist/dialogs/create-record-dialog.d.ts +3 -0
- package/dist/dialogs/create-record-dialog.d.ts.map +1 -0
- package/dist/dialogs/create-record-dialog.js +20 -0
- package/dist/dialogs/dynamic-record.d.ts +38 -1
- package/dist/dialogs/dynamic-record.d.ts.map +1 -1
- package/dist/dialogs/dynamic-record.js +50 -12
- package/dist/dialogs/types.d.ts +115 -0
- package/dist/dialogs/types.d.ts.map +1 -0
- package/dist/dialogs/types.js +15 -0
- package/dist/dialogs/view-record-dialog.d.ts +3 -0
- package/dist/dialogs/view-record-dialog.d.ts.map +1 -0
- package/dist/dialogs/view-record-dialog.js +15 -0
- package/dist/dynamic-form-schema.d.ts +9 -0
- package/dist/dynamic-form-schema.d.ts.map +1 -1
- package/dist/dynamic-form-schema.js +22 -0
- package/dist/dynamic-form.d.ts +1 -0
- package/dist/dynamic-form.d.ts.map +1 -1
- package/dist/dynamic-form.js +12 -1
- package/dist/dynamic-line-items.d.ts +9 -0
- package/dist/dynamic-line-items.d.ts.map +1 -0
- package/dist/dynamic-line-items.js +64 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/slot.d.ts.map +1 -1
- package/dist/slot.js +2 -0
- package/dist/types.d.ts +10 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +6 -6
- package/src/__tests__/dynamic-form.test.ts +56 -1
- package/src/__tests__/slot.test.ts +70 -0
- package/src/action-modal-dispatcher.tsx +22 -2
- package/src/dialogs/create-record-dialog.tsx +46 -0
- package/src/dialogs/dynamic-record.tsx +111 -15
- package/src/dialogs/types.ts +119 -0
- package/src/dialogs/view-record-dialog.tsx +37 -0
- package/src/dynamic-form-schema.ts +25 -0
- package/src/dynamic-form.tsx +12 -1
- package/src/dynamic-line-items.tsx +221 -0
- package/src/index.ts +10 -0
- package/src/slot.tsx +2 -0
- package/src/types.ts +10 -0
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared types for record-oriented dialogs in `@asteby/metacore-runtime-react`.
|
|
3
|
+
*
|
|
4
|
+
* These dialogs are intentionally generic: any addon that drives a metadata-
|
|
5
|
+
* backed CRUD surface can render them by passing a `modelKey`. The dialogs
|
|
6
|
+
* resolve field shape, labels, and validation hints from the metadata server
|
|
7
|
+
* (default: `/metadata/modal/:modelKey`) via the configured `useApi()`
|
|
8
|
+
* transport. Hosts may override transport behaviour by supplying explicit
|
|
9
|
+
* `endpoint` overrides and/or `onCreate` / `onDelete` callbacks.
|
|
10
|
+
*
|
|
11
|
+
* Wave 2.5 cleanup — replaces the product-specific dialogs that used to live
|
|
12
|
+
* in `ops/frontend/src/components/dynamic/` for any record kind that does not
|
|
13
|
+
* need pricing rules, media galleries, or category-driven custom fields.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Identifier for a model registered with the metadata service. The wire format
|
|
18
|
+
* is a plain string (e.g. `"products"`, `"organizations"`, `"users"`). The
|
|
19
|
+
* dialogs do not interpret this value; it is forwarded to the API transport.
|
|
20
|
+
*/
|
|
21
|
+
export type ModelKey = string
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Optional schema hint, accepted only as a passthrough for hosts that want to
|
|
25
|
+
* supply pre-fetched metadata instead of waiting for the dialog to call
|
|
26
|
+
* `/metadata/modal/:modelKey`. The runtime intentionally keeps the shape loose
|
|
27
|
+
* because metadata is owned by each backend addon.
|
|
28
|
+
*/
|
|
29
|
+
export interface ModelSchema {
|
|
30
|
+
title?: string
|
|
31
|
+
createTitle?: string
|
|
32
|
+
editTitle?: string
|
|
33
|
+
fields?: Array<{
|
|
34
|
+
key: string
|
|
35
|
+
label: string
|
|
36
|
+
type: string
|
|
37
|
+
required?: boolean
|
|
38
|
+
defaultValue?: unknown
|
|
39
|
+
placeholder?: string
|
|
40
|
+
readonly?: boolean
|
|
41
|
+
hidden?: boolean
|
|
42
|
+
// additional metadata may be present; the dialogs treat it as opaque
|
|
43
|
+
[extra: string]: unknown
|
|
44
|
+
}>
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Result shape returned by `onCreate` callbacks. Hosts may return `void` if
|
|
49
|
+
* the resulting record id is not needed downstream.
|
|
50
|
+
*/
|
|
51
|
+
export type CreateResult = { id: string } | void
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Base props shared by every record dialog.
|
|
55
|
+
*/
|
|
56
|
+
export interface RecordDialogProps {
|
|
57
|
+
/** Model key forwarded to the metadata service and transport calls. */
|
|
58
|
+
modelKey: ModelKey
|
|
59
|
+
/** Controlled open state. */
|
|
60
|
+
open: boolean
|
|
61
|
+
/** Open-state controller. */
|
|
62
|
+
onOpenChange: (open: boolean) => void
|
|
63
|
+
/**
|
|
64
|
+
* Optional custom endpoint base. When provided, the dialog uses
|
|
65
|
+
* `${endpoint}` (for create/list) and `${endpoint}/${recordId}` (for
|
|
66
|
+
* fetch/update/delete). When omitted, the dialog uses
|
|
67
|
+
* `/dynamic/${modelKey}` / `/dynamic/${modelKey}/${recordId}`.
|
|
68
|
+
*/
|
|
69
|
+
endpoint?: string
|
|
70
|
+
/**
|
|
71
|
+
* Optional pre-fetched schema. When omitted, the dialog fetches metadata
|
|
72
|
+
* from `/metadata/modal/${modelKey}` via the configured API transport.
|
|
73
|
+
*/
|
|
74
|
+
schema?: ModelSchema
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Props for the create / edit dialog.
|
|
79
|
+
*
|
|
80
|
+
* When `recordId` is provided the dialog operates in edit mode; otherwise it
|
|
81
|
+
* starts in create mode. Supplying `onCreate` overrides the default transport
|
|
82
|
+
* call so that hosts may route writes through custom mutations (optimistic
|
|
83
|
+
* updates, audit hooks, etc.). The default behaviour POSTs/PUTs through the
|
|
84
|
+
* configured `useApi()` transport.
|
|
85
|
+
*/
|
|
86
|
+
export interface CreateRecordDialogProps extends RecordDialogProps {
|
|
87
|
+
/** When set, the dialog operates as an editor for this record id. */
|
|
88
|
+
recordId?: string | null
|
|
89
|
+
/**
|
|
90
|
+
* Optional override invoked instead of the default POST. The dialog still
|
|
91
|
+
* closes and calls `onSaved` on success. Hosts that need to support both
|
|
92
|
+
* create and edit through callbacks should also pass `onUpdate`.
|
|
93
|
+
*/
|
|
94
|
+
onCreate?: (data: Record<string, unknown>) => Promise<CreateResult>
|
|
95
|
+
/**
|
|
96
|
+
* Optional override invoked instead of the default PUT when `recordId`
|
|
97
|
+
* is provided.
|
|
98
|
+
*/
|
|
99
|
+
onUpdate?: (recordId: string, data: Record<string, unknown>) => Promise<CreateResult>
|
|
100
|
+
/** Default values seeded into the form on create. */
|
|
101
|
+
defaults?: Record<string, unknown>
|
|
102
|
+
/** Notification when a create or update succeeds. */
|
|
103
|
+
onSaved?: () => void
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Props for the read-only viewer dialog.
|
|
108
|
+
*/
|
|
109
|
+
export interface ViewRecordDialogProps extends RecordDialogProps {
|
|
110
|
+
/** Identifier of the record to display. */
|
|
111
|
+
recordId: string
|
|
112
|
+
/** Optional handler triggered by the "Edit" affordance. */
|
|
113
|
+
onEdit?: () => void
|
|
114
|
+
/**
|
|
115
|
+
* Optional handler triggered by the "Delete" affordance. When omitted the
|
|
116
|
+
* delete button is hidden. The dialog awaits the promise before closing.
|
|
117
|
+
*/
|
|
118
|
+
onDelete?: () => Promise<void>
|
|
119
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ViewRecordDialog — read-only record viewer with optional edit/delete
|
|
3
|
+
* affordances.
|
|
4
|
+
*
|
|
5
|
+
* Thin wrapper around `DynamicRecordDialog` (mode = `'view'`) that exposes a
|
|
6
|
+
* narrower, intent-specific API matching the Wave 2.5 cleanup spec.
|
|
7
|
+
* The "Edit" and "Delete" footer buttons are only rendered when the host
|
|
8
|
+
* supplies `onEdit` / `onDelete`, so the dialog gracefully degrades to a
|
|
9
|
+
* pure viewer when those affordances are not needed.
|
|
10
|
+
*/
|
|
11
|
+
import { DynamicRecordDialog } from './dynamic-record'
|
|
12
|
+
import type { ViewRecordDialogProps } from './types'
|
|
13
|
+
|
|
14
|
+
export function ViewRecordDialog({
|
|
15
|
+
modelKey,
|
|
16
|
+
open,
|
|
17
|
+
onOpenChange,
|
|
18
|
+
recordId,
|
|
19
|
+
endpoint,
|
|
20
|
+
schema,
|
|
21
|
+
onEdit,
|
|
22
|
+
onDelete,
|
|
23
|
+
}: ViewRecordDialogProps) {
|
|
24
|
+
return (
|
|
25
|
+
<DynamicRecordDialog
|
|
26
|
+
open={open}
|
|
27
|
+
onOpenChange={onOpenChange}
|
|
28
|
+
mode="view"
|
|
29
|
+
model={modelKey}
|
|
30
|
+
recordId={recordId}
|
|
31
|
+
endpoint={endpoint}
|
|
32
|
+
schema={schema}
|
|
33
|
+
onEdit={onEdit}
|
|
34
|
+
onDelete={onDelete}
|
|
35
|
+
/>
|
|
36
|
+
)
|
|
37
|
+
}
|
|
@@ -47,7 +47,32 @@ export function buildZodSchema(fields: ActionFieldDef[]) {
|
|
|
47
47
|
return z.object(shape)
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
+
/**
|
|
51
|
+
* Returns the line-items columns of a repeatable-group field, tolerating both
|
|
52
|
+
* the camelCase `itemFields` (the authored SDK shape) and the raw snake_case
|
|
53
|
+
* `item_fields` that the kernel serves in action metadata. Empty when the
|
|
54
|
+
* field is not a line-items group.
|
|
55
|
+
*/
|
|
56
|
+
export function getItemFields(field: ActionFieldDef): ActionFieldDef[] {
|
|
57
|
+
const raw = field.itemFields ?? (field as { item_fields?: ActionFieldDef[] }).item_fields
|
|
58
|
+
return Array.isArray(raw) ? raw : []
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** A field is a repeatable line-items group when it declares item columns. */
|
|
62
|
+
export function isLineItemsField(field: ActionFieldDef): boolean {
|
|
63
|
+
return getItemFields(field).length > 0
|
|
64
|
+
}
|
|
65
|
+
|
|
50
66
|
function fieldToZod(field: ActionFieldDef): ZodTypeAny {
|
|
67
|
+
// Repeatable line-items group → array of row objects, each row built from
|
|
68
|
+
// the item field columns. Required keeps at least one row.
|
|
69
|
+
const itemFields = getItemFields(field)
|
|
70
|
+
if (itemFields.length > 0) {
|
|
71
|
+
const row = buildZodSchema(itemFields)
|
|
72
|
+
const arr = z.array(row)
|
|
73
|
+
return field.required ? arr.min(1, `${field.label} requiere al menos un renglón`) : arr
|
|
74
|
+
}
|
|
75
|
+
|
|
51
76
|
const v = field.validation ?? ({} as FieldValidation)
|
|
52
77
|
const isNumeric = field.type === 'number'
|
|
53
78
|
const isBool = field.type === 'boolean'
|
package/src/dynamic-form.tsx
CHANGED
|
@@ -15,10 +15,12 @@ import {
|
|
|
15
15
|
SelectValue,
|
|
16
16
|
} from '@asteby/metacore-ui/primitives'
|
|
17
17
|
import type { ActionFieldDef } from './types'
|
|
18
|
-
import { buildZodSchema, resolveWidget } from './dynamic-form-schema'
|
|
18
|
+
import { buildZodSchema, resolveWidget, isLineItemsField } from './dynamic-form-schema'
|
|
19
19
|
import { useOptionsResolver, type ResolvedOption } from './use-options-resolver'
|
|
20
|
+
import { DynamicLineItems } from './dynamic-line-items'
|
|
20
21
|
|
|
21
22
|
export { buildZodSchema, resolveWidget }
|
|
23
|
+
export { DynamicLineItems } from './dynamic-line-items'
|
|
22
24
|
|
|
23
25
|
export interface DynamicFormProps {
|
|
24
26
|
fields: ActionFieldDef[]
|
|
@@ -48,6 +50,10 @@ export function DynamicForm({
|
|
|
48
50
|
useEffect(() => {
|
|
49
51
|
const defaults: Record<string, any> = {}
|
|
50
52
|
for (const f of fields) {
|
|
53
|
+
if (isLineItemsField(f)) {
|
|
54
|
+
defaults[f.key] = initialValues?.[f.key] ?? f.defaultValue ?? []
|
|
55
|
+
continue
|
|
56
|
+
}
|
|
51
57
|
defaults[f.key] = initialValues?.[f.key] ?? f.defaultValue ?? (f.type === 'boolean' ? false : '')
|
|
52
58
|
}
|
|
53
59
|
setValues(defaults)
|
|
@@ -111,6 +117,11 @@ interface FieldRendererProps {
|
|
|
111
117
|
}
|
|
112
118
|
|
|
113
119
|
function FieldRenderer({ field, value, onChange }: FieldRendererProps) {
|
|
120
|
+
// Repeatable line-items group → render the row grid. Its value is an array
|
|
121
|
+
// of row objects rather than a scalar.
|
|
122
|
+
if (isLineItemsField(field)) {
|
|
123
|
+
return <DynamicLineItems field={field} value={value} onChange={onChange} />
|
|
124
|
+
}
|
|
114
125
|
const widget = resolveWidget(field)
|
|
115
126
|
// Ref-driven select: hook into useOptionsResolver so the canonical
|
|
116
127
|
// /api/options/<ref>?field=id endpoint feeds the dropdown. This is
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
// DynamicLineItems — renders a repeatable line-items group: a table/grid of
|
|
2
|
+
// rows where each column is one of the field's `itemFields` (the v3
|
|
3
|
+
// `item_fields`). Powers declarative multi-line action modals (e.g. the item
|
|
4
|
+
// rows of a "Recibir mercancía" modal, or the debit/credit lines of a journal
|
|
5
|
+
// entry) without needing a custom federated modal.
|
|
6
|
+
//
|
|
7
|
+
// The value is an array of row objects keyed by the item field keys. Add/remove
|
|
8
|
+
// row controls mutate the array; each cell is a widget resolved via
|
|
9
|
+
// `resolveWidget`, matching the flat-field renderer in dynamic-form.tsx.
|
|
10
|
+
import {
|
|
11
|
+
Input,
|
|
12
|
+
Textarea,
|
|
13
|
+
Switch,
|
|
14
|
+
Button,
|
|
15
|
+
Select,
|
|
16
|
+
SelectContent,
|
|
17
|
+
SelectItem,
|
|
18
|
+
SelectTrigger,
|
|
19
|
+
SelectValue,
|
|
20
|
+
} from '@asteby/metacore-ui/primitives'
|
|
21
|
+
import { Plus, Trash2 } from 'lucide-react'
|
|
22
|
+
import type { ActionFieldDef } from './types'
|
|
23
|
+
import { resolveWidget, getItemFields } from './dynamic-form-schema'
|
|
24
|
+
import { useOptionsResolver, type ResolvedOption } from './use-options-resolver'
|
|
25
|
+
|
|
26
|
+
export interface DynamicLineItemsProps {
|
|
27
|
+
field: ActionFieldDef
|
|
28
|
+
value: any[] | undefined
|
|
29
|
+
onChange: (rows: any[]) => void
|
|
30
|
+
disabled?: boolean
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function emptyRow(itemFields: ActionFieldDef[]): Record<string, any> {
|
|
34
|
+
const row: Record<string, any> = {}
|
|
35
|
+
for (const f of itemFields) {
|
|
36
|
+
row[f.key] = f.defaultValue ?? (f.type === 'boolean' ? false : '')
|
|
37
|
+
}
|
|
38
|
+
return row
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function DynamicLineItems({ field, value, onChange, disabled = false }: DynamicLineItemsProps) {
|
|
42
|
+
const itemFields = getItemFields(field)
|
|
43
|
+
const rows: any[] = Array.isArray(value) ? value : []
|
|
44
|
+
|
|
45
|
+
const addRow = () => onChange([...rows, emptyRow(itemFields)])
|
|
46
|
+
const removeRow = (idx: number) => onChange(rows.filter((_, i) => i !== idx))
|
|
47
|
+
const updateCell = (idx: number, key: string, cellValue: any) =>
|
|
48
|
+
onChange(rows.map((r, i) => (i === idx ? { ...r, [key]: cellValue } : r)))
|
|
49
|
+
|
|
50
|
+
return (
|
|
51
|
+
<div className="grid gap-2" data-widget="line_items">
|
|
52
|
+
<div className="overflow-x-auto rounded-md border">
|
|
53
|
+
<table className="w-full text-sm">
|
|
54
|
+
<thead className="bg-muted/50">
|
|
55
|
+
<tr>
|
|
56
|
+
{itemFields.map((col) => (
|
|
57
|
+
<th key={col.key} className="px-3 py-2 text-left font-medium">
|
|
58
|
+
{col.label}
|
|
59
|
+
{col.required && <span className="text-red-500 ml-1">*</span>}
|
|
60
|
+
</th>
|
|
61
|
+
))}
|
|
62
|
+
<th className="w-12 px-3 py-2" aria-label="acciones" />
|
|
63
|
+
</tr>
|
|
64
|
+
</thead>
|
|
65
|
+
<tbody>
|
|
66
|
+
{rows.length === 0 && (
|
|
67
|
+
<tr>
|
|
68
|
+
<td
|
|
69
|
+
colSpan={itemFields.length + 1}
|
|
70
|
+
className="px-3 py-4 text-center text-muted-foreground"
|
|
71
|
+
>
|
|
72
|
+
Sin renglones
|
|
73
|
+
</td>
|
|
74
|
+
</tr>
|
|
75
|
+
)}
|
|
76
|
+
{rows.map((row, idx) => (
|
|
77
|
+
<tr key={idx} className="border-t align-top">
|
|
78
|
+
{itemFields.map((col) => (
|
|
79
|
+
<td key={col.key} className="px-2 py-1.5">
|
|
80
|
+
<CellRenderer
|
|
81
|
+
field={col}
|
|
82
|
+
value={row?.[col.key]}
|
|
83
|
+
onChange={(v: any) => updateCell(idx, col.key, v)}
|
|
84
|
+
disabled={disabled}
|
|
85
|
+
/>
|
|
86
|
+
</td>
|
|
87
|
+
))}
|
|
88
|
+
<td className="px-2 py-1.5 text-center">
|
|
89
|
+
<Button
|
|
90
|
+
type="button"
|
|
91
|
+
variant="ghost"
|
|
92
|
+
size="icon"
|
|
93
|
+
onClick={() => removeRow(idx)}
|
|
94
|
+
disabled={disabled}
|
|
95
|
+
aria-label="Eliminar renglón"
|
|
96
|
+
>
|
|
97
|
+
<Trash2 className="h-4 w-4 text-red-500" />
|
|
98
|
+
</Button>
|
|
99
|
+
</td>
|
|
100
|
+
</tr>
|
|
101
|
+
))}
|
|
102
|
+
</tbody>
|
|
103
|
+
</table>
|
|
104
|
+
</div>
|
|
105
|
+
<div>
|
|
106
|
+
<Button type="button" variant="outline" size="sm" onClick={addRow} disabled={disabled}>
|
|
107
|
+
<Plus className="mr-1 h-4 w-4" />
|
|
108
|
+
Agregar renglón
|
|
109
|
+
</Button>
|
|
110
|
+
</div>
|
|
111
|
+
</div>
|
|
112
|
+
)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
interface CellRendererProps {
|
|
116
|
+
field: ActionFieldDef
|
|
117
|
+
value: any
|
|
118
|
+
onChange: (v: any) => void
|
|
119
|
+
disabled?: boolean
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Per-cell widget. Mirrors the flat FieldRenderer in dynamic-form.tsx but
|
|
123
|
+
// without the per-field Label (the column header is the label) and sized for a
|
|
124
|
+
// table cell. Nested line-items inside a row are not supported (a row column is
|
|
125
|
+
// a scalar widget).
|
|
126
|
+
function CellRenderer({ field, value, onChange, disabled }: CellRendererProps) {
|
|
127
|
+
const widget = resolveWidget(field)
|
|
128
|
+
if (widget === 'select' && field.ref) {
|
|
129
|
+
return <RefCell field={field} value={value} onChange={onChange} disabled={disabled} />
|
|
130
|
+
}
|
|
131
|
+
switch (widget) {
|
|
132
|
+
case 'textarea':
|
|
133
|
+
case 'richtext':
|
|
134
|
+
return (
|
|
135
|
+
<Textarea
|
|
136
|
+
value={value || ''}
|
|
137
|
+
onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) => onChange(e.target.value)}
|
|
138
|
+
placeholder={field.placeholder}
|
|
139
|
+
disabled={disabled}
|
|
140
|
+
rows={2}
|
|
141
|
+
/>
|
|
142
|
+
)
|
|
143
|
+
case 'color':
|
|
144
|
+
return (
|
|
145
|
+
<Input
|
|
146
|
+
type="color"
|
|
147
|
+
value={value || '#000000'}
|
|
148
|
+
onChange={(e: React.ChangeEvent<HTMLInputElement>) => onChange(e.target.value)}
|
|
149
|
+
disabled={disabled}
|
|
150
|
+
/>
|
|
151
|
+
)
|
|
152
|
+
case 'select':
|
|
153
|
+
return (
|
|
154
|
+
<Select value={value || ''} onValueChange={onChange} disabled={disabled}>
|
|
155
|
+
<SelectTrigger>
|
|
156
|
+
<SelectValue placeholder={field.placeholder || 'Seleccionar...'} />
|
|
157
|
+
</SelectTrigger>
|
|
158
|
+
<SelectContent>
|
|
159
|
+
{field.options?.map((opt) => (
|
|
160
|
+
<SelectItem key={opt.value} value={opt.value}>
|
|
161
|
+
{opt.label}
|
|
162
|
+
</SelectItem>
|
|
163
|
+
))}
|
|
164
|
+
</SelectContent>
|
|
165
|
+
</Select>
|
|
166
|
+
)
|
|
167
|
+
case 'switch':
|
|
168
|
+
return <Switch checked={!!value} onCheckedChange={onChange} disabled={disabled} />
|
|
169
|
+
case 'number':
|
|
170
|
+
return (
|
|
171
|
+
<Input
|
|
172
|
+
type="number"
|
|
173
|
+
value={value ?? ''}
|
|
174
|
+
onChange={(e: React.ChangeEvent<HTMLInputElement>) => onChange(e.target.valueAsNumber || '')}
|
|
175
|
+
placeholder={field.placeholder}
|
|
176
|
+
disabled={disabled}
|
|
177
|
+
/>
|
|
178
|
+
)
|
|
179
|
+
case 'date':
|
|
180
|
+
return (
|
|
181
|
+
<Input
|
|
182
|
+
type="date"
|
|
183
|
+
value={value || ''}
|
|
184
|
+
onChange={(e: React.ChangeEvent<HTMLInputElement>) => onChange(e.target.value)}
|
|
185
|
+
disabled={disabled}
|
|
186
|
+
/>
|
|
187
|
+
)
|
|
188
|
+
default:
|
|
189
|
+
return (
|
|
190
|
+
<Input
|
|
191
|
+
type={field.type === 'email' ? 'email' : field.type === 'url' ? 'url' : 'text'}
|
|
192
|
+
value={value || ''}
|
|
193
|
+
onChange={(e: React.ChangeEvent<HTMLInputElement>) => onChange(e.target.value)}
|
|
194
|
+
placeholder={field.placeholder}
|
|
195
|
+
disabled={disabled}
|
|
196
|
+
/>
|
|
197
|
+
)
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function RefCell({ field, value, onChange, disabled }: CellRendererProps) {
|
|
202
|
+
const { options, loading } = useOptionsResolver({
|
|
203
|
+
modelKey: '',
|
|
204
|
+
fieldKey: 'id',
|
|
205
|
+
ref: field.ref,
|
|
206
|
+
})
|
|
207
|
+
return (
|
|
208
|
+
<Select value={value || ''} onValueChange={onChange} disabled={disabled || loading}>
|
|
209
|
+
<SelectTrigger>
|
|
210
|
+
<SelectValue placeholder={loading ? 'Cargando…' : field.placeholder || 'Seleccionar...'} />
|
|
211
|
+
</SelectTrigger>
|
|
212
|
+
<SelectContent>
|
|
213
|
+
{options.map((opt: ResolvedOption) => (
|
|
214
|
+
<SelectItem key={String(opt.id)} value={String(opt.id)}>
|
|
215
|
+
{opt.label}
|
|
216
|
+
</SelectItem>
|
|
217
|
+
))}
|
|
218
|
+
</SelectContent>
|
|
219
|
+
</Select>
|
|
220
|
+
)
|
|
221
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -59,6 +59,16 @@ export {
|
|
|
59
59
|
type DynamicColumnsHelpers,
|
|
60
60
|
} from './dynamic-columns'
|
|
61
61
|
export { DynamicRecordDialog } from './dialogs/dynamic-record'
|
|
62
|
+
export { CreateRecordDialog } from './dialogs/create-record-dialog'
|
|
63
|
+
export { ViewRecordDialog } from './dialogs/view-record-dialog'
|
|
64
|
+
export type {
|
|
65
|
+
ModelKey,
|
|
66
|
+
ModelSchema,
|
|
67
|
+
CreateResult,
|
|
68
|
+
RecordDialogProps,
|
|
69
|
+
CreateRecordDialogProps,
|
|
70
|
+
ViewRecordDialogProps,
|
|
71
|
+
} from './dialogs/types'
|
|
62
72
|
export { ExportDialog } from './dialogs/export'
|
|
63
73
|
export { ImportDialog } from './dialogs/import'
|
|
64
74
|
export {
|
package/src/slot.tsx
CHANGED
|
@@ -22,6 +22,8 @@ class SlotRegistryImpl {
|
|
|
22
22
|
const entry: SlotEntry = { id: slotId, component, priority: opts?.priority ?? 0, source: opts?.source }
|
|
23
23
|
const list = this.slots.get(slotId) ?? []
|
|
24
24
|
list.push(entry)
|
|
25
|
+
// Higher priority renders first — canonical across SDK and runtime-react.
|
|
26
|
+
// See docs/slot-priority.md.
|
|
25
27
|
list.sort((a, b) => b.priority - a.priority)
|
|
26
28
|
this.slots.set(slotId, list)
|
|
27
29
|
this.emit()
|
package/src/types.ts
CHANGED
|
@@ -135,6 +135,16 @@ export interface ActionFieldDef {
|
|
|
135
135
|
* `useOptionsResolver` against `/api/options/<ref>?field=id`.
|
|
136
136
|
*/
|
|
137
137
|
ref?: string
|
|
138
|
+
/**
|
|
139
|
+
* Columns of a repeatable line-items group. Mirrors the kernel v3
|
|
140
|
+
* `ActionField.item_fields` (json `item_fields`). Present on a field
|
|
141
|
+
* with `type: "array"` — the multi-row container (e.g. the item rows
|
|
142
|
+
* of a "Recibir mercancía" modal, or the debit/credit lines of a
|
|
143
|
+
* journal entry). Each entry is itself an ActionFieldDef describing
|
|
144
|
+
* one column's cell widget. The field value is an array of objects
|
|
145
|
+
* keyed by these item field keys. Rendered by `DynamicLineItems`.
|
|
146
|
+
*/
|
|
147
|
+
itemFields?: ActionFieldDef[]
|
|
138
148
|
}
|
|
139
149
|
|
|
140
150
|
export interface ActionDefinition {
|