diff --git a/AGENTS.md b/AGENTS.md index 3701009..6fa2861 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -61,17 +61,7 @@ If available (e.g. via MCP), always use context7 when I need code generation, se # SPARQL Queries -These should be defined in `frontend/src/lib/queries`. Each query has its own query file \*.rq which must contain a comment that starts with its description at the top and then list its placeholders, then the actual SPARQL content. Example of header comment: - -``` -# Search for nanopubs of a specific RDF type. -# Excludes invalidated and superseded nanopubs. -# -# Placeholder: `?_searchTerm` - the user's search string. -# Placeholder: `?_rdfType` - URI: the full type. -``` - -Each query also has a \*.d.ts file which is (re)generated automatically using `npm run generate:query-types` in the frontend workspace. That same npm script will also generate `frontend/src/lib/queries/index.ts` barrel exports for convenient usage. A query can be run (including binding parameters) using functions in `frontend/src/lib/sparql.ts`, usually via `executeBindSparql()`. When using those query execution functions, especially within a React hook that could get run twice in dev mode (React Strict Mode), make sure you implement AbortSignal as per the comment above where `executeBindSparql()` is defined. +SPARQL queries are used to query the nanopublication repositories. Look in [agent-docs/SPARQL.md](agent-docs/SPARQL.md) for more guidance about SPARQL queries for nanopubs. # Library preferences @@ -82,7 +72,7 @@ Each query also has a \*.d.ts file which is (re)generated automatically using `n Under `frontend/src/pages/np/create/components/templates/` you will find a number of user-friendly forms for creating popular types of nanopubs using templates (which are themselves nanopubs, listed in `frontend/src/pages/np/create/components/templates/registry-metadata.ts`). -The lightweight library `formedible` provides simplified form generation functionality with reduced boilerplate (using TanStack Form, Zod and shadcn/ui). Use this approach for complex forms and templates for creating Nanopublications, where possible. More information, including examples, are availble in the file `agent-docs/FORMEDIBLE.md`. You can also follow some of the existing forms under `frontend/src/pages/np/create/components/templates/` as examples but feel free to improve on it. +The lightweight library `formedible` provides simplified form generation functionality with reduced boilerplate (using TanStack Form, Zod and shadcn/ui). Use this approach for complex forms and templates for creating Nanopublications, where possible. More information, including examples, are availble in the file [agent-docs/FORMEDIBLE.md](agent-docs/FORMEDIBLE.md). You can also follow some of the existing forms under `frontend/src/pages/np/create/components/templates/` as examples but feel free to improve on it. The forms gather the required information defined in the template ("placeholders"), then pass that to the `generateNanopublication()` function which generates the "statements" (RDF triples) and the rest of the output nanopub, including signing it and generating a globally unique trustyHash to indentify it. diff --git a/agent-docs/SPARQL.md b/agent-docs/SPARQL.md new file mode 100644 index 0000000..10f2077 --- /dev/null +++ b/agent-docs/SPARQL.md @@ -0,0 +1,96 @@ +# SPARQL Queries + +These should be defined in [frontend/src/lib/queries](frontend/src/lib/queries). Each query has its own query file \*.rq which must contain a comment that starts with its description at the top and then list its placeholders, then the actual SPARQL content. Example of header comment: + +``` +# Search for nanopubs of a specific RDF type. +# Excludes invalidated and superseded nanopubs. +# +# Placeholder: `?_searchTerm` - the user's search string. +# Placeholder: `?_rdfType` - URI: the full type. +``` + +Each query also has a \*.d.ts file which is (re)generated automatically using `npm run generate:query-types` in the frontend workspace. That same npm script will also generate `frontend/src/lib/queries/index.ts` barrel exports for convenient usage. A query can be run (including binding parameters) using functions in `frontend/src/lib/sparql.ts`, usually via `executeBindSparql()`. When using those query execution functions, especially within a React hook that could get run twice in dev mode (React Strict Mode), make sure you implement AbortSignal as per the comment above where `executeBindSparql()` is defined. + +## Graphs available + +``` +graph npa:graph +graph npa:networkGraph +graph ?pubinfo +graph ?assertion +``` + +The `?pubinfo` `?assertion` graphs are available in the `NANOPUB_SPARQL_ENDPOINT_FULL` endpoint. + +The `npa:graph` (Admin) and `npa:networkGraph` are always available, see next sub-section. + +## Admin graph + +The SPARQL query endpoints provide an admin graph (`npa:graph`) which is an efficient way to query various important properties of nanopubs. Here is an outline of what the admin graph contains, as [csv](https://github.com/knowledgepixels/nanopub-query/blob/main/doc%2Fadmin-triple-table.csv): + +```csv +Subject,Predicate,Object,Graph,Group,Comment +NANOPUB,npa:hasValidSignatureForPublicKey,FULL_PUBKEY,npa:graph,meta,full pubkey if signature is valid +NANOPUB,npa:hasValidSignatureForPublicKeyHash,PUBKEY_HASH,npa:graph,meta,hex-encoded SHA256 hash if signature is valid +NANOPUB,npx:signedBy,SIGNER,npa:graph,meta,ID of signer +NANOPUB1,RELATION,NANOPUB2,npa:networkGraph,meta,any inter-nanopub relation found in NANOPUB1 +NANOPUB,npx:introduces,THING,npa:graph,meta,when such a triple is present in pubinfo of NANOPUB +NANOPUB,npx:describes,THING,npa:graph,meta,when such a triple is present in pubinfo of NANOPUB +NANOPUB,npx:embeds,THING,npa:graph,meta,when such a triple is present in pubinfo of NANOPUB +NANOPUB,npa:hasSubIri,SUB_IRI,npa:graph,meta,for any IRI minted in the namespace of the NANOPUB +NANOPUB1,npa:refersToNanopub,NANOPUB2,npa:networkGraph,meta,generic inter-nanopub relation +NANOPUB,npx:invalidates,INVALIDATED_NANOPUB,npa:graph,meta,if the NANOPUB retracts or supersedes another nanopub +NANOPUB,npx:retracts,RETRACTED_NANOPUB,npa:graph,meta,if the NANOPUB retracts another nanopub +NANOPUB,npx:supersedes,SUPERSEDED_NANOPUB,npa:graph,meta,if the NANOPUB supersedes another nanopub +NANOPUB,npa:hasHeadGraph,HEAD_GRAPH,npa:graph,meta,direct link to the head graph of the NANOPUB +NANOPUB,npa:hasGraph,GRAPH,npa:graph,meta,generic link to all four graphs of the given NANOPUB +NANOPUB,np:hasAssertion,ASSERTION_GRAPH,npa:graph,meta,direct link to the assertion graph of the NANOPUB +NANOPUB,np:hasProvenance,PROVENANCE_GRAPH,npa:graph,meta,direct link to the provenance graph of the NANOPUB +NANOPUB,np:hasPublicationInfo,PUBINFO_GRAPH,npa:graph,meta,direct link to the pubinfo graph of the NANOPUB +NANOPUB,npa:artifactCode,ARTIFACT_CODE,npa:graph,meta,artifact code starting with 'RA...' +NANOPUB,npa:isIntroductionOf,AGENT,npa:graph,meta,linking intro nanopub to the agent it is introducing +NANOPUB,npa:declaresPubkey,FULL_PUBKEY,npa:graph,meta,full pubkey declared by the given intro NANOPUB +NANOPUB,dct:created,CREATION_DATE,npa:graph,meta,normalized creation timestamp +NANOPUB,npx:hasNanopubType,NANOPUB_TYPE,npa:graph,meta,type of NANOPUB +NANOPUB,rdfs:label,LABEL,npa:graph,meta,label of NANOPUB +NANOPUB,dct:description,LABEL,npa:graph,meta,description of NANOPUB +NANOPUB,dct:creator,CREATOR,npa:graph,meta,creator of NANOPUB (can be several) +NANOPUB,pav:authoredBy,AUTHOR,npa:graph,meta,author of NANOPUB (can be several) +NANOPUB,npa:hasFilterLiteral,FILTER_LITERAL,npa:graph,literal,auxiliary literal for filtering by type and pubkey in text repo +REPO,npa:hasNanopubCount,NANOPUB_COUNT,npa:graph,admin,number of nanopubs loaded +REPO,npa:hasNanopubChecksum,NANOPUB_CHECKSUM,npa:graph,admin,checksum of all loaded nanopubs (order-independent XOR checksum on trusty URIs in Base64 notation) +NANOPUB,npa:hasLoadNumber,LOAD_NUMBER,npa:graph,admin,the sequential number at which this NANOPUB was loaded +NANOPUB,npa:hasLoadChecksum,LOAD_CHECKSUM,npa:graph,admin,the checksum of all loaded nanopubs after loading the given NANOPUB +NANOPUB,npa:hasLoadTimestamp,LOAD_TIMESTAMP,npa:graph,admin,the time point at which this NANOPUB was loaded +``` + +## Endpoints for query + +For normal queries (no text search), always use `NANOPUB_SPARQL_ENDPOINT_FULL` as it provide the most data including access to the assertion, and pubinfo graphs. + +For text search queries (Lucene), then `NANOPUB_SPARQL_ENDPOINT_TEXT` must be used instead, however in that case the assertion and pubinfo graphs are not available, not even via the admin graph. + +## Title and Full text search (Lucene) + +The `/text` endpoint (`NANOPUB_SPARQL_ENDPOINT_TEXT`) supports title and full text search using Lucene Sail: + +``` +PREFIX search: + +?subj search:matches [ + search:query "search terms..."; + search:property my:property; + search:score ?score; + search:snippet ?snippet ] . +``` + +The ‘virtual’ properties in the search: namespace have the following meaning: + +- `search:matches` – links the resource to be found with the following query statements (required) +- `search:query` – specifies the Lucene query (required) +- `search:property` – specifies the property to search. If omitted all properties are searched. Use `rdfs:label` for label-only search or `npa:hasFilterLiteral` for full-text search (optional) +- `search:score` – specifies a variable for the score (optional) +- `search:snippet` – specifies a variable for a highlighted snippet (optional) + +More details about advanced queries (e.g. field boosting and per-field search) are available here if needed: https://raw.githubusercontent.com/eclipse-rdf4j/rdf4j/refs/heads/main/site/content/documentation/programming/lucene.md diff --git a/frontend/src/hooks/use-nanopub-search.ts b/frontend/src/hooks/use-nanopub-search.ts index a8f4d04..b777800 100644 --- a/frontend/src/hooks/use-nanopub-search.ts +++ b/frontend/src/hooks/use-nanopub-search.ts @@ -19,6 +19,7 @@ import { NANOPUB_SPARQL_ENDPOINT_FULL, NANOPUB_SPARQL_ENDPOINT_TEXT, } from "@/lib/sparql"; +import { bestLabelForRow } from "@/lib/string-format"; import { getTemplateUris, paginateRows, @@ -152,20 +153,22 @@ export function useNanopubSearch({ setHasMore(moreResultsAvailable); setSearchResults( - visibleRows.map((row: any) => ({ - np: row.np, - label: row.label || row.description || "", - date: new Date(row.date), - creator: row.creator || "", - types: row.types ? row.types.split("|") : [], - template: row.template, - maxScore: - row.maxScore != null ? parseFloat(row.maxScore) : undefined, - referenceCount: - row.referenceCount != null - ? parseInt(row.referenceCount) - : undefined, - })), + visibleRows.map((row: any) => { + return { + np: row.np, + label: bestLabelForRow(row), + date: new Date(row.date), + creator: row.creator || "", + types: row.types ? row.types.split("|") : [], + template: row.template, + maxScore: + row.maxScore != null ? parseFloat(row.maxScore) : undefined, + referenceCount: + row.referenceCount != null + ? parseInt(row.referenceCount) + : undefined, + }; + }), ); } catch (e: any) { if (e?.name === "AbortError") return; diff --git a/frontend/src/hooks/use-nanopub.ts b/frontend/src/hooks/use-nanopub.ts index 202d6ba..c57ed38 100644 --- a/frontend/src/hooks/use-nanopub.ts +++ b/frontend/src/hooks/use-nanopub.ts @@ -43,7 +43,7 @@ export function useNanopub( let newStore: NanopubStore; if (typeof uriOrStore === "string") { - newStore = await NanopubStore.load(uriOrStore); + newStore = await NanopubStore.load(uriOrStore, true); } else { newStore = uriOrStore; } diff --git a/frontend/src/lib/nanopub-store.ts b/frontend/src/lib/nanopub-store.ts index 61abce6..fe4e8df 100644 --- a/frontend/src/lib/nanopub-store.ts +++ b/frontend/src/lib/nanopub-store.ts @@ -6,7 +6,7 @@ import { Util, Writer, } from "n3"; -import { NANOPUB_TYPES } from "./queries"; +import { NANOPUB_LABELS, NANOPUB_REFERS_TO, NANOPUB_TYPES } from "./queries"; import { extractSubjectProps, fetchQuads, @@ -161,7 +161,7 @@ export class NanopubStore extends N3Store { * (Technically this will load any RDF given a URL, not just nanopubs) * */ - static async load(url: string) { + static async load(url: string, fillLabelCache = false) { const store = new NanopubStore(); const prefixes: any = {}; await fetchQuads( @@ -172,6 +172,9 @@ export class NanopubStore extends N3Store { store.prefixes = prefixes; store.extractGraphUris(); await store.extractMetadata(); + if (fillLabelCache) { + await store.fillLabelCacheFromRefersTo(); + } return store; } @@ -182,7 +185,7 @@ export class NanopubStore extends N3Store { * (Technically this will load any RDF, not just nanopubs) * */ - static async loadString(rdf: string) { + static async loadString(rdf: string, fillLabelCache = false) { const store = new NanopubStore(); const prefixes: any = {}; await parseRdf( @@ -193,6 +196,9 @@ export class NanopubStore extends N3Store { store.prefixes = prefixes; store.extractGraphUris(); await store.extractMetadata(); + if (fillLabelCache) { + await store.fillLabelCacheFromReferencedNanopubs(); + } return store; } @@ -210,6 +216,7 @@ export class NanopubStore extends N3Store { if (uri) { // Search the document internally, then the local labelCache, then a list of COMMON_LABELS label = + this.labelCache[uri] || this.matchOne(namedNode(uri), NS.FOAF("name"), null, null)?.object .value || this.matchOne(namedNode(uri), NS.NPTs("hasLabelFromApi"), null, null) @@ -474,6 +481,21 @@ export class NanopubStore extends N3Store { this.graphUris.pubinfo, ); + // This is a special workaround, as we used to fall back to using "NP created using..." + // if the NP was created using a template with no hasNanopubLabelPattern specified, and + // that was not ideal because all the NPs send up with the same name. + // So if we detect legacy nanopubs, try best effort to get the label of the first + // introduced subject, which is the newer strategy as of early May 2026. + // Also related: bestLabelForRow() helper function. + // Determine the title value, potentially overriding the default if it's a legacy placeholder + const defaultTitleValue = title?.object?.value || null; + const titleValue = + defaultTitleValue?.startsWith("NP created using") && + introduces && + introduces.length > 0 + ? (introduces.find((intro) => intro.label)?.label ?? defaultTitleValue) + : defaultTitleValue; + const license = this.matchOne( namedNode(this.prefixes["this"]), DCT("license"), @@ -499,7 +521,7 @@ export class NanopubStore extends N3Store { creators, types, introduces: introduces, - title: title?.object?.value || null, + title: titleValue, assertionSubjects: unique(assertionSubjects), license, uri: this.prefixes["this"], @@ -507,6 +529,82 @@ export class NanopubStore extends N3Store { }; } + /** + * Use the nanopub-refers-to SPARQL query to populate the labelCache + * with labels of nanopubs referred to by this nanopub. + * Suitable for published nanopubs loaded via URL. + */ + protected async fillLabelCacheFromRefersTo() { + const nanopubUri = this.metadata.uri ?? this.prefixes["this"]; + if (!nanopubUri) return; + + try { + const results = await executeBindSparql( + NANOPUB_REFERS_TO, + { nanopubUri }, + NANOPUB_SPARQL_ENDPOINT_FULL, + ); + if (results) { + for (const row of results) { + if (row.refNanopub && row.label) { + this.labelCache[row.refNanopub] = row.label; + } + } + } + } catch { + // Silently ignore – label cache is best-effort + } + } + + /** + * Scan the store for nanopub URIs referenced in quads (excluding this + * nanopub's own URI) and use a single SPARQL query with a VALUES clause + * to fetch their labels into the labelCache. + * Suitable for unpublished nanopubs loaded from a string, where the + * nanopub-refers-to network index query would not work. + */ + protected async fillLabelCacheFromReferencedNanopubs() { + const ownUri = this.metadata.uri ?? this.prefixes["this"]; + const seenUris = new Set(); + + // Collect unique nanopub URIs referenced as objects in quads + for (const quad of this.getQuads(null, null, null, null)) { + if (quad.object.termType === "NamedNode") { + const objUri = quad.object.value; + if ( + objUri && + objUri !== ownUri && + isNanopubUri(objUri) && + !seenUris.has(objUri) + ) { + seenUris.add(objUri); + } + } + } + + if (seenUris.size === 0) return; + + // Build a VALUES list of URI references and fetch all labels in a single query + const valuesList = [...seenUris].map((uri) => `<${uri}>`).join(" "); + + try { + const results = await executeBindSparql( + NANOPUB_LABELS, + { nanopubUris: valuesList }, + NANOPUB_SPARQL_ENDPOINT_FULL, + ); + if (results) { + for (const row of results) { + if (row.nanopubUri && row.label) { + this.labelCache[row.nanopubUri] = row.label; + } + } + } + } catch { + // Silently ignore – label cache is best-effort + } + } + /** * Get the citation as a string. */ diff --git a/frontend/src/lib/queries/index.ts b/frontend/src/lib/queries/index.ts index cacd960..b65c65c 100644 --- a/frontend/src/lib/queries/index.ts +++ b/frontend/src/lib/queries/index.ts @@ -9,7 +9,10 @@ export { default as LATEST_ALL } from "./latest-all.rq"; export { default as LATEST_BY_TEMPLATE } from "./latest-by-template.rq"; export { default as LATEST_BY_TEMPLATES } from "./latest-by-templates.rq"; export { default as NANOPUB_COMMENTS } from "./nanopub-comments.rq"; +export { default as NANOPUB_LABEL } from "./nanopub-label.rq"; +export { default as NANOPUB_LABELS } from "./nanopub-labels.rq"; export { default as NANOPUB_REFERENCES } from "./nanopub-references.rq"; +export { default as NANOPUB_REFERS_TO } from "./nanopub-refers-to.rq"; export { default as NANOPUB_STATUS } from "./nanopub-status.rq"; export { default as NANOPUB_TYPES } from "./nanopub-types.rq"; export { default as SEARCH_NANOPUBS } from "./search-nanopubs.rq"; diff --git a/frontend/src/lib/queries/nanopub-label.rq b/frontend/src/lib/queries/nanopub-label.rq new file mode 100644 index 0000000..e4ea492 --- /dev/null +++ b/frontend/src/lib/queries/nanopub-label.rq @@ -0,0 +1,13 @@ +# Get the label for a nanopub URI from the nanopub network. +# Returns the rdfs:label registered in the admin graph for the specified nanopub. +# +# Placeholder: `?_nanopubUri` - URI: the nanopub URI to look up the label for. + +prefix rdfs: +prefix npa: + +select ?label where { + graph npa:graph { + ?_nanopubUri rdfs:label ?label . + } +} limit 1 diff --git a/frontend/src/lib/queries/nanopub-label.rq.d.ts b/frontend/src/lib/queries/nanopub-label.rq.d.ts new file mode 100644 index 0000000..6220804 --- /dev/null +++ b/frontend/src/lib/queries/nanopub-label.rq.d.ts @@ -0,0 +1,11 @@ +/** + * Get the label for a nanopub URI from the nanopub network. Returns the rdfs:label registered in the admin graph for the specified nanopub. + * @see ./nanopub-label.rq + * @generator Generated by `npm run generate:query-types` + */ +declare const query: import("../sparql").SparqlQuery<{ + nanopubUri: "uri"; +}, { + label: "string"; +}>; +export default query; diff --git a/frontend/src/lib/queries/nanopub-labels.rq b/frontend/src/lib/queries/nanopub-labels.rq new file mode 100644 index 0000000..fb85cc8 --- /dev/null +++ b/frontend/src/lib/queries/nanopub-labels.rq @@ -0,0 +1,14 @@ +# Get the labels for a set of nanopub URIs from the nanopub network. +# Uses a VALUES clause to efficiently look up multiple labels in a single query. +# +# Placeholder: `?_nanopubUris` - Raw: space-separated URIs in VALUES syntax, e.g. ` ` + +prefix rdfs: +prefix npa: + +select ?nanopubUri ?label where { + graph npa:graph { + ?nanopubUri rdfs:label ?label . + } + values ?nanopubUri { ?_nanopubUris } +} diff --git a/frontend/src/lib/queries/nanopub-labels.rq.d.ts b/frontend/src/lib/queries/nanopub-labels.rq.d.ts new file mode 100644 index 0000000..1d6428a --- /dev/null +++ b/frontend/src/lib/queries/nanopub-labels.rq.d.ts @@ -0,0 +1,12 @@ +/** + * Get the labels for a set of nanopub URIs from the nanopub network. Uses a VALUES clause to efficiently look up multiple labels in a single query. + * @see ./nanopub-labels.rq + * @generator Generated by `npm run generate:query-types` + */ +declare const query: import("../sparql").SparqlQuery<{ + nanopubUris: "raw"; +}, { + nanopubUri: "string"; + label: "string"; +}>; +export default query; diff --git a/frontend/src/lib/queries/nanopub-references.rq b/frontend/src/lib/queries/nanopub-references.rq index 2713cb1..92b31c1 100644 --- a/frontend/src/lib/queries/nanopub-references.rq +++ b/frontend/src/lib/queries/nanopub-references.rq @@ -12,7 +12,7 @@ prefix npx: prefix dct: prefix nt: -select ?np ?label ?date ?creator ?template where { +select ?np ?label ?date ?creator ?template (max(?introducedLabel) as ?description) where { # Use npa:networkGraph to find nanopubs that refer to the specified nanopub graph npa:networkGraph { ?np npa:refersToNanopub ?_nanopubUri . @@ -41,6 +41,16 @@ select ?np ?label ?date ?creator ?template where { # Optionally get the template this nanopub was created from optional { graph npa:networkGraph { ?np nt:wasCreatedFromTemplate ?template . } } -} + + # Get label of introduced subject as fallback for legacy "NP created using..." labels + optional { + graph ?pubinfo { + ?np npx:introduces ?introduced . + } + graph ?assertion { + ?introduced rdfs:label ?introducedLabel . + } + } +} group by ?np ?label ?date ?creator ?template order by desc(?date) limit 100 diff --git a/frontend/src/lib/queries/nanopub-references.rq.d.ts b/frontend/src/lib/queries/nanopub-references.rq.d.ts index 3a077a7..ef914ce 100644 --- a/frontend/src/lib/queries/nanopub-references.rq.d.ts +++ b/frontend/src/lib/queries/nanopub-references.rq.d.ts @@ -6,6 +6,7 @@ declare const query: import("../sparql").SparqlQuery<{ nanopubUri: "uri"; }, { + description: "string"; np: "string"; label: "string"; date: "string"; diff --git a/frontend/src/lib/queries/nanopub-refers-to.rq b/frontend/src/lib/queries/nanopub-refers-to.rq new file mode 100644 index 0000000..c831f26 --- /dev/null +++ b/frontend/src/lib/queries/nanopub-refers-to.rq @@ -0,0 +1,18 @@ +# Get the labels of all nanopubs referred to within a specified nanopub. +# Uses npa:networkGraph to efficiently find outgoing references. +# +# Placeholder: `?_nanopubUri` - URI: the URI of the nanopub to find outgoing references for. + +prefix rdfs: +prefix npa: + +select ?refNanopub ?label where { + graph npa:networkGraph { + ?_nanopubUri npa:refersToNanopub ?refNanopub . + } + optional { + graph npa:graph { + ?refNanopub rdfs:label ?label . + } + } +} diff --git a/frontend/src/lib/queries/nanopub-refers-to.rq.d.ts b/frontend/src/lib/queries/nanopub-refers-to.rq.d.ts new file mode 100644 index 0000000..28cab30 --- /dev/null +++ b/frontend/src/lib/queries/nanopub-refers-to.rq.d.ts @@ -0,0 +1,12 @@ +/** + * Get the labels of all nanopubs referred to within a specified nanopub. Uses npa:networkGraph to efficiently find outgoing references. + * @see ./nanopub-refers-to.rq + * @generator Generated by `npm run generate:query-types` + */ +declare const query: import("../sparql").SparqlQuery<{ + nanopubUri: "uri"; +}, { + refNanopub: "string"; + label: "string"; +}>; +export default query; diff --git a/frontend/src/lib/queries/search-nanopubs.rq b/frontend/src/lib/queries/search-nanopubs.rq index f1bb851..df57732 100644 --- a/frontend/src/lib/queries/search-nanopubs.rq +++ b/frontend/src/lib/queries/search-nanopubs.rq @@ -30,13 +30,11 @@ select ?np ?label ?date ?creator (group_concat(distinct ?type; separator="|") as filter not exists { ?np npx:hasNanopubType nt:PubinfoTemplate } optional { ?np rdfs:label ?label . } - ?np np:hasAssertion ?assertion ; - dct:created ?date ; + ?np dct:created ?date ; npx:hasNanopubType ?type; dct:creator ?creator . } - ?np search:matches [ search:query ?_searchTerm ; search:property ?_searchProperty ; diff --git a/frontend/src/lib/rdf.ts b/frontend/src/lib/rdf.ts index 1feb1d8..f41b4c9 100644 --- a/frontend/src/lib/rdf.ts +++ b/frontend/src/lib/rdf.ts @@ -184,7 +184,6 @@ export async function fetchPossibleValuesFromQuads(url: string) { return options; } -// export type Store = N3Store; export type Statement = RDFT.Quad; export function groupByGraph( diff --git a/frontend/src/lib/string-format.ts b/frontend/src/lib/string-format.ts index a28dbdc..099ea40 100644 --- a/frontend/src/lib/string-format.ts +++ b/frontend/src/lib/string-format.ts @@ -36,3 +36,13 @@ export const wrapKeyPEM = (base64Key: string) => { return `${pemHeader}\n${formattedKey}\n${pemFooter}`; }; + +export const bestLabelForRow = (row: any) => { + // Largely to apply workaround for confusing legacy "NP created using..." + // labels appearing in search result listing. + // If label starts with "NP created using" and we have a description + // (e,g, from introduced subject's rdfs:label), use the description instead + return row.label?.startsWith("NP created using") && row.description + ? row.description + : row.label || ""; +}; diff --git a/frontend/src/pages/np/components/GeneralSearch.tsx b/frontend/src/pages/np/components/GeneralSearch.tsx index 7f52a3d..cfa07d0 100644 --- a/frontend/src/pages/np/components/GeneralSearch.tsx +++ b/frontend/src/pages/np/components/GeneralSearch.tsx @@ -205,7 +205,7 @@ export function GeneralSearch() { next.delete("page"); setSearchParams(next); }} - placeholder="Enter search query or nanopub URI..." + placeholder="Enter search terms or nanopub URI..." loading={loading} searchMode={searchMode} onSearchModeChange={handleSearchModeChange} diff --git a/frontend/src/pages/np/components/NanopubReferences.tsx b/frontend/src/pages/np/components/NanopubReferences.tsx index 13d8af2..d33e3c4 100644 --- a/frontend/src/pages/np/components/NanopubReferences.tsx +++ b/frontend/src/pages/np/components/NanopubReferences.tsx @@ -5,6 +5,7 @@ import { Spinner } from "@/components/ui/spinner"; import { NANOPUB_REFERENCES } from "@/lib/queries"; import { executeBindSparql, NANOPUB_SPARQL_ENDPOINT_FULL } from "@/lib/sparql"; +import { bestLabelForRow } from "@/lib/string-format"; import { Collapsible, CollapsibleContent, @@ -43,13 +44,15 @@ export function NanopubReferences({ nanopubUri }: { nanopubUri: string }) { ); setReferences( - rows.map((row) => ({ - np: row.np, - label: row.label || "", - date: new Date(row.date), - creator: row.creator || "", - template: row.template, - })), + rows.map((row) => { + return { + np: row.np, + label: bestLabelForRow(row), + date: new Date(row.date), + creator: row.creator || "", + template: row.template, + }; + }), ); hasFetched.current = true; } catch (e: any) { diff --git a/frontend/src/pages/np/components/search/NanopubPreviewPopover.tsx b/frontend/src/pages/np/components/search/NanopubPreviewPopover.tsx index 1acd7d6..5bd180d 100644 --- a/frontend/src/pages/np/components/search/NanopubPreviewPopover.tsx +++ b/frontend/src/pages/np/components/search/NanopubPreviewPopover.tsx @@ -32,7 +32,7 @@ export function NanopubPreviewPopover({ uri }: { uri: string }) { } let cancelled = false; setLoading(true); - NanopubStore.load(uri) + NanopubStore.load(uri, true) .then((s) => { if (cancelled) return; setStore(s); diff --git a/frontend/src/pages/np/components/search/SearchBar.tsx b/frontend/src/pages/np/components/search/SearchBar.tsx index 487d2e8..25e49d7 100644 --- a/frontend/src/pages/np/components/search/SearchBar.tsx +++ b/frontend/src/pages/np/components/search/SearchBar.tsx @@ -257,6 +257,9 @@ export function SearchBar({ Oldest first + + Wildcard match * in search term supported +