code.gitea.io/gitea@v1.22.3/web_src/js/components/DiffCommitSelector.vue (about)

     1  <script>
     2  import {SvgIcon} from '../svg.js';
     3  import {GET} from '../modules/fetch.js';
     4  
     5  export default {
     6    components: {SvgIcon},
     7    data: () => {
     8      const el = document.getElementById('diff-commit-select');
     9      return {
    10        menuVisible: false,
    11        isLoading: false,
    12        locale: {
    13          filter_changes_by_commit: el.getAttribute('data-filter_changes_by_commit'),
    14        },
    15        commits: [],
    16        hoverActivated: false,
    17        lastReviewCommitSha: null,
    18      };
    19    },
    20    computed: {
    21      commitsSinceLastReview() {
    22        if (this.lastReviewCommitSha) {
    23          return this.commits.length - this.commits.findIndex((x) => x.id === this.lastReviewCommitSha) - 1;
    24        }
    25        return 0;
    26      },
    27      queryParams() {
    28        return this.$el.parentNode.getAttribute('data-queryparams');
    29      },
    30      issueLink() {
    31        return this.$el.parentNode.getAttribute('data-issuelink');
    32      },
    33    },
    34    mounted() {
    35      document.body.addEventListener('click', this.onBodyClick);
    36      this.$el.addEventListener('keydown', this.onKeyDown);
    37      this.$el.addEventListener('keyup', this.onKeyUp);
    38    },
    39    unmounted() {
    40      document.body.removeEventListener('click', this.onBodyClick);
    41      this.$el.removeEventListener('keydown', this.onKeyDown);
    42      this.$el.removeEventListener('keyup', this.onKeyUp);
    43    },
    44    methods: {
    45      onBodyClick(event) {
    46        // close this menu on click outside of this element when the dropdown is currently visible opened
    47        if (this.$el.contains(event.target)) return;
    48        if (this.menuVisible) {
    49          this.toggleMenu();
    50        }
    51      },
    52      onKeyDown(event) {
    53        if (!this.menuVisible) return;
    54        const item = document.activeElement;
    55        if (!this.$el.contains(item)) return;
    56        switch (event.key) {
    57          case 'ArrowDown': // select next element
    58            event.preventDefault();
    59            this.focusElem(item.nextElementSibling, item);
    60            break;
    61          case 'ArrowUp': // select previous element
    62            event.preventDefault();
    63            this.focusElem(item.previousElementSibling, item);
    64            break;
    65          case 'Escape': // close menu
    66            event.preventDefault();
    67            item.tabIndex = -1;
    68            this.toggleMenu();
    69            break;
    70        }
    71      },
    72      onKeyUp(event) {
    73        if (!this.menuVisible) return;
    74        const item = document.activeElement;
    75        if (!this.$el.contains(item)) return;
    76        if (event.key === 'Shift' && this.hoverActivated) {
    77          // shift is not pressed anymore -> deactivate hovering and reset hovered and selected
    78          this.hoverActivated = false;
    79          for (const commit of this.commits) {
    80            commit.hovered = false;
    81            commit.selected = false;
    82          }
    83        }
    84      },
    85      highlight(commit) {
    86        if (!this.hoverActivated) return;
    87        const indexSelected = this.commits.findIndex((x) => x.selected);
    88        const indexCurrentElem = this.commits.findIndex((x) => x.id === commit.id);
    89        for (const [idx, commit] of this.commits.entries()) {
    90          commit.hovered = Math.min(indexSelected, indexCurrentElem) <= idx && idx <= Math.max(indexSelected, indexCurrentElem);
    91        }
    92      },
    93      /** Focus given element */
    94      focusElem(elem, prevElem) {
    95        if (elem) {
    96          elem.tabIndex = 0;
    97          if (prevElem) prevElem.tabIndex = -1;
    98          elem.focus();
    99        }
   100      },
   101      /** Opens our menu, loads commits before opening */
   102      async toggleMenu() {
   103        this.menuVisible = !this.menuVisible;
   104        // load our commits when the menu is not yet visible (it'll be toggled after loading)
   105        // and we got no commits
   106        if (!this.commits.length && this.menuVisible && !this.isLoading) {
   107          this.isLoading = true;
   108          try {
   109            await this.fetchCommits();
   110          } finally {
   111            this.isLoading = false;
   112          }
   113        }
   114        // set correct tabindex to allow easier navigation
   115        this.$nextTick(() => {
   116          const expandBtn = this.$el.querySelector('#diff-commit-list-expand');
   117          const showAllChanges = this.$el.querySelector('#diff-commit-list-show-all');
   118          if (this.menuVisible) {
   119            this.focusElem(showAllChanges, expandBtn);
   120          } else {
   121            this.focusElem(expandBtn, showAllChanges);
   122          }
   123        });
   124      },
   125      /** Load the commits to show in this dropdown */
   126      async fetchCommits() {
   127        const resp = await GET(`${this.issueLink}/commits/list`);
   128        const results = await resp.json();
   129        this.commits.push(...results.commits.map((x) => {
   130          x.hovered = false;
   131          return x;
   132        }));
   133        this.commits.reverse();
   134        this.lastReviewCommitSha = results.last_review_commit_sha || null;
   135        if (this.lastReviewCommitSha && this.commits.findIndex((x) => x.id === this.lastReviewCommitSha) === -1) {
   136          // the lastReviewCommit is not available (probably due to a force push)
   137          // reset the last review commit sha
   138          this.lastReviewCommitSha = null;
   139        }
   140        Object.assign(this.locale, results.locale);
   141      },
   142      showAllChanges() {
   143        window.location = `${this.issueLink}/files${this.queryParams}`;
   144      },
   145      /** Called when user clicks on since last review */
   146      changesSinceLastReviewClick() {
   147        window.location = `${this.issueLink}/files/${this.lastReviewCommitSha}..${this.commits.at(-1).id}${this.queryParams}`;
   148      },
   149      /** Clicking on a single commit opens this specific commit */
   150      commitClicked(commitId, newWindow = false) {
   151        const url = `${this.issueLink}/commits/${commitId}${this.queryParams}`;
   152        if (newWindow) {
   153          window.open(url);
   154        } else {
   155          window.location = url;
   156        }
   157      },
   158      /**
   159       * When a commit is clicked with shift this enables the range
   160       * selection. Second click (with shift) defines the end of the
   161       * range. This opens the diff of this range
   162       * Exception: first commit is the first commit of this PR. Then
   163       * the diff from beginning of PR up to the second clicked commit is
   164       * opened
   165       */
   166      commitClickedShift(commit) {
   167        this.hoverActivated = !this.hoverActivated;
   168        commit.selected = true;
   169        // Second click -> determine our range and open links accordingly
   170        if (!this.hoverActivated) {
   171          // find all selected commits and generate a link
   172          if (this.commits[0].selected) {
   173            // first commit is selected - generate a short url with only target sha
   174            const lastCommitIdx = this.commits.findLastIndex((x) => x.selected);
   175            if (lastCommitIdx === this.commits.length - 1) {
   176              // user selected all commits - just show the normal diff page
   177              window.location = `${this.issueLink}/files${this.queryParams}`;
   178            } else {
   179              window.location = `${this.issueLink}/files/${this.commits[lastCommitIdx].id}${this.queryParams}`;
   180            }
   181          } else {
   182            const start = this.commits[this.commits.findIndex((x) => x.selected) - 1].id;
   183            const end = this.commits.findLast((x) => x.selected).id;
   184            window.location = `${this.issueLink}/files/${start}..${end}${this.queryParams}`;
   185          }
   186        }
   187      },
   188    },
   189  };
   190  </script>
   191  <template>
   192    <div class="ui scrolling dropdown custom">
   193      <button
   194        class="ui basic button"
   195        id="diff-commit-list-expand"
   196        @click.stop="toggleMenu()"
   197        :data-tooltip-content="locale.filter_changes_by_commit"
   198        aria-haspopup="true"
   199        aria-controls="diff-commit-selector-menu"
   200        :aria-label="locale.filter_changes_by_commit"
   201        aria-activedescendant="diff-commit-list-show-all"
   202      >
   203        <svg-icon name="octicon-git-commit"/>
   204      </button>
   205      <div class="left menu transition" id="diff-commit-selector-menu" :class="{visible: menuVisible}" v-show="menuVisible" v-cloak :aria-expanded="menuVisible ? 'true': 'false'">
   206        <div class="loading-indicator is-loading" v-if="isLoading"/>
   207        <div v-if="!isLoading" class="vertical item" id="diff-commit-list-show-all" role="menuitem" @keydown.enter="showAllChanges()" @click="showAllChanges()">
   208          <div class="gt-ellipsis">
   209            {{ locale.show_all_commits }}
   210          </div>
   211          <div class="gt-ellipsis text light-2 tw-mb-0">
   212            {{ locale.stats_num_commits }}
   213          </div>
   214        </div>
   215        <!-- only show the show changes since last review if there is a review AND we are commits ahead of the last review -->
   216        <div
   217          v-if="lastReviewCommitSha != null" role="menuitem"
   218          class="vertical item"
   219          :class="{disabled: !commitsSinceLastReview}"
   220          @keydown.enter="changesSinceLastReviewClick()"
   221          @click="changesSinceLastReviewClick()"
   222        >
   223          <div class="gt-ellipsis">
   224            {{ locale.show_changes_since_your_last_review }}
   225          </div>
   226          <div class="gt-ellipsis text light-2">
   227            {{ commitsSinceLastReview }} commits
   228          </div>
   229        </div>
   230        <span v-if="!isLoading" class="info text light-2">{{ locale.select_commit_hold_shift_for_range }}</span>
   231        <template v-for="commit in commits" :key="commit.id">
   232          <div
   233            class="vertical item" role="menuitem"
   234            :class="{selection: commit.selected, hovered: commit.hovered}"
   235            @keydown.enter.exact="commitClicked(commit.id)"
   236            @keydown.enter.shift.exact="commitClickedShift(commit)"
   237            @mouseover.shift="highlight(commit)"
   238            @click.exact="commitClicked(commit.id)"
   239            @click.ctrl.exact="commitClicked(commit.id, true)"
   240            @click.meta.exact="commitClicked(commit.id, true)"
   241            @click.shift.exact.stop.prevent="commitClickedShift(commit)"
   242          >
   243            <div class="tw-flex-1 tw-flex tw-flex-col tw-gap-1">
   244              <div class="gt-ellipsis commit-list-summary">
   245                {{ commit.summary }}
   246              </div>
   247              <div class="gt-ellipsis text light-2">
   248                {{ commit.committer_or_author_name }}
   249                <span class="text right">
   250                  <!-- TODO: make this respect the PreferredTimestampTense setting -->
   251                  <relative-time prefix="" :datetime="commit.time" data-tooltip-content data-tooltip-interactive="true">{{ commit.time }}</relative-time>
   252                </span>
   253              </div>
   254            </div>
   255            <div class="tw-font-mono">
   256              {{ commit.short_sha }}
   257            </div>
   258          </div>
   259        </template>
   260      </div>
   261    </div>
   262  </template>
   263  <style scoped>
   264    .hovered:not(.selection) {
   265      background-color: var(--color-small-accent) !important;
   266    }
   267    .selection {
   268      background-color: var(--color-accent) !important;
   269    }
   270  
   271    .info {
   272      display: inline-block;
   273      padding: 7px 14px !important;
   274      line-height: 1.4;
   275      width: 100%;
   276    }
   277  
   278    #diff-commit-selector-menu {
   279      margin-top: 0.25em;
   280      overflow-x: hidden;
   281      max-height: 450px;
   282    }
   283  
   284    #diff-commit-selector-menu .loading-indicator {
   285      height: 200px;
   286      width: 350px;
   287    }
   288  
   289    #diff-commit-selector-menu .item,
   290    #diff-commit-selector-menu .info {
   291      display: flex !important;
   292      flex-direction: row;
   293      line-height: 1.4;
   294      padding: 7px 14px !important;
   295      gap: 0.25em;
   296    }
   297    #diff-commit-selector-menu .item:not(:first-child),
   298    #diff-commit-selector-menu .info:not(:first-child) {
   299      border-top: 1px solid var(--color-secondary) !important;
   300    }
   301    #diff-commit-selector-menu .item:focus {
   302      color: var(--color-text);
   303      background: var(--color-hover);
   304    }
   305  
   306    #diff-commit-selector-menu .commit-list-summary {
   307      max-width: min(380px, 96vw);
   308    }
   309  </style>