diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2021-09-20 16:18:24 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2021-09-20 16:18:24 +0300 |
commit | 0653e08efd039a5905f3fa4f6e9cef9f5d2f799c (patch) | |
tree | 4dcc884cf6d81db44adae4aa99f8ec1233a41f55 /app/assets/javascripts/invite_members | |
parent | 744144d28e3e7fddc117924fef88de5d9674fe4c (diff) |
Add latest changes from gitlab-org/gitlab@14-3-stable-eev14.3.0-rc42
Diffstat (limited to 'app/assets/javascripts/invite_members')
4 files changed, 324 insertions, 1 deletions
diff --git a/app/assets/javascripts/invite_members/components/import_a_project_modal.vue b/app/assets/javascripts/invite_members/components/import_a_project_modal.vue new file mode 100644 index 00000000000..d71468284ca --- /dev/null +++ b/app/assets/javascripts/invite_members/components/import_a_project_modal.vue @@ -0,0 +1,157 @@ +<script> +import { GlButton, GlFormGroup, GlModal, GlModalDirective, GlSprintf } from '@gitlab/ui'; +import { uniqueId } from 'lodash'; +import { importProjectMembers } from '~/api/projects_api'; +import { s__, __, sprintf } from '~/locale'; +import ProjectSelect from './project_select.vue'; + +export default { + components: { + GlButton, + GlFormGroup, + GlModal, + GlSprintf, + ProjectSelect, + }, + directives: { + GlModal: GlModalDirective, + }, + props: { + projectId: { + type: String, + required: true, + }, + projectName: { + type: String, + required: true, + }, + }, + data() { + return { + projectToBeImported: {}, + invalidFeedbackMessage: '', + isLoading: false, + }; + }, + computed: { + modalIntro() { + return sprintf(this.$options.i18n.modalIntro, { + name: this.projectName, + }); + }, + importDisabled() { + return Object.keys(this.projectToBeImported).length === 0; + }, + validationState() { + return this.invalidFeedbackMessage === '' ? null : false; + }, + }, + methods: { + submitImport() { + this.isLoading = true; + return importProjectMembers(this.projectId, this.projectToBeImported.id) + .then(this.showToastMessage) + .catch(this.showErrorAlert) + .finally(() => { + this.isLoading = false; + this.projectToBeImported = {}; + }); + }, + closeModal() { + this.invalidFeedbackMessage = ''; + + this.$refs.modal.hide(); + }, + showToastMessage() { + this.$toast.show(this.$options.i18n.successMessage, this.$options.toastOptions); + + this.closeModal(); + }, + showErrorAlert() { + this.invalidFeedbackMessage = this.$options.i18n.defaultError; + }, + }, + toastOptions() { + return { + onComplete: () => { + this.projectToBeImported = {}; + }, + }; + }, + i18n: { + buttonText: s__('ImportAProjectModal|Import from a project'), + projectLabel: __('Project'), + modalTitle: s__('ImportAProjectModal|Import members from another project'), + modalIntro: s__( + "ImportAProjectModal|You're importing members to the %{strongStart}%{name}%{strongEnd} project.", + ), + modalHelpText: s__( + 'ImportAProjectModal|Only project members (not group members) are imported, and they get the same permissions as the project you import from.', + ), + modalPrimaryButton: s__('ImportAProjectModal|Import project members'), + modalCancelButton: __('Cancel'), + defaultError: s__('ImportAProjectModal|Unable to import project members'), + successMessage: s__('ImportAProjectModal|Successfully imported'), + }, + projectSelectLabelId: 'project-select', + modalId: uniqueId('import-a-project-modal-'), + formClasses: 'gl-mt-3 gl-sm-w-auto gl-w-full', + buttonClasses: 'gl-w-full', +}; +</script> + +<template> + <form :class="$options.formClasses"> + <gl-button v-gl-modal="$options.modalId" :class="$options.buttonClasses" variant="default">{{ + $options.i18n.buttonText + }}</gl-button> + + <gl-modal + ref="modal" + :modal-id="$options.modalId" + size="sm" + :title="$options.i18n.modalTitle" + ok-variant="danger" + footer-class="gl-bg-gray-10 gl-p-5" + > + <div> + <p ref="modalIntro"> + <gl-sprintf :message="modalIntro"> + <template #strong="{ content }"> + <strong>{{ content }}</strong> + </template> + </gl-sprintf> + </p> + <gl-form-group + :invalid-feedback="invalidFeedbackMessage" + :state="validationState" + data-testid="form-group" + > + <label :id="$options.projectSelectLabelId" class="col-form-label">{{ + $options.i18n.projectLabel + }}</label> + <project-select v-model="projectToBeImported" /> + </gl-form-group> + <p>{{ $options.i18n.modalHelpText }}</p> + </div> + <template #modal-footer> + <div + class="gl-display-flex gl-flex-direction-row gl-justify-content-end gl-flex-wrap gl-m-0" + > + <gl-button data-testid="cancel-button" @click="closeModal"> + {{ $options.i18n.modalCancelButton }} + </gl-button> + <div class="gl-mr-3"></div> + <gl-button + :disabled="importDisabled" + :loading="isLoading" + variant="success" + data-testid="import-button" + @click="submitImport" + >{{ $options.i18n.modalPrimaryButton }}</gl-button + > + </div> + </template> + </gl-modal> + </form> +</template> diff --git a/app/assets/javascripts/invite_members/components/invite_members_trigger.vue b/app/assets/javascripts/invite_members/components/invite_members_trigger.vue index ec7d466336e..05be427742c 100644 --- a/app/assets/javascripts/invite_members/components/invite_members_trigger.vue +++ b/app/assets/javascripts/invite_members/components/invite_members_trigger.vue @@ -65,7 +65,7 @@ export default { if (this.event && this.label) { return { ...baseAttributes, - 'data-track-event': this.event, + 'data-track-action': this.event, 'data-track-label': this.label, }; } diff --git a/app/assets/javascripts/invite_members/components/project_select.vue b/app/assets/javascripts/invite_members/components/project_select.vue new file mode 100644 index 00000000000..b7a3918813b --- /dev/null +++ b/app/assets/javascripts/invite_members/components/project_select.vue @@ -0,0 +1,143 @@ +<script> +import { + GlAvatarLabeled, + GlDropdown, + GlDropdownItem, + GlDropdownText, + GlSearchBoxByType, +} from '@gitlab/ui'; +import { debounce } from 'lodash'; +import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; +import { s__ } from '~/locale'; +import { getProjects } from '~/rest_api'; +import { SEARCH_DELAY, GROUP_FILTERS } from '../constants'; + +export default { + name: 'ProjectSelect', + components: { + GlAvatarLabeled, + GlDropdown, + GlDropdownItem, + GlDropdownText, + GlSearchBoxByType, + }, + model: { + prop: 'selectedProject', + }, + props: { + groupsFilter: { + type: String, + required: false, + default: GROUP_FILTERS.ALL, + validator: (value) => Object.values(GROUP_FILTERS).includes(value), + }, + parentGroupId: { + type: Number, + required: false, + default: 0, + }, + }, + data() { + return { + isFetching: false, + projects: [], + selectedProject: {}, + searchTerm: '', + errorMessage: '', + }; + }, + computed: { + selectedProjectName() { + return this.selectedProject.name || this.$options.i18n.dropdownText; + }, + isFetchResultEmpty() { + return this.projects.length === 0 && !this.isFetching; + }, + }, + watch: { + searchTerm() { + this.retrieveProjects(); + }, + }, + mounted() { + this.retrieveProjects(); + }, + methods: { + retrieveProjects: debounce(function debouncedRetrieveProjects() { + this.isFetching = true; + this.errorMessage = ''; + return this.fetchProjects() + .then((response) => { + this.projects = response.data.map((project) => ({ + ...convertObjectPropsToCamelCase(project), + name: project.name_with_namespace, + })); + }) + .catch(() => { + this.errorMessage = this.$options.i18n.errorFetchingProjects; + }) + .finally(() => { + this.isFetching = false; + }); + }, SEARCH_DELAY), + fetchProjects() { + return getProjects(this.searchTerm, this.$options.defaultFetchOptions); + }, + selectProject(project) { + this.selectedProject = project; + + this.$emit('input', this.selectedProject); + }, + }, + i18n: { + dropdownText: s__('ProjectSelect|Select a project'), + searchPlaceholder: s__('ProjectSelect|Search projects'), + emptySearchResult: s__('ProjectSelect|No matching results'), + errorFetchingProjects: s__( + 'ProjectSelect|There was an error fetching the projects. Please try again.', + ), + }, + defaultFetchOptions: { + exclude_internal: true, + active: true, + }, +}; +</script> +<template> + <div> + <gl-dropdown + data-testid="project-select-dropdown" + :text="selectedProjectName" + toggle-class="gl-mb-2" + block + menu-class="gl-w-full!" + > + <gl-search-box-by-type + v-model="searchTerm" + :is-loading="isFetching" + :placeholder="$options.i18n.searchPlaceholder" + data-qa-selector="project_select_dropdown_search_field" + /> + <gl-dropdown-item + v-for="project in projects" + :key="project.id" + :name="project.name" + @click="selectProject(project)" + > + <gl-avatar-labeled + :label="project.name" + :src="project.avatarUrl" + :entity-id="project.id" + :entity-name="project.name" + :size="32" + /> + </gl-dropdown-item> + <gl-dropdown-text v-if="errorMessage" data-testid="error-message"> + <span class="gl-text-gray-500">{{ errorMessage }}</span> + </gl-dropdown-text> + <gl-dropdown-text v-else-if="isFetchResultEmpty" data-testid="empty-result-message"> + <span class="gl-text-gray-500">{{ $options.i18n.emptySearchResult }}</span> + </gl-dropdown-text> + </gl-dropdown> + </div> +</template> diff --git a/app/assets/javascripts/invite_members/init_import_a_project_modal.js b/app/assets/javascripts/invite_members/init_import_a_project_modal.js new file mode 100644 index 00000000000..954347467de --- /dev/null +++ b/app/assets/javascripts/invite_members/init_import_a_project_modal.js @@ -0,0 +1,23 @@ +import Vue from 'vue'; +import ImportAProjectModal from '~/invite_members/components/import_a_project_modal.vue'; + +export default function initImportAProjectModal() { + const el = document.querySelector('.js-import-a-project-modal'); + + if (!el) { + return false; + } + + const { projectId, projectName } = el.dataset; + + return new Vue({ + el, + render: (createElement) => + createElement(ImportAProjectModal, { + props: { + projectId, + projectName, + }, + }), + }); +} |