@khester/dynamics-cell-renderers 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 (51) hide show
  1. package/dist/editable/EditableDateCell.d.ts +29 -0
  2. package/dist/editable/EditableDateCell.d.ts.map +1 -0
  3. package/dist/editable/EditableLookupCell.d.ts +54 -0
  4. package/dist/editable/EditableLookupCell.d.ts.map +1 -0
  5. package/dist/editable/EditableNumberCell.d.ts +30 -0
  6. package/dist/editable/EditableNumberCell.d.ts.map +1 -0
  7. package/dist/editable/EditableOptionSetCell.d.ts +28 -0
  8. package/dist/editable/EditableOptionSetCell.d.ts.map +1 -0
  9. package/dist/editable/EditableRatingCell.d.ts +33 -0
  10. package/dist/editable/EditableRatingCell.d.ts.map +1 -0
  11. package/dist/editable/EditableTextCell.d.ts +28 -0
  12. package/dist/editable/EditableTextCell.d.ts.map +1 -0
  13. package/dist/index.d.ts +39 -0
  14. package/dist/index.d.ts.map +1 -0
  15. package/dist/index.esm.js +2107 -0
  16. package/dist/index.esm.js.map +1 -0
  17. package/dist/index.js +2156 -0
  18. package/dist/index.js.map +1 -0
  19. package/dist/renderers/ColoredCell.d.ts +8 -0
  20. package/dist/renderers/ColoredCell.d.ts.map +1 -0
  21. package/dist/renderers/CompositeCell.d.ts +13 -0
  22. package/dist/renderers/CompositeCell.d.ts.map +1 -0
  23. package/dist/renderers/CurrencyCell.d.ts +8 -0
  24. package/dist/renderers/CurrencyCell.d.ts.map +1 -0
  25. package/dist/renderers/DateCell.d.ts +8 -0
  26. package/dist/renderers/DateCell.d.ts.map +1 -0
  27. package/dist/renderers/IconCell.d.ts +8 -0
  28. package/dist/renderers/IconCell.d.ts.map +1 -0
  29. package/dist/renderers/LinkCell.d.ts +8 -0
  30. package/dist/renderers/LinkCell.d.ts.map +1 -0
  31. package/dist/renderers/LookupCell.d.ts +20 -0
  32. package/dist/renderers/LookupCell.d.ts.map +1 -0
  33. package/dist/renderers/NumberCell.d.ts +8 -0
  34. package/dist/renderers/NumberCell.d.ts.map +1 -0
  35. package/dist/renderers/OptionSetCell.d.ts +9 -0
  36. package/dist/renderers/OptionSetCell.d.ts.map +1 -0
  37. package/dist/renderers/ProgressBarCell.d.ts +8 -0
  38. package/dist/renderers/ProgressBarCell.d.ts.map +1 -0
  39. package/dist/renderers/RatingCell.d.ts +15 -0
  40. package/dist/renderers/RatingCell.d.ts.map +1 -0
  41. package/dist/renderers/TextCell.d.ts +8 -0
  42. package/dist/renderers/TextCell.d.ts.map +1 -0
  43. package/dist/renderers/ToggleCell.d.ts +8 -0
  44. package/dist/renderers/ToggleCell.d.ts.map +1 -0
  45. package/dist/renderers/emojiRating.d.ts +22 -0
  46. package/dist/renderers/emojiRating.d.ts.map +1 -0
  47. package/dist/styles/index.d.ts +121 -0
  48. package/dist/styles/index.d.ts.map +1 -0
  49. package/dist/types/index.d.ts +245 -0
  50. package/dist/types/index.d.ts.map +1 -0
  51. package/package.json +42 -0
@@ -0,0 +1,2107 @@
1
+ import { mergeStyles, Text, Icon, Toggle, IconButton, TextField, DatePicker, ComboBox, Dropdown, Callout, DirectionalHint } from '@fluentui/react';
2
+ import { jsx, jsxs } from 'react/jsx-runtime';
3
+ import { useCallback, useState, useRef, useEffect, useMemo } from 'react';
4
+ import { formatCurrency, formatDateTime, formatDate, formatNumber, getContrastColor, normalizeHexColor } from '@khester/dynamics-utils';
5
+
6
+ /**
7
+ * Dynamics 365 Grid color tokens.
8
+ * Inlined for standalone package use.
9
+ */
10
+ const dynamicsGridColors = {
11
+ headerBackground: '#f8f8f8',
12
+ headerBorder: '#e1e1e1',
13
+ rowSelected: '#cce4f6',
14
+ rowSelectedBorder: '#0078d4',
15
+ rowSelectedHover: '#b3d7f2',
16
+ rowHover: '#f3f2f1'};
17
+ // ============================================================================
18
+ // Cell-Level Styles
19
+ // ============================================================================
20
+ /**
21
+ * Cell container - ensures proper vertical alignment.
22
+ */
23
+ const cellClass = mergeStyles({
24
+ display: 'flex',
25
+ alignItems: 'center',
26
+ height: '100%',
27
+ });
28
+ /**
29
+ * Link cell style - clickable text with hover underline.
30
+ */
31
+ const linkCellClass = mergeStyles({
32
+ fontWeight: 600,
33
+ color: '#0078d4',
34
+ cursor: 'pointer',
35
+ '&:hover': {
36
+ textDecoration: 'underline',
37
+ },
38
+ '&:focus': {
39
+ outline: '2px solid #0078d4',
40
+ outlineOffset: 2,
41
+ },
42
+ });
43
+ /**
44
+ * Colored cell badge style.
45
+ */
46
+ const coloredBadgeClass = mergeStyles({
47
+ padding: '2px 8px',
48
+ borderRadius: 4,
49
+ fontSize: 12,
50
+ fontWeight: 600,
51
+ display: 'inline-block',
52
+ });
53
+ /**
54
+ * Currency cell style - right-aligned numbers.
55
+ */
56
+ const currencyCellClass = mergeStyles({
57
+ textAlign: 'right',
58
+ fontFamily: 'Consolas, "Courier New", monospace',
59
+ });
60
+ /**
61
+ * Progress bar container.
62
+ */
63
+ const progressBarContainerClass = mergeStyles({
64
+ display: 'flex',
65
+ alignItems: 'center',
66
+ gap: 8,
67
+ width: '100%',
68
+ });
69
+ /**
70
+ * Progress bar background.
71
+ */
72
+ const progressBarBackgroundClass = mergeStyles({
73
+ flex: 1,
74
+ height: 8,
75
+ backgroundColor: '#edebe9',
76
+ borderRadius: 4,
77
+ overflow: 'hidden',
78
+ });
79
+ /**
80
+ * Progress bar fill (requires dynamic width via style prop).
81
+ */
82
+ const progressBarFillClass = mergeStyles({
83
+ height: '100%',
84
+ backgroundColor: '#0078d4',
85
+ borderRadius: 4,
86
+ transition: 'width 0.3s ease',
87
+ });
88
+ /**
89
+ * Progress bar label.
90
+ */
91
+ const progressBarLabelClass = mergeStyles({
92
+ fontSize: 12,
93
+ color: '#605e5c',
94
+ minWidth: 40,
95
+ textAlign: 'right',
96
+ });
97
+ /**
98
+ * Icon button cell style.
99
+ */
100
+ const iconButtonCellClass = mergeStyles({
101
+ display: 'flex',
102
+ alignItems: 'center',
103
+ justifyContent: 'center',
104
+ });
105
+ /**
106
+ * Rating cell container - row of star glyphs.
107
+ */
108
+ const ratingCellClass = mergeStyles({
109
+ display: 'flex',
110
+ alignItems: 'center',
111
+ gap: 2,
112
+ height: '100%',
113
+ });
114
+ /**
115
+ * Single rating star glyph.
116
+ */
117
+ const ratingStarClass = mergeStyles({
118
+ fontSize: 16,
119
+ lineHeight: 1,
120
+ userSelect: 'none',
121
+ });
122
+ /**
123
+ * Composite cell container - lays out ordered slots.
124
+ */
125
+ const compositeCellClass = mergeStyles({
126
+ display: 'flex',
127
+ height: '100%',
128
+ });
129
+ /**
130
+ * Editable cell container - base wrapper for inline edit controls.
131
+ */
132
+ const editableCellClass = mergeStyles({
133
+ display: 'flex',
134
+ alignItems: 'center',
135
+ height: '100%',
136
+ width: '100%',
137
+ position: 'relative',
138
+ });
139
+ /**
140
+ * Editable cell input wrapper - provides the dirty/error border indicators.
141
+ */
142
+ const editableCellInputWrapperClass = mergeStyles({
143
+ display: 'flex',
144
+ alignItems: 'center',
145
+ width: '100%',
146
+ position: 'relative',
147
+ });
148
+ /**
149
+ * Dirty cell indicator - blue left border on edited cells.
150
+ * Apply as additional class when cell is dirty.
151
+ */
152
+ const dirtyCellIndicatorClass = mergeStyles({
153
+ '&::before': {
154
+ content: '""',
155
+ position: 'absolute',
156
+ left: -8,
157
+ top: 0,
158
+ bottom: 0,
159
+ width: 3,
160
+ backgroundColor: '#0078d4',
161
+ borderRadius: '1px',
162
+ },
163
+ });
164
+ /**
165
+ * Error cell indicator - red left border on cells with validation errors.
166
+ * Apply as additional class when cell has error.
167
+ */
168
+ const errorCellIndicatorClass = mergeStyles({
169
+ '&::before': {
170
+ content: '""',
171
+ position: 'absolute',
172
+ left: -8,
173
+ top: 0,
174
+ bottom: 0,
175
+ width: 3,
176
+ backgroundColor: '#d13438',
177
+ borderRadius: '1px',
178
+ },
179
+ });
180
+ /**
181
+ * Error message text below field.
182
+ */
183
+ const cellErrorTextClass = mergeStyles({
184
+ color: '#d13438',
185
+ fontSize: 11,
186
+ position: 'absolute',
187
+ bottom: -14,
188
+ left: 0,
189
+ whiteSpace: 'nowrap',
190
+ overflow: 'hidden',
191
+ textOverflow: 'ellipsis',
192
+ maxWidth: '100%',
193
+ });
194
+ // ============================================================================
195
+ // Lookup Cell Styles
196
+ // ============================================================================
197
+ /**
198
+ * Lookup cell container - wrapper for the search input and dropdown.
199
+ */
200
+ const lookupCellContainerClass = mergeStyles({
201
+ position: 'relative',
202
+ width: '100%',
203
+ display: 'flex',
204
+ alignItems: 'center',
205
+ });
206
+ /**
207
+ * Lookup dropdown container - positioned below the input.
208
+ */
209
+ const lookupDropdownClass = mergeStyles({
210
+ position: 'absolute',
211
+ top: '100%',
212
+ left: -8,
213
+ right: -8,
214
+ zIndex: 1000,
215
+ backgroundColor: 'white',
216
+ border: '1px solid #edebe9',
217
+ boxShadow: '0 3px 6px rgba(0, 0, 0, 0.16)',
218
+ maxHeight: 200,
219
+ overflowY: 'auto',
220
+ borderRadius: 2,
221
+ marginTop: 2,
222
+ });
223
+ /**
224
+ * Lookup dropdown item - each selectable option.
225
+ */
226
+ const lookupDropdownItemClass = mergeStyles({
227
+ padding: '8px 12px',
228
+ cursor: 'pointer',
229
+ fontSize: 13,
230
+ '&:hover': {
231
+ backgroundColor: '#f3f2f1',
232
+ },
233
+ });
234
+ /**
235
+ * Lookup dropdown item - active/selected state.
236
+ */
237
+ const lookupDropdownItemActiveClass = mergeStyles({
238
+ backgroundColor: '#f3f2f1',
239
+ });
240
+ /**
241
+ * Lookup dropdown item - none option (clear selection).
242
+ */
243
+ const lookupDropdownNoneOptionClass = mergeStyles({
244
+ padding: '8px 12px',
245
+ cursor: 'pointer',
246
+ fontSize: 13,
247
+ fontStyle: 'italic',
248
+ color: '#605e5c',
249
+ borderBottom: '1px solid #edebe9',
250
+ '&:hover': {
251
+ backgroundColor: '#f3f2f1',
252
+ },
253
+ });
254
+ /**
255
+ * Lookup loading indicator.
256
+ */
257
+ const lookupLoadingClass = mergeStyles({
258
+ padding: '8px 12px',
259
+ fontSize: 13,
260
+ color: '#605e5c',
261
+ fontStyle: 'italic',
262
+ });
263
+ /**
264
+ * Lookup no results message.
265
+ */
266
+ const lookupNoResultsClass = mergeStyles({
267
+ padding: '8px 12px',
268
+ fontSize: 13,
269
+ color: '#605e5c',
270
+ fontStyle: 'italic',
271
+ });
272
+ /**
273
+ * Lookup clear button - positioned inside the input.
274
+ */
275
+ const lookupClearButtonClass = mergeStyles({
276
+ position: 'absolute',
277
+ right: 4,
278
+ top: '50%',
279
+ transform: 'translateY(-50%)',
280
+ width: 20,
281
+ height: 20,
282
+ minWidth: 20,
283
+ padding: 0,
284
+ border: 'none',
285
+ backgroundColor: 'transparent',
286
+ cursor: 'pointer',
287
+ display: 'flex',
288
+ alignItems: 'center',
289
+ justifyContent: 'center',
290
+ color: '#605e5c',
291
+ '&:hover': {
292
+ color: '#323130',
293
+ backgroundColor: '#f3f2f1',
294
+ borderRadius: 2,
295
+ },
296
+ });
297
+ /**
298
+ * Lookup read mode link - displays lookup value as clickable link.
299
+ */
300
+ const lookupReadModeLinkClass = mergeStyles({
301
+ color: '#0078d4',
302
+ cursor: 'pointer',
303
+ textDecoration: 'none',
304
+ overflow: 'hidden',
305
+ textOverflow: 'ellipsis',
306
+ whiteSpace: 'nowrap',
307
+ '&:hover': {
308
+ textDecoration: 'underline',
309
+ },
310
+ '&:focus': {
311
+ outline: '2px solid #0078d4',
312
+ outlineOffset: 2,
313
+ },
314
+ });
315
+ // ============================================================================
316
+ // Grid-Level Styles (for consumers building full grids)
317
+ // ============================================================================
318
+ /**
319
+ * Get DetailsList styles for consistent Dynamics CE look.
320
+ */
321
+ function getDetailsListStyles() {
322
+ return {
323
+ root: {},
324
+ headerWrapper: {
325
+ '.ms-DetailsHeader': {
326
+ backgroundColor: dynamicsGridColors.headerBackground,
327
+ borderBottom: `1px solid ${dynamicsGridColors.headerBorder}`,
328
+ paddingTop: 0,
329
+ height: 42,
330
+ },
331
+ '.ms-DetailsHeader-cell': {
332
+ fontSize: 13,
333
+ fontWeight: 600,
334
+ color: '#323130',
335
+ },
336
+ '.ms-DetailsHeader-cellTitle': {
337
+ fontSize: 13,
338
+ fontWeight: 600,
339
+ },
340
+ },
341
+ };
342
+ }
343
+ /**
344
+ * Get row styles for consistent hover behavior.
345
+ */
346
+ function getRowStyles() {
347
+ return {
348
+ root: {
349
+ cursor: 'pointer',
350
+ '&:hover': {
351
+ backgroundColor: '#f3f2f1',
352
+ },
353
+ },
354
+ };
355
+ }
356
+ /**
357
+ * Get row styles with selection highlighting (Dynamics 365 native look).
358
+ * @param isSelected - Whether the row is currently selected
359
+ */
360
+ function getRowStylesWithSelection(isSelected) {
361
+ return {
362
+ root: {
363
+ cursor: 'pointer',
364
+ backgroundColor: isSelected ? dynamicsGridColors.rowSelected : undefined,
365
+ borderLeft: isSelected ? `3px solid ${dynamicsGridColors.rowSelectedBorder}` : '3px solid transparent',
366
+ '&:hover': {
367
+ backgroundColor: isSelected
368
+ ? dynamicsGridColors.rowSelectedHover
369
+ : dynamicsGridColors.rowHover,
370
+ },
371
+ },
372
+ };
373
+ }
374
+
375
+ /**
376
+ * Basic text cell renderer.
377
+ * Displays text with optional truncation and tooltip.
378
+ */
379
+ function TextCell({ value, column, item }) {
380
+ const displayValue = column.formatValue
381
+ ? column.formatValue(value, item)
382
+ : value;
383
+ const textValue = displayValue !== null && displayValue !== undefined
384
+ ? String(displayValue)
385
+ : '-';
386
+ return (jsx("div", { className: cellClass, children: jsx(Text, { title: textValue !== '-' ? textValue : undefined, styles: {
387
+ root: {
388
+ overflow: 'hidden',
389
+ textOverflow: 'ellipsis',
390
+ whiteSpace: 'nowrap',
391
+ },
392
+ }, children: textValue }) }));
393
+ }
394
+
395
+ /**
396
+ * Link cell renderer.
397
+ * Displays a clickable link with hover underline.
398
+ */
399
+ function LinkCell({ item, value, column }) {
400
+ const displayValue = column.formatValue
401
+ ? column.formatValue(value, item)
402
+ : value;
403
+ const textValue = displayValue !== null && displayValue !== undefined
404
+ ? String(displayValue)
405
+ : '(No Name)';
406
+ const handleClick = useCallback(() => {
407
+ if (column.onLinkClick) {
408
+ column.onLinkClick(item);
409
+ }
410
+ }, [column, item]);
411
+ const handleKeyDown = useCallback((event) => {
412
+ if ((event.key === 'Enter' || event.key === ' ') && column.onLinkClick) {
413
+ event.preventDefault();
414
+ column.onLinkClick(item);
415
+ }
416
+ }, [column, item]);
417
+ return (jsx("div", { className: cellClass, children: jsx("span", { className: linkCellClass, onClick: handleClick, onKeyDown: handleKeyDown, role: "button", tabIndex: 0, title: textValue, children: textValue }) }));
418
+ }
419
+
420
+ /**
421
+ * Currency cell renderer.
422
+ * Displays formatted currency values with locale support.
423
+ */
424
+ function CurrencyCell({ value, column, item }) {
425
+ const numericValue = typeof value === 'number' ? value : null;
426
+ const displayValue = column.formatValue
427
+ ? column.formatValue(value, item)
428
+ : numericValue !== null
429
+ ? formatCurrency(numericValue, column.currencyCode || 'USD', column.locale || 'en-US')
430
+ : '-';
431
+ return (jsx("div", { className: cellClass, children: jsx(Text, { className: currencyCellClass, title: displayValue, children: displayValue }) }));
432
+ }
433
+
434
+ /**
435
+ * Date cell renderer.
436
+ * Displays formatted date or datetime values.
437
+ */
438
+ function DateCell({ value, column, item }) {
439
+ const stringValue = value !== null && value !== undefined ? String(value) : null;
440
+ const locale = column.locale || 'en-US';
441
+ let displayValue;
442
+ if (column.formatValue) {
443
+ displayValue = column.formatValue(value, item);
444
+ }
445
+ else if (stringValue) {
446
+ displayValue = column.fieldType === 'datetime'
447
+ ? formatDateTime(stringValue, locale)
448
+ : formatDate(stringValue, locale);
449
+ }
450
+ else {
451
+ displayValue = '-';
452
+ }
453
+ return (jsx("div", { className: cellClass, children: jsx(Text, { title: displayValue !== '-' ? displayValue : undefined, children: displayValue }) }));
454
+ }
455
+
456
+ /**
457
+ * Number cell renderer.
458
+ * Displays formatted numeric values with locale support.
459
+ */
460
+ function NumberCell({ value, column, item }) {
461
+ const numericValue = typeof value === 'number' ? value : null;
462
+ const locale = column.locale || 'en-US';
463
+ const displayValue = column.formatValue
464
+ ? column.formatValue(value, item)
465
+ : numericValue !== null
466
+ ? formatNumber(numericValue, locale)
467
+ : '-';
468
+ return (jsx("div", { className: cellClass, children: jsx(Text, { title: displayValue !== '-' ? displayValue : undefined, styles: {
469
+ root: {
470
+ textAlign: 'right',
471
+ width: '100%',
472
+ },
473
+ }, children: displayValue }) }));
474
+ }
475
+
476
+ /**
477
+ * Default colors for option set values.
478
+ */
479
+ const DEFAULT_OPTIONSET_COLORS = {
480
+ 0: '#107c10', // Active/Yes (green)
481
+ 1: '#d13438', // Inactive/No (red)
482
+ 2: '#0078d4', // Open (blue)
483
+ 3: '#ffb900', // Pending (yellow)
484
+ 4: '#605e5c', // Closed (gray)
485
+ };
486
+ /**
487
+ * Resolve one optionset value → a badge (label + background + optional icon).
488
+ * Returns null for empty/unlabeled values. Color precedence: per-option `color` >
489
+ * `getBackgroundColor` > `colorMap` > numeric default > gray. Label precedence:
490
+ * per-option `label` > `formatValue` > raw string.
491
+ */
492
+ function resolveBadge(v, column, item) {
493
+ if (v === null || v === undefined || v === '')
494
+ return null;
495
+ const option = column.optionSetOptions?.find((o) => String(o.value) === String(v));
496
+ const label = option?.label ?? (column.formatValue ? column.formatValue(v, item) : String(v));
497
+ if (!label || label === '-')
498
+ return null;
499
+ let bg = option?.color;
500
+ if (!bg && column.getBackgroundColor)
501
+ bg = column.getBackgroundColor(v);
502
+ if (!bg && column.colorMap) {
503
+ const key = typeof v === 'number' ? v : String(v);
504
+ bg = column.colorMap[key];
505
+ }
506
+ if (!bg && typeof v === 'number' && v in DEFAULT_OPTIONSET_COLORS)
507
+ bg = DEFAULT_OPTIONSET_COLORS[v];
508
+ if (!bg)
509
+ bg = '#c8c6c4';
510
+ return { label, bg: normalizeHexColor(bg), iconName: option?.iconName };
511
+ }
512
+ /**
513
+ * OptionSet cell renderer. Displays a colored badge per value — one for a single
514
+ * value, several for a multi-select array. Per-option `iconName` renders a Fluent
515
+ * `<Icon>` (requires `initializeIcons()` on the host).
516
+ */
517
+ function OptionSetCell({ item, value, column }) {
518
+ const multi = Array.isArray(value);
519
+ const values = multi ? value : [value];
520
+ const badges = values.map((v) => resolveBadge(v, column, item)).filter((b) => b !== null);
521
+ if (badges.length === 0) {
522
+ return jsx("div", { className: cellClass, children: "-" });
523
+ }
524
+ return (jsx("div", { className: cellClass, style: multi ? { display: 'flex', gap: 4, flexWrap: 'wrap' } : undefined, children: badges.map((b, i) => (jsxs("span", { className: coloredBadgeClass, style: {
525
+ backgroundColor: b.bg,
526
+ color: getContrastColor(b.bg),
527
+ ...(b.iconName ? { display: 'inline-flex', alignItems: 'center', gap: 4 } : {}),
528
+ }, children: [b.iconName && jsx(Icon, { iconName: b.iconName, style: { fontSize: 12 }, "aria-hidden": true }), b.label] }, `${b.label}-${i}`))) }));
529
+ }
530
+
531
+ /**
532
+ * Toggle cell renderer.
533
+ * Displays a toggle switch for boolean values.
534
+ */
535
+ function ToggleCell({ item, value, column, itemKey }) {
536
+ const boolValue = Boolean(value);
537
+ const handleChange = useCallback((_event, checked) => {
538
+ if (column.onToggleChange && column.fieldName) {
539
+ column.onToggleChange(itemKey, column.fieldName, checked ?? false);
540
+ }
541
+ }, [column, itemKey]);
542
+ // Stop propagation to prevent row click when clicking toggle
543
+ const handleClick = useCallback((event) => {
544
+ event.stopPropagation();
545
+ }, []);
546
+ return (jsx("div", { className: cellClass, onClick: handleClick, children: jsx(Toggle, { checked: boolValue, onText: column.toggleOnText || 'Yes', offText: column.toggleOffText || 'No', onChange: handleChange, disabled: !column.editable, styles: {
547
+ root: { margin: 0 },
548
+ } }) }));
549
+ }
550
+
551
+ /**
552
+ * Icon cell renderer.
553
+ * Displays an icon button with click handler.
554
+ */
555
+ function IconCell({ item, column }) {
556
+ const iconProps = {
557
+ iconName: column.iconName || 'More',
558
+ };
559
+ const handleClick = useCallback((event) => {
560
+ event.stopPropagation(); // Prevent row click
561
+ if (column.onIconClick) {
562
+ column.onIconClick(item);
563
+ }
564
+ }, [column, item]);
565
+ return (jsx("div", { className: iconButtonCellClass, children: jsx(IconButton, { iconProps: iconProps, title: column.iconTitle || column.name, ariaLabel: column.iconTitle || column.name, onClick: handleClick, styles: {
566
+ root: {
567
+ width: 32,
568
+ height: 32,
569
+ },
570
+ rootHovered: {
571
+ backgroundColor: '#f3f2f1',
572
+ },
573
+ } }) }));
574
+ }
575
+
576
+ /**
577
+ * Progress bar cell renderer.
578
+ * Displays a horizontal progress bar with optional label.
579
+ */
580
+ function ProgressBarCell({ value, column }) {
581
+ const numericValue = typeof value === 'number' ? value : 0;
582
+ const maxValue = column.progressMax || 100;
583
+ const percentage = Math.min(100, Math.max(0, (numericValue / maxValue) * 100));
584
+ const progressColor = column.progressColor || '#0078d4';
585
+ const showLabel = column.showProgressLabel !== false;
586
+ return (jsxs("div", { className: progressBarContainerClass, children: [jsx("div", { className: progressBarBackgroundClass, children: jsx("div", { className: progressBarFillClass, style: {
587
+ width: `${percentage}%`,
588
+ backgroundColor: progressColor,
589
+ } }) }), showLabel && (jsxs("span", { className: progressBarLabelClass, children: [Math.round(percentage), "%"] }))] }));
590
+ }
591
+
592
+ /**
593
+ * Colored cell renderer.
594
+ * Displays text with a colored background based on value.
595
+ */
596
+ function ColoredCell({ item, value, column }) {
597
+ // Get display text
598
+ const displayValue = column.formatValue
599
+ ? column.formatValue(value, item)
600
+ : value !== null && value !== undefined
601
+ ? String(value)
602
+ : '-';
603
+ if (displayValue === '-') {
604
+ return jsx("div", { className: cellClass, children: "-" });
605
+ }
606
+ // Get background color
607
+ let backgroundColor;
608
+ if (column.getBackgroundColor && value !== null && value !== undefined) {
609
+ backgroundColor = column.getBackgroundColor(value);
610
+ }
611
+ else if (column.colorMap && value !== null && value !== undefined) {
612
+ const key = typeof value === 'number' ? value : String(value);
613
+ backgroundColor = column.colorMap[key];
614
+ }
615
+ // If no color mapping, render as plain text
616
+ if (!backgroundColor) {
617
+ return (jsx("div", { className: cellClass, children: displayValue }));
618
+ }
619
+ const normalizedBg = normalizeHexColor(backgroundColor);
620
+ const textColor = getContrastColor(normalizedBg);
621
+ return (jsx("div", { className: cellClass, children: jsx("span", { className: coloredBadgeClass, style: {
622
+ backgroundColor: normalizedBg,
623
+ color: textColor,
624
+ }, children: displayValue }) }));
625
+ }
626
+
627
+ /**
628
+ * Lookup cell renderer.
629
+ * Displays lookup field values with optional click handler or Dynamics navigation.
630
+ *
631
+ * Navigation is enabled when:
632
+ * - dynamicsBaseUrl is provided
633
+ * - column.lookupTargetEntity is configured
634
+ * - The lookup has a value (not null)
635
+ */
636
+ function LookupCell({ item, value, column, dynamicsBaseUrl }) {
637
+ // For lookup fields, try to get display value from the lookup display field
638
+ let displayValue;
639
+ if (column.formatValue) {
640
+ displayValue = column.formatValue(value, item);
641
+ }
642
+ else if (column.lookupDisplayField && typeof item === 'object' && item !== null) {
643
+ const lookupValue = item[column.lookupDisplayField];
644
+ displayValue = lookupValue !== null && lookupValue !== undefined
645
+ ? String(lookupValue)
646
+ : '-';
647
+ }
648
+ else if (typeof value === 'object' && value !== null) {
649
+ // Try common lookup value patterns
650
+ const lookupObj = value;
651
+ displayValue = String(lookupObj.name || lookupObj.fullname || lookupObj.title || '-');
652
+ }
653
+ else {
654
+ displayValue = value !== null && value !== undefined ? String(value) : '-';
655
+ }
656
+ // Extract lookup ID for navigation
657
+ // Dynamics stores lookup IDs in different formats depending on $expand usage:
658
+ // 1. Without $expand: _fieldname_value (e.g., _primarycontactid_value)
659
+ // 2. With $expand: item.fieldname.entityid (e.g., item.primarycontactid.contactid)
660
+ const fieldName = column.fieldName || column.key;
661
+ const itemRecord = item;
662
+ // Try _fieldname_value format first (non-expanded)
663
+ const lookupIdKey = `_${fieldName}_value`;
664
+ let lookupId = itemRecord[lookupIdKey];
665
+ // If not found, try expanded object format
666
+ if (!lookupId && column.lookupPrimaryIdAttribute) {
667
+ const expandedValue = itemRecord[fieldName];
668
+ if (expandedValue && typeof expandedValue === 'object') {
669
+ const expanded = expandedValue;
670
+ lookupId = expanded[column.lookupPrimaryIdAttribute];
671
+ }
672
+ }
673
+ // Fallback: try common ID patterns in expanded object
674
+ if (!lookupId) {
675
+ const expandedValue = itemRecord[fieldName];
676
+ if (expandedValue && typeof expandedValue === 'object') {
677
+ const expanded = expandedValue;
678
+ // Try targetentityid (e.g., contactid, accountid)
679
+ if (column.lookupTargetEntity) {
680
+ lookupId = expanded[`${column.lookupTargetEntity}id`];
681
+ }
682
+ // Try generic patterns
683
+ if (!lookupId) {
684
+ lookupId = (expanded.id || expanded.Id);
685
+ }
686
+ }
687
+ }
688
+ // Determine if navigation is available
689
+ const canNavigate = !!(dynamicsBaseUrl && column.lookupTargetEntity && lookupId && displayValue !== '-');
690
+ // Handle navigation to Dynamics record
691
+ const handleNavigate = () => {
692
+ if (canNavigate) {
693
+ const url = `${dynamicsBaseUrl}/main.aspx?etn=${column.lookupTargetEntity}&id=${lookupId}&pagetype=entityrecord`;
694
+ window.open(url, '_blank');
695
+ }
696
+ };
697
+ const handleKeyDown = (event) => {
698
+ if ((event.key === 'Enter' || event.key === ' ')) {
699
+ event.preventDefault();
700
+ if (column.onLinkClick) {
701
+ column.onLinkClick(item);
702
+ }
703
+ else if (canNavigate) {
704
+ handleNavigate();
705
+ }
706
+ }
707
+ };
708
+ // If there's a custom click handler, use it (priority over navigation)
709
+ if (column.onLinkClick && displayValue !== '-') {
710
+ const handleClick = () => {
711
+ if (column.onLinkClick) {
712
+ column.onLinkClick(item);
713
+ }
714
+ };
715
+ return (jsx("div", { className: cellClass, children: jsx("span", { className: linkCellClass, onClick: handleClick, onKeyDown: handleKeyDown, role: "button", tabIndex: 0, title: displayValue, children: displayValue }) }));
716
+ }
717
+ // If navigation is available, render as clickable link
718
+ if (canNavigate) {
719
+ return (jsx("div", { className: cellClass, children: jsx("span", { className: linkCellClass, onClick: handleNavigate, onKeyDown: handleKeyDown, role: "link", tabIndex: 0, title: `Open ${displayValue} in Dynamics 365`, children: displayValue }) }));
720
+ }
721
+ // Default: render as plain text
722
+ return (jsx("div", { className: cellClass, children: jsx(Text, { title: displayValue !== '-' ? displayValue : undefined, children: displayValue }) }));
723
+ }
724
+
725
+ /**
726
+ * Shared emoji-rating data + helpers for the 'emoji' rating style.
727
+ *
728
+ * Ported from the form-designer rating control (sentiment faces, 2–5 count).
729
+ * Used by both the read-only `RatingCell` (single selected face) and the
730
+ * `EditableRatingCell` (clickable face row).
731
+ *
732
+ * NOTE: the emoji style renders Fluent `<Icon>` faces, which require the host to
733
+ * have called `initializeIcons()`. The 'stars' style stays icon-registration-free.
734
+ */
735
+ /** Fluent icon names + accessible labels per face count (2–5). */
736
+ const EMOJI_SETS = {
737
+ 2: { icons: ['EmojiDisappointed', 'Emoji'], labels: ['Unhappy', 'Happy'] },
738
+ 3: {
739
+ icons: ['EmojiDisappointed', 'EmojiNeutral', 'Emoji'],
740
+ labels: ['Unhappy', 'Neutral', 'Happy'],
741
+ },
742
+ 4: {
743
+ icons: ['EmojiDisappointed', 'Sad', 'Emoji2', 'Emoji'],
744
+ labels: ['Very Unhappy', 'Unhappy', 'Happy', 'Very Happy'],
745
+ },
746
+ 5: {
747
+ icons: ['EmojiDisappointed', 'Sad', 'EmojiNeutral', 'Emoji2', 'Emoji'],
748
+ labels: ['Very Unhappy', 'Unhappy', 'Neutral', 'Happy', 'Very Happy'],
749
+ },
750
+ };
751
+ /** Sentiment colors (red→green) per face count, aligned to `EMOJI_SETS`. */
752
+ const EMOJI_COLORS = {
753
+ 2: ['#d13438', '#107c10'],
754
+ 3: ['#d13438', '#c8c820', '#107c10'],
755
+ 4: ['#d13438', '#ca5010', '#498205', '#107c10'],
756
+ 5: ['#d13438', '#ca5010', '#c8c820', '#498205', '#107c10'],
757
+ };
758
+ /** Clamp a face count into the supported 2–5 range, falling back to ratingMax then 5. */
759
+ function clampFaceCount(faceCount, ratingMax) {
760
+ const raw = faceCount ?? ratingMax ?? 5;
761
+ const n = Math.floor(Number(raw));
762
+ return Math.max(2, Math.min(5, Number.isFinite(n) ? n : 5));
763
+ }
764
+ /** 1-based selected face index from a numeric value (0 = none/unset). */
765
+ function selectedFaceIndex(value, faceCount) {
766
+ const raw = typeof value === 'number' ? value : Number(value);
767
+ if (!Number.isFinite(raw) || raw <= 0)
768
+ return 0;
769
+ return Math.min(faceCount, Math.max(1, Math.round(raw)));
770
+ }
771
+
772
+ /**
773
+ * Rating cell renderer.
774
+ * Displays a star rating (e.g. 0–5) from a numeric value. With `ratingAllowHalf`
775
+ * (default true) each star fills to the exact fraction of the value (so 3.5 shows
776
+ * three and a half filled stars); set it false to floor to whole stars.
777
+ *
778
+ * NOTE: the default `ratingStyle: 'stars'` uses unicode stars colored via CSS
779
+ * `background-clip: text`, so no Fluent icon registration is required. The
780
+ * `ratingStyle: 'emoji'` (sentiment faces) variant renders Fluent `<Icon>`s and
781
+ * therefore REQUIRES the host to have called `initializeIcons()` (same as IconCell).
782
+ */
783
+ function RatingCell({ value, column }) {
784
+ // Emoji (sentiment) style: a single selected face, optionally sentiment-colored.
785
+ if (column.ratingStyle === 'emoji') {
786
+ const faceCount = clampFaceCount(column.ratingFaceCount, column.ratingMax);
787
+ const set = EMOJI_SETS[faceCount];
788
+ const idx = selectedFaceIndex(value, faceCount); // 1-based, 0 = none
789
+ if (idx === 0) {
790
+ return (jsx("div", { className: ratingCellClass, role: "img", "aria-label": "No rating", children: "\u2013" }));
791
+ }
792
+ const useColor = column.ratingUseSentimentColor !== false; // default true for emoji
793
+ const color = useColor ? EMOJI_COLORS[faceCount][idx - 1] : column.ratingColor || '#605e5c';
794
+ return (jsx("div", { className: ratingCellClass, role: "img", "aria-label": `${set.labels[idx - 1]} (${idx} out of ${faceCount})`, children: jsx(Icon, { iconName: set.icons[idx - 1], style: { color, fontSize: 18 } }) }));
795
+ }
796
+ const max = column.ratingMax && column.ratingMax > 0 ? Math.floor(column.ratingMax) : 5;
797
+ const filledColor = column.ratingColor || '#ffb900';
798
+ const emptyColor = '#c8c6c4';
799
+ const allowHalf = column.ratingAllowHalf !== false;
800
+ const raw = typeof value === 'number' ? value : Number(value);
801
+ const rating = Number.isFinite(raw) ? Math.min(max, Math.max(0, raw)) : 0;
802
+ const stars = [];
803
+ for (let i = 1; i <= max; i++) {
804
+ // Fraction of THIS star that is filled (0..1).
805
+ let frac = Math.max(0, Math.min(1, rating - (i - 1)));
806
+ if (!allowHalf)
807
+ frac = frac >= 1 ? 1 : 0; // whole-star (floor) rounding
808
+ const pct = Math.round(frac * 100);
809
+ stars.push(jsx("span", { className: ratingStarClass, style: {
810
+ background: `linear-gradient(90deg, ${filledColor} ${pct}%, ${emptyColor} ${pct}%)`,
811
+ WebkitBackgroundClip: 'text',
812
+ backgroundClip: 'text',
813
+ color: 'transparent',
814
+ WebkitTextFillColor: 'transparent',
815
+ }, "aria-hidden": "true", children: "\u2605" }, i));
816
+ }
817
+ // Announce what is actually shown: the exact value (half mode) or the floored
818
+ // whole-star count.
819
+ const announced = allowHalf ? rating : Math.floor(rating);
820
+ return (jsx("div", { className: ratingCellClass, role: "img", "aria-label": `${announced} out of ${max}`, children: stars }));
821
+ }
822
+
823
+ /** Read a (possibly nested-free) field off the row. */
824
+ function getField(item, field) {
825
+ if (!field || item === null || typeof item !== 'object')
826
+ return undefined;
827
+ return item[field];
828
+ }
829
+ /** Coerce to a number, treating empty/null/undefined as NaN (not 0). */
830
+ function asNum(x) {
831
+ if (x === '' || x === null || x === undefined)
832
+ return NaN;
833
+ return typeof x === 'number' ? x : Number(x);
834
+ }
835
+ /** Evaluate a single conditional-style rule against the row. */
836
+ function conditionMatches(cond, item) {
837
+ const actual = getField(item, cond.field);
838
+ const a = asNum(actual);
839
+ const c = asNum(cond.value);
840
+ const bothNumeric = Number.isFinite(a) && Number.isFinite(c);
841
+ switch (cond.operator) {
842
+ case 'eq':
843
+ // Numeric equality when both sides are numbers (so 1 === '1' and
844
+ // 5 === 5.0), else exact string compare.
845
+ return bothNumeric ? a === c : String(actual) === String(cond.value);
846
+ case 'gt':
847
+ return bothNumeric && a > c;
848
+ case 'lt':
849
+ return bothNumeric && a < c;
850
+ case 'gte':
851
+ return bothNumeric && a >= c;
852
+ case 'lte':
853
+ return bothNumeric && a <= c;
854
+ case 'contains':
855
+ return String(actual ?? '')
856
+ .toLowerCase()
857
+ .includes(String(cond.value).toLowerCase());
858
+ case 'between': {
859
+ const to = asNum(cond.valueTo !== undefined ? cond.valueTo : cond.value);
860
+ return Number.isFinite(a) && Number.isFinite(c) && Number.isFinite(to) && a >= c && a <= to;
861
+ }
862
+ default:
863
+ return false;
864
+ }
865
+ }
866
+ /** Resolve a slot's style: first matching conditional style, else default. */
867
+ function resolveSlotStyle(slot, item) {
868
+ if (slot.conditionalStyles) {
869
+ for (const cs of slot.conditionalStyles) {
870
+ if (conditionMatches(cs, item))
871
+ return cs.style;
872
+ }
873
+ }
874
+ return slot.defaultStyle || {};
875
+ }
876
+ function renderSlot(slot, item) {
877
+ const style = resolveSlotStyle(slot, item);
878
+ const bound = getField(item, slot.fieldBinding);
879
+ const text = bound !== undefined && bound !== null ? String(bound) : slot.staticValue ?? '';
880
+ switch (slot.type) {
881
+ case 'spacer':
882
+ return jsx("span", { style: { flex: 1, ...style } }, slot.id);
883
+ case 'icon':
884
+ return (jsx(Icon, { iconName: slot.staticValue || (bound ? String(bound) : 'CircleFill'), style: style }, slot.id));
885
+ case 'image':
886
+ return (jsx("img", { src: text, alt: "", style: { height: 20, width: 20, objectFit: 'cover', borderRadius: 2, ...style } }, slot.id));
887
+ case 'badge':
888
+ return (jsx("span", { style: { padding: '2px 8px', borderRadius: 4, fontSize: 12, fontWeight: 600, ...style }, children: text }, slot.id));
889
+ case 'link':
890
+ return (jsx("a", { href: "#", onClick: (e) => e.preventDefault(), style: { color: '#0078d4', cursor: 'pointer', textDecoration: 'none', ...style }, children: text }, slot.id));
891
+ case 'progress': {
892
+ const pct = Math.min(100, Math.max(0, Number(bound) || 0));
893
+ return (jsx("span", { style: {
894
+ display: 'inline-block',
895
+ width: 60,
896
+ height: 8,
897
+ background: '#edebe9',
898
+ borderRadius: 4,
899
+ overflow: 'hidden',
900
+ ...style,
901
+ }, children: jsx("span", { style: { display: 'block', height: '100%', width: `${pct}%`, background: '#0078d4' } }) }, slot.id));
902
+ }
903
+ case 'rating': {
904
+ const r = Math.round(Number(bound) || 0);
905
+ return (jsxs("span", { style: { color: '#ffb900', ...style }, children: ['★'.repeat(Math.max(0, r)), '☆'.repeat(Math.max(0, 5 - r))] }, slot.id));
906
+ }
907
+ case 'text':
908
+ default:
909
+ return (jsx("span", { style: style, children: text }, slot.id));
910
+ }
911
+ }
912
+ /**
913
+ * Composite cell renderer.
914
+ * Renders an ordered list of slots (icon/text/badge/link/image/spacer/progress/
915
+ * rating) in one cell, each with optional conditional styling. Configured via
916
+ * `column.compositeSlots` / `compositeLayout` / `compositeGap`.
917
+ *
918
+ * NOTE: the `icon` slot uses Fluent UI's `Icon`, so the host app must call
919
+ * `initializeIcons()` once at startup or icon slots render blank.
920
+ */
921
+ function CompositeCell({ item, column }) {
922
+ const slots = column.compositeSlots || [];
923
+ const layout = column.compositeLayout || 'horizontal';
924
+ const gap = column.compositeGap ?? 4;
925
+ return (jsx("div", { className: compositeCellClass, style: {
926
+ flexDirection: layout === 'vertical' ? 'column' : 'row',
927
+ alignItems: layout === 'vertical' ? 'flex-start' : 'center',
928
+ gap,
929
+ }, children: slots.map((slot) => renderSlot(slot, item)) }));
930
+ }
931
+
932
+ // Styles for the text field in edit mode
933
+ const textFieldStyles = {
934
+ root: {
935
+ width: '100%',
936
+ },
937
+ fieldGroup: {
938
+ height: 28,
939
+ minHeight: 28,
940
+ border: 'none',
941
+ backgroundColor: 'transparent',
942
+ selectors: {
943
+ ':hover': {
944
+ border: '1px solid #c8c6c4',
945
+ backgroundColor: 'white',
946
+ },
947
+ ':focus-within': {
948
+ border: '1px solid #0078d4',
949
+ backgroundColor: 'white',
950
+ },
951
+ },
952
+ },
953
+ field: {
954
+ fontSize: 13,
955
+ padding: '0 8px',
956
+ },
957
+ };
958
+ const textFieldErrorStyles = {
959
+ ...textFieldStyles,
960
+ fieldGroup: {
961
+ ...textFieldStyles.fieldGroup,
962
+ border: '1px solid #d13438',
963
+ selectors: {
964
+ ':hover': {
965
+ border: '1px solid #d13438',
966
+ backgroundColor: 'white',
967
+ },
968
+ ':focus-within': {
969
+ border: '1px solid #d13438',
970
+ backgroundColor: 'white',
971
+ },
972
+ },
973
+ },
974
+ };
975
+ const readOnlyClass$2 = mergeStyles({
976
+ overflow: 'hidden',
977
+ textOverflow: 'ellipsis',
978
+ whiteSpace: 'nowrap',
979
+ fontSize: 13,
980
+ });
981
+ /**
982
+ * Editable text cell renderer.
983
+ * Displays as text in read mode, as TextField in edit mode.
984
+ * Shows blue indicator when dirty, red indicator when has validation error.
985
+ */
986
+ function EditableTextCell({ item, column, value, itemKey, isEditMode, isDirty, editedValue, onValueChange, errorMessage, onValidationError, }) {
987
+ const fieldName = column.fieldName || column.key;
988
+ const currentValue = editedValue !== undefined ? editedValue : value;
989
+ const stringValue = currentValue !== null && currentValue !== undefined
990
+ ? String(currentValue)
991
+ : '';
992
+ // Local state for immediate input feedback
993
+ const [localValue, setLocalValue] = useState(stringValue);
994
+ const inputRef = useRef(null);
995
+ // Sync local value when props change
996
+ useEffect(() => {
997
+ setLocalValue(stringValue);
998
+ }, [stringValue]);
999
+ // Handle input change
1000
+ const handleChange = useCallback((_, newValue) => {
1001
+ const val = newValue ?? '';
1002
+ setLocalValue(val);
1003
+ // Validate if column has validate function
1004
+ const col = column;
1005
+ if (col.validate && onValidationError) {
1006
+ const error = col.validate(val, itemKey);
1007
+ onValidationError(itemKey, fieldName, error);
1008
+ }
1009
+ // Notify parent of change
1010
+ onValueChange(itemKey, fieldName, val || null, value);
1011
+ }, [column, fieldName, itemKey, value, onValueChange, onValidationError]);
1012
+ // Handle blur - final validation
1013
+ const handleBlur = useCallback(() => {
1014
+ const col = column;
1015
+ if (col.validate && onValidationError) {
1016
+ const error = col.validate(localValue, itemKey);
1017
+ onValidationError(itemKey, fieldName, error);
1018
+ }
1019
+ }, [column, fieldName, itemKey, localValue, onValidationError]);
1020
+ // If not in edit mode, show read-only text
1021
+ if (!isEditMode) {
1022
+ const displayValue = column.formatValue
1023
+ ? column.formatValue(value, item)
1024
+ : stringValue || '-';
1025
+ return (jsx("div", { className: editableCellClass, children: jsx("span", { className: readOnlyClass$2, title: displayValue !== '-' ? displayValue : undefined, children: displayValue }) }));
1026
+ }
1027
+ // Determine indicator class
1028
+ let indicatorClass = '';
1029
+ if (errorMessage) {
1030
+ indicatorClass = errorCellIndicatorClass;
1031
+ }
1032
+ else if (isDirty) {
1033
+ indicatorClass = dirtyCellIndicatorClass;
1034
+ }
1035
+ const wrapperClass = mergeStyles(editableCellInputWrapperClass, indicatorClass);
1036
+ return (jsx("div", { className: editableCellClass, children: jsxs("div", { className: wrapperClass, children: [jsx(TextField, { componentRef: inputRef, value: localValue, onChange: handleChange, onBlur: handleBlur, borderless: true, styles: errorMessage ? textFieldErrorStyles : textFieldStyles, ariaLabel: `${column.name} for ${item.key}` }), errorMessage && (jsx("span", { className: cellErrorTextClass, title: errorMessage, children: errorMessage }))] }) }));
1037
+ }
1038
+
1039
+ // Styles for the number input in edit mode
1040
+ const numberFieldStyles = {
1041
+ root: {
1042
+ width: '100%',
1043
+ },
1044
+ fieldGroup: {
1045
+ height: 28,
1046
+ minHeight: 28,
1047
+ border: 'none',
1048
+ backgroundColor: 'transparent',
1049
+ selectors: {
1050
+ ':hover': {
1051
+ border: '1px solid #c8c6c4',
1052
+ backgroundColor: 'white',
1053
+ },
1054
+ ':focus-within': {
1055
+ border: '1px solid #0078d4',
1056
+ backgroundColor: 'white',
1057
+ },
1058
+ },
1059
+ },
1060
+ field: {
1061
+ fontSize: 13,
1062
+ padding: '0 8px',
1063
+ textAlign: 'right',
1064
+ },
1065
+ };
1066
+ const numberFieldErrorStyles = {
1067
+ ...numberFieldStyles,
1068
+ fieldGroup: {
1069
+ ...numberFieldStyles.fieldGroup,
1070
+ border: '1px solid #d13438',
1071
+ selectors: {
1072
+ ':hover': {
1073
+ border: '1px solid #d13438',
1074
+ backgroundColor: 'white',
1075
+ },
1076
+ ':focus-within': {
1077
+ border: '1px solid #d13438',
1078
+ backgroundColor: 'white',
1079
+ },
1080
+ },
1081
+ },
1082
+ };
1083
+ const readOnlyClass$1 = mergeStyles({
1084
+ overflow: 'hidden',
1085
+ textOverflow: 'ellipsis',
1086
+ whiteSpace: 'nowrap',
1087
+ fontSize: 13,
1088
+ textAlign: 'right',
1089
+ width: '100%',
1090
+ });
1091
+ /**
1092
+ * Parse a string to a number, handling currency formats.
1093
+ */
1094
+ function parseNumericValue(value) {
1095
+ if (!value || value.trim() === '')
1096
+ return null;
1097
+ // Remove currency symbols, thousands separators, and whitespace
1098
+ const cleaned = value.replace(/[$\u20ac\u00a3\u00a5,\s]/g, '');
1099
+ const num = parseFloat(cleaned);
1100
+ return isNaN(num) ? null : num;
1101
+ }
1102
+ /**
1103
+ * Editable number cell renderer.
1104
+ * Displays formatted number/currency in read mode, as TextField in edit mode.
1105
+ * Shows blue indicator when dirty, red indicator when has validation error.
1106
+ */
1107
+ function EditableNumberCell({ item, column, value, itemKey, isEditMode, isDirty, editedValue, onValueChange, errorMessage, onValidationError, isCurrency = false, }) {
1108
+ const fieldName = column.fieldName || column.key;
1109
+ const currentValue = editedValue !== undefined ? editedValue : value;
1110
+ const numericValue = typeof currentValue === 'number' ? currentValue : null;
1111
+ // For edit mode, show raw number
1112
+ const editDisplayValue = numericValue !== null ? String(numericValue) : '';
1113
+ // Local state for immediate input feedback
1114
+ const [localValue, setLocalValue] = useState(editDisplayValue);
1115
+ const inputRef = useRef(null);
1116
+ // Sync local value when props change
1117
+ useEffect(() => {
1118
+ setLocalValue(editDisplayValue);
1119
+ }, [editDisplayValue]);
1120
+ // Handle input change
1121
+ const handleChange = useCallback((_, newValue) => {
1122
+ const val = newValue ?? '';
1123
+ setLocalValue(val);
1124
+ // Parse to number
1125
+ const parsedValue = parseNumericValue(val);
1126
+ // Validate if column has validate function
1127
+ const col = column;
1128
+ if (col.validate && onValidationError) {
1129
+ const error = col.validate(parsedValue, itemKey);
1130
+ onValidationError(itemKey, fieldName, error);
1131
+ }
1132
+ // Notify parent of change
1133
+ onValueChange(itemKey, fieldName, parsedValue, value);
1134
+ }, [column, fieldName, itemKey, value, onValueChange, onValidationError]);
1135
+ // Handle blur - validate and format
1136
+ const handleBlur = useCallback(() => {
1137
+ const parsedValue = parseNumericValue(localValue);
1138
+ const col = column;
1139
+ if (col.validate && onValidationError) {
1140
+ const error = col.validate(parsedValue, itemKey);
1141
+ onValidationError(itemKey, fieldName, error);
1142
+ }
1143
+ // Update local value to show the parsed number
1144
+ if (parsedValue !== null) {
1145
+ setLocalValue(String(parsedValue));
1146
+ }
1147
+ else if (localValue.trim() === '') {
1148
+ setLocalValue('');
1149
+ }
1150
+ }, [column, fieldName, itemKey, localValue, onValidationError]);
1151
+ // If not in edit mode, show formatted read-only text
1152
+ if (!isEditMode) {
1153
+ const locale = column.locale || 'en-US';
1154
+ const currencyCode = column.currencyCode || 'USD';
1155
+ let displayValue;
1156
+ if (column.formatValue) {
1157
+ displayValue = column.formatValue(value, item);
1158
+ }
1159
+ else if (numericValue !== null) {
1160
+ displayValue = isCurrency || column.fieldType === 'currency'
1161
+ ? formatCurrency(numericValue, currencyCode, locale)
1162
+ : formatNumber(numericValue, locale);
1163
+ }
1164
+ else {
1165
+ displayValue = '-';
1166
+ }
1167
+ return (jsx("div", { className: editableCellClass, children: jsx("span", { className: mergeStyles(readOnlyClass$1, isCurrency || column.fieldType === 'currency' ? currencyCellClass : ''), title: displayValue !== '-' ? displayValue : undefined, children: displayValue }) }));
1168
+ }
1169
+ // Determine indicator class
1170
+ let indicatorClass = '';
1171
+ if (errorMessage) {
1172
+ indicatorClass = errorCellIndicatorClass;
1173
+ }
1174
+ else if (isDirty) {
1175
+ indicatorClass = dirtyCellIndicatorClass;
1176
+ }
1177
+ const wrapperClass = mergeStyles(editableCellInputWrapperClass, indicatorClass);
1178
+ return (jsx("div", { className: editableCellClass, children: jsxs("div", { className: wrapperClass, children: [jsx(TextField, { componentRef: inputRef, value: localValue, onChange: handleChange, onBlur: handleBlur, borderless: true, styles: errorMessage ? numberFieldErrorStyles : numberFieldStyles, ariaLabel: `${column.name} for ${item.key}`, type: "text", inputMode: "decimal" }), errorMessage && (jsx("span", { className: cellErrorTextClass, title: errorMessage, children: errorMessage }))] }) }));
1179
+ }
1180
+
1181
+ // Styles for the DatePicker in edit mode
1182
+ const datePickerStyles = {
1183
+ root: {
1184
+ width: '100%',
1185
+ },
1186
+ textField: {
1187
+ fieldGroup: {
1188
+ height: 28,
1189
+ minHeight: 28,
1190
+ border: 'none',
1191
+ backgroundColor: 'transparent',
1192
+ selectors: {
1193
+ ':hover': {
1194
+ border: '1px solid #c8c6c4',
1195
+ backgroundColor: 'white',
1196
+ },
1197
+ ':focus-within': {
1198
+ border: '1px solid #0078d4',
1199
+ backgroundColor: 'white',
1200
+ },
1201
+ },
1202
+ },
1203
+ field: {
1204
+ fontSize: 13,
1205
+ padding: '0 8px',
1206
+ lineHeight: '28px',
1207
+ },
1208
+ },
1209
+ };
1210
+ const datePickerErrorStyles = {
1211
+ ...datePickerStyles,
1212
+ textField: {
1213
+ ...datePickerStyles.textField,
1214
+ fieldGroup: {
1215
+ ...datePickerStyles.textField.fieldGroup,
1216
+ border: '1px solid #d13438',
1217
+ selectors: {
1218
+ ':hover': {
1219
+ border: '1px solid #d13438',
1220
+ backgroundColor: 'white',
1221
+ },
1222
+ ':focus-within': {
1223
+ border: '1px solid #d13438',
1224
+ backgroundColor: 'white',
1225
+ },
1226
+ },
1227
+ },
1228
+ },
1229
+ };
1230
+ // Narrower DatePicker when shown alongside time picker
1231
+ const dateTimePickerStyles = {
1232
+ ...datePickerStyles,
1233
+ root: {
1234
+ flex: '1 1 60%',
1235
+ minWidth: 0,
1236
+ },
1237
+ };
1238
+ const dateTimePickerErrorStyles = {
1239
+ ...datePickerErrorStyles,
1240
+ root: {
1241
+ flex: '1 1 60%',
1242
+ minWidth: 0,
1243
+ },
1244
+ };
1245
+ // Styles for the time ComboBox -- match DatePicker's 28px height exactly
1246
+ const timeComboBoxStyles = {
1247
+ root: {
1248
+ flex: '1 1 40%',
1249
+ minWidth: 0,
1250
+ height: 28,
1251
+ },
1252
+ container: {
1253
+ height: 28,
1254
+ minHeight: 28,
1255
+ selectors: {
1256
+ '::after': {
1257
+ // Remove Fluent's default focus border (we handle it ourselves)
1258
+ border: 'none',
1259
+ },
1260
+ },
1261
+ },
1262
+ input: {
1263
+ height: 26,
1264
+ fontSize: 13,
1265
+ padding: '0 0 0 8px',
1266
+ boxSizing: 'border-box',
1267
+ },
1268
+ rootHovered: {
1269
+ border: '1px solid #c8c6c4',
1270
+ backgroundColor: 'white',
1271
+ },
1272
+ rootFocused: {
1273
+ border: '1px solid #0078d4',
1274
+ backgroundColor: 'white',
1275
+ },
1276
+ rootPressed: {
1277
+ border: '1px solid #0078d4',
1278
+ backgroundColor: 'white',
1279
+ },
1280
+ };
1281
+ const dateTimeRowClass = mergeStyles({
1282
+ display: 'flex',
1283
+ gap: 4,
1284
+ width: '100%',
1285
+ alignItems: 'flex-start',
1286
+ });
1287
+ /**
1288
+ * Build time options in 15-minute increments (12-hour format).
1289
+ */
1290
+ function buildTimeOptions() {
1291
+ const options = [];
1292
+ for (let h = 0; h < 24; h++) {
1293
+ for (let m = 0; m < 60; m += 15) {
1294
+ const hour12 = h === 0 ? 12 : h > 12 ? h - 12 : h;
1295
+ const ampm = h < 12 ? 'AM' : 'PM';
1296
+ const label = `${hour12}:${m.toString().padStart(2, '0')} ${ampm}`;
1297
+ const key = `${h.toString().padStart(2, '0')}:${m.toString().padStart(2, '0')}`;
1298
+ options.push({ key, text: label });
1299
+ }
1300
+ }
1301
+ return options;
1302
+ }
1303
+ const TIME_OPTIONS = buildTimeOptions();
1304
+ /**
1305
+ * Parse an ISO string or Date into a Date object. Returns null if invalid.
1306
+ */
1307
+ function parseToDate(value) {
1308
+ if (!value)
1309
+ return null;
1310
+ if (value instanceof Date)
1311
+ return isNaN(value.getTime()) ? null : value;
1312
+ const d = new Date(String(value));
1313
+ return isNaN(d.getTime()) ? null : d;
1314
+ }
1315
+ /**
1316
+ * Format a Date for display in the DatePicker.
1317
+ */
1318
+ function formatDateForDisplay(date) {
1319
+ if (!date)
1320
+ return '';
1321
+ return date.toLocaleDateString('en-US', {
1322
+ year: 'numeric',
1323
+ month: 'short',
1324
+ day: 'numeric',
1325
+ });
1326
+ }
1327
+ /**
1328
+ * Get HH:MM key from a Date for the time ComboBox.
1329
+ * Rounds to nearest 15-minute increment.
1330
+ */
1331
+ function getTimeKey(date) {
1332
+ const h = date.getHours();
1333
+ const m = Math.round(date.getMinutes() / 15) * 15;
1334
+ const adjustedH = m === 60 ? h + 1 : h;
1335
+ const adjustedM = m === 60 ? 0 : m;
1336
+ const finalH = adjustedH % 24;
1337
+ return `${finalH.toString().padStart(2, '0')}:${adjustedM.toString().padStart(2, '0')}`;
1338
+ }
1339
+ /**
1340
+ * Build an ISO date-only string (YYYY-MM-DD) from a Date.
1341
+ */
1342
+ function toDateOnlyISO(date) {
1343
+ const y = date.getFullYear();
1344
+ const m = (date.getMonth() + 1).toString().padStart(2, '0');
1345
+ const d = date.getDate().toString().padStart(2, '0');
1346
+ return `${y}-${m}-${d}`;
1347
+ }
1348
+ /**
1349
+ * Build an ISO datetime string from a Date.
1350
+ */
1351
+ function toDateTimeISO(date) {
1352
+ return date.toISOString();
1353
+ }
1354
+ /**
1355
+ * Editable date/datetime cell renderer.
1356
+ * - Date-only fields: Fluent UI DatePicker (calendar popup)
1357
+ * - DateTime fields: DatePicker + time ComboBox side by side
1358
+ * Shows blue indicator when dirty, red indicator when has validation error.
1359
+ */
1360
+ function EditableDateCell({ item, column, value, itemKey, isEditMode, isDirty, editedValue, onValueChange, errorMessage, onValidationError, }) {
1361
+ const fieldName = column.fieldName || column.key;
1362
+ const currentValue = editedValue !== undefined ? editedValue : value;
1363
+ const isDateTime = column.fieldType === 'datetime';
1364
+ // Parse current value to a Date
1365
+ const currentDate = useMemo(() => parseToDate(currentValue), [currentValue]);
1366
+ // Current time key for the time ComboBox
1367
+ const currentTimeKey = useMemo(() => (currentDate ? getTimeKey(currentDate) : '09:00'), [currentDate]);
1368
+ // Validate helper
1369
+ const runValidation = useCallback((newValue) => {
1370
+ const col = column;
1371
+ if (col.validate && onValidationError) {
1372
+ const error = col.validate(newValue, itemKey);
1373
+ onValidationError(itemKey, fieldName, error);
1374
+ }
1375
+ }, [column, fieldName, itemKey, onValidationError]);
1376
+ // Handle date selection from DatePicker
1377
+ const handleDateSelect = useCallback((date) => {
1378
+ if (!date) {
1379
+ // Cleared
1380
+ runValidation(null);
1381
+ onValueChange(itemKey, fieldName, null, value);
1382
+ return;
1383
+ }
1384
+ if (isDateTime) {
1385
+ // Preserve existing time or default to 09:00
1386
+ const existingDate = parseToDate(currentValue);
1387
+ if (existingDate) {
1388
+ date.setHours(existingDate.getHours(), existingDate.getMinutes(), 0, 0);
1389
+ }
1390
+ else {
1391
+ date.setHours(9, 0, 0, 0);
1392
+ }
1393
+ const isoValue = toDateTimeISO(date);
1394
+ runValidation(isoValue);
1395
+ onValueChange(itemKey, fieldName, isoValue, value);
1396
+ }
1397
+ else {
1398
+ const isoValue = toDateOnlyISO(date);
1399
+ runValidation(isoValue);
1400
+ onValueChange(itemKey, fieldName, isoValue, value);
1401
+ }
1402
+ }, [isDateTime, currentValue, itemKey, fieldName, value, onValueChange, runValidation]);
1403
+ // Handle time selection from ComboBox
1404
+ const handleTimeChange = useCallback((_, option, _index, text) => {
1405
+ // Determine the time key from selection or typed text
1406
+ let timeKey = option?.key;
1407
+ if (!timeKey && text) {
1408
+ // Try to parse typed text like "2:30 PM"
1409
+ const match = text.match(/^(\d{1,2}):(\d{2})\s*(AM|PM)?$/i);
1410
+ if (match) {
1411
+ let h = parseInt(match[1], 10);
1412
+ const m = parseInt(match[2], 10);
1413
+ const ampm = match[3]?.toUpperCase();
1414
+ if (ampm === 'PM' && h < 12)
1415
+ h += 12;
1416
+ if (ampm === 'AM' && h === 12)
1417
+ h = 0;
1418
+ timeKey = `${h.toString().padStart(2, '0')}:${m.toString().padStart(2, '0')}`;
1419
+ }
1420
+ }
1421
+ if (!timeKey)
1422
+ return;
1423
+ const [hours, minutes] = timeKey.split(':').map(Number);
1424
+ const baseDate = parseToDate(currentValue) || new Date();
1425
+ const newDate = new Date(baseDate);
1426
+ newDate.setHours(hours, minutes, 0, 0);
1427
+ const isoValue = toDateTimeISO(newDate);
1428
+ runValidation(isoValue);
1429
+ onValueChange(itemKey, fieldName, isoValue, value);
1430
+ }, [currentValue, itemKey, fieldName, value, onValueChange, runValidation]);
1431
+ // If not in edit mode, delegate to DateCell
1432
+ if (!isEditMode) {
1433
+ return (jsx(DateCell, { item: item, column: column, value: value, itemKey: itemKey }));
1434
+ }
1435
+ // Determine indicator class
1436
+ let indicatorClass = '';
1437
+ if (errorMessage) {
1438
+ indicatorClass = errorCellIndicatorClass;
1439
+ }
1440
+ else if (isDirty) {
1441
+ indicatorClass = dirtyCellIndicatorClass;
1442
+ }
1443
+ const wrapperClass = mergeStyles(editableCellInputWrapperClass, indicatorClass);
1444
+ if (isDateTime) {
1445
+ // DateTime: DatePicker + Time ComboBox side by side
1446
+ return (jsx("div", { className: editableCellClass, children: jsxs("div", { className: wrapperClass, children: [jsxs("div", { className: dateTimeRowClass, children: [jsx(DatePicker, { value: currentDate || undefined, onSelectDate: handleDateSelect, formatDate: formatDateForDisplay, placeholder: "Select date", styles: errorMessage ? dateTimePickerErrorStyles : dateTimePickerStyles, ariaLabel: `${column.name} date for ${item.key}`, allowTextInput: true }), jsx(ComboBox, { selectedKey: currentDate ? currentTimeKey : undefined, options: TIME_OPTIONS, onChange: handleTimeChange, placeholder: "Time", styles: timeComboBoxStyles, ariaLabel: `${column.name} time for ${item.key}`, allowFreeform: true, autoComplete: "on" })] }), errorMessage && (jsx("span", { className: cellErrorTextClass, title: errorMessage, children: errorMessage }))] }) }));
1447
+ }
1448
+ // Date-only: DatePicker
1449
+ return (jsx("div", { className: editableCellClass, children: jsxs("div", { className: wrapperClass, children: [jsx(DatePicker, { value: currentDate || undefined, onSelectDate: handleDateSelect, formatDate: formatDateForDisplay, placeholder: "Select date", styles: errorMessage ? datePickerErrorStyles : datePickerStyles, ariaLabel: `${column.name} for ${item.key}`, allowTextInput: true }), errorMessage && (jsx("span", { className: cellErrorTextClass, title: errorMessage, children: errorMessage }))] }) }));
1450
+ }
1451
+
1452
+ // Styles for the dropdown in edit mode
1453
+ const dropdownStyles = {
1454
+ root: {
1455
+ width: '100%',
1456
+ },
1457
+ dropdown: {
1458
+ height: 28,
1459
+ minHeight: 28,
1460
+ border: 'none',
1461
+ backgroundColor: 'transparent',
1462
+ selectors: {
1463
+ ':hover': {
1464
+ border: '1px solid #c8c6c4',
1465
+ backgroundColor: 'white',
1466
+ },
1467
+ ':focus::after': {
1468
+ border: '1px solid #0078d4',
1469
+ },
1470
+ },
1471
+ },
1472
+ title: {
1473
+ height: 28,
1474
+ lineHeight: '26px',
1475
+ fontSize: 13,
1476
+ border: 'none',
1477
+ backgroundColor: 'transparent',
1478
+ },
1479
+ caretDownWrapper: {
1480
+ height: 28,
1481
+ lineHeight: '28px',
1482
+ },
1483
+ dropdownItem: {
1484
+ fontSize: 13,
1485
+ minHeight: 32,
1486
+ },
1487
+ dropdownItemSelected: {
1488
+ fontSize: 13,
1489
+ minHeight: 32,
1490
+ },
1491
+ };
1492
+ const dropdownErrorStyles = {
1493
+ ...dropdownStyles,
1494
+ dropdown: {
1495
+ ...dropdownStyles.dropdown,
1496
+ border: '1px solid #d13438',
1497
+ selectors: {
1498
+ ':hover': {
1499
+ border: '1px solid #d13438',
1500
+ backgroundColor: 'white',
1501
+ },
1502
+ ':focus::after': {
1503
+ border: '1px solid #d13438',
1504
+ },
1505
+ },
1506
+ },
1507
+ };
1508
+ /**
1509
+ * Editable option set cell renderer.
1510
+ * Displays as colored badge (OptionSetCell) in read mode, as Dropdown in edit mode.
1511
+ * Shows blue indicator when dirty, red indicator when has validation error.
1512
+ */
1513
+ function EditableOptionSetCell({ item, column, value, itemKey, isEditMode, isDirty, editedValue, onValueChange, errorMessage, onValidationError, }) {
1514
+ const fieldName = column.fieldName || column.key;
1515
+ const currentValue = editedValue !== undefined ? editedValue : value;
1516
+ const isMulti = !!column.optionSetMultiSelect;
1517
+ const hasIcons = column.optionSetOptions?.some((o) => !!o.iconName) ?? false;
1518
+ // Build dropdown options. Keys preserve the option's value type (number or
1519
+ // string) — no Number() coercion. Single-select prepends a (None) option;
1520
+ // multi-select clears by de-selecting. `data` carries icon/color for rendering.
1521
+ const dropdownOptions = useMemo(() => {
1522
+ const options = isMulti ? [] : [{ key: '__none__', text: '(None)' }];
1523
+ column.optionSetOptions?.forEach((opt) => {
1524
+ options.push({
1525
+ key: opt.value,
1526
+ text: opt.label,
1527
+ data: { iconName: opt.iconName, color: opt.color },
1528
+ });
1529
+ });
1530
+ return options;
1531
+ }, [column.optionSetOptions, isMulti]);
1532
+ const validate = useCallback((next) => {
1533
+ const col = column;
1534
+ if (col.validate && onValidationError) {
1535
+ onValidationError(itemKey, fieldName, col.validate(next, itemKey));
1536
+ }
1537
+ }, [column, fieldName, itemKey, onValidationError]);
1538
+ // Single-select change → the picked value (type preserved) or null for (None).
1539
+ const handleChangeSingle = useCallback((_, option) => {
1540
+ if (!option)
1541
+ return;
1542
+ const newValue = option.key === '__none__' ? null : option.key;
1543
+ validate(newValue);
1544
+ onValueChange(itemKey, fieldName, newValue, value);
1545
+ }, [fieldName, itemKey, value, onValueChange, validate]);
1546
+ // Multi-select change → toggle the option into/out of the value array.
1547
+ const handleChangeMulti = useCallback((_, option) => {
1548
+ if (!option)
1549
+ return;
1550
+ const prev = Array.isArray(currentValue) ? [...currentValue] : [];
1551
+ const next = option.selected
1552
+ ? [...prev, option.key]
1553
+ : prev.filter((k) => String(k) !== String(option.key));
1554
+ validate(next);
1555
+ onValueChange(itemKey, fieldName, next, value);
1556
+ }, [currentValue, fieldName, itemKey, value, onValueChange, validate]);
1557
+ // If not in edit mode, delegate to OptionSetCell for the colored badge display.
1558
+ if (!isEditMode) {
1559
+ return jsx(OptionSetCell, { item: item, column: column, value: value, itemKey: itemKey });
1560
+ }
1561
+ // Render per-option icon + label (only wired when some option has an icon).
1562
+ const onRenderOption = (option) => {
1563
+ if (!option)
1564
+ return null;
1565
+ const data = option.data;
1566
+ return (jsxs("span", { style: { display: 'inline-flex', alignItems: 'center', gap: 6 }, children: [data?.iconName && jsx(Icon, { iconName: data.iconName, style: { color: data.color, fontSize: 14 } }), jsx("span", { children: option.text })] }));
1567
+ };
1568
+ let indicatorClass = '';
1569
+ if (errorMessage) {
1570
+ indicatorClass = errorCellIndicatorClass;
1571
+ }
1572
+ else if (isDirty) {
1573
+ indicatorClass = dirtyCellIndicatorClass;
1574
+ }
1575
+ const wrapperClass = mergeStyles(editableCellInputWrapperClass, indicatorClass);
1576
+ const selectedKey = currentValue !== null && currentValue !== undefined ? currentValue : '__none__';
1577
+ // Cast to Fluent's homogeneous-array prop type; an optionset's keys are all the
1578
+ // same type at runtime (all numeric or all string), so this is safe.
1579
+ const selectedKeys = (Array.isArray(currentValue)
1580
+ ? currentValue
1581
+ : []);
1582
+ return (jsx("div", { className: editableCellClass, children: jsxs("div", { className: wrapperClass, children: [jsx(Dropdown, { multiSelect: isMulti || undefined, selectedKey: isMulti ? undefined : selectedKey, selectedKeys: isMulti ? selectedKeys : undefined, options: dropdownOptions, onChange: isMulti ? handleChangeMulti : handleChangeSingle, onRenderOption: hasIcons ? onRenderOption : undefined, styles: errorMessage ? dropdownErrorStyles : dropdownStyles, ariaLabel: `${column.name} for ${item.key}` }), errorMessage && (jsx("span", { className: cellErrorTextClass, title: errorMessage, children: errorMessage }))] }) }));
1583
+ }
1584
+
1585
+ /** Normalize a consumer search result to a `LookupOption` (accepts `{id,name}` or `{key,text}`). */
1586
+ function normalizeLookupOption(o) {
1587
+ if (o && typeof o === 'object' && 'id' in o) {
1588
+ return { id: String(o.id), name: String(o.name ?? '') };
1589
+ }
1590
+ const alt = o;
1591
+ return { id: String(alt?.key ?? ''), name: String(alt?.text ?? '') };
1592
+ }
1593
+ // Styles for the search input in edit mode
1594
+ const lookupFieldStyles = {
1595
+ root: {
1596
+ width: '100%',
1597
+ },
1598
+ fieldGroup: {
1599
+ height: 28,
1600
+ minHeight: 28,
1601
+ border: 'none',
1602
+ backgroundColor: 'transparent',
1603
+ selectors: {
1604
+ ':hover': {
1605
+ border: '1px solid #c8c6c4',
1606
+ backgroundColor: 'white',
1607
+ },
1608
+ ':focus-within': {
1609
+ border: '1px solid #0078d4',
1610
+ backgroundColor: 'white',
1611
+ },
1612
+ },
1613
+ },
1614
+ field: {
1615
+ fontSize: 13,
1616
+ padding: '0 28px 0 8px', // Extra padding for clear button
1617
+ },
1618
+ };
1619
+ const lookupFieldErrorStyles = {
1620
+ ...lookupFieldStyles,
1621
+ fieldGroup: {
1622
+ ...lookupFieldStyles.fieldGroup,
1623
+ border: '1px solid #d13438',
1624
+ selectors: {
1625
+ ':hover': {
1626
+ border: '1px solid #d13438',
1627
+ backgroundColor: 'white',
1628
+ },
1629
+ ':focus-within': {
1630
+ border: '1px solid #d13438',
1631
+ backgroundColor: 'white',
1632
+ },
1633
+ },
1634
+ },
1635
+ };
1636
+ const readOnlyClass = mergeStyles({
1637
+ overflow: 'hidden',
1638
+ textOverflow: 'ellipsis',
1639
+ whiteSpace: 'nowrap',
1640
+ fontSize: 13,
1641
+ width: '100%',
1642
+ });
1643
+ /**
1644
+ * Extract the lookup display value from the item.
1645
+ * Handles various formats: OData annotations, expanded objects, formatted values.
1646
+ */
1647
+ function extractLookupDisplayValue(item, column, value) {
1648
+ const fieldName = column.fieldName || column.key;
1649
+ // 1. Check for custom format function
1650
+ if (column.formatValue) {
1651
+ const formatted = column.formatValue(value, item);
1652
+ if (formatted && formatted !== '')
1653
+ return formatted;
1654
+ }
1655
+ // 2. Check for OData formatted value annotation on lookup
1656
+ const formattedKey = `_${fieldName}_value@OData.Community.Display.V1.FormattedValue`;
1657
+ const itemRecord = item;
1658
+ if (itemRecord[formattedKey] !== undefined) {
1659
+ return String(itemRecord[formattedKey]);
1660
+ }
1661
+ // 3. Check for lookupDisplayField on the item
1662
+ if (column.lookupDisplayField && itemRecord[column.lookupDisplayField] !== undefined) {
1663
+ return String(itemRecord[column.lookupDisplayField]);
1664
+ }
1665
+ // 4. Check for expanded lookup object
1666
+ if (value && typeof value === 'object') {
1667
+ const expanded = value;
1668
+ // Try common display field patterns
1669
+ if (expanded.fullname)
1670
+ return String(expanded.fullname);
1671
+ if (expanded.name)
1672
+ return String(expanded.name);
1673
+ if (expanded.title)
1674
+ return String(expanded.title);
1675
+ }
1676
+ return '';
1677
+ }
1678
+ /**
1679
+ * Extract the lookup ID value from the item.
1680
+ */
1681
+ function extractLookupId(item, column, value) {
1682
+ const fieldName = column.fieldName || column.key;
1683
+ const itemRecord = item;
1684
+ // Check for _fieldname_value format (OData lookup reference)
1685
+ const lookupValueKey = `_${fieldName}_value`;
1686
+ if (itemRecord[lookupValueKey]) {
1687
+ return String(itemRecord[lookupValueKey]);
1688
+ }
1689
+ // Check expanded object
1690
+ if (value && typeof value === 'object') {
1691
+ const expanded = value;
1692
+ // Try to find the ID from expanded lookup
1693
+ const idField = column.lookupPrimaryIdAttribute || 'id';
1694
+ if (expanded[idField])
1695
+ return String(expanded[idField]);
1696
+ // Check for common ID patterns
1697
+ if (expanded.contactid)
1698
+ return String(expanded.contactid);
1699
+ if (expanded.accountid)
1700
+ return String(expanded.accountid);
1701
+ if (expanded.systemuserid)
1702
+ return String(expanded.systemuserid);
1703
+ }
1704
+ return null;
1705
+ }
1706
+ /**
1707
+ * Editable lookup cell renderer.
1708
+ * Displays as a clickable link in read mode, as a searchable dropdown in edit mode.
1709
+ * Shows blue indicator when dirty, red indicator when has validation error.
1710
+ */
1711
+ function EditableLookupCell({ item, column, value, itemKey, isEditMode, isDirty, editedValue, onValueChange, errorMessage, onValidationError, apiService, dynamicsBaseUrl, searchLookup, }) {
1712
+ const fieldName = column.fieldName || column.key;
1713
+ // Prefer the consumer search callback (direct prop or column config) over apiService.
1714
+ const search = searchLookup ?? column.lookupSearch;
1715
+ // Extract current lookup value
1716
+ const originalDisplayValue = extractLookupDisplayValue(item, column, value);
1717
+ const originalId = extractLookupId(item, column, value);
1718
+ // Current value (edited or original)
1719
+ const currentValue = useMemo(() => {
1720
+ if (editedValue !== undefined) {
1721
+ return editedValue;
1722
+ }
1723
+ if (originalId && originalDisplayValue) {
1724
+ return { id: originalId, name: originalDisplayValue };
1725
+ }
1726
+ return null;
1727
+ }, [editedValue, originalId, originalDisplayValue]);
1728
+ // Local state
1729
+ // Initialize with original display value to ensure it shows immediately
1730
+ const initialDisplayValue = currentValue?.name || originalDisplayValue || '';
1731
+ const [searchText, setSearchText] = useState(initialDisplayValue);
1732
+ const [isDropdownOpen, setIsDropdownOpen] = useState(false);
1733
+ const [searchResults, setSearchResults] = useState([]);
1734
+ const [isSearching, setIsSearching] = useState(false);
1735
+ const [activeIndex, setActiveIndex] = useState(-1);
1736
+ const [hasSearched, setHasSearched] = useState(false);
1737
+ const inputRef = useRef(null);
1738
+ const containerRef = useRef(null);
1739
+ const debounceTimerRef = useRef(null);
1740
+ // Sync search text when edited value changes OR when initial value is available
1741
+ useEffect(() => {
1742
+ const displayValue = currentValue?.name || originalDisplayValue || '';
1743
+ setSearchText(displayValue);
1744
+ }, [currentValue, originalDisplayValue]);
1745
+ // Handle Callout dismiss
1746
+ const handleCalloutDismiss = useCallback(() => {
1747
+ setIsDropdownOpen(false);
1748
+ }, []);
1749
+ // Execute the search API call
1750
+ const executeSearchQuery = useCallback(async (query, useContains) => {
1751
+ const searchField = column.lookupSearchField || 'name';
1752
+ const primaryIdAttr = column.lookupPrimaryIdAttribute || `${column.lookupTargetEntity}id`;
1753
+ const additionalFields = column.lookupSelectFields || '';
1754
+ // Build select fields
1755
+ const selectFields = [primaryIdAttr, searchField];
1756
+ if (additionalFields) {
1757
+ additionalFields.split(',').forEach(f => {
1758
+ const trimmed = f.trim();
1759
+ if (trimmed && !selectFields.includes(trimmed)) {
1760
+ selectFields.push(trimmed);
1761
+ }
1762
+ });
1763
+ }
1764
+ // Build OData query
1765
+ let options = `?$select=${selectFields.join(',')}&$top=15&$orderby=${searchField} asc`;
1766
+ // Build filter conditions
1767
+ const filterConditions = [];
1768
+ // Add search filter if query is provided
1769
+ if (query.trim()) {
1770
+ const escapedQuery = query.replace(/'/g, "''");
1771
+ // Use contains for more flexible matching (finds "Nancy" anywhere in the name)
1772
+ // startswith is faster but less flexible
1773
+ if (useContains) {
1774
+ filterConditions.push(`contains(${searchField}, '${escapedQuery}')`);
1775
+ }
1776
+ else {
1777
+ filterConditions.push(`startswith(${searchField}, '${escapedQuery}')`);
1778
+ }
1779
+ }
1780
+ // Add custom filter from column config (e.g., "statecode eq 0" for active only)
1781
+ if (column.lookupFilter) {
1782
+ filterConditions.push(`(${column.lookupFilter})`);
1783
+ }
1784
+ // Combine filters with 'and'
1785
+ if (filterConditions.length > 0) {
1786
+ options += `&$filter=${filterConditions.join(' and ')}`;
1787
+ }
1788
+ const result = await apiService.retrieveMultipleRecords(column.lookupEntitySetName, options);
1789
+ return result.value.map((record) => ({
1790
+ id: String(record[primaryIdAttr]),
1791
+ name: String(record[searchField] || ''),
1792
+ }));
1793
+ }, [apiService, column.lookupEntitySetName, column.lookupSearchField, column.lookupPrimaryIdAttribute, column.lookupTargetEntity, column.lookupFilter, column.lookupSelectFields]);
1794
+ // Search for lookup records
1795
+ const performSearch = useCallback(async (query) => {
1796
+ // Fetch-free path: a consumer-supplied search callback (grid-kit's contract).
1797
+ // The consumer owns any startswith/contains fallback server-side.
1798
+ if (search) {
1799
+ setIsSearching(true);
1800
+ setHasSearched(true);
1801
+ try {
1802
+ const raw = await search(query);
1803
+ setSearchResults(raw.map(normalizeLookupOption));
1804
+ setActiveIndex(-1);
1805
+ }
1806
+ catch (err) {
1807
+ console.error('[EditableLookupCell] searchLookup error:', err);
1808
+ setSearchResults([]);
1809
+ }
1810
+ finally {
1811
+ setIsSearching(false);
1812
+ }
1813
+ return;
1814
+ }
1815
+ // apiService path (unchanged).
1816
+ if (!apiService || !column.lookupEntitySetName) {
1817
+ setSearchResults([]);
1818
+ return;
1819
+ }
1820
+ setIsSearching(true);
1821
+ setHasSearched(true);
1822
+ try {
1823
+ // First try with startswith (faster)
1824
+ let results = await executeSearchQuery(query, false);
1825
+ // If no results and we have a search query, try with contains (more flexible)
1826
+ if (results.length === 0 && query.trim()) {
1827
+ results = await executeSearchQuery(query, true);
1828
+ }
1829
+ setSearchResults(results);
1830
+ setActiveIndex(-1);
1831
+ }
1832
+ catch (err) {
1833
+ console.error('[EditableLookupCell] Search error:', err);
1834
+ setSearchResults([]);
1835
+ }
1836
+ finally {
1837
+ setIsSearching(false);
1838
+ }
1839
+ }, [search, apiService, column.lookupEntitySetName, executeSearchQuery]);
1840
+ // Debounced search
1841
+ const debouncedSearch = useCallback((query) => {
1842
+ if (debounceTimerRef.current) {
1843
+ window.clearTimeout(debounceTimerRef.current);
1844
+ }
1845
+ debounceTimerRef.current = window.setTimeout(() => {
1846
+ performSearch(query);
1847
+ }, 300);
1848
+ }, [performSearch]);
1849
+ // Handle input change
1850
+ const handleInputChange = useCallback((_, newValue) => {
1851
+ const val = newValue ?? '';
1852
+ setSearchText(val);
1853
+ // Open dropdown and trigger search
1854
+ setIsDropdownOpen(true);
1855
+ debouncedSearch(val);
1856
+ }, [debouncedSearch]);
1857
+ // Handle input focus - always load initial options
1858
+ const handleFocus = useCallback(() => {
1859
+ setIsDropdownOpen(true);
1860
+ // Always do an empty search on focus to populate with available options
1861
+ // This ensures users see options even if there's a current value
1862
+ if (!hasSearched || searchResults.length === 0) {
1863
+ // Search with empty string to get initial list of all options
1864
+ performSearch('');
1865
+ }
1866
+ }, [hasSearched, performSearch, searchResults.length]);
1867
+ // Handle selection of a lookup option
1868
+ const handleSelect = useCallback((option) => {
1869
+ setIsDropdownOpen(false);
1870
+ if (option === null) {
1871
+ // Clear selection
1872
+ setSearchText('');
1873
+ onValueChange(itemKey, fieldName, null, value);
1874
+ }
1875
+ else {
1876
+ // Set new selection
1877
+ setSearchText(option.name);
1878
+ const newValue = {
1879
+ id: option.id,
1880
+ name: option.name,
1881
+ entitySetName: column.lookupEntitySetName,
1882
+ };
1883
+ onValueChange(itemKey, fieldName, newValue, value);
1884
+ }
1885
+ // Clear any validation errors
1886
+ if (onValidationError) {
1887
+ onValidationError(itemKey, fieldName, undefined);
1888
+ }
1889
+ }, [itemKey, fieldName, value, column.lookupEntitySetName, onValueChange, onValidationError]);
1890
+ // Handle keyboard navigation
1891
+ const handleKeyDown = useCallback((event) => {
1892
+ if (!isDropdownOpen) {
1893
+ if (event.key === 'ArrowDown' || event.key === 'Enter') {
1894
+ setIsDropdownOpen(true);
1895
+ if (!hasSearched) {
1896
+ performSearch(searchText);
1897
+ }
1898
+ }
1899
+ return;
1900
+ }
1901
+ // Total items: 1 (None option) + searchResults.length
1902
+ const totalItems = searchResults.length + 1;
1903
+ switch (event.key) {
1904
+ case 'ArrowDown':
1905
+ event.preventDefault();
1906
+ setActiveIndex((prev) => (prev < totalItems - 1 ? prev + 1 : prev));
1907
+ break;
1908
+ case 'ArrowUp':
1909
+ event.preventDefault();
1910
+ setActiveIndex((prev) => (prev > 0 ? prev - 1 : 0));
1911
+ break;
1912
+ case 'Enter':
1913
+ event.preventDefault();
1914
+ if (activeIndex === 0) {
1915
+ // None option selected
1916
+ handleSelect(null);
1917
+ }
1918
+ else if (activeIndex > 0 && activeIndex <= searchResults.length) {
1919
+ // Regular option selected
1920
+ handleSelect(searchResults[activeIndex - 1]);
1921
+ }
1922
+ else if (searchResults.length === 1) {
1923
+ // Auto-select single result
1924
+ handleSelect(searchResults[0]);
1925
+ }
1926
+ break;
1927
+ case 'Tab':
1928
+ // Select active or first result on tab
1929
+ if (activeIndex === 0) {
1930
+ handleSelect(null);
1931
+ }
1932
+ else if (activeIndex > 0 && activeIndex <= searchResults.length) {
1933
+ handleSelect(searchResults[activeIndex - 1]);
1934
+ }
1935
+ else if (searchResults.length > 0) {
1936
+ handleSelect(searchResults[0]);
1937
+ }
1938
+ setIsDropdownOpen(false);
1939
+ break;
1940
+ case 'Escape':
1941
+ event.preventDefault();
1942
+ setIsDropdownOpen(false);
1943
+ setSearchText(currentValue?.name || '');
1944
+ break;
1945
+ }
1946
+ }, [isDropdownOpen, activeIndex, searchResults, hasSearched, performSearch, searchText, handleSelect, currentValue]);
1947
+ // Handle clear button click
1948
+ const handleClear = useCallback((event) => {
1949
+ event.stopPropagation();
1950
+ handleSelect(null);
1951
+ }, [handleSelect]);
1952
+ // Handle click on read-mode link (open in Dynamics)
1953
+ const handleLinkClick = useCallback(() => {
1954
+ if (!dynamicsBaseUrl || !currentValue?.id || !column.lookupTargetEntity)
1955
+ return;
1956
+ const url = `${dynamicsBaseUrl}/main.aspx?etn=${column.lookupTargetEntity}&id=${currentValue.id}&pagetype=entityrecord`;
1957
+ window.open(url, '_blank');
1958
+ }, [dynamicsBaseUrl, currentValue, column.lookupTargetEntity]);
1959
+ // Handle link keydown for accessibility
1960
+ const handleLinkKeyDown = useCallback((event) => {
1961
+ if (event.key === 'Enter' || event.key === ' ') {
1962
+ event.preventDefault();
1963
+ handleLinkClick();
1964
+ }
1965
+ }, [handleLinkClick]);
1966
+ // ============================================================================
1967
+ // Read Mode Rendering
1968
+ // ============================================================================
1969
+ if (!isEditMode) {
1970
+ const displayValue = currentValue?.name || originalDisplayValue || '-';
1971
+ // Consumer-supplied click handler (e.g. grid-kit navigation) wins over the
1972
+ // built-in dynamicsBaseUrl link — keeps read-mode parity with LookupCell.
1973
+ if (column.onLinkClick && displayValue !== '-') {
1974
+ const onClick = () => column.onLinkClick(item);
1975
+ return (jsx("div", { className: editableCellClass, children: jsx("span", { className: lookupReadModeLinkClass, onClick: onClick, onKeyDown: (e) => {
1976
+ if (e.key === 'Enter' || e.key === ' ') {
1977
+ e.preventDefault();
1978
+ onClick();
1979
+ }
1980
+ }, role: "button", tabIndex: 0, title: displayValue, children: displayValue }) }));
1981
+ }
1982
+ // If we have a link and a valid lookup, show as clickable link
1983
+ if (dynamicsBaseUrl && column.lookupTargetEntity && currentValue?.id && displayValue !== '-') {
1984
+ return (jsx("div", { className: editableCellClass, children: jsx("span", { className: lookupReadModeLinkClass, onClick: handleLinkClick, onKeyDown: handleLinkKeyDown, role: "button", tabIndex: 0, title: `Open ${displayValue} in Dynamics 365`, children: displayValue }) }));
1985
+ }
1986
+ // Plain text display
1987
+ return (jsx("div", { className: editableCellClass, children: jsx("span", { className: readOnlyClass, title: displayValue !== '-' ? displayValue : undefined, children: displayValue }) }));
1988
+ }
1989
+ // ============================================================================
1990
+ // Edit Mode Rendering
1991
+ // ============================================================================
1992
+ // Determine indicator class
1993
+ let indicatorClass = '';
1994
+ if (errorMessage) {
1995
+ indicatorClass = errorCellIndicatorClass;
1996
+ }
1997
+ else if (isDirty) {
1998
+ indicatorClass = dirtyCellIndicatorClass;
1999
+ }
2000
+ const wrapperClass = mergeStyles(editableCellInputWrapperClass, indicatorClass);
2001
+ // Callout styles
2002
+ const calloutStyles = {
2003
+ root: {
2004
+ padding: 0,
2005
+ boxShadow: '0 3px 6px rgba(0, 0, 0, 0.16)',
2006
+ },
2007
+ calloutMain: {
2008
+ padding: 0,
2009
+ maxHeight: 200,
2010
+ overflowY: 'auto',
2011
+ minWidth: 200,
2012
+ },
2013
+ };
2014
+ return (jsx("div", { className: editableCellClass, children: jsxs("div", { className: wrapperClass, children: [jsxs("div", { className: lookupCellContainerClass, ref: containerRef, children: [jsx(TextField, { componentRef: inputRef, value: searchText, onChange: handleInputChange, onFocus: handleFocus, onKeyDown: handleKeyDown, borderless: true, styles: errorMessage ? lookupFieldErrorStyles : lookupFieldStyles, ariaLabel: `${column.name} lookup for ${item.key}`, placeholder: originalDisplayValue || "Search...", autoComplete: "off" }), searchText && (jsx("button", { className: lookupClearButtonClass, onClick: handleClear, type: "button", "aria-label": "Clear selection", tabIndex: -1, children: jsx(Icon, { iconName: "Cancel", style: { fontSize: 10 } }) }))] }), isDropdownOpen && containerRef.current && (jsxs(Callout, { target: containerRef.current, onDismiss: handleCalloutDismiss, isBeakVisible: false, directionalHint: DirectionalHint.bottomLeftEdge, styles: calloutStyles, calloutMaxHeight: 200, setInitialFocus: false, children: [jsx("div", { className: mergeStyles(lookupDropdownNoneOptionClass, activeIndex === 0 ? lookupDropdownItemActiveClass : ''), onClick: () => handleSelect(null), role: "option", "aria-selected": activeIndex === 0, children: "(None)" }), isSearching && (jsx("div", { className: lookupLoadingClass, children: "Searching..." })), !isSearching && searchResults.length > 0 && searchResults.map((option, index) => (jsx("div", { className: mergeStyles(lookupDropdownItemClass, activeIndex === index + 1 ? lookupDropdownItemActiveClass : ''), onClick: () => handleSelect(option), role: "option", "aria-selected": activeIndex === index + 1, children: option.name }, option.id))), !isSearching && hasSearched && searchResults.length === 0 && (jsx("div", { className: lookupNoResultsClass, children: "No results found" }))] })), errorMessage && (jsx("span", { className: cellErrorTextClass, title: errorMessage, children: errorMessage }))] }) }));
2015
+ }
2016
+
2017
+ const btnBaseStyle = {
2018
+ background: 'none',
2019
+ border: 'none',
2020
+ padding: 0,
2021
+ margin: 0,
2022
+ cursor: 'pointer',
2023
+ lineHeight: 1,
2024
+ };
2025
+ const starBtnStyle = { ...btnBaseStyle, fontSize: 16 };
2026
+ /**
2027
+ * Editable rating cell renderer.
2028
+ * Read mode delegates to `RatingCell` (stars or a single sentiment emoji);
2029
+ * edit mode renders clickable stars / faces (click to set, click the selected
2030
+ * one again to clear, arrow keys to adjust). Shows blue indicator when dirty,
2031
+ * red when it has a validation error.
2032
+ *
2033
+ * NOTE: `ratingStyle: 'emoji'` renders Fluent `<Icon>`s — the host must have
2034
+ * called `initializeIcons()`.
2035
+ */
2036
+ function EditableRatingCell({ item, column, value, itemKey, isEditMode, isDirty, editedValue, onValueChange, errorMessage, onValidationError, }) {
2037
+ const fieldName = column.fieldName || column.key;
2038
+ const isEmoji = column.ratingStyle === 'emoji';
2039
+ const count = isEmoji
2040
+ ? clampFaceCount(column.ratingFaceCount, column.ratingMax)
2041
+ : column.ratingMax && column.ratingMax > 0
2042
+ ? Math.floor(column.ratingMax)
2043
+ : 5;
2044
+ const current = editedValue !== undefined ? editedValue : value;
2045
+ const rawCurrent = typeof current === 'number' ? current : Number(current);
2046
+ const selected = Number.isFinite(rawCurrent) ? Math.min(count, Math.max(0, Math.round(rawCurrent))) : 0;
2047
+ const commit = useCallback((n) => {
2048
+ const next = n <= 0 ? null : n;
2049
+ const col = column;
2050
+ if (col.validate && onValidationError) {
2051
+ onValidationError(itemKey, fieldName, col.validate(next, itemKey));
2052
+ }
2053
+ onValueChange(itemKey, fieldName, next, value);
2054
+ }, [column, fieldName, itemKey, value, onValueChange, onValidationError]);
2055
+ // Read mode → shared display (stars or single sentiment face).
2056
+ if (!isEditMode) {
2057
+ return jsx(RatingCell, { item: item, column: column, value: value, itemKey: itemKey });
2058
+ }
2059
+ const handleKeyDown = (e) => {
2060
+ if (e.key === 'ArrowRight' || e.key === 'ArrowUp') {
2061
+ e.preventDefault();
2062
+ commit(Math.min(count, selected + 1));
2063
+ }
2064
+ else if (e.key === 'ArrowLeft' || e.key === 'ArrowDown') {
2065
+ e.preventDefault();
2066
+ commit(Math.max(0, selected - 1));
2067
+ }
2068
+ else if (e.key === 'Home') {
2069
+ e.preventDefault();
2070
+ commit(0);
2071
+ }
2072
+ else if (e.key === 'End') {
2073
+ e.preventDefault();
2074
+ commit(count);
2075
+ }
2076
+ };
2077
+ let indicatorClass = '';
2078
+ if (errorMessage)
2079
+ indicatorClass = errorCellIndicatorClass;
2080
+ else if (isDirty)
2081
+ indicatorClass = dirtyCellIndicatorClass;
2082
+ const wrapperClass = mergeStyles(editableCellInputWrapperClass, indicatorClass);
2083
+ const buttons = [];
2084
+ for (let i = 1; i <= count; i++) {
2085
+ // Click the currently-selected value again to clear it.
2086
+ const onClick = () => commit(i === selected ? 0 : i);
2087
+ if (isEmoji) {
2088
+ const set = EMOJI_SETS[count];
2089
+ const isSel = i === selected;
2090
+ const useColor = column.ratingUseSentimentColor !== false;
2091
+ const color = isSel
2092
+ ? useColor
2093
+ ? EMOJI_COLORS[count][i - 1]
2094
+ : column.ratingColor || '#0078d4'
2095
+ : '#c8c6c4';
2096
+ buttons.push(jsx("button", { type: "button", onClick: onClick, title: set.labels[i - 1], "aria-label": set.labels[i - 1], "aria-hidden": true, tabIndex: -1, style: btnBaseStyle, children: jsx(Icon, { iconName: set.icons[i - 1], style: { color, fontSize: 18 } }) }, i));
2097
+ }
2098
+ else {
2099
+ const filledColor = column.ratingColor || '#ffb900';
2100
+ buttons.push(jsx("button", { type: "button", onClick: onClick, "aria-label": `${i} of ${count}`, "aria-hidden": true, tabIndex: -1, style: { ...starBtnStyle, color: i <= selected ? filledColor : '#c8c6c4' }, children: "\u2605" }, i));
2101
+ }
2102
+ }
2103
+ return (jsx("div", { className: editableCellClass, children: jsxs("div", { className: wrapperClass, children: [jsx("div", { role: "slider", "aria-valuemin": 0, "aria-valuemax": count, "aria-valuenow": selected, "aria-label": `${column.name} for ${item.key}`, tabIndex: 0, onKeyDown: handleKeyDown, style: { display: 'flex', gap: 2, alignItems: 'center' }, children: buttons }), errorMessage && (jsx("span", { className: cellErrorTextClass, title: errorMessage, children: errorMessage }))] }) }));
2104
+ }
2105
+
2106
+ export { ColoredCell, CompositeCell, CurrencyCell, DateCell, EditableDateCell, EditableLookupCell, EditableNumberCell, EditableOptionSetCell, EditableRatingCell, EditableTextCell, IconCell, LinkCell, LookupCell, NumberCell, OptionSetCell, ProgressBarCell, RatingCell, TextCell, ToggleCell, cellClass, cellErrorTextClass, coloredBadgeClass, compositeCellClass, currencyCellClass, dirtyCellIndicatorClass, editableCellClass, editableCellInputWrapperClass, errorCellIndicatorClass, getDetailsListStyles, getRowStyles, getRowStylesWithSelection, iconButtonCellClass, linkCellClass, lookupCellContainerClass, lookupClearButtonClass, lookupDropdownClass, lookupDropdownItemActiveClass, lookupDropdownItemClass, lookupDropdownNoneOptionClass, lookupLoadingClass, lookupNoResultsClass, lookupReadModeLinkClass, progressBarBackgroundClass, progressBarContainerClass, progressBarFillClass, progressBarLabelClass, ratingCellClass, ratingStarClass };
2107
+ //# sourceMappingURL=index.esm.js.map