Convert all GROUP_LIST routes to one unified regex pattern

Our goal is that all routes become regex patterns. At the moment some of
them are strings, which means that they will get special handling by
page.js, most importantly it will construct its own regex pattern for
tokens such as `:filter`. Apart from not wanting to deal with as few as
possible page.js specialities and trying to be consistent with all the
route patterns, these special page.js tokens also come along with a
fundamental problem: They use `[^/]+` and thus don't match `/`, which at
least for `repo` matching is a problem and creates extra complexity.

Release-Notes: skip
Change-Id: I450d90457d458333d66ecc0dc760071311b1a66c
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 a181348..65abc33 100644
--- a/polygerrit-ui/app/elements/core/gr-router/gr-router.ts
+++ b/polygerrit-ui/app/elements/core/gr-router/gr-router.ts
@@ -143,11 +143,6 @@
   // Matches /admin/groups/[uuid-]<group>,members
   GROUP_MEMBERS: /^\/admin\/groups\/(?:uuid-)?(.+),members$/,
 
-  // Matches /admin/groups[,<offset>][/].
-  GROUP_LIST_OFFSET: /^\/admin\/groups(,(\d+))?(\/)?$/,
-  GROUP_LIST_FILTER: '/admin/groups/q/filter::filter',
-  GROUP_LIST_FILTER_OFFSET: '/admin/groups/q/filter::filter,:offset',
-
   // Matches /admin/create-project
   LEGACY_CREATE_PROJECT: /^\/admin\/create-project\/?$/,
 
@@ -194,6 +189,8 @@
 
   // Matches /admin/plugins with optional filter and offset.
   PLUGIN_LIST: /^\/admin\/plugins\/?(?:\/q\/filter:(.*?))?(?:,(\d+))?$/,
+  // Matches /admin/groups with optional filter and offset.
+  GROUP_LIST: /^\/admin\/groups\/?(?:\/q\/filter:(.*?))?(?:,(\d+))?$/,
 
   QUERY: /^\/q\/([^,]+)(,(\d+))?$/,
 
@@ -692,23 +689,9 @@
     );
 
     this.mapRoute(
-      RoutePattern.GROUP_LIST_OFFSET,
-      'handleGroupListOffsetRoute',
-      ctx => this.handleGroupListOffsetRoute(ctx),
-      true
-    );
-
-    this.mapRoute(
-      RoutePattern.GROUP_LIST_FILTER_OFFSET,
-      'handleGroupListFilterOffsetRoute',
-      ctx => this.handleGroupListFilterOffsetRoute(ctx),
-      true
-    );
-
-    this.mapRoute(
-      RoutePattern.GROUP_LIST_FILTER,
-      'handleGroupListFilterRoute',
-      ctx => this.handleGroupListFilterRoute(ctx),
+      RoutePattern.GROUP_LIST,
+      'handleGroupListRoute',
+      ctx => this.handleGroupListRoute(ctx),
       true
     );
 
@@ -1145,39 +1128,14 @@
     this.groupViewModel.setState(state);
   }
 
-  handleGroupListOffsetRoute(ctx: PageContext) {
+  handleGroupListRoute(ctx: PageContext) {
     const state: AdminViewState = {
       view: GerritView.ADMIN,
       adminView: AdminChildView.GROUPS,
       offset: ctx.params[1] ?? '0',
-      filter: null,
-      openCreateModal: ctx.hash === 'create',
-    };
-    // Note that router model view must be updated before view models.
-    this.setState(state);
-    this.adminViewModel.setState(state);
-  }
-
-  handleGroupListFilterOffsetRoute(ctx: PageContext) {
-    const state: AdminViewState = {
-      view: GerritView.ADMIN,
-      adminView: AdminChildView.GROUPS,
-      offset: ctx.params['offset'],
-      filter: ctx.params['filter'],
-      openCreateModal: false,
-    };
-    // Note that router model view must be updated before view models.
-    this.setState(state);
-    this.adminViewModel.setState(state);
-  }
-
-  handleGroupListFilterRoute(ctx: PageContext) {
-    const state: AdminViewState = {
-      view: GerritView.ADMIN,
-      adminView: AdminChildView.GROUPS,
-      offset: ctx.params[1] ?? '0',
-      filter: ctx.params['filter'] || null,
-      openCreateModal: false,
+      filter: ctx.params[0] ?? null,
+      openCreateModal:
+        !ctx.params[0] && !ctx.params[1] && ctx.hash === 'create',
     };
     // Note that router model view must be updated before view models.
     this.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 8019ac9..50030a5 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
@@ -150,9 +150,7 @@
       'handleDiffEditRoute',
       'handleGroupAuditLogRoute',
       'handleGroupInfoRoute',
-      'handleGroupListFilterOffsetRoute',
-      'handleGroupListFilterRoute',
-      'handleGroupListOffsetRoute',
+      'handleGroupListRoute',
       'handleGroupMembersRoute',
       'handleGroupRoute',
       'handleGroupSelfRedirectRoute',
@@ -586,10 +584,8 @@
         });
       });
 
-      test('GROUP_LIST_*', async () => {
-        // GROUP_LIST_OFFSET: /^\/admin\/groups(,(\d+))?(\/)?$/,
-        // GROUP_LIST_FILTER: '/admin/groups/q/filter::filter',
-        // GROUP_LIST_FILTER_OFFSET: '/admin/groups/q/filter::filter,:offset',
+      test('GROUP_LIST', async () => {
+        // GROUP_LIST: /^\/admin\/groups(\/q\/filter:(.*?))?(,(\d+))?(\/)?$/,
 
         const defaultState: AdminViewState = {
           view: GerritView.ADMIN,
@@ -605,40 +601,35 @@
           ...defaultState,
           openCreateModal: true,
         });
-        await checkUrlToState('/admin/groups,123', {
+        await checkUrlToState('/admin/groups,42', {
           ...defaultState,
-          offset: '123',
+          offset: '42',
         });
-        await checkUrlToState('/admin/groups,123#create', {
+        // #create is ignored when there is an offset
+        await checkUrlToState('/admin/groups,42#create', {
           ...defaultState,
-          offset: '123',
-          openCreateModal: true,
+          offset: '42',
         });
 
-        await checkUrlToState('/admin/groups/q/filter:asdf', {
+        await checkUrlToState('/admin/groups/q/filter:foo', {
           ...defaultState,
-          filter: 'asdf',
+          filter: 'foo',
         });
-        await checkUrlToState('/admin/groups/q/filter:asdf,123', {
+        await checkUrlToState('/admin/groups/q/filter:foo/%2F%20%2525%252F', {
           ...defaultState,
-          filter: 'asdf',
-          offset: '123',
+          filter: 'foo// %/',
+        });
+        await checkUrlToState('/admin/groups/q/filter:foo,42', {
+          ...defaultState,
+          filter: 'foo',
+          offset: '42',
         });
         // #create is ignored when filtering
-        await checkUrlToState('/admin/groups/q/filter:asdf,123#create', {
+        await checkUrlToState('/admin/groups/q/filter:foo,42#create', {
           ...defaultState,
-          filter: 'asdf',
-          offset: '123',
+          filter: 'foo',
+          offset: '42',
         });
-        // filter is decoded (twice)
-        await checkUrlToState(
-          '/admin/groups/q/filter:XX%20XX%2520XX%252FXX%3FXX',
-          {...defaultState, filter: 'XX XX XX/XX?XX'}
-        );
-
-        // Slash must be double encoded in `filter` param.
-        await checkUrlNotMatched('/admin/groups/q/filter:asdf/qwer,11');
-        await checkUrlNotMatched('/admin/groups/q/filter:asdf%2Fqwer,11');
       });
 
       test('GROUP', async () => {