@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 CHANGED
@@ -1,5 +1,11 @@
1
1
  # @asteby/metacore-runtime-react
2
2
 
3
+ ## 37.0.2
4
+
5
+ ### Patch Changes
6
+
7
+ - 653afe4: Never flash raw dotted i18n keys in ModelActionToolbar: humanizeActionLabel fallback + onActionIntent for Module Federation prefetch.
8
+
3
9
  ## 37.0.1
4
10
 
5
11
  ### Patch Changes
@@ -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;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"}
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: () => 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));
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.1",
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-sdk": "3.7.0",
71
- "@asteby/metacore-ui": "2.17.3"
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 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.
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.getByText('integration_github.action.create_issue.label'),
64
- ).toBeTruthy()
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('integration_github.action.create_issue.label'),
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={() => setActive(toActionMetadata(a))}
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
- {t(a.label, { defaultValue: a.label })}
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
  })}