@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.
Files changed (107) hide show
  1. package/bin/create-dynamics-app.js +1 -1
  2. package/dist/index.js +140 -15
  3. package/dist/index.js.map +1 -1
  4. package/dist/utils/consultingHelpers.d.ts +13 -0
  5. package/dist/utils/consultingHelpers.d.ts.map +1 -0
  6. package/dist/utils/consultingHelpers.js +569 -0
  7. package/dist/utils/consultingHelpers.js.map +1 -0
  8. package/dist/utils/copyTemplate.d.ts.map +1 -1
  9. package/dist/utils/copyTemplate.js.map +1 -1
  10. package/dist/utils/initGit.d.ts.map +1 -1
  11. package/dist/utils/initGit.js.map +1 -1
  12. package/dist/utils/installDependencies.d.ts.map +1 -1
  13. package/dist/utils/installDependencies.js +3 -2
  14. package/dist/utils/installDependencies.js.map +1 -1
  15. package/dist/utils/updatePackageJson.d.ts +1 -1
  16. package/dist/utils/updatePackageJson.d.ts.map +1 -1
  17. package/dist/utils/updatePackageJson.js +11 -1
  18. package/dist/utils/updatePackageJson.js.map +1 -1
  19. package/package.json +1 -1
  20. package/templates/dynamics-365-starter/INTEGRATION_TEST_RESULTS.md +302 -0
  21. package/templates/dynamics-365-starter/PHASE_4_COMPLETION_SUMMARY.md +305 -0
  22. package/templates/dynamics-365-starter/README.md +566 -137
  23. package/templates/dynamics-365-starter/deployment/QUICKSTART-MAC.md +507 -0
  24. package/templates/dynamics-365-starter/deployment/QUICKSTART-WINDOWS.md +372 -0
  25. package/templates/dynamics-365-starter/deployment/README.md +484 -0
  26. package/templates/dynamics-365-starter/deployment/pipelines/README.md +375 -0
  27. package/templates/dynamics-365-starter/deployment/pipelines/azure-pipelines.yml +330 -0
  28. package/templates/dynamics-365-starter/deployment/pipelines/github-actions.yml +422 -0
  29. package/templates/dynamics-365-starter/deployment/pipelines/jenkins.groovy +636 -0
  30. package/templates/dynamics-365-starter/deployment/scripts/deploy.ps1 +417 -0
  31. package/templates/dynamics-365-starter/deployment/scripts/deploy.sh +582 -0
  32. package/templates/dynamics-365-starter/deployment/scripts/team-onboarding.ps1 +486 -0
  33. package/templates/dynamics-365-starter/deployment/scripts/team-onboarding.sh +567 -0
  34. package/templates/dynamics-365-starter/deployment/scripts/validate-setup.ps1 +703 -0
  35. package/templates/dynamics-365-starter/deployment/scripts/validate-setup.sh +671 -0
  36. package/templates/dynamics-365-starter/docs/ARCHITECTURE_OVERVIEW.md +506 -0
  37. package/templates/dynamics-365-starter/docs/BEST_PRACTICES.md +723 -0
  38. package/templates/dynamics-365-starter/docs/MIGRATION_GUIDE.md +447 -0
  39. package/templates/dynamics-365-starter/docs/team-standards/README.md +273 -0
  40. package/templates/dynamics-365-starter/docs/team-standards/client-onboarding.md +577 -0
  41. package/templates/dynamics-365-starter/docs/team-standards/code-review-checklist.md +359 -0
  42. package/templates/dynamics-365-starter/docs/team-standards/coding-standards.md +700 -0
  43. package/templates/dynamics-365-starter/docs/team-standards/cross-platform-team-guide.md +736 -0
  44. package/templates/dynamics-365-starter/docs/team-standards/development-workflows.md +727 -0
  45. package/templates/dynamics-365-starter/docs/troubleshooting/common-errors.md +758 -0
  46. package/templates/dynamics-365-starter/docs/troubleshooting/platform-specific-issues.md +878 -0
  47. package/templates/dynamics-365-starter/package.json +22 -1
  48. package/templates/dynamics-365-starter/public/index.html +8 -11
  49. package/templates/dynamics-365-starter/scripts/custom-build.js +255 -0
  50. package/templates/dynamics-365-starter/src/client-project-template/README.md +234 -0
  51. package/templates/dynamics-365-starter/src/client-project-template/config/client.template.json +114 -0
  52. package/templates/dynamics-365-starter/src/client-project-template/config/environments/template.json +186 -0
  53. package/templates/dynamics-365-starter/src/client-project-template/scripts/client-setup.js +667 -0
  54. package/templates/dynamics-365-starter/src/components/AccountForm.css +71 -0
  55. package/templates/dynamics-365-starter/src/components/AccountForm.tsx +541 -0
  56. package/templates/dynamics-365-starter/src/components/AccountManagement.css +86 -0
  57. package/templates/dynamics-365-starter/src/components/AccountManagement.tsx +370 -0
  58. package/templates/dynamics-365-starter/src/components/ContactForm.tsx +149 -63
  59. package/templates/dynamics-365-starter/src/components/ContactManagement.tsx +153 -63
  60. package/templates/dynamics-365-starter/src/components/Logging/LogDialog.tsx +291 -0
  61. package/templates/dynamics-365-starter/src/components/Logging/LoggingContext.tsx +166 -0
  62. package/templates/dynamics-365-starter/src/components/Logging/LoggingDebugPanel.css +192 -0
  63. package/templates/dynamics-365-starter/src/components/Logging/LoggingDebugPanel.tsx +177 -0
  64. package/templates/dynamics-365-starter/src/components/Logging/LoggingProvider.tsx +3 -0
  65. package/templates/dynamics-365-starter/src/components/Logging/logger.ts +193 -0
  66. package/templates/dynamics-365-starter/src/constants/account.ts +410 -0
  67. package/templates/dynamics-365-starter/src/constants/contact.ts +362 -0
  68. package/templates/dynamics-365-starter/src/examples/README.md +52 -0
  69. package/templates/dynamics-365-starter/src/examples/component-examples/opportunity-management.tsx +625 -0
  70. package/templates/dynamics-365-starter/src/examples/entity-examples/opportunity-model.ts +545 -0
  71. package/templates/dynamics-365-starter/src/examples/integration-examples/custom-pcf-wrapper.tsx +722 -0
  72. package/templates/dynamics-365-starter/src/examples/workflow-examples/sales-workflow.ts +662 -0
  73. package/templates/dynamics-365-starter/src/index.tsx +107 -19
  74. package/templates/dynamics-365-starter/src/models/Account.ts +480 -0
  75. package/templates/dynamics-365-starter/src/models/BaseEntity.ts +204 -0
  76. package/templates/dynamics-365-starter/src/models/Contact.ts +580 -0
  77. package/templates/dynamics-365-starter/src/page-templates/EntityDashboard.tsx +519 -0
  78. package/templates/dynamics-365-starter/src/page-templates/EntityDetailPage.tsx +456 -0
  79. package/templates/dynamics-365-starter/src/page-templates/EntityListPage.tsx +406 -0
  80. package/templates/dynamics-365-starter/src/page-templates/RelatedEntitiesPage.tsx +578 -0
  81. package/templates/dynamics-365-starter/src/page-templates/SearchPage.tsx +629 -0
  82. package/templates/dynamics-365-starter/src/pcf/ContactControlWrapper.tsx +75 -22
  83. package/templates/dynamics-365-starter/src/pcf/MultiEntityControlWrapper.tsx +205 -0
  84. package/templates/dynamics-365-starter/src/providers/DynamicsProvider.tsx +297 -80
  85. package/templates/dynamics-365-starter/src/services/MockApiService.ts +260 -0
  86. package/templates/dynamics-365-starter/src/services/ServiceFactory.ts +65 -0
  87. package/templates/dynamics-365-starter/src/services/XrmApiService.ts +213 -0
  88. package/templates/dynamics-365-starter/src/styles/index.css +74 -7
  89. package/templates/dynamics-365-starter/tools/entity-generator/index.js +168 -0
  90. package/templates/dynamics-365-starter/tools/entity-generator/templates/constants.template.ts +124 -0
  91. package/templates/dynamics-365-starter/tools/entity-generator/templates/form.template.css +283 -0
  92. package/templates/dynamics-365-starter/tools/entity-generator/templates/form.template.tsx +275 -0
  93. package/templates/dynamics-365-starter/tools/entity-generator/templates/management.template.css +204 -0
  94. package/templates/dynamics-365-starter/tools/entity-generator/templates/management.template.tsx +413 -0
  95. package/templates/dynamics-365-starter/tools/entity-generator/templates/model.template.ts +250 -0
  96. package/templates/dynamics-365-starter/tools/metadata-sync/d365-client.js +410 -0
  97. package/templates/dynamics-365-starter/tools/metadata-sync/index.js +512 -0
  98. package/templates/dynamics-365-starter/tools/metadata-sync/type-generator.js +675 -0
  99. package/templates/dynamics-365-starter/tsconfig.json +11 -8
  100. package/templates/dynamics-365-starter/webpack.config.js +8 -9
  101. package/templates/power-pages-starter/README.md +7 -1
  102. package/templates/power-pages-starter/public/index.html +8 -11
  103. package/templates/power-pages-starter/src/components/ContactForm.tsx +60 -41
  104. package/templates/power-pages-starter/src/index.tsx +3 -3
  105. package/templates/power-pages-starter/src/providers/PowerPagesProvider.tsx +46 -23
  106. package/templates/power-pages-starter/tsconfig.json +3 -9
  107. package/templates/power-pages-starter/webpack.config.js +8 -3
@@ -0,0 +1,629 @@
1
+ import React, { useState, useCallback } from 'react';
2
+ import {
3
+ Stack,
4
+ Text,
5
+ SearchBox,
6
+ Pivot,
7
+ PivotItem,
8
+ DetailsList,
9
+ IColumn,
10
+ SelectionMode,
11
+ MessageBar,
12
+ MessageBarType,
13
+ Spinner,
14
+ SpinnerSize,
15
+ CommandBar,
16
+ ICommandBarItemProps,
17
+ Dropdown,
18
+ IDropdownOption,
19
+ Checkbox,
20
+ DefaultButton,
21
+ Facepile,
22
+ IFacepilePersona,
23
+ } from '@fluentui/react';
24
+ import { BaseEntity } from '../models/BaseEntity';
25
+ import { useDynamicsApi } from '../providers/DynamicsProvider';
26
+ import { Logger } from '../components/Logging/logger';
27
+
28
+ interface EntitySearchConfig<T extends BaseEntity> {
29
+ key: string;
30
+ title: string;
31
+ entityLogicalName: string;
32
+ entityClass: any;
33
+ columns: IColumn[];
34
+ searchFields: string[];
35
+ fetchXmlTemplate: string; // Should include {{searchQuery}} placeholder
36
+ icon?: string;
37
+ color?: string;
38
+ }
39
+
40
+ interface SearchFilter {
41
+ key: string;
42
+ title: string;
43
+ type: 'dropdown' | 'checkbox' | 'date';
44
+ options?: IDropdownOption[];
45
+ value?: any;
46
+ }
47
+
48
+ interface SearchPageProps {
49
+ title?: string;
50
+ entityConfigs: EntitySearchConfig<any>[];
51
+ globalSearchFields?: string[];
52
+ filters?: SearchFilter[];
53
+ onItemSelected?: (item: any, entityType: string) => void;
54
+ onCreateNew?: (entityType: string) => void;
55
+ enableAdvancedSearch?: boolean;
56
+ enableSavedSearches?: boolean;
57
+ placeholder?: string;
58
+ minSearchLength?: number;
59
+ }
60
+
61
+ /**
62
+ * Generic search page template for Dynamics 365
63
+ * Supports cross-entity search with advanced filtering and saved searches
64
+ */
65
+ export function SearchPage({
66
+ title = 'Search',
67
+ entityConfigs,
68
+ globalSearchFields = [],
69
+ filters = [],
70
+ onItemSelected,
71
+ onCreateNew,
72
+ enableAdvancedSearch = true,
73
+ enableSavedSearches = false,
74
+ placeholder = 'Search across all entities...',
75
+ minSearchLength = 3,
76
+ }: SearchPageProps) {
77
+ const { apiService } = useDynamicsApi();
78
+ const [searchQuery, setSearchQuery] = useState('');
79
+ const [searchResults, setSearchResults] = useState<{ [key: string]: any[] }>(
80
+ {}
81
+ );
82
+ const [loading, setLoading] = useState(false);
83
+ const [error, setError] = useState<string | null>(null);
84
+ const [selectedTab, setSelectedTab] = useState('all');
85
+ const [activeFilters, setActiveFilters] = useState<{ [key: string]: any }>(
86
+ {}
87
+ );
88
+ const [showAdvancedSearch, setShowAdvancedSearch] = useState(false);
89
+ const [searchHistory, setSearchHistory] = useState<string[]>([]);
90
+ const [totalResults, setTotalResults] = useState(0);
91
+
92
+ // Perform search across all configured entities
93
+ const performSearch = useCallback(
94
+ async (query: string) => {
95
+ if (!query || query.length < minSearchLength) {
96
+ setSearchResults({});
97
+ setTotalResults(0);
98
+ return;
99
+ }
100
+
101
+ if (!apiService) {
102
+ setError('API service not available');
103
+ return;
104
+ }
105
+
106
+ try {
107
+ setLoading(true);
108
+ setError(null);
109
+ Logger.log(`Searching for: "${query}"`);
110
+
111
+ const results: { [key: string]: any[] } = {};
112
+ let total = 0;
113
+
114
+ // Search each entity type
115
+ for (const config of entityConfigs) {
116
+ try {
117
+ // Build search conditions
118
+ const searchConditions = config.searchFields
119
+ .map(
120
+ (field) =>
121
+ `<condition attribute="${field}" operator="like" value="%${query}%" />`
122
+ )
123
+ .join('\n ');
124
+
125
+ // Apply additional filters
126
+ const filterConditions = Object.entries(activeFilters)
127
+ .filter(
128
+ ([_, value]) =>
129
+ value !== null && value !== undefined && value !== ''
130
+ )
131
+ .map(([key, value]) => {
132
+ const filter = filters.find((f) => f.key === key);
133
+ if (filter?.type === 'checkbox') {
134
+ return value
135
+ ? `<condition attribute="${key}" operator="eq" value="true" />`
136
+ : '';
137
+ } else if (filter?.type === 'dropdown') {
138
+ return `<condition attribute="${key}" operator="eq" value="${value}" />`;
139
+ }
140
+ return `<condition attribute="${key}" operator="like" value="%${value}%" />`;
141
+ })
142
+ .filter((condition) => condition)
143
+ .join('\n ');
144
+
145
+ const allConditions = [searchConditions, filterConditions]
146
+ .filter((c) => c)
147
+ .join('\n ');
148
+
149
+ const fetchXml = config.fetchXmlTemplate
150
+ .replace('{{searchQuery}}', query)
151
+ .replace('{{searchConditions}}', allConditions);
152
+
153
+ const result = await apiService.retrieveMultipleRecords(
154
+ config.entityLogicalName,
155
+ fetchXml
156
+ );
157
+ const entities = (result.entities || []).map(
158
+ (data: any) => new config.entityClass(data)
159
+ );
160
+
161
+ results[config.key] = entities;
162
+ total += entities.length;
163
+
164
+ Logger.log(`Found ${entities.length} ${config.title} results`);
165
+ } catch (err) {
166
+ Logger.warn(
167
+ `Search failed for ${config.title}:`,
168
+ err instanceof Error ? err.message : String(err)
169
+ );
170
+ results[config.key] = [];
171
+ }
172
+ }
173
+
174
+ setSearchResults(results);
175
+ setTotalResults(total);
176
+
177
+ // Update search history
178
+ if (!searchHistory.includes(query)) {
179
+ setSearchHistory((prev) => [query, ...prev.slice(0, 9)]); // Keep last 10 searches
180
+ }
181
+
182
+ Logger.log(`Search completed. Total results: ${total}`);
183
+ } catch (err) {
184
+ const errorMessage =
185
+ err instanceof Error ? err.message : 'Search failed';
186
+ setError(errorMessage);
187
+ Logger.error('Search error:', 'SearchPage', err);
188
+ } finally {
189
+ setLoading(false);
190
+ }
191
+ },
192
+ [
193
+ apiService,
194
+ entityConfigs,
195
+ activeFilters,
196
+ filters,
197
+ minSearchLength,
198
+ searchHistory,
199
+ ]
200
+ );
201
+
202
+ // Handle search input
203
+ const handleSearch = (query: string) => {
204
+ setSearchQuery(query);
205
+ if (query.length >= minSearchLength) {
206
+ performSearch(query);
207
+ } else {
208
+ setSearchResults({});
209
+ setTotalResults(0);
210
+ }
211
+ };
212
+
213
+ // Handle filter change
214
+ const handleFilterChange = (filterKey: string, value: any) => {
215
+ setActiveFilters((prev) => ({
216
+ ...prev,
217
+ [filterKey]: value,
218
+ }));
219
+
220
+ // Re-search with new filters
221
+ if (searchQuery.length >= minSearchLength) {
222
+ performSearch(searchQuery);
223
+ }
224
+ };
225
+
226
+ // Clear all filters
227
+ const clearFilters = () => {
228
+ setActiveFilters({});
229
+ if (searchQuery.length >= minSearchLength) {
230
+ performSearch(searchQuery);
231
+ }
232
+ };
233
+
234
+ // Handle item selection
235
+ const handleItemClick = (
236
+ item: any,
237
+ entityConfig: EntitySearchConfig<any>
238
+ ) => {
239
+ if (onItemSelected) {
240
+ onItemSelected(item, entityConfig.key);
241
+ }
242
+ };
243
+
244
+ // Get all results for "All" tab
245
+ const getAllResults = () => {
246
+ const allResults: any[] = [];
247
+ entityConfigs.forEach((config) => {
248
+ const configResults = searchResults[config.key] || [];
249
+ configResults.forEach((item) => {
250
+ allResults.push({
251
+ ...item,
252
+ _entityType: config.key,
253
+ _entityTitle: config.title,
254
+ _entityIcon: config.icon,
255
+ _entityColor: config.color,
256
+ });
257
+ });
258
+ });
259
+ return allResults;
260
+ };
261
+
262
+ // Create columns for "All" tab
263
+ const createAllResultsColumns = (): IColumn[] => [
264
+ {
265
+ key: 'name',
266
+ name: 'Name',
267
+ minWidth: 200,
268
+ maxWidth: 300,
269
+ isResizable: true,
270
+ onRender: (item: any) => {
271
+ const config = entityConfigs.find((c) => c.key === item._entityType);
272
+ const nameField = config?.columns[0]?.fieldName || 'name';
273
+ return (
274
+ <Stack horizontal verticalAlign="center" tokens={{ childrenGap: 8 }}>
275
+ {item._entityIcon && (
276
+ <span
277
+ style={{ fontSize: 16, color: item._entityColor || '#605e5c' }}
278
+ >
279
+ {item._entityIcon}
280
+ </span>
281
+ )}
282
+ <span
283
+ style={{ color: '#0078d4', cursor: 'pointer', fontWeight: 500 }}
284
+ onClick={() => config && handleItemClick(item, config)}
285
+ >
286
+ {item[nameField] || 'N/A'}
287
+ </span>
288
+ </Stack>
289
+ );
290
+ },
291
+ },
292
+ {
293
+ key: 'entityType',
294
+ name: 'Type',
295
+ minWidth: 100,
296
+ maxWidth: 150,
297
+ isResizable: true,
298
+ onRender: (item: any) => (
299
+ <span
300
+ style={{
301
+ padding: '4px 8px',
302
+ borderRadius: 4,
303
+ backgroundColor: item._entityColor
304
+ ? `${item._entityColor}20`
305
+ : '#f3f2f1',
306
+ color: item._entityColor || '#605e5c',
307
+ fontSize: 12,
308
+ fontWeight: 600,
309
+ }}
310
+ >
311
+ {item._entityTitle}
312
+ </span>
313
+ ),
314
+ },
315
+ ];
316
+
317
+ // Enhanced columns with click handlers
318
+ const createEnhancedColumns = (
319
+ config: EntitySearchConfig<any>
320
+ ): IColumn[] => {
321
+ return config.columns.map((column) => ({
322
+ ...column,
323
+ onRender:
324
+ column.onRender ||
325
+ ((item: any) => {
326
+ const value = item[column.fieldName || ''];
327
+ if (column.key === config.columns[0].key) {
328
+ // Make first column clickable
329
+ return (
330
+ <span
331
+ style={{ color: '#0078d4', cursor: 'pointer', fontWeight: 500 }}
332
+ onClick={() => handleItemClick(item, config)}
333
+ >
334
+ {value || ''}
335
+ </span>
336
+ );
337
+ }
338
+ return <span>{value || ''}</span>;
339
+ }),
340
+ }));
341
+ };
342
+
343
+ // Command bar items
344
+ const commandBarItems: ICommandBarItemProps[] = [
345
+ ...(enableAdvancedSearch
346
+ ? [
347
+ {
348
+ key: 'advanced',
349
+ text: showAdvancedSearch ? 'Hide Filters' : 'Show Filters',
350
+ iconProps: { iconName: 'Filter' },
351
+ onClick: () => setShowAdvancedSearch(!showAdvancedSearch),
352
+ },
353
+ ]
354
+ : []),
355
+ ...(Object.keys(activeFilters).length > 0
356
+ ? [
357
+ {
358
+ key: 'clear',
359
+ text: 'Clear Filters',
360
+ iconProps: { iconName: 'ClearFilter' },
361
+ onClick: clearFilters,
362
+ },
363
+ ]
364
+ : []),
365
+ ...(onCreateNew
366
+ ? [
367
+ {
368
+ key: 'create',
369
+ text: 'Create New',
370
+ iconProps: { iconName: 'Add' },
371
+ subMenuProps: {
372
+ items: entityConfigs.map((config) => ({
373
+ key: config.key,
374
+ text: config.title,
375
+ iconProps: { iconName: 'Add' },
376
+ onClick: () => onCreateNew(config.key),
377
+ })),
378
+ },
379
+ },
380
+ ]
381
+ : []),
382
+ ];
383
+
384
+ // Get search suggestions
385
+ const getSearchSuggestions = (): IFacepilePersona[] => {
386
+ return searchHistory.slice(0, 5).map((query, index) => ({
387
+ personaName: query,
388
+ onClick: () => handleSearch(query),
389
+ }));
390
+ };
391
+
392
+ return (
393
+ <div style={{ padding: '20px', maxWidth: '100%' }}>
394
+ <Stack tokens={{ childrenGap: 20 }}>
395
+ {/* Header */}
396
+ <Stack>
397
+ <Text variant="xxLarge" style={{ fontWeight: 600, color: '#323130' }}>
398
+ {title}
399
+ </Text>
400
+ <Text variant="medium" style={{ color: '#605e5c' }}>
401
+ Search across multiple entity types
402
+ </Text>
403
+ </Stack>
404
+
405
+ {/* Search Box */}
406
+ <Stack tokens={{ childrenGap: 12 }}>
407
+ <SearchBox
408
+ placeholder={placeholder}
409
+ value={searchQuery}
410
+ onChange={(_, newValue) => handleSearch(newValue || '')}
411
+ onSearch={(newValue) => handleSearch(newValue)}
412
+ style={{ maxWidth: 600 }}
413
+ />
414
+
415
+ {/* Search History */}
416
+ {searchHistory.length > 0 && (
417
+ <Stack>
418
+ <Text
419
+ variant="small"
420
+ style={{ color: '#605e5c', marginBottom: 8 }}
421
+ >
422
+ Recent searches:
423
+ </Text>
424
+ <Facepile
425
+ personas={getSearchSuggestions()}
426
+ maxDisplayablePersonas={5}
427
+ styles={{
428
+ member: { cursor: 'pointer' },
429
+ }}
430
+ />
431
+ </Stack>
432
+ )}
433
+ </Stack>
434
+
435
+ {/* Advanced Search Filters */}
436
+ {enableAdvancedSearch && showAdvancedSearch && filters.length > 0 && (
437
+ <Stack
438
+ style={{
439
+ padding: 16,
440
+ border: '1px solid #edebe9',
441
+ borderRadius: 4,
442
+ backgroundColor: '#faf9f8',
443
+ }}
444
+ tokens={{ childrenGap: 16 }}
445
+ >
446
+ <Text variant="medium" style={{ fontWeight: 600 }}>
447
+ Advanced Filters
448
+ </Text>
449
+ <Stack horizontal wrap tokens={{ childrenGap: 16 }}>
450
+ {filters.map((filter) => (
451
+ <Stack key={filter.key} style={{ minWidth: 200 }}>
452
+ {filter.type === 'dropdown' && (
453
+ <Dropdown
454
+ label={filter.title}
455
+ options={filter.options || []}
456
+ selectedKey={activeFilters[filter.key]}
457
+ onChange={(_, option) =>
458
+ handleFilterChange(filter.key, option?.key)
459
+ }
460
+ />
461
+ )}
462
+ {filter.type === 'checkbox' && (
463
+ <Checkbox
464
+ label={filter.title}
465
+ checked={activeFilters[filter.key] || false}
466
+ onChange={(_, checked) =>
467
+ handleFilterChange(filter.key, checked)
468
+ }
469
+ />
470
+ )}
471
+ </Stack>
472
+ ))}
473
+ <DefaultButton
474
+ text="Clear Filters"
475
+ onClick={clearFilters}
476
+ disabled={Object.keys(activeFilters).length === 0}
477
+ style={{ alignSelf: 'end' }}
478
+ />
479
+ </Stack>
480
+ </Stack>
481
+ )}
482
+
483
+ {/* Error Message */}
484
+ {error && (
485
+ <MessageBar
486
+ messageBarType={MessageBarType.error}
487
+ onDismiss={() => setError(null)}
488
+ >
489
+ {error}
490
+ </MessageBar>
491
+ )}
492
+
493
+ {/* Command Bar */}
494
+ <CommandBar items={commandBarItems} />
495
+
496
+ {/* Results */}
497
+ {searchQuery.length > 0 && searchQuery.length < minSearchLength && (
498
+ <MessageBar messageBarType={MessageBarType.info}>
499
+ Please enter at least {minSearchLength} characters to search
500
+ </MessageBar>
501
+ )}
502
+
503
+ {loading ? (
504
+ <div
505
+ style={{
506
+ display: 'flex',
507
+ justifyContent: 'center',
508
+ alignItems: 'center',
509
+ minHeight: 200,
510
+ flexDirection: 'column',
511
+ }}
512
+ >
513
+ <Spinner size={SpinnerSize.large} label="Searching..." />
514
+ </div>
515
+ ) : totalResults > 0 ? (
516
+ <Stack>
517
+ <Text
518
+ variant="medium"
519
+ style={{ color: '#605e5c', marginBottom: 16 }}
520
+ >
521
+ Found {totalResults} results for "{searchQuery}"
522
+ </Text>
523
+
524
+ <Pivot
525
+ selectedKey={selectedTab}
526
+ onLinkClick={(item) =>
527
+ setSelectedTab(item?.props.itemKey || 'all')
528
+ }
529
+ >
530
+ {/* All Results Tab */}
531
+ <PivotItem headerText={`All (${totalResults})`} itemKey="all">
532
+ <div style={{ paddingTop: 16 }}>
533
+ <DetailsList
534
+ items={getAllResults()}
535
+ columns={createAllResultsColumns()}
536
+ setKey="all"
537
+ layoutMode={0}
538
+ selectionMode={SelectionMode.none}
539
+ isHeaderVisible={true}
540
+ />
541
+ </div>
542
+ </PivotItem>
543
+
544
+ {/* Individual Entity Tabs */}
545
+ {entityConfigs.map((config) => {
546
+ const results = searchResults[config.key] || [];
547
+ return (
548
+ <PivotItem
549
+ key={config.key}
550
+ headerText={`${config.title} (${results.length})`}
551
+ itemKey={config.key}
552
+ >
553
+ <div style={{ paddingTop: 16 }}>
554
+ {results.length === 0 ? (
555
+ <div
556
+ style={{
557
+ textAlign: 'center',
558
+ padding: '40px 20px',
559
+ color: '#605e5c',
560
+ }}
561
+ >
562
+ <Text variant="medium">No {config.title} found</Text>
563
+ </div>
564
+ ) : (
565
+ <DetailsList
566
+ items={results}
567
+ columns={createEnhancedColumns(config)}
568
+ setKey={config.key}
569
+ layoutMode={0}
570
+ selectionMode={SelectionMode.none}
571
+ isHeaderVisible={true}
572
+ />
573
+ )}
574
+ </div>
575
+ </PivotItem>
576
+ );
577
+ })}
578
+ </Pivot>
579
+ </Stack>
580
+ ) : searchQuery.length >= minSearchLength ? (
581
+ <div
582
+ style={{
583
+ textAlign: 'center',
584
+ padding: '40px 20px',
585
+ color: '#605e5c',
586
+ }}
587
+ >
588
+ <div style={{ fontSize: 48, marginBottom: 16, color: '#a19f9d' }}>
589
+ 🔍
590
+ </div>
591
+ <Text
592
+ variant="large"
593
+ style={{ fontWeight: 600, marginBottom: 8, color: '#323130' }}
594
+ >
595
+ No results found
596
+ </Text>
597
+ <Text variant="medium">
598
+ Try adjusting your search terms or filters
599
+ </Text>
600
+ </div>
601
+ ) : null}
602
+ </Stack>
603
+ </div>
604
+ );
605
+ }
606
+
607
+ // Helper function to create search FetchXML template
608
+ export function createSearchFetchXml(
609
+ entityName: string,
610
+ attributes: string[],
611
+ searchFields: string[],
612
+ orderBy?: string
613
+ ): string {
614
+ const attributeElements = attributes
615
+ .map((attr) => `<attribute name="${attr}" />`)
616
+ .join('\n ');
617
+ const orderElement = orderBy ? `<order attribute="${orderBy}" />` : '';
618
+
619
+ return `
620
+ <fetch top="50">
621
+ <entity name="${entityName}">
622
+ ${attributeElements}
623
+ ${orderElement}
624
+ <filter type="or">
625
+ {{searchConditions}}
626
+ </filter>
627
+ </entity>
628
+ </fetch>`;
629
+ }