Display file owner's vote value in the File Owners summary Display the vote value of a label that is required in order to satisfy the OWNERS definition as returned from the plugin's API (typically `Code-Review`). Note that default or different (e.g. `Verified`) labels are not shown as they are not relevant from the owners perspective. The `computeApprovalAndInfo` function (that is responsible for getting the necessary information) is covered in the unit tests. Note that a dedicated UI component (`gr-owner`) was introduced to encapsulate the owner's details. It has to be a part of `gr-owners` file as otherwise it is not properly loaded. TODO: add copy file owner's email button Bug: Issue 373151160 Change-Id: I07919c66f4cda95da40a62086a1b832aec7e8a4a
diff --git a/owners/web/gr-owners.ts b/owners/web/gr-owners.ts index b989eba..4730586 100644 --- a/owners/web/gr-owners.ts +++ b/owners/web/gr-owners.ts
@@ -20,12 +20,16 @@ import {customElement, property, state} from 'lit/decorators'; import { AccountInfo, + ApprovalInfo, ChangeInfo, ChangeStatus, + LabelInfo, + isDetailedLabelInfo, } from '@gerritcodereview/typescript-api/rest-api'; import { FilesOwners, isOwner, + OwnersLabels, OWNERS_SUBMIT_REQUIREMENT, OwnersService, } from './owners-service'; @@ -197,6 +201,58 @@ } } +/** + * It has to be part of this file as components defined in dedicated files are not visible + */ +@customElement('gr-owner') +export class GrOwner extends LitElement { + @property({type: Object}) + owner?: AccountInfo; + + @property({type: Object}) + approval?: ApprovalInfo; + + @property({type: Object}) + info?: LabelInfo; + + static override get styles() { + return [ + css` + .container { + display: flex; + } + gr-vote-chip { + margin-left: 5px; + --gr-vote-chip-width: 14px; + --gr-vote-chip-height: 14px; + } + `, + ]; + } + + override render() { + if (!this.owner) { + return nothing; + } + + const voteChip = this.approval + ? html` <gr-vote-chip + slot="vote-chip" + .vote=${this.approval} + .label=${this.info} + circle-shape + ></gr-vote-chip>` + : nothing; + + return html` + <div class="container"> + <gr-account-label .account=${this.owner}></gr-account-label> + ${voteChip} + </div> + `; + } +} + export const FILE_OWNERS_COLUMN_CONTENT = 'file-owners-column-content'; @customElement(FILE_OWNERS_COLUMN_CONTENT) export class FileOwnersColumnContent extends common { @@ -318,17 +374,27 @@ </div> <div class="section"> <div class="sectionContent"> - ${splicedOwners.map( - (owner, idx) => html` + ${splicedOwners.map((owner, idx) => { + const [approval, info] = + computeApprovalAndInfo( + owner, + this.filesOwners?.owners_labels ?? {}, + this.change + ) ?? []; + return html` <div class="row ${showEllipsis || idx + 1 < splicedOwners.length ? 'notLast' : ''}" > - <gr-account-label .account=${owner}></gr-account-label> + <gr-owner + .owner=${owner} + .approval=${approval} + .info=${info} + ></gr-owner> </div> - ` - )} + `; + })} ${showEllipsis ? html` <gr-tooltip-content @@ -430,3 +496,37 @@ owners: fileOwners, }) as unknown as FileOwnership; } + +export function computeApprovalAndInfo( + fileOwner: AccountInfo, + labels: OwnersLabels, + change?: ChangeInfo +): [ApprovalInfo, LabelInfo] | undefined { + if (!change?.labels) { + return; + } + const ownersLabel = labels[`${fileOwner._account_id}`]; + if (!ownersLabel) { + return; + } + + for (const label of Object.keys(ownersLabel)) { + const info = change.labels[label]; + if (!info || !isDetailedLabelInfo(info)) { + return; + } + + const vote = ownersLabel[label]; + if ((info.default_value && info.default_value === vote) || vote === 0) { + // ignore default value + return; + } + + const approval = info.all?.filter( + x => x._account_id === fileOwner._account_id + )[0]; + return approval ? [approval, info] : undefined; + } + + return; +}
diff --git a/owners/web/gr-owners_test.ts b/owners/web/gr-owners_test.ts index 0b4a5ec..4574c3e 100644 --- a/owners/web/gr-owners_test.ts +++ b/owners/web/gr-owners_test.ts
@@ -17,14 +17,21 @@ import {assert} from '@open-wc/testing'; -import {getFileOwnership, shouldHide} from './gr-owners'; +import { + computeApprovalAndInfo, + getFileOwnership, + shouldHide, +} from './gr-owners'; import {FileOwnership, FileStatus, PatchRange, UserRole} from './owners-model'; import { + AccountInfo, + ApprovalInfo, ChangeInfo, ChangeStatus, + DetailedLabelInfo, SubmitRequirementResultInfo, } from '@gerritcodereview/typescript-api/rest-api'; -import {FilesOwners} from './owners-service'; +import {FilesOwners, OwnersLabels} from './owners-service'; import {deepEqual} from './utils'; suite('owners status tests', () => { @@ -250,6 +257,101 @@ ); }); }); + + suite('computeApprovalAndInfo tests', () => { + const account = 1; + const fileOwner = {_account_id: account} as unknown as AccountInfo; + const label = 'Code-Review'; + const crPlus1OwnersVote = { + [`${account}`]: {[label]: 1}, + } as unknown as OwnersLabels; + const changeWithLabels = { + labels: { + [label]: { + all: [ + { + value: 1, + date: '2024-10-22 17:26:21.000000000', + permitted_voting_range: { + min: -2, + max: 2, + }, + _account_id: account, + }, + ], + values: { + '-2': 'This shall not be submitted', + '-1': 'I would prefer this is not submitted as is', + ' 0': 'No score', + '+1': 'Looks good to me, but someone else must approve', + '+2': 'Looks good to me, approved', + }, + description: '', + default_value: 0, + }, + }, + } as unknown as ChangeInfo; + + test('computeApprovalAndInfo - should be `undefined` when change is `undefined', () => { + const undefinedChange = undefined; + assert.equal( + computeApprovalAndInfo(fileOwner, crPlus1OwnersVote, undefinedChange), + undefined + ); + }); + + test('computeApprovalAndInfo - should be `undefined` when there is no owners vote', () => { + const emptyOwnersVote = {}; + assert.equal( + computeApprovalAndInfo(fileOwner, emptyOwnersVote, changeWithLabels), + undefined + ); + }); + + test('computeApprovalAndInfo - should be `undefined` for default owners vote', () => { + const defaultOwnersVote = {[label]: 0} as unknown as OwnersLabels; + assert.equal( + computeApprovalAndInfo(fileOwner, defaultOwnersVote, changeWithLabels), + undefined + ); + }); + + test('computeApprovalAndInfo - should be computed for CR+1 owners vote', () => { + const expectedApproval = { + value: 1, + date: '2024-10-22 17:26:21.000000000', + permitted_voting_range: { + min: -2, + max: 2, + }, + _account_id: account, + } as unknown as ApprovalInfo; + const expectedInfo = { + all: [expectedApproval], + values: { + '-2': 'This shall not be submitted', + '-1': 'I would prefer this is not submitted as is', + ' 0': 'No score', + '+1': 'Looks good to me, but someone else must approve', + '+2': 'Looks good to me, approved', + }, + description: '', + default_value: 0, + } as unknown as DetailedLabelInfo; + + assert.equal( + deepEqual( + computeApprovalAndInfo( + fileOwner, + crPlus1OwnersVote, + changeWithLabels + ), + [expectedApproval, expectedInfo] + ), + true + ); + }); + }); }); function getRandom<T>(...values: T[]): T {