@asteby/metacore-runtime-react 37.0.1 → 37.0.2
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 +6 -0
- package/dist/model-action-toolbar.d.ts +16 -1
- package/dist/model-action-toolbar.d.ts.map +1 -1
- package/dist/model-action-toolbar.js +30 -2
- package/package.json +3 -3
- package/src/__tests__/model-action-toolbar-i18n.test.tsx +29 -6
- package/src/model-action-toolbar.tsx +42 -3
package/CHANGELOG.md
CHANGED
|
@@ -17,11 +17,26 @@ export interface ModelActionToolbarProps {
|
|
|
17
17
|
onChange?: () => void;
|
|
18
18
|
/** Extra classes on the button row container. */
|
|
19
19
|
className?: string;
|
|
20
|
+
/**
|
|
21
|
+
* Fired on hover/focus of an action button so the host can prefetch a
|
|
22
|
+
* federated remote (e.g. collections) before the click — Module Federation
|
|
23
|
+
* lazy load without blocking first paint of the list page.
|
|
24
|
+
*/
|
|
25
|
+
onActionIntent?: (action: ActionDefinition) => void;
|
|
20
26
|
}
|
|
27
|
+
/**
|
|
28
|
+
* Last-resort label when `t(key)` has no translation yet: never flash the full
|
|
29
|
+
* dotted key in a primary CTA. Prefers the action/field segment over a trailing
|
|
30
|
+
* `.label` / `.confirm` leaf.
|
|
31
|
+
* "receivables.action.collect_payment_create" → "Collect payment create"
|
|
32
|
+
* "integration_github.action.create_issue.label" → "Create issue"
|
|
33
|
+
* Already-human labels pass through unchanged.
|
|
34
|
+
*/
|
|
35
|
+
export declare function humanizeActionLabel(label: string): string;
|
|
21
36
|
/**
|
|
22
37
|
* Returns the model's actions matching the requested placements. Reads from the
|
|
23
38
|
* `actions` prop when provided, else the metadata cache, else fetches once.
|
|
24
39
|
*/
|
|
25
40
|
export declare function useModelActions(model: string, placements?: ActionPlacement[], provided?: ActionDefinition[]): ActionDefinition[];
|
|
26
|
-
export declare function ModelActionToolbar({ model, endpoint, actions, placements, onChange, className, }: ModelActionToolbarProps): import("react").JSX.Element | null;
|
|
41
|
+
export declare function ModelActionToolbar({ model, endpoint, actions, placements, onChange, className, onActionIntent, }: ModelActionToolbarProps): import("react").JSX.Element | null;
|
|
27
42
|
//# sourceMappingURL=model-action-toolbar.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
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;
|
|
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;IAClB;;;;OAIG;IACH,cAAc,CAAC,EAAE,CAAC,MAAM,EAAE,gBAAgB,KAAK,IAAI,CAAA;CACtD;AAID;;;;;;;GAOG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAgBzD;AAiBD;;;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,EACT,cAAc,GACjB,EAAE,uBAAuB,sCAgEzB"}
|
|
@@ -27,6 +27,31 @@ import { DynamicIcon } from './dynamic-icon';
|
|
|
27
27
|
import { ActionModalDispatcher } from './action-modal-dispatcher';
|
|
28
28
|
import { useCan, modelCapability } from './permissions-context';
|
|
29
29
|
const DEFAULT_PLACEMENTS = ['table', 'create'];
|
|
30
|
+
/**
|
|
31
|
+
* Last-resort label when `t(key)` has no translation yet: never flash the full
|
|
32
|
+
* dotted key in a primary CTA. Prefers the action/field segment over a trailing
|
|
33
|
+
* `.label` / `.confirm` leaf.
|
|
34
|
+
* "receivables.action.collect_payment_create" → "Collect payment create"
|
|
35
|
+
* "integration_github.action.create_issue.label" → "Create issue"
|
|
36
|
+
* Already-human labels pass through unchanged.
|
|
37
|
+
*/
|
|
38
|
+
export function humanizeActionLabel(label) {
|
|
39
|
+
if (!label)
|
|
40
|
+
return label;
|
|
41
|
+
if (!label.includes('.') || /\s/.test(label))
|
|
42
|
+
return label;
|
|
43
|
+
const parts = label.split('.').filter(Boolean);
|
|
44
|
+
let leaf = parts[parts.length - 1] || label;
|
|
45
|
+
if ((leaf === 'label' || leaf === 'confirm' || leaf === 'title') &&
|
|
46
|
+
parts.length >= 2) {
|
|
47
|
+
leaf = parts[parts.length - 2];
|
|
48
|
+
}
|
|
49
|
+
return leaf
|
|
50
|
+
.split('_')
|
|
51
|
+
.filter(Boolean)
|
|
52
|
+
.map((w) => w.charAt(0).toUpperCase() + w.slice(1))
|
|
53
|
+
.join(' ');
|
|
54
|
+
}
|
|
30
55
|
function toActionMetadata(a) {
|
|
31
56
|
return {
|
|
32
57
|
key: a.key,
|
|
@@ -71,7 +96,7 @@ export function useModelActions(model, placements = DEFAULT_PLACEMENTS, provided
|
|
|
71
96
|
const all = provided ?? cached?.actions ?? fetched?.actions ?? [];
|
|
72
97
|
return useMemo(() => all.filter((a) => placements.includes((a.placement ?? 'row'))), [all, placements]);
|
|
73
98
|
}
|
|
74
|
-
export function ModelActionToolbar({ model, endpoint, actions, placements = DEFAULT_PLACEMENTS, onChange, className, }) {
|
|
99
|
+
export function ModelActionToolbar({ model, endpoint, actions, placements = DEFAULT_PLACEMENTS, onChange, className, onActionIntent, }) {
|
|
75
100
|
const { t } = useTranslation();
|
|
76
101
|
const all = useModelActions(model, placements, actions);
|
|
77
102
|
// Capability gating — always-true without a <PermissionsProvider>. Custom
|
|
@@ -84,7 +109,10 @@ export function ModelActionToolbar({ model, endpoint, actions, placements = DEFA
|
|
|
84
109
|
return null;
|
|
85
110
|
return (_jsxs(_Fragment, { children: [_jsx("div", { className: className ?? 'flex items-center gap-2', children: surfaced.map((a) => {
|
|
86
111
|
const isCreate = (a.placement ?? 'row') === 'create';
|
|
87
|
-
return (_jsxs(Button, { variant: isCreate ? 'default' : 'outline', onClick: () =>
|
|
112
|
+
return (_jsxs(Button, { variant: isCreate ? 'default' : 'outline', onClick: () => {
|
|
113
|
+
onActionIntent?.(a);
|
|
114
|
+
setActive(toActionMetadata(a));
|
|
115
|
+
}, onMouseEnter: () => onActionIntent?.(a), onFocus: () => onActionIntent?.(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: humanizeActionLabel(a.label) })] }, a.key));
|
|
88
116
|
}) }), active && (_jsx(ActionModalDispatcher, { open: !!active, onOpenChange: (open) => {
|
|
89
117
|
if (!open)
|
|
90
118
|
setActive(null);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@asteby/metacore-runtime-react",
|
|
3
|
-
"version": "37.0.
|
|
3
|
+
"version": "37.0.2",
|
|
4
4
|
"description": "React runtime for metacore hosts — renders addon contributions dynamically",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -67,8 +67,8 @@
|
|
|
67
67
|
"typescript": "^6.0.0",
|
|
68
68
|
"vitest": "^4.0.0",
|
|
69
69
|
"zustand": "^5.0.0",
|
|
70
|
-
"@asteby/metacore-
|
|
71
|
-
"@asteby/metacore-
|
|
70
|
+
"@asteby/metacore-ui": "2.17.3",
|
|
71
|
+
"@asteby/metacore-sdk": "3.7.0"
|
|
72
72
|
},
|
|
73
73
|
"scripts": {
|
|
74
74
|
"build": "tsc -p tsconfig.json",
|
|
@@ -56,12 +56,13 @@ describe('ModelActionToolbar — translates addon action labels at render', () =
|
|
|
56
56
|
const inst = makeI18n()
|
|
57
57
|
renderToolbar(inst)
|
|
58
58
|
|
|
59
|
-
// Before the addon bundle loads the toolbar
|
|
60
|
-
//
|
|
61
|
-
//
|
|
59
|
+
// Before the addon bundle loads the toolbar must NOT flash the full
|
|
60
|
+
// dotted key — humanizeActionLabel turns the last segment into a
|
|
61
|
+
// readable fallback until addResourceBundle fires.
|
|
62
|
+
expect(screen.getByText('Create Issue')).toBeTruthy()
|
|
62
63
|
expect(
|
|
63
|
-
screen.
|
|
64
|
-
).
|
|
64
|
+
screen.queryByText('integration_github.action.create_issue.label'),
|
|
65
|
+
).toBeNull()
|
|
65
66
|
|
|
66
67
|
// The addon locale bundle arrives asynchronously (OpsAddonLocaleLoader
|
|
67
68
|
// fetch → addResourceBundle). It is NESTED, as the host loader now merges
|
|
@@ -79,8 +80,30 @@ describe('ModelActionToolbar — translates addon action labels at render', () =
|
|
|
79
80
|
// The toolbar must re-render and show the translated label — the exact
|
|
80
81
|
// behaviour that was missing (a bare {a.label} never re-derived).
|
|
81
82
|
await waitFor(() => expect(screen.getByText('Crear issue')).toBeTruthy())
|
|
83
|
+
expect(screen.queryByText('Create Issue')).toBeNull()
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
it('humanizes collect_payment_create without flashing the receivables key', () => {
|
|
87
|
+
const inst = makeI18n()
|
|
88
|
+
render(
|
|
89
|
+
<I18nextProvider i18n={inst}>
|
|
90
|
+
<ApiProvider client={api}>
|
|
91
|
+
<ModelActionToolbar
|
|
92
|
+
model="account_statements"
|
|
93
|
+
actions={[
|
|
94
|
+
{
|
|
95
|
+
key: 'collect_payment_create',
|
|
96
|
+
label: 'receivables.action.collect_payment_create',
|
|
97
|
+
placement: 'create',
|
|
98
|
+
} as ActionDefinition,
|
|
99
|
+
]}
|
|
100
|
+
/>
|
|
101
|
+
</ApiProvider>
|
|
102
|
+
</I18nextProvider>,
|
|
103
|
+
)
|
|
104
|
+
expect(screen.getByText('Collect Payment Create')).toBeTruthy()
|
|
82
105
|
expect(
|
|
83
|
-
screen.queryByText('
|
|
106
|
+
screen.queryByText('receivables.action.collect_payment_create'),
|
|
84
107
|
).toBeNull()
|
|
85
108
|
})
|
|
86
109
|
|
|
@@ -46,10 +46,42 @@ export interface ModelActionToolbarProps {
|
|
|
46
46
|
onChange?: () => void
|
|
47
47
|
/** Extra classes on the button row container. */
|
|
48
48
|
className?: string
|
|
49
|
+
/**
|
|
50
|
+
* Fired on hover/focus of an action button so the host can prefetch a
|
|
51
|
+
* federated remote (e.g. collections) before the click — Module Federation
|
|
52
|
+
* lazy load without blocking first paint of the list page.
|
|
53
|
+
*/
|
|
54
|
+
onActionIntent?: (action: ActionDefinition) => void
|
|
49
55
|
}
|
|
50
56
|
|
|
51
57
|
const DEFAULT_PLACEMENTS: ActionPlacement[] = ['table', 'create']
|
|
52
58
|
|
|
59
|
+
/**
|
|
60
|
+
* Last-resort label when `t(key)` has no translation yet: never flash the full
|
|
61
|
+
* dotted key in a primary CTA. Prefers the action/field segment over a trailing
|
|
62
|
+
* `.label` / `.confirm` leaf.
|
|
63
|
+
* "receivables.action.collect_payment_create" → "Collect payment create"
|
|
64
|
+
* "integration_github.action.create_issue.label" → "Create issue"
|
|
65
|
+
* Already-human labels pass through unchanged.
|
|
66
|
+
*/
|
|
67
|
+
export function humanizeActionLabel(label: string): string {
|
|
68
|
+
if (!label) return label
|
|
69
|
+
if (!label.includes('.') || /\s/.test(label)) return label
|
|
70
|
+
const parts = label.split('.').filter(Boolean)
|
|
71
|
+
let leaf = parts[parts.length - 1] || label
|
|
72
|
+
if (
|
|
73
|
+
(leaf === 'label' || leaf === 'confirm' || leaf === 'title') &&
|
|
74
|
+
parts.length >= 2
|
|
75
|
+
) {
|
|
76
|
+
leaf = parts[parts.length - 2]
|
|
77
|
+
}
|
|
78
|
+
return leaf
|
|
79
|
+
.split('_')
|
|
80
|
+
.filter(Boolean)
|
|
81
|
+
.map((w) => w.charAt(0).toUpperCase() + w.slice(1))
|
|
82
|
+
.join(' ')
|
|
83
|
+
}
|
|
84
|
+
|
|
53
85
|
function toActionMetadata(a: ActionDefinition): ActionMetadata {
|
|
54
86
|
return {
|
|
55
87
|
key: a.key,
|
|
@@ -109,6 +141,7 @@ export function ModelActionToolbar({
|
|
|
109
141
|
placements = DEFAULT_PLACEMENTS,
|
|
110
142
|
onChange,
|
|
111
143
|
className,
|
|
144
|
+
onActionIntent,
|
|
112
145
|
}: ModelActionToolbarProps) {
|
|
113
146
|
const { t } = useTranslation()
|
|
114
147
|
const all = useModelActions(model, placements, actions)
|
|
@@ -133,7 +166,12 @@ export function ModelActionToolbar({
|
|
|
133
166
|
<Button
|
|
134
167
|
key={a.key}
|
|
135
168
|
variant={isCreate ? 'default' : 'outline'}
|
|
136
|
-
onClick={() =>
|
|
169
|
+
onClick={() => {
|
|
170
|
+
onActionIntent?.(a)
|
|
171
|
+
setActive(toActionMetadata(a))
|
|
172
|
+
}}
|
|
173
|
+
onMouseEnter={() => onActionIntent?.(a)}
|
|
174
|
+
onFocus={() => onActionIntent?.(a)}
|
|
137
175
|
style={a.color && !isCreate ? { borderColor: a.color, color: a.color } : undefined}
|
|
138
176
|
>
|
|
139
177
|
<DynamicIcon name={a.icon || (isCreate ? 'Plus' : 'Zap')} className="mr-2 h-4 w-4" />
|
|
@@ -142,8 +180,9 @@ export function ModelActionToolbar({
|
|
|
142
180
|
locale bundle loads asynchronously, so translate at render — a
|
|
143
181
|
bare `{a.label}` prints the raw key until (and after) the bundle
|
|
144
182
|
lands because nothing re-derives it. defaultValue keeps an
|
|
145
|
-
already-localized label untouched
|
|
146
|
-
|
|
183
|
+
already-localized label untouched; humanizeLastSegment avoids
|
|
184
|
+
flashing the full dotted key when the bundle is still in flight. */}
|
|
185
|
+
{t(a.label, { defaultValue: humanizeActionLabel(a.label) })}
|
|
147
186
|
</Button>
|
|
148
187
|
)
|
|
149
188
|
})}
|