Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

#9553: Improving readability of long attribute values in attribute table and table widgets #9701

Merged
merged 8 commits into from
Nov 21, 2023
2 changes: 1 addition & 1 deletion web/client/components/data/featuregrid/formatters/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import NumberFormat from '../../../I18N/Number';
import { dateFormats as defaultDateFormats } from "../../../../utils/FeatureGridUtils";

const BooleanFormatter = ({value} = {}) => !isNil(value) ? <span>{value.toString()}</span> : null;
const StringFormatter = ({value} = {}) => !isNil(value) ? reactStringReplace(value, /(https?:\/\/\S+)/g, (match, i) => (
export const StringFormatter = ({value} = {}) => !isNil(value) ? reactStringReplace(value, /(https?:\/\/\S+)/g, (match, i) => (
<a key={match + i} href={match} target={"_blank"}>{match}</a>
)) : null;
const NumberFormatter = ({value} = {}) => !isNil(value) ? <NumberFormat value={value} numberParams={{maximumFractionDigits: 17}}/> : null;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
/**
* Copyright 2023, GeoSolutions Sas.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree.
*/

import expect from 'expect';

import React from 'react';
import ReactDOM from 'react-dom';
import { handleLongTextEnhancer } from '../handleLongTextEnhancer';
import { StringFormatter } from '../../../data/featuregrid/formatters';

describe("handleLongTextEnhancer enhancer", () => {
beforeEach((done) => {
document.body.innerHTML = '<div id="container"></div>';
setTimeout(done);
});

afterEach((done) => {
ReactDOM.unmountComponentAtNode(document.getElementById("container"));
document.body.innerHTML = '';
setTimeout(done);
});

it('handleLongTextEnhancer by passing formatter as wrapper', () => {
const EnhancerWithFormatter = ()=> handleLongTextEnhancer(StringFormatter)({ value: "test12334567899999" });
ReactDOM.render(
<EnhancerWithFormatter />,
document.getElementById("container")
);
expect(document.getElementById("container").innerHTML).toExist();
expect(document.getElementsByTagName('span').length).toEqual(2);
expect(document.getElementsByTagName('span')[1].innerHTML).toExist();
});

it('handleLongTextEnhancer with by passing td as wrapper', () => {
const wrapper = () => (<td>15234568965</td>);
const EnhancerWithFormatter = ()=> handleLongTextEnhancer(wrapper)({ value: "15234568965" });
ReactDOM.render(
<EnhancerWithFormatter />,
document.getElementById("container")
);
expect(document.getElementById("container").innerHTML).toExist();
expect(document.getElementsByTagName('span').length).toEqual(2);
expect(document.getElementsByTagName('span')[1].innerHTML).toExist();
});


it('handleLongTextEnhancer with by passing span as wrapper', () => {
const wrapper = () => (<span>15234568965</span>);
const EnhancerWithFormatter = ()=> handleLongTextEnhancer(wrapper)({ value: "15234568965" });
ReactDOM.render(
<EnhancerWithFormatter />,
document.getElementById("container")
);
expect(document.getElementById("container").innerHTML).toExist();
expect(document.getElementsByTagName('span').length).toEqual(3);
expect(document.getElementsByTagName('span')[1].innerHTML).toExist();
});


it('handleLongTextEnhancer with by passing td div wrapper', () => {
const wrapper = () => (<div>test</div>);
const EnhancerWithFormatter = ()=> handleLongTextEnhancer(wrapper)({ value: "test" });
ReactDOM.render(
<EnhancerWithFormatter />,
document.getElementById("container")
);
expect(document.getElementById("container").innerHTML).toExist();
expect(document.getElementsByTagName('span').length).toEqual(2);
expect(document.getElementsByTagName('span')[1].innerHTML).toExist();
});
});
50 changes: 50 additions & 0 deletions web/client/components/misc/enhancers/handleLongTextEnhancer.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/*
* Copyright 2023, GeoSolutions Sas.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree.
*/

import React from "react";
import OverlayTrigger from "../OverlayTrigger";
import { Tooltip } from "react-bootstrap";
/**
* handleLongTextEnhancer enhancer. Enhances a long text content by adding a tooltip.
* @type {function}
* @name handleLongTextEnhancer
* @memberof components.misc.enhancers
* Wraps [wrapped component with content] to add tooltip for long content if shown content less than the main content
* @param {Component} Wrapped the component wrapped with a tooltip when its content is too long
* @param {object} props the props that contains value content
* @return {Component} the rendered component that renders the content with the tooltip if the content is long or renders the content with no tooltip if not long
* @example
* const wrapper = () = > <span>testtttttttttt</span>
* const Component = ()=> handleLongTextEnhancer(wrapper)(props);
* render (){
* return <Component />
* }
*
*/
export const handleLongTextEnhancer = (Wrapped) => (props) => {
const cellRef = React.useRef(null);
const contentRef = React.useRef(null);
const [isContentOverflowing, setIsContentOverflowing] = React.useState(false);

const handleMouseEnter = () => {
const cellWidth = cellRef.current.offsetWidth;
const contentWidth = contentRef.current.offsetWidth;
setIsContentOverflowing(contentWidth > cellWidth);
};

return (<OverlayTrigger
placement="top"
overlay={isContentOverflowing ? <Tooltip id="tooltip">{<Wrapped {...props} />}</Tooltip> : <></>}
>
<div ref={cellRef} onMouseEnter={handleMouseEnter}>
<span ref={contentRef}>
<span>{<Wrapped {...props} />}</span>
</span>
</div>
</OverlayTrigger>);
};
4 changes: 3 additions & 1 deletion web/client/utils/FeatureGridUtils.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
} from './ogc/WFS/base';

import { applyDefaultToLocalizedString } from '../components/I18N/LocalizedString';
import { handleLongTextEnhancer } from '../components/misc/enhancers/handleLongTextEnhancer';

const getGeometryName = (describe) => get(findGeometryProperty(describe), "name");
const getPropertyName = (name, describe) => name === "geometry" ? getGeometryName(describe) : name;
Expand Down Expand Up @@ -115,6 +116,7 @@ export const getCurrentPaginationOptions = ({ startPage, endPage }, oldPages, si
return { startIndex: nPs[0] * size, maxFeatures: needPages * size };
};


/**
* Utility function to get from a describeFeatureType response the columns to use in the react-data-grid
* @param {object} describe describeFeatureType response
Expand Down Expand Up @@ -146,7 +148,7 @@ export const featureTypeToGridColumns = (
editable,
filterable,
editor: getEditor(desc, field),
formatter: getFormatter(desc, field),
formatter: handleLongTextEnhancer(getFormatter(desc, field)),
filterRenderer: getFilterRenderer(desc, field)
};
});
Expand Down
35 changes: 35 additions & 0 deletions web/client/utils/__tests__/FeatureGridUtils-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,10 @@
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree.
*/
import React from "react";
import ReactDOM from "react-dom";
import expect from 'expect';

import {
updatePages,
gridUpdateToQueryUpdate,
Expand All @@ -18,6 +21,18 @@ import {


describe('FeatureGridUtils', () => {
beforeEach((done) => {
document.body.innerHTML = '<div id="container"></div>';
setTimeout(done);
});

afterEach((done) => {
ReactDOM.unmountComponentAtNode(
document.getElementById("container")
);
document.body.innerHTML = "";
setTimeout(done);
});
it('Test updatePages when needPages * size is less then features', () => {
const oldFeatures = Array(350);
const features = Array(60);
Expand Down Expand Up @@ -332,6 +347,26 @@ describe('FeatureGridUtils', () => {
// test localized alias with empty default
expect(featureTypeToGridColumns(describe, columnSettings, [{name: "Test1", alias: {"default": ""}}])[0].title.default).toEqual('Test1');

});
it('featureTypeToGridColumns formatters', () => {
const DUMMY = () => {};
const formatterWrapper = () => (<div>testtttt</div>);
const describe = {featureTypes: [{properties: [{name: 'Test1', type: "xsd:number"}, {name: 'Test2', type: "xsd:number"}]}]};
const columnSettings = {name: 'Test1', hide: false};
const options = [{name: 'Test1', title: 'Some title', description: 'Some description'}];
const featureGridColumns = featureTypeToGridColumns(describe, columnSettings, [], {options}, {getHeaderRenderer: () => DUMMY, getFilterRenderer: () => DUMMY, getFormatter: () => formatterWrapper, getEditor: () => DUMMY});
expect(featureGridColumns.length).toBe(2);
featureGridColumns.forEach((fgColumns)=>{
const Formatter = fgColumns.formatter;
ReactDOM.render(
<Formatter/>,
document.getElementById("container")
);
expect(document.getElementById("container").innerHTML).toExist();
expect(document.getElementsByTagName('span').length).toEqual(2);
expect(document.getElementsByTagName('span')[1].innerHTML).toExist();
});

});
describe("supportsFeatureEditing", () => {
it('test supportsFeatureEditing with valid layer type', () => {
Expand Down
Loading