@cellaware/utils 7.2.4 → 7.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chatwms/datagrid.d.ts +120 -0
- package/dist/chatwms/datagrid.js +781 -0
- package/dist/util.d.ts +7 -11
- package/dist/util.js +65 -28
- package/package.json +1 -1
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
export interface DatagridStateBase {
|
|
2
|
+
columnState?: ColumnState[];
|
|
3
|
+
columnGroupState?: any[];
|
|
4
|
+
isPivotMode?: boolean;
|
|
5
|
+
filterModel?: FilterModel;
|
|
6
|
+
columnFormats?: ColumnFormat[];
|
|
7
|
+
}
|
|
8
|
+
export interface DatagridState extends DatagridStateBase {
|
|
9
|
+
adjRowData: any[];
|
|
10
|
+
chartRowData: any[];
|
|
11
|
+
}
|
|
12
|
+
export declare function initDatagridState(): DatagridState;
|
|
13
|
+
export type ColumnState = {
|
|
14
|
+
colId: string;
|
|
15
|
+
hide?: boolean | null;
|
|
16
|
+
sort?: 'asc' | 'desc' | null;
|
|
17
|
+
sortIndex?: number | null;
|
|
18
|
+
aggFunc?: string | null;
|
|
19
|
+
pivot?: boolean;
|
|
20
|
+
pivotIndex?: number | null;
|
|
21
|
+
rowGroup?: boolean;
|
|
22
|
+
rowGroupIndex?: number | null;
|
|
23
|
+
};
|
|
24
|
+
type FilterType = 'text' | 'number' | 'set';
|
|
25
|
+
type Operator = 'AND' | 'OR';
|
|
26
|
+
type SimpleCondition = {
|
|
27
|
+
filterType: FilterType;
|
|
28
|
+
type: string;
|
|
29
|
+
filter?: any;
|
|
30
|
+
filterTo?: any;
|
|
31
|
+
values?: any[];
|
|
32
|
+
};
|
|
33
|
+
type CompoundFilter = {
|
|
34
|
+
filterType: FilterType;
|
|
35
|
+
operator: Operator;
|
|
36
|
+
conditions: SimpleCondition[];
|
|
37
|
+
};
|
|
38
|
+
type ColumnFilter = SimpleCondition | CompoundFilter;
|
|
39
|
+
type FilterModel = Record<string, ColumnFilter>;
|
|
40
|
+
export interface ColumnFormat {
|
|
41
|
+
name: string;
|
|
42
|
+
displayName: string;
|
|
43
|
+
/** `text` | `number` | `date` | `boolean` */
|
|
44
|
+
type: string;
|
|
45
|
+
valueFormat: ValueFormat;
|
|
46
|
+
conditionalFormats: ConditionalFormat[];
|
|
47
|
+
}
|
|
48
|
+
export declare const DEFAULT_VALUE_FORMAT_VALUE = "none";
|
|
49
|
+
export interface ValueFormat {
|
|
50
|
+
/** `none` | `lower` | `upper` | `title` */
|
|
51
|
+
txtCaseVal: string;
|
|
52
|
+
/** `none` | `0` | `1` | `2` | `3` | `4` */
|
|
53
|
+
numRoundVal: string;
|
|
54
|
+
/** `none` | `on` */
|
|
55
|
+
numCommaVal: string;
|
|
56
|
+
/** `none` | `dollar` */
|
|
57
|
+
numCurrencyVal: string;
|
|
58
|
+
/** `none` | `1` | `100` */
|
|
59
|
+
numPercentVal: string;
|
|
60
|
+
/** `none` | `slash` | `dash` */
|
|
61
|
+
dteSeparatorVal: string;
|
|
62
|
+
/** `none` | `numeric` | `2-digit` */
|
|
63
|
+
dteYearVal: string;
|
|
64
|
+
/** `none` | `numeric` | `2-digit` | `long` | `short` */
|
|
65
|
+
dteMonthVal: string;
|
|
66
|
+
/** `none` | `numeric` | `2-digit` */
|
|
67
|
+
dteDayVal: string;
|
|
68
|
+
/** `none` | `long` | `short` */
|
|
69
|
+
dteWeekdayVal: string;
|
|
70
|
+
/** `none` | `h12` | `h24` (we will translate to `h23`) */
|
|
71
|
+
dteHourcycleVal: string;
|
|
72
|
+
/** `none` | `numeric` | `2-digit` */
|
|
73
|
+
dteHourVal: string;
|
|
74
|
+
/** `none` | `numeric` | `2-digit` */
|
|
75
|
+
dteMinuteVal: string;
|
|
76
|
+
/** `none` | `numeric` | `2-digit` */
|
|
77
|
+
dteSecondVal: string;
|
|
78
|
+
}
|
|
79
|
+
export declare function evaluateValueFormat(colFmt: ColumnFormat, value: any, locale: string): any;
|
|
80
|
+
export declare function formatNumberEnabled(fmt: ValueFormat): boolean;
|
|
81
|
+
export declare enum ConditionalFormatRule {
|
|
82
|
+
CONTAINS = "conditional-format-rule-contains",
|
|
83
|
+
DOES_NOT_CONTAIN = "conditional-format-rule-does-not-contain",
|
|
84
|
+
BEGINS_WITH = "conditional-format-rule-begins-with",
|
|
85
|
+
ENDS_WITH = "conditional-format-rule-ends-with",
|
|
86
|
+
GREATER_THAN = "conditional-format-rule-greater-than",
|
|
87
|
+
GREATER_THAN_OR_EQUAL_TO = "conditional-format-rule-greater-than-or-equal-to",
|
|
88
|
+
LESS_THAN = "conditional-format-rule-less-than",
|
|
89
|
+
LESS_THAN_OR_EQUAL_TO = "conditional-format-rule-less-than-or-equal-to",
|
|
90
|
+
BETWEEN = "conditional-format-rule-between",
|
|
91
|
+
EQUALS = "conditional-format-rule-equals",
|
|
92
|
+
DOES_NOT_EQUAL = "conditional-format-rule-does-not-equal",
|
|
93
|
+
BLANK = "conditional-format-rule-blank",
|
|
94
|
+
NOT_BLANK = "conditional-format-rule-not-blank"
|
|
95
|
+
}
|
|
96
|
+
export declare const CONDITIONAL_FORMAT_RULES: string[];
|
|
97
|
+
export declare const CONDITIONAL_FORMAT_RULES_TEXT: string[];
|
|
98
|
+
export declare const CONDITIONAL_FORMAT_RULES_NUMBER: string[];
|
|
99
|
+
export declare const CONDITIONAL_FORMAT_RULES_BOOLEAN: string[];
|
|
100
|
+
export interface ConditionalFormat {
|
|
101
|
+
sequence: number;
|
|
102
|
+
rule: string;
|
|
103
|
+
value: string;
|
|
104
|
+
value2?: string;
|
|
105
|
+
style: string;
|
|
106
|
+
row: boolean;
|
|
107
|
+
}
|
|
108
|
+
export interface DatagridCondition {
|
|
109
|
+
columnName: string;
|
|
110
|
+
/** `text` | `number` | `date` | `boolean` */
|
|
111
|
+
dataType: string;
|
|
112
|
+
rule: string;
|
|
113
|
+
value: string;
|
|
114
|
+
value2?: string;
|
|
115
|
+
}
|
|
116
|
+
export declare function initDatagridCondition(): DatagridCondition;
|
|
117
|
+
export declare function summarizeCondition(condition: DatagridCondition, localeMessageFn: (language: string, messageId: string) => Promise<string>): Promise<string>;
|
|
118
|
+
export declare function codifyCondition(condition: DatagridCondition): string;
|
|
119
|
+
export declare function transformDatagrid(rows: any[], datagridState: DatagridStateBase, locale: string, condition?: DatagridCondition): DatagridState;
|
|
120
|
+
export {};
|
|
@@ -0,0 +1,781 @@
|
|
|
1
|
+
import { isDateString } from "../util.js";
|
|
2
|
+
import { CHATWMS_DEFAULT_LANGUAGE } from "./user.js";
|
|
3
|
+
export function initDatagridState() {
|
|
4
|
+
return {
|
|
5
|
+
adjRowData: [],
|
|
6
|
+
chartRowData: []
|
|
7
|
+
};
|
|
8
|
+
}
|
|
9
|
+
export const DEFAULT_VALUE_FORMAT_VALUE = 'none';
|
|
10
|
+
export function evaluateValueFormat(colFmt, value, locale) {
|
|
11
|
+
if (value == null)
|
|
12
|
+
return value;
|
|
13
|
+
const fmt = colFmt.valueFormat;
|
|
14
|
+
if (!fmt)
|
|
15
|
+
return value;
|
|
16
|
+
try {
|
|
17
|
+
switch (colFmt.type) {
|
|
18
|
+
case 'text':
|
|
19
|
+
return formatText(value, fmt);
|
|
20
|
+
case 'number':
|
|
21
|
+
return formatNumber(value, fmt);
|
|
22
|
+
case 'date':
|
|
23
|
+
return formatDate(value, fmt, locale);
|
|
24
|
+
default:
|
|
25
|
+
return value;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
return value;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
function formatTextEnabled(fmt) {
|
|
33
|
+
return fmt.txtCaseVal !== DEFAULT_VALUE_FORMAT_VALUE;
|
|
34
|
+
}
|
|
35
|
+
function formatText(value, fmt) {
|
|
36
|
+
if (!formatTextEnabled(fmt))
|
|
37
|
+
return value;
|
|
38
|
+
const str = String(value);
|
|
39
|
+
switch (fmt.txtCaseVal) {
|
|
40
|
+
case 'lower': return str.toLowerCase();
|
|
41
|
+
case 'upper': return str.toUpperCase();
|
|
42
|
+
case 'title':
|
|
43
|
+
return str
|
|
44
|
+
.toLowerCase()
|
|
45
|
+
.split(' ')
|
|
46
|
+
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
|
|
47
|
+
.join(' ');
|
|
48
|
+
default:
|
|
49
|
+
return value;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
export function formatNumberEnabled(fmt) {
|
|
53
|
+
return fmt.numCommaVal !== DEFAULT_VALUE_FORMAT_VALUE ||
|
|
54
|
+
fmt.numCurrencyVal !== DEFAULT_VALUE_FORMAT_VALUE ||
|
|
55
|
+
fmt.numPercentVal !== DEFAULT_VALUE_FORMAT_VALUE ||
|
|
56
|
+
fmt.numRoundVal !== DEFAULT_VALUE_FORMAT_VALUE;
|
|
57
|
+
}
|
|
58
|
+
function formatNumber(value, fmt) {
|
|
59
|
+
if (!formatNumberEnabled(fmt))
|
|
60
|
+
return value;
|
|
61
|
+
let num = parseFloat(value);
|
|
62
|
+
if (isNaN(num))
|
|
63
|
+
return value;
|
|
64
|
+
/*
|
|
65
|
+
We need to be intentional about the order of operations for number formatting.
|
|
66
|
+
For example, some formatting operations will transform the number to a string.
|
|
67
|
+
If we need to perform some arithmetic or round it, we need to do so first before
|
|
68
|
+
number is transformed into a string.
|
|
69
|
+
*/
|
|
70
|
+
if (fmt.numPercentVal === '100') {
|
|
71
|
+
num *= 100;
|
|
72
|
+
}
|
|
73
|
+
const decimals = parseInt(fmt.numRoundVal ?? '', 10);
|
|
74
|
+
if (!isNaN(decimals)) {
|
|
75
|
+
num = parseFloat(num.toFixed(decimals));
|
|
76
|
+
}
|
|
77
|
+
let formatted = num;
|
|
78
|
+
if (fmt.numCommaVal === 'on') {
|
|
79
|
+
formatted = num.toLocaleString();
|
|
80
|
+
}
|
|
81
|
+
if (fmt.numCurrencyVal === 'dollar') {
|
|
82
|
+
formatted = `$${formatted}`;
|
|
83
|
+
}
|
|
84
|
+
if (fmt.numPercentVal === '1' || fmt.numPercentVal === '100') {
|
|
85
|
+
formatted = `${formatted}%`;
|
|
86
|
+
}
|
|
87
|
+
return formatted;
|
|
88
|
+
}
|
|
89
|
+
function formatDate(value, fmt, locale) {
|
|
90
|
+
let valueStr;
|
|
91
|
+
if (value instanceof Date) {
|
|
92
|
+
valueStr = value.toISOString();
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
valueStr = String(value);
|
|
96
|
+
}
|
|
97
|
+
// Make sure we are dealing with a valid date string before we go any further.
|
|
98
|
+
if (!isDateString(valueStr)) {
|
|
99
|
+
return value;
|
|
100
|
+
}
|
|
101
|
+
// Remove time zone and milliseconds from date string.
|
|
102
|
+
valueStr = valueStr.replace(/Z/i, '');
|
|
103
|
+
valueStr = valueStr.replace(/\.\d+$/, '');
|
|
104
|
+
// Initialize date options and functionality.
|
|
105
|
+
const options = { hourCycle: 'h23' };
|
|
106
|
+
let formatDateEnabled = false;
|
|
107
|
+
const setOpt = (k, v) => {
|
|
108
|
+
if (v !== DEFAULT_VALUE_FORMAT_VALUE) {
|
|
109
|
+
options[k] = v;
|
|
110
|
+
formatDateEnabled = true;
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
setOpt('year', fmt.dteYearVal);
|
|
114
|
+
setOpt('month', fmt.dteMonthVal);
|
|
115
|
+
setOpt('day', fmt.dteDayVal);
|
|
116
|
+
setOpt('weekday', fmt.dteWeekdayVal);
|
|
117
|
+
setOpt('hour', fmt.dteHourVal);
|
|
118
|
+
setOpt('minute', fmt.dteMinuteVal);
|
|
119
|
+
setOpt('second', fmt.dteSecondVal);
|
|
120
|
+
setOpt('hourCycle', fmt.dteHourcycleVal === 'h24' ? 'h23' : fmt.dteHourcycleVal);
|
|
121
|
+
// Handle simple date edge cases.
|
|
122
|
+
if (valueStr.length === 10) {
|
|
123
|
+
// Special handling for ISO-ish short date string (YYYY-AA-BB or AA-BB-YYYY).
|
|
124
|
+
const c4 = valueStr.at(4);
|
|
125
|
+
const c5 = valueStr.at(5);
|
|
126
|
+
if (c4 === '-' || c4 === '/' || c5 === '-' || c5 === '/') {
|
|
127
|
+
valueStr += ' 00:00:00'; // Make sure time is zero'd out.
|
|
128
|
+
if (!formatDateEnabled) {
|
|
129
|
+
setOpt('day', '2-digit');
|
|
130
|
+
setOpt('month', '2-digit');
|
|
131
|
+
setOpt('year', 'numeric');
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
else if (valueStr.length === 8) {
|
|
136
|
+
// Special handling for ISO-ish shorter date string (YY-MM-DD in any order).
|
|
137
|
+
const c2 = valueStr.at(2);
|
|
138
|
+
const c5 = valueStr.at(5);
|
|
139
|
+
if ((c2 === '-' && c5 === '-') || (c2 === '/' && c5 === '/')) {
|
|
140
|
+
valueStr += ' 00:00:00'; // Make sure time is zero'd out.
|
|
141
|
+
if (!formatDateEnabled) {
|
|
142
|
+
setOpt('day', '2-digit');
|
|
143
|
+
setOpt('month', '2-digit');
|
|
144
|
+
setOpt('year', '2-digit');
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
let dteStr = new Date(valueStr).toLocaleString(locale, options);
|
|
149
|
+
// NOTE: by default, we should use slash as separator.
|
|
150
|
+
if (fmt.dteSeparatorVal === DEFAULT_VALUE_FORMAT_VALUE) {
|
|
151
|
+
dteStr = dteStr.replaceAll('-', '/');
|
|
152
|
+
}
|
|
153
|
+
else if (fmt.dteSeparatorVal === 'dash') {
|
|
154
|
+
dteStr = dteStr.replaceAll('/', '-');
|
|
155
|
+
}
|
|
156
|
+
else if (fmt.dteSeparatorVal === 'slash') {
|
|
157
|
+
dteStr = dteStr.replaceAll('-', '/');
|
|
158
|
+
}
|
|
159
|
+
dteStr = dteStr.replaceAll(',', '');
|
|
160
|
+
// We want to preserve simple dates.
|
|
161
|
+
// If no is formatting detected, the date's length should not be greater than the original value's.
|
|
162
|
+
if (!formatDateEnabled) {
|
|
163
|
+
dteStr = dteStr.substring(0, value.length);
|
|
164
|
+
dteStr = dteStr.trim();
|
|
165
|
+
// Trim any separator characters off.
|
|
166
|
+
if (dteStr.endsWith('-') || dteStr.endsWith('/') || dteStr.endsWith(':') || dteStr.endsWith('.') || dteStr.endsWith('T')) {
|
|
167
|
+
dteStr = dteStr.substring(0, dteStr.length - 1).trim();
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
return dteStr;
|
|
171
|
+
}
|
|
172
|
+
export var ConditionalFormatRule;
|
|
173
|
+
(function (ConditionalFormatRule) {
|
|
174
|
+
// TEXT:
|
|
175
|
+
ConditionalFormatRule["CONTAINS"] = "conditional-format-rule-contains";
|
|
176
|
+
ConditionalFormatRule["DOES_NOT_CONTAIN"] = "conditional-format-rule-does-not-contain";
|
|
177
|
+
ConditionalFormatRule["BEGINS_WITH"] = "conditional-format-rule-begins-with";
|
|
178
|
+
ConditionalFormatRule["ENDS_WITH"] = "conditional-format-rule-ends-with";
|
|
179
|
+
// NUMBER:
|
|
180
|
+
ConditionalFormatRule["GREATER_THAN"] = "conditional-format-rule-greater-than";
|
|
181
|
+
ConditionalFormatRule["GREATER_THAN_OR_EQUAL_TO"] = "conditional-format-rule-greater-than-or-equal-to";
|
|
182
|
+
ConditionalFormatRule["LESS_THAN"] = "conditional-format-rule-less-than";
|
|
183
|
+
ConditionalFormatRule["LESS_THAN_OR_EQUAL_TO"] = "conditional-format-rule-less-than-or-equal-to";
|
|
184
|
+
ConditionalFormatRule["BETWEEN"] = "conditional-format-rule-between";
|
|
185
|
+
// ANY:
|
|
186
|
+
ConditionalFormatRule["EQUALS"] = "conditional-format-rule-equals";
|
|
187
|
+
ConditionalFormatRule["DOES_NOT_EQUAL"] = "conditional-format-rule-does-not-equal";
|
|
188
|
+
ConditionalFormatRule["BLANK"] = "conditional-format-rule-blank";
|
|
189
|
+
ConditionalFormatRule["NOT_BLANK"] = "conditional-format-rule-not-blank";
|
|
190
|
+
})(ConditionalFormatRule || (ConditionalFormatRule = {}));
|
|
191
|
+
export const CONDITIONAL_FORMAT_RULES = [
|
|
192
|
+
ConditionalFormatRule.EQUALS,
|
|
193
|
+
ConditionalFormatRule.DOES_NOT_EQUAL,
|
|
194
|
+
ConditionalFormatRule.BLANK,
|
|
195
|
+
ConditionalFormatRule.NOT_BLANK,
|
|
196
|
+
ConditionalFormatRule.CONTAINS,
|
|
197
|
+
ConditionalFormatRule.DOES_NOT_CONTAIN,
|
|
198
|
+
ConditionalFormatRule.BEGINS_WITH,
|
|
199
|
+
ConditionalFormatRule.ENDS_WITH,
|
|
200
|
+
ConditionalFormatRule.GREATER_THAN,
|
|
201
|
+
ConditionalFormatRule.GREATER_THAN_OR_EQUAL_TO,
|
|
202
|
+
ConditionalFormatRule.LESS_THAN,
|
|
203
|
+
ConditionalFormatRule.LESS_THAN_OR_EQUAL_TO,
|
|
204
|
+
ConditionalFormatRule.BETWEEN
|
|
205
|
+
];
|
|
206
|
+
export const CONDITIONAL_FORMAT_RULES_TEXT = [
|
|
207
|
+
ConditionalFormatRule.EQUALS,
|
|
208
|
+
ConditionalFormatRule.DOES_NOT_EQUAL,
|
|
209
|
+
ConditionalFormatRule.BLANK,
|
|
210
|
+
ConditionalFormatRule.NOT_BLANK,
|
|
211
|
+
ConditionalFormatRule.CONTAINS,
|
|
212
|
+
ConditionalFormatRule.DOES_NOT_CONTAIN,
|
|
213
|
+
ConditionalFormatRule.BEGINS_WITH,
|
|
214
|
+
ConditionalFormatRule.ENDS_WITH
|
|
215
|
+
];
|
|
216
|
+
export const CONDITIONAL_FORMAT_RULES_NUMBER = [
|
|
217
|
+
ConditionalFormatRule.EQUALS,
|
|
218
|
+
ConditionalFormatRule.DOES_NOT_EQUAL,
|
|
219
|
+
ConditionalFormatRule.BLANK,
|
|
220
|
+
ConditionalFormatRule.NOT_BLANK,
|
|
221
|
+
ConditionalFormatRule.GREATER_THAN,
|
|
222
|
+
ConditionalFormatRule.GREATER_THAN_OR_EQUAL_TO,
|
|
223
|
+
ConditionalFormatRule.LESS_THAN,
|
|
224
|
+
ConditionalFormatRule.LESS_THAN_OR_EQUAL_TO,
|
|
225
|
+
ConditionalFormatRule.BETWEEN
|
|
226
|
+
];
|
|
227
|
+
export const CONDITIONAL_FORMAT_RULES_BOOLEAN = [
|
|
228
|
+
ConditionalFormatRule.EQUALS,
|
|
229
|
+
ConditionalFormatRule.DOES_NOT_EQUAL,
|
|
230
|
+
ConditionalFormatRule.BLANK,
|
|
231
|
+
ConditionalFormatRule.NOT_BLANK
|
|
232
|
+
];
|
|
233
|
+
export function initDatagridCondition() {
|
|
234
|
+
return {
|
|
235
|
+
columnName: '',
|
|
236
|
+
dataType: '',
|
|
237
|
+
rule: '',
|
|
238
|
+
value: ''
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
export async function summarizeCondition(condition, localeMessageFn) {
|
|
242
|
+
let ruleStr = '';
|
|
243
|
+
switch (condition.rule) {
|
|
244
|
+
case ConditionalFormatRule.BETWEEN:
|
|
245
|
+
ruleStr = `${await localeMessageFn(CHATWMS_DEFAULT_LANGUAGE, condition.rule)} ${condition.value} ${await localeMessageFn(CHATWMS_DEFAULT_LANGUAGE, 'conditional-format-rule-and')} ${condition.value2}`;
|
|
246
|
+
break;
|
|
247
|
+
case ConditionalFormatRule.BLANK:
|
|
248
|
+
case ConditionalFormatRule.NOT_BLANK:
|
|
249
|
+
ruleStr = `${await localeMessageFn(CHATWMS_DEFAULT_LANGUAGE, condition.rule)}`;
|
|
250
|
+
break;
|
|
251
|
+
default:
|
|
252
|
+
if (condition.dataType === 'text') {
|
|
253
|
+
ruleStr = `${await localeMessageFn(CHATWMS_DEFAULT_LANGUAGE, condition.rule)} '${condition.value}'`;
|
|
254
|
+
}
|
|
255
|
+
else {
|
|
256
|
+
ruleStr = `${await localeMessageFn(CHATWMS_DEFAULT_LANGUAGE, condition.rule)} ${condition.value}`;
|
|
257
|
+
}
|
|
258
|
+
break;
|
|
259
|
+
}
|
|
260
|
+
return `${condition.columnName} ${ruleStr}`;
|
|
261
|
+
}
|
|
262
|
+
export function codifyCondition(condition) {
|
|
263
|
+
// NOTE: since we check for action/unsafe SQL with every query, we do not need to check for SQL injection here.
|
|
264
|
+
if (condition.dataType === 'number') {
|
|
265
|
+
const v1 = !condition.value || condition.value == '' ? 0 : condition.value;
|
|
266
|
+
const v2 = !condition.value2 || condition.value2 == '' ? 0 : condition.value2;
|
|
267
|
+
// Need to wrap in quotes due to how ChatWMS will alias columns (ex: location_id -> Location ID).
|
|
268
|
+
const quotedColumnName = `"${condition.columnName}"`;
|
|
269
|
+
switch (condition.rule) {
|
|
270
|
+
case ConditionalFormatRule.EQUALS:
|
|
271
|
+
return `${quotedColumnName} = ${v1}`;
|
|
272
|
+
case ConditionalFormatRule.DOES_NOT_EQUAL:
|
|
273
|
+
return `${quotedColumnName} != ${v1}`;
|
|
274
|
+
case ConditionalFormatRule.GREATER_THAN:
|
|
275
|
+
return `${quotedColumnName} > ${v1}`;
|
|
276
|
+
case ConditionalFormatRule.GREATER_THAN_OR_EQUAL_TO:
|
|
277
|
+
return `${quotedColumnName} >= ${v1}`;
|
|
278
|
+
case ConditionalFormatRule.LESS_THAN:
|
|
279
|
+
return `${quotedColumnName} < ${v1}`;
|
|
280
|
+
case ConditionalFormatRule.LESS_THAN_OR_EQUAL_TO:
|
|
281
|
+
return `${quotedColumnName} <= ${v1}`;
|
|
282
|
+
case ConditionalFormatRule.BETWEEN:
|
|
283
|
+
return `${quotedColumnName} BETWEEN ${v1} AND ${v2}`;
|
|
284
|
+
case ConditionalFormatRule.BLANK:
|
|
285
|
+
return `${quotedColumnName} IS NULL`;
|
|
286
|
+
case ConditionalFormatRule.NOT_BLANK:
|
|
287
|
+
return `${quotedColumnName} IS NOT NULL`;
|
|
288
|
+
default:
|
|
289
|
+
return '1 = 2'; // Fail if we get here.
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
else if (condition.dataType === 'date') {
|
|
293
|
+
const v1 = !condition.value || condition.value == '' ? 0 : condition.value;
|
|
294
|
+
const v2 = !condition.value2 || condition.value2 == '' ? 0 : condition.value2;
|
|
295
|
+
// Need to wrap in quotes due to how ChatWMS will alias columns (ex: location_id -> Location ID).
|
|
296
|
+
const quotedColumnName = `"${condition.columnName}"`;
|
|
297
|
+
switch (condition.rule) {
|
|
298
|
+
case ConditionalFormatRule.EQUALS:
|
|
299
|
+
return `${quotedColumnName} = '${v1}'`;
|
|
300
|
+
case ConditionalFormatRule.DOES_NOT_EQUAL:
|
|
301
|
+
return `${quotedColumnName} != '${v1}'`;
|
|
302
|
+
case ConditionalFormatRule.GREATER_THAN:
|
|
303
|
+
return `${quotedColumnName} > '${v1}'`;
|
|
304
|
+
case ConditionalFormatRule.GREATER_THAN_OR_EQUAL_TO:
|
|
305
|
+
return `${quotedColumnName} >= '${v1}'`;
|
|
306
|
+
case ConditionalFormatRule.LESS_THAN:
|
|
307
|
+
return `${quotedColumnName} < '${v1}'`;
|
|
308
|
+
case ConditionalFormatRule.LESS_THAN_OR_EQUAL_TO:
|
|
309
|
+
return `${quotedColumnName} <= '${v1}'`;
|
|
310
|
+
case ConditionalFormatRule.BETWEEN:
|
|
311
|
+
return `${quotedColumnName} BETWEEN '${v1}' AND '${v2}'`;
|
|
312
|
+
case ConditionalFormatRule.BLANK:
|
|
313
|
+
return `${quotedColumnName} IS NULL`;
|
|
314
|
+
case ConditionalFormatRule.NOT_BLANK:
|
|
315
|
+
return `${quotedColumnName} IS NOT NULL`;
|
|
316
|
+
default:
|
|
317
|
+
return '1 = 2'; // Fail if we get here.
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
else {
|
|
321
|
+
// NOTE: it is a safe assumption to trim any whitespace off the value.
|
|
322
|
+
const v1 = (condition.value ?? '__DUMMY__').trim();
|
|
323
|
+
// Need to wrap in quotes due to how ChatWMS will alias columns (ex: location_id -> Location ID).
|
|
324
|
+
// NOTE: wrapping column and values in `UPPER` to support case insensitivity.
|
|
325
|
+
const quotedColumnName = `UPPER("${condition.columnName}")`;
|
|
326
|
+
switch (condition.rule) {
|
|
327
|
+
case ConditionalFormatRule.EQUALS:
|
|
328
|
+
return `${quotedColumnName} = UPPER('${v1}')`;
|
|
329
|
+
case ConditionalFormatRule.DOES_NOT_EQUAL:
|
|
330
|
+
return `${quotedColumnName} != UPPER('${v1}')`;
|
|
331
|
+
case ConditionalFormatRule.CONTAINS:
|
|
332
|
+
return `${quotedColumnName} LIKE UPPER('%${v1}%')`;
|
|
333
|
+
case ConditionalFormatRule.DOES_NOT_CONTAIN:
|
|
334
|
+
return `${quotedColumnName} NOT LIKE UPPER('%${v1}%')`;
|
|
335
|
+
case ConditionalFormatRule.BEGINS_WITH:
|
|
336
|
+
return `${quotedColumnName} LIKE UPPER('${v1}%')`;
|
|
337
|
+
case ConditionalFormatRule.ENDS_WITH:
|
|
338
|
+
return `${quotedColumnName} LIKE UPPER('%${v1}')`;
|
|
339
|
+
case ConditionalFormatRule.BLANK:
|
|
340
|
+
return `${quotedColumnName} IS NULL`;
|
|
341
|
+
case ConditionalFormatRule.NOT_BLANK:
|
|
342
|
+
return `${quotedColumnName} IS NOT NULL`;
|
|
343
|
+
default:
|
|
344
|
+
return '1 = 2'; // Fail if we get here.
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
/**
|
|
349
|
+
* Equivalent of `parseNumeric` in front end
|
|
350
|
+
*
|
|
351
|
+
* https://github.com/cellaware/chatwms-az-swa-ng/blob/development/src/app/utils/data.ts#L126
|
|
352
|
+
*/
|
|
353
|
+
function stripNumericValueFormat(value) {
|
|
354
|
+
if (value === undefined)
|
|
355
|
+
return null;
|
|
356
|
+
if (typeof value === 'number')
|
|
357
|
+
return value;
|
|
358
|
+
// Remove commas, dollar signs, percent signs
|
|
359
|
+
const cleaned = value.replace(/[$,%]/g, '').replace(/,/g, '');
|
|
360
|
+
return cleaned;
|
|
361
|
+
}
|
|
362
|
+
function buildDatagridConditionEvaluator(condition) {
|
|
363
|
+
const v1 = condition.value;
|
|
364
|
+
const v2 = condition.value2;
|
|
365
|
+
const isBlank = (v) => v === null || v === undefined || (condition.dataType !== 'number' && condition.dataType !== 'boolean' && v === '');
|
|
366
|
+
if (condition.dataType === 'number') {
|
|
367
|
+
switch (condition.rule) {
|
|
368
|
+
case ConditionalFormatRule.EQUALS:
|
|
369
|
+
return v => stripNumericValueFormat(v) == v1;
|
|
370
|
+
case ConditionalFormatRule.DOES_NOT_EQUAL:
|
|
371
|
+
return v => stripNumericValueFormat(v) != v1;
|
|
372
|
+
case ConditionalFormatRule.GREATER_THAN:
|
|
373
|
+
return v => stripNumericValueFormat(v) > v1;
|
|
374
|
+
case ConditionalFormatRule.GREATER_THAN_OR_EQUAL_TO:
|
|
375
|
+
return v => stripNumericValueFormat(v) >= v1;
|
|
376
|
+
case ConditionalFormatRule.LESS_THAN:
|
|
377
|
+
return v => stripNumericValueFormat(v) < v1;
|
|
378
|
+
case ConditionalFormatRule.LESS_THAN_OR_EQUAL_TO:
|
|
379
|
+
return v => stripNumericValueFormat(v) <= v1;
|
|
380
|
+
case ConditionalFormatRule.BETWEEN:
|
|
381
|
+
return v => stripNumericValueFormat(v) >= v1 && stripNumericValueFormat(v) <= (v2 ?? v1); // fallback to v1 if v2 missing
|
|
382
|
+
case ConditionalFormatRule.BLANK:
|
|
383
|
+
return v => isBlank(v);
|
|
384
|
+
case ConditionalFormatRule.NOT_BLANK:
|
|
385
|
+
return v => !isBlank(v);
|
|
386
|
+
default:
|
|
387
|
+
return () => false;
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
else {
|
|
391
|
+
switch (condition.rule) {
|
|
392
|
+
case ConditionalFormatRule.EQUALS:
|
|
393
|
+
return v => v == v1;
|
|
394
|
+
case ConditionalFormatRule.DOES_NOT_EQUAL:
|
|
395
|
+
return v => v != v1;
|
|
396
|
+
case ConditionalFormatRule.CONTAINS:
|
|
397
|
+
return v => typeof v === 'string' && v.includes(v1);
|
|
398
|
+
case ConditionalFormatRule.DOES_NOT_CONTAIN:
|
|
399
|
+
return v => typeof v === 'string' && !v.includes(v1);
|
|
400
|
+
case ConditionalFormatRule.BEGINS_WITH:
|
|
401
|
+
return v => typeof v === 'string' && v.startsWith(v1);
|
|
402
|
+
case ConditionalFormatRule.ENDS_WITH:
|
|
403
|
+
return v => typeof v === 'string' && v.endsWith(v1);
|
|
404
|
+
case ConditionalFormatRule.BLANK:
|
|
405
|
+
return v => isBlank(v);
|
|
406
|
+
case ConditionalFormatRule.NOT_BLANK:
|
|
407
|
+
return v => !isBlank(v);
|
|
408
|
+
default:
|
|
409
|
+
return () => false;
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
function buildPivotDatagridConditionEvaluator(condition, columnNames) {
|
|
414
|
+
const fn = buildDatagridConditionEvaluator(condition);
|
|
415
|
+
return row => columnNames.some(columnName => fn(row[columnName]));
|
|
416
|
+
}
|
|
417
|
+
function evaluateDatagridCondition(rows, condition) {
|
|
418
|
+
const fn = buildDatagridConditionEvaluator(condition);
|
|
419
|
+
return rows.filter(row => fn(row[condition.columnName]));
|
|
420
|
+
}
|
|
421
|
+
function evaluatePivotDatagridCondition(rows, condition, mappedColumnNames) {
|
|
422
|
+
let columnNames = [condition.columnName];
|
|
423
|
+
const mappedColumnNamesSet = mappedColumnNames.get(condition.columnName);
|
|
424
|
+
if (!!mappedColumnNamesSet) {
|
|
425
|
+
columnNames = Array.from(mappedColumnNamesSet);
|
|
426
|
+
}
|
|
427
|
+
const fn = buildPivotDatagridConditionEvaluator(condition, columnNames);
|
|
428
|
+
return rows.filter(row => fn(row));
|
|
429
|
+
}
|
|
430
|
+
function parseColumnState(columnState) {
|
|
431
|
+
const rowGroupCols = [];
|
|
432
|
+
const pivotCols = [];
|
|
433
|
+
const valueCols = [];
|
|
434
|
+
const sortModel = [];
|
|
435
|
+
for (const col of columnState) {
|
|
436
|
+
// Row Group Columns:
|
|
437
|
+
// NOTE: we only want the highest level data.
|
|
438
|
+
if (col.rowGroup && col.rowGroupIndex === 0) {
|
|
439
|
+
rowGroupCols.push({
|
|
440
|
+
field: col.colId
|
|
441
|
+
});
|
|
442
|
+
}
|
|
443
|
+
// Pivot Columns:
|
|
444
|
+
// NOTE: we only want the highest level data.
|
|
445
|
+
if (col.pivot && col.pivotIndex === 0) {
|
|
446
|
+
pivotCols.push({
|
|
447
|
+
field: col.colId
|
|
448
|
+
});
|
|
449
|
+
}
|
|
450
|
+
// Value Columns (Aggregations):
|
|
451
|
+
if (col.aggFunc) {
|
|
452
|
+
valueCols.push({
|
|
453
|
+
field: col.colId,
|
|
454
|
+
aggFunc: col.aggFunc
|
|
455
|
+
});
|
|
456
|
+
}
|
|
457
|
+
// Sorting:
|
|
458
|
+
if (col.sort) {
|
|
459
|
+
sortModel.push({
|
|
460
|
+
colId: col.colId,
|
|
461
|
+
sort: col.sort
|
|
462
|
+
});
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
// NOTE: commenting this out since we only care about the highest level data.
|
|
466
|
+
// rowGroupCols.sort((a, b) => {
|
|
467
|
+
// const iA = columnState.find(c => c.colId === a.field)?.rowGroupIndex ?? 0;
|
|
468
|
+
// const iB = columnState.find(c => c.colId === b.field)?.rowGroupIndex ?? 0;
|
|
469
|
+
// return iA - iB;
|
|
470
|
+
// });
|
|
471
|
+
// pivotCols.sort((a, b) => {
|
|
472
|
+
// const iA = columnState.find(c => c.colId === a.field)?.pivotIndex ?? 0;
|
|
473
|
+
// const iB = columnState.find(c => c.colId === b.field)?.pivotIndex ?? 0;
|
|
474
|
+
// return iA - iB;
|
|
475
|
+
// });
|
|
476
|
+
sortModel.sort((a, b) => {
|
|
477
|
+
const iA = columnState.find(c => c.colId === a.colId)?.sortIndex ?? 0;
|
|
478
|
+
const iB = columnState.find(c => c.colId === b.colId)?.sortIndex ?? 0;
|
|
479
|
+
return iA - iB;
|
|
480
|
+
});
|
|
481
|
+
return {
|
|
482
|
+
rowGroupCols,
|
|
483
|
+
pivotCols,
|
|
484
|
+
valueCols,
|
|
485
|
+
sortModel
|
|
486
|
+
};
|
|
487
|
+
}
|
|
488
|
+
function filterRows(data, filterModel) {
|
|
489
|
+
const textOps = {
|
|
490
|
+
equals: (v, f) => (v ?? '').toLowerCase() === f.toLowerCase(),
|
|
491
|
+
notEqual: (v, f) => (v ?? '').toLowerCase() !== f.toLowerCase(),
|
|
492
|
+
contains: (v, f) => (v ?? '').toLowerCase().includes(f.toLowerCase()),
|
|
493
|
+
notContains: (v, f) => !(v ?? '').toLowerCase().includes(f.toLowerCase()),
|
|
494
|
+
startsWith: (v, f) => (v ?? '').toLowerCase().startsWith(f.toLowerCase()),
|
|
495
|
+
endsWith: (v, f) => (v ?? '').toLowerCase().endsWith(f.toLowerCase()),
|
|
496
|
+
blank: (v) => v == null || v === '',
|
|
497
|
+
notBlank: (v) => !(v == null || v === '')
|
|
498
|
+
};
|
|
499
|
+
const numberOps = {
|
|
500
|
+
equals: (v, f) => v == f,
|
|
501
|
+
notEqual: (v, f) => v != f,
|
|
502
|
+
lessThan: (v, f) => v < f,
|
|
503
|
+
lessThanOrEqual: (v, f) => v <= f,
|
|
504
|
+
greaterThan: (v, f) => v > f,
|
|
505
|
+
greaterThanOrEqual: (v, f) => v >= f,
|
|
506
|
+
inRange: (v, f, t) => v >= f && v <= t,
|
|
507
|
+
blank: (v) => v == null,
|
|
508
|
+
notBlank: (v) => v != null
|
|
509
|
+
};
|
|
510
|
+
function checkCondition(value, cond) {
|
|
511
|
+
const { filterType, type, filter, filterTo, values } = cond;
|
|
512
|
+
if (filterType === 'text') {
|
|
513
|
+
const fn = textOps[type ?? 'contains'];
|
|
514
|
+
return fn?.(value, filter);
|
|
515
|
+
}
|
|
516
|
+
if (filterType === 'number') {
|
|
517
|
+
const fn = numberOps[type ?? 'equals'];
|
|
518
|
+
return type === 'inRange'
|
|
519
|
+
? fn?.(value, filter, filterTo)
|
|
520
|
+
: fn?.(value, filter);
|
|
521
|
+
}
|
|
522
|
+
if (filterType === 'set') {
|
|
523
|
+
// Only boolean types are supported by set filter -- need to represent as string for comparison.
|
|
524
|
+
return (values ?? []).includes(`${value}`);
|
|
525
|
+
}
|
|
526
|
+
return true;
|
|
527
|
+
}
|
|
528
|
+
function compilePredicate(model) {
|
|
529
|
+
return (row) => {
|
|
530
|
+
return Object.entries(model).every(([field, filter]) => {
|
|
531
|
+
const value = row[field];
|
|
532
|
+
if ('conditions' in filter && Array.isArray(filter.conditions)) {
|
|
533
|
+
const logicFn = filter.operator === 'OR' ? 'some' : 'every';
|
|
534
|
+
return filter.conditions[logicFn](cond => checkCondition(value, cond));
|
|
535
|
+
}
|
|
536
|
+
else {
|
|
537
|
+
return checkCondition(value, filter);
|
|
538
|
+
}
|
|
539
|
+
});
|
|
540
|
+
};
|
|
541
|
+
}
|
|
542
|
+
const predicate = compilePredicate(filterModel);
|
|
543
|
+
return data.filter(predicate);
|
|
544
|
+
}
|
|
545
|
+
function pivotData(data, rowGroupCols, pivotCols, valueCols) {
|
|
546
|
+
const groupBy = (row) => Object.fromEntries(rowGroupCols.map(col => [col.field, row[col.field]]));
|
|
547
|
+
const pivotBy = (row) => pivotCols.map(col => row[col.field]).join('_');
|
|
548
|
+
const grouped = new Map();
|
|
549
|
+
for (const row of data) {
|
|
550
|
+
const groupKey = JSON.stringify(groupBy(row));
|
|
551
|
+
if (!grouped.has(groupKey))
|
|
552
|
+
grouped.set(groupKey, []);
|
|
553
|
+
grouped.get(groupKey).push(row);
|
|
554
|
+
}
|
|
555
|
+
const rows = [];
|
|
556
|
+
const mappedColumnNames = new Map();
|
|
557
|
+
for (const [groupKey, groupRows] of grouped.entries()) {
|
|
558
|
+
const groupObj = JSON.parse(groupKey);
|
|
559
|
+
const pivotBuckets = new Map();
|
|
560
|
+
for (const row of groupRows) {
|
|
561
|
+
const pivotKey = pivotBy(row);
|
|
562
|
+
if (!pivotBuckets.has(pivotKey))
|
|
563
|
+
pivotBuckets.set(pivotKey, []);
|
|
564
|
+
pivotBuckets.get(pivotKey).push(row);
|
|
565
|
+
}
|
|
566
|
+
for (const [pivotKey, rows] of pivotBuckets.entries()) {
|
|
567
|
+
for (const { field, aggFunc } of valueCols) {
|
|
568
|
+
const values = rows.map(r => r[field]).filter(v => v != null);
|
|
569
|
+
const key = `${pivotKey} ${field}`;
|
|
570
|
+
let columnNames = mappedColumnNames.get(field);
|
|
571
|
+
if (!!columnNames) {
|
|
572
|
+
columnNames.add(key);
|
|
573
|
+
}
|
|
574
|
+
else {
|
|
575
|
+
columnNames = new Set([key]);
|
|
576
|
+
mappedColumnNames.set(field, columnNames);
|
|
577
|
+
}
|
|
578
|
+
switch (aggFunc) {
|
|
579
|
+
case 'sum':
|
|
580
|
+
groupObj[key] = values.map(Number).reduce((a, b) => a + b, 0);
|
|
581
|
+
break;
|
|
582
|
+
case 'count':
|
|
583
|
+
groupObj[key] = values.length;
|
|
584
|
+
break;
|
|
585
|
+
case 'min':
|
|
586
|
+
groupObj[key] = values.reduce((a, b) => (a < b ? a : b), values[0]);
|
|
587
|
+
break;
|
|
588
|
+
case 'max':
|
|
589
|
+
groupObj[key] = values.reduce((a, b) => (a > b ? a : b), values[0]);
|
|
590
|
+
break;
|
|
591
|
+
case 'avg': {
|
|
592
|
+
const nums = values.map(Number).filter(n => !isNaN(n));
|
|
593
|
+
groupObj[key] = nums.length ? nums.reduce((a, b) => a + b, 0) / nums.length : null;
|
|
594
|
+
break;
|
|
595
|
+
}
|
|
596
|
+
case 'first':
|
|
597
|
+
groupObj[key] = values[0];
|
|
598
|
+
break;
|
|
599
|
+
case 'last':
|
|
600
|
+
groupObj[key] = values[values.length - 1];
|
|
601
|
+
break;
|
|
602
|
+
default:
|
|
603
|
+
groupObj[key] = null;
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
rows.push(groupObj);
|
|
608
|
+
}
|
|
609
|
+
return { rows, mappedColumnNames };
|
|
610
|
+
}
|
|
611
|
+
function groupAndAggregate(data, rowGroupCols, groupKeys, valueCols) {
|
|
612
|
+
const level = groupKeys.length;
|
|
613
|
+
const groupField = rowGroupCols[level]?.field;
|
|
614
|
+
if (!groupField)
|
|
615
|
+
return data;
|
|
616
|
+
const grouped = new Map();
|
|
617
|
+
for (const row of data) {
|
|
618
|
+
const key = row[groupField];
|
|
619
|
+
if (!grouped.has(key))
|
|
620
|
+
grouped.set(key, []);
|
|
621
|
+
grouped.get(key).push(row);
|
|
622
|
+
}
|
|
623
|
+
const output = [];
|
|
624
|
+
for (const [key, rows] of grouped.entries()) {
|
|
625
|
+
if (level < rowGroupCols.length - 1) {
|
|
626
|
+
output.push({
|
|
627
|
+
group: true,
|
|
628
|
+
key,
|
|
629
|
+
children: groupAndAggregate(rows, rowGroupCols, [...groupKeys, key], valueCols)
|
|
630
|
+
});
|
|
631
|
+
}
|
|
632
|
+
else {
|
|
633
|
+
const aggData = {};
|
|
634
|
+
for (const { field, aggFunc } of valueCols) {
|
|
635
|
+
const rawValues = rows.map(r => r[field]).filter(v => v != null);
|
|
636
|
+
switch (aggFunc) {
|
|
637
|
+
case 'sum': {
|
|
638
|
+
const nums = rawValues.map(Number).filter(v => !isNaN(v));
|
|
639
|
+
aggData[field] = nums.reduce((a, b) => a + b, 0);
|
|
640
|
+
break;
|
|
641
|
+
}
|
|
642
|
+
case 'avg': {
|
|
643
|
+
const nums = rawValues.map(Number).filter(v => !isNaN(v));
|
|
644
|
+
const total = nums.reduce((a, b) => a + b, 0);
|
|
645
|
+
aggData[field] = nums.length > 0 ? total / nums.length : null;
|
|
646
|
+
break;
|
|
647
|
+
}
|
|
648
|
+
case 'min':
|
|
649
|
+
aggData[field] = rawValues.reduce((a, b) => (a < b ? a : b), rawValues[0]);
|
|
650
|
+
break;
|
|
651
|
+
case 'max':
|
|
652
|
+
aggData[field] = rawValues.reduce((a, b) => (a > b ? a : b), rawValues[0]);
|
|
653
|
+
break;
|
|
654
|
+
case 'count':
|
|
655
|
+
aggData[field] = rawValues.length;
|
|
656
|
+
break;
|
|
657
|
+
case 'first':
|
|
658
|
+
aggData[field] = rawValues[0];
|
|
659
|
+
break;
|
|
660
|
+
case 'last':
|
|
661
|
+
aggData[field] = rawValues[rawValues.length - 1];
|
|
662
|
+
break;
|
|
663
|
+
default:
|
|
664
|
+
aggData[field] = null;
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
// NOTE: we only want the highest level data -- we don't care about child rows.
|
|
668
|
+
output.push({
|
|
669
|
+
[groupField]: key,
|
|
670
|
+
...aggData,
|
|
671
|
+
// children: rows
|
|
672
|
+
});
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
return output;
|
|
676
|
+
}
|
|
677
|
+
function sortRows(data, sortModel) {
|
|
678
|
+
return [...data].sort((a, b) => {
|
|
679
|
+
for (const { colId, sort } of sortModel) {
|
|
680
|
+
const valA = a[colId];
|
|
681
|
+
const valB = b[colId];
|
|
682
|
+
if (valA === valB)
|
|
683
|
+
continue;
|
|
684
|
+
const cmp = valA > valB ? 1 : -1;
|
|
685
|
+
return sort === 'asc' ? cmp : -cmp;
|
|
686
|
+
}
|
|
687
|
+
return 0;
|
|
688
|
+
});
|
|
689
|
+
}
|
|
690
|
+
export function transformDatagrid(rows, datagridState, locale, condition) {
|
|
691
|
+
const columnState = datagridState.columnState ?? [];
|
|
692
|
+
const filterModel = datagridState.filterModel ?? {};
|
|
693
|
+
const isPivotMode = !!datagridState.isPivotMode;
|
|
694
|
+
const columnFormats = datagridState.columnFormats ?? [];
|
|
695
|
+
const { rowGroupCols, pivotCols, valueCols, sortModel } = parseColumnState(columnState);
|
|
696
|
+
rows = filterRows(rows, filterModel);
|
|
697
|
+
const chartRowData = [];
|
|
698
|
+
// IMPORTANT: we evaluate the datagrid condition AFTER we are done with all transformations.
|
|
699
|
+
if (isPivotMode && pivotCols.length > 0 && valueCols.length > 0) {
|
|
700
|
+
const pivotOutput = pivotData(rows, rowGroupCols, pivotCols, valueCols);
|
|
701
|
+
rows = pivotOutput.rows;
|
|
702
|
+
rows = sortModel.length > 0 ? sortRows(rows, sortModel) : rows;
|
|
703
|
+
let mappedDisplayColumnNames = new Map();
|
|
704
|
+
// Should not need to do hidden/visible column analysis -- pivoting creates new results with only the necessary columns.
|
|
705
|
+
rows.forEach(row => {
|
|
706
|
+
let chartRow = {};
|
|
707
|
+
columnFormats.forEach(columnFormat => {
|
|
708
|
+
if (columnFormat.name in row) {
|
|
709
|
+
const value = row[columnFormat.name];
|
|
710
|
+
const formattedValue = evaluateValueFormat(columnFormat, row[columnFormat.name], locale);
|
|
711
|
+
row[columnFormat.displayName] = formattedValue;
|
|
712
|
+
chartRow[columnFormat.displayName] = formatNumberEnabled(columnFormat.valueFormat) ? value : formattedValue;
|
|
713
|
+
// Remove name in favor of display name.
|
|
714
|
+
if (columnFormat.displayName !== columnFormat.name) {
|
|
715
|
+
delete row[columnFormat.name];
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
else {
|
|
719
|
+
const mappedColumnNamesSet = pivotOutput.mappedColumnNames.get(columnFormat.name);
|
|
720
|
+
if (!!mappedColumnNamesSet) {
|
|
721
|
+
const mappedColumnNamesArr = Array.from(mappedColumnNamesSet);
|
|
722
|
+
for (const mappedColumnName of mappedColumnNamesArr) {
|
|
723
|
+
if (mappedColumnName in row) {
|
|
724
|
+
const adjDisplayName = mappedColumnName.replace(columnFormat.name, columnFormat.displayName);
|
|
725
|
+
const value = row[mappedColumnName];
|
|
726
|
+
const formattedValue = evaluateValueFormat(columnFormat, row[mappedColumnName], locale);
|
|
727
|
+
row[adjDisplayName] = formattedValue;
|
|
728
|
+
chartRow[adjDisplayName] = formatNumberEnabled(columnFormat.valueFormat) ? value : formattedValue;
|
|
729
|
+
// Remove name in favor of display name.
|
|
730
|
+
if (adjDisplayName !== mappedColumnName) {
|
|
731
|
+
delete row[mappedColumnName];
|
|
732
|
+
}
|
|
733
|
+
let displayColumnNames = mappedDisplayColumnNames.get(columnFormat.displayName);
|
|
734
|
+
if (!!displayColumnNames) {
|
|
735
|
+
displayColumnNames.add(adjDisplayName);
|
|
736
|
+
}
|
|
737
|
+
else {
|
|
738
|
+
displayColumnNames = new Set([adjDisplayName]);
|
|
739
|
+
mappedDisplayColumnNames.set(columnFormat.displayName, displayColumnNames);
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
});
|
|
746
|
+
chartRowData.push(chartRow);
|
|
747
|
+
});
|
|
748
|
+
if (!!condition) {
|
|
749
|
+
rows = evaluatePivotDatagridCondition(rows, condition, mappedDisplayColumnNames);
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
else {
|
|
753
|
+
if (rowGroupCols.length > 0) {
|
|
754
|
+
rows = groupAndAggregate(rows, rowGroupCols, [], valueCols);
|
|
755
|
+
}
|
|
756
|
+
rows = sortModel.length > 0 ? sortRows(rows, sortModel) : rows;
|
|
757
|
+
const hiddenColumnNames = columnState.filter(column => !!column.hide && !column.rowGroup).map(column => column.colId) ?? [];
|
|
758
|
+
const visibleColumnFormats = columnFormats.filter(column => !hiddenColumnNames.includes(column.name)) ?? [];
|
|
759
|
+
rows.forEach(row => {
|
|
760
|
+
let chartRow = {};
|
|
761
|
+
hiddenColumnNames.forEach(columnName => {
|
|
762
|
+
delete row[columnName];
|
|
763
|
+
});
|
|
764
|
+
visibleColumnFormats.forEach(columnFormat => {
|
|
765
|
+
const value = row[columnFormat.name];
|
|
766
|
+
const formattedValue = evaluateValueFormat(columnFormat, row[columnFormat.name], locale);
|
|
767
|
+
row[columnFormat.displayName] = formattedValue;
|
|
768
|
+
chartRow[columnFormat.displayName] = formatNumberEnabled(columnFormat.valueFormat) ? value : formattedValue;
|
|
769
|
+
// Remove name in favor of display name.
|
|
770
|
+
if (columnFormat.displayName !== columnFormat.name) {
|
|
771
|
+
delete row[columnFormat.name];
|
|
772
|
+
}
|
|
773
|
+
});
|
|
774
|
+
chartRowData.push(chartRow);
|
|
775
|
+
});
|
|
776
|
+
if (!!condition) {
|
|
777
|
+
rows = evaluateDatagridCondition(rows, condition);
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
return { ...datagridState, adjRowData: rows, chartRowData };
|
|
781
|
+
}
|
package/dist/util.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export declare function sleep(ms: number): Promise<any>;
|
|
2
|
-
export declare function
|
|
3
|
-
export declare function
|
|
2
|
+
export declare function reverse(str: string): string;
|
|
3
|
+
export declare function base64Encode(str: string): string;
|
|
4
|
+
export declare function base64Decode(str: string): string;
|
|
4
5
|
export declare function initDate(timeZone?: string): Date;
|
|
5
6
|
export declare function convertDateTimeZone(date: Date, timeZone: string): Date;
|
|
6
7
|
export declare function isDaylightSavingTime(timeZone?: string): boolean;
|
|
@@ -10,6 +11,7 @@ export declare function getCurrentDayInMonth(timeZone?: string): number;
|
|
|
10
11
|
export declare function getDaysInYear(timeZone?: string): 366 | 365;
|
|
11
12
|
export declare function getCurrentMonth(timeZone?: string): number;
|
|
12
13
|
export declare function getCurrentYear(timeZone?: string): number;
|
|
14
|
+
export declare function isDateString(value: any): boolean;
|
|
13
15
|
/**
|
|
14
16
|
* NOTE: `stopIdx` represents last character index of word
|
|
15
17
|
* ex: ABC -> `startIdx`: 0 `stopIdx`: 2
|
|
@@ -20,17 +22,11 @@ export interface QueryRegexMatch {
|
|
|
20
22
|
stopIdx: number;
|
|
21
23
|
}
|
|
22
24
|
/**
|
|
23
|
-
* Will only
|
|
25
|
+
* Will only return matches that **are not** contained in single quotes,
|
|
24
26
|
* double quotes, single line, or multiline comments.
|
|
25
27
|
*/
|
|
26
28
|
export declare function getQueryMatches(query: string, regex: RegExp): QueryRegexMatch[];
|
|
27
|
-
/**
|
|
28
|
-
* Will only **remove** matches that **are not** contained in single quotes,
|
|
29
|
-
* double quotes, single line, or multiline comments.
|
|
30
|
-
*/
|
|
31
29
|
export declare function removeQueryMatches(query: string, matches: QueryRegexMatch[]): string;
|
|
32
|
-
/**
|
|
33
|
-
* Will only **replace** matches that **are not** contained in single quotes,
|
|
34
|
-
* double quotes, single line, or multiline comments.
|
|
35
|
-
*/
|
|
36
30
|
export declare function replaceQueryMatches(query: string, matches: QueryRegexMatch[], replacement: string): string;
|
|
31
|
+
export declare function removeMarkdownIndicators(input: string): string;
|
|
32
|
+
export declare function removePrefixIndicators(input: string, prefixes: string[]): string;
|
package/dist/util.js
CHANGED
|
@@ -2,26 +2,14 @@
|
|
|
2
2
|
export async function sleep(ms) {
|
|
3
3
|
return new Promise(resolve => setTimeout(resolve, ms));
|
|
4
4
|
}
|
|
5
|
-
export function
|
|
6
|
-
|
|
7
|
-
// Make sure markdown indicator exists.
|
|
8
|
-
if (output.includes('```')) {
|
|
9
|
-
// Remove first markdown indicator.
|
|
10
|
-
output = output.substring(output.indexOf('```'));
|
|
11
|
-
output = output.substring(output.indexOf('\n'));
|
|
12
|
-
// First markdown indicator removed, now do the last.
|
|
13
|
-
output = output.substring(0, output.indexOf('```')).trim();
|
|
14
|
-
}
|
|
15
|
-
return output;
|
|
5
|
+
export function reverse(str) {
|
|
6
|
+
return str.split('').reverse().join('');
|
|
16
7
|
}
|
|
17
|
-
export function
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
return output;
|
|
8
|
+
export function base64Encode(str) {
|
|
9
|
+
return Buffer.from(str, 'utf-8').toString('base64');
|
|
10
|
+
}
|
|
11
|
+
export function base64Decode(str) {
|
|
12
|
+
return Buffer.from(str, 'base64').toString('utf-8');
|
|
25
13
|
}
|
|
26
14
|
// Dates --------------------------------------------------------------------------
|
|
27
15
|
export function initDate(timeZone) {
|
|
@@ -72,8 +60,43 @@ export function getCurrentYear(timeZone) {
|
|
|
72
60
|
let date = initDate(timeZone);
|
|
73
61
|
return date.getFullYear();
|
|
74
62
|
}
|
|
63
|
+
export function isDateString(value) {
|
|
64
|
+
if (typeof value !== 'string')
|
|
65
|
+
return false;
|
|
66
|
+
const str = value.trim();
|
|
67
|
+
/* To be considered a date string, the string:
|
|
68
|
+
- Must contain at least one digit
|
|
69
|
+
- Must include a common date separator (e.g. '-', '/', ':', '.', or 'T')
|
|
70
|
+
- Must not contain any letters other than 'T' or 'Z'
|
|
71
|
+
- Must successfully parse
|
|
72
|
+
|
|
73
|
+
NOTE: We are not worried about month/day names or AM/PM. If these exist,
|
|
74
|
+
the user has clearly elected to format the date in the query itself. */
|
|
75
|
+
if (/[^TZ0-9\s:./-]/i.test(str))
|
|
76
|
+
return false;
|
|
77
|
+
/* Handle some edge cases:
|
|
78
|
+
- If 2 separators, should be at least 6 total characters
|
|
79
|
+
- Every part separated by '-' or '/' must be 1, 2, or >=4 chars long */
|
|
80
|
+
if (str.includes('-')) {
|
|
81
|
+
const dashes = str.split('-');
|
|
82
|
+
if (dashes.length > 1 && str.length < 6) {
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
else if (str.includes('/')) {
|
|
87
|
+
const slashes = str.split('/');
|
|
88
|
+
if (slashes.length > 1 && str.length < 6) {
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
const sections = str.split(/[-/]+/).filter(Boolean);
|
|
93
|
+
if (sections.some(s => !(s.length === 1 || s.length === 2 || s.length >= 4))) {
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
return !isNaN(Date.parse(str));
|
|
97
|
+
}
|
|
75
98
|
/**
|
|
76
|
-
* Will only
|
|
99
|
+
* Will only return matches that **are not** contained in single quotes,
|
|
77
100
|
* double quotes, single line, or multiline comments.
|
|
78
101
|
*/
|
|
79
102
|
export function getQueryMatches(query, regex) {
|
|
@@ -185,10 +208,6 @@ export function getQueryMatches(query, regex) {
|
|
|
185
208
|
}
|
|
186
209
|
return matches;
|
|
187
210
|
}
|
|
188
|
-
/**
|
|
189
|
-
* Will only **remove** matches that **are not** contained in single quotes,
|
|
190
|
-
* double quotes, single line, or multiline comments.
|
|
191
|
-
*/
|
|
192
211
|
export function removeQueryMatches(query, matches) {
|
|
193
212
|
let adjQuery = query;
|
|
194
213
|
let offset = 0;
|
|
@@ -200,10 +219,6 @@ export function removeQueryMatches(query, matches) {
|
|
|
200
219
|
}
|
|
201
220
|
return adjQuery;
|
|
202
221
|
}
|
|
203
|
-
/**
|
|
204
|
-
* Will only **replace** matches that **are not** contained in single quotes,
|
|
205
|
-
* double quotes, single line, or multiline comments.
|
|
206
|
-
*/
|
|
207
222
|
export function replaceQueryMatches(query, matches, replacement) {
|
|
208
223
|
let adjQuery = query;
|
|
209
224
|
let offset = 0;
|
|
@@ -218,3 +233,25 @@ export function replaceQueryMatches(query, matches, replacement) {
|
|
|
218
233
|
}
|
|
219
234
|
return adjQuery;
|
|
220
235
|
}
|
|
236
|
+
// LLMs --------------------------------------------------------------------------
|
|
237
|
+
export function removeMarkdownIndicators(input) {
|
|
238
|
+
let output = input;
|
|
239
|
+
// Make sure markdown indicator exists.
|
|
240
|
+
if (output.includes('```')) {
|
|
241
|
+
// Remove first markdown indicator.
|
|
242
|
+
output = output.substring(output.indexOf('```'));
|
|
243
|
+
output = output.substring(output.indexOf('\n'));
|
|
244
|
+
// First markdown indicator removed, now do the last.
|
|
245
|
+
output = output.substring(0, output.indexOf('```')).trim();
|
|
246
|
+
}
|
|
247
|
+
return output;
|
|
248
|
+
}
|
|
249
|
+
export function removePrefixIndicators(input, prefixes) {
|
|
250
|
+
let output = input;
|
|
251
|
+
for (const prefix of prefixes) {
|
|
252
|
+
if (output.includes(prefix)) {
|
|
253
|
+
output = output.substring(output.indexOf(prefix) + prefix.length);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
return output;
|
|
257
|
+
}
|