@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.
Files changed (46) hide show
  1. package/bin/create-dynamics-app.js +2 -0
  2. package/dist/index.d.ts +3 -0
  3. package/dist/index.d.ts.map +1 -0
  4. package/dist/index.js +102 -0
  5. package/dist/index.js.map +1 -0
  6. package/dist/utils/copyTemplate.d.ts +2 -0
  7. package/dist/utils/copyTemplate.d.ts.map +1 -0
  8. package/dist/utils/copyTemplate.js +28 -0
  9. package/dist/utils/copyTemplate.js.map +1 -0
  10. package/dist/utils/initGit.d.ts +2 -0
  11. package/dist/utils/initGit.d.ts.map +1 -0
  12. package/dist/utils/initGit.js +122 -0
  13. package/dist/utils/initGit.js.map +1 -0
  14. package/dist/utils/installDependencies.d.ts +2 -0
  15. package/dist/utils/installDependencies.d.ts.map +1 -0
  16. package/dist/utils/installDependencies.js +40 -0
  17. package/dist/utils/installDependencies.js.map +1 -0
  18. package/dist/utils/updatePackageJson.d.ts +2 -0
  19. package/dist/utils/updatePackageJson.d.ts.map +1 -0
  20. package/dist/utils/updatePackageJson.js +24 -0
  21. package/dist/utils/updatePackageJson.js.map +1 -0
  22. package/package.json +51 -0
  23. package/templates/dynamics-365-starter/README.md +178 -0
  24. package/templates/dynamics-365-starter/package.json +44 -0
  25. package/templates/dynamics-365-starter/public/index.html +18 -0
  26. package/templates/dynamics-365-starter/src/components/ContactForm.css +48 -0
  27. package/templates/dynamics-365-starter/src/components/ContactForm.tsx +241 -0
  28. package/templates/dynamics-365-starter/src/components/ContactManagement.css +86 -0
  29. package/templates/dynamics-365-starter/src/components/ContactManagement.tsx +267 -0
  30. package/templates/dynamics-365-starter/src/index.tsx +40 -0
  31. package/templates/dynamics-365-starter/src/pcf/ContactControlWrapper.tsx +54 -0
  32. package/templates/dynamics-365-starter/src/providers/DynamicsProvider.tsx +136 -0
  33. package/templates/dynamics-365-starter/src/styles/index.css +104 -0
  34. package/templates/dynamics-365-starter/tsconfig.json +26 -0
  35. package/templates/dynamics-365-starter/webpack.config.js +58 -0
  36. package/templates/power-pages-starter/.env.example +6 -0
  37. package/templates/power-pages-starter/README.md +89 -0
  38. package/templates/power-pages-starter/package.json +42 -0
  39. package/templates/power-pages-starter/public/index.html +18 -0
  40. package/templates/power-pages-starter/src/components/ContactForm.css +84 -0
  41. package/templates/power-pages-starter/src/components/ContactForm.tsx +239 -0
  42. package/templates/power-pages-starter/src/index.tsx +32 -0
  43. package/templates/power-pages-starter/src/providers/PowerPagesProvider.tsx +139 -0
  44. package/templates/power-pages-starter/src/styles/index.css +76 -0
  45. package/templates/power-pages-starter/tsconfig.json +26 -0
  46. package/templates/power-pages-starter/webpack.config.js +52 -0
@@ -0,0 +1,239 @@
1
+ import React, { useState, useCallback } from 'react';
2
+ import {
3
+ Button,
4
+ TextField,
5
+ Dropdown,
6
+ DatePicker
7
+ } from '@khester1/dynamics-ui-components';
8
+ import { usePowerPagesApi } from '../providers/PowerPagesProvider';
9
+ import './ContactForm.css';
10
+
11
+ interface ContactFormData {
12
+ firstname: string;
13
+ lastname: string;
14
+ emailaddress1: string;
15
+ telephone1: string;
16
+ preferredcontactmethodcode: number;
17
+ birthdate: Date | null;
18
+ }
19
+
20
+ const preferredContactOptions = [
21
+ { key: 'email', text: 'Email' },
22
+ { key: 'phone', text: 'Phone' },
23
+ { key: 'mail', text: 'Mail' }
24
+ ];
25
+
26
+ export const ContactForm: React.FC = () => {
27
+ const { createRecord, isAuthenticated } = usePowerPagesApi();
28
+
29
+ const [formData, setFormData] = useState<ContactFormData>({
30
+ firstname: '',
31
+ lastname: '',
32
+ emailaddress1: '',
33
+ telephone1: '',
34
+ preferredcontactmethodcode: 1,
35
+ birthdate: null
36
+ });
37
+
38
+ const [errors, setErrors] = useState<Partial<ContactFormData>>({});
39
+ const [isSubmitting, setIsSubmitting] = useState(false);
40
+ const [submitError, setSubmitError] = useState<string>('');
41
+ const [submitSuccess, setSubmitSuccess] = useState(false);
42
+
43
+ const validateForm = useCallback((): boolean => {
44
+ const newErrors: Partial<ContactFormData> = {};
45
+
46
+ if (!formData.firstname.trim()) {
47
+ newErrors.firstname = 'First name is required';
48
+ }
49
+
50
+ if (!formData.lastname.trim()) {
51
+ newErrors.lastname = 'Last name is required';
52
+ }
53
+
54
+ if (!formData.emailaddress1.trim()) {
55
+ newErrors.emailaddress1 = 'Email address is required';
56
+ } else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(formData.emailaddress1)) {
57
+ newErrors.emailaddress1 = 'Please enter a valid email address';
58
+ }
59
+
60
+ if (formData.telephone1 && !/^\+?[\d\s\-\(\)]+$/.test(formData.telephone1)) {
61
+ newErrors.telephone1 = 'Please enter a valid phone number';
62
+ }
63
+
64
+ setErrors(newErrors);
65
+ return Object.keys(newErrors).length === 0;
66
+ }, [formData]);
67
+
68
+ const handleInputChange = useCallback((field: keyof ContactFormData, value: any) => {
69
+ setFormData(prev => ({ ...prev, [field]: value }));
70
+
71
+ // Clear error for this field when user starts typing
72
+ if (errors[field]) {
73
+ setErrors(prev => ({ ...prev, [field]: undefined }));
74
+ }
75
+ setSubmitError('');
76
+ setSubmitSuccess(false);
77
+ }, [errors]);
78
+
79
+ const handleSubmit = useCallback(async () => {
80
+ if (!isAuthenticated) {
81
+ setSubmitError('You must be signed in to save contact information');
82
+ return;
83
+ }
84
+
85
+ if (!validateForm()) {
86
+ return;
87
+ }
88
+
89
+ setIsSubmitting(true);
90
+ setSubmitError('');
91
+ setSubmitSuccess(false);
92
+
93
+ try {
94
+ const contactData = {
95
+ firstname: formData.firstname.trim(),
96
+ lastname: formData.lastname.trim(),
97
+ emailaddress1: formData.emailaddress1.trim(),
98
+ telephone1: formData.telephone1.trim() || null,
99
+ preferredcontactmethodcode: formData.preferredcontactmethodcode,
100
+ birthdate: formData.birthdate ? formData.birthdate.toISOString() : null
101
+ };
102
+
103
+ await createRecord('contacts', contactData);
104
+
105
+ setSubmitSuccess(true);
106
+ // Reset form
107
+ setFormData({
108
+ firstname: '',
109
+ lastname: '',
110
+ emailaddress1: '',
111
+ telephone1: '',
112
+ preferredcontactmethodcode: 1,
113
+ birthdate: null
114
+ });
115
+ } catch (error) {
116
+ setSubmitError(error instanceof Error ? error.message : 'An error occurred while saving');
117
+ } finally {
118
+ setIsSubmitting(false);
119
+ }
120
+ }, [
121
+ formData,
122
+ isAuthenticated,
123
+ validateForm,
124
+ createRecord
125
+ ]);
126
+
127
+ const handlePreferredContactChange = useCallback((option: any) => {
128
+ const code = option.key === 'email' ? 1 : option.key === 'phone' ? 2 : 3;
129
+ handleInputChange('preferredcontactmethodcode', code);
130
+ }, [handleInputChange]);
131
+
132
+ return (
133
+ <div className="contact-form">
134
+ <div className="contact-form__header">
135
+ <h2 className="contact-form__title">Create New Contact</h2>
136
+ </div>
137
+
138
+ <div className="contact-form__content">
139
+ {submitSuccess && (
140
+ <div className="contact-form__success" role="alert">
141
+ Contact created successfully!
142
+ </div>
143
+ )}
144
+
145
+ {submitError && (
146
+ <div className="contact-form__error" role="alert">
147
+ {submitError}
148
+ </div>
149
+ )}
150
+
151
+ <div className="contact-form__row">
152
+ <div className="contact-form__field">
153
+ <TextField
154
+ label="First Name"
155
+ required
156
+ value={formData.firstname}
157
+ onChange={(_, value) => handleInputChange('firstname', value || '')}
158
+ errorMessage={errors.firstname}
159
+ disabled={isSubmitting}
160
+ />
161
+ </div>
162
+ <div className="contact-form__field">
163
+ <TextField
164
+ label="Last Name"
165
+ required
166
+ value={formData.lastname}
167
+ onChange={(_, value) => handleInputChange('lastname', value || '')}
168
+ errorMessage={errors.lastname}
169
+ disabled={isSubmitting}
170
+ />
171
+ </div>
172
+ </div>
173
+
174
+ <div className="contact-form__row">
175
+ <div className="contact-form__field">
176
+ <TextField
177
+ label="Email Address"
178
+ type="email"
179
+ required
180
+ value={formData.emailaddress1}
181
+ onChange={(_, value) => handleInputChange('emailaddress1', value || '')}
182
+ errorMessage={errors.emailaddress1}
183
+ disabled={isSubmitting}
184
+ />
185
+ </div>
186
+ <div className="contact-form__field">
187
+ <TextField
188
+ label="Phone Number"
189
+ type="tel"
190
+ value={formData.telephone1}
191
+ onChange={(_, value) => handleInputChange('telephone1', value || '')}
192
+ errorMessage={errors.telephone1}
193
+ disabled={isSubmitting}
194
+ />
195
+ </div>
196
+ </div>
197
+
198
+ <div className="contact-form__row">
199
+ <div className="contact-form__field">
200
+ <Dropdown
201
+ label="Preferred Contact Method"
202
+ options={preferredContactOptions}
203
+ selectedKey={
204
+ formData.preferredcontactmethodcode === 1 ? 'email' :
205
+ formData.preferredcontactmethodcode === 2 ? 'phone' : 'mail'
206
+ }
207
+ onChange={handlePreferredContactChange}
208
+ disabled={isSubmitting}
209
+ />
210
+ </div>
211
+ <div className="contact-form__field">
212
+ <DatePicker
213
+ label="Birth Date"
214
+ value={formData.birthdate || undefined}
215
+ onSelectDate={(date) => handleInputChange('birthdate', date)}
216
+ disabled={isSubmitting}
217
+ placeholder="Select date..."
218
+ />
219
+ </div>
220
+ </div>
221
+ </div>
222
+
223
+ <div className="contact-form__actions">
224
+ <Button
225
+ text="Save Contact"
226
+ variant="primary"
227
+ onClick={handleSubmit}
228
+ disabled={isSubmitting || !isAuthenticated}
229
+ />
230
+ </div>
231
+
232
+ {!isAuthenticated && (
233
+ <div className="contact-form__auth-notice">
234
+ Please sign in to save contact information.
235
+ </div>
236
+ )}
237
+ </div>
238
+ );
239
+ };
@@ -0,0 +1,32 @@
1
+ import React from 'react';
2
+ import { createRoot } from 'react-dom/client';
3
+ import { ContactForm } from './components/ContactForm';
4
+ import { PowerPagesProvider } from './providers/PowerPagesProvider';
5
+ import './styles/index.css';
6
+
7
+ const App: React.FC = () => {
8
+ return (
9
+ <PowerPagesProvider>
10
+ <div className="app">
11
+ <header className="app-header">
12
+ <h1>Power Pages Contact Management</h1>
13
+ <p>Built with Dynamics UI Kit</p>
14
+ </header>
15
+
16
+ <main className="app-main">
17
+ <ContactForm />
18
+ </main>
19
+
20
+ <footer className="app-footer">
21
+ <p>&copy; 2024 Your Organization. Powered by Dynamics UI Kit.</p>
22
+ </footer>
23
+ </div>
24
+ </PowerPagesProvider>
25
+ );
26
+ };
27
+
28
+ const container = document.getElementById('root');
29
+ if (!container) throw new Error('Failed to find the root element');
30
+
31
+ const root = createRoot(container);
32
+ root.render(<App />);
@@ -0,0 +1,139 @@
1
+ import React, { createContext, useContext, useState, useEffect } from 'react';
2
+ import { PowerPagesApiService } from '@khester1/dynamics-ui-api-client';
3
+
4
+ interface PowerPagesContextType {
5
+ apiService: PowerPagesApiService | null;
6
+ isAuthenticated: boolean;
7
+ createRecord: (entityName: string, data: any) => Promise<any>;
8
+ retrieveRecord: (entityName: string, id: string, select?: string) => Promise<any>;
9
+ updateRecord: (entityName: string, id: string, data: any) => Promise<any>;
10
+ deleteRecord: (entityName: string, id: string) => Promise<void>;
11
+ retrieveMultiple: (entityName: string, query?: string) => Promise<any>;
12
+ }
13
+
14
+ const PowerPagesContext = createContext<PowerPagesContextType | undefined>(undefined);
15
+
16
+ interface PowerPagesProviderProps {
17
+ children: React.ReactNode;
18
+ }
19
+
20
+ export const PowerPagesProvider: React.FC<PowerPagesProviderProps> = ({ children }) => {
21
+ const [apiService, setApiService] = useState<PowerPagesApiService | null>(null);
22
+ const [isAuthenticated, setIsAuthenticated] = useState(false);
23
+
24
+ useEffect(() => {
25
+ // Initialize the API service
26
+ const portalUrl = process.env.PORTAL_URL || window.location.origin;
27
+
28
+ // CSRF token function for Power Pages authentication
29
+ const getCsrfToken = async (): Promise<string> => {
30
+ // In a real Power Pages implementation, you would fetch the CSRF token
31
+ // For demo purposes, return empty string
32
+ return '';
33
+ };
34
+
35
+ const service = new PowerPagesApiService(portalUrl, getCsrfToken);
36
+
37
+ setApiService(service);
38
+
39
+ // Check authentication status
40
+ // In a real implementation, you would check if the user is logged in to the portal
41
+ // For demo purposes, we'll assume they are authenticated
42
+ setIsAuthenticated(true);
43
+ }, []);
44
+
45
+ const createRecord = async (entityName: string, data: any) => {
46
+ if (!apiService) throw new Error('API service not initialized');
47
+ return await apiService.createRecord(entityName, data);
48
+ };
49
+
50
+ const retrieveRecord = async (entityName: string, id: string, select?: string) => {
51
+ if (!apiService) throw new Error('API service not initialized');
52
+ // Use retrieveMultipleRecords with FetchXML to get a single record
53
+ const selectAttributes = select ? select.split(',').map(attr => attr.trim()) : ['*'];
54
+ const attributes = selectAttributes.map(attr => attr === '*' ? '' : `<attribute name="${attr}" />`).join('');
55
+ const fetchXml = `
56
+ <fetch top="1">
57
+ <entity name="${entityName}">
58
+ ${attributes}
59
+ <filter>
60
+ <condition attribute="${entityName}id" operator="eq" value="${id}" />
61
+ </filter>
62
+ </entity>
63
+ </fetch>
64
+ `;
65
+ const result = await apiService.retrieveMultipleRecords(entityName, fetchXml);
66
+ return result.entities.length > 0 ? result.entities[0] : null;
67
+ };
68
+
69
+ const updateRecord = async (entityName: string, id: string, data: any) => {
70
+ if (!apiService) throw new Error('API service not initialized');
71
+ return await apiService.updateRecord(entityName, id, data);
72
+ };
73
+
74
+ const deleteRecord = async (entityName: string, id: string) => {
75
+ if (!apiService) throw new Error('API service not initialized');
76
+ return await apiService.deleteRecord(entityName, id);
77
+ };
78
+
79
+ const retrieveMultiple = async (entityName: string, query?: string) => {
80
+ if (!apiService) throw new Error('API service not initialized');
81
+ // Convert OData-style query to FetchXML
82
+ let fetchXml = `<fetch>`;
83
+
84
+ if (query) {
85
+ // Parse basic OData query parameters
86
+ const selectMatch = query.match(/\$select=([^&]*)/i);
87
+ const orderByMatch = query.match(/\$orderby=([^&]*)/i);
88
+ const topMatch = query.match(/\$top=(\d+)/i);
89
+
90
+ if (topMatch) {
91
+ fetchXml = `<fetch top="${topMatch[1]}">`;
92
+ }
93
+
94
+ fetchXml += `<entity name="${entityName}">`;
95
+
96
+ if (selectMatch) {
97
+ const attributes = selectMatch[1].split(',').map(attr => attr.trim());
98
+ attributes.forEach(attr => {
99
+ fetchXml += `<attribute name="${attr}" />`;
100
+ });
101
+ }
102
+
103
+ if (orderByMatch) {
104
+ const [field, direction] = orderByMatch[1].split(' ');
105
+ fetchXml += `<order attribute="${field.trim()}" descending="${direction?.toLowerCase() === 'desc'}" />`;
106
+ }
107
+
108
+ fetchXml += `</entity></fetch>`;
109
+ } else {
110
+ fetchXml = `<fetch><entity name="${entityName}"></entity></fetch>`;
111
+ }
112
+
113
+ return await apiService.retrieveMultipleRecords(entityName, fetchXml);
114
+ };
115
+
116
+ const value: PowerPagesContextType = {
117
+ apiService,
118
+ isAuthenticated,
119
+ createRecord,
120
+ retrieveRecord,
121
+ updateRecord,
122
+ deleteRecord,
123
+ retrieveMultiple,
124
+ };
125
+
126
+ return (
127
+ <PowerPagesContext.Provider value={value}>
128
+ {children}
129
+ </PowerPagesContext.Provider>
130
+ );
131
+ };
132
+
133
+ export const usePowerPagesApi = (): PowerPagesContextType => {
134
+ const context = useContext(PowerPagesContext);
135
+ if (context === undefined) {
136
+ throw new Error('usePowerPagesApi must be used within a PowerPagesProvider');
137
+ }
138
+ return context;
139
+ };
@@ -0,0 +1,76 @@
1
+ /* Global Styles */
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
+ }
22
+
23
+ .app-header {
24
+ background: linear-gradient(135deg, #0078d4 0%, #106ebe 100%);
25
+ color: white;
26
+ padding: 24px;
27
+ text-align: center;
28
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
29
+ }
30
+
31
+ .app-header h1 {
32
+ margin: 0 0 8px 0;
33
+ font-size: 28px;
34
+ font-weight: 600;
35
+ }
36
+
37
+ .app-header p {
38
+ margin: 0;
39
+ font-size: 16px;
40
+ opacity: 0.9;
41
+ }
42
+
43
+ .app-main {
44
+ flex: 1;
45
+ padding: 32px 16px;
46
+ max-width: 1200px;
47
+ margin: 0 auto;
48
+ width: 100%;
49
+ }
50
+
51
+ .app-footer {
52
+ background-color: #f3f2f1;
53
+ border-top: 1px solid #edebe9;
54
+ padding: 16px;
55
+ text-align: center;
56
+ color: #605e5c;
57
+ font-size: 14px;
58
+ }
59
+
60
+ .app-footer p {
61
+ margin: 0;
62
+ }
63
+
64
+ @media (max-width: 768px) {
65
+ .app-header {
66
+ padding: 16px;
67
+ }
68
+
69
+ .app-header h1 {
70
+ font-size: 24px;
71
+ }
72
+
73
+ .app-main {
74
+ padding: 16px 8px;
75
+ }
76
+ }
@@ -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,52 @@
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
+ },
15
+ resolve: {
16
+ extensions: ['.tsx', '.ts', '.js', '.jsx'],
17
+ },
18
+ module: {
19
+ rules: [
20
+ {
21
+ test: /\.tsx?$/,
22
+ use: 'ts-loader',
23
+ exclude: /node_modules/,
24
+ },
25
+ {
26
+ test: /\.css$/i,
27
+ use: ['style-loader', 'css-loader'],
28
+ },
29
+ ],
30
+ },
31
+ plugins: [
32
+ new HtmlWebpackPlugin({
33
+ template: './public/index.html',
34
+ filename: 'index.html',
35
+ }),
36
+ ],
37
+ devServer: {
38
+ static: {
39
+ directory: path.join(__dirname, 'public'),
40
+ },
41
+ port: 3000,
42
+ open: true,
43
+ hot: true,
44
+ historyApiFallback: true,
45
+ },
46
+ optimization: {
47
+ splitChunks: {
48
+ chunks: 'all',
49
+ },
50
+ },
51
+ };
52
+ };