Render Code Review votes in patchset picker

https://imgur.com/a/U7Mjj4E

VoteChip component is already used in the Submit Requirements section.

We only limit the values to Code Review vote.

Release-Notes: skip
Google-bug-id: b/230605262
Change-Id: I0fbc665865fb542d5482d54c1dc9bd067c0751b5
diff --git a/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select.ts b/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select.ts
index c5a72fc..fb2a2b7 100644
--- a/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select.ts
+++ b/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select.ts
@@ -22,8 +22,12 @@
 } from '../../../utils/patch-set-util';
 import {ReportingService} from '../../../services/gr-reporting/gr-reporting';
 import {
+  AccountInfo,
+  ApprovalInfo,
   BasePatchSetNum,
+  ChangeInfo,
   EDIT,
+  LabelInfo,
   NumericChangeId,
   PARENT,
   PatchSetNum,
@@ -38,7 +42,7 @@
   DropdownItem,
   GrDropdownList,
 } from '../../shared/gr-dropdown-list/gr-dropdown-list';
-import {EditRevisionInfo} from '../../../types/types';
+import {EditRevisionInfo, ParsedChangeInfo} from '../../../types/types';
 import {a11yStyles} from '../../../styles/gr-a11y-styles';
 import {sharedStyles} from '../../../styles/shared-styles';
 import {css, html, LitElement, nothing} from 'lit';
@@ -55,6 +59,9 @@
 import {changeViewModelToken} from '../../../models/views/change';
 import {fireNoBubbleNoCompose} from '../../../utils/event-util';
 import {FlagsService, KnownExperimentId} from '../../../services/flags/flags';
+import {userModelToken} from '../../../models/user/user-model';
+import {getCodeReviewLabel} from '../../../utils/label-util';
+import {getCodeReviewVotesFromMessage} from '../../../utils/message-util';
 
 // Maximum length for patch set descriptions.
 const PATCH_DESC_MAX_LENGTH = 500;
@@ -102,6 +109,12 @@
   @state()
   changeNum?: NumericChangeId;
 
+  @state()
+  change?: ParsedChangeInfo;
+
+  @state()
+  selfAccount?: AccountInfo;
+
   @property()
   path?: string;
 
@@ -131,6 +144,8 @@
 
   private readonly flags: FlagsService = getAppContext().flagsService;
 
+  private readonly getUserModel = resolve(this, userModelToken);
+
   private readonly getCommentsModel = resolve(this, commentsModelToken);
 
   private readonly getChangeModel = resolve(this, changeModelToken);
@@ -147,6 +162,17 @@
     subscribe(
       this,
       () => this.getChangeModel().change$,
+      x => (this.change = x)
+    );
+    subscribe(
+      this,
+      () => this.getUserModel().account$,
+      x => (this.selfAccount = x)
+    );
+
+    subscribe(
+      this,
+      () => this.getChangeModel().change$,
       x => (this.revisionInfo = x ? new RevisionInfoClass(x) : undefined)
     );
     subscribe(
@@ -357,6 +383,8 @@
         !!this.path /* ignorePatchsetLevelComments*/
       ),
       deemphasizeReason: this.computeDeemphasizeReason(sha),
+      vote: this.computeVote(this.selfAccount, this.change, patchNum),
+      label: this.computeCodeReviewLabel(this.change),
     };
     const date = this.computePatchSetDate(patchNum);
     if (date) {
@@ -376,6 +404,36 @@
       : undefined;
   }
 
+  private computeVote(
+    reviewer?: AccountInfo,
+    change?: ParsedChangeInfo,
+    revisionNum?: PatchSetNum
+  ): ApprovalInfo | undefined {
+    if (!change || !reviewer || !revisionNum) return undefined;
+
+    const codeReviewVotes = getCodeReviewVotesFromMessage(
+      change as ChangeInfo,
+      reviewer
+    );
+    const vote = codeReviewVotes.get(revisionNum);
+
+    if (vote) {
+      return {
+        ...reviewer,
+        value: Number(vote.value),
+      };
+    }
+
+    return undefined;
+  }
+
+  private computeCodeReviewLabel(
+    change?: ParsedChangeInfo
+  ): LabelInfo | undefined {
+    if (!change?.labels) return;
+    return getCodeReviewLabel(change.labels);
+  }
+
   /**
    * The basePatchNum should always be <= patchNum -- because sortedRevisions
    * is sorted in reverse order (higher patchset nums first), invalid base
diff --git a/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select_test.ts b/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select_test.ts
index 097da3c..ca93e1a 100644
--- a/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select_test.ts
+++ b/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select_test.ts
@@ -12,7 +12,9 @@
 import {ChangeComments} from '../gr-comment-api/gr-comment-api';
 import {queryAll, stubReporting} from '../../../test/test-utils';
 import {
+  AccountId,
   BasePatchSetNum,
+  ChangeMessageId,
   CommentInfo,
   EDIT,
   PARENT,
@@ -26,6 +28,7 @@
 import {EditRevisionInfo, ParsedChangeInfo} from '../../../types/types';
 import {SpecialFilePath} from '../../../constants/constants';
 import {
+  createAccountDetailWithId,
   createChangeViewState,
   createEditRevision,
   createParsedChange,
@@ -46,6 +49,7 @@
   changeModelToken,
   RevisionFileUpdateStatus,
 } from '../../../models/change/change-model';
+import {userModelToken} from '../../../models/user/user-model';
 
 type RevIdToRevisionInfo = {
   [revisionId: string]: RevisionInfo | EditRevisionInfo;
@@ -149,12 +153,14 @@
         date: '2020-02-01 01:02:03.000000000' as Timestamp,
         commentThreads: [],
         deemphasizeReason: undefined,
+        label: undefined,
+        vote: undefined,
       } as DropdownItem,
       {
-        text: 'Base | ',
         triggerText: 'Base',
         value: PARENT,
         bottomText: undefined,
+        text: 'Base | ',
       } as DropdownItem,
     ];
     element.patchNum = 2 as PatchSetNumber;
@@ -265,6 +271,8 @@
         value: EDIT,
         commentThreads: [],
         deemphasizeReason: undefined,
+        vote: undefined,
+        label: undefined,
       },
       {
         triggerText: 'Patchset 3',
@@ -275,6 +283,8 @@
         date: '2020-02-01 01:02:03.000000000' as Timestamp,
         commentThreads: [],
         deemphasizeReason: undefined,
+        vote: undefined,
+        label: undefined,
       } as DropdownItem,
       {
         triggerText: 'Patchset 2',
@@ -285,6 +295,8 @@
         date: '2020-02-01 01:02:03.000000000' as Timestamp,
         commentThreads: [],
         deemphasizeReason: undefined,
+        vote: undefined,
+        label: undefined,
       } as DropdownItem,
     ];
 
@@ -559,3 +571,84 @@
     );
   });
 });
+
+suite('gr-patch-range-select with votes', () => {
+  let element: GrPatchRangeSelect;
+
+  setup(async () => {
+    const changeModel = testResolver(changeModelToken);
+    const userModel = testResolver(userModelToken);
+
+    const viewModel = testResolver(changeViewModelToken);
+    viewModel.setState({
+      ...createChangeViewState(),
+      patchNum: 2 as RevisionPatchSetNum,
+      basePatchNum: PARENT,
+    });
+
+    const account = createAccountDetailWithId(1);
+    userModel.setAccount(account);
+
+    const change: ParsedChangeInfo = {
+      ...createParsedChange(),
+      messages: [
+        {
+          id: '1' as ChangeMessageId,
+          author: {_account_id: 1 as AccountId},
+          date: '2020-01-01 10:00:00' as Timestamp,
+          message: 'Patch Set 1: Code-Review+1',
+          _revision_number: 1 as PatchSetNumber,
+        },
+        {
+          id: '2' as ChangeMessageId,
+          author: {_account_id: 1 as AccountId},
+          date: '2020-01-01 11:00:00' as Timestamp,
+          message: 'Patch Set 2: Code-Review-1',
+          _revision_number: 2 as PatchSetNumber,
+        },
+      ],
+      revisions: {
+        sha1: createRevision(1),
+        sha2: createRevision(2),
+      },
+      labels: {
+        'Code-Review': {
+          values: {
+            '-1': 'No',
+            ' 0': 'No score',
+            '+1': 'Yes',
+          },
+        },
+      },
+    };
+
+    changeModel.updateStateChange(change);
+
+    element = await fixture(
+      html`<gr-patch-range-select></gr-patch-range-select>`
+    );
+    await element.updateComplete;
+
+    // Unclear why but it's required twice
+    changeModel.updateStateChange(change);
+    await element.updateComplete;
+  });
+
+  test('shows votes in dropdown', async () => {
+    const dropdown = queryAndAssert<GrDropdownList>(
+      element,
+      '#patchNumDropdown'
+    );
+    await dropdown.updateComplete;
+
+    dropdown.open();
+    await dropdown.updateComplete;
+
+    const menu = dropdown.shadowRoot?.querySelector('md-menu');
+    assert.isDefined(menu);
+
+    const voteChip = menu!.querySelector('gr-vote-chip');
+    assert.isDefined(voteChip);
+    assert.equal(voteChip!.vote?.value, -1);
+  });
+});
diff --git a/polygerrit-ui/app/elements/shared/gr-dropdown-list/gr-dropdown-list.ts b/polygerrit-ui/app/elements/shared/gr-dropdown-list/gr-dropdown-list.ts
index 3ace525..389b53c 100644
--- a/polygerrit-ui/app/elements/shared/gr-dropdown-list/gr-dropdown-list.ts
+++ b/polygerrit-ui/app/elements/shared/gr-dropdown-list/gr-dropdown-list.ts
@@ -7,8 +7,10 @@
 import '../gr-button/gr-button';
 import '../gr-date-formatter/gr-date-formatter';
 import '../gr-file-status/gr-file-status';
+import '../gr-vote-chip/gr-vote-chip';
 import {css, html, LitElement, nothing, PropertyValues} from 'lit';
 import {customElement, property, query, state} from 'lit/decorators.js';
+import {ApprovalInfo, LabelInfo} from '../../../api/rest-api';
 import {CommentThread, Timestamp} from '../../../types/common';
 import {NormalizedFileInfo} from '../../change/gr-file-list/gr-file-list';
 import {GrButton} from '../gr-button/gr-button';
@@ -47,6 +49,8 @@
   file?: NormalizedFileInfo;
   commentThreads?: CommentThread[];
   deemphasizeReason?: string;
+  vote?: ApprovalInfo;
+  label?: LabelInfo;
 }
 
 declare global {
@@ -180,6 +184,9 @@
           font-family: var(--trigger-style-font-family);
           --gr-button-text-color: var(--trigger-style-text-color);
         }
+        gr-vote-chip {
+          margin-right: var(--spacing-s);
+        }
         gr-date-formatter {
           color: var(--deemphasized-text-color);
           margin-left: var(--spacing-xxl);
@@ -398,6 +405,16 @@
             )}
           </div>
           ${when(
+            item.vote,
+            () =>
+              html` <div>
+                <gr-vote-chip
+                  .vote=${item.vote}
+                  .label=${item.label}
+                ></gr-vote-chip>
+              </div>`
+          )}
+          ${when(
             item.date,
             () => html`
               <gr-date-formatter .dateStr=${item.date}></gr-date-formatter>