mirror of
https://github.com/galaxyproject/galaxy.git
synced 2026-09-24 16:30:27 +08:00
Merge pull request #22639 from ahmedhamidawan/fix_invocation_view_header_wrapping
Minor styling fixes for invocation view header and history list cards
This commit is contained in:
@@ -83,10 +83,12 @@ function revertToOriginal() {
|
||||
<component
|
||||
:is="props.component || 'label'"
|
||||
v-else
|
||||
v-g-tooltip.onoverflow
|
||||
role="button"
|
||||
for="click-to-edit-input"
|
||||
class="click-to-edit-label text-break"
|
||||
tabindex="0"
|
||||
:title="computedValue || title"
|
||||
@keyup.enter="editable = true"
|
||||
@click.stop="editable = true">
|
||||
<span v-if="computedValue">{{ computedValue }}</span>
|
||||
|
||||
@@ -268,6 +268,17 @@ const emit = defineEmits<{
|
||||
|
||||
const bookmarkLoading = ref(false);
|
||||
|
||||
// Track open state of the extra-actions dropdown so the card can raise its
|
||||
// stacking context while open. Without this, the open menu can visually drop
|
||||
// over the next card row but get pixel-intercepted by that card's primary
|
||||
// action buttons (see issue surfaced by Selenium test_delete_and_undelete_history).
|
||||
const extraActionsOpen = ref(false);
|
||||
|
||||
function onExtraDropdown(open: boolean) {
|
||||
extraActionsOpen.value = open;
|
||||
emit("dropdown", open);
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggles bookmark status with loading state
|
||||
*/
|
||||
@@ -316,6 +327,7 @@ function onKeyDown(event: KeyboardEvent) {
|
||||
{ 'g-card-published': published },
|
||||
{ 'g-card-clickable': props.clickable },
|
||||
{ 'g-card-dim': props.dimWhenUnselected && !props.selected },
|
||||
{ 'g-card-dropdown-open': extraActionsOpen },
|
||||
containerClass,
|
||||
]"
|
||||
:tabindex="props.clickable ? 0 : undefined"
|
||||
@@ -329,7 +341,7 @@ function onKeyDown(event: KeyboardEvent) {
|
||||
<div class="d-flex flex-column flex-gapy-1">
|
||||
<div
|
||||
:id="`g-card-${props.id}-header`"
|
||||
class="d-flex flex-wrap flex-gapy-1 flex-gapx-1 justify-content-between">
|
||||
class="d-flex flex-gapy-1 flex-gapx-1 justify-content-between">
|
||||
<div class="d-flex flex-column flex-grow-1 g-card-title-section">
|
||||
<div class="d-flex">
|
||||
<div v-if="selectable">
|
||||
@@ -349,7 +361,6 @@ function onKeyDown(event: KeyboardEvent) {
|
||||
:id="getElementId(props.id, 'title')"
|
||||
bold
|
||||
inline
|
||||
class="d-block"
|
||||
:size="props.titleSize">
|
||||
<FontAwesomeIcon
|
||||
v-if="props.titleIcon?.icon"
|
||||
@@ -371,7 +382,7 @@ function onKeyDown(event: KeyboardEvent) {
|
||||
<template v-else>
|
||||
<span
|
||||
:id="getElementId(props.id, 'title-text')"
|
||||
v-g-tooltip.hover
|
||||
v-g-tooltip.onoverflow
|
||||
:title="localize(title)"
|
||||
:class="{ 'g-card-title-truncate': props.titleNLines }">
|
||||
{{ title }}
|
||||
@@ -465,8 +476,8 @@ function onKeyDown(event: KeyboardEvent) {
|
||||
title="More options"
|
||||
toggle-class="inline-icon-button"
|
||||
variant="link"
|
||||
@show="() => emit('dropdown', true)"
|
||||
@hide="() => emit('dropdown', false)">
|
||||
@show="() => onExtraDropdown(true)"
|
||||
@hide="() => onExtraDropdown(false)">
|
||||
<template v-slot:button-content>
|
||||
<FontAwesomeIcon :icon="faCaretDown" fixed-width />
|
||||
</template>
|
||||
@@ -694,6 +705,20 @@ function onKeyDown(event: KeyboardEvent) {
|
||||
container: g-card / inline-size;
|
||||
width: 100%;
|
||||
|
||||
// While the extra-actions dropdown is open, raise this card above its
|
||||
// siblings so the menu does not get pixel-intercepted by a neighboring
|
||||
// card's primary action buttons drawn at the same screen coordinates,
|
||||
// and relax the content's overflow:hidden so popper-positioned menu
|
||||
// items (especially when flipped above the toggle) are not clipped.
|
||||
&.g-card-dropdown-open {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
|
||||
.g-card-content {
|
||||
overflow: visible;
|
||||
}
|
||||
}
|
||||
|
||||
&.g-card-grid-view {
|
||||
width: calc(100% / 3);
|
||||
|
||||
@@ -757,14 +782,7 @@ function onKeyDown(event: KeyboardEvent) {
|
||||
}
|
||||
|
||||
.g-card-rename {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
&:hover,
|
||||
&:focus-within {
|
||||
.g-card-rename {
|
||||
visibility: visible;
|
||||
}
|
||||
align-self: flex-start;
|
||||
}
|
||||
|
||||
.g-card-content {
|
||||
|
||||
@@ -29,7 +29,6 @@
|
||||
|
||||
import { storeToRefs } from "pinia";
|
||||
import { computed } from "vue";
|
||||
import { useRouter } from "vue-router/composables";
|
||||
|
||||
import { userOwnsHistory } from "@/api";
|
||||
import type { AnyHistoryEntry } from "@/api/histories";
|
||||
@@ -120,8 +119,6 @@ const props = withDefaults(defineProps<Props>(), {
|
||||
highlighted: false,
|
||||
});
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const historyStore = useHistoryStore();
|
||||
|
||||
const userStore = useUserStore();
|
||||
@@ -143,6 +140,12 @@ const emit = defineEmits<{
|
||||
*/
|
||||
(e: "titleClick", history: AnyHistoryEntry["id"]): void;
|
||||
|
||||
/**
|
||||
* Emitted when the rename action is triggered
|
||||
* @event rename
|
||||
*/
|
||||
(e: "rename", id: string, name: string): void;
|
||||
|
||||
/**
|
||||
* Emitted when a tag is clicked for filtering
|
||||
* @event tagClick
|
||||
@@ -178,8 +181,8 @@ const emit = defineEmits<{
|
||||
* Handles clicking on the history title to navigate to the history view
|
||||
* @function onTitleClick
|
||||
*/
|
||||
function onTitleClick() {
|
||||
router.push(`/histories/view?id=${props.history.id}`);
|
||||
async function onTitleClick() {
|
||||
await historyStore.setCurrentHistory(props.history.id);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -189,7 +192,7 @@ function onTitleClick() {
|
||||
const historyCardTitle = computed(() => {
|
||||
return {
|
||||
label: props.history.name,
|
||||
title: localize("Click to view this history"),
|
||||
title: localize("Click to set as current"),
|
||||
handler: onTitleClick,
|
||||
};
|
||||
});
|
||||
@@ -270,7 +273,7 @@ function onKeyDown(event: KeyboardEvent) {
|
||||
:clickable="props.clickable"
|
||||
:highlighted="props.highlighted"
|
||||
@titleClick="onTitleClick"
|
||||
@rename="() => router.push(`/histories/rename?id=${history.id}`)"
|
||||
@rename="emit('rename', history.id, history.name)"
|
||||
@select="isMyHistory(history) && emit('select', history)"
|
||||
@tagsUpdate="(tags) => onTagsUpdate(history.id, tags)"
|
||||
@tagClick="(tag) => emit('tagClick', tag)"
|
||||
|
||||
@@ -23,11 +23,13 @@
|
||||
* @tagClick="onTagClick" />
|
||||
*/
|
||||
|
||||
import type { Ref } from "vue";
|
||||
import { reactive, type Ref, ref } from "vue";
|
||||
|
||||
import type { AnyHistoryEntry, MyHistory } from "@/api/histories";
|
||||
import { isMyHistory } from "@/api/histories";
|
||||
import { useHistoryStore } from "@/stores/historyStore";
|
||||
|
||||
import RenameModal from "@/components/Common/RenameModal.vue";
|
||||
import HistoryCard from "@/components/History/HistoryCard.vue";
|
||||
|
||||
interface Props {
|
||||
@@ -150,6 +152,28 @@ const emit = defineEmits<{
|
||||
*/
|
||||
(e: "on-history-card-click", history: AnyHistoryEntry, event: Event): void;
|
||||
}>();
|
||||
|
||||
const historyStore = useHistoryStore();
|
||||
|
||||
const modalOptions = reactive({
|
||||
rename: {
|
||||
id: "",
|
||||
name: "",
|
||||
},
|
||||
});
|
||||
|
||||
const showRename = ref(false);
|
||||
|
||||
function onRenameClose() {
|
||||
showRename.value = false;
|
||||
emit("refreshList", true, true);
|
||||
}
|
||||
|
||||
function onRename(id: string, name: string) {
|
||||
modalOptions.rename.id = id;
|
||||
modalOptions.rename.name = name;
|
||||
showRename.value = true;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -173,8 +197,16 @@ const emit = defineEmits<{
|
||||
@tagClick="(...args) => emit('tagClick', ...args)"
|
||||
@refreshList="(...args) => emit('refreshList', ...args)"
|
||||
@updateFilter="(...args) => emit('updateFilter', ...args)"
|
||||
@rename="onRename"
|
||||
@on-key-down="(...args) => emit('on-key-down', ...args)"
|
||||
@on-history-card-click="(...args) => emit('on-history-card-click', ...args)" />
|
||||
|
||||
<RenameModal
|
||||
v-if="showRename"
|
||||
item-type="history"
|
||||
:name="modalOptions.rename.name"
|
||||
:rename-action="(newName) => historyStore.updateHistory(modalOptions.rename.id, { name: newName })"
|
||||
@close="onRenameClose" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import { faPen, faSave, faUndo } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
|
||||
import { BButton, BFormInput, BFormTextarea } from "bootstrap-vue";
|
||||
import { storeToRefs } from "pinia";
|
||||
import { computed, nextTick, onMounted, ref, watch } from "vue";
|
||||
import { computed, ref } from "vue";
|
||||
|
||||
import { useUserStore } from "@/stores/userStore";
|
||||
import l from "@/utils/localization";
|
||||
@@ -41,8 +41,6 @@ const userStore = useUserStore();
|
||||
const { isAnonymous } = storeToRefs(userStore);
|
||||
|
||||
const nameRef = ref<HTMLInputElement | null>(null);
|
||||
const clickToEditRef = ref<InstanceType<typeof ClickToEdit> | null>(null);
|
||||
const clickToEditClamped = ref(false);
|
||||
|
||||
const editing = ref(false);
|
||||
const textSelected = ref(false);
|
||||
@@ -107,19 +105,6 @@ function onToggle() {
|
||||
}
|
||||
}
|
||||
|
||||
function checkClickToEditClamped() {
|
||||
const el = clickToEditRef.value?.$el;
|
||||
if (el) {
|
||||
clickToEditClamped.value = el.scrollHeight > el.clientHeight;
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(checkClickToEditClamped);
|
||||
watch(
|
||||
() => props.name,
|
||||
() => nextTick(checkClickToEditClamped),
|
||||
);
|
||||
|
||||
function selectText() {
|
||||
if (!textSelected.value) {
|
||||
nameRef.value?.select();
|
||||
@@ -137,11 +122,8 @@ function selectText() {
|
||||
<template v-if="!summarized && !editing">
|
||||
<ClickToEdit
|
||||
v-if="renameable"
|
||||
ref="clickToEditRef"
|
||||
v-model="clickToEditName"
|
||||
v-g-tooltip.hover="clickToEditClamped ? name : ''"
|
||||
component="h3"
|
||||
title="..."
|
||||
data-description="name display"
|
||||
no-save-on-blur
|
||||
class="name-display my-2 w-100" />
|
||||
@@ -194,7 +176,7 @@ function selectText() {
|
||||
v-if="tags"
|
||||
:class="{
|
||||
'mt-2': !summarized,
|
||||
tags: ['both', 'tags'].includes(summarized),
|
||||
tags: ['both', 'tags'].includes(summarized || ''),
|
||||
hidden: summarized === 'hidden',
|
||||
}"
|
||||
:value="tags"
|
||||
|
||||
@@ -68,9 +68,10 @@ const toolPanelHeader = computed(() => {
|
||||
}
|
||||
});
|
||||
|
||||
const headingClass = computed(() =>
|
||||
currentPanelView.value !== "default" && !isFavoritesView.value ? "font-italic" : "",
|
||||
);
|
||||
const headingClass = computed<Record<string, boolean>>(() => ({
|
||||
"font-italic": currentPanelView.value !== "default" && !isFavoritesView.value,
|
||||
"text-left": true,
|
||||
}));
|
||||
const showPanelIcon = computed(() => !!panelIcon.value && !loading.value);
|
||||
|
||||
const groupedPanelViews = computed(() => {
|
||||
@@ -136,10 +137,10 @@ async function updatePanelView(panel: Panel) {
|
||||
<template v-slot:button-content>
|
||||
<span class="sr-only">View all tool panel configurations</span>
|
||||
<div class="d-flex panel-view-selector justify-content-between flex-gapx-1">
|
||||
<div>
|
||||
<div class="d-flex flex-gapx-1">
|
||||
<FontAwesomeIcon
|
||||
v-if="showPanelIcon && !isFavoritesView"
|
||||
class="mr-1"
|
||||
class="mr-1 mt-1"
|
||||
:icon="panelIcon"
|
||||
data-description="panel view header icon" />
|
||||
<Heading id="toolbox-heading" :class="headingClass" h2 inline size="sm">
|
||||
|
||||
@@ -128,6 +128,9 @@ initializePanel();
|
||||
:deep(.activity-panel-header) {
|
||||
margin-right: 1rem;
|
||||
margin-left: 1rem;
|
||||
.activity-panel-header-top {
|
||||
align-items: flex-start;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -178,7 +178,6 @@ function createNew(event: Event) {
|
||||
:filterable="false"
|
||||
:current-workflow-id="props.currentWorkflowId"
|
||||
editor-view
|
||||
compact
|
||||
@insertWorkflow="(...args) => emit('insertWorkflow', ...args)"
|
||||
@insertWorkflowSteps="(...args) => emit('insertWorkflowSteps', ...args)"
|
||||
@refreshList="refresh" />
|
||||
|
||||
@@ -26,7 +26,7 @@ const props = withDefaults(defineProps<Props>(), {
|
||||
|
||||
<template>
|
||||
<div class="my-1 progress-container">
|
||||
<small v-if="props.note" class="progress-note">
|
||||
<small v-if="props.note" v-g-tooltip.onoverflow class="progress-note" :title="props.note">
|
||||
{{ props.note }}<span v-if="props.loading">.<span class="blinking">..</span></span>
|
||||
</small>
|
||||
<BProgress :max="props.total">
|
||||
|
||||
@@ -93,7 +93,7 @@ function getInvocationBadges(invocation: WorkflowInvocation) {
|
||||
{
|
||||
id: "state",
|
||||
label: invocation.state,
|
||||
title: invocation.state,
|
||||
title: "",
|
||||
class: stateClass(invocation.state),
|
||||
visible: true,
|
||||
},
|
||||
|
||||
@@ -18,7 +18,6 @@ interface Props {
|
||||
filterable?: boolean;
|
||||
publishedView?: boolean;
|
||||
editorView?: boolean;
|
||||
compact?: boolean;
|
||||
current?: boolean;
|
||||
selected?: boolean;
|
||||
selectable?: boolean;
|
||||
@@ -32,7 +31,6 @@ const props = withDefaults(defineProps<Props>(), {
|
||||
hideRuns: false,
|
||||
filterable: true,
|
||||
editorView: false,
|
||||
compact: false,
|
||||
current: false,
|
||||
selected: false,
|
||||
selectable: false,
|
||||
@@ -131,10 +129,10 @@ function onKeyDown(event: KeyboardEvent) {
|
||||
<GCard
|
||||
:id="workflow.id"
|
||||
class="workflow-card"
|
||||
can-rename-title
|
||||
:can-rename-title="!props.workflow.deleted"
|
||||
:title="workflowCardTitle"
|
||||
:title-badges="workflowCardTitleBadges"
|
||||
:title-n-lines="props.compact ? 2 : undefined"
|
||||
:title-n-lines="2"
|
||||
:description="description || ''"
|
||||
:grid-view="props.gridView"
|
||||
:badges="workflowCardBadges"
|
||||
|
||||
@@ -19,7 +19,6 @@ interface Props {
|
||||
filterable?: boolean;
|
||||
publishedView?: boolean;
|
||||
editorView?: boolean;
|
||||
compact?: boolean;
|
||||
currentWorkflowId?: string;
|
||||
selectedWorkflowIds?: SelectedWorkflow[];
|
||||
itemRefs?: Record<string, Ref<InstanceType<typeof WorkflowCard> | null>>;
|
||||
@@ -33,7 +32,6 @@ const props = withDefaults(defineProps<Props>(), {
|
||||
filterable: true,
|
||||
publishedView: false,
|
||||
editorView: false,
|
||||
compact: false,
|
||||
currentWorkflowId: "",
|
||||
selectedWorkflowIds: () => [],
|
||||
itemRefs: () => ({}),
|
||||
@@ -107,7 +105,6 @@ const workflowPublished = ref<InstanceType<typeof WorkflowPublished>>();
|
||||
:filterable="props.filterable"
|
||||
:published-view="props.publishedView"
|
||||
:editor-view="props.editorView"
|
||||
:compact="props.compact"
|
||||
:current="workflow.id === props.currentWorkflowId"
|
||||
:clickable="props.clickable"
|
||||
:highlighted="props.rangeSelectAnchor?.id === workflow.id"
|
||||
|
||||
@@ -136,9 +136,9 @@ async function rerunWorkflow() {
|
||||
|
||||
<div class="position-relative">
|
||||
<div v-if="workflow" class="bg-secondary px-2 py-1 rounded d-flex flex-gapx-1 justify-content-between">
|
||||
<div class="py-1 d-flex flex-wrap align-items-center flex-gapx-1" data-description="workflow heading">
|
||||
<div class="py-1 align-items-center" data-description="workflow heading">
|
||||
<slot name="before-icon" />
|
||||
<FontAwesomeIcon :icon="faSitemap" fixed-width />
|
||||
<FontAwesomeIcon class="mr-1" :icon="faSitemap" fixed-width />
|
||||
<b> {{ props.invocation ? "Invoked " : "" }}Workflow: {{ getWorkflowName() }} </b>
|
||||
<span>(Version: {{ workflow.version + 1 }})</span>
|
||||
</div>
|
||||
|
||||
@@ -5,7 +5,6 @@ import {
|
||||
faExclamation,
|
||||
faSpinner,
|
||||
faSquare,
|
||||
faTimes,
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
|
||||
import { BAlert, BBadge, BNav, BNavItem } from "bootstrap-vue";
|
||||
@@ -450,17 +449,6 @@ async function onCancel() {
|
||||
<BBadge v-if="isPolling" v-g-tooltip.hover title="Polling for updates" variant="link">
|
||||
<FontAwesomeIcon :icon="faSpinner" spin />
|
||||
</BBadge>
|
||||
<GButton
|
||||
v-if="!invocationAndJobTerminal"
|
||||
tooltip
|
||||
class="my-1"
|
||||
title="Cancel scheduling of workflow invocation"
|
||||
data-description="cancel invocation button"
|
||||
size="small"
|
||||
@click="onCancel">
|
||||
<FontAwesomeIcon :icon="faTimes" fixed-width />
|
||||
Cancel Workflow
|
||||
</GButton>
|
||||
</div>
|
||||
</BNav>
|
||||
|
||||
@@ -582,7 +570,6 @@ async function onCancel() {
|
||||
// progress bar shrinks to fit divs on either side
|
||||
flex-grow: 1;
|
||||
flex-shrink: 1;
|
||||
max-width: 50%;
|
||||
|
||||
.steps-progress,
|
||||
.jobs-progress {
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
* Trigger: .hover (default), .focus
|
||||
* Content: .html (innerHTML instead of textContent)
|
||||
* Styling: .v-danger
|
||||
* Behavior: .onoverflow (only show tooltip when text overflows with ellipsis)
|
||||
*
|
||||
* Value forms:
|
||||
* v-g-tooltip → reads element's title attribute
|
||||
@@ -37,6 +38,8 @@ interface TooltipState {
|
||||
placement: Placement;
|
||||
isHtml: boolean;
|
||||
showDelay: ReturnType<typeof useDelayedAction>;
|
||||
onOverflow: boolean;
|
||||
resizeObserver: ResizeObserver | null;
|
||||
}
|
||||
|
||||
const stateMap = new WeakMap<HTMLElement, TooltipState>();
|
||||
@@ -219,6 +222,15 @@ function showTooltip(el: HTMLElement) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If onOverflow is enabled, only show tooltip when text is overflowing
|
||||
if (state.onOverflow) {
|
||||
// Use a threshold of 1px to account for line clamped text
|
||||
const isOverflowing = el.scrollWidth > el.clientWidth + 1 || el.scrollHeight > el.clientHeight + 1;
|
||||
if (!isOverflowing) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
suppressNativeTooltip(el);
|
||||
|
||||
if (!state.tooltipEl.isConnected) {
|
||||
@@ -339,6 +351,7 @@ export const vGTooltip: ObjectDirective<HTMLElement> = {
|
||||
const modifiers = binding.modifiers || {};
|
||||
const isDanger = !!modifiers["v-danger"];
|
||||
const isHtml = !!modifiers.html;
|
||||
const onOverflow = !!modifiers.onoverflow;
|
||||
const placement = getPlacement(modifiers, binding.value);
|
||||
|
||||
const { tooltipEl, arrowEl, contentEl } = createTooltipEl(isDanger);
|
||||
@@ -348,6 +361,21 @@ export const vGTooltip: ObjectDirective<HTMLElement> = {
|
||||
tooltipEl.id = uid;
|
||||
el.setAttribute("aria-describedby", uid);
|
||||
|
||||
let resizeObserver: ResizeObserver | null = null;
|
||||
if (onOverflow) {
|
||||
// Watch for size changes to re-evaluate overflow state
|
||||
resizeObserver = new ResizeObserver(() => {
|
||||
// If tooltip is showing but no longer overflowing, hide it
|
||||
if (tooltipEl.isConnected) {
|
||||
const isOverflowing = el.scrollWidth > el.clientWidth + 1 || el.scrollHeight > el.clientHeight + 1;
|
||||
if (!isOverflowing) {
|
||||
hideTooltip(el);
|
||||
}
|
||||
}
|
||||
});
|
||||
resizeObserver.observe(el);
|
||||
}
|
||||
|
||||
const state: TooltipState = {
|
||||
tooltipEl,
|
||||
arrowEl,
|
||||
@@ -357,6 +385,8 @@ export const vGTooltip: ObjectDirective<HTMLElement> = {
|
||||
placement,
|
||||
isHtml,
|
||||
showDelay: useDelayedAction(DEFAULT_TOOLTIP_HOVER_DELAY_MS),
|
||||
onOverflow,
|
||||
resizeObserver,
|
||||
};
|
||||
|
||||
stateMap.set(el, state);
|
||||
@@ -386,6 +416,7 @@ export const vGTooltip: ObjectDirective<HTMLElement> = {
|
||||
state.cleanupListeners();
|
||||
clearPendingShow(el);
|
||||
state.cleanupAutoUpdate?.();
|
||||
state.resizeObserver?.disconnect();
|
||||
state.tooltipEl.remove();
|
||||
restoreNativeTooltip(el);
|
||||
el.removeAttribute("aria-describedby");
|
||||
|
||||
@@ -778,12 +778,19 @@ class NavigatesGalaxy(HasDriverProxy[WaitType]):
|
||||
target_card = self.get_history_card(card_name)
|
||||
|
||||
if is_in_extra:
|
||||
# Open the extra-actions dropdown and click the menu item directly.
|
||||
# Bootstrap-vue keeps menu items in the DOM for every card, so the
|
||||
# action lookup must be scoped to target_card to avoid picking a
|
||||
# match from a closed dropdown on another card.
|
||||
target_card.find_element(By.CSS_SELECTOR, '[id^="g-card-extra-actions-history-"]').click()
|
||||
self.sleep_for(self.wait_types.UX_RENDER)
|
||||
target_card.find_element(By.CSS_SELECTOR, action_selector).click()
|
||||
return
|
||||
|
||||
action_selector = target_card.find_element(By.CSS_SELECTOR, action_selector)
|
||||
action_element = target_card.find_element(By.CSS_SELECTOR, action_selector)
|
||||
# Hover over parent card first to activate hover state in headless mode
|
||||
self.action_chains().move_to_element(target_card).perform()
|
||||
self.move_to_and_click(action_selector)
|
||||
self.move_to_and_click(action_element)
|
||||
|
||||
def edit_dataset_dbkey(self, dbkey_text):
|
||||
# precondition: need to be on the dataset edit component
|
||||
|
||||
@@ -65,12 +65,19 @@ class TestSavedHistories(SharedStateSeleniumTestCase):
|
||||
|
||||
self.select_history_card_operation("Unnamed history", '[id^="g-card-rename-history-"]')
|
||||
|
||||
# Rename the history
|
||||
history_name_input = self.wait_for_selector(".ui-form-element input.ui-input")
|
||||
history_name_input.clear()
|
||||
# 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("button#submit")
|
||||
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.navigate_to_histories_page()
|
||||
|
||||
@@ -368,3 +375,7 @@ class TestSavedHistories(SharedStateSeleniumTestCase):
|
||||
def create_history(self, name):
|
||||
self.home()
|
||||
self.history_panel_create_new_with_name(name)
|
||||
# Wait for the panel label to reflect the new name, confirming the
|
||||
# rename XHR has completed before any subsequent home() navigation
|
||||
# that would otherwise cancel the still-in-flight request.
|
||||
self.wait_for_selector(f'[data-description="name display"][title="{name}"]')
|
||||
|
||||
Reference in New Issue
Block a user