@khester/create-dynamics-app 1.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/bin/create-dynamics-app.js +2 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +102 -0
- package/dist/index.js.map +1 -0
- package/dist/utils/copyTemplate.d.ts +2 -0
- package/dist/utils/copyTemplate.d.ts.map +1 -0
- package/dist/utils/copyTemplate.js +28 -0
- package/dist/utils/copyTemplate.js.map +1 -0
- package/dist/utils/initGit.d.ts +2 -0
- package/dist/utils/initGit.d.ts.map +1 -0
- package/dist/utils/initGit.js +122 -0
- package/dist/utils/initGit.js.map +1 -0
- package/dist/utils/installDependencies.d.ts +2 -0
- package/dist/utils/installDependencies.d.ts.map +1 -0
- package/dist/utils/installDependencies.js +40 -0
- package/dist/utils/installDependencies.js.map +1 -0
- package/dist/utils/updatePackageJson.d.ts +2 -0
- package/dist/utils/updatePackageJson.d.ts.map +1 -0
- package/dist/utils/updatePackageJson.js +24 -0
- package/dist/utils/updatePackageJson.js.map +1 -0
- package/package.json +51 -0
- package/templates/dynamics-365-starter/README.md +178 -0
- package/templates/dynamics-365-starter/package.json +44 -0
- package/templates/dynamics-365-starter/public/index.html +18 -0
- package/templates/dynamics-365-starter/src/components/ContactForm.css +48 -0
- package/templates/dynamics-365-starter/src/components/ContactForm.tsx +241 -0
- package/templates/dynamics-365-starter/src/components/ContactManagement.css +86 -0
- package/templates/dynamics-365-starter/src/components/ContactManagement.tsx +267 -0
- package/templates/dynamics-365-starter/src/index.tsx +40 -0
- package/templates/dynamics-365-starter/src/pcf/ContactControlWrapper.tsx +54 -0
- package/templates/dynamics-365-starter/src/providers/DynamicsProvider.tsx +136 -0
- package/templates/dynamics-365-starter/src/styles/index.css +104 -0
- package/templates/dynamics-365-starter/tsconfig.json +26 -0
- package/templates/dynamics-365-starter/webpack.config.js +58 -0
- package/templates/power-pages-starter/.env.example +6 -0
- package/templates/power-pages-starter/README.md +89 -0
- package/templates/power-pages-starter/package.json +42 -0
- package/templates/power-pages-starter/public/index.html +18 -0
- package/templates/power-pages-starter/src/components/ContactForm.css +84 -0
- package/templates/power-pages-starter/src/components/ContactForm.tsx +239 -0
- package/templates/power-pages-starter/src/index.tsx +32 -0
- package/templates/power-pages-starter/src/providers/PowerPagesProvider.tsx +139 -0
- package/templates/power-pages-starter/src/styles/index.css +76 -0
- package/templates/power-pages-starter/tsconfig.json +26 -0
- package/templates/power-pages-starter/webpack.config.js +52 -0
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { ContactManagement } from '../components/ContactManagement';
|
|
3
|
+
import { DynamicsProvider } from '../providers/DynamicsProvider';
|
|
4
|
+
|
|
5
|
+
interface PCFContextType {
|
|
6
|
+
// Define PCF context properties based on your needs
|
|
7
|
+
webAPI: any;
|
|
8
|
+
utils: any;
|
|
9
|
+
parameters: any;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface ContactControlWrapperProps {
|
|
13
|
+
context: PCFContextType;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Wrapper component for integrating ContactManagement with PCF (PowerApps Component Framework)
|
|
18
|
+
* This allows the component to be used as a custom control in Dynamics 365 forms and views
|
|
19
|
+
*/
|
|
20
|
+
export const ContactControlWrapper: React.FC<ContactControlWrapperProps> = ({ context }) => {
|
|
21
|
+
// Extract configuration from PCF context
|
|
22
|
+
const baseUrl = context.parameters?.baseUrl?.raw || '';
|
|
23
|
+
|
|
24
|
+
// Create a custom API service that uses PCF's webAPI
|
|
25
|
+
const createPCFApiService = () => ({
|
|
26
|
+
createRecord: async (entityName: string, data: any) => {
|
|
27
|
+
return await context.webAPI.createRecord(entityName, data);
|
|
28
|
+
},
|
|
29
|
+
retrieveRecord: async (entityName: string, id: string, select?: string) => {
|
|
30
|
+
return await context.webAPI.retrieveRecord(entityName, id, select);
|
|
31
|
+
},
|
|
32
|
+
updateRecord: async (entityName: string, id: string, data: any) => {
|
|
33
|
+
return await context.webAPI.updateRecord(entityName, id, data);
|
|
34
|
+
},
|
|
35
|
+
deleteRecord: async (entityName: string, id: string) => {
|
|
36
|
+
return await context.webAPI.deleteRecord(entityName, id);
|
|
37
|
+
},
|
|
38
|
+
retrieveMultiple: async (entityName: string, query?: string) => {
|
|
39
|
+
const fetchXml = query || `<fetch><entity name="${entityName}"/></fetch>`;
|
|
40
|
+
return await context.webAPI.retrieveMultipleRecords(entityName, `?fetchXml=${encodeURIComponent(fetchXml)}`);
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
return (
|
|
45
|
+
<DynamicsProvider baseUrl={baseUrl}>
|
|
46
|
+
<div style={{ width: '100%', height: '100%' }}>
|
|
47
|
+
<ContactManagement />
|
|
48
|
+
</div>
|
|
49
|
+
</DynamicsProvider>
|
|
50
|
+
);
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
// Export for PCF integration
|
|
54
|
+
export default ContactControlWrapper;
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import React, { createContext, useContext, useState, useEffect } from 'react';
|
|
2
|
+
import { DynamicsApiService } from '@khester1/dynamics-ui-api-client';
|
|
3
|
+
|
|
4
|
+
interface DynamicsContextType {
|
|
5
|
+
apiService: DynamicsApiService | null;
|
|
6
|
+
createRecord: (entityName: string, data: any) => Promise<any>;
|
|
7
|
+
retrieveRecord: (entityName: string, id: string, select?: string) => Promise<any>;
|
|
8
|
+
updateRecord: (entityName: string, id: string, data: any) => Promise<any>;
|
|
9
|
+
deleteRecord: (entityName: string, id: string) => Promise<void>;
|
|
10
|
+
retrieveMultiple: (entityName: string, query?: string) => Promise<any>;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const DynamicsContext = createContext<DynamicsContextType | undefined>(undefined);
|
|
14
|
+
|
|
15
|
+
interface DynamicsProviderProps {
|
|
16
|
+
children: React.ReactNode;
|
|
17
|
+
baseUrl?: string;
|
|
18
|
+
accessToken?: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const DynamicsProvider: React.FC<DynamicsProviderProps> = ({
|
|
22
|
+
children,
|
|
23
|
+
baseUrl,
|
|
24
|
+
accessToken
|
|
25
|
+
}) => {
|
|
26
|
+
const [apiService, setApiService] = useState<DynamicsApiService | null>(null);
|
|
27
|
+
|
|
28
|
+
useEffect(() => {
|
|
29
|
+
// Initialize the API service
|
|
30
|
+
// In a real D365 environment, you would get these from your authentication flow
|
|
31
|
+
const getAccessToken = async () => {
|
|
32
|
+
return accessToken || process.env.DYNAMICS_ACCESS_TOKEN || '';
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const service = new DynamicsApiService(
|
|
36
|
+
baseUrl || process.env.DYNAMICS_BASE_URL || 'https://org.crm.dynamics.com',
|
|
37
|
+
getAccessToken
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
setApiService(service);
|
|
41
|
+
}, [baseUrl, accessToken]);
|
|
42
|
+
|
|
43
|
+
const createRecord = async (entityName: string, data: any) => {
|
|
44
|
+
if (!apiService) throw new Error('API service not initialized');
|
|
45
|
+
return await apiService.createRecord(entityName, data);
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const retrieveRecord = async (entityName: string, id: string, select?: string) => {
|
|
49
|
+
if (!apiService) throw new Error('API service not initialized');
|
|
50
|
+
// Use retrieveMultipleRecords with FetchXML to get a single record
|
|
51
|
+
const selectAttributes = select ? select.split(',').map(attr => attr.trim()) : ['*'];
|
|
52
|
+
const attributes = selectAttributes.map(attr => attr === '*' ? '' : `<attribute name="${attr}" />`).join('');
|
|
53
|
+
const fetchXml = `
|
|
54
|
+
<fetch top="1">
|
|
55
|
+
<entity name="${entityName}">
|
|
56
|
+
${attributes}
|
|
57
|
+
<filter>
|
|
58
|
+
<condition attribute="${entityName}id" operator="eq" value="${id}" />
|
|
59
|
+
</filter>
|
|
60
|
+
</entity>
|
|
61
|
+
</fetch>
|
|
62
|
+
`;
|
|
63
|
+
const result = await apiService.retrieveMultipleRecords(entityName, fetchXml);
|
|
64
|
+
return result.entities.length > 0 ? result.entities[0] : null;
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const updateRecord = async (entityName: string, id: string, data: any) => {
|
|
68
|
+
if (!apiService) throw new Error('API service not initialized');
|
|
69
|
+
return await apiService.updateRecord(entityName, id, data);
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const deleteRecord = async (entityName: string, id: string) => {
|
|
73
|
+
if (!apiService) throw new Error('API service not initialized');
|
|
74
|
+
return await apiService.deleteRecord(entityName, id);
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const retrieveMultiple = async (entityName: string, query?: string) => {
|
|
78
|
+
if (!apiService) throw new Error('API service not initialized');
|
|
79
|
+
// Convert OData-style query to FetchXML
|
|
80
|
+
let fetchXml = `<fetch>`;
|
|
81
|
+
|
|
82
|
+
if (query) {
|
|
83
|
+
// Parse basic OData query parameters
|
|
84
|
+
const selectMatch = query.match(/\$select=([^&]*)/i);
|
|
85
|
+
const orderByMatch = query.match(/\$orderby=([^&]*)/i);
|
|
86
|
+
const topMatch = query.match(/\$top=(\d+)/i);
|
|
87
|
+
|
|
88
|
+
if (topMatch) {
|
|
89
|
+
fetchXml = `<fetch top="${topMatch[1]}">`;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
fetchXml += `<entity name="${entityName}">`;
|
|
93
|
+
|
|
94
|
+
if (selectMatch) {
|
|
95
|
+
const attributes = selectMatch[1].split(',').map(attr => attr.trim());
|
|
96
|
+
attributes.forEach(attr => {
|
|
97
|
+
fetchXml += `<attribute name="${attr}" />`;
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (orderByMatch) {
|
|
102
|
+
const [field, direction] = orderByMatch[1].split(' ');
|
|
103
|
+
fetchXml += `<order attribute="${field.trim()}" descending="${direction?.toLowerCase() === 'desc'}" />`;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
fetchXml += `</entity></fetch>`;
|
|
107
|
+
} else {
|
|
108
|
+
fetchXml = `<fetch><entity name="${entityName}"></entity></fetch>`;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return await apiService.retrieveMultipleRecords(entityName, fetchXml);
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
const value: DynamicsContextType = {
|
|
115
|
+
apiService,
|
|
116
|
+
createRecord,
|
|
117
|
+
retrieveRecord,
|
|
118
|
+
updateRecord,
|
|
119
|
+
deleteRecord,
|
|
120
|
+
retrieveMultiple,
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
return (
|
|
124
|
+
<DynamicsContext.Provider value={value}>
|
|
125
|
+
{children}
|
|
126
|
+
</DynamicsContext.Provider>
|
|
127
|
+
);
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
export const useDynamicsApi = (): DynamicsContextType => {
|
|
131
|
+
const context = useContext(DynamicsContext);
|
|
132
|
+
if (context === undefined) {
|
|
133
|
+
throw new Error('useDynamicsApi must be used within a DynamicsProvider');
|
|
134
|
+
}
|
|
135
|
+
return context;
|
|
136
|
+
};
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/* Global Styles for Dynamics 365 App */
|
|
2
|
+
* {
|
|
3
|
+
box-sizing: border-box;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
body {
|
|
7
|
+
margin: 0;
|
|
8
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
|
9
|
+
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
|
|
10
|
+
sans-serif;
|
|
11
|
+
-webkit-font-smoothing: antialiased;
|
|
12
|
+
-moz-osx-font-smoothing: grayscale;
|
|
13
|
+
background-color: #f5f5f5;
|
|
14
|
+
color: #323130;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
.app {
|
|
18
|
+
min-height: 100vh;
|
|
19
|
+
display: flex;
|
|
20
|
+
flex-direction: column;
|
|
21
|
+
background-color: #faf9f8;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
.app-header {
|
|
25
|
+
background: linear-gradient(135deg, #0078d4 0%, #106ebe 100%);
|
|
26
|
+
color: white;
|
|
27
|
+
padding: 16px 24px;
|
|
28
|
+
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
.app-header h1 {
|
|
32
|
+
margin: 0 0 4px 0;
|
|
33
|
+
font-size: 24px;
|
|
34
|
+
font-weight: 600;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
.app-header p {
|
|
38
|
+
margin: 0;
|
|
39
|
+
font-size: 14px;
|
|
40
|
+
opacity: 0.9;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
.app-main {
|
|
44
|
+
flex: 1;
|
|
45
|
+
padding: 0;
|
|
46
|
+
max-width: 1200px;
|
|
47
|
+
margin: 0 auto;
|
|
48
|
+
width: 100%;
|
|
49
|
+
background-color: white;
|
|
50
|
+
margin-top: 16px;
|
|
51
|
+
margin-bottom: 16px;
|
|
52
|
+
border-radius: 8px;
|
|
53
|
+
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
.app-footer {
|
|
57
|
+
background-color: #f3f2f1;
|
|
58
|
+
border-top: 1px solid #edebe9;
|
|
59
|
+
padding: 12px 16px;
|
|
60
|
+
text-align: center;
|
|
61
|
+
color: #605e5c;
|
|
62
|
+
font-size: 12px;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
.app-footer p {
|
|
66
|
+
margin: 0;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/* Dynamics 365 specific styles */
|
|
70
|
+
.ms-Panel {
|
|
71
|
+
box-shadow: 0 6.4px 14.4px 0 rgba(0, 0, 0, 0.132), 0 1.2px 3.6px 0 rgba(0, 0, 0, 0.108);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
.ms-DetailsList {
|
|
75
|
+
border-radius: 0;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
.ms-DetailsHeader {
|
|
79
|
+
background-color: #f8f8f8;
|
|
80
|
+
border-bottom: 1px solid #edebe9;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
.ms-DetailsRow:hover {
|
|
84
|
+
background-color: #f3f2f1;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
.ms-DetailsRow.is-selected {
|
|
88
|
+
background-color: #deecf9;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
@media (max-width: 768px) {
|
|
92
|
+
.app-header {
|
|
93
|
+
padding: 12px 16px;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
.app-header h1 {
|
|
97
|
+
font-size: 20px;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
.app-main {
|
|
101
|
+
margin: 8px;
|
|
102
|
+
border-radius: 4px;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "es5",
|
|
4
|
+
"lib": [
|
|
5
|
+
"dom",
|
|
6
|
+
"dom.iterable",
|
|
7
|
+
"es6"
|
|
8
|
+
],
|
|
9
|
+
"allowJs": true,
|
|
10
|
+
"skipLibCheck": true,
|
|
11
|
+
"esModuleInterop": true,
|
|
12
|
+
"allowSyntheticDefaultImports": true,
|
|
13
|
+
"strict": true,
|
|
14
|
+
"forceConsistentCasingInFileNames": true,
|
|
15
|
+
"noFallthroughCasesInSwitch": true,
|
|
16
|
+
"module": "esnext",
|
|
17
|
+
"moduleResolution": "node",
|
|
18
|
+
"resolveJsonModule": true,
|
|
19
|
+
"isolatedModules": true,
|
|
20
|
+
"noEmit": true,
|
|
21
|
+
"jsx": "react-jsx"
|
|
22
|
+
},
|
|
23
|
+
"include": [
|
|
24
|
+
"src"
|
|
25
|
+
]
|
|
26
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
const HtmlWebpackPlugin = require('html-webpack-plugin');
|
|
3
|
+
|
|
4
|
+
module.exports = (env, argv) => {
|
|
5
|
+
const isProduction = argv.mode === 'production';
|
|
6
|
+
|
|
7
|
+
return {
|
|
8
|
+
entry: './src/index.tsx',
|
|
9
|
+
output: {
|
|
10
|
+
path: path.resolve(__dirname, 'dist'),
|
|
11
|
+
filename: isProduction ? '[name].[contenthash].js' : '[name].js',
|
|
12
|
+
clean: true,
|
|
13
|
+
publicPath: '/',
|
|
14
|
+
library: 'DynamicsApp',
|
|
15
|
+
libraryTarget: 'umd',
|
|
16
|
+
},
|
|
17
|
+
resolve: {
|
|
18
|
+
extensions: ['.tsx', '.ts', '.js', '.jsx'],
|
|
19
|
+
},
|
|
20
|
+
module: {
|
|
21
|
+
rules: [
|
|
22
|
+
{
|
|
23
|
+
test: /\.tsx?$/,
|
|
24
|
+
use: 'ts-loader',
|
|
25
|
+
exclude: /node_modules/,
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
test: /\.css$/i,
|
|
29
|
+
use: ['style-loader', 'css-loader'],
|
|
30
|
+
},
|
|
31
|
+
],
|
|
32
|
+
},
|
|
33
|
+
plugins: [
|
|
34
|
+
new HtmlWebpackPlugin({
|
|
35
|
+
template: './public/index.html',
|
|
36
|
+
filename: 'index.html',
|
|
37
|
+
}),
|
|
38
|
+
],
|
|
39
|
+
devServer: {
|
|
40
|
+
static: {
|
|
41
|
+
directory: path.join(__dirname, 'public'),
|
|
42
|
+
},
|
|
43
|
+
port: 3000,
|
|
44
|
+
open: true,
|
|
45
|
+
hot: true,
|
|
46
|
+
historyApiFallback: true,
|
|
47
|
+
},
|
|
48
|
+
externals: isProduction ? {
|
|
49
|
+
'react': 'React',
|
|
50
|
+
'react-dom': 'ReactDOM'
|
|
51
|
+
} : {},
|
|
52
|
+
optimization: {
|
|
53
|
+
splitChunks: {
|
|
54
|
+
chunks: 'all',
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
};
|
|
@@ -0,0 +1,89 @@
|
|
|
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
|
+
```bash
|
|
9
|
+
cp .env.example .env
|
|
10
|
+
```
|
|
11
|
+
Edit `.env` and set your `PORTAL_URL` to your Power Pages portal URL.
|
|
12
|
+
|
|
13
|
+
2. **Install Dependencies**
|
|
14
|
+
```bash
|
|
15
|
+
npm install
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
3. **Development**
|
|
19
|
+
```bash
|
|
20
|
+
npm run dev
|
|
21
|
+
```
|
|
22
|
+
This starts the development server at http://localhost:3000
|
|
23
|
+
|
|
24
|
+
4. **Build for Production**
|
|
25
|
+
```bash
|
|
26
|
+
npm run build
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Project Structure
|
|
30
|
+
|
|
31
|
+
```
|
|
32
|
+
src/
|
|
33
|
+
├── components/ # React components
|
|
34
|
+
│ ├── ContactForm.tsx # Main contact form component
|
|
35
|
+
│ └── ContactForm.css # Component styles
|
|
36
|
+
├── providers/ # Context providers
|
|
37
|
+
│ └── PowerPagesProvider.tsx # Power Pages API provider
|
|
38
|
+
├── styles/ # Global styles
|
|
39
|
+
│ └── index.css # Global CSS
|
|
40
|
+
└── index.tsx # Application entry point
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Features
|
|
44
|
+
|
|
45
|
+
- **Contact Management**: Create and manage contacts using Dynamics UI Kit components
|
|
46
|
+
- **Power Pages Integration**: Seamless integration with Power Pages API
|
|
47
|
+
- **Responsive Design**: Mobile-friendly responsive layout
|
|
48
|
+
- **Type Safety**: Full TypeScript support
|
|
49
|
+
|
|
50
|
+
## Deployment
|
|
51
|
+
|
|
52
|
+
To deploy your application to Power Pages:
|
|
53
|
+
|
|
54
|
+
1. Build the application:
|
|
55
|
+
```bash
|
|
56
|
+
npm run build
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
2. Upload the `dist` folder contents to your Power Pages portal's web files
|
|
60
|
+
|
|
61
|
+
3. Create a new web page in Power Pages and reference your built JavaScript and CSS files
|
|
62
|
+
|
|
63
|
+
## Customization
|
|
64
|
+
|
|
65
|
+
### Adding New Components
|
|
66
|
+
|
|
67
|
+
1. Create your component in `src/components/`
|
|
68
|
+
2. Use Dynamics UI Kit components for consistent styling
|
|
69
|
+
3. Import and use the `usePowerPagesApi` hook for data operations
|
|
70
|
+
|
|
71
|
+
### Styling
|
|
72
|
+
|
|
73
|
+
- Global styles are in `src/styles/index.css`
|
|
74
|
+
- Component-specific styles should be co-located with components
|
|
75
|
+
- Follow the existing CSS naming conventions
|
|
76
|
+
|
|
77
|
+
## Available Scripts
|
|
78
|
+
|
|
79
|
+
- `npm run dev` - Start development server
|
|
80
|
+
- `npm run build` - Build for production
|
|
81
|
+
- `npm run typecheck` - Type check without building
|
|
82
|
+
- `npm run clean` - Clean build directory
|
|
83
|
+
- `npm run lint` - Lint code
|
|
84
|
+
|
|
85
|
+
## Learn More
|
|
86
|
+
|
|
87
|
+
- [Dynamics UI Kit Documentation](https://github.com/your-org/dynamics-ui-kit)
|
|
88
|
+
- [Power Pages Documentation](https://docs.microsoft.com/power-pages/)
|
|
89
|
+
- [React Documentation](https://reactjs.org/)
|
|
@@ -0,0 +1,42 @@
|
|
|
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
|
+
"@khester1/dynamics-ui-components": "^1.0.0",
|
|
15
|
+
"react": "^18.2.0",
|
|
16
|
+
"react-dom": "^18.2.0"
|
|
17
|
+
},
|
|
18
|
+
"devDependencies": {
|
|
19
|
+
"@types/react": "^18.2.0",
|
|
20
|
+
"@types/react-dom": "^18.2.0",
|
|
21
|
+
"css-loader": "^6.8.1",
|
|
22
|
+
"html-webpack-plugin": "^5.5.3",
|
|
23
|
+
"style-loader": "^3.3.3",
|
|
24
|
+
"ts-loader": "^9.5.1",
|
|
25
|
+
"typescript": "^5.3.3",
|
|
26
|
+
"webpack": "^5.89.0",
|
|
27
|
+
"webpack-cli": "^5.1.4",
|
|
28
|
+
"webpack-dev-server": "^4.15.1"
|
|
29
|
+
},
|
|
30
|
+
"browserslist": {
|
|
31
|
+
"production": [
|
|
32
|
+
">0.2%",
|
|
33
|
+
"not dead",
|
|
34
|
+
"not op_mini all"
|
|
35
|
+
],
|
|
36
|
+
"development": [
|
|
37
|
+
"last 1 chrome version",
|
|
38
|
+
"last 1 firefox version",
|
|
39
|
+
"last 1 safari version"
|
|
40
|
+
]
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8" />
|
|
5
|
+
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
|
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
7
|
+
<meta name="theme-color" content="#0078d4" />
|
|
8
|
+
<meta
|
|
9
|
+
name="description"
|
|
10
|
+
content="Power Pages application built with Dynamics UI Kit"
|
|
11
|
+
/>
|
|
12
|
+
<title>Power Pages App - Dynamics UI Kit</title>
|
|
13
|
+
</head>
|
|
14
|
+
<body>
|
|
15
|
+
<noscript>You need to enable JavaScript to run this app.</noscript>
|
|
16
|
+
<div id="root"></div>
|
|
17
|
+
</body>
|
|
18
|
+
</html>
|
|
@@ -0,0 +1,84 @@
|
|
|
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
|
+
}
|