Add PolyGerrit-based screen for uploading images Change-Id: Ifbabbd8e41a2b465bb4efbb267de2a1686919089
diff --git a/src/main/resources/static/gr-imagare-list-item.html b/src/main/resources/static/gr-imagare-list-item.html new file mode 100644 index 0000000..2f66329 --- /dev/null +++ b/src/main/resources/static/gr-imagare-list-item.html
@@ -0,0 +1,135 @@ +<!-- +@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. +--> + +<dom-module id="gr-imagare-list-item"> + <template> + <style include="shared-styles"></style> + <style include="gr-subpage-styles"></style> + <style> + div.image-panel { + margin: 2em auto; + max-width: 50em; + height: 150px; + } + + div.title { + float: left; + width: 30%; + height: 100%; + } + + div.value { + text-align: center; + float: right; + width: 70%; + height: 100%; + } + + div.imageName { + font-weight: bold; + padding: 1em; + } + + a { + display: block; + } + + .ellipsis { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + float: left; + width: 80%; + } + + img { + width: 100%; + height: auto; + max-height: 100%; + max-width: 80%; + object-fit: contain; + } + + #editButton { + box-shadow: none; + } + </style> + <div class="image-panel"> + <div class="title"> + <img id="thumbnail"> + </div> + <div id="staging" class="value"> + <div class="imageName"> + <iron-input bind-value="{{imageName}}"> + <input id="imageNameInput" value="{{imageName::input}}" type="text" + disabled="[[!_editing]]" placeholder$="[[imageName]]"> + </iron-input> + <gr-button id="editButton" on-click="_handleEditImage" hidden="[[_editing]]"> + Edit + </gr-button> + <gr-button id="saveButton" on-click="_handleSaveName" hidden="[[!_editing]]"> + Save + </gr-button> + <gr-button id="cancelRenameButton" on-click="_handleCancelRenameName" + hidden="[[!_editing]]"> + Cancel + </gr-button> + </div> + <div> + <section> + <gr-button id="uploadButton" + on-click="_handleUploadImage" + disabled="[[_editing]]"> + Upload + </gr-button> + <gr-button id="cleanButton" + on-click="_handleClearImage"> + Clear + </gr-button> + </section> + </div> + </div> + <div id="uploading" class="value"> + <div class="imageName"> + [[imageName]] + </div> + <div> + <a class="ellipsis" href="[[imageUrl]]">[[imageUrl]]</a> + <gr-copy-clipboard has-tooltip button-title="Copy URL to Clipboard" + hide-input text="[[imageUrl]]"> + </gr-copy-clipboard> + </div> + <gr-button id="deleteButton" + on-click="_openDeleteDialog"> + Delete + </gr-button> + <gr-overlay id="deleteOverlay" with-backdrop> + <gr-dialog id="deleteDialog" + class="confirmDialog" + confirm-label="Delete" + confirm-on-enter + on-confirm="_handleDeleteImage"> + <div class="header" slot="header"> + Delete Image + </div> + <div class="main" slot="main"> + Are you sure you want to delete '[[imageName]]'? + </div> + </gr-dialog> + </gr-overlay> + </div> + </div> + </template> + <script src="gr-imagare-list-item.js"></script> +</dom-module>
diff --git a/src/main/resources/static/gr-imagare-list-item.js b/src/main/resources/static/gr-imagare-list-item.js new file mode 100644 index 0000000..5f1cffe --- /dev/null +++ b/src/main/resources/static/gr-imagare-list-item.js
@@ -0,0 +1,107 @@ +// 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. + +(function () { + 'use strict'; + + Polymer({ + is: 'gr-imagare-list-item', + + properties: { + imageUrl: { + type: String, + reflectToAttribute: true, + }, + imageName: { + type: String, + reflectToAttribute: true, + }, + imageData: { + type: String, + reflectToAttribute: true, + }, + uploaded: { + type: Boolean, + observer: '_uploadedChanged', + reflectToAttribute: true, + }, + _originalImageName: String, + _editing: { + type: Boolean, + value: false, + }, + _imageSrc: String, + }, + + attached() { + this._originalImageName = this.imageName; + this._setImage(); + }, + + _handleCancelRenameName() { + this.imageName = this._originalImageName; + this._editing = false; + }, + + _handleClearImage() { + this.fire("clear"); + }, + + _handleDeleteImage() { + this.fire("delete"); + }, + + _handleEditImage() { + this._editing = true; + }, + + _handleSaveName() { + this._editing = false; + + if (this._originalImageName === this.imageName) { + return; + } + + let oldFileType = this._originalImageName.split('.').pop(); + let newFileType = this.imageName.split('.').pop(); + if (oldFileType !== newFileType) { + this.imageName += `.${oldFileType}`; + } + + this.fire("editName", {oldName: this._originalImageName, newName: this.imageName}); + }, + + _handleUploadImage() { + this.fire("upload"); + }, + + _openDeleteDialog() { + this.$.deleteOverlay.open(); + }, + + _setImage() { + if (this.uploaded) { + this.$.thumbnail.setAttribute('src', this.imageUrl); + } else { + this.$.thumbnail.setAttribute('src', this.imageData); + } + }, + + _uploadedChanged(uploaded) { + this.$.uploading.hidden = !uploaded; + this.$.staging.hidden = uploaded; + this._setImage(); + }, + }); +})();
diff --git a/src/main/resources/static/gr-imagare-upload.html b/src/main/resources/static/gr-imagare-upload.html new file mode 100644 index 0000000..814ade2 --- /dev/null +++ b/src/main/resources/static/gr-imagare-upload.html
@@ -0,0 +1,132 @@ +<!-- +@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. +--> + +<link rel="import" + href="./gr-imagare-list-item.html"> + +<dom-module id="gr-imagare-upload"> + <template> + <style include="shared-styles"></style> + <style include="gr-subpage-styles"></style> + <style include="gr-form-styles"></style> + <style> + div.image-upload { + margin: 2em auto; + max-width: 50em; + } + + h1#title { + margin-bottom: 1em; + } + + div#dragDropArea { + padding: 2em 10em; + border: 2px dashed #ccc; + border-radius: 1em; + text-align: center; + } + + div#dragDropArea>p { + font-weight: bold; + text-transform: uppercase; + padding: 0.25em; + } + + div#dragDropArea>p.or { + color: #ccc; + } + + input#imagareImagePathInput { + border: 0; + clip: rect(0, 0, 0, 0); + height: 1px; + overflow: hidden; + padding: 0; + position: absolute !important; + white-space: nowrap; + width: 1px; + } + </style> + <div class="image-upload"> + <main class="gr-form-styles read-only"> + <h1 id="title">Image Upload</h1> + <div id="loading" + class$="[[_computeLoadingClass(_loading)]]"> + Loading... + </div> + <div id="form" + class$="[[_computeLoadingClass(_loading)]]"> + <fieldset> + <h2>Settings</h2> + <section> + The user preferences for the image upload can be changed + <a href="[[_computeSettingsUrl()]]">here</a>. + </section> + <section> + <span class="title">Project</span> + <span class="value"> + <gr-autocomplete id="imagareProjectInput" text="{{_imageProject}}" + value="{{_imageProject}}" query="[[_query]]"> + [[_defaultImageProject]] + </gr-autocomplete> + </span> + </section> + </fieldset> + <fieldset> + <h2>Image Selection</h2> + <section> + <div id="dragDropArea" contenteditable="true" on-paste="_handlePaste" + on-drop="_handleDrop" on-keypress="_handleKeyPress"> + <p>Drag and drop image here</p> + <p class="or">or</p> + <p>paste it here</p> + <p class="or">or</p> + <p> + <iron-input> + <input id="imagareImagePathInput" + type="file" + on-change="_handleImagePathChanged" + slot="input" + multiple> + </iron-input> + <label for="imagareImagePathInput"> + <gr-button> + Browse + </gr-button> + </label> + </p> + </div> + </section> + </fieldset> + <fieldset id="imageListContainer" hidden> + <h2>Images</h2> + <fieldset id="imageList"></fieldset> + <section> + <gr-button id="uploadButton" + on-click="_handleUploadAllImages" + disabled="[[_allUploaded]]"> + Upload All + </gr-button> + <gr-button id="cleanButton" + on-click="_handleClearAllImages"> + Clear List + </gr-button> + </section> + </fieldset> + </div> + </main> + </div> + </template> + <script src="gr-imagare-upload.js"></script> +</dom-module>
diff --git a/src/main/resources/static/gr-imagare-upload.js b/src/main/resources/static/gr-imagare-upload.js new file mode 100644 index 0000000..40150c1 --- /dev/null +++ b/src/main/resources/static/gr-imagare-upload.js
@@ -0,0 +1,374 @@ +// 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. + +(function () { + 'use strict'; + + function preventDefaultFn(event) { + event.preventDefault(); + } + + Polymer({ + is: 'gr-imagare-upload', + + properties: { + _loading: { + type: Boolean, + value: true, + }, + _allUploaded: { + type: Boolean, + value: false, + }, + _imageProject: String, + _defaultImageProject: String, + _images: { + type: Map, + value: () => new Map(), + }, + _stageImages: { + type: Boolean, + value: true, + }, + _undefinedFileCounter: { + type: Number, + value: 0, + }, + _query: { + type: Function, + value() { + return this._queryProjects.bind(this); + }, + }, + }, + + listeners: { + clear: '_handleClearImage', + delete: '_handleDeleteImage', + editName: '_handleEditImageName', + upload: '_handleUploadImage', + }, + + attached() { + this.fire('title-change', { title: 'Image Upload' }); + + window.addEventListener('dragover', preventDefaultFn, false); + window.addEventListener('drop', preventDefaultFn, false); + this.$.dragDropArea.addEventListener('paste', preventDefaultFn, false); + + this._getUserPreferences(); + }, + + detached() { + window.removeEventListener('dragover', preventDefaultFn, false); + window.removeEventListener('drop', preventDefaultFn, false); + this.$.dragDropArea.removeEventListener('paste', preventDefaultFn, false); + }, + + _computeFilenameWithCorrectType(filedata, filename) { + let realFiletype = filedata.slice( + filedata.indexOf('/') + 1, + filedata.indexOf(';')); + + let givenFiletype; + + if (filename.indexOf(".") !== -1) { + givenFiletype = filename.split('.').pop(); + } + + if (!givenFiletype || realFiletype !== givenFiletype) { + filename += `.${realFiletype}`; + } + + return filename; + }, + + _computeLoadingClass(loading) { + return loading ? 'loading' : ''; + }, + + _computeSettingsUrl() { + return `${location.origin}/settings#ImagarePreferences`; + }, + + _computeUploadAllDisabled() { + if (this._images) { + for (let value of this._images.values()) { + if (!value.uploaded) { + this._allUploaded = false; + return; + } + } + } + + this._allUploaded = true; + }, + + _createImageObject(name, data, url, list_entry, uploaded, ref) { + return { + data: data, + list_entry: list_entry, + name: name, + ref: ref, + url: url, + uploaded: uploaded, + } + }, + + _createListEntry(name, data, url) { + let imagePanel = document.createElement('gr-imagare-list-item'); + imagePanel.setAttribute("image-name", name); + + if (data) { + imagePanel.setAttribute("image-data", data); + } + + if (url) { + imagePanel.setAttribute("image-url", url); + imagePanel.uploaded = true; + } else { + imagePanel.uploaded = false; + } + + this.$.imageList.appendChild(imagePanel); + + return imagePanel; + }, + + _deleteImage(image) { + this.plugin.restApi('/projects') + .delete(`/${this._imageProject}/imagare~images/${image.ref}`) + .then(() => { + image.list_entry.remove(); + this._images.delete(image.name); + if (!this.$.imageList.hasChildNodes()) { + this.$.imageListContainer.hidden = true; + } + }).catch(response => { + this.fire('show-error', { message: response }); + }); + }, + + _extractImageRef(url) { + return url.split('/').slice(-2)[0]; + }, + + _getUserPreferences() { + this.plugin.restApi('/accounts/self/') + .get(`imagare~preference`) + .then(config => { + if (!config) { + return; + } + + this._defaultImageProject = config.default_project; + this._imageProject = config.default_project; + this._stageImages = config.stage; + this._loading = false; + }); + }, + + _handleClearAllImages() { + while (this.$.imageList.firstChild) { + this.$.imageList.removeChild(this.$.imageList.firstChild); + } + this.$.imageListContainer.hidden = true; + + this._images.clear() + }, + + _handleClearImage(event) { + event.stopPropagation(); + this._images.delete(event.target.imageName); + event.target.remove(); + if (!this.$.imageList.hasChildNodes()) { + this.$.imageListContainer.hidden = true; + } + }, + + _handleDeleteImage(event) { + event.stopPropagation(); + this._deleteImage(this._images.get(event.target.imageName)); + }, + + _handleDrop(event) { + event.preventDefault(); + event.stopPropagation(); + + for (let file of event.dataTransfer.files) { + if (!file.type.match('image/.*')) { + this.fire('show-error', { message: `No image file: ${file.name}` }); + } + let fr = new FileReader(); + fr.file = file; + fr.onload = fileLoadEvent => this._handleFileLoadEvent( + fr.file.name, fileLoadEvent); + fr.readAsDataURL(file); + } + }, + + _handleEditImageName(event) { + event.stopPropagation(); + let editedImage = this._images.get(event.detail.oldName); + if (this._images.has(event.detail.newName)) { + this.fire('show-error', { message: 'An image with the same name was already staged.' }); + editedImage.list_entry.setAttribute("image-name", event.detail.oldName); + } else { + editedImage.name = event.detail.newName; + this._images.set(editedImage.name, editedImage); + this._images.delete(event.detail.oldName); + } + }, + + _handleFileLoadEvent(filename, event) { + let correctedFilename = this._computeFilenameWithCorrectType( + event.target.result, filename); + if (this._stageImages) { + this._stageImage(correctedFilename, event.target.result); + } else { + let image = this._createImageObject(correctedFilename, event.target.result); + this._images.set(correctedFilename, image); + this._uploadImage(image); + } + }, + + _handleKeyPress(event) { + let ctrlDown = event.ctrlKey || event.metaKey; + if (!ctrlDown) { + event.preventDefault(); + event.stopPropagation(); + } + }, + + _handleImagePathChanged(event) { + for (let file of event.target.files) { + let fr = new FileReader(); + fr.file = file; + fr.onload = fileLoadEvent => this._handleFileLoadEvent( + fr.file.name, fileLoadEvent); + fr.readAsDataURL(file); + } + + event.target.value = ''; + }, + + _handlePaste(event) { + let clipboardData = event.clipboardData || event.originalEvent.clipboardData; + let items = clipboardData.items; + if (JSON.stringify(items)) { + let blob; + for (let item of items) { + if (item.type.indexOf("image") === 0) { + blob = item.getAsFile(); + } + } + if (blob) { + let fr = new FileReader(); + fr.onload = fileLoadEvent => { + let filename = `undefined-${this._undefinedFileCounter}`; + this._undefinedFileCounter++; + this._handleFileLoadEvent(filename, fileLoadEvent); + }; + fr.readAsDataURL(blob); + } else { + event.preventDefault(); + this.fire('show-error', { message: `No image file` }); + } + } + }, + + _handleUploadAllImages() { + for (let image of this._images.values()) { + this._uploadImage(image); + } + }, + + _handleUploadImage(event) { + event.stopPropagation(); + let image = this._createImageObject( + event.target.imageName, + event.target.imageData, + null, + event.target); + this._images.set(image.name, image); + this._uploadImage(image); + }, + + _queryProjects(input) { + let query; + if (!input || input === this._defaultImageProject) { + query = ''; + } else { + query = `?prefix=${input}`; + } + + return this.plugin.restApi('/a/projects/').get(query) + .then(response => { + const projects = []; + for (const key in response) { + if (!response.hasOwnProperty(key)) { continue; } + projects.push({ + name: key, + value: decodeURIComponent(response[key].id), + }); + } + return projects; + }); + }, + + _stageImage(name, data) { + if (this._images.has(name)) { + let fileName = name.slice(0, name.lastIndexOf('.')); + let fileExtension = name.slice(name.lastIndexOf('.')); + name = `${fileName}-${this._undefinedFileCounter}${fileExtension}`; + this._undefinedFileCounter++; + } + let imagePanel = this._createListEntry(name, data, null); + this._images.set(name, this._createImageObject(name, data, null, imagePanel)); + this.$.imageListContainer.hidden = false; + this._computeUploadAllDisabled(); + }, + + _uploadImage(image) { + if (image && image.uploaded) { + return; + } + + this.plugin.restApi('/projects') + .post(`/${this._imageProject}/imagare~images`, { + image_data: image.data, + file_name: image.name, + }) + .then(response => { + if (!image.list_entry) { + image.list_entry = this._createListEntry(image.name, image.data, response.url); + } else { + image.list_entry.setAttribute("image-url", response.url); + image.list_entry.uploaded = true; + } + + this._images.set( + image.name, + this._createImageObject( + image.name, image.data, response.url, image.list_entry, true, + this._extractImageRef(response.url))); + + this.$.imageListContainer.hidden = false; + this._computeUploadAllDisabled(); + }).catch(response => { + this.fire('show-error', { message: response }); + }); + } + }); +})();
diff --git a/src/main/resources/static/imagare.html b/src/main/resources/static/imagare.html index 42ea7ae..a06d629 100644 --- a/src/main/resources/static/imagare.html +++ b/src/main/resources/static/imagare.html
@@ -16,12 +16,18 @@ --> <link rel="import" href="./gr-imagare-inline.html"> +<link rel="import" href="./gr-imagare-upload.html"> <dom-module id="imagare"> <script> Gerrit.install(plugin => { if (!window.Polymer) { return; } + plugin.restApi('/config/server/').get('imagare~config').then(config => { + if (config && config.enable_image_server) { + plugin.screen('upload', 'gr-imagare-upload'); + } + }); plugin.registerCustomComponent('change-view-integration', 'gr-imagare-inline'); }); </script>