Merge pull request #17571 from itisAliRH/collections-common-refactors

Collections common refactors
This commit is contained in:
David López
2024-03-13 12:58:58 +01:00
committed by GitHub
8 changed files with 379 additions and 337 deletions
+6
View File
@@ -61,3 +61,9 @@ export const fetchCollectionAttributes = fetcher
.path("/api/dataset_collections/{id}/attributes")
.method("get")
.create();
const postCopyCollection = fetcher.path("/api/dataset_collections/{id}/copy").method("post").create();
export async function copyCollection(id: string, dbkey: string): Promise<Record<string, never>> {
const { data } = await postCopyCollection({ id, dbkey });
return data;
}
+4
View File
@@ -8,3 +8,7 @@ export const undeleteHistory = fetcher.path("/api/histories/deleted/{history_id}
export const undeleteHistories = fetcher.path("/api/histories/batch/undelete").method("put").create();
export const publishedHistoriesFetcher = fetcher.path("/api/histories/published").method("get").create();
export const historyFetcher = fetcher.path("/api/histories/{history_id}").method("get").create();
export const updateHistoryItemsInBulk = fetcher
.path("/api/histories/{history_id}/contents/bulk")
.method("put")
.create();
@@ -1,14 +1,53 @@
<script setup lang="ts">
import { computed, ref } from "vue";
import Multiselect from "vue-multiselect";
import localize from "@/utils/localization";
interface Props {
// TODO: Replace with actual datatype type
datatypes: any[];
datatypeFromElements: string;
}
const props = defineProps<Props>();
const emit = defineEmits<{
// TODO: Replace with actual datatype type
(e: "clicked-save", datatype: any): void;
}>();
const currentDatatype = ref(props.datatypeFromElements);
const selectedDatatype = ref(props.datatypes.find((element) => element.id == props.datatypeFromElements));
const hasSelectedDatatype = computed(() => {
return Boolean(selectedDatatype.value);
});
const enableSave = computed(() => {
return hasSelectedDatatype.value && selectedDatatype.value.id != currentDatatype.value;
});
function clickedSave() {
emit("clicked-save", selectedDatatype.value);
currentDatatype.value = selectedDatatype.value.id;
}
</script>
<template>
<div>
<div>
<span class="float-left h-sm">Change Datatype/Extension of all elements in collection</span>
<div class="text-right">
<button class="save-datatype-edit btn btn-primary" :disabled="!enableSave" @click="clickedSave">
{{ l("Save") }}
{{ localize("Save") }}
</button>
</div>
</div>
<b>{{ l("New Type") }}: </b>
<b>{{ localize("New Type") }}: </b>
<Multiselect
v-if="hasSelectedDatatype"
v-model="selectedDatatype"
@@ -23,44 +62,3 @@
</Multiselect>
</div>
</template>
<script>
import Multiselect from "vue-multiselect";
export default {
components: { Multiselect },
props: {
datatypes: {
type: Array,
required: true,
},
datatypeFromElements: {
type: String,
required: true,
},
},
data: function () {
return {
selectedDatatype: {},
currentDatatype: "",
};
},
computed: {
hasSelectedDatatype() {
return Boolean(this.selectedDatatype);
},
enableSave() {
return this.hasSelectedDatatype && this.selectedDatatype.id != this.currentDatatype;
},
},
created() {
this.selectedDatatype = this.datatypes.find((element) => element.id == this.datatypeFromElements);
this.currentDatatype = this.datatypeFromElements;
},
methods: {
clickedSave: function () {
this.$emit("clicked-save", this.selectedDatatype);
this.currentDatatype = this.selectedDatatype.id;
},
},
};
</script>
@@ -1,3 +1,35 @@
<script setup lang="ts">
import { ref, watch } from "vue";
interface Props {
value: string;
title?: string;
}
const props = defineProps<Props>();
const emit = defineEmits<{
(e: "input", value: string): void;
}>();
const editable = ref(false);
const localValue = ref(props.value);
watch(
() => localValue.value,
(newLocalValue) => {
emit("input", newLocalValue);
}
);
watch(
() => props.value,
(newValue) => {
localValue.value = newValue;
}
);
</script>
<template>
<input
v-if="editable"
@@ -6,41 +38,13 @@
contenteditable
@blur="editable = false"
@keyup.enter="editable = false" />
<label v-else @click="editable = true">
{{ localValue }}
</label>
</template>
<script>
export default {
props: {
value: {
required: true,
type: String,
},
title: {
required: false,
type: String,
},
},
data: function () {
return {
editable: false,
localValue: this.value,
};
},
watch: {
localValue(newValue) {
this.$emit("input", newValue);
},
value(newValue) {
this.localValue = newValue;
},
},
};
</script>
<style>
<style scoped lang="scss">
.click-to-edit-input {
width: 600px;
line-height: 1 !important;
@@ -1,3 +1,55 @@
<script setup lang="ts">
import { library } from "@fortawesome/fontawesome-svg-core";
import { faChevronDown, faChevronUp } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
import { computed, ref, watch } from "vue";
import localize from "@/utils/localization";
library.add(faChevronDown, faChevronUp);
interface Props {
oncancel: () => void;
hideSourceItems: boolean;
suggestedName?: string;
renderExtensionsToggle?: boolean;
}
const props = withDefaults(defineProps<Props>(), {
suggestedName: "",
});
const emit = defineEmits<{
(e: "remove-extensions-toggle"): void;
(e: "clicked-create", value: string): void;
(e: "onUpdateHideSourceItems", value: boolean): void;
}>();
const isExpanded = ref(false);
const collectionName = ref(props.suggestedName);
const localHideSourceItems = ref(props.hideSourceItems);
const validInput = computed(() => {
return collectionName.value.length > 0;
});
function clickForHelp() {
isExpanded.value = !isExpanded.value;
return isExpanded.value;
}
function cancelCreate() {
props.oncancel();
}
watch(
() => localHideSourceItems.value,
() => {
emit("onUpdateHideSourceItems", localHideSourceItems.value);
}
);
</script>
<template>
<div class="collection-creator">
<div class="header flex-row no-flex">
@@ -6,69 +58,78 @@
class="more-help"
href="javascript:void(0);"
role="button"
:title="titleForHelp"
@click="_clickForHelp">
:title="localize('Expand or Close Help')"
@click="clickForHelp">
<div v-if="!isExpanded">
<i class="fas fa-chevron-down"></i>
<FontAwesomeIcon :icon="faChevronDown" />
</div>
<div v-else>
<i class="fas fa-chevron-up"></i>
<FontAwesomeIcon :icon="faChevronUp" />
</div>
</a>
<div class="help-content">
<!-- each collection that extends this will add their own help content -->
<slot name="help-content"></slot>
<a
class="more-help"
href="javascript:void(0);"
role="button"
:title="titleForHelp"
@click="_clickForHelp">
:title="localize('Expand or Close Help')"
@click="clickForHelp">
</a>
</div>
</div>
</div>
<div class="middle flex-row flex-row-container">
<slot name="middle-content"></slot>
</div>
<div class="footer flex-row no-flex">
<div class="attributes clear">
<div class="clear">
<label v-if="renderExtensionsToggle" class="setting-prompt float-right">
{{ removeFileExtensionsText }}
{{ localize("Remove file extensions?") }}
<input
class="remove-extensions float-right"
type="checkbox"
checked
@click="$emit('remove-extensions-toggle')" />
@click="emit('remove-extensions-toggle')" />
</label>
<label class="setting-prompt float-right">
{{ hideOriginalsText }}
{{ localize("Hide original elements?") }}
<input v-model="localHideSourceItems" class="hide-originals float-right" type="checkbox" />
</label>
</div>
<div class="clear">
<input
v-model="collectionName"
class="collection-name form-control float-right"
:placeholder="placeholderEnterName" />
:placeholder="localize('Enter a name for your new collection')" />
<div class="collection-name-prompt float-right">
{{ l("Name:") }}
{{ localize("Name:") }}
</div>
</div>
</div>
<div class="actions clear vertically-spaced">
<div class="float-left">
<button class="cancel-create btn" tabindex="-1" @click="_cancelCreate">
{{ l("Cancel") }}
<button class="cancel-create btn" tabindex="-1" @click="cancelCreate">
{{ localize("Cancel") }}
</button>
</div>
<div class="main-options float-right">
<button
class="create-collection btn btn-primary"
:disabled="!validInput"
@click="$emit('clicked-create', collectionName)">
{{ l("Create collection") }}
@click="emit('clicked-create', collectionName)">
{{ localize("Create collection") }}
</button>
</div>
</div>
@@ -76,71 +137,6 @@
</div>
</template>
<script>
import _l from "utils/localization";
export default {
props: {
oncancel: {
type: Function,
required: true,
},
renderExtensionsToggle: {
type: Boolean,
default: false,
},
hideSourceItems: {
type: Boolean,
required: true,
},
suggestedName: {
type: String,
required: false,
default: "",
},
},
data: function () {
return {
titleForHelp: _l("Expand or Close Help"),
hideOriginalsText: _l("Hide original elements?"),
titleMoreHelp: _l("Close and show more help"),
placeholderEnterName: _l("Enter a name for your new collection"),
dropdownText: _l("Create a <i>single</> pair"),
isExpanded: false,
collectionName: this.suggestedName,
removeFileExtensionsText: "Remove file extensions?",
localHideSourceItems: this.hideSourceItems,
};
},
computed: {
validInput: function () {
return this.collectionName.length > 0;
},
},
watch: {
localHideSourceItems() {
this.$emit("onUpdateHideSourceItems", this.localHideSourceItems);
},
},
methods: {
l(str) {
// _l conflicts private methods of Vue internals, expose as l instead
return _l(str);
},
_clickForHelp: function () {
this.isExpanded = !this.isExpanded;
return this.isExpanded;
},
_cancelCreate: function () {
this.oncancel();
},
_getName: function () {
return this.collectionName;
},
},
};
</script>
<style lang="scss">
$fa-font-path: "../../../../node_modules/@fortawesome/fontawesome-free/webfonts/";
@import "~@fortawesome/fontawesome-free/scss/_variables";
@@ -1,21 +1,155 @@
<script setup lang="ts">
import { library } from "@fortawesome/fontawesome-svg-core";
import { faBars, faCog, faDatabase, faTable, faUser } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
import axios from "axios";
import { BAlert, BSpinner, BTab, BTabs } from "bootstrap-vue";
import { storeToRefs } from "pinia";
import { computed, ref } from "vue";
import { copyCollection } from "@/api/datasetCollections";
import { DatatypesProvider, DbKeyProvider, SuitableConvertersProvider } from "@/components/providers";
import { useConfig } from "@/composables/config";
import { useCollectionAttributesStore } from "@/stores/collectionAttributesStore";
import { useHistoryStore } from "@/stores/historyStore";
import localize from "@/utils/localization";
import { prependPath } from "@/utils/redirect";
import { errorMessageAsString } from "@/utils/simple-error";
import { HistoryContentBulkOperationPayload, updateHistoryItemsBulk } from "./services";
import ChangeDatatypeTab from "@/components/Collections/common/ChangeDatatypeTab.vue";
import DatabaseEditTab from "@/components/Collections/common/DatabaseEditTab.vue";
import SuitableConvertersTab from "@/components/Collections/common/SuitableConvertersTab.vue";
import LoadingSpan from "@/components/LoadingSpan.vue";
library.add(faBars, faCog, faDatabase, faTable, faUser);
interface Props {
collectionId: string;
}
const props = defineProps<Props>();
const { config, isConfigLoaded } = useConfig(true);
const collectionAttributesStore = useCollectionAttributesStore();
const historyStore = useHistoryStore();
const { currentHistoryId } = storeToRefs(historyStore);
const jobError = ref(null);
const errorMessage = ref("");
const infoMessage = ref("This will create a new collection in your History. Your quota will not increase.");
const attributesData = computed(() => {
return collectionAttributesStore.getAttributes(props.collectionId);
});
const databaseKeyFromElements = computed(() => {
return attributesData.value?.dbkey;
});
const datatypeFromElements = computed(() => {
return attributesData.value?.extension;
});
function updateInfoMessage(strMessage: string) {
infoMessage.value = strMessage;
}
// TODO: Replace with actual datatype type
async function clickedSave(attribute: string, newValue: any) {
if (attribute !== "dbkey") {
// TODO: extend this to other attributes that could be changed
console.error(`Changing ${attribute} not implemented`);
return;
}
const dbKey = newValue.id;
try {
await copyCollection(props.collectionId, dbKey);
} catch (err) {
errorMessage.value = errorMessageAsString(err, "History import failed.");
}
}
// TODO: Replace with actual datatype type
async function clickedConvert(selectedConverter: any) {
const url = prependPath(`/api/tools/${selectedConverter.tool_id}/convert`);
const data = {
src: "hdca",
id: props.collectionId,
source_type: selectedConverter.original_type,
target_type: selectedConverter.target_type,
};
try {
await axios.post(url, data).catch(handleError);
} catch (err) {
errorMessage.value = errorMessageAsString(err, "History import failed.");
}
}
// TODO: Replace with actual datatype type
async function clickedDatatypeChange(selectedDatatype: any) {
const data: HistoryContentBulkOperationPayload = {
items: [
{
history_content_type: "dataset_collection",
id: props.collectionId,
},
],
operation: "change_datatype",
params: {
type: "change_datatype",
datatype: selectedDatatype.id,
},
};
try {
await updateHistoryItemsBulk(currentHistoryId.value ?? "", data);
} catch (err) {
errorMessage.value = errorMessageAsString(err, "History import failed.");
}
}
function handleError(err: any) {
errorMessage.value = errorMessageAsString(err, "History import failed.");
if (err?.data?.stderr) {
jobError.value = err.data;
}
}
</script>
<template>
<div aria-labelledby="collection-edit-view-heading">
<h1 id="collection-edit-view-heading" class="h-lg">{{ l("Edit Collection Attributes") }}</h1>
<b-alert show variant="info" dismissible>
{{ l(infoMessage) }}
</b-alert>
<div v-if="jobError">
<b-alert show variant="danger" dismissible>
{{ l(errorMessage) }}
</b-alert>
</div>
<b-tabs content-class="mt-3">
<b-tab
<h1 id="collection-edit-view-heading" class="h-lg">{{ localize("Edit Collection Attributes") }}</h1>
<BAlert show variant="info" dismissible>
{{ localize(infoMessage) }}
</BAlert>
<BAlert v-if="jobError" show variant="danger" dismissible>
{{ localize(errorMessage) }}
</BAlert>
<BTabs content-class="mt-3">
<BTab
title-link-class="collection-edit-change-genome-nav"
@click="updateInfoMessage(newCollectionMessage + ' ' + noQuotaIncreaseMessage)">
<template v-slot:title> <FontAwesomeIcon icon="table" /> &nbsp; {{ l("Database/Build") }}</template>
@click="
updateInfoMessage(
'This will create a new collection in your History. Your quota will not increase.'
)
">
<template v-slot:title>
<FontAwesomeIcon :icon="faTable" />
&nbsp; {{ localize("Database/Build") }}
</template>
<DbKeyProvider v-slot="{ item, loading }">
<div v-if="loading"><b-spinner label="Loading Database/Builds..."></b-spinner></div>
<div v-if="loading">
<BSpinner label="Loading Database/Builds..." />
</div>
<div v-else>
<DatabaseEditTab
v-if="item && databaseKeyFromElements"
@@ -24,23 +158,39 @@
@clicked-save="clickedSave" />
</div>
</DbKeyProvider>
</b-tab>
</BTab>
<SuitableConvertersProvider :id="collectionId" v-slot="{ item }">
<b-tab
<BTab
v-if="item && item.length"
title-link-class="collection-edit-convert-datatype-nav"
@click="updateInfoMessage(newCollectionMessage)">
<template v-slot:title> <FontAwesomeIcon icon="cog" /> &nbsp; {{ l("Convert") }}</template>
@click="updateInfoMessage('This will create a new collection in your History.')">
<template v-slot:title>
<FontAwesomeIcon :icon="faCog" />
&nbsp; {{ localize("Convert") }}
</template>
<SuitableConvertersTab :suitable-converters="item" @clicked-convert="clickedConvert" />
</b-tab>
</BTab>
</SuitableConvertersProvider>
<b-tab
<BTab
v-if="isConfigLoaded && config.enable_celery_tasks"
title-link-class="collection-edit-change-datatype-nav"
@click="updateInfoMessage(expectWaitTimeMessage)">
<template v-slot:title> <FontAwesomeIcon icon="database" /> &nbsp; {{ l("Datatypes") }} </template>
@click="
updateInfoMessage(
'This operation might take a short while, depending on the size of your collection.'
)
">
<template v-slot:title>
<FontAwesomeIcon :icon="faDatabase" />
&nbsp; {{ localize("Datatypes") }}
</template>
<DatatypesProvider v-slot="{ item, loading }">
<div v-if="loading"><LoadingSpan :message="loadingString" /></div>
<div v-if="loading">
<LoadingSpan message="Loading Datatypes" />
</div>
<div v-else>
<ChangeDatatypeTab
v-if="item && datatypeFromElements"
@@ -49,131 +199,7 @@
@clicked-save="clickedDatatypeChange" />
</div>
</DatatypesProvider>
</b-tab>
</b-tabs>
</BTab>
</BTabs>
</div>
</template>
<script>
import { library } from "@fortawesome/fontawesome-svg-core";
import { faBars, faCog, faDatabase, faTable, faUser } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
import axios from "axios";
import BootstrapVue from "bootstrap-vue";
import { mapState } from "pinia";
import { prependPath } from "utils/redirect";
import { errorMessageAsString } from "utils/simple-error";
import Vue from "vue";
import { useConfig } from "@/composables/config";
import { useCollectionAttributesStore } from "@/stores/collectionAttributesStore";
import { useHistoryStore } from "@/stores/historyStore";
import { DatatypesProvider, DbKeyProvider, SuitableConvertersProvider } from "../../providers";
import ChangeDatatypeTab from "./ChangeDatatypeTab";
import DatabaseEditTab from "./DatabaseEditTab";
import SuitableConvertersTab from "./SuitableConvertersTab";
import LoadingSpan from "@/components/LoadingSpan.vue";
library.add(faDatabase, faTable, faBars, faUser, faCog);
Vue.use(BootstrapVue);
export default {
components: {
DatabaseEditTab,
SuitableConvertersTab,
FontAwesomeIcon,
DbKeyProvider,
SuitableConvertersProvider,
ChangeDatatypeTab,
DatatypesProvider,
LoadingSpan,
},
props: {
collectionId: {
type: String,
required: true,
},
},
setup() {
const { config, isConfigLoaded } = useConfig(true);
return { config, isConfigLoaded };
},
data: function () {
return {
errorMessage: null,
jobError: null,
noQuotaIncrease: true,
loadingString: "Loading Datatypes",
infoMessage: "This will create a new collection in your History. Your quota will not increase.", //initialmessage on first/database tab
newCollectionMessage: "This will create a new collection in your History.",
noQuotaIncreaseMessage: "Your quota will not increase.",
expectWaitTimeMessage: "This operation might take a short while, depending on the size of your collection.",
};
},
computed: {
...mapState(useHistoryStore, ["currentHistoryId"]),
...mapState(useCollectionAttributesStore, ["getAttributes"]),
attributesData() {
return this.getAttributes(this.collectionId);
},
databaseKeyFromElements: function () {
return this.attributesData?.dbkey;
},
datatypeFromElements: function () {
return this.attributesData?.extension;
},
},
methods: {
updateInfoMessage: function (strMessage) {
this.infoMessage = strMessage;
},
clickedSave: function (attribute, newValue) {
const url = prependPath(`/api/dataset_collections/${this.collectionId}/copy`);
const data = {};
if (attribute == "dbkey") {
data["dbkey"] = newValue.id;
} else {
// TODO: extend this to other attributes that could be changed
console.error(`Changing ${attribute} not implemented`);
return;
}
axios.post(url, data).catch(this.handleError);
},
clickedConvert: function (selectedConverter) {
const url = prependPath(`/api/tools/${selectedConverter.tool_id}/convert`);
const data = {
src: "hdca",
id: this.collectionId,
source_type: selectedConverter.original_type,
target_type: selectedConverter.target_type,
};
axios.post(url, data).catch(this.handleError);
},
clickedDatatypeChange: function (selectedDatatype) {
const url = prependPath(`/api/histories/${this.currentHistoryId}/contents/bulk`);
const data = {
operation: "change_datatype",
items: [
{
history_content_type: "dataset_collection",
id: this.collectionId,
},
],
params: {
type: "change_datatype",
datatype: selectedDatatype.id,
},
};
axios.put(url, data).catch(this.handleError);
},
handleError: function (err) {
this.errorMessage = errorMessageAsString(err, "History import failed.");
if (err?.data?.stderr) {
this.jobError = err.data;
}
},
},
};
</script>
@@ -1,17 +1,48 @@
<script setup lang="ts">
import { onMounted, ref } from "vue";
import Multiselect from "vue-multiselect";
import localize from "@/utils/localization";
interface Props {
genomes: { id: string; text: string }[];
databaseKeyFromElements: string;
}
const props = defineProps<Props>();
const emit = defineEmits<{
(event: "clicked-save", attribute: string, newValue: any): void;
}>();
const selectedGenome = ref();
function clickedSave() {
emit("clicked-save", "dbkey", selectedGenome.value);
}
onMounted(() => {
selectedGenome.value = props.genomes.find((element) => element.id == props.databaseKeyFromElements);
});
</script>
<template>
<div>
<div>
<span class="float-left h-sm">Change Database/Build of all elements in collection</span>
<div class="text-right">
<button
class="save-dbkey-edit btn btn-primary"
:disabled="selectedGenome.id == databaseKeyFromElements"
:disabled="selectedGenome?.id == databaseKeyFromElements"
@click="clickedSave">
{{ l("Save") }}
{{ localize("Save") }}
</button>
</div>
</div>
<b>{{ l("Database/Build") }}: </b>
<b>{{ localize("Database/Build") }}: </b>
<Multiselect
v-model="selectedGenome"
class="database-dropdown"
@@ -21,38 +52,7 @@
:options="genomes"
:searchable="true"
:allow-empty="false">
{{ selectedGenome.text }}
{{ selectedGenome?.text }}
</Multiselect>
</div>
</template>
<script>
import Multiselect from "vue-multiselect";
export default {
components: { Multiselect },
props: {
genomes: {
type: Array,
required: true,
},
databaseKeyFromElements: {
type: String,
required: true,
},
},
data: function () {
return {
selectedGenome: {},
};
},
created() {
this.selectedGenome = this.genomes.find((element) => element.id == this.databaseKeyFromElements);
},
methods: {
clickedSave: function () {
this.$emit("clicked-save", "dbkey", this.selectedGenome);
this.selectedGenome = this.genomes.find((element) => element.id == this.databaseKeyFromElements);
},
},
};
</script>
@@ -0,0 +1,8 @@
import { updateHistoryItemsInBulk } from "@/api/histories";
import { type components } from "@/api/schema";
export type HistoryContentBulkOperationPayload = components["schemas"]["HistoryContentBulkOperationPayload"];
export async function updateHistoryItemsBulk(historyId: string, changes: HistoryContentBulkOperationPayload) {
await updateHistoryItemsInBulk({ history_id: historyId, ...changes });
}