diff --git a/client/src/components/History/HistoryList.vue b/client/src/components/History/HistoryList.vue
index 26179330bdd6..6bc219d4e9bf 100644
--- a/client/src/components/History/HistoryList.vue
+++ b/client/src/components/History/HistoryList.vue
@@ -8,7 +8,7 @@
*
* - Filtering and searching histories with advanced filter options
* - Pagination for large history collections
- * - Bulk operations (delete, restore, add tags)
+ * - Bulk operations (delete, restore, purge, add tags, open in multiview)
* - Individual history selection and management
* - View mode switching (grid/list)
* - Sorting capabilities
@@ -19,7 +19,7 @@
*
*/
-import { faBurn, faPlus, faTags, faTrash, faTrashRestore } from "@fortawesome/free-solid-svg-icons";
+import { faBurn, faColumns, faPlus, faTags, faTrash, faTrashRestore } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
import { BAlert, BButton, BNav, BNavItem, BOverlay, BPagination } from "bootstrap-vue";
import { computed, onMounted, ref, watch } from "vue";
@@ -142,6 +142,7 @@ const noItems = computed(() => !loading.value && historiesLoaded.value.length ==
const noResults = computed(() => !loading.value && historiesLoaded.value.length === 0 && Boolean(filterText.value));
const deleteButtonTitle = computed(() => (showDeleted.value ? "Hide deleted histories" : "Show deleted histories"));
const showBulkPurge = computed(() => selectedHistories.value.some((h) => !h.purged));
+const showBulkMultiview = computed(() => selectedHistories.value.length > 1);
const historyListFilters = computed(() => getHistoryListFilters(props.activeList));
const rawFilters = computed(() =>
@@ -457,6 +458,55 @@ async function onBulkTagsAdd(tags: string[]) {
}
}
+const MULTIVIEW_MAX_HISTORIES = 10;
+
+/**
+ * Opens selected histories in the MultiviewPanel
+ * Pins the selected histories and navigates to the multiview page
+ */
+async function onBulkOpenInMultiview() {
+ const selectedHistoriesCount = selectedHistories.value.length;
+
+ if (selectedHistoriesCount === 0) {
+ return;
+ }
+
+ if (selectedHistoriesCount > MULTIVIEW_MAX_HISTORIES) {
+ const confirmed = await confirm(
+ `You have selected ${selectedHistoriesCount} histories to open in multiview.
+ However, the maximum number of histories allowed in multiview is ${MULTIVIEW_MAX_HISTORIES}.
+ Do you want to proceed with opening the first ${MULTIVIEW_MAX_HISTORIES} histories?`,
+ {
+ id: "bulk-open-multiview-histories",
+ title: `You can only open ${MULTIVIEW_MAX_HISTORIES} histories in multiview`,
+ okTitle: "Proceed",
+ okVariant: "primary",
+ cancelVariant: "outline-primary",
+ centered: true,
+ },
+ );
+ if (!confirmed) {
+ return;
+ }
+ selectedHistories.value.splice(MULTIVIEW_MAX_HISTORIES);
+ }
+
+ historyStore.clearPinnedHistories();
+ for (const history of selectedHistories.value) {
+ historyStore.pinHistory(history.id);
+ }
+
+ router.push("/histories/view_multiple");
+
+ const totalSelectedHistories = selectedHistories.value.length;
+
+ resetSelection();
+
+ Toast.success(
+ `Opened ${totalSelectedHistories} ${totalSelectedHistories === 1 ? "history" : "histories"} in multiview.`,
+ );
+}
+
/**
* Watches for changes in filter text, sort options, and sort direction
* to reload the history list with updated parameters
@@ -700,6 +750,18 @@ onMounted(async () => {
+
+
{
/** Reset to _default_ state; showing 4 latest updated histories */
function showRecent() {
- historyStore.pinnedHistories = [];
+ historyStore.clearPinnedHistories();
Toast.info(
"Showing the 4 most recently updated histories. Pin histories to this view by clicking on Select Histories.",
"History Multiview",
diff --git a/client/src/components/Panels/MultiviewPanel.vue b/client/src/components/Panels/MultiviewPanel.vue
index 3cd122055b46..00de7b68bcf9 100644
--- a/client/src/components/Panels/MultiviewPanel.vue
+++ b/client/src/components/Panels/MultiviewPanel.vue
@@ -72,7 +72,7 @@ async function createAndPin() {
/** Reset to _default_ state; showing 4 latest updated histories */
function pinRecent() {
- historyStore.pinnedHistories = [];
+ historyStore.clearPinnedHistories();
Toast.info(
"Showing the 4 most recently updated histories in Multiview. Pin histories to History Multiview by selecting them in the panel.",
"History Multiview",
diff --git a/client/src/stores/historyStore.ts b/client/src/stores/historyStore.ts
index 5634924480ea..6a452cc90d02 100644
--- a/client/src/stores/historyStore.ts
+++ b/client/src/stores/historyStore.ts
@@ -168,6 +168,10 @@ export const useHistoryStore = defineStore("historyStore", () => {
pinnedHistories.value = pinnedHistories.value.filter((h) => !historyIds.includes(h.id));
}
+ function clearPinnedHistories() {
+ pinnedHistories.value = [];
+ }
+
function selectHistory(history: HistorySummary) {
setHistory(history);
setCurrentHistoryId(history.id);
@@ -473,6 +477,7 @@ export const useHistoryStore = defineStore("historyStore", () => {
setHistories,
pinHistory,
unpinHistories,
+ clearPinnedHistories,
selectHistory,
applyFilters,
copyHistory,
diff --git a/client/src/utils/navigation/navigation.yml b/client/src/utils/navigation/navigation.yml
index 9ef8dbc62cda..aceb6592cc0d 100644
--- a/client/src/utils/navigation/navigation.yml
+++ b/client/src/utils/navigation/navigation.yml
@@ -239,7 +239,7 @@ history_panel:
metadata_file_download: '${_} [data-description="download ${metadata_name}"]'
dataset_operations: '${_} .dataset-actions'
-
+
expiration_indicator_badge: '${_} .expiration-indicator .badge'
# re-usable history editor, scoped for use in different layout scenarios (multi, etc.)
@@ -523,6 +523,8 @@ histories:
bulk_delete_confirm: '#bulk-delete-histories .btn.btn-danger'
bulk_restore_button: 'button[id="history-list-footer-bulk-restore-button"]'
bulk_restore_confirm: '#bulk-restore-histories .btn.btn-primary'
+ bulk_open_multiview_button: 'button[id="history-list-footer-bulk-open-multiview-button"]'
+ bulk_open_multiview_limit_confirm_button: '#bulk-open-multiview-histories .btn.btn-primary'
sharing:
selectors:
unshare_user_button: '.share_with_view .multiselect__tag-icon'
diff --git a/lib/galaxy_test/selenium/test_histories_list.py b/lib/galaxy_test/selenium/test_histories_list.py
index 5fb0fc19ba6a..338b5cc797c9 100644
--- a/lib/galaxy_test/selenium/test_histories_list.py
+++ b/lib/galaxy_test/selenium/test_histories_list.py
@@ -151,6 +151,79 @@ def test_delete_and_undelete_multiple_histories(self):
self.components.histories.reset_input.wait_for_and_click()
self.assert_histories_in_list([self.history2_name, self.history3_name])
+ @selenium_only("Not yet migrated to support Playwright backend")
+ @selenium_test
+ def test_bulk_open_in_multiview(self):
+ self._login()
+ self.navigate_to_histories_page()
+
+ # Select multiple histories
+ self.toggle_card_selection_in_list("#history-list", [self.history2_name, self.history3_name])
+
+ # Open selected histories in multiview
+ self.components.histories.bulk_open_multiview_button.wait_for_and_click()
+
+ # Wait for navigation to multiview page
+ self.sleep_for(self.wait_types.UX_RENDER)
+
+ # Verify we are on the multiview page
+ assert "/histories/view_multiple" in self.current_url
+
+ # Verify the selected histories are present in the multiview panel
+ present_histories = self.components.multi_history_panel.histories.all()
+ present_history_names = [self.get_history_name(history) for history in present_histories]
+ assert set(present_history_names) == {self.history2_name, self.history3_name}
+
+ @selenium_only("Not yet migrated to support Playwright backend")
+ @selenium_test
+ def test_bulk_open_in_multiview_limit_confirmation(self):
+ self._login()
+
+ # Create additional histories to exceed the limit (10 is the max)
+ additional_histories = []
+ for _i in range(10 + 1):
+ history_name = self._get_random_name()
+ self.create_history(history_name)
+ additional_histories.append(history_name)
+
+ self.navigate_to_histories_page()
+
+ # Select more than 10 histories (we'll select 11)
+ all_histories_to_select = additional_histories
+ self.toggle_card_selection_in_list("#history-list", all_histories_to_select)
+
+ # Click the bulk open multiview button
+ self.components.histories.bulk_open_multiview_button.wait_for_and_click()
+
+ # Wait for the confirmation dialog to appear
+ self.sleep_for(self.wait_types.UX_RENDER)
+
+ # Verify the confirmation dialog is displayed
+ confirm_dialog = self.wait_for_selector("#bulk-open-multiview-histories")
+ assert confirm_dialog.is_displayed()
+
+ # Verify the dialog contains information about the limit
+ dialog_text = confirm_dialog.text
+ assert "10" in dialog_text # The maximum number of histories
+ assert "11" in dialog_text # The number of histories selected
+
+ # Click confirm to proceed despite the limit
+ self.wait_for_and_click(self.components.histories.bulk_open_multiview_limit_confirm_button)
+
+ # Wait for dialog to close
+ self.sleep_for(self.wait_types.UX_RENDER)
+
+ # Verify we are on the multiview page
+ assert "/histories/view_multiple" in self.current_url
+
+ # Verify the maximum allowed histories are opened in multiview
+ present_histories = self.components.multi_history_panel.histories.all()
+ assert len(present_histories) == 10
+
+ def get_history_name(self, history):
+ history_name_element = history.find_element(By.CSS_SELECTOR, "[data-description='name display']")
+ return history_name_element.text
+
@selenium_only("Not yet migrated to support Playwright backend")
@selenium_test
def test_sort_by_name(self):