diff --git a/README.md b/README.md index 7532566..5536142 100644 --- a/README.md +++ b/README.md @@ -143,6 +143,7 @@ You can configure the following settings: - Logo - Colors - Fonts +- Searchable Fields The settings are explained in the following sections. @@ -275,6 +276,25 @@ url: [String] This will add the `url` field, being an array of strings. For other types, compare with already existing properties and just copy as you need. +## Adding Searchable Fields + +To add a field to be searchable you have to make the following adjustments: + +- Add the field in the `config.yaml` file to `searchableAttributes`, e.g. `editorialNote` +- In `src/queries.js` add it to `ConceptFields` (around line 176): +```graphql +editorialNote { + ${[...languages].join(" ")} +} +``` +- Add it to the labels to be indexed. Go to `gatsby-node.js` and add it the document object around line 341. For fields being *single* language tagged labels (e.g. `skos:prefLabel`) use `prefLabel` as an example. For fields being arrays of language tagged labels (e.g. `skos:altLabel`) use `altLabel` as an example. For `skos:editorialNote` it would be: +```js +...(concept.editorialNote && +Object.hasOwn(concept.editorialNote, language) && { + editorialNote: i18n(language)(concept.editorialNote), +}), +``` + ## Troubleshooting Depending on special circumstances you may get errors in the log files, e.g. diff --git a/config.default.yaml b/config.default.yaml index 06fab5e..e1f6e95 100644 --- a/config.default.yaml +++ b/config.default.yaml @@ -10,6 +10,7 @@ searchableAttributes: - "hiddenLabel" - "example" - "definition" + - "scopeNote" ui: title: "SkoHub Vocabs" # Title is mandatory logo: "skohub-signet-color.svg" # Path diff --git a/cypress/config.e2e.yaml b/cypress/config.e2e.yaml index 4b2ceb4..8046a3f 100644 --- a/cypress/config.e2e.yaml +++ b/cypress/config.e2e.yaml @@ -10,6 +10,7 @@ searchableAttributes: - "hiddenLabel" - "example" - "definition" + - "scopeNote" ui: title: "SkoHub Vocabs" # Title is mandatory logo: "skohub-signet-color.svg" # Path diff --git a/cypress/e2e/conceptSchemeAndConcept.cy.js b/cypress/e2e/conceptSchemeAndConcept.cy.js index 97ec3fe..7689c06 100644 --- a/cypress/e2e/conceptSchemeAndConcept.cy.js +++ b/cypress/e2e/conceptSchemeAndConcept.cy.js @@ -64,6 +64,16 @@ describe("Concept Scheme and Concept", () => { cy.get("h1").should("have.text", "Hash URI Konzept Schema") }) + it("Visting a hash URI Concept Scheme with hash uri in URL works", () => { + cy.visit("/example.org/hashURIConceptScheme.html#scheme", { + onBeforeLoad(win) { + Object.defineProperty(win.navigator, "language", { value: "de-DE" }) + }, + }) + cy.get(".conceptScheme > a").should("have.text", "Hash URI Konzept Schema") + cy.get("h1").should("have.text", "Hash URI Konzept Schema") + }) + it("Visting a hash URI Concept works", () => { cy.visit("/example.org/hashURIConceptScheme.html#concept1", { onBeforeLoad(win) { diff --git a/cypress/e2e/searchAndFilter.cy.js b/cypress/e2e/searchAndFilter.cy.js index b64e0f0..38503dc 100644 --- a/cypress/e2e/searchAndFilter.cy.js +++ b/cypress/e2e/searchAndFilter.cy.js @@ -151,4 +151,18 @@ describe("search and filter", () => { cy.get("#closeModal").click() cy.get("span").contains("Konzept 1").should("exist") }) + + it("turning on scopeNote checkbox returns scopeNote matches", () => { + cy.visit("/w3id.org/index.html", { + onBeforeLoad(win) { + Object.defineProperty(win.navigator, "language", { value: "de-DE" }) + }, + }) + cy.findByRole("textbox").type("Scope") + cy.get("p").contains("Nothing found").should("exist") + cy.get("#settings").click() + cy.get("#scopeNoteCheckBox").click() + cy.get("#closeModal").click() + cy.get("span").contains("Konzept 1").should("exist") + }) }) diff --git a/gatsby-node.js b/gatsby-node.js index dd869fc..6a2e1d8 100644 --- a/gatsby-node.js +++ b/gatsby-node.js @@ -289,14 +289,7 @@ exports.createPages = async ({ graphql, actions: { createPage } }) => { document: { id: "id", // store: ["prefLabel", "altLabel"], /* not working when importing, bug in flexsearch */ - index: [ - "notation", - "prefLabel", - "altLabel", - "hiddenLabel", - "definition", - "example", - ], + index: [...config.searchableAttributes], }, }) return [l, index] @@ -306,7 +299,14 @@ exports.createPages = async ({ graphql, actions: { createPage } }) => { const conceptsInScheme = await graphql( queries.allConcept(conceptScheme.id, languages) ) - const embeddedConcepts = [] + // embed concept scheme data + const embeddedConcepts = [ + { + json: omitEmpty(Object.assign({}, conceptScheme, context.jsonld)), + jsonld: omitEmpty(Object.assign({}, conceptScheme, context.jsonld)), + }, + ] + conceptsInScheme.data.allConcept.edges.forEach(({ node: concept }) => { const json = omitEmpty(Object.assign({}, concept, context.jsonld)) const jsonld = omitEmpty(Object.assign({}, concept, context.jsonld)) @@ -364,6 +364,10 @@ exports.createPages = async ({ graphql, actions: { createPage } }) => { Object.hasOwn(concept.example, language) && { example: i18n(language)(concept.example), }), + ...(concept.scopeNote && + Object.hasOwn(concept.scopeNote, language) && { + scopeNote: i18n(language)(concept.scopeNote), + }), notation: concept.notation, } indexes[language].add(document) diff --git a/shapes/skohub.shacl.ttl b/shapes/skohub.shacl.ttl index ecb16f9..546780f 100644 --- a/shapes/skohub.shacl.ttl +++ b/shapes/skohub.shacl.ttl @@ -43,7 +43,6 @@ sh:severity sh:Violation ; rdfs:comment "Tested with 01_cs_no_title.ttl" ; ] - ); sh:property [ sh:path skos:hasTopConcept ; @@ -60,15 +59,26 @@ sh:message "The target class for hasTopConcept should be skos:Concept" ; rdfs:comment "Tested with 03_cs_target_class_hasTopConcept.ttl" ; ] ; - sh:property [ - sh:path dct:description ; - sh:datatype rdf:langString ; - sh:severity sh:Warning ; - sh:minCount 1 ; - sh:message: "A description of the Concept Scheme should be provided." ; - rdfs:comment "Tested with 04_cs_no_description.ttl" ; - rdfs:comment "Tested with 05_cs_description_no_langstring.ttl" ; - ] ; + sh:or ( + [ + sh:path dct:description ; + sh:datatype rdf:langString ; + sh:severity sh:Warning ; + sh:minCount 1 ; + sh:message: "A description of the Concept Scheme should be provided." ; + rdfs:comment "Tested with 04_cs_no_description.ttl" ; + rdfs:comment "Tested with 05_cs_description_no_langstring.ttl" ; + ] + [ + sh:path dc:description ; + sh:datatype rdf:langString ; + sh:severity sh:Warning ; + sh:minCount 1 ; + sh:message: "A description of the Concept Scheme should be provided." ; + rdfs:comment "Tested with 04_cs_no_description.ttl" ; + rdfs:comment "Tested with 05_cs_description_no_langstring.ttl" ; + ] + ) ; sh:property [ sh:path dct:license ; sh:minCount 1 ; diff --git a/src/common.js b/src/common.js index 54c0efd..9064e1a 100644 --- a/src/common.js +++ b/src/common.js @@ -98,6 +98,7 @@ const parseLanguages = (graph) => { * @property {string} tokenizer * @property {Object} colors * @property {string} customDomain + * @property {string[]} searchableAttributes */ /** diff --git a/src/components/Collection.jsx b/src/components/Collection.jsx index 4dcd86b..5556116 100644 --- a/src/components/Collection.jsx +++ b/src/components/Collection.jsx @@ -15,7 +15,7 @@ const Collection = ({ pageContext: { node: collection, customDomain } }) => { }, [data?.selectedLanguage]) return ( -
+

{i18n(language)(collection.prefLabel)}

{collection.id}

diff --git a/src/components/Concept.jsx b/src/components/Concept.jsx index 58702c9..b148571 100644 --- a/src/components/Concept.jsx +++ b/src/components/Concept.jsx @@ -19,13 +19,14 @@ const Concept = ({ }, [data?.selectedLanguage]) return ( -
+

{concept.deprecated ? "Deprecated" : ""}

{concept.notation && {concept.notation.join(",")} } - {i18n(language)(concept.prefLabel)} + {(concept?.prefLabel && i18n(language)(concept.prefLabel)) || + (concept?.title && i18n(language)(concept.title))}

@@ -52,22 +53,55 @@ const Concept = ({
)} - {concept.scopeNote && ( + {concept.note && i18n(language)(concept.note) !== "" && (
-

Scope Note

- - {i18n(language)(concept.scopeNote) || - `*No scope note in language "${language}" provided.*`} - +

Note

+
    + {i18n(language)(concept.note).map((note, i) => ( +
  • {note}
  • + ))} +
)} - {concept.note && ( + {concept.changeNote && i18n(language)(concept.changeNote) !== "" && (
-

Note

- - {i18n(language)(concept.note) || - `*No note in language "${language}" provided.*`} - +

ChangeNote

+
    + {i18n(language)(concept.changeNote).map((changeNote, i) => ( +
  • {changeNote}
  • + ))} +
+
+ )} + {concept.editorialNote && + i18n(language)(concept.editorialNote) !== "" && ( +
+

EditorialNote

+
    + {i18n(language)(concept.editorialNote).map((editorialNote, i) => ( +
  • {editorialNote}
  • + ))} +
+
+ )} + {concept.historyNote && i18n(language)(concept.historyNote) !== "" && ( +
+

HistoryNote

+
    + {i18n(language)(concept.historyNote).map((historyNote, i) => ( +
  • {historyNote}
  • + ))} +
+
+ )} + {concept.scopeNote && i18n(language)(concept.scopeNote) !== "" && ( +
+

ScopeNote

+
    + {i18n(language)(concept.scopeNote).map((scopeNote, i) => ( +
  • {scopeNote}
  • + ))} +
)} {concept.altLabel && i18n(language)(concept.altLabel) !== "" && ( diff --git a/src/components/ConceptScheme.jsx b/src/components/ConceptScheme.jsx index 10bddd8..8029cb4 100644 --- a/src/components/ConceptScheme.jsx +++ b/src/components/ConceptScheme.jsx @@ -22,19 +22,13 @@ const ConceptScheme = ({ if (pathname.hash) { const filtered = embed.filter((c) => c.json.id.endsWith(pathname.hash)) return ( -
+
) } else { return ( -
+

{(conceptScheme?.title && i18n(language)(conceptScheme.title)) || diff --git a/src/components/Search.jsx b/src/components/Search.jsx index 0a790e8..22568ba 100644 --- a/src/components/Search.jsx +++ b/src/components/Search.jsx @@ -53,7 +53,7 @@ const Search = ({ handleQueryInput, labels, onLabelClick }) => { closeModal={() => setModal(false)} id="settingsModal" > -

Which labels do you want to include in the search?

+

Which fields do you want to include in the search?

diff --git a/src/context.js b/src/context.js index 27ca751..50829d6 100644 --- a/src/context.js +++ b/src/context.js @@ -52,11 +52,20 @@ const jsonld = { definition: { "@container": "@language", }, - scopeNote: { - "@container": "@language", - }, note: { - "@container": "@language", + "@container": ["@language", "@set"], + }, + changeNote: { + "@container": ["@language", "@set"], + }, + editorialNote: { + "@container": ["@language", "@set"], + }, + historyNote: { + "@container": ["@language", "@set"], + }, + scopeNote: { + "@container": ["@language", "@set"], }, notation: { "@container": "@set", diff --git a/src/hooks/configAndConceptSchemes.js b/src/hooks/configAndConceptSchemes.js index 2becd67..161f618 100644 --- a/src/hooks/configAndConceptSchemes.js +++ b/src/hooks/configAndConceptSchemes.js @@ -1,5 +1,45 @@ import { useStaticQuery, graphql } from "gatsby" +/** + * @returns {{ + * config: { + * colors: { + * skoHubWhite: string, + * skoHubDarkColor: string, + * skoHubMiddleColor: string, + * skoHubLightColor: string, + * skoHubThinColor: string, + * skoHubBlackColor: string, + * skoHubAction: string, + * skoHubNotice: string, + * skoHubDarkGrey: string, + * skoHubMiddleGrey: string, + * skoHubLightGrey: string + * }, + * logo: string, + * title: string, + * fonts: { + * bold: { + * font_family: string, + * font_style: string, + * font_weight: string, + * name: string + * }, + * regular: { + * font_family: string, + * font_style: string, + * font_weight: string, + * name: string + * } + * }, + * searchableAttributes: string[], + * customDomain: string, + * failOnValidation: boolean + * }, + * conceptSchemes: Object + * }} An object containing `config` and `conceptSchemes` + * + */ export const getConfigAndConceptSchemes = () => { const { site, allConceptScheme } = useStaticQuery(graphql` query Colors { diff --git a/src/queries.js b/src/queries.js index 9f6dcce..7fe8b12 100644 --- a/src/queries.js +++ b/src/queries.js @@ -49,10 +49,19 @@ module.exports.allConcept = (inScheme, languages) => ` definition { ${[...languages].join(" ")} } - scopeNote { + note { ${[...languages].join(" ")} } - note { + changeNote { + ${[...languages].join(" ")} + } + editorialNote { + ${[...languages].join(" ")} + } + historyNote { + ${[...languages].join(" ")} + } + scopeNote { ${[...languages].join(" ")} } notation @@ -188,6 +197,9 @@ module.exports.allConceptScheme = (languages) => ` example { ${[...languages].join(" ")} } + scopeNote { + ${[...languages].join(" ")} + } deprecated } ` diff --git a/src/templates/App.jsx b/src/templates/App.jsx index 57cd894..b9a6c93 100644 --- a/src/templates/App.jsx +++ b/src/templates/App.jsx @@ -130,7 +130,7 @@ const App = ({ pageContext, children, location }) => { labels, data.selectedLanguage, setIndex, - config.customDomain + config ) }, [data, language, labels]) @@ -198,7 +198,7 @@ const App = ({ pageContext, children, location }) => { )}
- {children} +
{children}
) diff --git a/src/templates/helpers.js b/src/templates/helpers.js index fafe5ec..c28d1b8 100644 --- a/src/templates/helpers.js +++ b/src/templates/helpers.js @@ -1,6 +1,6 @@ import { useEffect } from "react" import Document from "flexsearch/dist/module/document.js" -import { i18n, getFilePath } from "../common" +import { getFilePath } from "../common" import { withPrefix } from "gatsby" export const handleKeypresses = (labels, setLabels) => { @@ -43,6 +43,10 @@ export const handleKeypresses = (labels, setLabels) => { e.preventDefault() Object.keys(labels).includes("hiddenLabel") && setLabels({ ...labels, ["hiddenLabel"]: !labels["hiddenLabel"] }) + } else if (e.altKey && e.which === 83) { + e.preventDefault() + Object.keys(labels).includes("scopeNote") && + setLabels({ ...labels, ["scopeNote"]: !labels["scopeNote"] }) } } document.addEventListener("keydown", handleKeyDown) @@ -58,7 +62,7 @@ export const importIndex = async ( labels, language, setIndex, - customDomain + config ) => { if (!conceptSchemeId) return const idx = new Document({ @@ -68,14 +72,7 @@ export const importIndex = async ( document: { id: "id", // store: ["prefLabel", "altLabel"], /* not working flexsearchside */ - index: [ - "notation", - "prefLabel", - "altLabel", - "hiddenLabel", - "definition", - "example", - ], + index: [...config.searchableAttributes], }, }) // filter from labels object the selected entries @@ -97,7 +94,9 @@ export const importIndex = async ( try { const path = getFilePath(conceptSchemeId) + `-cs/search/${language}/${key}` - data = await fetch(withPrefix(getFilePath(path, `json`, customDomain))) + data = await fetch( + withPrefix(getFilePath(path, `json`, config.customDomain)) + ) const jsonData = await data.json() idx.import(key, jsonData ?? null) } catch (e) { diff --git a/src/types.js b/src/types.js index 517e642..520b90f 100644 --- a/src/types.js +++ b/src/types.js @@ -22,8 +22,11 @@ module.exports = (languages) => ` altLabel: LanguageMapArray, hiddenLabel: LanguageMapArray, definition: LanguageMap, - scopeNote: LanguageMap, - note: LanguageMap, + note: LanguageMapArray, + changeNote: LanguageMapArray, + editorialNote: LanguageMapArray, + historyNote: LanguageMapArray, + scopeNote: LanguageMapArray, notation: [String], example: LanguageMap, topConceptOf: [ConceptScheme] @link(from: "topConceptOf___NODE"), diff --git a/test/concept.test.jsx b/test/concept.test.jsx index 4272965..3667f8d 100644 --- a/test/concept.test.jsx +++ b/test/concept.test.jsx @@ -125,6 +125,71 @@ describe.concurrent("Concept", () => { ).toBeInTheDocument() }) + it("renders notes", () => { + render() + + expect(screen.getByText(/Meine Anmerkung/i)).toBeInTheDocument() + + const list = screen.getByRole("list", { + name: "Note", + }) + const { getAllByRole } = within(list) + const items = getAllByRole("listitem") + expect(items.length).toBe(2) + }) + + it("renders changeNotes", () => { + render() + + expect(screen.getByText(/Meine Change Note/i)).toBeInTheDocument() + + const list = screen.getByRole("list", { + name: /changenote/i, + }) + const { getAllByRole } = within(list) + const items = getAllByRole("listitem") + expect(items.length).toBe(2) + }) + + it("renders editorialNotes", () => { + render() + + expect(screen.getByText(/Meine Editorial Note/i)).toBeInTheDocument() + + const list = screen.getByRole("list", { + name: /editorialnote/i, + }) + const { getAllByRole } = within(list) + const items = getAllByRole("listitem") + expect(items.length).toBe(2) + }) + + it("renders historyNotes", () => { + render() + + expect(screen.getByText(/Meine History Note/i)).toBeInTheDocument() + + const list = screen.getByRole("list", { + name: /historynote/i, + }) + const { getAllByRole } = within(list) + const items = getAllByRole("listitem") + expect(items.length).toBe(2) + }) + + it("renders scopeNotes", () => { + render() + + expect(screen.getByText(/Meine Scope Note/i)).toBeInTheDocument() + + const list = screen.getByRole("list", { + name: /scopenote/i, + }) + const { getAllByRole } = within(list) + const items = getAllByRole("listitem") + expect(items.length).toBe(2) + }) + it("renders related Concepts", () => { render() expect( diff --git a/test/data/pageContext.js b/test/data/pageContext.js index 116a23f..c593355 100644 --- a/test/data/pageContext.js +++ b/test/data/pageContext.js @@ -49,11 +49,20 @@ export const topConcept = { example: { de: "Ein Beispiel", }, - scopeNote: { - de: "Meine Scope Note", - }, note: { - de: "Meine Anmerkung", + de: ["Meine Anmerkung", "Noch eine Anmerkung"], + }, + changeNote: { + de: ["Meine Change Note", "Noch eine Change Note"], + }, + editorialNote: { + de: ["Meine Editorial Note", "Noch eine Editorial Note"], + }, + historyNote: { + de: ["Meine History Note", "Noch eine History Note"], + }, + scopeNote: { + de: ["Meine Scope Note", "Noch eine Scope Note"], }, notation: ["1"], narrower: [concept2],