Merge "Workaround: process unhandled exceptions in tests"
diff --git a/polygerrit-ui/app/elements/admin/gr-repo/gr-repo.js b/polygerrit-ui/app/elements/admin/gr-repo/gr-repo.js
deleted file mode 100644
index 9cbfda0..0000000
--- a/polygerrit-ui/app/elements/admin/gr-repo/gr-repo.js
+++ /dev/null
@@ -1,382 +0,0 @@
-/**
- * @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 '@polymer/iron-autogrow-textarea/iron-autogrow-textarea.js';
-import '@polymer/iron-input/iron-input.js';
-import '../../plugins/gr-endpoint-decorator/gr-endpoint-decorator.js';
-import '../../plugins/gr-endpoint-param/gr-endpoint-param.js';
-import '../../shared/gr-download-commands/gr-download-commands.js';
-import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js';
-import '../../shared/gr-select/gr-select.js';
-import '../../../styles/gr-form-styles.js';
-import '../../../styles/gr-subpage-styles.js';
-import '../../../styles/shared-styles.js';
-import '../gr-repo-plugin-config/gr-repo-plugin-config.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-repo_html.js';
-import {GerritNav} from '../../core/gr-navigation/gr-navigation.js';
-
-const STATES = {
- active: {value: 'ACTIVE', label: 'Active'},
- readOnly: {value: 'READ_ONLY', label: 'Read Only'},
- hidden: {value: 'HIDDEN', label: 'Hidden'},
-};
-
-const SUBMIT_TYPES = {
- // Exclude INHERIT, which is handled specially.
- mergeIfNecessary: {
- value: 'MERGE_IF_NECESSARY',
- label: 'Merge if necessary',
- },
- fastForwardOnly: {
- value: 'FAST_FORWARD_ONLY',
- label: 'Fast forward only',
- },
- rebaseAlways: {
- value: 'REBASE_ALWAYS',
- label: 'Rebase Always',
- },
- rebaseIfNecessary: {
- value: 'REBASE_IF_NECESSARY',
- label: 'Rebase if necessary',
- },
- mergeAlways: {
- value: 'MERGE_ALWAYS',
- label: 'Merge always',
- },
- cherryPick: {
- value: 'CHERRY_PICK',
- label: 'Cherry pick',
- },
-};
-
-/**
- * @extends PolymerElement
- */
-class GrRepo extends GestureEventListeners(
- LegacyElementMixin(PolymerElement)) {
- // Notes for future TS conversion:
- // _repoConfig: ConfigInfo
- // _pluginData: PluginData[], can't be null, PluginData from gr-repo-plugin-config.ts
-
- static get template() { return htmlTemplate; }
-
- static get is() { return 'gr-repo'; }
-
- static get properties() {
- return {
- params: Object,
- repo: String,
-
- _configChanged: {
- type: Boolean,
- value: false,
- },
- _loading: {
- type: Boolean,
- value: true,
- },
- _loggedIn: {
- type: Boolean,
- value: false,
- observer: '_loggedInChanged',
- },
- /** @type {?} */
- _repoConfig: Object,
- /** @type {?} */
- _pluginData: {
- type: Array,
- computed: '_computePluginData(_repoConfig.plugin_config.*)',
- },
- _readOnly: {
- type: Boolean,
- value: true,
- },
- _states: {
- type: Array,
- value() {
- return Object.values(STATES);
- },
- },
- _submitTypes: {
- type: Array,
- value() {
- return Object.values(SUBMIT_TYPES);
- },
- },
- _schemes: {
- type: Array,
- value() { return []; },
- computed: '_computeSchemes(_schemesObj)',
- observer: '_schemesChanged',
- },
- _selectedCommand: {
- type: String,
- value: 'Clone',
- },
- _selectedScheme: String,
- _schemesObj: Object,
- };
- }
-
- static get observers() {
- return [
- '_handleConfigChanged(_repoConfig.*)',
- ];
- }
-
- /** @override */
- attached() {
- super.attached();
- this._loadRepo();
-
- this.dispatchEvent(new CustomEvent('title-change', {
- detail: {title: this.repo},
- composed: true, bubbles: true,
- }));
- }
-
- _computePluginData(configRecord) {
- if (!configRecord ||
- !configRecord.base) { return []; }
-
- const pluginConfig = configRecord.base;
- return Object.keys(pluginConfig)
- .map(name => { return {name, config: pluginConfig[name]}; });
- }
-
- _loadRepo() {
- if (!this.repo) { return Promise.resolve(); }
-
- const promises = [];
-
- const errFn = response => {
- this.dispatchEvent(new CustomEvent('page-error', {
- detail: {response},
- composed: true, bubbles: true,
- }));
- };
-
- promises.push(this._getLoggedIn().then(loggedIn => {
- this._loggedIn = loggedIn;
- if (loggedIn) {
- this.$.restAPI.getRepoAccess(this.repo).then(access => {
- if (!access) { return Promise.resolve(); }
-
- // If the user is not an owner, is_owner is not a property.
- this._readOnly = !access[this.repo].is_owner;
- });
- }
- }));
-
- promises.push(this.$.restAPI.getProjectConfig(this.repo, errFn)
- .then(config => {
- if (!config) { return Promise.resolve(); }
-
- if (config.default_submit_type) {
- // The gr-select is bound to submit_type, which needs to be the
- // *configured* submit type. When default_submit_type is
- // present, the server reports the *effective* submit type in
- // submit_type, so we need to overwrite it before storing the
- // config in this.
- config.submit_type =
- config.default_submit_type.configured_value;
- }
- if (!config.state) {
- config.state = STATES.active.value;
- }
- this._repoConfig = config;
- this._loading = false;
- }));
-
- promises.push(this.$.restAPI.getConfig().then(config => {
- if (!config) { return Promise.resolve(); }
-
- this._schemesObj = config.download.schemes;
- }));
-
- return Promise.all(promises);
- }
-
- _computeLoadingClass(loading) {
- return loading ? 'loading' : '';
- }
-
- _computeHideClass(arr) {
- return !arr || !arr.length ? 'hide' : '';
- }
-
- _loggedInChanged(_loggedIn) {
- if (!_loggedIn) { return; }
- this.$.restAPI.getPreferences().then(prefs => {
- if (prefs.download_scheme) {
- // Note (issue 5180): normalize the download scheme with lower-case.
- this._selectedScheme = prefs.download_scheme.toLowerCase();
- }
- });
- }
-
- _formatBooleanSelect(item) {
- if (!item) { return; }
- let inheritLabel = 'Inherit';
- if (!(item.inherited_value === undefined)) {
- inheritLabel = `Inherit (${item.inherited_value})`;
- }
- return [
- {
- label: inheritLabel,
- value: 'INHERIT',
- },
- {
- label: 'True',
- value: 'TRUE',
- }, {
- label: 'False',
- value: 'FALSE',
- },
- ];
- }
-
- _formatSubmitTypeSelect(projectConfig) {
- if (!projectConfig) { return; }
- const allValues = Object.values(SUBMIT_TYPES);
- const type = projectConfig.default_submit_type;
- if (!type) {
- // Server is too old to report default_submit_type, so assume INHERIT
- // is not a valid value.
- return allValues;
- }
-
- let inheritLabel = 'Inherit';
- if (type.inherited_value) {
- let inherited = type.inherited_value;
- for (const val of allValues) {
- if (val.value === type.inherited_value) {
- inherited = val.label;
- break;
- }
- }
- inheritLabel = `Inherit (${inherited})`;
- }
- return [
- {
- label: inheritLabel,
- value: 'INHERIT',
- },
- ...allValues,
- ];
- }
-
- _isLoading() {
- return this._loading || this._loading === undefined;
- }
-
- _getLoggedIn() {
- return this.$.restAPI.getLoggedIn();
- }
-
- _formatRepoConfigForSave(repoConfig) {
- const configInputObj = {};
- for (const key in repoConfig) {
- if (repoConfig.hasOwnProperty(key)) {
- if (key === 'default_submit_type') {
- // default_submit_type is not in the input type, and the
- // configured value was already copied to submit_type by
- // _loadProject. Omit this property when saving.
- continue;
- }
- if (key === 'plugin_config') {
- configInputObj.plugin_config_values = repoConfig[key];
- } else if (typeof repoConfig[key] === 'object') {
- configInputObj[key] = repoConfig[key].configured_value;
- } else {
- configInputObj[key] = repoConfig[key];
- }
- }
- }
- return configInputObj;
- }
-
- _handleSaveRepoConfig() {
- return this.$.restAPI.saveRepoConfig(this.repo,
- this._formatRepoConfigForSave(this._repoConfig)).then(() => {
- this._configChanged = false;
- });
- }
-
- _handleConfigChanged() {
- if (this._isLoading()) { return; }
- this._configChanged = true;
- }
-
- _computeButtonDisabled(readOnly, configChanged) {
- return readOnly || !configChanged;
- }
-
- _computeHeaderClass(configChanged) {
- return configChanged ? 'edited' : '';
- }
-
- _computeSchemes(schemesObj) {
- return Object.keys(schemesObj);
- }
-
- _schemesChanged(schemes) {
- if (schemes.length === 0) { return; }
- if (!schemes.includes(this._selectedScheme)) {
- this._selectedScheme = schemes.sort()[0];
- }
- }
-
- _computeCommands(repo, schemesObj, _selectedScheme) {
- if (!schemesObj || !repo || !_selectedScheme) {
- return [];
- }
- const commands = [];
- let commandObj;
- if (schemesObj.hasOwnProperty(_selectedScheme)) {
- commandObj = schemesObj[_selectedScheme].clone_commands;
- }
- for (const title in commandObj) {
- if (!commandObj.hasOwnProperty(title)) { continue; }
- commands.push({
- title,
- command: commandObj[title]
- .replace(/\${project}/gi, encodeURI(repo))
- .replace(/\${project-base-name}/gi,
- encodeURI(repo.substring(repo.lastIndexOf('/') + 1))),
- });
- }
- return commands;
- }
-
- _computeRepositoriesClass(config) {
- return config ? 'showConfig': '';
- }
-
- _computeChangesUrl(name) {
- return GerritNav.getUrlForProjectChanges(name);
- }
-
- _handlePluginConfigChanged({detail: {name, config, notifyPath}}) {
- this._repoConfig.plugin_config[name] = config;
- this.notifyPath('_repoConfig.plugin_config.' + notifyPath);
- }
-}
-
-customElements.define(GrRepo.is, GrRepo);
diff --git a/polygerrit-ui/app/elements/admin/gr-repo/gr-repo.ts b/polygerrit-ui/app/elements/admin/gr-repo/gr-repo.ts
new file mode 100644
index 0000000..101c77a
--- /dev/null
+++ b/polygerrit-ui/app/elements/admin/gr-repo/gr-repo.ts
@@ -0,0 +1,455 @@
+/**
+ * @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 '@polymer/iron-autogrow-textarea/iron-autogrow-textarea';
+import '@polymer/iron-input/iron-input';
+import '../../plugins/gr-endpoint-decorator/gr-endpoint-decorator';
+import '../../plugins/gr-endpoint-param/gr-endpoint-param';
+import '../../shared/gr-download-commands/gr-download-commands';
+import '../../shared/gr-rest-api-interface/gr-rest-api-interface';
+import '../../shared/gr-select/gr-select';
+import '../../../styles/gr-form-styles';
+import '../../../styles/gr-subpage-styles';
+import '../../../styles/shared-styles';
+import '../gr-repo-plugin-config/gr-repo-plugin-config';
+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-repo_html';
+import {GerritNav} from '../../core/gr-navigation/gr-navigation';
+import {customElement, property, observe} from '@polymer/decorators';
+import {
+ RestApiService,
+ ErrorCallback,
+} from '../../../services/services/gr-rest-api/gr-rest-api';
+import {
+ ConfigInfo,
+ RepoName,
+ InheritedBooleanInfo,
+ SchemesInfoMap,
+ ConfigInput,
+ PluginParameterToConfigParameterInfoMap,
+ PluginNameToPluginParametersMap,
+} from '../../../types/common';
+import {PluginData} from '../gr-repo-plugin-config/gr-repo-plugin-config';
+import {ProjectState} from '../../../constants/constants';
+import {PolymerDeepPropertyChange} from '@polymer/polymer/interfaces';
+import {hasOwnProperty} from '../../../utils/common-util';
+
+const STATES = {
+ active: {value: ProjectState.ACTIVE, label: 'Active'},
+ readOnly: {value: ProjectState.READ_ONLY, label: 'Read Only'},
+ hidden: {value: ProjectState.HIDDEN, label: 'Hidden'},
+};
+
+const SUBMIT_TYPES = {
+ // Exclude INHERIT, which is handled specially.
+ mergeIfNecessary: {
+ value: 'MERGE_IF_NECESSARY',
+ label: 'Merge if necessary',
+ },
+ fastForwardOnly: {
+ value: 'FAST_FORWARD_ONLY',
+ label: 'Fast forward only',
+ },
+ rebaseAlways: {
+ value: 'REBASE_ALWAYS',
+ label: 'Rebase Always',
+ },
+ rebaseIfNecessary: {
+ value: 'REBASE_IF_NECESSARY',
+ label: 'Rebase if necessary',
+ },
+ mergeAlways: {
+ value: 'MERGE_ALWAYS',
+ label: 'Merge always',
+ },
+ cherryPick: {
+ value: 'CHERRY_PICK',
+ label: 'Cherry pick',
+ },
+};
+
+export interface GrRepo {
+ $: {
+ restAPI: RestApiService & Element;
+ };
+}
+@customElement('gr-repo')
+export class GrRepo extends GestureEventListeners(
+ LegacyElementMixin(PolymerElement)
+) {
+ static get template() {
+ return htmlTemplate;
+ }
+
+ @property({type: String})
+ repo?: RepoName;
+
+ @property({type: Boolean})
+ _configChanged = false;
+
+ @property({type: Boolean})
+ _loading = true;
+
+ @property({type: Boolean, observer: '_loggedInChanged'})
+ _loggedIn = false;
+
+ @property({type: Object})
+ _repoConfig?: ConfigInfo;
+
+ @property({
+ type: Array,
+ computed: '_computePluginData(_repoConfig.plugin_config.*)',
+ })
+ _pluginData?: PluginData[];
+
+ @property({type: Boolean})
+ _readOnly = true;
+
+ @property({type: Array})
+ _states = Object.values(STATES);
+
+ @property({
+ type: Array,
+ computed: '_computeSchemes(_schemesDefault, _schemesObj)',
+ observer: '_schemesChanged',
+ })
+ _schemes: string[] = [];
+
+ // This is workaround to have _schemes with default value [],
+ // because assignment doesn't work when property has a computed attribute.
+ @property({type: Array})
+ _schemesDefault: string[] = [];
+
+ @property({type: String})
+ _selectedCommand = 'Clone';
+
+ @property({type: String})
+ _selectedScheme?: string;
+
+ @property({type: Object})
+ _schemesObj?: SchemesInfoMap;
+
+ /** @override */
+ attached() {
+ super.attached();
+ this._loadRepo();
+
+ this.dispatchEvent(
+ new CustomEvent('title-change', {
+ detail: {title: this.repo},
+ composed: true,
+ bubbles: true,
+ })
+ );
+ }
+
+ _computePluginData(
+ configRecord: PolymerDeepPropertyChange<
+ PluginNameToPluginParametersMap,
+ PluginNameToPluginParametersMap
+ >
+ ) {
+ if (!configRecord || !configRecord.base) {
+ return [];
+ }
+
+ const pluginConfig = configRecord.base;
+ return Object.keys(pluginConfig).map(name => {
+ return {name, config: pluginConfig[name]};
+ });
+ }
+
+ _loadRepo() {
+ if (!this.repo) {
+ return Promise.resolve();
+ }
+
+ const promises = [];
+
+ const errFn: ErrorCallback = response => {
+ this.dispatchEvent(
+ new CustomEvent('page-error', {
+ detail: {response},
+ composed: true,
+ bubbles: true,
+ })
+ );
+ };
+
+ promises.push(
+ this._getLoggedIn().then(loggedIn => {
+ this._loggedIn = loggedIn;
+ if (loggedIn) {
+ const repo = this.repo;
+ if (!repo) throw new Error('undefined repo');
+ this.$.restAPI.getRepoAccess(repo).then(access => {
+ if (!access || this.repo !== repo) {
+ return;
+ }
+
+ // If the user is not an owner, is_owner is not a property.
+ this._readOnly = !access[repo].is_owner;
+ });
+ }
+ })
+ );
+
+ promises.push(
+ this.$.restAPI.getProjectConfig(this.repo, errFn).then(config => {
+ if (!config) {
+ return;
+ }
+
+ if (config.default_submit_type) {
+ // The gr-select is bound to submit_type, which needs to be the
+ // *configured* submit type. When default_submit_type is
+ // present, the server reports the *effective* submit type in
+ // submit_type, so we need to overwrite it before storing the
+ // config in this.
+ config.submit_type = config.default_submit_type.configured_value;
+ }
+ if (!config.state) {
+ config.state = STATES.active.value;
+ }
+ this._repoConfig = config;
+ this._loading = false;
+ })
+ );
+
+ promises.push(
+ this.$.restAPI.getConfig().then(config => {
+ if (!config) {
+ return;
+ }
+
+ this._schemesObj = config.download.schemes;
+ })
+ );
+
+ return Promise.all(promises);
+ }
+
+ _computeLoadingClass(loading: boolean) {
+ return loading ? 'loading' : '';
+ }
+
+ _computeHideClass(arr?: PluginData[] | string[]) {
+ return !arr || !arr.length ? 'hide' : '';
+ }
+
+ _loggedInChanged(_loggedIn?: boolean) {
+ if (!_loggedIn) {
+ return;
+ }
+ this.$.restAPI.getPreferences().then(prefs => {
+ if (prefs?.download_scheme) {
+ // Note (issue 5180): normalize the download scheme with lower-case.
+ this._selectedScheme = prefs.download_scheme.toLowerCase();
+ }
+ });
+ }
+
+ _formatBooleanSelect(item: InheritedBooleanInfo) {
+ if (!item) {
+ return;
+ }
+ let inheritLabel = 'Inherit';
+ if (!(item.inherited_value === undefined)) {
+ inheritLabel = `Inherit (${item.inherited_value})`;
+ }
+ return [
+ {
+ label: inheritLabel,
+ value: 'INHERIT',
+ },
+ {
+ label: 'True',
+ value: 'TRUE',
+ },
+ {
+ label: 'False',
+ value: 'FALSE',
+ },
+ ];
+ }
+
+ _formatSubmitTypeSelect(projectConfig: ConfigInfo) {
+ if (!projectConfig) {
+ return;
+ }
+ const allValues = Object.values(SUBMIT_TYPES);
+ const type = projectConfig.default_submit_type;
+ if (!type) {
+ // Server is too old to report default_submit_type, so assume INHERIT
+ // is not a valid value.
+ return allValues;
+ }
+
+ let inheritLabel = 'Inherit';
+ if (type.inherited_value) {
+ inheritLabel = `Inherit (${type.inherited_value})`;
+ for (const val of allValues) {
+ if (val.value === type.inherited_value) {
+ inheritLabel = `Inherit (${val.label})`;
+ break;
+ }
+ }
+ }
+ return [
+ {
+ label: inheritLabel,
+ value: 'INHERIT',
+ },
+ ...allValues,
+ ];
+ }
+
+ _isLoading() {
+ return this._loading || this._loading === undefined;
+ }
+
+ _getLoggedIn() {
+ return this.$.restAPI.getLoggedIn();
+ }
+
+ _formatRepoConfigForSave(repoConfig: ConfigInfo): ConfigInput {
+ const configInputObj: ConfigInput = {};
+ for (const configKey of Object.keys(repoConfig)) {
+ const key = configKey as keyof ConfigInfo;
+ if (key === 'default_submit_type') {
+ // default_submit_type is not in the input type, and the
+ // configured value was already copied to submit_type by
+ // _loadProject. Omit this property when saving.
+ continue;
+ }
+ if (key === 'plugin_config') {
+ configInputObj.plugin_config_values = repoConfig.plugin_config;
+ } else if (typeof repoConfig[key] === 'object') {
+ const repoConfigObj: any = repoConfig[key];
+ if (repoConfigObj.configured_value) {
+ configInputObj[key as keyof ConfigInput] =
+ repoConfigObj.configured_value;
+ }
+ } else {
+ configInputObj[key as keyof ConfigInput] = repoConfig[key] as any;
+ }
+ }
+ return configInputObj;
+ }
+
+ _handleSaveRepoConfig() {
+ if (!this._repoConfig || !this.repo)
+ return Promise.reject(new Error('undefined repoConfig or repo'));
+ return this.$.restAPI
+ .saveRepoConfig(
+ this.repo,
+ this._formatRepoConfigForSave(this._repoConfig)
+ )
+ .then(() => {
+ this._configChanged = false;
+ });
+ }
+
+ @observe('_repoConfig.*')
+ _handleConfigChanged() {
+ if (this._isLoading()) {
+ return;
+ }
+ this._configChanged = true;
+ }
+
+ _computeButtonDisabled(readOnly: boolean, configChanged: boolean) {
+ return readOnly || !configChanged;
+ }
+
+ _computeHeaderClass(configChanged: boolean) {
+ return configChanged ? 'edited' : '';
+ }
+
+ _computeSchemes(schemesDefault: string[], schemesObj?: SchemesInfoMap) {
+ return !schemesObj ? schemesDefault : Object.keys(schemesObj);
+ }
+
+ _schemesChanged(schemes: string[]) {
+ if (schemes.length === 0) {
+ return;
+ }
+ if (!this._selectedScheme || !schemes.includes(this._selectedScheme)) {
+ this._selectedScheme = schemes.sort()[0];
+ }
+ }
+
+ _computeCommands(
+ repo?: RepoName,
+ schemesObj?: SchemesInfoMap,
+ _selectedScheme?: string
+ ) {
+ if (!schemesObj || !repo || !_selectedScheme) {
+ return [];
+ }
+ const commands = [];
+ let commandObj: {[title: string]: string} = {};
+ if (hasOwnProperty(schemesObj, _selectedScheme)) {
+ commandObj = schemesObj[_selectedScheme].clone_commands;
+ }
+ for (const title in commandObj) {
+ if (!hasOwnProperty(commandObj, title)) {
+ continue;
+ }
+ commands.push({
+ title,
+ command: commandObj[title]
+ .replace(/\${project}/gi, encodeURI(repo))
+ .replace(
+ /\${project-base-name}/gi,
+ encodeURI(repo.substring(repo.lastIndexOf('/') + 1))
+ ),
+ });
+ }
+ return commands;
+ }
+
+ _computeRepositoriesClass(config: InheritedBooleanInfo) {
+ return config ? 'showConfig' : '';
+ }
+
+ _computeChangesUrl(name: RepoName) {
+ return GerritNav.getUrlForProjectChanges(name);
+ }
+
+ _handlePluginConfigChanged({
+ detail: {name, config, notifyPath},
+ }: {
+ detail: {
+ name: string;
+ config: PluginParameterToConfigParameterInfoMap;
+ notifyPath: string;
+ };
+ }) {
+ if (this._repoConfig?.plugin_config) {
+ this._repoConfig.plugin_config[name] = config;
+ this.notifyPath('_repoConfig.plugin_config.' + notifyPath);
+ }
+ }
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ 'gr-repo': GrRepo;
+ }
+}
diff --git a/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.ts b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.ts
index 68e65bc..23e024b 100644
--- a/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.ts
+++ b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.ts
@@ -470,8 +470,7 @@
// prior to it being saved.
this.cancelDebouncer('store');
- if (!this.comment?.path || this.comment.line === undefined)
- throw new Error('Cannot erase Draft Comment');
+ if (!this.comment?.path) throw new Error('Cannot erase Draft Comment');
if (this.changeNum === undefined) {
throw new Error('undefined changeNum');
}
@@ -628,7 +627,7 @@
? this.comment.patch_set
: this._getPatchNum();
const {path, line, range} = this.comment;
- if (path && line !== undefined) {
+ if (path) {
this.debounce(
'store',
() => {
@@ -659,9 +658,7 @@
_handleAnchorClick(e: Event) {
e.preventDefault();
- if (!this.comment?.line) {
- return;
- }
+ if (!this.comment) return;
this.dispatchEvent(
new CustomEvent('comment-anchor-tap', {
bubbles: true,
@@ -944,8 +941,7 @@
comment.id ||
comment.message ||
comment.__otherEditing ||
- !comment.path ||
- !comment.line
+ !comment.path
) {
if (comment) delete comment.__otherEditing;
return;
diff --git a/polygerrit-ui/app/elements/shared/gr-comment/gr-comment_test.js b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment_test.js
index 6cda593..3847135 100644
--- a/polygerrit-ui/app/elements/shared/gr-comment/gr-comment_test.js
+++ b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment_test.js
@@ -19,6 +19,7 @@
import './gr-comment.js';
import {html} from '@polymer/polymer/lib/utils/html-tag.js';
import {__testOnly_UNSAVED_MESSAGE} from './gr-comment.js';
+import {SpecialFilePath} from '../../../constants/constants.js';
const basicFixture = fixtureFromElement('gr-comment');
@@ -727,6 +728,27 @@
});
});
+ test('patchset level comment', done => {
+ const comment = {...element.comment,
+ path: SpecialFilePath.PATCHSET_LEVEL_COMMENTS, line: undefined,
+ range: undefined};
+ element.comment = comment;
+ flushAsynchronousOperations();
+ MockInteractions.tap(element.shadowRoot
+ .querySelector('.edit'));
+ assert.isTrue(element.editing);
+
+ element._messageText = 'hello world';
+ const eraseMessageDraftSpy = sinon.spy(element.$.storage,
+ 'eraseDraftComment');
+ const mockEvent = {preventDefault: sinon.stub()};
+ element._handleSave(mockEvent);
+ flush(() => {
+ assert.isTrue(eraseMessageDraftSpy.called);
+ done();
+ });
+ });
+
test('draft creation/cancellation', done => {
assert.isFalse(element.editing);
element.draft = true;
diff --git a/polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager.ts b/polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager.ts
index ffdd662..47a1cbb 100644
--- a/polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager.ts
+++ b/polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager.ts
@@ -34,6 +34,18 @@
// Time in which pressing n key again after the toast navigates to next file
const NAVIGATE_TO_NEXT_FILE_TIMEOUT_MS = 5000;
+/**
+ * Return type for cursor moves, that indicate whether a move was possible.
+ */
+export enum CursorMoveResult {
+ /** The cursor was successfully moved. */
+ MOVED,
+ /** There were no stops - the cursor was reset. */
+ NO_STOPS,
+ /** There was no more stop to move to - the cursor was clipped to the end. */
+ CLIPPED,
+}
+
@customElement('gr-cursor-manager')
export class GrCursorManager extends GestureEventListeners(
LegacyElementMixin(PolymerElement)
@@ -104,16 +116,16 @@
* back to first instead of to last.
* @param navigateToNextFile Navigate to next unreviewed file
* if user presses next on the last diff chunk
+ * @return If a move was performed or why not.
* @private
*/
-
next(
condition?: Function,
getTargetHeight?: (target: HTMLElement) => number,
clipToTop?: boolean,
navigateToNextFile?: boolean
- ) {
- this._moveCursor(
+ ): CursorMoveResult {
+ return this._moveCursor(
1,
condition,
getTargetHeight,
@@ -122,8 +134,8 @@
);
}
- previous(condition?: Function) {
- this._moveCursor(-1, condition);
+ previous(condition?: Function): CursorMoveResult {
+ return this._moveCursor(-1, condition);
}
/**
@@ -269,6 +281,7 @@
* back to first instead of to last.
* @param navigateToNextFile Navigate to next unreviewed file
* if user presses next on the last diff chunk
+ * @return If a move was performed or why not.
* @private
*/
_moveCursor(
@@ -277,27 +290,25 @@
getTargetHeight?: (target: HTMLElement) => number,
clipToTop?: boolean,
navigateToNextFile?: boolean
- ) {
+ ): CursorMoveResult {
if (!this.stops.length) {
this.unsetCursor();
- return;
+ return CursorMoveResult.NO_STOPS;
}
this._unDecorateTarget();
const newIndex = this._getNextindex(delta, condition, clipToTop);
+ const newTarget = newIndex !== -1 ? this.stops[newIndex] : null;
- let newTarget = null;
- if (newIndex !== -1) {
- newTarget = this.stops[newIndex];
- }
+ const clipped = this.index === newIndex;
/*
* If user presses n on the last diff chunk, show a toast informing user
* that pressing n again will navigate them to next unreviewed file.
* If click happens within the time limit, then navigate to next file
*/
- if (navigateToNextFile && this.index === newIndex && this.isAtEnd()) {
+ if (navigateToNextFile && clipped && this.isAtEnd()) {
if (
this._lastDisplayedNavigateToNextFileToast &&
Date.now() - this._lastDisplayedNavigateToNextFileToast <=
@@ -311,7 +322,7 @@
bubbles: true,
})
);
- return;
+ return CursorMoveResult.CLIPPED;
}
this._lastDisplayedNavigateToNextFileToast = Date.now();
this.dispatchEvent(
@@ -323,14 +334,14 @@
bubbles: true,
})
);
- return;
+ return CursorMoveResult.CLIPPED;
}
this.index = newIndex;
this.target = newTarget as HTMLElement;
if (!newTarget) {
- return;
+ return CursorMoveResult.NO_STOPS;
}
if (getTargetHeight) {
@@ -344,6 +355,8 @@
}
this._decorateTarget();
+
+ return clipped ? CursorMoveResult.CLIPPED : CursorMoveResult.MOVED;
}
_decorateTarget() {
diff --git a/polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager_test.js b/polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager_test.js
index bc07d84..5c0bb42 100644
--- a/polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager_test.js
+++ b/polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager_test.js
@@ -18,6 +18,7 @@
import '../../../test/common-test-setup-karma.js';
import './gr-cursor-manager.js';
import {html} from '@polymer/polymer/lib/utils/html-tag.js';
+import {CursorMoveResult} from './gr-cursor-manager.js';
const basicTestFixutre = fixtureFromTemplate(html`
<gr-cursor-manager cursor-target-class="targeted"></gr-cursor-manager>
@@ -66,10 +67,11 @@
assert.isFalse(element.isAtEnd());
// Progress the cursor.
- element.next();
+ let result = element.next();
// Confirm that the next stop is selected and that the previous stop is
// unselected.
+ assert.equal(result, CursorMoveResult.MOVED);
assert.equal(element.index, 3);
assert.equal(element.target, list.children[3]);
assert.isTrue(element.isAtEnd());
@@ -77,19 +79,23 @@
assert.isTrue(list.children[3].classList.contains('targeted'));
// Progress the cursor.
- element.next();
+ result = element.next();
// We should still be at the end.
+ assert.equal(result, CursorMoveResult.CLIPPED);
assert.equal(element.index, 3);
assert.equal(element.target, list.children[3]);
assert.isTrue(element.isAtEnd());
// Wind the cursor all the way back to the first stop.
- element.previous();
- element.previous();
- element.previous();
+ result = element.previous();
+ assert.equal(result, CursorMoveResult.MOVED);
+ result = element.previous();
+ assert.equal(result, CursorMoveResult.MOVED);
+ result = element.previous();
+ assert.equal(result, CursorMoveResult.MOVED);
- // The element state should reflect the end of the list.
+ // The element state should reflect the start of the list.
assert.equal(element.index, 0);
assert.equal(element.target, list.children[0]);
assert.isTrue(element.isAtStart());
@@ -113,8 +119,9 @@
test('next() goes to first element when no cursor is set', () => {
element.stops = list.querySelectorAll('li');
- element.next();
+ const result = element.next();
+ assert.equal(result, CursorMoveResult.MOVED);
assert.equal(element.index, 0);
assert.equal(element.target, list.children[0]);
assert.isTrue(list.children[0].classList.contains('targeted'));
@@ -122,10 +129,23 @@
assert.isFalse(element.isAtEnd());
});
- test('next() goes to first element when no cursor is set', () => {
- element.stops = list.querySelectorAll('li');
- element.previous();
+ test('next() resets the cursor when there are no stops', () => {
+ element.stops = [];
+ const result = element.next();
+ assert.equal(result, CursorMoveResult.NO_STOPS);
+ assert.equal(element.index, -1);
+ assert.isNotOk(element.target);
+ assert.isFalse(list.children[1].classList.contains('targeted'));
+ assert.isFalse(element.isAtStart());
+ assert.isFalse(element.isAtEnd());
+ });
+
+ test('previous() goes to last element when no cursor is set', () => {
+ element.stops = list.querySelectorAll('li');
+ const result = element.previous();
+
+ assert.equal(result, CursorMoveResult.MOVED);
const lastIndex = list.children.length - 1;
assert.equal(element.index, lastIndex);
assert.equal(element.target, list.children[lastIndex]);
@@ -134,6 +154,18 @@
assert.isTrue(element.isAtEnd());
});
+ test('previous() resets the cursor when there are no stops', () => {
+ element.stops = [];
+ const result = element.previous();
+
+ assert.equal(result, CursorMoveResult.NO_STOPS);
+ assert.equal(element.index, -1);
+ assert.isNotOk(element.target);
+ assert.isFalse(list.children[1].classList.contains('targeted'));
+ assert.isFalse(element.isAtStart());
+ assert.isFalse(element.isAtEnd());
+ });
+
test('_moveCursor', () => {
// Initialize the cursor with its stops.
element.stops = list.querySelectorAll('li');
diff --git a/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text_html.ts b/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text_html.ts
index 468bbee..bc1dfe0 100644
--- a/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text_html.ts
+++ b/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text_html.ts
@@ -55,6 +55,7 @@
list-style-type: disc;
margin-left: var(--spacing-xl);
}
+ code,
gr-linked-text.pre {
font-family: var(--monospace-font-family);
font-size: var(--font-size-code);
diff --git a/polygerrit-ui/app/elements/shared/gr-storage/gr-storage.ts b/polygerrit-ui/app/elements/shared/gr-storage/gr-storage.ts
index 15914c5..3233420 100644
--- a/polygerrit-ui/app/elements/shared/gr-storage/gr-storage.ts
+++ b/polygerrit-ui/app/elements/shared/gr-storage/gr-storage.ts
@@ -24,7 +24,7 @@
changeNum: number;
patchNum: PatchSetNum;
path: string;
- line: number;
+ line?: number;
range?: CommentRange;
}
diff --git a/polygerrit-ui/app/services/services/gr-rest-api/gr-rest-api.ts b/polygerrit-ui/app/services/services/gr-rest-api/gr-rest-api.ts
index 5565180..c85e307 100644
--- a/polygerrit-ui/app/services/services/gr-rest-api/gr-rest-api.ts
+++ b/polygerrit-ui/app/services/services/gr-rest-api/gr-rest-api.ts
@@ -81,6 +81,7 @@
UrlEncodedCommentId,
TagInfo,
GitRef,
+ ConfigInput,
} from '../../../types/common';
import {ParsedChangeInfo} from '../../../elements/shared/gr-rest-api-interface/gr-reviewer-updates-parser';
import {HttpMethod} from '../../../constants/constants';
@@ -688,4 +689,5 @@
setRepoHead(repo: RepoName, ref: GitRef): Promise<Response>;
deleteRepoTags(repo: RepoName, ref: GitRef): Promise<Response>;
deleteRepoBranches(repo: RepoName, ref: GitRef): Promise<Response>;
+ saveRepoConfig(repo: RepoName, config: ConfigInput): Promise<Response>;
}
diff --git a/polygerrit-ui/app/types/common.ts b/polygerrit-ui/app/types/common.ts
index ea7fb75..e7539b5 100644
--- a/polygerrit-ui/app/types/common.ts
+++ b/polygerrit-ui/app/types/common.ts
@@ -793,16 +793,19 @@
new_value: string;
}
+export type SchemesInfoMap = {[name: string]: DownloadSchemeInfo};
+
/**
* The DownloadInfo entity contains information about supported download
* options.
* https://gerrit-review.googlesource.com/Documentation/rest-api-config.html
*/
export interface DownloadInfo {
- schemes: string;
+ schemes: SchemesInfoMap;
archives: string;
}
+export type CloneCommandMap = {[name: string]: string};
/**
* The DownloadSchemeInfo entity contains information about a supported download
* scheme and its commands.
@@ -813,7 +816,7 @@
is_auth_required: boolean;
is_auth_supported: boolean;
commands: string;
- clone_commands: string;
+ clone_commands: CloneCommandMap;
}
/**
@@ -1347,14 +1350,6 @@
reject_empty_commit?: InheritedBooleanInfo;
}
-export type PluginParameterToConfigParameterInfoMap = {
- [parameterName: string]: ConfigParameterInfo;
-};
-
-export type PluginNameToPluginParametersMap = {
- [pluginName: string]: PluginParameterToConfigParameterInfoMap;
-};
-
/**
* The ProjectAccessInfo entity contains information about the access rights for a project
* https://gerrit-review.googlesource.com/Documentation/rest-api-access.html#project-access-info
@@ -1447,23 +1442,31 @@
use_signed_off_by?: InheritedBooleanInfoConfiguredValue;
create_new_change_for_all_not_in_target?: InheritedBooleanInfoConfiguredValue;
require_change_id?: InheritedBooleanInfoConfiguredValue;
+ enable_signed_push?: InheritedBooleanInfoConfiguredValue;
+ require_signed_push?: InheritedBooleanInfoConfiguredValue;
+ private_by_default?: InheritedBooleanInfoConfiguredValue;
+ work_in_progress_by_default?: InheritedBooleanInfoConfiguredValue;
+ enable_reviewer_by_email?: InheritedBooleanInfoConfiguredValue;
+ match_author_to_committer_date?: InheritedBooleanInfoConfiguredValue;
reject_implicit_merges?: InheritedBooleanInfoConfiguredValue;
+ reject_empty_commit?: InheritedBooleanInfoConfiguredValue;
max_object_size_limit?: MaxObjectSizeLimitInfo;
submit_type?: SubmitType;
state?: ProjectState;
- plugin_config_values?: PluginConfigValues;
- reject_empty_commit?: InheritedBooleanInfoConfiguredValue;
+ plugin_config_values?: PluginNameToPluginParametersMap;
commentlinks?: ConfigInfoCommentLinks;
}
-
/**
* Plugin configuration values as map which maps the plugin name to a map of parameter names to values
* https://gerrit-review.googlesource.com/Documentation/rest-api-projects.html#config-input
*/
-export type PluginConfigValues = {
- [pluginName: string]: ParameterNameToValueMap;
+export type PluginNameToPluginParametersMap = {
+ [pluginName: string]: PluginParameterToConfigParameterInfoMap;
};
-export type ParameterNameToValueMap = {[parameterName: string]: string};
+
+export type PluginParameterToConfigParameterInfoMap = {
+ [parameterName: string]: ConfigParameterInfo;
+};
export type ConfigInfoCommentLinks = {
[commentLinkName: string]: CommentLinkInfo;
@@ -1490,7 +1493,6 @@
enable_signed_push?: InheritedBooleanInfoConfiguredValue;
require_signed_push?: InheritedBooleanInfoConfiguredValue;
max_object_size_limit?: string;
- plugin_config_values?: PluginConfigValues;
reject_empty_commit?: InheritedBooleanInfoConfiguredValue;
}