diff --git a/src/modules/login/LoginModule.vue b/src/modules/login/LoginModule.vue index dccb126a60942df6649ef8179f13de916e959027..9179e48161472de31898e308ca35a78fdd182c30 100644 --- a/src/modules/login/LoginModule.vue +++ b/src/modules/login/LoginModule.vue @@ -45,6 +45,7 @@ import { defineComponent } from "vue"; // import the store for current module import useLoginStore from "./store"; +import { Partner } from "../login/types"; export default defineComponent({ setup() { @@ -57,49 +58,50 @@ export default defineComponent({ moduleIsReady(): boolean { return true; }, - partners() { + partners(): Partner[] { return [ { key: "aims", - name: this.$t("page.partners.aims.headline"), - logo: this.$t("page.partners.aims.logo"), - url: this.$t("page.partners.aims.url"), + name: this.$t("page.partners.aims.headline").toString(), + logo: this.$t("page.partners.aims.logo").toString(), + url: this.$t("page.partners.aims.url").toString(), }, { key: "nfdi4ing", - name: this.$t("page.partners.nfdi4ing.headline"), - logo: this.$t("page.partners.nfdi4ing.logo"), - url: this.$t("page.partners.nfdi4ing.url"), + name: this.$t("page.partners.nfdi4ing.headline").toString(), + logo: this.$t("page.partners.nfdi4ing.logo").toString(), + url: this.$t("page.partners.nfdi4ing.url").toString(), }, { key: "nfdiMatWerk", - name: this.$t("page.partners.nfdiMatWerk.headline"), - logo: this.$t("page.partners.nfdiMatWerk.logo"), - url: this.$t("page.partners.nfdiMatWerk.url"), + name: this.$t("page.partners.nfdiMatWerk.headline").toString(), + logo: this.$t("page.partners.nfdiMatWerk.logo").toString(), + url: this.$t("page.partners.nfdiMatWerk.url").toString(), }, { key: "fdmNrw", - name: this.$t("page.partners.fdmNrw.headline"), - logo: this.$t("page.partners.fdmNrw.logo"), - url: this.$t("page.partners.fdmNrw.url"), + name: this.$t("page.partners.fdmNrw.headline").toString(), + logo: this.$t("page.partners.fdmNrw.logo").toString(), + url: this.$t("page.partners.fdmNrw.url").toString(), }, { key: "itCenter", - name: this.$t("page.partners.itCenter.headline"), - logo: this.$t("page.partners.itCenter.logo"), - url: this.$t("page.partners.itCenter.url"), + name: this.$t("page.partners.itCenter.headline").toString(), + logo: this.$t("page.partners.itCenter.logo").toString(), + url: this.$t("page.partners.itCenter.url").toString(), }, + { key: "dfg", - name: this.$t("page.partners.dfg.headline"), - logo: this.$t("page.partners.dfg.logo"), - url: this.$t("page.partners.dfg.url"), + name: this.$t("page.partners.dfg.headline").toString(), + logo: this.$t("page.partners.dfg.logo").toString(), + url: this.$t("page.partners.dfg.url").toString(), }, { key: "mkwNrw", - name: this.$t("page.partners.mkwNrw.headline"), - logo: this.$t("page.partners.mkwNrw.logo"), - url: this.$t("page.partners.mkwNrw.url"), + name: this.$t("page.partners.mkwNrw.headline").toString(), + logo: this.$t("page.partners.mkwNrw.logo").toString(), + url: this.$t("page.partners.mkwNrw.url").toString(), }, ]; }, diff --git a/src/modules/login/components/LoginMain.vue b/src/modules/login/components/LoginMain.vue index b8a8abaa83a043ad1703636028ffee06d67bab1a..21afc0137e63a16ef5ae19e54dd5ce9bb024e714 100644 --- a/src/modules/login/components/LoginMain.vue +++ b/src/modules/login/components/LoginMain.vue @@ -17,16 +17,15 @@ <div v-else class="v_gapped_container"> <!-- Institution Shibboleth Login --> <b-button - v-if="selectedInstitution" + v-if="selectedInstitution && !isFirstLogin" variant="primary" @click="toInstitutionLoginPage" > {{ selectedInstitution.displayName }} </b-button> - - <!-- Other Institutional Account --> + <!-- Institutional Account --> <b-button variant="primary" @click="switchInstitution"> - {{ $t("page.login.button_other_institution") }} + {{ institutionButtonText }} </b-button> <!-- ORCID Login Button --> @@ -85,10 +84,15 @@ export default defineComponent({ }, redirectUrl(): string | null { const query = this.$route.query["redirect"]; - if (query) { - return query as string; - } - return null; + return query ? (query as string) : null; + }, + isFirstLogin(): boolean { + return !this.selectedInstitution; + }, + institutionButtonText(): string { + return this.isFirstLogin + ? this.$t("page.login.button_institution").toString() + : this.$t("page.login.button_other_institution").toString(); }, }, diff --git a/src/modules/login/i18n/de.ts b/src/modules/login/i18n/de.ts index 7b47a16612a3e27c2c22aeca2e44e7e8e88855e7..0eaa0cd7e91fa48b116872a507e6c82a934d5608 100644 --- a/src/modules/login/i18n/de.ts +++ b/src/modules/login/i18n/de.ts @@ -70,6 +70,7 @@ export default { title: "Einloggen", button_orcid: "Anmelden mit ORCID®", button_other_institution: "Anderes institutionelles Konto", + button_institution: "Institutionelles Konto", }, logout: { diff --git a/src/modules/login/i18n/en.ts b/src/modules/login/i18n/en.ts index e4cebc4c1a28c47474298d12f95da0e32f366f51..7f812a082f2df46f49c7964439daff5694440bee 100644 --- a/src/modules/login/i18n/en.ts +++ b/src/modules/login/i18n/en.ts @@ -70,6 +70,7 @@ export default { title: "Login", button_orcid: "Sign in with ORCID®", button_other_institution: "Other Institutional Account", + button_institution: "Institutional Account", }, logout: { diff --git a/src/modules/login/types.ts b/src/modules/login/types.ts index 5e19685607ae269dd1a4bc81f927bdc56ad27aec..dfbed14c81754b5792bee6cadffaebd01b44c4fc 100644 --- a/src/modules/login/types.ts +++ b/src/modules/login/types.ts @@ -27,3 +27,9 @@ export interface DFNAAIData { }; logo?: string; } +export interface Partner { + key: string; + name: string; + logo: string; + url: string; +} diff --git a/src/modules/pid/i18n/de.ts b/src/modules/pid/i18n/de.ts index 9e6d76a460b2fb3227d9a5e12a5b1ae2856eadbb..0596309599628156b9cd4198eb22c434d91d07b3 100644 --- a/src/modules/pid/i18n/de.ts +++ b/src/modules/pid/i18n/de.ts @@ -19,6 +19,8 @@ export default { "In den meisten Fällen sind die referenzierten Daten nicht öffentlich zugänglich. Sie können sich daher an den Besitzer der Daten wenden und um Zugang bitten. Der Besitzer dieser Ressource wird dann von Coscine kontaktiert. Die Informationen, die Sie im Formular angeben, werden so an den Besitzer weitergeleitet.", toClipboard: "PID wurde in die Zwischenablage kopiert", + toLocation: "Speicherort wurde in die Zwischenablage kopiert", + openUrl: "Navigieren Sie zur Objekt-URL in einem neuen Tab", form: { labelSymbol: ":", @@ -29,6 +31,9 @@ export default { persistentIdPopover: "Für weitere Informationen zum @:(page.pid.form.persistentId) siehe", persistentIdPopoverUrl: "https://docs.coscine.de/de/resources/pid/", + objectLocation: "Speicherort", + objectLocationLabel: + "@:(page.pid.form.objectLocation)@:(page.pid.form.labelSymbol)", yourName: "Ihr Name", yourNameLabel: diff --git a/src/modules/pid/i18n/en.ts b/src/modules/pid/i18n/en.ts index 538ba77430fbe54b32a6a5a25239b7d71bfc9edc..fbcdb291f0eb7c14350d55c8558498433c160206 100644 --- a/src/modules/pid/i18n/en.ts +++ b/src/modules/pid/i18n/en.ts @@ -19,6 +19,8 @@ export default { "In most cases the referenced data is not publicly available. You can contact the owner of the data to ask for access. The owner of this resource will be contacted by Coscine. The information you provide in the form will be relayed to the owner.", toClipboard: "PID has been copied to clipboard", + toLocation: "Object location has been copied to clipboard", + openUrl: "Navigate to the object location in a new tab", form: { labelSymbol: ":", @@ -29,6 +31,9 @@ export default { persistentIdPopover: "For more information on @:(page.pid.form.persistentId) see", persistentIdPopoverUrl: "https://docs.coscine.de/en/resources/pid/", + objectLocation: "Object Location", + objectLocationLabel: + "@:(page.pid.form.objectLocation)@:(page.pid.form.labelSymbol)", yourName: "Your Name", yourNameLabel: diff --git a/src/modules/pid/pages/Pid.vue b/src/modules/pid/pages/Pid.vue index c25ee7b242b7d16d333186f911875eeb87fac862..afe5ed19d7a2664422927e08fe5c0b9a836330b9 100644 --- a/src/modules/pid/pages/Pid.vue +++ b/src/modules/pid/pages/Pid.vue @@ -50,6 +50,56 @@ </b-tooltip> </b-button-group> </CoscineFormGroup> + + <!-- Object Location --> + <CoscineFormGroup + v-if="digitalObjectLocation" + label-for="ObjectLocation" + :label="$t('page.pid.form.objectLocationLabel')" + > + <b-button-group class="w-100"> + <!-- Text Field --> + <b-form-input + id="ObjectLocation" + v-model="digitalObjectLocation" + :readonly="true" + /> + + <!-- Open URL Button --> + <b-button + v-if="isPidUiUrlFound" + id="openUrlButton" + @click="openObjectLocation()" + > + <b-icon icon="link-45deg" /> + </b-button> + <!-- Copy Button --> + <b-button v-else id="copyObjectLocation" @click="copyUrl"> + <b-icon icon="clipboard" /> + </b-button> + + <!-- Open URL Tooltip --> + <b-tooltip + v-if="isPidUiUrlFound" + id="openUrlTooltip" + target="openUrlButton" + placement="top" + triggers="hover focus" + > + {{ $t("page.pid.openUrl") }} + </b-tooltip> + <!-- Copied Tooltip --> + <b-tooltip + v-else + id="copyObjectLocationTooltip" + target="copyObjectLocation" + placement="top" + triggers="blur" + > + {{ $t("page.pid.toLocation") }} + </b-tooltip> + </b-button-group> + </CoscineFormGroup> <div class="h-divider" /> <!-- Contact PID Owner --> @@ -149,16 +199,24 @@ import usePidStore from "../store"; import useNotificationStore from "@/store/notification"; import { useVuelidate, type ValidationArgs } from "@vuelidate/core"; import { email, maxLength, required } from "@vuelidate/validators"; -import type { PidRequestDto } from "@coscine/api-client/dist/types/Coscine.Api"; +import { + type HandleDto, + type PidRequestDto, +} from "@coscine/api-client/dist/types/Coscine.Api"; +import { handleUrl, pidHandles } from "../utils/constants"; +import useProjectStore from "@/modules/project/store"; +import useResourceStore from "@/modules/resource/store"; export default defineComponent({ setup() { const pidStore = usePidStore(); + const projectStore = useProjectStore(); + const resourceStore = useResourceStore(); const notificationStore = useNotificationStore(); const v$ = useVuelidate(); - return { pidStore, notificationStore, v$ }; + return { pidStore, projectStore, resourceStore, notificationStore, v$ }; }, data() { @@ -169,8 +227,11 @@ export default defineComponent({ message: "", sendConfirmationEmail: true, } satisfies PidRequestDto, + handle: undefined as HandleDto | undefined, + digitalObjectLocation: undefined as string | undefined, isLoading: false, isPidValid: null as boolean | null, + isPidUiUrlFound: false, }; }, @@ -178,9 +239,27 @@ export default defineComponent({ pid(): string | null { const pid = this.$route.query["pid"]; if (pid) { - return pid.toString().replace("http://hdl.handle.net/", ""); + return pid.toString().replace(handleUrl, ""); } else return null; }, + /** + * A computed property that decodes the base64-encoded path from the URL hash. + * If the hash is not specified, an empty string will be returned. + * @returns {string} The decoded path from the URL hash. + */ + path(): string { + const hash = this.$route.hash; + // Check for hash + if (hash) { + // Hash always starts with '#', trim it + const parsedHash = new URLSearchParams(this.$route.hash.substring(1)); + // Url decode + const path = decodeURI(parsedHash.get("path") ?? ""); + //Base64 decode + return Buffer.from(path, "base64").toString(); + } + return ""; + }, }, validations() { @@ -198,6 +277,17 @@ export default defineComponent({ async pid() { await this.validatePid(); }, + handle: { + async handler() { + // Try to build the UI URL from the PID, if it fails, use the digital object location from the handle + this.digitalObjectLocation = + (await this.buildUiUrlFromPid()) ?? + this.handle?.values?.find( + (h) => h.type === pidHandles.digitalObjectLocation, + )?.parsed_data; + }, + deep: true, + }, }, async created() { @@ -207,10 +297,73 @@ export default defineComponent({ methods: { copyPid() { if (this.pid) { - navigator.clipboard.writeText("http://hdl.handle.net/" + this.pid); + navigator.clipboard.writeText(handleUrl + this.pid); this.$root.$emit("bv::show::tooltip", "copyPidTooltip"); } }, + copyUrl() { + if (this.digitalObjectLocation) { + navigator.clipboard.writeText(this.digitalObjectLocation); + this.$root.$emit("bv::show::tooltip", "copyObjectLocationTooltip"); + } + }, + openObjectLocation() { + if (this.digitalObjectLocation) { + window.open(this.digitalObjectLocation, "_blank"); + } + }, + /** + * Asynchronously builds a UI URL based on the PID of the handle object. + * + * @method buildUiUrlFromPid + * @async + * @returns {Promise<string | undefined>} A promise that resolves to a string representing the UI URL, or undefined if the PID suffix is not found. + * + * The method works as follows: + * 1. If the PID type is "project", it fetches the project by its ID and constructs a route to the project page. + * 2. If the PID type is "resource", it fetches the resource by its ID and constructs a route to the resource page. + * 3. If a route is found, it sets the `isPidUiUrlFound` flag to true. + * 4. Finally, it returns the constructed URL, or undefined if no PID suffix was found. + */ + async buildUiUrlFromPid(): Promise<string | undefined> { + const id = this.handle?.pid?.suffix; + if (!id) return; + let route; + let projectSlug; + // Process type "project" + if (this.handle?.pid?.type === "project") { + const project = await this.projectStore.getProjectById( + id, + /*silent*/ true, + ); + if (project && project.slug) { + projectSlug = project.slug; + route = this.$router.resolve({ + name: "project-page", + params: { slug: projectSlug }, + }); + } + } + // Process type "resource" + if (this.handle?.pid?.type === "resource") { + const resource = await this.resourceStore.getResourceById( + id, + /*silent*/ true, + ); + if (resource && resource.projects && resource.projects.length > 0) { + projectSlug = resource.projects[0].id; + route = this.$router.resolve({ + name: "resource-page", + params: { slug: projectSlug!, guid: id, dirTrail: this.path }, + }); + } + } + if (route) { + // We found a route + this.isPidUiUrlFound = true; + return `${window.location.origin}${route?.href}`; + } + }, async validatePid() { this.isLoading = true; if (this.pid) { @@ -218,6 +371,7 @@ export default defineComponent({ const id = this.pid.split("/").at(1); if (prefix && id) { this.isPidValid = await this.pidStore.validatePid(prefix, id); + this.handle = await this.pidStore.getHandle(prefix, id); } } this.isLoading = false; diff --git a/src/modules/pid/store.ts b/src/modules/pid/store.ts index f96b452809578e57749404eacf332b189459ecf3..87cc041abbceeb697e7dd17f5c4c51a63d5004a8 100644 --- a/src/modules/pid/store.ts +++ b/src/modules/pid/store.ts @@ -3,9 +3,13 @@ import type { PidState } from "./types"; import useNotificationStore from "@/store/notification"; import { PidApi } from "@coscine/api-client"; +import { HandleApi } from "@coscine/api-client"; import { AxiosError } from "axios"; import { StatusCodes } from "http-status-codes"; -import type { PidRequestDto } from "@coscine/api-client/dist/types/Coscine.Api"; +import type { + HandleDto, + PidRequestDto, +} from "@coscine/api-client/dist/types/Coscine.Api"; /* Store variable name is "this.<id>Store" @@ -65,7 +69,25 @@ export const usePidStore = defineStore({ return null; } }, - + /** + * Retrieves the PID handles using the provided PID prefix and ID. + * @param {string} prefix - The PID prefix value. + * @param {string} suffix - The ID value. + * @returns {Promise<HandleDto | undefined>} A promise that resolves to the handle data if successful, or undefined if there was an error. + */ + async getHandle( + prefix: string, + suffix: string, + ): Promise<HandleDto | undefined> { + const notificationStore = useNotificationStore(); + try { + const apiResponse = await HandleApi.getHandle({ prefix, suffix }); + return apiResponse.data.data; + } catch (error) { + // Handle other Status Codes + notificationStore.postApiErrorNotification(error as AxiosError); + } + }, /** * Contacts the PID owner using the provided PID prefix, ID, and PID enquiry data. * @param {string} prefix - The PID prefix value. diff --git a/src/modules/pid/types.ts b/src/modules/pid/types.ts index 61d187bb4a6b6efaafa7d91a6862ea1a86b57a08..e9565ed6e32a13bf9aaa482a42878a5d5e62cd1d 100644 --- a/src/modules/pid/types.ts +++ b/src/modules/pid/types.ts @@ -6,3 +6,40 @@ export interface PidState { -------------------------------------------------------------------------------------- */ } + +/** + * Provides a collection of constant identifiers relevant to the Coscine PID Record. + * These identifiers represent entries in the Data Type Registry (DTR) and are used to identify various + * types of information within the PID system. + */ +export interface PidHandles { + /** The general identifier for the Kernel Information Profile type. */ + kernelInformationProfile: string; + + /** The identifier for the Coscine-specific Kernel Information Profile type. */ + coscineKernelInformationProfile: string; + + /** The identifier representing the date the PID record was created. */ + dateCreated: string; + + /** The identifier for identifying the digital object's location. */ + digitalObjectLocation: string; + + /** The identifier for the digital object type. */ + digitalObjectType: string; + + /** The identifier for the digital object value corresponding to a resource. */ + digitalObjectTypeResource: string; + + /** The identifier for the digital object value corresponding to a project. */ + digitalObjectTypeProject: string; + + /** The identifier representing the type of license associated with the digital object. */ + license: string; + + /** The identifier for the contact type, representing contact information within the PID record. */ + contact: string; + + /** The identifier for the topic type, used to categorize or tag the digital object with specific topics. */ + topic: string; +} diff --git a/src/modules/pid/utils/constants.ts b/src/modules/pid/utils/constants.ts new file mode 100644 index 0000000000000000000000000000000000000000..f455a98b3da3223bea27ac72d097213d040a6323 --- /dev/null +++ b/src/modules/pid/utils/constants.ts @@ -0,0 +1,16 @@ +import { PidHandles } from "../types"; + +export const pidHandles: PidHandles = { + kernelInformationProfile: "21.T11148/076759916209e5d62bd5", + coscineKernelInformationProfile: "21.T11148/8882327b7c25331e3cdd", + dateCreated: "21.T11148/aafd5fb4c7222e2d950a", + digitalObjectLocation: "21.T11148/b8457812905b83046284", + digitalObjectType: "21.T11148/1c699a5d1b4ad3ba4956", + digitalObjectTypeResource: "21.T11148/12aad485b74d04f584c1", + digitalObjectTypeProject: "21.T11148/0f13b0a83bd926fe269f", + license: "21.T11148/2f314c8fe5fb6a0063a8", + contact: "21.T11148/1a73af9e7ae00182733b", + topic: "21.T11148/b415e16fbe4ca40f2270", +}; + +export const handleUrl = "http://hdl.handle.net/"; diff --git a/src/modules/project/store.ts b/src/modules/project/store.ts index 0c92c0979cfb00baa9bc597d087ad15c5e2b34c3..6ec5d0eedc3b678495a17e88271f670fb5180402 100644 --- a/src/modules/project/store.ts +++ b/src/modules/project/store.ts @@ -257,6 +257,23 @@ export const useProjectStore = defineStore({ } }, + async getProjectById( + projectId: string, + silent: boolean = false, + ): Promise<ProjectDto | null> { + const notificationStore = useNotificationStore(); + try { + const apiResponse = await ProjectApi.getProject({ projectId }); + return apiResponse.data.data as ProjectDto; + } catch (error) { + if (!silent) { + // Handle other Status Codes + notificationStore.postApiErrorNotification(error as AxiosError); + } + return null; + } + }, + async retrieveProjectBySlug(slug: string) { const notificationStore = useNotificationStore(); try { @@ -649,7 +666,8 @@ export const useProjectStore = defineStore({ if (!this.visitedProjects[routeParams.slug]) { await this.retrieveProjectBySlug(routeParams.slug); } - this.currentSlug = routeParams.slug; + // Handle slug and id + this.currentSlug = this.findSlug(routeParams.slug); } }, @@ -920,6 +938,21 @@ export const useProjectStore = defineStore({ notificationStore.postApiErrorNotification(error as AxiosError); } }, + + findSlug(projectIdOrSlug: string): string | null { + // Check if the projectIdOrSlug is a slug + if (this.visitedProjects[projectIdOrSlug]) { + return projectIdOrSlug; + } else { + // Check if the projectIdOrSlug is a projectId + for (const slug in this.visitedProjects) { + if (this.visitedProjects[slug].id === projectIdOrSlug) { + return slug; + } + } + } + return null; + }, }, }); diff --git a/src/modules/resource/store.ts b/src/modules/resource/store.ts index 81f17c43efa56af75b3c58d37a6d3f4fc3ae0684..434d7db58433cca3cebb14ddf84076f742896566 100644 --- a/src/modules/resource/store.ts +++ b/src/modules/resource/store.ts @@ -17,6 +17,7 @@ import { ProjectResourceApi, ProjectResourceQuotaApi, ProjectResourceTypeApi, + ResourceApi, ResourceTypeApi, TreeApi, VocabularyApi, @@ -179,6 +180,25 @@ export const useResourceStore = defineStore({ } }, + async getResourceById( + resourceId: string, + silent: boolean = false, + ): Promise<ResourceDto | null> { + const notificationStore = useNotificationStore(); + try { + const apiResponse = await ResourceApi.getResource({ + resourceId, + }); + return apiResponse.data.data as ResourceDto; + } catch (error) { + if (!silent) { + // Handle other Status Codes + notificationStore.postApiErrorNotification(error as AxiosError); + } + return null; + } + }, + async retrieveApplicationProfile(resource: VisitedResourceObject) { const notificationStore = useNotificationStore(); try { diff --git a/yarn.lock b/yarn.lock index d70becbd7b37b9368ae03f22cb2af983dc75d244..d2a9c9b170ae7896734436b6f85e17d8aa01b9fb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -14242,8 +14242,8 @@ __metadata: linkType: hard "vue-demi@npm:^0.14.0": - version: 0.14.7 - resolution: "vue-demi@npm:0.14.7" + version: 0.14.8 + resolution: "vue-demi@npm:0.14.8" peerDependencies: "@vue/composition-api": ^1.0.0-rc.1 vue: ^3.0.0-0 || ^2.6.0 @@ -14253,7 +14253,7 @@ __metadata: bin: vue-demi-fix: bin/vue-demi-fix.js vue-demi-switch: bin/vue-demi-switch.js - checksum: 10/04884677b8790320bcd3cbbf8dae1c4da9f4ab304659bf18d69b11255f7d16825d2135d2d0e565b1a1f1b7f601100eb26760254129c6bacec2c7e72ab0f61d52 + checksum: 10/88af227aa3a83607584c4f614efee9ac2fb8ea124db85f2f88bab62382cdfbd6611d9d2489ca757d3518afc7df7ea69257bf182705d20e473404e64959e9f838 languageName: node linkType: hard