Skip to content

Commit fff5d60

Browse files
authored
Display links to things in local systems (#1291)
1 parent 0bac6db commit fff5d60

File tree

13 files changed

+512
-44
lines changed

13 files changed

+512
-44
lines changed

lxl-web/src/app.css

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -262,6 +262,15 @@
262262
}
263263
}
264264

265+
.btn-outlined {
266+
color: var(--color-link);
267+
font-size: var(--text-xs);
268+
padding: calc(var(--spacing) * 1.5) calc(var(--spacing) * 6);
269+
height: calc(var(--spacing) * 10);
270+
border-radius: calc(infinity * 1px);
271+
border: 1px solid var(--color-link);
272+
}
273+
265274
.badge {
266275
display: flex;
267276
align-items: center;

lxl-web/src/lib/assets/json/display-web.json

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -251,6 +251,18 @@
251251
}
252252
}
253253
},
254+
"tokens": {
255+
"@id": "tokens",
256+
"@type": "fresnel:Group",
257+
"lenses": {
258+
"Publication": {
259+
"@id": "Publication-tokens",
260+
"@type": "fresnel:Lens",
261+
"classLensDomain": "Publication",
262+
"showProperties": ["agent", "year"]
263+
}
264+
}
265+
},
254266
"web-chips": {
255267
"@id": "web-chips",
256268
"@type": "fresnel:Group",
@@ -840,6 +852,26 @@
840852
"fresnel:contentBefore": ""
841853
}
842854
},
855+
"bibdb:PostalAddress-format": {
856+
"@id": "bibdb:PostalAddress-format",
857+
"@type": "fresnel:Format",
858+
"fresnel:classFormatDomain": ["bibdb:PostalAddress"],
859+
"fresnel:propertyFormatDomain": ["bibdb:streetAddress", "bibdb:postalCode", "bibdb:email"],
860+
"fresnel:propertyFormat": {
861+
"fresnel:contentBefore": "\n",
862+
"fresnel:contentFirst": ""
863+
}
864+
},
865+
"bibdb:PostalAddress-format2": {
866+
"@id": "bibdb:PostalAddress-format2",
867+
"@type": "fresnel:Format",
868+
"fresnel:classFormatDomain": ["bibdb:PostalAddress"],
869+
"fresnel:propertyFormatDomain": ["bibdb:addressLocality"],
870+
"fresnel:propertyFormat": {
871+
"fresnel:contentBefore": " ",
872+
"fresnel:contentFirst": ""
873+
}
874+
},
843875
"default-separators": {
844876
"@id": "default-separators",
845877
"@type": "fresnel:Format",

lxl-web/src/lib/i18n/locales/en.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -198,7 +198,12 @@ export default {
198198
loanStatusFailed: 'Failed to get loan status',
199199
available: 'Available',
200200
unavailable: 'Not available',
201-
map: 'map'
201+
map: 'map',
202+
linkToLocal: 'Show in local library catalog',
203+
loanReserveLink: 'Loan/reserve',
204+
linkToCatalog: 'Local library catalog',
205+
linkToSite: 'Library website',
206+
openingHoursEtc: 'Opening hours, address etc'
202207
},
203208
filterAlias: {
204209
'alias-myLibraries': 'My Libraries'

lxl-web/src/lib/i18n/locales/sv.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -197,7 +197,12 @@ export default {
197197
loanStatusFailed: 'Lånestatus kunde inte hämtas',
198198
available: 'Tillgänglig',
199199
unavailable: 'Ej tillgänglig',
200-
map: 'karta'
200+
map: 'karta',
201+
linkToLocal: 'Visa i bibliotekets katalog',
202+
loanReserveLink: 'Låna/reservera',
203+
linkToCatalog: 'Bibliotekets lokala katalog',
204+
linkToSite: 'Bibliotekets webbplats',
205+
openingHoursEtc: 'Öppettider, adress m.m.'
201206
},
202207
filterAlias: {
203208
'alias-myLibraries': 'Mina bibliotek'

lxl-web/src/lib/types/holdings.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { DisplayDecorated } from './xl';
1+
import type { DisplayDecorated, FramedData } from './xl';
22

33
export type BibIdObj = {
44
bibId: string;
@@ -7,6 +7,7 @@ export type BibIdObj = {
77
onr: string | null;
88
isbn: string[];
99
issn: string[];
10+
str: string;
1011
};
1112

1213
export type HoldingsByInstanceId = {
@@ -29,3 +30,15 @@ export type DecoratedHolder = {
2930
sigel: string;
3031
str: string;
3132
};
33+
34+
export type FullHolderBySigel = {
35+
[sigel: string]: FramedData;
36+
};
37+
38+
export type ItemLinksForHolder = {
39+
[sigel: string]: { [linkType: string]: string[] };
40+
};
41+
42+
export type ItemLinksByBibId = {
43+
[id: string]: ItemLinksForHolder;
44+
};

lxl-web/src/lib/types/xl.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,28 @@ export enum Platform {
4646
meta = 'meta'
4747
}
4848

49+
export enum BibDb {
50+
ils = 'bibdb:ils',
51+
lopac = 'bibdb:lopac',
52+
bibIdSearchUriByLang = 'bibdb:bibIdSearchUriByLang',
53+
bibIdSearchUri = 'bibdb:bibIdSearchUri',
54+
isbnSearchUri = 'bibdb:isbnSearchUri',
55+
issnSearchUri = 'bibdb:issnSearchUri',
56+
eodUri = 'bibdb:eodUri',
57+
itemStatusUri = 'bibdb:itemStatusUri',
58+
openingHours = 'bibdb:openingHours',
59+
address = 'bibdb:address',
60+
postalAddress = 'bibdb:PostalAddress',
61+
visitingAddress = 'bibdb:VisitingAddress',
62+
LinksToCatalog = 'linksToCatalog',
63+
LinksToSite = 'linksToSite',
64+
LinksToItem = 'linksToItem',
65+
Address = 'address',
66+
ItemStatus = 'itemStatus',
67+
OpeningHours = 'openingHours',
68+
LoanReserveLink = 'loanReserveLink'
69+
}
70+
4971
export type ClassName = string;
5072
export type PropertyName = string;
5173
export type LangCode = string;
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import type { FullHolderBySigel } from '$lib/types/holdings';
2+
3+
type Cache = {
4+
holders: FullHolderBySigel;
5+
};
6+
export const holdersCache: Cache = $state({ holders: {} });

lxl-web/src/lib/utils/holdings.test.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,18 @@ import { UserSettings } from './userSettings.svelte';
66

77
describe('getBibIdsByInstanceId', () => {
88
it('Returns a correctly mapped object (bibId, type & holders)', () => {
9-
expect(getBibIdsByInstanceId(mainEntity, record)).toStrictEqual({
9+
const instanceTokenStr = 'Natur och kultur, 2018';
10+
const DisplayUtil = { lensAndFormat: () => instanceTokenStr };
11+
12+
expect(getBibIdsByInstanceId(mainEntity, DisplayUtil, record, 'sv')).toStrictEqual({
1013
'0h96fs3b0c49qkt': {
1114
bibId: '7654300',
1215
'@type': 'Instance',
1316
holders: ['S', 'H', 'U', 'Um', 'Umdp', 'La', 'Q', 'L', 'Sbi', 'NB'],
1417
onr: '9176423484',
1518
isbn: ['9176423484'],
16-
issn: []
19+
issn: [],
20+
str: instanceTokenStr
1721
}
1822
});
1923
});

lxl-web/src/lib/utils/holdings.ts

Lines changed: 172 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,19 @@
11
import { pushState } from '$app/navigation';
22
import isFnurgel from '$lib/utils/isFnurgel';
3-
import type { BibIdObj, HoldersByType, HoldingsByInstanceId } from '$lib/types/holdings';
4-
import { LensType, type FramedData } from '$lib/types/xl';
3+
import type {
4+
BibIdObj,
5+
HoldersByType,
6+
HoldingsByInstanceId,
7+
ItemLinksByBibId,
8+
ItemLinksForHolder
9+
} from '$lib/types/holdings';
10+
import { LensType, type FramedData, JsonLd, BibDb } from '$lib/types/xl';
511
import type { LocaleCode } from '$lib/i18n/locales';
612
import type { LibraryItem, UserSettings } from '$lib/types/userSettings';
713
import { relativizeUrl } from '$lib/utils/http';
814
import { DisplayUtil, toString } from '$lib/utils/xl.js';
15+
import getAtPath from '$lib/utils/getAtPath';
16+
import { holdersCache } from '$lib/utils/holdersCache.svelte';
917

1018
export function getHoldingsLink(url: URL, value: string) {
1119
const newSearchParams = new URLSearchParams([...Array.from(url.searchParams.entries())]);
@@ -72,13 +80,24 @@ export function getHoldingsByInstanceId(
7280
}, {});
7381
}
7482

75-
export function getBibIdsByInstanceId(mainEntity, record): Record<string, BibIdObj> {
76-
return mainEntity['@reverse']?.instanceOf?.reduce((acc, instanceOfItem) => {
77-
const id = relativizeUrl(instanceOfItem['@id'])?.replace('#it', '');
83+
export function getBibIdsByInstanceId(
84+
mainEntity,
85+
displayUtil: DisplayUtil,
86+
record,
87+
locale: LocaleCode
88+
): Record<string, BibIdObj> {
89+
return mainEntity['@reverse']?.instanceOf?.reduce((acc, instance) => {
90+
const id = relativizeUrl(instance['@id'])?.replace('#it', '');
7891

79-
const bibId = instanceOfItem.meta?.controlNumber || record?.controlNumber;
80-
const type = instanceOfItem['@type'];
81-
const holders = instanceOfItem['@reverse']?.itemOf?.map((i) => i?.heldBy?.sigel);
92+
const bibId = instance.meta?.controlNumber || record?.controlNumber;
93+
const type = instance['@type'];
94+
const holders = instance['@reverse']?.itemOf?.map((i) => i?.heldBy?.sigel);
95+
const publication = instance.publication;
96+
let str = '';
97+
if (publication) {
98+
str =
99+
toString(displayUtil.lensAndFormat(instance.publication[0], LensType.Token, locale)) || '';
100+
}
82101

83102
// add Legacy Libris III system number for ONR param
84103
let onr = null;
@@ -90,7 +109,7 @@ export function getBibIdsByInstanceId(mainEntity, record): Record<string, BibIdO
90109

91110
const isbn: string[] = [];
92111
const issn: string[] = [];
93-
instanceOfItem.identifiedBy?.forEach((el: { '@type': string; value: string }) => {
112+
instance.identifiedBy?.forEach((el: { '@type': string; value: string }) => {
94113
if (el['@type'] === 'ISBN') {
95114
isbn.push(el.value);
96115
}
@@ -111,7 +130,8 @@ export function getBibIdsByInstanceId(mainEntity, record): Record<string, BibIdO
111130
holders,
112131
onr,
113132
isbn,
114-
issn
133+
issn,
134+
str
115135
}
116136
};
117137
}, {});
@@ -185,3 +205,145 @@ export function getMyLibsFromHoldings(
185205
}
186206
return Object.values(result);
187207
}
208+
209+
export async function fetchHoldersIfAbsent(holdersByType: HoldersByType) {
210+
const cachedHolders = holdersCache.holders;
211+
const allHolders = Object.values(holdersByType).flat();
212+
for (const h of allHolders) {
213+
const id = h.obj?.['@id'];
214+
215+
if (h.sigel && cachedHolders && !cachedHolders[h.sigel]) {
216+
const response = await fetch(`${id}?framed=true`, {
217+
headers: { Accept: 'application/ld+json' }
218+
});
219+
if (response.ok) {
220+
const resJson = await response.json();
221+
const libraryMainEntity = resJson['mainEntity'] as FramedData;
222+
if (libraryMainEntity) {
223+
cachedHolders[h.sigel] = libraryMainEntity;
224+
}
225+
} else {
226+
console.error(`Could not fetch holder data for ${id}`);
227+
}
228+
}
229+
}
230+
}
231+
232+
export function getItemLinksByBibId(
233+
bibIdsByInstanceId: Record<string, BibIdObj>,
234+
locale: LocaleCode,
235+
displayUtil: DisplayUtil
236+
): ItemLinksByBibId {
237+
const linksByInstanceId: ItemLinksByBibId = {};
238+
for (const bibIdObj of Object.values(bibIdsByInstanceId || [])) {
239+
const linksForHolder: ItemLinksForHolder = {};
240+
bibIdObj.holders?.forEach((sigel) => {
241+
if (holdersCache.holders) {
242+
const fullHolderData = holdersCache.holders[sigel];
243+
244+
const ilsPaths = [
245+
[BibDb.ils, BibDb.bibIdSearchUri],
246+
[BibDb.ils, BibDb.isbnSearchUri],
247+
[BibDb.ils, BibDb.issnSearchUri]
248+
];
249+
250+
const lopacPaths = [[BibDb.lopac, BibDb.bibIdSearchUriByLang]];
251+
252+
let linksToItem = getLinksToItemFor(bibIdObj, fullHolderData, ilsPaths, locale);
253+
const lopacLinksItem = getLinksToItemFor(bibIdObj, fullHolderData, lopacPaths, locale);
254+
255+
const linkTemplateEod = getAtPath(fullHolderData, [BibDb.eodUri], []);
256+
if (linkTemplateEod && linkTemplateEod.length !== 0) {
257+
linksToItem = [linkTemplateEod.replace(/%BIB_*ID%/, bibIdObj.bibId), ...linksToItem];
258+
}
259+
260+
//TODO: rename
261+
const allLinks: { [linkType: string]: string[] } = {};
262+
263+
const itemStatusUri = getAtPath(fullHolderData, [BibDb.ils, BibDb.itemStatusUri], []);
264+
265+
if (itemStatusUri && itemStatusUri.length !== 0) {
266+
allLinks[BibDb.ItemStatus] = [itemStatusUri];
267+
}
268+
269+
const linksToCatalog: string[] = [];
270+
const linkToCatalog = getAtPath(fullHolderData, [BibDb.ils, 'url'], undefined);
271+
if (linkToCatalog && linkToCatalog.length !== 0) {
272+
linksToCatalog.push(linkToCatalog);
273+
allLinks[BibDb.LinksToCatalog] = linksToCatalog;
274+
}
275+
276+
const linksToSite: string[] = [];
277+
const linkToSite = getAtPath(fullHolderData, ['url', JsonLd.ID], undefined);
278+
if (linkToSite && linkToSite.length !== 0) {
279+
linksToSite.push(linkToSite);
280+
allLinks[BibDb.LinksToSite] = linksToSite;
281+
}
282+
283+
const openingHoursList: string[] = [];
284+
const openingHours = getAtPath(fullHolderData, [BibDb.openingHours], undefined);
285+
if (openingHours && openingHours !== '') {
286+
openingHoursList.push(openingHours);
287+
allLinks[BibDb.OpeningHours] = openingHoursList;
288+
}
289+
290+
const addresses: string[] = [];
291+
const address = getAtPath(fullHolderData, [BibDb.address, '*'], undefined);
292+
const postalAddress = address.find((a) => a[JsonLd.TYPE] === BibDb.postalAddress);
293+
const visitingAddress = address.find((a) => a[JsonLd.TYPE] === BibDb.visitingAddress);
294+
295+
if (address && address.length !== 0) {
296+
addresses.push(
297+
toString(displayUtil.lensAndFormat(visitingAddress, LensType.Card, locale)) || ''
298+
);
299+
addresses.push(
300+
toString(displayUtil.lensAndFormat(postalAddress, LensType.Card, locale)) || ''
301+
);
302+
allLinks[BibDb.Address] = addresses;
303+
}
304+
305+
if (linksToItem.length !== 0) {
306+
allLinks[BibDb.LinksToItem] = linksToItem;
307+
}
308+
309+
if (lopacLinksItem.length !== 0) {
310+
allLinks[BibDb.LoanReserveLink] = lopacLinksItem;
311+
}
312+
313+
if (Object.keys(allLinks).length !== 0) {
314+
linksForHolder[sigel] = allLinks;
315+
}
316+
}
317+
});
318+
linksByInstanceId[bibIdObj.bibId] = linksForHolder;
319+
}
320+
return linksByInstanceId;
321+
}
322+
323+
function getLinksToItemFor(
324+
bibIdObj: BibIdObj,
325+
fullHolderData: FramedData,
326+
paths: string[][],
327+
locale: LocaleCode
328+
): string[] {
329+
let linksToItem: string[] = [];
330+
for (const path of paths) {
331+
const linkTemplate = getAtPath(fullHolderData, path, []);
332+
if (linkTemplate && linkTemplate.length !== 0) {
333+
if (path.includes(BibDb.bibIdSearchUriByLang) && bibIdObj.bibId !== '') {
334+
linksToItem = [linkTemplate[locale].replace(/%BIB_*ID%/g, bibIdObj.bibId), ...linksToItem];
335+
}
336+
if (path.includes(BibDb.bibIdSearchUri) && bibIdObj.bibId !== '') {
337+
// forms in the wild %BIB_ID%, %BIBID%, more???
338+
linksToItem = [linkTemplate.replace(/%BIB_*ID%/g, bibIdObj.bibId), ...linksToItem];
339+
}
340+
if (path.includes(BibDb.isbnSearchUri) && bibIdObj.isbn.length !== 0) {
341+
linksToItem = [linkTemplate.replace(/%ISBN%/g, bibIdObj.isbn), ...linksToItem];
342+
}
343+
if (path.includes(BibDb.issnSearchUri) && bibIdObj.issn.length !== 0) {
344+
linksToItem = [linkTemplate.replace(/%ISSN%/g, bibIdObj.issn), ...linksToItem];
345+
}
346+
}
347+
}
348+
return linksToItem;
349+
}

0 commit comments

Comments
 (0)