mirror of https://github.com/go-gitea/gitea.git
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
333 lines
13 KiB
333 lines
13 KiB
<script lang="ts"> |
|
import {defineComponent} from 'vue'; |
|
import {SvgIcon} from '../svg.ts'; |
|
import {GET} from '../modules/fetch.ts'; |
|
import {generateElemId} from '../utils/dom.ts'; |
|
|
|
type Commit = { |
|
id: string, |
|
hovered: boolean, |
|
selected: boolean, |
|
summary: string, |
|
committer_or_author_name: string, |
|
time: string, |
|
short_sha: string, |
|
} |
|
|
|
type CommitListResult = { |
|
commits: Array<Commit>, |
|
last_review_commit_sha: string, |
|
locale: Record<string, string>, |
|
} |
|
|
|
export default defineComponent({ |
|
components: {SvgIcon}, |
|
data: () => { |
|
const el = document.querySelector('#diff-commit-select'); |
|
return { |
|
menuVisible: false, |
|
isLoading: false, |
|
queryParams: el.getAttribute('data-queryparams'), |
|
issueLink: el.getAttribute('data-issuelink'), |
|
locale: { |
|
filter_changes_by_commit: el.getAttribute('data-filter_changes_by_commit'), |
|
} as Record<string, string>, |
|
mergeBase: el.getAttribute('data-merge-base'), |
|
commits: [] as Array<Commit>, |
|
hoverActivated: false, |
|
lastReviewCommitSha: '', |
|
uniqueIdMenu: generateElemId('diff-commit-selector-menu-'), |
|
uniqueIdShowAll: generateElemId('diff-commit-selector-show-all-'), |
|
}; |
|
}, |
|
computed: { |
|
commitsSinceLastReview() { |
|
if (this.lastReviewCommitSha) { |
|
return this.commits.length - this.commits.findIndex((x) => x.id === this.lastReviewCommitSha) - 1; |
|
} |
|
return 0; |
|
}, |
|
}, |
|
mounted() { |
|
document.body.addEventListener('click', this.onBodyClick); |
|
this.$el.addEventListener('keydown', this.onKeyDown); |
|
this.$el.addEventListener('keyup', this.onKeyUp); |
|
}, |
|
unmounted() { |
|
document.body.removeEventListener('click', this.onBodyClick); |
|
this.$el.removeEventListener('keydown', this.onKeyDown); |
|
this.$el.removeEventListener('keyup', this.onKeyUp); |
|
}, |
|
methods: { |
|
onBodyClick(event: MouseEvent) { |
|
// close this menu on click outside of this element when the dropdown is currently visible opened |
|
if (this.$el.contains(event.target)) return; |
|
if (this.menuVisible) { |
|
this.toggleMenu(); |
|
} |
|
}, |
|
onKeyDown(event: KeyboardEvent) { |
|
if (!this.menuVisible) return; |
|
const item = document.activeElement as HTMLElement; |
|
if (!this.$el.contains(item)) return; |
|
switch (event.key) { |
|
case 'ArrowDown': // select next element |
|
event.preventDefault(); |
|
this.focusElem(item.nextElementSibling as HTMLElement, item); |
|
break; |
|
case 'ArrowUp': // select previous element |
|
event.preventDefault(); |
|
this.focusElem(item.previousElementSibling as HTMLElement, item); |
|
break; |
|
case 'Escape': // close menu |
|
event.preventDefault(); |
|
item.tabIndex = -1; |
|
this.toggleMenu(); |
|
break; |
|
} |
|
if (event.key === 'ArrowDown' || event.key === 'ArrowUp') { |
|
const item = document.activeElement; // try to highlight the selected commits |
|
const commitIdx = item?.matches('.item') ? item.getAttribute('data-commit-idx') : null; |
|
if (commitIdx) this.highlight(this.commits[Number(commitIdx)]); |
|
} |
|
}, |
|
onKeyUp(event: KeyboardEvent) { |
|
if (!this.menuVisible) return; |
|
const item = document.activeElement; |
|
if (!this.$el.contains(item)) return; |
|
if (event.key === 'Shift' && this.hoverActivated) { |
|
// shift is not pressed anymore -> deactivate hovering and reset hovered and selected |
|
this.hoverActivated = false; |
|
for (const commit of this.commits) { |
|
commit.hovered = false; |
|
commit.selected = false; |
|
} |
|
} |
|
}, |
|
highlight(commit: Commit) { |
|
if (!this.hoverActivated) return; |
|
const indexSelected = this.commits.findIndex((x) => x.selected); |
|
const indexCurrentElem = this.commits.findIndex((x) => x.id === commit.id); |
|
for (const [idx, commit] of this.commits.entries()) { |
|
commit.hovered = Math.min(indexSelected, indexCurrentElem) <= idx && idx <= Math.max(indexSelected, indexCurrentElem); |
|
} |
|
}, |
|
/** Focus given element */ |
|
focusElem(elem: HTMLElement, prevElem: HTMLElement) { |
|
if (elem) { |
|
elem.tabIndex = 0; |
|
if (prevElem) prevElem.tabIndex = -1; |
|
elem.focus(); |
|
} |
|
}, |
|
/** Opens our menu, loads commits before opening */ |
|
async toggleMenu() { |
|
this.menuVisible = !this.menuVisible; |
|
// load our commits when the menu is not yet visible (it'll be toggled after loading) |
|
// and we got no commits |
|
if (!this.commits.length && this.menuVisible && !this.isLoading) { |
|
this.isLoading = true; |
|
try { |
|
await this.fetchCommits(); |
|
} finally { |
|
this.isLoading = false; |
|
} |
|
} |
|
// set correct tabindex to allow easier navigation |
|
this.$nextTick(() => { |
|
if (this.menuVisible) { |
|
this.focusElem(this.$refs.showAllChanges as HTMLElement, this.$refs.expandBtn as HTMLElement); |
|
} else { |
|
this.focusElem(this.$refs.expandBtn as HTMLElement, this.$refs.showAllChanges as HTMLElement); |
|
} |
|
}); |
|
}, |
|
|
|
/** Load the commits to show in this dropdown */ |
|
async fetchCommits() { |
|
const resp = await GET(`${this.issueLink}/commits/list`); |
|
const results = await resp.json() as CommitListResult; |
|
this.commits.push(...results.commits.map((x) => { |
|
x.hovered = false; |
|
return x; |
|
})); |
|
this.commits.reverse(); |
|
this.lastReviewCommitSha = results.last_review_commit_sha || null; |
|
if (this.lastReviewCommitSha && !this.commits.some((x) => x.id === this.lastReviewCommitSha)) { |
|
// the lastReviewCommit is not available (probably due to a force push) |
|
// reset the last review commit sha |
|
this.lastReviewCommitSha = null; |
|
} |
|
Object.assign(this.locale, results.locale); |
|
}, |
|
showAllChanges() { |
|
window.location.assign(`${this.issueLink}/files${this.queryParams}`); |
|
}, |
|
/** Called when user clicks on since last review */ |
|
changesSinceLastReviewClick() { |
|
window.location.assign(`${this.issueLink}/files/${this.lastReviewCommitSha}..${this.commits.at(-1).id}${this.queryParams}`); |
|
}, |
|
/** Clicking on a single commit opens this specific commit */ |
|
commitClicked(commitId: string, newWindow = false) { |
|
const url = `${this.issueLink}/commits/${commitId}${this.queryParams}`; |
|
if (newWindow) { |
|
window.open(url); |
|
} else { |
|
window.location.assign(url); |
|
} |
|
}, |
|
/** |
|
* When a commit is clicked while holding Shift, it enables range selection. |
|
* - The range selection is a half-open, half-closed range, meaning it excludes the start commit but includes the end commit. |
|
* - The start of the commit range is always the previous commit of the first clicked commit. |
|
* - If the first commit in the list is clicked, the mergeBase will be used as the start of the range instead. |
|
* - The second Shift-click defines the end of the range. |
|
* - Once both are selected, the diff view for the selected commit range will open. |
|
*/ |
|
commitClickedShift(commit: Commit) { |
|
this.hoverActivated = !this.hoverActivated; |
|
commit.selected = true; |
|
// Second click -> determine our range and open links accordingly |
|
if (!this.hoverActivated) { |
|
// since at least one commit is selected, we can determine the range |
|
// find all selected commits and generate a link |
|
const firstSelected = this.commits.findIndex((x) => x.selected); |
|
const lastSelected = this.commits.findLastIndex((x) => x.selected); |
|
let beforeCommitID: string; |
|
if (firstSelected === 0) { |
|
beforeCommitID = this.mergeBase; |
|
} else { |
|
beforeCommitID = this.commits[firstSelected - 1].id; |
|
} |
|
const afterCommitID = this.commits[lastSelected].id; |
|
|
|
if (firstSelected === lastSelected) { |
|
// if the start and end are the same, we show this single commit |
|
window.location.assign(`${this.issueLink}/commits/${afterCommitID}${this.queryParams}`); |
|
} else if (beforeCommitID === this.mergeBase && afterCommitID === this.commits.at(-1).id) { |
|
// if the first commit is selected and the last commit is selected, we show all commits |
|
window.location.assign(`${this.issueLink}/files${this.queryParams}`); |
|
} else { |
|
window.location.assign(`${this.issueLink}/files/${beforeCommitID}..${afterCommitID}${this.queryParams}`); |
|
} |
|
} |
|
}, |
|
}, |
|
}); |
|
</script> |
|
<template> |
|
<div class="ui scrolling dropdown custom diff-commit-selector"> |
|
<button |
|
ref="expandBtn" |
|
class="ui tiny basic button" |
|
@click.stop="toggleMenu()" |
|
:data-tooltip-content="locale.filter_changes_by_commit" |
|
aria-haspopup="true" |
|
:aria-label="locale.filter_changes_by_commit" |
|
:aria-controls="uniqueIdMenu" |
|
:aria-activedescendant="uniqueIdShowAll" |
|
> |
|
<svg-icon name="octicon-git-commit"/> |
|
</button> |
|
<!-- this dropdown is not managed by Fomantic UI, so it needs some classes like "transition" explicitly --> |
|
<div class="left menu transition" :id="uniqueIdMenu" :class="{visible: menuVisible}" v-show="menuVisible" v-cloak :aria-expanded="menuVisible ? 'true': 'false'"> |
|
<div class="loading-indicator is-loading" v-if="isLoading"/> |
|
<div v-if="!isLoading" class="item" :id="uniqueIdShowAll" ref="showAllChanges" role="menuitem" @keydown.enter="showAllChanges()" @click="showAllChanges()"> |
|
<div class="gt-ellipsis"> |
|
{{ locale.show_all_commits }} |
|
</div> |
|
<div class="gt-ellipsis text light-2 tw-mb-0"> |
|
{{ locale.stats_num_commits }} |
|
</div> |
|
</div> |
|
<!-- only show the show changes since last review if there is a review AND we are commits ahead of the last review --> |
|
<div |
|
v-if="lastReviewCommitSha != null" |
|
class="item" role="menuitem" |
|
:class="{disabled: !commitsSinceLastReview}" |
|
@keydown.enter="changesSinceLastReviewClick()" |
|
@click="changesSinceLastReviewClick()" |
|
> |
|
<div class="gt-ellipsis"> |
|
{{ locale.show_changes_since_your_last_review }} |
|
</div> |
|
<div class="gt-ellipsis text light-2"> |
|
{{ commitsSinceLastReview }} commits |
|
</div> |
|
</div> |
|
<span v-if="!isLoading" class="info text light-2">{{ locale.select_commit_hold_shift_for_range }}</span> |
|
<template v-for="(commit, idx) in commits" :key="commit.id"> |
|
<div |
|
class="item" role="menuitem" |
|
:class="{selected: commit.selected, hovered: commit.hovered}" |
|
:data-commit-idx="idx" |
|
@keydown.enter.exact="commitClicked(commit.id)" |
|
@keydown.enter.shift.exact="commitClickedShift(commit)" |
|
@mouseover.shift="highlight(commit)" |
|
@click.exact="commitClicked(commit.id)" |
|
@click.ctrl.exact="commitClicked(commit.id, true)" |
|
@click.meta.exact="commitClicked(commit.id, true)" |
|
@click.shift.exact.stop.prevent="commitClickedShift(commit)" |
|
> |
|
<div class="tw-flex-1 tw-flex tw-flex-col tw-gap-1"> |
|
<div class="gt-ellipsis commit-list-summary"> |
|
{{ commit.summary }} |
|
</div> |
|
<div class="gt-ellipsis text light-2"> |
|
{{ commit.committer_or_author_name }} |
|
<span class="text right"> |
|
<!-- TODO: make this respect the PreferredTimestampTense setting --> |
|
<relative-time prefix="" :datetime="commit.time" data-tooltip-content data-tooltip-interactive="true">{{ commit.time }}</relative-time> |
|
</span> |
|
</div> |
|
</div> |
|
<div class="tw-font-mono"> |
|
{{ commit.short_sha }} |
|
</div> |
|
</div> |
|
</template> |
|
</div> |
|
</div> |
|
</template> |
|
<style scoped> |
|
.ui.dropdown.diff-commit-selector .menu { |
|
margin-top: 0.25em; |
|
overflow-x: hidden; |
|
max-height: 450px; |
|
} |
|
|
|
.ui.dropdown.diff-commit-selector .menu .loading-indicator { |
|
height: 200px; |
|
width: 350px; |
|
} |
|
|
|
.ui.dropdown.diff-commit-selector .menu > .item, |
|
.ui.dropdown.diff-commit-selector .menu > .info { |
|
display: flex; |
|
flex-direction: row; |
|
line-height: 1.4; |
|
gap: 0.25em; |
|
padding: 7px 14px !important; |
|
} |
|
|
|
.ui.dropdown.diff-commit-selector .menu > .item:not(:first-child), |
|
.ui.dropdown.diff-commit-selector .menu > .info:not(:first-child) { |
|
border-top: 1px solid var(--color-secondary) !important; |
|
} |
|
|
|
.ui.dropdown.diff-commit-selector .menu > .item:focus { |
|
background: var(--color-active); |
|
} |
|
|
|
.ui.dropdown.diff-commit-selector .menu > .item.hovered { |
|
background-color: var(--color-small-accent); |
|
} |
|
|
|
.ui.dropdown.diff-commit-selector .menu > .item.selected { |
|
background-color: var(--color-accent); |
|
} |
|
|
|
.ui.dropdown.diff-commit-selector .menu .commit-list-summary { |
|
max-width: min(380px, 96vw); |
|
} |
|
</style>
|
|
|