@asteby/metacore-runtime-react 48.0.0 → 49.1.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 +41 -0
- package/dist/action-modal-dispatcher.d.ts +33 -3
- package/dist/action-modal-dispatcher.d.ts.map +1 -1
- package/dist/action-modal-dispatcher.js +84 -11
- 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 +7 -0
- package/dist/business/catalog-product-search.d.ts.map +1 -1
- package/dist/business/catalog-product-search.js +14 -0
- package/dist/business/document-editor-model.d.ts +14 -2
- package/dist/business/document-editor-model.d.ts.map +1 -1
- package/dist/business/document-editor-model.js +42 -4
- package/dist/business/document-editor.d.ts.map +1 -1
- package/dist/business/document-editor.js +68 -19
- package/dist/business/line-items.d.ts +9 -1
- package/dist/business/line-items.d.ts.map +1 -1
- package/dist/business/line-items.js +8 -1
- package/dist/calendar-date.d.ts +12 -0
- package/dist/calendar-date.d.ts.map +1 -1
- package/dist/calendar-date.js +36 -0
- package/dist/document/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 +23 -2
- package/dist/document-form-dialog.d.ts.map +1 -1
- package/dist/document-form-dialog.js +48 -9
- package/dist/dynamic-columns.d.ts +1 -11
- package/dist/dynamic-columns.d.ts.map +1 -1
- package/dist/dynamic-columns.js +7 -17
- package/dist/dynamic-crud-page.d.ts +19 -0
- package/dist/dynamic-crud-page.d.ts.map +1 -1
- package/dist/dynamic-crud-page.js +6 -5
- 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 +5 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -2
- package/dist/primitives/contributions.d.ts +14 -3
- package/dist/primitives/contributions.d.ts.map +1 -1
- package/dist/primitives/contributions.js +50 -13
- 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/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 +39 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/__tests__/document-editor-model.test.ts +55 -1
- package/src/__tests__/document-editor.test.tsx +80 -1
- package/src/__tests__/document-form-create-model.test.tsx +143 -0
- package/src/__tests__/document-form-dialog.test.tsx +40 -0
- package/src/__tests__/field-default-today.test.tsx +116 -0
- package/src/__tests__/prefill-scalar-from-record.test.ts +52 -1
- 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 -17
- package/src/action-priority.ts +82 -0
- package/src/business/business.test.ts +1 -1
- package/src/business/catalog-product-search.ts +18 -0
- package/src/business/document-editor-model.ts +48 -6
- package/src/business/document-editor.tsx +75 -20
- package/src/business/line-items.ts +16 -1
- package/src/calendar-date.ts +36 -0
- 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 +67 -8
- package/src/dynamic-columns.tsx +13 -57
- package/src/dynamic-crud-page.tsx +31 -4
- package/src/dynamic-row-actions.tsx +12 -2
- package/src/dynamic-table.tsx +3 -0
- package/src/index.ts +31 -0
- package/src/primitives/contributions.ts +59 -12
- package/src/record-actions.ts +101 -0
- package/src/row-actions-menu.tsx +159 -0
- package/src/slot.tsx +18 -53
- package/src/types.ts +35 -0
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
// RowActionsMenu — el «…» de una fila: la acción primaria (si hay) arriba y
|
|
2
|
+
// destacada, las principales debajo y las secundarias (compartir, imprimir,
|
|
3
|
+
// correo, chat, PDF/XML y lo que aporten addons vía registerRecordAction)
|
|
4
|
+
// agrupadas en un «Más…» discreto. Una acción por capacidad sin proveedor
|
|
5
|
+
// activo, o una contribución cuyo addon no está instalado, no aparece.
|
|
6
|
+
//
|
|
7
|
+
// DynamicTable lo usa por defecto (getDynamicColumns) y los hosts que arman su
|
|
8
|
+
// propia columna de acciones lo reutilizan en vez de duplicar el menú.
|
|
9
|
+
import * as React from 'react'
|
|
10
|
+
import { useTranslation } from 'react-i18next'
|
|
11
|
+
import { MoreHorizontal } from 'lucide-react'
|
|
12
|
+
// Root entry on purpose (not '/primitives'): see dropdown-menu-entry.test.ts.
|
|
13
|
+
import {
|
|
14
|
+
Button,
|
|
15
|
+
DropdownMenu,
|
|
16
|
+
DropdownMenuContent,
|
|
17
|
+
DropdownMenuItem,
|
|
18
|
+
DropdownMenuSeparator,
|
|
19
|
+
DropdownMenuSub,
|
|
20
|
+
DropdownMenuSubContent,
|
|
21
|
+
DropdownMenuSubTrigger,
|
|
22
|
+
DropdownMenuTrigger,
|
|
23
|
+
} from '@asteby/metacore-ui'
|
|
24
|
+
import { DynamicIcon } from './dynamic-icon'
|
|
25
|
+
import { translateMetadataLabel } from './dynamic-columns-helpers'
|
|
26
|
+
import { RequiresAddonLock, resolveRequiresAddon, useRequiresAddonLabel } from './requires-addon'
|
|
27
|
+
import { useInstalledAddons } from './installed-addons-context'
|
|
28
|
+
import { isActionProviderActive, splitActionsByPriority, useRecordActions, type ResolvedRecordAction } from './record-actions'
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* One entry of the row "…" menu. An action gated by a missing optional addon
|
|
32
|
+
* (`requiresAddon`) stays listed with a lock + tooltip; the click still goes to
|
|
33
|
+
* `onAction`, whose shared handler (useDynamicRowActions) opens the
|
|
34
|
+
* requires-addon dialog instead of executing.
|
|
35
|
+
*/
|
|
36
|
+
export function RowActionMenuItem({
|
|
37
|
+
action,
|
|
38
|
+
label,
|
|
39
|
+
onSelect,
|
|
40
|
+
className,
|
|
41
|
+
}: {
|
|
42
|
+
action: any
|
|
43
|
+
label: React.ReactNode
|
|
44
|
+
onSelect: (e: React.MouseEvent) => void
|
|
45
|
+
className?: string
|
|
46
|
+
}) {
|
|
47
|
+
const requirement = resolveRequiresAddon(action)
|
|
48
|
+
const requiresAddonLabel = useRequiresAddonLabel()
|
|
49
|
+
return (
|
|
50
|
+
<DropdownMenuItem
|
|
51
|
+
onClick={onSelect}
|
|
52
|
+
title={requirement ? requiresAddonLabel(requirement) : undefined}
|
|
53
|
+
data-requires-addon={requirement?.key}
|
|
54
|
+
data-action={action.key}
|
|
55
|
+
className={className}
|
|
56
|
+
>
|
|
57
|
+
<DynamicIcon name={action.icon || 'Zap'} className="mr-2 h-4 w-4" />
|
|
58
|
+
{label}
|
|
59
|
+
{requirement && <RequiresAddonLock requirement={requirement} />}
|
|
60
|
+
</DropdownMenuItem>
|
|
61
|
+
)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export interface RowActionsMenuProps {
|
|
65
|
+
/** Acciones de metadata ya filtradas por estado/condición de la fila. */
|
|
66
|
+
actions: any[]
|
|
67
|
+
row: Record<string, any>
|
|
68
|
+
/** Modelo de la fila: resuelve las acciones aportadas por addons. */
|
|
69
|
+
model?: string
|
|
70
|
+
onAction?: (actionKey: string, row: any) => void
|
|
71
|
+
/** Sustituye el ícono del disparador (p. ej. un spinner mientras imprime). */
|
|
72
|
+
triggerIcon?: React.ReactNode
|
|
73
|
+
disabled?: boolean
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Modelo de las filas que pinta la tabla. DynamicTable lo provee: la factoría
|
|
78
|
+
* de columnas (getDynamicColumns) no recibe el modelo y el menú lo necesita
|
|
79
|
+
* para resolver las acciones aportadas por addons.
|
|
80
|
+
*/
|
|
81
|
+
export const RowActionsModelContext = React.createContext<string | undefined>(undefined)
|
|
82
|
+
|
|
83
|
+
type MenuEntry =
|
|
84
|
+
| { kind: 'action'; action: any }
|
|
85
|
+
| { kind: 'contribution'; item: ResolvedRecordAction }
|
|
86
|
+
|
|
87
|
+
export function RowActionsMenu({ actions, row, model, onAction, triggerIcon, disabled }: RowActionsMenuProps) {
|
|
88
|
+
const { t } = useTranslation()
|
|
89
|
+
const installed = useInstalledAddons()
|
|
90
|
+
const ctxModel = React.useContext(RowActionsModelContext)
|
|
91
|
+
const recordModel = model ?? ctxModel
|
|
92
|
+
const contributed = useRecordActions(recordModel ?? '', recordModel ? row : null).filter((c) => !!c.run)
|
|
93
|
+
|
|
94
|
+
const available = actions.filter((a) => isActionProviderActive(a, installed))
|
|
95
|
+
const { primary, main, secondary } = splitActionsByPriority(available)
|
|
96
|
+
const mainEntries: MenuEntry[] = main.map((action) => ({ kind: 'action', action }))
|
|
97
|
+
const secondaryEntries: MenuEntry[] = secondary.map((action) => ({ kind: 'action', action }))
|
|
98
|
+
for (const item of contributed) {
|
|
99
|
+
;(item.priority === 'primary' ? mainEntries : secondaryEntries).push({ kind: 'contribution', item })
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (!primary && mainEntries.length === 0 && secondaryEntries.length === 0) return null
|
|
103
|
+
|
|
104
|
+
const renderEntry = (e: MenuEntry) =>
|
|
105
|
+
e.kind === 'action' ? (
|
|
106
|
+
<RowActionMenuItem
|
|
107
|
+
key={e.action.key}
|
|
108
|
+
action={e.action}
|
|
109
|
+
label={translateMetadataLabel(e.action.label, t)}
|
|
110
|
+
onSelect={() => onAction?.(e.action.key, row)}
|
|
111
|
+
/>
|
|
112
|
+
) : (
|
|
113
|
+
<DropdownMenuItem key={e.item.key} data-action={e.item.key} onClick={() => void e.item.run?.()}>
|
|
114
|
+
<DynamicIcon name={e.item.icon || 'Share2'} className="mr-2 h-4 w-4" />
|
|
115
|
+
{t(e.item.label, { defaultValue: e.item.label })}
|
|
116
|
+
</DropdownMenuItem>
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
const hasMain = !!primary || mainEntries.length > 0
|
|
120
|
+
const moreLabel = t('datatable.more_actions', { defaultValue: 'Más…' })
|
|
121
|
+
|
|
122
|
+
return (
|
|
123
|
+
<div className="flex items-center justify-end">
|
|
124
|
+
<DropdownMenu>
|
|
125
|
+
<DropdownMenuTrigger asChild>
|
|
126
|
+
<Button variant="ghost" className="h-8 w-8 p-0" disabled={disabled} onClick={(e: React.MouseEvent) => e.stopPropagation()}>
|
|
127
|
+
<span className="sr-only">{t('datatable.open_menu', { defaultValue: 'Abrir menú' })}</span>
|
|
128
|
+
{triggerIcon ?? <MoreHorizontal className="h-4 w-4" />}
|
|
129
|
+
</Button>
|
|
130
|
+
</DropdownMenuTrigger>
|
|
131
|
+
<DropdownMenuContent align="end" onClick={(e: React.MouseEvent) => e.stopPropagation()}>
|
|
132
|
+
{primary ? (
|
|
133
|
+
<RowActionMenuItem
|
|
134
|
+
action={primary}
|
|
135
|
+
label={translateMetadataLabel(primary.label, t)}
|
|
136
|
+
onSelect={() => onAction?.(primary.key, row)}
|
|
137
|
+
className="font-semibold"
|
|
138
|
+
/>
|
|
139
|
+
) : null}
|
|
140
|
+
{mainEntries.map(renderEntry)}
|
|
141
|
+
{secondaryEntries.length > 0 && hasMain ? (
|
|
142
|
+
<>
|
|
143
|
+
<DropdownMenuSeparator />
|
|
144
|
+
<DropdownMenuSub>
|
|
145
|
+
<DropdownMenuSubTrigger data-row-more="" className="text-muted-foreground">
|
|
146
|
+
{moreLabel}
|
|
147
|
+
</DropdownMenuSubTrigger>
|
|
148
|
+
<DropdownMenuSubContent data-row-secondary="">{secondaryEntries.map(renderEntry)}</DropdownMenuSubContent>
|
|
149
|
+
</DropdownMenuSub>
|
|
150
|
+
</>
|
|
151
|
+
) : (
|
|
152
|
+
// Sin principales no hay con qué competir: lista plana.
|
|
153
|
+
secondaryEntries.map(renderEntry)
|
|
154
|
+
)}
|
|
155
|
+
</DropdownMenuContent>
|
|
156
|
+
</DropdownMenu>
|
|
157
|
+
</div>
|
|
158
|
+
)
|
|
159
|
+
}
|
package/src/slot.tsx
CHANGED
|
@@ -1,56 +1,19 @@
|
|
|
1
|
-
// Slot
|
|
2
|
-
//
|
|
1
|
+
// Slot — named extension points the host renders and addons contribute to at
|
|
2
|
+
// register() time. Keyed by a slot id (e.g. "dashboard.widgets",
|
|
3
3
|
// "invoice.footer"). Each contribution is an arbitrary React element factory.
|
|
4
|
+
//
|
|
5
|
+
// El store es `slotStore` de @asteby/metacore-sdk: el mismo donde escribe
|
|
6
|
+
// `api.registry.registerSlot` (AddonAPI) y `registerDocumentContribution`, así
|
|
7
|
+
// que hay una sola lista por slot. Una entrada con addon dueño (`owner`) que
|
|
8
|
+
// consta como NO instalado (InstalledAddonsProvider) no se pinta.
|
|
4
9
|
import React, { useSyncExternalStore } from 'react'
|
|
10
|
+
import { slotStore, type SlotComponent, type SlotEntry } from '@asteby/metacore-sdk'
|
|
11
|
+
import { useInstalledAddons } from './installed-addons-context'
|
|
5
12
|
|
|
6
|
-
export type SlotComponent
|
|
13
|
+
export type { SlotComponent, SlotEntry }
|
|
7
14
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
component: SlotComponent
|
|
11
|
-
priority: number
|
|
12
|
-
source?: string
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
type Listener = () => void
|
|
16
|
-
|
|
17
|
-
class SlotRegistryImpl {
|
|
18
|
-
private slots = new Map<string, SlotEntry[]>()
|
|
19
|
-
private listeners = new Set<Listener>()
|
|
20
|
-
|
|
21
|
-
register(slotId: string, component: SlotComponent, opts?: { priority?: number; source?: string }): () => void {
|
|
22
|
-
const entry: SlotEntry = { id: slotId, component, priority: opts?.priority ?? 0, source: opts?.source }
|
|
23
|
-
const list = this.slots.get(slotId) ?? []
|
|
24
|
-
list.push(entry)
|
|
25
|
-
// Higher priority renders first — canonical across SDK and runtime-react.
|
|
26
|
-
// See docs/slot-priority.md.
|
|
27
|
-
list.sort((a, b) => b.priority - a.priority)
|
|
28
|
-
this.slots.set(slotId, list)
|
|
29
|
-
this.emit()
|
|
30
|
-
return () => {
|
|
31
|
-
const arr = this.slots.get(slotId)
|
|
32
|
-
if (!arr) return
|
|
33
|
-
const idx = arr.indexOf(entry)
|
|
34
|
-
if (idx >= 0) {
|
|
35
|
-
arr.splice(idx, 1)
|
|
36
|
-
this.emit()
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
get(slotId: string): SlotEntry[] {
|
|
42
|
-
return this.slots.get(slotId) ?? []
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
subscribe(listener: Listener): () => void {
|
|
46
|
-
this.listeners.add(listener)
|
|
47
|
-
return () => { this.listeners.delete(listener) }
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
private emit() { this.listeners.forEach(l => l()) }
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
export const slotRegistry = new SlotRegistryImpl()
|
|
15
|
+
/** Alias histórico del store canónico (`slotRegistry.register/get/subscribe`). */
|
|
16
|
+
export const slotRegistry = slotStore
|
|
54
17
|
|
|
55
18
|
export interface SlotProps {
|
|
56
19
|
/** Slot id. */
|
|
@@ -62,11 +25,13 @@ export interface SlotProps {
|
|
|
62
25
|
}
|
|
63
26
|
|
|
64
27
|
export function Slot({ name, props, fallback = null }: SlotProps) {
|
|
65
|
-
const
|
|
66
|
-
(cb) =>
|
|
67
|
-
() =>
|
|
68
|
-
() =>
|
|
28
|
+
const all = useSyncExternalStore(
|
|
29
|
+
(cb) => slotStore.subscribe(cb),
|
|
30
|
+
() => slotStore.get(name),
|
|
31
|
+
() => slotStore.get(name),
|
|
69
32
|
)
|
|
33
|
+
const installed = useInstalledAddons()
|
|
34
|
+
const entries = installed ? all.filter((e) => !e.owner || installed.addons.has(e.owner)) : all
|
|
70
35
|
if (entries.length === 0) return <>{fallback}</>
|
|
71
36
|
return (
|
|
72
37
|
<>
|
package/src/types.ts
CHANGED
|
@@ -107,6 +107,13 @@ export interface DocumentFormType {
|
|
|
107
107
|
* reparte el pago en PaymentAllocation dentro de una transacción.
|
|
108
108
|
*/
|
|
109
109
|
submit_action?: string
|
|
110
|
+
/**
|
|
111
|
+
* El alta de este tipo vive en OTRO modelo (`customers.Invoice`): «Crear»
|
|
112
|
+
* no pinta formulario, el host abre la página de ese modelo con su alta
|
|
113
|
+
* abierta (`onDelegateCreate`). P. ej. Documentos fiscales → Facturas: la
|
|
114
|
+
* factura nace en customers y se timbra después. Sin campos ni renglones.
|
|
115
|
+
*/
|
|
116
|
+
create_model?: string
|
|
110
117
|
}
|
|
111
118
|
|
|
112
119
|
/** Contraparte: qué campo del encabezado la elige y qué muestra su tarjeta. */
|
|
@@ -141,6 +148,20 @@ export interface DocumentFormSource {
|
|
|
141
148
|
option_filter?: unknown
|
|
142
149
|
/** Addon requerido (gating de UI; el servidor revalida). */
|
|
143
150
|
requires_addon?: string
|
|
151
|
+
/** Columna de cantidad del renglón origen (default `quantity`). */
|
|
152
|
+
qty_field?: string
|
|
153
|
+
/**
|
|
154
|
+
* Columna del renglón de ESTE documento que guarda el id del renglón origen
|
|
155
|
+
* (p. ej. `sales_order_item_id`). Con ella el host sirve lo pendiente por
|
|
156
|
+
* renglón (`<endpoint>/source-lines`) y rechaza al guardar si se excede.
|
|
157
|
+
*/
|
|
158
|
+
line_link_field?: string
|
|
159
|
+
/** Columna del renglón origen que ya trae lo pendiente (la mantiene su addon). */
|
|
160
|
+
remaining_qty_field?: string
|
|
161
|
+
/** Endpoint del addon que sirve los renglones con `remaining_quantity` (`?id=`). */
|
|
162
|
+
remaining_endpoint?: string
|
|
163
|
+
/** Estados de este documento que no consumen el origen (cancelada…). */
|
|
164
|
+
exclude_states?: string[]
|
|
144
165
|
}
|
|
145
166
|
|
|
146
167
|
/** Vista previa: acción de fila sobre el borrador, llamada con `analyze: true`. */
|
|
@@ -907,6 +928,18 @@ export interface ActionDefinition {
|
|
|
907
928
|
*/
|
|
908
929
|
replaces_create?: boolean
|
|
909
930
|
replacesCreate?: boolean
|
|
931
|
+
/**
|
|
932
|
+
* Manifest v3 `priority`. "primary": la única acción destacada del menú de
|
|
933
|
+
* fila / cabecera. "secondary": compartir, imprimir, correo, chat → pie del
|
|
934
|
+
* documento y «Más…» del menú de fila. Sin valor: convención por clave
|
|
935
|
+
* (ver classifyActionPriority).
|
|
936
|
+
*/
|
|
937
|
+
priority?: 'primary' | 'secondary'
|
|
938
|
+
/**
|
|
939
|
+
* Cómo despacha (served `trigger`). `type: "capability"` necesita un addon
|
|
940
|
+
* que provea `capability`; sin proveedor activo la acción no se ofrece.
|
|
941
|
+
*/
|
|
942
|
+
trigger?: { type: string; capability?: string; connector?: string }
|
|
910
943
|
/**
|
|
911
944
|
* Optional addon this action depends on, stamped by the host only while it
|
|
912
945
|
* is NOT installed (served as `requires_addon`; read through
|
|
@@ -958,6 +991,8 @@ export interface ActionMetadata {
|
|
|
958
991
|
/** Federated modal slug "<addon_key>.<action_key>"; see ActionModalDispatcher. */
|
|
959
992
|
modal?: string
|
|
960
993
|
placement?: 'row' | 'table' | 'create'
|
|
994
|
+
/** Manifest v3 `priority`; ver ActionDefinition.priority. */
|
|
995
|
+
priority?: 'primary' | 'secondary'
|
|
961
996
|
/**
|
|
962
997
|
* Optional addon this action depends on, stamped by the host only while it
|
|
963
998
|
* is NOT installed (served as `requires_addon`; read through
|