@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.
- package/.release-please-manifest.json +1 -1
- package/CHANGELOG.md +18 -0
- package/lib/components/DataDictionary/components/Entities/entities.d.ts +1 -1
- package/lib/components/DataDictionary/components/Entities/entities.js +2 -2
- package/lib/components/DataDictionary/components/Entities/types.d.ts +0 -2
- package/lib/components/DataDictionary/components/Entity/entity.d.ts +1 -1
- package/lib/components/DataDictionary/components/Entity/entity.js +3 -3
- package/lib/components/DataDictionary/components/Entity/entity.styles.js +0 -2
- package/lib/components/DataDictionary/components/Entity/types.d.ts +0 -2
- package/lib/components/DataDictionary/components/Table/table.js +6 -8
- package/lib/components/DataDictionary/components/Table/table.styles.d.ts +7 -0
- package/lib/components/DataDictionary/components/Table/table.styles.js +8 -0
- package/lib/components/DataDictionary/dataDictionary.js +6 -1
- package/lib/components/Detail/components/Table/components/TableRows/components/CollapsableRows/collapsableRows.js +1 -1
- package/lib/components/Detail/components/Table/components/TableRows/tableRows.js +1 -1
- package/lib/components/Index/index.styles.js +3 -1
- package/lib/components/MarkdownRenderer/markdownRenderer.d.ts +1 -1
- package/lib/components/MarkdownRenderer/markdownRenderer.js +6 -3
- package/lib/components/MarkdownRenderer/rehypeHighlight.d.ts +10 -0
- package/lib/components/MarkdownRenderer/rehypeHighlight.js +49 -0
- package/lib/components/MarkdownRenderer/stories/args.d.ts +3 -0
- package/lib/components/MarkdownRenderer/stories/args.js +4 -0
- package/lib/components/MarkdownRenderer/stories/markdownRenderer.stories.d.ts +6 -0
- package/lib/components/MarkdownRenderer/stories/markdownRenderer.stories.js +22 -0
- package/lib/components/MarkdownRenderer/types.d.ts +1 -0
- package/lib/components/Table/columnDef/globalFilter/utils.js +5 -5
- package/lib/components/Table/components/TableCell/components/MarkdownCell/markdownCell.d.ts +2 -3
- package/lib/components/Table/components/TableCell/components/MarkdownCell/markdownCell.js +10 -5
- package/lib/components/Table/components/TableCell/components/MarkdownCell/stories/args.js +8 -6
- package/lib/components/Table/components/TableCell/components/RankedCell/rankedCell.d.ts +2 -2
- package/lib/components/Table/components/TableCell/components/RankedCell/rankedCell.js +12 -4
- package/lib/components/Table/components/TableCell/components/RankedCell/utils.d.ts +0 -9
- package/lib/components/Table/components/TableCell/components/RankedCell/utils.js +0 -27
- package/lib/components/common/AnchorLink/anchorLink.d.ts +3 -1
- package/lib/components/common/AnchorLink/anchorLink.js +2 -2
- package/lib/components/common/AnchorLink/anchorLink.styles.js +2 -2
- package/lib/hooks/useHtmlStyle/constants.d.ts +2 -0
- package/lib/hooks/useHtmlStyle/constants.js +3 -0
- package/lib/hooks/useHtmlStyle/hook.d.ts +2 -0
- package/lib/hooks/useHtmlStyle/hook.js +18 -0
- package/lib/hooks/useHtmlStyle/types.d.ts +2 -0
- package/package.json +1 -1
- package/src/components/DataDictionary/components/Entities/entities.tsx +1 -2
- package/src/components/DataDictionary/components/Entities/types.ts +0 -2
- package/src/components/DataDictionary/components/Entity/entity.styles.ts +1 -4
- package/src/components/DataDictionary/components/Entity/entity.tsx +9 -9
- package/src/components/DataDictionary/components/Entity/types.ts +0 -2
- package/src/components/DataDictionary/components/Table/table.styles.ts +9 -0
- package/src/components/DataDictionary/components/Table/table.tsx +17 -20
- package/src/components/DataDictionary/dataDictionary.tsx +7 -1
- package/src/components/Detail/components/Table/components/TableRows/components/CollapsableRows/collapsableRows.tsx +5 -1
- package/src/components/Detail/components/Table/components/TableRows/tableRows.tsx +1 -0
- package/src/components/Index/index.styles.ts +3 -1
- package/src/components/MarkdownRenderer/markdownRenderer.tsx +12 -2
- package/src/components/MarkdownRenderer/rehypeHighlight.ts +54 -0
- package/src/components/MarkdownRenderer/stories/args.ts +8 -0
- package/src/components/MarkdownRenderer/stories/markdownRenderer.stories.tsx +33 -0
- package/src/components/MarkdownRenderer/types.ts +1 -0
- package/src/components/Table/columnDef/globalFilter/utils.ts +5 -5
- package/src/components/Table/components/TableCell/components/MarkdownCell/markdownCell.tsx +17 -7
- package/src/components/Table/components/TableCell/components/MarkdownCell/stories/args.ts +14 -13
- package/src/components/Table/components/TableCell/components/RankedCell/rankedCell.tsx +21 -4
- package/src/components/Table/components/TableCell/components/RankedCell/utils.ts +0 -37
- package/src/components/common/AnchorLink/anchorLink.styles.ts +2 -2
- package/src/components/common/AnchorLink/anchorLink.tsx +4 -1
- package/src/hooks/useHtmlStyle/constants.ts +5 -0
- package/src/hooks/useHtmlStyle/hook.ts +23 -0
- package/src/hooks/useHtmlStyle/types.ts +3 -0
- package/lib/components/Table/components/TableCell/components/MarkdownCell/stories/types.d.ts +0 -3
- package/lib/components/Table/components/TableCell/components/MarkdownCell/types.d.ts +0 -3
- package/lib/components/Table/components/TableCell/components/MarkdownCell/types.js +0 -1
- package/src/components/Table/components/TableCell/components/MarkdownCell/stories/types.ts +0 -4
- package/src/components/Table/components/TableCell/components/MarkdownCell/types.ts +0 -3
- /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
|
+
};
|
package/package.json
CHANGED
|
@@ -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}
|
|
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
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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 {
|
|
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
|
-
<
|
|
19
|
-
<
|
|
20
|
-
<
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
|
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
|
|
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}
|
|
@@ -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
|
|
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, {
|
|
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 <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)\\" <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)\\" <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
|
+
};
|
|
@@ -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
|
-
|
|
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
|
|
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
|
|
17
|
-
if (!
|
|
18
|
-
|
|
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
|
-
|
|
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
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
}
|
|
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 <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)\\" <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)\\" <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
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
}
|
|
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 &
|
|
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)
|
|
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)
|
|
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 {
|
|
3
|
+
import { PALETTE } from "../../../styles/common/constants/palette";
|
|
4
4
|
|
|
5
5
|
export const StyledNextLink = styled(Link)`
|
|
6
|
-
color: ${
|
|
6
|
+
color: ${PALETTE.INK_LIGHT};
|
|
7
7
|
margin-left: 4px;
|
|
8
8
|
opacity: 0;
|
|
9
9
|
position: absolute;
|