@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,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="Dynamics 365 application built with Dynamics UI Kit"
11
+ />
12
+ <title>Dynamics 365 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,48 @@
1
+ .contact-form {
2
+ padding: 16px 0;
3
+ }
4
+
5
+ .contact-form__content {
6
+ margin-bottom: 24px;
7
+ }
8
+
9
+ .contact-form__row {
10
+ display: grid;
11
+ grid-template-columns: 1fr 1fr;
12
+ gap: 16px;
13
+ margin-bottom: 16px;
14
+ }
15
+
16
+ .contact-form__field {
17
+ display: flex;
18
+ flex-direction: column;
19
+ }
20
+
21
+ .contact-form__actions {
22
+ display: flex;
23
+ gap: 12px;
24
+ justify-content: flex-start;
25
+ padding-top: 16px;
26
+ border-top: 1px solid #edebe9;
27
+ }
28
+
29
+ .contact-form__error {
30
+ padding: 12px 16px;
31
+ margin-bottom: 16px;
32
+ background-color: #fef7f1;
33
+ border: 1px solid #d13438;
34
+ border-radius: 4px;
35
+ color: #d13438;
36
+ font-weight: 500;
37
+ }
38
+
39
+ @media (max-width: 768px) {
40
+ .contact-form__row {
41
+ grid-template-columns: 1fr;
42
+ gap: 12px;
43
+ }
44
+
45
+ .contact-form__actions {
46
+ flex-direction: column;
47
+ }
48
+ }
@@ -0,0 +1,241 @@
1
+ import React, { useState, useCallback, useEffect } from 'react';
2
+ import {
3
+ Button,
4
+ TextField,
5
+ Dropdown,
6
+ DatePicker
7
+ } from '@khester1/dynamics-ui-components';
8
+ import { useDynamicsApi } from '../providers/DynamicsProvider';
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
+ interface ContactFormProps {
21
+ contactId?: string;
22
+ initialData?: Partial<ContactFormData>;
23
+ onSave?: () => void;
24
+ onCancel?: () => void;
25
+ }
26
+
27
+ const preferredContactOptions = [
28
+ { key: 1, text: 'Email' },
29
+ { key: 2, text: 'Phone' },
30
+ { key: 3, text: 'Mail' }
31
+ ];
32
+
33
+ export const ContactForm: React.FC<ContactFormProps> = ({
34
+ contactId,
35
+ initialData,
36
+ onSave,
37
+ onCancel
38
+ }) => {
39
+ const { createRecord, updateRecord } = useDynamicsApi();
40
+
41
+ const [formData, setFormData] = useState<ContactFormData>({
42
+ firstname: '',
43
+ lastname: '',
44
+ emailaddress1: '',
45
+ telephone1: '',
46
+ preferredcontactmethodcode: 1,
47
+ birthdate: null
48
+ });
49
+
50
+ const [errors, setErrors] = useState<Partial<ContactFormData>>({});
51
+ const [isSubmitting, setIsSubmitting] = useState(false);
52
+ const [submitError, setSubmitError] = useState<string>('');
53
+
54
+ useEffect(() => {
55
+ if (initialData) {
56
+ setFormData({
57
+ firstname: initialData.firstname || '',
58
+ lastname: initialData.lastname || '',
59
+ emailaddress1: initialData.emailaddress1 || '',
60
+ telephone1: initialData.telephone1 || '',
61
+ preferredcontactmethodcode: initialData.preferredcontactmethodcode || 1,
62
+ birthdate: initialData.birthdate || null
63
+ });
64
+ }
65
+ }, [initialData]);
66
+
67
+ const validateForm = useCallback((): boolean => {
68
+ const newErrors: Partial<ContactFormData> = {};
69
+
70
+ if (!formData.firstname.trim()) {
71
+ newErrors.firstname = 'First name is required';
72
+ }
73
+
74
+ if (!formData.lastname.trim()) {
75
+ newErrors.lastname = 'Last name is required';
76
+ }
77
+
78
+ if (!formData.emailaddress1.trim()) {
79
+ newErrors.emailaddress1 = 'Email address is required';
80
+ } else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(formData.emailaddress1)) {
81
+ newErrors.emailaddress1 = 'Please enter a valid email address';
82
+ }
83
+
84
+ if (formData.telephone1 && !/^\+?[\d\s\-\(\)]+$/.test(formData.telephone1)) {
85
+ newErrors.telephone1 = 'Please enter a valid phone number';
86
+ }
87
+
88
+ setErrors(newErrors);
89
+ return Object.keys(newErrors).length === 0;
90
+ }, [formData]);
91
+
92
+ const handleInputChange = useCallback((field: keyof ContactFormData, value: any) => {
93
+ setFormData(prev => ({ ...prev, [field]: value }));
94
+
95
+ // Clear error for this field when user starts typing
96
+ if (errors[field]) {
97
+ setErrors(prev => ({ ...prev, [field]: undefined }));
98
+ }
99
+ setSubmitError('');
100
+ }, [errors]);
101
+
102
+ const handleSubmit = useCallback(async () => {
103
+ if (!validateForm()) {
104
+ return;
105
+ }
106
+
107
+ setIsSubmitting(true);
108
+ setSubmitError('');
109
+
110
+ try {
111
+ const contactData = {
112
+ firstname: formData.firstname.trim(),
113
+ lastname: formData.lastname.trim(),
114
+ emailaddress1: formData.emailaddress1.trim(),
115
+ telephone1: formData.telephone1.trim() || null,
116
+ preferredcontactmethodcode: formData.preferredcontactmethodcode,
117
+ birthdate: formData.birthdate ? formData.birthdate.toISOString() : null
118
+ };
119
+
120
+ if (contactId) {
121
+ await updateRecord('contacts', contactId, contactData);
122
+ } else {
123
+ await createRecord('contacts', contactData);
124
+ }
125
+
126
+ if (onSave) {
127
+ onSave();
128
+ }
129
+ } catch (error) {
130
+ setSubmitError(error instanceof Error ? error.message : 'An error occurred while saving');
131
+ } finally {
132
+ setIsSubmitting(false);
133
+ }
134
+ }, [
135
+ formData,
136
+ contactId,
137
+ validateForm,
138
+ createRecord,
139
+ updateRecord,
140
+ onSave
141
+ ]);
142
+
143
+ const handleCancel = useCallback(() => {
144
+ if (onCancel) {
145
+ onCancel();
146
+ }
147
+ }, [onCancel]);
148
+
149
+ return (
150
+ <div className="contact-form">
151
+ {submitError && (
152
+ <div className="contact-form__error" role="alert">
153
+ {submitError}
154
+ </div>
155
+ )}
156
+
157
+ <div className="contact-form__content">
158
+ <div className="contact-form__row">
159
+ <div className="contact-form__field">
160
+ <TextField
161
+ label="First Name"
162
+ required
163
+ value={formData.firstname}
164
+ onChange={(_, value) => handleInputChange('firstname', value || '')}
165
+ errorMessage={errors.firstname}
166
+ disabled={isSubmitting}
167
+ />
168
+ </div>
169
+ <div className="contact-form__field">
170
+ <TextField
171
+ label="Last Name"
172
+ required
173
+ value={formData.lastname}
174
+ onChange={(_, value) => handleInputChange('lastname', value || '')}
175
+ errorMessage={errors.lastname}
176
+ disabled={isSubmitting}
177
+ />
178
+ </div>
179
+ </div>
180
+
181
+ <div className="contact-form__row">
182
+ <div className="contact-form__field">
183
+ <TextField
184
+ label="Email Address"
185
+ type="email"
186
+ required
187
+ value={formData.emailaddress1}
188
+ onChange={(_, value) => handleInputChange('emailaddress1', value || '')}
189
+ errorMessage={errors.emailaddress1}
190
+ disabled={isSubmitting}
191
+ />
192
+ </div>
193
+ <div className="contact-form__field">
194
+ <TextField
195
+ label="Phone Number"
196
+ type="tel"
197
+ value={formData.telephone1}
198
+ onChange={(_, value) => handleInputChange('telephone1', value || '')}
199
+ errorMessage={errors.telephone1}
200
+ disabled={isSubmitting}
201
+ />
202
+ </div>
203
+ </div>
204
+
205
+ <div className="contact-form__row">
206
+ <div className="contact-form__field">
207
+ <Dropdown
208
+ label="Preferred Contact Method"
209
+ options={preferredContactOptions}
210
+ selectedKey={formData.preferredcontactmethodcode}
211
+ onChange={(_, option) => handleInputChange('preferredcontactmethodcode', option?.key)}
212
+ disabled={isSubmitting}
213
+ />
214
+ </div>
215
+ <div className="contact-form__field">
216
+ <DatePicker
217
+ label="Birth Date"
218
+ value={formData.birthdate ?? undefined}
219
+ onSelectDate={(date) => handleInputChange('birthdate', date)}
220
+ disabled={isSubmitting}
221
+ placeholder="Select date..."
222
+ />
223
+ </div>
224
+ </div>
225
+ </div>
226
+
227
+ <div className="contact-form__actions">
228
+ <Button
229
+ text={contactId ? 'Update' : 'Save'}
230
+ onClick={handleSubmit}
231
+ disabled={isSubmitting}
232
+ />
233
+ <Button
234
+ text="Cancel"
235
+ onClick={handleCancel}
236
+ disabled={isSubmitting}
237
+ />
238
+ </div>
239
+ </div>
240
+ );
241
+ };
@@ -0,0 +1,86 @@
1
+ .contact-management {
2
+ padding: 16px;
3
+ }
4
+
5
+ .contact-management__header {
6
+ display: flex;
7
+ justify-content: space-between;
8
+ align-items: center;
9
+ margin-bottom: 16px;
10
+ flex-wrap: wrap;
11
+ gap: 16px;
12
+ }
13
+
14
+ .contact-management__header h2 {
15
+ margin: 0;
16
+ font-size: 20px;
17
+ font-weight: 600;
18
+ color: #323130;
19
+ }
20
+
21
+ .contact-management__actions {
22
+ display: flex;
23
+ gap: 12px;
24
+ align-items: center;
25
+ flex-wrap: wrap;
26
+ }
27
+
28
+ .contact-management__toolbar {
29
+ display: flex;
30
+ gap: 8px;
31
+ margin-bottom: 16px;
32
+ padding: 8px 0;
33
+ border-bottom: 1px solid #edebe9;
34
+ }
35
+
36
+ .contact-management__list {
37
+ min-height: 400px;
38
+ border: 1px solid #edebe9;
39
+ border-radius: 4px;
40
+ }
41
+
42
+ .contact-management__loading {
43
+ display: flex;
44
+ justify-content: center;
45
+ align-items: center;
46
+ height: 200px;
47
+ color: #605e5c;
48
+ font-style: italic;
49
+ }
50
+
51
+ .contact-management__empty {
52
+ display: flex;
53
+ justify-content: center;
54
+ align-items: center;
55
+ height: 200px;
56
+ color: #605e5c;
57
+ font-style: italic;
58
+ text-align: center;
59
+ padding: 16px;
60
+ }
61
+
62
+ .contact-management__dialog-actions {
63
+ display: flex;
64
+ gap: 12px;
65
+ justify-content: flex-end;
66
+ padding-top: 16px;
67
+ }
68
+
69
+ @media (max-width: 768px) {
70
+ .contact-management {
71
+ padding: 8px;
72
+ }
73
+
74
+ .contact-management__header {
75
+ flex-direction: column;
76
+ align-items: stretch;
77
+ }
78
+
79
+ .contact-management__actions {
80
+ justify-content: stretch;
81
+ }
82
+
83
+ .contact-management__toolbar {
84
+ flex-wrap: wrap;
85
+ }
86
+ }
@@ -0,0 +1,267 @@
1
+ import React, { useState, useEffect, useCallback } from 'react';
2
+ import {
3
+ Button,
4
+ TextField,
5
+ DetailsList,
6
+ Dialog,
7
+ Panel
8
+ } from '@khester1/dynamics-ui-components';
9
+ import { SelectionMode, DetailsListLayoutMode, CheckboxVisibility } from '@fluentui/react';
10
+ import { useDynamicsApi } from '../providers/DynamicsProvider';
11
+ import { ContactForm } from './ContactForm';
12
+ import './ContactManagement.css';
13
+
14
+ interface Contact {
15
+ contactid: string;
16
+ firstname: string;
17
+ lastname: string;
18
+ emailaddress1: string;
19
+ telephone1: string;
20
+ createdon: string;
21
+ fullname?: string;
22
+ }
23
+
24
+ const columns = [
25
+ {
26
+ key: 'fullname',
27
+ name: 'Name',
28
+ fieldName: 'fullname',
29
+ minWidth: 150,
30
+ maxWidth: 200,
31
+ isResizable: true,
32
+ },
33
+ {
34
+ key: 'emailaddress1',
35
+ name: 'Email',
36
+ fieldName: 'emailaddress1',
37
+ minWidth: 200,
38
+ maxWidth: 300,
39
+ isResizable: true,
40
+ },
41
+ {
42
+ key: 'telephone1',
43
+ name: 'Phone',
44
+ fieldName: 'telephone1',
45
+ minWidth: 150,
46
+ maxWidth: 200,
47
+ isResizable: true,
48
+ },
49
+ {
50
+ key: 'createdon',
51
+ name: 'Created',
52
+ fieldName: 'createdon',
53
+ minWidth: 150,
54
+ maxWidth: 200,
55
+ isResizable: true,
56
+ },
57
+ ];
58
+
59
+ export const ContactManagement: React.FC = () => {
60
+ const { retrieveMultiple, deleteRecord } = useDynamicsApi();
61
+
62
+ const [contacts, setContacts] = useState<Contact[]>([]);
63
+ const [filteredContacts, setFilteredContacts] = useState<Contact[]>([]);
64
+ const [loading, setLoading] = useState(false);
65
+ const [searchText, setSearchText] = useState('');
66
+ const [selectedContact, setSelectedContact] = useState<Contact | null>(null);
67
+ const [showNewContactPanel, setShowNewContactPanel] = useState(false);
68
+ const [showEditContactPanel, setShowEditContactPanel] = useState(false);
69
+ const [showDeleteDialog, setShowDeleteDialog] = useState(false);
70
+ const [contactToDelete, setContactToDelete] = useState<Contact | null>(null);
71
+
72
+ const loadContacts = useCallback(async () => {
73
+ setLoading(true);
74
+ try {
75
+ const query = '$select=contactid,firstname,lastname,emailaddress1,telephone1,createdon&$orderby=createdon desc';
76
+ const response = await retrieveMultiple('contacts', query);
77
+
78
+ const contactsWithFullName = response.value.map((contact: Contact) => ({
79
+ ...contact,
80
+ fullname: `${contact.firstname || ''} ${contact.lastname || ''}`.trim(),
81
+ createdon: new Date(contact.createdon).toLocaleDateString()
82
+ }));
83
+
84
+ setContacts(contactsWithFullName);
85
+ setFilteredContacts(contactsWithFullName);
86
+ } catch (error) {
87
+ console.error('Error loading contacts:', error);
88
+ } finally {
89
+ setLoading(false);
90
+ }
91
+ }, [retrieveMultiple]);
92
+
93
+ useEffect(() => {
94
+ loadContacts();
95
+ }, [loadContacts]);
96
+
97
+ useEffect(() => {
98
+ if (!searchText) {
99
+ setFilteredContacts(contacts);
100
+ } else {
101
+ const filtered = contacts.filter(contact =>
102
+ contact.fullname?.toLowerCase().includes(searchText.toLowerCase()) ||
103
+ contact.emailaddress1?.toLowerCase().includes(searchText.toLowerCase()) ||
104
+ contact.telephone1?.includes(searchText)
105
+ );
106
+ setFilteredContacts(filtered);
107
+ }
108
+ }, [contacts, searchText]);
109
+
110
+ const handleItemInvoked = useCallback((item: Contact) => {
111
+ if (selectedContact?.contactid === item.contactid) {
112
+ setShowEditContactPanel(true);
113
+ } else {
114
+ setSelectedContact(item);
115
+ }
116
+ }, [selectedContact]);
117
+
118
+ const handleNewContact = useCallback(() => {
119
+ setShowNewContactPanel(true);
120
+ }, []);
121
+
122
+ const handleEditContact = useCallback(() => {
123
+ if (selectedContact) {
124
+ setShowEditContactPanel(true);
125
+ }
126
+ }, [selectedContact]);
127
+
128
+ const handleDeleteContact = useCallback(() => {
129
+ if (selectedContact) {
130
+ setContactToDelete(selectedContact);
131
+ setShowDeleteDialog(true);
132
+ }
133
+ }, [selectedContact]);
134
+
135
+ const confirmDelete = useCallback(async () => {
136
+ if (contactToDelete) {
137
+ try {
138
+ await deleteRecord('contacts', contactToDelete.contactid);
139
+ await loadContacts();
140
+ setShowDeleteDialog(false);
141
+ setContactToDelete(null);
142
+ setSelectedContact(null);
143
+ } catch (error) {
144
+ console.error('Error deleting contact:', error);
145
+ }
146
+ }
147
+ }, [contactToDelete, deleteRecord, loadContacts]);
148
+
149
+ const handleContactSaved = useCallback(() => {
150
+ loadContacts();
151
+ setShowNewContactPanel(false);
152
+ setShowEditContactPanel(false);
153
+ setSelectedContact(null);
154
+ }, [loadContacts]);
155
+
156
+
157
+ return (
158
+ <div className="contact-management">
159
+ <div className="contact-management__header">
160
+ <h2>Contact Management</h2>
161
+ <div className="contact-management__actions">
162
+ <TextField
163
+ placeholder="Search contacts..."
164
+ value={searchText}
165
+ onChange={(_, value) => setSearchText(value || '')}
166
+ iconProps={{ iconName: 'Search' }}
167
+ />
168
+ <Button
169
+ text="New Contact"
170
+ variant="primary"
171
+ onClick={handleNewContact}
172
+ iconProps={{ iconName: 'Add' }}
173
+ />
174
+ </div>
175
+ </div>
176
+
177
+ <div className="contact-management__toolbar">
178
+ <Button
179
+ text="Edit"
180
+ onClick={handleEditContact}
181
+ disabled={!selectedContact}
182
+ iconProps={{ iconName: 'Edit' }}
183
+ />
184
+ <Button
185
+ text="Delete"
186
+ onClick={handleDeleteContact}
187
+ disabled={!selectedContact}
188
+ iconProps={{ iconName: 'Delete' }}
189
+ />
190
+ <Button
191
+ text="Refresh"
192
+ onClick={loadContacts}
193
+ iconProps={{ iconName: 'Refresh' }}
194
+ />
195
+ </div>
196
+
197
+ <div className="contact-management__list">
198
+ <DetailsList
199
+ items={filteredContacts}
200
+ columns={columns}
201
+ onItemInvoked={handleItemInvoked}
202
+ selectionMode={SelectionMode.single}
203
+ layoutMode={DetailsListLayoutMode.justified}
204
+ checkboxVisibility={CheckboxVisibility.onHover}
205
+ />
206
+
207
+ {loading && (
208
+ <div className="contact-management__loading">
209
+ Loading contacts...
210
+ </div>
211
+ )}
212
+
213
+ {!loading && filteredContacts.length === 0 && (
214
+ <div className="contact-management__empty">
215
+ {searchText ? 'No contacts found matching your search.' : 'No contacts found. Create your first contact!'}
216
+ </div>
217
+ )}
218
+ </div>
219
+
220
+ {/* New Contact Panel */}
221
+ <Panel
222
+ isOpen={showNewContactPanel}
223
+ onDismiss={() => setShowNewContactPanel(false)}
224
+ headerText="New Contact"
225
+ >
226
+ <ContactForm
227
+ onSave={handleContactSaved}
228
+ onCancel={() => setShowNewContactPanel(false)}
229
+ />
230
+ </Panel>
231
+
232
+ {/* Edit Contact Panel */}
233
+ <Panel
234
+ isOpen={showEditContactPanel}
235
+ onDismiss={() => {
236
+ setShowEditContactPanel(false);
237
+ setSelectedContact(null);
238
+ }}
239
+ headerText="Edit Contact"
240
+ >
241
+ {selectedContact && (
242
+ <ContactForm
243
+ contactId={selectedContact.contactid}
244
+ initialData={selectedContact}
245
+ onSave={handleContactSaved}
246
+ onCancel={() => {
247
+ setShowEditContactPanel(false);
248
+ setSelectedContact(null);
249
+ }}
250
+ />
251
+ )}
252
+ </Panel>
253
+
254
+ {/* Delete Confirmation Dialog */}
255
+ <Dialog
256
+ hidden={!showDeleteDialog}
257
+ onDismiss={() => setShowDeleteDialog(false)}
258
+ title="Confirm Delete"
259
+ content={`Are you sure you want to delete ${contactToDelete?.fullname}? This action cannot be undone.`}
260
+ actions={[
261
+ { text: 'Delete', onClick: confirmDelete, primary: true },
262
+ { text: 'Cancel', onClick: () => setShowDeleteDialog(false) }
263
+ ]}
264
+ />
265
+ </div>
266
+ );
267
+ };
@@ -0,0 +1,40 @@
1
+ import React from 'react';
2
+ import { createRoot } from 'react-dom/client';
3
+ import { ContactManagement } from './components/ContactManagement';
4
+ import { DynamicsProvider } from './providers/DynamicsProvider';
5
+ import './styles/index.css';
6
+
7
+ const App: React.FC = () => {
8
+ return (
9
+ <DynamicsProvider>
10
+ <div className="app">
11
+ <header className="app-header">
12
+ <h1>Dynamics 365 Contact Management</h1>
13
+ <p>Built with Dynamics UI Kit</p>
14
+ </header>
15
+
16
+ <main className="app-main">
17
+ <ContactManagement />
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
+ </DynamicsProvider>
25
+ );
26
+ };
27
+
28
+ // For development/testing
29
+ if (typeof document !== 'undefined') {
30
+ const container = document.getElementById('root');
31
+ if (container) {
32
+ const root = createRoot(container);
33
+ root.render(<App />);
34
+ }
35
+ }
36
+
37
+ // Export for PCF integration
38
+ export { App, ContactManagement };
39
+ export * from './components/ContactManagement';
40
+ export * from './providers/DynamicsProvider';