Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2023-12-02 15:10:59 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2023-12-02 15:10:59 +0300
commitf78aa88c769acebd95eca52b07169a57196a3318 (patch)
treefa4e1ce197ced08f86066e2d8d98e9d7d66a47d1
parentcbd97a2467d53b89fe4896b61ed5ab3f7203f111 (diff)
Add latest changes from gitlab-org/gitlab@master
-rw-r--r--.rubocop_todo/layout/line_continuation_spacing.yml1
-rw-r--r--.rubocop_todo/layout/line_end_string_concatenation_indentation.yml1
-rw-r--r--.rubocop_todo/style/if_unless_modifier.yml1
-rw-r--r--.rubocop_todo/style/inline_disable_annotation.yml1
-rw-r--r--app/assets/javascripts/members/components/table/max_role.vue (renamed from app/assets/javascripts/members/components/table/role_dropdown.vue)90
-rw-r--r--app/assets/javascripts/members/components/table/members_table.vue7
-rw-r--r--app/assets/javascripts/members/index.js4
-rw-r--r--app/assets/javascripts/members/store/actions.js12
-rw-r--r--app/assets/javascripts/members/store/mutation_types.js1
-rw-r--r--app/assets/javascripts/members/store/mutations.js9
-rw-r--r--lib/gitlab/checks/tag_check.rb108
-rw-r--r--locale/gitlab.pot3
-rw-r--r--qa/qa/page/component/members/members_table.rb2
-rw-r--r--spec/frontend/members/components/table/max_role_spec.js (renamed from spec/frontend/members/components/table/role_dropdown_spec.js)93
-rw-r--r--spec/frontend/members/components/table/members_table_spec.js18
-rw-r--r--spec/frontend/members/mock_data.js1
-rw-r--r--spec/frontend/members/store/actions_spec.js21
-rw-r--r--spec/frontend/members/store/mutations_spec.js13
18 files changed, 169 insertions, 217 deletions
diff --git a/.rubocop_todo/layout/line_continuation_spacing.yml b/.rubocop_todo/layout/line_continuation_spacing.yml
index c406fd1cae9..46e997246ea 100644
--- a/.rubocop_todo/layout/line_continuation_spacing.yml
+++ b/.rubocop_todo/layout/line_continuation_spacing.yml
@@ -107,7 +107,6 @@ Layout/LineContinuationSpacing:
- 'lib/api/metrics/dashboard/annotations.rb'
- 'lib/gitlab/auth/user_access_denied_reason.rb'
- 'lib/gitlab/background_migration/populate_operation_visibility_permissions_from_operations.rb'
- - 'lib/gitlab/checks/tag_check.rb'
- 'lib/gitlab/ci/parsers/security/validators/schema_validator.rb'
- 'lib/gitlab/database/background_migration/batched_migration_runner.rb'
- 'lib/gitlab/database/load_balancing/sidekiq_server_middleware.rb'
diff --git a/.rubocop_todo/layout/line_end_string_concatenation_indentation.yml b/.rubocop_todo/layout/line_end_string_concatenation_indentation.yml
index e38219963fd..68d0f57006e 100644
--- a/.rubocop_todo/layout/line_end_string_concatenation_indentation.yml
+++ b/.rubocop_todo/layout/line_end_string_concatenation_indentation.yml
@@ -155,7 +155,6 @@ Layout/LineEndStringConcatenationIndentation:
- 'lib/gitlab/auth.rb'
- 'lib/gitlab/background_migration/populate_operation_visibility_permissions_from_operations.rb'
- 'lib/gitlab/changelog/config.rb'
- - 'lib/gitlab/checks/tag_check.rb'
- 'lib/gitlab/ci/parsers/security/validators/schema_validator.rb'
- 'lib/gitlab/ci/pipeline/chain/populate.rb'
- 'lib/gitlab/ci/pipeline/seed/build.rb'
diff --git a/.rubocop_todo/style/if_unless_modifier.yml b/.rubocop_todo/style/if_unless_modifier.yml
index bf767c1701e..6c52c2d9f8e 100644
--- a/.rubocop_todo/style/if_unless_modifier.yml
+++ b/.rubocop_todo/style/if_unless_modifier.yml
@@ -730,7 +730,6 @@ Style/IfUnlessModifier:
- 'lib/gitlab/checks/matching_merge_request.rb'
- 'lib/gitlab/checks/push_check.rb'
- 'lib/gitlab/checks/push_file_count_check.rb'
- - 'lib/gitlab/checks/tag_check.rb'
- 'lib/gitlab/ci/ansi2html.rb'
- 'lib/gitlab/ci/ansi2json/converter.rb'
- 'lib/gitlab/ci/ansi2json/style.rb'
diff --git a/.rubocop_todo/style/inline_disable_annotation.yml b/.rubocop_todo/style/inline_disable_annotation.yml
index 6140c5227a6..54203bada3d 100644
--- a/.rubocop_todo/style/inline_disable_annotation.yml
+++ b/.rubocop_todo/style/inline_disable_annotation.yml
@@ -2410,7 +2410,6 @@ Style/InlineDisableAnnotation:
- 'lib/gitlab/checks/branch_check.rb'
- 'lib/gitlab/checks/diff_check.rb'
- 'lib/gitlab/checks/matching_merge_request.rb'
- - 'lib/gitlab/checks/tag_check.rb'
- 'lib/gitlab/ci/ansi2html.rb'
- 'lib/gitlab/ci/ansi2json/parser.rb'
- 'lib/gitlab/ci/badge/pipeline/status.rb'
diff --git a/app/assets/javascripts/members/components/table/role_dropdown.vue b/app/assets/javascripts/members/components/table/max_role.vue
index 2b72a3fe6e8..d0b04752ef2 100644
--- a/app/assets/javascripts/members/components/table/role_dropdown.vue
+++ b/app/assets/javascripts/members/components/table/max_role.vue
@@ -1,5 +1,5 @@
<script>
-import { GlCollapsibleListbox } from '@gitlab/ui';
+import { GlBadge, GlCollapsibleListbox } from '@gitlab/ui';
import { GlBreakpointInstance as bp } from '@gitlab/ui/dist/utils';
// eslint-disable-next-line no-restricted-imports
import { mapActions } from 'vuex';
@@ -9,11 +9,12 @@ import { roleDropdownItems, initialSelectedRole } from 'ee_else_ce/members/utils
import { s__ } from '~/locale';
export default {
- name: 'RoleDropdown',
components: {
GlCollapsibleListbox,
+ GlBadge,
LdapDropdownFooter: () =>
import('ee_component/members/components/action_dropdowns/ldap_dropdown_footer.vue'),
+ CustomPermissions: () => import('ee_component/members/components/table/custom_permissions.vue'),
},
inject: ['namespace', 'group'],
props: {
@@ -27,37 +28,40 @@ export default {
},
},
data() {
+ const accessLevelOptions = roleDropdownItems(this.member);
return {
- isDesktop: false,
+ accessLevelOptions,
busy: false,
- selectedRole: null,
+ customPermissions: this.member.customPermissions ?? [],
+ isDesktop: false,
+ memberRoleId: this.member.accessLevel.memberRoleId ?? null,
+ selectedRole: initialSelectedRole(accessLevelOptions.flatten, this.member),
};
},
computed: {
disabled() {
return this.permissions.canOverride && !this.member.isOverridden;
},
- dropdownItems() {
- return roleDropdownItems(this.member);
- },
- },
- created() {
- this.selectedRole = initialSelectedRole(this.dropdownItems.flatten, this.member);
},
mounted() {
this.isDesktop = bp.isDesktop();
},
methods: {
...mapActions({
- updateMemberRole(dispatch, payload) {
- return dispatch(`${this.namespace}/updateMemberRole`, payload);
+ updateMemberRole(dispatch, { memberId, accessLevel, memberRoleId }) {
+ return dispatch(`${this.namespace}/updateMemberRole`, {
+ memberId,
+ accessLevel,
+ memberRoleId,
+ });
},
}),
async handleSelect(value) {
this.busy = true;
- const newRole = this.dropdownItems.flatten.find((item) => item.value === value);
+ const newRole = this.accessLevelOptions.flatten.find((item) => item.value === value);
const previousRole = this.selectedRole;
+ const previousMemberRoleId = this.memberRoleId;
try {
const confirmed = await guestOverageConfirmAction({
@@ -74,18 +78,18 @@ export default {
}
this.selectedRole = value;
+ this.memberRoleId = newRole.memberRoleId;
await this.updateMemberRole({
memberId: this.member.id,
- accessLevel: {
- integerValue: newRole.accessLevel,
- memberRoleId: newRole.memberRoleId,
- },
+ accessLevel: newRole.accessLevel,
+ memberRoleId: newRole.memberRoleId,
});
this.$toast.show(s__('Members|Role updated successfully.'));
} catch (error) {
this.selectedRole = previousRole;
+ this.memberRoleId = previousMemberRoleId;
Sentry.captureException(error);
} finally {
this.busy = false;
@@ -96,25 +100,35 @@ export default {
</script>
<template>
- <gl-collapsible-listbox
- :placement="isDesktop ? 'left' : 'right'"
- :toggle-text="member.accessLevel.stringValue"
- :header-text="__('Change role')"
- :disabled="disabled"
- :loading="busy"
- data-qa-selector="access_level_dropdown"
- :items="dropdownItems.formatted"
- :selected="selectedRole"
- @select="handleSelect"
- >
- <template #list-item="{ item }">
- <span data-qa-selector="access_level_link">{{ item.text }}</span>
- </template>
- <template #footer>
- <ldap-dropdown-footer
- v-if="permissions.canOverride && member.isOverridden"
- :member-id="member.id"
- />
- </template>
- </gl-collapsible-listbox>
+ <div>
+ <gl-collapsible-listbox
+ v-if="permissions.canUpdate"
+ :placement="isDesktop ? 'left' : 'right'"
+ :header-text="__('Change role')"
+ :disabled="disabled"
+ :loading="busy"
+ data-qa-selector="access_level_dropdown"
+ :items="accessLevelOptions.formatted"
+ :selected="selectedRole"
+ @select="handleSelect"
+ >
+ <template #list-item="{ item }">
+ <span data-qa-selector="access_level_link">{{ item.text }}</span>
+ </template>
+ <template #footer>
+ <ldap-dropdown-footer
+ v-if="permissions.canOverride && member.isOverridden"
+ :member-id="member.id"
+ />
+ </template>
+ </gl-collapsible-listbox>
+
+ <gl-badge v-else>{{ member.accessLevel.stringValue }}</gl-badge>
+
+ <custom-permissions
+ v-if="memberRoleId !== null"
+ :member-role-id="memberRoleId"
+ :custom-permissions="customPermissions"
+ />
+ </div>
</template>
diff --git a/app/assets/javascripts/members/components/table/members_table.vue b/app/assets/javascripts/members/components/table/members_table.vue
index 2b3294c1c79..149947397ad 100644
--- a/app/assets/javascripts/members/components/table/members_table.vue
+++ b/app/assets/javascripts/members/components/table/members_table.vue
@@ -31,7 +31,7 @@ import MemberActions from './member_actions.vue';
import MemberAvatar from './member_avatar.vue';
import MemberSource from './member_source.vue';
import MemberActivity from './member_activity.vue';
-import RoleDropdown from './role_dropdown.vue';
+import MaxRole from './max_role.vue';
export default {
name: 'MembersTable',
@@ -44,7 +44,7 @@ export default {
MembersTableCell,
MemberSource,
MemberActions,
- RoleDropdown,
+ MaxRole,
RemoveGroupLinkModal,
RemoveMemberModal,
ExpirationDatepicker,
@@ -292,8 +292,7 @@ export default {
<template #cell(maxRole)="{ item: member }">
<members-table-cell #default="{ permissions }" :member="member">
- <role-dropdown v-if="permissions.canUpdate" :permissions="permissions" :member="member" />
- <gl-badge v-else>{{ member.accessLevel.stringValue }}</gl-badge>
+ <max-role :permissions="permissions" :member="member" />
</members-table-cell>
</template>
diff --git a/app/assets/javascripts/members/index.js b/app/assets/javascripts/members/index.js
index 87ae670c146..ad477d8b4b6 100644
--- a/app/assets/javascripts/members/index.js
+++ b/app/assets/javascripts/members/index.js
@@ -2,6 +2,8 @@ import { GlToast } from '@gitlab/ui';
import Vue from 'vue';
// eslint-disable-next-line no-restricted-imports
import Vuex from 'vuex';
+import VueApollo from 'vue-apollo';
+import createDefaultClient from '~/lib/graphql';
import { parseDataAttributes } from '~/members/utils';
import { MEMBER_TYPES } from 'ee_else_ce/members/constants';
import MembersTabs from './components/members_tabs.vue';
@@ -13,6 +15,7 @@ export const initMembersApp = (el, options) => {
}
Vue.use(Vuex);
+ Vue.use(VueApollo);
Vue.use(GlToast);
const {
@@ -61,6 +64,7 @@ export const initMembersApp = (el, options) => {
el,
components: { MembersTabs },
store,
+ apolloProvider: new VueApollo({ defaultClient: createDefaultClient() }),
provide: {
currentUserId: gon.current_user_id || null,
sourceId,
diff --git a/app/assets/javascripts/members/store/actions.js b/app/assets/javascripts/members/store/actions.js
index d696f618a3c..54a53e7c0a9 100644
--- a/app/assets/javascripts/members/store/actions.js
+++ b/app/assets/javascripts/members/store/actions.js
@@ -2,17 +2,15 @@ import axios from '~/lib/utils/axios_utils';
import { formatDate } from '~/lib/utils/datetime_utility';
import * as types from './mutation_types';
-export const updateMemberRole = async ({ state, commit }, { memberId, accessLevel }) => {
+export const updateMemberRole = async (
+ { state, commit },
+ { memberId, accessLevel, memberRoleId },
+) => {
try {
await axios.put(
state.memberPath.replace(/:id$/, memberId),
- state.requestFormatter({
- accessLevel: accessLevel.integerValue,
- memberRoleId: accessLevel.memberRoleId,
- }),
+ state.requestFormatter({ accessLevel, memberRoleId }),
);
-
- commit(types.RECEIVE_MEMBER_ROLE_SUCCESS, { memberId, accessLevel });
} catch (error) {
commit(types.RECEIVE_MEMBER_ROLE_ERROR, { error });
diff --git a/app/assets/javascripts/members/store/mutation_types.js b/app/assets/javascripts/members/store/mutation_types.js
index 5fa75725552..c1cdbf6146f 100644
--- a/app/assets/javascripts/members/store/mutation_types.js
+++ b/app/assets/javascripts/members/store/mutation_types.js
@@ -1,4 +1,3 @@
-export const RECEIVE_MEMBER_ROLE_SUCCESS = 'RECEIVE_MEMBER_ROLE_SUCCESS';
export const RECEIVE_MEMBER_ROLE_ERROR = 'RECEIVE_MEMBER_ROLE_ERROR';
export const RECEIVE_MEMBER_EXPIRATION_SUCCESS = 'RECEIVE_MEMBER_EXPIRATION_SUCCESS';
diff --git a/app/assets/javascripts/members/store/mutations.js b/app/assets/javascripts/members/store/mutations.js
index b4cf9f3480f..edc400aef7d 100644
--- a/app/assets/javascripts/members/store/mutations.js
+++ b/app/assets/javascripts/members/store/mutations.js
@@ -4,15 +4,6 @@ import * as types from './mutation_types';
import { findMember } from './utils';
export default {
- [types.RECEIVE_MEMBER_ROLE_SUCCESS](state, { memberId, accessLevel }) {
- const member = findMember(state, memberId);
-
- if (!member) {
- return;
- }
-
- Vue.set(member, 'accessLevel', accessLevel);
- },
[types.RECEIVE_MEMBER_ROLE_ERROR](state, { error }) {
state.errorMessage =
error.response?.data?.message ||
diff --git a/lib/gitlab/checks/tag_check.rb b/lib/gitlab/checks/tag_check.rb
index d5addab74b8..5684b897256 100644
--- a/lib/gitlab/checks/tag_check.rb
+++ b/lib/gitlab/checks/tag_check.rb
@@ -6,8 +6,8 @@ module Gitlab
ERROR_MESSAGES = {
change_existing_tags: 'You are not allowed to change existing tags on this project.',
update_protected_tag: 'Protected tags cannot be updated.',
- delete_protected_tag: 'You are not allowed to delete protected tags from this project. '\
- 'Only a project maintainer or owner can delete a protected tag.',
+ delete_protected_tag: 'You are not allowed to delete protected tags from this project. ' \
+ 'Only a project maintainer or owner can delete a protected tag.',
delete_protected_tag_non_web: 'You can only delete protected tags using the web interface.',
create_protected_tag: 'You are not allowed to create this tag as it is protected.',
default_branch_collision: 'You cannot use default branch name to create a tag',
@@ -24,69 +24,85 @@ module Gitlab
def validate!
return unless tag_name
- logger.log_timed(LOG_MESSAGES[:tag_checks]) do
- if tag_exists? && user_access.cannot_do_action?(:admin_tag)
- raise GitAccess::ForbiddenError, ERROR_MESSAGES[:change_existing_tags]
- end
- end
-
- default_branch_collision_check
+ logger.log_timed(LOG_MESSAGES[:tag_checks]) { tag_checks }
+ logger.log_timed(LOG_MESSAGES[:default_branch_collision_check]) { default_branch_collision_check }
prohibited_tag_checks
- protected_tag_checks
+ logger.log_timed(LOG_MESSAGES[:protected_tag_checks]) { protected_tag_checks }
end
private
+ def tag_checks
+ return unless tag_exists? && user_access.cannot_do_action?(:admin_tag)
+
+ raise GitAccess::ForbiddenError, ERROR_MESSAGES[:change_existing_tags]
+ end
+
+ def default_branch_collision_check
+ return unless creation? && tag_name == project.default_branch
+
+ raise GitAccess::ForbiddenError, ERROR_MESSAGES[:default_branch_collision]
+ end
+
def prohibited_tag_checks
return if deletion?
- unless Gitlab::GitRefValidator.validate(tag_name)
- raise GitAccess::ForbiddenError, ERROR_MESSAGES[:prohibited_tag_name]
- end
+ # Incorrectly encoded tags names may raise during other checks so we
+ # need to validate the encoding first
+ validate_encoding!
+ validate_valid_tag_name!
+ validate_tag_name_not_fully_qualified!
+ end
- if tag_name.start_with?("refs/tags/") # rubocop: disable Style/GuardClause
- raise GitAccess::ForbiddenError, ERROR_MESSAGES[:prohibited_tag_name]
- end
+ def protected_tag_checks
+ return unless ProtectedTag.protected?(project, tag_name)
- # rubocop: disable Style/GuardClause
- # rubocop: disable Style/SoleNestedConditional
- if Feature.enabled?(:prohibited_tag_name_encoding_check, project)
- unless Gitlab::EncodingHelper.force_encode_utf8(tag_name).valid_encoding?
- raise GitAccess::ForbiddenError, ERROR_MESSAGES[:prohibited_tag_name_encoding]
- end
- end
- # rubocop: enable Style/SoleNestedConditional
- # rubocop: enable Style/GuardClause
+ validate_protected_tag_update!
+ validate_protected_tag_deletion!
+ validate_protected_tag_creation!
end
- def protected_tag_checks
- logger.log_timed(LOG_MESSAGES[__method__]) do
- return unless ProtectedTag.protected?(project, tag_name) # rubocop:disable Cop/AvoidReturnFromBlocks
+ def validate_encoding!
+ return unless Feature.enabled?(:prohibited_tag_name_encoding_check, project)
+ return if Gitlab::EncodingHelper.force_encode_utf8(tag_name).valid_encoding?
- raise(GitAccess::ForbiddenError, ERROR_MESSAGES[:update_protected_tag]) if update?
+ raise GitAccess::ForbiddenError, ERROR_MESSAGES[:prohibited_tag_name_encoding]
+ end
+
+ def validate_valid_tag_name!
+ return if Gitlab::GitRefValidator.validate(tag_name)
- if deletion?
- unless user_access.user.can?(:maintainer_access, project)
- raise(GitAccess::ForbiddenError, ERROR_MESSAGES[:delete_protected_tag])
- end
+ raise GitAccess::ForbiddenError, ERROR_MESSAGES[:prohibited_tag_name]
+ end
- unless updated_from_web?
- raise GitAccess::ForbiddenError, ERROR_MESSAGES[:delete_protected_tag_non_web]
- end
- end
+ def validate_tag_name_not_fully_qualified!
+ return unless tag_name.start_with?("refs/tags/")
- unless user_access.can_create_tag?(tag_name)
- raise GitAccess::ForbiddenError, ERROR_MESSAGES[:create_protected_tag]
- end
- end
+ raise GitAccess::ForbiddenError, ERROR_MESSAGES[:prohibited_tag_name]
end
- def default_branch_collision_check
- logger.log_timed(LOG_MESSAGES[:default_branch_collision_check]) do
- if creation? && tag_name == project.default_branch
- raise GitAccess::ForbiddenError, ERROR_MESSAGES[:default_branch_collision]
- end
+ def validate_protected_tag_update!
+ return unless update?
+
+ raise(GitAccess::ForbiddenError, ERROR_MESSAGES[:update_protected_tag])
+ end
+
+ def validate_protected_tag_deletion!
+ return unless deletion?
+
+ unless user_access.user.can?(:maintainer_access, project)
+ raise(GitAccess::ForbiddenError, ERROR_MESSAGES[:delete_protected_tag])
end
+
+ return if updated_from_web?
+
+ raise GitAccess::ForbiddenError, ERROR_MESSAGES[:delete_protected_tag_non_web]
+ end
+
+ def validate_protected_tag_creation!
+ return if user_access.can_create_tag?(tag_name)
+
+ raise GitAccess::ForbiddenError, ERROR_MESSAGES[:create_protected_tag]
end
end
end
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 71dfa498a66..2ebe97ba74b 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -29632,6 +29632,9 @@ msgstr ""
msgid "MemberRole|%{requirement} has to be enabled in order to enable %{permission}."
msgstr ""
+msgid "MemberRole|Custom permissions:"
+msgstr ""
+
msgid "MemberRole|can't be changed"
msgstr ""
diff --git a/qa/qa/page/component/members/members_table.rb b/qa/qa/page/component/members/members_table.rb
index 8bc20da83af..655a28f947e 100644
--- a/qa/qa/page/component/members/members_table.rb
+++ b/qa/qa/page/component/members/members_table.rb
@@ -21,7 +21,7 @@ module QA
element :member_row
end
- base.view 'app/assets/javascripts/members/components/table/role_dropdown.vue' do
+ base.view 'app/assets/javascripts/members/components/table/max_role.vue' do
element :access_level_dropdown
element :access_level_link
end
diff --git a/spec/frontend/members/components/table/role_dropdown_spec.js b/spec/frontend/members/components/table/max_role_spec.js
index 62275a05dc5..75e1e05afb1 100644
--- a/spec/frontend/members/components/table/role_dropdown_spec.js
+++ b/spec/frontend/members/components/table/max_role_spec.js
@@ -1,4 +1,4 @@
-import { GlCollapsibleListbox, GlListboxItem } from '@gitlab/ui';
+import { GlBadge, GlCollapsibleListbox, GlListboxItem } from '@gitlab/ui';
import { GlBreakpointInstance as bp } from '@gitlab/ui/dist/utils';
import { mount } from '@vue/test-utils';
import Vue, { nextTick } from 'vue';
@@ -6,16 +6,19 @@ import Vue, { nextTick } from 'vue';
import Vuex from 'vuex';
import * as Sentry from '~/sentry/sentry_browser_wrapper';
import waitForPromises from 'helpers/wait_for_promises';
-import RoleDropdown from '~/members/components/table/role_dropdown.vue';
+import MaxRole from '~/members/components/table/max_role.vue';
import { MEMBER_TYPES } from '~/members/constants';
import { guestOverageConfirmAction } from 'ee_else_ce/members/guest_overage_confirm_action';
import { member } from '../../mock_data';
Vue.use(Vuex);
+
jest.mock('ee_else_ce/members/guest_overage_confirm_action');
jest.mock('~/sentry/sentry_browser_wrapper');
-describe('RoleDropdown', () => {
+guestOverageConfirmAction.mockReturnValue(true);
+
+describe('MaxRole', () => {
let wrapper;
let actions;
const $toast = {
@@ -35,7 +38,7 @@ describe('RoleDropdown', () => {
};
const createComponent = (propsData = {}, store = createStore()) => {
- wrapper = mount(RoleDropdown, {
+ wrapper = mount(MaxRole, {
provide: {
namespace: MEMBER_TYPES.user,
group: {
@@ -45,7 +48,9 @@ describe('RoleDropdown', () => {
},
propsData: {
member,
- permissions: {},
+ permissions: {
+ canUpdate: true,
+ },
...propsData,
},
store,
@@ -55,6 +60,7 @@ describe('RoleDropdown', () => {
});
};
+ const findBadge = () => wrapper.findComponent(GlBadge);
const findListbox = () => wrapper.findComponent(GlCollapsibleListbox);
const findListboxItems = () => wrapper.findAllComponents(GlListboxItem);
const findListboxItemByText = (text) =>
@@ -64,6 +70,18 @@ describe('RoleDropdown', () => {
gon.features = { showOverageOnRolePromotion: true };
});
+ describe('when member can not be updated', () => {
+ it('renders a badge instead of a collapsible listbox', () => {
+ createComponent({
+ permissions: {
+ canUpdate: false,
+ },
+ });
+
+ expect(findBadge().text()).toBe('Owner');
+ });
+ });
+
it('has correct header text props', () => {
createComponent();
expect(findListbox().props('headerText')).toBe('Change role');
@@ -77,14 +95,12 @@ describe('RoleDropdown', () => {
describe('when listbox is open', () => {
beforeEach(async () => {
- guestOverageConfirmAction.mockReturnValue(true);
createComponent();
await findListbox().vm.$emit('click');
});
it('sets dropdown toggle and checks selected role', () => {
- expect(findListbox().props('toggleText')).toBe('Owner');
expect(findListbox().find('[aria-selected=true]').text()).toBe('Owner');
});
@@ -100,7 +116,8 @@ describe('RoleDropdown', () => {
expect(actions.updateMemberRole).toHaveBeenCalledWith(expect.any(Object), {
memberId: member.id,
- accessLevel: { integerValue: 30, memberRoleId: null },
+ accessLevel: 30,
+ memberRoleId: null,
});
});
@@ -108,7 +125,7 @@ describe('RoleDropdown', () => {
it('displays toast', async () => {
await findListboxItemByText('Developer').trigger('click');
- await nextTick();
+ await waitForPromises();
expect($toast.show).toHaveBeenCalledWith('Role updated successfully.');
});
@@ -146,7 +163,7 @@ describe('RoleDropdown', () => {
it('does not display toast', async () => {
await findListboxItemByText('Developer').trigger('click');
- await nextTick();
+ await waitForPromises();
expect($toast.show).not.toHaveBeenCalled();
});
@@ -176,12 +193,6 @@ describe('RoleDropdown', () => {
});
});
- it("sets initial dropdown toggle value to member's role", () => {
- createComponent();
-
- expect(findListbox().props('toggleText')).toBe('Owner');
- });
-
it('sets the dropdown alignment to right on mobile', async () => {
jest.spyOn(bp, 'isDesktop').mockReturnValue(false);
createComponent();
@@ -199,54 +210,4 @@ describe('RoleDropdown', () => {
expect(findListbox().props('placement')).toBe('left');
});
-
- describe('guestOverageConfirmAction', () => {
- const mockConfirmAction = ({ confirmed }) => {
- guestOverageConfirmAction.mockResolvedValueOnce(confirmed);
- };
-
- beforeEach(() => {
- createComponent();
-
- findListbox().vm.$emit('click');
- });
-
- afterEach(() => {
- guestOverageConfirmAction.mockReset();
- });
-
- describe('when guestOverageConfirmAction returns true', () => {
- beforeEach(() => {
- mockConfirmAction({ confirmed: true });
-
- findListboxItemByText('Reporter').trigger('click');
- });
-
- it('calls updateMemberRole', () => {
- expect(actions.updateMemberRole).toHaveBeenCalled();
- });
- });
-
- describe('when guestOverageConfirmAction returns false', () => {
- beforeEach(() => {
- mockConfirmAction({ confirmed: false });
-
- findListboxItemByText('Reporter').trigger('click');
- });
-
- it('does not call updateMemberRole', () => {
- expect(actions.updateMemberRole).not.toHaveBeenCalled();
- });
-
- it('re-enables dropdown', async () => {
- await waitForPromises();
-
- expect(findListbox().props('disabled')).toBe(false);
- });
-
- it('resets selected dropdown item', () => {
- expect(findListbox().props('selected')).toMatch(/role-static-\d+/);
- });
- });
- });
});
diff --git a/spec/frontend/members/components/table/members_table_spec.js b/spec/frontend/members/components/table/members_table_spec.js
index 791155fcd1b..c2400fbc142 100644
--- a/spec/frontend/members/components/table/members_table_spec.js
+++ b/spec/frontend/members/components/table/members_table_spec.js
@@ -1,4 +1,4 @@
-import { GlBadge, GlPagination, GlTable } from '@gitlab/ui';
+import { GlPagination, GlTable } from '@gitlab/ui';
import Vue from 'vue';
// eslint-disable-next-line no-restricted-imports
import Vuex from 'vuex';
@@ -11,7 +11,7 @@ import MemberAvatar from '~/members/components/table/member_avatar.vue';
import MemberSource from '~/members/components/table/member_source.vue';
import MemberActivity from '~/members/components/table/member_activity.vue';
import MembersTable from '~/members/components/table/members_table.vue';
-import RoleDropdown from '~/members/components/table/role_dropdown.vue';
+import MaxRole from '~/members/components/table/max_role.vue';
import {
MEMBER_TYPES,
MEMBER_STATE_CREATED,
@@ -74,7 +74,7 @@ describe('MembersTable', () => {
'member-source',
'created-at',
'member-actions',
- 'role-dropdown',
+ 'max-role',
'remove-group-link-modal',
'remove-member-modal',
'expiration-datepicker',
@@ -110,7 +110,7 @@ describe('MembersTable', () => {
${'source'} | ${'Source'} | ${memberMock} | ${MemberSource}
${'invited'} | ${'Invited'} | ${invite} | ${CreatedAt}
${'requested'} | ${'Requested'} | ${accessRequest} | ${CreatedAt}
- ${'maxRole'} | ${'Max role'} | ${memberCanUpdate} | ${RoleDropdown}
+ ${'maxRole'} | ${'Max role'} | ${memberCanUpdate} | ${MaxRole}
${'expiration'} | ${'Expiration'} | ${memberMock} | ${ExpirationDatepicker}
${'activity'} | ${'Activity'} | ${memberMock} | ${MemberActivity}
`('renders the $label field', ({ field, label, member, expectedComponent }) => {
@@ -274,16 +274,6 @@ describe('MembersTable', () => {
});
});
- describe('when member can not be updated', () => {
- it('renders badge in "Max role" field', () => {
- createComponent({ members: [memberMock], tableFields: ['maxRole'] });
-
- expect(
- wrapper.find(`[data-label="Max role"][role="cell"]`).findComponent(GlBadge).text(),
- ).toBe(memberMock.accessLevel.stringValue);
- });
- });
-
it('adds QA selector to table', () => {
createComponent();
diff --git a/spec/frontend/members/mock_data.js b/spec/frontend/members/mock_data.js
index e0dc765b9e4..f550039bfdc 100644
--- a/spec/frontend/members/mock_data.js
+++ b/spec/frontend/members/mock_data.js
@@ -51,6 +51,7 @@ export const member = {
'Minimal access': 5,
},
customRoles: [],
+ customPermissions: [],
};
export const group = {
diff --git a/spec/frontend/members/store/actions_spec.js b/spec/frontend/members/store/actions_spec.js
index 1e7f09fe62b..30ea83abf22 100644
--- a/spec/frontend/members/store/actions_spec.js
+++ b/spec/frontend/members/store/actions_spec.js
@@ -37,28 +37,21 @@ describe('Vuex members actions', () => {
describe('updateMemberRole', () => {
const memberId = members[0].id;
- const accessLevel = { integerValue: 30, memberRoleId: 90 };
+ const accessLevel = 30;
+ const memberRoleId = 90;
- const payload = {
- memberId,
- accessLevel,
- };
+ const payload = { memberId, accessLevel, memberRoleId };
describe('successful request', () => {
- it(`commits ${types.RECEIVE_MEMBER_ROLE_SUCCESS} mutation`, async () => {
+ it(`updates member role`, async () => {
mock.onPut().replyOnce(HTTP_STATUS_OK);
- await testAction(updateMemberRole, payload, state, [
- {
- type: types.RECEIVE_MEMBER_ROLE_SUCCESS,
- payload,
- },
- ]);
+ await testAction(updateMemberRole, payload, state, []);
expect(mock.history.put[0].url).toBe('/groups/foo-bar/-/group_members/238');
expect(mockedRequestFormatter).toHaveBeenCalledWith({
- accessLevel: accessLevel.integerValue,
- memberRoleId: accessLevel.memberRoleId,
+ accessLevel,
+ memberRoleId,
});
});
});
diff --git a/spec/frontend/members/store/mutations_spec.js b/spec/frontend/members/store/mutations_spec.js
index 8160cc373d8..240a14b2836 100644
--- a/spec/frontend/members/store/mutations_spec.js
+++ b/spec/frontend/members/store/mutations_spec.js
@@ -14,19 +14,6 @@ describe('Vuex members mutations', () => {
};
});
- describe(types.RECEIVE_MEMBER_ROLE_SUCCESS, () => {
- it('updates member', () => {
- const accessLevel = { integerValue: 30, stringValue: 'Developer' };
-
- mutations[types.RECEIVE_MEMBER_ROLE_SUCCESS](state, {
- memberId: members[0].id,
- accessLevel,
- });
-
- expect(state.members[0].accessLevel).toEqual(accessLevel);
- });
- });
-
describe(types.RECEIVE_MEMBER_ROLE_ERROR, () => {
describe('when error does not have a message', () => {
it('shows default error message', () => {