@khester/create-dynamics-app 2.10.0 → 3.0.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 (38) hide show
  1. package/README.md +28 -3
  2. package/dist/artifacts/registry.d.ts +7 -5
  3. package/dist/artifacts/registry.d.ts.map +1 -1
  4. package/dist/artifacts/registry.js +30 -13
  5. package/dist/artifacts/registry.js.map +1 -1
  6. package/dist/artifacts/types.d.ts +15 -8
  7. package/dist/artifacts/types.d.ts.map +1 -1
  8. package/dist/index.js +7 -5
  9. package/dist/index.js.map +1 -1
  10. package/dist/scaffold.d.ts.map +1 -1
  11. package/dist/scaffold.js +9 -4
  12. package/dist/scaffold.js.map +1 -1
  13. package/dist/utils/copyTemplate.d.ts +1 -1
  14. package/dist/utils/copyTemplate.js +1 -1
  15. package/package.json +1 -1
  16. package/templates/power-pages-island/README.md +81 -0
  17. package/templates/power-pages-island/package.json +27 -0
  18. package/templates/power-pages-island/public/index.html +13 -0
  19. package/templates/power-pages-island/src/App.tsx +25 -0
  20. package/templates/power-pages-island/src/Component.tsx +89 -0
  21. package/templates/power-pages-island/src/core/services/MockApiService.ts +58 -0
  22. package/templates/power-pages-island/src/core/services/portalSeam.assert.ts +14 -0
  23. package/templates/power-pages-island/src/island-env.d.ts +12 -0
  24. package/templates/power-pages-island/src/islands.tsx +38 -0
  25. package/templates/power-pages-island/tsconfig.json +15 -0
  26. package/templates/power-pages-island/webpack.config.js +22 -0
  27. package/templates/power-pages-starter/.env.example +0 -6
  28. package/templates/power-pages-starter/README.md +0 -95
  29. package/templates/power-pages-starter/package.json +0 -43
  30. package/templates/power-pages-starter/public/index.html +0 -15
  31. package/templates/power-pages-starter/src/components/ContactForm.css +0 -84
  32. package/templates/power-pages-starter/src/components/ContactForm.tsx +0 -258
  33. package/templates/power-pages-starter/src/index.tsx +0 -32
  34. package/templates/power-pages-starter/src/providers/PowerPagesProvider.tsx +0 -162
  35. package/templates/power-pages-starter/src/styles/index.css +0 -76
  36. package/templates/power-pages-starter/tsconfig.json +0 -20
  37. package/templates/power-pages-starter/webpack.config.js +0 -57
  38. package/templates/{power-pages-starter → power-pages-island}/gitignore +1 -1
@@ -0,0 +1,25 @@
1
+ import * as React from "react";
2
+ import type { IApiService } from "./core/services/IApiService";
3
+ import { MockApiService } from "./core/services/MockApiService";
4
+ import { PortalSeamService } from "@dataverse-kit/portal-api-service";
5
+ import { Component } from "./Component";
6
+
7
+ // A Power Pages island is served from the portal origin, so a same-origin `/_api`
8
+ // fetch is authenticated by the portal session cookie + anti-forgery token —
9
+ // PortalSeamService handles both (token fetch/cache, 403-retry, idempotency-aware
10
+ // retry). On localhost (the webpack dev server) fall back to the in-memory mock so
11
+ // it renders with no portal.
12
+ //
13
+ // PortalSeamService implements the create-app 6-method `dataverse-seam`, so it is
14
+ // structurally assignable to IApiService with NO cast — and THIS assignment is the
15
+ // load-bearing compile-time drift guard: if the seam shapes ever diverge, this line
16
+ // fails `tsc`. (See core/services/portalSeam.assert.ts for the explicit fixture.)
17
+ export const App: React.FC<{ island?: string }> = () => {
18
+ // `island` is the `data-dvkit-island` value — a hook for routing multiple widgets
19
+ // on a page. The starter renders the accounts widget for any island.
20
+ const api = React.useMemo<IApiService>(() => {
21
+ const onLocalhost = /^(localhost|127\.0\.0\.1)$/.test(window.location.hostname);
22
+ return onLocalhost ? new MockApiService() : new PortalSeamService();
23
+ }, []);
24
+ return <Component api={api} />;
25
+ };
@@ -0,0 +1,89 @@
1
+ import * as React from "react";
2
+ import {
3
+ DetailsList,
4
+ SelectionMode,
5
+ MessageBar,
6
+ MessageBarType,
7
+ Spinner,
8
+ Stack,
9
+ Text,
10
+ type IColumn,
11
+ } from "@fluentui/react";
12
+ import { initializeIcons } from "@fluentui/react/lib/Icons";
13
+ import type { IApiService } from "./core/services/IApiService";
14
+
15
+ // Initialize Fluent UI icons once (DetailsList sort/column glyphs).
16
+ initializeIcons();
17
+
18
+ /**
19
+ * The ONE host-agnostic component. It depends on nothing but an injected
20
+ * `IApiService` — no PCF `context`, no Xrm, no lifecycle, no bundler globals — so
21
+ * the SAME source renders unchanged inside a PCF control and a Dynamics custom
22
+ * page. Each host's thin wrapper builds the right `IApiService` and mounts this.
23
+ */
24
+ export interface ComponentProps {
25
+ api: IApiService;
26
+ }
27
+
28
+ const ACCOUNTS_FETCHXML = [
29
+ '<fetch top="25">',
30
+ ' <entity name="account">',
31
+ ' <attribute name="accountid" />',
32
+ ' <attribute name="name" />',
33
+ ' <attribute name="telephone1" />',
34
+ ' <order attribute="name" />',
35
+ " </entity>",
36
+ "</fetch>",
37
+ ].join("\n");
38
+
39
+ interface AccountRow {
40
+ accountid: string;
41
+ name?: string;
42
+ telephone1?: string;
43
+ }
44
+
45
+ const COLUMNS: IColumn[] = [
46
+ { key: "name", name: "Account", fieldName: "name", minWidth: 180, maxWidth: 320, isResizable: true },
47
+ { key: "telephone1", name: "Phone", fieldName: "telephone1", minWidth: 140, isResizable: true },
48
+ ];
49
+
50
+ export const Component: React.FC<ComponentProps> = ({ api }) => {
51
+ const [rows, setRows] = React.useState<AccountRow[] | null>(null);
52
+ const [error, setError] = React.useState<string | null>(null);
53
+
54
+ React.useEffect(() => {
55
+ let cancelled = false;
56
+ api
57
+ .retrieveMultipleRecords("accounts", ACCOUNTS_FETCHXML)
58
+ .then((res) => {
59
+ if (!cancelled) setRows(res.entities as AccountRow[]);
60
+ })
61
+ .catch((e) => {
62
+ if (!cancelled) setError(e instanceof Error ? e.message : String(e));
63
+ });
64
+ return () => {
65
+ cancelled = true;
66
+ };
67
+ }, [api]);
68
+
69
+ return (
70
+ <Stack tokens={{ childrenGap: 12 }} styles={{ root: { padding: 16 } }}>
71
+ <Text variant="xLarge">Accounts</Text>
72
+ <Text variant="small">
73
+ One host-agnostic component — the same source renders in a PCF control and a
74
+ Dynamics custom page; only the thin host wrapper changes.
75
+ </Text>
76
+ {error && <MessageBar messageBarType={MessageBarType.error}>{error}</MessageBar>}
77
+ {!rows && !error && <Spinner label="Loading accounts…" />}
78
+ {rows && (
79
+ <DetailsList
80
+ items={rows}
81
+ columns={COLUMNS}
82
+ setKey="accounts"
83
+ getKey={(item: AccountRow) => item.accountid}
84
+ selectionMode={SelectionMode.none}
85
+ />
86
+ )}
87
+ </Stack>
88
+ );
89
+ };
@@ -0,0 +1,58 @@
1
+ import { IApiService } from "./IApiService";
2
+ import { logCrud } from "./crudLogging";
3
+
4
+ // A few seeded in-memory accounts so the control/page renders with no org or
5
+ // token (the PCF test harness and `npm run dev` both land here). Replace this
6
+ // seed as your component grows. The other IApiService impls (Xrm/Fetch) and the
7
+ // shared contract are pulled from templates/_shared/dataverse-seam at scaffold.
8
+ const SEED_ACCOUNTS = [
9
+ { accountid: "00000000-0000-0000-0000-000000000001", name: "Contoso Ltd", telephone1: "+1 (425) 555-0100" },
10
+ { accountid: "00000000-0000-0000-0000-000000000002", name: "Fabrikam Inc", telephone1: "+1 (206) 555-0140" },
11
+ { accountid: "00000000-0000-0000-0000-000000000003", name: "Adventure Works", telephone1: "+1 (312) 555-0190" },
12
+ ];
13
+
14
+ export class MockApiService implements IApiService {
15
+ private accounts: Record<string, any> = Object.fromEntries(
16
+ SEED_ACCOUNTS.map((a) => [a.accountid, { ...a }]),
17
+ );
18
+ private nextId = SEED_ACCOUNTS.length + 1;
19
+
20
+ retrieveMultipleRecords(entity: string): Promise<{ entities: any[] }> {
21
+ return logCrud(
22
+ { op: "READ", entity, resultCount: (r) => r?.entities?.length },
23
+ async () => ({
24
+ entities: entity === "accounts" ? Object.values(this.accounts) : [],
25
+ }),
26
+ );
27
+ }
28
+
29
+ createRecord(entity: string, record: any): Promise<any> {
30
+ return logCrud({ op: "CREATE", entity, resultId: (r) => r?.id }, async () => {
31
+ const id = `00000000-0000-0000-0000-${String(this.nextId++).padStart(12, "0")}`;
32
+ if (entity === "accounts") this.accounts[id] = { accountid: id, ...record };
33
+ return { id };
34
+ });
35
+ }
36
+
37
+ updateRecord(entity: string, id: string, record: any): Promise<any> {
38
+ return logCrud({ op: "UPDATE", entity, id }, async () => {
39
+ const key = id.replace(/[{}]/g, "");
40
+ if (this.accounts[key]) this.accounts[key] = { ...this.accounts[key], ...record };
41
+ return { success: true };
42
+ });
43
+ }
44
+
45
+ deleteRecord(entity: string, id: string): Promise<void> {
46
+ return logCrud({ op: "DELETE", entity, id }, async () => {
47
+ delete this.accounts[id.replace(/[{}]/g, "")];
48
+ });
49
+ }
50
+
51
+ executeRequest(requestName: string): Promise<any> {
52
+ return logCrud({ op: "EXECUTE", entity: requestName }, async () => ({}));
53
+ }
54
+
55
+ associateRecord(entity: string, id: string): Promise<void> {
56
+ return logCrud({ op: "ASSOCIATE", entity, id }, async () => undefined);
57
+ }
58
+ }
@@ -0,0 +1,14 @@
1
+ // Compile-time drift guard (typechecked by `npm run build` → `tsc --noEmit`, and by
2
+ // the create-dynamics-app power-pages-island template build e2e). `PortalSeamService`
3
+ // — the create-app 6-method `dataverse-seam` adapter shipped in
4
+ // `@dataverse-kit/portal-api-service` — MUST stay structurally assignable to THIS
5
+ // template's local seam `IApiService`, so `App.tsx` can inject it with no cast. That
6
+ // package can't import the create-app template interface, so the assertion lives here,
7
+ // where both types resolve. Type-only — no runtime, tree-shaken from the bundle.
8
+ import type { IApiService } from "./IApiService";
9
+ import type { PortalSeamService } from "@dataverse-kit/portal-api-service";
10
+
11
+ // If PortalSeamService drifts from the seam shape, `true` is no longer assignable to
12
+ // `never` and this fails to compile.
13
+ type PortalSeamSatisfiesSeam = PortalSeamService extends IApiService ? true : never;
14
+ export const _portalSeamDriftGuard: PortalSeamSatisfiesSeam = true;
@@ -0,0 +1,12 @@
1
+ // Webpack/ts-loader builds have no `vite/client` types. The shared data-access seam
2
+ // (templates/_shared/dataverse-seam) includes ServiceFactory, which reads
3
+ // `import.meta.env.VITE_USE_PROXY` for the Vite custom-page host. This Power Pages
4
+ // island does NOT use ServiceFactory (App.tsx builds MockApiService / PortalSeamService
5
+ // directly), so it is tree-shaken from the bundle — but `tsc` / ts-loader still
6
+ // typecheck it. This minimal ambient declaration lets that unused file compile here.
7
+ //
8
+ // NOTE: this file MUST live under src/ — the tsconfig is `include: ["src"]`, so a
9
+ // project-root *.d.ts would be excluded and the shim would be missed.
10
+ interface ImportMeta {
11
+ readonly env: Record<string, unknown>;
12
+ }
@@ -0,0 +1,38 @@
1
+ import * as React from "react";
2
+ import { createRoot } from "react-dom/client";
3
+ import { initLogging } from "@khester/reusable-components";
4
+ import { App } from "./App";
5
+
6
+ // Wire up the localhost-only troubleshooting hooks (window.dumpAppLogs()); no-op in a
7
+ // live portal.
8
+ initLogging();
9
+
10
+ // A traditional Power Pages portal is server-rendered (Liquid). This bundle is an
11
+ // ISLAND: it hydrates React ONLY into the nodes a maker tags in Liquid, leaving the
12
+ // rest of the page untouched — the delta the MS code-site scaffolder (whole-page SPA)
13
+ // can't do. Tag a mount point with `data-dvkit-island="<name>"` (the name is a hook
14
+ // for future per-widget routing) and reference this web file:
15
+ //
16
+ // <div data-dvkit-island="accounts"></div>
17
+ // <script src="/{{projectName}}.js"></script>
18
+ //
19
+ const SELECTOR = "[data-dvkit-island]";
20
+
21
+ function mountIslands(): void {
22
+ document.querySelectorAll<HTMLElement>(SELECTOR).forEach((node) => {
23
+ // Idempotent — a partial-page refresh or a duplicated include must not double-mount.
24
+ if (node.dataset.dvkitMounted === "true") return;
25
+ node.dataset.dvkitMounted = "true";
26
+ createRoot(node).render(
27
+ <React.StrictMode>
28
+ <App island={node.dataset.dvkitIsland} />
29
+ </React.StrictMode>,
30
+ );
31
+ });
32
+ }
33
+
34
+ if (document.readyState === "loading") {
35
+ document.addEventListener("DOMContentLoaded", mountIslands);
36
+ } else {
37
+ mountIslands();
38
+ }
@@ -0,0 +1,15 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2020",
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", "ES2020"],
12
+ "types": []
13
+ },
14
+ "include": ["src"]
15
+ }
@@ -0,0 +1,22 @@
1
+ const path = require('path');
2
+ const HtmlWebpackPlugin = require('html-webpack-plugin');
3
+
4
+ // A Power Pages ISLAND: one self-contained bundle, uploaded as a portal WEB FILE and
5
+ // referenced from a Liquid page (see README). The output filename is STABLE (no
6
+ // contenthash) so the Liquid `<script src>` URL stays fixed across rebuilds.
7
+ // HtmlWebpackPlugin exists only for the local `webpack serve` preview — on a real
8
+ // portal the Liquid page is the host, so the emitted index.html is never uploaded.
9
+ module.exports = {
10
+ entry: './src/islands.tsx',
11
+ output: {
12
+ path: path.resolve(__dirname, 'dist'),
13
+ filename: '{{projectName}}.js',
14
+ clean: true,
15
+ },
16
+ resolve: { extensions: ['.ts', '.tsx', '.js'] },
17
+ module: {
18
+ rules: [{ test: /\.tsx?$/, use: 'ts-loader', exclude: /node_modules/ }],
19
+ },
20
+ plugins: [new HtmlWebpackPlugin({ template: './public/index.html' })],
21
+ devServer: { static: './dist', port: 8080, hot: true },
22
+ };
@@ -1,6 +0,0 @@
1
- # Power Pages Configuration
2
- PORTAL_URL=https://your-portal.powerappsportals.com
3
-
4
- # Optional: API Configuration
5
- API_VERSION=v9.0
6
- ENABLE_LOGGING=false
@@ -1,95 +0,0 @@
1
- # Power Pages Application
2
-
3
- This is a Power Pages application built with Dynamics UI Kit components.
4
-
5
- ## Getting Started
6
-
7
- 1. **Environment Setup**
8
-
9
- ```bash
10
- cp .env.example .env
11
- ```
12
-
13
- Edit `.env` and set your `PORTAL_URL` to your Power Pages portal URL.
14
-
15
- 2. **Install Dependencies**
16
-
17
- ```bash
18
- npm install
19
- ```
20
-
21
- 3. **Development**
22
-
23
- ```bash
24
- npm run dev
25
- ```
26
-
27
- This starts the development server at http://localhost:3000
28
-
29
- 4. **Build for Production**
30
- ```bash
31
- npm run build
32
- ```
33
-
34
- ## Project Structure
35
-
36
- ```
37
- src/
38
- ├── components/ # React components
39
- │ ├── ContactForm.tsx # Main contact form component
40
- │ └── ContactForm.css # Component styles
41
- ├── providers/ # Context providers
42
- │ └── PowerPagesProvider.tsx # Power Pages API provider
43
- ├── styles/ # Global styles
44
- │ └── index.css # Global CSS
45
- └── index.tsx # Application entry point
46
- ```
47
-
48
- ## Features
49
-
50
- - **Contact Management**: Create and manage contacts using Dynamics UI Kit components
51
- - **Power Pages Integration**: Seamless integration with Power Pages API
52
- - **Responsive Design**: Mobile-friendly responsive layout
53
- - **Type Safety**: Full TypeScript support
54
-
55
- ## Deployment
56
-
57
- To deploy your application to Power Pages:
58
-
59
- 1. Build the application:
60
-
61
- ```bash
62
- npm run build
63
- ```
64
-
65
- 2. Upload the `dist` folder contents to your Power Pages portal's web files
66
-
67
- 3. Create a new web page in Power Pages and reference your built JavaScript and CSS files
68
-
69
- ## Customization
70
-
71
- ### Adding New Components
72
-
73
- 1. Create your component in `src/components/`
74
- 2. Use Dynamics UI Kit components for consistent styling
75
- 3. Import and use the `usePowerPagesApi` hook for data operations
76
-
77
- ### Styling
78
-
79
- - Global styles are in `src/styles/index.css`
80
- - Component-specific styles should be co-located with components
81
- - Follow the existing CSS naming conventions
82
-
83
- ## Available Scripts
84
-
85
- - `npm run dev` - Start development server
86
- - `npm run build` - Build for production
87
- - `npm run typecheck` - Type check without building
88
- - `npm run clean` - Clean build directory
89
- - `npm run lint` - Lint code
90
-
91
- ## Learn More
92
-
93
- - [Dynamics UI Kit Documentation](https://github.com/your-org/dynamics-ui-kit)
94
- - [Power Pages Documentation](https://docs.microsoft.com/power-pages/)
95
- - [React Documentation](https://reactjs.org/)
@@ -1,43 +0,0 @@
1
- {
2
- "name": "power-pages-app",
3
- "version": "1.0.0",
4
- "description": "Power Pages application built with Dynamics UI Kit",
5
- "main": "dist/index.js",
6
- "scripts": {
7
- "build": "webpack --mode=production",
8
- "dev": "webpack serve --mode=development",
9
- "typecheck": "tsc --noEmit",
10
- "clean": "rimraf dist",
11
- "lint": "eslint src --ext .ts,.tsx"
12
- },
13
- "dependencies": {
14
- "@khester/dynamics-ui-api-client": "^1.0.0",
15
- "@khester/dynamics-ui-components": "^1.0.0",
16
- "react": "^18.2.0",
17
- "react-dom": "^18.2.0"
18
- },
19
- "devDependencies": {
20
- "@types/react": "^18.2.0",
21
- "@types/react-dom": "^18.2.0",
22
- "css-loader": "^6.8.1",
23
- "html-webpack-plugin": "^5.5.3",
24
- "style-loader": "^3.3.3",
25
- "ts-loader": "^9.5.1",
26
- "typescript": "^5.3.3",
27
- "webpack": "^5.89.0",
28
- "webpack-cli": "^5.1.4",
29
- "webpack-dev-server": "^4.15.1"
30
- },
31
- "browserslist": {
32
- "production": [
33
- ">0.2%",
34
- "not dead",
35
- "not op_mini all"
36
- ],
37
- "development": [
38
- "last 1 chrome version",
39
- "last 1 firefox version",
40
- "last 1 safari version"
41
- ]
42
- }
43
- }
@@ -1,15 +0,0 @@
1
- <!DOCTYPE html>
2
- <html lang="en">
3
- <head>
4
- <meta charset="utf-8" />
5
- <link rel="icon" href="data:," />
6
- <meta name="viewport" content="width=device-width, initial-scale=1" />
7
- <meta name="theme-color" content="#000000" />
8
- <meta name="description" content="Power Pages Application built with Dynamics UI Kit" />
9
- <title>Power Pages App - Dynamics UI Kit</title>
10
- </head>
11
- <body>
12
- <noscript>You need to enable JavaScript to run this app.</noscript>
13
- <div id="root"></div>
14
- </body>
15
- </html>
@@ -1,84 +0,0 @@
1
- .contact-form {
2
- max-width: 800px;
3
- margin: 0 auto;
4
- padding: 24px;
5
- background: #ffffff;
6
- border-radius: 8px;
7
- box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
8
- }
9
-
10
- .contact-form__header {
11
- margin-bottom: 24px;
12
- }
13
-
14
- .contact-form__title {
15
- margin: 0;
16
- font-size: 24px;
17
- font-weight: 600;
18
- color: #323130;
19
- }
20
-
21
- .contact-form__content {
22
- margin-bottom: 24px;
23
- }
24
-
25
- .contact-form__row {
26
- display: grid;
27
- grid-template-columns: 1fr 1fr;
28
- gap: 16px;
29
- margin-bottom: 16px;
30
- }
31
-
32
- .contact-form__field {
33
- display: flex;
34
- flex-direction: column;
35
- }
36
-
37
- .contact-form__actions {
38
- display: flex;
39
- gap: 12px;
40
- justify-content: flex-start;
41
- margin-top: 24px;
42
- }
43
-
44
- .contact-form__success {
45
- padding: 12px 16px;
46
- margin-bottom: 16px;
47
- background-color: #dff6dd;
48
- border: 1px solid #107c10;
49
- border-radius: 4px;
50
- color: #107c10;
51
- font-weight: 500;
52
- }
53
-
54
- .contact-form__error {
55
- padding: 12px 16px;
56
- margin-bottom: 16px;
57
- background-color: #fef7f1;
58
- border: 1px solid #d13438;
59
- border-radius: 4px;
60
- color: #d13438;
61
- font-weight: 500;
62
- }
63
-
64
- .contact-form__auth-notice {
65
- margin-top: 16px;
66
- padding: 12px 16px;
67
- background-color: #fff4ce;
68
- border: 1px solid #ffb900;
69
- border-radius: 4px;
70
- color: #8a6d00;
71
- text-align: center;
72
- }
73
-
74
- @media (max-width: 768px) {
75
- .contact-form {
76
- padding: 16px;
77
- margin: 16px;
78
- }
79
-
80
- .contact-form__row {
81
- grid-template-columns: 1fr;
82
- gap: 12px;
83
- }
84
- }