@teselagen/ui 0.0.11 → 0.0.13

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 (125) 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 +5 -11
  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.mjs +0 -109378
  124. package/index.umd.js +0 -109381
  125. package/style.css +0 -10421
@@ -0,0 +1,3537 @@
1
+ /* eslint react/jsx-no-bind: 0 */
2
+ import React, { useState } from "react";
3
+ import ReactDOM from "react-dom";
4
+ import { arrayMove } from "react-sortable-hoc";
5
+ import copy from "copy-to-clipboard";
6
+ import {
7
+ invert,
8
+ toNumber,
9
+ isEmpty,
10
+ min,
11
+ max,
12
+ flatMap,
13
+ set,
14
+ map,
15
+ toString,
16
+ camelCase,
17
+ startCase,
18
+ noop,
19
+ isEqual,
20
+ cloneDeep,
21
+ keyBy,
22
+ omit,
23
+ forEach,
24
+ lowerCase,
25
+ get,
26
+ padStart,
27
+ omitBy,
28
+ times
29
+ } from "lodash";
30
+ import joinUrl from "url-join";
31
+
32
+ import {
33
+ Button,
34
+ Menu,
35
+ MenuItem,
36
+ Classes,
37
+ ContextMenu,
38
+ Checkbox,
39
+ Icon,
40
+ Popover,
41
+ Intent,
42
+ Callout,
43
+ Tooltip
44
+ } from "@blueprintjs/core";
45
+ import classNames from "classnames";
46
+ import scrollIntoView from "dom-scroll-into-view";
47
+ import { SortableElement } from "react-sortable-hoc";
48
+ import ReactTable from "@teselagen/react-table";
49
+ import { withProps, branch, compose } from "recompose";
50
+ import dayjs from "dayjs";
51
+ import localizedFormat from "dayjs/plugin/localizedFormat";
52
+ import ReactMarkdown from "react-markdown";
53
+ import immer, { produceWithPatches, enablePatches, applyPatches } from "immer";
54
+ import papaparse from "papaparse";
55
+ import remarkGfm from "remark-gfm";
56
+
57
+ import TgSelect from "../TgSelect";
58
+ import { withHotkeys } from "../utils/hotkeyUtils";
59
+ import InfoHelper from "../InfoHelper";
60
+ import getTextFromEl from "../utils/getTextFromEl";
61
+ import { getSelectedRowsFromEntities } from "./utils/selection";
62
+ import rowClick, {
63
+ changeSelectedEntities,
64
+ finalizeSelection
65
+ } from "./utils/rowClick";
66
+ import PagingTool from "./PagingTool";
67
+ import FilterAndSortMenu from "./FilterAndSortMenu";
68
+ import getIdOrCodeOrIndex from "./utils/getIdOrCodeOrIndex";
69
+ import SearchBar from "./SearchBar";
70
+ import DisplayOptions from "./DisplayOptions";
71
+ import DisabledLoadingComponent from "./DisabledLoadingComponent";
72
+ import SortableColumns from "./SortableColumns";
73
+ import computePresets from "./utils/computePresets";
74
+ import dataTableEnhancer from "./dataTableEnhancer";
75
+ import defaultProps from "./defaultProps";
76
+
77
+ import "../toastr";
78
+ import "@teselagen/react-table/react-table.css";
79
+ import "./style.css";
80
+ import { getRecordsFromIdMap } from "./utils/withSelectedEntities";
81
+ import { CellDragHandle } from "./CellDragHandle";
82
+ import { nanoid } from "nanoid";
83
+ import { SwitchField } from "../FormComponents";
84
+ import { validateTableWideErrors } from "./validateTableWideErrors";
85
+ import { editCellHelper } from "./editCellHelper";
86
+ import { getCellVal } from "./getCellVal";
87
+ import { getVals } from "./getVals";
88
+ enablePatches();
89
+
90
+ const PRIMARY_SELECTED_VAL = "main_cell";
91
+
92
+ dayjs.extend(localizedFormat);
93
+ const IS_LINUX = window.navigator.platform.toLowerCase().search("linux") > -1;
94
+ class DataTable extends React.Component {
95
+ constructor(props) {
96
+ super(props);
97
+ this.hotkeyEnabler = withHotkeys({
98
+ moveUpARow: {
99
+ global: false,
100
+ combo: "up",
101
+ label: "Move Up a Row",
102
+ onKeyDown: this.handleRowMove("up")
103
+ },
104
+ moveDownARow: {
105
+ global: false,
106
+ combo: "down",
107
+ label: "Move Down a Row",
108
+ onKeyDown: this.handleRowMove("down")
109
+ },
110
+ moveUpARowShift: {
111
+ global: false,
112
+ combo: "up+shift",
113
+ label: "Move Up a Row",
114
+ onKeyDown: this.handleRowMove("up", true)
115
+ },
116
+ ...(props.isCellEditable && {
117
+ enter: {
118
+ global: false,
119
+ combo: "enter",
120
+ label: "Enter -> Start Cell Edit",
121
+ onKeyDown: this.handleEnterStartCellEdit
122
+ },
123
+
124
+ cut: {
125
+ global: false,
126
+ combo: "mod+x",
127
+ label: "Cut",
128
+ onKeyDown: this.handleCut
129
+ },
130
+ undo: {
131
+ global: false,
132
+ combo: IS_LINUX ? "alt+z" : "mod+z",
133
+ label: "Undo",
134
+ onKeyDown: this.handleUndo
135
+ },
136
+ redo: {
137
+ global: false,
138
+ combo: IS_LINUX ? "alt+shift+z" : "mod+shift+z",
139
+ label: "Redo",
140
+ onKeyDown: this.handleRedo
141
+ },
142
+ deleteCell: {
143
+ global: false,
144
+ combo: "backspace",
145
+ label: "Delete Cell",
146
+ onKeyDown: this.handleDeleteCell
147
+ }
148
+ }),
149
+ moveDownARowShift: {
150
+ global: false,
151
+ combo: "down+shift",
152
+ label: "Move Down a Row",
153
+ onKeyDown: this.handleRowMove("down", true)
154
+ },
155
+ copyHotkey: {
156
+ global: false,
157
+ combo: "mod + c",
158
+ label: "Copy rows",
159
+ onKeyDown: this.handleCopyHotkey
160
+ },
161
+ selectAllRows: {
162
+ global: false,
163
+ combo: "mod + a",
164
+ label: "Select rows",
165
+ onKeyDown: this.handleSelectAllRows
166
+ }
167
+ });
168
+ }
169
+ state = {
170
+ columns: [],
171
+ fullscreen: false
172
+ };
173
+
174
+ static defaultProps = defaultProps;
175
+
176
+ toggleFullscreen = () => {
177
+ this.setState({
178
+ fullscreen: !this.state.fullscreen
179
+ });
180
+ };
181
+ handleEnterStartCellEdit = e => {
182
+ e.stopPropagation();
183
+ this.startCellEdit(this.getPrimarySelectedCellId());
184
+ };
185
+ flashTableBorder = () => {
186
+ try {
187
+ const table = ReactDOM.findDOMNode(this.table);
188
+ table.classList.add("tgBorderBlue");
189
+ setTimeout(() => {
190
+ table.classList.remove("tgBorderBlue");
191
+ }, 300);
192
+ } catch (e) {
193
+ console.error(`err when flashing table border:`, e);
194
+ }
195
+ };
196
+ handleUndo = () => {
197
+ const {
198
+ change,
199
+ entities,
200
+ reduxFormEntitiesUndoRedoStack = { currentVersion: 0 }
201
+ } = this.props;
202
+
203
+ if (reduxFormEntitiesUndoRedoStack.currentVersion > 0) {
204
+ this.flashTableBorder();
205
+ const nextState = applyPatches(
206
+ entities,
207
+ reduxFormEntitiesUndoRedoStack[
208
+ reduxFormEntitiesUndoRedoStack.currentVersion
209
+ ].inversePatches
210
+ );
211
+ const { newEnts, validationErrors } = this.formatAndValidateEntities(
212
+ nextState
213
+ );
214
+ change("reduxFormEntities", newEnts);
215
+ this.updateValidation(newEnts, validationErrors);
216
+ change("reduxFormEntitiesUndoRedoStack", {
217
+ ...reduxFormEntitiesUndoRedoStack,
218
+ currentVersion: reduxFormEntitiesUndoRedoStack.currentVersion - 1
219
+ });
220
+ }
221
+ };
222
+ handleRedo = () => {
223
+ const {
224
+ change,
225
+ entities,
226
+ reduxFormEntitiesUndoRedoStack = { currentVersion: 0 }
227
+ } = this.props;
228
+
229
+ const nextV = reduxFormEntitiesUndoRedoStack.currentVersion + 1;
230
+ if (reduxFormEntitiesUndoRedoStack[nextV]) {
231
+ this.flashTableBorder();
232
+ const nextState = applyPatches(
233
+ entities,
234
+ reduxFormEntitiesUndoRedoStack[nextV].patches
235
+ );
236
+ const { newEnts, validationErrors } = this.formatAndValidateEntities(
237
+ nextState
238
+ );
239
+ change("reduxFormEntities", newEnts);
240
+ this.updateValidation(newEnts, validationErrors);
241
+ change("reduxFormEntitiesUndoRedoStack", {
242
+ ...reduxFormEntitiesUndoRedoStack,
243
+ currentVersion: nextV
244
+ });
245
+ }
246
+ };
247
+ updateFromProps = (oldProps, newProps) => {
248
+ const {
249
+ selectedIds,
250
+ entities = [],
251
+ isEntityDisabled,
252
+ expandAllByDefault,
253
+ selectAllByDefault,
254
+ reduxFormSelectedEntityIdMap,
255
+ reduxFormExpandedEntityIdMap,
256
+ change
257
+ } = newProps;
258
+ const table = ReactDOM.findDOMNode(this.table);
259
+
260
+ const idMap = reduxFormSelectedEntityIdMap;
261
+
262
+ //handle programatic filter adding
263
+ if (!isEqual(newProps.additionalFilters, oldProps.additionalFilters)) {
264
+ newProps.addFilters(newProps.additionalFilters);
265
+ }
266
+ if (!isEqual(newProps.schema, oldProps.schema)) {
267
+ const { schema = {} } = newProps;
268
+ const columns = schema.fields
269
+ ? schema.fields.reduce(function(columns, field, i) {
270
+ if (field.isHidden) {
271
+ return columns;
272
+ }
273
+ return columns.concat({
274
+ ...field,
275
+ columnIndex: i
276
+ });
277
+ }, [])
278
+ : [];
279
+ this.setState({ columns });
280
+ }
281
+ let selectedIdsToUse = selectedIds;
282
+ let newIdMap;
283
+ //handle selecting all or expanding all
284
+ if (
285
+ (selectAllByDefault || expandAllByDefault) &&
286
+ !isEqual(
287
+ (newProps.entities || []).map(({ id }) => id),
288
+ oldProps.entities && oldProps.entities.map(({ id }) => id)
289
+ )
290
+ ) {
291
+ if (
292
+ selectAllByDefault &&
293
+ !this.alreadySelected &&
294
+ entities &&
295
+ entities.length
296
+ ) {
297
+ this.alreadySelected = true;
298
+ newIdMap = {
299
+ ...(entities || []).reduce((acc, entity) => {
300
+ acc[entity.id || entity.code] = { entity, time: Date.now() };
301
+ return acc;
302
+ }, {}),
303
+ ...(reduxFormSelectedEntityIdMap || {})
304
+ };
305
+ selectedIdsToUse = map(newIdMap, (a, key) => key);
306
+ }
307
+ if (expandAllByDefault) {
308
+ change("reduxFormExpandedEntityIdMap", {
309
+ ...(entities || []).reduce((acc, e) => {
310
+ acc[e.id || e.code] = true;
311
+ return acc;
312
+ }, {}),
313
+ ...(reduxFormExpandedEntityIdMap || {})
314
+ });
315
+ }
316
+ }
317
+
318
+ // handle programmatic selection and scrolling
319
+ const { selectedIds: oldSelectedIds } = oldProps;
320
+ if (isEqual(selectedIdsToUse, oldSelectedIds)) {
321
+ // if not changing selectedIds then we just want to make sure selected entities
322
+ // stored in redux are in proper format
323
+ // if selected ids have changed then it will handle redux selection
324
+ const tableScrollElement = table.getElementsByClassName("rt-table")[0];
325
+ const {
326
+ entities: oldEntities = [],
327
+ reduxFormSelectedEntityIdMap: oldIdMap
328
+ } = oldProps;
329
+ const reloaded = oldProps.isLoading && !this.props.isLoading;
330
+ const entitiesHaveChanged =
331
+ oldEntities.length !== entities.length ||
332
+ getIdOrCodeOrIndex(entities[0] || {}) !==
333
+ getIdOrCodeOrIndex(oldEntities[0] || {});
334
+ // if switching pages or searching the table we want to reset the scrollbar
335
+ if (tableScrollElement.scrollTop > 0 && !this.props.isCellEditable) {
336
+ if (reloaded || entitiesHaveChanged) {
337
+ tableScrollElement.scrollTop = 0;
338
+ }
339
+ }
340
+ // re-index entities in redux form so that sorting will be correct in withSelectedEntities
341
+ if (change) {
342
+ if (entitiesHaveChanged && (!isEmpty(oldIdMap) || !isEmpty(idMap))) {
343
+ changeSelectedEntities({ idMap, entities, change });
344
+ } else if (
345
+ !isEmpty(idMap) &&
346
+ idMap[Object.keys(idMap)[0]] &&
347
+ idMap[Object.keys(idMap)[0]].rowIndex === undefined
348
+ ) {
349
+ // if programmatically selected will still want the order to match the table sorting.
350
+ changeSelectedEntities({ idMap, entities, change });
351
+ }
352
+ }
353
+ } else {
354
+ const idArray = Array.isArray(selectedIdsToUse)
355
+ ? selectedIdsToUse
356
+ : [selectedIdsToUse];
357
+ const selectedEntities = entities.filter(
358
+ e => idArray.indexOf(getIdOrCodeOrIndex(e)) > -1 && !isEntityDisabled(e)
359
+ );
360
+ newIdMap =
361
+ newIdMap ||
362
+ selectedEntities.reduce((acc, entity) => {
363
+ acc[getIdOrCodeOrIndex(entity)] = { entity };
364
+ return acc;
365
+ }, {});
366
+ change("reduxFormExpandedEntityIdMap", newIdMap);
367
+ finalizeSelection({ idMap: newIdMap, entities, props: newProps });
368
+ const idToScrollTo = idArray[0];
369
+ if (!idToScrollTo && idToScrollTo !== 0) return;
370
+ const entityIndexToScrollTo = entities.findIndex(
371
+ e => e.id === idToScrollTo || e.code === idToScrollTo
372
+ );
373
+ if (entityIndexToScrollTo === -1 || !table) return;
374
+ const tableBody = table.querySelector(".rt-tbody");
375
+ if (!tableBody) return;
376
+ const rowEl = tableBody.getElementsByClassName("rt-tr-group")[
377
+ entityIndexToScrollTo
378
+ ];
379
+ if (!rowEl) return;
380
+ setTimeout(() => {
381
+ //we need to delay for a teeny bit to make sure the table has drawn
382
+ rowEl &&
383
+ tableBody &&
384
+ scrollIntoView(rowEl, tableBody, {
385
+ alignWithTop: true
386
+ });
387
+ }, 0);
388
+ }
389
+ };
390
+ formatAndValidateEntities = entities => {
391
+ const { schema } = this.props;
392
+ const editableFields = schema.fields.filter(f => !f.isNotEditable);
393
+ const validationErrors = {};
394
+
395
+ const newEnts = immer(entities, entities => {
396
+ entities.forEach((e, index) => {
397
+ editableFields.forEach(columnSchema => {
398
+ //mutative
399
+ const { error } = editCellHelper({
400
+ entity: e,
401
+ columnSchema,
402
+ newVal: e[columnSchema.path]
403
+ });
404
+ if (error) {
405
+ const rowId = getIdOrCodeOrIndex(e, index);
406
+ validationErrors[`${rowId}:${columnSchema.path}`] = error;
407
+ }
408
+ });
409
+ });
410
+ });
411
+ return {
412
+ newEnts,
413
+ validationErrors
414
+ };
415
+ };
416
+ formatAndValidateTableInitial = () => {
417
+ const {
418
+ _origEntities: entities,
419
+ initialEntities,
420
+ change,
421
+ reduxFormCellValidation
422
+ } = this.props;
423
+ const { newEnts, validationErrors } = this.formatAndValidateEntities(
424
+ initialEntities || entities
425
+ );
426
+ change("reduxFormEntities", newEnts);
427
+ const toKeep = {};
428
+ //on the initial load we want to keep any async table wide errors
429
+ forEach(reduxFormCellValidation, (v, k) => {
430
+ if (v && v._isTableAsyncWideError) {
431
+ toKeep[k] = v;
432
+ }
433
+ });
434
+ this.updateValidation(newEnts, {
435
+ ...toKeep,
436
+ ...validationErrors
437
+ });
438
+ };
439
+
440
+ componentDidMount() {
441
+ const {
442
+ isCellEditable,
443
+ entities = [],
444
+ isLoading,
445
+ showForcedHiddenColumns,
446
+ setShowForcedHidden
447
+ } = this.props;
448
+ isCellEditable && this.formatAndValidateTableInitial();
449
+ this.updateFromProps({}, computePresets(this.props));
450
+ document.addEventListener("paste", this.handlePaste);
451
+
452
+ if (!entities.length && !isLoading && !showForcedHiddenColumns) {
453
+ setShowForcedHidden(true);
454
+ }
455
+ // const table = ReactDOM.findDOMNode(this.table);
456
+ // let theads = table.getElementsByClassName("rt-thead");
457
+ // let tbody = table.getElementsByClassName("rt-tbody")[0];
458
+
459
+ // tbody.addEventListener("scroll", () => {
460
+ // for (let i = 0; i < theads.length; i++) {
461
+ // theads.item(i).scrollLeft = tbody.scrollLeft;
462
+ // }
463
+ // });
464
+ }
465
+
466
+ componentDidUpdate(oldProps) {
467
+ // const tableBody = table.querySelector(".rt-tbody");
468
+ // const headerNode = table.querySelector(".rt-thead.-header");
469
+ // if (headerNode) headerNode.style.overflowY = "inherit";
470
+ // if (tableBody && tableBody.scrollHeight > tableBody.clientHeight) {
471
+ // if (headerNode) {
472
+ // headerNode.style.overflowY = "scroll";
473
+ // headerNode.style.overflowX = "hidden";
474
+ // }
475
+ // }
476
+
477
+ this.updateFromProps(computePresets(oldProps), computePresets(this.props));
478
+
479
+ // comment in to test what is causing re-render
480
+ // Object.entries(this.props).forEach(
481
+ // ([key, val]) =>
482
+ // oldProps[key] !== val && console.info(`Prop '${key}' changed`)
483
+ // );
484
+ }
485
+
486
+ componentWillUnmount() {
487
+ document.removeEventListener("paste", this.handlePaste);
488
+ }
489
+
490
+ handleRowMove = (type, shiftHeld) => e => {
491
+ e.preventDefault();
492
+ e.stopPropagation();
493
+ const props = computePresets(this.props);
494
+ const {
495
+ noSelect,
496
+ entities,
497
+ reduxFormSelectedEntityIdMap: idMap,
498
+ isEntityDisabled,
499
+ isSingleSelect
500
+ } = props;
501
+ let newIdMap = {};
502
+ const lastSelectedEnt = getLastSelectedEntity(idMap);
503
+
504
+ if (noSelect) return;
505
+ if (lastSelectedEnt) {
506
+ let lastSelectedIndex = entities.findIndex(
507
+ ent => ent === lastSelectedEnt
508
+ );
509
+ if (lastSelectedIndex === -1) {
510
+ if (lastSelectedEnt.id !== undefined) {
511
+ lastSelectedIndex = entities.findIndex(
512
+ ent => ent.id === lastSelectedEnt.id
513
+ );
514
+ } else if (lastSelectedEnt.code !== undefined) {
515
+ lastSelectedIndex = entities.findIndex(
516
+ ent => ent.code === lastSelectedEnt.code
517
+ );
518
+ }
519
+ }
520
+ if (lastSelectedIndex === -1) {
521
+ return;
522
+ }
523
+ const newEntToSelect = getNewEntToSelect({
524
+ type,
525
+ lastSelectedIndex,
526
+ entities,
527
+ isEntityDisabled
528
+ });
529
+
530
+ if (!newEntToSelect) return;
531
+ if (shiftHeld && !isSingleSelect) {
532
+ if (idMap[newEntToSelect.id || newEntToSelect.code]) {
533
+ //the entity being moved to has already been selected
534
+ newIdMap = omit(idMap, [lastSelectedEnt.id || lastSelectedEnt.code]);
535
+ newIdMap[newEntToSelect.id || newEntToSelect.code].time =
536
+ Date.now() + 1;
537
+ } else {
538
+ //the entity being moved to has NOT been selected yet
539
+ newIdMap = {
540
+ ...idMap,
541
+ [newEntToSelect.id || newEntToSelect.code]: {
542
+ entity: newEntToSelect,
543
+ time: Date.now()
544
+ }
545
+ };
546
+ }
547
+ } else {
548
+ //no shiftHeld
549
+ newIdMap[newEntToSelect.id || newEntToSelect.code] = {
550
+ entity: newEntToSelect,
551
+ time: Date.now()
552
+ };
553
+ }
554
+ }
555
+
556
+ finalizeSelection({
557
+ idMap: newIdMap,
558
+ entities,
559
+ props
560
+ });
561
+ };
562
+ handleCopyHotkey = e => {
563
+ const { isCellEditable, reduxFormSelectedEntityIdMap } = computePresets(
564
+ this.props
565
+ );
566
+
567
+ if (isCellEditable) {
568
+ this.handleCopySelectedCells(e);
569
+ } else {
570
+ this.handleCopySelectedRows(
571
+ getRecordsFromIdMap(reduxFormSelectedEntityIdMap),
572
+ e
573
+ );
574
+ }
575
+ };
576
+
577
+ getPrimarySelectedCellId = () => {
578
+ const { reduxFormSelectedCells = {} } = this.props;
579
+ for (const k of Object.keys(reduxFormSelectedCells)) {
580
+ if (reduxFormSelectedCells[k] === PRIMARY_SELECTED_VAL) {
581
+ return k;
582
+ }
583
+ }
584
+ };
585
+
586
+ handlePaste = e => {
587
+ const {
588
+ isCellEditable,
589
+ reduxFormSelectedCells,
590
+ reduxFormCellValidation,
591
+ change,
592
+ schema,
593
+ entities
594
+ } = computePresets(this.props);
595
+
596
+ if (isCellEditable) {
597
+ if (isEmpty(reduxFormSelectedCells)) return;
598
+ try {
599
+ let pasteData = [];
600
+ let toPaste;
601
+ if (window.clipboardData && window.clipboardData.getData) {
602
+ // IE
603
+ toPaste = window.clipboardData.getData("Text");
604
+ } else if (e.clipboardData && e.clipboardData.getData) {
605
+ toPaste = e.clipboardData.getData("text/plain");
606
+ }
607
+ if (toPaste.includes(",")) {
608
+ //try papaparsing it out as a csv if it contains commas
609
+ try {
610
+ const { data, errors } = papaparse.parse(toPaste, {
611
+ header: false
612
+ });
613
+ if (data?.length && !errors?.length) {
614
+ pasteData = data;
615
+ }
616
+ } catch (error) {
617
+ console.error(`error p982qhgpf9qh`, error);
618
+ }
619
+ }
620
+ pasteData = pasteData.length ? pasteData : defaultParsePaste(toPaste);
621
+
622
+ if (!pasteData || !pasteData.length) return;
623
+
624
+ if (pasteData.length === 1 && pasteData[0].length === 1) {
625
+ const newCellValidate = {
626
+ ...reduxFormCellValidation
627
+ };
628
+ // single paste value, fill all cells with value
629
+ const newVal = pasteData[0][0];
630
+ this.updateEntitiesHelper(entities, entities => {
631
+ const entityIdToEntity = getEntityIdToEntity(entities);
632
+ Object.keys(reduxFormSelectedCells).forEach(cellId => {
633
+ const [rowId, path] = cellId.split(":");
634
+ const entity = entityIdToEntity[rowId].e;
635
+ delete entity._isClean;
636
+ const { error } = editCellHelper({
637
+ entity,
638
+ path,
639
+ schema,
640
+ newVal
641
+ });
642
+ if (error) {
643
+ newCellValidate[cellId] = error;
644
+ } else {
645
+ delete newCellValidate[cellId];
646
+ }
647
+ });
648
+ this.updateValidation(entities, newCellValidate);
649
+ });
650
+ } else {
651
+ // handle paste in same format
652
+ const primarySelectedCell = this.getPrimarySelectedCellId();
653
+ if (primarySelectedCell) {
654
+ const newCellValidate = {
655
+ ...reduxFormCellValidation
656
+ };
657
+
658
+ const newSelectedCells = { ...reduxFormSelectedCells };
659
+ this.updateEntitiesHelper(entities, entities => {
660
+ const entityIdToEntity = getEntityIdToEntity(entities);
661
+ const [rowId, primaryCellPath] = primarySelectedCell.split(":");
662
+ const primaryEntityInfo = entityIdToEntity[rowId];
663
+ const startIndex = primaryEntityInfo.i;
664
+ const endIndex = primaryEntityInfo.i + pasteData.length;
665
+ for (let i = startIndex; i < endIndex; i++) {
666
+ if (!entities[i]) {
667
+ entities[i] = { id: nanoid() };
668
+ }
669
+ }
670
+ const entitiesToManipulate = entities.slice(startIndex, endIndex);
671
+ const pathToIndex = getFieldPathToIndex(schema);
672
+ const indexToPath = invert(pathToIndex);
673
+ const startCellIndex = pathToIndex[primaryCellPath];
674
+ pasteData.forEach((row, i) => {
675
+ row.forEach((cell, j) => {
676
+ if (cell) {
677
+ const cellIndexToChange = startCellIndex + j;
678
+ const entity = entitiesToManipulate[i];
679
+ if (entity) {
680
+ delete entity._isClean;
681
+ const path = indexToPath[cellIndexToChange];
682
+ if (path) {
683
+ const { error } = editCellHelper({
684
+ entity,
685
+ path,
686
+ schema,
687
+ newVal: cell
688
+ });
689
+ const cellId = `${getIdOrCodeOrIndex(entity)}:${path}`;
690
+ if (!newSelectedCells[cellId]) {
691
+ newSelectedCells[cellId] = true;
692
+ }
693
+ if (error) {
694
+ newCellValidate[cellId] = error;
695
+ } else {
696
+ delete newCellValidate[cellId];
697
+ }
698
+ }
699
+ }
700
+ }
701
+ });
702
+ });
703
+ this.updateValidation(entities, newCellValidate);
704
+ });
705
+ change("reduxFormSelectedCells", newSelectedCells);
706
+ }
707
+ }
708
+ } catch (error) {
709
+ console.error(`error:`, error);
710
+ }
711
+ }
712
+ };
713
+ handleSelectAllRows = e => {
714
+ const {
715
+ change,
716
+ isEntityDisabled,
717
+ entities,
718
+ isSingleSelect,
719
+ isCellEditable,
720
+ schema
721
+ } = computePresets(this.props);
722
+ if (isSingleSelect) return;
723
+ e.preventDefault();
724
+
725
+ if (isCellEditable) {
726
+ const schemaPaths = schema.fields.map(f => f.path);
727
+ const newSelectedCells = {};
728
+ entities.forEach((entity, i) => {
729
+ if (isEntityDisabled(entity)) return;
730
+ const entityId = getIdOrCodeOrIndex(entity, i);
731
+ schemaPaths.forEach(p => {
732
+ newSelectedCells[`${entityId}:${p}`] = true;
733
+ });
734
+ });
735
+ change("reduxFormSelectedCells", newSelectedCells);
736
+ } else {
737
+ const newIdMap = {};
738
+
739
+ entities.forEach((entity, i) => {
740
+ if (isEntityDisabled(entity)) return;
741
+ const entityId = getIdOrCodeOrIndex(entity, i);
742
+ newIdMap[entityId] = { entity };
743
+ });
744
+ finalizeSelection({
745
+ idMap: newIdMap,
746
+ entities,
747
+ props: computePresets(this.props)
748
+ });
749
+ }
750
+ };
751
+
752
+ updateValidation = (entities, newCellValidate) => {
753
+ const { change, schema } = computePresets(this.props);
754
+ change(
755
+ "reduxFormCellValidation",
756
+ validateTableWideErrors({ entities, schema, newCellValidate })
757
+ );
758
+ };
759
+ handleDeleteCell = () => {
760
+ const {
761
+ reduxFormSelectedCells,
762
+ reduxFormCellValidation,
763
+ schema,
764
+ entities
765
+ } = computePresets(this.props);
766
+ const newCellValidate = {
767
+ ...reduxFormCellValidation
768
+ };
769
+ if (isEmpty(reduxFormSelectedCells)) return;
770
+ const rowIds = [];
771
+ this.updateEntitiesHelper(entities, entities => {
772
+ const entityIdToEntity = getEntityIdToEntity(entities);
773
+ Object.keys(reduxFormSelectedCells).forEach(cellId => {
774
+ const [rowId, path] = cellId.split(":");
775
+ rowIds.push(rowId);
776
+ const entity = entityIdToEntity[rowId].e;
777
+ delete entity._isClean;
778
+ const { error } = editCellHelper({
779
+ entity,
780
+ path,
781
+ schema,
782
+ newVal: ""
783
+ });
784
+ if (error) {
785
+ newCellValidate[cellId] = error;
786
+ } else {
787
+ delete newCellValidate[cellId];
788
+ }
789
+ });
790
+ this.updateValidation(entities, newCellValidate);
791
+ });
792
+ };
793
+
794
+ handleCut = e => {
795
+ this.handleDeleteCell();
796
+ this.handleCopyHotkey(e);
797
+ };
798
+
799
+ getCellCopyText = cellWrapper => {
800
+ const text = cellWrapper && cellWrapper.getAttribute("data-copy-text");
801
+
802
+ const toRet = text || cellWrapper.textContent || "";
803
+ return toRet;
804
+ };
805
+
806
+ handleCopyRow = rowEl => {
807
+ //takes in a row element
808
+ const text = this.getRowCopyText(rowEl);
809
+ if (!text) return window.toastr.warning("No text to copy");
810
+ this.handleCopyHelper(text, "Row Copied");
811
+ };
812
+ handleCopyColumn = (e, cellWrapper) => {
813
+ const cellType = cellWrapper.getAttribute("data-test");
814
+ const allRowEls = getAllRows(e);
815
+ if (!allRowEls) return;
816
+ const textToCopy = map(allRowEls, rowEl =>
817
+ this.getRowCopyText(rowEl, { cellType })
818
+ )
819
+ .filter(text => text)
820
+ .join("\n");
821
+ if (!textToCopy) return window.toastr.warning("No text to copy");
822
+
823
+ this.handleCopyHelper(textToCopy, "Column copied");
824
+ };
825
+ updateEntitiesHelper = (ents, fn) => {
826
+ const {
827
+ change,
828
+ reduxFormEntitiesUndoRedoStack = { currentVersion: 0 }
829
+ } = this.props;
830
+ const [nextState, patches, inversePatches] = produceWithPatches(ents, fn);
831
+ if (!inversePatches.length) return;
832
+ const thatNewNew = [...nextState];
833
+ thatNewNew.isDirty = true;
834
+ change("reduxFormEntities", thatNewNew);
835
+ change("reduxFormEntitiesUndoRedoStack", {
836
+ ...omitBy(reduxFormEntitiesUndoRedoStack, (v, k) => {
837
+ return toNumber(k) > reduxFormEntitiesUndoRedoStack.currentVersion + 1;
838
+ }),
839
+ currentVersion: reduxFormEntitiesUndoRedoStack.currentVersion + 1,
840
+ [reduxFormEntitiesUndoRedoStack.currentVersion + 1]: {
841
+ inversePatches,
842
+ patches
843
+ }
844
+ });
845
+ };
846
+
847
+ getRowCopyText = (rowEl, { cellType } = {}) => {
848
+ //takes in a row element
849
+ if (!rowEl) return;
850
+ return flatMap(rowEl.children, cellEl => {
851
+ const cellChild = cellEl.querySelector(`[data-copy-text]`);
852
+ if (!cellChild) {
853
+ if (cellType) return []; //strip it
854
+ return; //just leave it blank
855
+ }
856
+ if (cellType && cellChild.getAttribute("data-test") !== cellType) {
857
+ return [];
858
+ }
859
+ return this.getCellCopyText(cellChild);
860
+ }).join("\t");
861
+ };
862
+
863
+ handleCopyHelper = (stringToCopy, message) => {
864
+ const copyHandler = e => {
865
+ e.preventDefault();
866
+ e.clipboardData.setData("text/plain", stringToCopy);
867
+ };
868
+ document.addEventListener("copy", copyHandler);
869
+ !window.Cypress &&
870
+ copy(stringToCopy, {
871
+ // keep this so that pasting into spreadsheets works.
872
+ format: "text/plain"
873
+ });
874
+ document.removeEventListener("copy", copyHandler);
875
+ window.toastr.success(message);
876
+ };
877
+
878
+ handleCopyTable = e => {
879
+ try {
880
+ const allRowEls = getAllRows(e);
881
+ if (!allRowEls) return;
882
+ //get row elements and call this.handleCopyRow for each
883
+ const textToCopy = map(allRowEls, rowEl => this.getRowCopyText(rowEl))
884
+ .filter(text => text)
885
+ .join("\n");
886
+ if (!textToCopy) return window.toastr.warning("No text to copy");
887
+
888
+ this.handleCopyHelper(textToCopy, "Table copied");
889
+ } catch (error) {
890
+ console.error(`error:`, error);
891
+ window.toastr.error("Error copying rows.");
892
+ }
893
+ };
894
+ handleCopySelectedCells = e => {
895
+ const { entities = [], reduxFormSelectedCells, schema } = computePresets(
896
+ this.props
897
+ );
898
+ // if the current selection is consecutive cells then copy with
899
+ // tabs between. if not then just select primary selected cell
900
+ if (isEmpty(reduxFormSelectedCells)) return;
901
+ const pathToIndex = getFieldPathToIndex(schema);
902
+ const entityIdToEntity = getEntityIdToEntity(entities);
903
+ const selectionGrid = [];
904
+ let firstRowIndex;
905
+ let firstCellIndex;
906
+ Object.keys(reduxFormSelectedCells).forEach(key => {
907
+ const [rowId, path] = key.split(":");
908
+ const eInfo = entityIdToEntity[rowId];
909
+ if (eInfo) {
910
+ if (firstRowIndex === undefined || eInfo.i < firstRowIndex) {
911
+ firstRowIndex = eInfo.i;
912
+ }
913
+ if (!selectionGrid[eInfo.i]) {
914
+ selectionGrid[eInfo.i] = [];
915
+ }
916
+ const cellIndex = pathToIndex[path];
917
+ if (firstCellIndex === undefined || cellIndex < firstCellIndex) {
918
+ firstCellIndex = cellIndex;
919
+ }
920
+ selectionGrid[eInfo.i][cellIndex] = true;
921
+ }
922
+ });
923
+ if (firstRowIndex === undefined) return;
924
+ const allRows = getAllRows(e);
925
+ let fullCellText = "";
926
+ times(selectionGrid.length, i => {
927
+ const row = selectionGrid[i];
928
+ if (fullCellText) {
929
+ fullCellText += "\n";
930
+ }
931
+ if (!row) {
932
+ return;
933
+ } else {
934
+ // ignore header
935
+ const rowCopyText = this.getRowCopyText(allRows[i + 1]).split("\t");
936
+ times(row.length, i => {
937
+ const cell = row[i];
938
+ if (cell) {
939
+ fullCellText += rowCopyText[i];
940
+ }
941
+ if (i !== row.length - 1 && i >= firstCellIndex) fullCellText += "\t";
942
+ });
943
+ }
944
+ });
945
+ if (!fullCellText) return window.toastr.warning("No text to copy");
946
+
947
+ this.handleCopyHelper(fullCellText, "Selected cells copied");
948
+ };
949
+
950
+ handleCopySelectedRows = (selectedRecords, e) => {
951
+ const { entities = [] } = computePresets(this.props);
952
+ const idToIndex = entities.reduce((acc, e, i) => {
953
+ acc[e.id || e.code] = i;
954
+ return acc;
955
+ }, {});
956
+
957
+ //index 0 of the table is the column titles
958
+ //must add 1 to rowNum
959
+ const rowNumbersToCopy = selectedRecords
960
+ .map(rec => idToIndex[rec.id || rec.code] + 1)
961
+ .sort();
962
+
963
+ if (!rowNumbersToCopy.length) return;
964
+ rowNumbersToCopy.unshift(0); //add in the header row
965
+ try {
966
+ const allRowEls = getAllRows(e);
967
+ if (!allRowEls) return;
968
+ const rowEls = rowNumbersToCopy.map(i => allRowEls[i]);
969
+
970
+ //get row elements and call this.handleCopyRow for each const rowEls = this.getRowEls(rowNumbersToCopy)
971
+ const textToCopy = map(rowEls, rowEl => this.getRowCopyText(rowEl))
972
+ .filter(text => text)
973
+ .join("\n");
974
+ if (!textToCopy) return window.toastr.warning("No text to copy");
975
+
976
+ this.handleCopyHelper(textToCopy, "Selected rows copied");
977
+ } catch (error) {
978
+ console.error(`error:`, error);
979
+ window.toastr.error("Error copying rows.");
980
+ }
981
+ };
982
+
983
+ moveColumn = ({ oldIndex, newIndex }) => {
984
+ const { columns } = this.state;
985
+ let oldStateColumnIndex, newStateColumnIndex;
986
+ columns.forEach((column, i) => {
987
+ if (oldIndex === column.columnIndex) oldStateColumnIndex = i;
988
+ if (newIndex === column.columnIndex) newStateColumnIndex = i;
989
+ });
990
+ // because it is all handled in state we need
991
+ // to perform the move and update the columnIndices
992
+ // because they are used for the sortable columns
993
+ const newColumns = arrayMove(
994
+ columns,
995
+ oldStateColumnIndex,
996
+ newStateColumnIndex
997
+ ).map((column, i) => {
998
+ return {
999
+ ...column,
1000
+ columnIndex: i
1001
+ };
1002
+ });
1003
+ this.setState({
1004
+ columns: newColumns
1005
+ });
1006
+ };
1007
+
1008
+ getTheadComponent = props => {
1009
+ const {
1010
+ withDisplayOptions,
1011
+ moveColumnPersist,
1012
+ syncDisplayOptionsToDb,
1013
+ change
1014
+ } = computePresets(this.props);
1015
+ let moveColumnPersistToUse = moveColumnPersist;
1016
+ if (moveColumnPersist && withDisplayOptions && !syncDisplayOptionsToDb) {
1017
+ //little hack to make localstorage changes get reflected in UI (we force an update to get the enhancers to run again :)
1018
+ moveColumnPersistToUse = (...args) => {
1019
+ moveColumnPersist(...args);
1020
+ change("localStorageForceUpdate", Math.random());
1021
+ };
1022
+ }
1023
+ return (
1024
+ <SortableColumns
1025
+ {...props}
1026
+ withDisplayOptions={withDisplayOptions}
1027
+ moveColumn={moveColumnPersistToUse || this.moveColumn}
1028
+ />
1029
+ );
1030
+ };
1031
+ getThComponent = compose(
1032
+ withProps(props => {
1033
+ const { columnindex } = props;
1034
+ return {
1035
+ index: columnindex || 0
1036
+ };
1037
+ }),
1038
+ branch(({ immovable }) => "true" !== immovable, SortableElement)
1039
+ )(({ toggleSort, className, children, ...rest }) => (
1040
+ <div
1041
+ className={classNames("rt-th", className)}
1042
+ onClick={e => toggleSort && toggleSort(e)}
1043
+ role="columnheader"
1044
+ tabIndex="-1" // Resolves eslint issues without implementing keyboard navigation incorrectly
1045
+ {...rest}
1046
+ >
1047
+ {children}
1048
+ </div>
1049
+ ));
1050
+
1051
+ addEntitiesToSelection = entities => {
1052
+ const propPresets = computePresets(this.props);
1053
+ const { isEntityDisabled, reduxFormSelectedEntityIdMap } = propPresets;
1054
+ const idMap = reduxFormSelectedEntityIdMap || {};
1055
+ const newIdMap = cloneDeep(idMap) || {};
1056
+ entities.forEach((entity, i) => {
1057
+ if (isEntityDisabled(entity)) return;
1058
+ const entityId = getIdOrCodeOrIndex(entity, i);
1059
+ newIdMap[entityId] = { entity };
1060
+ });
1061
+ finalizeSelection({
1062
+ idMap: newIdMap,
1063
+ entities,
1064
+ props: propPresets
1065
+ });
1066
+ };
1067
+
1068
+ render() {
1069
+ const { fullscreen } = this.state;
1070
+ const propPresets = computePresets(this.props);
1071
+ const {
1072
+ extraClasses,
1073
+ className,
1074
+ tableName,
1075
+ isLoading,
1076
+ searchTerm,
1077
+ setSearchTerm,
1078
+ clearFilters,
1079
+ hidePageSizeWhenPossible,
1080
+ doNotShowEmptyRows,
1081
+ withTitle,
1082
+ withSearch,
1083
+ withPaging,
1084
+ isInfinite,
1085
+ disabled,
1086
+ noHeader,
1087
+ noFooter,
1088
+ noPadding,
1089
+ noFullscreenButton,
1090
+ withDisplayOptions,
1091
+ resized,
1092
+ resizePersist,
1093
+ updateColumnVisibility,
1094
+ persistPageSize,
1095
+ updateTableDisplayDensity,
1096
+ change,
1097
+ syncDisplayOptionsToDb,
1098
+ resetDefaultVisibility,
1099
+ maxHeight,
1100
+ style,
1101
+ pageSize,
1102
+ formName,
1103
+ reduxFormSearchInput,
1104
+ reduxFormSelectedEntityIdMap,
1105
+ reduxFormExpandedEntityIdMap,
1106
+ schema,
1107
+ filters,
1108
+ errorParsingUrlString,
1109
+ hideDisplayOptionsIcon,
1110
+ compact,
1111
+ extraCompact,
1112
+ compactPaging,
1113
+ entityCount,
1114
+ showCount,
1115
+ isSingleSelect,
1116
+ noSelect,
1117
+ noRowsFoundMessage,
1118
+ SubComponent,
1119
+ shouldShowSubComponent,
1120
+ ReactTableProps = {},
1121
+ hideSelectedCount,
1122
+ hideColumnHeader,
1123
+ subHeader,
1124
+ isViewable,
1125
+ minimalStyle,
1126
+ entities,
1127
+ onlyShowRowsWErrors,
1128
+ reduxFormCellValidation,
1129
+ entitiesAcrossPages,
1130
+ children: maybeChildren,
1131
+ topLeftItems,
1132
+ leftOfSearchBarItems,
1133
+ currentParams,
1134
+ hasOptionForForcedHidden,
1135
+ showForcedHiddenColumns,
1136
+ searchMenuButton,
1137
+ setShowForcedHidden,
1138
+ autoFocusSearch,
1139
+ additionalFooterButtons,
1140
+ isEntityDisabled,
1141
+ isLocalCall,
1142
+ withSelectAll,
1143
+ variables,
1144
+ fragment,
1145
+ safeQuery,
1146
+ isCellEditable
1147
+ } = propPresets;
1148
+
1149
+ if (withSelectAll && !safeQuery) {
1150
+ throw new Error("safeQuery is needed for selecting all table records");
1151
+ }
1152
+ let updateColumnVisibilityToUse = updateColumnVisibility;
1153
+ let persistPageSizeToUse = persistPageSize;
1154
+ let updateTableDisplayDensityToUse = updateTableDisplayDensity;
1155
+ let resetDefaultVisibilityToUse = resetDefaultVisibility;
1156
+ if (withDisplayOptions && !syncDisplayOptionsToDb) {
1157
+ //little hack to make localstorage changes get reflected in UI (we force an update to get the enhancers to run again :)
1158
+
1159
+ const wrapUpdate = fn => (...args) => {
1160
+ fn(...args);
1161
+ change("localStorageForceUpdate", Math.random());
1162
+ };
1163
+ updateColumnVisibilityToUse = wrapUpdate(updateColumnVisibility);
1164
+ updateTableDisplayDensityToUse = wrapUpdate(updateTableDisplayDensity);
1165
+ resetDefaultVisibilityToUse = wrapUpdate(resetDefaultVisibility);
1166
+ persistPageSizeToUse = wrapUpdate(persistPageSize);
1167
+ }
1168
+ let compactClassName = "";
1169
+ if (compactPaging) {
1170
+ compactClassName += " tg-compact-paging";
1171
+ }
1172
+ compactClassName += extraCompact
1173
+ ? " tg-extra-compact-table"
1174
+ : compact
1175
+ ? " tg-compact-table"
1176
+ : "";
1177
+
1178
+ const hasFilters =
1179
+ filters.length ||
1180
+ searchTerm ||
1181
+ schema.fields.some(
1182
+ field => field.filterIsActive && field.filterIsActive(currentParams)
1183
+ );
1184
+ const additionalFilterKeys = schema.fields.reduce((acc, field) => {
1185
+ if (field.filterKey) acc.push(field.filterKey);
1186
+ return acc;
1187
+ }, []);
1188
+ const filtersOnNonDisplayedFields = [];
1189
+ if (filters && filters.length) {
1190
+ schema.fields.forEach(({ isHidden, displayName, path }) => {
1191
+ const ccDisplayName = camelCase(displayName || path);
1192
+ if (isHidden) {
1193
+ filters.forEach(filter => {
1194
+ if (filter.filterOn === ccDisplayName) {
1195
+ filtersOnNonDisplayedFields.push({
1196
+ ...filter,
1197
+ displayName
1198
+ });
1199
+ }
1200
+ });
1201
+ }
1202
+ });
1203
+ }
1204
+ const numRows = isInfinite ? entities.length : pageSize;
1205
+ const idMap = reduxFormSelectedEntityIdMap || {};
1206
+ const selectedRowCount = Object.keys(idMap).filter(key => idMap[key])
1207
+ .length;
1208
+
1209
+ let rowsToShow = doNotShowEmptyRows
1210
+ ? Math.min(numRows, entities.length)
1211
+ : numRows;
1212
+ // if there are no entities then provide enough space to show
1213
+ // no rows found message
1214
+ if (entities.length === 0 && rowsToShow < 3) rowsToShow = 3;
1215
+ const expandedRows = entities.reduce((acc, row, index) => {
1216
+ const rowId = getIdOrCodeOrIndex(row, index);
1217
+ acc[index] = reduxFormExpandedEntityIdMap[rowId];
1218
+ return acc;
1219
+ }, {});
1220
+ let children = maybeChildren;
1221
+ if (children && typeof children === "function") {
1222
+ children = children(propPresets);
1223
+ }
1224
+ const showHeader = (withTitle || withSearch || children) && !noHeader;
1225
+ const toggleFullscreenButton = (
1226
+ <Button
1227
+ icon="fullscreen"
1228
+ active={fullscreen}
1229
+ minimal
1230
+ onClick={this.toggleFullscreen}
1231
+ />
1232
+ );
1233
+
1234
+ let showSelectAll = false;
1235
+ let showClearAll = false;
1236
+ // we want to show select all if every row on the current page is selected
1237
+ // and not every row across all pages are already selected.
1238
+ if (!isInfinite) {
1239
+ const canShowSelectAll =
1240
+ withSelectAll ||
1241
+ (entitiesAcrossPages && numRows < entitiesAcrossPages.length);
1242
+ if (canShowSelectAll) {
1243
+ // could all be disabled
1244
+ let atLeastOneRowOnCurrentPageSelected = false;
1245
+ const allRowsOnCurrentPageSelected = entities.every(e => {
1246
+ const rowId = getIdOrCodeOrIndex(e);
1247
+ const selected = idMap[rowId] || isEntityDisabled(e);
1248
+ if (selected) atLeastOneRowOnCurrentPageSelected = true;
1249
+ return selected;
1250
+ });
1251
+ if (
1252
+ atLeastOneRowOnCurrentPageSelected &&
1253
+ allRowsOnCurrentPageSelected
1254
+ ) {
1255
+ let everyEntitySelected;
1256
+ if (isLocalCall) {
1257
+ everyEntitySelected = entitiesAcrossPages.every(e => {
1258
+ const rowId = getIdOrCodeOrIndex(e);
1259
+ return idMap[rowId] || isEntityDisabled(e);
1260
+ });
1261
+ } else {
1262
+ everyEntitySelected = entityCount <= selectedRowCount;
1263
+ }
1264
+ if (everyEntitySelected) {
1265
+ showClearAll = selectedRowCount;
1266
+ }
1267
+ // only show if not all selected
1268
+ showSelectAll = !everyEntitySelected;
1269
+ }
1270
+ }
1271
+ }
1272
+
1273
+ const showNumSelected = !noSelect && !isSingleSelect && !hideSelectedCount;
1274
+ let selectedAndTotalMessage = "";
1275
+ if (showNumSelected) {
1276
+ selectedAndTotalMessage += `${selectedRowCount} Selected `;
1277
+ }
1278
+ if (showCount && showNumSelected) {
1279
+ selectedAndTotalMessage += `/ `;
1280
+ }
1281
+ if (showCount) {
1282
+ selectedAndTotalMessage += `${entityCount || 0} Total`;
1283
+ }
1284
+ if (selectedAndTotalMessage) {
1285
+ selectedAndTotalMessage = <div>{selectedAndTotalMessage}</div>;
1286
+ }
1287
+
1288
+ const shouldShowPaging =
1289
+ !isInfinite &&
1290
+ withPaging &&
1291
+ (hidePageSizeWhenPossible ? entityCount > pageSize : true);
1292
+
1293
+ let SubComponentToUse;
1294
+ if (SubComponent) {
1295
+ SubComponentToUse = row => {
1296
+ let shouldShow = true;
1297
+ if (shouldShowSubComponent) {
1298
+ shouldShow = shouldShowSubComponent(row.original);
1299
+ }
1300
+ if (shouldShow) {
1301
+ return SubComponent(row);
1302
+ }
1303
+ };
1304
+ }
1305
+ let nonDisplayedFilterComp;
1306
+ if (filtersOnNonDisplayedFields.length) {
1307
+ const content = filtersOnNonDisplayedFields.map(
1308
+ ({ displayName, path, selectedFilter, filterValue }) => {
1309
+ let filterValToDisplay = filterValue;
1310
+ if (selectedFilter === "inList") {
1311
+ filterValToDisplay = Array.isArray(filterValToDisplay)
1312
+ ? filterValToDisplay
1313
+ : filterValToDisplay && filterValToDisplay.split(".");
1314
+ }
1315
+ if (Array.isArray(filterValToDisplay)) {
1316
+ filterValToDisplay = filterValToDisplay.join(", ");
1317
+ }
1318
+ return (
1319
+ <div
1320
+ key={displayName || startCase(camelCase(path))}
1321
+ className="tg-filter-on-non-displayed-field"
1322
+ >
1323
+ {displayName || startCase(camelCase(path))}{" "}
1324
+ {lowerCase(selectedFilter)} {filterValToDisplay}
1325
+ </div>
1326
+ );
1327
+ }
1328
+ );
1329
+ nonDisplayedFilterComp = (
1330
+ <div style={{ marginRight: 5, marginLeft: "auto" }}>
1331
+ <Tooltip
1332
+ content={
1333
+ <div>
1334
+ Active filters on hidden columns:
1335
+ <br />
1336
+ <br />
1337
+ {content}
1338
+ </div>
1339
+ }
1340
+ >
1341
+ <Icon icon="filter-list" />
1342
+ </Tooltip>
1343
+ </div>
1344
+ );
1345
+ }
1346
+ let filteredEnts = entities;
1347
+
1348
+ if (onlyShowRowsWErrors) {
1349
+ const rowToErrorMap = {};
1350
+ forEach(reduxFormCellValidation, (err, cellId) => {
1351
+ if (err) {
1352
+ const [rowId] = cellId.split(":");
1353
+ rowToErrorMap[rowId] = true;
1354
+ }
1355
+ });
1356
+ filteredEnts = entities.filter(e => {
1357
+ return rowToErrorMap[e.id];
1358
+ });
1359
+ }
1360
+
1361
+ return (
1362
+ // eslint-disable-next-line no-undef
1363
+ <this.hotkeyEnabler>
1364
+ <div
1365
+ className={classNames(
1366
+ "data-table-container",
1367
+ extraClasses,
1368
+ className,
1369
+ compactClassName,
1370
+ {
1371
+ fullscreen,
1372
+ in_cypress_test: window.Cypress, //tnr: this is a hack to make cypress be able to correctly click the table without the sticky header getting in the way. remove me once https://github.com/cypress-io/cypress/issues/871 is fixed
1373
+ "dt-isViewable": isViewable,
1374
+ "dt-minimalStyle": minimalStyle,
1375
+ "no-padding": noPadding,
1376
+ "hide-column-header": hideColumnHeader
1377
+ }
1378
+ )}
1379
+ >
1380
+ <div
1381
+ className="data-table-container-inner"
1382
+ {...(isCellEditable && {
1383
+ tabIndex: -1,
1384
+ onKeyDown: e => {
1385
+ const isArrowKey =
1386
+ (e.keyCode >= 37 && e.keyCode <= 40) || e.keyCode === 9;
1387
+ if (isArrowKey) {
1388
+ const { schema, entities } = computePresets(this.props);
1389
+ const left = e.keyCode === 37;
1390
+ const up = e.keyCode === 38;
1391
+ const down = e.keyCode === 40;
1392
+ let cellIdToUse = this.getPrimarySelectedCellId();
1393
+ const pathToIndex = getFieldPathToIndex(schema);
1394
+ const entityMap = getEntityIdToEntity(entities);
1395
+ if (!cellIdToUse) return;
1396
+ const {
1397
+ isRect,
1398
+ firstCellIndex,
1399
+ lastCellIndex,
1400
+ lastRowIndex,
1401
+ firstRowIndex
1402
+ } = this.isSelectionARectangle();
1403
+
1404
+ if (isRect) {
1405
+ const [rowId, columnPath] = cellIdToUse.split(":");
1406
+
1407
+ const columnIndex = pathToIndex[columnPath];
1408
+ const indexToPath = invert(pathToIndex);
1409
+ // we want to grab the cell opposite to the primary selected cell
1410
+ if (
1411
+ firstCellIndex === columnIndex &&
1412
+ firstRowIndex === entityMap[rowId]?.i
1413
+ ) {
1414
+ cellIdToUse = `${entities[lastRowIndex].id}:${indexToPath[lastCellIndex]}`;
1415
+ } else if (
1416
+ firstCellIndex === columnIndex &&
1417
+ lastRowIndex === entityMap[rowId]?.i
1418
+ ) {
1419
+ cellIdToUse = `${entities[firstRowIndex].id}:${indexToPath[lastCellIndex]}`;
1420
+ } else if (
1421
+ lastCellIndex === columnIndex &&
1422
+ firstRowIndex === entityMap[rowId]?.i
1423
+ ) {
1424
+ cellIdToUse = `${entities[lastRowIndex].id}:${indexToPath[firstCellIndex]}`;
1425
+ } else {
1426
+ cellIdToUse = `${entities[firstRowIndex].id}:${indexToPath[firstCellIndex]}`;
1427
+ }
1428
+ }
1429
+ if (!cellIdToUse) return;
1430
+ const [rowId, columnPath] = cellIdToUse.split(":");
1431
+ const columnIndex = pathToIndex[columnPath];
1432
+
1433
+ const { i: rowIndex } = entityMap[rowId];
1434
+
1435
+ const {
1436
+ cellIdAbove,
1437
+ cellIdToRight,
1438
+ cellIdBelow,
1439
+ cellIdToLeft
1440
+ } = getCellInfo({
1441
+ columnIndex,
1442
+ columnPath,
1443
+ rowId,
1444
+ schema,
1445
+ entities,
1446
+ rowIndex,
1447
+ isEntityDisabled,
1448
+ entity: entityMap[rowId].e
1449
+ });
1450
+ const nextCellId = up
1451
+ ? cellIdAbove
1452
+ : down
1453
+ ? cellIdBelow
1454
+ : left
1455
+ ? cellIdToLeft
1456
+ : cellIdToRight;
1457
+
1458
+ e.stopPropagation();
1459
+ e.preventDefault();
1460
+ if (!nextCellId) return;
1461
+ // this.handleCellBlur();
1462
+ // this.finishCellEdit
1463
+ if (
1464
+ document.activeElement?.parentElement?.classList.contains(
1465
+ "rt-td"
1466
+ )
1467
+ ) {
1468
+ document.activeElement.blur();
1469
+ }
1470
+ this.handleCellClick({
1471
+ event: e,
1472
+ cellId: nextCellId
1473
+ });
1474
+ }
1475
+ if (e.metaKey || e.ctrlKey || e.altKey) return;
1476
+ const cellId = this.getPrimarySelectedCellId();
1477
+ if (!cellId) return;
1478
+ const entityIdToEntity = getEntityIdToEntity(entities);
1479
+ const [rowId] = cellId.split(":");
1480
+ if (!rowId) return;
1481
+ const entity = entityIdToEntity[rowId].e;
1482
+ if (!entity) return;
1483
+ const rowDisabled = isEntityDisabled(entity);
1484
+ const isNum = e.keyCode >= 48 && e.keyCode <= 57;
1485
+ const isLetter = e.keyCode >= 65 && e.keyCode <= 90;
1486
+ if (!isNum && !isLetter) return;
1487
+ if (rowDisabled) return;
1488
+ this.startCellEdit(cellId, { shouldSelectAll: true });
1489
+ e.stopPropagation();
1490
+ // e.preventDefault();
1491
+ }
1492
+ })}
1493
+ >
1494
+ {isCellEditable && entities.length > 50 && (
1495
+ <SwitchField
1496
+ name="onlyShowRowsWErrors"
1497
+ inlineLabel={true}
1498
+ label="Only Show Rows With Errors"
1499
+ />
1500
+ )}
1501
+ {showHeader && (
1502
+ <div className="data-table-header">
1503
+ <div className="data-table-title-and-buttons">
1504
+ {tableName && withTitle && (
1505
+ <span className="data-table-title">{tableName}</span>
1506
+ )}
1507
+ {children}
1508
+ {topLeftItems}
1509
+ </div>
1510
+ {errorParsingUrlString && (
1511
+ <Callout
1512
+ icon="error"
1513
+ style={{
1514
+ width: "unset"
1515
+ }}
1516
+ intent={Intent.WARNING}
1517
+ >
1518
+ Error parsing URL
1519
+ </Callout>
1520
+ )}
1521
+ {nonDisplayedFilterComp}
1522
+ {withSearch && (
1523
+ <div className="data-table-search-and-clear-filter-container">
1524
+ {leftOfSearchBarItems}
1525
+ {hasFilters ? (
1526
+ <Tooltip content="Clear Filters">
1527
+ <Button
1528
+ minimal
1529
+ intent="danger"
1530
+ icon="filter-remove"
1531
+ disabled={disabled}
1532
+ className="data-table-clear-filters"
1533
+ onClick={() => {
1534
+ clearFilters(additionalFilterKeys);
1535
+ }}
1536
+ />
1537
+ </Tooltip>
1538
+ ) : (
1539
+ ""
1540
+ )}
1541
+ <SearchBar
1542
+ {...{
1543
+ reduxFormSearchInput,
1544
+ setSearchTerm,
1545
+ loading: isLoading,
1546
+ searchMenuButton,
1547
+ disabled,
1548
+ autoFocusSearch
1549
+ }}
1550
+ />
1551
+ </div>
1552
+ )}
1553
+ </div>
1554
+ )}
1555
+ {subHeader}
1556
+ {showSelectAll && !isSingleSelect && (
1557
+ <div
1558
+ style={{
1559
+ marginTop: 5,
1560
+ marginBottom: 5,
1561
+ display: "flex",
1562
+ alignItems: "center"
1563
+ }}
1564
+ >
1565
+ All items on this page are selected.{" "}
1566
+ <Button
1567
+ small
1568
+ minimal
1569
+ intent="primary"
1570
+ text={`Select all ${entityCount ||
1571
+ entitiesAcrossPages.length} items`}
1572
+ loading={this.state.selectingAll}
1573
+ onClick={async () => {
1574
+ if (withSelectAll) {
1575
+ // this will be by querying for everything
1576
+ this.setState({
1577
+ selectingAll: true
1578
+ });
1579
+ try {
1580
+ const allEntities = await safeQuery(fragment, {
1581
+ variables: {
1582
+ filter: variables.filter,
1583
+ sort: variables.sort
1584
+ },
1585
+ canCancel: true
1586
+ });
1587
+ this.addEntitiesToSelection(allEntities);
1588
+ } catch (error) {
1589
+ console.error(`error:`, error);
1590
+ window.toastr.error("Error selecting all constructs");
1591
+ }
1592
+ this.setState({
1593
+ selectingAll: false
1594
+ });
1595
+ } else {
1596
+ this.addEntitiesToSelection(entitiesAcrossPages);
1597
+ }
1598
+ }}
1599
+ />
1600
+ </div>
1601
+ )}
1602
+ {showClearAll > 0 && (
1603
+ <div
1604
+ style={{
1605
+ marginTop: 5,
1606
+ marginBottom: 5,
1607
+ display: "flex",
1608
+ alignItems: "center"
1609
+ }}
1610
+ >
1611
+ All {showClearAll} items are selected.{" "}
1612
+ <Button
1613
+ small
1614
+ minimal
1615
+ intent="primary"
1616
+ text="Clear Selection"
1617
+ onClick={() => {
1618
+ finalizeSelection({
1619
+ idMap: {},
1620
+ entities,
1621
+ props: computePresets(this.props)
1622
+ });
1623
+ }}
1624
+ />
1625
+ </div>
1626
+ )}
1627
+ <ReactTable
1628
+ data={filteredEnts}
1629
+ ref={n => {
1630
+ if (n) this.table = n;
1631
+ }}
1632
+ additionalBodyEl={
1633
+ isCellEditable &&
1634
+ !onlyShowRowsWErrors && (
1635
+ <Button
1636
+ icon="add"
1637
+ style={{ marginTop: "auto" }}
1638
+ onClick={() => {
1639
+ this.insertRows({ numRows: 10, appendToBottom: true });
1640
+ }}
1641
+ minimal
1642
+ >
1643
+ Add 10 Rows
1644
+ </Button>
1645
+ )
1646
+ }
1647
+ className={classNames({
1648
+ isCellEditable,
1649
+ "tg-table-loading": isLoading,
1650
+ "tg-table-disabled": disabled
1651
+ })}
1652
+ itemSizeEstimator={
1653
+ extraCompact
1654
+ ? itemSizeEstimators.compact
1655
+ : compact
1656
+ ? itemSizeEstimators.normal
1657
+ : itemSizeEstimators.comfortable
1658
+ }
1659
+ TfootComponent={() => {
1660
+ return <button>hasdfasdf</button>;
1661
+ }}
1662
+ columns={this.renderColumns()}
1663
+ pageSize={rowsToShow}
1664
+ expanded={expandedRows}
1665
+ showPagination={false}
1666
+ sortable={false}
1667
+ loading={isLoading || disabled}
1668
+ defaultResized={resized}
1669
+ onResizedChange={(newResized = []) => {
1670
+ const resizedToUse = newResized.map(column => {
1671
+ // have a min width of 50 so that columns don't disappear
1672
+ if (column.value < 50) {
1673
+ return {
1674
+ ...column,
1675
+ value: 50
1676
+ };
1677
+ } else {
1678
+ return column;
1679
+ }
1680
+ });
1681
+ resizePersist(resizedToUse);
1682
+ }}
1683
+ TheadComponent={this.getTheadComponent}
1684
+ ThComponent={this.getThComponent}
1685
+ getTrGroupProps={this.getTableRowProps}
1686
+ getTdProps={this.getTableCellProps}
1687
+ NoDataComponent={({ children }) =>
1688
+ isLoading ? null : (
1689
+ <div className="rt-noData">
1690
+ {noRowsFoundMessage || children}
1691
+ </div>
1692
+ )
1693
+ }
1694
+ LoadingComponent={props => (
1695
+ <DisabledLoadingComponent {...{ ...props, disabled }} />
1696
+ )}
1697
+ style={{
1698
+ maxHeight,
1699
+ minHeight: 150,
1700
+ ...style
1701
+ }}
1702
+ SubComponent={SubComponentToUse}
1703
+ {...ReactTableProps}
1704
+ />
1705
+
1706
+ {!noFooter && (
1707
+ <div
1708
+ className="data-table-footer"
1709
+ style={{
1710
+ justifyContent:
1711
+ !showNumSelected && !showCount
1712
+ ? "flex-end"
1713
+ : "space-between"
1714
+ }}
1715
+ >
1716
+ {selectedAndTotalMessage}
1717
+ <div style={{ display: "flex", flexWrap: "wrap" }}>
1718
+ {additionalFooterButtons}
1719
+ {!noFullscreenButton && toggleFullscreenButton}
1720
+ {withDisplayOptions && (
1721
+ <DisplayOptions
1722
+ compact={compact}
1723
+ extraCompact={extraCompact}
1724
+ disabled={disabled}
1725
+ hideDisplayOptionsIcon={hideDisplayOptionsIcon}
1726
+ resetDefaultVisibility={resetDefaultVisibilityToUse}
1727
+ updateColumnVisibility={updateColumnVisibilityToUse}
1728
+ updateTableDisplayDensity={updateTableDisplayDensityToUse}
1729
+ showForcedHiddenColumns={showForcedHiddenColumns}
1730
+ setShowForcedHidden={setShowForcedHidden}
1731
+ hasOptionForForcedHidden={hasOptionForForcedHidden}
1732
+ formName={formName}
1733
+ schema={schema}
1734
+ />
1735
+ )}
1736
+ {shouldShowPaging && (
1737
+ <PagingTool
1738
+ {...propPresets}
1739
+ persistPageSize={persistPageSizeToUse}
1740
+ />
1741
+ )}
1742
+ </div>
1743
+ </div>
1744
+ )}
1745
+ </div>
1746
+ </div>
1747
+ </this.hotkeyEnabler>
1748
+ );
1749
+ }
1750
+
1751
+ getTableRowProps = (state, rowInfo) => {
1752
+ const {
1753
+ reduxFormSelectedEntityIdMap,
1754
+ reduxFormExpandedEntityIdMap,
1755
+ withCheckboxes,
1756
+ onDoubleClick,
1757
+ history,
1758
+ mustClickCheckboxToSelect,
1759
+ entities,
1760
+ isEntityDisabled,
1761
+ change,
1762
+ getRowClassName,
1763
+ isCellEditable
1764
+ } = computePresets(this.props);
1765
+ if (!rowInfo) {
1766
+ return {
1767
+ className: "no-row-data"
1768
+ };
1769
+ }
1770
+ const entity = rowInfo.original;
1771
+ const rowId = getIdOrCodeOrIndex(entity, rowInfo.index);
1772
+ const rowSelected = reduxFormSelectedEntityIdMap[rowId];
1773
+ const isExpanded = reduxFormExpandedEntityIdMap[rowId];
1774
+ const rowDisabled = isEntityDisabled(entity);
1775
+ const dataId = entity.id || entity.code;
1776
+ return {
1777
+ onClick: e => {
1778
+ if (isCellEditable) return;
1779
+ // if checkboxes are activated or row expander is clicked don't select row
1780
+ if (e.target.matches(".tg-expander, .tg-expander *")) {
1781
+ change("reduxFormExpandedEntityIdMap", {
1782
+ ...reduxFormExpandedEntityIdMap,
1783
+ [rowId]: !isExpanded
1784
+ });
1785
+ return;
1786
+ } else if (
1787
+ e.target.closest(".tg-react-table-checkbox-cell-container")
1788
+ ) {
1789
+ return;
1790
+ } else if (mustClickCheckboxToSelect) {
1791
+ return;
1792
+ }
1793
+ if (e.detail > 1) {
1794
+ return; //cancel multiple quick clicks
1795
+ }
1796
+ rowClick(e, rowInfo, entities, computePresets(this.props));
1797
+ },
1798
+ //row right click
1799
+ onContextMenu: e => {
1800
+ e.preventDefault();
1801
+ if (rowId === undefined || rowDisabled || isCellEditable) return;
1802
+ const oldIdMap = cloneDeep(reduxFormSelectedEntityIdMap) || {};
1803
+ let newIdMap;
1804
+ if (withCheckboxes) {
1805
+ newIdMap = oldIdMap;
1806
+ } else {
1807
+ // if we are not using checkboxes we need to make sure
1808
+ // that the id of the record gets added to the id map
1809
+ newIdMap = oldIdMap[rowId] ? oldIdMap : { [rowId]: { entity } };
1810
+
1811
+ // tgreen: this will refresh the selection with fresh data. The entities in redux might not be up to date
1812
+ const keyedEntities = keyBy(entities, getIdOrCodeOrIndex);
1813
+ forEach(newIdMap, (val, key) => {
1814
+ const freshEntity = keyedEntities[key];
1815
+ if (freshEntity) {
1816
+ newIdMap[key] = { ...newIdMap[key], entity: freshEntity };
1817
+ }
1818
+ });
1819
+
1820
+ finalizeSelection({
1821
+ idMap: newIdMap,
1822
+ entities,
1823
+ props: computePresets(this.props)
1824
+ });
1825
+ }
1826
+ this.showContextMenu(e, newIdMap);
1827
+ },
1828
+ className: classNames(
1829
+ "with-row-data",
1830
+ getRowClassName && getRowClassName(rowInfo, state, this.props),
1831
+ {
1832
+ disabled: rowDisabled,
1833
+ selected: rowSelected && !withCheckboxes
1834
+ }
1835
+ ),
1836
+ "data-test-id": dataId === undefined ? rowInfo.index : dataId,
1837
+ "data-index": rowInfo.index,
1838
+ onDoubleClick: e => {
1839
+ if (rowDisabled) return;
1840
+ this.dblClickTriggered = true;
1841
+ onDoubleClick &&
1842
+ onDoubleClick(rowInfo.original, rowInfo.index, history, e);
1843
+ }
1844
+ };
1845
+ };
1846
+
1847
+ startCellEdit = (cellId, { shouldSelectAll } = {}) => {
1848
+ const {
1849
+ change,
1850
+ reduxFormSelectedCells = {},
1851
+ reduxFormEditingCell
1852
+ } = computePresets(this.props);
1853
+ const newSelectedCells = { ...reduxFormSelectedCells };
1854
+ newSelectedCells[cellId] = PRIMARY_SELECTED_VAL;
1855
+ //check if the cell is already selected and editing and if so, don't change it
1856
+ if (reduxFormEditingCell === cellId) return;
1857
+ change("reduxFormSelectedCells", newSelectedCells);
1858
+ change("reduxFormEditingCell", cellId);
1859
+ if (shouldSelectAll) {
1860
+ //we should select the text
1861
+ change("reduxFormEditingCellSelectAll", true);
1862
+ }
1863
+ };
1864
+
1865
+ getTableCellProps = (state, rowInfo, column) => {
1866
+ const {
1867
+ entities,
1868
+ schema,
1869
+ doNotValidateUntouchedRows,
1870
+ reduxFormEditingCell,
1871
+ isCellEditable,
1872
+ reduxFormCellValidation,
1873
+ reduxFormSelectedCells = {},
1874
+ isEntityDisabled,
1875
+ change
1876
+ } = computePresets(this.props);
1877
+ if (!isCellEditable) return {}; //only allow cell selection to do stuff here
1878
+ if (!rowInfo) return {};
1879
+ const entity = rowInfo.original;
1880
+ const rowIndex = rowInfo.index;
1881
+ const rowId = getIdOrCodeOrIndex(entity, rowIndex);
1882
+ const {
1883
+ cellId,
1884
+ cellIdAbove,
1885
+ cellIdToRight,
1886
+ cellIdBelow,
1887
+ cellIdToLeft,
1888
+ rowDisabled,
1889
+ columnIndex
1890
+ } = getCellInfo({
1891
+ columnIndex: column.index,
1892
+ columnPath: column.path,
1893
+ rowId,
1894
+ schema,
1895
+ entities,
1896
+ rowIndex,
1897
+ isEntityDisabled,
1898
+ entity
1899
+ });
1900
+ const _isClean = entity._isClean && doNotValidateUntouchedRows;
1901
+
1902
+ const err = !_isClean && reduxFormCellValidation[cellId];
1903
+ let selectedTopBorder,
1904
+ selectedRightBorder,
1905
+ selectedBottomBorder,
1906
+ selectedLeftBorder;
1907
+ if (reduxFormSelectedCells[cellId]) {
1908
+ selectedTopBorder = !reduxFormSelectedCells[cellIdAbove];
1909
+ selectedRightBorder = !reduxFormSelectedCells[cellIdToRight];
1910
+ selectedBottomBorder = !reduxFormSelectedCells[cellIdBelow];
1911
+ selectedLeftBorder = !reduxFormSelectedCells[cellIdToLeft];
1912
+ }
1913
+ const isPrimarySelected =
1914
+ reduxFormSelectedCells[cellId] === PRIMARY_SELECTED_VAL;
1915
+ const className = classNames({
1916
+ isSelectedCell: reduxFormSelectedCells[cellId],
1917
+ isPrimarySelected,
1918
+ isSecondarySelected: reduxFormSelectedCells[cellId] === true,
1919
+ noSelectedTopBorder: !selectedTopBorder,
1920
+ isCleanRow: _isClean,
1921
+ noSelectedRightBorder: !selectedRightBorder,
1922
+ noSelectedBottomBorder: !selectedBottomBorder,
1923
+ noSelectedLeftBorder: !selectedLeftBorder,
1924
+ isDropdownCell: column.type === "dropdown",
1925
+ isEditingCell: reduxFormEditingCell === cellId,
1926
+ hasCellError: !!err,
1927
+ "no-data-tip": reduxFormSelectedCells[cellId]
1928
+ });
1929
+ return {
1930
+ onDoubleClick: () => {
1931
+ // cell double click
1932
+ if (rowDisabled) return;
1933
+ this.startCellEdit(cellId);
1934
+ },
1935
+ ...(err && {
1936
+ "data-tip": err?.message || err
1937
+ }),
1938
+ onContextMenu: e => {
1939
+ if (!isPrimarySelected) {
1940
+ const primaryCellId = this.getPrimarySelectedCellId();
1941
+ const newSelectedCells = { ...reduxFormSelectedCells };
1942
+ if (primaryCellId) {
1943
+ newSelectedCells[primaryCellId] = true;
1944
+ }
1945
+ newSelectedCells[cellId] = PRIMARY_SELECTED_VAL;
1946
+ change("reduxFormSelectedCells", newSelectedCells);
1947
+ }
1948
+ setTimeout(() => {
1949
+ //need a timeout so reduxFormSelectedCells is up to date in the context menu
1950
+ this.showContextMenu(e);
1951
+ }, 0);
1952
+ },
1953
+ onClick: event => {
1954
+ this.handleCellClick({
1955
+ event,
1956
+ cellId,
1957
+ rowDisabled,
1958
+ rowIndex,
1959
+ columnIndex
1960
+ });
1961
+ },
1962
+ className
1963
+ };
1964
+ };
1965
+
1966
+ handleCellClick = ({ event, cellId }) => {
1967
+ if (!cellId) return;
1968
+ // cell click, cellclick
1969
+ const {
1970
+ entities,
1971
+ schema,
1972
+ change,
1973
+ reduxFormEditingCell,
1974
+ reduxFormSelectedCells = {},
1975
+ isEntityDisabled
1976
+ } = computePresets(this.props);
1977
+ const [rowId, cellPath] = cellId.split(":");
1978
+ const entityMap = getEntityIdToEntity(entities);
1979
+ const { e: entity, i: rowIndex } = entityMap[rowId];
1980
+ const pathToIndex = getFieldPathToIndex(schema);
1981
+ const columnIndex = pathToIndex[cellPath];
1982
+ const rowDisabled = isEntityDisabled(entity);
1983
+
1984
+ if (rowDisabled) return;
1985
+ let newSelectedCells = {
1986
+ ...reduxFormSelectedCells
1987
+ };
1988
+ if (newSelectedCells[cellId] && !event.shiftKey) {
1989
+ // don't deselect if editing
1990
+ if (reduxFormEditingCell === cellId) return;
1991
+ if (event.metaKey) {
1992
+ delete newSelectedCells[cellId];
1993
+ } else {
1994
+ newSelectedCells = {};
1995
+ newSelectedCells[cellId] = PRIMARY_SELECTED_VAL;
1996
+ }
1997
+ } else {
1998
+ const primarySelectedCellId = this.getPrimarySelectedCellId();
1999
+ if (event.metaKey) {
2000
+ if (isEmpty(newSelectedCells)) {
2001
+ newSelectedCells[cellId] = PRIMARY_SELECTED_VAL;
2002
+ } else {
2003
+ newSelectedCells[cellId] = PRIMARY_SELECTED_VAL;
2004
+ if (primarySelectedCellId)
2005
+ newSelectedCells[primarySelectedCellId] = true;
2006
+ }
2007
+ } else if (event.shiftKey) {
2008
+ if (primarySelectedCellId) {
2009
+ const [rowId, colPath] = primarySelectedCellId.split(":");
2010
+ const primaryRowIndex = entities.findIndex((e, i) => {
2011
+ return getIdOrCodeOrIndex(e, i) === rowId;
2012
+ });
2013
+ const fieldToIndex = getFieldPathToIndex(schema);
2014
+ const primaryColIndex = fieldToIndex[colPath];
2015
+
2016
+ if (primaryRowIndex === -1 || primaryColIndex === -1) {
2017
+ newSelectedCells = {};
2018
+ newSelectedCells[cellId] = PRIMARY_SELECTED_VAL;
2019
+ } else {
2020
+ const minRowIndex = min([primaryRowIndex, rowIndex]);
2021
+ const minColIndex = min([primaryColIndex, columnIndex]);
2022
+ const maxRowIndex = max([primaryRowIndex, rowIndex]);
2023
+ const maxColIndex = max([primaryColIndex, columnIndex]);
2024
+ const entitiesBetweenRows = entities.slice(
2025
+ minRowIndex,
2026
+ maxRowIndex + 1
2027
+ );
2028
+ const fieldsBetweenCols = schema.fields.slice(
2029
+ minColIndex,
2030
+ maxColIndex + 1
2031
+ );
2032
+ newSelectedCells = {
2033
+ [primarySelectedCellId]: PRIMARY_SELECTED_VAL
2034
+ };
2035
+ entitiesBetweenRows.forEach(e => {
2036
+ const rowId = getIdOrCodeOrIndex(e, entities.indexOf(e));
2037
+ fieldsBetweenCols.forEach(f => {
2038
+ const cellId = `${rowId}:${f.path}`;
2039
+ if (!newSelectedCells[cellId]) newSelectedCells[cellId] = true;
2040
+ });
2041
+ });
2042
+ // newSelectedCells[cellId] = PRIMARY_SELECTED_VAL;
2043
+ // newSelectedCells[primarySelectedCellId] = true;
2044
+ }
2045
+ } else {
2046
+ newSelectedCells = {};
2047
+ newSelectedCells[cellId] = PRIMARY_SELECTED_VAL;
2048
+ }
2049
+ } else {
2050
+ newSelectedCells = {};
2051
+ newSelectedCells[cellId] = PRIMARY_SELECTED_VAL;
2052
+ }
2053
+ }
2054
+
2055
+ change("reduxFormSelectedCells", newSelectedCells);
2056
+ };
2057
+ renderCheckboxHeader = () => {
2058
+ const {
2059
+ reduxFormSelectedEntityIdMap,
2060
+ isSingleSelect,
2061
+ noSelect,
2062
+ noUserSelect,
2063
+ entities,
2064
+ isEntityDisabled
2065
+ } = computePresets(this.props);
2066
+ const checkedRows = getSelectedRowsFromEntities(
2067
+ entities,
2068
+ reduxFormSelectedEntityIdMap
2069
+ );
2070
+ const checkboxProps = {
2071
+ checked: false,
2072
+ indeterminate: false
2073
+ };
2074
+ const notDisabledEntityCount = entities.reduce((acc, e) => {
2075
+ return isEntityDisabled(e) ? acc : acc + 1;
2076
+ }, 0);
2077
+ if (checkedRows.length === notDisabledEntityCount) {
2078
+ //tnr: maybe this will need to change if we want enable select all across pages
2079
+ checkboxProps.checked = notDisabledEntityCount !== 0;
2080
+ } else {
2081
+ if (checkedRows.length) {
2082
+ checkboxProps.indeterminate = true;
2083
+ }
2084
+ }
2085
+
2086
+ return !isSingleSelect ? (
2087
+ <Checkbox
2088
+ disabled={noSelect || noUserSelect}
2089
+ /* eslint-disable react/jsx-no-bind */
2090
+ onChange={() => {
2091
+ const newIdMap = cloneDeep(reduxFormSelectedEntityIdMap) || {};
2092
+ entities.forEach((entity, i) => {
2093
+ if (isEntityDisabled(entity)) return;
2094
+ const entityId = getIdOrCodeOrIndex(entity, i);
2095
+ if (checkboxProps.checked) {
2096
+ delete newIdMap[entityId];
2097
+ } else {
2098
+ newIdMap[entityId] = { entity };
2099
+ }
2100
+ });
2101
+
2102
+ finalizeSelection({
2103
+ idMap: newIdMap,
2104
+ entities,
2105
+ props: computePresets(this.props)
2106
+ });
2107
+ }}
2108
+ /* eslint-enable react/jsx-no-bind */
2109
+ {...checkboxProps}
2110
+ />
2111
+ ) : null;
2112
+ };
2113
+
2114
+ renderCheckboxCell = row => {
2115
+ const rowIndex = row.index;
2116
+ const {
2117
+ reduxFormSelectedEntityIdMap,
2118
+ noSelect,
2119
+ noUserSelect,
2120
+ entities,
2121
+ isEntityDisabled
2122
+ } = computePresets(this.props);
2123
+ const checkedRows = getSelectedRowsFromEntities(
2124
+ entities,
2125
+ reduxFormSelectedEntityIdMap
2126
+ );
2127
+
2128
+ const isSelected = checkedRows.some(rowNum => {
2129
+ return rowNum === rowIndex;
2130
+ });
2131
+ if (rowIndex >= entities.length) {
2132
+ return <div />;
2133
+ }
2134
+ const entity = entities[rowIndex];
2135
+ return (
2136
+ <Checkbox
2137
+ disabled={noSelect || noUserSelect || isEntityDisabled(entity)}
2138
+ onClick={e => {
2139
+ rowClick(e, row, entities, computePresets(this.props));
2140
+ }}
2141
+ checked={isSelected}
2142
+ />
2143
+ );
2144
+ };
2145
+
2146
+ finishCellEdit = (cellId, newVal, doNotStopEditing) => {
2147
+ const {
2148
+ entities = [],
2149
+ change,
2150
+ schema,
2151
+ reduxFormCellValidation
2152
+ } = computePresets(this.props);
2153
+ const [rowId, path] = cellId.split(":");
2154
+ !doNotStopEditing && change("reduxFormEditingCell", null);
2155
+ this.updateEntitiesHelper(entities, entities => {
2156
+ const entity = entities.find((e, i) => {
2157
+ return getIdOrCodeOrIndex(e, i) === rowId;
2158
+ });
2159
+ delete entity._isClean;
2160
+ const { error } = editCellHelper({
2161
+ entity,
2162
+ path,
2163
+ schema,
2164
+ newVal
2165
+ });
2166
+ this.updateValidation(entities, {
2167
+ ...reduxFormCellValidation,
2168
+ [cellId]: error
2169
+ });
2170
+ });
2171
+ !doNotStopEditing && this.refocusTable();
2172
+ };
2173
+
2174
+ cancelCellEdit = () => {
2175
+ const { change } = computePresets(this.props);
2176
+ change("reduxFormEditingCell", null);
2177
+ this.refocusTable();
2178
+ };
2179
+ refocusTable = () => {
2180
+ setTimeout(() => {
2181
+ const table = ReactDOM.findDOMNode(this.table)?.closest(
2182
+ ".data-table-container>div"
2183
+ );
2184
+ table?.focus();
2185
+ }, 0);
2186
+ };
2187
+
2188
+ isSelectionARectangle = () => {
2189
+ const { entities, reduxFormSelectedCells, schema } = computePresets(
2190
+ this.props
2191
+ );
2192
+ if (
2193
+ reduxFormSelectedCells &&
2194
+ Object.keys(reduxFormSelectedCells).length > 1
2195
+ ) {
2196
+ const pathToIndex = getFieldPathToIndex(schema);
2197
+ const entityMap = getEntityIdToEntity(entities);
2198
+ // let primaryCellId;
2199
+ let selectionGrid = [];
2200
+ let firstCellIndex;
2201
+ let lastCellIndex;
2202
+ let lastRowIndex;
2203
+ let firstRowIndex;
2204
+ const selectedPaths = [];
2205
+ Object.keys(reduxFormSelectedCells).forEach(key => {
2206
+ // if (reduxFormSelectedCells[key] === PRIMARY_SELECTED_VAL) {
2207
+ // primaryCellId = key;
2208
+ // }
2209
+ const [rowId, cellPath] = key.split(":");
2210
+ if (!selectedPaths.includes(cellPath)) selectedPaths.push(cellPath);
2211
+ const cellIndex = pathToIndex[cellPath];
2212
+ const ent = entityMap[rowId];
2213
+ if (!ent) return;
2214
+ const { i } = ent;
2215
+ if (firstRowIndex === undefined || i < firstRowIndex) {
2216
+ firstRowIndex = i;
2217
+ }
2218
+ if (lastRowIndex === undefined || i > lastRowIndex) {
2219
+ lastRowIndex = i;
2220
+ }
2221
+ if (!selectionGrid[i]) selectionGrid[i] = [];
2222
+ selectionGrid[i][cellIndex] = { cellId: key, rowIndex: i, cellIndex };
2223
+ if (firstCellIndex === undefined || cellIndex < firstCellIndex) {
2224
+ firstCellIndex = cellIndex;
2225
+ }
2226
+ if (lastCellIndex === undefined || cellIndex > lastCellIndex) {
2227
+ lastCellIndex = cellIndex;
2228
+ }
2229
+ });
2230
+ selectionGrid = selectionGrid.slice(firstRowIndex);
2231
+ let isRectangle = true;
2232
+ for (let i = 0; i < selectionGrid.length; i++) {
2233
+ const row = selectionGrid[i];
2234
+ if (!row) {
2235
+ isRectangle = false;
2236
+ break;
2237
+ } else {
2238
+ for (let j = firstCellIndex; j < row.length; j++) {
2239
+ if (!row[j]) {
2240
+ isRectangle = false;
2241
+ break;
2242
+ }
2243
+ }
2244
+ }
2245
+ }
2246
+
2247
+ if (isRectangle) {
2248
+ return {
2249
+ isRect: true,
2250
+ selectedPaths,
2251
+ selectionGrid,
2252
+ lastRowIndex,
2253
+ lastCellIndex,
2254
+ firstCellIndex,
2255
+ firstRowIndex,
2256
+ entityMap,
2257
+ pathToIndex
2258
+ };
2259
+ } else {
2260
+ return {};
2261
+ }
2262
+ }
2263
+ return {};
2264
+ };
2265
+
2266
+ renderColumns = () => {
2267
+ const {
2268
+ isCellEditable,
2269
+ cellRenderer,
2270
+ withCheckboxes,
2271
+ SubComponent,
2272
+ shouldShowSubComponent,
2273
+ entities,
2274
+ reduxFormEditingCellSelectAll,
2275
+ isEntityDisabled,
2276
+ getCellHoverText,
2277
+ withExpandAndCollapseAllButton,
2278
+ reduxFormExpandedEntityIdMap,
2279
+ change,
2280
+ reduxFormSelectedCells,
2281
+ reduxFormEditingCell
2282
+ } = computePresets(this.props);
2283
+ const { columns } = this.state;
2284
+ if (!columns.length) {
2285
+ return columns;
2286
+ }
2287
+ const columnsToRender = [];
2288
+ if (SubComponent) {
2289
+ columnsToRender.push({
2290
+ ...(withExpandAndCollapseAllButton && {
2291
+ Header: () => {
2292
+ const showCollapseAll =
2293
+ Object.values(reduxFormExpandedEntityIdMap).filter(i => i)
2294
+ .length === entities.length;
2295
+ return (
2296
+ <InfoHelper
2297
+ content={showCollapseAll ? "Collapse All" : "Expand All"}
2298
+ isButton
2299
+ minimal
2300
+ small
2301
+ style={{ padding: 2 }}
2302
+ popoverProps={{
2303
+ modifiers: {
2304
+ preventOverflow: { enabled: false },
2305
+ hide: { enabled: false }
2306
+ }
2307
+ }}
2308
+ onClick={() => {
2309
+ showCollapseAll
2310
+ ? change("reduxFormExpandedEntityIdMap", {})
2311
+ : change(
2312
+ "reduxFormExpandedEntityIdMap",
2313
+ entities.reduce((acc, e) => {
2314
+ acc[e.id] = true;
2315
+ return acc;
2316
+ }, {})
2317
+ );
2318
+ }}
2319
+ className={classNames("tg-expander-all")}
2320
+ icon={showCollapseAll ? "chevron-down" : "chevron-right"}
2321
+ />
2322
+ );
2323
+ }
2324
+ }),
2325
+ expander: true,
2326
+ Expander: ({ isExpanded, original: record }) => {
2327
+ let shouldShow = true;
2328
+ if (shouldShowSubComponent) {
2329
+ shouldShow = shouldShowSubComponent(record);
2330
+ }
2331
+ if (!shouldShow) return null;
2332
+ return (
2333
+ <Button
2334
+ className={classNames(
2335
+ "tg-expander",
2336
+ Classes.MINIMAL,
2337
+ Classes.SMALL
2338
+ )}
2339
+ icon={isExpanded ? "chevron-down" : "chevron-right"}
2340
+ />
2341
+ );
2342
+ }
2343
+ });
2344
+ }
2345
+
2346
+ if (withCheckboxes) {
2347
+ columnsToRender.push({
2348
+ Header: this.renderCheckboxHeader,
2349
+ Cell: this.renderCheckboxCell,
2350
+ width: 35,
2351
+ resizable: false,
2352
+ getHeaderProps: () => {
2353
+ return {
2354
+ className: "tg-react-table-checkbox-header-container",
2355
+ immovable: "true"
2356
+ };
2357
+ },
2358
+ getProps: () => {
2359
+ return {
2360
+ className: "tg-react-table-checkbox-cell-container"
2361
+ };
2362
+ }
2363
+ });
2364
+ }
2365
+
2366
+ columns.forEach(column => {
2367
+ const tableColumn = {
2368
+ ...column,
2369
+ Header: this.renderColumnHeader(column),
2370
+ accessor: column.path,
2371
+ getHeaderProps: () => ({
2372
+ // needs to be a string because it is getting passed
2373
+ // to the dom
2374
+ immovable: column.immovable ? "true" : "false",
2375
+ columnindex: column.columnIndex
2376
+ })
2377
+ };
2378
+ let noEllipsis = column.noEllipsis;
2379
+ if (column.width) {
2380
+ tableColumn.width = column.width;
2381
+ }
2382
+ if (cellRenderer && cellRenderer[column.path]) {
2383
+ tableColumn.Cell = row => {
2384
+ const val = cellRenderer[column.path](
2385
+ row.value,
2386
+ row.original,
2387
+ row,
2388
+ this.props
2389
+ );
2390
+ return val;
2391
+ };
2392
+ } else if (column.render) {
2393
+ tableColumn.Cell = row => {
2394
+ const val = column.render(row.value, row.original, row, this.props);
2395
+ return val;
2396
+ };
2397
+ } else if (column.type === "timestamp") {
2398
+ tableColumn.Cell = props => {
2399
+ return props.value ? dayjs(props.value).format("lll") : "";
2400
+ };
2401
+ } else if (column.type === "color") {
2402
+ tableColumn.Cell = props => {
2403
+ return props.value ? (
2404
+ <div
2405
+ style={{
2406
+ height: 20,
2407
+ width: 40,
2408
+ background: props.value,
2409
+ border: "1px solid #182026",
2410
+ borderRadius: 5
2411
+ }}
2412
+ />
2413
+ ) : (
2414
+ ""
2415
+ );
2416
+ };
2417
+ } else if (column.type === "boolean") {
2418
+ if (isCellEditable) {
2419
+ tableColumn.Cell = props => (props.value ? "True" : "False");
2420
+ } else {
2421
+ tableColumn.Cell = props => (
2422
+ <Icon
2423
+ className={classNames({
2424
+ [Classes.TEXT_MUTED]: !props.value
2425
+ })}
2426
+ icon={props.value ? "tick" : "cross"}
2427
+ />
2428
+ );
2429
+ }
2430
+ } else if (column.type === "markdown") {
2431
+ tableColumn.Cell = props => (
2432
+ <ReactMarkdown remarkPlugins={[remarkGfm]}>
2433
+ {props.value}
2434
+ </ReactMarkdown>
2435
+ );
2436
+ } else {
2437
+ tableColumn.Cell = props => props.value;
2438
+ }
2439
+ const oldFunc = tableColumn.Cell;
2440
+
2441
+ tableColumn.Cell = (...args) => {
2442
+ const [row] = args;
2443
+ const rowId = getIdOrCodeOrIndex(row.original, row.index);
2444
+ const cellId = `${rowId}:${row.column.path}`;
2445
+ let val = oldFunc(...args);
2446
+ const oldVal = val;
2447
+ const text = this.getCopyTextForCell(val, row, column);
2448
+ const isBool = column.type === "boolean";
2449
+ if (isCellEditable && isBool) {
2450
+ val = (
2451
+ <Checkbox
2452
+ disabled={isEntityDisabled(row.original)}
2453
+ className="tg-cell-edit-boolean-checkbox"
2454
+ checked={oldVal === "True"}
2455
+ onChange={e => {
2456
+ const checked = e.target.checked;
2457
+ this.finishCellEdit(cellId, checked);
2458
+ }}
2459
+ />
2460
+ );
2461
+ noEllipsis = true;
2462
+ } else {
2463
+ if (reduxFormEditingCell === cellId) {
2464
+ if (column.type === "dropdown" || column.type === "dropdownMulti") {
2465
+ return (
2466
+ <DropdownCell
2467
+ isMulti={column.type === "dropdownMulti"}
2468
+ initialValue={text}
2469
+ options={getVals(column.values)}
2470
+ finishEdit={(newVal, doNotStopEditing) => {
2471
+ this.finishCellEdit(cellId, newVal, doNotStopEditing);
2472
+ }}
2473
+ cancelEdit={this.cancelCellEdit}
2474
+ ></DropdownCell>
2475
+ );
2476
+ } else {
2477
+ return (
2478
+ <EditableCell
2479
+ stopSelectAll={() =>
2480
+ change("reduxFormEditingCellSelectAll", false)
2481
+ }
2482
+ shouldSelectAll={reduxFormEditingCellSelectAll}
2483
+ cancelEdit={this.cancelCellEdit}
2484
+ isNumeric={column.type === "number"}
2485
+ initialValue={text}
2486
+ finishEdit={newVal => {
2487
+ this.finishCellEdit(cellId, newVal);
2488
+ }}
2489
+ ></EditableCell>
2490
+ );
2491
+ }
2492
+ }
2493
+ }
2494
+
2495
+ //wrap the original tableColumn.Cell function in another div in order to add a title attribute
2496
+ let title = text;
2497
+ if (getCellHoverText) title = getCellHoverText(...args);
2498
+ else if (column.getTitleAttr) title = column.getTitleAttr(...args);
2499
+ const isSelectedCell = reduxFormSelectedCells?.[cellId];
2500
+ // if (isSelectedCell) {
2501
+ // const [rowId2, path] = cellId.split(":");
2502
+ // const selectedEnt = entities.find((e, i) => {
2503
+ // return getIdOrCodeOrIndex(e, i) === rowId2;
2504
+ // });
2505
+ // }
2506
+
2507
+ const {
2508
+ isRect,
2509
+ selectionGrid,
2510
+ lastRowIndex,
2511
+ lastCellIndex,
2512
+ entityMap,
2513
+ pathToIndex
2514
+ } = this.isSelectionARectangle();
2515
+ return (
2516
+ <>
2517
+ <div
2518
+ style={{
2519
+ ...(!noEllipsis && {
2520
+ textOverflow: "ellipsis",
2521
+ overflow: "hidden"
2522
+ })
2523
+ }}
2524
+ data-test={"tgCell_" + column.path}
2525
+ className="tg-cell-wrapper"
2526
+ data-copy-text={text}
2527
+ title={title || undefined}
2528
+ >
2529
+ {val}
2530
+ </div>
2531
+ {isCellEditable &&
2532
+ (column.type === "dropdown" ||
2533
+ column.type === "dropdownMulti") && (
2534
+ <Icon
2535
+ icon="caret-down"
2536
+ style={{
2537
+ position: "absolute",
2538
+ right: 5,
2539
+ opacity: 0.3
2540
+ }}
2541
+ className="cell-edit-dropdown"
2542
+ onClick={() => {
2543
+ this.startCellEdit(cellId);
2544
+ }}
2545
+ />
2546
+ )}
2547
+
2548
+ {isSelectedCell &&
2549
+ (isRect
2550
+ ? this.isBottomRightCornerOfRectangle({
2551
+ cellId,
2552
+ selectionGrid,
2553
+ lastRowIndex,
2554
+ lastCellIndex,
2555
+ entityMap,
2556
+ pathToIndex
2557
+ })
2558
+ : isSelectedCell === PRIMARY_SELECTED_VAL) && (
2559
+ <CellDragHandle
2560
+ key={cellId}
2561
+ thisTable={this.table}
2562
+ cellId={cellId}
2563
+ isSelectionARectangle={this.isSelectionARectangle}
2564
+ onDragEnd={this.onDragEnd}
2565
+ ></CellDragHandle>
2566
+ )}
2567
+ </>
2568
+ );
2569
+ };
2570
+
2571
+ columnsToRender.push(tableColumn);
2572
+ });
2573
+ return columnsToRender;
2574
+ };
2575
+ isBottomRightCornerOfRectangle = ({
2576
+ cellId,
2577
+ selectionGrid,
2578
+ lastRowIndex,
2579
+ lastCellIndex,
2580
+ entityMap,
2581
+ pathToIndex
2582
+ }) => {
2583
+ selectionGrid.forEach(row => {
2584
+ // remove undefineds from start of row
2585
+ while (row[0] === undefined && row.length) row.shift();
2586
+ });
2587
+ const [rowId, cellPath] = cellId.split(":");
2588
+ const ent = entityMap[rowId];
2589
+ if (!ent) return;
2590
+ const { i } = ent;
2591
+ const cellIndex = pathToIndex[cellPath];
2592
+ const isBottomRight = i === lastRowIndex && cellIndex === lastCellIndex;
2593
+ return isBottomRight;
2594
+ };
2595
+
2596
+ onDragEnd = cellsToSelect => {
2597
+ const {
2598
+ entities,
2599
+ schema,
2600
+ reduxFormCellValidation,
2601
+ change,
2602
+ reduxFormSelectedCells
2603
+ } = this.props;
2604
+ const primaryCellId = this.getPrimarySelectedCellId();
2605
+ const [primaryRowId, primaryCellPath] = primaryCellId.split(":");
2606
+ const pathToField = getFieldPathToField(schema);
2607
+ const { selectedPaths, selectionGrid } = this.isSelectionARectangle();
2608
+ let allSelectedPaths = selectedPaths;
2609
+ if (!allSelectedPaths) {
2610
+ allSelectedPaths = [primaryCellPath];
2611
+ }
2612
+
2613
+ this.updateEntitiesHelper(entities, entities => {
2614
+ let newReduxFormSelectedCells;
2615
+ if (selectedPaths) {
2616
+ newReduxFormSelectedCells = {
2617
+ ...reduxFormSelectedCells
2618
+ };
2619
+ } else {
2620
+ newReduxFormSelectedCells = {
2621
+ [primaryCellId]: PRIMARY_SELECTED_VAL
2622
+ };
2623
+ }
2624
+
2625
+ const newCellValidate = {
2626
+ ...reduxFormCellValidation
2627
+ };
2628
+ const entityMap = getEntityIdToEntity(entities);
2629
+ const { e: selectedEnt } = entityMap[primaryRowId];
2630
+ const firstCellToSelectRowIndex =
2631
+ entityMap[cellsToSelect[0]?.split(":")[0]]?.i;
2632
+ const pathToIndex = getFieldPathToIndex(schema);
2633
+
2634
+ allSelectedPaths.forEach(selectedPath => {
2635
+ const column = pathToField[selectedPath];
2636
+
2637
+ const selectedCellVal = getCellVal(selectedEnt, selectedPath, column);
2638
+ const cellIndexOfSelectedPath = pathToIndex[selectedPath];
2639
+ let incrementStart;
2640
+ let incrementPrefix;
2641
+ let incrementPad = 0;
2642
+ if (column.type === "string" || column.type === "number") {
2643
+ const cellNumStr = getNumberStrAtEnd(selectedCellVal);
2644
+ const cellNum = Number(cellNumStr);
2645
+ const entityAbovePrimaryCell =
2646
+ entities[entityMap[primaryRowId].i - 1];
2647
+ if (cellNumStr !== null && !isNaN(cellNum)) {
2648
+ if (
2649
+ entityAbovePrimaryCell &&
2650
+ (!selectionGrid || selectionGrid.length <= 1)
2651
+ ) {
2652
+ const cellAboveVal = get(
2653
+ entityAbovePrimaryCell,
2654
+ selectedPath,
2655
+ ""
2656
+ );
2657
+ const cellAboveNumStr = getNumberStrAtEnd(cellAboveVal);
2658
+ const cellAboveNum = Number(cellAboveNumStr);
2659
+ if (!isNaN(cellAboveNum)) {
2660
+ const isIncremental = cellNum - cellAboveNum === 1;
2661
+ if (isIncremental) {
2662
+ const cellTextNoNum = stripNumberAtEnd(selectedCellVal);
2663
+ const sameText =
2664
+ stripNumberAtEnd(cellAboveVal) === cellTextNoNum;
2665
+ if (sameText) {
2666
+ incrementStart = cellNum + 1;
2667
+ incrementPrefix = cellTextNoNum || "";
2668
+ if (cellNumStr && cellNumStr.startsWith("0")) {
2669
+ incrementPad = cellNumStr.length;
2670
+ }
2671
+ }
2672
+ }
2673
+ }
2674
+ }
2675
+ if (incrementStart === undefined) {
2676
+ const draggingDown =
2677
+ firstCellToSelectRowIndex > selectionGrid?.[0][0].rowIndex;
2678
+ if (selectedPaths && draggingDown) {
2679
+ let checkIncrement;
2680
+ let prefix;
2681
+ let maybePad;
2682
+ // determine if all the cells in this column of the selectionGrid are incrementing
2683
+ const allAreIncrementing = selectionGrid.every(row => {
2684
+ // see if cell is selected
2685
+ const cellInfo = row[cellIndexOfSelectedPath];
2686
+ if (!cellInfo) return false;
2687
+ const { cellId } = cellInfo;
2688
+ const [rowId] = cellId.split(":");
2689
+ const cellVal = getCellVal(
2690
+ entityMap[rowId].e,
2691
+ selectedPath,
2692
+ pathToField[selectedPath]
2693
+ );
2694
+ const cellNumStr = getNumberStrAtEnd(cellVal);
2695
+ const cellNum = Number(cellNumStr);
2696
+ const cellTextNoNum = stripNumberAtEnd(cellVal);
2697
+ if (cellNumStr.startsWith("0")) {
2698
+ maybePad = cellNumStr.length;
2699
+ }
2700
+ if (cellTextNoNum && !prefix) {
2701
+ prefix = cellTextNoNum;
2702
+ }
2703
+ if (cellTextNoNum && prefix !== cellTextNoNum) {
2704
+ return false;
2705
+ }
2706
+ if (!isNaN(cellNum)) {
2707
+ if (!checkIncrement) {
2708
+ checkIncrement = cellNum;
2709
+ return true;
2710
+ } else {
2711
+ return ++checkIncrement === cellNum;
2712
+ }
2713
+ } else {
2714
+ return false;
2715
+ }
2716
+ });
2717
+
2718
+ if (allAreIncrementing) {
2719
+ incrementStart = checkIncrement + 1;
2720
+ incrementPrefix = prefix || "";
2721
+ incrementPad = maybePad;
2722
+ }
2723
+ }
2724
+ }
2725
+ }
2726
+ }
2727
+
2728
+ let firstSelectedCellRowIndex;
2729
+ if (selectionGrid) {
2730
+ selectionGrid[0].some(cell => {
2731
+ if (cell) {
2732
+ firstSelectedCellRowIndex = cell.rowIndex;
2733
+ return true;
2734
+ }
2735
+ return false;
2736
+ });
2737
+ }
2738
+
2739
+ cellsToSelect.forEach(cellId => {
2740
+ const [rowId, cellPath] = cellId.split(":");
2741
+ if (cellPath !== selectedPath) return;
2742
+ newReduxFormSelectedCells[cellId] = true;
2743
+ const { e: entityToUpdate, i: rowIndex } = entityMap[rowId] || {};
2744
+ if (entityToUpdate) {
2745
+ delete entityToUpdate._isClean;
2746
+ let newVal;
2747
+ if (incrementStart !== undefined) {
2748
+ const num = incrementStart++;
2749
+ newVal = incrementPrefix + padStart(num, incrementPad, "0");
2750
+ } else {
2751
+ if (selectionGrid && selectionGrid.length > 1) {
2752
+ // if there are multiple cells selected then we want to copy them repeating
2753
+ // ex: if we have 1,2,3 selected and we drag for 5 more rows we want it to
2754
+ // be 1,2,3,1,2 for the new row cells in this column
2755
+ const draggingDown = rowIndex > firstSelectedCellRowIndex;
2756
+ const cellIndex = pathToIndex[cellPath];
2757
+ let cellIdToCopy;
2758
+ if (draggingDown) {
2759
+ const { cellId } = selectionGrid[
2760
+ (rowIndex - firstSelectedCellRowIndex) %
2761
+ selectionGrid.length
2762
+ ].find(g => g && g.cellIndex === cellIndex);
2763
+ cellIdToCopy = cellId;
2764
+ } else {
2765
+ const lastIndexInGrid =
2766
+ selectionGrid[selectionGrid.length - 1][0].rowIndex;
2767
+ const { cellId } = selectionGrid[
2768
+ (rowIndex + lastIndexInGrid + 1) % selectionGrid.length
2769
+ ].find(g => g.cellIndex === cellIndex);
2770
+ cellIdToCopy = cellId;
2771
+ }
2772
+
2773
+ const [rowIdToCopy, cellPathToCopy] = cellIdToCopy.split(":");
2774
+ newVal = getCellVal(
2775
+ entityMap[rowIdToCopy].e,
2776
+ cellPathToCopy,
2777
+ pathToField[cellPathToCopy]
2778
+ );
2779
+ } else {
2780
+ newVal = selectedCellVal;
2781
+ }
2782
+ }
2783
+ const { error } = editCellHelper({
2784
+ entity: entityToUpdate,
2785
+ path: cellPath,
2786
+ schema,
2787
+ newVal
2788
+ });
2789
+ newCellValidate[cellId] = error;
2790
+ }
2791
+ });
2792
+ });
2793
+
2794
+ // select the new cells
2795
+ this.updateValidation(entities, newCellValidate);
2796
+ change("reduxFormSelectedCells", newReduxFormSelectedCells);
2797
+ });
2798
+ };
2799
+ getCopyTextForCell = (val, row = {}, column = {}) => {
2800
+ const { cellRenderer } = computePresets(this.props);
2801
+ // TODOCOPY we need a way to potentially omit certain columns from being added as a \t element (talk to taoh about this)
2802
+ let text = typeof val !== "string" ? row.value : val;
2803
+
2804
+ const record = row.original;
2805
+ if (column.getClipboardData) {
2806
+ text = column.getClipboardData(row.value, record, row, this.props);
2807
+ } else if (column.getValueToFilterOn) {
2808
+ text = column.getValueToFilterOn(record, this.props);
2809
+ } else if (column.render) {
2810
+ text = column.render(row.value, record, row, this.props);
2811
+ } else if (cellRenderer && cellRenderer[column.path]) {
2812
+ text = cellRenderer[column.path](
2813
+ row.value,
2814
+ row.original,
2815
+ row,
2816
+ this.props
2817
+ );
2818
+ } else if (text) {
2819
+ text = React.isValidElement(text) ? text : String(text);
2820
+ }
2821
+ const getTextFromElementOrLink = text => {
2822
+ if (React.isValidElement(text)) {
2823
+ if (text.props?.to) {
2824
+ // this will convert Link elements to url strings
2825
+ return joinUrl(
2826
+ window.location.origin,
2827
+ window.frontEndConfig?.clientBasePath || "",
2828
+ text.props.to
2829
+ );
2830
+ } else {
2831
+ return getTextFromEl(text);
2832
+ }
2833
+ } else {
2834
+ return text;
2835
+ }
2836
+ };
2837
+ text = getTextFromElementOrLink(text);
2838
+
2839
+ if (Array.isArray(text)) {
2840
+ let arrText = text.map(getTextFromElementOrLink).join(", ");
2841
+ // because we sometimes insert commas after links when mapping over an array of elements we will have double ,'s
2842
+ arrText = arrText.replace(/, ,/g, ",");
2843
+ text = arrText;
2844
+ }
2845
+
2846
+ const stringText = toString(text);
2847
+ if (stringText === "[object Object]") return "";
2848
+ return stringText;
2849
+ };
2850
+
2851
+ insertRows = ({ above, numRows = 1, appendToBottom } = {}) => {
2852
+ const { entities = [], reduxFormCellValidation } = computePresets(
2853
+ this.props
2854
+ );
2855
+
2856
+ const primaryCellId = this.getPrimarySelectedCellId();
2857
+ const [rowId] = primaryCellId?.split(":") || [];
2858
+ this.updateEntitiesHelper(entities, entities => {
2859
+ const newEntities = times(numRows).map(() => ({ id: nanoid() }));
2860
+
2861
+ const indexToInsert = entities.findIndex((e, i) => {
2862
+ return getIdOrCodeOrIndex(e, i) === rowId;
2863
+ });
2864
+ const insertIndex = above ? indexToInsert : indexToInsert + 1;
2865
+ let { newEnts, validationErrors } = this.formatAndValidateEntities(
2866
+ newEntities
2867
+ );
2868
+ newEnts = newEnts.map(e => ({
2869
+ ...e,
2870
+ _isClean: true
2871
+ }));
2872
+ this.updateValidation(entities, {
2873
+ ...reduxFormCellValidation,
2874
+ ...validationErrors
2875
+ });
2876
+
2877
+ entities.splice(
2878
+ appendToBottom ? entities.length : insertIndex,
2879
+ 0,
2880
+ ...newEnts
2881
+ );
2882
+ });
2883
+ this.refocusTable();
2884
+ };
2885
+
2886
+ showContextMenu = (e, idMap) => {
2887
+ const {
2888
+ history,
2889
+ contextMenu,
2890
+ isCopyable,
2891
+ isCellEditable,
2892
+ entities = [],
2893
+ reduxFormSelectedCells = {}
2894
+ } = computePresets(this.props);
2895
+ let selectedRecords;
2896
+ if (isCellEditable) {
2897
+ const rowIds = {};
2898
+ Object.keys(reduxFormSelectedCells).forEach(cellKey => {
2899
+ const [rowId] = cellKey.split(":");
2900
+ rowIds[rowId] = true;
2901
+ });
2902
+ selectedRecords = entities.filter(e => rowIds[getIdOrCodeOrIndex(e)]);
2903
+ } else {
2904
+ selectedRecords = getRecordsFromIdMap(idMap);
2905
+ }
2906
+
2907
+ const itemsToRender = contextMenu({
2908
+ selectedRecords,
2909
+ history
2910
+ });
2911
+ if (!itemsToRender && !isCopyable) return null;
2912
+ const copyMenuItems = [];
2913
+
2914
+ e.persist();
2915
+ if (isCopyable) {
2916
+ //compute the cellWrapper here so we don't lose access to it
2917
+ const cellWrapper =
2918
+ e.target.querySelector(".tg-cell-wrapper") ||
2919
+ e.target.closest(".tg-cell-wrapper");
2920
+ if (cellWrapper) {
2921
+ copyMenuItems.push(
2922
+ <MenuItem
2923
+ key="copyCell"
2924
+ onClick={() => {
2925
+ //TODOCOPY: we need to make sure that the cell copy is being used by the row copy.. right now we have 2 different things going on
2926
+ //do we need to be able to copy hidden cells? It seems like it should just copy what's on the page..?
2927
+ const text = this.getCellCopyText(cellWrapper);
2928
+ this.handleCopyHelper(text, "Cell copied");
2929
+ }}
2930
+ text="Cell"
2931
+ />
2932
+ );
2933
+ copyMenuItems.push(
2934
+ <MenuItem
2935
+ key="copyColumn"
2936
+ onClick={() => {
2937
+ this.handleCopyColumn(e, cellWrapper);
2938
+ }}
2939
+ text="Column"
2940
+ />
2941
+ );
2942
+ }
2943
+ if (selectedRecords.length === 0 || selectedRecords.length === 1) {
2944
+ //compute the row here so we don't lose access to it
2945
+ const cell =
2946
+ e.target.querySelector(".tg-cell-wrapper") ||
2947
+ e.target.closest(".tg-cell-wrapper") ||
2948
+ e.target.closest(".rt-td");
2949
+ const row = cell.closest(".rt-tr");
2950
+ copyMenuItems.push(
2951
+ <MenuItem
2952
+ key="copySelectedRows"
2953
+ onClick={() => {
2954
+ this.handleCopyRow(row);
2955
+ // loop through each cell in the row
2956
+ }}
2957
+ text="Row"
2958
+ />
2959
+ );
2960
+ } else if (selectedRecords.length > 1) {
2961
+ copyMenuItems.push(
2962
+ <MenuItem
2963
+ key="copySelectedRows"
2964
+ onClick={() => {
2965
+ this.handleCopySelectedRows(selectedRecords, e);
2966
+ // loop through each cell in the row
2967
+ }}
2968
+ text="Rows"
2969
+ />
2970
+ );
2971
+ }
2972
+ copyMenuItems.push(
2973
+ <MenuItem
2974
+ key="copyFullTableRows"
2975
+ onClick={() => {
2976
+ this.handleCopyTable(e);
2977
+ // loop through each cell in the row
2978
+ }}
2979
+ text="Table"
2980
+ />
2981
+ );
2982
+ }
2983
+ const selectedRowIds = Object.keys(reduxFormSelectedCells).map(cellId => {
2984
+ const [rowId] = cellId.split(":");
2985
+ return rowId;
2986
+ });
2987
+
2988
+ const menu = (
2989
+ <Menu>
2990
+ {itemsToRender}
2991
+ {copyMenuItems.length && (
2992
+ <MenuItem icon="clipboard" key="copyOpts" text="Copy">
2993
+ {copyMenuItems}
2994
+ </MenuItem>
2995
+ )}
2996
+ {isCellEditable && (
2997
+ <>
2998
+ <MenuItem
2999
+ icon="add-row-top"
3000
+ text="Add Row Above"
3001
+ key="addRowAbove"
3002
+ onClick={() => {
3003
+ this.insertRows({ above: true });
3004
+ }}
3005
+ ></MenuItem>
3006
+ <MenuItem
3007
+ icon="add-row-top"
3008
+ text="Add Row Below"
3009
+ key="addRowBelow"
3010
+ onClick={() => {
3011
+ this.insertRows({});
3012
+ }}
3013
+ ></MenuItem>
3014
+ <MenuItem
3015
+ icon="remove"
3016
+ text={`Remove Row${selectedRowIds.length > 1 ? "s" : ""}`}
3017
+ key="removeRow"
3018
+ onClick={() => {
3019
+ const {
3020
+ entities = [],
3021
+ reduxFormCellValidation,
3022
+ reduxFormSelectedCells = {}
3023
+ } = computePresets(this.props);
3024
+ const selectedRowIds = Object.keys(reduxFormSelectedCells).map(
3025
+ cellId => {
3026
+ const [rowId] = cellId.split(":");
3027
+ return rowId;
3028
+ }
3029
+ );
3030
+ this.updateEntitiesHelper(entities, entities => {
3031
+ const ents = entities.filter(
3032
+ (e, i) => !selectedRowIds.includes(getIdOrCodeOrIndex(e, i))
3033
+ );
3034
+ this.updateValidation(
3035
+ ents,
3036
+ omitBy(reduxFormCellValidation, (v, cellId) =>
3037
+ selectedRowIds.includes(cellId.split(":")[0])
3038
+ )
3039
+ );
3040
+ return ents;
3041
+ });
3042
+ this.refocusTable();
3043
+ }}
3044
+ ></MenuItem>
3045
+ </>
3046
+ )}
3047
+ </Menu>
3048
+ );
3049
+ ContextMenu.show(menu, { left: e.clientX, top: e.clientY });
3050
+ };
3051
+
3052
+ renderColumnHeader = column => {
3053
+ const {
3054
+ addFilters,
3055
+ setOrder,
3056
+ order,
3057
+ withFilter,
3058
+ withSort,
3059
+ filters,
3060
+ removeSingleFilter,
3061
+ currentParams,
3062
+ isLocalCall,
3063
+ setNewParams,
3064
+ compact,
3065
+ isCellEditable,
3066
+ extraCompact,
3067
+ entities
3068
+ } = computePresets(this.props);
3069
+ const {
3070
+ displayName,
3071
+ description,
3072
+ isUnique,
3073
+ sortDisabled,
3074
+ filterDisabled,
3075
+ columnFilterDisabled,
3076
+ renderTitleInner,
3077
+ filterIsActive = noop,
3078
+ noTitle,
3079
+ isNotEditable,
3080
+ type,
3081
+ path
3082
+ } = column;
3083
+ const columnDataType = column.type;
3084
+ const isActionColumn = columnDataType === "action";
3085
+ const disableSorting =
3086
+ sortDisabled ||
3087
+ isActionColumn ||
3088
+ (!isLocalCall && typeof path === "string" && path.includes(".")) ||
3089
+ columnDataType === "color";
3090
+ const disableFiltering =
3091
+ filterDisabled ||
3092
+ columnDataType === "color" ||
3093
+ isActionColumn ||
3094
+ columnFilterDisabled;
3095
+ const ccDisplayName = camelCase(displayName || path);
3096
+ let columnTitle = displayName || startCase(camelCase(path));
3097
+ if (isActionColumn) columnTitle = "";
3098
+
3099
+ const currentFilter =
3100
+ filters &&
3101
+ !!filters.length &&
3102
+ filters.filter(({ filterOn }) => {
3103
+ return filterOn === ccDisplayName;
3104
+ })[0];
3105
+ const filterActiveForColumn =
3106
+ !!currentFilter || filterIsActive(currentParams);
3107
+ let ordering;
3108
+ if (order && order.length) {
3109
+ order.forEach(order => {
3110
+ const orderField = order.replace("-", "");
3111
+ if (orderField === ccDisplayName) {
3112
+ if (orderField === order) {
3113
+ ordering = "asc";
3114
+ } else {
3115
+ ordering = "desc";
3116
+ }
3117
+ }
3118
+ });
3119
+ }
3120
+
3121
+ const sortDown = ordering && ordering === "asc";
3122
+ const sortUp = ordering && !sortDown;
3123
+ const sortComponent =
3124
+ withSort && !disableSorting ? (
3125
+ <div className="tg-sort-arrow-container">
3126
+ <Icon
3127
+ data-tip="Sort Z-A (Hold shift to sort multiple columns)"
3128
+ icon="chevron-up"
3129
+ className={classNames({
3130
+ active: sortUp
3131
+ })}
3132
+ color={sortUp ? "#106ba3" : undefined}
3133
+ iconSize={extraCompact ? 10 : 12}
3134
+ onClick={e => {
3135
+ setOrder("-" + ccDisplayName, sortUp, e.shiftKey);
3136
+ }}
3137
+ />
3138
+ <Icon
3139
+ data-tip="Sort A-Z (Hold shift to sort multiple columns)"
3140
+ icon="chevron-down"
3141
+ className={classNames({
3142
+ active: sortDown
3143
+ })}
3144
+ color={sortDown ? "#106ba3" : undefined}
3145
+ iconSize={extraCompact ? 10 : 12}
3146
+ onClick={e => {
3147
+ setOrder(ccDisplayName, sortDown, e.shiftKey);
3148
+ }}
3149
+ />
3150
+ </div>
3151
+ ) : null;
3152
+ const FilterMenu = column.FilterMenu || FilterAndSortMenu;
3153
+
3154
+ const filterMenu =
3155
+ withFilter && !disableFiltering ? (
3156
+ <ColumnFilterMenu
3157
+ FilterMenu={FilterMenu}
3158
+ filterActiveForColumn={filterActiveForColumn}
3159
+ addFilters={addFilters}
3160
+ removeSingleFilter={removeSingleFilter}
3161
+ currentFilter={currentFilter}
3162
+ filterOn={ccDisplayName}
3163
+ dataType={columnDataType}
3164
+ schemaForField={column}
3165
+ currentParams={currentParams}
3166
+ setNewParams={setNewParams}
3167
+ compact={compact}
3168
+ extraCompact={extraCompact}
3169
+ />
3170
+ ) : null;
3171
+ let maybeCheckbox;
3172
+ if (isCellEditable && !isNotEditable && type === "boolean") {
3173
+ let isIndeterminate = false;
3174
+ let isChecked = !!entities.length;
3175
+ let hasFalse;
3176
+ let hasTrue;
3177
+ entities.some(e => {
3178
+ if (!get(e, path)) {
3179
+ isChecked = false;
3180
+ hasFalse = true;
3181
+ } else {
3182
+ hasTrue = true;
3183
+ }
3184
+ if (hasFalse && hasTrue) {
3185
+ isIndeterminate = true;
3186
+ return true;
3187
+ }
3188
+ return false;
3189
+ });
3190
+ maybeCheckbox = (
3191
+ <Checkbox
3192
+ style={{ marginBottom: 0, marginLeft: 3 }}
3193
+ onChange={() => {
3194
+ this.updateEntitiesHelper(entities, ents => {
3195
+ ents.forEach(e => {
3196
+ delete e._isClean;
3197
+ set(e, path, isIndeterminate ? true : !isChecked);
3198
+ });
3199
+ });
3200
+ }}
3201
+ indeterminate={isIndeterminate}
3202
+ checked={isChecked}
3203
+ ></Checkbox>
3204
+ );
3205
+ }
3206
+
3207
+ const columnTitleTextified = getTextFromEl(columnTitle);
3208
+
3209
+ return (
3210
+ <div
3211
+ {...(description && {
3212
+ "data-tip": `<div>
3213
+ <strong>${columnTitle}:</strong> <br>
3214
+ ${description} ${isUnique ? "<br>Must be unique" : ""}</div>`
3215
+ })}
3216
+ data-test={columnTitleTextified}
3217
+ data-copy-text={columnTitleTextified}
3218
+ className={classNames("tg-react-table-column-header", {
3219
+ "sort-active": sortUp || sortDown
3220
+ })}
3221
+ >
3222
+ {columnTitleTextified && !noTitle && (
3223
+ <React.Fragment>
3224
+ {maybeCheckbox}
3225
+ <span
3226
+ title={columnTitleTextified}
3227
+ className={classNames({
3228
+ "tg-react-table-name": true,
3229
+ "no-data-tip": !!description
3230
+ })}
3231
+ style={{
3232
+ ...(description && { fontStyle: "italic" }),
3233
+ display: "inline-block"
3234
+ }}
3235
+ >
3236
+ {renderTitleInner ? renderTitleInner : columnTitle}{" "}
3237
+ </span>
3238
+ </React.Fragment>
3239
+ )}
3240
+ <div
3241
+ style={{ display: "flex", marginLeft: "auto", alignItems: "center" }}
3242
+ >
3243
+ {sortComponent}
3244
+ {filterMenu}
3245
+ </div>
3246
+ </div>
3247
+ );
3248
+ };
3249
+ }
3250
+
3251
+ // const CompToExport = dataTableEnhancer(HotkeysTarget(DataTable));
3252
+ // // CompToExport.selectRecords = (form, value) => {
3253
+ // // return change(form, "reduxFormSelectedEntityIdMap", value)
3254
+ // // }
3255
+ // export default CompToExport
3256
+ const WrappedDT = dataTableEnhancer(DataTable);
3257
+ export default WrappedDT;
3258
+ const ConnectedPagingTool = dataTableEnhancer(PagingTool);
3259
+ export { ConnectedPagingTool };
3260
+
3261
+ const itemSizeEstimators = {
3262
+ compact: () => 25.34,
3263
+ normal: () => 33.34,
3264
+ comfortable: () => 41.34
3265
+ };
3266
+
3267
+ function getCellInfo({
3268
+ columnIndex,
3269
+ columnPath,
3270
+ rowId,
3271
+ schema,
3272
+ entities,
3273
+ rowIndex,
3274
+ isEntityDisabled,
3275
+ entity
3276
+ }) {
3277
+ const leftpath = schema.fields[columnIndex - 1]?.path;
3278
+ const rightpath = schema.fields[columnIndex + 1]?.path;
3279
+ const cellIdToLeft = leftpath && `${rowId}:${leftpath}`;
3280
+ const cellIdToRight = rightpath && `${rowId}:${rightpath}`;
3281
+ const rowAboveId =
3282
+ entities[rowIndex - 1] &&
3283
+ getIdOrCodeOrIndex(entities[rowIndex - 1], rowIndex - 1);
3284
+ const rowBelowId =
3285
+ entities[rowIndex + 1] &&
3286
+ getIdOrCodeOrIndex(entities[rowIndex + 1], rowIndex + 1);
3287
+ const cellIdAbove = rowAboveId && `${rowAboveId}:${columnPath}`;
3288
+ const cellIdBelow = rowBelowId && `${rowBelowId}:${columnPath}`;
3289
+
3290
+ const cellId = `${rowId}:${columnPath}`;
3291
+ const rowDisabled = isEntityDisabled(entity);
3292
+ return {
3293
+ cellId,
3294
+ cellIdAbove,
3295
+ cellIdToRight,
3296
+ cellIdBelow,
3297
+ cellIdToLeft,
3298
+ rowDisabled
3299
+ };
3300
+ }
3301
+
3302
+ function ColumnFilterMenu({
3303
+ FilterMenu,
3304
+ filterActiveForColumn,
3305
+ compact,
3306
+ extraCompact,
3307
+ ...rest
3308
+ }) {
3309
+ const [columnFilterMenuOpen, setColumnFilterMenuOpen] = useState(false);
3310
+ return (
3311
+ <Popover
3312
+ position="bottom"
3313
+ onClose={() => {
3314
+ setColumnFilterMenuOpen(false);
3315
+ }}
3316
+ isOpen={columnFilterMenuOpen}
3317
+ modifiers={{
3318
+ preventOverflow: { enabled: true },
3319
+ hide: { enabled: false },
3320
+ flip: { enabled: false }
3321
+ }}
3322
+ >
3323
+ <Icon
3324
+ style={{ marginLeft: 5 }}
3325
+ icon="filter"
3326
+ iconSize={extraCompact ? 14 : undefined}
3327
+ onClick={() => {
3328
+ setColumnFilterMenuOpen(!columnFilterMenuOpen);
3329
+ }}
3330
+ className={classNames("tg-filter-menu-button", {
3331
+ "tg-active-filter": !!filterActiveForColumn
3332
+ })}
3333
+ />
3334
+ <FilterMenu
3335
+ togglePopover={() => {
3336
+ setColumnFilterMenuOpen(false);
3337
+ }}
3338
+ {...rest}
3339
+ />
3340
+ </Popover>
3341
+ );
3342
+ }
3343
+
3344
+ function getLastSelectedEntity(idMap) {
3345
+ let lastSelectedEnt;
3346
+ let latestTime;
3347
+ forEach(idMap, ({ time, entity }) => {
3348
+ if (!latestTime || time > latestTime) {
3349
+ lastSelectedEnt = entity;
3350
+ latestTime = time;
3351
+ }
3352
+ });
3353
+ return lastSelectedEnt;
3354
+ }
3355
+
3356
+ function getNewEntToSelect({
3357
+ type,
3358
+ lastSelectedIndex,
3359
+ entities,
3360
+ isEntityDisabled
3361
+ }) {
3362
+ let newIndexToSelect;
3363
+ if (type === "up") {
3364
+ newIndexToSelect = lastSelectedIndex - 1;
3365
+ } else {
3366
+ newIndexToSelect = lastSelectedIndex + 1;
3367
+ }
3368
+ const newEntToSelect = entities[newIndexToSelect];
3369
+ if (!newEntToSelect) return;
3370
+ if (isEntityDisabled && isEntityDisabled(newEntToSelect)) {
3371
+ return getNewEntToSelect({
3372
+ type,
3373
+ lastSelectedIndex: newIndexToSelect,
3374
+ entities,
3375
+ isEntityDisabled
3376
+ });
3377
+ } else {
3378
+ return newEntToSelect;
3379
+ }
3380
+ }
3381
+
3382
+ function getAllRows(e) {
3383
+ const el = e.target.querySelector(".data-table-container")
3384
+ ? e.target.querySelector(".data-table-container")
3385
+ : e.target.closest(".data-table-container");
3386
+
3387
+ const allRowEls = el.querySelectorAll(".rt-tr");
3388
+ if (!allRowEls || !allRowEls.length) {
3389
+ return;
3390
+ }
3391
+ return allRowEls;
3392
+ }
3393
+
3394
+ function EditableCell({
3395
+ shouldSelectAll,
3396
+ stopSelectAll,
3397
+ initialValue,
3398
+ finishEdit,
3399
+ cancelEdit,
3400
+ isNumeric
3401
+ }) {
3402
+ const [v, setV] = useState(initialValue);
3403
+ return (
3404
+ <input
3405
+ style={{
3406
+ border: 0,
3407
+ width: "95%",
3408
+ fontSize: 12,
3409
+ background: "none"
3410
+ }}
3411
+ ref={r => {
3412
+ if (shouldSelectAll && r) {
3413
+ r?.select();
3414
+ stopSelectAll();
3415
+ }
3416
+ }}
3417
+ type={isNumeric ? "number" : undefined}
3418
+ value={v}
3419
+ autoFocus
3420
+ onKeyDown={e => {
3421
+ if (e.key === "Enter") {
3422
+ finishEdit(v);
3423
+ e.stopPropagation();
3424
+ } else if (e.key === "Escape") {
3425
+ e.stopPropagation();
3426
+ cancelEdit();
3427
+ }
3428
+ }}
3429
+ onBlur={() => {
3430
+ finishEdit(v);
3431
+ }}
3432
+ onChange={e => {
3433
+ setV(e.target.value);
3434
+ }}
3435
+ ></input>
3436
+ );
3437
+ }
3438
+
3439
+ function DropdownCell({
3440
+ options,
3441
+ isMulti,
3442
+ initialValue,
3443
+ finishEdit,
3444
+ cancelEdit
3445
+ }) {
3446
+ const [v, setV] = useState(
3447
+ isMulti
3448
+ ? initialValue.split(",").map(v => ({ value: v, label: v }))
3449
+ : initialValue
3450
+ );
3451
+ return (
3452
+ <div
3453
+ className={classNames("tg-dropdown-cell-edit-container", {
3454
+ "tg-dropdown-cell-edit-container-multi": isMulti
3455
+ })}
3456
+ >
3457
+ <TgSelect
3458
+ small
3459
+ multi={isMulti}
3460
+ autoOpen
3461
+ value={v}
3462
+ onChange={val => {
3463
+ if (isMulti) {
3464
+ setV(val);
3465
+ return;
3466
+ }
3467
+ finishEdit(val ? val.value : null);
3468
+ }}
3469
+ popoverProps={{
3470
+ onClose: e => {
3471
+ if (isMulti) {
3472
+ if (e && e.key === "Escape") {
3473
+ cancelEdit();
3474
+ } else {
3475
+ finishEdit(
3476
+ v && v.map
3477
+ ? v
3478
+ .map(v => v.value)
3479
+ .filter(v => v)
3480
+ .join(",")
3481
+ : v
3482
+ );
3483
+ }
3484
+ } else {
3485
+ cancelEdit();
3486
+ }
3487
+ }
3488
+ }}
3489
+ options={options.map(value => ({ label: value, value }))}
3490
+ ></TgSelect>
3491
+ </div>
3492
+ );
3493
+ }
3494
+
3495
+ function getFieldPathToIndex(schema) {
3496
+ const fieldToIndex = {};
3497
+ schema.fields.forEach((f, i) => {
3498
+ fieldToIndex[f.path] = i;
3499
+ });
3500
+ return fieldToIndex;
3501
+ }
3502
+
3503
+ function getFieldPathToField(schema) {
3504
+ const fieldPathToField = {};
3505
+ schema.fields.forEach(f => {
3506
+ fieldPathToField[f.path] = f;
3507
+ });
3508
+ return fieldPathToField;
3509
+ }
3510
+
3511
+ const defaultParsePaste = str => {
3512
+ return str.split(/\r\n|\n|\r/).map(row => row.split("\t"));
3513
+ };
3514
+
3515
+ function getEntityIdToEntity(entities) {
3516
+ const entityIdToEntity = {};
3517
+ entities.forEach((e, i) => {
3518
+ entityIdToEntity[getIdOrCodeOrIndex(e, i)] = { e, i };
3519
+ });
3520
+ return entityIdToEntity;
3521
+ }
3522
+
3523
+ function endsWithNumber(str) {
3524
+ return /[0-9]+$/.test(str);
3525
+ }
3526
+
3527
+ function getNumberStrAtEnd(str) {
3528
+ if (endsWithNumber(str)) {
3529
+ return str.match(/[0-9]+$/)[0];
3530
+ }
3531
+
3532
+ return null;
3533
+ }
3534
+
3535
+ function stripNumberAtEnd(str) {
3536
+ return str.replace(getNumberStrAtEnd(str), "");
3537
+ }