Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
c922904e97
commit
c5198a0a78
|
|
@ -1 +1 @@
|
|||
31965b083fa3accbc788af7cda2e69359879b221
|
||||
889fff0a39dc1d79dc585f345e4fd82105b57a7f
|
||||
|
|
|
|||
|
|
@ -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'));
|
||||
}
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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') }}
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
},
|
||||
});
|
||||
};
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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];
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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];
|
||||
|
|
|
|||
|
|
@ -4,6 +4,5 @@ export default () => ({
|
|||
isPublishing: false,
|
||||
currentlyPublishingDrafts: [],
|
||||
shouldAnimateReviewButton: false,
|
||||
reviewBarRendered: false,
|
||||
isMergeRequest: false,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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 }}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
|
|
|||
|
|
@ -275,7 +275,7 @@ export default {
|
|||
issuable-type="merge_request"
|
||||
/>
|
||||
</template>
|
||||
<submit-review-button v-if="glFeatures.improvedReviewExperience" />
|
||||
<submit-review-button />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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};
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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) +
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -0,0 +1 @@
|
|||
efd929efe3f9d02b551ade39b906564ef1d6e00349162b3bb0da883ea9167c5a
|
||||
|
|
@ -0,0 +1 @@
|
|||
cfe361eec5f6eca86a20a587334cb4462a416e8491cac63849a7519b09fbbdcc
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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". |
|
||||
|
|
|
|||
|
|
@ -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
|
||||
```
|
||||
````
|
||||
|
|
|
|||
|
|
@ -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
|
||||
```
|
||||
````
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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 ""
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 \
|
||||
|
|
|
|||
|
|
@ -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 } }
|
||||
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
`;
|
||||
|
|
@ -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`);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -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();
|
||||
},
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
@ -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,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -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', () => {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in New Issue