@khester/create-dynamics-app 1.1.0 → 2.1.0
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/README.md +74 -0
- package/dist/artifacts/registry.d.ts +18 -0
- package/dist/artifacts/registry.d.ts.map +1 -0
- package/dist/artifacts/registry.js +340 -0
- package/dist/artifacts/registry.js.map +1 -0
- package/dist/artifacts/types.d.ts +122 -0
- package/dist/artifacts/types.d.ts.map +1 -0
- package/dist/artifacts/types.js +7 -0
- package/dist/artifacts/types.js.map +1 -0
- package/dist/artifacts/validators.d.ts +16 -0
- package/dist/artifacts/validators.d.ts.map +1 -0
- package/dist/artifacts/validators.js +45 -0
- package/dist/artifacts/validators.js.map +1 -0
- package/dist/fromDesign.d.ts +5 -0
- package/dist/fromDesign.d.ts.map +1 -0
- package/dist/fromDesign.js +98 -0
- package/dist/fromDesign.js.map +1 -0
- package/dist/index.js +129 -177
- package/dist/index.js.map +1 -1
- package/dist/injectDevTools.d.ts +28 -0
- package/dist/injectDevTools.d.ts.map +1 -0
- package/dist/injectDevTools.js +148 -0
- package/dist/injectDevTools.js.map +1 -0
- package/dist/scaffold.d.ts +48 -0
- package/dist/scaffold.d.ts.map +1 -0
- package/dist/scaffold.js +180 -0
- package/dist/scaffold.js.map +1 -0
- package/dist/templatePlan.d.ts +3 -0
- package/dist/templatePlan.d.ts.map +1 -0
- package/dist/templatePlan.js +43 -0
- package/dist/templatePlan.js.map +1 -0
- package/dist/utils/copyTemplate.d.ts +13 -1
- package/dist/utils/copyTemplate.d.ts.map +1 -1
- package/dist/utils/copyTemplate.js +98 -4
- package/dist/utils/copyTemplate.js.map +1 -1
- package/dist/utils/updatePackageJson.d.ts +11 -1
- package/dist/utils/updatePackageJson.d.ts.map +1 -1
- package/dist/utils/updatePackageJson.js +12 -10
- package/dist/utils/updatePackageJson.js.map +1 -1
- package/package.json +10 -7
- package/templates/_shared/dev-tools/auth/get-token.js +72 -0
- package/templates/_shared/dev-tools/dev/mock-xrm.js +42 -0
- package/templates/_shared/dev-tools/metadata-sync/index.js +152 -0
- package/templates/_shared/dev-tools/smoke/test-retrieve.js +44 -0
- package/templates/dialog-form/README.md +27 -0
- package/templates/dialog-form/_variants/App.v8.tsx +39 -0
- package/templates/dialog-form/_variants/App.v9.tsx +41 -0
- package/templates/dialog-form/gitignore +5 -0
- package/templates/dialog-form/package.json +27 -0
- package/templates/dialog-form/public/index.html +11 -0
- package/templates/dialog-form/src/index.tsx +10 -0
- package/templates/dialog-form/src/services/dataverse.ts +30 -0
- package/templates/dialog-form/tsconfig.json +15 -0
- package/templates/dialog-form/webpack.config.js +17 -0
- package/templates/grid-customizer/README.md +28 -0
- package/templates/grid-customizer/gitignore +4 -0
- package/templates/grid-customizer/package.json +25 -0
- package/templates/grid-customizer/src/GridCustomizer.ts +28 -0
- package/templates/grid-customizer/src/cell-renderers.tsx +35 -0
- package/templates/grid-customizer/src/index.ts +4 -0
- package/templates/grid-customizer/src/types/grid-types.ts +30 -0
- package/templates/grid-customizer/src/utils/color-utils.ts +24 -0
- package/templates/grid-customizer/tsconfig.json +15 -0
- package/templates/grid-customizer/webpack.config.js +17 -0
- package/templates/pcf-dataset/ControlManifest.Input.xml +16 -0
- package/templates/pcf-dataset/README.md +21 -0
- package/templates/pcf-dataset/gitignore +5 -0
- package/templates/pcf-dataset/index.ts +39 -0
- package/templates/pcf-dataset/package.json +30 -0
- package/templates/pcf-dataset/strings/{{componentName}}.1033.resx +47 -0
- package/templates/pcf-dataset/tsconfig.json +8 -0
- package/templates/pcf-dataset/{{componentName}}Component.tsx +39 -0
- package/templates/pcf-field/ControlManifest.Input.xml +17 -0
- package/templates/pcf-field/README.md +95 -0
- package/templates/pcf-field/_variants/ValueInput.boolean.tsx +24 -0
- package/templates/pcf-field/_variants/ValueInput.date.tsx +27 -0
- package/templates/pcf-field/_variants/ValueInput.number.tsx +35 -0
- package/templates/pcf-field/_variants/ValueInput.text.tsx +27 -0
- package/templates/pcf-field/gitignore +5 -0
- package/templates/pcf-field/index.ts +61 -0
- package/templates/pcf-field/package.json +30 -0
- package/templates/pcf-field/strings/{{componentName}}.1033.resx +47 -0
- package/templates/pcf-field/tsconfig.json +8 -0
- package/templates/pcf-field/{{componentName}}Component.tsx +35 -0
- package/templates/power-pages-starter/gitignore +5 -0
- package/templates/react-custom-page/gitignore +5 -0
- package/templates/{dynamics-365-starter → react-custom-page}/package.json +3 -3
- package/templates/react-custom-page/tools/metadata-sync/index.js +152 -0
- package/templates/static-web-app/README.md +36 -0
- package/templates/static-web-app/_variants/App.v8.tsx +32 -0
- package/templates/static-web-app/_variants/App.v9.tsx +31 -0
- package/templates/static-web-app/api/host.json +12 -0
- package/templates/static-web-app/api/package.json +19 -0
- package/templates/static-web-app/api/src/functions/hello.ts +16 -0
- package/templates/static-web-app/api/tsconfig.json +14 -0
- package/templates/static-web-app/frontend/index.html +12 -0
- package/templates/static-web-app/frontend/package.json +23 -0
- package/templates/static-web-app/frontend/src/index.tsx +8 -0
- package/templates/static-web-app/frontend/tsconfig.json +16 -0
- package/templates/static-web-app/frontend/vite.config.ts +13 -0
- package/templates/static-web-app/gitignore +8 -0
- package/templates/static-web-app/package.json +15 -0
- package/templates/static-web-app/staticwebapp.config.json +7 -0
- package/templates/teams-app/README.md +27 -0
- package/templates/teams-app/_variants/graph.off.ts +7 -0
- package/templates/teams-app/_variants/graph.on.ts +22 -0
- package/templates/teams-app/appPackage/manifest.json +26 -0
- package/templates/teams-app/gitignore +5 -0
- package/templates/teams-app/index.html +12 -0
- package/templates/teams-app/package.json +26 -0
- package/templates/teams-app/src/App.tsx +25 -0
- package/templates/teams-app/src/index.tsx +8 -0
- package/templates/teams-app/tsconfig.json +16 -0
- package/templates/teams-app/vite.config.ts +9 -0
- package/templates/web-resource/README.md +39 -0
- package/templates/web-resource/_variants/App.v8.tsx +29 -0
- package/templates/web-resource/_variants/App.v9.tsx +28 -0
- package/templates/web-resource/gitignore +5 -0
- package/templates/web-resource/package.json +27 -0
- package/templates/web-resource/public/index.html +11 -0
- package/templates/web-resource/src/index.tsx +10 -0
- package/templates/web-resource/src/services/dataverse.ts +30 -0
- package/templates/web-resource/tsconfig.json +15 -0
- package/templates/web-resource/webpack.config.js +17 -0
- package/dist/utils/consultingHelpers.d.ts +0 -13
- package/dist/utils/consultingHelpers.d.ts.map +0 -1
- package/dist/utils/consultingHelpers.js +0 -569
- package/dist/utils/consultingHelpers.js.map +0 -1
- package/templates/dynamics-365-starter/INTEGRATION_TEST_RESULTS.md +0 -302
- package/templates/dynamics-365-starter/PHASE_4_COMPLETION_SUMMARY.md +0 -305
- package/templates/dynamics-365-starter/deployment/QUICKSTART-MAC.md +0 -507
- package/templates/dynamics-365-starter/deployment/QUICKSTART-WINDOWS.md +0 -372
- package/templates/dynamics-365-starter/deployment/pipelines/README.md +0 -375
- package/templates/dynamics-365-starter/deployment/pipelines/azure-pipelines.yml +0 -330
- package/templates/dynamics-365-starter/deployment/pipelines/github-actions.yml +0 -422
- package/templates/dynamics-365-starter/deployment/pipelines/jenkins.groovy +0 -636
- package/templates/dynamics-365-starter/deployment/scripts/deploy.ps1 +0 -417
- package/templates/dynamics-365-starter/deployment/scripts/deploy.sh +0 -582
- package/templates/dynamics-365-starter/deployment/scripts/team-onboarding.ps1 +0 -486
- package/templates/dynamics-365-starter/deployment/scripts/team-onboarding.sh +0 -567
- package/templates/dynamics-365-starter/deployment/scripts/validate-setup.ps1 +0 -703
- package/templates/dynamics-365-starter/deployment/scripts/validate-setup.sh +0 -671
- package/templates/dynamics-365-starter/docs/team-standards/README.md +0 -273
- package/templates/dynamics-365-starter/docs/team-standards/client-onboarding.md +0 -577
- package/templates/dynamics-365-starter/docs/team-standards/code-review-checklist.md +0 -359
- package/templates/dynamics-365-starter/docs/team-standards/coding-standards.md +0 -700
- package/templates/dynamics-365-starter/docs/team-standards/cross-platform-team-guide.md +0 -736
- package/templates/dynamics-365-starter/docs/team-standards/development-workflows.md +0 -727
- package/templates/dynamics-365-starter/docs/troubleshooting/common-errors.md +0 -758
- package/templates/dynamics-365-starter/docs/troubleshooting/platform-specific-issues.md +0 -878
- package/templates/dynamics-365-starter/src/client-project-template/README.md +0 -234
- package/templates/dynamics-365-starter/src/client-project-template/config/client.template.json +0 -114
- package/templates/dynamics-365-starter/src/client-project-template/config/environments/template.json +0 -186
- package/templates/dynamics-365-starter/src/client-project-template/scripts/client-setup.js +0 -667
- package/templates/dynamics-365-starter/src/examples/README.md +0 -52
- package/templates/dynamics-365-starter/src/examples/component-examples/opportunity-management.tsx +0 -625
- package/templates/dynamics-365-starter/src/examples/entity-examples/opportunity-model.ts +0 -545
- package/templates/dynamics-365-starter/src/examples/integration-examples/custom-pcf-wrapper.tsx +0 -722
- package/templates/dynamics-365-starter/src/examples/workflow-examples/sales-workflow.ts +0 -662
- package/templates/dynamics-365-starter/src/page-templates/EntityDashboard.tsx +0 -519
- package/templates/dynamics-365-starter/src/page-templates/EntityDetailPage.tsx +0 -456
- package/templates/dynamics-365-starter/src/page-templates/EntityListPage.tsx +0 -406
- package/templates/dynamics-365-starter/src/page-templates/RelatedEntitiesPage.tsx +0 -578
- package/templates/dynamics-365-starter/src/page-templates/SearchPage.tsx +0 -629
- package/templates/dynamics-365-starter/tools/entity-generator/index.js +0 -168
- package/templates/dynamics-365-starter/tools/entity-generator/templates/constants.template.ts +0 -124
- package/templates/dynamics-365-starter/tools/entity-generator/templates/form.template.css +0 -283
- package/templates/dynamics-365-starter/tools/entity-generator/templates/form.template.tsx +0 -275
- package/templates/dynamics-365-starter/tools/entity-generator/templates/management.template.css +0 -204
- package/templates/dynamics-365-starter/tools/entity-generator/templates/management.template.tsx +0 -413
- package/templates/dynamics-365-starter/tools/entity-generator/templates/model.template.ts +0 -250
- package/templates/dynamics-365-starter/tools/metadata-sync/d365-client.js +0 -410
- package/templates/dynamics-365-starter/tools/metadata-sync/index.js +0 -512
- package/templates/dynamics-365-starter/tools/metadata-sync/type-generator.js +0 -675
- /package/templates/{dynamics-365-starter → react-custom-page}/README.md +0 -0
- /package/templates/{dynamics-365-starter → react-custom-page}/deployment/README.md +0 -0
- /package/templates/{dynamics-365-starter → react-custom-page}/docs/ARCHITECTURE_OVERVIEW.md +0 -0
- /package/templates/{dynamics-365-starter → react-custom-page}/docs/BEST_PRACTICES.md +0 -0
- /package/templates/{dynamics-365-starter → react-custom-page}/docs/MIGRATION_GUIDE.md +0 -0
- /package/templates/{dynamics-365-starter → react-custom-page}/public/index.html +0 -0
- /package/templates/{dynamics-365-starter → react-custom-page}/scripts/custom-build.js +0 -0
- /package/templates/{dynamics-365-starter → react-custom-page}/src/components/AccountForm.css +0 -0
- /package/templates/{dynamics-365-starter → react-custom-page}/src/components/AccountForm.tsx +0 -0
- /package/templates/{dynamics-365-starter → react-custom-page}/src/components/AccountManagement.css +0 -0
- /package/templates/{dynamics-365-starter → react-custom-page}/src/components/AccountManagement.tsx +0 -0
- /package/templates/{dynamics-365-starter → react-custom-page}/src/components/ContactForm.css +0 -0
- /package/templates/{dynamics-365-starter → react-custom-page}/src/components/ContactForm.tsx +0 -0
- /package/templates/{dynamics-365-starter → react-custom-page}/src/components/ContactManagement.css +0 -0
- /package/templates/{dynamics-365-starter → react-custom-page}/src/components/ContactManagement.tsx +0 -0
- /package/templates/{dynamics-365-starter → react-custom-page}/src/components/Logging/LogDialog.tsx +0 -0
- /package/templates/{dynamics-365-starter → react-custom-page}/src/components/Logging/LoggingContext.tsx +0 -0
- /package/templates/{dynamics-365-starter → react-custom-page}/src/components/Logging/LoggingDebugPanel.css +0 -0
- /package/templates/{dynamics-365-starter → react-custom-page}/src/components/Logging/LoggingDebugPanel.tsx +0 -0
- /package/templates/{dynamics-365-starter → react-custom-page}/src/components/Logging/LoggingProvider.tsx +0 -0
- /package/templates/{dynamics-365-starter → react-custom-page}/src/components/Logging/logger.ts +0 -0
- /package/templates/{dynamics-365-starter → react-custom-page}/src/constants/account.ts +0 -0
- /package/templates/{dynamics-365-starter → react-custom-page}/src/constants/contact.ts +0 -0
- /package/templates/{dynamics-365-starter → react-custom-page}/src/index.tsx +0 -0
- /package/templates/{dynamics-365-starter → react-custom-page}/src/models/Account.ts +0 -0
- /package/templates/{dynamics-365-starter → react-custom-page}/src/models/BaseEntity.ts +0 -0
- /package/templates/{dynamics-365-starter → react-custom-page}/src/models/Contact.ts +0 -0
- /package/templates/{dynamics-365-starter → react-custom-page}/src/pcf/ContactControlWrapper.tsx +0 -0
- /package/templates/{dynamics-365-starter → react-custom-page}/src/pcf/MultiEntityControlWrapper.tsx +0 -0
- /package/templates/{dynamics-365-starter → react-custom-page}/src/providers/DynamicsProvider.tsx +0 -0
- /package/templates/{dynamics-365-starter → react-custom-page}/src/services/MockApiService.ts +0 -0
- /package/templates/{dynamics-365-starter → react-custom-page}/src/services/ServiceFactory.ts +0 -0
- /package/templates/{dynamics-365-starter → react-custom-page}/src/services/XrmApiService.ts +0 -0
- /package/templates/{dynamics-365-starter → react-custom-page}/src/styles/index.css +0 -0
- /package/templates/{dynamics-365-starter → react-custom-page}/tsconfig.json +0 -0
- /package/templates/{dynamics-365-starter → react-custom-page}/webpack.config.js +0 -0
|
@@ -2,23 +2,25 @@ import fs from 'fs-extra';
|
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import chalk from 'chalk';
|
|
4
4
|
import ora from 'ora';
|
|
5
|
-
export async function updatePackageJson(projectPath, projectName,
|
|
5
|
+
export async function updatePackageJson(projectPath, projectName, options = {}) {
|
|
6
6
|
const spinner = ora('Updating package.json...').start();
|
|
7
|
+
const { provenance } = options;
|
|
7
8
|
try {
|
|
8
9
|
const packageJsonPath = path.join(projectPath, 'package.json');
|
|
9
10
|
if (await fs.pathExists(packageJsonPath)) {
|
|
10
11
|
const packageJson = await fs.readJson(packageJsonPath);
|
|
11
12
|
// Update the name
|
|
12
13
|
packageJson.name = projectName;
|
|
13
|
-
//
|
|
14
|
-
if (
|
|
15
|
-
packageJson.
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
14
|
+
// Stamp provenance so we can detect the generating CLI/template later.
|
|
15
|
+
if (provenance) {
|
|
16
|
+
packageJson.dynamicsKit = {
|
|
17
|
+
cliVersion: provenance.cliVersion,
|
|
18
|
+
artifact: provenance.artifact,
|
|
19
|
+
...(provenance.componentLibrary
|
|
20
|
+
? { componentLibrary: provenance.componentLibrary }
|
|
21
|
+
: {}),
|
|
22
|
+
templateVersion: provenance.templateVersion,
|
|
23
|
+
};
|
|
22
24
|
}
|
|
23
25
|
// Remove any template-specific fields that shouldn't be in final package
|
|
24
26
|
delete packageJson.template;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"updatePackageJson.js","sourceRoot":"","sources":["../../src/utils/updatePackageJson.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,UAAU,CAAC;AAC1B,OAAO,IAAI,MAAM,MAAM,CAAC;AACxB,OAAO,KAAK,MAAM,OAAO,CAAC;AAC1B,OAAO,GAAG,MAAM,KAAK,CAAC;
|
|
1
|
+
{"version":3,"file":"updatePackageJson.js","sourceRoot":"","sources":["../../src/utils/updatePackageJson.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,UAAU,CAAC;AAC1B,OAAO,IAAI,MAAM,MAAM,CAAC;AACxB,OAAO,KAAK,MAAM,OAAO,CAAC;AAC1B,OAAO,GAAG,MAAM,KAAK,CAAC;AActB,MAAM,CAAC,KAAK,UAAU,iBAAiB,CACrC,WAAmB,EACnB,WAAmB,EACnB,UAAoC,EAAE;IAEtC,MAAM,OAAO,GAAG,GAAG,CAAC,0BAA0B,CAAC,CAAC,KAAK,EAAE,CAAC;IACxD,MAAM,EAAE,UAAU,EAAE,GAAG,OAAO,CAAC;IAE/B,IAAI,CAAC;QACH,MAAM,eAAe,GAAG,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,cAAc,CAAC,CAAC;QAE/D,IAAI,MAAM,EAAE,CAAC,UAAU,CAAC,eAAe,CAAC,EAAE,CAAC;YACzC,MAAM,WAAW,GAAG,MAAM,EAAE,CAAC,QAAQ,CAAC,eAAe,CAAC,CAAC;YAEvD,kBAAkB;YAClB,WAAW,CAAC,IAAI,GAAG,WAAW,CAAC;YAE/B,uEAAuE;YACvE,IAAI,UAAU,EAAE,CAAC;gBACf,WAAW,CAAC,WAAW,GAAG;oBACxB,UAAU,EAAE,UAAU,CAAC,UAAU;oBACjC,QAAQ,EAAE,UAAU,CAAC,QAAQ;oBAC7B,GAAG,CAAC,UAAU,CAAC,gBAAgB;wBAC7B,CAAC,CAAC,EAAE,gBAAgB,EAAE,UAAU,CAAC,gBAAgB,EAAE;wBACnD,CAAC,CAAC,EAAE,CAAC;oBACP,eAAe,EAAE,UAAU,CAAC,eAAe;iBAC5C,CAAC;YACJ,CAAC;YAED,yEAAyE;YACzE,OAAO,WAAW,CAAC,QAAQ,CAAC;YAE5B,MAAM,EAAE,CAAC,SAAS,CAAC,eAAe,EAAE,WAAW,EAAE,EAAE,MAAM,EAAE,CAAC,EAAE,CAAC,CAAC;QAClE,CAAC;QAED,OAAO,CAAC,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC,sBAAsB,CAAC,CAAC,CAAC;IACvD,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,+BAA+B,CAAC,CAAC,CAAC;QACzD,MAAM,KAAK,CAAC;IACd,CAAC;AACH,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@khester/create-dynamics-app",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "CLI
|
|
3
|
+
"version": "2.1.0",
|
|
4
|
+
"description": "Unified CLI to scaffold Dynamics 365 / Power Platform artifacts",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"bin": {
|
|
8
|
-
"create-dynamics-app": "
|
|
8
|
+
"create-dynamics-app": "bin/create-dynamics-app.js"
|
|
9
9
|
},
|
|
10
10
|
"files": [
|
|
11
11
|
"dist",
|
|
@@ -13,15 +13,18 @@
|
|
|
13
13
|
"templates"
|
|
14
14
|
],
|
|
15
15
|
"scripts": {
|
|
16
|
+
"prebuild": "rimraf dist tsconfig.tsbuildinfo",
|
|
16
17
|
"build": "tsc",
|
|
17
18
|
"dev": "tsc --watch",
|
|
18
19
|
"prepublishOnly": "npm run build",
|
|
19
|
-
"test": "
|
|
20
|
+
"test": "vitest run",
|
|
21
|
+
"test:watch": "vitest",
|
|
20
22
|
"typecheck": "tsc --noEmit",
|
|
21
23
|
"lint": "eslint src --ext .ts,.tsx",
|
|
22
|
-
"clean": "rimraf dist"
|
|
24
|
+
"clean": "rimraf dist tsconfig.tsbuildinfo"
|
|
23
25
|
},
|
|
24
26
|
"dependencies": {
|
|
27
|
+
"@dataverse-kit/export-engine": "^1.0.0",
|
|
25
28
|
"@khester/dynamics-ui-components": "^1.0.0",
|
|
26
29
|
"chalk": "^5.3.0",
|
|
27
30
|
"commander": "^11.1.0",
|
|
@@ -44,9 +47,9 @@
|
|
|
44
47
|
"create-app"
|
|
45
48
|
],
|
|
46
49
|
"engines": {
|
|
47
|
-
"node": ">=
|
|
50
|
+
"node": ">=20.0.0"
|
|
48
51
|
},
|
|
49
52
|
"publishConfig": {
|
|
50
|
-
"access": "
|
|
53
|
+
"access": "public"
|
|
51
54
|
}
|
|
52
55
|
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Acquire a Dataverse bearer token via the Azure CLI and write it to `.env`
|
|
5
|
+
* (DYNAMICS_URL + DYNAMICS_TOKEN — the exact names @dataverse-kit/dataverse-codegen's
|
|
6
|
+
* resolveConnection reads). Tokens are short-lived; re-run before a codegen/smoke run.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* node tools/auth/get-token.js --url https://org.crm.dynamics.com
|
|
10
|
+
* (or set DYNAMICS_URL in the environment / .env)
|
|
11
|
+
*/
|
|
12
|
+
const { spawnSync } = require('child_process');
|
|
13
|
+
const fs = require('fs');
|
|
14
|
+
const path = require('path');
|
|
15
|
+
|
|
16
|
+
function arg(name) {
|
|
17
|
+
const i = process.argv.indexOf(name);
|
|
18
|
+
return i !== -1 ? process.argv[i + 1] : undefined;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function readEnv(file) {
|
|
22
|
+
if (!fs.existsSync(file)) return {};
|
|
23
|
+
const out = {};
|
|
24
|
+
for (const line of fs.readFileSync(file, 'utf8').split(/\r?\n/)) {
|
|
25
|
+
const m = line.match(/^([A-Z0-9_]+)=(.*)$/);
|
|
26
|
+
if (m) out[m[1]] = m[2];
|
|
27
|
+
}
|
|
28
|
+
return out;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function writeEnv(file, env) {
|
|
32
|
+
const body = Object.entries(env)
|
|
33
|
+
.map(([k, v]) => `${k}=${v}`)
|
|
34
|
+
.join('\n');
|
|
35
|
+
fs.writeFileSync(file, body + '\n', 'utf8');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const envPath = path.join(process.cwd(), '.env');
|
|
39
|
+
const env = readEnv(envPath);
|
|
40
|
+
const url = arg('--url') || process.env.DYNAMICS_URL || env.DYNAMICS_URL;
|
|
41
|
+
|
|
42
|
+
if (!url) {
|
|
43
|
+
console.error(
|
|
44
|
+
'✗ No Dataverse URL. Pass --url https://org.crm.dynamics.com or set DYNAMICS_URL in .env.'
|
|
45
|
+
);
|
|
46
|
+
process.exit(1);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
console.log(`→ Acquiring token for ${url} via Azure CLI...`);
|
|
50
|
+
const res = spawnSync(
|
|
51
|
+
'az',
|
|
52
|
+
['account', 'get-access-token', '--resource', url, '--query', 'accessToken', '-o', 'tsv'],
|
|
53
|
+
{ encoding: 'utf8' }
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
if (res.status !== 0) {
|
|
57
|
+
console.error('✗ az account get-access-token failed. Run `az login` first.');
|
|
58
|
+
if (res.stderr) console.error(res.stderr.trim());
|
|
59
|
+
process.exit(1);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const token = (res.stdout || '').trim();
|
|
63
|
+
if (!token) {
|
|
64
|
+
console.error('✗ Azure CLI returned an empty token.');
|
|
65
|
+
process.exit(1);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Merge keys — never clobber an existing .env wholesale.
|
|
69
|
+
env.DYNAMICS_URL = url;
|
|
70
|
+
env.DYNAMICS_TOKEN = token;
|
|
71
|
+
writeEnv(envPath, env);
|
|
72
|
+
console.log('✓ Wrote DYNAMICS_URL + DYNAMICS_TOKEN to .env');
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal mock of the Dynamics `Xrm` global for local development. Lets an
|
|
3
|
+
* artifact that talks to `Xrm.WebApi` run in a plain browser/dev-server without a
|
|
4
|
+
* model-driven host. Pair with a ServiceFactory that flips to a mock service on
|
|
5
|
+
* localhost (see the React Custom Page template's ServiceFactory.isMockEnvironment).
|
|
6
|
+
*
|
|
7
|
+
* Usage (entry HTML or dev bootstrap):
|
|
8
|
+
* import { installMockXrm } from './tools/dev/mock-xrm.js';
|
|
9
|
+
* if (location.hostname === 'localhost') installMockXrm();
|
|
10
|
+
*/
|
|
11
|
+
export function installMockXrm(seed = {}) {
|
|
12
|
+
const store = new Map(Object.entries(seed));
|
|
13
|
+
const ok = (data) => Promise.resolve(data);
|
|
14
|
+
|
|
15
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
16
|
+
globalThis.Xrm = {
|
|
17
|
+
WebApi: {
|
|
18
|
+
retrieveRecord: (entity, id) => ok(store.get(`${entity}:${id}`) ?? { [`${entity}id`]: id }),
|
|
19
|
+
retrieveMultipleRecords: () => ok({ entities: [...store.values()] }),
|
|
20
|
+
createRecord: (entity, data) => {
|
|
21
|
+
const id = `mock-${store.size + 1}`;
|
|
22
|
+
store.set(`${entity}:${id}`, { ...data, [`${entity}id`]: id });
|
|
23
|
+
return ok({ id });
|
|
24
|
+
},
|
|
25
|
+
updateRecord: (entity, id, data) => {
|
|
26
|
+
store.set(`${entity}:${id}`, { ...(store.get(`${entity}:${id}`) || {}), ...data });
|
|
27
|
+
return ok(undefined);
|
|
28
|
+
},
|
|
29
|
+
deleteRecord: (entity, id) => {
|
|
30
|
+
store.delete(`${entity}:${id}`);
|
|
31
|
+
return ok(undefined);
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
Utility: {
|
|
35
|
+
getGlobalContext: () => ({
|
|
36
|
+
getClientUrl: () => 'http://localhost',
|
|
37
|
+
userSettings: { userId: 'mock-user', userName: 'Mock User' },
|
|
38
|
+
}),
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
return globalThis.Xrm;
|
|
42
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Metadata Sync — thin wrapper around @dataverse-kit/dataverse-codegen (dvgen).
|
|
5
|
+
*
|
|
6
|
+
* Pulls REAL entity metadata from a live Dataverse org and generates field
|
|
7
|
+
* constants + typed models (and, optionally, the IApiService CRUD layer) into
|
|
8
|
+
* `src/`. All org connection + auth is delegated to dvgen, which resolves, in
|
|
9
|
+
* order: --url/--token/--env-file flags, the DYNAMICS_* / VITE_DYNAMICS_* /
|
|
10
|
+
* REACT_APP_DYNAMICS_* env vars, then the Azure CLI (`az account get-access-token`).
|
|
11
|
+
*
|
|
12
|
+
* This replaces the previous mock pipeline (which returned hard-coded metadata).
|
|
13
|
+
* dvgen is the same tool the rest of the toolkit uses, so output stays consistent
|
|
14
|
+
* with `dvgen design`/`constants`/`models` everywhere.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
const { program } = require('commander');
|
|
18
|
+
const { spawnSync } = require('child_process');
|
|
19
|
+
const path = require('path');
|
|
20
|
+
const fs = require('fs');
|
|
21
|
+
|
|
22
|
+
const DVGEN_CLI = path.join(
|
|
23
|
+
process.cwd(),
|
|
24
|
+
'node_modules',
|
|
25
|
+
'@dataverse-kit',
|
|
26
|
+
'dataverse-codegen',
|
|
27
|
+
'dist',
|
|
28
|
+
'cli.cjs'
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
function ensureDvgen() {
|
|
32
|
+
if (!fs.existsSync(DVGEN_CLI)) {
|
|
33
|
+
console.error(
|
|
34
|
+
'✗ dvgen not found. Install it first:\n' +
|
|
35
|
+
' npm install -D @dataverse-kit/dataverse-codegen'
|
|
36
|
+
);
|
|
37
|
+
process.exit(1);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function dvgen(args) {
|
|
42
|
+
const result = spawnSync(process.execPath, [DVGEN_CLI, ...args], {
|
|
43
|
+
stdio: 'inherit',
|
|
44
|
+
});
|
|
45
|
+
return result.status === 0;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function resolveEntities(opts) {
|
|
49
|
+
if (opts.entities) {
|
|
50
|
+
return opts.entities
|
|
51
|
+
.split(',')
|
|
52
|
+
.map((e) => e.trim())
|
|
53
|
+
.filter(Boolean);
|
|
54
|
+
}
|
|
55
|
+
return ['account', 'contact'];
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function connectionArgs(opts) {
|
|
59
|
+
const args = [];
|
|
60
|
+
if (opts.url) args.push('--url', opts.url);
|
|
61
|
+
if (opts.token) args.push('--token', opts.token);
|
|
62
|
+
if (opts.envFile) args.push('--env-file', opts.envFile);
|
|
63
|
+
return args;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function run(opts, { diff = false } = {}) {
|
|
67
|
+
ensureDvgen();
|
|
68
|
+
const entities = resolveEntities(opts);
|
|
69
|
+
const out = opts.out || 'src';
|
|
70
|
+
const conn = connectionArgs(opts);
|
|
71
|
+
const extra = diff ? ['--diff'] : [];
|
|
72
|
+
if (opts.force && !diff) extra.push('--force');
|
|
73
|
+
// The starter's shipped models import IApiService from
|
|
74
|
+
// @khester/dynamics-ui-api-client, so point generated models at the same
|
|
75
|
+
// module. When the dvgen-local CRUD layer is generated (--api), use that.
|
|
76
|
+
const apiServiceImport = opts.api ? '../services/IApiService' : opts.apiService;
|
|
77
|
+
|
|
78
|
+
console.log(
|
|
79
|
+
`${diff ? '🔍 Diffing' : '🔄 Generating'} ${entities.join(
|
|
80
|
+
', '
|
|
81
|
+
)} via dvgen → ${out}/\n`
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
// Shared CRUD layer once (offline; only when explicitly requested).
|
|
85
|
+
if (opts.api && !diff) {
|
|
86
|
+
if (!dvgen(['api', '--out', out])) process.exit(1);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
let ok = true;
|
|
90
|
+
for (const entity of entities) {
|
|
91
|
+
console.log(`\n→ ${entity}`);
|
|
92
|
+
if (
|
|
93
|
+
!dvgen(['constants', '--entity', entity, '--out', out, ...conn, ...extra])
|
|
94
|
+
) {
|
|
95
|
+
ok = false;
|
|
96
|
+
}
|
|
97
|
+
const modelArgs = ['models', '--entity', entity, '--out', out, ...conn, ...extra];
|
|
98
|
+
if (apiServiceImport) modelArgs.push('--api-service', apiServiceImport);
|
|
99
|
+
if (opts.withRetrieve) modelArgs.push('--with-retrieve');
|
|
100
|
+
if (!dvgen(modelArgs)) ok = false;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (!ok) {
|
|
104
|
+
console.error('\n✗ One or more entities failed.');
|
|
105
|
+
process.exit(1);
|
|
106
|
+
}
|
|
107
|
+
console.log('\n✓ Done.');
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function withCommon(cmd) {
|
|
111
|
+
return cmd
|
|
112
|
+
.option(
|
|
113
|
+
'-e, --entities <list>',
|
|
114
|
+
'comma-separated entities (default: account,contact)'
|
|
115
|
+
)
|
|
116
|
+
.option('--url <url>', 'Dataverse org URL (else dvgen resolves env / Azure CLI)')
|
|
117
|
+
.option('--token <token>', 'bearer token (else dvgen resolves env / Azure CLI)')
|
|
118
|
+
.option('--env-file <path>', '.env file with DYNAMICS_URL / DYNAMICS_TOKEN')
|
|
119
|
+
.option('-o, --out <dir>', 'output base dir', 'src')
|
|
120
|
+
.option('--force', 'overwrite existing generated files (default: skip)')
|
|
121
|
+
.option('--with-retrieve', 'include retrieveWithRelated() FetchXML scaffolds')
|
|
122
|
+
.option(
|
|
123
|
+
'--api-service <import>',
|
|
124
|
+
'IApiService module specifier for generated models',
|
|
125
|
+
'@khester/dynamics-ui-api-client'
|
|
126
|
+
)
|
|
127
|
+
.option('--api', 'also (re)generate the IApiService CRUD layer');
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
program
|
|
131
|
+
.name('metadata-sync')
|
|
132
|
+
.description(
|
|
133
|
+
'Live Dataverse metadata → TypeScript (constants + models) via dvgen'
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
withCommon(program.command('sync'))
|
|
137
|
+
.description('Pull live metadata and generate constants + models')
|
|
138
|
+
.action((o) => run(o));
|
|
139
|
+
|
|
140
|
+
withCommon(program.command('pull'))
|
|
141
|
+
.description('Alias of sync (pull live metadata + generate)')
|
|
142
|
+
.action((o) => run(o));
|
|
143
|
+
|
|
144
|
+
withCommon(program.command('generate'))
|
|
145
|
+
.description('Alias of sync (generate constants + models from the live org)')
|
|
146
|
+
.action((o) => run(o));
|
|
147
|
+
|
|
148
|
+
withCommon(program.command('validate'))
|
|
149
|
+
.description('Diff generated code against the live org (no writes)')
|
|
150
|
+
.action((o) => run(o, { diff: true }));
|
|
151
|
+
|
|
152
|
+
program.parse();
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Live-org smoke test: retrieve a few rows of an entity to confirm the generated
|
|
5
|
+
* data layer + token work end-to-end before deploy. Delegates to dvgen's
|
|
6
|
+
* `test-retrieve --live`, which resolves the connection (flag/env/Azure CLI).
|
|
7
|
+
*
|
|
8
|
+
* Usage: node tools/smoke/test-retrieve.js --entity account
|
|
9
|
+
*/
|
|
10
|
+
const { spawnSync } = require('child_process');
|
|
11
|
+
const path = require('path');
|
|
12
|
+
const fs = require('fs');
|
|
13
|
+
|
|
14
|
+
function arg(name, fallback) {
|
|
15
|
+
const i = process.argv.indexOf(name);
|
|
16
|
+
return i !== -1 ? process.argv[i + 1] : fallback;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const DVGEN = path.join(
|
|
20
|
+
process.cwd(),
|
|
21
|
+
'node_modules',
|
|
22
|
+
'@dataverse-kit',
|
|
23
|
+
'dataverse-codegen',
|
|
24
|
+
'dist',
|
|
25
|
+
'cli.cjs'
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
if (!fs.existsSync(DVGEN)) {
|
|
29
|
+
console.error('✗ dvgen not found. Install @dataverse-kit/dataverse-codegen first.');
|
|
30
|
+
process.exit(1);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const entity = arg('--entity');
|
|
34
|
+
if (!entity) {
|
|
35
|
+
console.error('✗ Pass --entity <logicalName> (e.g. --entity account).');
|
|
36
|
+
process.exit(1);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const res = spawnSync(
|
|
40
|
+
process.execPath,
|
|
41
|
+
[DVGEN, 'test-retrieve', '--entity', entity, '--live'],
|
|
42
|
+
{ stdio: 'inherit' }
|
|
43
|
+
);
|
|
44
|
+
process.exit(res.status ?? 1);
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# {{projectName}}
|
|
2
|
+
|
|
3
|
+
A Dynamics 365 **dialog form** — a single React form (Fluent UI {{componentLibrary}}) you host as a
|
|
4
|
+
web resource and open as a dialog (e.g. via `Xrm.Navigation.navigateTo` / `openWebResource`).
|
|
5
|
+
|
|
6
|
+
## Develop
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
npm install
|
|
10
|
+
npm run dev # webpack dev server on http://localhost:8080
|
|
11
|
+
npm run build # production bundle → dist/{{projectName}}.js
|
|
12
|
+
npm run typecheck # tsc --noEmit
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
The form (`src/App.tsx`) saves via `src/services/dataverse.ts` (`Xrm.WebApi`). Outside a host, stub
|
|
16
|
+
Xrm with the `serve` dev-tool's `tools/dev/mock-xrm.js`.
|
|
17
|
+
|
|
18
|
+
## Dev-tools
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
npm run auth:token -- --url https://YOUR_ORG.crm.dynamics.com # token → .env
|
|
22
|
+
npm run metadata:pull -- --entities contact # typed entities
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Deploy
|
|
26
|
+
|
|
27
|
+
Upload `dist/{{projectName}}.js` as a web resource and open it as a dialog from a form/ribbon command.
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { ThemeProvider } from '@fluentui/react/lib/Theme';
|
|
3
|
+
import { Stack } from '@fluentui/react/lib/Stack';
|
|
4
|
+
import { Text } from '@fluentui/react/lib/Text';
|
|
5
|
+
import { TextField } from '@fluentui/react/lib/TextField';
|
|
6
|
+
import { PrimaryButton, DefaultButton } from '@fluentui/react/lib/Button';
|
|
7
|
+
import { createRecord } from './services/dataverse';
|
|
8
|
+
|
|
9
|
+
export const App: React.FC = () => {
|
|
10
|
+
const [name, setName] = React.useState('');
|
|
11
|
+
const [email, setEmail] = React.useState('');
|
|
12
|
+
const [saving, setSaving] = React.useState(false);
|
|
13
|
+
|
|
14
|
+
const onSave = async () => {
|
|
15
|
+
setSaving(true);
|
|
16
|
+
try {
|
|
17
|
+
await createRecord('contact', { fullname: name, emailaddress1: email });
|
|
18
|
+
alert('Saved.');
|
|
19
|
+
} catch (e) {
|
|
20
|
+
alert(`Save failed (Xrm not available here?): ${String(e)}`);
|
|
21
|
+
} finally {
|
|
22
|
+
setSaving(false);
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
return (
|
|
27
|
+
<ThemeProvider>
|
|
28
|
+
<Stack tokens={{ childrenGap: 12, padding: 16 }} styles={{ root: { maxWidth: 480 } }}>
|
|
29
|
+
<Text variant="xLarge">{{projectName}}</Text>
|
|
30
|
+
<TextField label="Full name" value={name} onChange={(_, v) => setName(v ?? '')} required />
|
|
31
|
+
<TextField label="Email" value={email} onChange={(_, v) => setEmail(v ?? '')} />
|
|
32
|
+
<Stack horizontal tokens={{ childrenGap: 8 }}>
|
|
33
|
+
<PrimaryButton text="Save" onClick={onSave} disabled={saving} />
|
|
34
|
+
<DefaultButton text="Cancel" onClick={() => window.history.back()} />
|
|
35
|
+
</Stack>
|
|
36
|
+
</Stack>
|
|
37
|
+
</ThemeProvider>
|
|
38
|
+
);
|
|
39
|
+
};
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { FluentProvider, webLightTheme, Text, Input, Button, Field } from '@fluentui/react-components';
|
|
3
|
+
import { createRecord } from './services/dataverse';
|
|
4
|
+
|
|
5
|
+
export const App: React.FC = () => {
|
|
6
|
+
const [name, setName] = React.useState('');
|
|
7
|
+
const [email, setEmail] = React.useState('');
|
|
8
|
+
const [saving, setSaving] = React.useState(false);
|
|
9
|
+
|
|
10
|
+
const onSave = async () => {
|
|
11
|
+
setSaving(true);
|
|
12
|
+
try {
|
|
13
|
+
await createRecord('contact', { fullname: name, emailaddress1: email });
|
|
14
|
+
alert('Saved.');
|
|
15
|
+
} catch (e) {
|
|
16
|
+
alert(`Save failed (Xrm not available here?): ${String(e)}`);
|
|
17
|
+
} finally {
|
|
18
|
+
setSaving(false);
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
return (
|
|
23
|
+
<FluentProvider theme={webLightTheme}>
|
|
24
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: 12, padding: 16, maxWidth: 480 }}>
|
|
25
|
+
<Text size={600} weight="semibold">{{projectName}}</Text>
|
|
26
|
+
<Field label="Full name" required>
|
|
27
|
+
<Input value={name} onChange={(_, d) => setName(d.value)} />
|
|
28
|
+
</Field>
|
|
29
|
+
<Field label="Email">
|
|
30
|
+
<Input value={email} onChange={(_, d) => setEmail(d.value)} />
|
|
31
|
+
</Field>
|
|
32
|
+
<div style={{ display: 'flex', gap: 8 }}>
|
|
33
|
+
<Button appearance="primary" onClick={onSave} disabled={saving}>
|
|
34
|
+
Save
|
|
35
|
+
</Button>
|
|
36
|
+
<Button onClick={() => window.history.back()}>Cancel</Button>
|
|
37
|
+
</div>
|
|
38
|
+
</div>
|
|
39
|
+
</FluentProvider>
|
|
40
|
+
);
|
|
41
|
+
};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "{{projectName}}",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "{{projectName}} — Dynamics 365 dialog form (React, Fluent {{componentLibrary}})",
|
|
5
|
+
"private": true,
|
|
6
|
+
"scripts": {
|
|
7
|
+
"dev": "webpack serve --mode development",
|
|
8
|
+
"build": "webpack --mode production",
|
|
9
|
+
"typecheck": "tsc --noEmit",
|
|
10
|
+
"lint": "eslint src --ext .ts,.tsx"
|
|
11
|
+
},
|
|
12
|
+
"dependencies": {
|
|
13
|
+
"react": "^18.2.0",
|
|
14
|
+
"react-dom": "^18.2.0"
|
|
15
|
+
},
|
|
16
|
+
"devDependencies": {
|
|
17
|
+
"@types/react": "^18.2.0",
|
|
18
|
+
"@types/react-dom": "^18.2.0",
|
|
19
|
+
"@types/xrm": "^9.0.74",
|
|
20
|
+
"html-webpack-plugin": "^5.5.3",
|
|
21
|
+
"ts-loader": "^9.5.1",
|
|
22
|
+
"typescript": "^5.3.3",
|
|
23
|
+
"webpack": "^5.89.0",
|
|
24
|
+
"webpack-cli": "^5.1.4",
|
|
25
|
+
"webpack-dev-server": "^4.15.1"
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { createRoot } from 'react-dom/client';
|
|
3
|
+
import { App } from './App';
|
|
4
|
+
|
|
5
|
+
// On localhost, `tools/dev/mock-xrm.js` (from the `serve` dev-tool) can be loaded
|
|
6
|
+
// to stub the Xrm global so this resource runs outside a model-driven host.
|
|
7
|
+
const container = document.getElementById('root');
|
|
8
|
+
if (container) {
|
|
9
|
+
createRoot(container).render(<App />);
|
|
10
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Thin wrapper over Xrm.WebApi for this web resource's data access. The Xrm global
|
|
3
|
+
* is typed via @types/xrm; on localhost, stub it with tools/dev/mock-xrm.js.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export type DataverseRecord = Record<string, unknown>;
|
|
7
|
+
|
|
8
|
+
export async function retrieveMultiple(
|
|
9
|
+
entityLogicalName: string,
|
|
10
|
+
options = ''
|
|
11
|
+
): Promise<DataverseRecord[]> {
|
|
12
|
+
const result = await Xrm.WebApi.retrieveMultipleRecords(entityLogicalName, options);
|
|
13
|
+
return result.entities;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export async function retrieveRecord(
|
|
17
|
+
entityLogicalName: string,
|
|
18
|
+
id: string,
|
|
19
|
+
options = ''
|
|
20
|
+
): Promise<DataverseRecord> {
|
|
21
|
+
return Xrm.WebApi.retrieveRecord(entityLogicalName, id, options);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export async function createRecord(
|
|
25
|
+
entityLogicalName: string,
|
|
26
|
+
data: Record<string, unknown>
|
|
27
|
+
): Promise<string> {
|
|
28
|
+
const res = await Xrm.WebApi.createRecord(entityLogicalName, data);
|
|
29
|
+
return res.id;
|
|
30
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2018",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"moduleResolution": "node",
|
|
6
|
+
"jsx": "react-jsx",
|
|
7
|
+
"strict": true,
|
|
8
|
+
"esModuleInterop": true,
|
|
9
|
+
"forceConsistentCasingInFileNames": true,
|
|
10
|
+
"skipLibCheck": true,
|
|
11
|
+
"lib": ["DOM", "DOM.Iterable", "ES2018"],
|
|
12
|
+
"types": ["xrm"]
|
|
13
|
+
},
|
|
14
|
+
"include": ["src"]
|
|
15
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
const HtmlWebpackPlugin = require('html-webpack-plugin');
|
|
3
|
+
|
|
4
|
+
module.exports = {
|
|
5
|
+
entry: './src/index.tsx',
|
|
6
|
+
output: {
|
|
7
|
+
path: path.resolve(__dirname, 'dist'),
|
|
8
|
+
filename: '{{projectName}}.js',
|
|
9
|
+
clean: true,
|
|
10
|
+
},
|
|
11
|
+
resolve: { extensions: ['.ts', '.tsx', '.js'] },
|
|
12
|
+
module: {
|
|
13
|
+
rules: [{ test: /\.tsx?$/, use: 'ts-loader', exclude: /node_modules/ }],
|
|
14
|
+
},
|
|
15
|
+
plugins: [new HtmlWebpackPlugin({ template: './public/index.html' })],
|
|
16
|
+
devServer: { static: './dist', port: 8080, hot: true },
|
|
17
|
+
};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# {{componentName}} (grid customizer)
|
|
2
|
+
|
|
3
|
+
A Dynamics 365 **editable-grid customizer** — maps column data types / logical names to custom Fluent
|
|
4
|
+
UI v9 cell renderers (status badge, currency, date, progress).
|
|
5
|
+
|
|
6
|
+
## Develop
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
npm install
|
|
10
|
+
npm run build # webpack → dist/{{projectName}}.js (UMD)
|
|
11
|
+
npm run typecheck # tsc --noEmit
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Customize
|
|
15
|
+
|
|
16
|
+
Edit `src/GridCustomizer.ts` to map columns to renderers, and add renderers in
|
|
17
|
+
`src/cell-renderers.tsx`:
|
|
18
|
+
|
|
19
|
+
```ts
|
|
20
|
+
cellRendererOverrides = {
|
|
21
|
+
OptionSet: StatusBadgeRenderer, // by data type
|
|
22
|
+
prioritycode: StatusBadgeRenderer // by column logical name
|
|
23
|
+
};
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Deploy
|
|
27
|
+
|
|
28
|
+
Bundle and register `dist/{{projectName}}.js` on the grid control's customizer property.
|