@databiosphere/findable-ui 38.3.0 → 39.1.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 (74) hide show
  1. package/.release-please-manifest.json +1 -1
  2. package/CHANGELOG.md +18 -0
  3. package/lib/components/DataDictionary/components/Entities/entities.d.ts +1 -1
  4. package/lib/components/DataDictionary/components/Entities/entities.js +2 -2
  5. package/lib/components/DataDictionary/components/Entities/types.d.ts +0 -2
  6. package/lib/components/DataDictionary/components/Entity/entity.d.ts +1 -1
  7. package/lib/components/DataDictionary/components/Entity/entity.js +3 -3
  8. package/lib/components/DataDictionary/components/Entity/entity.styles.js +0 -2
  9. package/lib/components/DataDictionary/components/Entity/types.d.ts +0 -2
  10. package/lib/components/DataDictionary/components/Table/table.js +6 -8
  11. package/lib/components/DataDictionary/components/Table/table.styles.d.ts +7 -0
  12. package/lib/components/DataDictionary/components/Table/table.styles.js +8 -0
  13. package/lib/components/DataDictionary/dataDictionary.js +6 -1
  14. package/lib/components/Detail/components/Table/components/TableRows/components/CollapsableRows/collapsableRows.js +1 -1
  15. package/lib/components/Detail/components/Table/components/TableRows/tableRows.js +1 -1
  16. package/lib/components/Index/index.styles.js +3 -1
  17. package/lib/components/MarkdownRenderer/markdownRenderer.d.ts +1 -1
  18. package/lib/components/MarkdownRenderer/markdownRenderer.js +6 -3
  19. package/lib/components/MarkdownRenderer/rehypeHighlight.d.ts +10 -0
  20. package/lib/components/MarkdownRenderer/rehypeHighlight.js +49 -0
  21. package/lib/components/MarkdownRenderer/stories/args.d.ts +3 -0
  22. package/lib/components/MarkdownRenderer/stories/args.js +4 -0
  23. package/lib/components/MarkdownRenderer/stories/markdownRenderer.stories.d.ts +6 -0
  24. package/lib/components/MarkdownRenderer/stories/markdownRenderer.stories.js +22 -0
  25. package/lib/components/MarkdownRenderer/types.d.ts +1 -0
  26. package/lib/components/Table/columnDef/globalFilter/utils.js +5 -5
  27. package/lib/components/Table/components/TableCell/components/MarkdownCell/markdownCell.d.ts +2 -3
  28. package/lib/components/Table/components/TableCell/components/MarkdownCell/markdownCell.js +10 -5
  29. package/lib/components/Table/components/TableCell/components/MarkdownCell/stories/args.js +8 -6
  30. package/lib/components/Table/components/TableCell/components/RankedCell/rankedCell.d.ts +2 -2
  31. package/lib/components/Table/components/TableCell/components/RankedCell/rankedCell.js +12 -4
  32. package/lib/components/Table/components/TableCell/components/RankedCell/utils.d.ts +0 -9
  33. package/lib/components/Table/components/TableCell/components/RankedCell/utils.js +0 -27
  34. package/lib/components/common/AnchorLink/anchorLink.d.ts +3 -1
  35. package/lib/components/common/AnchorLink/anchorLink.js +2 -2
  36. package/lib/components/common/AnchorLink/anchorLink.styles.js +2 -2
  37. package/lib/hooks/useHtmlStyle/constants.d.ts +2 -0
  38. package/lib/hooks/useHtmlStyle/constants.js +3 -0
  39. package/lib/hooks/useHtmlStyle/hook.d.ts +2 -0
  40. package/lib/hooks/useHtmlStyle/hook.js +18 -0
  41. package/lib/hooks/useHtmlStyle/types.d.ts +2 -0
  42. package/package.json +1 -1
  43. package/src/components/DataDictionary/components/Entities/entities.tsx +1 -2
  44. package/src/components/DataDictionary/components/Entities/types.ts +0 -2
  45. package/src/components/DataDictionary/components/Entity/entity.styles.ts +1 -4
  46. package/src/components/DataDictionary/components/Entity/entity.tsx +9 -9
  47. package/src/components/DataDictionary/components/Entity/types.ts +0 -2
  48. package/src/components/DataDictionary/components/Table/table.styles.ts +9 -0
  49. package/src/components/DataDictionary/components/Table/table.tsx +17 -20
  50. package/src/components/DataDictionary/dataDictionary.tsx +7 -1
  51. package/src/components/Detail/components/Table/components/TableRows/components/CollapsableRows/collapsableRows.tsx +5 -1
  52. package/src/components/Detail/components/Table/components/TableRows/tableRows.tsx +1 -0
  53. package/src/components/Index/index.styles.ts +3 -1
  54. package/src/components/MarkdownRenderer/markdownRenderer.tsx +12 -2
  55. package/src/components/MarkdownRenderer/rehypeHighlight.ts +54 -0
  56. package/src/components/MarkdownRenderer/stories/args.ts +8 -0
  57. package/src/components/MarkdownRenderer/stories/markdownRenderer.stories.tsx +33 -0
  58. package/src/components/MarkdownRenderer/types.ts +1 -0
  59. package/src/components/Table/columnDef/globalFilter/utils.ts +5 -5
  60. package/src/components/Table/components/TableCell/components/MarkdownCell/markdownCell.tsx +17 -7
  61. package/src/components/Table/components/TableCell/components/MarkdownCell/stories/args.ts +14 -13
  62. package/src/components/Table/components/TableCell/components/RankedCell/rankedCell.tsx +21 -4
  63. package/src/components/Table/components/TableCell/components/RankedCell/utils.ts +0 -37
  64. package/src/components/common/AnchorLink/anchorLink.styles.ts +2 -2
  65. package/src/components/common/AnchorLink/anchorLink.tsx +4 -1
  66. package/src/hooks/useHtmlStyle/constants.ts +5 -0
  67. package/src/hooks/useHtmlStyle/hook.ts +23 -0
  68. package/src/hooks/useHtmlStyle/types.ts +3 -0
  69. package/lib/components/Table/components/TableCell/components/MarkdownCell/stories/types.d.ts +0 -3
  70. package/lib/components/Table/components/TableCell/components/MarkdownCell/types.d.ts +0 -3
  71. package/lib/components/Table/components/TableCell/components/MarkdownCell/types.js +0 -1
  72. package/src/components/Table/components/TableCell/components/MarkdownCell/stories/types.ts +0 -4
  73. package/src/components/Table/components/TableCell/components/MarkdownCell/types.ts +0 -3
  74. /package/lib/{components/Table/components/TableCell/components/MarkdownCell/stories → hooks/useHtmlStyle}/types.js +0 -0
@@ -0,0 +1,18 @@
1
+ import { useLayoutEffect } from "react";
2
+ export const useHtmlStyle = (property, value) => {
3
+ useLayoutEffect(() => {
4
+ // Get the HTML element.
5
+ const el = document.documentElement;
6
+ // Get the previous value.
7
+ const previousValue = el.style.getPropertyValue(property);
8
+ if (value !== null)
9
+ el.style.setProperty(property, value);
10
+ return () => {
11
+ // Restore or remove property value on the HTML element.
12
+ if (previousValue)
13
+ el.style.setProperty(property, previousValue);
14
+ else
15
+ el.style.removeProperty(property);
16
+ };
17
+ }, [property, value]);
18
+ };
@@ -0,0 +1,2 @@
1
+ import type * as CSS from "csstype";
2
+ export type CSSPropHyphen = keyof CSS.PropertiesHyphen;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@databiosphere/findable-ui",
3
- "version": "38.3.0",
3
+ "version": "39.1.0",
4
4
  "description": "",
5
5
  "scripts": {
6
6
  "test": "node --experimental-vm-modules node_modules/jest/bin/jest.js",
@@ -8,7 +8,6 @@ import { GRID_PROPS, NO_RESULTS_PROPS } from "./constants";
8
8
  import { ClassesProps } from "./types";
9
9
 
10
10
  export const Entities = <T extends RowData = Attribute>({
11
- spacing,
12
11
  table,
13
12
  }: ClassesProps<T>): JSX.Element => {
14
13
  const { getGroupedRowModel } = table;
@@ -20,7 +19,7 @@ export const Entities = <T extends RowData = Attribute>({
20
19
  <Grid {...GRID_PROPS}>
21
20
  {/* Render grouped rows where each "group" is a class e.g. "donor" */}
22
21
  {rows.map((row) => (
23
- <Entity key={row.id} row={row} spacing={spacing} table={table} />
22
+ <Entity key={row.id} row={row} table={table} />
24
23
  ))}
25
24
  </Grid>
26
25
  );
@@ -1,8 +1,6 @@
1
1
  import { RowData, Table } from "@tanstack/react-table";
2
2
  import { Attribute } from "../../../../common/entities";
3
- import { LayoutSpacing } from "../../../../hooks/UseLayoutSpacing/types";
4
3
 
5
4
  export interface ClassesProps<T extends RowData = Attribute> {
6
- spacing?: LayoutSpacing;
7
5
  table: Table<T>;
8
6
  }
@@ -1,10 +1,7 @@
1
1
  import styled from "@emotion/styled";
2
2
  import { Typography } from "@mui/material";
3
- import { LayoutSpacing } from "../../../../hooks/UseLayoutSpacing/types";
4
-
5
- export const StyledTypography = styled(Typography)<Partial<LayoutSpacing>>`
6
- scroll-margin-top: ${({ top = 0 }) => top}px;
7
3
 
4
+ export const StyledTypography = styled(Typography)`
8
5
  &:hover a {
9
6
  opacity: 1;
10
7
  }
@@ -12,7 +12,6 @@ import { getClassMeta } from "./utils";
12
12
 
13
13
  export const Entity = <T extends RowData = Attribute>({
14
14
  row,
15
- spacing,
16
15
  table,
17
16
  }: EntityProps<T>): JSX.Element | null => {
18
17
  // Get class key from row.
@@ -32,17 +31,18 @@ export const Entity = <T extends RowData = Attribute>({
32
31
  component="h3"
33
32
  id={classKey}
34
33
  variant={TYPOGRAPHY_PROPS.VARIANT.TEXT_HEADING_SMALL}
35
- {...spacing}
36
34
  >
37
35
  {cls.title} <AnchorLink anchorLink={classKey} />
38
36
  </StyledTypography>
39
- <Typography
40
- color={TYPOGRAPHY_PROPS.COLOR.INK_LIGHT}
41
- component="div"
42
- variant={TYPOGRAPHY_PROPS.VARIANT.TEXT_BODY_400_2_LINES}
43
- >
44
- {cls.description}
45
- </Typography>
37
+ {cls.description && (
38
+ <Typography
39
+ color={TYPOGRAPHY_PROPS.COLOR.INK_LIGHT}
40
+ component="div"
41
+ variant={TYPOGRAPHY_PROPS.VARIANT.TEXT_BODY_400_2_LINES}
42
+ >
43
+ {cls.description}
44
+ </Typography>
45
+ )}
46
46
  </Grid>
47
47
  {/* Class attributes table */}
48
48
  <Table row={row} table={table} />
@@ -1,9 +1,7 @@
1
1
  import { Row, RowData, Table } from "@tanstack/react-table";
2
2
  import { Attribute } from "../../../../common/entities";
3
- import { LayoutSpacing } from "../../../../hooks/UseLayoutSpacing/types";
4
3
 
5
4
  export interface EntityProps<T extends RowData = Attribute> {
6
5
  row: Row<T>;
7
- spacing?: LayoutSpacing;
8
6
  table: Table<T>;
9
7
  }
@@ -0,0 +1,9 @@
1
+ import styled from "@emotion/styled";
2
+ import { PALETTE } from "../../../../styles/common/constants/palette";
3
+ import { RoundedPaper } from "../../../common/Paper/components/RoundedPaper/roundedPaper";
4
+
5
+ export const StyledRoundedPaper = styled(RoundedPaper)`
6
+ background-color: ${PALETTE.SMOKE_MAIN};
7
+ display: grid;
8
+ gap: 1px;
9
+ `;
@@ -6,8 +6,7 @@ import { ROW_DIRECTION } from "../../../Table/common/entities";
6
6
  import { TableHead } from "../../../Table/components/TableHead/tableHead";
7
7
  import { GridTable } from "../../../Table/table.styles";
8
8
  import { getColumnTrackSizing } from "../../../TableCreator/options/columnTrackSizing/utils";
9
- import { RoundedPaper } from "../../../common/Paper/components/RoundedPaper/roundedPaper";
10
- import { GridPaper } from "../../../common/Paper/paper.styles";
9
+ import { StyledRoundedPaper } from "./table.styles";
11
10
  import { TableProps } from "./types";
12
11
 
13
12
  export const Table = <T extends RowData>({
@@ -15,23 +14,21 @@ export const Table = <T extends RowData>({
15
14
  table,
16
15
  }: TableProps<T>): JSX.Element => {
17
16
  return (
18
- <RoundedPaper elevation={0}>
19
- <GridPaper>
20
- <TableContainer>
21
- <GridTable
22
- gridTemplateColumns={getColumnTrackSizing(
23
- table.getVisibleFlatColumns()
24
- )}
25
- >
26
- <TableHead tableInstance={table} />
27
- <TableBody
28
- rowDirection={ROW_DIRECTION.DEFAULT}
29
- rows={row.getLeafRows()}
30
- tableInstance={table}
31
- />
32
- </GridTable>
33
- </TableContainer>
34
- </GridPaper>
35
- </RoundedPaper>
17
+ <StyledRoundedPaper elevation={0}>
18
+ <TableContainer>
19
+ <GridTable
20
+ gridTemplateColumns={getColumnTrackSizing(
21
+ table.getVisibleFlatColumns()
22
+ )}
23
+ >
24
+ <TableHead tableInstance={table} />
25
+ <TableBody
26
+ rowDirection={ROW_DIRECTION.DEFAULT}
27
+ rows={row.getLeafRows()}
28
+ tableInstance={table}
29
+ />
30
+ </GridTable>
31
+ </TableContainer>
32
+ </StyledRoundedPaper>
36
33
  );
37
34
  };
@@ -2,6 +2,8 @@ import { Fade } from "@mui/material";
2
2
  import { RowData } from "@tanstack/react-table";
3
3
  import React from "react";
4
4
  import { Attribute } from "../../common/entities";
5
+ import { PROPERTY } from "../../hooks/useHtmlStyle/constants";
6
+ import { useHtmlStyle } from "../../hooks/useHtmlStyle/hook";
5
7
  import { useLayoutSpacing } from "../../hooks/UseLayoutSpacing/hook";
6
8
  import { Description } from "./components/Description/description";
7
9
  import { Entities } from "./components/Entities/entities";
@@ -48,6 +50,10 @@ export const DataDictionary = <T extends RowData = Attribute>({
48
50
  // Dictionary outline.
49
51
  const outline = buildClassesOutline<T>(table);
50
52
 
53
+ // Update scroll-padding-top on the HTML element.
54
+ // Scroll-snaps table rows to below the sticky filters.
55
+ useHtmlStyle(PROPERTY.SCROLL_PADDING_TOP, `${dimensions.height}px`);
56
+
51
57
  return (
52
58
  <Fade in={spacing.top > 0}>
53
59
  {/* Fade in when header is measured. */}
@@ -66,7 +72,7 @@ export const DataDictionary = <T extends RowData = Attribute>({
66
72
  {/* Fade in entities when filters are measured. */}
67
73
  <EntitiesLayout spacing={entitiesSpacing}>
68
74
  <Description description={description} />
69
- <Entities spacing={entitiesSpacing} table={table} />
75
+ <Entities table={table} />
70
76
  </EntitiesLayout>
71
77
  </Fade>
72
78
  </View>
@@ -27,7 +27,11 @@ export const CollapsableRows = <T extends RowData>({
27
27
  {(leafOrSubRows || rows).map((row) => {
28
28
  if (row.depth > 0) return null; // Hide sub rows.
29
29
  return (
30
- <StyledTableRow key={row.id} isPreview={row.getIsPreview()}>
30
+ <StyledTableRow
31
+ key={row.id}
32
+ id={row.id}
33
+ isPreview={row.getIsPreview()}
34
+ >
31
35
  <CollapsableCell
32
36
  isDisabled={isCollapsableRowDisabled(tableInstance)}
33
37
  row={row}
@@ -35,6 +35,7 @@ export const TableRows = <T extends RowData>({
35
35
  return (
36
36
  <StyledTableRow
37
37
  key={row.id}
38
+ id={row.id}
38
39
  canExpand={row.getCanExpand()}
39
40
  isExpanded={row.getIsExpanded()}
40
41
  isGrouped={row.getIsGrouped()}
@@ -5,7 +5,9 @@ import { PALETTE } from "../../styles/common/constants/palette";
5
5
  import { mediaTabletDown } from "../../styles/common/mixins/breakpoints";
6
6
  import { FluidPaper } from "../common/Paper/components/FluidPaper/fluidPaper";
7
7
 
8
- export const StyledGridEntityView = styled(Grid)<LayoutSpacing>`
8
+ export const StyledGridEntityView = styled(Grid, {
9
+ shouldForwardProp: (prop) => prop !== "bottom" && prop !== "top",
10
+ })<LayoutSpacing>`
9
11
  align-content: flex-start;
10
12
  display: grid;
11
13
  flex: 1;
@@ -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);
@@ -1,29 +1,39 @@
1
1
  import { CellContext, RowData } from "@tanstack/react-table";
2
- import { BaseComponentProps } from "components/types";
3
2
  import React from "react";
4
3
  import { COMPONENTS } from "../../../../../MarkdownRenderer/constants";
4
+ import { BaseComponentProps } from "../../../../../types";
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>>;
@@ -1,18 +1,22 @@
1
1
  import { CellContext, RowData } from "@tanstack/react-table";
2
2
  import React, { Fragment } from "react";
3
- import { BaseComponentProps } from "../../../../../types";
3
+ import { BaseComponentProps, ChildrenProps } from "../../../../../types";
4
4
  import { getTokens, getTokensRegex, isRankedCell } from "./utils";
5
5
 
6
6
  export const RankedCell = <
7
7
  T extends RowData,
8
8
  TValue = string | undefined | null
9
9
  >({
10
+ /* children - e.g. AnchorLink component */
11
+ children,
10
12
  className,
11
13
  column,
12
14
  getValue,
13
15
  row,
14
16
  table,
15
- }: BaseComponentProps & CellContext<T, TValue>): JSX.Element | null => {
17
+ }: BaseComponentProps &
18
+ CellContext<T, TValue> &
19
+ ChildrenProps): JSX.Element | null => {
16
20
  // Get the cell value.
17
21
  const value = getValue();
18
22
 
@@ -26,13 +30,25 @@ export const RankedCell = <
26
30
  const isRanked = isRankedCell(table, row, column.id);
27
31
 
28
32
  // Return the unranked cell, as-is.
29
- if (!isRanked) return <Fragment>{stringValue}</Fragment>;
33
+ if (!isRanked)
34
+ return (
35
+ <Fragment>
36
+ {stringValue}
37
+ {children}
38
+ </Fragment>
39
+ );
30
40
 
31
41
  // Tokenise the current global filter.
32
42
  const tokens = getTokens(table);
33
43
 
34
44
  // If there are no tokens, return the value as-is.
35
- if (tokens.length === 0) return <Fragment>{stringValue}</Fragment>;
45
+ if (tokens.length === 0)
46
+ return (
47
+ <Fragment>
48
+ {stringValue}
49
+ {children}
50
+ </Fragment>
51
+ );
36
52
 
37
53
  // Create regex pattern.
38
54
  const regex = getTokensRegex(tokens);
@@ -45,6 +61,7 @@ export const RankedCell = <
45
61
  {part.match(regex) ? <mark>{part}</mark> : part}
46
62
  </Fragment>
47
63
  ))}
64
+ {children}
48
65
  </span>
49
66
  );
50
67
  };
@@ -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.
@@ -1,9 +1,9 @@
1
1
  import styled from "@emotion/styled";
2
2
  import Link from "next/link";
3
- import { inkLight } from "../../../styles/common/mixins/colors";
3
+ import { PALETTE } from "../../../styles/common/constants/palette";
4
4
 
5
5
  export const StyledNextLink = styled(Link)`
6
- color: ${inkLight};
6
+ color: ${PALETTE.INK_LIGHT};
7
7
  margin-left: 4px;
8
8
  opacity: 0;
9
9
  position: absolute;