Skip to content

Commit fff95db

Browse files
lrosenstromolovy
andauthored
Filter groups and group descriptions (#1648)
* Group filters using filter groups specified by definitions * Handle different screen sizes * Minor adjustments * Handle language containers for specified properties * Add short table description --------- Co-authored-by: Olov Ylinenpää <olov.ylinenpaa@kb.se>
1 parent 625962e commit fff95db

10 files changed

Lines changed: 199 additions & 68 deletions

File tree

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,8 +60,8 @@ export default {
6060
description: 'Description',
6161
filter: 'Filter',
6262
reference: 'Reference',
63-
searchIn: 'Search in',
64-
keyword: 'Filter / keyword'
63+
searchIn: 'Searches in',
64+
code: 'Code'
6565
},
6666
myPages: {
6767
pageTitle: 'My pages',

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,8 @@ export default {
5959
description: 'Beskrivning',
6060
reference: 'Referens',
6161
searchIn: 'Söker i',
62-
keyword: 'Filter / nyckelord'
62+
filter: 'Filter',
63+
code: 'Kod'
6364
},
6465
myPages: {
6566
pageTitle: 'Mina sidor',

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -296,6 +296,12 @@ export interface QualifierDefinition extends QualifierSuggestion2 {
296296
propertyChainAxiom?: PropertyChain[];
297297
}
298298

299+
export interface QualifierDefinitionGroup {
300+
filters: QualifierDefinition[];
301+
filterGroupDescription?: string;
302+
label: string;
303+
}
304+
299305
export interface PropertyChain {
300306
label: string;
301307
path: string;

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ export enum Platform {
6767
impliedByObject = 'impliedByObject',
6868
preferLike = 'preferLike',
6969
composite = 'https://id.kb.se/ns/librissearch/composite',
70+
filters = 'https://id.kb.se/ns/librissearch/filters',
7071
meta = 'meta'
7172
}
7273

lxl-web/src/lib/utils/xl.server.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -326,6 +326,45 @@ export class DisplayUtil {
326326
this.registeredDerivedLensTypes[def.name] = def;
327327
}
328328

329+
getTranslated(data: FramedData | undefined, property: string, locale: LangCode): string {
330+
if (!data) {
331+
return undefined;
332+
}
333+
334+
if (property in this.langContainerAlias) {
335+
if (!data[this.langContainerAlias[property]]) {
336+
return undefined;
337+
}
338+
return this.pickLang(data[this.langContainerAlias[property]], locale);
339+
}
340+
341+
return undefined;
342+
}
343+
344+
//TODO: Duplicated from Formatter class in this file
345+
pickLang(container: LangContainer, locale) {
346+
if (container[locale]) {
347+
return container[locale];
348+
}
349+
350+
if (container[JsonLd.NONE]) {
351+
return container[JsonLd.NONE];
352+
}
353+
354+
for (const locale of this.locales) {
355+
if (container[locale]) {
356+
return container[locale];
357+
}
358+
}
359+
360+
const langKeys = Object.keys(container);
361+
if (langKeys.length > 0) {
362+
return container[langKeys.toSorted()[0]];
363+
}
364+
365+
return undefined;
366+
}
367+
329368
private deriveLens(type: ClassName, def: DerivedLensTypeDefinition): Lens {
330369
const empty = {
331370
[JsonLd.TYPE]: Fresnel.Lens,

lxl-web/src/routes/(app)/[[lang=lang]]/help/filters/+page.server.ts

Lines changed: 20 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -11,23 +11,28 @@ import {
1111
} from '$lib/types/xl';
1212
import { asArray, toString, isObject } from '$lib/utils/misc';
1313
import { type DisplayUtil, isLink, VocabUtil } from '$lib/utils/xl.server';
14-
import type { PropertyChain, QualifierDefinition } from '$lib/types/search';
14+
import type {
15+
PropertyChain,
16+
QualifierDefinition,
17+
QualifierDefinitionGroup
18+
} from '$lib/types/search';
1519
import { getUriSlug } from '$lib/utils/http';
1620

1721
export async function load({ locals, params }) {
1822
const locale = getSupportedLocale(params?.lang);
19-
20-
const collator = new Intl.Collator(locale).compare;
21-
22-
const filters = locals.vocab.getPropertiesByCategory(Platform.searchfilter);
23-
const filterDefs = filters
24-
.map((p) => mapSearchFilterDefinition(p, locale, locals.vocab, locals.display))
25-
.filter((p) => p !== null)
26-
.sort((a, b) => collator(a?.label, b?.label));
27-
23+
const groupedFilters = locals.vocab.getDefinition(Platform.filters);
24+
const all = groupedFilters.items[JsonLd.LIST];
25+
26+
const filterGroups: QualifierDefinitionGroup[] = all.map((group) => ({
27+
filters: group.items[JsonLd.LIST]
28+
.map((p) => locals.vocab.getDefinition(p[JsonLd.ID]))
29+
.map((p) => mapSearchFilterDefinition(p, locale, locals.vocab, locals.display))
30+
.filter((p): p is QualifierDefinition => p !== null),
31+
label: locals.display.getTranslated(group, 'label', locale),
32+
filterGroupDescription: locals.display.getTranslated(group, 'ls:filterGroupDescription', locale)
33+
}));
2834
return {
29-
filters: filters,
30-
filterDefs: filterDefs
35+
filterGroups: filterGroups
3136
};
3237
}
3338

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

4651
const propertyChain = mapPropertyChain(def, locale, vocab, display);
47-
4852
return {
49-
// FIXME???,
5053
key: key,
5154
label: toString(display.lensAndFormat(def, LensType.Chip, locale)),
5255
queryCodes: (asArray(def['librisQueryCode']) || []) as string[],
5356
altLabels: otherLangLabels,
54-
filterDescription: def['ls:filterDescription'] as string,
57+
filterDescription: display.getTranslated(def, 'ls:filterDescription', locale),
5558
...(propertyChain && { propertyChainAxiom: propertyChain }),
56-
descriptionRemark: (asArray(def['ls:descriptionRemark']) || []) as string[]
59+
descriptionRemark: (asArray(display.getTranslated(def, 'ls:descriptionRemark', locale)) ||
60+
[]) as string[]
5761
};
5862
} catch (error) {
5963
console.warn('Error mapping filter definition', error);

lxl-web/src/routes/(app)/[[lang=lang]]/help/filters/+page.svelte

Lines changed: 114 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -38,39 +38,94 @@
3838
siteName={getPageTitle(undefined, page.data.siteName)}
3939
/>
4040

41-
<article class="mx-auto mt-8 mb-12 w-7xl p-4 sm:px-6">
41+
<article class="@container mx-auto mt-8 mb-12 w-full max-w-7xl p-4 sm:px-6">
4242
{#if data.locale === 'en'}
4343
<EnContent />
4444
{:else}
4545
<SvContent />
4646
{/if}
47-
<table class="mt-2 w-full">
48-
<thead class="border-b border-gray-300">
49-
<tr class="[&>th]:p-3 [&>th]:text-left [&>th]:align-top">
50-
<th>{page.data.t('help.keyword')}</th>
51-
<th>{page.data.t('help.description')}</th>
52-
</tr>
53-
</thead>
54-
<tbody class="divide-y divide-gray-300 [&>tr>td]:p-3 [&>tr>td]:text-left [&>tr>td]:align-top">
55-
{#each data.filterDefs as f (f.key)}
56-
<tr id={f.key}>
57-
<td>
58-
<button
59-
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"
60-
onclick={() => addQualifierKey(f.key)}
47+
<div role="table" class="@container mt-2">
48+
<!-- Header row -->
49+
<div
50+
role="row"
51+
class="sr-only border-b border-gray-300 @3xl:not-sr-only @3xl:grid
52+
@3xl:grid-cols-[1fr_2fr_1.5fr_1fr] @3xl:gap-x-4"
53+
>
54+
<div id="filter-header" role="columnheader" class="p-3 font-medium">
55+
{page.data.t('help.filter')}
56+
</div>
57+
<div id="description-header" role="columnheader" class="p-3 font-medium">
58+
{page.data.t('help.description')}
59+
</div>
60+
<div id="searchin-header" role="columnheader" class="p-3 font-medium">
61+
{page.data.t('help.searchIn')}
62+
</div>
63+
<div id="code-header" role="columnheader" class="p-3 font-medium">
64+
{page.data.t('help.code')}
65+
</div>
66+
</div>
67+
68+
{#each data.filterGroups as g, i (i)}
69+
<div role="rowgroup" aria-label={g.label}>
70+
{#if g.label || g.filterGroupDescription}
71+
<div role="row" class="border-b border-gray-300 bg-neutral-100 px-4 py-3">
72+
{#if g.label}
73+
<div role="columnheader" aria-colspan="4" class="font-semibold">
74+
{g.label}
75+
</div>
76+
{/if}
77+
78+
{#if g.filterGroupDescription}
79+
<div class="text-2s text-subtle pt-2 whitespace-pre-line">
80+
{g.filterGroupDescription}
81+
</div>
82+
{/if}
83+
</div>
84+
{/if}
85+
86+
{#each g.filters as f (f.key)}
87+
<div
88+
role="row"
89+
id={f.key}
90+
class="border-b border-gray-300 py-4
91+
@3xl:grid
92+
@3xl:grid-cols-[1fr_2fr_1.5fr_1fr]
93+
@3xl:gap-x-4
94+
@3xl:py-0"
95+
>
96+
<!-- Filter -->
97+
<div
98+
role="rowheader"
99+
id={`row-${f.key}`}
100+
aria-labelledby="filter-header row-{f.key}"
101+
class="min-w-0 p-3"
61102
>
62-
{f.label}
63-
</button>
64-
<ul class="mt-2 font-mono">
65-
<li class="text-xs">{f.key}</li>
66-
{#each f.queryCodes as q (q)}
67-
<li class="text-subtle text-xs">{q}</li>
68-
{/each}
69-
</ul>
70-
</td>
71-
<td class={['grid grid-cols-1 gap-3 sm:grid-cols-2']}>
72-
<div>
73-
<span class="whitespace-pre-line">{f.filterDescription}</span>
103+
<div class="mb-1 text-xs font-medium text-neutral-500 @3xl:hidden">
104+
{page.data.t('help.filter')}
105+
</div>
106+
107+
<button
108+
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"
109+
onclick={() => addQualifierKey(f.key)}
110+
>
111+
{f.label}
112+
</button>
113+
</div>
114+
115+
<!-- Description -->
116+
<div
117+
role="cell"
118+
aria-labelledby={`description-header row-${f.key}`}
119+
class="min-w-0 p-3"
120+
>
121+
<div class="mb-1 text-xs font-medium text-neutral-500 @3xl:hidden">
122+
{page.data.t('help.description')}
123+
</div>
124+
125+
<span class="whitespace-pre-line">
126+
{f.filterDescription}
127+
</span>
128+
74129
{#if f.descriptionRemark}
75130
<div class="mt-4">
76131
{#each f.descriptionRemark as remark, i (i)}
@@ -81,30 +136,44 @@
81136
</div>
82137
{/if}
83138
</div>
84-
{#if f.propertyChainAxiom}
85-
<div>
86-
<span class="text-2s text-subtle">{page.data.t('help.searchIn')}</span>
139+
140+
<!-- Search in -->
141+
<div role="cell" aria-labelledby={`searchin-header row-${f.key}`} class="min-w-0 p-3">
142+
<div class="mb-1 text-xs font-medium text-neutral-500 @3xl:hidden">
143+
{page.data.t('help.searchIn')}
144+
</div>
145+
146+
{#if f.propertyChainAxiom}
87147
<ul>
88148
{#each f.propertyChainAxiom as p (p)}
89149
<li class="text-s">{p.label}</li>
90-
<li class="text-2xs text-subtle mb-2 font-mono">{p.path}</li>
150+
<li class="text-2xs text-subtle mb-2 font-mono [overflow-wrap:anywhere]">
151+
{p.path}
152+
</li>
91153
{/each}
92154
</ul>
155+
{/if}
156+
</div>
157+
158+
<!-- Code -->
159+
<div role="cell" aria-labelledby={`code-header row-${f.key}`} class="min-w-0 p-3">
160+
<div class="mb-1 text-xs font-medium text-neutral-500 @3xl:hidden">
161+
{page.data.t('help.code')}
93162
</div>
94-
{/if}
95-
</td>
96-
</tr>
97-
{/each}
98-
</tbody>
99-
</table>
100-
<!--
101-
{#each data.filters as f (f['@id'])}
102-
<pre>{JSON.stringify(f, null, 2)}</pre>
103-
{/each}
104-
{#each data.filterDefs as f (f.key)}
105-
<pre>{JSON.stringify(f, null, 2)}</pre>
106-
{/each}
107-
-->
163+
164+
<ul class="font-mono">
165+
<li class="text-xs [overflow-wrap:anywhere]">{f.key}</li>
166+
167+
{#each f.queryCodes as q (q)}
168+
<li class="text-subtle text-xs">{q}</li>
169+
{/each}
170+
</ul>
171+
</div>
172+
</div>
173+
{/each}
174+
</div>
175+
{/each}
176+
</div>
108177
</article>
109178

110179
<style lang="postcss">

lxl-web/src/routes/(app)/[[lang=lang]]/help/filters/en.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,10 @@ title: 'en'
44

55
# Search filters
66

7-
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.
7+
All keywords that can be used to filter a search are listed here. The [search help page](/help/search) provides examples of
8+
how filters can be added, combined, and used.
9+
10+
Each row in the table describes a search filter that can be added by clicking the blue button on the far left.
11+
For each filter, the table provides a brief description of its purpose, indicates which part of the bibliographic
12+
record's metadata the filter searches, and lists the search codes (for example, from the legacy Libris system) that
13+
correspond to the filter.

lxl-web/src/routes/(app)/[[lang=lang]]/help/filters/sv.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,9 @@ title: 'se'
44

55
# Sökfilter
66

7-
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.
7+
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
8+
filter läggs till, kombineras och används.
9+
10+
Varje rad i tabellen beskriver ett sökfilter som kan läggas till genom att klicka på den blå knappen längst
11+
till vänster. För varje filter anges en kort beskrivning av dess funktion, vilken del av den bibliografiska postens
12+
metadata filtret söker i, samt vilka sökkoder (till exempel från gamla Libris) som motsvaras av filtret.

lxl-web/tests/help.filters.spec.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,6 @@ test('qualifier keys can be added from filter list', async ({ page }) => {
1717
await expect(page.getByRole('combobox').first()).toContainText('Ingår i bibliografi');
1818
await expect(page.getByRole('combobox').last()).toContainText('Ingår i bibliografi');
1919
await page.keyboard.press('Escape');
20-
await page.getByRole('main').getByRole('button').getByText('Bibliotek').click();
21-
await expect(page.getByRole('combobox').first()).toContainText('Ingår i bibliografi');
20+
await page.getByRole('main').getByRole('button').getByText('Bibliotek').first().click();
21+
await expect(page.getByRole('combobox').first()).toContainText('Bibliotek');
2222
});

0 commit comments

Comments
 (0)