@teselagen/ui 0.9.8 → 0.10.3

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 (83) hide show
  1. package/CollapsibleCard/index.d.ts +2 -1
  2. package/DataTable/Columns.d.ts +4 -1
  3. package/DataTable/DisplayOptions.d.ts +2 -1
  4. package/DataTable/index.d.ts +3 -3
  5. package/DividerWithText/index.d.ts +16 -0
  6. package/index.cjs.js +21122 -21084
  7. package/index.d.ts +1 -0
  8. package/index.es.js +21125 -21087
  9. package/package.json +2 -1
  10. package/src/CollapsibleCard/index.js +8 -1
  11. package/src/DataTable/Columns.js +69 -1
  12. package/src/DataTable/DisplayOptions.js +35 -6
  13. package/src/DataTable/PagingTool.js +2 -2
  14. package/src/DataTable/RenderCell.js +8 -4
  15. package/src/DataTable/index.js +1253 -229
  16. package/src/DataTable/utils/tableQueryParamsToHasuraClauses.js +2 -2
  17. package/src/DataTable/utils/tableQueryParamsToHasuraClauses.test.js +21 -0
  18. package/src/DataTable/utils/useTableEntities.js +3 -2
  19. package/src/DataTable/utils/withTableParams.js +34 -13
  20. package/src/DividerWithText/index.js +30 -0
  21. package/src/DividerWithText/style.css +37 -0
  22. package/src/InfoHelper/style.css +1 -0
  23. package/src/UploadCsvWizard.js +7 -5
  24. package/src/index.js +1 -0
  25. package/src/utils/hooks/useDeepEqualMemo.js +2 -2
  26. package/src/utils/isEqualIgnoreFunctions.js +23 -0
  27. package/src/utils/pureNoFunc.js +4 -20
  28. package/ui.css +38 -0
  29. package/utils/isEqualIgnoreFunctions.d.ts +1 -0
  30. package/DataTable/EditabelCell.d.ts +0 -7
  31. package/DataTable/ReactTable.d.ts +0 -78
  32. package/DataTable/defaultProps.d.ts +0 -43
  33. package/DataTable/utils/computePresets.d.ts +0 -1
  34. package/DataTable/utils/types/Entity.d.ts +0 -9
  35. package/DataTable/utils/types/Field.d.ts +0 -4
  36. package/DataTable/utils/types/OrderBy.d.ts +0 -11
  37. package/DataTable/utils/types/Schema.d.ts +0 -4
  38. package/DataTable/utils/useDeepEqualMemo.d.ts +0 -1
  39. package/DataTable/utils/useHotKeysWrapper.d.ts +0 -29
  40. package/DataTable/utils/useTableParams.d.ts +0 -49
  41. package/src/DataTable/Columns.jsx +0 -945
  42. package/src/DataTable/EditabelCell.js +0 -44
  43. package/src/DataTable/EditabelCell.jsx +0 -44
  44. package/src/DataTable/ReactTable.js +0 -738
  45. package/src/DataTable/RenderCell.jsx +0 -191
  46. package/src/DataTable/defaultProps.js +0 -45
  47. package/src/DataTable/utils/computePresets.js +0 -42
  48. package/src/DataTable/utils/convertSchema.ts +0 -79
  49. package/src/DataTable/utils/formatPasteData.ts +0 -34
  50. package/src/DataTable/utils/getAllRows.ts +0 -11
  51. package/src/DataTable/utils/getCellCopyText.ts +0 -7
  52. package/src/DataTable/utils/getCellInfo.ts +0 -46
  53. package/src/DataTable/utils/getFieldPathToField.ts +0 -10
  54. package/src/DataTable/utils/getIdOrCodeOrIndex.ts +0 -14
  55. package/src/DataTable/utils/getLastSelectedEntity.ts +0 -15
  56. package/src/DataTable/utils/getNewEntToSelect.ts +0 -32
  57. package/src/DataTable/utils/initializeHasuraWhereAndFilter.ts +0 -35
  58. package/src/DataTable/utils/isBottomRightCornerOfRectangle.ts +0 -27
  59. package/src/DataTable/utils/isEntityClean.ts +0 -15
  60. package/src/DataTable/utils/primarySelectedValue.ts +0 -1
  61. package/src/DataTable/utils/removeCleanRows.ts +0 -26
  62. package/src/DataTable/utils/selection.ts +0 -11
  63. package/src/DataTable/utils/types/Entity.ts +0 -13
  64. package/src/DataTable/utils/types/Field.ts +0 -4
  65. package/src/DataTable/utils/types/OrderBy.ts +0 -15
  66. package/src/DataTable/utils/types/Schema.ts +0 -5
  67. package/src/DataTable/utils/useDeepEqualMemo.js +0 -10
  68. package/src/DataTable/utils/useHotKeysWrapper.js +0 -395
  69. package/src/DataTable/utils/useTableEntities.ts +0 -60
  70. package/src/DataTable/utils/useTableParams.js +0 -361
  71. package/src/DataTable/utils/utils.ts +0 -39
  72. package/src/Timeline/TimelineEvent.tsx +0 -36
  73. package/src/Timeline/index.tsx +0 -21
  74. package/src/utils/browserUtils.ts +0 -3
  75. package/src/utils/determineBlackOrWhiteTextColor.ts +0 -11
  76. package/src/utils/getTextFromEl.ts +0 -45
  77. package/src/utils/handlerHelpers.ts +0 -32
  78. package/src/utils/hooks/index.ts +0 -1
  79. package/src/utils/hooks/useDeepEqualMemo.ts +0 -10
  80. package/src/utils/hooks/useStableReference.ts +0 -9
  81. package/src/utils/hotkeyUtils.tsx +0 -155
  82. package/src/utils/isBeingCalledExcessively.ts +0 -37
  83. 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,21 +98,27 @@ 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,
89
119
  formName = "tgDataTable",
90
120
  history,
121
+ doNotSearchHiddenColumns,
91
122
  isSimple,
92
123
  isLocalCall = true,
93
124
  isTableParamsConnected,
@@ -143,15 +174,15 @@ const DataTable = ({
143
174
  reduxFormEntities,
144
175
  reduxFormQueryParams: _reduxFormQueryParams = {},
145
176
  reduxFormSelectedEntityIdMap: _reduxFormSelectedEntityIdMap = {}
146
- } = useSelector(state =>
147
- formValueSelector(formName)(
177
+ } = useSelector(function dtFormParamsSelector(state) {
178
+ return formValueSelector(formName)(
148
179
  state,
149
180
  "reduxFormCellValidation",
150
181
  "reduxFormEntities",
151
182
  "reduxFormQueryParams",
152
183
  "reduxFormSelectedEntityIdMap"
153
- )
154
- );
184
+ );
185
+ });
155
186
 
156
187
  // We want to make sure we don't rerender everything unnecessary
157
188
  // with redux-forms we tend to do unnecessary renders
@@ -376,7 +407,7 @@ const DataTable = ({
376
407
  expandAllByDefault,
377
408
  extraClasses = "",
378
409
  extraCompact: _extraCompact,
379
- filters,
410
+ filters = [],
380
411
  fragment,
381
412
  getCellHoverText,
382
413
  getRowClassName,
@@ -451,15 +482,16 @@ const DataTable = ({
451
482
  withSelectAll,
452
483
  withSort,
453
484
  withTitle = !isSimple,
454
- noExcessiveCheck
485
+ noExcessiveCheck,
486
+ isEntityCountLoading
455
487
  } = props;
456
488
 
457
489
  const _entities = useMemo(
458
490
  () => (reduxFormEntities?.length ? reduxFormEntities : _origEntities) || [],
459
491
  [_origEntities, reduxFormEntities]
460
492
  );
461
-
462
493
  const entities = useDeepEqualMemo(_entities);
494
+
463
495
  const entitiesAcrossPages = useDeepEqualMemo(_entitiesAcrossPages);
464
496
 
465
497
  // This is because we need to maintain the reduxFormSelectedEntityIdMap and
@@ -474,16 +506,34 @@ const DataTable = ({
474
506
  entities,
475
507
  change
476
508
  });
477
- // eslint-disable-next-line react-hooks/exhaustive-deps
478
509
  }, [
479
510
  entitiesAcrossPages,
480
511
  reduxFormSelectedEntityIdMap,
481
512
  change,
482
- noExcessiveCheck
513
+ noExcessiveCheck,
514
+ entities,
515
+ formName
483
516
  ]);
484
517
 
485
- const [tableConfig, setTableConfig] = useState({ fieldOptions: [] });
486
-
518
+ const [tableConfig, _setTableConfig] = useState({ fieldOptions: [] });
519
+ const setTableConfig = useCallback(
520
+ newConfig => {
521
+ _setTableConfig(prev => {
522
+ let newConfigVal = newConfig;
523
+ if (typeof newConfig === "function") {
524
+ newConfigVal = newConfig(prev);
525
+ }
526
+ if (!isEqual(prev.fieldOptions, newConfigVal.fieldOptions)) {
527
+ change(
528
+ "reduxFormReadOnlyFieldOptions",
529
+ newConfigVal.fieldOptions || []
530
+ );
531
+ }
532
+ return newConfigVal;
533
+ });
534
+ },
535
+ [change]
536
+ );
487
537
  useEffect(() => {
488
538
  if (withDisplayOptions) {
489
539
  let newTableConfig = {};
@@ -632,6 +682,7 @@ const DataTable = ({
632
682
  convertedSchema,
633
683
  currentParams,
634
684
  entities,
685
+ setTableConfig,
635
686
  history,
636
687
  isInfinite,
637
688
  isOpenable,
@@ -749,6 +800,7 @@ const DataTable = ({
749
800
  currentUser?.user?.id,
750
801
  deleteTableConfiguration,
751
802
  formName,
803
+ setTableConfig,
752
804
  schema.fields,
753
805
  syncDisplayOptionsToDb,
754
806
  tableConfig,
@@ -765,6 +817,11 @@ const DataTable = ({
765
817
  extraCompact = tableConfig.density === "extraCompact";
766
818
  }
767
819
 
820
+ const resized = useMemo(
821
+ () => tableConfig.resized || [],
822
+ [tableConfig?.resized]
823
+ );
824
+
768
825
  const pageSize = controlled_pageSize || _pageSize;
769
826
 
770
827
  const [expandedEntityIdMap, setExpandedEntityIdMap] = useState(() => {
@@ -849,85 +906,189 @@ const DataTable = ({
849
906
  [schema.fields]
850
907
  );
851
908
 
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
- };
909
+ const updateValidationHelper = useCallback(() => {
910
+ updateValidation(entities, reduxFormCellValidation);
911
+ }, [entities, reduxFormCellValidation, updateValidation]);
859
912
 
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
- }));
913
+ const addEditableTableEntities = useCallback(
914
+ incomingEnts => {
915
+ updateEntitiesHelper(entities, entities => {
916
+ const newEntities = incomingEnts.map(e => ({
917
+ ...e,
918
+ id: e.id || nanoid(),
919
+ _isClean: false
920
+ }));
867
921
 
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);
922
+ const { newEnts, validationErrors } = formatAndValidateEntities(
923
+ newEntities,
924
+ {
925
+ useDefaultValues: true,
926
+ indexToStartAt: entities.length
881
927
  }
882
-
883
- updateValidation(entities, {
884
- ...reduxFormCellValidation,
885
- ...validationErrors
928
+ );
929
+ if (every(entities, "_isClean")) {
930
+ forEach(newEnts, (e, i) => {
931
+ entities[i] = e;
886
932
  });
887
- });
888
- };
933
+ } else {
934
+ entities.splice(entities.length, 0, ...newEnts);
935
+ }
889
936
 
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
937
+ updateValidation(entities, {
938
+ ...reduxFormCellValidation,
939
+ ...validationErrors
899
940
  });
941
+ });
942
+ },
943
+ [
944
+ entities,
945
+ formatAndValidateEntities,
946
+ reduxFormCellValidation,
947
+ updateEntitiesHelper,
948
+ updateValidation
949
+ ]
950
+ );
900
951
 
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
- }
952
+ const getEditableTableInfoAndThrowFormError = useCallback(() => {
953
+ const { entsToUse, validationToUse } = removeCleanRows(
954
+ reduxFormEntities,
955
+ reduxFormCellValidation
956
+ );
957
+ const validationWTableErrs = validateTableWideErrors({
958
+ entities: entsToUse,
959
+ schema,
960
+ newCellValidate: validationToUse
961
+ });
916
962
 
917
- return entsToUse;
918
- };
963
+ if (!entsToUse?.length) {
964
+ throwFormError(
965
+ "Please add at least one row to the table before submitting."
966
+ );
967
+ }
968
+ const invalid =
969
+ isEmpty(validationWTableErrs) || !some(validationWTableErrs, v => v)
970
+ ? undefined
971
+ : validationWTableErrs;
972
+
973
+ if (invalid) {
974
+ throwFormError("Please fix the errors in the table before submitting.");
975
+ }
976
+
977
+ return entsToUse;
978
+ }, [reduxFormCellValidation, reduxFormEntities, schema]);
979
+
980
+ useEffect(() => {
981
+ // This is bad practice, we shouldn't be assigning value to an
982
+ // external variable
983
+ if (helperProp) {
984
+ helperProp.updateValidationHelper = updateValidationHelper;
985
+ helperProp.addEditableTableEntities = addEditableTableEntities;
986
+ helperProp.getEditableTableInfoAndThrowFormError =
987
+ getEditableTableInfoAndThrowFormError;
919
988
  }
920
989
  }, [
921
- entities,
922
- formatAndValidateEntities,
990
+ addEditableTableEntities,
991
+ getEditableTableInfoAndThrowFormError,
923
992
  helperProp,
924
- reduxFormCellValidation,
925
- reduxFormEntities,
926
- schema,
927
- updateEntitiesHelper,
928
- updateValidation
993
+ updateValidationHelper
929
994
  ]);
930
995
 
996
+ const handleRowMove = useCallback(
997
+ (type, shiftHeld) => e => {
998
+ e.preventDefault();
999
+ e.stopPropagation();
1000
+ let newIdMap = {};
1001
+ const lastSelectedEnt = getLastSelectedEntity(
1002
+ reduxFormSelectedEntityIdMap
1003
+ );
1004
+
1005
+ if (noSelect) return;
1006
+ if (lastSelectedEnt) {
1007
+ let lastSelectedIndex = entities.findIndex(
1008
+ ent => ent === lastSelectedEnt
1009
+ );
1010
+ if (lastSelectedIndex === -1) {
1011
+ if (lastSelectedEnt.id !== undefined) {
1012
+ lastSelectedIndex = entities.findIndex(
1013
+ ent => ent.id === lastSelectedEnt.id
1014
+ );
1015
+ } else if (lastSelectedEnt.code !== undefined) {
1016
+ lastSelectedIndex = entities.findIndex(
1017
+ ent => ent.code === lastSelectedEnt.code
1018
+ );
1019
+ }
1020
+ }
1021
+ if (lastSelectedIndex === -1) {
1022
+ return;
1023
+ }
1024
+ const newEntToSelect = getNewEntToSelect({
1025
+ type,
1026
+ lastSelectedIndex,
1027
+ entities,
1028
+ isEntityDisabled
1029
+ });
1030
+
1031
+ if (!newEntToSelect) return;
1032
+ if (shiftHeld && !isSingleSelect) {
1033
+ if (
1034
+ reduxFormSelectedEntityIdMap[
1035
+ newEntToSelect.id || newEntToSelect.code
1036
+ ]
1037
+ ) {
1038
+ //the entity being moved to has already been selected
1039
+ newIdMap = omit(reduxFormSelectedEntityIdMap, [
1040
+ lastSelectedEnt.id || lastSelectedEnt.code
1041
+ ]);
1042
+ newIdMap[newEntToSelect.id || newEntToSelect.code].time =
1043
+ Date.now() + 1;
1044
+ } else {
1045
+ //the entity being moved to has NOT been selected yet
1046
+ newIdMap = {
1047
+ ...reduxFormSelectedEntityIdMap,
1048
+ [newEntToSelect.id || newEntToSelect.code]: {
1049
+ entity: newEntToSelect,
1050
+ time: Date.now()
1051
+ }
1052
+ };
1053
+ }
1054
+ } else {
1055
+ //no shiftHeld
1056
+ newIdMap[newEntToSelect.id || newEntToSelect.code] = {
1057
+ entity: newEntToSelect,
1058
+ time: Date.now()
1059
+ };
1060
+ }
1061
+ }
1062
+
1063
+ finalizeSelection({
1064
+ idMap: newIdMap,
1065
+ entities,
1066
+ props: {
1067
+ onDeselect,
1068
+ onSingleRowSelect,
1069
+ onMultiRowSelect,
1070
+ noDeselectAll,
1071
+ onRowSelect,
1072
+ noSelect,
1073
+ change
1074
+ }
1075
+ });
1076
+ },
1077
+ [
1078
+ change,
1079
+ entities,
1080
+ isEntityDisabled,
1081
+ isSingleSelect,
1082
+ noDeselectAll,
1083
+ noSelect,
1084
+ onDeselect,
1085
+ onMultiRowSelect,
1086
+ onRowSelect,
1087
+ onSingleRowSelect,
1088
+ reduxFormSelectedEntityIdMap
1089
+ ]
1090
+ );
1091
+
931
1092
  const primarySelectedCellId = useMemo(() => {
932
1093
  for (const k of Object.keys(selectedCells)) {
933
1094
  if (selectedCells[k] === PRIMARY_SELECTED_VAL) {
@@ -963,6 +1124,50 @@ const DataTable = ({
963
1124
  [change]
964
1125
  );
965
1126
 
1127
+ const handleEnterStartCellEdit = useCallback(
1128
+ e => {
1129
+ e.stopPropagation();
1130
+ startCellEdit(primarySelectedCellId);
1131
+ },
1132
+ [primarySelectedCellId, startCellEdit]
1133
+ );
1134
+
1135
+ const handleDeleteCell = useCallback(() => {
1136
+ const newCellValidate = {
1137
+ ...reduxFormCellValidation
1138
+ };
1139
+ if (isEmpty(selectedCells)) return;
1140
+ const rowIds = [];
1141
+ updateEntitiesHelper(entities, entities => {
1142
+ const entityIdToEntity = getEntityIdToEntity(entities);
1143
+ Object.keys(selectedCells).forEach(cellId => {
1144
+ const [rowId, path] = cellId.split(":");
1145
+ rowIds.push(rowId);
1146
+ const entity = entityIdToEntity[rowId].e;
1147
+ delete entity._isClean;
1148
+ const { error } = editCellHelper({
1149
+ entity,
1150
+ path,
1151
+ schema,
1152
+ newVal: ""
1153
+ });
1154
+ if (error) {
1155
+ newCellValidate[cellId] = error;
1156
+ } else {
1157
+ delete newCellValidate[cellId];
1158
+ }
1159
+ });
1160
+ updateValidation(entities, newCellValidate);
1161
+ });
1162
+ }, [
1163
+ entities,
1164
+ reduxFormCellValidation,
1165
+ schema,
1166
+ selectedCells,
1167
+ updateEntitiesHelper,
1168
+ updateValidation
1169
+ ]);
1170
+
966
1171
  const waitUntilAllRowsAreRendered = useCallback(() => {
967
1172
  return new Promise(resolve => {
968
1173
  const interval = setInterval(() => {
@@ -977,6 +1182,74 @@ const DataTable = ({
977
1182
  // eslint-disable-next-line react-hooks/exhaustive-deps
978
1183
  }, []);
979
1184
 
1185
+ const handleCopySelectedCells = useCallback(async () => {
1186
+ // if the current selection is consecutive cells then copy with
1187
+ // tabs between. if not then just select primary selected cell
1188
+ if (isEmpty(selectedCells)) return;
1189
+
1190
+ // Temporarily disable virtualization for large tables
1191
+ if (entities.length > VIRTUALIZE_CUTOFF_LENGTH) {
1192
+ setNoVirtual(true);
1193
+ await waitUntilAllRowsAreRendered();
1194
+ }
1195
+
1196
+ const pathToIndex = getFieldPathToIndex(schema);
1197
+ const entityIdToEntity = getEntityIdToEntity(entities);
1198
+ const selectionGrid = [];
1199
+ let firstRowIndex;
1200
+ let firstCellIndex;
1201
+ Object.keys(selectedCells).forEach(key => {
1202
+ const [rowId, path] = key.split(":");
1203
+ const eInfo = entityIdToEntity[rowId];
1204
+ if (eInfo) {
1205
+ if (firstRowIndex === undefined || eInfo.i < firstRowIndex) {
1206
+ firstRowIndex = eInfo.i;
1207
+ }
1208
+ if (!selectionGrid[eInfo.i]) {
1209
+ selectionGrid[eInfo.i] = [];
1210
+ }
1211
+ const cellIndex = pathToIndex[path];
1212
+ if (firstCellIndex === undefined || cellIndex < firstCellIndex) {
1213
+ firstCellIndex = cellIndex;
1214
+ }
1215
+ selectionGrid[eInfo.i][cellIndex] = true;
1216
+ }
1217
+ });
1218
+ if (firstRowIndex === undefined) return;
1219
+ const allRows = getAllRows(tableRef);
1220
+ let fullCellText = "";
1221
+ const fullJson = [];
1222
+ times(selectionGrid.length, i => {
1223
+ const row = selectionGrid[i];
1224
+ if (fullCellText) {
1225
+ fullCellText += "\n";
1226
+ }
1227
+ if (!row) {
1228
+ return;
1229
+ } else {
1230
+ const jsonRow = [];
1231
+ // ignore header
1232
+ let [rowCopyText, json] = getRowCopyText(allRows[i + 1]);
1233
+ rowCopyText = rowCopyText.split("\t");
1234
+ times(row.length, i => {
1235
+ const cell = row[i];
1236
+ if (cell) {
1237
+ fullCellText += rowCopyText[i];
1238
+ jsonRow.push(json[i]);
1239
+ }
1240
+ if (i !== row.length - 1 && i >= firstCellIndex) fullCellText += "\t";
1241
+ });
1242
+ fullJson.push(jsonRow);
1243
+ }
1244
+ });
1245
+ if (!fullCellText) return window.toastr.warning("No text to copy");
1246
+
1247
+ handleCopyHelper(fullCellText, fullJson, "Selected cells copied");
1248
+
1249
+ // Re-enable virtualization if it was disabled
1250
+ setNoVirtual(false);
1251
+ }, [entities, selectedCells, schema, waitUntilAllRowsAreRendered]);
1252
+
980
1253
  const handleCopySelectedRows = useCallback(
981
1254
  async selectedRecords => {
982
1255
  if (entities.length > VIRTUALIZE_CUTOFF_LENGTH) {
@@ -1019,36 +1292,237 @@ const DataTable = ({
1019
1292
  [entities, waitUntilAllRowsAreRendered]
1020
1293
  );
1021
1294
 
1022
- const { handleKeyDown, handleKeyUp } = useHotKeysWrapper({
1295
+ const handleCopyHotkey = useCallback(
1296
+ e => {
1297
+ if (isCellEditable) {
1298
+ handleCopySelectedCells(e);
1299
+ } else {
1300
+ handleCopySelectedRows(
1301
+ getRecordsFromIdMap(reduxFormSelectedEntityIdMap),
1302
+ e
1303
+ );
1304
+ }
1305
+ },
1306
+ [
1307
+ handleCopySelectedCells,
1308
+ handleCopySelectedRows,
1309
+ isCellEditable,
1310
+ reduxFormSelectedEntityIdMap
1311
+ ]
1312
+ );
1313
+
1314
+ const handleCut = useCallback(
1315
+ e => {
1316
+ handleDeleteCell();
1317
+ handleCopyHotkey(e);
1318
+ },
1319
+ [handleCopyHotkey, handleDeleteCell]
1320
+ );
1321
+
1322
+ const flashTableBorder = () => {
1323
+ try {
1324
+ const table = tableRef.current.tableRef;
1325
+ table.classList.add("tgBorderBlue");
1326
+ setTimeout(() => {
1327
+ table.classList.remove("tgBorderBlue");
1328
+ }, 300);
1329
+ } catch (e) {
1330
+ console.error(`err when flashing table border:`, e);
1331
+ }
1332
+ };
1333
+
1334
+ const handleUndo = useCallback(() => {
1335
+ if (entitiesUndoRedoStack.currentVersion > 0) {
1336
+ flashTableBorder();
1337
+ const nextState = applyPatches(
1338
+ entities,
1339
+ entitiesUndoRedoStack[entitiesUndoRedoStack.currentVersion]
1340
+ .inversePatches
1341
+ );
1342
+ const { newEnts, validationErrors } =
1343
+ formatAndValidateEntities(nextState);
1344
+ setEntitiesUndoRedoStack(prev => ({
1345
+ ...prev,
1346
+ currentVersion: prev.currentVersion - 1
1347
+ }));
1348
+ updateValidation(newEnts, validationErrors);
1349
+ change("reduxFormEntities", newEnts);
1350
+ }
1351
+ }, [
1023
1352
  change,
1024
1353
  entities,
1354
+ formatAndValidateEntities,
1025
1355
  entitiesUndoRedoStack,
1356
+ updateValidation
1357
+ ]);
1358
+
1359
+ const handleRedo = useCallback(() => {
1360
+ const nextV = entitiesUndoRedoStack.currentVersion + 1;
1361
+ if (entitiesUndoRedoStack[nextV]) {
1362
+ flashTableBorder();
1363
+ const nextState = applyPatches(
1364
+ entities,
1365
+ entitiesUndoRedoStack[nextV].patches
1366
+ );
1367
+ const { newEnts, validationErrors } =
1368
+ formatAndValidateEntities(nextState);
1369
+ change("reduxFormEntities", newEnts);
1370
+ updateValidation(newEnts, validationErrors);
1371
+ setEntitiesUndoRedoStack(prev => ({
1372
+ ...prev,
1373
+ currentVersion: nextV
1374
+ }));
1375
+ }
1376
+ }, [
1377
+ change,
1378
+ entities,
1026
1379
  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
- });
1380
+ entitiesUndoRedoStack,
1381
+ updateValidation
1382
+ ]);
1383
+
1384
+ const handleSelectAllRows = useCallback(
1385
+ e => {
1386
+ if (isSingleSelect) return;
1387
+ e.preventDefault();
1388
+
1389
+ if (isCellEditable) {
1390
+ const schemaPaths = schema.fields.map(f => f.path);
1391
+ const newSelectedCells = {};
1392
+ entities.forEach((entity, i) => {
1393
+ if (isEntityDisabled(entity)) return;
1394
+ const entityId = getIdOrCodeOrIndex(entity, i);
1395
+ schemaPaths.forEach(p => {
1396
+ newSelectedCells[`${entityId}:${p}`] = true;
1397
+ });
1398
+ });
1399
+ setSelectedCells(newSelectedCells);
1400
+ } else {
1401
+ const newIdMap = {};
1402
+
1403
+ entities.forEach((entity, i) => {
1404
+ if (isEntityDisabled(entity)) return;
1405
+ const entityId = getIdOrCodeOrIndex(entity, i);
1406
+ newIdMap[entityId] = { entity };
1407
+ });
1408
+ finalizeSelection({
1409
+ idMap: newIdMap,
1410
+ entities,
1411
+ props: {
1412
+ onDeselect,
1413
+ onSingleRowSelect,
1414
+ onMultiRowSelect,
1415
+ noDeselectAll,
1416
+ onRowSelect,
1417
+ noSelect,
1418
+ change
1419
+ }
1420
+ });
1421
+ }
1422
+ },
1423
+ [
1424
+ change,
1425
+ entities,
1426
+ isCellEditable,
1427
+ isEntityDisabled,
1428
+ isSingleSelect,
1429
+ noDeselectAll,
1430
+ noSelect,
1431
+ onDeselect,
1432
+ onMultiRowSelect,
1433
+ onRowSelect,
1434
+ onSingleRowSelect,
1435
+ schema.fields
1436
+ ]
1437
+ );
1051
1438
 
1439
+ const hotKeys = useMemo(
1440
+ () => [
1441
+ {
1442
+ global: false,
1443
+ combo: "up",
1444
+ label: "Move Up a Row",
1445
+ onKeyDown: handleRowMove("up")
1446
+ },
1447
+ {
1448
+ global: false,
1449
+ combo: "down",
1450
+ label: "Move Down a Row",
1451
+ onKeyDown: handleRowMove("down")
1452
+ },
1453
+ {
1454
+ global: false,
1455
+ combo: "up+shift",
1456
+ label: "Move Up a Row",
1457
+ onKeyDown: handleRowMove("up", true)
1458
+ },
1459
+ ...(isCellEditable
1460
+ ? [
1461
+ {
1462
+ global: false,
1463
+ combo: "enter",
1464
+ label: "Enter -> Start Cell Edit",
1465
+ onKeyDown: handleEnterStartCellEdit
1466
+ },
1467
+ {
1468
+ global: false,
1469
+ combo: "mod+x",
1470
+ label: "Cut",
1471
+ onKeyDown: handleCut
1472
+ },
1473
+ {
1474
+ global: false,
1475
+ combo: IS_LINUX ? "alt+z" : "mod+z",
1476
+ label: "Undo",
1477
+ onKeyDown: handleUndo
1478
+ },
1479
+ {
1480
+ global: false,
1481
+ combo: IS_LINUX ? "alt+shift+z" : "mod+shift+z",
1482
+ label: "Redo",
1483
+ onKeyDown: handleRedo
1484
+ },
1485
+ {
1486
+ global: false,
1487
+ combo: "backspace",
1488
+ label: "Delete Cell",
1489
+ onKeyDown: handleDeleteCell
1490
+ }
1491
+ ]
1492
+ : []),
1493
+ {
1494
+ global: false,
1495
+ combo: "down+shift",
1496
+ label: "Move Down a Row",
1497
+ onKeyDown: handleRowMove("down", true)
1498
+ },
1499
+ {
1500
+ global: false,
1501
+ combo: "mod + c",
1502
+ label: "Copy rows",
1503
+ onKeyDown: handleCopyHotkey
1504
+ },
1505
+ {
1506
+ global: false,
1507
+ combo: "mod + a",
1508
+ label: "Select rows",
1509
+ onKeyDown: handleSelectAllRows
1510
+ }
1511
+ ],
1512
+ [
1513
+ handleCopyHotkey,
1514
+ handleCut,
1515
+ handleDeleteCell,
1516
+ handleEnterStartCellEdit,
1517
+ handleRedo,
1518
+ handleRowMove,
1519
+ handleSelectAllRows,
1520
+ handleUndo,
1521
+ isCellEditable
1522
+ ]
1523
+ );
1524
+
1525
+ const { handleKeyDown, handleKeyUp } = useHotkeys(hotKeys);
1052
1526
  const [columns, setColumns] = useState([]);
1053
1527
  const [fullscreen, setFullscreen] = useState(false);
1054
1528
  const [selectingAll, setSelectingAll] = useState(false);
@@ -1352,6 +1826,42 @@ const DataTable = ({
1352
1826
  }
1353
1827
  }, [entities, selectedIds, selectAllByDefault, setSelectedIds]);
1354
1828
 
1829
+ const TheadComponent = useCallback(
1830
+ ({ className, style, children }) => {
1831
+ const moveColumn = ({ oldIndex, newIndex }) => {
1832
+ let oldStateColumnIndex, newStateColumnIndex;
1833
+ columns.forEach((column, i) => {
1834
+ if (oldIndex === column.columnIndex) oldStateColumnIndex = i;
1835
+ if (newIndex === column.columnIndex) newStateColumnIndex = i;
1836
+ });
1837
+ // because it is all handled in state we need
1838
+ // to perform the move and update the columnIndices
1839
+ // because they are used for the sortable columns
1840
+ const newColumns = arrayMove(
1841
+ columns,
1842
+ oldStateColumnIndex,
1843
+ newStateColumnIndex
1844
+ ).map((column, i) => {
1845
+ return {
1846
+ ...column,
1847
+ columnIndex: i
1848
+ };
1849
+ });
1850
+ setColumns(newColumns);
1851
+ };
1852
+ return (
1853
+ <SortableColumns
1854
+ className={className}
1855
+ style={style}
1856
+ moveColumn={moveColumnPersist || moveColumn}
1857
+ >
1858
+ {children}
1859
+ </SortableColumns>
1860
+ );
1861
+ },
1862
+ [columns, moveColumnPersist]
1863
+ );
1864
+
1355
1865
  const addEntitiesToSelection = entities => {
1356
1866
  const idMap = reduxFormSelectedEntityIdMap || {};
1357
1867
  const newIdMap = cloneDeep(idMap) || {};
@@ -1588,6 +2098,426 @@ const DataTable = ({
1588
2098
  ]
1589
2099
  );
1590
2100
 
2101
+ const showContextMenu = useCallback(
2102
+ (e, { idMap, selectedCells } = {}) => {
2103
+ let selectedRecords;
2104
+ if (isCellEditable) {
2105
+ const rowIds = {};
2106
+ Object.keys(selectedCells).forEach(cellKey => {
2107
+ const [rowId] = cellKey.split(":");
2108
+ rowIds[rowId] = true;
2109
+ });
2110
+ selectedRecords = entities.filter(
2111
+ ent => rowIds[getIdOrCodeOrIndex(ent)]
2112
+ );
2113
+ } else {
2114
+ selectedRecords = getRecordsFromIdMap(idMap);
2115
+ }
2116
+
2117
+ const itemsToRender = contextMenu({
2118
+ selectedRecords,
2119
+ history
2120
+ });
2121
+ if (!itemsToRender && !isCopyable) return null;
2122
+ const copyMenuItems = [];
2123
+
2124
+ e.persist();
2125
+ if (isCopyable) {
2126
+ //compute the cellWrapper here so we don't lose access to it
2127
+ const cellWrapper =
2128
+ e.target.querySelector(".tg-cell-wrapper") ||
2129
+ e.target.closest(".tg-cell-wrapper");
2130
+ if (cellWrapper) {
2131
+ copyMenuItems.push(
2132
+ <MenuItem
2133
+ key="copyCell"
2134
+ onClick={() => {
2135
+ //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
2136
+ //do we need to be able to copy hidden cells? It seems like it should just copy what's on the page..?
2137
+ const specificColumn = cellWrapper.getAttribute("data-test");
2138
+ handleCopyRows([cellWrapper.closest(".rt-tr")], {
2139
+ specificColumn,
2140
+ onFinishMsg: "Cell copied"
2141
+ });
2142
+ const [text, jsonText] = getCellCopyText(cellWrapper);
2143
+ handleCopyHelper(text, jsonText);
2144
+ }}
2145
+ text="Cell"
2146
+ />
2147
+ );
2148
+
2149
+ copyMenuItems.push(
2150
+ <MenuItem
2151
+ key="copyColumn"
2152
+ onClick={() => {
2153
+ handleCopyColumn(tableRef, cellWrapper);
2154
+ }}
2155
+ text="Column"
2156
+ />
2157
+ );
2158
+ if (selectedRecords.length > 1) {
2159
+ copyMenuItems.push(
2160
+ <MenuItem
2161
+ key="copyColumnSelected"
2162
+ onClick={() => {
2163
+ handleCopyColumn(tableRef, cellWrapper, selectedRecords);
2164
+ }}
2165
+ text="Column (Selected)"
2166
+ />
2167
+ );
2168
+ }
2169
+ }
2170
+ if (selectedRecords.length === 0 || selectedRecords.length === 1) {
2171
+ //compute the row here so we don't lose access to it
2172
+ const cell =
2173
+ e.target.querySelector(".tg-cell-wrapper") ||
2174
+ e.target.closest(".tg-cell-wrapper") ||
2175
+ e.target.closest(".rt-td");
2176
+ const row = cell.closest(".rt-tr");
2177
+ copyMenuItems.push(
2178
+ <MenuItem
2179
+ key="copySelectedRows"
2180
+ onClick={() => {
2181
+ handleCopyRows([row]);
2182
+ // loop through each cell in the row
2183
+ }}
2184
+ text="Row"
2185
+ />
2186
+ );
2187
+ } else if (selectedRecords.length > 1) {
2188
+ copyMenuItems.push(
2189
+ <MenuItem
2190
+ key="copySelectedRows"
2191
+ onClick={() => {
2192
+ handleCopySelectedRows(selectedRecords, e);
2193
+ // loop through each cell in the row
2194
+ }}
2195
+ text="Rows"
2196
+ />
2197
+ );
2198
+ }
2199
+ copyMenuItems.push(
2200
+ <MenuItem
2201
+ key="copyFullTableRows"
2202
+ onClick={() => {
2203
+ handleCopyTable(tableRef);
2204
+ // loop through each cell in the row
2205
+ }}
2206
+ text="Table"
2207
+ />
2208
+ );
2209
+ }
2210
+ const selectedRowIds = Object.keys(selectedCells).map(cellId => {
2211
+ const [rowId] = cellId.split(":");
2212
+ return rowId;
2213
+ });
2214
+
2215
+ const menu = (
2216
+ <Menu>
2217
+ {itemsToRender}
2218
+ {copyMenuItems.length && (
2219
+ <MenuItem icon="clipboard" key="copyOpts" text="Copy">
2220
+ {copyMenuItems}
2221
+ </MenuItem>
2222
+ )}
2223
+ {isCellEditable && (
2224
+ <>
2225
+ <MenuItem
2226
+ icon="add-row-top"
2227
+ text="Add Row Above"
2228
+ key="addRowAbove"
2229
+ onClick={() => {
2230
+ insertRows({ above: true });
2231
+ }}
2232
+ />
2233
+ <MenuItem
2234
+ icon="add-row-top"
2235
+ text="Add Row Below"
2236
+ key="addRowBelow"
2237
+ onClick={() => {
2238
+ insertRows({});
2239
+ }}
2240
+ />
2241
+ <MenuItem
2242
+ icon="remove"
2243
+ text={`Remove Row${selectedRowIds.length > 1 ? "s" : ""}`}
2244
+ key="removeRow"
2245
+ onClick={() => {
2246
+ const selectedRowIds = Object.keys(selectedCells).map(
2247
+ cellId => {
2248
+ const [rowId] = cellId.split(":");
2249
+ return rowId;
2250
+ }
2251
+ );
2252
+ updateEntitiesHelper(entities, entities => {
2253
+ const ents = entities.filter(
2254
+ (e, i) =>
2255
+ !selectedRowIds.includes(getIdOrCodeOrIndex(e, i))
2256
+ );
2257
+ updateValidation(
2258
+ ents,
2259
+ omitBy(reduxFormCellValidation, (v, cellId) =>
2260
+ selectedRowIds.includes(cellId.split(":")[0])
2261
+ )
2262
+ );
2263
+ return ents;
2264
+ });
2265
+ refocusTable();
2266
+ }}
2267
+ />
2268
+ </>
2269
+ )}
2270
+ </Menu>
2271
+ );
2272
+ ContextMenu.show(menu, { left: e.clientX, top: e.clientY });
2273
+ },
2274
+ [
2275
+ contextMenu,
2276
+ entities,
2277
+ handleCopySelectedRows,
2278
+ history,
2279
+ insertRows,
2280
+ isCellEditable,
2281
+ isCopyable,
2282
+ reduxFormCellValidation,
2283
+ refocusTable,
2284
+ updateEntitiesHelper,
2285
+ updateValidation
2286
+ ]
2287
+ );
2288
+
2289
+ const getTableRowProps = useCallback(
2290
+ (state, rowInfo) => {
2291
+ if (!rowInfo) {
2292
+ return {
2293
+ className: "no-row-data"
2294
+ };
2295
+ }
2296
+ const entity = rowInfo.original;
2297
+ const rowId = getIdOrCodeOrIndex(entity, rowInfo.index);
2298
+ const rowSelected = reduxFormSelectedEntityIdMap[rowId];
2299
+ const isExpanded = expandedEntityIdMap[rowId];
2300
+ const rowDisabled = isEntityDisabled(entity);
2301
+ const dataId = entity.id || entity.code;
2302
+ return {
2303
+ onClick: e => {
2304
+ if (isCellEditable) return;
2305
+ // if checkboxes are activated or row expander is clicked don't select row
2306
+ if (e.target.matches(".tg-expander, .tg-expander *")) {
2307
+ setExpandedEntityIdMap(prev => ({ ...prev, [rowId]: !isExpanded }));
2308
+ return;
2309
+ } else if (
2310
+ e.target.closest(".tg-react-table-checkbox-cell-container")
2311
+ ) {
2312
+ return;
2313
+ } else if (mustClickCheckboxToSelect) {
2314
+ return;
2315
+ }
2316
+ if (e.detail > 1) {
2317
+ return; //cancel multiple quick clicks
2318
+ }
2319
+ rowClick(e, rowInfo, entities, {
2320
+ reduxFormSelectedEntityIdMap,
2321
+ isSingleSelect,
2322
+ noSelect,
2323
+ onRowClick,
2324
+ isEntityDisabled,
2325
+ withCheckboxes,
2326
+ onDeselect,
2327
+ onSingleRowSelect,
2328
+ onMultiRowSelect,
2329
+ noDeselectAll,
2330
+ onRowSelect,
2331
+ change
2332
+ });
2333
+ },
2334
+ //row right click
2335
+ onContextMenu: e => {
2336
+ e.preventDefault();
2337
+ if (rowId === undefined || rowDisabled || isCellEditable) return;
2338
+ const oldIdMap = cloneDeep(reduxFormSelectedEntityIdMap) || {};
2339
+ let newIdMap;
2340
+ if (withCheckboxes) {
2341
+ newIdMap = oldIdMap;
2342
+ } else {
2343
+ // if we are not using checkboxes we need to make sure
2344
+ // that the id of the record gets added to the id map
2345
+ newIdMap = oldIdMap[rowId] ? oldIdMap : { [rowId]: { entity } };
2346
+
2347
+ // tgreen: this will refresh the selection with fresh data. The entities in redux might not be up to date
2348
+ const keyedEntities = keyBy(entities, getIdOrCodeOrIndex);
2349
+ forEach(newIdMap, (val, key) => {
2350
+ const freshEntity = keyedEntities[key];
2351
+ if (freshEntity) {
2352
+ newIdMap[key] = { ...newIdMap[key], entity: freshEntity };
2353
+ }
2354
+ });
2355
+ finalizeSelection({
2356
+ idMap: newIdMap,
2357
+ entities,
2358
+ props: {
2359
+ onDeselect,
2360
+ onSingleRowSelect,
2361
+ onMultiRowSelect,
2362
+ noDeselectAll,
2363
+ onRowSelect,
2364
+ noSelect,
2365
+ change
2366
+ }
2367
+ });
2368
+ }
2369
+ showContextMenu(e, { idMap: newIdMap, selectedCells });
2370
+ },
2371
+ className: classNames(
2372
+ "with-row-data",
2373
+ getRowClassName && getRowClassName(rowInfo, state),
2374
+ {
2375
+ disabled: rowDisabled,
2376
+ selected: rowSelected && !withCheckboxes,
2377
+ "rt-tr-last-row": rowInfo.index === entities.length - 1
2378
+ }
2379
+ ),
2380
+ "data-test-id": dataId === undefined ? rowInfo.index : dataId,
2381
+ "data-index": rowInfo.index,
2382
+ "data-tip": typeof rowDisabled === "string" ? rowDisabled : undefined,
2383
+ onDoubleClick: e => {
2384
+ if (rowDisabled) return;
2385
+ onDoubleClick &&
2386
+ onDoubleClick(rowInfo.original, rowInfo.index, history, e);
2387
+ }
2388
+ };
2389
+ },
2390
+ [
2391
+ change,
2392
+ entities,
2393
+ expandedEntityIdMap,
2394
+ getRowClassName,
2395
+ history,
2396
+ isCellEditable,
2397
+ isEntityDisabled,
2398
+ isSingleSelect,
2399
+ mustClickCheckboxToSelect,
2400
+ noDeselectAll,
2401
+ noSelect,
2402
+ onDeselect,
2403
+ onDoubleClick,
2404
+ onMultiRowSelect,
2405
+ onRowClick,
2406
+ onRowSelect,
2407
+ onSingleRowSelect,
2408
+ reduxFormSelectedEntityIdMap,
2409
+ selectedCells,
2410
+ showContextMenu,
2411
+ withCheckboxes
2412
+ ]
2413
+ );
2414
+
2415
+ const getTableCellProps = useCallback(
2416
+ (state, rowInfo, column) => {
2417
+ if (!isCellEditable) return {}; //only allow cell selection to do stuff here
2418
+ if (!rowInfo) return {};
2419
+ if (!reduxFormCellValidation) return {};
2420
+ const entity = rowInfo.original;
2421
+ const rowIndex = rowInfo.index;
2422
+ const rowId = getIdOrCodeOrIndex(entity, rowIndex);
2423
+ const {
2424
+ cellId,
2425
+ cellIdAbove,
2426
+ cellIdToRight,
2427
+ cellIdBelow,
2428
+ cellIdToLeft,
2429
+ rowDisabled,
2430
+ columnIndex
2431
+ } = getCellInfo({
2432
+ columnIndex: column.index,
2433
+ columnPath: column.path,
2434
+ rowId,
2435
+ schema,
2436
+ entities,
2437
+ rowIndex,
2438
+ isEntityDisabled,
2439
+ entity
2440
+ });
2441
+
2442
+ const _isClean =
2443
+ (entity._isClean && doNotValidateUntouchedRows) ||
2444
+ isEntityClean(entity);
2445
+
2446
+ const err = !_isClean && reduxFormCellValidation[cellId];
2447
+ let selectedTopBorder,
2448
+ selectedRightBorder,
2449
+ selectedBottomBorder,
2450
+ selectedLeftBorder;
2451
+ if (selectedCells[cellId]) {
2452
+ selectedTopBorder = !selectedCells[cellIdAbove];
2453
+ selectedRightBorder = !selectedCells[cellIdToRight];
2454
+ selectedBottomBorder = !selectedCells[cellIdBelow];
2455
+ selectedLeftBorder = !selectedCells[cellIdToLeft];
2456
+ }
2457
+ const isPrimarySelected = selectedCells[cellId] === PRIMARY_SELECTED_VAL;
2458
+ const className = classNames({
2459
+ isSelectedCell: selectedCells[cellId],
2460
+ isPrimarySelected,
2461
+ isSecondarySelected: selectedCells[cellId] === true,
2462
+ noSelectedTopBorder: !selectedTopBorder,
2463
+ isCleanRow: _isClean,
2464
+ noSelectedRightBorder: !selectedRightBorder,
2465
+ noSelectedBottomBorder: !selectedBottomBorder,
2466
+ noSelectedLeftBorder: !selectedLeftBorder,
2467
+ isDropdownCell: column.type === "dropdown",
2468
+ isEditingCell: reduxFormEditingCell === cellId,
2469
+ hasCellError: !!err,
2470
+ "no-data-tip": selectedCells[cellId]
2471
+ });
2472
+ return {
2473
+ onDoubleClick: () => {
2474
+ // cell double click
2475
+ if (rowDisabled) return;
2476
+ startCellEdit(cellId);
2477
+ },
2478
+ ...(err && {
2479
+ "data-tip": err?.message || err,
2480
+ "data-no-child-data-tip": true
2481
+ }),
2482
+ onContextMenu: e => {
2483
+ const newSelectedCells = { ...selectedCells };
2484
+ if (!isPrimarySelected) {
2485
+ if (primarySelectedCellId) {
2486
+ newSelectedCells[primarySelectedCellId] = true;
2487
+ }
2488
+ newSelectedCells[cellId] = PRIMARY_SELECTED_VAL;
2489
+ setSelectedCells(newSelectedCells);
2490
+ }
2491
+ showContextMenu(e, { selectedCells: newSelectedCells });
2492
+ },
2493
+ onClick: event => {
2494
+ handleCellClick({
2495
+ event,
2496
+ cellId,
2497
+ rowDisabled,
2498
+ rowIndex,
2499
+ columnIndex
2500
+ });
2501
+ },
2502
+ className
2503
+ };
2504
+ },
2505
+ [
2506
+ doNotValidateUntouchedRows,
2507
+ entities,
2508
+ handleCellClick,
2509
+ isCellEditable,
2510
+ isEntityDisabled,
2511
+ primarySelectedCellId,
2512
+ reduxFormCellValidation,
2513
+ reduxFormEditingCell,
2514
+ schema,
2515
+ selectedCells,
2516
+ showContextMenu,
2517
+ startCellEdit
2518
+ ]
2519
+ );
2520
+
1591
2521
  if (withSelectAll && !safeQuery) {
1592
2522
  throw new Error("safeQuery is needed for selecting all table records");
1593
2523
  }
@@ -1603,7 +2533,7 @@ const DataTable = ({
1603
2533
  : "";
1604
2534
 
1605
2535
  const hasFilters =
1606
- filters?.length ||
2536
+ filters.length ||
1607
2537
  searchTerm ||
1608
2538
  schema.fields.some(
1609
2539
  field => field.filterIsActive && field.filterIsActive(currentParams)
@@ -1616,7 +2546,7 @@ const DataTable = ({
1616
2546
 
1617
2547
  const filtersOnNonDisplayedFields = useMemo(() => {
1618
2548
  const _filtersOnNonDisplayedFields = [];
1619
- if (filters?.length) {
2549
+ if (filters && filters.length) {
1620
2550
  schema.fields.forEach(field => {
1621
2551
  const ccDisplayName = getCCDisplayName(field);
1622
2552
  if (field.isHidden) {
@@ -1641,6 +2571,18 @@ const DataTable = ({
1641
2571
  );
1642
2572
  const selectedRowCount = Object.keys(idMap).filter(key => idMap[key]).length;
1643
2573
 
2574
+ let rowsToShow = doNotShowEmptyRows
2575
+ ? Math.min(numRows, entities.length)
2576
+ : numRows;
2577
+ // if there are no entities then provide enough space to show
2578
+ // no rows found message
2579
+ if (entities.length === 0 && rowsToShow < 3) rowsToShow = 3;
2580
+ const expandedRows = entities.reduce((acc, row, index) => {
2581
+ const rowId = getIdOrCodeOrIndex(row, index);
2582
+ acc[index] = expandedEntityIdMap[rowId];
2583
+ return acc;
2584
+ }, {});
2585
+
1644
2586
  const showHeader = (withTitle || withSearch || children) && !noHeader;
1645
2587
  const toggleFullscreenButton = (
1646
2588
  <Button
@@ -1714,19 +2656,44 @@ const DataTable = ({
1714
2656
  _selectedAndTotalMessage += `/ `;
1715
2657
  }
1716
2658
  if (showCount) {
1717
- _selectedAndTotalMessage += `${entityCount || 0} Total`;
2659
+ if (isEntityCountLoading && entityCount < 1) {
2660
+ _selectedAndTotalMessage += `Loading...`;
2661
+ } else {
2662
+ _selectedAndTotalMessage += `${entityCount || 0} Total`;
2663
+ }
1718
2664
  }
1719
2665
  if (_selectedAndTotalMessage) {
1720
2666
  _selectedAndTotalMessage = <div>{_selectedAndTotalMessage}</div>;
1721
2667
  }
1722
2668
  return _selectedAndTotalMessage;
1723
- }, [entityCount, selectedRowCount, showCount, showNumSelected]);
2669
+ }, [
2670
+ entityCount,
2671
+ selectedRowCount,
2672
+ showCount,
2673
+ showNumSelected,
2674
+ isEntityCountLoading
2675
+ ]);
1724
2676
 
1725
2677
  const shouldShowPaging =
1726
2678
  !isInfinite &&
1727
2679
  withPaging &&
1728
2680
  (hidePageSizeWhenPossible ? entityCount > pageSize : true);
1729
2681
 
2682
+ const SubComponentToUse = useMemo(() => {
2683
+ if (SubComponent) {
2684
+ return row => {
2685
+ let shouldShow = true;
2686
+ if (shouldShowSubComponent) {
2687
+ shouldShow = shouldShowSubComponent(row.original);
2688
+ }
2689
+ if (shouldShow) {
2690
+ return SubComponent(row);
2691
+ }
2692
+ };
2693
+ }
2694
+ return;
2695
+ }, [SubComponent, shouldShowSubComponent]);
2696
+
1730
2697
  const nonDisplayedFilterComp = useMemo(() => {
1731
2698
  if (filtersOnNonDisplayedFields.length) {
1732
2699
  const content = filtersOnNonDisplayedFields.map(
@@ -1771,12 +2738,179 @@ const DataTable = ({
1771
2738
  return null;
1772
2739
  }, [filtersOnNonDisplayedFields]);
1773
2740
 
2741
+ const filteredEnts = useMemo(() => {
2742
+ if (onlyShowRowsWErrors) {
2743
+ const rowToErrorMap = {};
2744
+ forEach(reduxFormCellValidation, (err, cellId) => {
2745
+ if (err) {
2746
+ const [rowId] = cellId.split(":");
2747
+ rowToErrorMap[rowId] = true;
2748
+ }
2749
+ });
2750
+ return entities.filter(e => {
2751
+ return rowToErrorMap[e.id];
2752
+ });
2753
+ }
2754
+ return entities;
2755
+ }, [entities, onlyShowRowsWErrors, reduxFormCellValidation]);
2756
+
2757
+ const renderColumns = useColumns({
2758
+ addFilters,
2759
+ cellRenderer,
2760
+ columns,
2761
+ currentParams,
2762
+ compact,
2763
+ withDisplayOptions,
2764
+ resetDefaultVisibility,
2765
+ editingCellSelectAll,
2766
+ entities,
2767
+ expandedEntityIdMap,
2768
+ extraCompact,
2769
+ filters,
2770
+ formName,
2771
+ getCellHoverText,
2772
+ isCellEditable,
2773
+ isEntityDisabled,
2774
+ isLocalCall,
2775
+ isSimple,
2776
+ isSingleSelect,
2777
+ isSelectionARectangle,
2778
+ noDeselectAll,
2779
+ noSelect,
2780
+ noUserSelect,
2781
+ onDeselect,
2782
+ onMultiRowSelect,
2783
+ onRowClick,
2784
+ onRowSelect,
2785
+ onSingleRowSelect,
2786
+ order,
2787
+ primarySelectedCellId,
2788
+ reduxFormCellValidation,
2789
+ reduxFormSelectedEntityIdMap,
2790
+ refocusTable,
2791
+ removeSingleFilter,
2792
+ schema,
2793
+ selectedCells,
2794
+ setExpandedEntityIdMap,
2795
+ setNewParams,
2796
+ setOrder,
2797
+ setSelectedCells,
2798
+ shouldShowSubComponent,
2799
+ startCellEdit,
2800
+ SubComponent,
2801
+ tableRef,
2802
+ updateColumnVisibility,
2803
+ updateEntitiesHelper,
2804
+ updateValidation,
2805
+ withCheckboxes,
2806
+ withExpandAndCollapseAllButton,
2807
+ withFilter,
2808
+ withSort,
2809
+ recordIdToIsVisibleMap,
2810
+ setRecordIdToIsVisibleMap
2811
+ });
2812
+
1774
2813
  const scrollToTop = useCallback(
1775
2814
  () =>
1776
2815
  tableRef.current?.tableRef?.children?.[0]?.children?.[0]?.scrollIntoView(),
1777
2816
  []
1778
2817
  );
1779
2818
 
2819
+ const reactTable = useMemo(
2820
+ () => (
2821
+ <ReactTable
2822
+ data={filteredEnts}
2823
+ ref={tableRef}
2824
+ noVirtual={noVirtual}
2825
+ className={classNames({
2826
+ isCellEditable,
2827
+ "tg-table-loading": isLoading,
2828
+ "tg-table-disabled": disabled
2829
+ })}
2830
+ itemSizeEstimator={
2831
+ extraCompact
2832
+ ? itemSizeEstimators.compact
2833
+ : compact
2834
+ ? itemSizeEstimators.normal
2835
+ : itemSizeEstimators.comfortable
2836
+ }
2837
+ TfootComponent={() => {
2838
+ return <button>hasdfasdf</button>;
2839
+ }}
2840
+ // We should try to not give all the props to the render column
2841
+ columns={renderColumns}
2842
+ pageSize={rowsToShow}
2843
+ expanded={expandedRows}
2844
+ showPagination={false}
2845
+ sortable={false}
2846
+ // loading={isLoading || disabled}
2847
+ loading={disabled}
2848
+ defaultResized={resized}
2849
+ onResizedChange={(newResized = []) => {
2850
+ const resizedToUse = newResized.map(column => {
2851
+ // have a min width of 50 so that columns don't disappear
2852
+ if (column.value < 50) {
2853
+ return {
2854
+ ...column,
2855
+ value: 50
2856
+ };
2857
+ } else {
2858
+ return column;
2859
+ }
2860
+ });
2861
+ resizePersist(resizedToUse);
2862
+ }}
2863
+ TheadComponent={TheadComponent}
2864
+ ThComponent={ThComponent}
2865
+ getTrGroupProps={getTableRowProps}
2866
+ getTdProps={getTableCellProps}
2867
+ NoDataComponent={({ children }) =>
2868
+ isLoading ? (
2869
+ <div className="rt-noData">Loading...</div>
2870
+ ) : (
2871
+ <div className="rt-noData">{noRowsFoundMessage || children}</div>
2872
+ )
2873
+ }
2874
+ LoadingComponent={({ loadingText, loading }) => (
2875
+ <DisabledLoadingComponent
2876
+ loading={loading}
2877
+ loadingText={loadingText}
2878
+ disabled={disabled}
2879
+ />
2880
+ )}
2881
+ style={{
2882
+ maxHeight,
2883
+ minHeight: 150,
2884
+ ...style
2885
+ }}
2886
+ SubComponent={SubComponentToUse}
2887
+ {...ReactTableProps}
2888
+ />
2889
+ ),
2890
+ [
2891
+ ReactTableProps,
2892
+ SubComponentToUse,
2893
+ TheadComponent,
2894
+ compact,
2895
+ disabled,
2896
+ expandedRows,
2897
+ extraCompact,
2898
+ filteredEnts,
2899
+ getTableCellProps,
2900
+ getTableRowProps,
2901
+ isCellEditable,
2902
+ isLoading,
2903
+ maxHeight,
2904
+ noRowsFoundMessage,
2905
+ renderColumns,
2906
+ resizePersist,
2907
+ resized,
2908
+ rowsToShow,
2909
+ style,
2910
+ noVirtual
2911
+ ]
2912
+ );
2913
+
1780
2914
  return (
1781
2915
  <div
1782
2916
  tabIndex="1"
@@ -2072,84 +3206,7 @@ const DataTable = ({
2072
3206
  />
2073
3207
  </div>
2074
3208
  )}
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
- />
3209
+ {reactTable}
2153
3210
  {isCellEditable && (
2154
3211
  <div style={{ display: "flex" }}>
2155
3212
  <div
@@ -2195,6 +3252,7 @@ const DataTable = ({
2195
3252
  {!noFullscreenButton && toggleFullscreenButton}
2196
3253
  {withDisplayOptions && (
2197
3254
  <DisplayOptions
3255
+ doNotSearchHiddenColumns={doNotSearchHiddenColumns}
2198
3256
  compact={compact}
2199
3257
  extraCompact={extraCompact}
2200
3258
  disabled={disabled}
@@ -2247,41 +3305,7 @@ const DataTable = ({
2247
3305
  );
2248
3306
  };
2249
3307
 
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;
3308
+ const WrappedDT = dataTableEnhancer(DataTable);
3309
+ export default WrappedDT;
3310
+ const ConnectedPagingTool = dataTableEnhancer(PagingTool);
2287
3311
  export { ConnectedPagingTool };