@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
@@ -1,258 +0,0 @@
1
- import React, { useState, useCallback } from 'react';
2
- import {
3
- Button,
4
- TextField,
5
- Dropdown,
6
- DatePicker,
7
- } from '@khester/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 (
61
- formData.telephone1 &&
62
- !/^\+?[\d\s\-\(\)]+$/.test(formData.telephone1)
63
- ) {
64
- newErrors.telephone1 = 'Please enter a valid phone number';
65
- }
66
-
67
- setErrors(newErrors);
68
- return Object.keys(newErrors).length === 0;
69
- }, [formData]);
70
-
71
- const handleInputChange = useCallback(
72
- (field: keyof ContactFormData, value: any) => {
73
- setFormData((prev) => ({ ...prev, [field]: value }));
74
-
75
- // Clear error for this field when user starts typing
76
- if (errors[field]) {
77
- setErrors((prev) => ({ ...prev, [field]: undefined }));
78
- }
79
- setSubmitError('');
80
- setSubmitSuccess(false);
81
- },
82
- [errors]
83
- );
84
-
85
- const handleSubmit = useCallback(async () => {
86
- if (!isAuthenticated) {
87
- setSubmitError('You must be signed in to save contact information');
88
- return;
89
- }
90
-
91
- if (!validateForm()) {
92
- return;
93
- }
94
-
95
- setIsSubmitting(true);
96
- setSubmitError('');
97
- setSubmitSuccess(false);
98
-
99
- try {
100
- const contactData = {
101
- firstname: formData.firstname.trim(),
102
- lastname: formData.lastname.trim(),
103
- emailaddress1: formData.emailaddress1.trim(),
104
- telephone1: formData.telephone1.trim() || null,
105
- preferredcontactmethodcode: formData.preferredcontactmethodcode,
106
- birthdate: formData.birthdate ? formData.birthdate.toISOString() : null,
107
- };
108
-
109
- await createRecord('contacts', contactData);
110
-
111
- setSubmitSuccess(true);
112
- // Reset form
113
- setFormData({
114
- firstname: '',
115
- lastname: '',
116
- emailaddress1: '',
117
- telephone1: '',
118
- preferredcontactmethodcode: 1,
119
- birthdate: null,
120
- });
121
- } catch (error) {
122
- setSubmitError(
123
- error instanceof Error
124
- ? error.message
125
- : 'An error occurred while saving'
126
- );
127
- } finally {
128
- setIsSubmitting(false);
129
- }
130
- }, [formData, isAuthenticated, validateForm, createRecord]);
131
-
132
- const handlePreferredContactChange = useCallback(
133
- (option: any) => {
134
- const code = option.key === 'email' ? 1 : option.key === 'phone' ? 2 : 3;
135
- handleInputChange('preferredcontactmethodcode', code);
136
- },
137
- [handleInputChange]
138
- );
139
-
140
- return (
141
- <div className="contact-form">
142
- <div className="contact-form__header">
143
- <h2 className="contact-form__title">Create New Contact</h2>
144
- </div>
145
-
146
- <div className="contact-form__content">
147
- {submitSuccess && (
148
- <div className="contact-form__success" role="alert">
149
- Contact created successfully!
150
- </div>
151
- )}
152
-
153
- {submitError && (
154
- <div className="contact-form__error" role="alert">
155
- {submitError}
156
- </div>
157
- )}
158
-
159
- <div className="contact-form__row">
160
- <div className="contact-form__field">
161
- <TextField
162
- label="First Name"
163
- required
164
- value={formData.firstname}
165
- onChange={(_, value) =>
166
- handleInputChange('firstname', value || '')
167
- }
168
- errorMessage={errors.firstname}
169
- disabled={isSubmitting}
170
- />
171
- </div>
172
- <div className="contact-form__field">
173
- <TextField
174
- label="Last Name"
175
- required
176
- value={formData.lastname}
177
- onChange={(_, value) =>
178
- handleInputChange('lastname', value || '')
179
- }
180
- errorMessage={errors.lastname}
181
- disabled={isSubmitting}
182
- />
183
- </div>
184
- </div>
185
-
186
- <div className="contact-form__row">
187
- <div className="contact-form__field">
188
- <TextField
189
- label="Email Address"
190
- type="email"
191
- required
192
- value={formData.emailaddress1}
193
- onChange={(_, value) =>
194
- handleInputChange('emailaddress1', value || '')
195
- }
196
- errorMessage={errors.emailaddress1}
197
- disabled={isSubmitting}
198
- />
199
- </div>
200
- <div className="contact-form__field">
201
- <TextField
202
- label="Phone Number"
203
- type="tel"
204
- value={formData.telephone1}
205
- onChange={(_, value) =>
206
- handleInputChange('telephone1', value || '')
207
- }
208
- errorMessage={errors.telephone1}
209
- disabled={isSubmitting}
210
- />
211
- </div>
212
- </div>
213
-
214
- <div className="contact-form__row">
215
- <div className="contact-form__field">
216
- <Dropdown
217
- label="Preferred Contact Method"
218
- options={preferredContactOptions}
219
- selectedKey={
220
- formData.preferredcontactmethodcode === 1
221
- ? 'email'
222
- : formData.preferredcontactmethodcode === 2
223
- ? 'phone'
224
- : 'mail'
225
- }
226
- onChange={handlePreferredContactChange}
227
- disabled={isSubmitting}
228
- />
229
- </div>
230
- <div className="contact-form__field">
231
- <DatePicker
232
- label="Birth Date"
233
- value={formData.birthdate || undefined}
234
- onSelectDate={(date) => handleInputChange('birthdate', date)}
235
- disabled={isSubmitting}
236
- placeholder="Select date..."
237
- />
238
- </div>
239
- </div>
240
- </div>
241
-
242
- <div className="contact-form__actions">
243
- <Button
244
- text="Save Contact"
245
- variant="primary"
246
- onClick={handleSubmit}
247
- disabled={isSubmitting || !isAuthenticated}
248
- />
249
- </div>
250
-
251
- {!isAuthenticated && (
252
- <div className="contact-form__auth-notice">
253
- Please sign in to save contact information.
254
- </div>
255
- )}
256
- </div>
257
- );
258
- };
@@ -1,32 +0,0 @@
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 />);
@@ -1,162 +0,0 @@
1
- import React, { createContext, useContext, useState, useEffect } from 'react';
2
- import { PowerPagesApiService } from '@khester/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: (
9
- entityName: string,
10
- id: string,
11
- select?: string
12
- ) => Promise<any>;
13
- updateRecord: (entityName: string, id: string, data: any) => Promise<any>;
14
- deleteRecord: (entityName: string, id: string) => Promise<void>;
15
- retrieveMultiple: (entityName: string, query?: string) => Promise<any>;
16
- }
17
-
18
- const PowerPagesContext = createContext<PowerPagesContextType | undefined>(
19
- undefined
20
- );
21
-
22
- interface PowerPagesProviderProps {
23
- children: React.ReactNode;
24
- }
25
-
26
- export const PowerPagesProvider: React.FC<PowerPagesProviderProps> = ({
27
- children,
28
- }) => {
29
- const [apiService, setApiService] = useState<PowerPagesApiService | null>(
30
- null
31
- );
32
- const [isAuthenticated, setIsAuthenticated] = useState(false);
33
-
34
- useEffect(() => {
35
- // Initialize the API service
36
- const portalUrl = window.location.origin;
37
-
38
- // CSRF token function for Power Pages authentication
39
- const getCsrfToken = async (): Promise<string> => {
40
- // In a real Power Pages implementation, you would fetch the CSRF token
41
- // For demo purposes, return empty string
42
- return '';
43
- };
44
-
45
- const service = new PowerPagesApiService(portalUrl, getCsrfToken);
46
-
47
- setApiService(service);
48
-
49
- // Check authentication status
50
- // In a real implementation, you would check if the user is logged in to the portal
51
- // For demo purposes, we'll assume they are authenticated
52
- setIsAuthenticated(true);
53
- }, []);
54
-
55
- const createRecord = async (entityName: string, data: any) => {
56
- if (!apiService) throw new Error('API service not initialized');
57
- return await apiService.createRecord(entityName, data);
58
- };
59
-
60
- const retrieveRecord = async (
61
- entityName: string,
62
- id: string,
63
- select?: string
64
- ) => {
65
- if (!apiService) throw new Error('API service not initialized');
66
- // Use retrieveMultipleRecords with FetchXML to get a single record
67
- const selectAttributes = select
68
- ? select.split(',').map((attr) => attr.trim())
69
- : ['*'];
70
- const attributes = selectAttributes
71
- .map((attr) => (attr === '*' ? '' : `<attribute name="${attr}" />`))
72
- .join('');
73
- const fetchXml = `
74
- <fetch top="1">
75
- <entity name="${entityName}">
76
- ${attributes}
77
- <filter>
78
- <condition attribute="${entityName}id" operator="eq" value="${id}" />
79
- </filter>
80
- </entity>
81
- </fetch>
82
- `;
83
- const result = await apiService.retrieveMultipleRecords(
84
- entityName,
85
- fetchXml
86
- );
87
- return result.entities.length > 0 ? result.entities[0] : null;
88
- };
89
-
90
- const updateRecord = async (entityName: string, id: string, data: any) => {
91
- if (!apiService) throw new Error('API service not initialized');
92
- return await apiService.updateRecord(entityName, id, data);
93
- };
94
-
95
- const deleteRecord = async (entityName: string, id: string) => {
96
- if (!apiService) throw new Error('API service not initialized');
97
- return await apiService.deleteRecord(entityName, id);
98
- };
99
-
100
- const retrieveMultiple = async (entityName: string, query?: string) => {
101
- if (!apiService) throw new Error('API service not initialized');
102
- // Convert OData-style query to FetchXML
103
- let fetchXml = `<fetch>`;
104
-
105
- if (query) {
106
- // Parse basic OData query parameters
107
- const selectMatch = query.match(/\$select=([^&]*)/i);
108
- const orderByMatch = query.match(/\$orderby=([^&]*)/i);
109
- const topMatch = query.match(/\$top=(\d+)/i);
110
-
111
- if (topMatch) {
112
- fetchXml = `<fetch top="${topMatch[1]}">`;
113
- }
114
-
115
- fetchXml += `<entity name="${entityName}">`;
116
-
117
- if (selectMatch) {
118
- const attributes = selectMatch[1].split(',').map((attr) => attr.trim());
119
- attributes.forEach((attr) => {
120
- fetchXml += `<attribute name="${attr}" />`;
121
- });
122
- }
123
-
124
- if (orderByMatch) {
125
- const [field, direction] = orderByMatch[1].split(' ');
126
- fetchXml += `<order attribute="${field.trim()}" descending="${direction?.toLowerCase() === 'desc'}" />`;
127
- }
128
-
129
- fetchXml += `</entity></fetch>`;
130
- } else {
131
- fetchXml = `<fetch><entity name="${entityName}"></entity></fetch>`;
132
- }
133
-
134
- return await apiService.retrieveMultipleRecords(entityName, fetchXml);
135
- };
136
-
137
- const value: PowerPagesContextType = {
138
- apiService,
139
- isAuthenticated,
140
- createRecord,
141
- retrieveRecord,
142
- updateRecord,
143
- deleteRecord,
144
- retrieveMultiple,
145
- };
146
-
147
- return (
148
- <PowerPagesContext.Provider value={value}>
149
- {children}
150
- </PowerPagesContext.Provider>
151
- );
152
- };
153
-
154
- export const usePowerPagesApi = (): PowerPagesContextType => {
155
- const context = useContext(PowerPagesContext);
156
- if (context === undefined) {
157
- throw new Error(
158
- 'usePowerPagesApi must be used within a PowerPagesProvider'
159
- );
160
- }
161
- return context;
162
- };
@@ -1,76 +0,0 @@
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
- }
@@ -1,20 +0,0 @@
1
- {
2
- "compilerOptions": {
3
- "target": "es5",
4
- "lib": ["dom", "dom.iterable", "es6"],
5
- "allowJs": true,
6
- "skipLibCheck": true,
7
- "esModuleInterop": true,
8
- "allowSyntheticDefaultImports": true,
9
- "strict": true,
10
- "forceConsistentCasingInFileNames": true,
11
- "noFallthroughCasesInSwitch": true,
12
- "module": "esnext",
13
- "moduleResolution": "node",
14
- "resolveJsonModule": true,
15
- "isolatedModules": true,
16
- "noEmit": true,
17
- "jsx": "react-jsx"
18
- },
19
- "include": ["src"]
20
- }
@@ -1,57 +0,0 @@
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: {
23
- loader: 'ts-loader',
24
- options: {
25
- transpileOnly: true,
26
- },
27
- },
28
- exclude: /node_modules/,
29
- },
30
- {
31
- test: /\.css$/i,
32
- use: ['style-loader', 'css-loader'],
33
- },
34
- ],
35
- },
36
- plugins: [
37
- new HtmlWebpackPlugin({
38
- template: './public/index.html',
39
- filename: 'index.html',
40
- }),
41
- ],
42
- devServer: {
43
- static: {
44
- directory: path.join(__dirname, 'public'),
45
- },
46
- port: 3000,
47
- open: true,
48
- hot: true,
49
- historyApiFallback: true,
50
- },
51
- optimization: {
52
- splitChunks: {
53
- chunks: 'all',
54
- },
55
- },
56
- };
57
- };
@@ -1,5 +1,5 @@
1
1
  node_modules/
2
2
  dist/
3
- *.log
4
3
  .env
5
4
  .env.local
5
+ *.log