Skip to content

Commit

Permalink
Merge pull request #18134 from davelopez/display_doi
Browse files Browse the repository at this point in the history
Display DOIs in Archived Histories
  • Loading branch information
dannon authored May 16, 2024
2 parents cb9bce4 + 440d2db commit d0f15e8
Show file tree
Hide file tree
Showing 17 changed files with 349 additions and 113 deletions.
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 @@ -2667,12 +2667,16 @@ export interface components {
* @description The URI root used by this type of plugin.
*/
uri_root: string;
/**
* URL
* @description Optional URL that might be provided by some plugins to link to the remote source.
*/
url?: string | null;
/**
* Writeable
* @description Whether this files source plugin allows write access.
*/
writable: boolean;
[key: string]: unknown | undefined;
};
/** BulkOperationItemError */
BulkOperationItemError: {
Expand Down Expand Up @@ -5236,6 +5240,11 @@ export interface components {
* @description The type of the plugin.
*/
type: string;
/**
* URL
* @description Optional URL that might be provided by some plugins to link to the remote source.
*/
url?: string | null;
/**
* Writeable
* @description Whether this files source plugin allows write access.
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.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

0 comments on commit d0f15e8

Please sign in to comment.