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>2020-11-24 18:09:13 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2020-11-24 18:09:13 +0300
commit94c1ea61908ce610ba40786e2d80d5701acd8cbf (patch)
treead8f2463a5046119c8e4d098fe82bb57d2dc7a09
parent7db8721ff9eeb914afa30da7fb8df07ff6c10796 (diff)
Add latest changes from gitlab-org/gitlab@master
-rw-r--r--app/assets/javascripts/alert_management/components/sidebar/sidebar_assignee.vue19
-rw-r--r--app/assets/javascripts/alert_management/components/sidebar/sidebar_assignees.vue146
-rw-r--r--app/assets/javascripts/issue_show/components/header_actions.vue44
-rw-r--r--app/assets/javascripts/notes/components/comment_form.vue143
-rw-r--r--app/assets/javascripts/notes/stores/actions.js18
-rw-r--r--app/assets/javascripts/notes/stores/modules/index.js1
-rw-r--r--app/assets/javascripts/notes/stores/mutation_types.js1
-rw-r--r--app/assets/javascripts/notes/stores/mutations.js4
-rw-r--r--app/assets/javascripts/vue_shared/components/gfm_autocomplete/gfm_autocomplete.vue5
-rw-r--r--app/assets/javascripts/vue_shared/components/gfm_autocomplete/utils.js8
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/field.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/notes/noteable_warning.vue2
-rw-r--r--app/assets/stylesheets/page_bundles/alert_management_details.scss23
-rw-r--r--app/controllers/concerns/snippets_actions.rb3
-rw-r--r--app/models/container_repository.rb3
-rw-r--r--changelogs/unreleased/255171-dropdown-alerts-replacement.yml5
-rw-r--r--config/feature_flags/development/usage_data_i_snippets_show.yml8
-rw-r--r--doc/user/group/epics/img/new_epic_form_v13.2.pngbin50977 -> 0 bytes
-rw-r--r--doc/user/group/epics/img/new_epic_from_groups_v13.2.pngbin39158 -> 0 bytes
-rw-r--r--doc/user/group/epics/img/new_epic_from_groups_v13.7.pngbin0 -> 10505 bytes
-rw-r--r--doc/user/group/epics/manage_epics.md46
-rw-r--r--lib/gitlab/danger/roulette.rb2
-rw-r--r--lib/gitlab/usage_data_counters/known_events/common.yml5
-rw-r--r--locale/gitlab.pot4
-rw-r--r--spec/controllers/snippets_controller_spec.rb7
-rw-r--r--spec/frontend/alert_management/components/sidebar/alert_managment_sidebar_assignees_spec.js2
-rw-r--r--spec/frontend/issue_show/components/header_actions_spec.js27
-rw-r--r--spec/frontend/notes/components/comment_form_spec.js405
-rw-r--r--spec/frontend/notes/stores/actions_spec.js32
-rw-r--r--spec/frontend/notes/stores/mutation_spec.js36
-rw-r--r--spec/lib/gitlab/danger/roulette_spec.rb8
-rw-r--r--spec/lib/gitlab/usage_data_counters/hll_redis_counter_spec.rb3
-rw-r--r--spec/lib/gitlab/usage_data_spec.rb2
33 files changed, 460 insertions, 554 deletions
diff --git a/app/assets/javascripts/alert_management/components/sidebar/sidebar_assignee.vue b/app/assets/javascripts/alert_management/components/sidebar/sidebar_assignee.vue
index df07038151e..c39a72a45b9 100644
--- a/app/assets/javascripts/alert_management/components/sidebar/sidebar_assignee.vue
+++ b/app/assets/javascripts/alert_management/components/sidebar/sidebar_assignee.vue
@@ -27,25 +27,12 @@ export default {
<gl-dropdown-item
:key="user.username"
data-testid="assigneeDropdownItem"
- class="assignee-dropdown-item gl-vertical-align-middle"
:active="active"
active-class="is-active"
+ :avatar-url="user.avatar_url"
+ :secondary-text="`@${user.username}`"
@click="$emit('update-alert-assignees', user.username)"
>
- <span class="gl-relative mr-2">
- <img
- :alt="user.username"
- :src="user.avatar_url"
- :width="32"
- class="avatar avatar-inline gl-m-0 s32"
- data-qa-selector="avatar_image"
- />
- </span>
- <span class="d-flex gl-flex-direction-column gl-overflow-hidden">
- <strong class="dropdown-menu-user-full-name">
- {{ user.name }}
- </strong>
- <span class="dropdown-menu-user-username"> {{ user.username }}</span>
- </span>
+ {{ user.name }}
</gl-dropdown-item>
</template>
diff --git a/app/assets/javascripts/alert_management/components/sidebar/sidebar_assignees.vue b/app/assets/javascripts/alert_management/components/sidebar/sidebar_assignees.vue
index 5e4fd56738b..3af68d42ddf 100644
--- a/app/assets/javascripts/alert_management/components/sidebar/sidebar_assignees.vue
+++ b/app/assets/javascripts/alert_management/components/sidebar/sidebar_assignees.vue
@@ -13,7 +13,7 @@ import {
} from '@gitlab/ui';
import { debounce } from 'lodash';
import axios from '~/lib/utils/axios_utils';
-import { s__ } from '~/locale';
+import { s__, __ } from '~/locale';
import alertSetAssignees from '../../graphql/mutations/alert_set_assignees.mutation.graphql';
import SidebarAssignee from './sidebar_assignee.vue';
@@ -96,7 +96,10 @@ export default {
.sort((a, b) => (a.active === b.active ? 0 : a.active ? -1 : 1)); // eslint-disable-line no-nested-ternary
},
dropdownClass() {
- return this.isDropdownShowing ? 'show' : 'gl-display-none';
+ return this.isDropdownShowing ? 'dropdown-menu-selectable show' : 'gl-display-none';
+ },
+ dropDownTitle() {
+ return this.userName ?? __('Select assignee');
},
userListValid() {
return !this.isDropdownSearching && this.users.length > 0;
@@ -217,81 +220,80 @@ export default {
</a>
</p>
- <div class="dropdown dropdown-menu-selectable" :class="dropdownClass">
- <gl-dropdown
- ref="dropdown"
- :text="userName"
- class="w-100"
- toggle-class="dropdown-menu-toggle"
- @keydown.esc.native="hideDropdown"
- @hide="hideDropdown"
- >
- <p class="gl-new-dropdown-header-top">
- {{ __('Assign To') }}
- </p>
- <gl-search-box-by-type v-model.trim="search" :placeholder="__('Search users')" />
- <div class="dropdown-content dropdown-body">
- <template v-if="userListValid">
- <gl-dropdown-item
- :active="!userName"
- active-class="is-active"
- @click="updateAlertAssignees('')"
- >
- {{ __('Unassigned') }}
- </gl-dropdown-item>
- <gl-dropdown-divider />
-
- <gl-dropdown-section-header>
- {{ __('Assignee') }}
- </gl-dropdown-section-header>
- <sidebar-assignee
- v-for="user in sortedUsers"
- :key="user.username"
- :user="user"
- :active="user.active"
- @update-alert-assignees="updateAlertAssignees"
- />
- </template>
- <p v-else-if="userListEmpty" class="mx-3 my-2">
- {{ __('No Matching Results') }}
- </p>
- <gl-loading-icon v-else />
- </div>
- </gl-dropdown>
- </div>
+ <gl-dropdown
+ ref="dropdown"
+ :text="dropDownTitle"
+ class="gl-w-full"
+ :class="dropdownClass"
+ toggle-class="dropdown-menu-toggle"
+ @keydown.esc.native="hideDropdown"
+ @hide="hideDropdown"
+ >
+ <p class="gl-new-dropdown-header-top">
+ {{ __('Assign To') }}
+ </p>
+ <gl-search-box-by-type v-model.trim="search" :placeholder="__('Search users')" />
+ <div class="dropdown-content dropdown-body">
+ <template v-if="userListValid">
+ <gl-dropdown-item
+ :active="!userName"
+ active-class="is-active"
+ @click="updateAlertAssignees('')"
+ >
+ {{ __('Unassigned') }}
+ </gl-dropdown-item>
+ <gl-dropdown-divider />
- <gl-loading-icon v-if="isUpdating" :inline="true" />
- <div v-else-if="!isDropdownShowing" class="value gl-m-0" :class="{ 'no-value': !userName }">
- <div v-if="userName" class="gl-display-inline-flex gl-mt-2" data-testid="assigned-users">
- <span class="gl-relative mr-2">
- <img
- :alt="userName"
- :src="userImg"
- :width="32"
- class="avatar avatar-inline gl-m-0 s32"
- data-qa-selector="avatar_image"
+ <gl-dropdown-section-header>
+ {{ __('Assignee') }}
+ </gl-dropdown-section-header>
+ <sidebar-assignee
+ v-for="user in sortedUsers"
+ :key="user.username"
+ :user="user"
+ :active="user.active"
+ @update-alert-assignees="updateAlertAssignees"
/>
- </span>
- <span class="gl-display-flex gl-flex-direction-column gl-overflow-hidden">
- <strong class="dropdown-menu-user-full-name">
- {{ userFullName }}
- </strong>
- <span class="dropdown-menu-user-username">{{ userName }}</span>
- </span>
+ </template>
+ <p v-else-if="userListEmpty" class="gl-mx-5 gl-my-4">
+ {{ __('No Matching Results') }}
+ </p>
+ <gl-loading-icon v-else />
</div>
- <span v-else class="gl-display-flex gl-align-items-center gl-line-height-normal">
- {{ __('None') }} -
- <gl-button
- class="gl-ml-2"
- href="#"
- variant="link"
- data-testid="unassigned-users"
- @click="updateAlertAssignees(currentUser)"
- >
- {{ __('assign yourself') }}
- </gl-button>
+ </gl-dropdown>
+ </div>
+
+ <gl-loading-icon v-if="isUpdating" :inline="true" />
+ <div v-else-if="!isDropdownShowing" class="value gl-m-0" :class="{ 'no-value': !userName }">
+ <div v-if="userName" class="gl-display-inline-flex gl-mt-2" data-testid="assigned-users">
+ <span class="gl-relative gl-mr-4">
+ <img
+ :alt="userName"
+ :src="userImg"
+ :width="32"
+ class="avatar avatar-inline gl-m-0 s32"
+ data-qa-selector="avatar_image"
+ />
+ </span>
+ <span class="gl-display-flex gl-flex-direction-column gl-overflow-hidden">
+ <strong class="dropdown-menu-user-full-name">
+ {{ userFullName }}
+ </strong>
+ <span class="dropdown-menu-user-username">@{{ userName }}</span>
</span>
</div>
+ <span v-else class="gl-display-flex gl-align-items-center gl-line-height-normal">
+ {{ __('None') }} -
+ <gl-button
+ class="gl-ml-2"
+ href="#"
+ variant="link"
+ data-testid="unassigned-users"
+ @click="updateAlertAssignees(currentUser)"
+ >
+ {{ __('assign yourself') }}
+ </gl-button>
+ </span>
</div>
</div>
</template>
diff --git a/app/assets/javascripts/issue_show/components/header_actions.vue b/app/assets/javascripts/issue_show/components/header_actions.vue
index 4c8c86390f4..a7b05c93c61 100644
--- a/app/assets/javascripts/issue_show/components/header_actions.vue
+++ b/app/assets/javascripts/issue_show/components/header_actions.vue
@@ -1,12 +1,13 @@
<script>
import { GlButton, GlDropdown, GlDropdownItem, GlIcon, GlLink, GlModal } from '@gitlab/ui';
-import { mapGetters } from 'vuex';
+import { mapActions, mapGetters, mapState } from 'vuex';
import createFlash, { FLASH_TYPES } from '~/flash';
import { IssuableType } from '~/issuable_show/constants';
import { IssuableStatus, IssueStateEvent } from '~/issue_show/constants';
import { capitalizeFirstCharacter } from '~/lib/utils/text_utility';
import { visitUrl } from '~/lib/utils/url_utility';
import { __, sprintf } from '~/locale';
+import eventHub from '~/notes/event_hub';
import promoteToEpicMutation from '../queries/promote_to_epic.mutation.graphql';
import updateIssueMutation from '../queries/update_issue.mutation.graphql';
@@ -72,15 +73,11 @@ export default {
default: '',
},
},
- data() {
- return {
- isUpdatingState: false,
- };
- },
computed: {
- ...mapGetters(['getNoteableData']),
+ ...mapState(['isToggleStateButtonLoading']),
+ ...mapGetters(['openState', 'getBlockedByIssues']),
isClosed() {
- return this.getNoteableData.state === IssuableStatus.Closed;
+ return this.openState === IssuableStatus.Closed;
},
buttonText() {
return this.isClosed
@@ -107,9 +104,16 @@ export default {
return canClose || canReopen;
},
},
+ created() {
+ eventHub.$on('toggle.issuable.state', this.toggleIssueState);
+ },
+ beforeDestroy() {
+ eventHub.$off('toggle.issuable.state', this.toggleIssueState);
+ },
methods: {
+ ...mapActions(['toggleStateButtonLoading']),
toggleIssueState() {
- if (!this.isClosed && this.getNoteableData?.blocked_by_issues?.length) {
+ if (!this.isClosed && this.getBlockedByIssues.length) {
this.$refs.blockedByIssuesModal.show();
return;
}
@@ -117,7 +121,7 @@ export default {
this.invokeUpdateIssueMutation();
},
invokeUpdateIssueMutation() {
- this.isUpdatingState = true;
+ this.toggleStateButtonLoading(true);
this.$apollo
.mutate({
@@ -148,11 +152,11 @@ export default {
})
.catch(() => createFlash({ message: __('Update failed. Please try again.') }))
.finally(() => {
- this.isUpdatingState = false;
+ this.toggleStateButtonLoading(false);
});
},
promoteToEpic() {
- this.isUpdatingState = true;
+ this.toggleStateButtonLoading(true);
this.$apollo
.mutate({
@@ -179,7 +183,7 @@ export default {
})
.catch(() => createFlash({ message: this.$options.i18n.promoteErrorMessage }))
.finally(() => {
- this.isUpdatingState = false;
+ this.toggleStateButtonLoading(false);
});
},
},
@@ -191,7 +195,7 @@ export default {
<gl-dropdown class="gl-display-block gl-display-sm-none!" block :text="dropdownText">
<gl-dropdown-item
v-if="showToggleIssueStateButton"
- :disabled="isUpdatingState"
+ :disabled="isToggleStateButtonLoading"
@click="toggleIssueState"
>
{{ buttonText }}
@@ -199,7 +203,11 @@ export default {
<gl-dropdown-item v-if="canCreateIssue" :href="newIssuePath">
{{ newIssueTypeText }}
</gl-dropdown-item>
- <gl-dropdown-item v-if="canPromoteToEpic" :disabled="isUpdatingState" @click="promoteToEpic">
+ <gl-dropdown-item
+ v-if="canPromoteToEpic"
+ :disabled="isToggleStateButtonLoading"
+ @click="promoteToEpic"
+ >
{{ __('Promote to epic') }}
</gl-dropdown-item>
<gl-dropdown-item v-if="!isIssueAuthor" :href="reportAbusePath">
@@ -220,7 +228,7 @@ export default {
class="gl-display-none gl-display-sm-inline-flex!"
category="secondary"
:data-qa-selector="qaSelector"
- :loading="isUpdatingState"
+ :loading="isToggleStateButtonLoading"
:variant="buttonVariant"
@click="toggleIssueState"
>
@@ -243,7 +251,7 @@ export default {
</gl-dropdown-item>
<gl-dropdown-item
v-if="canPromoteToEpic"
- :disabled="isUpdatingState"
+ :disabled="isToggleStateButtonLoading"
data-testid="promote-button"
@click="promoteToEpic"
>
@@ -272,7 +280,7 @@ export default {
>
<p>{{ __('This issue is currently blocked by the following issues:') }}</p>
<ul>
- <li v-for="issue in getNoteableData.blocked_by_issues" :key="issue.iid">
+ <li v-for="issue in getBlockedByIssues" :key="issue.iid">
<gl-link :href="issue.web_url">#{{ issue.iid }}</gl-link>
</li>
</ul>
diff --git a/app/assets/javascripts/notes/components/comment_form.vue b/app/assets/javascripts/notes/components/comment_form.vue
index 9cc53a320b8..b70e999c765 100644
--- a/app/assets/javascripts/notes/components/comment_form.vue
+++ b/app/assets/javascripts/notes/components/comment_form.vue
@@ -3,23 +3,23 @@ import $ from 'jquery';
import { mapActions, mapGetters, mapState } from 'vuex';
import { isEmpty } from 'lodash';
import Autosize from 'autosize';
-import { GlAlert, GlIntersperse, GlLink, GlSprintf, GlButton, GlIcon } from '@gitlab/ui';
+import { GlButton, GlIcon } from '@gitlab/ui';
import { __, sprintf } from '~/locale';
import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue';
-import { deprecatedCreateFlash as Flash } from '../../flash';
-import Autosave from '../../autosave';
+import { deprecatedCreateFlash as Flash } from '~/flash';
+import Autosave from '~/autosave';
import {
capitalizeFirstCharacter,
convertToCamelCase,
splitCamelCase,
slugifyWithUnderscore,
-} from '../../lib/utils/text_utility';
+} from '~/lib/utils/text_utility';
import { refreshUserMergeRequestCounts } from '~/commons/nav/user_merge_requests';
import * as constants from '../constants';
import eventHub from '../event_hub';
-import NoteableWarning from '../../vue_shared/components/notes/noteable_warning.vue';
-import markdownField from '../../vue_shared/components/markdown/field.vue';
-import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue';
+import NoteableWarning from '~/vue_shared/components/notes/noteable_warning.vue';
+import markdownField from '~/vue_shared/components/markdown/field.vue';
+import userAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue';
import noteSignedOutWidget from './note_signed_out_widget.vue';
import discussionLockedWidget from './discussion_locked_widget.vue';
import issuableStateMixin from '../mixins/issuable_state';
@@ -34,10 +34,6 @@ export default {
userAvatarLink,
GlButton,
TimelineEntryItem,
- GlAlert,
- GlIntersperse,
- GlLink,
- GlSprintf,
GlIcon,
},
mixins: [issuableStateMixin],
@@ -63,9 +59,8 @@ export default {
'getNoteableDataByProp',
'getNotesData',
'openState',
- 'getBlockedByIssues',
]),
- ...mapState(['isToggleStateButtonLoading', 'isToggleBlockedIssueWarning']),
+ ...mapState(['isToggleStateButtonLoading']),
noteableDisplayName() {
return splitCamelCase(this.noteableType).toLowerCase();
},
@@ -143,8 +138,8 @@ export default {
? __('merge request')
: __('issue');
},
- isIssueType() {
- return this.noteableDisplayName === constants.ISSUE_NOTEABLE_TYPE;
+ isMergeRequest() {
+ return this.noteableType === constants.MERGE_REQUEST_NOTEABLE_TYPE;
},
trackingLabel() {
return slugifyWithUnderscore(`${this.commentButtonTitle} button`);
@@ -172,11 +167,9 @@ export default {
'stopPolling',
'restartPolling',
'removePlaceholderNotes',
- 'closeIssue',
- 'reopenIssue',
+ 'closeMergeRequest',
+ 'reopenMergeRequest',
'toggleIssueLocalState',
- 'toggleStateButtonLoading',
- 'toggleBlockedIssueWarning',
]),
setIsSubmitButtonDisabled(note, isSubmitting) {
if (!isEmpty(note) && !isSubmitting) {
@@ -186,8 +179,6 @@ export default {
}
},
handleSave(withIssueAction) {
- this.isSubmitting = true;
-
if (this.note.length) {
const noteData = {
endpoint: this.endpoint,
@@ -210,9 +201,10 @@ export default {
this.resizeTextarea();
this.stopPolling();
+ this.isSubmitting = true;
+
this.saveNote(noteData)
.then(() => {
- this.enableButton();
this.restartPolling();
this.discard();
@@ -221,7 +213,6 @@ export default {
}
})
.catch(() => {
- this.enableButton();
this.discard(false);
const msg = __(
'Your comment could not be submitted! Please check your network connection and try again.',
@@ -229,64 +220,31 @@ export default {
Flash(msg, 'alert', this.$el);
this.note = noteData.data.note.note; // Restore textarea content.
this.removePlaceholderNotes();
+ })
+ .finally(() => {
+ this.isSubmitting = false;
});
} else {
this.toggleIssueState();
}
},
- enableButton() {
- this.isSubmitting = false;
- },
toggleIssueState() {
- if (
- this.noteableType.toLowerCase() === constants.ISSUE_NOTEABLE_TYPE &&
- this.isOpen &&
- this.getBlockedByIssues &&
- this.getBlockedByIssues.length > 0
- ) {
- this.toggleBlockedIssueWarning(true);
+ if (!this.isMergeRequest) {
+ eventHub.$emit('toggle.issuable.state');
return;
}
- if (this.isOpen) {
- this.forceCloseIssue();
- } else {
- this.reopenIssue()
- .then(() => {
- this.enableButton();
- refreshUserMergeRequestCounts();
- })
- .catch(({ data }) => {
- this.enableButton();
- this.toggleStateButtonLoading(false);
- let errorMessage = sprintf(
- __('Something went wrong while reopening the %{issuable}. Please try again later'),
- { issuable: this.noteableDisplayName },
- );
- if (data) {
- errorMessage = Object.values(data).join('\n');
- }
+ const toggleMergeRequestState = this.isOpen
+ ? this.closeMergeRequest
+ : this.reopenMergeRequest;
- Flash(errorMessage);
- });
- }
- },
- forceCloseIssue() {
- this.closeIssue()
- .then(() => {
- this.enableButton();
- refreshUserMergeRequestCounts();
- })
- .catch(() => {
- this.enableButton();
- this.toggleStateButtonLoading(false);
- Flash(
- sprintf(
- __('Something went wrong while closing the %{issuable}. Please try again later'),
- { issuable: this.noteableDisplayName },
- ),
- );
- });
+ const errorMessage = this.isOpen
+ ? __('Something went wrong while closing the merge request. Please try again later')
+ : __('Something went wrong while reopening the merge request. Please try again later');
+
+ toggleMergeRequestState()
+ .then(refreshUserMergeRequestCounts)
+ .catch(() => Flash(errorMessage));
},
discard(shouldClear = true) {
// `blur` is needed to clear slash commands autocomplete cache if event fired.
@@ -384,6 +342,7 @@ export default {
name="note[note]"
class="note-textarea js-vue-comment-form js-note-text js-gfm-input js-autosize markdown-area"
data-qa-selector="comment_field"
+ data-testid="comment-field"
data-supports-quick-actions="true"
:aria-label="__('Description')"
:placeholder="__('Write a comment or drag your files here…')"
@@ -392,36 +351,7 @@ export default {
@keydown.ctrl.enter="handleSave()"
></textarea>
</markdown-field>
- <gl-alert
- v-if="isToggleBlockedIssueWarning"
- class="gl-mt-5"
- :title="__('Are you sure you want to close this blocked issue?')"
- :primary-button-text="__('Yes, close issue')"
- :secondary-button-text="__('Cancel')"
- variant="warning"
- :dismissible="false"
- @primaryAction="toggleBlockedIssueWarning(false) && forceCloseIssue()"
- @secondaryAction="toggleBlockedIssueWarning(false) && enableButton()"
- >
- <p>
- <gl-sprintf
- :message="
- __('This issue is currently blocked by the following issues: %{issues}.')
- "
- >
- <template #issues>
- <gl-intersperse>
- <gl-link
- v-for="blockingIssue in getBlockedByIssues"
- :key="blockingIssue.web_url"
- :href="blockingIssue.web_url"
- >#{{ blockingIssue.iid }}</gl-link
- >
- </gl-intersperse>
- </template>
- </gl-sprintf>
- </p>
- </gl-alert>
+
<div class="note-form-actions">
<div
class="btn-group gl-mr-3 comment-type-dropdown js-comment-type-dropdown droplab-dropdown"
@@ -430,6 +360,7 @@ export default {
:disabled="isSubmitButtonDisabled"
class="js-comment-button js-comment-submit-button"
data-qa-selector="comment_button"
+ data-testid="comment-button"
type="submit"
category="primary"
variant="success"
@@ -488,15 +419,13 @@ export default {
</div>
<gl-button
- v-if="canToggleIssueState && !isToggleBlockedIssueWarning"
+ v-if="canToggleIssueState"
:loading="isToggleStateButtonLoading"
category="secondary"
:variant="buttonVariant"
- :class="[
- actionButtonClassNames,
- 'btn-comment btn-comment-and-close js-action-button',
- ]"
- :disabled="isToggleStateButtonLoading || isSubmitting"
+ :class="[actionButtonClassNames, 'btn-comment btn-comment-and-close']"
+ :disabled="isSubmitting"
+ data-testid="close-reopen-button"
@click="handleSave(true)"
>{{ issueActionButtonTitle }}</gl-button
>
diff --git a/app/assets/javascripts/notes/stores/actions.js b/app/assets/javascripts/notes/stores/actions.js
index ee668f4406f..f62b17de10c 100644
--- a/app/assets/javascripts/notes/stores/actions.js
+++ b/app/assets/javascripts/notes/stores/actions.js
@@ -244,21 +244,7 @@ export const toggleResolveNote = ({ commit, dispatch }, { endpoint, isResolved,
});
};
-export const toggleBlockedIssueWarning = ({ commit }, value) => {
- commit(types.TOGGLE_BLOCKED_ISSUE_WARNING, value);
- // Hides Close issue button at the top of issue page
- const closeDropdown = document.querySelector('.js-issuable-close-dropdown');
- if (closeDropdown) {
- closeDropdown.classList.toggle('d-none');
- } else {
- const closeButton = document.querySelector(
- '.detail-page-header-actions .btn-close.btn-grouped',
- );
- closeButton.classList.toggle('d-md-block');
- }
-};
-
-export const closeIssue = ({ commit, dispatch, state }) => {
+export const closeMergeRequest = ({ commit, dispatch, state }) => {
dispatch('toggleStateButtonLoading', true);
return axios.put(state.notesData.closePath).then(({ data }) => {
commit(types.CLOSE_ISSUE);
@@ -267,7 +253,7 @@ export const closeIssue = ({ commit, dispatch, state }) => {
});
};
-export const reopenIssue = ({ commit, dispatch, state }) => {
+export const reopenMergeRequest = ({ commit, dispatch, state }) => {
dispatch('toggleStateButtonLoading', true);
return axios.put(state.notesData.reopenPath).then(({ data }) => {
commit(types.REOPEN_ISSUE);
diff --git a/app/assets/javascripts/notes/stores/modules/index.js b/app/assets/javascripts/notes/stores/modules/index.js
index 3194a2099ea..4421a84a6b1 100644
--- a/app/assets/javascripts/notes/stores/modules/index.js
+++ b/app/assets/javascripts/notes/stores/modules/index.js
@@ -26,7 +26,6 @@ export default () => ({
// View layer
isToggleStateButtonLoading: false,
- isToggleBlockedIssueWarning: false,
isNotesFetched: false,
isLoading: true,
isLoadingDescriptionVersion: false,
diff --git a/app/assets/javascripts/notes/stores/mutation_types.js b/app/assets/javascripts/notes/stores/mutation_types.js
index 8270f2a225b..5c4f62f4575 100644
--- a/app/assets/javascripts/notes/stores/mutation_types.js
+++ b/app/assets/javascripts/notes/stores/mutation_types.js
@@ -44,7 +44,6 @@ export const SET_RESOLVING_DISCUSSION = 'SET_RESOLVING_DISCUSSION';
export const CLOSE_ISSUE = 'CLOSE_ISSUE';
export const REOPEN_ISSUE = 'REOPEN_ISSUE';
export const TOGGLE_STATE_BUTTON_LOADING = 'TOGGLE_STATE_BUTTON_LOADING';
-export const TOGGLE_BLOCKED_ISSUE_WARNING = 'TOGGLE_BLOCKED_ISSUE_WARNING';
export const SET_ISSUE_CONFIDENTIAL = 'SET_ISSUE_CONFIDENTIAL';
export const SET_ISSUABLE_LOCK = 'SET_ISSUABLE_LOCK';
diff --git a/app/assets/javascripts/notes/stores/mutations.js b/app/assets/javascripts/notes/stores/mutations.js
index 85bdf60e8f9..53387b2eaff 100644
--- a/app/assets/javascripts/notes/stores/mutations.js
+++ b/app/assets/javascripts/notes/stores/mutations.js
@@ -305,10 +305,6 @@ export default {
Object.assign(state, { isToggleStateButtonLoading: value });
},
- [types.TOGGLE_BLOCKED_ISSUE_WARNING](state, value) {
- Object.assign(state, { isToggleBlockedIssueWarning: value });
- },
-
[types.SET_NOTES_FETCHED_STATE](state, value) {
Object.assign(state, { isNotesFetched: value });
},
diff --git a/app/assets/javascripts/vue_shared/components/gfm_autocomplete/gfm_autocomplete.vue b/app/assets/javascripts/vue_shared/components/gfm_autocomplete/gfm_autocomplete.vue
index c45666e69eb..fb61c13983f 100644
--- a/app/assets/javascripts/vue_shared/components/gfm_autocomplete/gfm_autocomplete.vue
+++ b/app/assets/javascripts/vue_shared/components/gfm_autocomplete/gfm_autocomplete.vue
@@ -1,10 +1,13 @@
<script>
import Tribute from 'tributejs';
+import {
+ GfmAutocompleteType,
+ tributeConfig,
+} from 'ee_else_ce/vue_shared/components/gfm_autocomplete/utils';
import createFlash from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { __ } from '~/locale';
import SidebarMediator from '~/sidebar/sidebar_mediator';
-import { GfmAutocompleteType, tributeConfig } from '~/vue_shared/components/gfm_autocomplete/utils';
export default {
errorMessage: __(
diff --git a/app/assets/javascripts/vue_shared/components/gfm_autocomplete/utils.js b/app/assets/javascripts/vue_shared/components/gfm_autocomplete/utils.js
index b2e995d0f17..2581888b504 100644
--- a/app/assets/javascripts/vue_shared/components/gfm_autocomplete/utils.js
+++ b/app/assets/javascripts/vue_shared/components/gfm_autocomplete/utils.js
@@ -24,7 +24,7 @@ export const tributeConfig = {
[GfmAutocompleteType.Issues]: {
config: {
trigger: '#',
- lookup: value => value.iid + value.title,
+ lookup: value => `${value.iid}${value.title}`,
menuItemTemplate: ({ original }) =>
`<small>${original.reference || original.iid}</small> ${escape(original.title)}`,
selectTemplate: ({ original }) => original.reference || `#${original.iid}`,
@@ -61,7 +61,7 @@ export const tributeConfig = {
trigger: '@',
fillAttr: 'username',
lookup: value =>
- value.type === groupType ? last(value.name.split(' / ')) : value.name + value.username,
+ value.type === groupType ? last(value.name.split(' / ')) : `${value.name}${value.username}`,
menuItemTemplate: ({ original }) => {
const commonClasses = 'gl-avatar gl-avatar-s24 gl-flex-shrink-0';
const noAvatarClasses = `${commonClasses} gl-rounded-small
@@ -115,7 +115,7 @@ export const tributeConfig = {
[GfmAutocompleteType.MergeRequests]: {
config: {
trigger: '!',
- lookup: value => value.iid + value.title,
+ lookup: value => `${value.iid}${value.title}`,
menuItemTemplate: ({ original }) =>
`<small>${original.reference || original.iid}</small> ${escape(original.title)}`,
selectTemplate: ({ original }) => original.reference || `!${original.iid}`,
@@ -135,7 +135,7 @@ export const tributeConfig = {
config: {
trigger: '$',
fillAttr: 'id',
- lookup: value => value.id + value.title,
+ lookup: value => `${value.id}${value.title}`,
menuItemTemplate: ({ original }) => `<small>${original.id}</small> ${escape(original.title)}`,
},
},
diff --git a/app/assets/javascripts/vue_shared/components/markdown/field.vue b/app/assets/javascripts/vue_shared/components/markdown/field.vue
index 0d703545073..232a3054cd0 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/field.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/field.vue
@@ -173,7 +173,7 @@ export default {
members: this.enableAutocomplete && !this.glFeatures.tributeAutocomplete,
issues: this.enableAutocomplete && !this.glFeatures.tributeAutocomplete,
mergeRequests: this.enableAutocomplete && !this.glFeatures.tributeAutocomplete,
- epics: this.enableAutocomplete,
+ epics: this.enableAutocomplete && !this.glFeatures.tributeAutocomplete,
milestones: this.enableAutocomplete && !this.glFeatures.tributeAutocomplete,
labels: this.enableAutocomplete && !this.glFeatures.tributeAutocomplete,
snippets: this.enableAutocomplete && !this.glFeatures.tributeAutocomplete,
diff --git a/app/assets/javascripts/vue_shared/components/notes/noteable_warning.vue b/app/assets/javascripts/vue_shared/components/notes/noteable_warning.vue
index c12012d8419..ad6f6e0e2e3 100644
--- a/app/assets/javascripts/vue_shared/components/notes/noteable_warning.vue
+++ b/app/assets/javascripts/vue_shared/components/notes/noteable_warning.vue
@@ -88,7 +88,7 @@ export default {
};
</script>
<template>
- <div class="issuable-note-warning">
+ <div class="issuable-note-warning" data-testid="confidential-warning">
<gl-icon v-if="!isLockedAndConfidential" :name="warningIcon" :size="16" class="icon inline" />
<span v-if="isLockedAndConfidential" ref="lockedAndConfidential">
diff --git a/app/assets/stylesheets/page_bundles/alert_management_details.scss b/app/assets/stylesheets/page_bundles/alert_management_details.scss
index beb80a14c5a..2eaf4517710 100644
--- a/app/assets/stylesheets/page_bundles/alert_management_details.scss
+++ b/app/assets/stylesheets/page_bundles/alert_management_details.scss
@@ -17,22 +17,19 @@
}
}
- .assignee-dropdown-item {
- .dropdown-item {
- @include gl-display-flex;
- @include gl-align-items-center;
-
+ .dropdown-item {
+ &:first-child {
&::before {
- top: 50% !important;
+ @include gl-pt-0;
}
+ }
- &.is-active {
- &:last-child {
- @include gl-border-b-gray-100;
- @include gl-border-b-1;
- @include gl-border-b-solid;
- }
- }
+ &::before {
+ @include gl-pt-8;
+ }
+
+ .gl-new-dropdown-item-text-wrapper {
+ @include gl-py-0;
}
}
diff --git a/app/controllers/concerns/snippets_actions.rb b/app/controllers/concerns/snippets_actions.rb
index 0153ede2821..df7a574848f 100644
--- a/app/controllers/concerns/snippets_actions.rb
+++ b/app/controllers/concerns/snippets_actions.rb
@@ -9,11 +9,14 @@ module SnippetsActions
include Gitlab::NoteableMetadata
include Snippets::SendBlob
include SnippetsSort
+ include RedisTracking
included do
skip_before_action :verify_authenticity_token,
if: -> { action_name == 'show' && js_request? }
+ track_redis_hll_event :show, name: 'i_snippets_show', feature: :usage_data_i_snippets_show, feature_default_enabled: false
+
respond_to :html
end
diff --git a/app/models/container_repository.rb b/app/models/container_repository.rb
index 4adbd37608f..0d7ce966537 100644
--- a/app/models/container_repository.rb
+++ b/app/models/container_repository.rb
@@ -25,8 +25,7 @@ class ContainerRepository < ApplicationRecord
.with_container_registry
.select(:id)
- ContainerRepository
- .joins("INNER JOIN (#{project_scope.to_sql}) projects on projects.id=container_repositories.project_id")
+ joins("INNER JOIN (#{project_scope.to_sql}) projects on projects.id=container_repositories.project_id")
end
scope :for_project_id, ->(project_id) { where(project_id: project_id) }
scope :search_by_name, ->(query) { fuzzy_search(query, [:name], use_minimum_char_limit: false) }
diff --git a/changelogs/unreleased/255171-dropdown-alerts-replacement.yml b/changelogs/unreleased/255171-dropdown-alerts-replacement.yml
new file mode 100644
index 00000000000..fed1d5dcffd
--- /dev/null
+++ b/changelogs/unreleased/255171-dropdown-alerts-replacement.yml
@@ -0,0 +1,5 @@
+---
+title: Update alert details sidebar assignee dropdown to use correct styling and formatting
+merge_request: 48285
+author:
+type: fixed
diff --git a/config/feature_flags/development/usage_data_i_snippets_show.yml b/config/feature_flags/development/usage_data_i_snippets_show.yml
new file mode 100644
index 00000000000..01dcb4da1e9
--- /dev/null
+++ b/config/feature_flags/development/usage_data_i_snippets_show.yml
@@ -0,0 +1,8 @@
+---
+name: usage_data_i_snippets_show
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/48113
+rollout_issue_url:
+milestone: '13.7'
+type: development
+group: group::editor
+default_enabled: false
diff --git a/doc/user/group/epics/img/new_epic_form_v13.2.png b/doc/user/group/epics/img/new_epic_form_v13.2.png
deleted file mode 100644
index ac1450ae111..00000000000
--- a/doc/user/group/epics/img/new_epic_form_v13.2.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/group/epics/img/new_epic_from_groups_v13.2.png b/doc/user/group/epics/img/new_epic_from_groups_v13.2.png
deleted file mode 100644
index bb75605af60..00000000000
--- a/doc/user/group/epics/img/new_epic_from_groups_v13.2.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/group/epics/img/new_epic_from_groups_v13.7.png b/doc/user/group/epics/img/new_epic_from_groups_v13.7.png
new file mode 100644
index 00000000000..3607d5c7a3f
--- /dev/null
+++ b/doc/user/group/epics/img/new_epic_from_groups_v13.7.png
Binary files differ
diff --git a/doc/user/group/epics/manage_epics.md b/doc/user/group/epics/manage_epics.md
index 5895b611bb3..7cc49783707 100644
--- a/doc/user/group/epics/manage_epics.md
+++ b/doc/user/group/epics/manage_epics.md
@@ -14,42 +14,28 @@ to them.
## Create an epic
-A paginated list of epics is available in each group from where you can create
-a new epic. The list of epics includes also epics from all subgroups of the
-selected group. From your group page:
+> - The New Epic form [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/211533) in [GitLab Premium](https://about.gitlab.com/pricing/) 13.2.
+> - In [GitLab 13.7](https://gitlab.com/gitlab-org/gitlab/-/issues/229621) and later, the New Epic button on the Epics list opens the New Epic form.
-### Create an epic from the epic list
+To create an epic in the group you're in:
-To create an epic from the epic list, in a group:
+1. Get to the New Epic form:
+ - From the **Epics** list in your group, select the **New Epic** button.
+ - From an epic in your group, select the **New Epic** button.
+ - From anywhere, in the top menu, select **New...** (**{plus-square}**) **> New epic**.
-1. Go to **{epic}** **Epics**.
-1. Select **New epic**.
-1. Enter a descriptive title.
-1. Select **Create epic**.
+ ![New epic from an open epic](img/new_epic_from_groups_v13.7.png)
-### Access the New Epic form
+1. Fill in these fields:
-> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/211533) in [GitLab Premium](https://about.gitlab.com/pricing/) 13.2.
+ - Title
+ - Description
+ - [Confidentiality checkbox](#make-an-epic-confidential)
+ - Labels
+ - Start date
+ - Due date
-There are two ways to get to the New Epic form and create an epic in the group you're in:
-
-- From an epic in your group, select **New Epic**.
-- From anywhere, in the top menu, select **plus** (**{plus-square}**) **> New epic**.
-
- ![New epic from an open epic](img/new_epic_from_groups_v13.2.png)
-
-### Elements of the New Epic form
-
-When you're creating a new epic, these are the fields you can fill in:
-
-- Title
-- Description
-- Confidentiality checkbox
-- Labels
-- Start date
-- Due date
-
-![New epic form](img/new_epic_form_v13.2.png)
+1. Select **Create epic**. You are taken to view the newly created epic.
## Edit an epic
diff --git a/lib/gitlab/danger/roulette.rb b/lib/gitlab/danger/roulette.rb
index 23f877b4e0f..328083f7002 100644
--- a/lib/gitlab/danger/roulette.rb
+++ b/lib/gitlab/danger/roulette.rb
@@ -24,7 +24,7 @@ module Gitlab
#
# @return [Array<Spin>]
def spin(project, categories, timezone_experiment: false)
- spins = categories.map do |category|
+ spins = categories.sort.map do |category|
including_timezone = INCLUDE_TIMEZONE_FOR_CATEGORY.fetch(category, timezone_experiment)
spin_for_category(project, category, timezone_experiment: including_timezone)
diff --git a/lib/gitlab/usage_data_counters/known_events/common.yml b/lib/gitlab/usage_data_counters/known_events/common.yml
index 58b023d374c..35e74c803d7 100644
--- a/lib/gitlab/usage_data_counters/known_events/common.yml
+++ b/lib/gitlab/usage_data_counters/known_events/common.yml
@@ -408,3 +408,8 @@
redis_slot: ci_secrets_management
aggregation: weekly
feature_flag: usage_data_i_ci_secrets_management_vault_build_created
+- name: i_snippets_show
+ category: snippets
+ redis_slot: snippets
+ aggregation: weekly
+ feature_flag: usage_data_i_snippets_show
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 7ec22748d11..81f45cb0a91 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -25422,7 +25422,7 @@ msgstr ""
msgid "Something went wrong while archiving a requirement."
msgstr ""
-msgid "Something went wrong while closing the %{issuable}. Please try again later"
+msgid "Something went wrong while closing the merge request. Please try again later"
msgstr ""
msgid "Something went wrong while creating a requirement."
@@ -25509,7 +25509,7 @@ msgstr ""
msgid "Something went wrong while reopening a requirement."
msgstr ""
-msgid "Something went wrong while reopening the %{issuable}. Please try again later"
+msgid "Something went wrong while reopening the merge request. Please try again later"
msgstr ""
msgid "Something went wrong while resolving this discussion. Please try again."
diff --git a/spec/controllers/snippets_controller_spec.rb b/spec/controllers/snippets_controller_spec.rb
index 993ab5d1c72..51cecb348c8 100644
--- a/spec/controllers/snippets_controller_spec.rb
+++ b/spec/controllers/snippets_controller_spec.rb
@@ -172,6 +172,13 @@ RSpec.describe SnippetsController do
expect(assigns(:snippet)).to eq(public_snippet)
expect(response).to have_gitlab_http_status(:ok)
end
+
+ it_behaves_like 'tracking unique hll events', :usage_data_i_snippets_show do
+ subject(:request) { get :show, params: { id: public_snippet.to_param } }
+
+ let(:target_id) { 'i_snippets_show' }
+ let(:expected_type) { instance_of(String) }
+ end
end
context 'when not signed in' do
diff --git a/spec/frontend/alert_management/components/sidebar/alert_managment_sidebar_assignees_spec.js b/spec/frontend/alert_management/components/sidebar/alert_managment_sidebar_assignees_spec.js
index 1d87301aac9..6430273ec59 100644
--- a/spec/frontend/alert_management/components/sidebar/alert_managment_sidebar_assignees_spec.js
+++ b/spec/frontend/alert_management/components/sidebar/alert_managment_sidebar_assignees_spec.js
@@ -179,7 +179,7 @@ describe('Alert Details Sidebar Assignees', () => {
findAssigned()
.find('.dropdown-menu-user-username')
.text(),
- ).toBe('root');
+ ).toBe('@root');
});
});
});
diff --git a/spec/frontend/issue_show/components/header_actions_spec.js b/spec/frontend/issue_show/components/header_actions_spec.js
index 67b8665a889..b9836ae7240 100644
--- a/spec/frontend/issue_show/components/header_actions_spec.js
+++ b/spec/frontend/issue_show/components/header_actions_spec.js
@@ -7,6 +7,7 @@ import HeaderActions from '~/issue_show/components/header_actions.vue';
import { IssuableStatus, IssueStateEvent } from '~/issue_show/constants';
import promoteToEpicMutation from '~/issue_show/queries/promote_to_epic.mutation.graphql';
import * as urlUtility from '~/lib/utils/url_utility';
+import eventHub from '~/notes/event_hub';
import createStore from '~/notes/stores';
jest.mock('~/flash');
@@ -82,8 +83,10 @@ describe('HeaderActions component', () => {
} = {}) => {
mutateMock = jest.fn().mockResolvedValue(mutateResponse);
- store.getters.getNoteableData.state = issueState;
- store.getters.getNoteableData.blocked_by_issues = blockedByIssues;
+ store.dispatch('setNoteableData', {
+ blocked_by_issues: blockedByIssues,
+ state: issueState,
+ });
return shallowMount(HeaderActions, {
localVue,
@@ -273,6 +276,26 @@ describe('HeaderActions component', () => {
});
});
+ describe('when `toggle.issuable.state` event is emitted', () => {
+ it('invokes a method to toggle the issue state', () => {
+ wrapper = mountComponent({ mutateResponse: updateIssueMutationResponse });
+
+ eventHub.$emit('toggle.issuable.state');
+
+ expect(mutateMock).toHaveBeenCalledWith(
+ expect.objectContaining({
+ variables: {
+ input: {
+ iid: defaultProps.iid,
+ projectPath: defaultProps.projectPath,
+ stateEvent: IssueStateEvent.Close,
+ },
+ },
+ }),
+ );
+ });
+ });
+
describe('modal', () => {
const blockedByIssues = [
{ iid: 13, web_url: 'gitlab-org/gitlab-test/-/issues/13' },
diff --git a/spec/frontend/notes/components/comment_form_spec.js b/spec/frontend/notes/components/comment_form_spec.js
index 59fa7b372ed..2f5cd2895b9 100644
--- a/spec/frontend/notes/components/comment_form_spec.js
+++ b/spec/frontend/notes/components/comment_form_spec.js
@@ -1,14 +1,14 @@
-import $ from 'jquery';
-import { mount } from '@vue/test-utils';
+import { nextTick } from 'vue';
+import { mount, shallowMount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
import Autosize from 'autosize';
-import { trimText } from 'helpers/text_helper';
import axios from '~/lib/utils/axios_utils';
import createStore from '~/notes/stores';
import CommentForm from '~/notes/components/comment_form.vue';
import * as constants from '~/notes/constants';
+import eventHub from '~/notes/event_hub';
import { refreshUserMergeRequestCounts } from '~/commons/nav/user_merge_requests';
-import { keyboardDownEvent } from '../../issue_show/helpers';
+import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue';
import { loggedOutnoteableData, notesDataMock, userDataMock, noteableDataMock } from '../mock_data';
jest.mock('autosize');
@@ -20,17 +20,33 @@ describe('issue_comment_form component', () => {
let wrapper;
let axiosMock;
- const setupStore = (userData, noteableData) => {
- store.dispatch('setUserData', userData);
+ const findCloseReopenButton = () => wrapper.find('[data-testid="close-reopen-button"]');
+
+ const findCommentButton = () => wrapper.find('[data-testid="comment-button"]');
+
+ const findTextArea = () => wrapper.find('[data-testid="comment-field"]');
+
+ const mountComponent = ({
+ initialData = {},
+ noteableType = 'issue',
+ noteableData = noteableDataMock,
+ notesData = notesDataMock,
+ userData = userDataMock,
+ mountFunction = shallowMount,
+ } = {}) => {
store.dispatch('setNoteableData', noteableData);
- store.dispatch('setNotesData', notesDataMock);
- };
+ store.dispatch('setNotesData', notesData);
+ store.dispatch('setUserData', userData);
- const mountComponent = (noteableType = 'issue') => {
- wrapper = mount(CommentForm, {
+ wrapper = mountFunction(CommentForm, {
propsData: {
noteableType,
},
+ data() {
+ return {
+ ...initialData,
+ };
+ },
store,
});
};
@@ -46,168 +62,157 @@ describe('issue_comment_form component', () => {
});
describe('user is logged in', () => {
- beforeEach(() => {
- setupStore(userDataMock, noteableDataMock);
-
- mountComponent();
- });
+ describe('avatar', () => {
+ it('should render user avatar with link', () => {
+ mountComponent({ mountFunction: mount });
- it('should render user avatar with link', () => {
- expect(wrapper.find('.timeline-icon .user-avatar-link').attributes('href')).toEqual(
- userDataMock.path,
- );
+ expect(wrapper.find(UserAvatarLink).attributes('href')).toBe(userDataMock.path);
+ });
});
describe('handleSave', () => {
it('should request to save note when note is entered', () => {
- wrapper.vm.note = 'hello world';
- jest.spyOn(wrapper.vm, 'saveNote').mockReturnValue(new Promise(() => {}));
+ mountComponent({ mountFunction: mount, initialData: { note: 'hello world' } });
+
+ jest.spyOn(wrapper.vm, 'saveNote').mockResolvedValue();
jest.spyOn(wrapper.vm, 'resizeTextarea');
jest.spyOn(wrapper.vm, 'stopPolling');
- wrapper.vm.handleSave();
+ findCloseReopenButton().trigger('click');
- expect(wrapper.vm.isSubmitting).toEqual(true);
- expect(wrapper.vm.note).toEqual('');
+ expect(wrapper.vm.isSubmitting).toBe(true);
+ expect(wrapper.vm.note).toBe('');
expect(wrapper.vm.saveNote).toHaveBeenCalled();
expect(wrapper.vm.stopPolling).toHaveBeenCalled();
expect(wrapper.vm.resizeTextarea).toHaveBeenCalled();
});
it('should toggle issue state when no note', () => {
+ mountComponent({ mountFunction: mount });
+
jest.spyOn(wrapper.vm, 'toggleIssueState');
- wrapper.vm.handleSave();
+ findCloseReopenButton().trigger('click');
expect(wrapper.vm.toggleIssueState).toHaveBeenCalled();
});
- it('should disable action button while submitting', done => {
+ it('should disable action button while submitting', async () => {
+ mountComponent({ mountFunction: mount, initialData: { note: 'hello world' } });
+
const saveNotePromise = Promise.resolve();
- wrapper.vm.note = 'hello world';
+
jest.spyOn(wrapper.vm, 'saveNote').mockReturnValue(saveNotePromise);
jest.spyOn(wrapper.vm, 'stopPolling');
- const actionButton = wrapper.find('.js-action-button');
-
- wrapper.vm.handleSave();
-
- wrapper.vm
- .$nextTick()
- .then(() => {
- expect(actionButton.vm.disabled).toBeTruthy();
- })
- .then(saveNotePromise)
- .then(wrapper.vm.$nextTick)
- .then(() => {
- expect(actionButton.vm.disabled).toBeFalsy();
- })
- .then(done)
- .catch(done.fail);
+ const actionButton = findCloseReopenButton();
+
+ await actionButton.trigger('click');
+
+ expect(actionButton.props('disabled')).toBe(true);
+
+ await saveNotePromise;
+
+ await nextTick();
+
+ expect(actionButton.props('disabled')).toBe(false);
});
});
describe('textarea', () => {
- it('should render textarea with placeholder', () => {
- expect(wrapper.find('.js-main-target-form textarea').attributes('placeholder')).toEqual(
- 'Write a comment or drag your files here…',
- );
- });
+ describe('general', () => {
+ it('should render textarea with placeholder', () => {
+ mountComponent({ mountFunction: mount });
- it('should make textarea disabled while requesting', done => {
- const $submitButton = $(wrapper.find('.js-comment-submit-button').element);
- wrapper.vm.note = 'hello world';
- jest.spyOn(wrapper.vm, 'stopPolling');
- jest.spyOn(wrapper.vm, 'saveNote').mockReturnValue(new Promise(() => {}));
-
- wrapper.vm.$nextTick(() => {
- // Wait for wrapper.vm.note change triggered. It should enable $submitButton.
- $submitButton.trigger('click');
-
- wrapper.vm.$nextTick(() => {
- // Wait for wrapper.isSubmitting triggered. It should disable textarea.
- expect(wrapper.find('.js-main-target-form textarea').attributes('disabled')).toBe(
- 'disabled',
- );
- done();
- });
+ expect(findTextArea().attributes('placeholder')).toBe(
+ 'Write a comment or drag your files here…',
+ );
});
- });
- it('should support quick actions', () => {
- expect(
- wrapper.find('.js-main-target-form textarea').attributes('data-supports-quick-actions'),
- ).toBe('true');
- });
+ it('should make textarea disabled while requesting', async () => {
+ mountComponent({ mountFunction: mount });
- it('should link to markdown docs', () => {
- const { markdownDocsPath } = notesDataMock;
+ jest.spyOn(wrapper.vm, 'stopPolling');
+ jest.spyOn(wrapper.vm, 'saveNote').mockResolvedValue();
- expect(
- wrapper
- .find(`a[href="${markdownDocsPath}"]`)
- .text()
- .trim(),
- ).toEqual('Markdown');
- });
+ await wrapper.setData({ note: 'hello world' });
- it('should link to quick actions docs', () => {
- const { quickActionsDocsPath } = notesDataMock;
+ await findCommentButton().trigger('click');
- expect(
- wrapper
- .find(`a[href="${quickActionsDocsPath}"]`)
- .text()
- .trim(),
- ).toEqual('quick actions');
- });
+ expect(findTextArea().attributes('disabled')).toBe('disabled');
+ });
+
+ it('should support quick actions', () => {
+ mountComponent({ mountFunction: mount });
+
+ expect(findTextArea().attributes('data-supports-quick-actions')).toBe('true');
+ });
+
+ it('should link to markdown docs', () => {
+ mountComponent({ mountFunction: mount });
+
+ const { markdownDocsPath } = notesDataMock;
- it('should resize textarea after note discarded', done => {
- jest.spyOn(wrapper.vm, 'discard');
+ expect(wrapper.find(`a[href="${markdownDocsPath}"]`).text()).toBe('Markdown');
+ });
+
+ it('should link to quick actions docs', () => {
+ mountComponent({ mountFunction: mount });
- wrapper.vm.note = 'foo';
- wrapper.vm.discard();
+ const { quickActionsDocsPath } = notesDataMock;
+
+ expect(wrapper.find(`a[href="${quickActionsDocsPath}"]`).text()).toBe('quick actions');
+ });
+
+ it('should resize textarea after note discarded', async () => {
+ mountComponent({ mountFunction: mount, initialData: { note: 'foo' } });
+
+ jest.spyOn(wrapper.vm, 'discard');
+
+ wrapper.vm.discard();
+
+ await nextTick();
- wrapper.vm.$nextTick(() => {
expect(Autosize.update).toHaveBeenCalled();
- done();
});
});
describe('edit mode', () => {
+ beforeEach(() => {
+ mountComponent();
+ });
+
it('should enter edit mode when arrow up is pressed', () => {
jest.spyOn(wrapper.vm, 'editCurrentUserLastNote');
- wrapper.find('.js-main-target-form textarea').value = 'Foo';
- wrapper
- .find('.js-main-target-form textarea')
- .element.dispatchEvent(keyboardDownEvent(38, true));
+
+ findTextArea().trigger('keydown.up');
expect(wrapper.vm.editCurrentUserLastNote).toHaveBeenCalled();
});
it('inits autosave', () => {
expect(wrapper.vm.autosave).toBeDefined();
- expect(wrapper.vm.autosave.key).toEqual(`autosave/Note/Issue/${noteableDataMock.id}`);
+ expect(wrapper.vm.autosave.key).toBe(`autosave/Note/Issue/${noteableDataMock.id}`);
});
});
describe('event enter', () => {
+ beforeEach(() => {
+ mountComponent();
+ });
+
it('should save note when cmd+enter is pressed', () => {
jest.spyOn(wrapper.vm, 'handleSave');
- wrapper.find('.js-main-target-form textarea').value = 'Foo';
- wrapper
- .find('.js-main-target-form textarea')
- .element.dispatchEvent(keyboardDownEvent(13, true));
+
+ findTextArea().trigger('keydown.enter', { metaKey: true });
expect(wrapper.vm.handleSave).toHaveBeenCalled();
});
it('should save note when ctrl+enter is pressed', () => {
jest.spyOn(wrapper.vm, 'handleSave');
- wrapper.find('.js-main-target-form textarea').value = 'Foo';
- wrapper
- .find('.js-main-target-form textarea')
- .element.dispatchEvent(keyboardDownEvent(13, false, true));
+
+ findTextArea().trigger('keydown.enter', { ctrlKey: true });
expect(wrapper.vm.handleSave).toHaveBeenCalled();
});
@@ -216,137 +221,147 @@ describe('issue_comment_form component', () => {
describe('actions', () => {
it('should be possible to close the issue', () => {
- expect(
- wrapper
- .find('.btn-comment-and-close')
- .text()
- .trim(),
- ).toEqual('Close issue');
+ mountComponent();
+
+ expect(findCloseReopenButton().text()).toBe('Close issue');
});
it('should render comment button as disabled', () => {
- expect(wrapper.find('.js-comment-submit-button').attributes('disabled')).toEqual(
- 'disabled',
- );
+ mountComponent();
+
+ expect(findCommentButton().props('disabled')).toBe(true);
});
- it('should enable comment button if it has note', done => {
- wrapper.vm.note = 'Foo';
- wrapper.vm.$nextTick(() => {
- expect(wrapper.find('.js-comment-submit-button').attributes('disabled')).toBeFalsy();
- done();
- });
+ it('should enable comment button if it has note', async () => {
+ mountComponent();
+
+ await wrapper.setData({ note: 'Foo' });
+
+ expect(findCommentButton().props('disabled')).toBe(false);
});
- it('should update buttons texts when it has note', done => {
- wrapper.vm.note = 'Foo';
- wrapper.vm.$nextTick(() => {
- expect(
- wrapper
- .find('.btn-comment-and-close')
- .text()
- .trim(),
- ).toEqual('Comment & close issue');
-
- done();
- });
+ it('should update buttons texts when it has note', () => {
+ mountComponent({ initialData: { note: 'Foo' } });
+
+ expect(findCloseReopenButton().text()).toBe('Comment & close issue');
});
- it('updates button text with noteable type', done => {
- wrapper.setProps({ noteableType: constants.MERGE_REQUEST_NOTEABLE_TYPE });
-
- wrapper.vm.$nextTick(() => {
- expect(
- wrapper
- .find('.btn-comment-and-close')
- .text()
- .trim(),
- ).toEqual('Close merge request');
- done();
- });
+ it('updates button text with noteable type', () => {
+ mountComponent({ noteableType: constants.MERGE_REQUEST_NOTEABLE_TYPE });
+
+ expect(findCloseReopenButton().text()).toBe('Close merge request');
});
describe('when clicking close/reopen button', () => {
- it('should disable button and show a loading spinner', () => {
- const toggleStateButton = wrapper.find('.js-action-button');
+ it('should show a loading spinner', async () => {
+ mountComponent({
+ noteableType: constants.MERGE_REQUEST_NOTEABLE_TYPE,
+ mountFunction: mount,
+ });
- toggleStateButton.trigger('click');
+ await findCloseReopenButton().trigger('click');
- return wrapper.vm.$nextTick().then(() => {
- expect(toggleStateButton.element.disabled).toEqual(true);
- expect(toggleStateButton.props('loading')).toBe(true);
- });
+ expect(findCloseReopenButton().props('loading')).toBe(true);
});
});
describe('when toggling state', () => {
- it('should update MR count', done => {
- jest.spyOn(wrapper.vm, 'closeIssue').mockResolvedValue();
+ describe('when issue', () => {
+ it('emits event to toggle state', () => {
+ mountComponent({ mountFunction: mount });
- wrapper.vm.toggleIssueState();
+ jest.spyOn(eventHub, '$emit');
- wrapper.vm.$nextTick(() => {
- expect(refreshUserMergeRequestCounts).toHaveBeenCalled();
+ findCloseReopenButton().trigger('click');
- done();
+ expect(eventHub.$emit).toHaveBeenCalledWith('toggle.issuable.state');
+ });
+ });
+
+ describe('when merge request', () => {
+ describe('when open', () => {
+ it('makes an API call to open the merge request', () => {
+ mountComponent({
+ noteableType: constants.MERGE_REQUEST_NOTEABLE_TYPE,
+ noteableData: { ...noteableDataMock, state: constants.OPENED },
+ mountFunction: mount,
+ });
+
+ jest.spyOn(wrapper.vm, 'closeMergeRequest').mockResolvedValue();
+
+ findCloseReopenButton().trigger('click');
+
+ expect(wrapper.vm.closeMergeRequest).toHaveBeenCalled();
+ });
+ });
+
+ describe('when closed', () => {
+ it('makes an API call to close the merge request', () => {
+ mountComponent({
+ noteableType: constants.MERGE_REQUEST_NOTEABLE_TYPE,
+ noteableData: { ...noteableDataMock, state: constants.CLOSED },
+ mountFunction: mount,
+ });
+
+ jest.spyOn(wrapper.vm, 'reopenMergeRequest').mockResolvedValue();
+
+ findCloseReopenButton().trigger('click');
+
+ expect(wrapper.vm.reopenMergeRequest).toHaveBeenCalled();
+ });
+ });
+
+ it('should update MR count', async () => {
+ mountComponent({
+ noteableType: constants.MERGE_REQUEST_NOTEABLE_TYPE,
+ mountFunction: mount,
+ });
+
+ jest.spyOn(wrapper.vm, 'closeMergeRequest').mockResolvedValue();
+
+ await findCloseReopenButton().trigger('click');
+
+ expect(refreshUserMergeRequestCounts).toHaveBeenCalled();
});
});
});
});
describe('issue is confidential', () => {
- it('shows information warning', done => {
- store.dispatch('setNoteableData', Object.assign(noteableDataMock, { confidential: true }));
- wrapper.vm.$nextTick(() => {
- expect(wrapper.find('.confidential-issue-warning')).toBeDefined();
- done();
+ it('shows information warning', () => {
+ mountComponent({
+ noteableData: { ...noteableDataMock, confidential: true },
+ mountFunction: mount,
});
+
+ expect(wrapper.find('[data-testid="confidential-warning"]').exists()).toBe(true);
});
});
});
describe('user is not logged in', () => {
beforeEach(() => {
- setupStore(null, loggedOutnoteableData);
-
- mountComponent();
+ mountComponent({ userData: null, noteableData: loggedOutnoteableData, mountFunction: mount });
});
it('should render signed out widget', () => {
- expect(trimText(wrapper.text())).toEqual('Please register or sign in to reply');
+ expect(wrapper.text()).toBe('Please register or sign in to reply');
});
it('should not render submission form', () => {
- expect(wrapper.find('textarea').exists()).toBe(false);
+ expect(findTextArea().exists()).toBe(false);
});
});
- describe('when issuable is open', () => {
- beforeEach(() => {
- setupStore(userDataMock, noteableDataMock);
- });
-
- it.each([['opened', 'warning'], ['reopened', 'warning']])(
- 'when %i, it changes the variant of the btn to %i',
- (a, expected) => {
- store.state.noteableData.state = a;
-
- mountComponent();
-
- expect(wrapper.find('.js-action-button').props('variant')).toBe(expected);
- },
- );
- });
-
- describe('when issuable is not open', () => {
- beforeEach(() => {
- setupStore(userDataMock, noteableDataMock);
-
- mountComponent();
- });
+ describe('close/reopen button variants', () => {
+ it.each([
+ [constants.OPENED, 'warning'],
+ [constants.REOPENED, 'warning'],
+ [constants.CLOSED, 'default'],
+ ])('when %s, the variant of the btn is %s', (state, expected) => {
+ mountComponent({ noteableData: { ...noteableDataMock, state } });
- it('should render the "default" variant of the button', () => {
- expect(wrapper.find('.js-action-button').props('variant')).toBe('warning');
+ expect(findCloseReopenButton().props('variant')).toBe(expected);
});
});
});
diff --git a/spec/frontend/notes/stores/actions_spec.js b/spec/frontend/notes/stores/actions_spec.js
index 4a1d42647f8..69c6b7d4a14 100644
--- a/spec/frontend/notes/stores/actions_spec.js
+++ b/spec/frontend/notes/stores/actions_spec.js
@@ -174,10 +174,10 @@ describe('Actions Notes Store', () => {
axiosMock.onAny().reply(200, {});
});
- describe('closeIssue', () => {
+ describe('closeMergeRequest', () => {
it('sets state as closed', done => {
store
- .dispatch('closeIssue', { notesData: { closeIssuePath: '' } })
+ .dispatch('closeMergeRequest', { notesData: { closeIssuePath: '' } })
.then(() => {
expect(store.state.noteableData.state).toEqual('closed');
expect(store.state.isToggleStateButtonLoading).toEqual(false);
@@ -187,10 +187,10 @@ describe('Actions Notes Store', () => {
});
});
- describe('reopenIssue', () => {
+ describe('reopenMergeRequest', () => {
it('sets state as reopened', done => {
store
- .dispatch('reopenIssue', { notesData: { reopenIssuePath: '' } })
+ .dispatch('reopenMergeRequest', { notesData: { reopenIssuePath: '' } })
.then(() => {
expect(store.state.noteableData.state).toEqual('reopened');
expect(store.state.isToggleStateButtonLoading).toEqual(false);
@@ -253,30 +253,6 @@ describe('Actions Notes Store', () => {
});
});
- describe('toggleBlockedIssueWarning', () => {
- it('should set issue warning as true', done => {
- testAction(
- actions.toggleBlockedIssueWarning,
- true,
- {},
- [{ type: 'TOGGLE_BLOCKED_ISSUE_WARNING', payload: true }],
- [],
- done,
- );
- });
-
- it('should set issue warning as false', done => {
- testAction(
- actions.toggleBlockedIssueWarning,
- false,
- {},
- [{ type: 'TOGGLE_BLOCKED_ISSUE_WARNING', payload: false }],
- [],
- done,
- );
- });
- });
-
describe('fetchData', () => {
describe('given there are no notes', () => {
const lastFetchedAt = '13579';
diff --git a/spec/frontend/notes/stores/mutation_spec.js b/spec/frontend/notes/stores/mutation_spec.js
index 922918ef50b..ec4de925721 100644
--- a/spec/frontend/notes/stores/mutation_spec.js
+++ b/spec/frontend/notes/stores/mutation_spec.js
@@ -697,42 +697,6 @@ describe('Notes Store mutations', () => {
});
});
- describe('TOGGLE_BLOCKED_ISSUE_WARNING', () => {
- it('should set isToggleBlockedIssueWarning as true', () => {
- const state = {
- discussions: [],
- targetNoteHash: null,
- lastFetchedAt: null,
- isToggleStateButtonLoading: false,
- isToggleBlockedIssueWarning: false,
- notesData: {},
- userData: {},
- noteableData: {},
- };
-
- mutations.TOGGLE_BLOCKED_ISSUE_WARNING(state, true);
-
- expect(state.isToggleBlockedIssueWarning).toEqual(true);
- });
-
- it('should set isToggleBlockedIssueWarning as false', () => {
- const state = {
- discussions: [],
- targetNoteHash: null,
- lastFetchedAt: null,
- isToggleStateButtonLoading: false,
- isToggleBlockedIssueWarning: true,
- notesData: {},
- userData: {},
- noteableData: {},
- };
-
- mutations.TOGGLE_BLOCKED_ISSUE_WARNING(state, false);
-
- expect(state.isToggleBlockedIssueWarning).toEqual(false);
- });
- });
-
describe('SET_APPLYING_BATCH_STATE', () => {
const buildDiscussions = suggestionsInfo => {
const suggestions = suggestionsInfo.map(({ suggestionId }) => ({ id: suggestionId }));
diff --git a/spec/lib/gitlab/danger/roulette_spec.rb b/spec/lib/gitlab/danger/roulette_spec.rb
index 1a900dfba22..561e108bf31 100644
--- a/spec/lib/gitlab/danger/roulette_spec.rb
+++ b/spec/lib/gitlab/danger/roulette_spec.rb
@@ -165,6 +165,14 @@ RSpec.describe Gitlab::Danger::Roulette do
end
end
+ context 'when change contains many categories' do
+ let(:categories) { [:frontend, :test, :qa, :engineering_productivity, :ci_template, :backend] }
+
+ it 'has a deterministic sorting order' do
+ expect(spins.map(&:category)).to eq categories.sort
+ end
+ end
+
context 'when change contains QA category' do
let(:categories) { [:qa] }
diff --git a/spec/lib/gitlab/usage_data_counters/hll_redis_counter_spec.rb b/spec/lib/gitlab/usage_data_counters/hll_redis_counter_spec.rb
index 579fc048663..70ee9871486 100644
--- a/spec/lib/gitlab/usage_data_counters/hll_redis_counter_spec.rb
+++ b/spec/lib/gitlab/usage_data_counters/hll_redis_counter_spec.rb
@@ -44,7 +44,8 @@ RSpec.describe Gitlab::UsageDataCounters::HLLRedisCounter, :clean_gitlab_redis_s
'golang_packages',
'debian_packages',
'container_packages',
- 'tag_packages'
+ 'tag_packages',
+ 'snippets'
)
end
end
diff --git a/spec/lib/gitlab/usage_data_spec.rb b/spec/lib/gitlab/usage_data_spec.rb
index 746c2aef7e5..7de7916c04d 100644
--- a/spec/lib/gitlab/usage_data_spec.rb
+++ b/spec/lib/gitlab/usage_data_spec.rb
@@ -1235,7 +1235,7 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do
subject { described_class.redis_hll_counters }
let(:categories) { ::Gitlab::UsageDataCounters::HLLRedisCounter.categories }
- let(:ineligible_total_categories) { %w[source_code testing ci_secrets_management incident_management_alerts] }
+ let(:ineligible_total_categories) { %w[source_code testing ci_secrets_management incident_management_alerts snippets] }
it 'has all known_events' do
expect(subject).to have_key(:redis_hll_counters)