summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorBen Rohlfs <brohlfs@google.com>2022-09-28 12:57:47 +0000
committerBen Rohlfs <brohlfs@google.com>2022-09-28 12:57:47 +0000
commit27e9569414f0b19f70fb11c641cea0a3c9e78917 (patch)
tree3389b70af61f4555bc26ff0f16709ea255d9b6ee
parentc8a096deb789a0cabb41a7d8a40ce47823988b28 (diff)
Revert "Move behavior from component into view model for change-list-view"
This reverts commit c8a096deb789a0cabb41a7d8a40ce47823988b28. Reason for revert: Files are not shown on change page when not logged in. Change-Id: I9c5dafe4e76e0d34c0233b2a76c29d6645937dc3
-rw-r--r--polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view.ts149
-rw-r--r--polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view_test.ts124
-rw-r--r--polygerrit-ui/app/elements/core/gr-router/gr-router.ts4
-rw-r--r--polygerrit-ui/app/elements/core/gr-router/gr-router_test.ts6
-rw-r--r--polygerrit-ui/app/models/change/change-model.ts19
-rw-r--r--polygerrit-ui/app/models/change/files-model.ts11
-rw-r--r--polygerrit-ui/app/models/checks/checks-model.ts8
-rw-r--r--polygerrit-ui/app/models/comments/comments-model.ts9
-rw-r--r--polygerrit-ui/app/models/config/config-model.ts11
-rw-r--r--polygerrit-ui/app/models/model.ts8
-rw-r--r--polygerrit-ui/app/models/user/user-model.ts80
-rw-r--r--polygerrit-ui/app/models/views/search.ts149
-rw-r--r--polygerrit-ui/app/models/views/search_test.ts77
-rw-r--r--polygerrit-ui/app/services/app-context-init.ts11
-rw-r--r--polygerrit-ui/app/test/test-app-context-init.ts5
-rw-r--r--polygerrit-ui/app/test/test-data-generators.ts2
-rw-r--r--polygerrit-ui/app/utils/common-util.ts4
17 files changed, 306 insertions, 371 deletions
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view.ts b/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view.ts
index 6b74921ccb..a58c7bb14d 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view.ts
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view.ts
@@ -7,6 +7,7 @@ import '../gr-change-list/gr-change-list';
import '../gr-repo-header/gr-repo-header';
import '../gr-user-header/gr-user-header';
import {page} from '../../../utils/page-wrapper-utils';
+import {navigationToken} from '../../core/gr-navigation/gr-navigation';
import {
AccountDetailInfo,
AccountId,
@@ -25,9 +26,20 @@ import {ValueChangedEvent} from '../../../types/events';
import {
createSearchUrl,
searchViewModelToken,
+ SearchViewState,
} from '../../../models/views/search';
import {resolve} from '../../../models/dependency';
import {subscribe} from '../../lit/subscription-controller';
+import {createChangeUrl} from '../../../models/views/change';
+import {debounce, DelayedTask} from '../../../utils/async-util';
+
+const GET_CHANGES_DEBOUNCE_INTERVAL_MS = 10;
+
+const LOOKUP_QUERY_PATTERNS: RegExp[] = [
+ /^\s*i?[0-9a-f]{7,40}\s*$/i, // CHANGE_ID
+ /^\s*[1-9][0-9]*\s*$/g, // CHANGE_NUM
+ /[0-9a-f]{40}/, // COMMIT
+];
const USER_QUERY_PATTERN = /^owner:\s?("[^"]+"|[^ ]+)$/;
@@ -48,6 +60,21 @@ export class GrChangeListView extends LitElement {
@query('#nextArrow') protected nextArrow?: HTMLAnchorElement;
+ private _viewState?: SearchViewState;
+
+ @state()
+ get viewState() {
+ return this._viewState;
+ }
+
+ set viewState(viewState: SearchViewState | undefined) {
+ if (this._viewState === viewState) return;
+ const oldViewState = this._viewState;
+ this._viewState = viewState;
+ this.viewStateChanged();
+ this.requestUpdate('viewState', oldViewState);
+ }
+
// private but used in test
@state() account?: AccountDetailInfo;
@@ -64,19 +91,19 @@ export class GrChangeListView extends LitElement {
@state() query = '';
// private but used in test
- @state() offset = 0;
+ @state() offset?: number;
// private but used in test
- @state() changes: ChangeInfo[] = [];
+ @state() changes?: ChangeInfo[];
// private but used in test
@state() loading = true;
// private but used in test
- @state() userId?: AccountId | EmailAddress;
+ @state() userId: AccountId | EmailAddress | null = null;
// private but used in test
- @state() repo?: RepoName;
+ @state() repo: RepoName | null = null;
@state() selectedIndex = 0;
@@ -88,30 +115,17 @@ export class GrChangeListView extends LitElement {
private readonly getViewModel = resolve(this, searchViewModelToken);
+ private readonly getNavigation = resolve(this, navigationToken);
+
constructor() {
super();
this.addEventListener('next-page', () => this.handleNextPage());
this.addEventListener('previous-page', () => this.handlePreviousPage());
-
- subscribe(
- this,
- () => this.getViewModel().query$,
- x => (this.query = x)
- );
- subscribe(
- this,
- () => this.getViewModel().offsetNumber$,
- x => (this.offset = x)
- );
+ this.addEventListener('reload', () => this.reload());
subscribe(
this,
- () => this.getViewModel().loading$,
- x => (this.loading = x)
- );
- subscribe(
- this,
- () => this.getViewModel().changes$,
- x => (this.changes = x)
+ () => this.getViewModel().state$,
+ x => (this.viewState = x)
);
subscribe(
this,
@@ -125,16 +139,22 @@ export class GrChangeListView extends LitElement {
);
subscribe(
this,
- () => this.userModel.preferenceChangesPerPage$,
- x => (this.changesPerPage = x)
- );
- subscribe(
- this,
() => this.userModel.preferences$,
- x => (this.preferences = x)
+ x => {
+ this.preferences = x;
+ if (this.changesPerPage !== x.changes_per_page) {
+ this.changesPerPage = x.changes_per_page;
+ this.debouncedGetChanges();
+ }
+ }
);
}
+ override disconnectedCallback() {
+ this.getChangesTask?.flush();
+ super.disconnectedCallback();
+ }
+
static override get styles() {
return [
sharedStyles,
@@ -262,10 +282,63 @@ export class GrChangeListView extends LitElement {
}
}
- override updated(changedProperties: PropertyValues) {
- if (changedProperties.has('query')) {
- fireTitleChange(this, this.query);
+ reload() {
+ if (!this.loading) this.debouncedGetChanges();
+ }
+
+ // private, but visible for testing
+ viewStateChanged() {
+ if (!this.viewState) return;
+
+ let offset = Number(this.viewState.offset);
+ if (isNaN(offset)) offset = 0;
+ const query = this.viewState.query ?? '';
+
+ if (this.query !== query) this.selectedIndex = 0;
+ this.loading = true;
+ this.query = query;
+ this.offset = offset;
+
+ // NOTE: This method may be called before attachment. Fire title-change
+ // in an async so that attachment to the DOM can take place first.
+ setTimeout(() => fireTitleChange(this, this.query));
+
+ this.debouncedGetChanges(true);
+ }
+
+ private getChangesTask?: DelayedTask;
+
+ private debouncedGetChanges(shouldSingleMatchRedirect = false) {
+ this.getChangesTask = debounce(
+ this.getChangesTask,
+ () => {
+ this.getChanges(shouldSingleMatchRedirect);
+ },
+ GET_CHANGES_DEBOUNCE_INTERVAL_MS
+ );
+ }
+
+ async getChanges(shouldSingleMatchRedirect = false) {
+ this.loading = true;
+ const changes =
+ (await this.restApiService.getChanges(
+ this.changesPerPage,
+ this.query,
+ this.offset
+ )) ?? [];
+ if (shouldSingleMatchRedirect && this.query && changes.length === 1) {
+ for (const queryPattern of LOOKUP_QUERY_PATTERNS) {
+ if (this.query.match(queryPattern)) {
+ // "Back"/"Forward" buttons work correctly only with replaceUrl()
+ this.getNavigation().replaceUrl(
+ createChangeUrl({change: changes[0]})
+ );
+ return;
+ }
+ }
}
+ this.changes = changes;
+ this.loading = false;
}
// private but used in test
@@ -299,12 +372,14 @@ export class GrChangeListView extends LitElement {
}
private changesChanged() {
- this.selectedIndex = 0;
- this.userId = undefined;
- this.repo = undefined;
- if (this.changes.length === 0) return;
+ this.userId = null;
+ this.repo = null;
+ const changes = this.changes;
+ if (!changes || !changes.length) {
+ return;
+ }
if (USER_QUERY_PATTERN.test(this.query)) {
- const owner = this.changes[0].owner;
+ const owner = changes[0].owner;
const userId = owner._account_id ? owner._account_id : owner.email;
if (userId) {
this.userId = userId;
@@ -312,7 +387,7 @@ export class GrChangeListView extends LitElement {
}
}
if (REPO_QUERY_PATTERN.test(this.query)) {
- this.repo = this.changes[0].project;
+ this.repo = changes[0].project;
}
}
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view_test.ts b/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view_test.ts
index 4dc3701cb0..b003b66f9d 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view_test.ts
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view_test.ts
@@ -7,20 +7,45 @@ import '../../../test/common-test-setup';
import './gr-change-list-view';
import {GrChangeListView} from './gr-change-list-view';
import {page} from '../../../utils/page-wrapper-utils';
-import {query, queryAndAssert} from '../../../test/test-utils';
+import {navigationToken} from '../../core/gr-navigation/gr-navigation';
+import {
+ query,
+ stubRestApi,
+ queryAndAssert,
+ stubFlags,
+} from '../../../test/test-utils';
import {createChange} from '../../../test/test-data-generators';
-import {ChangeInfo, EmailAddress, RepoName} from '../../../api/rest-api';
+import {
+ ChangeInfo,
+ EmailAddress,
+ NumericChangeId,
+ RepoName,
+} from '../../../api/rest-api';
import {fixture, html, waitUntil, assert} from '@open-wc/testing';
+import {GerritView} from '../../../services/router/router-model';
+import {testResolver} from '../../../test/common-test-setup';
+import {SinonFakeTimers, SinonStub} from 'sinon';
import {GrChangeList} from '../gr-change-list/gr-change-list';
import {GrChangeListSection} from '../gr-change-list-section/gr-change-list-section';
import {GrChangeListItem} from '../gr-change-list-item/gr-change-list-item';
+const CHANGE_ID = 'IcA3dAB3edAB9f60B8dcdA6ef71A75980e4B7127';
+const COMMIT_HASH = '12345678';
+
suite('gr-change-list-view tests', () => {
let element: GrChangeListView;
+ let changes: ChangeInfo[] | undefined = [];
+ let clock: SinonFakeTimers;
setup(async () => {
+ clock = sinon.useFakeTimers();
+ stubRestApi('getChanges').callsFake(() => Promise.resolve(changes));
element = await fixture(html`<gr-change-list-view></gr-change-list-view>`);
- element.query = 'test-query';
+ element.viewState = {
+ view: GerritView.SEARCH,
+ query: 'test-query',
+ offset: '0',
+ };
await element.updateComplete;
});
@@ -50,9 +75,11 @@ suite('gr-change-list-view tests', () => {
suite('bulk actions', () => {
setup(async () => {
+ stubFlags('isEnabled').returns(true);
+ changes = [createChange()];
element.loading = false;
- element.changes = [createChange()];
- await element.updateComplete;
+ element.reload();
+ clock.tick(100);
await element.updateComplete;
await waitUntil(() => element.loading === false);
});
@@ -80,9 +107,8 @@ suite('gr-change-list-view tests', () => {
checkbox.click();
await waitUntil(() => checkbox.checked);
- element.changes = [createChange()];
+ element.reload();
await element.updateComplete;
-
checkbox = queryAndAssert<HTMLInputElement>(
query(
query(query(element, 'gr-change-list'), 'gr-change-list-section'),
@@ -196,7 +222,7 @@ suite('gr-change-list-view tests', () => {
});
test('userId query', async () => {
- assert.isUndefined(element.userId);
+ assert.isNull(element.userId);
element.query = 'owner: foo@bar';
element.changes = [
{...createChange(), owner: {email: 'foo@bar' as EmailAddress}},
@@ -209,19 +235,19 @@ suite('gr-change-list-view tests', () => {
{...createChange(), owner: {email: 'foo@bar' as EmailAddress}},
];
await element.updateComplete;
- assert.isUndefined(element.userId);
+ assert.isNull(element.userId);
});
test('userId query without email', async () => {
- assert.isUndefined(element.userId);
+ assert.isNull(element.userId);
element.query = 'owner: foo@bar';
element.changes = [{...createChange(), owner: {}}];
await element.updateComplete;
- assert.isUndefined(element.userId);
+ assert.isNull(element.userId);
});
test('repo query', async () => {
- assert.isUndefined(element.repo);
+ assert.isNull(element.repo);
element.query = 'project: test-repo';
element.changes = [
{
@@ -238,11 +264,11 @@ suite('gr-change-list-view tests', () => {
{...createChange(), owner: {email: 'foo@bar' as EmailAddress}},
];
await element.updateComplete;
- assert.isUndefined(element.repo);
+ assert.isNull(element.repo);
});
test('repo query with open status', async () => {
- assert.isUndefined(element.repo);
+ assert.isNull(element.repo);
element.query = 'project:test-repo status:open';
element.changes = [
{
@@ -259,6 +285,74 @@ suite('gr-change-list-view tests', () => {
{...createChange(), owner: {email: 'foo@bar' as EmailAddress}},
];
await element.updateComplete;
- assert.isUndefined(element.repo);
+ assert.isNull(element.repo);
+ });
+
+ suite('query based navigation', () => {
+ let replaceUrlStub: SinonStub;
+ setup(() => {
+ replaceUrlStub = sinon.stub(testResolver(navigationToken), 'replaceUrl');
+ });
+
+ teardown(async () => {
+ await element.updateComplete;
+ sinon.restore();
+ });
+
+ test('Searching for a change ID redirects to change', async () => {
+ const change = {...createChange(), _number: 1 as NumericChangeId};
+ changes = [change];
+
+ element.viewState = {view: GerritView.SEARCH, query: CHANGE_ID};
+ clock.tick(100);
+ await element.updateComplete;
+
+ assert.isTrue(replaceUrlStub.called);
+ assert.equal(replaceUrlStub.lastCall.firstArg, '/c/test-project/+/1');
+ });
+
+ test('Searching for a change num redirects to change', async () => {
+ const change = {...createChange(), _number: 1 as NumericChangeId};
+ changes = [change];
+
+ element.viewState = {view: GerritView.SEARCH, query: '1'};
+ clock.tick(100);
+ await element.updateComplete;
+
+ assert.isTrue(replaceUrlStub.called);
+ assert.equal(replaceUrlStub.lastCall.firstArg, '/c/test-project/+/1');
+ });
+
+ test('Commit hash redirects to change', async () => {
+ const change = {...createChange(), _number: 1 as NumericChangeId};
+ changes = [change];
+
+ element.viewState = {view: GerritView.SEARCH, query: COMMIT_HASH};
+ clock.tick(100);
+ await element.updateComplete;
+
+ assert.isTrue(replaceUrlStub.called);
+ assert.equal(replaceUrlStub.lastCall.firstArg, '/c/test-project/+/1');
+ });
+
+ test('Searching for an invalid change ID searches', async () => {
+ changes = [];
+
+ element.viewState = {view: GerritView.SEARCH, query: CHANGE_ID};
+ clock.tick(100);
+ await element.updateComplete;
+
+ assert.isFalse(replaceUrlStub.called);
+ });
+
+ test('Change ID with multiple search results searches', async () => {
+ changes = undefined;
+
+ element.viewState = {view: GerritView.SEARCH, query: CHANGE_ID};
+ clock.tick(100);
+ await element.updateComplete;
+
+ assert.isFalse(replaceUrlStub.called);
+ });
});
});
diff --git a/polygerrit-ui/app/elements/core/gr-router/gr-router.ts b/polygerrit-ui/app/elements/core/gr-router/gr-router.ts
index e40a1619a1..762f6b70ce 100644
--- a/polygerrit-ui/app/elements/core/gr-router/gr-router.ts
+++ b/polygerrit-ui/app/elements/core/gr-router/gr-router.ts
@@ -1292,8 +1292,6 @@ export class GrRouter implements Finalizable, NavigationService {
view: GerritView.SEARCH,
query: ctx.params[0],
offset: ctx.params[2],
- changes: [],
- loading: false,
};
this.setState(state);
this.searchViewModel.setState(state);
@@ -1306,8 +1304,6 @@ export class GrRouter implements Finalizable, NavigationService {
const state: SearchViewState = {
view: GerritView.SEARCH,
query: ctx.params[0],
- changes: [],
- loading: false,
};
this.setState(state);
this.searchViewModel.setState(state);
diff --git a/polygerrit-ui/app/elements/core/gr-router/gr-router_test.ts b/polygerrit-ui/app/elements/core/gr-router/gr-router_test.ts
index 6c29336476..ce3ed0d68b 100644
--- a/polygerrit-ui/app/elements/core/gr-router/gr-router_test.ts
+++ b/polygerrit-ui/app/elements/core/gr-router/gr-router_test.ts
@@ -403,8 +403,6 @@ suite('gr-router tests', () => {
view: GerritView.SEARCH,
query: 'project:foo/bar/baz',
offset: undefined,
- changes: [],
- loading: false,
});
ctx.params[1] = '123';
@@ -413,8 +411,6 @@ suite('gr-router tests', () => {
view: GerritView.SEARCH,
query: 'project:foo/bar/baz',
offset: '123',
- changes: [],
- loading: false,
});
});
@@ -433,8 +429,6 @@ suite('gr-router tests', () => {
assertctxToParams(ctx, 'handleChangeIdQueryRoute', {
view: GerritView.SEARCH,
query: 'I0123456789abcdef0123456789abcdef01234567',
- changes: [],
- loading: false,
});
});
diff --git a/polygerrit-ui/app/models/change/change-model.ts b/polygerrit-ui/app/models/change/change-model.ts
index b4f16abcc8..5cfe670fbc 100644
--- a/polygerrit-ui/app/models/change/change-model.ts
+++ b/polygerrit-ui/app/models/change/change-model.ts
@@ -14,7 +14,15 @@ import {
RevisionPatchSetNum,
} from '../../types/common';
import {DefaultBase} from '../../constants/constants';
-import {combineLatest, from, fromEvent, Observable, forkJoin, of} from 'rxjs';
+import {
+ combineLatest,
+ from,
+ fromEvent,
+ Observable,
+ Subscription,
+ forkJoin,
+ of,
+} from 'rxjs';
import {
map,
filter,
@@ -253,6 +261,8 @@ export class ChangeModel extends Model<ChangeState> implements Finalizable {
([change, account]) => isOwner(change, account)
);
+ private subscriptions: Subscription[] = [];
+
// For usage in `combineLatest` we need `startWith` such that reload$ has an
// initial value.
readonly reload$: Observable<unknown> = fromEvent(document, 'reload').pipe(
@@ -306,6 +316,13 @@ export class ChangeModel extends Model<ChangeState> implements Finalizable {
];
}
+ override finalize() {
+ for (const s of this.subscriptions) {
+ s.unsubscribe();
+ }
+ this.subscriptions = [];
+ }
+
// Temporary workaround until path is derived in the model itself.
updatePath(diffPath?: string) {
this.updateState({diffPath});
diff --git a/polygerrit-ui/app/models/change/files-model.ts b/polygerrit-ui/app/models/change/files-model.ts
index 6922f6dce5..31e42a6090 100644
--- a/polygerrit-ui/app/models/change/files-model.ts
+++ b/polygerrit-ui/app/models/change/files-model.ts
@@ -12,7 +12,7 @@ import {
PatchSetNumber,
RevisionPatchSetNum,
} from '../../types/common';
-import {combineLatest, of, from} from 'rxjs';
+import {combineLatest, Subscription, of, from} from 'rxjs';
import {switchMap, map} from 'rxjs/operators';
import {RestApiService} from '../../services/gr-rest-api/gr-rest-api';
import {Finalizable} from '../../services/registry';
@@ -131,6 +131,8 @@ export class FilesModel extends Model<FilesState> implements Finalizable {
state => state.filesRightBase
);
+ private subscriptions: Subscription[] = [];
+
constructor(
readonly changeModel: ChangeModel,
readonly commentsModel: CommentsModel,
@@ -196,4 +198,11 @@ export class FilesModel extends Model<FilesState> implements Finalizable {
this.updateState(state);
});
}
+
+ override finalize() {
+ for (const s of this.subscriptions) {
+ s.unsubscribe();
+ }
+ this.subscriptions = [];
+ }
}
diff --git a/polygerrit-ui/app/models/checks/checks-model.ts b/polygerrit-ui/app/models/checks/checks-model.ts
index 61c2f77ad7..bb46f278de 100644
--- a/polygerrit-ui/app/models/checks/checks-model.ts
+++ b/polygerrit-ui/app/models/checks/checks-model.ts
@@ -20,6 +20,7 @@ import {
Observable,
of,
Subject,
+ Subscription,
timer,
} from 'rxjs';
import {
@@ -207,6 +208,8 @@ export class ChecksModel extends Model<ChecksState> implements Finalizable {
private readonly visibilityChangeListener: () => void;
+ private subscriptions: Subscription[] = [];
+
public checksSelectedPatchsetNumber$ = select(
this.state$,
state => state.patchsetNumberSelected
@@ -480,7 +483,10 @@ export class ChecksModel extends Model<ChecksState> implements Finalizable {
'visibilitychange',
this.visibilityChangeListener
);
- super.finalize();
+ for (const s of this.subscriptions) {
+ s.unsubscribe();
+ }
+ this.subscriptions = [];
}
// Must only be used by the checks service or whatever is in control of this
diff --git a/polygerrit-ui/app/models/comments/comments-model.ts b/polygerrit-ui/app/models/comments/comments-model.ts
index 8ed4b69cc7..39d9eaaabf 100644
--- a/polygerrit-ui/app/models/comments/comments-model.ts
+++ b/polygerrit-ui/app/models/comments/comments-model.ts
@@ -30,7 +30,7 @@ import {select} from '../../utils/observable-util';
import {RouterModel} from '../../services/router/router-model';
import {Finalizable} from '../../services/registry';
import {define} from '../dependency';
-import {combineLatest, forkJoin, from, Observable} from 'rxjs';
+import {combineLatest, forkJoin, from, Observable, Subscription} from 'rxjs';
import {fire, fireAlert, fireEvent} from '../../utils/event-util';
import {CURRENT} from '../../utils/patch-set-util';
import {RestApiService} from '../../services/gr-rest-api/gr-rest-api';
@@ -370,6 +370,8 @@ export class CommentsModel extends Model<CommentState> implements Finalizable {
private readonly reloadListener: () => void;
+ private readonly subscriptions: Subscription[] = [];
+
private drafts: {[path: string]: DraftInfo[]} = {};
private draftToastTask?: DelayedTask;
@@ -419,7 +421,10 @@ export class CommentsModel extends Model<CommentState> implements Finalizable {
override finalize() {
document.removeEventListener('reload', this.reloadListener);
- super.finalize();
+ for (const s of this.subscriptions) {
+ s.unsubscribe();
+ }
+ this.subscriptions.splice(0, this.subscriptions.length);
}
// Note that this does *not* reload ported comments.
diff --git a/polygerrit-ui/app/models/config/config-model.ts b/polygerrit-ui/app/models/config/config-model.ts
index 6e374d1ca2..c4bd637885 100644
--- a/polygerrit-ui/app/models/config/config-model.ts
+++ b/polygerrit-ui/app/models/config/config-model.ts
@@ -4,7 +4,7 @@
* SPDX-License-Identifier: Apache-2.0
*/
import {ConfigInfo, RepoName, ServerInfo} from '../../types/common';
-import {from, of} from 'rxjs';
+import {from, of, Subscription} from 'rxjs';
import {switchMap} from 'rxjs/operators';
import {Finalizable} from '../../services/registry';
import {RestApiService} from '../../services/gr-rest-api/gr-rest-api';
@@ -50,6 +50,8 @@ export class ConfigModel extends Model<ConfigState> implements Finalizable {
url => url
);
+ private subscriptions: Subscription[];
+
constructor(
readonly changeModel: ChangeModel,
readonly restApiService: RestApiService
@@ -81,4 +83,11 @@ export class ConfigModel extends Model<ConfigState> implements Finalizable {
updateServerConfig(serverConfig?: ServerInfo) {
this.updateState({serverConfig});
}
+
+ override finalize() {
+ for (const s of this.subscriptions) {
+ s.unsubscribe();
+ }
+ this.subscriptions = [];
+ }
}
diff --git a/polygerrit-ui/app/models/model.ts b/polygerrit-ui/app/models/model.ts
index c44c0bf4f8..7cd8b2a9a5 100644
--- a/polygerrit-ui/app/models/model.ts
+++ b/polygerrit-ui/app/models/model.ts
@@ -3,7 +3,7 @@
* Copyright 2021 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
-import {BehaviorSubject, Observable, Subscription} from 'rxjs';
+import {BehaviorSubject, Observable} from 'rxjs';
import {Finalizable} from '../services/registry';
/**
@@ -25,8 +25,6 @@ export abstract class Model<T> implements Finalizable {
public state$: Observable<T>;
- protected subscriptions: Subscription[] = [];
-
constructor(initialState: T) {
this.subject$ = new BehaviorSubject(initialState);
this.state$ = this.subject$.asObservable();
@@ -46,9 +44,5 @@ export abstract class Model<T> implements Finalizable {
finalize() {
this.subject$.complete();
- for (const s of this.subscriptions) {
- s.unsubscribe();
- }
- this.subscriptions = [];
}
}
diff --git a/polygerrit-ui/app/models/user/user-model.ts b/polygerrit-ui/app/models/user/user-model.ts
index bb607e9271..b69c7150e7 100644
--- a/polygerrit-ui/app/models/user/user-model.ts
+++ b/polygerrit-ui/app/models/user/user-model.ts
@@ -3,8 +3,8 @@
* Copyright 2021 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
-import {from, of, Observable} from 'rxjs';
-import {filter, switchMap} from 'rxjs/operators';
+import {from, of, Observable, Subscription} from 'rxjs';
+import {switchMap} from 'rxjs/operators';
import {
DiffPreferencesInfo as DiffPreferencesInfoAPI,
DiffViewMode,
@@ -26,62 +26,24 @@ import {DiffPreferencesInfo} from '../../types/diff';
import {Finalizable} from '../../services/registry';
import {select} from '../../utils/observable-util';
import {Model} from '../model';
-import {isDefined} from '../../utils/common-util';
export interface UserState {
/**
* Keeps being defined even when credentials have expired.
- *
- * `undefined` can mean that the app is still starting up and we have not
- * tried loading an account object yet. If you want to wait until the
- * `account` is known, then use `accountLoaded` below.
*/
account?: AccountDetailInfo;
- /**
- * Starts as `false` and switches to `true` after the first `getAccount` call.
- * A common use case for this is to wait with loading or doing something until
- * we know whether the user is logged in or not, see `loadedAccount$` below.
- *
- * This value cannot change back to `false` once it has become `true`.
- *
- * This value does *not* indicate whether the user is logged in or whether an
- * `account` object is available. If the first `getAccount()` call returns
- * `undefined`, then `accountLoaded` still becomes true, even if `account`
- * stays `undefined`.
- */
- accountLoaded: boolean;
- preferences?: PreferencesInfo;
- diffPreferences?: DiffPreferencesInfo;
- editPreferences?: EditPreferencesInfo;
+ preferences: PreferencesInfo;
+ diffPreferences: DiffPreferencesInfo;
+ editPreferences: EditPreferencesInfo;
capabilities?: AccountCapabilityInfo;
}
export class UserModel extends Model<UserState> implements Finalizable {
- /**
- * Note that the initially emitted `undefined` value can mean "not loaded
- * the account into object yet" or "user is not logged in". Consider using
- * `loadedAccount$` below.
- *
- * TODO: Maybe consider changing all usages to `loadedAccount$`.
- */
readonly account$: Observable<AccountDetailInfo | undefined> = select(
this.state$,
userState => userState.account
);
- /**
- * Only emits once we have tried to actually load the account. Note that
- * this does not initially emit a value.
- *
- * So if this emits `undefined`, then you actually know that the user is not
- * logged in. And for logged in users you will never get an initial
- * `undefined` emission.
- */
- readonly loadedAccount$: Observable<AccountDetailInfo | undefined> = select(
- this.state$.pipe(filter(s => s.accountLoaded)),
- userState => userState.account
- );
-
/** Note that this may still be true, even if credentials have expired. */
readonly loggedIn$: Observable<boolean> = select(
this.account$,
@@ -99,17 +61,17 @@ export class UserModel extends Model<UserState> implements Finalizable {
readonly preferences$: Observable<PreferencesInfo> = select(
this.state$,
userState => userState.preferences
- ).pipe(filter(isDefined));
+ );
readonly diffPreferences$: Observable<DiffPreferencesInfo> = select(
this.state$,
userState => userState.diffPreferences
- ).pipe(filter(isDefined));
+ );
readonly editPreferences$: Observable<EditPreferencesInfo> = select(
this.state$,
userState => userState.editPreferences
- ).pipe(filter(isDefined));
+ );
readonly preferenceDiffViewMode$: Observable<DiffViewMode> = select(
this.preferences$,
@@ -121,14 +83,13 @@ export class UserModel extends Model<UserState> implements Finalizable {
preference => preference.theme
);
- readonly preferenceChangesPerPage$: Observable<number> = select(
- this.preferences$,
- preference => preference.changes_per_page
- );
+ private subscriptions: Subscription[] = [];
constructor(readonly restApiService: RestApiService) {
super({
- accountLoaded: false,
+ preferences: createDefaultPreferences(),
+ diffPreferences: createDefaultDiffPrefs(),
+ editPreferences: createDefaultEditPrefs(),
});
this.subscriptions = [
from(this.restApiService.getAccount()).subscribe(
@@ -136,7 +97,7 @@ export class UserModel extends Model<UserState> implements Finalizable {
this.setAccount(account);
}
),
- this.loadedAccount$
+ this.account$
.pipe(
switchMap(account => {
if (!account) return of(createDefaultPreferences());
@@ -146,7 +107,7 @@ export class UserModel extends Model<UserState> implements Finalizable {
.subscribe((preferences?: PreferencesInfo) => {
this.setPreferences(preferences ?? createDefaultPreferences());
}),
- this.loadedAccount$
+ this.account$
.pipe(
switchMap(account => {
if (!account) return of(createDefaultDiffPrefs());
@@ -156,7 +117,7 @@ export class UserModel extends Model<UserState> implements Finalizable {
.subscribe((diffPrefs?: DiffPreferencesInfoAPI) => {
this.setDiffPreferences(diffPrefs ?? createDefaultDiffPrefs());
}),
- this.loadedAccount$
+ this.account$
.pipe(
switchMap(account => {
if (!account) return of(createDefaultEditPrefs());
@@ -166,7 +127,7 @@ export class UserModel extends Model<UserState> implements Finalizable {
.subscribe((editPrefs?: EditPreferencesInfo) => {
this.setEditPreferences(editPrefs ?? createDefaultEditPrefs());
}),
- this.loadedAccount$
+ this.account$
.pipe(
switchMap(account => {
if (!account) return of(undefined);
@@ -179,6 +140,13 @@ export class UserModel extends Model<UserState> implements Finalizable {
];
}
+ override finalize() {
+ for (const s of this.subscriptions) {
+ s.unsubscribe();
+ }
+ this.subscriptions = [];
+ }
+
updatePreferences(prefs: Partial<PreferencesInfo>) {
return this.restApiService
.savePreferences(prefs)
@@ -237,6 +205,6 @@ export class UserModel extends Model<UserState> implements Finalizable {
}
setAccount(account?: AccountDetailInfo) {
- this.updateState({account, accountLoaded: true});
+ this.updateState({account});
}
}
diff --git a/polygerrit-ui/app/models/views/search.ts b/polygerrit-ui/app/models/views/search.ts
index d601488287..58cb8f7ab5 100644
--- a/polygerrit-ui/app/models/views/search.ts
+++ b/polygerrit-ui/app/models/views/search.ts
@@ -3,61 +3,18 @@
* Copyright 2022 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
-import {combineLatest, fromEvent, Observable} from 'rxjs';
-import {
- filter,
- map,
- startWith,
- switchMap,
- tap,
- withLatestFrom,
-} from 'rxjs/operators';
-import {RepoName, BranchName, TopicName, ChangeInfo} from '../../api/rest-api';
-import {NavigationService} from '../../elements/core/gr-navigation/gr-navigation';
-import {RestApiService} from '../../services/gr-rest-api/gr-rest-api';
+import {RepoName, BranchName, TopicName} from '../../api/rest-api';
import {GerritView} from '../../services/router/router-model';
-import {select} from '../../utils/observable-util';
import {addQuotesWhen} from '../../utils/string-util';
import {encodeURL} from '../../utils/url-util';
import {define} from '../dependency';
import {Model} from '../model';
-import {UserModel} from '../user/user-model';
import {ViewState} from './base';
-import {createChangeUrl} from './change';
-
-const LOOKUP_QUERY_PATTERNS: RegExp[] = [
- /^\s*i?[0-9a-f]{7,40}\s*$/i, // CHANGE_ID
- /^\s*[1-9][0-9]*\s*$/g, // CHANGE_NUM
- /[0-9a-f]{40}/, // COMMIT
-];
export interface SearchViewState extends ViewState {
view: GerritView.SEARCH;
- /**
- * The query for searching changes.
- *
- * Changing this to something non-empty will trigger search.
- */
- query: string;
- /**
- * How many initial search results should be skipped? This is for showing
- * more than one search result page. This must be a non-negative number.
- * If the string is not provided or cannot be parsed as expected, then the
- * offset falls back to 0.
- *
- * TODO: Consider converting from string to number before writing to the
- * state object.
- */
+ query?: string;
offset?: string;
-
- /**
- * Is a search API call currrently in progress?
- */
- loading: boolean;
- /**
- * The search results for the current query.
- */
- changes: ChangeInfo[];
}
export interface SearchUrlOptions {
@@ -129,108 +86,8 @@ export function createSearchUrl(params: SearchUrlOptions): string {
export const searchViewModelToken =
define<SearchViewModel>('search-view-model');
-/**
- * This is the view model for the search page.
- *
- * It keeps track of the overall search view state and provides selectors for
- * subscribing to certain slices of the state.
- *
- * It manages loading the changes to be shown on the search page by providing
- * `changes` in its state. Changes to the view state or certain user preferences
- * will automatically trigger reloading the changes.
- */
export class SearchViewModel extends Model<SearchViewState | undefined> {
- public readonly query$ = select(this.state$, s => s?.query ?? '');
-
- private readonly offset$ = select(this.state$, s => s?.offset ?? '0');
-
- /**
- * Convenience selector for getting the `offset` as a number.
- *
- * TODO: Consider changing the type of `offset$` and `state.offset` to
- * `number`.
- */
- public readonly offsetNumber$ = select(this.offset$, offset => {
- const offsetNumber = Number(offset);
- return Number.isFinite(offsetNumber) ? offsetNumber : 0;
- });
-
- public readonly changes$ = select(this.state$, s => s?.changes ?? []);
-
- public readonly loading$ = select(this.state$, s => s?.loading ?? false);
-
- // For usage in `combineLatest` we need `startWith` such that reload$ has an
- // initial value.
- private readonly reload$: Observable<unknown> = fromEvent(
- document,
- 'reload'
- ).pipe(startWith(undefined));
-
- private readonly reloadChangesTrigger$ = combineLatest([
- this.reload$,
- this.query$,
- this.offsetNumber$,
- this.userModel.preferenceChangesPerPage$,
- ]).pipe(
- map(([_reload, query, offsetNumber, changesPerPage]) => {
- const params: [string, number, number] = [
- query,
- offsetNumber,
- changesPerPage,
- ];
- return params;
- })
- );
-
- constructor(
- private readonly restApiService: RestApiService,
- private readonly userModel: UserModel,
- private readonly getNavigation: () => NavigationService
- ) {
+ constructor() {
super(undefined);
- this.subscriptions = [
- this.reloadChangesTrigger$
- .pipe(
- switchMap(a => this.reloadChanges(a)),
- tap(changes => this.updateState({changes, loading: false}))
- )
- .subscribe(),
- this.changes$
- .pipe(
- filter(changes => changes.length === 1),
- withLatestFrom(this.query$)
- )
- .subscribe(([changes, query]) =>
- this.redirectSingleResult(query, changes)
- ),
- ];
- }
-
- private async reloadChanges([query, offset, changesPerPage]: [
- string,
- number,
- number
- ]): Promise<ChangeInfo[]> {
- if (this.getState() === undefined) return [];
- if (query.trim().length === 0) return [];
- this.updateState({loading: true});
- const changes = await this.restApiService.getChanges(
- changesPerPage,
- query,
- offset
- );
- return changes ?? [];
- }
-
- // visible for testing
- redirectSingleResult(query: string, changes: ChangeInfo[]): void {
- if (changes.length !== 1) return;
- for (const queryPattern of LOOKUP_QUERY_PATTERNS) {
- if (query.match(queryPattern)) {
- // "Back"/"Forward" buttons work correctly only with replaceUrl()
- this.getNavigation().replaceUrl(createChangeUrl({change: changes[0]}));
- return;
- }
- }
}
}
diff --git a/polygerrit-ui/app/models/views/search_test.ts b/polygerrit-ui/app/models/views/search_test.ts
index bb2c0e33f5..138ce1ec09 100644
--- a/polygerrit-ui/app/models/views/search_test.ts
+++ b/polygerrit-ui/app/models/views/search_test.ts
@@ -4,26 +4,9 @@
* SPDX-License-Identifier: Apache-2.0
*/
import {assert} from '@open-wc/testing';
-import {SinonStub} from 'sinon';
-import {
- BranchName,
- NumericChangeId,
- RepoName,
- TopicName,
-} from '../../api/rest-api';
-import {navigationToken} from '../../elements/core/gr-navigation/gr-navigation';
+import {BranchName, RepoName, TopicName} from '../../api/rest-api';
import '../../test/common-test-setup';
-import {testResolver} from '../../test/common-test-setup';
-import {createChange} from '../../test/test-data-generators';
-import {
- createSearchUrl,
- SearchUrlOptions,
- SearchViewModel,
- searchViewModelToken,
-} from './search';
-
-const CHANGE_ID = 'IcA3dAB3edAB9f60B8dcdA6ef71A75980e4B7127';
-const COMMIT_HASH = '12345678';
+import {createSearchUrl, SearchUrlOptions} from './search';
suite('search view state tests', () => {
test('createSearchUrl', () => {
@@ -70,60 +53,4 @@ suite('search view state tests', () => {
options = {topic: 'test:test' as TopicName};
assert.equal(createSearchUrl(options), '/q/topic:"test:test"');
});
-
- suite('query based navigation', () => {
- let replaceUrlStub: SinonStub;
- let model: SearchViewModel;
-
- setup(() => {
- model = testResolver(searchViewModelToken);
- replaceUrlStub = sinon.stub(testResolver(navigationToken), 'replaceUrl');
- });
-
- teardown(() => {
- model.finalize();
- });
-
- test('Searching for a change ID redirects to change', async () => {
- const change = {...createChange(), _number: 1 as NumericChangeId};
-
- model.redirectSingleResult(CHANGE_ID, [change]);
-
- assert.isTrue(replaceUrlStub.called);
- assert.equal(replaceUrlStub.lastCall.firstArg, '/c/test-project/+/1');
- });
-
- test('Searching for a change num redirects to change', async () => {
- const change = {...createChange(), _number: 1 as NumericChangeId};
-
- model.redirectSingleResult('1', [change]);
-
- assert.isTrue(replaceUrlStub.called);
- assert.equal(replaceUrlStub.lastCall.firstArg, '/c/test-project/+/1');
- });
-
- test('Commit hash redirects to change', async () => {
- const change = {...createChange(), _number: 1 as NumericChangeId};
-
- model.redirectSingleResult(COMMIT_HASH, [change]);
-
- assert.isTrue(replaceUrlStub.called);
- assert.equal(replaceUrlStub.lastCall.firstArg, '/c/test-project/+/1');
- });
-
- test('No results: no redirect', async () => {
- model.redirectSingleResult(CHANGE_ID, []);
-
- assert.isFalse(replaceUrlStub.called);
- });
-
- test('More than 1 result: no redirect', async () => {
- const change1 = {...createChange(), _number: 1 as NumericChangeId};
- const change2 = {...createChange(), _number: 2 as NumericChangeId};
-
- model.redirectSingleResult(CHANGE_ID, [change1, change2]);
-
- assert.isFalse(replaceUrlStub.called);
- });
- });
});
diff --git a/polygerrit-ui/app/services/app-context-init.ts b/polygerrit-ui/app/services/app-context-init.ts
index f74cfdffa4..9f7a0c09bf 100644
--- a/polygerrit-ui/app/services/app-context-init.ts
+++ b/polygerrit-ui/app/services/app-context-init.ts
@@ -57,10 +57,7 @@ import {GroupViewModel, groupViewModelToken} from '../models/views/group';
import {PluginViewModel, pluginViewModelToken} from '../models/views/plugin';
import {RepoViewModel, repoViewModelToken} from '../models/views/repo';
import {SearchViewModel, searchViewModelToken} from '../models/views/search';
-import {
- NavigationService,
- navigationToken,
-} from '../elements/core/gr-navigation/gr-navigation';
+import {navigationToken} from '../elements/core/gr-navigation/gr-navigation';
/**
* The AppContext lazy initializator for all services
@@ -134,11 +131,7 @@ export function createAppDependencies(
dependencies.set(pluginViewModelToken, pluginViewModel);
const repoViewModel = new RepoViewModel();
dependencies.set(repoViewModelToken, repoViewModel);
- const searchViewModel = new SearchViewModel(
- appContext.restApiService,
- appContext.userModel,
- () => dependencies.get(navigationToken) as unknown as NavigationService
- );
+ const searchViewModel = new SearchViewModel();
dependencies.set(searchViewModelToken, searchViewModel);
const settingsViewModel = new SettingsViewModel();
dependencies.set(settingsViewModelToken, settingsViewModel);
diff --git a/polygerrit-ui/app/test/test-app-context-init.ts b/polygerrit-ui/app/test/test-app-context-init.ts
index eec5446b98..e34773eb38 100644
--- a/polygerrit-ui/app/test/test-app-context-init.ts
+++ b/polygerrit-ui/app/test/test-app-context-init.ts
@@ -138,10 +138,7 @@ export function createTestDependencies(
dependencies.set(pluginViewModelToken, pluginViewModelCreator);
const repoViewModelCreator = () => new RepoViewModel();
dependencies.set(repoViewModelToken, repoViewModelCreator);
- const searchViewModelCreator = () =>
- new SearchViewModel(appContext.restApiService, appContext.userModel, () =>
- resolver(navigationToken)
- );
+ const searchViewModelCreator = () => new SearchViewModel();
dependencies.set(searchViewModelToken, searchViewModelCreator);
const settingsViewModelCreator = () => new SettingsViewModel();
dependencies.set(settingsViewModelToken, settingsViewModelCreator);
diff --git a/polygerrit-ui/app/test/test-data-generators.ts b/polygerrit-ui/app/test/test-data-generators.ts
index 310fe67569..3579bb370d 100644
--- a/polygerrit-ui/app/test/test-data-generators.ts
+++ b/polygerrit-ui/app/test/test-data-generators.ts
@@ -705,8 +705,6 @@ export function createAppElementSearchViewParams(): SearchViewState {
view: GerritView.SEARCH,
query: TEST_NUMERIC_CHANGE_ID.toString(),
offset: '0',
- changes: [],
- loading: false,
};
}
diff --git a/polygerrit-ui/app/utils/common-util.ts b/polygerrit-ui/app/utils/common-util.ts
index 6c2774eaa9..05c054b8ae 100644
--- a/polygerrit-ui/app/utils/common-util.ts
+++ b/polygerrit-ui/app/utils/common-util.ts
@@ -47,10 +47,6 @@ export function assert(
if (!condition) throw new Error(errorMessage);
}
-export function isDefined<T>(x: T): x is NonNullable<T> {
- return x !== undefined && x !== null;
-}
-
/**
* Throws an error if the property is not defined.
*/