diff --git a/GITLAB_PAGES_VERSION b/GITLAB_PAGES_VERSION index bb120e876c6..4d5fde5bd16 100644 --- a/GITLAB_PAGES_VERSION +++ b/GITLAB_PAGES_VERSION @@ -1 +1 @@ -1.59.0 +1.60.0 diff --git a/app/assets/javascripts/design_management/constants.js b/app/assets/javascripts/design_management/constants.js index 92928ca429f..afe621ac3c5 100644 --- a/app/assets/javascripts/design_management/constants.js +++ b/app/assets/javascripts/design_management/constants.js @@ -12,3 +12,5 @@ export const ACTIVE_DISCUSSION_SOURCE_TYPES = { }; export const DESIGN_DETAIL_LAYOUT_CLASSLIST = ['design-detail-layout', 'overflow-hidden', 'm-0']; + +export const MAXIMUM_FILE_UPLOAD_LIMIT = 10; diff --git a/app/assets/javascripts/design_management/pages/index.vue b/app/assets/javascripts/design_management/pages/index.vue index f81d4f6662f..51983b19677 100644 --- a/app/assets/javascripts/design_management/pages/index.vue +++ b/app/assets/javascripts/design_management/pages/index.vue @@ -4,16 +4,15 @@ import { GlBreakpointInstance } from '@gitlab/ui/dist/utils'; import VueDraggable from 'vuedraggable'; import permissionsQuery from 'shared_queries/design_management/design_permissions.query.graphql'; import getDesignListQuery from 'shared_queries/design_management/get_design_list.query.graphql'; -import createFlash, { FLASH_TYPES } from '~/flash'; import { getFilename, validateImageName } from '~/lib/utils/file_upload'; -import { __, s__, sprintf } from '~/locale'; +import { __, s__ } from '~/locale'; import DesignDropzone from '~/vue_shared/components/upload_dropzone/upload_dropzone.vue'; import DeleteButton from '../components/delete_button.vue'; import DesignDestroyer from '../components/design_destroyer.vue'; import Design from '../components/list/item.vue'; import UploadButton from '../components/upload/button.vue'; import DesignVersionDropdown from '../components/upload/design_version_dropdown.vue'; -import { VALID_DESIGN_FILE_MIMETYPE } from '../constants'; +import { MAXIMUM_FILE_UPLOAD_LIMIT, VALID_DESIGN_FILE_MIMETYPE } from '../constants'; import moveDesignMutation from '../graphql/mutations/move_design.mutation.graphql'; import uploadDesignMutation from '../graphql/mutations/upload_design.mutation.graphql'; import allDesignsMixin from '../mixins/all_designs'; @@ -35,11 +34,10 @@ import { UPLOAD_DESIGN_INVALID_FILETYPE_ERROR, designUploadSkippedWarning, designDeletionError, + MAXIMUM_FILE_UPLOAD_LIMIT_REACHED, } from '../utils/error_messages'; import { trackDesignCreate, trackDesignUpdate } from '../utils/tracking'; -const MAXIMUM_FILE_UPLOAD_LIMIT = 10; - export default { components: { GlLoadingIcon, @@ -87,6 +85,7 @@ export default { isDraggingDesign: false, reorderedDesigns: null, isReorderingInProgress: false, + uploadError: null, }; }, computed: { @@ -159,16 +158,7 @@ export default { if (!this.canCreateDesign) return false; if (files.length > MAXIMUM_FILE_UPLOAD_LIMIT) { - createFlash({ - message: sprintf( - s__( - 'DesignManagement|The maximum number of designs allowed to be uploaded is %{upload_limit}. Please try again.', - ), - { - upload_limit: MAXIMUM_FILE_UPLOAD_LIMIT, - }, - ), - }); + this.uploadError = MAXIMUM_FILE_UPLOAD_LIMIT_REACHED; return false; } @@ -206,7 +196,7 @@ export default { const skippedFiles = res?.data?.designManagementUpload?.skippedDesigns || []; const skippedWarningMessage = designUploadSkippedWarning(this.filesToBeSaved, skippedFiles); if (skippedWarningMessage) { - createFlash({ message: skippedWarningMessage, types: FLASH_TYPES.WARNING }); + this.uploadError = skippedWarningMessage; } // if this upload resulted in a new version being created, redirect user to the latest version @@ -229,7 +219,7 @@ export default { }, onUploadDesignError() { this.resetFilesToBeSaved(); - createFlash({ message: UPLOAD_DESIGN_ERROR }); + this.uploadError = UPLOAD_DESIGN_ERROR; }, changeSelectedDesigns(filename) { if (this.isDesignSelected(filename)) { @@ -260,21 +250,21 @@ export default { }, onDesignDeleteError() { const errorMessage = designDeletionError(this.selectedDesigns.length); - createFlash({ message: errorMessage }); + this.uploadError = errorMessage; }, onDesignDropzoneError() { - createFlash({ message: UPLOAD_DESIGN_INVALID_FILETYPE_ERROR }); + this.uploadError = UPLOAD_DESIGN_INVALID_FILETYPE_ERROR; }, onExistingDesignDropzoneChange(files, existingDesignFilename) { const filesArr = Array.from(files); if (filesArr.length > 1) { - createFlash({ message: EXISTING_DESIGN_DROP_MANY_FILES_MESSAGE }); + this.uploadError = EXISTING_DESIGN_DROP_MANY_FILES_MESSAGE; return; } if (!filesArr.some(({ name }) => existingDesignFilename === name)) { - createFlash({ message: EXISTING_DESIGN_DROP_INVALID_FILENAME_MESSAGE }); + this.uploadError = EXISTING_DESIGN_DROP_INVALID_FILENAME_MESSAGE; return; } @@ -329,7 +319,7 @@ export default { optimisticResponse: moveDesignOptimisticResponse(this.reorderedDesigns), }) .catch(() => { - createFlash({ message: MOVE_DESIGN_ERROR }); + this.uploadError = MOVE_DESIGN_ERROR; }) .finally(() => { this.isReorderingInProgress = false; @@ -338,6 +328,9 @@ export default { onDesignMove(designs) { this.reorderedDesigns = designs; }, + unsetUpdateError() { + this.uploadError = null; + }, }, dragOptions: { animation: 200, @@ -356,6 +349,15 @@ export default { @mouseenter="toggleOnPasteListener" @mouseleave="toggleOffPasteListener" > + + {{ uploadError }} +
{ return someDesignsSkippedMessage(skippedFiles); }; + +export const MAXIMUM_FILE_UPLOAD_LIMIT_REACHED = sprintf( + s__( + 'DesignManagement|The maximum number of designs allowed to be uploaded is %{upload_limit}. Please try again.', + ), + { + upload_limit: MAXIMUM_FILE_UPLOAD_LIMIT, + }, +); diff --git a/app/assets/javascripts/invite_members/components/invite_members_modal.vue b/app/assets/javascripts/invite_members/components/invite_members_modal.vue index d597c7e53bb..b71cfbb6112 100644 --- a/app/assets/javascripts/invite_members/components/invite_members_modal.vue +++ b/app/assets/javascripts/invite_members/components/invite_members_modal.vue @@ -7,12 +7,13 @@ import { GlSprintf, GlFormCheckboxGroup, } from '@gitlab/ui'; -import { partition, isString, uniqueId } from 'lodash'; +import { partition, isString, uniqueId, isEmpty } from 'lodash'; import InviteModalBase from 'ee_else_ce/invite_members/components/invite_modal_base.vue'; import Api from '~/api'; import ExperimentTracking from '~/experimentation/experiment_tracking'; import { BV_SHOW_MODAL, BV_HIDE_MODAL } from '~/lib/utils/constants'; import { getParameterValues } from '~/lib/utils/url_utility'; +import { n__ } from '~/locale'; import { CLOSE_TO_LIMIT_COUNT, USERS_FILTER_ALL, @@ -21,7 +22,8 @@ import { LEARN_GITLAB, } from '../constants'; import eventHub from '../event_hub'; -import { responseMessageFromSuccess } from '../utils/response_message_parser'; +import { responseFromSuccess } from '../utils/response_message_parser'; +import { memberName } from '../utils/member_utils'; import { getInvalidFeedbackMessage } from '../utils/get_invalid_feedback_message'; import ModalConfetti from './confetti.vue'; import MembersTokenSelect from './members_token_select.vue'; @@ -101,6 +103,7 @@ export default { isLoading: false, modalId: uniqueId('invite-members-modal-'), newUsersToInvite: [], + invalidMembers: {}, selectedTasksToBeDone: [], selectedTaskProject: this.projects[0], source: 'unknown', @@ -125,6 +128,16 @@ export default { inviteDisabled() { return this.newUsersToInvite.length === 0; }, + hasInvalidMembers() { + return !isEmpty(this.invalidMembers); + }, + memberErrorTitle() { + return n__( + "InviteMembersModal|The following member couldn't be invited", + "InviteMembersModal|The following %d members couldn't be invited", + Object.keys(this.invalidMembers).length, + ); + }, tasksToBeDoneEnabled() { return ( (getParameterValues('open_modal')[0] === 'invite_members_for_task' || @@ -218,7 +231,7 @@ export default { }, sendInvite({ accessLevel, expiresAt }) { this.isLoading = true; - this.invalidFeedbackMessage = ''; + this.clearValidation(); const [usersToInviteByEmail, usersToAddById] = this.partitionNewUsersToInvite(); @@ -242,12 +255,10 @@ export default { ...userId, }) .then((response) => { - const message = responseMessageFromSuccess(response); + const { error, message } = responseFromSuccess(response); - if (message) { - this.showInvalidFeedbackMessage({ - response: { data: { message } }, - }); + if (error) { + this.showMemberErrors(message); } else { this.showSuccessMessage(); } @@ -257,6 +268,13 @@ export default { this.isLoading = false; }); }, + showMemberErrors(message) { + this.invalidMembers = message; + }, + tokenName(username) { + // initial token creation hits this and nothing is found... so safe navigation + return this.newUsersToInvite.find((member) => memberName(member) === username)?.name; + }, trackinviteMembersForTask() { const label = 'selected_tasks_to_be_done'; const property = this.selectedTasksToBeDone.join(','); @@ -264,8 +282,8 @@ export default { tracking.event(INVITE_MEMBERS_FOR_TASK.submit); }, resetFields() { + this.clearValidation(); this.isLoading = false; - this.invalidFeedbackMessage = ''; this.newUsersToInvite = []; this.selectedTasksToBeDone = []; [this.selectedTaskProject] = this.projects; @@ -287,6 +305,11 @@ export default { }, clearValidation() { this.invalidFeedbackMessage = ''; + this.invalidMembers = {}; + }, + removeToken(token) { + delete this.invalidMembers[memberName(token)]; + this.invalidMembers = { ...this.invalidMembers }; }, }, labels: MEMBER_MODAL_LABELS, @@ -324,23 +347,40 @@ export default { -