Merge branch 'stable-3.9'

* stable-3.9:
  Fix logic for email address text field
  Make username column show usernames instead of full names
  Fix another link for context path users
  Fix anchor element for createNotesAsync in config doc
  Make serviceuser work with Gerrit setups with context paths

Solves: bug 350779993
Change-Id: Ie2de27a4b1b7ee9e35d4fd4bb58873207c98fc28
diff --git a/src/main/resources/Documentation/config.md b/src/main/resources/Documentation/config.md
index b66c181..b75fcaf 100644
--- a/src/main/resources/Documentation/config.md
+++ b/src/main/resources/Documentation/config.md
@@ -82,7 +82,7 @@
 	user the 'Forge Committer' access right must be blocked for service
 	users. By default true.
 
-<a id="createNotes"></a>
+<a id="createNotesAsync"></a>
 `plugin.@PLUGIN@.createNotesAsync`
 :	Whether the Git notes on commits that are pushed by a service user
 	should be created asynchronously. By default false.
diff --git a/web/gr-serviceuser-create.ts b/web/gr-serviceuser-create.ts
index 2e6367d..3ee2a70 100644
--- a/web/gr-serviceuser-create.ts
+++ b/web/gr-serviceuser-create.ts
@@ -221,9 +221,11 @@
   }
 
   private forwardToDetails() {
-    window.location.href = `${
-      window.location.origin
-    }/x/${this.plugin.getPluginName()}/user/${this.accountId}`;
+    window.location.href = `${this.getPluginBaseURL()}/user/${this.accountId}`;
+  }
+
+  private getPluginBaseURL() {
+    return `${window.location.origin}${window.CANONICAL_PATH || ''}/x/${this.plugin.getPluginName()}`;
   }
 
   private getConfig() {
diff --git a/web/gr-serviceuser-detail.ts b/web/gr-serviceuser-detail.ts
index e2d4f94..befa4dd 100644
--- a/web/gr-serviceuser-detail.ts
+++ b/web/gr-serviceuser-detail.ts
@@ -88,7 +88,7 @@
   fullName?: String;
 
   @property({type: String})
-  email?: String;
+  email = '';
 
   @property({type: Array})
   availableOwners?: Array<GroupInfo>;
@@ -273,7 +273,7 @@
           type="text"
           class="wide"
           .value="${this.email}"
-          .placeholder="${this.serviceUser?.email}"
+          .placeholder="${this.serviceUser.email ?? ''}"
           ?disabled="${this.changingPrefs}"
           @input="${this.emailChanged}"
         />
@@ -345,7 +345,7 @@
       this.computeStatusButtonText();
       this.loading = false;
       this.fullName = this.serviceUser?.name;
-      this.email = this.serviceUser?.email;
+      this.email = this.serviceUser.email ?? '';
       this.owner = this.getCurrentOwnerGroup() ?? NOT_FOUND_MESSAGE;
     });
   }
@@ -457,11 +457,11 @@
       : NOT_FOUND_MESSAGE;
   }
 
-  private isEmailValid(email: String) {
-    if (!email) {
-      return false;
+  private isNewValidEmail(email: String) {
+    if (!this.serviceUser.email) {
+      return email.includes('@');
     }
-    return email.includes('@');
+    return email !== this.serviceUser.email && (email.includes('@') || email.length === 0);
   }
 
   private getGroupSuggestions(input: String) {
@@ -506,11 +506,8 @@
   }
 
   private emailChanged() {
-    const newEmail = this.serviceUserEmailInput.value;
-    if (this.isEmailValid(newEmail)) {
-      this.email = this.serviceUserEmailInput.value;
-      this.computePrefsChanged();
-    }
+    this.email = this.serviceUserEmailInput.value;
+    this.computePrefsChanged();
   }
 
   private ownerChanged() {
@@ -530,7 +527,7 @@
 
     if (
       this.owner === this.getCurrentOwnerGroup() &&
-      this.email === this.serviceUser.email &&
+      !this.isNewValidEmail(this.email) &&
       this.fullName === this.serviceUser.name
     ) {
       this.prefsChanged = false;
@@ -548,7 +545,7 @@
   }
 
   private applyNewEmail() {
-    if (!this.isEmailValid(this.email ?? '')) {
+    if (!this.isNewValidEmail(this.email)) {
       return;
     }
     return this.pluginRestApi.put(
diff --git a/web/gr-serviceuser-list.ts b/web/gr-serviceuser-list.ts
index b4bece4..939d54a 100644
--- a/web/gr-serviceuser-list.ts
+++ b/web/gr-serviceuser-list.ts
@@ -40,8 +40,8 @@
   @state()
   canCreate = false;
 
-  @property({type: Array})
-  serviceUsers = new Array<ServiceUserInfo>();
+  @property({type: Map})
+  serviceUsers = new Map<String, ServiceUserInfo>();
 
   static override get styles() {
     return [
@@ -94,8 +94,8 @@
           <td>Loading...</td>
         </tr>
         <tbody class="${this.computeLoadingClass()}">
-          ${this.serviceUsers.map(serviceUser =>
-            this.renderServiceUserList(serviceUser)
+          ${[...this.serviceUsers].map(([username, serviceUser]) =>
+            this.renderServiceUserList(username, serviceUser)
           )}
         </tbody>
       </table>
@@ -120,7 +120,7 @@
     return html``;
   }
 
-  private renderServiceUserList(serviceUser: ServiceUserInfo) {
+  private renderServiceUserList(username: String, serviceUser: ServiceUserInfo) {
     if (!serviceUser._account_id) {
       return;
     }
@@ -128,7 +128,7 @@
       <tr class="table">
         <td class="name">
           <a href="${this.computeServiceUserUrl(serviceUser._account_id)}"
-            >${serviceUser.name}</a
+            >${username}</a
           >
         </td>
         <td class="fullName">${serviceUser.name}</td>
@@ -171,9 +171,7 @@
     return this.pluginRestApi
       .get<Object>('/a/config/server/serviceuser~serviceusers/')
       .then(serviceUsers => {
-        new Map<String, ServiceUserInfo>(Object.entries(serviceUsers)).forEach(
-          v => this.serviceUsers.push(v)
-        );
+        this.serviceUsers = new Map<String, ServiceUserInfo>(Object.entries(serviceUsers));
       });
   }
 
@@ -210,14 +208,14 @@
   }
 
   private computeServiceUserUrl(id: AccountId) {
-    return `${
-      window.location.origin
-    }/x/${this.plugin.getPluginName()}/user/${id}`;
+    return `${this.getPluginBaseURL()}/user/${id}`;
   }
 
   private createNewServiceUser() {
-    window.location.href = `${
-      window.location.origin
-    }/x/${this.plugin.getPluginName()}/create`;
+    window.location.href = `${this.getPluginBaseURL()}/create`;
+  }
+
+  private getPluginBaseURL() {
+    return `${window.location.origin}${window.CANONICAL_PATH || ''}/x/${this.plugin.getPluginName()}`;
   }
 }
diff --git a/web/plugin.ts b/web/plugin.ts
index 1f777f0..d6f90b1 100644
--- a/web/plugin.ts
+++ b/web/plugin.ts
@@ -21,6 +21,12 @@
 import './gr-serviceuser-detail';
 import './gr-serviceuser-list';
 
+declare global {
+  interface Window {
+    CANONICAL_PATH?: string;
+  }
+}
+
 export interface AccountCapabilityInfo {
   administrateServer: boolean;
   'serviceuser-createServiceUser': boolean;
@@ -40,6 +46,6 @@
       }
       plugin.screen('list', 'gr-serviceuser-list');
       plugin.screen('user', 'gr-serviceuser-detail');
-      plugin.admin().addMenuLink('Service Users', '/x/serviceuser/list');
+      plugin.admin().addMenuLink('Service Users', `${window.CANONICAL_PATH || ''}/x/serviceuser/list`);
     });
 });