diff --git a/README.md b/README.md index 2f59196a..647eb15d 100644 --- a/README.md +++ b/README.md @@ -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) @@ -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 @@ -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 diff --git a/dtale/static/css/main.css b/dtale/static/css/main.css index 7a402c33..31eac160 100644 --- a/dtale/static/css/main.css +++ b/dtale/static/css/main.css @@ -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; @@ -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; @@ -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; diff --git a/dtale/views.py b/dtale/views.py index a933160e..8cb92ea7 100644 --- a/dtale/views.py +++ b/dtale/views.py @@ -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/") @exception_decorator def update_formats(data_id): diff --git a/static/__tests__/dtale/GridEventHandler-test.jsx b/static/__tests__/dtale/GridEventHandler-test.jsx index b5c5499f..9cf135d9 100644 --- a/static/__tests__/dtale/GridEventHandler-test.jsx +++ b/static/__tests__/dtale/GridEventHandler-test.jsx @@ -22,6 +22,8 @@ describe("RibbonDropdown", () => { setRibbonVisibility: jest.fn(), ribbonMenuOpen: false, ribbonDropdownOpen: false, + showTooltip: jest.fn(), + hideTooltip: jest.fn(), }; wrapper = shallow(); }); @@ -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(); + 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"); + }); }); diff --git a/static/__tests__/dtale/edited/EditedCellInfo-test.jsx b/static/__tests__/dtale/edited/EditedCellInfo-test.jsx new file mode 100644 index 00000000..fda8a27d --- /dev/null +++ b/static/__tests__/dtale/edited/EditedCellInfo-test.jsx @@ -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( + + + , + { 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(); + 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(); + }); +}); diff --git a/static/__tests__/dtale/menu/MaxWidthOption-test.jsx b/static/__tests__/dtale/menu/MaxWidthOption-test.jsx new file mode 100644 index 00000000..facb0bc5 --- /dev/null +++ b/static/__tests__/dtale/menu/MaxWidthOption-test.jsx @@ -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( + + , + , + { + 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); + }); +}); diff --git a/static/__tests__/dtale/serverStateManagement-test.js b/static/__tests__/dtale/serverStateManagement-test.js new file mode 100644 index 00000000..e77b1d88 --- /dev/null +++ b/static/__tests__/dtale/serverStateManagement-test.js @@ -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); + }); +}); diff --git a/static/__tests__/reducers/dtale-test.jsx b/static/__tests__/reducers/dtale-test.jsx index 8cd4fa8c..48cc9e39 100644 --- a/static/__tests__/reducers/dtale-test.jsx +++ b/static/__tests__/reducers/dtale-test.jsx @@ -49,6 +49,7 @@ describe("reducer tests", () => { predefinedFilters: [], maxColumnWidth: null, dragResize: null, + editedTextAreaHeight: 0, }; expect(state).toEqual(store.getState()); }); diff --git a/static/__tests__/test-utils.js b/static/__tests__/test-utils.js index 41aeefff..da9bc9e2 100644 --- a/static/__tests__/test-utils.js +++ b/static/__tests__/test-utils.js @@ -57,7 +57,7 @@ function buildInnerHTML(props = {}, store = null) { ``, ``, ``, - ``, + ``, `
`, ``, ``, diff --git a/static/actions/dtale.js b/static/actions/dtale.js index 0c7cea5e..1e99d4d1 100644 --- a/static/actions/dtale.js +++ b/static/actions/dtale.js @@ -85,6 +85,20 @@ function updateFilteredRanges(query) { }; } +function updateMaxWidth(width) { + return dispatch => { + dispatch({ type: "update-max-width", width }); + dispatch({ type: "data-viewer-update", update: { type: "update-max-width", width } }); + }; +} + +function clearMaxWidth() { + return dispatch => { + dispatch({ type: "clear-max-width" }); + dispatch({ type: "data-viewer-update", update: { type: "update-max-width", width: null } }); + }; +} + export default { init, toggleColumnMenu, @@ -97,4 +111,6 @@ export default { setTheme, setLanguage, updateFilteredRanges, + updateMaxWidth, + clearMaxWidth, }; diff --git a/static/dtale/DataViewer.jsx b/static/dtale/DataViewer.jsx index ea54b726..b6873cd9 100644 --- a/static/dtale/DataViewer.jsx +++ b/static/dtale/DataViewer.jsx @@ -22,6 +22,7 @@ import { DataViewerMenu } from "./menu/DataViewerMenu"; import * as reduxUtils from "./reduxGridUtils"; import { RibbonDropdown } from "./ribbon/RibbonDropdown"; import { RibbonMenu } from "./ribbon/RibbonMenu"; +import { EditedCellInfo } from "./edited/EditedCellInfo"; require("./DataViewer.css"); const URL_PROPS = ["ids", "sortInfo"]; @@ -221,29 +222,26 @@ class ReactDataViewer extends React.Component { rowCount={this.state.rowCount}> {({ onRowsRendered }) => { this._onRowsRendered = onRowsRendered; - const noInfo = gu.hasNoInfo({ ...this.props.settings, columns: this.state.columns }); return ( this._grid.recomputeGridSize()}> - {({ width, height }) => { - const gridHeight = height - (noInfo ? 3 : 30) - (this.props.ribbonMenuOpen ? 25 : 0); - return ( - <> - - - gu.getColWidth(index, this.state)} - onSectionRendered={this._onSectionRendered} - ref={mg => (this._grid = mg)} - /> - - ); - }} + {({ width, height }) => ( + <> + + + + gu.getColWidth(index, this.state)} + onSectionRendered={this._onSectionRendered} + ref={mg => (this._grid = mg)} + /> + + )} ); }} @@ -279,6 +277,7 @@ ReactDataViewer.propTypes = { dataViewerUpdate: PropTypes.object, clearDataViewerUpdate: PropTypes.func, maxColumnWidth: PropTypes.number, + editedTextAreaHeight: PropTypes.number, }; const ReduxDataViewer = connect(gu.reduxState, gu.reduxDispatch)(ReactDataViewer); export { ReduxDataViewer as DataViewer, ReactDataViewer }; diff --git a/static/dtale/GridCellEditor.jsx b/static/dtale/GridCellEditor.jsx index 6712c747..13ea98ff 100644 --- a/static/dtale/GridCellEditor.jsx +++ b/static/dtale/GridCellEditor.jsx @@ -1,68 +1,36 @@ -import $ from "jquery"; -import _ from "lodash"; import PropTypes from "prop-types"; import React from "react"; import { connect } from "react-redux"; import { openChart } from "../actions/charts"; -import * as gu from "./gridUtils"; -import serverState from "./serverStateManagement"; +import { onKeyDown } from "./edited/editUtils"; class ReactGridCellEditor extends React.Component { constructor(props) { super(props); this.state = { value: props.value || "" }; + this.input = React.createRef(); this.onKeyDown = this.onKeyDown.bind(this); } componentDidMount() { - $(this._input).keydown(this.onKeyDown); - $(this._input).focus(); + this.input.current?.focus(); } onKeyDown(e) { - if (e.key === "Enter") { - const { gridState, colCfg, rowIndex, propagateState, dataId, settings, maxColumnWidth } = this.props; - if (this.props.value === this.state.value) { - this.props.clearEdit(); - return; - } - const { data, columns, columnFormats } = gridState; - const callback = editData => { - if (editData.error) { - this.props.openChart({ ...editData, type: "error" }); - return; - } - const updatedData = _.cloneDeep(data); - updatedData[rowIndex - 1][colCfg.name] = gu.buildDataProps(colCfg, this.state.value, { - columnFormats, - settings, - }); - const width = gu.calcColWidth(colCfg, { - ...gridState, - ...settings, - maxColumnWidth, - }); - const updatedColumns = _.map(columns, c => ({ - ...c, - ...(c.name === colCfg.name ? width : {}), - })); - propagateState({ columns: updatedColumns, data: updatedData, triggerResize: true }, this.props.clearEdit); - }; - serverState.editCell(dataId, colCfg.name, rowIndex - 1, this.state.value, callback); - } else if (e.key === "Escape") { - this.props.clearEdit(); - } + const { colCfg, rowIndex } = this.props; + onKeyDown(e, colCfg, rowIndex, this.state.value, this.props.value, this.props); } render() { return ( (this._input = i)} + ref={this.input} style={{ background: "lightblue", width: "inherit" }} type="text" value={this.state.value} onChange={e => this.setState({ value: e.target.value })} + onKeyPress={this.onKeyDown} /> ); } @@ -71,18 +39,18 @@ ReactGridCellEditor.propTypes = { value: PropTypes.node, colCfg: PropTypes.object, rowIndex: PropTypes.number, - propagateState: PropTypes.func, - openChart: PropTypes.func, - clearEdit: PropTypes.func, - dataId: PropTypes.string, + propagateState: PropTypes.func, // eslint-disable-line react/no-unused-prop-types + openChart: PropTypes.func, // eslint-disable-line react/no-unused-prop-types + clearEdit: PropTypes.func, // eslint-disable-line react/no-unused-prop-types + dataId: PropTypes.string, // eslint-disable-line react/no-unused-prop-types gridState: PropTypes.shape({ data: PropTypes.object, columns: PropTypes.arrayOf(PropTypes.object), sortInfo: PropTypes.arrayOf(PropTypes.array), columnFormats: PropTypes.object, }), - settings: PropTypes.object, - maxColumnWidth: PropTypes.number, + settings: PropTypes.object, // eslint-disable-line react/no-unused-prop-types + maxColumnWidth: PropTypes.number, // eslint-disable-line react/no-unused-prop-types }; const ReduxGridCellEditor = connect( ({ dataId, editedCell, settings, maxColumnWidth }) => ({ diff --git a/static/dtale/GridEventHandler.jsx b/static/dtale/GridEventHandler.jsx index e912ebf5..1fb176fe 100644 --- a/static/dtale/GridEventHandler.jsx +++ b/static/dtale/GridEventHandler.jsx @@ -6,16 +6,9 @@ import { connect } from "react-redux"; import { openChart } from "../actions/charts"; import { MeasureText } from "./MeasureText"; +import { convertCellIdxToCoords, getCell } from "./gridUtils"; import { MenuTooltip } from "./menu/MenuTooltip"; - -import { - buildCopyText, - buildRangeState, - buildRowCopyText, - convertCellIdxToCoords, - toggleSelection, -} from "./rangeSelectUtils"; - +import { buildCopyText, buildRangeState, buildRowCopyText, toggleSelection } from "./rangeSelectUtils"; import { SidePanel } from "./side/SidePanel"; function handleRangeSelect(props, cellIdx) { @@ -106,7 +99,8 @@ class ReactGridEventHandler extends React.Component { } handleMouseOver(e) { - const { rangeSelect, rowRange } = this.props.gridState; + const { gridState } = this.props; + const { rangeSelect, rowRange } = gridState ?? {}; const rangeExists = rangeSelect && rangeSelect.start; const rowRangeExists = rowRange && rowRange.start; const cellIdx = _.get(e, "target.attributes.cell_idx.nodeValue"); @@ -128,6 +122,13 @@ class ReactGridEventHandler extends React.Component { } } else if (rangeExists || rowRangeExists) { this.props.propagateState(buildRangeState()); + } else if (cellIdx && gridState && !_.startsWith(cellIdx, "0|") && !_.endsWith(cellIdx, "|0")) { + const { rec } = getCell(cellIdx, gridState); + if (e.target.clientWidth < e.target.scrollWidth) { + this.props.showTooltip(e.target, rec.raw); + } + } else { + this.props.hideTooltip(); } } @@ -209,24 +210,29 @@ ReactGridEventHandler.propTypes = { sidePanel: PropTypes.string, menuPinned: PropTypes.bool, dragResize: PropTypes.number, + showTooltip: PropTypes.func, + hideTooltip: PropTypes.func, t: PropTypes.func, // eslint-disable-line react/no-unused-prop-types }; const TranslateReactGridEventHandler = withTranslation("main")(ReactGridEventHandler); const ReduxGridEventHandler = connect( - ({ allowCellEdits, dataId, ribbonMenuOpen, ribbonDropdown, sidePanel, menuPinned, dragResize }) => ({ - allowCellEdits, - dataId, - ribbonMenuOpen, - menuPinned, - ribbonDropdownOpen: ribbonDropdown.visible, - sidePanelOpen: sidePanel.visible, - sidePanel: sidePanel.view, - dragResize, + state => ({ + allowCellEdits: state.allowCellEdits, + dataId: state.dataId, + ribbonMenuOpen: state.ribbonMenuOpen, + menuPinned: state.menuPinned, + ribbonDropdownOpen: state.ribbonDropdown.visible, + sidePanelOpen: state.sidePanel.visible, + sidePanel: state.sidePanel.view, + dragResize: state.dragResize, + hoveredValue: state.hoveredValue, }), dispatch => ({ openChart: chartProps => dispatch(openChart(chartProps)), editCell: editedCell => dispatch({ type: "edit-cell", editedCell }), setRibbonVisibility: show => dispatch({ type: `${show ? "show" : "hide"}-ribbon-menu` }), + showTooltip: (element, content) => dispatch({ type: "show-menu-tooltip", element, content }), + hideTooltip: () => dispatch({ type: "hide-menu-tooltip" }), }) )(TranslateReactGridEventHandler); diff --git a/static/dtale/edited/EditedCellInfo.jsx b/static/dtale/edited/EditedCellInfo.jsx new file mode 100644 index 00000000..a67e824d --- /dev/null +++ b/static/dtale/edited/EditedCellInfo.jsx @@ -0,0 +1,103 @@ +import PropTypes from "prop-types"; +import React from "react"; +import { withTranslation } from "react-i18next"; +import { connect } from "react-redux"; + +import { openChart } from "../../actions/charts"; +import { getCell } from "../gridUtils"; +import { onKeyDown } from "./editUtils"; + +require("./EditedCellInfo.scss"); + +function buildState(props) { + const { editedCell, gridState } = props; + if (editedCell === null) { + return { value: null, rowIndex: null, colCfg: null, origValue: null }; + } + const { rec, colCfg, rowIndex } = getCell(editedCell, gridState); + return { value: rec.raw, rowIndex, colCfg, origValue: rec.raw }; +} + +class ReactEditedCellInfo extends React.Component { + constructor(props) { + super(props); + this.state = buildState(props); + this.input = React.createRef(); + this.onKeyDown = this.onKeyDown.bind(this); + } + + componentDidUpdate(prevProps, prevState) { + if (prevProps.editedCell !== this.props.editedCell) { + this.setState(buildState(this.props)); + } + if (this.state.origValue && this.state.origValue !== prevState.origValue) { + const ref = this.input.current; + ref.style.height = "0px"; + ref.style.height = `${ref.scrollHeight}px`; + ref.focus(); + this.props.updateHeight(ref.scrollHeight + 25); + } + } + + onKeyDown(e) { + const { colCfg, rowIndex, value, origValue } = this.state; + onKeyDown(e, colCfg, rowIndex, value, origValue, this.props); + } + + render() { + const { editedCell } = this.props; + const { colCfg, rowIndex } = this.state; + return ( +
+
+ Editing Cell + [Column: + {colCfg?.name} + , Row: + {rowIndex - 1} + ] + (Press ENTER to submit or ESC to exit) +