diff --git a/client/__tests__/e2e/__snapshots__/e2e.test.js.snap b/client/__tests__/e2e/__snapshots__/e2e.test.js.snap new file mode 100644 index 00000000..9f866bf8 --- /dev/null +++ b/client/__tests__/e2e/__snapshots__/e2e.test.js.snap @@ -0,0 +1,5 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`did launch page launched 1`] = `"pbmc3kc3k"`; + +exports[`metadata loads categories and values from dataset appear 1`] = `"
louvainvain
tint
"`; diff --git a/client/__tests__/e2e/cellxgeneActions.js b/client/__tests__/e2e/cellxgeneActions.js index fb8b12f4..e6b5afb6 100644 --- a/client/__tests__/e2e/cellxgeneActions.js +++ b/client/__tests__/e2e/cellxgeneActions.js @@ -44,9 +44,10 @@ export const cellxgeneActions = (page, utils) => ({ (rows) => Object.fromEntries( rows.map((row) => { - const cat = row.querySelector( - "[data-testclass='categorical-value']" - ).innerText; + const cat = row + .querySelector("[data-testclass='categorical-value']") + .getAttribute("aria-label"); + const count = row.querySelector( "[data-testclass='categorical-value-count']" ).innerText; diff --git a/client/__tests__/e2e/e2e.test.js b/client/__tests__/e2e/e2e.test.js index 6694348e..58b4367e 100644 --- a/client/__tests__/e2e/e2e.test.js +++ b/client/__tests__/e2e/e2e.test.js @@ -27,8 +27,10 @@ afterAll(() => { describe("did launch", () => { test("page launched", async () => { - const element = await utils.getOneElementInnerHTML("[data-testid='header']"); - expect(element).toBe(data.title); + const element = await utils.getOneElementInnerHTML( + "[data-testid='header']" + ); + expect(element).toMatchSnapshot(); }); test("terms of service, if they are there", async () => { @@ -46,10 +48,10 @@ describe("did launch", () => { describe("metadata loads", () => { test("categories and values from dataset appear", async () => { for (const label in data.categorical) { - const categoryName = await utils.getOneElementInnerText( + const elem = await utils.getOneElementInnerHTML( `[data-testid="category-${label}"]` ); - expect(categoryName).toMatch(label); + expect(elem).toMatchSnapshot(); await utils.clickOn(`${label}:category-expand`); const categories = await cxgActions.getAllCategoriesAndCounts(label); expect(Object.keys(categories)).toMatchObject( diff --git a/client/__tests__/e2e/e2eAnnotations.test.js b/client/__tests__/e2e/e2eAnnotations.test.js index f0041529..3678b872 100644 --- a/client/__tests__/e2e/e2eAnnotations.test.js +++ b/client/__tests__/e2e/e2eAnnotations.test.js @@ -201,16 +201,18 @@ describe.each([ }); async function assertCategoryExists(categoryName) { - const handle = await utils.waitByID(`${categoryName}:category-expand`); - const result = await handle.evaluate((node) => node.innerText); - // slice beginning and end of category name result to account for truncation of long names - expect(result.slice(0, 10)).toBe(categoryName.slice(0, 10)); - expect(result.slice(-10)).toBe(categoryName.slice(-10)); + const handle = await utils.waitByID(`${categoryName}:category-label`); + + const result = await handle.evaluate((node) => + node.getAttribute("aria-label") + ); + + expect(result).toBe(categoryName); } async function assertCategoryDoesNotExist(categoryName) { const result = await page.$( - `[data-testid='${categoryName}:category-expand']` + `[data-testid='${categoryName}:category-label']` ); expect(result).toBeNull(); } @@ -222,7 +224,9 @@ describe.each([ const previous = await utils.waitByID( `categorical-value-${categoryName}-${labelName}` ); - expect(await previous.evaluate((node) => node.innerText)).toBe(labelName); + expect( + await previous.evaluate((node) => node.getAttribute("aria-label")) + ).toBe(labelName); } async function assertLabelDoesNotExist(categoryName, labelName) { diff --git a/client/configuration/eslint/eslint.js b/client/configuration/eslint/eslint.js index 75ffaa2a..5ebc32ef 100644 --- a/client/configuration/eslint/eslint.js +++ b/client/configuration/eslint/eslint.js @@ -21,13 +21,7 @@ module.exports = { "react/jsx-filename-extension": "off", "comma-dangle": "off", "no-underscore-dangle": "off", - quotes: ["error", "double"], "implicit-arrow-linebreak": "off", - "operator-linebreak": [ - "error", - "after", - { overrides: { "?": "before", ":": "before" } }, - ], "no-console": "off", "spaced-comment": ["error", "always", { exceptions: ["*"] }], "no-param-reassign": "off", diff --git a/client/configuration/lint-staged/lint-staged.config.js b/client/configuration/lint-staged/lint-staged.config.js index f96cf216..8d9c56b8 100644 --- a/client/configuration/lint-staged/lint-staged.config.js +++ b/client/configuration/lint-staged/lint-staged.config.js @@ -1,3 +1,3 @@ module.exports = { - "./src/**/*.js": "eslint --fix ./src/", + "./src/**/*.js": "eslint --fix", }; diff --git a/client/src/components/categorical/category/index.js b/client/src/components/categorical/category/index.js index c6f9b292..ab687d97 100644 --- a/client/src/components/categorical/category/index.js +++ b/client/src/components/categorical/category/index.js @@ -2,14 +2,18 @@ import React from "react"; import _ from "lodash"; import { connect } from "react-redux"; import { FaChevronRight, FaChevronDown } from "react-icons/fa"; -import { AnchorButton, Button, Tooltip, Position } from "@blueprintjs/core"; +import { AnchorButton, Button, Tooltip } from "@blueprintjs/core"; import CategoryFlipperLayout from "./categoryFlipperLayout"; import AnnoMenu from "./annoMenuCategory"; import AnnoDialogEditCategoryName from "./annoDialogEditCategoryName"; import AnnoDialogAddLabel from "./annoDialogAddLabel"; +import Truncate from "../../util/truncate"; import * as globals from "../../../globals"; -import maybeTruncateString from "../../../util/maybeTruncateString"; + +const LABEL_WIDTH = globals.leftSidebarWidth - 100; +const ANNO_BUTTON_WIDTH = 50; +const LABEL_WIDTH_ANNO = LABEL_WIDTH - ANNO_BUTTON_WIDTH; @connect((state, ownProps) => { const { metadataField } = ownProps; @@ -114,10 +118,6 @@ class Category extends React.Component { We are still loading this category, so render a "busy" signal. */ const { metadataField } = this.props; - const truncatedString = maybeTruncateString( - metadataField, - globals.categoryDisplayStringMaxLength - ); const checkboxID = `category-select-${metadataField}`; @@ -145,26 +145,17 @@ class Category extends React.Component { - + - {truncatedString || metadataField} + {metadataField} - +
{} {} diff --git a/client/src/components/categorical/value/index.js b/client/src/components/categorical/value/index.js index 01ca7595..c6c2a6a9 100644 --- a/client/src/components/categorical/value/index.js +++ b/client/src/components/categorical/value/index.js @@ -9,16 +9,15 @@ import { Position, Icon, PopoverInteractionKind, - Tooltip, } from "@blueprintjs/core"; import Occupancy from "./occupancy"; import * as globals from "../../../globals"; import styles from "../categorical.css"; import AnnoDialog from "../annoDialog"; import LabelInput from "../labelInput"; +import Truncate from "../../util/truncate"; import { AnnotationsHelpers } from "../../../util/stateManager"; -import maybeTruncateString from "../../../util/maybeTruncateString"; import { labelPrompt, isLabelErroneous } from "../labelUtil"; /* this is defined outside of the class so we can use it in connect() */ @@ -317,13 +316,6 @@ class CategoryValue extends React.Component { categories = schema.annotations.obsByName[colorAccessor]?.categories; } - const truncatedString = maybeTruncateString( - displayString, - colorAccessor && !isColorBy - ? globals.categoryLabelDisplayStringShortLength - : globals.categoryLabelDisplayStringLongLength - ); - const editModeActive = isUserAnno && annotations.labelEditable.category === metadataField && @@ -332,6 +324,26 @@ class CategoryValue extends React.Component { const valueToggleLabel = `value-toggle-checkbox-${displayString}`; + const LEFT_MARGIN = 33; + const CHECKBOX = 26; + const CELL_NUMBER = 61; + const ANNO_MENU = 26; + const LABEL_MARGIN = 24; + + const otherElementsWidth = + LEFT_MARGIN + + CHECKBOX + + CELL_NUMBER + + LABEL_MARGIN + + (isUserAnno ? ANNO_MENU : 0); + + const OCCUPANCY_WIDTH = 100; + + const labelWidth = + colorAccessor && !isColorBy + ? globals.leftSidebarWidth - otherElementsWidth - OCCUPANCY_WIDTH + : globals.leftSidebarWidth - otherElementsWidth; + return (
- + - {truncatedString || displayString} + {displayString} - + {editModeActive ? (
-
+

This histograms shows the distribution of{" "} {colorAccessor} within{" "} diff --git a/client/src/components/graph/overlays/centroidLabels.js b/client/src/components/graph/overlays/centroidLabels.js index 02249f9d..5d85b452 100644 --- a/client/src/components/graph/overlays/centroidLabels.js +++ b/client/src/components/graph/overlays/centroidLabels.js @@ -85,7 +85,6 @@ class CentroidLabels extends PureComponent { textAnchor="middle" data-label={label} style={{ - fontFamily: "Roboto Condensed", fontSize, fontWeight, fill: "black", diff --git a/client/src/components/leftSidebar/topLeftLogoAndTitle.js b/client/src/components/leftSidebar/topLeftLogoAndTitle.js index 1efebed3..ee274b6f 100644 --- a/client/src/components/leftSidebar/topLeftLogoAndTitle.js +++ b/client/src/components/leftSidebar/topLeftLogoAndTitle.js @@ -3,6 +3,10 @@ import React from "react"; import { connect } from "react-redux"; import * as globals from "../../globals"; import Logo from "../framework/logo"; +import Truncate from "../util/truncate"; + +const DATASET_TITLE_WIDTH = 190; +const DATASET_TITLE_FONT_SIZE = 14; @connect((state) => ({ datasetTitle: state.config?.displayNames?.dataset ?? "", @@ -14,16 +18,6 @@ class LeftSideBar extends React.Component { render() { const { datasetTitle, aboutURL } = this.props; - const displayTitle = - datasetTitle.length > globals.datasetTitleMaxCharacterCount - ? `${datasetTitle.substring( - 0, - Math.floor(globals.datasetTitleMaxCharacterCount / 2) - )}…${datasetTitle.slice( - -Math.floor(globals.datasetTitleMaxCharacterCount / 2) - )}` - : datasetTitle; - return (

{aboutURL ? ( - - {displayTitle} - + + + {datasetTitle} + + ) : ( - displayTitle + + + {datasetTitle} + + )}
diff --git a/client/src/components/util/truncate.js b/client/src/components/util/truncate.js new file mode 100644 index 00000000..618c7831 --- /dev/null +++ b/client/src/components/util/truncate.js @@ -0,0 +1,85 @@ +import React, { cloneElement } from "react"; +import { Tooltip, Position } from "@blueprintjs/core"; + +import { tooltipHoverOpenDelayQuick } from "../../globals"; + +const SPLIT_STYLE = { + display: "flex", + overflow: "hidden", + justifyContent: "flex-start", +}; + +const FIRST_HALF_STYLE = { + overflow: "hidden", + textOverflow: "ellipsis", + whiteSpace: "nowrap", + flexShrink: 1, + minWidth: "5px", +}; + +const SECOND_HALF_INNER_STYLE = { + position: "absolute", + right: 0, +}; +const SECOND_HALF_STYLE = { + color: "transparent", + position: "relative", + overflow: "hidden", + whiteSpace: "nowrap", +}; + +export default (props) => { + const { children } = props; + // Truncate only support a single child with a text child + + if ( + React.Children.count(children) !== 1 || + React.Children.count(children.props?.children) !== 1 + ) { + throw Error("Only pass a single child with text to Truncate"); + } + const originalString = children.props.children; + + const firstString = originalString.substr(0, originalString.length / 2); + const secondString = originalString.substr(originalString.length / 2); + + const inheritedColor = children.props.style.color; + + const splitStyle = { ...children.props.style, ...SPLIT_STYLE }; + const secondHalfInnerStyle = { + ...SECOND_HALF_INNER_STYLE, + color: inheritedColor || "initial", + }; + + const truncatedJSX = ( + + {firstString} + + {secondString} + {secondString} + + + ); + + // clone children, changing the children(text) to the truncated string + const newChildren = React.Children.map(children, (child) => + cloneElement(child, { + children: truncatedJSX, + "aria-label": originalString, + }) + ); + return ( + + {newChildren} + + ); +}; diff --git a/client/src/util/maybeTruncateString.js b/client/src/util/maybeTruncateString.js deleted file mode 100644 index 9165f4f7..00000000 --- a/client/src/util/maybeTruncateString.js +++ /dev/null @@ -1,12 +0,0 @@ -const maybeTruncateString = (str, maxLength) => { - let truncatedString = null; - if (str.length > maxLength) { - truncatedString = `${str.slice(0, maxLength / 2)}…${str.slice( - -maxLength / 2 - )}`; - } - - return truncatedString; -}; - -export default maybeTruncateString;