Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2025-06-23 18:12:30 +00:00
parent c922904e97
commit c5198a0a78
60 changed files with 522 additions and 1523 deletions

View File

@ -1 +1 @@
31965b083fa3accbc788af7cda2e69359879b221
889fff0a39dc1d79dc585f345e4fd82105b57a7f

View File

@ -27,6 +27,12 @@ export default class ActionCableLink extends ApolloLink {
observer.complete();
}
},
// we want to react to websocket reconnection events to update data on stale browser tabs
connected({ reconnected }) {
if (reconnected) {
document.dispatchEvent(new CustomEvent('actioncable:reconnected'));
}
},
},
);

View File

@ -1,83 +0,0 @@
<script>
import { GlIcon, GlDisclosureDropdown, GlButton } from '@gitlab/ui';
import { mapState, mapActions } from 'pinia';
// eslint-disable-next-line no-restricted-imports
import { mapGetters as mapVuexGetters } from 'vuex';
import { setUrlParams, visitUrl } from '~/lib/utils/url_utility';
import { useBatchComments } from '~/batch_comments/store';
import { useLegacyDiffs } from '~/diffs/stores/legacy_diffs';
import PreviewItem from './preview_item.vue';
import DraftsCount from './drafts_count.vue';
export default {
components: {
GlIcon,
PreviewItem,
DraftsCount,
GlDisclosureDropdown,
GlButton,
},
computed: {
...mapState(useLegacyDiffs, ['viewDiffsFileByFile']),
...mapState(useBatchComments, ['draftsCount', 'sortedDrafts']),
...mapVuexGetters(['getNoteableData']),
listItems() {
const sortedDraftCount = this.sortedDrafts.length - 1;
return this.sortedDrafts.map((item, index) => ({
text: item.id.toString(),
action: () => {
this.onClickDraft(item);
},
last: index === sortedDraftCount,
...item,
}));
},
},
methods: {
...mapActions(useLegacyDiffs, ['goToFile']),
...mapActions(useBatchComments, ['scrollToDraft']),
isOnLatestDiff(draft) {
return draft.position?.head_sha === this.getNoteableData.diff_head_sha;
},
async onClickDraft(draft) {
if (this.viewDiffsFileByFile) {
await this.goToFile({ path: draft.file_path });
}
if (draft.position && !this.isOnLatestDiff(draft)) {
const url = new URL(setUrlParams({ commit_id: draft.position.head_sha }));
url.hash = `note_${draft.id}`;
visitUrl(url.toString());
} else {
await this.scrollToDraft(draft);
}
},
},
};
</script>
<template>
<gl-disclosure-dropdown :items="listItems" dropup data-testid="review-preview-dropdown">
<template #toggle>
<gl-button>
{{ __('Pending comments') }}
<drafts-count variant="neutral" />
<gl-icon class="dropdown-chevron" name="chevron-up" />
</gl-button>
</template>
<template #header>
<div
class="gl-flex gl-min-h-8 gl-items-center gl-border-b-1 gl-border-b-dropdown !gl-p-4 gl-border-b-solid"
>
<span class="gl-grow gl-pr-2 gl-text-sm gl-font-bold">
{{ n__('%d pending comment', '%d pending comments', draftsCount) }}
</span>
</div>
</template>
<template #list-item="{ item }">
<preview-item :draft="item" />
</template>
</gl-disclosure-dropdown>
</template>

View File

@ -3,7 +3,6 @@ import { GlSprintf, GlIcon, GlButton } from '@gitlab/ui';
// eslint-disable-next-line no-restricted-imports
import { mapGetters } from 'vuex';
import { mapState } from 'pinia';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { IMAGE_DIFF_POSITION_TYPE } from '~/diffs/constants';
import { sprintf, __ } from '~/locale';
import {
@ -20,7 +19,7 @@ export default {
GlSprintf,
GlButton,
},
mixins: [resolvedStatusMixin, glFeatureFlagsMixin()],
mixins: [resolvedStatusMixin],
props: {
draft: {
type: Object,
@ -101,23 +100,14 @@ export default {
<template>
<span>
<component
:is="glFeatures.improvedReviewExperience ? 'gl-button' : 'span'"
:variant="glFeatures.improvedReviewExperience ? 'link' : undefined"
class="review-preview-item-header"
:class="{ '!gl-justify-start': glFeatures.improvedReviewExperience }"
<gl-button
variant="link"
class="review-preview-item-header !gl-justify-start"
data-testid="preview-item-header"
@click="$emit('click', draft)"
>
<gl-icon class="flex-shrink-0" :name="iconName" /><span
class="text-nowrap gl-items-center"
:class="{ 'gl-font-bold': !glFeatures.improvedReviewExperience }"
>
<span
class="review-preview-item-header-text block-truncated"
:class="{ 'gl-ml-2': !glFeatures.improvedReviewExperience }"
>{{ titleText }}</span
>
<gl-icon class="flex-shrink-0" :name="iconName" /><span class="text-nowrap gl-items-center">
<span class="review-preview-item-header-text block-truncated">{{ titleText }}</span>
<template v-if="showLinePosition">
<template v-if="startLineNumber === endLineNumber">
:<span :class="getLineClasses(startLineNumber)">{{ startLineNumber }}</span>
@ -136,7 +126,7 @@ export default {
</gl-sprintf>
</template>
</span>
</component>
</gl-button>
<span class="review-preview-item-content">
<p>{{ content }}</p>
</span>

View File

@ -1,102 +0,0 @@
<script>
import { mapActions } from 'pinia';
// eslint-disable-next-line no-restricted-imports
import { mapGetters as mapVuexGetters } from 'vuex';
import { GlButton, GlTooltipDirective as GlTooltip, GlModal } from '@gitlab/ui';
import { __ } from '~/locale';
import toast from '~/vue_shared/plugins/global_toast';
import { SET_REVIEW_BAR_RENDERED } from '~/batch_comments/stores/modules/batch_comments/mutation_types';
import { useBatchComments } from '~/batch_comments/store';
import { REVIEW_BAR_VISIBLE_CLASS_NAME } from '../constants';
import PreviewDropdown from './preview_dropdown.vue';
import SubmitDropdown from './submit_dropdown.vue';
export default {
components: {
GlModal,
GlButton,
PreviewDropdown,
SubmitDropdown,
},
directives: {
GlTooltip,
},
data() {
return {
discarding: false,
showDiscardModal: false,
};
},
computed: {
...mapVuexGetters(['isNotesFetched']),
},
watch: {
isNotesFetched() {
if (this.isNotesFetched) {
this.expandAllDiscussions();
}
},
},
mounted() {
document.body.classList.add(REVIEW_BAR_VISIBLE_CLASS_NAME);
useBatchComments()[SET_REVIEW_BAR_RENDERED]();
},
beforeDestroy() {
document.body.classList.remove(REVIEW_BAR_VISIBLE_CLASS_NAME);
},
methods: {
...mapActions(useBatchComments, ['expandAllDiscussions', 'discardDrafts']),
async discardReviews() {
this.discarding = true;
try {
await this.discardDrafts();
toast(__('Review discarded'));
} finally {
this.discarding = false;
}
},
},
modal: {
cancelAction: { text: __('Keep review') },
primaryAction: { text: __('Discard review'), attributes: { variant: 'danger' } },
},
};
</script>
<template>
<nav class="review-bar-component js-review-bar" data-testid="review_bar_component">
<div class="review-bar-content gl-flex gl-justify-end" data-testid="review-bar-content">
<gl-button
v-gl-tooltip
icon="remove"
variant="danger"
category="tertiary"
class="gl-mr-3"
:title="__('Discard review')"
:aria-label="__('Discard review')"
:loading="discarding"
data-testid="discard-review-btn"
@click="showDiscardModal = true"
/>
<preview-dropdown />
<submit-dropdown />
</div>
<gl-modal
v-model="showDiscardModal"
modal-id="discard-review-modal"
:title="__('Discard pending review?')"
:action-primary="$options.modal.primaryAction"
:action-cancel="$options.modal.cancelAction"
data-testid="discard-review-modal"
@primary="discardReviews"
>
{{
__(
'Are you sure you want to discard your pending review comments? This action cannot be undone.',
)
}}
</gl-modal>
</nav>
</template>

View File

@ -1,333 +0,0 @@
<script>
import {
GlDisclosureDropdown,
GlButton,
GlIcon,
GlForm,
GlFormRadioGroup,
GlLoadingIcon,
} from '@gitlab/ui';
import { mapActions, mapState } from 'pinia';
// eslint-disable-next-line no-restricted-imports
import { mapGetters as mapVuexGetters } from 'vuex';
import { __ } from '~/locale';
import { createAlert } from '~/alert';
import MarkdownEditor from '~/vue_shared/components/markdown/markdown_editor.vue';
import MarkdownHeaderDivider from '~/vue_shared/components/markdown/header_divider.vue';
import { scrollToElement } from '~/lib/utils/common_utils';
import { fetchPolicies } from '~/lib/graphql';
import { CLEAR_AUTOSAVE_ENTRY_EVENT, CONTENT_EDITOR_PASTE } from '~/vue_shared/constants';
import markdownEditorEventHub from '~/vue_shared/components/markdown/eventhub';
import { trackSavedUsingEditor } from '~/vue_shared/components/markdown/tracking';
import { updateText } from '~/lib/utils/text_markdown';
import { useBatchComments } from '~/batch_comments/store';
import { useLegacyDiffs } from '~/diffs/stores/legacy_diffs';
import userCanApproveQuery from '../queries/can_approve.query.graphql';
export default {
apollo: {
userPermissions: {
fetchPolicy: fetchPolicies.NETWORK_ONLY,
query: userCanApproveQuery,
variables() {
return {
projectPath: this.projectPath.replace(/^\//, ''),
iid: `${this.getNoteableData.iid}`,
};
},
update: (data) => data.project?.mergeRequest?.userPermissions,
skip() {
return !this.dropdownVisible;
},
},
},
components: {
GlDisclosureDropdown,
GlButton,
GlIcon,
GlForm,
GlFormRadioGroup,
GlLoadingIcon,
MarkdownEditor,
MarkdownHeaderDivider,
ApprovalPassword: () => import('ee_component/batch_comments/components/approval_password.vue'),
SummarizeMyReview: () =>
import('ee_component/batch_comments/components/summarize_my_review.vue'),
},
inject: {
canSummarize: { default: false },
},
data() {
return {
isSubmitting: false,
summarizeReviewLoading: false,
dropdownVisible: false,
noteData: {
noteable_type: '',
noteable_id: '',
note: '',
approve: false,
approval_password: '',
reviewer_state: 'reviewed',
},
formFieldProps: {
id: 'review-note-body',
name: 'review[note]',
placeholder: __('Write a comment or drag your files here…'),
'aria-label': __('Comment'),
'data-testid': 'comment-textarea',
},
userPermissions: {},
};
},
computed: {
...mapVuexGetters([
'getNotesData',
'getNoteableData',
'noteableType',
'getCurrentUserLastNote',
]),
...mapState(useBatchComments, ['shouldAnimateReviewButton']),
...mapState(useLegacyDiffs, ['projectPath']),
autocompleteDataSources() {
return gl.GfmAutoComplete?.dataSources;
},
autosaveKey() {
return `submit_review_dropdown/${this.getNoteableData.id}`;
},
radioGroupOptions() {
return [
{
html: [
__('Comment'),
`<p class="help-text">
${__('Submit general feedback without explicit approval.')}
</p>`,
].join('<br />'),
value: 'reviewed',
},
{
html: [
__('Approve'),
`<p class="help-text">
${__('Submit feedback and approve these changes.')}
</p>`,
].join('<br />'),
value: 'approved',
disabled: !this.userPermissions.canApprove,
},
{
html: [
__('Request changes'),
`<p class="help-text">
${__('Submit feedback that should be addressed before merging.')}
</p>`,
].join('<br />'),
value: 'requested_changes',
},
];
},
},
watch: {
dropdownVisible(val) {
if (!val) {
this.userPermissions = {};
}
},
userPermissions: {
handler() {
this.repositionDropdown();
},
deep: true,
},
},
mounted() {
this.noteData.noteable_type = this.noteableType;
this.noteData.noteable_id = this.getNoteableData.id;
},
methods: {
...mapActions(useBatchComments, ['publishReview', 'clearDrafts']),
repositionDropdown() {
this.$refs.submitDropdown?.$refs.dropdown?.updatePopper();
},
async submitReview() {
this.isSubmitting = true;
if (this.userLastNoteWatcher) this.userLastNoteWatcher();
trackSavedUsingEditor(this.$refs.markdownEditor.isContentEditorActive, 'MergeRequest_review');
try {
const { note, reviewer_state: reviewerState } = this.noteData;
await this.publishReview({ ...this.noteData });
markdownEditorEventHub.$emit(CLEAR_AUTOSAVE_ENTRY_EVENT, this.autosaveKey);
this.noteData.note = '';
this.noteData.reviewer_state = 'reviewed';
this.noteData.approval_password = '';
if (note) {
this.userLastNoteWatcher = this.$watch(
'getCurrentUserLastNote',
() => {
if (note) {
window.location.hash = `note_${this.getCurrentUserLastNote.id}`;
}
window.mrTabs?.tabShown('show');
setTimeout(() => {
scrollToElement(document.getElementById(`note_${this.getCurrentUserLastNote.id}`));
this.clearDrafts();
});
this.userLastNoteWatcher();
},
{ deep: true },
);
} else {
if (reviewerState === 'approved') {
window.mrTabs?.tabShown('show');
}
this.clearDrafts();
}
} catch (e) {
if (e.data?.message) {
createAlert({ message: e.data.message, captureError: true });
}
this.isSubmitting = false;
}
},
updateNote(note) {
const textArea = this.$el.querySelector('textarea');
if (textArea) {
updateText({
textArea,
tag: note,
cursorOffset: 0,
wrap: false,
});
} else {
markdownEditorEventHub.$emit(CONTENT_EDITOR_PASTE, note);
}
},
onBeforeClose({ originalEvent: { target }, preventDefault }) {
if (
target &&
[
document.querySelector('.atwho-container'),
document.querySelector('.dz-hidden-input'),
document.querySelector('.comment-templates-modal'),
]
.filter(Boolean)
.some((el) => el.contains(target))
) {
preventDefault();
}
},
setDropdownVisible(val) {
this.dropdownVisible = val;
},
},
restrictedToolbarItems: ['full-screen'],
};
</script>
<template>
<gl-disclosure-dropdown
ref="submitDropdown"
placement="bottom-end"
class="submit-review-dropdown"
:class="{ 'submit-review-dropdown-animated': shouldAnimateReviewButton }"
data-testid="review-drawer-toggle"
fluid-width
@beforeClose="onBeforeClose"
@shown="setDropdownVisible(true)"
@hidden="setDropdownVisible(false)"
>
<template #toggle>
<gl-button variant="confirm" category="primary">
{{ __('Finish review') }}
<gl-icon class="dropdown-chevron" name="chevron-up" />
</gl-button>
</template>
<template #default>
<gl-form
class="submit-review-dropdown-form gl-p-4"
data-testid="submit-gl-form"
@submit.prevent="submitReview"
>
<div class="gl-mb-4 gl-flex gl-items-center">
<label for="review-note-body" class="gl-mb-0">
{{ __('Summary comment (optional)') }}
</label>
</div>
<div class="common-note-form gfm-form">
<markdown-editor
ref="markdownEditor"
v-model="noteData.note"
class="js-no-autosize"
:is-submitting="isSubmitting"
:render-markdown-path="getNoteableData.preview_note_path"
:markdown-docs-path="getNotesData.markdownDocsPath"
:form-field-props="formFieldProps"
enable-autocomplete
:autocomplete-data-sources="autocompleteDataSources"
:disabled="isSubmitting"
:restricted-tool-bar-items="$options.restrictedToolbarItems"
:force-autosize="false"
:autosave-key="autosaveKey"
supports-quick-actions
@input="$emit('input', $event)"
@keydown.meta.enter="submitReview"
@keydown.ctrl.enter="submitReview"
>
<template v-if="canSummarize" #header-buttons>
<markdown-header-divider class="gl-ml-2" />
<summarize-my-review
:id="getNoteableData.id"
v-model="summarizeReviewLoading"
@input="updateNote"
/>
</template>
<template v-if="summarizeReviewLoading" #toolbar>
<div class="gl-ml-auto gl-mr-2 gl-inline-flex">
{{ __('Generating review summary') }}
<gl-loading-icon class="gl-ml-2 gl-mt-2" />
</div>
</template>
</markdown-editor>
</div>
<gl-form-radio-group
v-model="noteData.reviewer_state"
:options="radioGroupOptions"
class="gl-mt-4"
data-testid="reviewer_states"
/>
<approval-password
v-if="userPermissions.canApprove && getNoteableData.require_password_to_approve"
v-show="noteData.reviewer_state === 'approved'"
v-model="noteData.approval_password"
class="gl-mt-3"
data-testid="approve_password"
/>
<div class="gl-mt-4 gl-flex gl-justify-start">
<gl-button
:loading="isSubmitting"
variant="confirm"
type="submit"
class="js-no-auto-disable"
data-testid="submit-review-button"
>
{{ __('Submit review') }}
</gl-button>
</div>
</gl-form>
</template>
</gl-disclosure-dropdown>
</template>

View File

@ -13,8 +13,11 @@ export default {
computed: {
...mapState(useBatchComments, ['draftsCount', 'isReviewer', 'shouldAnimateReviewButton']),
},
mounted() {
this.fetchDrafts();
},
methods: {
...mapActions(useBatchComments, ['setDrawerOpened']),
...mapActions(useBatchComments, ['fetchDrafts', 'setDrawerOpened']),
},
};
</script>
@ -24,7 +27,9 @@ export default {
<gl-button
variant="confirm"
data-testid="review-drawer-toggle"
:class="{ 'submit-review-dropdown-animated': shouldAnimateReviewButton }"
:class="{
'motion-safe:gl-animate-[review-btn-animate_300ms_ease-in]': shouldAnimateReviewButton,
}"
@click="setDrawerOpened(true)"
>
{{ __('Your review') }}

View File

@ -1,5 +1,3 @@
export const CHANGES_TAB = 'diffs';
export const DISCUSSION_TAB = 'notes';
export const SHOW_TAB = 'show';
export const REVIEW_BAR_VISIBLE_CLASS_NAME = 'review-bar-visible';

View File

@ -1,45 +0,0 @@
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import { mapActions, mapState } from 'pinia';
import { parseBoolean } from '~/lib/utils/common_utils';
import { apolloProvider } from '~/graphql_shared/issuable_client';
import store from '~/mr_notes/stores';
import { pinia } from '~/pinia/instance';
import { useBatchComments } from '~/batch_comments/store';
export const initReviewBar = () => {
const el = document.getElementById('js-review-bar');
if (!el) return;
Vue.use(VueApollo);
// eslint-disable-next-line no-new
new Vue({
el,
store,
pinia,
apolloProvider,
components: {
ReviewBar: () => import('./components/review_bar.vue'),
},
provide: {
newCommentTemplatePaths: JSON.parse(el.dataset.newCommentTemplatePaths),
canSummarize: parseBoolean(el.dataset.canSummarize),
},
computed: {
...mapState(useBatchComments, ['draftsCount']),
},
mounted() {
this.fetchDrafts();
},
methods: {
...mapActions(useBatchComments, ['fetchDrafts']),
},
render(createElement) {
if (window.gon?.features?.improvedReviewExperience || this.draftsCount === 0) return null;
return createElement('review-bar');
},
});
};

View File

@ -17,7 +17,6 @@ export const useBatchComments = defineStore('batchComments', {
isPublishing: false,
currentlyPublishingDrafts: [],
shouldAnimateReviewButton: false,
reviewBarRendered: false,
isMergeRequest: false,
drawerOpened: false,
// TODO: this gets populated from the sidebar_reviewers.vue, we should have a separate store for this

View File

@ -68,9 +68,6 @@ export default {
[types.CLEAR_DRAFTS]() {
this.drafts = [];
},
[types.SET_REVIEW_BAR_RENDERED]() {
this.reviewBarRendered = true;
},
[types.SET_DRAFT_EDITING]({ draftId, isEditing }) {
const draftIndex = this.drafts.findIndex((draft) => draft.id === draftId);
const draft = this.drafts[draftIndex];

View File

@ -17,6 +17,4 @@ export const TOGGLE_RESOLVE_DISCUSSION = 'TOGGLE_RESOLVE_DISCUSSION';
export const CLEAR_DRAFTS = 'CLEAR_DRAFTS';
export const SET_REVIEW_BAR_RENDERED = 'SET_REVIEW_BAR_RENDERED';
export const SET_DRAFT_EDITING = 'SET_DRAFT_EDITING';

View File

@ -68,9 +68,6 @@ export default {
[types.CLEAR_DRAFTS](state) {
state.drafts = [];
},
[types.SET_REVIEW_BAR_RENDERED](state) {
state.reviewBarRendered = true;
},
[types.SET_DRAFT_EDITING](state, { draftId, isEditing }) {
const draftIndex = state.drafts.findIndex((draft) => draft.id === draftId);
const draft = state.drafts[draftIndex];

View File

@ -4,6 +4,5 @@ export default () => ({
isPublishing: false,
currentlyPublishingDrafts: [],
shouldAnimateReviewButton: false,
reviewBarRendered: false,
isMergeRequest: false,
});

View File

@ -45,7 +45,7 @@ export default {
{{ __('Job') }}
</div>
<div role="rowheader" class="table-section section-25">
{{ __('Duration') }}
{{ __('Execution time') }}
</div>
<div role="rowheader" class="table-section section-10 gl-text-center">
{{ __('Failed') }}
@ -93,7 +93,7 @@ export default {
<div class="table-section section-25">
<div role="rowheader" class="table-mobile-header gl-font-bold">
{{ __('Duration') }}
{{ __('Execution time') }}
</div>
<div class="table-mobile-content gl-text-left">
{{ testSuite.formattedTime }}

View File

@ -2,7 +2,6 @@
import { mapState } from 'pinia';
import StickyViewportFillerHeight from '~/diffs/components/sticky_viewport_filler_height.vue';
import { useBatchComments } from '~/batch_comments/store';
import { observeElementOnce } from '~/lib/utils/dom_utils';
export default {
name: 'FileBrowserHeight',
@ -18,7 +17,7 @@ export default {
};
},
computed: {
...mapState(useBatchComments, ['reviewBarRendered', 'draftsCount']),
...mapState(useBatchComments, ['draftsCount']),
reviewBarEnabled() {
return this.draftsCount > 0;
},
@ -28,17 +27,6 @@ export default {
: this.bottomPadding;
},
},
watch: {
reviewBarRendered: {
handler(rendered) {
if (!rendered || this.reviewBarCachedHeight) return;
observeElementOnce(document.querySelector('.js-review-bar'), ([bar]) => {
this.reviewBarCachedHeight = bar.boundingClientRect.height;
});
},
immediate: true,
},
},
mounted() {
const styles = getComputedStyle(this.$el);
const largeBreakpointSize = parseInt(styles.getPropertyValue('--breakpoint-lg'), 10);

View File

@ -55,6 +55,7 @@ export default {
<div class="stats gl-text-subtle">
<item-stats-value
v-if="displayValue(item.subgroupCount)"
tabindex="0"
:title="__('Subgroups')"
:value="subgroupCount"
css-class="number-subgroups gl-ml-5"
@ -63,6 +64,7 @@ export default {
/>
<item-stats-value
v-if="displayValue(item.projectCount)"
tabindex="0"
:title="__('Projects')"
:value="projectCount"
css-class="number-projects gl-ml-5"
@ -71,6 +73,7 @@ export default {
/>
<item-stats-value
v-if="displayValue(item.memberCount)"
tabindex="0"
:title="__('Direct members')"
:value="item.memberCount"
css-class="number-users gl-ml-5"
@ -79,6 +82,7 @@ export default {
/>
<item-stats-value
v-if="isProject"
tabindex="0"
:value="starCount"
css-class="project-stars"
data-testid="star-count"

View File

@ -791,6 +791,8 @@ export const isCurrentUser = (userId) => {
/**
* Clones an object via JSON stringifying and re-parsing.
* This ensures object references are not persisted (e.g. unlike lodash cloneDeep)
* See https://github.com/lodash/lodash/issues/4710#issuecomment-606892867 for details on cloneDeep circular references
* See https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm for the underlying mechanism used by Lodash
*/
export const cloneWithoutReferences = (obj) => {
return JSON.parse(JSON.stringify(obj));

View File

@ -275,7 +275,7 @@ export default {
issuable-type="merge_request"
/>
</template>
<submit-review-button v-if="glFeatures.improvedReviewExperience" />
<submit-review-button />
</div>
</div>
</div>

View File

@ -2,7 +2,6 @@ import { parseBoolean, getCookie } from '~/lib/utils/common_utils';
import store from '~/mr_notes/stores';
import { getLocationHash, getParameterValues } from '~/lib/utils/url_utility';
import eventHub from '~/notes/event_hub';
import { initReviewBar } from '~/batch_comments';
import { initDiscussionCounter } from '~/mr_notes/discussion_counter';
import { initOverviewTabCounter } from '~/mr_notes/init_count';
import { getDerivedMergeRequestInformation } from '~/diffs/utils/merge_request';
@ -66,7 +65,6 @@ export function initMrStateLazyLoad() {
eventHub.$once('fetchNotesData', () => store.dispatch('fetchNotes'));
requestIdleCallback(() => {
initReviewBar();
initOverviewTabCounter();
initDiscussionCounter();
});

View File

@ -81,8 +81,6 @@ const initMrStickyHeader = () => {
};
const initReviewDrawer = () => {
if (!window.gon?.features?.improvedReviewExperience) return;
// Review drawer has to be located outside the MR sticky/non-sticky header
// Otherwise it will disappear when header switches between sticky/non-sticky components
const el = document.querySelector('#js-review-drawer');

View File

@ -49,8 +49,6 @@ function getSidebarOptions(sidebarOptEl = document.querySelector('.js-sidebar-op
}
function mountSubmitReviewButton(pinia) {
if (!window.gon?.features?.improvedReviewExperience) return;
const el = document.querySelector('#js-submit-review-button');
// eslint-disable-next-line no-new

View File

@ -210,6 +210,7 @@ export default {
info: getParameterByName('resolves_discussion'),
showSidebar: true,
truncationEnabled: true,
lastRealtimeUpdatedAt: new Date(),
};
},
apollo: {
@ -591,8 +592,12 @@ export default {
: this.newCommentTemplatePaths;
},
},
beforeDestroy() {
document.removeEventListener('actioncable:reconnected', this.refetchIfStale);
},
mounted() {
addShortcutsExtension(ShortcutsWorkItems);
document.addEventListener('actioncable:reconnected', this.refetchIfStale);
},
methods: {
handleWorkItemCreated() {
@ -864,6 +869,14 @@ export default {
label: this.truncationEnabled.toString(), // New user truncation setting
});
},
refetchIfStale() {
const now = new Date();
const staleThreshold = 5 * 60 * 1000; // 5 minutes in milliseconds
if (now - this.lastRealtimeUpdatedAt > staleThreshold) {
this.$apollo.queries.workItem.refetch();
this.lastRealtimeUpdatedAt = now;
}
},
},
WORK_ITEM_TYPE_NAME_OBJECTIVE,
WORKSPACE_PROJECT,

View File

@ -25,10 +25,6 @@
--system-footer-height: #{$system-footer-height};
}
.review-bar-visible {
--mr-review-bar-height: #{$mr-review-bar-height};
}
@include media-breakpoint-up(sm) {
.right-sidebar-collapsed {
--application-bar-right: #{$right-sidebar-collapsed-width};

View File

@ -306,7 +306,7 @@
@mixin right-sidebar {
position: fixed;
bottom: calc(#{$calc-application-footer-height} + var(--mr-review-bar-height));
bottom: $calc-application-footer-height;
right: 0;
@apply gl-bg-default;
z-index: 200;
@ -484,10 +484,10 @@
.issuable-context-form {
top: calc(#{$calc-application-header-height} + #{$mr-sticky-header-height});
height: calc(#{$calc-application-viewport-height} - #{$mr-sticky-header-height} - var(--mr-review-bar-height));
height: calc(#{$calc-application-viewport-height} - #{$mr-sticky-header-height});
position: sticky;
padding: 0 $gl-spacing-scale-3;
margin-bottom: calc((#{$content-wrapper-padding} * -1) + var(--mr-review-bar-height));
margin-bottom: calc((#{$content-wrapper-padding} * -1));
overflow-y: auto;
overscroll-behavior: contain;
}

View File

@ -826,32 +826,14 @@ $diff-file-header-top: 11px;
}
}
// stylelint-disable-next-line gitlab/no-gl-class
.submit-review-dropdown .gl-new-dropdown-panel {
max-width: none;
}
.submit-review-dropdown-form {
width: calc(100vw - 20px);
max-width: 756px;
}
.submit-review-dropdown-animated {
animation: review-btn-animate 300ms ease-in;
}
@include keyframes(review-btn-animate) {
0% {
0%, 100% {
transform: scale(1);
}
75% {
transform: scale(1.2);
}
100% {
transform: scale(1);
}
}
@ -897,42 +879,6 @@ $diff-file-header-top: 11px;
@apply gl-bg-default gl-border;
}
.review-bar-component {
position: fixed;
bottom: $calc-application-footer-height;
left: 0;
z-index: $zindex-dropdown-menu;
display: flex;
align-items: center;
width: 100%;
height: var(--mr-review-bar-height);
padding-left: $super-sidebar-width;
padding-right: $right-sidebar-collapsed-width;
background: var(--gl-background-color-default);
border-top: 1px solid var(--gl-border-color-default);
@apply gl-transition-padding;
@media (max-width: map-get($grid-breakpoints, sm)-1) {
padding-left: 0;
padding-right: 0;
}
.submit-review-dropdown {
margin-left: $grid-size;
.md-header {
top: -$gl-spacing-scale-2;
}
}
}
.review-bar-content {
max-width: $limited-layout-width;
padding: 0 $container-margin;
width: 100%;
margin: 0 auto;
}
.review-preview-item-header {
display: flex;
align-items: center;

View File

@ -7,7 +7,6 @@
--top-bar-height: 0px;
--broadcast-message-height: 0px;
--system-footer-height: 0px;
--mr-review-bar-height: 0px;
--application-header-height: calc(
var(--header-height) +
var(--system-header-height) +

View File

@ -42,7 +42,6 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
push_frontend_feature_flag(:mr_experience_survey, project)
push_frontend_feature_flag(:mr_pipelines_graphql, project)
push_frontend_feature_flag(:notifications_todos_buttons, current_user)
push_frontend_feature_flag(:improved_review_experience, current_user)
end
around_action :allow_gitaly_ref_name_caching, only: [:index, :show, :diffs, :rapid_diffs, :discussions]

View File

@ -34,10 +34,12 @@
# language: int
# language_name: string
# active: boolean - Whether to include projects that are not archived.
# namespace_path: string - Full path of the project's namespace (group or user).
class ProjectsFinder < UnionFinder
include CustomAttributesFilter
include UpdatedAtFilter
include Projects::SearchFilter
include Gitlab::Utils::StrongMemoize
attr_accessor :params
attr_reader :current_user, :project_ids_relation
@ -51,6 +53,8 @@ class ProjectsFinder < UnionFinder
end
def execute
return Project.none if params[:namespace_path].present? && namespace_id.nil?
user = params.delete(:user)
collection =
if user
@ -78,6 +82,7 @@ class ProjectsFinder < UnionFinder
# EE would override this to add more filters
def filter_projects(collection)
collection = by_namespace_path(collection)
collection = by_deleted_status(collection)
collection = by_ids(collection)
collection = by_full_paths(collection)
@ -179,6 +184,10 @@ class ProjectsFinder < UnionFinder
params[:full_paths].present? ? items.where_full_path_in(params[:full_paths], preload_routes: false) : items
end
def by_namespace_path(items)
params[:namespace_path].present? ? items.in_namespace(namespace_id) : items
end
def union(items)
find_union(items, Project).with_route
end
@ -329,6 +338,11 @@ class ProjectsFinder < UnionFinder
organization_ids.flatten.uniq.compact
end
end
def namespace_id
Namespace.find_by_full_path(params[:namespace_path])&.id
end
strong_memoize_attr :namespace_id
end
ProjectsFinder.prepend_mod_with('ProjectsFinder')

View File

@ -29,6 +29,10 @@ module ProjectSearchArguments
default_value: 'id_desc',
description: "Sort order of results. Format: `<field_name>_<sort_direction>`, " \
"for example: `id_desc` or `name_asc`"
argument :namespace_path, GraphQL::Types::ID,
required: false,
description: "Filter projects by their namespace's full path (group or user)."
end
private
@ -40,7 +44,8 @@ module ProjectSearchArguments
search_namespaces: params[:search_namespaces],
sort: params[:sort],
topic: params[:topics],
personal: params[:personal]
personal: params[:personal],
namespace_path: params[:namespace_path]
}.compact
end
end

View File

@ -109,8 +109,6 @@
- if @merge_request.can_be_cherry_picked?
= render "projects/commit/change", type: 'cherry-pick', commit: @merge_request.commit_to_cherry_pick
#js-review-bar{ data: review_bar_data(@merge_request, current_user) }
#js-reviewer-drawer-portal
- if current_user && Feature.enabled?(:mr_experience_survey, current_user)

View File

@ -4,6 +4,6 @@ feature_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/534328
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/187616
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/541373
milestone: '17.11'
group: group::import and integrate
type: wip
default_enabled: false
group: group::import
type: beta
default_enabled: true

View File

@ -1,9 +0,0 @@
---
name: improved_review_experience
feature_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/525841
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/185795
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/535461
milestone: '17.11'
group: group::code review
type: beta
default_enabled: true

View File

@ -5,4 +5,4 @@ feature_category: package_registry
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/185225
milestone: '17.11'
queued_migration_version: 20250320090255
finalized_by: # version of the migration that finalized this BBM
finalized_by: '20250617212337'

View File

@ -8,14 +8,6 @@ description: Holds Customer Relations (CRM) Contacts
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/71007
milestone: '14.4'
gitlab_schema: gitlab_main_cell
desired_sharding_key:
namespace_id:
references: namespaces
backfill_via:
parent:
foreign_key: issue_id
table: issues
sharding_key: namespace_id
belongs_to: issue
sharding_key:
namespace_id: namespaces
table_size: small
desired_sharding_key_migration_job_name: BackfillIssueCustomerRelationsContactsNamespaceId

View File

@ -0,0 +1,14 @@
# frozen_string_literal: true
class AddIssueCustomerRelationsContactsNamespaceIdNotNull < Gitlab::Database::Migration[2.3]
milestone '18.2'
disable_ddl_transaction!
def up
add_not_null_constraint :issue_customer_relations_contacts, :namespace_id
end
def down
remove_not_null_constraint :issue_customer_relations_contacts, :namespace_id
end
end

View File

@ -0,0 +1,20 @@
# frozen_string_literal: true
class FinalizeBackfillPackagesHelmFileMetadataProjectId < Gitlab::Database::Migration[2.3]
milestone '18.2'
disable_ddl_transaction!
restrict_gitlab_migration gitlab_schema: :gitlab_main_cell
def up
ensure_batched_background_migration_is_finished(
job_class_name: 'BackfillPackagesHelmFileMetadataProjectId',
table_name: :packages_helm_file_metadata,
column_name: :package_file_id,
job_arguments: [:project_id, :packages_package_files, :project_id, :package_file_id],
finalize: true
)
end
def down; end
end

View File

@ -0,0 +1 @@
efd929efe3f9d02b551ade39b906564ef1d6e00349162b3bb0da883ea9167c5a

View File

@ -0,0 +1 @@
cfe361eec5f6eca86a20a587334cb4462a416e8491cac63849a7519b09fbbdcc

View File

@ -16339,7 +16339,8 @@ CREATE TABLE issue_customer_relations_contacts (
contact_id bigint NOT NULL,
created_at timestamp with time zone NOT NULL,
updated_at timestamp with time zone NOT NULL,
namespace_id bigint
namespace_id bigint,
CONSTRAINT check_9fd68b6ded CHECK ((namespace_id IS NOT NULL))
);
CREATE SEQUENCE issue_customer_relations_contacts_id_seq

View File

@ -76,12 +76,12 @@ of whether you can use these features with Duo Core or Duo Pro when
| Feature | Available on GitLab Duo Self-Hosted | GitLab version | Status |
| ----------------------------------------------------------------------------------------------------------------- | ------------------------------------------- | ---------------------- | --- |
| [General](../../user/gitlab_duo_chat/_index.md) | {{< icon name="check-circle-filled" >}} Yes | GitLab 17.9 and later | Generally available |
| [Explain Code](../../user/gitlab_duo_chat/examples.md#explain-selected-code) | {{< icon name="check-circle-filled" >}} Yes | GitLab 17.9 and later | Generally available |
| [Write Test](../../user/gitlab_duo_chat/examples.md#write-tests-in-the-ide) | {{< icon name="check-circle-filled" >}} Yes | GitLab 17.9 and later | Generally available |
| [Code Explanation](../../user/gitlab_duo_chat/examples.md#explain-selected-code) | {{< icon name="check-circle-filled" >}} Yes | GitLab 17.9 and later | Generally available |
| [Test Generation](../../user/gitlab_duo_chat/examples.md#write-tests-in-the-ide) | {{< icon name="check-circle-filled" >}} Yes | GitLab 17.9 and later | Generally available |
| [Refactor Code](../../user/gitlab_duo_chat/examples.md#refactor-code-in-the-ide) | {{< icon name="check-circle-filled" >}} Yes | GitLab 17.9 and later | Generally available |
| [Fix Code](../../user/gitlab_duo_chat/examples.md#fix-code-in-the-ide) | {{< icon name="check-circle-filled" >}} Yes | GitLab 17.9 and later | Generally available |
| [Troubleshoot Job](../../user/gitlab_duo_chat/examples.md#troubleshoot-failed-cicd-jobs-with-root-cause-analysis) | {{< icon name="check-circle-filled" >}} Yes | GitLab 17.10 and later | Beta |
| [Explain Vulnerability](../../user/application_security/vulnerabilities/_index.md#explaining-a-vulnerability) | {{< icon name="check-circle-filled" >}} Yes | GitLab 18.1 and later | Beta |
| [Root Cause Analysis](../../user/gitlab_duo_chat/examples.md#troubleshoot-failed-cicd-jobs-with-root-cause-analysis) | {{< icon name="check-circle-filled" >}} Yes | GitLab 17.10 and later | Beta |
| [Vulnerability Explanation](../../user/application_security/vulnerabilities/_index.md#explaining-a-vulnerability) | {{< icon name="check-circle-filled" >}} Yes | GitLab 18.1 and later | Beta |
For more examples of a question you can ask, see
[Ask about GitLab](../../user/gitlab_duo_chat/examples.md).
@ -90,8 +90,8 @@ For more examples of a question you can ask, see
| Feature | Available on GitLab Duo Self-Hosted | GitLab version | Status |
| -------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------- | ---------------------- | --- |
| [Generate Commit Message](../../user/project/merge_requests/duo_in_merge_requests.md#generate-a-merge-commit-message) | {{< icon name="check-circle-filled" >}} Yes | GitLab 18.1 and later | Beta |
| [Summarize New Merge Request](../../user/project/merge_requests/duo_in_merge_requests.md#generate-a-description-by-summarizing-code-changes) | {{< icon name="check-circle-filled" >}} Yes | GitLab 18.1 and later | Beta |
| [Merge Commit Message Generation](../../user/project/merge_requests/duo_in_merge_requests.md#generate-a-merge-commit-message) | {{< icon name="check-circle-filled" >}} Yes | GitLab 18.1 and later | Beta |
| [Merge Request Summary](../../user/project/merge_requests/duo_in_merge_requests.md#generate-a-description-by-summarizing-code-changes) | {{< icon name="check-circle-filled" >}} Yes | GitLab 18.1 and later | Beta |
| [Code Review](../../user/project/merge_requests/duo_in_merge_requests.md#have-gitlab-duo-review-your-code) | {{< icon name="dash-circle" >}} No | Not applicable | Not applicable |
| [Code Review Summary](../../user/project/merge_requests/duo_in_merge_requests.md#summarize-a-code-review) | {{< icon name="check-circle-filled" >}} Yes | GitLab 18.1 and later | Experiment |
@ -100,7 +100,7 @@ For more examples of a question you can ask, see
| Feature | Available on GitLab Duo Self-Hosted | GitLab version | Status |
| -------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------- | ---------------------- | --- |
| [Issue Description Generation](../../user/project/issues/managing_issues.md#populate-an-issue-with-issue-description-generation) | {{< icon name="dash-circle" >}} No | Not applicable | Not applicable |
| [Issue Discussion Summary](../../user/discussions/_index.md#summarize-issue-discussions-with-duo-chat) | {{< icon name="check-circle-filled" >}} Yes | GitLab 18.1 and later | Beta |
| [Discussion Summary](../../user/discussions/_index.md#summarize-issue-discussions-with-duo-chat) | {{< icon name="check-circle-filled" >}} Yes | GitLab 18.1 and later | Beta |
#### Other features

View File

@ -1254,6 +1254,7 @@ four standard [pagination arguments](#pagination-arguments):
| <a id="queryprojectsmarkedfordeletionon"></a>`markedForDeletionOn` | [`Date`](#date) | Date when the project was marked for deletion. |
| <a id="queryprojectsmembership"></a>`membership` | [`Boolean`](#boolean) | Return only projects that the current user is a member of. |
| <a id="queryprojectsminaccesslevel"></a>`minAccessLevel` | [`AccessLevelEnum`](#accesslevelenum) | Return only projects where current user has at least the specified access level. |
| <a id="queryprojectsnamespacepath"></a>`namespacePath` | [`ID`](#id) | Filter projects by their namespace's full path (group or user). |
| <a id="queryprojectsnotaimedfordeletion"></a>`notAimedForDeletion` | [`Boolean`](#boolean) | Exclude projects that are marked for deletion. |
| <a id="queryprojectspersonal"></a>`personal` | [`Boolean`](#boolean) | Return only personal projects. |
| <a id="queryprojectsprogramminglanguagename"></a>`programmingLanguageName` | [`String`](#string) | Filter projects by programming language name (case insensitive). For example: "css" or "ruby". |
@ -23864,6 +23865,7 @@ four standard [pagination arguments](#pagination-arguments):
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="cirunnerprojectsmembership"></a>`membership` | [`Boolean`](#boolean) | Return only projects that the current user is a member of. |
| <a id="cirunnerprojectsnamespacepath"></a>`namespacePath` | [`ID`](#id) | Filter projects by their namespace's full path (group or user). |
| <a id="cirunnerprojectspersonal"></a>`personal` | [`Boolean`](#boolean) | Return only personal projects. |
| <a id="cirunnerprojectssearch"></a>`search` | [`String`](#string) | Search query, which can be for the project name, a path, or a description. |
| <a id="cirunnerprojectssearchnamespaces"></a>`searchNamespaces` | [`Boolean`](#boolean) | Include namespace in project search. |
@ -34778,6 +34780,7 @@ four standard [pagination arguments](#pagination-arguments):
| <a id="organizationprojectsmarkedfordeletionon"></a>`markedForDeletionOn` | [`Date`](#date) | Date when the project was marked for deletion. |
| <a id="organizationprojectsmembership"></a>`membership` | [`Boolean`](#boolean) | Return only projects that the current user is a member of. |
| <a id="organizationprojectsminaccesslevel"></a>`minAccessLevel` | [`AccessLevelEnum`](#accesslevelenum) | Return only projects where current user has at least the specified access level. |
| <a id="organizationprojectsnamespacepath"></a>`namespacePath` | [`ID`](#id) | Filter projects by their namespace's full path (group or user). |
| <a id="organizationprojectsnotaimedfordeletion"></a>`notAimedForDeletion` | [`Boolean`](#boolean) | Exclude projects that are marked for deletion. |
| <a id="organizationprojectspersonal"></a>`personal` | [`Boolean`](#boolean) | Return only personal projects. |
| <a id="organizationprojectsprogramminglanguagename"></a>`programmingLanguageName` | [`String`](#string) | Filter projects by programming language name (case insensitive). For example: "css" or "ruby". |

View File

@ -153,14 +153,15 @@ Supported parameters:
| `sort` | `updated desc` | The [field to sort the data by](fields.md#fields-to-sort-glql-views-by) followed by a sort order (`asc` or `desc`). |
| `title` | `GLQL table` or `GLQL list` | A title displayed at the top of the GLQL view. |
For example, to display first five issues assigned to current user in the `gitlab-org/gitlab`
project as a list, displaying fields `title`, `health`, and `due`:
For example, to display the first five issues assigned to the current user in the `gitlab-org/gitlab`
project as a list, sorted by due date (earliest first) and displaying the `title`, `health`, and `due` fields:
````yaml
```glql
display: list
fields: title, health, due
limit: 5
sort: due asc
query: project = "gitlab-org/gitlab" AND assignee = currentUser() AND state = opened
```
````

View File

@ -1582,7 +1582,9 @@ can be used to indicate what fields to include in the rendered GLQL view.
**Description**: Display the start date of epics listed in the view.
**Field name**: `startDate`
**Field name**: `start`
**Aliases**: `startDate`
**Supported for**: Epics
@ -1641,16 +1643,80 @@ In GLQL views, the `sort` view parameter is a field name followed by
a sort order (`asc` or `desc`) that sorts the results by the specified
field and order.
| Field | Supported for | Description |
|---------------|-------------------------------|-------------------------------------------------|
| Closed at | Issues, epics, merge requests | Sort by closed date |
| Created | Issues, epics, merge requests | Sort by created date |
| Due date | Issues, epics | Sort by due date |
| Health status | Issues | Sort by health status |
| Merged at | Merge requests | Sort by merge date |
| Milestone | Issues, merge requests | Sort by milestone due date |
| Popularity | Issues, epics, merge requests | Sort by the number of thumbs up emoji reactions |
| Start date | Epics | Sort by start date |
| Title | Issues, epics, merge requests | Sort by title |
| Updated | Issues, epics, merge requests | Sort by last updated date |
| Weight | Issues | Sort by weight |
| Field | Name (and alias) | Supported for | Description |
|---------------|--------------------------|-------------------------------|-------------------------------------------------|
| Closed at | `closed`, `closedAt` | Issues, epics, merge requests | Sort by closed date |
| Created | `created`, `createdAt` | Issues, epics, merge requests | Sort by created date |
| Due date | `due`, `dueDate` | Issues, epics | Sort by due date |
| Health status | `health`, `healthStatus` | Issues | Sort by health status |
| Merged at | `merged`, `mergedAt` | Merge requests | Sort by merge date |
| Milestone | `milestone` | Issues, merge requests | Sort by milestone due date |
| Popularity | `popularity` | Issues, epics, merge requests | Sort by the number of thumbs up emoji reactions |
| Start date | `start`, `startDate` | Epics | Sort by start date |
| Title | `title` | Issues, epics, merge requests | Sort by title |
| Updated at | `updated`, `updatedAt` | Issues, epics, merge requests | Sort by last updated date |
| Weight | `weight` | Issues | Sort by weight |
**Examples**:
- List all issues in the `gitlab-org/gitlab` project sorted by title. Display columns
`state`, `title`, and `updated`.
````yaml
```glql
display: table
fields: state, title, updated
sort: title asc
query: project = "gitlab-org/gitlab" and type = Issue
```
````
- List all merge requests in the `gitlab-org` group assigned to the
authenticated user sorted by the merge date (latest first). Display columns
`title`, `reviewer`, and `merged`.
````yaml
```glql
display: table
fields: title, reviewer, merged
sort: merged desc
query: group = "gitlab-org" and type = MergeRequest and state = merged and author = currentUser()
limit: 10
```
````
- List all epics in the `gitlab-org` group sorted by the start date (oldest
first). Display columns `title`, `state`, and `startDate`.
````yaml
```glql
display: table
fields: title, state, startDate
sort: startDate asc
query: group = "gitlab-org" and type = Epic
```
````
- List all issues in the `gitlab-org` group with an assigned weight sorted by
the weight (highest first). Display columns `title`, `weight`, and `health`.
````yaml
```glql
display: table
fields: title, weight, health
sort: weight desc
query: group = "gitlab-org" and weight = any
```
````
- List all issues in the `gitlab-org` group due up to a week from today sorted by the due
date (earliest first). Display columns `title`, `duedate`, and `assignee`.
````yaml
```glql
display: table
fields: title, dueDate, assignee
sort: dueDate asc
query: group = "gitlab-org" and due >= today() and due <= 1w
```
````

View File

@ -68,41 +68,41 @@ The following table lists group permissions available for each role:
Group permission for [analytics](analytics/_index.md) features including value streams, product analytics, and insights:
| Action | Guest | Planner | Reporter | Developer | Maintainer | Owner | Notes |
| ------------------------------------------------------------------ | :---: | :-----: | :------: | :-------: | :--------: | :---: | ----- |
| View [Insights](project/insights/_index.md) | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | |
| View [Insights](project/insights/_index.md) charts | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | |
| View [Issue analytics](group/issues_analytics/_index.md) | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | |
| View Contribution analytics | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | |
| View value stream analytics | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | |
| View [Productivity analytics](analytics/productivity_analytics.md) | | | ✓ | ✓ | ✓ | ✓ | |
| View [Group DevOps Adoption](group/devops_adoption/_index.md) | | | ✓ | ✓ | ✓ | ✓ | |
| View metrics dashboard annotations | | | ✓ | ✓ | ✓ | ✓ | |
| Create/edit/delete metrics dashboard annotations | | | | ✓ | ✓ | ✓ | |
| Action | Guest | Planner | Reporter | Developer | Maintainer | Owner |
| ------------------------------------------------------------------ | :---: | :-----: | :------: | :-------: | :--------: | :---: |
| View [Insights](project/insights/_index.md) | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| View [Insights](project/insights/_index.md) charts | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| View [Issue analytics](group/issues_analytics/_index.md) | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| View Contribution analytics | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| View value stream analytics | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| View [Productivity analytics](analytics/productivity_analytics.md) | | | ✓ | ✓ | ✓ | ✓ |
| View [Group DevOps Adoption](group/devops_adoption/_index.md) | | | ✓ | ✓ | ✓ | ✓ |
| View metrics dashboard annotations | | | ✓ | ✓ | ✓ | ✓ |
| Create/edit/delete metrics dashboard annotations | | | | ✓ | ✓ | ✓ |
### Application security group permissions
Group permissions for [Application Security](application_security/secure_your_application.md) features including dependency management, security analyzers, security policies, and vulnerability management.
| Action | Guest | Planner | Reporter | Developer | Maintainer | Owner | Notes |
| ------------------------------------------------------------------------------- | :---: | :-----: | :------: | :-------: | :--------: | :---: | ----- |
| View [dependency list](application_security/dependency_list/_index.md) | | | | ✓ | ✓ | ✓ | |
| View [vulnerability report](application_security/vulnerability_report/_index.md) | | | | ✓ | ✓ | ✓ | |
| View [security dashboard](application_security/security_dashboard/_index.md) | | | | ✓ | ✓ | ✓ | |
| Create [security policy project](application_security/policies/_index.md) | | | | | | ✓ | |
| Assign [security policy project](application_security/policies/_index.md) | | | | | | ✓ | |
| Action | Guest | Planner | Reporter | Developer | Maintainer | Owner |
| -------------------------------------------------------------------------------- | :---: | :-----: | :------: | :-------: | :--------: | :---: |
| View [dependency list](application_security/dependency_list/_index.md) | | | | ✓ | ✓ | ✓ |
| View [vulnerability report](application_security/vulnerability_report/_index.md) | | | | ✓ | ✓ | ✓ |
| View [security dashboard](application_security/security_dashboard/_index.md) | | | | ✓ | ✓ | ✓ |
| Create [security policy project](application_security/policies/_index.md) | | | | | | ✓ |
| Assign [security policy project](application_security/policies/_index.md) | | | | | | ✓ |
### CI/CD group permissions
Group permissions for [CI/CD](../ci/_index.md) features including runners, variables, and protected environments:
| Action | Guest | Planner | Reporter | Developer | Maintainer | Owner | Notes |
| ------------------------------------- | :---: | :-----: | :------: | :-------: | :--------: | :---: | ----- |
| View group runners | | | | | ✓ | ✓ | |
| Manage group-level Kubernetes cluster | | | | | ✓ | ✓ | |
| Manage group runners | | | | | | ✓ | |
| Manage group level CI/CD variables | | | | | | ✓ | |
| Manage group protected environments | | | | | | ✓ | |
| Action | Guest | Planner | Reporter | Developer | Maintainer | Owner |
| ------------------------------------- | :---: | :-----: | :------: | :-------: | :--------: | :---: |
| View group runners | | | | | ✓ | ✓ |
| Manage group-level Kubernetes cluster | | | | | ✓ | ✓ |
| Manage group runners | | | | | | ✓ |
| Manage group level CI/CD variables | | | | | | ✓ |
| Manage group protected environments | | | | | | ✓ |
### Compliance group permissions
@ -166,11 +166,11 @@ Group permissions for [group features](group/_index.md):
Group permissions for project planning features including iterations, milestones, and labels:
| Action | Guest | Planner | Reporter | Developer | Maintainer | Owner | Notes |
| ----------------------- | :---: | :-----: | :------: | :-------: | :--------: | :---: | ----- |
| Manage group labels | | ✓ | ✓ | ✓ | ✓ | ✓ | |
| Manage group milestones | | ✓ | ✓ | ✓ | ✓ | ✓ | |
| Manage iterations | | ✓ | ✓ | ✓ | ✓ | ✓ | |
| Action | Guest | Planner | Reporter | Developer | Maintainer | Owner |
| ----------------------- | :---: | :-----: | :------: | :-------: | :--------: | :---: |
| Manage group labels | | ✓ | ✓ | ✓ | ✓ | ✓ |
| Manage group milestones | | ✓ | ✓ | ✓ | ✓ | ✓ |
| Manage iterations | | ✓ | ✓ | ✓ | ✓ | ✓ |
Group permissions for [epics](group/epics/_index.md):
@ -208,49 +208,49 @@ Group permissions for [container registry](packages/_index.md):
Group permissions for [package registry](packages/_index.md):
| Action | Guest | Planner | Reporter | Developer | Maintainer | Owner | Notes |
| ---------------------------------------- | :---: | :-----: | :------: | :-------: | :--------: | :---: | ----- |
| Pull packages | | | ✓ | ✓ | ✓ | ✓ | |
| Publish packages | | | | ✓ | ✓ | ✓ | |
| Delete packages | | | | | ✓ | ✓ | |
| Manage package settings | | | | | | ✓ | |
| Manage dependency proxy cleanup policies | | | | | | ✓ | |
| Enable dependency proxy | | | | | | ✓ | |
| Disable dependency proxy | | | | | | ✓ | |
| Purge the dependency proxy for a group | | | | | | ✓ | |
| Enable package request forwarding | | | | | | ✓ | |
| Disable package request forwarding | | | | | | ✓ | |
| Action | Guest | Planner | Reporter | Developer | Maintainer | Owner |
| ---------------------------------------- | :---: | :-----: | :------: | :-------: | :--------: | :---: |
| Pull packages | | | ✓ | ✓ | ✓ | ✓ |
| Publish packages | | | | ✓ | ✓ | ✓ |
| Delete packages | | | | | ✓ | ✓ |
| Manage package settings | | | | | | ✓ |
| Manage dependency proxy cleanup policies | | | | | | ✓ |
| Enable dependency proxy | | | | | | ✓ |
| Disable dependency proxy | | | | | | ✓ |
| Purge the dependency proxy for a group | | | | | | ✓ |
| Enable package request forwarding | | | | | | ✓ |
| Disable package request forwarding | | | | | | ✓ |
### Repository group permissions
Group permissions for [repository](project/repository/_index.md) features including merge requests, push rules, and deploy tokens.
| Action | Guest | Planner | Reporter | Developer | Maintainer | Owner | Notes |
| -------------------------------------------------------------------------------------- | :---: | :-----: | :------: | :-------: | :--------: | :---: | ----- |
| Manage [deploy tokens](project/deploy_tokens/_index.md) | | | | | | ✓ | |
| Manage [merge request settings](group/manage.md#group-merge-request-approval-settings) | | | | | | ✓ | |
| Manage [push rules](group/access_and_permissions.md#group-push-rules) | | | | | | ✓ | |
| Action | Guest | Planner | Reporter | Developer | Maintainer | Owner |
| -------------------------------------------------------------------------------------- | :---: | :-----: | :------: | :-------: | :--------: | :---: |
| Manage [deploy tokens](project/deploy_tokens/_index.md) | | | | | | ✓ |
| Manage [merge request settings](group/manage.md#group-merge-request-approval-settings) | | | | | | ✓ |
| Manage [push rules](group/access_and_permissions.md#group-push-rules) | | | | | | ✓ |
### User management group permissions
Group permissions for user management:
| Action | Guest | Planner | Reporter | Developer | Maintainer | Owner | Notes |
| ------------------------------- | :---: | :-----: | :------: | :-------: | :--------: | :---: | ----- |
| View 2FA status of members | | | | | | ✓ | |
| Manage group members | | | | | | ✓ | |
| Manage group-level custom roles | | | | | | ✓ | |
| Share (invite) groups to groups | | | | | | ✓ | |
| Filter members by 2FA status | | | | | | ✓ | |
| Action | Guest | Planner | Reporter | Developer | Maintainer | Owner |
| ------------------------------- | :---: | :-----: | :------: | :-------: | :--------: | :---: |
| View 2FA status of members | | | | | | ✓ |
| Manage group members | | | | | | ✓ |
| Manage group-level custom roles | | | | | | ✓ |
| Share (invite) groups to groups | | | | | | ✓ |
| Filter members by 2FA status | | | | | | ✓ |
### Workspace group permissions
Groups permissions for workspaces:
| Action | Guest | Planner | Reporter | Developer | Maintainer | Owner | Notes |
| --------------------------------------------------------- | :---: | :-----: | :------: | :-------: | :--------: | :---: | ----- |
| View workspace cluster agents mapped to a group | | | | | ✓ | ✓ | |
| Map or unmap workspace cluster agents to and from a group | | | | | | ✓ | |
| Action | Guest | Planner | Reporter | Developer | Maintainer | Owner |
| --------------------------------------------------------- | :---: | :-----: | :------: | :-------: | :--------: | :---: |
| View workspace cluster agents mapped to a group | | | | | ✓ | ✓ |
| Map or unmap workspace cluster agents to and from a group | | | | | | ✓ |
## Project members permissions
@ -274,16 +274,16 @@ The following tables list the project permissions available for each role.
Project permissions for [analytics](analytics/_index.md) features including value streams, usage trends, product analytics, and insights.
| Action | Guest | Planner | Reporter | Developer | Maintainer | Owner | Notes |
| ------------------------------------------------------------------------------------------ | :---: | :-----: | :------: | :-------: | :--------: | :---: | ----- |
| View [issue analytics](group/issues_analytics/_index.md) | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | |
| View [value stream analytics](group/value_stream_analytics/_index.md) | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | |
| View [CI/CD analytics](analytics/ci_cd_analytics.md) | | | ✓ | ✓ | ✓ | ✓ | |
| View [code review analytics](analytics/code_review_analytics.md) | | | ✓ | ✓ | ✓ | ✓ | |
| View [DORA metrics](analytics/ci_cd_analytics.md) | | | ✓ | ✓ | ✓ | ✓ | |
| View [merge request analytics](analytics/merge_request_analytics.md) | | | ✓ | ✓ | ✓ | ✓ | |
| View [repository analytics](analytics/repository_analytics.md) | | | ✓ | ✓ | ✓ | ✓ | |
| View [Value Streams Dashboard & AI impact analytics](analytics/value_streams_dashboard.md) | | | ✓ | ✓ | ✓ | ✓ | |
| Action | Guest | Planner | Reporter | Developer | Maintainer | Owner |
| ------------------------------------------------------------------------------------------ | :---: | :-----: | :------: | :-------: | :--------: | :---: |
| View [issue analytics](group/issues_analytics/_index.md) | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| View [value stream analytics](group/value_stream_analytics/_index.md) | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| View [CI/CD analytics](analytics/ci_cd_analytics.md) | | | ✓ | ✓ | ✓ | ✓ |
| View [code review analytics](analytics/code_review_analytics.md) | | | ✓ | ✓ | ✓ | ✓ |
| View [DORA metrics](analytics/ci_cd_analytics.md) | | | ✓ | ✓ | ✓ | ✓ |
| View [merge request analytics](analytics/merge_request_analytics.md) | | | ✓ | ✓ | ✓ | ✓ |
| View [repository analytics](analytics/repository_analytics.md) | | | ✓ | ✓ | ✓ | ✓ |
| View [Value Streams Dashboard & AI impact analytics](analytics/value_streams_dashboard.md) | | | ✓ | ✓ | ✓ | ✓ |
### Application security
@ -403,23 +403,23 @@ Project permissions for [model registry](project/ml/model_registry/_index.md) an
Project permissions for monitoring including [error tracking](../operations/error_tracking.md) and [incident management](../operations/incident_management/_index.md):
| Action | Guest | Planner | Reporter | Developer | Maintainer | Owner | Notes |
| ------------------------------------------------------------------------------------------------------------------- | :---: | :-----: | :------: | :-------: | :--------: | :---: | ----- |
| View an [incident](../operations/incident_management/incidents.md) | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | |
| Assign an [incident management](../operations/incident_management/_index.md) alert | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | |
| Participate in on-call rotation for [Incident Management](../operations/incident_management/_index.md) | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | |
| View [alerts](../operations/incident_management/alerts.md) | | | ✓ | ✓ | ✓ | ✓ | |
| View [error tracking](../operations/error_tracking.md) list | | | ✓ | ✓ | ✓ | ✓ | |
| View [escalation policies](../operations/incident_management/escalation_policies.md) | | | ✓ | ✓ | ✓ | ✓ | |
| View [on-call schedules](../operations/incident_management/oncall_schedules.md) | | | ✓ | ✓ | ✓ | ✓ | |
| Create [incident](../operations/incident_management/incidents.md) | | | ✓ | ✓ | ✓ | ✓ | |
| Change [alert status](../operations/incident_management/alerts.md#change-an-alerts-status) | | | ✓ | ✓ | ✓ | ✓ | |
| Change [incident severity](../operations/incident_management/manage_incidents.md#change-severity) | | | ✓ | ✓ | ✓ | ✓ | |
| Change [incident escalation status](../operations/incident_management/manage_incidents.md#change-status) | | | | ✓ | ✓ | ✓ | |
| Change [incident escalation policy](../operations/incident_management/manage_incidents.md#change-escalation-policy) | | | | ✓ | ✓ | ✓ | |
| Manage [error tracking](../operations/error_tracking.md) | | | | | ✓ | ✓ | |
| Manage [escalation policies](../operations/incident_management/escalation_policies.md) | | | | | ✓ | ✓ | |
| Manage [on-call schedules](../operations/incident_management/oncall_schedules.md) | | | | | ✓ | ✓ | |
| Action | Guest | Planner | Reporter | Developer | Maintainer | Owner |
| ------------------------------------------------------------------------------------------------------------------- | :---: | :-----: | :------: | :-------: | :--------: | :---: |
| View an [incident](../operations/incident_management/incidents.md) | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| Assign an [incident management](../operations/incident_management/_index.md) alert | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| Participate in on-call rotation for [Incident Management](../operations/incident_management/_index.md) | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| View [alerts](../operations/incident_management/alerts.md) | | | ✓ | ✓ | ✓ | ✓ |
| View [error tracking](../operations/error_tracking.md) list | | | ✓ | ✓ | ✓ | ✓ |
| View [escalation policies](../operations/incident_management/escalation_policies.md) | | | ✓ | ✓ | ✓ | ✓ |
| View [on-call schedules](../operations/incident_management/oncall_schedules.md) | | | ✓ | ✓ | ✓ | ✓ |
| Create [incident](../operations/incident_management/incidents.md) | | | ✓ | ✓ | ✓ | ✓ |
| Change [alert status](../operations/incident_management/alerts.md#change-an-alerts-status) | | | ✓ | ✓ | ✓ | ✓ |
| Change [incident severity](../operations/incident_management/manage_incidents.md#change-severity) | | | ✓ | ✓ | ✓ | ✓ |
| Change [incident escalation status](../operations/incident_management/manage_incidents.md#change-status) | | | | ✓ | ✓ | ✓ |
| Change [incident escalation policy](../operations/incident_management/manage_incidents.md#change-escalation-policy) | | | | ✓ | ✓ | ✓ |
| Manage [error tracking](../operations/error_tracking.md) | | | | | ✓ | ✓ |
| Manage [escalation policies](../operations/incident_management/escalation_policies.md) | | | | | ✓ | ✓ |
| Manage [on-call schedules](../operations/incident_management/oncall_schedules.md) | | | | | ✓ | ✓ |
### Project planning
@ -494,14 +494,14 @@ Project permissions for [wikis](project/wiki/_index.md):
Project permissions for [container registry](packages/_index.md):
| Action | Guest | Planner | Reporter | Developer | Maintainer | Owner | Notes |
|-------------------------------------------------------------------------------------------------|:-----:|:-------:|:--------:|:---------:|:----------:|:-----:|-------|
| Pull an image from the container registry <sup>1</sup> | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | |
| Push an image to the container registry | | | | ✓ | ✓ | ✓ | |
| Delete a container registry image | | | | ✓ | ✓ | ✓ | |
| Manage cleanup policies | | | | | ✓ | ✓ | |
| Create [tag protection](packages/container_registry/protected_container_tags.md) rule | | | | | ✓ | ✓ | |
| Create [immutable tag protection](packages/container_registry/immutable_container_tags.md) rule | | | | | | ✓ | |
| Action | Guest | Planner | Reporter | Developer | Maintainer | Owner |
| ----------------------------------------------------------------------------------------------- | :---: | :-----: | :------: | :-------: | :--------: | :---: |
| Pull an image from the container registry <sup>1</sup> | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| Push an image to the container registry | | | | ✓ | ✓ | ✓ |
| Delete a container registry image | | | | ✓ | ✓ | ✓ |
| Manage cleanup policies | | | | | ✓ | ✓ |
| Create [tag protection](packages/container_registry/protected_container_tags.md) rule | | | | | ✓ | ✓ |
| Create [immutable tag protection](packages/container_registry/immutable_container_tags.md) rule | | | | | | ✓ |
**Footnotes**:
@ -567,64 +567,132 @@ Project permissions for [GitLab Pages](project/pages/_index.md):
Project permissions for [repository](project/repository/_index.md) features including source code, branches, push rules, and more:
| Action | Guest | Planner | Reporter | Developer | Maintainer | Owner | Notes |
|-----------------------------------------------------------------------|:-----:|:-------:|:--------:|:---------:|:----------:|:-----:|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| View project code | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | On GitLab Self-Managed, users with the Guest role are able to perform this action only on public and internal projects (not on private projects). [External users](../administration/external_users.md) must be given explicit access (at least the **Reporter** role) even if the project is internal. Users with the Guest role on GitLab.com are only able to perform this action on public projects because internal visibility is not available. In GitLab 15.9 and later, users with the Guest role and an Ultimate license can view private repository content if an administrator (on GitLab Self-Managed or GitLab Dedicated) or group owner (on GitLab.com) gives those users permission. The administrator or group owner can create a [custom role](custom_roles/_index.md) through the API or UI and assign that role to the users. |
| [Search](search/_index.md) project code | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | On GitLab Self-Managed, users with the Guest role are able to perform this action only on public and internal projects (not on private projects). [External users](../administration/external_users.md) must be given explicit access (at least the **Reporter** role) even if the project is internal. Users with the Guest role on GitLab.com are only able to perform this action on public projects because internal visibility is not available. In GitLab 15.9 and later, users with the Guest role and an Ultimate license can search private repository content if an administrator (on GitLab Self-Managed or GitLab Dedicated) or group owner (on GitLab.com) gives those users permission. The administrator or group owner can create a [custom role](custom_roles/_index.md) through the API or UI and assign that role to the users. |
| Pull project code | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | On GitLab Self-Managed, users with the Guest role are able to perform this action only on public and internal projects (not on private projects). [External users](../administration/external_users.md) must be given explicit access (at least the **Reporter** role) even if the project is internal. Users with the Guest role on GitLab.com are only able to perform this action on public projects because internal visibility is not available. |
| View commit status | | | ✓ | ✓ | ✓ | ✓ | |
| Create commit status | | | | ✓ | ✓ | ✓ | If the [branch is protected](project/repository/branches/protected.md), this depends on the access given to Developers and Maintainers. |
| Update commit status | | | | ✓ | ✓ | ✓ | If the [branch is protected](project/repository/branches/protected.md), this depends on the access given to Developers and Maintainers. |
| [Search](search/_index.md) commits and comments | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | On GitLab Self-Managed, users with the Guest role are able to perform this action only on public and internal projects (not on private projects). [External users](../administration/external_users.md) must be given explicit access (at least the **Reporter** role) even if the project is internal. Users with the Guest role on GitLab.com are only able to perform this action on public projects because internal visibility is not available. In GitLab 15.9 and later, users with the Guest role and an Ultimate license can view private repository content if an administrator (on GitLab Self-Managed or GitLab Dedicated) or group owner (on GitLab.com) gives those users permission. The administrator or group owner can create a [custom role](custom_roles/_index.md) through the API or UI and assign that role to the users. |
| Create [Git tags](project/repository/tags/_index.md) | | | | ✓ | ✓ | ✓ | |
| Delete [Git tags](project/repository/tags/_index.md) | | | | ✓ | ✓ | ✓ | |
| Create new [branches](project/repository/branches/_index.md) | | | | ✓ | ✓ | ✓ | |
| Delete non-protected branches | | | | ✓ | ✓ | ✓ | |
| Force push to non-protected branches | | | | ✓ | ✓ | ✓ | |
| Push to non-protected branches | | | | ✓ | ✓ | ✓ | |
| Manage [protected branches](project/repository/branches/protected.md) | | | | | ✓ | ✓ | |
| Delete protected branches | | | | | ✓ | ✓ | |
| Push to protected branches | | | | | ✓ | ✓ | If the [branch is protected](project/repository/branches/protected.md), this depends on the access given to Developers and Maintainers. |
| Manage [protected tags](project/protected_tags.md) | | | | | ✓ | ✓ | |
| Manage [push rules](project/repository/push_rules.md) | | | | | ✓ | ✓ | |
| Remove fork relationship | | | | | | ✓ | |
| Force push to protected branches | | | | | | | Not allowed for Guest, Reporter, Developer, Maintainer, or Owner. See [protected branches](project/repository/branches/protected.md#allow-force-push). |
| Action | Guest | Planner | Reporter | Developer | Maintainer | Owner |
| --------------------------------------------------------------------- | :---: | :-----: | :------: | :-------: | :--------: | :---: |
| View project code <sup>1</sup> | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| [Search](search/_index.md) project code <sup>2</sup> | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| Pull project code <sup>3</sup> | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| View commit status | | | ✓ | ✓ | ✓ | ✓ |
| Create commit status <sup>4</sup> | | | | ✓ | ✓ | ✓ |
| Update commit status <sup>4</sup> | | | | ✓ | ✓ | ✓ |
| [Search](search/_index.md) commits and comments <sup>5</sup> | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| Create [Git tags](project/repository/tags/_index.md) | | | | ✓ | ✓ | ✓ |
| Delete [Git tags](project/repository/tags/_index.md) | | | | ✓ | ✓ | ✓ |
| Create new [branches](project/repository/branches/_index.md) | | | | ✓ | ✓ | ✓ |
| Delete non-protected branches | | | | ✓ | ✓ | ✓ |
| Force push to non-protected branches | | | | ✓ | ✓ | ✓ |
| Push to non-protected branches | | | | ✓ | ✓ | ✓ |
| Manage [protected branches](project/repository/branches/protected.md) | | | | | ✓ | ✓ |
| Delete protected branches | | | | | ✓ | ✓ |
| Push to protected branches <sup>4</sup> | | | | | ✓ | ✓ |
| Manage [protected tags](project/protected_tags.md) | | | | | ✓ | ✓ |
| Manage [push rules](project/repository/push_rules.md) | | | | | ✓ | ✓ |
| Remove fork relationship | | | | | | ✓ |
| Force push to protected branches <sup>6</sup> | | | | | | |
**Footnotes**
<!-- Disable ordered list rule https://github.com/DavidAnson/markdownlint/blob/main/doc/Rules.md#md029---ordered-list-item-prefix -->
<!-- markdownlint-disable MD029 -->
1. On GitLab Self-Managed, users with the Guest role are able to perform this action only on public
and internal projects (not on private projects). [External users](../administration/external_users.md)
must be given explicit access (at least the **Reporter** role) even if the project is internal.
Users with the Guest role on GitLab.com are only able to perform this action on public projects because
internal visibility is not available. In GitLab 15.9 and later, users with the Guest role and an
Ultimate license can view private repository content if an administrator (on GitLab Self-Managed
or GitLab Dedicated) or group owner (on GitLab.com) gives those users permission. The administrator
or group owner can create a [custom role](custom_roles/_index.md) through the API or UI and assign
that role to the users.
2. On GitLab Self-Managed, users with the Guest role are able to perform this action only on public
and internal projects (not on private projects). [External users](../administration/external_users.md)
must be given explicit access (at least the **Reporter** role) even if the project is internal. Users
with the Guest role on GitLab.com are only able to perform this action on public projects because
internal visibility is not available. In GitLab 15.9 and later, users with the Guest role and an
Ultimate license can search private repository content if an administrator (on GitLab Self-Managed
or GitLab Dedicated) or group owner (on GitLab.com) gives those users permission. The administrator
or group owner can create a [custom role](custom_roles/_index.md) through the API or UI and assign
that role to the users.
3. On GitLab Self-Managed, users with the Guest role are able to perform this action only on public
and internal projects (not on private projects). [External users](../administration/external_users.md)
must be given explicit access (at least the **Reporter** role) even if the project is internal. Users
with the Guest role on GitLab.com are only able to perform this action on public projects because
internal visibility is not available.
4. If the [branch is protected](project/repository/branches/protected.md), this depends on the
access given to Developers and Maintainers.
5. On GitLab Self-Managed, users with the Guest role are able to perform this action only on public
and internal projects (not on private projects). [External users](../administration/external_users.md)
must be given explicit access (at least the **Reporter** role) even if the project is internal. Users
with the Guest role on GitLab.com are only able to perform this action on public projects because
internal visibility is not available. In GitLab 15.9 and later, users with the Guest role and an
Ultimate license can view private repository content if an administrator (on GitLab Self-Managed
or GitLab Dedicated) or group owner (on GitLab.com) gives those users permission. The administrator
or group owner can create a [custom role](custom_roles/_index.md) through the API or UI and assign
that role to the users.
6. Not allowed for Guest, Reporter, Developer, Maintainer, or Owner. See [protected branches](project/repository/branches/protected.md#allow-force-push).
<!-- markdownlint-enable MD029 -->
### Merge requests
Project permissions for [merge requests](project/merge_requests/_index.md):
| Action | Guest | Planner | Reporter | Developer | Maintainer | Owner | Notes |
|---------------------------------------------------------------------------------------------------|:-----:|:-------:|:--------:|:---------:|:----------:|:-----:|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| [View](project/merge_requests/_index.md#view-merge-requests) a merge request | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | On GitLab Self-Managed, users with the Guest role are able to perform this action only on public and internal projects (not on private projects). [External users](../administration/external_users.md) must be given explicit access (at least the **Reporter** role) even if the project is internal. Users with the Guest role on GitLab.com are only able to perform this action on public projects because internal visibility is not available. |
| [Search](search/_index.md) merge requests and comments | ✓ | | ✓ | ✓ | ✓ | ✓ | On GitLab Self-Managed, users with the Guest role are able to perform this action only on public and internal projects (not on private projects). [External users](../administration/external_users.md) must be given explicit access (at least the **Reporter** role) even if the project is internal. Users with the Guest role on GitLab.com are only able to perform this action on public projects because internal visibility is not available. |
| Create [snippets](snippets.md) | | | ✓ | ✓ | ✓ | ✓ | |
| Create [merge request](project/merge_requests/creating_merge_requests.md) | | | | ✓ | ✓ | ✓ | In projects that accept contributions from external members, users can create, edit, and close their own merge requests. For **private** projects, this excludes the Guest role as those users [cannot clone private projects](public_access.md#private-projects-and-groups). For **internal** projects, includes users with read-only access to the project, as [they can clone internal projects](public_access.md#internal-projects-and-groups). |
| Comment and add suggestions to a merge request | | ✓ | ✓ | ✓ | ✓ | ✓ | |
| Update merge request including assign, review, approve, labels, lock threads, and resolve threads | | | | ✓ | ✓ | ✓ | For information on eligible approvers for merge requests, see [Eligible approvers](project/merge_requests/approvals/rules.md#eligible-approvers). |
| Manage [merge request settings](project/merge_requests/approvals/settings.md) | | | | | ✓ | ✓ | |
| Manage [merge request approval rules](project/merge_requests/approvals/rules.md) | | | | | ✓ | ✓ | |
| Add internal note | | ✓ | ✓ | ✓ | ✓ | ✓ | |
| Delete merge request | | | | | | ✓ | |
| Action | Guest | Planner | Reporter | Developer | Maintainer | Owner |
| -------------------------------------------------------------------------------------------------------------- | :---: | :-----: | :------: | :-------: | :--------: | :---: |
| [View](project/merge_requests/_index.md#view-merge-requests) a merge request <sup>1</sup> | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| [Search](search/_index.md) merge requests and comments <sup>1</sup> | ✓ | | ✓ | ✓ | ✓ | ✓ |
| Create [snippets](snippets.md) | | | ✓ | ✓ | ✓ | ✓ |
| Create [merge request](project/merge_requests/creating_merge_requests.md) <sup>2</sup> | | | | ✓ | ✓ | ✓ |
| Comment and add suggestions to a merge request | | ✓ | ✓ | ✓ | ✓ | ✓ |
| Update merge request including assign, review, approve, labels, lock threads, and resolve threads <sup>3</sup> | | | | ✓ | ✓ | ✓ |
| Manage [merge request settings](project/merge_requests/approvals/settings.md) | | | | | ✓ | ✓ |
| Manage [merge request approval rules](project/merge_requests/approvals/rules.md) | | | | | ✓ | ✓ |
| Add internal note | | ✓ | ✓ | ✓ | ✓ | ✓ |
| Delete merge request | | | | | | ✓ |
**Footnotes**
1. On GitLab Self-Managed, users with the Guest role are able to perform this action only on public
and internal projects (not on private projects). [External users](../administration/external_users.md)
must be given explicit access (at least the **Reporter** role) even if the project is internal. Users
with the Guest role on GitLab.com are only able to perform this action on public projects because
internal visibility is not available.
1. In projects that accept contributions from external members, users can create, edit, and close their
own merge requests. For **private** projects, this excludes the Guest role as those users
[cannot clone private projects](public_access.md#private-projects-and-groups). For **internal**
projects, includes users with read-only access to the project, as
[they can clone internal projects](public_access.md#internal-projects-and-groups).
1. For information on eligible approvers for merge requests, see [Eligible approvers](project/merge_requests/approvals/rules.md#eligible-approvers).
### User management
Project permissions for [user management](project/members/_index.md).
| Action | Guest | Planner | Reporter | Developer | Maintainer | Owner | Notes |
| ----------------------------------------------- | :---: | :-----: | :------: | :-------: | :--------: | :---: | ----- |
| Manage [team members](project/members/_index.md) | | | | | ✓ | ✓ | Maintainers cannot create, demote, or remove Owners, and they cannot promote users to the Owner role. They also cannot approve Owner role access requests. |
| Share (invite) projects with groups | | | | | ✓ | ✓ | When [Share Group Lock](project/members/sharing_projects_groups.md#prevent-a-project-from-being-shared-with-groups) is enabled the project can't be shared with other groups. It does not affect group with group sharing. |
| View 2FA status of members | | | | | ✓ | ✓ | |
| Action | Guest | Planner | Reporter | Developer | Maintainer | Owner |
| ------------------------------------------------------------- | :---: | :-----: | :------: | :-------: | :--------: | :---: |
| Manage [team members](project/members/_index.md) <sup>1</sup> | | | | | ✓ | ✓ |
| Share (invite) projects with groups <sup>2</sup> | | | | | ✓ | ✓ |
| View 2FA status of members | | | | | ✓ | ✓ |
**Footnotes**
1. Maintainers cannot create, demote, or remove Owners, and they cannot promote users to the Owner role.
They also cannot approve Owner role access requests.
1. When [Share Group Lock](project/members/sharing_projects_groups.md#prevent-a-project-from-being-shared-with-groups)
is enabled the project can't be shared with other groups. It does not affect group with group sharing.
### GitLab Duo
Project permissions for [GitLab Duo](gitlab_duo/_index.md):
| Action | Non-member | Guest | Planner | Reporter | Developer | Maintainer | Owner | Notes |
| -------------------------------------------------------------------------------------- | ---------- | ----- | ------- | -------- | --------- | ---------- | ----- | ----- |
| Use Duo features | | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | Code Suggestions requires a [user being assigned a seat to gain access to a Duo add-on](../subscriptions/subscription-add-ons.md#assign-gitlab-duo-seats). |
| Configure [Duo feature availability](gitlab_duo/turn_on_off.md#for-a-project) | | | | | | ✓ | ✓ | |
| Action | Non-member | Guest | Planner | Reporter | Developer | Maintainer | Owner |
| -------------------------------------------------------------------------------------- | ---------- | ----- | ------- | -------- | --------- | ---------- | ----- |
| Use Duo features <sup>1</sup> | | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| Configure [Duo feature availability](gitlab_duo/turn_on_off.md#for-a-project) | | | | | | ✓ | ✓ |
**Footnotes**
1. Code Suggestions requires a [user being assigned a seat to gain access to a Duo add-on](../subscriptions/subscription-add-ons.md#assign-gitlab-duo-seats).
## Subgroup permissions

View File

@ -27051,9 +27051,6 @@ msgstr ""
msgid "Finish editing this message first!"
msgstr ""
msgid "Finish review"
msgstr ""
msgid "Finished"
msgstr ""
@ -44773,9 +44770,6 @@ msgstr ""
msgid "Pending"
msgstr ""
msgid "Pending comments"
msgstr ""
msgid "Pending comments are hidden until you submit your review."
msgstr ""
@ -60885,9 +60879,6 @@ msgstr ""
msgid "Summary"
msgstr ""
msgid "Summary comment (optional)"
msgstr ""
msgid "Summary generated by AI"
msgstr ""

View File

@ -7,19 +7,6 @@ module QA
include Page::Component::Note
include Page::Component::Issuable::Sidebar
view 'app/assets/javascripts/batch_comments/components/preview_dropdown.vue' do
element 'review-preview-dropdown'
end
view 'app/assets/javascripts/batch_comments/components/review_bar.vue' do
element 'review-bar-content'
end
view 'app/assets/javascripts/batch_comments/components/submit_dropdown.vue' do
element 'submit-review-dropdown'
element 'submit-review-button'
end
view 'app/assets/javascripts/batch_comments/components/review_drawer.vue' do
element 'submit-review-button'
end

View File

@ -344,6 +344,9 @@ helm repo update
helm --namespace "${GITLAB_WORKSPACES_PROXY_HELM_RELEASE_NAMESPACE}" uninstall "${GITLAB_WORKSPACES_PROXY_HELM_RELEASE_NAME}" --ignore-not-found --timeout=600s --wait
# NOTE: We had to change default sshService.port from 22 to 30022 because of port 22 stopped working
# sometime around Jan 2025. Perhaps a MacOS update or Rancher change caused it, we don't know yet.
# This means you need to pass `-p 30022` to `ssh` command to connect to the workspace.
helm upgrade --install "${GITLAB_WORKSPACES_PROXY_HELM_RELEASE_NAME}" \
gitlab-workspaces-proxy/gitlab-workspaces-proxy \
--version="${GITLAB_WORKSPACES_PROXY_HELM_CHART_VERSION}" \
@ -360,6 +363,7 @@ helm upgrade --install "${GITLAB_WORKSPACES_PROXY_HELM_RELEASE_NAME}" \
--set="ingress.tls[1].hosts[0]=${GITLAB_WORKSPACES_PROXY_WILDCARD_DOMAIN}" \
--set="ingress.tls[1].secretName=${GITLAB_WORKSPACES_PROXY_WILDCARD_TLS_SECRET}" \
--set="ingress.className=nginx" \
--set="sshService.port=30022" \
--timeout=600s --wait --wait-for-jobs
kubectl wait pod \

View File

@ -9,7 +9,7 @@ RSpec.describe ProjectsFinder, feature_category: :groups_and_projects do
describe '#execute' do
let_it_be(:group) { create(:group, :public) }
let_it_be(:user) { create(:user, organizations: [group.organization]) }
let_it_be(:user) { create(:user, :with_namespace, organizations: [group.organization]) }
let_it_be(:private_project) do
create(:project, :private, name: 'A', path: 'A')
@ -561,6 +561,41 @@ RSpec.describe ProjectsFinder, feature_category: :groups_and_projects do
it { is_expected.to match_array([organization_project]) }
end
context 'filter by namespace_path' do
context 'when project is in group' do
let_it_be(:group) { create(:group, owners: [user]) }
let_it_be(:project) { create(:project, group: group, owners: [user]) }
context 'when `namespace_path` matches group path' do
let(:params) { { namespace_path: group.full_path } }
it { is_expected.to contain_exactly(project) }
end
context 'when `namespace_path` does not match' do
let(:params) { { namespace_path: 'non_existent_path' } }
it { is_expected.to be_empty }
end
end
context 'when project is owned by user' do
let_it_be(:project) { create(:project, namespace: user.namespace, owners: [user]) }
context 'when `namespace_path` matches user namespace' do
let(:params) { { namespace_path: user.namespace.full_path } }
it { is_expected.to contain_exactly(project) }
end
context 'when `namespace_path` does not match' do
let(:params) { { namespace_path: 'non_existent_path' } }
it { is_expected.to be_empty }
end
end
end
describe 'when with_issues_enabled is true' do
let(:params) { { with_issues_enabled: true } }

View File

@ -68,7 +68,7 @@ describe('~/actioncable_link', () => {
...TEST_OPERATION,
query: print(TEST_OPERATION.query),
},
{ received: expect.any(Function) },
{ received: expect.any(Function), connected: expect.any(Function) },
);
});
@ -106,5 +106,21 @@ describe('~/actioncable_link', () => {
['error', ['boom!']],
]);
});
it('emits "actioncable:reconnected" event on reconnected', () => {
const eventSpy = jest.spyOn(document, 'dispatchEvent');
cable.subscriptions.subscriptions[0].connected({ reconnected: true });
expect(eventSpy).toHaveBeenCalledWith(new CustomEvent('actioncable:reconnected'));
});
it('does not emit "actioncable:reconnected" event on initial connection', () => {
const eventSpy = jest.spyOn(document, 'dispatchEvent');
cable.subscriptions.subscriptions[0].connected({ reconnected: false });
expect(eventSpy).not.toHaveBeenCalled();
});
});
});

View File

@ -1,60 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Batch comments draft preview item component renders as button when improvedReviewExperience is true 1`] = `
<button
class="!gl-justify-start btn btn-link btn-md gl-button review-preview-item-header"
data-testid="preview-item-header"
type="button"
>
<span
class="gl-button-text"
>
<svg
aria-hidden="true"
class="flex-shrink-0 gl-fill-current gl-icon s16"
data-testid="comment-icon"
role="img"
>
<use
href="file-mock#comment"
/>
</svg>
<span
class="gl-items-center text-nowrap"
>
<span
class="block-truncated review-preview-item-header-text"
>
Your new comment
</span>
</span>
</span>
</button>
`;
exports[`Batch comments draft preview item component renders as span when improvedReviewExperience is false 1`] = `
<span
class="review-preview-item-header"
data-testid="preview-item-header"
>
<svg
aria-hidden="true"
class="flex-shrink-0 gl-fill-current gl-icon s16"
data-testid="comment-icon"
role="img"
>
<use
href="file-mock#comment"
/>
</svg>
<span
class="gl-font-bold gl-items-center text-nowrap"
>
<span
class="block-truncated gl-ml-2 review-preview-item-header-text"
>
Your new comment
</span>
</span>
</span>
`;

View File

@ -1,121 +0,0 @@
import Vue, { nextTick } from 'vue';
// eslint-disable-next-line no-restricted-imports
import Vuex from 'vuex';
import { mount } from '@vue/test-utils';
import { createTestingPinia } from '@pinia/testing';
import { PiniaVuePlugin } from 'pinia';
import { TEST_HOST } from 'helpers/test_constants';
import { visitUrl } from '~/lib/utils/url_utility';
import PreviewDropdown from '~/batch_comments/components/preview_dropdown.vue';
import PreviewItem from '~/batch_comments/components/preview_item.vue';
import { globalAccessorPlugin } from '~/pinia/plugins';
import { useLegacyDiffs } from '~/diffs/stores/legacy_diffs';
import { useNotes } from '~/notes/store/legacy_notes';
import { createCustomGetters } from 'helpers/pinia_helpers';
import { useBatchComments } from '~/batch_comments/store';
jest.mock('~/lib/utils/url_utility', () => ({
visitUrl: jest.fn(),
setUrlParams: jest.requireActual('~/lib/utils/url_utility').setUrlParams,
getParameterValues: jest.requireActual('~/lib/utils/url_utility').getParameterValues,
joinPaths: jest.fn(),
doesHashExistInUrl: jest.fn(),
}));
Vue.use(Vuex);
Vue.use(PiniaVuePlugin);
describe('Batch comments preview dropdown', () => {
let wrapper;
let pinia;
let batchCommentsGetters;
const findPreviewItem = () => wrapper.findComponent(PreviewItem);
function factory({ draftsCount = 1, sortedDrafts = [] } = {}) {
batchCommentsGetters = {
draftsCount,
sortedDrafts,
};
const store = new Vuex.Store({
modules: {
notes: {
getters: {
getNoteableData: () => ({ diff_head_sha: '123' }),
},
},
},
});
wrapper = mount(PreviewDropdown, {
store,
pinia,
stubs: {
PreviewItem: true,
},
});
}
beforeEach(() => {
batchCommentsGetters = {};
pinia = createTestingPinia({
plugins: [
globalAccessorPlugin,
createCustomGetters(() => ({
batchComments: batchCommentsGetters,
legacyNotes: {},
legacyDiffs: {},
})),
],
});
useLegacyDiffs();
useNotes();
});
describe('clicking draft', () => {
it('toggles active file when viewDiffsFileByFile is true', async () => {
useLegacyDiffs().viewDiffsFileByFile = true;
factory({
sortedDrafts: [{ id: 1, file_hash: 'hash', file_path: 'foo' }],
});
findPreviewItem().trigger('click');
await nextTick();
expect(useLegacyDiffs().goToFile).toHaveBeenCalledWith({ path: 'foo' });
await nextTick();
expect(useBatchComments().scrollToDraft).toHaveBeenCalledWith(
expect.objectContaining({ id: 1, file_hash: 'hash' }),
);
});
it('calls scrollToDraft', async () => {
useLegacyDiffs().viewDiffsFileByFile = false;
factory({
sortedDrafts: [{ id: 1 }],
});
findPreviewItem().trigger('click');
await nextTick();
expect(useBatchComments().scrollToDraft).toHaveBeenCalledWith(
expect.objectContaining({ id: 1 }),
);
});
it('changes window location to navigate to commit', async () => {
useLegacyDiffs().viewDiffsFileByFile = false;
factory({
sortedDrafts: [{ id: 1, position: { head_sha: '1234' } }],
});
findPreviewItem().trigger('click');
await nextTick();
expect(useBatchComments().scrollToDraft).not.toHaveBeenCalled();
expect(visitUrl).toHaveBeenCalledWith(`${TEST_HOST}/?commit_id=1234#note_1`);
});
});
});

View File

@ -126,17 +126,4 @@ describe('Batch comments draft preview item component', () => {
expect(wrapper.find('.review-preview-item-header-text').text()).toContain('Your new comment');
});
});
it.each`
improvedReviewExperience | component
${true} | ${'button'}
${false} | ${'span'}
`(
'renders as $component when improvedReviewExperience is $improvedReviewExperience',
({ improvedReviewExperience }) => {
createComponent({}, improvedReviewExperience);
expect(wrapper.findByTestId('preview-item-header').element).toMatchSnapshot();
},
);
});

View File

@ -1,57 +0,0 @@
import { GlDisclosureDropdown } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { createTestingPinia } from '@pinia/testing';
import Vue from 'vue';
// eslint-disable-next-line no-restricted-imports
import Vuex from 'vuex';
import { PiniaVuePlugin } from 'pinia';
import PreviewDropdown from '~/batch_comments/components/preview_dropdown.vue';
import { createStore } from '~/mr_notes/stores';
import { globalAccessorPlugin } from '~/pinia/plugins';
import { useLegacyDiffs } from '~/diffs/stores/legacy_diffs';
import { useNotes } from '~/notes/store/legacy_notes';
import { useBatchComments } from '~/batch_comments/store';
import { createDraft } from '../mock_data';
jest.mock('~/behaviors/markdown/render_gfm');
Vue.use(Vuex);
Vue.use(PiniaVuePlugin);
describe('Batch comments publish dropdown component', () => {
let wrapper;
let pinia;
const draft = createDraft();
function createComponent() {
const store = createStore();
wrapper = shallowMount(PreviewDropdown, {
store,
pinia,
stubs: { GlDisclosureDropdown },
});
}
beforeEach(() => {
pinia = createTestingPinia({ plugins: [globalAccessorPlugin] });
useLegacyDiffs();
useNotes();
useBatchComments().drafts = [draft, { ...draft, id: 2 }];
});
it('renders list of drafts', () => {
createComponent();
expect(wrapper.findComponent(GlDisclosureDropdown).props('items')).toMatchObject([
draft,
{ ...draft, id: 2 },
]);
});
it('renders draft count in dropdown title', () => {
createComponent();
expect(wrapper.findComponent(GlDisclosureDropdown).text()).toEqual('2 pending comments');
});
});

View File

@ -1,94 +0,0 @@
import { nextTick } from 'vue';
import { createTestingPinia } from '@pinia/testing';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import ReviewBar from '~/batch_comments/components/review_bar.vue';
import SubmitReviewButton from '~/batch_comments/components/submit_review_button.vue';
import { REVIEW_BAR_VISIBLE_CLASS_NAME } from '~/batch_comments/constants';
import toast from '~/vue_shared/plugins/global_toast';
import { globalAccessorPlugin } from '~/pinia/plugins';
import { useLegacyDiffs } from '~/diffs/stores/legacy_diffs';
import { useNotes } from '~/notes/store/legacy_notes';
import { useBatchComments } from '~/batch_comments/store';
import createStore from '../create_batch_comments_store';
jest.mock('~/vue_shared/plugins/global_toast');
describe('Batch comments review bar component', () => {
let store;
let wrapper;
const findDiscardReviewButton = () => wrapper.findByTestId('discard-review-btn');
const findDiscardReviewModal = () => wrapper.findByTestId('discard-review-modal');
const createComponent = (propsData = {}) => {
store = createStore();
wrapper = shallowMountExtended(ReviewBar, {
store,
propsData,
});
};
beforeEach(() => {
createTestingPinia({ plugins: [globalAccessorPlugin] });
useLegacyDiffs();
useNotes();
document.body.className = '';
});
it('adds review-bar-visible class to body when review bar is mounted', () => {
expect(document.body.classList.contains(REVIEW_BAR_VISIBLE_CLASS_NAME)).toBe(false);
createComponent();
expect(document.body.classList.contains(REVIEW_BAR_VISIBLE_CLASS_NAME)).toBe(true);
});
it('removes review-bar-visible class to body when review bar is destroyed', () => {
createComponent();
wrapper.destroy();
expect(document.body.classList.contains(REVIEW_BAR_VISIBLE_CLASS_NAME)).toBe(false);
});
describe('when discarding a review', () => {
it('shows modal when clicking discard button', async () => {
createComponent();
expect(findDiscardReviewModal().props('visible')).toBe(false);
findDiscardReviewButton().vm.$emit('click');
await nextTick();
expect(findDiscardReviewModal().props('visible')).toBe(true);
});
it('calls discardReviews when primary action on modal is triggered', () => {
createComponent();
findDiscardReviewModal().vm.$emit('primary');
expect(useBatchComments().discardDrafts).toHaveBeenCalled();
});
it('creates a toast message when finished', async () => {
createComponent();
jest.spyOn(store, 'dispatch').mockImplementation();
findDiscardReviewModal().vm.$emit('primary');
await nextTick();
expect(toast).toHaveBeenCalledWith('Review discarded');
});
});
it('does not render submit drawer when improvedReviewExperience is false', () => {
createComponent({}, false);
expect(wrapper.findComponent(SubmitReviewButton).exists()).toBe(false);
});
});

View File

@ -1,268 +0,0 @@
import { GlDisclosureDropdown } from '@gitlab/ui';
import { createTestingPinia } from '@pinia/testing';
import Vue from 'vue';
import VueApollo from 'vue-apollo';
// eslint-disable-next-line no-restricted-imports
import Vuex from 'vuex';
import { PiniaVuePlugin } from 'pinia';
import createMockApollo from 'helpers/mock_apollo_helper';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
import SubmitDropdown from '~/batch_comments/components/submit_dropdown.vue';
import { mockTracking } from 'helpers/tracking_helper';
import userCanApproveQuery from '~/batch_comments/queries/can_approve.query.graphql';
import { CLEAR_AUTOSAVE_ENTRY_EVENT } from '~/vue_shared/constants';
import markdownEditorEventHub from '~/vue_shared/components/markdown/eventhub';
import { globalAccessorPlugin } from '~/pinia/plugins';
import { useLegacyDiffs } from '~/diffs/stores/legacy_diffs';
import { useNotes } from '~/notes/store/legacy_notes';
import { useBatchComments } from '~/batch_comments/store';
jest.mock('~/autosave');
jest.mock('~/vue_shared/components/markdown/eventhub');
Vue.use(VueApollo);
Vue.use(Vuex);
Vue.use(PiniaVuePlugin);
let wrapper;
let pinia;
let trackingSpy;
let getCurrentUserLastNote;
function factory({ canApprove = true, shouldAnimateReviewButton = false } = {}) {
trackingSpy = mockTracking(undefined, null, jest.spyOn);
const requestHandlers = [
[
userCanApproveQuery,
() =>
Promise.resolve({
data: {
project: {
id: 1,
mergeRequest: {
id: 1,
userPermissions: {
canApprove,
},
},
},
},
}),
],
];
const apolloProvider = createMockApollo(requestHandlers);
getCurrentUserLastNote = Vue.observable({ id: 1 });
useBatchComments().shouldAnimateReviewButton = shouldAnimateReviewButton;
const store = new Vuex.Store({
getters: {
getNotesData: () => ({
markdownDocsPath: '/markdown/docs',
quickActionsDocsPath: '/quickactions/docs',
}),
getNoteableData: () => ({
id: 1,
preview_note_path: '/preview',
}),
noteableType: () => 'merge_request',
getCurrentUserLastNote: () => getCurrentUserLastNote,
},
modules: {
diffs: {
namespaced: true,
state: {
projectPath: 'gitlab-org/gitlab',
},
},
},
});
wrapper = mountExtended(SubmitDropdown, {
store,
pinia,
apolloProvider,
});
}
const findCommentTextarea = () => wrapper.findByTestId('comment-textarea');
const findSubmitButton = () => wrapper.findByTestId('submit-review-button');
const findForm = () => wrapper.findByTestId('submit-gl-form');
const findSubmitDropdown = () => wrapper.findComponent(GlDisclosureDropdown);
describe('Batch comments submit dropdown', () => {
beforeEach(() => {
pinia = createTestingPinia({ plugins: [globalAccessorPlugin] });
useLegacyDiffs();
useNotes();
});
afterEach(() => {
window.mrTabs = null;
});
it('calls publishReview with note data', async () => {
factory();
await findCommentTextarea().setValue('Hello world');
await findForm().vm.$emit('submit', { preventDefault: jest.fn() });
expect(useBatchComments().publishReview).toHaveBeenCalledWith({
noteable_type: 'merge_request',
noteable_id: 1,
note: 'Hello world',
approve: false,
approval_password: '',
reviewer_state: 'reviewed',
});
});
it('emits CLEAR_AUTOSAVE_ENTRY_EVENT with autosave key', async () => {
factory();
findCommentTextarea().setValue('Hello world');
findForm().vm.$emit('submit', { preventDefault: jest.fn() });
await waitForPromises();
expect(markdownEditorEventHub.$emit).toHaveBeenCalledWith(
CLEAR_AUTOSAVE_ENTRY_EVENT,
'submit_review_dropdown/1',
);
});
it('clears textarea value', async () => {
factory();
findCommentTextarea().setValue('Hello world');
findForm().vm.$emit('submit', { preventDefault: jest.fn() });
await waitForPromises();
expect(findCommentTextarea().element.value).toBe('');
});
it('tracks submit action', () => {
factory();
findCommentTextarea().setValue('Hello world');
findForm().vm.$emit('submit', { preventDefault: jest.fn() });
expect(trackingSpy).toHaveBeenCalledWith(undefined, 'save_markdown', {
label: 'markdown_editor',
property: 'MergeRequest_review',
});
});
it('switches to the overview tab after submit', async () => {
window.mrTabs = { tabShown: jest.fn() };
factory();
findCommentTextarea().setValue('Hello world');
await findForm().vm.$emit('submit', { preventDefault: jest.fn() });
await waitForPromises();
getCurrentUserLastNote.id = 2;
await Vue.nextTick();
expect(window.mrTabs.tabShown).toHaveBeenCalledWith('show');
});
it('sets submit dropdown to loading', async () => {
factory();
findCommentTextarea().setValue('Hello world');
await findForm().vm.$emit('submit', { preventDefault: jest.fn() });
expect(findSubmitButton().props('loading')).toBe(true);
});
it.each`
canApprove | exists | existsText
${true} | ${undefined} | ${'shows'}
${false} | ${'disabled'} | ${'hides'}
`(
'$existsText approve checkbox if can_approve is $canApprove',
async ({ canApprove, exists }) => {
factory({ canApprove });
wrapper.findComponent(GlDisclosureDropdown).vm.$emit('shown');
await waitForPromises();
expect(wrapper.findAll('input').at(1).attributes('disabled')).toBe(exists);
},
);
it.each`
shouldAnimateReviewButton | animationClassApplied | classText
${true} | ${true} | ${'applies'}
${false} | ${false} | ${'does not apply'}
`(
'$classText animation class to `Finish review` button if `shouldAnimateReviewButton` is $shouldAnimateReviewButton',
({ shouldAnimateReviewButton, animationClassApplied }) => {
factory({ shouldAnimateReviewButton });
expect(findSubmitDropdown().classes('submit-review-dropdown-animated')).toBe(
animationClassApplied,
);
},
);
it('renders a radio group with review state options', async () => {
factory();
await waitForPromises();
expect(wrapper.findAll('.gl-form-radio').length).toBe(3);
});
it('renders disabled approve radio button when user can not approve', async () => {
factory({ mrRequestChanges: true, canApprove: false });
wrapper.findComponent(GlDisclosureDropdown).vm.$emit('shown');
await waitForPromises();
expect(wrapper.find('.custom-control-input[value="approved"]').attributes('disabled')).toBe(
'disabled',
);
});
it.each`
value
${'approved'}
${'reviewed'}
${'requested_changes'}
`('sends $value review state to api when submitting', async ({ value }) => {
factory();
wrapper.findComponent(GlDisclosureDropdown).vm.$emit('shown');
await waitForPromises();
await wrapper.find(`.custom-control-input[value="${value}"]`).trigger('change');
findCommentTextarea().setValue('Hello world');
findForm().vm.$emit('submit', { preventDefault: jest.fn() });
expect(useBatchComments().publishReview).toHaveBeenCalledWith({
noteable_type: 'merge_request',
noteable_id: 1,
note: 'Hello world',
approve: false,
approval_password: '',
reviewer_state: value,
});
});
});

View File

@ -8,7 +8,6 @@ import { globalAccessorPlugin } from '~/pinia/plugins';
import { useLegacyDiffs } from '~/diffs/stores/legacy_diffs';
import { useNotes } from '~/notes/store/legacy_notes';
import { useBatchComments } from '~/batch_comments/store';
import { observeElementOnce } from '~/lib/utils/dom_utils';
jest.mock('~/lib/utils/dom_utils');
@ -87,21 +86,6 @@ describe('FileBrowserHeight', () => {
expect(filler.props('stickyTopOffset')).toBe(topPadding);
expect(filler.props('stickyBottomOffset')).toBe(bottomPadding);
});
it('updates bottom offset when review bar is shown', async () => {
const reviewBarHeight = 50;
let callback;
useBatchComments().drafts = [{}];
useBatchComments().reviewBarRendered = true;
observeElementOnce.mockImplementation((element, cb) => {
callback = cb;
});
createComponent();
callback([{ boundingClientRect: { height: reviewBarHeight } }]);
await nextTick();
const filler = wrapper.findComponent(StickyViewportFillerHeight);
expect(filler.props('stickyBottomOffset')).toBe(bottomPadding + reviewBarHeight);
});
});
describe('when screen is narrow', () => {

View File

@ -92,9 +92,7 @@ describe('Merge requests sticky header component', () => {
describe('submit review', () => {
it('renders submit review button', () => {
createComponent({
provide: { glFeatures: { improvedReviewExperience: true } },
});
createComponent();
expect(wrapper.findComponent(SubmitReviewButton).exists()).toBe(true);
});

View File

@ -7,6 +7,7 @@ import { isLoggedIn } from '~/lib/utils/common_utils';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import setWindowLocation from 'helpers/set_window_location_helper';
import { useRealDate } from 'helpers/fake_date';
import WorkItemLoading from '~/work_items/components/work_item_loading.vue';
import WorkItemDetail from '~/work_items/components/work_item_detail.vue';
import WorkItemActions from '~/work_items/components/work_item_actions.vue';
@ -165,6 +166,7 @@ describe('WorkItemDetail component', () => {
hasLinkedItemsEpicsFeature = true,
showSidebar = true,
newCommentTemplatePaths = [],
lastRealtimeUpdatedAt = new Date('2023-01-01T12:00:00.000Z'),
} = {}) => {
wrapper = shallowMountExtended(WorkItemDetail, {
apolloProvider: createMockApollo([
@ -192,6 +194,7 @@ describe('WorkItemDetail component', () => {
updateInProgress,
error,
showSidebar,
lastRealtimeUpdatedAt,
};
},
provide: {
@ -1468,4 +1471,32 @@ describe('WorkItemDetail component', () => {
expect(findWorkItemDesigns().props('canPasteDesign')).toBe(true);
});
describe('when websocket is reconnecting', () => {
useRealDate();
it('refetches work item when `actioncable:reconnected` event is emitted', async () => {
createComponent();
await waitForPromises();
expect(successHandler).toHaveBeenCalledTimes(1);
document.dispatchEvent(new CustomEvent('actioncable:reconnected'));
await waitForPromises();
expect(successHandler).toHaveBeenCalledTimes(2);
});
it('does not refetch work item if less than 5 minutes have passed since last fetch', async () => {
createComponent({ lastRealtimeUpdatedAt: new Date() });
await waitForPromises();
expect(successHandler).toHaveBeenCalledTimes(1);
document.dispatchEvent(new CustomEvent('actioncable:reconnected'));
await waitForPromises();
expect(successHandler).toHaveBeenCalledTimes(1);
});
});
});

View File

@ -291,4 +291,28 @@ RSpec.describe 'getting a collection of projects', feature_category: :source_cod
end
end
end
context 'when providing namespace_path filter' do
let_it_be(:path) { %i[projects nodes] }
let_it_be(:group) { create(:group, owners: [current_user]) }
let_it_be(:project) { create(:project, group: group, owners: [current_user]) }
before do
post_graphql(query, current_user: current_user)
end
subject { graphql_data_at(*path).pluck('id') }
context 'when `namespace_path` has match' do
let(:filters) { { namespace_path: group.full_path } }
it { is_expected.to contain_exactly(project.to_global_id.to_s) }
end
context 'when `namespace_path` has no match' do
let(:filters) { { namespace_path: 'non_existent_path' } }
it { is_expected.to be_empty }
end
end
end