use PageDisplayToolbar in editor view as well

This commit is contained in:
Ahmed Awan
2026-06-16 12:09:11 -05:00
parent 56293a7e87
commit aad2f585ba
8 changed files with 80 additions and 145 deletions
@@ -23,6 +23,7 @@ const localVue = getLocalVue();
const WORKFLOW_ID = "workflow-abc123";
const WORKFLOW_NAME = "My Test Workflow";
const INPUT_SELECTOR = "[data-description='workflow name input']";
async function mountRenameModal(name = WORKFLOW_NAME) {
const wrapper = mount(RenameModal as object, {
@@ -52,7 +53,7 @@ describe("RenameModal tested for renaming workflows", () => {
const wrapper = await mountRenameModal();
await createWrapper(document.body).find("#workflow-name-input").setValue("Renamed Workflow");
await createWrapper(document.body).find(INPUT_SELECTOR).setValue("Renamed Workflow");
wrapper.findComponent(GModal).vm.$emit("ok");
await flushPromises();
@@ -65,7 +66,7 @@ describe("RenameModal tested for renaming workflows", () => {
const wrapper = await mountRenameModal();
await createWrapper(document.body).find("#workflow-name-input").setValue("Attempted New Name");
await createWrapper(document.body).find(INPUT_SELECTOR).setValue("Attempted New Name");
wrapper.findComponent(GModal).vm.$emit("ok");
await flushPromises();
@@ -77,7 +78,7 @@ describe("RenameModal tested for renaming workflows", () => {
// Simulate parent closing and reopening the modal (destroys old instance)
wrapper.destroy();
await mountRenameModal();
expect((createWrapper(document.body).find("#workflow-name-input").element as HTMLInputElement).value).toBe(
expect((createWrapper(document.body).find(INPUT_SELECTOR).element as HTMLInputElement).value).toBe(
WORKFLOW_NAME,
);
});
+2 -2
View File
@@ -63,15 +63,15 @@ async function onRename(newName: string) {
show
:ok-text="localize('Rename')"
:ok-disabled="nameInvalid || renaming"
:title="`Rename ${props.itemType}: ${props.name}`"
:title="`Rename ${capitalizeFirstLetter(props.itemType)}: ${props.name}`"
confirm
:close-on-ok="false"
@ok="onRename(nameModel)"
@close="emit('close')"
@cancel="emit('close')">
<GFormInput
:id="`${props.itemType}-name-input`"
v-model="nameModel"
:data-description="`${props.itemType} name input`"
class="w-100"
:disabled="renaming"
type="text"
@@ -1,22 +1,23 @@
<script setup lang="ts">
import { faArrowLeft, faEye, faHistory, faSave, faSpinner, faUsers } from "@fortawesome/free-solid-svg-icons";
import { faSpinner, faUsers } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
import { BAlert, BBadge, BButton } from "bootstrap-vue";
import { BAlert } from "bootstrap-vue";
import { computed, onMounted, onUnmounted, ref, watch } from "vue";
import { useRouter } from "vue-router/composables";
import { getGalaxyInstance } from "@/app";
import type { RouterPushOptions } from "@/components/History/Content/router-push-options";
// import { getGalaxyInstance } from "@/app";
// import type { RouterPushOptions } from "@/components/History/Content/router-push-options";
import { PAGE_LABELS } from "@/components/Page/constants";
import { useWindowAwareNavigation } from "@/composables/windowAwareNavigation";
import { useHistoryStore } from "@/stores/historyStore";
import { type PageEditorMode, usePageEditorStore } from "@/stores/pageEditorStore";
import GButton from "../BaseComponents/GButton.vue";
import ObjectPermissionsModal from "./ObjectPermissionsModal.vue";
import PageDisplayOnly from "./PageDisplayOnly.vue";
import PageDisplayToolbar from "./PageDisplayToolbar.vue";
import PageRevisionList from "./PageRevisionList.vue";
import PageRevisionView from "./PageRevisionView.vue";
import ClickToEdit from "@/components/ClickToEdit.vue";
import MarkdownEditor from "@/components/Markdown/MarkdownEditor.vue";
const props = defineProps<{
@@ -125,39 +126,33 @@ function handleEdit() {
}
}
async function handleSave() {
await store.savePage();
}
async function handleSaveAndView() {
await store.savePage();
if (props.invocationId) {
router.push(`/workflows/invocations/${props.invocationId}/reports?id=${props.pageId}`);
return;
}
if (store.currentPage) {
const Galaxy = getGalaxyInstance();
const isWmActive = Galaxy?.frame?.active;
if (isWmActive) {
const url = `/published/page?id=${props.pageId}&embed=true`;
const options: RouterPushOptions = {
title: `${labels.value.entityName}: ${store.currentTitle || labels.value.defaultTitle}`,
preventWindowManager: false,
};
// @ts-ignore - monkeypatched router
router.push(url, options);
} else {
const data = store.currentPage as any;
if (data.username && data.slug) {
window.location.href = `/u/${data.username}/p/${data.slug}`;
}
}
}
}
function handleTitleChange(newTitle: string) {
store.updateTitle(newTitle);
}
// TODO: Uncomment when router guards with unsaved changes protection are implemented
// Before, we had a Save & View button that is now removed.
// async function handleSaveAndView() {
// await store.savePage();
// if (props.invocationId) {
// router.push(`/workflows/invocations/${props.invocationId}/reports?id=${props.pageId}`);
// return;
// }
// if (store.currentPage) {
// const Galaxy = getGalaxyInstance();
// const isWmActive = Galaxy?.frame?.active;
// if (isWmActive) {
// const url = `/published/page?id=${props.pageId}&embed=true`;
// const options: RouterPushOptions = {
// title: `${labels.value.entityName}: ${store.currentTitle || labels.value.defaultTitle}`,
// preventWindowManager: false,
// };
// // @ts-ignore - monkeypatched router
// router.push(url, options);
// } else {
// const data = store.currentPage as any;
// if (data.username && data.slug) {
// window.location.href = `/u/${data.username}/p/${data.slug}`;
// }
// }
// }
// }
function handleContentUpdate(newContent: string) {
store.updateContent(newContent);
@@ -208,78 +203,24 @@ function handleRevisionRestore(revisionId: string) {
<!-- Edit mode: toolbar + editor + optional chat/revision panels -->
<template v-else-if="store.hasCurrentPage">
<div class="d-flex align-items-center p-2 border-bottom" data-description="page editor toolbar">
<BButton variant="link" size="sm" data-description="page back button" @click="handleBack">
<FontAwesomeIcon :icon="faArrowLeft" />
{{ labels.editorBackLabel }}
</BButton>
<ClickToEdit
:value="store.currentTitle || labels.defaultTitle"
tag-name="span"
:placeholder="labels.defaultTitle"
class="flex-grow-1 text-center font-weight-bold"
data-description="page editor title"
@input="handleTitleChange" />
<BButton
variant="outline-primary"
size="sm"
class="mr-2"
data-description="page revisions button"
@click="store.toggleRevisions">
<FontAwesomeIcon :icon="faHistory" />
Revisions
<BBadge v-if="store.revisionCount > 0" variant="light" class="ml-1">
{{ store.revisionCount }}
</BBadge>
</BButton>
<BButton
variant="outline-primary"
size="sm"
class="mr-2"
data-description="page preview button"
@click="handlePreview">
<FontAwesomeIcon :icon="faEye" />
Preview
</BButton>
<template v-if="isStandalone">
<PageDisplayToolbar :labels="labels" mode="editor" @preview="handlePreview" @back="handleBack">
<template v-if="isStandalone" v-slot:extra-actions>
<ObjectPermissionsModal
id="object-permissions-modal"
v-model="showPermissions"
:markdown-content="store.currentContent" />
<BButton
<GButton
v-b-modal:object-permissions-modal
variant="outline-primary"
size="sm"
class="mr-2"
data-description="page permissions button">
color="blue"
outline
size="small"
data-description="page permissions button"
@click="showPermissions = true">
<FontAwesomeIcon :icon="faUsers" />
Permissions
</BButton>
</GButton>
</template>
<BButton
variant="primary"
size="sm"
:class="{ 'mr-2': isStandalone }"
data-description="page save button"
:disabled="!store.canSave"
@click="handleSave">
<FontAwesomeIcon :icon="store.isSaving ? faSpinner : faSave" :spin="store.isSaving" />
Save
</BButton>
<BButton
v-if="isStandalone"
variant="primary"
size="sm"
data-description="page save-view button"
:disabled="!store.canSave"
@click="handleSaveAndView">
<FontAwesomeIcon :icon="faEye" />
Save &amp; View
</BButton>
<span v-if="store.isDirty" class="ml-2 text-warning small" data-description="page unsaved indicator">
Unsaved
</span>
</div>
</PageDisplayToolbar>
<div class="page-body d-flex flex-grow-1 overflow-hidden">
<div class="page-content flex-grow-1 overflow-auto">
+4 -5
View File
@@ -644,7 +644,7 @@ pages:
toolbar:
type: data-description
selector: 'page editor toolbar'
toolbar_title: '[data-description="page editor title"] > span'
toolbar_title: '[data-description="page editor title"]'
save_button:
type: data-description
selector: 'page save button'
@@ -694,16 +694,15 @@ pages:
edit_button:
type: data-description
selector: 'page edit button'
toolbar_title_input: '[data-description="page editor title"] input'
rename_button:
type: data-description
selector: 'page rename button'
back_button:
type: data-description
selector: 'page back button'
permissions_button:
type: data-description
selector: 'page permissions button'
save_view_button:
type: data-description
selector: 'page save-view button'
revision_compare_current_button:
type: data-description
selector: 'revision compare current button'
+20 -12
View File
@@ -1961,10 +1961,7 @@ class NavigatesGalaxy(HasDriverProxy[WaitType]):
action_chains = self.action_chains()
action_chains.move_to_element(workflow).perform()
workflow.find_element(By.CSS_SELECTOR, ".g-card-rename").click()
self.components.workflows.rename_input.wait_for_visible().clear()
self.components.workflows.rename_input.wait_for_and_send_keys(new_name)
self.components.workflows.rename_input.wait_for_and_send_keys(Keys.ENTER)
self.wait_for_selector_absent_or_hidden(".g-modal")
self.rename_modal_rename("workflow", new_name)
def workflow_delete_by_name(self, name):
self.workflow_index_search_for(name)
@@ -2330,14 +2327,25 @@ class NavigatesGalaxy(HasDriverProxy[WaitType]):
items = self.components.pages.history.revision_item.all()
assert len(items) == n, f"Expected {n} revision items, found {len(items)}"
def history_page_rename(self, new_name):
"""Rename page via ClickToEdit in toolbar."""
self.components.pages.history.toolbar_title.wait_for_and_click()
title_input = self.components.pages.history.toolbar_title_input.wait_for_visible()
self.aggressive_clear(title_input)
title_input.send_keys(new_name)
self.send_enter(title_input)
self.sleep_for(self.wait_types.UX_RENDER)
def history_page_rename(self, page_type: str, new_name: str):
"""Rename page via RenameModal in toolbar."""
self.components.pages.history.rename_button.wait_for_and_click()
self.rename_modal_rename(page_type, new_name)
def rename_modal_rename(self, item_type: str, new_name: str):
"""Rename via RenameModal: opens the rename modal, types new name, and submits."""
# Clear via JS + dispatch Vue-compatible input event so nameModel is updated to empty,
# then type the new name so each keystroke fires input events and updates nameModel.
item_name_input = self.wait_for_selector(f"[data-description='{item_type} name input']")
self.execute_script(
"arguments[0].value = ''; arguments[0].dispatchEvent(new Event('input', {bubbles: true}));",
item_name_input,
)
item_name_input.send_keys(new_name)
self.wait_for_and_click_selector(".g-modal-confirm-buttons button:last-child")
# Wait for the rename promise to resolve (modal closes in the finally block)
self.wait_for_selector_absent_or_hidden(f"#{item_type}-name-input")
@retry_during_transitions
def click_history_options(self):
@@ -65,19 +65,7 @@ class TestSavedHistories(SharedStateSeleniumTestCase):
self.select_history_card_operation("Unnamed history", '[id^="g-card-rename-history-"]')
# Rename the history using the RenameModal.
# Clear via JS + dispatch Vue-compatible input event so nameModel is updated to empty,
# then type the new name so each keystroke fires input events and updates nameModel.
history_name_input = self.wait_for_selector("#history-name-input")
self.execute_script(
"arguments[0].value = ''; arguments[0].dispatchEvent(new Event('input', {bubbles: true}));",
history_name_input,
)
history_name_input.send_keys(self.history1_name)
self.wait_for_and_click_selector(".g-modal-confirm-buttons button:last-child")
# Wait for the rename API call to complete (modal closes in the finally block)
self.wait_for_selector_absent_or_hidden("#history-name-input")
self.rename_modal_rename("history", self.history1_name)
self.navigate_to_histories_page()
@@ -104,7 +104,7 @@ class TestHistoryPages(SeleniumTestCase):
self.components.pages.history.editor.wait_for_visible()
save_button = self.components.pages.history.save_button
save_button.assert_disabled()
assert save_button.has_class("g-disabled")
self.components.pages.history.unsaved_indicator.assert_absent_or_hidden()
@@ -112,7 +112,7 @@ class TestHistoryPages(SeleniumTestCase):
@retry_assertion_during_transitions
def assert_save_enabled():
assert not save_button.has_class("disabled")
assert not save_button.has_class("g-disabled")
assert_save_enabled()
@@ -120,7 +120,7 @@ class TestHistoryPages(SeleniumTestCase):
@retry_assertion_during_transitions
def assert_save_disabled_again():
save_button.assert_disabled()
assert save_button.has_class("g-disabled")
assert_save_disabled_again()
self.screenshot("history_page_save_disabled")
@@ -439,8 +439,8 @@ class TestHistoryPages(SeleniumTestCase):
@selenium_test
@managed_history
def test_inline_rename_page(self):
"""Rename page title via ClickToEdit, save, verify persistence."""
def test_rename_page(self):
"""Rename page title via the RenameModal, save, verify persistence."""
history_id = self.current_history_id()
self.dataset_populator.new_history_page(history_id, title="Original Name", content="# Content")
@@ -448,7 +448,7 @@ class TestHistoryPages(SeleniumTestCase):
self.components.pages.history.item_edit.wait_for_and_click()
self.components.pages.history.editor.wait_for_visible()
self.history_page_rename("Renamed Page")
self.history_page_rename("galaxy notebook", "Renamed Page")
self.components.pages.history.unsaved_indicator.wait_for_visible()
self.screenshot("history_page_renamed_unsaved")
@@ -557,7 +557,6 @@ class TestHistoryPages(SeleniumTestCase):
# Standalone-only controls absent
self.components.pages.history.permissions_button.assert_absent_or_hidden()
self.components.pages.history.save_view_button.assert_absent_or_hidden()
# Back button says "This History's Pages" not "Back to Pages"
back_text = self.components.pages.history.back_button.wait_for_text()
-1
View File
@@ -213,7 +213,6 @@ class TestPages(SeleniumTestCase):
self.components.pages.history.revisions_button.wait_for_visible()
self.components.pages.history.preview_button.wait_for_visible()
self.components.pages.history.permissions_button.wait_for_visible()
self.components.pages.history.save_view_button.wait_for_visible()
# Back button says "Back to Reports" not "This History's Notebooks"
back_text = self.components.pages.history.back_button.wait_for_text()