@asteby/metacore-runtime-react 11.0.0 → 13.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 +74 -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-crud-page.d.ts.map +1 -1
- package/dist/dynamic-crud-page.js +6 -2
- 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/dynamic-table.d.ts.map +1 -1
- package/dist/dynamic-table.js +7 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/model-action-toolbar.d.ts +27 -0
- package/dist/model-action-toolbar.d.ts.map +1 -0
- package/dist/model-action-toolbar.js +88 -0
- package/dist/types.d.ts +18 -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/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-crud-page.tsx +11 -1
- 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/dynamic-table.tsx +7 -1
- package/src/index.ts +16 -0
- package/src/model-action-toolbar.tsx +154 -0
- package/src/types.ts +18 -0
|
@@ -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/dynamic-table.tsx
CHANGED
|
@@ -569,7 +569,13 @@ export function DynamicTable({
|
|
|
569
569
|
|
|
570
570
|
const columns = useMemo(() => {
|
|
571
571
|
if (!metadata) return []
|
|
572
|
-
|
|
572
|
+
// Row-action column only renders per-row actions. Table-level placements
|
|
573
|
+
// ("table"/"create") are surfaced by <ModelActionToolbar> at the page
|
|
574
|
+
// level, so strip them here to avoid a meaningless per-row button.
|
|
575
|
+
const rowMetadata = metadata.actions?.some((a) => a.placement === 'table' || a.placement === 'create')
|
|
576
|
+
? { ...metadata, actions: metadata.actions.filter((a) => !a.placement || a.placement === 'row') }
|
|
577
|
+
: metadata
|
|
578
|
+
const baseColumns = getDynamicColumns(rowMetadata, handleInternalAction, t, i18n.language, columnFilterConfigs)
|
|
573
579
|
const filteredBase = baseColumns.filter((col: ColumnDef<any>) => !hiddenColumns.includes(col.id as string))
|
|
574
580
|
const actionsCol = filteredBase.find((c: ColumnDef<any>) => c.id === 'actions')
|
|
575
581
|
const otherCols = filteredBase.filter((c: ColumnDef<any>) => c.id !== 'actions')
|
package/src/index.ts
CHANGED
|
@@ -11,6 +11,12 @@ export {
|
|
|
11
11
|
ActionModalDispatcher,
|
|
12
12
|
type ActionModalProps,
|
|
13
13
|
} from './action-modal-dispatcher'
|
|
14
|
+
export {
|
|
15
|
+
ModelActionToolbar,
|
|
16
|
+
useModelActions,
|
|
17
|
+
type ModelActionToolbarProps,
|
|
18
|
+
type ActionPlacement,
|
|
19
|
+
} from './model-action-toolbar'
|
|
14
20
|
export * from './addon-loader'
|
|
15
21
|
export {
|
|
16
22
|
AddonLayoutProvider,
|
|
@@ -59,6 +65,16 @@ export {
|
|
|
59
65
|
type DynamicColumnsHelpers,
|
|
60
66
|
} from './dynamic-columns'
|
|
61
67
|
export { DynamicRecordDialog } from './dialogs/dynamic-record'
|
|
68
|
+
export { CreateRecordDialog } from './dialogs/create-record-dialog'
|
|
69
|
+
export { ViewRecordDialog } from './dialogs/view-record-dialog'
|
|
70
|
+
export type {
|
|
71
|
+
ModelKey,
|
|
72
|
+
ModelSchema,
|
|
73
|
+
CreateResult,
|
|
74
|
+
RecordDialogProps,
|
|
75
|
+
CreateRecordDialogProps,
|
|
76
|
+
ViewRecordDialogProps,
|
|
77
|
+
} from './dialogs/types'
|
|
62
78
|
export { ExportDialog } from './dialogs/export'
|
|
63
79
|
export { ImportDialog } from './dialogs/import'
|
|
64
80
|
export {
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
// ModelActionToolbar — renders page-level (toolbar) triggers for a model's
|
|
2
|
+
// declarative actions and owns the modal dispatch for them.
|
|
3
|
+
//
|
|
4
|
+
// A model's actions carry a `placement` hint (see manifest/v3 Action.placement):
|
|
5
|
+
// "row" (default) — rendered per-row inside <DynamicTable>'s action column.
|
|
6
|
+
// "table" — a plain toolbar button (no record context).
|
|
7
|
+
// "create" — a primary toolbar button that replaces the generic
|
|
8
|
+
// "create" button, for addons shipping a custom create
|
|
9
|
+
// experience (e.g. a journal entry with debit/credit lines).
|
|
10
|
+
//
|
|
11
|
+
// This component renders the buttons for the placements it's asked to surface
|
|
12
|
+
// (default: table + create) and mounts the <ActionModalDispatcher>, which
|
|
13
|
+
// resolves either a custom federated modal (registered via the action registry)
|
|
14
|
+
// or the generic declarative form. For create-style actions there is no record
|
|
15
|
+
// yet, so the dispatcher receives an empty record `{}`.
|
|
16
|
+
//
|
|
17
|
+
// It is the single generic primitive every host consumes — DynamicCRUDPage uses
|
|
18
|
+
// it internally, and bespoke host pages (e.g. ops `/m/$model`) mount it directly
|
|
19
|
+
// next to their own toolbar. Hosts never reimplement action-button plumbing.
|
|
20
|
+
import { useEffect, useMemo, useState } from 'react'
|
|
21
|
+
import { Button } from '@asteby/metacore-ui/primitives'
|
|
22
|
+
import { useApi } from './api-context'
|
|
23
|
+
import { useMetadataCache } from './metadata-cache'
|
|
24
|
+
import { DynamicIcon } from './dynamic-icon'
|
|
25
|
+
import { ActionModalDispatcher } from './action-modal-dispatcher'
|
|
26
|
+
import type { ActionDefinition, ActionMetadata, TableMetadata } from './types'
|
|
27
|
+
|
|
28
|
+
export type ActionPlacement = 'row' | 'table' | 'create'
|
|
29
|
+
|
|
30
|
+
export interface ModelActionToolbarProps {
|
|
31
|
+
/** Model key as registered on the backend (e.g. "JournalEntry"). */
|
|
32
|
+
model: string
|
|
33
|
+
/** Data endpoint passed to the dispatcher. Defaults to `/data/<model>/me`. */
|
|
34
|
+
endpoint?: string
|
|
35
|
+
/**
|
|
36
|
+
* Pre-fetched action definitions. When omitted the toolbar reads them from
|
|
37
|
+
* the metadata cache, falling back to `/metadata/table/<model>`. Pass this
|
|
38
|
+
* when the host page already holds the metadata to avoid a second fetch.
|
|
39
|
+
*/
|
|
40
|
+
actions?: ActionDefinition[]
|
|
41
|
+
/** Which placements to render. Defaults to `['table', 'create']`. */
|
|
42
|
+
placements?: ActionPlacement[]
|
|
43
|
+
/** Fired after an action's modal reports success. */
|
|
44
|
+
onChange?: () => void
|
|
45
|
+
/** Extra classes on the button row container. */
|
|
46
|
+
className?: string
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const DEFAULT_PLACEMENTS: ActionPlacement[] = ['table', 'create']
|
|
50
|
+
|
|
51
|
+
function toActionMetadata(a: ActionDefinition): ActionMetadata {
|
|
52
|
+
return {
|
|
53
|
+
key: a.key,
|
|
54
|
+
label: a.label,
|
|
55
|
+
icon: a.icon || 'Zap',
|
|
56
|
+
color: a.color,
|
|
57
|
+
confirm: a.confirm,
|
|
58
|
+
confirmMessage: a.confirmMessage,
|
|
59
|
+
fields: a.fields,
|
|
60
|
+
requiresState: a.requiresState,
|
|
61
|
+
executable: a.executable,
|
|
62
|
+
placement: a.placement,
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Returns the model's actions matching the requested placements. Reads from the
|
|
68
|
+
* `actions` prop when provided, else the metadata cache, else fetches once.
|
|
69
|
+
*/
|
|
70
|
+
export function useModelActions(
|
|
71
|
+
model: string,
|
|
72
|
+
placements: ActionPlacement[] = DEFAULT_PLACEMENTS,
|
|
73
|
+
provided?: ActionDefinition[],
|
|
74
|
+
): ActionDefinition[] {
|
|
75
|
+
const api = useApi()
|
|
76
|
+
const cached = useMetadataCache((s) => s.getMetadata(model))
|
|
77
|
+
const [fetched, setFetched] = useState<TableMetadata | null>(null)
|
|
78
|
+
|
|
79
|
+
const haveSource = provided != null || cached != null
|
|
80
|
+
useEffect(() => {
|
|
81
|
+
if (haveSource) return
|
|
82
|
+
let cancelled = false
|
|
83
|
+
api
|
|
84
|
+
.get(`/metadata/table/${model}`)
|
|
85
|
+
.then((res) => {
|
|
86
|
+
if (!cancelled) setFetched((res.data?.data ?? res.data) as TableMetadata)
|
|
87
|
+
})
|
|
88
|
+
.catch(() => {
|
|
89
|
+
if (!cancelled) setFetched(null)
|
|
90
|
+
})
|
|
91
|
+
return () => {
|
|
92
|
+
cancelled = true
|
|
93
|
+
}
|
|
94
|
+
}, [model, haveSource, api])
|
|
95
|
+
|
|
96
|
+
const all = provided ?? cached?.actions ?? fetched?.actions ?? []
|
|
97
|
+
return useMemo(
|
|
98
|
+
() => all.filter((a) => placements.includes((a.placement ?? 'row') as ActionPlacement)),
|
|
99
|
+
[all, placements],
|
|
100
|
+
)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function ModelActionToolbar({
|
|
104
|
+
model,
|
|
105
|
+
endpoint,
|
|
106
|
+
actions,
|
|
107
|
+
placements = DEFAULT_PLACEMENTS,
|
|
108
|
+
onChange,
|
|
109
|
+
className,
|
|
110
|
+
}: ModelActionToolbarProps) {
|
|
111
|
+
const surfaced = useModelActions(model, placements, actions)
|
|
112
|
+
const [active, setActive] = useState<ActionMetadata | null>(null)
|
|
113
|
+
const dataEndpoint = endpoint ?? `/data/${model}/me`
|
|
114
|
+
|
|
115
|
+
if (surfaced.length === 0) return null
|
|
116
|
+
|
|
117
|
+
return (
|
|
118
|
+
<>
|
|
119
|
+
<div className={className ?? 'flex items-center gap-2'}>
|
|
120
|
+
{surfaced.map((a) => {
|
|
121
|
+
const isCreate = (a.placement ?? 'row') === 'create'
|
|
122
|
+
return (
|
|
123
|
+
<Button
|
|
124
|
+
key={a.key}
|
|
125
|
+
variant={isCreate ? 'default' : 'outline'}
|
|
126
|
+
onClick={() => setActive(toActionMetadata(a))}
|
|
127
|
+
style={a.color && !isCreate ? { borderColor: a.color, color: a.color } : undefined}
|
|
128
|
+
>
|
|
129
|
+
<DynamicIcon name={a.icon || (isCreate ? 'Plus' : 'Zap')} className="mr-2 h-4 w-4" />
|
|
130
|
+
{a.label}
|
|
131
|
+
</Button>
|
|
132
|
+
)
|
|
133
|
+
})}
|
|
134
|
+
</div>
|
|
135
|
+
|
|
136
|
+
{active && (
|
|
137
|
+
<ActionModalDispatcher
|
|
138
|
+
open={!!active}
|
|
139
|
+
onOpenChange={(open) => {
|
|
140
|
+
if (!open) setActive(null)
|
|
141
|
+
}}
|
|
142
|
+
action={active}
|
|
143
|
+
model={model}
|
|
144
|
+
record={{}}
|
|
145
|
+
endpoint={dataEndpoint}
|
|
146
|
+
onSuccess={() => {
|
|
147
|
+
setActive(null)
|
|
148
|
+
onChange?.()
|
|
149
|
+
}}
|
|
150
|
+
/>
|
|
151
|
+
)}
|
|
152
|
+
</>
|
|
153
|
+
)
|
|
154
|
+
}
|
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 {
|
|
@@ -152,6 +162,13 @@ export interface ActionDefinition {
|
|
|
152
162
|
fields?: ActionFieldDef[]
|
|
153
163
|
requiresState?: string[]
|
|
154
164
|
executable?: boolean
|
|
165
|
+
/**
|
|
166
|
+
* Where the host surfaces the trigger. Mirrors manifest/v3 Action.placement.
|
|
167
|
+
* "row" (default) — per-row table action.
|
|
168
|
+
* "table" — page toolbar button (no record context).
|
|
169
|
+
* "create" — toolbar button that replaces the generic create button.
|
|
170
|
+
*/
|
|
171
|
+
placement?: 'row' | 'table' | 'create'
|
|
155
172
|
}
|
|
156
173
|
|
|
157
174
|
export interface ApiResponse<T> {
|
|
@@ -184,4 +201,5 @@ export interface ActionMetadata {
|
|
|
184
201
|
fields?: ActionFieldDef[]
|
|
185
202
|
requiresState?: string[]
|
|
186
203
|
executable?: boolean
|
|
204
|
+
placement?: 'row' | 'table' | 'create'
|
|
187
205
|
}
|