Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
0e0890828e
commit
b1a0a71628
15681
CHANGELOG.md
15681
CHANGELOG.md
File diff suppressed because it is too large
Load Diff
2
Gemfile
2
Gemfile
|
@ -191,7 +191,7 @@ gem 'typhoeus', '~> 1.4.0' # Used with Elasticsearch to support http keep-alive
|
|||
# Markdown and HTML processing
|
||||
gem 'html-pipeline', '~> 2.14.3'
|
||||
gem 'deckar01-task_list', '2.3.2'
|
||||
gem 'gitlab-markup', '~> 1.8.0', require: 'github/markup'
|
||||
gem 'gitlab-markup', '~> 1.9.0', require: 'github/markup'
|
||||
gem 'commonmarker', '~> 0.23.6'
|
||||
gem 'kramdown', '~> 2.3.1'
|
||||
gem 'RedCloth', '~> 4.3.2'
|
||||
|
|
|
@ -206,7 +206,7 @@
|
|||
{"name":"gitlab-labkit","version":"0.30.1","platform":"ruby","checksum":"bdedbd86014c83dfd6a50d20dbc1709697bba2bb9e3666383e5f28cbd312b113"},
|
||||
{"name":"gitlab-license","version":"2.2.1","platform":"ruby","checksum":"39fcf6be8b2887df8afe01b5dcbae8d08b7c5d937ff56b0fb40484a8c4f02d30"},
|
||||
{"name":"gitlab-mail_room","version":"0.0.9","platform":"ruby","checksum":"6700374b5c0aa9d9ad4e711aeb677f0b7d415a6d01d3baa699efab25349d851c"},
|
||||
{"name":"gitlab-markup","version":"1.8.1","platform":"ruby","checksum":"ab1f9fd016977497c2af25b76341dea670533014f406861834a0bd99f646707b"},
|
||||
{"name":"gitlab-markup","version":"1.9.0","platform":"ruby","checksum":"7eda045a08ec2d110084252fa13a8c9eac8bdac0e302035ca7db4b82bcbd7ed4"},
|
||||
{"name":"gitlab-net-dns","version":"0.9.1","platform":"ruby","checksum":"bcd1a08dcb31b731e8ff602d828de619d2d9f53f5812f6abacf11c720873d4cb"},
|
||||
{"name":"gitlab-sidekiq-fetcher","version":"0.9.0","platform":"ruby","checksum":"54041aec059f20c8e6dfce394e1b60e0c0a9c7cef32da912a58abbd333e13897"},
|
||||
{"name":"gitlab-styles","version":"10.0.0","platform":"ruby","checksum":"8a1b20f7b5f351605ff4ed4ec648ef37226f2774d1e1377ed99389448d6913f0"},
|
||||
|
|
|
@ -594,7 +594,7 @@ GEM
|
|||
redis (> 3.0.0, < 6.0.0)
|
||||
gitlab-license (2.2.1)
|
||||
gitlab-mail_room (0.0.9)
|
||||
gitlab-markup (1.8.1)
|
||||
gitlab-markup (1.9.0)
|
||||
gitlab-net-dns (0.9.1)
|
||||
gitlab-sidekiq-fetcher (0.9.0)
|
||||
json (>= 2.5)
|
||||
|
@ -1682,7 +1682,7 @@ DEPENDENCIES
|
|||
gitlab-labkit (~> 0.30.1)
|
||||
gitlab-license (~> 2.2.1)
|
||||
gitlab-mail_room (~> 0.0.9)
|
||||
gitlab-markup (~> 1.8.0)
|
||||
gitlab-markup (~> 1.9.0)
|
||||
gitlab-net-dns (~> 0.9.1)
|
||||
gitlab-sidekiq-fetcher (= 0.9.0)
|
||||
gitlab-styles (~> 10.0.0)
|
||||
|
|
4
Pipfile
4
Pipfile
|
@ -6,7 +6,7 @@ verify_ssl = true
|
|||
[dev-packages]
|
||||
|
||||
[packages]
|
||||
docutils = "==0.13.1"
|
||||
docutils = "==0.19"
|
||||
|
||||
[requires]
|
||||
python_version = "3.4"
|
||||
python_version = "3.9.6"
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
{
|
||||
"_meta": {
|
||||
"hash": {
|
||||
"sha256": "ec82d5e7c10fd591aeebbc9b7b62d730f7fd70dc52e4e4818834891aa4194c73"
|
||||
"sha256": "dccfc5b143b954e7fe0acb14d452f4f8e4eef9d611040d77d104b28f3d1b7b55"
|
||||
},
|
||||
"pipfile-spec": 6,
|
||||
"requires": {
|
||||
"python_version": "3.4"
|
||||
"python_version": "3.9.6"
|
||||
},
|
||||
"sources": [
|
||||
{
|
||||
|
@ -18,12 +18,11 @@
|
|||
"default": {
|
||||
"docutils": {
|
||||
"hashes": [
|
||||
"sha256:718c0f5fb677be0f34b781e04241c4067cbd9327b66bdd8e763201130f5175be",
|
||||
"sha256:cb3ebcb09242804f84bdbf0b26504077a054da6772c6f4d625f335cc53ebf94d",
|
||||
"sha256:de454f1015958450b72641165c08afe7023cd7e3944396448f2fb1b0ccba9d77"
|
||||
"sha256:33995a6753c30b7f577febfc2c50411fec6aac7f7ffeb7c4cfe5991072dcf9e6",
|
||||
"sha256:5e1de4d849fee02c63b040a4a3fd567f4ab104defd8a5511fbbc24a8a017efbc"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==0.13.1"
|
||||
"version": "==0.19"
|
||||
}
|
||||
},
|
||||
"develop": {}
|
||||
|
|
|
@ -117,6 +117,7 @@ export default {
|
|||
:toggle-text="s__('Boards|Move card')"
|
||||
:text-sr-only="true"
|
||||
no-caret
|
||||
data-testid="board-move-to-position"
|
||||
@action="selectMoveAction"
|
||||
/>
|
||||
</template>
|
||||
|
|
|
@ -13,6 +13,7 @@ import {
|
|||
toggleFormEventPrefix,
|
||||
DraggableItemTypes,
|
||||
listIssuablesQueries,
|
||||
ListType,
|
||||
} from 'ee_else_ce/boards/constants';
|
||||
import eventHub from '../eventhub';
|
||||
import BoardCard from './board_card.vue';
|
||||
|
@ -196,6 +197,9 @@ export default {
|
|||
disableScrollingWhenMutationInProgress() {
|
||||
return this.hasNextPage && this.isUpdateIssueOrderInProgress;
|
||||
},
|
||||
showMoveToPosition() {
|
||||
return !this.disabled && this.list.listType !== ListType.closed;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
boardListItems() {
|
||||
|
@ -382,9 +386,8 @@ export default {
|
|||
:data-draggable-item-type="$options.draggableItemTypes.card"
|
||||
:show-work-item-type-icon="!isEpicBoard"
|
||||
>
|
||||
<!-- TODO: remove the condition when https://gitlab.com/gitlab-org/gitlab/-/issues/377862 is resolved -->
|
||||
<board-card-move-to-position
|
||||
v-if="!isEpicBoard && !disabled"
|
||||
v-if="showMoveToPosition"
|
||||
:item="item"
|
||||
:index="index"
|
||||
:list="list"
|
||||
|
|
|
@ -15,9 +15,11 @@ const updateListItemsCount = ({ state, listId, value }) => {
|
|||
}
|
||||
};
|
||||
|
||||
export const removeItemFromList = ({ state, listId, itemId }) => {
|
||||
export const removeItemFromList = ({ state, listId, itemId, reordering = false }) => {
|
||||
Vue.set(state.boardItemsByListId, listId, pull(state.boardItemsByListId[listId], itemId));
|
||||
if (!reordering) {
|
||||
updateListItemsCount({ state, listId, value: -1 });
|
||||
}
|
||||
};
|
||||
|
||||
export const addItemToList = ({
|
||||
|
@ -28,6 +30,7 @@ export const addItemToList = ({
|
|||
moveAfterId,
|
||||
atIndex,
|
||||
positionInList,
|
||||
reordering = false,
|
||||
}) => {
|
||||
const listIssues = state.boardItemsByListId[listId];
|
||||
let newIndex = atIndex || 0;
|
||||
|
@ -41,7 +44,9 @@ export const addItemToList = ({
|
|||
}
|
||||
listIssues.splice(newIndex, 0, itemId);
|
||||
Vue.set(state.boardItemsByListId, listId, listIssues);
|
||||
if (!reordering) {
|
||||
updateListItemsCount({ state, listId, value: moveToStartOrLast ? 0 : 1 });
|
||||
}
|
||||
};
|
||||
|
||||
export default {
|
||||
|
|
|
@ -136,6 +136,7 @@ export default {
|
|||
'gl-align-items-flex-start! gl-justify-content-start! mr-compare-dropdown',
|
||||
toggleClass,
|
||||
]"
|
||||
:data-qa-selector="qaSelector"
|
||||
@shown="fetchData"
|
||||
@search="searchData"
|
||||
@select="selectItem"
|
||||
|
|
|
@ -1,32 +1,28 @@
|
|||
<script>
|
||||
import { GlAvatar, GlButton } from '@gitlab/ui';
|
||||
import * as Sentry from '@sentry/browser';
|
||||
import { helpPagePath } from '~/helpers/help_page_helper';
|
||||
import { getDraft, clearDraft, updateDraft } from '~/lib/utils/autosave';
|
||||
import { confirmAction } from '~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal';
|
||||
import { __, s__ } from '~/locale';
|
||||
import { clearDraft } from '~/lib/utils/autosave';
|
||||
import Tracking from '~/tracking';
|
||||
import { ASC } from '~/notes/constants';
|
||||
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
|
||||
import { updateCommentState } from '~/work_items/graphql/cache_utils';
|
||||
import MarkdownEditor from '~/vue_shared/components/markdown/markdown_editor.vue';
|
||||
import { getWorkItemQuery } from '../utils';
|
||||
import createNoteMutation from '../graphql/notes/create_work_item_note.mutation.graphql';
|
||||
import { TRACKING_CATEGORY_SHOW, i18n } from '../constants';
|
||||
import { getWorkItemQuery } from '../../utils';
|
||||
import createNoteMutation from '../../graphql/notes/create_work_item_note.mutation.graphql';
|
||||
import { TRACKING_CATEGORY_SHOW, i18n } from '../../constants';
|
||||
import WorkItemNoteSignedOut from './work_item_note_signed_out.vue';
|
||||
import WorkItemCommentLocked from './work_item_comment_locked.vue';
|
||||
import WorkItemCommentForm from './work_item_comment_form.vue';
|
||||
|
||||
export default {
|
||||
constantOptions: {
|
||||
markdownDocsPath: helpPagePath('user/markdown'),
|
||||
avatarUrl: window.gon.current_user_avatar_url,
|
||||
},
|
||||
components: {
|
||||
GlAvatar,
|
||||
GlButton,
|
||||
MarkdownEditor,
|
||||
WorkItemNoteSignedOut,
|
||||
WorkItemCommentLocked,
|
||||
WorkItemCommentForm,
|
||||
},
|
||||
mixins: [glFeatureFlagMixin(), Tracking.mixin()],
|
||||
props: {
|
||||
|
@ -78,13 +74,6 @@ export default {
|
|||
isEditing: false,
|
||||
isSubmitting: false,
|
||||
isSubmittingWithKeydown: false,
|
||||
commentText: '',
|
||||
formFieldProps: {
|
||||
'aria-label': __('Add a comment'),
|
||||
placeholder: __('Write a comment or drag your files here…'),
|
||||
id: 'work-item-add-comment',
|
||||
name: 'work-item-add-comment',
|
||||
},
|
||||
};
|
||||
},
|
||||
apollo: {
|
||||
|
@ -112,7 +101,7 @@ export default {
|
|||
},
|
||||
autosaveKey() {
|
||||
// eslint-disable-next-line @gitlab/require-i18n-strings
|
||||
return `${this.workItemId}-comment`;
|
||||
return this.discussionId ? `${this.discussionId}-comment` : `${this.workItemId}-comment`;
|
||||
},
|
||||
tracking() {
|
||||
return {
|
||||
|
@ -150,37 +139,9 @@ export default {
|
|||
},
|
||||
},
|
||||
methods: {
|
||||
startEditing() {
|
||||
this.isEditing = true;
|
||||
this.commentText = getDraft(this.autosaveKey) || '';
|
||||
},
|
||||
async cancelEditing() {
|
||||
if (this.commentText) {
|
||||
const msg = s__('WorkItem|Are you sure you want to cancel editing?');
|
||||
|
||||
const confirmed = await confirmAction(msg, {
|
||||
primaryBtnText: __('Discard changes'),
|
||||
cancelBtnText: __('Continue editing'),
|
||||
});
|
||||
|
||||
if (!confirmed) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
this.$emit('cancelEditing');
|
||||
this.isEditing = false;
|
||||
clearDraft(this.autosaveKey);
|
||||
},
|
||||
async updateWorkItem(event = {}) {
|
||||
const { key } = event;
|
||||
|
||||
if (key) {
|
||||
this.isSubmittingWithKeydown = true;
|
||||
}
|
||||
|
||||
async updateWorkItem(commentText) {
|
||||
this.isSubmitting = true;
|
||||
this.$emit('replying', this.commentText);
|
||||
this.$emit('replying', commentText);
|
||||
const { queryVariables, fetchByIid } = this;
|
||||
|
||||
try {
|
||||
|
@ -191,7 +152,7 @@ export default {
|
|||
variables: {
|
||||
input: {
|
||||
noteableId: this.workItemId,
|
||||
body: this.commentText,
|
||||
body: commentText,
|
||||
discussionId: this.discussionId || null,
|
||||
},
|
||||
},
|
||||
|
@ -204,7 +165,7 @@ export default {
|
|||
});
|
||||
clearDraft(this.autosaveKey);
|
||||
this.$emit('replied');
|
||||
this.isEditing = false;
|
||||
this.cancelEditing();
|
||||
} catch (error) {
|
||||
this.$emit('error', error.message);
|
||||
Sentry.captureException(error);
|
||||
|
@ -212,9 +173,9 @@ export default {
|
|||
|
||||
this.isSubmitting = false;
|
||||
},
|
||||
setCommentText(newText) {
|
||||
this.commentText = newText;
|
||||
updateDraft(this.autosaveKey, this.commentText);
|
||||
cancelEditing() {
|
||||
this.isEditing = false;
|
||||
this.$emit('cancelEditing');
|
||||
},
|
||||
},
|
||||
};
|
||||
|
@ -230,37 +191,19 @@ export default {
|
|||
/>
|
||||
<div v-else class="gl-relative gl-display-flex gl-align-items-flex-start gl-flex-wrap-nowrap">
|
||||
<gl-avatar :src="$options.constantOptions.avatarUrl" :size="32" class="gl-mr-3" />
|
||||
<form v-if="isEditing" class="common-note-form gfm-form js-main-target-form gl-flex-grow-1">
|
||||
<markdown-editor
|
||||
class="gl-mb-3"
|
||||
:value="commentText"
|
||||
:render-markdown-path="markdownPreviewPath"
|
||||
:markdown-docs-path="$options.constantOptions.markdownDocsPath"
|
||||
:form-field-props="formFieldProps"
|
||||
data-testid="work-item-add-comment"
|
||||
enable-autocomplete
|
||||
autofocus
|
||||
use-bottom-toolbar
|
||||
@input="setCommentText"
|
||||
@keydown.meta.enter="updateWorkItem"
|
||||
@keydown.ctrl.enter="updateWorkItem"
|
||||
@keydown.esc="cancelEditing"
|
||||
<work-item-comment-form
|
||||
v-if="isEditing"
|
||||
:work-item-type="workItemType"
|
||||
:aria-label="__('Add a comment')"
|
||||
:is-submitting="isSubmitting"
|
||||
:autosave-key="autosaveKey"
|
||||
@submitForm="updateWorkItem"
|
||||
@cancelEditing="cancelEditing"
|
||||
/>
|
||||
<gl-button
|
||||
category="primary"
|
||||
variant="confirm"
|
||||
:loading="isSubmitting"
|
||||
@click="updateWorkItem"
|
||||
>{{ __('Comment') }}
|
||||
</gl-button>
|
||||
<gl-button category="primary" class="gl-ml-3" @click="cancelEditing"
|
||||
>{{ __('Cancel') }}
|
||||
</gl-button>
|
||||
</form>
|
||||
<gl-button
|
||||
v-else
|
||||
class="gl-flex-grow-1 gl-justify-content-start! gl-text-secondary!"
|
||||
@click="startEditing"
|
||||
@click="isEditing = true"
|
||||
>{{ __('Add a comment') }}</gl-button
|
||||
>
|
||||
</div>
|
|
@ -0,0 +1,126 @@
|
|||
<script>
|
||||
import { GlButton } from '@gitlab/ui';
|
||||
import { helpPagePath } from '~/helpers/help_page_helper';
|
||||
import { s__, __ } from '~/locale';
|
||||
import { joinPaths } from '~/lib/utils/url_utility';
|
||||
import { getDraft, clearDraft, updateDraft } from '~/lib/utils/autosave';
|
||||
import { confirmAction } from '~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal';
|
||||
import MarkdownEditor from '~/vue_shared/components/markdown/markdown_editor.vue';
|
||||
|
||||
export default {
|
||||
constantOptions: {
|
||||
markdownDocsPath: helpPagePath('user/markdown'),
|
||||
},
|
||||
components: {
|
||||
GlButton,
|
||||
MarkdownEditor,
|
||||
},
|
||||
inject: ['fullPath'],
|
||||
props: {
|
||||
workItemType: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
ariaLabel: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
autosaveKey: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
isSubmitting: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
initialValue: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
commentButtonText: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: __('Comment'),
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
commentText: getDraft(this.autosaveKey) || this.initialValue || '',
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
markdownPreviewPath() {
|
||||
return joinPaths(
|
||||
'/',
|
||||
gon.relative_url_root || '',
|
||||
this.fullPath,
|
||||
`/preview_markdown?target_type=${this.workItemType}`,
|
||||
);
|
||||
},
|
||||
formFieldProps() {
|
||||
return {
|
||||
'aria-label': this.ariaLabel,
|
||||
placeholder: __('Write a comment or drag your files here…'),
|
||||
id: 'work-item-add-or-edit-comment',
|
||||
name: 'work-item-add-or-edit-comment',
|
||||
};
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
setCommentText(newText) {
|
||||
this.commentText = newText;
|
||||
updateDraft(this.autosaveKey, this.commentText);
|
||||
},
|
||||
async cancelEditing() {
|
||||
if (this.commentText && this.commentText !== this.initialValue) {
|
||||
const msg = s__('WorkItem|Are you sure you want to cancel editing?');
|
||||
|
||||
const confirmed = await confirmAction(msg, {
|
||||
primaryBtnText: __('Discard changes'),
|
||||
cancelBtnText: __('Continue editing'),
|
||||
primaryBtnVariant: 'danger',
|
||||
});
|
||||
|
||||
if (!confirmed) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
this.$emit('cancelEditing');
|
||||
clearDraft(this.autosaveKey);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<form class="common-note-form gfm-form js-main-target-form gl-flex-grow-1">
|
||||
<markdown-editor
|
||||
:value="commentText"
|
||||
:render-markdown-path="markdownPreviewPath"
|
||||
:markdown-docs-path="$options.constantOptions.markdownDocsPath"
|
||||
:form-field-props="formFieldProps"
|
||||
data-testid="work-item-add-comment"
|
||||
class="gl-mb-3"
|
||||
autofocus
|
||||
use-bottom-toolbar
|
||||
@input="setCommentText"
|
||||
@keydown.meta.enter="$emit('submitForm', commentText)"
|
||||
@keydown.ctrl.enter="$emit('submitForm', commentText)"
|
||||
@keydown.esc.stop="cancelEditing"
|
||||
/>
|
||||
<gl-button
|
||||
category="primary"
|
||||
variant="confirm"
|
||||
data-testid="confirm-button"
|
||||
:loading="isSubmitting"
|
||||
@click="$emit('submitForm', commentText)"
|
||||
>{{ commentButtonText }}
|
||||
</gl-button>
|
||||
<gl-button data-testid="cancel-button" category="primary" class="gl-ml-3" @click="cancelEditing"
|
||||
>{{ __('Cancel') }}
|
||||
</gl-button>
|
||||
</form>
|
||||
</template>
|
|
@ -6,7 +6,7 @@ import DiscussionNotesRepliesWrapper from '~/notes/components/discussion_notes_r
|
|||
import ToggleRepliesWidget from '~/notes/components/toggle_replies_widget.vue';
|
||||
import WorkItemNote from '~/work_items/components/notes/work_item_note.vue';
|
||||
import WorkItemNoteReplying from '~/work_items/components/notes/work_item_note_replying.vue';
|
||||
import WorkItemCommentForm from '../work_item_comment_form.vue';
|
||||
import WorkItemAddNote from './work_item_add_note.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
|
@ -14,7 +14,7 @@ export default {
|
|||
GlAvatarLink,
|
||||
GlAvatar,
|
||||
WorkItemNote,
|
||||
WorkItemCommentForm,
|
||||
WorkItemAddNote,
|
||||
ToggleRepliesWidget,
|
||||
DiscussionNotesRepliesWrapper,
|
||||
WorkItemNoteReplying,
|
||||
|
@ -139,8 +139,11 @@ export default {
|
|||
:is-first-note="true"
|
||||
:note="note"
|
||||
:discussion-id="discussionId"
|
||||
:work-item-type="workItemType"
|
||||
:class="{ 'gl-mb-5': hasReplies }"
|
||||
@startReplying="showReplyForm"
|
||||
@deleteNote="$emit('deleteNote', note)"
|
||||
@error="$emit('error', $event)"
|
||||
/>
|
||||
<discussion-notes-replies-wrapper>
|
||||
<toggle-replies-widget
|
||||
|
@ -153,14 +156,16 @@ export default {
|
|||
<template v-for="reply in replies">
|
||||
<work-item-note
|
||||
:key="threadKey(reply)"
|
||||
discussion-id="discussionId"
|
||||
:discussion-id="discussionId"
|
||||
:note="reply"
|
||||
:work-item-type="workItemType"
|
||||
@startReplying="showReplyForm"
|
||||
@deleteNote="$emit('deleteNote', reply)"
|
||||
@error="$emit('error', $event)"
|
||||
/>
|
||||
</template>
|
||||
<work-item-note-replying v-if="isReplying" :body="replyingText" />
|
||||
<work-item-comment-form
|
||||
<work-item-add-note
|
||||
:autofocus="autofocus"
|
||||
:query-variables="queryVariables"
|
||||
:full-path="fullPath"
|
||||
|
|
|
@ -1,11 +1,16 @@
|
|||
<script>
|
||||
import { GlAvatarLink, GlAvatar, GlDropdown, GlDropdownItem, GlTooltipDirective } from '@gitlab/ui';
|
||||
import * as Sentry from '@sentry/browser';
|
||||
import { __ } from '~/locale';
|
||||
import { updateDraft, clearDraft } from '~/lib/utils/autosave';
|
||||
import { renderMarkdown } from '~/notes/utils';
|
||||
import EditedAt from '~/issues/show/components/edited.vue';
|
||||
import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue';
|
||||
import NoteBody from '~/work_items/components/notes/work_item_note_body.vue';
|
||||
import NoteHeader from '~/notes/components/note_header.vue';
|
||||
import NoteActions from '~/work_items/components/notes/work_item_note_actions.vue';
|
||||
import { renderGFM } from '~/behaviors/markdown/render_gfm';
|
||||
import updateWorkItemNoteMutation from '../../graphql/notes/update_work_item_note.mutation.graphql';
|
||||
import WorkItemCommentForm from './work_item_comment_form.vue';
|
||||
|
||||
export default {
|
||||
name: 'WorkItemNoteThread',
|
||||
|
@ -22,6 +27,8 @@ export default {
|
|||
GlAvatarLink,
|
||||
GlDropdown,
|
||||
GlDropdownItem,
|
||||
WorkItemCommentForm,
|
||||
EditedAt,
|
||||
},
|
||||
directives: {
|
||||
GlTooltip: GlTooltipDirective,
|
||||
|
@ -36,6 +43,15 @@ export default {
|
|||
required: false,
|
||||
default: false,
|
||||
},
|
||||
workItemType: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isEditing: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
author() {
|
||||
|
@ -50,14 +66,54 @@ export default {
|
|||
showReply() {
|
||||
return this.note.userPermissions.createNote && this.isFirstNote;
|
||||
},
|
||||
autosaveKey() {
|
||||
// eslint-disable-next-line @gitlab/require-i18n-strings
|
||||
return `${this.note.id}-comment`;
|
||||
},
|
||||
lastEditedBy() {
|
||||
return this.note.lastEditedBy;
|
||||
},
|
||||
hasAdminPermission() {
|
||||
return this.note.userPermissions.adminNote;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
renderGFM() {
|
||||
renderGFM(this.$refs['note-body']);
|
||||
},
|
||||
showReplyForm() {
|
||||
this.$emit('startReplying');
|
||||
},
|
||||
startEditing() {
|
||||
this.isEditing = true;
|
||||
updateDraft(this.autosaveKey, this.note.body);
|
||||
},
|
||||
async updateNote(newText) {
|
||||
this.isEditing = false;
|
||||
try {
|
||||
await this.$apollo.mutate({
|
||||
mutation: updateWorkItemNoteMutation,
|
||||
variables: {
|
||||
input: {
|
||||
id: this.note.id,
|
||||
body: newText,
|
||||
},
|
||||
},
|
||||
optimisticResponse: {
|
||||
updateNote: {
|
||||
errors: [],
|
||||
note: {
|
||||
...this.note,
|
||||
bodyHtml: renderMarkdown(newText),
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
clearDraft(this.autosaveKey);
|
||||
} catch (error) {
|
||||
updateDraft(this.autosaveKey, newText);
|
||||
this.isEditing = true;
|
||||
this.$emit('error', __('Something went wrong when updating a comment. Please try again'));
|
||||
Sentry.captureException(error);
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
@ -74,13 +130,29 @@ export default {
|
|||
/>
|
||||
</gl-avatar-link>
|
||||
</div>
|
||||
<div class="timeline-content-inner">
|
||||
<work-item-comment-form
|
||||
v-if="isEditing"
|
||||
:work-item-type="workItemType"
|
||||
:aria-label="__('Edit comment')"
|
||||
:autosave-key="autosaveKey"
|
||||
:initial-value="note.body"
|
||||
:comment-button-text="__('Save comment')"
|
||||
:class="{ 'gl-pl-8': !isFirstNote }"
|
||||
@cancelEditing="isEditing = false"
|
||||
@submitForm="updateNote"
|
||||
/>
|
||||
<div v-else class="timeline-content-inner" data-testid="note-wrapper">
|
||||
<div class="note-header">
|
||||
<note-header :author="author" :created-at="note.createdAt" :note-id="note.id" />
|
||||
<note-actions :show-reply="showReply" @startReplying="showReplyForm" />
|
||||
<note-actions
|
||||
:show-reply="showReply"
|
||||
:show-edit="hasAdminPermission"
|
||||
@startReplying="showReplyForm"
|
||||
@startEditing="startEditing"
|
||||
/>
|
||||
<!-- v-if condition should be moved to "delete" dropdown item as soon as we implement copying the link -->
|
||||
<gl-dropdown
|
||||
v-if="note.userPermissions.adminNote"
|
||||
v-if="hasAdminPermission"
|
||||
v-gl-tooltip
|
||||
icon="ellipsis_v"
|
||||
text-sr-only
|
||||
|
@ -102,6 +174,13 @@ export default {
|
|||
<div class="timeline-discussion-body">
|
||||
<note-body ref="noteBody" :note="note" />
|
||||
</div>
|
||||
<edited-at
|
||||
v-if="note.lastEditedBy"
|
||||
:updated-at="note.lastEditedAt"
|
||||
:updated-by-name="lastEditedBy.name"
|
||||
:updated-by-path="lastEditedBy.webPath"
|
||||
:class="isFirstNote ? 'gl-pl-3' : 'gl-pl-8'"
|
||||
/>
|
||||
</div>
|
||||
</timeline-entry-item>
|
||||
</template>
|
||||
|
|
|
@ -1,16 +1,29 @@
|
|||
<script>
|
||||
import { GlButton, GlTooltipDirective } from '@gitlab/ui';
|
||||
import { __ } from '~/locale';
|
||||
import ReplyButton from '~/notes/components/note_actions/reply_button.vue';
|
||||
|
||||
export default {
|
||||
name: 'WorkItemNoteActions',
|
||||
i18n: {
|
||||
editButtonText: __('Edit comment'),
|
||||
},
|
||||
components: {
|
||||
GlButton,
|
||||
ReplyButton,
|
||||
},
|
||||
directives: {
|
||||
GlTooltip: GlTooltipDirective,
|
||||
},
|
||||
props: {
|
||||
showReply: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
showEdit: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
@ -18,5 +31,17 @@ export default {
|
|||
<template>
|
||||
<div class="note-actions">
|
||||
<reply-button v-if="showReply" ref="replyButton" @startReplying="$emit('startReplying')" />
|
||||
<gl-button
|
||||
v-if="showEdit"
|
||||
v-gl-tooltip
|
||||
data-testid="edit-work-item-note"
|
||||
data-track-action="click_button"
|
||||
data-track-label="edit_button"
|
||||
category="tertiary"
|
||||
icon="pencil"
|
||||
:title="$options.i18n.editButtonText"
|
||||
:aria-label="$options.i18n.editButtonText"
|
||||
@click="$emit('startEditing')"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
@ -13,9 +13,18 @@ export default {
|
|||
required: true,
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
watch: {
|
||||
'note.bodyHtml': {
|
||||
immediate: true,
|
||||
async handler(newVal, oldVal) {
|
||||
if (newVal === oldVal) {
|
||||
return;
|
||||
}
|
||||
await this.$nextTick();
|
||||
this.renderGFM();
|
||||
},
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
renderGFM() {
|
||||
renderGFM(this.$refs['note-body']);
|
||||
|
|
|
@ -10,7 +10,7 @@ import { ASC, DESC } from '~/notes/constants';
|
|||
import { getWorkItemNotesQuery } from '~/work_items/utils';
|
||||
import WorkItemDiscussion from '~/work_items/components/notes/work_item_discussion.vue';
|
||||
import deleteNoteMutation from '../graphql/notes/delete_work_item_notes.mutation.graphql';
|
||||
import WorkItemCommentForm from './work_item_comment_form.vue';
|
||||
import WorkItemAddNote from './notes/work_item_add_note.vue';
|
||||
|
||||
export default {
|
||||
i18n: {
|
||||
|
@ -26,7 +26,7 @@ export default {
|
|||
GlModal,
|
||||
ActivityFilter,
|
||||
SystemNote,
|
||||
WorkItemCommentForm,
|
||||
WorkItemAddNote,
|
||||
WorkItemDiscussion,
|
||||
},
|
||||
props: {
|
||||
|
@ -249,7 +249,7 @@ export default {
|
|||
<div v-else class="issuable-discussion gl-mb-5 gl-clearfix!">
|
||||
<template v-if="!initialLoading">
|
||||
<ul class="notes main-notes-list timeline gl-clearfix!">
|
||||
<work-item-comment-form
|
||||
<work-item-add-note
|
||||
v-if="formAtTop"
|
||||
v-bind="workItemCommentFormProps"
|
||||
@error="$emit('error', $event)"
|
||||
|
@ -271,11 +271,12 @@ export default {
|
|||
:fetch-by-iid="fetchByIid"
|
||||
:work-item-type="workItemType"
|
||||
@deleteNote="showDeleteNoteModal($event, discussion)"
|
||||
@error="$emit('error', $event)"
|
||||
/>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<work-item-comment-form
|
||||
<work-item-add-note
|
||||
v-if="!formAtTop"
|
||||
v-bind="workItemCommentFormProps"
|
||||
@error="$emit('error', $event)"
|
||||
|
|
|
@ -11,11 +11,8 @@ mutation createWorkItemNote($input: CreateNoteInput!) {
|
|||
...WorkItemNote
|
||||
}
|
||||
}
|
||||
__typename
|
||||
}
|
||||
__typename
|
||||
}
|
||||
errors
|
||||
__typename
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
#import "./work_item_note.fragment.graphql"
|
||||
|
||||
mutation updateWorkItemNote($input: UpdateNoteInput!) {
|
||||
updateNote(input: $input) {
|
||||
note {
|
||||
...WorkItemNote
|
||||
}
|
||||
errors
|
||||
}
|
||||
}
|
|
@ -2,11 +2,17 @@
|
|||
|
||||
fragment WorkItemNote on Note {
|
||||
id
|
||||
body
|
||||
bodyHtml
|
||||
system
|
||||
internal
|
||||
systemNoteIconName
|
||||
createdAt
|
||||
lastEditedAt
|
||||
lastEditedBy {
|
||||
...User
|
||||
webPath
|
||||
}
|
||||
discussion {
|
||||
id
|
||||
}
|
||||
|
|
|
@ -28,20 +28,18 @@ class Environment < ApplicationRecord
|
|||
has_many :self_managed_prometheus_alert_events, inverse_of: :environment
|
||||
has_many :alert_management_alerts, class_name: 'AlertManagement::Alert', inverse_of: :environment
|
||||
|
||||
# NOTE:
|
||||
# 1) no-op arguments is to prevent accidental legacy preloading. See: https://gitlab.com/gitlab-org/gitlab/-/issues/369240
|
||||
# 2) If you preload multiple last deployments of environments, use Preloaders::Environments::DeploymentPreloader.
|
||||
has_one :last_deployment, -> (_env) { success.ordered }, class_name: 'Deployment', inverse_of: :environment
|
||||
has_one :last_visible_deployment, -> (_env) { visible.order(id: :desc) }, inverse_of: :environment, class_name: 'Deployment'
|
||||
has_one :upcoming_deployment, -> (_env) { upcoming.order(id: :desc) }, class_name: 'Deployment', inverse_of: :environment
|
||||
# NOTE: If you preload multiple last deployments of environments, use Preloaders::Environments::DeploymentPreloader.
|
||||
has_one :last_deployment, -> { success.ordered }, class_name: 'Deployment', inverse_of: :environment
|
||||
has_one :last_visible_deployment, -> { visible.order(id: :desc) }, inverse_of: :environment, class_name: 'Deployment'
|
||||
has_one :upcoming_deployment, -> { upcoming.order(id: :desc) }, class_name: 'Deployment', inverse_of: :environment
|
||||
|
||||
Deployment::FINISHED_STATUSES.each do |status|
|
||||
has_one :"last_#{status}_deployment", -> (_env) { where(status: status).ordered },
|
||||
has_one :"last_#{status}_deployment", -> { where(status: status).ordered },
|
||||
class_name: 'Deployment', inverse_of: :environment
|
||||
end
|
||||
|
||||
Deployment::UPCOMING_STATUSES.each do |status|
|
||||
has_one :"last_#{status}_deployment", -> (_env) { where(status: status).ordered_as_upcoming },
|
||||
has_one :"last_#{status}_deployment", -> { where(status: status).ordered_as_upcoming },
|
||||
class_name: 'Deployment', inverse_of: :environment
|
||||
end
|
||||
|
||||
|
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
@ -5,4 +5,4 @@ rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/389538
|
|||
milestone: '15.9'
|
||||
type: development
|
||||
group: group::authentication and authorization
|
||||
default_enabled: false
|
||||
default_enabled: true
|
||||
|
|
|
@ -1,16 +1,16 @@
|
|||
- title: "Slack Notifications integration" # (required) Clearly explain the change, or planned change. For example, "The `confidential` field for a `Note` is deprecated" or "CI/CD job names will be limited to 250 characters."
|
||||
announcement_milestone: "15.8" # (required) The milestone when this feature was first announced as deprecated.
|
||||
removal_milestone: "16.0" # (required) The milestone when this feature is planned to be removed
|
||||
- title: "Slack notifications integration" # (required) Clearly explain the change, or planned change. For example, "The `confidential` field for a `Note` is deprecated" or "CI/CD job names will be limited to 250 characters."
|
||||
announcement_milestone: "15.9" # (required) The milestone when this feature was first announced as deprecated.
|
||||
removal_milestone: "17.0" # (required) The milestone when this feature is planned to be removed
|
||||
breaking_change: true # (required) Change to false if this is not a breaking change.
|
||||
reporter: g.hickman # (required) GitLab username of the person reporting the change
|
||||
stage: manage # (required) String value of the stage that the feature was created in. e.g., Growth
|
||||
issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/372411 # (required) Link to the deprecation issue in GitLab
|
||||
body: | # (required) Do not modify this line, instead modify the lines below.
|
||||
As we're consolidating all Slack capabilities into the
|
||||
GitLab for Slack app, we're [deprecating the Slack Notifications
|
||||
GitLab for Slack app, we're [deprecating the Slack notifications
|
||||
integration](https://gitlab.com/gitlab-org/gitlab/-/issues/372411).
|
||||
GitLab.com users can now use the GitLab for Slack app to manage notifications
|
||||
to their Slack workspace. For self-managed users of the Slack Notifications integration,
|
||||
to their Slack workspace. For self-managed users of the Slack notifications integration,
|
||||
we'll be introducing support in [this epic](https://gitlab.com/groups/gitlab-org/-/epics/1211).
|
||||
|
||||
#
|
||||
|
@ -19,7 +19,7 @@
|
|||
# If an End of Support period applies, the announcement should be shared with GitLab Support
|
||||
# in the `#spt_managers` channel in Slack, and mention `@gitlab-com/support` in this MR.
|
||||
#
|
||||
end_of_support_milestone: "16.0" # (optional) Use "XX.YY" format. The milestone when support for this feature will end.
|
||||
end_of_support_milestone: "17.0" # (optional) Use "XX.YY" format. The milestone when support for this feature will end.
|
||||
#
|
||||
# OTHER OPTIONAL FIELDS
|
||||
#
|
||||
|
|
|
@ -13,7 +13,7 @@ some of the important concepts related to them.
|
|||
You can structure your pipelines with different methods, each with their
|
||||
own advantages. These methods can be mixed and matched if needed:
|
||||
|
||||
- [Basic](#basic-pipelines): Good for straightforward projects where all the configuration is in one easy to find place.
|
||||
- [Basic](#basic-pipelines): Good for straightforward projects where all the configuration is in one easy-to-find place.
|
||||
- [Directed Acyclic Graph](#directed-acyclic-graph-pipelines): Good for large, complex projects that need efficient execution.
|
||||
- [Parent-child pipelines](#parent-child-pipelines): Good for monorepos and projects with lots of independently defined components.
|
||||
|
||||
|
|
|
@ -154,8 +154,11 @@ When a test frequently fails in `master`,
|
|||
create [a ~"failure::flaky-test" issue](https://about.gitlab.com/handbook/engineering/workflow/#broken-master).
|
||||
|
||||
If the test cannot be fixed in a timely fashion, there is an impact on the
|
||||
productivity of all the developers, so it should be quarantined by
|
||||
assigning the `:quarantine` metadata with the issue URL, and add the `~"quarantined test"` label to the issue.
|
||||
productivity of all the developers, so it should be quarantined. There are two ways to quarantine tests, depending on the test framework being used: RSpec and Jest.
|
||||
|
||||
### RSpec
|
||||
|
||||
For RSpec tests, you can use the `:quarantine` metadata with the issue URL.
|
||||
|
||||
```ruby
|
||||
it 'succeeds', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/12345' do
|
||||
|
@ -169,6 +172,26 @@ This means it is skipped unless run with `--tag quarantine`:
|
|||
bin/rspec --tag quarantine
|
||||
```
|
||||
|
||||
### Jest
|
||||
|
||||
For Jest specs, you can use the `.skip` method along with the `eslint-disable-next-line` comment to disable the `jest/no-disabled-tests` ESLint rule and include the issue URL. Here's an example:
|
||||
|
||||
```javascript
|
||||
// https://gitlab.com/gitlab-org/gitlab/-/issues/56789
|
||||
// eslint-disable-next-line jest/no-disabled-tests
|
||||
it.skip('should throw an error', () => {
|
||||
expect(response).toThrowError(expected_error)
|
||||
});
|
||||
```
|
||||
|
||||
This means it is skipped unless the test suit is run with `--runInBand` Jest command line option:
|
||||
|
||||
```shell
|
||||
jest --runInBand
|
||||
```
|
||||
|
||||
For both test frameworks, make sure to add the `~"quarantined test"` label to the issue.
|
||||
|
||||
Once a test is in quarantine, there are 3 choices:
|
||||
|
||||
- Fix the test (that is, get rid of its flakiness).
|
||||
|
|
|
@ -406,6 +406,26 @@ automatically from GitLab 16.0 onwards.
|
|||
|
||||
</div>
|
||||
|
||||
<div class="deprecation removal-170 breaking-change">
|
||||
|
||||
### Slack notifications integration
|
||||
|
||||
End of Support: GitLab <span class="removal-milestone">17.0</span> <span class="support-end-date"></span><br />
|
||||
Planned removal: GitLab <span class="removal-milestone">17.0</span> <span class="removal-date"></span>
|
||||
|
||||
WARNING:
|
||||
This is a [breaking change](https://docs.gitlab.com/ee/development/deprecation_guidelines/).
|
||||
Review the details carefully before upgrading.
|
||||
|
||||
As we're consolidating all Slack capabilities into the
|
||||
GitLab for Slack app, we're [deprecating the Slack notifications
|
||||
integration](https://gitlab.com/gitlab-org/gitlab/-/issues/372411).
|
||||
GitLab.com users can now use the GitLab for Slack app to manage notifications
|
||||
to their Slack workspace. For self-managed users of the Slack notifications integration,
|
||||
we'll be introducing support in [this epic](https://gitlab.com/groups/gitlab-org/-/epics/1211).
|
||||
|
||||
</div>
|
||||
|
||||
<div class="deprecation removal-160 breaking-change">
|
||||
|
||||
### Support for Praefect custom metrics endpoint configuration
|
||||
|
@ -832,26 +852,6 @@ Alternatives to using the `gitlab:import:repos` Rake task include:
|
|||
|
||||
<div class="deprecation removal-160 breaking-change">
|
||||
|
||||
### Slack Notifications integration
|
||||
|
||||
End of Support: GitLab <span class="removal-milestone">16.0</span> <span class="support-end-date"></span><br />
|
||||
Planned removal: GitLab <span class="removal-milestone">16.0</span> <span class="removal-date"></span>
|
||||
|
||||
WARNING:
|
||||
This is a [breaking change](https://docs.gitlab.com/ee/development/deprecation_guidelines/).
|
||||
Review the details carefully before upgrading.
|
||||
|
||||
As we're consolidating all Slack capabilities into the
|
||||
GitLab for Slack app, we're [deprecating the Slack Notifications
|
||||
integration](https://gitlab.com/gitlab-org/gitlab/-/issues/372411).
|
||||
GitLab.com users can now use the GitLab for Slack app to manage notifications
|
||||
to their Slack workspace. For self-managed users of the Slack Notifications integration,
|
||||
we'll be introducing support in [this epic](https://gitlab.com/groups/gitlab-org/-/epics/1211).
|
||||
|
||||
</div>
|
||||
|
||||
<div class="deprecation removal-160 breaking-change">
|
||||
|
||||
### Support for third party registries
|
||||
|
||||
Planned removal: GitLab <span class="removal-milestone">16.0</span> <span class="removal-date"></span>
|
||||
|
|
|
@ -470,8 +470,11 @@ subscriptions.
|
|||
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/106256) in GitLab 15.7 [with a flag](../administration/feature_flags.md) named `customizable_roles`.
|
||||
> - [Enabled by default](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/110810) in GitLab 15.9.
|
||||
|
||||
Custom roles allow group members who are assigned the Owner role to create roles specific to the needs
|
||||
of their organization.
|
||||
Custom roles allow group members who are assigned the Owner role to create roles
|
||||
specific to the needs of their organization.
|
||||
|
||||
<i class="fa fa-youtube-play youtube" aria-hidden="true"></i>
|
||||
For a demo of the custom roles feature, see [[Demo] Ultimate Guest can view code on private repositories via custom role](https://www.youtube.com/watch?v=46cp_-Rtxps).
|
||||
|
||||
### Create a custom role
|
||||
|
||||
|
@ -495,7 +498,7 @@ the Owner role:
|
|||
1. Associates the group member with the Guest+1 role using the [Group and Project Members API endpoint](../api/members.md#edit-a-member-of-a-group-or-project)
|
||||
|
||||
```shell
|
||||
curl --request PUT --header "Content-Type: application/json" --header "Authorization: Bearer $YOUR_ACCESS_TOKEN" --data '{"member_role_id":$MEMBER_ROLE_ID},"access_level":10' "https://example.gitlab.com/api/v4/groups/$GROUP_PATH/members/$GUEST_USER_ID"
|
||||
curl --request PUT --header "Content-Type: application/json" --header "Authorization: Bearer $YOUR_ACCESS_TOKEN" --data '{"member_role_id": '$MEMBER_ROLE_ID', "access_level": 10}' "https://example.gitlab.com/api/v4/groups/$GROUP_PATH/members/$GUEST_USER_ID"
|
||||
```
|
||||
|
||||
Where:
|
||||
|
@ -510,7 +513,7 @@ To remove a custom role from a group member, use the [Group and Project Members
|
|||
and pass an empty `member_role_id` value.
|
||||
|
||||
```shell
|
||||
curl --request PUT --header "Content-Type: application/json" --header "Authorization: Bearer $YOUR_ACCESS_TOKEN" --data '{"member_role_id":""},"access_level":10' "https://example.gitlab.com/api/v4/groups/$GROUP_PATH/members/$GUEST_USER_ID"
|
||||
curl --request PUT --header "Content-Type: application/json" --header "Authorization: Bearer $YOUR_ACCESS_TOKEN" --data '{"member_role_id": "", "access_level": 10}' "https://example.gitlab.com/api/v4/groups/$GROUP_PATH/members/$GUEST_USER_ID"
|
||||
```
|
||||
|
||||
Now the user is a regular Guest.
|
||||
|
|
|
@ -269,8 +269,6 @@ A `_sidebar` example, formatted with Markdown:
|
|||
- [Sidebar](_sidebar)
|
||||
```
|
||||
|
||||
Support for displaying a generated table of contents with a custom side navigation is being considered.
|
||||
|
||||
## Enable or disable a project wiki
|
||||
|
||||
Wikis are enabled by default in GitLab. Project [administrators](../../permissions.md)
|
||||
|
|
|
@ -40313,6 +40313,9 @@ msgstr ""
|
|||
msgid "Something went wrong when sending the incident link to Slack."
|
||||
msgstr ""
|
||||
|
||||
msgid "Something went wrong when updating a comment. Please try again"
|
||||
msgstr ""
|
||||
|
||||
msgid "Something went wrong while adding timeline event."
|
||||
msgstr ""
|
||||
|
||||
|
|
|
@ -12,8 +12,8 @@ module QA
|
|||
element :compare_branches_button
|
||||
end
|
||||
|
||||
view 'app/assets/javascripts/pages/projects/merge_requests/creations/new/index.js' do
|
||||
element :source_branch_dropdown
|
||||
view 'app/assets/javascripts/merge_requests/components/compare_dropdown.vue' do
|
||||
element :source_branch_dropdown, ':data-qa-selector="qaSelector"' # rubocop:disable QA/ElementWithPattern
|
||||
end
|
||||
|
||||
view 'app/views/projects/merge_requests/_page.html.haml' do
|
||||
|
|
|
@ -130,6 +130,41 @@ RSpec.describe 'Issue Boards', :js, feature_category: :team_planning do
|
|||
end
|
||||
end
|
||||
|
||||
context 'ordering in list using move to position' do
|
||||
let(:move_to_position) { find('[data-testid="board-move-to-position"]') }
|
||||
|
||||
before do
|
||||
visit project_board_path(project, board)
|
||||
wait_for_requests
|
||||
end
|
||||
|
||||
it 'moves to end of list' do
|
||||
expect(all('.board-card').first).to have_content(issue3.title)
|
||||
|
||||
page.within(find('.board:nth-child(2)')) do
|
||||
first('.board-card').hover
|
||||
move_to_position.click
|
||||
|
||||
click_button 'Move to end of list'
|
||||
end
|
||||
|
||||
expect(all('.board-card').last).to have_content(issue3.title)
|
||||
end
|
||||
|
||||
it 'moves to start of list' do
|
||||
expect(all('.board-card').last).to have_content(issue1.title)
|
||||
|
||||
page.within(find('.board:nth-child(2)')) do
|
||||
all('.board-card').last.hover
|
||||
move_to_position.click
|
||||
|
||||
click_button 'Move to start of list'
|
||||
end
|
||||
|
||||
expect(all('.board-card').first).to have_content(issue1.title)
|
||||
end
|
||||
end
|
||||
|
||||
context 'ordering when changing list' do
|
||||
let(:label2) { create(:label, project: project) }
|
||||
let!(:list2) { create(:list, board: board, label: label2, position: 1) }
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import Draggable from 'vuedraggable';
|
||||
import { nextTick } from 'vue';
|
||||
import { DraggableItemTypes } from 'ee_else_ce/boards/constants';
|
||||
import { DraggableItemTypes, ListType } from 'ee_else_ce/boards/constants';
|
||||
import { useFakeRequestAnimationFrame } from 'helpers/fake_request_animation_frame';
|
||||
import waitForPromises from 'helpers/wait_for_promises';
|
||||
import createComponent from 'jest/boards/board_list_helper';
|
||||
|
@ -107,6 +107,20 @@ describe('Board list component', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('when ListType is Closed', () => {
|
||||
beforeEach(() => {
|
||||
wrapper = createComponent({
|
||||
listProps: {
|
||||
listType: ListType.closed,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('Board card move to position is not visible', () => {
|
||||
expect(findMoveToPositionComponent().exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('load more issues', () => {
|
||||
const actions = {
|
||||
fetchItemsForList: jest.fn(),
|
||||
|
|
|
@ -5,21 +5,23 @@ import VueApollo from 'vue-apollo';
|
|||
import createMockApollo from 'helpers/mock_apollo_helper';
|
||||
import { mockTracking } from 'helpers/tracking_helper';
|
||||
import waitForPromises from 'helpers/wait_for_promises';
|
||||
import { updateDraft } from '~/lib/utils/autosave';
|
||||
import MarkdownField from '~/vue_shared/components/markdown/field.vue';
|
||||
import MarkdownEditor from '~/vue_shared/components/markdown/markdown_editor.vue';
|
||||
import WorkItemCommentForm from '~/work_items/components/work_item_comment_form.vue';
|
||||
import WorkItemCommentLocked from '~/work_items/components/work_item_comment_locked.vue';
|
||||
import { clearDraft } from '~/lib/utils/autosave';
|
||||
import { config } from '~/graphql_shared/issuable_client';
|
||||
import WorkItemAddNote from '~/work_items/components/notes/work_item_add_note.vue';
|
||||
import WorkItemCommentLocked from '~/work_items/components/notes/work_item_comment_locked.vue';
|
||||
import WorkItemCommentForm from '~/work_items/components/notes/work_item_comment_form.vue';
|
||||
import createNoteMutation from '~/work_items/graphql/notes/create_work_item_note.mutation.graphql';
|
||||
import { TRACKING_CATEGORY_SHOW } from '~/work_items/constants';
|
||||
import workItemQuery from '~/work_items/graphql/work_item.query.graphql';
|
||||
import workItemNotesQuery from '~/work_items/graphql/notes/work_item_notes.query.graphql';
|
||||
import workItemByIidQuery from '~/work_items/graphql/work_item_by_iid.query.graphql';
|
||||
import {
|
||||
workItemResponseFactory,
|
||||
workItemQueryResponse,
|
||||
projectWorkItemResponse,
|
||||
createWorkItemNoteResponse,
|
||||
} from '../mock_data';
|
||||
mockWorkItemNotesResponse,
|
||||
} from '../../mock_data';
|
||||
|
||||
jest.mock('~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal');
|
||||
jest.mock('~/lib/utils/autosave');
|
||||
|
@ -35,18 +37,7 @@ describe('WorkItemCommentForm', () => {
|
|||
const workItemByIidResponseHandler = jest.fn().mockResolvedValue(projectWorkItemResponse);
|
||||
let workItemResponseHandler;
|
||||
|
||||
const findMarkdownEditor = () => wrapper.findComponent(MarkdownEditor);
|
||||
|
||||
const setText = (newText) => {
|
||||
return findMarkdownEditor().vm.$emit('input', newText);
|
||||
};
|
||||
|
||||
const clickSave = () =>
|
||||
wrapper
|
||||
.findAllComponents(GlButton)
|
||||
.filter((button) => button.text().startsWith('Comment'))
|
||||
.at(0)
|
||||
.vm.$emit('click', {});
|
||||
const findCommentForm = () => wrapper.findComponent(WorkItemCommentForm);
|
||||
|
||||
const createComponent = async ({
|
||||
mutationHandler = mutationSuccessHandler,
|
||||
|
@ -65,13 +56,28 @@ describe('WorkItemCommentForm', () => {
|
|||
window.gon.current_user_avatar_url = 'avatar.png';
|
||||
}
|
||||
|
||||
const { id } = workItemQueryResponse.data.workItem;
|
||||
wrapper = shallowMount(WorkItemCommentForm, {
|
||||
apolloProvider: createMockApollo([
|
||||
const apolloProvider = createMockApollo(
|
||||
[
|
||||
[workItemQuery, workItemResponseHandler],
|
||||
[createNoteMutation, mutationHandler],
|
||||
[workItemByIidQuery, workItemByIidResponseHandler],
|
||||
]),
|
||||
],
|
||||
{},
|
||||
{ ...config.cacheConfig },
|
||||
);
|
||||
|
||||
apolloProvider.clients.defaultClient.writeQuery({
|
||||
query: workItemNotesQuery,
|
||||
variables: {
|
||||
id: workItemId,
|
||||
pageSize: 100,
|
||||
},
|
||||
data: mockWorkItemNotesResponse.data,
|
||||
});
|
||||
|
||||
const { id } = workItemQueryResponse.data.workItem;
|
||||
wrapper = shallowMount(WorkItemAddNote, {
|
||||
apolloProvider,
|
||||
propsData: {
|
||||
workItemId: id,
|
||||
fullPath: 'test-project-path',
|
||||
|
@ -80,7 +86,6 @@ describe('WorkItemCommentForm', () => {
|
|||
workItemType,
|
||||
},
|
||||
stubs: {
|
||||
MarkdownField,
|
||||
WorkItemCommentLocked,
|
||||
},
|
||||
});
|
||||
|
@ -101,9 +106,7 @@ describe('WorkItemCommentForm', () => {
|
|||
signedIn: true,
|
||||
});
|
||||
|
||||
setText(noteText);
|
||||
|
||||
clickSave();
|
||||
findCommentForm().vm.$emit('submitForm', noteText);
|
||||
|
||||
await waitForPromises();
|
||||
|
||||
|
@ -120,9 +123,7 @@ describe('WorkItemCommentForm', () => {
|
|||
await createComponent();
|
||||
const trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
|
||||
|
||||
setText('test');
|
||||
|
||||
clickSave();
|
||||
findCommentForm().vm.$emit('submitForm', 'test');
|
||||
|
||||
await waitForPromises();
|
||||
|
||||
|
@ -133,6 +134,33 @@ describe('WorkItemCommentForm', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('emits `replied` event and hides form after successful mutation', async () => {
|
||||
await createComponent({
|
||||
isEditing: true,
|
||||
signedIn: true,
|
||||
queryVariables: {
|
||||
id: mockWorkItemNotesResponse.data.workItem.id,
|
||||
},
|
||||
});
|
||||
|
||||
findCommentForm().vm.$emit('submitForm', 'some text');
|
||||
await waitForPromises();
|
||||
|
||||
expect(wrapper.emitted('replied')).toEqual([[]]);
|
||||
});
|
||||
|
||||
it('clears a draft after successful mutation', async () => {
|
||||
await createComponent({
|
||||
isEditing: true,
|
||||
signedIn: true,
|
||||
});
|
||||
|
||||
findCommentForm().vm.$emit('submitForm', 'some text');
|
||||
await waitForPromises();
|
||||
|
||||
expect(clearDraft).toHaveBeenCalledWith('gid://gitlab/WorkItem/1-comment');
|
||||
});
|
||||
|
||||
it('emits error when mutation returns error', async () => {
|
||||
const error = 'eror';
|
||||
|
||||
|
@ -160,9 +188,7 @@ describe('WorkItemCommentForm', () => {
|
|||
}),
|
||||
});
|
||||
|
||||
setText('updated desc');
|
||||
|
||||
clickSave();
|
||||
findCommentForm().vm.$emit('submitForm', 'updated desc');
|
||||
|
||||
await waitForPromises();
|
||||
|
||||
|
@ -177,24 +203,12 @@ describe('WorkItemCommentForm', () => {
|
|||
mutationHandler: jest.fn().mockRejectedValue(new Error(error)),
|
||||
});
|
||||
|
||||
setText('updated desc');
|
||||
|
||||
clickSave();
|
||||
findCommentForm().vm.$emit('submitForm', 'updated desc');
|
||||
|
||||
await waitForPromises();
|
||||
|
||||
expect(wrapper.emitted('error')).toEqual([[error]]);
|
||||
});
|
||||
|
||||
it('autosaves', async () => {
|
||||
await createComponent({
|
||||
isEditing: true,
|
||||
});
|
||||
|
||||
setText('updated');
|
||||
|
||||
expect(updateDraft).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
it('calls the global ID work item query when `fetchByIid` prop is false', async () => {
|
|
@ -0,0 +1,164 @@
|
|||
import { shallowMount } from '@vue/test-utils';
|
||||
import { nextTick } from 'vue';
|
||||
import waitForPromises from 'helpers/wait_for_promises';
|
||||
import * as autosave from '~/lib/utils/autosave';
|
||||
import { ESC_KEY, ENTER_KEY } from '~/lib/utils/keys';
|
||||
import * as confirmViaGlModal from '~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal';
|
||||
import WorkItemCommentForm from '~/work_items/components/notes/work_item_comment_form.vue';
|
||||
import MarkdownEditor from '~/vue_shared/components/markdown/markdown_editor.vue';
|
||||
|
||||
const draftComment = 'draft comment';
|
||||
|
||||
jest.mock('~/lib/utils/autosave', () => ({
|
||||
updateDraft: jest.fn(),
|
||||
clearDraft: jest.fn(),
|
||||
getDraft: jest.fn().mockReturnValue(draftComment),
|
||||
}));
|
||||
jest.mock('~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal', () => ({
|
||||
confirmAction: jest.fn().mockResolvedValue(true),
|
||||
}));
|
||||
|
||||
describe('Work item comment form component', () => {
|
||||
let wrapper;
|
||||
|
||||
const mockAutosaveKey = 'test-auto-save-key';
|
||||
|
||||
const findMarkdownEditor = () => wrapper.findComponent(MarkdownEditor);
|
||||
const findCancelButton = () => wrapper.find('[data-testid="cancel-button"]');
|
||||
const findConfirmButton = () => wrapper.find('[data-testid="confirm-button"]');
|
||||
|
||||
const createComponent = ({ isSubmitting = false, initialValue = '' } = {}) => {
|
||||
wrapper = shallowMount(WorkItemCommentForm, {
|
||||
propsData: {
|
||||
workItemType: 'Issue',
|
||||
ariaLabel: 'test-aria-label',
|
||||
autosaveKey: mockAutosaveKey,
|
||||
isSubmitting,
|
||||
initialValue,
|
||||
},
|
||||
provide: {
|
||||
fullPath: 'test-project-path',
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
it('passes correct markdown preview path to markdown editor', () => {
|
||||
createComponent();
|
||||
|
||||
expect(findMarkdownEditor().props('renderMarkdownPath')).toBe(
|
||||
'/test-project-path/preview_markdown?target_type=Issue',
|
||||
);
|
||||
});
|
||||
|
||||
it('passes correct form field props to markdown editor', () => {
|
||||
createComponent();
|
||||
|
||||
expect(findMarkdownEditor().props('formFieldProps')).toEqual({
|
||||
'aria-label': 'test-aria-label',
|
||||
id: 'work-item-add-or-edit-comment',
|
||||
name: 'work-item-add-or-edit-comment',
|
||||
placeholder: 'Write a comment or drag your files here…',
|
||||
});
|
||||
});
|
||||
|
||||
it('passes correct `loading` prop to confirm button', () => {
|
||||
createComponent({ isSubmitting: true });
|
||||
|
||||
expect(findConfirmButton().props('loading')).toBe(true);
|
||||
});
|
||||
|
||||
it('passes a draft from local storage as a value to markdown editor if the draft exists', () => {
|
||||
createComponent({ initialValue: 'parent comment' });
|
||||
expect(findMarkdownEditor().props('value')).toBe(draftComment);
|
||||
});
|
||||
|
||||
it('passes an initialValue prop as a value to markdown editor if storage draft does not exist', () => {
|
||||
jest.spyOn(autosave, 'getDraft').mockImplementation(() => '');
|
||||
createComponent({ initialValue: 'parent comment' });
|
||||
|
||||
expect(findMarkdownEditor().props('value')).toBe('parent comment');
|
||||
});
|
||||
|
||||
it('passes an empty string as a value to markdown editor if storage draft and initialValue are empty', () => {
|
||||
createComponent();
|
||||
|
||||
expect(findMarkdownEditor().props('value')).toBe('');
|
||||
});
|
||||
|
||||
describe('on markdown editor input', () => {
|
||||
beforeEach(() => {
|
||||
createComponent();
|
||||
});
|
||||
|
||||
it('sets correct comment text value', async () => {
|
||||
expect(findMarkdownEditor().props('value')).toBe('');
|
||||
|
||||
findMarkdownEditor().vm.$emit('input', 'new comment');
|
||||
await nextTick();
|
||||
|
||||
expect(findMarkdownEditor().props('value')).toBe('new comment');
|
||||
});
|
||||
|
||||
it('calls `updateDraft` with correct parameters', async () => {
|
||||
findMarkdownEditor().vm.$emit('input', 'new comment');
|
||||
|
||||
expect(autosave.updateDraft).toHaveBeenCalledWith(mockAutosaveKey, 'new comment');
|
||||
});
|
||||
});
|
||||
|
||||
describe('on cancel editing', () => {
|
||||
beforeEach(() => {
|
||||
jest.spyOn(autosave, 'getDraft').mockImplementation(() => draftComment);
|
||||
createComponent();
|
||||
findMarkdownEditor().vm.$emit('keydown', new KeyboardEvent('keydown', { key: ESC_KEY }));
|
||||
|
||||
return waitForPromises();
|
||||
});
|
||||
|
||||
it('confirms a user action if comment text is not empty', () => {
|
||||
expect(confirmViaGlModal.confirmAction).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('emits `cancelEditing` and clears draft from the local storage', () => {
|
||||
expect(wrapper.emitted('cancelEditing')).toHaveLength(1);
|
||||
expect(autosave.clearDraft).toHaveBeenCalledWith(mockAutosaveKey);
|
||||
});
|
||||
});
|
||||
|
||||
it('cancels editing on clicking cancel button', async () => {
|
||||
createComponent();
|
||||
findCancelButton().vm.$emit('click');
|
||||
|
||||
await waitForPromises();
|
||||
|
||||
expect(wrapper.emitted('cancelEditing')).toHaveLength(1);
|
||||
expect(autosave.clearDraft).toHaveBeenCalledWith(mockAutosaveKey);
|
||||
});
|
||||
|
||||
it('emits `submitForm` event on confirm button click', () => {
|
||||
createComponent();
|
||||
findConfirmButton().vm.$emit('click');
|
||||
|
||||
expect(wrapper.emitted('submitForm')).toEqual([[draftComment]]);
|
||||
});
|
||||
|
||||
it('emits `submitForm` event on pressing enter with meta key on markdown editor', () => {
|
||||
createComponent();
|
||||
findMarkdownEditor().vm.$emit(
|
||||
'keydown',
|
||||
new KeyboardEvent('keydown', { key: ENTER_KEY, metaKey: true }),
|
||||
);
|
||||
|
||||
expect(wrapper.emitted('submitForm')).toEqual([[draftComment]]);
|
||||
});
|
||||
|
||||
it('emits `submitForm` event on pressing ctrl+enter on markdown editor', () => {
|
||||
createComponent();
|
||||
findMarkdownEditor().vm.$emit(
|
||||
'keydown',
|
||||
new KeyboardEvent('keydown', { key: ENTER_KEY, ctrlKey: true }),
|
||||
);
|
||||
|
||||
expect(wrapper.emitted('submitForm')).toEqual([[draftComment]]);
|
||||
});
|
||||
});
|
|
@ -1,6 +1,6 @@
|
|||
import { GlLink, GlIcon } from '@gitlab/ui';
|
||||
import { shallowMount } from '@vue/test-utils';
|
||||
import WorkItemCommentLocked from '~/work_items/components/work_item_comment_locked.vue';
|
||||
import WorkItemCommentLocked from '~/work_items/components/notes/work_item_comment_locked.vue';
|
||||
|
||||
const createComponent = ({ workItemType = 'Task', isProjectArchived = false } = {}) =>
|
||||
shallowMount(WorkItemCommentLocked, {
|
|
@ -6,7 +6,7 @@ import ToggleRepliesWidget from '~/notes/components/toggle_replies_widget.vue';
|
|||
import WorkItemDiscussion from '~/work_items/components/notes/work_item_discussion.vue';
|
||||
import WorkItemNote from '~/work_items/components/notes/work_item_note.vue';
|
||||
import WorkItemNoteReplying from '~/work_items/components/notes/work_item_note_replying.vue';
|
||||
import WorkItemCommentForm from '~/work_items/components/work_item_comment_form.vue';
|
||||
import WorkItemAddNote from '~/work_items/components/notes/work_item_add_note.vue';
|
||||
import {
|
||||
mockWorkItemCommentNote,
|
||||
mockWorkItemNotesResponseWithComments,
|
||||
|
@ -27,7 +27,7 @@ describe('Work Item Discussion', () => {
|
|||
const findToggleRepliesWidget = () => wrapper.findComponent(ToggleRepliesWidget);
|
||||
const findAllThreads = () => wrapper.findAllComponents(WorkItemNote);
|
||||
const findThreadAtIndex = (index) => findAllThreads().at(index);
|
||||
const findWorkItemCommentForm = () => wrapper.findComponent(WorkItemCommentForm);
|
||||
const findWorkItemAddNote = () => wrapper.findComponent(WorkItemAddNote);
|
||||
const findWorkItemNoteReplying = () => wrapper.findComponent(WorkItemNoteReplying);
|
||||
|
||||
const createComponent = ({
|
||||
|
@ -73,7 +73,7 @@ describe('Work Item Discussion', () => {
|
|||
});
|
||||
|
||||
it('should not show the comment form by default', () => {
|
||||
expect(findWorkItemCommentForm().exists()).toBe(false);
|
||||
expect(findWorkItemAddNote().exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -101,8 +101,8 @@ describe('Work Item Discussion', () => {
|
|||
|
||||
mainComment.vm.$emit('startReplying');
|
||||
await nextTick();
|
||||
expect(findWorkItemCommentForm().exists()).toBe(true);
|
||||
expect(findWorkItemCommentForm().props('autofocus')).toBe(true);
|
||||
expect(findWorkItemAddNote().exists()).toBe(true);
|
||||
expect(findWorkItemAddNote().props('autofocus')).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -115,7 +115,7 @@ describe('Work Item Discussion', () => {
|
|||
|
||||
mainComment.vm.$emit('startReplying');
|
||||
await nextTick();
|
||||
await findWorkItemCommentForm().vm.$emit('replying', 'reply text');
|
||||
await findWorkItemAddNote().vm.$emit('replying', 'reply text');
|
||||
});
|
||||
|
||||
it('should show optimistic behavior when replying', async () => {
|
||||
|
@ -124,7 +124,7 @@ describe('Work Item Discussion', () => {
|
|||
});
|
||||
|
||||
it('should be expanded when the reply is successful', async () => {
|
||||
findWorkItemCommentForm().vm.$emit('replied');
|
||||
findWorkItemAddNote().vm.$emit('replied');
|
||||
await nextTick();
|
||||
expect(findToggleRepliesWidget().exists()).toBe(true);
|
||||
expect(findToggleRepliesWidget().props('collapsed')).toBe(false);
|
||||
|
@ -137,4 +137,13 @@ describe('Work Item Discussion', () => {
|
|||
|
||||
expect(wrapper.emitted('deleteNote')).toEqual([[mockWorkItemCommentNote]]);
|
||||
});
|
||||
|
||||
it('emits `error` event when child note emits an `error`', () => {
|
||||
const mockErrorText = 'Houston, we have a problem';
|
||||
|
||||
createComponent();
|
||||
findThreadAtIndex(0).vm.$emit('error', mockErrorText);
|
||||
|
||||
expect(wrapper.emitted('error')).toEqual([[mockErrorText]]);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -6,11 +6,13 @@ describe('Work Item Note Actions', () => {
|
|||
let wrapper;
|
||||
|
||||
const findReplyButton = () => wrapper.findComponent(ReplyButton);
|
||||
const findEditButton = () => wrapper.find('[data-testid="edit-work-item-note"]');
|
||||
|
||||
const createComponent = ({ showReply = true } = {}) => {
|
||||
const createComponent = ({ showReply = true, showEdit = true } = {}) => {
|
||||
wrapper = shallowMount(WorkItemNoteActions, {
|
||||
propsData: {
|
||||
showReply,
|
||||
showEdit,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
@ -28,4 +30,23 @@ describe('Work Item Note Actions', () => {
|
|||
expect(findReplyButton().exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
it('shows edit button when `showEdit` prop is true', () => {
|
||||
createComponent();
|
||||
|
||||
expect(findEditButton().exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('does not show edit button when `showEdit` prop is false', () => {
|
||||
createComponent({ showEdit: false });
|
||||
|
||||
expect(findEditButton().exists()).toBe(false);
|
||||
});
|
||||
|
||||
it('emits `startEditing` event when edit button is clicked', () => {
|
||||
createComponent();
|
||||
findEditButton().vm.$emit('click');
|
||||
|
||||
expect(wrapper.emitted('startEditing')).toEqual([[]]);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,14 +1,41 @@
|
|||
import { GlAvatarLink, GlDropdown } from '@gitlab/ui';
|
||||
import { shallowMount } from '@vue/test-utils';
|
||||
import Vue, { nextTick } from 'vue';
|
||||
import VueApollo from 'vue-apollo';
|
||||
import mockApollo from 'helpers/mock_apollo_helper';
|
||||
import waitForPromises from 'helpers/wait_for_promises';
|
||||
import { updateDraft } from '~/lib/utils/autosave';
|
||||
import EditedAt from '~/issues/show/components/edited.vue';
|
||||
import WorkItemNote from '~/work_items/components/notes/work_item_note.vue';
|
||||
import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue';
|
||||
import NoteBody from '~/work_items/components/notes/work_item_note_body.vue';
|
||||
import NoteHeader from '~/notes/components/note_header.vue';
|
||||
import NoteActions from '~/work_items/components/notes/work_item_note_actions.vue';
|
||||
import WorkItemCommentForm from '~/work_items/components/notes/work_item_comment_form.vue';
|
||||
import updateWorkItemNoteMutation from '~/work_items/graphql/notes/update_work_item_note.mutation.graphql';
|
||||
import { mockWorkItemCommentNote } from 'jest/work_items/mock_data';
|
||||
|
||||
Vue.use(VueApollo);
|
||||
jest.mock('~/lib/utils/autosave');
|
||||
|
||||
describe('Work Item Note', () => {
|
||||
let wrapper;
|
||||
const updatedNoteText = '# Some title';
|
||||
const updatedNoteBody = '<h1 data-sourcepos="1:1-1:12" dir="auto">Some title</h1>';
|
||||
|
||||
const successHandler = jest.fn().mockResolvedValue({
|
||||
data: {
|
||||
updateNote: {
|
||||
errors: [],
|
||||
note: {
|
||||
...mockWorkItemCommentNote,
|
||||
body: updatedNoteText,
|
||||
bodyHtml: updatedNoteBody,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
const errorHandler = jest.fn().mockRejectedValue('Oops');
|
||||
|
||||
const findAuthorAvatarLink = () => wrapper.findComponent(GlAvatarLink);
|
||||
const findTimelineEntryItem = () => wrapper.findComponent(TimelineEntryItem);
|
||||
|
@ -16,60 +43,192 @@ describe('Work Item Note', () => {
|
|||
const findNoteBody = () => wrapper.findComponent(NoteBody);
|
||||
const findNoteActions = () => wrapper.findComponent(NoteActions);
|
||||
const findDropdown = () => wrapper.findComponent(GlDropdown);
|
||||
const findDeleteNoteButton = () => wrapper.find('[data-testid="delete-note-action"]');
|
||||
const findCommentForm = () => wrapper.findComponent(WorkItemCommentForm);
|
||||
const findEditedAt = () => wrapper.findComponent(EditedAt);
|
||||
|
||||
const createComponent = ({ note = mockWorkItemCommentNote, isFirstNote = false } = {}) => {
|
||||
const findDeleteNoteButton = () => wrapper.find('[data-testid="delete-note-action"]');
|
||||
const findNoteWrapper = () => wrapper.find('[data-testid="note-wrapper"]');
|
||||
|
||||
const createComponent = ({
|
||||
note = mockWorkItemCommentNote,
|
||||
isFirstNote = false,
|
||||
updateNoteMutationHandler = successHandler,
|
||||
} = {}) => {
|
||||
wrapper = shallowMount(WorkItemNote, {
|
||||
propsData: {
|
||||
note,
|
||||
isFirstNote,
|
||||
workItemType: 'Task',
|
||||
},
|
||||
apolloProvider: mockApollo([[updateWorkItemNoteMutation, updateNoteMutationHandler]]),
|
||||
});
|
||||
};
|
||||
|
||||
describe('Main comment', () => {
|
||||
describe('when editing', () => {
|
||||
beforeEach(() => {
|
||||
createComponent();
|
||||
findNoteActions().vm.$emit('startEditing');
|
||||
return nextTick();
|
||||
});
|
||||
|
||||
it('should render a comment form', () => {
|
||||
expect(findCommentForm().exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('should not render note wrapper', () => {
|
||||
expect(findNoteWrapper().exists()).toBe(false);
|
||||
});
|
||||
|
||||
it('updates saved draft with current note text', () => {
|
||||
expect(updateDraft).toHaveBeenCalledWith(
|
||||
`${mockWorkItemCommentNote.id}-comment`,
|
||||
mockWorkItemCommentNote.body,
|
||||
);
|
||||
});
|
||||
|
||||
it('passes correct autosave key prop to comment form component', () => {
|
||||
expect(findCommentForm().props('autosaveKey')).toBe(`${mockWorkItemCommentNote.id}-comment`);
|
||||
});
|
||||
|
||||
it('should hide a form and show wrapper when user cancels editing', async () => {
|
||||
findCommentForm().vm.$emit('cancelEditing');
|
||||
await nextTick();
|
||||
|
||||
expect(findCommentForm().exists()).toBe(false);
|
||||
expect(findNoteWrapper().exists()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when submitting a form to edit a note', () => {
|
||||
it('calls update mutation with correct variables', async () => {
|
||||
createComponent();
|
||||
findNoteActions().vm.$emit('startEditing');
|
||||
await nextTick();
|
||||
|
||||
findCommentForm().vm.$emit('submitForm', updatedNoteText);
|
||||
|
||||
expect(successHandler).toHaveBeenCalledWith({
|
||||
input: {
|
||||
id: mockWorkItemCommentNote.id,
|
||||
body: updatedNoteText,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('hides the form after succesful mutation', async () => {
|
||||
createComponent();
|
||||
findNoteActions().vm.$emit('startEditing');
|
||||
await nextTick();
|
||||
|
||||
findCommentForm().vm.$emit('submitForm', updatedNoteText);
|
||||
await waitForPromises();
|
||||
|
||||
expect(findCommentForm().exists()).toBe(false);
|
||||
});
|
||||
|
||||
describe('when mutation fails', () => {
|
||||
beforeEach(async () => {
|
||||
createComponent({ updateNoteMutationHandler: errorHandler });
|
||||
findNoteActions().vm.$emit('startEditing');
|
||||
await nextTick();
|
||||
|
||||
findCommentForm().vm.$emit('submitForm', updatedNoteText);
|
||||
await waitForPromises();
|
||||
});
|
||||
|
||||
it('opens the form again', () => {
|
||||
expect(findCommentForm().exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('updates the saved draft with the latest comment text', () => {
|
||||
expect(updateDraft).toHaveBeenCalledWith(
|
||||
`${mockWorkItemCommentNote.id}-comment`,
|
||||
updatedNoteText,
|
||||
);
|
||||
});
|
||||
|
||||
it('emits an error', () => {
|
||||
expect(wrapper.emitted('error')).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when not editing', () => {
|
||||
it('should not render a comment form', () => {
|
||||
createComponent();
|
||||
expect(findCommentForm().exists()).toBe(false);
|
||||
});
|
||||
|
||||
it('should render note wrapper', () => {
|
||||
createComponent();
|
||||
expect(findNoteWrapper().exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('renders no "edited at" information by default', () => {
|
||||
createComponent();
|
||||
expect(findEditedAt().exists()).toBe(false);
|
||||
});
|
||||
|
||||
it('renders "edited at" information if the note was edited', () => {
|
||||
createComponent({
|
||||
note: {
|
||||
...mockWorkItemCommentNote,
|
||||
lastEditedAt: '2023-02-12T07:47:40Z',
|
||||
lastEditedBy: { ...mockWorkItemCommentNote.author, webPath: 'test-path' },
|
||||
},
|
||||
});
|
||||
|
||||
expect(findEditedAt().exists()).toBe(true);
|
||||
expect(findEditedAt().props()).toEqual({
|
||||
updatedAt: '2023-02-12T07:47:40Z',
|
||||
updatedByName: 'Administrator',
|
||||
updatedByPath: 'test-path',
|
||||
});
|
||||
});
|
||||
|
||||
describe('main comment', () => {
|
||||
beforeEach(() => {
|
||||
createComponent({ isFirstNote: true });
|
||||
});
|
||||
|
||||
it('Should have the note header, actions and body', () => {
|
||||
it('should have the note header, actions and body', () => {
|
||||
expect(findTimelineEntryItem().exists()).toBe(true);
|
||||
expect(findNoteHeader().exists()).toBe(true);
|
||||
expect(findNoteBody().exists()).toBe(true);
|
||||
expect(findNoteActions().exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('Should not have the Avatar link for main thread inside the timeline-entry', () => {
|
||||
it('should not have the Avatar link for main thread inside the timeline-entry', () => {
|
||||
expect(findAuthorAvatarLink().exists()).toBe(false);
|
||||
});
|
||||
|
||||
it('Should have the reply button props', () => {
|
||||
it('should have the reply button props', () => {
|
||||
expect(findNoteActions().props('showReply')).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Comment threads', () => {
|
||||
describe('comment threads', () => {
|
||||
beforeEach(() => {
|
||||
createComponent();
|
||||
});
|
||||
|
||||
it('Should have the note header, actions and body', () => {
|
||||
it('should have the note header, actions and body', () => {
|
||||
expect(findTimelineEntryItem().exists()).toBe(true);
|
||||
expect(findNoteHeader().exists()).toBe(true);
|
||||
expect(findNoteBody().exists()).toBe(true);
|
||||
expect(findNoteActions().exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('Should have the Avatar link for comment threads', () => {
|
||||
it('should have the Avatar link for comment threads', () => {
|
||||
expect(findAuthorAvatarLink().exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('Should not have the reply button props', () => {
|
||||
it('should not have the reply button props', () => {
|
||||
expect(findNoteActions().props('showReply')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
it('should display a dropdown if user has a permission to delete note', () => {
|
||||
it('should display a dropdown if user has a permission to delete a note', () => {
|
||||
createComponent({
|
||||
note: {
|
||||
...mockWorkItemCommentNote,
|
||||
|
@ -80,7 +239,7 @@ describe('Work Item Note', () => {
|
|||
expect(findDropdown().exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('should not display a dropdown if user has no permission to delete note', () => {
|
||||
it('should not display a dropdown if user has no permission to delete a note', () => {
|
||||
createComponent();
|
||||
|
||||
expect(findDropdown().exists()).toBe(false);
|
||||
|
@ -98,4 +257,5 @@ describe('Work Item Note', () => {
|
|||
|
||||
expect(wrapper.emitted('deleteNote')).toEqual([[]]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -8,7 +8,7 @@ import waitForPromises from 'helpers/wait_for_promises';
|
|||
import SystemNote from '~/work_items/components/notes/system_note.vue';
|
||||
import WorkItemNotes from '~/work_items/components/work_item_notes.vue';
|
||||
import WorkItemDiscussion from '~/work_items/components/notes/work_item_discussion.vue';
|
||||
import WorkItemCommentForm from '~/work_items/components/work_item_comment_form.vue';
|
||||
import WorkItemAddNote from '~/work_items/components/notes/work_item_add_note.vue';
|
||||
import ActivityFilter from '~/work_items/components/notes/activity_filter.vue';
|
||||
import workItemNotesQuery from '~/work_items/graphql/notes/work_item_notes.query.graphql';
|
||||
import workItemNotesByIidQuery from '~/work_items/graphql/notes/work_item_notes_by_iid.query.graphql';
|
||||
|
@ -54,7 +54,7 @@ describe('WorkItemNotes component', () => {
|
|||
const findAllSystemNotes = () => wrapper.findAllComponents(SystemNote);
|
||||
const findAllListItems = () => wrapper.findAll('ul.timeline > *');
|
||||
const findActivityLabel = () => wrapper.find('label');
|
||||
const findWorkItemCommentForm = () => wrapper.findComponent(WorkItemCommentForm);
|
||||
const findWorkItemAddNote = () => wrapper.findComponent(WorkItemAddNote);
|
||||
const findSkeletonLoader = () => wrapper.findComponent(GlSkeletonLoader);
|
||||
const findSortingFilter = () => wrapper.findComponent(ActivityFilter);
|
||||
const findSystemNoteAtIndex = (index) => findAllSystemNotes().at(index);
|
||||
|
@ -123,7 +123,7 @@ describe('WorkItemNotes component', () => {
|
|||
});
|
||||
await waitForPromises();
|
||||
|
||||
expect(findWorkItemCommentForm().props('fetchByIid')).toEqual(false);
|
||||
expect(findWorkItemAddNote().props('fetchByIid')).toEqual(false);
|
||||
});
|
||||
|
||||
describe('when notes are loading', () => {
|
||||
|
@ -161,7 +161,7 @@ describe('WorkItemNotes component', () => {
|
|||
});
|
||||
|
||||
it('passes correct props to comment form component', () => {
|
||||
expect(findWorkItemCommentForm().props('fetchByIid')).toEqual(true);
|
||||
expect(findWorkItemAddNote().props('fetchByIid')).toEqual(true);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -218,13 +218,13 @@ describe('WorkItemNotes component', () => {
|
|||
it('puts form at start of list in when sorting by newest first', async () => {
|
||||
await findSortingFilter().vm.$emit('changeSortOrder', DESC);
|
||||
|
||||
expect(findAllListItems().at(0).is(WorkItemCommentForm)).toEqual(true);
|
||||
expect(findAllListItems().at(0).is(WorkItemAddNote)).toEqual(true);
|
||||
});
|
||||
|
||||
it('puts form at end of list in when sorting by oldest first', async () => {
|
||||
await findSortingFilter().vm.$emit('changeSortOrder', ASC);
|
||||
|
||||
expect(findAllListItems().at(-1).is(WorkItemCommentForm)).toEqual(true);
|
||||
expect(findAllListItems().at(-1).is(WorkItemAddNote)).toEqual(true);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -1631,7 +1631,7 @@ export const projectWorkItemResponse = {
|
|||
export const mockWorkItemNotesResponse = {
|
||||
data: {
|
||||
workItem: {
|
||||
id: 'gid://gitlab/WorkItem/600',
|
||||
id: 'gid://gitlab/WorkItem/1',
|
||||
iid: '60',
|
||||
widgets: [
|
||||
{
|
||||
|
@ -1675,10 +1675,13 @@ export const mockWorkItemNotesResponse = {
|
|||
nodes: [
|
||||
{
|
||||
id: 'gid://gitlab/Note/2428',
|
||||
body: 'added #31 as parent issue',
|
||||
bodyHtml:
|
||||
'<p data-sourcepos="1:1-1:25" dir="auto">added <a href="/flightjs/Flight/-/issues/31" data-reference-type="issue" data-original="#31" data-link="false" data-link-reference="false" data-project="6" data-issue="224" data-project-path="flightjs/Flight" data-iid="31" data-issue-type="issue" data-container=body data-placement="top" title="Perferendis est quae totam quia laborum tempore ut voluptatem." class="gfm gfm-issue">#31</a> as parent issue</p>',
|
||||
systemNoteIconName: 'link',
|
||||
createdAt: '2022-11-14T04:18:59Z',
|
||||
lastEditedAt: null,
|
||||
lastEditedBy: null,
|
||||
system: true,
|
||||
internal: false,
|
||||
discussion: {
|
||||
|
@ -1715,10 +1718,13 @@ export const mockWorkItemNotesResponse = {
|
|||
nodes: [
|
||||
{
|
||||
id: 'gid://gitlab/MilestoneNote/0f2f195ec0d1ef95ee9d5b10446b8e96a7d83864',
|
||||
body: 'changed milestone to %v4.0',
|
||||
bodyHtml:
|
||||
'<p data-sourcepos="1:1-1:23" dir="auto">changed milestone to <a href="/flightjs/Flight/-/milestones/5" data-reference-type="milestone" data-original="%5" data-link="false" data-link-reference="false" data-project="6" data-milestone="30" data-container=body data-placement="top" title="" class="gfm gfm-milestone has-tooltip">%v4.0</a></p>',
|
||||
systemNoteIconName: 'clock',
|
||||
createdAt: '2022-11-14T04:18:59Z',
|
||||
lastEditedAt: null,
|
||||
lastEditedBy: null,
|
||||
system: true,
|
||||
internal: false,
|
||||
discussion: {
|
||||
|
@ -1755,9 +1761,12 @@ export const mockWorkItemNotesResponse = {
|
|||
nodes: [
|
||||
{
|
||||
id: 'gid://gitlab/WeightNote/0f2f195ec0d1ef95ee9d5b10446b8e96a9883864',
|
||||
body: 'changed weight to **89**',
|
||||
bodyHtml: '<p dir="auto">changed weight to <strong>89</strong></p>',
|
||||
systemNoteIconName: 'weight',
|
||||
createdAt: '2022-11-25T07:16:20Z',
|
||||
lastEditedAt: null,
|
||||
lastEditedBy: null,
|
||||
system: true,
|
||||
internal: false,
|
||||
discussion: {
|
||||
|
@ -1853,10 +1862,13 @@ export const mockWorkItemNotesByIidResponse = {
|
|||
nodes: [
|
||||
{
|
||||
id: 'gid://gitlab/Note/2428',
|
||||
body: 'added as parent issue',
|
||||
bodyHtml:
|
||||
'\u003cp data-sourcepos="1:1-1:25" dir="auto"\u003eadded \u003ca href="/flightjs/Flight/-/issues/31" data-reference-type="issue" data-original="#31" data-link="false" data-link-reference="false" data-project="6" data-issue="224" data-project-path="flightjs/Flight" data-iid="31" data-issue-type="issue" data-container="body" data-placement="top" title="Perferendis est quae totam quia laborum tempore ut voluptatem." class="gfm gfm-issue"\u003e#31\u003c/a\u003e as parent issue\u003c/p\u003e',
|
||||
systemNoteIconName: 'link',
|
||||
createdAt: '2022-11-14T04:18:59Z',
|
||||
lastEditedAt: null,
|
||||
lastEditedBy: null,
|
||||
system: true,
|
||||
internal: false,
|
||||
discussion: {
|
||||
|
@ -1895,10 +1907,13 @@ export const mockWorkItemNotesByIidResponse = {
|
|||
{
|
||||
id:
|
||||
'gid://gitlab/MilestoneNote/7b08b89a728a5ceb7de8334246837ba1d07270dc',
|
||||
body: 'changed milestone to %v4.0',
|
||||
bodyHtml:
|
||||
'\u003cp data-sourcepos="1:1-1:23" dir="auto"\u003echanged milestone to \u003ca href="/flightjs/Flight/-/milestones/5" data-reference-type="milestone" data-original="%5" data-link="false" data-link-reference="false" data-project="6" data-milestone="30" data-container="body" data-placement="top" title="" class="gfm gfm-milestone has-tooltip"\u003e%v4.0\u003c/a\u003e\u003c/p\u003e',
|
||||
systemNoteIconName: 'clock',
|
||||
createdAt: '2022-11-14T04:18:59Z',
|
||||
lastEditedAt: null,
|
||||
lastEditedBy: null,
|
||||
system: true,
|
||||
internal: false,
|
||||
discussion: {
|
||||
|
@ -1937,10 +1952,14 @@ export const mockWorkItemNotesByIidResponse = {
|
|||
{
|
||||
id:
|
||||
'gid://gitlab/IterationNote/addbc177f7664699a135130ab05ffb78c57e4db3',
|
||||
body:
|
||||
'changed iteration to Et autem debitis nam suscipit eos ut. Jul 13, 2022 - Jul 19, 2022',
|
||||
bodyHtml:
|
||||
'\u003cp data-sourcepos="1:1-1:36" dir="auto"\u003echanged iteration to \u003ca href="/groups/flightjs/-/iterations/5352" data-reference-type="iteration" data-original="*iteration:5352" data-link="false" data-link-reference="false" data-project="6" data-iteration="5352" data-container="body" data-placement="top" title="Iteration" class="gfm gfm-iteration has-tooltip"\u003eEt autem debitis nam suscipit eos ut. Jul 13, 2022 - Jul 19, 2022\u003c/a\u003e\u003c/p\u003e',
|
||||
systemNoteIconName: 'iteration',
|
||||
createdAt: '2022-11-14T04:19:00Z',
|
||||
lastEditedAt: null,
|
||||
lastEditedBy: null,
|
||||
system: true,
|
||||
internal: false,
|
||||
discussion: {
|
||||
|
@ -2034,10 +2053,13 @@ export const mockMoreWorkItemNotesResponse = {
|
|||
nodes: [
|
||||
{
|
||||
id: 'gid://gitlab/Note/2428',
|
||||
body: 'added #31 as parent issue',
|
||||
bodyHtml:
|
||||
'<p data-sourcepos="1:1-1:25" dir="auto">added <a href="/flightjs/Flight/-/issues/31" data-reference-type="issue" data-original="#31" data-link="false" data-link-reference="false" data-project="6" data-issue="224" data-project-path="flightjs/Flight" data-iid="31" data-issue-type="issue" data-container=body data-placement="top" title="Perferendis est quae totam quia laborum tempore ut voluptatem." class="gfm gfm-issue">#31</a> as parent issue</p>',
|
||||
systemNoteIconName: 'link',
|
||||
createdAt: '2022-11-14T04:18:59Z',
|
||||
lastEditedAt: null,
|
||||
lastEditedBy: null,
|
||||
system: true,
|
||||
internal: false,
|
||||
discussion: {
|
||||
|
@ -2074,10 +2096,13 @@ export const mockMoreWorkItemNotesResponse = {
|
|||
nodes: [
|
||||
{
|
||||
id: 'gid://gitlab/MilestoneNote/0f2f195ec0d1ef95ee9d5b10446b8e96a7d83823',
|
||||
body: 'changed milestone to %v4.0',
|
||||
bodyHtml:
|
||||
'<p data-sourcepos="1:1-1:23" dir="auto">changed milestone to <a href="/flightjs/Flight/-/milestones/5" data-reference-type="milestone" data-original="%5" data-link="false" data-link-reference="false" data-project="6" data-milestone="30" data-container=body data-placement="top" title="" class="gfm gfm-milestone has-tooltip">%v4.0</a></p>',
|
||||
systemNoteIconName: 'clock',
|
||||
createdAt: '2022-11-14T04:18:59Z',
|
||||
lastEditedAt: null,
|
||||
lastEditedBy: null,
|
||||
system: true,
|
||||
internal: false,
|
||||
discussion: {
|
||||
|
@ -2114,9 +2139,12 @@ export const mockMoreWorkItemNotesResponse = {
|
|||
nodes: [
|
||||
{
|
||||
id: 'gid://gitlab/WeightNote/0f2f195ec0d1ef95ee9d5b10446b8e96a7d83864',
|
||||
body: 'changed weight to **89**',
|
||||
bodyHtml: '<p dir="auto">changed weight to <strong>89</strong></p>',
|
||||
systemNoteIconName: 'weight',
|
||||
createdAt: '2022-11-25T07:16:20Z',
|
||||
lastEditedAt: null,
|
||||
lastEditedBy: null,
|
||||
system: true,
|
||||
internal: false,
|
||||
discussion: {
|
||||
|
@ -2163,17 +2191,21 @@ export const createWorkItemNoteResponse = {
|
|||
createNote: {
|
||||
errors: [],
|
||||
note: {
|
||||
id: 'gid://gitlab/Note/569',
|
||||
discussion: {
|
||||
id: 'gid://gitlab/Discussion/c872ba2d7d3eb780d2255138d67ca8b04f65b122',
|
||||
notes: {
|
||||
nodes: [
|
||||
{
|
||||
id: 'gid://gitlab/Note/569',
|
||||
body: 'Main comment',
|
||||
bodyHtml: '<p data-sourcepos="1:1-1:9" dir="auto">Main comment</p>',
|
||||
system: false,
|
||||
internal: false,
|
||||
systemNoteIconName: null,
|
||||
createdAt: '2023-01-25T04:49:46Z',
|
||||
lastEditedAt: null,
|
||||
lastEditedBy: null,
|
||||
discussion: {
|
||||
id: 'gid://gitlab/Discussion/c872ba2d7d3eb780d2255138d67ca8b04f65b122',
|
||||
__typename: 'Discussion',
|
||||
|
@ -2214,10 +2246,13 @@ export const createWorkItemNoteResponse = {
|
|||
|
||||
export const mockWorkItemCommentNote = {
|
||||
id: 'gid://gitlab/Note/158',
|
||||
body: 'How are you ? what do you think about this ?',
|
||||
bodyHtml:
|
||||
'<p data-sourcepos="1:1-1:76" dir="auto"><gl-emoji title="waving hand sign" data-name="wave" data-unicode-version="6.0">👋</gl-emoji> Hi <a href="/fredda.brekke" data-reference-type="user" data-user="3" data-container="body" data-placement="top" class="gfm gfm-project_member js-user-link" title="Sherie Nitzsche">@fredda.brekke</a> How are you ? what do you think about this ? <gl-emoji title="person with folded hands" data-name="pray" data-unicode-version="6.0">🙏</gl-emoji></p>',
|
||||
systemNoteIconName: false,
|
||||
createdAt: '2022-11-25T07:16:20Z',
|
||||
lastEditedAt: null,
|
||||
lastEditedBy: null,
|
||||
system: false,
|
||||
internal: false,
|
||||
discussion: {
|
||||
|
@ -2289,11 +2324,14 @@ export const mockWorkItemNotesResponseWithComments = {
|
|||
nodes: [
|
||||
{
|
||||
id: 'gid://gitlab/DiscussionNote/174',
|
||||
body: 'Separate thread',
|
||||
bodyHtml: '<p data-sourcepos="1:1-1:15" dir="auto">Separate thread</p>',
|
||||
system: false,
|
||||
internal: false,
|
||||
systemNoteIconName: null,
|
||||
createdAt: '2023-01-12T07:47:40Z',
|
||||
lastEditedAt: null,
|
||||
lastEditedBy: null,
|
||||
discussion: {
|
||||
id: 'gid://gitlab/Discussion/2bb1162fd0d39297d1a68fdd7d4083d3780af0f3',
|
||||
__typename: 'Discussion',
|
||||
|
@ -2320,11 +2358,14 @@ export const mockWorkItemNotesResponseWithComments = {
|
|||
},
|
||||
{
|
||||
id: 'gid://gitlab/DiscussionNote/235',
|
||||
body: 'Thread comment',
|
||||
bodyHtml: '<p data-sourcepos="1:1-1:15" dir="auto">Thread comment</p>',
|
||||
system: false,
|
||||
internal: false,
|
||||
systemNoteIconName: null,
|
||||
createdAt: '2023-01-18T09:09:54Z',
|
||||
lastEditedAt: null,
|
||||
lastEditedBy: null,
|
||||
discussion: {
|
||||
id: 'gid://gitlab/Discussion/2bb1162fd0d39297d1a68fdd7d4083d3780af0f3',
|
||||
__typename: 'Discussion',
|
||||
|
@ -2360,9 +2401,12 @@ export const mockWorkItemNotesResponseWithComments = {
|
|||
nodes: [
|
||||
{
|
||||
id: 'gid://gitlab/WeightNote/0f2f195ec0d1ef95ee9d5b10446b8e96a9883864',
|
||||
body: 'Main thread 2',
|
||||
bodyHtml: '<p data-sourcepos="1:1-1:15" dir="auto">Main thread 2</p>',
|
||||
systemNoteIconName: 'weight',
|
||||
createdAt: '2022-11-25T07:16:20Z',
|
||||
lastEditedAt: null,
|
||||
lastEditedBy: null,
|
||||
system: false,
|
||||
internal: false,
|
||||
discussion: {
|
||||
|
|
|
@ -91,34 +91,6 @@ RSpec.describe Environment, :use_clean_rails_memory_store_caching, feature_categ
|
|||
end
|
||||
end
|
||||
|
||||
describe 'preloading deployment associations' do
|
||||
let!(:environment) { create(:environment, project: project) }
|
||||
|
||||
associations = [:last_deployment, :last_visible_deployment, :upcoming_deployment]
|
||||
associations.concat Deployment::FINISHED_STATUSES.map { |status| "last_#{status}_deployment".to_sym }
|
||||
associations.concat Deployment::UPCOMING_STATUSES.map { |status| "last_#{status}_deployment".to_sym }
|
||||
|
||||
context 'raises error for legacy approach' do
|
||||
let!(:error_pattern) { /Preloading instance dependent scopes is not supported/ }
|
||||
|
||||
subject { described_class.preload(association_name).find_by(id: environment) }
|
||||
|
||||
shared_examples 'raises error' do
|
||||
it do
|
||||
expect { subject }.to raise_error(error_pattern)
|
||||
end
|
||||
end
|
||||
|
||||
associations.each do |association|
|
||||
context association.to_s do
|
||||
let!(:association_name) { association }
|
||||
|
||||
include_examples "raises error"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'validate and sanitize external url' do
|
||||
let_it_be_with_refind(:environment) { create(:environment) }
|
||||
|
||||
|
|
Loading…
Reference in New Issue