@teselagen/ui 0.7.36 → 0.8.2

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 (167) hide show
  1. package/DataTable/EditabelCell.d.ts +7 -0
  2. package/DataTable/defaultProps.d.ts +43 -0
  3. package/DataTable/utils/computePresets.d.ts +1 -0
  4. package/DataTable/utils/useDeepEqualMemo.d.ts +1 -0
  5. package/DataTable/utils/useTableParams.d.ts +49 -0
  6. package/index.cjs.js +30 -9
  7. package/index.es.js +30 -9
  8. package/package.json +1 -1
  9. package/src/AdvancedOptions.spec.js +26 -0
  10. package/src/AsyncValidateFieldSpinner/index.js +12 -0
  11. package/src/BlueprintError/index.js +14 -0
  12. package/src/BounceLoader/index.js +16 -0
  13. package/src/BounceLoader/style.css +45 -0
  14. package/src/CollapsibleCard/index.js +68 -0
  15. package/src/CollapsibleCard/style.css +23 -0
  16. package/src/DNALoader/index.js +20 -0
  17. package/src/DNALoader/style.css +251 -0
  18. package/src/DataTable/Columns.jsx +945 -0
  19. package/src/DataTable/EditabelCell.jsx +44 -0
  20. package/src/DataTable/EditableCell.js +44 -0
  21. package/src/DataTable/RenderCell.js +191 -0
  22. package/{RenderCell.js → src/DataTable/RenderCell.jsx} +1 -1
  23. package/src/DataTable/defaultProps.js +45 -0
  24. package/src/DataTable/index.js +3244 -0
  25. package/src/DataTable/style.css +608 -0
  26. package/src/DataTable/utils/computePresets.js +42 -0
  27. package/src/DataTable/utils/index.js +55 -0
  28. package/src/DataTable/utils/useDeepEqualMemo.js +10 -0
  29. package/src/DataTable/utils/useTableParams.js +361 -0
  30. package/src/DialogFooter/index.js +86 -0
  31. package/src/DialogFooter/style.css +9 -0
  32. package/src/FormComponents/index.js +1266 -0
  33. package/src/FormComponents/style.css +275 -0
  34. package/src/FormComponents/utils.js +6 -0
  35. package/src/HotkeysDialog/index.js +79 -0
  36. package/src/HotkeysDialog/style.css +54 -0
  37. package/src/InfoHelper/index.js +78 -0
  38. package/src/InfoHelper/style.css +7 -0
  39. package/src/IntentText/index.js +18 -0
  40. package/src/Loading/index.js +70 -0
  41. package/src/Loading/style.css +4 -0
  42. package/src/MenuBar/index.js +423 -0
  43. package/src/MenuBar/style.css +45 -0
  44. package/src/PromptUnsavedChanges/index.js +38 -0
  45. package/src/ResizableDraggableDialog/index.js +141 -0
  46. package/src/ResizableDraggableDialog/style.css +42 -0
  47. package/src/ScrollToTop/index.js +72 -0
  48. package/src/TagSelect/index.js +69 -0
  49. package/src/TagSelect/style.css +13 -0
  50. package/src/TgHtmlSelect/index.js +20 -0
  51. package/src/TgSelect/index.js +537 -0
  52. package/src/TgSelect/style.css +61 -0
  53. package/src/TgSuggest/index.js +124 -0
  54. package/src/Timeline/index.js +15 -0
  55. package/src/Timeline/style.css +29 -0
  56. package/src/enhancers/withDialog/index.js +196 -0
  57. package/src/index.js +87 -0
  58. package/src/showConfirmationDialog/index.js +148 -0
  59. package/src/style.css +265 -0
  60. package/{isBeingCalledExcessively.js → src/utils/isBeingCalledExcessively.js} +0 -2
  61. package/style.css +10508 -0
  62. /package/{AdvancedOptions.js → src/AdvancedOptions.js} +0 -0
  63. /package/{AssignDefaultsModeContext.js → src/AssignDefaultsModeContext.js} +0 -0
  64. /package/{CellDragHandle.js → src/DataTable/CellDragHandle.js} +0 -0
  65. /package/{ColumnFilterMenu.js → src/DataTable/ColumnFilterMenu.js} +0 -0
  66. /package/{Columns.js → src/DataTable/Columns.js} +0 -0
  67. /package/{DisabledLoadingComponent.js → src/DataTable/DisabledLoadingComponent.js} +0 -0
  68. /package/{DisplayOptions.js → src/DataTable/DisplayOptions.js} +0 -0
  69. /package/{DropdownCell.js → src/DataTable/DropdownCell.js} +0 -0
  70. /package/{EditableCell.js → src/DataTable/EditabelCell.js} +0 -0
  71. /package/{FilterAndSortMenu.js → src/DataTable/FilterAndSortMenu.js} +0 -0
  72. /package/{PagingTool.js → src/DataTable/PagingTool.js} +0 -0
  73. /package/{SearchBar.js → src/DataTable/SearchBar.js} +0 -0
  74. /package/{SortableColumns.js → src/DataTable/SortableColumns.js} +0 -0
  75. /package/{TableFormTrackerContext.js → src/DataTable/TableFormTrackerContext.js} +0 -0
  76. /package/{ThComponent.js → src/DataTable/ThComponent.js} +0 -0
  77. /package/{dataTableEnhancer.js → src/DataTable/dataTableEnhancer.js} +0 -0
  78. /package/{defaultFormatters.js → src/DataTable/defaultFormatters.js} +0 -0
  79. /package/{defaultValidators.js → src/DataTable/defaultValidators.js} +0 -0
  80. /package/{editCellHelper.js → src/DataTable/editCellHelper.js} +0 -0
  81. /package/{getCellVal.js → src/DataTable/getCellVal.js} +0 -0
  82. /package/{getVals.js → src/DataTable/getVals.js} +0 -0
  83. /package/{isTruthy.js → src/DataTable/isTruthy.js} +0 -0
  84. /package/{isValueEmpty.js → src/DataTable/isValueEmpty.js} +0 -0
  85. /package/{convertSchema.js → src/DataTable/utils/convertSchema.js} +0 -0
  86. /package/{formatPasteData.js → src/DataTable/utils/formatPasteData.js} +0 -0
  87. /package/{getAllRows.js → src/DataTable/utils/getAllRows.js} +0 -0
  88. /package/{getCellCopyText.js → src/DataTable/utils/getCellCopyText.js} +0 -0
  89. /package/{getCellInfo.js → src/DataTable/utils/getCellInfo.js} +0 -0
  90. /package/{getFieldPathToField.js → src/DataTable/utils/getFieldPathToField.js} +0 -0
  91. /package/{getIdOrCodeOrIndex.js → src/DataTable/utils/getIdOrCodeOrIndex.js} +0 -0
  92. /package/{getLastSelectedEntity.js → src/DataTable/utils/getLastSelectedEntity.js} +0 -0
  93. /package/{getNewEntToSelect.js → src/DataTable/utils/getNewEntToSelect.js} +0 -0
  94. /package/{getRowCopyText.js → src/DataTable/utils/getRowCopyText.js} +0 -0
  95. /package/{getTableConfigFromStorage.js → src/DataTable/utils/getTableConfigFromStorage.js} +0 -0
  96. /package/{handleCopyColumn.js → src/DataTable/utils/handleCopyColumn.js} +0 -0
  97. /package/{handleCopyHelper.js → src/DataTable/utils/handleCopyHelper.js} +0 -0
  98. /package/{handleCopyRows.js → src/DataTable/utils/handleCopyRows.js} +0 -0
  99. /package/{handleCopyTable.js → src/DataTable/utils/handleCopyTable.js} +0 -0
  100. /package/{isBottomRightCornerOfRectangle.js → src/DataTable/utils/isBottomRightCornerOfRectangle.js} +0 -0
  101. /package/{isEntityClean.js → src/DataTable/utils/isEntityClean.js} +0 -0
  102. /package/{primarySelectedValue.js → src/DataTable/utils/primarySelectedValue.js} +0 -0
  103. /package/{queryParams.js → src/DataTable/utils/queryParams.js} +0 -0
  104. /package/{removeCleanRows.js → src/DataTable/utils/removeCleanRows.js} +0 -0
  105. /package/{rowClick.js → src/DataTable/utils/rowClick.js} +0 -0
  106. /package/{selection.js → src/DataTable/utils/selection.js} +0 -0
  107. /package/{useTableEntities.js → src/DataTable/utils/useTableEntities.js} +0 -0
  108. /package/{utils.js → src/DataTable/utils/utils.js} +0 -0
  109. /package/{withSelectedEntities.js → src/DataTable/utils/withSelectedEntities.js} +0 -0
  110. /package/{withTableParams.js → src/DataTable/utils/withTableParams.js} +0 -0
  111. /package/{validateTableWideErrors.js → src/DataTable/validateTableWideErrors.js} +0 -0
  112. /package/{viewColumn.js → src/DataTable/viewColumn.js} +0 -0
  113. /package/{DropdownButton.js → src/DropdownButton.js} +0 -0
  114. /package/{FillWindow.css → src/FillWindow.css} +0 -0
  115. /package/{FillWindow.js → src/FillWindow.js} +0 -0
  116. /package/{FormSeparator.js → src/FormComponents/FormSeparator.js} +0 -0
  117. /package/{LoadingDots.js → src/FormComponents/LoadingDots.js} +0 -0
  118. /package/{Uploader.js → src/FormComponents/Uploader.js} +0 -0
  119. /package/{getNewName.js → src/FormComponents/getNewName.js} +0 -0
  120. /package/{itemUpload.js → src/FormComponents/itemUpload.js} +0 -0
  121. /package/{sortify.js → src/FormComponents/sortify.js} +0 -0
  122. /package/{tryToMatchSchemas.js → src/FormComponents/tryToMatchSchemas.js} +0 -0
  123. /package/{MatchHeaders.js → src/MatchHeaders.js} +0 -0
  124. /package/{SimpleStepViz.js → src/SimpleStepViz.js} +0 -0
  125. /package/{Tag.js → src/Tag.js} +0 -0
  126. /package/{TimelineEvent.js → src/Timeline/TimelineEvent.js} +0 -0
  127. /package/{UploadCsvWizard.css → src/UploadCsvWizard.css} +0 -0
  128. /package/{UploadCsvWizard.js → src/UploadCsvWizard.js} +0 -0
  129. /package/{autoTooltip.js → src/autoTooltip.js} +0 -0
  130. /package/{constants.js → src/constants.js} +0 -0
  131. /package/{customIcons.js → src/customIcons.js} +0 -0
  132. /package/{tg_modalState.js → src/enhancers/withDialog/tg_modalState.js} +0 -0
  133. /package/{withField.js → src/enhancers/withField.js} +0 -0
  134. /package/{withFields.js → src/enhancers/withFields.js} +0 -0
  135. /package/{withLocalStorage.js → src/enhancers/withLocalStorage.js} +0 -0
  136. /package/{rerenderOnWindowResize.js → src/rerenderOnWindowResize.js} +0 -0
  137. /package/{showAppSpinner.js → src/showAppSpinner.js} +0 -0
  138. /package/{showDialogOnDocBody.js → src/showDialogOnDocBody.js} +0 -0
  139. /package/{throwFormError.js → src/throwFormError.js} +0 -0
  140. /package/{toastr.js → src/toastr.js} +0 -0
  141. /package/{typeToCommonType.js → src/typeToCommonType.js} +0 -0
  142. /package/{useDialog.js → src/useDialog.js} +0 -0
  143. /package/{adHoc.js → src/utils/adHoc.js} +0 -0
  144. /package/{basicHandleActionsWithFullState.js → src/utils/basicHandleActionsWithFullState.js} +0 -0
  145. /package/{browserUtils.js → src/utils/browserUtils.js} +0 -0
  146. /package/{combineReducersWithFullState.js → src/utils/combineReducersWithFullState.js} +0 -0
  147. /package/{commandControls.js → src/utils/commandControls.js} +0 -0
  148. /package/{commandUtils.js → src/utils/commandUtils.js} +0 -0
  149. /package/{determineBlackOrWhiteTextColor.js → src/utils/determineBlackOrWhiteTextColor.js} +0 -0
  150. /package/{getDayjsFormatter.js → src/utils/getDayjsFormatter.js} +0 -0
  151. /package/{getTextFromEl.js → src/utils/getTextFromEl.js} +0 -0
  152. /package/{handlerHelpers.js → src/utils/handlerHelpers.js} +0 -0
  153. /package/{index.js → src/utils/hooks/index.js} +0 -0
  154. /package/{useDeepEqualMemo.js → src/utils/hooks/useDeepEqualMemo.js} +0 -0
  155. /package/{useStableReference.js → src/utils/hooks/useStableReference.js} +0 -0
  156. /package/{hotkeyUtils.js → src/utils/hotkeyUtils.js} +0 -0
  157. /package/{menuUtils.js → src/utils/menuUtils.js} +0 -0
  158. /package/{popoverOverflowModifiers.js → src/utils/popoverOverflowModifiers.js} +0 -0
  159. /package/{pureNoFunc.js → src/utils/pureNoFunc.js} +0 -0
  160. /package/{renderOnDoc.js → src/utils/renderOnDoc.js} +0 -0
  161. /package/{showProgressToast.js → src/utils/showProgressToast.js} +0 -0
  162. /package/{tagUtils.js → src/utils/tagUtils.js} +0 -0
  163. /package/{tgFormValues.js → src/utils/tgFormValues.js} +0 -0
  164. /package/{useTraceUpdate.js → src/utils/useTraceUpdate.js} +0 -0
  165. /package/{withSelectTableRecords.js → src/utils/withSelectTableRecords.js} +0 -0
  166. /package/{withStore.js → src/utils/withStore.js} +0 -0
  167. /package/{wrapDialog.js → src/wrapDialog.js} +0 -0
@@ -0,0 +1,3244 @@
1
+ import React, {
2
+ useEffect,
3
+ useRef,
4
+ useMemo,
5
+ useState,
6
+ useCallback,
7
+ useContext
8
+ } from "react";
9
+ import {
10
+ invert,
11
+ toNumber,
12
+ isEmpty,
13
+ min,
14
+ max,
15
+ camelCase,
16
+ startCase,
17
+ noop,
18
+ cloneDeep,
19
+ keyBy,
20
+ omit,
21
+ forEach,
22
+ lowerCase,
23
+ get,
24
+ omitBy,
25
+ times,
26
+ toArray,
27
+ isFunction,
28
+ isEqual,
29
+ every,
30
+ some
31
+ } from "lodash-es";
32
+ import {
33
+ Button,
34
+ Menu,
35
+ MenuItem,
36
+ ContextMenu,
37
+ Icon,
38
+ Intent,
39
+ Callout,
40
+ Tooltip,
41
+ useHotkeys
42
+ } from "@blueprintjs/core";
43
+ import { arrayMove } from "@dnd-kit/sortable";
44
+ import classNames from "classnames";
45
+ import scrollIntoView from "dom-scroll-into-view";
46
+ import ReactTable from "@teselagen/react-table";
47
+ import immer, { produceWithPatches, enablePatches, applyPatches } from "immer";
48
+ import papaparse from "papaparse";
49
+ import { useDispatch, useSelector } from "react-redux";
50
+ import { ThComponent } from "./ThComponent";
51
+ import {
52
+ defaultParsePaste,
53
+ formatPasteData,
54
+ getAllRows,
55
+ getCellCopyText,
56
+ getCellInfo,
57
+ getEntityIdToEntity,
58
+ getFieldPathToIndex,
59
+ getIdOrCodeOrIndex,
60
+ getLastSelectedEntity,
61
+ getNewEntToSelect,
62
+ getRecordsFromIdMap,
63
+ getRowCopyText,
64
+ handleCopyColumn,
65
+ handleCopyHelper,
66
+ handleCopyRows,
67
+ handleCopyTable,
68
+ isEntityClean,
69
+ PRIMARY_SELECTED_VAL,
70
+ removeCleanRows
71
+ } from "./utils";
72
+ import { useDeepEqualMemo } from "../utils/hooks";
73
+ import rowClick, {
74
+ changeSelectedEntities,
75
+ finalizeSelection
76
+ } from "./utils/rowClick";
77
+ import PagingTool from "./PagingTool";
78
+ import SearchBar from "./SearchBar";
79
+ import DisplayOptions from "./DisplayOptions";
80
+ import DisabledLoadingComponent from "./DisabledLoadingComponent";
81
+ import SortableColumns from "./SortableColumns";
82
+ import dataTableEnhancer from "./dataTableEnhancer";
83
+ import "../toastr";
84
+ import "@teselagen/react-table/react-table.css";
85
+ import "./style.css";
86
+ import { nanoid } from "nanoid";
87
+ import { SwitchField } from "../FormComponents";
88
+ import { validateTableWideErrors } from "./validateTableWideErrors";
89
+ import { editCellHelper } from "./editCellHelper";
90
+ import getTableConfigFromStorage from "./utils/getTableConfigFromStorage";
91
+ import { viewColumn, openColumn, multiViewColumn } from "./viewColumn";
92
+ import convertSchema from "./utils/convertSchema";
93
+ import TableFormTrackerContext from "./TableFormTrackerContext";
94
+ import {
95
+ getCCDisplayName,
96
+ getCurrentParamsFromUrl,
97
+ getQueryParams,
98
+ makeDataTableHandlers,
99
+ setCurrentParamsOnUrl
100
+ } from "./utils/queryParams";
101
+ import { useColumns } from "./Columns";
102
+ import { formValueSelector, change as _change } from "redux-form";
103
+ import { throwFormError } from "../throwFormError";
104
+ import { isObservableArray, toJS } from "mobx";
105
+ import { isBeingCalledExcessively } from "../utils/isBeingCalledExcessively";
106
+
107
+ enablePatches();
108
+ const IS_LINUX = window.navigator.platform.toLowerCase().search("linux") > -1;
109
+
110
+ const itemSizeEstimators = {
111
+ compact: () => 25.34,
112
+ normal: () => 33.34,
113
+ comfortable: () => 41.34
114
+ };
115
+
116
+ const DataTable = ({
117
+ controlled_pageSize,
118
+ formName = "tgDataTable",
119
+ history,
120
+ isSimple,
121
+ isLocalCall = true,
122
+ isTableParamsConnected,
123
+ noForm,
124
+ orderByFirstColumn,
125
+ schema: _schema,
126
+ showEmptyColumnsByDefault,
127
+ tableParams: _tableParams,
128
+ anyTouched,
129
+ blur,
130
+ ...ownProps
131
+ }) => {
132
+ if (isTableParamsConnected && _tableParams && !_tableParams.entities) {
133
+ throw new Error(
134
+ `No entities array detected in tableParams object (<DataTable {...tableParams}/>). You need to call withQuery() after withTableParams() like: compose(withTableParams(), withQuery(something)).`
135
+ );
136
+ }
137
+ const dispatch = useDispatch();
138
+ const change = useCallback(
139
+ (...args) => dispatch(_change(formName, ...args)),
140
+ [dispatch, formName]
141
+ );
142
+ const tableRef = useRef();
143
+ const alreadySelected = useRef(false);
144
+ const [onlyShowRowsWErrors, setOnlyShowRowsWErrors] = useState(false);
145
+ const [entitiesUndoRedoStack, setEntitiesUndoRedoStack] = useState({
146
+ currentVersion: 0
147
+ });
148
+ const [selectedCells, setSelectedCells] = useState({});
149
+ const _tableConfig = getTableConfigFromStorage(formName);
150
+ // make user set page size persist
151
+ const userSetPageSize =
152
+ _tableConfig?.userSetPageSize && parseInt(_tableConfig.userSetPageSize, 10);
153
+
154
+ const formTracker = useContext(TableFormTrackerContext);
155
+ useEffect(() => {
156
+ if (formTracker.isActive && !formTracker.formNames.includes(formName)) {
157
+ formTracker.pushFormName(formName);
158
+ }
159
+ }, [formTracker, formName]);
160
+
161
+ const [showForcedHiddenColumns, setShowForcedHidden] = useState(() => {
162
+ if (showEmptyColumnsByDefault) {
163
+ return true;
164
+ }
165
+ return false;
166
+ });
167
+
168
+ const {
169
+ reduxFormCellValidation: _reduxFormCellValidation,
170
+ reduxFormEditingCell,
171
+ reduxFormEntities,
172
+ reduxFormQueryParams: _reduxFormQueryParams = {},
173
+ reduxFormSelectedEntityIdMap: _reduxFormSelectedEntityIdMap = {}
174
+ } = useSelector(state =>
175
+ formValueSelector(formName)(
176
+ state,
177
+ "reduxFormCellValidation",
178
+ "reduxFormEntities",
179
+ "reduxFormQueryParams",
180
+ "reduxFormSelectedEntityIdMap"
181
+ )
182
+ );
183
+
184
+ // We want to make sure we don't rerender everything unnecessary
185
+ // with redux-forms we tend to do unnecessary renders
186
+ const reduxFormCellValidation = useDeepEqualMemo(_reduxFormCellValidation);
187
+ const reduxFormQueryParams = useDeepEqualMemo(_reduxFormQueryParams);
188
+ const reduxFormSelectedEntityIdMap = useDeepEqualMemo(
189
+ _reduxFormSelectedEntityIdMap
190
+ );
191
+
192
+ let props = ownProps;
193
+ if (!isTableParamsConnected) {
194
+ // When using mobx values we need to transform it to a js array instead of a proxy so the next hooks get the right values
195
+ const normalizedEntities = isObservableArray(ownProps.entities)
196
+ ? toJS(ownProps.entities)
197
+ : ownProps.entities;
198
+ //this is the case where we're hooking up to withTableParams locally, so we need to take the tableParams off the props
199
+ props = {
200
+ ...ownProps,
201
+ ..._tableParams,
202
+ entities: normalizedEntities
203
+ };
204
+ }
205
+
206
+ const convertedSchema = useMemo(() => convertSchema(_schema), [_schema]);
207
+
208
+ if (isLocalCall) {
209
+ if (!noForm && (!formName || formName === "tgDataTable")) {
210
+ throw new Error(
211
+ "Please pass a unique 'formName' prop to the locally connected <DataTable/> component with schema: ",
212
+ _schema
213
+ );
214
+ }
215
+ } else {
216
+ //in user instantiated withTableParams() call
217
+ if (!formName || formName === "tgDataTable") {
218
+ throw new Error(
219
+ "Please pass a unique 'formName' prop to the withTableParams() with schema: ",
220
+ _schema
221
+ );
222
+ }
223
+ }
224
+
225
+ const { withPaging = !isSimple } = props;
226
+ const {
227
+ doNotCoercePageSize,
228
+ isInfinite = isSimple && !withPaging,
229
+ syncDisplayOptionsToDb,
230
+ urlConnected,
231
+ withSelectedEntities
232
+ } = props;
233
+
234
+ const defaults = useMemo(() => {
235
+ const _defaults = {
236
+ pageSize: controlled_pageSize || 25,
237
+ order: [], // ["-name", "statusCode"] //an array of camelCase display names with - sign to denote reverse
238
+ searchTerm: "",
239
+ page: 1,
240
+ filters: [
241
+ // filters look like this:
242
+ // {
243
+ // selectedFilter: 'textContains', //camel case
244
+ // filterOn: ccDisplayName, //camel case display name
245
+ // filterValue: 'thomas',
246
+ // }
247
+ ],
248
+ ...(props.defaults || {})
249
+ };
250
+ if (isLocalCall && orderByFirstColumn && !_defaults?.order?.length) {
251
+ const r = [getCCDisplayName(convertedSchema.fields[0])];
252
+ _defaults.order = r;
253
+ }
254
+
255
+ if (!syncDisplayOptionsToDb && userSetPageSize) {
256
+ _defaults.pageSize = userSetPageSize;
257
+ }
258
+
259
+ return _defaults;
260
+ }, [
261
+ controlled_pageSize,
262
+ convertedSchema.fields,
263
+ isLocalCall,
264
+ orderByFirstColumn,
265
+ props.defaults,
266
+ syncDisplayOptionsToDb,
267
+ userSetPageSize
268
+ ]);
269
+
270
+ const selectedEntities = withSelectedEntities
271
+ ? getRecordsFromIdMap(reduxFormSelectedEntityIdMap)
272
+ : undefined;
273
+
274
+ props = {
275
+ ...props,
276
+ ...(withSelectedEntities &&
277
+ typeof withSelectedEntities === "string" && {
278
+ [withSelectedEntities]: selectedEntities
279
+ })
280
+ };
281
+
282
+ const _currentParams = useMemo(() => {
283
+ const tmp =
284
+ (urlConnected
285
+ ? getCurrentParamsFromUrl(history.location) //important to use history location and not ownProps.location because for some reason the location path lags one render behind!!
286
+ : reduxFormQueryParams) || {};
287
+ return tmp;
288
+ }, [history, reduxFormQueryParams, urlConnected]);
289
+
290
+ const currentParams = useDeepEqualMemo(_currentParams);
291
+
292
+ const tableParams = useMemo(() => {
293
+ if (!isTableParamsConnected) {
294
+ const setNewParams = newParams => {
295
+ // we always will update the redux params as a workaround for withRouter not always working
296
+ // if inside a redux-connected container https://github.com/ReactTraining/react-router/issues/5037
297
+ change("reduxFormQueryParams", prev => {
298
+ let tmp = newParams;
299
+ if (typeof tmp === "function") tmp = newParams(prev);
300
+ urlConnected && setCurrentParamsOnUrl(tmp, history?.replace);
301
+ return tmp;
302
+ });
303
+ };
304
+
305
+ const dispatchProps = makeDataTableHandlers({
306
+ setNewParams,
307
+ defaults,
308
+ onlyOneFilter: props.onlyOneFilter
309
+ });
310
+
311
+ const changeFormValue = (...args) => change(...args);
312
+
313
+ return {
314
+ changeFormValue,
315
+ selectedEntities,
316
+ ..._tableParams,
317
+ ...dispatchProps,
318
+ isTableParamsConnected: true //let the table know not to do local sorting/filtering etc.
319
+ };
320
+ }
321
+ return _tableParams;
322
+ }, [
323
+ _tableParams,
324
+ change,
325
+ defaults,
326
+ history?.replace,
327
+ isTableParamsConnected,
328
+ props.onlyOneFilter,
329
+ selectedEntities,
330
+ urlConnected
331
+ ]);
332
+
333
+ props = {
334
+ ...props,
335
+ ...tableParams
336
+ };
337
+
338
+ const queryParams = useMemo(() => {
339
+ if (!isTableParamsConnected) {
340
+ const additionalFilterToUse =
341
+ typeof props.additionalFilter === "function"
342
+ ? props.additionalFilter
343
+ : () => props.additionalFilter;
344
+
345
+ const additionalOrFilterToUse =
346
+ typeof props.additionalOrFilter === "function"
347
+ ? props.additionalOrFilter
348
+ : () => props.additionalOrFilter;
349
+
350
+ return getQueryParams({
351
+ doNotCoercePageSize,
352
+ currentParams,
353
+ entities: props.entities, // for local table
354
+ urlConnected,
355
+ defaults,
356
+ schema: convertedSchema,
357
+ isInfinite,
358
+ isLocalCall,
359
+ additionalFilter: additionalFilterToUse,
360
+ additionalOrFilter: additionalOrFilterToUse,
361
+ noOrderError: props.noOrderError,
362
+ isCodeModel: props.isCodeModel,
363
+ ownProps: props
364
+ });
365
+ }
366
+ return {};
367
+ // eslint-disable-next-line react-hooks/exhaustive-deps
368
+ }, [
369
+ props.entities,
370
+ props.noOrderError,
371
+ props.isCodeModel,
372
+ convertedSchema,
373
+ currentParams,
374
+ doNotCoercePageSize,
375
+ isInfinite,
376
+ isLocalCall,
377
+ isTableParamsConnected,
378
+ urlConnected
379
+ ]);
380
+
381
+ props = {
382
+ ...props,
383
+ ...queryParams
384
+ };
385
+
386
+ const {
387
+ addFilters = noop,
388
+ additionalFilters,
389
+ additionalFooterButtons,
390
+ autoFocusSearch,
391
+ cellRenderer,
392
+ children,
393
+ className = "",
394
+ clearFilters = noop,
395
+ compact: _compact = true,
396
+ compactPaging,
397
+ contextMenu = noop,
398
+ controlled_hasNextPage,
399
+ controlled_onRefresh,
400
+ controlled_page,
401
+ controlled_setPage,
402
+ controlled_setPageSize,
403
+ controlled_total,
404
+ currentUser,
405
+ deleteTableConfiguration,
406
+ disabled = false,
407
+ disableSetPageSize,
408
+ doNotShowEmptyRows,
409
+ doNotValidateUntouchedRows,
410
+ editingCellSelectAll,
411
+ entities: _origEntities = [],
412
+ entitiesAcrossPages: _entitiesAcrossPages,
413
+ entityCount,
414
+ errorParsingUrlString,
415
+ expandAllByDefault,
416
+ extraClasses = "",
417
+ extraCompact: _extraCompact,
418
+ filters = [],
419
+ fragment,
420
+ getCellHoverText,
421
+ getRowClassName,
422
+ helperProp,
423
+ hideColumnHeader,
424
+ hideDisplayOptionsIcon,
425
+ hidePageSizeWhenPossible = isSimple ? !withPaging : false,
426
+ hideSelectedCount = isSimple,
427
+ hideSetPageSize,
428
+ hideTotalPages,
429
+ selectedIds,
430
+ isCellEditable,
431
+ isCopyable = true,
432
+ isEntityDisabled = noop,
433
+ isLoading = false,
434
+ isOpenable,
435
+ isSingleSelect = false,
436
+ isViewable,
437
+ recordIdToIsVisibleMap,
438
+ setRecordIdToIsVisibleMap,
439
+ keepSelectionOnPageChange,
440
+ leftOfSearchBarItems,
441
+ maxHeight = 600,
442
+ minimalStyle,
443
+ mustClickCheckboxToSelect,
444
+ noDeselectAll,
445
+ noFooter = isSimple ? !withPaging : false,
446
+ noFullscreenButton = isSimple,
447
+ noHeader = false,
448
+ noPadding = isSimple,
449
+ noRowsFoundMessage,
450
+ noSelect = false,
451
+ noUserSelect,
452
+ onDeselect = noop,
453
+ onDoubleClick,
454
+ onMultiRowSelect = noop,
455
+ onRefresh,
456
+ onRowClick = noop,
457
+ onRowSelect = noop,
458
+ onSingleRowSelect = noop,
459
+ order,
460
+ page = 1,
461
+ pageSize: _pageSize = 10,
462
+ pagingDisabled,
463
+ removeSingleFilter,
464
+ ReactTableProps = {},
465
+ safeQuery,
466
+ searchMenuButton,
467
+ searchTerm,
468
+ selectAllByDefault,
469
+ setNewParams,
470
+ setOrder,
471
+ setPage = noop,
472
+ setPageSize = noop,
473
+ setSearchTerm = noop,
474
+ shouldShowSubComponent,
475
+ showCount = false,
476
+ style = {},
477
+ SubComponent,
478
+ subHeader,
479
+ tableConfigurations,
480
+ tableName,
481
+ topLeftItems,
482
+ upsertFieldOption,
483
+ upsertTableConfiguration,
484
+ variables,
485
+ withCheckboxes = false,
486
+ withDisplayOptions,
487
+ withExpandAndCollapseAllButton,
488
+ withFilter,
489
+ withSearch = !isSimple,
490
+ withSelectAll,
491
+ withSort,
492
+ withTitle = !isSimple,
493
+ noExcessiveCheck
494
+ } = props;
495
+
496
+ const _entities = useMemo(
497
+ () => (reduxFormEntities?.length ? reduxFormEntities : _origEntities) || [],
498
+ [_origEntities, reduxFormEntities]
499
+ );
500
+
501
+ const entities = useDeepEqualMemo(_entities);
502
+
503
+ const entitiesAcrossPages = useDeepEqualMemo(_entitiesAcrossPages);
504
+
505
+ // This is because we need to maintain the reduxFormSelectedEntityIdMap and
506
+ // allOrderedEntities updated
507
+ useEffect(() => {
508
+ !noExcessiveCheck &&
509
+ isBeingCalledExcessively({ uniqName: `dt_entities_${formName}` });
510
+ change("allOrderedEntities", entitiesAcrossPages);
511
+ if (entities.length === 0 || isEmpty(reduxFormSelectedEntityIdMap)) return;
512
+ changeSelectedEntities({
513
+ idMap: reduxFormSelectedEntityIdMap,
514
+ entities,
515
+ change
516
+ });
517
+ // eslint-disable-next-line react-hooks/exhaustive-deps
518
+ }, [
519
+ entitiesAcrossPages,
520
+ reduxFormSelectedEntityIdMap,
521
+ change,
522
+ noExcessiveCheck
523
+ ]);
524
+
525
+ const [tableConfig, setTableConfig] = useState({ fieldOptions: [] });
526
+
527
+ useEffect(() => {
528
+ if (withDisplayOptions) {
529
+ let newTableConfig = {};
530
+ if (syncDisplayOptionsToDb) {
531
+ newTableConfig = tableConfigurations && tableConfigurations[0];
532
+ } else {
533
+ newTableConfig = getTableConfigFromStorage(formName);
534
+ }
535
+ !noExcessiveCheck &&
536
+ isBeingCalledExcessively({ uniqName: `dt_setTableConfig_${formName}` });
537
+ // if the tableConfig is the same as the newTableConfig, don't update
538
+ setTableConfig(prev => {
539
+ if (!newTableConfig) {
540
+ newTableConfig = {
541
+ fieldOptions: []
542
+ };
543
+ if (isEqual(prev, newTableConfig)) {
544
+ return prev;
545
+ } else {
546
+ return newTableConfig;
547
+ }
548
+ } else {
549
+ return newTableConfig;
550
+ }
551
+ });
552
+ }
553
+ }, [
554
+ convertedSchema, // If the schema changes we want to take into account the synced tableConfig again
555
+ formName,
556
+ syncDisplayOptionsToDb,
557
+ tableConfigurations,
558
+ withDisplayOptions,
559
+ noExcessiveCheck
560
+ ]);
561
+
562
+ const schema = useMemo(() => {
563
+ const schema = { ...convertedSchema };
564
+ if (isViewable) {
565
+ schema.fields = [viewColumn, ...schema.fields];
566
+ }
567
+ if (recordIdToIsVisibleMap) {
568
+ schema.fields = [multiViewColumn, ...schema.fields];
569
+ }
570
+ if (isOpenable) {
571
+ schema.fields = [
572
+ openColumn({ onDoubleClick, history }),
573
+ ...schema.fields
574
+ ];
575
+ }
576
+ // this must come before handling orderings.
577
+ schema.fields = schema.fields.map(field => {
578
+ if (field.placementPath) {
579
+ return {
580
+ ...field,
581
+ sortDisabled:
582
+ field.sortDisabled ||
583
+ (typeof field.path === "string" && field.path.includes(".")),
584
+ path: field.placementPath
585
+ };
586
+ } else {
587
+ return field;
588
+ }
589
+ });
590
+
591
+ if (withDisplayOptions) {
592
+ const fieldOptsByPath = keyBy(tableConfig.fieldOptions, "path");
593
+ schema.fields = schema.fields.map(field => {
594
+ const fieldOpt = fieldOptsByPath[field.path];
595
+ let noValsForField = false;
596
+ // only add this hidden column ability if no paging
597
+ if (
598
+ !showForcedHiddenColumns &&
599
+ withDisplayOptions &&
600
+ (isSimple || isInfinite)
601
+ ) {
602
+ noValsForField = entities.every(e => {
603
+ const val = get(e, field.path);
604
+ return field.render
605
+ ? !field.render(val, e, undefined, {
606
+ currentParams,
607
+ setNewParams
608
+ })
609
+ : cellRenderer?.[field.path]
610
+ ? !cellRenderer[field.path](val, e, undefined, {
611
+ currentParams,
612
+ setNewParams
613
+ })
614
+ : !val;
615
+ });
616
+ }
617
+ if (noValsForField) {
618
+ return {
619
+ ...field,
620
+ isHidden: true,
621
+ isForcedHidden: true
622
+ };
623
+ } else if (fieldOpt) {
624
+ return {
625
+ ...field,
626
+ isHidden: fieldOpt.isHidden
627
+ };
628
+ } else {
629
+ return field;
630
+ }
631
+ });
632
+
633
+ const columnOrderings = tableConfig.columnOrderings;
634
+ if (columnOrderings) {
635
+ const fieldsWithOrders = [];
636
+ const fieldsWithoutOrder = [];
637
+ // if a new field has been added since the orderings were set then we want
638
+ // it to be at the end instead of the beginning
639
+ schema.fields.forEach(field => {
640
+ if (columnOrderings.indexOf(field.path) > -1) {
641
+ fieldsWithOrders.push(field);
642
+ } else {
643
+ fieldsWithoutOrder.push(field);
644
+ }
645
+ });
646
+ schema.fields = fieldsWithOrders
647
+ .sort(({ path: path1 }, { path: path2 }) => {
648
+ return (
649
+ columnOrderings.indexOf(path1) - columnOrderings.indexOf(path2)
650
+ );
651
+ })
652
+ .concat(fieldsWithoutOrder);
653
+ // We shouldn't need to update the columnOrderings here, this could lead
654
+ // to unnecessary updates. TableConfig and schema have circular
655
+ // dependencies, which is bad at the moment of updating.
656
+ if (
657
+ !isEqual(
658
+ schema.fields.map(f => f.path),
659
+ columnOrderings
660
+ )
661
+ ) {
662
+ setTableConfig(prev => ({
663
+ ...prev,
664
+ columnOrderings: schema.fields.map(f => f.path)
665
+ }));
666
+ }
667
+ }
668
+ }
669
+ return schema;
670
+ }, [
671
+ cellRenderer,
672
+ convertedSchema,
673
+ currentParams,
674
+ entities,
675
+ history,
676
+ isInfinite,
677
+ isOpenable,
678
+ isSimple,
679
+ isViewable,
680
+ onDoubleClick,
681
+ setNewParams,
682
+ showForcedHiddenColumns,
683
+ tableConfig.columnOrderings,
684
+ tableConfig.fieldOptions,
685
+ withDisplayOptions,
686
+ recordIdToIsVisibleMap
687
+ ]);
688
+
689
+ const {
690
+ moveColumnPersist,
691
+ persistPageSize,
692
+ resetDefaultVisibility,
693
+ resizePersist,
694
+ updateColumnVisibility,
695
+ updateTableDisplayDensity
696
+ } = useMemo(() => {
697
+ let resetDefaultVisibility;
698
+ let updateColumnVisibility;
699
+ let updateTableDisplayDensity;
700
+ let persistPageSize;
701
+ let moveColumnPersist;
702
+ let resizePersist = noop;
703
+
704
+ if (withDisplayOptions) {
705
+ const fieldOptsByPath = keyBy(tableConfig.fieldOptions, "path");
706
+ if (syncDisplayOptionsToDb) {
707
+ // sync up to db
708
+ // There must be a better way to set this variable...
709
+ let tableConfigurationId;
710
+ resetDefaultVisibility = function () {
711
+ tableConfigurationId = tableConfig.id;
712
+ if (tableConfigurationId) {
713
+ deleteTableConfiguration(tableConfigurationId);
714
+ }
715
+ };
716
+ updateColumnVisibility = function ({ shouldShow, path }) {
717
+ if (tableConfigurationId) {
718
+ const existingFieldOpt = fieldOptsByPath[path] || {};
719
+ upsertFieldOption({
720
+ id: existingFieldOpt.id,
721
+ path,
722
+ isHidden: !shouldShow,
723
+ tableConfigurationId
724
+ });
725
+ } else {
726
+ upsertTableConfiguration({
727
+ userId: currentUser.user.id,
728
+ formName,
729
+ fieldOptions: [
730
+ {
731
+ path,
732
+ isHidden: !shouldShow
733
+ }
734
+ ]
735
+ });
736
+ }
737
+ };
738
+ } else {
739
+ const syncStorage = newTableConfig => {
740
+ setTableConfig(newTableConfig);
741
+ window.localStorage.setItem(formName, JSON.stringify(newTableConfig));
742
+ };
743
+
744
+ //sync display options with localstorage
745
+ resetDefaultVisibility = function () {
746
+ setTableConfig({ fieldOptions: [] });
747
+ window.localStorage.removeItem(formName);
748
+ };
749
+ updateColumnVisibility = function ({ path, paths, shouldShow }) {
750
+ const newFieldOpts = {
751
+ ...fieldOptsByPath
752
+ };
753
+ const pathsToUse = paths ? paths : [path];
754
+ pathsToUse.forEach(path => {
755
+ newFieldOpts[path] = { path, isHidden: !shouldShow };
756
+ });
757
+ syncStorage({ ...tableConfig, fieldOptions: toArray(newFieldOpts) });
758
+ };
759
+ updateTableDisplayDensity = function (density) {
760
+ syncStorage({ ...tableConfig, density: density });
761
+ };
762
+ persistPageSize = function (pageSize) {
763
+ syncStorage({ ...tableConfig, userSetPageSize: pageSize });
764
+ };
765
+ moveColumnPersist = function ({ oldIndex, newIndex }) {
766
+ // we might already have an array of the fields [path1, path2, ..etc]
767
+ const columnOrderings =
768
+ tableConfig.columnOrderings ||
769
+ schema.fields.map(({ path }) => path); // columnOrderings is [path1, path2, ..etc]
770
+ syncStorage({
771
+ ...tableConfig,
772
+ columnOrderings: arrayMove(columnOrderings, oldIndex, newIndex)
773
+ });
774
+ };
775
+ resizePersist = function (newResized) {
776
+ syncStorage({ ...tableConfig, resized: newResized });
777
+ };
778
+ }
779
+ }
780
+ return {
781
+ moveColumnPersist,
782
+ persistPageSize,
783
+ resetDefaultVisibility,
784
+ resizePersist,
785
+ updateColumnVisibility,
786
+ updateTableDisplayDensity
787
+ };
788
+ }, [
789
+ currentUser?.user?.id,
790
+ deleteTableConfiguration,
791
+ formName,
792
+ schema.fields,
793
+ syncDisplayOptionsToDb,
794
+ tableConfig,
795
+ upsertFieldOption,
796
+ upsertTableConfiguration,
797
+ withDisplayOptions
798
+ ]);
799
+
800
+ let compact = _compact;
801
+ let extraCompact = _extraCompact;
802
+
803
+ if (withDisplayOptions && tableConfig.density) {
804
+ compact = tableConfig.density === "compact";
805
+ extraCompact = tableConfig.density === "extraCompact";
806
+ }
807
+
808
+ const resized = useMemo(
809
+ () => tableConfig.resized || [],
810
+ [tableConfig?.resized]
811
+ );
812
+
813
+ const pageSize = controlled_pageSize || _pageSize;
814
+
815
+ const [expandedEntityIdMap, setExpandedEntityIdMap] = useState(() => {
816
+ const initialExpandedEntityIdMap = {};
817
+ if (expandAllByDefault) {
818
+ entities.forEach(entity => {
819
+ initialExpandedEntityIdMap[entity.id || entity.code] = true;
820
+ });
821
+ }
822
+ return initialExpandedEntityIdMap;
823
+ });
824
+
825
+ const updateValidation = useCallback(
826
+ (entities, newCellValidate) => {
827
+ const tableWideErr = validateTableWideErrors({
828
+ entities,
829
+ schema,
830
+ newCellValidate
831
+ });
832
+ change("reduxFormCellValidation", tableWideErr);
833
+ },
834
+ [schema, change]
835
+ );
836
+
837
+ const updateEntitiesHelper = useCallback(
838
+ (ents, fn) => {
839
+ const [nextState, patches, inversePatches] = produceWithPatches(ents, fn);
840
+ if (!inversePatches.length) return;
841
+ const thatNewNew = [...nextState];
842
+ thatNewNew.isDirty = true;
843
+ change("reduxFormEntities", thatNewNew);
844
+ setEntitiesUndoRedoStack(prev => ({
845
+ ...omitBy(prev, (v, k) => {
846
+ return toNumber(k) > prev.currentVersion + 1;
847
+ }),
848
+ currentVersion: prev.currentVersion + 1,
849
+ [prev.currentVersion + 1]: {
850
+ inversePatches,
851
+ patches
852
+ }
853
+ }));
854
+ },
855
+ [change]
856
+ );
857
+
858
+ const formatAndValidateEntities = useCallback(
859
+ (entities, { useDefaultValues, indexToStartAt } = {}) => {
860
+ const editableFields = schema.fields.filter(f => !f.isNotEditable);
861
+ const validationErrors = {};
862
+
863
+ const newEnts = immer(entities, entities => {
864
+ entities.forEach((e, index) => {
865
+ editableFields.forEach(columnSchema => {
866
+ if (useDefaultValues) {
867
+ if (e[columnSchema.path] === undefined) {
868
+ if (isFunction(columnSchema.defaultValue)) {
869
+ e[columnSchema.path] = columnSchema.defaultValue(
870
+ index + indexToStartAt,
871
+ e
872
+ );
873
+ } else e[columnSchema.path] = columnSchema.defaultValue;
874
+ }
875
+ }
876
+ //mutative
877
+ const { error } = editCellHelper({
878
+ entity: e,
879
+ columnSchema,
880
+ newVal: e[columnSchema.path]
881
+ });
882
+ if (error) {
883
+ const rowId = getIdOrCodeOrIndex(e, index);
884
+ validationErrors[`${rowId}:${columnSchema.path}`] = error;
885
+ }
886
+ });
887
+ });
888
+ });
889
+ return {
890
+ newEnts,
891
+ validationErrors
892
+ };
893
+ },
894
+ [schema.fields]
895
+ );
896
+
897
+ const updateValidationHelper = useCallback(() => {
898
+ updateValidation(entities, reduxFormCellValidation);
899
+ }, [entities, reduxFormCellValidation, updateValidation]);
900
+
901
+ const addEditableTableEntities = useCallback(
902
+ incomingEnts => {
903
+ updateEntitiesHelper(entities, entities => {
904
+ const newEntities = incomingEnts.map(e => ({
905
+ ...e,
906
+ id: e.id || nanoid(),
907
+ _isClean: false
908
+ }));
909
+
910
+ const { newEnts, validationErrors } = formatAndValidateEntities(
911
+ newEntities,
912
+ {
913
+ useDefaultValues: true,
914
+ indexToStartAt: entities.length
915
+ }
916
+ );
917
+ if (every(entities, "_isClean")) {
918
+ forEach(newEnts, (e, i) => {
919
+ entities[i] = e;
920
+ });
921
+ } else {
922
+ entities.splice(entities.length, 0, ...newEnts);
923
+ }
924
+
925
+ updateValidation(entities, {
926
+ ...reduxFormCellValidation,
927
+ ...validationErrors
928
+ });
929
+ });
930
+ },
931
+ [
932
+ entities,
933
+ formatAndValidateEntities,
934
+ reduxFormCellValidation,
935
+ updateEntitiesHelper,
936
+ updateValidation
937
+ ]
938
+ );
939
+
940
+ const getEditableTableInfoAndThrowFormError = useCallback(() => {
941
+ const { entsToUse, validationToUse } = removeCleanRows(
942
+ reduxFormEntities,
943
+ reduxFormCellValidation
944
+ );
945
+ const validationWTableErrs = validateTableWideErrors({
946
+ entities: entsToUse,
947
+ schema,
948
+ newCellValidate: validationToUse
949
+ });
950
+
951
+ if (!entsToUse?.length) {
952
+ throwFormError(
953
+ "Please add at least one row to the table before submitting."
954
+ );
955
+ }
956
+ const invalid =
957
+ isEmpty(validationWTableErrs) || !some(validationWTableErrs, v => v)
958
+ ? undefined
959
+ : validationWTableErrs;
960
+
961
+ if (invalid) {
962
+ throwFormError("Please fix the errors in the table before submitting.");
963
+ }
964
+
965
+ return entsToUse;
966
+ }, [reduxFormCellValidation, reduxFormEntities, schema]);
967
+
968
+ useEffect(() => {
969
+ // This is bad practice, we shouldn't be assigning value to an
970
+ // external variable
971
+ if (helperProp) {
972
+ helperProp.updateValidationHelper = updateValidationHelper;
973
+ helperProp.addEditableTableEntities = addEditableTableEntities;
974
+ helperProp.getEditableTableInfoAndThrowFormError =
975
+ getEditableTableInfoAndThrowFormError;
976
+ }
977
+ }, [
978
+ addEditableTableEntities,
979
+ getEditableTableInfoAndThrowFormError,
980
+ helperProp,
981
+ updateValidationHelper
982
+ ]);
983
+
984
+ const handleRowMove = useCallback(
985
+ (type, shiftHeld) => e => {
986
+ e.preventDefault();
987
+ e.stopPropagation();
988
+ let newIdMap = {};
989
+ const lastSelectedEnt = getLastSelectedEntity(
990
+ reduxFormSelectedEntityIdMap
991
+ );
992
+
993
+ if (noSelect) return;
994
+ if (lastSelectedEnt) {
995
+ let lastSelectedIndex = entities.findIndex(
996
+ ent => ent === lastSelectedEnt
997
+ );
998
+ if (lastSelectedIndex === -1) {
999
+ if (lastSelectedEnt.id !== undefined) {
1000
+ lastSelectedIndex = entities.findIndex(
1001
+ ent => ent.id === lastSelectedEnt.id
1002
+ );
1003
+ } else if (lastSelectedEnt.code !== undefined) {
1004
+ lastSelectedIndex = entities.findIndex(
1005
+ ent => ent.code === lastSelectedEnt.code
1006
+ );
1007
+ }
1008
+ }
1009
+ if (lastSelectedIndex === -1) {
1010
+ return;
1011
+ }
1012
+ const newEntToSelect = getNewEntToSelect({
1013
+ type,
1014
+ lastSelectedIndex,
1015
+ entities,
1016
+ isEntityDisabled
1017
+ });
1018
+
1019
+ if (!newEntToSelect) return;
1020
+ if (shiftHeld && !isSingleSelect) {
1021
+ if (
1022
+ reduxFormSelectedEntityIdMap[
1023
+ newEntToSelect.id || newEntToSelect.code
1024
+ ]
1025
+ ) {
1026
+ //the entity being moved to has already been selected
1027
+ newIdMap = omit(reduxFormSelectedEntityIdMap, [
1028
+ lastSelectedEnt.id || lastSelectedEnt.code
1029
+ ]);
1030
+ newIdMap[newEntToSelect.id || newEntToSelect.code].time =
1031
+ Date.now() + 1;
1032
+ } else {
1033
+ //the entity being moved to has NOT been selected yet
1034
+ newIdMap = {
1035
+ ...reduxFormSelectedEntityIdMap,
1036
+ [newEntToSelect.id || newEntToSelect.code]: {
1037
+ entity: newEntToSelect,
1038
+ time: Date.now()
1039
+ }
1040
+ };
1041
+ }
1042
+ } else {
1043
+ //no shiftHeld
1044
+ newIdMap[newEntToSelect.id || newEntToSelect.code] = {
1045
+ entity: newEntToSelect,
1046
+ time: Date.now()
1047
+ };
1048
+ }
1049
+ }
1050
+
1051
+ finalizeSelection({
1052
+ idMap: newIdMap,
1053
+ entities,
1054
+ props: {
1055
+ onDeselect,
1056
+ onSingleRowSelect,
1057
+ onMultiRowSelect,
1058
+ noDeselectAll,
1059
+ onRowSelect,
1060
+ noSelect,
1061
+ change
1062
+ }
1063
+ });
1064
+ },
1065
+ [
1066
+ change,
1067
+ entities,
1068
+ isEntityDisabled,
1069
+ isSingleSelect,
1070
+ noDeselectAll,
1071
+ noSelect,
1072
+ onDeselect,
1073
+ onMultiRowSelect,
1074
+ onRowSelect,
1075
+ onSingleRowSelect,
1076
+ reduxFormSelectedEntityIdMap
1077
+ ]
1078
+ );
1079
+
1080
+ const primarySelectedCellId = useMemo(() => {
1081
+ for (const k of Object.keys(selectedCells)) {
1082
+ if (selectedCells[k] === PRIMARY_SELECTED_VAL) {
1083
+ return k;
1084
+ }
1085
+ }
1086
+ }, [selectedCells]);
1087
+
1088
+ const startCellEdit = useCallback(
1089
+ (cellId, shouldClear) => {
1090
+ // console.log(`startCellEdit initialValue:`, initialValue)
1091
+ // This initial value is not needed if the event is propagated accordingly.
1092
+ // This is directly connected to the RenderCell component, which does set
1093
+ // the initial value.
1094
+ // change("shouldEditableCellInputBeCleared", undefined);
1095
+ if (shouldClear) {
1096
+ change("shouldEditableCellInputBeCleared", true);
1097
+ } else {
1098
+ change("shouldEditableCellInputBeCleared", false);
1099
+ }
1100
+ change("reduxFormEditingCell", prev => {
1101
+ //check if the cell is already selected and editing and if so, don't change it
1102
+ if (prev === cellId) return cellId;
1103
+ setSelectedCells(prev => {
1104
+ if (prev[cellId] === PRIMARY_SELECTED_VAL) {
1105
+ return prev;
1106
+ }
1107
+ return { ...prev, [cellId]: PRIMARY_SELECTED_VAL };
1108
+ });
1109
+ return cellId;
1110
+ });
1111
+ },
1112
+ [change]
1113
+ );
1114
+
1115
+ const handleEnterStartCellEdit = useCallback(
1116
+ e => {
1117
+ e.stopPropagation();
1118
+ startCellEdit(primarySelectedCellId);
1119
+ },
1120
+ [primarySelectedCellId, startCellEdit]
1121
+ );
1122
+
1123
+ const handleDeleteCell = useCallback(() => {
1124
+ const newCellValidate = {
1125
+ ...reduxFormCellValidation
1126
+ };
1127
+ if (isEmpty(selectedCells)) return;
1128
+ const rowIds = [];
1129
+ updateEntitiesHelper(entities, entities => {
1130
+ const entityIdToEntity = getEntityIdToEntity(entities);
1131
+ Object.keys(selectedCells).forEach(cellId => {
1132
+ const [rowId, path] = cellId.split(":");
1133
+ rowIds.push(rowId);
1134
+ const entity = entityIdToEntity[rowId].e;
1135
+ delete entity._isClean;
1136
+ const { error } = editCellHelper({
1137
+ entity,
1138
+ path,
1139
+ schema,
1140
+ newVal: ""
1141
+ });
1142
+ if (error) {
1143
+ newCellValidate[cellId] = error;
1144
+ } else {
1145
+ delete newCellValidate[cellId];
1146
+ }
1147
+ });
1148
+ updateValidation(entities, newCellValidate);
1149
+ });
1150
+ }, [
1151
+ entities,
1152
+ reduxFormCellValidation,
1153
+ schema,
1154
+ selectedCells,
1155
+ updateEntitiesHelper,
1156
+ updateValidation
1157
+ ]);
1158
+
1159
+ const handleCopySelectedCells = useCallback(
1160
+ e => {
1161
+ // if the current selection is consecutive cells then copy with
1162
+ // tabs between. if not then just select primary selected cell
1163
+ if (isEmpty(selectedCells)) return;
1164
+ const pathToIndex = getFieldPathToIndex(schema);
1165
+ const entityIdToEntity = getEntityIdToEntity(entities);
1166
+ const selectionGrid = [];
1167
+ let firstRowIndex;
1168
+ let firstCellIndex;
1169
+ Object.keys(selectedCells).forEach(key => {
1170
+ const [rowId, path] = key.split(":");
1171
+ const eInfo = entityIdToEntity[rowId];
1172
+ if (eInfo) {
1173
+ if (firstRowIndex === undefined || eInfo.i < firstRowIndex) {
1174
+ firstRowIndex = eInfo.i;
1175
+ }
1176
+ if (!selectionGrid[eInfo.i]) {
1177
+ selectionGrid[eInfo.i] = [];
1178
+ }
1179
+ const cellIndex = pathToIndex[path];
1180
+ if (firstCellIndex === undefined || cellIndex < firstCellIndex) {
1181
+ firstCellIndex = cellIndex;
1182
+ }
1183
+ selectionGrid[eInfo.i][cellIndex] = true;
1184
+ }
1185
+ });
1186
+ if (firstRowIndex === undefined) return;
1187
+ const allRows = getAllRows(e);
1188
+ let fullCellText = "";
1189
+ const fullJson = [];
1190
+ times(selectionGrid.length, i => {
1191
+ const row = selectionGrid[i];
1192
+ if (fullCellText) {
1193
+ fullCellText += "\n";
1194
+ }
1195
+ if (!row) {
1196
+ return;
1197
+ } else {
1198
+ const jsonRow = [];
1199
+ // ignore header
1200
+ let [rowCopyText, json] = getRowCopyText(allRows[i + 1]);
1201
+ rowCopyText = rowCopyText.split("\t");
1202
+ times(row.length, i => {
1203
+ const cell = row[i];
1204
+ if (cell) {
1205
+ fullCellText += rowCopyText[i];
1206
+ jsonRow.push(json[i]);
1207
+ }
1208
+ if (i !== row.length - 1 && i >= firstCellIndex)
1209
+ fullCellText += "\t";
1210
+ });
1211
+ fullJson.push(jsonRow);
1212
+ }
1213
+ });
1214
+ if (!fullCellText) return window.toastr.warning("No text to copy");
1215
+
1216
+ handleCopyHelper(fullCellText, fullJson, "Selected cells copied");
1217
+ },
1218
+ [entities, selectedCells, schema]
1219
+ );
1220
+
1221
+ const handleCopySelectedRows = useCallback(
1222
+ (selectedRecords, e) => {
1223
+ const idToIndex = entities.reduce((acc, e, i) => {
1224
+ acc[e.id || e.code] = i;
1225
+ return acc;
1226
+ }, {});
1227
+
1228
+ //index 0 of the table is the column titles
1229
+ //must add 1 to rowNum
1230
+ const rowNumbersToCopy = selectedRecords
1231
+ .map(rec => idToIndex[rec.id || rec.code] + 1)
1232
+ .sort();
1233
+
1234
+ if (!rowNumbersToCopy.length) return;
1235
+ rowNumbersToCopy.unshift(0); //add in the header row
1236
+ try {
1237
+ const allRowEls = getAllRows(e);
1238
+ if (!allRowEls) return;
1239
+ const rowEls = rowNumbersToCopy.map(i => allRowEls[i]);
1240
+
1241
+ handleCopyRows(rowEls, {
1242
+ onFinishMsg: "Selected rows copied"
1243
+ });
1244
+ } catch (error) {
1245
+ console.error(`error:`, error);
1246
+ window.toastr.error("Error copying rows.");
1247
+ }
1248
+ },
1249
+ [entities]
1250
+ );
1251
+
1252
+ const handleCopyHotkey = useCallback(
1253
+ e => {
1254
+ if (isCellEditable) {
1255
+ handleCopySelectedCells(e);
1256
+ } else {
1257
+ handleCopySelectedRows(
1258
+ getRecordsFromIdMap(reduxFormSelectedEntityIdMap),
1259
+ e
1260
+ );
1261
+ }
1262
+ },
1263
+ [
1264
+ handleCopySelectedCells,
1265
+ handleCopySelectedRows,
1266
+ isCellEditable,
1267
+ reduxFormSelectedEntityIdMap
1268
+ ]
1269
+ );
1270
+
1271
+ const handleCut = useCallback(
1272
+ e => {
1273
+ handleDeleteCell();
1274
+ handleCopyHotkey(e);
1275
+ },
1276
+ [handleCopyHotkey, handleDeleteCell]
1277
+ );
1278
+
1279
+ const flashTableBorder = () => {
1280
+ try {
1281
+ const table = tableRef.current.tableRef;
1282
+ table.classList.add("tgBorderBlue");
1283
+ setTimeout(() => {
1284
+ table.classList.remove("tgBorderBlue");
1285
+ }, 300);
1286
+ } catch (e) {
1287
+ console.error(`err when flashing table border:`, e);
1288
+ }
1289
+ };
1290
+
1291
+ const handleUndo = useCallback(() => {
1292
+ if (entitiesUndoRedoStack.currentVersion > 0) {
1293
+ flashTableBorder();
1294
+ const nextState = applyPatches(
1295
+ entities,
1296
+ entitiesUndoRedoStack[entitiesUndoRedoStack.currentVersion]
1297
+ .inversePatches
1298
+ );
1299
+ const { newEnts, validationErrors } =
1300
+ formatAndValidateEntities(nextState);
1301
+ setEntitiesUndoRedoStack(prev => ({
1302
+ ...prev,
1303
+ currentVersion: prev.currentVersion - 1
1304
+ }));
1305
+ updateValidation(newEnts, validationErrors);
1306
+ change("reduxFormEntities", newEnts);
1307
+ }
1308
+ }, [
1309
+ change,
1310
+ entities,
1311
+ formatAndValidateEntities,
1312
+ entitiesUndoRedoStack,
1313
+ updateValidation
1314
+ ]);
1315
+
1316
+ const handleRedo = useCallback(() => {
1317
+ const nextV = entitiesUndoRedoStack.currentVersion + 1;
1318
+ if (entitiesUndoRedoStack[nextV]) {
1319
+ flashTableBorder();
1320
+ const nextState = applyPatches(
1321
+ entities,
1322
+ entitiesUndoRedoStack[nextV].patches
1323
+ );
1324
+ const { newEnts, validationErrors } =
1325
+ formatAndValidateEntities(nextState);
1326
+ change("reduxFormEntities", newEnts);
1327
+ updateValidation(newEnts, validationErrors);
1328
+ setEntitiesUndoRedoStack(prev => ({
1329
+ ...prev,
1330
+ currentVersion: nextV
1331
+ }));
1332
+ }
1333
+ }, [
1334
+ change,
1335
+ entities,
1336
+ formatAndValidateEntities,
1337
+ entitiesUndoRedoStack,
1338
+ updateValidation
1339
+ ]);
1340
+
1341
+ const handleSelectAllRows = useCallback(
1342
+ e => {
1343
+ if (isSingleSelect) return;
1344
+ e.preventDefault();
1345
+
1346
+ if (isCellEditable) {
1347
+ const schemaPaths = schema.fields.map(f => f.path);
1348
+ const newSelectedCells = {};
1349
+ entities.forEach((entity, i) => {
1350
+ if (isEntityDisabled(entity)) return;
1351
+ const entityId = getIdOrCodeOrIndex(entity, i);
1352
+ schemaPaths.forEach(p => {
1353
+ newSelectedCells[`${entityId}:${p}`] = true;
1354
+ });
1355
+ });
1356
+ setSelectedCells(newSelectedCells);
1357
+ } else {
1358
+ const newIdMap = {};
1359
+
1360
+ entities.forEach((entity, i) => {
1361
+ if (isEntityDisabled(entity)) return;
1362
+ const entityId = getIdOrCodeOrIndex(entity, i);
1363
+ newIdMap[entityId] = { entity };
1364
+ });
1365
+ finalizeSelection({
1366
+ idMap: newIdMap,
1367
+ entities,
1368
+ props: {
1369
+ onDeselect,
1370
+ onSingleRowSelect,
1371
+ onMultiRowSelect,
1372
+ noDeselectAll,
1373
+ onRowSelect,
1374
+ noSelect,
1375
+ change
1376
+ }
1377
+ });
1378
+ }
1379
+ },
1380
+ [
1381
+ change,
1382
+ entities,
1383
+ isCellEditable,
1384
+ isEntityDisabled,
1385
+ isSingleSelect,
1386
+ noDeselectAll,
1387
+ noSelect,
1388
+ onDeselect,
1389
+ onMultiRowSelect,
1390
+ onRowSelect,
1391
+ onSingleRowSelect,
1392
+ schema.fields
1393
+ ]
1394
+ );
1395
+
1396
+ const hotKeys = useMemo(
1397
+ () => [
1398
+ {
1399
+ global: false,
1400
+ combo: "up",
1401
+ label: "Move Up a Row",
1402
+ onKeyDown: handleRowMove("up")
1403
+ },
1404
+ {
1405
+ global: false,
1406
+ combo: "down",
1407
+ label: "Move Down a Row",
1408
+ onKeyDown: handleRowMove("down")
1409
+ },
1410
+ {
1411
+ global: false,
1412
+ combo: "up+shift",
1413
+ label: "Move Up a Row",
1414
+ onKeyDown: handleRowMove("up", true)
1415
+ },
1416
+ ...(isCellEditable
1417
+ ? [
1418
+ {
1419
+ global: false,
1420
+ combo: "enter",
1421
+ label: "Enter -> Start Cell Edit",
1422
+ onKeyDown: handleEnterStartCellEdit
1423
+ },
1424
+ {
1425
+ global: false,
1426
+ combo: "mod+x",
1427
+ label: "Cut",
1428
+ onKeyDown: handleCut
1429
+ },
1430
+ {
1431
+ global: false,
1432
+ combo: IS_LINUX ? "alt+z" : "mod+z",
1433
+ label: "Undo",
1434
+ onKeyDown: handleUndo
1435
+ },
1436
+ {
1437
+ global: false,
1438
+ combo: IS_LINUX ? "alt+shift+z" : "mod+shift+z",
1439
+ label: "Redo",
1440
+ onKeyDown: handleRedo
1441
+ },
1442
+ {
1443
+ global: false,
1444
+ combo: "backspace",
1445
+ label: "Delete Cell",
1446
+ onKeyDown: handleDeleteCell
1447
+ }
1448
+ ]
1449
+ : []),
1450
+ {
1451
+ global: false,
1452
+ combo: "down+shift",
1453
+ label: "Move Down a Row",
1454
+ onKeyDown: handleRowMove("down", true)
1455
+ },
1456
+ {
1457
+ global: false,
1458
+ combo: "mod + c",
1459
+ label: "Copy rows",
1460
+ onKeyDown: handleCopyHotkey
1461
+ },
1462
+ {
1463
+ global: false,
1464
+ combo: "mod + a",
1465
+ label: "Select rows",
1466
+ onKeyDown: handleSelectAllRows
1467
+ }
1468
+ ],
1469
+ [
1470
+ handleCopyHotkey,
1471
+ handleCut,
1472
+ handleDeleteCell,
1473
+ handleEnterStartCellEdit,
1474
+ handleRedo,
1475
+ handleRowMove,
1476
+ handleSelectAllRows,
1477
+ handleUndo,
1478
+ isCellEditable
1479
+ ]
1480
+ );
1481
+
1482
+ const { handleKeyDown, handleKeyUp } = useHotkeys(hotKeys);
1483
+ const [columns, setColumns] = useState([]);
1484
+ const [fullscreen, setFullscreen] = useState(false);
1485
+ const [selectingAll, setSelectingAll] = useState(false);
1486
+
1487
+ // format in the schema shouldn't be something that changes the value
1488
+ // everytime, it produces weird behavior since it keeps rerendering,
1489
+ // we should make enforce the user set the format as something that
1490
+ // "formats", not "changes".
1491
+ useEffect(() => {
1492
+ const formatAndValidateTableInitial = () => {
1493
+ const { newEnts, validationErrors } = formatAndValidateEntities(entities);
1494
+ const toKeep = {};
1495
+ //on the initial load we want to keep any async table wide errors
1496
+ forEach(reduxFormCellValidation, (v, k) => {
1497
+ if (v && v._isTableAsyncWideError) {
1498
+ toKeep[k] = v;
1499
+ }
1500
+ });
1501
+ change("reduxFormEntities", prev => {
1502
+ if (!isEqual(prev, newEnts)) {
1503
+ return newEnts;
1504
+ }
1505
+ return prev;
1506
+ });
1507
+ updateValidation(newEnts, {
1508
+ ...toKeep,
1509
+ ...validationErrors
1510
+ });
1511
+ };
1512
+ isCellEditable && formatAndValidateTableInitial();
1513
+ }, [
1514
+ change,
1515
+ entities,
1516
+ formatAndValidateEntities,
1517
+ isCellEditable,
1518
+ reduxFormCellValidation,
1519
+ updateValidation
1520
+ ]);
1521
+
1522
+ const handlePaste = useCallback(
1523
+ e => {
1524
+ if (isCellEditable) {
1525
+ if (isEmpty(selectedCells)) return;
1526
+ try {
1527
+ let pasteData = [];
1528
+ let toPaste;
1529
+ if (window.clipboardData && window.clipboardData.getData) {
1530
+ // IE
1531
+ toPaste = window.clipboardData.getData("Text");
1532
+ } else if (e.clipboardData && e.clipboardData.getData) {
1533
+ toPaste = e.clipboardData.getData("text/plain");
1534
+ }
1535
+ const jsonToPaste = e.clipboardData.getData("application/json");
1536
+ try {
1537
+ const pastedJson = [];
1538
+ JSON.parse(jsonToPaste).forEach(row => {
1539
+ const newRow = [];
1540
+ Object.values(row).forEach(cell => {
1541
+ const cellVal = JSON.parse(cell);
1542
+ newRow.push(cellVal);
1543
+ });
1544
+ pastedJson.push(newRow);
1545
+ });
1546
+ pasteData = pastedJson;
1547
+ // try to remove the header row if it exists
1548
+ if (
1549
+ pasteData[0] &&
1550
+ pasteData[0][0] &&
1551
+ pasteData[0][0].__isHeaderCell
1552
+ ) {
1553
+ pasteData = pasteData.slice(1);
1554
+ }
1555
+ } catch (e) {
1556
+ if (toPaste.includes(",")) {
1557
+ //try papaparsing it out as a csv if it contains commas
1558
+ try {
1559
+ const { data, errors } = papaparse.parse(toPaste, {
1560
+ header: false
1561
+ });
1562
+ if (data?.length && !errors?.length) {
1563
+ pasteData = data;
1564
+ }
1565
+ } catch (error) {
1566
+ console.error(`error p982qhgpf9qh`, error);
1567
+ }
1568
+ }
1569
+ }
1570
+ pasteData = pasteData.length ? pasteData : defaultParsePaste(toPaste);
1571
+
1572
+ if (!pasteData || !pasteData.length) return;
1573
+
1574
+ if (pasteData.length === 1 && pasteData[0].length === 1) {
1575
+ const newCellValidate = {
1576
+ ...reduxFormCellValidation
1577
+ };
1578
+ // single paste value, fill all cells with value
1579
+ const newVal = pasteData[0][0];
1580
+ updateEntitiesHelper(entities, entities => {
1581
+ const entityIdToEntity = getEntityIdToEntity(entities);
1582
+ Object.keys(selectedCells).forEach(cellId => {
1583
+ const [rowId, path] = cellId.split(":");
1584
+
1585
+ const entity = entityIdToEntity[rowId].e;
1586
+ delete entity._isClean;
1587
+ const { error } = editCellHelper({
1588
+ entity,
1589
+ path,
1590
+ schema,
1591
+ newVal: formatPasteData({ newVal, path, schema })
1592
+ });
1593
+ if (error) {
1594
+ newCellValidate[cellId] = error;
1595
+ } else {
1596
+ delete newCellValidate[cellId];
1597
+ }
1598
+ });
1599
+ updateValidation(entities, newCellValidate);
1600
+ });
1601
+ } else {
1602
+ // handle paste in same format
1603
+ if (primarySelectedCellId) {
1604
+ const newCellValidate = {
1605
+ ...reduxFormCellValidation
1606
+ };
1607
+
1608
+ const newSelectedCells = { ...selectedCells };
1609
+ updateEntitiesHelper(entities, entities => {
1610
+ const entityIdToEntity = getEntityIdToEntity(entities);
1611
+ const [rowId, primaryCellPath] =
1612
+ primarySelectedCellId.split(":");
1613
+ const primaryEntityInfo = entityIdToEntity[rowId];
1614
+ const startIndex = primaryEntityInfo.i;
1615
+ const endIndex = primaryEntityInfo.i + pasteData.length;
1616
+ for (let i = startIndex; i < endIndex; i++) {
1617
+ if (!entities[i]) {
1618
+ entities[i] = { id: nanoid() };
1619
+ }
1620
+ }
1621
+ const entitiesToManipulate = entities.slice(
1622
+ startIndex,
1623
+ endIndex
1624
+ );
1625
+ const pathToIndex = getFieldPathToIndex(schema);
1626
+ const indexToPath = invert(pathToIndex);
1627
+ const startCellIndex = pathToIndex[primaryCellPath];
1628
+ pasteData.forEach((row, i) => {
1629
+ row.forEach((newVal, j) => {
1630
+ if (newVal) {
1631
+ const cellIndexToChange = startCellIndex + j;
1632
+ const entity = entitiesToManipulate[i];
1633
+ if (entity) {
1634
+ delete entity._isClean;
1635
+ const path = indexToPath[cellIndexToChange];
1636
+ if (path) {
1637
+ const { error } = editCellHelper({
1638
+ entity,
1639
+ path,
1640
+ schema,
1641
+ newVal: formatPasteData({
1642
+ newVal,
1643
+ path,
1644
+ schema
1645
+ })
1646
+ });
1647
+ const cellId = `${getIdOrCodeOrIndex(entity)}:${path}`;
1648
+ if (!newSelectedCells[cellId]) {
1649
+ newSelectedCells[cellId] = true;
1650
+ }
1651
+ if (error) {
1652
+ newCellValidate[cellId] = error;
1653
+ } else {
1654
+ delete newCellValidate[cellId];
1655
+ }
1656
+ }
1657
+ }
1658
+ }
1659
+ });
1660
+ });
1661
+ updateValidation(entities, newCellValidate);
1662
+ });
1663
+ setSelectedCells(newSelectedCells);
1664
+ }
1665
+ }
1666
+ } catch (error) {
1667
+ console.error(`error:`, error);
1668
+ }
1669
+ }
1670
+ },
1671
+ [
1672
+ entities,
1673
+ isCellEditable,
1674
+ primarySelectedCellId,
1675
+ reduxFormCellValidation,
1676
+ schema,
1677
+ selectedCells,
1678
+ updateEntitiesHelper,
1679
+ updateValidation
1680
+ ]
1681
+ );
1682
+
1683
+ useEffect(() => {
1684
+ document.addEventListener("paste", handlePaste);
1685
+ return () => {
1686
+ document.removeEventListener("paste", handlePaste);
1687
+ };
1688
+ }, [handlePaste]);
1689
+
1690
+ useEffect(() => {
1691
+ addFilters(additionalFilters);
1692
+ // eslint-disable-next-line react-hooks/exhaustive-deps
1693
+ }, [additionalFilters]);
1694
+
1695
+ useEffect(() => {
1696
+ setColumns(
1697
+ schema.fields
1698
+ ? schema.fields.reduce((col, field, i) => {
1699
+ if (field.isHidden) {
1700
+ return col;
1701
+ }
1702
+ return col.concat({
1703
+ ...field,
1704
+ columnIndex: i
1705
+ });
1706
+ }, [])
1707
+ : []
1708
+ );
1709
+ }, [schema?.fields]);
1710
+
1711
+ const setSelectedIds = useCallback(
1712
+ (selectedIds, scrollToFirst) => {
1713
+ const idArray = Array.isArray(selectedIds) ? selectedIds : [selectedIds];
1714
+ const selectedEntities = entities.filter(
1715
+ e => idArray.indexOf(getIdOrCodeOrIndex(e)) > -1 && !isEntityDisabled(e)
1716
+ );
1717
+ const newIdMap = selectedEntities.reduce((acc, entity) => {
1718
+ acc[getIdOrCodeOrIndex(entity)] = { entity };
1719
+ return acc;
1720
+ }, {});
1721
+ setExpandedEntityIdMap(newIdMap);
1722
+ finalizeSelection({
1723
+ idMap: newIdMap,
1724
+ entities,
1725
+ props: {
1726
+ onDeselect,
1727
+ onSingleRowSelect,
1728
+ onMultiRowSelect,
1729
+ noDeselectAll,
1730
+ onRowSelect,
1731
+ noSelect,
1732
+ change
1733
+ }
1734
+ });
1735
+ // This option could be eliminated, keeping it because it was prior in the
1736
+ // code, but it is a fuctionality not needed
1737
+ if (scrollToFirst) {
1738
+ const idToScrollTo = idArray[0];
1739
+ if (!idToScrollTo && idToScrollTo !== 0) return;
1740
+ const entityIndexToScrollTo = entities.findIndex(
1741
+ e => e.id === idToScrollTo || e.code === idToScrollTo
1742
+ );
1743
+ if (entityIndexToScrollTo === -1 || !tableRef.current) return;
1744
+ const tableBody = tableRef.current.tableRef.querySelector(".rt-tbody");
1745
+ if (!tableBody) return;
1746
+ const rowEl =
1747
+ tableBody.getElementsByClassName("rt-tr-group")[
1748
+ entityIndexToScrollTo
1749
+ ];
1750
+ if (!rowEl) return;
1751
+ setTimeout(() => {
1752
+ //we need to delay for a teeny bit to make sure the table has drawn
1753
+ rowEl &&
1754
+ tableBody &&
1755
+ scrollIntoView(rowEl, tableBody, {
1756
+ alignWithTop: true
1757
+ });
1758
+ }, 0);
1759
+ }
1760
+ },
1761
+ [
1762
+ change,
1763
+ entities,
1764
+ isEntityDisabled,
1765
+ noDeselectAll,
1766
+ noSelect,
1767
+ onDeselect,
1768
+ onMultiRowSelect,
1769
+ onRowSelect,
1770
+ onSingleRowSelect
1771
+ ]
1772
+ );
1773
+
1774
+ // We need to make sure this only runs at the beggining
1775
+ useEffect(() => {
1776
+ if (selectedIds) {
1777
+ setSelectedIds(selectedIds);
1778
+ }
1779
+ if (selectAllByDefault && entities && entities.length) {
1780
+ if (alreadySelected.current) return;
1781
+ setSelectedIds(entities.map(getIdOrCodeOrIndex));
1782
+ alreadySelected.current = true;
1783
+ }
1784
+ }, [entities, selectedIds, selectAllByDefault, setSelectedIds]);
1785
+
1786
+ const TheadComponent = useCallback(
1787
+ ({ className, style, children }) => {
1788
+ const moveColumn = ({ oldIndex, newIndex }) => {
1789
+ let oldStateColumnIndex, newStateColumnIndex;
1790
+ columns.forEach((column, i) => {
1791
+ if (oldIndex === column.columnIndex) oldStateColumnIndex = i;
1792
+ if (newIndex === column.columnIndex) newStateColumnIndex = i;
1793
+ });
1794
+ // because it is all handled in state we need
1795
+ // to perform the move and update the columnIndices
1796
+ // because they are used for the sortable columns
1797
+ const newColumns = arrayMove(
1798
+ columns,
1799
+ oldStateColumnIndex,
1800
+ newStateColumnIndex
1801
+ ).map((column, i) => {
1802
+ return {
1803
+ ...column,
1804
+ columnIndex: i
1805
+ };
1806
+ });
1807
+ setColumns(newColumns);
1808
+ };
1809
+ return (
1810
+ <SortableColumns
1811
+ className={className}
1812
+ style={style}
1813
+ moveColumn={moveColumnPersist || moveColumn}
1814
+ >
1815
+ {children}
1816
+ </SortableColumns>
1817
+ );
1818
+ },
1819
+ [columns, moveColumnPersist]
1820
+ );
1821
+
1822
+ const addEntitiesToSelection = entities => {
1823
+ const idMap = reduxFormSelectedEntityIdMap || {};
1824
+ const newIdMap = cloneDeep(idMap) || {};
1825
+ entities.forEach((entity, i) => {
1826
+ if (isEntityDisabled(entity)) return;
1827
+ const entityId = getIdOrCodeOrIndex(entity, i);
1828
+ newIdMap[entityId] = { entity };
1829
+ });
1830
+ finalizeSelection({
1831
+ idMap: newIdMap,
1832
+ entities,
1833
+ props: {
1834
+ onDeselect,
1835
+ onSingleRowSelect,
1836
+ onMultiRowSelect,
1837
+ noDeselectAll,
1838
+ onRowSelect,
1839
+ noSelect,
1840
+ change
1841
+ }
1842
+ });
1843
+ };
1844
+
1845
+ const refocusTable = useCallback(() => {
1846
+ const table = tableRef.current?.tableRef?.closest(
1847
+ ".data-table-container>div"
1848
+ );
1849
+ table?.focus();
1850
+ }, []);
1851
+
1852
+ const isSelectionARectangle = useCallback(() => {
1853
+ if (selectedCells && Object.keys(selectedCells).length > 1) {
1854
+ const pathToIndex = getFieldPathToIndex(schema);
1855
+ const entityMap = getEntityIdToEntity(entities);
1856
+ let selectionGrid = [];
1857
+ let firstCellIndex;
1858
+ let lastCellIndex;
1859
+ let lastRowIndex;
1860
+ let firstRowIndex;
1861
+ const selectedPaths = [];
1862
+ Object.keys(selectedCells).forEach(key => {
1863
+ // if (reduxFormSelectedCells[key] === PRIMARY_SELECTED_VAL) {
1864
+ // primaryCellId = key;
1865
+ // }
1866
+ const [rowId, cellPath] = key.split(":");
1867
+ if (!selectedPaths.includes(cellPath)) selectedPaths.push(cellPath);
1868
+ const cellIndex = pathToIndex[cellPath];
1869
+ const ent = entityMap[rowId];
1870
+ if (!ent) return;
1871
+ const { i } = ent;
1872
+ if (firstRowIndex === undefined || i < firstRowIndex) {
1873
+ firstRowIndex = i;
1874
+ }
1875
+ if (lastRowIndex === undefined || i > lastRowIndex) {
1876
+ lastRowIndex = i;
1877
+ }
1878
+ if (!selectionGrid[i]) selectionGrid[i] = [];
1879
+ selectionGrid[i][cellIndex] = { cellId: key, rowIndex: i, cellIndex };
1880
+ if (firstCellIndex === undefined || cellIndex < firstCellIndex) {
1881
+ firstCellIndex = cellIndex;
1882
+ }
1883
+ if (lastCellIndex === undefined || cellIndex > lastCellIndex) {
1884
+ lastCellIndex = cellIndex;
1885
+ }
1886
+ });
1887
+ selectionGrid = selectionGrid.slice(firstRowIndex);
1888
+ let isRectangle = true;
1889
+ for (let i = 0; i < selectionGrid.length; i++) {
1890
+ const row = selectionGrid[i];
1891
+ if (!row) {
1892
+ isRectangle = false;
1893
+ break;
1894
+ } else {
1895
+ for (let j = firstCellIndex; j < row.length; j++) {
1896
+ if (!row[j]) {
1897
+ isRectangle = false;
1898
+ break;
1899
+ }
1900
+ }
1901
+ }
1902
+ }
1903
+ if (isRectangle) {
1904
+ return {
1905
+ entityMap,
1906
+ firstCellIndex,
1907
+ firstRowIndex,
1908
+ isRect: true,
1909
+ lastCellIndex,
1910
+ lastRowIndex,
1911
+ selectedPaths,
1912
+ selectionGrid,
1913
+ pathToIndex
1914
+ };
1915
+ } else {
1916
+ return {};
1917
+ }
1918
+ }
1919
+ return {};
1920
+ }, [entities, schema, selectedCells]);
1921
+
1922
+ const handleCellClick = useCallback(
1923
+ ({ event, cellId }) => {
1924
+ if (!cellId) return;
1925
+ const [rowId, cellPath] = cellId.split(":");
1926
+ const entityMap = getEntityIdToEntity(entities);
1927
+ const { e: entity, i: rowIndex } = entityMap[rowId];
1928
+ const pathToIndex = getFieldPathToIndex(schema);
1929
+ const columnIndex = pathToIndex[cellPath];
1930
+ const rowDisabled = isEntityDisabled(entity);
1931
+
1932
+ if (rowDisabled) return;
1933
+ let newSelectedCells = {
1934
+ ...selectedCells
1935
+ };
1936
+ if (newSelectedCells[cellId] && !event.shiftKey) {
1937
+ // don't deselect if editing
1938
+ if (reduxFormEditingCell === cellId) return;
1939
+ if (event.metaKey) {
1940
+ delete newSelectedCells[cellId];
1941
+ } else {
1942
+ newSelectedCells = {};
1943
+ newSelectedCells[cellId] = PRIMARY_SELECTED_VAL;
1944
+ }
1945
+ } else {
1946
+ if (event.metaKey) {
1947
+ if (isEmpty(newSelectedCells)) {
1948
+ newSelectedCells[cellId] = PRIMARY_SELECTED_VAL;
1949
+ } else {
1950
+ newSelectedCells[cellId] = PRIMARY_SELECTED_VAL;
1951
+ if (primarySelectedCellId)
1952
+ newSelectedCells[primarySelectedCellId] = true;
1953
+ }
1954
+ } else if (event.shiftKey) {
1955
+ if (primarySelectedCellId) {
1956
+ const [rowId, colPath] = primarySelectedCellId.split(":");
1957
+ const primaryRowIndex = entities.findIndex((e, i) => {
1958
+ return getIdOrCodeOrIndex(e, i) === rowId;
1959
+ });
1960
+ const fieldToIndex = getFieldPathToIndex(schema);
1961
+ const primaryColIndex = fieldToIndex[colPath];
1962
+
1963
+ if (primaryRowIndex === -1 || primaryColIndex === -1) {
1964
+ newSelectedCells = {};
1965
+ newSelectedCells[cellId] = PRIMARY_SELECTED_VAL;
1966
+ } else {
1967
+ const minRowIndex = min([primaryRowIndex, rowIndex]);
1968
+ const minColIndex = min([primaryColIndex, columnIndex]);
1969
+ const maxRowIndex = max([primaryRowIndex, rowIndex]);
1970
+ const maxColIndex = max([primaryColIndex, columnIndex]);
1971
+ const entitiesBetweenRows = entities.slice(
1972
+ minRowIndex,
1973
+ maxRowIndex + 1
1974
+ );
1975
+ const fieldsBetweenCols = schema.fields.slice(
1976
+ minColIndex,
1977
+ maxColIndex + 1
1978
+ );
1979
+ newSelectedCells = {
1980
+ [primarySelectedCellId]: PRIMARY_SELECTED_VAL
1981
+ };
1982
+ entitiesBetweenRows.forEach(e => {
1983
+ const rowId = getIdOrCodeOrIndex(e, entities.indexOf(e));
1984
+ fieldsBetweenCols.forEach(f => {
1985
+ const cellId = `${rowId}:${f.path}`;
1986
+ if (!newSelectedCells[cellId])
1987
+ newSelectedCells[cellId] = true;
1988
+ });
1989
+ });
1990
+ // newSelectedCells[cellId] = PRIMARY_SELECTED_VAL;
1991
+ // newSelectedCells[primarySelectedCellId] = true;
1992
+ }
1993
+ } else {
1994
+ newSelectedCells = {};
1995
+ newSelectedCells[cellId] = PRIMARY_SELECTED_VAL;
1996
+ }
1997
+ } else {
1998
+ newSelectedCells = {};
1999
+ newSelectedCells[cellId] = PRIMARY_SELECTED_VAL;
2000
+ }
2001
+ }
2002
+
2003
+ setSelectedCells(newSelectedCells);
2004
+ },
2005
+ [
2006
+ entities,
2007
+ isEntityDisabled,
2008
+ primarySelectedCellId,
2009
+ reduxFormEditingCell,
2010
+ schema,
2011
+ selectedCells
2012
+ ]
2013
+ );
2014
+
2015
+ const insertRows = useCallback(
2016
+ ({ above, numRows = 1, appendToBottom } = {}) => {
2017
+ const [rowId] = primarySelectedCellId?.split(":") || [];
2018
+ updateEntitiesHelper(entities, entities => {
2019
+ const newEntities = times(numRows).map(() => ({ id: nanoid() }));
2020
+
2021
+ const indexToInsert = entities.findIndex((e, i) => {
2022
+ return getIdOrCodeOrIndex(e, i) === rowId;
2023
+ });
2024
+ const insertIndex = above ? indexToInsert : indexToInsert + 1;
2025
+ const insertIndexToUse = appendToBottom ? entities.length : insertIndex;
2026
+ let { newEnts, validationErrors } = formatAndValidateEntities(
2027
+ newEntities,
2028
+ {
2029
+ useDefaultValues: true,
2030
+ indexToStartAt: insertIndexToUse
2031
+ }
2032
+ );
2033
+
2034
+ newEnts = newEnts.map(e => ({
2035
+ ...e,
2036
+ _isClean: true
2037
+ }));
2038
+ updateValidation(entities, {
2039
+ ...reduxFormCellValidation,
2040
+ ...validationErrors
2041
+ });
2042
+
2043
+ entities.splice(insertIndexToUse, 0, ...newEnts);
2044
+ });
2045
+ refocusTable();
2046
+ },
2047
+ [
2048
+ entities,
2049
+ formatAndValidateEntities,
2050
+ primarySelectedCellId,
2051
+ reduxFormCellValidation,
2052
+ refocusTable,
2053
+ updateEntitiesHelper,
2054
+ updateValidation
2055
+ ]
2056
+ );
2057
+
2058
+ const showContextMenu = useCallback(
2059
+ (e, { idMap, selectedCells } = {}) => {
2060
+ let selectedRecords;
2061
+ if (isCellEditable) {
2062
+ const rowIds = {};
2063
+ Object.keys(selectedCells).forEach(cellKey => {
2064
+ const [rowId] = cellKey.split(":");
2065
+ rowIds[rowId] = true;
2066
+ });
2067
+ selectedRecords = entities.filter(
2068
+ ent => rowIds[getIdOrCodeOrIndex(ent)]
2069
+ );
2070
+ } else {
2071
+ selectedRecords = getRecordsFromIdMap(idMap);
2072
+ }
2073
+
2074
+ const itemsToRender = contextMenu({
2075
+ selectedRecords,
2076
+ history
2077
+ });
2078
+ if (!itemsToRender && !isCopyable) return null;
2079
+ const copyMenuItems = [];
2080
+
2081
+ e.persist();
2082
+ if (isCopyable) {
2083
+ //compute the cellWrapper here so we don't lose access to it
2084
+ const cellWrapper =
2085
+ e.target.querySelector(".tg-cell-wrapper") ||
2086
+ e.target.closest(".tg-cell-wrapper");
2087
+ if (cellWrapper) {
2088
+ copyMenuItems.push(
2089
+ <MenuItem
2090
+ key="copyCell"
2091
+ onClick={() => {
2092
+ //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
2093
+ //do we need to be able to copy hidden cells? It seems like it should just copy what's on the page..?
2094
+ const specificColumn = cellWrapper.getAttribute("data-test");
2095
+ handleCopyRows([cellWrapper.closest(".rt-tr")], {
2096
+ specificColumn,
2097
+ onFinishMsg: "Cell copied"
2098
+ });
2099
+ const [text, jsonText] = getCellCopyText(cellWrapper);
2100
+ handleCopyHelper(text, jsonText);
2101
+ }}
2102
+ text="Cell"
2103
+ />
2104
+ );
2105
+
2106
+ copyMenuItems.push(
2107
+ <MenuItem
2108
+ key="copyColumn"
2109
+ onClick={() => {
2110
+ handleCopyColumn(e, cellWrapper);
2111
+ }}
2112
+ text="Column"
2113
+ />
2114
+ );
2115
+ if (selectedRecords.length > 1) {
2116
+ copyMenuItems.push(
2117
+ <MenuItem
2118
+ key="copyColumnSelected"
2119
+ onClick={() => {
2120
+ handleCopyColumn(e, cellWrapper, selectedRecords);
2121
+ }}
2122
+ text="Column (Selected)"
2123
+ />
2124
+ );
2125
+ }
2126
+ }
2127
+ if (selectedRecords.length === 0 || selectedRecords.length === 1) {
2128
+ //compute the row here so we don't lose access to it
2129
+ const cell =
2130
+ e.target.querySelector(".tg-cell-wrapper") ||
2131
+ e.target.closest(".tg-cell-wrapper") ||
2132
+ e.target.closest(".rt-td");
2133
+ const row = cell.closest(".rt-tr");
2134
+ copyMenuItems.push(
2135
+ <MenuItem
2136
+ key="copySelectedRows"
2137
+ onClick={() => {
2138
+ handleCopyRows([row]);
2139
+ // loop through each cell in the row
2140
+ }}
2141
+ text="Row"
2142
+ />
2143
+ );
2144
+ } else if (selectedRecords.length > 1) {
2145
+ copyMenuItems.push(
2146
+ <MenuItem
2147
+ key="copySelectedRows"
2148
+ onClick={() => {
2149
+ handleCopySelectedRows(selectedRecords, e);
2150
+ // loop through each cell in the row
2151
+ }}
2152
+ text="Rows"
2153
+ />
2154
+ );
2155
+ }
2156
+ copyMenuItems.push(
2157
+ <MenuItem
2158
+ key="copyFullTableRows"
2159
+ onClick={() => {
2160
+ handleCopyTable(e);
2161
+ // loop through each cell in the row
2162
+ }}
2163
+ text="Table"
2164
+ />
2165
+ );
2166
+ }
2167
+ const selectedRowIds = Object.keys(selectedCells).map(cellId => {
2168
+ const [rowId] = cellId.split(":");
2169
+ return rowId;
2170
+ });
2171
+
2172
+ const menu = (
2173
+ <Menu>
2174
+ {itemsToRender}
2175
+ {copyMenuItems.length && (
2176
+ <MenuItem icon="clipboard" key="copyOpts" text="Copy">
2177
+ {copyMenuItems}
2178
+ </MenuItem>
2179
+ )}
2180
+ {isCellEditable && (
2181
+ <>
2182
+ <MenuItem
2183
+ icon="add-row-top"
2184
+ text="Add Row Above"
2185
+ key="addRowAbove"
2186
+ onClick={() => {
2187
+ insertRows({ above: true });
2188
+ }}
2189
+ />
2190
+ <MenuItem
2191
+ icon="add-row-top"
2192
+ text="Add Row Below"
2193
+ key="addRowBelow"
2194
+ onClick={() => {
2195
+ insertRows({});
2196
+ }}
2197
+ />
2198
+ <MenuItem
2199
+ icon="remove"
2200
+ text={`Remove Row${selectedRowIds.length > 1 ? "s" : ""}`}
2201
+ key="removeRow"
2202
+ onClick={() => {
2203
+ const selectedRowIds = Object.keys(selectedCells).map(
2204
+ cellId => {
2205
+ const [rowId] = cellId.split(":");
2206
+ return rowId;
2207
+ }
2208
+ );
2209
+ updateEntitiesHelper(entities, entities => {
2210
+ const ents = entities.filter(
2211
+ (e, i) =>
2212
+ !selectedRowIds.includes(getIdOrCodeOrIndex(e, i))
2213
+ );
2214
+ updateValidation(
2215
+ ents,
2216
+ omitBy(reduxFormCellValidation, (v, cellId) =>
2217
+ selectedRowIds.includes(cellId.split(":")[0])
2218
+ )
2219
+ );
2220
+ return ents;
2221
+ });
2222
+ refocusTable();
2223
+ }}
2224
+ />
2225
+ </>
2226
+ )}
2227
+ </Menu>
2228
+ );
2229
+ ContextMenu.show(menu, { left: e.clientX, top: e.clientY });
2230
+ },
2231
+ [
2232
+ contextMenu,
2233
+ entities,
2234
+ handleCopySelectedRows,
2235
+ history,
2236
+ insertRows,
2237
+ isCellEditable,
2238
+ isCopyable,
2239
+ reduxFormCellValidation,
2240
+ refocusTable,
2241
+ updateEntitiesHelper,
2242
+ updateValidation
2243
+ ]
2244
+ );
2245
+
2246
+ const getTableRowProps = useCallback(
2247
+ (state, rowInfo) => {
2248
+ if (!rowInfo) {
2249
+ return {
2250
+ className: "no-row-data"
2251
+ };
2252
+ }
2253
+ const entity = rowInfo.original;
2254
+ const rowId = getIdOrCodeOrIndex(entity, rowInfo.index);
2255
+ const rowSelected = reduxFormSelectedEntityIdMap[rowId];
2256
+ const isExpanded = expandedEntityIdMap[rowId];
2257
+ const rowDisabled = isEntityDisabled(entity);
2258
+ const dataId = entity.id || entity.code;
2259
+ return {
2260
+ onClick: e => {
2261
+ if (isCellEditable) return;
2262
+ // if checkboxes are activated or row expander is clicked don't select row
2263
+ if (e.target.matches(".tg-expander, .tg-expander *")) {
2264
+ setExpandedEntityIdMap(prev => ({ ...prev, [rowId]: !isExpanded }));
2265
+ return;
2266
+ } else if (
2267
+ e.target.closest(".tg-react-table-checkbox-cell-container")
2268
+ ) {
2269
+ return;
2270
+ } else if (mustClickCheckboxToSelect) {
2271
+ return;
2272
+ }
2273
+ if (e.detail > 1) {
2274
+ return; //cancel multiple quick clicks
2275
+ }
2276
+ rowClick(e, rowInfo, entities, {
2277
+ reduxFormSelectedEntityIdMap,
2278
+ isSingleSelect,
2279
+ noSelect,
2280
+ onRowClick,
2281
+ isEntityDisabled,
2282
+ withCheckboxes,
2283
+ onDeselect,
2284
+ onSingleRowSelect,
2285
+ onMultiRowSelect,
2286
+ noDeselectAll,
2287
+ onRowSelect,
2288
+ change
2289
+ });
2290
+ },
2291
+ //row right click
2292
+ onContextMenu: e => {
2293
+ e.preventDefault();
2294
+ if (rowId === undefined || rowDisabled || isCellEditable) return;
2295
+ const oldIdMap = cloneDeep(reduxFormSelectedEntityIdMap) || {};
2296
+ let newIdMap;
2297
+ if (withCheckboxes) {
2298
+ newIdMap = oldIdMap;
2299
+ } else {
2300
+ // if we are not using checkboxes we need to make sure
2301
+ // that the id of the record gets added to the id map
2302
+ newIdMap = oldIdMap[rowId] ? oldIdMap : { [rowId]: { entity } };
2303
+
2304
+ // tgreen: this will refresh the selection with fresh data. The entities in redux might not be up to date
2305
+ const keyedEntities = keyBy(entities, getIdOrCodeOrIndex);
2306
+ forEach(newIdMap, (val, key) => {
2307
+ const freshEntity = keyedEntities[key];
2308
+ if (freshEntity) {
2309
+ newIdMap[key] = { ...newIdMap[key], entity: freshEntity };
2310
+ }
2311
+ });
2312
+ finalizeSelection({
2313
+ idMap: newIdMap,
2314
+ entities,
2315
+ props: {
2316
+ onDeselect,
2317
+ onSingleRowSelect,
2318
+ onMultiRowSelect,
2319
+ noDeselectAll,
2320
+ onRowSelect,
2321
+ noSelect,
2322
+ change
2323
+ }
2324
+ });
2325
+ }
2326
+ showContextMenu(e, { idMap: newIdMap, selectedCells });
2327
+ },
2328
+ className: classNames(
2329
+ "with-row-data",
2330
+ getRowClassName && getRowClassName(rowInfo, state),
2331
+ {
2332
+ disabled: rowDisabled,
2333
+ selected: rowSelected && !withCheckboxes,
2334
+ "rt-tr-last-row": rowInfo.index === entities.length - 1
2335
+ }
2336
+ ),
2337
+ "data-test-id": dataId === undefined ? rowInfo.index : dataId,
2338
+ "data-index": rowInfo.index,
2339
+ "data-tip": typeof rowDisabled === "string" ? rowDisabled : undefined,
2340
+ onDoubleClick: e => {
2341
+ if (rowDisabled) return;
2342
+ onDoubleClick &&
2343
+ onDoubleClick(rowInfo.original, rowInfo.index, history, e);
2344
+ }
2345
+ };
2346
+ },
2347
+ [
2348
+ change,
2349
+ entities,
2350
+ expandedEntityIdMap,
2351
+ getRowClassName,
2352
+ history,
2353
+ isCellEditable,
2354
+ isEntityDisabled,
2355
+ isSingleSelect,
2356
+ mustClickCheckboxToSelect,
2357
+ noDeselectAll,
2358
+ noSelect,
2359
+ onDeselect,
2360
+ onDoubleClick,
2361
+ onMultiRowSelect,
2362
+ onRowClick,
2363
+ onRowSelect,
2364
+ onSingleRowSelect,
2365
+ reduxFormSelectedEntityIdMap,
2366
+ selectedCells,
2367
+ showContextMenu,
2368
+ withCheckboxes
2369
+ ]
2370
+ );
2371
+
2372
+ const getTableCellProps = useCallback(
2373
+ (state, rowInfo, column) => {
2374
+ if (!isCellEditable) return {}; //only allow cell selection to do stuff here
2375
+ if (!rowInfo) return {};
2376
+ if (!reduxFormCellValidation) return {};
2377
+ const entity = rowInfo.original;
2378
+ const rowIndex = rowInfo.index;
2379
+ const rowId = getIdOrCodeOrIndex(entity, rowIndex);
2380
+ const {
2381
+ cellId,
2382
+ cellIdAbove,
2383
+ cellIdToRight,
2384
+ cellIdBelow,
2385
+ cellIdToLeft,
2386
+ rowDisabled,
2387
+ columnIndex
2388
+ } = getCellInfo({
2389
+ columnIndex: column.index,
2390
+ columnPath: column.path,
2391
+ rowId,
2392
+ schema,
2393
+ entities,
2394
+ rowIndex,
2395
+ isEntityDisabled,
2396
+ entity
2397
+ });
2398
+
2399
+ const _isClean =
2400
+ (entity._isClean && doNotValidateUntouchedRows) ||
2401
+ isEntityClean(entity);
2402
+
2403
+ const err = !_isClean && reduxFormCellValidation[cellId];
2404
+ let selectedTopBorder,
2405
+ selectedRightBorder,
2406
+ selectedBottomBorder,
2407
+ selectedLeftBorder;
2408
+ if (selectedCells[cellId]) {
2409
+ selectedTopBorder = !selectedCells[cellIdAbove];
2410
+ selectedRightBorder = !selectedCells[cellIdToRight];
2411
+ selectedBottomBorder = !selectedCells[cellIdBelow];
2412
+ selectedLeftBorder = !selectedCells[cellIdToLeft];
2413
+ }
2414
+ const isPrimarySelected = selectedCells[cellId] === PRIMARY_SELECTED_VAL;
2415
+ const className = classNames({
2416
+ isSelectedCell: selectedCells[cellId],
2417
+ isPrimarySelected,
2418
+ isSecondarySelected: selectedCells[cellId] === true,
2419
+ noSelectedTopBorder: !selectedTopBorder,
2420
+ isCleanRow: _isClean,
2421
+ noSelectedRightBorder: !selectedRightBorder,
2422
+ noSelectedBottomBorder: !selectedBottomBorder,
2423
+ noSelectedLeftBorder: !selectedLeftBorder,
2424
+ isDropdownCell: column.type === "dropdown",
2425
+ isEditingCell: reduxFormEditingCell === cellId,
2426
+ hasCellError: !!err,
2427
+ "no-data-tip": selectedCells[cellId]
2428
+ });
2429
+ return {
2430
+ onDoubleClick: () => {
2431
+ // cell double click
2432
+ if (rowDisabled) return;
2433
+ startCellEdit(cellId);
2434
+ },
2435
+ ...(err && {
2436
+ "data-tip": err?.message || err,
2437
+ "data-no-child-data-tip": true
2438
+ }),
2439
+ onContextMenu: e => {
2440
+ const newSelectedCells = { ...selectedCells };
2441
+ if (!isPrimarySelected) {
2442
+ if (primarySelectedCellId) {
2443
+ newSelectedCells[primarySelectedCellId] = true;
2444
+ }
2445
+ newSelectedCells[cellId] = PRIMARY_SELECTED_VAL;
2446
+ setSelectedCells(newSelectedCells);
2447
+ }
2448
+ showContextMenu(e, { selectedCells: newSelectedCells });
2449
+ },
2450
+ onClick: event => {
2451
+ handleCellClick({
2452
+ event,
2453
+ cellId,
2454
+ rowDisabled,
2455
+ rowIndex,
2456
+ columnIndex
2457
+ });
2458
+ },
2459
+ className
2460
+ };
2461
+ },
2462
+ [
2463
+ doNotValidateUntouchedRows,
2464
+ entities,
2465
+ handleCellClick,
2466
+ isCellEditable,
2467
+ isEntityDisabled,
2468
+ primarySelectedCellId,
2469
+ reduxFormCellValidation,
2470
+ reduxFormEditingCell,
2471
+ schema,
2472
+ selectedCells,
2473
+ showContextMenu,
2474
+ startCellEdit
2475
+ ]
2476
+ );
2477
+
2478
+ if (withSelectAll && !safeQuery) {
2479
+ throw new Error("safeQuery is needed for selecting all table records");
2480
+ }
2481
+
2482
+ let compactClassName = "";
2483
+ if (compactPaging) {
2484
+ compactClassName += " tg-compact-paging";
2485
+ }
2486
+ compactClassName += extraCompact
2487
+ ? " tg-extra-compact-table"
2488
+ : compact
2489
+ ? " tg-compact-table"
2490
+ : "";
2491
+
2492
+ const hasFilters =
2493
+ filters.length ||
2494
+ searchTerm ||
2495
+ schema.fields.some(
2496
+ field => field.filterIsActive && field.filterIsActive(currentParams)
2497
+ );
2498
+
2499
+ const additionalFilterKeys = schema.fields.reduce((acc, field) => {
2500
+ if (field.filterKey) acc.push(field.filterKey);
2501
+ return acc;
2502
+ }, []);
2503
+
2504
+ const filtersOnNonDisplayedFields = useMemo(() => {
2505
+ const _filtersOnNonDisplayedFields = [];
2506
+ if (filters && filters.length) {
2507
+ schema.fields.forEach(field => {
2508
+ const ccDisplayName = getCCDisplayName(field);
2509
+ if (field.isHidden) {
2510
+ filters.forEach(filter => {
2511
+ if (filter.filterOn === ccDisplayName) {
2512
+ _filtersOnNonDisplayedFields.push({
2513
+ ...filter,
2514
+ displayName: field.displayName
2515
+ });
2516
+ }
2517
+ });
2518
+ }
2519
+ });
2520
+ }
2521
+ return _filtersOnNonDisplayedFields;
2522
+ }, [filters, schema.fields]);
2523
+
2524
+ const numRows = isInfinite ? entities.length : pageSize;
2525
+ const idMap = useMemo(
2526
+ () => reduxFormSelectedEntityIdMap || {},
2527
+ [reduxFormSelectedEntityIdMap]
2528
+ );
2529
+ const selectedRowCount = Object.keys(idMap).filter(key => idMap[key]).length;
2530
+
2531
+ let rowsToShow = doNotShowEmptyRows
2532
+ ? Math.min(numRows, entities.length)
2533
+ : numRows;
2534
+ // if there are no entities then provide enough space to show
2535
+ // no rows found message
2536
+ if (entities.length === 0 && rowsToShow < 3) rowsToShow = 3;
2537
+ const expandedRows = entities.reduce((acc, row, index) => {
2538
+ const rowId = getIdOrCodeOrIndex(row, index);
2539
+ acc[index] = expandedEntityIdMap[rowId];
2540
+ return acc;
2541
+ }, {});
2542
+
2543
+ const showHeader = (withTitle || withSearch || children) && !noHeader;
2544
+ const toggleFullscreenButton = (
2545
+ <Button
2546
+ icon="fullscreen"
2547
+ active={fullscreen}
2548
+ minimal
2549
+ onClick={() => setFullscreen(prev => !prev)}
2550
+ />
2551
+ );
2552
+
2553
+ const { showSelectAll, showClearAll } = useMemo(() => {
2554
+ let _showSelectAll = false;
2555
+ let _showClearAll = false;
2556
+ // we want to show select all if every row on the current page is selected
2557
+ // and not every row across all pages are already selected.
2558
+ if (!isInfinite) {
2559
+ const canShowSelectAll =
2560
+ withSelectAll ||
2561
+ (entitiesAcrossPages && numRows < entitiesAcrossPages.length);
2562
+ if (canShowSelectAll) {
2563
+ // could all be disabled
2564
+ let atLeastOneRowOnCurrentPageSelected = false;
2565
+ const allRowsOnCurrentPageSelected = entities.every(e => {
2566
+ const rowId = getIdOrCodeOrIndex(e);
2567
+ const selected = idMap[rowId] || isEntityDisabled(e);
2568
+ if (selected) atLeastOneRowOnCurrentPageSelected = true;
2569
+ return selected;
2570
+ });
2571
+ if (
2572
+ atLeastOneRowOnCurrentPageSelected &&
2573
+ allRowsOnCurrentPageSelected
2574
+ ) {
2575
+ let everyEntitySelected;
2576
+ if (isLocalCall) {
2577
+ everyEntitySelected = entitiesAcrossPages.every(e => {
2578
+ const rowId = getIdOrCodeOrIndex(e);
2579
+ return idMap[rowId] || isEntityDisabled(e);
2580
+ });
2581
+ } else {
2582
+ everyEntitySelected = entityCount <= selectedRowCount;
2583
+ }
2584
+ if (everyEntitySelected) {
2585
+ _showClearAll = selectedRowCount;
2586
+ }
2587
+ // only show if not all selected
2588
+ _showSelectAll = !everyEntitySelected;
2589
+ }
2590
+ }
2591
+ }
2592
+ return { showSelectAll: _showSelectAll, showClearAll: _showClearAll };
2593
+ }, [
2594
+ entities,
2595
+ entitiesAcrossPages,
2596
+ entityCount,
2597
+ idMap,
2598
+ isEntityDisabled,
2599
+ isInfinite,
2600
+ isLocalCall,
2601
+ numRows,
2602
+ selectedRowCount,
2603
+ withSelectAll
2604
+ ]);
2605
+
2606
+ const showNumSelected = !noSelect && !isSingleSelect && !hideSelectedCount;
2607
+ const selectedAndTotalMessage = useMemo(() => {
2608
+ let _selectedAndTotalMessage = "";
2609
+ if (showNumSelected) {
2610
+ _selectedAndTotalMessage += `${selectedRowCount} Selected `;
2611
+ }
2612
+ if (showCount && showNumSelected) {
2613
+ _selectedAndTotalMessage += `/ `;
2614
+ }
2615
+ if (showCount) {
2616
+ _selectedAndTotalMessage += `${entityCount || 0} Total`;
2617
+ }
2618
+ if (_selectedAndTotalMessage) {
2619
+ _selectedAndTotalMessage = <div>{_selectedAndTotalMessage}</div>;
2620
+ }
2621
+ return _selectedAndTotalMessage;
2622
+ }, [entityCount, selectedRowCount, showCount, showNumSelected]);
2623
+
2624
+ const shouldShowPaging =
2625
+ !isInfinite &&
2626
+ withPaging &&
2627
+ (hidePageSizeWhenPossible ? entityCount > pageSize : true);
2628
+
2629
+ const SubComponentToUse = useMemo(() => {
2630
+ if (SubComponent) {
2631
+ return row => {
2632
+ let shouldShow = true;
2633
+ if (shouldShowSubComponent) {
2634
+ shouldShow = shouldShowSubComponent(row.original);
2635
+ }
2636
+ if (shouldShow) {
2637
+ return SubComponent(row);
2638
+ }
2639
+ };
2640
+ }
2641
+ return;
2642
+ }, [SubComponent, shouldShowSubComponent]);
2643
+
2644
+ const nonDisplayedFilterComp = useMemo(() => {
2645
+ if (filtersOnNonDisplayedFields.length) {
2646
+ const content = filtersOnNonDisplayedFields.map(
2647
+ ({ displayName, path, selectedFilter, filterValue }) => {
2648
+ let filterValToDisplay = filterValue;
2649
+ if (selectedFilter === "inList") {
2650
+ filterValToDisplay = Array.isArray(filterValToDisplay)
2651
+ ? filterValToDisplay
2652
+ : filterValToDisplay && filterValToDisplay.split(";");
2653
+ }
2654
+ if (Array.isArray(filterValToDisplay)) {
2655
+ filterValToDisplay = filterValToDisplay.join(", ");
2656
+ }
2657
+ return (
2658
+ <div
2659
+ key={displayName || startCase(camelCase(path))}
2660
+ className="tg-filter-on-non-displayed-field"
2661
+ >
2662
+ {displayName || startCase(camelCase(path))}{" "}
2663
+ {lowerCase(selectedFilter)} {filterValToDisplay}
2664
+ </div>
2665
+ );
2666
+ }
2667
+ );
2668
+ return (
2669
+ <div style={{ marginRight: 5, marginLeft: "auto" }}>
2670
+ <Tooltip
2671
+ content={
2672
+ <div>
2673
+ Active filters on hidden columns:
2674
+ <br />
2675
+ <br />
2676
+ {content}
2677
+ </div>
2678
+ }
2679
+ >
2680
+ <Icon icon="filter-list" />
2681
+ </Tooltip>
2682
+ </div>
2683
+ );
2684
+ }
2685
+ return null;
2686
+ }, [filtersOnNonDisplayedFields]);
2687
+
2688
+ const filteredEnts = useMemo(() => {
2689
+ if (onlyShowRowsWErrors) {
2690
+ const rowToErrorMap = {};
2691
+ forEach(reduxFormCellValidation, (err, cellId) => {
2692
+ if (err) {
2693
+ const [rowId] = cellId.split(":");
2694
+ rowToErrorMap[rowId] = true;
2695
+ }
2696
+ });
2697
+ return entities.filter(e => {
2698
+ return rowToErrorMap[e.id];
2699
+ });
2700
+ }
2701
+ return entities;
2702
+ }, [entities, onlyShowRowsWErrors, reduxFormCellValidation]);
2703
+
2704
+ const renderColumns = useColumns({
2705
+ addFilters,
2706
+ cellRenderer,
2707
+ columns,
2708
+ currentParams,
2709
+ compact,
2710
+ editingCellSelectAll,
2711
+ entities,
2712
+ expandedEntityIdMap,
2713
+ extraCompact,
2714
+ filters,
2715
+ formName,
2716
+ getCellHoverText,
2717
+ isCellEditable,
2718
+ isEntityDisabled,
2719
+ isLocalCall,
2720
+ isSimple,
2721
+ isSingleSelect,
2722
+ isSelectionARectangle,
2723
+ noDeselectAll,
2724
+ noSelect,
2725
+ noUserSelect,
2726
+ onDeselect,
2727
+ onMultiRowSelect,
2728
+ onRowClick,
2729
+ onRowSelect,
2730
+ onSingleRowSelect,
2731
+ order,
2732
+ primarySelectedCellId,
2733
+ reduxFormCellValidation,
2734
+ reduxFormSelectedEntityIdMap,
2735
+ refocusTable,
2736
+ removeSingleFilter,
2737
+ schema,
2738
+ selectedCells,
2739
+ setExpandedEntityIdMap,
2740
+ setNewParams,
2741
+ setOrder,
2742
+ setSelectedCells,
2743
+ shouldShowSubComponent,
2744
+ startCellEdit,
2745
+ SubComponent,
2746
+ tableRef,
2747
+ updateEntitiesHelper,
2748
+ updateValidation,
2749
+ withCheckboxes,
2750
+ withExpandAndCollapseAllButton,
2751
+ withFilter,
2752
+ withSort,
2753
+ recordIdToIsVisibleMap,
2754
+ setRecordIdToIsVisibleMap
2755
+ });
2756
+
2757
+ const scrollToTop = useCallback(
2758
+ () =>
2759
+ tableRef.current?.tableRef?.children?.[0]?.children?.[0]?.scrollIntoView(),
2760
+ []
2761
+ );
2762
+
2763
+ const reactTable = useMemo(
2764
+ () => (
2765
+ <ReactTable
2766
+ data={filteredEnts}
2767
+ ref={tableRef}
2768
+ className={classNames({
2769
+ isCellEditable,
2770
+ "tg-table-loading": isLoading,
2771
+ "tg-table-disabled": disabled
2772
+ })}
2773
+ itemSizeEstimator={
2774
+ extraCompact
2775
+ ? itemSizeEstimators.compact
2776
+ : compact
2777
+ ? itemSizeEstimators.normal
2778
+ : itemSizeEstimators.comfortable
2779
+ }
2780
+ TfootComponent={() => {
2781
+ return <button>hasdfasdf</button>;
2782
+ }}
2783
+ // We should try to not give all the props to the render column
2784
+ columns={renderColumns}
2785
+ pageSize={rowsToShow}
2786
+ expanded={expandedRows}
2787
+ showPagination={false}
2788
+ sortable={false}
2789
+ loading={isLoading || disabled}
2790
+ defaultResized={resized}
2791
+ onResizedChange={(newResized = []) => {
2792
+ const resizedToUse = newResized.map(column => {
2793
+ // have a min width of 50 so that columns don't disappear
2794
+ if (column.value < 50) {
2795
+ return {
2796
+ ...column,
2797
+ value: 50
2798
+ };
2799
+ } else {
2800
+ return column;
2801
+ }
2802
+ });
2803
+ resizePersist(resizedToUse);
2804
+ }}
2805
+ TheadComponent={TheadComponent}
2806
+ ThComponent={ThComponent}
2807
+ getTrGroupProps={getTableRowProps}
2808
+ getTdProps={getTableCellProps}
2809
+ NoDataComponent={({ children }) =>
2810
+ isLoading ? null : (
2811
+ <div className="rt-noData">{noRowsFoundMessage || children}</div>
2812
+ )
2813
+ }
2814
+ LoadingComponent={({ loadingText, loading }) => (
2815
+ <DisabledLoadingComponent
2816
+ loading={loading}
2817
+ loadingText={loadingText}
2818
+ disabled={disabled}
2819
+ />
2820
+ )}
2821
+ style={{
2822
+ maxHeight,
2823
+ minHeight: 150,
2824
+ ...style
2825
+ }}
2826
+ SubComponent={SubComponentToUse}
2827
+ {...ReactTableProps}
2828
+ />
2829
+ ),
2830
+ [
2831
+ ReactTableProps,
2832
+ SubComponentToUse,
2833
+ TheadComponent,
2834
+ compact,
2835
+ disabled,
2836
+ expandedRows,
2837
+ extraCompact,
2838
+ filteredEnts,
2839
+ getTableCellProps,
2840
+ getTableRowProps,
2841
+ isCellEditable,
2842
+ isLoading,
2843
+ maxHeight,
2844
+ noRowsFoundMessage,
2845
+ renderColumns,
2846
+ resizePersist,
2847
+ resized,
2848
+ rowsToShow,
2849
+ style
2850
+ ]
2851
+ );
2852
+
2853
+ return (
2854
+ <div tabIndex="1" onKeyDown={handleKeyDown} onKeyUp={handleKeyUp}>
2855
+ <div
2856
+ className={classNames(
2857
+ "data-table-container",
2858
+ extraClasses,
2859
+ className,
2860
+ compactClassName,
2861
+ {
2862
+ fullscreen,
2863
+ 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
2864
+ "dt-isViewable": isViewable,
2865
+ "dt-minimalStyle": minimalStyle,
2866
+ "no-padding": noPadding,
2867
+ "hide-column-header": hideColumnHeader
2868
+ }
2869
+ )}
2870
+ >
2871
+ <div
2872
+ className="data-table-container-inner"
2873
+ {...(isCellEditable && {
2874
+ tabIndex: -1,
2875
+ onKeyDown: e => {
2876
+ const isTabKey = e.key === "Tab";
2877
+ const isArrowKey = e.key.startsWith("Arrow");
2878
+ if ((isArrowKey && e.target?.tagName !== "INPUT") || isTabKey) {
2879
+ const left = e.key === "ArrowLeft";
2880
+ const up = e.key === "ArrowUp";
2881
+ const down = e.key === "ArrowDown" || e.key === "Enter";
2882
+ let cellIdToUse = primarySelectedCellId;
2883
+ const pathToIndex = getFieldPathToIndex(schema);
2884
+ const entityMap = getEntityIdToEntity(entities);
2885
+ if (!cellIdToUse) return;
2886
+ const {
2887
+ isRect,
2888
+ firstCellIndex,
2889
+ lastCellIndex,
2890
+ lastRowIndex,
2891
+ firstRowIndex
2892
+ } = isSelectionARectangle();
2893
+
2894
+ if (isRect) {
2895
+ const [rowId, columnPath] = cellIdToUse.split(":");
2896
+
2897
+ const columnIndex = pathToIndex[columnPath];
2898
+ const indexToPath = invert(pathToIndex);
2899
+ // we want to grab the cell opposite to the primary selected cell
2900
+ if (
2901
+ firstCellIndex === columnIndex &&
2902
+ firstRowIndex === entityMap[rowId]?.i
2903
+ ) {
2904
+ cellIdToUse = `${entities[lastRowIndex].id}:${indexToPath[lastCellIndex]}`;
2905
+ } else if (
2906
+ firstCellIndex === columnIndex &&
2907
+ lastRowIndex === entityMap[rowId]?.i
2908
+ ) {
2909
+ cellIdToUse = `${entities[firstRowIndex].id}:${indexToPath[lastCellIndex]}`;
2910
+ } else if (
2911
+ lastCellIndex === columnIndex &&
2912
+ firstRowIndex === entityMap[rowId]?.i
2913
+ ) {
2914
+ cellIdToUse = `${entities[lastRowIndex].id}:${indexToPath[firstCellIndex]}`;
2915
+ } else {
2916
+ cellIdToUse = `${entities[firstRowIndex].id}:${indexToPath[firstCellIndex]}`;
2917
+ }
2918
+ }
2919
+ if (!cellIdToUse) return;
2920
+ const [rowId, columnPath] = cellIdToUse.split(":");
2921
+ const columnIndex = pathToIndex[columnPath];
2922
+
2923
+ const { i: rowIndex } = entityMap[rowId];
2924
+
2925
+ const {
2926
+ cellIdAbove,
2927
+ cellIdToRight,
2928
+ cellIdBelow,
2929
+ cellIdToLeft
2930
+ } = getCellInfo({
2931
+ columnIndex,
2932
+ columnPath,
2933
+ rowId,
2934
+ schema,
2935
+ entities,
2936
+ rowIndex,
2937
+ isEntityDisabled,
2938
+ entity: entityMap[rowId].e
2939
+ });
2940
+ const nextCellId = up
2941
+ ? cellIdAbove
2942
+ : down
2943
+ ? cellIdBelow
2944
+ : left
2945
+ ? cellIdToLeft
2946
+ : cellIdToRight;
2947
+
2948
+ e.stopPropagation();
2949
+ e.preventDefault();
2950
+ if (!nextCellId) return;
2951
+ // this.handleCellBlur();
2952
+ // this.finishCellEdit
2953
+ if (
2954
+ document.activeElement?.parentElement?.classList.contains(
2955
+ "rt-td"
2956
+ )
2957
+ ) {
2958
+ document.activeElement.blur();
2959
+ }
2960
+ handleCellClick({
2961
+ event: e,
2962
+ cellId: nextCellId
2963
+ });
2964
+ }
2965
+ if (e.metaKey || e.ctrlKey || e.altKey) return;
2966
+ if (!primarySelectedCellId) return;
2967
+ const entityIdToEntity = getEntityIdToEntity(entities);
2968
+ const [rowId] = primarySelectedCellId.split(":");
2969
+ if (!rowId) return;
2970
+ const entity = entityIdToEntity[rowId].e;
2971
+ if (!entity) return;
2972
+ const rowDisabled = isEntityDisabled(entity);
2973
+ const isNum = e.code?.startsWith("Digit");
2974
+ const isLetter = e.code?.startsWith("Key");
2975
+ const allowedSpecialChars = [
2976
+ "Minus",
2977
+ "Equal",
2978
+ "Backquote",
2979
+ "BracketLeft",
2980
+ "BracketRight",
2981
+ "Backslash",
2982
+ "IntlBackslash",
2983
+ "Semicolon",
2984
+ "Quote",
2985
+ "Comma",
2986
+ "Period",
2987
+ "Slash",
2988
+ "IntlRo",
2989
+ "IntlYen",
2990
+ "Space"
2991
+ ];
2992
+ const isSpecialChar = allowedSpecialChars.includes(e.code);
2993
+ if (!isNum && !isLetter && !isSpecialChar) {
2994
+ return;
2995
+ }
2996
+ if (rowDisabled) return;
2997
+ e.stopPropagation();
2998
+ startCellEdit(primarySelectedCellId, true);
2999
+ }
3000
+ })}
3001
+ >
3002
+ {isCellEditable && entities.length > 50 && (
3003
+ // test for this!!
3004
+ <SwitchField
3005
+ name="onlyShowRowsWErrors"
3006
+ inlineLabel={true}
3007
+ label="Only Show Rows With Errors"
3008
+ onChange={e => {
3009
+ setOnlyShowRowsWErrors(e.target.value);
3010
+ }}
3011
+ />
3012
+ )}
3013
+ {showHeader && (
3014
+ <div className="data-table-header">
3015
+ <div className="data-table-title-and-buttons">
3016
+ {tableName && withTitle && (
3017
+ <span className="data-table-title">{tableName}</span>
3018
+ )}
3019
+ {children}
3020
+ {topLeftItems}
3021
+ </div>
3022
+ {errorParsingUrlString && (
3023
+ <Callout
3024
+ icon="error"
3025
+ style={{
3026
+ width: "unset"
3027
+ }}
3028
+ intent={Intent.WARNING}
3029
+ >
3030
+ Error parsing URL
3031
+ </Callout>
3032
+ )}
3033
+ {nonDisplayedFilterComp}
3034
+ {withSearch && (
3035
+ <div className="data-table-search-and-clear-filter-container">
3036
+ {leftOfSearchBarItems}
3037
+ {hasFilters ? (
3038
+ <Tooltip content="Clear Filters">
3039
+ <Button
3040
+ minimal
3041
+ intent="danger"
3042
+ icon="filter-remove"
3043
+ disabled={disabled}
3044
+ className="data-table-clear-filters"
3045
+ onClick={() => {
3046
+ clearFilters(additionalFilterKeys);
3047
+ }}
3048
+ />
3049
+ </Tooltip>
3050
+ ) : (
3051
+ ""
3052
+ )}
3053
+ <SearchBar
3054
+ noForm={noForm}
3055
+ searchInput={currentParams.searchTerm}
3056
+ setSearchTerm={setSearchTerm}
3057
+ loading={isLoading}
3058
+ searchMenuButton={searchMenuButton}
3059
+ disabled={disabled}
3060
+ autoFocusSearch={autoFocusSearch}
3061
+ />
3062
+ </div>
3063
+ )}
3064
+ </div>
3065
+ )}
3066
+ {subHeader}
3067
+ {showSelectAll && !isSingleSelect && (
3068
+ <div
3069
+ style={{
3070
+ marginTop: 5,
3071
+ marginBottom: 5,
3072
+ display: "flex",
3073
+ alignItems: "center"
3074
+ }}
3075
+ >
3076
+ All items on this page are selected.{" "}
3077
+ <Button
3078
+ small
3079
+ minimal
3080
+ intent="primary"
3081
+ text={`Select all ${
3082
+ entityCount || entitiesAcrossPages.length
3083
+ } items`}
3084
+ loading={selectingAll}
3085
+ onClick={async () => {
3086
+ if (withSelectAll) {
3087
+ // this will be by querying for everything
3088
+ setSelectingAll(true);
3089
+ try {
3090
+ const allEntities = await safeQuery(fragment, {
3091
+ variables: {
3092
+ filter: variables.filter,
3093
+ sort: variables.sort
3094
+ },
3095
+ canCancel: true
3096
+ });
3097
+ addEntitiesToSelection(allEntities);
3098
+ } catch (error) {
3099
+ console.error(`error:`, error);
3100
+ window.toastr.error("Error selecting all constructs");
3101
+ }
3102
+ setSelectingAll(false);
3103
+ } else {
3104
+ addEntitiesToSelection(entitiesAcrossPages);
3105
+ }
3106
+ }}
3107
+ />
3108
+ </div>
3109
+ )}
3110
+ {showClearAll > 0 && (
3111
+ <div
3112
+ style={{
3113
+ marginTop: 5,
3114
+ marginBottom: 5,
3115
+ display: "flex",
3116
+ alignItems: "center"
3117
+ }}
3118
+ >
3119
+ All {showClearAll} items are selected.
3120
+ <Button
3121
+ small
3122
+ minimal
3123
+ intent="primary"
3124
+ text="Clear Selection"
3125
+ onClick={() => {
3126
+ finalizeSelection({
3127
+ idMap: {},
3128
+ entities,
3129
+ props: {
3130
+ onDeselect,
3131
+ onSingleRowSelect,
3132
+ onMultiRowSelect,
3133
+ noDeselectAll,
3134
+ onRowSelect,
3135
+ noSelect,
3136
+ change
3137
+ }
3138
+ });
3139
+ }}
3140
+ />
3141
+ </div>
3142
+ )}
3143
+ {reactTable}
3144
+ {isCellEditable && (
3145
+ <div style={{ display: "flex" }}>
3146
+ <div
3147
+ style={{
3148
+ width: "100%",
3149
+ display: "flex",
3150
+ justifyContent: "center"
3151
+ }}
3152
+ >
3153
+ {!onlyShowRowsWErrors && (
3154
+ <Button
3155
+ icon="add"
3156
+ onClick={() => {
3157
+ insertRows({ numRows: 10, appendToBottom: true });
3158
+ }}
3159
+ minimal
3160
+ >
3161
+ Add 10 Rows
3162
+ </Button>
3163
+ )}
3164
+ </div>
3165
+ <Button
3166
+ onClick={e => {
3167
+ handleCopyTable(e, { isDownload: true });
3168
+ }}
3169
+ data-tip="Download Table as CSV"
3170
+ minimal
3171
+ icon="download"
3172
+ />
3173
+ </div>
3174
+ )}
3175
+ {!noFooter && (
3176
+ <div
3177
+ className="data-table-footer"
3178
+ style={{
3179
+ justifyContent:
3180
+ !showNumSelected && !showCount ? "flex-end" : "space-between"
3181
+ }}
3182
+ >
3183
+ {selectedAndTotalMessage}
3184
+ <div style={{ display: "flex", flexWrap: "wrap" }}>
3185
+ {additionalFooterButtons}
3186
+ {!noFullscreenButton && toggleFullscreenButton}
3187
+ {withDisplayOptions && (
3188
+ <DisplayOptions
3189
+ compact={compact}
3190
+ extraCompact={extraCompact}
3191
+ disabled={disabled}
3192
+ hideDisplayOptionsIcon={hideDisplayOptionsIcon}
3193
+ resetDefaultVisibility={resetDefaultVisibility}
3194
+ updateColumnVisibility={updateColumnVisibility}
3195
+ updateTableDisplayDensity={updateTableDisplayDensity}
3196
+ showForcedHiddenColumns={showForcedHiddenColumns}
3197
+ setShowForcedHidden={setShowForcedHidden}
3198
+ hasOptionForForcedHidden={
3199
+ withDisplayOptions && (isSimple || isInfinite)
3200
+ }
3201
+ schema={schema}
3202
+ />
3203
+ )}
3204
+ {shouldShowPaging && (
3205
+ <PagingTool
3206
+ controlled_hasNextPage={controlled_hasNextPage}
3207
+ controlled_onRefresh={controlled_onRefresh}
3208
+ controlled_page={controlled_page}
3209
+ controlled_setPage={controlled_setPage}
3210
+ controlled_setPageSize={controlled_setPageSize}
3211
+ controlled_total={controlled_total}
3212
+ disabled={disabled}
3213
+ disableSetPageSize={disableSetPageSize}
3214
+ entities={entities}
3215
+ entityCount={entityCount}
3216
+ hideSetPageSize={hideSetPageSize}
3217
+ hideTotalPages={hideTotalPages}
3218
+ keepSelectionOnPageChange={keepSelectionOnPageChange}
3219
+ onRefresh={onRefresh}
3220
+ page={page}
3221
+ pageSize={pageSize}
3222
+ pagingDisabled={pagingDisabled}
3223
+ persistPageSize={persistPageSize}
3224
+ setPage={setPage}
3225
+ setPageSize={setPageSize}
3226
+ setSelectedEntityIdMap={newIdMap => {
3227
+ change("reduxFormSelectedEntityIdMap", newIdMap);
3228
+ }}
3229
+ scrollToTop={scrollToTop}
3230
+ />
3231
+ )}
3232
+ </div>
3233
+ </div>
3234
+ )}
3235
+ </div>
3236
+ </div>
3237
+ </div>
3238
+ );
3239
+ };
3240
+
3241
+ const WrappedDT = dataTableEnhancer(DataTable);
3242
+ export default WrappedDT;
3243
+ const ConnectedPagingTool = dataTableEnhancer(PagingTool);
3244
+ export { ConnectedPagingTool };