@asteby/metacore-runtime-react 23.7.0 → 23.7.1
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 +15 -0
- package/dist/action-modal-dispatcher.js +14 -6
- package/dist/model-action-toolbar.d.ts.map +1 -1
- package/dist/model-action-toolbar.js +3 -1
- package/package.json +1 -1
- package/src/__tests__/model-action-toolbar-i18n.test.tsx +101 -0
- package/src/action-modal-dispatcher.tsx +16 -8
- package/src/model-action-toolbar.tsx +9 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
# @asteby/metacore-runtime-react
|
|
2
2
|
|
|
3
|
+
## 23.7.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- a3538da: fix(runtime-react): translate addon action labels at render (toolbar + modal)
|
|
8
|
+
|
|
9
|
+
`ModelActionToolbar` and `ActionModalDispatcher` rendered an action's `label`
|
|
10
|
+
(and each field's `label`) verbatim. For an addon's custom action these are i18n
|
|
11
|
+
keys (e.g. `integration_github.action.create_issue.label`) whose locale bundle
|
|
12
|
+
loads asynchronously, so the create button, the modal title/submit, and the
|
|
13
|
+
field labels showed the raw key — and never re-derived once the bundle landed.
|
|
14
|
+
They now translate at render with `t(label, { defaultValue: label })`, so the
|
|
15
|
+
label resolves the moment the addon i18n arrives (via the i18next store `added`
|
|
16
|
+
event) and an already-localized string passes through unchanged.
|
|
17
|
+
|
|
3
18
|
## 23.7.0
|
|
4
19
|
|
|
5
20
|
### Minor Changes
|
|
@@ -111,6 +111,10 @@ function ConfirmActionDialog({ open, onOpenChange, action, model, record, endpoi
|
|
|
111
111
|
const { t } = useTranslation();
|
|
112
112
|
const api = useApi();
|
|
113
113
|
const [executing, setExecuting] = useState(false);
|
|
114
|
+
// `action.label` is an addon-contributed i18n key; its locale bundle loads
|
|
115
|
+
// asynchronously, so translate at render (defaultValue keeps an already
|
|
116
|
+
// localized label unchanged).
|
|
117
|
+
const label = t(action.label, { defaultValue: action.label });
|
|
114
118
|
const execute = async () => {
|
|
115
119
|
setExecuting(true);
|
|
116
120
|
try {
|
|
@@ -132,10 +136,14 @@ function ConfirmActionDialog({ open, onOpenChange, action, model, record, endpoi
|
|
|
132
136
|
setExecuting(false);
|
|
133
137
|
}
|
|
134
138
|
};
|
|
135
|
-
return (_jsx(AlertDialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(AlertDialogContent, { children: [_jsxs(AlertDialogHeader, { children: [_jsxs(AlertDialogTitle, { className: "flex items-center gap-2", children: [_jsx(DynamicIcon, { name: action.icon, className: "h-5 w-5" }),
|
|
139
|
+
return (_jsx(AlertDialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(AlertDialogContent, { children: [_jsxs(AlertDialogHeader, { children: [_jsxs(AlertDialogTitle, { className: "flex items-center gap-2", children: [_jsx(DynamicIcon, { name: action.icon, className: "h-5 w-5" }), label] }), _jsx(AlertDialogDescription, { children: action.confirmMessage || `${label}?` })] }), _jsxs(AlertDialogFooter, { children: [_jsx(AlertDialogCancel, { disabled: executing, children: t('common.cancel') }), _jsxs(AlertDialogAction, { onClick: (e) => { e.preventDefault(); execute(); }, disabled: executing, style: action.color ? { backgroundColor: action.color } : undefined, children: [executing ? _jsx(Loader2, { className: "mr-2 h-4 w-4 animate-spin" }) : _jsx(DynamicIcon, { name: action.icon, className: "mr-2 h-4 w-4" }), label] })] })] }) }));
|
|
136
140
|
}
|
|
137
141
|
function GenericActionModal({ open, onOpenChange, action, model, record, endpoint, onSuccess }) {
|
|
138
142
|
const { t } = useTranslation();
|
|
143
|
+
// Addon-contributed labels (action + fields) are i18n keys whose locale
|
|
144
|
+
// bundle loads asynchronously; translate at render so they don't render raw.
|
|
145
|
+
// defaultValue keeps an already-localized string unchanged.
|
|
146
|
+
const tl = (s) => t(s, { defaultValue: s });
|
|
139
147
|
const api = useApi();
|
|
140
148
|
const [formData, setFormData] = useState({});
|
|
141
149
|
const [executing, setExecuting] = useState(false);
|
|
@@ -194,13 +202,13 @@ function GenericActionModal({ open, onOpenChange, action, model, record, endpoin
|
|
|
194
202
|
if (isLineItemsField(field)) {
|
|
195
203
|
const rows = formData[field.key];
|
|
196
204
|
if (!Array.isArray(rows) || rows.length === 0) {
|
|
197
|
-
toast.error(`${field.label} requiere al menos un renglón`);
|
|
205
|
+
toast.error(`${tl(field.label)} requiere al menos un renglón`);
|
|
198
206
|
return;
|
|
199
207
|
}
|
|
200
208
|
continue;
|
|
201
209
|
}
|
|
202
210
|
if (!formData[field.key] && formData[field.key] !== false) {
|
|
203
|
-
toast.error(`${field.label} es requerido`);
|
|
211
|
+
toast.error(`${tl(field.label)} es requerido`);
|
|
204
212
|
return;
|
|
205
213
|
}
|
|
206
214
|
}
|
|
@@ -239,12 +247,12 @@ function GenericActionModal({ open, onOpenChange, action, model, record, endpoin
|
|
|
239
247
|
: hasLineItems
|
|
240
248
|
? '820px'
|
|
241
249
|
: undefined;
|
|
242
|
-
return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { className: 'flex max-h-[90vh] flex-col overflow-hidden ' + (widthPx ? '' : 'sm:max-w-xl'), style: { maxHeight: '90vh', ...(widthPx ? { maxWidth: widthPx, width: '95vw' } : {}) }, children: [_jsxs(DialogHeader, { className: "shrink-0", children: [_jsxs(DialogTitle, { className: "flex items-center gap-2", children: [_jsx(DynamicIcon, { name: action.icon, className: "h-5 w-5" }), action.label] }), action.confirmMessage && _jsx(DialogDescription, { children: action.confirmMessage })] }), _jsx("div", { className: "-mx-1 min-h-0 flex-1 overflow-y-auto px-1 py-4", children: _jsxs(FieldGrid, { children: [action.fields?.map((field) => {
|
|
250
|
+
return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { className: 'flex max-h-[90vh] flex-col overflow-hidden ' + (widthPx ? '' : 'sm:max-w-xl'), style: { maxHeight: '90vh', ...(widthPx ? { maxWidth: widthPx, width: '95vw' } : {}) }, children: [_jsxs(DialogHeader, { className: "shrink-0", children: [_jsxs(DialogTitle, { className: "flex items-center gap-2", children: [_jsx(DynamicIcon, { name: action.icon, className: "h-5 w-5" }), tl(action.label)] }), action.confirmMessage && _jsx(DialogDescription, { children: action.confirmMessage })] }), _jsx("div", { className: "-mx-1 min-h-0 flex-1 overflow-y-auto px-1 py-4", children: _jsxs(FieldGrid, { children: [action.fields?.map((field) => {
|
|
243
251
|
const fullWidth = isLineItemsField(field) ||
|
|
244
252
|
resolveWidget(field) === 'textarea' ||
|
|
245
253
|
resolveWidget(field) === 'richtext';
|
|
246
|
-
return (_jsxs(FieldCell, { fullWidth: fullWidth, children: [_jsx(FieldLabel, { htmlFor: field.key, required: field.required, children: field.label }), renderField(field, formData[field.key], (v) => updateField(field.key, v), formData)] }, field.key));
|
|
247
|
-
}), relations.length > 0 && (_jsx(FieldCell, { fullWidth: true, children: _jsx(DynamicRelations, { record: record, relations: relations }) }))] }) }), _jsxs(DialogFooter, { className: "shrink-0", children: [_jsx(Button, { variant: "outline", onClick: () => onOpenChange(false), disabled: executing, children: t('common.cancel') }), _jsxs(Button, { onClick: execute, disabled: executing, style: action.color ? { backgroundColor: action.color, color: 'white' } : undefined, children: [executing ? _jsx(Loader2, { className: "mr-2 h-4 w-4 animate-spin" }) : _jsx(DynamicIcon, { name: action.icon, className: "mr-2 h-4 w-4" }), action.label] })] })] }) }));
|
|
254
|
+
return (_jsxs(FieldCell, { fullWidth: fullWidth, children: [_jsx(FieldLabel, { htmlFor: field.key, required: field.required, children: tl(field.label) }), renderField(field, formData[field.key], (v) => updateField(field.key, v), formData)] }, field.key));
|
|
255
|
+
}), relations.length > 0 && (_jsx(FieldCell, { fullWidth: true, children: _jsx(DynamicRelations, { record: record, relations: relations }) }))] }) }), _jsxs(DialogFooter, { className: "shrink-0", children: [_jsx(Button, { variant: "outline", onClick: () => onOpenChange(false), disabled: executing, children: t('common.cancel') }), _jsxs(Button, { onClick: execute, disabled: executing, style: action.color ? { backgroundColor: action.color, color: 'white' } : undefined, children: [executing ? _jsx(Loader2, { className: "mr-2 h-4 w-4 animate-spin" }) : _jsx(DynamicIcon, { name: action.icon, className: "mr-2 h-4 w-4" }), tl(action.label)] })] })] }) }));
|
|
248
256
|
}
|
|
249
257
|
function renderField(field, value, onChange,
|
|
250
258
|
// Full current form values — lets a line-items grid (and any cascading
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"model-action-toolbar.d.ts","sourceRoot":"","sources":["../src/model-action-toolbar.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"model-action-toolbar.d.ts","sourceRoot":"","sources":["../src/model-action-toolbar.tsx"],"names":[],"mappings":"AA2BA,OAAO,KAAK,EAAE,gBAAgB,EAAiC,MAAM,SAAS,CAAA;AAE9E,MAAM,MAAM,eAAe,GAAG,KAAK,GAAG,OAAO,GAAG,QAAQ,CAAA;AAExD,MAAM,WAAW,uBAAuB;IACpC,oEAAoE;IACpE,KAAK,EAAE,MAAM,CAAA;IACb,8EAA8E;IAC9E,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB;;;;OAIG;IACH,OAAO,CAAC,EAAE,gBAAgB,EAAE,CAAA;IAC5B,qEAAqE;IACrE,UAAU,CAAC,EAAE,eAAe,EAAE,CAAA;IAC9B,qDAAqD;IACrD,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAA;IACrB,iDAAiD;IACjD,SAAS,CAAC,EAAE,MAAM,CAAA;CACrB;AAmBD;;;GAGG;AACH,wBAAgB,eAAe,CAC3B,KAAK,EAAE,MAAM,EACb,UAAU,GAAE,eAAe,EAAuB,EAClD,QAAQ,CAAC,EAAE,gBAAgB,EAAE,GAC9B,gBAAgB,EAAE,CA2BpB;AAED,wBAAgB,kBAAkB,CAAC,EAC/B,KAAK,EACL,QAAQ,EACR,OAAO,EACP,UAA+B,EAC/B,QAAQ,EACR,SAAS,GACZ,EAAE,uBAAuB,sCA0DzB"}
|
|
@@ -19,6 +19,7 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
19
19
|
// it internally, and bespoke host pages (e.g. ops `/m/$model`) mount it directly
|
|
20
20
|
// next to their own toolbar. Hosts never reimplement action-button plumbing.
|
|
21
21
|
import { useEffect, useMemo, useState } from 'react';
|
|
22
|
+
import { useTranslation } from 'react-i18next';
|
|
22
23
|
import { Button } from '@asteby/metacore-ui/primitives';
|
|
23
24
|
import { useApi } from './api-context';
|
|
24
25
|
import { useMetadataCache } from './metadata-cache';
|
|
@@ -71,6 +72,7 @@ export function useModelActions(model, placements = DEFAULT_PLACEMENTS, provided
|
|
|
71
72
|
return useMemo(() => all.filter((a) => placements.includes((a.placement ?? 'row'))), [all, placements]);
|
|
72
73
|
}
|
|
73
74
|
export function ModelActionToolbar({ model, endpoint, actions, placements = DEFAULT_PLACEMENTS, onChange, className, }) {
|
|
75
|
+
const { t } = useTranslation();
|
|
74
76
|
const all = useModelActions(model, placements, actions);
|
|
75
77
|
// Capability gating — always-true without a <PermissionsProvider>. Custom
|
|
76
78
|
// table/create actions map onto `lowercase(model).<action_key>`.
|
|
@@ -82,7 +84,7 @@ export function ModelActionToolbar({ model, endpoint, actions, placements = DEFA
|
|
|
82
84
|
return null;
|
|
83
85
|
return (_jsxs(_Fragment, { children: [_jsx("div", { className: className ?? 'flex items-center gap-2', children: surfaced.map((a) => {
|
|
84
86
|
const isCreate = (a.placement ?? 'row') === 'create';
|
|
85
|
-
return (_jsxs(Button, { variant: isCreate ? 'default' : 'outline', onClick: () => setActive(toActionMetadata(a)), style: a.color && !isCreate ? { borderColor: a.color, color: a.color } : undefined, children: [_jsx(DynamicIcon, { name: a.icon || (isCreate ? 'Plus' : 'Zap'), className: "mr-2 h-4 w-4" }), a.label] }, a.key));
|
|
87
|
+
return (_jsxs(Button, { variant: isCreate ? 'default' : 'outline', onClick: () => setActive(toActionMetadata(a)), style: a.color && !isCreate ? { borderColor: a.color, color: a.color } : undefined, children: [_jsx(DynamicIcon, { name: a.icon || (isCreate ? 'Plus' : 'Zap'), className: "mr-2 h-4 w-4" }), t(a.label, { defaultValue: a.label })] }, a.key));
|
|
86
88
|
}) }), active && (_jsx(ActionModalDispatcher, { open: !!active, onOpenChange: (open) => {
|
|
87
89
|
if (!open)
|
|
88
90
|
setActive(null);
|
package/package.json
CHANGED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
// @vitest-environment happy-dom
|
|
2
|
+
import { afterEach, describe, expect, it } from 'vitest'
|
|
3
|
+
import { act, cleanup, render, screen, waitFor } from '@testing-library/react'
|
|
4
|
+
import i18next from 'i18next'
|
|
5
|
+
import { I18nextProvider } from 'react-i18next'
|
|
6
|
+
|
|
7
|
+
import { ModelActionToolbar } from '../model-action-toolbar'
|
|
8
|
+
import { ApiProvider } from '../api-context'
|
|
9
|
+
import type { ActionDefinition } from '../types'
|
|
10
|
+
|
|
11
|
+
afterEach(cleanup)
|
|
12
|
+
|
|
13
|
+
// Stub api client — the toolbar only calls it when it must fetch metadata, and
|
|
14
|
+
// we pass `actions` directly so it never does. Present only to satisfy useApi().
|
|
15
|
+
const api = {
|
|
16
|
+
get: async () => ({ data: { data: {} } }),
|
|
17
|
+
post: async () => ({ data: { success: true } }),
|
|
18
|
+
} as never
|
|
19
|
+
|
|
20
|
+
// A fresh i18next configured EXACTLY like the failing prod instance: keySeparator
|
|
21
|
+
// default and ignoreJSONStructure:false, so a flat literal key would NOT resolve
|
|
22
|
+
// — the fix must build/read a nested tree AND translate at render time.
|
|
23
|
+
function makeI18n() {
|
|
24
|
+
const inst = i18next.createInstance()
|
|
25
|
+
inst.init({
|
|
26
|
+
lng: 'es',
|
|
27
|
+
fallbackLng: 'es',
|
|
28
|
+
ignoreJSONStructure: false,
|
|
29
|
+
react: { bindI18nStore: 'added removed', useSuspense: false },
|
|
30
|
+
resources: { es: { translation: {} } },
|
|
31
|
+
})
|
|
32
|
+
return inst
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// The addon's create action exactly as the metadata endpoint ships it: the
|
|
36
|
+
// label is an i18n KEY, not display text (backend does not localize action
|
|
37
|
+
// labels — see the raw-key bug).
|
|
38
|
+
const createAction: ActionDefinition = {
|
|
39
|
+
key: 'create_issue',
|
|
40
|
+
label: 'integration_github.action.create_issue.label',
|
|
41
|
+
placement: 'create',
|
|
42
|
+
} as ActionDefinition
|
|
43
|
+
|
|
44
|
+
function renderToolbar(inst: ReturnType<typeof makeI18n>) {
|
|
45
|
+
return render(
|
|
46
|
+
<I18nextProvider i18n={inst}>
|
|
47
|
+
<ApiProvider client={api}>
|
|
48
|
+
<ModelActionToolbar model="github_issues" actions={[createAction]} />
|
|
49
|
+
</ApiProvider>
|
|
50
|
+
</I18nextProvider>,
|
|
51
|
+
)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
describe('ModelActionToolbar — translates addon action labels at render', () => {
|
|
55
|
+
it('shows the translation once the addon bundle lands (no raw key)', async () => {
|
|
56
|
+
const inst = makeI18n()
|
|
57
|
+
renderToolbar(inst)
|
|
58
|
+
|
|
59
|
+
// Before the addon bundle loads the toolbar has only the key; with the
|
|
60
|
+
// fix it renders the key text (t() falls back to defaultValue = the key).
|
|
61
|
+
// The point of the test is the TRANSITION below.
|
|
62
|
+
expect(
|
|
63
|
+
screen.getByText('integration_github.action.create_issue.label'),
|
|
64
|
+
).toBeTruthy()
|
|
65
|
+
|
|
66
|
+
// The addon locale bundle arrives asynchronously (OpsAddonLocaleLoader
|
|
67
|
+
// fetch → addResourceBundle). It is NESTED, as the host loader now merges
|
|
68
|
+
// it. This fires the i18next store 'added' event.
|
|
69
|
+
act(() => {
|
|
70
|
+
inst.addResourceBundle(
|
|
71
|
+
'es',
|
|
72
|
+
'translation',
|
|
73
|
+
{ integration_github: { action: { create_issue: { label: 'Crear issue' } } } },
|
|
74
|
+
true,
|
|
75
|
+
true,
|
|
76
|
+
)
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
// The toolbar must re-render and show the translated label — the exact
|
|
80
|
+
// behaviour that was missing (a bare {a.label} never re-derived).
|
|
81
|
+
await waitFor(() => expect(screen.getByText('Crear issue')).toBeTruthy())
|
|
82
|
+
expect(
|
|
83
|
+
screen.queryByText('integration_github.action.create_issue.label'),
|
|
84
|
+
).toBeNull()
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
it('leaves an already-localized label untouched (defaultValue passthrough)', () => {
|
|
88
|
+
const inst = makeI18n()
|
|
89
|
+
render(
|
|
90
|
+
<I18nextProvider i18n={inst}>
|
|
91
|
+
<ApiProvider client={api}>
|
|
92
|
+
<ModelActionToolbar
|
|
93
|
+
model="github_issues"
|
|
94
|
+
actions={[{ key: 'x', label: 'Crear issue', placement: 'create' } as ActionDefinition]}
|
|
95
|
+
/>
|
|
96
|
+
</ApiProvider>
|
|
97
|
+
</I18nextProvider>,
|
|
98
|
+
)
|
|
99
|
+
expect(screen.getByText('Crear issue')).toBeTruthy()
|
|
100
|
+
})
|
|
101
|
+
})
|
|
@@ -218,6 +218,10 @@ function ConfirmActionDialog({ open, onOpenChange, action, model, record, endpoi
|
|
|
218
218
|
const { t } = useTranslation()
|
|
219
219
|
const api = useApi()
|
|
220
220
|
const [executing, setExecuting] = useState(false)
|
|
221
|
+
// `action.label` is an addon-contributed i18n key; its locale bundle loads
|
|
222
|
+
// asynchronously, so translate at render (defaultValue keeps an already
|
|
223
|
+
// localized label unchanged).
|
|
224
|
+
const label = t(action.label, { defaultValue: action.label })
|
|
221
225
|
|
|
222
226
|
const execute = async () => {
|
|
223
227
|
setExecuting(true)
|
|
@@ -244,10 +248,10 @@ function ConfirmActionDialog({ open, onOpenChange, action, model, record, endpoi
|
|
|
244
248
|
<AlertDialogHeader>
|
|
245
249
|
<AlertDialogTitle className="flex items-center gap-2">
|
|
246
250
|
<DynamicIcon name={action.icon} className="h-5 w-5" />
|
|
247
|
-
{
|
|
251
|
+
{label}
|
|
248
252
|
</AlertDialogTitle>
|
|
249
253
|
<AlertDialogDescription>
|
|
250
|
-
{action.confirmMessage || `${
|
|
254
|
+
{action.confirmMessage || `${label}?`}
|
|
251
255
|
</AlertDialogDescription>
|
|
252
256
|
</AlertDialogHeader>
|
|
253
257
|
<AlertDialogFooter>
|
|
@@ -258,7 +262,7 @@ function ConfirmActionDialog({ open, onOpenChange, action, model, record, endpoi
|
|
|
258
262
|
style={action.color ? { backgroundColor: action.color } : undefined}
|
|
259
263
|
>
|
|
260
264
|
{executing ? <Loader2 className="mr-2 h-4 w-4 animate-spin" /> : <DynamicIcon name={action.icon} className="mr-2 h-4 w-4" />}
|
|
261
|
-
{
|
|
265
|
+
{label}
|
|
262
266
|
</AlertDialogAction>
|
|
263
267
|
</AlertDialogFooter>
|
|
264
268
|
</AlertDialogContent>
|
|
@@ -268,6 +272,10 @@ function ConfirmActionDialog({ open, onOpenChange, action, model, record, endpoi
|
|
|
268
272
|
|
|
269
273
|
function GenericActionModal({ open, onOpenChange, action, model, record, endpoint, onSuccess }: ActionModalProps) {
|
|
270
274
|
const { t } = useTranslation()
|
|
275
|
+
// Addon-contributed labels (action + fields) are i18n keys whose locale
|
|
276
|
+
// bundle loads asynchronously; translate at render so they don't render raw.
|
|
277
|
+
// defaultValue keeps an already-localized string unchanged.
|
|
278
|
+
const tl = (s: string) => t(s, { defaultValue: s })
|
|
271
279
|
const api = useApi()
|
|
272
280
|
const [formData, setFormData] = useState<Record<string, any>>({})
|
|
273
281
|
const [executing, setExecuting] = useState(false)
|
|
@@ -326,13 +334,13 @@ function GenericActionModal({ open, onOpenChange, action, model, record, endpoin
|
|
|
326
334
|
if (isLineItemsField(field)) {
|
|
327
335
|
const rows = formData[field.key]
|
|
328
336
|
if (!Array.isArray(rows) || rows.length === 0) {
|
|
329
|
-
toast.error(`${field.label} requiere al menos un renglón`)
|
|
337
|
+
toast.error(`${tl(field.label)} requiere al menos un renglón`)
|
|
330
338
|
return
|
|
331
339
|
}
|
|
332
340
|
continue
|
|
333
341
|
}
|
|
334
342
|
if (!formData[field.key] && formData[field.key] !== false) {
|
|
335
|
-
toast.error(`${field.label} es requerido`)
|
|
343
|
+
toast.error(`${tl(field.label)} es requerido`)
|
|
336
344
|
return
|
|
337
345
|
}
|
|
338
346
|
}
|
|
@@ -389,7 +397,7 @@ function GenericActionModal({ open, onOpenChange, action, model, record, endpoin
|
|
|
389
397
|
<DialogHeader className="shrink-0">
|
|
390
398
|
<DialogTitle className="flex items-center gap-2">
|
|
391
399
|
<DynamicIcon name={action.icon} className="h-5 w-5" />
|
|
392
|
-
{action.label}
|
|
400
|
+
{tl(action.label)}
|
|
393
401
|
</DialogTitle>
|
|
394
402
|
{action.confirmMessage && <DialogDescription>{action.confirmMessage}</DialogDescription>}
|
|
395
403
|
</DialogHeader>
|
|
@@ -408,7 +416,7 @@ function GenericActionModal({ open, onOpenChange, action, model, record, endpoin
|
|
|
408
416
|
return (
|
|
409
417
|
<FieldCell key={field.key} fullWidth={fullWidth}>
|
|
410
418
|
<FieldLabel htmlFor={field.key} required={field.required}>
|
|
411
|
-
{field.label}
|
|
419
|
+
{tl(field.label)}
|
|
412
420
|
</FieldLabel>
|
|
413
421
|
{renderField(field, formData[field.key], (v: any) => updateField(field.key, v), formData)}
|
|
414
422
|
</FieldCell>
|
|
@@ -431,7 +439,7 @@ function GenericActionModal({ open, onOpenChange, action, model, record, endpoin
|
|
|
431
439
|
style={action.color ? { backgroundColor: action.color, color: 'white' } : undefined}
|
|
432
440
|
>
|
|
433
441
|
{executing ? <Loader2 className="mr-2 h-4 w-4 animate-spin" /> : <DynamicIcon name={action.icon} className="mr-2 h-4 w-4" />}
|
|
434
|
-
{action.label}
|
|
442
|
+
{tl(action.label)}
|
|
435
443
|
</Button>
|
|
436
444
|
</DialogFooter>
|
|
437
445
|
</DialogContent>
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
// it internally, and bespoke host pages (e.g. ops `/m/$model`) mount it directly
|
|
19
19
|
// next to their own toolbar. Hosts never reimplement action-button plumbing.
|
|
20
20
|
import { useEffect, useMemo, useState } from 'react'
|
|
21
|
+
import { useTranslation } from 'react-i18next'
|
|
21
22
|
import { Button } from '@asteby/metacore-ui/primitives'
|
|
22
23
|
import { useApi } from './api-context'
|
|
23
24
|
import { useMetadataCache } from './metadata-cache'
|
|
@@ -109,6 +110,7 @@ export function ModelActionToolbar({
|
|
|
109
110
|
onChange,
|
|
110
111
|
className,
|
|
111
112
|
}: ModelActionToolbarProps) {
|
|
113
|
+
const { t } = useTranslation()
|
|
112
114
|
const all = useModelActions(model, placements, actions)
|
|
113
115
|
// Capability gating — always-true without a <PermissionsProvider>. Custom
|
|
114
116
|
// table/create actions map onto `lowercase(model).<action_key>`.
|
|
@@ -135,7 +137,13 @@ export function ModelActionToolbar({
|
|
|
135
137
|
style={a.color && !isCreate ? { borderColor: a.color, color: a.color } : undefined}
|
|
136
138
|
>
|
|
137
139
|
<DynamicIcon name={a.icon || (isCreate ? 'Plus' : 'Zap')} className="mr-2 h-4 w-4" />
|
|
138
|
-
{a.label
|
|
140
|
+
{/* `a.label` is an addon-contributed i18n key (e.g.
|
|
141
|
+
"integration_github.action.create_issue.label"); the addon's
|
|
142
|
+
locale bundle loads asynchronously, so translate at render — a
|
|
143
|
+
bare `{a.label}` prints the raw key until (and after) the bundle
|
|
144
|
+
lands because nothing re-derives it. defaultValue keeps an
|
|
145
|
+
already-localized label untouched. */}
|
|
146
|
+
{t(a.label, { defaultValue: a.label })}
|
|
139
147
|
</Button>
|
|
140
148
|
)
|
|
141
149
|
})}
|