| /** |
| * @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 '../../../scripts/bundled-polymer.js'; |
| |
| import '@polymer/iron-input/iron-input.js'; |
| import '../../shared/gr-autocomplete/gr-autocomplete.js'; |
| import '../../shared/gr-button/gr-button.js'; |
| import '../../shared/gr-dialog/gr-dialog.js'; |
| import '../../shared/gr-dropdown/gr-dropdown.js'; |
| import '../../shared/gr-overlay/gr-overlay.js'; |
| import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js'; |
| import '../../../styles/shared-styles.js'; |
| import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js'; |
| import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js'; |
| import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js'; |
| import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js'; |
| import {PolymerElement} from '@polymer/polymer/polymer-element.js'; |
| import {htmlTemplate} from './gr-edit-controls_html.js'; |
| import {PatchSetBehavior} from '../../../behaviors/gr-patch-set-behavior/gr-patch-set-behavior.js'; |
| import {GrEditConstants} from '../gr-edit-constants.js'; |
| import {GerritNav} from '../../core/gr-navigation/gr-navigation.js'; |
| |
| /** |
| * @extends Polymer.Element |
| */ |
| class GrEditControls extends mixinBehaviors( [ |
| PatchSetBehavior, |
| ], GestureEventListeners( |
| LegacyElementMixin( |
| PolymerElement))) { |
| static get template() { return htmlTemplate; } |
| |
| static get is() { return 'gr-edit-controls'; } |
| |
| static get properties() { |
| return { |
| change: Object, |
| patchNum: String, |
| |
| /** |
| * TODO(kaspern): by default, the RESTORE action should be hidden in the |
| * file-list as it is a per-file action only. Remove this default value |
| * when the Actions dictionary is moved to a shared constants file and |
| * use the hiddenActions property in the parent component. |
| */ |
| hiddenActions: { |
| type: Array, |
| value() { return [GrEditConstants.Actions.RESTORE.id]; }, |
| }, |
| |
| _actions: { |
| type: Array, |
| value() { return Object.values(GrEditConstants.Actions); }, |
| }, |
| _path: { |
| type: String, |
| value: '', |
| }, |
| _newPath: { |
| type: String, |
| value: '', |
| }, |
| _query: { |
| type: Function, |
| value() { |
| return this._queryFiles.bind(this); |
| }, |
| }, |
| }; |
| } |
| |
| _handleTap(e) { |
| e.preventDefault(); |
| const action = dom(e).localTarget.id; |
| switch (action) { |
| case GrEditConstants.Actions.OPEN.id: |
| this.openOpenDialog(); |
| return; |
| case GrEditConstants.Actions.DELETE.id: |
| this.openDeleteDialog(); |
| return; |
| case GrEditConstants.Actions.RENAME.id: |
| this.openRenameDialog(); |
| return; |
| case GrEditConstants.Actions.RESTORE.id: |
| this.openRestoreDialog(); |
| return; |
| } |
| } |
| |
| /** |
| * @param {string=} opt_path |
| */ |
| openOpenDialog(opt_path) { |
| if (opt_path) { this._path = opt_path; } |
| return this._showDialog(this.$.openDialog); |
| } |
| |
| /** |
| * @param {string=} opt_path |
| */ |
| openDeleteDialog(opt_path) { |
| if (opt_path) { this._path = opt_path; } |
| return this._showDialog(this.$.deleteDialog); |
| } |
| |
| /** |
| * @param {string=} opt_path |
| */ |
| openRenameDialog(opt_path) { |
| if (opt_path) { this._path = opt_path; } |
| return this._showDialog(this.$.renameDialog); |
| } |
| |
| /** |
| * @param {string=} opt_path |
| */ |
| openRestoreDialog(opt_path) { |
| if (opt_path) { this._path = opt_path; } |
| return this._showDialog(this.$.restoreDialog); |
| } |
| |
| /** |
| * Given a path string, checks that it is a valid file path. |
| * |
| * @param {string} path |
| * @return {boolean} |
| */ |
| _isValidPath(path) { |
| // Double negation needed for strict boolean return type. |
| return !!path.length && !path.endsWith('/'); |
| } |
| |
| _computeRenameDisabled(path, newPath) { |
| return this._isValidPath(path) && this._isValidPath(newPath); |
| } |
| |
| /** |
| * Given a dom event, gets the dialog that lies along this event path. |
| * |
| * @param {!Event} e |
| * @return {!Element|undefined} |
| */ |
| _getDialogFromEvent(e) { |
| return dom(e).path.find(element => { |
| if (!element.classList) { return false; } |
| return element.classList.contains('dialog'); |
| }); |
| } |
| |
| _showDialog(dialog) { |
| // Some dialogs may not fire their on-close event when closed in certain |
| // ways (e.g. by clicking outside the dialog body). This call prevents |
| // multiple dialogs from being shown in the same overlay. |
| this._hideAllDialogs(); |
| |
| return this.$.overlay.open().then(() => { |
| dialog.classList.toggle('invisible', false); |
| const autocomplete = dialog.querySelector('gr-autocomplete'); |
| if (autocomplete) { autocomplete.focus(); } |
| this.async(() => { this.$.overlay.center(); }, 1); |
| }); |
| } |
| |
| _hideAllDialogs() { |
| const dialogs = dom(this.root).querySelectorAll('.dialog'); |
| for (const dialog of dialogs) { this._closeDialog(dialog); } |
| } |
| |
| /** |
| * @param {Element|undefined} dialog |
| * @param {boolean=} clearInputs |
| */ |
| _closeDialog(dialog, clearInputs) { |
| if (!dialog) { return; } |
| |
| if (clearInputs) { |
| // Dialog may have autocompletes and plain inputs -- as these have |
| // different properties representing their bound text, it is easier to |
| // just make two separate queries. |
| dialog.querySelectorAll('gr-autocomplete') |
| .forEach(input => { input.text = ''; }); |
| |
| dialog.querySelectorAll('iron-input') |
| .forEach(input => { input.bindValue = ''; }); |
| } |
| |
| dialog.classList.toggle('invisible', true); |
| return this.$.overlay.close(); |
| } |
| |
| _handleDialogCancel(e) { |
| this._closeDialog(this._getDialogFromEvent(e)); |
| } |
| |
| _handleOpenConfirm(e) { |
| const url = GerritNav.getEditUrlForDiff(this.change, this._path, |
| this.patchNum); |
| GerritNav.navigateToRelativeUrl(url); |
| this._closeDialog(this._getDialogFromEvent(e), true); |
| } |
| |
| _handleUploadConfirm(path, fileData) { |
| if (!this.change || !path || !fileData) { |
| this._closeDialog(this.$.openDialog, true); |
| return; |
| } |
| return this.$.restAPI.saveFileUploadChangeEdit(this.change._number, path, |
| fileData).then(res => { |
| if (!res.ok) { return; } |
| this._closeDialog(this.$.openDialog, true); |
| GerritNav.navigateToChange(this.change); |
| }); |
| } |
| |
| _handleDeleteConfirm(e) { |
| // Get the dialog before the api call as the event will change during bubbling |
| // which will make Polymer.dom(e).path an emtpy array in polymer 2 |
| const dialog = this._getDialogFromEvent(e); |
| this.$.restAPI.deleteFileInChangeEdit(this.change._number, this._path) |
| .then(res => { |
| if (!res.ok) { return; } |
| this._closeDialog(dialog, true); |
| GerritNav.navigateToChange(this.change); |
| }); |
| } |
| |
| _handleRestoreConfirm(e) { |
| const dialog = this._getDialogFromEvent(e); |
| this.$.restAPI.restoreFileInChangeEdit(this.change._number, this._path) |
| .then(res => { |
| if (!res.ok) { return; } |
| this._closeDialog(dialog, true); |
| GerritNav.navigateToChange(this.change); |
| }); |
| } |
| |
| _handleRenameConfirm(e) { |
| const dialog = this._getDialogFromEvent(e); |
| return this.$.restAPI.renameFileInChangeEdit(this.change._number, |
| this._path, this._newPath).then(res => { |
| if (!res.ok) { return; } |
| this._closeDialog(dialog, true); |
| GerritNav.navigateToChange(this.change); |
| }); |
| } |
| |
| _queryFiles(input) { |
| return this.$.restAPI.queryChangeFiles(this.change._number, |
| this.patchNum, input).then(res => res.map(file => { |
| return {name: file}; |
| })); |
| } |
| |
| _computeIsInvisible(id, hiddenActions) { |
| return hiddenActions.includes(id) ? 'invisible' : ''; |
| } |
| |
| _handleDragAndDropUpload(event) { |
| // We prevent the default clicking. |
| event.preventDefault(); |
| event.stopPropagation(); |
| |
| this._fileUpload(event); |
| } |
| |
| _handleFileUploadChanged(event) { |
| this._fileUpload(event); |
| } |
| |
| _fileUpload(event) { |
| const e = event.target.files || event.dataTransfer.files; |
| for (const file of e) { |
| if (!file) continue; |
| |
| let path = this._path; |
| if (!path) { |
| path = file.name; |
| } |
| |
| const fr = new FileReader(); |
| fr.file = file; |
| fr.onload = fileLoadEvent => { |
| if (!fileLoadEvent) return; |
| const fileData = fileLoadEvent.target.result; |
| this._handleUploadConfirm(path, fileData); |
| }; |
| fr.readAsDataURL(file); |
| } |
| } |
| |
| _handleKeyPress(event) { |
| event.preventDefault(); |
| event.stopImmediatePropagation(); |
| } |
| } |
| |
| customElements.define(GrEditControls.is, GrEditControls); |