Merge "Fix typo in owners docs"
diff --git a/owners/src/main/resources/Documentation/config.md b/owners/src/main/resources/Documentation/config.md
index 89c6248..5b85b7e 100644
--- a/owners/src/main/resources/Documentation/config.md
+++ b/owners/src/main/resources/Documentation/config.md
@@ -69,7 +69,7 @@
     > Please also note, that project's `rules.pl` should be removed in this
     > case so that it doesn't interfere with a change evaluation.
     >
-    > The minial configuration looks like below (see
+    > The minimal configuration looks like below (see
     > [submit requirements documentation](/Documentation/config-submit-requirements.html) for more details):
     > ```
     > [submit-requirement "Owner-Approval"]
diff --git a/owners/web/gr-owned-files.ts b/owners/web/gr-owned-files.ts
index 969d91c..56274d4 100644
--- a/owners/web/gr-owned-files.ts
+++ b/owners/web/gr-owned-files.ts
@@ -23,6 +23,7 @@
   AccountInfo,
   ChangeInfo,
   ChangeStatus,
+  EmailAddress,
   RevisionInfo,
   EDIT,
 } from '@gerritcodereview/typescript-api/rest-api';
@@ -70,11 +71,36 @@
 @customElement(OWNED_FILES_TAB_HEADER)
 export class OwnedFilesTabHeader extends OwnedFilesCommon {
   static override get styles() {
-    return [...OwnedFilesCommon.commonStyles()];
+    return [
+      ...OwnedFilesCommon.commonStyles(),
+      css`
+        [hidden] {
+          display: none;
+        }
+      `,
+    ];
   }
 
   override render() {
-    if (this.hidden) return nothing;
+    // even if `nothing` is returned Gerrit still shows the pointer and allows
+    // clicking at it, redirecting to the empty tab when done; traverse through
+    // the shadowRoots down to the tab and disable/enable it when needed
+    const tabParent = document
+      .querySelector('#pg-app')
+      ?.shadowRoot?.querySelector('#app-element')
+      ?.shadowRoot?.querySelector('main > gr-change-view')
+      ?.shadowRoot?.querySelector(
+        '#tabs > paper-tab[data-name="change-view-tab-header-owners"]'
+      );
+    if (this.hidden) {
+      if (tabParent && !tabParent.getAttribute('disabled')) {
+        tabParent.setAttribute('disabled', 'disabled');
+      }
+      return nothing;
+    }
+    if (tabParent && tabParent.getAttribute('disabled')) {
+      tabParent.removeAttribute('disabled');
+    }
     return html`<div>Owned Files</div>`;
   }
 }
@@ -296,12 +322,22 @@
     return;
   }
 
+  const groupPrefix = 'group/';
+  const emailWithoutDomain = toEmailWithoutDomain(owner.email);
   const ownedFiles = [];
   for (const file of Object.keys(files)) {
     if (
-      files[file].find(
-        fileOwner => isOwner(fileOwner) && fileOwner.id === owner._account_id
-      )
+      files[file].find(fileOwner => {
+        if (isOwner(fileOwner)) {
+          return fileOwner.id === owner._account_id;
+        }
+
+        return (
+          !fileOwner.name?.startsWith(groupPrefix) &&
+          (fileOwner.name === emailWithoutDomain ||
+            fileOwner.name === owner.name)
+        );
+      })
     ) {
       ownedFiles.push(file);
     }
@@ -329,3 +365,8 @@
 
   return `${getBaseUrl()}/c/${repo}${change._number}${range}${path}`;
 }
+
+function toEmailWithoutDomain(email?: EmailAddress): string | undefined {
+  const startDomainIndex = email?.indexOf('@');
+  return startDomainIndex ? email?.substring(0, startDomainIndex) : undefined;
+}
diff --git a/owners/web/gr-owned-files_test.ts b/owners/web/gr-owned-files_test.ts
index 0437ab9..f9c7f3a 100644
--- a/owners/web/gr-owned-files_test.ts
+++ b/owners/web/gr-owned-files_test.ts
@@ -17,6 +17,7 @@
 
 import {assert} from '@open-wc/testing';
 import {
+  AccountId,
   AccountInfo,
   ChangeInfo,
   ChangeStatus,
@@ -25,7 +26,7 @@
   SubmitRequirementResultInfo,
 } from '@gerritcodereview/typescript-api/rest-api';
 import {ownedFiles, shouldHide} from './gr-owned-files';
-import {OwnedFiles, Owner} from './owners-service';
+import {GroupOwner, OwnedFiles, Owner} from './owners-service';
 import {deepEqual} from './utils';
 import {User, UserRole} from './owners-model';
 import {getRandom} from './test-utils';
@@ -59,6 +60,27 @@
     test('ownedFiles - should return owned files', () => {
       assert.equal(deepEqual(ownedFiles(owner, files), [ownedFile]), true);
     });
+
+    test('ownedFiles - should match file owner through email without domain name', () => {
+      const files = {
+        [ownedFile]: [fileOwnerWithNameOnly(`${ownerAccountId}_email`)],
+        'some.text': [fileOwnerWithNameOnly('random_joe')],
+      } as unknown as OwnedFiles;
+      assert.equal(deepEqual(ownedFiles(owner, files), [ownedFile]), true);
+    });
+
+    test('ownedFiles - should match file owner through full name', () => {
+      const files = {
+        [ownedFile]: [fileOwnerWithNameOnly(`${ownerAccountId}_name`)],
+        'some.text': [fileOwnerWithNameOnly('random_joe')],
+      } as unknown as OwnedFiles;
+      assert.equal(deepEqual(ownedFiles(owner, files), [ownedFile]), true);
+    });
+
+    test('ownedFiles - should NOT match file owner over email without domain or full name when account id is different', () => {
+      const notFileOwner = {...owner, _account_id: 2 as unknown as AccountId};
+      assert.equal(deepEqual(ownedFiles(notFileOwner, files), []), true);
+    });
   });
 
   suite('shouldHide tests', () => {
@@ -174,12 +196,18 @@
 function account(id: number): AccountInfo {
   return {
     _account_id: id,
+    email: `${id}_email@example.com`,
+    name: `${id}_name`,
   } as unknown as AccountInfo;
 }
 
 function fileOwner(id: number): Owner {
   return {
     id,
-    name: `name for account: ${id}`,
+    name: `${id}_name`,
   } as unknown as Owner;
 }
+
+function fileOwnerWithNameOnly(name: string): GroupOwner {
+  return {name} as unknown as GroupOwner;
+}