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 ( +
+
+
+ +