@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.
Files changed (97) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/dist/action-modal-dispatcher.d.ts +33 -3
  3. package/dist/action-modal-dispatcher.d.ts.map +1 -1
  4. package/dist/action-modal-dispatcher.js +84 -11
  5. package/dist/action-priority.d.ts +34 -0
  6. package/dist/action-priority.d.ts.map +1 -0
  7. package/dist/action-priority.js +60 -0
  8. package/dist/business/catalog-product-search.d.ts +7 -0
  9. package/dist/business/catalog-product-search.d.ts.map +1 -1
  10. package/dist/business/catalog-product-search.js +14 -0
  11. package/dist/business/document-editor-model.d.ts +14 -2
  12. package/dist/business/document-editor-model.d.ts.map +1 -1
  13. package/dist/business/document-editor-model.js +42 -4
  14. package/dist/business/document-editor.d.ts.map +1 -1
  15. package/dist/business/document-editor.js +68 -19
  16. package/dist/business/line-items.d.ts +9 -1
  17. package/dist/business/line-items.d.ts.map +1 -1
  18. package/dist/business/line-items.js +8 -1
  19. package/dist/calendar-date.d.ts +12 -0
  20. package/dist/calendar-date.d.ts.map +1 -1
  21. package/dist/calendar-date.js +36 -0
  22. package/dist/document/document-page.d.ts.map +1 -1
  23. package/dist/document/document-page.js +8 -1
  24. package/dist/document/evaluate.d.ts.map +1 -1
  25. package/dist/document/evaluate.js +17 -1
  26. package/dist/document/index.d.ts +1 -0
  27. package/dist/document/index.d.ts.map +1 -1
  28. package/dist/document/index.js +1 -0
  29. package/dist/document/secondary-bar.d.ts +20 -0
  30. package/dist/document/secondary-bar.d.ts.map +1 -0
  31. package/dist/document/secondary-bar.js +44 -0
  32. package/dist/document/types.d.ts +8 -0
  33. package/dist/document/types.d.ts.map +1 -1
  34. package/dist/document-form-dialog.d.ts +23 -2
  35. package/dist/document-form-dialog.d.ts.map +1 -1
  36. package/dist/document-form-dialog.js +48 -9
  37. package/dist/dynamic-columns.d.ts +1 -11
  38. package/dist/dynamic-columns.d.ts.map +1 -1
  39. package/dist/dynamic-columns.js +7 -17
  40. package/dist/dynamic-crud-page.d.ts +19 -0
  41. package/dist/dynamic-crud-page.d.ts.map +1 -1
  42. package/dist/dynamic-crud-page.js +6 -5
  43. package/dist/dynamic-row-actions.d.ts +8 -0
  44. package/dist/dynamic-row-actions.d.ts.map +1 -1
  45. package/dist/dynamic-row-actions.js +12 -2
  46. package/dist/dynamic-table.d.ts.map +1 -1
  47. package/dist/dynamic-table.js +64 -63
  48. package/dist/index.d.ts +5 -2
  49. package/dist/index.d.ts.map +1 -1
  50. package/dist/index.js +9 -2
  51. package/dist/primitives/contributions.d.ts +14 -3
  52. package/dist/primitives/contributions.d.ts.map +1 -1
  53. package/dist/primitives/contributions.js +50 -13
  54. package/dist/record-actions.d.ts +21 -0
  55. package/dist/record-actions.d.ts.map +1 -0
  56. package/dist/record-actions.js +76 -0
  57. package/dist/row-actions-menu.d.ts +32 -0
  58. package/dist/row-actions-menu.d.ts.map +1 -0
  59. package/dist/row-actions-menu.js +59 -0
  60. package/dist/slot.d.ts +4 -21
  61. package/dist/slot.d.ts.map +1 -1
  62. package/dist/slot.js +14 -36
  63. package/dist/types.d.ts +39 -0
  64. package/dist/types.d.ts.map +1 -1
  65. package/package.json +3 -3
  66. package/src/__tests__/document-editor-model.test.ts +55 -1
  67. package/src/__tests__/document-editor.test.tsx +80 -1
  68. package/src/__tests__/document-form-create-model.test.tsx +143 -0
  69. package/src/__tests__/document-form-dialog.test.tsx +40 -0
  70. package/src/__tests__/field-default-today.test.tsx +116 -0
  71. package/src/__tests__/prefill-scalar-from-record.test.ts +52 -1
  72. package/src/__tests__/row-action-link-navigation.test.ts +17 -0
  73. package/src/__tests__/secondary-actions.test.tsx +261 -0
  74. package/src/action-modal-dispatcher.tsx +97 -17
  75. package/src/action-priority.ts +82 -0
  76. package/src/business/business.test.ts +1 -1
  77. package/src/business/catalog-product-search.ts +18 -0
  78. package/src/business/document-editor-model.ts +48 -6
  79. package/src/business/document-editor.tsx +75 -20
  80. package/src/business/line-items.ts +16 -1
  81. package/src/calendar-date.ts +36 -0
  82. package/src/document/document-page.tsx +15 -0
  83. package/src/document/evaluate.ts +17 -1
  84. package/src/document/index.ts +1 -0
  85. package/src/document/secondary-bar.tsx +92 -0
  86. package/src/document/types.ts +8 -0
  87. package/src/document-form-dialog.tsx +67 -8
  88. package/src/dynamic-columns.tsx +13 -57
  89. package/src/dynamic-crud-page.tsx +31 -4
  90. package/src/dynamic-row-actions.tsx +12 -2
  91. package/src/dynamic-table.tsx +3 -0
  92. package/src/index.ts +31 -0
  93. package/src/primitives/contributions.ts +59 -12
  94. package/src/record-actions.ts +101 -0
  95. package/src/row-actions-menu.tsx +159 -0
  96. package/src/slot.tsx +18 -53
  97. 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 / SlotRegistry — named extension points the host renders and addons
2
- // contribute to at register() time. Keyed by a slot id (e.g. "dashboard.widgets",
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<P = any> = React.ComponentType<P>
13
+ export type { SlotComponent, SlotEntry }
7
14
 
8
- interface SlotEntry {
9
- id: string
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 entries = useSyncExternalStore(
66
- (cb) => slotRegistry.subscribe(cb),
67
- () => slotRegistry.get(name),
68
- () => slotRegistry.get(name),
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