Move generating of group URLs into group.ts

Release-Notes: skip
Google-Bug-Id: b/244279450
Change-Id: If0948fcae14c80edd8a6b5a380ee5b303e48de88
diff --git a/polygerrit-ui/app/elements/admin/gr-admin-group-list/gr-admin-group-list.ts b/polygerrit-ui/app/elements/admin/gr-admin-group-list/gr-admin-group-list.ts
index 02b4d31..5d32d32 100644
--- a/polygerrit-ui/app/elements/admin/gr-admin-group-list/gr-admin-group-list.ts
+++ b/polygerrit-ui/app/elements/admin/gr-admin-group-list/gr-admin-group-list.ts
@@ -7,7 +7,6 @@
 import '../../shared/gr-list-view/gr-list-view';
 import '../../shared/gr-overlay/gr-overlay';
 import '../gr-create-group-dialog/gr-create-group-dialog';
-import {GerritNav} from '../../core/gr-navigation/gr-navigation';
 import {GrOverlay} from '../../shared/gr-overlay/gr-overlay';
 import {GroupId, GroupInfo, GroupName} from '../../../types/common';
 import {GrCreateGroupDialog} from '../gr-create-group-dialog/gr-create-group-dialog';
@@ -20,6 +19,7 @@
 import {customElement, query, property, state} from 'lit/decorators.js';
 import {assertIsDefined} from '../../../utils/common-util';
 import {AdminViewState} from '../../../models/views/admin';
+import {createGroupUrl} from '../../../models/views/group';
 
 declare global {
   interface HTMLElementTagNameMap {
@@ -178,8 +178,9 @@
    *
    * private but used in test
    */
-  computeGroupUrl(id: string) {
-    return GerritNav.getUrlForGroup(decodeURIComponent(id) as GroupId);
+  computeGroupUrl(encodedId: string) {
+    const groupId = decodeURIComponent(encodedId) as GroupId;
+    return createGroupUrl({groupId});
   }
 
   private getCreateGroupCapability() {
diff --git a/polygerrit-ui/app/elements/admin/gr-admin-group-list/gr-admin-group-list_test.ts b/polygerrit-ui/app/elements/admin/gr-admin-group-list/gr-admin-group-list_test.ts
index 6e9b802..e484489 100644
--- a/polygerrit-ui/app/elements/admin/gr-admin-group-list/gr-admin-group-list_test.ts
+++ b/polygerrit-ui/app/elements/admin/gr-admin-group-list/gr-admin-group-list_test.ts
@@ -6,7 +6,6 @@
 import '../../../test/common-test-setup';
 import './gr-admin-group-list';
 import {GrAdminGroupList} from './gr-admin-group-list';
-import {GerritNav} from '../../core/gr-navigation/gr-navigation';
 import {queryAndAssert, stubRestApi} from '../../../test/test-utils';
 import {
   GroupId,
@@ -110,31 +109,6 @@
     );
   });
 
-  test('computeGroupUrl', () => {
-    let urlStub = sinon
-      .stub(GerritNav, 'getUrlForGroup')
-      .callsFake(
-        () => '/admin/groups/e2cd66f88a2db4d391ac068a92d987effbe872f5'
-      );
-
-    let group = 'e2cd66f88a2db4d391ac068a92d987effbe872f5';
-    assert.equal(
-      element.computeGroupUrl(group),
-      '/admin/groups/e2cd66f88a2db4d391ac068a92d987effbe872f5'
-    );
-
-    urlStub.restore();
-
-    urlStub = sinon
-      .stub(GerritNav, 'getUrlForGroup')
-      .callsFake(() => '/admin/groups/user/test');
-
-    group = 'user%2Ftest';
-    assert.equal(element.computeGroupUrl(group), '/admin/groups/user/test');
-
-    urlStub.restore();
-  });
-
   suite('list with groups', () => {
     setup(async () => {
       groups = createGroupObjectList('test', 26);
diff --git a/polygerrit-ui/app/elements/admin/gr-group-audit-log/gr-group-audit-log.ts b/polygerrit-ui/app/elements/admin/gr-group-audit-log/gr-group-audit-log.ts
index dc07025..e0c0d30 100644
--- a/polygerrit-ui/app/elements/admin/gr-group-audit-log/gr-group-audit-log.ts
+++ b/polygerrit-ui/app/elements/admin/gr-group-audit-log/gr-group-audit-log.ts
@@ -4,7 +4,6 @@
  * SPDX-License-Identifier: Apache-2.0
  */
 import '../../shared/gr-account-label/gr-account-label';
-import {GerritNav} from '../../core/gr-navigation/gr-navigation';
 import {
   GroupInfo,
   AccountInfo,
@@ -20,6 +19,7 @@
 import {tableStyles} from '../../../styles/gr-table-styles';
 import {LitElement, PropertyValues, css, html} from 'lit';
 import {customElement, property, state} from 'lit/decorators.js';
+import {createGroupUrl} from '../../../models/views/group';
 
 declare global {
   interface HTMLElementTagNameMap {
@@ -168,12 +168,9 @@
     return isGroupAuditGroupEventInfo(event);
   }
 
-  private computeGroupUrl(group: GroupInfo) {
-    if (group && group.url && group.id) {
-      return GerritNav.getUrlForGroup(group.id);
-    }
-
-    return '';
+  private computeGroupUrl(group?: GroupInfo) {
+    if (!group?.id) return '';
+    return createGroupUrl({groupId: group.id});
   }
 
   // private but used in test
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 e638d28..e9490a9 100644
--- a/polygerrit-ui/app/elements/core/gr-navigation/gr-navigation.ts
+++ b/polygerrit-ui/app/elements/core/gr-navigation/gr-navigation.ts
@@ -10,7 +10,6 @@
   CommentLinks,
   CommitId,
   DashboardId,
-  GroupId,
   NumericChangeId,
   PARENT,
   PatchSetNum,
@@ -26,7 +25,6 @@
   GenerateUrlParameters,
 } from '../../../utils/router-util';
 import {createRepoUrl} from '../../../models/views/repo';
-import {GroupDetailView} from '../../../models/views/group';
 import {createSearchUrl} from '../../../models/views/search';
 import {createDiffUrl} from '../../../models/views/diff';
 
@@ -457,29 +455,6 @@
     this._navigate(createRepoUrl({repo}));
   },
 
-  getUrlForGroup(groupId: GroupId) {
-    return this._getUrlFor({
-      view: GerritView.GROUP,
-      groupId,
-    });
-  },
-
-  getUrlForGroupLog(groupId: GroupId) {
-    return this._getUrlFor({
-      view: GerritView.GROUP,
-      groupId,
-      detail: GroupDetailView.LOG,
-    });
-  },
-
-  getUrlForGroupMembers(groupId: GroupId) {
-    return this._getUrlFor({
-      view: GerritView.GROUP,
-      groupId,
-      detail: GroupDetailView.MEMBERS,
-    });
-  },
-
   getEditWebLinks(
     repo: RepoName,
     commit: CommitId,
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 a1ae858..c27776a 100644
--- a/polygerrit-ui/app/elements/core/gr-router/gr-router.ts
+++ b/polygerrit-ui/app/elements/core/gr-router/gr-router.ts
@@ -20,10 +20,7 @@
   WeblinkType,
 } from '../gr-navigation/gr-navigation';
 import {getAppContext} from '../../../services/app-context';
-import {
-  convertToPatchSetNum,
-  PatchRangeParams,
-} from '../../../utils/patch-set-util';
+import {convertToPatchSetNum} from '../../../utils/patch-set-util';
 import {assertIsDefined, assertNever} from '../../../utils/common-util';
 import {
   BasePatchSetNum,
@@ -43,6 +40,7 @@
 import {windowLocationReload} from '../../../utils/dom-util';
 import {
   getBaseUrl,
+  PatchRangeParams,
   toPath,
   toPathname,
   toSearchParams,
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 81b5a37..136ae2e 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
@@ -42,7 +42,7 @@
 import {GroupDetailView} from '../../../models/views/group';
 import {EditViewState} from '../../../models/views/edit';
 import {ChangeViewState} from '../../../models/views/change';
-import {PatchRangeParams} from '../../../utils/patch-set-util';
+import {PatchRangeParams} from '../../../utils/url-util';
 
 suite('gr-router tests', () => {
   let router: GrRouter;
diff --git a/polygerrit-ui/app/elements/settings/gr-group-list/gr-group-list.ts b/polygerrit-ui/app/elements/settings/gr-group-list/gr-group-list.ts
index bbd0ef5..68a2293 100644
--- a/polygerrit-ui/app/elements/settings/gr-group-list/gr-group-list.ts
+++ b/polygerrit-ui/app/elements/settings/gr-group-list/gr-group-list.ts
@@ -3,13 +3,13 @@
  * Copyright 2016 Google LLC
  * SPDX-License-Identifier: Apache-2.0
  */
-import {GerritNav} from '../../core/gr-navigation/gr-navigation';
 import {GroupInfo, GroupId} from '../../../types/common';
 import {getAppContext} from '../../../services/app-context';
 import {formStyles} from '../../../styles/gr-form-styles';
 import {sharedStyles} from '../../../styles/shared-styles';
 import {LitElement, css, html} from 'lit';
 import {customElement, state} from 'lit/decorators.js';
+import {createGroupUrl} from '../../../models/views/group';
 
 declare global {
   interface HTMLElementTagNameMap {
@@ -64,7 +64,7 @@
         </thead>
         <tbody>
           ${(this._groups ?? []).map(group => {
-            const href = this._computeGroupPath(group);
+            const href = this._computeGroupPath(group) ?? '';
             return html`
               <tr>
                 <td class="nameColumn">
@@ -82,13 +82,12 @@
     </div>`;
   }
 
-  _computeGroupPath(group: GroupInfo) {
-    if (!group || !group.id) {
-      return;
-    }
+  _computeGroupPath(group?: GroupInfo) {
+    if (!group?.id) return;
 
     // Group ID is already encoded from the API
     // Decode it here to match with our router encoding behavior
-    return GerritNav.getUrlForGroup(decodeURIComponent(group.id) as GroupId);
+    const decodedGroupId = decodeURIComponent(group.id) as GroupId;
+    return createGroupUrl({groupId: decodedGroupId});
   }
 }
diff --git a/polygerrit-ui/app/elements/settings/gr-group-list/gr-group-list_test.ts b/polygerrit-ui/app/elements/settings/gr-group-list/gr-group-list_test.ts
index 8fa9f3e..08ce11c 100644
--- a/polygerrit-ui/app/elements/settings/gr-group-list/gr-group-list_test.ts
+++ b/polygerrit-ui/app/elements/settings/gr-group-list/gr-group-list_test.ts
@@ -6,7 +6,6 @@
 import '../../../test/common-test-setup';
 import './gr-group-list';
 import {GrGroupList} from './gr-group-list';
-import {GerritNav} from '../../core/gr-navigation/gr-navigation';
 import {GroupId, GroupInfo, GroupName} from '../../../types/common';
 import {stubRestApi, waitEventLoop} from '../../../test/test-utils';
 import {fixture, html, assert} from '@open-wc/testing';
@@ -65,21 +64,21 @@
             <tbody>
               <tr>
                 <td class="nameColumn">
-                  <a href=""> Group 1 </a>
+                  <a href="/admin/groups/abc"> Group 1 </a>
                 </td>
                 <td>Group 1 description</td>
                 <td class="visibleCell">No</td>
               </tr>
               <tr>
                 <td class="nameColumn">
-                  <a href=""> Group 2 </a>
+                  <a href="/admin/groups/456"> Group 2 </a>
                 </td>
                 <td></td>
                 <td class="visibleCell">Yes</td>
               </tr>
               <tr>
                 <td class="nameColumn">
-                  <a href=""> Group 3 </a>
+                  <a href="/admin/groups/789"> Group 3 </a>
                 </td>
                 <td></td>
                 <td class="visibleCell">No</td>
@@ -90,31 +89,4 @@
       `
     );
   });
-
-  test('_computeGroupPath', () => {
-    let urlStub = sinon
-      .stub(GerritNav, 'getUrlForGroup')
-      .callsFake(
-        () => '/admin/groups/e2cd66f88a2db4d391ac068a92d987effbe872f5'
-      );
-
-    let group = {
-      id: 'e2cd66f88a2db4d391ac068a92d987effbe872f5' as GroupId,
-    };
-    assert.equal(
-      element._computeGroupPath(group),
-      '/admin/groups/e2cd66f88a2db4d391ac068a92d987effbe872f5'
-    );
-
-    urlStub.restore();
-
-    urlStub = sinon
-      .stub(GerritNav, 'getUrlForGroup')
-      .callsFake(() => '/admin/groups/user/test');
-
-    group = {
-      id: 'user%2Ftest' as GroupId,
-    };
-    assert.equal(element._computeGroupPath(group), '/admin/groups/user/test');
-  });
 });
diff --git a/polygerrit-ui/app/models/views/change.ts b/polygerrit-ui/app/models/views/change.ts
index de701f5..ed23457d 100644
--- a/polygerrit-ui/app/models/views/change.ts
+++ b/polygerrit-ui/app/models/views/change.ts
@@ -11,8 +11,7 @@
 } from '../../api/rest-api';
 import {GerritView} from '../../services/router/router-model';
 import {UrlEncodedCommentId} from '../../types/common';
-import {getPatchRangeExpression} from '../../utils/patch-set-util';
-import {encodeURL} from '../../utils/url-util';
+import {encodeURL, getPatchRangeExpression} from '../../utils/url-util';
 import {AttemptChoice} from '../checks/checks-util';
 import {Model} from '../model';
 import {ViewState} from './base';
diff --git a/polygerrit-ui/app/models/views/diff.ts b/polygerrit-ui/app/models/views/diff.ts
index 3831ab9..68f416f 100644
--- a/polygerrit-ui/app/models/views/diff.ts
+++ b/polygerrit-ui/app/models/views/diff.ts
@@ -11,8 +11,7 @@
 } from '../../api/rest-api';
 import {GerritView} from '../../services/router/router-model';
 import {UrlEncodedCommentId} from '../../types/common';
-import {getPatchRangeExpression} from '../../utils/patch-set-util';
-import {encodeURL} from '../../utils/url-util';
+import {encodeURL, getPatchRangeExpression} from '../../utils/url-util';
 import {Model} from '../model';
 import {ViewState} from './base';
 
diff --git a/polygerrit-ui/app/models/views/edit.ts b/polygerrit-ui/app/models/views/edit.ts
index 0a2f07a..102a7e0 100644
--- a/polygerrit-ui/app/models/views/edit.ts
+++ b/polygerrit-ui/app/models/views/edit.ts
@@ -10,8 +10,7 @@
   RevisionPatchSetNum,
 } from '../../api/rest-api';
 import {GerritView} from '../../services/router/router-model';
-import {getPatchRangeExpression} from '../../utils/patch-set-util';
-import {encodeURL} from '../../utils/url-util';
+import {encodeURL, getPatchRangeExpression} from '../../utils/url-util';
 import {Model} from '../model';
 import {ViewState} from './base';
 
diff --git a/polygerrit-ui/app/models/views/group.ts b/polygerrit-ui/app/models/views/group.ts
index 50a5229..bac8eb5 100644
--- a/polygerrit-ui/app/models/views/group.ts
+++ b/polygerrit-ui/app/models/views/group.ts
@@ -5,6 +5,7 @@
  */
 import {GerritView} from '../../services/router/router-model';
 import {GroupId} from '../../types/common';
+import {encodeURL, getBaseUrl} from '../../utils/url-util';
 import {Model} from '../model';
 import {ViewState} from './base';
 
@@ -15,16 +16,22 @@
 
 export interface GroupViewState extends ViewState {
   view: GerritView.GROUP;
+  groupId: GroupId;
   detail?: GroupDetailView;
-  groupId?: GroupId;
 }
 
-const DEFAULT_STATE: GroupViewState = {
-  view: GerritView.GROUP,
-};
+export function createGroupUrl(state: Omit<GroupViewState, 'view'>) {
+  let url = `/admin/groups/${encodeURL(`${state.groupId}`, true)}`;
+  if (state.detail === GroupDetailView.MEMBERS) {
+    url += ',members';
+  } else if (state.detail === GroupDetailView.LOG) {
+    url += ',audit-log';
+  }
+  return getBaseUrl() + url;
+}
 
-export class GroupViewModel extends Model<GroupViewState> {
+export class GroupViewModel extends Model<GroupViewState | undefined> {
   constructor() {
-    super(DEFAULT_STATE);
+    super(undefined);
   }
 }
diff --git a/polygerrit-ui/app/models/views/group_test.ts b/polygerrit-ui/app/models/views/group_test.ts
new file mode 100644
index 0000000..e1fbe66
--- /dev/null
+++ b/polygerrit-ui/app/models/views/group_test.ts
@@ -0,0 +1,38 @@
+/**
+ * @license
+ * Copyright 2022 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+import {assert} from '@open-wc/testing';
+import {GroupId} from '../../api/rest-api';
+import {GerritView} from '../../services/router/router-model';
+import '../../test/common-test-setup';
+import {createGroupUrl, GroupDetailView, GroupViewState} from './group';
+
+suite('group view state tests', () => {
+  test('createGroupUrl() info', () => {
+    const params: GroupViewState = {
+      view: GerritView.GROUP,
+      groupId: '1234' as GroupId,
+    };
+    assert.equal(createGroupUrl(params), '/admin/groups/1234');
+  });
+
+  test('createGroupUrl() members', () => {
+    const params: GroupViewState = {
+      view: GerritView.GROUP,
+      groupId: '1234' as GroupId,
+      detail: 'members' as GroupDetailView,
+    };
+    assert.equal(createGroupUrl(params), '/admin/groups/1234,members');
+  });
+
+  test('createGroupUrl() audit log', () => {
+    const params: GroupViewState = {
+      view: GerritView.GROUP,
+      groupId: '1234' as GroupId,
+      detail: 'log' as GroupDetailView,
+    };
+    assert.equal(createGroupUrl(params), '/admin/groups/1234,audit-log');
+  });
+});
diff --git a/polygerrit-ui/app/utils/admin-nav-util.ts b/polygerrit-ui/app/utils/admin-nav-util.ts
index 0a56fb1..c8fc9fb 100644
--- a/polygerrit-ui/app/utils/admin-nav-util.ts
+++ b/polygerrit-ui/app/utils/admin-nav-util.ts
@@ -3,7 +3,6 @@
  * Copyright 2018 Google LLC
  * SPDX-License-Identifier: Apache-2.0
  */
-import {GerritNav} from '../elements/core/gr-navigation/gr-navigation';
 import {
   RepoName,
   GroupId,
@@ -14,7 +13,7 @@
 import {GerritView} from '../services/router/router-model';
 import {MenuLink} from '../api/admin';
 import {AdminChildView} from '../models/views/admin';
-import {GroupDetailView} from '../models/views/group';
+import {createGroupUrl, GroupDetailView} from '../models/views/group';
 import {createRepoUrl, RepoDetailView} from '../models/views/repo';
 
 const ADMIN_LINKS: NavLink[] = [
@@ -152,7 +151,7 @@
   const subsection: SubsectionInterface = {
     name: groupName,
     view: GerritView.GROUP,
-    url: GerritNav.getUrlForGroup(groupId),
+    url: createGroupUrl({groupId}),
     children,
   };
   if (groupIsInternal) {
@@ -160,7 +159,7 @@
       name: 'Members',
       detailType: GroupDetailView.MEMBERS,
       view: GerritView.GROUP,
-      url: GerritNav.getUrlForGroupMembers(groupId),
+      url: createGroupUrl({groupId, detail: GroupDetailView.MEMBERS}),
     });
   }
   if (groupIsInternal && (isAdmin || groupOwner)) {
@@ -168,7 +167,7 @@
       name: 'Audit Log',
       detailType: GroupDetailView.LOG,
       view: GerritView.GROUP,
-      url: GerritNav.getUrlForGroupLog(groupId),
+      url: createGroupUrl({groupId, detail: GroupDetailView.LOG}),
     });
   }
   return subsection;
diff --git a/polygerrit-ui/app/utils/patch-set-util.ts b/polygerrit-ui/app/utils/patch-set-util.ts
index c040548..515f2e7 100644
--- a/polygerrit-ui/app/utils/patch-set-util.ts
+++ b/polygerrit-ui/app/utils/patch-set-util.ts
@@ -312,24 +312,3 @@
 export function getParentIndex(rangeBase: PatchSetNum) {
   return -Number(`${rangeBase}`);
 }
-
-export interface PatchRangeParams {
-  patchNum?: RevisionPatchSetNum;
-  basePatchNum?: BasePatchSetNum;
-}
-
-/**
- * Given an object of parameters, potentially including a `patchNum` or a
- * `basePatchNum` or both, return a string representation of that range. If
- * no range is indicated in the params, the empty string is returned.
- */
-export function getPatchRangeExpression(params: PatchRangeParams): string {
-  let range = '';
-  if (params.patchNum) {
-    range = `${params.patchNum}`;
-  }
-  if (params.basePatchNum && params.basePatchNum !== PARENT) {
-    range = `${params.basePatchNum}..${range}`;
-  }
-  return range;
-}
diff --git a/polygerrit-ui/app/utils/path-list-util_test.ts b/polygerrit-ui/app/utils/path-list-util_test.ts
index 27ff5f7..50f5c0e 100644
--- a/polygerrit-ui/app/utils/path-list-util_test.ts
+++ b/polygerrit-ui/app/utils/path-list-util_test.ts
@@ -12,10 +12,9 @@
   specialFilePathCompare,
   truncatePath,
 } from './path-list-util';
-import {BasePatchSetNum, FileInfo, RevisionPatchSetNum} from '../api/rest-api';
+import {FileInfo} from '../api/rest-api';
 import {hasOwnProperty} from './common-util';
 import {assert} from '@open-wc/testing';
-import {getPatchRangeExpression, PatchRangeParams} from './patch-set-util';
 
 suite('path-list-utl tests', () => {
   test('special sort', () => {
@@ -163,22 +162,4 @@
     const shortenedPath = truncatePath(path);
     assert.equal(shortenedPath, expectedPath);
   });
-
-  test('getPatchRangeExpression', () => {
-    const params: PatchRangeParams = {};
-    let actual = getPatchRangeExpression(params);
-    assert.equal(actual, '');
-
-    params.patchNum = 4 as RevisionPatchSetNum;
-    actual = getPatchRangeExpression(params);
-    assert.equal(actual, '4');
-
-    params.basePatchNum = 2 as BasePatchSetNum;
-    actual = getPatchRangeExpression(params);
-    assert.equal(actual, '2..4');
-
-    delete params.patchNum;
-    actual = getPatchRangeExpression(params);
-    assert.equal(actual, '2..');
-  });
 });
diff --git a/polygerrit-ui/app/utils/router-util.ts b/polygerrit-ui/app/utils/router-util.ts
index e04bf9d..c722177 100644
--- a/polygerrit-ui/app/utils/router-util.ts
+++ b/polygerrit-ui/app/utils/router-util.ts
@@ -7,7 +7,6 @@
 import {encodeURL, getBaseUrl} from './url-util';
 import {assertNever} from './common-util';
 import {GerritView} from '../services/router/router-model';
-import {GroupDetailView, GroupViewState} from '../models/views/group';
 import {DashboardViewState} from '../models/views/dashboard';
 import {createEditUrl, EditViewState} from '../models/views/edit';
 import {createDiffUrl, DiffViewState} from '../models/views/diff';
@@ -25,7 +24,6 @@
 export type GenerateUrlParameters =
   | ChangeViewState
   | DashboardViewState
-  | GroupViewState
   | EditViewState
   | DiffViewState;
 
@@ -59,8 +57,6 @@
     url = createDiffUrl(params);
   } else if (params.view === GerritView.EDIT) {
     url = createEditUrl(params);
-  } else if (params.view === GerritView.GROUP) {
-    url = generateGroupUrl(params);
   } else {
     assertNever(params, "Can't generate");
   }
@@ -103,13 +99,3 @@
     return `/dashboard/${params.user || 'self'}`;
   }
 }
-
-function generateGroupUrl(params: GroupViewState) {
-  let url = `/admin/groups/${encodeURL(`${params.groupId}`, true)}`;
-  if (params.detail === GroupDetailView.MEMBERS) {
-    url += ',members';
-  } else if (params.detail === GroupDetailView.LOG) {
-    url += ',audit-log';
-  }
-  return url;
-}
diff --git a/polygerrit-ui/app/utils/router-util_test.ts b/polygerrit-ui/app/utils/router-util_test.ts
index 26b806f..4f08b3c 100644
--- a/polygerrit-ui/app/utils/router-util_test.ts
+++ b/polygerrit-ui/app/utils/router-util_test.ts
@@ -10,12 +10,10 @@
   RevisionPatchSetNum,
   BasePatchSetNum,
   EDIT,
-  GroupId,
 } from '../api/rest-api';
 import {ChangeViewState} from '../models/views/change';
 import {DashboardViewState} from '../models/views/dashboard';
 import {EditViewState} from '../models/views/edit';
-import {GroupDetailView, GroupViewState} from '../models/views/group';
 import {GerritView} from '../services/router/router-model';
 import '../test/common-test-setup';
 import {DashboardId} from '../types/common';
@@ -148,33 +146,5 @@
         );
       });
     });
-
-    suite('groups', () => {
-      test('group info', () => {
-        const params: GroupViewState = {
-          view: GerritView.GROUP,
-          groupId: '1234' as GroupId,
-        };
-        assert.equal(generateUrl(params), '/admin/groups/1234');
-      });
-
-      test('group members', () => {
-        const params: GroupViewState = {
-          view: GerritView.GROUP,
-          groupId: '1234' as GroupId,
-          detail: 'members' as GroupDetailView,
-        };
-        assert.equal(generateUrl(params), '/admin/groups/1234,members');
-      });
-
-      test('group audit log', () => {
-        const params: GroupViewState = {
-          view: GerritView.GROUP,
-          groupId: '1234' as GroupId,
-          detail: 'log' as GroupDetailView,
-        };
-        assert.equal(generateUrl(params), '/admin/groups/1234,audit-log');
-      });
-    });
   });
 });
diff --git a/polygerrit-ui/app/utils/url-util.ts b/polygerrit-ui/app/utils/url-util.ts
index 467fe38..afa7d18 100644
--- a/polygerrit-ui/app/utils/url-util.ts
+++ b/polygerrit-ui/app/utils/url-util.ts
@@ -4,7 +4,12 @@
  * SPDX-License-Identifier: Apache-2.0
  */
 
-import {ServerInfo} from '../types/common';
+import {
+  BasePatchSetNum,
+  PARENT,
+  RevisionPatchSetNum,
+  ServerInfo,
+} from '../types/common';
 import {RestApiService} from '../services/gr-rest-api/gr-rest-api';
 
 const PROBE_PATH = '/Documentation/index.html';
@@ -16,6 +21,27 @@
   return self.CANONICAL_PATH || '';
 }
 
+export interface PatchRangeParams {
+  patchNum?: RevisionPatchSetNum;
+  basePatchNum?: BasePatchSetNum;
+}
+
+/**
+ * Given an object of parameters, potentially including a `patchNum` or a
+ * `basePatchNum` or both, return a string representation of that range. If
+ * no range is indicated in the params, the empty string is returned.
+ */
+export function getPatchRangeExpression(params: PatchRangeParams) {
+  let range = '';
+  if (params.patchNum) {
+    range = `${params.patchNum}`;
+  }
+  if (params.basePatchNum && params.basePatchNum !== PARENT) {
+    range = `${params.basePatchNum}..${range}`;
+  }
+  return range;
+}
+
 export function prependOrigin(path: string): string {
   if (path.startsWith('http')) return path;
   if (path.startsWith('/')) return window.location.origin + path;
diff --git a/polygerrit-ui/app/utils/url-util_test.ts b/polygerrit-ui/app/utils/url-util_test.ts
index 2153f94..aa80b73 100644
--- a/polygerrit-ui/app/utils/url-util_test.ts
+++ b/polygerrit-ui/app/utils/url-util_test.ts
@@ -3,7 +3,11 @@
  * Copyright 2020 Google LLC
  * SPDX-License-Identifier: Apache-2.0
  */
-import {ServerInfo} from '../api/rest-api';
+import {
+  BasePatchSetNum,
+  RevisionPatchSetNum,
+  ServerInfo,
+} from '../api/rest-api';
 import '../test/common-test-setup';
 import {createGerritInfo, createServerInfo} from '../test/test-data-generators';
 import {
@@ -15,6 +19,8 @@
   toPath,
   toPathname,
   toSearchParams,
+  getPatchRangeExpression,
+  PatchRangeParams,
 } from './url-util';
 import {getAppContext, AppContext} from '../services/app-context';
 import {stubRestApi} from '../test/test-utils';
@@ -151,4 +157,22 @@
       'asdf?qwer=zxcv'
     );
   });
+
+  test('getPatchRangeExpression', () => {
+    const params: PatchRangeParams = {};
+    let actual = getPatchRangeExpression(params);
+    assert.equal(actual, '');
+
+    params.patchNum = 4 as RevisionPatchSetNum;
+    actual = getPatchRangeExpression(params);
+    assert.equal(actual, '4');
+
+    params.basePatchNum = 2 as BasePatchSetNum;
+    actual = getPatchRangeExpression(params);
+    assert.equal(actual, '2..4');
+
+    delete params.patchNum;
+    actual = getPatchRangeExpression(params);
+    assert.equal(actual, '2..');
+  });
 });