Skip to content

Commit 5da1799

Browse files
Merge pull request #209 from samply/feature/get-site-names-from-directory
feat: pull biobank names from directory
2 parents a4d33a3 + e889258 commit 5da1799

5 files changed

Lines changed: 357 additions & 5 deletions

File tree

src/App.svelte

Lines changed: 85 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,14 @@
1111
querySpot,
1212
removeFailedSite,
1313
} from "@samply/lens";
14+
import { base } from "$app/paths";
1415
import { env } from "$env/dynamic/public";
1516
import { onMount } from "svelte";
1617
import { v4 as uuidv4 } from "uuid";
18+
import {
19+
cloneLensOptions,
20+
loadOptionsWithDirectoryBiobankNames,
21+
} from "$lib/directoryBiobankNames";
1722
import optionsProd from "./config/options.json";
1823
import optionsTest from "./config/options-test.json";
1924
import optionsAcceptance from "./config/options-acceptance.json";
@@ -77,16 +82,79 @@
7782
scrollToResults();
7883
});
7984
80-
onMount(async () => {
85+
const setupResultTableNameTooltips = () => {
86+
let animationFrameId: number | undefined;
87+
let tableObserver: MutationObserver | undefined;
88+
89+
const updateTooltips = (resultTable: HTMLElement) => {
90+
const rows = resultTable.shadowRoot?.querySelectorAll<HTMLElement>(
91+
'[part~="lens-result-table-item-body-row"]',
92+
);
93+
94+
rows?.forEach((row) => {
95+
const cells = row.querySelectorAll<HTMLElement>(
96+
'[part~="lens-result-table-item-body-cell"]',
97+
);
98+
const siteCell = cells[1];
99+
const siteName = siteCell?.textContent?.replace(/\s+/g, " ").trim();
100+
101+
if (!siteCell || !siteName) {
102+
return;
103+
}
104+
105+
siteCell.title = siteName;
106+
siteCell
107+
.querySelector<HTMLElement>(
108+
'[part~="lens-result-table-item-body-cell-link"]',
109+
)
110+
?.setAttribute("title", siteName);
111+
});
112+
};
113+
114+
const observeResultTable = () => {
115+
const resultTable =
116+
document.querySelector<HTMLElement>("lens-result-table");
117+
118+
if (!resultTable?.shadowRoot) {
119+
animationFrameId = window.requestAnimationFrame(observeResultTable);
120+
return;
121+
}
122+
123+
updateTooltips(resultTable);
124+
tableObserver = new MutationObserver(() => updateTooltips(resultTable));
125+
tableObserver.observe(resultTable.shadowRoot, {
126+
childList: true,
127+
subtree: true,
128+
characterData: true,
129+
});
130+
};
131+
132+
observeResultTable();
133+
134+
return () => {
135+
tableObserver?.disconnect();
136+
if (animationFrameId) {
137+
window.cancelAnimationFrame(animationFrameId);
138+
}
139+
};
140+
};
141+
142+
const initializeLens = async () => {
81143
// Set the options based on the environment
82-
let options: LensOptions = optionsProd;
144+
let optionsSource: LensOptions = optionsProd;
83145
if (env.PUBLIC_ENVIRONMENT === "test") {
84-
options = optionsTest;
146+
optionsSource = optionsTest;
85147
} else if (env.PUBLIC_ENVIRONMENT === "acceptance") {
86-
options = optionsAcceptance;
148+
optionsSource = optionsAcceptance;
87149
}
150+
151+
let options = cloneLensOptions(optionsSource);
152+
88153
if (env.PUBLIC_SPOT_URL) {
89-
options.spotUrl = env.PUBLIC_SPOT_URL;
154+
options = {
155+
...options,
156+
spotUrl: env.PUBLIC_SPOT_URL,
157+
};
90158
}
91159
92160
// Check if prism URL parameter is set
@@ -105,6 +173,11 @@
105173
};
106174
}
107175
176+
options = await loadOptionsWithDirectoryBiobankNames(
177+
options,
178+
`${base}/api/directory-biobank-names`,
179+
);
180+
108181
setOptions(options);
109182
110183
// Set the catalogue
@@ -113,6 +186,13 @@
113186
// Wait for the search bar to initialize (load query from URL) before sending the initial query.
114187
// Using setTimeout to ensure the custom element's onMount has completed.
115188
setTimeout(() => sendQuery(), 0);
189+
};
190+
191+
onMount(() => {
192+
const cleanupResultTableNameTooltips = setupResultTableNameTooltips();
193+
void initializeLens();
194+
195+
return cleanupResultTableNameTooltips;
116196
});
117197
118198
let results: HTMLElement;

src/app.css

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -263,6 +263,31 @@ lens-result-summary {
263263
display: block;
264264
}
265265

266+
lens-result-table::part(lens-result-table) {
267+
table-layout: fixed;
268+
}
269+
270+
lens-result-table::part(lens-result-table-item-body-cell) {
271+
max-width: 0;
272+
overflow: hidden;
273+
text-overflow: ellipsis;
274+
white-space: nowrap;
275+
}
276+
277+
lens-result-table::part(lens-result-table-item-body-cell-checkbox) {
278+
max-width: none;
279+
overflow: visible;
280+
}
281+
282+
lens-result-table::part(lens-result-table-item-body-cell-link) {
283+
display: inline-block;
284+
max-width: 100%;
285+
overflow: hidden;
286+
text-overflow: ellipsis;
287+
vertical-align: bottom;
288+
white-space: nowrap;
289+
}
290+
266291
lens-result-summary::part(result-summary-content) {
267292
gap: var(--gap-l);
268293
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import { describe, expect, it } from "vitest";
2+
import type { LensOptions } from "@samply/lens";
3+
import {
4+
getConfiguredCollectionIds,
5+
mergeDirectoryBiobankNames,
6+
} from "./directoryBiobankNames";
7+
8+
describe("directory biobank names", () => {
9+
const options: LensOptions = {
10+
siteMappings: {
11+
aachen: {
12+
displayName: "Aachen",
13+
collectionId: "bbmri-eric:ID:DE_RWTHCBMB:collection:RWTHCBMB_BC",
14+
},
15+
test: "Configured fallback",
16+
},
17+
};
18+
19+
it("gets configured collection ids from structured site mappings", () => {
20+
expect(getConfiguredCollectionIds(options)).toEqual([
21+
"bbmri-eric:ID:DE_RWTHCBMB:collection:RWTHCBMB_BC",
22+
]);
23+
});
24+
25+
it("replaces configured display names with Directory biobank names", () => {
26+
expect(
27+
mergeDirectoryBiobankNames(options, [
28+
{
29+
collectionId: "bbmri-eric:ID:DE_RWTHCBMB:collection:RWTHCBMB_BC",
30+
biobankName:
31+
"Zentralisierte Biomaterialbank der RWTH Aachen University",
32+
},
33+
]).siteMappings,
34+
).toEqual({
35+
aachen: {
36+
displayName:
37+
"Zentralisierte Biomaterialbank der RWTH Aachen University",
38+
collectionId: "bbmri-eric:ID:DE_RWTHCBMB:collection:RWTHCBMB_BC",
39+
},
40+
test: {
41+
displayName: "Configured fallback",
42+
},
43+
});
44+
});
45+
});

src/lib/directoryBiobankNames.ts

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
import type { LensOptions, SiteInfo } from "@samply/lens";
2+
3+
type DirectoryBiobankName = {
4+
collectionId: string;
5+
biobankName: string;
6+
};
7+
8+
type DirectoryBiobankNamesResponse = {
9+
names?: DirectoryBiobankName[];
10+
};
11+
12+
const isSiteInfo = (siteInfo: string | SiteInfo): siteInfo is SiteInfo =>
13+
typeof siteInfo === "object" && siteInfo !== null;
14+
15+
export const cloneLensOptions = (options: LensOptions): LensOptions => ({
16+
...options,
17+
siteMappings: options.siteMappings
18+
? Object.fromEntries(
19+
Object.entries(options.siteMappings).map(([site, siteInfo]) => [
20+
site,
21+
isSiteInfo(siteInfo) ? { ...siteInfo } : siteInfo,
22+
]),
23+
)
24+
: undefined,
25+
});
26+
27+
export const getConfiguredCollectionIds = (options: LensOptions): string[] => {
28+
const collectionIds = Object.values(options.siteMappings ?? {})
29+
.map((siteInfo) =>
30+
isSiteInfo(siteInfo) ? siteInfo.collectionId : undefined,
31+
)
32+
.filter((collectionId): collectionId is string => Boolean(collectionId));
33+
34+
return [...new Set(collectionIds)];
35+
};
36+
37+
export const mergeDirectoryBiobankNames = (
38+
options: LensOptions,
39+
directoryNames: DirectoryBiobankName[],
40+
): LensOptions => {
41+
const biobankNameByCollectionId = new Map(
42+
directoryNames.map(({ collectionId, biobankName }) => [
43+
collectionId,
44+
biobankName,
45+
]),
46+
);
47+
const siteMappings = Object.fromEntries(
48+
Object.entries(options.siteMappings ?? {}).map(([site, siteInfo]) => {
49+
const siteInfoObject = isSiteInfo(siteInfo)
50+
? siteInfo
51+
: { displayName: siteInfo };
52+
const directoryName = siteInfoObject.collectionId
53+
? biobankNameByCollectionId.get(siteInfoObject.collectionId)
54+
: undefined;
55+
56+
return [
57+
site,
58+
{
59+
...siteInfoObject,
60+
displayName: directoryName ?? siteInfoObject.displayName,
61+
},
62+
];
63+
}),
64+
);
65+
66+
return {
67+
...options,
68+
siteMappings,
69+
};
70+
};
71+
72+
export const loadOptionsWithDirectoryBiobankNames = async (
73+
options: LensOptions,
74+
apiUrl: string,
75+
fetcher: typeof fetch = fetch,
76+
): Promise<LensOptions> => {
77+
const collectionIds = getConfiguredCollectionIds(options);
78+
if (!collectionIds.length) {
79+
return options;
80+
}
81+
82+
try {
83+
const response = await fetcher(apiUrl, {
84+
method: "POST",
85+
headers: {
86+
"Content-Type": "application/json",
87+
},
88+
body: JSON.stringify({ collectionIds }),
89+
});
90+
91+
if (!response.ok) {
92+
throw new Error(`Directory lookup failed with status ${response.status}`);
93+
}
94+
95+
const payload = (await response.json()) as DirectoryBiobankNamesResponse;
96+
return mergeDirectoryBiobankNames(options, payload.names ?? []);
97+
} catch (error) {
98+
console.warn(
99+
"Failed to load biobank names from the BBMRI Directory; using configured display names.",
100+
error,
101+
);
102+
return options;
103+
}
104+
};

0 commit comments

Comments
 (0)