@khester/create-dynamics-app 2.1.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.
Files changed (121) hide show
  1. package/dist/artifacts/registry.d.ts +4 -3
  2. package/dist/artifacts/registry.d.ts.map +1 -1
  3. package/dist/artifacts/registry.js +121 -11
  4. package/dist/artifacts/registry.js.map +1 -1
  5. package/dist/artifacts/types.d.ts +1 -1
  6. package/dist/artifacts/types.d.ts.map +1 -1
  7. package/dist/index.js +2 -1
  8. package/dist/index.js.map +1 -1
  9. package/dist/injectDevTools.d.ts.map +1 -1
  10. package/dist/injectDevTools.js +4 -2
  11. package/dist/injectDevTools.js.map +1 -1
  12. package/dist/scaffold.d.ts +1 -0
  13. package/dist/scaffold.d.ts.map +1 -1
  14. package/dist/scaffold.js +3 -1
  15. package/dist/scaffold.js.map +1 -1
  16. package/package.json +3 -2
  17. package/templates/grid-starter/ARCHITECTURE.md +66 -0
  18. package/templates/grid-starter/README.md +122 -0
  19. package/templates/grid-starter/env.example +16 -0
  20. package/templates/grid-starter/gitignore +6 -0
  21. package/templates/grid-starter/index.html +16 -0
  22. package/templates/grid-starter/package.json +39 -0
  23. package/templates/grid-starter/src/App.tsx +23 -0
  24. package/templates/grid-starter/src/core/services/FetchApiService.ts +117 -0
  25. package/templates/grid-starter/src/core/services/IApiService.ts +37 -0
  26. package/templates/grid-starter/src/core/services/MockApiService.ts +72 -0
  27. package/templates/grid-starter/src/core/services/ServiceFactory.ts +58 -0
  28. package/templates/grid-starter/src/core/services/XrmApiService.ts +135 -0
  29. package/templates/grid-starter/src/core/services/crudLogging.ts +52 -0
  30. package/templates/grid-starter/src/dev-tools/DevPanel.tsx +239 -0
  31. package/templates/grid-starter/src/grid/GridPage.tsx +119 -0
  32. package/templates/grid-starter/src/index.tsx +18 -0
  33. package/templates/grid-starter/src/vite-env.d.ts +15 -0
  34. package/templates/grid-starter/tools/deploy/deploy-webresource.cjs +117 -0
  35. package/templates/grid-starter/tsconfig.json +19 -0
  36. package/templates/grid-starter/vite.config.ts +76 -0
  37. package/templates/pcf-field/_variants/ValueInput.boolean.tsx +2 -0
  38. package/templates/pcf-field/_variants/ValueInput.date.tsx +2 -0
  39. package/templates/pcf-field/_variants/ValueInput.number.tsx +2 -0
  40. package/templates/pcf-field/_variants/ValueInput.optionset.tsx +77 -0
  41. package/templates/pcf-field/_variants/ValueInput.text.tsx +2 -0
  42. package/templates/pcf-field/index.ts +1 -1
  43. package/templates/pcf-field/package.json +3 -1
  44. package/templates/pcf-field/{{componentName}}Component.tsx +2 -0
  45. package/templates/react-custom-page/ARCHITECTURE.md +75 -0
  46. package/templates/react-custom-page/README.md +74 -568
  47. package/templates/react-custom-page/env.example +16 -0
  48. package/templates/react-custom-page/gitignore +1 -0
  49. package/templates/react-custom-page/index.html +16 -0
  50. package/templates/react-custom-page/package.json +21 -49
  51. package/templates/react-custom-page/src/App.tsx +26 -0
  52. package/templates/react-custom-page/src/core/recordContext.test.ts +30 -0
  53. package/templates/react-custom-page/src/core/recordContext.ts +51 -0
  54. package/templates/react-custom-page/src/core/services/FetchApiService.ts +117 -0
  55. package/templates/react-custom-page/src/core/services/IApiService.ts +37 -0
  56. package/templates/react-custom-page/src/core/services/MockApiService.ts +73 -0
  57. package/templates/react-custom-page/src/core/services/ServiceFactory.ts +58 -0
  58. package/templates/react-custom-page/src/core/services/XrmApiService.ts +135 -0
  59. package/templates/react-custom-page/src/core/services/crudLogging.ts +52 -0
  60. package/templates/react-custom-page/src/dev-tools/DevPanel.tsx +238 -0
  61. package/templates/react-custom-page/src/domain/diff.test.ts +87 -0
  62. package/templates/react-custom-page/src/domain/diff.ts +38 -0
  63. package/templates/react-custom-page/src/example/ExamplePage.tsx +140 -0
  64. package/templates/react-custom-page/src/example/exampleError.ts +36 -0
  65. package/templates/react-custom-page/src/example/hooks/useExampleData.ts +40 -0
  66. package/templates/react-custom-page/src/example/hooks/useExampleForm.ts +99 -0
  67. package/templates/react-custom-page/src/example/mappers/accountMapper.test.ts +38 -0
  68. package/templates/react-custom-page/src/example/mappers/accountMapper.ts +55 -0
  69. package/templates/react-custom-page/src/example/models/Account.ts +74 -0
  70. package/templates/react-custom-page/src/index.tsx +18 -128
  71. package/templates/react-custom-page/src/vite-env.d.ts +15 -0
  72. package/templates/react-custom-page/tools/deploy/deploy-webresource.cjs +117 -0
  73. package/templates/react-custom-page/tsconfig.json +12 -22
  74. package/templates/react-custom-page/vite.config.ts +76 -0
  75. package/templates/starter-page/README.md +38 -0
  76. package/templates/starter-page/_variants/App.dashboard.v8.tsx +46 -0
  77. package/templates/starter-page/_variants/App.form.v8.tsx +59 -0
  78. package/templates/starter-page/_variants/App.master-detail.v8.tsx +61 -0
  79. package/templates/starter-page/_variants/App.panel.v8.tsx +99 -0
  80. package/templates/starter-page/gitignore +5 -0
  81. package/templates/starter-page/package.json +27 -0
  82. package/templates/starter-page/public/index.html +11 -0
  83. package/templates/starter-page/src/index.tsx +10 -0
  84. package/templates/starter-page/src/services/dataverse.ts +30 -0
  85. package/templates/starter-page/tsconfig.json +15 -0
  86. package/templates/starter-page/webpack.config.js +17 -0
  87. package/templates/react-custom-page/deployment/README.md +0 -484
  88. package/templates/react-custom-page/docs/ARCHITECTURE_OVERVIEW.md +0 -506
  89. package/templates/react-custom-page/docs/BEST_PRACTICES.md +0 -723
  90. package/templates/react-custom-page/docs/MIGRATION_GUIDE.md +0 -447
  91. package/templates/react-custom-page/public/index.html +0 -15
  92. package/templates/react-custom-page/scripts/custom-build.js +0 -255
  93. package/templates/react-custom-page/src/components/AccountForm.css +0 -71
  94. package/templates/react-custom-page/src/components/AccountForm.tsx +0 -541
  95. package/templates/react-custom-page/src/components/AccountManagement.css +0 -86
  96. package/templates/react-custom-page/src/components/AccountManagement.tsx +0 -370
  97. package/templates/react-custom-page/src/components/ContactForm.css +0 -48
  98. package/templates/react-custom-page/src/components/ContactForm.tsx +0 -327
  99. package/templates/react-custom-page/src/components/ContactManagement.css +0 -86
  100. package/templates/react-custom-page/src/components/ContactManagement.tsx +0 -357
  101. package/templates/react-custom-page/src/components/Logging/LogDialog.tsx +0 -291
  102. package/templates/react-custom-page/src/components/Logging/LoggingContext.tsx +0 -166
  103. package/templates/react-custom-page/src/components/Logging/LoggingDebugPanel.css +0 -192
  104. package/templates/react-custom-page/src/components/Logging/LoggingDebugPanel.tsx +0 -177
  105. package/templates/react-custom-page/src/components/Logging/LoggingProvider.tsx +0 -3
  106. package/templates/react-custom-page/src/components/Logging/logger.ts +0 -193
  107. package/templates/react-custom-page/src/constants/account.ts +0 -410
  108. package/templates/react-custom-page/src/constants/contact.ts +0 -362
  109. package/templates/react-custom-page/src/models/Account.ts +0 -480
  110. package/templates/react-custom-page/src/models/BaseEntity.ts +0 -204
  111. package/templates/react-custom-page/src/models/Contact.ts +0 -580
  112. package/templates/react-custom-page/src/pcf/ContactControlWrapper.tsx +0 -107
  113. package/templates/react-custom-page/src/pcf/MultiEntityControlWrapper.tsx +0 -205
  114. package/templates/react-custom-page/src/providers/DynamicsProvider.tsx +0 -353
  115. package/templates/react-custom-page/src/services/MockApiService.ts +0 -260
  116. package/templates/react-custom-page/src/services/ServiceFactory.ts +0 -65
  117. package/templates/react-custom-page/src/services/XrmApiService.ts +0 -213
  118. package/templates/react-custom-page/src/styles/index.css +0 -171
  119. package/templates/react-custom-page/tools/metadata-sync/index.js +0 -152
  120. package/templates/react-custom-page/webpack.config.js +0 -57
  121. /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
- }