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 () => { + + + + Open in Multiview ({{ selectedHistories.length }}) + { /** 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):