@databiosphere/findable-ui 45.1.0 → 46.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 (52) hide show
  1. package/.release-please-manifest.json +1 -1
  2. package/CHANGELOG.md +39 -0
  3. package/lib/components/Filter/components/adapters/tanstack/ColumnFiltersAdapter/utils.js +10 -1
  4. package/lib/components/Index/components/EntityView/components/layout/Summary/summary.js +1 -1
  5. package/lib/components/Layout/components/Outline/outline.js +3 -1
  6. package/lib/components/Layout/components/Outline/types.d.ts +1 -1
  7. package/lib/components/common/Button/components/FileDownloadButton/fileDownloadButton.d.ts +5 -0
  8. package/lib/components/common/Button/components/FileDownloadButton/fileDownloadButton.js +14 -0
  9. package/lib/utils/mdx/files/mapMDXSlugByFilePaths.d.ts +8 -0
  10. package/lib/utils/mdx/files/mapMDXSlugByFilePaths.js +39 -0
  11. package/lib/utils/mdx/files/resolveRelativeDirs.d.ts +6 -0
  12. package/lib/utils/mdx/files/resolveRelativeDirs.js +9 -0
  13. package/lib/utils/mdx/frontmatter/getMatter.d.ts +8 -0
  14. package/lib/utils/mdx/frontmatter/getMatter.js +12 -0
  15. package/lib/utils/mdx/frontmatter/types.d.ts +7 -0
  16. package/lib/utils/mdx/frontmatter/types.js +1 -0
  17. package/lib/utils/mdx/frontmatter/validateMatter.d.ts +8 -0
  18. package/lib/utils/mdx/frontmatter/validateMatter.js +13 -0
  19. package/lib/utils/mdx/plugins/rehypeSlug.d.ts +6 -0
  20. package/lib/utils/mdx/plugins/rehypeSlug.js +34 -0
  21. package/lib/utils/mdx/plugins/remarkHeadings.d.ts +29 -0
  22. package/lib/utils/mdx/plugins/remarkHeadings.js +50 -0
  23. package/lib/utils/mdx/plugins/utils.d.ts +13 -0
  24. package/lib/utils/mdx/plugins/utils.js +25 -0
  25. package/lib/utils/mdx/staticGeneration/staticPaths.d.ts +8 -0
  26. package/lib/utils/mdx/staticGeneration/staticPaths.js +12 -0
  27. package/lib/utils/mdx/staticGeneration/staticProps.d.ts +5 -0
  28. package/lib/utils/mdx/staticGeneration/staticProps.js +44 -0
  29. package/lib/utils/mdx/staticGeneration/types.d.ts +12 -0
  30. package/lib/utils/mdx/staticGeneration/types.js +1 -0
  31. package/lib/utils/mdx/staticGeneration/utils.d.ts +15 -0
  32. package/lib/utils/mdx/staticGeneration/utils.js +26 -0
  33. package/package.json +8 -5
  34. package/src/components/Filter/components/adapters/tanstack/ColumnFiltersAdapter/utils.ts +20 -4
  35. package/src/components/Index/components/EntityView/components/layout/Summary/summary.tsx +8 -6
  36. package/src/components/Layout/components/Outline/outline.tsx +6 -1
  37. package/src/components/Layout/components/Outline/types.ts +1 -1
  38. package/src/components/common/Button/components/FileDownloadButton/fileDownloadButton.tsx +27 -0
  39. package/src/utils/mdx/files/mapMDXSlugByFilePaths.ts +48 -0
  40. package/src/utils/mdx/files/resolveRelativeDirs.ts +10 -0
  41. package/src/utils/mdx/frontmatter/getMatter.ts +13 -0
  42. package/src/utils/mdx/frontmatter/types.ts +7 -0
  43. package/src/utils/mdx/frontmatter/validateMatter.ts +20 -0
  44. package/src/utils/mdx/plugins/rehypeSlug.ts +36 -0
  45. package/src/utils/mdx/plugins/remarkHeadings.ts +67 -0
  46. package/src/utils/mdx/plugins/utils.ts +27 -0
  47. package/src/utils/mdx/staticGeneration/staticPaths.ts +18 -0
  48. package/src/utils/mdx/staticGeneration/staticProps.ts +62 -0
  49. package/src/utils/mdx/staticGeneration/types.ts +16 -0
  50. package/src/utils/mdx/staticGeneration/utils.ts +32 -0
  51. package/tests/chart.test.tsx +4 -2
  52. package/tests/markdownCell.test.tsx +1 -2
@@ -1,3 +1,3 @@
1
1
  {
2
- ".": "45.1.0"
2
+ ".": "46.1.0"
3
3
  }
package/CHANGELOG.md CHANGED
@@ -1,5 +1,44 @@
1
1
  # Changelog
2
2
 
3
+ ## [46.1.0](https://github.com/DataBiosphere/findable-ui/compare/v46.0.0...v46.1.0) (2025-11-03)
4
+
5
+
6
+ ### Features
7
+
8
+ * update column filter adapter to render custom filter values ([#694](https://github.com/DataBiosphere/findable-ui/issues/694)) ([#695](https://github.com/DataBiosphere/findable-ui/issues/695)) ([67404e9](https://github.com/DataBiosphere/findable-ui/commit/67404e940ea7610153e8a756609cd402225fa0cc))
9
+
10
+
11
+ ### Bug Fixes
12
+
13
+ * update column filter adapter to always show filter values with a zero count ([#696](https://github.com/DataBiosphere/findable-ui/issues/696)) ([#697](https://github.com/DataBiosphere/findable-ui/issues/697)) ([a693f3b](https://github.com/DataBiosphere/findable-ui/commit/a693f3bf4f84cacf5602bbbe134b7a4f611e36dc))
14
+
15
+ ## [46.0.0](https://github.com/DataBiosphere/findable-ui/compare/v45.1.0...v46.0.0) (2025-10-28)
16
+
17
+
18
+ ### ⚠ BREAKING CHANGES
19
+
20
+ * add get static props helper function ([#691](https://github.com/DataBiosphere/findable-ui/issues/691)) (#692)
21
+ * add helper functions remarkHeadings and rehypeSlug ([#684](https://github.com/DataBiosphere/findable-ui/issues/684)) (#685)
22
+ * add get static paths helper function ([#688](https://github.com/DataBiosphere/findable-ui/issues/688)) (#693)
23
+
24
+ ### Features
25
+
26
+ * add get static paths helper function ([#688](https://github.com/DataBiosphere/findable-ui/issues/688)) ([#693](https://github.com/DataBiosphere/findable-ui/issues/693)) ([887b1d8](https://github.com/DataBiosphere/findable-ui/commit/887b1d82770ce472957e03b483aeeacb06ab8fdd))
27
+ * add get static props helper function ([#691](https://github.com/DataBiosphere/findable-ui/issues/691)) ([#692](https://github.com/DataBiosphere/findable-ui/issues/692)) ([71b2927](https://github.com/DataBiosphere/findable-ui/commit/71b2927059e5e55b6e03bda6dc87dad009533fe9))
28
+ * add helper functions remarkHeadings and rehypeSlug ([#684](https://github.com/DataBiosphere/findable-ui/issues/684)) ([#685](https://github.com/DataBiosphere/findable-ui/issues/685)) ([b130b3c](https://github.com/DataBiosphere/findable-ui/commit/b130b3c4a2527566eadcdab174c937e95338a394))
29
+ * update entity list summary component to optionally display summary label ([#679](https://github.com/DataBiosphere/findable-ui/issues/679)) ([#681](https://github.com/DataBiosphere/findable-ui/issues/681)) ([e57e366](https://github.com/DataBiosphere/findable-ui/commit/e57e3660d92e0c155f5cdb56dcd5769bfc0ca2b0))
30
+ * update outline component prop 'outline' to be optional ([#686](https://github.com/DataBiosphere/findable-ui/issues/686)) ([#687](https://github.com/DataBiosphere/findable-ui/issues/687)) ([eaba690](https://github.com/DataBiosphere/findable-ui/commit/eaba690d4277e69e88f3a6cd11d558c9e27918e6))
31
+
32
+
33
+ ### Bug Fixes
34
+
35
+ * reintstate file download button component ([#667](https://github.com/DataBiosphere/findable-ui/issues/667)) ([#678](https://github.com/DataBiosphere/findable-ui/issues/678)) ([aa538a3](https://github.com/DataBiosphere/findable-ui/commit/aa538a39f64b07e02f4bc512c51c25fb1234fb89))
36
+
37
+
38
+ ### Chores
39
+
40
+ * upgrade packages (minor) and fix security warnings ([#671](https://github.com/DataBiosphere/findable-ui/issues/671)) ([#673](https://github.com/DataBiosphere/findable-ui/issues/673)) ([6b067d5](https://github.com/DataBiosphere/findable-ui/commit/6b067d5e189d72253e6b39551c1ab5d49aa8126a))
41
+
3
42
  ## [45.1.0](https://github.com/DataBiosphere/findable-ui/compare/v45.0.0...v45.1.0) (2025-09-18)
4
43
 
5
44
 
@@ -1,6 +1,7 @@
1
1
  import { isRangeCategoryConfig } from "../../../../../../common/categories/config/range/typeGuards";
2
2
  import { FILTER_SORT } from "../../../../../../common/filters/sort/config/types";
3
3
  import { sortCategoryValueViews } from "../../../../../../common/filters/sort/models/utils";
4
+ import { getSelectCategoryValue } from "../../../../../../hooks/useCategoryFilter";
4
5
  import { getColumnHeader } from "../../../../../Table/common/utils";
5
6
  /**
6
7
  * Adapter for TanStack table to category configs.
@@ -132,8 +133,16 @@ function mapColumnToRangeCategoryView(column, categoryConfig) {
132
133
  */
133
134
  function mapColumnToSelectCategoryView(column, filterSort, categoryConfig) {
134
135
  const facetedUniqueValues = column.getFacetedUniqueValues();
136
+ // Selected values for the column.
137
+ const filterValue = (column.getFilterValue() || []);
138
+ // Update faceted unique values with selected filters that are not in the faceted unique values.
139
+ for (const value of filterValue) {
140
+ if (facetedUniqueValues.has(value))
141
+ continue;
142
+ facetedUniqueValues.set(value, 0);
143
+ }
135
144
  const isDisabled = facetedUniqueValues.size === 0;
136
- const values = mapColumnToSelectCategoryValueView(column, filterSort);
145
+ const values = mapColumnToSelectCategoryValueView(column, filterSort).map(categoryConfig?.mapSelectCategoryValue || getSelectCategoryValue);
137
146
  return {
138
147
  annotation: undefined,
139
148
  enableChartView: false,
@@ -12,5 +12,5 @@ export const Summary = ({ className, }) => {
12
12
  return (React.createElement(StyledGrid, { ...GRID_PROPS, className: className, "data-testid": TEST_IDS.ENTITY_SUMMARY }, summaries.map(([count, label], i) => (React.createElement(Fragment, { key: i },
13
13
  i !== 0 && React.createElement(StyledDot, null),
14
14
  React.createElement(Typography, { variant: TYPOGRAPHY_PROPS.VARIANT.BODY_SMALL_500 }, count),
15
- React.createElement(Typography, { color: TYPOGRAPHY_PROPS.COLOR.INK_LIGHT, variant: TYPOGRAPHY_PROPS.VARIANT.BODY_SMALL_400 }, label))))));
15
+ label && (React.createElement(Typography, { color: TYPOGRAPHY_PROPS.COLOR.INK_LIGHT, variant: TYPOGRAPHY_PROPS.VARIANT.BODY_SMALL_400 }, label)))))));
16
16
  };
@@ -3,7 +3,9 @@ import { DEFAULT_TAB_VALUE } from "./hooks/UseTabs/constants";
3
3
  import { useTabs } from "./hooks/UseTabs/hook";
4
4
  import { StyledTab, StyledTabs } from "./outline.styles";
5
5
  export const Outline = ({ className, Contents, outline, ...props /* MuiTabsProps */ }) => {
6
- const { indicatorColor, onChange, orientation, value } = useTabs(outline);
6
+ const { indicatorColor, onChange, orientation, value } = useTabs(outline ?? []);
7
+ if (!outline?.length)
8
+ return null;
7
9
  return (React.createElement(StyledTabs, { className: className, indicatorColor: indicatorColor, onChange: onChange, orientation: orientation, value: value, ...props },
8
10
  React.createElement(Contents, { value: DEFAULT_TAB_VALUE }),
9
11
  outline.map(({ depth, disabled, hash, value }) => (React.createElement(StyledTab, { depth: depth, disabled: disabled, key: hash, label: value, value: hash })))));
@@ -10,5 +10,5 @@ export interface OutlineItem {
10
10
  }
11
11
  export interface OutlineProps extends BaseComponentProps, TabsProps {
12
12
  Contents: ElementType<ContentsTabProps>;
13
- outline: OutlineItem[];
13
+ outline?: OutlineItem[] | null;
14
14
  }
@@ -0,0 +1,5 @@
1
+ export interface FileDownloadButtonProps {
2
+ fileName?: string;
3
+ fileUrl?: string;
4
+ }
5
+ export declare const FileDownloadButton: ({ fileName, fileUrl, }: FileDownloadButtonProps) => JSX.Element;
@@ -0,0 +1,14 @@
1
+ import { Box } from "@mui/material";
2
+ import React, { useEffect, useRef } from "react";
3
+ export const FileDownloadButton = ({ fileName, fileUrl, }) => {
4
+ const downloadRef = useRef(null);
5
+ // Initiates file download when file url request is successful.
6
+ useEffect(() => {
7
+ if (downloadRef.current && fileName && fileUrl) {
8
+ downloadRef.current.setAttribute("href", fileUrl);
9
+ downloadRef.current.setAttribute("download", fileName);
10
+ downloadRef.current.click();
11
+ }
12
+ }, [fileName, fileUrl]);
13
+ return (React.createElement(Box, { component: "a", download: true, ref: downloadRef, sx: { display: "none" } }));
14
+ };
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Maps each MDX file path to its slug.
3
+ * @param docsDirectory - Docs directory.
4
+ * @param dirPath - Directory path.
5
+ * @param slugByFilePaths - Accumulator: Map of slug by mdx file path.
6
+ * @returns returns slug by mdx file path.
7
+ */
8
+ export declare function mapMDXSlugByFilePaths(docsDirectory: string, dirPath?: string, slugByFilePaths?: Map<string, string[]>): Map<string, string[]>;
@@ -0,0 +1,39 @@
1
+ import fs from "fs";
2
+ import * as path from "path";
3
+ /**
4
+ * Returns true if the file is an MDX file.
5
+ * @param fileName - File name.
6
+ * @returns true if the file is an MDX file.
7
+ */
8
+ function isMdxFile(fileName) {
9
+ return fileName.endsWith(".mdx");
10
+ }
11
+ /**
12
+ * Maps each MDX file path to its slug.
13
+ * @param docsDirectory - Docs directory.
14
+ * @param dirPath - Directory path.
15
+ * @param slugByFilePaths - Accumulator: Map of slug by mdx file path.
16
+ * @returns returns slug by mdx file path.
17
+ */
18
+ export function mapMDXSlugByFilePaths(docsDirectory, dirPath = docsDirectory, slugByFilePaths = new Map()) {
19
+ const dirents = fs.readdirSync(dirPath, { withFileTypes: true });
20
+ return dirents.reduce((acc, dirent) => {
21
+ /* Accumulate the slug for each MDX file. */
22
+ if (dirent.isFile() && isMdxFile(dirent.name)) {
23
+ const mdxPath = path.resolve(dirPath, dirent.name);
24
+ /* Build the slug from the file relative directory and file name. */
25
+ const mdxRelativePath = path.relative(docsDirectory, mdxPath);
26
+ const { dir, name } = path.parse(mdxRelativePath);
27
+ let slug = [];
28
+ if (dir)
29
+ slug = dir.split(path.sep);
30
+ slug.push(name);
31
+ acc.set(mdxPath, slug);
32
+ }
33
+ /* Recurse into subdirectories. */
34
+ if (dirent.isDirectory()) {
35
+ mapMDXSlugByFilePaths(docsDirectory, path.resolve(dirPath, dirent.name), acc);
36
+ }
37
+ return acc;
38
+ }, slugByFilePaths);
39
+ }
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Resolves an absolute docs path from a single set of directory segments.
3
+ * @param relativeDirs - Directory segments relative to the docs root.
4
+ * @returns Absolute path.
5
+ */
6
+ export declare function resolveRelativeDirs(relativeDirs: string[]): string;
@@ -0,0 +1,9 @@
1
+ import * as path from "path";
2
+ /**
3
+ * Resolves an absolute docs path from a single set of directory segments.
4
+ * @param relativeDirs - Directory segments relative to the docs root.
5
+ * @returns Absolute path.
6
+ */
7
+ export function resolveRelativeDirs(relativeDirs) {
8
+ return path.join(process.cwd(), ...relativeDirs);
9
+ }
@@ -0,0 +1,8 @@
1
+ import matter from "gray-matter";
2
+ /**
3
+ * Returns matter object (frontmatter and content) from the given MD/MDX path.
4
+ * Server-only: uses Node fs, import only in SSG/SSR/build contexts.
5
+ * @param filePath - File path of MD/MDX file.
6
+ * @returns matter object.
7
+ */
8
+ export declare function getMatter(filePath: string): matter.GrayMatterFile<string>;
@@ -0,0 +1,12 @@
1
+ import fs from "fs";
2
+ import matter from "gray-matter";
3
+ /**
4
+ * Returns matter object (frontmatter and content) from the given MD/MDX path.
5
+ * Server-only: uses Node fs, import only in SSG/SSR/build contexts.
6
+ * @param filePath - File path of MD/MDX file.
7
+ * @returns matter object.
8
+ */
9
+ export function getMatter(filePath) {
10
+ const markdownWithMeta = fs.readFileSync(filePath, "utf-8");
11
+ return matter(markdownWithMeta);
12
+ }
@@ -0,0 +1,7 @@
1
+ interface BaseFrontmatterProps {
2
+ description: string;
3
+ hidden?: boolean;
4
+ title: string;
5
+ }
6
+ export type FrontmatterProps<F extends object> = BaseFrontmatterProps & F;
7
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,8 @@
1
+ import matter from "gray-matter";
2
+ import { FrontmatterProps } from "./types";
3
+ /**
4
+ * Returns the frontmatter from the given gray matter file data.
5
+ * @param data - Gray matter file data.
6
+ * @returns Frontmatter.
7
+ */
8
+ export declare function validateMatter<F extends object>(data: matter.GrayMatterFile<string>["data"]): FrontmatterProps<F> | undefined;
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Returns the frontmatter from the given gray matter file data.
3
+ * @param data - Gray matter file data.
4
+ * @returns Frontmatter.
5
+ */
6
+ export function validateMatter(data) {
7
+ if ("title" in data &&
8
+ typeof data.title === "string" &&
9
+ "description" in data &&
10
+ typeof data.description === "string") {
11
+ return data;
12
+ }
13
+ }
@@ -0,0 +1,6 @@
1
+ import { Plugin } from "unified";
2
+ /**
3
+ * Rehype plugin to generate an ID for each heading in MDX content.
4
+ * @returns plugin to generate an ID for each heading in MDX content.
5
+ */
6
+ export declare function rehypeSlug(): Plugin;
@@ -0,0 +1,34 @@
1
+ import { visit } from "unist-util-visit";
2
+ import { getHeadingTextValue } from "./remarkHeadings";
3
+ import { generateUniqueId, slugifyHeading } from "./utils";
4
+ /**
5
+ * Rehype plugin to generate an ID for each heading in MDX content.
6
+ * @returns plugin to generate an ID for each heading in MDX content.
7
+ */
8
+ export function rehypeSlug() {
9
+ return (tree) => {
10
+ const setOfIds = new Set();
11
+ visit(tree, "element", (node) => {
12
+ if (/^h[1-6]$/.test(node.tagName)) {
13
+ const headingText = getHeadingTextValue(node.children);
14
+ const headingSlug = slugifyHeading(headingText);
15
+ const id = generateUniqueId(setOfIds, headingSlug);
16
+ // Add the ID to the heading element.
17
+ node.properties.id = id;
18
+ node.properties.style = "position: relative;";
19
+ // Append AnchorLink to the heading element.
20
+ node.children.push({
21
+ attributes: [
22
+ {
23
+ name: "anchorLink",
24
+ type: "mdxJsxAttribute",
25
+ value: id,
26
+ },
27
+ ],
28
+ name: "AnchorLink",
29
+ type: "mdxJsxFlowElement",
30
+ });
31
+ }
32
+ });
33
+ };
34
+ }
@@ -0,0 +1,29 @@
1
+ import { PhrasingContent } from "mdast";
2
+ import { Plugin } from "unified";
3
+ import { OutlineItem } from "../../../components/Layout/components/Outline/types";
4
+ interface Options {
5
+ depth?: {
6
+ max?: number;
7
+ min?: number;
8
+ };
9
+ outline: OutlineItem[];
10
+ }
11
+ /**
12
+ * Remark plugin to generate an outline from MDX content.
13
+ * The outline is a list of headings with their depth, hash, and value.
14
+ * @param options - Options.
15
+ * @param options.depth - Depth of the headings.
16
+ * @param options.depth.max - Maximum depth of the headings.
17
+ * @param options.depth.min - Minimum depth of the headings.
18
+ * @param options.outline - Outline items.
19
+ * @returns plugin to generate an outline from MDX content.
20
+ */
21
+ export declare function remarkHeadings({ depth: { max, min }, outline, }: Options): Plugin;
22
+ /**
23
+ * Returns the value of the heading.
24
+ * @param children - Phrasing content.
25
+ * @param value - List of heading text values.
26
+ * @returns heading text value.
27
+ */
28
+ export declare function getHeadingTextValue(children: PhrasingContent[], value?: string[]): string;
29
+ export {};
@@ -0,0 +1,50 @@
1
+ import { visit } from "unist-util-visit";
2
+ import { generateUniqueId, slugifyHeading } from "./utils";
3
+ /**
4
+ * Remark plugin to generate an outline from MDX content.
5
+ * The outline is a list of headings with their depth, hash, and value.
6
+ * @param options - Options.
7
+ * @param options.depth - Depth of the headings.
8
+ * @param options.depth.max - Maximum depth of the headings.
9
+ * @param options.depth.min - Minimum depth of the headings.
10
+ * @param options.outline - Outline items.
11
+ * @returns plugin to generate an outline from MDX content.
12
+ */
13
+ export function remarkHeadings({ depth: { max = 3, min = 2 } = {}, outline, }) {
14
+ return (tree) => {
15
+ const setOfIds = new Set();
16
+ visit(tree, "heading", (node) => {
17
+ const heading = node;
18
+ const { children, depth } = heading;
19
+ if (depth < min || depth > max)
20
+ return;
21
+ const value = getHeadingTextValue(children);
22
+ const headingSlug = slugifyHeading(value);
23
+ const hash = generateUniqueId(setOfIds, headingSlug);
24
+ outline.push({
25
+ depth,
26
+ hash,
27
+ value,
28
+ });
29
+ });
30
+ };
31
+ }
32
+ /**
33
+ * Returns the value of the heading.
34
+ * @param children - Phrasing content.
35
+ * @param value - List of heading text values.
36
+ * @returns heading text value.
37
+ */
38
+ export function getHeadingTextValue(children, value = []) {
39
+ for (const child of children) {
40
+ if ("value" in child) {
41
+ value.push(child.value);
42
+ }
43
+ if ("children" in child) {
44
+ // Recurse into nested children, accumulating text into the shared `value` array.
45
+ // The return value is ignored here because accumulation happens via in-place mutation.
46
+ getHeadingTextValue(child.children, value);
47
+ }
48
+ }
49
+ return value.join("");
50
+ }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Returns node ID, ensuring uniqueness.
3
+ * @param setOfIds - Set of IDs.
4
+ * @param slug - Slug.
5
+ * @returns node ID.
6
+ */
7
+ export declare function generateUniqueId(setOfIds: Set<string>, slug: string): string;
8
+ /**
9
+ * Slugify the heading text to generate an ID.
10
+ * @param headingText - Heading text.
11
+ * @returns heading ID.
12
+ */
13
+ export declare function slugifyHeading(headingText: string): string;
@@ -0,0 +1,25 @@
1
+ import slugify from "slugify";
2
+ /**
3
+ * Returns node ID, ensuring uniqueness.
4
+ * @param setOfIds - Set of IDs.
5
+ * @param slug - Slug.
6
+ * @returns node ID.
7
+ */
8
+ export function generateUniqueId(setOfIds, slug) {
9
+ let id = slug;
10
+ let i = 1;
11
+ while (setOfIds.has(id)) {
12
+ id = `${slug}-${i}`;
13
+ i++;
14
+ }
15
+ setOfIds.add(id);
16
+ return id;
17
+ }
18
+ /**
19
+ * Slugify the heading text to generate an ID.
20
+ * @param headingText - Heading text.
21
+ * @returns heading ID.
22
+ */
23
+ export function slugifyHeading(headingText) {
24
+ return slugify(headingText, { lower: true, strict: true });
25
+ }
@@ -0,0 +1,8 @@
1
+ import { GetStaticPathsResult } from "next";
2
+ /**
3
+ * Builds Next.js static paths for MDX page files found under the given relative directories.
4
+ * Each path’s `slug` is composed of the file’s relative directory segments plus the MDX filename (without extension).
5
+ * @param relativeDirs - Relative directories to scan for MDX pages.
6
+ * @returns Array of path objects suitable for `getStaticPaths`.
7
+ */
8
+ export declare function buildStaticPaths(relativeDirs: string[]): GetStaticPathsResult["paths"];
@@ -0,0 +1,12 @@
1
+ import { mapMDXSlugByFilePaths } from "../files/mapMDXSlugByFilePaths";
2
+ import { resolveRelativeDirs } from "../files/resolveRelativeDirs";
3
+ /**
4
+ * Builds Next.js static paths for MDX page files found under the given relative directories.
5
+ * Each path’s `slug` is composed of the file’s relative directory segments plus the MDX filename (without extension).
6
+ * @param relativeDirs - Relative directories to scan for MDX pages.
7
+ * @returns Array of path objects suitable for `getStaticPaths`.
8
+ */
9
+ export function buildStaticPaths(relativeDirs) {
10
+ const slugByFilePaths = mapMDXSlugByFilePaths(resolveRelativeDirs(relativeDirs));
11
+ return [...slugByFilePaths].map(([, slug]) => ({ params: { slug } }));
12
+ }
@@ -0,0 +1,5 @@
1
+ import { SerializeOptions } from "next-mdx-remote/dist/types";
2
+ import { GetStaticPropsResult } from "next/types";
3
+ import { FrontmatterProps } from "../frontmatter/types";
4
+ import { StaticProps } from "./types";
5
+ export declare function buildStaticProps<F extends object, P extends object>(fileName: string | undefined, slug: string[] | undefined, frontmatterFn?: (frontmatter: FrontmatterProps<F> | undefined) => FrontmatterProps<F> | undefined, serializeOptions?: SerializeOptions, otherProps?: P): Promise<GetStaticPropsResult<StaticProps<FrontmatterProps<F>, P>> | undefined>;
@@ -0,0 +1,44 @@
1
+ import { serialize } from "next-mdx-remote/serialize";
2
+ import remarkGfm from "remark-gfm";
3
+ import { getMatter } from "../frontmatter/getMatter";
4
+ import { validateMatter } from "../frontmatter/validateMatter";
5
+ import { rehypeSlug } from "../plugins/rehypeSlug";
6
+ import { remarkHeadings } from "../plugins/remarkHeadings";
7
+ export async function buildStaticProps(fileName, slug, frontmatterFn = (frontmatter) => frontmatter, serializeOptions = {}, otherProps = {}) {
8
+ if (!slug)
9
+ return;
10
+ if (!fileName)
11
+ return;
12
+ // Extract frontmatter and content from the MDX file.
13
+ const { content, data } = getMatter(fileName);
14
+ const frontmatter = frontmatterFn(validateMatter(data));
15
+ // If the frontmatter is hidden, return.
16
+ if (!frontmatter || frontmatter.hidden)
17
+ return;
18
+ // We expect the frontmatter to have a title.
19
+ if (!frontmatter.title)
20
+ return;
21
+ // Serialize the MDX content.
22
+ const outline = [];
23
+ const mdxSource = await serialize(content, {
24
+ ...serializeOptions,
25
+ mdxOptions: {
26
+ development: false,
27
+ rehypePlugins: [rehypeSlug],
28
+ remarkPlugins: [[remarkHeadings, { outline }], remarkGfm],
29
+ ...serializeOptions.mdxOptions,
30
+ },
31
+ scope: { ...serializeOptions.scope, frontmatter },
32
+ });
33
+ const { title: pageTitle } = frontmatter;
34
+ return {
35
+ props: {
36
+ frontmatter,
37
+ mdxSource,
38
+ outline,
39
+ pageTitle,
40
+ slug,
41
+ ...otherProps,
42
+ },
43
+ };
44
+ }
@@ -0,0 +1,12 @@
1
+ import { MDXRemoteSerializeResult } from "next-mdx-remote";
2
+ import { OutlineItem } from "../../../components/Layout/components/Outline/types";
3
+ import { FrontmatterProps } from "../frontmatter/types";
4
+ interface BaseStaticProps<F extends object> {
5
+ frontmatter: FrontmatterProps<F> | null;
6
+ mdxSource: MDXRemoteSerializeResult | null;
7
+ outline: OutlineItem[] | null;
8
+ pageTitle: string;
9
+ slug: string[];
10
+ }
11
+ export type StaticProps<F extends object, P extends object> = BaseStaticProps<F> & P;
12
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,15 @@
1
+ import { GetStaticPropsContext } from "next/types";
2
+ /**
3
+ * Builds the MDX file path from the given list of doc directories and slug.
4
+ * @param dirs - Doc directories e.g. ["app", "docs"].
5
+ * @param slug - Doc slug e.g. ["learn", "featured-analyses"].
6
+ * @returns MDX file path.
7
+ */
8
+ export declare function buildMDXFilePath(dirs: string[], slug: string[] | undefined): string | undefined;
9
+ /**
10
+ * Returns the MDX page slug for the given static props context and section.
11
+ * @param props - Static props context.
12
+ * @param section - Docs section e.g. "learn".
13
+ * @returns MDX page slug.
14
+ */
15
+ export declare function buildMDXSlug(props: GetStaticPropsContext, section?: string): string[] | undefined;
@@ -0,0 +1,26 @@
1
+ import { resolveRelativeDirs } from "../files/resolveRelativeDirs";
2
+ /**
3
+ * Builds the MDX file path from the given list of doc directories and slug.
4
+ * @param dirs - Doc directories e.g. ["app", "docs"].
5
+ * @param slug - Doc slug e.g. ["learn", "featured-analyses"].
6
+ * @returns MDX file path.
7
+ */
8
+ export function buildMDXFilePath(dirs, slug) {
9
+ if (!slug)
10
+ return;
11
+ return resolveRelativeDirs(dirs.concat(slug)).concat(".mdx");
12
+ }
13
+ /**
14
+ * Returns the MDX page slug for the given static props context and section.
15
+ * @param props - Static props context.
16
+ * @param section - Docs section e.g. "learn".
17
+ * @returns MDX page slug.
18
+ */
19
+ export function buildMDXSlug(props, section) {
20
+ const slug = props.params?.slug;
21
+ if (!slug || typeof slug === "string")
22
+ return;
23
+ if (section)
24
+ return [section, ...slug];
25
+ return slug;
26
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@databiosphere/findable-ui",
3
- "version": "45.1.0",
3
+ "version": "46.1.0",
4
4
  "description": "",
5
5
  "scripts": {
6
6
  "test": "node --experimental-vm-modules node_modules/jest/bin/jest.js",
@@ -26,7 +26,7 @@
26
26
  "@commitlint/config-conventional": "^17.4.2",
27
27
  "@emotion/jest": "^11.13.0",
28
28
  "@mui/types": "^7.4.0",
29
- "@next/eslint-plugin-next": "^14.2.30",
29
+ "@next/eslint-plugin-next": "^14.2.32",
30
30
  "@storybook/addon-actions": "^8.6.4",
31
31
  "@storybook/addon-essentials": "^8.6.4",
32
32
  "@storybook/addon-interactions": "^8.6.4",
@@ -46,7 +46,7 @@
46
46
  "@types/uuid": "8.3.4",
47
47
  "@typescript-eslint/eslint-plugin": "^7.18.0",
48
48
  "eslint": "^8.33.0",
49
- "eslint-config-next": "^14.2.30",
49
+ "eslint-config-next": "^14.2.32",
50
50
  "eslint-config-prettier": "^8.6.0",
51
51
  "eslint-plugin-eslint-comments": "^3.2.0",
52
52
  "eslint-plugin-jsdoc": "^48.1.0",
@@ -76,10 +76,12 @@
76
76
  "@tanstack/react-table": "^8.19.2",
77
77
  "@tanstack/react-virtual": "^3.13.12",
78
78
  "copy-to-clipboard": "3.3.1",
79
+ "gray-matter": "^4.0.3",
79
80
  "isomorphic-dompurify": "^2.22.0",
80
81
  "ky": "^1.7.2",
81
- "next": "^14.2.30",
82
- "next-auth": "^4.24.7",
82
+ "next": "^14.2.32",
83
+ "next-auth": "4.24.11",
84
+ "next-mdx-remote": "^4.4.1",
83
85
  "react": "^18.3.1",
84
86
  "react-dom": "^18.3.1",
85
87
  "react-dropzone": "^14.2.3",
@@ -92,6 +94,7 @@
92
94
  "remark-gfm": "^3.0.1",
93
95
  "remark-parse": "^10.0.2",
94
96
  "remark-rehype": "^10.1.0",
97
+ "slugify": "^1.6.6",
95
98
  "unified": "^10.1.2",
96
99
  "uuid": "8.3.2",
97
100
  "yup": "^1.6.1"
@@ -1,6 +1,9 @@
1
1
  import { Column, RowData, Table } from "@tanstack/react-table";
2
2
  import { isRangeCategoryConfig } from "../../../../../../common/categories/config/range/typeGuards";
3
- import { CategoryConfig } from "../../../../../../common/categories/config/types";
3
+ import {
4
+ CategoryConfig,
5
+ SelectCategoryConfig,
6
+ } from "../../../../../../common/categories/config/types";
4
7
  import { RangeCategoryView } from "../../../../../../common/categories/views/range/types";
5
8
  import { CategoryView } from "../../../../../../common/categories/views/types";
6
9
  import {
@@ -10,6 +13,7 @@ import {
10
13
  import { FILTER_SORT } from "../../../../../../common/filters/sort/config/types";
11
14
  import { sortCategoryValueViews } from "../../../../../../common/filters/sort/models/utils";
12
15
  import { CategoryGroup } from "../../../../../../config/entities";
16
+ import { getSelectCategoryValue } from "../../../../../../hooks/useCategoryFilter";
13
17
  import { getColumnHeader } from "../../../../../Table/common/utils";
14
18
  import { CategoryFilter } from "../../../Filters/filters";
15
19
  import { SurfaceProps } from "../../../surfaces/types";
@@ -142,7 +146,7 @@ function mapCategoryFilter<T extends RowData>(
142
146
  categoryView = mapColumnToSelectCategoryView(
143
147
  column,
144
148
  filterSort,
145
- categoryConfig
149
+ categoryConfig as SelectCategoryConfig
146
150
  );
147
151
  }
148
152
 
@@ -198,11 +202,23 @@ function mapColumnToRangeCategoryView<T extends RowData>(
198
202
  function mapColumnToSelectCategoryView<T extends RowData>(
199
203
  column: Column<T>,
200
204
  filterSort: FILTER_SORT,
201
- categoryConfig?: CategoryConfig
205
+ categoryConfig?: SelectCategoryConfig
202
206
  ): SelectCategoryView {
203
207
  const facetedUniqueValues = column.getFacetedUniqueValues();
208
+
209
+ // Selected values for the column.
210
+ const filterValue = (column.getFilterValue() || []) as unknown[];
211
+
212
+ // Update faceted unique values with selected filters that are not in the faceted unique values.
213
+ for (const value of filterValue) {
214
+ if (facetedUniqueValues.has(value)) continue;
215
+ facetedUniqueValues.set(value, 0);
216
+ }
217
+
204
218
  const isDisabled = facetedUniqueValues.size === 0;
205
- const values = mapColumnToSelectCategoryValueView(column, filterSort);
219
+ const values = mapColumnToSelectCategoryValueView(column, filterSort).map(
220
+ categoryConfig?.mapSelectCategoryValue || getSelectCategoryValue
221
+ );
206
222
  return {
207
223
  annotation: undefined,
208
224
  enableChartView: false,
@@ -26,12 +26,14 @@ export const Summary = ({
26
26
  <Typography variant={TYPOGRAPHY_PROPS.VARIANT.BODY_SMALL_500}>
27
27
  {count}
28
28
  </Typography>
29
- <Typography
30
- color={TYPOGRAPHY_PROPS.COLOR.INK_LIGHT}
31
- variant={TYPOGRAPHY_PROPS.VARIANT.BODY_SMALL_400}
32
- >
33
- {label}
34
- </Typography>
29
+ {label && (
30
+ <Typography
31
+ color={TYPOGRAPHY_PROPS.COLOR.INK_LIGHT}
32
+ variant={TYPOGRAPHY_PROPS.VARIANT.BODY_SMALL_400}
33
+ >
34
+ {label}
35
+ </Typography>
36
+ )}
35
37
  </Fragment>
36
38
  ))}
37
39
  </StyledGrid>
@@ -10,7 +10,12 @@ export const Outline = ({
10
10
  outline,
11
11
  ...props /* MuiTabsProps */
12
12
  }: OutlineProps): JSX.Element | null => {
13
- const { indicatorColor, onChange, orientation, value } = useTabs(outline);
13
+ const { indicatorColor, onChange, orientation, value } = useTabs(
14
+ outline ?? []
15
+ );
16
+
17
+ if (!outline?.length) return null;
18
+
14
19
  return (
15
20
  <StyledTabs
16
21
  className={className}
@@ -12,5 +12,5 @@ export interface OutlineItem {
12
12
 
13
13
  export interface OutlineProps extends BaseComponentProps, TabsProps {
14
14
  Contents: ElementType<ContentsTabProps>;
15
- outline: OutlineItem[];
15
+ outline?: OutlineItem[] | null;
16
16
  }
@@ -0,0 +1,27 @@
1
+ import { Box } from "@mui/material";
2
+ import React, { useEffect, useRef } from "react";
3
+
4
+ export interface FileDownloadButtonProps {
5
+ fileName?: string;
6
+ fileUrl?: string;
7
+ }
8
+
9
+ export const FileDownloadButton = ({
10
+ fileName,
11
+ fileUrl,
12
+ }: FileDownloadButtonProps): JSX.Element => {
13
+ const downloadRef = useRef<HTMLAnchorElement>(null);
14
+
15
+ // Initiates file download when file url request is successful.
16
+ useEffect(() => {
17
+ if (downloadRef.current && fileName && fileUrl) {
18
+ downloadRef.current.setAttribute("href", fileUrl);
19
+ downloadRef.current.setAttribute("download", fileName);
20
+ downloadRef.current.click();
21
+ }
22
+ }, [fileName, fileUrl]);
23
+
24
+ return (
25
+ <Box component="a" download ref={downloadRef} sx={{ display: "none" }} />
26
+ );
27
+ };
@@ -0,0 +1,48 @@
1
+ import fs from "fs";
2
+ import * as path from "path";
3
+
4
+ /**
5
+ * Returns true if the file is an MDX file.
6
+ * @param fileName - File name.
7
+ * @returns true if the file is an MDX file.
8
+ */
9
+ function isMdxFile(fileName: string): boolean {
10
+ return fileName.endsWith(".mdx");
11
+ }
12
+
13
+ /**
14
+ * Maps each MDX file path to its slug.
15
+ * @param docsDirectory - Docs directory.
16
+ * @param dirPath - Directory path.
17
+ * @param slugByFilePaths - Accumulator: Map of slug by mdx file path.
18
+ * @returns returns slug by mdx file path.
19
+ */
20
+ export function mapMDXSlugByFilePaths(
21
+ docsDirectory: string,
22
+ dirPath = docsDirectory,
23
+ slugByFilePaths: Map<string, string[]> = new Map()
24
+ ): Map<string, string[]> {
25
+ const dirents = fs.readdirSync(dirPath, { withFileTypes: true });
26
+ return dirents.reduce((acc, dirent) => {
27
+ /* Accumulate the slug for each MDX file. */
28
+ if (dirent.isFile() && isMdxFile(dirent.name)) {
29
+ const mdxPath = path.resolve(dirPath, dirent.name);
30
+ /* Build the slug from the file relative directory and file name. */
31
+ const mdxRelativePath = path.relative(docsDirectory, mdxPath);
32
+ const { dir, name } = path.parse(mdxRelativePath);
33
+ let slug = [] as string[];
34
+ if (dir) slug = dir.split(path.sep);
35
+ slug.push(name);
36
+ acc.set(mdxPath, slug);
37
+ }
38
+ /* Recurse into subdirectories. */
39
+ if (dirent.isDirectory()) {
40
+ mapMDXSlugByFilePaths(
41
+ docsDirectory,
42
+ path.resolve(dirPath, dirent.name),
43
+ acc
44
+ );
45
+ }
46
+ return acc;
47
+ }, slugByFilePaths);
48
+ }
@@ -0,0 +1,10 @@
1
+ import * as path from "path";
2
+
3
+ /**
4
+ * Resolves an absolute docs path from a single set of directory segments.
5
+ * @param relativeDirs - Directory segments relative to the docs root.
6
+ * @returns Absolute path.
7
+ */
8
+ export function resolveRelativeDirs(relativeDirs: string[]): string {
9
+ return path.join(process.cwd(), ...relativeDirs);
10
+ }
@@ -0,0 +1,13 @@
1
+ import fs from "fs";
2
+ import matter from "gray-matter";
3
+
4
+ /**
5
+ * Returns matter object (frontmatter and content) from the given MD/MDX path.
6
+ * Server-only: uses Node fs, import only in SSG/SSR/build contexts.
7
+ * @param filePath - File path of MD/MDX file.
8
+ * @returns matter object.
9
+ */
10
+ export function getMatter(filePath: string): matter.GrayMatterFile<string> {
11
+ const markdownWithMeta = fs.readFileSync(filePath, "utf-8");
12
+ return matter(markdownWithMeta);
13
+ }
@@ -0,0 +1,7 @@
1
+ interface BaseFrontmatterProps {
2
+ description: string;
3
+ hidden?: boolean;
4
+ title: string;
5
+ }
6
+
7
+ export type FrontmatterProps<F extends object> = BaseFrontmatterProps & F;
@@ -0,0 +1,20 @@
1
+ import matter from "gray-matter";
2
+ import { FrontmatterProps } from "./types";
3
+
4
+ /**
5
+ * Returns the frontmatter from the given gray matter file data.
6
+ * @param data - Gray matter file data.
7
+ * @returns Frontmatter.
8
+ */
9
+ export function validateMatter<F extends object>(
10
+ data: matter.GrayMatterFile<string>["data"]
11
+ ): FrontmatterProps<F> | undefined {
12
+ if (
13
+ "title" in data &&
14
+ typeof data.title === "string" &&
15
+ "description" in data &&
16
+ typeof data.description === "string"
17
+ ) {
18
+ return data as FrontmatterProps<F>;
19
+ }
20
+ }
@@ -0,0 +1,36 @@
1
+ import { Plugin } from "unified";
2
+ import { visit } from "unist-util-visit";
3
+ import { getHeadingTextValue } from "./remarkHeadings";
4
+ import { generateUniqueId, slugifyHeading } from "./utils";
5
+
6
+ /**
7
+ * Rehype plugin to generate an ID for each heading in MDX content.
8
+ * @returns plugin to generate an ID for each heading in MDX content.
9
+ */
10
+ export function rehypeSlug(): Plugin {
11
+ return (tree) => {
12
+ const setOfIds = new Set<string>();
13
+ visit(tree, "element", (node) => {
14
+ if (/^h[1-6]$/.test(node.tagName)) {
15
+ const headingText = getHeadingTextValue(node.children);
16
+ const headingSlug = slugifyHeading(headingText);
17
+ const id = generateUniqueId(setOfIds, headingSlug);
18
+ // Add the ID to the heading element.
19
+ node.properties.id = id;
20
+ node.properties.style = "position: relative;";
21
+ // Append AnchorLink to the heading element.
22
+ node.children.push({
23
+ attributes: [
24
+ {
25
+ name: "anchorLink",
26
+ type: "mdxJsxAttribute",
27
+ value: id,
28
+ },
29
+ ],
30
+ name: "AnchorLink",
31
+ type: "mdxJsxFlowElement",
32
+ });
33
+ }
34
+ });
35
+ };
36
+ }
@@ -0,0 +1,67 @@
1
+ import { Heading, PhrasingContent, Root } from "mdast";
2
+ import { Plugin } from "unified";
3
+ import { visit } from "unist-util-visit";
4
+ import { OutlineItem } from "../../../components/Layout/components/Outline/types";
5
+ import { generateUniqueId, slugifyHeading } from "./utils";
6
+
7
+ interface Options {
8
+ depth?: { max?: number; min?: number };
9
+ outline: OutlineItem[];
10
+ }
11
+
12
+ /**
13
+ * Remark plugin to generate an outline from MDX content.
14
+ * The outline is a list of headings with their depth, hash, and value.
15
+ * @param options - Options.
16
+ * @param options.depth - Depth of the headings.
17
+ * @param options.depth.max - Maximum depth of the headings.
18
+ * @param options.depth.min - Minimum depth of the headings.
19
+ * @param options.outline - Outline items.
20
+ * @returns plugin to generate an outline from MDX content.
21
+ */
22
+ export function remarkHeadings({
23
+ depth: { max = 3, min = 2 } = {},
24
+ outline,
25
+ }: Options): Plugin {
26
+ return (tree: Root): void => {
27
+ const setOfIds = new Set<string>();
28
+ visit(tree, "heading", (node) => {
29
+ const heading = node as Heading;
30
+ const { children, depth } = heading;
31
+
32
+ if (depth < min || depth > max) return;
33
+
34
+ const value = getHeadingTextValue(children);
35
+ const headingSlug = slugifyHeading(value);
36
+ const hash = generateUniqueId(setOfIds, headingSlug);
37
+ outline.push({
38
+ depth,
39
+ hash,
40
+ value,
41
+ });
42
+ });
43
+ };
44
+ }
45
+
46
+ /**
47
+ * Returns the value of the heading.
48
+ * @param children - Phrasing content.
49
+ * @param value - List of heading text values.
50
+ * @returns heading text value.
51
+ */
52
+ export function getHeadingTextValue(
53
+ children: PhrasingContent[],
54
+ value: string[] = []
55
+ ): string {
56
+ for (const child of children) {
57
+ if ("value" in child) {
58
+ value.push(child.value);
59
+ }
60
+ if ("children" in child) {
61
+ // Recurse into nested children, accumulating text into the shared `value` array.
62
+ // The return value is ignored here because accumulation happens via in-place mutation.
63
+ getHeadingTextValue(child.children, value);
64
+ }
65
+ }
66
+ return value.join("");
67
+ }
@@ -0,0 +1,27 @@
1
+ import slugify from "slugify";
2
+
3
+ /**
4
+ * Returns node ID, ensuring uniqueness.
5
+ * @param setOfIds - Set of IDs.
6
+ * @param slug - Slug.
7
+ * @returns node ID.
8
+ */
9
+ export function generateUniqueId(setOfIds: Set<string>, slug: string): string {
10
+ let id = slug;
11
+ let i = 1;
12
+ while (setOfIds.has(id)) {
13
+ id = `${slug}-${i}`;
14
+ i++;
15
+ }
16
+ setOfIds.add(id);
17
+ return id;
18
+ }
19
+
20
+ /**
21
+ * Slugify the heading text to generate an ID.
22
+ * @param headingText - Heading text.
23
+ * @returns heading ID.
24
+ */
25
+ export function slugifyHeading(headingText: string): string {
26
+ return slugify(headingText, { lower: true, strict: true });
27
+ }
@@ -0,0 +1,18 @@
1
+ import { GetStaticPathsResult } from "next";
2
+ import { mapMDXSlugByFilePaths } from "../files/mapMDXSlugByFilePaths";
3
+ import { resolveRelativeDirs } from "../files/resolveRelativeDirs";
4
+
5
+ /**
6
+ * Builds Next.js static paths for MDX page files found under the given relative directories.
7
+ * Each path’s `slug` is composed of the file’s relative directory segments plus the MDX filename (without extension).
8
+ * @param relativeDirs - Relative directories to scan for MDX pages.
9
+ * @returns Array of path objects suitable for `getStaticPaths`.
10
+ */
11
+ export function buildStaticPaths(
12
+ relativeDirs: string[]
13
+ ): GetStaticPathsResult["paths"] {
14
+ const slugByFilePaths = mapMDXSlugByFilePaths(
15
+ resolveRelativeDirs(relativeDirs)
16
+ );
17
+ return [...slugByFilePaths].map(([, slug]) => ({ params: { slug } }));
18
+ }
@@ -0,0 +1,62 @@
1
+ import { SerializeOptions } from "next-mdx-remote/dist/types";
2
+ import { serialize } from "next-mdx-remote/serialize";
3
+ import { GetStaticPropsResult } from "next/types";
4
+ import remarkGfm from "remark-gfm";
5
+ import { OutlineItem } from "../../../components/Layout/components/Outline/types";
6
+ import { getMatter } from "../frontmatter/getMatter";
7
+ import { FrontmatterProps } from "../frontmatter/types";
8
+ import { validateMatter } from "../frontmatter/validateMatter";
9
+ import { rehypeSlug } from "../plugins/rehypeSlug";
10
+ import { remarkHeadings } from "../plugins/remarkHeadings";
11
+ import { StaticProps } from "./types";
12
+
13
+ export async function buildStaticProps<F extends object, P extends object>(
14
+ fileName: string | undefined,
15
+ slug: string[] | undefined,
16
+ frontmatterFn = (
17
+ frontmatter: FrontmatterProps<F> | undefined
18
+ ): FrontmatterProps<F> | undefined => frontmatter,
19
+ serializeOptions: SerializeOptions = {},
20
+ otherProps: P = {} as P
21
+ ): Promise<
22
+ GetStaticPropsResult<StaticProps<FrontmatterProps<F>, P>> | undefined
23
+ > {
24
+ if (!slug) return;
25
+ if (!fileName) return;
26
+
27
+ // Extract frontmatter and content from the MDX file.
28
+ const { content, data } = getMatter(fileName);
29
+ const frontmatter = frontmatterFn(validateMatter(data));
30
+
31
+ // If the frontmatter is hidden, return.
32
+ if (!frontmatter || frontmatter.hidden) return;
33
+
34
+ // We expect the frontmatter to have a title.
35
+ if (!frontmatter.title) return;
36
+
37
+ // Serialize the MDX content.
38
+ const outline: OutlineItem[] = [];
39
+ const mdxSource = await serialize(content, {
40
+ ...serializeOptions,
41
+ mdxOptions: {
42
+ development: false,
43
+ rehypePlugins: [rehypeSlug],
44
+ remarkPlugins: [[remarkHeadings, { outline }], remarkGfm],
45
+ ...serializeOptions.mdxOptions,
46
+ },
47
+ scope: { ...serializeOptions.scope, frontmatter },
48
+ });
49
+
50
+ const { title: pageTitle } = frontmatter;
51
+
52
+ return {
53
+ props: {
54
+ frontmatter,
55
+ mdxSource,
56
+ outline,
57
+ pageTitle,
58
+ slug,
59
+ ...otherProps,
60
+ },
61
+ };
62
+ }
@@ -0,0 +1,16 @@
1
+ import { MDXRemoteSerializeResult } from "next-mdx-remote";
2
+ import { OutlineItem } from "../../../components/Layout/components/Outline/types";
3
+ import { FrontmatterProps } from "../frontmatter/types";
4
+
5
+ interface BaseStaticProps<F extends object> {
6
+ frontmatter: FrontmatterProps<F> | null;
7
+ mdxSource: MDXRemoteSerializeResult | null;
8
+ outline: OutlineItem[] | null;
9
+ pageTitle: string;
10
+ slug: string[];
11
+ }
12
+
13
+ export type StaticProps<
14
+ F extends object,
15
+ P extends object
16
+ > = BaseStaticProps<F> & P;
@@ -0,0 +1,32 @@
1
+ import { GetStaticPropsContext } from "next/types";
2
+ import { resolveRelativeDirs } from "../files/resolveRelativeDirs";
3
+
4
+ /**
5
+ * Builds the MDX file path from the given list of doc directories and slug.
6
+ * @param dirs - Doc directories e.g. ["app", "docs"].
7
+ * @param slug - Doc slug e.g. ["learn", "featured-analyses"].
8
+ * @returns MDX file path.
9
+ */
10
+ export function buildMDXFilePath(
11
+ dirs: string[],
12
+ slug: string[] | undefined
13
+ ): string | undefined {
14
+ if (!slug) return;
15
+ return resolveRelativeDirs(dirs.concat(slug)).concat(".mdx");
16
+ }
17
+
18
+ /**
19
+ * Returns the MDX page slug for the given static props context and section.
20
+ * @param props - Static props context.
21
+ * @param section - Docs section e.g. "learn".
22
+ * @returns MDX page slug.
23
+ */
24
+ export function buildMDXSlug(
25
+ props: GetStaticPropsContext,
26
+ section?: string
27
+ ): string[] | undefined {
28
+ const slug = props.params?.slug;
29
+ if (!slug || typeof slug === "string") return;
30
+ if (section) return [section, ...slug];
31
+ return slug;
32
+ }
@@ -18,8 +18,10 @@ const CLASSNAMES = {
18
18
 
19
19
  const { Default, Selected } = composeStories(stories);
20
20
 
21
- const DATA = Default.args.selectCategoryValueViews || [];
22
- const SELECTED_DATA = Selected.args.selectCategoryValueViews || [];
21
+ const DATA: SelectCategoryValueView[] =
22
+ Default.args.selectCategoryValueViews || [];
23
+ const SELECTED_DATA: SelectCategoryValueView[] =
24
+ Selected.args.selectCategoryValueViews || [];
23
25
  const TOTAL_COUNT = getCategoryTotalCount(DATA);
24
26
 
25
27
  describe("Chart", () => {
@@ -10,7 +10,6 @@ import {
10
10
  } from "../src/components/Links/common/entities";
11
11
  import { STYLED_ANCHOR } from "../src/components/Table/components/TableCell/components/MarkdownCell/stories/constants";
12
12
  import * as stories from "../src/components/Table/components/TableCell/components/MarkdownCell/stories/markdownCell.stories";
13
- import { MarkdownCellProps } from "../src/components/Table/components/TableCell/components/MarkdownCell/types";
14
13
 
15
14
  expect.extend(matchers);
16
15
 
@@ -41,7 +40,7 @@ describe("MarkdownCell", () => {
41
40
  column={
42
41
  {
43
42
  columnDef: { meta: { components: { a: STYLED_ANCHOR } } },
44
- } as unknown as Column<unknown, MarkdownCellProps>
43
+ } as unknown as Column<unknown, string>
45
44
  }
46
45
  />
47
46
  );