@teselagen/ui 0.9.8 → 0.10.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 (68) hide show
  1. package/DataTable/index.d.ts +3 -3
  2. package/index.cjs.js +21120 -21239
  3. package/index.es.js +21028 -21147
  4. package/package.json +2 -1
  5. package/src/DataTable/PagingTool.js +2 -2
  6. package/src/DataTable/RenderCell.js +8 -4
  7. package/src/DataTable/index.js +1210 -224
  8. package/src/DataTable/utils/useTableEntities.js +3 -2
  9. package/src/DataTable/utils/withTableParams.js +8 -7
  10. package/src/UploadCsvWizard.js +7 -5
  11. package/src/utils/hooks/useDeepEqualMemo.js +2 -2
  12. package/src/utils/isEqualIgnoreFunctions.js +23 -0
  13. package/src/utils/pureNoFunc.js +4 -20
  14. package/utils/isEqualIgnoreFunctions.d.ts +1 -0
  15. package/DataTable/EditabelCell.d.ts +0 -7
  16. package/DataTable/ReactTable.d.ts +0 -78
  17. package/DataTable/defaultProps.d.ts +0 -43
  18. package/DataTable/utils/computePresets.d.ts +0 -1
  19. package/DataTable/utils/types/Entity.d.ts +0 -9
  20. package/DataTable/utils/types/Field.d.ts +0 -4
  21. package/DataTable/utils/types/OrderBy.d.ts +0 -11
  22. package/DataTable/utils/types/Schema.d.ts +0 -4
  23. package/DataTable/utils/useDeepEqualMemo.d.ts +0 -1
  24. package/DataTable/utils/useHotKeysWrapper.d.ts +0 -29
  25. package/DataTable/utils/useTableParams.d.ts +0 -49
  26. package/src/DataTable/Columns.jsx +0 -945
  27. package/src/DataTable/EditabelCell.js +0 -44
  28. package/src/DataTable/EditabelCell.jsx +0 -44
  29. package/src/DataTable/ReactTable.js +0 -738
  30. package/src/DataTable/RenderCell.jsx +0 -191
  31. package/src/DataTable/defaultProps.js +0 -45
  32. package/src/DataTable/utils/computePresets.js +0 -42
  33. package/src/DataTable/utils/convertSchema.ts +0 -79
  34. package/src/DataTable/utils/formatPasteData.ts +0 -34
  35. package/src/DataTable/utils/getAllRows.ts +0 -11
  36. package/src/DataTable/utils/getCellCopyText.ts +0 -7
  37. package/src/DataTable/utils/getCellInfo.ts +0 -46
  38. package/src/DataTable/utils/getFieldPathToField.ts +0 -10
  39. package/src/DataTable/utils/getIdOrCodeOrIndex.ts +0 -14
  40. package/src/DataTable/utils/getLastSelectedEntity.ts +0 -15
  41. package/src/DataTable/utils/getNewEntToSelect.ts +0 -32
  42. package/src/DataTable/utils/initializeHasuraWhereAndFilter.ts +0 -35
  43. package/src/DataTable/utils/isBottomRightCornerOfRectangle.ts +0 -27
  44. package/src/DataTable/utils/isEntityClean.ts +0 -15
  45. package/src/DataTable/utils/primarySelectedValue.ts +0 -1
  46. package/src/DataTable/utils/removeCleanRows.ts +0 -26
  47. package/src/DataTable/utils/selection.ts +0 -11
  48. package/src/DataTable/utils/types/Entity.ts +0 -13
  49. package/src/DataTable/utils/types/Field.ts +0 -4
  50. package/src/DataTable/utils/types/OrderBy.ts +0 -15
  51. package/src/DataTable/utils/types/Schema.ts +0 -5
  52. package/src/DataTable/utils/useDeepEqualMemo.js +0 -10
  53. package/src/DataTable/utils/useHotKeysWrapper.js +0 -395
  54. package/src/DataTable/utils/useTableEntities.ts +0 -60
  55. package/src/DataTable/utils/useTableParams.js +0 -361
  56. package/src/DataTable/utils/utils.ts +0 -39
  57. package/src/Timeline/TimelineEvent.tsx +0 -36
  58. package/src/Timeline/index.tsx +0 -21
  59. package/src/utils/browserUtils.ts +0 -3
  60. package/src/utils/determineBlackOrWhiteTextColor.ts +0 -11
  61. package/src/utils/getTextFromEl.ts +0 -45
  62. package/src/utils/handlerHelpers.ts +0 -32
  63. package/src/utils/hooks/index.ts +0 -1
  64. package/src/utils/hooks/useDeepEqualMemo.ts +0 -10
  65. package/src/utils/hooks/useStableReference.ts +0 -9
  66. package/src/utils/hotkeyUtils.tsx +0 -155
  67. package/src/utils/isBeingCalledExcessively.ts +0 -37
  68. package/style.css +0 -10537
@@ -17,6 +17,7 @@ import {
17
17
  noop,
18
18
  cloneDeep,
19
19
  keyBy,
20
+ omit,
20
21
  forEach,
21
22
  lowerCase,
22
23
  get,
@@ -29,33 +30,57 @@ import {
29
30
  some,
30
31
  identity
31
32
  } from "lodash-es";
32
- import { Button, Icon, Intent, Callout, Tooltip } from "@blueprintjs/core";
33
+ import {
34
+ Button,
35
+ Menu,
36
+ MenuItem,
37
+ ContextMenu,
38
+ Icon,
39
+ Intent,
40
+ Callout,
41
+ Tooltip,
42
+ useHotkeys
43
+ } from "@blueprintjs/core";
33
44
  import { arrayMove } from "@dnd-kit/sortable";
34
45
  import classNames from "classnames";
35
46
  import scrollIntoView from "dom-scroll-into-view";
36
- import { VIRTUALIZE_CUTOFF_LENGTH } from "@teselagen/react-table";
37
- import immer, { produceWithPatches, enablePatches } from "immer";
47
+ import ReactTable, { VIRTUALIZE_CUTOFF_LENGTH } from "@teselagen/react-table";
48
+ import immer, { produceWithPatches, enablePatches, applyPatches } from "immer";
38
49
  import papaparse from "papaparse";
39
50
  import { useDispatch, useSelector } from "react-redux";
51
+ import { ThComponent } from "./ThComponent";
40
52
  import {
41
53
  defaultParsePaste,
42
54
  formatPasteData,
43
55
  getAllRows,
56
+ getCellCopyText,
44
57
  getCellInfo,
45
58
  getEntityIdToEntity,
46
59
  getFieldPathToIndex,
47
60
  getIdOrCodeOrIndex,
61
+ getLastSelectedEntity,
62
+ getNewEntToSelect,
48
63
  getRecordsFromIdMap,
64
+ getRowCopyText,
65
+ handleCopyColumn,
66
+ handleCopyHelper,
49
67
  handleCopyRows,
50
68
  handleCopyTable,
69
+ isEntityClean,
51
70
  PRIMARY_SELECTED_VAL,
52
71
  removeCleanRows
53
72
  } from "./utils";
54
73
  import { useDeepEqualMemo } from "../utils/hooks";
55
- import { changeSelectedEntities, finalizeSelection } from "./utils/rowClick";
74
+ import rowClick, {
75
+ changeSelectedEntities,
76
+ finalizeSelection
77
+ } from "./utils/rowClick";
56
78
  import PagingTool from "./PagingTool";
57
79
  import SearchBar from "./SearchBar";
58
80
  import DisplayOptions from "./DisplayOptions";
81
+ import DisabledLoadingComponent from "./DisabledLoadingComponent";
82
+ import SortableColumns from "./SortableColumns";
83
+ import dataTableEnhancer from "./dataTableEnhancer";
59
84
  import "../toastr";
60
85
  import "@teselagen/react-table/react-table.css";
61
86
  import "./style.css";
@@ -73,16 +98,21 @@ import {
73
98
  getCurrentParamsFromUrl,
74
99
  setCurrentParamsOnUrl
75
100
  } from "./utils/queryParams";
76
- import { formValueSelector, change as _change, reduxForm } from "redux-form";
101
+ import { useColumns } from "./Columns";
102
+ import { formValueSelector, change as _change } from "redux-form";
77
103
  import { throwFormError } from "../throwFormError";
78
104
  import { isObservableArray, toJS } from "mobx";
79
105
  import { isBeingCalledExcessively } from "../utils/isBeingCalledExcessively";
80
106
  import { getCCDisplayName } from "./utils/tableQueryParamsToHasuraClauses";
81
- import { useHotKeysWrapper } from "./utils/useHotKeysWrapper";
82
- import { withRouter } from "react-router-dom";
83
- import { ReactTable } from "./ReactTable";
84
107
 
85
108
  enablePatches();
109
+ const IS_LINUX = window.navigator.platform.toLowerCase().search("linux") > -1;
110
+
111
+ const itemSizeEstimators = {
112
+ compact: () => 25.34,
113
+ normal: () => 33.34,
114
+ comfortable: () => 41.34
115
+ };
86
116
 
87
117
  const DataTable = ({
88
118
  controlled_pageSize,
@@ -143,15 +173,15 @@ const DataTable = ({
143
173
  reduxFormEntities,
144
174
  reduxFormQueryParams: _reduxFormQueryParams = {},
145
175
  reduxFormSelectedEntityIdMap: _reduxFormSelectedEntityIdMap = {}
146
- } = useSelector(state =>
147
- formValueSelector(formName)(
176
+ } = useSelector(function dtFormParamsSelector(state) {
177
+ return formValueSelector(formName)(
148
178
  state,
149
179
  "reduxFormCellValidation",
150
180
  "reduxFormEntities",
151
181
  "reduxFormQueryParams",
152
182
  "reduxFormSelectedEntityIdMap"
153
- )
154
- );
183
+ );
184
+ });
155
185
 
156
186
  // We want to make sure we don't rerender everything unnecessary
157
187
  // with redux-forms we tend to do unnecessary renders
@@ -376,7 +406,7 @@ const DataTable = ({
376
406
  expandAllByDefault,
377
407
  extraClasses = "",
378
408
  extraCompact: _extraCompact,
379
- filters,
409
+ filters = [],
380
410
  fragment,
381
411
  getCellHoverText,
382
412
  getRowClassName,
@@ -458,8 +488,8 @@ const DataTable = ({
458
488
  () => (reduxFormEntities?.length ? reduxFormEntities : _origEntities) || [],
459
489
  [_origEntities, reduxFormEntities]
460
490
  );
461
-
462
491
  const entities = useDeepEqualMemo(_entities);
492
+
463
493
  const entitiesAcrossPages = useDeepEqualMemo(_entitiesAcrossPages);
464
494
 
465
495
  // This is because we need to maintain the reduxFormSelectedEntityIdMap and
@@ -474,12 +504,13 @@ const DataTable = ({
474
504
  entities,
475
505
  change
476
506
  });
477
- // eslint-disable-next-line react-hooks/exhaustive-deps
478
507
  }, [
479
508
  entitiesAcrossPages,
480
509
  reduxFormSelectedEntityIdMap,
481
510
  change,
482
- noExcessiveCheck
511
+ noExcessiveCheck,
512
+ entities,
513
+ formName
483
514
  ]);
484
515
 
485
516
  const [tableConfig, setTableConfig] = useState({ fieldOptions: [] });
@@ -765,6 +796,11 @@ const DataTable = ({
765
796
  extraCompact = tableConfig.density === "extraCompact";
766
797
  }
767
798
 
799
+ const resized = useMemo(
800
+ () => tableConfig.resized || [],
801
+ [tableConfig?.resized]
802
+ );
803
+
768
804
  const pageSize = controlled_pageSize || _pageSize;
769
805
 
770
806
  const [expandedEntityIdMap, setExpandedEntityIdMap] = useState(() => {
@@ -849,85 +885,189 @@ const DataTable = ({
849
885
  [schema.fields]
850
886
  );
851
887
 
852
- useEffect(() => {
853
- // This is bad practice, we shouldn't be assigning value to an
854
- // external variable
855
- if (helperProp) {
856
- helperProp.updateValidationHelper = () => {
857
- updateValidation(entities, reduxFormCellValidation);
858
- };
888
+ const updateValidationHelper = useCallback(() => {
889
+ updateValidation(entities, reduxFormCellValidation);
890
+ }, [entities, reduxFormCellValidation, updateValidation]);
859
891
 
860
- helperProp.addEditableTableEntities = incomingEnts => {
861
- updateEntitiesHelper(entities, entities => {
862
- const newEntities = incomingEnts.map(e => ({
863
- ...e,
864
- id: e.id || nanoid(),
865
- _isClean: false
866
- }));
892
+ const addEditableTableEntities = useCallback(
893
+ incomingEnts => {
894
+ updateEntitiesHelper(entities, entities => {
895
+ const newEntities = incomingEnts.map(e => ({
896
+ ...e,
897
+ id: e.id || nanoid(),
898
+ _isClean: false
899
+ }));
867
900
 
868
- const { newEnts, validationErrors } = formatAndValidateEntities(
869
- newEntities,
870
- {
871
- useDefaultValues: true,
872
- indexToStartAt: entities.length
873
- }
874
- );
875
- if (every(entities, "_isClean")) {
876
- forEach(newEnts, (e, i) => {
877
- entities[i] = e;
878
- });
879
- } else {
880
- entities.splice(entities.length, 0, ...newEnts);
901
+ const { newEnts, validationErrors } = formatAndValidateEntities(
902
+ newEntities,
903
+ {
904
+ useDefaultValues: true,
905
+ indexToStartAt: entities.length
881
906
  }
882
-
883
- updateValidation(entities, {
884
- ...reduxFormCellValidation,
885
- ...validationErrors
907
+ );
908
+ if (every(entities, "_isClean")) {
909
+ forEach(newEnts, (e, i) => {
910
+ entities[i] = e;
886
911
  });
887
- });
888
- };
912
+ } else {
913
+ entities.splice(entities.length, 0, ...newEnts);
914
+ }
889
915
 
890
- helperProp.getEditableTableInfoAndThrowFormError = () => {
891
- const { entsToUse, validationToUse } = removeCleanRows(
892
- reduxFormEntities,
893
- reduxFormCellValidation
894
- );
895
- const validationWTableErrs = validateTableWideErrors({
896
- entities: entsToUse,
897
- schema,
898
- newCellValidate: validationToUse
916
+ updateValidation(entities, {
917
+ ...reduxFormCellValidation,
918
+ ...validationErrors
899
919
  });
920
+ });
921
+ },
922
+ [
923
+ entities,
924
+ formatAndValidateEntities,
925
+ reduxFormCellValidation,
926
+ updateEntitiesHelper,
927
+ updateValidation
928
+ ]
929
+ );
900
930
 
901
- if (!entsToUse?.length) {
902
- throwFormError(
903
- "Please add at least one row to the table before submitting."
904
- );
905
- }
906
- const invalid =
907
- isEmpty(validationWTableErrs) || !some(validationWTableErrs, v => v)
908
- ? undefined
909
- : validationWTableErrs;
910
-
911
- if (invalid) {
912
- throwFormError(
913
- "Please fix the errors in the table before submitting."
914
- );
915
- }
931
+ const getEditableTableInfoAndThrowFormError = useCallback(() => {
932
+ const { entsToUse, validationToUse } = removeCleanRows(
933
+ reduxFormEntities,
934
+ reduxFormCellValidation
935
+ );
936
+ const validationWTableErrs = validateTableWideErrors({
937
+ entities: entsToUse,
938
+ schema,
939
+ newCellValidate: validationToUse
940
+ });
916
941
 
917
- return entsToUse;
918
- };
942
+ if (!entsToUse?.length) {
943
+ throwFormError(
944
+ "Please add at least one row to the table before submitting."
945
+ );
946
+ }
947
+ const invalid =
948
+ isEmpty(validationWTableErrs) || !some(validationWTableErrs, v => v)
949
+ ? undefined
950
+ : validationWTableErrs;
951
+
952
+ if (invalid) {
953
+ throwFormError("Please fix the errors in the table before submitting.");
954
+ }
955
+
956
+ return entsToUse;
957
+ }, [reduxFormCellValidation, reduxFormEntities, schema]);
958
+
959
+ useEffect(() => {
960
+ // This is bad practice, we shouldn't be assigning value to an
961
+ // external variable
962
+ if (helperProp) {
963
+ helperProp.updateValidationHelper = updateValidationHelper;
964
+ helperProp.addEditableTableEntities = addEditableTableEntities;
965
+ helperProp.getEditableTableInfoAndThrowFormError =
966
+ getEditableTableInfoAndThrowFormError;
919
967
  }
920
968
  }, [
921
- entities,
922
- formatAndValidateEntities,
969
+ addEditableTableEntities,
970
+ getEditableTableInfoAndThrowFormError,
923
971
  helperProp,
924
- reduxFormCellValidation,
925
- reduxFormEntities,
926
- schema,
927
- updateEntitiesHelper,
928
- updateValidation
972
+ updateValidationHelper
929
973
  ]);
930
974
 
975
+ const handleRowMove = useCallback(
976
+ (type, shiftHeld) => e => {
977
+ e.preventDefault();
978
+ e.stopPropagation();
979
+ let newIdMap = {};
980
+ const lastSelectedEnt = getLastSelectedEntity(
981
+ reduxFormSelectedEntityIdMap
982
+ );
983
+
984
+ if (noSelect) return;
985
+ if (lastSelectedEnt) {
986
+ let lastSelectedIndex = entities.findIndex(
987
+ ent => ent === lastSelectedEnt
988
+ );
989
+ if (lastSelectedIndex === -1) {
990
+ if (lastSelectedEnt.id !== undefined) {
991
+ lastSelectedIndex = entities.findIndex(
992
+ ent => ent.id === lastSelectedEnt.id
993
+ );
994
+ } else if (lastSelectedEnt.code !== undefined) {
995
+ lastSelectedIndex = entities.findIndex(
996
+ ent => ent.code === lastSelectedEnt.code
997
+ );
998
+ }
999
+ }
1000
+ if (lastSelectedIndex === -1) {
1001
+ return;
1002
+ }
1003
+ const newEntToSelect = getNewEntToSelect({
1004
+ type,
1005
+ lastSelectedIndex,
1006
+ entities,
1007
+ isEntityDisabled
1008
+ });
1009
+
1010
+ if (!newEntToSelect) return;
1011
+ if (shiftHeld && !isSingleSelect) {
1012
+ if (
1013
+ reduxFormSelectedEntityIdMap[
1014
+ newEntToSelect.id || newEntToSelect.code
1015
+ ]
1016
+ ) {
1017
+ //the entity being moved to has already been selected
1018
+ newIdMap = omit(reduxFormSelectedEntityIdMap, [
1019
+ lastSelectedEnt.id || lastSelectedEnt.code
1020
+ ]);
1021
+ newIdMap[newEntToSelect.id || newEntToSelect.code].time =
1022
+ Date.now() + 1;
1023
+ } else {
1024
+ //the entity being moved to has NOT been selected yet
1025
+ newIdMap = {
1026
+ ...reduxFormSelectedEntityIdMap,
1027
+ [newEntToSelect.id || newEntToSelect.code]: {
1028
+ entity: newEntToSelect,
1029
+ time: Date.now()
1030
+ }
1031
+ };
1032
+ }
1033
+ } else {
1034
+ //no shiftHeld
1035
+ newIdMap[newEntToSelect.id || newEntToSelect.code] = {
1036
+ entity: newEntToSelect,
1037
+ time: Date.now()
1038
+ };
1039
+ }
1040
+ }
1041
+
1042
+ finalizeSelection({
1043
+ idMap: newIdMap,
1044
+ entities,
1045
+ props: {
1046
+ onDeselect,
1047
+ onSingleRowSelect,
1048
+ onMultiRowSelect,
1049
+ noDeselectAll,
1050
+ onRowSelect,
1051
+ noSelect,
1052
+ change
1053
+ }
1054
+ });
1055
+ },
1056
+ [
1057
+ change,
1058
+ entities,
1059
+ isEntityDisabled,
1060
+ isSingleSelect,
1061
+ noDeselectAll,
1062
+ noSelect,
1063
+ onDeselect,
1064
+ onMultiRowSelect,
1065
+ onRowSelect,
1066
+ onSingleRowSelect,
1067
+ reduxFormSelectedEntityIdMap
1068
+ ]
1069
+ );
1070
+
931
1071
  const primarySelectedCellId = useMemo(() => {
932
1072
  for (const k of Object.keys(selectedCells)) {
933
1073
  if (selectedCells[k] === PRIMARY_SELECTED_VAL) {
@@ -963,6 +1103,50 @@ const DataTable = ({
963
1103
  [change]
964
1104
  );
965
1105
 
1106
+ const handleEnterStartCellEdit = useCallback(
1107
+ e => {
1108
+ e.stopPropagation();
1109
+ startCellEdit(primarySelectedCellId);
1110
+ },
1111
+ [primarySelectedCellId, startCellEdit]
1112
+ );
1113
+
1114
+ const handleDeleteCell = useCallback(() => {
1115
+ const newCellValidate = {
1116
+ ...reduxFormCellValidation
1117
+ };
1118
+ if (isEmpty(selectedCells)) return;
1119
+ const rowIds = [];
1120
+ updateEntitiesHelper(entities, entities => {
1121
+ const entityIdToEntity = getEntityIdToEntity(entities);
1122
+ Object.keys(selectedCells).forEach(cellId => {
1123
+ const [rowId, path] = cellId.split(":");
1124
+ rowIds.push(rowId);
1125
+ const entity = entityIdToEntity[rowId].e;
1126
+ delete entity._isClean;
1127
+ const { error } = editCellHelper({
1128
+ entity,
1129
+ path,
1130
+ schema,
1131
+ newVal: ""
1132
+ });
1133
+ if (error) {
1134
+ newCellValidate[cellId] = error;
1135
+ } else {
1136
+ delete newCellValidate[cellId];
1137
+ }
1138
+ });
1139
+ updateValidation(entities, newCellValidate);
1140
+ });
1141
+ }, [
1142
+ entities,
1143
+ reduxFormCellValidation,
1144
+ schema,
1145
+ selectedCells,
1146
+ updateEntitiesHelper,
1147
+ updateValidation
1148
+ ]);
1149
+
966
1150
  const waitUntilAllRowsAreRendered = useCallback(() => {
967
1151
  return new Promise(resolve => {
968
1152
  const interval = setInterval(() => {
@@ -977,6 +1161,74 @@ const DataTable = ({
977
1161
  // eslint-disable-next-line react-hooks/exhaustive-deps
978
1162
  }, []);
979
1163
 
1164
+ const handleCopySelectedCells = useCallback(async () => {
1165
+ // if the current selection is consecutive cells then copy with
1166
+ // tabs between. if not then just select primary selected cell
1167
+ if (isEmpty(selectedCells)) return;
1168
+
1169
+ // Temporarily disable virtualization for large tables
1170
+ if (entities.length > VIRTUALIZE_CUTOFF_LENGTH) {
1171
+ setNoVirtual(true);
1172
+ await waitUntilAllRowsAreRendered();
1173
+ }
1174
+
1175
+ const pathToIndex = getFieldPathToIndex(schema);
1176
+ const entityIdToEntity = getEntityIdToEntity(entities);
1177
+ const selectionGrid = [];
1178
+ let firstRowIndex;
1179
+ let firstCellIndex;
1180
+ Object.keys(selectedCells).forEach(key => {
1181
+ const [rowId, path] = key.split(":");
1182
+ const eInfo = entityIdToEntity[rowId];
1183
+ if (eInfo) {
1184
+ if (firstRowIndex === undefined || eInfo.i < firstRowIndex) {
1185
+ firstRowIndex = eInfo.i;
1186
+ }
1187
+ if (!selectionGrid[eInfo.i]) {
1188
+ selectionGrid[eInfo.i] = [];
1189
+ }
1190
+ const cellIndex = pathToIndex[path];
1191
+ if (firstCellIndex === undefined || cellIndex < firstCellIndex) {
1192
+ firstCellIndex = cellIndex;
1193
+ }
1194
+ selectionGrid[eInfo.i][cellIndex] = true;
1195
+ }
1196
+ });
1197
+ if (firstRowIndex === undefined) return;
1198
+ const allRows = getAllRows(tableRef);
1199
+ let fullCellText = "";
1200
+ const fullJson = [];
1201
+ times(selectionGrid.length, i => {
1202
+ const row = selectionGrid[i];
1203
+ if (fullCellText) {
1204
+ fullCellText += "\n";
1205
+ }
1206
+ if (!row) {
1207
+ return;
1208
+ } else {
1209
+ const jsonRow = [];
1210
+ // ignore header
1211
+ let [rowCopyText, json] = getRowCopyText(allRows[i + 1]);
1212
+ rowCopyText = rowCopyText.split("\t");
1213
+ times(row.length, i => {
1214
+ const cell = row[i];
1215
+ if (cell) {
1216
+ fullCellText += rowCopyText[i];
1217
+ jsonRow.push(json[i]);
1218
+ }
1219
+ if (i !== row.length - 1 && i >= firstCellIndex) fullCellText += "\t";
1220
+ });
1221
+ fullJson.push(jsonRow);
1222
+ }
1223
+ });
1224
+ if (!fullCellText) return window.toastr.warning("No text to copy");
1225
+
1226
+ handleCopyHelper(fullCellText, fullJson, "Selected cells copied");
1227
+
1228
+ // Re-enable virtualization if it was disabled
1229
+ setNoVirtual(false);
1230
+ }, [entities, selectedCells, schema, waitUntilAllRowsAreRendered]);
1231
+
980
1232
  const handleCopySelectedRows = useCallback(
981
1233
  async selectedRecords => {
982
1234
  if (entities.length > VIRTUALIZE_CUTOFF_LENGTH) {
@@ -1019,36 +1271,237 @@ const DataTable = ({
1019
1271
  [entities, waitUntilAllRowsAreRendered]
1020
1272
  );
1021
1273
 
1022
- const { handleKeyDown, handleKeyUp } = useHotKeysWrapper({
1274
+ const handleCopyHotkey = useCallback(
1275
+ e => {
1276
+ if (isCellEditable) {
1277
+ handleCopySelectedCells(e);
1278
+ } else {
1279
+ handleCopySelectedRows(
1280
+ getRecordsFromIdMap(reduxFormSelectedEntityIdMap),
1281
+ e
1282
+ );
1283
+ }
1284
+ },
1285
+ [
1286
+ handleCopySelectedCells,
1287
+ handleCopySelectedRows,
1288
+ isCellEditable,
1289
+ reduxFormSelectedEntityIdMap
1290
+ ]
1291
+ );
1292
+
1293
+ const handleCut = useCallback(
1294
+ e => {
1295
+ handleDeleteCell();
1296
+ handleCopyHotkey(e);
1297
+ },
1298
+ [handleCopyHotkey, handleDeleteCell]
1299
+ );
1300
+
1301
+ const flashTableBorder = () => {
1302
+ try {
1303
+ const table = tableRef.current.tableRef;
1304
+ table.classList.add("tgBorderBlue");
1305
+ setTimeout(() => {
1306
+ table.classList.remove("tgBorderBlue");
1307
+ }, 300);
1308
+ } catch (e) {
1309
+ console.error(`err when flashing table border:`, e);
1310
+ }
1311
+ };
1312
+
1313
+ const handleUndo = useCallback(() => {
1314
+ if (entitiesUndoRedoStack.currentVersion > 0) {
1315
+ flashTableBorder();
1316
+ const nextState = applyPatches(
1317
+ entities,
1318
+ entitiesUndoRedoStack[entitiesUndoRedoStack.currentVersion]
1319
+ .inversePatches
1320
+ );
1321
+ const { newEnts, validationErrors } =
1322
+ formatAndValidateEntities(nextState);
1323
+ setEntitiesUndoRedoStack(prev => ({
1324
+ ...prev,
1325
+ currentVersion: prev.currentVersion - 1
1326
+ }));
1327
+ updateValidation(newEnts, validationErrors);
1328
+ change("reduxFormEntities", newEnts);
1329
+ }
1330
+ }, [
1023
1331
  change,
1024
1332
  entities,
1333
+ formatAndValidateEntities,
1025
1334
  entitiesUndoRedoStack,
1335
+ updateValidation
1336
+ ]);
1337
+
1338
+ const handleRedo = useCallback(() => {
1339
+ const nextV = entitiesUndoRedoStack.currentVersion + 1;
1340
+ if (entitiesUndoRedoStack[nextV]) {
1341
+ flashTableBorder();
1342
+ const nextState = applyPatches(
1343
+ entities,
1344
+ entitiesUndoRedoStack[nextV].patches
1345
+ );
1346
+ const { newEnts, validationErrors } =
1347
+ formatAndValidateEntities(nextState);
1348
+ change("reduxFormEntities", newEnts);
1349
+ updateValidation(newEnts, validationErrors);
1350
+ setEntitiesUndoRedoStack(prev => ({
1351
+ ...prev,
1352
+ currentVersion: nextV
1353
+ }));
1354
+ }
1355
+ }, [
1356
+ change,
1357
+ entities,
1026
1358
  formatAndValidateEntities,
1027
- handleCopySelectedRows,
1028
- isCellEditable,
1029
- isEntityDisabled,
1030
- isSingleSelect,
1031
- noDeselectAll,
1032
- noSelect,
1033
- onDeselect,
1034
- onMultiRowSelect,
1035
- onRowSelect,
1036
- onSingleRowSelect,
1037
- primarySelectedCellId,
1038
- reduxFormCellValidation,
1039
- reduxFormSelectedEntityIdMap,
1040
- schema,
1041
- selectedCells,
1042
- setEntitiesUndoRedoStack,
1043
- setNoVirtual,
1044
- setSelectedCells,
1045
- startCellEdit,
1046
- tableRef,
1047
- updateEntitiesHelper,
1048
- updateValidation,
1049
- waitUntilAllRowsAreRendered
1050
- });
1359
+ entitiesUndoRedoStack,
1360
+ updateValidation
1361
+ ]);
1362
+
1363
+ const handleSelectAllRows = useCallback(
1364
+ e => {
1365
+ if (isSingleSelect) return;
1366
+ e.preventDefault();
1367
+
1368
+ if (isCellEditable) {
1369
+ const schemaPaths = schema.fields.map(f => f.path);
1370
+ const newSelectedCells = {};
1371
+ entities.forEach((entity, i) => {
1372
+ if (isEntityDisabled(entity)) return;
1373
+ const entityId = getIdOrCodeOrIndex(entity, i);
1374
+ schemaPaths.forEach(p => {
1375
+ newSelectedCells[`${entityId}:${p}`] = true;
1376
+ });
1377
+ });
1378
+ setSelectedCells(newSelectedCells);
1379
+ } else {
1380
+ const newIdMap = {};
1381
+
1382
+ entities.forEach((entity, i) => {
1383
+ if (isEntityDisabled(entity)) return;
1384
+ const entityId = getIdOrCodeOrIndex(entity, i);
1385
+ newIdMap[entityId] = { entity };
1386
+ });
1387
+ finalizeSelection({
1388
+ idMap: newIdMap,
1389
+ entities,
1390
+ props: {
1391
+ onDeselect,
1392
+ onSingleRowSelect,
1393
+ onMultiRowSelect,
1394
+ noDeselectAll,
1395
+ onRowSelect,
1396
+ noSelect,
1397
+ change
1398
+ }
1399
+ });
1400
+ }
1401
+ },
1402
+ [
1403
+ change,
1404
+ entities,
1405
+ isCellEditable,
1406
+ isEntityDisabled,
1407
+ isSingleSelect,
1408
+ noDeselectAll,
1409
+ noSelect,
1410
+ onDeselect,
1411
+ onMultiRowSelect,
1412
+ onRowSelect,
1413
+ onSingleRowSelect,
1414
+ schema.fields
1415
+ ]
1416
+ );
1417
+
1418
+ const hotKeys = useMemo(
1419
+ () => [
1420
+ {
1421
+ global: false,
1422
+ combo: "up",
1423
+ label: "Move Up a Row",
1424
+ onKeyDown: handleRowMove("up")
1425
+ },
1426
+ {
1427
+ global: false,
1428
+ combo: "down",
1429
+ label: "Move Down a Row",
1430
+ onKeyDown: handleRowMove("down")
1431
+ },
1432
+ {
1433
+ global: false,
1434
+ combo: "up+shift",
1435
+ label: "Move Up a Row",
1436
+ onKeyDown: handleRowMove("up", true)
1437
+ },
1438
+ ...(isCellEditable
1439
+ ? [
1440
+ {
1441
+ global: false,
1442
+ combo: "enter",
1443
+ label: "Enter -> Start Cell Edit",
1444
+ onKeyDown: handleEnterStartCellEdit
1445
+ },
1446
+ {
1447
+ global: false,
1448
+ combo: "mod+x",
1449
+ label: "Cut",
1450
+ onKeyDown: handleCut
1451
+ },
1452
+ {
1453
+ global: false,
1454
+ combo: IS_LINUX ? "alt+z" : "mod+z",
1455
+ label: "Undo",
1456
+ onKeyDown: handleUndo
1457
+ },
1458
+ {
1459
+ global: false,
1460
+ combo: IS_LINUX ? "alt+shift+z" : "mod+shift+z",
1461
+ label: "Redo",
1462
+ onKeyDown: handleRedo
1463
+ },
1464
+ {
1465
+ global: false,
1466
+ combo: "backspace",
1467
+ label: "Delete Cell",
1468
+ onKeyDown: handleDeleteCell
1469
+ }
1470
+ ]
1471
+ : []),
1472
+ {
1473
+ global: false,
1474
+ combo: "down+shift",
1475
+ label: "Move Down a Row",
1476
+ onKeyDown: handleRowMove("down", true)
1477
+ },
1478
+ {
1479
+ global: false,
1480
+ combo: "mod + c",
1481
+ label: "Copy rows",
1482
+ onKeyDown: handleCopyHotkey
1483
+ },
1484
+ {
1485
+ global: false,
1486
+ combo: "mod + a",
1487
+ label: "Select rows",
1488
+ onKeyDown: handleSelectAllRows
1489
+ }
1490
+ ],
1491
+ [
1492
+ handleCopyHotkey,
1493
+ handleCut,
1494
+ handleDeleteCell,
1495
+ handleEnterStartCellEdit,
1496
+ handleRedo,
1497
+ handleRowMove,
1498
+ handleSelectAllRows,
1499
+ handleUndo,
1500
+ isCellEditable
1501
+ ]
1502
+ );
1051
1503
 
1504
+ const { handleKeyDown, handleKeyUp } = useHotkeys(hotKeys);
1052
1505
  const [columns, setColumns] = useState([]);
1053
1506
  const [fullscreen, setFullscreen] = useState(false);
1054
1507
  const [selectingAll, setSelectingAll] = useState(false);
@@ -1352,6 +1805,42 @@ const DataTable = ({
1352
1805
  }
1353
1806
  }, [entities, selectedIds, selectAllByDefault, setSelectedIds]);
1354
1807
 
1808
+ const TheadComponent = useCallback(
1809
+ ({ className, style, children }) => {
1810
+ const moveColumn = ({ oldIndex, newIndex }) => {
1811
+ let oldStateColumnIndex, newStateColumnIndex;
1812
+ columns.forEach((column, i) => {
1813
+ if (oldIndex === column.columnIndex) oldStateColumnIndex = i;
1814
+ if (newIndex === column.columnIndex) newStateColumnIndex = i;
1815
+ });
1816
+ // because it is all handled in state we need
1817
+ // to perform the move and update the columnIndices
1818
+ // because they are used for the sortable columns
1819
+ const newColumns = arrayMove(
1820
+ columns,
1821
+ oldStateColumnIndex,
1822
+ newStateColumnIndex
1823
+ ).map((column, i) => {
1824
+ return {
1825
+ ...column,
1826
+ columnIndex: i
1827
+ };
1828
+ });
1829
+ setColumns(newColumns);
1830
+ };
1831
+ return (
1832
+ <SortableColumns
1833
+ className={className}
1834
+ style={style}
1835
+ moveColumn={moveColumnPersist || moveColumn}
1836
+ >
1837
+ {children}
1838
+ </SortableColumns>
1839
+ );
1840
+ },
1841
+ [columns, moveColumnPersist]
1842
+ );
1843
+
1355
1844
  const addEntitiesToSelection = entities => {
1356
1845
  const idMap = reduxFormSelectedEntityIdMap || {};
1357
1846
  const newIdMap = cloneDeep(idMap) || {};
@@ -1588,6 +2077,426 @@ const DataTable = ({
1588
2077
  ]
1589
2078
  );
1590
2079
 
2080
+ const showContextMenu = useCallback(
2081
+ (e, { idMap, selectedCells } = {}) => {
2082
+ let selectedRecords;
2083
+ if (isCellEditable) {
2084
+ const rowIds = {};
2085
+ Object.keys(selectedCells).forEach(cellKey => {
2086
+ const [rowId] = cellKey.split(":");
2087
+ rowIds[rowId] = true;
2088
+ });
2089
+ selectedRecords = entities.filter(
2090
+ ent => rowIds[getIdOrCodeOrIndex(ent)]
2091
+ );
2092
+ } else {
2093
+ selectedRecords = getRecordsFromIdMap(idMap);
2094
+ }
2095
+
2096
+ const itemsToRender = contextMenu({
2097
+ selectedRecords,
2098
+ history
2099
+ });
2100
+ if (!itemsToRender && !isCopyable) return null;
2101
+ const copyMenuItems = [];
2102
+
2103
+ e.persist();
2104
+ if (isCopyable) {
2105
+ //compute the cellWrapper here so we don't lose access to it
2106
+ const cellWrapper =
2107
+ e.target.querySelector(".tg-cell-wrapper") ||
2108
+ e.target.closest(".tg-cell-wrapper");
2109
+ if (cellWrapper) {
2110
+ copyMenuItems.push(
2111
+ <MenuItem
2112
+ key="copyCell"
2113
+ onClick={() => {
2114
+ //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
2115
+ //do we need to be able to copy hidden cells? It seems like it should just copy what's on the page..?
2116
+ const specificColumn = cellWrapper.getAttribute("data-test");
2117
+ handleCopyRows([cellWrapper.closest(".rt-tr")], {
2118
+ specificColumn,
2119
+ onFinishMsg: "Cell copied"
2120
+ });
2121
+ const [text, jsonText] = getCellCopyText(cellWrapper);
2122
+ handleCopyHelper(text, jsonText);
2123
+ }}
2124
+ text="Cell"
2125
+ />
2126
+ );
2127
+
2128
+ copyMenuItems.push(
2129
+ <MenuItem
2130
+ key="copyColumn"
2131
+ onClick={() => {
2132
+ handleCopyColumn(tableRef, cellWrapper);
2133
+ }}
2134
+ text="Column"
2135
+ />
2136
+ );
2137
+ if (selectedRecords.length > 1) {
2138
+ copyMenuItems.push(
2139
+ <MenuItem
2140
+ key="copyColumnSelected"
2141
+ onClick={() => {
2142
+ handleCopyColumn(tableRef, cellWrapper, selectedRecords);
2143
+ }}
2144
+ text="Column (Selected)"
2145
+ />
2146
+ );
2147
+ }
2148
+ }
2149
+ if (selectedRecords.length === 0 || selectedRecords.length === 1) {
2150
+ //compute the row here so we don't lose access to it
2151
+ const cell =
2152
+ e.target.querySelector(".tg-cell-wrapper") ||
2153
+ e.target.closest(".tg-cell-wrapper") ||
2154
+ e.target.closest(".rt-td");
2155
+ const row = cell.closest(".rt-tr");
2156
+ copyMenuItems.push(
2157
+ <MenuItem
2158
+ key="copySelectedRows"
2159
+ onClick={() => {
2160
+ handleCopyRows([row]);
2161
+ // loop through each cell in the row
2162
+ }}
2163
+ text="Row"
2164
+ />
2165
+ );
2166
+ } else if (selectedRecords.length > 1) {
2167
+ copyMenuItems.push(
2168
+ <MenuItem
2169
+ key="copySelectedRows"
2170
+ onClick={() => {
2171
+ handleCopySelectedRows(selectedRecords, e);
2172
+ // loop through each cell in the row
2173
+ }}
2174
+ text="Rows"
2175
+ />
2176
+ );
2177
+ }
2178
+ copyMenuItems.push(
2179
+ <MenuItem
2180
+ key="copyFullTableRows"
2181
+ onClick={() => {
2182
+ handleCopyTable(tableRef);
2183
+ // loop through each cell in the row
2184
+ }}
2185
+ text="Table"
2186
+ />
2187
+ );
2188
+ }
2189
+ const selectedRowIds = Object.keys(selectedCells).map(cellId => {
2190
+ const [rowId] = cellId.split(":");
2191
+ return rowId;
2192
+ });
2193
+
2194
+ const menu = (
2195
+ <Menu>
2196
+ {itemsToRender}
2197
+ {copyMenuItems.length && (
2198
+ <MenuItem icon="clipboard" key="copyOpts" text="Copy">
2199
+ {copyMenuItems}
2200
+ </MenuItem>
2201
+ )}
2202
+ {isCellEditable && (
2203
+ <>
2204
+ <MenuItem
2205
+ icon="add-row-top"
2206
+ text="Add Row Above"
2207
+ key="addRowAbove"
2208
+ onClick={() => {
2209
+ insertRows({ above: true });
2210
+ }}
2211
+ />
2212
+ <MenuItem
2213
+ icon="add-row-top"
2214
+ text="Add Row Below"
2215
+ key="addRowBelow"
2216
+ onClick={() => {
2217
+ insertRows({});
2218
+ }}
2219
+ />
2220
+ <MenuItem
2221
+ icon="remove"
2222
+ text={`Remove Row${selectedRowIds.length > 1 ? "s" : ""}`}
2223
+ key="removeRow"
2224
+ onClick={() => {
2225
+ const selectedRowIds = Object.keys(selectedCells).map(
2226
+ cellId => {
2227
+ const [rowId] = cellId.split(":");
2228
+ return rowId;
2229
+ }
2230
+ );
2231
+ updateEntitiesHelper(entities, entities => {
2232
+ const ents = entities.filter(
2233
+ (e, i) =>
2234
+ !selectedRowIds.includes(getIdOrCodeOrIndex(e, i))
2235
+ );
2236
+ updateValidation(
2237
+ ents,
2238
+ omitBy(reduxFormCellValidation, (v, cellId) =>
2239
+ selectedRowIds.includes(cellId.split(":")[0])
2240
+ )
2241
+ );
2242
+ return ents;
2243
+ });
2244
+ refocusTable();
2245
+ }}
2246
+ />
2247
+ </>
2248
+ )}
2249
+ </Menu>
2250
+ );
2251
+ ContextMenu.show(menu, { left: e.clientX, top: e.clientY });
2252
+ },
2253
+ [
2254
+ contextMenu,
2255
+ entities,
2256
+ handleCopySelectedRows,
2257
+ history,
2258
+ insertRows,
2259
+ isCellEditable,
2260
+ isCopyable,
2261
+ reduxFormCellValidation,
2262
+ refocusTable,
2263
+ updateEntitiesHelper,
2264
+ updateValidation
2265
+ ]
2266
+ );
2267
+
2268
+ const getTableRowProps = useCallback(
2269
+ (state, rowInfo) => {
2270
+ if (!rowInfo) {
2271
+ return {
2272
+ className: "no-row-data"
2273
+ };
2274
+ }
2275
+ const entity = rowInfo.original;
2276
+ const rowId = getIdOrCodeOrIndex(entity, rowInfo.index);
2277
+ const rowSelected = reduxFormSelectedEntityIdMap[rowId];
2278
+ const isExpanded = expandedEntityIdMap[rowId];
2279
+ const rowDisabled = isEntityDisabled(entity);
2280
+ const dataId = entity.id || entity.code;
2281
+ return {
2282
+ onClick: e => {
2283
+ if (isCellEditable) return;
2284
+ // if checkboxes are activated or row expander is clicked don't select row
2285
+ if (e.target.matches(".tg-expander, .tg-expander *")) {
2286
+ setExpandedEntityIdMap(prev => ({ ...prev, [rowId]: !isExpanded }));
2287
+ return;
2288
+ } else if (
2289
+ e.target.closest(".tg-react-table-checkbox-cell-container")
2290
+ ) {
2291
+ return;
2292
+ } else if (mustClickCheckboxToSelect) {
2293
+ return;
2294
+ }
2295
+ if (e.detail > 1) {
2296
+ return; //cancel multiple quick clicks
2297
+ }
2298
+ rowClick(e, rowInfo, entities, {
2299
+ reduxFormSelectedEntityIdMap,
2300
+ isSingleSelect,
2301
+ noSelect,
2302
+ onRowClick,
2303
+ isEntityDisabled,
2304
+ withCheckboxes,
2305
+ onDeselect,
2306
+ onSingleRowSelect,
2307
+ onMultiRowSelect,
2308
+ noDeselectAll,
2309
+ onRowSelect,
2310
+ change
2311
+ });
2312
+ },
2313
+ //row right click
2314
+ onContextMenu: e => {
2315
+ e.preventDefault();
2316
+ if (rowId === undefined || rowDisabled || isCellEditable) return;
2317
+ const oldIdMap = cloneDeep(reduxFormSelectedEntityIdMap) || {};
2318
+ let newIdMap;
2319
+ if (withCheckboxes) {
2320
+ newIdMap = oldIdMap;
2321
+ } else {
2322
+ // if we are not using checkboxes we need to make sure
2323
+ // that the id of the record gets added to the id map
2324
+ newIdMap = oldIdMap[rowId] ? oldIdMap : { [rowId]: { entity } };
2325
+
2326
+ // tgreen: this will refresh the selection with fresh data. The entities in redux might not be up to date
2327
+ const keyedEntities = keyBy(entities, getIdOrCodeOrIndex);
2328
+ forEach(newIdMap, (val, key) => {
2329
+ const freshEntity = keyedEntities[key];
2330
+ if (freshEntity) {
2331
+ newIdMap[key] = { ...newIdMap[key], entity: freshEntity };
2332
+ }
2333
+ });
2334
+ finalizeSelection({
2335
+ idMap: newIdMap,
2336
+ entities,
2337
+ props: {
2338
+ onDeselect,
2339
+ onSingleRowSelect,
2340
+ onMultiRowSelect,
2341
+ noDeselectAll,
2342
+ onRowSelect,
2343
+ noSelect,
2344
+ change
2345
+ }
2346
+ });
2347
+ }
2348
+ showContextMenu(e, { idMap: newIdMap, selectedCells });
2349
+ },
2350
+ className: classNames(
2351
+ "with-row-data",
2352
+ getRowClassName && getRowClassName(rowInfo, state),
2353
+ {
2354
+ disabled: rowDisabled,
2355
+ selected: rowSelected && !withCheckboxes,
2356
+ "rt-tr-last-row": rowInfo.index === entities.length - 1
2357
+ }
2358
+ ),
2359
+ "data-test-id": dataId === undefined ? rowInfo.index : dataId,
2360
+ "data-index": rowInfo.index,
2361
+ "data-tip": typeof rowDisabled === "string" ? rowDisabled : undefined,
2362
+ onDoubleClick: e => {
2363
+ if (rowDisabled) return;
2364
+ onDoubleClick &&
2365
+ onDoubleClick(rowInfo.original, rowInfo.index, history, e);
2366
+ }
2367
+ };
2368
+ },
2369
+ [
2370
+ change,
2371
+ entities,
2372
+ expandedEntityIdMap,
2373
+ getRowClassName,
2374
+ history,
2375
+ isCellEditable,
2376
+ isEntityDisabled,
2377
+ isSingleSelect,
2378
+ mustClickCheckboxToSelect,
2379
+ noDeselectAll,
2380
+ noSelect,
2381
+ onDeselect,
2382
+ onDoubleClick,
2383
+ onMultiRowSelect,
2384
+ onRowClick,
2385
+ onRowSelect,
2386
+ onSingleRowSelect,
2387
+ reduxFormSelectedEntityIdMap,
2388
+ selectedCells,
2389
+ showContextMenu,
2390
+ withCheckboxes
2391
+ ]
2392
+ );
2393
+
2394
+ const getTableCellProps = useCallback(
2395
+ (state, rowInfo, column) => {
2396
+ if (!isCellEditable) return {}; //only allow cell selection to do stuff here
2397
+ if (!rowInfo) return {};
2398
+ if (!reduxFormCellValidation) return {};
2399
+ const entity = rowInfo.original;
2400
+ const rowIndex = rowInfo.index;
2401
+ const rowId = getIdOrCodeOrIndex(entity, rowIndex);
2402
+ const {
2403
+ cellId,
2404
+ cellIdAbove,
2405
+ cellIdToRight,
2406
+ cellIdBelow,
2407
+ cellIdToLeft,
2408
+ rowDisabled,
2409
+ columnIndex
2410
+ } = getCellInfo({
2411
+ columnIndex: column.index,
2412
+ columnPath: column.path,
2413
+ rowId,
2414
+ schema,
2415
+ entities,
2416
+ rowIndex,
2417
+ isEntityDisabled,
2418
+ entity
2419
+ });
2420
+
2421
+ const _isClean =
2422
+ (entity._isClean && doNotValidateUntouchedRows) ||
2423
+ isEntityClean(entity);
2424
+
2425
+ const err = !_isClean && reduxFormCellValidation[cellId];
2426
+ let selectedTopBorder,
2427
+ selectedRightBorder,
2428
+ selectedBottomBorder,
2429
+ selectedLeftBorder;
2430
+ if (selectedCells[cellId]) {
2431
+ selectedTopBorder = !selectedCells[cellIdAbove];
2432
+ selectedRightBorder = !selectedCells[cellIdToRight];
2433
+ selectedBottomBorder = !selectedCells[cellIdBelow];
2434
+ selectedLeftBorder = !selectedCells[cellIdToLeft];
2435
+ }
2436
+ const isPrimarySelected = selectedCells[cellId] === PRIMARY_SELECTED_VAL;
2437
+ const className = classNames({
2438
+ isSelectedCell: selectedCells[cellId],
2439
+ isPrimarySelected,
2440
+ isSecondarySelected: selectedCells[cellId] === true,
2441
+ noSelectedTopBorder: !selectedTopBorder,
2442
+ isCleanRow: _isClean,
2443
+ noSelectedRightBorder: !selectedRightBorder,
2444
+ noSelectedBottomBorder: !selectedBottomBorder,
2445
+ noSelectedLeftBorder: !selectedLeftBorder,
2446
+ isDropdownCell: column.type === "dropdown",
2447
+ isEditingCell: reduxFormEditingCell === cellId,
2448
+ hasCellError: !!err,
2449
+ "no-data-tip": selectedCells[cellId]
2450
+ });
2451
+ return {
2452
+ onDoubleClick: () => {
2453
+ // cell double click
2454
+ if (rowDisabled) return;
2455
+ startCellEdit(cellId);
2456
+ },
2457
+ ...(err && {
2458
+ "data-tip": err?.message || err,
2459
+ "data-no-child-data-tip": true
2460
+ }),
2461
+ onContextMenu: e => {
2462
+ const newSelectedCells = { ...selectedCells };
2463
+ if (!isPrimarySelected) {
2464
+ if (primarySelectedCellId) {
2465
+ newSelectedCells[primarySelectedCellId] = true;
2466
+ }
2467
+ newSelectedCells[cellId] = PRIMARY_SELECTED_VAL;
2468
+ setSelectedCells(newSelectedCells);
2469
+ }
2470
+ showContextMenu(e, { selectedCells: newSelectedCells });
2471
+ },
2472
+ onClick: event => {
2473
+ handleCellClick({
2474
+ event,
2475
+ cellId,
2476
+ rowDisabled,
2477
+ rowIndex,
2478
+ columnIndex
2479
+ });
2480
+ },
2481
+ className
2482
+ };
2483
+ },
2484
+ [
2485
+ doNotValidateUntouchedRows,
2486
+ entities,
2487
+ handleCellClick,
2488
+ isCellEditable,
2489
+ isEntityDisabled,
2490
+ primarySelectedCellId,
2491
+ reduxFormCellValidation,
2492
+ reduxFormEditingCell,
2493
+ schema,
2494
+ selectedCells,
2495
+ showContextMenu,
2496
+ startCellEdit
2497
+ ]
2498
+ );
2499
+
1591
2500
  if (withSelectAll && !safeQuery) {
1592
2501
  throw new Error("safeQuery is needed for selecting all table records");
1593
2502
  }
@@ -1603,7 +2512,7 @@ const DataTable = ({
1603
2512
  : "";
1604
2513
 
1605
2514
  const hasFilters =
1606
- filters?.length ||
2515
+ filters.length ||
1607
2516
  searchTerm ||
1608
2517
  schema.fields.some(
1609
2518
  field => field.filterIsActive && field.filterIsActive(currentParams)
@@ -1616,7 +2525,7 @@ const DataTable = ({
1616
2525
 
1617
2526
  const filtersOnNonDisplayedFields = useMemo(() => {
1618
2527
  const _filtersOnNonDisplayedFields = [];
1619
- if (filters?.length) {
2528
+ if (filters && filters.length) {
1620
2529
  schema.fields.forEach(field => {
1621
2530
  const ccDisplayName = getCCDisplayName(field);
1622
2531
  if (field.isHidden) {
@@ -1641,6 +2550,18 @@ const DataTable = ({
1641
2550
  );
1642
2551
  const selectedRowCount = Object.keys(idMap).filter(key => idMap[key]).length;
1643
2552
 
2553
+ let rowsToShow = doNotShowEmptyRows
2554
+ ? Math.min(numRows, entities.length)
2555
+ : numRows;
2556
+ // if there are no entities then provide enough space to show
2557
+ // no rows found message
2558
+ if (entities.length === 0 && rowsToShow < 3) rowsToShow = 3;
2559
+ const expandedRows = entities.reduce((acc, row, index) => {
2560
+ const rowId = getIdOrCodeOrIndex(row, index);
2561
+ acc[index] = expandedEntityIdMap[rowId];
2562
+ return acc;
2563
+ }, {});
2564
+
1644
2565
  const showHeader = (withTitle || withSearch || children) && !noHeader;
1645
2566
  const toggleFullscreenButton = (
1646
2567
  <Button
@@ -1727,6 +2648,21 @@ const DataTable = ({
1727
2648
  withPaging &&
1728
2649
  (hidePageSizeWhenPossible ? entityCount > pageSize : true);
1729
2650
 
2651
+ const SubComponentToUse = useMemo(() => {
2652
+ if (SubComponent) {
2653
+ return row => {
2654
+ let shouldShow = true;
2655
+ if (shouldShowSubComponent) {
2656
+ shouldShow = shouldShowSubComponent(row.original);
2657
+ }
2658
+ if (shouldShow) {
2659
+ return SubComponent(row);
2660
+ }
2661
+ };
2662
+ }
2663
+ return;
2664
+ }, [SubComponent, shouldShowSubComponent]);
2665
+
1730
2666
  const nonDisplayedFilterComp = useMemo(() => {
1731
2667
  if (filtersOnNonDisplayedFields.length) {
1732
2668
  const content = filtersOnNonDisplayedFields.map(
@@ -1771,12 +2707,173 @@ const DataTable = ({
1771
2707
  return null;
1772
2708
  }, [filtersOnNonDisplayedFields]);
1773
2709
 
2710
+ const filteredEnts = useMemo(() => {
2711
+ if (onlyShowRowsWErrors) {
2712
+ const rowToErrorMap = {};
2713
+ forEach(reduxFormCellValidation, (err, cellId) => {
2714
+ if (err) {
2715
+ const [rowId] = cellId.split(":");
2716
+ rowToErrorMap[rowId] = true;
2717
+ }
2718
+ });
2719
+ return entities.filter(e => {
2720
+ return rowToErrorMap[e.id];
2721
+ });
2722
+ }
2723
+ return entities;
2724
+ }, [entities, onlyShowRowsWErrors, reduxFormCellValidation]);
2725
+
2726
+ const renderColumns = useColumns({
2727
+ addFilters,
2728
+ cellRenderer,
2729
+ columns,
2730
+ currentParams,
2731
+ compact,
2732
+ editingCellSelectAll,
2733
+ entities,
2734
+ expandedEntityIdMap,
2735
+ extraCompact,
2736
+ filters,
2737
+ formName,
2738
+ getCellHoverText,
2739
+ isCellEditable,
2740
+ isEntityDisabled,
2741
+ isLocalCall,
2742
+ isSimple,
2743
+ isSingleSelect,
2744
+ isSelectionARectangle,
2745
+ noDeselectAll,
2746
+ noSelect,
2747
+ noUserSelect,
2748
+ onDeselect,
2749
+ onMultiRowSelect,
2750
+ onRowClick,
2751
+ onRowSelect,
2752
+ onSingleRowSelect,
2753
+ order,
2754
+ primarySelectedCellId,
2755
+ reduxFormCellValidation,
2756
+ reduxFormSelectedEntityIdMap,
2757
+ refocusTable,
2758
+ removeSingleFilter,
2759
+ schema,
2760
+ selectedCells,
2761
+ setExpandedEntityIdMap,
2762
+ setNewParams,
2763
+ setOrder,
2764
+ setSelectedCells,
2765
+ shouldShowSubComponent,
2766
+ startCellEdit,
2767
+ SubComponent,
2768
+ tableRef,
2769
+ updateEntitiesHelper,
2770
+ updateValidation,
2771
+ withCheckboxes,
2772
+ withExpandAndCollapseAllButton,
2773
+ withFilter,
2774
+ withSort,
2775
+ recordIdToIsVisibleMap,
2776
+ setRecordIdToIsVisibleMap
2777
+ });
2778
+
1774
2779
  const scrollToTop = useCallback(
1775
2780
  () =>
1776
2781
  tableRef.current?.tableRef?.children?.[0]?.children?.[0]?.scrollIntoView(),
1777
2782
  []
1778
2783
  );
1779
2784
 
2785
+ const reactTable = useMemo(
2786
+ () => (
2787
+ <ReactTable
2788
+ data={filteredEnts}
2789
+ ref={tableRef}
2790
+ noVirtual={noVirtual}
2791
+ className={classNames({
2792
+ isCellEditable,
2793
+ "tg-table-loading": isLoading,
2794
+ "tg-table-disabled": disabled
2795
+ })}
2796
+ itemSizeEstimator={
2797
+ extraCompact
2798
+ ? itemSizeEstimators.compact
2799
+ : compact
2800
+ ? itemSizeEstimators.normal
2801
+ : itemSizeEstimators.comfortable
2802
+ }
2803
+ TfootComponent={() => {
2804
+ return <button>hasdfasdf</button>;
2805
+ }}
2806
+ // We should try to not give all the props to the render column
2807
+ columns={renderColumns}
2808
+ pageSize={rowsToShow}
2809
+ expanded={expandedRows}
2810
+ showPagination={false}
2811
+ sortable={false}
2812
+ loading={isLoading || disabled}
2813
+ defaultResized={resized}
2814
+ onResizedChange={(newResized = []) => {
2815
+ const resizedToUse = newResized.map(column => {
2816
+ // have a min width of 50 so that columns don't disappear
2817
+ if (column.value < 50) {
2818
+ return {
2819
+ ...column,
2820
+ value: 50
2821
+ };
2822
+ } else {
2823
+ return column;
2824
+ }
2825
+ });
2826
+ resizePersist(resizedToUse);
2827
+ }}
2828
+ TheadComponent={TheadComponent}
2829
+ ThComponent={ThComponent}
2830
+ getTrGroupProps={getTableRowProps}
2831
+ getTdProps={getTableCellProps}
2832
+ NoDataComponent={({ children }) =>
2833
+ isLoading ? null : (
2834
+ <div className="rt-noData">{noRowsFoundMessage || children}</div>
2835
+ )
2836
+ }
2837
+ LoadingComponent={({ loadingText, loading }) => (
2838
+ <DisabledLoadingComponent
2839
+ loading={loading}
2840
+ loadingText={loadingText}
2841
+ disabled={disabled}
2842
+ />
2843
+ )}
2844
+ style={{
2845
+ maxHeight,
2846
+ minHeight: 150,
2847
+ ...style
2848
+ }}
2849
+ SubComponent={SubComponentToUse}
2850
+ {...ReactTableProps}
2851
+ />
2852
+ ),
2853
+ [
2854
+ ReactTableProps,
2855
+ SubComponentToUse,
2856
+ TheadComponent,
2857
+ compact,
2858
+ disabled,
2859
+ expandedRows,
2860
+ extraCompact,
2861
+ filteredEnts,
2862
+ getTableCellProps,
2863
+ getTableRowProps,
2864
+ isCellEditable,
2865
+ isLoading,
2866
+ maxHeight,
2867
+ noRowsFoundMessage,
2868
+ renderColumns,
2869
+ resizePersist,
2870
+ resized,
2871
+ rowsToShow,
2872
+ style,
2873
+ noVirtual
2874
+ ]
2875
+ );
2876
+
1780
2877
  return (
1781
2878
  <div
1782
2879
  tabIndex="1"
@@ -2072,84 +3169,7 @@ const DataTable = ({
2072
3169
  />
2073
3170
  </div>
2074
3171
  )}
2075
- <ReactTable
2076
- addFilters={addFilters}
2077
- cellRenderer={cellRenderer}
2078
- change={change}
2079
- columns={columns}
2080
- compact={compact}
2081
- contextMenu={contextMenu}
2082
- currentParams={currentParams}
2083
- disabled={disabled}
2084
- doNotShowEmptyRows={doNotShowEmptyRows}
2085
- doNotValidateUntouchedRows={doNotValidateUntouchedRows}
2086
- editingCellSelectAll={editingCellSelectAll}
2087
- entities={entities}
2088
- expandedEntityIdMap={expandedEntityIdMap}
2089
- extraCompact={extraCompact}
2090
- filters={filters}
2091
- formName={formName}
2092
- getCellHoverText={getCellHoverText}
2093
- getRowClassName={getRowClassName}
2094
- handleCellClick={handleCellClick}
2095
- handleCopySelectedRows={handleCopySelectedRows}
2096
- history={history}
2097
- insertRows={insertRows}
2098
- isCellEditable={isCellEditable}
2099
- isCopyable={isCopyable}
2100
- isEntityDisabled={isEntityDisabled}
2101
- isLoading={isLoading}
2102
- isLocalCall={isLocalCall}
2103
- isSelectionARectangle={isSelectionARectangle}
2104
- isSimple={isSimple}
2105
- isSingleSelect={isSingleSelect}
2106
- maxHeight={maxHeight}
2107
- moveColumnPersist={moveColumnPersist}
2108
- mustClickCheckboxToSelect={mustClickCheckboxToSelect}
2109
- noDeselectAll={noDeselectAll}
2110
- noRowsFoundMessage={noRowsFoundMessage}
2111
- noSelect={noSelect}
2112
- noUserSelect={noUserSelect}
2113
- noVirtual={noVirtual}
2114
- numRows={numRows}
2115
- onDeselect={onDeselect}
2116
- onDoubleClick={onDoubleClick}
2117
- onlyShowRowsWErrors={onlyShowRowsWErrors}
2118
- onMultiRowSelect={onMultiRowSelect}
2119
- onRowClick={onRowClick}
2120
- onRowSelect={onRowSelect}
2121
- onSingleRowSelect={onSingleRowSelect}
2122
- order={order}
2123
- primarySelectedCellId={primarySelectedCellId}
2124
- ReactTableProps={ReactTableProps}
2125
- recordIdToIsVisibleMap={recordIdToIsVisibleMap}
2126
- reduxFormCellValidation={reduxFormCellValidation}
2127
- reduxFormEditingCell={reduxFormEditingCell}
2128
- reduxFormSelectedEntityIdMap={reduxFormSelectedEntityIdMap}
2129
- refocusTable={refocusTable}
2130
- removeSingleFilter={removeSingleFilter}
2131
- resizePersist={resizePersist}
2132
- schema={schema}
2133
- selectedCells={selectedCells}
2134
- setColumns={setColumns}
2135
- setExpandedEntityIdMap={setExpandedEntityIdMap}
2136
- setNewParams={setNewParams}
2137
- setOrder={setOrder}
2138
- setRecordIdToIsVisibleMap={setRecordIdToIsVisibleMap}
2139
- setSelectedCells={setSelectedCells}
2140
- shouldShowSubComponent={shouldShowSubComponent}
2141
- startCellEdit={startCellEdit}
2142
- style={style}
2143
- SubComponent={SubComponent}
2144
- tableConfig={tableConfig}
2145
- tableRef={tableRef}
2146
- updateEntitiesHelper={updateEntitiesHelper}
2147
- updateValidation={updateValidation}
2148
- withCheckboxes={withCheckboxes}
2149
- withExpandAndCollapseAllButton={withExpandAndCollapseAllButton}
2150
- withFilter={withFilter}
2151
- withSort={withSort}
2152
- />
3172
+ {reactTable}
2153
3173
  {isCellEditable && (
2154
3174
  <div style={{ display: "flex" }}>
2155
3175
  <div
@@ -2247,41 +3267,7 @@ const DataTable = ({
2247
3267
  );
2248
3268
  };
2249
3269
 
2250
- const WithRouterDatatable = withRouter(DataTable);
2251
-
2252
- const RouterDTWrapper = props => {
2253
- if (props.noRouter) {
2254
- return <DataTable {...props} />;
2255
- }
2256
- return <WithRouterDatatable {...props} />;
2257
- };
2258
-
2259
- const WithReduxFormDataTable = reduxForm({})(RouterDTWrapper);
2260
-
2261
- const ReduxFormDTWrapper = props => {
2262
- if (props.noForm) {
2263
- return <RouterDTWrapper {...props} form={props.formName} />;
2264
- }
2265
- return <WithReduxFormDataTable {...props} form={props.formName} />;
2266
- };
2267
-
2268
- const WithRouterPagingTool = withRouter(PagingTool);
2269
-
2270
- const RouterPTWrapper = props => {
2271
- if (props.noRouter) {
2272
- return <DataTable {...props} />;
2273
- }
2274
- return <WithRouterPagingTool {...props} />;
2275
- };
2276
-
2277
- const WithReduxFormPagingTool = reduxForm({})(RouterPTWrapper);
2278
-
2279
- const ConnectedPagingTool = props => {
2280
- if (props.noForm) {
2281
- return <RouterPTWrapper {...props} form={props.formName} />;
2282
- }
2283
- return <WithReduxFormPagingTool {...props} form={props.formName} />;
2284
- };
2285
-
2286
- export default ReduxFormDTWrapper;
3270
+ const WrappedDT = dataTableEnhancer(DataTable);
3271
+ export default WrappedDT;
3272
+ const ConnectedPagingTool = dataTableEnhancer(PagingTool);
2287
3273
  export { ConnectedPagingTool };