Move loading of `mergeable` from change-view into change-model
Release-Notes: skip
Google-Bug-Id: b/247042673
Change-Id: I9b8e34e18e8082404fdebe26b25cae084a302c7b
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.ts b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.ts
index a80c058..7e28af9 100644
--- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.ts
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.ts
@@ -408,9 +408,7 @@
);
}
- // Private but used in tests.
- @state()
- mergeable: boolean | null = null;
+ @state() mergeable?: boolean;
/**
* Plugins can provide (multiple) tabs. For each plugin tab we render an
@@ -721,6 +719,11 @@
);
subscribe(
this,
+ () => this.getChangeModel().mergeable$,
+ mergeable => (this.mergeable = mergeable)
+ );
+ subscribe(
+ this,
() => this.getChangeModel().revision$,
revision => (this.revision = revision)
);
@@ -843,6 +846,8 @@
changedProperties.has('mergeable') ||
changedProperties.has('currentRevisionActions')
) {
+ // TODO: Just compute `changeStatuses` on the fly. No need for it to be a
+ // @state object.
this.changeStatuses = this.computeChangeStatusChips();
}
}
@@ -1421,7 +1426,6 @@
<div class="relatedChanges">
<gr-related-changes-list
id="relatedChanges"
- .mergeable=${this.mergeable}
></gr-related-changes-list>
</div>
<div class="emptySpace"></div>
@@ -1764,18 +1768,11 @@
}
private computeChangeStatusChips() {
- if (!this.change) {
- return [];
- }
-
- // Show no chips until mergeability is loaded.
- if (this.mergeable === null) {
- return [];
- }
+ if (!this.change || this.mergeable === undefined) return [];
const options = {
includeDerived: true,
- mergeable: !!this.mergeable,
+ mergeable: this.mergeable,
submitEnabled: !!this.isSubmitEnabled(),
};
return changeStatuses(this.change as ChangeInfo, options);
@@ -2130,7 +2127,7 @@
this.getPluginLoader().jsApiService.handleShowChange({
change: this.change,
patchNum: this.patchRange.patchNum,
- info: {mergeable: this.mergeable},
+ info: {mergeable: this.mergeable ?? null},
});
}
@@ -2697,11 +2694,6 @@
});
const coreDataPromise = loadingFlagSet;
- const mergeabilityLoaded = coreDataPromise.then(() =>
- this.getMergeability()
- );
- allDataPromises.push(mergeabilityLoaded);
-
coreDataPromise.then(() => {
fire(this, 'change-details-loaded', {});
this.reporting.timeEnd(Timing.CHANGE_RELOAD);
@@ -2769,43 +2761,6 @@
);
}
- // Private but used in tests
- getMergeability(): Promise<void> {
- if (!this.change) {
- this.mergeable = null;
- return Promise.resolve();
- }
- // If the change is closed, it is not mergeable. Note: already merged
- // changes are obviously not mergeable, but the mergeability API will not
- // answer for abandoned changes.
- if (
- this.change.status === ChangeStatus.MERGED ||
- this.change.status === ChangeStatus.ABANDONED
- ) {
- this.mergeable = false;
- return Promise.resolve();
- }
-
- if (!this.changeNum) {
- return Promise.reject(new Error('missing required changeNum property'));
- }
-
- // If mergeable bit was already returned in detail REST endpoint, use it.
- if (this.change.mergeable !== undefined) {
- this.mergeable = this.change.mergeable;
- return Promise.resolve();
- }
-
- this.mergeable = null;
- return this.restApiService
- .getMergeable(this.changeNum)
- .then(mergableInfo => {
- if (mergableInfo) {
- this.mergeable = mergableInfo.mergeable;
- }
- });
- }
-
/**
* Returns the text to be copied when
* click the copy icon next to change subject
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.ts b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.ts
index 44a8428..6ea19c7 100644
--- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.ts
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.ts
@@ -35,7 +35,6 @@
createChange,
createChangeMessages,
createCommit,
- createMergeable,
createPreferences,
createRevision,
createRevisions,
@@ -75,8 +74,7 @@
SavingState,
} from '../../../types/common';
import {GrEditControls} from '../../edit/gr-edit-controls/gr-edit-controls';
-import {SinonFakeTimers, SinonStubbedMember} from 'sinon';
-import {RestApiService} from '../../../services/gr-rest-api/gr-rest-api';
+import {SinonFakeTimers} from 'sinon';
import {GerritView} from '../../../services/router/router-model';
import {ParsedChangeInfo} from '../../../types/types';
import {GrRelatedChangesList} from '../gr-related-changes-list/gr-related-changes-list';
@@ -800,7 +798,6 @@
Promise.resolve(createParsedChange())
);
sinon.stub(element, 'performPostChangeLoadTasks');
- sinon.stub(element, 'getMergeability');
const change = {
...createChangeViewChange(),
revisions: createRevisions(1),
@@ -1539,9 +1536,6 @@
'#relatedChanges'
) as GrRelatedChangesList;
const reloadStub = sinon.stub(relatedChanges, 'reload');
- stubRestApi('getMergeable').returns(
- Promise.resolve({...createMergeable(), mergeable: true})
- );
element.viewState = createChangeViewState();
changeModel.setState({
@@ -2103,42 +2097,6 @@
});
});
- suite('getMergeability', () => {
- let getMergeableStub: SinonStubbedMember<RestApiService['getMergeable']>;
- setup(() => {
- element.change = {...createChangeViewChange(), labels: {}};
- getMergeableStub = stubRestApi('getMergeable').returns(
- Promise.resolve({...createMergeable(), mergeable: true})
- );
- });
-
- test('merged change', () => {
- element.mergeable = null;
- element.change!.status = ChangeStatus.MERGED;
- return element.getMergeability().then(() => {
- assert.isFalse(element.mergeable);
- assert.isFalse(getMergeableStub.called);
- });
- });
-
- test('abandoned change', () => {
- element.mergeable = null;
- element.change!.status = ChangeStatus.ABANDONED;
- return element.getMergeability().then(() => {
- assert.isFalse(element.mergeable);
- assert.isFalse(getMergeableStub.called);
- });
- });
-
- test('open change', () => {
- element.mergeable = null;
- return element.getMergeability().then(() => {
- assert.isTrue(element.mergeable);
- assert.isTrue(getMergeableStub.called);
- });
- });
- });
-
test('handleToggleStar called when star is tapped', async () => {
element.change = {
...createChangeViewChange(),
@@ -2162,7 +2120,6 @@
patchNum: 1 as RevisionPatchSetNum,
};
sinon.stub(element, 'performPostChangeLoadTasks');
- sinon.stub(element, 'getMergeability').returns(Promise.resolve());
});
test("don't report changeDisplayed on reply", async () => {
diff --git a/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list.ts b/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list.ts
index 05acb29..9d03d15 100644
--- a/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list.ts
+++ b/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list.ts
@@ -11,7 +11,7 @@
import '../../shared/gr-icon/gr-icon';
import {classMap} from 'lit/directives/class-map.js';
import {LitElement, css, html, TemplateResult} from 'lit';
-import {customElement, property, state} from 'lit/decorators.js';
+import {customElement, state} from 'lit/decorators.js';
import {sharedStyles} from '../../../styles/shared-styles';
import {
ChangeInfo,
@@ -54,7 +54,7 @@
@customElement('gr-related-changes-list')
export class GrRelatedChangesList extends LitElement {
- @property({type: Boolean})
+ @state()
mergeable?: boolean;
@state()
@@ -97,6 +97,11 @@
() => this.getChangeModel().latestPatchNum$,
x => (this.latestPatchNum = x)
);
+ subscribe(
+ this,
+ () => this.getChangeModel().mergeable$,
+ x => (this.mergeable = x)
+ );
}
static override get styles() {
@@ -616,8 +621,6 @@
}),
];
- // Get conflicts if change is open and is mergeable.
- // Mergeable is output of restApiServict.getMergeable from gr-change-view
if (changeIsOpen(change) && this.mergeable) {
promises.push(
this.restApiService
diff --git a/polygerrit-ui/app/models/change/change-model.ts b/polygerrit-ui/app/models/change/change-model.ts
index 6d575ce..da88bfc 100644
--- a/polygerrit-ui/app/models/change/change-model.ts
+++ b/polygerrit-ui/app/models/change/change-model.ts
@@ -15,7 +15,7 @@
PatchSetNumber,
CommitId,
} from '../../types/common';
-import {DefaultBase} from '../../constants/constants';
+import {ChangeStatus, DefaultBase} from '../../constants/constants';
import {combineLatest, from, fromEvent, Observable, forkJoin, of} from 'rxjs';
import {
map,
@@ -71,6 +71,13 @@
* Undefined means it's still loading and empty set means no files reviewed.
*/
reviewedFiles?: string[];
+ /**
+ * Either filled from `change.mergeable`, or from a dedicated REST API call.
+ * Is initially `undefined`, such that you can identify whether this
+ * information has already been loaded once for this change or not. Will never
+ * go back to `undefined` after being set for a change.
+ */
+ mergeable?: boolean;
}
/**
@@ -201,6 +208,11 @@
changeState => changeState?.reviewedFiles
);
+ public readonly mergeable$ = select(
+ this.state$,
+ changeState => changeState.mergeable
+ );
+
public readonly changeNum$ = select(this.change$, change => change?._number);
public readonly repo$ = select(this.change$, change => change?.project);
@@ -353,6 +365,21 @@
// helps with that.
this.updateStateChange(change ?? undefined);
}),
+ this.change$
+ .pipe(
+ switchMap(change => {
+ if (change?._number === undefined) return of(undefined);
+ if (change.mergeable !== undefined) return of(change.mergeable);
+ if (change.status === ChangeStatus.MERGED) return of(false);
+ if (change.status === ChangeStatus.ABANDONED) return of(false);
+ return from(
+ this.restApiService
+ .getMergeable(change._number)
+ .then(mergableInfo => mergableInfo?.mergeable ?? false)
+ );
+ })
+ )
+ .subscribe(mergeable => this.updateState({mergeable})),
this.change$.subscribe(change => (this.change = change)),
this.patchNum$.subscribe(patchNum => (this.patchNum = patchNum)),
this.basePatchNum$.subscribe(
diff --git a/polygerrit-ui/app/models/change/change-model_test.ts b/polygerrit-ui/app/models/change/change-model_test.ts
index 2ec0b20..5ce848d 100644
--- a/polygerrit-ui/app/models/change/change-model_test.ts
+++ b/polygerrit-ui/app/models/change/change-model_test.ts
@@ -11,6 +11,7 @@
createChangeMessageInfo,
createChangeViewState,
createEditInfo,
+ createMergeable,
createParsedChange,
createRevision,
} from '../../test/test-data-generators';
@@ -41,6 +42,7 @@
import {userModelToken} from '../user/user-model';
import {ChangeViewModel, changeViewModelToken} from '../views/change';
import {navigationToken} from '../../elements/core/gr-navigation/gr-navigation';
+import {SinonStub} from 'sinon';
suite('updateRevisionsWithCommitShas() tests', () => {
test('undefined edit', async () => {
@@ -123,6 +125,93 @@
changeModel.finalize();
});
+ suite('mergeability', async () => {
+ let getMergeableStub: SinonStub;
+ let mergeableApiResponse = false;
+
+ setup(() => {
+ getMergeableStub = stubRestApi('getMergeable').callsFake(() =>
+ Promise.resolve(createMergeable(mergeableApiResponse))
+ );
+ });
+
+ test('mergeability initially undefined', async () => {
+ waitUntilObserved(
+ changeModel.mergeable$,
+ mergeable => mergeable === undefined
+ );
+ assert.isFalse(getMergeableStub.called);
+ });
+
+ test('mergeability true from change', async () => {
+ changeModel.updateStateChange({...knownChange, mergeable: true});
+
+ waitUntilObserved(
+ changeModel.mergeable$,
+ mergeable => mergeable === true
+ );
+ assert.isFalse(getMergeableStub.called);
+ });
+
+ test('mergeability false from change', async () => {
+ changeModel.updateStateChange({...knownChange, mergeable: false});
+
+ waitUntilObserved(
+ changeModel.mergeable$,
+ mergeable => mergeable === true
+ );
+ assert.isFalse(getMergeableStub.called);
+ });
+
+ test('mergeability false for MERGED change', async () => {
+ changeModel.updateStateChange({
+ ...knownChange,
+ status: ChangeStatus.MERGED,
+ });
+
+ waitUntilObserved(
+ changeModel.mergeable$,
+ mergeable => mergeable === false
+ );
+ assert.isFalse(getMergeableStub.called);
+ });
+
+ test('mergeability false for ABANDONED change', async () => {
+ changeModel.updateStateChange({
+ ...knownChange,
+ status: ChangeStatus.ABANDONED,
+ });
+
+ waitUntilObserved(
+ changeModel.mergeable$,
+ mergeable => mergeable === false
+ );
+ assert.isFalse(getMergeableStub.called);
+ });
+
+ test('mergeability true from API', async () => {
+ mergeableApiResponse = true;
+ changeModel.updateStateChange(knownChange);
+
+ waitUntilObserved(
+ changeModel.mergeable$,
+ mergeable => mergeable === true
+ );
+ assert.isTrue(getMergeableStub.calledOnce);
+ });
+
+ test('mergeability false from API', async () => {
+ mergeableApiResponse = false;
+ changeModel.updateStateChange(knownChange);
+
+ waitUntilObserved(
+ changeModel.mergeable$,
+ mergeable => mergeable === false
+ );
+ assert.isTrue(getMergeableStub.calledOnce);
+ });
+ });
+
test('load a change', async () => {
const promise = mockPromise<ParsedChangeInfo | undefined>();
const stub = stubRestApi('getChangeDetail').callsFake(() => promise);
diff --git a/polygerrit-ui/app/test/mocks/gr-rest-api_mock.ts b/polygerrit-ui/app/test/mocks/gr-rest-api_mock.ts
index b60ace5..f8e4160 100644
--- a/polygerrit-ui/app/test/mocks/gr-rest-api_mock.ts
+++ b/polygerrit-ui/app/test/mocks/gr-rest-api_mock.ts
@@ -67,6 +67,7 @@
createChange,
createCommit,
createConfig,
+ createMergeable,
createPreferences,
createServerInfo,
createSubmittedTogetherInfo,
@@ -368,7 +369,7 @@
return Promise.resolve(true);
},
getMergeable(): Promise<MergeableInfo | undefined> {
- throw new Error('getMergeable() not implemented by RestApiMock.');
+ return Promise.resolve(createMergeable());
},
getPlugins(): Promise<{[p: string]: PluginInfo} | undefined> {
return Promise.resolve({});
diff --git a/polygerrit-ui/app/test/test-data-generators.ts b/polygerrit-ui/app/test/test-data-generators.ts
index a92e932..dce8e4b 100644
--- a/polygerrit-ui/app/test/test-data-generators.ts
+++ b/polygerrit-ui/app/test/test-data-generators.ts
@@ -672,10 +672,10 @@
};
}
-export function createMergeable(): MergeableInfo {
+export function createMergeable(mergeable = false): MergeableInfo {
return {
submit_type: SubmitType.MERGE_IF_NECESSARY,
- mergeable: false,
+ mergeable,
};
}