Add latest changes from gitlab-org/gitlab@master
|
|
@ -1,5 +1,5 @@
|
|||
<script>
|
||||
import { GlForm, GlModal, GlAlert } from '@gitlab/ui';
|
||||
import { GlForm, GlModal, GlAlert, GlButton } from '@gitlab/ui';
|
||||
import { visitUrl } from '~/lib/utils/url_utility';
|
||||
import { __, s__ } from '~/locale';
|
||||
import { formType } from '../constants';
|
||||
|
|
@ -32,6 +32,7 @@ export default {
|
|||
[formType.edit]: { title: s__('Board|Configure board'), btnText: __('Save changes') },
|
||||
scopeModalTitle: s__('Board|Board configuration'),
|
||||
cancelButtonText: __('Cancel'),
|
||||
deleteButtonText: s__('Board|Delete board'),
|
||||
deleteErrorMessage: s__('Board|Failed to delete board. Please try again.'),
|
||||
saveErrorMessage: __('Unable to save your changes. Please try again.'),
|
||||
deleteConfirmationMessage: s__('Board|Are you sure you want to delete this board?'),
|
||||
|
|
@ -41,6 +42,7 @@ export default {
|
|||
components: {
|
||||
BoardScope: () => import('ee_component/boards/components/board_scope.vue'),
|
||||
GlModal,
|
||||
GlButton,
|
||||
BoardConfigurationOptions,
|
||||
GlAlert,
|
||||
GlForm,
|
||||
|
|
@ -82,6 +84,11 @@ export default {
|
|||
type: String,
|
||||
required: true,
|
||||
},
|
||||
showDelete: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
|
@ -170,12 +177,16 @@ export default {
|
|||
mutationVariables() {
|
||||
return this.baseMutationVariables;
|
||||
},
|
||||
canDelete() {
|
||||
return this.canAdminBoard && this.showDelete && this.isEditForm;
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.resetFormState();
|
||||
if (this.$refs.name) {
|
||||
this.$refs.name.focus();
|
||||
}
|
||||
this.$emit('shown');
|
||||
},
|
||||
methods: {
|
||||
setError,
|
||||
|
|
@ -197,6 +208,9 @@ export default {
|
|||
|
||||
return response.data.updateBoard.board;
|
||||
},
|
||||
openDeleteModal() {
|
||||
this.$emit('showBoardModal', this.$options.formType.delete);
|
||||
},
|
||||
async deleteBoard() {
|
||||
await this.$apollo.mutate({
|
||||
mutation: this.deleteMutation,
|
||||
|
|
@ -265,6 +279,7 @@ export default {
|
|||
this.$set(this.board, 'weight', weight);
|
||||
},
|
||||
},
|
||||
formType,
|
||||
};
|
||||
</script>
|
||||
|
||||
|
|
@ -331,5 +346,23 @@ export default {
|
|||
@set-weight="setWeight"
|
||||
/>
|
||||
</gl-form>
|
||||
<template v-if="canDelete" #modal-footer>
|
||||
<div class="gl-display-flex gl-justify-content-space-between gl-w-full gl-m-0">
|
||||
<gl-button
|
||||
category="secondary"
|
||||
variant="danger"
|
||||
data-testid="delete-board-button"
|
||||
@click="openDeleteModal"
|
||||
>
|
||||
{{ $options.i18n.deleteButtonText }}</gl-button
|
||||
>
|
||||
<div>
|
||||
<gl-button @click="cancel">{{ cancelProps.text }}</gl-button>
|
||||
<gl-button v-bind="primaryProps.attributes" @click="submit">{{
|
||||
primaryProps.text
|
||||
}}</gl-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</gl-modal>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -303,18 +303,6 @@ export default {
|
|||
>
|
||||
{{ s__('IssueBoards|Create new board') }}
|
||||
</gl-button>
|
||||
|
||||
<gl-button
|
||||
v-if="showDelete"
|
||||
v-gl-modal-directive="'board-config-modal'"
|
||||
block
|
||||
category="tertiary"
|
||||
variant="danger"
|
||||
class="gl-mt-0! gl-justify-content-start!"
|
||||
@click="$emit('showBoardModal', $options.formType.delete)"
|
||||
>
|
||||
{{ s__('IssueBoards|Delete board') }}
|
||||
</gl-button>
|
||||
</div>
|
||||
</template>
|
||||
</gl-collapsible-listbox>
|
||||
|
|
@ -326,8 +314,11 @@ export default {
|
|||
:weights="weights"
|
||||
:current-board="board"
|
||||
:current-page="boardModalForm"
|
||||
:show-delete="showDelete"
|
||||
@addBoard="addBoard"
|
||||
@updateBoard="$emit('updateBoard', $event)"
|
||||
@showBoardModal="$emit('showBoardModal', $event)"
|
||||
@shown="loadBoards"
|
||||
@cancel="cancel"
|
||||
/>
|
||||
</span>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<script>
|
||||
import CommentTemplatesDropdown from '~/vue_shared/components/markdown/comment_templates_dropdown.vue';
|
||||
import CommentTemplatesModal from '~/vue_shared/components/markdown/comment_templates_modal.vue';
|
||||
import { __, sprintf } from '~/locale';
|
||||
import { getModifierKey } from '~/constants';
|
||||
import trackUIControl from '../services/track_ui_control';
|
||||
|
|
@ -17,7 +17,7 @@ export default {
|
|||
ToolbarTableButton,
|
||||
ToolbarAttachmentButton,
|
||||
ToolbarMoreDropdown,
|
||||
CommentTemplatesDropdown,
|
||||
CommentTemplatesModal,
|
||||
HeaderDivider,
|
||||
},
|
||||
inject: {
|
||||
|
|
@ -201,7 +201,7 @@ export default {
|
|||
/>
|
||||
<header-divider v-if="newCommentTemplatePaths.length" />
|
||||
</div>
|
||||
<comment-templates-dropdown
|
||||
<comment-templates-modal
|
||||
v-if="newCommentTemplatePaths.length"
|
||||
:new-comment-template-paths="newCommentTemplatePaths"
|
||||
@select="insertSavedReply"
|
||||
|
|
|
|||
|
|
@ -0,0 +1,17 @@
|
|||
<script>
|
||||
export default {
|
||||
props: {
|
||||
podName: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
namespace: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<div>{{ podName }} {{ namespace }}</div>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
<script>
|
||||
import { GlBreadcrumb } from '@gitlab/ui';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
GlBreadcrumb,
|
||||
},
|
||||
computed: {
|
||||
rootRoute() {
|
||||
const rootName = this.$route.meta.environmentName;
|
||||
return {
|
||||
text: rootName,
|
||||
to: `/`,
|
||||
};
|
||||
},
|
||||
logsRoute() {
|
||||
const { podName } = this.$route.params;
|
||||
return {
|
||||
text: `${podName}`,
|
||||
to: this.$route.path,
|
||||
};
|
||||
},
|
||||
isRootRoute() {
|
||||
return this.$route.name === 'environment_details';
|
||||
},
|
||||
isLoaded() {
|
||||
return Boolean(this.$route.meta.environmentName);
|
||||
},
|
||||
breadcrumbs() {
|
||||
if (!this.isLoaded) {
|
||||
return [];
|
||||
}
|
||||
const breadCrumbs = [this.rootRoute];
|
||||
|
||||
if (!this.isRootRoute) {
|
||||
breadCrumbs.push(this.logsRoute);
|
||||
}
|
||||
return breadCrumbs;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<gl-breadcrumb v-if="isLoaded" :items="breadcrumbs" />
|
||||
</template>
|
||||
|
|
@ -3,6 +3,8 @@ import VueApollo from 'vue-apollo';
|
|||
import VueRouter from 'vue-router';
|
||||
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
|
||||
import { removeLastSlashInUrlPath } from '~/lib/utils/url_utility';
|
||||
import { injectVueAppBreadcrumbs } from '~/lib/utils/breadcrumbs';
|
||||
import EnvironmentBreadcrumbs from './environment_details/environment_breadcrumbs.vue';
|
||||
import EnvironmentsDetailHeader from './components/environments_detail_header.vue';
|
||||
import { apolloProvider as createApolloProvider } from './graphql/client';
|
||||
import environmentsMixin from './mixins/environments_mixin';
|
||||
|
|
@ -60,8 +62,6 @@ export const initHeader = () => {
|
|||
};
|
||||
|
||||
export const initPage = async () => {
|
||||
const EnvironmentsDetailPageModule = await import('./environment_details/index.vue');
|
||||
const EnvironmentsDetailPage = EnvironmentsDetailPageModule.default;
|
||||
const dataElement = document.getElementById('environments-detail-view');
|
||||
const dataSet = convertObjectPropsToCamelCase(JSON.parse(dataElement.dataset.details));
|
||||
|
||||
|
|
@ -70,12 +70,27 @@ export const initPage = async () => {
|
|||
|
||||
const router = new VueRouter({
|
||||
mode: 'history',
|
||||
base: window.location.pathname,
|
||||
base: dataSet.basePath,
|
||||
routes: [
|
||||
{
|
||||
path: '/k8s/namespace/:namespace/pods/:podName/logs',
|
||||
name: 'logs',
|
||||
meta: {
|
||||
environmentName: dataSet.name,
|
||||
},
|
||||
component: () => import('./environment_details/components/kubernetes/kubernetes_logs.vue'),
|
||||
props: (route) => ({
|
||||
podName: route.params.podName,
|
||||
namespace: route.params.namespace,
|
||||
}),
|
||||
},
|
||||
{
|
||||
path: '/',
|
||||
name: 'environment_details',
|
||||
component: EnvironmentsDetailPage,
|
||||
meta: {
|
||||
environmentName: dataSet.name,
|
||||
},
|
||||
component: () => import('./environment_details/index.vue'),
|
||||
props: (route) => ({
|
||||
after: route.query.after,
|
||||
before: route.query.before,
|
||||
|
|
@ -92,6 +107,8 @@ export const initPage = async () => {
|
|||
},
|
||||
});
|
||||
|
||||
injectVueAppBreadcrumbs(router, EnvironmentBreadcrumbs);
|
||||
|
||||
return new Vue({
|
||||
el,
|
||||
apolloProvider,
|
||||
|
|
|
|||
|
|
@ -0,0 +1 @@
|
|||
import '../show';
|
||||
|
|
@ -2,18 +2,10 @@
|
|||
import { GlSearchBoxByType, GlOutsideDirective as Outside, GlModal } from '@gitlab/ui';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { mapState, mapActions, mapGetters } from 'vuex';
|
||||
import { debounce, clamp } from 'lodash';
|
||||
import { debounce } from 'lodash';
|
||||
import { visitUrl } from '~/lib/utils/url_utility';
|
||||
import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
|
||||
import { sprintf } from '~/locale';
|
||||
import {
|
||||
ARROW_DOWN_KEY,
|
||||
ARROW_UP_KEY,
|
||||
END_KEY,
|
||||
HOME_KEY,
|
||||
ESC_KEY,
|
||||
NUMPAD_ENTER_KEY,
|
||||
} from '~/lib/utils/keys';
|
||||
import {
|
||||
COMMAND_PALETTE,
|
||||
MIN_SEARCH_TERM,
|
||||
|
|
@ -23,6 +15,7 @@ import {
|
|||
SEARCH_RESULTS_LOADING,
|
||||
COMMAND_PALETTE_TIP,
|
||||
} from '~/vue_shared/global_search/constants';
|
||||
import modalKeyboardNavigationMixin from '~/vue_shared/mixins/modal_keyboard_navigation_mixin';
|
||||
import { darkModeEnabled } from '~/lib/utils/color_utils';
|
||||
import ScrollScrim from '~/super_sidebar/components/scroll_scrim.vue';
|
||||
import {
|
||||
|
|
@ -30,8 +23,6 @@ import {
|
|||
SEARCH_RESULTS_DESCRIPTION,
|
||||
SEARCH_SHORTCUTS_MIN_CHARACTERS,
|
||||
SEARCH_MODAL_ID,
|
||||
SEARCH_INPUT_SELECTOR,
|
||||
SEARCH_RESULTS_ITEM_SELECTOR,
|
||||
KEY_K,
|
||||
} from '../constants';
|
||||
import CommandPaletteItems from '../command_palette/command_palette_items.vue';
|
||||
|
|
@ -71,6 +62,7 @@ export default {
|
|||
ScrollScrim,
|
||||
CommandsOverviewDropdown,
|
||||
},
|
||||
mixins: [modalKeyboardNavigationMixin],
|
||||
data() {
|
||||
return {
|
||||
nextFocusedItemIndex: null,
|
||||
|
|
@ -152,51 +144,6 @@ export default {
|
|||
this.fetchAutocompleteOptions();
|
||||
}
|
||||
}, DEFAULT_DEBOUNCE_AND_THROTTLE_MS),
|
||||
getFocusableOptions() {
|
||||
return Array.from(
|
||||
this.$refs.resultsList?.querySelectorAll(SEARCH_RESULTS_ITEM_SELECTOR) || [],
|
||||
);
|
||||
},
|
||||
onKeydown(event) {
|
||||
const { code, target } = event;
|
||||
|
||||
let stop = true;
|
||||
|
||||
const elements = this.getFocusableOptions();
|
||||
if (elements.length < 1) return;
|
||||
|
||||
const isSearchInput = target.matches(SEARCH_INPUT_SELECTOR);
|
||||
|
||||
if (code === HOME_KEY) {
|
||||
if (isSearchInput) return;
|
||||
|
||||
this.focusItem(0, elements);
|
||||
} else if (code === END_KEY) {
|
||||
if (isSearchInput) return;
|
||||
|
||||
this.focusItem(elements.length - 1, elements);
|
||||
} else if (code === ARROW_UP_KEY) {
|
||||
if (isSearchInput) return;
|
||||
|
||||
if (elements.indexOf(target) === 0) {
|
||||
this.focusSearchInput();
|
||||
} else {
|
||||
this.focusNextItem(event, elements, -1);
|
||||
}
|
||||
} else if (code === ARROW_DOWN_KEY) {
|
||||
this.focusNextItem(event, elements, 1);
|
||||
} else if (code === ESC_KEY) {
|
||||
this.$refs.searchModal.close();
|
||||
} else if (code === NUMPAD_ENTER_KEY) {
|
||||
event.target?.firstChild.click();
|
||||
} else {
|
||||
stop = false;
|
||||
}
|
||||
|
||||
if (stop) {
|
||||
event.preventDefault();
|
||||
}
|
||||
},
|
||||
onKeyComboDown(event) {
|
||||
const { code, metaKey } = event;
|
||||
|
||||
|
|
@ -209,21 +156,6 @@ export default {
|
|||
this.commandPaletteDropdownOpen = !this.commandPaletteDropdownOpen;
|
||||
}
|
||||
},
|
||||
focusSearchInput() {
|
||||
this.$refs.searchInput.$el.querySelector('input')?.focus();
|
||||
},
|
||||
focusNextItem(event, elements, offset) {
|
||||
const { target } = event;
|
||||
const currentIndex = elements.indexOf(target);
|
||||
const nextIndex = clamp(currentIndex + offset, 0, elements.length - 1);
|
||||
|
||||
this.focusItem(nextIndex, elements);
|
||||
},
|
||||
focusItem(index, elements) {
|
||||
this.nextFocusedItemIndex = index;
|
||||
|
||||
elements[index]?.focus();
|
||||
},
|
||||
submitSearch() {
|
||||
if (this.isCommandMode) {
|
||||
this.runFirstCommand();
|
||||
|
|
@ -283,7 +215,7 @@ export default {
|
|||
|
||||
<template>
|
||||
<gl-modal
|
||||
ref="searchModal"
|
||||
ref="modal"
|
||||
:modal-id="$options.SEARCH_MODAL_ID"
|
||||
hide-header
|
||||
hide-header-close
|
||||
|
|
|
|||
|
|
@ -271,6 +271,8 @@ export const Tracker = {
|
|||
if (window.glClient) {
|
||||
window.glClient?.setReferrerUrl(pageLinks.referrer);
|
||||
}
|
||||
} else {
|
||||
window.snowplow('setReferrerUrl', window.gl?.maskedDefaultReferrerUrl);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,186 +0,0 @@
|
|||
<script>
|
||||
import { GlCollapsibleListbox, GlTooltip, GlButton } from '@gitlab/ui';
|
||||
import fuzzaldrinPlus from 'fuzzaldrin-plus';
|
||||
import { getDerivedMergeRequestInformation } from '~/diffs/utils/merge_request';
|
||||
import { InternalEvents } from '~/tracking';
|
||||
import savedRepliesQuery from 'ee_else_ce/vue_shared/components/markdown/saved_replies.query.graphql';
|
||||
import {
|
||||
TRACKING_SAVED_REPLIES_USE,
|
||||
TRACKING_SAVED_REPLIES_USE_IN_MR,
|
||||
TRACKING_SAVED_REPLIES_USE_IN_OTHER,
|
||||
COMMENT_TEMPLATES_KEYS,
|
||||
COMMENT_TEMPLATES_TITLES,
|
||||
} from 'ee_else_ce/vue_shared/components/markdown/constants';
|
||||
|
||||
export default {
|
||||
apollo: {
|
||||
savedReplies: {
|
||||
query: savedRepliesQuery,
|
||||
manual: true,
|
||||
result({ data, loading }) {
|
||||
if (!loading) {
|
||||
this.savedReplies = data;
|
||||
}
|
||||
},
|
||||
variables() {
|
||||
const groupPath = document.body.dataset.groupFullPath;
|
||||
const projectPath = document.body.dataset.projectFullPath;
|
||||
|
||||
return {
|
||||
groupPath,
|
||||
hideGroup: !groupPath,
|
||||
projectPath,
|
||||
hideProject: !projectPath,
|
||||
};
|
||||
},
|
||||
skip() {
|
||||
return !this.shouldFetchCommentTemplates;
|
||||
},
|
||||
},
|
||||
},
|
||||
components: {
|
||||
GlCollapsibleListbox,
|
||||
GlButton,
|
||||
GlTooltip,
|
||||
},
|
||||
mixins: [InternalEvents.mixin()],
|
||||
props: {
|
||||
newCommentTemplatePaths: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
shouldFetchCommentTemplates: false,
|
||||
savedReplies: {},
|
||||
commentTemplateSearch: '',
|
||||
loadingSavedReplies: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
allSavedReplies() {
|
||||
return COMMENT_TEMPLATES_KEYS.map((key) => ({
|
||||
text: COMMENT_TEMPLATES_TITLES[key],
|
||||
options: (this.savedReplies[key]?.savedReplies?.nodes || []).map((r) => ({
|
||||
value: r.id,
|
||||
text: r.name,
|
||||
content: r.content,
|
||||
})),
|
||||
}));
|
||||
},
|
||||
filteredSavedReplies() {
|
||||
let savedReplies = this.allSavedReplies;
|
||||
|
||||
if (this.commentTemplateSearch) {
|
||||
savedReplies = savedReplies
|
||||
.map((group) => ({
|
||||
...group,
|
||||
options: fuzzaldrinPlus.filter(group.options, this.commentTemplateSearch, {
|
||||
key: ['text'],
|
||||
}),
|
||||
}))
|
||||
.filter(({ options }) => options.length);
|
||||
}
|
||||
|
||||
return savedReplies.filter(({ options }) => options.length);
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.tooltipTarget = this.$el.querySelector('.js-comment-template-toggle');
|
||||
},
|
||||
methods: {
|
||||
fetchCommentTemplates() {
|
||||
this.shouldFetchCommentTemplates = true;
|
||||
},
|
||||
setCommentTemplateSearch(search) {
|
||||
this.commentTemplateSearch = search;
|
||||
},
|
||||
onSelect(id) {
|
||||
let savedReply;
|
||||
const isInMr = Boolean(getDerivedMergeRequestInformation({ endpoint: window.location }).id);
|
||||
|
||||
for (let i = 0, len = this.allSavedReplies.length; i < len; i += 1) {
|
||||
const { options } = this.allSavedReplies[i];
|
||||
savedReply = options.find(({ value }) => value === id);
|
||||
|
||||
if (savedReply) break;
|
||||
}
|
||||
|
||||
if (savedReply) {
|
||||
this.$emit('select', savedReply.content);
|
||||
this.trackEvent(TRACKING_SAVED_REPLIES_USE);
|
||||
this.trackEvent(
|
||||
isInMr ? TRACKING_SAVED_REPLIES_USE_IN_MR : TRACKING_SAVED_REPLIES_USE_IN_OTHER,
|
||||
);
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<span>
|
||||
<gl-collapsible-listbox
|
||||
:header-text="__('Insert comment template')"
|
||||
:items="filteredSavedReplies"
|
||||
:toggle-text="__('Insert comment template')"
|
||||
text-sr-only
|
||||
no-caret
|
||||
toggle-class="js-comment-template-toggle"
|
||||
icon="comment-lines"
|
||||
category="tertiary"
|
||||
placement="right"
|
||||
searchable
|
||||
size="small"
|
||||
class="comment-template-dropdown gl-mr-2"
|
||||
positioning-strategy="fixed"
|
||||
:searching="$apollo.queries.savedReplies.loading"
|
||||
@shown="fetchCommentTemplates"
|
||||
@search="setCommentTemplateSearch"
|
||||
@select="onSelect"
|
||||
>
|
||||
<template #list-item="{ item }">
|
||||
<div class="gl-display-flex js-comment-template-content">
|
||||
<div class="gl-text-truncate">
|
||||
<strong>{{ item.text }}</strong
|
||||
><span class="gl-ml-2">{{ item.content }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #footer>
|
||||
<div
|
||||
class="gl-border-t-solid gl-border-t-1 gl-border-t-gray-200 gl-display-flex gl-justify-content-center gl-flex-direction-column gl-p-2"
|
||||
>
|
||||
<gl-button
|
||||
v-for="(manage, index) in newCommentTemplatePaths"
|
||||
:key="index"
|
||||
:href="manage.path"
|
||||
category="tertiary"
|
||||
block
|
||||
class="gl-justify-content-start! gl-mt-0! gl-mb-0! gl-px-3!"
|
||||
data-testid="manage-button"
|
||||
>{{ manage.text }}</gl-button
|
||||
>
|
||||
</div>
|
||||
</template>
|
||||
</gl-collapsible-listbox>
|
||||
<gl-tooltip :target="() => tooltipTarget">
|
||||
{{ __('Insert comment template') }}
|
||||
</gl-tooltip>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
.comment-template-dropdown .gl-new-dropdown-panel {
|
||||
width: 350px !important;
|
||||
}
|
||||
|
||||
.comment-template-dropdown .gl-new-dropdown-item-check-icon {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.comment-template-dropdown input {
|
||||
border-radius: 0;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,196 @@
|
|||
<script>
|
||||
import {
|
||||
GlTooltipDirective,
|
||||
GlButton,
|
||||
GlModal,
|
||||
GlSearchBoxByType,
|
||||
GlTruncate,
|
||||
GlDisclosureDropdown,
|
||||
GlDisclosureDropdownGroup,
|
||||
} from '@gitlab/ui';
|
||||
import fuzzaldrinPlus from 'fuzzaldrin-plus';
|
||||
import { uniqueId } from 'lodash';
|
||||
import modalKeyboardNavigationMixin from '~/vue_shared/mixins/modal_keyboard_navigation_mixin';
|
||||
import { getDerivedMergeRequestInformation } from '~/diffs/utils/merge_request';
|
||||
import { InternalEvents } from '~/tracking';
|
||||
import savedRepliesQuery from 'ee_else_ce/vue_shared/components/markdown/saved_replies.query.graphql';
|
||||
import {
|
||||
TRACKING_SAVED_REPLIES_USE,
|
||||
TRACKING_SAVED_REPLIES_USE_IN_MR,
|
||||
TRACKING_SAVED_REPLIES_USE_IN_OTHER,
|
||||
COMMENT_TEMPLATES_KEYS,
|
||||
COMMENT_TEMPLATES_TITLES,
|
||||
} from 'ee_else_ce/vue_shared/components/markdown/constants';
|
||||
|
||||
export default {
|
||||
apollo: {
|
||||
savedReplies: {
|
||||
query: savedRepliesQuery,
|
||||
manual: true,
|
||||
result({ data, loading }) {
|
||||
if (!loading) {
|
||||
this.savedReplies = data;
|
||||
}
|
||||
},
|
||||
variables() {
|
||||
const groupPath = document.body.dataset.groupFullPath;
|
||||
const projectPath = document.body.dataset.projectFullPath;
|
||||
|
||||
return {
|
||||
groupPath,
|
||||
hideGroup: !groupPath,
|
||||
projectPath,
|
||||
hideProject: !projectPath,
|
||||
};
|
||||
},
|
||||
skip() {
|
||||
return !this.shouldFetchCommentTemplates;
|
||||
},
|
||||
},
|
||||
},
|
||||
components: {
|
||||
GlButton,
|
||||
GlModal,
|
||||
GlSearchBoxByType,
|
||||
GlTruncate,
|
||||
GlDisclosureDropdown,
|
||||
GlDisclosureDropdownGroup,
|
||||
},
|
||||
directives: {
|
||||
GlTooltip: GlTooltipDirective,
|
||||
},
|
||||
mixins: [InternalEvents.mixin(), modalKeyboardNavigationMixin],
|
||||
props: {
|
||||
newCommentTemplatePaths: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
shouldFetchCommentTemplates: false,
|
||||
savedReplies: {},
|
||||
commentTemplateSearch: '',
|
||||
loadingSavedReplies: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
allSavedReplies() {
|
||||
return COMMENT_TEMPLATES_KEYS.map((key) => ({
|
||||
name: COMMENT_TEMPLATES_TITLES[key],
|
||||
items: (this.savedReplies[key]?.savedReplies?.nodes || []).map((r) => ({
|
||||
value: r.id,
|
||||
text: r.name,
|
||||
content: r.content,
|
||||
})),
|
||||
}));
|
||||
},
|
||||
filteredSavedReplies() {
|
||||
let savedReplies = this.allSavedReplies;
|
||||
|
||||
if (this.commentTemplateSearch) {
|
||||
savedReplies = savedReplies
|
||||
.map((group) => ({
|
||||
...group,
|
||||
items: fuzzaldrinPlus.filter(group.items, this.commentTemplateSearch, {
|
||||
key: ['text'],
|
||||
}),
|
||||
}))
|
||||
.filter(({ items }) => items.length);
|
||||
}
|
||||
|
||||
return savedReplies.filter(({ items }) => items.length);
|
||||
},
|
||||
modalId() {
|
||||
return uniqueId('insert-comment-template-modal-');
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
onSelect(savedReply) {
|
||||
const isInMr = Boolean(getDerivedMergeRequestInformation({ endpoint: window.location }).id);
|
||||
|
||||
this.$emit('select', savedReply.content);
|
||||
this.trackEvent(TRACKING_SAVED_REPLIES_USE);
|
||||
this.trackEvent(
|
||||
isInMr ? TRACKING_SAVED_REPLIES_USE_IN_MR : TRACKING_SAVED_REPLIES_USE_IN_OTHER,
|
||||
);
|
||||
this.shouldFetchCommentTemplates = false;
|
||||
},
|
||||
toggleModal() {
|
||||
this.shouldFetchCommentTemplates = !this.shouldFetchCommentTemplates;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<span>
|
||||
<gl-modal
|
||||
ref="modal"
|
||||
v-model="shouldFetchCommentTemplates"
|
||||
:title="__('Select a comment template')"
|
||||
scrollable
|
||||
:modal-id="modalId"
|
||||
modal-class="comment-templates-modal"
|
||||
>
|
||||
<gl-search-box-by-type
|
||||
ref="searchInput"
|
||||
v-model="commentTemplateSearch"
|
||||
:placeholder="__('Search comment templates')"
|
||||
@keydown="onKeydown"
|
||||
/>
|
||||
<section v-if="!filteredSavedReplies.length" class="gl-mt-3">
|
||||
{{ __('No comment templates found.') }}
|
||||
</section>
|
||||
<ul
|
||||
v-else
|
||||
ref="resultsList"
|
||||
class="gl-m-0 gl-p-0 gl-list-none comment-templates-options"
|
||||
data-testid="comment-templates-list"
|
||||
@keydown="onKeydown"
|
||||
>
|
||||
<gl-disclosure-dropdown-group
|
||||
v-for="(commentTemplateGroup, index) in filteredSavedReplies"
|
||||
:key="commentTemplateGroup.name"
|
||||
:class="{ 'gl-mt-0! gl-border-t-0! gl-pt-0': index === 0 }"
|
||||
:group="commentTemplateGroup"
|
||||
bordered
|
||||
@action="onSelect"
|
||||
>
|
||||
<template #list-item="{ item }">
|
||||
<strong class="gl-block gl-w-full">{{ item.text }}</strong>
|
||||
<gl-truncate class="gl-mt-2" :text="item.content" position="end" />
|
||||
</template>
|
||||
</gl-disclosure-dropdown-group>
|
||||
</ul>
|
||||
<template #modal-footer>
|
||||
<gl-disclosure-dropdown
|
||||
:items="newCommentTemplatePaths"
|
||||
:toggle-text="__('Manage comment templates')"
|
||||
placement="bottom-end"
|
||||
fluid-width
|
||||
data-testid="manage-dropdown"
|
||||
>
|
||||
<template #header>
|
||||
<div
|
||||
class="gl-font-sm gl-font-weight-bold gl-p-4! gl-min-h-8 gl-border-b-1 gl-border-b-solid gl-border-b-gray-200"
|
||||
>
|
||||
{{ __('Manage') }}
|
||||
</div>
|
||||
</template>
|
||||
</gl-disclosure-dropdown>
|
||||
</template>
|
||||
</gl-modal>
|
||||
<gl-button
|
||||
v-gl-tooltip
|
||||
:title="__('Insert comment template')"
|
||||
:aria-label="__('Insert comment template')"
|
||||
category="tertiary"
|
||||
size="small"
|
||||
icon="comment-lines"
|
||||
class="js-comment-template-toggle"
|
||||
data-testid="comment-templates-dropdown-toggle"
|
||||
@click="toggleModal"
|
||||
/>
|
||||
</span>
|
||||
</template>
|
||||
|
|
@ -20,7 +20,7 @@ import { CopyAsGFM } from '~/behaviors/markdown/copy_as_gfm';
|
|||
import { updateText } from '~/lib/utils/text_markdown';
|
||||
import ToolbarButton from './toolbar_button.vue';
|
||||
import DrawioToolbarButton from './drawio_toolbar_button.vue';
|
||||
import CommentTemplatesDropdown from './comment_templates_dropdown.vue';
|
||||
import CommentTemplatesModal from './comment_templates_modal.vue';
|
||||
import HeaderDivider from './header_divider.vue';
|
||||
|
||||
export default {
|
||||
|
|
@ -29,7 +29,7 @@ export default {
|
|||
GlPopover,
|
||||
GlButton,
|
||||
DrawioToolbarButton,
|
||||
CommentTemplatesDropdown,
|
||||
CommentTemplatesModal,
|
||||
AiActionsDropdown: () => import('ee_component/ai/components/ai_actions_dropdown.vue'),
|
||||
HeaderDivider,
|
||||
},
|
||||
|
|
@ -235,6 +235,10 @@ export default {
|
|||
},
|
||||
insertSavedReply(savedReply) {
|
||||
this.insertIntoTextarea(savedReply);
|
||||
|
||||
setTimeout(() => {
|
||||
this.$el.closest('.md-area')?.querySelector('textarea')?.focus();
|
||||
}, 500);
|
||||
},
|
||||
},
|
||||
shortcuts: {
|
||||
|
|
@ -521,7 +525,7 @@ export default {
|
|||
icon="quick-actions"
|
||||
tracking-property="quickAction"
|
||||
/>
|
||||
<comment-templates-dropdown
|
||||
<comment-templates-modal
|
||||
v-if="!previewMarkdown && newCommentTemplatePaths.length"
|
||||
:new-comment-template-paths="newCommentTemplatePaths"
|
||||
@select="insertSavedReply"
|
||||
|
|
|
|||
|
|
@ -0,0 +1,72 @@
|
|||
import { clamp } from 'lodash';
|
||||
import {
|
||||
ARROW_DOWN_KEY,
|
||||
ARROW_UP_KEY,
|
||||
END_KEY,
|
||||
HOME_KEY,
|
||||
ESC_KEY,
|
||||
NUMPAD_ENTER_KEY,
|
||||
} from '~/lib/utils/keys';
|
||||
|
||||
export default {
|
||||
methods: {
|
||||
getFocusableOptions() {
|
||||
return Array.from(this.$refs.resultsList?.querySelectorAll('.gl-new-dropdown-item') || []);
|
||||
},
|
||||
onKeydown(event) {
|
||||
const { code, target } = event;
|
||||
|
||||
let stop = true;
|
||||
|
||||
const elements = this.getFocusableOptions();
|
||||
if (elements.length < 1) return;
|
||||
|
||||
const isSearchInput = target.matches('input[role="searchbox"]');
|
||||
|
||||
if (code === HOME_KEY) {
|
||||
if (isSearchInput) return;
|
||||
|
||||
this.focusItem(0, elements);
|
||||
} else if (code === END_KEY) {
|
||||
if (isSearchInput) return;
|
||||
|
||||
this.focusItem(elements.length - 1, elements);
|
||||
} else if (code === ARROW_UP_KEY) {
|
||||
if (isSearchInput) return;
|
||||
|
||||
if (elements.indexOf(target) === 0) {
|
||||
this.focusSearchInput();
|
||||
} else {
|
||||
this.focusNextItem(event, elements, -1);
|
||||
}
|
||||
} else if (code === ARROW_DOWN_KEY) {
|
||||
this.focusNextItem(event, elements, 1);
|
||||
} else if (code === ESC_KEY) {
|
||||
this.$refs.modal.close();
|
||||
} else if (code === NUMPAD_ENTER_KEY) {
|
||||
event.target?.firstChild.click();
|
||||
} else {
|
||||
stop = false;
|
||||
}
|
||||
|
||||
if (stop) {
|
||||
event.preventDefault();
|
||||
}
|
||||
},
|
||||
focusSearchInput() {
|
||||
this.$refs.searchInput.$el.querySelector('input')?.focus();
|
||||
},
|
||||
focusNextItem(event, elements, offset) {
|
||||
const { target } = event;
|
||||
const currentIndex = elements.indexOf(target);
|
||||
const nextIndex = clamp(currentIndex + offset, 0, elements.length - 1);
|
||||
|
||||
this.focusItem(nextIndex, elements);
|
||||
},
|
||||
focusItem(index, elements) {
|
||||
this.nextFocusedItemIndex = index;
|
||||
|
||||
elements[index]?.focus();
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
@ -105,3 +105,24 @@
|
|||
font-weight: $gl-font-weight-normal;
|
||||
}
|
||||
}
|
||||
|
||||
.comment-templates-modal {
|
||||
padding: 3rem 0.5rem 0;
|
||||
|
||||
&.gl-modal .modal-dialog {
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
@include gl-media-breakpoint-up(sm) {
|
||||
padding: 5rem 1rem 0;
|
||||
}
|
||||
|
||||
[id*='gl-disclosure-dropdown-group'] {
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
.comment-templates-options .gl-new-dropdown-item {
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,13 +19,13 @@ class Projects::EnvironmentsController < Projects::ApplicationController
|
|||
before_action :authorize_stop_environment!, only: [:stop]
|
||||
before_action :authorize_update_environment!, only: [:edit, :update, :cancel_auto_stop]
|
||||
before_action :authorize_admin_environment!, only: [:terminal, :terminal_websocket_authorize]
|
||||
before_action :environment, only: [:show, :edit, :update, :stop, :terminal, :terminal_websocket_authorize, :cancel_auto_stop]
|
||||
before_action :environment, only: [:show, :edit, :update, :stop, :terminal, :terminal_websocket_authorize, :cancel_auto_stop, :k8s]
|
||||
before_action :verify_api_request!, only: :terminal_websocket_authorize
|
||||
before_action :expire_etag_cache, only: [:index], unless: -> { request.format.json? }
|
||||
before_action :set_kas_cookie, only: [:edit, :new, :show], if: -> { current_user && request.format.html? }
|
||||
before_action :set_kas_cookie, only: [:edit, :new, :show, :k8s], if: -> { current_user && request.format.html? }
|
||||
after_action :expire_etag_cache, only: [:cancel_auto_stop]
|
||||
|
||||
track_event :index, :folder, :show, :new, :edit, :create, :update, :stop, :cancel_auto_stop, :terminal,
|
||||
track_event :index, :folder, :show, :new, :edit, :create, :update, :stop, :cancel_auto_stop, :terminal, :k8s,
|
||||
name: 'users_visiting_environments_pages'
|
||||
|
||||
feature_category :continuous_delivery
|
||||
|
|
@ -81,7 +81,6 @@ class Projects::EnvironmentsController < Projects::ApplicationController
|
|||
# rubocop: enable CodeReuse/ActiveRecord
|
||||
|
||||
def show
|
||||
@deployments = deployments
|
||||
end
|
||||
|
||||
def new
|
||||
|
|
@ -91,6 +90,10 @@ class Projects::EnvironmentsController < Projects::ApplicationController
|
|||
def edit
|
||||
end
|
||||
|
||||
def k8s
|
||||
render action: :show
|
||||
end
|
||||
|
||||
def create
|
||||
@environment = project.environments.create(environment_params)
|
||||
|
||||
|
|
@ -130,7 +133,7 @@ class Projects::EnvironmentsController < Projects::ApplicationController
|
|||
|
||||
def cancel_auto_stop
|
||||
result = Environments::ResetAutoStopService.new(project, current_user)
|
||||
.execute(environment)
|
||||
.execute(environment)
|
||||
|
||||
if result[:status] == :success
|
||||
respond_to do |format|
|
||||
|
|
|
|||
|
|
@ -0,0 +1,63 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Mutations
|
||||
module Projects
|
||||
class BlobsRemove < BaseMutation
|
||||
graphql_name 'projectBlobsRemove'
|
||||
|
||||
include FindsProject
|
||||
|
||||
EMPTY_BLOBS_OIDS_ARG = <<~ERROR
|
||||
Argument 'blobOids' on InputObject 'projectBlobsRemoveInput' is required. Expected type [String!]!
|
||||
ERROR
|
||||
|
||||
authorize :owner_access
|
||||
|
||||
argument :project_path, GraphQL::Types::ID,
|
||||
required: true,
|
||||
description: 'Full path of the project to replace.'
|
||||
|
||||
argument :blob_oids, [GraphQL::Types::String],
|
||||
required: true,
|
||||
description: 'List of blob oids.',
|
||||
prepare: ->(blob_oids, _ctx) do
|
||||
blob_oids.reject!(&:blank?)
|
||||
|
||||
break blob_oids if blob_oids.present?
|
||||
|
||||
raise GraphQL::ExecutionError, EMPTY_BLOBS_OIDS_ARG
|
||||
end
|
||||
|
||||
def resolve(project_path:, blob_oids:)
|
||||
project = authorized_find!(project_path)
|
||||
|
||||
begin
|
||||
project.set_repository_read_only!
|
||||
client = Gitlab::GitalyClient::CleanupService.new(project.repository)
|
||||
client.rewrite_history(blobs: blob_oids)
|
||||
|
||||
audit_removals(project, blob_oids)
|
||||
|
||||
{ errors: [] }
|
||||
ensure
|
||||
project.set_repository_writable!
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def audit_removals(project, blob_oids)
|
||||
context = {
|
||||
name: 'project_blobs_removal',
|
||||
author: current_user,
|
||||
scope: project,
|
||||
target: project,
|
||||
message: 'Project blobs removed',
|
||||
additional_details: { blob_oids: blob_oids }
|
||||
}
|
||||
|
||||
::Gitlab::Audit::Auditor.audit(context)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -111,6 +111,7 @@ module Types
|
|||
mount_mutation Mutations::Notes::Destroy
|
||||
mount_mutation Mutations::Organizations::Create, alpha: { milestone: '16.6' }
|
||||
mount_mutation Mutations::Organizations::Update, alpha: { milestone: '16.7' }
|
||||
mount_mutation Mutations::Projects::BlobsRemove, calls_gitaly: true, alpha: { milestone: '17.1' }
|
||||
mount_mutation Mutations::Projects::SyncFork, calls_gitaly: true, alpha: { milestone: '15.9' }
|
||||
mount_mutation Mutations::Projects::Star, alpha: { milestone: '16.7' }
|
||||
mount_mutation Mutations::BranchRules::Update, alpha: { milestone: '16.7' }
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ module EnvironmentHelper
|
|||
name: environment.name,
|
||||
id: environment.id,
|
||||
project_full_path: project.full_path,
|
||||
base_path: project_environment_path(project, environment),
|
||||
external_url: environment.external_url,
|
||||
can_update_environment: can?(current_user, :update_environment, environment),
|
||||
can_destroy_environment: can_destroy_environment?(environment),
|
||||
|
|
|
|||
|
|
@ -421,8 +421,8 @@ module IssuablesHelper
|
|||
|
||||
def new_comment_template_paths(group, project = nil)
|
||||
[{
|
||||
text: _('Manage your comment templates'),
|
||||
path: profile_comment_templates_path
|
||||
text: _('Your comment templates'),
|
||||
href: profile_comment_templates_path
|
||||
}]
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -2,6 +2,9 @@
|
|||
|
||||
module Routing
|
||||
module PseudonymizationHelper
|
||||
PSEUDONOMIZED_NAMESPACE = "namespace"
|
||||
PSEUDONOMIZED_PROJECT = "project"
|
||||
|
||||
class MaskHelper
|
||||
QUERY_PARAMS_TO_NOT_MASK = %w[
|
||||
scope
|
||||
|
|
@ -93,5 +96,49 @@ module Routing
|
|||
Gitlab::ErrorTracking.track_exception(e, url: request.original_fullpath)
|
||||
nil
|
||||
end
|
||||
|
||||
def masked_referrer_url(url)
|
||||
return unless url
|
||||
|
||||
params = referrer_params(url)
|
||||
|
||||
return unless params && params[:controller]
|
||||
return if params[:action] == "route_not_found"
|
||||
|
||||
case params[:controller]
|
||||
when 'groups'
|
||||
params[:id] = PSEUDONOMIZED_NAMESPACE
|
||||
when 'projects'
|
||||
params[:id] = PSEUDONOMIZED_PROJECT
|
||||
params[:namespace_id] = PSEUDONOMIZED_NAMESPACE
|
||||
else
|
||||
params[:project_id] = PSEUDONOMIZED_PROJECT if params[:project_id]
|
||||
params[:namespace_id] = PSEUDONOMIZED_NAMESPACE if params[:namespace_id]
|
||||
end
|
||||
|
||||
masked_query_params = masked_query_params(URI.parse(url))
|
||||
|
||||
Gitlab::Routing.url_helpers.url_for(params.merge(params: masked_query_params))
|
||||
end
|
||||
|
||||
def masked_query_params(uri)
|
||||
query_params = CGI.parse(uri.query.to_s)
|
||||
query_params.transform_keys!(&:downcase)
|
||||
|
||||
return if query_params.empty?
|
||||
|
||||
query_params.each do |key, _|
|
||||
query_params[key] = ["masked_#{key}"] unless MaskHelper::QUERY_PARAMS_TO_NOT_MASK.include?(key)
|
||||
end
|
||||
|
||||
query_params
|
||||
end
|
||||
|
||||
def referrer_params(url)
|
||||
Rails.application.routes.recognize_path(url)
|
||||
rescue StandardError => e
|
||||
Gitlab::ErrorTracking.track_exception(e, url: request.original_fullpath)
|
||||
nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -16,3 +16,4 @@
|
|||
user_id: current_user&.id
|
||||
).to_context.to_json.to_json}
|
||||
gl.snowplowPseudonymizedPageUrl = #{masked_page_url(group: namespace, project: @project).to_json};
|
||||
gl.maskedDefaultReferrerUrl = #{masked_referrer_url(request.referer).to_json};
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
- namespace_id = local_assigns.fetch(:namespace_id, nil)
|
||||
- page_title _('Comment templates')
|
||||
|
||||
#js-comment-templates-root.settings-section.gl-mt-3{ data: { base_path: base_path, namespace_id: namespace_id, } }
|
||||
#js-comment-templates-root.settings-section.gl-mt-3{ data: { base_path: base_path, namespace_id: namespace_id } }
|
||||
.settings-sticky-header
|
||||
.settings-sticky-header-inner
|
||||
%h4.gl-my-0
|
||||
|
|
|
|||
|
|
@ -0,0 +1,11 @@
|
|||
---
|
||||
name: project_blobs_removal
|
||||
description: Triggered when removing blobs via the GraphQL API or project settings
|
||||
UI
|
||||
introduced_by_issue: https://gitlab.com/gitlab-org/gitlab/-/issues/450701
|
||||
introduced_by_mr: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/152522
|
||||
feature_category: source_code_management
|
||||
milestone: '17.0'
|
||||
saved_to_database: true
|
||||
streamed: true
|
||||
scope: [Project]
|
||||
|
|
@ -327,6 +327,7 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
|
|||
post :stop
|
||||
post :cancel_auto_stop
|
||||
get :terminal
|
||||
get '/k8s(/*vueroute)', to: 'environments#k8s', as: :k8s_subroute
|
||||
|
||||
# This route is also defined in gitlab-workhorse. Make sure to update accordingly.
|
||||
get '/terminal.ws/authorize', to: 'environments#terminal_websocket_authorize', format: false
|
||||
|
|
|
|||
|
|
@ -445,6 +445,7 @@ Audit event types belong to the following product categories.
|
|||
| [`protected_branch_updated`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/107530) | Event triggered on the setting for protected branches is update| **{check-circle}** Yes | **{check-circle}** Yes | GitLab [15.8](https://gitlab.com/gitlab-org/gitlab/-/issues/369318) | Project |
|
||||
| [`repository_git_operation`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/76719) | Triggered when authenticated users push, pull, or clone a project using SSH, HTTP(S), or the UI| **{dotted-circle}** No | **{check-circle}** Yes | GitLab [14.9](https://gitlab.com/gitlab-org/gitlab/-/issues/373950) | Project |
|
||||
| [`manually_trigger_housekeeping`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/112095) | Triggered when manually triggering housekeeping via API or admin UI| **{check-circle}** Yes | **{check-circle}** Yes | GitLab [15.9](https://gitlab.com/gitlab-org/gitlab/-/issues/390761) | Project |
|
||||
| [`project_blobs_removal`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/152522) | Triggered when removing blobs via the GraphQL API or project settings UI| **{check-circle}** Yes | **{check-circle}** Yes | GitLab [17.0](https://gitlab.com/gitlab-org/gitlab/-/issues/450701) | Project |
|
||||
|
||||
### Subgroup
|
||||
|
||||
|
|
|
|||
|
|
@ -394,19 +394,6 @@ Although most S3 compatible services (like [MinIO](https://min.io/)) should work
|
|||
we only guarantee support for AWS S3. Because we cannot assert the correctness of third-party S3 implementations,
|
||||
we can debug issues, but we cannot patch the registry unless an issue is reproducible against an AWS S3 bucket.
|
||||
|
||||
<!--- start_remove The following content will be removed on remove_date: '2024-05-16' -->
|
||||
|
||||
WARNING:
|
||||
Support for the following drivers was [deprecated](https://gitlab.com/gitlab-org/container-registry/-/issues/1141)
|
||||
in GitLab 16.6, and is planned for removal in 17.0. This change is a breaking change.
|
||||
|
||||
| Driver | Description |
|
||||
|---------|-------------|
|
||||
| `swift` | OpenStack Swift Object Storage |
|
||||
| `oss` | Aliyun OSS |
|
||||
|
||||
<!--- end_remove -->
|
||||
|
||||
### Use file system
|
||||
|
||||
If you want to store your images on the file system, you can change the storage
|
||||
|
|
|
|||
|
|
@ -284,32 +284,7 @@ Example response:
|
|||
}
|
||||
```
|
||||
|
||||
When the [unified approval setting](../ci/environments/deployment_approvals.md#unified-approval-setting-deprecated) is configured, deployments created by users on GitLab Premium or Ultimate include the `approvals` and `pending_approval_count` properties:
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "created",
|
||||
"pending_approval_count": 0,
|
||||
"approvals": [
|
||||
{
|
||||
"user": {
|
||||
"id": 49,
|
||||
"username": "project_6_bot",
|
||||
"name": "****",
|
||||
"state": "active",
|
||||
"avatar_url": "https://www.gravatar.com/avatar/e83ac685f68ea07553ad3054c738c709?s=80&d=identicon",
|
||||
"web_url": "http://localhost:3000/project_6_bot"
|
||||
},
|
||||
"status": "approved",
|
||||
"created_at": "2022-02-24T20:22:30.097Z",
|
||||
"comment": "Looks good to me"
|
||||
}
|
||||
],
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
When the [multiple approval rules](../ci/environments/deployment_approvals.md#add-multiple-approval-rules) is configured, deployments created by users on GitLab Premium or Ultimate include the `approval_summary` property:
|
||||
When [multiple approval rules](../ci/environments/deployment_approvals.md#add-multiple-approval-rules) are configured, deployments created by users on GitLab Premium or Ultimate include the `approval_summary` property:
|
||||
|
||||
```json
|
||||
{
|
||||
|
|
|
|||
|
|
@ -7248,6 +7248,29 @@ Input type: `ProductAnalyticsProjectSettingsUpdateInput`
|
|||
| <a id="mutationproductanalyticsprojectsettingsupdateproductanalyticsconfiguratorconnectionstring"></a>`productAnalyticsConfiguratorConnectionString` | [`String`](#string) | Connection string for the product analytics configurator. |
|
||||
| <a id="mutationproductanalyticsprojectsettingsupdateproductanalyticsdatacollectorhost"></a>`productAnalyticsDataCollectorHost` | [`String`](#string) | Host for the product analytics data collector. |
|
||||
|
||||
### `Mutation.projectBlobsRemove`
|
||||
|
||||
DETAILS:
|
||||
**Introduced** in GitLab 17.1.
|
||||
**Status**: Experiment.
|
||||
|
||||
Input type: `projectBlobsRemoveInput`
|
||||
|
||||
#### Arguments
|
||||
|
||||
| Name | Type | Description |
|
||||
| ---- | ---- | ----------- |
|
||||
| <a id="mutationprojectblobsremovebloboids"></a>`blobOids` | [`[String!]!`](#string) | List of blob oids. |
|
||||
| <a id="mutationprojectblobsremoveclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
|
||||
| <a id="mutationprojectblobsremoveprojectpath"></a>`projectPath` | [`ID!`](#id) | Full path of the project to replace. |
|
||||
|
||||
#### Fields
|
||||
|
||||
| Name | Type | Description |
|
||||
| ---- | ---- | ----------- |
|
||||
| <a id="mutationprojectblobsremoveclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
|
||||
| <a id="mutationprojectblobsremoveerrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. |
|
||||
|
||||
### `Mutation.projectCiCdSettingsUpdate`
|
||||
|
||||
Input type: `ProjectCiCdSettingsUpdateInput`
|
||||
|
|
|
|||
|
|
@ -65,8 +65,6 @@ Example response:
|
|||
- key_path: redis_hll_counters.search.i_search_paid_monthly
|
||||
description: Calculated unique users to perform a search with a paid license enabled
|
||||
by month
|
||||
product_section: enablement
|
||||
product_stage: enablement
|
||||
product_group: global_search
|
||||
value_type: number
|
||||
status: active
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 112 KiB After Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 115 KiB After Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 70 KiB After Width: | Height: | Size: 16 KiB |
|
|
@ -66,71 +66,6 @@ Make sure the number of required approvals is less than the number of users allo
|
|||
|
||||
After a deployment job is approved, you must [run the job manually](../jobs/job_control.md#run-a-manual-job).
|
||||
|
||||
<!--- start_remove The following content will be removed on remove_date: '2024-05-22' -->
|
||||
|
||||
### Unified approval setting (deprecated)
|
||||
|
||||
> - UI configuration [removed](https://gitlab.com/gitlab-org/gitlab/-/issues/378447) in GitLab
|
||||
> 15.11.
|
||||
|
||||
WARNING:
|
||||
This feature was [deprecated](https://gitlab.com/groups/gitlab-org/-/epics/9662) in GitLab 16.1 and is planned for removal
|
||||
in 17.0. Use [multiple approval rules](https://gitlab.com/gitlab-org/gitlab/-/issues/404579) instead. This change
|
||||
is a breaking change.
|
||||
|
||||
To configure approvals for a protected environment:
|
||||
|
||||
- Using the [REST API](../../api/protected_environments.md#protect-a-single-environment),
|
||||
set the `required_approval_count` field to 1 or more.
|
||||
|
||||
After this setting is configured, all jobs deploying to this environment automatically go into a blocked state and wait for approvals before running. Ensure that the number of required approvals is less than the number of users allowed to deploy.
|
||||
|
||||
Example:
|
||||
|
||||
```shell
|
||||
curl --header 'Content-Type: application/json' --request POST \
|
||||
--data '{"name": "production", "deploy_access_levels": [{"group_id": 9899826}], "required_approval_count": 1}' \
|
||||
--header "PRIVATE-TOKEN: <your_access_token>" \
|
||||
"https://gitlab.example.com/api/v4/projects/22034114/protected_environments"
|
||||
```
|
||||
|
||||
### Migrate to multiple approval rules
|
||||
|
||||
You can migrate a protected environment from unified approval rules to multiple
|
||||
approval rules. Unified approval rules allow all entities that can deploy to an
|
||||
environment to approve deployment jobs. To migrate to multiple approval rules,
|
||||
create a new approval rule for each entity allowed to deploy to the environment.
|
||||
|
||||
To migrate to multiple approval rules:
|
||||
|
||||
1. On the left sidebar, select **Search or go to** and find your project.
|
||||
1. Select **Settings > CI/CD**.
|
||||
1. Expand **Protected environments**.
|
||||
1. From the **Environment** list, select your environment.
|
||||
1. For each entity allowed to deploy to the environment:
|
||||
1. Select **Add approval rules**.
|
||||
1. On the dialog, select which entity is allowed to approve the
|
||||
deployment job.
|
||||
1. Enter the number of required approvals.
|
||||
1. Select **Save**.
|
||||
|
||||
Each deployment requires the specified number of approvals from each entity.
|
||||
|
||||
For example, the `Production` environment below requires five total approvals,
|
||||
and allows deployments from only the group `Very Important Group` and the user
|
||||
`Administrator`:
|
||||
|
||||

|
||||
|
||||
To migrate, create rules for the `Very Important Group` and `Administrator`. To
|
||||
preserve the number of required approvals, set the number of required approvals
|
||||
for `Very Important Group` to four and `Administrator` to one. The new rules
|
||||
require `Administrator` to approve every deployment job in `Production`.
|
||||
|
||||

|
||||
|
||||
<!--- end_remove -->
|
||||
|
||||
### Allow self-approval
|
||||
|
||||
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/381418) in GitLab 15.8.
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 107 KiB |
|
Before Width: | Height: | Size: 121 KiB |
|
|
@ -29,8 +29,6 @@ Each event is defined in a separate YAML file consisting of the following fields
|
|||
| `category` | no | Required for legacy events. Should not be used for Internal Events. |
|
||||
| `action` | yes | A unique name for the event. Only lowercase, numbers, and underscores are allowed. Use the format `<operation>_<target_of_operation>_<where/when>`. <br/><br/> Ex: `publish_go_module_to_the_registry_from_pipeline` <br/>`<operation> = publish`<br/>`<target> = go_module`<br/>`<when/where> = to_the_registry_from_pipeline`. |
|
||||
| `identifiers` | no | A list of identifiers sent with the event. Can be set to one or more of `project`, `user`, or `namespace`. |
|
||||
| `product_section` | yes | The [section](https://gitlab.com/gitlab-com/www-gitlab-com/-/blob/master/data/sections.yml). |
|
||||
| `product_stage` | no | The [stage](https://gitlab.com/gitlab-com/www-gitlab-com/blob/master/data/stages.yml) for the event. |
|
||||
| `product_group` | yes | The [group](https://gitlab.com/gitlab-com/www-gitlab-com/blob/master/data/stages.yml) that owns the event. |
|
||||
| `milestone` | no | The milestone when the event is introduced. |
|
||||
| `introduced_by_url` | no | The URL to the merge request that introduced the event. |
|
||||
|
|
@ -49,8 +47,6 @@ identifiers:
|
|||
- project
|
||||
- user
|
||||
- namespace
|
||||
product_section: dev
|
||||
product_stage: monitor
|
||||
product_group: group::product analytics
|
||||
milestone: "16.4"
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/128029
|
||||
|
|
|
|||
|
|
@ -33,8 +33,6 @@ Each metric is defined in a separate YAML file consisting of a number of fields:
|
|||
|------------------------------|----------|------------------------|
|
||||
| `key_path` | yes | JSON key path for the metric, location in Service Ping payload. |
|
||||
| `description` | yes | |
|
||||
| `product_section` | yes | The [section](https://gitlab.com/gitlab-com/www-gitlab-com/-/blob/master/data/sections.yml). |
|
||||
| `product_stage` | yes | The [stage](https://gitlab.com/gitlab-com/www-gitlab-com/blob/master/data/stages.yml) for the metric. |
|
||||
| `product_group` | yes | The [group](https://gitlab.com/gitlab-com/www-gitlab-com/blob/master/data/stages.yml) that owns the metric. |
|
||||
| `value_type` | yes | `string`; one of [`string`, `number`, `boolean`, `object`](https://json-schema.org/understanding-json-schema/reference/type). |
|
||||
| `status` | yes | `string`; [status](#metric-statuses) of the metric, may be set to `active`, `removed`, `broken`. |
|
||||
|
|
@ -117,8 +115,6 @@ instance unique identifier.
|
|||
```yaml
|
||||
key_path: uuid
|
||||
description: GitLab instance unique identifier
|
||||
product_section: analytics
|
||||
product_stage: analytics
|
||||
product_group: analytics_instrumentation
|
||||
value_type: string
|
||||
status: active
|
||||
|
|
|
|||
|
|
@ -53,7 +53,7 @@ In most cases, an Analytics Instrumentation review is automatically added, but i
|
|||
- For a metric's YAML definition:
|
||||
- Check the metric's `description`.
|
||||
- Check the metric's `key_path`.
|
||||
- Check the `product_section`, `product_stage`, and `product_group` fields.
|
||||
- Check the `product_group` field.
|
||||
They should correspond to the [stages file](https://gitlab.com/gitlab-com/www-gitlab-com/blob/master/data/stages.yml).
|
||||
- Check the file location. Consider the time frame, and if the file should be under `ee`.
|
||||
- Check the tiers.
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 60 KiB After Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 103 KiB After Width: | Height: | Size: 37 KiB |
|
Before Width: | Height: | Size: 106 KiB After Width: | Height: | Size: 36 KiB |
|
Before Width: | Height: | Size: 72 KiB After Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 93 KiB After Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 77 KiB After Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 83 KiB After Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 37 KiB After Width: | Height: | Size: 17 KiB |
|
|
@ -162,7 +162,7 @@ Prerequisites:
|
|||
|
||||
Prerequisites:
|
||||
|
||||
- You must have an active paid Premium or Ultimate subscription.
|
||||
- You must have an active paid Premium or Ultimate subscription. GitLab Duo Pro trials are not available to free users of self-managed at this time.
|
||||
- You must have GitLab 16.8 or later and your instance must be able to [synchronize your subscription data](self_managed/index.md#subscription-data-synchronization) with GitLab.
|
||||
|
||||
1. Go to the [GitLab Duo Pro trial page](https://about.gitlab.com/solutions/gitlab-duo-pro/self-managed-and-gitlab-dedicated-trial/).
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 241 KiB After Width: | Height: | Size: 83 KiB |
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 13 KiB |
|
|
@ -1,169 +1,11 @@
|
|||
---
|
||||
stage: Manage
|
||||
group: Import and Integrate
|
||||
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://handbook.gitlab.com/handbook/product/ux/technical-writing/#assignments
|
||||
redirect_to: 'gitlab_slack_application.md'
|
||||
remove_date: '2024-08-23'
|
||||
---
|
||||
<!--- start_remove The following content will be removed on remove_date: '2024-05-22' -->
|
||||
|
||||
# Slack notifications (deprecated)
|
||||
This document was moved to [another location](gitlab_slack_application.md).
|
||||
|
||||
DETAILS:
|
||||
**Tier:** Free, Premium, Ultimate
|
||||
**Offering:** GitLab.com, Self-managed, GitLab Dedicated
|
||||
|
||||
WARNING:
|
||||
This feature was [deprecated](https://gitlab.com/gitlab-org/gitlab/-/issues/372411) in GitLab 15.9
|
||||
and is planned for removal in 18.0. Use the [GitLab for Slack app](gitlab_slack_application.md) instead.
|
||||
This change is a breaking change.
|
||||
|
||||
The Slack notifications integration enables your GitLab project to send events
|
||||
(such as issue creation) to your existing Slack team as notifications. Setting up
|
||||
Slack notifications requires configuration changes for both Slack and GitLab.
|
||||
|
||||
You can also use [Slack slash commands](slack_slash_commands.md)
|
||||
to control GitLab from Slack. Slash commands are configured separately.
|
||||
|
||||
## Configure Slack
|
||||
|
||||
1. Sign in to your Slack team and [start a new Incoming WebHooks configuration](https://my.slack.com/services/new/incoming-webhook).
|
||||
1. Identify the Slack channel where notifications should be sent to by default.
|
||||
Select **Add Incoming WebHooks integration** to add the configuration.
|
||||
1. Copy the **Webhook URL** to use later when you configure GitLab.
|
||||
|
||||
## Configure GitLab
|
||||
|
||||
> - [Changed](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/106760) in GitLab 15.9 to limit Slack channels to 10 per event.
|
||||
|
||||
1. On the left sidebar, select **Search or go to** and find your project.
|
||||
1. Select **Settings > Integrations**.
|
||||
1. Select **Slack notifications**.
|
||||
1. Under **Enable integration**, select the **Active** checkbox.
|
||||
1. In the **Trigger** section, select the checkboxes for each type of GitLab
|
||||
event to send to Slack as a notification. For a full list, see
|
||||
[Triggers for Slack notifications](#triggers-for-slack-notifications).
|
||||
By default, messages are sent to the channel you configured during
|
||||
[Slack configuration](#configure-slack).
|
||||
1. Optional. To send messages to a different channel, multiple channels, or as
|
||||
a direct message:
|
||||
- *To send messages to channels,* enter the Slack channel names, separated by
|
||||
commas.
|
||||
- *To send direct messages,* use the Member ID found in the user's Slack profile.
|
||||
1. In **Webhook**, enter the webhook URL you copied in the
|
||||
[Slack configuration](#configure-slack) step.
|
||||
1. Optional. In **Username**, enter the username of the Slack bot that sends
|
||||
the notifications.
|
||||
1. Select the **Notify only broken pipelines** checkbox to notify only on failures.
|
||||
1. In the **Branches for which notifications are to be sent** dropdown list, select which types of branches
|
||||
to send notifications for.
|
||||
1. Leave the **Labels to be notified** field blank to get all notifications, or
|
||||
add labels that the issue or merge request must have to trigger a
|
||||
notification.
|
||||
1. Optional. Select **Test settings**.
|
||||
1. Select **Save changes**.
|
||||
|
||||
Your Slack team now starts receiving GitLab event notifications as configured.
|
||||
|
||||
## Triggers for Slack notifications
|
||||
|
||||
The following triggers are available for Slack notifications:
|
||||
|
||||
| Trigger name | Trigger event |
|
||||
|--------------------------------------------------------------------------|------------------------------------------------------|
|
||||
| **Push** | A push to the repository. |
|
||||
| **Issue** | An issue is created, closed, or reopened. |
|
||||
| **Incident** | An incident is created, closed, or reopened. |
|
||||
| **Confidential issue** | A confidential issue is created, closed, or reopened.|
|
||||
| **Merge request** | A merge request is created, merged, closed, or reopened.|
|
||||
| **Note** | A comment is added. |
|
||||
| **Confidential note** | An internal note or comment on a confidential issue is added.|
|
||||
| **Tag push** | A new tag is pushed to the repository or removed. |
|
||||
| **Pipeline** | A pipeline status changed. |
|
||||
| **Wiki page** | A wiki page is created or updated. |
|
||||
| **Deployment** | A deployment starts or finishes. |
|
||||
| **Alert** | A new, unique alert is recorded. |
|
||||
| **[Group mention](#trigger-notifications-for-group-mentions) in public** | A group is mentioned in a public context. |
|
||||
| **[Group mention](#trigger-notifications-for-group-mentions) in private** | A group is mentioned in a confidential context. |
|
||||
| [**Vulnerability**](../../application_security/vulnerabilities/index.md) | A new, unique vulnerability is recorded. |
|
||||
|
||||
## Trigger notifications for group mentions
|
||||
|
||||
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/417751) in GitLab 16.4.
|
||||
|
||||
To trigger a [notification event](#triggers-for-slack-notifications) for a group mention, use `@<group_name>` in:
|
||||
|
||||
- Issue and merge request descriptions
|
||||
- Comments on issues, merge requests, and commits
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
If your Slack integration is not working, start troubleshooting by
|
||||
searching through the [Sidekiq logs](../../../administration/logs/index.md#sidekiqlog)
|
||||
for errors relating to your Slack service.
|
||||
|
||||
### Something went wrong on our end
|
||||
|
||||
You might get this generic error message in the GitLab UI.
|
||||
Review [the logs](../../../administration/logs/index.md#productionlog) to find
|
||||
the error message and keep troubleshooting from there.
|
||||
|
||||
### `certificate verify failed`
|
||||
|
||||
You might see an entry like the following in your Sidekiq log:
|
||||
|
||||
```plaintext
|
||||
2019-01-10_13:22:08.42572 2019-01-10T13:22:08.425Z 6877 TID-abcdefg Integrations::ExecuteWorker JID-3bade5fb3dd47a85db6d78c5 ERROR: {:class=>"Integrations::ExecuteWorker :integration_class=>"SlackService", :message=>"SSL_connect returned=1 errno=0 state=error: certificate verify failed"}
|
||||
```
|
||||
|
||||
This issue occurs when there is a problem with GitLab communicating with Slack,
|
||||
or GitLab communicating with itself.
|
||||
The former is less likely, as Slack security certificates should always be trusted.
|
||||
|
||||
To view which of these problems is the cause of the issue:
|
||||
|
||||
1. Start a Rails console:
|
||||
|
||||
```shell
|
||||
sudo gitlab-rails console -e production
|
||||
|
||||
# for source installs:
|
||||
bundle exec rails console -e production
|
||||
```
|
||||
|
||||
1. Run the following commands:
|
||||
|
||||
```ruby
|
||||
# replace <SLACK URL> with your actual Slack URL
|
||||
result = Net::HTTP.get(URI('https://<SLACK URL>'));0
|
||||
|
||||
# replace <GITLAB URL> with your actual GitLab URL
|
||||
result = Net::HTTP.get(URI('https://<GITLAB URL>'));0
|
||||
```
|
||||
|
||||
If GitLab does not trust HTTPS connections to itself,
|
||||
[add your certificate to the GitLab trusted certificates](https://docs.gitlab.com/omnibus/settings/ssl/index.html#install-custom-public-certificates).
|
||||
|
||||
If GitLab does not trust connections to Slack,
|
||||
the GitLab OpenSSL trust store is incorrect. Typical causes are:
|
||||
|
||||
- Overriding the trust store with `gitlab_rails['env'] = {"SSL_CERT_FILE" => "/path/to/file.pem"}`.
|
||||
- Accidentally modifying the default CA bundle `/opt/gitlab/embedded/ssl/certs/cacert.pem`.
|
||||
|
||||
### Bulk update to disable the Slack Notification integration
|
||||
|
||||
To disable notifications for all projects that have Slack integration enabled,
|
||||
[start a rails console session](../../../administration/operations/rails_console.md#starting-a-rails-console-session) and use a script similar to the following:
|
||||
|
||||
WARNING:
|
||||
Commands that change data can cause damage if not run correctly or under the right conditions. Always run commands in a test environment first and have a backup instance ready to restore.
|
||||
|
||||
```ruby
|
||||
# Grab all projects that have the Slack notifications enabled
|
||||
p = Project.find_by_sql("SELECT p.id FROM projects p LEFT JOIN integrations s ON p.id = s.project_id WHERE s.type_new = 'Integrations::Slack' AND s.active = true")
|
||||
|
||||
# Disable the integration on each of the projects that were found.
|
||||
p.each do |project|
|
||||
project.slack_integration.update!(:active, false)
|
||||
end
|
||||
```
|
||||
|
||||
<!--- end_remove -->
|
||||
<!-- This redirect file can be deleted after <2024-08-23>. -->
|
||||
<!-- Redirects that point to other docs in the same project expire in three months. -->
|
||||
<!-- Redirects that point to docs in a different project or site (for example, link is not relative and starts with `https:`) expire in one year. -->
|
||||
<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/redirects.html -->
|
||||
|
|
|
|||
|
|
@ -93,7 +93,7 @@ Prerequisites:
|
|||
|
||||
To delete the currently active issue board:
|
||||
|
||||
1. In the upper-left corner of the issue board page, select the dropdown list with the current board name.
|
||||
1. In the upper-right corner of the issue board page, select **Configure board** (**{settings}**).
|
||||
1. Select **Delete board**.
|
||||
1. Select **Delete** to confirm.
|
||||
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 5.9 KiB |
|
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 8.8 KiB |
|
|
@ -24649,6 +24649,9 @@ msgstr ""
|
|||
msgid "Group '%{group_name}' could not be updated."
|
||||
msgstr ""
|
||||
|
||||
msgid "Group '%{group_name}' has been successfully restored."
|
||||
msgstr ""
|
||||
|
||||
msgid "Group '%{group_name}' was successfully updated."
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -24691,6 +24694,9 @@ msgstr ""
|
|||
msgid "Group by:"
|
||||
msgstr ""
|
||||
|
||||
msgid "Group comment templates"
|
||||
msgstr ""
|
||||
|
||||
msgid "Group description (optional)"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -28479,9 +28485,6 @@ msgstr ""
|
|||
msgid "IssueBoards|Create new board"
|
||||
msgstr ""
|
||||
|
||||
msgid "IssueBoards|Delete board"
|
||||
msgstr ""
|
||||
|
||||
msgid "IssueBoards|No matching boards found"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -31084,6 +31087,9 @@ msgstr ""
|
|||
msgid "Makes this %{type} confidential."
|
||||
msgstr ""
|
||||
|
||||
msgid "Manage"
|
||||
msgstr ""
|
||||
|
||||
msgid "Manage %{workspace} labels"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -31102,10 +31108,10 @@ msgstr ""
|
|||
msgid "Manage branch rules"
|
||||
msgstr ""
|
||||
|
||||
msgid "Manage git repositories with fine-grained access controls that keep your code secure."
|
||||
msgid "Manage comment templates"
|
||||
msgstr ""
|
||||
|
||||
msgid "Manage group comment templates"
|
||||
msgid "Manage git repositories with fine-grained access controls that keep your code secure."
|
||||
msgstr ""
|
||||
|
||||
msgid "Manage group labels"
|
||||
|
|
@ -31120,9 +31126,6 @@ msgstr ""
|
|||
msgid "Manage milestones"
|
||||
msgstr ""
|
||||
|
||||
msgid "Manage project comment templates"
|
||||
msgstr ""
|
||||
|
||||
msgid "Manage project labels"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -31141,9 +31144,6 @@ msgstr ""
|
|||
msgid "Manage usage"
|
||||
msgstr ""
|
||||
|
||||
msgid "Manage your comment templates"
|
||||
msgstr ""
|
||||
|
||||
msgid "Manage your subscription"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -34192,6 +34192,9 @@ msgstr ""
|
|||
msgid "No child epics match applied filters"
|
||||
msgstr ""
|
||||
|
||||
msgid "No comment templates found."
|
||||
msgstr ""
|
||||
|
||||
msgid "No commenters"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -40168,15 +40171,15 @@ msgstr ""
|
|||
msgid "Project & Group can not be assigned at the same time"
|
||||
msgstr ""
|
||||
|
||||
msgid "Project '%{project_name}' has been successfully restored."
|
||||
msgstr ""
|
||||
|
||||
msgid "Project '%{project_name}' is being imported."
|
||||
msgstr ""
|
||||
|
||||
msgid "Project '%{project_name}' is in the process of being deleted."
|
||||
msgstr ""
|
||||
|
||||
msgid "Project '%{project_name}' is restored."
|
||||
msgstr ""
|
||||
|
||||
msgid "Project '%{project_name}' queued for deletion."
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -40228,6 +40231,9 @@ msgstr ""
|
|||
msgid "Project cannot be shared with the group it is in or one of its ancestors."
|
||||
msgstr ""
|
||||
|
||||
msgid "Project comment templates"
|
||||
msgstr ""
|
||||
|
||||
msgid "Project configuration, excluding integrations"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -46085,6 +46091,9 @@ msgstr ""
|
|||
msgid "Search by name"
|
||||
msgstr ""
|
||||
|
||||
msgid "Search comment templates"
|
||||
msgstr ""
|
||||
|
||||
msgid "Search files"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -48030,6 +48039,9 @@ msgstr ""
|
|||
msgid "Select a color from the color picker or from the presets below."
|
||||
msgstr ""
|
||||
|
||||
msgid "Select a comment template"
|
||||
msgstr ""
|
||||
|
||||
msgid "Select a file from the left sidebar to begin editing. Afterwards, you'll be able to commit your changes."
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -60496,6 +60508,9 @@ msgstr ""
|
|||
msgid "Your comment could not be submitted! Please check your network connection and try again."
|
||||
msgstr ""
|
||||
|
||||
msgid "Your comment templates"
|
||||
msgstr ""
|
||||
|
||||
msgid "Your comment will be discarded."
|
||||
msgstr ""
|
||||
|
||||
|
|
|
|||
|
|
@ -268,6 +268,26 @@ RSpec.describe Projects::EnvironmentsController, feature_category: :continuous_d
|
|||
end
|
||||
end
|
||||
|
||||
describe 'GET k8s' do
|
||||
context 'with valid id' do
|
||||
it 'responds with a status code 200' do
|
||||
get :k8s, params: environment_params
|
||||
|
||||
expect(response).to be_ok
|
||||
end
|
||||
end
|
||||
|
||||
context 'with invalid id' do
|
||||
it 'responds with a status code 404' do
|
||||
params = environment_params
|
||||
params[:id] = non_existing_record_id
|
||||
get :k8s, params: params
|
||||
|
||||
expect(response).to have_gitlab_http_status(:not_found)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'GET show' do
|
||||
context 'with valid id' do
|
||||
it 'responds with a status code 200' do
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ require 'spec_helper'
|
|||
# - https://gitlab.com/gitlab-org/gitlab/-/merge_requests/105849
|
||||
# - https://gitlab.com/gitlab-org/gitlab/-/issues/383970
|
||||
#
|
||||
RSpec.describe 'Project issue boards', :js, feature_category: :team_planning do
|
||||
RSpec.describe 'Project issue boards', :js, feature_category: :portfolio_management do
|
||||
include DragTo
|
||||
include MobileHelpers
|
||||
include BoardHelpers
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe 'Issue Boards focus mode', :js, feature_category: :team_planning do
|
||||
RSpec.describe 'Issue Boards focus mode', :js, feature_category: :portfolio_management do
|
||||
let(:project) { create(:project, :public) }
|
||||
|
||||
before do
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe 'Issue Boards', :js, feature_category: :team_planning do
|
||||
RSpec.describe 'Issue Boards', :js, feature_category: :portfolio_management do
|
||||
include DragTo
|
||||
|
||||
let(:project) { create(:project, :public) }
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe 'Issue Boards shortcut', :js, feature_category: :team_planning do
|
||||
RSpec.describe 'Issue Boards shortcut', :js, feature_category: :portfolio_management do
|
||||
context 'issues are enabled' do
|
||||
let(:project) { create(:project) }
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe 'Multiple Issue Boards', :js, feature_category: :team_planning do
|
||||
RSpec.describe 'Multiple Issue Boards', :js, feature_category: :portfolio_management do
|
||||
let_it_be(:user) { create(:user) }
|
||||
let_it_be(:project) { create(:project, :public) }
|
||||
let_it_be(:planning) { create(:label, project: project, name: 'Planning') }
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe 'Issue Boards new issue', :js, feature_category: :team_planning do
|
||||
RSpec.describe 'Issue Boards new issue', :js, feature_category: :portfolio_management do
|
||||
let_it_be(:project) { create(:project, :public) }
|
||||
let_it_be(:board) { create(:board, project: project) }
|
||||
let_it_be(:label) { create(:label, project: project, name: 'Label 1') }
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe 'Ensure Boards do not show stale data on browser back', :js, feature_category: :team_planning do
|
||||
RSpec.describe 'Ensure Boards do not show stale data on browser back', :js, feature_category: :portfolio_management do
|
||||
let(:project) { create(:project, :public) }
|
||||
let(:board) { create(:board, project: project) }
|
||||
let(:user) { create(:user) }
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ require 'spec_helper'
|
|||
|
||||
RSpec.describe 'Project issue boards sidebar assignee', :js,
|
||||
quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/332078',
|
||||
feature_category: :team_planning do
|
||||
feature_category: :portfolio_management do
|
||||
include BoardHelpers
|
||||
|
||||
let_it_be(:user) { create(:user) }
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe 'Issue boards sidebar labels select', :js, feature_category: :team_planning do
|
||||
RSpec.describe 'Issue boards sidebar labels select', :js, feature_category: :portfolio_management do
|
||||
include BoardHelpers
|
||||
|
||||
include_context 'labels from nested groups and projects'
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe 'Project issue boards sidebar labels', :js, feature_category: :team_planning do
|
||||
RSpec.describe 'Project issue boards sidebar labels', :js, feature_category: :portfolio_management do
|
||||
include BoardHelpers
|
||||
|
||||
let_it_be(:group) { create(:group, :public) }
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe 'Project issue boards sidebar', :js, feature_category: :team_planning do
|
||||
RSpec.describe 'Project issue boards sidebar', :js, feature_category: :portfolio_management do
|
||||
include BoardHelpers
|
||||
|
||||
let_it_be(:user) { create(:user) }
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe 'User visits issue boards', :js, feature_category: :team_planning do
|
||||
RSpec.describe 'User visits issue boards', :js, feature_category: :portfolio_management do
|
||||
using RSpec::Parameterized::TableSyntax
|
||||
|
||||
let_it_be(:group) { create_default(:group, :public) }
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe 'Group Issue Boards', :js, feature_category: :team_planning do
|
||||
RSpec.describe 'Group Issue Boards', :js, feature_category: :portfolio_management do
|
||||
include BoardHelpers
|
||||
|
||||
let(:group) { create(:group) }
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe 'Group Boards', feature_category: :team_planning do
|
||||
RSpec.describe 'Group Boards', feature_category: :portfolio_management do
|
||||
include DragTo
|
||||
include MobileHelpers
|
||||
include BoardHelpers
|
||||
|
|
|
|||
|
|
@ -49,6 +49,7 @@ describe('BoardForm', () => {
|
|||
const findDeleteConfirmation = () => wrapper.findByTestId('delete-confirmation-message');
|
||||
const findInput = () => wrapper.find('#board-new-name');
|
||||
const findInputFormWrapper = () => wrapper.findComponent(GlForm);
|
||||
const findDeleteButton = () => wrapper.findByTestId('delete-board-button');
|
||||
|
||||
const defaultHandlers = {
|
||||
createBoardMutationHandler: jest.fn().mockResolvedValue({
|
||||
|
|
@ -84,7 +85,12 @@ describe('BoardForm', () => {
|
|||
]);
|
||||
};
|
||||
|
||||
const createComponent = ({ props, provide, handlers = defaultHandlers } = {}) => {
|
||||
const createComponent = ({
|
||||
props,
|
||||
provide,
|
||||
handlers = defaultHandlers,
|
||||
stubs = { GlForm },
|
||||
} = {}) => {
|
||||
wrapper = shallowMountExtended(BoardForm, {
|
||||
apolloProvider: createMockApolloProvider(handlers),
|
||||
propsData: { ...defaultProps, ...props },
|
||||
|
|
@ -95,9 +101,7 @@ describe('BoardForm', () => {
|
|||
...provide,
|
||||
},
|
||||
attachTo: document.body,
|
||||
stubs: {
|
||||
GlForm,
|
||||
},
|
||||
stubs,
|
||||
});
|
||||
};
|
||||
|
||||
|
|
@ -261,6 +265,19 @@ describe('BoardForm', () => {
|
|||
it('renders form wrapper', () => {
|
||||
expect(findFormWrapper().exists()).toBe(true);
|
||||
});
|
||||
it('emits showBoardModal with delete when clicking on delete board button', () => {
|
||||
createComponent({
|
||||
props: {
|
||||
currentPage: formType.edit,
|
||||
showDelete: true,
|
||||
canAdminBoard: true,
|
||||
},
|
||||
stubs: { GlModal },
|
||||
});
|
||||
|
||||
findDeleteButton().vm.$emit('click');
|
||||
expect(wrapper.emitted('showBoardModal')).toEqual([[formType.delete]]);
|
||||
});
|
||||
});
|
||||
|
||||
it('calls GraphQL mutation with correct parameters when issues are not grouped', async () => {
|
||||
|
|
|
|||
|
|
@ -342,13 +342,15 @@ describe('BoardsSelector', () => {
|
|||
expect(wrapper.emitted('showBoardModal')).toEqual([[formType.new]]);
|
||||
});
|
||||
|
||||
it('emits showBoardModal with delete when clicking on delete board button', async () => {
|
||||
createComponent({ isProjectBoard: true });
|
||||
it('emits showBoardModal when BoardForm emits showBoardModal', () => {
|
||||
createComponent({
|
||||
isProjectBoard: true,
|
||||
props: {
|
||||
boardModalForm: formType.edit,
|
||||
},
|
||||
});
|
||||
|
||||
findDropdown().vm.$emit('shown');
|
||||
await waitForPromises();
|
||||
|
||||
wrapper.findAllComponents(GlButton).at(1).vm.$emit('click');
|
||||
findBoardForm().vm.$emit('showBoardModal', formType.delete);
|
||||
expect(wrapper.emitted('showBoardModal')).toEqual([[formType.delete]]);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { GlTabs, GlTab } from '@gitlab/ui';
|
|||
import { mockTracking } from 'helpers/tracking_helper';
|
||||
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
|
||||
import FormattingToolbar from '~/content_editor/components/formatting_toolbar.vue';
|
||||
import CommentTemplatesDropdown from '~/vue_shared/components/markdown/comment_templates_dropdown.vue';
|
||||
import CommentTemplatesModal from '~/vue_shared/components/markdown/comment_templates_modal.vue';
|
||||
import {
|
||||
TOOLBAR_CONTROL_TRACKING_ACTION,
|
||||
CONTENT_EDITOR_TRACKING_LABEL,
|
||||
|
|
@ -117,9 +117,7 @@ describe('content_editor/components/formatting_toolbar', () => {
|
|||
});
|
||||
|
||||
const commands = mockChainedCommands(tiptapEditor, ['focus', 'pasteContent', 'run']);
|
||||
await wrapper
|
||||
.findComponent(CommentTemplatesDropdown)
|
||||
.vm.$emit('select', 'Some saved comment');
|
||||
await wrapper.findComponent(CommentTemplatesModal).vm.$emit('select', 'Some saved comment');
|
||||
|
||||
expect(commands.focus).toHaveBeenCalled();
|
||||
expect(commands.pasteContent).toHaveBeenCalledWith('Some saved comment');
|
||||
|
|
@ -129,7 +127,7 @@ describe('content_editor/components/formatting_toolbar', () => {
|
|||
it('does not show the saved replies icon if newCommentTemplatePath is not provided', () => {
|
||||
buildWrapper();
|
||||
|
||||
expect(wrapper.findComponent(CommentTemplatesDropdown).exists()).toBe(false);
|
||||
expect(wrapper.findComponent(CommentTemplatesModal).exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,73 @@
|
|||
import { mount, RouterLinkStub } from '@vue/test-utils';
|
||||
|
||||
import EnvironmentBreadcrumb from '~/environments/environment_details/environment_breadcrumbs.vue';
|
||||
|
||||
describe('Environment Breadcrumb', () => {
|
||||
let wrapper;
|
||||
const environmentName = 'production';
|
||||
|
||||
const routes = [
|
||||
{ name: 'environment_details', path: '/', meta: { environmentName } },
|
||||
{
|
||||
name: 'logs',
|
||||
path: '/k8s/namespace/namespace/pods/podName/logs',
|
||||
meta: { environmentName },
|
||||
params: {
|
||||
namespace: 'namespace',
|
||||
podName: 'podName',
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const mountComponent = ($route) => {
|
||||
wrapper = mount(EnvironmentBreadcrumb, {
|
||||
mocks: {
|
||||
$route,
|
||||
$router: {
|
||||
options: {
|
||||
routes,
|
||||
},
|
||||
},
|
||||
},
|
||||
stubs: {
|
||||
RouterLink: RouterLinkStub,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
describe('when is rootRoute', () => {
|
||||
beforeEach(() => {
|
||||
mountComponent(routes[0]);
|
||||
});
|
||||
|
||||
it('contains only a single router-link to list', () => {
|
||||
const links = wrapper.findAll('a');
|
||||
|
||||
expect(links).toHaveLength(1);
|
||||
expect(links.at(0).props('to')).toEqual(routes[0].path);
|
||||
});
|
||||
|
||||
it('the link text is environmentName', () => {
|
||||
expect(wrapper.text()).toContain(environmentName);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when is not rootRoute', () => {
|
||||
beforeEach(() => {
|
||||
mountComponent(routes[1]);
|
||||
});
|
||||
|
||||
it('contains two router-links to list and details', () => {
|
||||
const links = wrapper.findAll('a');
|
||||
|
||||
expect(links).toHaveLength(2);
|
||||
expect(links.at(0).props('to')).toEqual(routes[0].path);
|
||||
expect(links.at(1).props('to')).toBe(routes[1].path);
|
||||
});
|
||||
|
||||
it('the last link text is podName', () => {
|
||||
const lastLink = wrapper.findAll('a').at(1);
|
||||
expect(lastLink.text()).toContain(routes[1].params.podName);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -396,6 +396,8 @@ describe('Tracking', () => {
|
|||
});
|
||||
|
||||
it('ignores and removes old entries from the cache', () => {
|
||||
window.gl.maskedDefaultReferrerUrl =
|
||||
'https://gitlab.com/namespace:#/project:#/-/merge_requests/';
|
||||
const oldTimestamp = Date.now() - (REFERRER_TTL + 1);
|
||||
Object.defineProperty(document, 'referrer', { value: testOriginalUrl });
|
||||
setUrlsCache([
|
||||
|
|
@ -408,9 +410,29 @@ describe('Tracking', () => {
|
|||
|
||||
Tracking.setAnonymousUrls();
|
||||
|
||||
expect(snowplowSpy).not.toHaveBeenCalledWith('setReferrerUrl', testUrl);
|
||||
expect(snowplowSpy).toHaveBeenCalledWith(
|
||||
'setReferrerUrl',
|
||||
window.gl.maskedDefaultReferrerUrl,
|
||||
);
|
||||
expect(localStorage.getItem(URLS_CACHE_STORAGE_KEY)).not.toContain(oldTimestamp.toString());
|
||||
});
|
||||
|
||||
it('sets the referrer URL to maskedDefaultReferrerUrl if no referrer is found in cache', () => {
|
||||
window.gl.maskedDefaultReferrerUrl =
|
||||
'https://gitlab.com/namespace:#/project:#/-/merge_requests/';
|
||||
setUrlsCache([]);
|
||||
|
||||
Object.defineProperty(document, 'referrer', {
|
||||
value: 'https://gitlab.com/my-namespace/my-project/-/merge_requests/',
|
||||
});
|
||||
|
||||
Tracking.setAnonymousUrls();
|
||||
|
||||
expect(snowplowSpy).toHaveBeenCalledWith(
|
||||
'setReferrerUrl',
|
||||
window.gl.maskedDefaultReferrerUrl,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -1,13 +1,18 @@
|
|||
import { GlCollapsibleListbox } from '@gitlab/ui';
|
||||
import {
|
||||
GlModal,
|
||||
GlDisclosureDropdown,
|
||||
GlDisclosureDropdownGroup,
|
||||
GlDisclosureDropdownItem,
|
||||
} from '@gitlab/ui';
|
||||
import Vue from 'vue';
|
||||
import VueApollo from 'vue-apollo';
|
||||
import savedRepliesResponse from 'test_fixtures/graphql/comment_templates/saved_replies.query.graphql.json';
|
||||
import { mockTracking } from 'helpers/tracking_helper';
|
||||
import { mountExtended } from 'helpers/vue_test_utils_helper';
|
||||
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
|
||||
import createMockApollo from 'helpers/mock_apollo_helper';
|
||||
import { useMockLocationHelper } from 'helpers/mock_window_location_helper';
|
||||
import waitForPromises from 'helpers/wait_for_promises';
|
||||
import CommentTemplatesDropdown from '~/vue_shared/components/markdown/comment_templates_dropdown.vue';
|
||||
import CommentTemplatesDropdown from '~/vue_shared/components/markdown/comment_templates_modal.vue';
|
||||
import savedRepliesQuery from 'ee_else_ce/vue_shared/components/markdown/saved_replies.query.graphql';
|
||||
import {
|
||||
TRACKING_SAVED_REPLIES_USE,
|
||||
|
|
@ -19,8 +24,8 @@ let wrapper;
|
|||
let savedRepliesResp;
|
||||
|
||||
const newCommentTemplatePaths = [
|
||||
{ path: '/user/comment_templates', text: 'Manage user' },
|
||||
{ path: '/group/comment_templates', text: 'Manage group' },
|
||||
{ href: '/user/comment_templates', text: 'Manage user' },
|
||||
{ href: '/group/comment_templates', text: 'Manage group' },
|
||||
];
|
||||
|
||||
function createMockApolloProvider(response) {
|
||||
|
|
@ -38,26 +43,31 @@ function createMockApolloProvider(response) {
|
|||
function createComponent(options = {}) {
|
||||
const { mockApollo } = options;
|
||||
|
||||
return mountExtended(CommentTemplatesDropdown, {
|
||||
return shallowMountExtended(CommentTemplatesDropdown, {
|
||||
propsData: {
|
||||
newCommentTemplatePaths,
|
||||
},
|
||||
apolloProvider: mockApollo,
|
||||
stubs: {
|
||||
GlModal,
|
||||
GlDisclosureDropdown,
|
||||
GlDisclosureDropdownGroup,
|
||||
GlDisclosureDropdownItem,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function findDropdownComponent() {
|
||||
return wrapper.findComponent(GlCollapsibleListbox);
|
||||
}
|
||||
const findToggleButton = () => wrapper.findByTestId('comment-templates-dropdown-toggle');
|
||||
const findModalComponent = () => wrapper.findComponent(GlModal);
|
||||
const findActionButton = () =>
|
||||
findModalComponent().findComponent(GlDisclosureDropdownItem).find('button');
|
||||
|
||||
async function selectSavedReply() {
|
||||
const dropdown = findDropdownComponent();
|
||||
|
||||
dropdown.vm.$emit('shown');
|
||||
findToggleButton().vm.$emit('click');
|
||||
|
||||
await waitForPromises();
|
||||
|
||||
dropdown.vm.$emit('select', savedRepliesResponse.data.object.savedReplies.nodes[0].id);
|
||||
await findActionButton().trigger('click');
|
||||
}
|
||||
|
||||
useMockLocationHelper();
|
||||
|
|
@ -67,7 +77,7 @@ describe('Comment templates dropdown', () => {
|
|||
const mockApollo = createMockApolloProvider(savedRepliesResponse);
|
||||
wrapper = createComponent({ mockApollo });
|
||||
|
||||
wrapper.find('.js-comment-template-toggle').trigger('click');
|
||||
findToggleButton().vm.$emit('click');
|
||||
|
||||
await waitForPromises();
|
||||
|
||||
|
|
@ -78,16 +88,13 @@ describe('Comment templates dropdown', () => {
|
|||
const mockApollo = createMockApolloProvider(savedRepliesResponse);
|
||||
wrapper = createComponent({ mockApollo });
|
||||
|
||||
wrapper.find('.js-comment-template-toggle').trigger('click');
|
||||
findToggleButton().vm.$emit('click');
|
||||
|
||||
const links = wrapper.findAllByTestId('manage-button');
|
||||
const manageDropdown = wrapper.findByTestId('manage-dropdown');
|
||||
const links = manageDropdown.props('items');
|
||||
|
||||
expect(links).toHaveLength(newCommentTemplatePaths.length);
|
||||
|
||||
newCommentTemplatePaths.forEach(({ path, text }, index) => {
|
||||
expect(links.at(index).attributes('href')).toBe(path);
|
||||
expect(links.at(index).text()).toBe(text);
|
||||
});
|
||||
expect(links).toBe(newCommentTemplatePaths);
|
||||
});
|
||||
|
||||
describe('when selecting a comment', () => {
|
||||
|
|
@ -101,11 +108,7 @@ describe('Comment templates dropdown', () => {
|
|||
});
|
||||
|
||||
it('emits a select event', async () => {
|
||||
wrapper.find('.js-comment-template-toggle').trigger('click');
|
||||
|
||||
await waitForPromises();
|
||||
|
||||
wrapper.find('.gl-new-dropdown-item').trigger('click');
|
||||
await selectSavedReply();
|
||||
|
||||
expect(wrapper.emitted().select[0]).toEqual(['Saved Reply Content']);
|
||||
});
|
||||
|
|
@ -3,7 +3,7 @@ import { nextTick } from 'vue';
|
|||
import { GlToggle, GlButton } from '@gitlab/ui';
|
||||
import HeaderComponent from '~/vue_shared/components/markdown/header.vue';
|
||||
import HeaderDividerComponent from '~/vue_shared/components/markdown/header_divider.vue';
|
||||
import CommentTemplatesDropdown from '~/vue_shared/components/markdown/comment_templates_dropdown.vue';
|
||||
import CommentTemplatesModal from '~/vue_shared/components/markdown/comment_templates_modal.vue';
|
||||
import ToolbarButton from '~/vue_shared/components/markdown/toolbar_button.vue';
|
||||
import DrawioToolbarButton from '~/vue_shared/components/markdown/drawio_toolbar_button.vue';
|
||||
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
|
||||
|
|
@ -36,7 +36,7 @@ describe('Markdown field header component', () => {
|
|||
.filter((button) => button.props(prop) === value)
|
||||
.at(0);
|
||||
const findDrawioToolbarButton = () => wrapper.findComponent(DrawioToolbarButton);
|
||||
const findCommentTemplatesDropdown = () => wrapper.findComponent(CommentTemplatesDropdown);
|
||||
const findCommentTemplatesModal = () => wrapper.findComponent(CommentTemplatesModal);
|
||||
|
||||
beforeEach(() => {
|
||||
window.gl = {
|
||||
|
|
@ -269,7 +269,7 @@ describe('Markdown field header component', () => {
|
|||
},
|
||||
});
|
||||
|
||||
await findCommentTemplatesDropdown().vm.$emit('select', 'Some saved comment');
|
||||
await findCommentTemplatesModal().vm.$emit('select', 'Some saved comment');
|
||||
|
||||
expect(updateText).toHaveBeenCalledWith({
|
||||
textArea: document.querySelector('textarea'),
|
||||
|
|
@ -288,7 +288,7 @@ describe('Markdown field header component', () => {
|
|||
},
|
||||
});
|
||||
|
||||
expect(findCommentTemplatesDropdown().exists()).toBe(false);
|
||||
expect(findCommentTemplatesModal().exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -289,7 +289,7 @@ RSpec.describe Resolvers::MergeRequestsResolver, feature_category: :code_review_
|
|||
|
||||
context 'with deployment' do
|
||||
it 'returns merge request with matching deployment' do
|
||||
result = resolve_mr(project, deployment_id: gprd.id)
|
||||
result = resolve_mr(project, deployment_id: deploy2.id)
|
||||
|
||||
expect(result).to contain_exactly(merge_request_2)
|
||||
end
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ RSpec.describe EnvironmentHelper, feature_category: :environment_management do
|
|||
name: environment.name,
|
||||
id: environment.id,
|
||||
project_full_path: project.full_path,
|
||||
base_path: project_environment_path(project, environment),
|
||||
external_url: environment.external_url,
|
||||
can_update_environment: true,
|
||||
can_destroy_environment: true,
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe ::Routing::PseudonymizationHelper do
|
||||
RSpec.describe ::Routing::PseudonymizationHelper, feature_category: :product_analytics_data_management do
|
||||
let_it_be(:group) { create(:group) }
|
||||
let_it_be(:subgroup) { create(:group, parent: group) }
|
||||
let_it_be(:project) { create(:project, group: group) }
|
||||
|
|
@ -307,4 +307,90 @@ RSpec.describe ::Routing::PseudonymizationHelper do
|
|||
expect(subject).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
describe '#masked_referrer_url' do
|
||||
let(:original_url) { "http://localhost/#{project.full_path}/-/issues/123" }
|
||||
let(:masked_url) { 'http://localhost/namespace/project/-/issues/123' }
|
||||
|
||||
it 'masks sensitive parameters in the URL' do
|
||||
expect(helper.masked_referrer_url(original_url)).to eq(masked_url)
|
||||
end
|
||||
|
||||
context 'when an error occurs' do
|
||||
before do
|
||||
allow(Rails.application.routes).to receive(:recognize_path)
|
||||
.with(original_url)
|
||||
.and_raise(ActionController::RoutingError, 'Some routing error')
|
||||
allow(helper).to receive(:request).and_return(
|
||||
double(
|
||||
:Request,
|
||||
original_url: original_url,
|
||||
original_fullpath: '/dashboard/issues?assignee_username=root'
|
||||
)
|
||||
)
|
||||
end
|
||||
|
||||
it 'calls error tracking and returns nil' do
|
||||
expect(Gitlab::ErrorTracking).to receive(:track_exception)
|
||||
.with(
|
||||
ActionController::RoutingError,
|
||||
url: '/dashboard/issues?assignee_username=root'
|
||||
).and_call_original
|
||||
expect(helper.masked_referrer_url(original_url)).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
context 'with controller for projects' do
|
||||
let(:original_url) { "http://localhost/#{project.full_path}" }
|
||||
let(:masked_url) { 'http://localhost/namespace/project' }
|
||||
|
||||
it 'masks sensitive parameters in the URL for projects controller' do
|
||||
expect(helper.masked_referrer_url(original_url)).to eq(masked_url)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with controller for projects/issues' do
|
||||
let(:original_url) { "http://localhost/#{project.full_path}/-/issues" }
|
||||
let(:masked_url) { 'http://localhost/namespace/project/-/issues' }
|
||||
|
||||
it 'masks sensitive parameters in the URL for projects/issues controller' do
|
||||
expect(helper.masked_referrer_url(original_url)).to eq(masked_url)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'masked_query_params' do
|
||||
let(:helper) { Class.new { include Routing::PseudonymizationHelper }.new }
|
||||
|
||||
context 'when there are no query parameters' do
|
||||
it 'returns nil' do
|
||||
uri = URI.parse('https://gitlab.com')
|
||||
expect(helper.masked_query_params(uri)).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
context 'when there are query parameters to mask' do
|
||||
it 'masks the appropriate query parameters' do
|
||||
uri = URI.parse('https://gitlab.com?user_id=123&token=abc')
|
||||
result = helper.masked_query_params(uri)
|
||||
expect(result).to eq({ 'user_id' => ['masked_user_id'], 'token' => ['masked_token'] })
|
||||
end
|
||||
end
|
||||
|
||||
context 'when there are query parameters that should not be masked' do
|
||||
it 'does not mask the excluded query parameters' do
|
||||
uri = URI.parse('https://gitlab.com?scope=all&user_id=123')
|
||||
result = helper.masked_query_params(uri)
|
||||
expect(result).to eq({ 'scope' => ['all'], 'user_id' => ['masked_user_id'] })
|
||||
end
|
||||
end
|
||||
|
||||
context 'when there are mixed query parameters' do
|
||||
it 'masks only the non-excluded query parameters' do
|
||||
uri = URI.parse('http://localhost?scope=all&state=opened&user_id=123')
|
||||
result = helper.masked_query_params(uri)
|
||||
expect(result).to eq({ 'scope' => ['all'], 'state' => ['opened'], 'user_id' => ['masked_user_id'] })
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ RSpec.describe WorkItemsHelper, feature_category: :team_planning do
|
|||
register_path: new_user_registration_path(redirect_to_referer: 'yes'),
|
||||
sign_in_path: user_session_path(redirect_to_referer: 'yes'),
|
||||
new_comment_template_paths:
|
||||
[{ text: "Manage your comment templates", path: profile_comment_templates_path }].to_json,
|
||||
[{ text: "Your comment templates", href: profile_comment_templates_path }].to_json,
|
||||
report_abuse_path: add_category_abuse_reports_path
|
||||
}
|
||||
)
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe API::Boards, :with_license, feature_category: :team_planning do
|
||||
RSpec.describe API::Boards, :with_license, feature_category: :portfolio_management do
|
||||
let_it_be(:user) { create(:user) }
|
||||
let_it_be(:non_member) { create(:user) }
|
||||
let_it_be(:guest) { create(:user) }
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe 'get list of boards', feature_category: :team_planning do
|
||||
RSpec.describe 'get list of boards', feature_category: :portfolio_management do
|
||||
include GraphqlHelpers
|
||||
|
||||
include_context 'group and project boards query context'
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe 'getting group recent issue boards', feature_category: :team_planning do
|
||||
RSpec.describe 'getting group recent issue boards', feature_category: :portfolio_management do
|
||||
include GraphqlHelpers
|
||||
|
||||
it_behaves_like 'querying a GraphQL type recent boards' do
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Mutations::Boards::Create, feature_category: :team_planning do
|
||||
RSpec.describe Mutations::Boards::Create, feature_category: :portfolio_management do
|
||||
let_it_be(:parent) { create(:project) }
|
||||
let_it_be(:current_user, reload: true) { create(:user) }
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Mutations::Boards::Destroy, feature_category: :team_planning do
|
||||
RSpec.describe Mutations::Boards::Destroy, feature_category: :portfolio_management do
|
||||
include GraphqlHelpers
|
||||
|
||||
let_it_be(:current_user, reload: true) { create(:user) }
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Mutations::Boards::Lists::Destroy, feature_category: :team_planning do
|
||||
RSpec.describe Mutations::Boards::Lists::Destroy, feature_category: :portfolio_management do
|
||||
include GraphqlHelpers
|
||||
|
||||
let_it_be(:current_user, reload: true) { create(:user) }
|
||||
|
|
|
|||
|
|
@ -0,0 +1,124 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe "projectBlobsRemove", feature_category: :source_code_management do
|
||||
include GraphqlHelpers
|
||||
|
||||
let_it_be(:project) { create(:project, :repository) }
|
||||
let_it_be(:current_user) { create(:user, owner_of: project) }
|
||||
let_it_be(:repo) { project.repository }
|
||||
|
||||
let(:project_path) { project.full_path }
|
||||
let(:mutation_params) { { project_path: project_path, blob_oids: blob_oids } }
|
||||
let(:mutation) { graphql_mutation(:project_blobs_remove, mutation_params) }
|
||||
let(:blob_oids) { ['53855584db773c3df5b5f61f72974cb298822fbb'] }
|
||||
|
||||
subject(:post_mutation) { post_graphql_mutation(mutation, current_user: current_user) }
|
||||
|
||||
describe 'Removing blobs:' do
|
||||
before do
|
||||
::Gitlab::GitalyClient.clear_stubs!
|
||||
end
|
||||
|
||||
it 'submits blobs to rewriteHistory RPC' do
|
||||
expect_next_instance_of(Gitaly::CleanupService::Stub) do |instance|
|
||||
blobs = array_including(gitaly_request_with_params(blobs: blob_oids))
|
||||
expect(instance).to receive(:rewrite_history)
|
||||
.with(blobs, kind_of(Hash))
|
||||
.and_return(Gitaly::RewriteHistoryResponse.new)
|
||||
end
|
||||
|
||||
post_mutation
|
||||
|
||||
expect(graphql_mutation_response(:project_blobs_remove)['errors']).not_to be_present
|
||||
end
|
||||
|
||||
it 'does not create an audit event' do
|
||||
allow_next_instance_of(Gitaly::CleanupService::Stub) do |instance|
|
||||
blobs = array_including(gitaly_request_with_params(blobs: blob_oids))
|
||||
allow(instance).to receive(:rewrite_history)
|
||||
.with(blobs, kind_of(Hash))
|
||||
.and_return(Gitaly::RewriteHistoryResponse.new)
|
||||
end
|
||||
|
||||
expect { post_mutation }.not_to change { AuditEvent.count }
|
||||
end
|
||||
end
|
||||
|
||||
describe 'Invalid requests:' do
|
||||
context 'when the current_user is a maintainer' do
|
||||
let(:current_user) { create(:user, maintainer_of: project) }
|
||||
|
||||
it_behaves_like 'a mutation on an unauthorized resource'
|
||||
end
|
||||
|
||||
context 'when arg `projectPath` is invalid' do
|
||||
let(:project_path) { 'gid://Gitlab/User/1' }
|
||||
|
||||
it 'returns an error' do
|
||||
post_mutation
|
||||
|
||||
expect(graphql_errors).to include(a_hash_including('message' => <<~MESSAGE.strip))
|
||||
The resource that you are attempting to access does not exist or you don't have permission to perform this action
|
||||
MESSAGE
|
||||
end
|
||||
end
|
||||
|
||||
context 'when arg `blob_oids` is nil' do
|
||||
let(:blob_oids) { nil }
|
||||
|
||||
it 'returns an error' do
|
||||
post_mutation
|
||||
|
||||
expect(graphql_errors).to include(a_hash_including('message' => <<~MESSAGE.strip))
|
||||
Variable $projectBlobsRemoveInput of type projectBlobsRemoveInput! was provided invalid value for blobOids (Expected value to not be null)
|
||||
MESSAGE
|
||||
end
|
||||
end
|
||||
|
||||
context 'when arg `blob_oids` is an empty list' do
|
||||
let(:blob_oids) { [] }
|
||||
|
||||
it 'returns an error' do
|
||||
post_mutation
|
||||
|
||||
expect(graphql_errors).to include(a_hash_including('message' => <<~MESSAGE))
|
||||
Argument 'blobOids' on InputObject 'projectBlobsRemoveInput' is required. Expected type [String!]!
|
||||
MESSAGE
|
||||
end
|
||||
end
|
||||
|
||||
context 'when arg `blob_oids` does not contain any valid strings' do
|
||||
let(:blob_oids) { ["", ""] }
|
||||
|
||||
it 'returns an error' do
|
||||
post_mutation
|
||||
|
||||
expect(graphql_errors).to include(a_hash_including('message' => <<~MESSAGE))
|
||||
Argument 'blobOids' on InputObject 'projectBlobsRemoveInput' is required. Expected type [String!]!
|
||||
MESSAGE
|
||||
end
|
||||
end
|
||||
|
||||
context 'when Gitaly RPC returns an error' do
|
||||
before do
|
||||
::Gitlab::GitalyClient.clear_stubs!
|
||||
end
|
||||
|
||||
let(:error_message) { 'error message' }
|
||||
|
||||
it 'returns a generic error message' do
|
||||
expect_next_instance_of(Gitaly::CleanupService::Stub) do |instance|
|
||||
blobs = array_including(gitaly_request_with_params(blobs: blob_oids))
|
||||
generic_error = GRPC::BadStatus.new(GRPC::Core::StatusCodes::FAILED_PRECONDITION, error_message)
|
||||
expect(instance).to receive(:rewrite_history).with(blobs, kind_of(Hash)).and_raise(generic_error)
|
||||
end
|
||||
|
||||
post_mutation
|
||||
|
||||
expect(graphql_errors).to include(a_hash_including('message' => "Internal server error: 9:#{error_message}"))
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe 'getting project recent issue boards', feature_category: :team_planning do
|
||||
RSpec.describe 'getting project recent issue boards', feature_category: :portfolio_management do
|
||||
include GraphqlHelpers
|
||||
|
||||
it_behaves_like 'querying a GraphQL type recent boards' do
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe API::GroupBoards, :with_license, feature_category: :team_planning do
|
||||
RSpec.describe API::GroupBoards, :with_license, feature_category: :portfolio_management do
|
||||
let_it_be(:user) { create(:user) }
|
||||
let_it_be(:non_member) { create(:user) }
|
||||
let_it_be(:guest) { create(:user) }
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Boards::CreateService, feature_category: :team_planning do
|
||||
RSpec.describe Boards::CreateService, feature_category: :portfolio_management do
|
||||
describe '#execute' do
|
||||
context 'when board parent is a project' do
|
||||
let(:parent) { create(:project) }
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Boards::DestroyService, feature_category: :team_planning do
|
||||
RSpec.describe Boards::DestroyService, feature_category: :portfolio_management do
|
||||
context 'with project board' do
|
||||
let_it_be(:parent) { create(:project) }
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Boards::Issues::CreateService, feature_category: :team_planning do
|
||||
RSpec.describe Boards::Issues::CreateService, feature_category: :portfolio_management do
|
||||
describe '#execute' do
|
||||
let(:project) { create(:project) }
|
||||
let(:board) { create(:board, project: project) }
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Boards::Issues::ListService, feature_category: :team_planning do
|
||||
RSpec.describe Boards::Issues::ListService, feature_category: :portfolio_management do
|
||||
describe '#execute' do
|
||||
let_it_be(:user) { create(:user) }
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Boards::Issues::MoveService, feature_category: :team_planning do
|
||||
RSpec.describe Boards::Issues::MoveService, feature_category: :portfolio_management do
|
||||
describe '#execute' do
|
||||
context 'when parent is a project' do
|
||||
let(:user) { create(:user) }
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Boards::Lists::CreateService, feature_category: :team_planning do
|
||||
RSpec.describe Boards::Lists::CreateService, feature_category: :portfolio_management do
|
||||
context 'when board parent is a project' do
|
||||
let_it_be(:parent) { create(:project) }
|
||||
let_it_be(:board) { create(:board, project: parent) }
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Boards::Lists::DestroyService, feature_category: :team_planning do
|
||||
RSpec.describe Boards::Lists::DestroyService, feature_category: :portfolio_management do
|
||||
let_it_be(:user) { create(:user) }
|
||||
|
||||
let(:list_type) { :list }
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Boards::Lists::ListService, feature_category: :team_planning do
|
||||
RSpec.describe Boards::Lists::ListService, feature_category: :portfolio_management do
|
||||
let_it_be(:user) { create(:user) }
|
||||
let_it_be(:group) { create(:group) }
|
||||
|
||||
|
|
|
|||