Merge "Submit requirements - trigger vote hovercard"
diff --git a/polygerrit-ui/app/elements/change/gr-submit-requirement-hovercard/gr-submit-requirement-hovercard.ts b/polygerrit-ui/app/elements/change/gr-submit-requirement-hovercard/gr-submit-requirement-hovercard.ts
index d90d173..5feb1ae 100644
--- a/polygerrit-ui/app/elements/change/gr-submit-requirement-hovercard/gr-submit-requirement-hovercard.ts
+++ b/polygerrit-ui/app/elements/change/gr-submit-requirement-hovercard/gr-submit-requirement-hovercard.ts
@@ -16,7 +16,6 @@
  */
 import '../../shared/gr-button/gr-button';
 import '../../shared/gr-label-info/gr-label-info';
-import '../../shared/gr-limited-text/gr-limited-text';
 import {customElement, property} from 'lit/decorators';
 import {
   AccountInfo,
@@ -37,7 +36,7 @@
 const base = HovercardMixin(LitElement);
 
 @customElement('gr-submit-requirement-hovercard')
-export class GrHovercardRun extends base {
+export class GrSubmitRequirementHovercard extends base {
   @property({type: Object})
   requirement?: SubmitRequirementResultInfo;
 
@@ -261,6 +260,6 @@
 
 declare global {
   interface HTMLElementTagNameMap {
-    'gr-submit-requirement-hovercard': GrHovercardRun;
+    'gr-submit-requirement-hovercard': GrSubmitRequirementHovercard;
   }
 }
diff --git a/polygerrit-ui/app/elements/change/gr-submit-requirements/gr-submit-requirements.ts b/polygerrit-ui/app/elements/change/gr-submit-requirements/gr-submit-requirements.ts
index 004d594..40d80bd 100644
--- a/polygerrit-ui/app/elements/change/gr-submit-requirements/gr-submit-requirements.ts
+++ b/polygerrit-ui/app/elements/change/gr-submit-requirements/gr-submit-requirements.ts
@@ -14,7 +14,9 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+import '../../shared/gr-label-info/gr-label-info';
 import '../gr-submit-requirement-hovercard/gr-submit-requirement-hovercard';
+import '../gr-trigger-vote-hovercard/gr-trigger-vote-hovercard';
 import {LitElement, css, html} from 'lit';
 import {customElement, property, state} from 'lit/decorators';
 import {ParsedChangeInfo} from '../../../types/types';
@@ -271,6 +273,9 @@
             html`<gr-trigger-vote
               .label="${label}"
               .labelInfo="${labels[label]}"
+              .change="${this.change}"
+              .account="${this.account}"
+              .mutable="${this.mutable ?? false}"
             ></gr-trigger-vote>`
         )}
       </section>`;
@@ -285,6 +290,15 @@
   @property({type: Object})
   labelInfo?: LabelInfo;
 
+  @property({type: Object})
+  change?: ParsedChangeInfo;
+
+  @property({type: Object})
+  account?: AccountInfo;
+
+  @property({type: Boolean})
+  mutable?: boolean;
+
   static override get styles() {
     return css`
       :host {
@@ -309,6 +323,10 @@
         --gr-vote-chip-width: 14px;
         --gr-vote-chip-height: 14px;
         margin-right: 0px;
+        margin-left: var(--spacing-xs);
+      }
+      gr-vote-chip:first-of-type {
+        margin-left: 0px;
       }
     `;
   }
@@ -317,6 +335,17 @@
     if (!this.labelInfo) return;
     return html`
       <div class="container">
+        <gr-trigger-vote-hovercard .labelName=${this.label}>
+          <gr-label-info
+            slot="label-info"
+            .change=${this.change}
+            .account=${this.account}
+            .mutable=${this.mutable}
+            .label=${this.label}
+            .labelInfo=${this.labelInfo}
+            .showAllReviewers=${false}
+          ></gr-label-info>
+        </gr-trigger-vote-hovercard>
         <span class="label">${this.label}</span>
         ${this.renderVotes()}
       </div>
diff --git a/polygerrit-ui/app/elements/change/gr-trigger-vote-hovercard/gr-trigger-vote-hovercard.ts b/polygerrit-ui/app/elements/change/gr-trigger-vote-hovercard/gr-trigger-vote-hovercard.ts
new file mode 100644
index 0000000..552cc69
--- /dev/null
+++ b/polygerrit-ui/app/elements/change/gr-trigger-vote-hovercard/gr-trigger-vote-hovercard.ts
@@ -0,0 +1,94 @@
+/**
+ * @license
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import {customElement, property} from 'lit/decorators';
+import {css, html, LitElement} from 'lit';
+import {HovercardMixin} from '../../../mixins/hovercard-mixin/hovercard-mixin';
+import {fontStyles} from '../../../styles/gr-font-styles';
+
+// This avoids JSC_DYNAMIC_EXTENDS_WITHOUT_JSDOC closure compiler error.
+const base = HovercardMixin(LitElement);
+
+@customElement('gr-trigger-vote-hovercard')
+export class GrTriggerVoteHovercard extends base {
+  @property()
+  labelName?: string;
+
+  static override get styles() {
+    return [
+      fontStyles,
+      base.styles || [],
+      css`
+        #container {
+          min-width: 300px;
+          max-width: 300px;
+          padding: var(--spacing-xl) 0 var(--spacing-m) 0;
+        }
+        .row {
+          display: flex;
+        }
+        .title {
+          color: var(--deemphasized-text-color);
+          margin-right: var(--spacing-m);
+        }
+        div.section {
+          margin: 0 var(--spacing-xl) var(--spacing-m) var(--spacing-xl);
+          display: flex;
+          align-items: flex-start;
+        }
+        div.sectionIcon {
+          flex: 0 0 30px;
+        }
+        div.sectionIcon iron-icon {
+          position: relative;
+          width: 20px;
+          height: 20px;
+        }
+      `,
+    ];
+  }
+
+  override render() {
+    return html` <div id="container" role="tooltip" tabindex="-1">
+      <div class="section">
+        <div class="sectionContent">
+          <h3 class="name heading-3">
+            <span>${this.labelName}</span>
+          </h3>
+        </div>
+      </div>
+      <div class="section">
+        <div class="sectionIcon">
+          <iron-icon class="small" icon="gr-icons:info-outline"></iron-icon>
+        </div>
+        <div class="sectionContent">
+          <div class="row">
+            <div class="title">Status</div>
+            <div>
+              <slot name="label-info"></slot>
+            </div>
+          </div>
+        </div>
+      </div>
+    </div>`;
+  }
+}
+
+declare global {
+  interface HTMLElementTagNameMap {
+    'gr-trigger-vote-hovercard': GrTriggerVoteHovercard;
+  }
+}
diff --git a/polygerrit-ui/app/elements/shared/gr-label-info/gr-label-info.ts b/polygerrit-ui/app/elements/shared/gr-label-info/gr-label-info.ts
index f60b9b6..947ef3e 100644
--- a/polygerrit-ui/app/elements/shared/gr-label-info/gr-label-info.ts
+++ b/polygerrit-ui/app/elements/shared/gr-label-info/gr-label-info.ts
@@ -43,6 +43,7 @@
   getApprovalInfo,
   getVotingRangeOrDefault,
   hasNeutralStatus,
+  hasVoted,
 } from '../../../utils/label-util';
 import {appContext} from '../../../services/app-context';
 import {ParsedChangeInfo} from '../../../types/types';
@@ -86,9 +87,21 @@
   @property({type: Object})
   account?: AccountInfo;
 
+  /**
+   * A user is able to delete a vote iff the mutable property is true and the
+   * reviewer that left the vote exists in the list of removable_reviewers
+   * received from the backend.
+   */
   @property({type: Boolean})
   mutable = false;
 
+  /**
+   * if true - show all reviewers that can vote on label
+   * if false - show only reviewers that voted on label
+   */
+  @property({type: Boolean})
+  showAllReviewers = true;
+
   private readonly restApiService = appContext.restApiService;
 
   private readonly reporting = appContext.reportingService;
@@ -201,7 +214,9 @@
     const labelInfo = this.labelInfo;
     if (!labelInfo) return;
     const reviewers = (this.change?.reviewers['REVIEWER'] ?? []).filter(
-      reviewer => canVote(labelInfo, reviewer)
+      reviewer =>
+        (this.showAllReviewers && canVote(labelInfo, reviewer)) ||
+        (!this.showAllReviewers && hasVoted(labelInfo, reviewer))
     );
     return html`<div>
       ${reviewers.map(reviewer => this.renderReviewerVote(reviewer))}
diff --git a/polygerrit-ui/app/utils/label-util.ts b/polygerrit-ui/app/utils/label-util.ts
index 960b02f..a8a2719 100644
--- a/polygerrit-ui/app/utils/label-util.ts
+++ b/polygerrit-ui/app/utils/label-util.ts
@@ -92,8 +92,9 @@
 
 export function hasNeutralStatus(
   label: DetailedLabelInfo,
-  approvalInfo: ApprovalInfo
+  approvalInfo?: ApprovalInfo
 ) {
+  if (!approvalInfo) return true;
   return getLabelStatus(label, approvalInfo.value) === LabelStatus.NEUTRAL;
 }
 
@@ -134,6 +135,15 @@
   return label.all?.filter(x => x._account_id === account._account_id)[0];
 }
 
+export function hasVoted(label: LabelInfo, account: AccountInfo) {
+  if (isDetailedLabelInfo(label)) {
+    return !hasNeutralStatus(label, getApprovalInfo(label, account));
+  } else if (isQuickLabelInfo(label)) {
+    return label.approved === account || label.rejected === account;
+  }
+  return false;
+}
+
 export function canVote(label: DetailedLabelInfo, account: AccountInfo) {
   const approvalInfo = getApprovalInfo(label, account);
   if (!approvalInfo) return false;