Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Display DOIs in Archived Histories #18134

Merged
merged 15 commits into from
May 16, 2024
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions client/src/api/remoteFiles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,15 +28,15 @@ export interface FilterFileSourcesOptions {
exclude?: FileSourcePluginKind[];
}

const getRemoteFilesPlugins = fetcher.path("/api/remote_files/plugins").method("get").create();
const remoteFilesPluginsFetcher = fetcher.path("/api/remote_files/plugins").method("get").create();

/**
* Get the list of available file sources from the server that can be browsed.
* @param options The options to filter the file sources.
* @returns The list of available (browsable) file sources from the server.
*/
export async function getFileSources(options: FilterFileSourcesOptions = {}): Promise<BrowsableFilesSourcePlugin[]> {
const { data } = await getRemoteFilesPlugins({
export async function fetchFileSources(options: FilterFileSourcesOptions = {}): Promise<BrowsableFilesSourcePlugin[]> {
const { data } = await remoteFilesPluginsFetcher({
browsable_only: true,
include_kind: options.include,
exclude_kind: options.exclude,
Expand Down
11 changes: 10 additions & 1 deletion client/src/api/schema/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2637,6 +2637,11 @@ export interface components {
* @description Documentation or extended description for this plugin.
*/
doc: string;
/**
* Extra
* @description Extra configuration options that the plugin may serialize. This is plugin specific.
*/
extra?: Record<string, never> | null;
/**
* ID
* @description The `FilesSource` plugin identifier
Expand Down Expand Up @@ -2672,7 +2677,6 @@ export interface components {
* @description Whether this files source plugin allows write access.
*/
writable: boolean;
[key: string]: unknown | undefined;
};
/** BulkOperationItemError */
BulkOperationItemError: {
Expand Down Expand Up @@ -5211,6 +5215,11 @@ export interface components {
* @description Documentation or extended description for this plugin.
*/
doc: string;
/**
* Extra
* @description Extra configuration options that the plugin may serialize. This is plugin specific.
*/
extra?: Record<string, never> | null;
/**
* ID
* @description The `FilesSource` plugin identifier
Expand Down
30 changes: 30 additions & 0 deletions client/src/components/Common/DOILink.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<script setup lang="ts">
import { library } from "@fortawesome/fontawesome-svg-core";
import { faLink } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
import { BBadge } from "bootstrap-vue";
import { computed } from "vue";

library.add(faLink);

interface Props {
doi: string;
}

const props = defineProps<Props>();

const doiLink = computed(() => `https://doi.org/${props.doi}`);
</script>

<template>
<BBadge class="doi-badge">
<FontAwesomeIcon :icon="faLink" />
<a :href="doiLink" target="_blank" rel="noopener noreferrer">{{ props.doi }}</a>
</BBadge>
</template>

<style scoped>
.doi-badge {
font-size: 1rem;
}
</style>
102 changes: 102 additions & 0 deletions client/src/components/Common/ExportRecordDOILink.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
<script setup lang="ts">
import axios from "axios";
import { ref, watch } from "vue";

import { BrowsableFilesSourcePlugin } from "@/api/remoteFiles";
import { useFileSources } from "@/composables/fileSources";

import DOILink from "./DOILink.vue";

const { getFileSourceByUri, isLoading: isLoadingFileSources } = useFileSources({ include: ["rdm"] });

interface Props {
exportRecordUri?: string;
rdmFileSources?: BrowsableFilesSourcePlugin[];
}

const props = defineProps<Props>();

const doi = ref<string | undefined>(undefined);

// File sources need to be loaded before we can get the DOI
watch(isLoadingFileSources, async (isLoading) => {
if (!isLoading) {
doi.value = await getDOIFromExportRecordUri(props.exportRecordUri);
}
});

/**
* Gets the DOI from an export record URI.
* The URI should be in the format: `<scheme>://<source-id>/<record-id>/<file-name>`.
* @param uri The target URI of the export record to get the DOI from.
* @returns The DOI or undefined if it could not be retrieved.
*/
async function getDOIFromExportRecordUri(uri?: string) {
if (!uri) {
return undefined;
}

const fileSource = getFileSourceByUri(uri);
if (!fileSource) {
console.debug("No file source found for URI: ", uri);
return undefined;
}
const repositoryUrl = fileSource.extra?.url;
if (!repositoryUrl) {
console.debug("Invalid repository URL for file source: ", fileSource);
return undefined;
}
const recordId = getRecordIdFromUri(uri);
if (!recordId) {
console.debug("No record ID found for URI: ", uri);
return undefined;
}
const recordUrl = `${repositoryUrl}/api/records/${recordId}`;
return getDOIFromInvenioRecordUrl(recordUrl);
}

/**
* Extracts the record ID from a URI.
* The URI should be in the format: `<scheme>://<source-id>/<record-id>/<file-name>`.
* @param targetUri The URI to extract the record ID from.
* @returns The record ID or undefined if it could not be extracted.
*/
function getRecordIdFromUri(targetUri?: string): string | undefined {
if (!targetUri) {
return undefined;
}
return targetUri.split("//")[1]?.split("/")[1];
}

/**
* Gets the DOI from an Invenio record URL.
* @param recordUrl The URL of the record to get the DOI from.
* @returns The DOI or undefined if it could not be retrieved.
*/
async function getDOIFromInvenioRecordUrl(recordUrl?: string): Promise<string | undefined> {
if (!recordUrl) {
return undefined;
}

try {
const response = await axios.get(recordUrl);
if (response.status !== 200) {
console.debug("Failed to get record from URL: ", recordUrl);
return undefined;
}
const record = response.data;
if (!record?.doi) {
console.debug("No DOI found in record: ", record);
return undefined;
}
return record.doi;
} catch (error) {
console.warn("Failed to get record from URL: ", recordUrl, error);
return undefined;
}
}
</script>

<template>
<DOILink v-if="doi" :doi="doi" />
</template>
4 changes: 2 additions & 2 deletions client/src/components/FilesDialog/FilesDialog.vue
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ import Vue, { computed, onMounted, ref } from "vue";

import {
browseRemoteFiles,
fetchFileSources,
FileSourceBrowsingMode,
FilterFileSourcesOptions,
getFileSources,
RemoteEntry,
} from "@/api/remoteFiles";
import { UrlTracker } from "@/components/DataDialog/utilities";
Expand Down Expand Up @@ -231,7 +231,7 @@ function load(record?: SelectionItem) {
undoShow.value = !urlTracker.value.atRoot();
if (urlTracker.value.atRoot() || errorMessage.value) {
errorMessage.value = undefined;
getFileSources(props.filterOptions)
fetchFileSources(props.filterOptions)
.then((results) => {
const convertedItems = results
.filter((item) => !props.requireWritable || item.writable)
Expand Down
120 changes: 120 additions & 0 deletions client/src/components/History/Archiving/ArchivedHistoryCard.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
<script setup lang="ts">
import { library } from "@fortawesome/fontawesome-svg-core";
import { faCopy, faEye, faUndo } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
import { BBadge, BButton, BButtonGroup } from "bootstrap-vue";
import { computed } from "vue";

import { ArchivedHistorySummary } from "@/api/histories.archived";
import localize from "@/utils/localization";

import ExportRecordDOILink from "@/components/Common/ExportRecordDOILink.vue";
import Heading from "@/components/Common/Heading.vue";
import StatelessTags from "@/components/TagsMultiselect/StatelessTags.vue";
import UtcDate from "@/components/UtcDate.vue";

interface Props {
history: ArchivedHistorySummary;
}

const props = defineProps<Props>();

const canImportCopy = computed(() => props.history.export_record_data?.target_uri !== undefined);

const emit = defineEmits<{
(e: "onView", history: ArchivedHistorySummary): void;
(e: "onRestore", history: ArchivedHistorySummary): void;
(e: "onImportCopy", history: ArchivedHistorySummary): void;
}>();

library.add(faUndo, faCopy, faEye);

function onViewHistoryInCenterPanel() {
emit("onView", props.history);
}

async function onRestoreHistory() {
emit("onRestore", props.history);
}

async function onImportCopy() {
emit("onImportCopy", props.history);
}
</script>

<template>
<div class="archived-history-card">
<div class="d-flex justify-content-between align-items-center">
<Heading h3 inline bold size="sm">
{{ history.name }} <ExportRecordDOILink :export-record-uri="history.export_record_data?.target_uri" />
</Heading>

<div class="d-flex align-items-center flex-gapx-1 badges">
<BBadge v-if="history.published" v-b-tooltip pill :title="localize('This history is public.')">
{{ localize("Published") }}
</BBadge>
<BBadge v-if="!history.purged" v-b-tooltip pill :title="localize('Amount of items in history')">
{{ history.count }} {{ localize("items") }}
</BBadge>
<BBadge
v-if="history.export_record_data"
v-b-tooltip
pill
:title="
localize(
'This history has an associated export record containing a snapshot of the history that can be used to import a copy of the history.'
)
">
{{ localize("Snapshot available") }}
</BBadge>
<BBadge v-b-tooltip pill :title="localize('Last edited/archived')">
<UtcDate :date="history.update_time" mode="elapsed" />
</BBadge>
</div>
</div>

<div class="d-flex justify-content-start align-items-center mt-1">
<BButtonGroup class="actions">
<BButton
v-b-tooltip
:title="localize('View this history')"
variant="link"
class="p-0 px-1"
@click.stop="onViewHistoryInCenterPanel">
<FontAwesomeIcon :icon="faEye" size="lg" />
View
</BButton>
<BButton
v-b-tooltip
:title="localize('Unarchive this history and move it back to your active histories')"
variant="link"
class="p-0 px-1"
@click.stop="onRestoreHistory">
<FontAwesomeIcon :icon="faUndo" size="lg" />
Unarchive
</BButton>

<BButton
v-if="canImportCopy"
v-b-tooltip
:title="localize('Import a new copy of this history from the associated export record')"
variant="link"
class="p-0 px-1"
@click.stop="onImportCopy">
<FontAwesomeIcon :icon="faCopy" size="lg" />
Import Copy
</BButton>
</BButtonGroup>
</div>

<p v-if="history.annotation" class="my-1">{{ history.annotation }}</p>

<StatelessTags class="my-1" :value="history.tags" :disabled="true" :max-visible-tags="10" />
</div>
</template>

<style scoped>
.badges {
font-size: 1rem;
}
</style>
Loading
Loading