Navigate to topic page if user queries for a topic

If the user queries for a topic and *only* queries for the topic and
nothing else, then navigate to the topic page instead of showing the
change list page.

Google-bug-id: b/201529567
Change-Id: I0e1a87f8e886dd281e31e878fa825962c44e90e8
diff --git a/polygerrit-ui/app/elements/core/gr-navigation/gr-navigation.ts b/polygerrit-ui/app/elements/core/gr-navigation/gr-navigation.ts
index 6b4006c..fcc853d 100644
--- a/polygerrit-ui/app/elements/core/gr-navigation/gr-navigation.ts
+++ b/polygerrit-ui/app/elements/core/gr-navigation/gr-navigation.ts
@@ -316,11 +316,17 @@
   commentLink?: boolean;
 }
 
+export interface GenerateUrlTopicViewParams {
+  view: GerritView.TOPIC;
+  topic?: string;
+}
+
 export type GenerateUrlParameters =
   | GenerateUrlSearchViewParameters
   | GenerateUrlChangeViewParameters
   | GenerateUrlRepoViewParameters
   | GenerateUrlDashboardViewParameters
+  | GenerateUrlTopicViewParams
   | GenerateUrlGroupViewParameters
   | GenerateUrlEditViewParameters
   | GenerateUrlRootViewParameters
@@ -675,6 +681,15 @@
     );
   },
 
+  navigateToTopicPage(topic: string) {
+    this._navigate(
+      this._getUrlFor({
+        view: GerritView.TOPIC,
+        topic,
+      })
+    );
+  },
+
   /**
    * @param basePatchNum The string 'PARENT' can be used for none.
    */
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 243af95..5fac268 100644
--- a/polygerrit-ui/app/elements/core/gr-router/gr-router.ts
+++ b/polygerrit-ui/app/elements/core/gr-router/gr-router.ts
@@ -43,6 +43,7 @@
   isGenerateUrlDiffViewParameters,
   RepoDetailView,
   WeblinkType,
+  GenerateUrlTopicViewParams,
 } from '../gr-navigation/gr-navigation';
 import {appContext} from '../../../services/app-context';
 import {convertToPatchSetNum} from '../../../utils/patch-set-util';
@@ -77,6 +78,7 @@
   toSearchParams,
 } from '../../../utils/url-util';
 import {Execution, LifeCycle, Timing} from '../../../constants/reporting';
+import {KnownExperimentId} from '../../../services/flags/flags';
 
 const RoutePattern = {
   ROOT: '/',
@@ -311,6 +313,8 @@
 
   private readonly restApiService = appContext.restApiService;
 
+  private readonly flagsService = appContext.flagsService;
+
   start() {
     if (!this._app) {
       return;
@@ -357,6 +361,8 @@
       url = this._generateChangeUrl(params);
     } else if (params.view === GerritView.DASHBOARD) {
       url = this._generateDashboardUrl(params);
+    } else if (params.view === GerritView.TOPIC) {
+      url = this._generateTopicPageUrl(params);
     } else if (
       params.view === GerritView.DIFF ||
       params.view === GerritView.EDIT
@@ -579,6 +585,10 @@
     }
   }
 
+  _generateTopicPageUrl(params: GenerateUrlTopicViewParams) {
+    return `/c/topic/${params.topic ?? ''}`;
+  }
+
   _sectionsToEncodedParams(sections: DashboardSection[], repoName?: RepoName) {
     return sections.map(section => {
       // If there is a repo name provided, make sure to substitute it into the
@@ -1545,6 +1555,16 @@
   }
 
   _handleQueryRoute(data: PageContextWithQueryMap) {
+    if (this.flagsService.isEnabled(KnownExperimentId.TOPICS_PAGE)) {
+      const query = data.params[0];
+      const terms = query.split(' ');
+      if (terms.length === 1) {
+        const tokens = terms[0].split(':');
+        if (tokens[0] === 'topic') {
+          return GerritNav.navigateToTopicPage(tokens[1]);
+        }
+      }
+    }
     this._setParams({
       view: GerritView.SEARCH,
       query: data.params[0],
diff --git a/polygerrit-ui/app/elements/core/gr-router/gr-router_test.js b/polygerrit-ui/app/elements/core/gr-router/gr-router_test.js
index 406a572..6462816 100644
--- a/polygerrit-ui/app/elements/core/gr-router/gr-router_test.js
+++ b/polygerrit-ui/app/elements/core/gr-router/gr-router_test.js
@@ -19,10 +19,11 @@
 import './gr-router.js';
 import {page} from '../../../utils/page-wrapper-utils.js';
 import {GerritNav} from '../gr-navigation/gr-navigation.js';
-import {stubBaseUrl, stubRestApi, addListenerForTest} from '../../../test/test-utils.js';
+import {stubBaseUrl, stubRestApi, addListenerForTest, stubFlags} from '../../../test/test-utils.js';
 import {_testOnly_RoutePattern} from './gr-router.js';
 import {GerritView} from '../../../services/router/router-model.js';
 import {ParentPatchSetNum} from '../../../types/common.js';
+import {KnownExperimentId} from '../../../services/flags/flags.js';
 
 const basicFixture = fixtureFromElement('gr-router');
 
@@ -260,6 +261,15 @@
   });
 
   suite('generateUrl', () => {
+    test('topic page', () => {
+      const params = {
+        view: GerritView.TOPIC,
+        topic: 'ggh',
+      };
+      assert.equal(element._generateUrl(params),
+          '/c/topic/ggh');
+    });
+
     test('search', () => {
       let params = {
         view: GerritNav.View.SEARCH,
@@ -665,6 +675,21 @@
       });
     });
 
+    test('_handleQueryRoute to topic page', () => {
+      stubFlags('isEnabled').withArgs(KnownExperimentId.TOPICS_PAGE)
+          .returns(true);
+      const navStub = sinon.stub(GerritNav, 'navigateToTopicPage');
+      let data = {params: ['topic:abcd']};
+      element._handleQueryRoute(data);
+
+      assert.isTrue(navStub.called);
+
+      // multiple terms so topic page is not loaded
+      data = {params: ['topic:abcd owner:self']};
+      element._handleQueryRoute(data);
+      assert.isTrue(navStub.calledOnce);
+    });
+
     test('_handleQueryLegacySuffixRoute', () => {
       element._handleQueryLegacySuffixRoute({path: '/q/foo+bar,n,z'});
       assert.isTrue(redirectStub.calledOnce);
diff --git a/polygerrit-ui/app/test/test-utils.ts b/polygerrit-ui/app/test/test-utils.ts
index 63c125e..faecda0 100644
--- a/polygerrit-ui/app/test/test-utils.ts
+++ b/polygerrit-ui/app/test/test-utils.ts
@@ -27,6 +27,7 @@
 import {UserService} from '../services/user/user-service';
 import {ShortcutsService} from '../services/shortcuts/shortcuts-service';
 import {queryAndAssert, query} from '../utils/common-util';
+import {FlagsService} from '../services/flags/flags';
 export {query, queryAll, queryAndAssert} from '../utils/common-util';
 
 export interface MockPromise<T> extends Promise<T> {
@@ -138,6 +139,10 @@
   return sinon.stub(appContext.reportingService, method);
 }
 
+export function stubFlags<K extends keyof FlagsService>(method: K) {
+  return sinon.stub(appContext.flagsService, method);
+}
+
 export type SinonSpyMember<F extends (...args: any) => any> = SinonSpy<
   Parameters<F>,
   ReturnType<F>