Skip to content

Commit 62ae113

Browse files
committed
fix(actions): reliably auto-expand running steps & auto-scroll appended logs Fixes #35570
1 parent 71360a9 commit 62ae113

File tree

1 file changed

+64
-2
lines changed

1 file changed

+64
-2
lines changed

web_src/js/components/RepoActionView.vue

Lines changed: 64 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<script lang="ts">
22
import {SvgIcon} from '../svg.ts';
33
import ActionRunStatus from './ActionRunStatus.vue';
4-
import {defineComponent, type PropType} from 'vue';
4+
import {defineComponent, nextTick, type PropType} from 'vue';
55
import {createElementFromAttrs, toggleElem} from '../utils/dom.ts';
66
import {formatDatetime} from '../utils/time.ts';
77
import {renderAnsi} from '../render/ansi.ts';
@@ -104,6 +104,7 @@ export default defineComponent({
104104
// internal state
105105
loadingAbortController: null as AbortController | null,
106106
intervalID: null as IntervalId | null,
107+
mutationObserver: null as MutationObserver | null, // Observer for auto-expand/auto-scroll functionality
107108
currentJobStepsStates: [] as Array<Record<string, any>>,
108109
artifacts: [] as Array<Record<string, any>>,
109110
menuVisible: false,
@@ -185,11 +186,72 @@ export default defineComponent({
185186
document.body.addEventListener('click', this.closeDropdown);
186187
this.hashChangeListener();
187188
window.addEventListener('hashchange', this.hashChangeListener);
189+
190+
// === Auto Expand + Auto Scroll Fix (for Issue #35570) ===
191+
// Ensure Vue has updated DOM for steps after initial load
192+
await nextTick();
193+
194+
// Set up observer on the steps container (safer and more efficient than document.body)
195+
const stepsContainer = (this.$refs.steps as HTMLElement);
196+
if (stepsContainer && typeof MutationObserver !== 'undefined') {
197+
this.mutationObserver = new MutationObserver((mutations) => {
198+
for (const m of mutations) {
199+
// Auto-scroll new log lines as they appear
200+
if (m.type === 'childList') {
201+
for (const n of m.addedNodes) {
202+
if (n.nodeType === 1 && (n as Element).classList.contains('job-log-line')) {
203+
if (this.optionAlwaysAutoScroll) {
204+
try { (n as Element).scrollIntoView({ behavior: 'smooth', block: 'end' }); }
205+
catch { (n as Element).scrollIntoView(); }
206+
}
207+
}
208+
}
209+
}
210+
211+
// Auto-expand running steps when their class changes
212+
if (m.type === 'attributes' && m.attributeName === 'class') {
213+
const t = m.target as Element;
214+
if (t.classList && t.classList.contains('job-step-summary')) {
215+
const stepAttr = t.getAttribute('data-step');
216+
if (!stepAttr) continue;
217+
const stepIndex = Number(stepAttr);
218+
// If expand-running option is on and step is expandable but not selected, open it via state
219+
if (this.optionAlwaysExpandRunning &&
220+
t.classList.contains('step-expandable') &&
221+
!t.classList.contains('selected')) {
222+
// Update state inside nextTick to ensure Vue has finished rendering
223+
nextTick(() => {
224+
if (!this.currentJobStepsStates[stepIndex]?.expanded) {
225+
// Update internal state so logs are immediately loaded
226+
this.currentJobStepsStates[stepIndex].expanded = true;
227+
this.loadJob();
228+
}
229+
});
230+
}
231+
}
232+
}
233+
}
234+
});
235+
236+
// Observe only the steps container subtree (minimized observation area)
237+
this.mutationObserver.observe(stepsContainer, {
238+
childList: true,
239+
subtree: true,
240+
attributes: true,
241+
attributeFilter: ['class'],
242+
});
243+
}
244+
// === End Fix ===
188245
},
189246
190247
beforeUnmount() {
191248
document.body.removeEventListener('click', this.closeDropdown);
192249
window.removeEventListener('hashchange', this.hashChangeListener);
250+
// Clean up MutationObserver to prevent memory leaks
251+
if (this.mutationObserver) {
252+
this.mutationObserver.disconnect();
253+
this.mutationObserver = null;
254+
}
193255
},
194256
195257
unmounted() {
@@ -568,7 +630,7 @@ export default defineComponent({
568630
</div>
569631
<div class="job-step-container" ref="steps" v-if="currentJob.steps.length">
570632
<div class="job-step-section" v-for="(jobStep, i) in currentJob.steps" :key="i">
571-
<div class="job-step-summary" @click.stop="isExpandable(jobStep.status) && toggleStepLogs(i)" :class="[currentJobStepsStates[i].expanded ? 'selected' : '', isExpandable(jobStep.status) && 'step-expandable']">
633+
<div class="job-step-summary" :data-step="i" @click.stop="isExpandable(jobStep.status) && toggleStepLogs(i)" :class="[currentJobStepsStates[i].expanded ? 'selected' : '', isExpandable(jobStep.status) && 'step-expandable']">
572634
<!-- If the job is done and the job step log is loaded for the first time, show the loading icon
573635
currentJobStepsStates[i].cursor === null means the log is loaded for the first time
574636
-->

0 commit comments

Comments
 (0)