Skip to content

Commit

Permalink
#477: tooltips for long strings and edited cell textarea
Browse files Browse the repository at this point in the history
  • Loading branch information
aschonfeld committed May 21, 2021
1 parent d4e6bca commit 0582d3a
Show file tree
Hide file tree
Showing 33 changed files with 803 additions and 159 deletions.
29 changes: 27 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ D-Tale was the product of a SAS to Python conversion. What was originally a per
- [UI](#ui)
- [Dimensions/Ribbon Menu/Main Menu](#dimensionsribbon-menumain-menu)
- [Header](#header)
- Resize Columns (#resize-columns)
- [Editing Cells](#editing-cells)
- [Copy Cells Into Clipboard](#copy-cells-into-clipboard)
- [Main Menu Functions](#main-menu-functions)
Expand Down Expand Up @@ -623,11 +624,30 @@ When performing multiple of the same operation the description will become too l
|-----|-------|--------------|
|![](https://raw.githubusercontent.com/aschonfeld/dtale-media/master/images/header/sorts.PNG)|![](https://raw.githubusercontent.com/aschonfeld/dtale-media/master/images/header/filters.PNG)|![](https://raw.githubusercontent.com/aschonfeld/dtale-media/master/images/header/hidden.PNG)|

### Resize Columns

Currently there are two ways which you can resize columns.
* Dragging the right border of the column's header cell.

![](https://raw.githubusercontent.com/aschonfeld/dtale-media/master/gifs/resize_columns_w_drag.gif)

* Altering the "Maximum Column Width" property from the ribbon menu.

![](https://raw.githubusercontent.com/aschonfeld/dtale-media/master/gifs/resize_columns_max_width.gif)

* __Side Note:__ You can also set the `max_column_width` property ahead of time in your [global configuration](https://github.com/man-group/dtale/blob/master/docs/CONFIGURATION.md) or programmatically using:

```python
import dtale.global_state as global_state

global_state.set_app_settings(dict(max_column_width=100))
```

### Editing Cells

You may edit any cells in your grid (with the exception of the row indexes or headers, the ladder can be edited using the [Rename](#rename) column menu function).

In order to eddit a cell simply double-click on it. This will convert it into a text-input field and you should see a blinking cursor. It is assumed that the value you type in will match the data type of the column you editing. For example:
In order to edit a cell simply double-click on it. This will convert it into a text-input field and you should see a blinking cursor. In addition to turning that cell into an input it will also display an input at the top of the screen for better viewing of long strings. It is assumed that the value you type in will match the data type of the column you editing. For example:

* integers -> should be a valid positive or negative integer
* float -> should be a valid positive or negative float
Expand All @@ -647,7 +667,12 @@ If there is a conversion issue with the value you have entered it will display a

Here's a quick demo:

[![](http://img.youtube.com/vi/MY5w0m_4IAc/0.jpg)](http://www.youtube.com/watch?v=MY5w0m_4IAc "Editing Cells")
[![](http://img.youtube.com/vi/MY5w0m_4IAc/0.jpg)](http://www.youtube.com/watch?v=MY5w0m_4IAc "Editing Long String Cells")

Here's a demo of editing cells with long strings:

[![](http://img.youtube.com/vi/3p9ltzdBaDQ/0.jpg)](http://www.youtube.com/watch?v=3p9ltzdBaDQ "Editing Cells")


### Copy Cells Into Clipboard

Expand Down
22 changes: 16 additions & 6 deletions dtale/static/css/main.css
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,12 @@
visibility: hidden;
}

#textarea-measure {
position: absolute;
float: left;
visibility: hidden;
}

body.dark-mode,
div.preview.dark-mode {
background-color: black !important;
Expand Down Expand Up @@ -10459,10 +10465,12 @@ div.hoverable.label > div.hoverable__content {
transform: rotate(270deg);
}
.hoverable__content.menu-description.bottom::before {
left: -0.7em;
top: calc(100% - 0.8em);
right: inherit;
transform: rotate(270deg);
}
.hoverable__content.menu-description.right::before {
left: unset;
right: -0.3em;
transform: rotate(90deg);
}
.hoverable__content.edit-cell::before {
left: -0.7em;
Expand Down Expand Up @@ -10520,10 +10528,12 @@ div.hoverable.label > div.hoverable__content {
transform: rotate(270deg);
}
.hoverable__content.menu-description.bottom::after {
left: -0.6em;
top: calc(100% - 0.8em);
right: inherit;
transform: rotate(270deg);
}
.hoverable__content.menu-description.right::after {
left: unset;
right: -0.2em;
transform: rotate(90deg);
}
.hoverable__content.edit-cell::after {
left: -0.6em;
Expand Down
12 changes: 12 additions & 0 deletions dtale/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -1246,6 +1246,18 @@ def update_language():
return jsonify(dict(success=True))


@dtale.route("/update-maximum-column-width")
@exception_decorator
def update_maximum_column_width():
width = get_str_arg(request, "width")
if width:
width = int(width)
curr_app_settings = global_state.get_app_settings()
curr_app_settings["max_column_width"] = width
global_state.set_app_settings(curr_app_settings)
return jsonify(dict(success=True))


@dtale.route("/update-formats/<data_id>")
@exception_decorator
def update_formats(data_id):
Expand Down
27 changes: 27 additions & 0 deletions static/__tests__/dtale/GridEventHandler-test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ describe("RibbonDropdown", () => {
setRibbonVisibility: jest.fn(),
ribbonMenuOpen: false,
ribbonDropdownOpen: false,
showTooltip: jest.fn(),
hideTooltip: jest.fn(),
};
wrapper = shallow(<ReactGridEventHandler {...props} />);
});
Expand Down Expand Up @@ -55,4 +57,29 @@ describe("RibbonDropdown", () => {
wrapper.setProps({ dragResize: 5 });
expect(wrapper.find("div.blue-line")).toHaveLength(1);
});

it("hides tooltip when cellIdx is empty", () => {
wrapper.find("div").last().props().onMouseOver({ clientY: 100 });
expect(props.hideTooltip).toHaveBeenCalledTimes(1);
});

it("shows tooltip when cellIdx is populated", () => {
const columns = [
{ name: "index", visible: true },
{ name: "a", dtype: "string", index: 1, visible: true },
];
const data = { 0: { a: { raw: "Hello World" } } };
props.gridState = { data, columns };
wrapper = shallow(<ReactGridEventHandler {...props} />);
const target = { attributes: { cell_idx: { nodeValue: "0|1" } } };
wrapper.find("div").last().props().onMouseOver({ target });
expect(props.showTooltip).not.toHaveBeenCalled();
target.attributes.cell_idx.nodeValue = "1|1";
wrapper.find("div").last().props().onMouseOver({ target });
expect(props.showTooltip).not.toHaveBeenCalled();
target.clientWidth = 100;
target.scrollWidth = 150;
wrapper.find("div").last().props().onMouseOver({ target });
expect(props.showTooltip).toHaveBeenLastCalledWith(target, "Hello World");
});
});
102 changes: 102 additions & 0 deletions static/__tests__/dtale/edited/EditedCellInfo-test.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import { mount, shallow } from "enzyme";
import React from "react";
import { Provider } from "react-redux";

import { expect, it } from "@jest/globals";

import serverState from "../../../dtale/serverStateManagement";
import mockPopsicle from "../../MockPopsicle";
import reduxUtils from "../../redux-test-utils";
import { buildInnerHTML, withGlobalJquery } from "../../test-utils";

describe("DataViewerInfo tests", () => {
let EditedCellInfo, ReactEditedCellInfo, store, props;

beforeAll(() => {
const mockBuildLibs = withGlobalJquery(() =>
mockPopsicle.mock(url => {
const { urlFetcher } = require("../../redux-test-utils").default;
return urlFetcher(url);
})
);
jest.mock("popsicle", () => mockBuildLibs);
});

beforeEach(() => {
store = reduxUtils.createDtaleStore();
const components = require("../../../dtale/edited/EditedCellInfo");
EditedCellInfo = components.EditedCellInfo;
ReactEditedCellInfo = components.ReactEditedCellInfo;
buildInnerHTML({ settings: "" }, store);
});

const buildInfo = (additionalProps, editedCell) => {
const columns = [{ name: "a", dtype: "string", index: 1, visible: true }];
const data = { 0: { a: { raw: "Hello World" } } };
props = {
propagateState: jest.fn(),
gridState: { data, columns },
...additionalProps,
};
if (editedCell) {
store.getState().editedCell = editedCell;
}
return mount(
<Provider store={store}>
<EditedCellInfo {...props} />
</Provider>,
{ attachTo: document.getElementById("content") }
);
};

it("EditedCellInfo renders successfully", () => {
const result = buildInfo({});
expect(result.find("div.edited-cell-info")).toHaveLength(1);
});

it("EditedCellInfo renders edited data", () => {
const result = buildInfo({}, "0|1");
expect(result.find("textarea").props().value).toBe("Hello World");
});

it("EditedCellInfo handles updates", () => {
const shallowProps = {
...props,
openChart: jest.fn(),
clearEdit: jest.fn(),
updateHeight: jest.fn(),
};
jest.spyOn(React, "createRef").mockReturnValueOnce({ current: document.createElement("textarea") });
const editCellSpy = jest.spyOn(serverState, "editCell");
editCellSpy.mockImplementation(() => undefined);
const wrapper = shallow(<ReactEditedCellInfo {...shallowProps} />);
wrapper.setProps({ editedCell: "0|1" });
expect(wrapper.find("textarea").props().value).toBe("Hello World");
expect(shallowProps.updateHeight).toHaveBeenCalled();

wrapper.instance().onKeyDown({ key: "Enter" });
expect(shallowProps.clearEdit).toHaveBeenCalledTimes(1);
wrapper.setState({ value: "Hello World2" });
wrapper.instance().onKeyDown({ key: "Enter" });
expect(editCellSpy).toHaveBeenCalledTimes(1);
jest.restoreAllMocks();
});

it("handles save errors", () => {
const editCellSpy = jest.spyOn(serverState, "editCell");
editCellSpy.mockImplementation(() => undefined);
const result = buildInfo({}, "0|1");
expect(result.find("textarea").props().value).toBe("Hello World");

result.find("textarea").simulate("change", { target: { value: "Hello World2" } });
expect(result.find(ReactEditedCellInfo).state().value).toBe("Hello World2");
result.find("textarea").simulate("keyPress", { key: "Enter" });
editCellSpy.mock.calls[0][4]({ error: "bad value" });
expect(store.getState().chartData).toEqual({
visible: true,
error: "bad value",
type: "error",
});
jest.restoreAllMocks();
});
});
81 changes: 81 additions & 0 deletions static/__tests__/dtale/menu/MaxWidthOption-test.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { mount } from "enzyme";
import React from "react";
import { Provider } from "react-redux";

import { describe, expect, it } from "@jest/globals";

import { MaxWidthOption, ReactMaxWidthOption } from "../../../dtale/menu/MaxWidthOption";
import serverState from "../../../dtale/serverStateManagement";
import { StyledSlider } from "../../../sliderUtils";
import reduxUtils from "../../redux-test-utils";
import { buildInnerHTML } from "../../test-utils";

describe("MaxWidthOption tests", () => {
let result, store, udpateMaxColumnWidthSpy;

const setupOption = (maxColumnWidth = null) => {
store = reduxUtils.createDtaleStore();
buildInnerHTML({ settings: "", maxColumnWidth }, store);
result = mount(
<Provider store={store}>
<MaxWidthOption />,
</Provider>,
{
attachTo: document.getElementById("content"),
}
);
};

beforeEach(() => {
udpateMaxColumnWidthSpy = jest.spyOn(serverState, "updateMaxColumnWidth");
udpateMaxColumnWidthSpy.mockImplementation(() => undefined);
setupOption();
});

afterEach(jest.resetAllMocks);

afterAll(jest.restoreAllMocks);

it("renders successfully with defaults", () => {
expect(result.find("i.ico-check-box-outline-blank")).toHaveLength(1);
});

it("renders successfully with specified value", () => {
setupOption("55");
expect(result.find("i.ico-check-box")).toHaveLength(1);
expect(result.find("input").props().value).toBe("55");
});

it("handles changes to text input", () => {
result.find("input").simulate("change", { target: { value: "f150" } });
expect(result.find(ReactMaxWidthOption).state().currMaxWidth).toBe(100);
result.find("input").simulate("change", { target: { value: "150" } });
expect(result.find(ReactMaxWidthOption).state().currMaxWidth).toBe(150);
result.find("input").simulate("keyPress", { key: "Enter" });
expect(udpateMaxColumnWidthSpy).toBeCalledTimes(1);
udpateMaxColumnWidthSpy.mock.calls[0][1]();
expect(store.getState().maxColumnWidth).toBe(150);
});

it("handles changes to slider", () => {
result.find(StyledSlider).props().onAfterChange(150);
expect(result.find(ReactMaxWidthOption).state().currMaxWidth).toBe(150);
expect(udpateMaxColumnWidthSpy).toBeCalledTimes(1);
udpateMaxColumnWidthSpy.mock.calls[0][1]();
expect(store.getState().maxColumnWidth).toBe(150);
});

it("handles changes to checkbox", () => {
result.find("i.ico-check-box-outline-blank").simulate("click");
expect(result.find(ReactMaxWidthOption).state().currMaxWidth).toBe(100);
expect(udpateMaxColumnWidthSpy).toBeCalledTimes(1);
udpateMaxColumnWidthSpy.mock.calls[0][1]();
expect(store.getState().maxColumnWidth).toBe(100);
result.update();
result.find("i.ico-check-box").simulate("click");
expect(udpateMaxColumnWidthSpy).toBeCalledTimes(2);
expect(udpateMaxColumnWidthSpy.mock.calls[1][0]).toBe("");
udpateMaxColumnWidthSpy.mock.calls[1][1]();
expect(store.getState().maxColumnWidth).toBe(null);
});
});
34 changes: 34 additions & 0 deletions static/__tests__/dtale/serverStateManagement-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { expect, it } from "@jest/globals";

import * as fetcher from "../../fetcher";

import serverStateManagement from "../../dtale/serverStateManagement";

describe("serverstateManagement", () => {
let fetchJsonSpy;
const callback = () => undefined;

beforeEach(() => {
fetchJsonSpy = jest.spyOn(fetcher, "fetchJson");
fetchJsonSpy.mockImplementation(() => undefined);
});

afterEach(jest.resetAllMocks);

afterAll(jest.restoreAllMocks);

it("updatePinMenu calls right URL", () => {
serverStateManagement.updatePinMenu(true, callback);
expect(fetchJsonSpy).toHaveBeenLastCalledWith("/dtale/update-pin-menu?pinned=true", callback);
});

it("updateLanguage calls right URL", () => {
serverStateManagement.updateLanguage("cn", callback);
expect(fetchJsonSpy).toHaveBeenLastCalledWith("/dtale/update-language?language=cn", callback);
});

it("updateMaxColumnWidth calls right URL", () => {
serverStateManagement.updateMaxColumnWidth(100, callback);
expect(fetchJsonSpy).toHaveBeenLastCalledWith("/dtale/update-maximum-column-width?width=100", callback);
});
});
1 change: 1 addition & 0 deletions static/__tests__/reducers/dtale-test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ describe("reducer tests", () => {
predefinedFilters: [],
maxColumnWidth: null,
dragResize: null,
editedTextAreaHeight: 0,
};
expect(state).toEqual(store.getState());
});
Expand Down
2 changes: 1 addition & 1 deletion static/__tests__/test-utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ function buildInnerHTML(props = {}, store = null) {
`<input type="hidden" id="auth" value="${auth ?? "False"}" />`,
`<input type="hidden" id="username" value="${username ?? ""}" />`,
`<input type="hidden" id="predefined_filters" value="${predefinedFilters ?? "[]"}" />`,
`<input type="hidden" id="max_column_width" value=${maxColumnWidth ?? "null"} />`,
`<input type="hidden" id="max_column_width" value=${maxColumnWidth ?? "None"} />`,
`<div id="content" style="height: 1000px;width: 1000px;" ></div>`,
`<div id="popup-content"></div>`,
`<span id="code-title" />`,
Expand Down
Loading

0 comments on commit 0582d3a

Please sign in to comment.