diff --git a/README.md b/README.md index aef41d9..7532566 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,12 @@ To determine the language displayed of the vocabulary the browser language is us If the browser language is not present in the vocabulary a default language is chosen. If you want to link to a specific language, you can use a URL parameter: `?lang=de`. +## Deprecation of Concepts + +To mark a concept as deprecated you can mark it with `owl:deprecated true`. +To point to a successor add `dct:isReplacedBy`. +The information will be available in the machine readable version as well as in the html page. + ## Set up ### Install Node.js diff --git a/gatsby-node.js b/gatsby-node.js index 99decc5..04fa0d7 100644 --- a/gatsby-node.js +++ b/gatsby-node.js @@ -144,6 +144,7 @@ exports.onPreBootstrap = async ({ createContentDigest, actions, getNode }) => { topConceptOf, hasTopConcept, member, + deprecated, ...properties } = graph const type = Array.isArray(properties.type) @@ -174,6 +175,7 @@ exports.onPreBootstrap = async ({ createContentDigest, actions, getNode }) => { * a concept scheme not present in the graphql data layer would not be found and not * be shown on the concepts page. */ + deprecated: Boolean(deprecated) || null, inSchemeAll: inSchemeFiltered.map((inScheme) => ({ id: inScheme.id })) || null, // topConceptOf nodes are also set to inScheme to facilitate parsing and filtering later diff --git a/src/components/Concept.jsx b/src/components/Concept.jsx index 4d3aaf9..58702c9 100644 --- a/src/components/Concept.jsx +++ b/src/components/Concept.jsx @@ -1,7 +1,7 @@ import Markdown from "markdown-to-jsx" import { Link } from "gatsby" import JsonLink from "./JsonLink.jsx" -import { getConceptSchemes } from "../hooks/getConceptSchemes" +import { getConfigAndConceptSchemes } from "../hooks/configAndConceptSchemes.js" import { useSkoHubContext } from "../context/Context.jsx" import { i18n, getDomId, getFilePath } from "../common" import ConceptURI from "./ConceptURI.jsx" @@ -10,7 +10,7 @@ import { useEffect, useState } from "react" const Concept = ({ pageContext: { node: concept, collections, customDomain }, }) => { - const conceptSchemes = getConceptSchemes() + const { config, conceptSchemes } = getConfigAndConceptSchemes() const { data } = useSkoHubContext() const [language, setLanguage] = useState("") @@ -20,12 +20,29 @@ const Concept = ({ return (
+

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

{concept.notation && {concept.notation.join(",")} } {i18n(language)(concept.prefLabel)}

+ {concept.isReplacedBy && concept.isReplacedBy.length > 0 && ( +
+

Is replaced by

+
    + {concept.isReplacedBy.map((isReplacedBy) => ( +
  • + + {isReplacedBy.id} + +
  • + ))} +
+
+ )} {concept.definition && (

Definition

@@ -88,9 +105,7 @@ const Concept = ({
    {concept.related.map((related) => (
  • - + {i18n(language)(related.prefLabel) || related.id}
  • diff --git a/src/components/nestedList.jsx b/src/components/nestedList.jsx index 058ed56..7a9e15f 100644 --- a/src/components/nestedList.jsx +++ b/src/components/nestedList.jsx @@ -191,6 +191,9 @@ const NestedList = ({ const renderPrefLabel = () => { // Function for handling highlighting function handleHighlight(text, highlight) { + text = item.deprecated + ? `(DEPRECATED) ${text}` + : text if (highlight) { return text.replace(highlight, (str) => `${str}`) } else { diff --git a/src/context.js b/src/context.js index dab1450..0ae32b3 100644 --- a/src/context.js +++ b/src/context.js @@ -9,6 +9,7 @@ const jsonld = { schema: "https://schema.org/", vann: "http://purl.org/vocab/vann/", ldp: "http://www.w3.org/ns/ldp#", + owl: "http://www.w3.org/2002/07/owl#", title: { "@id": "dct:title", "@container": "@language", @@ -95,6 +96,14 @@ const jsonld = { topConceptOf: { "@container": "@set", }, + deprecated: { + "@id": "owl:deprecated", + "@type": "xsd:boolean", + }, + isReplacedBy: { + "@id": "dct:isReplacedBy", + "@container": "@set", + }, }, } diff --git a/src/queries.js b/src/queries.js index b70d33b..da97cb4 100644 --- a/src/queries.js +++ b/src/queries.js @@ -119,6 +119,10 @@ module.exports.allConcept = (inScheme, languages) => ` ${[...languages].join(" ")} } } + deprecated + isReplacedBy { + id + } } } } @@ -178,6 +182,7 @@ module.exports.allConceptScheme = (languages) => ` example { ${[...languages].join(" ")} } + deprecated } ` module.exports.tokenizer = `{ diff --git a/src/types.js b/src/types.js index e295921..911dc60 100644 --- a/src/types.js +++ b/src/types.js @@ -37,7 +37,9 @@ module.exports = (languages) => ` exactMatch: [Concept], inScheme: [ConceptScheme] @link(from: "inScheme___NODE"), inSchemeAll: [ConceptScheme], - hub: String + hub: String, + deprecated: Boolean, + isReplacedBy: [Concept] } type LanguageMap { diff --git a/test/concept.test.jsx b/test/concept.test.jsx index a90d85e..4272965 100644 --- a/test/concept.test.jsx +++ b/test/concept.test.jsx @@ -4,7 +4,7 @@ import * as Gatsby from "gatsby" import React from "react" import Concept from "../src/components/Concept.jsx" -import { ConceptPC } from "./data/pageContext" +import { ConceptPC, ConceptPCDeprecated } from "./data/pageContext" import mockFetch from "./mocks/mockFetch" import { mockConfig } from "./mocks/mockConfig" import { useSkoHubContext } from "../src/context/Context.jsx" @@ -130,7 +130,10 @@ describe.concurrent("Concept", () => { expect( screen.getByRole("heading", { name: /^related$/i }) ).toBeInTheDocument() - expect(screen.getByRole("link", { name: /konzept 4/i })).toBeInTheDocument() + const href = screen.getByRole("link", { name: /konzept 4/i }) + expect(href).toBeInTheDocument() + // ensure there is no language tag in the link + expect(href.getAttribute("href")).not.toMatch(/\..{2}\.html$/) }) it("renders narrow matches", () => { @@ -226,4 +229,26 @@ describe.concurrent("Concept", () => { screen.getByRole("link", { name: /just-another-scheme/i }) ).toHaveAttribute("href", "http://just-another-scheme.org/") }) + + it("renders deprecated notice, if concept is deprecaed", () => { + render() + expect( + screen.getByRole("heading", { name: /Deprecated/i }) + ).toBeInTheDocument() + }) + + it("adds a isReplacedBy notice if concept is replaced", () => { + render() + expect( + screen.getByRole("heading", { name: /is replaced by/i }) + ).toBeInTheDocument() + const linkElement = screen.getByRole("link", { + name: "http://w3id.org/replacement", + }) // Adjust the query to match your link + const href = linkElement.getAttribute("href") + + // Assert the URL ends with .html but not .xx.html + expect(href).toMatch(/\.html$/) + expect(href).not.toMatch(/\..{2}\.html$/) + }) }) diff --git a/test/data/pageContext.js b/test/data/pageContext.js index e033f1d..116a23f 100644 --- a/test/data/pageContext.js +++ b/test/data/pageContext.js @@ -14,6 +14,20 @@ const concept2 = { inScheme: [{ id: "http://w3id.org/" }], } +const topConceptDeprecated = { + id: "http://w3id.org/c1", + type: "Concept", + deprecated: true, + isReplacedBy: [{ id: "http://w3id.org/replacement" }], + hub: "https://test.skohub.io/hub", + prefLabel: { + de: "Konzept 1", + en: "Concept 1", + }, + narrower: [concept2], + topConceptOf: null, +} + export const topConcept = { id: "http://w3id.org/c1", type: "Concept", @@ -98,6 +112,28 @@ export const ConceptPC = { ], } +export const ConceptPCDeprecated = { + node: topConceptDeprecated, + language: "de", + collections: [ + { + id: "http://w3id.org/collection", + prefLabel: { de: "Meine Collection", en: "My Collection" }, + member: [topConcept, concept2], + }, + ], +} + +export const ConceptSchemeDeprecated = { + id: "http://w3id.org/", + type: "ConceptScheme", + title: { + de: "Test Vokabular", + en: "Test Vocabulary", + }, + hasTopConcept: [topConceptDeprecated], +} + export const ConceptScheme = { id: "http://w3id.org/", type: "ConceptScheme", diff --git a/test/nestedList.test.jsx b/test/nestedList.test.jsx index 3eb33c7..5f6e810 100644 --- a/test/nestedList.test.jsx +++ b/test/nestedList.test.jsx @@ -2,7 +2,7 @@ import { beforeEach, describe, expect, it, vi } from "vitest" import { render, screen } from "@testing-library/react" import React from "react" import NestedList from "../src/components/nestedList" -import { ConceptScheme } from "./data/pageContext" +import { ConceptScheme, ConceptSchemeDeprecated } from "./data/pageContext" import userEvent from "@testing-library/user-event" import * as Gatsby from "gatsby" import { mockConfig } from "./mocks/mockConfig" @@ -59,4 +59,19 @@ describe("Nested List", () => { await user.click(screen.getByRole("button", { expanded: true })) expect(screen.getByRole("button", { expanded: false })) }) + + it("shows deprecation notice for deprecated concepts", () => { + render( + + ) + expect( + screen.getByRole("link", { name: "(DEPRECATED) Konzept 1" }) + ).toBeInTheDocument() + }) })