From 67fde5cccecb271253421cbe4e968877bccd94ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jesper=20Engstr=C3=B6m?= Date: Wed, 11 Sep 2024 10:51:08 +0200 Subject: [PATCH 01/10] Add checkbox icon --- .../src/lib/components/find/FacetGroup.svelte | 17 ++++++++++++----- lxl-web/src/lib/i18n/locales/en.js | 2 +- lxl-web/src/lib/i18n/locales/sv.js | 2 +- 3 files changed, 14 insertions(+), 7 deletions(-) diff --git a/lxl-web/src/lib/components/find/FacetGroup.svelte b/lxl-web/src/lib/components/find/FacetGroup.svelte index 97c938fcf..3f8f3601d 100644 --- a/lxl-web/src/lib/components/find/FacetGroup.svelte +++ b/lxl-web/src/lib/components/find/FacetGroup.svelte @@ -3,6 +3,8 @@ import { page } from '$app/stores'; import { type FacetGroup } from '$lib/types/search'; import BiChevronRight from '~icons/bi/chevron-right'; + import CheckSquareFill from '~icons/bi/check-square-fill'; + import Square from '~icons/bi/square'; import FacetRange from './FacetRange.svelte'; import DecoratedData from '../DecoratedData.svelte'; import { ShowLabelsOptions } from '$lib/types/decoratedData'; @@ -65,16 +67,21 @@ {#each shownFacets as facet (facet.view['@id'])}
  • {#if 'selected' in facet} - {facet.selected ? $page.data.t('search.activeFilters') : ''}{facet.selected ? $page.data.t('search.activeFilter') : ''} - + {/if} @@ -94,7 +101,7 @@ {#if canShowMoreFacets || canShowLessFacets} -
    - {#if group.search && !(searchPhrase && hasHits)} - - - {/if} -
      - {#each shownFacets as facet (facet.view['@id'])} -
    1. - - - {#if 'selected' in facet} - {facet.selected ? $page.data.t('search.activeFilter') : ''}{group.label} + + + +
    2. + {/each} +
    + {#if canShowMoreFacets || canShowLessFacets} + + {/if} + {#if searchPhrase && filteredFacets.length === 0} + {$page.data.t('search.noResults')} + {/if} +
    +
  • diff --git a/lxl-web/src/lib/constants/facets.ts b/lxl-web/src/lib/constants/facets.ts new file mode 100644 index 000000000..dcbb9b21f --- /dev/null +++ b/lxl-web/src/lib/constants/facets.ts @@ -0,0 +1,2 @@ +export const DEFAULT_FACETS_SHOWN = 5; +export const DEFAULT_FACET_SORT = 'hits.desc'; diff --git a/lxl-web/src/lib/i18n/locales/en.js b/lxl-web/src/lib/i18n/locales/en.js index c19241eb6..c7fffbd8a 100644 --- a/lxl-web/src/lib/i18n/locales/en.js +++ b/lxl-web/src/lib/i18n/locales/en.js @@ -102,7 +102,11 @@ export default { alphaDesc: 'Z-A', publicationAsc: 'Publication year (oldest first)', publicationDesc: 'Publication year (newest first)', - linksDesc: 'Most linked' + linksDesc: 'Most linked', + hitsAsc: 'Hits asc.', + hitsDesc: 'Hits desc.', + yearAsc: 'Oldest first', + yearDesc: 'Newest first' }, errors: { somethingWentWrong: 'Something went wrong', diff --git a/lxl-web/src/lib/i18n/locales/sv.js b/lxl-web/src/lib/i18n/locales/sv.js index 828198fd9..fc4e99330 100644 --- a/lxl-web/src/lib/i18n/locales/sv.js +++ b/lxl-web/src/lib/i18n/locales/sv.js @@ -101,7 +101,11 @@ export default { alphaDesc: 'Ö-A', publicationAsc: 'Utgivningsår (äldst först)', publicationDesc: 'Utgivningsår (nyast först)', - linksDesc: 'Mest länkad' + linksDesc: 'Mest länkad', + hitsAsc: 'Minst träffar', + hitsDesc: 'Flest träffar', + yearAsc: 'Äldst först', + yearDesc: 'Nyast först' }, errors: { somethingWentWrong: 'Något gick fel', diff --git a/lxl-web/src/lib/types/userSettings.ts b/lxl-web/src/lib/types/userSettings.ts new file mode 100644 index 000000000..39c5047ca --- /dev/null +++ b/lxl-web/src/lib/types/userSettings.ts @@ -0,0 +1,7 @@ +export type UserSettings = SettingsObj | undefined; + +interface SettingsObj { + facetSort: { + [dimension: string]: string; + }; +} diff --git a/lxl-web/src/lib/utils/userSettings.ts b/lxl-web/src/lib/utils/userSettings.ts new file mode 100644 index 000000000..8447e4662 --- /dev/null +++ b/lxl-web/src/lib/utils/userSettings.ts @@ -0,0 +1,30 @@ +import Cookies from 'js-cookie'; +import { browser } from '$app/environment'; +import type { UserSettings } from '$lib/types/userSettings'; + +export function saveUserSetting(namespace: 'facetSort', value: { [dimension: string]: string }) { + if (browser) { + const userSettings = Cookies.get('userSettings'); + let newSettings; + + if (userSettings) { + try { + const parsedSettings = JSON.parse(userSettings) as UserSettings; + if (parsedSettings) { + newSettings = { + ...parsedSettings, + [namespace]: { + ...(namespace in parsedSettings && parsedSettings[namespace]), + ...value + } + }; + } + } catch (e) { + console.warn(e); + } + } else { + newSettings = { [namespace]: value }; + } + Cookies.set('userSettings', JSON.stringify(newSettings)); + } +} diff --git a/lxl-web/src/routes/+layout.server.ts b/lxl-web/src/routes/+layout.server.ts new file mode 100644 index 000000000..bdcf6801d --- /dev/null +++ b/lxl-web/src/routes/+layout.server.ts @@ -0,0 +1,16 @@ +import type { UserSettings } from '$lib/types/userSettings.js'; + +export async function load({ cookies }) { + let userSettings: UserSettings; + const settingsCookie = cookies.get('userSettings'); + if (settingsCookie) { + try { + userSettings = JSON.parse(settingsCookie); + } catch (e) { + console.warn('Failed to parse user settings', e); + } + } + return { + userSettings + }; +} diff --git a/lxl-web/src/routes/+layout.ts b/lxl-web/src/routes/+layout.ts index 50d97e242..096c80bc4 100644 --- a/lxl-web/src/routes/+layout.ts +++ b/lxl-web/src/routes/+layout.ts @@ -1,9 +1,10 @@ import { getTranslator } from '$lib/i18n/index.js'; import { getSupportedLocale, defaultLocale } from '$lib/i18n/locales'; -export async function load({ params }) { +export async function load({ params, data }) { const locale = getSupportedLocale(params?.lang); // will use default locale if no lang param const t = await getTranslator(locale); const base = locale === defaultLocale ? '/' : `/${locale}/`; - return { locale, t, base }; + const userSettings = data.userSettings; + return { locale, t, base, userSettings }; } From 62611dd24ecbf3d835a1ab2e051518dbe741ac31 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jesper=20Engstr=C3=B6m?= Date: Fri, 13 Sep 2024 17:49:09 +0200 Subject: [PATCH 06/10] Use handle hook instead of layout load to get cookie --- lxl-web/src/app.d.ts | 7 ++++++- lxl-web/src/hooks.server.ts | 13 +++++++++++++ .../[fnurgel=fnurgel]/+page.server.ts | 10 ++++++---- .../(app)/[[lang=lang]]/find/+page.server.ts | 8 ++++---- lxl-web/src/routes/+layout.server.ts | 16 ---------------- lxl-web/src/routes/+layout.ts | 5 ++--- 6 files changed, 31 insertions(+), 28 deletions(-) delete mode 100644 lxl-web/src/routes/+layout.server.ts diff --git a/lxl-web/src/app.d.ts b/lxl-web/src/app.d.ts index ac259c0f4..4aeeab391 100644 --- a/lxl-web/src/app.d.ts +++ b/lxl-web/src/app.d.ts @@ -1,6 +1,7 @@ // See https://kit.svelte.dev/docs/types#app // for information about these interfaces import type { UserSettings } from '$lib/types/userSettings'; +import type { DisplayUtil, VocabUtil } from '$lib/utils/xl'; import 'unplugin-icons/types/svelte'; declare global { @@ -8,7 +9,11 @@ declare global { interface Error { status?: string; } - // interface Locals {} + interface Locals { + vocab: VocabUtil; + display: DisplayUtil; + userSettings: UserSettings; + } interface PageData { locale: import('$lib/i18n/locales').LocaleCode; t: Awaited>; diff --git a/lxl-web/src/hooks.server.ts b/lxl-web/src/hooks.server.ts index 4650569d3..6b66e36d1 100644 --- a/lxl-web/src/hooks.server.ts +++ b/lxl-web/src/hooks.server.ts @@ -4,6 +4,7 @@ import { DisplayUtil, VocabUtil } from '$lib/utils/xl'; import fs from 'fs'; import { DERIVED_LENSES } from '$lib/types/display'; import displayWeb from '$lib/assets/json/display-web.json'; +import type { UserSettings } from '$lib/types/userSettings'; let utilCache; @@ -12,6 +13,18 @@ export const handle = async ({ event, resolve }) => { event.locals.vocab = vocabUtil; event.locals.display = displayUtil; + // Get the settings cookie + let userSettings: UserSettings; + const settingsCookie = event.cookies.get('userSettings'); + if (settingsCookie) { + try { + userSettings = JSON.parse(settingsCookie); + } catch (e) { + console.warn('Failed to parse user settings', e); + } + } + event.locals.userSettings = userSettings; + // set HTML lang // https://github.com/sveltejs/kit/issues/3091#issuecomment-1112589090 const path = event.url.pathname; diff --git a/lxl-web/src/routes/(app)/[[lang=lang]]/[fnurgel=fnurgel]/+page.server.ts b/lxl-web/src/routes/(app)/[[lang=lang]]/[fnurgel=fnurgel]/+page.server.ts index a1d3a768a..8bb55e9b5 100644 --- a/lxl-web/src/routes/(app)/[[lang=lang]]/[fnurgel=fnurgel]/+page.server.ts +++ b/lxl-web/src/routes/(app)/[[lang=lang]]/[fnurgel=fnurgel]/+page.server.ts @@ -8,7 +8,7 @@ import { LxlLens } from '$lib/types/display'; import { type ApiError } from '$lib/types/api.js'; import type { PartialCollectionView, SearchResult } from '$lib/types/search.js'; -import { DisplayUtil, pickProperty, toString, VocabUtil, asArray } from '$lib/utils/xl.js'; +import { pickProperty, toString, asArray } from '$lib/utils/xl.js'; import { getImages, toSecure } from '$lib/utils/auxd'; import addDefaultSearchParams from '$lib/utils/addDefaultSearchParams.js'; import getSortedSearchParams from '$lib/utils/getSortedSearchParams.js'; @@ -22,9 +22,10 @@ import { } from '$lib/utils/holdings.js'; export const load = async ({ params, url, locals, fetch }) => { - const displayUtil: DisplayUtil = locals.display; - const vocabUtil: VocabUtil = locals.vocab; + const displayUtil = locals.display; + const vocabUtil = locals.vocab; const locale = getSupportedLocale(params?.lang); + const userSettings = locals.userSettings; let resourceId: null | string = null; let searchPromise: Promise | null = null; @@ -78,7 +79,8 @@ export const load = async ({ params, url, locals, fetch }) => { holdersByType, full: overview, images, - searchResult: searchPromise ? await searchPromise : null + searchResult: searchPromise ? await searchPromise : null, + userSettings }; async function getRelated() { diff --git a/lxl-web/src/routes/(app)/[[lang=lang]]/find/+page.server.ts b/lxl-web/src/routes/(app)/[[lang=lang]]/find/+page.server.ts index 9bc1764f6..6581ed489 100644 --- a/lxl-web/src/routes/(app)/[[lang=lang]]/find/+page.server.ts +++ b/lxl-web/src/routes/(app)/[[lang=lang]]/find/+page.server.ts @@ -4,13 +4,13 @@ import { getSupportedLocale } from '$lib/i18n/locales.js'; import { type ApiError } from '$lib/types/api.js'; import type { PartialCollectionView } from '$lib/types/search.js'; -import { DisplayUtil, VocabUtil } from '$lib/utils/xl.js'; import { asResult } from '$lib/utils/search'; export const load = async ({ params, url, locals, fetch }) => { - const displayUtil: DisplayUtil = locals.display; - const vocabUtil: VocabUtil = locals.vocab; + const displayUtil = locals.display; + const vocabUtil = locals.vocab; const locale = getSupportedLocale(params?.lang); + const userSettings = locals.userSettings; if (!url.searchParams.size) { redirect(303, `/`); // redirect to home page if no search params are given @@ -49,5 +49,5 @@ export const load = async ({ params, url, locals, fetch }) => { url.pathname ); - return { searchResult }; + return { searchResult, userSettings }; }; diff --git a/lxl-web/src/routes/+layout.server.ts b/lxl-web/src/routes/+layout.server.ts deleted file mode 100644 index bdcf6801d..000000000 --- a/lxl-web/src/routes/+layout.server.ts +++ /dev/null @@ -1,16 +0,0 @@ -import type { UserSettings } from '$lib/types/userSettings.js'; - -export async function load({ cookies }) { - let userSettings: UserSettings; - const settingsCookie = cookies.get('userSettings'); - if (settingsCookie) { - try { - userSettings = JSON.parse(settingsCookie); - } catch (e) { - console.warn('Failed to parse user settings', e); - } - } - return { - userSettings - }; -} diff --git a/lxl-web/src/routes/+layout.ts b/lxl-web/src/routes/+layout.ts index 096c80bc4..50d97e242 100644 --- a/lxl-web/src/routes/+layout.ts +++ b/lxl-web/src/routes/+layout.ts @@ -1,10 +1,9 @@ import { getTranslator } from '$lib/i18n/index.js'; import { getSupportedLocale, defaultLocale } from '$lib/i18n/locales'; -export async function load({ params, data }) { +export async function load({ params }) { const locale = getSupportedLocale(params?.lang); // will use default locale if no lang param const t = await getTranslator(locale); const base = locale === defaultLocale ? '/' : `/${locale}/`; - const userSettings = data.userSettings; - return { locale, t, base, userSettings }; + return { locale, t, base }; } From ef2c916ee96042765f954b8bb7bbaeef8d80f4f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jesper=20Engstr=C3=B6m?= Date: Mon, 16 Sep 2024 12:14:20 +0200 Subject: [PATCH 07/10] Set cookie options --- lxl-web/src/lib/utils/userSettings.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lxl-web/src/lib/utils/userSettings.ts b/lxl-web/src/lib/utils/userSettings.ts index 8447e4662..c41fec1e8 100644 --- a/lxl-web/src/lib/utils/userSettings.ts +++ b/lxl-web/src/lib/utils/userSettings.ts @@ -25,6 +25,10 @@ export function saveUserSetting(namespace: 'facetSort', value: { [dimension: str } else { newSettings = { [namespace]: value }; } - Cookies.set('userSettings', JSON.stringify(newSettings)); + Cookies.set('userSettings', JSON.stringify(newSettings), { + expires: 365, + secure: true, + sameSite: 'strict' + }); } } From 22a7c8efba4828a041b620dc92f1989564b96f1b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jesper=20Engstr=C3=B6m?= Date: Mon, 16 Sep 2024 15:03:40 +0200 Subject: [PATCH 08/10] Style fixes --- lxl-web/src/lib/components/find/FacetGroup.svelte | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/lxl-web/src/lib/components/find/FacetGroup.svelte b/lxl-web/src/lib/components/find/FacetGroup.svelte index 7cfc3297f..fe1731938 100644 --- a/lxl-web/src/lib/components/find/FacetGroup.svelte +++ b/lxl-web/src/lib/components/find/FacetGroup.svelte @@ -87,14 +87,17 @@ - + +
    @@ -181,10 +184,4 @@ @apply bg-pill/8; } } - - select { - @apply text-right; - /* Safari text-align fix */ - text-align-last: right; - } From 1a329f0502b1abf2292618c912e0b4478c261022 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jesper=20Engstr=C3=B6m?= Date: Mon, 16 Sep 2024 15:44:13 +0200 Subject: [PATCH 09/10] Fix failing test --- .../src/lib/components/find/FacetGroup.svelte | 36 +++++++++---------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/lxl-web/src/lib/components/find/FacetGroup.svelte b/lxl-web/src/lib/components/find/FacetGroup.svelte index fe1731938..ec9631058 100644 --- a/lxl-web/src/lib/components/find/FacetGroup.svelte +++ b/lxl-web/src/lib/components/find/FacetGroup.svelte @@ -73,7 +73,7 @@ class:hidden={searchPhrase && !hasHits} data-dimension={group.dimension} > -
    +
    {group.label} - - + +
    {#if group.search && !(searchPhrase && hasHits)} From 49f44264fee3a9f1c6a9ada9a6c4da6ea53b2788 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jesper=20Engstr=C3=B6m?= Date: Mon, 16 Sep 2024 16:59:49 +0200 Subject: [PATCH 10/10] Add tests --- lxl-web/tests/find.spec.ts | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/lxl-web/tests/find.spec.ts b/lxl-web/tests/find.spec.ts index 138f12b5a..ff96e7040 100644 --- a/lxl-web/tests/find.spec.ts +++ b/lxl-web/tests/find.spec.ts @@ -54,6 +54,27 @@ test('expanded filters have no detectable a11y issues', async ({ page }) => { expect.soft(accessibilityScanResults.violations).toEqual([]); }); +test('sorting the facet sets a cookie', async ({ page, context }) => { + const beforeCookies = await context.cookies(); + expect(beforeCookies).toEqual([]); + await page.getByTestId('facet-toggle').first().click(); + await page.getByTestId('facets').getByRole('combobox').selectOption('alpha.asc'); + const afterCookies = await context.cookies(); + expect(afterCookies[0].name).toEqual('userSettings'); + expect(afterCookies[0].value).toEqual('{%22facetSort%22:{%22rdf:type%22:%22alpha.asc%22}}'); +}); + +test('user sorting is persisted after navigating', async ({ page }) => { + await page.getByTestId('facet-toggle').first().click(); + const selectValue = await page.getByTestId('facets').getByRole('combobox').inputValue(); + expect(selectValue).toBe('hits.desc'); + await page.getByTestId('facets').getByRole('combobox').selectOption('alpha.asc'); + await page.goto('/find?_q=a&_limit=20&_offset=0&_sort=&_i=f'); + await page.getByTestId('facet-toggle').first().click(); + const newSelectValue = await page.getByTestId('facets').getByRole('combobox').inputValue(); + expect(newSelectValue).toBe('alpha.asc'); +}); + test('displays hits info', async ({ page }) => { await expect(page.getByTestId('result-info')).toBeVisible(); });