blob: b86dd902715c102113fd421aeae6899851f97923 [file] [log] [blame]
/**
* @license
* Copyright (C) 2017 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 '../../../styles/shared-styles';
import '../../diff/gr-diff-mode-selector/gr-diff-mode-selector';
import '../../diff/gr-patch-range-select/gr-patch-range-select';
import '../../edit/gr-edit-controls/gr-edit-controls';
import '../../shared/gr-editable-label/gr-editable-label';
import '../../shared/gr-linked-chip/gr-linked-chip';
import '../../shared/gr-rest-api-interface/gr-rest-api-interface';
import '../../shared/gr-select/gr-select';
import '../../shared/gr-button/gr-button';
import '../../shared/gr-icons/gr-icons';
import '../gr-commit-info/gr-commit-info';
import {dom, EventApi} from '@polymer/polymer/lib/legacy/polymer.dom';
import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners';
import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin';
import {PolymerElement} from '@polymer/polymer/polymer-element';
import {htmlTemplate} from './gr-file-list-header_html';
import {KeyboardShortcutMixin} from '../../../mixins/keyboard-shortcut-mixin/keyboard-shortcut-mixin';
import {FilesExpandedState} from '../gr-file-list-constants';
import {GerritNav} from '../../core/gr-navigation/gr-navigation';
import {
computeLatestPatchNum,
getRevisionByPatchNum,
patchNumEquals,
PatchSet,
} from '../../../utils/patch-set-util';
import {property, computed, observe, customElement} from '@polymer/decorators';
import {
AccountInfo,
ChangeInfo,
PatchSetNum,
CommitInfo,
ServerInfo,
DiffPreferencesInfo,
RevisionInfo,
NumericChangeId,
} from '../../../types/common';
import {ChangeComments} from '../../diff/gr-comment-api/gr-comment-api';
import {GrDiffModeSelector} from '../../diff/gr-diff-mode-selector/gr-diff-mode-selector';
import {DiffViewMode} from '../../../constants/constants';
import {RestApiService} from '../../../services/services/gr-rest-api/gr-rest-api';
import {GrButton} from '../../shared/gr-button/gr-button';
// Maximum length for patch set descriptions.
const PATCH_DESC_MAX_LENGTH = 500;
const MERGED_STATUS = 'MERGED';
declare global {
interface HTMLElementTagNameMap {
'gr-file-list-header': GrFileListHeader;
}
}
export interface GrFileListHeader {
$: {
modeSelect: GrDiffModeSelector;
restAPI: RestApiService & Element;
expandBtn: GrButton;
collapseBtn: GrButton;
};
}
@customElement('gr-file-list-header')
export class GrFileListHeader extends KeyboardShortcutMixin(
GestureEventListeners(LegacyElementMixin(PolymerElement))
) {
static get template() {
return htmlTemplate;
}
/**
* @event expand-diffs
*/
/**
* @event collapse-diffs
*/
/**
* @event open-diff-prefs
*/
/**
* @event open-included-in-dialog
*/
/**
* @event open-download-dialog
*/
/**
* @event open-upload-help-dialog
*/
@property({type: Object})
account: AccountInfo | undefined;
@property({type: Array})
allPatchSets?: PatchSet[];
@property({type: Object})
change: ChangeInfo | undefined;
@property({type: String})
changeNum?: NumericChangeId;
@property({type: String})
changeUrl?: string;
@property({type: Object})
changeComments?: ChangeComments;
@property({type: Object})
commitInfo?: CommitInfo;
@property({type: Boolean})
editMode?: boolean;
@property({type: Boolean})
loggedIn: boolean | undefined;
@property({type: Object})
serverConfig?: ServerInfo;
@property({type: Number})
shownFileCount?: number;
@property({type: Object})
diffPrefs?: DiffPreferencesInfo;
@property({type: Boolean})
diffPrefsDisabled?: boolean;
@property({type: String, notify: true})
diffViewMode?: DiffViewMode;
@property({type: String})
patchNum?: PatchSetNum;
@property({type: String})
basePatchNum?: PatchSetNum;
@property({type: String})
filesExpanded?: FilesExpandedState;
// Caps the number of files that can be shown and have the 'show diffs' /
// 'hide diffs' buttons still be functional.
@property({type: Number})
readonly _maxFilesForBulkActions = 225;
@property({type: String})
_patchsetDescription = '';
@property({type: Object})
revisionInfo?: RevisionInfo;
@computed('loggedIn', 'change', 'account')
get _descriptionReadOnly(): boolean {
if (
this.loggedIn === undefined ||
this.change === undefined ||
this.account === undefined
) {
return true;
}
return !(
this.loggedIn &&
this.account._account_id === this.change.owner._account_id
);
}
setDiffViewMode(mode: DiffViewMode) {
this.$.modeSelect.setMode(mode);
}
_expandAllDiffs() {
this.dispatchEvent(
new CustomEvent('expand-diffs', {
composed: true,
bubbles: true,
})
);
}
_collapseAllDiffs() {
this.dispatchEvent(
new CustomEvent('collapse-diffs', {
composed: true,
bubbles: true,
})
);
}
_computeExpandedClass(filesExpanded: FilesExpandedState) {
const classes = [];
if (filesExpanded === FilesExpandedState.ALL) {
classes.push('expanded');
}
if (
filesExpanded === FilesExpandedState.SOME ||
filesExpanded === FilesExpandedState.ALL
) {
classes.push('openFile');
}
return classes.join(' ');
}
_computeDescriptionPlaceholder(readOnly: boolean) {
return (readOnly ? 'No' : 'Add') + ' patchset description';
}
@observe('change', 'patchNum')
_computePatchSetDescription(change: ChangeInfo, patchNum: PatchSetNum) {
// Polymer 2: check for undefined
if (
change === undefined ||
change.revisions === undefined ||
patchNum === undefined
) {
return;
}
const rev = getRevisionByPatchNum(
Object.values(change.revisions),
patchNum
);
this._patchsetDescription = rev?.description
? rev.description.substring(0, PATCH_DESC_MAX_LENGTH)
: '';
}
_handleDescriptionRemoved(e: CustomEvent) {
return this._updateDescription('', e);
}
/**
* @param revisions The revisions object keyed by revision hashes
* @param patchSet A revision already fetched from {revisions}
* @return the SHA hash corresponding to the revision.
*/
_getPatchsetHash(
revisions: {[revisionId: string]: RevisionInfo},
patchSet: RevisionInfo
) {
for (const sha of Object.keys(revisions)) {
if (revisions[sha] === patchSet) {
return sha;
}
}
throw new Error('patchset hash not found');
}
_handleDescriptionChanged(e: CustomEvent) {
const desc = e.detail.trim();
this._updateDescription(desc, e);
}
/**
* Update the patchset description with the rest API.
*/
_updateDescription(desc: string, e: CustomEvent) {
if (
!this.change ||
!this.change.revisions ||
!this.patchNum ||
!this.changeNum
)
return;
// target can be either gr-editable-label or gr-linked-chip
const target = (dom(e) as EventApi).rootTarget as HTMLElement & {
disabled: boolean;
};
if (target) {
target.disabled = true;
}
const rev = getRevisionByPatchNum(
Object.values(this.change.revisions),
this.patchNum
)!;
const sha = this._getPatchsetHash(this.change.revisions, rev);
return this.$.restAPI
.setDescription(this.changeNum, this.patchNum, desc)
.then((res: Response) => {
if (res.ok) {
if (target) {
target.disabled = false;
}
this.set(['change', 'revisions', sha, 'description'], desc);
this._patchsetDescription = desc;
}
})
.catch(() => {
if (target) {
target.disabled = false;
}
return;
});
}
_computePrefsButtonHidden(
prefs: DiffPreferencesInfo,
diffPrefsDisabled: boolean
) {
return diffPrefsDisabled || !prefs;
}
_fileListActionsVisible(
shownFileCount: number,
maxFilesForBulkActions: number
) {
return shownFileCount <= maxFilesForBulkActions;
}
_handlePatchChange(e: CustomEvent) {
const {basePatchNum, patchNum} = e.detail;
if (
(patchNumEquals(basePatchNum, this.basePatchNum) &&
patchNumEquals(patchNum, this.patchNum)) ||
!this.change
) {
return;
}
GerritNav.navigateToChange(this.change, patchNum, basePatchNum);
}
_handlePrefsTap(e: Event) {
e.preventDefault();
this.dispatchEvent(
new CustomEvent('open-diff-prefs', {
composed: true,
bubbles: true,
})
);
}
_handleIncludedInTap(e: Event) {
e.preventDefault();
this.dispatchEvent(
new CustomEvent('open-included-in-dialog', {
composed: true,
bubbles: true,
})
);
}
_handleDownloadTap(e: Event) {
e.preventDefault();
e.stopPropagation();
this.dispatchEvent(
new CustomEvent('open-download-dialog', {bubbles: false})
);
}
_computeEditModeClass(editMode?: boolean) {
return editMode ? 'editMode' : '';
}
_computePatchInfoClass(patchNum?: PatchSetNum, allPatchSets?: PatchSet[]) {
const latestNum = computeLatestPatchNum(allPatchSets);
if (patchNumEquals(patchNum, latestNum)) {
return '';
}
return 'patchInfoOldPatchSet';
}
_hideIncludedIn(change?: ChangeInfo) {
return change?.status === MERGED_STATUS ? '' : 'hide';
}
_handleUploadTap(e: Event) {
e.preventDefault();
e.stopPropagation();
this.dispatchEvent(
new CustomEvent('open-upload-help-dialog', {bubbles: false})
);
}
_computeUploadHelpContainerClass(change: ChangeInfo, account: AccountInfo) {
const changeIsMerged = change?.status === MERGED_STATUS;
const ownerId = change?.owner?._account_id || null;
const userId = account && account._account_id;
const userIsOwner = ownerId && userId && ownerId === userId;
const hideContainer = !userIsOwner || changeIsMerged;
return 'uploadContainer desktop' + (hideContainer ? ' hide' : '');
}
}