From b6118d8934edce8327d5dbf9135a78971449ce28 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jesper=20Engstr=C3=B6m?= Date: Mon, 9 Dec 2024 16:59:36 +0100 Subject: [PATCH 01/23] Update codemirror after navigate to apply filters --- .../src/lib/components/CodeMirror.svelte | 30 +++++++++++++------ 1 file changed, 21 insertions(+), 9 deletions(-) diff --git a/packages/supersearch/src/lib/components/CodeMirror.svelte b/packages/supersearch/src/lib/components/CodeMirror.svelte index abeff226e..93e06739e 100644 --- a/packages/supersearch/src/lib/components/CodeMirror.svelte +++ b/packages/supersearch/src/lib/components/CodeMirror.svelte @@ -7,7 +7,8 @@ -
- {#if env?.PUBLIC_USE_SUPERSEARCH === 'true'} + + {#if useSuperSearch} {#snippet resultItem(item)} {/if} + {#each searchParams as [name, value]} + {#if name !== '_q'} + + {/if} + {/each} diff --git a/lxl-web/src/lib/utils/getLabelsFromMapping.svelte.ts b/lxl-web/src/lib/utils/getLabelsFromMapping.svelte.ts new file mode 100644 index 000000000..cbd0b053a --- /dev/null +++ b/lxl-web/src/lib/utils/getLabelsFromMapping.svelte.ts @@ -0,0 +1,51 @@ +import type { DisplayMapping } from '$lib/types/search'; + +function getLabelFromMappings( + key: string, + value?: string, + pageMapping?: DisplayMapping[], + suggestMapping?: DisplayMapping[] +) { + const pageLabels = iterateMapping(key, value, pageMapping); + const suggestLabels = iterateMapping(key, value, suggestMapping); + + const keyLabel = suggestLabels.keyLabel || pageLabels.keyLabel; + const valueLabel = suggestLabels.valueLabel || pageLabels.valueLabel; + // only page data have 'up' links we can use + const removeLink = pageLabels.keyLabel ? pageLabels.removeLink : undefined; + + return { keyLabel, valueLabel, removeLink }; +} + +function iterateMapping( + key: string, + value: string | undefined, + mapping: DisplayMapping[] | undefined | null +) { + let keyLabel: string | undefined; + let valueLabel: string | undefined; + let removeLink: string | undefined; + + if (mapping && Array.isArray(mapping)) { + _iterate(mapping); + + function _iterate(m: DisplayMapping[]) { + m.forEach((el) => { + if (el.children) { + _iterate(el.children); + } else if (el.property === key) { + keyLabel = el.label; + const isLinked = !!el.display?.['@id']; + // only use atomic ranges for linked values + if (isLinked && el.displayStr) { + valueLabel = el.displayStr; + } + removeLink = el.up?.['@id']; + } + }); + } + } + return { keyLabel, valueLabel, removeLink }; +} + +export default getLabelFromMappings; From 523709b47dd8a0476020fb1a30b1942abd645d69 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jesper=20Engstr=C3=B6m?= Date: Mon, 9 Dec 2024 17:11:30 +0100 Subject: [PATCH 03/23] add supersearch package:watch --- packages/supersearch/package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/supersearch/package.json b/packages/supersearch/package.json index fb422c9f8..01b485ae5 100644 --- a/packages/supersearch/package.json +++ b/packages/supersearch/package.json @@ -14,7 +14,8 @@ "test:unit": "vitest", "lint": "eslint . && prettier --check .", "format": "prettier --write .", - "prepare": "npm run package" + "prepare": "npm run package", + "package:watch": "svelte-kit sync && svelte-package -w" }, "files": [ "dist", From f19ae0ddde2226deea305353da5887695ba012ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jesper=20Engstr=C3=B6m?= Date: Mon, 9 Dec 2024 17:22:16 +0100 Subject: [PATCH 04/23] Update response from server side with needed mapping stuff --- lxl-web/src/lib/types/search.ts | 2 ++ lxl-web/src/lib/utils/search.ts | 20 +++++++++++++------ .../api/[[lang=lang]]/supersearch/+server.ts | 6 ++++++ 3 files changed, 22 insertions(+), 6 deletions(-) diff --git a/lxl-web/src/lib/types/search.ts b/lxl-web/src/lib/types/search.ts index 13a82d877..1aeec90df 100644 --- a/lxl-web/src/lib/types/search.ts +++ b/lxl-web/src/lib/types/search.ts @@ -68,10 +68,12 @@ interface SpellingSuggestion { export interface DisplayMapping { '@id'?: string; display?: DisplayDecorated; + displayStr?: string; up?: Link; children?: DisplayMapping[]; label?: string; operator: keyof typeof SearchOperators; + property?: string; } export interface PartialCollectionView { diff --git a/lxl-web/src/lib/utils/search.ts b/lxl-web/src/lib/utils/search.ts index a31fa70ef..2925ba60e 100644 --- a/lxl-web/src/lib/utils/search.ts +++ b/lxl-web/src/lib/utils/search.ts @@ -75,12 +75,12 @@ export async function asResult( }; } -function displayMappings( +export function displayMappings( view: PartialCollectionView, displayUtil: DisplayUtil, locale: LangCode, translate: translateFn, - usePath: string + usePath?: string ): DisplayMapping[] { const mapping = view.search?.mapping || []; return _iterateMapping(mapping); @@ -94,11 +94,16 @@ function displayMappings( return { ...(isObject(m.property) && { '@id': m.property['@id'] }), display: displayUtil.lensAndFormat(property, LensType.Chip, locale), + displayStr: toString(displayUtil.lensAndFormat(property, LensType.Chip, locale)) || '', label: m.alias ? translate(`facet.${m.alias}`) : capitalize(m.property?.labelByLang?.[locale] || m.property?.label) || m.property?.['@id'] || 'No label', // lensandformat? + property: + m.property?.librisQueryCode || + m.property?.['@id'].replace('https://id.kb.se/vocab/', '') || + '', //TODO replace with something better operator, ...('up' in m && { up: replacePath(m.up as Link, usePath) }) } as DisplayMapping; @@ -238,10 +243,13 @@ function displayBoolFilters( /** * prevent links on resource page from pointing to /find */ -function replacePath(view: Link, usePath: string) { - return { - '@id': view['@id'].replace('/find', usePath) - }; +function replacePath(view: Link, usePath: string | undefined) { + if (usePath) { + return { + '@id': view['@id'].replace('/find', usePath) + }; + } + return view; } function capitalize(str: string | undefined) { diff --git a/lxl-web/src/routes/api/[[lang=lang]]/supersearch/+server.ts b/lxl-web/src/routes/api/[[lang=lang]]/supersearch/+server.ts index 2a156b87f..f40b6ea9e 100644 --- a/lxl-web/src/routes/api/[[lang=lang]]/supersearch/+server.ts +++ b/lxl-web/src/routes/api/[[lang=lang]]/supersearch/+server.ts @@ -4,6 +4,8 @@ import type { RequestHandler } from './$types.ts'; import { LxlLens } from '$lib/types/display'; import { getSupportedLocale } from '$lib/i18n/locales.js'; import { toString } from '$lib/utils/xl.js'; +import { displayMappings } from '$lib/utils/search.js'; +import { getTranslator } from '$lib/i18n/index.js'; import getEditedPartEntries from './getEditedPartEntries.js'; /** @@ -15,6 +17,7 @@ import getEditedPartEntries from './getEditedPartEntries.js'; export const GET: RequestHandler = async ({ url, params, locals }) => { const displayUtil = locals.display; const locale = getSupportedLocale(params?.lang); + const translate = await getTranslator(locale); const _q = url.searchParams.get('_q'); const cursor = parseInt(url.searchParams.get('cursor') || '0', 10); @@ -42,6 +45,9 @@ export const GET: RequestHandler = async ({ url, params, locals }) => { '@type': item['@type'], heading: toString(displayUtil.lensAndFormat(item, LxlLens.CardHeading, locale)) })), + ...(data?.search?.mapping && { + mapping: displayMappings(data, displayUtil, locale, translate) + }), totalItems: data.totalItems, '@context': data['@context'] }); From 2fc0734e815eabf38fe79053139065f2a3aff66f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jesper=20Engstr=C3=B6m?= Date: Mon, 9 Dec 2024 17:39:27 +0100 Subject: [PATCH 05/23] Refactor lxlQualifier extension, add qualifier component, styling --- lxl-web/src/lib/styles/lxlquery.css | 36 ++- .../QualifierComponent.svelte | 36 +++ .../lxlQualifierPlugin/QualifierKey.svelte | 21 -- .../lxlQualifierPlugin/QualifierRemove.svelte | 23 -- .../extensions/lxlQualifierPlugin/index.ts | 227 ++++++++++-------- 5 files changed, 189 insertions(+), 154 deletions(-) create mode 100644 packages/supersearch/src/lib/extensions/lxlQualifierPlugin/QualifierComponent.svelte delete mode 100644 packages/supersearch/src/lib/extensions/lxlQualifierPlugin/QualifierKey.svelte delete mode 100644 packages/supersearch/src/lib/extensions/lxlQualifierPlugin/QualifierRemove.svelte diff --git a/lxl-web/src/lib/styles/lxlquery.css b/lxl-web/src/lib/styles/lxlquery.css index 63c411d15..f5e41ad21 100644 --- a/lxl-web/src/lib/styles/lxlquery.css +++ b/lxl-web/src/lib/styles/lxlquery.css @@ -1,11 +1,39 @@ -.lxl-qualifier, +.lxl-qualifier-key, +.lxl-qualifier-value { + color: green; +} + +.lxl-qualifier-key.atomic { + padding: 3px 0 3px 5px; + border-top-left-radius: 5px; + border-top-left-radius: 5px; + border-bottom-left-radius: 5px; + background: rgba(14, 113, 128, 0.15); +} + +.lxl-qualifier-value.atomic { + padding: 3px 0 3px 5px; + background: rgba(14, 113, 128, 0.15); +} + +.lxl-qualifier-value.unlinked { + display: inline-flex; + padding: 3px 5px 3px 5px; + background: rgba(14, 113, 128, 0.05); + border-top-right-radius: 5px; + border-bottom-right-radius: 5px; +} + .lxl-qualifier-remove { background: rgba(14, 113, 128, 0.15); - padding: 2px 5px; + padding: 3px 5px 3px 10px; + border-top-right-radius: 5px; + border-bottom-right-radius: 5px; } -.lxl-qualifier-key { - color: green; +.lxl-qualifier-key.invalid { + text-decoration: underline 2px solid; + text-decoration-color: rgba(255, 0, 0, 0.326); } .lxl-boolean-query { diff --git a/packages/supersearch/src/lib/extensions/lxlQualifierPlugin/QualifierComponent.svelte b/packages/supersearch/src/lib/extensions/lxlQualifierPlugin/QualifierComponent.svelte new file mode 100644 index 000000000..ada1f8141 --- /dev/null +++ b/packages/supersearch/src/lib/extensions/lxlQualifierPlugin/QualifierComponent.svelte @@ -0,0 +1,36 @@ + + + + {keyLabel}{operator} + +{#if valueLabel} + + {valueLabel} + +{/if} +{#if valueLabel && removeLink} + + X + +{/if} + + diff --git a/packages/supersearch/src/lib/extensions/lxlQualifierPlugin/QualifierKey.svelte b/packages/supersearch/src/lib/extensions/lxlQualifierPlugin/QualifierKey.svelte deleted file mode 100644 index 702ae8233..000000000 --- a/packages/supersearch/src/lib/extensions/lxlQualifierPlugin/QualifierKey.svelte +++ /dev/null @@ -1,21 +0,0 @@ - - - - {label || key}{operator} - - - diff --git a/packages/supersearch/src/lib/extensions/lxlQualifierPlugin/QualifierRemove.svelte b/packages/supersearch/src/lib/extensions/lxlQualifierPlugin/QualifierRemove.svelte deleted file mode 100644 index f8fc5d39b..000000000 --- a/packages/supersearch/src/lib/extensions/lxlQualifierPlugin/QualifierRemove.svelte +++ /dev/null @@ -1,23 +0,0 @@ - - - - - - X - - diff --git a/packages/supersearch/src/lib/extensions/lxlQualifierPlugin/index.ts b/packages/supersearch/src/lib/extensions/lxlQualifierPlugin/index.ts index 7433e89ed..6c2daf700 100644 --- a/packages/supersearch/src/lib/extensions/lxlQualifierPlugin/index.ts +++ b/packages/supersearch/src/lib/extensions/lxlQualifierPlugin/index.ts @@ -9,8 +9,7 @@ import { import { EditorState, type Range } from '@codemirror/state'; import { syntaxTree } from '@codemirror/language'; import { mount } from 'svelte'; -import QualifierRemove from './QualifierRemove.svelte'; -import QualifierKey from './QualifierKey.svelte'; +import QualifierComponent from './QualifierComponent.svelte'; import insertQuotes from './insertQuotes.js'; export type Qualifier = { @@ -19,55 +18,51 @@ export type Qualifier = { operator: string; }; -class RemoveWidget extends WidgetType { - constructor( - readonly range: { from: number; to: number }, - readonly atomic: boolean - ) { - super(); - } - eq(other: RemoveWidget) { - return this.range.from === other.range.from && this.range.to === other.range.to; - } - // is there a way to pass new props to component instead of re-mounting every time range changes? - toDOM(): HTMLElement { - const container = document.createElement('span'); - container.style.cssText = `position: relative;`; - mount(QualifierRemove, { - props: { - range: this.range, - url: new URL(window?.location.href) - }, - target: container - }); - return container; - } -} +type GetLabelFunction = ( + key: string, + value?: string +) => { + keyLabel?: string; + valueLabel?: string; + removeLink?: string; +}; -class QualifierKeyWidget extends WidgetType { +class QualifierWidget extends WidgetType { constructor( readonly key: string, - readonly operator: string, - readonly label: string | undefined, + readonly keyLabel: string | undefined, readonly keyType: string | undefined, + readonly value: string | undefined, + readonly valueLabel: string | undefined, + readonly operator: string, readonly operatorType: string | undefined, + readonly removeLink: string | undefined, readonly atomic: boolean ) { super(); } - eq(other: QualifierKeyWidget): boolean { - return this.key === other.key && this.operator === other.operator; + eq(other: QualifierWidget): boolean { + return ( + this.key === other.key && + this.keyLabel === other.keyLabel && + this.operator === other.operator && + this.value === other.value && + this.valueLabel === other.valueLabel + ); } toDOM(): HTMLElement { const container = document.createElement('span'); - container.style.cssText = `position: relative;`; - mount(QualifierKey, { + container.style.cssText = `position: relative; display:inline-flex`; + mount(QualifierComponent, { props: { key: this.key, - operator: this.operator, - label: this.label, + keyLabel: this.keyLabel, keyType: this.keyType, - operatorType: this.operatorType + value: this.value, + valueLabel: this.valueLabel, + operator: this.operator, + operatorType: this.operatorType, + removeLink: this.removeLink }, target: container }); @@ -75,87 +70,107 @@ class QualifierKeyWidget extends WidgetType { } } -function getQualifiers(view: EditorView) { - const widgets: Range[] = []; - const doc = view.state.doc.toString(); +function lxlQualifierPlugin(getLabelFn?: GetLabelFunction) { + function getQualifiers(view: EditorView) { + const widgets: Range[] = []; + const doc = view.state.doc.toString(); + + for (const { from, to } of view.visibleRanges) { + syntaxTree(view.state).iterate({ + from, + to, + enter: (node) => { + if (node.name === 'Qualifier') { + const keyNode = node.node.getChild('QualifierKey'); + const key = keyNode ? doc.slice(keyNode?.from, keyNode?.to) : ''; + const keyType = keyNode?.firstChild?.type.name; - for (const { from, to } of view.visibleRanges) { - syntaxTree(view.state).iterate({ - from, - to, - enter: (node) => { - if (node.name === 'Qualifier') { - // Mark decoration to create wrapper element, non-atomic - const qualifierMark = Decoration.mark({ - class: 'lxl-qualifier', - inclusive: true, - atomic: false // invented! - }); - widgets.push(qualifierMark.range(node.from, node.to)); + const operatorNode = node.node.getChild('QualifierOperator'); + const operator = operatorNode ? doc.slice(operatorNode?.from, operatorNode?.to) : ''; + const operatorType = operatorNode?.firstChild?.type.name; - // Remove decoration (x-button) widget - atomic - const removeDecoration = Decoration.widget({ - widget: new RemoveWidget({ from: node.from, to: node.to }, true), - side: 1 - }); - widgets.push(removeDecoration.range(node.to)); + const valueNode = node.node.getChild('QualifierValue'); + const value = valueNode ? doc.slice(valueNode?.from, valueNode?.to) : undefined; - // Qualifier key + operator widget - atomic - const keyNode = node.node.getChild('QualifierKey'); - const operatorNode = node.node.getChild('QualifierOperator'); + const { keyLabel, valueLabel, removeLink } = getLabelFn?.(key, value) || {}; - if (keyNode && operatorNode) { - const keyDecoration = Decoration.replace({ - widget: new QualifierKeyWidget( - doc.slice(keyNode?.from, keyNode?.to), - doc.slice(operatorNode?.from, operatorNode?.to), - doc.slice(keyNode?.from, keyNode?.to), // label should be found using vocab - keyNode.firstChild?.type.name, - operatorNode.firstChild?.type.name, - true - ) - }); - widgets.push(keyDecoration.range(keyNode?.from, operatorNode?.to)); + // Add qualifier widget + if (keyLabel) { + const qualifierDecoration = Decoration.replace({ + widget: new QualifierWidget( + key, + keyLabel, + keyType, + value, + valueLabel, + operator, + operatorType, + removeLink, + true // atomic + ) + }); + const rangeFrom = node.from; + const rangeTo = valueLabel ? node.to : operatorNode?.to; + widgets.push(qualifierDecoration.range(rangeFrom, rangeTo)); + } else { + // Add invalid key mark decoration + const qualifierMark = Decoration.mark({ + class: 'lxl-qualifier-key invalid', + inclusive: true, + atomic: false + }); + widgets.push(qualifierMark.range(node.from, operatorNode?.to)); + } + + // add mark decoration for qualifier value if not included in widget + if (valueNode && value && !valueLabel) { + const qualifierMark = Decoration.mark({ + class: keyLabel ? 'lxl-qualifier-value unlinked' : 'lxl-qualifier-value', + inclusive: true, + atomic: false + }); + widgets.push(qualifierMark.range(valueNode.from, valueNode.to)); + } } } - } - }); + }); + } + return Decoration.set(widgets, true); // true = sort } - return Decoration.set(widgets, true); // true = sort -} - -/** - * filter out non-atomics using custom property 'atomic' - */ -const filterAtomic = (from: number, to: number, decoration: Decoration) => { - return decoration.spec?.atomic || decoration.spec?.widget?.atomic; -}; + /** + * filter out non-atomics using custom property 'atomic' + */ + const filterAtomic = (from: number, to: number, decoration: Decoration) => { + return decoration.spec?.atomic || decoration.spec?.widget?.atomic; + }; -export const lxlQualifierPlugin = ViewPlugin.fromClass( - class { - qualifiers: DecorationSet; - constructor(view: EditorView) { - this.qualifiers = getQualifiers(view); - } + const qualifierPlugin = ViewPlugin.fromClass( + class { + qualifiers: DecorationSet; + constructor(view: EditorView) { + this.qualifiers = getQualifiers(view); + } - update(update: ViewUpdate) { - if (update.docChanged || syntaxTree(update.startState) != syntaxTree(update.state)) { - this.qualifiers = getQualifiers(update.view); + update(update: ViewUpdate) { + if (update.docChanged || syntaxTree(update.startState) != syntaxTree(update.state)) { + this.qualifiers = getQualifiers(update.view); + } } + }, + { + decorations: (instance) => instance.qualifiers, + eventHandlers: {}, + provide: (plugin) => [ + EditorView.atomicRanges.of((view) => { + const filteredRanges = view.plugin(plugin)?.qualifiers.update({ filter: filterAtomic }); + return filteredRanges || Decoration.none; + }), + EditorState.transactionFilter.of(insertQuotes) + ] } - }, - { - decorations: (instance) => instance.qualifiers, - eventHandlers: {}, - provide: (plugin) => [ - EditorView.atomicRanges.of((view) => { - const filteredRanges = view.plugin(plugin)?.qualifiers.update({ filter: filterAtomic }); - return filteredRanges || Decoration.none; - }), - EditorState.transactionFilter.of(insertQuotes) - ] - } -); + ); + return qualifierPlugin; +} export default lxlQualifierPlugin; From c5e16919abef37095fb2800bdf6245f46bec7be0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jesper=20Engstr=C3=B6m?= Date: Mon, 9 Dec 2024 17:40:10 +0100 Subject: [PATCH 06/23] Make supersearch routes not crash --- packages/supersearch/src/routes/+page.svelte | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/supersearch/src/routes/+page.svelte b/packages/supersearch/src/routes/+page.svelte index 62437b6c0..46e6f4b87 100644 --- a/packages/supersearch/src/routes/+page.svelte +++ b/packages/supersearch/src/routes/+page.svelte @@ -48,7 +48,7 @@ paginationQueryFn={handlePaginationQuery} transformFn={handleTransform} language={lxlQuery} - extensions={[lxlQualifierPlugin]} + extensions={[lxlQualifierPlugin()]} > {#snippet resultItem(item)} + + {#if $page.url.searchParams.get('_x') === 'advanced'} + + + {/if} {/if} + {#each searchParams as [name, value]} {#if name !== '_q'} diff --git a/lxl-web/tests/index.spec.ts b/lxl-web/tests/index.spec.ts index e0299be07..a821fa6b0 100644 --- a/lxl-web/tests/index.spec.ts +++ b/lxl-web/tests/index.spec.ts @@ -35,6 +35,6 @@ test('url is populated with correct searchparams', async ({ page }) => { await page.getByTestId('main-search').fill('somephrase'); await page.getByTestId('main-search').press('Enter'); await expect(page).toHaveURL( - /_q=somephrase&_limit=20&_offset=0&_sort=&_spell=true&_i=somephrase/ + /_q=somephrase&_i=somephrase&_limit=20&_offset=0&_sort=&_spell=true/ ); }); From 9f96eef1264362bb31f05b38c09836e2fa199b2a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jesper=20Engstr=C3=B6m?= Date: Wed, 11 Dec 2024 10:31:41 +0100 Subject: [PATCH 09/23] Accomplish url sync with $effect, not afterNavigate --- .../src/lib/components/CodeMirror.svelte | 18 ++++++------------ .../src/lib/components/SuperSearch.svelte | 2 +- 2 files changed, 7 insertions(+), 13 deletions(-) diff --git a/packages/supersearch/src/lib/components/CodeMirror.svelte b/packages/supersearch/src/lib/components/CodeMirror.svelte index 93e06739e..5d15030e7 100644 --- a/packages/supersearch/src/lib/components/CodeMirror.svelte +++ b/packages/supersearch/src/lib/components/CodeMirror.svelte @@ -7,8 +7,7 @@ - - {keyLabel}{operator} + + {keyLabel} + + + {operator} {#if valueLabel} - + {valueLabel} {/if} {#if valueLabel && removeLink} - + X {/if} - - diff --git a/packages/supersearch/src/lib/extensions/lxlQualifierPlugin/index.ts b/packages/supersearch/src/lib/extensions/lxlQualifierPlugin/index.ts index b56ecc9c6..e74303981 100644 --- a/packages/supersearch/src/lib/extensions/lxlQualifierPlugin/index.ts +++ b/packages/supersearch/src/lib/extensions/lxlQualifierPlugin/index.ts @@ -52,7 +52,7 @@ class QualifierWidget extends WidgetType { } toDOM(): HTMLElement { const container = document.createElement('span'); - container.style.cssText = `position: relative; display:inline-flex`; + container.style.cssText = `position: relative; display:inline-table`; mount(QualifierComponent, { props: { key: this.key, @@ -109,27 +109,20 @@ function lxlQualifierPlugin(getLabelFn?: GetLabelFunction) { true // atomic ) }); - const rangeFrom = node.from; - const rangeTo = valueLabel ? node.to : operatorNode?.to; - widgets.push(qualifierDecoration.range(rangeFrom, rangeTo)); + const decorationRangeFrom = node.from; + const decorationRangeTo = valueLabel ? node.to : operatorNode?.to; + widgets.push(qualifierDecoration.range(decorationRangeFrom, decorationRangeTo)); } else { // Add invalid key mark decoration const qualifierMark = Decoration.mark({ - class: 'lxl-qualifier-key invalid', + class: 'invalid', inclusive: true, atomic: false }); - widgets.push(qualifierMark.range(node.from, operatorNode?.to)); - } + const invalidRangeFrom = keyNode ? keyNode.from : node.from; + const invalidRangeTo = keyNode ? keyNode.to : operatorNode?.from; - // add mark decoration for qualifier value if not included in widget - if (valueNode && value && !valueLabel) { - const qualifierMark = Decoration.mark({ - class: keyLabel ? 'lxl-qualifier-value unlinked' : 'lxl-qualifier-value', - inclusive: true, - atomic: false - }); - widgets.push(qualifierMark.range(valueNode.from, valueNode.to)); + widgets.push(qualifierMark.range(invalidRangeFrom, invalidRangeTo)); } } } @@ -154,7 +147,9 @@ function lxlQualifierPlugin(getLabelFn?: GetLabelFunction) { update(update: ViewUpdate) { if (update.docChanged || syntaxTree(update.startState) != syntaxTree(update.state)) { - this.qualifiers = getQualifiers(update.view); + // let's see how it works to run getQualifiers only on new data and not on input + // should be much better for performande... + // this.qualifiers = getQualifiers(update.view); } else { for (const tr of update.transactions) { for (const e of tr.effects) { From 56f6fafd1015609361fbf5ea1e6749a8d715cf07 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jesper=20Engstr=C3=B6m?= Date: Thu, 12 Dec 2024 23:30:57 +0100 Subject: [PATCH 13/23] CSS fix --- lxl-web/src/lib/styles/lxlquery.css | 8 ++++++-- .../src/lib/extensions/lxlQualifierPlugin/index.ts | 2 +- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/lxl-web/src/lib/styles/lxlquery.css b/lxl-web/src/lib/styles/lxlquery.css index c1d46ebcd..7bd9f0a6e 100644 --- a/lxl-web/src/lib/styles/lxlquery.css +++ b/lxl-web/src/lib/styles/lxlquery.css @@ -1,5 +1,5 @@ .lxl-qualifier { - display: inline; + display: inline-flex; color: rgb(0, 128, 0); background: rgba(14, 113, 128, 0.1); padding-top: 3px; @@ -14,12 +14,16 @@ .lxl-qualifier-value, .lxl-qualifier-remove { - padding-left: 5px; padding-right: 5px; border-top-right-radius: 5px; border-bottom-right-radius: 5px; } +.lxl-qualifier-remove, +.lxl-qualifier-value.atomic { + padding-left: 5px; +} + .lxl-qualifier-value:has(~ .lxl-qualifier-remove) { border-radius: 0; } diff --git a/packages/supersearch/src/lib/extensions/lxlQualifierPlugin/index.ts b/packages/supersearch/src/lib/extensions/lxlQualifierPlugin/index.ts index e74303981..8bd1bd3a0 100644 --- a/packages/supersearch/src/lib/extensions/lxlQualifierPlugin/index.ts +++ b/packages/supersearch/src/lib/extensions/lxlQualifierPlugin/index.ts @@ -52,7 +52,7 @@ class QualifierWidget extends WidgetType { } toDOM(): HTMLElement { const container = document.createElement('span'); - container.style.cssText = `position: relative; display:inline-table`; + container.style.cssText = `position: relative; display:inline-flex`; mount(QualifierComponent, { props: { key: this.key, From 28a5d0705025876c3fee257cfbb5b91c04831fdf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jesper=20Engstr=C3=B6m?= Date: Thu, 12 Dec 2024 23:39:55 +0100 Subject: [PATCH 14/23] Invalid underline fix --- lxl-web/src/lib/styles/lxlquery.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lxl-web/src/lib/styles/lxlquery.css b/lxl-web/src/lib/styles/lxlquery.css index 7bd9f0a6e..f8f57f0cc 100644 --- a/lxl-web/src/lib/styles/lxlquery.css +++ b/lxl-web/src/lib/styles/lxlquery.css @@ -28,7 +28,7 @@ border-radius: 0; } -.invalid { +.invalid > .lxl-qualifier-key { text-decoration: underline 2px solid; text-decoration-color: rgba(255, 0, 0, 0.326); } From 62a638041a799a0800bec6cf22b8f5c91077ac67 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jesper=20Engstr=C3=B6m?= Date: Tue, 17 Dec 2024 12:00:55 +0100 Subject: [PATCH 15/23] Update readme --- packages/supersearch/README.md | 6 ++++++ .../src/lib/extensions/lxlQualifierPlugin/index.ts | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/supersearch/README.md b/packages/supersearch/README.md index c73225f42..4a5e81ea1 100644 --- a/packages/supersearch/README.md +++ b/packages/supersearch/README.md @@ -31,6 +31,12 @@ To use `supersearch` in a non-Svelte project ... | `resultItem` | `Snippet<[ResultItem]>` | A [Snippet](https://svelte.dev/docs/svelte/snippet) used for customized rendering of result items. | `undefined` | | `debouncedWait` | `number` | The wait time, in milliseconds that debounce function should wait between invocated search queries. | `300` | +  +Supersearch also exports a `lxlQualifierPlugin` that can be used (passed to the extensions prop) if you want atomic, stylable, removable, labeled pills from some key-value pairs in your editor. This requires: + +- Your language exporting `Qualifier` nodes consisting of `QualifierKey`, `QualifierOperator` and `QualifierValue` (i.e `key:value`). +- Your pass a function of type `GetLabelFunction`, returning labels to be displayed and an optional remove link. + ## Developing Install dependencies with `npm install` and start a development server: diff --git a/packages/supersearch/src/lib/extensions/lxlQualifierPlugin/index.ts b/packages/supersearch/src/lib/extensions/lxlQualifierPlugin/index.ts index 8bd1bd3a0..704e69e46 100644 --- a/packages/supersearch/src/lib/extensions/lxlQualifierPlugin/index.ts +++ b/packages/supersearch/src/lib/extensions/lxlQualifierPlugin/index.ts @@ -18,7 +18,7 @@ export type Qualifier = { operator: string; }; -type GetLabelFunction = ( +export type GetLabelFunction = ( key: string, value?: string ) => { From d1b8c456b0a0396b189cdf5aa15a0d245f7eeeac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jesper=20Engstr=C3=B6m?= Date: Tue, 17 Dec 2024 14:05:30 +0100 Subject: [PATCH 16/23] Add tests --- .../lib/utils/getLabelsFromMapping.svelte.ts | 11 +- .../lib/utils/getLabelsFromMapping.test.ts | 192 ++++++++++++++++++ 2 files changed, 199 insertions(+), 4 deletions(-) create mode 100644 lxl-web/src/lib/utils/getLabelsFromMapping.test.ts diff --git a/lxl-web/src/lib/utils/getLabelsFromMapping.svelte.ts b/lxl-web/src/lib/utils/getLabelsFromMapping.svelte.ts index 19b93e038..d4d9f4d85 100644 --- a/lxl-web/src/lib/utils/getLabelsFromMapping.svelte.ts +++ b/lxl-web/src/lib/utils/getLabelsFromMapping.svelte.ts @@ -44,11 +44,14 @@ function iterateMapping( } else if (el.property === key) { keyLabel = el.label; const isLinked = !!el.display?.['@id']; - // only use atomic ranges for linked values - if (isLinked && el.displayStr) { - valueLabel = el.displayStr; + if (isLinked) { + if (el.displayStr) { + // only use atomic ranges for linked values + valueLabel = el.displayStr; + } + // only show remove btn for pills that can't be edited + removeLink = el.up?.['@id']; } - removeLink = el.up?.['@id']; } }); } diff --git a/lxl-web/src/lib/utils/getLabelsFromMapping.test.ts b/lxl-web/src/lib/utils/getLabelsFromMapping.test.ts new file mode 100644 index 000000000..b9da44750 --- /dev/null +++ b/lxl-web/src/lib/utils/getLabelsFromMapping.test.ts @@ -0,0 +1,192 @@ +import { describe, it, expect } from 'vitest'; +import getLabelsFromMapping from './getLabelsFromMapping.svelte'; +import type { DisplayMapping } from '$lib/types/search'; + +describe('getLabelsFromMapping', () => { + it('it returns labels when supplied with page mappings', () => { + const labels = getLabelsFromMapping('genreForm', 'saogf:Romaner', pageMapping, null); + expect(labels.keyLabel).toBe('Genre/form'); + expect(labels.valueLabel).toBe('Romaner'); + }); + + it('it returns labels when supplied with suggest mappings', () => { + const labels = getLabelsFromMapping('genreForm', 'saogf:Romaner', null, suggestMapping); + expect(labels.keyLabel).toBe('Genre/form'); + expect(labels.valueLabel).toBe('Romaner'); + }); + + it('it returns suggest labels when supplied with both mappings', () => { + const labels = getLabelsFromMapping('SPRÅK', 'lang:swe', pageMapping, suggestMapping); + expect(labels.keyLabel).toBe('Språk'); + expect(labels.valueLabel).toBe('Svenska'); + }); + + it('it returns undefined values when no correct key match', () => { + const labels = getLabelsFromMapping('invalid', 'saogf:Romaner', pageMapping, suggestMapping); + expect(labels.keyLabel).toBe(undefined); + expect(labels.valueLabel).toBe(undefined); + }); + + // TODO run test when value matching is done properly + // it('it returns no value label when value does not match', () => { + // const labels = getLabelsFromMapping('genreForm', 'saogf:SomeInvalidTerm', pageMapping, null); + // expect(labels.keyLabel).toBe('Genre/form'); + // expect(labels.valueLabel).toBe(undefined); + // }); + + it('does not return value labels for unlinked items', () => { + const labels = getLabelsFromMapping('ÅR', '2023', pageMapping, suggestMapping); + expect(labels.keyLabel).toBe('Utgivningsår'); + expect(labels.valueLabel).toBe(undefined); + }); + + it('does not return a removelink for unlinked items', () => { + const labels = getLabelsFromMapping('ÅR', '2023', pageMapping, suggestMapping); + expect(labels.removeLink).toBe(undefined); + }); + + it('does not return a removelink when using suggest mappings', () => { + const labels = getLabelsFromMapping('genreForm', 'saogf:Romaner', null, suggestMapping); + expect(labels.removeLink).toBe(undefined); + }); + + it('returns a removelink when using page mappings', () => { + const labels = getLabelsFromMapping('genreForm', 'saogf:Romaner', pageMapping, null); + expect(labels.removeLink).toBe('/find?_i=sommar&_q=sommar+%C3%85R:2023&_limit=20&_spell=true'); + }); +}); + +// sommar genreForm:"saogf:Romaner" ÅR:2023 +const pageMapping: DisplayMapping[] = [ + { + children: [ + { + '@id': 'https://id.kb.se/vocab/textQuery', + display: 'sommar', + displayStr: 'sommar', + label: 'Fritextsökning', + property: 'textQuery', + operator: 'equals', + up: { + '@id': '/find?_i=&_q=genreForm:%22saogf:Romaner%22+%C3%85R:2023&_limit=20&_spell=true' + } + }, + { + '@id': 'https://id.kb.se/vocab/genreForm', + display: { + '@id': 'https://id.kb.se/term/saogf/Romaner', + '@type': 'GenreForm', + _display: [ + { + prefLabel: 'Romaner', + _label: 'föredragen benämning' + } + ], + _style: ['link', 'pill'], + _label: 'Genre/form' + }, + displayStr: 'Romaner', + label: 'Genre/form', + property: 'genreForm', + operator: 'equals', + up: { + '@id': '/find?_i=sommar&_q=sommar+%C3%85R:2023&_limit=20&_spell=true' + } + }, + { + '@id': 'https://id.kb.se/vocab/yearPublished', + display: '2023', + displayStr: '2023', + label: 'Utgivningsår', + property: 'ÅR', + operator: 'equals', + up: { + '@id': '/find?_i=sommar&_q=sommar+genreForm:%22saogf:Romaner%22&_limit=20&_spell=true' + } + } + ], + operator: 'and', + up: { + '@id': '/find?_i=&_q=*&_limit=20&_spell=true' + } + } +]; + +// sommar genreForm:"saogf:Romaner" ÅR:2023 SPRÅK:"lang:swe" +const suggestMapping: DisplayMapping[] = [ + { + children: [ + { + '@id': 'https://id.kb.se/vocab/textQuery', + display: 'sommar', + displayStr: 'sommar', + label: 'Fritextsökning', + property: 'textQuery', + operator: 'equals', + up: { + '@id': + '/find?_i=&_q=genreForm:%22saogf:Romaner%22+%C3%85R:2023+SPR%C3%85K:%22lang:swe%22&_limit=10' + } + }, + { + '@id': 'https://id.kb.se/vocab/genreForm', + display: { + '@id': 'https://id.kb.se/term/saogf/Romaner', + '@type': 'GenreForm', + _display: [ + { + prefLabel: 'Romaner', + _label: 'föredragen benämning' + } + ], + _style: ['link', 'pill'], + _label: 'Genre/form' + }, + displayStr: 'Romaner', + label: 'Genre/form', + property: 'genreForm', + operator: 'equals', + up: { + '@id': '/find?_i=sommar&_q=sommar+%C3%85R:2023+SPR%C3%85K:%22lang:swe%22&_limit=10' + } + }, + { + '@id': 'https://id.kb.se/vocab/yearPublished', + display: '2023', + displayStr: '2023', + label: 'Utgivningsår', + property: 'ÅR', + operator: 'equals', + up: { + '@id': + '/find?_i=sommar&_q=sommar+genreForm:%22saogf:Romaner%22+SPR%C3%85K:%22lang:swe%22&_limit=10' + } + }, + { + '@id': 'https://id.kb.se/vocab/language', + display: { + '@id': 'https://id.kb.se/language/swe', + '@type': 'Language', + _display: [ + { + prefLabel: 'Svenska', + _label: 'föredragen benämning' + } + ], + _label: 'Språk' + }, + displayStr: 'Svenska', + label: 'Språk', + property: 'SPRÅK', + operator: 'equals', + up: { + '@id': '/find?_i=sommar&_q=sommar+genreForm:%22saogf:Romaner%22+%C3%85R:2023&_limit=10' + } + } + ], + operator: 'and', + up: { + '@id': '/find?_i=&_q=*&_limit=10' + } + } +]; From 506953c2b159fc11aad31dcd6d49b0bf1871987a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jesper=20Engstr=C3=B6m?= Date: Tue, 17 Dec 2024 14:07:05 +0100 Subject: [PATCH 17/23] Test fix --- lxl-web/src/lib/utils/getLabelsFromMapping.test.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lxl-web/src/lib/utils/getLabelsFromMapping.test.ts b/lxl-web/src/lib/utils/getLabelsFromMapping.test.ts index b9da44750..6f04d6929 100644 --- a/lxl-web/src/lib/utils/getLabelsFromMapping.test.ts +++ b/lxl-web/src/lib/utils/getLabelsFromMapping.test.ts @@ -4,13 +4,13 @@ import type { DisplayMapping } from '$lib/types/search'; describe('getLabelsFromMapping', () => { it('it returns labels when supplied with page mappings', () => { - const labels = getLabelsFromMapping('genreForm', 'saogf:Romaner', pageMapping, null); + const labels = getLabelsFromMapping('genreForm', 'saogf:Romaner', pageMapping, undefined); expect(labels.keyLabel).toBe('Genre/form'); expect(labels.valueLabel).toBe('Romaner'); }); it('it returns labels when supplied with suggest mappings', () => { - const labels = getLabelsFromMapping('genreForm', 'saogf:Romaner', null, suggestMapping); + const labels = getLabelsFromMapping('genreForm', 'saogf:Romaner', undefined, suggestMapping); expect(labels.keyLabel).toBe('Genre/form'); expect(labels.valueLabel).toBe('Romaner'); }); @@ -46,12 +46,12 @@ describe('getLabelsFromMapping', () => { }); it('does not return a removelink when using suggest mappings', () => { - const labels = getLabelsFromMapping('genreForm', 'saogf:Romaner', null, suggestMapping); + const labels = getLabelsFromMapping('genreForm', 'saogf:Romaner', undefined, suggestMapping); expect(labels.removeLink).toBe(undefined); }); it('returns a removelink when using page mappings', () => { - const labels = getLabelsFromMapping('genreForm', 'saogf:Romaner', pageMapping, null); + const labels = getLabelsFromMapping('genreForm', 'saogf:Romaner', pageMapping, undefined); expect(labels.removeLink).toBe('/find?_i=sommar&_q=sommar+%C3%85R:2023&_limit=20&_spell=true'); }); }); From 7f28c08131dc4f5c9954b79192cc2539a212acf0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jesper=20Engstr=C3=B6m?= Date: Tue, 17 Dec 2024 14:17:52 +0100 Subject: [PATCH 18/23] Remove package:watch (add in another PR) --- packages/supersearch/package.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/supersearch/package.json b/packages/supersearch/package.json index 01b485ae5..fb422c9f8 100644 --- a/packages/supersearch/package.json +++ b/packages/supersearch/package.json @@ -14,8 +14,7 @@ "test:unit": "vitest", "lint": "eslint . && prettier --check .", "format": "prettier --write .", - "prepare": "npm run package", - "package:watch": "svelte-kit sync && svelte-package -w" + "prepare": "npm run package" }, "files": [ "dist", From 15b1e43fb52fa5595bb29fb15093b15036df5b82 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jesper=20Engstr=C3=B6m?= Date: Tue, 17 Dec 2024 14:22:31 +0100 Subject: [PATCH 19/23] Remove comment --- lxl-web/src/lib/components/Search.svelte | 3 --- 1 file changed, 3 deletions(-) diff --git a/lxl-web/src/lib/components/Search.svelte b/lxl-web/src/lib/components/Search.svelte index ea73556c3..65402e46b 100644 --- a/lxl-web/src/lib/components/Search.svelte +++ b/lxl-web/src/lib/components/Search.svelte @@ -68,9 +68,6 @@ } function handleTransform(data) { - // hijacking this function to set get autosuggest labels - // to not hardcode lxl-needs into useSearchRequest - // could also be new separate callback suggestMapping = data?.mapping; return data; } From 061012a3025bf1fddf5b1e82ffd17cf970d8c10f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jesper=20Engstr=C3=B6m?= Date: Tue, 17 Dec 2024 15:11:08 +0100 Subject: [PATCH 20/23] Export messages constant --- packages/codemirror-lang-lxlquery/src/index.ts | 1 - .../src/lib/components/SuperSearch.svelte | 17 +++++++++-------- .../supersearch/src/lib/constants/messages.ts | 3 +++ .../lib/extensions/lxlQualifierPlugin/index.ts | 3 ++- 4 files changed, 14 insertions(+), 10 deletions(-) create mode 100644 packages/supersearch/src/lib/constants/messages.ts diff --git a/packages/codemirror-lang-lxlquery/src/index.ts b/packages/codemirror-lang-lxlquery/src/index.ts index c388cdc07..1312975b3 100644 --- a/packages/codemirror-lang-lxlquery/src/index.ts +++ b/packages/codemirror-lang-lxlquery/src/index.ts @@ -34,7 +34,6 @@ export const lxlQueryLanguage = LRLanguage.define({ }); const highlighter = tagHighlighter([ - // adding qualifier classes handled by sypersearch/lxlQualifier plugin { tag: tags.BooleanQuery, class: 'lxl-boolean-query' }, { tag: tags.Wildcard, class: 'lxl-wildcard' }, { tag: tags.Qualifier, class: 'lxl-qualifier' }, diff --git a/packages/supersearch/src/lib/components/SuperSearch.svelte b/packages/supersearch/src/lib/components/SuperSearch.svelte index 40fa2a759..95029d9da 100644 --- a/packages/supersearch/src/lib/components/SuperSearch.svelte +++ b/packages/supersearch/src/lib/components/SuperSearch.svelte @@ -7,6 +7,7 @@ import submitFormOnEnterKey from '$lib/extensions/submitFormOnEnterKey.js'; import preventNewLine from '$lib/extensions/preventNewLine.js'; import useSearchRequest from '$lib/utils/useSearchRequest.svelte.js'; + import { messages } from '$lib/constants/messages.js'; import type { QueryFunction, PaginationQueryFunction, @@ -58,19 +59,19 @@ transformFn }); + const sendMessage = StateEffect.define<{ message: string }>({}); + const newDataMessage = { effects: sendMessage.of({ message: messages.NEW_DATA }) }; + $effect(() => { - if (value && value.trim()) { - search.debouncedFetchData(value, cursor); + if (search.data) { + expandedEditorView?.dispatch(newDataMessage); + collapsedEditorView?.dispatch(newDataMessage); } }); - const searchStatus = StateEffect.define<{ message: string }>({}); - $effect(() => { - if (search.data) { - const effects = { effects: searchStatus.of({ message: 'new_data' }) }; - expandedEditorView?.dispatch(effects); - collapsedEditorView?.dispatch(effects); + if (value && value.trim()) { + search.debouncedFetchData(value, cursor); } }); diff --git a/packages/supersearch/src/lib/constants/messages.ts b/packages/supersearch/src/lib/constants/messages.ts new file mode 100644 index 000000000..49366458f --- /dev/null +++ b/packages/supersearch/src/lib/constants/messages.ts @@ -0,0 +1,3 @@ +export const messages = { + NEW_DATA: 'new_response_data' +}; diff --git a/packages/supersearch/src/lib/extensions/lxlQualifierPlugin/index.ts b/packages/supersearch/src/lib/extensions/lxlQualifierPlugin/index.ts index 704e69e46..1e15d147d 100644 --- a/packages/supersearch/src/lib/extensions/lxlQualifierPlugin/index.ts +++ b/packages/supersearch/src/lib/extensions/lxlQualifierPlugin/index.ts @@ -11,6 +11,7 @@ import { syntaxTree } from '@codemirror/language'; import { mount } from 'svelte'; import QualifierComponent from './QualifierComponent.svelte'; import insertQuotes from './insertQuotes.js'; +import { messages } from '$lib/constants/messages.js'; export type Qualifier = { key: string; @@ -153,7 +154,7 @@ function lxlQualifierPlugin(getLabelFn?: GetLabelFunction) { } else { for (const tr of update.transactions) { for (const e of tr.effects) { - if (e.value.message === 'new_data') { + if (e.value.message === messages.NEW_DATA) { this.qualifiers = getQualifiers(update.view); } } From aecdeb1c79000cbef4024ef4dc9e5ec6c7b6d696 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jesper=20Engstr=C3=B6m?= Date: Tue, 17 Dec 2024 15:49:50 +0100 Subject: [PATCH 21/23] Derive lxlQualifier plugin --- lxl-web/src/lib/components/Search.svelte | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/lxl-web/src/lib/components/Search.svelte b/lxl-web/src/lib/components/Search.svelte index 65402e46b..1d71cc7d8 100644 --- a/lxl-web/src/lib/components/Search.svelte +++ b/lxl-web/src/lib/components/Search.svelte @@ -72,10 +72,13 @@ return data; } - function getLabels(key: string, value?: string) { - let pageMapping = $page.data.searchResult?.mapping; - return getLabelFromMappings(key, value, pageMapping, suggestMapping); - } + let derivedLxlQualifierPlugin = $derived.by(() => { + function getLabels(key: string, value?: string) { + let pageMapping = $page.data.searchResult?.mapping; + return getLabelFromMappings(key, value, pageMapping, suggestMapping); + } + return lxlQualifierPlugin(getLabels); + });
@@ -95,7 +98,7 @@ }} transformFn={handleTransform} paginationQueryFn={handlePaginationQuery} - extensions={[lxlQualifierPlugin(getLabels)]} + extensions={[derivedLxlQualifierPlugin]} > {#snippet resultItem(item)}