@cellaware/utils 8.11.19 → 8.11.20
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.
- package/dist/azure/cosmos.d.ts +112 -0
- package/dist/azure/cosmos.js +305 -0
- package/dist/azure/email.d.ts +3 -0
- package/dist/azure/email.js +20 -0
- package/dist/azure/function.d.ts +14 -0
- package/dist/azure/function.js +124 -0
- package/dist/azure/slot.d.ts +1 -0
- package/dist/azure/slot.js +4 -0
- package/dist/azure/storage.d.ts +14 -0
- package/dist/azure/storage.js +81 -0
- package/dist/chatwms/alert.d.ts +97 -0
- package/dist/chatwms/alert.js +74 -0
- package/dist/chatwms/azure/cosmos.d.ts +25 -0
- package/dist/chatwms/azure/cosmos.js +43 -0
- package/dist/chatwms/azure/function.d.ts +21 -0
- package/dist/chatwms/azure/function.js +29 -0
- package/dist/chatwms/azure/storage.d.ts +15 -0
- package/dist/chatwms/azure/storage.js +27 -0
- package/dist/chatwms/client.d.ts +18 -0
- package/dist/chatwms/client.js +48 -0
- package/dist/chatwms/cosmos.d.ts +24 -0
- package/dist/chatwms/cosmos.js +532 -0
- package/dist/chatwms/dashboard.d.ts +80 -0
- package/dist/chatwms/dashboard.js +17 -0
- package/dist/chatwms/datagrid.d.ts +215 -0
- package/dist/chatwms/datagrid.js +1454 -0
- package/dist/chatwms/developer.d.ts +27 -0
- package/dist/chatwms/developer.js +12 -0
- package/dist/chatwms/github/issue.d.ts +1 -0
- package/dist/chatwms/github/issue.js +4 -0
- package/dist/chatwms/instance.d.ts +16 -0
- package/dist/chatwms/instance.js +18 -0
- package/dist/chatwms/integration.d.ts +24 -0
- package/dist/chatwms/integration.js +19 -0
- package/dist/chatwms/pdf.d.ts +95 -0
- package/dist/chatwms/pdf.js +147 -0
- package/dist/chatwms/report.d.ts +126 -0
- package/dist/chatwms/report.js +55 -0
- package/dist/chatwms/response.d.ts +18 -0
- package/dist/chatwms/response.js +25 -0
- package/dist/chatwms/search.d.ts +12 -0
- package/dist/chatwms/search.js +9 -0
- package/dist/chatwms/teams.d.ts +237 -0
- package/dist/chatwms/teams.js +205 -0
- package/dist/chatwms/user.d.ts +31 -0
- package/dist/chatwms/user.js +42 -0
- package/dist/chatwms/warehouse.d.ts +3 -0
- package/dist/chatwms/warehouse.js +3 -0
- package/dist/github/issue.d.ts +1 -0
- package/dist/github/issue.js +23 -0
- package/dist/llm/chain-store.d.ts +49 -0
- package/dist/llm/chain-store.js +284 -0
- package/dist/llm/cost.d.ts +3 -0
- package/dist/llm/cost.js +42 -0
- package/dist/llm/model.d.ts +12 -0
- package/dist/llm/model.js +1 -0
- package/dist/stopwatch.d.ts +8 -0
- package/dist/stopwatch.js +36 -0
- package/dist/util.d.ts +45 -0
- package/dist/util.js +288 -0
- package/dist/version.d.ts +4 -0
- package/dist/version.js +12 -0
- package/package.json +1 -1
|
@@ -0,0 +1,1454 @@
|
|
|
1
|
+
import { isDateString, truncateValue } from "../util.js";
|
|
2
|
+
import { CHATWMS_DEFAULT_LANGUAGE } from "./user.js";
|
|
3
|
+
export const DATAGRID_HTML_ROWS = 1000;
|
|
4
|
+
export const DATAGRID_HTML_COLS = 8;
|
|
5
|
+
export const DATAGRID_TEAMS_ROWS = 24;
|
|
6
|
+
export function initDatagridState() {
|
|
7
|
+
return {
|
|
8
|
+
adjRows: []
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
export function initVisualDatagridState() {
|
|
12
|
+
return {
|
|
13
|
+
...initDatagridState(),
|
|
14
|
+
chartRows: [],
|
|
15
|
+
html: '',
|
|
16
|
+
teamsRows: [],
|
|
17
|
+
htmlTranspose: '',
|
|
18
|
+
teamsTranspose: []
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
export const DEFAULT_VALUE_FORMAT_VALUE = 'none';
|
|
22
|
+
export function evaluateValueFormat(colFmt, value, locale) {
|
|
23
|
+
// NOTE: this syntax checks for both undefined and null.
|
|
24
|
+
if (value == null) {
|
|
25
|
+
return value;
|
|
26
|
+
}
|
|
27
|
+
const fmt = colFmt.valueFormat;
|
|
28
|
+
if (!fmt) {
|
|
29
|
+
return value;
|
|
30
|
+
}
|
|
31
|
+
try {
|
|
32
|
+
switch (colFmt.type) {
|
|
33
|
+
case 'text':
|
|
34
|
+
return formatText(value, fmt);
|
|
35
|
+
case 'number':
|
|
36
|
+
return formatNumber(value, fmt);
|
|
37
|
+
case 'date':
|
|
38
|
+
return formatDate(value, fmt, locale);
|
|
39
|
+
default:
|
|
40
|
+
return value;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
return value;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
function formatTextEnabled(fmt) {
|
|
48
|
+
return fmt.txtCaseVal !== DEFAULT_VALUE_FORMAT_VALUE;
|
|
49
|
+
}
|
|
50
|
+
function formatText(value, fmt) {
|
|
51
|
+
if (!formatTextEnabled(fmt)) {
|
|
52
|
+
return value;
|
|
53
|
+
}
|
|
54
|
+
const str = String(value);
|
|
55
|
+
switch (fmt.txtCaseVal) {
|
|
56
|
+
case 'lower': return str.toLowerCase();
|
|
57
|
+
case 'upper': return str.toUpperCase();
|
|
58
|
+
case 'title':
|
|
59
|
+
return str
|
|
60
|
+
.toLowerCase()
|
|
61
|
+
.split(' ')
|
|
62
|
+
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
|
|
63
|
+
.join(' ');
|
|
64
|
+
default:
|
|
65
|
+
return value;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
export function formatNumberEnabled(fmt) {
|
|
69
|
+
return fmt.numCommaVal !== DEFAULT_VALUE_FORMAT_VALUE ||
|
|
70
|
+
fmt.numCurrencyVal !== DEFAULT_VALUE_FORMAT_VALUE ||
|
|
71
|
+
fmt.numPercentVal !== DEFAULT_VALUE_FORMAT_VALUE ||
|
|
72
|
+
fmt.numRoundVal !== DEFAULT_VALUE_FORMAT_VALUE;
|
|
73
|
+
}
|
|
74
|
+
function formatNumber(value, fmt) {
|
|
75
|
+
if (!formatNumberEnabled(fmt)) {
|
|
76
|
+
return value;
|
|
77
|
+
}
|
|
78
|
+
let num = parseFloat(value);
|
|
79
|
+
if (isNaN(num)) {
|
|
80
|
+
return value;
|
|
81
|
+
}
|
|
82
|
+
/*
|
|
83
|
+
We need to be intentional about the order of operations for number formatting.
|
|
84
|
+
For example, some formatting operations will transform the number to a string.
|
|
85
|
+
If we need to perform some arithmetic or round it, we need to do so first before
|
|
86
|
+
number is transformed into a string.
|
|
87
|
+
*/
|
|
88
|
+
if (fmt.numPercentVal === '100') {
|
|
89
|
+
num *= 100;
|
|
90
|
+
}
|
|
91
|
+
const decimals = parseInt(fmt.numRoundVal ?? '', 10);
|
|
92
|
+
if (!isNaN(decimals)) {
|
|
93
|
+
num = parseFloat(num.toFixed(decimals));
|
|
94
|
+
}
|
|
95
|
+
let formatted = num;
|
|
96
|
+
if (fmt.numCommaVal === 'on') {
|
|
97
|
+
formatted = num.toLocaleString();
|
|
98
|
+
}
|
|
99
|
+
if (fmt.numCurrencyVal === 'dollar') {
|
|
100
|
+
formatted = `$${formatted}`;
|
|
101
|
+
}
|
|
102
|
+
if (fmt.numPercentVal === '1' || fmt.numPercentVal === '100') {
|
|
103
|
+
formatted = `${formatted}%`;
|
|
104
|
+
}
|
|
105
|
+
return formatted;
|
|
106
|
+
}
|
|
107
|
+
function formatDate(value, fmt, locale) {
|
|
108
|
+
let valueStr;
|
|
109
|
+
if (value instanceof Date) {
|
|
110
|
+
valueStr = value.toISOString();
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
113
|
+
valueStr = String(value);
|
|
114
|
+
}
|
|
115
|
+
// Make sure we are dealing with a valid date string before we go any further.
|
|
116
|
+
if (!isDateString(valueStr)) {
|
|
117
|
+
return value;
|
|
118
|
+
}
|
|
119
|
+
// Remove time zone and milliseconds from date string.
|
|
120
|
+
valueStr = valueStr.replace(/Z/i, '');
|
|
121
|
+
valueStr = valueStr.replace(/\.\d+$/, '');
|
|
122
|
+
// Initialize date options and functionality.
|
|
123
|
+
const options = { hourCycle: 'h23' };
|
|
124
|
+
let formatDateEnabled = false;
|
|
125
|
+
const setOpt = (k, v) => {
|
|
126
|
+
if (v !== DEFAULT_VALUE_FORMAT_VALUE) {
|
|
127
|
+
options[k] = v;
|
|
128
|
+
formatDateEnabled = true;
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
setOpt('year', fmt.dteYearVal);
|
|
132
|
+
setOpt('month', fmt.dteMonthVal);
|
|
133
|
+
setOpt('day', fmt.dteDayVal);
|
|
134
|
+
setOpt('weekday', fmt.dteWeekdayVal);
|
|
135
|
+
setOpt('hour', fmt.dteHourVal);
|
|
136
|
+
setOpt('minute', fmt.dteMinuteVal);
|
|
137
|
+
setOpt('second', fmt.dteSecondVal);
|
|
138
|
+
setOpt('hourCycle', fmt.dteHourcycleVal === 'h24' ? 'h23' : fmt.dteHourcycleVal);
|
|
139
|
+
// Handle simple date edge cases.
|
|
140
|
+
if (valueStr.length === 10) {
|
|
141
|
+
// Special handling for ISO-ish short date string (YYYY-AA-BB or AA-BB-YYYY).
|
|
142
|
+
const c4 = valueStr.at(4);
|
|
143
|
+
const c5 = valueStr.at(5);
|
|
144
|
+
if (c4 === '-' || c4 === '/' || c5 === '-' || c5 === '/') {
|
|
145
|
+
valueStr += ' 00:00:00'; // Make sure time is zero'd out.
|
|
146
|
+
if (!formatDateEnabled) {
|
|
147
|
+
setOpt('day', '2-digit');
|
|
148
|
+
setOpt('month', '2-digit');
|
|
149
|
+
setOpt('year', 'numeric');
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
else if (valueStr.length === 8) {
|
|
154
|
+
// Special handling for ISO-ish shorter date string (YY-MM-DD in any order).
|
|
155
|
+
const c2 = valueStr.at(2);
|
|
156
|
+
const c5 = valueStr.at(5);
|
|
157
|
+
if ((c2 === '-' && c5 === '-') || (c2 === '/' && c5 === '/')) {
|
|
158
|
+
valueStr += ' 00:00:00'; // Make sure time is zero'd out.
|
|
159
|
+
if (!formatDateEnabled) {
|
|
160
|
+
setOpt('day', '2-digit');
|
|
161
|
+
setOpt('month', '2-digit');
|
|
162
|
+
setOpt('year', '2-digit');
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
let dteStr = new Date(valueStr).toLocaleString(locale, options);
|
|
167
|
+
// NOTE: by default, we should use slash as separator.
|
|
168
|
+
if (fmt.dteSeparatorVal === DEFAULT_VALUE_FORMAT_VALUE) {
|
|
169
|
+
dteStr = dteStr.replaceAll('-', '/');
|
|
170
|
+
}
|
|
171
|
+
else if (fmt.dteSeparatorVal === 'dash') {
|
|
172
|
+
dteStr = dteStr.replaceAll('/', '-');
|
|
173
|
+
}
|
|
174
|
+
else if (fmt.dteSeparatorVal === 'slash') {
|
|
175
|
+
dteStr = dteStr.replaceAll('-', '/');
|
|
176
|
+
}
|
|
177
|
+
dteStr = dteStr.replaceAll(',', '');
|
|
178
|
+
// We want to preserve simple dates.
|
|
179
|
+
// If no is formatting detected, the date's length should not be greater than the original value's.
|
|
180
|
+
if (!formatDateEnabled) {
|
|
181
|
+
dteStr = dteStr.substring(0, value.length);
|
|
182
|
+
dteStr = dteStr.trim();
|
|
183
|
+
// Trim any separator characters off.
|
|
184
|
+
if (dteStr.endsWith('-') || dteStr.endsWith('/') || dteStr.endsWith(':') || dteStr.endsWith('.') || dteStr.endsWith('T')) {
|
|
185
|
+
dteStr = dteStr.substring(0, dteStr.length - 1).trim();
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
return dteStr;
|
|
189
|
+
}
|
|
190
|
+
export var ConditionalFormatRule;
|
|
191
|
+
(function (ConditionalFormatRule) {
|
|
192
|
+
// TEXT:
|
|
193
|
+
ConditionalFormatRule["CONTAINS"] = "conditional-format-rule-contains";
|
|
194
|
+
ConditionalFormatRule["DOES_NOT_CONTAIN"] = "conditional-format-rule-does-not-contain";
|
|
195
|
+
ConditionalFormatRule["BEGINS_WITH"] = "conditional-format-rule-begins-with";
|
|
196
|
+
ConditionalFormatRule["ENDS_WITH"] = "conditional-format-rule-ends-with";
|
|
197
|
+
// NUMBER:
|
|
198
|
+
ConditionalFormatRule["GREATER_THAN"] = "conditional-format-rule-greater-than";
|
|
199
|
+
ConditionalFormatRule["GREATER_THAN_OR_EQUAL_TO"] = "conditional-format-rule-greater-than-or-equal-to";
|
|
200
|
+
ConditionalFormatRule["LESS_THAN"] = "conditional-format-rule-less-than";
|
|
201
|
+
ConditionalFormatRule["LESS_THAN_OR_EQUAL_TO"] = "conditional-format-rule-less-than-or-equal-to";
|
|
202
|
+
ConditionalFormatRule["BETWEEN"] = "conditional-format-rule-between";
|
|
203
|
+
// ANY:
|
|
204
|
+
ConditionalFormatRule["EQUALS"] = "conditional-format-rule-equals";
|
|
205
|
+
ConditionalFormatRule["DOES_NOT_EQUAL"] = "conditional-format-rule-does-not-equal";
|
|
206
|
+
ConditionalFormatRule["BLANK"] = "conditional-format-rule-blank";
|
|
207
|
+
ConditionalFormatRule["NOT_BLANK"] = "conditional-format-rule-not-blank";
|
|
208
|
+
})(ConditionalFormatRule || (ConditionalFormatRule = {}));
|
|
209
|
+
export const CONDITIONAL_FORMAT_RULES = [
|
|
210
|
+
ConditionalFormatRule.EQUALS,
|
|
211
|
+
ConditionalFormatRule.DOES_NOT_EQUAL,
|
|
212
|
+
ConditionalFormatRule.BLANK,
|
|
213
|
+
ConditionalFormatRule.NOT_BLANK,
|
|
214
|
+
ConditionalFormatRule.CONTAINS,
|
|
215
|
+
ConditionalFormatRule.DOES_NOT_CONTAIN,
|
|
216
|
+
ConditionalFormatRule.BEGINS_WITH,
|
|
217
|
+
ConditionalFormatRule.ENDS_WITH,
|
|
218
|
+
ConditionalFormatRule.GREATER_THAN,
|
|
219
|
+
ConditionalFormatRule.GREATER_THAN_OR_EQUAL_TO,
|
|
220
|
+
ConditionalFormatRule.LESS_THAN,
|
|
221
|
+
ConditionalFormatRule.LESS_THAN_OR_EQUAL_TO,
|
|
222
|
+
ConditionalFormatRule.BETWEEN
|
|
223
|
+
];
|
|
224
|
+
export const CONDITIONAL_FORMAT_RULES_TEXT = [
|
|
225
|
+
ConditionalFormatRule.EQUALS,
|
|
226
|
+
ConditionalFormatRule.DOES_NOT_EQUAL,
|
|
227
|
+
ConditionalFormatRule.BLANK,
|
|
228
|
+
ConditionalFormatRule.NOT_BLANK,
|
|
229
|
+
ConditionalFormatRule.CONTAINS,
|
|
230
|
+
ConditionalFormatRule.DOES_NOT_CONTAIN,
|
|
231
|
+
ConditionalFormatRule.BEGINS_WITH,
|
|
232
|
+
ConditionalFormatRule.ENDS_WITH
|
|
233
|
+
];
|
|
234
|
+
export const CONDITIONAL_FORMAT_RULES_NUMBER = [
|
|
235
|
+
ConditionalFormatRule.EQUALS,
|
|
236
|
+
ConditionalFormatRule.DOES_NOT_EQUAL,
|
|
237
|
+
ConditionalFormatRule.BLANK,
|
|
238
|
+
ConditionalFormatRule.NOT_BLANK,
|
|
239
|
+
ConditionalFormatRule.GREATER_THAN,
|
|
240
|
+
ConditionalFormatRule.GREATER_THAN_OR_EQUAL_TO,
|
|
241
|
+
ConditionalFormatRule.LESS_THAN,
|
|
242
|
+
ConditionalFormatRule.LESS_THAN_OR_EQUAL_TO,
|
|
243
|
+
ConditionalFormatRule.BETWEEN
|
|
244
|
+
];
|
|
245
|
+
export const CONDITIONAL_FORMAT_RULES_BOOLEAN = [
|
|
246
|
+
ConditionalFormatRule.EQUALS,
|
|
247
|
+
ConditionalFormatRule.DOES_NOT_EQUAL,
|
|
248
|
+
ConditionalFormatRule.BLANK,
|
|
249
|
+
ConditionalFormatRule.NOT_BLANK
|
|
250
|
+
];
|
|
251
|
+
export var ConditionalFormatStyle;
|
|
252
|
+
(function (ConditionalFormatStyle) {
|
|
253
|
+
ConditionalFormatStyle["NONE"] = "conditional-format-style-none";
|
|
254
|
+
ConditionalFormatStyle["GREEN_BACKGROUND"] = "conditional-format-style-green-background";
|
|
255
|
+
ConditionalFormatStyle["YELLOW_BACKGROUND"] = "conditional-format-style-yellow-background";
|
|
256
|
+
ConditionalFormatStyle["RED_BACKGROUND"] = "conditional-format-style-red-background";
|
|
257
|
+
ConditionalFormatStyle["BLUE_BACKGROUND"] = "conditional-format-style-blue-background";
|
|
258
|
+
ConditionalFormatStyle["PURPLE_BACKGROUND"] = "conditional-format-style-purple-background";
|
|
259
|
+
ConditionalFormatStyle["BOLD"] = "conditional-format-style-bold";
|
|
260
|
+
ConditionalFormatStyle["GREEN_BOLD"] = "conditional-format-style-green-bold";
|
|
261
|
+
ConditionalFormatStyle["YELLOW_BOLD"] = "conditional-format-style-yellow-bold";
|
|
262
|
+
ConditionalFormatStyle["RED_BOLD"] = "conditional-format-style-red-bold";
|
|
263
|
+
ConditionalFormatStyle["BLUE_BOLD"] = "conditional-format-style-blue-bold";
|
|
264
|
+
ConditionalFormatStyle["PURPLE_BOLD"] = "conditional-format-style-purple-bold";
|
|
265
|
+
ConditionalFormatStyle["NOT_IMPORTANT"] = "conditional-format-style-not-important";
|
|
266
|
+
})(ConditionalFormatStyle || (ConditionalFormatStyle = {}));
|
|
267
|
+
export function evaluateConditionalFormat(condFmt, dataType, value) {
|
|
268
|
+
const isBlank = (v) => v === null || v === undefined || (dataType !== 'number' && dataType !== 'boolean' && v === '');
|
|
269
|
+
const v1 = condFmt.value;
|
|
270
|
+
const v2 = condFmt.value2;
|
|
271
|
+
const ruleHandlers = {
|
|
272
|
+
[ConditionalFormatRule.EQUALS]: () => value == v1,
|
|
273
|
+
[ConditionalFormatRule.DOES_NOT_EQUAL]: () => value != v1,
|
|
274
|
+
[ConditionalFormatRule.GREATER_THAN]: () => value > v1,
|
|
275
|
+
[ConditionalFormatRule.GREATER_THAN_OR_EQUAL_TO]: () => value >= v1,
|
|
276
|
+
[ConditionalFormatRule.LESS_THAN]: () => value < v1,
|
|
277
|
+
[ConditionalFormatRule.LESS_THAN_OR_EQUAL_TO]: () => value <= v1,
|
|
278
|
+
[ConditionalFormatRule.BETWEEN]: () => value >= v1 && value <= (v2 ?? 0),
|
|
279
|
+
[ConditionalFormatRule.CONTAINS]: () => String(value).includes(v1),
|
|
280
|
+
[ConditionalFormatRule.DOES_NOT_CONTAIN]: () => !String(value).includes(v1),
|
|
281
|
+
[ConditionalFormatRule.BEGINS_WITH]: () => String(value).startsWith(v1),
|
|
282
|
+
[ConditionalFormatRule.ENDS_WITH]: () => String(value).endsWith(v1),
|
|
283
|
+
[ConditionalFormatRule.BLANK]: () => isBlank(value),
|
|
284
|
+
[ConditionalFormatRule.NOT_BLANK]: () => !isBlank(value)
|
|
285
|
+
};
|
|
286
|
+
try {
|
|
287
|
+
const fn = ruleHandlers[condFmt.rule];
|
|
288
|
+
return fn ? fn() : false;
|
|
289
|
+
}
|
|
290
|
+
catch {
|
|
291
|
+
return false;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
export function evaluateValueStyles(datagridState, columnName, value) {
|
|
295
|
+
const styles = [];
|
|
296
|
+
// Reminder: Value Formatting is performed **before** Conditional Formatting.
|
|
297
|
+
// https://chatwms.io/user-manual/concepts/column-formatting#conditional-formatting
|
|
298
|
+
const colFmt = datagridState.columnFormats?.find(colFmt => colFmt.displayName === columnName);
|
|
299
|
+
if (!!colFmt) {
|
|
300
|
+
for (const condFmt of colFmt.conditionalFormats.slice().reverse()) {
|
|
301
|
+
// `conditionalFormats` has already been sorted by `sequence`.
|
|
302
|
+
// NOTE: need to reverse the array for sequence to be correctly applied.
|
|
303
|
+
// NOTE: using `slice` to make shallow copy of array since `reverse` does so in place.
|
|
304
|
+
if (evaluateConditionalFormat(condFmt, colFmt.type, formatNumberEnabled(colFmt.valueFormat) ? stripNumericValueFormat(value) : value)) {
|
|
305
|
+
styles.push(condFmt.style);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
return styles;
|
|
310
|
+
}
|
|
311
|
+
export function initDatagridCondition() {
|
|
312
|
+
return {
|
|
313
|
+
columnName: '',
|
|
314
|
+
dataType: '',
|
|
315
|
+
rule: '',
|
|
316
|
+
value: ''
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
export async function summarizeCondition(condition, localeMessageFn) {
|
|
320
|
+
let ruleStr = '';
|
|
321
|
+
switch (condition.rule) {
|
|
322
|
+
case ConditionalFormatRule.BETWEEN:
|
|
323
|
+
ruleStr = `${await localeMessageFn(CHATWMS_DEFAULT_LANGUAGE, condition.rule)} ${condition.value} ${await localeMessageFn(CHATWMS_DEFAULT_LANGUAGE, 'conditional-format-rule-and')} ${condition.value2}`;
|
|
324
|
+
break;
|
|
325
|
+
case ConditionalFormatRule.BLANK:
|
|
326
|
+
case ConditionalFormatRule.NOT_BLANK:
|
|
327
|
+
ruleStr = `${await localeMessageFn(CHATWMS_DEFAULT_LANGUAGE, condition.rule)}`;
|
|
328
|
+
break;
|
|
329
|
+
default:
|
|
330
|
+
if (condition.dataType === 'text') {
|
|
331
|
+
ruleStr = `${await localeMessageFn(CHATWMS_DEFAULT_LANGUAGE, condition.rule)} '${condition.value}'`;
|
|
332
|
+
}
|
|
333
|
+
else {
|
|
334
|
+
ruleStr = `${await localeMessageFn(CHATWMS_DEFAULT_LANGUAGE, condition.rule)} ${condition.value}`;
|
|
335
|
+
}
|
|
336
|
+
break;
|
|
337
|
+
}
|
|
338
|
+
return `${condition.columnName} ${ruleStr}`;
|
|
339
|
+
}
|
|
340
|
+
export async function summarizeConditionMarkdown(condition, localeMessageFn) {
|
|
341
|
+
let ruleStr = '';
|
|
342
|
+
switch (condition.rule) {
|
|
343
|
+
case ConditionalFormatRule.BETWEEN:
|
|
344
|
+
ruleStr = `${await localeMessageFn(CHATWMS_DEFAULT_LANGUAGE, condition.rule)} ${condition.value} ${await localeMessageFn(CHATWMS_DEFAULT_LANGUAGE, 'conditional-format-rule-and')} ${condition.value2}`;
|
|
345
|
+
break;
|
|
346
|
+
case ConditionalFormatRule.BLANK:
|
|
347
|
+
case ConditionalFormatRule.NOT_BLANK:
|
|
348
|
+
ruleStr = `${await localeMessageFn(CHATWMS_DEFAULT_LANGUAGE, condition.rule)}`;
|
|
349
|
+
break;
|
|
350
|
+
default:
|
|
351
|
+
if (condition.dataType === 'text') {
|
|
352
|
+
ruleStr = `${await localeMessageFn(CHATWMS_DEFAULT_LANGUAGE, condition.rule)} '${condition.value}'`;
|
|
353
|
+
}
|
|
354
|
+
else {
|
|
355
|
+
ruleStr = `${await localeMessageFn(CHATWMS_DEFAULT_LANGUAGE, condition.rule)} ${condition.value}`;
|
|
356
|
+
}
|
|
357
|
+
break;
|
|
358
|
+
}
|
|
359
|
+
return `\`${condition.columnName}\` ${ruleStr}`;
|
|
360
|
+
}
|
|
361
|
+
export async function summarizeConditionHtml(condition, localeMessageFn) {
|
|
362
|
+
let ruleStr = '';
|
|
363
|
+
switch (condition.rule) {
|
|
364
|
+
case ConditionalFormatRule.BETWEEN:
|
|
365
|
+
ruleStr = `${await localeMessageFn(CHATWMS_DEFAULT_LANGUAGE, condition.rule)} ${condition.value} ${await localeMessageFn(CHATWMS_DEFAULT_LANGUAGE, 'conditional-format-rule-and')} ${condition.value2}`;
|
|
366
|
+
break;
|
|
367
|
+
case ConditionalFormatRule.BLANK:
|
|
368
|
+
case ConditionalFormatRule.NOT_BLANK:
|
|
369
|
+
ruleStr = `${await localeMessageFn(CHATWMS_DEFAULT_LANGUAGE, condition.rule)}`;
|
|
370
|
+
break;
|
|
371
|
+
default:
|
|
372
|
+
if (condition.dataType === 'text') {
|
|
373
|
+
ruleStr = `${await localeMessageFn(CHATWMS_DEFAULT_LANGUAGE, condition.rule)} '${condition.value}'`;
|
|
374
|
+
}
|
|
375
|
+
else {
|
|
376
|
+
ruleStr = `${await localeMessageFn(CHATWMS_DEFAULT_LANGUAGE, condition.rule)} ${condition.value}`;
|
|
377
|
+
}
|
|
378
|
+
break;
|
|
379
|
+
}
|
|
380
|
+
return `<code>${condition.columnName}</code> ${ruleStr}`;
|
|
381
|
+
}
|
|
382
|
+
export function codifyCondition(condition) {
|
|
383
|
+
// NOTE: since we check for action/unsafe SQL with every query, we do not need to check for SQL injection here.
|
|
384
|
+
if (condition.dataType === 'number') {
|
|
385
|
+
const v1 = !condition.value || condition.value == '' ? 0 : condition.value;
|
|
386
|
+
const v2 = !condition.value2 || condition.value2 == '' ? 0 : condition.value2;
|
|
387
|
+
// Need to wrap in quotes due to how ChatWMS will alias columns (ex: location_id -> Location ID).
|
|
388
|
+
const quotedColumnName = `"${condition.columnName}"`;
|
|
389
|
+
switch (condition.rule) {
|
|
390
|
+
case ConditionalFormatRule.EQUALS:
|
|
391
|
+
return `${quotedColumnName} = ${v1}`;
|
|
392
|
+
case ConditionalFormatRule.DOES_NOT_EQUAL:
|
|
393
|
+
return `${quotedColumnName} != ${v1}`;
|
|
394
|
+
case ConditionalFormatRule.GREATER_THAN:
|
|
395
|
+
return `${quotedColumnName} > ${v1}`;
|
|
396
|
+
case ConditionalFormatRule.GREATER_THAN_OR_EQUAL_TO:
|
|
397
|
+
return `${quotedColumnName} >= ${v1}`;
|
|
398
|
+
case ConditionalFormatRule.LESS_THAN:
|
|
399
|
+
return `${quotedColumnName} < ${v1}`;
|
|
400
|
+
case ConditionalFormatRule.LESS_THAN_OR_EQUAL_TO:
|
|
401
|
+
return `${quotedColumnName} <= ${v1}`;
|
|
402
|
+
case ConditionalFormatRule.BETWEEN:
|
|
403
|
+
return `${quotedColumnName} BETWEEN ${v1} AND ${v2}`;
|
|
404
|
+
case ConditionalFormatRule.BLANK:
|
|
405
|
+
return `${quotedColumnName} IS NULL`;
|
|
406
|
+
case ConditionalFormatRule.NOT_BLANK:
|
|
407
|
+
return `${quotedColumnName} IS NOT NULL`;
|
|
408
|
+
default:
|
|
409
|
+
return '1 = 2'; // Fail if we get here.
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
else if (condition.dataType === 'date') {
|
|
413
|
+
const v1 = !condition.value || condition.value == '' ? 0 : condition.value;
|
|
414
|
+
const v2 = !condition.value2 || condition.value2 == '' ? 0 : condition.value2;
|
|
415
|
+
// Need to wrap in quotes due to how ChatWMS will alias columns (ex: location_id -> Location ID).
|
|
416
|
+
const quotedColumnName = `"${condition.columnName}"`;
|
|
417
|
+
switch (condition.rule) {
|
|
418
|
+
case ConditionalFormatRule.EQUALS:
|
|
419
|
+
return `${quotedColumnName} = '${v1}'`;
|
|
420
|
+
case ConditionalFormatRule.DOES_NOT_EQUAL:
|
|
421
|
+
return `${quotedColumnName} != '${v1}'`;
|
|
422
|
+
case ConditionalFormatRule.GREATER_THAN:
|
|
423
|
+
return `${quotedColumnName} > '${v1}'`;
|
|
424
|
+
case ConditionalFormatRule.GREATER_THAN_OR_EQUAL_TO:
|
|
425
|
+
return `${quotedColumnName} >= '${v1}'`;
|
|
426
|
+
case ConditionalFormatRule.LESS_THAN:
|
|
427
|
+
return `${quotedColumnName} < '${v1}'`;
|
|
428
|
+
case ConditionalFormatRule.LESS_THAN_OR_EQUAL_TO:
|
|
429
|
+
return `${quotedColumnName} <= '${v1}'`;
|
|
430
|
+
case ConditionalFormatRule.BETWEEN:
|
|
431
|
+
return `${quotedColumnName} BETWEEN '${v1}' AND '${v2}'`;
|
|
432
|
+
case ConditionalFormatRule.BLANK:
|
|
433
|
+
return `${quotedColumnName} IS NULL`;
|
|
434
|
+
case ConditionalFormatRule.NOT_BLANK:
|
|
435
|
+
return `${quotedColumnName} IS NOT NULL`;
|
|
436
|
+
default:
|
|
437
|
+
return '1 = 2'; // Fail if we get here.
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
else {
|
|
441
|
+
// NOTE: it is a safe assumption to trim any whitespace off the value.
|
|
442
|
+
const v1 = (condition.value ?? '__DUMMY__').trim();
|
|
443
|
+
// Need to wrap in quotes due to how ChatWMS will alias columns (ex: location_id -> Location ID).
|
|
444
|
+
// NOTE: wrapping column and values in `UPPER` to support case insensitivity.
|
|
445
|
+
const quotedColumnName = `UPPER("${condition.columnName}")`;
|
|
446
|
+
switch (condition.rule) {
|
|
447
|
+
case ConditionalFormatRule.EQUALS:
|
|
448
|
+
return `${quotedColumnName} = UPPER('${v1}')`;
|
|
449
|
+
case ConditionalFormatRule.DOES_NOT_EQUAL:
|
|
450
|
+
return `${quotedColumnName} != UPPER('${v1}')`;
|
|
451
|
+
case ConditionalFormatRule.CONTAINS:
|
|
452
|
+
return `${quotedColumnName} LIKE UPPER('%${v1}%')`;
|
|
453
|
+
case ConditionalFormatRule.DOES_NOT_CONTAIN:
|
|
454
|
+
return `${quotedColumnName} NOT LIKE UPPER('%${v1}%')`;
|
|
455
|
+
case ConditionalFormatRule.BEGINS_WITH:
|
|
456
|
+
return `${quotedColumnName} LIKE UPPER('${v1}%')`;
|
|
457
|
+
case ConditionalFormatRule.ENDS_WITH:
|
|
458
|
+
return `${quotedColumnName} LIKE UPPER('%${v1}')`;
|
|
459
|
+
case ConditionalFormatRule.BLANK:
|
|
460
|
+
return `${quotedColumnName} IS NULL`;
|
|
461
|
+
case ConditionalFormatRule.NOT_BLANK:
|
|
462
|
+
return `${quotedColumnName} IS NOT NULL`;
|
|
463
|
+
default:
|
|
464
|
+
return '1 = 2'; // Fail if we get here.
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
/**
|
|
469
|
+
* Equivalent of `parseNumeric` in front end
|
|
470
|
+
*
|
|
471
|
+
* https://github.com/cellaware/chatwms-az-swa-ng/blob/development/src/app/utils/data.ts#L126
|
|
472
|
+
*/
|
|
473
|
+
export function stripNumericValueFormat(value) {
|
|
474
|
+
// NOTE: this syntax checks for both undefined and null.
|
|
475
|
+
if (value == null) {
|
|
476
|
+
return null;
|
|
477
|
+
}
|
|
478
|
+
if (typeof value === 'number') {
|
|
479
|
+
return value;
|
|
480
|
+
}
|
|
481
|
+
// Remove commas, dollar signs, percent signs
|
|
482
|
+
const cleaned = value.replace(/[$,%]/g, '').replace(/,/g, '');
|
|
483
|
+
// NOTE: not sure we need this based on how this function is used in these contexts...
|
|
484
|
+
// const number = parseFloat(cleaned);
|
|
485
|
+
// return isNaN(number) ? null : number;
|
|
486
|
+
return cleaned;
|
|
487
|
+
}
|
|
488
|
+
function buildDatagridConditionEvaluator(condition) {
|
|
489
|
+
const v1 = condition.value;
|
|
490
|
+
const v2 = condition.value2;
|
|
491
|
+
const isBlank = (v) => v === null || v === undefined || (condition.dataType !== 'number' && condition.dataType !== 'boolean' && v === '');
|
|
492
|
+
if (condition.dataType === 'number') {
|
|
493
|
+
switch (condition.rule) {
|
|
494
|
+
case ConditionalFormatRule.EQUALS:
|
|
495
|
+
return v => stripNumericValueFormat(v) == v1;
|
|
496
|
+
case ConditionalFormatRule.DOES_NOT_EQUAL:
|
|
497
|
+
return v => stripNumericValueFormat(v) != v1;
|
|
498
|
+
case ConditionalFormatRule.GREATER_THAN:
|
|
499
|
+
return v => stripNumericValueFormat(v) > v1;
|
|
500
|
+
case ConditionalFormatRule.GREATER_THAN_OR_EQUAL_TO:
|
|
501
|
+
return v => stripNumericValueFormat(v) >= v1;
|
|
502
|
+
case ConditionalFormatRule.LESS_THAN:
|
|
503
|
+
return v => stripNumericValueFormat(v) < v1;
|
|
504
|
+
case ConditionalFormatRule.LESS_THAN_OR_EQUAL_TO:
|
|
505
|
+
return v => stripNumericValueFormat(v) <= v1;
|
|
506
|
+
case ConditionalFormatRule.BETWEEN:
|
|
507
|
+
return v => stripNumericValueFormat(v) >= v1 && stripNumericValueFormat(v) <= (v2 ?? v1); // fallback to v1 if v2 missing
|
|
508
|
+
case ConditionalFormatRule.BLANK:
|
|
509
|
+
return v => isBlank(v);
|
|
510
|
+
case ConditionalFormatRule.NOT_BLANK:
|
|
511
|
+
return v => !isBlank(v);
|
|
512
|
+
default:
|
|
513
|
+
return () => false;
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
else {
|
|
517
|
+
switch (condition.rule) {
|
|
518
|
+
case ConditionalFormatRule.EQUALS:
|
|
519
|
+
return v => v == v1;
|
|
520
|
+
case ConditionalFormatRule.DOES_NOT_EQUAL:
|
|
521
|
+
return v => v != v1;
|
|
522
|
+
case ConditionalFormatRule.CONTAINS:
|
|
523
|
+
return v => typeof v === 'string' && v.includes(v1);
|
|
524
|
+
case ConditionalFormatRule.DOES_NOT_CONTAIN:
|
|
525
|
+
return v => typeof v === 'string' && !v.includes(v1);
|
|
526
|
+
case ConditionalFormatRule.BEGINS_WITH:
|
|
527
|
+
return v => typeof v === 'string' && v.startsWith(v1);
|
|
528
|
+
case ConditionalFormatRule.ENDS_WITH:
|
|
529
|
+
return v => typeof v === 'string' && v.endsWith(v1);
|
|
530
|
+
case ConditionalFormatRule.BLANK:
|
|
531
|
+
return v => isBlank(v);
|
|
532
|
+
case ConditionalFormatRule.NOT_BLANK:
|
|
533
|
+
return v => !isBlank(v);
|
|
534
|
+
default:
|
|
535
|
+
return () => false;
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
function buildPivotDatagridConditionEvaluator(condition, columnNames) {
|
|
540
|
+
const fn = buildDatagridConditionEvaluator(condition);
|
|
541
|
+
return row => columnNames.some(columnName => fn(row[columnName]));
|
|
542
|
+
}
|
|
543
|
+
function evaluateDatagridCondition(rows, condition) {
|
|
544
|
+
const fn = buildDatagridConditionEvaluator(condition);
|
|
545
|
+
return rows.filter(row => fn(row[condition.columnName]));
|
|
546
|
+
}
|
|
547
|
+
function evaluatePivotDatagridCondition(rows, condition, mappedColumnNames) {
|
|
548
|
+
let columnNames = [condition.columnName];
|
|
549
|
+
const mappedColumnNamesSet = mappedColumnNames.get(condition.columnName);
|
|
550
|
+
if (!!mappedColumnNamesSet) {
|
|
551
|
+
columnNames = Array.from(mappedColumnNamesSet);
|
|
552
|
+
}
|
|
553
|
+
const fn = buildPivotDatagridConditionEvaluator(condition, columnNames);
|
|
554
|
+
return rows.filter(row => fn(row));
|
|
555
|
+
}
|
|
556
|
+
function parseColumnState(columnState) {
|
|
557
|
+
const rowGroupCols = [];
|
|
558
|
+
const pivotCols = [];
|
|
559
|
+
const valueCols = [];
|
|
560
|
+
const sortModel = [];
|
|
561
|
+
for (const col of columnState) {
|
|
562
|
+
// Row Group Columns:
|
|
563
|
+
// NOTE: we only want the highest level data.
|
|
564
|
+
if (col.rowGroup && col.rowGroupIndex === 0) {
|
|
565
|
+
rowGroupCols.push({
|
|
566
|
+
field: col.colId
|
|
567
|
+
});
|
|
568
|
+
}
|
|
569
|
+
// Pivot Columns:
|
|
570
|
+
// NOTE: we only want the highest level data.
|
|
571
|
+
if (col.pivot && col.pivotIndex === 0) {
|
|
572
|
+
pivotCols.push({
|
|
573
|
+
field: col.colId
|
|
574
|
+
});
|
|
575
|
+
}
|
|
576
|
+
// Value Columns (Aggregations):
|
|
577
|
+
if (col.aggFunc) {
|
|
578
|
+
valueCols.push({
|
|
579
|
+
field: col.colId,
|
|
580
|
+
aggFunc: col.aggFunc
|
|
581
|
+
});
|
|
582
|
+
}
|
|
583
|
+
// Sorting:
|
|
584
|
+
if (col.sort) {
|
|
585
|
+
sortModel.push({
|
|
586
|
+
colId: col.colId,
|
|
587
|
+
sort: col.sort
|
|
588
|
+
});
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
// NOTE: commenting this out since we only care about the highest level data.
|
|
592
|
+
// rowGroupCols.sort((a, b) => {
|
|
593
|
+
// const iA = columnState.find(c => c.colId === a.field)?.rowGroupIndex ?? 0;
|
|
594
|
+
// const iB = columnState.find(c => c.colId === b.field)?.rowGroupIndex ?? 0;
|
|
595
|
+
// return iA - iB;
|
|
596
|
+
// });
|
|
597
|
+
// pivotCols.sort((a, b) => {
|
|
598
|
+
// const iA = columnState.find(c => c.colId === a.field)?.pivotIndex ?? 0;
|
|
599
|
+
// const iB = columnState.find(c => c.colId === b.field)?.pivotIndex ?? 0;
|
|
600
|
+
// return iA - iB;
|
|
601
|
+
// });
|
|
602
|
+
sortModel.sort((a, b) => {
|
|
603
|
+
const iA = columnState.find(c => c.colId === a.colId)?.sortIndex ?? 0;
|
|
604
|
+
const iB = columnState.find(c => c.colId === b.colId)?.sortIndex ?? 0;
|
|
605
|
+
return iA - iB;
|
|
606
|
+
});
|
|
607
|
+
return {
|
|
608
|
+
rowGroupCols,
|
|
609
|
+
pivotCols,
|
|
610
|
+
valueCols,
|
|
611
|
+
sortModel
|
|
612
|
+
};
|
|
613
|
+
}
|
|
614
|
+
function filterRows(data, filterModel) {
|
|
615
|
+
const textOps = {
|
|
616
|
+
equals: (v, f) => (v ?? '').toLowerCase() === f.toLowerCase(),
|
|
617
|
+
notEqual: (v, f) => (v ?? '').toLowerCase() !== f.toLowerCase(),
|
|
618
|
+
contains: (v, f) => (v ?? '').toLowerCase().includes(f.toLowerCase()),
|
|
619
|
+
notContains: (v, f) => !(v ?? '').toLowerCase().includes(f.toLowerCase()),
|
|
620
|
+
startsWith: (v, f) => (v ?? '').toLowerCase().startsWith(f.toLowerCase()),
|
|
621
|
+
endsWith: (v, f) => (v ?? '').toLowerCase().endsWith(f.toLowerCase()),
|
|
622
|
+
blank: (v) => v == null || v === '',
|
|
623
|
+
notBlank: (v) => !(v == null || v === '')
|
|
624
|
+
};
|
|
625
|
+
const numberOps = {
|
|
626
|
+
equals: (v, f) => v == f,
|
|
627
|
+
notEqual: (v, f) => v != f,
|
|
628
|
+
lessThan: (v, f) => v < f,
|
|
629
|
+
lessThanOrEqual: (v, f) => v <= f,
|
|
630
|
+
greaterThan: (v, f) => v > f,
|
|
631
|
+
greaterThanOrEqual: (v, f) => v >= f,
|
|
632
|
+
inRange: (v, f, t) => v >= f && v <= t,
|
|
633
|
+
blank: (v) => v == null,
|
|
634
|
+
notBlank: (v) => v != null
|
|
635
|
+
};
|
|
636
|
+
function checkCondition(value, cond) {
|
|
637
|
+
const { filterType, type, filter, filterTo, values } = cond;
|
|
638
|
+
if (filterType === 'text') {
|
|
639
|
+
const fn = textOps[type ?? 'contains'];
|
|
640
|
+
return fn?.(value, filter);
|
|
641
|
+
}
|
|
642
|
+
if (filterType === 'number') {
|
|
643
|
+
const fn = numberOps[type ?? 'equals'];
|
|
644
|
+
return type === 'inRange'
|
|
645
|
+
? fn?.(value, filter, filterTo)
|
|
646
|
+
: fn?.(value, filter);
|
|
647
|
+
}
|
|
648
|
+
if (filterType === 'set') {
|
|
649
|
+
// Only boolean types are supported by set filter -- need to represent as string for comparison.
|
|
650
|
+
return (values ?? []).includes(`${value}`);
|
|
651
|
+
}
|
|
652
|
+
return true;
|
|
653
|
+
}
|
|
654
|
+
function compilePredicate(model) {
|
|
655
|
+
return (row) => {
|
|
656
|
+
return Object.entries(model).every(([field, filter]) => {
|
|
657
|
+
const value = row[field];
|
|
658
|
+
if ('conditions' in filter && Array.isArray(filter.conditions)) {
|
|
659
|
+
const logicFn = filter.operator === 'OR' ? 'some' : 'every';
|
|
660
|
+
return filter.conditions[logicFn](cond => checkCondition(value, cond));
|
|
661
|
+
}
|
|
662
|
+
else {
|
|
663
|
+
return checkCondition(value, filter);
|
|
664
|
+
}
|
|
665
|
+
});
|
|
666
|
+
};
|
|
667
|
+
}
|
|
668
|
+
const predicate = compilePredicate(filterModel);
|
|
669
|
+
return data.filter(predicate);
|
|
670
|
+
}
|
|
671
|
+
function pivotData(data, rowGroupCols, pivotCols, valueCols) {
|
|
672
|
+
const groupBy = (row) => Object.fromEntries(rowGroupCols.map(col => [col.field, row[col.field]]));
|
|
673
|
+
const pivotBy = (row) => pivotCols.map(col => row[col.field]).join('_');
|
|
674
|
+
const grouped = new Map();
|
|
675
|
+
for (const row of data) {
|
|
676
|
+
const groupKey = JSON.stringify(groupBy(row));
|
|
677
|
+
if (!grouped.has(groupKey)) {
|
|
678
|
+
grouped.set(groupKey, []);
|
|
679
|
+
}
|
|
680
|
+
grouped.get(groupKey).push(row);
|
|
681
|
+
}
|
|
682
|
+
const rows = [];
|
|
683
|
+
const mappedColumnNames = new Map();
|
|
684
|
+
for (const [groupKey, groupRows] of grouped.entries()) {
|
|
685
|
+
const groupObj = JSON.parse(groupKey);
|
|
686
|
+
const pivotBuckets = new Map();
|
|
687
|
+
for (const row of groupRows) {
|
|
688
|
+
const pivotKey = pivotBy(row);
|
|
689
|
+
if (!pivotBuckets.has(pivotKey)) {
|
|
690
|
+
pivotBuckets.set(pivotKey, []);
|
|
691
|
+
}
|
|
692
|
+
pivotBuckets.get(pivotKey).push(row);
|
|
693
|
+
}
|
|
694
|
+
for (const [pivotKey, rows] of pivotBuckets.entries()) {
|
|
695
|
+
for (const { field, aggFunc } of valueCols) {
|
|
696
|
+
const values = rows.map(r => r[field] ?? 0);
|
|
697
|
+
const key = `${pivotKey} ${field}`;
|
|
698
|
+
let columnNames = mappedColumnNames.get(field);
|
|
699
|
+
if (!!columnNames) {
|
|
700
|
+
columnNames.add(key);
|
|
701
|
+
}
|
|
702
|
+
else {
|
|
703
|
+
columnNames = new Set([key]);
|
|
704
|
+
mappedColumnNames.set(field, columnNames);
|
|
705
|
+
}
|
|
706
|
+
switch (aggFunc) {
|
|
707
|
+
case 'sum':
|
|
708
|
+
groupObj[key] = values.map(Number).reduce((a, b) => a + b, 0);
|
|
709
|
+
if (!groupObj[key]) {
|
|
710
|
+
groupObj[key] = 0;
|
|
711
|
+
}
|
|
712
|
+
break;
|
|
713
|
+
case 'count':
|
|
714
|
+
groupObj[key] = values.length;
|
|
715
|
+
if (!groupObj[key]) {
|
|
716
|
+
groupObj[key] = 0;
|
|
717
|
+
}
|
|
718
|
+
break;
|
|
719
|
+
case 'min':
|
|
720
|
+
groupObj[key] = values.reduce((a, b) => (a < b ? a : b), values[0]);
|
|
721
|
+
if (!groupObj[key]) {
|
|
722
|
+
groupObj[key] = 0;
|
|
723
|
+
}
|
|
724
|
+
break;
|
|
725
|
+
case 'max':
|
|
726
|
+
groupObj[key] = values.reduce((a, b) => (a > b ? a : b), values[0]);
|
|
727
|
+
if (!groupObj[key]) {
|
|
728
|
+
groupObj[key] = 0;
|
|
729
|
+
}
|
|
730
|
+
break;
|
|
731
|
+
case 'avg': {
|
|
732
|
+
const nums = values.map(Number).filter(n => !isNaN(n));
|
|
733
|
+
groupObj[key] = nums.length ? nums.reduce((a, b) => a + b, 0) / nums.length : null;
|
|
734
|
+
if (!groupObj[key]) {
|
|
735
|
+
groupObj[key] = 0;
|
|
736
|
+
}
|
|
737
|
+
break;
|
|
738
|
+
}
|
|
739
|
+
case 'first':
|
|
740
|
+
groupObj[key] = values[0];
|
|
741
|
+
if (!groupObj[key]) {
|
|
742
|
+
groupObj[key] = '';
|
|
743
|
+
}
|
|
744
|
+
break;
|
|
745
|
+
case 'last':
|
|
746
|
+
groupObj[key] = values[values.length - 1];
|
|
747
|
+
if (!groupObj[key]) {
|
|
748
|
+
groupObj[key] = '';
|
|
749
|
+
}
|
|
750
|
+
break;
|
|
751
|
+
default:
|
|
752
|
+
groupObj[key] = '';
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
rows.push(groupObj);
|
|
757
|
+
}
|
|
758
|
+
return { rows, mappedColumnNames };
|
|
759
|
+
}
|
|
760
|
+
function groupAndAggregate(data, rowGroupCols, groupKeys, valueCols) {
|
|
761
|
+
const level = groupKeys.length;
|
|
762
|
+
const groupField = rowGroupCols[level]?.field;
|
|
763
|
+
if (!groupField) {
|
|
764
|
+
return data;
|
|
765
|
+
}
|
|
766
|
+
const grouped = new Map();
|
|
767
|
+
for (const row of data) {
|
|
768
|
+
const key = row[groupField];
|
|
769
|
+
if (!grouped.has(key))
|
|
770
|
+
grouped.set(key, []);
|
|
771
|
+
grouped.get(key).push(row);
|
|
772
|
+
}
|
|
773
|
+
const output = [];
|
|
774
|
+
for (const [key, rows] of grouped.entries()) {
|
|
775
|
+
if (level < rowGroupCols.length - 1) {
|
|
776
|
+
output.push({
|
|
777
|
+
group: true,
|
|
778
|
+
key,
|
|
779
|
+
children: groupAndAggregate(rows, rowGroupCols, [...groupKeys, key], valueCols)
|
|
780
|
+
});
|
|
781
|
+
}
|
|
782
|
+
else {
|
|
783
|
+
const aggData = {};
|
|
784
|
+
for (const { field, aggFunc } of valueCols) {
|
|
785
|
+
const rawValues = rows.map(r => r[field]).filter(v => v != null);
|
|
786
|
+
switch (aggFunc) {
|
|
787
|
+
case 'sum': {
|
|
788
|
+
const nums = rawValues.map(Number).filter(v => !isNaN(v));
|
|
789
|
+
aggData[field] = nums.reduce((a, b) => a + b, 0);
|
|
790
|
+
break;
|
|
791
|
+
}
|
|
792
|
+
case 'avg': {
|
|
793
|
+
const nums = rawValues.map(Number).filter(v => !isNaN(v));
|
|
794
|
+
const total = nums.reduce((a, b) => a + b, 0);
|
|
795
|
+
aggData[field] = nums.length > 0 ? total / nums.length : null;
|
|
796
|
+
break;
|
|
797
|
+
}
|
|
798
|
+
case 'min':
|
|
799
|
+
aggData[field] = rawValues.reduce((a, b) => (a < b ? a : b), rawValues[0]);
|
|
800
|
+
break;
|
|
801
|
+
case 'max':
|
|
802
|
+
aggData[field] = rawValues.reduce((a, b) => (a > b ? a : b), rawValues[0]);
|
|
803
|
+
break;
|
|
804
|
+
case 'count':
|
|
805
|
+
aggData[field] = rawValues.length;
|
|
806
|
+
break;
|
|
807
|
+
case 'first':
|
|
808
|
+
aggData[field] = rawValues[0];
|
|
809
|
+
break;
|
|
810
|
+
case 'last':
|
|
811
|
+
aggData[field] = rawValues[rawValues.length - 1];
|
|
812
|
+
break;
|
|
813
|
+
default:
|
|
814
|
+
aggData[field] = null;
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
// NOTE: we only want the highest level data -- we don't care about child rows.
|
|
818
|
+
output.push({
|
|
819
|
+
[groupField]: key,
|
|
820
|
+
...aggData,
|
|
821
|
+
// children: rows
|
|
822
|
+
});
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
return output;
|
|
826
|
+
}
|
|
827
|
+
function sortRows(data, sortModel) {
|
|
828
|
+
return [...data].sort((a, b) => {
|
|
829
|
+
for (const { colId, sort } of sortModel) {
|
|
830
|
+
const valA = a[colId];
|
|
831
|
+
const valB = b[colId];
|
|
832
|
+
if (valA === valB)
|
|
833
|
+
continue;
|
|
834
|
+
const cmp = valA > valB ? 1 : -1;
|
|
835
|
+
return sort === 'asc' ? cmp : -cmp;
|
|
836
|
+
}
|
|
837
|
+
return 0;
|
|
838
|
+
});
|
|
839
|
+
}
|
|
840
|
+
function processHtmlStyles(value, columnName, columnFormat, rowStyles, columnStyles) {
|
|
841
|
+
let columnStyle = {
|
|
842
|
+
columnName,
|
|
843
|
+
styles: []
|
|
844
|
+
};
|
|
845
|
+
// Reminder: Value Formatting is performed **before** Conditional Formatting.
|
|
846
|
+
// https://chatwms.io/user-manual/concepts/column-formatting#conditional-formatting
|
|
847
|
+
for (const condFmt of columnFormat.conditionalFormats.slice().reverse()) {
|
|
848
|
+
// `conditionalFormats` has already been sorted by `sequence`.
|
|
849
|
+
// NOTE: need to reverse the array for sequence to be correctly applied.
|
|
850
|
+
// NOTE: using `slice` to make shallow copy of array since `reverse` does so in place.
|
|
851
|
+
if (evaluateConditionalFormat(condFmt, columnFormat.type, formatNumberEnabled(columnFormat.valueFormat) ? stripNumericValueFormat(value) : value)) {
|
|
852
|
+
if (condFmt.row) {
|
|
853
|
+
rowStyles.push(condFmt.style);
|
|
854
|
+
}
|
|
855
|
+
else {
|
|
856
|
+
columnStyle.styles.push(condFmt.style);
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
columnStyles.push(columnStyle);
|
|
861
|
+
return [rowStyles, columnStyles];
|
|
862
|
+
}
|
|
863
|
+
function processHtmlTransposeStyles(value, columnName, columnFormat, columnStyles) {
|
|
864
|
+
let columnStyle = {
|
|
865
|
+
columnName,
|
|
866
|
+
styles: []
|
|
867
|
+
};
|
|
868
|
+
// Everything is the same as ^^^ except for row styles -- we will consolidate those into columns styles.
|
|
869
|
+
for (const condFmt of columnFormat.conditionalFormats.slice().reverse()) {
|
|
870
|
+
if (evaluateConditionalFormat(condFmt, columnFormat.type, formatNumberEnabled(columnFormat.valueFormat) ? stripNumericValueFormat(value) : value)) {
|
|
871
|
+
columnStyle.styles.push(condFmt.style);
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
columnStyles.push(columnStyle);
|
|
875
|
+
return columnStyles;
|
|
876
|
+
}
|
|
877
|
+
export function mapTeamsStyles(columnNames, htmlRowStyles, htmlColumnStyles, teamsRowStyles, teamsColumnStyles) {
|
|
878
|
+
// IMPORTANT: both column/row style sorting have **already** been reversed (line 1170).
|
|
879
|
+
// We can simply iterate over all the matched styles and accumulate them.
|
|
880
|
+
const getColumnStyle = (style) => {
|
|
881
|
+
switch (style) {
|
|
882
|
+
case ConditionalFormatStyle.GREEN_BACKGROUND:
|
|
883
|
+
return {
|
|
884
|
+
color: 'Good',
|
|
885
|
+
style: 'good'
|
|
886
|
+
};
|
|
887
|
+
case ConditionalFormatStyle.YELLOW_BACKGROUND:
|
|
888
|
+
return {
|
|
889
|
+
color: 'Warning',
|
|
890
|
+
style: 'warning'
|
|
891
|
+
};
|
|
892
|
+
case ConditionalFormatStyle.RED_BACKGROUND:
|
|
893
|
+
return {
|
|
894
|
+
color: 'Attention',
|
|
895
|
+
style: 'attention'
|
|
896
|
+
};
|
|
897
|
+
case ConditionalFormatStyle.BLUE_BACKGROUND:
|
|
898
|
+
case ConditionalFormatStyle.PURPLE_BACKGROUND:
|
|
899
|
+
return {
|
|
900
|
+
color: 'Accent',
|
|
901
|
+
style: 'accent'
|
|
902
|
+
};
|
|
903
|
+
case ConditionalFormatStyle.BOLD:
|
|
904
|
+
return {
|
|
905
|
+
weight: 'Bolder'
|
|
906
|
+
};
|
|
907
|
+
case ConditionalFormatStyle.GREEN_BOLD:
|
|
908
|
+
return {
|
|
909
|
+
color: 'Good',
|
|
910
|
+
weight: 'Bolder'
|
|
911
|
+
};
|
|
912
|
+
case ConditionalFormatStyle.YELLOW_BOLD:
|
|
913
|
+
return {
|
|
914
|
+
color: 'Warning',
|
|
915
|
+
weight: 'Bolder'
|
|
916
|
+
};
|
|
917
|
+
case ConditionalFormatStyle.RED_BOLD:
|
|
918
|
+
return {
|
|
919
|
+
color: 'Attention',
|
|
920
|
+
weight: 'Bolder'
|
|
921
|
+
};
|
|
922
|
+
case ConditionalFormatStyle.BLUE_BOLD:
|
|
923
|
+
case ConditionalFormatStyle.PURPLE_BOLD:
|
|
924
|
+
return {
|
|
925
|
+
color: 'Accent',
|
|
926
|
+
weight: 'Bolder'
|
|
927
|
+
};
|
|
928
|
+
case ConditionalFormatStyle.NOT_IMPORTANT:
|
|
929
|
+
return {
|
|
930
|
+
isSubtle: true,
|
|
931
|
+
italic: true,
|
|
932
|
+
weight: 'Lighter'
|
|
933
|
+
};
|
|
934
|
+
default:
|
|
935
|
+
return {};
|
|
936
|
+
}
|
|
937
|
+
};
|
|
938
|
+
for (const htmlColumnStyle of htmlColumnStyles) {
|
|
939
|
+
teamsColumnStyles.push({
|
|
940
|
+
columnName: htmlColumnStyle.columnName,
|
|
941
|
+
styles: htmlColumnStyle.styles.map(style => getColumnStyle(style))
|
|
942
|
+
});
|
|
943
|
+
}
|
|
944
|
+
const applyRowStyleToColumns = (style) => {
|
|
945
|
+
for (const columnName of columnNames) {
|
|
946
|
+
const columnStyle = getColumnStyle(style);
|
|
947
|
+
const teamsColumnStyle = teamsColumnStyles.find(c => c.columnName === columnName);
|
|
948
|
+
if (!!teamsColumnStyle) {
|
|
949
|
+
teamsColumnStyle.styles.push(columnStyle);
|
|
950
|
+
}
|
|
951
|
+
else {
|
|
952
|
+
teamsColumnStyles.push({
|
|
953
|
+
columnName,
|
|
954
|
+
styles: [columnStyle]
|
|
955
|
+
});
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
};
|
|
959
|
+
for (const htmlRowStyle of htmlRowStyles) {
|
|
960
|
+
switch (htmlRowStyle) {
|
|
961
|
+
case ConditionalFormatStyle.GREEN_BACKGROUND:
|
|
962
|
+
teamsRowStyles.push('good');
|
|
963
|
+
applyRowStyleToColumns(htmlRowStyle);
|
|
964
|
+
break;
|
|
965
|
+
case ConditionalFormatStyle.YELLOW_BACKGROUND:
|
|
966
|
+
teamsRowStyles.push('warning');
|
|
967
|
+
applyRowStyleToColumns(htmlRowStyle);
|
|
968
|
+
break;
|
|
969
|
+
case ConditionalFormatStyle.RED_BACKGROUND:
|
|
970
|
+
teamsRowStyles.push('attention');
|
|
971
|
+
applyRowStyleToColumns(htmlRowStyle);
|
|
972
|
+
break;
|
|
973
|
+
case ConditionalFormatStyle.BLUE_BACKGROUND:
|
|
974
|
+
case ConditionalFormatStyle.PURPLE_BACKGROUND:
|
|
975
|
+
teamsRowStyles.push('accent');
|
|
976
|
+
applyRowStyleToColumns(htmlRowStyle);
|
|
977
|
+
break;
|
|
978
|
+
case ConditionalFormatStyle.BOLD:
|
|
979
|
+
applyRowStyleToColumns(htmlRowStyle);
|
|
980
|
+
break;
|
|
981
|
+
case ConditionalFormatStyle.GREEN_BOLD:
|
|
982
|
+
applyRowStyleToColumns(htmlRowStyle);
|
|
983
|
+
break;
|
|
984
|
+
case ConditionalFormatStyle.YELLOW_BOLD:
|
|
985
|
+
applyRowStyleToColumns(htmlRowStyle);
|
|
986
|
+
break;
|
|
987
|
+
case ConditionalFormatStyle.RED_BOLD:
|
|
988
|
+
applyRowStyleToColumns(htmlRowStyle);
|
|
989
|
+
break;
|
|
990
|
+
case ConditionalFormatStyle.BLUE_BOLD:
|
|
991
|
+
case ConditionalFormatStyle.PURPLE_BOLD:
|
|
992
|
+
applyRowStyleToColumns(htmlRowStyle);
|
|
993
|
+
break;
|
|
994
|
+
case ConditionalFormatStyle.NOT_IMPORTANT:
|
|
995
|
+
applyRowStyleToColumns(htmlRowStyle);
|
|
996
|
+
break;
|
|
997
|
+
default:
|
|
998
|
+
break;
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
return [teamsRowStyles, teamsColumnStyles];
|
|
1002
|
+
}
|
|
1003
|
+
export function mapTeamsTransposeStyles(htmlColumnStyle) {
|
|
1004
|
+
// Generally the same logic as ^^^, just handling a few things differently for transpose.
|
|
1005
|
+
const getColumnStyle = (style) => {
|
|
1006
|
+
switch (style) {
|
|
1007
|
+
case ConditionalFormatStyle.GREEN_BACKGROUND:
|
|
1008
|
+
return {
|
|
1009
|
+
color: 'Good',
|
|
1010
|
+
style: 'good'
|
|
1011
|
+
};
|
|
1012
|
+
case ConditionalFormatStyle.YELLOW_BACKGROUND:
|
|
1013
|
+
return {
|
|
1014
|
+
color: 'Warning',
|
|
1015
|
+
style: 'warning'
|
|
1016
|
+
};
|
|
1017
|
+
case ConditionalFormatStyle.RED_BACKGROUND:
|
|
1018
|
+
return {
|
|
1019
|
+
color: 'Attention',
|
|
1020
|
+
style: 'attention'
|
|
1021
|
+
};
|
|
1022
|
+
case ConditionalFormatStyle.BLUE_BACKGROUND:
|
|
1023
|
+
case ConditionalFormatStyle.PURPLE_BACKGROUND:
|
|
1024
|
+
return {
|
|
1025
|
+
color: 'Accent',
|
|
1026
|
+
style: 'accent'
|
|
1027
|
+
};
|
|
1028
|
+
case ConditionalFormatStyle.BOLD:
|
|
1029
|
+
return {
|
|
1030
|
+
weight: 'Bolder'
|
|
1031
|
+
};
|
|
1032
|
+
case ConditionalFormatStyle.GREEN_BOLD:
|
|
1033
|
+
return {
|
|
1034
|
+
color: 'Good',
|
|
1035
|
+
weight: 'Bolder'
|
|
1036
|
+
};
|
|
1037
|
+
case ConditionalFormatStyle.YELLOW_BOLD:
|
|
1038
|
+
return {
|
|
1039
|
+
color: 'Warning',
|
|
1040
|
+
weight: 'Bolder'
|
|
1041
|
+
};
|
|
1042
|
+
case ConditionalFormatStyle.RED_BOLD:
|
|
1043
|
+
return {
|
|
1044
|
+
color: 'Attention',
|
|
1045
|
+
weight: 'Bolder'
|
|
1046
|
+
};
|
|
1047
|
+
case ConditionalFormatStyle.BLUE_BOLD:
|
|
1048
|
+
case ConditionalFormatStyle.PURPLE_BOLD:
|
|
1049
|
+
return {
|
|
1050
|
+
color: 'Accent',
|
|
1051
|
+
weight: 'Bolder'
|
|
1052
|
+
};
|
|
1053
|
+
case ConditionalFormatStyle.NOT_IMPORTANT:
|
|
1054
|
+
return {
|
|
1055
|
+
isSubtle: true,
|
|
1056
|
+
italic: true,
|
|
1057
|
+
weight: 'Lighter'
|
|
1058
|
+
};
|
|
1059
|
+
default:
|
|
1060
|
+
return {};
|
|
1061
|
+
}
|
|
1062
|
+
};
|
|
1063
|
+
return {
|
|
1064
|
+
columnName: htmlColumnStyle.columnName,
|
|
1065
|
+
styles: htmlColumnStyle.styles.map(style => getColumnStyle(style))
|
|
1066
|
+
};
|
|
1067
|
+
}
|
|
1068
|
+
function buildHtmlTableHeader(columnNames) {
|
|
1069
|
+
let buf = '<table>\n\t<thead>\n\t\t<tr>';
|
|
1070
|
+
for (const columnName of columnNames) {
|
|
1071
|
+
buf += `\n\t\t\t<th>${truncateValue(columnName, 24)}</th>`;
|
|
1072
|
+
}
|
|
1073
|
+
buf += '\n\t\t</tr>\n\t</thead>';
|
|
1074
|
+
buf += '\n\t<tbody>';
|
|
1075
|
+
return buf;
|
|
1076
|
+
}
|
|
1077
|
+
function buildHtmlTableTransposeHeader() {
|
|
1078
|
+
return '<table>\n\t<thead></thead>\n\t<tbody>';
|
|
1079
|
+
}
|
|
1080
|
+
function buildHtmlTableFooter() {
|
|
1081
|
+
return '\n\t</tbody>\n</table>';
|
|
1082
|
+
}
|
|
1083
|
+
function buildHtmlRow(rowValues, rowStyles, columnStyles, columnCount) {
|
|
1084
|
+
let buf = '';
|
|
1085
|
+
// Apply row styles.
|
|
1086
|
+
if (rowStyles.length > 0) {
|
|
1087
|
+
buf += `\n\t\t<tr class="${rowStyles.join(' ')}">`;
|
|
1088
|
+
}
|
|
1089
|
+
else {
|
|
1090
|
+
buf += '\n\t\t<tr>';
|
|
1091
|
+
}
|
|
1092
|
+
// Write cell values with styles.
|
|
1093
|
+
let colIdx = 0;
|
|
1094
|
+
for (const rowValue of rowValues) {
|
|
1095
|
+
if (colIdx >= columnCount) {
|
|
1096
|
+
break;
|
|
1097
|
+
}
|
|
1098
|
+
const truncValue = truncateValue(rowValue, 24);
|
|
1099
|
+
if (columnStyles[colIdx].styles.length > 0) {
|
|
1100
|
+
buf += `\n\t\t\t<td class="${columnStyles[colIdx].styles.join(' ')}">${truncValue}</td>`;
|
|
1101
|
+
}
|
|
1102
|
+
else {
|
|
1103
|
+
buf += `\n\t\t\t<td>${truncValue}</td>`;
|
|
1104
|
+
}
|
|
1105
|
+
colIdx++;
|
|
1106
|
+
}
|
|
1107
|
+
buf += '\n\t\t</tr>';
|
|
1108
|
+
return buf;
|
|
1109
|
+
}
|
|
1110
|
+
function buildHtmlTransposeRow(row, columnStyle) {
|
|
1111
|
+
let buf = '';
|
|
1112
|
+
buf += `\n\t\t\t<td style="font-size: 13px; font-weight: 600; padding: 15px 14px; background: white; justify-items: end; align-items: center; width: 33%;"><span style="display: grid;">${truncateValue(row.key, 24)}</span></td>`;
|
|
1113
|
+
const truncValue = truncateValue(row.value, 42);
|
|
1114
|
+
if (columnStyle.styles.length > 0) {
|
|
1115
|
+
buf += `\n\t\t\t<td style="font-size: 16px; padding: 15px 14px; background: white;" class="${columnStyle.styles.join(' ')}">${truncValue}</td>`;
|
|
1116
|
+
}
|
|
1117
|
+
else {
|
|
1118
|
+
buf += `\n\t\t\t<td style="font-size: 16px; padding: 15px 14px; background: white;">${truncValue}</td>`;
|
|
1119
|
+
}
|
|
1120
|
+
buf += '\n\t\t</tr>';
|
|
1121
|
+
return buf;
|
|
1122
|
+
}
|
|
1123
|
+
function createTeamsTableColumnText(columnName) {
|
|
1124
|
+
return {
|
|
1125
|
+
type: "TextBlock",
|
|
1126
|
+
text: truncateValue(columnName, 16),
|
|
1127
|
+
size: "Small",
|
|
1128
|
+
weight: "Bolder"
|
|
1129
|
+
};
|
|
1130
|
+
}
|
|
1131
|
+
export function createTeamsTableColumns(columnNames) {
|
|
1132
|
+
return {
|
|
1133
|
+
type: "TableRow",
|
|
1134
|
+
cells: columnNames.map((columnName) => ({
|
|
1135
|
+
type: "TableCell",
|
|
1136
|
+
items: [createTeamsTableColumnText(columnName)]
|
|
1137
|
+
}))
|
|
1138
|
+
};
|
|
1139
|
+
}
|
|
1140
|
+
function createTeamsTableCellText(value, columnStyle) {
|
|
1141
|
+
let color = undefined;
|
|
1142
|
+
let weight = undefined;
|
|
1143
|
+
let isSubtle = undefined;
|
|
1144
|
+
let italic = undefined;
|
|
1145
|
+
for (const styles of columnStyle?.styles ?? []) {
|
|
1146
|
+
if (!!styles.color) {
|
|
1147
|
+
color = styles.color;
|
|
1148
|
+
}
|
|
1149
|
+
if (!!styles.weight) {
|
|
1150
|
+
weight = styles.weight;
|
|
1151
|
+
}
|
|
1152
|
+
if (!!styles.isSubtle) {
|
|
1153
|
+
isSubtle = styles.isSubtle;
|
|
1154
|
+
}
|
|
1155
|
+
if (!!styles.italic) {
|
|
1156
|
+
italic = styles.italic;
|
|
1157
|
+
}
|
|
1158
|
+
}
|
|
1159
|
+
const truncValue = truncateValue(value, 16);
|
|
1160
|
+
return {
|
|
1161
|
+
type: "TextBlock",
|
|
1162
|
+
text: italic ? `*${truncValue}*` : truncValue,
|
|
1163
|
+
size: "Small",
|
|
1164
|
+
color,
|
|
1165
|
+
weight,
|
|
1166
|
+
isSubtle
|
|
1167
|
+
};
|
|
1168
|
+
}
|
|
1169
|
+
function createTeamsTransposeTableCellText(value, key, columnStyle) {
|
|
1170
|
+
// Same as ^^^, just a couple minor tweaks.
|
|
1171
|
+
let color = undefined;
|
|
1172
|
+
let weight = undefined;
|
|
1173
|
+
let isSubtle = undefined;
|
|
1174
|
+
let italic = undefined;
|
|
1175
|
+
for (const styles of columnStyle?.styles ?? []) {
|
|
1176
|
+
if (!!styles.color) {
|
|
1177
|
+
color = styles.color;
|
|
1178
|
+
}
|
|
1179
|
+
if (!!styles.weight) {
|
|
1180
|
+
weight = styles.weight;
|
|
1181
|
+
}
|
|
1182
|
+
if (!!styles.isSubtle) {
|
|
1183
|
+
isSubtle = styles.isSubtle;
|
|
1184
|
+
}
|
|
1185
|
+
if (!!styles.italic) {
|
|
1186
|
+
italic = styles.italic;
|
|
1187
|
+
}
|
|
1188
|
+
}
|
|
1189
|
+
const truncValue = truncateValue(value, 16);
|
|
1190
|
+
return {
|
|
1191
|
+
type: "TextBlock",
|
|
1192
|
+
text: italic ? `*${truncValue}*` : truncValue,
|
|
1193
|
+
size: key ? "Small" : "Medium",
|
|
1194
|
+
horizontalAlignment: key ? "Right" : "Left",
|
|
1195
|
+
color,
|
|
1196
|
+
weight,
|
|
1197
|
+
isSubtle
|
|
1198
|
+
};
|
|
1199
|
+
}
|
|
1200
|
+
export function createTeamsTableRow(columnNames, values, rowStyles, columnStyles) {
|
|
1201
|
+
let rowStyle = undefined;
|
|
1202
|
+
for (const style of rowStyles ?? []) {
|
|
1203
|
+
rowStyle = style;
|
|
1204
|
+
}
|
|
1205
|
+
return {
|
|
1206
|
+
type: "TableRow",
|
|
1207
|
+
style: rowStyle,
|
|
1208
|
+
cells: values.map((value, idx) => {
|
|
1209
|
+
const columnName = columnNames[idx];
|
|
1210
|
+
const columnStyle = columnStyles?.find(cs => cs.columnName === columnName);
|
|
1211
|
+
let style = undefined;
|
|
1212
|
+
for (const styles of columnStyle?.styles ?? []) {
|
|
1213
|
+
if (!!styles.style) {
|
|
1214
|
+
style = styles.style;
|
|
1215
|
+
}
|
|
1216
|
+
}
|
|
1217
|
+
return {
|
|
1218
|
+
type: "TableCell",
|
|
1219
|
+
style,
|
|
1220
|
+
items: [createTeamsTableCellText(value, columnStyle)]
|
|
1221
|
+
};
|
|
1222
|
+
})
|
|
1223
|
+
};
|
|
1224
|
+
}
|
|
1225
|
+
export function createTeamsTransposeTableRow(row, columnStyle) {
|
|
1226
|
+
const keyCell = {
|
|
1227
|
+
type: "TableCell",
|
|
1228
|
+
items: [createTeamsTransposeTableCellText(row.key, true, {
|
|
1229
|
+
columnName: 'key',
|
|
1230
|
+
styles: [{ weight: 'Bolder' }]
|
|
1231
|
+
})]
|
|
1232
|
+
};
|
|
1233
|
+
let style = undefined;
|
|
1234
|
+
for (const styles of columnStyle?.styles ?? []) {
|
|
1235
|
+
if (!!styles.style) {
|
|
1236
|
+
style = styles.style;
|
|
1237
|
+
}
|
|
1238
|
+
}
|
|
1239
|
+
const valueCell = {
|
|
1240
|
+
type: "TableCell",
|
|
1241
|
+
style,
|
|
1242
|
+
items: [createTeamsTransposeTableCellText(row.value, false, columnStyle)]
|
|
1243
|
+
};
|
|
1244
|
+
return {
|
|
1245
|
+
type: "TableRow",
|
|
1246
|
+
cells: [keyCell, valueCell]
|
|
1247
|
+
};
|
|
1248
|
+
}
|
|
1249
|
+
function getColumnSequenceRank(column, pivot) {
|
|
1250
|
+
let seq = 2;
|
|
1251
|
+
// Check pin state first.
|
|
1252
|
+
// Important: we do not care about pinning if the grid is pivoted.
|
|
1253
|
+
if (!pivot) {
|
|
1254
|
+
if (column.pinned === 'left') {
|
|
1255
|
+
seq = 0;
|
|
1256
|
+
}
|
|
1257
|
+
else if (column.pinned === 'right') {
|
|
1258
|
+
seq = 3;
|
|
1259
|
+
}
|
|
1260
|
+
}
|
|
1261
|
+
// Row group second.
|
|
1262
|
+
if (!!column.rowGroup && column.rowGroupIndex === 0) {
|
|
1263
|
+
seq = 1;
|
|
1264
|
+
}
|
|
1265
|
+
// Nothing else to check.
|
|
1266
|
+
return seq;
|
|
1267
|
+
}
|
|
1268
|
+
/**
|
|
1269
|
+
* **Important:** `html` and `teamsRows` output will be limited to **1,000 rows/24 rows** and **8 columns**, respectively.
|
|
1270
|
+
*
|
|
1271
|
+
* ~All outputs respect and will be filtered by optional `condition` argument.~
|
|
1272
|
+
*/
|
|
1273
|
+
export function transformDatagrid(rows, datagridState, locale, condition) {
|
|
1274
|
+
const columnState = datagridState.columnState ?? [];
|
|
1275
|
+
const filterModel = datagridState.filterModel ?? {};
|
|
1276
|
+
const isPivotMode = !!datagridState.isPivotMode;
|
|
1277
|
+
const columnFormats = datagridState.columnFormats ?? [];
|
|
1278
|
+
const { rowGroupCols, pivotCols, valueCols, sortModel } = parseColumnState(columnState);
|
|
1279
|
+
rows = filterRows(rows, filterModel);
|
|
1280
|
+
let adjRows = [];
|
|
1281
|
+
const chartRows = [];
|
|
1282
|
+
let htmlBuf = '';
|
|
1283
|
+
let htmlTransposeBuf = '';
|
|
1284
|
+
const htmlColumnNames = [];
|
|
1285
|
+
const teamsRows = [];
|
|
1286
|
+
const teamsTranspose = [];
|
|
1287
|
+
// IMPORTANT: we evaluate the datagrid condition AFTER we are done with all transformations.
|
|
1288
|
+
// NOTE: we do not need any pivot columns for pivot mode to be valid.
|
|
1289
|
+
// https://chatwms.io/user-manual/chatwms/faq#q-do-i-have-to-have-a-group-column-to-just-display-a-count-in-the-datagrid
|
|
1290
|
+
if (isPivotMode && valueCols.length > 0) {
|
|
1291
|
+
const pivotOutput = pivotData(rows, rowGroupCols, pivotCols, valueCols);
|
|
1292
|
+
rows = pivotOutput.rows;
|
|
1293
|
+
rows = sortModel.length > 0 ? sortRows(rows, sortModel) : rows;
|
|
1294
|
+
let mappedDisplayColumnNames = new Map();
|
|
1295
|
+
// For pivots, we cannot trust the `hide` field.
|
|
1296
|
+
// Visibility will depend on `rowGroup`, `pivot`, and `aggFunc`.
|
|
1297
|
+
const visibleColumnState = columnState.filter(column => (!!column.rowGroup && column.rowGroupIndex === 0) || // We only display the **first** row group.
|
|
1298
|
+
(!!column.pivot) ||
|
|
1299
|
+
(!!column.aggFunc)).sort((a, b) => getColumnSequenceRank(a, true) - getColumnSequenceRank(b, true));
|
|
1300
|
+
let rowIdx = 0;
|
|
1301
|
+
rows.forEach((row) => {
|
|
1302
|
+
let adjRow = {};
|
|
1303
|
+
let chartRow = {};
|
|
1304
|
+
let htmlRowValues = [];
|
|
1305
|
+
let htmlRowStyles = [];
|
|
1306
|
+
let htmlColumnStyles = [];
|
|
1307
|
+
let htmlTransposeColumnStyles = [];
|
|
1308
|
+
let teamsRowStyles = [];
|
|
1309
|
+
let teamsColumnStyles = [];
|
|
1310
|
+
// Important: anchoring to ColumnState will ensure we add columns in the correct sequence.
|
|
1311
|
+
visibleColumnState.forEach(column => {
|
|
1312
|
+
const columnFormat = columnFormats.find(columnFormat => columnFormat.name === column.colId);
|
|
1313
|
+
if (!!columnFormat) {
|
|
1314
|
+
if (columnFormat.name in row) {
|
|
1315
|
+
const value = row[columnFormat.name];
|
|
1316
|
+
const formattedValue = evaluateValueFormat(columnFormat, value, locale);
|
|
1317
|
+
adjRow[columnFormat.displayName] = formattedValue;
|
|
1318
|
+
chartRow[columnFormat.displayName] = formatNumberEnabled(columnFormat.valueFormat) ? value : formattedValue;
|
|
1319
|
+
htmlRowValues.push(formattedValue);
|
|
1320
|
+
[htmlRowStyles, htmlColumnStyles] = processHtmlStyles(formattedValue, columnFormat.displayName, columnFormat, htmlRowStyles, htmlColumnStyles);
|
|
1321
|
+
htmlTransposeColumnStyles = processHtmlTransposeStyles(formattedValue, columnFormat.displayName, columnFormat, htmlTransposeColumnStyles);
|
|
1322
|
+
if (rowIdx === 0) {
|
|
1323
|
+
htmlColumnNames.push(columnFormat.displayName);
|
|
1324
|
+
}
|
|
1325
|
+
}
|
|
1326
|
+
else {
|
|
1327
|
+
const mappedColumnNamesSet = pivotOutput.mappedColumnNames.get(columnFormat.name);
|
|
1328
|
+
if (!!mappedColumnNamesSet) {
|
|
1329
|
+
const mappedColumnNamesArr = Array.from(mappedColumnNamesSet);
|
|
1330
|
+
for (const mappedColumnName of mappedColumnNamesArr) {
|
|
1331
|
+
if (mappedColumnName in row) {
|
|
1332
|
+
const adjDisplayName = mappedColumnName.replace(columnFormat.name, columnFormat.displayName);
|
|
1333
|
+
const value = row[mappedColumnName];
|
|
1334
|
+
const formattedValue = evaluateValueFormat(columnFormat, value, locale);
|
|
1335
|
+
adjRow[adjDisplayName] = formattedValue;
|
|
1336
|
+
chartRow[adjDisplayName] = formatNumberEnabled(columnFormat.valueFormat) ? value : formattedValue;
|
|
1337
|
+
htmlRowValues.push(formattedValue);
|
|
1338
|
+
[htmlRowStyles, htmlColumnStyles] = processHtmlStyles(formattedValue, adjDisplayName, columnFormat, htmlRowStyles, htmlColumnStyles);
|
|
1339
|
+
htmlTransposeColumnStyles = processHtmlTransposeStyles(formattedValue, adjDisplayName, columnFormat, htmlTransposeColumnStyles);
|
|
1340
|
+
if (rowIdx === 0) {
|
|
1341
|
+
htmlColumnNames.push(adjDisplayName);
|
|
1342
|
+
}
|
|
1343
|
+
let displayColumnNames = mappedDisplayColumnNames.get(columnFormat.displayName);
|
|
1344
|
+
if (!!displayColumnNames) {
|
|
1345
|
+
displayColumnNames.add(adjDisplayName);
|
|
1346
|
+
}
|
|
1347
|
+
else {
|
|
1348
|
+
displayColumnNames = new Set([adjDisplayName]);
|
|
1349
|
+
mappedDisplayColumnNames.set(columnFormat.displayName, displayColumnNames);
|
|
1350
|
+
}
|
|
1351
|
+
}
|
|
1352
|
+
}
|
|
1353
|
+
}
|
|
1354
|
+
}
|
|
1355
|
+
}
|
|
1356
|
+
});
|
|
1357
|
+
adjRows.push(adjRow);
|
|
1358
|
+
chartRows.push(chartRow);
|
|
1359
|
+
if (rowIdx < DATAGRID_HTML_ROWS) {
|
|
1360
|
+
htmlBuf += buildHtmlRow(htmlRowValues, htmlRowStyles, htmlColumnStyles, DATAGRID_HTML_COLS);
|
|
1361
|
+
if (rowIdx < DATAGRID_TEAMS_ROWS) {
|
|
1362
|
+
[teamsRowStyles, teamsColumnStyles] = mapTeamsStyles(htmlColumnNames, htmlRowStyles, htmlColumnStyles, teamsRowStyles, teamsColumnStyles);
|
|
1363
|
+
teamsRows.push(createTeamsTableRow(htmlColumnNames, htmlRowValues, teamsRowStyles, teamsColumnStyles));
|
|
1364
|
+
}
|
|
1365
|
+
}
|
|
1366
|
+
if (rowIdx === 0) {
|
|
1367
|
+
const transposedRows = htmlColumnNames.map((key, index) => ({
|
|
1368
|
+
key,
|
|
1369
|
+
value: htmlRowValues[index],
|
|
1370
|
+
}));
|
|
1371
|
+
for (let i = 0; i < transposedRows.length; i++) {
|
|
1372
|
+
htmlTransposeBuf += buildHtmlTransposeRow(transposedRows[i], htmlTransposeColumnStyles[i]);
|
|
1373
|
+
teamsTranspose.push(createTeamsTransposeTableRow(transposedRows[i], mapTeamsTransposeStyles(htmlTransposeColumnStyles[i])));
|
|
1374
|
+
}
|
|
1375
|
+
}
|
|
1376
|
+
rowIdx++;
|
|
1377
|
+
});
|
|
1378
|
+
if (!!condition) {
|
|
1379
|
+
rows = evaluatePivotDatagridCondition(rows, condition, mappedDisplayColumnNames);
|
|
1380
|
+
}
|
|
1381
|
+
}
|
|
1382
|
+
else {
|
|
1383
|
+
if (rowGroupCols.length > 0) {
|
|
1384
|
+
rows = groupAndAggregate(rows, rowGroupCols, [], valueCols);
|
|
1385
|
+
}
|
|
1386
|
+
rows = sortModel.length > 0 ? sortRows(rows, sortModel) : rows;
|
|
1387
|
+
// We cannot always trust the `hide` field -- we need to look at the row group configuration.
|
|
1388
|
+
const visibleColumnState = columnState.filter(column => !column.hide || // Important: `hide` will be true if we are dealing with a row group.
|
|
1389
|
+
(!!column.rowGroup && column.rowGroupIndex === 0) // We only display the **first** row group.
|
|
1390
|
+
).sort((a, b) => getColumnSequenceRank(a) - getColumnSequenceRank(b));
|
|
1391
|
+
let rowIdx = 0;
|
|
1392
|
+
rows.forEach((row) => {
|
|
1393
|
+
let adjRow = {};
|
|
1394
|
+
let chartRow = {};
|
|
1395
|
+
let htmlRowValues = [];
|
|
1396
|
+
let htmlRowStyles = [];
|
|
1397
|
+
let htmlColumnStyles = [];
|
|
1398
|
+
let htmlTransposeColumnStyles = [];
|
|
1399
|
+
let teamsRowStyles = [];
|
|
1400
|
+
let teamsColumnStyles = [];
|
|
1401
|
+
// Important: anchoring to ColumnState will ensure we add columns in the correct sequence.
|
|
1402
|
+
visibleColumnState.forEach(column => {
|
|
1403
|
+
const columnFormat = columnFormats.find(columnFormat => columnFormat.name === column.colId);
|
|
1404
|
+
if (!!columnFormat) {
|
|
1405
|
+
const value = row[columnFormat.name];
|
|
1406
|
+
const formattedValue = evaluateValueFormat(columnFormat, value, locale);
|
|
1407
|
+
adjRow[columnFormat.displayName] = formattedValue;
|
|
1408
|
+
chartRow[columnFormat.displayName] = formatNumberEnabled(columnFormat.valueFormat) ? value : formattedValue;
|
|
1409
|
+
htmlRowValues.push(formattedValue);
|
|
1410
|
+
[htmlRowStyles, htmlColumnStyles] = processHtmlStyles(formattedValue, columnFormat.displayName, columnFormat, htmlRowStyles, htmlColumnStyles);
|
|
1411
|
+
htmlTransposeColumnStyles = processHtmlTransposeStyles(formattedValue, columnFormat.displayName, columnFormat, htmlTransposeColumnStyles);
|
|
1412
|
+
if (rowIdx === 0) {
|
|
1413
|
+
htmlColumnNames.push(columnFormat.displayName);
|
|
1414
|
+
}
|
|
1415
|
+
}
|
|
1416
|
+
});
|
|
1417
|
+
adjRows.push(adjRow);
|
|
1418
|
+
chartRows.push(chartRow);
|
|
1419
|
+
if (rowIdx < DATAGRID_HTML_ROWS) {
|
|
1420
|
+
htmlBuf += buildHtmlRow(htmlRowValues, htmlRowStyles, htmlColumnStyles, DATAGRID_HTML_COLS);
|
|
1421
|
+
if (rowIdx < DATAGRID_TEAMS_ROWS) {
|
|
1422
|
+
[teamsRowStyles, teamsColumnStyles] = mapTeamsStyles(htmlColumnNames, htmlRowStyles, htmlColumnStyles, teamsRowStyles, teamsColumnStyles);
|
|
1423
|
+
teamsRows.push(createTeamsTableRow(htmlColumnNames, htmlRowValues, teamsRowStyles, teamsColumnStyles));
|
|
1424
|
+
}
|
|
1425
|
+
}
|
|
1426
|
+
if (rowIdx === 0) {
|
|
1427
|
+
const transposedRows = htmlColumnNames.map((key, index) => ({
|
|
1428
|
+
key,
|
|
1429
|
+
value: htmlRowValues[index],
|
|
1430
|
+
}));
|
|
1431
|
+
for (let i = 0; i < transposedRows.length; i++) {
|
|
1432
|
+
htmlTransposeBuf += buildHtmlTransposeRow(transposedRows[i], htmlTransposeColumnStyles[i]);
|
|
1433
|
+
teamsTranspose.push(createTeamsTransposeTableRow(transposedRows[i], mapTeamsTransposeStyles(htmlTransposeColumnStyles[i])));
|
|
1434
|
+
}
|
|
1435
|
+
}
|
|
1436
|
+
rowIdx++;
|
|
1437
|
+
});
|
|
1438
|
+
if (!!condition) {
|
|
1439
|
+
adjRows = evaluateDatagridCondition(rows, condition);
|
|
1440
|
+
}
|
|
1441
|
+
}
|
|
1442
|
+
return {
|
|
1443
|
+
...datagridState,
|
|
1444
|
+
adjRows,
|
|
1445
|
+
chartRows,
|
|
1446
|
+
html: `<p style="font-size: 12px;">Displaying first <b>${DATAGRID_HTML_ROWS} rows</b> and <b>${DATAGRID_HTML_COLS} columns</b></p>\n\n${buildHtmlTableHeader(htmlColumnNames.slice(0, DATAGRID_HTML_COLS))}${htmlBuf}${buildHtmlTableFooter()}`,
|
|
1447
|
+
teamsRows: [
|
|
1448
|
+
createTeamsTableColumns(htmlColumnNames),
|
|
1449
|
+
...teamsRows
|
|
1450
|
+
],
|
|
1451
|
+
htmlTranspose: `${buildHtmlTableTransposeHeader()}${htmlTransposeBuf}${buildHtmlTableFooter()}`,
|
|
1452
|
+
teamsTranspose
|
|
1453
|
+
};
|
|
1454
|
+
}
|