diff --git a/CHANGELOG-hmp-235.md b/CHANGELOG-hmp-235.md
new file mode 100644
index 0000000000..74760ff298
--- /dev/null
+++ b/CHANGELOG-hmp-235.md
@@ -0,0 +1 @@
+- Add support for text-only publication vignettes without Vitessce visualizations.
diff --git a/context/app/static/js/components/Contexts.jsx b/context/app/static/js/components/Contexts.jsx
index a60aa2cd67..02c773bb70 100644
--- a/context/app/static/js/components/Contexts.jsx
+++ b/context/app/static/js/components/Contexts.jsx
@@ -1,7 +1,20 @@
-import React, { useContext } from 'react';
+import { useContext, createContext } from 'react';
-const FlaskDataContext = React.createContext({});
-const AppContext = React.createContext({});
+// TODO:
+// I tried converting this to a .tsx file, but it made storybook fail; created HMP-250 to track this
+// We should continue specifying shapes of contexts as we start using them in our TS files
+const FlaskDataContext = createContext({});
+
+/**
+ * @typedef AppContextType
+ * @property {string} assetsEndpoint
+ * @property {string} groupsToken
+ */
+
+/**
+ * @type {AppContextType}
+ */
+const AppContext = createContext({});
FlaskDataContext.displayName = 'FlaskDataContext';
diff --git a/context/app/static/js/components/publications/PublicationVignette/PublicationVignette.jsx b/context/app/static/js/components/publications/PublicationVignette/PublicationVignette.jsx
index 3b1bf62dfa..0f06d04389 100644
--- a/context/app/static/js/components/publications/PublicationVignette/PublicationVignette.jsx
+++ b/context/app/static/js/components/publications/PublicationVignette/PublicationVignette.jsx
@@ -1,88 +1,26 @@
-import React, { useEffect, useState } from 'react';
+import React from 'react';
import ReactMarkdown from 'react-markdown';
-import { useAppContext } from 'js/components/Contexts';
import VisualizationWrapper from 'js/components/detailPage/visualization/VisualizationWrapper';
import useStickyToggle from 'js/hooks/useStickyToggle';
-import { fillUrls } from './utils';
-
-async function fetchVitessceConf({ assetsEndpoint, uuid, filePath, groupsToken, vignetteDirName, signal }) {
- const urlHandler = (url, isZarr) => {
- return `${url.replace('{{ base_url }}', `${assetsEndpoint}/${uuid}/data`)}${isZarr ? '' : `?token=${groupsToken}`}`;
- };
-
- const requestInitHandler = () => {
- return {
- headers: { Authorization: `Bearer ${groupsToken}` },
- };
- };
- const response = await fetch(
- `${assetsEndpoint}/${uuid}/vignettes/${vignetteDirName}/${filePath}?token=${groupsToken}`,
- {
- signal,
- headers: {
- 'Content-Type': 'application/json',
- },
- },
- );
-
- if (!response.ok) {
- console.error('Assets API failed', response);
- return undefined;
- }
- const conf = await response.json();
- return fillUrls(conf, urlHandler, requestInitHandler);
-}
-
-function useVitessceConfs(assetsEndpoint, groupsToken, uuid, vignette, vignetteDirName) {
- const [vitessceConfs, setVitessceConfs] = useState(undefined);
-
- useEffect(() => {
- const abortController = new AbortController();
- async function getAndSetVitessceConf() {
- const figuresConfs = await Promise.all(
- vignette.figures.map((figure) => {
- return fetchVitessceConf({
- assetsEndpoint,
- uuid,
- filePath: figure.file,
- groupsToken,
- vignetteDirName,
- signal: abortController.signal,
- });
- }),
- );
- setVitessceConfs(figuresConfs);
- }
- // Only fetch the vitessce confs if they haven't been fetched yet
- if (!vitessceConfs) {
- getAndSetVitessceConf();
- return () => {
- abortController.abort();
- };
- }
- return () => {
- // Do nothing if the vitessce confs have already been fetched
- };
- }, [assetsEndpoint, groupsToken, uuid, vignette.figures, vignetteDirName, vitessceConfs]);
-
- return vitessceConfs;
-}
+import { usePublicationVignetteConfs } from './hooks';
function PublicationVignette({ vignette, vignetteDirName, uuid, mounted }) {
- const { assetsEndpoint, groupsToken } = useAppContext();
-
- const vitessceConfs = useVitessceConfs(assetsEndpoint, groupsToken, uuid, vignette, vignetteDirName);
+ const vitessceConfs = usePublicationVignetteConfs({
+ uuid,
+ vignette,
+ vignetteDirName,
+ });
// Workaround to make the visualization render only after the accordion section has been expanded while
// still letting the prerequisites for the visualizations prefetch
const hasBeenMounted = useStickyToggle(mounted);
- if (vitessceConfs) {
- return (
- <>
- {vignette.description}
+ return (
+ <>
+ {vignette.description}
+ {vitessceConfs && (
- >
- );
- }
-
- return null;
+ )}
+ >
+ );
}
export default PublicationVignette;
diff --git a/context/app/static/js/components/publications/PublicationVignette/hooks.ts b/context/app/static/js/components/publications/PublicationVignette/hooks.ts
new file mode 100644
index 0000000000..67ac367266
--- /dev/null
+++ b/context/app/static/js/components/publications/PublicationVignette/hooks.ts
@@ -0,0 +1,42 @@
+import useSWR from 'swr';
+import { useAppContext } from 'js/components/Contexts';
+import { multiFetcher } from 'js/helpers/multiFetcher';
+import { fillUrls } from './utils';
+import { PublicationVignette } from '../types';
+
+type PublicationVignetteConfsInput = {
+ uuid: string;
+ vignette: PublicationVignette;
+ vignetteDirName: string;
+};
+
+export function usePublicationVignetteConfs({ uuid, vignetteDirName, vignette }: PublicationVignetteConfsInput) {
+ const { assetsEndpoint, groupsToken } = useAppContext();
+ // Extract file paths from the vignette object to form the urls to fetch for this vignette
+ const urls = vignette.figures?.map(
+ ({ file }) => `${assetsEndpoint}/${uuid}/vignettes/${vignetteDirName}/${file}?token=${groupsToken}`,
+ );
+ const { data } = useSWR(urls, multiFetcher, {
+ revalidateOnFocus: false,
+ revalidateOnReconnect: false,
+ });
+
+ if (data) {
+ const urlHandler = (url: string, isZarr: boolean) => {
+ return `${url.replace('{{ base_url }}', `${assetsEndpoint}/${uuid}/data`)}${
+ isZarr ? '' : `?token=${groupsToken}`
+ }`;
+ };
+
+ const requestInitHandler = () => {
+ return {
+ headers: { Authorization: `Bearer ${groupsToken}` },
+ };
+ };
+ // Formats the vitessce config data to replace the {{ base_url }} placeholder with the actual url.
+ // TODO: Improve this `unknown`; I couldn't figure out how to import the appropriate `VitessceConfig` type from Vitessce.
+ const formattedData: unknown[] = data.map((d) => fillUrls(d, urlHandler, requestInitHandler));
+ return formattedData;
+ }
+ return undefined;
+}
diff --git a/context/app/static/js/components/publications/types.ts b/context/app/static/js/components/publications/types.ts
new file mode 100644
index 0000000000..266eed5b54
--- /dev/null
+++ b/context/app/static/js/components/publications/types.ts
@@ -0,0 +1,11 @@
+export type VignetteFigure = {
+ file: string;
+ name: string;
+};
+
+export type PublicationVignette = {
+ name: string;
+ description: string;
+ figures?: VignetteFigure[];
+ directory_name: string;
+};
diff --git a/context/app/static/js/helpers/multiFetcher.ts b/context/app/static/js/helpers/multiFetcher.ts
new file mode 100644
index 0000000000..e12f94db6a
--- /dev/null
+++ b/context/app/static/js/helpers/multiFetcher.ts
@@ -0,0 +1,5 @@
+// Fetcher function that lets SWR fetch multiple urls at once
+export const multiFetcher = (...urls: string[]) => {
+ const f = (url: string) => fetch(url).then((response) => response.json());
+ return Promise.all(urls.map((url) => f(url)));
+};
diff --git a/context/tsconfig.json b/context/tsconfig.json
index b1f5c87964..5b22ff1268 100644
--- a/context/tsconfig.json
+++ b/context/tsconfig.json
@@ -20,8 +20,8 @@
"sourceMap": true,
"noEmit": true,
"paths": {
- "@/*": [
- "./app/static/js/*"
+ "js/*": [
+ "./*"
]
}
},