@khester/create-dynamics-app 1.0.8 → 1.1.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.
- package/bin/create-dynamics-app.js +1 -1
- package/dist/index.js +140 -15
- package/dist/index.js.map +1 -1
- package/dist/utils/consultingHelpers.d.ts +13 -0
- package/dist/utils/consultingHelpers.d.ts.map +1 -0
- package/dist/utils/consultingHelpers.js +569 -0
- package/dist/utils/consultingHelpers.js.map +1 -0
- package/dist/utils/copyTemplate.d.ts.map +1 -1
- package/dist/utils/copyTemplate.js.map +1 -1
- package/dist/utils/initGit.d.ts.map +1 -1
- package/dist/utils/initGit.js.map +1 -1
- package/dist/utils/installDependencies.d.ts.map +1 -1
- package/dist/utils/installDependencies.js +3 -2
- package/dist/utils/installDependencies.js.map +1 -1
- package/dist/utils/updatePackageJson.d.ts +1 -1
- package/dist/utils/updatePackageJson.d.ts.map +1 -1
- package/dist/utils/updatePackageJson.js +11 -1
- package/dist/utils/updatePackageJson.js.map +1 -1
- package/package.json +1 -1
- package/templates/dynamics-365-starter/INTEGRATION_TEST_RESULTS.md +302 -0
- package/templates/dynamics-365-starter/PHASE_4_COMPLETION_SUMMARY.md +305 -0
- package/templates/dynamics-365-starter/README.md +566 -137
- package/templates/dynamics-365-starter/deployment/QUICKSTART-MAC.md +507 -0
- package/templates/dynamics-365-starter/deployment/QUICKSTART-WINDOWS.md +372 -0
- package/templates/dynamics-365-starter/deployment/README.md +484 -0
- package/templates/dynamics-365-starter/deployment/pipelines/README.md +375 -0
- package/templates/dynamics-365-starter/deployment/pipelines/azure-pipelines.yml +330 -0
- package/templates/dynamics-365-starter/deployment/pipelines/github-actions.yml +422 -0
- package/templates/dynamics-365-starter/deployment/pipelines/jenkins.groovy +636 -0
- package/templates/dynamics-365-starter/deployment/scripts/deploy.ps1 +417 -0
- package/templates/dynamics-365-starter/deployment/scripts/deploy.sh +582 -0
- package/templates/dynamics-365-starter/deployment/scripts/team-onboarding.ps1 +486 -0
- package/templates/dynamics-365-starter/deployment/scripts/team-onboarding.sh +567 -0
- package/templates/dynamics-365-starter/deployment/scripts/validate-setup.ps1 +703 -0
- package/templates/dynamics-365-starter/deployment/scripts/validate-setup.sh +671 -0
- package/templates/dynamics-365-starter/docs/ARCHITECTURE_OVERVIEW.md +506 -0
- package/templates/dynamics-365-starter/docs/BEST_PRACTICES.md +723 -0
- package/templates/dynamics-365-starter/docs/MIGRATION_GUIDE.md +447 -0
- package/templates/dynamics-365-starter/docs/team-standards/README.md +273 -0
- package/templates/dynamics-365-starter/docs/team-standards/client-onboarding.md +577 -0
- package/templates/dynamics-365-starter/docs/team-standards/code-review-checklist.md +359 -0
- package/templates/dynamics-365-starter/docs/team-standards/coding-standards.md +700 -0
- package/templates/dynamics-365-starter/docs/team-standards/cross-platform-team-guide.md +736 -0
- package/templates/dynamics-365-starter/docs/team-standards/development-workflows.md +727 -0
- package/templates/dynamics-365-starter/docs/troubleshooting/common-errors.md +758 -0
- package/templates/dynamics-365-starter/docs/troubleshooting/platform-specific-issues.md +878 -0
- package/templates/dynamics-365-starter/package.json +22 -1
- package/templates/dynamics-365-starter/public/index.html +8 -11
- package/templates/dynamics-365-starter/scripts/custom-build.js +255 -0
- package/templates/dynamics-365-starter/src/client-project-template/README.md +234 -0
- package/templates/dynamics-365-starter/src/client-project-template/config/client.template.json +114 -0
- package/templates/dynamics-365-starter/src/client-project-template/config/environments/template.json +186 -0
- package/templates/dynamics-365-starter/src/client-project-template/scripts/client-setup.js +667 -0
- package/templates/dynamics-365-starter/src/components/AccountForm.css +71 -0
- package/templates/dynamics-365-starter/src/components/AccountForm.tsx +541 -0
- package/templates/dynamics-365-starter/src/components/AccountManagement.css +86 -0
- package/templates/dynamics-365-starter/src/components/AccountManagement.tsx +370 -0
- package/templates/dynamics-365-starter/src/components/ContactForm.tsx +149 -63
- package/templates/dynamics-365-starter/src/components/ContactManagement.tsx +153 -63
- package/templates/dynamics-365-starter/src/components/Logging/LogDialog.tsx +291 -0
- package/templates/dynamics-365-starter/src/components/Logging/LoggingContext.tsx +166 -0
- package/templates/dynamics-365-starter/src/components/Logging/LoggingDebugPanel.css +192 -0
- package/templates/dynamics-365-starter/src/components/Logging/LoggingDebugPanel.tsx +177 -0
- package/templates/dynamics-365-starter/src/components/Logging/LoggingProvider.tsx +3 -0
- package/templates/dynamics-365-starter/src/components/Logging/logger.ts +193 -0
- package/templates/dynamics-365-starter/src/constants/account.ts +410 -0
- package/templates/dynamics-365-starter/src/constants/contact.ts +362 -0
- package/templates/dynamics-365-starter/src/examples/README.md +52 -0
- package/templates/dynamics-365-starter/src/examples/component-examples/opportunity-management.tsx +625 -0
- package/templates/dynamics-365-starter/src/examples/entity-examples/opportunity-model.ts +545 -0
- package/templates/dynamics-365-starter/src/examples/integration-examples/custom-pcf-wrapper.tsx +722 -0
- package/templates/dynamics-365-starter/src/examples/workflow-examples/sales-workflow.ts +662 -0
- package/templates/dynamics-365-starter/src/index.tsx +107 -19
- package/templates/dynamics-365-starter/src/models/Account.ts +480 -0
- package/templates/dynamics-365-starter/src/models/BaseEntity.ts +204 -0
- package/templates/dynamics-365-starter/src/models/Contact.ts +580 -0
- package/templates/dynamics-365-starter/src/page-templates/EntityDashboard.tsx +519 -0
- package/templates/dynamics-365-starter/src/page-templates/EntityDetailPage.tsx +456 -0
- package/templates/dynamics-365-starter/src/page-templates/EntityListPage.tsx +406 -0
- package/templates/dynamics-365-starter/src/page-templates/RelatedEntitiesPage.tsx +578 -0
- package/templates/dynamics-365-starter/src/page-templates/SearchPage.tsx +629 -0
- package/templates/dynamics-365-starter/src/pcf/ContactControlWrapper.tsx +75 -22
- package/templates/dynamics-365-starter/src/pcf/MultiEntityControlWrapper.tsx +205 -0
- package/templates/dynamics-365-starter/src/providers/DynamicsProvider.tsx +297 -80
- package/templates/dynamics-365-starter/src/services/MockApiService.ts +260 -0
- package/templates/dynamics-365-starter/src/services/ServiceFactory.ts +65 -0
- package/templates/dynamics-365-starter/src/services/XrmApiService.ts +213 -0
- package/templates/dynamics-365-starter/src/styles/index.css +74 -7
- package/templates/dynamics-365-starter/tools/entity-generator/index.js +168 -0
- package/templates/dynamics-365-starter/tools/entity-generator/templates/constants.template.ts +124 -0
- package/templates/dynamics-365-starter/tools/entity-generator/templates/form.template.css +283 -0
- package/templates/dynamics-365-starter/tools/entity-generator/templates/form.template.tsx +275 -0
- package/templates/dynamics-365-starter/tools/entity-generator/templates/management.template.css +204 -0
- package/templates/dynamics-365-starter/tools/entity-generator/templates/management.template.tsx +413 -0
- package/templates/dynamics-365-starter/tools/entity-generator/templates/model.template.ts +250 -0
- package/templates/dynamics-365-starter/tools/metadata-sync/d365-client.js +410 -0
- package/templates/dynamics-365-starter/tools/metadata-sync/index.js +512 -0
- package/templates/dynamics-365-starter/tools/metadata-sync/type-generator.js +675 -0
- package/templates/dynamics-365-starter/tsconfig.json +11 -8
- package/templates/dynamics-365-starter/webpack.config.js +8 -9
- package/templates/power-pages-starter/README.md +7 -1
- package/templates/power-pages-starter/public/index.html +8 -11
- package/templates/power-pages-starter/src/components/ContactForm.tsx +60 -41
- package/templates/power-pages-starter/src/index.tsx +3 -3
- package/templates/power-pages-starter/src/providers/PowerPagesProvider.tsx +46 -23
- package/templates/power-pages-starter/tsconfig.json +3 -9
- package/templates/power-pages-starter/webpack.config.js +8 -3
|
@@ -1,55 +1,52 @@
|
|
|
1
1
|
import React, { useState, useEffect, useCallback } from 'react';
|
|
2
|
-
import {
|
|
3
|
-
Button,
|
|
4
|
-
TextField,
|
|
2
|
+
import {
|
|
3
|
+
Button,
|
|
4
|
+
TextField,
|
|
5
5
|
DetailsList,
|
|
6
6
|
Dialog,
|
|
7
|
-
Panel
|
|
7
|
+
Panel,
|
|
8
8
|
} from '@khester/dynamics-ui-components';
|
|
9
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
SelectionMode,
|
|
11
|
+
DetailsListLayoutMode,
|
|
12
|
+
CheckboxVisibility,
|
|
13
|
+
} from '@fluentui/react';
|
|
10
14
|
import { useDynamicsApi } from '../providers/DynamicsProvider';
|
|
15
|
+
import { Contact } from '../models/Contact';
|
|
16
|
+
import { ContactConstants } from '../constants/contact';
|
|
17
|
+
import { Logger } from './Logging/logger';
|
|
11
18
|
import { ContactForm } from './ContactForm';
|
|
12
19
|
import './ContactManagement.css';
|
|
13
20
|
|
|
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
21
|
const columns = [
|
|
25
22
|
{
|
|
26
|
-
key:
|
|
23
|
+
key: ContactConstants.PrimaryName,
|
|
27
24
|
name: 'Name',
|
|
28
|
-
fieldName:
|
|
25
|
+
fieldName: ContactConstants.PrimaryName,
|
|
29
26
|
minWidth: 150,
|
|
30
27
|
maxWidth: 200,
|
|
31
28
|
isResizable: true,
|
|
32
29
|
},
|
|
33
30
|
{
|
|
34
|
-
key:
|
|
31
|
+
key: ContactConstants.EMailAddress1,
|
|
35
32
|
name: 'Email',
|
|
36
|
-
fieldName:
|
|
33
|
+
fieldName: ContactConstants.EMailAddress1,
|
|
37
34
|
minWidth: 200,
|
|
38
35
|
maxWidth: 300,
|
|
39
36
|
isResizable: true,
|
|
40
37
|
},
|
|
41
38
|
{
|
|
42
|
-
key:
|
|
39
|
+
key: ContactConstants.BusinessPhone,
|
|
43
40
|
name: 'Phone',
|
|
44
|
-
fieldName:
|
|
41
|
+
fieldName: ContactConstants.BusinessPhone,
|
|
45
42
|
minWidth: 150,
|
|
46
43
|
maxWidth: 200,
|
|
47
44
|
isResizable: true,
|
|
48
45
|
},
|
|
49
46
|
{
|
|
50
|
-
key:
|
|
47
|
+
key: ContactConstants.CreatedOn,
|
|
51
48
|
name: 'Created',
|
|
52
|
-
fieldName:
|
|
49
|
+
fieldName: ContactConstants.CreatedOn,
|
|
53
50
|
minWidth: 150,
|
|
54
51
|
maxWidth: 200,
|
|
55
52
|
isResizable: true,
|
|
@@ -57,8 +54,8 @@ const columns = [
|
|
|
57
54
|
];
|
|
58
55
|
|
|
59
56
|
export const ContactManagement: React.FC = () => {
|
|
60
|
-
const {
|
|
61
|
-
|
|
57
|
+
const { apiService, isEnvironmentMock, environmentType } = useDynamicsApi();
|
|
58
|
+
|
|
62
59
|
const [contacts, setContacts] = useState<Contact[]>([]);
|
|
63
60
|
const [filteredContacts, setFilteredContacts] = useState<Contact[]>([]);
|
|
64
61
|
const [loading, setLoading] = useState(false);
|
|
@@ -69,26 +66,58 @@ export const ContactManagement: React.FC = () => {
|
|
|
69
66
|
const [showDeleteDialog, setShowDeleteDialog] = useState(false);
|
|
70
67
|
const [contactToDelete, setContactToDelete] = useState<Contact | null>(null);
|
|
71
68
|
|
|
69
|
+
useEffect(() => {
|
|
70
|
+
Logger.userAction(
|
|
71
|
+
'ContactManagement loaded',
|
|
72
|
+
{ environmentType, isEnvironmentMock },
|
|
73
|
+
'ContactManagement'
|
|
74
|
+
);
|
|
75
|
+
}, [environmentType, isEnvironmentMock]);
|
|
76
|
+
|
|
72
77
|
const loadContacts = useCallback(async () => {
|
|
78
|
+
if (!apiService) {
|
|
79
|
+
Logger.error(
|
|
80
|
+
'API service not available',
|
|
81
|
+
'ContactManagement.loadContacts'
|
|
82
|
+
);
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
|
|
73
86
|
setLoading(true);
|
|
87
|
+
Logger.log('Loading contacts', 'ContactManagement.loadContacts');
|
|
88
|
+
|
|
74
89
|
try {
|
|
75
|
-
const
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
90
|
+
const contactsData = await Contact.retrieveActiveContacts(apiService);
|
|
91
|
+
|
|
92
|
+
const contactsWithDisplayData = contactsData.map((contact) => {
|
|
93
|
+
const contactInstance = new Contact(contact);
|
|
94
|
+
return {
|
|
95
|
+
...contactInstance,
|
|
96
|
+
fullname:
|
|
97
|
+
`${contact.firstname || ''} ${contact.lastname || ''}`.trim(),
|
|
98
|
+
createdon: contact.createdon
|
|
99
|
+
? new Date(contact.createdon).toLocaleDateString()
|
|
100
|
+
: '',
|
|
101
|
+
} as Contact;
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
setContacts(contactsWithDisplayData);
|
|
105
|
+
setFilteredContacts(contactsWithDisplayData);
|
|
106
|
+
|
|
107
|
+
Logger.log(
|
|
108
|
+
`Successfully loaded ${contactsWithDisplayData.length} contacts`,
|
|
109
|
+
'ContactManagement.loadContacts'
|
|
110
|
+
);
|
|
86
111
|
} catch (error) {
|
|
87
|
-
|
|
112
|
+
Logger.error(
|
|
113
|
+
'Failed to load contacts',
|
|
114
|
+
'ContactManagement.loadContacts',
|
|
115
|
+
error
|
|
116
|
+
);
|
|
88
117
|
} finally {
|
|
89
118
|
setLoading(false);
|
|
90
119
|
}
|
|
91
|
-
}, [
|
|
120
|
+
}, [apiService]);
|
|
92
121
|
|
|
93
122
|
useEffect(() => {
|
|
94
123
|
loadContacts();
|
|
@@ -98,62 +127,118 @@ export const ContactManagement: React.FC = () => {
|
|
|
98
127
|
if (!searchText) {
|
|
99
128
|
setFilteredContacts(contacts);
|
|
100
129
|
} else {
|
|
101
|
-
const filtered = contacts.filter(
|
|
102
|
-
contact
|
|
103
|
-
|
|
104
|
-
|
|
130
|
+
const filtered = contacts.filter(
|
|
131
|
+
(contact) =>
|
|
132
|
+
contact.fullname?.toLowerCase().includes(searchText.toLowerCase()) ||
|
|
133
|
+
contact.emailaddress1
|
|
134
|
+
?.toLowerCase()
|
|
135
|
+
.includes(searchText.toLowerCase()) ||
|
|
136
|
+
contact.telephone1?.includes(searchText)
|
|
105
137
|
);
|
|
106
138
|
setFilteredContacts(filtered);
|
|
107
139
|
}
|
|
108
140
|
}, [contacts, searchText]);
|
|
109
141
|
|
|
110
|
-
const handleItemInvoked = useCallback(
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
142
|
+
const handleItemInvoked = useCallback(
|
|
143
|
+
(item: Contact) => {
|
|
144
|
+
Logger.userAction(
|
|
145
|
+
'Contact item invoked',
|
|
146
|
+
{ contactId: item.contactid, contactName: item.fullname },
|
|
147
|
+
'ContactManagement.handleItemInvoked'
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
if (selectedContact?.contactid === item.contactid) {
|
|
151
|
+
setShowEditContactPanel(true);
|
|
152
|
+
} else {
|
|
153
|
+
setSelectedContact(item);
|
|
154
|
+
}
|
|
155
|
+
},
|
|
156
|
+
[selectedContact]
|
|
157
|
+
);
|
|
117
158
|
|
|
118
159
|
const handleNewContact = useCallback(() => {
|
|
160
|
+
Logger.userAction(
|
|
161
|
+
'New contact panel opened',
|
|
162
|
+
{},
|
|
163
|
+
'ContactManagement.handleNewContact'
|
|
164
|
+
);
|
|
119
165
|
setShowNewContactPanel(true);
|
|
120
166
|
}, []);
|
|
121
167
|
|
|
122
168
|
const handleEditContact = useCallback(() => {
|
|
123
169
|
if (selectedContact) {
|
|
170
|
+
Logger.userAction(
|
|
171
|
+
'Edit contact panel opened',
|
|
172
|
+
{
|
|
173
|
+
contactId: selectedContact.contactid,
|
|
174
|
+
contactName: selectedContact.fullname,
|
|
175
|
+
},
|
|
176
|
+
'ContactManagement.handleEditContact'
|
|
177
|
+
);
|
|
124
178
|
setShowEditContactPanel(true);
|
|
125
179
|
}
|
|
126
180
|
}, [selectedContact]);
|
|
127
181
|
|
|
128
182
|
const handleDeleteContact = useCallback(() => {
|
|
129
183
|
if (selectedContact) {
|
|
184
|
+
Logger.userAction(
|
|
185
|
+
'Delete contact dialog opened',
|
|
186
|
+
{
|
|
187
|
+
contactId: selectedContact.contactid,
|
|
188
|
+
contactName: selectedContact.fullname,
|
|
189
|
+
},
|
|
190
|
+
'ContactManagement.handleDeleteContact'
|
|
191
|
+
);
|
|
130
192
|
setContactToDelete(selectedContact);
|
|
131
193
|
setShowDeleteDialog(true);
|
|
132
194
|
}
|
|
133
195
|
}, [selectedContact]);
|
|
134
196
|
|
|
135
197
|
const confirmDelete = useCallback(async () => {
|
|
136
|
-
if (contactToDelete) {
|
|
198
|
+
if (contactToDelete && apiService) {
|
|
137
199
|
try {
|
|
138
|
-
|
|
200
|
+
Logger.userAction(
|
|
201
|
+
'Contact deletion confirmed',
|
|
202
|
+
{
|
|
203
|
+
contactId: contactToDelete.contactid,
|
|
204
|
+
contactName: contactToDelete.fullname,
|
|
205
|
+
},
|
|
206
|
+
'ContactManagement.confirmDelete'
|
|
207
|
+
);
|
|
208
|
+
|
|
209
|
+
await Contact.delete(apiService, contactToDelete.contactid!);
|
|
139
210
|
await loadContacts();
|
|
211
|
+
|
|
140
212
|
setShowDeleteDialog(false);
|
|
141
213
|
setContactToDelete(null);
|
|
142
214
|
setSelectedContact(null);
|
|
215
|
+
|
|
216
|
+
Logger.log(
|
|
217
|
+
`Successfully deleted contact: ${contactToDelete.fullname}`,
|
|
218
|
+
'ContactManagement.confirmDelete'
|
|
219
|
+
);
|
|
143
220
|
} catch (error) {
|
|
144
|
-
|
|
221
|
+
Logger.error(
|
|
222
|
+
`Failed to delete contact: ${contactToDelete.fullname}`,
|
|
223
|
+
'ContactManagement.confirmDelete',
|
|
224
|
+
error
|
|
225
|
+
);
|
|
145
226
|
}
|
|
146
227
|
}
|
|
147
|
-
}, [contactToDelete,
|
|
228
|
+
}, [contactToDelete, apiService, loadContacts]);
|
|
148
229
|
|
|
149
230
|
const handleContactSaved = useCallback(() => {
|
|
231
|
+
Logger.userAction(
|
|
232
|
+
'Contact saved, refreshing list',
|
|
233
|
+
{},
|
|
234
|
+
'ContactManagement.handleContactSaved'
|
|
235
|
+
);
|
|
150
236
|
loadContacts();
|
|
151
237
|
setShowNewContactPanel(false);
|
|
152
238
|
setShowEditContactPanel(false);
|
|
153
239
|
setSelectedContact(null);
|
|
154
240
|
}, [loadContacts]);
|
|
155
241
|
|
|
156
|
-
|
|
157
242
|
return (
|
|
158
243
|
<div className="contact-management">
|
|
159
244
|
<div className="contact-management__header">
|
|
@@ -203,16 +288,16 @@ export const ContactManagement: React.FC = () => {
|
|
|
203
288
|
layoutMode={DetailsListLayoutMode.justified}
|
|
204
289
|
checkboxVisibility={CheckboxVisibility.onHover}
|
|
205
290
|
/>
|
|
206
|
-
|
|
291
|
+
|
|
207
292
|
{loading && (
|
|
208
|
-
<div className="contact-management__loading">
|
|
209
|
-
Loading contacts...
|
|
210
|
-
</div>
|
|
293
|
+
<div className="contact-management__loading">Loading contacts...</div>
|
|
211
294
|
)}
|
|
212
|
-
|
|
295
|
+
|
|
213
296
|
{!loading && filteredContacts.length === 0 && (
|
|
214
297
|
<div className="contact-management__empty">
|
|
215
|
-
{searchText
|
|
298
|
+
{searchText
|
|
299
|
+
? 'No contacts found matching your search.'
|
|
300
|
+
: 'No contacts found. Create your first contact!'}
|
|
216
301
|
</div>
|
|
217
302
|
)}
|
|
218
303
|
</div>
|
|
@@ -241,7 +326,12 @@ export const ContactManagement: React.FC = () => {
|
|
|
241
326
|
{selectedContact && (
|
|
242
327
|
<ContactForm
|
|
243
328
|
contactId={selectedContact.contactid}
|
|
244
|
-
initialData={
|
|
329
|
+
initialData={{
|
|
330
|
+
...selectedContact,
|
|
331
|
+
birthdate: selectedContact.birthdate
|
|
332
|
+
? new Date(selectedContact.birthdate)
|
|
333
|
+
: undefined,
|
|
334
|
+
}}
|
|
245
335
|
onSave={handleContactSaved}
|
|
246
336
|
onCancel={() => {
|
|
247
337
|
setShowEditContactPanel(false);
|
|
@@ -259,9 +349,9 @@ export const ContactManagement: React.FC = () => {
|
|
|
259
349
|
content={`Are you sure you want to delete ${contactToDelete?.fullname}? This action cannot be undone.`}
|
|
260
350
|
actions={[
|
|
261
351
|
{ text: 'Delete', onClick: confirmDelete, primary: true },
|
|
262
|
-
{ text: 'Cancel', onClick: () => setShowDeleteDialog(false) }
|
|
352
|
+
{ text: 'Cancel', onClick: () => setShowDeleteDialog(false) },
|
|
263
353
|
]}
|
|
264
354
|
/>
|
|
265
355
|
</div>
|
|
266
356
|
);
|
|
267
|
-
};
|
|
357
|
+
};
|
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import {
|
|
3
|
+
Dialog,
|
|
4
|
+
DynamicsPrimaryButton,
|
|
5
|
+
DynamicsDefaultButton,
|
|
6
|
+
} from '@khester/dynamics-ui-components';
|
|
7
|
+
import { useLogging, LogEntry } from './LoggingContext';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Props for LogDialog component
|
|
11
|
+
*/
|
|
12
|
+
interface LogDialogProps {
|
|
13
|
+
/** Whether the dialog is open */
|
|
14
|
+
isOpen: boolean;
|
|
15
|
+
/** Callback when dialog should be closed */
|
|
16
|
+
onClose: () => void;
|
|
17
|
+
/** Optional title for the dialog */
|
|
18
|
+
title?: string;
|
|
19
|
+
/** Optional maximum height for the log content */
|
|
20
|
+
maxHeight?: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Dialog component for displaying logs in a modal
|
|
25
|
+
*/
|
|
26
|
+
export const LogDialog: React.FC<LogDialogProps> = ({
|
|
27
|
+
isOpen,
|
|
28
|
+
onClose,
|
|
29
|
+
title = 'Application Logs',
|
|
30
|
+
maxHeight = '400px',
|
|
31
|
+
}) => {
|
|
32
|
+
const { logs, clearLogs } = useLogging();
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Handle clearing all logs
|
|
36
|
+
*/
|
|
37
|
+
const handleClearLogs = () => {
|
|
38
|
+
clearLogs();
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Handle copying logs to clipboard
|
|
43
|
+
*/
|
|
44
|
+
const handleCopyLogs = async () => {
|
|
45
|
+
const logText = logs.map(formatLogEntry).join('\n');
|
|
46
|
+
try {
|
|
47
|
+
await navigator.clipboard.writeText(logText);
|
|
48
|
+
// Could add a notification here if needed
|
|
49
|
+
} catch (error) {
|
|
50
|
+
// Fallback for browsers that don't support clipboard API
|
|
51
|
+
console.warn('Failed to copy logs to clipboard:', error);
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Format a log entry for display
|
|
57
|
+
*/
|
|
58
|
+
const formatLogEntry = (log: LogEntry): string => {
|
|
59
|
+
const timestamp = new Date(log.timestamp).toLocaleString();
|
|
60
|
+
const source = log.source ? ` [${log.source}]` : '';
|
|
61
|
+
return `${timestamp}${source}: ${log.message}`;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Format a log entry for JSX display
|
|
66
|
+
*/
|
|
67
|
+
const formatLogEntryJSX = (log: LogEntry, index: number): JSX.Element => {
|
|
68
|
+
const timestamp = new Date(log.timestamp).toLocaleString();
|
|
69
|
+
const source = log.source ? ` [${log.source}]` : '';
|
|
70
|
+
|
|
71
|
+
return (
|
|
72
|
+
<div key={index} style={{ marginBottom: '8px', fontSize: '12px' }}>
|
|
73
|
+
<span style={{ color: '#666', fontFamily: 'monospace' }}>
|
|
74
|
+
{timestamp}
|
|
75
|
+
</span>
|
|
76
|
+
{source && (
|
|
77
|
+
<span
|
|
78
|
+
style={{
|
|
79
|
+
color: '#0078d4',
|
|
80
|
+
fontFamily: 'monospace',
|
|
81
|
+
marginLeft: '4px',
|
|
82
|
+
}}
|
|
83
|
+
>
|
|
84
|
+
{source}
|
|
85
|
+
</span>
|
|
86
|
+
)}
|
|
87
|
+
<span style={{ fontFamily: 'monospace', marginLeft: '4px' }}>
|
|
88
|
+
: {log.message}
|
|
89
|
+
</span>
|
|
90
|
+
</div>
|
|
91
|
+
);
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
const dialogActions = [
|
|
95
|
+
{
|
|
96
|
+
text: 'Close',
|
|
97
|
+
onClick: onClose,
|
|
98
|
+
primary: true,
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
text: 'Clear Logs',
|
|
102
|
+
onClick: handleClearLogs,
|
|
103
|
+
disabled: logs.length === 0,
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
text: 'Copy to Clipboard',
|
|
107
|
+
onClick: handleCopyLogs,
|
|
108
|
+
disabled: logs.length === 0,
|
|
109
|
+
},
|
|
110
|
+
];
|
|
111
|
+
|
|
112
|
+
return (
|
|
113
|
+
<Dialog
|
|
114
|
+
hidden={!isOpen}
|
|
115
|
+
onDismiss={onClose}
|
|
116
|
+
title={title}
|
|
117
|
+
subText={`${logs.length} log ${logs.length === 1 ? 'entry' : 'entries'}`}
|
|
118
|
+
actions={dialogActions}
|
|
119
|
+
content={
|
|
120
|
+
<div
|
|
121
|
+
style={{
|
|
122
|
+
maxHeight: maxHeight,
|
|
123
|
+
overflowY: 'auto',
|
|
124
|
+
padding: '8px',
|
|
125
|
+
backgroundColor: '#f8f8f8',
|
|
126
|
+
border: '1px solid #ddd',
|
|
127
|
+
borderRadius: '4px',
|
|
128
|
+
fontFamily: 'Consolas, "Courier New", monospace',
|
|
129
|
+
minWidth: '600px',
|
|
130
|
+
maxWidth: '800px',
|
|
131
|
+
}}
|
|
132
|
+
>
|
|
133
|
+
{logs.length === 0 ? (
|
|
134
|
+
<div style={{ fontStyle: 'italic', color: '#666' }}>
|
|
135
|
+
No logs available
|
|
136
|
+
</div>
|
|
137
|
+
) : (
|
|
138
|
+
logs.map((log, index) => formatLogEntryJSX(log, index))
|
|
139
|
+
)}
|
|
140
|
+
</div>
|
|
141
|
+
}
|
|
142
|
+
/>
|
|
143
|
+
);
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Simple log viewer component for inline display
|
|
148
|
+
*/
|
|
149
|
+
interface LogViewerProps {
|
|
150
|
+
/** Maximum height for the log container */
|
|
151
|
+
maxHeight?: string;
|
|
152
|
+
/** Whether to show timestamps */
|
|
153
|
+
showTimestamps?: boolean;
|
|
154
|
+
/** Whether to show source information */
|
|
155
|
+
showSources?: boolean;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export const LogViewer: React.FC<LogViewerProps> = ({
|
|
159
|
+
maxHeight = '200px',
|
|
160
|
+
showTimestamps = true,
|
|
161
|
+
showSources = true,
|
|
162
|
+
}) => {
|
|
163
|
+
const { logs } = useLogging();
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Format a log entry for inline display
|
|
167
|
+
*/
|
|
168
|
+
const formatLogEntry = (log: LogEntry, index: number): JSX.Element => {
|
|
169
|
+
const timestamp = showTimestamps
|
|
170
|
+
? new Date(log.timestamp).toLocaleString()
|
|
171
|
+
: '';
|
|
172
|
+
const source = showSources && log.source ? ` [${log.source}]` : '';
|
|
173
|
+
|
|
174
|
+
return (
|
|
175
|
+
<div
|
|
176
|
+
key={index}
|
|
177
|
+
style={{
|
|
178
|
+
marginBottom: '4px',
|
|
179
|
+
fontSize: '11px',
|
|
180
|
+
fontFamily: 'Consolas, "Courier New", monospace',
|
|
181
|
+
lineHeight: '1.4',
|
|
182
|
+
}}
|
|
183
|
+
>
|
|
184
|
+
{showTimestamps && (
|
|
185
|
+
<span style={{ color: '#666', marginRight: '8px' }}>{timestamp}</span>
|
|
186
|
+
)}
|
|
187
|
+
{showSources && source && (
|
|
188
|
+
<span style={{ color: '#0078d4', marginRight: '4px' }}>{source}</span>
|
|
189
|
+
)}
|
|
190
|
+
<span>{log.message}</span>
|
|
191
|
+
</div>
|
|
192
|
+
);
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
return (
|
|
196
|
+
<div
|
|
197
|
+
style={{
|
|
198
|
+
maxHeight: maxHeight,
|
|
199
|
+
overflowY: 'auto',
|
|
200
|
+
padding: '8px',
|
|
201
|
+
backgroundColor: '#f8f8f8',
|
|
202
|
+
border: '1px solid #ddd',
|
|
203
|
+
borderRadius: '4px',
|
|
204
|
+
fontSize: '12px',
|
|
205
|
+
}}
|
|
206
|
+
>
|
|
207
|
+
{logs.length === 0 ? (
|
|
208
|
+
<div style={{ fontStyle: 'italic', color: '#666', fontSize: '12px' }}>
|
|
209
|
+
No logs available
|
|
210
|
+
</div>
|
|
211
|
+
) : (
|
|
212
|
+
logs.map((log, index) => formatLogEntry(log, index))
|
|
213
|
+
)}
|
|
214
|
+
</div>
|
|
215
|
+
);
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Floating log viewer button that opens the log dialog
|
|
220
|
+
*/
|
|
221
|
+
interface LogViewerButtonProps {
|
|
222
|
+
/** Position from bottom in pixels */
|
|
223
|
+
bottom?: number;
|
|
224
|
+
/** Position from right in pixels */
|
|
225
|
+
right?: number;
|
|
226
|
+
/** Whether to show log count badge */
|
|
227
|
+
showCount?: boolean;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
export const LogViewerButton: React.FC<LogViewerButtonProps> = ({
|
|
231
|
+
bottom = 20,
|
|
232
|
+
right = 20,
|
|
233
|
+
showCount = true,
|
|
234
|
+
}) => {
|
|
235
|
+
const { logs } = useLogging();
|
|
236
|
+
const [isDialogOpen, setIsDialogOpen] = React.useState(false);
|
|
237
|
+
|
|
238
|
+
const buttonStyle: React.CSSProperties = {
|
|
239
|
+
position: 'fixed',
|
|
240
|
+
bottom: `${bottom}px`,
|
|
241
|
+
right: `${right}px`,
|
|
242
|
+
zIndex: 1000,
|
|
243
|
+
borderRadius: '50%',
|
|
244
|
+
width: '50px',
|
|
245
|
+
height: '50px',
|
|
246
|
+
backgroundColor: '#0078d4',
|
|
247
|
+
border: 'none',
|
|
248
|
+
color: 'white',
|
|
249
|
+
cursor: 'pointer',
|
|
250
|
+
boxShadow: '0 4px 8px rgba(0,0,0,0.2)',
|
|
251
|
+
display: 'flex',
|
|
252
|
+
alignItems: 'center',
|
|
253
|
+
justifyContent: 'center',
|
|
254
|
+
fontSize: '20px',
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
const badgeStyle: React.CSSProperties = {
|
|
258
|
+
position: 'absolute',
|
|
259
|
+
top: '-5px',
|
|
260
|
+
right: '-5px',
|
|
261
|
+
backgroundColor: '#d83b01',
|
|
262
|
+
color: 'white',
|
|
263
|
+
borderRadius: '50%',
|
|
264
|
+
width: '20px',
|
|
265
|
+
height: '20px',
|
|
266
|
+
fontSize: '12px',
|
|
267
|
+
display: 'flex',
|
|
268
|
+
alignItems: 'center',
|
|
269
|
+
justifyContent: 'center',
|
|
270
|
+
fontWeight: 'bold',
|
|
271
|
+
};
|
|
272
|
+
|
|
273
|
+
return (
|
|
274
|
+
<>
|
|
275
|
+
<button
|
|
276
|
+
style={buttonStyle}
|
|
277
|
+
onClick={() => setIsDialogOpen(true)}
|
|
278
|
+
title={`View logs (${logs.length} entries)`}
|
|
279
|
+
>
|
|
280
|
+
📋
|
|
281
|
+
{showCount && logs.length > 0 && (
|
|
282
|
+
<span style={badgeStyle}>
|
|
283
|
+
{logs.length > 99 ? '99+' : logs.length}
|
|
284
|
+
</span>
|
|
285
|
+
)}
|
|
286
|
+
</button>
|
|
287
|
+
|
|
288
|
+
<LogDialog isOpen={isDialogOpen} onClose={() => setIsDialogOpen(false)} />
|
|
289
|
+
</>
|
|
290
|
+
);
|
|
291
|
+
};
|