diff --git a/package-lock.json b/package-lock.json index b31d3012..85da8d43 100644 --- a/package-lock.json +++ b/package-lock.json @@ -54,6 +54,7 @@ "@testing-library/jest-dom": "^6.4.6", "@testing-library/react": "^16.0.0", "@testing-library/user-event": "^14.5.2", + "@types/css-font-loading-module": "^0.0.14", "@types/cytoscape": "^3.21.5", "@types/jsonpath": "^0.2.4", "@types/leaflet": "^1.9.12", @@ -1920,6 +1921,12 @@ "@pixi/core": "7.4.2" } }, + "node_modules/@pixi/assets/node_modules/@types/css-font-loading-module": { + "version": "0.0.12", + "resolved": "https://registry.npmjs.org/@types/css-font-loading-module/-/css-font-loading-module-0.0.12.tgz", + "integrity": "sha512-x2tZZYkSxXqWvTDgveSynfjq/T2HyiZHXb00j/+gy19yp70PHCizM48XFdjBCWH7eHBD0R5i/pw9yMBP/BH5uA==", + "license": "MIT" + }, "node_modules/@pixi/color": { "version": "7.4.2", "resolved": "https://registry.npmjs.org/@pixi/color/-/color-7.4.2.tgz", @@ -2166,6 +2173,12 @@ "ismobilejs": "^1.1.0" } }, + "node_modules/@pixi/settings/node_modules/@types/css-font-loading-module": { + "version": "0.0.12", + "resolved": "https://registry.npmjs.org/@types/css-font-loading-module/-/css-font-loading-module-0.0.12.tgz", + "integrity": "sha512-x2tZZYkSxXqWvTDgveSynfjq/T2HyiZHXb00j/+gy19yp70PHCizM48XFdjBCWH7eHBD0R5i/pw9yMBP/BH5uA==", + "license": "MIT" + }, "node_modules/@pixi/sprite": { "version": "7.4.2", "resolved": "https://registry.npmjs.org/@pixi/sprite/-/sprite-7.4.2.tgz", @@ -2871,9 +2884,10 @@ "license": "MIT" }, "node_modules/@types/css-font-loading-module": { - "version": "0.0.12", - "resolved": "https://registry.npmjs.org/@types/css-font-loading-module/-/css-font-loading-module-0.0.12.tgz", - "integrity": "sha512-x2tZZYkSxXqWvTDgveSynfjq/T2HyiZHXb00j/+gy19yp70PHCizM48XFdjBCWH7eHBD0R5i/pw9yMBP/BH5uA==", + "version": "0.0.14", + "resolved": "https://registry.npmjs.org/@types/css-font-loading-module/-/css-font-loading-module-0.0.14.tgz", + "integrity": "sha512-+EwJ/RW2vPqbYn0JXRHy593huPCtgmLF/kg57iLK9KUn6neTqGGOTZ0CbssP8Uou/gqT/5XmWKQ8A7ve7xNV6A==", + "dev": true, "license": "MIT" }, "node_modules/@types/cytoscape": { diff --git a/package.json b/package.json index 1853f67a..cdcd0eed 100644 --- a/package.json +++ b/package.json @@ -58,6 +58,7 @@ "@testing-library/jest-dom": "^6.4.6", "@testing-library/react": "^16.0.0", "@testing-library/user-event": "^14.5.2", + "@types/css-font-loading-module": "^0.0.14", "@types/cytoscape": "^3.21.5", "@types/jsonpath": "^0.2.4", "@types/leaflet": "^1.9.12", diff --git a/src/app/Store.ts b/src/app/Store.ts index dc9f2eb4..838df07d 100644 --- a/src/app/Store.ts +++ b/src/app/Store.ts @@ -5,10 +5,11 @@ import type { PreloadedStateShapeFromReducersMapObject } from '@reduxjs/toolkit' /* Import Redux Slices */ import AnnotateReducer from 'redux-store/AnnotateSlice'; import BootReducder from 'redux-store/BootSlice'; -import GlobalReducer from 'redux-store/GlobalSlice'; -import SearchReducer from 'redux-store/SearchSlice'; import DigitalMediaReducer from 'redux-store/DigitalMediaSlice'; import DigitalSpecimenReducer from 'redux-store/DigitalSpecimenSlice'; +import GlobalReducer from 'redux-store/GlobalSlice'; +import SearchReducer from 'redux-store/SearchSlice'; +import TourReducer from 'redux-store/TourSlice'; const rootReducer = combineReducers({ @@ -18,6 +19,7 @@ const rootReducer = combineReducers({ digitalSpecimen: DigitalSpecimenReducer, global: GlobalReducer, search: SearchReducer, + tour: TourReducer }); export const setupStore = (preloadedState?: PreloadedStateShapeFromReducersMapObject) => { diff --git a/src/app/types/Annotation.d.ts b/src/app/types/Annotation.d.ts index 33bf556f..146b54bb 100644 --- a/src/app/types/Annotation.d.ts +++ b/src/app/types/Annotation.d.ts @@ -20,7 +20,7 @@ export interface Annotation { /** * The handle of the annotation. It is a unique identifier for the annotation. It is composed of the handle of the document followed by a slash and a unique identifier for the annotation. */ - "dcterms:identifier"?: string; + "dcterms:identifier": string; /** * The DOI to the FDO type of the object */ diff --git a/src/app/types/ChronometricAge.d.ts b/src/app/types/ChronometricAge.d.ts index 9bd6c4bb..d47afe1c 100644 --- a/src/app/types/ChronometricAge.d.ts +++ b/src/app/types/ChronometricAge.d.ts @@ -46,6 +46,10 @@ export interface ChronometricAge { * The reference system associated with the latestChronometricAge */ "chrono:latestChronometricAgeReferenceSystem"?: string; + /** + * A description of or reference to the methods used to determine the chronometric age. + */ + "chrono:chronometricAgeProtocol"?: string; /** * The temporal uncertainty of the earliestChronometricAge and latestChronometicAge in years */ @@ -78,4 +82,174 @@ export interface ChronometricAge { * Notes or comments about the ChronometricAge */ "chrono:chronometricAgeRemarks"?: string; + /** + * The agent(s) involved in the determination of the chronometric age, uses `ods:Agent` + */ + "ods:hasAgents"?: Agent[]; +} +export interface Agent { + /** + * The identifier for the Agent object + */ + "@id"?: string; + /** + * The type of the agent, the prov ontology is only used in the prov-o ods:CreateUpdateTombstoneEvent + */ + "@type": + | "schema:Person" + | "schema:Organization" + | "schema:SoftwareApplication" + | "prov:Person" + | "prov:SoftwareAgent"; + /** + * The primary unique identifier of the Agent object. All identifiers will also be added to the ods:hasIdentifiers array + */ + "schema:identifier"?: string; + /** + * Full name of the agent + */ + "schema:name"?: string; + /** + * Contains all roles associated with the agent in the context of the Digital Object. Should always contain at least one role + * + * @minItems 1 + */ + "ods:hasRoles"?: [ + { + /** + * The identifier for the agent role, preferably a URL to a controlled vocabulary + */ + "@id"?: string; + /** + * The type of the object, in this case schema:Role + */ + "@type": "schema:Role"; + /** + * The category that best matches the nature of a role of an Agent + */ + "schema:roleName": string; + /** + * Date the agent began the role + */ + "schema:startDate"?: string; + /** + * Date the agent ended the role + */ + "schema:endDate"?: string; + /** + * Can be used to indicate the order of importance when there are multiple agents with the same role. Lower order means higher importance. + */ + "schema:position"?: number; + }, + ...{ + /** + * The identifier for the agent role, preferably a URL to a controlled vocabulary + */ + "@id"?: string; + /** + * The type of the object, in this case schema:Role + */ + "@type": "schema:Role"; + /** + * The category that best matches the nature of a role of an Agent + */ + "schema:roleName": string; + /** + * Date the agent began the role + */ + "schema:startDate"?: string; + /** + * Date the agent ended the role + */ + "schema:endDate"?: string; + /** + * Can be used to indicate the order of importance when there are multiple agents with the same role. Lower order means higher importance. + */ + "schema:position"?: number; + }[] + ]; + /** + * Email of the agent + */ + "schema:email"?: string; + /** + * URL to a website of the agent + */ + "schema:url"?: string; + /** + * Contains all identifiers associated with the agent + */ + "ods:hasIdentifiers"?: Identifier[]; +} +/** + * Object used to describe identifiers of a Digital Object, based on https://rs.gbif.org/extension/gbif/1.0/identifier.xml but includes ods specific terms + */ +export interface Identifier { + /** + * The identifier for the Identifier object. + */ + "@id"?: string; + /** + * The type of the digital object, in this case a ods:Identifier + */ + "@type": "ods:Identifier"; + /** + * A name for the identifier + */ + "dcterms:title": string; + /** + * The type of the value in the `dcterms:identifier` field + */ + "dcterms:type"?: + | "ARK" + | "arXiv" + | "bibcode" + | "DOI" + | "EAN13" + | "EISSN" + | "Handle" + | "IGSN" + | "ISBN" + | "ISSN" + | "ISTC" + | "LISSN" + | "LSID" + | "PMID" + | "PURL" + | "UPC" + | "URL" + | "URN" + | "w3id" + | "UUID" + | "Other" + | "Locally unique identifier"; + /** + * The value for the identifier + */ + "dcterms:identifier": string; + /** + * All possible mime types of content that can be returned by identifier in case the identifier is resolvable. Plain UUIDs for example do not have a dc:format return type, as they are not resolvable on their own. For a list of MIME types see the list maintained by IANA: http://www.iana.org/assignments/media-types/index.html, in particular the text http://www.iana.org/assignments/media-types/text/ and application http://www.iana.org/assignments/media-types/application/ types. Frequently used values are text/html, text/xml, application/rdf+xml, application/json + */ + "dcterms:format"?: string[]; + /** + * Additional keywords that the publisher may prefer to be attached to the identifier + */ + "dcterms:subject"?: string[]; + /** + * Indicates whether the identifier is part of the physical label + */ + "ods:isPartOfLabel"?: boolean; + /** + * Indicates whether the identifier is a persistent identifier + */ + "ods:gupriLevel"?: + | "LocallyUniqueStable" + | "GloballyUniqueStable" + | "GloballyUniqueStableResolvable" + | "GloballyUniqueStablePersistentResolvable" + | "GloballyUniqueStablePersistentResolvableFDOCompliant"; + /** + * Indicates the status of the identifier + */ + "ods:identifierStatus"?: "Preferred" | "Alternative" | "Superseded"; } diff --git a/src/app/types/DigitalSpecimen.d.ts b/src/app/types/DigitalSpecimen.d.ts index a7e8a1ba..0cfe277d 100644 --- a/src/app/types/DigitalSpecimen.d.ts +++ b/src/app/types/DigitalSpecimen.d.ts @@ -241,11 +241,15 @@ export interface DigitalSpecimen { /** * Contains all agents that are connected to the specimen. Agents that are part of a specific part of the specimen should be added there. For example a Collector is connected to the CollectingEvent so is add in the event */ - "ods:hasAgents"?: Agent7[]; + "ods:hasAgents"?: Agent8[]; /** * Contains information about any events that occurred specifically on the specimen part, for example a sampling event */ "ods:hasEvents"?: Event1[]; + /** + * Contains information about the chronometric age of the specimen part + */ + "ods:hasChronometricAges"?: ChronometricAge1[]; "ods:hasTombstoneMetadata"?: TombstoneMetadata; } export interface SpecimenPart { @@ -748,6 +752,10 @@ export interface TaxonIdentification { * A Hyper Text Markup Language (HTML) representation of the scientific name. Includes correct formatting of the name. */ "ods:scientificNameHTMLLabel"?: string; + /** + * A Hyper Text Markup Language (HTML) representation of the genus name. Includes correct formatting of the name. + */ + "ods:genusHTMLLabel"?: string; /** * The authorship information for the dwc:scientificName formatted according to the conventions of the applicable dwc:nomenclaturalCode */ @@ -1958,6 +1966,10 @@ export interface ChronometricAge { * The reference system associated with the latestChronometricAge */ "chrono:latestChronometricAgeReferenceSystem"?: string; + /** + * A description of or reference to the methods used to determine the chronometric age. + */ + "chrono:chronometricAgeProtocol"?: string; /** * The temporal uncertainty of the earliestChronometricAge and latestChronometicAge in years */ @@ -1990,6 +2002,104 @@ export interface ChronometricAge { * Notes or comments about the ChronometricAge */ "chrono:chronometricAgeRemarks"?: string; + /** + * The agent(s) involved in the determination of the chronometric age, uses `ods:Agent` + */ + "ods:hasAgents"?: Agent6[]; +} +export interface Agent6 { + /** + * The identifier for the Agent object + */ + "@id"?: string; + /** + * The type of the agent, the prov ontology is only used in the prov-o ods:CreateUpdateTombstoneEvent + */ + "@type": + | "schema:Person" + | "schema:Organization" + | "schema:SoftwareApplication" + | "prov:Person" + | "prov:SoftwareAgent"; + /** + * The primary unique identifier of the Agent object. All identifiers will also be added to the ods:hasIdentifiers array + */ + "schema:identifier"?: string; + /** + * Full name of the agent + */ + "schema:name"?: string; + /** + * Contains all roles associated with the agent in the context of the Digital Object. Should always contain at least one role + * + * @minItems 1 + */ + "ods:hasRoles"?: [ + { + /** + * The identifier for the agent role, preferably a URL to a controlled vocabulary + */ + "@id"?: string; + /** + * The type of the object, in this case schema:Role + */ + "@type": "schema:Role"; + /** + * The category that best matches the nature of a role of an Agent + */ + "schema:roleName": string; + /** + * Date the agent began the role + */ + "schema:startDate"?: string; + /** + * Date the agent ended the role + */ + "schema:endDate"?: string; + /** + * Can be used to indicate the order of importance when there are multiple agents with the same role. Lower order means higher importance. + */ + "schema:position"?: number; + }, + ...{ + /** + * The identifier for the agent role, preferably a URL to a controlled vocabulary + */ + "@id"?: string; + /** + * The type of the object, in this case schema:Role + */ + "@type": "schema:Role"; + /** + * The category that best matches the nature of a role of an Agent + */ + "schema:roleName": string; + /** + * Date the agent began the role + */ + "schema:startDate"?: string; + /** + * Date the agent ended the role + */ + "schema:endDate"?: string; + /** + * Can be used to indicate the order of importance when there are multiple agents with the same role. Lower order means higher importance. + */ + "schema:position"?: number; + }[] + ]; + /** + * Email of the agent + */ + "schema:email"?: string; + /** + * URL to a website of the agent + */ + "schema:url"?: string; + /** + * Contains all identifiers associated with the agent + */ + "ods:hasIdentifiers"?: Identifier[]; } export interface Assertion2 { /** @@ -2100,9 +2210,9 @@ export interface EntityRelationship { /** * The agent(s) who created the entityRelationship, contains an ods:Agent object */ - "ods:hasAgents"?: Agent6[]; + "ods:hasAgents"?: Agent7[]; } -export interface Agent6 { +export interface Agent7 { /** * The identifier for the Agent object */ @@ -2371,7 +2481,7 @@ export interface Citation2 { */ "ods:hasAgents"?: Agent1[]; } -export interface Agent7 { +export interface Agent8 { /** * The identifier for the Agent object */ @@ -2599,6 +2709,88 @@ export interface Event1 { "ods:hasAgents"?: Agent3[]; "ods:hasLocation"?: Location; } +export interface ChronometricAge1 { + /** + * The identifier for the Chronometric Age object. + */ + "@id"?: string; + /** + * The type of the digital object, in this case ods:ChronometricAge + */ + "@type": "ods:ChronometricAge"; + /** + * An identifier for the set of information associated with a ChronometricAge + */ + "chrono:chronometricAgeID"?: string; + /** + * The verbatim age for a specimen, whether reported by a dating assay, associated references, or legacy information + */ + "chrono:verbatimChronometricAge"?: string; + /** + * The output of a dating assay before it is calibrated into an age using a specific conversion protocol. + */ + "chrono:uncalibratedChronometricAge"?: string; + /** + * The method used for converting the uncalibratedChronometricAge into a chronometric age in years, as captured in the earliestChronometricAge, earliestChronometricAgeReferenceSystem, latestChronometricAge, and latestChronometricAgeReferenceSystem fields. + */ + "chrono:chronometricAgeConversionProtocol"?: string; + /** + * The maximum/earliest/oldest possible age of a specimen as determined by a dating method + */ + "chrono:earliestChronometricAge"?: number; + /** + * The reference system associated with the earliestChronometricAge + */ + "chrono:earliestChronometricAgeReferenceSystem"?: string; + /** + * The minimum/latest/youngest possible age of a specimen as determined by a dating method + */ + "chrono:latestChronometricAge"?: number; + /** + * The reference system associated with the latestChronometricAge + */ + "chrono:latestChronometricAgeReferenceSystem"?: string; + /** + * A description of or reference to the methods used to determine the chronometric age. + */ + "chrono:chronometricAgeProtocol"?: string; + /** + * The temporal uncertainty of the earliestChronometricAge and latestChronometicAge in years + */ + "chrono:chronometricAgeUncertaintyInYears"?: number; + /** + * The method used to generate the value of chronometricAgeUncertaintyInYears + */ + "chrono:chronometricAgeUncertaintyMethod"?: string; + /** + * A description of the material on which the chronometricAgeProtocol was actually performed, if known. + */ + "chrono:materialDated"?: string; + /** + * An identifier for the MaterialSample on which the chronometricAgeProtocol was performed, if applicable + */ + "chrono:materialDatedID"?: string; + /** + * The relationship of the materialDated to the subject of the ChronometricAge record, from which the ChronometricAge of the subject is inferred + */ + "chrono:materialDatedRelationship"?: string; + /** + * The date on which the ChronometricAge was determined + */ + "chrono:chronometricAgeDeterminedDate"?: string; + /** + * A list (concatenated and separated) of identifiers (publication, bibliographic reference, global unique identifier, URI) of literature associated with the ChronometricAge. + */ + "chrono:chronometricAgeReferences"?: string; + /** + * Notes or comments about the ChronometricAge + */ + "chrono:chronometricAgeRemarks"?: string; + /** + * The agent(s) involved in the determination of the chronometric age, uses `ods:Agent` + */ + "ods:hasAgents"?: Agent6[]; +} /** * Object containing the tombstone metadata of the object. Only present when ods:status is ods:Tombstone */ @@ -2620,7 +2812,7 @@ export interface TombstoneMetadata { * * @minItems 1 */ - "ods:hasAgents": [Agent8, ...Agent8[]]; + "ods:hasAgents": [Agent9, ...Agent9[]]; /** * The PIDs of the object the tombstoned object is related to */ @@ -2643,7 +2835,7 @@ export interface TombstoneMetadata { "ods:relationshipType": string; }[]; } -export interface Agent8 { +export interface Agent9 { /** * The identifier for the Agent object */ diff --git a/src/app/types/Identification.d.ts b/src/app/types/Identification.d.ts index c197f761..f87b037b 100644 --- a/src/app/types/Identification.d.ts +++ b/src/app/types/Identification.d.ts @@ -393,6 +393,10 @@ export interface TaxonIdentification { * A Hyper Text Markup Language (HTML) representation of the scientific name. Includes correct formatting of the name. */ "ods:scientificNameHTMLLabel"?: string; + /** + * A Hyper Text Markup Language (HTML) representation of the genus name. Includes correct formatting of the name. + */ + "ods:genusHTMLLabel"?: string; /** * The authorship information for the dwc:scientificName formatted according to the conventions of the applicable dwc:nomenclaturalCode */ diff --git a/src/app/types/SpecimenPart.d.ts b/src/app/types/SpecimenPart.d.ts index 4fd467ab..1c3e7bb1 100644 --- a/src/app/types/SpecimenPart.d.ts +++ b/src/app/types/SpecimenPart.d.ts @@ -505,6 +505,10 @@ export interface TaxonIdentification { * A Hyper Text Markup Language (HTML) representation of the scientific name. Includes correct formatting of the name. */ "ods:scientificNameHTMLLabel"?: string; + /** + * A Hyper Text Markup Language (HTML) representation of the genus name. Includes correct formatting of the name. + */ + "ods:genusHTMLLabel"?: string; /** * The authorship information for the dwc:scientificName formatted according to the conventions of the applicable dwc:nomenclaturalCode */ @@ -1715,6 +1719,10 @@ export interface ChronometricAge { * The reference system associated with the latestChronometricAge */ "chrono:latestChronometricAgeReferenceSystem"?: string; + /** + * A description of or reference to the methods used to determine the chronometric age. + */ + "chrono:chronometricAgeProtocol"?: string; /** * The temporal uncertainty of the earliestChronometricAge and latestChronometicAge in years */ @@ -1747,4 +1755,102 @@ export interface ChronometricAge { * Notes or comments about the ChronometricAge */ "chrono:chronometricAgeRemarks"?: string; + /** + * The agent(s) involved in the determination of the chronometric age, uses `ods:Agent` + */ + "ods:hasAgents"?: Agent6[]; +} +export interface Agent6 { + /** + * The identifier for the Agent object + */ + "@id"?: string; + /** + * The type of the agent, the prov ontology is only used in the prov-o ods:CreateUpdateTombstoneEvent + */ + "@type": + | "schema:Person" + | "schema:Organization" + | "schema:SoftwareApplication" + | "prov:Person" + | "prov:SoftwareAgent"; + /** + * The primary unique identifier of the Agent object. All identifiers will also be added to the ods:hasIdentifiers array + */ + "schema:identifier"?: string; + /** + * Full name of the agent + */ + "schema:name"?: string; + /** + * Contains all roles associated with the agent in the context of the Digital Object. Should always contain at least one role + * + * @minItems 1 + */ + "ods:hasRoles"?: [ + { + /** + * The identifier for the agent role, preferably a URL to a controlled vocabulary + */ + "@id"?: string; + /** + * The type of the object, in this case schema:Role + */ + "@type": "schema:Role"; + /** + * The category that best matches the nature of a role of an Agent + */ + "schema:roleName": string; + /** + * Date the agent began the role + */ + "schema:startDate"?: string; + /** + * Date the agent ended the role + */ + "schema:endDate"?: string; + /** + * Can be used to indicate the order of importance when there are multiple agents with the same role. Lower order means higher importance. + */ + "schema:position"?: number; + }, + ...{ + /** + * The identifier for the agent role, preferably a URL to a controlled vocabulary + */ + "@id"?: string; + /** + * The type of the object, in this case schema:Role + */ + "@type": "schema:Role"; + /** + * The category that best matches the nature of a role of an Agent + */ + "schema:roleName": string; + /** + * Date the agent began the role + */ + "schema:startDate"?: string; + /** + * Date the agent ended the role + */ + "schema:endDate"?: string; + /** + * Can be used to indicate the order of importance when there are multiple agents with the same role. Lower order means higher importance. + */ + "schema:position"?: number; + }[] + ]; + /** + * Email of the agent + */ + "schema:email"?: string; + /** + * URL to a website of the agent + */ + "schema:url"?: string; + /** + * Contains all identifiers associated with the agent + */ + "ods:hasIdentifiers"?: Identifier[]; } diff --git a/src/app/types/TaxonIdentification.d.ts b/src/app/types/TaxonIdentification.d.ts index 49f9f298..7c89fff9 100644 --- a/src/app/types/TaxonIdentification.d.ts +++ b/src/app/types/TaxonIdentification.d.ts @@ -30,6 +30,10 @@ export interface TaxonIdentification { * A Hyper Text Markup Language (HTML) representation of the scientific name. Includes correct formatting of the name. */ "ods:scientificNameHTMLLabel"?: string; + /** + * A Hyper Text Markup Language (HTML) representation of the genus name. Includes correct formatting of the name. + */ + "ods:genusHTMLLabel"?: string; /** * The authorship information for the dwc:scientificName formatted according to the conventions of the applicable dwc:nomenclaturalCode */ diff --git a/src/app/utilities/TourUtilities.ts b/src/app/utilities/TourUtilities.ts new file mode 100644 index 00000000..cf207941 --- /dev/null +++ b/src/app/utilities/TourUtilities.ts @@ -0,0 +1,31 @@ +/* Import Types */ +import { Dict } from "app/Types"; + + +/* Utilities associated with the tour functionality */ + + +/** + * Function for handling the annotation wizard tour trigger + * @param tourAnnotationWizardFormValues The given annotation wizard form values from the tour + * @param SetFieldValue Function to set the value of a form field in the annotation form + */ +const AnnotationWizardTourTrigger = (tourAnnotationWizardFormValues: Dict, SetFieldValue?: Function) => { + const { jsonPath, annotationValues } = tourAnnotationWizardFormValues; + + /* Set tour class */ + SetFieldValue?.('class', tourAnnotationWizardFormValues.class); + + /* Set motivation */ + SetFieldValue?.('motivation', 'ods:adding'); + + /* Set JSON path */ + SetFieldValue?.('jsonPath', jsonPath); + + /* Reset annotation values */ + SetFieldValue?.('annotationValues', annotationValues); +}; + +export { + AnnotationWizardTourTrigger +}; \ No newline at end of file diff --git a/src/components/digitalMedia/DigitalMedia.tsx b/src/components/digitalMedia/DigitalMedia.tsx index 8b34fd9c..b9b90c9d 100644 --- a/src/components/digitalMedia/DigitalMedia.tsx +++ b/src/components/digitalMedia/DigitalMedia.tsx @@ -103,6 +103,7 @@ const DigitalMedia = () => { setAnnotationMode(!annotationMode)} SetAnnotoriousMode={(mode: string) => setAnnotoriousMode(mode)} /> diff --git a/src/components/digitalMedia/components/topBar/TopBar.tsx b/src/components/digitalMedia/components/topBar/TopBar.tsx index 4ab65333..112fc7b6 100644 --- a/src/components/digitalMedia/components/topBar/TopBar.tsx +++ b/src/components/digitalMedia/components/topBar/TopBar.tsx @@ -24,6 +24,7 @@ type Props = { digitalMedia: DigitalMedia, annotationMode: boolean, annotoriousMode: string, + selectedTabIndex: number, ToggleAnnotationSidePanel: Function, SetAnnotoriousMode: Function }; @@ -34,12 +35,13 @@ type Props = { * @param digitalSpecimen The selected digital specimen * @param annotationMode Boolean that indicates if the annotation mode is toggled * @param annotoriousMode String indicating the Annotorious mode + * @param selectedTabIndex The index of the selected content block tab * @param ToggleAnnotationSidePanel Function to toggle the annotation side panel * @param SetAnnotoriousMode Function to set the Annotorious mode * @returns JSX Component */ const TopBar = (props: Props) => { - const { digitalMedia, annotationMode, annotoriousMode, ToggleAnnotationSidePanel, SetAnnotoriousMode } = props; + const { digitalMedia, annotationMode, annotoriousMode, selectedTabIndex, ToggleAnnotationSidePanel, SetAnnotoriousMode } = props; /* Hooks */ const navigate = useNavigate(); @@ -140,8 +142,8 @@ const TopBar = (props: Props) => { } - {KeycloakService.IsLoggedIn() && - + {(KeycloakService.IsLoggedIn() && !selectedTabIndex) && + } - + { const [digitalSpecimenDigitalMedia, setDigitalSpecimenDigitalMedia] = useState(); const [annotationMode, setAnnotationMode] = useState(false); const [selectedTabIndex, setSelectedTabIndex] = useState(0); - const tourTopics: TourTopic[] = [{ - name: 'digitalSpecimen', - title: 'About this page' - }]; + const tourTopics: TourTopic[] = [ + { + name: 'digitalSpecimen', + title: 'About This Page' + }, + { + name: 'annotate', + title: 'Using Annotations' + }, + { + name: 'mas', + title: 'Machine Annotation Services' + } + ]; /* OnLoad, fetch digital specimen data */ fetch.FetchMultiple({ @@ -191,6 +203,8 @@ const DigitalSpecimen = () => { + + ); }; diff --git a/src/components/digitalSpecimen/components/contentBlock/components/DigitalSpecimenOverview.tsx b/src/components/digitalSpecimen/components/contentBlock/components/DigitalSpecimenOverview.tsx index dbb83b65..80d7e17f 100644 --- a/src/components/digitalSpecimen/components/contentBlock/components/DigitalSpecimenOverview.tsx +++ b/src/components/digitalSpecimen/components/contentBlock/components/DigitalSpecimenOverview.tsx @@ -36,7 +36,7 @@ const DigitalSpecimenOverview = (props: Props) => { const [copyMessage, setCopyMessage] = useState('Copy'); const acceptedIdentification = digitalSpecimen['ods:hasIdentifications']?.find(identification => identification['ods:isVerifiedIdentification']); const collectors: string[] = []; - const collectionEvent: Event | undefined = digitalSpecimen['ods:hasEvents']?.find(event => event['dwc:eventType'] === 'Collection'); + const collectionEvent: Event | undefined = digitalSpecimen['ods:hasEvents']?.find(event => event['dwc:eventType'] === 'Collecting Event'); const topicDisciplinesWithIdentifications: string[] = [ 'Anthropology', 'Botany', @@ -48,11 +48,16 @@ const DigitalSpecimenOverview = (props: Props) => { ]; /* Construct collectors array */ - digitalSpecimen['ods:hasAgents']?.filter(agent => agent['ods:hasRoles']?.find(role => role['schema:roleName'] === 'collector')).forEach(agent => { - if (agent['schema:name']) { - collectors.push(agent['schema:name']); - } - }); + digitalSpecimen['ods:hasEvents']?.filter( + event => event['ods:hasAgents']?.filter( + agent => agent['ods:hasRoles']?.find( + role => role['schema:roleName'] === 'collector') + ).forEach(agent => { + if (agent['schema:name']) { + collectors.push(agent['schema:name']); + } + }) + ); /** * Function to craft a citation string for this digital specimen @@ -98,7 +103,7 @@ const DigitalSpecimenOverview = (props: Props) => {

- Collection date: + Collection date: {collectionEvent?.['dwc:eventDate']}

@@ -107,7 +112,7 @@ const DigitalSpecimenOverview = (props: Props) => {

- Country: + Country: {collectionEvent?.['ods:hasLocation']?.['dwc:country']}

@@ -116,7 +121,7 @@ const DigitalSpecimenOverview = (props: Props) => {

- Locality: + Locality: {collectionEvent?.['ods:hasLocation']?.['dwc:locality']}

diff --git a/src/components/digitalSpecimen/tourSteps/AnnotateTourSteps.tsx b/src/components/digitalSpecimen/tourSteps/AnnotateTourSteps.tsx new file mode 100644 index 00000000..8f3268f3 --- /dev/null +++ b/src/components/digitalSpecimen/tourSteps/AnnotateTourSteps.tsx @@ -0,0 +1,229 @@ +/* Import Dependencies */ +import { Steps } from 'intro.js-react'; +import KeycloakService from 'app/Keycloak'; +import { useRef, useState } from 'react'; + +/* Import Config */ +import StepsConfig from 'app/config/StepsConfig'; + +/* Import Hooks */ +import { useAppSelector, useAppDispatch, useTrigger } from 'app/Hooks'; + +/* Import Store */ +import { setAnnotationTarget } from 'redux-store/AnnotateSlice'; +import { + setAnnotationWizardDummyAnnotation, setAnnotationWizardSelectedIndex, + setAnnotationWizardToggle, setAnnotationWizardFormValues +} from 'redux-store/TourSlice'; +import { getTourTopic, setTourTopic } from 'redux-store/GlobalSlice'; + +/* Import Types */ +import { Annotation } from 'app/types/Annotation'; +import { Dict } from 'app/Types'; + +/* Import Sources */ +import DigitalSpecimenTourStepsText from 'sources/tourText/digitalSpecimen.json'; + + +/* Props Type */ +type Props = { + SetAnnotationMode: Function +}; + + +/** + * Component that renders the tour steps for the annotation tour on the digital specimen page + * @param SetAnnotationMode Function to set the annotation mode + * @returns JSX Component + */ +const AnnotateTourSteps = (props: Props) => { + const { SetAnnotationMode } = props; + + /* Hooks */ + const dispatch = useAppDispatch(); + const trigger = useTrigger(); + const stepsRef = useRef(null); + + /* Base variables */ + const tourTopic = useAppSelector(getTourTopic); + const annotateSteps = DigitalSpecimenTourStepsText.annotate; + const { options } = StepsConfig(); + const [steps, setSteps] = useState<{ + intro: string, + element?: string + }[]>([]); + const stepsConfig: Dict = { + annotationModeOff: [1, 2, 3], + wizardOn: [7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18], + annotationTarget: [10, 11, 12, 13, 14, 15, 16, 17, 18], + formSteps: { + 0: [9, 10, 11], + 1: [12, 13, 14], + 2: [15, 16, 17, 18], + 3: [19] + }, + selectedIndex: { + 0: [7, 8, 9], + 1: [10, 11, 12], + 2: [13, 14, 15, 16], + 3: [17, 18] + }, + dummyAnnotation: [19] + }; + const dummyAnnotation: Annotation = { + '@id': 'dummyAnnotation', + '@type': 'ods:Annotation', + "dcterms:identifier": 'dummyAnnotation', + 'ods:fdoType': 'https://doi.org/21.T11148/cf458ca9ee1d44a5608f', + 'ods:status': 'Active', + 'ods:version': 1, + 'oa:motivation': 'ods:adding', + 'oa:motivatedBy': '', + 'oa:hasTarget': { + '@id': 'dummyAnnotationTarget', + '@type': 'ods:DigitalSpecimen', + 'dcterms:identifier': 'dummyAnnotationTarget', + 'ods:fdoType': 'https://doi.org/21.T11148/894b1e6cad57e921764e', + 'oa:hasSelector': { + '@type': 'ods:ClassSelector', + 'ods:class': "$['ods:hasEntityRelationships']" + } + }, + 'oa:hasBody': { + '@type': 'oa:TextualBody', + 'oa:value': [ + '{"dwc:relationshipOfResource":"GeoCASe","ods:relatedResourceURI":"https://geocase.eu/specimen/GIT338-118"}' + ] + }, + 'dcterms:creator': { + '@id': 'dummyAnnotationCreator', + '@type': 'prov:SoftwareAgent', + 'schema:identifier': 'dummyAnnotationCreator', + 'schema:name': 'Tour Manager' + }, + 'dcterms:created': '2024-11-15T08:56:50.758Z', + 'dcterms:modified': '2024-11-15T08:56:50.758Z', + 'dcterms:issued': '2024-11-15T08:56:50.758Z', + 'as:generator': { + '@id': 'dummyAnnotationGenerator', + '@type': 'prov:SoftwareAgent', + 'schema:identifier': 'dummyAnnotationGenerator' + } + }; + + /* Construct Intro.js steps for annotation functionality on digital specimen page */ + trigger.SetTrigger(() => { + const steps: { + intro: string, + element?: string + }[] = []; + + annotateSteps.forEach((step, index) => { + if ([0, 1, 2, 3, 4, 5, 20].includes(index) || (KeycloakService.IsLoggedIn() && KeycloakService.GetParsedToken()?.orcid)) { + steps.push({ + intro: step, + element: `.tourAnnotate${index + 1}` + }); + } + }); + + setSteps(steps); + }, []); + + /** + * Function to construct the form values, based upon the current step index + * @param nextStep The index of the next step in the tour + * @returns Form values object + */ + const ConstructFormValues = (nextStep: number): Dict => ({ + class: { + label: 'Entity Relationships', + value: "$['ods:hasEntityRelationships']" + }, + annotationValues: nextStep > 14 ? { + "$'ods:hasEntityRelationships'_999": { + "dwc:relationshipOfResource": 'GeoCASe', + "ods:relatedResourceURI": 'https://geocase.eu/specimen/GIT338-118' + } + } : {}, + motivation: nextStep > 11 ? 'ods:adding' : '', + jsonPath: nextStep > 11 ? "$['ods:hasEntityRelationships'][999]" : '' + }); + + /** + * Function that checks what to do on a step change + * @param nextIndex The next (selected) index in the step chain + * @param resolve Function to resolve the step promise + */ + const OnStepChange = async (nextIndex: number, resolve: Function) => { + /* Handler for setting annotation mode on or off */ + SetAnnotationMode(!stepsConfig.annotationModeOff.includes(nextIndex)); + + /* Handler for setting the annotation wizard toggle on or off */ + dispatch(setAnnotationWizardToggle(stepsConfig.wizardOn.includes(nextIndex))); + + /* Handler for setting the annotation target */ + dispatch(setAnnotationTarget( + stepsConfig.annotationTarget.includes(nextIndex) ? { + jsonPath: "$['ods:hasEntityRelationships']", + type: 'class' + } : undefined + )); + + /* Handler for form steps */ + if ([...stepsConfig.formSteps[0], ...stepsConfig.formSteps[1], ...stepsConfig.formSteps[2]].includes(nextIndex)) { + dispatch(setAnnotationWizardFormValues(ConstructFormValues(nextIndex))); + } else if (stepsConfig.formSteps[3].includes(nextIndex)) { + dispatch(setAnnotationWizardDummyAnnotation(dummyAnnotation)); + } else { + dispatch(setAnnotationWizardFormValues({ + class: undefined, + annotationValues: {}, + motivation: '', + jsonPath: '' + })); + } + + /* Handler for annotation wizard selected index of tabs */ + dispatch(setAnnotationWizardSelectedIndex(Object.values(stepsConfig.selectedIndex).findIndex((stepArray: unknown) => + (stepArray as number[]).includes(nextIndex) + ))); + + /* Handler for annotation dummy */ + dispatch(setAnnotationWizardDummyAnnotation(stepsConfig.dummyAnnotation.includes(nextIndex) ? dummyAnnotation : undefined)); + + setTimeout(() => { + stepsRef.current.updateStepElement(nextIndex - 1); + + resolve(true); + }, 500); + }; + + return ( + { + return new Promise((resolve) => { + + OnStepChange(nextIndex + 1, resolve); + }); + }} + onStart={() => { + dispatch(setAnnotationWizardToggle(false)); + }} + onExit={() => { + dispatch(setTourTopic(undefined)); + dispatch(setAnnotationWizardFormValues(undefined)); + dispatch(setAnnotationWizardSelectedIndex(undefined)); + dispatch(setAnnotationTarget(undefined)); + dispatch(setAnnotationWizardToggle(false)); + dispatch(setAnnotationWizardDummyAnnotation(undefined)); + }} + options={options} + ref={stepsRef} + /> + ); +}; + +export default AnnotateTourSteps; \ No newline at end of file diff --git a/src/components/digitalSpecimen/tourSteps/masTourSteps.tsx b/src/components/digitalSpecimen/tourSteps/masTourSteps.tsx new file mode 100644 index 00000000..d677b2ed --- /dev/null +++ b/src/components/digitalSpecimen/tourSteps/masTourSteps.tsx @@ -0,0 +1,178 @@ +/* Import Dependencies */ +import { Steps } from 'intro.js-react'; +import KeycloakService from 'app/Keycloak'; +import { useRef, useState } from 'react'; + +/* Import Config */ +import StepsConfig from 'app/config/StepsConfig'; + +/* Import Hooks */ +import { useAppSelector, useAppDispatch, useTrigger } from 'app/Hooks'; + +/* Import Store */ +import { setAnnotationWizardToggle, setMasMenuToggle, setMasScheduleMenuToggle, setMasDummy, setMasMachineJobRecordDummy } from 'redux-store/TourSlice'; +import { getTourTopic, setTourTopic } from 'redux-store/GlobalSlice'; + +/* Import Types */ +import { MachineAnnotationService } from 'app/types/MachineAnnotationService'; +import { Dict } from 'app/Types'; + +/* Import Sources */ +import DigitalSpecimenTourStepsText from 'sources/tourText/digitalSpecimen.json'; + + +/* Props Type */ +type Props = { + SetAnnotationMode: Function +}; + + +/** + * Component that renders the tour steps for the machine annotation services on the digital specimen page + * @param SetAnnotationMode Function to set the annotation mode + * @returns JSX Component + */ +const MasTourSteps = (props: Props) => { + const { SetAnnotationMode } = props; + + /* Hooks */ + const dispatch = useAppDispatch(); + const trigger = useTrigger(); + const stepsRef = useRef(null); + + /* Base variables */ + const tourTopic = useAppSelector(getTourTopic); + const masSteps = DigitalSpecimenTourStepsText.mas; + const { options } = StepsConfig(); + const [steps, setSteps] = useState<{ + intro: string, + element?: string + }[]>([]); + const stepsConfig: Dict = { + annotationModeOff: [1, 2, 3], + masMenuToggleOn: [5, 6, 7, 8, 9, 10, 11, 12, 13], + masScheduleMenuOn: [8, 9, 10], + masDummy: [9, 10], + masMachineJobRecordDummy: [11, 12, 13] + }; + const masDummy: MachineAnnotationService = { + '@id': 'MachineAnnotationServiceDummy', + '@type': 'ods:MachineAnnotationService', + 'schema:identifier': 'MachineAnnotationServiceDummy', + 'ods:fdoType': 'https://doi.org/10.15468/1a2b3c', + 'schema:name': 'Dummy Machine Annotation Service', + 'schema:description': 'A machine annotation service for showing off functionality', + 'schema:dateCreated': '2024-11-15T08:56:50.758Z', + 'schema:creator': { + '@id': 'MachineAnnotationServiceDummyAgent', + '@type': 'prov:SoftwareAgent' + }, + 'schema:dateModified': '2024-11-15T08:56:50.758Z', + 'ods:containerImage': 'dummyContainerImage', + 'ods:containerTag': 'dummyContainerTag', + 'ods:batchingPermitted': false, + 'ods:timeToLive': 1000 + }; + + /* Construct Intro.js steps for MAS functionality on the digital specimen page */ + trigger.SetTrigger(() => { + const steps: { + intro: string, + element?: string + }[] = []; + + masSteps.forEach((step, index) => { + if ([0, 1, 2, 3, 4, 5, 6].includes(index) || (KeycloakService.IsLoggedIn() && KeycloakService.GetParsedToken()?.orcid)) { + steps.push({ + intro: step, + element: `.tourMas${index + 1}` + }); + } + }); + + setSteps(steps); + }, []); + + /** + * Function to construct the MAS machine job record dummy object + * @param nextIndex The next selected step in the MAS tour + * @returns MAS machine job record dummy object + */ + const ConstructMASMachineJobRecordDummy = (nextIndex: number) => { + let state: 'SCHEDULED' | 'RUNNING' | 'FAILED' | 'COMPLETED' = 'SCHEDULED'; + + if (nextIndex === 12) { + state = 'COMPLETED'; + } else if (nextIndex === 13) { + state = 'FAILED'; + } + + return { + annotations: [], + batchingRequested: false, + jobHandle: 'MachineJobRecordDummy', + masId: 'MachineAnnotationServiceDummy', + orcid: 'Tour Manager', + state, + targetId: 'DigitalSpecimenDummy', + targetType: 'ods:DigitalSpecimen', + timeCompleted: '2024-11-15T08:56:50.758Z', + timeStarted: '2024-11-15T08:56:50.758Z', + timeToLive: 1000 + } + }; + + /** + * Function that checks what to do on a step change + * @param nextIndex The next (selected) index in the step chain + * @param resolve Function to resolve the step promise + */ + const OnStepChange = async (nextIndex: number, resolve: Function) => { + /* Handler for setting annotation mode on or off */ + SetAnnotationMode(!stepsConfig.annotationModeOff.includes(nextIndex)); + + /* Handler for MAS menu toggle */ + dispatch(setMasMenuToggle(stepsConfig.masMenuToggleOn.includes(nextIndex))); + + /* Handler for schedule MAS menu toggle */ + dispatch(setMasScheduleMenuToggle(stepsConfig.masScheduleMenuOn.includes(nextIndex))); + + /* Handler for MAS dummy */ + dispatch(setMasDummy(stepsConfig.masDummy.includes(nextIndex) ? masDummy : undefined)); + + /* Handler for MAS machine job record dummy */ + dispatch(setMasMachineJobRecordDummy(stepsConfig.masMachineJobRecordDummy.includes(nextIndex) ? ConstructMASMachineJobRecordDummy(nextIndex) : undefined)); + + setTimeout(() => { + stepsRef.current.updateStepElement(nextIndex - 1); + + resolve(true); + }, 500); + }; + + return ( + { + return new Promise((resolve) => { + OnStepChange(nextIndex + 1, resolve); + }); + }} + onStart={() => { + dispatch(setAnnotationWizardToggle(false)); + }} + onExit={() => { + dispatch(setTourTopic(undefined)); + dispatch(setMasDummy(undefined)); + dispatch(setMasMachineJobRecordDummy(undefined)); + dispatch(setMasMenuToggle(false)); + dispatch(setMasScheduleMenuToggle(false)); + }} + options={options} + ref={stepsRef} + /> + ); +}; + +export default MasTourSteps; \ No newline at end of file diff --git a/src/components/elements/annotationSidePanel/AnnotationSidePanel.tsx b/src/components/elements/annotationSidePanel/AnnotationSidePanel.tsx index 9852dd52..79656903 100644 --- a/src/components/elements/annotationSidePanel/AnnotationSidePanel.tsx +++ b/src/components/elements/annotationSidePanel/AnnotationSidePanel.tsx @@ -8,6 +8,7 @@ import { useAppDispatch, useAppSelector, useFetch } from 'app/Hooks'; /* Import Store */ import { getAnnotationTarget, setAnnotationTarget } from 'redux-store/AnnotateSlice'; +import { getAnnotationWizardToggle, getMasMenuToggle } from 'redux-store/TourSlice'; /* Import Types */ import { Annotation } from 'app/types/Annotation'; @@ -60,6 +61,8 @@ const AnnotationSidePanel = (props: Props) => { /* Base variables */ const annotationTarget = useAppSelector(getAnnotationTarget); + const tourAnnotationWizardToggle = useAppSelector(getAnnotationWizardToggle); + const tourMasMenuToggle = useAppSelector(getMasMenuToggle); const [annotations, setAnnotations] = useState([]); const [annotationWizardToggle, setAnnotationWizardToggle] = useState(false); const [masMenuToggle, setMasMenuToggle] = useState(false); @@ -81,7 +84,7 @@ const AnnotationSidePanel = (props: Props) => { triggers: [superClass, annotationWizardToggle], Method: GetAnnotations, Handler: (annotations: Annotation[]) => { - setAnnotations(annotations) + setAnnotations(annotations); } }); @@ -109,7 +112,9 @@ const AnnotationSidePanel = (props: Props) => { }); return ( -
+
{/* Top bar */} @@ -122,7 +127,7 @@ const AnnotationSidePanel = (props: Props) => { {/* Annotations overview or wizard depending on state */} - {(annotationWizardToggle && superClass) ? + {((annotationWizardToggle || tourAnnotationWizardToggle) && superClass) ? { SetFilterSortValues={setFilterSortValues} /> : <> - {(masMenuToggle && superClass) ? setMasMenuToggle(false)} SetLoading={setLoading} GetMas={GetMas} diff --git a/src/components/elements/annotationSidePanel/components/AnnotationsOverview.tsx b/src/components/elements/annotationSidePanel/components/AnnotationsOverview.tsx index 48a26ea2..1c112ec8 100644 --- a/src/components/elements/annotationSidePanel/components/AnnotationsOverview.tsx +++ b/src/components/elements/annotationSidePanel/components/AnnotationsOverview.tsx @@ -5,10 +5,11 @@ import KeycloakService from "app/Keycloak"; import { Row, Col, Card } from "react-bootstrap"; /* Import Hooks */ -import { useAppDispatch } from "app/Hooks"; +import { useAppSelector, useAppDispatch } from "app/Hooks"; /* Import Store */ import { setAnnotationTarget } from "redux-store/AnnotateSlice"; +import { getAnnotationWizardDummyAnnotation } from "redux-store/TourSlice"; /* Import Types */ import { Annotation } from "app/types/Annotation"; @@ -56,6 +57,9 @@ const AnnotationsOverview = (props: Props) => { /* Hooks */ const dispatch = useAppDispatch(); + /* Base variables */ + const tourAnnotationWizardDummyAnnotation = useAppSelector(getAnnotationWizardDummyAnnotation); + /** * Function to sort and filter annotations by the selected values * @param motivation The annotation motivation to filter by @@ -82,7 +86,10 @@ const AnnotationsOverview = (props: Props) => { }; /* Set overview annotations */ - const overviewAnnotations = SortAndFilerAnnotations(filterSortValues.motivation, filterSortValues.sortBy); + const overviewAnnotations = [ + ...(tourAnnotationWizardDummyAnnotation ? [tourAnnotationWizardDummyAnnotation] : []), + ...SortAndFilerAnnotations(filterSortValues.motivation, filterSortValues.sortBy) + ]; /** * Function to start editing an existing annotation @@ -148,9 +155,9 @@ const AnnotationsOverview = (props: Props) => { {/* Annotations */} - {overviewAnnotations.length ? overviewAnnotations.map(annotation => ( + {overviewAnnotations.length ? overviewAnnotations.map((annotation, index) => (
{ {/* Add annotation button */} {/* Machine annotation services button */} - + - +