Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found
Select Git revision
Loading items

Target

Select target project
  • coscine/frontend/apps/ui
1 result
Select Git revision
Loading items
Show changes
Commits on Source (6)
{
"name": "ui",
"version": "2.16.0",
"version": "2.17.0",
"scripts": {
"dev": "vite",
"build": "vite build",
......
......@@ -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(),
},
];
},
......
......@@ -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();
},
},
......
......@@ -70,6 +70,7 @@ export default {
title: "Einloggen",
button_orcid: "Anmelden mit ORCID®",
button_other_institution: "Anderes institutionelles Konto",
button_institution: "Institutionelles Konto",
},
logout: {
......
......@@ -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: {
......
......@@ -27,3 +27,9 @@ export interface DFNAAIData {
};
logo?: string;
}
export interface Partner {
key: string;
name: string;
logo: string;
url: string;
}
......@@ -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:
......
......@@ -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:
......
......@@ -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;
......
......@@ -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.
......
......@@ -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;
}
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/";
......@@ -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;
},
},
});
......
......@@ -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 {
......
......@@ -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
......