diff options
author | sebastian-sauer <sauer.sebastian@gmail.com> | 2023-07-28 21:18:12 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2023-07-28 21:18:12 +0200 |
commit | 55532061c83d38d33ef48bdc5eeac0f652844e8a (patch) | |
tree | d6d21c0afb8b6df197e6eb618274dd16948f3753 /web_src/js | |
parent | 4971a1054317ae68b1eb59a3dc30f61c7503dadc (diff) | |
download | gitea-55532061c83d38d33ef48bdc5eeac0f652844e8a.tar.gz gitea-55532061c83d38d33ef48bdc5eeac0f652844e8a.zip |
Add commits dropdown in PR files view and allow commit by commit review (#25528)
This PR adds a new dropdown to select a commit or a commit range
(shift-click like github) of a Pull Request.
After selection of a commit only the changes of this commit will be shown.
When selecting a range of commits the diff of this range is shown.
This allows to review a PR commit by commit or by viewing only commit ranges.
The "Show changes since your last review" mechanism github uses is implemented, too.
When reviewing a single commit or a commit range the "Viewed" functionality is disabled.
## Screenshots
### The commit dropdown
![image](https://github.com/go-gitea/gitea/assets/51889757/0db3ae62-1272-436c-be64-4730c5d611e3)
### Selecting a commit range
![image](https://github.com/go-gitea/gitea/assets/51889757/ad81eedb-8437-42b0-8073-2d940c25fe8f)
### Show changes of a single commit only
![image](https://github.com/go-gitea/gitea/assets/51889757/6b1a113b-73ef-4ecc-adf6-bc2340bb8f97)
### Show changes of a commit range
![image](https://github.com/go-gitea/gitea/assets/51889757/6401b358-cd66-4c09-8baa-6cf6177f23a7)
Fixes https://github.com/go-gitea/gitea/issues/20989
Fixes https://github.com/go-gitea/gitea/issues/19263
---------
Co-authored-by: silverwind <me@silverwind.io>
Co-authored-by: KN4CK3R <admin@oldschoolhack.me>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
Co-authored-by: delvh <dev.lh@web.de>
Diffstat (limited to 'web_src/js')
-rw-r--r-- | web_src/js/components/DiffCommitSelector.vue | 299 | ||||
-rw-r--r-- | web_src/js/features/repo-diff-commitselect.js | 10 | ||||
-rw-r--r-- | web_src/js/features/repo-diff.js | 2 | ||||
-rw-r--r-- | web_src/js/svg.js | 2 |
4 files changed, 313 insertions, 0 deletions
diff --git a/web_src/js/components/DiffCommitSelector.vue b/web_src/js/components/DiffCommitSelector.vue new file mode 100644 index 0000000000..a0fc4b2a91 --- /dev/null +++ b/web_src/js/components/DiffCommitSelector.vue @@ -0,0 +1,299 @@ +<template> + <div class="ui scrolling dropdown custom"> + <button + class="ui basic button" + id="diff-commit-list-expand" + @click.stop="toggleMenu()" + :data-tooltip-content="locale.filter_changes_by_commit" + aria-haspopup="true" + tabindex="0" + aria-controls="diff-commit-selector-menu" + :aria-label="locale.filter_changes_by_commit" + aria-activedescendant="diff-commit-list-show-all" + > + <svg-icon name="octicon-git-commit"/> + </button> + <div class="menu left transition" id="diff-commit-selector-menu" :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="vertical item gt-df gt-fc gt-gap-2" id="diff-commit-list-show-all" role="menuitem" tabindex="-1" @keydown.enter="showAllChanges()" @click="showAllChanges()"> + <div class="gt-ellipsis"> + {{ locale.show_all_commits }} + </div> + <div class="gt-ellipsis text light-2 gt-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" role="menuitem" tabindex="-1" + class="vertical item gt-df gt-fc gt-gap-2 gt-border-secondary-top" + :class="{disabled: commitsSinceLastReview === 0}" + @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 gt-border-secondary-top text light-2">{{ locale.select_commit_hold_shift_for_range }}</span> + <template v-for="commit in commits" :key="commit.id"> + <div + class="vertical item gt-df gt-gap-2 gt-border-secondary-top" role="menuitem" tabindex="-1" + :class="{selection: commit.selected, hovered: commit.hovered}" + @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="gt-f1 gt-df gt-fc gt-gap-2"> + <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"> + <relative-time class="time-since" prefix="" :datetime="commit.time" data-tooltip-content data-tooltip-interactive="true">{{ commit.time }}</relative-time> + </span> + </div> + </div> + <div class="gt-mono"> + {{ commit.short_sha }} + </div> + </div> + </template> + </div> + </div> +</template> + +<script> +import {SvgIcon} from '../svg.js'; + +export default { + components: {SvgIcon}, + data: () => { + return { + menuVisible: false, + isLoading: false, + locale: {}, + commits: [], + hoverActivated: false, + lastReviewCommitSha: null + }; + }, + computed: { + commitsSinceLastReview() { + if (this.lastReviewCommitSha) { + return this.commits.length - this.commits.findIndex((x) => x.id === this.lastReviewCommitSha) - 1; + } + return 0; + }, + queryParams() { + return this.$el.parentNode.getAttribute('data-queryparams'); + }, + issueLink() { + return this.$el.parentNode.getAttribute('data-issuelink'); + } + }, + 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) { + // 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) { + if (!this.menuVisible) return; + const item = document.activeElement; + if (!this.$el.contains(item)) return; + switch (event.key) { + case 'ArrowDown': // select next element + event.preventDefault(); + this.focusElem(item.nextElementSibling, item); + break; + case 'ArrowUp': // select previous element + event.preventDefault(); + this.focusElem(item.previousElementSibling, item); + break; + case 'Escape': // close menu + event.preventDefault(); + item.tabIndex = -1; + this.toggleMenu(); + break; + } + }, + onKeyUp(event) { + 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) { + 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, prevElem) { + if (elem) { + elem.tabIndex = 0; + 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 === 0 && this.menuVisible && !this.isLoading) { + this.isLoading = true; + try { + await this.fetchCommits(); + } finally { + this.isLoading = false; + } + } + // set correct tabindex to allow easier navigation + this.$nextTick(() => { + const expandBtn = this.$el.querySelector('#diff-commit-list-expand'); + const showAllChanges = this.$el.querySelector('#diff-commit-list-show-all'); + if (this.menuVisible) { + this.focusElem(showAllChanges, expandBtn); + } else { + this.focusElem(expandBtn, showAllChanges); + } + }); + }, + /** Load the commits to show in this dropdown */ + async fetchCommits() { + const resp = await fetch(`${this.issueLink}/commits/list`); + const results = await resp.json(); + 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.findIndex((x) => x.id === this.lastReviewCommitSha) === -1) { + // 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 = `${this.issueLink}/files${this.queryParams}`; + }, + /** Called when user clicks on since last review */ + changesSinceLastReviewClick() { + window.location = `${this.issueLink}/files/${this.lastReviewCommitSha}..${this.commits.at(-1).id}${this.queryParams}`; + }, + /** Clicking on a single commit opens this specific commit */ + commitClicked(commitId, newWindow = false) { + const url = `${this.issueLink}/commits/${commitId}${this.queryParams}`; + if (newWindow) { + window.open(url); + } else { + window.location = url; + } + }, + /** + * When a commit is clicked with shift this enables the range + * selection. Second click (with shift) defines the end of the + * range. This opens the diff of this range + * Exception: first commit is the first commit of this PR. Then + * the diff from beginning of PR up to the second clicked commit is + * opened + */ + commitClickedShift(commit) { + this.hoverActivated = !this.hoverActivated; + commit.selected = true; + // Second click -> determine our range and open links accordingly + if (!this.hoverActivated) { + // find all selected commits and generate a link + if (this.commits[0].selected) { + // first commit is selected - generate a short url with only target sha + const lastCommitIdx = this.commits.findLastIndex((x) => x.selected); + if (lastCommitIdx === this.commits.length - 1) { + // user selected all commits - just show the normal diff page + window.location = `${this.issueLink}/files${this.queryParams}`; + } else { + window.location = `${this.issueLink}/files/${this.commits[lastCommitIdx].id}${this.queryParams}`; + } + } else { + const start = this.commits[this.commits.findIndex((x) => x.selected) - 1].id; + const end = this.commits.findLast((x) => x.selected).id; + window.location = `${this.issueLink}/files/${start}..${end}${this.queryParams}`; + } + } + }, + } +}; +</script> +<style scoped> + .hovered:not(.selection) { + background-color: var(--color-small-accent) !important; + } + .selection { + background-color: var(--color-accent) !important; + } + + .info { + display: inline-block; + padding: 7px 14px !important; + line-height: 1.4; + width: 100%; + } + + #diff-commit-selector-menu { + overflow-x: hidden; + max-height: 450px; + } + + #diff-commit-selector-menu .loading-indicator { + height: 200px; + width: 350px; + } + + #diff-commit-selector-menu .item { + flex-direction: row; + line-height: 1.4; + padding: 7px 14px !important; + } + + #diff-commit-selector-menu .item:focus { + color: var(--color-text); + background: var(--color-hover); + } + + #diff-commit-selector-menu .commit-list-summary { + max-width: min(380px, 96vw); + } +</style> diff --git a/web_src/js/features/repo-diff-commitselect.js b/web_src/js/features/repo-diff-commitselect.js new file mode 100644 index 0000000000..ebac64e855 --- /dev/null +++ b/web_src/js/features/repo-diff-commitselect.js @@ -0,0 +1,10 @@ +import {createApp} from 'vue'; +import DiffCommitSelector from '../components/DiffCommitSelector.vue'; + +export function initDiffCommitSelect() { + const el = document.getElementById('diff-commit-select'); + if (!el) return; + + const commitSelect = createApp(DiffCommitSelector); + commitSelect.mount(el); +} diff --git a/web_src/js/features/repo-diff.js b/web_src/js/features/repo-diff.js index f4bb724fe5..b79ca0f5b1 100644 --- a/web_src/js/features/repo-diff.js +++ b/web_src/js/features/repo-diff.js @@ -2,6 +2,7 @@ import $ from 'jquery'; import {initCompReactionSelector} from './comp/ReactionSelector.js'; import {initRepoIssueContentHistory} from './repo-issue-content.js'; import {initDiffFileTree} from './repo-diff-filetree.js'; +import {initDiffCommitSelect} from './repo-diff-commitselect.js'; import {validateTextareaNonEmpty} from './comp/ComboMarkdownEditor.js'; import {initViewedCheckboxListenerFor, countAndUpdateViewedFiles, initExpandAndCollapseFilesButton} from './pull-view-file.js'; import {initImageDiff} from './imagediff.js'; @@ -188,6 +189,7 @@ export function initRepoDiffView() { const diffFileList = $('#diff-file-list'); if (diffFileList.length === 0) return; initDiffFileTree(); + initDiffCommitSelect(); initRepoDiffShowMore(); initRepoDiffReviewButton(); initRepoDiffFileViewToggle(); diff --git a/web_src/js/svg.js b/web_src/js/svg.js index 2ef839aa21..46372e7d62 100644 --- a/web_src/js/svg.js +++ b/web_src/js/svg.js @@ -29,6 +29,7 @@ import octiconFileDirectoryFill from '../../public/assets/img/svg/octicon-file-d import octiconFilter from '../../public/assets/img/svg/octicon-filter.svg'; import octiconGear from '../../public/assets/img/svg/octicon-gear.svg'; import octiconGitBranch from '../../public/assets/img/svg/octicon-git-branch.svg'; +import octiconGitCommit from '../../public/assets/img/svg/octicon-git-commit.svg'; import octiconGitMerge from '../../public/assets/img/svg/octicon-git-merge.svg'; import octiconGitPullRequest from '../../public/assets/img/svg/octicon-git-pull-request.svg'; import octiconHeading from '../../public/assets/img/svg/octicon-heading.svg'; @@ -99,6 +100,7 @@ const svgs = { 'octicon-filter': octiconFilter, 'octicon-gear': octiconGear, 'octicon-git-branch': octiconGitBranch, + 'octicon-git-commit': octiconGitCommit, 'octicon-git-merge': octiconGitMerge, 'octicon-git-pull-request': octiconGitPullRequest, 'octicon-heading': octiconHeading, |