summaryrefslogtreecommitdiff
path: root/app/assets/javascripts/members
diff options
context:
space:
mode:
Diffstat (limited to 'app/assets/javascripts/members')
-rw-r--r--app/assets/javascripts/members/components/action_buttons/remove_member_button.vue35
-rw-r--r--app/assets/javascripts/members/components/filter_sort/members_filtered_search_bar.vue25
-rw-r--r--app/assets/javascripts/members/components/members_tabs.vue22
-rw-r--r--app/assets/javascripts/members/components/modals/remove_member_modal.vue114
-rw-r--r--app/assets/javascripts/members/components/table/members_table.vue3
-rw-r--r--app/assets/javascripts/members/index.js10
-rw-r--r--app/assets/javascripts/members/store/actions.js8
-rw-r--r--app/assets/javascripts/members/store/mutation_types.js3
-rw-r--r--app/assets/javascripts/members/store/mutations.js7
-rw-r--r--app/assets/javascripts/members/store/state.js2
10 files changed, 197 insertions, 32 deletions
diff --git a/app/assets/javascripts/members/components/action_buttons/remove_member_button.vue b/app/assets/javascripts/members/components/action_buttons/remove_member_button.vue
index a477aedd233..665e8ee69f7 100644
--- a/app/assets/javascripts/members/components/action_buttons/remove_member_button.vue
+++ b/app/assets/javascripts/members/components/action_buttons/remove_member_button.vue
@@ -1,6 +1,6 @@
<script>
import { GlButton, GlTooltipDirective } from '@gitlab/ui';
-import { mapState } from 'vuex';
+import { mapActions, mapState } from 'vuex';
export default {
name: 'RemoveMemberButton',
@@ -45,7 +45,7 @@ export default {
oncallSchedules: {
type: Object,
required: false,
- default: () => {},
+ default: () => ({}),
},
},
computed: {
@@ -54,30 +54,35 @@ export default {
return state[this.namespace].memberPath;
},
}),
- computedMemberPath() {
- return this.memberPath.replace(':id', this.memberId);
- },
- stringifiedSchedules() {
- return JSON.stringify(this.oncallSchedules);
+ modalData() {
+ return {
+ isAccessRequest: this.isAccessRequest,
+ isInvite: this.isInvite,
+ memberPath: this.memberPath.replace(':id', this.memberId),
+ memberType: this.memberType,
+ message: this.message,
+ oncallSchedules: this.oncallSchedules,
+ };
},
},
+ methods: {
+ ...mapActions({
+ showRemoveMemberModal(dispatch, payload) {
+ return dispatch(`${this.namespace}/showRemoveMemberModal`, payload);
+ },
+ }),
+ },
};
</script>
<template>
<gl-button
- v-gl-tooltip.hover
- class="js-remove-member-button"
+ v-gl-tooltip
variant="danger"
:title="title"
:aria-label="title"
:icon="icon"
- :data-member-path="computedMemberPath"
- :data-member-type="memberType"
- :data-is-access-request="isAccessRequest"
- :data-is-invite="isInvite"
- :data-message="message"
- :data-oncall-schedules="stringifiedSchedules"
data-qa-selector="delete_member_button"
+ @click="showRemoveMemberModal(modalData)"
/>
</template>
diff --git a/app/assets/javascripts/members/components/filter_sort/members_filtered_search_bar.vue b/app/assets/javascripts/members/components/filter_sort/members_filtered_search_bar.vue
index 33d86dec767..e9329fb1d88 100644
--- a/app/assets/javascripts/members/components/filter_sort/members_filtered_search_bar.vue
+++ b/app/assets/javascripts/members/components/filter_sort/members_filtered_search_bar.vue
@@ -1,7 +1,12 @@
<script>
import { GlFilteredSearchToken } from '@gitlab/ui';
import { mapState } from 'vuex';
-import { getParameterByName, setUrlParams, queryToObject } from '~/lib/utils/url_utility';
+import {
+ getParameterByName,
+ setUrlParams,
+ queryToObject,
+ redirectTo,
+} from '~/lib/utils/url_utility';
import { s__ } from '~/locale';
import {
SEARCH_TOKEN_TYPE,
@@ -122,14 +127,16 @@ export default {
const sortParamValue = getParameterByName(SORT_QUERY_PARAM_NAME);
const activeTabParamValue = getParameterByName(ACTIVE_TAB_QUERY_PARAM_NAME);
- window.location.href = setUrlParams(
- {
- ...params,
- ...(sortParamValue && { [SORT_QUERY_PARAM_NAME]: sortParamValue }),
- ...(activeTabParamValue && { [ACTIVE_TAB_QUERY_PARAM_NAME]: activeTabParamValue }),
- },
- window.location.href,
- true,
+ redirectTo(
+ setUrlParams(
+ {
+ ...params,
+ ...(sortParamValue && { [SORT_QUERY_PARAM_NAME]: sortParamValue }),
+ ...(activeTabParamValue && { [ACTIVE_TAB_QUERY_PARAM_NAME]: activeTabParamValue }),
+ },
+ window.location.href,
+ true,
+ ),
);
},
},
diff --git a/app/assets/javascripts/members/components/members_tabs.vue b/app/assets/javascripts/members/components/members_tabs.vue
index 7c21e33d892..ee4743010cf 100644
--- a/app/assets/javascripts/members/components/members_tabs.vue
+++ b/app/assets/javascripts/members/components/members_tabs.vue
@@ -1,8 +1,7 @@
<script>
-import { GlTabs, GlTab, GlBadge } from '@gitlab/ui';
+import { GlTabs, GlTab, GlBadge, GlButton } from '@gitlab/ui';
import { mapState } from 'vuex';
-// eslint-disable-next-line import/no-deprecated
-import { urlParamsToObject } from '~/lib/utils/url_utility';
+import { queryToObject } from '~/lib/utils/url_utility';
import { __ } from '~/locale';
import { MEMBER_TYPES, TAB_QUERY_PARAM_VALUES, ACTIVE_TAB_QUERY_PARAM_NAME } from '../constants';
import MembersApp from './app.vue';
@@ -36,8 +35,8 @@ export default {
queryParamValue: TAB_QUERY_PARAM_VALUES.accessRequest,
},
],
- components: { MembersApp, GlTabs, GlTab, GlBadge },
- inject: ['canManageMembers'],
+ components: { MembersApp, GlTabs, GlTab, GlBadge, GlButton },
+ inject: ['canManageMembers', 'canExportMembers', 'exportCsvPath'],
data() {
return {
selectedTabIndex: 0,
@@ -59,8 +58,7 @@ export default {
},
}),
urlParams() {
- // eslint-disable-next-line import/no-deprecated
- return Object.keys(urlParamsToObject(window.location.search));
+ return Object.keys(queryToObject(window.location.search, { gatherArrays: true }));
},
activeTabIndexCalculatedFromUrlParams() {
return this.$options.TABS.findIndex(({ namespace }) => {
@@ -123,5 +121,15 @@ export default {
<members-app :namespace="tab.namespace" :tab-query-param-value="tab.queryParamValue" />
</gl-tab>
</template>
+ <template #tabs-end>
+ <gl-button
+ v-if="canExportMembers"
+ class="gl-align-self-center gl-ml-auto"
+ icon="export"
+ :href="exportCsvPath"
+ >
+ {{ __('Export as CSV') }}
+ </gl-button>
+ </template>
</gl-tabs>
</template>
diff --git a/app/assets/javascripts/members/components/modals/remove_member_modal.vue b/app/assets/javascripts/members/components/modals/remove_member_modal.vue
new file mode 100644
index 00000000000..00b6ebf9a73
--- /dev/null
+++ b/app/assets/javascripts/members/components/modals/remove_member_modal.vue
@@ -0,0 +1,114 @@
+<script>
+import { GlFormCheckbox, GlModal } from '@gitlab/ui';
+import { mapActions, mapState } from 'vuex';
+import csrf from '~/lib/utils/csrf';
+import { s__, __ } from '~/locale';
+import OncallSchedulesList from '~/vue_shared/components/oncall_schedules_list.vue';
+
+export default {
+ actionCancel: {
+ text: __('Cancel'),
+ },
+ csrf,
+ components: {
+ GlFormCheckbox,
+ GlModal,
+ OncallSchedulesList,
+ },
+ inject: ['namespace'],
+ computed: {
+ ...mapState({
+ isAccessRequest(state) {
+ return state[this.namespace].removeMemberModalData.isAccessRequest;
+ },
+ isInvite(state) {
+ return state[this.namespace].removeMemberModalData.isInvite;
+ },
+ memberPath(state) {
+ return state[this.namespace].removeMemberModalData.memberPath;
+ },
+ memberType(state) {
+ return state[this.namespace].removeMemberModalData.memberType;
+ },
+ message(state) {
+ return state[this.namespace].removeMemberModalData.message;
+ },
+ oncallSchedules(state) {
+ return state[this.namespace].removeMemberModalData.oncallSchedules ?? {};
+ },
+ removeMemberModalVisible(state) {
+ return state[this.namespace].removeMemberModalVisible;
+ },
+ }),
+ isGroupMember() {
+ return this.memberType === 'GroupMember';
+ },
+ actionText() {
+ if (this.isAccessRequest) {
+ return __('Deny access request');
+ } else if (this.isInvite) {
+ return s__('Member|Revoke invite');
+ }
+
+ return __('Remove member');
+ },
+ actionPrimary() {
+ return {
+ text: this.actionText,
+ attributes: {
+ variant: 'danger',
+ },
+ };
+ },
+ showUnassignIssuablesCheckbox() {
+ return !this.isAccessRequest && !this.isInvite;
+ },
+ isPartOfOncallSchedules() {
+ return !this.isAccessRequest && this.oncallSchedules.schedules?.length;
+ },
+ },
+ methods: {
+ ...mapActions({
+ hideRemoveMemberModal(dispatch) {
+ return dispatch(`${this.namespace}/hideRemoveMemberModal`);
+ },
+ }),
+ submitForm() {
+ this.$refs.form.submit();
+ },
+ },
+};
+</script>
+
+<template>
+ <gl-modal
+ ref="modal"
+ modal-id="remove-member-modal"
+ :action-cancel="$options.actionCancel"
+ :action-primary="actionPrimary"
+ :title="actionText"
+ :visible="removeMemberModalVisible"
+ data-qa-selector="remove_member_modal_content"
+ @primary="submitForm"
+ @hide="hideRemoveMemberModal"
+ >
+ <form ref="form" :action="memberPath" method="post">
+ <p>{{ message }}</p>
+
+ <oncall-schedules-list
+ v-if="isPartOfOncallSchedules"
+ :schedules="oncallSchedules.schedules"
+ :user-name="oncallSchedules.name"
+ />
+
+ <input ref="method" type="hidden" name="_method" value="delete" />
+ <input :value="$options.csrf.token" type="hidden" name="authenticity_token" />
+ <gl-form-checkbox v-if="isGroupMember" name="remove_sub_memberships">
+ {{ __('Also remove direct user membership from subgroups and projects') }}
+ </gl-form-checkbox>
+ <gl-form-checkbox v-if="showUnassignIssuablesCheckbox" name="unassign_issuables">
+ {{ __('Also unassign this user from related issues and merge requests') }}
+ </gl-form-checkbox>
+ </form>
+ </gl-modal>
+</template>
diff --git a/app/assets/javascripts/members/components/table/members_table.vue b/app/assets/javascripts/members/components/table/members_table.vue
index b9c80edbc49..debc3fc31f6 100644
--- a/app/assets/javascripts/members/components/table/members_table.vue
+++ b/app/assets/javascripts/members/components/table/members_table.vue
@@ -7,6 +7,7 @@ import { mergeUrlParams } from '~/lib/utils/url_utility';
import initUserPopovers from '~/user_popovers';
import { FIELDS, ACTIVE_TAB_QUERY_PARAM_NAME } from '../../constants';
import RemoveGroupLinkModal from '../modals/remove_group_link_modal.vue';
+import RemoveMemberModal from '../modals/remove_member_modal.vue';
import CreatedAt from './created_at.vue';
import ExpirationDatepicker from './expiration_datepicker.vue';
import ExpiresAt from './expires_at.vue';
@@ -29,6 +30,7 @@ export default {
MemberActionButtons,
RoleDropdown,
RemoveGroupLinkModal,
+ RemoveMemberModal,
ExpirationDatepicker,
LdapOverrideConfirmationModal: () =>
import('ee_component/members/components/ldap/ldap_override_confirmation_modal.vue'),
@@ -225,6 +227,7 @@ export default {
align="center"
/>
<remove-group-link-modal />
+ <remove-member-modal />
<ldap-override-confirmation-modal />
</div>
</template>
diff --git a/app/assets/javascripts/members/index.js b/app/assets/javascripts/members/index.js
index 2ed0958d1dc..510e89240f4 100644
--- a/app/assets/javascripts/members/index.js
+++ b/app/assets/javascripts/members/index.js
@@ -14,7 +14,13 @@ export const initMembersApp = (el, options) => {
Vue.use(Vuex);
Vue.use(GlToast);
- const { sourceId, canManageMembers, ...vuexStoreAttributes } = parseDataAttributes(el);
+ const {
+ sourceId,
+ canManageMembers,
+ canExportMembers,
+ exportCsvPath,
+ ...vuexStoreAttributes
+ } = parseDataAttributes(el);
const modules = Object.keys(MEMBER_TYPES).reduce((accumulator, namespace) => {
const namespacedOptions = options[namespace];
@@ -54,6 +60,8 @@ export const initMembersApp = (el, options) => {
currentUserId: gon.current_user_id || null,
sourceId,
canManageMembers,
+ canExportMembers,
+ exportCsvPath,
},
render: (createElement) => createElement('members-tabs'),
});
diff --git a/app/assets/javascripts/members/store/actions.js b/app/assets/javascripts/members/store/actions.js
index 7b191dd85d0..712f0d6caa7 100644
--- a/app/assets/javascripts/members/store/actions.js
+++ b/app/assets/javascripts/members/store/actions.js
@@ -25,6 +25,14 @@ export const hideRemoveGroupLinkModal = ({ commit }) => {
commit(types.HIDE_REMOVE_GROUP_LINK_MODAL);
};
+export const showRemoveMemberModal = ({ commit }, modalData) => {
+ commit(types.SHOW_REMOVE_MEMBER_MODAL, modalData);
+};
+
+export const hideRemoveMemberModal = ({ commit }) => {
+ commit(types.HIDE_REMOVE_MEMBER_MODAL);
+};
+
export const updateMemberExpiration = async ({ state, commit }, { memberId, expiresAt }) => {
try {
await axios.put(
diff --git a/app/assets/javascripts/members/store/mutation_types.js b/app/assets/javascripts/members/store/mutation_types.js
index 77307aa745b..5fa75725552 100644
--- a/app/assets/javascripts/members/store/mutation_types.js
+++ b/app/assets/javascripts/members/store/mutation_types.js
@@ -8,3 +8,6 @@ export const HIDE_ERROR = 'HIDE_ERROR';
export const SHOW_REMOVE_GROUP_LINK_MODAL = 'SHOW_REMOVE_GROUP_LINK_MODAL';
export const HIDE_REMOVE_GROUP_LINK_MODAL = 'HIDE_REMOVE_GROUP_LINK_MODAL';
+
+export const SHOW_REMOVE_MEMBER_MODAL = 'SHOW_REMOVE_MEMBER_MODAL';
+export const HIDE_REMOVE_MEMBER_MODAL = 'HIDE_REMOVE_MEMBER_MODAL';
diff --git a/app/assets/javascripts/members/store/mutations.js b/app/assets/javascripts/members/store/mutations.js
index f4aac1571d6..b4cf9f3480f 100644
--- a/app/assets/javascripts/members/store/mutations.js
+++ b/app/assets/javascripts/members/store/mutations.js
@@ -47,4 +47,11 @@ export default {
[types.HIDE_REMOVE_GROUP_LINK_MODAL](state) {
state.removeGroupLinkModalVisible = false;
},
+ [types.SHOW_REMOVE_MEMBER_MODAL](state, modalData) {
+ state.removeMemberModalData = modalData;
+ state.removeMemberModalVisible = true;
+ },
+ [types.HIDE_REMOVE_MEMBER_MODAL](state) {
+ state.removeMemberModalVisible = false;
+ },
};
diff --git a/app/assets/javascripts/members/store/state.js b/app/assets/javascripts/members/store/state.js
index 5415b1c5f25..c233a660840 100644
--- a/app/assets/javascripts/members/store/state.js
+++ b/app/assets/javascripts/members/store/state.js
@@ -20,4 +20,6 @@ export default ({
errorMessage: '',
removeGroupLinkModalVisible: false,
groupLinkToRemove: null,
+ removeMemberModalData: {},
+ removeMemberModalVisible: false,
});