create-ec-app 0.0.2 → 0.0.4
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 +20 -17
- package/bin/index.js +0 -0
- package/dist/creators/mobile.d.ts.map +1 -1
- package/dist/creators/mobile.js +24 -0
- package/dist/creators/mobile.js.map +1 -1
- package/dist/creators/portal.d.ts.map +1 -1
- package/dist/creators/portal.js +124 -4
- package/dist/creators/portal.js.map +1 -1
- package/dist/creators/powerpages.d.ts.map +1 -1
- package/dist/creators/powerpages.js +317 -42
- package/dist/creators/powerpages.js.map +1 -1
- package/dist/creators/webresource.d.ts.map +1 -1
- package/dist/creators/webresource.js +37 -3
- package/dist/creators/webresource.js.map +1 -1
- package/dist/index.js +17 -17
- package/dist/index.js.map +1 -1
- package/dist/readmes/mobile.md +42 -0
- package/dist/readmes/portal.md +161 -0
- package/dist/readmes/powerpages.md +108 -0
- package/dist/readmes/webresource.md +260 -0
- package/package.json +59 -59
package/dist/index.js
CHANGED
|
@@ -6,8 +6,8 @@ import { createPortalApp } from "./creators/portal.js";
|
|
|
6
6
|
import { createPowerPagesApp } from "./creators/powerpages.js";
|
|
7
7
|
import { createMobileApp } from "./creators/mobile.js";
|
|
8
8
|
const main = async () => {
|
|
9
|
-
console.log(chalk.bold.hex("#F5AB00")("\
|
|
10
|
-
console.log(chalk.gray("Create
|
|
9
|
+
console.log(chalk.bold.hex("#F5AB00")("\nEC App Creator\n"));
|
|
10
|
+
console.log(chalk.gray("Create applications for your EC ecosystem.\n"));
|
|
11
11
|
// Prompt for project name if not provided
|
|
12
12
|
let projectName = process.argv[2];
|
|
13
13
|
if (!projectName) {
|
|
@@ -26,24 +26,24 @@ const main = async () => {
|
|
|
26
26
|
// Prompt for application type
|
|
27
27
|
const appTypes = [
|
|
28
28
|
{
|
|
29
|
-
name: "
|
|
29
|
+
name: "Webresource App",
|
|
30
30
|
value: "webresource",
|
|
31
|
-
description: "React app for Dynamics 365 webresources with Vite, Kendo UI, and Tailwind"
|
|
31
|
+
description: "React app for Dynamics 365 webresources with Vite, optional Kendo UI, and Tailwind"
|
|
32
32
|
},
|
|
33
33
|
{
|
|
34
|
-
name: "
|
|
34
|
+
name: "Portal App",
|
|
35
35
|
value: "portal",
|
|
36
36
|
description: "Next.js app for customer portals with authentication and Dynamics integration"
|
|
37
37
|
},
|
|
38
38
|
{
|
|
39
|
-
name: "
|
|
39
|
+
name: "Power Pages App",
|
|
40
40
|
value: "powerpages",
|
|
41
|
-
description: "React SPA for Power Pages with
|
|
41
|
+
description: "React SPA for Power Pages with authentication context and data services"
|
|
42
42
|
},
|
|
43
43
|
{
|
|
44
|
-
name: "
|
|
44
|
+
name: "Mobile App",
|
|
45
45
|
value: "mobile",
|
|
46
|
-
description: "React Native Expo app with
|
|
46
|
+
description: "React Native Expo app with TypeScript and NativeWind"
|
|
47
47
|
}
|
|
48
48
|
];
|
|
49
49
|
const { appType } = await inquirer.prompt([
|
|
@@ -54,12 +54,12 @@ const main = async () => {
|
|
|
54
54
|
choices: appTypes.map(type => ({
|
|
55
55
|
name: `${type.name}\n ${chalk.gray(type.description)}`,
|
|
56
56
|
value: type.value,
|
|
57
|
-
short: type.name
|
|
57
|
+
short: type.name
|
|
58
58
|
})),
|
|
59
59
|
pageSize: 10
|
|
60
60
|
},
|
|
61
61
|
]);
|
|
62
|
-
console.log(`\n${chalk.green("
|
|
62
|
+
console.log(`\n${chalk.green("Creating")} ${chalk.bold(appType)} app: ${chalk.cyan(projectName)}\n`);
|
|
63
63
|
// Route to the appropriate creator
|
|
64
64
|
switch (appType) {
|
|
65
65
|
case "webresource":
|
|
@@ -79,17 +79,17 @@ const main = async () => {
|
|
|
79
79
|
process.exit(1);
|
|
80
80
|
}
|
|
81
81
|
// Final success message
|
|
82
|
-
console.log(chalk.green.bold("\
|
|
82
|
+
console.log(chalk.green.bold("\nProject created successfully."));
|
|
83
83
|
console.log(`\n${chalk.cyan("Next steps:")}`);
|
|
84
|
-
console.log(`
|
|
84
|
+
console.log(` - cd ${projectName}`);
|
|
85
85
|
if (appType === "webresource" || appType === "powerpages") {
|
|
86
|
-
console.log(`
|
|
86
|
+
console.log(` - If you selected Kendo UI: ${chalk.yellow("npx kendo-ui-license activate")}`);
|
|
87
87
|
}
|
|
88
|
-
console.log(`
|
|
89
|
-
console.log(`
|
|
88
|
+
console.log(` - npm run dev`);
|
|
89
|
+
console.log(` - npm run build\n`);
|
|
90
90
|
};
|
|
91
91
|
main().catch((error) => {
|
|
92
|
-
console.error(chalk.red("
|
|
92
|
+
console.error(chalk.red("An error occurred:"), error);
|
|
93
93
|
process.exit(1);
|
|
94
94
|
});
|
|
95
95
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAEA,OAAO,KAAK,MAAM,OAAO,CAAC;AAC1B,OAAO,QAAQ,MAAM,UAAU,CAAC;AAChC,OAAO,EAAE,oBAAoB,EAAE,MAAM,2BAA2B,CAAC;AACjE,OAAO,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC;AACvD,OAAO,EAAE,mBAAmB,EAAE,MAAM,0BAA0B,CAAC;AAC/D,OAAO,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC;AAQvD,MAAM,IAAI,GAAG,KAAK,IAAmB,EAAE;IACtC,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC,
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# EC Mobile App
|
|
2
|
+
|
|
3
|
+
Expo + React Native + TypeScript template with Tailwind (NativeWind-style utilities), Expo Router, and a small set of UI utilities. Generated by `create-ec-app` for rapid mobile development.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- Expo Router with typed layouts
|
|
8
|
+
- Tailwind utilities for React Native
|
|
9
|
+
- Theming helpers for React Navigation (`lib/theme.ts`)
|
|
10
|
+
- Utility helpers (`lib/utils.ts` with `cn`)
|
|
11
|
+
- Portal support via `@rn-primitives/portal` (included in root layout)
|
|
12
|
+
|
|
13
|
+
## Prerequisites
|
|
14
|
+
|
|
15
|
+
- Node.js 18+
|
|
16
|
+
- Xcode (macOS) and/or Android Studio SDKs for local simulators
|
|
17
|
+
- Expo CLI (installed automatically via `npx`)
|
|
18
|
+
|
|
19
|
+
## Getting Started
|
|
20
|
+
|
|
21
|
+
- Install dependencies: `npm install`
|
|
22
|
+
- Start development: `npx expo start`
|
|
23
|
+
- Press `i` for iOS simulator, `a` for Android, or scan the QR code with Expo Go
|
|
24
|
+
|
|
25
|
+
## Key Files
|
|
26
|
+
|
|
27
|
+
- `app/_layout.tsx`: Root layout with theme provider, Expo Router stack, and `PortalHost`.
|
|
28
|
+
- `lib/theme.ts`: Light/dark palettes mapped to React Navigation theme.
|
|
29
|
+
- `lib/utils.ts`: `cn` helper for conditional class names.
|
|
30
|
+
- `global.css`: Tailwind directives and custom styles (imported by the root layout).
|
|
31
|
+
|
|
32
|
+
## Styling
|
|
33
|
+
|
|
34
|
+
- The template uses Tailwind utilities for React Native components. Adjust Tailwind config as needed for your design system.
|
|
35
|
+
|
|
36
|
+
## Building
|
|
37
|
+
|
|
38
|
+
- Use the Expo tooling for builds and previews:
|
|
39
|
+
- `npx expo prebuild` to generate native projects (if needed)
|
|
40
|
+
- `npx expo run:ios` / `npx expo run:android` to build and run
|
|
41
|
+
- See the Expo docs for EAS Build for cloud builds
|
|
42
|
+
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
# EC Portal App
|
|
2
|
+
|
|
3
|
+
Next.js (App Router) + TypeScript template for customer portals integrated with Dynamics 365/Dataverse. Generated by `create-ec-app`, it includes Tailwind CSS, TanStack Query, Azure AD authentication via NextAuth, and a choice of UI library (Kendo UI or Shadcn/ui).
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- Next.js App Router with TypeScript
|
|
8
|
+
- Tailwind CSS; Kendo preset applied when using Kendo UI
|
|
9
|
+
- UI choice: Kendo UI (with theme import) or Shadcn/ui
|
|
10
|
+
- TanStack Query provider (`src/app/providers.tsx`)
|
|
11
|
+
- Azure AD auth via NextAuth (`auth.ts`, `src/app/api/auth/[...nextauth]/route.ts`)
|
|
12
|
+
- Example Dynamics API route (`src/app/api/dynamics/accounts/route.ts`)
|
|
13
|
+
- React Query hook (`src/hooks/useDynamicsAccounts.ts`)
|
|
14
|
+
- `next.config.ts` set to `output: 'standalone'` (Azure-friendly)
|
|
15
|
+
- Example GitHub Actions workflow (`github.example.deploy.yml`)
|
|
16
|
+
- Example Azure DevOps pipeline (`azure-pipelines.example.yml`)
|
|
17
|
+
|
|
18
|
+
## Prerequisites
|
|
19
|
+
|
|
20
|
+
- Node.js 18+ and npm 9+
|
|
21
|
+
- Azure AD app registration (client id/secret, tenant)
|
|
22
|
+
- Dynamics 365/Dataverse environment and base URL
|
|
23
|
+
|
|
24
|
+
## Getting Started
|
|
25
|
+
|
|
26
|
+
1. Install dependencies: `npm install`
|
|
27
|
+
|
|
28
|
+
2. Configure environment in `.env.local`:
|
|
29
|
+
|
|
30
|
+
```
|
|
31
|
+
NEXTAUTH_URL=http://localhost:3000
|
|
32
|
+
AZURE_AD_CLIENT_ID=your-azure-ad-client-id
|
|
33
|
+
AZURE_AD_CLIENT_SECRET=your-azure-ad-client-secret
|
|
34
|
+
AZURE_AD_TENANT_ID=your-azure-ad-tenant-id
|
|
35
|
+
DYNAMICS_BASE_URL=https://your-org.crm.dynamics.com
|
|
36
|
+
DYNAMICS_API_VERSION=v9.2
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
3. Development: `npm run dev`
|
|
40
|
+
|
|
41
|
+
4. Production build: `npm run build` then `npm start`
|
|
42
|
+
|
|
43
|
+
If you selected Kendo UI, activate your license after install:
|
|
44
|
+
|
|
45
|
+
```
|
|
46
|
+
npx kendo-ui-license activate
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Dynamics Data Access
|
|
50
|
+
|
|
51
|
+
- Library: `src/lib/dynamics.ts` exposes `getDynamicsData(entity, options)`
|
|
52
|
+
- API route: `src/app/api/dynamics/accounts/route.ts` fetches Accounts via the above
|
|
53
|
+
- Hook: `src/hooks/useDynamicsAccounts.ts` wraps the API with TanStack Query
|
|
54
|
+
|
|
55
|
+
Example component:
|
|
56
|
+
|
|
57
|
+
```tsx
|
|
58
|
+
"use client";
|
|
59
|
+
import { useDynamicsAccounts } from "@/hooks/useDynamicsAccounts";
|
|
60
|
+
|
|
61
|
+
export default function AccountsList() {
|
|
62
|
+
const { data, isLoading, error } = useDynamicsAccounts();
|
|
63
|
+
if (isLoading) return <div>Loading…</div>;
|
|
64
|
+
if (error) return <div>Failed to load</div>;
|
|
65
|
+
return (
|
|
66
|
+
<ul>
|
|
67
|
+
{data?.map((a) => (
|
|
68
|
+
<li key={a.accountid}>{a.name}</li>
|
|
69
|
+
))}
|
|
70
|
+
</ul>
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## UI Libraries
|
|
76
|
+
|
|
77
|
+
- Kendo UI: Theme CSS is imported in `src/app/layout.tsx` and components can be used from `@progress/kendo-react-*` packages.
|
|
78
|
+
- Shadcn/ui: Components are installed and available via the `@/components` alias.
|
|
79
|
+
|
|
80
|
+
## Deployment
|
|
81
|
+
|
|
82
|
+
- The project is configured for standalone output (good for Azure App Service and containers).
|
|
83
|
+
- An example GitHub Actions workflow is provided in `example.deploy.yml`. Configure secrets and adapt steps to your environment.
|
|
84
|
+
|
|
85
|
+
### Azure App Service (Web App)
|
|
86
|
+
|
|
87
|
+
Step 1 — Create a Web App and connect CI
|
|
88
|
+
|
|
89
|
+
- Create a new App Service in Azure and connect deployment to your repository (GitHub) or set up an Azure DevOps pipeline.
|
|
90
|
+
|
|
91
|
+
Step 2 — Environment variables
|
|
92
|
+
|
|
93
|
+
It is important to set configuration early to avoid restarts due to missing settings. Ensure you set the following app settings. Note the `SCM_DO_BUILD_DURING_DEPLOYMENT` flag controls whether App Service attempts a build during deployment.
|
|
94
|
+
|
|
95
|
+
Example app settings payload:
|
|
96
|
+
|
|
97
|
+
```
|
|
98
|
+
[
|
|
99
|
+
{
|
|
100
|
+
"name": "AUTH_MICROSOFT_ENTRA_ID_ID",
|
|
101
|
+
"value": "",
|
|
102
|
+
"slotSetting": false
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
"name": "AUTH_MICROSOFT_ENTRA_ID_ISSUER",
|
|
106
|
+
"value": "<https://login.microsoftonline.com/TENANT_ID/v2.0>",
|
|
107
|
+
"slotSetting": false
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
"name": "AUTH_MICROSOFT_ENTRA_ID_SECRET",
|
|
111
|
+
"value": "CLIENT_SECRET",
|
|
112
|
+
"slotSetting": false
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
"name": "AUTH_MICROSOFT_ENTRA_ID_TENANT_ID",
|
|
116
|
+
"value": "TENANT_ID",
|
|
117
|
+
"slotSetting": false
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
"name": "AUTH_SECRET",
|
|
121
|
+
"value": "AUTH_SECRET",
|
|
122
|
+
"slotSetting": false
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
"name": "AUTH_URL",
|
|
126
|
+
"value": "https://WEBSITE_URL/",
|
|
127
|
+
"slotSetting": false
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
"name": "DYNAMICS_API_URL",
|
|
131
|
+
"value": "https://DYNAMICS_URL/",
|
|
132
|
+
"slotSetting": false
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
"name": "NEXT_PUBLIC_BASE_URL",
|
|
136
|
+
"value": "https://BASE_URL",
|
|
137
|
+
"slotSetting": false
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
"name": "SCM_DO_BUILD_DURING_DEPLOYMENT",
|
|
141
|
+
"value": "false",
|
|
142
|
+
"slotSetting": false
|
|
143
|
+
}
|
|
144
|
+
]
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
Notes:
|
|
148
|
+
|
|
149
|
+
- If your CI pipeline builds the app and you deploy the standalone output, set `SCM_DO_BUILD_DURING_DEPLOYMENT` to `false` to prevent an extra App Service build.
|
|
150
|
+
- If you push source and want App Service (Kudu/Oryx) to build, set `SCM_DO_BUILD_DURING_DEPLOYMENT` to `true`.
|
|
151
|
+
- This template’s `auth.ts` uses `AZURE_AD_CLIENT_ID|SECRET|TENANT_ID`. If you prefer Auth.js environment variable names (`AUTH_MICROSOFT_ENTRA_ID_*`), update `auth.ts` accordingly or map the variables in your deployment.
|
|
152
|
+
|
|
153
|
+
Step 3 — Startup command
|
|
154
|
+
|
|
155
|
+
- In App Service > Settings > Configuration, set Startup command:
|
|
156
|
+
|
|
157
|
+
```
|
|
158
|
+
node server.js
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
- This ensures the standalone Next.js server is launched on startup. Without it, you may see “next not found” in Log Stream and an Application Error on launch.
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# EC Power Pages Single Page Application (SPA)
|
|
2
|
+
|
|
3
|
+
This is a Power Pages Single Page Application (SPA) built with React, Vite, and TypeScript, generated by `create-ec-app`. It follows Microsoft’s official Power Pages code site model, providing a modern, scalable, and maintainable frontend architecture tailored specifically for Power Pages environments. The app includes integrated authentication, data fetching, and UI options designed to work seamlessly within the Power Pages runtime.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **React + Vite + TypeScript**: Modern frontend stack for fast development and optimized builds.
|
|
8
|
+
- **Tailwind CSS**: Utility-first CSS framework with an optional Kendo UI preset for styling consistency.
|
|
9
|
+
- **UI Library Choice**: Select between Kendo UI (including theme selection) or Shadcn/ui components.
|
|
10
|
+
- **TanStack Query**: Preconfigured React Query provider for efficient data fetching and caching.
|
|
11
|
+
- **Power Pages AuthContext**: Integrated authentication context that detects the Power Pages user (`Microsoft.Dynamics365.Portal.User`) and retrieves tokens via `window.shell.getTokenDeferred()`.
|
|
12
|
+
- **Local Development Support**: Includes a mock token file and configuration to simulate authentication during local development.
|
|
13
|
+
- **Optimized Vite Build Output**: Builds optimized assets tailored for upload to Power Pages code sites.
|
|
14
|
+
|
|
15
|
+
## Auth and API Access
|
|
16
|
+
|
|
17
|
+
This SPA integrates with Power Pages authentication and API access mechanisms. The `AuthContext` handles user detection and token retrieval using the Power Pages runtime API. Tokens are obtained asynchronously through `window.shell.getTokenDeferred()`, enabling authenticated API calls to Power Pages endpoints.
|
|
18
|
+
|
|
19
|
+
### Example: API URL and Auth Headers
|
|
20
|
+
|
|
21
|
+
Here is an example of utility functions to build API URLs and set authentication headers for requests within the Power Pages SPA:
|
|
22
|
+
|
|
23
|
+
```ts
|
|
24
|
+
// src/utils/api.ts
|
|
25
|
+
|
|
26
|
+
export function getApiUrl(entity: string, query = ''): string {
|
|
27
|
+
// Base path for Power Pages API (adjust if your site uses a different base)
|
|
28
|
+
const baseUrl = '/_api';
|
|
29
|
+
return `${baseUrl}/${entity}${query ? `?${query}` : ''}`;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function getAuthHeaders(token: string) {
|
|
33
|
+
return {
|
|
34
|
+
Authorization: `Bearer ${token}`,
|
|
35
|
+
'Content-Type': 'application/json',
|
|
36
|
+
Accept: 'application/json',
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### Azure CLI Token Generation for Local Development
|
|
42
|
+
|
|
43
|
+
For local development and debugging outside the Power Pages runtime, you can generate an Azure AD token using the Azure CLI. This token can be used to simulate authenticated requests against your Power Pages environment:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
az account get-access-token --resource=https://<your-powerpages-environment>.crm.dynamics.com
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Replace `<your-powerpages-environment>` with your actual environment URL. Use the obtained token in your local mock token file or directly in API request headers.
|
|
50
|
+
|
|
51
|
+
## Example Service Using TanStack Query
|
|
52
|
+
|
|
53
|
+
Here is an example of a service module `src/services/contacts.ts` that demonstrates fetching contacts data from the Power Pages API using TanStack Query:
|
|
54
|
+
|
|
55
|
+
```ts
|
|
56
|
+
// src/services/contacts.ts
|
|
57
|
+
import { useQuery } from '@tanstack/react-query';
|
|
58
|
+
import { getApiUrl, getAuthHeaders } from '@/utils/api';
|
|
59
|
+
import { useAuth } from '@/context/AuthContext';
|
|
60
|
+
|
|
61
|
+
export interface Contact {
|
|
62
|
+
contactid: string;
|
|
63
|
+
firstname: string;
|
|
64
|
+
lastname: string;
|
|
65
|
+
emailaddress1?: string;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async function fetchContacts(token: string): Promise<Contact[]> {
|
|
69
|
+
const response = await fetch(getApiUrl('contacts'), {
|
|
70
|
+
headers: getAuthHeaders(token),
|
|
71
|
+
});
|
|
72
|
+
if (!response.ok) {
|
|
73
|
+
throw new Error('Failed to fetch contacts');
|
|
74
|
+
}
|
|
75
|
+
const data = await response.json();
|
|
76
|
+
return data.value;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function useContacts() {
|
|
80
|
+
const { token } = useAuth();
|
|
81
|
+
|
|
82
|
+
return useQuery(['contacts'], () => fetchContacts(token!), {
|
|
83
|
+
enabled: !!token,
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
This pattern leverages the `AuthContext` to access the current token and uses TanStack Query to manage caching and background updates.
|
|
89
|
+
|
|
90
|
+
## Deployment
|
|
91
|
+
|
|
92
|
+
To deploy your Power Pages SPA:
|
|
93
|
+
|
|
94
|
+
- Build the project using `npm run build` or `npm run build:dev` for development builds.
|
|
95
|
+
- Upload the contents of the `dist` folder to your Power Pages site’s **code site**. This is a dedicated location for hosting SPA assets as static content.
|
|
96
|
+
- Reference your built JavaScript and CSS files in your Power Pages pages as needed.
|
|
97
|
+
|
|
98
|
+
For detailed guidance on Power Pages code sites and deployment, see the [Microsoft Power Pages documentation on code sites](https://learn.microsoft.com/en-us/power-pages/developer/code-sites).
|
|
99
|
+
|
|
100
|
+
Power Pages code sites support continuous deployment workflows via the Power Platform CLI, enabling streamlined updates and integration with source control.
|
|
101
|
+
|
|
102
|
+
## Configuration
|
|
103
|
+
|
|
104
|
+
The `powerpages.config.json` file defines your site configuration, including output paths and landing page settings. Adjust this file to match your project and deployment conventions.
|
|
105
|
+
|
|
106
|
+
---
|
|
107
|
+
|
|
108
|
+
This SPA template provides a robust foundation for building Power Pages applications with modern web development tools and best practices, ensuring smooth integration with the Power Pages platform and scalable, maintainable code.
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
# EC Webresource App
|
|
2
|
+
|
|
3
|
+
React + TypeScript template for Dynamics 365/Dataverse web resources. Generated by `create-ec-app`, it provides a lean setup with Vite, Tailwind CSS, and a choice of UI library (Kendo UI or Shadcn/ui), pre-configured for running inside Dynamics 365 and for local development.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- React + TypeScript with Vite (Client side only)
|
|
8
|
+
- Tailwind CSS configured out of the box
|
|
9
|
+
- UI library choice: Kendo UI (with theme selection) or Shadcn/ui
|
|
10
|
+
- TanStack Query provider pre-wired (`QueryClientProvider` in `src/main.tsx`)
|
|
11
|
+
- XRM-aware runtime: adds `ClientGlobalContext.js.aspx` and detects `window.Xrm`
|
|
12
|
+
- Local development via `token.json` (excluded from bundling) and helper functions
|
|
13
|
+
- Vite build tuned for web resources: single JS bundle, `main.css`, deterministic names
|
|
14
|
+
- Zustand and `@types/xrm` included for state and typings
|
|
15
|
+
|
|
16
|
+
## Prerequisites
|
|
17
|
+
|
|
18
|
+
- Node.js 18+ and npm 9+
|
|
19
|
+
- Dynamics 365/Dataverse environment for deployment
|
|
20
|
+
- Kendo UI license (only if you picked Kendo UI)
|
|
21
|
+
|
|
22
|
+
## Getting Started
|
|
23
|
+
|
|
24
|
+
- Install dependencies (if needed): `npm install`
|
|
25
|
+
- For Kendo UI projects, activate your license: `npx kendo-ui-license activate` (you need to copy over your kendo-license.txt to the root to activate or stick with non-premium components)
|
|
26
|
+
- Start dev server: `npm run dev`
|
|
27
|
+
- Production build: `npm run build`
|
|
28
|
+
- Development build (readable, no minify): `npm run build:dev`
|
|
29
|
+
|
|
30
|
+
## Key Files
|
|
31
|
+
|
|
32
|
+
- `src/main.tsx`: Sets up React, Tailwind, and TanStack Query. If Kendo UI was selected, imports the chosen theme CSS (`<theme>/dist/all.css`).
|
|
33
|
+
- `src/services/authService.ts`: Utilities to build API URLs and headers based on environment (inside Dynamics vs. local dev).
|
|
34
|
+
- `token.json`: Local development token store. Build is configured to treat this as external and not bundle it.
|
|
35
|
+
- `index.html`: Injects `ClientGlobalContext.js.aspx` for Dynamics runtime.
|
|
36
|
+
- `vite.config.ts`: Uses base `./`, disables code splitting, emits `main.css`, and places assets at the top of `dist`.
|
|
37
|
+
|
|
38
|
+
## Auth and API Access
|
|
39
|
+
|
|
40
|
+
The app auto-detects whether it runs inside Dynamics 365 (uses `window.Xrm` and does not add an Authorization header) or locally (reads a bearer token from `token.json`).
|
|
41
|
+
|
|
42
|
+
```ts
|
|
43
|
+
// src/services/authService.ts
|
|
44
|
+
export const getApiUrl = (): string => {
|
|
45
|
+
if (window.parent && window.parent.Xrm) {
|
|
46
|
+
const clientUrl = window.Xrm.Utility.getGlobalContext().getClientUrl();
|
|
47
|
+
return `${clientUrl}/api/data/v9.2`;
|
|
48
|
+
}
|
|
49
|
+
return "https://DOMAIN.REGION.dynamics.com/api/data/v9.2";
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
export const getAuthHeaders = async (): Promise<HeadersInit> => {
|
|
53
|
+
if (window.parent && window.parent.Xrm) {
|
|
54
|
+
return {
|
|
55
|
+
"Content-Type": "application/json",
|
|
56
|
+
"OData-MaxVersion": "4.0",
|
|
57
|
+
"OData-Version": "4.0",
|
|
58
|
+
Prefer: 'odata.include-annotations="*"',
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
const { default: token } = await import("../../token.json");
|
|
62
|
+
return {
|
|
63
|
+
Authorization: `Bearer ${token.accessToken}`,
|
|
64
|
+
"Content-Type": "application/json",
|
|
65
|
+
"OData-MaxVersion": "4.0",
|
|
66
|
+
"OData-Version": "4.0",
|
|
67
|
+
Prefer: 'odata.include-annotations="*"',
|
|
68
|
+
};
|
|
69
|
+
};
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Example usage:
|
|
73
|
+
|
|
74
|
+
```ts
|
|
75
|
+
import { getApiUrl, getAuthHeaders } from "@/services/authService";
|
|
76
|
+
|
|
77
|
+
const res = await fetch(`${getApiUrl()}/accounts?$top=10`, {
|
|
78
|
+
headers: await getAuthHeaders(),
|
|
79
|
+
});
|
|
80
|
+
const data = await res.json();
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## Azure CLI: Generate a Dataverse Token (Local Dev)
|
|
84
|
+
|
|
85
|
+
Use Azure CLI to obtain a bearer token for your Dataverse environment and paste it into `token.json` for local development.
|
|
86
|
+
|
|
87
|
+
Prerequisites:
|
|
88
|
+
|
|
89
|
+
- Azure CLI installed and you have access to the target tenant and environment.
|
|
90
|
+
|
|
91
|
+
Steps:
|
|
92
|
+
|
|
93
|
+
1. Sign in to Azure (optionally targeting a specific tenant):
|
|
94
|
+
|
|
95
|
+
```
|
|
96
|
+
az login --tenant <tenant-id>
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
2. Request an access token for your Dataverse environment URL (replace `<org>` and domain as applicable):
|
|
100
|
+
|
|
101
|
+
```
|
|
102
|
+
az account get-access-token \
|
|
103
|
+
--tenant <tenant-id> \
|
|
104
|
+
--resource https://<org>.crm.dynamics.com \
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
This prints the raw token to stdout. Copy it.
|
|
108
|
+
|
|
109
|
+
3. Paste the token value into `token.json` under `accessToken`:
|
|
110
|
+
|
|
111
|
+
```
|
|
112
|
+
{
|
|
113
|
+
"accessToken": "<paste-token-here>",
|
|
114
|
+
"expiresIn": "",
|
|
115
|
+
"expires_on": 0,
|
|
116
|
+
"subscription": "",
|
|
117
|
+
"tenant": "",
|
|
118
|
+
"tokenType": "Bearer"
|
|
119
|
+
}
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
Notes:
|
|
123
|
+
|
|
124
|
+
- Tokens expire; regenerate as needed. (Max expiry is 1 hour)
|
|
125
|
+
- If your CLI requires scopes, use `--scope https://<org>.crm.dynamics.com/.default` instead of `--resource`.
|
|
126
|
+
|
|
127
|
+
One‑liner (writes full JSON to `token.json`):
|
|
128
|
+
|
|
129
|
+
```
|
|
130
|
+
az account get-access-token --resource=https://<org>.crm.dynamics.com > token.json
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
Replace the URL with your environment’s Dataverse URL (e.g., `https://<org>.crm.dynamics.com`, `https://<org>.crm4.dynamics.com`, etc.). This overwrites `token.json` at the project root with the full output from Azure CLI, which includes `accessToken` used by the local dev flow.
|
|
134
|
+
|
|
135
|
+
## Accounts Data Service (TanStack Query)
|
|
136
|
+
|
|
137
|
+
The following example shows a minimal data service and hooks to fetch and update Accounts using the Dataverse Web API and TanStack Query.
|
|
138
|
+
|
|
139
|
+
Create `src/services/accounts.ts`:
|
|
140
|
+
|
|
141
|
+
```ts
|
|
142
|
+
import { getApiUrl, getAuthHeaders } from "@/services/authService";
|
|
143
|
+
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
|
144
|
+
|
|
145
|
+
export interface Account {
|
|
146
|
+
accountid: string;
|
|
147
|
+
name?: string | null;
|
|
148
|
+
description?: string | null;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export const listAccounts = async (): Promise<Account[]> => {
|
|
152
|
+
const res = await fetch(
|
|
153
|
+
`${getApiUrl()}/accounts?$select=accountid,name,description&$top=50`,
|
|
154
|
+
{ headers: await getAuthHeaders() }
|
|
155
|
+
);
|
|
156
|
+
if (!res.ok) throw new Error(`Failed to fetch accounts: ${res.status}`);
|
|
157
|
+
const json = await res.json();
|
|
158
|
+
return json.value as Account[];
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
export const patchAccount = async (
|
|
162
|
+
id: string,
|
|
163
|
+
data: Partial<Account>
|
|
164
|
+
): Promise<void> => {
|
|
165
|
+
const headers = await getAuthHeaders();
|
|
166
|
+
const res = await fetch(`${getApiUrl()}/accounts(${id})`, {
|
|
167
|
+
method: "PATCH",
|
|
168
|
+
headers: {
|
|
169
|
+
...headers,
|
|
170
|
+
},
|
|
171
|
+
body: JSON.stringify(data),
|
|
172
|
+
});
|
|
173
|
+
if (!res.ok) throw new Error(`Failed to update account: ${res.status}`);
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
export const useAccounts = () =>
|
|
177
|
+
useQuery({ queryKey: ["accounts"], queryFn: listAccounts });
|
|
178
|
+
|
|
179
|
+
export const useUpdateAccount = () => {
|
|
180
|
+
const qc = useQueryClient();
|
|
181
|
+
return useMutation({
|
|
182
|
+
mutationFn: ({ id, data }: { id: string; data: Partial<Account> }) =>
|
|
183
|
+
patchAccount(id, data),
|
|
184
|
+
onSuccess: () => {
|
|
185
|
+
qc.invalidateQueries({ queryKey: ["accounts"] });
|
|
186
|
+
},
|
|
187
|
+
});
|
|
188
|
+
};
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
Usage in a component (e.g. `src/AccountsList.tsx`):
|
|
192
|
+
|
|
193
|
+
```tsx
|
|
194
|
+
import { useAccounts, useUpdateAccount } from "@/services/accounts";
|
|
195
|
+
|
|
196
|
+
export function AccountsList() {
|
|
197
|
+
const { data, isLoading, error } = useAccounts();
|
|
198
|
+
const updateAccount = useUpdateAccount();
|
|
199
|
+
|
|
200
|
+
if (isLoading) return <div>Loading…</div>;
|
|
201
|
+
if (error) return <div>Failed to load accounts</div>;
|
|
202
|
+
|
|
203
|
+
return (
|
|
204
|
+
<ul className="space-y-2">
|
|
205
|
+
{data?.map((a) => (
|
|
206
|
+
<li key={a.accountid} className="flex items-center gap-2">
|
|
207
|
+
<span className="flex-1">{a.name ?? "(no name)"}</span>
|
|
208
|
+
<button
|
|
209
|
+
className="px-2 py-1 border rounded"
|
|
210
|
+
onClick={() =>
|
|
211
|
+
updateAccount.mutate({
|
|
212
|
+
id: a.accountid,
|
|
213
|
+
data: { name: `${a.name ?? ""}*` },
|
|
214
|
+
})
|
|
215
|
+
}
|
|
216
|
+
>
|
|
217
|
+
Rename
|
|
218
|
+
</button>
|
|
219
|
+
</li>
|
|
220
|
+
))}
|
|
221
|
+
</ul>
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
## UI Libraries
|
|
227
|
+
|
|
228
|
+
- Kendo UI: Theme CSS is imported in `src/main.tsx`. Example:
|
|
229
|
+
```tsx
|
|
230
|
+
import { Button } from "@progress/kendo-react-buttons";
|
|
231
|
+
<Button>Click me</Button>;
|
|
232
|
+
```
|
|
233
|
+
- Shadcn/ui: Components are installed and available under the `@/components` alias. Example:
|
|
234
|
+
```tsx
|
|
235
|
+
import { Button } from "@/components/ui/button";
|
|
236
|
+
<Button>Click me</Button>;
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
## Build Output
|
|
240
|
+
|
|
241
|
+
- Output directory: `dist`
|
|
242
|
+
- Single JavaScript bundle with deterministic name
|
|
243
|
+
- Single CSS file: `main.css`
|
|
244
|
+
- `base: "./"` to support deployment as a web resource
|
|
245
|
+
|
|
246
|
+
## Deployment
|
|
247
|
+
|
|
248
|
+
- Upload files from `dist` to Dynamics 365/Dataverse as web resources.
|
|
249
|
+
- Use `index.html` as the HTML web resource; upload the JS bundle and `main.css` as script/style web resources referenced by it.
|
|
250
|
+
- Consider automating uploads with your preferred tooling (DevOps pipelines, XrmToolBox, etc.).
|
|
251
|
+
- One quick and easy way to handle deployment is with Webresource Manager.
|
|
252
|
+
- Open up webresource manager, and navigate to your specific solution
|
|
253
|
+
- Create a new root (example GlobalAccounts* or CustomDev*)
|
|
254
|
+
- Add a new folder for your webresources
|
|
255
|
+
- Upload your index.html, index.js and main.css to your folder.
|
|
256
|
+
- This will now allow you to use auto publisher to bind to your deployed resources.
|
|
257
|
+
|
|
258
|
+
## Notes
|
|
259
|
+
|
|
260
|
+
- If you change the build, ensure code splitting stays disabled and asset names remain predictable to simplify web resource updates.
|