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
4 changes: 2 additions & 2 deletions lxl-web/src/lib/i18n/locales/en.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,8 @@ export default {
description: 'Description',
filter: 'Filter',
reference: 'Reference',
searchIn: 'Search in',
keyword: 'Filter / keyword'
searchIn: 'Searches in',
code: 'Code'
},
myPages: {
pageTitle: 'My pages',
Expand Down
3 changes: 2 additions & 1 deletion lxl-web/src/lib/i18n/locales/sv.js
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,8 @@ export default {
description: 'Beskrivning',
reference: 'Referens',
searchIn: 'Söker i',
keyword: 'Filter / nyckelord'
filter: 'Filter',
code: 'Kod'
},
myPages: {
pageTitle: 'Mina sidor',
Expand Down
6 changes: 6 additions & 0 deletions lxl-web/src/lib/types/search.ts
Original file line number Diff line number Diff line change
Expand Up @@ -296,6 +296,12 @@ export interface QualifierDefinition extends QualifierSuggestion2 {
propertyChainAxiom?: PropertyChain[];
}

export interface QualifierDefinitionGroup {
filters: QualifierDefinition[];
filterGroupDescription?: string;
label: string;
}

export interface PropertyChain {
label: string;
path: string;
Expand Down
1 change: 1 addition & 0 deletions lxl-web/src/lib/types/xl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ export enum Platform {
impliedByObject = 'impliedByObject',
preferLike = 'preferLike',
composite = 'https://id.kb.se/ns/librissearch/composite',
filters = 'https://id.kb.se/ns/librissearch/filters',
meta = 'meta'
}

Expand Down
39 changes: 39 additions & 0 deletions lxl-web/src/lib/utils/xl.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -326,6 +326,45 @@ export class DisplayUtil {
this.registeredDerivedLensTypes[def.name] = def;
}

getTranslated(data: FramedData | undefined, property: string, locale: LangCode): string {
if (!data) {
return undefined;
}

if (property in this.langContainerAlias) {
if (!data[this.langContainerAlias[property]]) {
return undefined;
}
return this.pickLang(data[this.langContainerAlias[property]], locale);
}

return undefined;
}

//TODO: Duplicated from Formatter class in this file
pickLang(container: LangContainer, locale) {
if (container[locale]) {
return container[locale];
}

if (container[JsonLd.NONE]) {
return container[JsonLd.NONE];
}

for (const locale of this.locales) {
if (container[locale]) {
return container[locale];
}
}

const langKeys = Object.keys(container);
if (langKeys.length > 0) {
return container[langKeys.toSorted()[0]];
}

return undefined;
}

private deriveLens(type: ClassName, def: DerivedLensTypeDefinition): Lens {
const empty = {
[JsonLd.TYPE]: Fresnel.Lens,
Expand Down
36 changes: 20 additions & 16 deletions lxl-web/src/routes/(app)/[[lang=lang]]/help/filters/+page.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,23 +11,28 @@ import {
} from '$lib/types/xl';
import { asArray, toString, isObject } from '$lib/utils/misc';
import { type DisplayUtil, isLink, VocabUtil } from '$lib/utils/xl.server';
import type { PropertyChain, QualifierDefinition } from '$lib/types/search';
import type {
PropertyChain,
QualifierDefinition,
QualifierDefinitionGroup
} from '$lib/types/search';
import { getUriSlug } from '$lib/utils/http';

export async function load({ locals, params }) {
const locale = getSupportedLocale(params?.lang);

const collator = new Intl.Collator(locale).compare;

const filters = locals.vocab.getPropertiesByCategory(Platform.searchfilter);
const filterDefs = filters
.map((p) => mapSearchFilterDefinition(p, locale, locals.vocab, locals.display))
.filter((p) => p !== null)
.sort((a, b) => collator(a?.label, b?.label));

const groupedFilters = locals.vocab.getDefinition(Platform.filters);
const all = groupedFilters.items[JsonLd.LIST];

const filterGroups: QualifierDefinitionGroup[] = all.map((group) => ({
filters: group.items[JsonLd.LIST]
.map((p) => locals.vocab.getDefinition(p[JsonLd.ID]))
.map((p) => mapSearchFilterDefinition(p, locale, locals.vocab, locals.display))
.filter((p): p is QualifierDefinition => p !== null),
label: locals.display.getTranslated(group, 'label', locale),
filterGroupDescription: locals.display.getTranslated(group, 'ls:filterGroupDescription', locale)
}));
return {
filters: filters,
filterDefs: filterDefs
filterGroups: filterGroups
};
}

Expand All @@ -44,16 +49,15 @@ function mapSearchFilterDefinition(
const key = getUriSlug(def[JsonLd.ID]) as string;

const propertyChain = mapPropertyChain(def, locale, vocab, display);

return {
// FIXME???,
key: key,
label: toString(display.lensAndFormat(def, LensType.Chip, locale)),
queryCodes: (asArray(def['librisQueryCode']) || []) as string[],
altLabels: otherLangLabels,
filterDescription: def['ls:filterDescription'] as string,
filterDescription: display.getTranslated(def, 'ls:filterDescription', locale),
...(propertyChain && { propertyChainAxiom: propertyChain }),
descriptionRemark: (asArray(def['ls:descriptionRemark']) || []) as string[]
descriptionRemark: (asArray(display.getTranslated(def, 'ls:descriptionRemark', locale)) ||
[]) as string[]
};
} catch (error) {
console.warn('Error mapping filter definition', error);
Expand Down
159 changes: 114 additions & 45 deletions lxl-web/src/routes/(app)/[[lang=lang]]/help/filters/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -38,39 +38,94 @@
siteName={getPageTitle(undefined, page.data.siteName)}
/>

<article class="mx-auto mt-8 mb-12 w-7xl p-4 sm:px-6">
<article class="@container mx-auto mt-8 mb-12 w-full max-w-7xl p-4 sm:px-6">
{#if data.locale === 'en'}
<EnContent />
{:else}
<SvContent />
{/if}
<table class="mt-2 w-full">
<thead class="border-b border-gray-300">
<tr class="[&>th]:p-3 [&>th]:text-left [&>th]:align-top">
<th>{page.data.t('help.keyword')}</th>
<th>{page.data.t('help.description')}</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-300 [&>tr>td]:p-3 [&>tr>td]:text-left [&>tr>td]:align-top">
{#each data.filterDefs as f (f.key)}
<tr id={f.key}>
<td>
<button
class="qualifier text-body bg-accent-50 text-2xs hover:bg-accent-100 inline-block min-h-8 min-w-9 shrink-0 rounded-md px-1.5 font-medium whitespace-nowrap first-letter:capitalize"
onclick={() => addQualifierKey(f.key)}
<div role="table" class="@container mt-2">
<!-- Header row -->
<div
role="row"
class="sr-only border-b border-gray-300 @3xl:not-sr-only @3xl:grid
@3xl:grid-cols-[1fr_2fr_1.5fr_1fr] @3xl:gap-x-4"
>
<div id="filter-header" role="columnheader" class="p-3 font-medium">
{page.data.t('help.filter')}
</div>
<div id="description-header" role="columnheader" class="p-3 font-medium">
{page.data.t('help.description')}
</div>
<div id="searchin-header" role="columnheader" class="p-3 font-medium">
{page.data.t('help.searchIn')}
</div>
<div id="code-header" role="columnheader" class="p-3 font-medium">
{page.data.t('help.code')}
</div>
</div>

{#each data.filterGroups as g, i (i)}
<div role="rowgroup" aria-label={g.label}>
{#if g.label || g.filterGroupDescription}
<div role="row" class="border-b border-gray-300 bg-neutral-100 px-4 py-3">
{#if g.label}
<div role="columnheader" aria-colspan="4" class="font-semibold">
{g.label}
</div>
{/if}

{#if g.filterGroupDescription}
<div class="text-2s text-subtle pt-2 whitespace-pre-line">
{g.filterGroupDescription}
</div>
{/if}
</div>
{/if}

{#each g.filters as f (f.key)}
<div
role="row"
id={f.key}
class="border-b border-gray-300 py-4
@3xl:grid
@3xl:grid-cols-[1fr_2fr_1.5fr_1fr]
@3xl:gap-x-4
@3xl:py-0"
>
<!-- Filter -->
<div
role="rowheader"
id={`row-${f.key}`}
aria-labelledby="filter-header row-{f.key}"
class="min-w-0 p-3"
>
{f.label}
</button>
<ul class="mt-2 font-mono">
<li class="text-xs">{f.key}</li>
{#each f.queryCodes as q (q)}
<li class="text-subtle text-xs">{q}</li>
{/each}
</ul>
</td>
<td class={['grid grid-cols-1 gap-3 sm:grid-cols-2']}>
<div>
<span class="whitespace-pre-line">{f.filterDescription}</span>
<div class="mb-1 text-xs font-medium text-neutral-500 @3xl:hidden">
{page.data.t('help.filter')}
</div>

<button
class="qualifier text-body bg-accent-50 text-2xs hover:bg-accent-100 inline-block min-h-8 min-w-9 shrink-0 rounded-md px-1.5 font-medium whitespace-nowrap first-letter:capitalize"
onclick={() => addQualifierKey(f.key)}
>
{f.label}
</button>
</div>

<!-- Description -->
<div
role="cell"
aria-labelledby={`description-header row-${f.key}`}
class="min-w-0 p-3"
>
<div class="mb-1 text-xs font-medium text-neutral-500 @3xl:hidden">
{page.data.t('help.description')}
</div>

<span class="whitespace-pre-line">
{f.filterDescription}
</span>

{#if f.descriptionRemark}
<div class="mt-4">
{#each f.descriptionRemark as remark, i (i)}
Expand All @@ -81,30 +136,44 @@
</div>
{/if}
</div>
{#if f.propertyChainAxiom}
<div>
<span class="text-2s text-subtle">{page.data.t('help.searchIn')}</span>

<!-- Search in -->
<div role="cell" aria-labelledby={`searchin-header row-${f.key}`} class="min-w-0 p-3">
<div class="mb-1 text-xs font-medium text-neutral-500 @3xl:hidden">
{page.data.t('help.searchIn')}
</div>

{#if f.propertyChainAxiom}
<ul>
{#each f.propertyChainAxiom as p (p)}
<li class="text-s">{p.label}</li>
<li class="text-2xs text-subtle mb-2 font-mono">{p.path}</li>
<li class="text-2xs text-subtle mb-2 font-mono [overflow-wrap:anywhere]">
{p.path}
</li>
{/each}
</ul>
{/if}
</div>

<!-- Code -->
<div role="cell" aria-labelledby={`code-header row-${f.key}`} class="min-w-0 p-3">
<div class="mb-1 text-xs font-medium text-neutral-500 @3xl:hidden">
{page.data.t('help.code')}
</div>
{/if}
</td>
</tr>
{/each}
</tbody>
</table>
<!--
{#each data.filters as f (f['@id'])}
<pre>{JSON.stringify(f, null, 2)}</pre>
{/each}
{#each data.filterDefs as f (f.key)}
<pre>{JSON.stringify(f, null, 2)}</pre>
{/each}
-->

<ul class="font-mono">
<li class="text-xs [overflow-wrap:anywhere]">{f.key}</li>

{#each f.queryCodes as q (q)}
<li class="text-subtle text-xs">{q}</li>
{/each}
</ul>
</div>
</div>
{/each}
</div>
{/each}
</div>
</article>

<style lang="postcss">
Expand Down
8 changes: 7 additions & 1 deletion lxl-web/src/routes/(app)/[[lang=lang]]/help/filters/en.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,10 @@ title: 'en'

# Search filters

All keywords that can be used to filter a search are listed here. The [search help page](/help/search) provides examples of how filters can be added, combined, and used.
All keywords that can be used to filter a search are listed here. The [search help page](/help/search) provides examples of
how filters can be added, combined, and used.

Each row in the table describes a search filter that can be added by clicking the blue button on the far left.
For each filter, the table provides a brief description of its purpose, indicates which part of the bibliographic
record's metadata the filter searches, and lists the search codes (for example, from the legacy Libris system) that
correspond to the filter.
7 changes: 6 additions & 1 deletion lxl-web/src/routes/(app)/[[lang=lang]]/help/filters/sv.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,9 @@ title: 'se'

# Sökfilter

Här listas alla nyckelord som kan användas för att filtrera en sökning. På [sökhjälpsidan](/help/search) finns exempel på hur filter läggs till, kombineras och används.
Här listas alla nyckelord som kan användas för att filtrera en sökning. På [sökhjälpsidan](/help/search) finns exempel på hur
filter läggs till, kombineras och används.

Varje rad i tabellen beskriver ett sökfilter som kan läggas till genom att klicka på den blå knappen längst
till vänster. För varje filter anges en kort beskrivning av dess funktion, vilken del av den bibliografiska postens
metadata filtret söker i, samt vilka sökkoder (till exempel från gamla Libris) som motsvaras av filtret.
4 changes: 2 additions & 2 deletions lxl-web/tests/help.filters.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,6 @@ test('qualifier keys can be added from filter list', async ({ page }) => {
await expect(page.getByRole('combobox').first()).toContainText('Ingår i bibliografi');
await expect(page.getByRole('combobox').last()).toContainText('Ingår i bibliografi');
await page.keyboard.press('Escape');
await page.getByRole('main').getByRole('button').getByText('Bibliotek').click();
await expect(page.getByRole('combobox').first()).toContainText('Ingår i bibliografi');
await page.getByRole('main').getByRole('button').getByText('Bibliotek').first().click();
await expect(page.getByRole('combobox').first()).toContainText('Bibliotek');
});
Loading