@juspay/blend-design-system 0.0.37-beta.3 → 0.0.37-beta.4

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 (162) hide show
  1. package/dist/components/AccordionV2/index.d.ts +3 -0
  2. package/dist/components/AvatarV2/avatarV2.utils.d.ts +1 -1
  3. package/dist/components/AvatarV2/index.d.ts +1 -2
  4. package/dist/components/BreadcrumbV2/index.d.ts +10 -0
  5. package/dist/components/ButtonV2/ButtonGroupV2/index.d.ts +1 -0
  6. package/dist/components/ButtonV2/buttonV2.types.d.ts +0 -4
  7. package/dist/components/ButtonV2/index.d.ts +3 -0
  8. package/dist/components/ButtonV2/utils.d.ts +1 -1
  9. package/dist/components/ChartsV2/index.d.ts +5 -0
  10. package/dist/components/CodeEditorV2/CodeEditorV2.d.ts +1 -1
  11. package/dist/components/CodeEditorV2/codeEditorV2.tokens.d.ts +5 -5
  12. package/dist/components/CodeEditorV2/codeEditorV2.types.d.ts +5 -5
  13. package/dist/components/CodeEditorV2/index.d.ts +2 -0
  14. package/dist/components/CodeEditorV2/utils.d.ts +1 -1
  15. package/dist/components/DataTable/DataTable.d.ts +2 -1
  16. package/dist/components/DataTable/PivotTableModal/PivotPreviewPanel.d.ts +3 -0
  17. package/dist/components/DataTable/PivotTableModal/PivotTableIllustration.d.ts +7 -0
  18. package/dist/components/DataTable/PivotTableModal/index.d.ts +3 -0
  19. package/dist/components/DataTable/PivotTableModal/pivotModalStyleTokens.d.ts +123 -0
  20. package/dist/components/DataTable/PivotTableModal/types.d.ts +62 -0
  21. package/dist/components/DataTable/PivotTableModal/utils.d.ts +32 -0
  22. package/dist/components/DataTable/TableBody/types.d.ts +2 -0
  23. package/dist/components/DataTable/TableHeader/types.d.ts +1 -0
  24. package/dist/components/DataTable/index.d.ts +2 -0
  25. package/dist/components/DataTable/types.d.ts +56 -0
  26. package/dist/components/DataTable/utils.d.ts +19 -1
  27. package/dist/components/InputsV2/ChatInputV2/AttachmentDropdown.d.ts +3 -3
  28. package/dist/components/InputsV2/ChatInputV2/ChatInputTagV2.d.ts +2 -2
  29. package/dist/components/InputsV2/ChatInputV2/ChatInputV2.d.ts +4 -4
  30. package/dist/components/InputsV2/ChatInputV2/ChatInputV2.types.d.ts +8 -8
  31. package/dist/components/InputsV2/ChatInputV2/ChatInputV2AttachmentRow.d.ts +3 -3
  32. package/dist/components/InputsV2/ChatInputV2/MobileChatInputV2.d.ts +2 -2
  33. package/dist/components/InputsV2/ChatInputV2/utils.d.ts +4 -4
  34. package/dist/components/InputsV2/SearchInputV2/utils.d.ts +39 -0
  35. package/dist/components/InputsV2/TextInputV2/TextInputV2.types.d.ts +2 -2
  36. package/dist/components/InputsV2/TextInputV2/index.d.ts +2 -0
  37. package/dist/components/InputsV2/utils/utils.d.ts +1 -1
  38. package/dist/components/KeyValuePairV2/KeyValuePairV2.d.ts +1 -1
  39. package/dist/components/KeyValuePairV2/ResponsiveText.d.ts +2 -2
  40. package/dist/components/KeyValuePairV2/index.d.ts +3 -0
  41. package/dist/components/KeyValuePairV2/keyValuePairV2.types.d.ts +2 -2
  42. package/dist/components/KeyValuePairV2/responsiveTextStyles.d.ts +3 -3
  43. package/dist/components/KeyValuePairV2/utils.d.ts +2 -2
  44. package/dist/components/MenuV2/index.d.ts +1 -0
  45. package/dist/components/MenuV2/menuV2.utils.d.ts +2 -2
  46. package/dist/components/MultiSelectV2/index.d.ts +3 -0
  47. package/dist/components/MultiSelectV2/multiSelectV2.types.d.ts +1 -1
  48. package/dist/components/MultiSelectV2/utils.d.ts +2 -2
  49. package/dist/components/ProgressBarV2/index.d.ts +3 -0
  50. package/dist/components/ProgressBarV2/utils.d.ts +1 -1
  51. package/dist/components/SelectV2/index.d.ts +1 -0
  52. package/dist/components/SelectorV2/CheckboxV2/index.d.ts +4 -0
  53. package/dist/components/SelectorV2/CheckboxV2/utils.d.ts +1 -1
  54. package/dist/components/SelectorV2/RadioV2/index.d.ts +3 -0
  55. package/dist/components/SelectorV2/SwitchV2/index.d.ts +1 -0
  56. package/dist/components/SidebarV2/index.d.ts +5 -0
  57. package/dist/components/SingleSelectV2/SingleSelectV2VirtualList.d.ts +2 -2
  58. package/dist/components/SingleSelectV2/index.d.ts +3 -0
  59. package/dist/components/SingleSelectV2/singleSelectV2.types.d.ts +2 -2
  60. package/dist/components/SingleSelectV2/utils.d.ts +6 -6
  61. package/dist/components/StatCardV2/index.d.ts +10 -1
  62. package/dist/components/StepperV2/index.d.ts +3 -1
  63. package/dist/components/StepperV2/stepperV2.types.d.ts +2 -2
  64. package/dist/components/TabsV2/index.d.ts +3 -1
  65. package/dist/components/TagV2/index.d.ts +3 -0
  66. package/dist/components/TooltipV2/index.d.ts +1 -0
  67. package/dist/components/common/index.d.ts +1 -1
  68. package/dist/main.d.ts +30 -70
  69. package/dist/main.js +87817 -85412
  70. package/dist/{node-CRWdZOVN.js → node-C2uf3sNA.js} +1303 -1300
  71. package/dist/node.js +1 -1
  72. package/dist/tokens.js +1 -1
  73. package/lib/components/AccordionV2/index.ts +3 -0
  74. package/lib/components/AvatarV2/AvatarV2.tsx +2 -2
  75. package/lib/components/AvatarV2/avatarV2.utils.ts +1 -1
  76. package/lib/components/AvatarV2/index.ts +1 -12
  77. package/lib/components/BreadcrumbV2/index.ts +10 -0
  78. package/lib/components/ButtonV2/ButtonGroupV2/index.ts +1 -0
  79. package/lib/components/ButtonV2/ButtonV2.tsx +2 -2
  80. package/lib/components/ButtonV2/LinkButton.tsx +2 -2
  81. package/lib/components/ButtonV2/buttonV2.types.ts +0 -6
  82. package/lib/components/ButtonV2/index.ts +3 -0
  83. package/lib/components/ButtonV2/utils.ts +2 -2
  84. package/lib/components/Charts/BlendChart.tsx +1 -1
  85. package/lib/components/ChartsV2/ChartV2.tsx +3 -2
  86. package/lib/components/ChartsV2/index.ts +5 -0
  87. package/lib/components/CodeEditorV2/CodeEditorV2.tsx +2 -2
  88. package/lib/components/CodeEditorV2/codeEditorV2.dark.tokens.ts +37 -25
  89. package/lib/components/CodeEditorV2/codeEditorV2.light.token.ts +37 -25
  90. package/lib/components/CodeEditorV2/codeEditorV2.tokens.ts +5 -5
  91. package/lib/components/CodeEditorV2/codeEditorV2.types.ts +5 -5
  92. package/lib/components/CodeEditorV2/index.ts +2 -0
  93. package/lib/components/CodeEditorV2/utils.ts +1 -1
  94. package/lib/components/DataTable/DataTable.tsx +148 -4
  95. package/lib/components/DataTable/PivotTableModal/PivotPreviewPanel.tsx +174 -0
  96. package/lib/components/DataTable/PivotTableModal/PivotTableIllustration.tsx +28 -0
  97. package/lib/components/DataTable/PivotTableModal/index.tsx +859 -0
  98. package/lib/components/DataTable/PivotTableModal/pivot-table-illustration.png +0 -0
  99. package/lib/components/DataTable/PivotTableModal/pivotModal.styled.ts +13 -0
  100. package/lib/components/DataTable/PivotTableModal/pivotModalStyleTokens.ts +250 -0
  101. package/lib/components/DataTable/PivotTableModal/types.ts +69 -0
  102. package/lib/components/DataTable/PivotTableModal/utils.ts +360 -0
  103. package/lib/components/DataTable/TableBody/index.tsx +16 -5
  104. package/lib/components/DataTable/TableBody/types.ts +2 -0
  105. package/lib/components/DataTable/TableHeader/index.tsx +6 -3
  106. package/lib/components/DataTable/TableHeader/types.ts +1 -0
  107. package/lib/components/DataTable/index.ts +4 -0
  108. package/lib/components/DataTable/types.ts +57 -0
  109. package/lib/components/DataTable/utils.ts +197 -0
  110. package/lib/components/InputsV2/ChatInputV2/AttachmentDropdown.tsx +3 -3
  111. package/lib/components/InputsV2/ChatInputV2/ChatInputTagV2.tsx +3 -3
  112. package/lib/components/InputsV2/ChatInputV2/ChatInputV2.types.ts +8 -8
  113. package/lib/components/InputsV2/ChatInputV2/ChatInputV2AttachmentRow.tsx +7 -7
  114. package/lib/components/InputsV2/ChatInputV2/utils.ts +8 -8
  115. package/lib/components/InputsV2/SearchInputV2/utils.ts +14 -1
  116. package/lib/components/InputsV2/TextInputV2/TextInputV2.tsx +3 -3
  117. package/lib/components/InputsV2/TextInputV2/TextInputV2.types.ts +2 -2
  118. package/lib/components/InputsV2/TextInputV2/index.ts +2 -0
  119. package/lib/components/KeyValuePairV2/KeyValuePairV2.tsx +6 -2
  120. package/lib/components/KeyValuePairV2/ResponsiveText.tsx +2 -2
  121. package/lib/components/KeyValuePairV2/index.ts +3 -0
  122. package/lib/components/KeyValuePairV2/keyValuePairV2.types.ts +2 -2
  123. package/lib/components/KeyValuePairV2/responsiveTextStyles.ts +3 -3
  124. package/lib/components/KeyValuePairV2/utils.ts +3 -3
  125. package/lib/components/MenuV2/MenuV2.tsx +2 -2
  126. package/lib/components/MenuV2/MenuV2SubMenu.tsx +2 -2
  127. package/lib/components/MenuV2/index.ts +1 -0
  128. package/lib/components/MenuV2/menuV2.utils.ts +4 -4
  129. package/lib/components/MultiSelectV2/MultiSelectV2.tsx +2 -2
  130. package/lib/components/MultiSelectV2/MultiSelectV2Menu.tsx +5 -2
  131. package/lib/components/MultiSelectV2/index.ts +3 -0
  132. package/lib/components/MultiSelectV2/mobile/MobileMultiSelectV2.tsx +7 -4
  133. package/lib/components/MultiSelectV2/multiSelectV2.types.ts +1 -1
  134. package/lib/components/MultiSelectV2/utils.ts +2 -2
  135. package/lib/components/ProgressBarV2/ProgressBarV2.tsx +5 -2
  136. package/lib/components/ProgressBarV2/index.ts +3 -0
  137. package/lib/components/ProgressBarV2/utils.ts +1 -1
  138. package/lib/components/SelectV2/index.ts +1 -0
  139. package/lib/components/SelectorV2/CheckboxV2/CheckboxV2.tsx +2 -2
  140. package/lib/components/SelectorV2/CheckboxV2/index.ts +4 -0
  141. package/lib/components/SelectorV2/CheckboxV2/utils.ts +1 -1
  142. package/lib/components/SelectorV2/RadioV2/index.ts +3 -0
  143. package/lib/components/SelectorV2/SwitchV2/index.ts +1 -0
  144. package/lib/components/Sidebar/Sidebar.tsx +7 -2
  145. package/lib/components/SidebarV2/index.ts +5 -0
  146. package/lib/components/SingleSelectV2/MobileSingleSelectV2.tsx +2 -2
  147. package/lib/components/SingleSelectV2/SingleSelectV2.tsx +10 -3
  148. package/lib/components/SingleSelectV2/SingleSelectV2Menu.tsx +4 -2
  149. package/lib/components/SingleSelectV2/SingleSelectV2VirtualList.tsx +5 -2
  150. package/lib/components/SingleSelectV2/index.ts +7 -0
  151. package/lib/components/SingleSelectV2/singleSelectV2.types.ts +2 -2
  152. package/lib/components/SingleSelectV2/utils.ts +10 -10
  153. package/lib/components/StatCardV2/index.ts +13 -1
  154. package/lib/components/StepperV2/index.ts +3 -1
  155. package/lib/components/StepperV2/stepperV2.types.ts +2 -2
  156. package/lib/components/TabsV2/index.ts +13 -1
  157. package/lib/components/TagV2/index.ts +3 -0
  158. package/lib/components/TooltipV2/index.ts +1 -0
  159. package/lib/components/common/index.ts +1 -1
  160. package/lib/main.ts +34 -258
  161. package/lib/types/assets.d.ts +24 -0
  162. package/package.json +2 -1
@@ -0,0 +1,859 @@
1
+ import {
2
+ CSSProperties,
3
+ forwardRef,
4
+ useEffect,
5
+ useMemo,
6
+ useRef,
7
+ useState,
8
+ } from 'react'
9
+ import { X, Plus } from 'lucide-react'
10
+ import { ButtonSubType } from '../../Button/types'
11
+ import Modal from '../../Modal/Modal'
12
+ import Block from '../../Primitives/Block/Block'
13
+ import PrimitiveText from '../../Primitives/PrimitiveText/PrimitiveText'
14
+ import Button from '../../Button/Button'
15
+ import { ButtonSize, ButtonType } from '../../Button/types'
16
+ import SingleSelect from '../../SingleSelect/SingleSelect'
17
+ import {
18
+ SelectMenuAlignment,
19
+ SelectMenuSize,
20
+ SelectMenuVariant,
21
+ } from '../../SingleSelect/types'
22
+ import { Checkbox } from '../../Checkbox/Checkbox'
23
+ import { CheckboxSize } from '../../Checkbox/types'
24
+ import { TruncatedTextWithTooltipV2 } from '../../common/TruncatedTextWithTooltipV2'
25
+ import { TooltipV2Align, TooltipV2Side } from '../../TooltipV2/tooltipV2.types'
26
+ import { ColumnDefinition, PivotAggregationType, ColumnType } from '../types'
27
+ import { TableTokenType } from '../dataTable.tokens'
28
+ import { downloadCSV } from '../utils'
29
+ import { useResponsiveTokens } from '../../../hooks/useResponsiveTokens'
30
+ import { FOUNDATION_THEME } from '../../../tokens'
31
+ import { PivotTableModalProps, PivotFieldConfig } from './types'
32
+ import {
33
+ buildPivotPreview,
34
+ getPivotFieldOptions,
35
+ getSupportedAggregationsForField,
36
+ } from './utils'
37
+ import { NoScrollbar } from './pivotModal.styled'
38
+ import { getPivotModalStyleTokens } from './pivotModalStyleTokens'
39
+ import PivotPreviewPanel from './PivotPreviewPanel'
40
+
41
+ type PivotSectionKey = 'rows' | 'columns' | 'values'
42
+
43
+ const PivotTableModal = forwardRef<
44
+ HTMLDivElement,
45
+ PivotTableModalProps<Record<string, unknown>>
46
+ >(
47
+ (
48
+ {
49
+ isOpen,
50
+ onClose,
51
+ columns,
52
+ data,
53
+ title = 'Create Pivot Table',
54
+ description: subtitle,
55
+ initialConfig,
56
+ onConfigChange,
57
+ onExport,
58
+ availableAggregations,
59
+ trigger,
60
+ onTriggerClick,
61
+ },
62
+ ref
63
+ ) => {
64
+ const tableToken = useResponsiveTokens('TABLE') as TableTokenType
65
+ const pivot = useMemo(
66
+ () => getPivotModalStyleTokens(FOUNDATION_THEME, tableToken),
67
+ [tableToken]
68
+ )
69
+
70
+ const removeButtonStyle = useMemo(
71
+ (): CSSProperties => ({
72
+ minWidth: pivot.removeButton.minWidth,
73
+ padding: pivot.removeButton.padding,
74
+ borderRadius: '50%',
75
+ border: 'none',
76
+ background: 'transparent',
77
+ cursor: 'pointer',
78
+ display: 'flex',
79
+ alignItems: 'center',
80
+ justifyContent: 'center',
81
+ transition: 'all 0.2s ease',
82
+ }),
83
+ [pivot]
84
+ )
85
+
86
+ // State for pivot configuration
87
+ const [rowFields, setRowFields] = useState<PivotFieldConfig[]>(
88
+ () =>
89
+ initialConfig?.rows?.map((field) => ({
90
+ field: String(field),
91
+ showTotal: false,
92
+ })) || []
93
+ )
94
+ const [columnFields, setColumnFields] = useState<PivotFieldConfig[]>(
95
+ () =>
96
+ initialConfig?.columns?.map((field) => ({
97
+ field: String(field),
98
+ showTotal: false,
99
+ })) || []
100
+ )
101
+ const [valueConfigs, setValueConfigs] = useState<
102
+ Array<{
103
+ field: keyof Record<string, unknown>
104
+ aggregation: PivotAggregationType
105
+ }>
106
+ >(
107
+ () =>
108
+ (initialConfig?.values as Array<{
109
+ field: keyof Record<string, unknown>
110
+ aggregation: PivotAggregationType
111
+ }>) || []
112
+ )
113
+
114
+ const [addRowSelection, setAddRowSelection] = useState<string>('')
115
+ const [addColumnSelection, setAddColumnSelection] = useState<string>('')
116
+ const [addValueSelection, setAddValueSelection] = useState<string>('')
117
+
118
+ // Compute field options from columns
119
+ const fieldOptions = useMemo(
120
+ () => getPivotFieldOptions(columns),
121
+ [columns]
122
+ )
123
+ const columnTypeByField = useMemo(
124
+ () =>
125
+ new Map(
126
+ columns.map((column) => [String(column.field), column.type])
127
+ ),
128
+ [columns]
129
+ )
130
+ const allowedAggregations = useMemo(
131
+ () => availableAggregations || Object.values(PivotAggregationType),
132
+ [availableAggregations]
133
+ )
134
+ const supportedAggregationsByField = useMemo(() => {
135
+ const map = new Map<string, PivotAggregationType[]>()
136
+ fieldOptions.forEach((option) => {
137
+ const supported = getSupportedAggregationsForField(
138
+ data,
139
+ option.key as keyof Record<string, unknown>,
140
+ allowedAggregations,
141
+ [ColumnType.NUMBER, ColumnType.SLIDER].includes(
142
+ columnTypeByField.get(option.key) as ColumnType
143
+ )
144
+ )
145
+ map.set(option.key, supported)
146
+ })
147
+ return map
148
+ }, [fieldOptions, data, allowedAggregations, columnTypeByField])
149
+
150
+ const getSupportedAggregations = (field: string) => {
151
+ return supportedAggregationsByField.get(field) || []
152
+ }
153
+
154
+ const getDefaultAggregationForField = (field: string) => {
155
+ const supported = getSupportedAggregations(field)
156
+ if (supported.includes(PivotAggregationType.SUM)) {
157
+ return PivotAggregationType.SUM
158
+ }
159
+ return supported[0]
160
+ }
161
+
162
+ // Add field handlers (Sheets-style)
163
+ const addRowField = (field: string) => {
164
+ if (!field || rowFields.some((f) => f.field === field)) return
165
+ // mutual exclusivity: move from Columns -> Rows
166
+ setColumnFields((prev) => prev.filter((f) => f.field !== field))
167
+ setRowFields((prev) => [...prev, { field, showTotal: false }])
168
+ }
169
+
170
+ const removeRowField = (field: string) => {
171
+ setRowFields((prev) => prev.filter((f) => f.field !== field))
172
+ }
173
+
174
+ const addColumnField = (field: string) => {
175
+ if (!field || columnFields.some((f) => f.field === field)) return
176
+ // mutual exclusivity: move from Rows -> Columns
177
+ setRowFields((prev) => prev.filter((f) => f.field !== field))
178
+ setColumnFields((prev) => [...prev, { field, showTotal: false }])
179
+ }
180
+
181
+ const removeColumnField = (field: string) => {
182
+ setColumnFields((prev) => prev.filter((f) => f.field !== field))
183
+ }
184
+
185
+ const updateRowFieldTotal = (field: string, showTotal: boolean) => {
186
+ if (process.env.NODE_ENV !== 'production') {
187
+ // eslint-disable-next-line no-console
188
+ console.debug('[PivotUI] updateRowFieldTotal', {
189
+ field,
190
+ showTotal,
191
+ })
192
+ }
193
+ setRowFields((prev) =>
194
+ prev.map((f) => (f.field === field ? { ...f, showTotal } : f))
195
+ )
196
+ }
197
+
198
+ const updateColumnFieldTotal = (field: string, showTotal: boolean) => {
199
+ if (process.env.NODE_ENV !== 'production') {
200
+ // eslint-disable-next-line no-console
201
+ console.debug('[PivotUI] updateColumnFieldTotal', {
202
+ field,
203
+ showTotal,
204
+ })
205
+ }
206
+ setColumnFields((prev) =>
207
+ prev.map((f) => (f.field === field ? { ...f, showTotal } : f))
208
+ )
209
+ }
210
+
211
+ const addValueField = (field: string) => {
212
+ if (!field) return
213
+ const aggregation = getDefaultAggregationForField(field)
214
+ if (!aggregation) return
215
+ // allow duplicates by (field, aggregation), but prevent exact duplicates
216
+ if (
217
+ valueConfigs.some(
218
+ (v) =>
219
+ String(v.field) === field &&
220
+ v.aggregation === aggregation
221
+ )
222
+ ) {
223
+ return
224
+ }
225
+ setValueConfigs((prev) => [
226
+ ...prev,
227
+ {
228
+ field: field as keyof Record<string, unknown>,
229
+ aggregation,
230
+ },
231
+ ])
232
+ }
233
+
234
+ const removeValueField = (index: number) => {
235
+ setValueConfigs((prev) => prev.filter((_, i) => i !== index))
236
+ }
237
+
238
+ const updateValueAggregation = (
239
+ index: number,
240
+ aggregation: PivotAggregationType
241
+ ) => {
242
+ setValueConfigs((prev) => {
243
+ const target = prev[index]
244
+ if (!target) return prev
245
+ const isDuplicate = prev.some(
246
+ (v, i) =>
247
+ i !== index &&
248
+ String(v.field) === String(target.field) &&
249
+ v.aggregation === aggregation
250
+ )
251
+ if (isDuplicate) return prev
252
+ const newConfigs = [...prev]
253
+ newConfigs[index].aggregation = aggregation
254
+ return newConfigs
255
+ })
256
+ }
257
+
258
+ const updateRowField = (index: number, nextField: string) => {
259
+ setRowFields((prev) => {
260
+ const usedInOtherSections = columnFields.some(
261
+ (field) => field.field === nextField
262
+ )
263
+ if (
264
+ !nextField ||
265
+ usedInOtherSections ||
266
+ prev.some((f, i) => i !== index && f.field === nextField)
267
+ ) {
268
+ return prev
269
+ }
270
+ return prev.map((field, i) =>
271
+ i === index ? { ...field, field: nextField } : field
272
+ )
273
+ })
274
+ }
275
+
276
+ const updateColumnField = (index: number, nextField: string) => {
277
+ setColumnFields((prev) => {
278
+ const usedInOtherSections = rowFields.some(
279
+ (field) => field.field === nextField
280
+ )
281
+ if (
282
+ !nextField ||
283
+ usedInOtherSections ||
284
+ prev.some((f, i) => i !== index && f.field === nextField)
285
+ ) {
286
+ return prev
287
+ }
288
+ return prev.map((field, i) =>
289
+ i === index ? { ...field, field: nextField } : field
290
+ )
291
+ })
292
+ }
293
+
294
+ const updateValueField = (index: number, nextField: string) => {
295
+ setValueConfigs((prev) => {
296
+ if (
297
+ !nextField ||
298
+ prev.some(
299
+ (config, i) =>
300
+ i !== index && String(config.field) === nextField
301
+ )
302
+ ) {
303
+ return prev
304
+ }
305
+
306
+ const defaultAggregation =
307
+ getDefaultAggregationForField(nextField)
308
+ if (!defaultAggregation) return prev
309
+
310
+ return prev.map((config, i) => {
311
+ if (i !== index) return config
312
+ const supported = getSupportedAggregations(nextField)
313
+ let resolvedAggregation: PivotAggregationType
314
+ if (supported.includes(PivotAggregationType.SUM)) {
315
+ resolvedAggregation = PivotAggregationType.SUM
316
+ } else {
317
+ resolvedAggregation = supported.includes(
318
+ config.aggregation
319
+ )
320
+ ? config.aggregation
321
+ : defaultAggregation
322
+ }
323
+
324
+ // prevent creating an exact duplicate (field + aggregation) pair
325
+ if (
326
+ prev.some(
327
+ (other, j) =>
328
+ j !== index &&
329
+ String(other.field) === nextField &&
330
+ other.aggregation === resolvedAggregation
331
+ )
332
+ ) {
333
+ return config
334
+ }
335
+ return {
336
+ ...config,
337
+ field: nextField as keyof Record<string, unknown>,
338
+ aggregation: resolvedAggregation,
339
+ }
340
+ })
341
+ })
342
+ }
343
+
344
+ const lastEmittedConfigRef = useRef<string>('')
345
+
346
+ useEffect(() => {
347
+ if (!isOpen) {
348
+ lastEmittedConfigRef.current = ''
349
+ return
350
+ }
351
+
352
+ const config = {
353
+ rows: rowFields.map((f) => f.field),
354
+ columns: columnFields.map((f) => f.field),
355
+ values: valueConfigs,
356
+ }
357
+ const configString = JSON.stringify(config)
358
+
359
+ if (lastEmittedConfigRef.current !== configString) {
360
+ lastEmittedConfigRef.current = configString
361
+ onConfigChange?.(config)
362
+ }
363
+ }, [isOpen, rowFields, columnFields, valueConfigs, onConfigChange])
364
+
365
+ const exportPivotTable = () => {
366
+ const exportColumns = effectivePreviewColumns
367
+ const exportRows = effectivePreviewRows
368
+
369
+ if (onExport) {
370
+ onExport({
371
+ rows: rowFields.map((f) => f.field),
372
+ columns: columnFields.map((f) => f.field),
373
+ values: valueConfigs,
374
+ })
375
+ return
376
+ }
377
+
378
+ if (!exportRows || exportRows.length === 0) return
379
+
380
+ const headers = exportColumns?.map((col) => `"${col.label}"`) || []
381
+ const rows = exportRows.map((row) =>
382
+ exportColumns?.map(
383
+ (col) =>
384
+ `"${String(row[col.key] ?? '').replace(/"/g, '""')}"`
385
+ )
386
+ )
387
+
388
+ const csvContent = [
389
+ headers.join(','),
390
+ ...rows.map((r) => r?.join(',') || ''),
391
+ ].join('\n')
392
+ downloadCSV(csvContent, 'pivot-table.csv')
393
+ }
394
+
395
+ const effectivePreview = useMemo(() => {
396
+ return buildPivotPreview(
397
+ data as Record<string, unknown>[],
398
+ rowFields.map((f) => ({
399
+ field: f.field as keyof Record<string, unknown>,
400
+ showTotal: f.showTotal,
401
+ })),
402
+ columnFields.map((f) => ({
403
+ field: f.field as keyof Record<string, unknown>,
404
+ showTotal: f.showTotal,
405
+ })),
406
+ valueConfigs,
407
+ Object.fromEntries(
408
+ fieldOptions.map((opt) => [opt.key, opt.label])
409
+ )
410
+ )
411
+ }, [data, rowFields, columnFields, valueConfigs, fieldOptions])
412
+ const effectivePreviewColumns = effectivePreview.columns
413
+ const effectivePreviewRows = effectivePreview.rows
414
+
415
+ const previewTableColumns: ColumnDefinition<Record<string, unknown>>[] =
416
+ useMemo(
417
+ () =>
418
+ effectivePreviewColumns.map((col) => ({
419
+ field: col.key,
420
+ header: col.label,
421
+ type: ColumnType.TEXT,
422
+ })),
423
+ [effectivePreviewColumns]
424
+ )
425
+
426
+ const rowAddOptions = useMemo(() => {
427
+ const blocked = new Set([
428
+ ...rowFields.map((f) => f.field),
429
+ ...columnFields.map((f) => f.field),
430
+ ])
431
+ return fieldOptions.filter((opt) => !blocked.has(opt.key))
432
+ }, [fieldOptions, rowFields, columnFields])
433
+
434
+ const columnAddOptions = useMemo(() => {
435
+ const blocked = new Set([
436
+ ...rowFields.map((f) => f.field),
437
+ ...columnFields.map((f) => f.field),
438
+ ])
439
+ return fieldOptions.filter((opt) => !blocked.has(opt.key))
440
+ }, [fieldOptions, rowFields, columnFields])
441
+
442
+ const valueAddOptions = useMemo(() => fieldOptions, [fieldOptions])
443
+
444
+ const fieldLabelByKey = useMemo(() => {
445
+ return new Map(fieldOptions.map((opt) => [opt.key, opt.label]))
446
+ }, [fieldOptions])
447
+
448
+ const getFieldLabel = (fieldKey: string) => {
449
+ return fieldLabelByKey.get(fieldKey) || fieldKey
450
+ }
451
+
452
+ // Render field config card
453
+ const renderFieldConfig = (
454
+ field: PivotFieldConfig,
455
+ onRemove: () => void,
456
+ onFieldChange?: (nextField: string) => void,
457
+ showAggSelector?: boolean,
458
+ aggValue?: PivotAggregationType,
459
+ onAggChange?: (agg: PivotAggregationType) => void,
460
+ onShowTotalChange?: (checked: boolean) => void,
461
+ showTotalToggle = true
462
+ ) => (
463
+ <Block
464
+ key={field.field}
465
+ style={{
466
+ padding: FOUNDATION_THEME.unit[12],
467
+ backgroundColor: FOUNDATION_THEME.colors.gray[25] as string,
468
+ borderRadius: FOUNDATION_THEME.border.radius[8],
469
+ border: `${FOUNDATION_THEME.border.width[1]} solid ${FOUNDATION_THEME.colors.gray[200]}`,
470
+ marginBottom: FOUNDATION_THEME.unit[12],
471
+ }}
472
+ >
473
+ <Block
474
+ style={{
475
+ display: 'flex',
476
+ alignItems: 'center',
477
+ justifyContent: 'space-between',
478
+ marginBottom: showAggSelector
479
+ ? FOUNDATION_THEME.unit[8]
480
+ : '0',
481
+ gap: FOUNDATION_THEME.unit[8],
482
+ }}
483
+ >
484
+ <Block style={{ flex: 1, minWidth: 0 }}>
485
+ <TruncatedTextWithTooltipV2
486
+ text={getFieldLabel(field.field)}
487
+ side={TooltipV2Side.TOP}
488
+ align={TooltipV2Align.START}
489
+ style={{
490
+ fontSize:
491
+ FOUNDATION_THEME.font.size.body.sm.fontSize,
492
+ fontWeight: FOUNDATION_THEME.font.weight[500],
493
+ color: pivot.text.fieldLabel.color,
494
+ }}
495
+ />
496
+ </Block>
497
+ <button
498
+ type="button"
499
+ style={removeButtonStyle}
500
+ onClick={onRemove}
501
+ onMouseEnter={(e) => {
502
+ e.currentTarget.style.backgroundColor =
503
+ FOUNDATION_THEME.colors.gray[100] as string
504
+ }}
505
+ onMouseLeave={(e) => {
506
+ e.currentTarget.style.backgroundColor =
507
+ 'transparent'
508
+ }}
509
+ aria-label={`Remove ${field.field}`}
510
+ >
511
+ <X
512
+ size={16}
513
+ color={FOUNDATION_THEME.colors.gray[500] as string}
514
+ />
515
+ </button>
516
+ </Block>
517
+ <Block
518
+ style={{
519
+ display: 'flex',
520
+ gap: FOUNDATION_THEME.unit[8],
521
+ marginBottom: FOUNDATION_THEME.unit[8],
522
+ }}
523
+ >
524
+ <Block style={{ flex: 1, minWidth: 0 }}>
525
+ <SingleSelect
526
+ placeholder="Column Name"
527
+ items={[
528
+ {
529
+ items: fieldOptions.map((option) => ({
530
+ label: option.label,
531
+ value: option.key,
532
+ })),
533
+ },
534
+ ]}
535
+ selected={field.field}
536
+ onSelect={(value) => onFieldChange?.(String(value))}
537
+ variant={SelectMenuVariant.CONTAINER}
538
+ size={SelectMenuSize.SMALL}
539
+ fullWidth
540
+ />
541
+ </Block>
542
+ {showAggSelector && aggValue && onAggChange && (
543
+ <Block style={{ flex: 1, minWidth: 0 }}>
544
+ <SingleSelect
545
+ placeholder="operation"
546
+ items={[
547
+ {
548
+ items: getSupportedAggregations(
549
+ field.field
550
+ ).map((item) => ({
551
+ label: item.toUpperCase(),
552
+ value: item,
553
+ })),
554
+ },
555
+ ]}
556
+ selected={aggValue}
557
+ onSelect={(value) =>
558
+ onAggChange(value as PivotAggregationType)
559
+ }
560
+ variant={SelectMenuVariant.CONTAINER}
561
+ size={SelectMenuSize.SMALL}
562
+ alignment={SelectMenuAlignment.END}
563
+ fullWidth
564
+ />
565
+ </Block>
566
+ )}
567
+ </Block>
568
+ {showTotalToggle && (
569
+ <Block>
570
+ <Checkbox
571
+ checked={field.showTotal}
572
+ size={CheckboxSize.SMALL}
573
+ onCheckedChange={(checked) =>
574
+ onShowTotalChange?.(
575
+ checked === true ||
576
+ checked === 'indeterminate'
577
+ )
578
+ }
579
+ >
580
+ Show Total
581
+ </Checkbox>
582
+ </Block>
583
+ )}
584
+ </Block>
585
+ )
586
+
587
+ const renderSection = (
588
+ title: string,
589
+ sectionKey: PivotSectionKey,
590
+ fields: PivotFieldConfig[],
591
+ onRemove: (field: string) => void,
592
+ showAgg?: boolean,
593
+ aggs?: PivotAggregationType[],
594
+ onAggChange?: (index: number, agg: PivotAggregationType) => void,
595
+ onShowTotalChange?: (field: string, showTotal: boolean) => void
596
+ ) => (
597
+ <Block
598
+ style={{
599
+ marginBottom: '0',
600
+ }}
601
+ >
602
+ <Block
603
+ style={{
604
+ display: 'flex',
605
+ alignItems: 'center',
606
+ justifyContent: 'space-between',
607
+ marginBottom:
608
+ fields.length > 0 ? FOUNDATION_THEME.unit[10] : '0',
609
+ }}
610
+ >
611
+ <PrimitiveText
612
+ style={{
613
+ fontSize:
614
+ FOUNDATION_THEME.font.size.body.md.fontSize,
615
+ fontWeight: FOUNDATION_THEME.font.weight[500],
616
+ color: pivot.text.sectionTitle.color,
617
+ }}
618
+ >
619
+ {title}
620
+ </PrimitiveText>
621
+ <SingleSelect
622
+ placeholder="Add"
623
+ items={[
624
+ {
625
+ items:
626
+ sectionKey === 'rows'
627
+ ? rowAddOptions.map((o) => ({
628
+ label: o.label,
629
+ value: o.key,
630
+ }))
631
+ : sectionKey === 'columns'
632
+ ? columnAddOptions.map((o) => ({
633
+ label: o.label,
634
+ value: o.key,
635
+ }))
636
+ : valueAddOptions.map((o) => ({
637
+ label: o.label,
638
+ value: o.key,
639
+ })),
640
+ },
641
+ ]}
642
+ selected={
643
+ sectionKey === 'rows'
644
+ ? addRowSelection
645
+ : sectionKey === 'columns'
646
+ ? addColumnSelection
647
+ : addValueSelection
648
+ }
649
+ onSelect={(value) => {
650
+ const next = String(value)
651
+ if (sectionKey === 'rows') {
652
+ setAddRowSelection(next)
653
+ addRowField(next)
654
+ setAddRowSelection('')
655
+ return
656
+ }
657
+ if (sectionKey === 'columns') {
658
+ setAddColumnSelection(next)
659
+ addColumnField(next)
660
+ setAddColumnSelection('')
661
+ return
662
+ }
663
+ setAddValueSelection(next)
664
+ addValueField(next)
665
+ setAddValueSelection('')
666
+ }}
667
+ variant={SelectMenuVariant.NO_CONTAINER}
668
+ size={SelectMenuSize.SMALL}
669
+ alignment={SelectMenuAlignment.END}
670
+ allowDeselect={false}
671
+ customTrigger={
672
+ <Button
673
+ buttonType={ButtonType.SECONDARY}
674
+ size={ButtonSize.SMALL}
675
+ subType={ButtonSubType.ICON_ONLY}
676
+ leadingIcon={<Plus size={16} />}
677
+ aria-label={`Add ${title.toLowerCase()} field`}
678
+ />
679
+ }
680
+ />
681
+ </Block>
682
+ {fields.length > 0 && (
683
+ <Block>
684
+ {fields.map((field, index) =>
685
+ renderFieldConfig(
686
+ field,
687
+ () => onRemove(field.field),
688
+ (nextField) => {
689
+ if (sectionKey === 'rows') {
690
+ updateRowField(index, nextField)
691
+ return
692
+ }
693
+ if (sectionKey === 'columns') {
694
+ updateColumnField(index, nextField)
695
+ return
696
+ }
697
+ updateValueField(index, nextField)
698
+ },
699
+ showAgg,
700
+ showAgg && aggs ? aggs[index] : undefined,
701
+ showAgg && onAggChange
702
+ ? (agg) => onAggChange(index, agg)
703
+ : undefined,
704
+ onShowTotalChange
705
+ ? (checked: boolean) =>
706
+ onShowTotalChange(
707
+ field.field,
708
+ checked
709
+ )
710
+ : undefined,
711
+ sectionKey !== 'values'
712
+ )
713
+ )}
714
+ </Block>
715
+ )}
716
+ {fields.length === 0 && <Block />}
717
+ </Block>
718
+ )
719
+
720
+ const secondaryActionButton = {
721
+ text: 'Download',
722
+ buttonType: ButtonType.SECONDARY,
723
+ size: ButtonSize.SMALL,
724
+ onClick: exportPivotTable,
725
+ disabled: valueConfigs.length === 0,
726
+ }
727
+
728
+ return (
729
+ <>
730
+ {/* Trigger - rendered outside modal */}
731
+ {trigger && (
732
+ <Block
733
+ onClick={onTriggerClick}
734
+ style={{ cursor: 'pointer', display: 'inline-block' }}
735
+ >
736
+ {trigger}
737
+ </Block>
738
+ )}
739
+
740
+ <Modal
741
+ ref={ref}
742
+ isOpen={isOpen}
743
+ onClose={onClose}
744
+ title={title}
745
+ subtitle={subtitle}
746
+ minWidth={pivot.modal.minWidth}
747
+ maxWidth={pivot.modal.maxWidth}
748
+ maxHeight={pivot.modal.maxHeight}
749
+ showFooter={true}
750
+ secondaryAction={secondaryActionButton}
751
+ useDrawerOnMobile={false}
752
+ isCustom
753
+ >
754
+ <NoScrollbar
755
+ style={{
756
+ display: 'flex',
757
+ flexWrap: 'nowrap',
758
+ height: '72vh',
759
+ minHeight: '520px',
760
+ overflow: 'hidden',
761
+ padding: pivot.modal.bodyPadding,
762
+ gap: pivot.modal.bodyGap,
763
+ }}
764
+ >
765
+ <Block
766
+ style={{
767
+ flex: '1 1 0',
768
+ minWidth: 0,
769
+ overflow: 'hidden',
770
+ }}
771
+ >
772
+ <PivotPreviewPanel
773
+ pivot={pivot}
774
+ tableToken={tableToken}
775
+ showExport={false}
776
+ previewRows={effectivePreviewRows}
777
+ previewColumns={effectivePreviewColumns}
778
+ previewTableColumns={previewTableColumns}
779
+ onExport={exportPivotTable}
780
+ hasValues={valueConfigs.length > 0}
781
+ />
782
+ </Block>
783
+ <NoScrollbar
784
+ style={{
785
+ padding: FOUNDATION_THEME.unit[16],
786
+ overflow: 'auto',
787
+ overflowX: 'hidden',
788
+ border: `${FOUNDATION_THEME.border.width[1]} solid ${FOUNDATION_THEME.colors.gray[200]}`,
789
+ borderRadius: FOUNDATION_THEME.border.radius[8],
790
+ width: pivot.rightPanel.width,
791
+ minWidth: pivot.rightPanel.width,
792
+ maxWidth: pivot.rightPanel.width,
793
+ flex: `0 0 ${pivot.rightPanel.width}`,
794
+ boxSizing: 'border-box',
795
+ }}
796
+ >
797
+ <Block />
798
+
799
+ <Block
800
+ style={{
801
+ display: 'flex',
802
+ flexDirection: 'column',
803
+ gap: FOUNDATION_THEME.unit[32],
804
+ }}
805
+ >
806
+ {/* Rows Section */}
807
+ {renderSection(
808
+ 'Rows',
809
+ 'rows',
810
+ rowFields,
811
+ removeRowField,
812
+ false,
813
+ undefined,
814
+ undefined,
815
+ updateRowFieldTotal
816
+ )}
817
+
818
+ {/* Columns Section */}
819
+ {renderSection(
820
+ 'Columns',
821
+ 'columns',
822
+ columnFields,
823
+ removeColumnField,
824
+ false,
825
+ undefined,
826
+ undefined,
827
+ updateColumnFieldTotal
828
+ )}
829
+
830
+ {/* Values Section */}
831
+ {renderSection(
832
+ 'Values',
833
+ 'values',
834
+ valueConfigs.map((v) => ({
835
+ field: String(v.field),
836
+ showTotal: true,
837
+ })),
838
+ (field) => {
839
+ const index = valueConfigs.findIndex(
840
+ (v) => String(v.field) === field
841
+ )
842
+ if (index >= 0) removeValueField(index)
843
+ },
844
+ true,
845
+ valueConfigs.map((v) => v.aggregation),
846
+ updateValueAggregation
847
+ )}
848
+ </Block>
849
+ </NoScrollbar>
850
+ </NoScrollbar>
851
+ </Modal>
852
+ </>
853
+ )
854
+ }
855
+ )
856
+
857
+ PivotTableModal.displayName = 'PivotTableModal'
858
+
859
+ export default PivotTableModal