@xrmforge/devkit 0.7.13 → 0.7.15
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/LICENSE +21 -21
- package/dist/index.js +5 -1
- package/dist/index.js.map +1 -1
- package/dist/templates/AGENT.md +707 -707
- package/dist/templates/azure-pipelines.yml +5 -9
- package/dist/templates/constants.ts +32 -32
- package/dist/templates/error-handler.ts +96 -96
- package/dist/templates/eslint.config.js +21 -21
- package/dist/templates/example-form.test.ts +33 -33
- package/dist/templates/example-form.ts +77 -77
- package/dist/templates/github-actions-ci.yml +5 -9
- package/dist/templates/gitignore +4 -0
- package/dist/templates/logger.ts +67 -67
- package/dist/templates/validate-form.mjs +393 -393
- package/package.json +1 -1
|
@@ -15,16 +15,12 @@ steps:
|
|
|
15
15
|
- script: npm ci
|
|
16
16
|
displayName: 'Install dependencies'
|
|
17
17
|
|
|
18
|
-
# Connection +
|
|
19
|
-
#
|
|
18
|
+
# Connection + credentials come from the XRMFORGE_* environment below; the CLI reads
|
|
19
|
+
# them directly, so the secret never appears as a command-line argument (and stays
|
|
20
|
+
# out of the process list). Entity scope (--entities or --solutions) and --output
|
|
21
|
+
# belong in xrmforge.config.json.
|
|
20
22
|
# See "npx xrmforge generate --help" for all options (--actions, labels, ...).
|
|
21
|
-
- script:
|
|
22
|
-
npx xrmforge generate
|
|
23
|
-
--url "$XRMFORGE_URL"
|
|
24
|
-
--auth client-credentials
|
|
25
|
-
--tenant-id "$XRMFORGE_TENANT_ID"
|
|
26
|
-
--client-id "$XRMFORGE_CLIENT_ID"
|
|
27
|
-
--client-secret "$XRMFORGE_CLIENT_SECRET"
|
|
23
|
+
- script: npx xrmforge generate --auth client-credentials
|
|
28
24
|
displayName: 'Generate types from Dataverse'
|
|
29
25
|
env:
|
|
30
26
|
XRMFORGE_URL: $(XRMFORGE_URL)
|
|
@@ -1,32 +1,32 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Central constants for notifications and messages.
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
/** Unique IDs for form-level notifications. */
|
|
6
|
-
export const NOTIFICATION_IDS = {
|
|
7
|
-
genericError: '{{namespace}}.notification.generic-error'.toLowerCase(),
|
|
8
|
-
} as const;
|
|
9
|
-
|
|
10
|
-
/** Localized message strings (extend as needed). */
|
|
11
|
-
export const MESSAGES = {
|
|
12
|
-
de: {
|
|
13
|
-
unsavedRecord: 'Der Datensatz muss zuerst gespeichert werden.',
|
|
14
|
-
},
|
|
15
|
-
en: {
|
|
16
|
-
unsavedRecord: 'The record must be saved first.',
|
|
17
|
-
},
|
|
18
|
-
} as const;
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* Pick the correct language table based on the user's D365 language setting.
|
|
22
|
-
*
|
|
23
|
-
* @param languageId - LCID from Xrm.Utility.getGlobalContext().userSettings.languageId
|
|
24
|
-
* @param table - Object with 'de' and 'en' keys containing the same message keys
|
|
25
|
-
* @returns The matching language table (defaults to English)
|
|
26
|
-
*/
|
|
27
|
-
export function pickLang<K extends string>(
|
|
28
|
-
languageId: number,
|
|
29
|
-
table: { de: Record<K, string>; en: Record<K, string> },
|
|
30
|
-
): Record<K, string> {
|
|
31
|
-
return languageId === 1031 ? table.de : table.en;
|
|
32
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Central constants for notifications and messages.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/** Unique IDs for form-level notifications. */
|
|
6
|
+
export const NOTIFICATION_IDS = {
|
|
7
|
+
genericError: '{{namespace}}.notification.generic-error'.toLowerCase(),
|
|
8
|
+
} as const;
|
|
9
|
+
|
|
10
|
+
/** Localized message strings (extend as needed). */
|
|
11
|
+
export const MESSAGES = {
|
|
12
|
+
de: {
|
|
13
|
+
unsavedRecord: 'Der Datensatz muss zuerst gespeichert werden.',
|
|
14
|
+
},
|
|
15
|
+
en: {
|
|
16
|
+
unsavedRecord: 'The record must be saved first.',
|
|
17
|
+
},
|
|
18
|
+
} as const;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Pick the correct language table based on the user's D365 language setting.
|
|
22
|
+
*
|
|
23
|
+
* @param languageId - LCID from Xrm.Utility.getGlobalContext().userSettings.languageId
|
|
24
|
+
* @param table - Object with 'de' and 'en' keys containing the same message keys
|
|
25
|
+
* @returns The matching language table (defaults to English)
|
|
26
|
+
*/
|
|
27
|
+
export function pickLang<K extends string>(
|
|
28
|
+
languageId: number,
|
|
29
|
+
table: { de: Record<K, string>; en: Record<K, string> },
|
|
30
|
+
): Record<K, string> {
|
|
31
|
+
return languageId === 1031 ? table.de : table.en;
|
|
32
|
+
}
|
|
@@ -1,96 +1,96 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Unified error handling for D365 form event handlers.
|
|
3
|
-
* Wraps sync and async handlers with try/catch and form notifications.
|
|
4
|
-
*/
|
|
5
|
-
import type { Logger } from './logger.js';
|
|
6
|
-
import { NOTIFICATION_IDS } from './constants.js';
|
|
7
|
-
import { FormNotificationLevel } from '@xrmforge/helpers';
|
|
8
|
-
|
|
9
|
-
type EventHandler = (ctx: Xrm.Events.EventContext, ...args: never[]) => unknown;
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* Wrap a form event handler with error handling.
|
|
13
|
-
*
|
|
14
|
-
* Catches both sync and async errors, logs them, and shows a form notification.
|
|
15
|
-
* The original handler is never rethrown, so form execution continues.
|
|
16
|
-
*
|
|
17
|
-
* @param name - Handler name for logging (e.g. 'MyApp.Account.onLoad')
|
|
18
|
-
* @param logger - Logger instance for error reporting
|
|
19
|
-
* @param handler - The actual event handler function
|
|
20
|
-
*/
|
|
21
|
-
export function wrapHandler(name: string, logger: Logger, handler: EventHandler): EventHandler {
|
|
22
|
-
const wrapped: EventHandler = (ctx, ...args) => {
|
|
23
|
-
try {
|
|
24
|
-
const result = handler(ctx, ...args);
|
|
25
|
-
if (result && typeof (result as Promise<unknown>).then === 'function') {
|
|
26
|
-
return (result as Promise<unknown>).catch((err: unknown) => {
|
|
27
|
-
logAndNotify(ctx, name, logger, err);
|
|
28
|
-
});
|
|
29
|
-
}
|
|
30
|
-
return result;
|
|
31
|
-
} catch (err: unknown) {
|
|
32
|
-
logAndNotify(ctx, name, logger, err);
|
|
33
|
-
}
|
|
34
|
-
};
|
|
35
|
-
return wrapped;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* Wrap a ribbon command handler with error handling.
|
|
40
|
-
*
|
|
41
|
-
* Unlike wrapHandler, this accepts a FormContext directly (not an EventContext),
|
|
42
|
-
* which is the calling convention for ribbon/command bar handlers.
|
|
43
|
-
*
|
|
44
|
-
* @param name - Handler name for logging
|
|
45
|
-
* @param logger - Logger instance for error reporting
|
|
46
|
-
* @param handler - The actual command handler function
|
|
47
|
-
*/
|
|
48
|
-
export function wrapCommand(
|
|
49
|
-
name: string,
|
|
50
|
-
logger: Logger,
|
|
51
|
-
handler: (formContext: Xrm.FormContext, ...args: never[]) => unknown,
|
|
52
|
-
): (formContext: Xrm.FormContext, ...args: never[]) => unknown {
|
|
53
|
-
return (formContext, ...args) => {
|
|
54
|
-
try {
|
|
55
|
-
const result = handler(formContext, ...args);
|
|
56
|
-
if (result && typeof (result as Promise<unknown>).then === 'function') {
|
|
57
|
-
return (result as Promise<unknown>).catch((err: unknown) => {
|
|
58
|
-
logAndNotifyForm(formContext, name, logger, err);
|
|
59
|
-
});
|
|
60
|
-
}
|
|
61
|
-
return result;
|
|
62
|
-
} catch (err: unknown) {
|
|
63
|
-
logAndNotifyForm(formContext, name, logger, err);
|
|
64
|
-
}
|
|
65
|
-
};
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
function logAndNotify(
|
|
69
|
-
ctx: Xrm.Events.EventContext,
|
|
70
|
-
name: string,
|
|
71
|
-
logger: Logger,
|
|
72
|
-
err: unknown,
|
|
73
|
-
): void {
|
|
74
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
75
|
-
logger.error(`${name} failed`, { err });
|
|
76
|
-
try {
|
|
77
|
-
ctx.getFormContext().ui.setFormNotification(message, FormNotificationLevel.Error, NOTIFICATION_IDS.genericError);
|
|
78
|
-
} catch {
|
|
79
|
-
/* ignore */
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
function logAndNotifyForm(
|
|
84
|
-
formContext: Xrm.FormContext,
|
|
85
|
-
name: string,
|
|
86
|
-
logger: Logger,
|
|
87
|
-
err: unknown,
|
|
88
|
-
): void {
|
|
89
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
90
|
-
logger.error(`${name} failed`, { err });
|
|
91
|
-
try {
|
|
92
|
-
formContext.ui.setFormNotification(message, FormNotificationLevel.Error, NOTIFICATION_IDS.genericError);
|
|
93
|
-
} catch {
|
|
94
|
-
/* ignore */
|
|
95
|
-
}
|
|
96
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Unified error handling for D365 form event handlers.
|
|
3
|
+
* Wraps sync and async handlers with try/catch and form notifications.
|
|
4
|
+
*/
|
|
5
|
+
import type { Logger } from './logger.js';
|
|
6
|
+
import { NOTIFICATION_IDS } from './constants.js';
|
|
7
|
+
import { FormNotificationLevel } from '@xrmforge/helpers';
|
|
8
|
+
|
|
9
|
+
type EventHandler = (ctx: Xrm.Events.EventContext, ...args: never[]) => unknown;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Wrap a form event handler with error handling.
|
|
13
|
+
*
|
|
14
|
+
* Catches both sync and async errors, logs them, and shows a form notification.
|
|
15
|
+
* The original handler is never rethrown, so form execution continues.
|
|
16
|
+
*
|
|
17
|
+
* @param name - Handler name for logging (e.g. 'MyApp.Account.onLoad')
|
|
18
|
+
* @param logger - Logger instance for error reporting
|
|
19
|
+
* @param handler - The actual event handler function
|
|
20
|
+
*/
|
|
21
|
+
export function wrapHandler(name: string, logger: Logger, handler: EventHandler): EventHandler {
|
|
22
|
+
const wrapped: EventHandler = (ctx, ...args) => {
|
|
23
|
+
try {
|
|
24
|
+
const result = handler(ctx, ...args);
|
|
25
|
+
if (result && typeof (result as Promise<unknown>).then === 'function') {
|
|
26
|
+
return (result as Promise<unknown>).catch((err: unknown) => {
|
|
27
|
+
logAndNotify(ctx, name, logger, err);
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
return result;
|
|
31
|
+
} catch (err: unknown) {
|
|
32
|
+
logAndNotify(ctx, name, logger, err);
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
return wrapped;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Wrap a ribbon command handler with error handling.
|
|
40
|
+
*
|
|
41
|
+
* Unlike wrapHandler, this accepts a FormContext directly (not an EventContext),
|
|
42
|
+
* which is the calling convention for ribbon/command bar handlers.
|
|
43
|
+
*
|
|
44
|
+
* @param name - Handler name for logging
|
|
45
|
+
* @param logger - Logger instance for error reporting
|
|
46
|
+
* @param handler - The actual command handler function
|
|
47
|
+
*/
|
|
48
|
+
export function wrapCommand(
|
|
49
|
+
name: string,
|
|
50
|
+
logger: Logger,
|
|
51
|
+
handler: (formContext: Xrm.FormContext, ...args: never[]) => unknown,
|
|
52
|
+
): (formContext: Xrm.FormContext, ...args: never[]) => unknown {
|
|
53
|
+
return (formContext, ...args) => {
|
|
54
|
+
try {
|
|
55
|
+
const result = handler(formContext, ...args);
|
|
56
|
+
if (result && typeof (result as Promise<unknown>).then === 'function') {
|
|
57
|
+
return (result as Promise<unknown>).catch((err: unknown) => {
|
|
58
|
+
logAndNotifyForm(formContext, name, logger, err);
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
return result;
|
|
62
|
+
} catch (err: unknown) {
|
|
63
|
+
logAndNotifyForm(formContext, name, logger, err);
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function logAndNotify(
|
|
69
|
+
ctx: Xrm.Events.EventContext,
|
|
70
|
+
name: string,
|
|
71
|
+
logger: Logger,
|
|
72
|
+
err: unknown,
|
|
73
|
+
): void {
|
|
74
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
75
|
+
logger.error(`${name} failed`, { err });
|
|
76
|
+
try {
|
|
77
|
+
ctx.getFormContext().ui.setFormNotification(message, FormNotificationLevel.Error, NOTIFICATION_IDS.genericError);
|
|
78
|
+
} catch {
|
|
79
|
+
/* ignore */
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function logAndNotifyForm(
|
|
84
|
+
formContext: Xrm.FormContext,
|
|
85
|
+
name: string,
|
|
86
|
+
logger: Logger,
|
|
87
|
+
err: unknown,
|
|
88
|
+
): void {
|
|
89
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
90
|
+
logger.error(`${name} failed`, { err });
|
|
91
|
+
try {
|
|
92
|
+
formContext.ui.setFormNotification(message, FormNotificationLevel.Error, NOTIFICATION_IDS.genericError);
|
|
93
|
+
} catch {
|
|
94
|
+
/* ignore */
|
|
95
|
+
}
|
|
96
|
+
}
|
|
@@ -1,21 +1,21 @@
|
|
|
1
|
-
import tseslint from '@typescript-eslint/eslint-plugin';
|
|
2
|
-
import tsparser from '@typescript-eslint/parser';
|
|
3
|
-
import xrmforge from '@xrmforge/eslint-plugin';
|
|
4
|
-
|
|
5
|
-
export default [
|
|
6
|
-
{
|
|
7
|
-
files: ['src/**/*.ts'],
|
|
8
|
-
languageOptions: {
|
|
9
|
-
parser: tsparser,
|
|
10
|
-
parserOptions: { ecmaVersion: 'latest', sourceType: 'module' },
|
|
11
|
-
},
|
|
12
|
-
plugins: { '@typescript-eslint': tseslint },
|
|
13
|
-
rules: {
|
|
14
|
-
'@typescript-eslint/no-explicit-any': 'error',
|
|
15
|
-
'@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
|
|
16
|
-
},
|
|
17
|
-
},
|
|
18
|
-
xrmforge.configs.recommended,
|
|
19
|
-
{ rules: { 'no-console': ['error'] } },
|
|
20
|
-
{ files: ['src/shared/logger.ts'], rules: { 'no-console': 'off' } },
|
|
21
|
-
];
|
|
1
|
+
import tseslint from '@typescript-eslint/eslint-plugin';
|
|
2
|
+
import tsparser from '@typescript-eslint/parser';
|
|
3
|
+
import xrmforge from '@xrmforge/eslint-plugin';
|
|
4
|
+
|
|
5
|
+
export default [
|
|
6
|
+
{
|
|
7
|
+
files: ['src/**/*.ts'],
|
|
8
|
+
languageOptions: {
|
|
9
|
+
parser: tsparser,
|
|
10
|
+
parserOptions: { ecmaVersion: 'latest', sourceType: 'module' },
|
|
11
|
+
},
|
|
12
|
+
plugins: { '@typescript-eslint': tseslint },
|
|
13
|
+
rules: {
|
|
14
|
+
'@typescript-eslint/no-explicit-any': 'error',
|
|
15
|
+
'@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
xrmforge.configs.recommended,
|
|
19
|
+
{ rules: { 'no-console': ['error'] } },
|
|
20
|
+
{ files: ['src/shared/logger.ts'], rules: { 'no-console': 'off' } },
|
|
21
|
+
];
|
|
@@ -1,33 +1,33 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Example test for the form script.
|
|
5
|
-
*
|
|
6
|
-
* Uses @xrmforge/testing for type-safe mocking once you have
|
|
7
|
-
* generated types. Replace with real form tests after 'xrmforge generate'.
|
|
8
|
-
*/
|
|
9
|
-
describe('{{namespace}}.Example', () => {
|
|
10
|
-
it('should export onLoad handler', async () => {
|
|
11
|
-
const mod = await import('../../src/forms/example-form.js');
|
|
12
|
-
expect(typeof mod.onLoad).toBe('function');
|
|
13
|
-
});
|
|
14
|
-
|
|
15
|
-
it('should export onSave handler', async () => {
|
|
16
|
-
const mod = await import('../../src/forms/example-form.js');
|
|
17
|
-
expect(typeof mod.onSave).toBe('function');
|
|
18
|
-
});
|
|
19
|
-
|
|
20
|
-
// TODO: After running 'xrmforge generate', add real form tests.
|
|
21
|
-
// createFormMock<TForm>() takes the generated FORM interface and returns a
|
|
22
|
-
// FormMock: use mock.asEventContext() for the handler and mock.ui /
|
|
23
|
-
// mock.getControl() for assertions (plain mocks, not vi.fn() spies).
|
|
24
|
-
//
|
|
25
|
-
// import { createFormMock } from '@xrmforge/testing';
|
|
26
|
-
// import type { ExampleForm } from '../../generated/forms/example.js';
|
|
27
|
-
//
|
|
28
|
-
// it('should show notification on load', () => {
|
|
29
|
-
// const mock = createFormMock<ExampleForm>({ name: 'Contoso Ltd' });
|
|
30
|
-
// onLoad(mock.asEventContext());
|
|
31
|
-
// expect(mock.ui.getFormNotification('example-load')?.message).toBe('Form loaded');
|
|
32
|
-
// });
|
|
33
|
-
});
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Example test for the form script.
|
|
5
|
+
*
|
|
6
|
+
* Uses @xrmforge/testing for type-safe mocking once you have
|
|
7
|
+
* generated types. Replace with real form tests after 'xrmforge generate'.
|
|
8
|
+
*/
|
|
9
|
+
describe('{{namespace}}.Example', () => {
|
|
10
|
+
it('should export onLoad handler', async () => {
|
|
11
|
+
const mod = await import('../../src/forms/example-form.js');
|
|
12
|
+
expect(typeof mod.onLoad).toBe('function');
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it('should export onSave handler', async () => {
|
|
16
|
+
const mod = await import('../../src/forms/example-form.js');
|
|
17
|
+
expect(typeof mod.onSave).toBe('function');
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
// TODO: After running 'xrmforge generate', add real form tests.
|
|
21
|
+
// createFormMock<TForm>() takes the generated FORM interface and returns a
|
|
22
|
+
// FormMock: use mock.asEventContext() for the handler and mock.ui /
|
|
23
|
+
// mock.getControl() for assertions (plain mocks, not vi.fn() spies).
|
|
24
|
+
//
|
|
25
|
+
// import { createFormMock } from '@xrmforge/testing';
|
|
26
|
+
// import type { ExampleForm } from '../../generated/forms/example.js';
|
|
27
|
+
//
|
|
28
|
+
// it('should show notification on load', () => {
|
|
29
|
+
// const mock = createFormMock<ExampleForm>({ name: 'Contoso Ltd' });
|
|
30
|
+
// onLoad(mock.asEventContext());
|
|
31
|
+
// expect(mock.ui.getFormNotification('example-load')?.message).toBe('Form loaded');
|
|
32
|
+
// });
|
|
33
|
+
});
|
|
@@ -1,77 +1,77 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Example Form Script for Dynamics 365 using XrmForge best practices.
|
|
3
|
-
*
|
|
4
|
-
* Register in D365 as: {{namespace}}.Example.onLoad
|
|
5
|
-
*
|
|
6
|
-
* This template demonstrates the correct patterns:
|
|
7
|
-
* - typedForm() for direct typed field access
|
|
8
|
-
* - Fields Enum for addOnChange via $context, controls proxy for control access
|
|
9
|
-
* - Entity-level Fields for Web API select() queries
|
|
10
|
-
* - wrapHandler for unified error handling
|
|
11
|
-
* - Logger instead of console.log
|
|
12
|
-
* - FormNotificationLevel constant instead of raw string
|
|
13
|
-
* - pickLang() for localized UI strings
|
|
14
|
-
*
|
|
15
|
-
* Replace this file with your actual form logic after running 'xrmforge generate'.
|
|
16
|
-
*/
|
|
17
|
-
import { createLogger } from '../shared/logger.js';
|
|
18
|
-
import { wrapHandler } from '../shared/error-handler.js';
|
|
19
|
-
import { MESSAGES, pickLang } from '../shared/constants.js';
|
|
20
|
-
// For Web API queries and lookups also import: select, formLookupId
|
|
21
|
-
import { typedForm, FormNotificationLevel } from '@xrmforge/helpers';
|
|
22
|
-
// TODO: After 'xrmforge generate', replace with your actual imports:
|
|
23
|
-
// import type { ExampleFormTypeInfo } from '../../generated/forms/example.js';
|
|
24
|
-
// import { ExampleFormFieldsEnum as Fields } from '../../generated/forms/example.js';
|
|
25
|
-
// import { ExampleFields } from '../../generated/fields/example.js';
|
|
26
|
-
// import { EntityNames } from '../../generated/entity-names.js';
|
|
27
|
-
|
|
28
|
-
const logger = createLogger('{{namespace}}.Example');
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* Called when the form loads.
|
|
32
|
-
*/
|
|
33
|
-
export const onLoad = wrapHandler('{{namespace}}.Example.onLoad', logger, async (ctx) => {
|
|
34
|
-
// TODO: Replace 'Xrm.FormContext' with your generated <Form>TypeInfo type:
|
|
35
|
-
// const form = typedForm<ExampleFormTypeInfo>(ctx.getFormContext());
|
|
36
|
-
const form = typedForm<Xrm.FormContext>(ctx.getFormContext());
|
|
37
|
-
|
|
38
|
-
// Direct field access via typedForm proxy (fully typed):
|
|
39
|
-
// const name = form.name.getValue(); // string | null
|
|
40
|
-
// form.revenue.setValue(150000); // NumberAttribute
|
|
41
|
-
|
|
42
|
-
// addOnChange uses Fields Enum via $context:
|
|
43
|
-
// form.$context.getAttribute(Fields.Name).addOnChange(() => {
|
|
44
|
-
// logger.debug('Name changed', { value: form.name.getValue() });
|
|
45
|
-
// });
|
|
46
|
-
|
|
47
|
-
// Web API queries use entity-level Fields:
|
|
48
|
-
// const result = await Xrm.WebApi.retrieveRecord(
|
|
49
|
-
// EntityNames.Account, id,
|
|
50
|
-
// select(ExampleFields.Name, ExampleFields.WebsiteUrl)
|
|
51
|
-
// );
|
|
52
|
-
|
|
53
|
-
// Lookup access:
|
|
54
|
-
// const parentId = formLookupId(form.parentaccountid);
|
|
55
|
-
|
|
56
|
-
// Localized UI strings:
|
|
57
|
-
const lang = pickLang(
|
|
58
|
-
Xrm.Utility.getGlobalContext().userSettings.languageId,
|
|
59
|
-
MESSAGES,
|
|
60
|
-
);
|
|
61
|
-
logger.info('Form loaded', { language: lang });
|
|
62
|
-
|
|
63
|
-
// Form notifications use constants:
|
|
64
|
-
form.$context.ui.setFormNotification(
|
|
65
|
-
'Form loaded',
|
|
66
|
-
FormNotificationLevel.Info,
|
|
67
|
-
'example-load',
|
|
68
|
-
);
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
/**
|
|
72
|
-
* Called when the form is saved.
|
|
73
|
-
*/
|
|
74
|
-
export const onSave = wrapHandler('{{namespace}}.Example.onSave', logger, (ctx) => {
|
|
75
|
-
const form = typedForm<Xrm.FormContext>(ctx.getFormContext());
|
|
76
|
-
form.$context.ui.clearFormNotification('example-load');
|
|
77
|
-
});
|
|
1
|
+
/**
|
|
2
|
+
* Example Form Script for Dynamics 365 using XrmForge best practices.
|
|
3
|
+
*
|
|
4
|
+
* Register in D365 as: {{namespace}}.Example.onLoad
|
|
5
|
+
*
|
|
6
|
+
* This template demonstrates the correct patterns:
|
|
7
|
+
* - typedForm() for direct typed field access
|
|
8
|
+
* - Fields Enum for addOnChange via $context, controls proxy for control access
|
|
9
|
+
* - Entity-level Fields for Web API select() queries
|
|
10
|
+
* - wrapHandler for unified error handling
|
|
11
|
+
* - Logger instead of console.log
|
|
12
|
+
* - FormNotificationLevel constant instead of raw string
|
|
13
|
+
* - pickLang() for localized UI strings
|
|
14
|
+
*
|
|
15
|
+
* Replace this file with your actual form logic after running 'xrmforge generate'.
|
|
16
|
+
*/
|
|
17
|
+
import { createLogger } from '../shared/logger.js';
|
|
18
|
+
import { wrapHandler } from '../shared/error-handler.js';
|
|
19
|
+
import { MESSAGES, pickLang } from '../shared/constants.js';
|
|
20
|
+
// For Web API queries and lookups also import: select, formLookupId
|
|
21
|
+
import { typedForm, FormNotificationLevel } from '@xrmforge/helpers';
|
|
22
|
+
// TODO: After 'xrmforge generate', replace with your actual imports:
|
|
23
|
+
// import type { ExampleFormTypeInfo } from '../../generated/forms/example.js';
|
|
24
|
+
// import { ExampleFormFieldsEnum as Fields } from '../../generated/forms/example.js';
|
|
25
|
+
// import { ExampleFields } from '../../generated/fields/example.js';
|
|
26
|
+
// import { EntityNames } from '../../generated/entity-names.js';
|
|
27
|
+
|
|
28
|
+
const logger = createLogger('{{namespace}}.Example');
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Called when the form loads.
|
|
32
|
+
*/
|
|
33
|
+
export const onLoad = wrapHandler('{{namespace}}.Example.onLoad', logger, async (ctx) => {
|
|
34
|
+
// TODO: Replace 'Xrm.FormContext' with your generated <Form>TypeInfo type:
|
|
35
|
+
// const form = typedForm<ExampleFormTypeInfo>(ctx.getFormContext());
|
|
36
|
+
const form = typedForm<Xrm.FormContext>(ctx.getFormContext());
|
|
37
|
+
|
|
38
|
+
// Direct field access via typedForm proxy (fully typed):
|
|
39
|
+
// const name = form.name.getValue(); // string | null
|
|
40
|
+
// form.revenue.setValue(150000); // NumberAttribute
|
|
41
|
+
|
|
42
|
+
// addOnChange uses Fields Enum via $context:
|
|
43
|
+
// form.$context.getAttribute(Fields.Name).addOnChange(() => {
|
|
44
|
+
// logger.debug('Name changed', { value: form.name.getValue() });
|
|
45
|
+
// });
|
|
46
|
+
|
|
47
|
+
// Web API queries use entity-level Fields:
|
|
48
|
+
// const result = await Xrm.WebApi.retrieveRecord(
|
|
49
|
+
// EntityNames.Account, id,
|
|
50
|
+
// select(ExampleFields.Name, ExampleFields.WebsiteUrl)
|
|
51
|
+
// );
|
|
52
|
+
|
|
53
|
+
// Lookup access:
|
|
54
|
+
// const parentId = formLookupId(form.parentaccountid);
|
|
55
|
+
|
|
56
|
+
// Localized UI strings:
|
|
57
|
+
const lang = pickLang(
|
|
58
|
+
Xrm.Utility.getGlobalContext().userSettings.languageId,
|
|
59
|
+
MESSAGES,
|
|
60
|
+
);
|
|
61
|
+
logger.info('Form loaded', { language: lang });
|
|
62
|
+
|
|
63
|
+
// Form notifications use constants:
|
|
64
|
+
form.$context.ui.setFormNotification(
|
|
65
|
+
'Form loaded',
|
|
66
|
+
FormNotificationLevel.Info,
|
|
67
|
+
'example-load',
|
|
68
|
+
);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Called when the form is saved.
|
|
73
|
+
*/
|
|
74
|
+
export const onSave = wrapHandler('{{namespace}}.Example.onSave', logger, (ctx) => {
|
|
75
|
+
const form = typedForm<Xrm.FormContext>(ctx.getFormContext());
|
|
76
|
+
form.$context.ui.clearFormNotification('example-load');
|
|
77
|
+
});
|
|
@@ -20,16 +20,12 @@ jobs:
|
|
|
20
20
|
- run: npm ci
|
|
21
21
|
|
|
22
22
|
- name: Generate types from Dataverse
|
|
23
|
-
# Connection +
|
|
24
|
-
#
|
|
23
|
+
# Connection + credentials come from the XRMFORGE_* environment below; the CLI
|
|
24
|
+
# reads them directly, so the secret never appears as a command-line argument
|
|
25
|
+
# (and stays out of the process list). Entity scope (--entities or --solutions)
|
|
26
|
+
# and --output belong in xrmforge.config.json.
|
|
25
27
|
# See "npx xrmforge generate --help" for all options (--actions, labels, ...).
|
|
26
|
-
run:
|
|
27
|
-
npx xrmforge generate
|
|
28
|
-
--url "$XRMFORGE_URL"
|
|
29
|
-
--auth client-credentials
|
|
30
|
-
--tenant-id "$XRMFORGE_TENANT_ID"
|
|
31
|
-
--client-id "$XRMFORGE_CLIENT_ID"
|
|
32
|
-
--client-secret "$XRMFORGE_CLIENT_SECRET"
|
|
28
|
+
run: npx xrmforge generate --auth client-credentials
|
|
33
29
|
env:
|
|
34
30
|
XRMFORGE_URL: ${{ secrets.XRMFORGE_URL }}
|
|
35
31
|
XRMFORGE_TENANT_ID: ${{ secrets.XRMFORGE_TENANT_ID }}
|