blob: 689a9fbd53bacf0741f737586e4eff7b5ebd8c50 [file] [log] [blame]
/**
* @license
* Copyright (C) 2016 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 '../gr-account-label/gr-account-label';
import '../gr-button/gr-button';
import '../gr-icons/gr-icons';
import {
AccountInfo,
ApprovalInfo,
ChangeInfo,
LabelInfo,
} from '../../../types/common';
import {getAppContext} from '../../../services/app-context';
import {LitElement, css, html} from 'lit';
import {customElement, property} from 'lit/decorators';
import {ClassInfo, classMap} from 'lit/directives/class-map';
import {KnownExperimentId} from '../../../services/flags/flags';
import {getLabelStatus, hasVoted, LabelStatus} from '../../../utils/label-util';
@customElement('gr-account-chip')
export class GrAccountChip extends LitElement {
/**
* Fired to indicate a key was pressed while this chip was focused.
*
* @event account-chip-keydown
*/
/**
* Fired to indicate this chip should be removed, i.e. when the x button is
* clicked or when the remove function is called.
*
* @event remove
*/
@property({type: Object})
account?: AccountInfo;
/**
* Optional ChangeInfo object, typically comes from the change page or
* from a row in a list of search results. This is needed for some change
* related features like adding the user as a reviewer.
*/
@property({type: Object})
change?: ChangeInfo;
/**
* Should this user be considered to be in the attention set, regardless
* of the current state of the change object?
*/
@property({type: Boolean})
forceAttention = false;
@property({type: String})
voteableText?: string;
@property({type: Boolean, reflect: true})
disabled = false;
@property({type: Boolean, reflect: true})
removable = false;
/**
* Should attention set related features be shown in the component? Note
* that the information whether the user is in the attention set or not is
* part of the ChangeInfo object in the change property.
*/
@property({type: Boolean})
highlightAttention = false;
@property({type: Boolean, reflect: true})
showAvatar?: boolean;
@property({type: Boolean})
transparentBackground = false;
@property({type: Object})
vote?: ApprovalInfo;
@property({type: Object})
label?: LabelInfo;
private readonly restApiService = getAppContext().restApiService;
private readonly flagsService = getAppContext().flagsService;
static override get styles() {
return [
css`
:host {
display: block;
overflow: hidden;
}
.container {
align-items: center;
background-color: var(--background-color-primary);
/** round */
border-radius: var(--account-chip-border-radius, 20px);
border: 1px solid var(--border-color);
display: inline-flex;
padding: 0 1px;
/* Any outermost circular icon would fit neatly in the border-radius
and won't need padding, but the exact outermost elements will
depend on account state and the context gr-account-chip is used.
So, these values are passed down to gr-account-label and any
outermost elements will use the value and then override it. */
--account-label-padding-left: 6px;
--account-label-padding-right: 6px;
--account-label-circle-padding-left: 0;
--account-label-circle-padding-right: 0;
}
:host:focus {
border-color: transparent;
box-shadow: none;
outline: none;
}
:host:focus .container,
:host:focus gr-button {
background: #ccc;
}
.transparentBackground,
gr-button.transparentBackground {
background-color: transparent;
}
:host([disabled]) {
opacity: 0.6;
pointer-events: none;
}
iron-icon {
height: 1.2rem;
width: 1.2rem;
}
.container gr-account-label::part(gr-account-label-text) {
color: var(--deemphasized-text-color);
}
.container.disliked {
border: 1px solid var(--vote-outline-disliked);
}
.container.recommended {
border: 1px solid var(--vote-outline-recommended);
}
.container.disliked,
.container.recommended {
--account-label-padding-right: var(--spacing-xs);
--account-label-circle-padding-right: var(--spacing-xs);
}
.container.closeShown {
--account-label-padding-right: 3px;
--account-label-circle-padding-right: 3px;
}
`,
];
}
override render() {
// To pass CSS mixins for @apply to Polymer components, they need to appear
// in <style> inside the template.
/* eslint-disable lit/prefer-static-styles */
const customStyle = html`
<style>
gr-button.remove::part(paper-button),
gr-button.remove:hover::part(paper-button),
gr-button.remove:focus::part(paper-button) {
border-top-width: 0;
border-right-width: 0;
border-bottom-width: 0;
border-left-width: 0;
color: var(--deemphasized-text-color);
font-weight: var(--font-weight-normal);
height: 0.6em;
line-height: 10px;
/* This cancels most of the --account-label-padding-horizontal. */
margin-left: -4px;
padding: 0 2px 0 1px;
text-decoration: none;
}
</style>
`;
return html`${customStyle}
<div
class=${classMap({
...this.computeVoteClasses(),
container: true,
transparentBackground: this.transparentBackground,
closeShown: this.removable,
})}
>
<div>
<gr-account-label
.account=${this.account}
.change=${this.change}
?forceAttention=${this.forceAttention}
?highlightAttention=${this.highlightAttention}
.voteableText=${this.voteableText}
clickable
>
</gr-account-label>
</div>
<slot name="vote-chip"></slot>
<gr-button
id="remove"
link=""
?hidden=${!this.removable}
aria-label="Remove"
class=${classMap({
remove: true,
transparentBackground: this.transparentBackground,
})}
@click=${this._handleRemoveTap}
>
<iron-icon icon="gr-icons:close"></iron-icon>
</gr-button>
</div>`;
}
constructor() {
super();
this._getHasAvatars().then(hasAvatars => {
this.showAvatar = hasAvatars;
});
}
_handleRemoveTap(e: MouseEvent) {
e.preventDefault();
this.dispatchEvent(
new CustomEvent('remove', {
detail: {account: this.account},
composed: true,
bubbles: true,
})
);
}
_getHasAvatars() {
return this.restApiService
.getConfig()
.then(cfg =>
Promise.resolve(!!(cfg && cfg.plugin && cfg.plugin.has_avatars))
);
}
private computeVoteClasses(): ClassInfo {
if (
!this.flagsService.isEnabled(KnownExperimentId.SUBMIT_REQUIREMENTS_UI) ||
!this.label ||
!this.account ||
!hasVoted(this.label, this.account)
) {
return {};
}
const status = getLabelStatus(this.label, this.vote?.value);
if ([LabelStatus.APPROVED, LabelStatus.RECOMMENDED].includes(status)) {
return {recommended: true};
} else if ([LabelStatus.REJECTED, LabelStatus.DISLIKED].includes(status)) {
return {disliked: true};
} else {
return {};
}
}
}
declare global {
interface HTMLElementTagNameMap {
'gr-account-chip': GrAccountChip;
}
}