Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2024-09-06 18:10:13 +00:00
parent fad8f90d7a
commit 23985334ba
66 changed files with 893 additions and 374 deletions

View File

@ -240,7 +240,6 @@
"WorkItemWidgetDefinition": [
"WorkItemWidgetDefinitionAssignees",
"WorkItemWidgetDefinitionGeneric",
"WorkItemWidgetDefinitionHealthStatus",
"WorkItemWidgetDefinitionHierarchy",
"WorkItemWidgetDefinitionLabels",
"WorkItemWidgetDefinitionWeight"

View File

@ -1,6 +1,6 @@
#import "~/graphql_shared/fragments/author.fragment.graphql"
query note($id: NoteID!) {
query noteForTooltip($id: NoteID!) {
note(id: $id) {
id
author {

View File

@ -818,7 +818,10 @@ export default {
this.viewType = ISSUES_LIST_VIEW_KEY;
},
handleSelectIssuable(issuable) {
this.activeIssuable = issuable;
this.activeIssuable = {
...issuable,
fullPath: this.fullPath,
};
},
updateIssuablesCache(workItem) {
const client = this.$apollo.provider.clients.defaultClient;

View File

@ -5,7 +5,6 @@ import errorTrackingStore from '~/error_tracking/store';
import { apolloProvider } from '~/graphql_shared/issuable_client';
import { TYPE_INCIDENT, TYPE_ISSUE } from '~/issues/constants';
import { convertObjectPropsToCamelCase, parseBoolean } from '~/lib/utils/common_utils';
import { scrollToTargetOnResize } from '~/lib/utils/resize_observer';
import initLinkedResources from '~/linked_resources';
import IssueApp from './components/app.vue';
import DescriptionComponent from './components/description.vue';
@ -73,8 +72,6 @@ export function initIssuableApp(store) {
bootstrapApollo({ ...issueState, issueType });
scrollToTargetOnResize();
if (issueType === TYPE_INCIDENT) {
initLinkedResources();
}

View File

@ -21,7 +21,7 @@ export default {
{ id: 0, href: this.switchDashboardPath, text: this.dashboardLinkText },
{
id: 1,
href: 'https://gitlab.com/gitlab-org/gitlab/-/issues/460910',
href: 'https://gitlab.com/gitlab-org/gitlab/-/issues/478844',
text: __('Provide feedback'),
extraAttrs: {
target: '__blank',

View File

@ -136,7 +136,7 @@ export default {
class="gl-relative"
data-testid="assigned-user"
>
<gl-avatar :src="user.avatarUrl" :size="32" />
<gl-avatar :src="user.avatarUrl" :size="32" class="!gl-bg-white" />
<span
v-if="isCurrentUser(user)"
class="gl-absolute -gl-left-2 -gl-top-2 gl-flex gl-h-5 gl-w-5 gl-items-center gl-justify-center gl-rounded-full gl-bg-blue-500 gl-p-1 gl-text-white"

View File

@ -90,15 +90,18 @@ export default {
},
canEdit: {
type: Boolean,
required: true,
required: false,
default: false,
},
canAwardEmoji: {
type: Boolean,
required: true,
required: false,
default: false,
},
canDelete: {
type: Boolean,
required: true,
required: false,
default: false,
},
canResolve: {
type: Boolean,

View File

@ -0,0 +1,134 @@
<script>
import { renderGFM } from '~/behaviors/markdown/render_gfm';
import { convertToGraphQLId, getIdFromGraphQLId } from '~/graphql_shared/utils';
import { TYPENAME_NOTE } from '~/graphql_shared/constants';
import * as Sentry from '~/sentry/sentry_browser_wrapper';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import SafeHtml from '~/vue_shared/directives/safe_html';
import timeagoMixin from '~/vue_shared/mixins/timeago';
import noteQuery from '../graphql/note.query.graphql';
import NoteEditedText from './note_edited_text.vue';
import NoteableNote from './noteable_note.vue';
export default {
components: {
NoteEditedText,
NoteableNote,
},
directives: {
SafeHtml,
},
mixins: [timeagoMixin, glFeatureFlagMixin()],
props: {
noteId: {
type: String,
required: false,
default: '',
},
},
data() {
return {
note: null,
hidden: false,
};
},
computed: {
showNote() {
return this.note && !this.hidden && !this.isSyntheticNote;
},
showEdited() {
return this.note && this.note.created_at !== this.note.last_edited_at;
},
isSyntheticNote() {
return Boolean(this.noteId?.match(/([a-f0-9]{40})/));
},
noteHtml() {
return this.note?.body_html;
},
},
watch: {
async noteHtml() {
try {
await this.$nextTick();
renderGFM(this.$refs.noteBody);
} catch {
this.fallback();
}
},
},
mounted() {
if (this.isSyntheticNote) {
this.fallback();
}
},
methods: {
fallback() {
this.hidden = true;
},
},
apollo: {
note: {
skip() {
return !this.noteId || this.isSyntheticNote;
},
query: noteQuery,
variables() {
return {
id: convertToGraphQLId(TYPENAME_NOTE, this.noteId),
};
},
update(data) {
return {
...data.note,
author: {
...data.note.author,
id: getIdFromGraphQLId(data.note.author.id),
},
last_edited_by: {
...data.note.last_edited_by,
id: getIdFromGraphQLId(data.note.last_edited_by?.id),
},
id: getIdFromGraphQLId(data.note.id),
};
},
result(result) {
if (result?.errors?.length > 0) {
Sentry.captureException(result.errors[0].message);
this.fallback();
}
if (!result?.data?.note) {
this.fallback();
}
},
error(error) {
Sentry.captureException(error);
this.fallback();
},
},
},
};
</script>
<template>
<noteable-note
v-if="showNote"
:id="`note_${noteId}`"
:note="note"
:show-reply-button="false"
should-scroll-to-note
>
<template #note-body>
<div ref="noteBody" class="note-body">
<div v-safe-html:[$options.safeHtmlConfig]="noteHtml" class="note-text md"></div>
<note-edited-text
v-if="showEdited"
:edited-at="note.last_edited_at"
:edited-by="note.last_edited_by"
:action-text="__('Edited')"
class="note_edited_ago"
/>
</div>
</template>
</noteable-note>
</template>

View File

@ -1,9 +1,9 @@
<script>
import { GlSprintf, GlAvatarLink, GlAvatar } from '@gitlab/ui';
import $ from 'jquery';
import { escape } from 'lodash';
// eslint-disable-next-line no-restricted-imports
import { mapGetters, mapActions } from 'vuex';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import SafeHtml from '~/vue_shared/directives/safe_html';
import { confirmAction } from '~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal';
import { INLINE_DIFF_LINES_KEY } from '~/diffs/constants';
@ -126,6 +126,9 @@ export default {
author() {
return this.note.author;
},
authorId() {
return getIdFromGraphQLId(this.author.id);
},
commentType() {
return this.note.internal ? __('internal note') : __('comment');
},
@ -136,11 +139,17 @@ export default {
'is-requesting being-posted': this.isRequesting,
'disabled-content': this.isDeleting,
target: this.isTarget,
'is-editable': this.note.current_user.can_edit,
'is-editable': this.canEdit,
};
},
canAwardEmoji() {
return this.note.current_user?.can_award_emoji ?? false;
},
canEdit() {
return this.note.current_user?.can_edit ?? false;
},
canReportAsAbuse() {
return Boolean(this.reportAbusePath) && this.author.id !== this.getUserData.id;
return Boolean(this.reportAbusePath) && this.authorId !== this.getUserData.id;
},
noteAnchorId() {
return `note_${this.note.id}`;
@ -176,7 +185,7 @@ export default {
if (!this.discussionRoot) return false;
if (!this.note.resolvable) return false;
return this.note.current_user.can_resolve_discussion;
return this.note.current_user?.can_resolve_discussion;
},
lineRange() {
return this.note.position?.line_range;
@ -236,14 +245,14 @@ export default {
if (noteId === this.note.id) {
this.isEditing = true;
this.setSelectedCommentPositionHover();
this.scrollToNoteIfNeeded($(this.$el));
this.$el.scrollIntoView();
}
});
},
mounted() {
if (this.isTarget && this.shouldScrollToNote) {
this.scrollToNoteIfNeeded($(this.$el));
this.$el.scrollIntoView({ duration: 0 });
}
},
@ -253,7 +262,6 @@ export default {
'removeNote',
'updateNote',
'toggleResolveNote',
'scrollToNoteIfNeeded',
'updateAssignees',
'setSelectedCommentPositionHover',
]),
@ -428,7 +436,7 @@ export default {
<div v-if="isMRDiffView" class="timeline-avatar gl-float-left gl-pt-2">
<gl-avatar-link
:href="author.path"
:data-user-id="author.id"
:data-user-id="authorId"
:data-username="author.username"
class="js-user-link"
>
@ -446,7 +454,7 @@ export default {
<div v-else class="timeline-avatar gl-float-left">
<gl-avatar-link
:href="author.path"
:data-user-id="author.id"
:data-user-id="authorId"
:data-username="author.username"
class="js-user-link gl-relative"
>
@ -480,7 +488,7 @@ export default {
</note-header>
<note-actions
:author="author"
:author-id="author.id"
:author-id="authorId"
:note-id="note.id"
:note-url="note.noteable_note_url"
:access-level="note.human_access"
@ -489,9 +497,9 @@ export default {
:project-name="note.project_name"
:noteable-type="note.noteable_type"
:show-reply="showReplyButton"
:can-edit="note.current_user.can_edit"
:can-award-emoji="note.current_user.can_award_emoji"
:can-delete="note.current_user.can_edit"
:can-edit="canEdit"
:can-award-emoji="canAwardEmoji"
:can-delete="canEdit"
:can-report-as-abuse="canReportAsAbuse"
:can-resolve="canResolve"
:resolvable="note.resolvable || note.isDraft"
@ -511,10 +519,11 @@ export default {
</div>
<div class="timeline-discussion-body">
<slot name="discussion-resolved-text"></slot>
<slot name="note-body">
<note-body
ref="noteBody"
:note="note"
:can-edit="note.current_user.can_edit"
:can-edit="canEdit"
:line="line"
:file="diffFile"
:is-editing="isEditing"
@ -522,6 +531,7 @@ export default {
@handleFormUpdate="formUpdateHandler"
@cancelForm="formCancelHandler"
/>
</slot>
<div class="timeline-discussion-body-footer">
<slot name="after-note-body"></slot>
</div>

View File

@ -5,11 +5,13 @@ import { v4 as uuidv4 } from 'uuid';
import { InternalEvents } from '~/tracking';
import { getDraft, getAutoSaveKeyFromDiscussion } from '~/lib/utils/autosave';
import highlightCurrentUser from '~/behaviors/markdown/highlight_current_user';
import { scrollToTargetOnResize } from '~/lib/utils/resize_observer';
import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue';
import OrderedLayout from '~/vue_shared/components/ordered_layout.vue';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import DraftNote from '~/batch_comments/components/draft_note.vue';
import { getLocationHash } from '~/lib/utils/url_utility';
import NotePreview from '~/notes/components/note_preview.vue';
import PlaceholderNote from '~/vue_shared/components/notes/placeholder_note.vue';
import PlaceholderSystemNote from '~/vue_shared/components/notes/placeholder_system_note.vue';
import SkeletonLoadingContainer from '~/vue_shared/components/notes/skeleton_note.vue';
@ -26,6 +28,7 @@ import NotesActivityHeader from './notes_activity_header.vue';
export default {
name: 'NotesApp',
components: {
NotePreview,
NotesActivityHeader,
NoteableNote,
NoteableDiscussion,
@ -97,6 +100,7 @@ export default {
'userCanReply',
'sortDirection',
'timelineEnabled',
'targetNoteHash',
]),
sortDirDesc() {
return this.sortDirection === constants.DESC;
@ -104,6 +108,10 @@ export default {
noteableType() {
return this.noteableData.noteableType;
},
previewNoteId() {
if (!this.isLoading || !this.targetNoteHash) return null;
return this.targetNoteHash.replace('note_', '');
},
allDiscussions() {
let skeletonNotes = [];
@ -113,6 +121,17 @@ export default {
skeletonNotes = new Array(prerenderedNotesCount).fill({
isSkeletonNote: true,
});
if (
this.previewNoteId &&
!this.discussions.find((d) => d.notes[0].id === this.previewNoteId)
) {
const previewNote = {
id: this.previewNoteId,
isPreviewNote: true,
};
skeletonNotes.splice(prerenderedNotesCount / 2, 0, previewNote);
}
}
if (this.sortDirDesc) {
@ -172,6 +191,10 @@ export default {
window.addEventListener('hashchange', this.handleHashChanged);
if (this.targetNoteHash && this.targetNoteHash.startsWith('note_')) {
scrollToTargetOnResize();
}
eventHub.$on('notesApp.updateIssuableConfidentiality', this.setConfidentiality);
},
updated() {
@ -213,7 +236,7 @@ export default {
},
checkLocationHash() {
const hash = getLocationHash();
const noteId = hash && hash.replace(/^note_/, '');
const noteId = (hash && hash.startsWith('note_') && hash.replace(/^note_/, '')) ?? null;
if (noteId) {
const discussion = this.discussions.find((d) => d.notes.some(({ id }) => id === noteId));
@ -278,6 +301,13 @@ export default {
:key="discussion.id"
class="note-skeleton"
/>
<timeline-entry-item
v-else-if="discussion.isPreviewNote"
:key="discussion.id"
class="target note note-wrapper note-comment"
>
<note-preview :note-id="previewNoteId" />
</timeline-entry-item>
<timeline-entry-item v-else-if="discussion.isDraft" :key="discussion.id">
<draft-note :draft="discussion" />
</timeline-entry-item>

View File

@ -0,0 +1,26 @@
query snakeCaseNote($id: NoteID!) {
note(id: $id) {
id
author {
id
avatar_url: avatarUrl
name
username
web_url: webUrl
web_path: webPath
}
body_html: bodyHtml
created_at: createdAt
last_edited_at: lastEditedAt
last_edited_by: lastEditedBy {
id
avatar_url: avatarUrl
name
username
web_url: webUrl
web_path: webPath
}
internal
url
}
}

View File

@ -13,7 +13,7 @@ import { confidentialWidget } from '~/sidebar/components/confidential/sidebar_co
import updateIssueLockMutation from '~/sidebar/queries/update_issue_lock.mutation.graphql';
import updateMergeRequestLockMutation from '~/sidebar/queries/update_merge_request_lock.mutation.graphql';
import loadAwardsHandler from '~/awards_handler';
import { isInViewport, scrollToElement, isInMRPage } from '~/lib/utils/common_utils';
import { isInMRPage } from '~/lib/utils/common_utils';
import { mergeUrlParams } from '~/lib/utils/url_utility';
import sidebarTimeTrackingEventHub from '~/sidebar/event_hub';
import TaskList from '~/task_list';
@ -632,12 +632,6 @@ export const toggleAwardRequest = ({ dispatch }, data) => {
});
};
export const scrollToNoteIfNeeded = (context, el) => {
if (!isInViewport(el[0])) {
scrollToElement(el);
}
};
export const fetchDiscussionDiffLines = ({ commit }, discussion) =>
axios.get(discussion.truncated_diff_lines_path).then(({ data }) => {
commit(types.SET_DISCUSSION_DIFF_LINES, {

View File

@ -116,11 +116,6 @@ export default {
default: '',
},
},
data() {
return {
showForm: this.isFormVisible || this.hasError,
};
},
computed: {
hasRelatedIssues() {
return this.relatedIssues.length > 0;
@ -176,29 +171,25 @@ export default {
: sprintf(this.$options.i18n.emptyItemsFree, { issuableType: this.issuableType });
},
},
mounted() {
this.$nextTick(() => {
if (this.showForm) {
this.$refs.relatedIssuesWidget.showForm();
}
});
},
updated() {
this.$nextTick(() => {
if (this.hasError) {
watch: {
isFormVisible(newVal) {
if (newVal === true) {
this.$refs.relatedIssuesWidget.showForm();
} else {
this.$refs.relatedIssuesWidget.hideForm();
}
});
},
},
mounted() {
if (this.isFormVisible) {
this.$refs.relatedIssuesWidget.showForm();
}
},
methods: {
handleFormSubmit(event) {
this.showForm = false;
this.$emit('addIssuableFormSubmit', event);
},
handleFormCancel(event) {
this.showForm = false;
this.$emit('addIssuableFormCancel', event);
this.$refs.relatedIssuesWidget.hideForm();
},
@ -227,6 +218,8 @@ export default {
:help-link-text="helpLinkText"
anchor-id="related-issues"
data-testid="related-issues-block"
@showForm="$emit('showForm')"
@hideForm="$emit('hideForm')"
>
<template #actions>
<slot name="header-actions"></slot>
@ -253,7 +246,7 @@ export default {
/>
</template>
<template v-if="!shouldShowTokenBody && !isFormVisible" #empty>
<template v-if="!shouldShowTokenBody" #empty>
<slot name="empty-state-message">{{ emptyStateMessage }}</slot>
<gl-link
v-if="hasHelpPath"

View File

@ -179,6 +179,7 @@ export default {
if (response && response.data && response.data.message) {
this.errorMessage = response.data.message;
}
this.isFormVisible = true;
})
.finally(() => {
this.isSubmitting = false;
@ -282,5 +283,7 @@ export default {
@addIssuableFormCancel="onPendingFormCancel"
@pendingIssuableRemoveRequest="onPendingIssueRemoveRequest"
@relatedIssueRemoveRequest="onRelatedIssueRemoveRequest"
@showForm="isFormVisible = true"
@hideForm="isFormVisible = false"
/>
</template>

View File

@ -108,9 +108,11 @@ export default {
showForm() {
this.isFormVisible = true;
this.collapsed = false;
this.$emit('showForm');
},
hideForm() {
this.isFormVisible = false;
this.$emit('hideForm');
},
toggleForm() {
if (this.isFormVisible) {

View File

@ -245,7 +245,7 @@ export default {
e.preventDefault();
this.$emit('select-issuable', {
iid: this.issuableIid,
webUrl: this.issuableLinkHref,
webUrl: this.issuable.webUrl,
fullPath: this.workItemFullPath,
});
},

View File

@ -1,6 +1,7 @@
<script>
import { GlLink, GlDrawer } from '@gitlab/ui';
import { GlLink, GlDrawer, GlButton, GlTooltipDirective } from '@gitlab/ui';
import { escapeRegExp } from 'lodash';
import { __ } from '~/locale';
import deleteWorkItemMutation from '~/work_items/graphql/delete_work_item.mutation.graphql';
import { TYPE_EPIC, TYPE_ISSUE } from '~/issues/constants';
import * as Sentry from '~/sentry/sentry_browser_wrapper';
@ -8,9 +9,13 @@ import { visitUrl } from '~/lib/utils/url_utility';
export default {
name: 'WorkItemDrawer',
directives: {
GlTooltip: GlTooltipDirective,
},
components: {
GlLink,
GlDrawer,
GlButton,
WorkItemDetail: () => import('~/work_items/components/work_item_detail.vue'),
},
inject: ['fullPath'],
@ -31,6 +36,11 @@ export default {
default: TYPE_ISSUE,
},
},
data() {
return {
copyTooltipText: this.$options.i18n.copyTooltipText,
};
},
computed: {
activeItemFullPath() {
if (this.activeItem?.fullPath) {
@ -45,6 +55,10 @@ export default {
modalIsGroup() {
return this.issuableType === TYPE_EPIC;
},
headerReference() {
const path = this.activeItemFullPath.substring(this.activeItemFullPath.lastIndexOf('/') + 1);
return `${path}#${this.activeItem.iid}`;
},
},
methods: {
async deleteWorkItem({ workItemId }) {
@ -62,8 +76,12 @@ export default {
Sentry.captureException(error);
}
},
redirectToWorkItem() {
redirectToWorkItem(e) {
const workItem = this.activeItem;
if (e.metaKey || e.ctrlKey) {
return;
}
e.preventDefault();
const escapedFullPath = escapeRegExp(this.fullPath);
// eslint-disable-next-line no-useless-escape
const regex = new RegExp(`groups\/${escapedFullPath}\/-\/(work_items|epics)\/\\d+`);
@ -80,6 +98,17 @@ export default {
visitUrl(workItem.webUrl);
}
},
handleCopyToClipboard() {
this.copyTooltipText = this.$options.i18n.copiedTooltipText;
setTimeout(() => {
this.copyTooltipText = this.$options.i18n.copyTooltipText;
}, 2000);
},
},
i18n: {
copyTooltipText: __('Copy item URL'),
copiedTooltipText: __('Copied'),
openTooltipText: __('Open in full page'),
},
};
</script>
@ -94,12 +123,39 @@ export default {
@close="$emit('close')"
>
<template #title>
<div class="gl-text gl-flex gl-w-full gl-items-center gl-gap-x-2 xl:gl-px-4">
<gl-link
class="gl-text-default"
:href="activeItem.webUrl"
@click.prevent="redirectToWorkItem"
>{{ __('Open full view') }}</gl-link
class="gl-text-sm gl-font-bold gl-text-default"
@click="redirectToWorkItem"
>
{{ headerReference }}
</gl-link>
<gl-button
v-gl-tooltip
data-testid="work-item-drawer-copy-button"
:title="copyTooltipText"
category="tertiary"
class="gl-text-secondary"
icon="link"
size="small"
:aria-label="$options.i18n.copyTooltipText"
:data-clipboard-text="activeItem.webUrl"
@click="handleCopyToClipboard"
/>
<gl-button
v-gl-tooltip
data-testid="work-item-drawer-link-button"
:href="activeItem.webUrl"
:title="$options.i18n.openTooltipText"
category="tertiary"
class="gl-text-secondary"
icon="maximize"
size="small"
:aria-label="$options.i18n.openTooltipText"
@click="redirectToWorkItem"
/>
</div>
</template>
<template #default>
<work-item-detail

View File

@ -107,7 +107,11 @@ $diff-file-header: 41px;
.image {
@apply gl-bg-strong;
text-align: center;
padding: 30px;
padding: $gl-padding;
&:has(> .view) {
padding-bottom: $gl-padding-32;
}
.wrap {
display: inline-block;

View File

@ -253,16 +253,6 @@ ul.related-merge-requests > li gl-emoji {
}
}
.issuable-header-slide-enter-active,
.issuable-header-slide-leave-active {
@apply gl-transition-all;
}
.issuable-header-slide-enter,
.issuable-header-slide-leave-to {
transform: translateY(-100%);
}
.issuable-sticky-header-visible {
--issuable-sticky-header-height: 40px;
}

View File

@ -17,13 +17,11 @@ module TimeTrackable
alias_method :time_spent?, :time_spent
attribute :time_estimate, default: 0
validate :check_time_estimate
validate :check_negative_time_spent
has_many :timelogs, dependent: :destroy, autosave: true # rubocop:disable Cop/ActiveRecordDependent
after_initialize :set_time_estimate_default_value
before_save :set_time_estimate_default_value
after_save :clear_memoized_total_time_spent
end
@ -95,11 +93,18 @@ module TimeTrackable
val.is_a?(Integer) ? super([val, Gitlab::Database::MAX_INT_VALUE].min) : super(val)
end
def time_estimate
super || self.class.column_defaults['time_estimate']
end
def set_time_estimate_default_value
return if new_record?
return unless has_attribute?(:time_estimate)
# time estimate can be set to nil, in case of an invalid value, e.g. a String instead of a number, in which case
# we should not be overwriting it to default value, but rather have the validation catch the error
return if time_estimate_changed?
self.time_estimate ||= self.class.column_defaults['time_estimate']
self.time_estimate = self.class.column_defaults['time_estimate'] if read_attribute(:time_estimate).nil?
end
private
@ -136,8 +141,9 @@ module TimeTrackable
end
def check_time_estimate
return unless new_record? || time_estimate_changed?
return if time_estimate.is_a?(Numeric) && time_estimate >= 0
# we'll set the time_tracking to zero at DB level through default value
return unless time_estimate_changed?
return if read_attribute(:time_estimate).is_a?(Numeric) && read_attribute(:time_estimate) >= 0
errors.add(:time_estimate, _('must have a valid format and be greater than or equal to zero.'))
end

View File

@ -0,0 +1,17 @@
# frozen_string_literal: true
class ChangeTimeEstimateDefaultFromNullToZeroOnIssues < Gitlab::Database::Migration[2.2]
milestone '17.4'
enable_lock_retries!
TABLE_NAME = :issues
COLUMN_NAME = :time_estimate
def up
change_column_default(TABLE_NAME, COLUMN_NAME, from: nil, to: 0)
end
def down
remove_column_default(TABLE_NAME, COLUMN_NAME)
end
end

View File

@ -0,0 +1,17 @@
# frozen_string_literal: true
class ChangeTimeEstimateDefaultFromNullToZeroOnMergeRequests < Gitlab::Database::Migration[2.2]
milestone '17.4'
enable_lock_retries!
TABLE_NAME = :merge_requests
COLUMN_NAME = :time_estimate
def up
change_column_default(TABLE_NAME, COLUMN_NAME, from: nil, to: 0)
end
def down
remove_column_default(TABLE_NAME, COLUMN_NAME)
end
end

View File

@ -0,0 +1 @@
39c3afa6ca1f4005d7d26d3ea510d406a2d4aa101bf5bf2208961b796d0d47bf

View File

@ -0,0 +1 @@
d7f9e2f68e4d0375e381f2345bc2b8fe4eac2f965fa467529d9a27c70e73f5a4

View File

@ -12367,7 +12367,7 @@ CREATE TABLE issues (
lock_version integer DEFAULT 0,
title_html text,
description_html text,
time_estimate integer,
time_estimate integer DEFAULT 0,
relative_position integer,
service_desk_reply_to character varying,
cached_markdown_version integer,
@ -13232,7 +13232,7 @@ CREATE TABLE merge_requests (
lock_version integer DEFAULT 0,
title_html text,
description_html text,
time_estimate integer,
time_estimate integer DEFAULT 0,
squash boolean DEFAULT false NOT NULL,
cached_markdown_version integer,
last_edited_at timestamp without time zone,

View File

@ -1,31 +0,0 @@
---
# Error: gitlab_base.CodeBlockNesting
#
# Ensures content nested in lists are spaced correctly.
#
extends: existence
message: "Items under an ordered list must be indented three spaces. Items under an unordered list must be indented two spaces."
link: https://docs.gitlab.com/ee/development/documentation/styleguide/#nesting-inside-a-list-item
level: error
nonword: true
ignorecase: true
scope: raw
tokens:
- '^1. .*\n\n? ( )?[`\w-]'
- '^- .*\n\n? ( )?[`\w-]'
# Regex guide:
#
# "^1. .*" - Lines that start with an ordered list.
# "^- .*" - Lines that start with an unordered list.
#
# "\n\n?" - Then one or two newlines
#
# Ordered lists: " ( )?" - Two or four spaces (three = correct)
# Unordered lists: " ( )?" - One or three spaces (two = correct)
#
# "[`\w-]" - Any one of:
#
# - A backtick - For code blocks after a list.
# - A letter/number - For alert boxes, sentences, and nested ordered lists (after a list).
# - A hyphen - For nested unordered lists (after a list).

View File

@ -0,0 +1,31 @@
---
# Error: gitlab_base.ListIndentation
#
# Ensures content nested in lists is spaced correctly.
#
extends: existence
message: "Items nested under an ordered list must be indented three spaces. Items nested under an unordered list must be indented two spaces."
link: https://docs.gitlab.com/ee/development/documentation/styleguide/#nesting-inside-a-list-item
level: error
nonword: true
ignorecase: true
scope: raw
tokens:
- '^1\. [^\n]*\n\n?( | | )[`\w-]'
- '^- [^\n]*\n\n?( | | )[`\w-]'
# Regex guide:
#
# "^1. [^\n]*" - Lines that start with an ordered list.
# "^- [^\n]*" - Lines that start with an unordered list.
#
# "\n\n?" - Then one or two newlines
#
# Ordered lists: "( | | )" - One, two, or four spaces (three = OK)
# Unordered lists: "( | | )" - One, three, or four spaces (two = OK)
#
# "[`\w-]" - Any one of:
#
# - A backtick - For code blocks (after a list item).
# - A letter/number - For alert boxes, sentences, and nested ordered lists (after a list item).
# - A hyphen - For nested unordered lists (after a list item).

View File

@ -43,17 +43,17 @@ To set up your self-hosted model deployment infrastructure:
Install one of the following GitLab-approved LLM models:
| Model | Code Completion | Code Generation | Duo Chat |
|-----------------------------------------------------------------------------------------------------------------------------|-----------------|-----------------|----------|
| [CodeGemma 2b](https://huggingface.co/google/codegemma-2b) | ✅ | - | - |
| [CodeGemma 7b-it](https://huggingface.co/google/codegemma-7b-it) (Instruction) | - | ✅ | - |
| [CodeGemma 7b-code](https://huggingface.co/google/codegemma-7b) (Code) | ✅ | - | - |
| [Code-Llama 13b-code](https://huggingface.co/meta-llama/CodeLlama-13b-hf) | ✅ | - | - |
| [Code-Llama 13b](https://huggingface.co/meta-llama/CodeLlama-13b-Instruct-hf) | - | ✅ | - |
| [Codestral 22B](https://huggingface.co/mistralai/Codestral-22B-v0.1) (see [setup instructions](litellm_proxy_setup.md#example-setup-for-codestral-with-ollama)) | ✅ | ✅ | - |
| [Mistral 7B](https://huggingface.co/mistralai/Mistral-7B-v0.1) | - | ✅ | ✅ |
| [Mixtral 8x22B](https://huggingface.co/mistral-community/Mixtral-8x22B-v0.1) | - | ✅ | ✅ |
| [Mixtral 8x7B](https://huggingface.co/mistralai/Mixtral-8x7B-Instruct-v0.1) | - | ✅ | ✅ |
| Model | Code completion | Code generation | GitLab Duo Chat |
|------------------------------------------------------------------------------------|-----------------|-----------------|---------|
| [CodeGemma 2b](https://huggingface.co/google/codegemma-2b) | **{check-circle}** Yes | **{dotted-circle}** No | **{dotted-circle}** No |
| [CodeGemma 7b-it](https://huggingface.co/google/codegemma-7b-it) (Instruction) | **{dotted-circle}** No | **{check-circle}** Yes | **{dotted-circle}** No |
| [CodeGemma 7b-code](https://huggingface.co/google/codegemma-7b) (Code) | **{check-circle}** Yes | **{dotted-circle}** No | **{dotted-circle}** No |
| [Code-Llama 13b-code](https://huggingface.co/meta-llama/CodeLlama-13b-hf) | **{check-circle}** Yes | **{dotted-circle}** No | **{dotted-circle}** No |
| [Code-Llama 13b](https://huggingface.co/meta-llama/CodeLlama-13b-Instruct-hf) | **{dotted-circle}** No | **{check-circle}** Yes | **{dotted-circle}** No |
| [Codestral 22B](https://huggingface.co/mistralai/Codestral-22B-v0.1) (see [setup instructions](litellm_proxy_setup.md#example-setup-for-codestral-with-ollama)) | **{check-circle}** Yes | **{check-circle}** Yes | **{dotted-circle}** No |
| [Mistral 7B](https://huggingface.co/mistralai/Mistral-7B-v0.1) | **{dotted-circle}** No | **{check-circle}** Yes | **{check-circle}** Yes |
| [Mixtral 8x22B](https://huggingface.co/mistral-community/Mixtral-8x22B-v0.1) | **{dotted-circle}** No | **{check-circle}** Yes | **{check-circle}** Yes |
| [Mixtral 8x7B](https://huggingface.co/mistralai/Mixtral-8x7B-Instruct-v0.1) | **{dotted-circle}** No | **{check-circle}** Yes | **{check-circle}** Yes |
### Recommended serving architectures

View File

@ -34889,18 +34889,6 @@ Represents a generic widget definition.
| ---- | ---- | ----------- |
| <a id="workitemwidgetdefinitiongenerictype"></a>`type` | [`WorkItemWidgetType!`](#workitemwidgettype) | Widget type. |
### `WorkItemWidgetDefinitionHealthStatus`
Represents a health status widget definition.
#### Fields
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="workitemwidgetdefinitionhealthstatuseditable"></a>`editable` | [`Boolean!`](#boolean) | Indicates whether editable health status is available. |
| <a id="workitemwidgetdefinitionhealthstatusrollup"></a>`rollUp` | [`Boolean!`](#boolean) | Indicates whether rolled up health status is available. |
| <a id="workitemwidgetdefinitionhealthstatustype"></a>`type` | [`WorkItemWidgetType!`](#workitemwidgettype) | Widget type. |
### `WorkItemWidgetDefinitionHierarchy`
Represents a hierarchy widget definition.
@ -34998,7 +34986,6 @@ Represents a health status widget.
| <a id="workitemwidgethealthstatushealthstatus"></a>`healthStatus` | [`HealthStatus`](#healthstatus) | Health status of the work item. |
| <a id="workitemwidgethealthstatusrolleduphealthstatus"></a>`rolledUpHealthStatus` **{warning-solid}** | [`[WorkItemWidgetHealthStatusCount!]`](#workitemwidgethealthstatuscount) | **Introduced** in GitLab 17.3. **Status**: Experiment. Rolled up health status of the work item. |
| <a id="workitemwidgethealthstatustype"></a>`type` | [`WorkItemWidgetType`](#workitemwidgettype) | Widget type. |
| <a id="workitemwidgethealthstatuswidgetdefinition"></a>`widgetDefinition` | [`WorkItemWidgetDefinitionHealthStatus`](#workitemwidgetdefinitionhealthstatus) | Health status widget definition. |
### `WorkItemWidgetHealthStatusCount`
@ -40735,7 +40722,6 @@ Implementations:
- [`WorkItemWidgetDefinitionAssignees`](#workitemwidgetdefinitionassignees)
- [`WorkItemWidgetDefinitionGeneric`](#workitemwidgetdefinitiongeneric)
- [`WorkItemWidgetDefinitionHealthStatus`](#workitemwidgetdefinitionhealthstatus)
- [`WorkItemWidgetDefinitionHierarchy`](#workitemwidgetdefinitionhierarchy)
- [`WorkItemWidgetDefinitionLabels`](#workitemwidgetdefinitionlabels)
- [`WorkItemWidgetDefinitionWeight`](#workitemwidgetdefinitionweight)

View File

@ -102,7 +102,7 @@ plan:
key: TP
name: test plan
stages:
- Default Stage:
- Default Stage:
manual: false
final: false
jobs:
@ -125,10 +125,10 @@ Default Job:
description: run bundler
artifact-subscriptions: []
repositories:
- Demo Project:
- Demo Project:
scope: global
triggers:
- polling:
- polling:
period: '180'
branches:
create: manually
@ -150,7 +150,7 @@ version: 2
plan:
key: AB-TP
plan-permissions:
- users:
- users:
- root
permissions:
- view
@ -159,7 +159,7 @@ plan-permissions:
- clone
- admin
- view-configuration
- roles:
- roles:
- logged-in
- anonymous
permissions:
@ -175,7 +175,7 @@ default:
image: ruby:latest
stages:
- default-stage
- default-stage
job1:
stage: default-stage
@ -489,7 +489,7 @@ For example, in a Bamboo build plan:
version: 2
#...
triggers:
- polling:
- polling:
period: '180'
```

View File

@ -169,6 +169,7 @@ to stop them from running:
- Use [`rules`](../yaml/index.md#rules) to skip tests that aren't needed. For example,
skip backend tests when only the frontend code is changed.
- Run non-essential [scheduled pipelines](schedules.md) less frequently.
- Distribute [`cron` schedules](schedules.md#view-and-optimize-pipeline-schedules) evenly across time.
### Fail fast

View File

@ -96,7 +96,8 @@ When working with pipeline schedules, you might encounter the following issues.
### Short refs are expanded to full refs
When you provide a short `ref` to the API, it is automatically expanded to a full `ref`. This behavior is intended and ensures explicit resource identification.
When you provide a short `ref` to the API, it is automatically expanded to a full `ref`.
This behavior is intended and ensures explicit resource identification.
The API accepts both short refs (such as `main`) and full refs (such as `refs/heads/main` or `refs/tags/main`).
@ -108,3 +109,31 @@ In some cases, the API can't automatically expand a short `ref` to a full `ref`.
- You provide a short `ref`, but no branch or tag with that name exists.
To resolve this issue, provide the full `ref` to ensure the correct resource is identified.
### View and optimize pipeline schedules
To prevent [excessive load](pipeline_efficiency.md) caused by too many pipelines starting simultaneously,
you can review and optimize your pipeline schedules.
To get an overview of all existing schedules and identify opportunities to distribute them more evenly:
1. Run this command to extract and format schedule data:
```shell
outfile=/tmp/gitlab_ci_schedules.tsv
sudo gitlab-psql --command "
COPY (SELECT
ci_pipeline_schedules.cron,
projects.path AS project,
users.email
FROM ci_pipeline_schedules
JOIN projects ON projects.id = ci_pipeline_schedules.project_id
JOIN users ON users.id = ci_pipeline_schedules.owner_id
) TO '$outfile' CSV HEADER DELIMITER E'\t' ;"
sort "$outfile" | uniq -c | sort -n
```
1. Review the output to identify popular `cron` patterns.
For example, you might see many schedules set to run at the start of each hour (`0 * * * *`).
1. Adjust the schedules to create a staggered [`cron` pattern](../../topics/cron/index.md#cron-syntax), especially for large repositories.
For example, instead of multiple schedules running at the start of each hour, distribute them throughout the hour (`5 * * * *`, `15 * * * *`, `25 * * * *`).

View File

@ -512,7 +512,7 @@ Implementing Geo provides the following benefits:
developers to clone and fetch large repositories and projects.
- Enable developers to contribute ideas and work in parallel, across
regions..
regions.
- Balance the read-only load between primary and secondary sites.

View File

@ -38,15 +38,10 @@ If you are a self-managed user, ensure that Code Suggestions for the [GitLab Web
If the settings are enabled, but suggestions are still not displayed, try the following steps:
1. In the GitLab Workflow **Extension Settings**, enable **GitLab: Debug**.
1. Open the extension log in **View > Output** and change the dropdown list to **GitLab Workflow** as the log filter. The command palette command is `GitLab: Show Extension Logs`.
1. Disable and re-enable the **Enable code completion** checkbox.
1. Verify that the debug log contains similar output:
```shell
2023-07-14T17:29:00:763 [debug]: Disabling code completion
2023-07-14T17:29:01:802 [debug]: Enabling code completion
2023-07-14T17:29:01:802 [debug]: AI Assist: Using server: https://cloud.gitlab.com/ai/v2/code/completions
```
1. On the top menu, select **View > Output** to open the bottom panel, then either:
- In the command palette, select `GitLab: Show Extension Logs`.
- In the bottom panel, on the right, select the dropdown list to filter the logs. Select **GitLab Workflow**.
1. In the GitLab Workflow **Extension Settings**, clear and re-select the **GitLab Duo Code Suggestions** checkbox.
To enable debug logging for the Language Server (LS), enable **GitLab Ls: Debug**.

View File

@ -87,11 +87,12 @@ For more information, see [Pack-objects cache](../../../../administration/gitaly
### Reduce concurrent clones in CI/CD
CI/CD loads tend to be concurrent because pipelines are scheduled during set times.
CI/CD loads tend to be concurrent because pipelines are [scheduled during set times](../../../../ci/pipelines/pipeline_efficiency.md#reduce-how-often-jobs-run).
As a result, the Git requests against the repositories can spike notably during
these times and lead to reduced performance for both CI/CD and users alike.
Reduce CI/CD pipeline concurrency by staggering them to run at different times.
Reduce CI/CD pipeline concurrency by [staggering them](../../../../ci/pipelines/schedules.md#view-and-optimize-pipeline-schedules)
to run at different times.
For example, a set running at one time and another set running several minutes
later.

View File

@ -15625,6 +15625,9 @@ msgstr ""
msgid "Copy issue URL to clipboard"
msgstr ""
msgid "Copy item URL"
msgstr ""
msgid "Copy key"
msgstr ""
@ -37712,9 +37715,6 @@ msgstr ""
msgid "Open evidence JSON in new tab"
msgstr ""
msgid "Open full view"
msgstr ""
msgid "Open in Gitpod"
msgstr ""
@ -37724,6 +37724,9 @@ msgstr ""
msgid "Open in file view"
msgstr ""
msgid "Open in full page"
msgstr ""
msgid "Open in your IDE"
msgstr ""

View File

@ -249,7 +249,7 @@
"yaml": "^2.0.0-10"
},
"devDependencies": {
"@gitlab/eslint-plugin": "20.2.0",
"@gitlab/eslint-plugin": "20.2.1",
"@gitlab/stylelint-config": "6.2.1",
"@graphql-eslint/eslint-plugin": "3.20.1",
"@originjs/vite-plugin-commonjs": "^1.0.3",

View File

@ -97,22 +97,6 @@ tests = [
expected: ['spec/initializers/action_mailer_hooks_spec.rb']
},
{
explanation: 'FOSS Snowplow definitions map to event validation spec',
changed_file: 'config/events/status_page_incident_unpublished.yml',
expected: ['spec/lib/gitlab/tracking/event_definition_validator_validate_all_spec.rb']
},
{
explanation: 'EE Snowplow definitions map to event validation spec',
changed_file: 'ee/config/events/licenses_list_viewed.yml',
expected: ['spec/lib/gitlab/tracking/event_definition_validator_validate_all_spec.rb']
},
{
explanation: 'Snowplow schema maps to event validation spec',
changed_file: 'config/events/schema.json',
expected: ['spec/lib/gitlab/tracking/event_definition_validator_validate_all_spec.rb']
},
{
explanation: 'DB structure should map to schema spec',
changed_file: 'db/structure.sql',
@ -255,7 +239,101 @@ tests = [
# Note: The metrics seem to be changed every year or so, so this test will fail once a year or so.
# You will need to change the metric below for another metric present in the project.
changed_file: 'ee/config/metrics/counts_all/20221114065035_delete_merge_request.yml',
expected: ['ee/spec/config/metrics/every_metric_definition_spec.rb']
expected: %w[
spec/config/metrics/every_metric_definition_spec.rb
ee/spec/config/metrics/every_metric_definition_spec.rb
spec/lib/gitlab/usage/metric_definition_validate_all_spec.rb
]
},
{
explanation: 'FOSS metric defintions map to metric validation specs',
changed_file: 'config/metrics/counts_28d/count_total_merge_request_click_start_review_on_overview_tab_monthly.yml',
expected: %w[
spec/config/metrics/every_metric_definition_spec.rb
ee/spec/config/metrics/every_metric_definition_spec.rb
spec/lib/gitlab/usage/metric_definition_validate_all_spec.rb
]
},
{
explanation: 'Metric schema maps to metric validation spec',
changed_file: 'config/metrics/schema/base.json',
expected: %w[
spec/config/metrics/every_metric_definition_spec.rb
ee/spec/config/metrics/every_metric_definition_spec.rb
spec/lib/gitlab/usage/metric_definition_validate_all_spec.rb
spec/scripts/internal_events/cli_spec.rb
spec/support_specs/matchers/internal_events_matchers_spec.rb
]
},
{
explanation: 'FOSS Snowplow definitions map to event validation spec',
changed_file: 'config/events/status_page_incident_unpublished.yml',
expected: ['spec/lib/gitlab/tracking/event_definition_validator_validate_all_spec.rb']
},
{
explanation: 'EE Snowplow definitions map to event validation spec',
changed_file: 'ee/config/events/licenses_list_viewed.yml',
expected: ['spec/lib/gitlab/tracking/event_definition_validator_validate_all_spec.rb']
},
{
explanation: 'Snowplow schema maps to event validation spec',
changed_file: 'config/events/schema.json',
expected: %w[
spec/lib/gitlab/tracking/event_definition_validator_validate_all_spec.rb
spec/scripts/internal_events/cli_spec.rb
spec/support_specs/matchers/internal_events_matchers_spec.rb
]
},
{
explanation: 'Internal Events application logic maps to internal events tooling',
changed_file: 'lib/gitlab/internal_events.rb',
expected: %w[
spec/lib/gitlab/internal_events_spec.rb
spec/scripts/internal_events/cli_spec.rb
spec/support_specs/matchers/internal_events_matchers_spec.rb
]
},
{
explanation: 'Internal Events CLI entrypoint maps to internal events tooling',
changed_file: 'scripts/internal_events/cli.rb',
expected: %w[
spec/scripts/internal_events/cli_spec.rb
spec/support_specs/matchers/internal_events_matchers_spec.rb
]
},
{
explanation: 'Internal Events CLI logic maps to internal events tooling',
changed_file: 'scripts/internal_events/cli/global_state.rb',
expected: %w[
spec/scripts/internal_events/cli_spec.rb
spec/support_specs/matchers/internal_events_matchers_spec.rb
]
},
{
explanation: 'Internal Events default test setup maps to internal events tooling',
changed_file: 'spec/support/shared_examples/controllers/internal_event_tracking_examples.rb',
expected: %w[
spec/scripts/internal_events/cli_spec.rb
spec/support_specs/matchers/internal_events_matchers_spec.rb
]
},
{
explanation: 'Internal Events custom test setup maps to internal events tooling',
changed_file: 'spec/support/matchers/internal_events_matchers.rb',
expected: %w[
spec/scripts/internal_events/cli_spec.rb
spec/support_specs/matchers/internal_events_matchers_spec.rb
]
},
{
explanation: 'Internal Events test cases map to internal events tooling',
changed_file: 'spec/fixtures/scripts/internal_events/new_events.yml',
expected: %w[
spec/scripts/internal_events/cli_spec.rb
spec/support_specs/matchers/internal_events_matchers_spec.rb
]
},
{

View File

@ -1126,9 +1126,10 @@ describe('CE IssuesListApp component', () => {
});
it('selects active issuable', () => {
expect(findIssuableList().props('activeIssuable')).toEqual(
getIssuesQueryResponse.data.project.issues.nodes[0],
);
expect(findIssuableList().props('activeIssuable')).toEqual({
...getIssuesQueryResponse.data.project.issues.nodes[0],
fullPath: defaultProvide.fullPath,
});
});
describe('when closing the drawer', () => {

View File

@ -36,7 +36,7 @@ exports[`Merge request dashboard assigned users component renders user avatars 1
>
<img
alt="avatar"
class="gl-avatar gl-avatar-circle gl-avatar-s32"
class="!gl-bg-white gl-avatar gl-avatar-circle gl-avatar-s32"
src="/root"
/>
<span
@ -67,7 +67,7 @@ exports[`Merge request dashboard assigned users component renders user avatars 1
>
<img
alt="avatar"
class="gl-avatar gl-avatar-circle gl-avatar-s32"
class="!gl-bg-white gl-avatar gl-avatar-circle gl-avatar-s32"
src="/root"
/>
<span

View File

@ -0,0 +1,72 @@
import { shallowMount } from '@vue/test-utils';
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import NotePreview from '~/notes/components/note_preview.vue';
import NoteableNote from '~/notes/components/noteable_note.vue';
import noteQuery from '~/notes/graphql/note.query.graphql';
const noteQueryHandler = jest.fn().mockResolvedValue({
data: {
note: {
id: 'gid://gitlab/Note/1',
author: {
id: 'gid://gitlab/User/1',
name: 'Administrator',
username: 'root',
avatar_url: '',
web_url: '',
web_path: '',
},
body_html: 'my quick note',
created_at: '2020-01-01T10:00:00.000Z',
last_edited_at: null,
last_edited_by: null,
internal: false,
url: '/note/1',
},
},
});
describe('Note preview', () => {
let wrapper;
Vue.use(VueApollo);
const createComponent = ({ noteId = '1' }) => {
wrapper = shallowMount(NotePreview, {
apolloProvider: createMockApollo([[noteQuery, noteQueryHandler]]),
propsData: {
noteId,
},
});
};
const findNoteableNote = () => wrapper.findComponent(NoteableNote);
it('does nothing if URL does not contain a note id', () => {
createComponent({ noteId: null });
expect(noteQueryHandler).not.toHaveBeenCalled();
expect(wrapper.html()).toBe('');
});
it('does nothing if URL links to a system note', () => {
createComponent({
noteId: '50f036b11addf3c1dc3d4b43a96cfeb799ae2f7c',
});
expect(noteQueryHandler).not.toHaveBeenCalled();
expect(wrapper.html()).toBe('');
});
it('renders a note', async () => {
createComponent({ noteId: '1234' });
await waitForPromises();
expect(findNoteableNote().exists()).toBe(true);
expect(findNoteableNote().props('showReplyButton')).toBe(false);
});
});

View File

@ -15,6 +15,7 @@ import notesEventHub from '~/notes/event_hub';
import CommentForm from '~/notes/components/comment_form.vue';
import NotesApp from '~/notes/components/notes_app.vue';
import NotesActivityHeader from '~/notes/components/notes_activity_header.vue';
import NotePreview from '~/notes/components/note_preview.vue';
import * as constants from '~/notes/constants';
import createStore from '~/notes/stores';
import OrderedLayout from '~/vue_shared/components/ordered_layout.vue';
@ -22,6 +23,9 @@ import OrderedLayout from '~/vue_shared/components/ordered_layout.vue';
import * as mockData from '../mock_data';
jest.mock('~/behaviors/markdown/render_gfm');
jest.mock('~/lib/utils/resize_observer', () => ({
scrollToTargetOnResize: jest.fn(),
}));
const TYPE_COMMENT_FORM = 'comment-form';
const TYPE_NOTES_LIST = 'notes-list';
@ -316,13 +320,13 @@ describe('note_app', () => {
return waitForPromises();
});
it('should listen hashchange event', () => {
const hash = 'some dummy hash';
it('should listen hashchange event for notes', () => {
const hash = 'note_1234';
jest.spyOn(urlUtility, 'getLocationHash').mockReturnValue(hash);
const dispatchMock = jest.spyOn(store, 'dispatch');
window.dispatchEvent(new Event('hashchange'), hash);
expect(dispatchMock).toHaveBeenCalledWith('setTargetNoteHash', 'some dummy hash');
expect(dispatchMock).toHaveBeenCalledWith('setTargetNoteHash', 'note_1234');
});
});
@ -375,6 +379,30 @@ describe('note_app', () => {
});
});
describe('preview note shown inside skeleton notes', () => {
it.each`
urlHash | exists
${''} | ${false}
${'note_123'} | ${true}
`('url is `$urlHash`', ({ urlHash, exists }) => {
jest.spyOn(urlUtility, 'getLocationHash').mockReturnValue(urlHash);
store = createStore();
store.state.isLoading = true;
store.state.targetNoteHash = urlHash;
wrapper = shallowMount(NotesApp, {
propsData,
store,
stubs: {
'ordered-layout': OrderedLayout,
},
});
expect(wrapper.findComponent(NotePreview).exists()).toBe(exists);
});
});
describe('when multiple draft types are present', () => {
beforeEach(() => {
store = createStore();

View File

@ -45,7 +45,6 @@ describe('Work item add note', () => {
workItemResponse = workItemByIidResponseFactory({ canUpdate, canCreateNote }),
signedIn = true,
isEditing = true,
isGroup = false,
workItemType = 'Task',
isInternalThread = false,
isNewDiscussion = false,
@ -66,9 +65,6 @@ describe('Work item add note', () => {
[workItemByIidQuery, workItemResponseHandler],
[createNoteMutation, mutationHandler],
]),
provide: {
isGroup,
},
propsData: {
fullPath: 'test-project-path',
workItemId: id,

View File

@ -48,15 +48,11 @@ describe('Work item comment form component', () => {
isNewDiscussion = false,
workItemState = STATE_OPEN,
workItemType = 'Task',
isGroup = false,
hasReplies = false,
isDiscussionResolved = false,
isDiscussionResolvable = false,
} = {}) => {
wrapper = shallowMountExtended(WorkItemCommentForm, {
provide: {
isGroup,
},
propsData: {
fullPath: 'test-project-path',
workItemIid: '1',

View File

@ -90,7 +90,6 @@ describe('Work Item Note', () => {
const createComponent = ({
note = mockWorkItemCommentNote,
isFirstNote = false,
isGroup = false,
updateNoteMutationHandler = successHandler,
workItemId = mockWorkItemId,
updateWorkItemMutationHandler = updateWorkItemMutationSuccessHandler,
@ -98,9 +97,6 @@ describe('Work Item Note', () => {
workItemByIidResponseHandler = workItemResponseHandler,
} = {}) => {
wrapper = shallowMount(WorkItemNote, {
provide: {
isGroup,
},
propsData: {
fullPath: 'test-project-path',
workItemId,
@ -155,7 +151,7 @@ describe('Work Item Note', () => {
});
it('should show the awards list when in edit mode', async () => {
createComponent({ note: mockWorkItemCommentNote, workItemsAlpha: true });
createComponent({ note: mockWorkItemCommentNote });
findNoteActions().vm.$emit('startEditing');
await nextTick();
expect(findAwardsList().exists()).toBe(true);
@ -438,7 +434,7 @@ describe('Work Item Note', () => {
});
it('passes note props to awards list', () => {
createComponent({ note: mockWorkItemCommentNote, workItemsAlpha: true });
createComponent({ note: mockWorkItemCommentNote });
expect(findAwardsList().props('note')).toBe(mockWorkItemCommentNote);
expect(findAwardsList().props('workItemIid')).toBe('1');

View File

@ -148,10 +148,6 @@ describe('WorkItemActions component', () => {
hideSubscribe,
hasChildren,
},
provide: {
isGroup: false,
glFeatures: { workItemsBeta: true, workItemsAlpha: true },
},
mocks: {
$toast,
},

View File

@ -55,7 +55,6 @@ describe('WorkItemAttributesWrapper component', () => {
const createComponent = ({
workItem = workItemQueryResponse.data.workItem,
workItemsBeta = false,
workItemsAlpha = false,
groupPath = '',
workItemParticipantsQueryHandler = workItemParticipantsQuerySuccessHandler,
@ -72,7 +71,6 @@ describe('WorkItemAttributesWrapper component', () => {
provide: {
hasSubepicsFeature: true,
glFeatures: {
workItemsBeta,
workItemsAlpha,
},
},

View File

@ -32,7 +32,6 @@ describe('WorkItemCreatedUpdated component', () => {
confidential = false,
discussionLocked = false,
updateInProgress = false,
isGroup = false,
} = {}) => {
const workItemQueryResponse = workItemByIidResponseFactory({
author,
@ -45,9 +44,6 @@ describe('WorkItemCreatedUpdated component', () => {
wrapper = shallowMount(WorkItemCreatedUpdated, {
apolloProvider: createMockApollo([[workItemByIidQuery, successHandler]]),
provide: {
isGroup,
},
propsData: {
fullPath: '/some/project',
workItemIid,

View File

@ -29,7 +29,6 @@ describe('WorkItemDescriptionRendered', () => {
workItemDescription = defaultWorkItemDescription,
canEdit = false,
mockComputed = {},
hasWorkItemsBeta = false,
} = {}) => {
wrapper = shallowMount(WorkItemDescriptionRendered, {
propsData: {
@ -40,7 +39,6 @@ describe('WorkItemDescriptionRendered', () => {
computed: mockComputed,
provide: {
fullPath: 'full/path',
workItemsBeta: hasWorkItemsBeta,
},
});
};

View File

@ -109,12 +109,10 @@ describe('WorkItemDetail component', () => {
mutationHandler,
error = undefined,
workItemsAlphaEnabled = false,
workItemsBeta = false,
namespaceLevelWorkItems = true,
hasSubepicsFeature = true,
router = true,
modalIsGroup = null,
isGroup = false,
} = {}) => {
wrapper = shallowMountExtended(WorkItemDetail, {
apolloProvider: createMockApollo([
@ -141,14 +139,12 @@ describe('WorkItemDetail component', () => {
provide: {
glFeatures: {
workItemsAlpha: workItemsAlphaEnabled,
workItemsBeta,
namespaceLevelWorkItems,
},
hasSubepicsFeature,
fullPath: 'group/project',
groupPath: 'group',
reportAbusePath: '/report/abuse/path',
isGroup,
},
stubs: {
WorkItemAncestors: true,

View File

@ -132,7 +132,6 @@ describe('WorkItemDevelopment CE', () => {
.mockResolvedValue(closedWorkItemWithAutoCloseFlagEnabled);
const createComponent = ({
isGroup = false,
workItemId = 'gid://gitlab/WorkItem/1',
workItemIid = '1',
workItemFullPath = 'full-path',
@ -153,7 +152,6 @@ describe('WorkItemDevelopment CE', () => {
workItemFullPath,
},
provide: {
isGroup,
glFeatures: {
workItemsAlpha: workItemsAlphaEnabled,
},

View File

@ -1,5 +1,4 @@
import { GlDrawer, GlLink } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import Vue from 'vue';
import VueApollo from 'vue-apollo';
@ -10,6 +9,7 @@ import { TYPE_EPIC, TYPE_ISSUE } from '~/issues/constants';
import WorkItemDrawer from '~/work_items/components/work_item_drawer.vue';
import WorkItemDetail from '~/work_items/components/work_item_detail.vue';
import deleteWorkItemMutation from '~/work_items/graphql/delete_work_item.mutation.graphql';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
Vue.use(VueApollo);
@ -27,10 +27,10 @@ describe('WorkItemDrawer', () => {
const createComponent = ({
open = false,
activeItem = { iid: '1', webUrl: 'test' },
activeItem = { iid: '1', webUrl: 'test', fullPath: 'gitlab-org/gitlab' },
issuableType = TYPE_ISSUE,
} = {}) => {
wrapper = shallowMount(WorkItemDrawer, {
wrapper = shallowMountExtended(WorkItemDrawer, {
propsData: {
activeItem,
open,
@ -44,7 +44,6 @@ describe('WorkItemDrawer', () => {
reportAbusePath: '',
groupPath: '',
hasSubepicsFeature: false,
isGroup: false,
},
apolloProvider: createMockApollo([[deleteWorkItemMutation, deleteWorkItemMutationHandler]]),
});
@ -56,10 +55,26 @@ describe('WorkItemDrawer', () => {
expect(findGlDrawer().props('open')).toBe(false);
});
it('displays correct URL in link', () => {
it('displays correct URL and text in link', () => {
createComponent();
expect(wrapper.findComponent(GlLink).attributes('href')).toBe('test');
const link = wrapper.findComponent(GlLink);
expect(link.attributes('href')).toBe('test');
expect(link.text()).toBe('gitlab#1');
});
it('displays the correct URL in the full page button', () => {
createComponent();
expect(wrapper.findByTestId('work-item-drawer-link-button').attributes('href')).toBe('test');
});
it('has a copy to clipboard button for the item URL', () => {
createComponent();
expect(
wrapper.findByTestId('work-item-drawer-copy-button').attributes('data-clipboard-text'),
).toBe('test');
});
it('emits `close` event when drawer is closed', () => {

View File

@ -87,9 +87,6 @@ describe('WorkItemChildrenWrapper', () => {
wrapper = shallowMountExtended(WorkItemChildrenWrapper, {
apolloProvider: mockApollo,
provide: {
isGroup: false,
},
propsData: {
fullPath: 'test/project',
workItemType,

View File

@ -64,13 +64,9 @@ describe('WorkItemActions component', () => {
workItemType,
workItemReference,
},
provide: {
glFeatures: { workItemsBeta: true, workItemsAlpha: true },
},
mocks: {
$toast,
},
stubs: {},
});
};

View File

@ -49,7 +49,6 @@ describe('Work Item State toggle button component', () => {
workItemState = STATE_OPEN,
workItemType = 'Task',
hasComment = false,
isGroup = false,
} = {}) => {
wrapper = shallowMount(WorkItemStateToggle, {
apolloProvider: createMockApollo([
@ -57,9 +56,6 @@ describe('Work Item State toggle button component', () => {
[workItemByIidQuery, querySuccessHander],
[workItemLinkedItemsQuery, workItemLinkedItemsHandler],
]),
provide: {
isGroup,
},
propsData: {
workItemId: id,
workItemIid: iid,

View File

@ -31,7 +31,6 @@ describe('WorkItemTimeTracking component', () => {
},
provide: {
fullPath: 'gitlab-org/gitlab',
isGroup: false,
},
stubs: {
GlProgressBar: true,

View File

@ -84,9 +84,6 @@ describe('WorkItemTodo component', () => {
workItemFullpath: mockWorkItemFullpath,
currentUserTodos,
},
provide: {
isGroup: false,
},
});
};

View File

@ -452,7 +452,7 @@ RSpec.describe Issuable, feature_category: :team_planning do
end
it 'skips coercion for not Integer values' do
expect { issue.time_estimate = nil }.to change { issue.time_estimate }.to(nil)
expect { issue.time_estimate = nil }.to change { issue.read_attribute(:time_estimate) }.to(nil)
expect { issue.time_estimate = 'invalid time' }.not_to raise_error
expect { issue.time_estimate = 22.33 }.not_to raise_error
end

View File

@ -2060,6 +2060,25 @@ RSpec.describe Issue, feature_category: :team_planning do
end
end
describe '#time_estimate' do
let_it_be(:project) { create(:project) }
let_it_be(:issue) { create(:issue, project: project) }
context 'when time estimate on the issue record is NULL' do
before do
issue.update_column(:time_estimate, nil)
end
it 'sets time estimate to zeor on save' do
expect(issue.read_attribute(:time_estimate)).to be_nil
issue.save!
expect(issue.reload.read_attribute(:time_estimate)).to eq(0)
end
end
end
describe '#supports_move_and_clone?' do
let_it_be(:project) { create(:project) }
let_it_be_with_refind(:issue) { create(:incident, project: project) }

View File

@ -13,14 +13,14 @@ RSpec.describe ProjectStatistics do
describe 'scopes' do
describe '.for_project_ids' do
it 'returns only requested projects', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/448381' do
it 'returns only requested projects' do
stats = create_list(:project_statistics, 3)
project_ids = stats[0..1].map(&:project_id)
expected_ids = stats[0..1].map(&:id)
requested_stats = described_class.for_project_ids(project_ids).pluck(:id)
expect(requested_stats).to eq(expected_ids)
expect(requested_stats).to match_array(expected_ids)
end
end
end

View File

@ -472,8 +472,7 @@ RSpec.describe Issues::CreateService, feature_category: :team_planning do
iid: { current: kind_of(Integer), previous: nil },
project_id: { current: project.id, previous: nil },
title: { current: opts[:title], previous: nil },
updated_at: { current: kind_of(Time), previous: nil },
time_estimate: { current: 0, previous: nil }
updated_at: { current: kind_of(Time), previous: nil }
},
object_attributes: include(
opts.merge(

View File

@ -12,7 +12,6 @@
- ee/spec/services/ee/users/create_service_spec.rb
- ee/spec/services/users/service_accounts/create_service_spec.rb
- ee/spec/services/epics/update_dates_service_spec.rb
- ee/spec/services/gitlab_subscriptions/trials/create_service_spec.rb
- spec/controllers/admin/groups_controller_spec.rb
- spec/controllers/admin/users_controller_spec.rb
- spec/controllers/groups_controller_spec.rb

View File

@ -49,12 +49,6 @@ mapping:
- source: 'config/initializers/(?<rest>.+)\.rb'
test: 'spec/initializers/%{rest}_spec.rb'
# Snowplow definitions map to schema validation spec
- source:
- '(?<prefix>ee/)?config/events/.+\.yml'
- 'config/events/schema\.json'
test: 'spec/lib/gitlab/tracking/event_definition_validator_validate_all_spec.rb'
# DB structure should map to schema spec
- source: 'db/structure\.sql'
test: 'spec/db/schema_spec.rb'
@ -136,13 +130,46 @@ mapping:
test:
- 'ee/spec/requests/api/graphql/remote_development/workspace/with_id_arg_spec.rb'
# Any change to metrics definition should trigger the specs in the ee/spec/config/metrics/ folder.
#
# Note: We only have those tests for ee, even though we have non-ee metrics.
#
# Usage metric schema changes should trigger validations for all metrics and tooling
- source: 'config/metrics/schema/.*\.json'
test:
- 'spec/config/metrics/every_metric_definition_spec.rb'
- 'ee/spec/config/metrics/every_metric_definition_spec.rb'
- 'spec/lib/gitlab/usage/metric_definition_validate_all_spec.rb'
- 'spec/scripts/internal_events/cli_spec.rb'
- 'spec/support_specs/matchers/internal_events_matchers_spec.rb'
# Internal events schema changes should trigger validations for all events and tooling
- source: 'config/events/schema\.json'
test:
- 'spec/lib/gitlab/tracking/event_definition_validator_validate_all_spec.rb'
- 'spec/scripts/internal_events/cli_spec.rb'
- 'spec/support_specs/matchers/internal_events_matchers_spec.rb'
# Any change to metric/event definitions should trigger the specs in the ee/spec/config/metrics/ folder.
# See https://gitlab.com/gitlab-org/quality/engineering-productivity/master-broken-incidents/-/issues/287#note_1192008962
- source: 'ee/config/metrics/.*\.yml'
test: 'ee/spec/config/metrics/every_metric_definition_spec.rb'
- source:
- '(ee/)?config/metrics/.*\.yml'
test:
- 'spec/config/metrics/every_metric_definition_spec.rb'
- 'ee/spec/config/metrics/every_metric_definition_spec.rb'
- 'spec/lib/gitlab/usage/metric_definition_validate_all_spec.rb'
# Internal event/Snowplow event definitions map to schema validation spec
- source:
- '(ee/)?config/events/.+\.yml'
test: 'spec/lib/gitlab/tracking/event_definition_validator_validate_all_spec.rb'
# Changes to Internal Events behavior should trigger tests for the CLI and shared matchers
- source:
- 'lib/gitlab/internal_events\.rb'
- 'scripts/internal_events/cli(/.+)?\.rb'
- 'spec/support/shared_examples/controllers/internal_event_tracking_examples\.rb'
- 'spec/support/matchers/internal_events_matchers\.rb'
- 'spec/fixtures/scripts/internal_events/.+\.yml'
test:
- 'spec/scripts/internal_events/cli_spec.rb'
- 'spec/support_specs/matchers/internal_events_matchers_spec.rb'
# See https://gitlab.com/gitlab-org/quality/engineering-productivity/team/-/issues/146
- source: 'config/feature_categories\.yml'

View File

@ -1315,10 +1315,10 @@
core-js "^3.29.1"
mitt "^3.0.1"
"@gitlab/eslint-plugin@20.2.0":
version "20.2.0"
resolved "https://registry.yarnpkg.com/@gitlab/eslint-plugin/-/eslint-plugin-20.2.0.tgz#12cefe534ef3204b800c4793ae5fbbd2d34f7d50"
integrity sha512-euERL6TPP9GrUCXJqZ3KvUsBdhh88pBtTxQMZa3RuRL+TwMBskwCZY+XQnXKKwiz55olZU6ZbUUN0NE9G4Ox0g==
"@gitlab/eslint-plugin@20.2.1":
version "20.2.1"
resolved "https://registry.yarnpkg.com/@gitlab/eslint-plugin/-/eslint-plugin-20.2.1.tgz#2414f621ef3d88e90405436460febbfb247b4f91"
integrity sha512-n0ISWpRI/h6ZO8bfX10EY4u3y31Pdo9T2ieaRo1dSaC0nz7ZXHd23ykJ2gQDmYogjh6EHGS/SiqL1chMA4C+eQ==
dependencies:
"@typescript-eslint/eslint-plugin" "^7.14.1"
eslint-config-airbnb-base "^15.0.0"
@ -13104,7 +13104,16 @@ string-length@^4.0.1:
char-regex "^1.0.2"
strip-ansi "^6.0.0"
"string-width-cjs@npm:string-width@^4.2.0", "string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
"string-width-cjs@npm:string-width@^4.2.0":
version "4.2.3"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
dependencies:
emoji-regex "^8.0.0"
is-fullwidth-code-point "^3.0.0"
strip-ansi "^6.0.1"
"string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
version "4.2.3"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
@ -13157,7 +13166,7 @@ string_decoder@^1.0.0, string_decoder@^1.1.1, string_decoder@~1.1.1:
dependencies:
safe-buffer "~5.1.0"
"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1:
"strip-ansi-cjs@npm:strip-ansi@^6.0.1":
version "6.0.1"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
@ -13171,6 +13180,13 @@ strip-ansi@^5.2.0:
dependencies:
ansi-regex "^4.1.0"
strip-ansi@^6.0.0, strip-ansi@^6.0.1:
version "6.0.1"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
dependencies:
ansi-regex "^5.0.1"
strip-ansi@^7.0.1, strip-ansi@^7.1.0:
version "7.1.0"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.1.0.tgz#d5b6568ca689d8561370b0707685d22434faff45"
@ -14862,7 +14878,7 @@ worker-loader@^3.0.8:
loader-utils "^2.0.0"
schema-utils "^3.0.0"
"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0:
"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0":
version "7.0.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
@ -14880,6 +14896,15 @@ wrap-ansi@^6.2.0:
string-width "^4.1.0"
strip-ansi "^6.0.0"
wrap-ansi@^7.0.0:
version "7.0.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
dependencies:
ansi-styles "^4.0.0"
string-width "^4.1.0"
strip-ansi "^6.0.0"
wrap-ansi@^8.1.0:
version "8.1.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214"