Skip to content

Commit 492a447

Browse files
authored
enterprise update 3 (#76)
* Add new FilterType and getBaseType function to TableUtils Cherry-picked from defcd686b4 * Add basic paste functionality to an input grid Cherry-picked from 9192cb71a6
1 parent b6ce6e7 commit 492a447

20 files changed

+712
-13
lines changed

packages/code-studio/package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,6 @@
6060
"react-virtualized-auto-sizer": "^1.0.2",
6161
"react-window": "^1.8.5",
6262
"reactstrap": "^8.4.1",
63-
"reduce-reducers": "^1.0.1",
6463
"redux": "^4.0.5",
6564
"redux-thunk": "^2.3.0",
6665
"shortid": "^2.2.15",

packages/grid/src/Grid.jsx

Lines changed: 102 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,12 @@ import './Grid.scss';
2727
import KeyHandler from './KeyHandler';
2828
import {
2929
EditKeyHandler,
30+
PasteKeyHandler,
3031
SelectionKeyHandler,
3132
TreeKeyHandler,
3233
} from './key-handlers';
3334
import CellInputField from './CellInputField';
35+
import PasteError from './errors/PasteError';
3436

3537
/**
3638
* High performance, extendible, themeable grid component.
@@ -115,6 +117,7 @@ class Grid extends PureComponent {
115117
// specify handler ordering, such that any extensions can insert handlers in between
116118
this.keyHandlers = [
117119
new EditKeyHandler(400),
120+
new PasteKeyHandler(450),
118121
new SelectionKeyHandler(500),
119122
new TreeKeyHandler(900),
120123
];
@@ -934,6 +937,100 @@ class Grid extends PureComponent {
934937
return model.isValidForCell(modelColumn, modelRow, value);
935938
}
936939

940+
/**
941+
* Paste a value with the current selection
942+
* It first needs to validate that the pasted table is valid for the given selection.
943+
* Also may update selection if single cells are selected and a table is pasted.
944+
* @param {string[][] | string} value Table or a string that is being pasted
945+
*/
946+
async pasteValue(value) {
947+
const { model } = this.props;
948+
const { movedColumns, movedRows, selectedRanges } = this.state;
949+
950+
try {
951+
if (
952+
!model.isEditable ||
953+
!selectedRanges.every(range => model.isEditableRange(range))
954+
) {
955+
throw new PasteError("Can't paste in to read-only area.");
956+
}
957+
958+
if (selectedRanges.length <= 0) {
959+
throw new PasteError('Select an area to paste to.');
960+
}
961+
962+
if (typeof value === 'string') {
963+
// Just paste the value into all the selected cells
964+
const edits = [];
965+
966+
const modelRanges = GridUtils.getModelRanges(
967+
selectedRanges,
968+
movedColumns,
969+
movedRows
970+
);
971+
GridRange.forEachCell(modelRanges, (x, y) => {
972+
edits.push({ x, y, text: value });
973+
});
974+
await model.setValues(edits);
975+
return;
976+
}
977+
978+
// Otherwise it's a table of data
979+
const tableHeight = value.length;
980+
const tableWidth = value[0].length;
981+
const { columnCount, rowCount } = model;
982+
let ranges = selectedRanges;
983+
// If each cell is a single selection, we need to update the selection to map to the newly pasted data
984+
if (
985+
ranges.every(
986+
range =>
987+
GridRange.cellCount([range]) === 1 &&
988+
range.startColumn + tableWidth <= columnCount &&
989+
range.startRow + tableHeight <= rowCount
990+
)
991+
) {
992+
// Remap the selected ranges
993+
ranges = ranges.map(
994+
range =>
995+
new GridRange(
996+
range.startColumn,
997+
range.startRow,
998+
range.startColumn + tableWidth - 1,
999+
range.startRow + tableHeight - 1
1000+
)
1001+
);
1002+
this.setSelectedRanges(ranges);
1003+
}
1004+
1005+
if (
1006+
!ranges.every(
1007+
range =>
1008+
GridRange.rowCount([range]) === tableHeight &&
1009+
GridRange.columnCount([range]) === tableWidth
1010+
)
1011+
) {
1012+
throw new PasteError('Copy and paste area are not same size.');
1013+
}
1014+
1015+
const edits = [];
1016+
ranges.forEach(range => {
1017+
for (let x = 0; x < tableWidth; x += 1) {
1018+
for (let y = 0; y < tableHeight; y += 1) {
1019+
edits.push({
1020+
x: range.startColumn + x,
1021+
y: range.startRow + y,
1022+
text: value[y][x],
1023+
});
1024+
}
1025+
}
1026+
});
1027+
await model.setValues(edits);
1028+
} catch (e) {
1029+
const { onError } = this.props;
1030+
onError(e);
1031+
}
1032+
}
1033+
9371034
setValueForCell(column, row, value) {
9381035
const { model } = this.props;
9391036

@@ -1117,8 +1214,8 @@ class Grid extends PureComponent {
11171214
const keyHandler = keyHandlers[i];
11181215
const result = keyHandler.onDown(e, this);
11191216
if (result) {
1120-
e.stopPropagation();
1121-
e.preventDefault();
1217+
if (result?.stopPropagation ?? true) e.stopPropagation();
1218+
if (result?.preventDefault ?? true) e.preventDefault();
11221219
break;
11231220
}
11241221
}
@@ -1494,7 +1591,7 @@ class Grid extends PureComponent {
14941591
onMouseDown={this.handleMouseDown}
14951592
onMouseMove={this.handleMouseMove}
14961593
onMouseLeave={this.handleMouseLeave}
1497-
tabIndex="0"
1594+
tabIndex={0}
14981595
>
14991596
Your browser does not support HTML canvas. Update your browser?
15001597
</canvas>
@@ -1524,6 +1621,7 @@ Grid.propTypes = {
15241621
to: PropTypes.number.isRequired,
15251622
})
15261623
),
1624+
onError: PropTypes.func,
15271625
onSelectionChanged: PropTypes.func,
15281626
onMovedColumnsChanged: PropTypes.func,
15291627
onMoveColumnComplete: PropTypes.func,
@@ -1545,6 +1643,7 @@ Grid.defaultProps = {
15451643
mouseHandlers: [],
15461644
movedColumns: [],
15471645
movedRows: [],
1646+
onError: () => {},
15481647
onSelectionChanged: () => {},
15491648
onMovedColumnsChanged: () => {},
15501649
onMoveColumnComplete: () => {},

packages/grid/src/Grid.test.jsx

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,8 @@ const defaultTheme = { ...GridTheme, autoSizeColumns: false };
4242

4343
const VIEW_SIZE = 5000;
4444

45+
const DEFAULT_PASTE_DATA = 'TEST_PASTE_DATA';
46+
4547
function makeMockCanvas() {
4648
return {
4749
clientWidth: VIEW_SIZE,
@@ -220,6 +222,10 @@ function pageDown(component, extraArgs) {
220222
keyDown('PageDown', component, extraArgs);
221223
}
222224

225+
function paste(component, data = DEFAULT_PASTE_DATA) {
226+
component.pasteValue(data);
227+
}
228+
223229
it('renders default model without crashing', () => {
224230
makeGridComponent(new GridModel());
225231
});
@@ -755,3 +761,53 @@ describe('truncate to width', () => {
755761
expectTruncate(MockGridData.JSON, '{"command…');
756762
});
757763
});
764+
765+
describe('paste tests', () => {
766+
describe('non-editable', () => {
767+
it('does nothing if table is not editable', () => {
768+
const model = new MockGridModel();
769+
model.setValues = jest.fn();
770+
771+
const component = makeGridComponent(model);
772+
paste(component);
773+
expect(model.setValues).not.toHaveBeenCalled();
774+
});
775+
});
776+
777+
describe('editable', () => {
778+
let model = null;
779+
let component = null;
780+
781+
beforeEach(() => {
782+
model = new MockGridModel({ isEditable: true });
783+
model.setValues = jest.fn();
784+
785+
component = makeGridComponent(model);
786+
});
787+
788+
it('does nothing if no selection', () => {
789+
paste(component);
790+
expect(model.setValues).not.toHaveBeenCalled();
791+
});
792+
793+
it('modifies a single cell if only one selection', () => {
794+
mouseClick(5, 7, component);
795+
paste(component);
796+
expect(model.setValues).toHaveBeenCalledTimes(1);
797+
expect(model.setValues).toHaveBeenCalledWith([
798+
expect.objectContaining({
799+
x: 5,
800+
y: 7,
801+
text: DEFAULT_PASTE_DATA,
802+
}),
803+
]);
804+
});
805+
806+
it('does the whole selected range', () => {
807+
mouseClick(5, 7, component);
808+
mouseClick(3, 2, component, { shiftKey: true });
809+
paste(component);
810+
expect(model.setValues).toHaveBeenCalledTimes(1);
811+
});
812+
});
813+
});

packages/grid/src/GridModel.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,18 @@ class GridModel extends EventTarget {
136136
throw new Error('setValueForRanges not implemented');
137137
}
138138

139+
/**
140+
* Apply edits to the model
141+
* @param {{
142+
* x: number,
143+
* y: number,
144+
* text: string,
145+
* }[]} edits The edits to apply to the model
146+
*/
147+
async setValues(edits) {
148+
throw new Error('setValues not implemented');
149+
}
150+
139151
/**
140152
* Check if a text value is a valid edit for a cell
141153
* @param {number} x The column to check

packages/grid/src/GridRange.js

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -496,6 +496,19 @@ class GridRange {
496496
);
497497
}
498498

499+
/**
500+
* Count the number of columns in the provided grid ranges
501+
* @param {GridRange[]} ranges The ranges to count the columns of
502+
* @returns {number|NaN} The number of columns in the ranges, or `NaN` if any of the ranges were unbounded
503+
*/
504+
static columnCount(ranges) {
505+
return ranges.reduce(
506+
(columnCount, range) =>
507+
columnCount + (range.endColumn ?? NaN) - (range.startColumn ?? NaN) + 1,
508+
0
509+
);
510+
}
511+
499512
/**
500513
* Check if the provided ranges contain the provided cell
501514
* @param {GridRange[]} ranges The ranges to check

packages/grid/src/KeyHandler.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,12 @@
88
// eslint-disable-next-line import/no-cycle
99
import Grid from './Grid';
1010

11+
// True if consumed and to stop event propagation/prevent default, false if not consumed.
12+
// OR an object if consumed with boolean properties to control whether to stopPropagation/preventDefault
13+
export type KeyHandlerResponse =
14+
| boolean
15+
| { stopPropagation?: boolean; preventDefault?: boolean };
16+
1117
class KeyHandler {
1218
private order;
1319

@@ -21,9 +27,9 @@ class KeyHandler {
2127
* Handle a keydown event on the grid.
2228
* @param event The keyboard event
2329
* @param grid The grid component the key press is on
24-
* @returns True if consumed and to stop event propagation/prevent default, false if not consumed.
30+
* @returns Response indicating if the key was consumed
2531
*/
26-
onDown(event: KeyboardEvent, grid: Grid): boolean {
32+
onDown(event: KeyboardEvent, grid: Grid): KeyHandlerResponse {
2733
return false;
2834
}
2935
}

packages/grid/src/MockGridModel.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,13 @@ class MockGridModel extends GridModel {
8686
});
8787
}
8888

89+
async setValues(edits) {
90+
for (let i = 0; i < edits.length; i += 1) {
91+
const edit = edits[i];
92+
this.setValueForCell(edit.x, edit.y, edit.text);
93+
}
94+
}
95+
8996
editValueForCell(x, y) {
9097
return this.textForCell(x, y);
9198
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
class PasteError extends Error {
2+
isPasteError = true;
3+
}
4+
5+
export default PasteError;

packages/grid/src/errors/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
// eslint-disable-next-line import/prefer-default-export
2+
export { default as PasteError } from './PasteError';

packages/grid/src/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,6 @@ export { default as GridUtils } from './GridUtils';
1010
export { default as KeyHandler } from './KeyHandler';
1111
export { default as MockGridModel } from './MockGridModel';
1212
export { default as MockTreeGridModel } from './MockTreeGridModel';
13+
export * from './key-handlers';
14+
export * from './mouse-handlers';
15+
export * from './errors';

0 commit comments

Comments
 (0)