@databiosphere/findable-ui 38.2.0 → 39.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/.release-please-manifest.json +1 -1
  2. package/CHANGELOG.md +18 -0
  3. package/lib/components/DataDictionary/components/Table/options/expanded/constants.d.ts +1 -1
  4. package/lib/components/DataDictionary/components/Table/options/expanded/constants.js +1 -0
  5. package/lib/components/Detail/components/Table/components/TableRows/tableRows.js +2 -1
  6. package/lib/components/Links/components/Link/components/ExploreViewLink/exploreViewLink.js +2 -2
  7. package/lib/components/Links/components/Link/link.d.ts +0 -1
  8. package/lib/components/MarkdownRenderer/components/Anchor/anchor.js +6 -1
  9. package/lib/components/MarkdownRenderer/markdownRenderer.d.ts +1 -1
  10. package/lib/components/MarkdownRenderer/markdownRenderer.js +6 -3
  11. package/lib/components/MarkdownRenderer/rehypeHighlight.d.ts +10 -0
  12. package/lib/components/MarkdownRenderer/rehypeHighlight.js +49 -0
  13. package/lib/components/MarkdownRenderer/stories/args.d.ts +3 -0
  14. package/lib/components/MarkdownRenderer/stories/args.js +4 -0
  15. package/lib/components/MarkdownRenderer/stories/markdownRenderer.stories.d.ts +6 -0
  16. package/lib/components/MarkdownRenderer/stories/markdownRenderer.stories.js +22 -0
  17. package/lib/components/MarkdownRenderer/types.d.ts +1 -0
  18. package/lib/components/Table/columnDef/globalFilter/utils.js +5 -5
  19. package/lib/components/Table/components/TableCell/components/LinkCell/linkCell.js +6 -1
  20. package/lib/components/Table/components/TableCell/components/MarkdownCell/markdownCell.d.ts +1 -2
  21. package/lib/components/Table/components/TableCell/components/MarkdownCell/markdownCell.js +10 -5
  22. package/lib/components/Table/components/TableCell/components/MarkdownCell/stories/args.js +8 -6
  23. package/lib/components/Table/components/TableCell/components/RankedCell/utils.d.ts +0 -9
  24. package/lib/components/Table/components/TableCell/components/RankedCell/utils.js +0 -27
  25. package/lib/components/Table/components/TableCell/components/RowSelectionCell/rowSelectionCell.js +6 -1
  26. package/lib/components/Table/components/TableFeatures/RowExpanding/utils.d.ts +10 -0
  27. package/lib/components/Table/components/TableFeatures/RowExpanding/utils.js +22 -0
  28. package/lib/components/Table/components/TableRow/tableRow.styles.d.ts +2 -0
  29. package/lib/components/Table/components/TableRow/tableRow.styles.js +23 -5
  30. package/lib/components/Table/components/TableRows/tableRows.js +3 -2
  31. package/package.json +1 -1
  32. package/src/components/DataDictionary/components/Table/options/expanded/constants.ts +2 -1
  33. package/src/components/Detail/components/Table/components/TableRows/tableRows.tsx +4 -0
  34. package/src/components/Links/components/Link/components/ExploreViewLink/exploreViewLink.tsx +2 -2
  35. package/src/components/Links/components/Link/link.tsx +0 -1
  36. package/src/components/MarkdownRenderer/components/Anchor/anchor.tsx +5 -0
  37. package/src/components/MarkdownRenderer/markdownRenderer.tsx +12 -2
  38. package/src/components/MarkdownRenderer/rehypeHighlight.ts +54 -0
  39. package/src/components/MarkdownRenderer/stories/args.ts +8 -0
  40. package/src/components/MarkdownRenderer/stories/markdownRenderer.stories.tsx +33 -0
  41. package/src/components/MarkdownRenderer/types.ts +1 -0
  42. package/src/components/Table/columnDef/globalFilter/utils.ts +5 -5
  43. package/src/components/Table/components/TableCell/components/LinkCell/linkCell.tsx +5 -0
  44. package/src/components/Table/components/TableCell/components/MarkdownCell/markdownCell.tsx +16 -6
  45. package/src/components/Table/components/TableCell/components/MarkdownCell/stories/args.ts +14 -13
  46. package/src/components/Table/components/TableCell/components/RankedCell/utils.ts +0 -37
  47. package/src/components/Table/components/TableCell/components/RowSelectionCell/rowSelectionCell.tsx +5 -0
  48. package/src/components/Table/components/TableFeatures/RowExpanding/utils.ts +25 -0
  49. package/src/components/Table/components/TableRow/tableRow.styles.ts +28 -9
  50. package/src/components/Table/components/TableRows/tableRows.tsx +5 -1
  51. package/lib/components/Table/components/TableCell/components/MarkdownCell/stories/types.d.ts +0 -3
  52. package/lib/components/Table/components/TableCell/components/MarkdownCell/stories/types.js +0 -1
  53. package/lib/components/Table/components/TableCell/components/MarkdownCell/types.d.ts +0 -3
  54. package/lib/components/Table/components/TableCell/components/MarkdownCell/types.js +0 -1
  55. package/src/components/Table/components/TableCell/components/MarkdownCell/stories/types.ts +0 -4
  56. package/src/components/Table/components/TableCell/components/MarkdownCell/types.ts +0 -3
@@ -1,3 +1,3 @@
1
1
  {
2
- ".": "38.2.0"
2
+ ".": "39.0.0"
3
3
  }
package/CHANGELOG.md CHANGED
@@ -1,5 +1,23 @@
1
1
  # Changelog
2
2
 
3
+ ## [39.0.0](https://github.com/DataBiosphere/findable-ui/compare/v38.3.0...v39.0.0) (2025-07-23)
4
+
5
+
6
+ ### ⚠ BREAKING CHANGES
7
+
8
+ * fix global search when a marked value is inside another HTML tag ([#539](https://github.com/DataBiosphere/findable-ui/issues/539)) (#573)
9
+
10
+ ### Features
11
+
12
+ * fix global search when a marked value is inside another HTML tag ([#539](https://github.com/DataBiosphere/findable-ui/issues/539)) ([#573](https://github.com/DataBiosphere/findable-ui/issues/573)) ([7b58c24](https://github.com/DataBiosphere/findable-ui/commit/7b58c2429ba947e752358a012a6a7d545f0ffc28))
13
+
14
+ ## [38.3.0](https://github.com/DataBiosphere/findable-ui/compare/v38.2.0...v38.3.0) (2025-07-22)
15
+
16
+
17
+ ### Features
18
+
19
+ * add table row expansion to table components ([#569](https://github.com/DataBiosphere/findable-ui/issues/569)) ([#570](https://github.com/DataBiosphere/findable-ui/issues/570)) ([d23c5b8](https://github.com/DataBiosphere/findable-ui/commit/d23c5b8c091a899295a3005ff80d7cd297949033))
20
+
3
21
  ## [38.2.0](https://github.com/DataBiosphere/findable-ui/compare/v38.1.1...v38.2.0) (2025-07-11)
4
22
 
5
23
 
@@ -1,2 +1,2 @@
1
1
  import { ExpandedOptions, RowData } from "@tanstack/react-table";
2
- export declare const EXPANDED_OPTIONS: Pick<ExpandedOptions<RowData>, "enableExpanding" | "getExpandedRowModel">;
2
+ export declare const EXPANDED_OPTIONS: Pick<ExpandedOptions<RowData>, "enableExpanding" | "getExpandedRowModel" | "getRowCanExpand">;
@@ -2,4 +2,5 @@ import { getExpandedRowModel, } from "@tanstack/react-table";
2
2
  export const EXPANDED_OPTIONS = {
3
3
  enableExpanding: true,
4
4
  getExpandedRowModel: getExpandedRowModel(),
5
+ getRowCanExpand: () => true,
5
6
  };
@@ -2,6 +2,7 @@ import { TableCell } from "@mui/material";
2
2
  import { flexRender } from "@tanstack/react-table";
3
3
  import React, { Fragment } from "react";
4
4
  import { getTableCellAlign, getTableCellPadding, } from "../../../../../Table/components/TableCell/common/utils";
5
+ import { handleToggleExpanded } from "../../../../../Table/components/TableFeatures/RowExpanding/utils";
5
6
  import { StyledTableRow } from "../../../../../Table/components/TableRow/tableRow.styles";
6
7
  export const TableRows = ({ rows: leafOrSubRows, tableInstance, tableView, }) => {
7
8
  const { getRowModel } = tableInstance;
@@ -9,7 +10,7 @@ export const TableRows = ({ rows: leafOrSubRows, tableInstance, tableView, }) =>
9
10
  const { tableCell } = tableView || {};
10
11
  const { size: tableCellSize = "medium" } = tableCell || {};
11
12
  return (React.createElement(Fragment, null, (leafOrSubRows || rows).map((row) => {
12
- return (React.createElement(StyledTableRow, { key: row.id, isGrouped: row.getIsGrouped(), isPreview: row.getIsPreview() }, row.getVisibleCells().map((cell) => {
13
+ return (React.createElement(StyledTableRow, { key: row.id, canExpand: row.getCanExpand(), isExpanded: row.getIsExpanded(), isGrouped: row.getIsGrouped(), isPreview: row.getIsPreview(), onClick: () => handleToggleExpanded(row) }, row.getVisibleCells().map((cell) => {
13
14
  if (cell.getIsAggregated())
14
15
  return null; // Display of aggregated cells is currently not supported.
15
16
  if (cell.getIsPlaceholder())
@@ -15,12 +15,12 @@ export const ExploreViewLink = ({ className, label, onClick, target = ANCHOR_TAR
15
15
  const filters = getSelectedFilters(url.query);
16
16
  const grouping = getGrouping(url.query);
17
17
  const sorting = getSorting(url.query);
18
- return (React.createElement(Link, { className: className, href: url.href, onClick: () => {
18
+ return (React.createElement(Link, { className: className, href: url.href, onClick: (e) => {
19
19
  exploreDispatch({
20
20
  payload: { entityListType, filters, grouping, sorting },
21
21
  type: ExploreActionKind.UpdateEntityFilters,
22
22
  });
23
- onClick?.();
23
+ onClick?.(e);
24
24
  }, rel: REL_ATTRIBUTE.NO_OPENER, target: target }, label));
25
25
  };
26
26
  /**
@@ -6,7 +6,6 @@ import { ANCHOR_TARGET, Url } from "../../common/entities";
6
6
  export interface LinkProps extends BaseComponentProps, Omit<MLinkProps, "children" | "component"> {
7
7
  copyable?: boolean;
8
8
  label: ReactNode;
9
- onClick?: () => void;
10
9
  target?: ANCHOR_TARGET;
11
10
  TypographyProps?: TypographyProps;
12
11
  url: Url;
@@ -17,5 +17,10 @@ import { Link } from "../../../Links/components/Link/link";
17
17
  * Note: This component currently does not support these excluded attributes.
18
18
  */
19
19
  export const Anchor = (props) => {
20
- return (React.createElement(Link, { className: props.className, label: props.children, url: props.href || "" }));
20
+ return (React.createElement(Link, { className: props.className, label: props.children,
21
+ /*
22
+ * Prevents click events from bubbling up to parent components
23
+ * (such as CardActionArea or Accordion) when the link is activated.
24
+ */
25
+ onClick: (e) => e.stopPropagation(), url: props.href || "" }));
21
26
  };
@@ -11,4 +11,4 @@ import { MarkdownRendererProps } from "./types";
11
11
  * rehype plug-ins are chosen from versions that still target Unified 10:
12
12
  * rehype-raw 7.x, rehype-sanitize 5.x and rehype-react 7.x.
13
13
  */
14
- export declare const MarkdownRenderer: ({ className, components: componentOptions, value, }: MarkdownRendererProps) => JSX.Element;
14
+ export declare const MarkdownRenderer: ({ className, components: componentOptions, regex: markdownRegex, value, }: MarkdownRendererProps) => JSX.Element;
@@ -1,5 +1,5 @@
1
1
  import { Typography } from "@mui/material";
2
- import React, { Fragment, createElement, useEffect, useState } from "react";
2
+ import React, { Fragment, createElement, useEffect, useMemo, useState, } from "react";
3
3
  import rehypeRaw from "rehype-raw";
4
4
  import rehypeReact from "rehype-react";
5
5
  import rehypeSanitize from "rehype-sanitize";
@@ -10,6 +10,7 @@ import { unified } from "unified";
10
10
  import { TYPOGRAPHY_PROPS } from "../../styles/common/mui/typography";
11
11
  import { COMPONENTS } from "./constants";
12
12
  import { StyledContainer } from "./markdownRenderer.styles";
13
+ import { rehypeHighlight } from "./rehypeHighlight";
13
14
  /**
14
15
  * Markdown Rendering - Pipeline Version Constraints
15
16
  *
@@ -22,10 +23,11 @@ import { StyledContainer } from "./markdownRenderer.styles";
22
23
  * rehype plug-ins are chosen from versions that still target Unified 10:
23
24
  * rehype-raw 7.x, rehype-sanitize 5.x and rehype-react 7.x.
24
25
  */
25
- export const MarkdownRenderer = ({ className, components: componentOptions = COMPONENTS, value, }) => {
26
+ export const MarkdownRenderer = ({ className, components: componentOptions = COMPONENTS, regex: markdownRegex, value, }) => {
26
27
  const [element, setElement] = useState(null);
27
28
  const [error, setError] = useState(null);
28
29
  const [components] = useState(componentOptions);
30
+ const regex = useMemo(() => markdownRegex, [markdownRegex]);
29
31
  useEffect(() => {
30
32
  let cancelled = false;
31
33
  setError(null);
@@ -35,6 +37,7 @@ export const MarkdownRenderer = ({ className, components: componentOptions = COM
35
37
  .use(remarkRehype, { allowDangerousHtml: true })
36
38
  .use(rehypeRaw)
37
39
  .use(rehypeSanitize)
40
+ .use(rehypeHighlight, { regex })
38
41
  .use(rehypeReact, { Fragment, components, createElement });
39
42
  processor
40
43
  .process(value)
@@ -49,7 +52,7 @@ export const MarkdownRenderer = ({ className, components: componentOptions = COM
49
52
  return () => {
50
53
  cancelled = true;
51
54
  };
52
- }, [components, value]);
55
+ }, [components, regex, value]);
53
56
  if (error)
54
57
  return (React.createElement(Typography, { color: TYPOGRAPHY_PROPS.COLOR.ERROR, variant: TYPOGRAPHY_PROPS.VARIANT.TEXT_BODY_SMALL_400 }, error));
55
58
  return React.createElement(StyledContainer, { className: className }, element);
@@ -0,0 +1,10 @@
1
+ import { Root } from "hast";
2
+ /**
3
+ * Custom rehype plugin to highlight markdown from given regex.
4
+ * @param options - Options.
5
+ * @param options.regex - Regex to match.
6
+ * @returns A rehype plugin.
7
+ */
8
+ export declare function rehypeHighlight(options: {
9
+ regex: RegExp | undefined;
10
+ }): (tree: Root) => void;
@@ -0,0 +1,49 @@
1
+ import { visit } from "unist-util-visit";
2
+ /**
3
+ * Custom rehype plugin to highlight markdown from given regex.
4
+ * @param options - Options.
5
+ * @param options.regex - Regex to match.
6
+ * @returns A rehype plugin.
7
+ */
8
+ // eslint-disable-next-line sonarjs/cognitive-complexity -- ignoring for readability
9
+ export function rehypeHighlight(options) {
10
+ const { regex } = options;
11
+ if (!regex)
12
+ return () => { };
13
+ return (tree) => {
14
+ visit(tree, "text", (node, index, parent) => {
15
+ if (!parent)
16
+ return;
17
+ if (typeof index !== "number")
18
+ return;
19
+ // Avoid double marking, breaking scripts, and breaking styles.
20
+ if ("tagName" in parent &&
21
+ ["mark", "script", "style"].includes(parent.tagName)) {
22
+ return;
23
+ }
24
+ const parts = node.value.split(regex);
25
+ if (parts.length === 1)
26
+ return; // No text to highlight.
27
+ const newNodes = [];
28
+ parts.forEach((part, i) => {
29
+ if (!part)
30
+ return; // Skip empties.
31
+ if (i % 2) {
32
+ // Captured text.
33
+ newNodes.push({
34
+ children: [{ type: "text", value: part }],
35
+ properties: {},
36
+ tagName: "mark",
37
+ type: "element",
38
+ });
39
+ }
40
+ else {
41
+ // Normal text.
42
+ newNodes.push({ type: "text", value: part });
43
+ }
44
+ });
45
+ // Replace the original text node with the new nodes.
46
+ parent.children.splice(index, 1, ...newNodes);
47
+ });
48
+ };
49
+ }
@@ -0,0 +1,3 @@
1
+ import { ComponentProps } from "react";
2
+ import { MarkdownRenderer } from "../markdownRenderer";
3
+ export declare const DEFAULT_ARGS: ComponentProps<typeof MarkdownRenderer>;
@@ -0,0 +1,4 @@
1
+ export const DEFAULT_ARGS = {
2
+ regex: /(UBERON|955)/gi,
3
+ value: '| Key | Annotator | Value |\n| --- | --- | --- |\n| `tissue_ontology_term_id` | Curator | categorical with `str` categories. This **MUST** be the UBERON or CL term that best describes the tissue the cell was derived from, depending on the sample type. |\n\n**Mapping guidance**\n\n| For | Use |\n| --- | --- |\n| Tissue | STRONGLY RECOMMENDED to be an UBERON term&nbsp;<br />(e.g. [`UBERON:0008930`](http://purl.obolibrary.org/obo/UBERON_0008930) for a *somatosensory cortex* tissue sample) |\n| Cell culture | MUST be a CL term appended with \\" (cell culture)\\"&nbsp;<br />(e.g. [`CL:0000057`](http://purl.obolibrary.org/obo/CL_0000057) **(cell culture)** for the *WTC-11* cell line) |\n| Organoid | MUST be an UBERON term appended with \\" (organoid)\\"&nbsp;<br />(e.g. [`UBERON:0000955`](http://purl.obolibrary.org/obo/UBERON_0000955) **(organoid)** for a *brain organoid*) |\n| Enriched / sorted / isolated cells from a tissue | MUST be an UBERON or CL term and **SHOULD NOT** use terms that do not capture the tissue of origin.<br /><br />• *CD3+ kidney cells* → [`UBERON:0002113`](https://www.ebi.ac.uk/ols/ontologies/uberon/terms?iri=http://purl.obolibrary.org/obo/UBERON_0002113) (*kidney*) instead of [`CL:000084`](https://www.ebi.ac.uk/ols/ontologies/cl/terms?iri=http://purl.obolibrary.org/obo/CL_0000084) (*T cell*).<br />• *EPCAM+ cervical cells* → [`CL:0000066`](https://www.ebi.ac.uk/ols/ontologies/cl/terms?iri=http://purl.obolibrary.org/obo/CL_0000066) (*epithelial cell* of the cervix). |\n\n---\n\nWhen a dataset is uploaded, the **cellxgene Data Portal** MUST automatically add the matching human-readable name for the corresponding ontology term to the `obs` dataframe. Curators **MUST NOT** annotate the following columns.\n\n### `assay`\n\n| Key | Annotator | Value |\n| --- | --- | --- |\n| `assay` | Data Portal | categorical with `str` categories. This **MUST** be the human-readable name assigned to the value of `assay_ontology_term_id`. |',
4
+ };
@@ -0,0 +1,6 @@
1
+ import { Meta, StoryObj } from "@storybook/react";
2
+ import { MarkdownRenderer } from "../markdownRenderer";
3
+ declare const meta: Meta<typeof MarkdownRenderer>;
4
+ export default meta;
5
+ type Story = StoryObj<typeof meta>;
6
+ export declare const Default: Story;
@@ -0,0 +1,22 @@
1
+ import { Box } from "@mui/material";
2
+ import React from "react";
3
+ import { PALETTE } from "../../../styles/common/constants/palette";
4
+ import { MarkdownRenderer } from "../markdownRenderer";
5
+ import { DEFAULT_ARGS } from "./args";
6
+ const meta = {
7
+ component: MarkdownRenderer,
8
+ decorators: [
9
+ (Story) => (React.createElement(Box, { sx: {
10
+ backgroundColor: PALETTE.COMMON_WHITE,
11
+ fontSize: "14px",
12
+ lineHeight: "20px",
13
+ padding: 3,
14
+ width: 480,
15
+ } },
16
+ React.createElement(Story, null))),
17
+ ],
18
+ };
19
+ export default meta;
20
+ export const Default = {
21
+ args: DEFAULT_ARGS,
22
+ };
@@ -3,5 +3,6 @@ import { BaseComponentProps } from "../types";
3
3
  export type MarkdownRendererComponents = Record<string, ComponentType<any>>;
4
4
  export interface MarkdownRendererProps extends BaseComponentProps {
5
5
  components?: MarkdownRendererComponents;
6
+ regex?: RegExp;
6
7
  value: string;
7
8
  }
@@ -6,11 +6,14 @@ import { RANK_ITEM_OPTIONS } from "./constants";
6
6
  * @returns Array of terms.
7
7
  */
8
8
  export function parseSearchTerms(value) {
9
- return String(value ?? "")
9
+ const terms = String(value ?? "")
10
10
  .toLowerCase()
11
11
  .trim()
12
12
  .split(/\s+/)
13
- .filter(Boolean);
13
+ .filter(Boolean)
14
+ .sort((a, b) => b.length - a.length);
15
+ const termsSet = new Set(terms);
16
+ return [...termsSet];
14
17
  }
15
18
  /**
16
19
  * Ranks a column's value against search terms.
@@ -38,7 +41,6 @@ export function rankColumnValue(row, columnId, terms) {
38
41
  * @param terms - Search terms to match against.
39
42
  */
40
43
  export function rankRowColumns(row, columnId, terms) {
41
- const columnFiltersMeta = row.columnFiltersMeta;
42
44
  // Process other columns.
43
45
  for (const { column } of row.getAllCells()) {
44
46
  // Column is not searchable.
@@ -47,8 +49,6 @@ export function rankRowColumns(row, columnId, terms) {
47
49
  // Column has already been processed.
48
50
  if (column.id === columnId)
49
51
  continue;
50
- if (column.id in columnFiltersMeta)
51
- continue;
52
52
  // Rank the column value.
53
53
  const passed = rankColumnValue(row, column.id, terms);
54
54
  // Add the filter metadata.
@@ -17,5 +17,10 @@ export const LinkCell = ({ className, getValue, }) => {
17
17
  if (!isValid)
18
18
  return (React.createElement(Typography, { className: className, color: TYPOGRAPHY_PROPS.COLOR.INHERIT, component: "span", variant: TYPOGRAPHY_PROPS.VARIANT.INHERIT, ...linkProps }, children));
19
19
  // If the href is valid, return a Link component.
20
- return (React.createElement(MLink, { className: className, color: color, component: getComponent(href, isClientSide), href: href, rel: getRelAttribute(rel, isClientSide), target: getTargetAttribute(target, isClientSide), underline: underline, ...linkProps }, children));
20
+ return (React.createElement(MLink, { className: className, color: color, component: getComponent(href, isClientSide), href: href,
21
+ /*
22
+ * Prevents click events from bubbling up to parent components
23
+ * (such as CardActionArea or Accordion) when the link is activated.
24
+ */
25
+ onClick: (e) => e.stopPropagation(), rel: getRelAttribute(rel, isClientSide), target: getTargetAttribute(target, isClientSide), underline: underline, ...linkProps }, children));
21
26
  };
@@ -1,4 +1,3 @@
1
1
  import { CellContext, RowData } from "@tanstack/react-table";
2
2
  import { BaseComponentProps } from "components/types";
3
- import { MarkdownCellProps } from "./types";
4
- export declare const MarkdownCell: <T extends RowData, TValue extends MarkdownCellProps = MarkdownCellProps>({ className, column, getValue, }: BaseComponentProps & CellContext<T, TValue>) => JSX.Element | null;
3
+ export declare const MarkdownCell: <T extends RowData, TValue extends string = string>({ className, column, getValue, row, table, }: BaseComponentProps & CellContext<T, TValue>) => JSX.Element | null;
@@ -1,13 +1,18 @@
1
1
  import React from "react";
2
2
  import { COMPONENTS } from "../../../../../MarkdownRenderer/constants";
3
+ import { getTokens, getTokensRegex, isRankedCell } from "../RankedCell/utils";
3
4
  import { StyledMarkdownRenderer } from "./markdownCell.styles";
4
- export const MarkdownCell = ({ className, column, getValue, }) => {
5
- const props = getValue();
6
- if (!props)
5
+ export const MarkdownCell = ({ className, column, getValue, row, table, }) => {
6
+ const value = getValue();
7
+ if (!value)
7
8
  return null;
8
- const { values } = props;
9
+ // Get column metadata (components to be rendered in MarkdownRenderer).
9
10
  const columnDef = column?.columnDef;
10
11
  const columnMeta = columnDef?.meta;
11
12
  const components = columnMeta?.components;
12
- return (React.createElement(StyledMarkdownRenderer, { className: className, components: { ...COMPONENTS, ...components }, value: values }));
13
+ // Determine if the cell is ranked.
14
+ const isRanked = isRankedCell(table, row, column.id);
15
+ // Build regex for markdown highlighting.
16
+ const regex = isRanked ? getTokensRegex(getTokens(table)) : undefined;
17
+ return (React.createElement(StyledMarkdownRenderer, { className: className, components: { ...COMPONENTS, ...components }, regex: regex, value: value }));
13
18
  };
@@ -1,10 +1,12 @@
1
1
  export const DEFAULT_ARGS = {
2
- getValue: (() => ({
3
- values: '| Key | Annotator | Value |\n| --- | --- | --- |\n| `tissue_ontology_term_id` | Curator | categorical with `str` categories. This **MUST** be the UBERON or CL term that best describes the tissue the cell was derived from, depending on the sample type. |\n\n**Mapping guidance**\n\n| For | Use |\n| --- | --- |\n| Tissue | STRONGLY RECOMMENDED to be an UBERON term&nbsp;<br />(e.g. [`UBERON:0008930`](http://purl.obolibrary.org/obo/UBERON_0008930) for a *somatosensory cortex* tissue sample) |\n| Cell culture | MUST be a CL term appended with \\" (cell culture)\\"&nbsp;<br />(e.g. [`CL:0000057`](http://purl.obolibrary.org/obo/CL_0000057) **(cell culture)** for the *WTC-11* cell line) |\n| Organoid | MUST be an UBERON term appended with \\" (organoid)\\"&nbsp;<br />(e.g. [`UBERON:0000955`](http://purl.obolibrary.org/obo/UBERON_0000955) **(organoid)** for a *brain organoid*) |\n| Enriched / sorted / isolated cells from a tissue | MUST be an UBERON or CL term and **SHOULD NOT** use terms that do not capture the tissue of origin.<br /><br />• *CD3+ kidney cells* → [`UBERON:0002113`](https://www.ebi.ac.uk/ols/ontologies/uberon/terms?iri=http://purl.obolibrary.org/obo/UBERON_0002113) (*kidney*) instead of [`CL:000084`](https://www.ebi.ac.uk/ols/ontologies/cl/terms?iri=http://purl.obolibrary.org/obo/CL_0000084) (*T cell*).<br />• *EPCAM+ cervical cells* → [`CL:0000066`](https://www.ebi.ac.uk/ols/ontologies/cl/terms?iri=http://purl.obolibrary.org/obo/CL_0000066) (*epithelial cell* of the cervix). |\n\n---\n\nWhen a dataset is uploaded, the **cellxgene Data Portal** MUST automatically add the matching human-readable name for the corresponding ontology term to the `obs` dataframe. Curators **MUST NOT** annotate the following columns.\n\n### `assay`\n\n| Key | Annotator | Value |\n| --- | --- | --- |\n| `assay` | Data Portal | categorical with `str` categories. This **MUST** be the human-readable name assigned to the value of `assay_ontology_term_id`. |',
4
- })),
2
+ column: { id: "description" },
3
+ getValue: () => '| Key | Annotator | Value |\n| --- | --- | --- |\n| `tissue_ontology_term_id` | Curator | categorical with `str` categories. This **MUST** be the UBERON or CL term that best describes the tissue the cell was derived from, depending on the sample type. |\n\n**Mapping guidance**\n\n| For | Use |\n| --- | --- |\n| Tissue | STRONGLY RECOMMENDED to be an UBERON term&nbsp;<br />(e.g. [`UBERON:0008930`](http://purl.obolibrary.org/obo/UBERON_0008930) for a *somatosensory cortex* tissue sample) |\n| Cell culture | MUST be a CL term appended with \\" (cell culture)\\"&nbsp;<br />(e.g. [`CL:0000057`](http://purl.obolibrary.org/obo/CL_0000057) **(cell culture)** for the *WTC-11* cell line) |\n| Organoid | MUST be an UBERON term appended with \\" (organoid)\\"&nbsp;<br />(e.g. [`UBERON:0000955`](http://purl.obolibrary.org/obo/UBERON_0000955) **(organoid)** for a *brain organoid*) |\n| Enriched / sorted / isolated cells from a tissue | MUST be an UBERON or CL term and **SHOULD NOT** use terms that do not capture the tissue of origin.<br /><br />• *CD3+ kidney cells* → [`UBERON:0002113`](https://www.ebi.ac.uk/ols/ontologies/uberon/terms?iri=http://purl.obolibrary.org/obo/UBERON_0002113) (*kidney*) instead of [`CL:000084`](https://www.ebi.ac.uk/ols/ontologies/cl/terms?iri=http://purl.obolibrary.org/obo/CL_0000084) (*T cell*).<br />• *EPCAM+ cervical cells* → [`CL:0000066`](https://www.ebi.ac.uk/ols/ontologies/cl/terms?iri=http://purl.obolibrary.org/obo/CL_0000066) (*epithelial cell* of the cervix). |\n\n---\n\nWhen a dataset is uploaded, the **cellxgene Data Portal** MUST automatically add the matching human-readable name for the corresponding ontology term to the `obs` dataframe. Curators **MUST NOT** annotate the following columns.\n\n### `assay`\n\n| Key | Annotator | Value |\n| --- | --- | --- |\n| `assay` | Data Portal | categorical with `str` categories. This **MUST** be the human-readable name assigned to the value of `assay_ontology_term_id`. |',
4
+ row: { columnFiltersMeta: { description: { passed: false } } },
5
+ table: { getState: () => ({ globalFilter: "" }) },
5
6
  };
6
7
  export const WITH_HTML_ARGS = {
7
- getValue: (() => ({
8
- values: "Hello <br />World <a href='https://www.example.com'>example link</a>",
9
- })),
8
+ column: { id: "description" },
9
+ getValue: () => "Hello <br />World <a href='https://www.example.com'>example link</a>",
10
+ row: { columnFiltersMeta: { description: { passed: false } } },
11
+ table: { getState: () => ({ globalFilter: "" }) },
10
12
  };
@@ -1,13 +1,4 @@
1
1
  import { Row, RowData, Table } from "@tanstack/react-table";
2
- /**
3
- * Renders a cell value with highlighting if it matches item rank filter criteria.
4
- * @param table - Table.
5
- * @param row - Row.
6
- * @param columnId - Column identifier.
7
- * @param value - Cell value.
8
- * @returns Rendered cell value with highlighting.
9
- */
10
- export declare function renderRankedCell<T extends RowData>(table: Table<T>, row: Row<T>, columnId: string, value: string | undefined | null): string;
11
2
  /**
12
3
  * Returns the current global filter tokens from the table.
13
4
  * @param table - Table.
@@ -1,32 +1,5 @@
1
1
  import { escapeRegExp } from "../../../../../../common/utils";
2
2
  import { parseSearchTerms } from "../../../../columnDef/globalFilter/utils";
3
- /**
4
- * Renders a cell value with highlighting if it matches item rank filter criteria.
5
- * @param table - Table.
6
- * @param row - Row.
7
- * @param columnId - Column identifier.
8
- * @param value - Cell value.
9
- * @returns Rendered cell value with highlighting.
10
- */
11
- export function renderRankedCell(table, row, columnId, value) {
12
- // If the cell value is undefined or null, return an empty string.
13
- if (value === undefined || value === null)
14
- return "";
15
- const stringValue = String(value);
16
- // Check if the cell is ranked.
17
- const isRanked = isRankedCell(table, row, columnId);
18
- // Return the unranked cell, as-is, in string form.
19
- if (!isRanked)
20
- return stringValue;
21
- // Tokenise the current global filter.
22
- const tokens = getTokens(table);
23
- // If there are no tokens, return the value as-is, in string form.
24
- if (tokens.length === 0)
25
- return stringValue;
26
- // Create regex pattern.
27
- const regex = getTokensRegex(tokens);
28
- return stringValue.replace(regex, "<mark>$1</mark>");
29
- }
30
3
  /**
31
4
  * Returns the current global filter tokens from the table.
32
5
  * @param table - Table.
@@ -4,5 +4,10 @@ import { CheckedIcon } from "../../../../../common/CustomIcon/components/Checked
4
4
  import { UncheckedIcon } from "../../../../../common/CustomIcon/components/UncheckedIcon/uncheckedIcon";
5
5
  export const RowSelectionCell = ({ row, }) => {
6
6
  const { getIsSelected, getToggleSelectedHandler } = row;
7
- return (React.createElement(MCheckbox, { checked: getIsSelected(), checkedIcon: React.createElement(CheckedIcon, null), icon: React.createElement(UncheckedIcon, null), onChange: getToggleSelectedHandler() }));
7
+ return (React.createElement(MCheckbox, { checked: getIsSelected(), checkedIcon: React.createElement(CheckedIcon, null), icon: React.createElement(UncheckedIcon, null),
8
+ /*
9
+ * Prevents click events from bubbling up to parent components
10
+ * (such as CardActionArea or Accordion) when the checkbox is activated.
11
+ */
12
+ onClick: (e) => e.stopPropagation(), onChange: getToggleSelectedHandler() }));
8
13
  };
@@ -0,0 +1,10 @@
1
+ import { Row, RowData } from "@tanstack/react-table";
2
+ /**
3
+ * Handles toggling the expanded state of a row.
4
+ * Rows can not be expanded if:
5
+ * - the row can not be expanded.
6
+ * - the row is grouped (expanded rows are not supported on grouped rows).
7
+ * - the user is selecting text.
8
+ * @param row - Row.
9
+ */
10
+ export declare function handleToggleExpanded<T extends RowData>(row: Row<T>): void;
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Handles toggling the expanded state of a row.
3
+ * Rows can not be expanded if:
4
+ * - the row can not be expanded.
5
+ * - the row is grouped (expanded rows are not supported on grouped rows).
6
+ * - the user is selecting text.
7
+ * @param row - Row.
8
+ */
9
+ export function handleToggleExpanded(row) {
10
+ const { getCanExpand, getIsGrouped, toggleExpanded } = row;
11
+ // Row can not be expanded.
12
+ if (!getCanExpand())
13
+ return;
14
+ // Row is grouped - row expansion not supported on a grouped row.
15
+ if (getIsGrouped())
16
+ return;
17
+ // User is selecting text - do not toggle row expanded state.
18
+ if (window.getSelection()?.toString())
19
+ return;
20
+ // Toggle row expanded state.
21
+ toggleExpanded();
22
+ }
@@ -1,4 +1,6 @@
1
1
  export interface StyledTableRowProps {
2
+ canExpand?: boolean;
3
+ isExpanded?: boolean;
2
4
  isGrouped?: boolean;
3
5
  isPreview?: boolean;
4
6
  }
@@ -1,17 +1,20 @@
1
1
  import { css } from "@emotion/react";
2
2
  import styled from "@emotion/styled";
3
3
  import { TableRow as MTableRow } from "@mui/material";
4
- import { primaryLightest, smokeLightest, } from "../../../../styles/common/mixins/colors";
4
+ import { PALETTE } from "../../../../styles/common/constants/palette";
5
5
  import { textBodySmall500 } from "../../../../styles/common/mixins/fonts";
6
6
  export const StyledTableRow = styled(MTableRow, {
7
- shouldForwardProp: (prop) => prop !== "isPreview" && prop !== "isGrouped",
7
+ shouldForwardProp: (prop) => prop !== "canExpand" &&
8
+ prop !== "isExpanded" &&
9
+ prop !== "isPreview" &&
10
+ prop !== "isGrouped",
8
11
  }) `
9
12
  && {
10
13
  transition: background-color 300ms ease-in;
11
14
 
12
15
  ${(props) => props.isGrouped &&
13
16
  css `
14
- background-color: ${smokeLightest(props)};
17
+ background-color: ${PALETTE.SMOKE_LIGHTEST};
15
18
 
16
19
  td {
17
20
  ${textBodySmall500(props)};
@@ -20,9 +23,24 @@ export const StyledTableRow = styled(MTableRow, {
20
23
  }
21
24
  `}
22
25
 
23
- ${(props) => props.isPreview &&
26
+ ${({ canExpand, isExpanded, isGrouped }) => !isGrouped &&
27
+ canExpand &&
24
28
  css `
25
- background-color: ${primaryLightest(props)};
29
+ cursor: pointer;
30
+
31
+ &:hover {
32
+ background-color: #f8fbfd;
33
+ }
34
+
35
+ ${isExpanded &&
36
+ css `
37
+ background-color: #f8fbfd;
38
+ `}
39
+ `}
40
+
41
+ ${({ isPreview }) => isPreview &&
42
+ css `
43
+ background-color: ${PALETTE.PRIMARY_LIGHTEST};
26
44
  `}
27
45
  }
28
46
  `;
@@ -3,14 +3,15 @@ import { flexRender } from "@tanstack/react-table";
3
3
  import React, { Fragment } from "react";
4
4
  import { TEST_IDS } from "../../../../tests/testIds";
5
5
  import { getTableCellAlign, getTableCellPadding, } from "../TableCell/common/utils";
6
+ import { handleToggleExpanded } from "../TableFeatures/RowExpanding/utils";
6
7
  import { StyledTableRow } from "../TableRow/tableRow.styles";
7
8
  export const TableRows = ({ rows, virtualizer, }) => {
8
9
  const virtualItems = virtualizer.getVirtualItems();
9
10
  return (React.createElement(Fragment, null, virtualItems.map((virtualRow) => {
10
11
  const rowIndex = virtualRow.index;
11
12
  const row = rows[rowIndex];
12
- const { getIsGrouped, getIsPreview } = row;
13
- return (React.createElement(StyledTableRow, { key: row.id, "data-index": rowIndex, isGrouped: getIsGrouped(), isPreview: getIsPreview(), ref: virtualizer.measureElement }, row.getVisibleCells().map((cell, i) => {
13
+ const { getCanExpand, getIsExpanded, getIsGrouped, getIsPreview } = row;
14
+ return (React.createElement(StyledTableRow, { key: row.id, canExpand: getCanExpand(), "data-index": rowIndex, isExpanded: getIsExpanded(), isGrouped: getIsGrouped(), isPreview: getIsPreview(), onClick: () => handleToggleExpanded(row), ref: virtualizer.measureElement }, row.getVisibleCells().map((cell, i) => {
14
15
  if (cell.getIsAggregated())
15
16
  return null; // Display of aggregated cells is currently not supported.
16
17
  if (cell.getIsPlaceholder())
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@databiosphere/findable-ui",
3
- "version": "38.2.0",
3
+ "version": "39.0.0",
4
4
  "description": "",
5
5
  "scripts": {
6
6
  "test": "node --experimental-vm-modules node_modules/jest/bin/jest.js",
@@ -6,8 +6,9 @@ import {
6
6
 
7
7
  export const EXPANDED_OPTIONS: Pick<
8
8
  ExpandedOptions<RowData>,
9
- "enableExpanding" | "getExpandedRowModel"
9
+ "enableExpanding" | "getExpandedRowModel" | "getRowCanExpand"
10
10
  > = {
11
11
  enableExpanding: true,
12
12
  getExpandedRowModel: getExpandedRowModel(),
13
+ getRowCanExpand: () => true,
13
14
  };
@@ -5,6 +5,7 @@ import {
5
5
  getTableCellAlign,
6
6
  getTableCellPadding,
7
7
  } from "../../../../../Table/components/TableCell/common/utils";
8
+ import { handleToggleExpanded } from "../../../../../Table/components/TableFeatures/RowExpanding/utils";
8
9
  import { StyledTableRow } from "../../../../../Table/components/TableRow/tableRow.styles";
9
10
  import { TableView } from "../../table";
10
11
 
@@ -34,8 +35,11 @@ export const TableRows = <T extends RowData>({
34
35
  return (
35
36
  <StyledTableRow
36
37
  key={row.id}
38
+ canExpand={row.getCanExpand()}
39
+ isExpanded={row.getIsExpanded()}
37
40
  isGrouped={row.getIsGrouped()}
38
41
  isPreview={row.getIsPreview()}
42
+ onClick={() => handleToggleExpanded(row)}
39
43
  >
40
44
  {row.getVisibleCells().map((cell) => {
41
45
  if (cell.getIsAggregated()) return null; // Display of aggregated cells is currently not supported.
@@ -46,12 +46,12 @@ export const ExploreViewLink = ({
46
46
  <Link
47
47
  className={className}
48
48
  href={url.href}
49
- onClick={(): void => {
49
+ onClick={(e): void => {
50
50
  exploreDispatch({
51
51
  payload: { entityListType, filters, grouping, sorting },
52
52
  type: ExploreActionKind.UpdateEntityFilters,
53
53
  });
54
- onClick?.();
54
+ onClick?.(e);
55
55
  }}
56
56
  rel={REL_ATTRIBUTE.NO_OPENER}
57
57
  target={target}
@@ -22,7 +22,6 @@ export interface LinkProps
22
22
  Omit<MLinkProps, "children" | "component"> {
23
23
  copyable?: boolean;
24
24
  label: ReactNode /* link label may be an element */;
25
- onClick?: () => void;
26
25
  target?: ANCHOR_TARGET;
27
26
  TypographyProps?: TypographyProps;
28
27
  url: Url /* url specified as UrlObject with href and query defined, and is currently only used for internal links */;
@@ -28,6 +28,11 @@ export const Anchor = (
28
28
  <Link
29
29
  className={props.className}
30
30
  label={props.children}
31
+ /*
32
+ * Prevents click events from bubbling up to parent components
33
+ * (such as CardActionArea or Accordion) when the link is activated.
34
+ */
35
+ onClick={(e) => e.stopPropagation()}
31
36
  url={props.href || ""}
32
37
  />
33
38
  );
@@ -1,5 +1,11 @@
1
1
  import { Typography } from "@mui/material";
2
- import React, { Fragment, createElement, useEffect, useState } from "react";
2
+ import React, {
3
+ Fragment,
4
+ createElement,
5
+ useEffect,
6
+ useMemo,
7
+ useState,
8
+ } from "react";
3
9
  import rehypeRaw from "rehype-raw";
4
10
  import rehypeReact from "rehype-react";
5
11
  import rehypeSanitize from "rehype-sanitize";
@@ -10,6 +16,7 @@ import { unified } from "unified";
10
16
  import { TYPOGRAPHY_PROPS } from "../../styles/common/mui/typography";
11
17
  import { COMPONENTS } from "./constants";
12
18
  import { StyledContainer } from "./markdownRenderer.styles";
19
+ import { rehypeHighlight } from "./rehypeHighlight";
13
20
  import { MarkdownRendererComponents, MarkdownRendererProps } from "./types";
14
21
 
15
22
  /**
@@ -28,11 +35,13 @@ import { MarkdownRendererComponents, MarkdownRendererProps } from "./types";
28
35
  export const MarkdownRenderer = ({
29
36
  className,
30
37
  components: componentOptions = COMPONENTS,
38
+ regex: markdownRegex,
31
39
  value,
32
40
  }: MarkdownRendererProps): JSX.Element => {
33
41
  const [element, setElement] = useState<JSX.Element | null>(null);
34
42
  const [error, setError] = useState<string | null>(null);
35
43
  const [components] = useState<MarkdownRendererComponents>(componentOptions);
44
+ const regex = useMemo(() => markdownRegex, [markdownRegex]);
36
45
 
37
46
  useEffect(() => {
38
47
  let cancelled = false;
@@ -44,6 +53,7 @@ export const MarkdownRenderer = ({
44
53
  .use(remarkRehype, { allowDangerousHtml: true })
45
54
  .use(rehypeRaw)
46
55
  .use(rehypeSanitize)
56
+ .use(rehypeHighlight, { regex })
47
57
  .use(rehypeReact, { Fragment, components, createElement });
48
58
 
49
59
  processor
@@ -58,7 +68,7 @@ export const MarkdownRenderer = ({
58
68
  return (): void => {
59
69
  cancelled = true;
60
70
  };
61
- }, [components, value]);
71
+ }, [components, regex, value]);
62
72
 
63
73
  if (error)
64
74
  return (
@@ -0,0 +1,54 @@
1
+ import { Element, Root, Text } from "hast";
2
+ import { visit } from "unist-util-visit";
3
+
4
+ /**
5
+ * Custom rehype plugin to highlight markdown from given regex.
6
+ * @param options - Options.
7
+ * @param options.regex - Regex to match.
8
+ * @returns A rehype plugin.
9
+ */
10
+ // eslint-disable-next-line sonarjs/cognitive-complexity -- ignoring for readability
11
+ export function rehypeHighlight(options: { regex: RegExp | undefined }) {
12
+ const { regex } = options;
13
+
14
+ if (!regex) return (): void => {};
15
+
16
+ return (tree: Root): void => {
17
+ visit(tree, "text", (node, index, parent) => {
18
+ if (!parent) return;
19
+ if (typeof index !== "number") return;
20
+
21
+ // Avoid double marking, breaking scripts, and breaking styles.
22
+ if (
23
+ "tagName" in parent &&
24
+ ["mark", "script", "style"].includes(parent.tagName)
25
+ ) {
26
+ return;
27
+ }
28
+
29
+ const parts = node.value.split(regex);
30
+ if (parts.length === 1) return; // No text to highlight.
31
+
32
+ const newNodes: (Element | Text)[] = [];
33
+
34
+ parts.forEach((part, i) => {
35
+ if (!part) return; // Skip empties.
36
+ if (i % 2) {
37
+ // Captured text.
38
+ newNodes.push({
39
+ children: [{ type: "text", value: part }],
40
+ properties: {},
41
+ tagName: "mark",
42
+ type: "element",
43
+ });
44
+ } else {
45
+ // Normal text.
46
+ newNodes.push({ type: "text", value: part });
47
+ }
48
+ });
49
+
50
+ // Replace the original text node with the new nodes.
51
+ parent.children.splice(index, 1, ...newNodes);
52
+ });
53
+ };
54
+ }
@@ -0,0 +1,8 @@
1
+ import { ComponentProps } from "react";
2
+ import { MarkdownRenderer } from "../markdownRenderer";
3
+
4
+ export const DEFAULT_ARGS: ComponentProps<typeof MarkdownRenderer> = {
5
+ regex: /(UBERON|955)/gi,
6
+ value:
7
+ '| Key | Annotator | Value |\n| --- | --- | --- |\n| `tissue_ontology_term_id` | Curator | categorical with `str` categories. This **MUST** be the UBERON or CL term that best describes the tissue the cell was derived from, depending on the sample type. |\n\n**Mapping guidance**\n\n| For | Use |\n| --- | --- |\n| Tissue | STRONGLY RECOMMENDED to be an UBERON term&nbsp;<br />(e.g. [`UBERON:0008930`](http://purl.obolibrary.org/obo/UBERON_0008930) for a *somatosensory cortex* tissue sample) |\n| Cell culture | MUST be a CL term appended with \\" (cell culture)\\"&nbsp;<br />(e.g. [`CL:0000057`](http://purl.obolibrary.org/obo/CL_0000057) **(cell culture)** for the *WTC-11* cell line) |\n| Organoid | MUST be an UBERON term appended with \\" (organoid)\\"&nbsp;<br />(e.g. [`UBERON:0000955`](http://purl.obolibrary.org/obo/UBERON_0000955) **(organoid)** for a *brain organoid*) |\n| Enriched / sorted / isolated cells from a tissue | MUST be an UBERON or CL term and **SHOULD NOT** use terms that do not capture the tissue of origin.<br /><br />• *CD3+ kidney cells* → [`UBERON:0002113`](https://www.ebi.ac.uk/ols/ontologies/uberon/terms?iri=http://purl.obolibrary.org/obo/UBERON_0002113) (*kidney*) instead of [`CL:000084`](https://www.ebi.ac.uk/ols/ontologies/cl/terms?iri=http://purl.obolibrary.org/obo/CL_0000084) (*T cell*).<br />• *EPCAM+ cervical cells* → [`CL:0000066`](https://www.ebi.ac.uk/ols/ontologies/cl/terms?iri=http://purl.obolibrary.org/obo/CL_0000066) (*epithelial cell* of the cervix). |\n\n---\n\nWhen a dataset is uploaded, the **cellxgene Data Portal** MUST automatically add the matching human-readable name for the corresponding ontology term to the `obs` dataframe. Curators **MUST NOT** annotate the following columns.\n\n### `assay`\n\n| Key | Annotator | Value |\n| --- | --- | --- |\n| `assay` | Data Portal | categorical with `str` categories. This **MUST** be the human-readable name assigned to the value of `assay_ontology_term_id`. |',
8
+ };
@@ -0,0 +1,33 @@
1
+ import { Box } from "@mui/material";
2
+ import { Meta, StoryObj } from "@storybook/react";
3
+ import React from "react";
4
+ import { PALETTE } from "../../../styles/common/constants/palette";
5
+ import { MarkdownRenderer } from "../markdownRenderer";
6
+ import { DEFAULT_ARGS } from "./args";
7
+
8
+ const meta: Meta<typeof MarkdownRenderer> = {
9
+ component: MarkdownRenderer,
10
+ decorators: [
11
+ (Story): JSX.Element => (
12
+ <Box
13
+ sx={{
14
+ backgroundColor: PALETTE.COMMON_WHITE,
15
+ fontSize: "14px",
16
+ lineHeight: "20px",
17
+ padding: 3,
18
+ width: 480,
19
+ }}
20
+ >
21
+ <Story />
22
+ </Box>
23
+ ),
24
+ ],
25
+ };
26
+
27
+ export default meta;
28
+
29
+ type Story = StoryObj<typeof meta>;
30
+
31
+ export const Default: Story = {
32
+ args: DEFAULT_ARGS,
33
+ };
@@ -6,5 +6,6 @@ export type MarkdownRendererComponents = Record<string, ComponentType<any>>;
6
6
 
7
7
  export interface MarkdownRendererProps extends BaseComponentProps {
8
8
  components?: MarkdownRendererComponents;
9
+ regex?: RegExp;
9
10
  value: string;
10
11
  }
@@ -8,11 +8,14 @@ import { RANK_ITEM_OPTIONS } from "./constants";
8
8
  * @returns Array of terms.
9
9
  */
10
10
  export function parseSearchTerms(value: unknown): string[] {
11
- return String(value ?? "")
11
+ const terms = String(value ?? "")
12
12
  .toLowerCase()
13
13
  .trim()
14
14
  .split(/\s+/)
15
- .filter(Boolean);
15
+ .filter(Boolean)
16
+ .sort((a, b) => b.length - a.length);
17
+ const termsSet = new Set(terms);
18
+ return [...termsSet];
16
19
  }
17
20
 
18
21
  /**
@@ -54,8 +57,6 @@ export function rankRowColumns<T extends RowData>(
54
57
  columnId: string,
55
58
  terms: string[]
56
59
  ): void {
57
- const columnFiltersMeta = row.columnFiltersMeta;
58
-
59
60
  // Process other columns.
60
61
  for (const { column } of row.getAllCells()) {
61
62
  // Column is not searchable.
@@ -63,7 +64,6 @@ export function rankRowColumns<T extends RowData>(
63
64
 
64
65
  // Column has already been processed.
65
66
  if (column.id === columnId) continue;
66
- if (column.id in columnFiltersMeta) continue;
67
67
 
68
68
  // Rank the column value.
69
69
  const passed = rankColumnValue(row, column.id, terms);
@@ -53,6 +53,11 @@ export const LinkCell = <
53
53
  color={color}
54
54
  component={getComponent(href, isClientSide)}
55
55
  href={href}
56
+ /*
57
+ * Prevents click events from bubbling up to parent components
58
+ * (such as CardActionArea or Accordion) when the link is activated.
59
+ */
60
+ onClick={(e) => e.stopPropagation()}
56
61
  rel={getRelAttribute(rel, isClientSide)}
57
62
  target={getTargetAttribute(target, isClientSide)}
58
63
  underline={underline}
@@ -2,28 +2,38 @@ import { CellContext, RowData } from "@tanstack/react-table";
2
2
  import { BaseComponentProps } from "components/types";
3
3
  import React from "react";
4
4
  import { COMPONENTS } from "../../../../../MarkdownRenderer/constants";
5
+ import { getTokens, getTokensRegex, isRankedCell } from "../RankedCell/utils";
5
6
  import { StyledMarkdownRenderer } from "./markdownCell.styles";
6
- import { MarkdownCellProps } from "./types";
7
7
 
8
8
  export const MarkdownCell = <
9
9
  T extends RowData,
10
- TValue extends MarkdownCellProps = MarkdownCellProps
10
+ TValue extends string = string
11
11
  >({
12
12
  className,
13
13
  column,
14
14
  getValue,
15
+ row,
16
+ table,
15
17
  }: BaseComponentProps & CellContext<T, TValue>): JSX.Element | null => {
16
- const props = getValue();
17
- if (!props) return null;
18
- const { values } = props;
18
+ const value = getValue();
19
+ if (!value) return null;
20
+
21
+ // Get column metadata (components to be rendered in MarkdownRenderer).
19
22
  const columnDef = column?.columnDef;
20
23
  const columnMeta = columnDef?.meta;
21
24
  const components = columnMeta?.components;
25
+
26
+ // Determine if the cell is ranked.
27
+ const isRanked = isRankedCell(table, row, column.id);
28
+ // Build regex for markdown highlighting.
29
+ const regex = isRanked ? getTokensRegex(getTokens(table)) : undefined;
30
+
22
31
  return (
23
32
  <StyledMarkdownRenderer
24
33
  className={className}
25
34
  components={{ ...COMPONENTS, ...components }}
26
- value={values}
35
+ regex={regex}
36
+ value={value}
27
37
  />
28
38
  );
29
39
  };
@@ -1,17 +1,18 @@
1
1
  import { ComponentProps } from "react";
2
2
  import { MarkdownCell } from "../markdownCell";
3
- import { GetValue } from "./types";
4
3
 
5
- export const DEFAULT_ARGS: Partial<ComponentProps<typeof MarkdownCell>> = {
6
- getValue: (() => ({
7
- values:
8
- '| Key | Annotator | Value |\n| --- | --- | --- |\n| `tissue_ontology_term_id` | Curator | categorical with `str` categories. This **MUST** be the UBERON or CL term that best describes the tissue the cell was derived from, depending on the sample type. |\n\n**Mapping guidance**\n\n| For | Use |\n| --- | --- |\n| Tissue | STRONGLY RECOMMENDED to be an UBERON term&nbsp;<br />(e.g. [`UBERON:0008930`](http://purl.obolibrary.org/obo/UBERON_0008930) for a *somatosensory cortex* tissue sample) |\n| Cell culture | MUST be a CL term appended with \\" (cell culture)\\"&nbsp;<br />(e.g. [`CL:0000057`](http://purl.obolibrary.org/obo/CL_0000057) **(cell culture)** for the *WTC-11* cell line) |\n| Organoid | MUST be an UBERON term appended with \\" (organoid)\\"&nbsp;<br />(e.g. [`UBERON:0000955`](http://purl.obolibrary.org/obo/UBERON_0000955) **(organoid)** for a *brain organoid*) |\n| Enriched / sorted / isolated cells from a tissue | MUST be an UBERON or CL term and **SHOULD NOT** use terms that do not capture the tissue of origin.<br /><br />• *CD3+ kidney cells* → [`UBERON:0002113`](https://www.ebi.ac.uk/ols/ontologies/uberon/terms?iri=http://purl.obolibrary.org/obo/UBERON_0002113) (*kidney*) instead of [`CL:000084`](https://www.ebi.ac.uk/ols/ontologies/cl/terms?iri=http://purl.obolibrary.org/obo/CL_0000084) (*T cell*).<br />• *EPCAM+ cervical cells* → [`CL:0000066`](https://www.ebi.ac.uk/ols/ontologies/cl/terms?iri=http://purl.obolibrary.org/obo/CL_0000066) (*epithelial cell* of the cervix). |\n\n---\n\nWhen a dataset is uploaded, the **cellxgene Data Portal** MUST automatically add the matching human-readable name for the corresponding ontology term to the `obs` dataframe. Curators **MUST NOT** annotate the following columns.\n\n### `assay`\n\n| Key | Annotator | Value |\n| --- | --- | --- |\n| `assay` | Data Portal | categorical with `str` categories. This **MUST** be the human-readable name assigned to the value of `assay_ontology_term_id`. |',
9
- })) as GetValue,
10
- };
4
+ export const DEFAULT_ARGS = {
5
+ column: { id: "description" },
6
+ getValue: () =>
7
+ '| Key | Annotator | Value |\n| --- | --- | --- |\n| `tissue_ontology_term_id` | Curator | categorical with `str` categories. This **MUST** be the UBERON or CL term that best describes the tissue the cell was derived from, depending on the sample type. |\n\n**Mapping guidance**\n\n| For | Use |\n| --- | --- |\n| Tissue | STRONGLY RECOMMENDED to be an UBERON term&nbsp;<br />(e.g. [`UBERON:0008930`](http://purl.obolibrary.org/obo/UBERON_0008930) for a *somatosensory cortex* tissue sample) |\n| Cell culture | MUST be a CL term appended with \\" (cell culture)\\"&nbsp;<br />(e.g. [`CL:0000057`](http://purl.obolibrary.org/obo/CL_0000057) **(cell culture)** for the *WTC-11* cell line) |\n| Organoid | MUST be an UBERON term appended with \\" (organoid)\\"&nbsp;<br />(e.g. [`UBERON:0000955`](http://purl.obolibrary.org/obo/UBERON_0000955) **(organoid)** for a *brain organoid*) |\n| Enriched / sorted / isolated cells from a tissue | MUST be an UBERON or CL term and **SHOULD NOT** use terms that do not capture the tissue of origin.<br /><br />• *CD3+ kidney cells* → [`UBERON:0002113`](https://www.ebi.ac.uk/ols/ontologies/uberon/terms?iri=http://purl.obolibrary.org/obo/UBERON_0002113) (*kidney*) instead of [`CL:000084`](https://www.ebi.ac.uk/ols/ontologies/cl/terms?iri=http://purl.obolibrary.org/obo/CL_0000084) (*T cell*).<br />• *EPCAM+ cervical cells* → [`CL:0000066`](https://www.ebi.ac.uk/ols/ontologies/cl/terms?iri=http://purl.obolibrary.org/obo/CL_0000066) (*epithelial cell* of the cervix). |\n\n---\n\nWhen a dataset is uploaded, the **cellxgene Data Portal** MUST automatically add the matching human-readable name for the corresponding ontology term to the `obs` dataframe. Curators **MUST NOT** annotate the following columns.\n\n### `assay`\n\n| Key | Annotator | Value |\n| --- | --- | --- |\n| `assay` | Data Portal | categorical with `str` categories. This **MUST** be the human-readable name assigned to the value of `assay_ontology_term_id`. |',
8
+ row: { columnFiltersMeta: { description: { passed: false } } },
9
+ table: { getState: () => ({ globalFilter: "" }) },
10
+ } as unknown as Partial<ComponentProps<typeof MarkdownCell>>;
11
11
 
12
- export const WITH_HTML_ARGS: Partial<ComponentProps<typeof MarkdownCell>> = {
13
- getValue: (() => ({
14
- values:
15
- "Hello <br />World <a href='https://www.example.com'>example link</a>",
16
- })) as GetValue,
17
- };
12
+ export const WITH_HTML_ARGS = {
13
+ column: { id: "description" },
14
+ getValue: () =>
15
+ "Hello <br />World <a href='https://www.example.com'>example link</a>",
16
+ row: { columnFiltersMeta: { description: { passed: false } } },
17
+ table: { getState: () => ({ globalFilter: "" }) },
18
+ } as unknown as Partial<ComponentProps<typeof MarkdownCell>>;
@@ -3,43 +3,6 @@ import { escapeRegExp } from "../../../../../../common/utils";
3
3
  import { ColumnFilterMeta } from "../../../../columnDef/globalFilter/types";
4
4
  import { parseSearchTerms } from "../../../../columnDef/globalFilter/utils";
5
5
 
6
- /**
7
- * Renders a cell value with highlighting if it matches item rank filter criteria.
8
- * @param table - Table.
9
- * @param row - Row.
10
- * @param columnId - Column identifier.
11
- * @param value - Cell value.
12
- * @returns Rendered cell value with highlighting.
13
- */
14
- export function renderRankedCell<T extends RowData>(
15
- table: Table<T>,
16
- row: Row<T>,
17
- columnId: string,
18
- value: string | undefined | null
19
- ): string {
20
- // If the cell value is undefined or null, return an empty string.
21
- if (value === undefined || value === null) return "";
22
-
23
- const stringValue = String(value);
24
-
25
- // Check if the cell is ranked.
26
- const isRanked = isRankedCell(table, row, columnId);
27
-
28
- // Return the unranked cell, as-is, in string form.
29
- if (!isRanked) return stringValue;
30
-
31
- // Tokenise the current global filter.
32
- const tokens = getTokens(table);
33
-
34
- // If there are no tokens, return the value as-is, in string form.
35
- if (tokens.length === 0) return stringValue;
36
-
37
- // Create regex pattern.
38
- const regex = getTokensRegex(tokens);
39
-
40
- return stringValue.replace(regex, "<mark>$1</mark>");
41
- }
42
-
43
6
  /**
44
7
  * Returns the current global filter tokens from the table.
45
8
  * @param table - Table.
@@ -13,6 +13,11 @@ export const RowSelectionCell = <T extends RowData, TValue = unknown>({
13
13
  checked={getIsSelected()}
14
14
  checkedIcon={<CheckedIcon />}
15
15
  icon={<UncheckedIcon />}
16
+ /*
17
+ * Prevents click events from bubbling up to parent components
18
+ * (such as CardActionArea or Accordion) when the checkbox is activated.
19
+ */
20
+ onClick={(e) => e.stopPropagation()}
16
21
  onChange={getToggleSelectedHandler()}
17
22
  />
18
23
  );
@@ -0,0 +1,25 @@
1
+ import { Row, RowData } from "@tanstack/react-table";
2
+
3
+ /**
4
+ * Handles toggling the expanded state of a row.
5
+ * Rows can not be expanded if:
6
+ * - the row can not be expanded.
7
+ * - the row is grouped (expanded rows are not supported on grouped rows).
8
+ * - the user is selecting text.
9
+ * @param row - Row.
10
+ */
11
+ export function handleToggleExpanded<T extends RowData>(row: Row<T>): void {
12
+ const { getCanExpand, getIsGrouped, toggleExpanded } = row;
13
+
14
+ // Row can not be expanded.
15
+ if (!getCanExpand()) return;
16
+
17
+ // Row is grouped - row expansion not supported on a grouped row.
18
+ if (getIsGrouped()) return;
19
+
20
+ // User is selecting text - do not toggle row expanded state.
21
+ if (window.getSelection()?.toString()) return;
22
+
23
+ // Toggle row expanded state.
24
+ toggleExpanded();
25
+ }
@@ -1,19 +1,22 @@
1
1
  import { css } from "@emotion/react";
2
2
  import styled from "@emotion/styled";
3
3
  import { TableRow as MTableRow } from "@mui/material";
4
- import {
5
- primaryLightest,
6
- smokeLightest,
7
- } from "../../../../styles/common/mixins/colors";
4
+ import { PALETTE } from "../../../../styles/common/constants/palette";
8
5
  import { textBodySmall500 } from "../../../../styles/common/mixins/fonts";
9
6
 
10
7
  export interface StyledTableRowProps {
8
+ canExpand?: boolean;
9
+ isExpanded?: boolean;
11
10
  isGrouped?: boolean;
12
11
  isPreview?: boolean;
13
12
  }
14
13
 
15
14
  export const StyledTableRow = styled(MTableRow, {
16
- shouldForwardProp: (prop) => prop !== "isPreview" && prop !== "isGrouped",
15
+ shouldForwardProp: (prop) =>
16
+ prop !== "canExpand" &&
17
+ prop !== "isExpanded" &&
18
+ prop !== "isPreview" &&
19
+ prop !== "isGrouped",
17
20
  })<StyledTableRowProps>`
18
21
  && {
19
22
  transition: background-color 300ms ease-in;
@@ -21,7 +24,7 @@ export const StyledTableRow = styled(MTableRow, {
21
24
  ${(props) =>
22
25
  props.isGrouped &&
23
26
  css`
24
- background-color: ${smokeLightest(props)};
27
+ background-color: ${PALETTE.SMOKE_LIGHTEST};
25
28
 
26
29
  td {
27
30
  ${textBodySmall500(props)};
@@ -30,10 +33,26 @@ export const StyledTableRow = styled(MTableRow, {
30
33
  }
31
34
  `}
32
35
 
33
- ${(props) =>
34
- props.isPreview &&
36
+ ${({ canExpand, isExpanded, isGrouped }) =>
37
+ !isGrouped &&
38
+ canExpand &&
39
+ css`
40
+ cursor: pointer;
41
+
42
+ &:hover {
43
+ background-color: #f8fbfd;
44
+ }
45
+
46
+ ${isExpanded &&
47
+ css`
48
+ background-color: #f8fbfd;
49
+ `}
50
+ `}
51
+
52
+ ${({ isPreview }) =>
53
+ isPreview &&
35
54
  css`
36
- background-color: ${primaryLightest(props)};
55
+ background-color: ${PALETTE.PRIMARY_LIGHTEST};
37
56
  `}
38
57
  }
39
58
  `;
@@ -7,6 +7,7 @@ import {
7
7
  getTableCellAlign,
8
8
  getTableCellPadding,
9
9
  } from "../TableCell/common/utils";
10
+ import { handleToggleExpanded } from "../TableFeatures/RowExpanding/utils";
10
11
  import { StyledTableRow } from "../TableRow/tableRow.styles";
11
12
 
12
13
  export interface TableRowsProps<T extends RowData> {
@@ -24,13 +25,16 @@ export const TableRows = <T extends RowData>({
24
25
  {virtualItems.map((virtualRow) => {
25
26
  const rowIndex = virtualRow.index;
26
27
  const row = rows[rowIndex] as Row<T>;
27
- const { getIsGrouped, getIsPreview } = row;
28
+ const { getCanExpand, getIsExpanded, getIsGrouped, getIsPreview } = row;
28
29
  return (
29
30
  <StyledTableRow
30
31
  key={row.id}
32
+ canExpand={getCanExpand()}
31
33
  data-index={rowIndex}
34
+ isExpanded={getIsExpanded()}
32
35
  isGrouped={getIsGrouped()}
33
36
  isPreview={getIsPreview()}
37
+ onClick={() => handleToggleExpanded(row)}
34
38
  ref={virtualizer.measureElement}
35
39
  >
36
40
  {row.getVisibleCells().map((cell, i) => {
@@ -1,3 +0,0 @@
1
- import { CellContext, RowData } from "@tanstack/react-table";
2
- import { MarkdownCellProps } from "../types";
3
- export type GetValue = CellContext<RowData, MarkdownCellProps>["getValue"];
@@ -1,3 +0,0 @@
1
- export type MarkdownCellProps = {
2
- values: string;
3
- };
@@ -1,4 +0,0 @@
1
- import { CellContext, RowData } from "@tanstack/react-table";
2
- import { MarkdownCellProps } from "../types";
3
-
4
- export type GetValue = CellContext<RowData, MarkdownCellProps>["getValue"];
@@ -1,3 +0,0 @@
1
- export type MarkdownCellProps = {
2
- values: string;
3
- };