@teselagen/ui 0.0.9 → 0.0.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (126) hide show
  1. package/README.md +7 -0
  2. package/cypress.config.ts +6 -0
  3. package/index.html +12 -0
  4. package/package.json +2 -2
  5. package/project.json +74 -0
  6. package/src/AdvancedOptions.js +33 -0
  7. package/src/AdvancedOptions.spec.js +24 -0
  8. package/src/AssignDefaultsModeContext.js +21 -0
  9. package/src/AsyncValidateFieldSpinner/index.js +12 -0
  10. package/src/BlueprintError/index.js +14 -0
  11. package/src/BounceLoader/index.js +16 -0
  12. package/src/BounceLoader/style.css +45 -0
  13. package/src/CollapsibleCard/index.js +92 -0
  14. package/src/CollapsibleCard/style.css +21 -0
  15. package/src/DNALoader/index.js +20 -0
  16. package/src/DNALoader/style.css +251 -0
  17. package/src/DataTable/CellDragHandle.js +130 -0
  18. package/src/DataTable/DisabledLoadingComponent.js +15 -0
  19. package/src/DataTable/DisplayOptions.js +218 -0
  20. package/src/DataTable/FilterAndSortMenu.js +397 -0
  21. package/src/DataTable/PagingTool.js +232 -0
  22. package/src/DataTable/SearchBar.js +57 -0
  23. package/src/DataTable/SortableColumns.js +53 -0
  24. package/src/DataTable/TableFormTrackerContext.js +10 -0
  25. package/src/DataTable/dataTableEnhancer.js +291 -0
  26. package/src/DataTable/defaultFormatters.js +32 -0
  27. package/src/DataTable/defaultProps.js +45 -0
  28. package/src/DataTable/defaultValidators.js +40 -0
  29. package/src/DataTable/editCellHelper.js +44 -0
  30. package/src/DataTable/getCellVal.js +20 -0
  31. package/src/DataTable/getVals.js +8 -0
  32. package/src/DataTable/index.js +3537 -0
  33. package/src/DataTable/isTruthy.js +12 -0
  34. package/src/DataTable/isValueEmpty.js +3 -0
  35. package/src/DataTable/style.css +600 -0
  36. package/src/DataTable/utils/computePresets.js +42 -0
  37. package/src/DataTable/utils/convertSchema.js +69 -0
  38. package/src/DataTable/utils/getIdOrCodeOrIndex.js +9 -0
  39. package/src/DataTable/utils/getTableConfigFromStorage.js +5 -0
  40. package/src/DataTable/utils/queryParams.js +1032 -0
  41. package/src/DataTable/utils/rowClick.js +156 -0
  42. package/src/DataTable/utils/selection.js +8 -0
  43. package/src/DataTable/utils/withSelectedEntities.js +65 -0
  44. package/src/DataTable/utils/withTableParams.js +328 -0
  45. package/src/DataTable/validateTableWideErrors.js +135 -0
  46. package/src/DataTable/viewColumn.js +37 -0
  47. package/src/DialogFooter/index.js +79 -0
  48. package/src/DialogFooter/style.css +9 -0
  49. package/src/DropdownButton.js +36 -0
  50. package/src/FillWindow.css +6 -0
  51. package/src/FillWindow.js +69 -0
  52. package/src/FormComponents/Uploader.js +1197 -0
  53. package/src/FormComponents/getNewName.js +31 -0
  54. package/src/FormComponents/index.js +1384 -0
  55. package/src/FormComponents/itemUpload.js +84 -0
  56. package/src/FormComponents/sortify.js +73 -0
  57. package/src/FormComponents/style.css +247 -0
  58. package/src/FormComponents/tryToMatchSchemas.js +222 -0
  59. package/src/FormComponents/utils.js +6 -0
  60. package/src/HotkeysDialog/index.js +79 -0
  61. package/src/HotkeysDialog/style.css +54 -0
  62. package/src/InfoHelper/index.js +83 -0
  63. package/src/InfoHelper/style.css +7 -0
  64. package/src/IntentText/index.js +18 -0
  65. package/src/Loading/index.js +74 -0
  66. package/src/Loading/style.css +4 -0
  67. package/src/MatchHeaders.js +223 -0
  68. package/src/MenuBar/index.js +416 -0
  69. package/src/MenuBar/style.css +45 -0
  70. package/src/PromptUnsavedChanges/index.js +40 -0
  71. package/src/ResizableDraggableDialog/index.js +138 -0
  72. package/src/ResizableDraggableDialog/style.css +42 -0
  73. package/src/ScrollToTop/index.js +72 -0
  74. package/src/SimpleStepViz.js +26 -0
  75. package/src/TgSelect/index.js +465 -0
  76. package/src/TgSelect/style.css +34 -0
  77. package/src/TgSuggest/index.js +121 -0
  78. package/src/Timeline/TimelineEvent.js +31 -0
  79. package/src/Timeline/index.js +22 -0
  80. package/src/Timeline/style.css +29 -0
  81. package/src/UploadCsvWizard.css +4 -0
  82. package/src/UploadCsvWizard.js +731 -0
  83. package/src/autoTooltip.js +89 -0
  84. package/src/constants.js +1 -0
  85. package/src/customIcons.js +361 -0
  86. package/src/enhancers/withDialog/index.js +196 -0
  87. package/src/enhancers/withDialog/tg_modalState.js +46 -0
  88. package/src/enhancers/withField.js +20 -0
  89. package/src/enhancers/withFields.js +11 -0
  90. package/src/enhancers/withLocalStorage.js +11 -0
  91. package/src/index.js +76 -0
  92. package/src/rerenderOnWindowResize.js +27 -0
  93. package/src/showAppSpinner.js +12 -0
  94. package/src/showConfirmationDialog/index.js +116 -0
  95. package/src/showDialogOnDocBody.js +37 -0
  96. package/src/style.css +214 -0
  97. package/src/toastr.js +92 -0
  98. package/src/typeToCommonType.js +6 -0
  99. package/src/useDialog.js +64 -0
  100. package/src/utils/S3Download.js +14 -0
  101. package/src/utils/adHoc.js +10 -0
  102. package/src/utils/basicHandleActionsWithFullState.js +14 -0
  103. package/src/utils/combineReducersWithFullState.js +14 -0
  104. package/src/utils/commandControls.js +83 -0
  105. package/src/utils/commandUtils.js +112 -0
  106. package/src/utils/determineBlackOrWhiteTextColor.js +4 -0
  107. package/src/utils/getDayjsFormatter.js +35 -0
  108. package/src/utils/getTextFromEl.js +28 -0
  109. package/src/utils/handlerHelpers.js +30 -0
  110. package/src/utils/hotkeyUtils.js +129 -0
  111. package/src/utils/menuUtils.js +402 -0
  112. package/src/utils/popoverOverflowModifiers.js +11 -0
  113. package/src/utils/pureNoFunc.js +31 -0
  114. package/src/utils/renderOnDoc.js +29 -0
  115. package/src/utils/showProgressToast.js +22 -0
  116. package/src/utils/tagUtils.js +45 -0
  117. package/src/utils/tgFormValues.js +32 -0
  118. package/src/utils/withSelectTableRecords.js +38 -0
  119. package/src/utils/withStore.js +10 -0
  120. package/src/wrapDialog.js +112 -0
  121. package/tsconfig.json +4 -0
  122. package/vite.config.ts +7 -0
  123. package/index.js +0 -80652
  124. package/index.mjs +0 -80636
  125. package/index.umd.js +0 -80649
  126. package/style.css +0 -10421
@@ -0,0 +1,1032 @@
1
+ import queryString from "qs";
2
+ import {
3
+ uniqBy,
4
+ uniq,
5
+ get,
6
+ clone,
7
+ camelCase,
8
+ startsWith,
9
+ endsWith,
10
+ last,
11
+ orderBy,
12
+ take,
13
+ drop,
14
+ isEmpty,
15
+ isInteger
16
+ } from "lodash";
17
+ import dayjs from "dayjs";
18
+ import { flatMap } from "lodash";
19
+
20
+ const defaultPageSizes = [5, 10, 15, 25, 50, 100, 200, 400];
21
+
22
+ export { defaultPageSizes };
23
+
24
+ export function getMergedOpts(topLevel = {}, instanceLevel = {}) {
25
+ const merged = {
26
+ ...topLevel,
27
+ ...instanceLevel
28
+ };
29
+ return {
30
+ formName: "tgDataTable",
31
+ ...merged,
32
+ pageSize: merged.controlled_pageSize || merged.pageSize,
33
+ defaults: {
34
+ pageSize: merged.controlled_pageSize || 25,
35
+ order: [], //[-name, statusCode] //an array of camelCase display names with - sign to denote reverse
36
+ searchTerm: "",
37
+ page: 1,
38
+ filters: [
39
+ //filters look like this:
40
+ // {
41
+ // selectedFilter: 'textContains', //camel case
42
+ // filterOn: ccDisplayName, //camel case display name
43
+ // filterValue: 'thomas',
44
+ // }
45
+ ],
46
+ ...(topLevel.defaults || {}),
47
+ ...(instanceLevel.defaults || {})
48
+ }
49
+ };
50
+ }
51
+
52
+ function safeStringify(val) {
53
+ if (val !== null && typeof val === "object") {
54
+ return JSON.stringify(val);
55
+ }
56
+ return val;
57
+ }
58
+
59
+ function safeParse(val) {
60
+ try {
61
+ return JSON.parse(val);
62
+ } catch (e) {
63
+ return val;
64
+ }
65
+ }
66
+ function getFieldsMappedByCCDisplayName(schema) {
67
+ return schema.fields.reduce((acc, field) => {
68
+ acc[camelCase(field.displayName || field.path)] = field;
69
+ return acc;
70
+ }, {});
71
+ }
72
+
73
+ function orderEntitiesLocal(orderArray, entities, schema, ownProps) {
74
+ if (orderArray && orderArray.length) {
75
+ const orderFuncs = [];
76
+ const ascOrDescArray = [];
77
+ orderArray.forEach((order) => {
78
+ const ccDisplayName = order.replace(/^-/gi, "");
79
+ const ccFields = getFieldsMappedByCCDisplayName(schema);
80
+ const field = ccFields[ccDisplayName];
81
+ if (!field) {
82
+ throw new Error(
83
+ "Ruh roh, there should have been a column to sort on for " +
84
+ order +
85
+ "but none was found in " +
86
+ schema.fields
87
+ );
88
+ }
89
+ const { path, getValueToFilterOn, sortFn } = field;
90
+ if (field.type === "timestamp") {
91
+ //with the timestamp logic below, make sure empty dates always end up on the bottom of the stack
92
+ ascOrDescArray.push("desc");
93
+ }
94
+ ascOrDescArray.push(ccDisplayName === order ? "asc" : "desc");
95
+ //push the actual sorting function
96
+ if (field.type === "timestamp") {
97
+ //with the timestamp logic above, make sure empty dates always end up on the bottom of the stack
98
+ orderFuncs.push((r) => {
99
+ const val = get(r, path);
100
+ return !!val;
101
+ });
102
+ }
103
+ if (path && endsWith(path.toLowerCase(), "id")) {
104
+ orderFuncs.push((o) => {
105
+ return parseInt(get(o, path), 10);
106
+ });
107
+ } else if (sortFn) {
108
+ const toOrder = Array.isArray(sortFn) ? sortFn : [sortFn];
109
+ orderFuncs.push(...toOrder);
110
+ } else if (getValueToFilterOn) {
111
+ orderFuncs.push((o) => {
112
+ return getValueToFilterOn(o, ownProps);
113
+ });
114
+ } else {
115
+ orderFuncs.push((r) => {
116
+ const val = get(r, path);
117
+ return val && val.toLowerCase ? val.toLowerCase() : val;
118
+ });
119
+ }
120
+ });
121
+ entities = orderBy(entities, orderFuncs, ascOrDescArray);
122
+ }
123
+ return entities;
124
+ }
125
+
126
+ function getAndAndOrFilters(allFilters) {
127
+ const orFilters = [];
128
+ const andFilters = [];
129
+ const otherOrFilters = [];
130
+
131
+ allFilters.forEach((filter) => {
132
+ if (
133
+ filter.isOrFilter &&
134
+ typeof filter.filterValue === "string" &&
135
+ filter.filterValue.includes(",")
136
+ ) {
137
+ // handle comma separated filters by adding more orWheres
138
+ const allFilterValues = filter.filterValue.split(",");
139
+ allFilterValues.forEach((filterValue, i) => {
140
+ filterValue = filterValue.trim();
141
+ if (!filterValue) return;
142
+ const newFilter = {
143
+ ...filter,
144
+ filterValue
145
+ };
146
+ if (i === 0) {
147
+ orFilters.push(newFilter);
148
+ } else {
149
+ const iMinus = i - 1;
150
+ if (!otherOrFilters[iMinus]) otherOrFilters[iMinus] = [];
151
+ otherOrFilters[iMinus].push(newFilter);
152
+ }
153
+ });
154
+ } else if (filter.isOrFilter) {
155
+ orFilters.push(filter);
156
+ } else {
157
+ andFilters.push(filter);
158
+ }
159
+ });
160
+ return {
161
+ orFilters,
162
+ andFilters,
163
+ otherOrFilters
164
+ };
165
+ }
166
+
167
+ function filterEntitiesLocal(
168
+ filters = [],
169
+ searchTerm,
170
+ entities,
171
+ schema,
172
+ ownProps
173
+ ) {
174
+ const allFilters = getAllFilters(filters, searchTerm, schema);
175
+
176
+ if (allFilters.length) {
177
+ const ccFields = getFieldsMappedByCCDisplayName(schema);
178
+ const { andFilters, orFilters, otherOrFilters } =
179
+ getAndAndOrFilters(allFilters);
180
+ //filter ands first
181
+ andFilters.forEach((filter) => {
182
+ entities = getEntitiesForGivenFilter(
183
+ entities,
184
+ filter,
185
+ ccFields,
186
+ ownProps
187
+ );
188
+ });
189
+ //then filter ors
190
+ if (orFilters.length) {
191
+ let orEntities = [];
192
+ orFilters.concat(...otherOrFilters).forEach((filter) => {
193
+ orEntities = orEntities.concat(
194
+ getEntitiesForGivenFilter(entities, filter, ccFields, ownProps)
195
+ );
196
+ });
197
+ entities = uniq(orEntities);
198
+ }
199
+ }
200
+ return entities;
201
+ }
202
+
203
+ function cleanFilterValue(_filterValue, type) {
204
+ let filterValue = _filterValue;
205
+ if (type === "number" || type === "integer") {
206
+ filterValue = Array.isArray(filterValue)
207
+ ? filterValue.map((val) => Number(val))
208
+ : Number(filterValue);
209
+ }
210
+ return filterValue;
211
+ }
212
+
213
+ function getEntitiesForGivenFilter(entities, filter, ccFields, ownProps) {
214
+ const { filterOn, filterValue: _filterValue, selectedFilter } = filter;
215
+ const field = ccFields[filterOn];
216
+ const { path, getValueToFilterOn } = field;
217
+ const filterValue = cleanFilterValue(_filterValue, field.type);
218
+ const subFilter = getSubFilter(false, selectedFilter, filterValue);
219
+ entities = entities.filter((entity) => {
220
+ const fieldVal = getValueToFilterOn
221
+ ? getValueToFilterOn(entity, ownProps)
222
+ : get(entity, path);
223
+ const shouldKeep = subFilter(fieldVal);
224
+ return shouldKeep;
225
+ });
226
+ return entities;
227
+ }
228
+
229
+ function getFiltersFromSearchTerm(searchTerm, schema) {
230
+ const searchTermFilters = [];
231
+ if (searchTerm) {
232
+ const sharedFields = {
233
+ isOrFilter: true,
234
+ isSearchTermFilter: true
235
+ };
236
+ schema.fields.forEach((field) => {
237
+ const { type, displayName, path, searchDisabled } = field;
238
+ if (searchDisabled || field.filterDisabled || type === "color") return;
239
+ const nameToUse = camelCase(displayName || path);
240
+ const filterValue = cleanFilterValue(searchTerm, type);
241
+ if (type === "string" || type === "lookup") {
242
+ searchTermFilters.push({
243
+ ...sharedFields,
244
+ filterOn: nameToUse,
245
+ filterValue: searchTerm,
246
+ selectedFilter: "contains"
247
+ });
248
+ } else if (type === "boolean") {
249
+ let regex;
250
+ try {
251
+ regex = new RegExp("^" + searchTerm, "ig");
252
+ } catch (error) {
253
+ //ignore
254
+ }
255
+ if (regex) {
256
+ if ("true".replace(regex, "") !== "true") {
257
+ searchTermFilters.push({
258
+ ...sharedFields,
259
+ filterOn: nameToUse,
260
+ filterValue: true,
261
+ selectedFilter: "true"
262
+ });
263
+ } else if ("false".replace(regex, "") !== "false") {
264
+ searchTermFilters.push({
265
+ ...sharedFields,
266
+ filterOn: nameToUse,
267
+ filterValue: false,
268
+ selectedFilter: "false"
269
+ });
270
+ }
271
+ }
272
+ } else if (
273
+ (type === "number" || type === "integer") &&
274
+ !isNaN(filterValue)
275
+ ) {
276
+ if (type === "integer" && !isInteger(filterValue)) {
277
+ return;
278
+ }
279
+ searchTermFilters.push({
280
+ ...sharedFields,
281
+ filterOn: nameToUse,
282
+ filterValue: filterValue,
283
+ selectedFilter: "equalTo"
284
+ });
285
+ }
286
+ });
287
+ }
288
+ return searchTermFilters;
289
+ }
290
+
291
+ function getSubFilter(
292
+ qb, //if no qb is passed, it means we are filtering locally and want to get a function back that can be used in an array filter
293
+ selectedFilter,
294
+ filterValue
295
+ ) {
296
+ const ccSelectedFilter = camelCase(selectedFilter);
297
+ let stringFilterValue =
298
+ filterValue && filterValue.toString ? filterValue.toString() : filterValue;
299
+ if (stringFilterValue === false) {
300
+ // we still want to be able to search for the string "false" which will get parsed to false
301
+ stringFilterValue = "false";
302
+ } else {
303
+ stringFilterValue = stringFilterValue || "";
304
+ }
305
+ const filterValLower =
306
+ stringFilterValue.toLowerCase && stringFilterValue.toLowerCase();
307
+ const arrayFilterValue = Array.isArray(filterValue)
308
+ ? filterValue
309
+ : stringFilterValue.split(".");
310
+ if (ccSelectedFilter === "startsWith") {
311
+ return qb
312
+ ? qb.startsWith(stringFilterValue) //filter using qb (aka we're backend connected)
313
+ : (fieldVal) => {
314
+ //filter using plain old javascript (aka we've got a local table that isn't backend connected)
315
+ if (!fieldVal || !fieldVal.toLowerCase) return false;
316
+ return startsWith(fieldVal.toLowerCase(), filterValLower);
317
+ };
318
+ } else if (ccSelectedFilter === "endsWith") {
319
+ return qb
320
+ ? qb.endsWith(stringFilterValue) //filter using qb (aka we're backend connected)
321
+ : (fieldVal) => {
322
+ //filter using plain old javascript (aka we've got a local table that isn't backend connected)
323
+ if (!fieldVal || !fieldVal.toLowerCase) return false;
324
+ return endsWith(fieldVal.toLowerCase(), filterValLower);
325
+ };
326
+ } else if (
327
+ ccSelectedFilter === "contains" ||
328
+ ccSelectedFilter === "notContains"
329
+ ) {
330
+ return qb
331
+ ? ccSelectedFilter === "contains"
332
+ ? qb.contains(stringFilterValue.replace(/_/g, "\\_"))
333
+ : qb.notContains(stringFilterValue.replace(/_/g, "\\_"))
334
+ : (fieldVal) => {
335
+ if (!fieldVal || !fieldVal.toLowerCase) return false;
336
+ return ccSelectedFilter === "contains"
337
+ ? fieldVal.toLowerCase().replace(filterValLower, "") !==
338
+ fieldVal.toLowerCase()
339
+ : fieldVal.toLowerCase().replace(filterValLower, "") ===
340
+ fieldVal.toLowerCase();
341
+ };
342
+ } else if (ccSelectedFilter === "inList") {
343
+ return qb
344
+ ? qb.inList(arrayFilterValue) //filter using qb (aka we're backend connected)
345
+ : (fieldVal) => {
346
+ //filter using plain old javascript (aka we've got a local table that isn't backend connected)
347
+ if (!fieldVal?.toString) return false;
348
+ return (
349
+ arrayFilterValue
350
+ .map((val) => val && val.toLowerCase())
351
+ .indexOf(fieldVal.toString().toLowerCase()) > -1
352
+ );
353
+ };
354
+ } else if (ccSelectedFilter === "notInList") {
355
+ return qb
356
+ ? qb.notInList(arrayFilterValue) //filter using qb (aka we're backend connected)
357
+ : (fieldVal) => {
358
+ //filter using plain old javascript (aka we've got a local table that isn't backend connected)
359
+ if (!fieldVal?.toString) return false;
360
+ return (
361
+ arrayFilterValue
362
+ .map((val) => val && val.toLowerCase())
363
+ .indexOf(fieldVal.toString().toLowerCase()) === -1
364
+ );
365
+ };
366
+ } else if (ccSelectedFilter === "isEmpty") {
367
+ return qb
368
+ ? qb.isEmpty()
369
+ : (fieldVal) => {
370
+ return !fieldVal;
371
+ };
372
+ } else if (ccSelectedFilter === "notEmpty") {
373
+ return qb
374
+ ? [qb.notNull(), qb.notEquals("")]
375
+ : (fieldVal) => {
376
+ return !!fieldVal;
377
+ };
378
+ } else if (ccSelectedFilter === "isExactly") {
379
+ return qb
380
+ ? filterValue
381
+ : (fieldVal) => {
382
+ return fieldVal === filterValue;
383
+ };
384
+ } else if (ccSelectedFilter === "true") {
385
+ return qb
386
+ ? qb.equals(true) //filter using qb (aka we're backend connected)
387
+ : (fieldVal) => {
388
+ //filter using plain old javascript (aka we've got a local table that isn't backend connected)
389
+ return !!fieldVal;
390
+ };
391
+ } else if (ccSelectedFilter === "false") {
392
+ return qb
393
+ ? qb.equals(false) //filter using qb (aka we're backend connected)
394
+ : (fieldVal) => {
395
+ //filter using plain old javascript (aka we've got a local table that isn't backend connected)
396
+ return !fieldVal;
397
+ };
398
+ } else if (ccSelectedFilter === "isBetween") {
399
+ return qb
400
+ ? qb.between(
401
+ new Date(arrayFilterValue[0]),
402
+ new Date(new Date(arrayFilterValue[1]).setHours(23, 59)) // set end of day for more accurate filtering
403
+ )
404
+ : (fieldVal) => {
405
+ return (
406
+ dayjs(arrayFilterValue[0]).valueOf() <= dayjs(fieldVal).valueOf() &&
407
+ dayjs(fieldVal).valueOf() <= dayjs(arrayFilterValue[1]).valueOf()
408
+ );
409
+ };
410
+ } else if (ccSelectedFilter === "notBetween") {
411
+ return qb
412
+ ? qb.notBetween(
413
+ new Date(arrayFilterValue[0]),
414
+ new Date(new Date(arrayFilterValue[1]).setHours(23, 59)) // set end of day for more accurate filtering
415
+ )
416
+ : (fieldVal) => {
417
+ return (
418
+ dayjs(arrayFilterValue[0]).valueOf() > dayjs(fieldVal).valueOf() ||
419
+ dayjs(fieldVal).valueOf() > dayjs(arrayFilterValue[1]).valueOf()
420
+ );
421
+ };
422
+ } else if (ccSelectedFilter === "isBefore") {
423
+ return qb
424
+ ? qb.lessThan(new Date(filterValue))
425
+ : (fieldVal) => {
426
+ return dayjs(fieldVal).valueOf() < dayjs(filterValue).valueOf();
427
+ };
428
+ } else if (ccSelectedFilter === "isAfter") {
429
+ return qb
430
+ ? qb.greaterThan(new Date(new Date(filterValue).setHours(23, 59))) // set end of day for more accurate filtering
431
+ : (fieldVal) => {
432
+ return dayjs(fieldVal).valueOf() > dayjs(filterValue).valueOf();
433
+ };
434
+ } else if (ccSelectedFilter === "greaterThan") {
435
+ return qb
436
+ ? qb.greaterThan(filterValue)
437
+ : (fieldVal) => {
438
+ return fieldVal > filterValue;
439
+ };
440
+ } else if (ccSelectedFilter === "lessThan") {
441
+ return qb
442
+ ? qb.lessThan(filterValue)
443
+ : (fieldVal) => {
444
+ return fieldVal < filterValue;
445
+ };
446
+ } else if (ccSelectedFilter === "inRange") {
447
+ return qb
448
+ ? qb.between(filterValue[0], filterValue[1])
449
+ : (fieldVal) => {
450
+ return filterValue[0] <= fieldVal && fieldVal <= filterValue[1];
451
+ };
452
+ } else if (ccSelectedFilter === "outsideRange") {
453
+ return qb
454
+ ? qb.notBetween(filterValue[0], filterValue[1])
455
+ : (fieldVal) => {
456
+ return filterValue[0] > fieldVal || fieldVal > filterValue[1];
457
+ };
458
+ } else if (ccSelectedFilter === "equalTo") {
459
+ return qb
460
+ ? filterValue
461
+ : (fieldVal) => {
462
+ return fieldVal === filterValue;
463
+ };
464
+ } else if (ccSelectedFilter === "regex") {
465
+ return qb
466
+ ? qb.matchesRegex(filterValue)
467
+ : (fieldVal) => {
468
+ new RegExp(filterValue).test(fieldVal);
469
+ return fieldVal;
470
+ };
471
+ }
472
+
473
+ throw new Error(
474
+ `Unsupported filter ${selectedFilter}. Please make a new filter if you need one`
475
+ );
476
+ }
477
+
478
+ export function getCurrentParamsFromUrl(location, isSimple) {
479
+ let { search } = location;
480
+ if (isSimple) {
481
+ search = window.location.href.split("?")[1] || "";
482
+ }
483
+
484
+ return parseFilters(queryString.parse(search, { ignoreQueryPrefix: true }));
485
+ }
486
+ export function setCurrentParamsOnUrl(newParams, replace, isSimple) {
487
+ const stringifiedFilters = stringifyFilters(newParams);
488
+ const search = `?${queryString.stringify(stringifiedFilters)}`;
489
+ if (isSimple) {
490
+ return window.location.replace(
491
+ `${window.location.href.split("?")[0]}${search}`
492
+ );
493
+ }
494
+ replace({
495
+ search
496
+ });
497
+ }
498
+
499
+ function stringifyFilters(newParams) {
500
+ let filters;
501
+ if (newParams.filters && newParams.filters.length) {
502
+ filters = newParams.filters.reduce(
503
+ (acc, { filterOn, selectedFilter, filterValue }, index) => {
504
+ acc +=
505
+ (index > 0 ? "::" : "") +
506
+ `${filterOn}__${camelCase(selectedFilter)}__${safeStringify(
507
+ Array.isArray(filterValue) ? filterValue.join(";") : filterValue
508
+ )}`;
509
+ return acc;
510
+ },
511
+ ""
512
+ );
513
+ }
514
+ let order;
515
+ if (newParams.order && newParams.order.length) {
516
+ order = newParams.order.reduce((acc, order, index) => {
517
+ acc += (index > 0 ? "___" : "") + order;
518
+ return acc;
519
+ }, "");
520
+ }
521
+ return {
522
+ ...newParams,
523
+ filters,
524
+ order
525
+ };
526
+ }
527
+ function parseFilters(newParams) {
528
+ return {
529
+ ...newParams,
530
+ order: newParams.order && newParams.order.split("___"),
531
+ filters:
532
+ newParams.filters &&
533
+ newParams.filters.split("::").map((filter) => {
534
+ const splitFilter = filter.split("__");
535
+ const [filterOn, selectedFilter, filterValue] = splitFilter;
536
+ const parseFilterValue = (filterValue) => {
537
+ if (selectedFilter === "inList" || selectedFilter === "notInList") {
538
+ return filterValue.split(";");
539
+ }
540
+ if (
541
+ selectedFilter === "inRange" ||
542
+ selectedFilter === "outsideRange"
543
+ ) {
544
+ return filterValue.split(";").map(Number);
545
+ }
546
+ return safeParse(filterValue);
547
+ };
548
+ return {
549
+ filterOn,
550
+ selectedFilter,
551
+ filterValue: parseFilterValue(filterValue)
552
+ };
553
+ })
554
+ };
555
+ }
556
+
557
+ function buildRef(qb, reference, searchField, expression) {
558
+ if (reference.reference) {
559
+ // qb[reference.target] = {}
560
+ return qb.related(reference.target).whereAny({
561
+ [reference.sourceField]: buildRef(
562
+ qb,
563
+ reference.reference,
564
+ searchField,
565
+ expression
566
+ )
567
+ });
568
+ }
569
+ return qb.related(reference.target).whereAny({
570
+ [searchField]: expression
571
+ });
572
+ }
573
+
574
+ export function makeDataTableHandlers({
575
+ setNewParams,
576
+ updateSearch,
577
+ defaults,
578
+ onlyOneFilter
579
+ }) {
580
+ //all of these actions have currentParams bound to them as their last arg in withTableParams
581
+ function setSearchTerm(searchTerm, currentParams) {
582
+ const newParams = {
583
+ ...currentParams,
584
+ page: undefined, //set page undefined to return the table to page 1
585
+ searchTerm: searchTerm === defaults.searchTerm ? undefined : searchTerm
586
+ };
587
+ setNewParams(newParams);
588
+ updateSearch(searchTerm);
589
+ onlyOneFilter && clearFilters();
590
+ }
591
+ function addFilters(newFilters, currentParams) {
592
+ if (!newFilters) return;
593
+ const filters = uniqBy(
594
+ [...newFilters, ...(onlyOneFilter ? [] : currentParams.filters || [])],
595
+ "filterOn"
596
+ );
597
+
598
+ const newParams = {
599
+ ...currentParams,
600
+ page: undefined, //set page undefined to return the table to page 1
601
+ filters
602
+ };
603
+ setNewParams(newParams);
604
+ onlyOneFilter && updateSearch();
605
+ }
606
+ function removeSingleFilter(filterOn, currentParams) {
607
+ const filters = currentParams.filters
608
+ ? currentParams.filters.filter((filter) => {
609
+ return filter.filterOn !== filterOn;
610
+ })
611
+ : undefined;
612
+ const newParams = {
613
+ ...currentParams,
614
+ filters
615
+ };
616
+ setNewParams(newParams);
617
+ }
618
+ function clearFilters(additionalFilterKeys = []) {
619
+ const toClear = {
620
+ filters: undefined,
621
+ searchTerm: undefined,
622
+ tags: undefined
623
+ };
624
+ additionalFilterKeys.forEach((key) => {
625
+ toClear[key] = undefined;
626
+ });
627
+ setNewParams(toClear);
628
+ updateSearch();
629
+ }
630
+ function setPageSize(pageSize, currentParams) {
631
+ const newParams = {
632
+ ...currentParams,
633
+ pageSize: pageSize === defaults.pageSize ? undefined : pageSize,
634
+ page: undefined //set page undefined to return the table to page 1
635
+ };
636
+ setNewParams(newParams);
637
+ }
638
+ function setOrder(order, isRemove, shiftHeld, currentParams) {
639
+ let newOrder = [];
640
+ if (shiftHeld) {
641
+ //first remove the old order
642
+ newOrder = [...(currentParams.order || [])].filter((value) => {
643
+ const shouldRemove =
644
+ value.replace(/^-/, "") === order.replace(/^-/, "");
645
+ return !shouldRemove;
646
+ });
647
+ //then, if we are adding, pop the order onto the array
648
+ if (!isRemove) {
649
+ newOrder.push(order);
650
+ }
651
+ } else {
652
+ if (isRemove) {
653
+ newOrder = [];
654
+ } else {
655
+ newOrder = [order];
656
+ }
657
+ }
658
+ const newParams = {
659
+ ...currentParams,
660
+ order: newOrder
661
+ };
662
+ setNewParams(newParams);
663
+ }
664
+ function setPage(page, currentParams) {
665
+ const newParams = {
666
+ ...currentParams,
667
+ page: page === defaults.page ? undefined : page
668
+ };
669
+ setNewParams(newParams);
670
+ }
671
+ return {
672
+ setSearchTerm,
673
+ addFilters,
674
+ clearFilters,
675
+ removeSingleFilter,
676
+ setPageSize,
677
+ setPage,
678
+ setOrder,
679
+ setNewParams
680
+ };
681
+ }
682
+
683
+ // if an inList value only has two items like
684
+ // 2.3 then it will get parsed to a number and
685
+ // break, convert it back to a string here
686
+ function cleanupFilter(filter) {
687
+ let filterToUse = filter;
688
+ if (
689
+ filterToUse.selectedFilter === "inList" &&
690
+ typeof filterToUse.filterValue === "number"
691
+ ) {
692
+ filterToUse = {
693
+ ...filterToUse,
694
+ filterValue: filterToUse.filterValue.toString()
695
+ };
696
+ }
697
+ return filterToUse;
698
+ }
699
+
700
+ function getAllFilters(filters, searchTerm, schema) {
701
+ let allFilters = [
702
+ ...filters,
703
+ ...getFiltersFromSearchTerm(searchTerm, schema)
704
+ ];
705
+
706
+ allFilters = allFilters.filter((val) => {
707
+ return val !== "";
708
+ }); //get rid of erroneous filters
709
+
710
+ return allFilters.map(cleanupFilter);
711
+ }
712
+
713
+ export function getQueryParams({
714
+ currentParams,
715
+ urlConnected,
716
+ defaults,
717
+ schema,
718
+ isInfinite,
719
+ entities,
720
+ isLocalCall,
721
+ additionalFilter,
722
+ additionalOrFilter,
723
+ doNotCoercePageSize,
724
+ noOrderError,
725
+ isCodeModel,
726
+ ownProps
727
+ }) {
728
+ Object.keys(currentParams).forEach(function (key) {
729
+ if (currentParams[key] === undefined) {
730
+ delete currentParams[key]; //we want to use the default value if any of these are undefined
731
+ }
732
+ });
733
+ const tableQueryParams = {
734
+ ...defaults,
735
+ ...currentParams
736
+ };
737
+ let { page, pageSize, searchTerm, filters, order } = tableQueryParams;
738
+ if (page <= 0 || isNaN(page)) {
739
+ page = undefined;
740
+ }
741
+ if (isInfinite) {
742
+ page = undefined;
743
+ pageSize = undefined;
744
+ }
745
+ if (pageSize !== undefined && !doNotCoercePageSize) {
746
+ //pageSize might come in as an unexpected number so we coerce it to be one of the nums in our pageSizes array
747
+ const closest = clone(window.tgPageSizes || defaultPageSizes).sort(
748
+ (a, b) => Math.abs(pageSize - a) - Math.abs(pageSize - b)
749
+ )[0];
750
+ pageSize = closest;
751
+ }
752
+ const toReturn = {
753
+ //these are values that might be generally useful for the wrapped component
754
+ page,
755
+ pageSize: ownProps.controlled_pageSize || pageSize,
756
+ order,
757
+ filters,
758
+ searchTerm
759
+ };
760
+
761
+ if (isLocalCall) {
762
+ let newEntities = entities;
763
+ //if the table is local (aka not directly connected to a db) then we need to
764
+ //handle filtering/paging/sorting all on the front end
765
+ newEntities = filterEntitiesLocal(
766
+ filters,
767
+ searchTerm,
768
+ newEntities,
769
+ schema,
770
+ ownProps
771
+ );
772
+ newEntities = orderEntitiesLocal(order, newEntities, schema, ownProps);
773
+
774
+ const entitiesAcrossPages = newEntities;
775
+
776
+ const newEntityCount = newEntities.length;
777
+ //calculate the sorted, filtered, paged entities for the local table
778
+ if (!isInfinite && !ownProps.controlled_pageSize) {
779
+ const offset = (page - 1) * pageSize;
780
+ newEntities = take(drop(newEntities, offset), pageSize);
781
+ }
782
+ toReturn.entities = newEntities;
783
+ toReturn.entitiesAcrossPages = entitiesAcrossPages;
784
+ toReturn.entityCount = newEntityCount;
785
+ //if this call is being made by a local-data only connected datatable component,
786
+ //we don't want to do the following gql stuff
787
+ return toReturn;
788
+ } else {
789
+ const graphqlQueryParams = {
790
+ // need to make sure sort exists because of https://github.com/apollographql/apollo-client/issues/3077
791
+ sort: []
792
+ };
793
+ if (isInfinite) {
794
+ graphqlQueryParams.pageSize = 999;
795
+ graphqlQueryParams.pageNumber = 1;
796
+ } else {
797
+ graphqlQueryParams.pageNumber = Number(page);
798
+ graphqlQueryParams.pageSize =
799
+ ownProps.controlled_pageSize || Number(pageSize);
800
+ }
801
+
802
+ const { model } = schema;
803
+ if (!window.QueryBuilder) return toReturn;
804
+ const qb = new window.QueryBuilder(model);
805
+ // qb = qb.filter('user')
806
+ // qb = qb.whereAny({
807
+ // userStatus: qb.related('userStatus').whereAny({
808
+ // code: qb.contains('pending')
809
+ // })
810
+ // })
811
+ // qb = qb.andWhere({
812
+ // age: qb.lessThan(12)
813
+ // })
814
+ // qb.toJSON()
815
+ // let filterBuilder = qb.filter(model); //start filter on model
816
+
817
+ const ccFields = getFieldsMappedByCCDisplayName(schema);
818
+
819
+ if (tableQueryParams.order && tableQueryParams.order.length) {
820
+ tableQueryParams.order.forEach((orderVal) => {
821
+ const ccDisplayName = orderVal.replace(/^-/gi, "");
822
+ const schemaForField = ccFields[ccDisplayName];
823
+ if (schemaForField) {
824
+ const { path } = schemaForField;
825
+ const reversed = ccDisplayName !== orderVal;
826
+ const prefix = reversed ? "-" : "";
827
+ graphqlQueryParams.sort = [
828
+ ...(graphqlQueryParams.sort || []),
829
+ prefix + path
830
+ ];
831
+ } else {
832
+ !noOrderError &&
833
+ console.error(
834
+ "No schema for field found!",
835
+ ccDisplayName,
836
+ schema.fields
837
+ );
838
+ }
839
+ });
840
+ }
841
+
842
+ let errorParsingUrlString;
843
+
844
+ const additionalFilterToUse = additionalFilter(qb, currentParams);
845
+ let additionalOrFilterToUse = additionalOrFilter(qb, currentParams);
846
+ if (additionalOrFilterToUse && additionalOrFilterToUse.ignoreSearchTerm) {
847
+ searchTerm = "";
848
+ additionalOrFilterToUse = additionalOrFilterToUse.additionalOrFilterToUse;
849
+ }
850
+
851
+ const allFilters = getAllFilters(filters, searchTerm, schema);
852
+ const { andFilters, orFilters, otherOrFilters } =
853
+ getAndAndOrFilters(allFilters);
854
+ try {
855
+ const flattenFilters = (filterObj) => {
856
+ return flatMap(Object.keys(filterObj), (key) => {
857
+ return filterObj[key].map((filter) => ({
858
+ [key]: filter
859
+ }));
860
+ });
861
+ };
862
+
863
+ const orFiltersObject = getQueries(orFilters, qb, ccFields);
864
+ let allOrFilters = flattenFilters(orFiltersObject);
865
+
866
+ otherOrFilters.forEach((orFilters) => {
867
+ const otherOrFiltersObject = getQueries(orFilters, qb, ccFields);
868
+ allOrFilters = allOrFilters.concat(
869
+ flattenFilters(otherOrFiltersObject)
870
+ );
871
+ });
872
+ allOrFilters.push(additionalOrFilterToUse);
873
+ allOrFilters = allOrFilters.filter((obj) => !isEmpty(obj));
874
+
875
+ const unflattenedAndQueries = getQueries(andFilters, qb, ccFields);
876
+ let allAndFilters = flattenFilters(unflattenedAndQueries);
877
+ allAndFilters.push(additionalFilterToUse);
878
+ allAndFilters = allAndFilters.filter((obj) => !isEmpty(obj));
879
+ if (allAndFilters.length) {
880
+ qb.whereAll(...allAndFilters);
881
+ }
882
+ if (allOrFilters.length) {
883
+ qb.andWhereAny(...allOrFilters);
884
+ }
885
+ const columnCustomFilters = getColumnCustomFilters(
886
+ andFilters,
887
+ qb,
888
+ ccFields
889
+ );
890
+ if (columnCustomFilters.length) {
891
+ qb.whereAll(...columnCustomFilters);
892
+ }
893
+ } catch (e) {
894
+ if (urlConnected) {
895
+ errorParsingUrlString = e;
896
+ console.error(
897
+ "The following error occurred when trying to build the query params. This is probably due to a malformed URL:",
898
+ e
899
+ );
900
+ } else {
901
+ console.error("Error building query params from filter:");
902
+ throw e;
903
+ }
904
+ }
905
+
906
+ if (qb.query.filters.length) {
907
+ graphqlQueryParams.filter = qb.toJSON();
908
+ }
909
+
910
+ // by default make sort by updated at
911
+ if (!graphqlQueryParams.sort.length) {
912
+ graphqlQueryParams.sort.push("-updatedAt");
913
+ }
914
+
915
+ // in case entries that have the same value in the column being sorted on
916
+ // fall back to id as a secondary sort to make sure ordering happens correctly
917
+ graphqlQueryParams.sort.push(
918
+ isCodeModel ? "code" : window.__sortId || "id"
919
+ );
920
+
921
+ return {
922
+ ...toReturn,
923
+ //the query params will get passed directly to as variables to the graphql query
924
+ variables: graphqlQueryParams,
925
+ errorParsingUrlString
926
+ };
927
+ }
928
+ }
929
+
930
+ function getSubFiltersAndPath(filter, qb, ccFields) {
931
+ const { selectedFilter, filterValue, filterOn } = filter;
932
+ const fieldSchema = ccFields[filterOn];
933
+ let filterValueToUse = filterValue;
934
+
935
+ if (fieldSchema) {
936
+ if (fieldSchema.normalizeFilter) {
937
+ filterValueToUse = fieldSchema.normalizeFilter(
938
+ filterValue,
939
+ selectedFilter,
940
+ filterOn
941
+ );
942
+ }
943
+ }
944
+ const _subFilters = getSubFilter(qb, selectedFilter, filterValueToUse);
945
+
946
+ let filterField;
947
+ if (fieldSchema) {
948
+ const { path, reference } = fieldSchema;
949
+ if (reference) {
950
+ filterField = reference.sourceField;
951
+ } else {
952
+ filterField = path;
953
+ }
954
+ } else if (filterOn === "id") {
955
+ filterField = filterOn;
956
+ } else {
957
+ console.error("Trying to filter on unknown field");
958
+ }
959
+ const subFiltersToUse = [];
960
+ const subFilters = Array.isArray(_subFilters) ? _subFilters : [_subFilters];
961
+ subFilters.forEach((subFilter) => {
962
+ let subFilterToUse = subFilter;
963
+ if (fieldSchema) {
964
+ const { path, reference } = fieldSchema;
965
+ if (reference) {
966
+ subFilterToUse = buildRef(
967
+ qb,
968
+ reference,
969
+ last(path.split(".")),
970
+ subFilter
971
+ );
972
+ }
973
+ }
974
+ subFiltersToUse.push(subFilterToUse);
975
+ });
976
+
977
+ return {
978
+ path: filterField,
979
+ subFilters: subFiltersToUse
980
+ };
981
+ }
982
+
983
+ function getQueries(filters, qb, ccFields) {
984
+ const subQueries = filters.reduce((acc, filter) => {
985
+ if (!filter) {
986
+ console.warn("We should always have a filter object!");
987
+ return acc;
988
+ }
989
+ const { filterOn } = filter;
990
+ const fieldSchema = ccFields[filterOn];
991
+ // will be handled below
992
+ if (!filter.isSearchTermFilter && fieldSchema?.additionalColumnFilter)
993
+ return acc;
994
+ const { path, subFilters } = getSubFiltersAndPath(filter, qb, ccFields);
995
+ acc[path] = subFilters;
996
+ return acc;
997
+ }, {});
998
+ return subQueries;
999
+ }
1000
+
1001
+ function getColumnCustomFilters(filters, qb, ccFields) {
1002
+ const subQueries = filters.reduce((acc, filter) => {
1003
+ if (!filter) {
1004
+ console.warn("We should always have a filter object!");
1005
+ return acc;
1006
+ }
1007
+ const { filterOn } = filter;
1008
+ const fieldSchema = ccFields[filterOn];
1009
+ if (filter.isSearchTermFilter || !fieldSchema?.additionalColumnFilter) {
1010
+ return acc;
1011
+ }
1012
+ const { path, subFilters } = getSubFiltersAndPath(filter, qb, ccFields);
1013
+ /* the column filters need to have access to this sub filter but also be able to add additional
1014
+ filter logic.
1015
+ ex.
1016
+ qb.whereAny({
1017
+ id: qb.related("extendedStringValueView.buildSampleId").whereAll({
1018
+ value: "something",
1019
+ extendedPropertyId: "myId"
1020
+ })
1021
+ ...
1022
+ })
1023
+
1024
+ is possible because the returned accumulator will be passed to whereAny
1025
+ */
1026
+ subFilters.forEach((subFilter) => {
1027
+ acc.push(fieldSchema.additionalColumnFilter(qb, subFilter, path));
1028
+ });
1029
+ return acc;
1030
+ }, []);
1031
+ return subQueries;
1032
+ }