From 4b00fa398caf8fb04f20c33f8f5bb8f1d27e3ad1 Mon Sep 17 00:00:00 2001 From: Ivo Velitchkov Date: Fri, 11 Sep 2026 16:44:59 +0200 Subject: [PATCH] Add candidate-element search filtering MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This change adds `elementIris` support to lookup params and `SearchCriteria`, allowing entity searches to be restricted to a specific set of candidate elements. `SparqlDataProvider` applies this as a `VALUES` restriction and `RdfDataProvider` supports the same filtering semantics. The UI renders this as a removable “Among N given entities” criterion. It also adds `renderCriteriaActions` to `InstancesSearch` and `SearchSectionEntities` so custom actions can be displayed next to the search criteria. The SPARQL example now includes a dialog that runs a `SELECT` query against the connected endpoint and uses the first projected variable as the candidate set. Translation keys, schema entries, and a provider test were updated to cover the new behavior. --- CHANGELOG.md | 4 + examples/resources/sparqlFilter.tsx | 187 ++++++++++++++++++ examples/sparql.tsx | 89 +++++++-- i18n/i18n.schema.json | 1 + .../en.reactodia-translation.json | 1 + src/data/dataProvider.ts | 8 + src/data/rdf/rdfDataProvider.ts | 16 ++ src/data/sparql/sparqlDataProvider.ts | 13 +- src/widgets/instancesSearch.tsx | 62 +++++- .../unifiedSearch/builtinSearchSections.tsx | 13 +- src/workspace.ts | 1 + test/data/sparql/sparqlProviderBasic.test.ts | 17 ++ 12 files changed, 385 insertions(+), 27 deletions(-) create mode 100644 examples/resources/sparqlFilter.tsx diff --git a/CHANGELOG.md b/CHANGELOG.md index 5510435d..325dc121 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,10 @@ All notable changes to the Reactodia will be documented in this document. The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/). ## [Unreleased] +#### 🚀 New Features +- Add `elementIris` lookup parameter to `DataProvider.lookup()` and `SearchCriteria` to restrict the entity search to a given set of candidate elements (`VALUES` restriction in `SparqlDataProvider`, supported by `RdfDataProvider` as well), shown as a removable "Among N given entities" criterion. +- Add `renderCriteriaActions` prop to `InstancesSearch` and `SearchSectionEntities` to render custom content next to the search criteria with access to the current criteria and a way to set them. +- SPARQL example: "Filter by SPARQL query…" button in the entity search which runs a `SELECT` query on the connected endpoint and restricts the results to the entities bound by its first projected variable, in addition to the other criteria (e.g. the selected entity type). ## [0.35.2] - 2026-08-08 #### 🐛 Fixed diff --git a/examples/resources/sparqlFilter.tsx b/examples/resources/sparqlFilter.tsx new file mode 100644 index 00000000..c7625255 --- /dev/null +++ b/examples/resources/sparqlFilter.tsx @@ -0,0 +1,187 @@ +import * as React from 'react'; +import * as Reactodia from '../../src/workspace'; + +const LAST_QUERY_SESSION_KEY = 'reactodia-sparql-filter-query'; +/** + * Upper bound for the candidate set: the IRIs are inlined into the lookup + * query as a `VALUES` block, which is sent as a GET request. + */ +const MAX_CANDIDATES = 500; + +/** + * Button for {@link Reactodia.InstancesSearchProps.renderCriteriaActions} which + * opens a dialog to restrict the entity search results to the bindings + * of a SPARQL `SELECT` query. + */ +export function SparqlFilterAction(props: { + context: Reactodia.CriteriaActionsContext; +}) { + const {context} = props; + const workspace = Reactodia.useWorkspace(); + return ( + + ); +} + +export function showSparqlFilterDialog( + context: Reactodia.CriteriaActionsContext, + workspace: Reactodia.WorkspaceContext +): void { + const {overlay} = workspace; + overlay.showDialog({ + style: { + caption: 'Filter by SPARQL query', + defaultSize: {width: 600, height: 500}, + resizableBy: 'all', + }, + content: ( + { + overlay.hideDialog(); + context.setCriteria({...context.criteria, elementIris}); + }} + /> + ), + }); +} + +export function SparqlFilterForm(props: { + onApply: (elementIris: ReadonlyArray) => void; +}) { + const {onApply} = props; + const {model} = Reactodia.useWorkspace(); + const [query, setQuery] = React.useState(loadLastQuery); + const [querying, setQuerying] = React.useState(false); + const [error, setError] = React.useState(); + const cancellation = React.useRef(); + React.useEffect(() => () => cancellation.current?.abort(), []); + + const canSubmit = query.trim().length > 0 && !querying; + const submit = async () => { + const {dataProvider} = model; + if (!(dataProvider instanceof Reactodia.SparqlDataProvider)) { + setError('The workspace is not connected to a SPARQL endpoint.'); + return; + } + cancellation.current?.abort(); + const controller = new AbortController(); + cancellation.current = controller; + setQuerying(true); + setError(undefined); + try { + const elementIris = await queryElementIris(dataProvider, query, controller.signal); + if (controller.signal.aborted) { + return; + } + storeLastQuery(query); + onApply(elementIris); + } catch (err) { + if (controller.signal.aborted) { + return; + } + setError(err instanceof Error ? err.message : String(err)); + } finally { + if (!controller.signal.aborted) { + setQuerying(false); + } + } + }; + + return ( +
+
+
+ +