Use CodeOwnersModel in suggest-owners
Change-Id: I1dbfbc00013478a54dd6216b62fd5b17953c399b
diff --git a/ui/code-owners-model-loader.js b/ui/code-owners-model-loader.js
index ea10813..2074fcb 100644
--- a/ui/code-owners-model-loader.js
+++ b/ui/code-owners-model-loader.js
@@ -15,6 +15,8 @@
* limitations under the License.
*/
+import {SuggestionsState} from './code-owners-model.js';
+
/**
* ModelLoader provides a method for loading data into the model.
* It is a bridge between an ownersModel and an ownersService.
@@ -75,4 +77,22 @@
value => this.ownersModel.setAreAllFilesApproved(value)
);
}
+
+ async loadSuggestions() {
+ // If a loading has been started already, do nothing
+ if (this.ownersModel.suggestionsState
+ !== SuggestionsState.NotLoaded) return;
+
+ this.ownersModel.setSuggestionsState(SuggestionsState.Loading);
+ const suggestedOwners = await this.ownersService.getSuggestedOwners();
+ this.ownersModel.setSuggestions(suggestedOwners.suggestions);
+ this.ownersModel.setSuggestionsState(SuggestionsState.Loaded);
+ }
+
+ async updateLoadSuggestionsProgress() {
+ const suggestedOwners =
+ await this.ownersService.getSuggestedOwnersProgress();
+ this.ownersModel.setSuggestionsLoadProgress(suggestedOwners.progress);
+ this.ownersModel.setSuggestions(suggestedOwners.suggestions);
+ }
}
diff --git a/ui/code-owners-model.js b/ui/code-owners-model.js
index 5600773..4c63419 100644
--- a/ui/code-owners-model.js
+++ b/ui/code-owners-model.js
@@ -15,11 +15,17 @@
* limitations under the License.
*/
+export const SuggestionsState = {
+ NotLoaded: 'NotLoaded',
+ Loaded: 'Loaded',
+ Loading: 'Loading',
+};
+
/**
* Maintain the state of code-owners.
* Raises 'model-property-changed' event when a property is changed.
- * The plugin shares the same model between all UI elements (thought it is not
- * required).
+ * The plugin shares the same model between all UI elements (if it is not,
+ * the plugin can't maintain showSuggestions state across different UI elements).
* UI elements use values from this model to display information
* and listens for the model-property-changed event. To do so, UI elements
* add CodeOwnersModelMixin, which is doing the listening and the translation
@@ -41,6 +47,10 @@
this.userRole = undefined;
this.isCodeOwnerEnabled = undefined;
this.areAllFilesApproved = undefined;
+ this.suggestions = undefined;
+ this.suggestionsState = SuggestionsState.NotLoaded;
+ this.suggestionsLoadProgress = undefined;
+ this.showSuggestions = false;
}
setBranchConfig(config) {
@@ -73,6 +83,30 @@
this._firePropertyChanged('areAllFilesApproved');
}
+ setSuggestions(suggestions) {
+ if (this.suggestions === suggestions) return;
+ this.suggestions = suggestions;
+ this._firePropertyChanged('suggestions');
+ }
+
+ setSuggestionsState(state) {
+ if (this.suggestionsState === state) return;
+ this.suggestionsState = state;
+ this._firePropertyChanged('suggestionsState');
+ }
+
+ setSuggestionsLoadProgress(progress) {
+ if (this.suggestionsLoadProgress === progress) return;
+ this.suggestionsLoadProgress = progress;
+ this._firePropertyChanged('suggestionsLoadProgress');
+ }
+
+ setShowSuggestions(show) {
+ if (this.showSuggestions === show) return;
+ this.showSuggestions = show;
+ this._firePropertyChanged('showSuggestions');
+ }
+
_firePropertyChanged(propertyName) {
this.dispatchEvent(new CustomEvent('model-property-changed', {
detail: {
diff --git a/ui/code-owners-service.js b/ui/code-owners-service.js
index 52638ac..9b670b9 100644
--- a/ui/code-owners-service.js
+++ b/ui/code-owners-service.js
@@ -440,6 +440,17 @@
};
}
+ async getSuggestedOwnersProgress() {
+ const {codeOwnerStatusMap} = await this.getStatus();
+ return {
+ finished: this.ownersFetcher.getStatus() === FetchStatus.FINISHED,
+ status: this.ownersFetcher.getStatus(),
+ progress: this.ownersFetcher.getProgressString(),
+ suggestions: this._groupFilesByOwners(codeOwnerStatusMap,
+ this.ownersFetcher.getFiles()),
+ };
+ }
+
_formatStatuses(statuses) {
// convert the array of statuses to map between file path -> status
return statuses.reduce((prev, cur) => {
diff --git a/ui/owner-requirement.js b/ui/owner-requirement.js
index b7b9dbb..c9d9cb8 100644
--- a/ui/owner-requirement.js
+++ b/ui/owner-requirement.js
@@ -16,7 +16,6 @@
*/
import {OwnerStatus} from './code-owners-service.js';
-import {ownerState} from './owner-ui-state.js';
import {CodeOwnersModelMixin} from './code-owners-model-mixin.js';
/**
@@ -215,6 +214,7 @@
}
_openReplyDialog() {
+ this.model.setShowSuggestions(true);
this.dispatchEvent(
new CustomEvent('open-reply-dialog', {
detail: {},
@@ -222,7 +222,6 @@
bubbles: true,
})
);
- ownerState.expandSuggestion = true;
this.reporting.reportInteraction('suggest-owners-from-submit-requirement',
{user_role: this.model.userRole});
}
diff --git a/ui/owner-ui-state.js b/ui/owner-ui-state.js
deleted file mode 100644
index 3d47ef9..0000000
--- a/ui/owner-ui-state.js
+++ /dev/null
@@ -1,69 +0,0 @@
-/**
- * @license
- * Copyright (C) 2020 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.
- */
-
-/**
- * @enum
- */
-const OwnerUIStateEventType = {
- EXPAND_SUGGESTION: 'expandSuggestion',
-};
-
-/**
- * For states used in code owners plugin across multiple components.
- */
-class OwnerUIState {
- constructor() {
- this._expandSuggestion = false;
- this._listeners = new Map();
- this._listeners.set(OwnerUIStateEventType.EXPAND_SUGGESTION, []);
- }
-
- get expandSuggestion() {
- return this._expandSuggestion;
- }
-
- set expandSuggestion(value) {
- this._expandSuggestion = value;
- this._listeners.get(OwnerUIStateEventType.EXPAND_SUGGESTION).forEach(cb => {
- try {
- cb(value);
- } catch (e) {
- console.warn(e);
- }
- });
- }
-
- _subscribeEvent(eventType, cb) {
- this._listeners.get(eventType).push(cb);
- return () => {
- this._unsubscribeEvent(eventType, cb);
- };
- }
-
- _unsubscribeEvent(eventType, cb) {
- this._listeners.set(
- eventType,
- this._listeners.get(eventType).filter(handler => handler !== cb)
- );
- }
-
- onExpandSuggestionChange(cb) {
- return this._subscribeEvent(OwnerUIStateEventType.EXPAND_SUGGESTION, cb);
- }
-}
-
-export const ownerState = new OwnerUIState();
diff --git a/ui/suggest-owners-trigger.js b/ui/suggest-owners-trigger.js
index c6fd55d..8e0e293 100644
--- a/ui/suggest-owners-trigger.js
+++ b/ui/suggest-owners-trigger.js
@@ -14,7 +14,6 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-import {ownerState} from './owner-ui-state.js';
import {CodeOwnersModelMixin} from './code-owners-model-mixin.js';
export class SuggestOwnersTrigger extends
@@ -23,17 +22,8 @@
return 'suggest-owners-trigger';
}
- constructor(props) {
- super(props);
- this.expandSuggestionStateUnsubscriber = undefined;
- }
-
static get properties() {
return {
- expanded: {
- type: Boolean,
- value: false,
- },
hidden: {
type: Boolean,
computed: '_computeHidden(model.isCodeOwnerEnabled,' +
@@ -62,7 +52,7 @@
has-tooltip
title="Suggest owners for your change"
>
- [[computeButtonText(expanded)]]
+ [[computeButtonText(model.showSuggestions)]]
</gr-button>
<span>
<a on-click="_reportBugClick" href="https://bugs.chromium.org/p/gerrit/issues/entry?template=code-owners-plugin" target="_blank">
@@ -82,22 +72,6 @@
this.modelLoader.loadAreAllFilesApproved();
}
- connectedCallback() {
- super.connectedCallback();
- this.expandSuggestionStateUnsubscriber = ownerState
- .onExpandSuggestionChange(expanded => {
- this.expanded = expanded;
- });
- }
-
- disconnnectedCallback() {
- super.disconnectedCallback();
- if (this.expandSuggestionStateUnsubscriber) {
- this.expandSuggestionStateUnsubscriber();
- this.expandSuggestionStateUnsubscriber = undefined;
- }
- }
-
_computeHidden(enabled, allFilesApproved, userRole) {
if (enabled === undefined ||
allFilesApproved === undefined ||
@@ -112,8 +86,7 @@
}
toggleControlContent() {
- this.expanded = !this.expanded;
- ownerState.expandSuggestion = this.expanded;
+ this.model.setShowSuggestions(!this.model.showSuggestions);
this.reporting.reportInteraction('toggle-suggest-owners', {
expanded: this.expanded,
user_role: this.model.userRole ?
diff --git a/ui/suggest-owners.js b/ui/suggest-owners.js
index 29e989a..068b933 100644
--- a/ui/suggest-owners.js
+++ b/ui/suggest-owners.js
@@ -14,8 +14,8 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-import {CodeOwnerService} from './code-owners-service.js';
-import {ownerState} from './owner-ui-state.js';
+import {CodeOwnersModelMixin} from './code-owners-model-mixin.js';
+import {SuggestionsState} from './code-owners-model.js';
const SUGGESTION_POLLING_INTERVAL = 1000;
@@ -24,11 +24,6 @@
return 'owner-group-file-list';
}
- constructor() {
- super();
- this.expandSuggestionStateUnsubscriber = undefined;
- }
-
static get properties() {
return {
files: Array,
@@ -89,7 +84,7 @@
customElements.define(OwnerGroupFileList.is, OwnerGroupFileList);
-export class SuggestOwners extends Polymer.Element {
+export class SuggestOwners extends CodeOwnersModelMixin(Polymer.Element) {
static get is() {
return 'suggest-owners';
}
@@ -299,18 +294,14 @@
static get properties() {
return {
- // @input
- change: Object,
- restApi: Object,
- reporting: Object,
-
// @internal attributes
hidden: {
type: Boolean,
value: true,
reflectToAttribute: true,
+ computed: '_isHidden(model.areAllFilesApproved, ' +
+ 'model.showSuggestions)',
},
- ownerService: Object,
suggestedOwners: Array,
isLoading: {
type: Boolean,
@@ -325,95 +316,87 @@
static get observers() {
return [
- 'onInputChanged(restApi, change)',
- 'onReviewerChange(reviewers)',
+ '_onReviewerChanged(reviewers)',
+ '_onShowSuggestionsChanged(model.showSuggestions)',
+ '_onSuggestionsStateChanged(model.suggestionsState)',
+ '_onSuggestionsChanged(model.suggestions, model.suggestionsState)',
+ '_onSuggestionsLoadProgressChanged(model.suggestionsLoadProgress)',
];
}
- connectedCallback() {
- super.connectedCallback();
- this.expandSuggestionStateUnsubscriber = ownerState
- .onExpandSuggestionChange(expanded => {
- this.hidden = !expanded;
- if (expanded) {
- // this is more of a hack to let reivew input lose focus
- // to avoid suggestion dropdown
- // gr-autocomplete has a internal state for tracking focus
- // that will be canceled if any click happens outside of
- // it's target
- // Can not use `this.async` as it's only available in
- // legacy element mixin which not used in this plugin.
- Polymer.Async.timeOut.run(() => this.click(), 100);
+ _onShowSuggestionsChanged(showSuggestions) {
+ if (!showSuggestions ||
+ this.model.suggestionsLoadProgress === SuggestionsState.NotLoaded) {
+ return;
+ }
+ // this is more of a hack to let review input lose focus
+ // to avoid suggestion dropdown
+ // gr-autocomplete has a internal state for tracking focus
+ // that will be canceled if any click happens outside of
+ // it's target
+ // Can not use `this.async` as it's only available in
+ // legacy element mixin which not used in this plugin.
+ Polymer.Async.timeOut.run(() => this.click(), 100);
- // start fetching suggestions
- if (!this._suggestionsTimer) {
- this._suggestionsTimer = setInterval(() => {
- this._pollingSuggestions();
- }, SUGGESTION_POLLING_INTERVAL);
-
- // poll immediately to kick start the fetching
- this.reporting
- .reportLifeCycle('owners-suggestions-fetching-start');
- this._pollingSuggestions();
- }
- }
- });
+ this.modelLoader.loadSuggestions();
+ this.reporting.reportLifeCycle('owners-suggestions-fetching-start');
}
disconnectedCallback() {
super.disconnectedCallback();
- if (this.expandSuggestionStateUnsubscriber) {
- this.expandSuggestionStateUnsubscriber();
- this.expandSuggestionStateUnsubscriber = undefined;
- }
+ this._stopUpdateProgressTimer();
}
- onInputChanged(restApi, change) {
- ownerState.expandSuggestion = false;
- if ([restApi, change].includes(undefined)) return;
- this.isLoading = true;
- const ownerService = CodeOwnerService.getOwnerService(this.restApi, change);
- if (this.ownerService && this.ownerService !== ownerService) {
- // abort all pending requests
- this.ownerService.abort();
- clearInterval(this._suggestionsTimer);
- this._suggestionsTimer = null;
- }
- this.ownerService = ownerService;
+ _startUpdateProgressTimer() {
+ if (this._progressUpdateTimer) return;
+ this._progressUpdateTimer = setInterval(() => {
+ this.modelLoader.updateLoadSuggestionsProgress();
+ }, SUGGESTION_POLLING_INTERVAL);
+ }
+ _stopUpdateProgressTimer() {
+ if (!this._progressUpdateTimer) return;
+ clearInterval(this._progressUpdateTimer);
+ this._progressUpdateTimer = undefined;
+ }
+
+ _onSuggestionsStateChanged(state) {
+ this._stopUpdateProgressTimer();
+ if (state === SuggestionsState.Loading) {
+ this._startUpdateProgressTimer();
+ }
+ this.isLoading = state === SuggestionsState.Loading;
+ }
+
+ _isHidden(allFilesApproved, showSuggestions) {
+ if (!showSuggestions) return true;
// if all approved, no need to show the container
- this.ownerService.areAllFilesApproved().then(approved => {
- if (approved) {
- this.hidden = approved;
- }
- });
+ return allFilesApproved === undefined || !!allFilesApproved;
}
- _pollingSuggestions() {
- this.ownerService
- .getSuggestedOwners()
- .then(res => {
- if (res.finished) {
- clearInterval(this._suggestionsTimer);
- this._suggestionsTimer = null;
- const reportDetails = res.suggestions.reduce((details, cur) => {
- details.totalGroups++;
- details.stats.push([cur.files.length,
- cur.owners ? cur.owners.length : 0]);
- return details;
- }, {totalGroups: 0, stats: []});
- this.reporting.reportLifeCycle(
- 'owners-suggestions-fetching-finished', reportDetails);
- }
- this.progressText = res.progress;
- this.isLoading = !res.finished;
+ loadPropertiesAfterModelChanged() {
+ super.loadPropertiesAfterModelChanged();
+ this._stopUpdateProgressTimer();
+ this.modelLoader.loadAreAllFilesApproved();
+ }
- this._updateSuggestions(res.suggestions);
+ _onSuggestionsChanged(suggestions, suggestionsState) {
+ // The updateLoadSuggestionsProgress method also updates suggestions
+ this._updateSuggestions(suggestions || []);
+ this._updateAllChips(this._currentReviewers);
+ if (!suggestions || suggestionsState !== SuggestionsState.Loaded) return;
+ const reportDetails = suggestions.reduce((details, cur) => {
+ details.totalGroups++;
+ details.stats.push([cur.files.length,
+ cur.owners ? cur.owners.length : 0]);
+ return details;
+ }, {totalGroups: 0, stats: []});
+ this.reporting.reportLifeCycle(
+ 'owners-suggestions-fetching-finished', reportDetails);
+ }
- // in case `_updateAllChips` called before suggestedOwners ready
- // from onReviewerChange
- this._updateAllChips(this._currentReviewers);
- });
+ _onSuggestionsLoadProgressChanged(progress) {
+ this.progressText = progress;
}
_updateSuggestions(suggestions) {
@@ -423,7 +406,7 @@
});
}
- onReviewerChange(reviewers) {
+ _onReviewerChanged(reviewers) {
this._currentReviewers = reviewers;
this._updateAllChips(reviewers);
}