@khester/create-dynamics-app 2.1.0 → 2.2.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.
Files changed (121) hide show
  1. package/dist/artifacts/registry.d.ts +4 -3
  2. package/dist/artifacts/registry.d.ts.map +1 -1
  3. package/dist/artifacts/registry.js +121 -11
  4. package/dist/artifacts/registry.js.map +1 -1
  5. package/dist/artifacts/types.d.ts +1 -1
  6. package/dist/artifacts/types.d.ts.map +1 -1
  7. package/dist/index.js +2 -1
  8. package/dist/index.js.map +1 -1
  9. package/dist/injectDevTools.d.ts.map +1 -1
  10. package/dist/injectDevTools.js +4 -2
  11. package/dist/injectDevTools.js.map +1 -1
  12. package/dist/scaffold.d.ts +1 -0
  13. package/dist/scaffold.d.ts.map +1 -1
  14. package/dist/scaffold.js +3 -1
  15. package/dist/scaffold.js.map +1 -1
  16. package/package.json +3 -2
  17. package/templates/grid-starter/ARCHITECTURE.md +66 -0
  18. package/templates/grid-starter/README.md +122 -0
  19. package/templates/grid-starter/env.example +16 -0
  20. package/templates/grid-starter/gitignore +6 -0
  21. package/templates/grid-starter/index.html +16 -0
  22. package/templates/grid-starter/package.json +39 -0
  23. package/templates/grid-starter/src/App.tsx +23 -0
  24. package/templates/grid-starter/src/core/services/FetchApiService.ts +117 -0
  25. package/templates/grid-starter/src/core/services/IApiService.ts +37 -0
  26. package/templates/grid-starter/src/core/services/MockApiService.ts +72 -0
  27. package/templates/grid-starter/src/core/services/ServiceFactory.ts +58 -0
  28. package/templates/grid-starter/src/core/services/XrmApiService.ts +135 -0
  29. package/templates/grid-starter/src/core/services/crudLogging.ts +52 -0
  30. package/templates/grid-starter/src/dev-tools/DevPanel.tsx +239 -0
  31. package/templates/grid-starter/src/grid/GridPage.tsx +119 -0
  32. package/templates/grid-starter/src/index.tsx +18 -0
  33. package/templates/grid-starter/src/vite-env.d.ts +15 -0
  34. package/templates/grid-starter/tools/deploy/deploy-webresource.cjs +117 -0
  35. package/templates/grid-starter/tsconfig.json +19 -0
  36. package/templates/grid-starter/vite.config.ts +76 -0
  37. package/templates/pcf-field/_variants/ValueInput.boolean.tsx +2 -0
  38. package/templates/pcf-field/_variants/ValueInput.date.tsx +2 -0
  39. package/templates/pcf-field/_variants/ValueInput.number.tsx +2 -0
  40. package/templates/pcf-field/_variants/ValueInput.optionset.tsx +77 -0
  41. package/templates/pcf-field/_variants/ValueInput.text.tsx +2 -0
  42. package/templates/pcf-field/index.ts +1 -1
  43. package/templates/pcf-field/package.json +3 -1
  44. package/templates/pcf-field/{{componentName}}Component.tsx +2 -0
  45. package/templates/react-custom-page/ARCHITECTURE.md +75 -0
  46. package/templates/react-custom-page/README.md +74 -568
  47. package/templates/react-custom-page/env.example +16 -0
  48. package/templates/react-custom-page/gitignore +1 -0
  49. package/templates/react-custom-page/index.html +16 -0
  50. package/templates/react-custom-page/package.json +21 -49
  51. package/templates/react-custom-page/src/App.tsx +26 -0
  52. package/templates/react-custom-page/src/core/recordContext.test.ts +30 -0
  53. package/templates/react-custom-page/src/core/recordContext.ts +51 -0
  54. package/templates/react-custom-page/src/core/services/FetchApiService.ts +117 -0
  55. package/templates/react-custom-page/src/core/services/IApiService.ts +37 -0
  56. package/templates/react-custom-page/src/core/services/MockApiService.ts +73 -0
  57. package/templates/react-custom-page/src/core/services/ServiceFactory.ts +58 -0
  58. package/templates/react-custom-page/src/core/services/XrmApiService.ts +135 -0
  59. package/templates/react-custom-page/src/core/services/crudLogging.ts +52 -0
  60. package/templates/react-custom-page/src/dev-tools/DevPanel.tsx +238 -0
  61. package/templates/react-custom-page/src/domain/diff.test.ts +87 -0
  62. package/templates/react-custom-page/src/domain/diff.ts +38 -0
  63. package/templates/react-custom-page/src/example/ExamplePage.tsx +140 -0
  64. package/templates/react-custom-page/src/example/exampleError.ts +36 -0
  65. package/templates/react-custom-page/src/example/hooks/useExampleData.ts +40 -0
  66. package/templates/react-custom-page/src/example/hooks/useExampleForm.ts +99 -0
  67. package/templates/react-custom-page/src/example/mappers/accountMapper.test.ts +38 -0
  68. package/templates/react-custom-page/src/example/mappers/accountMapper.ts +55 -0
  69. package/templates/react-custom-page/src/example/models/Account.ts +74 -0
  70. package/templates/react-custom-page/src/index.tsx +18 -128
  71. package/templates/react-custom-page/src/vite-env.d.ts +15 -0
  72. package/templates/react-custom-page/tools/deploy/deploy-webresource.cjs +117 -0
  73. package/templates/react-custom-page/tsconfig.json +12 -22
  74. package/templates/react-custom-page/vite.config.ts +76 -0
  75. package/templates/starter-page/README.md +38 -0
  76. package/templates/starter-page/_variants/App.dashboard.v8.tsx +46 -0
  77. package/templates/starter-page/_variants/App.form.v8.tsx +59 -0
  78. package/templates/starter-page/_variants/App.master-detail.v8.tsx +61 -0
  79. package/templates/starter-page/_variants/App.panel.v8.tsx +99 -0
  80. package/templates/starter-page/gitignore +5 -0
  81. package/templates/starter-page/package.json +27 -0
  82. package/templates/starter-page/public/index.html +11 -0
  83. package/templates/starter-page/src/index.tsx +10 -0
  84. package/templates/starter-page/src/services/dataverse.ts +30 -0
  85. package/templates/starter-page/tsconfig.json +15 -0
  86. package/templates/starter-page/webpack.config.js +17 -0
  87. package/templates/react-custom-page/deployment/README.md +0 -484
  88. package/templates/react-custom-page/docs/ARCHITECTURE_OVERVIEW.md +0 -506
  89. package/templates/react-custom-page/docs/BEST_PRACTICES.md +0 -723
  90. package/templates/react-custom-page/docs/MIGRATION_GUIDE.md +0 -447
  91. package/templates/react-custom-page/public/index.html +0 -15
  92. package/templates/react-custom-page/scripts/custom-build.js +0 -255
  93. package/templates/react-custom-page/src/components/AccountForm.css +0 -71
  94. package/templates/react-custom-page/src/components/AccountForm.tsx +0 -541
  95. package/templates/react-custom-page/src/components/AccountManagement.css +0 -86
  96. package/templates/react-custom-page/src/components/AccountManagement.tsx +0 -370
  97. package/templates/react-custom-page/src/components/ContactForm.css +0 -48
  98. package/templates/react-custom-page/src/components/ContactForm.tsx +0 -327
  99. package/templates/react-custom-page/src/components/ContactManagement.css +0 -86
  100. package/templates/react-custom-page/src/components/ContactManagement.tsx +0 -357
  101. package/templates/react-custom-page/src/components/Logging/LogDialog.tsx +0 -291
  102. package/templates/react-custom-page/src/components/Logging/LoggingContext.tsx +0 -166
  103. package/templates/react-custom-page/src/components/Logging/LoggingDebugPanel.css +0 -192
  104. package/templates/react-custom-page/src/components/Logging/LoggingDebugPanel.tsx +0 -177
  105. package/templates/react-custom-page/src/components/Logging/LoggingProvider.tsx +0 -3
  106. package/templates/react-custom-page/src/components/Logging/logger.ts +0 -193
  107. package/templates/react-custom-page/src/constants/account.ts +0 -410
  108. package/templates/react-custom-page/src/constants/contact.ts +0 -362
  109. package/templates/react-custom-page/src/models/Account.ts +0 -480
  110. package/templates/react-custom-page/src/models/BaseEntity.ts +0 -204
  111. package/templates/react-custom-page/src/models/Contact.ts +0 -580
  112. package/templates/react-custom-page/src/pcf/ContactControlWrapper.tsx +0 -107
  113. package/templates/react-custom-page/src/pcf/MultiEntityControlWrapper.tsx +0 -205
  114. package/templates/react-custom-page/src/providers/DynamicsProvider.tsx +0 -353
  115. package/templates/react-custom-page/src/services/MockApiService.ts +0 -260
  116. package/templates/react-custom-page/src/services/ServiceFactory.ts +0 -65
  117. package/templates/react-custom-page/src/services/XrmApiService.ts +0 -213
  118. package/templates/react-custom-page/src/styles/index.css +0 -171
  119. package/templates/react-custom-page/tools/metadata-sync/index.js +0 -152
  120. package/templates/react-custom-page/webpack.config.js +0 -57
  121. /package/templates/_shared/dev-tools/auth/{get-token.js → get-token.cjs} +0 -0
@@ -7,6 +7,8 @@ export interface ValueInputProps {
7
7
  disabled?: boolean;
8
8
  onChange: (value: number | null) => void;
9
9
  onBlur: () => void;
10
+ /** Optional PCF context (forwarded uniformly; only the optionset variant reads it). */
11
+ context?: ComponentFramework.Context<any>;
10
12
  }
11
13
 
12
14
  export const ValueInput: React.FC<ValueInputProps> = ({
@@ -0,0 +1,77 @@
1
+ import * as React from 'react';
2
+ import { Dropdown, IDropdownOption } from '@fluentui/react/lib/Dropdown';
3
+ import { TextField } from '@fluentui/react/lib/TextField';
4
+
5
+ export interface ValueInputProps {
6
+ value: number | null;
7
+ placeholder?: string;
8
+ disabled?: boolean;
9
+ onChange: (value: number | null) => void;
10
+ onBlur: () => void;
11
+ /**
12
+ * The bound PCF context. OptionSet choices are read at runtime from the bound
13
+ * column's metadata: `context.parameters.value.attributes.Options` is the
14
+ * platform-supplied `Array<{ Value: number; Label: string }>` (note the
15
+ * UPPERCASE field names — this is the live PCF API, not the form-builder's
16
+ * design-time config). `context` is typed `<any>`, so this access is
17
+ * unchecked: keep `Options` / `Value` / `Label` spelled exactly.
18
+ */
19
+ context?: ComponentFramework.Context<any>;
20
+ }
21
+
22
+ export const ValueInput: React.FC<ValueInputProps> = ({
23
+ value,
24
+ placeholder,
25
+ disabled,
26
+ onChange,
27
+ onBlur,
28
+ context,
29
+ }) => {
30
+ const rawOptions = context?.parameters?.value?.attributes?.Options as
31
+ | Array<{ Value: number; Label: string }>
32
+ | undefined;
33
+ const options: IDropdownOption[] = (rawOptions ?? []).map(
34
+ (o: { Value: number; Label: string }) => ({ key: o.Value, text: o.Label })
35
+ );
36
+
37
+ // Degrade to a numeric input when no choice metadata is available — e.g. an
38
+ // unbound control or a test harness. An empty dropdown is unusable, so we fall
39
+ // back to a raw option-value number entry (mirrors the form-builder's
40
+ // optionset → number convention). NOTE: an empty `Options` is ambiguous — it
41
+ // means either "genuinely no choices" or "wrong metadata path"; the unit test
42
+ // pins the path so only the legitimate case reaches here.
43
+ if (options.length === 0) {
44
+ return (
45
+ <TextField
46
+ type="number"
47
+ value={value === null ? '' : String(value)}
48
+ placeholder={placeholder}
49
+ disabled={disabled}
50
+ onChange={(_, v) => {
51
+ if (!v) {
52
+ onChange(null);
53
+ return;
54
+ }
55
+ const n = Number(v);
56
+ onChange(Number.isNaN(n) ? null : n);
57
+ }}
58
+ onBlur={onBlur}
59
+ styles={{ root: { width: '100%' } }}
60
+ />
61
+ );
62
+ }
63
+
64
+ return (
65
+ <Dropdown
66
+ selectedKey={value ?? undefined}
67
+ placeholder={placeholder}
68
+ disabled={disabled}
69
+ options={options}
70
+ onChange={(_, option) =>
71
+ onChange(typeof option?.key === 'number' ? option.key : null)
72
+ }
73
+ onBlur={onBlur}
74
+ styles={{ root: { width: '100%' } }}
75
+ />
76
+ );
77
+ };
@@ -7,6 +7,8 @@ export interface ValueInputProps {
7
7
  disabled?: boolean;
8
8
  onChange: (value: string) => void;
9
9
  onBlur: () => void;
10
+ /** Optional PCF context (forwarded uniformly; only the optionset variant reads it). */
11
+ context?: ComponentFramework.Context<any>;
10
12
  }
11
13
 
12
14
  export const ValueInput: React.FC<ValueInputProps> = ({
@@ -51,7 +51,7 @@ export class {{componentName}} implements ComponentFramework.StandardControl<IIn
51
51
 
52
52
  public getOutputs(): IOutputs {
53
53
  return {
54
- value: this._value
54
+ value: this._value ?? undefined
55
55
  };
56
56
  }
57
57
 
@@ -11,7 +11,9 @@
11
11
  },
12
12
  "dependencies": {
13
13
  "@fluentui/react": "^8.110.10",
14
- "@types/powerapps-component-framework": "^1.3.4"
14
+ "@types/powerapps-component-framework": "^1.3.4",
15
+ "react": "^16.14.0",
16
+ "react-dom": "^16.14.0"
15
17
  },
16
18
  "devDependencies": {
17
19
  "@microsoft/eslint-config-spfx": "^1.18.2",
@@ -20,6 +20,7 @@ export const {{componentName}}Component: React.FC<I{{componentName}}ComponentPro
20
20
  disabled,
21
21
  onChange,
22
22
  onBlur,
23
+ context,
23
24
  }) => {
24
25
  return (
25
26
  <div style={{ width: '100%', height: '100%' }}>
@@ -29,6 +30,7 @@ export const {{componentName}}Component: React.FC<I{{componentName}}ComponentPro
29
30
  disabled={disabled}
30
31
  onChange={onChange}
31
32
  onBlur={onBlur}
33
+ context={context}
32
34
  />
33
35
  </div>
34
36
  );
@@ -0,0 +1,75 @@
1
+ # Architecture
2
+
3
+ A small, layered custom page. The goal is that **business logic lives in pure,
4
+ testable functions** and the React component stays thin.
5
+
6
+ ## Layers
7
+
8
+ ```
9
+ @khester/reusable-components UI primitives + theme + logging (npm dependency)
10
+ │ (imported)
11
+
12
+ src/example/ ExamplePage.tsx view — JSX only, no CRUD
13
+ hooks/ view-models — state, handlers, dirty tracking
14
+ mappers/ PURE entity <-> form translation (unit-tested)
15
+ models/Account.ts where CRUD lives (calls IApiService)
16
+
17
+
18
+ src/domain/ diff.ts PURE change detection (unit-tested)
19
+
20
+
21
+ src/core/services/ IApiService the data-access contract
22
+ ServiceFactory picks one impl per environment (the seam)
23
+ Mock / Fetch / Xrm three implementations
24
+ ```
25
+
26
+ **Dependency rule (one direction):** view → hook → pure domain/mapper → model →
27
+ `IApiService`. Nothing below imports from `example/`; `core/` imports nothing app-
28
+ specific. UI/theme/logging are consumed from the package, never reached into.
29
+
30
+ ## The thin-form pattern
31
+
32
+ | Concern | Lives in | Why |
33
+ |---------|----------|-----|
34
+ | Rendering | `ExamplePage.tsx` | swap freely; no logic to retest |
35
+ | State + handlers | `hooks/useExampleForm` | view-model, not the view |
36
+ | Load | `hooks/useExampleData` | data fetching isolated from form state |
37
+ | Field ↔ attribute mapping | `mappers/accountMapper` | pure → cheap to unit-test |
38
+ | Change detection | `domain/diff` | pure; sends only changed fields to Dataverse |
39
+ | CRUD | `models/Account` | one place that talks to `IApiService` |
40
+
41
+ The component calls two hooks and renders. It never builds a Dataverse payload or
42
+ calls a service directly.
43
+
44
+ ## The ServiceFactory seam
45
+
46
+ `ServiceFactory.createApiService(Xrm)` is the single decision point:
47
+
48
+ - **Mock** (`localhost`, no `DYNAMICS_URL`) → `MockApiService`, in-memory.
49
+ - **Token** (`localhost` + `DYNAMICS_URL`) → `FetchApiService`. `vite.config.ts`
50
+ sets `import.meta.env.VITE_USE_PROXY` (a boolean via `define`) and proxies
51
+ `/api/data/*` to the org, injecting the bearer **server-side**. The token never
52
+ reaches the client bundle — an intentional improvement over inlining it.
53
+ - **Production** (deployed) → `XrmApiService`, hitting the Web API on the same
54
+ origin (session auth). `Xrm` is resolved from `window.parent` in `App.tsx`.
55
+
56
+ All three implement the same `IApiService` and address entities by their Web API
57
+ **set name** (e.g. `accounts`), so reads and writes use one consistent identifier.
58
+
59
+ ## Logging
60
+
61
+ `logger` / `withCrudLog` / `initLogging` come from the component library.
62
+ `core/services/crudLogging.ts` binds `withCrudLog` to the Dataverse error shapes
63
+ so every service method emits one structured `[CRUD] …` line. Console output is
64
+ **localhost-gated** (errors always surface); on localhost the buffer is exposed as
65
+ `window.__APP_LOGS__` + `window.dumpAppLogs(filter?)`.
66
+
67
+ ## Testing
68
+
69
+ Unit-test the **pure layers** — they hold the rules and need no DOM:
70
+
71
+ - `src/domain/diff.test.ts`
72
+ - `src/example/mappers/accountMapper.test.ts`
73
+
74
+ The view and the `IApiService` implementations are best covered by running the app
75
+ against the mock seam (`npm run dev`). Run `npm run test` (Vitest).