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