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>