diff --git a/client/src/api/index.ts b/client/src/api/index.ts index bfbda047abde..24ebdb666137 100644 --- a/client/src/api/index.ts +++ b/client/src/api/index.ts @@ -10,6 +10,13 @@ export { type components, GalaxyApi, type GalaxyApiPaths }; */ export type HistorySummary = components["schemas"]["HistorySummary"]; +/** + * Represents the possible values for the `sort_by` parameter when querying histories. + * We can not extract this from the schema for an unknown reason. + * The desired solution would be: `GalaxyApiPaths["/api/histories"]["get"]["parameters"]["query"]["sort_by"]`. + */ +export type HistorySortByLiteral = "create_time" | "name" | "update_time" | "username" | undefined; + /** * Contains minimal information about a History with additional content stats. * This is a subset of information that can be relatively frequently updated after diff --git a/client/src/components/FilesDialog/FilesDialog.test.ts b/client/src/components/FilesDialog/FilesDialog.test.ts index c158f71a21bb..54cd7dcddf8f 100644 --- a/client/src/components/FilesDialog/FilesDialog.test.ts +++ b/client/src/components/FilesDialog/FilesDialog.test.ts @@ -4,7 +4,7 @@ import flushPromises from "flush-promises"; import { getLocalVue } from "tests/jest/helpers"; import { useServerMock } from "@/api/client/__mocks__"; -import { SELECTION_STATES, type SelectionItem } from "@/components/SelectionDialog/selectionTypes"; +import { SELECTION_STATES, type SelectionItem, type SelectionState } from "@/components/SelectionDialog/selectionTypes"; /** * The following imports mock a remote file resource directory structure, @@ -57,7 +57,7 @@ jest.mock("@/composables/config", () => ({ const { server, http } = useServerMock(); interface RowElement extends SelectionItem, Element { - _rowVariant: string; + _rowVariant: SelectionState; } function paramsToKey(query: { target?: string | null; recursive?: string | null; writeable?: string | null }): string { @@ -392,7 +392,7 @@ class Utils { } expectSelectAllIconStatusToBe(status: string) { - expect(this.getSelectionDialog().props("selectAllIcon")).toBe(status); + expect(this.getSelectionDialog().props("selectAllVariant")).toBe(status); } expectNoErrorMessage() { diff --git a/client/src/components/FilesDialog/FilesDialog.vue b/client/src/components/FilesDialog/FilesDialog.vue index f61c7c372c85..74c0647e3680 100644 --- a/client/src/components/FilesDialog/FilesDialog.vue +++ b/client/src/components/FilesDialog/FilesDialog.vue @@ -16,6 +16,7 @@ import { type ItemsProviderContext, SELECTION_STATES, type SelectionItem, + type SelectionState, } from "@/components/SelectionDialog/selectionTypes"; import { useConfig } from "@/composables/config"; import { useFileSources } from "@/composables/fileSources"; @@ -73,7 +74,7 @@ const showDetails = ref(true); const isBusy = ref(false); const currentDirectory = ref(); const showFTPHelper = ref(false); -const selectAllIcon = ref(SELECTION_STATES.UNSELECTED); +const selectAllIcon = ref(SELECTION_STATES.UNSELECTED); const urlTracker = ref(new UrlTracker("")); const totalItems = ref(0); @@ -415,7 +416,7 @@ onMounted(() => { :modal-static="modalStatic" :multiple="multiple" :options-show="optionsShow" - :select-all-icon="selectAllIcon" + :select-all-variant="selectAllIcon" :show-select-icon="undoShow && multiple" :undo-show="undoShow" @onCancel="() => (modalShow = false)" diff --git a/client/src/components/Grid/configs/histories.ts b/client/src/components/Grid/configs/histories.ts index c50dc108b267..cee19089ade9 100644 --- a/client/src/components/Grid/configs/histories.ts +++ b/client/src/components/Grid/configs/histories.ts @@ -12,6 +12,7 @@ import { import { useEventBus } from "@vueuse/core"; import { GalaxyApi } from "@/api"; +import { type HistorySortByLiteral } from "@/api"; import { updateTags } from "@/api/tags"; import { useHistoryStore } from "@/stores/historyStore"; import Filtering, { contains, equals, expandNameTag, toBool, type ValidFilter } from "@/utils/filtering"; @@ -26,7 +27,6 @@ const { emit } = useEventBus("grid-router-push"); * Local types */ type HistoryEntry = Record; -type SortKeyLiteral = "create_time" | "name" | "update_time" | undefined; /** * Request and return data from server @@ -40,7 +40,7 @@ async function getData(offset: number, limit: number, search: string, sort_by: s limit, offset, search, - sort_by: sort_by as SortKeyLiteral, + sort_by: sort_by as HistorySortByLiteral, sort_desc, show_own: true, show_published: false, diff --git a/client/src/components/Grid/configs/historiesShared.ts b/client/src/components/Grid/configs/historiesShared.ts index 667ef220a338..6baab4a2ece5 100644 --- a/client/src/components/Grid/configs/historiesShared.ts +++ b/client/src/components/Grid/configs/historiesShared.ts @@ -2,6 +2,7 @@ import { faEye } from "@fortawesome/free-solid-svg-icons"; import { useEventBus } from "@vueuse/core"; import { GalaxyApi } from "@/api"; +import { type HistorySortByLiteral } from "@/api"; import { updateTags } from "@/api/tags"; import Filtering, { contains, expandNameTag, type ValidFilter } from "@/utils/filtering"; import _l from "@/utils/localization"; @@ -15,7 +16,6 @@ const { emit } = useEventBus("grid-router-push"); * Local types */ type HistoryEntry = Record; -type SortKeyLiteral = "create_time" | "name" | "update_time" | undefined; /** * Request and return data from server @@ -29,7 +29,7 @@ async function getData(offset: number, limit: number, search: string, sort_by: s limit, offset, search, - sort_by: sort_by as SortKeyLiteral, + sort_by: sort_by as HistorySortByLiteral, sort_desc, show_own: false, show_published: false, diff --git a/client/src/components/Libraries/LibraryFolder/TopToolbar/FolderTopBar.vue b/client/src/components/Libraries/LibraryFolder/TopToolbar/FolderTopBar.vue index bd1f6434f1e9..bb023fdc3ca2 100644 --- a/client/src/components/Libraries/LibraryFolder/TopToolbar/FolderTopBar.vue +++ b/client/src/components/Libraries/LibraryFolder/TopToolbar/FolderTopBar.vue @@ -2,17 +2,27 @@ import { library } from "@fortawesome/fontawesome-svg-core"; import { faBook, faCaretDown, faDownload, faHome, faPlus, faTrash } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome"; -import { BButton, BDropdown, BDropdownDivider, BDropdownGroup, BDropdownItem, BFormCheckbox } from "bootstrap-vue"; +import { + BAlert, + BButton, + BDropdown, + BDropdownDivider, + BDropdownGroup, + BDropdownItem, + BFormCheckbox, +} from "bootstrap-vue"; import { storeToRefs } from "pinia"; -import { computed, onMounted, ref } from "vue"; +import { computed, reactive, ref } from "vue"; -import { getGalaxyInstance } from "@/app"; +import { GalaxyApi } from "@/api"; import { Services } from "@/components/Libraries/LibraryFolder/services"; import mod_add_datasets from "@/components/Libraries/LibraryFolder/TopToolbar/add-datasets"; import { deleteSelectedItems } from "@/components/Libraries/LibraryFolder/TopToolbar/delete-selected"; import download from "@/components/Libraries/LibraryFolder/TopToolbar/download"; import mod_import_collection from "@/components/Libraries/LibraryFolder/TopToolbar/import-to-history/import-collection"; import mod_import_dataset from "@/components/Libraries/LibraryFolder/TopToolbar/import-to-history/import-dataset"; +import { type SelectionItem } from "@/components/SelectionDialog/selectionTypes"; +import { useConfig } from "@/composables/config"; import { type DetailedDatatypes, useDetailedDatatypes } from "@/composables/datatypes"; import { Toast } from "@/composables/toast"; import { useDbKeyStore } from "@/stores/dbKeyStore"; @@ -21,6 +31,8 @@ import { useUserStore } from "@/stores/userStore"; import FolderDetails from "@/components/Libraries/LibraryFolder/FolderDetails/FolderDetails.vue"; import LibraryBreadcrumb from "@/components/Libraries/LibraryFolder/LibraryBreadcrumb.vue"; import SearchField from "@/components/Libraries/LibraryFolder/SearchField.vue"; +import ProgressBar from "@/components/ProgressBar.vue"; +import HistoryDatasetPicker from "@/components/SelectionDialog/HistoryDatasetPicker.vue"; library.add(faBook, faCaretDown, faDownload, faHome, faPlus, faTrash); @@ -51,6 +63,8 @@ const emit = defineEmits<{ (e: "update:includeDeleted", value: boolean): void; }>(); +const { config, isConfigLoaded } = useConfig(); + const userStore = useUserStore(); const { isAdmin } = storeToRefs(userStore); @@ -58,11 +72,17 @@ const { datatypes } = useDetailedDatatypes(); const dbKeyStore = useDbKeyStore(); -const libraryImportDir = ref(false); -const allowLibraryPathPaste = ref(false); +const modalShow = ref(""); const genomesList = ref([]); const extensionsList = ref([]); -const userLibraryImportDirAvailable = ref(false); +const progress = ref(false); +const progressNote = ref(""); +const progressStatus = reactive({ + total: 0, + okCount: 0, + errorCount: 0, + runningCount: 0, +}); const auto = ref({ id: "auto", extension: "auto", @@ -76,10 +96,11 @@ const auto = ref({ description_url: "", }); -const Galaxy = getGalaxyInstance(); - const services = new Services(); +const libraryImportDir = computed(() => isConfigLoaded && config.value?.library_import_dir); +const allowLibraryPathPaste = computed(() => isConfigLoaded && config.value?.allow_library_path_paste); +const userLibraryImportDirAvailable = computed(() => isConfigLoaded && config.value?.user_library_import_dir_available); const containsFileOrFolder = computed(() => { return props.folderContents.find((el) => el.type === "folder" || el.type === "file"); }); @@ -87,7 +108,7 @@ const canDelete = computed(() => { return !!(containsFileOrFolder.value && isAdmin.value); }); const datasetManipulation = computed(() => { - return !!(containsFileOrFolder.value && Galaxy.user); + return !!(containsFileOrFolder.value && userStore.currentUser); }); const totalRows = computed(() => { return props.metadata?.total_rows ?? 0; @@ -201,6 +222,10 @@ async function importToHistoryModal(isCollection: boolean) { } } +function onAddDatasets(source: string = "") { + modalShow.value = source; +} + // TODO: after replacing the selection dialog with the new component that is not using jquery async function addDatasets(source: string) { await fetchExtAndGenomes(); @@ -241,11 +266,63 @@ async function fetchExtAndGenomes() { } } -onMounted(async () => { - libraryImportDir.value = Galaxy.config.library_import_dir; - allowLibraryPathPaste.value = Galaxy.config.allow_library_path_paste; - userLibraryImportDirAvailable.value = Galaxy.config.user_library_import_dir_available; -}); +function resetProgress() { + progressStatus.total = 0; + progressStatus.okCount = 0; + progressStatus.errorCount = 0; + progressStatus.runningCount = 0; +} + +async function onAddDatasetsFromHistory(selectedDatasets: SelectionItem[]) { + resetProgress(); + + progress.value = true; + progressStatus.total = selectedDatasets.length; + progressNote.value = "Adding datasets to the folder"; + + emit("setBusy", true); + + for (const dataset of selectedDatasets) { + try { + progressStatus.runningCount++; + + const { error } = await GalaxyApi().POST("/api/folders/{folder_id}/contents", { + params: { + path: { folder_id: props.folderId }, + }, + body: { + ldda_message: null, + from_hda_id: dataset.id, + }, + }); + + if (error) { + throw new Error(error.err_msg); + } + + progressStatus.okCount++; + } catch (err) { + progressStatus.errorCount++; + } finally { + progressStatus.runningCount--; + } + } + + if (progressStatus.errorCount > 0) { + progressNote.value = `Added ${progressStatus.okCount} dataset${ + progressStatus.okCount > 1 ? "s" : "" + }, but failed to add ${progressStatus.errorCount} dataset${ + progressStatus.errorCount > 1 ? "s" : "" + } to the folder`; + } else { + progressNote.value = `Added ${progressStatus.okCount} dataset${ + progressStatus.okCount > 1 ? "s" : "" + } to the folder`; + } + + emit("setBusy", false); + emit("fetchFolderContents"); +} - from History + from History from User Directory @@ -352,9 +429,25 @@ onMounted(async () => { + + + + + + diff --git a/client/src/components/Libraries/LibraryFolder/TopToolbar/add-datasets.js b/client/src/components/Libraries/LibraryFolder/TopToolbar/add-datasets.js index 5733e40d4957..42e5db51eb4b 100644 --- a/client/src/components/Libraries/LibraryFolder/TopToolbar/add-datasets.js +++ b/client/src/components/Libraries/LibraryFolder/TopToolbar/add-datasets.js @@ -32,9 +32,6 @@ var AddDatasets = Backbone.View.extend({ */ showImportModal: function (options) { switch (options.source) { - case "history": - this.addFilesFromHistoryModal(); - break; case "importdir": this.importFilesFromGalaxyFolderModal({ source: "importdir", @@ -176,46 +173,6 @@ var AddDatasets = Backbone.View.extend({ value: "auto", }); }, - addFilesFromHistoryModal: function () { - const Galaxy = getGalaxyInstance(); - this.histories = new mod_library_model.GalaxyHistories(); - this.histories - .fetch() - .done(() => { - this.modal = Galaxy.modal; - var template_modal = this.templateAddFilesFromHistory(); - this.modal.show({ - closing_events: true, - title: _l("Adding datasets from your history"), - body: template_modal({ - histories: this.histories.models, - }), - buttons: { - Add: () => { - this.addAllDatasetsFromHistory(); - }, - Close: () => { - Galaxy.modal.hide(); - }, - }, - closing_callback: () => { - // TODO update table without fetching new content from the server - // Galaxy.libraries.library_router.navigate(`folders/${this.id}`, { trigger: true }); - }, - }); - this.fetchAndDisplayHistoryContents(this.histories.models[0].id); - $("#dataset_add_bulk").change((event) => { - this.fetchAndDisplayHistoryContents(event.target.value); - }); - }) - .fail((model, response) => { - if (typeof response.responseJSON !== "undefined") { - Toast.error(response.responseJSON.err_msg); - } else { - Toast.error("An error occurred."); - } - }); - }, /** * Create modal for importing from given directory * on Galaxy. Bind jQuery events. @@ -546,49 +503,6 @@ var AddDatasets = Backbone.View.extend({ this.chainCallImportingFolders(options); }); }, - - fetchAndDisplayHistoryContents: function (history_id) { - var history_contents = new mod_library_model.HistoryContents({ - id: history_id, - }); - history_contents.fetch({ - success: (history_contents) => { - var history_contents_template = this.templateHistoryContents(); - - if (history_contents.length > 0) { - this.histories.get(history_id).set({ contents: history_contents }); - this.modal.$el.find(".library_selected_history_content").html( - history_contents_template({ - history_contents: history_contents.models.reverse(), - }) - ); - this.modal.$el.find(".history-import-select-all").bind("click", () => { - $(".library_selected_history_content [type=checkbox]").prop("checked", true); - }); - this.modal.$el.find(".history-import-unselect-all").bind("click", () => { - $(".library_selected_history_content [type=checkbox]").prop("checked", false); - }); - - this.modal.$el.find(".history-import-toggle-all").bind("click", (e) => { - this.selectAll(e); - }); - - this.modal.$el.find(".dataset_row").bind("click", (e) => { - this.selectClickedRow(e); - }); - } else { - this.modal.$el.find(".library_selected_history_content").html(`

Selected history is empty.

`); - } - }, - error: (model, response) => { - if (typeof response.responseJSON !== "undefined") { - Toast.error(response.responseJSON.err_msg); - } else { - Toast.error("An error occurred."); - } - }, - }); - }, templateAddingDatasetsProgressBar: function () { return _.template( `
@@ -602,180 +516,6 @@ var AddDatasets = Backbone.View.extend({
` ); }, - templateAddFilesFromHistory: function () { - return _.template( - `
-
- - -
-
-
-
` - ); - }, - templateHistoryContents: function () { - return _.template( - `
- -
- - - - - - - - - - <% _.each(history_contents, function(history_item) { %> - <% if (history_item.get("deleted") != true ) { %> - <% var item_name = history_item.get("name") %> - <% if (history_item.get("type") === "collection") { %> - <% var collection_type = history_item.get("collection_type") %> - <% if (collection_type === "list") { %> - " - data-name="<%= _.escape(history_item.get("type")) %>"> - - - - - <% } else { %> - - - - - - <% } %> - <% } else if (history_item.get("visible") === true && history_item.get("state") === "ok") { %> - " - data-name="<%= _.escape(history_item.get("type")) %>"> - - - - - <% } %> - <% } %> - <% }); %> - -
- - Name
<%= _.escape(history_item.get("hid")) %> - <%= item_name.length > 75 ? _.escape("...".concat(item_name.substr(-75))) : _.escape(item_name) %> - (Dataset Collection) -
<%= _.escape(history_item.get("hid")) %> - <%= item_name.length > 75 ? _.escape("...".concat(item_name.substr(-75))) : _.escape(item_name) %> - (Dataset Collection of type <%= _.escape(collection_type) %> not supported.) -
<%= _.escape(history_item.get("hid")) %> - <%= item_name.length > 75 ? _.escape("...".concat(item_name.substr(-75))) : _.escape(item_name) %> -
-
-
` - ); - }, - /** - * Check checkbox if user clicks on the whole row or - * on the checkbox itself - */ - selectClickedRow: function (event) { - var checkbox = ""; - var $row; - var source; - $row = $(event.target).closest("tr"); - if (event.target.localName === "input") { - checkbox = event.target; - source = "input"; - } else if (event.target.localName === "td") { - checkbox = $row.find(":checkbox")[0]; - source = "td"; - } - if (checkbox.checked) { - if (source === "td") { - checkbox.checked = ""; - this.makeWhiteRow($row); - } else if (source === "input") { - this.makeDarkRow($row); - } - } else { - if (source === "td") { - checkbox.checked = "selected"; - this.makeDarkRow($row); - } else if (source === "input") { - this.makeWhiteRow($row); - } - } - }, - - /** - * User clicked the checkbox in the table heading - * @param {context} event - */ - selectAll: function (event) { - var selected = event.target.checked; - var self = this; - // Iterate each checkbox - $(":checkbox", "#dataset_list tbody").each(function () { - this.checked = selected; - var $row = $(this).closest("tr"); - // Change color of selected/unselected - if (selected) { - self.makeDarkRow($row); - } else { - self.makeWhiteRow($row); - } - }); - }, - - makeDarkRow: function ($row) { - $row.addClass("table-primary"); - }, - - makeWhiteRow: function ($row) { - $row.removeClass("table-primary"); - }, - /** - * Import all selected datasets from history into the current folder. - */ - addAllDatasetsFromHistory: function () { - var checked_hdas = this.modal.$el.find(".library_selected_history_content").find(":checked"); - var history_item_ids = []; // can be hda or hdca - var history_item_types = []; - var items_to_add = []; - if (checked_hdas.length < 1) { - Toast.info("You must select some datasets first."); - } else { - this.modal.disableButton("Add"); - checked_hdas.each(function () { - var hid = $(this).closest("tr").data("id"); - if (hid) { - var item_type = $(this).closest("tr").data("name"); - history_item_ids.push(hid); - history_item_types.push(item_type); - } - }); - for (let i = history_item_ids.length - 1; i >= 0; i--) { - var history_item_id = history_item_ids[i]; - var folder_item = new mod_library_model.Item(); - folder_item.url = `${getAppRoot()}api/folders/${this.options.id}/contents`; - if (history_item_types[i] === "collection") { - folder_item.set({ from_hdca_id: history_item_id }); - } else { - folder_item.set({ from_hda_id: history_item_id }); - } - items_to_add.push(folder_item); - } - this.initChainCallControlAddingDatasets({ - length: items_to_add.length, - }); - this.chainCallAddingHdas(items_to_add); - } - }, initChainCallControlAddingDatasets: function (options) { var template; template = this.templateAddingDatasetsProgressBar(); diff --git a/client/src/components/SelectionDialog/HistoryDatasetPicker.vue b/client/src/components/SelectionDialog/HistoryDatasetPicker.vue new file mode 100644 index 000000000000..598146cb699e --- /dev/null +++ b/client/src/components/SelectionDialog/HistoryDatasetPicker.vue @@ -0,0 +1,325 @@ + + + diff --git a/client/src/components/SelectionDialog/SelectionDialog.vue b/client/src/components/SelectionDialog/SelectionDialog.vue index 7a9c08ebaa0d..b05d8d1ee08c 100644 --- a/client/src/components/SelectionDialog/SelectionDialog.vue +++ b/client/src/components/SelectionDialog/SelectionDialog.vue @@ -1,15 +1,18 @@