From bade3be76cee5c31b63ed9b8f6104df7d077d62d Mon Sep 17 00:00:00 2001
From: Peter Hegman <phegman@gitlab.com>
Date: Wed, 21 Feb 2024 15:43:55 -0800
Subject: [PATCH] Add access level badge to organization group and project list

Now that API supports them
---
 .../javascripts/organizations/mock_data.js    | 42 +++++++++++++++++
 .../graphql/queries/groups.query.graphql      |  3 ++
 .../graphql/queries/projects.query.graphql    |  3 ++
 .../javascripts/organizations/shared/utils.js |  5 +-
 .../groups_list/groups_list_item.vue          | 14 ++++--
 .../projects_list/projects_list_item.vue      | 20 ++++----
 .../organizations/shared/utils_spec.js        |  6 +++
 .../groups_list/groups_list_item_spec.js      | 32 ++++++++++++-
 .../projects_list/projects_list_item_spec.js  | 46 ++++++++++++++++---
 9 files changed, 149 insertions(+), 22 deletions(-)

diff --git a/app/assets/javascripts/organizations/mock_data.js b/app/assets/javascripts/organizations/mock_data.js
index b3e408c50aabe..0b0554e9e8c5a 100644
--- a/app/assets/javascripts/organizations/mock_data.js
+++ b/app/assets/javascripts/organizations/mock_data.js
@@ -66,6 +66,9 @@ export const organizationProjects = [
     userPermissions: {
       removeProject: true,
     },
+    maxAccessLevel: {
+      integerValue: 30,
+    },
   },
   {
     id: 'gid://gitlab/Project/7',
@@ -92,6 +95,9 @@ export const organizationProjects = [
     userPermissions: {
       removeProject: true,
     },
+    maxAccessLevel: {
+      integerValue: 30,
+    },
   },
   {
     id: 'gid://gitlab/Project/6',
@@ -118,6 +124,9 @@ export const organizationProjects = [
     userPermissions: {
       removeProject: true,
     },
+    maxAccessLevel: {
+      integerValue: 30,
+    },
   },
   {
     id: 'gid://gitlab/Project/5',
@@ -144,6 +153,9 @@ export const organizationProjects = [
     userPermissions: {
       removeProject: true,
     },
+    maxAccessLevel: {
+      integerValue: 30,
+    },
   },
   {
     id: 'gid://gitlab/Project/1',
@@ -170,6 +182,9 @@ export const organizationProjects = [
     userPermissions: {
       removeProject: false,
     },
+    maxAccessLevel: {
+      integerValue: 30,
+    },
   },
 ];
 
@@ -186,6 +201,9 @@ export const organizationGroups = [
     projectsCount: 3,
     groupMembersCount: 2,
     visibility: 'public',
+    maxAccessLevel: {
+      integerValue: 30,
+    },
   },
   {
     id: 'gid://gitlab/Group/33',
@@ -199,6 +217,9 @@ export const organizationGroups = [
     projectsCount: 3,
     groupMembersCount: 1,
     visibility: 'private',
+    maxAccessLevel: {
+      integerValue: 30,
+    },
   },
   {
     id: 'gid://gitlab/Group/24',
@@ -212,6 +233,9 @@ export const organizationGroups = [
     projectsCount: 1,
     groupMembersCount: 2,
     visibility: 'internal',
+    maxAccessLevel: {
+      integerValue: 30,
+    },
   },
   {
     id: 'gid://gitlab/Group/27',
@@ -225,6 +249,9 @@ export const organizationGroups = [
     projectsCount: 2,
     groupMembersCount: 3,
     visibility: 'public',
+    maxAccessLevel: {
+      integerValue: 30,
+    },
   },
   {
     id: 'gid://gitlab/Group/31',
@@ -237,6 +264,9 @@ export const organizationGroups = [
     projectsCount: 3,
     groupMembersCount: 10,
     visibility: 'private',
+    maxAccessLevel: {
+      integerValue: 30,
+    },
   },
   {
     id: 'gid://gitlab/Group/22',
@@ -250,6 +280,9 @@ export const organizationGroups = [
     projectsCount: 3,
     groupMembersCount: 40,
     visibility: 'internal',
+    maxAccessLevel: {
+      integerValue: 30,
+    },
   },
   {
     id: 'gid://gitlab/Group/35',
@@ -263,6 +296,9 @@ export const organizationGroups = [
     projectsCount: 30,
     groupMembersCount: 100,
     visibility: 'public',
+    maxAccessLevel: {
+      integerValue: 30,
+    },
   },
   {
     id: 'gid://gitlab/Group/73',
@@ -275,6 +311,9 @@ export const organizationGroups = [
     projectsCount: 1,
     groupMembersCount: 1,
     visibility: 'private',
+    maxAccessLevel: {
+      integerValue: 30,
+    },
   },
   {
     id: 'gid://gitlab/Group/74',
@@ -289,6 +328,9 @@ export const organizationGroups = [
     projectsCount: 4,
     groupMembersCount: 4,
     visibility: 'internal',
+    maxAccessLevel: {
+      integerValue: 30,
+    },
   },
 ];
 
diff --git a/app/assets/javascripts/organizations/shared/graphql/queries/groups.query.graphql b/app/assets/javascripts/organizations/shared/graphql/queries/groups.query.graphql
index 32e4c5772c423..c751f436e0ea7 100644
--- a/app/assets/javascripts/organizations/shared/graphql/queries/groups.query.graphql
+++ b/app/assets/javascripts/organizations/shared/graphql/queries/groups.query.graphql
@@ -31,6 +31,9 @@ query getOrganizationGroups(
         projectsCount
         groupMembersCount
         visibility
+        maxAccessLevel {
+          integerValue
+        }
       }
       pageInfo {
         ...PageInfo
diff --git a/app/assets/javascripts/organizations/shared/graphql/queries/projects.query.graphql b/app/assets/javascripts/organizations/shared/graphql/queries/projects.query.graphql
index 69ebd81e2eb91..0b7cd2680d2c5 100644
--- a/app/assets/javascripts/organizations/shared/graphql/queries/projects.query.graphql
+++ b/app/assets/javascripts/organizations/shared/graphql/queries/projects.query.graphql
@@ -34,6 +34,9 @@ query getOrganizationProjects(
         userPermissions {
           removeProject
         }
+        maxAccessLevel {
+          integerValue
+        }
       }
       pageInfo {
         ...PageInfo
diff --git a/app/assets/javascripts/organizations/shared/utils.js b/app/assets/javascripts/organizations/shared/utils.js
index 496cbeb62e4f7..f8ea903ce4fb9 100644
--- a/app/assets/javascripts/organizations/shared/utils.js
+++ b/app/assets/javascripts/organizations/shared/utils.js
@@ -22,6 +22,7 @@ export const formatProjects = (projects) =>
       forkingAccessLevel,
       webUrl,
       userPermissions,
+      maxAccessLevel: accessLevel,
       ...project
     }) => ({
       ...project,
@@ -32,6 +33,7 @@ export const formatProjects = (projects) =>
       forkingAccessLevel: forkingAccessLevel.stringValue,
       webUrl,
       isForked: false,
+      accessLevel,
       editPath: `${webUrl}/edit`,
       availableActions: availableProjectActions(userPermissions),
       actionLoadingStates: {
@@ -41,11 +43,12 @@ export const formatProjects = (projects) =>
   );
 
 export const formatGroups = (groups) =>
-  groups.map(({ id, webUrl, parent, ...group }) => ({
+  groups.map(({ id, webUrl, parent, maxAccessLevel: accessLevel, ...group }) => ({
     ...group,
     id: getIdFromGraphQLId(id),
     webUrl,
     parent: parent?.id || null,
+    accessLevel,
     editPath: `${webUrl}/-/edit`,
     availableActions: [ACTION_EDIT, ACTION_DELETE],
   }));
diff --git a/app/assets/javascripts/vue_shared/components/groups_list/groups_list_item.vue b/app/assets/javascripts/vue_shared/components/groups_list/groups_list_item.vue
index ace3846723c65..7babc11a27729 100644
--- a/app/assets/javascripts/vue_shared/components/groups_list/groups_list_item.vue
+++ b/app/assets/javascripts/vue_shared/components/groups_list/groups_list_item.vue
@@ -3,7 +3,7 @@ import { GlAvatarLabeled, GlIcon, GlTooltipDirective, GlTruncateText, GlBadge }
 import uniqueId from 'lodash/uniqueId';
 
 import { VISIBILITY_TYPE_ICON, GROUP_VISIBILITY_TYPE } from '~/visibility_level/constants';
-import { ACCESS_LEVEL_LABELS } from '~/access_level/constants';
+import { ACCESS_LEVEL_LABELS, ACCESS_LEVEL_NO_ACCESS_INTEGER } from '~/access_level/constants';
 import { __ } from '~/locale';
 import { numberToMetricPrefix } from '~/lib/utils/number_utils';
 import SafeHtml from '~/vue_shared/directives/safe_html';
@@ -66,7 +66,7 @@ export default {
       return ACCESS_LEVEL_LABELS[this.accessLevel];
     },
     shouldShowAccessLevel() {
-      return this.accessLevel !== undefined;
+      return this.accessLevel !== undefined && this.accessLevel !== ACCESS_LEVEL_NO_ACCESS_INTEGER;
     },
     groupIconName() {
       return this.group.parent ? 'subgroup' : 'group';
@@ -138,9 +138,13 @@ export default {
                   />
                 </div>
                 <div class="gl-px-2">
-                  <gl-badge v-if="shouldShowAccessLevel" size="sm" class="gl-display-block">{{
-                    accessLevelLabel
-                  }}</gl-badge>
+                  <gl-badge
+                    v-if="shouldShowAccessLevel"
+                    size="sm"
+                    class="gl-display-block"
+                    data-testid="access-level-badge"
+                    >{{ accessLevelLabel }}</gl-badge
+                  >
                 </div>
               </div>
             </div>
diff --git a/app/assets/javascripts/vue_shared/components/projects_list/projects_list_item.vue b/app/assets/javascripts/vue_shared/components/projects_list/projects_list_item.vue
index 0dc411acc749a..41d9fc10f71e6 100644
--- a/app/assets/javascripts/vue_shared/components/projects_list/projects_list_item.vue
+++ b/app/assets/javascripts/vue_shared/components/projects_list/projects_list_item.vue
@@ -12,7 +12,7 @@ import {
 import uniqueId from 'lodash/uniqueId';
 
 import { VISIBILITY_TYPE_ICON, PROJECT_VISIBILITY_TYPE } from '~/visibility_level/constants';
-import { ACCESS_LEVEL_LABELS } from '~/access_level/constants';
+import { ACCESS_LEVEL_LABELS, ACCESS_LEVEL_NO_ACCESS_INTEGER } from '~/access_level/constants';
 import { FEATURABLE_ENABLED } from '~/featurable/constants';
 import { __ } from '~/locale';
 import { numberToMetricPrefix } from '~/lib/utils/number_utils';
@@ -74,8 +74,8 @@ export default {
      *   issuesAccessLevel: string;
      *   forkingAccessLevel: string;
      *   openIssuesCount: number;
-     *   permissions: {
-     *     projectAccess: { accessLevel: 50 };
+     *   maxAccessLevel: {
+     *     integerValue: number;
      *   };
      *   descriptionHtml: string;
      *   updatedAt: string;
@@ -111,13 +111,13 @@ export default {
       return PROJECT_VISIBILITY_TYPE[this.visibility];
     },
     accessLevel() {
-      return this.project.permissions?.projectAccess?.accessLevel;
+      return this.project.accessLevel?.integerValue;
     },
     accessLevelLabel() {
       return ACCESS_LEVEL_LABELS[this.accessLevel];
     },
     shouldShowAccessLevel() {
-      return this.accessLevel !== undefined;
+      return this.accessLevel !== undefined && this.accessLevel !== ACCESS_LEVEL_NO_ACCESS_INTEGER;
     },
     starsHref() {
       return `${this.project.webUrl}/-/starrers`;
@@ -254,9 +254,13 @@ export default {
                   />
                 </div>
                 <div class="gl-px-2">
-                  <gl-badge v-if="shouldShowAccessLevel" size="sm" class="gl-display-block">{{
-                    accessLevelLabel
-                  }}</gl-badge>
+                  <gl-badge
+                    v-if="shouldShowAccessLevel"
+                    size="sm"
+                    class="gl-display-block"
+                    data-testid="access-level-badge"
+                    >{{ accessLevelLabel }}</gl-badge
+                  >
                 </div>
               </div>
             </div>
diff --git a/spec/frontend/organizations/shared/utils_spec.js b/spec/frontend/organizations/shared/utils_spec.js
index 590996e958a24..b3ef1b6bffcb7 100644
--- a/spec/frontend/organizations/shared/utils_spec.js
+++ b/spec/frontend/organizations/shared/utils_spec.js
@@ -15,6 +15,9 @@ describe('formatProjects', () => {
       mergeRequestsAccessLevel: firstMockProject.mergeRequestsAccessLevel.stringValue,
       issuesAccessLevel: firstMockProject.issuesAccessLevel.stringValue,
       forkingAccessLevel: firstMockProject.forkingAccessLevel.stringValue,
+      accessLevel: {
+        integerValue: 30,
+      },
       availableActions: [ACTION_EDIT, ACTION_DELETE],
       actionLoadingStates: {
         [ACTION_DELETE]: false,
@@ -55,6 +58,9 @@ describe('formatGroups', () => {
       id: getIdFromGraphQLId(firstMockGroup.id),
       parent: null,
       editPath: `${firstFormattedGroup.webUrl}/-/edit`,
+      accessLevel: {
+        integerValue: 30,
+      },
       availableActions: [ACTION_EDIT, ACTION_DELETE],
     });
     expect(formattedGroups.length).toBe(organizationGroups.length);
diff --git a/spec/frontend/vue_shared/components/groups_list/groups_list_item_spec.js b/spec/frontend/vue_shared/components/groups_list/groups_list_item_spec.js
index f0b3328412566..0f6a1d13b8c43 100644
--- a/spec/frontend/vue_shared/components/groups_list/groups_list_item_spec.js
+++ b/spec/frontend/vue_shared/components/groups_list/groups_list_item_spec.js
@@ -7,7 +7,7 @@ import {
   VISIBILITY_LEVEL_INTERNAL_STRING,
   GROUP_VISIBILITY_TYPE,
 } from '~/visibility_level/constants';
-import { ACCESS_LEVEL_LABELS } from '~/access_level/constants';
+import { ACCESS_LEVEL_LABELS, ACCESS_LEVEL_NO_ACCESS_INTEGER } from '~/access_level/constants';
 import ListActions from '~/vue_shared/components/list_actions/list_actions.vue';
 import { ACTION_EDIT, ACTION_DELETE } from '~/vue_shared/components/list_actions/constants';
 import DangerConfirmModal from '~/vue_shared/components/confirm_danger/confirm_danger_modal.vue';
@@ -34,6 +34,7 @@ describe('GroupsListItem', () => {
   const findVisibilityIcon = () => findAvatarLabeled().findComponent(GlIcon);
   const findListActions = () => wrapper.findComponent(ListActions);
   const findConfirmationModal = () => wrapper.findComponent(DangerConfirmModal);
+  const findAccessLevelBadge = () => wrapper.findByTestId('access-level-badge');
 
   it('renders group avatar', () => {
     createComponent();
@@ -108,7 +109,7 @@ describe('GroupsListItem', () => {
     });
   });
 
-  it('renders access role badge', () => {
+  it('renders access level badge', () => {
     createComponent();
 
     expect(findAvatarLabeled().findComponent(GlBadge).text()).toBe(
@@ -116,6 +117,33 @@ describe('GroupsListItem', () => {
     );
   });
 
+  describe('when access level is not available', () => {
+    const { accessLevel, ...groupWithoutAccessLevel } = group;
+    beforeEach(() => {
+      createComponent({
+        propsData: { group: groupWithoutAccessLevel },
+      });
+    });
+
+    it('does not render level role badge', () => {
+      expect(findAccessLevelBadge().exists()).toBe(false);
+    });
+  });
+
+  describe('when access level is `No access`', () => {
+    beforeEach(() => {
+      createComponent({
+        propsData: {
+          group: { ...group, accessLevel: { integerValue: ACCESS_LEVEL_NO_ACCESS_INTEGER } },
+        },
+      });
+    });
+
+    it('does not render level role badge', () => {
+      expect(findAccessLevelBadge().exists()).toBe(false);
+    });
+  });
+
   describe('when group has a description', () => {
     it('renders description', () => {
       const descriptionHtml = '<p>Foo bar</p>';
diff --git a/spec/frontend/vue_shared/components/projects_list/projects_list_item_spec.js b/spec/frontend/vue_shared/components/projects_list/projects_list_item_spec.js
index 7f60195288ced..95a5a56e7874c 100644
--- a/spec/frontend/vue_shared/components/projects_list/projects_list_item_spec.js
+++ b/spec/frontend/vue_shared/components/projects_list/projects_list_item_spec.js
@@ -12,7 +12,7 @@ import {
   VISIBILITY_LEVEL_PRIVATE_STRING,
   PROJECT_VISIBILITY_TYPE,
 } from '~/visibility_level/constants';
-import { ACCESS_LEVEL_LABELS } from '~/access_level/constants';
+import { ACCESS_LEVEL_LABELS, ACCESS_LEVEL_NO_ACCESS_INTEGER } from '~/access_level/constants';
 import { FEATURABLE_DISABLED, FEATURABLE_ENABLED } from '~/featurable/constants';
 import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
 import DeleteModal from '~/projects/components/shared/delete_modal.vue';
@@ -22,9 +22,16 @@ jest.mock('lodash/uniqueId');
 describe('ProjectsListItem', () => {
   let wrapper;
 
-  const [project] = convertObjectPropsToCamelCase(projects, { deep: true });
+  const [{ permissions, ...project }] = convertObjectPropsToCamelCase(projects, { deep: true });
 
-  const defaultPropsData = { project };
+  const defaultPropsData = {
+    project: {
+      ...project,
+      accessLevel: {
+        integerValue: permissions.projectAccess.accessLevel,
+      },
+    },
+  };
 
   const createComponent = ({ propsData = {} } = {}) => {
     wrapper = mountExtended(ProjectsListItem, {
@@ -45,6 +52,7 @@ describe('ProjectsListItem', () => {
   const findProjectDescription = () => wrapper.findByTestId('project-description');
   const findVisibilityIcon = () => findAvatarLabeled().findComponent(GlIcon);
   const findListActions = () => wrapper.findComponent(ListActions);
+  const findAccessLevelBadge = () => wrapper.findByTestId('access-level-badge');
 
   beforeEach(() => {
     uniqueId.mockImplementation(jest.requireActual('lodash/uniqueId'));
@@ -90,14 +98,40 @@ describe('ProjectsListItem', () => {
     });
   });
 
-  it('renders access role badge', () => {
+  it('renders access level badge', () => {
     createComponent();
 
-    expect(findAvatarLabeled().findComponent(GlBadge).text()).toBe(
-      ACCESS_LEVEL_LABELS[project.permissions.projectAccess.accessLevel],
+    expect(findAccessLevelBadge().text()).toBe(
+      ACCESS_LEVEL_LABELS[defaultPropsData.project.accessLevel.integerValue],
     );
   });
 
+  describe('when access level is not available', () => {
+    beforeEach(() => {
+      createComponent({
+        propsData: { project },
+      });
+    });
+
+    it('does not render access level badge', () => {
+      expect(findAccessLevelBadge().exists()).toBe(false);
+    });
+  });
+
+  describe('when access level is `No access`', () => {
+    beforeEach(() => {
+      createComponent({
+        propsData: {
+          project: { ...project, accessLevel: { integerValue: ACCESS_LEVEL_NO_ACCESS_INTEGER } },
+        },
+      });
+    });
+
+    it('does not render access level badge', () => {
+      expect(findAccessLevelBadge().exists()).toBe(false);
+    });
+  });
+
   describe('if project is archived', () => {
     beforeEach(() => {
       createComponent({
-- 
GitLab