|  | /** | 
|  | * @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. | 
|  | */ | 
|  | (function() { | 
|  | 'use strict'; | 
|  |  | 
|  | const Defs = {}; | 
|  |  | 
|  | const NOTHING_TO_SAVE = 'No changes to save.'; | 
|  |  | 
|  | const MAX_AUTOCOMPLETE_RESULTS = 50; | 
|  |  | 
|  | /** | 
|  | * Fired when save is a no-op | 
|  | * | 
|  | * @event show-alert | 
|  | */ | 
|  |  | 
|  | /** | 
|  | * @typedef {{ | 
|  | *    value: !Object, | 
|  | * }} | 
|  | */ | 
|  | Defs.rule; | 
|  |  | 
|  | /** | 
|  | * @typedef {{ | 
|  | *    rules: !Object<string, Defs.rule> | 
|  | * }} | 
|  | */ | 
|  | Defs.permission; | 
|  |  | 
|  | /** | 
|  | * Can be an empty object or consist of permissions. | 
|  | * | 
|  | * @typedef {{ | 
|  | *    permissions: !Object<string, Defs.permission> | 
|  | * }} | 
|  | */ | 
|  | Defs.permissions; | 
|  |  | 
|  | /** | 
|  | * Can be an empty object or consist of permissions. | 
|  | * | 
|  | * @typedef {!Object<string, Defs.permissions>} | 
|  | */ | 
|  | Defs.sections; | 
|  |  | 
|  | /** | 
|  | * @typedef {{ | 
|  | *    remove: !Defs.sections, | 
|  | *    add: !Defs.sections, | 
|  | * }} | 
|  | */ | 
|  | Defs.projectAccessInput; | 
|  |  | 
|  |  | 
|  | Polymer({ | 
|  | is: 'gr-repo-access', | 
|  | _legacyUndefinedCheck: true, | 
|  |  | 
|  | properties: { | 
|  | repo: { | 
|  | type: String, | 
|  | observer: '_repoChanged', | 
|  | }, | 
|  | // The current path | 
|  | path: String, | 
|  |  | 
|  | _canUpload: { | 
|  | type: Boolean, | 
|  | value: false, | 
|  | }, | 
|  | _inheritFromFilter: String, | 
|  | _query: { | 
|  | type: Function, | 
|  | value() { | 
|  | return this._getInheritFromSuggestions.bind(this); | 
|  | }, | 
|  | }, | 
|  | _ownerOf: Array, | 
|  | _capabilities: Object, | 
|  | _groups: Object, | 
|  | /** @type {?} */ | 
|  | _inheritsFrom: Object, | 
|  | _labels: Object, | 
|  | _local: Object, | 
|  | _editing: { | 
|  | type: Boolean, | 
|  | value: false, | 
|  | observer: '_handleEditingChanged', | 
|  | }, | 
|  | _modified: { | 
|  | type: Boolean, | 
|  | value: false, | 
|  | }, | 
|  | _sections: Array, | 
|  | _weblinks: Array, | 
|  | _loading: { | 
|  | type: Boolean, | 
|  | value: true, | 
|  | }, | 
|  | }, | 
|  |  | 
|  | behaviors: [ | 
|  | Gerrit.AccessBehavior, | 
|  | Gerrit.BaseUrlBehavior, | 
|  | Gerrit.URLEncodingBehavior, | 
|  | ], | 
|  |  | 
|  | listeners: { | 
|  | 'access-modified': '_handleAccessModified', | 
|  | }, | 
|  |  | 
|  | _handleAccessModified() { | 
|  | this._modified = true; | 
|  | }, | 
|  |  | 
|  | /** | 
|  | * @param {string} repo | 
|  | * @return {!Promise} | 
|  | */ | 
|  | _repoChanged(repo) { | 
|  | this._loading = true; | 
|  |  | 
|  | if (!repo) { return Promise.resolve(); } | 
|  |  | 
|  | return this._reload(repo); | 
|  | }, | 
|  |  | 
|  | _reload(repo) { | 
|  | const promises = []; | 
|  |  | 
|  | const errFn = response => { | 
|  | this.fire('page-error', {response}); | 
|  | }; | 
|  |  | 
|  | this._editing = false; | 
|  |  | 
|  | // Always reset sections when a project changes. | 
|  | this._sections = []; | 
|  | promises.push(this.$.restAPI.getRepoAccessRights(repo, errFn) | 
|  | .then(res => { | 
|  | if (!res) { return Promise.resolve(); } | 
|  |  | 
|  | // Keep a copy of the original inherit from values separate from | 
|  | // the ones data bound to gr-autocomplete, so the original value | 
|  | // can be restored if the user cancels. | 
|  | this._inheritsFrom = res.inherits_from ? Object.assign({}, | 
|  | res.inherits_from) : null; | 
|  | this._originalInheritsFrom = res.inherits_from ? Object.assign({}, | 
|  | res.inherits_from) : null; | 
|  | // Initialize the filter value so when the user clicks edit, the | 
|  | // current value appears. If there is no parent repo, it is | 
|  | // initialized as an empty string. | 
|  | this._inheritFromFilter = res.inherits_from ? | 
|  | this._inheritsFrom.name : ''; | 
|  | this._local = res.local; | 
|  | this._groups = res.groups; | 
|  | this._weblinks = res.config_web_links || []; | 
|  | this._canUpload = res.can_upload; | 
|  | this._ownerOf = res.owner_of || []; | 
|  | return this.toSortedArray(this._local); | 
|  | })); | 
|  |  | 
|  | promises.push(this.$.restAPI.getCapabilities(errFn) | 
|  | .then(res => { | 
|  | if (!res) { return Promise.resolve(); } | 
|  |  | 
|  | return res; | 
|  | })); | 
|  |  | 
|  | promises.push(this.$.restAPI.getRepo(repo, errFn) | 
|  | .then(res => { | 
|  | if (!res) { return Promise.resolve(); } | 
|  |  | 
|  | return res.labels; | 
|  | })); | 
|  |  | 
|  | return Promise.all(promises).then(([sections, capabilities, labels]) => { | 
|  | this._capabilities = capabilities; | 
|  | this._labels = labels; | 
|  | this._sections = sections; | 
|  | this._loading = false; | 
|  | }); | 
|  | }, | 
|  |  | 
|  | _handleUpdateInheritFrom(e) { | 
|  | if (!this._inheritsFrom) { | 
|  | this._inheritsFrom = {}; | 
|  | } | 
|  | this._inheritsFrom.id = e.detail.value; | 
|  | this._inheritsFrom.name = this._inheritFromFilter; | 
|  | this._handleAccessModified(); | 
|  | }, | 
|  |  | 
|  | _getInheritFromSuggestions() { | 
|  | return this.$.restAPI.getRepos( | 
|  | this._inheritFromFilter, | 
|  | MAX_AUTOCOMPLETE_RESULTS) | 
|  | .then(response => { | 
|  | const projects = []; | 
|  | for (const key in response) { | 
|  | if (!response.hasOwnProperty(key)) { continue; } | 
|  | projects.push({ | 
|  | name: response[key].name, | 
|  | value: response[key].id, | 
|  | }); | 
|  | } | 
|  | return projects; | 
|  | }); | 
|  | }, | 
|  |  | 
|  | _computeLoadingClass(loading) { | 
|  | return loading ? 'loading' : ''; | 
|  | }, | 
|  |  | 
|  | _handleEdit() { | 
|  | this._editing = !this._editing; | 
|  | }, | 
|  |  | 
|  | _editOrCancel(editing) { | 
|  | return editing ? 'Cancel' : 'Edit'; | 
|  | }, | 
|  |  | 
|  | _computeWebLinkClass(weblinks) { | 
|  | return weblinks.length ? 'show' : ''; | 
|  | }, | 
|  |  | 
|  | _computeShowInherit(inheritsFrom) { | 
|  | return inheritsFrom ? 'show' : ''; | 
|  | }, | 
|  |  | 
|  | _handleAddedSectionRemoved(e) { | 
|  | const index = e.model.index; | 
|  | this._sections = this._sections.slice(0, index) | 
|  | .concat(this._sections.slice(index + 1, this._sections.length)); | 
|  | }, | 
|  |  | 
|  | _handleEditingChanged(editing, editingOld) { | 
|  | // Ignore when editing gets set initially. | 
|  | if (!editingOld || editing) { return; } | 
|  | // Remove any unsaved but added refs. | 
|  | if (this._sections) { | 
|  | this._sections = this._sections.filter(p => !p.value.added); | 
|  | } | 
|  | // Restore inheritFrom. | 
|  | if (this._inheritsFrom) { | 
|  | this._inheritsFrom = Object.assign({}, this._originalInheritsFrom); | 
|  | this._inheritFromFilter = this._inheritsFrom.name; | 
|  | } | 
|  | for (const key of Object.keys(this._local)) { | 
|  | if (this._local[key].added) { | 
|  | delete this._local[key]; | 
|  | } | 
|  | } | 
|  | }, | 
|  |  | 
|  | /** | 
|  | * @param {!Defs.projectAccessInput} addRemoveObj | 
|  | * @param {!Array} path | 
|  | * @param {string} type add or remove | 
|  | * @param {!Object=} opt_value value to add if the type is 'add' | 
|  | * @return {!Defs.projectAccessInput} | 
|  | */ | 
|  | _updateAddRemoveObj(addRemoveObj, path, type, opt_value) { | 
|  | let curPos = addRemoveObj[type]; | 
|  | for (const item of path) { | 
|  | if (!curPos[item]) { | 
|  | if (item === path[path.length - 1] && type === 'remove') { | 
|  | if (path[path.length - 2] === 'permissions') { | 
|  | curPos[item] = {rules: {}}; | 
|  | } else if (path.length === 1) { | 
|  | curPos[item] = {permissions: {}}; | 
|  | } else { | 
|  | curPos[item] = {}; | 
|  | } | 
|  | } else if (item === path[path.length - 1] && type === 'add') { | 
|  | curPos[item] = opt_value; | 
|  | } else { | 
|  | curPos[item] = {}; | 
|  | } | 
|  | } | 
|  | curPos = curPos[item]; | 
|  | } | 
|  | return addRemoveObj; | 
|  | }, | 
|  |  | 
|  | /** | 
|  | * Used to recursively remove any objects with a 'deleted' bit. | 
|  | */ | 
|  | _recursivelyRemoveDeleted(obj) { | 
|  | for (const k in obj) { | 
|  | if (!obj.hasOwnProperty(k)) { continue; } | 
|  |  | 
|  | if (typeof obj[k] == 'object') { | 
|  | if (obj[k].deleted) { | 
|  | delete obj[k]; | 
|  | return; | 
|  | } | 
|  | this._recursivelyRemoveDeleted(obj[k]); | 
|  | } | 
|  | } | 
|  | }, | 
|  |  | 
|  | _recursivelyUpdateAddRemoveObj(obj, addRemoveObj, path = []) { | 
|  | for (const k in obj) { | 
|  | if (!obj.hasOwnProperty(k)) { continue; } | 
|  | if (typeof obj[k] == 'object') { | 
|  | const updatedId = obj[k].updatedId; | 
|  | const ref = updatedId ? updatedId : k; | 
|  | if (obj[k].deleted) { | 
|  | this._updateAddRemoveObj(addRemoveObj, | 
|  | path.concat(k), 'remove'); | 
|  | continue; | 
|  | } else if (obj[k].modified) { | 
|  | this._updateAddRemoveObj(addRemoveObj, | 
|  | path.concat(k), 'remove'); | 
|  | this._updateAddRemoveObj(addRemoveObj, path.concat(ref), 'add', | 
|  | obj[k]); | 
|  | /* Special case for ref changes because they need to be added and | 
|  | removed in a different way. The new ref needs to include all | 
|  | changes but also the initial state. To do this, instead of | 
|  | continuing with the same recursion, just remove anything that is | 
|  | deleted in the current state. */ | 
|  | if (updatedId && updatedId !== k) { | 
|  | this._recursivelyRemoveDeleted(addRemoveObj.add[updatedId]); | 
|  | } | 
|  | continue; | 
|  | } else if (obj[k].added) { | 
|  | this._updateAddRemoveObj(addRemoveObj, | 
|  | path.concat(ref), 'add', obj[k]); | 
|  | /** | 
|  | * As add / delete both can happen in the new section, | 
|  | * so here to make sure it will remove the deleted ones. | 
|  | * | 
|  | * @see Issue 11339 | 
|  | */ | 
|  | this._recursivelyRemoveDeleted(addRemoveObj.add[k]); | 
|  | continue; | 
|  | } | 
|  | this._recursivelyUpdateAddRemoveObj(obj[k], addRemoveObj, | 
|  | path.concat(k)); | 
|  | } | 
|  | } | 
|  | }, | 
|  |  | 
|  | /** | 
|  | * Returns an object formatted for saving or submitting access changes for | 
|  | * review | 
|  | * | 
|  | * @return {!Defs.projectAccessInput} | 
|  | */ | 
|  | _computeAddAndRemove() { | 
|  | const addRemoveObj = { | 
|  | add: {}, | 
|  | remove: {}, | 
|  | }; | 
|  |  | 
|  | const originalInheritsFromId = this._originalInheritsFrom ? | 
|  | this.singleDecodeURL(this._originalInheritsFrom.id) : | 
|  | null; | 
|  | const inheritsFromId = this._inheritsFrom ? | 
|  | this.singleDecodeURL(this._inheritsFrom.id) : | 
|  | null; | 
|  |  | 
|  | const inheritFromChanged = | 
|  | // Inherit from changed | 
|  | (originalInheritsFromId | 
|  | && originalInheritsFromId !== inheritsFromId) || | 
|  | // Inherit from added (did not have one initially); | 
|  | (!originalInheritsFromId && inheritsFromId); | 
|  |  | 
|  | this._recursivelyUpdateAddRemoveObj(this._local, addRemoveObj); | 
|  |  | 
|  | if (inheritFromChanged) { | 
|  | addRemoveObj.parent = inheritsFromId; | 
|  | } | 
|  | return addRemoveObj; | 
|  | }, | 
|  |  | 
|  | _handleCreateSection() { | 
|  | let newRef = 'refs/for/*'; | 
|  | // Avoid using an already used key for the placeholder, since it | 
|  | // immediately gets added to an object. | 
|  | while (this._local[newRef]) { | 
|  | newRef = `${newRef}*`; | 
|  | } | 
|  | const section = {permissions: {}, added: true}; | 
|  | this.push('_sections', {id: newRef, value: section}); | 
|  | this.set(['_local', newRef], section); | 
|  | Polymer.dom.flush(); | 
|  | Polymer.dom(this.root).querySelector('gr-access-section:last-of-type') | 
|  | .editReference(); | 
|  | }, | 
|  |  | 
|  | _getObjforSave() { | 
|  | const addRemoveObj = this._computeAddAndRemove(); | 
|  | // If there are no changes, don't actually save. | 
|  | if (!Object.keys(addRemoveObj.add).length && | 
|  | !Object.keys(addRemoveObj.remove).length && | 
|  | !addRemoveObj.parent) { | 
|  | this.dispatchEvent(new CustomEvent('show-alert', | 
|  | {detail: {message: NOTHING_TO_SAVE}, bubbles: true})); | 
|  | return; | 
|  | } | 
|  | const obj = { | 
|  | add: addRemoveObj.add, | 
|  | remove: addRemoveObj.remove, | 
|  | }; | 
|  | if (addRemoveObj.parent) { | 
|  | obj.parent = addRemoveObj.parent; | 
|  | } | 
|  | return obj; | 
|  | }, | 
|  |  | 
|  | _handleSave() { | 
|  | const obj = this._getObjforSave(); | 
|  | if (!obj) { return; } | 
|  | return this.$.restAPI.setRepoAccessRights(this.repo, obj) | 
|  | .then(() => { | 
|  | this._reload(this.repo); | 
|  | }); | 
|  | }, | 
|  |  | 
|  | _handleSaveForReview() { | 
|  | const obj = this._getObjforSave(); | 
|  | if (!obj) { return; } | 
|  | return this.$.restAPI.setRepoAccessRightsForReview(this.repo, obj) | 
|  | .then(change => { | 
|  | Gerrit.Nav.navigateToChange(change); | 
|  | }); | 
|  | }, | 
|  |  | 
|  | _computeSaveReviewBtnClass(canUpload) { | 
|  | return !canUpload ? 'invisible' : ''; | 
|  | }, | 
|  |  | 
|  | _computeSaveBtnClass(ownerOf) { | 
|  | return ownerOf.length < 0 ? 'invisible' : ''; | 
|  | }, | 
|  |  | 
|  | _computeMainClass(ownerOf, canUpload, editing) { | 
|  | const classList = []; | 
|  | if (ownerOf.length > 0 || canUpload) { | 
|  | classList.push('admin'); | 
|  | } | 
|  | if (editing) { | 
|  | classList.push('editing'); | 
|  | } | 
|  | return classList.join(' '); | 
|  | }, | 
|  |  | 
|  | _computeParentHref(repoName) { | 
|  | return this.getBaseUrl() + | 
|  | `/admin/repos/${this.encodeURL(repoName, true)},access`; | 
|  | }, | 
|  | }); | 
|  | })(); |