|
1 | 1 | <script lang="ts"> |
2 | 2 | import {SvgIcon} from '../svg.ts'; |
3 | 3 | import ActionRunStatus from './ActionRunStatus.vue'; |
4 | | -import {defineComponent, type PropType} from 'vue'; |
| 4 | +import {defineComponent, nextTick, type PropType} from 'vue'; |
5 | 5 | import {createElementFromAttrs, toggleElem} from '../utils/dom.ts'; |
6 | 6 | import {formatDatetime} from '../utils/time.ts'; |
7 | 7 | import {renderAnsi} from '../render/ansi.ts'; |
@@ -104,6 +104,7 @@ export default defineComponent({ |
104 | 104 | // internal state |
105 | 105 | loadingAbortController: null as AbortController | null, |
106 | 106 | intervalID: null as IntervalId | null, |
| 107 | + mutationObserver: null as MutationObserver | null, // Observer for auto-expand/auto-scroll functionality |
107 | 108 | currentJobStepsStates: [] as Array<Record<string, any>>, |
108 | 109 | artifacts: [] as Array<Record<string, any>>, |
109 | 110 | menuVisible: false, |
@@ -185,11 +186,72 @@ export default defineComponent({ |
185 | 186 | document.body.addEventListener('click', this.closeDropdown); |
186 | 187 | this.hashChangeListener(); |
187 | 188 | 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 === |
188 | 245 | }, |
189 | 246 |
|
190 | 247 | beforeUnmount() { |
191 | 248 | document.body.removeEventListener('click', this.closeDropdown); |
192 | 249 | 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 | + } |
193 | 255 | }, |
194 | 256 |
|
195 | 257 | unmounted() { |
@@ -568,7 +630,7 @@ export default defineComponent({ |
568 | 630 | </div> |
569 | 631 | <div class="job-step-container" ref="steps" v-if="currentJob.steps.length"> |
570 | 632 | <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']"> |
572 | 634 | <!-- If the job is done and the job step log is loaded for the first time, show the loading icon |
573 | 635 | currentJobStepsStates[i].cursor === null means the log is loaded for the first time |
574 | 636 | --> |
|
0 commit comments