@khester/create-dynamics-app 2.10.0 → 3.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 +28 -3
- package/dist/artifacts/registry.d.ts +7 -5
- package/dist/artifacts/registry.d.ts.map +1 -1
- package/dist/artifacts/registry.js +82 -27
- package/dist/artifacts/registry.js.map +1 -1
- package/dist/artifacts/types.d.ts +15 -8
- package/dist/artifacts/types.d.ts.map +1 -1
- package/dist/index.js +7 -5
- package/dist/index.js.map +1 -1
- package/dist/scaffold.d.ts.map +1 -1
- package/dist/scaffold.js +9 -4
- package/dist/scaffold.js.map +1 -1
- package/dist/templatePlan.d.ts.map +1 -1
- package/dist/templatePlan.js +20 -3
- package/dist/templatePlan.js.map +1 -1
- package/dist/utils/copyTemplate.d.ts +1 -1
- package/dist/utils/copyTemplate.d.ts.map +1 -1
- package/dist/utils/copyTemplate.js +5 -1
- package/dist/utils/copyTemplate.js.map +1 -1
- package/package.json +1 -1
- package/templates/_shared/dataverse-seam/XrmApiService.ts +26 -5
- package/templates/host-swap/README.md +57 -2
- package/templates/host-swap/_variants/host.custom-page/src/App.tsx +9 -5
- package/templates/host-swap/_variants/host.pcf/ControlManifest.Input.xml +26 -2
- package/templates/host-swap/_variants/host.pcf/gitignore +7 -0
- package/templates/host-swap/_variants/host.pcf/index.ts +21 -3
- package/templates/host-swap/_variants/host.pcf/package.json +20 -4
- package/templates/host-swap/_variants/host.pcf/{{componentName}}.pcfproj +67 -0
- package/templates/host-swap/_variants/host.web-resource/src/App.tsx +5 -1
- package/templates/host-swap/src/Component.tsx +54 -7
- package/templates/host-swap/src/useHostWidth.ts +53 -0
- package/templates/pcf-dataset/dev/harness/dataverse.ts +82 -0
- package/templates/pcf-dataset/dev/index.html +20 -0
- package/templates/pcf-dataset/dev/main.tsx +190 -0
- package/templates/pcf-dataset/dev/vite-env.d.ts +1 -0
- package/templates/pcf-dataset/env.local.example +24 -0
- package/templates/pcf-dataset/eslintrc.json +11 -0
- package/templates/pcf-dataset/gitignore +4 -0
- package/templates/pcf-dataset/package.json +18 -8
- package/templates/pcf-dataset/pcfconfig.json +3 -0
- package/templates/pcf-dataset/tsconfig.dev.json +15 -0
- package/templates/pcf-dataset/tsconfig.json +11 -2
- package/templates/pcf-dataset/vite.config.ts +63 -0
- package/templates/pcf-dataset/{{componentName}}/ControlManifest.Input.xml +45 -0
- package/templates/pcf-dataset/{{componentName}}/{{componentName}}App.tsx +64 -0
- package/templates/pcf-dataset/{{{componentName}}Component.tsx → {{componentName}}/{{componentName}}Component.tsx} +9 -46
- package/templates/pcf-field/eslintrc.json +11 -0
- package/templates/pcf-field/package.json +1 -0
- package/templates/pcf-field/pcfconfig.json +3 -0
- package/templates/pcf-field/{{componentName}}/ControlManifest.Input.xml +42 -0
- package/templates/power-pages-island/README.md +93 -0
- package/templates/power-pages-island/package.json +27 -0
- package/templates/power-pages-island/public/index.html +13 -0
- package/templates/power-pages-island/src/App.tsx +37 -0
- package/templates/power-pages-island/src/Component.tsx +136 -0
- package/templates/power-pages-island/src/core/services/MockApiService.ts +58 -0
- package/templates/power-pages-island/src/core/services/portalSeam.assert.ts +14 -0
- package/templates/power-pages-island/src/island-env.d.ts +12 -0
- package/templates/power-pages-island/src/islands.tsx +38 -0
- package/templates/power-pages-island/src/useHostWidth.ts +53 -0
- package/templates/power-pages-island/tsconfig.json +15 -0
- package/templates/power-pages-island/webpack.config.js +22 -0
- package/templates/react-custom-page/tools/deploy/deploy-webresource.cjs +8 -2
- package/templates/pcf-dataset/ControlManifest.Input.xml +0 -16
- package/templates/pcf-field/ControlManifest.Input.xml +0 -17
- package/templates/power-pages-starter/.env.example +0 -6
- package/templates/power-pages-starter/README.md +0 -95
- package/templates/power-pages-starter/package.json +0 -43
- package/templates/power-pages-starter/public/index.html +0 -15
- package/templates/power-pages-starter/src/components/ContactForm.css +0 -84
- package/templates/power-pages-starter/src/components/ContactForm.tsx +0 -258
- package/templates/power-pages-starter/src/index.tsx +0 -32
- package/templates/power-pages-starter/src/providers/PowerPagesProvider.tsx +0 -162
- package/templates/power-pages-starter/src/styles/index.css +0 -76
- package/templates/power-pages-starter/tsconfig.json +0 -20
- package/templates/power-pages-starter/webpack.config.js +0 -57
- package/templates/pcf-dataset/{index.ts → {{componentName}}/index.ts} +0 -0
- package/templates/pcf-dataset/{strings → {{componentName}}/strings}/{{componentName}}.1033.resx +0 -0
- package/templates/pcf-field/{index.ts → {{componentName}}/index.ts} +0 -0
- package/templates/pcf-field/{strings → {{componentName}}/strings}/{{componentName}}.1033.resx +0 -0
- package/templates/pcf-field/{{{componentName}}Component.tsx → {{componentName}}/{{componentName}}Component.tsx} +0 -0
- package/templates/{power-pages-starter → power-pages-island}/gitignore +1 -1
package/dist/templatePlan.js
CHANGED
|
@@ -6,6 +6,23 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import fs from 'fs-extra';
|
|
8
8
|
import path from 'path';
|
|
9
|
+
/**
|
|
10
|
+
* Substitute `{{key}}` in a variant's `from`/`to` path.
|
|
11
|
+
*
|
|
12
|
+
* Needed because PCF templates put the control in a `{{componentName}}/` subfolder (the
|
|
13
|
+
* standard `pac pcf init` layout), while the `_variants/` pool stays at the project root
|
|
14
|
+
* so its paths remain static. So a variant reads from a fixed root path and writes into
|
|
15
|
+
* a per-scaffold directory: `to: '{{componentName}}/ValueInput.tsx'`.
|
|
16
|
+
*/
|
|
17
|
+
function substitutePath(rel, vars) {
|
|
18
|
+
if (!rel.includes('{{'))
|
|
19
|
+
return rel;
|
|
20
|
+
let out = rel;
|
|
21
|
+
for (const [key, value] of Object.entries(vars)) {
|
|
22
|
+
out = out.replace(new RegExp(`{{${key}}}`, 'g'), () => value);
|
|
23
|
+
}
|
|
24
|
+
return out;
|
|
25
|
+
}
|
|
9
26
|
/** Resolve a template-relative path, refusing anything that escapes the project dir. */
|
|
10
27
|
function safeJoin(root, rel) {
|
|
11
28
|
const resolvedRoot = path.resolve(root);
|
|
@@ -27,8 +44,8 @@ export async function applyTemplatePlan(projectPath, descriptor, vars) {
|
|
|
27
44
|
throw new Error(`No variant case for ${variant.choose}=${JSON.stringify(key)} ` +
|
|
28
45
|
`(available: ${Object.keys(variant.cases).join(', ')}).`);
|
|
29
46
|
}
|
|
30
|
-
const from = safeJoin(projectPath, source);
|
|
31
|
-
const to = safeJoin(projectPath, variant.to);
|
|
47
|
+
const from = safeJoin(projectPath, substitutePath(source, vars));
|
|
48
|
+
const to = safeJoin(projectPath, substitutePath(variant.to, vars));
|
|
32
49
|
if (!(await fs.pathExists(from))) {
|
|
33
50
|
throw new Error(`Variant source not found in template: ${source}`);
|
|
34
51
|
}
|
|
@@ -37,7 +54,7 @@ export async function applyTemplatePlan(projectPath, descriptor, vars) {
|
|
|
37
54
|
}
|
|
38
55
|
// 2. Prune scaffolding leftovers (e.g. the _variants/ directory).
|
|
39
56
|
for (const rel of descriptor.prune ?? []) {
|
|
40
|
-
await fs.remove(safeJoin(projectPath, rel));
|
|
57
|
+
await fs.remove(safeJoin(projectPath, substitutePath(rel, vars)));
|
|
41
58
|
}
|
|
42
59
|
}
|
|
43
60
|
//# sourceMappingURL=templatePlan.js.map
|
package/dist/templatePlan.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"templatePlan.js","sourceRoot":"","sources":["../src/templatePlan.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,EAAE,MAAM,UAAU,CAAC;AAC1B,OAAO,IAAI,MAAM,MAAM,CAAC;AAGxB,wFAAwF;AACxF,SAAS,QAAQ,CAAC,IAAY,EAAE,GAAW;IACzC,MAAM,YAAY,GAAG,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;IACxC,MAAM,IAAI,GAAG,IAAI,CAAC,OAAO,CAAC,YAAY,EAAE,GAAG,CAAC,CAAC;IAC7C,IAAI,IAAI,KAAK,YAAY,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,YAAY,GAAG,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC;QACvE,MAAM,IAAI,KAAK,CAAC,QAAQ,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,iCAAiC,CAAC,CAAC;IAChF,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,iBAAiB,CACrC,WAAmB,EACnB,UAA8B,EAC9B,IAA4B;IAE5B,yEAAyE;IACzE,KAAK,MAAM,OAAO,IAAI,UAAU,CAAC,QAAQ,IAAI,EAAE,EAAE,CAAC;QAChD,MAAM,GAAG,GAAG,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;QACjC,IAAI,GAAG,KAAK,SAAS,EAAE,CAAC;YACtB,MAAM,IAAI,KAAK,CACb,qBAAqB,OAAO,CAAC,MAAM,sDAAsD,CAC1F,CAAC;QACJ,CAAC;QACD,MAAM,MAAM,GAAG,OAAO,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;QAClC,IAAI,MAAM,KAAK,SAAS,EAAE,CAAC;YACzB,MAAM,IAAI,KAAK,CACb,uBAAuB,OAAO,CAAC,MAAM,IAAI,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,GAAG;gBAC7D,eAAe,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAC3D,CAAC;QACJ,CAAC;QACD,MAAM,IAAI,GAAG,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC,CAAC;
|
|
1
|
+
{"version":3,"file":"templatePlan.js","sourceRoot":"","sources":["../src/templatePlan.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,EAAE,MAAM,UAAU,CAAC;AAC1B,OAAO,IAAI,MAAM,MAAM,CAAC;AAGxB;;;;;;;GAOG;AACH,SAAS,cAAc,CAAC,GAAW,EAAE,IAA4B;IAC/D,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC,IAAI,CAAC;QAAE,OAAO,GAAG,CAAC;IACpC,IAAI,GAAG,GAAG,GAAG,CAAC;IACd,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC;QAChD,GAAG,GAAG,GAAG,CAAC,OAAO,CAAC,IAAI,MAAM,CAAC,KAAK,GAAG,IAAI,EAAE,GAAG,CAAC,EAAE,GAAG,EAAE,CAAC,KAAK,CAAC,CAAC;IAChE,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC;AAED,wFAAwF;AACxF,SAAS,QAAQ,CAAC,IAAY,EAAE,GAAW;IACzC,MAAM,YAAY,GAAG,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;IACxC,MAAM,IAAI,GAAG,IAAI,CAAC,OAAO,CAAC,YAAY,EAAE,GAAG,CAAC,CAAC;IAC7C,IAAI,IAAI,KAAK,YAAY,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,YAAY,GAAG,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC;QACvE,MAAM,IAAI,KAAK,CAAC,QAAQ,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,iCAAiC,CAAC,CAAC;IAChF,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,iBAAiB,CACrC,WAAmB,EACnB,UAA8B,EAC9B,IAA4B;IAE5B,yEAAyE;IACzE,KAAK,MAAM,OAAO,IAAI,UAAU,CAAC,QAAQ,IAAI,EAAE,EAAE,CAAC;QAChD,MAAM,GAAG,GAAG,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;QACjC,IAAI,GAAG,KAAK,SAAS,EAAE,CAAC;YACtB,MAAM,IAAI,KAAK,CACb,qBAAqB,OAAO,CAAC,MAAM,sDAAsD,CAC1F,CAAC;QACJ,CAAC;QACD,MAAM,MAAM,GAAG,OAAO,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;QAClC,IAAI,MAAM,KAAK,SAAS,EAAE,CAAC;YACzB,MAAM,IAAI,KAAK,CACb,uBAAuB,OAAO,CAAC,MAAM,IAAI,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,GAAG;gBAC7D,eAAe,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAC3D,CAAC;QACJ,CAAC;QACD,MAAM,IAAI,GAAG,QAAQ,CAAC,WAAW,EAAE,cAAc,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC,CAAC;QACjE,MAAM,EAAE,GAAG,QAAQ,CAAC,WAAW,EAAE,cAAc,CAAC,OAAO,CAAC,EAAE,EAAE,IAAI,CAAC,CAAC,CAAC;QACnE,IAAI,CAAC,CAAC,MAAM,EAAE,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC,EAAE,CAAC;YACjC,MAAM,IAAI,KAAK,CAAC,yCAAyC,MAAM,EAAE,CAAC,CAAC;QACrE,CAAC;QACD,MAAM,EAAE,CAAC,SAAS,CAAC,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC,CAAC;QACrC,MAAM,EAAE,CAAC,IAAI,CAAC,IAAI,EAAE,EAAE,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAC/C,CAAC;IAED,kEAAkE;IAClE,KAAK,MAAM,GAAG,IAAI,UAAU,CAAC,KAAK,IAAI,EAAE,EAAE,CAAC;QACzC,MAAM,EAAE,CAAC,MAAM,CAAC,QAAQ,CAAC,WAAW,EAAE,cAAc,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC,CAAC,CAAC;IACpE,CAAC;AACH,CAAC"}
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* placeholders in file contents and filenames.
|
|
4
4
|
*
|
|
5
5
|
* `templateDir` is the LITERAL directory name under `templates/` (e.g.
|
|
6
|
-
* `react-custom-page`, `power-pages-
|
|
6
|
+
* `react-custom-page`, `power-pages-island`, `pcf-field`) — resolved from the
|
|
7
7
|
* artifact registry's `templateDir`, with no `-starter` suffix heuristic.
|
|
8
8
|
* Substitution runs whenever `templateVariables` is provided (no per-template
|
|
9
9
|
* special-casing).
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"copyTemplate.d.ts","sourceRoot":"","sources":["../../src/utils/copyTemplate.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"copyTemplate.d.ts","sourceRoot":"","sources":["../../src/utils/copyTemplate.ts"],"names":[],"mappings":"AA+HA;;;;;;;;;GASG;AACH,wBAAsB,YAAY,CAChC,WAAW,EAAE,MAAM,EACnB,eAAe,EAAE,MAAM,EACvB,iBAAiB,CAAC,EAAE;IAAE,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAAA;CAAE,GAC5C,OAAO,CAAC,IAAI,CAAC,CA8Bf"}
|
|
@@ -46,6 +46,10 @@ async function safeRename(dirPath, fullPath, newName) {
|
|
|
46
46
|
const DOTFILE_RENAMES = {
|
|
47
47
|
gitignore: '.gitignore',
|
|
48
48
|
npmignore: '.npmignore',
|
|
49
|
+
// PCF templates: `pcf-scripts start` lints the control folder and dies with
|
|
50
|
+
// "No ESLint configuration found in <control>/" if none resolves. Shipped
|
|
51
|
+
// dotless for the same publish-safety reason as the two above.
|
|
52
|
+
'eslintrc.json': '.eslintrc.json',
|
|
49
53
|
};
|
|
50
54
|
async function renameDotfiles(dirPath) {
|
|
51
55
|
for (const file of await fs.readdir(dirPath, { withFileTypes: true })) {
|
|
@@ -107,7 +111,7 @@ async function replaceTemplateVariables(dirPath, variables) {
|
|
|
107
111
|
* placeholders in file contents and filenames.
|
|
108
112
|
*
|
|
109
113
|
* `templateDir` is the LITERAL directory name under `templates/` (e.g.
|
|
110
|
-
* `react-custom-page`, `power-pages-
|
|
114
|
+
* `react-custom-page`, `power-pages-island`, `pcf-field`) — resolved from the
|
|
111
115
|
* artifact registry's `templateDir`, with no `-starter` suffix heuristic.
|
|
112
116
|
* Substitution runs whenever `templateVariables` is provided (no per-template
|
|
113
117
|
* special-casing).
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"copyTemplate.js","sourceRoot":"","sources":["../../src/utils/copyTemplate.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,OAAO,EAAE,aAAa,EAAE,MAAM,KAAK,CAAC;AAEpC,MAAM,UAAU,GAAG,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AAClD,MAAM,SAAS,GAAG,IAAI,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC;AAE3C,oFAAoF;AACpF,MAAM,WAAW,GAAG,iBAAiB,CAAC;AAEtC;;;;GAIG;AACH,MAAM,iBAAiB,GAAG,IAAI,GAAG,CAAC;IAChC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO;IAChD,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM;CAClD,CAAC,CAAC;AAEH,0GAA0G;AAC1G,SAAS,cAAc,CAAC,IAAY,EAAE,SAAoC;IACxE,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC;QAAE,OAAO,IAAI,CAAC;IACtC,IAAI,GAAG,GAAG,IAAI,CAAC;IACf,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,SAAS,CAAC,EAAE,CAAC;QACrD,GAAG,GAAG,GAAG,CAAC,OAAO,CAAC,IAAI,MAAM,CAAC,KAAK,GAAG,IAAI,EAAE,GAAG,CAAC,EAAE,GAAG,EAAE,CAAC,KAAK,CAAC,CAAC;IAChE,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC;AAED,2FAA2F;AAC3F,KAAK,UAAU,UAAU,CACvB,OAAe,EACf,QAAgB,EAChB,OAAe;IAEf,IAAI,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAC,IAAI,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC;QAC9E,MAAM,IAAI,KAAK,CAAC,yCAAyC,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;IACtF,CAAC;IACD,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;IAC5C,MAAM,WAAW,GAAG,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC;IAC1C,IAAI,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,KAAK,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,OAAO,CAAC,EAAE,CAAC;QAC9D,MAAM,IAAI,KAAK,CAAC,wDAAwD,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;IACrG,CAAC;IACD,MAAM,EAAE,CAAC,MAAM,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC;AACrC,CAAC;AAED;;;;GAIG;AACH,MAAM,eAAe,GAA2B;IAC9C,SAAS,EAAE,YAAY;IACvB,SAAS,EAAE,YAAY;
|
|
1
|
+
{"version":3,"file":"copyTemplate.js","sourceRoot":"","sources":["../../src/utils/copyTemplate.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -2,10 +2,14 @@ import { IApiService } from "./IApiService";
|
|
|
2
2
|
import { logCrud } from "./crudLogging";
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
|
-
* Production API service for a model-driven
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* "accounts") so reads and writes use one consistent identifier.
|
|
5
|
+
* Production API service for an app hosted INSIDE a model-driven app — an HTML web resource on a
|
|
6
|
+
* form or behind a sitemap SubArea, or a PCF control. Every call hits the Dataverse Web API on the
|
|
7
|
+
* same origin (the hosting org authenticates via its session), addressing entities by their SET
|
|
8
|
+
* name (e.g. "accounts") so reads and writes use one consistent identifier.
|
|
9
|
+
*
|
|
10
|
+
* NOT for a Power Apps *custom page* hosted standalone: this class needs an `Xrm` object, and a
|
|
11
|
+
* canvasapp does not provide one. To put a component in a real custom page, use a PCF control —
|
|
12
|
+
* `context.webAPI`, or (verified live 2026-07-25) a same-origin fetch, both work there.
|
|
9
13
|
*/
|
|
10
14
|
export class XrmApiService implements IApiService {
|
|
11
15
|
private Xrm: any;
|
|
@@ -15,8 +19,25 @@ export class XrmApiService implements IApiService {
|
|
|
15
19
|
this.Xrm = Xrm;
|
|
16
20
|
}
|
|
17
21
|
|
|
22
|
+
/**
|
|
23
|
+
* Resolve the org URL. `Xrm.Utility.getGlobalContext()` is the supported API and is available in
|
|
24
|
+
* every model-driven host; `Xrm.Page` is deprecated and only exists on a FORM, so it is a fallback
|
|
25
|
+
* rather than the primary. Both are probed defensively: reaching straight for
|
|
26
|
+
* `Xrm.Page.context.getClientUrl()` throws a bare `TypeError` when the app is opened outside a form
|
|
27
|
+
* (e.g. the raw /WebResources/ URL), which tells the developer nothing about the actual cause.
|
|
28
|
+
*/
|
|
18
29
|
private clientUrl(): string {
|
|
19
|
-
|
|
30
|
+
const url =
|
|
31
|
+
this.Xrm?.Utility?.getGlobalContext?.()?.getClientUrl?.() ??
|
|
32
|
+
this.Xrm?.Page?.context?.getClientUrl?.();
|
|
33
|
+
if (!url) {
|
|
34
|
+
throw new Error(
|
|
35
|
+
"Could not resolve the Dataverse client URL from the Xrm object. This app must run inside a " +
|
|
36
|
+
"model-driven app (a web resource on a form or a sitemap SubArea). Opening the web resource " +
|
|
37
|
+
"URL directly gives it no parent Xrm.",
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
return url;
|
|
20
41
|
}
|
|
21
42
|
|
|
22
43
|
private headers(): HeadersInit {
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
# {{projectName}} — host-swap component
|
|
2
2
|
|
|
3
3
|
One React component, written once against an injected `IApiService`, that you can
|
|
4
|
-
host as a **PCF control
|
|
4
|
+
host as a **PCF control** or as a **web resource** (in two flavours — webpack, or Vite under the
|
|
5
|
+
legacy `custom-page` host name; ★ despite the name that host emits an HTML web resource, NOT a
|
|
6
|
+
Power Apps custom page — for a real one, use `--host pcf` and drop the control on the page).
|
|
5
7
|
You chose the host at scaffold time (`--host pcf`, `--host custom-page`, or
|
|
6
8
|
`--host web-resource`); to retarget, scaffold again with another host and copy your
|
|
7
9
|
`src/Component.tsx` across — the component never changes, only the thin wrapper does.
|
|
@@ -17,7 +19,7 @@ You chose the host at scaffold time (`--host pcf`, `--host custom-page`, or
|
|
|
17
19
|
|
|
18
20
|
## How each host supplies the data service
|
|
19
21
|
|
|
20
|
-
-
|
|
22
|
+
- **`custom-page` host (Vite web resource):** `App.tsx` resolves the Xrm context and `ServiceFactory`
|
|
21
23
|
picks Mock (localhost), the token-proxy (`npm run dev` with `DYNAMICS_URL`), or
|
|
22
24
|
the production `XrmApiService` (deployed).
|
|
23
25
|
- **PCF control (pcf-scripts):** the control builds the service directly — a
|
|
@@ -36,3 +38,56 @@ npm install
|
|
|
36
38
|
# pcf: npm run build (pcf-scripts build) | npm start (PCF test harness)
|
|
37
39
|
# web-resource: npm run build (webpack --mode production) | npm run dev (webpack dev server)
|
|
38
40
|
```
|
|
41
|
+
|
|
42
|
+
> ★ **`pcf-scripts build` exits 0 even when the build FAILS.** If you script this in CI, grep the
|
|
43
|
+
> log for `[build] Succeeded` and for the absence of `compiled with N errors` — don't trust `$?`.
|
|
44
|
+
|
|
45
|
+
## Deploy
|
|
46
|
+
|
|
47
|
+
The three hosts are **not** symmetric here. Only the PCF host has a first-class deploy path.
|
|
48
|
+
|
|
49
|
+
### PCF
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
pac pcf push --publisher-prefix <your-prefix>
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Builds, wraps the control in a temporary `PowerAppsToolsTemp_<prefix>` solution, imports it and
|
|
56
|
+
publishes. This needs the `.pcfproj` this template ships; its `ProjectGuid` is generated per
|
|
57
|
+
scaffold, so two controls from this template never collide inside one solution.
|
|
58
|
+
|
|
59
|
+
To ship versioned **managed + unmanaged** solution zips instead of a temp solution, wrap the
|
|
60
|
+
control in a solution project and build that:
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
pac solution init --publisher-name <you> --publisher-prefix <p>
|
|
64
|
+
pac solution add-reference --path .
|
|
65
|
+
dotnet build -c Release -p:SolutionPackageType=Both
|
|
66
|
+
pac solution import --path bin/Release/<Solution>.zip
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
(If you have the `claude-workspace` checkout, `msft/04-dynamics/scripts/deploy/package-control.sh
|
|
70
|
+
--source . --version 1.0.0 --output ./dist` automates exactly that — it discards this `.pcfproj`
|
|
71
|
+
and generates its own, so the two paths never conflict.)
|
|
72
|
+
|
|
73
|
+
★ **Bump `ControlManifest.Input.xml`'s `version=` on every change.** Neither command does it for
|
|
74
|
+
you, and without a bump the platform serves the cached previous build — the single most common
|
|
75
|
+
"my fix didn't deploy".
|
|
76
|
+
|
|
77
|
+
★ **The `<platform-library>` versions must be exact.** `pcf-scripts` matches a *range* and
|
|
78
|
+
silently remaps the bundle, but leaves your number in the manifest — so a wrong version **builds
|
|
79
|
+
green and fails to load in every host**. React must be `16.14.0`; Fluent `@fluentui/react` must be
|
|
80
|
+
`8.29.0` or `8.121.1`.
|
|
81
|
+
|
|
82
|
+
### Web resource
|
|
83
|
+
|
|
84
|
+
No deploy script ships. Build, then upload `dist/` as a JavaScript web resource — via the maker
|
|
85
|
+
portal, `pac webresource push`, or the workspace's `webresource-manager.sh` if you have that
|
|
86
|
+
checkout — and reference it from a form or the sitemap.
|
|
87
|
+
|
|
88
|
+
### Custom page
|
|
89
|
+
|
|
90
|
+
★ **No deploy path ships for this host.** Despite the name it emits a Vite-built **HTML web
|
|
91
|
+
resource**, not a Power Apps custom page — see the registry description. If you want an uploader,
|
|
92
|
+
the `react-custom-page` artifact has one (`tools/deploy/deploy-webresource.cjs`); if you want a
|
|
93
|
+
*real* model-driven custom page, use `--host pcf` and drop the control onto the page in Studio.
|
|
@@ -1,14 +1,18 @@
|
|
|
1
1
|
import * as React from "react";
|
|
2
2
|
import { ServiceFactory } from "./core/services/ServiceFactory";
|
|
3
3
|
import { Component } from "./Component";
|
|
4
|
+
import { useHostWidth } from "./useHostWidth";
|
|
4
5
|
|
|
5
|
-
// Resolve the Xrm context
|
|
6
|
-
// an iframe
|
|
7
|
-
// IApiService per environment: Mock on
|
|
8
|
-
// production XrmApiService when deployed.
|
|
6
|
+
// Resolve the Xrm context. This host is an HTML WEB RESOURCE (not a Power Apps
|
|
7
|
+
// custom page): it runs in an iframe inside a model-driven app, so Xrm comes from
|
|
8
|
+
// window.parent. ServiceFactory then picks the IApiService per environment: Mock on
|
|
9
|
+
// localhost, the token-proxy in dev, or the production XrmApiService when deployed.
|
|
9
10
|
const Xrm = (window.parent as any)?.Xrm ?? (window as any).Xrm;
|
|
10
11
|
|
|
11
12
|
export const App: React.FC = () => {
|
|
12
13
|
const api = React.useMemo(() => ServiceFactory.createApiService(Xrm), []);
|
|
13
|
-
|
|
14
|
+
// Embedded in a form iframe this is often narrow on a phone, so measure rather
|
|
15
|
+
// than assume desktop.
|
|
16
|
+
const width = useHostWidth();
|
|
17
|
+
return <Component api={api} width={width} />;
|
|
14
18
|
};
|
|
@@ -6,8 +6,32 @@
|
|
|
6
6
|
<property name="value" display-name-key="value_Display_Key" description-key="value_Desc_Key" of-type="SingleLine.Text" usage="bound" required="false" />
|
|
7
7
|
<resources>
|
|
8
8
|
<code path="index.ts" order="1"/>
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
<!--
|
|
10
|
+
★ These versions MUST come from Microsoft's supported platform-library list; arbitrary
|
|
11
|
+
versions do not fail the build, they fail the RUNTIME. `pcf-scripts` silently maps an
|
|
12
|
+
unsupported version to the nearest supported one when bundling (8.115.6 was emitted as
|
|
13
|
+
`external "FluentUIReactv81211"`), but the MANIFEST keeps the number the platform then
|
|
14
|
+
cannot resolve — so the control builds green and renders "Error loading control" in every
|
|
15
|
+
host. Live-diagnosed 2026-07-26: this template previously declared React 18.2.0 + Fluent
|
|
16
|
+
8.115.6, neither of which is supported, and could not load in a form OR a custom page.
|
|
17
|
+
Supported (learn.microsoft.com/power-apps/developer/component-framework/react-controls-platform-libraries):
|
|
18
|
+
React → 16.14.0 only (the platform loads 17.0.2 in model-driven, 16.14.0 in canvas)
|
|
19
|
+
Fluent → @fluentui/react 8.29.0 or 8.121.1; @fluentui/react-components >=9.4.0 <=9.46.2
|
|
20
|
+
v8 and v9 cannot both be declared in one manifest.
|
|
21
|
+
|
|
22
|
+
★ The rule is EQUALITY, not range membership. pcf-scripts matches a supported
|
|
23
|
+
{minVersion, maxVersion} range and remaps the bundle to that row's platformVersion,
|
|
24
|
+
but never rewrites this file — so a version inside the range yet unequal to its
|
|
25
|
+
platformVersion builds green and cannot be resolved at runtime. Asserted in
|
|
26
|
+
src/__tests__/registry.test.ts.
|
|
27
|
+
|
|
28
|
+
★ package.json intentionally carries react@^18 while this says 16.14.0: the shared
|
|
29
|
+
dataverse-seam pulls @khester/reusable-components, whose peer is react>=17. So the
|
|
30
|
+
types will NOT stop you writing a React 18 API that crashes only in a custom page
|
|
31
|
+
(canvas serves 16.14.0). Don't. See package.json `_comment_versions`.
|
|
32
|
+
-->
|
|
33
|
+
<platform-library name="React" version="16.14.0" />
|
|
34
|
+
<platform-library name="Fluent" version="8.121.1" />
|
|
11
35
|
<resx path="strings/{{componentName}}.1033.resx" version="1.0.0" />
|
|
12
36
|
</resources>
|
|
13
37
|
<feature-usage>
|
|
@@ -15,12 +15,18 @@ import { MockApiService } from "./src/core/services/MockApiService";
|
|
|
15
15
|
* the session cookie — no Xrm or clientUrl needed. On localhost (the pcf-scripts
|
|
16
16
|
* test harness) it falls back to the in-memory mock so the control renders with
|
|
17
17
|
* no org.
|
|
18
|
+
*
|
|
19
|
+
* ★ This holds in a model-driven CUSTOM PAGE too — verified live 2026-07-25: a
|
|
20
|
+
* custom page reports the org origin (not apps.powerapps.com) and a same-origin
|
|
21
|
+
* fetch returns 200, in a desktop browser and in the native Power Apps iOS app.
|
|
22
|
+
* That is observed behaviour rather than a documented contract, so if it ever
|
|
23
|
+
* regresses, swap this line for `context.webAPI` (the API Microsoft documents).
|
|
18
24
|
*/
|
|
19
25
|
export class {{componentName}} implements ComponentFramework.ReactControl<IInputs, IOutputs> {
|
|
20
26
|
private api!: IApiService;
|
|
21
27
|
|
|
22
28
|
public init(
|
|
23
|
-
|
|
29
|
+
context: ComponentFramework.Context<IInputs>,
|
|
24
30
|
_notifyOutputChanged: () => void,
|
|
25
31
|
_state: ComponentFramework.Dictionary,
|
|
26
32
|
): void {
|
|
@@ -28,10 +34,22 @@ export class {{componentName}} implements ComponentFramework.ReactControl<IInput
|
|
|
28
34
|
typeof window !== "undefined" &&
|
|
29
35
|
/^(localhost|127\.0\.0\.1)$/.test(window.location.hostname);
|
|
30
36
|
this.api = onLocalhost ? new MockApiService() : new FetchApiService("");
|
|
37
|
+
|
|
38
|
+
// Opt in to container-size reporting. WITHOUT THIS `allocatedWidth` never
|
|
39
|
+
// updates, so the control cannot respond to the device or to a resize — the
|
|
40
|
+
// usual reason a "responsive" PCF silently isn't.
|
|
41
|
+
context.mode.trackContainerResize(true);
|
|
31
42
|
}
|
|
32
43
|
|
|
33
|
-
public updateView(
|
|
34
|
-
|
|
44
|
+
public updateView(context: ComponentFramework.Context<IInputs>): React.ReactElement {
|
|
45
|
+
// Pass the measured width down. Do NOT use context.client.getFormFactor():
|
|
46
|
+
// verified live on a physical iPhone in the native Power Apps app, a custom
|
|
47
|
+
// page reports formFactor 1 (Desktop) and client "Web", so form-factor
|
|
48
|
+
// branching hands every phone the desktop layout.
|
|
49
|
+
return React.createElement(Component, {
|
|
50
|
+
api: this.api,
|
|
51
|
+
width: context.mode.allocatedWidth,
|
|
52
|
+
});
|
|
35
53
|
}
|
|
36
54
|
|
|
37
55
|
public getOutputs(): IOutputs {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "{{projectName}}",
|
|
3
3
|
"version": "1.0.0",
|
|
4
|
-
"description": "{{componentName}}
|
|
4
|
+
"description": "{{componentName}} \u2014 host-swap component hosted as a PCF control",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"build": "pcf-scripts build",
|
|
7
7
|
"clean": "pcf-scripts clean",
|
|
@@ -9,8 +9,24 @@
|
|
|
9
9
|
"start": "pcf-scripts start",
|
|
10
10
|
"refreshTypes": "pcf-scripts refreshTypes"
|
|
11
11
|
},
|
|
12
|
+
"_comment_versions": [
|
|
13
|
+
"\u2605 @fluentui/react is PINNED EXACTLY to the <platform-library> version in",
|
|
14
|
+
"ControlManifest.Input.xml and must move with it. This is a VIRTUAL control: react, react-dom",
|
|
15
|
+
"and @fluentui/react are externalized by pcf-scripts and supplied by the platform at runtime \u2014",
|
|
16
|
+
"and by pcf-start in the harness \u2014 so none of them reaches the bundle. That makes the version a",
|
|
17
|
+
"TYPES decision: the compiler should see the API the platform will actually serve. A caret",
|
|
18
|
+
"defeats it \u2014 ^8.120.0 resolved to 8.125.7 while the platform served 8.121.1.",
|
|
19
|
+
"",
|
|
20
|
+
"\u2605 react/react-dom stay at ^18 on purpose: pinning them to the manifest's 16.14.0 fails",
|
|
21
|
+
"npm install ERESOLVE, because the shared dataverse-seam imports @khester/reusable-components",
|
|
22
|
+
"(peer react>=17). But the @types ARE pinned to 16, which is what actually matters here \u2014 the",
|
|
23
|
+
"canvas runtime behind a custom page serves React 16.14.0, so useId/useTransition/",
|
|
24
|
+
"useSyncExternalStore must be a COMPILE error rather than a crash that only reproduces on a",
|
|
25
|
+
"phone. Since nothing from these packages is bundled, the runtime versions are inert and the",
|
|
26
|
+
"types carry the contract."
|
|
27
|
+
],
|
|
12
28
|
"dependencies": {
|
|
13
|
-
"@fluentui/react": "
|
|
29
|
+
"@fluentui/react": "8.121.1",
|
|
14
30
|
"@types/powerapps-component-framework": "^1.3.18",
|
|
15
31
|
"react": "^18.2.0",
|
|
16
32
|
"react-dom": "^18.2.0"
|
|
@@ -18,8 +34,8 @@
|
|
|
18
34
|
"devDependencies": {
|
|
19
35
|
"@microsoft/eslint-config-spfx": "^1.18.2",
|
|
20
36
|
"@types/node": "^18.16.9",
|
|
21
|
-
"@types/react": "^
|
|
22
|
-
"@types/react-dom": "^
|
|
37
|
+
"@types/react": "^16.14.0",
|
|
38
|
+
"@types/react-dom": "^16.9.0",
|
|
23
39
|
"eslint-plugin-react-hooks": "^4.6.0",
|
|
24
40
|
"pcf-scripts": "^1",
|
|
25
41
|
"pcf-start": "^1",
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="utf-8"?>
|
|
2
|
+
<!--
|
|
3
|
+
MSBuild project for the PCF control. Without this file `pac pcf push` fails
|
|
4
|
+
outright, so a scaffolded control has no deploy path at all — which is exactly
|
|
5
|
+
what this template shipped before.
|
|
6
|
+
|
|
7
|
+
Two ways to deploy (see the README's Deploy section):
|
|
8
|
+
• pac pcf push --publisher-prefix <p> — fastest dev loop; wraps the control
|
|
9
|
+
in a temporary PowerAppsToolsTemp_<p> solution and imports it.
|
|
10
|
+
• package-control.sh --source <dir> — emits versioned managed/unmanaged
|
|
11
|
+
solution zips with no temp solution. That script DISCARDS this file and
|
|
12
|
+
generates its own, so the two never conflict.
|
|
13
|
+
|
|
14
|
+
ProjectGuid is generated per scaffold, so two controls from this template never collide
|
|
15
|
+
inside one solution.
|
|
16
|
+
-->
|
|
17
|
+
<Project ToolsVersion="15.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
|
18
|
+
<PropertyGroup>
|
|
19
|
+
<PowerAppsTargetsPath>$(MSBuildExtensionsPath)\Microsoft\VisualStudio\v$(VisualStudioVersion)\PowerApps</PowerAppsTargetsPath>
|
|
20
|
+
</PropertyGroup>
|
|
21
|
+
|
|
22
|
+
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" />
|
|
23
|
+
<Import Project="$(PowerAppsTargetsPath)\Microsoft.PowerApps.VisualStudio.Pcf.props" Condition="Exists('$(PowerAppsTargetsPath)\Microsoft.PowerApps.VisualStudio.Pcf.props')" />
|
|
24
|
+
|
|
25
|
+
<PropertyGroup>
|
|
26
|
+
<Name>{{componentName}}</Name>
|
|
27
|
+
<ProjectGuid>{{{projectGuid}}}</ProjectGuid>
|
|
28
|
+
<OutputPath>$(MSBuildThisFileDirectory)out\controls</OutputPath>
|
|
29
|
+
</PropertyGroup>
|
|
30
|
+
|
|
31
|
+
<PropertyGroup>
|
|
32
|
+
<TargetFrameworkVersion>v4.6.2</TargetFrameworkVersion>
|
|
33
|
+
<TargetFramework>net462</TargetFramework>
|
|
34
|
+
<RestoreProjectStyle>PackageReference</RestoreProjectStyle>
|
|
35
|
+
</PropertyGroup>
|
|
36
|
+
|
|
37
|
+
<ItemGroup>
|
|
38
|
+
<PackageReference Include="Microsoft.PowerApps.MSBuild.Pcf" Version="1.*" />
|
|
39
|
+
<!-- Supplies the net462 reference assemblies on macOS/Linux so this builds
|
|
40
|
+
without a Windows image (otherwise MSB3644). -->
|
|
41
|
+
<PackageReference Include="Microsoft.NETFramework.ReferenceAssemblies" Version="1.0.3" PrivateAssets="All" />
|
|
42
|
+
</ItemGroup>
|
|
43
|
+
|
|
44
|
+
<ItemGroup>
|
|
45
|
+
<ExcludeDirectories Include="$(MSBuildThisFileDirectory)\.gitignore" />
|
|
46
|
+
<!-- A scaffolded project always has a nested .git (initGit runs unless --skip-git); the
|
|
47
|
+
probe this was modelled on lives inside a repo and never had one. package-control.sh
|
|
48
|
+
rm -rf's it before building, which is the same signal. -->
|
|
49
|
+
<ExcludeDirectories Include="$(MSBuildThisFileDirectory)\.git\**" />
|
|
50
|
+
<ExcludeDirectories Include="$(MSBuildThisFileDirectory)\.vs\**" />
|
|
51
|
+
<ExcludeDirectories Include="$(MSBuildThisFileDirectory)\bin\**" />
|
|
52
|
+
<ExcludeDirectories Include="$(MSBuildThisFileDirectory)\obj\**" />
|
|
53
|
+
<ExcludeDirectories Include="$(OutputPath)\**" />
|
|
54
|
+
<ExcludeDirectories Include="$(MSBuildThisFileDirectory)\*.pcfproj" />
|
|
55
|
+
<ExcludeDirectories Include="$(MSBuildThisFileDirectory)\*.pcfproj.user" />
|
|
56
|
+
<ExcludeDirectories Include="$(MSBuildThisFileDirectory)\*.sln" />
|
|
57
|
+
<ExcludeDirectories Include="$(MSBuildThisFileDirectory)\node_modules\**" />
|
|
58
|
+
</ItemGroup>
|
|
59
|
+
|
|
60
|
+
<ItemGroup>
|
|
61
|
+
<None Include="$(MSBuildThisFileDirectory)\**" Exclude="@(ExcludeDirectories)" />
|
|
62
|
+
</ItemGroup>
|
|
63
|
+
|
|
64
|
+
<Import Project="$(MSBuildToolsPath)\Microsoft.Common.targets" />
|
|
65
|
+
<Import Project="$(PowerAppsTargetsPath)\Microsoft.PowerApps.VisualStudio.Pcf.targets" Condition="Exists('$(PowerAppsTargetsPath)\Microsoft.PowerApps.VisualStudio.Pcf.targets')" />
|
|
66
|
+
|
|
67
|
+
</Project>
|
|
@@ -3,6 +3,7 @@ import type { IApiService } from "./core/services/IApiService";
|
|
|
3
3
|
import { FetchApiService } from "./core/services/FetchApiService";
|
|
4
4
|
import { MockApiService } from "./core/services/MockApiService";
|
|
5
5
|
import { Component } from "./Component";
|
|
6
|
+
import { useHostWidth } from "./useHostWidth";
|
|
6
7
|
|
|
7
8
|
// A web resource is served from the org origin, so a same-origin relative
|
|
8
9
|
// /api/data/v9.2 fetch is authenticated by the session cookie — no Xrm or
|
|
@@ -14,5 +15,8 @@ export const App: React.FC = () => {
|
|
|
14
15
|
const onLocalhost = /^(localhost|127\.0\.0\.1)$/.test(window.location.hostname);
|
|
15
16
|
return onLocalhost ? new MockApiService() : new FetchApiService("");
|
|
16
17
|
}, []);
|
|
17
|
-
|
|
18
|
+
// A web resource embedded on a form is often narrow on a phone, so measure the
|
|
19
|
+
// viewport rather than assuming desktop.
|
|
20
|
+
const width = useHostWidth();
|
|
21
|
+
return <Component api={api} width={width} />;
|
|
18
22
|
};
|
|
@@ -17,14 +17,33 @@ initializeIcons();
|
|
|
17
17
|
|
|
18
18
|
/**
|
|
19
19
|
* The ONE host-agnostic component. It depends on nothing but an injected
|
|
20
|
-
* `IApiService` — no PCF `context`, no Xrm, no
|
|
21
|
-
* the SAME source renders unchanged inside a
|
|
22
|
-
*
|
|
20
|
+
* `IApiService` and a plain number for its width — no PCF `context`, no Xrm, no
|
|
21
|
+
* lifecycle, no bundler globals — so the SAME source renders unchanged inside a
|
|
22
|
+
* PCF control, a web resource, and a model-driven custom page. Each host's thin
|
|
23
|
+
* wrapper builds the right `IApiService` and supplies the width.
|
|
23
24
|
*/
|
|
24
25
|
export interface ComponentProps {
|
|
25
26
|
api: IApiService;
|
|
27
|
+
/**
|
|
28
|
+
* Width available to the control, in px. Hosts that can measure it should pass it
|
|
29
|
+
* (the PCF wrapper passes `context.mode.allocatedWidth`); when omitted the layout
|
|
30
|
+
* falls back to the roomy variant.
|
|
31
|
+
*
|
|
32
|
+
* ★ Deliberately a NUMBER, not a form factor. Do NOT branch on
|
|
33
|
+
* `context.client.getFormFactor()` / `getClient()`: verified live 2026-07-25 on a
|
|
34
|
+
* physical iPhone in the native Power Apps app, a custom page reports
|
|
35
|
+
* client "Web" and formFactor 1 (Desktop). Branching on those silently gives every
|
|
36
|
+
* phone the desktop layout, and passes every test not run on a real device.
|
|
37
|
+
*/
|
|
38
|
+
width?: number;
|
|
26
39
|
}
|
|
27
40
|
|
|
41
|
+
/**
|
|
42
|
+
* Below this the Fluent `DetailsList` stops being usable — columns collapse and
|
|
43
|
+
* horizontal scrolling takes over — so narrow hosts get a stacked card list instead.
|
|
44
|
+
*/
|
|
45
|
+
const NARROW_BREAKPOINT_PX = 480;
|
|
46
|
+
|
|
28
47
|
const ACCOUNTS_FETCHXML = [
|
|
29
48
|
'<fetch top="25">',
|
|
30
49
|
' <entity name="account">',
|
|
@@ -47,10 +66,14 @@ const COLUMNS: IColumn[] = [
|
|
|
47
66
|
{ key: "telephone1", name: "Phone", fieldName: "telephone1", minWidth: 140, isResizable: true },
|
|
48
67
|
];
|
|
49
68
|
|
|
50
|
-
export const Component: React.FC<ComponentProps> = ({ api }) => {
|
|
69
|
+
export const Component: React.FC<ComponentProps> = ({ api, width }) => {
|
|
51
70
|
const [rows, setRows] = React.useState<AccountRow[] | null>(null);
|
|
52
71
|
const [error, setError] = React.useState<string | null>(null);
|
|
53
72
|
|
|
73
|
+
// `width === undefined` means the host cannot measure itself; prefer the roomy
|
|
74
|
+
// layout rather than guessing narrow.
|
|
75
|
+
const isNarrow = width !== undefined && width < NARROW_BREAKPOINT_PX;
|
|
76
|
+
|
|
54
77
|
React.useEffect(() => {
|
|
55
78
|
let cancelled = false;
|
|
56
79
|
api
|
|
@@ -70,12 +93,13 @@ export const Component: React.FC<ComponentProps> = ({ api }) => {
|
|
|
70
93
|
<Stack tokens={{ childrenGap: 12 }} styles={{ root: { padding: 16 } }}>
|
|
71
94
|
<Text variant="xLarge">Accounts</Text>
|
|
72
95
|
<Text variant="small">
|
|
73
|
-
One host-agnostic component — the same source renders in a PCF control
|
|
74
|
-
|
|
96
|
+
One host-agnostic component — the same source renders in a PCF control, a web
|
|
97
|
+
resource and a model-driven custom page; only the thin host wrapper changes.
|
|
98
|
+
{width !== undefined && ` (${isNarrow ? "narrow" : "wide"} layout · ${width}px)`}
|
|
75
99
|
</Text>
|
|
76
100
|
{error && <MessageBar messageBarType={MessageBarType.error}>{error}</MessageBar>}
|
|
77
101
|
{!rows && !error && <Spinner label="Loading accounts…" />}
|
|
78
|
-
{rows && (
|
|
102
|
+
{rows && !isNarrow && (
|
|
79
103
|
<DetailsList
|
|
80
104
|
items={rows}
|
|
81
105
|
columns={COLUMNS}
|
|
@@ -84,6 +108,29 @@ export const Component: React.FC<ComponentProps> = ({ api }) => {
|
|
|
84
108
|
selectionMode={SelectionMode.none}
|
|
85
109
|
/>
|
|
86
110
|
)}
|
|
111
|
+
{rows && isNarrow && (
|
|
112
|
+
<Stack tokens={{ childrenGap: 8 }}>
|
|
113
|
+
{rows.map((row) => (
|
|
114
|
+
<Stack
|
|
115
|
+
key={row.accountid}
|
|
116
|
+
tokens={{ childrenGap: 2 }}
|
|
117
|
+
styles={{
|
|
118
|
+
root: {
|
|
119
|
+
padding: 12,
|
|
120
|
+
border: "1px solid #edebe9",
|
|
121
|
+
borderRadius: 4,
|
|
122
|
+
// Long account names must wrap rather than force the card wider
|
|
123
|
+
// than the phone — the failure mode DetailsList has here.
|
|
124
|
+
wordBreak: "break-word",
|
|
125
|
+
},
|
|
126
|
+
}}
|
|
127
|
+
>
|
|
128
|
+
<Text variant="mediumPlus">{row.name ?? "(no name)"}</Text>
|
|
129
|
+
<Text variant="small">{row.telephone1 ?? "—"}</Text>
|
|
130
|
+
</Stack>
|
|
131
|
+
))}
|
|
132
|
+
</Stack>
|
|
133
|
+
)}
|
|
87
134
|
</Stack>
|
|
88
135
|
);
|
|
89
136
|
};
|