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