blob: fb05b02ccd099a91006e983f2aa29999f12b2826 [file] [log] [blame]
/**
* @license
* Copyright (C) 2019 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 '@polymer/iron-icon/iron-icon';
import '../../../styles/shared-styles';
import '../../shared/gr-dialog/gr-dialog';
import '../../shared/gr-overlay/gr-overlay';
import '../gr-diff/gr-diff';
import {PolymerElement} from '@polymer/polymer/polymer-element';
import {htmlTemplate} from './gr-apply-fix-dialog_html';
import {GerritNav} from '../../core/gr-navigation/gr-navigation';
import {customElement, property} from '@polymer/decorators';
import {
NumericChangeId,
EditPatchSetNum,
FixId,
FixSuggestionInfo,
PatchSetNum,
RobotId,
BasePatchSetNum,
} from '../../../types/common';
import {DiffInfo, DiffPreferencesInfo} from '../../../types/diff';
import {GrOverlay} from '../../shared/gr-overlay/gr-overlay';
import {isRobot} from '../../../utils/comment-util';
import {OpenFixPreviewEvent} from '../../../types/events';
import {getAppContext} from '../../../services/app-context';
import {fireCloseFixPreview, fireEvent} from '../../../utils/event-util';
import {DiffLayer, ParsedChangeInfo} from '../../../types/types';
import {GrButton} from '../../shared/gr-button/gr-button';
import {TokenHighlightLayer} from '../gr-diff-builder/token-highlight-layer';
export interface GrApplyFixDialog {
$: {
applyFixOverlay: GrOverlay;
nextFix: GrButton;
};
}
interface FilePreview {
filepath: string;
preview: DiffInfo;
}
@customElement('gr-apply-fix-dialog')
export class GrApplyFixDialog extends PolymerElement {
static get template() {
return htmlTemplate;
}
@property({type: Object})
prefs?: DiffPreferencesInfo;
@property({type: Object})
change?: ParsedChangeInfo;
@property({type: String})
changeNum?: NumericChangeId;
@property({type: Number})
_patchNum?: PatchSetNum;
@property({type: String})
_robotId?: RobotId;
@property({type: Object})
_currentFix?: FixSuggestionInfo;
@property({type: Array})
_currentPreviews: FilePreview[] = [];
@property({type: Array})
_fixSuggestions?: FixSuggestionInfo[];
@property({type: Boolean})
_isApplyFixLoading = false;
@property({type: Number})
_selectedFixIdx = 0;
@property({
type: Boolean,
computed:
'_computeDisableApplyFixButton(_isApplyFixLoading, change, ' +
'_patchNum)',
})
_disableApplyFixButton = false;
@property({type: Array})
layers: DiffLayer[] = [];
private refitOverlay?: () => void;
private readonly restApiService = getAppContext().restApiService;
constructor() {
super();
this.restApiService.getPreferences().then(prefs => {
if (!prefs?.disable_token_highlighting) {
this.layers = [new TokenHighlightLayer(this)];
}
});
}
/**
* Given robot comment CustomEvent object, fetch diffs associated
* with first robot comment suggested fix and open dialog.
*
* @param e to be passed from gr-comment with robot comment detail.
* @return Promise that resolves either when all
* preview diffs are fetched or no fix suggestions in custom event detail.
*/
open(e: OpenFixPreviewEvent) {
const detail = e.detail;
const comment = detail.comment;
if (!detail.patchNum || !comment || !isRobot(comment)) {
return Promise.resolve();
}
this._patchNum = detail.patchNum;
this._fixSuggestions = comment.fix_suggestions;
this._robotId = comment.robot_id;
if (!this._fixSuggestions || !this._fixSuggestions.length) {
return Promise.resolve();
}
this._selectedFixIdx = 0;
const promises = [];
promises.push(
this._showSelectedFixSuggestion(this._fixSuggestions[0]),
this.$.applyFixOverlay.open()
);
return Promise.all(promises).then(() => {
// ensures gr-overlay repositions overlay in center
fireEvent(this.$.applyFixOverlay, 'iron-resize');
});
}
override connectedCallback() {
super.connectedCallback();
this.refitOverlay = () => {
// re-center the dialog as content changed
fireEvent(this.$.applyFixOverlay, 'iron-resize');
};
this.addEventListener('diff-context-expanded', this.refitOverlay);
}
override disconnectedCallback() {
if (this.refitOverlay) {
this.removeEventListener('diff-context-expanded', this.refitOverlay);
}
super.disconnectedCallback();
}
_showSelectedFixSuggestion(fixSuggestion: FixSuggestionInfo) {
this._currentFix = fixSuggestion;
return this._fetchFixPreview(fixSuggestion.fix_id);
}
_fetchFixPreview(fixId: FixId) {
if (!this.changeNum || !this._patchNum) {
return Promise.reject(
new Error('Both _patchNum and changeNum must be set')
);
}
return this.restApiService
.getRobotCommentFixPreview(this.changeNum, this._patchNum, fixId)
.then(res => {
if (res) {
this._currentPreviews = Object.keys(res).map(key => {
return {filepath: key, preview: res[key]};
});
}
})
.catch(err => {
this._close(false);
throw err;
});
}
hasSingleFix(_fixSuggestions?: FixSuggestionInfo[]) {
return (_fixSuggestions || []).length === 1;
}
overridePartialPrefs(prefs?: DiffPreferencesInfo) {
if (!prefs) return undefined;
// generate a smaller gr-diff than fullscreen for dialog
return {...prefs, line_length: 50};
}
onCancel(e: Event) {
if (e) {
e.stopPropagation();
}
this._close(false);
}
addOneTo(_selectedFixIdx: number) {
return _selectedFixIdx + 1;
}
_onPrevFixClick(e: Event) {
if (e) e.stopPropagation();
if (this._selectedFixIdx >= 1 && this._fixSuggestions) {
this._selectedFixIdx -= 1;
this._showSelectedFixSuggestion(
this._fixSuggestions[this._selectedFixIdx]
);
}
}
_onNextFixClick(e: Event) {
if (e) e.stopPropagation();
if (
this._fixSuggestions &&
this._selectedFixIdx < this._fixSuggestions.length
) {
this._selectedFixIdx += 1;
this._showSelectedFixSuggestion(
this._fixSuggestions[this._selectedFixIdx]
);
}
}
_noPrevFix(_selectedFixIdx: number) {
return _selectedFixIdx === 0;
}
_noNextFix(_selectedFixIdx: number, fixSuggestions?: FixSuggestionInfo[]) {
if (!fixSuggestions) return true;
return _selectedFixIdx === fixSuggestions.length - 1;
}
_close(fixApplied: boolean) {
this._currentFix = undefined;
this._currentPreviews = [];
this._isApplyFixLoading = false;
fireCloseFixPreview(this, fixApplied);
this.$.applyFixOverlay.close();
}
_getApplyFixButtonLabel(isLoading: boolean) {
return isLoading ? 'Saving...' : 'Apply Fix';
}
_computeTooltip(change?: ParsedChangeInfo, patchNum?: PatchSetNum) {
if (!change || !patchNum) return '';
const latestPatchNum = change.revisions[change.current_revision]._number;
return latestPatchNum !== patchNum
? 'Fix can only be applied to the latest patchset'
: '';
}
_computeDisableApplyFixButton(
isApplyFixLoading: boolean,
change?: ParsedChangeInfo,
patchNum?: PatchSetNum
) {
if (!change || isApplyFixLoading === undefined || patchNum === undefined) {
return true;
}
const currentPatchNum = change.revisions[change.current_revision]._number;
if (patchNum !== currentPatchNum) {
return true;
}
return isApplyFixLoading;
}
_handleApplyFix(e: Event) {
if (e) {
e.stopPropagation();
}
const changeNum = this.changeNum;
const patchNum = this._patchNum;
const change = this.change;
if (!changeNum || !patchNum || !change || !this._currentFix) {
return Promise.reject(new Error('Not all required properties are set.'));
}
this._isApplyFixLoading = true;
return this.restApiService
.applyFixSuggestion(changeNum, patchNum, this._currentFix.fix_id)
.then(res => {
if (res && res.ok) {
GerritNav.navigateToChange(change, {
patchNum: EditPatchSetNum,
basePatchNum: patchNum as BasePatchSetNum,
});
this._close(true);
}
this._isApplyFixLoading = false;
});
}
getFixDescription(currentFix?: FixSuggestionInfo) {
return currentFix && currentFix.description ? currentFix.description : '';
}
}
declare global {
interface HTMLElementTagNameMap {
'gr-apply-fix-dialog': GrApplyFixDialog;
}
}