@khester/create-dynamics-app 2.0.0 → 2.2.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/README.md +28 -0
- package/dist/artifacts/registry.d.ts +4 -3
- package/dist/artifacts/registry.d.ts.map +1 -1
- package/dist/artifacts/registry.js +145 -11
- package/dist/artifacts/registry.js.map +1 -1
- package/dist/artifacts/types.d.ts +10 -1
- package/dist/artifacts/types.d.ts.map +1 -1
- package/dist/index.js +19 -2
- package/dist/index.js.map +1 -1
- package/dist/injectDevTools.d.ts.map +1 -1
- package/dist/injectDevTools.js +4 -2
- package/dist/injectDevTools.js.map +1 -1
- package/dist/scaffold.d.ts +23 -1
- package/dist/scaffold.d.ts.map +1 -1
- package/dist/scaffold.js +27 -1
- package/dist/scaffold.js.map +1 -1
- package/package.json +3 -2
- package/templates/grid-starter/ARCHITECTURE.md +66 -0
- package/templates/grid-starter/README.md +122 -0
- package/templates/grid-starter/env.example +16 -0
- package/templates/grid-starter/gitignore +6 -0
- package/templates/grid-starter/index.html +16 -0
- package/templates/grid-starter/package.json +39 -0
- package/templates/grid-starter/src/App.tsx +23 -0
- package/templates/grid-starter/src/core/services/FetchApiService.ts +117 -0
- package/templates/grid-starter/src/core/services/IApiService.ts +37 -0
- package/templates/grid-starter/src/core/services/MockApiService.ts +72 -0
- package/templates/grid-starter/src/core/services/ServiceFactory.ts +58 -0
- package/templates/grid-starter/src/core/services/XrmApiService.ts +135 -0
- package/templates/grid-starter/src/core/services/crudLogging.ts +52 -0
- package/templates/grid-starter/src/dev-tools/DevPanel.tsx +239 -0
- package/templates/grid-starter/src/grid/GridPage.tsx +119 -0
- package/templates/grid-starter/src/index.tsx +18 -0
- package/templates/grid-starter/src/vite-env.d.ts +15 -0
- package/templates/grid-starter/tools/deploy/deploy-webresource.cjs +117 -0
- package/templates/grid-starter/tsconfig.json +19 -0
- package/templates/grid-starter/vite.config.ts +76 -0
- package/templates/pcf-field/_variants/ValueInput.boolean.tsx +2 -0
- package/templates/pcf-field/_variants/ValueInput.date.tsx +2 -0
- package/templates/pcf-field/_variants/ValueInput.number.tsx +2 -0
- package/templates/pcf-field/_variants/ValueInput.optionset.tsx +77 -0
- package/templates/pcf-field/_variants/ValueInput.text.tsx +2 -0
- package/templates/pcf-field/index.ts +1 -1
- package/templates/pcf-field/package.json +3 -1
- package/templates/pcf-field/{{componentName}}Component.tsx +2 -0
- package/templates/react-custom-page/ARCHITECTURE.md +75 -0
- package/templates/react-custom-page/README.md +74 -568
- package/templates/react-custom-page/env.example +16 -0
- package/templates/react-custom-page/gitignore +1 -0
- package/templates/react-custom-page/index.html +16 -0
- package/templates/react-custom-page/package.json +21 -49
- package/templates/react-custom-page/src/App.tsx +26 -0
- package/templates/react-custom-page/src/core/recordContext.test.ts +30 -0
- package/templates/react-custom-page/src/core/recordContext.ts +51 -0
- package/templates/react-custom-page/src/core/services/FetchApiService.ts +117 -0
- package/templates/react-custom-page/src/core/services/IApiService.ts +37 -0
- package/templates/react-custom-page/src/core/services/MockApiService.ts +73 -0
- package/templates/react-custom-page/src/core/services/ServiceFactory.ts +58 -0
- package/templates/react-custom-page/src/core/services/XrmApiService.ts +135 -0
- package/templates/react-custom-page/src/core/services/crudLogging.ts +52 -0
- package/templates/react-custom-page/src/dev-tools/DevPanel.tsx +238 -0
- package/templates/react-custom-page/src/domain/diff.test.ts +87 -0
- package/templates/react-custom-page/src/domain/diff.ts +38 -0
- package/templates/react-custom-page/src/example/ExamplePage.tsx +140 -0
- package/templates/react-custom-page/src/example/exampleError.ts +36 -0
- package/templates/react-custom-page/src/example/hooks/useExampleData.ts +40 -0
- package/templates/react-custom-page/src/example/hooks/useExampleForm.ts +99 -0
- package/templates/react-custom-page/src/example/mappers/accountMapper.test.ts +38 -0
- package/templates/react-custom-page/src/example/mappers/accountMapper.ts +55 -0
- package/templates/react-custom-page/src/example/models/Account.ts +74 -0
- package/templates/react-custom-page/src/index.tsx +18 -128
- package/templates/react-custom-page/src/vite-env.d.ts +15 -0
- package/templates/react-custom-page/tools/deploy/deploy-webresource.cjs +117 -0
- package/templates/react-custom-page/tsconfig.json +12 -22
- package/templates/react-custom-page/vite.config.ts +76 -0
- package/templates/starter-page/README.md +38 -0
- package/templates/starter-page/_variants/App.dashboard.v8.tsx +46 -0
- package/templates/starter-page/_variants/App.form.v8.tsx +59 -0
- package/templates/starter-page/_variants/App.master-detail.v8.tsx +61 -0
- package/templates/starter-page/_variants/App.panel.v8.tsx +99 -0
- package/templates/starter-page/gitignore +5 -0
- package/templates/starter-page/package.json +27 -0
- package/templates/starter-page/public/index.html +11 -0
- package/templates/starter-page/src/index.tsx +10 -0
- package/templates/starter-page/src/services/dataverse.ts +30 -0
- package/templates/starter-page/tsconfig.json +15 -0
- package/templates/starter-page/webpack.config.js +17 -0
- package/templates/react-custom-page/deployment/README.md +0 -484
- package/templates/react-custom-page/docs/ARCHITECTURE_OVERVIEW.md +0 -506
- package/templates/react-custom-page/docs/BEST_PRACTICES.md +0 -723
- package/templates/react-custom-page/docs/MIGRATION_GUIDE.md +0 -447
- package/templates/react-custom-page/public/index.html +0 -15
- package/templates/react-custom-page/scripts/custom-build.js +0 -255
- package/templates/react-custom-page/src/components/AccountForm.css +0 -71
- package/templates/react-custom-page/src/components/AccountForm.tsx +0 -541
- package/templates/react-custom-page/src/components/AccountManagement.css +0 -86
- package/templates/react-custom-page/src/components/AccountManagement.tsx +0 -370
- package/templates/react-custom-page/src/components/ContactForm.css +0 -48
- package/templates/react-custom-page/src/components/ContactForm.tsx +0 -327
- package/templates/react-custom-page/src/components/ContactManagement.css +0 -86
- package/templates/react-custom-page/src/components/ContactManagement.tsx +0 -357
- package/templates/react-custom-page/src/components/Logging/LogDialog.tsx +0 -291
- package/templates/react-custom-page/src/components/Logging/LoggingContext.tsx +0 -166
- package/templates/react-custom-page/src/components/Logging/LoggingDebugPanel.css +0 -192
- package/templates/react-custom-page/src/components/Logging/LoggingDebugPanel.tsx +0 -177
- package/templates/react-custom-page/src/components/Logging/LoggingProvider.tsx +0 -3
- package/templates/react-custom-page/src/components/Logging/logger.ts +0 -193
- package/templates/react-custom-page/src/constants/account.ts +0 -410
- package/templates/react-custom-page/src/constants/contact.ts +0 -362
- package/templates/react-custom-page/src/models/Account.ts +0 -480
- package/templates/react-custom-page/src/models/BaseEntity.ts +0 -204
- package/templates/react-custom-page/src/models/Contact.ts +0 -580
- package/templates/react-custom-page/src/pcf/ContactControlWrapper.tsx +0 -107
- package/templates/react-custom-page/src/pcf/MultiEntityControlWrapper.tsx +0 -205
- package/templates/react-custom-page/src/providers/DynamicsProvider.tsx +0 -353
- package/templates/react-custom-page/src/services/MockApiService.ts +0 -260
- package/templates/react-custom-page/src/services/ServiceFactory.ts +0 -65
- package/templates/react-custom-page/src/services/XrmApiService.ts +0 -213
- package/templates/react-custom-page/src/styles/index.css +0 -171
- package/templates/react-custom-page/tools/metadata-sync/index.js +0 -152
- package/templates/react-custom-page/webpack.config.js +0 -57
- /package/templates/_shared/dev-tools/auth/{get-token.js → get-token.cjs} +0 -0
|
@@ -1,260 +0,0 @@
|
|
|
1
|
-
import { IApiService } from '@khester/dynamics-ui-api-client';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Mock implementation of IApiService for local development and testing.
|
|
5
|
-
* Provides in-memory storage and simulated API responses.
|
|
6
|
-
*/
|
|
7
|
-
export class MockApiService implements IApiService {
|
|
8
|
-
private storage: Map<string, Map<string, any>> = new Map();
|
|
9
|
-
|
|
10
|
-
constructor() {
|
|
11
|
-
// Initialize with some mock data
|
|
12
|
-
this.initializeMockData();
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
async createRecord(entityName: string, record: any): Promise<any> {
|
|
16
|
-
console.log(`MockApiService: Creating ${entityName} record`, record);
|
|
17
|
-
|
|
18
|
-
// Simulate network delay
|
|
19
|
-
await this.simulateDelay();
|
|
20
|
-
|
|
21
|
-
// Get or create entity collection
|
|
22
|
-
if (!this.storage.has(entityName)) {
|
|
23
|
-
this.storage.set(entityName, new Map());
|
|
24
|
-
}
|
|
25
|
-
const collection = this.storage.get(entityName)!;
|
|
26
|
-
|
|
27
|
-
// Generate ID based on entity type
|
|
28
|
-
const id = this.generateId(entityName);
|
|
29
|
-
const primaryKey = this.getPrimaryKey(entityName);
|
|
30
|
-
|
|
31
|
-
// Create record with ID
|
|
32
|
-
const newRecord = {
|
|
33
|
-
...record,
|
|
34
|
-
[primaryKey]: id,
|
|
35
|
-
createdon: new Date().toISOString(),
|
|
36
|
-
modifiedon: new Date().toISOString(),
|
|
37
|
-
};
|
|
38
|
-
|
|
39
|
-
collection.set(id, newRecord);
|
|
40
|
-
|
|
41
|
-
return newRecord;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
async updateRecord(
|
|
45
|
-
entityName: string,
|
|
46
|
-
id: string,
|
|
47
|
-
record: any
|
|
48
|
-
): Promise<any> {
|
|
49
|
-
console.log(`MockApiService: Updating ${entityName} record ${id}`, record);
|
|
50
|
-
|
|
51
|
-
await this.simulateDelay();
|
|
52
|
-
|
|
53
|
-
const collection = this.storage.get(entityName);
|
|
54
|
-
if (!collection || !collection.has(id)) {
|
|
55
|
-
throw new Error(`Record not found: ${entityName} with ID ${id}`);
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
const existing = collection.get(id);
|
|
59
|
-
const updated = {
|
|
60
|
-
...existing,
|
|
61
|
-
...record,
|
|
62
|
-
modifiedon: new Date().toISOString(),
|
|
63
|
-
};
|
|
64
|
-
|
|
65
|
-
collection.set(id, updated);
|
|
66
|
-
|
|
67
|
-
return updated;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
async deleteRecord(entityName: string, id: string): Promise<void> {
|
|
71
|
-
console.log(`MockApiService: Deleting ${entityName} record ${id}`);
|
|
72
|
-
|
|
73
|
-
await this.simulateDelay();
|
|
74
|
-
|
|
75
|
-
const collection = this.storage.get(entityName);
|
|
76
|
-
if (!collection || !collection.has(id)) {
|
|
77
|
-
throw new Error(`Record not found: ${entityName} with ID ${id}`);
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
collection.delete(id);
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
async retrieveMultipleRecords(
|
|
84
|
-
entityName: string,
|
|
85
|
-
fetchXml: string
|
|
86
|
-
): Promise<{ entities: any[] }> {
|
|
87
|
-
console.log(
|
|
88
|
-
`MockApiService: Retrieving ${entityName} records with FetchXML`,
|
|
89
|
-
fetchXml
|
|
90
|
-
);
|
|
91
|
-
|
|
92
|
-
await this.simulateDelay();
|
|
93
|
-
|
|
94
|
-
const collection = this.storage.get(entityName);
|
|
95
|
-
if (!collection) {
|
|
96
|
-
return { entities: [] };
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
// Convert Map values to array
|
|
100
|
-
const entities = Array.from(collection.values());
|
|
101
|
-
|
|
102
|
-
// Simple filter parsing from FetchXML (basic implementation)
|
|
103
|
-
const filteredEntities = this.applyBasicFetchXmlFilter(entities, fetchXml);
|
|
104
|
-
|
|
105
|
-
return { entities: filteredEntities };
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
async executeRequest(requestName: string, requestData: any): Promise<any> {
|
|
109
|
-
console.log(
|
|
110
|
-
`MockApiService: Executing request ${requestName}`,
|
|
111
|
-
requestData
|
|
112
|
-
);
|
|
113
|
-
|
|
114
|
-
await this.simulateDelay();
|
|
115
|
-
|
|
116
|
-
// Mock implementation - return success response
|
|
117
|
-
return {
|
|
118
|
-
success: true,
|
|
119
|
-
requestName,
|
|
120
|
-
timestamp: new Date().toISOString(),
|
|
121
|
-
data: requestData,
|
|
122
|
-
};
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
async uploadFile(file: File): Promise<string> {
|
|
126
|
-
console.log(`MockApiService: Uploading file ${file.name}`);
|
|
127
|
-
|
|
128
|
-
await this.simulateDelay(1000); // Longer delay for file upload
|
|
129
|
-
|
|
130
|
-
// Return mock file URL
|
|
131
|
-
return `https://mock-storage.dynamics365.com/files/${this.generateId('file')}-${file.name}`;
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
/**
|
|
135
|
-
* Initialize mock data for development
|
|
136
|
-
*/
|
|
137
|
-
private initializeMockData(): void {
|
|
138
|
-
// Create accounts collection
|
|
139
|
-
const accounts = new Map<string, any>();
|
|
140
|
-
accounts.set('00000000-0000-0000-0000-000000000001', {
|
|
141
|
-
accountid: '00000000-0000-0000-0000-000000000001',
|
|
142
|
-
name: 'Contoso Ltd',
|
|
143
|
-
emailaddress1: 'info@contoso.com',
|
|
144
|
-
telephone1: '555-0100',
|
|
145
|
-
address1_city: 'Seattle',
|
|
146
|
-
address1_stateorprovince: 'WA',
|
|
147
|
-
address1_country: 'USA',
|
|
148
|
-
revenue: 5000000,
|
|
149
|
-
numberofemployees: 250,
|
|
150
|
-
createdon: '2024-01-15T10:00:00Z',
|
|
151
|
-
modifiedon: '2024-01-15T10:00:00Z',
|
|
152
|
-
});
|
|
153
|
-
accounts.set('00000000-0000-0000-0000-000000000002', {
|
|
154
|
-
accountid: '00000000-0000-0000-0000-000000000002',
|
|
155
|
-
name: 'Adventure Works',
|
|
156
|
-
emailaddress1: 'contact@adventureworks.com',
|
|
157
|
-
telephone1: '555-0200',
|
|
158
|
-
address1_city: 'Redmond',
|
|
159
|
-
address1_stateorprovince: 'WA',
|
|
160
|
-
address1_country: 'USA',
|
|
161
|
-
revenue: 10000000,
|
|
162
|
-
numberofemployees: 500,
|
|
163
|
-
createdon: '2024-01-20T14:30:00Z',
|
|
164
|
-
modifiedon: '2024-01-20T14:30:00Z',
|
|
165
|
-
});
|
|
166
|
-
this.storage.set('accounts', accounts);
|
|
167
|
-
|
|
168
|
-
// Create contacts collection
|
|
169
|
-
const contacts = new Map<string, any>();
|
|
170
|
-
contacts.set('00000000-0000-0000-0000-000000000101', {
|
|
171
|
-
contactid: '00000000-0000-0000-0000-000000000101',
|
|
172
|
-
firstname: 'John',
|
|
173
|
-
lastname: 'Doe',
|
|
174
|
-
emailaddress1: 'john.doe@contoso.com',
|
|
175
|
-
telephone1: '555-0101',
|
|
176
|
-
parentcustomerid: '00000000-0000-0000-0000-000000000001',
|
|
177
|
-
parentcustomerid_account: {
|
|
178
|
-
accountid: '00000000-0000-0000-0000-000000000001',
|
|
179
|
-
name: 'Contoso Ltd',
|
|
180
|
-
},
|
|
181
|
-
createdon: '2024-01-16T09:00:00Z',
|
|
182
|
-
modifiedon: '2024-01-16T09:00:00Z',
|
|
183
|
-
});
|
|
184
|
-
contacts.set('00000000-0000-0000-0000-000000000102', {
|
|
185
|
-
contactid: '00000000-0000-0000-0000-000000000102',
|
|
186
|
-
firstname: 'Jane',
|
|
187
|
-
lastname: 'Smith',
|
|
188
|
-
emailaddress1: 'jane.smith@adventureworks.com',
|
|
189
|
-
telephone1: '555-0201',
|
|
190
|
-
parentcustomerid: '00000000-0000-0000-0000-000000000002',
|
|
191
|
-
parentcustomerid_account: {
|
|
192
|
-
accountid: '00000000-0000-0000-0000-000000000002',
|
|
193
|
-
name: 'Adventure Works',
|
|
194
|
-
},
|
|
195
|
-
createdon: '2024-01-21T11:00:00Z',
|
|
196
|
-
modifiedon: '2024-01-21T11:00:00Z',
|
|
197
|
-
});
|
|
198
|
-
this.storage.set('contacts', contacts);
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
/**
|
|
202
|
-
* Generate a mock ID for an entity
|
|
203
|
-
*/
|
|
204
|
-
private generateId(entityName: string): string {
|
|
205
|
-
const timestamp = Date.now();
|
|
206
|
-
const random = Math.floor(Math.random() * 1000000);
|
|
207
|
-
return `${entityName}-${timestamp}-${random}`;
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
/**
|
|
211
|
-
* Get the primary key field name for an entity
|
|
212
|
-
*/
|
|
213
|
-
private getPrimaryKey(entityName: string): string {
|
|
214
|
-
const primaryKeys: Record<string, string> = {
|
|
215
|
-
accounts: 'accountid',
|
|
216
|
-
contacts: 'contactid',
|
|
217
|
-
opportunities: 'opportunityid',
|
|
218
|
-
leads: 'leadid',
|
|
219
|
-
incidents: 'incidentid',
|
|
220
|
-
quotes: 'quoteid',
|
|
221
|
-
salesorders: 'salesorderid',
|
|
222
|
-
invoices: 'invoiceid',
|
|
223
|
-
};
|
|
224
|
-
|
|
225
|
-
return primaryKeys[entityName] || `${entityName.replace(/s$/, '')}id`;
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
/**
|
|
229
|
-
* Apply basic FetchXML filtering (simplified implementation)
|
|
230
|
-
*/
|
|
231
|
-
private applyBasicFetchXmlFilter(entities: any[], fetchXml: string): any[] {
|
|
232
|
-
// Extract attribute names from FetchXML
|
|
233
|
-
const attributeMatches = fetchXml.match(/<attribute name="([^"]+)"/g);
|
|
234
|
-
if (!attributeMatches || attributeMatches.length === 0) {
|
|
235
|
-
return entities;
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
const attributes = attributeMatches.map((match) =>
|
|
239
|
-
match.replace('<attribute name="', '').replace('"', '')
|
|
240
|
-
);
|
|
241
|
-
|
|
242
|
-
// Filter entities to only include requested attributes
|
|
243
|
-
return entities.map((entity) => {
|
|
244
|
-
const filtered: any = {};
|
|
245
|
-
attributes.forEach((attr) => {
|
|
246
|
-
if (Object.prototype.hasOwnProperty.call(entity, attr)) {
|
|
247
|
-
filtered[attr] = entity[attr];
|
|
248
|
-
}
|
|
249
|
-
});
|
|
250
|
-
return filtered;
|
|
251
|
-
});
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
/**
|
|
255
|
-
* Simulate network delay for realistic behavior
|
|
256
|
-
*/
|
|
257
|
-
private async simulateDelay(ms: number = 200): Promise<void> {
|
|
258
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
259
|
-
}
|
|
260
|
-
}
|
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
import { IApiService } from '@khester/dynamics-ui-api-client';
|
|
2
|
-
import { MockApiService } from './MockApiService';
|
|
3
|
-
import { XrmApiService } from './XrmApiService';
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Factory class for creating API service instances based on the current environment.
|
|
7
|
-
* Automatically detects mock vs production environments and returns the appropriate service.
|
|
8
|
-
*/
|
|
9
|
-
export class ServiceFactory {
|
|
10
|
-
/**
|
|
11
|
-
* Determines if the current environment is a mock/development environment.
|
|
12
|
-
* Checks for localhost or 127.0.0.1 hostnames.
|
|
13
|
-
*/
|
|
14
|
-
public static get isMockEnvironment(): boolean {
|
|
15
|
-
const hostname = window.location.hostname;
|
|
16
|
-
return hostname === 'localhost' || hostname === '127.0.0.1';
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* Creates an API service instance appropriate for the current environment.
|
|
21
|
-
* @param Xrm The Xrm object from Dynamics 365 (required in production)
|
|
22
|
-
* @returns An IApiService implementation
|
|
23
|
-
* @throws Error if Xrm is not provided in production environment
|
|
24
|
-
*/
|
|
25
|
-
public static createApiService(Xrm?: any): IApiService {
|
|
26
|
-
if (this.isMockEnvironment) {
|
|
27
|
-
console.log(
|
|
28
|
-
'ServiceFactory: Running in mock environment - using MockApiService'
|
|
29
|
-
);
|
|
30
|
-
return new MockApiService();
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
if (!Xrm) {
|
|
34
|
-
throw new Error(
|
|
35
|
-
'ServiceFactory: Xrm object is required in production environment. ' +
|
|
36
|
-
'Please ensure this code is running within Dynamics 365 context.'
|
|
37
|
-
);
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
console.log(
|
|
41
|
-
'ServiceFactory: Running in production environment - using XrmApiService'
|
|
42
|
-
);
|
|
43
|
-
return new XrmApiService(Xrm);
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* Gets the current environment type as a string.
|
|
48
|
-
* @returns 'mock' or 'production'
|
|
49
|
-
*/
|
|
50
|
-
public static getEnvironmentType(): 'mock' | 'production' {
|
|
51
|
-
return this.isMockEnvironment ? 'mock' : 'production';
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* Checks if the code is running in a Dynamics 365 context.
|
|
56
|
-
* @returns True if Xrm global is available
|
|
57
|
-
*/
|
|
58
|
-
public static isDynamics365Context(): boolean {
|
|
59
|
-
return (
|
|
60
|
-
typeof window !== 'undefined' &&
|
|
61
|
-
Object.prototype.hasOwnProperty.call(window, 'Xrm') &&
|
|
62
|
-
(window as any).Xrm !== undefined
|
|
63
|
-
);
|
|
64
|
-
}
|
|
65
|
-
}
|
|
@@ -1,213 +0,0 @@
|
|
|
1
|
-
import { IApiService } from '@khester/dynamics-ui-api-client';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Implementation of IApiService that uses Dynamics 365's Xrm.WebApi
|
|
5
|
-
* for direct integration with the platform.
|
|
6
|
-
*/
|
|
7
|
-
export class XrmApiService implements IApiService {
|
|
8
|
-
private xrm: any;
|
|
9
|
-
|
|
10
|
-
constructor(xrm: any) {
|
|
11
|
-
if (!xrm || !xrm.WebApi) {
|
|
12
|
-
throw new Error('Valid Xrm object with WebApi is required');
|
|
13
|
-
}
|
|
14
|
-
this.xrm = xrm;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
async createRecord(entityName: string, record: any): Promise<any> {
|
|
18
|
-
try {
|
|
19
|
-
console.log(`XrmApiService: Creating ${entityName} record`, record);
|
|
20
|
-
|
|
21
|
-
// Remove any system fields that shouldn't be in create payload
|
|
22
|
-
const createPayload = { ...record };
|
|
23
|
-
delete createPayload.createdon;
|
|
24
|
-
delete createPayload.modifiedon;
|
|
25
|
-
delete createPayload.createdby;
|
|
26
|
-
delete createPayload.modifiedby;
|
|
27
|
-
|
|
28
|
-
const result = await this.xrm.WebApi.createRecord(
|
|
29
|
-
entityName,
|
|
30
|
-
createPayload
|
|
31
|
-
);
|
|
32
|
-
|
|
33
|
-
// Xrm.WebApi.createRecord returns an object with id property
|
|
34
|
-
// We need to fetch the full record to return it
|
|
35
|
-
const createdRecord = await this.xrm.WebApi.retrieveRecord(
|
|
36
|
-
entityName,
|
|
37
|
-
result.id
|
|
38
|
-
);
|
|
39
|
-
|
|
40
|
-
return createdRecord;
|
|
41
|
-
} catch (error) {
|
|
42
|
-
console.error(`XrmApiService: Error creating ${entityName}`, error);
|
|
43
|
-
throw error;
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
async updateRecord(
|
|
48
|
-
entityName: string,
|
|
49
|
-
id: string,
|
|
50
|
-
record: any
|
|
51
|
-
): Promise<any> {
|
|
52
|
-
try {
|
|
53
|
-
console.log(`XrmApiService: Updating ${entityName} record ${id}`, record);
|
|
54
|
-
|
|
55
|
-
// Remove any system fields and primary key from update payload
|
|
56
|
-
const updatePayload = { ...record };
|
|
57
|
-
const primaryKey = this.getPrimaryKeyName(entityName);
|
|
58
|
-
delete updatePayload[primaryKey];
|
|
59
|
-
delete updatePayload.createdon;
|
|
60
|
-
delete updatePayload.modifiedon;
|
|
61
|
-
delete updatePayload.createdby;
|
|
62
|
-
delete updatePayload.modifiedby;
|
|
63
|
-
|
|
64
|
-
await this.xrm.WebApi.updateRecord(entityName, id, updatePayload);
|
|
65
|
-
|
|
66
|
-
// Fetch and return the updated record
|
|
67
|
-
const updatedRecord = await this.xrm.WebApi.retrieveRecord(
|
|
68
|
-
entityName,
|
|
69
|
-
id
|
|
70
|
-
);
|
|
71
|
-
return updatedRecord;
|
|
72
|
-
} catch (error) {
|
|
73
|
-
console.error(`XrmApiService: Error updating ${entityName}`, error);
|
|
74
|
-
throw error;
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
async deleteRecord(entityName: string, id: string): Promise<void> {
|
|
79
|
-
try {
|
|
80
|
-
console.log(`XrmApiService: Deleting ${entityName} record ${id}`);
|
|
81
|
-
await this.xrm.WebApi.deleteRecord(entityName, id);
|
|
82
|
-
} catch (error) {
|
|
83
|
-
console.error(`XrmApiService: Error deleting ${entityName}`, error);
|
|
84
|
-
throw error;
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
async retrieveMultipleRecords(
|
|
89
|
-
entityName: string,
|
|
90
|
-
fetchXml: string
|
|
91
|
-
): Promise<{ entities: any[] }> {
|
|
92
|
-
try {
|
|
93
|
-
console.log(
|
|
94
|
-
`XrmApiService: Retrieving ${entityName} records with FetchXML`,
|
|
95
|
-
fetchXml
|
|
96
|
-
);
|
|
97
|
-
|
|
98
|
-
// Convert FetchXML to OData query if needed
|
|
99
|
-
// For now, we'll use Xrm.WebApi.retrieveMultipleRecords with options
|
|
100
|
-
const options = `?fetchXml=${encodeURIComponent(fetchXml)}`;
|
|
101
|
-
|
|
102
|
-
const result = await this.xrm.WebApi.retrieveMultipleRecords(
|
|
103
|
-
entityName,
|
|
104
|
-
options
|
|
105
|
-
);
|
|
106
|
-
|
|
107
|
-
return {
|
|
108
|
-
entities: result.entities || [],
|
|
109
|
-
};
|
|
110
|
-
} catch (error) {
|
|
111
|
-
console.error(
|
|
112
|
-
`XrmApiService: Error retrieving ${entityName} records`,
|
|
113
|
-
error
|
|
114
|
-
);
|
|
115
|
-
throw error;
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
async executeRequest(requestName: string, requestData: any): Promise<any> {
|
|
120
|
-
try {
|
|
121
|
-
console.log(
|
|
122
|
-
`XrmApiService: Executing request ${requestName}`,
|
|
123
|
-
requestData
|
|
124
|
-
);
|
|
125
|
-
|
|
126
|
-
// Xrm.WebApi.execute expects a request object with specific structure
|
|
127
|
-
const request = {
|
|
128
|
-
...requestData,
|
|
129
|
-
getMetadata: () => ({
|
|
130
|
-
boundParameter: null,
|
|
131
|
-
operationType: 0, // Action
|
|
132
|
-
operationName: requestName,
|
|
133
|
-
}),
|
|
134
|
-
};
|
|
135
|
-
|
|
136
|
-
const response = await this.xrm.WebApi.execute(request);
|
|
137
|
-
|
|
138
|
-
// Parse the response
|
|
139
|
-
if (response.ok) {
|
|
140
|
-
const responseData = await response.json();
|
|
141
|
-
return responseData;
|
|
142
|
-
} else {
|
|
143
|
-
throw new Error(`Request failed with status: ${response.status}`);
|
|
144
|
-
}
|
|
145
|
-
} catch (error) {
|
|
146
|
-
console.error(
|
|
147
|
-
`XrmApiService: Error executing request ${requestName}`,
|
|
148
|
-
error
|
|
149
|
-
);
|
|
150
|
-
throw error;
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
async uploadFile(file: File): Promise<string> {
|
|
155
|
-
try {
|
|
156
|
-
console.log(`XrmApiService: Uploading file ${file.name}`);
|
|
157
|
-
|
|
158
|
-
// File upload in Dynamics 365 typically involves:
|
|
159
|
-
// 1. Creating an annotation (note) record
|
|
160
|
-
// 2. Attaching the file content as base64
|
|
161
|
-
|
|
162
|
-
// Convert file to base64
|
|
163
|
-
const base64Content = await this.fileToBase64(file);
|
|
164
|
-
|
|
165
|
-
// Create annotation record
|
|
166
|
-
const annotation = {
|
|
167
|
-
subject: file.name,
|
|
168
|
-
filename: file.name,
|
|
169
|
-
mimetype: file.type,
|
|
170
|
-
documentbody: base64Content,
|
|
171
|
-
isdocument: true,
|
|
172
|
-
};
|
|
173
|
-
|
|
174
|
-
const result = await this.xrm.WebApi.createRecord(
|
|
175
|
-
'annotation',
|
|
176
|
-
annotation
|
|
177
|
-
);
|
|
178
|
-
|
|
179
|
-
// Return the annotation ID as the file reference
|
|
180
|
-
return result.id;
|
|
181
|
-
} catch (error) {
|
|
182
|
-
console.error(`XrmApiService: Error uploading file`, error);
|
|
183
|
-
throw error;
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
/**
|
|
188
|
-
* Helper method to convert File to base64 string
|
|
189
|
-
*/
|
|
190
|
-
private fileToBase64(file: File): Promise<string> {
|
|
191
|
-
return new Promise((resolve, reject) => {
|
|
192
|
-
const reader = new FileReader();
|
|
193
|
-
reader.readAsDataURL(file);
|
|
194
|
-
reader.onload = () => {
|
|
195
|
-
// Remove data URL prefix to get just the base64 content
|
|
196
|
-
const base64 = (reader.result as string).split(',')[1];
|
|
197
|
-
resolve(base64);
|
|
198
|
-
};
|
|
199
|
-
reader.onerror = (error) => reject(error);
|
|
200
|
-
});
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
/**
|
|
204
|
-
* Helper method to get the primary key name for an entity
|
|
205
|
-
*/
|
|
206
|
-
private getPrimaryKeyName(entityName: string): string {
|
|
207
|
-
// Remove trailing 's' if present and add 'id'
|
|
208
|
-
const singularName = entityName.endsWith('s')
|
|
209
|
-
? entityName.slice(0, -1)
|
|
210
|
-
: entityName;
|
|
211
|
-
return `${singularName}id`;
|
|
212
|
-
}
|
|
213
|
-
}
|
|
@@ -1,171 +0,0 @@
|
|
|
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__content {
|
|
32
|
-
display: flex;
|
|
33
|
-
justify-content: space-between;
|
|
34
|
-
align-items: center;
|
|
35
|
-
max-width: 1200px;
|
|
36
|
-
margin: 0 auto;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
.app-header__title h1 {
|
|
40
|
-
margin: 0 0 4px 0;
|
|
41
|
-
font-size: 24px;
|
|
42
|
-
font-weight: 600;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
.app-header__title p {
|
|
46
|
-
margin: 0;
|
|
47
|
-
font-size: 14px;
|
|
48
|
-
opacity: 0.9;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
.app-header__actions {
|
|
52
|
-
display: flex;
|
|
53
|
-
gap: 12px;
|
|
54
|
-
align-items: center;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
.debug-toggle-btn {
|
|
58
|
-
background-color: rgba(255, 255, 255, 0.1);
|
|
59
|
-
border: 1px solid rgba(255, 255, 255, 0.3);
|
|
60
|
-
color: white;
|
|
61
|
-
padding: 6px 12px;
|
|
62
|
-
border-radius: 4px;
|
|
63
|
-
cursor: pointer;
|
|
64
|
-
font-size: 12px;
|
|
65
|
-
transition: background-color 0.2s ease;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
.debug-toggle-btn:hover {
|
|
69
|
-
background-color: rgba(255, 255, 255, 0.2);
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
.app-navigation {
|
|
73
|
-
background-color: white;
|
|
74
|
-
border-bottom: 1px solid #edebe9;
|
|
75
|
-
max-width: 1200px;
|
|
76
|
-
margin: 0 auto;
|
|
77
|
-
margin-top: 16px;
|
|
78
|
-
border-radius: 8px 8px 0 0;
|
|
79
|
-
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
.app-main {
|
|
83
|
-
flex: 1;
|
|
84
|
-
padding: 0;
|
|
85
|
-
max-width: 1200px;
|
|
86
|
-
margin: 0 auto;
|
|
87
|
-
width: 100%;
|
|
88
|
-
background-color: white;
|
|
89
|
-
margin-bottom: 16px;
|
|
90
|
-
border-radius: 0 0 8px 8px;
|
|
91
|
-
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
.debug-panel-overlay {
|
|
95
|
-
position: fixed;
|
|
96
|
-
top: 0;
|
|
97
|
-
left: 0;
|
|
98
|
-
right: 0;
|
|
99
|
-
bottom: 0;
|
|
100
|
-
background-color: rgba(0, 0, 0, 0.5);
|
|
101
|
-
z-index: 1000;
|
|
102
|
-
display: flex;
|
|
103
|
-
justify-content: center;
|
|
104
|
-
align-items: center;
|
|
105
|
-
padding: 20px;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
.app-footer {
|
|
109
|
-
background-color: #f3f2f1;
|
|
110
|
-
border-top: 1px solid #edebe9;
|
|
111
|
-
padding: 12px 16px;
|
|
112
|
-
text-align: center;
|
|
113
|
-
color: #605e5c;
|
|
114
|
-
font-size: 12px;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
.app-footer p {
|
|
118
|
-
margin: 0;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
/* Dynamics 365 specific styles */
|
|
122
|
-
.ms-Panel {
|
|
123
|
-
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);
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
.ms-DetailsList {
|
|
127
|
-
border-radius: 0;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
.ms-DetailsHeader {
|
|
131
|
-
background-color: #f8f8f8;
|
|
132
|
-
border-bottom: 1px solid #edebe9;
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
.ms-DetailsRow:hover {
|
|
136
|
-
background-color: #f3f2f1;
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
.ms-DetailsRow.is-selected {
|
|
140
|
-
background-color: #deecf9;
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
@media (max-width: 768px) {
|
|
144
|
-
.app-header {
|
|
145
|
-
padding: 12px 16px;
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
.app-header__content {
|
|
149
|
-
flex-direction: column;
|
|
150
|
-
gap: 12px;
|
|
151
|
-
align-items: flex-start;
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
.app-header__title h1 {
|
|
155
|
-
font-size: 20px;
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
.app-navigation {
|
|
159
|
-
margin: 8px;
|
|
160
|
-
border-radius: 4px 4px 0 0;
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
.app-main {
|
|
164
|
-
margin: 0 8px 8px 8px;
|
|
165
|
-
border-radius: 0 0 4px 4px;
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
.debug-panel-overlay {
|
|
169
|
-
padding: 10px;
|
|
170
|
-
}
|
|
171
|
-
}
|