@xrmforge/devkit 0.7.29 → 0.7.30

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.
@@ -305,6 +305,13 @@ export const onLoad = wrapHandler('Namespace.Entity.onLoad', logger, async (ctx)
305
305
  });
306
306
  ```
307
307
 
308
+ Ribbon/command bar commands use `wrapCommand` (it receives a FormContext, not an
309
+ EventContext). Pass extra command parameters through its `TArgs` type parameter
310
+ instead of widening to `any`. A command registered on a **subgrid** receives a
311
+ `GridControl` as PrimaryControl (which has no form `ui`), so wrap it with
312
+ `wrapGridCommand` - its error surface is an app-level banner, and its default extra
313
+ argument is the selected record ids (`string[]`).
314
+
308
315
  ### 8. Custom API Executors from generated/actions/
309
316
 
310
317
  Never build your own ExecuteFunctionCall wrapper. Use the generated executors.
@@ -515,8 +522,13 @@ export function createLogger(namespace: string): Logger;
515
522
  ### error-handler.ts
516
523
  ```typescript
517
524
  export function wrapHandler(name: string, logger: Logger, handler: EventHandler): EventHandler;
518
- export function wrapCommand(name: string, logger: Logger, handler: CommandHandler): CommandHandler;
519
- // Catches sync+async errors, shows form notification via FormNotificationLevel.Error
525
+ // Ribbon/command bar command on a form. Pass extra command parameters through TArgs (default none).
526
+ export function wrapCommand<TArgs extends unknown[] = []>(name, logger, handler): (formContext, ...args) => unknown;
527
+ // Command registered on a SUBGRID: PrimaryControl may be a GridControl (no form ui).
528
+ // Default extra arg is the selected record ids (string[]).
529
+ export function wrapGridCommand<TArgs extends unknown[] = [string[]]>(name, logger, handler): (primaryControl, ...args) => unknown;
530
+ // Catches sync+async errors. wrapHandler/wrapCommand show a form notification via
531
+ // FormNotificationLevel.Error; wrapGridCommand uses an app-level banner (GridControl has no form ui).
520
532
  ```
521
533
 
522
534
  ### constants.ts
@@ -850,7 +862,7 @@ regenerate and commit.
850
862
  ```
851
863
  src/forms/{entity}-form.ts - Form scripts (one per entity)
852
864
  src/shared/logger.ts - Structured logger (only file with console.*)
853
- src/shared/error-handler.ts - wrapHandler + wrapCommand
865
+ src/shared/error-handler.ts - wrapHandler + wrapCommand + wrapGridCommand
854
866
  src/shared/constants.ts - NOTIFICATION_IDS, MESSAGES, pickLang
855
867
  generated/ - Generated types (do not edit manually)
856
868
  tests/forms/{entity}.test.ts - Tests
@@ -1,10 +1,11 @@
1
1
  /**
2
- * Unified error handling for D365 form event handlers.
3
- * Wraps sync and async handlers with try/catch and form notifications.
2
+ * Unified error handling for D365 form event handlers and ribbon commands.
3
+ * Wraps sync and async handlers with try/catch: form handlers and form commands
4
+ * show a form notification, subgrid commands an app-level notification banner.
4
5
  */
5
6
  import type { Logger } from './logger.js';
6
7
  import { NOTIFICATION_IDS } from './constants.js';
7
- import { FormNotificationLevel } from '@xrmforge/helpers';
8
+ import { FormNotificationLevel, AppNotificationLevel, addAppNotification } from '@xrmforge/helpers';
8
9
 
9
10
  type EventHandler = (ctx: Xrm.Events.EventContext, ...args: never[]) => unknown;
10
11
 
@@ -36,20 +37,28 @@ export function wrapHandler(name: string, logger: Logger, handler: EventHandler)
36
37
  }
37
38
 
38
39
  /**
39
- * Wrap a ribbon command handler with error handling.
40
+ * Wrap a ribbon command handler (form context) with error handling.
40
41
  *
41
42
  * Unlike wrapHandler, this accepts a FormContext directly (not an EventContext),
42
43
  * which is the calling convention for ribbon/command bar handlers.
43
44
  *
45
+ * Pass extra ribbon command parameters via the TArgs type parameter so they stay
46
+ * typed end to end (e.g. `wrapCommand<[boolean]>(...)` for a handler that takes a
47
+ * flag after the form context). TArgs defaults to `[]` (no extra parameters).
48
+ *
49
+ * For commands registered on a subgrid (the PrimaryControl may be a GridControl,
50
+ * not a FormContext) use {@link wrapGridCommand} instead.
51
+ *
52
+ * @typeParam TArgs - Tuple of extra parameters passed after the form context
44
53
  * @param name - Handler name for logging
45
54
  * @param logger - Logger instance for error reporting
46
55
  * @param handler - The actual command handler function
47
56
  */
48
- export function wrapCommand(
57
+ export function wrapCommand<TArgs extends unknown[] = []>(
49
58
  name: string,
50
59
  logger: Logger,
51
- handler: (formContext: Xrm.FormContext, ...args: never[]) => unknown,
52
- ): (formContext: Xrm.FormContext, ...args: never[]) => unknown {
60
+ handler: (formContext: Xrm.FormContext, ...args: TArgs) => unknown,
61
+ ): (formContext: Xrm.FormContext, ...args: TArgs) => unknown {
53
62
  return (formContext, ...args) => {
54
63
  try {
55
64
  const result = handler(formContext, ...args);
@@ -65,6 +74,43 @@ export function wrapCommand(
65
74
  };
66
75
  }
67
76
 
77
+ /**
78
+ * Wrap a ribbon command handler whose PrimaryControl can be a subgrid.
79
+ *
80
+ * Commands registered on a subgrid (or on both a form and a subgrid) receive a
81
+ * `GridControl` as the first argument when fired from the grid, plus the selected
82
+ * record ids. A `GridControl` has no form `ui`, so a form notification cannot be
83
+ * shown - this variant reports errors via the logger and an app-level banner
84
+ * ({@link addAppNotification}) that works independently of the form context.
85
+ *
86
+ * TArgs defaults to `[string[]]` (the selected record ids that D365 passes as the
87
+ * SelectedControlSelectedItemIds command parameter).
88
+ *
89
+ * @typeParam TArgs - Tuple of extra parameters passed after the primary control
90
+ * @param name - Handler name for logging
91
+ * @param logger - Logger instance for error reporting
92
+ * @param handler - The actual command handler function
93
+ */
94
+ export function wrapGridCommand<TArgs extends unknown[] = [string[]]>(
95
+ name: string,
96
+ logger: Logger,
97
+ handler: (primaryControl: Xrm.FormContext | Xrm.Controls.GridControl, ...args: TArgs) => unknown,
98
+ ): (primaryControl: Xrm.FormContext | Xrm.Controls.GridControl, ...args: TArgs) => unknown {
99
+ return (primaryControl, ...args) => {
100
+ try {
101
+ const result = handler(primaryControl, ...args);
102
+ if (result && typeof (result as Promise<unknown>).then === 'function') {
103
+ return (result as Promise<unknown>).catch((err: unknown) => {
104
+ logAndNotifyApp(name, logger, err);
105
+ });
106
+ }
107
+ return result;
108
+ } catch (err: unknown) {
109
+ logAndNotifyApp(name, logger, err);
110
+ }
111
+ };
112
+ }
113
+
68
114
  function logAndNotify(
69
115
  ctx: Xrm.Events.EventContext,
70
116
  name: string,
@@ -94,3 +140,19 @@ function logAndNotifyForm(
94
140
  /* ignore */
95
141
  }
96
142
  }
143
+
144
+ /**
145
+ * Log an error and surface it as an app-level (global) notification banner.
146
+ *
147
+ * Used by {@link wrapGridCommand}: the PrimaryControl may be a GridControl that
148
+ * has no form `ui`, so a form notification is not available. The app banner is
149
+ * shown regardless of the calling control. The async banner call is
150
+ * fire-and-forget so the command handler is not forced to be async.
151
+ */
152
+ function logAndNotifyApp(name: string, logger: Logger, err: unknown): void {
153
+ const message = err instanceof Error ? err.message : String(err);
154
+ logger.error(`${name} failed`, { err });
155
+ void addAppNotification(message, AppNotificationLevel.Error).catch(() => {
156
+ /* ignore */
157
+ });
158
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xrmforge/devkit",
3
- "version": "0.7.29",
3
+ "version": "0.7.30",
4
4
  "description": "Build orchestration and project tooling for Dynamics 365 WebResources",
5
5
  "keywords": [
6
6
  "dynamics-365",