Submit requirements - reviewer chip vote

The design is close to final design, but it will be polished later.

After: https://imgur.com/a/qp0p8go

Change-Id: I68ac379f28dfbb34e54e8dadbbaf0876ba47e285
diff --git a/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list.ts b/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list.ts
index ffcafdd..61785fc 100644
--- a/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list.ts
+++ b/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list.ts
@@ -16,6 +16,7 @@
  */
 import '../../shared/gr-account-chip/gr-account-chip';
 import '../../shared/gr-button/gr-button';
+import '../../shared/gr-vote-chip/gr-vote-chip';
 import '../../../styles/shared-styles';
 import {dom, EventApi} from '@polymer/polymer/lib/legacy/polymer.dom';
 import {PolymerElement} from '@polymer/polymer/polymer-element';
@@ -33,6 +34,8 @@
   DetailedLabelInfo,
   EmailAddress,
   AccountDetailInfo,
+  isDetailedLabelInfo,
+  LabelInfo,
 } from '../../../types/common';
 import {PolymerDeepPropertyChange} from '@polymer/polymer/interfaces';
 import {GrAccountChip} from '../../shared/gr-account-chip/gr-account-chip';
@@ -41,6 +44,7 @@
 import {ReviewerState} from '../../../constants/constants';
 import {appContext} from '../../../services/app-context';
 import {fireAlert} from '../../../utils/event-util';
+import {getApprovalInfo, getCodeReviewLabel} from '../../../utils/label-util';
 
 @customElement('gr-reviewer-list')
 export class GrReviewerList extends PolymerElement {
@@ -197,6 +201,20 @@
     return maxScores.join(', ');
   }
 
+  _computeVote(
+    reviewer: AccountInfo,
+    change?: ChangeInfo
+  ): ApprovalInfo | undefined {
+    const codeReviewLabel = this._computeCodeReviewLabel(change);
+    if (!codeReviewLabel || !isDetailedLabelInfo(codeReviewLabel)) return;
+    return getApprovalInfo(codeReviewLabel, reviewer);
+  }
+
+  _computeCodeReviewLabel(change?: ChangeInfo): LabelInfo | undefined {
+    if (!change || !change.labels) return;
+    return getCodeReviewLabel(change.labels);
+  }
+
   @observe('change.reviewers.*', 'change.owner')
   _reviewersChanged(
     changeRecord: PolymerDeepPropertyChange<Reviewers, Reviewers>,
diff --git a/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list_html.ts b/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list_html.ts
index ca8bf87..c579a59 100644
--- a/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list_html.ts
+++ b/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list_html.ts
@@ -70,6 +70,11 @@
           voteable-text="[[_computeVoteableText(reviewer, change)]]"
           removable="[[_computeCanRemoveReviewer(reviewer, mutable)]]"
         >
+          <gr-vote-chip
+            slot="vote-chip"
+            vote="[[_computeVote(reviewer, change)]]"
+            label="[[_computeCodeReviewLabel(change)]]"
+          ></gr-vote-chip>
         </gr-account-chip>
       </template>
       <div class="controlsContainer" hidden$="[[!mutable]]">
diff --git a/polygerrit-ui/app/elements/shared/gr-account-chip/gr-account-chip.ts b/polygerrit-ui/app/elements/shared/gr-account-chip/gr-account-chip.ts
index f703037..670302f 100644
--- a/polygerrit-ui/app/elements/shared/gr-account-chip/gr-account-chip.ts
+++ b/polygerrit-ui/app/elements/shared/gr-account-chip/gr-account-chip.ts
@@ -178,6 +178,7 @@
           .voteableText=${this.voteableText}
         >
         </gr-account-link>
+        <slot name="vote-chip"></slot>
         <gr-button
           id="remove"
           link=""
diff --git a/polygerrit-ui/app/elements/shared/gr-vote-chip/gr-vote-chip.ts b/polygerrit-ui/app/elements/shared/gr-vote-chip/gr-vote-chip.ts
new file mode 100644
index 0000000..789637c
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-vote-chip/gr-vote-chip.ts
@@ -0,0 +1,100 @@
+/**
+ * @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 {css, customElement, html, property} from 'lit-element';
+import {ApprovalInfo, LabelInfo} from '../../../api/rest-api';
+import {appContext} from '../../../services/app-context';
+import {KnownExperimentId} from '../../../services/flags/flags';
+import {getVotingRangeOrDefault, valueString} from '../../../utils/label-util';
+import {GrLitElement} from '../../lit/gr-lit-element';
+
+declare global {
+  interface HTMLElementTagNameMap {
+    'gr-vote-chip': GrVoteChip;
+  }
+}
+
+@customElement('gr-vote-chip')
+export class GrVoteChip extends GrLitElement {
+  @property({type: Object})
+  vote?: ApprovalInfo;
+
+  @property({type: Object})
+  label?: LabelInfo;
+
+  private readonly flagsService = appContext.flagsService;
+
+  static get styles() {
+    return [
+      css`
+        .chipVote {
+          display: flex;
+          justify-content: center;
+          margin-right: var(--spacing-s);
+          padding: 1px;
+          border-radius: var(--border-radius);
+          color: var(--vote-text-color);
+          border: 1px solid var(--border-color);
+          line-height: calc(var(--line-height-normal) - 4px);
+        }
+        .max {
+          background-color: var(--vote-color-approved);
+        }
+        .min {
+          background-color: var(--vote-color-rejected);
+        }
+        .positive {
+          background-color: var(--vote-color-recommended);
+          border: 1px solid var(--vote-outline-recommended);
+          color: var(--chip-color);
+        }
+        .negative {
+          background-color: var(--vote-color-disliked);
+          border: 1px solid var(--vote-outline-disliked);
+          color: var(--chip-color);
+        }
+      `,
+    ];
+  }
+
+  render() {
+    if (!this.flagsService.isEnabled(KnownExperimentId.SUBMIT_REQUIREMENTS_UI))
+      return;
+    if (!this.vote?.value) return;
+    const className = this.computeClass(this.vote.value, this.label);
+    return html`<div class="chipVote ${className}">
+      ${valueString(this.vote.value)}
+    </div>`;
+  }
+
+  computeClass(vote: Number, label?: LabelInfo) {
+    const votingRange = getVotingRangeOrDefault(label);
+    if (vote > 0) {
+      if (vote === votingRange.max) {
+        return 'max';
+      } else {
+        return 'positive';
+      }
+    } else if (vote < 0) {
+      if (vote === votingRange.min) {
+        return 'min';
+      } else {
+        return 'negative';
+      }
+    }
+    return '';
+  }
+}
diff --git a/polygerrit-ui/app/utils/label-util.ts b/polygerrit-ui/app/utils/label-util.ts
index bcc92e8..92bbdac 100644
--- a/polygerrit-ui/app/utils/label-util.ts
+++ b/polygerrit-ui/app/utils/label-util.ts
@@ -20,6 +20,7 @@
   DetailedLabelInfo,
   isDetailedLabelInfo,
   LabelInfo,
+  LabelNameToInfoMap,
   VotingRangeInfo,
 } from '../types/common';
 
@@ -102,3 +103,14 @@
 
   return labelName1.localeCompare(labelName2);
 }
+
+export function getCodeReviewLabel(
+  labels: LabelNameToInfoMap
+): LabelInfo | undefined {
+  for (const label of Object.keys(labels)) {
+    if (label === CODE_REVIEW) {
+      return labels[label];
+    }
+  }
+  return;
+}