Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 0 additions & 3 deletions gatsby-config.js
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,4 @@ module.exports = {
}, // must be after other CSS plugins
"gatsby-plugin-netlify", // make sure to keep it last in the array
],
mapping: {
"MarkdownRemark.frontmatter.dataset": `MarkdownRemark.frontmatter.name`,
},
};
15 changes: 8 additions & 7 deletions gatsby-node.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@ const { createFilePath } = require("gatsby-source-filesystem");
const {
stringWithDefault,
resolveToArray,
resolveSlug,
resolveSoftwareTools,
} = require("./gatsbyutils/gatsby-resolver-utils");
const { DATASET_PATH } = require("./gatsbyutils/constants");

/**
* Markdown in /src/pages/ with these templateKeys are data-only
Expand Down Expand Up @@ -47,7 +50,7 @@ exports.createSchemaCustomization = ({ actions, schema }) => {
Nested materials and methods block for idea posts.
"""
type MaterialsAndMethods {
dataset: MarkdownRemark @link(by: "frontmatter.name")
dataset: MarkdownRemark @link(by: "fields.slug")
protocols: [ProtocolItem!]!
cellLines: [CellLineItem!]!
software: [SoftwareTool!]!
Expand All @@ -66,7 +69,7 @@ exports.createSchemaCustomization = ({ actions, schema }) => {
Software tool reference with optional custom description.
"""
type SoftwareTool {
softwareTool: MarkdownRemark @link(by: "frontmatter.name")
softwareTool: MarkdownRemark @link(by: "fields.slug")
customDescription: String
}`,
];
Expand Down Expand Up @@ -113,13 +116,11 @@ exports.createResolvers = ({ createResolvers }) => {
return current;
}

current.dataset = stringWithDefault(
raw.dataset,
current.dataset
);
const resolvedDatasetSlug = resolveSlug(raw.dataset, DATASET_PATH);
current.dataset = resolvedDatasetSlug;
current.cellLines = resolveToArray(raw.cellLines);
current.protocols = resolveToArray(raw.protocols);
current.software = resolveToArray(raw.software);
current.software = resolveSoftwareTools(raw.software);

return current;
},
Expand Down
7 changes: 7 additions & 0 deletions gatsbyutils/constants.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
const DATASET_PATH = `dataset`;
const SOFTWARE_PATH = `software`;

module.exports = {
DATASET_PATH,
SOFTWARE_PATH,
};
64 changes: 63 additions & 1 deletion gatsbyutils/gatsby-resolver-utils.js
Original file line number Diff line number Diff line change
@@ -1,17 +1,79 @@
const { SOFTWARE_PATH } = require("../gatsbyutils/constants");
const slugify = require("slugify");

/**
* Returns the raw value if it's a non-empty string, otherwise returns the fallback.
* @param {unknown} rawValue - The value to check
* @param {T} fallback - The fallback value to return if rawValue is not a valid string
* @returns {string|T} The raw string value or the fallback
* @template T
*/
function stringWithDefault(rawValue, fallback) {
if (typeof rawValue !== "string") return fallback;
if (rawValue.trim() === "") return fallback;
return rawValue;
}

/**
* Ensures the value is returned as an array.
* @param {unknown} value - The value to convert to an array
* @returns {Array} The original array if value is an array, otherwise an empty array
*/
function resolveToArray(value) {
if (Array.isArray(value)) {
return value;
}
return [];
}

/**
* Prepares software tool references for Gatsby's @link directive.
* Transforms tool names into slug paths that @link(by: "fields.slug") uses
* to resolve the actual MarkdownRemark nodes. Filters out invalid entries
* and preserves custom descriptions.
* @param {Array<{softwareTool?: string, customDescription?: string}>|unknown} rawSoftware - Array of software tool objects from frontmatter
* @returns {Array<{softwareTool: string|null, customDescription: string|null}>} Array with slug keys for @link resolution
*/
const resolveSoftwareTools = (rawSoftware) => {
if (!Array.isArray(rawSoftware)) {
return [];
}
return rawSoftware
.map((item) => {
if (item && typeof item === "object" && item.softwareTool) {
return {
softwareTool: resolveSlug(item.softwareTool, SOFTWARE_PATH),
customDescription: stringWithDefault(
item.customDescription,
null
),
};
}
return null;
})
.filter((item) => item !== null);
};

/**
* Builds slugs from directory paths and ids/names.
* Uses slugs in place of names to prevent namespace
* collisions when using @link directive in Gatsby schema.
* @param {string|null|undefined} id - The identifier to slugify (e.g., "My Dataset")
* @param {string} directory - The directory prefix (e.g., "dataset", "software")
* @returns {string|null} The full slug path (e.g., "/dataset/my-dataset/") or null if id is falsy
*/
const resolveSlug = (id, directory) => {
if (!id) return null;
const slugPart = slugify(id, { lower: true, strict: true }).replace(
/^\/+|\/+$/g,
""
); // Slugify and remove leading/trailing slashes
return `/${directory}/${slugPart}/`;
};

module.exports = {
stringWithDefault,
resolveToArray,
};
resolveSlug,
resolveSoftwareTools,
};
109 changes: 109 additions & 0 deletions gatsbyutils/test/gatsby-resolver-utils.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import { describe, it, expect } from 'vitest';
import { resolveSlug, resolveSoftwareTools } from '../gatsby-resolver-utils';
import { DATASET_PATH, SOFTWARE_PATH } from '../constants';

describe('resolveSlug', () => {
it('should return null when id is falsy', () => {
expect(resolveSlug(null, 'software')).toBe(null);
expect(resolveSlug(undefined, 'software')).toBe(null);
expect(resolveSlug('', 'software')).toBe(null);
});

it('should build a slug from id and directory', () => {
expect(resolveSlug("released-emt-dataset", DATASET_PATH)).toBe(
"/dataset/released-emt-dataset/"
);
});

it('should slugify the id to lowercase', () => {
expect(resolveSlug("UPPERCASE", DATASET_PATH)).toBe(
"/dataset/uppercase/"
);
});

it('should handle special characters in id', () => {
expect(resolveSlug("Tool & Library", SOFTWARE_PATH)).toBe(
"/software/tool-and-library/"
);
expect(resolveSlug("Some/Path/Name", SOFTWARE_PATH)).toBe(
"/software/somepathname/"
);
});

it('should handle ids with leading/trailing spaces', () => {
expect(resolveSlug(' trimmed ', 'software')).toBe('/software/trimmed/');
});
});

describe('resolveSoftwareTools', () => {
it('should return empty array for non-array inputs', () => {
expect(resolveSoftwareTools(null)).toEqual([]);
expect(resolveSoftwareTools(undefined)).toEqual([]);
expect(resolveSoftwareTools('string')).toEqual([]);
expect(resolveSoftwareTools({})).toEqual([]);
expect(resolveSoftwareTools(123)).toEqual([]);
});

it('should return empty array for empty array', () => {
expect(resolveSoftwareTools([])).toEqual([]);
});

it('should filter out invalid items', () => {
const input = [null, undefined, 'string', {}, { other: 'prop' }];
expect(resolveSoftwareTools(input)).toEqual([]);
});

it('should transform valid items with softwareTool', () => {
const input = [{ softwareTool: 'Simularium' }];
expect(resolveSoftwareTools(input)).toEqual([
{
softwareTool: '/software/simularium/',
customDescription: null,
},
]);
});

it('should preserve customDescription when provided', () => {
const input = [
{
softwareTool: 'Simularium',
customDescription: 'Custom description here',
},
];
expect(resolveSoftwareTools(input)).toEqual([
{
softwareTool: '/software/simularium/',
customDescription: 'Custom description here',
},
]);
});

it('should return null for empty customDescription', () => {
const input = [{ softwareTool: 'Simularium', customDescription: '' }];
expect(resolveSoftwareTools(input)).toEqual([
{
softwareTool: '/software/simularium/',
customDescription: null,
},
]);
});

it('should handle mixed valid and invalid items', () => {
const input = [
null,
{ softwareTool: 'Simularium' },
{ other: 'invalid' },
{ softwareTool: 'TFE', customDescription: 'Time explorer' },
];
expect(resolveSoftwareTools(input)).toEqual([
{
softwareTool: '/software/simularium/',
customDescription: null,
},
{
softwareTool: '/software/tfe/',
customDescription: 'Time explorer',
},
]);
});
});
2 changes: 1 addition & 1 deletion netlify.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,6 @@
publish = "public"
command = "npm run build"
[build.environment]
NODE_VERSION = "18.17.1"
NODE_VERSION = "22.16.0"
YARN_VERSION = "1.22.4"
YARN_FLAGS = "--no-ignore-optional"
6 changes: 4 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,9 @@
"react-dom": "^18.3.1",
"react-helmet-async": "^2.0.5",
"sass": "^1.43.2",
"uuid": "^8.0.0"
"slugify": "^1.6.6",
"uuid": "^8.0.0",
"vitest": "^4.0.18"
},
"keywords": [
"gatsby"
Expand All @@ -47,7 +49,7 @@
"develop": "npm run clean && gatsby develop",
"serve": "gatsby serve",
"format": "prettier --trailing-comma es5 --no-semi --single-quote --write \"{gatsby-*.js,src/**/*.js}\"",
"test": "echo \"Error: no test specified\" && exit 1",
"test": "vitest",
"dev": "npx concurrently \"npx netlify-cms-proxy-server\" \"npm start -- --progress\""
},
"devDependencies": {
Expand Down
3 changes: 2 additions & 1 deletion src/pages/dataset/released-emt-dataset.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
---
templateKey: dataset
name: Released EMT dataset
link: test
link: "https://www.biorxiv.org/content/10.1101/2024.08.16.608353v1.full"
description: Description for released EMT dataset in the markdown file for the dataset.
status: Public
date: 2025-06-02T16:11:00.000Z
---
2 changes: 1 addition & 1 deletion src/pages/ideas/dev-example.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ materialsAndMethods:
link: https://allencell.org/cell-catalog
software:
- softwareTool: Simularium
customDescription: CUSTOM DESCRIPTION Used to visualize 3D trajectories and other cool stuff.
customDescription: Custom description in the markdown file for the idea, not in the software file markdown.
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we talked about this. I'm skeptical of including this but it's fine for now

- softwareTool: Timelapse Feature Explorer
nextSteps:
- "Expand analysis to all EMT datasets and stratify by treatment."
Expand Down
File renamed without changes.
Loading