blob: 09b5cc9f399be43412f371a92f7498aefb362da0 [file] [log] [blame]
/**
* @license
* Copyright (C) 2022 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 '../../change/gr-submit-requirement-dashboard-hovercard/gr-submit-requirement-dashboard-hovercard';
import '../../shared/gr-change-status/gr-change-status';
import {LitElement, css, html} from 'lit';
import {customElement, property} from 'lit/decorators';
import {
ApprovalInfo,
ChangeInfo,
isDetailedLabelInfo,
isQuickLabelInfo,
LabelInfo,
SubmitRequirementResultInfo,
SubmitRequirementStatus,
} from '../../../api/rest-api';
import {submitRequirementsStyles} from '../../../styles/gr-submit-requirements-styles';
import {
extractAssociatedLabels,
getAllUniqueApprovals,
getRequirements,
getTriggerVotes,
hasNeutralStatus,
hasVotes,
iconForStatus,
} from '../../../utils/label-util';
import {sharedStyles} from '../../../styles/shared-styles';
import {ifDefined} from 'lit/directives/if-defined';
import {capitalizeFirstLetter} from '../../../utils/string-util';
@customElement('gr-change-list-column-requirement')
export class GrChangeListColumnRequirement extends LitElement {
@property({type: Object})
change?: ChangeInfo;
@property()
labelName?: string;
static override get styles() {
return [
submitRequirementsStyles,
sharedStyles,
css`
iron-icon {
vertical-align: top;
}
.container {
display: flex;
align-items: center;
justify-content: center;
}
.container.not-applicable {
background-color: var(--table-header-background-color);
height: calc(var(--line-height-normal) + var(--spacing-m));
}
`,
];
}
override render() {
return html`<div
class="container ${this.computeClass()}"
title=${ifDefined(this.computeLabelTitle())}
>
${this.renderContent()}
</div>`;
}
private renderContent() {
if (!this.labelName) return;
const requirements = this.getRequirement(this.labelName);
if (requirements.length === 0) {
return this.renderTriggerVote();
}
const requirement = requirements[0];
if (requirement.status === SubmitRequirementStatus.UNSATISFIED) {
return this.renderUnsatisfiedState(requirement);
} else {
return this.renderStatusIcon(requirement.status);
}
}
private renderTriggerVote() {
if (!this.labelName || !this.isTriggerVote(this.labelName)) return;
const allLabels = this.change?.labels ?? {};
const labelInfo = allLabels[this.labelName];
if (isDetailedLabelInfo(labelInfo)) {
// votes sorted from best e.g +2 to worst e.g -2
const votes = this.getSortedVotes(this.labelName);
if (votes.length > 0) {
const bestVote = votes[0];
return html`<gr-vote-chip
.vote=${bestVote}
.label=${labelInfo}
tooltip-with-who-voted
></gr-vote-chip>`;
}
}
if (isQuickLabelInfo(labelInfo)) {
return html`<gr-vote-chip .label=${labelInfo}></gr-vote-chip>`;
}
return;
}
private renderUnsatisfiedState(requirement: SubmitRequirementResultInfo) {
const requirementLabels = extractAssociatedLabels(
requirement,
'onlySubmittability'
);
const allLabels = this.change?.labels ?? {};
const associatedLabels = Object.keys(allLabels).filter(label =>
requirementLabels.includes(label)
);
let worstVote: ApprovalInfo | undefined;
let labelInfo: LabelInfo | undefined;
for (const label of associatedLabels) {
// votes sorted from worst e.g -2 to best e.g +2
const votes = this.getSortedVotes(label).sort(
(a, b) => (a.value ?? 0) - (b.value ?? 0)
);
if (votes.length === 0) break;
if (!worstVote || (worstVote.value ?? 0) > (votes[0].value ?? 0)) {
worstVote = votes[0];
labelInfo = allLabels[label];
}
}
if (worstVote === undefined) {
return this.renderStatusIcon(requirement.status);
} else {
return html`<gr-vote-chip
.vote=${worstVote}
.label=${labelInfo}
tooltip-with-who-voted
></gr-vote-chip>`;
}
}
private renderStatusIcon(status: SubmitRequirementStatus) {
const icon = iconForStatus(status ?? SubmitRequirementStatus.ERROR);
return html`<iron-icon class=${icon} icon="gr-icons:${icon}"></iron-icon>`;
}
private computeClass(): string {
if (!this.labelName) return '';
const requirements = this.getRequirement(this.labelName);
if (requirements.length === 0 && !this.isTriggerVote(this.labelName)) {
return 'not-applicable';
}
return '';
}
private computeLabelTitle() {
if (!this.labelName) return;
const requirements = this.getRequirement(this.labelName);
if (requirements.length === 0) {
if (this.isTriggerVote(this.labelName)) {
return;
} else {
return 'Requirement not applicable';
}
}
const requirement = requirements[0];
if (requirement.status === SubmitRequirementStatus.UNSATISFIED) {
const requirementLabels = extractAssociatedLabels(
requirement,
'onlySubmittability'
);
const allLabels = this.change?.labels ?? {};
const associatedLabels = Object.keys(allLabels).filter(label =>
requirementLabels.includes(label)
);
const requirementWithoutLabelToVoteOn = associatedLabels.length === 0;
if (requirementWithoutLabelToVoteOn) {
const status = capitalizeFirstLetter(requirement.status.toLowerCase());
return status;
}
const everyAssociatedLabelsIsWithoutVotes = associatedLabels.every(
label => !hasVotes(allLabels[label])
);
if (everyAssociatedLabelsIsWithoutVotes) {
return 'No votes';
} else {
return; // there is a vote with tooltip, so undefined label title
}
} else {
return capitalizeFirstLetter(requirement.status.toLowerCase());
}
}
private getRequirement(labelName: string) {
const requirements = getRequirements(this.change).filter(
sr => sr.name === labelName
);
// TODO(milutin): Remove this after migration from legacy requirements.
if (requirements.length > 1) {
return requirements.filter(sr => !sr.is_legacy);
} else {
return requirements;
}
}
private getSortedVotes(label: string) {
const allLabels = this.change?.labels ?? {};
const labelInfo = allLabels[label];
if (isDetailedLabelInfo(labelInfo)) {
return getAllUniqueApprovals(labelInfo).filter(
approval => !hasNeutralStatus(labelInfo, approval)
);
}
return [];
}
private isTriggerVote(labelName: string) {
const triggerVotes = getTriggerVotes(this.change);
return triggerVotes.includes(labelName);
}
}
declare global {
interface HTMLElementTagNameMap {
'gr-change-list-column-requirement': GrChangeListColumnRequirement;
}
}