@teselagen/ui 0.5.23-beta.18 → 0.5.23-beta.19

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.
package/index.cjs.js CHANGED
@@ -48360,7 +48360,7 @@ const DataTable = /* @__PURE__ */ __name((_G) => {
48360
48360
  const {
48361
48361
  reduxFormCellValidation: _reduxFormCellValidation,
48362
48362
  reduxFormEditingCell,
48363
- reduxFormEntities: _reduxFormEntities,
48363
+ reduxFormEntities,
48364
48364
  reduxFormQueryParams: _reduxFormQueryParams = {},
48365
48365
  reduxFormSearchInput: _reduxFormSearchInput = "",
48366
48366
  reduxFormSelectedEntityIdMap: _reduxFormSelectedEntityIdMap = {}
@@ -48375,7 +48375,6 @@ const DataTable = /* @__PURE__ */ __name((_G) => {
48375
48375
  )
48376
48376
  );
48377
48377
  const reduxFormCellValidation = useDeepEqualMemo(_reduxFormCellValidation);
48378
- const reduxFormEntities = useDeepEqualMemo(_reduxFormEntities);
48379
48378
  const reduxFormQueryParams = useDeepEqualMemo(_reduxFormQueryParams);
48380
48379
  const reduxFormSearchInput = useDeepEqualMemo(_reduxFormSearchInput);
48381
48380
  const reduxFormSelectedEntityIdMap = useDeepEqualMemo(
@@ -48922,8 +48921,7 @@ const DataTable = /* @__PURE__ */ __name((_G) => {
48922
48921
  }
48923
48922
  }));
48924
48923
  },
48925
- // eslint-disable-next-line react-hooks/exhaustive-deps
48926
- []
48924
+ [change]
48927
48925
  );
48928
48926
  const formatAndValidateEntities = React$1.useCallback(
48929
48927
  (entities2, { useDefaultValues, indexToStartAt } = {}) => {
@@ -49486,22 +49484,27 @@ const DataTable = /* @__PURE__ */ __name((_G) => {
49486
49484
  const [selectingAll, setSelectingAll] = React$1.useState(false);
49487
49485
  React$1.useEffect(() => {
49488
49486
  const formatAndValidateTableInitial = /* @__PURE__ */ __name(() => {
49489
- const { newEnts, validationErrors } = formatAndValidateEntities(_origEntities);
49487
+ const { newEnts, validationErrors } = formatAndValidateEntities(entities);
49490
49488
  const toKeep = {};
49491
49489
  forEach(reduxFormCellValidation, (v2, k2) => {
49492
49490
  if (v2 && v2._isTableAsyncWideError) {
49493
49491
  toKeep[k2] = v2;
49494
49492
  }
49495
49493
  });
49496
- change("reduxFormEntities", newEnts);
49494
+ change("reduxFormEntities", (prev) => {
49495
+ if (!isEqual(prev, newEnts)) {
49496
+ return newEnts;
49497
+ }
49498
+ return prev;
49499
+ });
49497
49500
  updateValidation(newEnts, __spreadValues(__spreadValues({}, toKeep), validationErrors));
49498
49501
  }, "formatAndValidateTableInitial");
49499
49502
  isCellEditable && formatAndValidateTableInitial();
49500
49503
  }, [
49501
- _origEntities,
49502
- isCellEditable,
49503
49504
  change,
49505
+ entities,
49504
49506
  formatAndValidateEntities,
49507
+ isCellEditable,
49505
49508
  reduxFormCellValidation,
49506
49509
  updateValidation
49507
49510
  ]);
package/index.es.js CHANGED
@@ -48342,7 +48342,7 @@ const DataTable = /* @__PURE__ */ __name((_G) => {
48342
48342
  const {
48343
48343
  reduxFormCellValidation: _reduxFormCellValidation,
48344
48344
  reduxFormEditingCell,
48345
- reduxFormEntities: _reduxFormEntities,
48345
+ reduxFormEntities,
48346
48346
  reduxFormQueryParams: _reduxFormQueryParams = {},
48347
48347
  reduxFormSearchInput: _reduxFormSearchInput = "",
48348
48348
  reduxFormSelectedEntityIdMap: _reduxFormSelectedEntityIdMap = {}
@@ -48357,7 +48357,6 @@ const DataTable = /* @__PURE__ */ __name((_G) => {
48357
48357
  )
48358
48358
  );
48359
48359
  const reduxFormCellValidation = useDeepEqualMemo(_reduxFormCellValidation);
48360
- const reduxFormEntities = useDeepEqualMemo(_reduxFormEntities);
48361
48360
  const reduxFormQueryParams = useDeepEqualMemo(_reduxFormQueryParams);
48362
48361
  const reduxFormSearchInput = useDeepEqualMemo(_reduxFormSearchInput);
48363
48362
  const reduxFormSelectedEntityIdMap = useDeepEqualMemo(
@@ -48904,8 +48903,7 @@ const DataTable = /* @__PURE__ */ __name((_G) => {
48904
48903
  }
48905
48904
  }));
48906
48905
  },
48907
- // eslint-disable-next-line react-hooks/exhaustive-deps
48908
- []
48906
+ [change$1]
48909
48907
  );
48910
48908
  const formatAndValidateEntities = useCallback(
48911
48909
  (entities2, { useDefaultValues, indexToStartAt } = {}) => {
@@ -49468,22 +49466,27 @@ const DataTable = /* @__PURE__ */ __name((_G) => {
49468
49466
  const [selectingAll, setSelectingAll] = useState(false);
49469
49467
  useEffect(() => {
49470
49468
  const formatAndValidateTableInitial = /* @__PURE__ */ __name(() => {
49471
- const { newEnts, validationErrors } = formatAndValidateEntities(_origEntities);
49469
+ const { newEnts, validationErrors } = formatAndValidateEntities(entities);
49472
49470
  const toKeep = {};
49473
49471
  forEach(reduxFormCellValidation, (v2, k2) => {
49474
49472
  if (v2 && v2._isTableAsyncWideError) {
49475
49473
  toKeep[k2] = v2;
49476
49474
  }
49477
49475
  });
49478
- change$1("reduxFormEntities", newEnts);
49476
+ change$1("reduxFormEntities", (prev) => {
49477
+ if (!isEqual(prev, newEnts)) {
49478
+ return newEnts;
49479
+ }
49480
+ return prev;
49481
+ });
49479
49482
  updateValidation(newEnts, __spreadValues(__spreadValues({}, toKeep), validationErrors));
49480
49483
  }, "formatAndValidateTableInitial");
49481
49484
  isCellEditable && formatAndValidateTableInitial();
49482
49485
  }, [
49483
- _origEntities,
49484
- isCellEditable,
49485
49486
  change$1,
49487
+ entities,
49486
49488
  formatAndValidateEntities,
49489
+ isCellEditable,
49487
49490
  reduxFormCellValidation,
49488
49491
  updateValidation
49489
49492
  ]);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@teselagen/ui",
3
- "version": "0.5.23-beta.18",
3
+ "version": "0.5.23-beta.19",
4
4
  "main": "./src/index.js",
5
5
  "exports": {
6
6
  ".": {
@@ -0,0 +1,945 @@
1
+ import React, { isValidElement, useCallback } from "react";
2
+ import classNames from "classnames";
3
+ import { Button, Classes, Checkbox, Icon } from "@blueprintjs/core";
4
+ import {
5
+ set,
6
+ toString,
7
+ camelCase,
8
+ startCase,
9
+ noop,
10
+ cloneDeep,
11
+ get,
12
+ padStart
13
+ } from "lodash-es";
14
+ import dayjs from "dayjs";
15
+ import localizedFormat from "dayjs/plugin/localizedFormat";
16
+ import ReactMarkdown from "react-markdown";
17
+ import remarkGfm from "remark-gfm";
18
+ import joinUrl from "url-join";
19
+ import InfoHelper from "../InfoHelper";
20
+ import {
21
+ getEntityIdToEntity,
22
+ getFieldPathToIndex,
23
+ getFieldPathToField,
24
+ getIdOrCodeOrIndex,
25
+ getNumberStrAtEnd,
26
+ getSelectedRowsFromEntities,
27
+ PRIMARY_SELECTED_VAL,
28
+ stripNumberAtEnd
29
+ } from "./utils";
30
+ import FilterAndSortMenu from "./FilterAndSortMenu";
31
+ import { ColumnFilterMenu } from "./ColumnFilterMenu";
32
+ import getTextFromEl from "../utils/getTextFromEl";
33
+ import rowClick, { finalizeSelection } from "./utils/rowClick";
34
+ import { editCellHelper } from "./editCellHelper";
35
+ import { getCellVal } from "./getCellVal";
36
+ import { getCCDisplayName } from "./utils/queryParams";
37
+ import { useDispatch } from "react-redux";
38
+ import { change as _change } from "redux-form";
39
+ import { RenderCell } from "./RenderCell";
40
+
41
+ dayjs.extend(localizedFormat);
42
+
43
+ const RenderColumnHeader = ({
44
+ addFilters,
45
+ column,
46
+ compact,
47
+ currentParams,
48
+ entities,
49
+ extraCompact,
50
+ filters,
51
+ isCellEditable,
52
+ isLocalCall,
53
+ order,
54
+ removeSingleFilter,
55
+ setNewParams,
56
+ setOrder,
57
+ updateEntitiesHelper,
58
+ withFilter,
59
+ withSort
60
+ }) => {
61
+ const {
62
+ displayName,
63
+ description,
64
+ isUnique,
65
+ sortDisabled,
66
+ filterDisabled,
67
+ columnFilterDisabled,
68
+ renderTitleInner,
69
+ filterIsActive = noop,
70
+ noTitle,
71
+ isNotEditable,
72
+ type,
73
+ path
74
+ } = column;
75
+ const columnDataType = column.type;
76
+ const isActionColumn = columnDataType === "action";
77
+ const disableSorting =
78
+ sortDisabled ||
79
+ isActionColumn ||
80
+ (!isLocalCall && typeof path === "string" && path.includes(".")) ||
81
+ columnDataType === "color";
82
+ const disableFiltering =
83
+ filterDisabled ||
84
+ columnDataType === "color" ||
85
+ isActionColumn ||
86
+ columnFilterDisabled;
87
+ const ccDisplayName = getCCDisplayName(column);
88
+ let columnTitle = displayName || startCase(camelCase(path));
89
+ if (isActionColumn) columnTitle = "";
90
+
91
+ const currentFilter =
92
+ filters &&
93
+ !!filters.length &&
94
+ filters.filter(({ filterOn }) => {
95
+ return filterOn === ccDisplayName;
96
+ })[0];
97
+ const filterActiveForColumn =
98
+ !!currentFilter || filterIsActive(currentParams);
99
+ let ordering;
100
+ if (order && order.length) {
101
+ order.forEach(order => {
102
+ const orderField = order.replace("-", "");
103
+ if (orderField === ccDisplayName) {
104
+ if (orderField === order) {
105
+ ordering = "asc";
106
+ } else {
107
+ ordering = "desc";
108
+ }
109
+ }
110
+ });
111
+ }
112
+
113
+ const sortDown = ordering && ordering === "asc";
114
+ const sortUp = ordering && !sortDown;
115
+ const FilterMenu = column.FilterMenu || FilterAndSortMenu;
116
+
117
+ let maybeCheckbox;
118
+ if (isCellEditable && !isNotEditable && type === "boolean") {
119
+ let isIndeterminate = false;
120
+ let isChecked = !!entities.length;
121
+ let hasFalse;
122
+ let hasTrue;
123
+ entities.some(e => {
124
+ if (!get(e, path)) {
125
+ isChecked = false;
126
+ hasFalse = true;
127
+ } else {
128
+ hasTrue = true;
129
+ }
130
+ if (hasFalse && hasTrue) {
131
+ isIndeterminate = true;
132
+ return true;
133
+ }
134
+ return false;
135
+ });
136
+ maybeCheckbox = (
137
+ <Checkbox
138
+ style={{ marginBottom: 0, marginLeft: 3 }}
139
+ onChange={() => {
140
+ updateEntitiesHelper(entities, ents => {
141
+ ents.forEach(e => {
142
+ delete e._isClean;
143
+ set(e, path, isIndeterminate ? true : !isChecked);
144
+ });
145
+ });
146
+ }}
147
+ indeterminate={isIndeterminate}
148
+ checked={isChecked}
149
+ />
150
+ );
151
+ }
152
+
153
+ const columnTitleTextified = getTextFromEl(columnTitle);
154
+
155
+ return (
156
+ <div
157
+ {...(description && {
158
+ "data-tip": `<div>
159
+ <strong>${columnTitle}:</strong> <br>
160
+ ${description} ${isUnique ? "<br>Must be unique" : ""}</div>`
161
+ })}
162
+ data-test={columnTitleTextified}
163
+ data-path={path}
164
+ data-copy-text={columnTitleTextified}
165
+ data-copy-json={JSON.stringify({
166
+ __strVal: columnTitleTextified,
167
+ __isHeaderCell: true
168
+ })}
169
+ className={classNames("tg-react-table-column-header", {
170
+ "sort-active": sortUp || sortDown
171
+ })}
172
+ >
173
+ {columnTitleTextified && !noTitle && (
174
+ <>
175
+ {maybeCheckbox}
176
+ <span
177
+ title={columnTitleTextified}
178
+ className={classNames({
179
+ "tg-react-table-name": true,
180
+ "no-data-tip": !!description
181
+ })}
182
+ style={{
183
+ ...(description && { fontStyle: "italic" }),
184
+ display: "inline-block"
185
+ }}
186
+ >
187
+ {renderTitleInner ? renderTitleInner : columnTitle}{" "}
188
+ </span>
189
+ </>
190
+ )}
191
+ <div
192
+ style={{ display: "flex", marginLeft: "auto", alignItems: "center" }}
193
+ >
194
+ {withSort && !disableSorting && (
195
+ <div className="tg-sort-arrow-container">
196
+ <Icon
197
+ data-tip="Sort Z-A (Hold shift to sort multiple columns)"
198
+ icon="chevron-up"
199
+ className={classNames({
200
+ active: sortUp
201
+ })}
202
+ color={sortUp ? "#106ba3" : undefined}
203
+ iconSize={extraCompact ? 10 : 12}
204
+ onClick={e => {
205
+ setOrder("-" + ccDisplayName, sortUp, e.shiftKey);
206
+ }}
207
+ />
208
+ <Icon
209
+ data-tip="Sort A-Z (Hold shift to sort multiple columns)"
210
+ icon="chevron-down"
211
+ className={classNames({
212
+ active: sortDown
213
+ })}
214
+ color={sortDown ? "#106ba3" : undefined}
215
+ iconSize={extraCompact ? 10 : 12}
216
+ onClick={e => {
217
+ setOrder(ccDisplayName, sortDown, e.shiftKey);
218
+ }}
219
+ />
220
+ </div>
221
+ )}
222
+ {withFilter && !disableFiltering && (
223
+ <ColumnFilterMenu
224
+ FilterMenu={FilterMenu}
225
+ filterActiveForColumn={filterActiveForColumn}
226
+ addFilters={addFilters}
227
+ removeSingleFilter={removeSingleFilter}
228
+ currentFilter={currentFilter}
229
+ filterOn={ccDisplayName}
230
+ dataType={columnDataType}
231
+ schemaForField={column}
232
+ currentParams={currentParams}
233
+ setNewParams={setNewParams}
234
+ compact={compact}
235
+ extraCompact={extraCompact}
236
+ />
237
+ )}
238
+ </div>
239
+ </div>
240
+ );
241
+ };
242
+
243
+ const renderCheckboxHeader = ({
244
+ change,
245
+ entities,
246
+ isEntityDisabled,
247
+ isSingleSelect,
248
+ noDeselectAll,
249
+ noSelect,
250
+ noUserSelect = false,
251
+ onDeselect,
252
+ onMultiRowSelect,
253
+ onRowSelect,
254
+ onSingleRowSelect,
255
+ reduxFormSelectedEntityIdMap
256
+ }) => {
257
+ const checkedRows = getSelectedRowsFromEntities(
258
+ entities,
259
+ reduxFormSelectedEntityIdMap
260
+ );
261
+ const checkboxProps = {
262
+ checked: false,
263
+ indeterminate: false
264
+ };
265
+ const notDisabledEntityCount = entities.reduce((acc, e) => {
266
+ return isEntityDisabled(e) ? acc : acc + 1;
267
+ }, 0);
268
+ if (checkedRows.length === notDisabledEntityCount) {
269
+ //tnr: maybe this will need to change if we want enable select all across pages
270
+ checkboxProps.checked = notDisabledEntityCount !== 0;
271
+ } else {
272
+ if (checkedRows.length) {
273
+ checkboxProps.indeterminate = true;
274
+ }
275
+ }
276
+
277
+ return !isSingleSelect ? (
278
+ <Checkbox
279
+ name="checkBoxHeader"
280
+ disabled={noSelect || noUserSelect}
281
+ onChange={() => {
282
+ const newIdMap = cloneDeep(reduxFormSelectedEntityIdMap) || {};
283
+ entities.forEach((entity, i) => {
284
+ if (isEntityDisabled(entity)) return;
285
+ const entityId = getIdOrCodeOrIndex(entity, i);
286
+ if (checkboxProps.checked) {
287
+ delete newIdMap[entityId];
288
+ } else {
289
+ newIdMap[entityId] = { entity };
290
+ }
291
+ });
292
+
293
+ finalizeSelection({
294
+ idMap: newIdMap,
295
+ entities,
296
+ props: {
297
+ onDeselect,
298
+ onSingleRowSelect,
299
+ onMultiRowSelect,
300
+ noDeselectAll,
301
+ onRowSelect,
302
+ noSelect,
303
+ change
304
+ }
305
+ });
306
+ }}
307
+ {...checkboxProps}
308
+ />
309
+ ) : null;
310
+ };
311
+
312
+ export const useColumns = ({
313
+ addFilters,
314
+ cellRenderer,
315
+ columns,
316
+ currentParams,
317
+ compact,
318
+ editingCell,
319
+ editingCellSelectAll,
320
+ entities,
321
+ expandedEntityIdMap,
322
+ extraCompact,
323
+ filters,
324
+ formName,
325
+ getCellHoverText,
326
+ isCellEditable,
327
+ isEntityDisabled,
328
+ isLocalCall,
329
+ isSimple,
330
+ isSingleSelect,
331
+ isSelectionARectangle,
332
+ noDeselectAll,
333
+ noSelect,
334
+ noUserSelect,
335
+ onDeselect,
336
+ onMultiRowSelect,
337
+ onRowClick,
338
+ onRowSelect,
339
+ onSingleRowSelect,
340
+ order,
341
+ primarySelectedCellId,
342
+ reduxFormCellValidation,
343
+ reduxFormSelectedEntityIdMap,
344
+ refocusTable,
345
+ removeSingleFilter = noop,
346
+ schema,
347
+ selectedCells,
348
+ setExpandedEntityIdMap,
349
+ setNewParams,
350
+ setOrder = noop,
351
+ setSelectedCells,
352
+ shouldShowSubComponent,
353
+ startCellEdit,
354
+ SubComponent,
355
+ tableRef,
356
+ updateEntitiesHelper,
357
+ updateValidation,
358
+ withCheckboxes,
359
+ withExpandAndCollapseAllButton,
360
+ withFilter: _withFilter,
361
+ withSort = true
362
+ }) => {
363
+ const dispatch = useDispatch();
364
+ const change = useCallback(
365
+ (...args) => dispatch(_change(formName, ...args)),
366
+ [dispatch, formName]
367
+ );
368
+ const withFilter = _withFilter === undefined ? !isSimple : _withFilter;
369
+
370
+ const onDragEnd = useCallback(
371
+ cellsToSelect => {
372
+ const [primaryRowId, primaryCellPath] = primarySelectedCellId.split(":");
373
+ const pathToField = getFieldPathToField(schema);
374
+ const { selectedPaths, selectionGrid } = isSelectionARectangle();
375
+ let allSelectedPaths = selectedPaths;
376
+ if (!allSelectedPaths) {
377
+ allSelectedPaths = [primaryCellPath];
378
+ }
379
+
380
+ updateEntitiesHelper(entities, entities => {
381
+ let newSelectedCells;
382
+ if (selectedPaths) {
383
+ newSelectedCells = {
384
+ ...selectedCells
385
+ };
386
+ } else {
387
+ newSelectedCells = {
388
+ [primarySelectedCellId]: PRIMARY_SELECTED_VAL
389
+ };
390
+ }
391
+
392
+ const newCellValidate = {
393
+ ...reduxFormCellValidation
394
+ };
395
+ const entityMap = getEntityIdToEntity(entities);
396
+ const { e: selectedEnt } = entityMap[primaryRowId];
397
+ const firstCellToSelectRowIndex =
398
+ entityMap[cellsToSelect[0]?.split(":")[0]]?.i;
399
+ const pathToIndex = getFieldPathToIndex(schema);
400
+
401
+ allSelectedPaths.forEach(selectedPath => {
402
+ const column = pathToField[selectedPath];
403
+
404
+ const selectedCellVal = getCellVal(selectedEnt, selectedPath, column);
405
+ const cellIndexOfSelectedPath = pathToIndex[selectedPath];
406
+ let incrementStart;
407
+ let incrementPrefix;
408
+ let incrementPad = 0;
409
+ if (column.type === "string" || column.type === "number") {
410
+ const cellNumStr = getNumberStrAtEnd(selectedCellVal);
411
+ const cellNum = Number(cellNumStr);
412
+ const entityAbovePrimaryCell =
413
+ entities[entityMap[primaryRowId].i - 1];
414
+ if (cellNumStr !== null && !isNaN(cellNum)) {
415
+ if (
416
+ entityAbovePrimaryCell &&
417
+ (!selectionGrid || selectionGrid.length <= 1)
418
+ ) {
419
+ const cellAboveVal = get(
420
+ entityAbovePrimaryCell,
421
+ selectedPath,
422
+ ""
423
+ );
424
+ const cellAboveNumStr = getNumberStrAtEnd(cellAboveVal);
425
+ const cellAboveNum = Number(cellAboveNumStr);
426
+ if (!isNaN(cellAboveNum)) {
427
+ const isIncremental = cellNum - cellAboveNum === 1;
428
+ if (isIncremental) {
429
+ const cellTextNoNum = stripNumberAtEnd(selectedCellVal);
430
+ const sameText =
431
+ stripNumberAtEnd(cellAboveVal) === cellTextNoNum;
432
+ if (sameText) {
433
+ incrementStart = cellNum + 1;
434
+ incrementPrefix = cellTextNoNum || "";
435
+ if (cellNumStr && cellNumStr.startsWith("0")) {
436
+ incrementPad = cellNumStr.length;
437
+ }
438
+ }
439
+ }
440
+ }
441
+ }
442
+ if (incrementStart === undefined) {
443
+ const draggingDown =
444
+ firstCellToSelectRowIndex > selectionGrid?.[0][0].rowIndex;
445
+ if (selectedPaths && draggingDown) {
446
+ let checkIncrement;
447
+ let prefix;
448
+ let maybePad;
449
+ // determine if all the cells in this column of the selectionGrid are incrementing
450
+ const allAreIncrementing = selectionGrid.every(row => {
451
+ // see if cell is selected
452
+ const cellInfo = row[cellIndexOfSelectedPath];
453
+ if (!cellInfo) return false;
454
+ const { cellId } = cellInfo;
455
+ const [rowId] = cellId.split(":");
456
+ const cellVal = getCellVal(
457
+ entityMap[rowId].e,
458
+ selectedPath,
459
+ pathToField[selectedPath]
460
+ );
461
+ const cellNumStr = getNumberStrAtEnd(cellVal);
462
+ const cellNum = Number(cellNumStr);
463
+ const cellTextNoNum = stripNumberAtEnd(cellVal);
464
+ if (cellNumStr?.startsWith("0")) {
465
+ maybePad = cellNumStr.length;
466
+ }
467
+ if (cellTextNoNum && !prefix) {
468
+ prefix = cellTextNoNum;
469
+ }
470
+ if (cellTextNoNum && prefix !== cellTextNoNum) {
471
+ return false;
472
+ }
473
+ if (!isNaN(cellNum)) {
474
+ if (!checkIncrement) {
475
+ checkIncrement = cellNum;
476
+ return true;
477
+ } else {
478
+ return ++checkIncrement === cellNum;
479
+ }
480
+ } else {
481
+ return false;
482
+ }
483
+ });
484
+
485
+ if (allAreIncrementing) {
486
+ incrementStart = checkIncrement + 1;
487
+ incrementPrefix = prefix || "";
488
+ incrementPad = maybePad;
489
+ }
490
+ }
491
+ }
492
+ }
493
+ }
494
+
495
+ let firstSelectedCellRowIndex;
496
+ if (selectionGrid) {
497
+ selectionGrid[0].some(cell => {
498
+ if (cell) {
499
+ firstSelectedCellRowIndex = cell.rowIndex;
500
+ return true;
501
+ }
502
+ return false;
503
+ });
504
+ }
505
+
506
+ cellsToSelect.forEach(cellId => {
507
+ const [rowId, cellPath] = cellId.split(":");
508
+ if (cellPath !== selectedPath) return;
509
+ newSelectedCells[cellId] = true;
510
+ const { e: entityToUpdate, i: rowIndex } = entityMap[rowId] || {};
511
+ if (entityToUpdate) {
512
+ delete entityToUpdate._isClean;
513
+ let newVal;
514
+ if (incrementStart !== undefined) {
515
+ const num = incrementStart++;
516
+ newVal = incrementPrefix + padStart(num, incrementPad, "0");
517
+ } else {
518
+ if (selectionGrid && selectionGrid.length > 1) {
519
+ // if there are multiple cells selected then we want to copy them repeating
520
+ // ex: if we have 1,2,3 selected and we drag for 5 more rows we want it to
521
+ // be 1,2,3,1,2 for the new row cells in this column
522
+ const draggingDown = rowIndex > firstSelectedCellRowIndex;
523
+ const cellIndex = pathToIndex[cellPath];
524
+ let cellIdToCopy;
525
+ if (draggingDown) {
526
+ const { cellId } = selectionGrid[
527
+ (rowIndex - firstSelectedCellRowIndex) %
528
+ selectionGrid.length
529
+ ].find(g => g && g.cellIndex === cellIndex);
530
+ cellIdToCopy = cellId;
531
+ } else {
532
+ const lastIndexInGrid =
533
+ selectionGrid[selectionGrid.length - 1][0].rowIndex;
534
+ const { cellId } = selectionGrid[
535
+ (rowIndex + lastIndexInGrid + 1) % selectionGrid.length
536
+ ].find(g => g.cellIndex === cellIndex);
537
+ cellIdToCopy = cellId;
538
+ }
539
+
540
+ const [rowIdToCopy, cellPathToCopy] = cellIdToCopy.split(":");
541
+ newVal = getCellVal(
542
+ entityMap[rowIdToCopy].e,
543
+ cellPathToCopy,
544
+ pathToField[cellPathToCopy]
545
+ );
546
+ } else {
547
+ newVal = selectedCellVal;
548
+ }
549
+ }
550
+ const { error } = editCellHelper({
551
+ entity: entityToUpdate,
552
+ path: cellPath,
553
+ schema,
554
+ newVal
555
+ });
556
+ newCellValidate[cellId] = error;
557
+ }
558
+ });
559
+ });
560
+
561
+ // select the new cells
562
+ updateValidation(entities, newCellValidate);
563
+ setSelectedCells(newSelectedCells);
564
+ });
565
+ },
566
+ [
567
+ entities,
568
+ isSelectionARectangle,
569
+ primarySelectedCellId,
570
+ reduxFormCellValidation,
571
+ schema,
572
+ selectedCells,
573
+ setSelectedCells,
574
+ updateEntitiesHelper,
575
+ updateValidation
576
+ ]
577
+ );
578
+
579
+ const getCopyTextForCell = useCallback(
580
+ (val, row = {}, column = {}) => {
581
+ // TODOCOPY we need a way to potentially omit certain columns from being added as a \t element (talk to taoh about this)
582
+ let text = typeof val !== "string" ? row.value : val;
583
+
584
+ // We should try to take out the props from here, it produces
585
+ // unnecessary rerenders
586
+ const record = row.original;
587
+ if (column.getClipboardData) {
588
+ text = column.getClipboardData(row.value, record, row);
589
+ } else if (column.getValueToFilterOn) {
590
+ text = column.getValueToFilterOn(record);
591
+ } else if (column.render) {
592
+ text = column.render(row.value, record, row, {
593
+ currentParams,
594
+ setNewParams
595
+ });
596
+ } else if (cellRenderer && cellRenderer[column.path]) {
597
+ text = cellRenderer[column.path](row.value, row.original, row, {
598
+ currentParams,
599
+ setNewParams
600
+ });
601
+ } else if (text) {
602
+ text = isValidElement(text) ? text : String(text);
603
+ }
604
+ const getTextFromElementOrLink = text => {
605
+ if (isValidElement(text)) {
606
+ if (text.props?.to) {
607
+ // this will convert Link elements to url strings
608
+ return joinUrl(
609
+ window.location.origin,
610
+ window.frontEndConfig?.clientBasePath || "",
611
+ text.props.to
612
+ );
613
+ } else {
614
+ return getTextFromEl(text);
615
+ }
616
+ } else {
617
+ return text;
618
+ }
619
+ };
620
+ text = getTextFromElementOrLink(text);
621
+
622
+ if (Array.isArray(text)) {
623
+ let arrText = text.map(getTextFromElementOrLink).join(", ");
624
+ // because we sometimes insert commas after links when mapping over an array of elements we will have double ,'s
625
+ arrText = arrText.replace(/, ,/g, ",");
626
+ text = arrText;
627
+ }
628
+
629
+ const stringText = toString(text);
630
+ if (stringText === "[object Object]") return "";
631
+ return stringText;
632
+ },
633
+ [cellRenderer, currentParams, setNewParams]
634
+ );
635
+
636
+ const renderCheckboxCell = useCallback(
637
+ row => {
638
+ const rowIndex = row.index;
639
+ const checkedRows = getSelectedRowsFromEntities(
640
+ entities,
641
+ reduxFormSelectedEntityIdMap
642
+ );
643
+
644
+ const isSelected = checkedRows.some(rowNum => {
645
+ return rowNum === rowIndex;
646
+ });
647
+ if (rowIndex >= entities.length) {
648
+ return <div />;
649
+ }
650
+ const entity = entities[rowIndex];
651
+ return (
652
+ <Checkbox
653
+ name={`${getIdOrCodeOrIndex(entity, rowIndex)}-checkbox`}
654
+ disabled={noSelect || noUserSelect || isEntityDisabled(entity)}
655
+ onClick={e => {
656
+ rowClick(e, row, entities, {
657
+ reduxFormSelectedEntityIdMap,
658
+ isSingleSelect,
659
+ noSelect,
660
+ onRowClick,
661
+ isEntityDisabled,
662
+ withCheckboxes,
663
+ onDeselect,
664
+ onSingleRowSelect,
665
+ onMultiRowSelect,
666
+ noDeselectAll,
667
+ onRowSelect,
668
+ change
669
+ });
670
+ }}
671
+ checked={isSelected}
672
+ />
673
+ );
674
+ },
675
+ [
676
+ change,
677
+ entities,
678
+ isEntityDisabled,
679
+ isSingleSelect,
680
+ noDeselectAll,
681
+ noSelect,
682
+ noUserSelect,
683
+ onDeselect,
684
+ onMultiRowSelect,
685
+ onRowClick,
686
+ onRowSelect,
687
+ onSingleRowSelect,
688
+ reduxFormSelectedEntityIdMap,
689
+ withCheckboxes
690
+ ]
691
+ );
692
+
693
+ const finishCellEdit = useCallback(
694
+ (cellId, newVal, doNotStopEditing) => {
695
+ const [rowId, path] = cellId.split(":");
696
+ !doNotStopEditing && change("reduxFormEditingCell", null);
697
+ updateEntitiesHelper(entities, entities => {
698
+ const entity = entities.find((e, i) => {
699
+ return getIdOrCodeOrIndex(e, i) === rowId;
700
+ });
701
+ delete entity._isClean;
702
+ const { error } = editCellHelper({
703
+ entity,
704
+ path,
705
+ schema,
706
+ newVal
707
+ });
708
+
709
+ updateValidation(entities, {
710
+ ...reduxFormCellValidation,
711
+ [cellId]: error
712
+ });
713
+ });
714
+ !doNotStopEditing && refocusTable();
715
+ },
716
+ [
717
+ change,
718
+ entities,
719
+ reduxFormCellValidation,
720
+ refocusTable,
721
+ schema,
722
+ updateEntitiesHelper,
723
+ updateValidation
724
+ ]
725
+ );
726
+
727
+ const cancelCellEdit = useCallback(() => {
728
+ change("reduxFormEditingCell", null);
729
+ refocusTable();
730
+ }, [change, refocusTable]);
731
+
732
+ if (!columns.length) {
733
+ return columns;
734
+ }
735
+
736
+ const columnsToRender = [];
737
+ if (SubComponent) {
738
+ columnsToRender.push({
739
+ ...(withExpandAndCollapseAllButton && {
740
+ Header: () => {
741
+ const showCollapseAll =
742
+ Object.values(expandedEntityIdMap).filter(i => i).length ===
743
+ entities.length;
744
+ return (
745
+ <InfoHelper
746
+ content={showCollapseAll ? "Collapse All" : "Expand All"}
747
+ isButton
748
+ minimal
749
+ small
750
+ style={{ padding: 2 }}
751
+ popoverProps={{
752
+ modifiers: {
753
+ preventOverflow: { enabled: false },
754
+ hide: { enabled: false }
755
+ }
756
+ }}
757
+ onClick={() => {
758
+ showCollapseAll
759
+ ? setExpandedEntityIdMap({})
760
+ : setExpandedEntityIdMap(prev => {
761
+ const newMap = { ...prev };
762
+ entities.forEach(e => {
763
+ newMap[getIdOrCodeOrIndex(e)] = true;
764
+ });
765
+ return newMap;
766
+ });
767
+ }}
768
+ className={classNames("tg-expander-all")}
769
+ icon={showCollapseAll ? "chevron-down" : "chevron-right"}
770
+ />
771
+ );
772
+ }
773
+ }),
774
+ expander: true,
775
+ Expander: ({ isExpanded, original: record }) => {
776
+ let shouldShow = true;
777
+ if (shouldShowSubComponent) {
778
+ shouldShow = shouldShowSubComponent(record);
779
+ }
780
+ if (!shouldShow) return null;
781
+ return (
782
+ <Button
783
+ className={classNames(
784
+ "tg-expander",
785
+ Classes.MINIMAL,
786
+ Classes.SMALL
787
+ )}
788
+ icon={isExpanded ? "chevron-down" : "chevron-right"}
789
+ />
790
+ );
791
+ }
792
+ });
793
+ }
794
+
795
+ if (withCheckboxes) {
796
+ columnsToRender.push({
797
+ Header: renderCheckboxHeader({
798
+ change,
799
+ entities,
800
+ isEntityDisabled,
801
+ isSingleSelect,
802
+ noDeselectAll,
803
+ noSelect,
804
+ noUserSelect,
805
+ onDeselect,
806
+ onMultiRowSelect,
807
+ onRowSelect,
808
+ onSingleRowSelect,
809
+ reduxFormSelectedEntityIdMap
810
+ }),
811
+ Cell: renderCheckboxCell,
812
+ width: 35,
813
+ resizable: false,
814
+ getHeaderProps: () => {
815
+ return {
816
+ className: "tg-react-table-checkbox-header-container",
817
+ immovable: "true"
818
+ };
819
+ },
820
+ getProps: () => {
821
+ return {
822
+ className: "tg-react-table-checkbox-cell-container"
823
+ };
824
+ }
825
+ });
826
+ }
827
+
828
+ const tableColumns = columns.map(column => {
829
+ const tableColumn = {
830
+ ...column,
831
+ Header: RenderColumnHeader({
832
+ column,
833
+ isLocalCall,
834
+ filters,
835
+ currentParams,
836
+ order,
837
+ setOrder,
838
+ withSort,
839
+ extraCompact,
840
+ withFilter,
841
+ addFilters,
842
+ removeSingleFilter,
843
+ setNewParams,
844
+ compact,
845
+ isCellEditable,
846
+ entities,
847
+ updateEntitiesHelper
848
+ }),
849
+ accessor: column.path,
850
+ getHeaderProps: () => ({
851
+ // needs to be a string because it is getting passed
852
+ // to the dom
853
+ immovable: column.immovable ? "true" : "false",
854
+ columnindex: column.columnIndex
855
+ })
856
+ };
857
+ const noEllipsis = column.noEllipsis;
858
+ if (column.width) {
859
+ tableColumn.width = column.width;
860
+ }
861
+ if (cellRenderer && cellRenderer[column.path]) {
862
+ tableColumn.Cell = row => {
863
+ const val = cellRenderer[column.path](row.value, row.original, row, {
864
+ currentParams,
865
+ setNewParams
866
+ });
867
+ return val;
868
+ };
869
+ } else if (column.render) {
870
+ tableColumn.Cell = row => {
871
+ const val = column.render(row.value, row.original, row, {
872
+ currentParams,
873
+ setNewParams
874
+ });
875
+ return val;
876
+ };
877
+ } else if (column.type === "timestamp") {
878
+ tableColumn.Cell = ({ value }) => {
879
+ return value ? dayjs(value).format("lll") : "";
880
+ };
881
+ } else if (column.type === "color") {
882
+ tableColumn.Cell = ({ value }) => {
883
+ return value ? (
884
+ <div
885
+ style={{
886
+ height: 20,
887
+ width: 40,
888
+ background: value,
889
+ border: "1px solid #182026",
890
+ borderRadius: 5
891
+ }}
892
+ />
893
+ ) : (
894
+ ""
895
+ );
896
+ };
897
+ } else if (column.type === "boolean") {
898
+ if (isCellEditable) {
899
+ tableColumn.Cell = ({ value }) => (value ? "True" : "False");
900
+ } else {
901
+ tableColumn.Cell = ({ value }) => (
902
+ <Icon
903
+ className={classNames({
904
+ [Classes.TEXT_MUTED]: !value
905
+ })}
906
+ icon={value ? "tick" : "cross"}
907
+ />
908
+ );
909
+ }
910
+ } else if (column.type === "markdown") {
911
+ tableColumn.Cell = ({ value }) => (
912
+ <ReactMarkdown remarkPlugins={[remarkGfm]}>{value}</ReactMarkdown>
913
+ );
914
+ } else {
915
+ tableColumn.Cell = ({ value }) => value;
916
+ }
917
+ const oldFunc = tableColumn.Cell;
918
+
919
+ tableColumn.Cell = (...args) => (
920
+ <RenderCell
921
+ oldFunc={oldFunc}
922
+ formName={formName}
923
+ getCopyTextForCell={getCopyTextForCell}
924
+ column={column}
925
+ isCellEditable={isCellEditable}
926
+ isEntityDisabled={isEntityDisabled}
927
+ finishCellEdit={finishCellEdit}
928
+ noEllipsis={noEllipsis}
929
+ editingCell={editingCell}
930
+ cancelCellEdit={cancelCellEdit}
931
+ editingCellSelectAll={editingCellSelectAll}
932
+ getCellHoverText={getCellHoverText}
933
+ selectedCells={selectedCells}
934
+ isSelectionARectangle={isSelectionARectangle}
935
+ startCellEdit={startCellEdit}
936
+ tableRef={tableRef}
937
+ onDragEnd={onDragEnd}
938
+ args={args}
939
+ />
940
+ );
941
+ return tableColumn;
942
+ });
943
+
944
+ return columnsToRender.concat(tableColumns);
945
+ };
@@ -0,0 +1,44 @@
1
+ import React, { useEffect, useRef, useState } from "react";
2
+
3
+ export const EditableCell = ({
4
+ cancelEdit,
5
+ dataTest,
6
+ finishEdit,
7
+ isNumeric,
8
+ initialValue
9
+ }) => {
10
+ const [value, setValue] = useState(initialValue);
11
+ const inputRef = useRef(null);
12
+
13
+ useEffect(() => {
14
+ if (inputRef.current) {
15
+ inputRef.current.focus();
16
+ }
17
+ }, [isNumeric]);
18
+
19
+ return (
20
+ <input
21
+ style={{
22
+ border: 0,
23
+ width: "95%",
24
+ fontSize: 12,
25
+ background: "none"
26
+ }}
27
+ ref={inputRef}
28
+ {...dataTest}
29
+ autoFocus
30
+ onKeyDown={e => {
31
+ e.stopPropagation();
32
+ if (e.key === "Enter") {
33
+ e.target.blur();
34
+ } else if (e.key === "Escape") {
35
+ cancelEdit();
36
+ }
37
+ }}
38
+ onBlur={() => finishEdit(value)}
39
+ onChange={e => setValue(e.target.value)}
40
+ type={isNumeric ? "number" : undefined}
41
+ value={value}
42
+ />
43
+ );
44
+ };
@@ -166,7 +166,7 @@ const DataTable = ({
166
166
  const {
167
167
  reduxFormCellValidation: _reduxFormCellValidation,
168
168
  reduxFormEditingCell,
169
- reduxFormEntities: _reduxFormEntities,
169
+ reduxFormEntities,
170
170
  reduxFormQueryParams: _reduxFormQueryParams = {},
171
171
  reduxFormSearchInput: _reduxFormSearchInput = "",
172
172
  reduxFormSelectedEntityIdMap: _reduxFormSelectedEntityIdMap = {}
@@ -184,7 +184,6 @@ const DataTable = ({
184
184
  // We want to make sure we don't rerender everything unnecessary
185
185
  // with redux-forms we tend to do unnecessary renders
186
186
  const reduxFormCellValidation = useDeepEqualMemo(_reduxFormCellValidation);
187
- const reduxFormEntities = useDeepEqualMemo(_reduxFormEntities);
188
187
  const reduxFormQueryParams = useDeepEqualMemo(_reduxFormQueryParams);
189
188
  const reduxFormSearchInput = useDeepEqualMemo(_reduxFormSearchInput);
190
189
  const reduxFormSelectedEntityIdMap = useDeepEqualMemo(
@@ -839,8 +838,7 @@ const DataTable = ({
839
838
  }
840
839
  }));
841
840
  },
842
- // eslint-disable-next-line react-hooks/exhaustive-deps
843
- []
841
+ [change]
844
842
  );
845
843
 
846
844
  const formatAndValidateEntities = useCallback(
@@ -1472,8 +1470,7 @@ const DataTable = ({
1472
1470
  // "formats", not "changes".
1473
1471
  useEffect(() => {
1474
1472
  const formatAndValidateTableInitial = () => {
1475
- const { newEnts, validationErrors } =
1476
- formatAndValidateEntities(_origEntities);
1473
+ const { newEnts, validationErrors } = formatAndValidateEntities(entities);
1477
1474
  const toKeep = {};
1478
1475
  //on the initial load we want to keep any async table wide errors
1479
1476
  forEach(reduxFormCellValidation, (v, k) => {
@@ -1481,7 +1478,12 @@ const DataTable = ({
1481
1478
  toKeep[k] = v;
1482
1479
  }
1483
1480
  });
1484
- change("reduxFormEntities", newEnts);
1481
+ change("reduxFormEntities", prev => {
1482
+ if (!isEqual(prev, newEnts)) {
1483
+ return newEnts;
1484
+ }
1485
+ return prev;
1486
+ });
1485
1487
  updateValidation(newEnts, {
1486
1488
  ...toKeep,
1487
1489
  ...validationErrors
@@ -1489,10 +1491,10 @@ const DataTable = ({
1489
1491
  };
1490
1492
  isCellEditable && formatAndValidateTableInitial();
1491
1493
  }, [
1492
- _origEntities,
1493
- isCellEditable,
1494
1494
  change,
1495
+ entities,
1495
1496
  formatAndValidateEntities,
1497
+ isCellEditable,
1496
1498
  reduxFormCellValidation,
1497
1499
  updateValidation
1498
1500
  ]);