summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2021-12-15 21:11:32 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2021-12-15 21:11:32 +0000
commit1c898dc5c10bbedf94386d917259153d73608495 (patch)
treef939cf185da9e96f7aba2200fa5ac74deffd71f9
parent22baaecaa84003c554f35752a729331e956d7659 (diff)
downloadgitlab-ce-1c898dc5c10bbedf94386d917259153d73608495.tar.gz
Add latest changes from gitlab-org/gitlab@master
-rw-r--r--app/assets/javascripts/emoji/index.js2
-rw-r--r--app/assets/javascripts/ide/components/ide_tree_list.vue14
-rw-r--r--app/assets/javascripts/ide/ide_router.js69
-rw-r--r--app/assets/javascripts/ide/index.js11
-rw-r--r--app/assets/javascripts/ide/services/index.js36
-rw-r--r--app/assets/javascripts/ide/stores/actions/project.js57
-rw-r--r--app/assets/javascripts/ide/stores/mutation_types.js1
-rw-r--r--app/assets/javascripts/ide/stores/mutations/project.js12
-rw-r--r--app/assets/javascripts/pages/dashboard/projects/index/components/customize_homepage_banner.vue102
-rw-r--r--app/assets/javascripts/pages/dashboard/projects/index/index.js2
-rw-r--r--app/assets/javascripts/pages/dashboard/projects/index/init_customize_homepage_banner.js16
-rw-r--r--app/helpers/ide_helper.rb2
-rw-r--r--app/helpers/users/callouts_helper.rb5
-rw-r--r--app/models/user.rb4
-rw-r--r--app/models/users/callout.rb1
-rw-r--r--app/services/events/destroy_service.rb21
-rw-r--r--app/services/projects/destroy_service.rb13
-rw-r--r--app/views/groups/_import_group_from_file_panel.html.haml14
-rw-r--r--app/views/groups/settings/_export.html.haml6
-rw-r--r--app/views/layouts/_page.html.haml1
-rw-r--r--app/views/projects/hooks/edit.html.haml16
-rw-r--r--app/views/root/index.html.haml10
-rw-r--r--app/views/shared/web_hooks/_hook.html.haml6
-rw-r--r--config/feature_flags/development/scim_token_vue.yml2
-rw-r--r--doc/api/graphql/reference/index.md1
-rw-r--r--doc/development/documentation/styleguide/word_list.md8
-rw-r--r--doc/user/application_security/dependency_scanning/index.md16
-rw-r--r--lib/gitlab/emoji.rb2
-rw-r--r--lib/gitlab/import_export/project/import_export.yml2
-rw-r--r--lib/gitlab/import_export/project/relation_factory.rb4
-rw-r--r--locale/gitlab.pot36
-rw-r--r--spec/controllers/root_controller_spec.rb24
-rw-r--r--spec/features/dashboard/root_spec.rb19
-rw-r--r--spec/frontend/ide/components/ide_tree_list_spec.js13
-rw-r--r--spec/frontend/ide/ide_router_spec.js61
-rw-r--r--spec/frontend/ide/services/index_spec.js63
-rw-r--r--spec/frontend/ide/stores/actions/project_spec.js93
-rw-r--r--spec/frontend/ide/stores/mutations/project_spec.js37
-rw-r--r--spec/frontend/pages/dashboard/projects/index/components/customize_homepage_banner_spec.js108
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select_vue/mock_data.js2
-rw-r--r--spec/frontend_integration/ide/helpers/ide_helper.js14
-rw-r--r--spec/frontend_integration/ide/helpers/start.js4
-rw-r--r--spec/frontend_integration/ide/user_opens_ide_spec.js6
-rw-r--r--spec/helpers/ide_helper_spec.rb2
-rw-r--r--spec/helpers/users/callouts_helper_spec.rb30
-rw-r--r--spec/lib/gitlab/import_export/safe_model_attributes.yml1
-rw-r--r--spec/models/user_spec.rb14
-rw-r--r--spec/services/events/destroy_service_spec.rb50
-rw-r--r--spec/services/projects/destroy_service_spec.rb21
-rw-r--r--spec/views/projects/hooks/edit.html.haml_spec.rb33
-rw-r--r--spec/views/projects/hooks/index.html.haml_spec.rb36
51 files changed, 586 insertions, 537 deletions
diff --git a/app/assets/javascripts/emoji/index.js b/app/assets/javascripts/emoji/index.js
index 478e3f6aed9..c0f2153ce5b 100644
--- a/app/assets/javascripts/emoji/index.js
+++ b/app/assets/javascripts/emoji/index.js
@@ -10,7 +10,7 @@ let validEmojiNames = null;
export const FALLBACK_EMOJI_KEY = 'grey_question';
// Keep the version in sync with `lib/gitlab/emoji.rb`
-export const EMOJI_VERSION = '1';
+export const EMOJI_VERSION = '2';
const isLocalStorageAvailable = AccessorUtilities.canUseLocalStorage();
diff --git a/app/assets/javascripts/ide/components/ide_tree_list.vue b/app/assets/javascripts/ide/components/ide_tree_list.vue
index b987adc8bae..0fc7337ad26 100644
--- a/app/assets/javascripts/ide/components/ide_tree_list.vue
+++ b/app/assets/javascripts/ide/components/ide_tree_list.vue
@@ -29,14 +29,20 @@ export default {
},
},
watch: {
- showLoading(newVal) {
- if (!newVal) {
- this.$emit('tree-ready');
- }
+ showLoading() {
+ this.notifyTreeReady();
},
},
+ mounted() {
+ this.notifyTreeReady();
+ },
methods: {
...mapActions(['toggleTreeOpen']),
+ notifyTreeReady() {
+ if (!this.showLoading) {
+ this.$emit('tree-ready');
+ }
+ },
clickedFile() {
performanceMarkAndMeasure({ mark: WEBIDE_MARK_FILE_CLICKED });
},
diff --git a/app/assets/javascripts/ide/ide_router.js b/app/assets/javascripts/ide/ide_router.js
index 27cedd80347..1fc447886bb 100644
--- a/app/assets/javascripts/ide/ide_router.js
+++ b/app/assets/javascripts/ide/ide_router.js
@@ -1,8 +1,6 @@
import Vue from 'vue';
-import createFlash from '~/flash';
import IdeRouter from '~/ide/ide_router_extension';
import { joinPaths } from '~/lib/utils/url_utility';
-import { __ } from '~/locale';
import {
WEBIDE_MARK_FETCH_PROJECT_DATA_START,
WEBIDE_MARK_FETCH_PROJECT_DATA_FINISH,
@@ -75,49 +73,34 @@ export const createRouter = (store, defaultBranch) => {
router.beforeEach((to, from, next) => {
if (to.params.namespace && to.params.project) {
- performanceMarkAndMeasure({ mark: WEBIDE_MARK_FETCH_PROJECT_DATA_START });
- store
- .dispatch('getProjectData', {
- namespace: to.params.namespace,
- projectId: to.params.project,
- })
- .then(() => {
- const basePath = to.params.pathMatch || '';
- const projectId = `${to.params.namespace}/${to.params.project}`;
- const branchId = to.params.branchid;
- const mergeRequestId = to.params.mrid;
+ const basePath = to.params.pathMatch || '';
+ const projectId = `${to.params.namespace}/${to.params.project}`;
+ const branchId = to.params.branchid;
+ const mergeRequestId = to.params.mrid;
- if (branchId) {
- performanceMarkAndMeasure({
- mark: WEBIDE_MARK_FETCH_PROJECT_DATA_FINISH,
- measures: [
- {
- name: WEBIDE_MEASURE_FETCH_PROJECT_DATA,
- start: WEBIDE_MARK_FETCH_PROJECT_DATA_START,
- },
- ],
- });
- store.dispatch('openBranch', {
- projectId,
- branchId,
- basePath,
- });
- } else if (mergeRequestId) {
- store.dispatch('openMergeRequest', {
- projectId,
- mergeRequestId,
- targetProjectId: to.query.target_project,
- });
- }
- })
- .catch((e) => {
- createFlash({
- message: __('Error while loading the project data. Please try again.'),
- fadeTransition: false,
- addBodyClass: true,
- });
- throw e;
+ performanceMarkAndMeasure({ mark: WEBIDE_MARK_FETCH_PROJECT_DATA_START });
+ if (branchId) {
+ performanceMarkAndMeasure({
+ mark: WEBIDE_MARK_FETCH_PROJECT_DATA_FINISH,
+ measures: [
+ {
+ name: WEBIDE_MEASURE_FETCH_PROJECT_DATA,
+ start: WEBIDE_MARK_FETCH_PROJECT_DATA_START,
+ },
+ ],
+ });
+ store.dispatch('openBranch', {
+ projectId,
+ branchId,
+ basePath,
+ });
+ } else if (mergeRequestId) {
+ store.dispatch('openMergeRequest', {
+ projectId,
+ mergeRequestId,
+ targetProjectId: to.query.target_project,
});
+ }
}
next();
diff --git a/app/assets/javascripts/ide/index.js b/app/assets/javascripts/ide/index.js
index bdffed70882..df643675357 100644
--- a/app/assets/javascripts/ide/index.js
+++ b/app/assets/javascripts/ide/index.js
@@ -34,11 +34,18 @@ Vue.use(PerformancePlugin, {
* @param {extendStoreCallback} options.extendStore -
* Function that receives the default store and returns an extended one.
*/
-export function initIde(el, options = {}) {
+export const initIde = (el, options = {}) => {
if (!el) return null;
const { rootComponent = ide, extendStore = identity } = options;
+
const store = createStore();
+ const project = JSON.parse(el.dataset.project);
+ store.dispatch('setProject', { project });
+
+ // fire and forget fetching non-critical project info
+ store.dispatch('fetchProjectPermissions');
+
const router = createRouter(store, el.dataset.defaultBranch || DEFAULT_BRANCH);
return new Vue({
@@ -77,7 +84,7 @@ export function initIde(el, options = {}) {
return createElement(rootComponent);
},
});
-}
+};
/**
* Start the IDE.
diff --git a/app/assets/javascripts/ide/services/index.js b/app/assets/javascripts/ide/services/index.js
index 37a08bc4feb..805476c71bc 100644
--- a/app/assets/javascripts/ide/services/index.js
+++ b/app/assets/javascripts/ide/services/index.js
@@ -1,23 +1,12 @@
-import getIdeProject from 'ee_else_ce/ide/queries/get_ide_project.query.graphql';
import Api from '~/api';
+import getIdeProject from 'ee_else_ce/ide/queries/get_ide_project.query.graphql';
import dismissUserCallout from '~/graphql_shared/mutations/dismiss_user_callout.mutation.graphql';
+import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import axios from '~/lib/utils/axios_utils';
import { joinPaths, escapeFileUrl } from '~/lib/utils/url_utility';
import ciConfig from '~/pipeline_editor/graphql/queries/ci_config.query.graphql';
-import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { query, mutate } from './gql';
-const fetchApiProjectData = (projectPath) => Api.project(projectPath).then(({ data }) => data);
-
-const fetchGqlProjectData = (projectPath) =>
- query({
- query: getIdeProject,
- variables: { projectPath },
- }).then(({ data }) => ({
- ...data.project,
- id: getIdFromGraphQLId(data.project.id),
- }));
-
export default {
getFileData(endpoint) {
return axios.get(endpoint, {
@@ -65,18 +54,6 @@ export default {
)
.then(({ data }) => data);
},
- getProjectData(namespace, project) {
- const projectPath = `${namespace}/${project}`;
-
- return Promise.all([fetchApiProjectData(projectPath), fetchGqlProjectData(projectPath)]).then(
- ([apiProjectData, gqlProjectData]) => ({
- data: {
- ...apiProjectData,
- ...gqlProjectData,
- },
- }),
- );
- },
getProjectMergeRequests(projectId, params = {}) {
return Api.projectMergeRequests(projectId, params);
},
@@ -119,4 +96,13 @@ export default {
variables: { input: { featureName: name } },
}).then(({ data }) => data);
},
+ getProjectPermissionsData(projectPath) {
+ return query({
+ query: getIdeProject,
+ variables: { projectPath },
+ }).then(({ data }) => ({
+ ...data.project,
+ id: getIdFromGraphQLId(data.project.id),
+ }));
+ },
};
diff --git a/app/assets/javascripts/ide/stores/actions/project.js b/app/assets/javascripts/ide/stores/actions/project.js
index 93ad19ba81e..0ec808339fb 100644
--- a/app/assets/javascripts/ide/stores/actions/project.js
+++ b/app/assets/javascripts/ide/stores/actions/project.js
@@ -1,35 +1,44 @@
import { escape } from 'lodash';
import createFlash from '~/flash';
import { __, sprintf } from '~/locale';
+import { logError } from '~/lib/logger';
import api from '../../../api';
import service from '../../services';
import * as types from '../mutation_types';
-export const getProjectData = ({ commit, state }, { namespace, projectId, force = false } = {}) =>
- new Promise((resolve, reject) => {
- if (!state.projects[`${namespace}/${projectId}`] || force) {
- commit(types.TOGGLE_LOADING, { entry: state });
- service
- .getProjectData(namespace, projectId)
- .then((res) => res.data)
- .then((data) => {
- commit(types.TOGGLE_LOADING, { entry: state });
- commit(types.SET_PROJECT, { projectPath: `${namespace}/${projectId}`, project: data });
- commit(types.SET_CURRENT_PROJECT, `${namespace}/${projectId}`);
- resolve(data);
- })
- .catch(() => {
- createFlash({
- message: __('Error loading project data. Please try again.'),
- fadeTransition: false,
- addBodyClass: true,
- });
- reject(new Error(`Project not loaded ${namespace}/${projectId}`));
- });
- } else {
- resolve(state.projects[`${namespace}/${projectId}`]);
- }
+const ERROR_LOADING_PROJECT = __('Error loading project data. Please try again.');
+
+const errorFetchingData = (e) => {
+ logError(ERROR_LOADING_PROJECT, e);
+
+ createFlash({
+ message: ERROR_LOADING_PROJECT,
+ fadeTransition: false,
+ addBodyClass: true,
});
+};
+
+export const setProject = ({ commit }, { project } = {}) => {
+ if (!project) {
+ return;
+ }
+ const projectPath = project.path_with_namespace;
+ commit(types.SET_PROJECT, { projectPath, project });
+ commit(types.SET_CURRENT_PROJECT, projectPath);
+};
+
+export const fetchProjectPermissions = ({ commit, state }) => {
+ const projectPath = state.currentProjectId;
+ if (!projectPath) {
+ return undefined;
+ }
+ return service
+ .getProjectPermissionsData(projectPath)
+ .then((permissions) => {
+ commit(types.UPDATE_PROJECT, { projectPath, props: permissions });
+ })
+ .catch(errorFetchingData);
+};
export const refreshLastCommitData = ({ commit }, { projectId, branchId } = {}) =>
service
diff --git a/app/assets/javascripts/ide/stores/mutation_types.js b/app/assets/javascripts/ide/stores/mutation_types.js
index 77755b179ef..13f338c4a48 100644
--- a/app/assets/javascripts/ide/stores/mutation_types.js
+++ b/app/assets/javascripts/ide/stores/mutation_types.js
@@ -8,6 +8,7 @@ export const SET_LINKS = 'SET_LINKS';
// Project Mutation Types
export const SET_PROJECT = 'SET_PROJECT';
export const SET_CURRENT_PROJECT = 'SET_CURRENT_PROJECT';
+export const UPDATE_PROJECT = 'UPDATE_PROJECT';
export const TOGGLE_EMPTY_STATE = 'TOGGLE_EMPTY_STATE';
// Merge request mutation types
diff --git a/app/assets/javascripts/ide/stores/mutations/project.js b/app/assets/javascripts/ide/stores/mutations/project.js
index 034fdad4305..9f65d3a543e 100644
--- a/app/assets/javascripts/ide/stores/mutations/project.js
+++ b/app/assets/javascripts/ide/stores/mutations/project.js
@@ -1,3 +1,4 @@
+import Vue from 'vue';
import * as types from '../mutation_types';
export default {
@@ -24,4 +25,15 @@ export default {
empty_repo: value,
});
},
+ [types.UPDATE_PROJECT](state, { projectPath, props }) {
+ const project = state.projects[projectPath];
+
+ if (!project || !props) {
+ return;
+ }
+
+ Object.keys(props).forEach((key) => {
+ Vue.set(project, key, props[key]);
+ });
+ },
};
diff --git a/app/assets/javascripts/pages/dashboard/projects/index/components/customize_homepage_banner.vue b/app/assets/javascripts/pages/dashboard/projects/index/components/customize_homepage_banner.vue
deleted file mode 100644
index 99461475af0..00000000000
--- a/app/assets/javascripts/pages/dashboard/projects/index/components/customize_homepage_banner.vue
+++ /dev/null
@@ -1,102 +0,0 @@
-<script>
-import { GlBanner } from '@gitlab/ui';
-import axios from '~/lib/utils/axios_utils';
-import { s__ } from '~/locale';
-import Tracking from '~/tracking';
-
-const trackingMixin = Tracking.mixin();
-
-export default {
- components: {
- GlBanner,
- },
- mixins: [trackingMixin],
- inject: {
- svgPath: {
- default: '',
- },
- preferencesBehaviorPath: {
- default: '',
- },
- calloutsPath: {
- default: '',
- },
- calloutsFeatureId: {
- default: '',
- },
- trackLabel: {
- default: '',
- },
- },
- i18n: {
- title: s__('CustomizeHomepageBanner|Do you want to customize this page?'),
- body: s__(
- 'CustomizeHomepageBanner|This page shows a list of your projects by default but it can be changed to show projects\' activity, groups, your to-do list, assigned issues, assigned merge requests, and more. You can change this under "Homepage content" in your preferences',
- ),
- button_text: s__('CustomizeHomepageBanner|Go to preferences'),
- },
- data() {
- return {
- visible: true,
- tracking: {
- label: this.trackLabel,
- },
- };
- },
- created() {
- this.$nextTick(() => {
- this.addTrackingAttributesToButton();
- });
- },
- mounted() {
- this.trackOnShow();
- },
- methods: {
- handleClose() {
- axios
- .post(this.calloutsPath, {
- feature_name: this.calloutsFeatureId,
- })
- .catch((e) => {
- // eslint-disable-next-line @gitlab/require-i18n-strings, no-console
- console.error('Failed to dismiss banner.', e);
- });
-
- this.visible = false;
- this.track('click_dismiss');
- },
- trackOnShow() {
- if (this.visible) this.track('show_home_page_banner');
- },
- addTrackingAttributesToButton() {
- // we can't directly add these on the button like we need to due to
- // button not being modifiable currently
- // https://gitlab.com/gitlab-org/gitlab-ui/-/blob/9209ec424e5cca14bc8a1b5c9fa12636d8c83dad/src/components/base/banner/banner.vue#L60
- const button = this.$refs.banner.$el.querySelector(
- `[href='${this.preferencesBehaviorPath}']`,
- );
-
- if (button) {
- button.setAttribute('data-track-action', 'click_go_to_preferences');
- button.setAttribute('data-track-label', this.trackLabel);
- }
- },
- },
-};
-</script>
-
-<template>
- <gl-banner
- v-if="visible"
- ref="banner"
- :title="$options.i18n.title"
- :button-text="$options.i18n.button_text"
- :button-link="preferencesBehaviorPath"
- :svg-path="svgPath"
- @close="handleClose"
- >
- <p>
- {{ $options.i18n.body }}
- </p>
- </gl-banner>
-</template>
diff --git a/app/assets/javascripts/pages/dashboard/projects/index/index.js b/app/assets/javascripts/pages/dashboard/projects/index/index.js
index c34d15b869a..6c9378b7231 100644
--- a/app/assets/javascripts/pages/dashboard/projects/index/index.js
+++ b/app/assets/javascripts/pages/dashboard/projects/index/index.js
@@ -1,5 +1,3 @@
import ProjectsList from '~/projects_list';
-import initCustomizeHomepageBanner from './init_customize_homepage_banner';
new ProjectsList(); // eslint-disable-line no-new
-initCustomizeHomepageBanner();
diff --git a/app/assets/javascripts/pages/dashboard/projects/index/init_customize_homepage_banner.js b/app/assets/javascripts/pages/dashboard/projects/index/init_customize_homepage_banner.js
deleted file mode 100644
index 8cdcd3134ee..00000000000
--- a/app/assets/javascripts/pages/dashboard/projects/index/init_customize_homepage_banner.js
+++ /dev/null
@@ -1,16 +0,0 @@
-import Vue from 'vue';
-import CustomizeHomepageBanner from './components/customize_homepage_banner.vue';
-
-export default () => {
- const el = document.querySelector('.js-customize-homepage-banner');
-
- if (!el) {
- return false;
- }
-
- return new Vue({
- el,
- provide: { ...el.dataset },
- render: (createElement) => createElement(CustomizeHomepageBanner),
- });
-};
diff --git a/app/helpers/ide_helper.rb b/app/helpers/ide_helper.rb
index 09ff57e2baf..4d81aeca37a 100644
--- a/app/helpers/ide_helper.rb
+++ b/app/helpers/ide_helper.rb
@@ -29,7 +29,7 @@ module IdeHelper
def convert_to_project_entity_json(project)
return unless project
- API::Entities::Project.represent(project).to_json
+ API::Entities::Project.represent(project, current_user: current_user).to_json
end
def enable_environments_guidance?
diff --git a/app/helpers/users/callouts_helper.rb b/app/helpers/users/callouts_helper.rb
index 5ed17357e9b..32b0d7b3fe3 100644
--- a/app/helpers/users/callouts_helper.rb
+++ b/app/helpers/users/callouts_helper.rb
@@ -6,7 +6,6 @@ module Users
GCP_SIGNUP_OFFER = 'gcp_signup_offer'
SUGGEST_POPOVER_DISMISSED = 'suggest_popover_dismissed'
TABS_POSITION_HIGHLIGHT = 'tabs_position_highlight'
- CUSTOMIZE_HOMEPAGE = 'customize_homepage'
FEATURE_FLAGS_NEW_VERSION = 'feature_flags_new_version'
REGISTRATION_ENABLED_CALLOUT = 'registration_enabled_callout'
UNFINISHED_TAG_CLEANUP_CALLOUT = 'unfinished_tag_cleanup_callout'
@@ -36,10 +35,6 @@ module Users
!user_dismissed?(SUGGEST_POPOVER_DISMISSED)
end
- def show_customize_homepage_banner?
- current_user.default_dashboard? && !user_dismissed?(CUSTOMIZE_HOMEPAGE)
- end
-
def show_feature_flags_new_version?
!user_dismissed?(FEATURE_FLAGS_NEW_VERSION)
end
diff --git a/app/models/user.rb b/app/models/user.rb
index 98d2ceb6dbe..942b37c3cae 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -844,10 +844,6 @@ class User < ApplicationRecord
# Instance methods
#
- def default_dashboard?
- dashboard == self.class.column_defaults['dashboard']
- end
-
def full_path
username
end
diff --git a/app/models/users/callout.rb b/app/models/users/callout.rb
index 9a729072051..9ce0beed3b3 100644
--- a/app/models/users/callout.rb
+++ b/app/models/users/callout.rb
@@ -24,7 +24,6 @@ module Users
buy_pipeline_minutes_notification_dot: 19, # EE-only
personal_access_token_expiry: 21, # EE-only
suggest_pipeline: 22,
- customize_homepage: 23,
feature_flags_new_version: 24,
registration_enabled_callout: 25,
new_user_signups_cap_reached: 26, # EE-only
diff --git a/app/services/events/destroy_service.rb b/app/services/events/destroy_service.rb
new file mode 100644
index 00000000000..fdb718f0fcb
--- /dev/null
+++ b/app/services/events/destroy_service.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+module Events
+ class DestroyService
+ def initialize(project)
+ @project = project
+ end
+
+ def execute
+ project.events.all.delete_all
+
+ ServiceResponse.success(message: 'Events were deleted.')
+ rescue StandardError
+ ServiceResponse.error(message: 'Failed to remove events.')
+ end
+
+ private
+
+ attr_reader :project
+ end
+end
diff --git a/app/services/projects/destroy_service.rb b/app/services/projects/destroy_service.rb
index 3a9418f1dfa..0939ced38dd 100644
--- a/app/services/projects/destroy_service.rb
+++ b/app/services/projects/destroy_service.rb
@@ -75,6 +75,18 @@ module Projects
response.success?
end
+ def destroy_events!
+ unless remove_events
+ raise_error(s_('DeleteProject|Failed to remove events. Please try again or contact administrator.'))
+ end
+ end
+
+ def remove_events
+ response = ::Events::DestroyService.new(project).execute
+
+ response.success?
+ end
+
def remove_repository(repository)
return true unless repository
@@ -117,6 +129,7 @@ module Projects
log_destroy_event
trash_relation_repositories!
trash_project_repositories!
+ destroy_events!
destroy_web_hooks!
destroy_project_bots!
diff --git a/app/views/groups/_import_group_from_file_panel.html.haml b/app/views/groups/_import_group_from_file_panel.html.haml
index 8d6e043ebf7..0644910dd3e 100644
--- a/app/views/groups/_import_group_from_file_panel.html.haml
+++ b/app/views/groups/_import_group_from_file_panel.html.haml
@@ -6,9 +6,17 @@
.gl-border-l-solid.gl-border-r-solid.gl-border-gray-100.gl-border-1.gl-p-5
%h4
= _('Import group from file')
- %p
- = s_('GroupsNew|Provide credentials for another instance of GitLab to import your groups directly.')
- .form-group.gl-display-flex.gl-flex-direction-column
+ .gl-alert.gl-alert-warning{ role: 'alert' }
+ = sprite_icon('warning', css_class: 'gl-icon s16 gl-alert-icon gl-alert-icon-no-title')
+ .gl-alert-body
+ - docs_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: help_page_path('user/group/import/index.md') }
+ - link_end = '</a>'.html_safe
+ = s_('GroupsNew|This feature is deprecated and replaced by %{docs_link_start}Group Migration%{docs_link_end}.').html_safe % { docs_link_start: docs_link_start, docs_link_end: link_end }
+ - if Feature.enabled?(:bulk_import, default_enabled: :yaml)
+ - enable_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: help_page_path('user/group/import/index.md', anchor: 'enable-or-disable-gitlab-group-migration') }
+ = s_('GroupsNew|Ask your administrator to %{enable_link_start}enable%{enable_link_end} Group Migration.').html_safe % { enable_link_start: enable_link_start, enable_link_end: link_end }
+
+ .form-group.gl-display-flex.gl-flex-direction-column.gl-mt-5
= f.label :name, _('New group name'), for: 'import_group_name'
= f.text_field :name, placeholder: s_('GroupsNew|My Awesome Group'), class: 'js-autofill-group-name gl-form-input col-xs-12 col-sm-8',
required: true,
diff --git a/app/views/groups/settings/_export.html.haml b/app/views/groups/settings/_export.html.haml
index 3e3646df665..ff00ff1f6e8 100644
--- a/app/views/groups/settings/_export.html.haml
+++ b/app/views/groups/settings/_export.html.haml
@@ -5,6 +5,12 @@
.sub-section
%h4= s_('GroupSettings|Export group')
%p= _('Export this group with all related data.')
+ .gl-alert.gl-alert-warning.gl-mb-4{ role: 'alert' }
+ = sprite_icon('warning', css_class: 'gl-icon s16 gl-alert-icon gl-alert-icon-no-title')
+ .gl-alert-body
+ - docs_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: help_page_path('user/group/import/index.md') }
+ - docs_link_end = '</a>'.html_safe
+ = s_('GroupsNew|This feature is deprecated and replaced by %{docs_link_start}Group Migration%{docs_link_end}.').html_safe % { docs_link_start: docs_link_start, docs_link_end: docs_link_end }
%p
- export_information = _('After the export is complete, download the data file from a notification email or from this page. You can then import the data file from the %{strong_text_start}Create new group%{strong_text_end} page of another GitLab instance.') % { strong_text_start: '<strong>'.html_safe, strong_text_end: '</strong>'.html_safe}
= export_information.html_safe
diff --git a/app/views/layouts/_page.html.haml b/app/views/layouts/_page.html.haml
index be4beea78e1..3e875a0eb24 100644
--- a/app/views/layouts/_page.html.haml
+++ b/app/views/layouts/_page.html.haml
@@ -21,7 +21,6 @@
= render_if_exists "shared/namespace_user_cap_reached_alert"
= render_if_exists "shared/new_user_signups_cap_reached_alert"
= yield :page_level_alert
- = yield :customize_homepage_banner
- unless @hide_breadcrumbs
= render "layouts/nav/breadcrumbs"
%div{ class: "#{(container_class unless @no_container)} #{@content_class}" }
diff --git a/app/views/projects/hooks/edit.html.haml b/app/views/projects/hooks/edit.html.haml
index e8ea4ad90dc..3358025b237 100644
--- a/app/views/projects/hooks/edit.html.haml
+++ b/app/views/projects/hooks/edit.html.haml
@@ -1,7 +1,21 @@
- @content_class = 'limit-container-width' unless fluid_layout
-- add_to_breadcrumbs _('Webhook Settings'), namespace_project_hooks_path
+- add_to_breadcrumbs _('Webhook Settings'), project_hooks_path(@project)
- page_title _('Webhook')
+- if @hook.rate_limited?
+ - placeholders = { strong_start: '<strong>'.html_safe,
+ strong_end: '</strong>'.html_safe,
+ limit: @hook.rate_limit,
+ support_link_start: '<a href="https://support.gitlab.com/hc/en-us/requests/new">'.html_safe,
+ support_link_end: '</a>'.html_safe }
+ = render 'shared/global_alert',
+ title: s_('Webhooks|Webhook was automatically disabled'),
+ variant: :danger,
+ is_contained: true,
+ close_button_class: 'js-close' do
+ .gl-alert-body
+ = s_('Webhooks|The webhook was triggered more than %{limit} times per minute and is now disabled. To re-enable this webhook, fix the problems shown in %{strong_start}Recent events%{strong_end}, then re-test your settings. %{support_link_start}Contact Support%{support_link_end} if you need help re-enabling your webhook.').html_safe % placeholders
+
.row.gl-mt-3
.col-lg-3
= render 'shared/web_hooks/title_and_docs', hook: @hook
diff --git a/app/views/root/index.html.haml b/app/views/root/index.html.haml
deleted file mode 100644
index 4b1ac213d68..00000000000
--- a/app/views/root/index.html.haml
+++ /dev/null
@@ -1,10 +0,0 @@
-- if show_customize_homepage_banner?
- = content_for :customize_homepage_banner do
- .gl-display-none.gl-md-display-block{ class: "gl-pt-6! gl-pb-2! #{(container_class unless @no_container)} #{@content_class}" }
- .js-customize-homepage-banner{ data: { svg_path: image_path('illustrations/monitoring/getting_started.svg'),
- preferences_behavior_path: profile_preferences_path(anchor: 'behavior'),
- callouts_path: callouts_path,
- callouts_feature_id: Users::CalloutsHelper::CUSTOMIZE_HOMEPAGE,
- track_label: 'home_page' } }
-
-= render template: 'dashboard/projects/index'
diff --git a/app/views/shared/web_hooks/_hook.html.haml b/app/views/shared/web_hooks/_hook.html.haml
index fd124c2967d..ef1eb6b4e45 100644
--- a/app/views/shared/web_hooks/_hook.html.haml
+++ b/app/views/shared/web_hooks/_hook.html.haml
@@ -1,7 +1,11 @@
%li
.row
.col-md-8.col-lg-7
- %strong.light-header= hook.url
+ %strong.light-header
+ = hook.url
+ - if hook.rate_limited?
+ %span.gl-badge.badge-danger.badge-pill.sm= _('Disabled')
+
%div
- hook.class.triggers.each_value do |trigger|
- if hook.public_send(trigger)
diff --git a/config/feature_flags/development/scim_token_vue.yml b/config/feature_flags/development/scim_token_vue.yml
index 8bf7f2c4da5..8cc82bafd66 100644
--- a/config/feature_flags/development/scim_token_vue.yml
+++ b/config/feature_flags/development/scim_token_vue.yml
@@ -5,4 +5,4 @@ rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/347270
milestone: '14.6'
type: development
group: group::access
-default_enabled: false
+default_enabled: true
diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md
index 5269b3b65d1..18f57965f21 100644
--- a/doc/api/graphql/reference/index.md
+++ b/doc/api/graphql/reference/index.md
@@ -17190,7 +17190,6 @@ Name of the feature that the callout is for.
| <a id="usercalloutfeaturenameenumcanary_deployment"></a>`CANARY_DEPLOYMENT` | Callout feature name for canary_deployment. |
| <a id="usercalloutfeaturenameenumcloud_licensing_subscription_activation_banner"></a>`CLOUD_LICENSING_SUBSCRIPTION_ACTIVATION_BANNER` | Callout feature name for cloud_licensing_subscription_activation_banner. |
| <a id="usercalloutfeaturenameenumcluster_security_warning"></a>`CLUSTER_SECURITY_WARNING` | Callout feature name for cluster_security_warning. |
-| <a id="usercalloutfeaturenameenumcustomize_homepage"></a>`CUSTOMIZE_HOMEPAGE` | Callout feature name for customize_homepage. |
| <a id="usercalloutfeaturenameenumeoa_bronze_plan_banner"></a>`EOA_BRONZE_PLAN_BANNER` | Callout feature name for eoa_bronze_plan_banner. |
| <a id="usercalloutfeaturenameenumfeature_flags_new_version"></a>`FEATURE_FLAGS_NEW_VERSION` | Callout feature name for feature_flags_new_version. |
| <a id="usercalloutfeaturenameenumgcp_signup_offer"></a>`GCP_SIGNUP_OFFER` | Callout feature name for gcp_signup_offer. |
diff --git a/doc/development/documentation/styleguide/word_list.md b/doc/development/documentation/styleguide/word_list.md
index 5a497da0ea9..1b607cdac1b 100644
--- a/doc/development/documentation/styleguide/word_list.md
+++ b/doc/development/documentation/styleguide/word_list.md
@@ -5,9 +5,11 @@ info: To determine the technical writer assigned to the Stage/Group associated w
description: 'Writing styles, markup, formatting, and other standards for GitLab Documentation.'
---
-# A-Z word list
+# Recommended word list
-To help ensure consistency in the documentation, follow this guidance.
+To help ensure consistency in the documentation, the Technical Writing team
+recommends these wording choices. The GitLab handbook also maintains a list of
+[top misused terms](https://about.gitlab.com/handbook/communication/top-misused-terms/).
For guidance not on this page, we defer to these style guides:
@@ -769,7 +771,7 @@ Use **you**, **your**, and **yours** instead of [**the user** and **the user's**
Documentation should be from the [point of view](https://design.gitlab.com/content/voice-tone#point-of-view) of the reader.
- Do: You can configure a pipeline.
-- Do not: Users can configure a pipeline.
+- Do not: Users can configure a pipeline.
<!-- vale on -->
<!-- markdownlint-enable -->
diff --git a/doc/user/application_security/dependency_scanning/index.md b/doc/user/application_security/dependency_scanning/index.md
index 0e81151bd46..66f6d94a602 100644
--- a/doc/user/application_security/dependency_scanning/index.md
+++ b/doc/user/application_security/dependency_scanning/index.md
@@ -1037,3 +1037,19 @@ scan occurs. Because the cache is downloaded before the analyzer run occurs, the
file in the `CI_BUILDS_DIR` directory triggers the dependency scanning job.
We recommend committing the lock files, which prevents this warning.
+
+### I no longer get the latest Docker image after setting `DS_MAJOR_VERSION` or `DS_ANALYZER_IMAGE`
+
+If you have manually set `DS_MAJOR_VERSION` or `DS_ANALYZER_IMAGE` for specific reasons,
+and now must update your configuration to again get the latest patched versions of our
+analyzers, edit your `gitlab-ci.yml` file and either:
+
+- Set your `DS_MAJOR_VERSION` to match the latest version as seen in
+ [our current Dependency Scanning template](https://gitlab.com/gitlab-org/gitlab/blob/master/lib/gitlab/ci/templates/Security/Dependency-Scanning.gitlab-ci.yml#L18).
+- If you hardcoded the `DS_ANALYZER_IMAGE` variable directly, change it to match the latest
+ line as found in our [current Dependency Scanning template](https://gitlab.com/gitlab-org/gitlab/blob/master/lib/gitlab/ci/templates/Security/Dependency-Scanning.gitlab-ci.yml).
+ The line number will vary depending on which scanning job you edited.
+
+ For example, currently the `gemnasium-maven-dependency_scanning` job pulls the latest
+ `gemnasium-maven` Docker image because `DS_ANALYZER_IMAGE` is set to
+ `"$SECURE_ANALYZERS_PREFIX/gemnasium-maven:$DS_MAJOR_VERSION"`.
diff --git a/lib/gitlab/emoji.rb b/lib/gitlab/emoji.rb
index 519b1d94bf5..3c5d223b106 100644
--- a/lib/gitlab/emoji.rb
+++ b/lib/gitlab/emoji.rb
@@ -6,7 +6,7 @@ module Gitlab
# When updating emoji assets increase the version below
# and update the version number in `app/assets/javascripts/emoji/index.js`
- EMOJI_VERSION = 1
+ EMOJI_VERSION = 2
# Return a Pathname to emoji's current versioned folder
#
diff --git a/lib/gitlab/import_export/project/import_export.yml b/lib/gitlab/import_export/project/import_export.yml
index 8376417502d..ef146359da9 100644
--- a/lib/gitlab/import_export/project/import_export.yml
+++ b/lib/gitlab/import_export/project/import_export.yml
@@ -413,7 +413,6 @@ included_attributes:
- :b_mode
- :too_large
- :binary
- - :diff
metrics:
- :created_at
- :updated_at
@@ -797,6 +796,7 @@ excluded_attributes:
- :verification_checksum
- :verification_failure
merge_request_diff_files:
+ - :diff
- :external_diff_offset
- :external_diff_size
- :merge_request_diff_id
diff --git a/lib/gitlab/import_export/project/relation_factory.rb b/lib/gitlab/import_export/project/relation_factory.rb
index d84db92fe69..c391f86b47b 100644
--- a/lib/gitlab/import_export/project/relation_factory.rb
+++ b/lib/gitlab/import_export/project/relation_factory.rb
@@ -131,7 +131,9 @@ module Gitlab
end
def setup_diff
- @relation_hash['diff'] = @relation_hash.delete('utf8_diff')
+ diff = @relation_hash.delete('utf8_diff')
+
+ parsed_relation_hash['diff'] = diff
end
def setup_pipeline
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 180c3e85499..3e1ebdf0fa2 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -10444,15 +10444,6 @@ msgstr ""
msgid "Customize your pipeline configuration."
msgstr ""
-msgid "CustomizeHomepageBanner|Do you want to customize this page?"
-msgstr ""
-
-msgid "CustomizeHomepageBanner|Go to preferences"
-msgstr ""
-
-msgid "CustomizeHomepageBanner|This page shows a list of your projects by default but it can be changed to show projects' activity, groups, your to-do list, assigned issues, assigned merge requests, and more. You can change this under \"Homepage content\" in your preferences"
-msgstr ""
-
msgid "Cycle Time"
msgstr ""
@@ -11419,6 +11410,9 @@ msgstr ""
msgid "Delete variable"
msgstr ""
+msgid "DeleteProject|Failed to remove events. Please try again or contact administrator."
+msgstr ""
+
msgid "DeleteProject|Failed to remove project repository. Please try again or contact administrator."
msgstr ""
@@ -13902,9 +13896,6 @@ msgstr ""
msgid "Error while loading the merge request. Please try again."
msgstr ""
-msgid "Error while loading the project data. Please try again."
-msgstr ""
-
msgid "Error while migrating %{upload_id}: %{error_message}"
msgstr ""
@@ -17183,6 +17174,9 @@ msgstr ""
msgid "GroupsNew|%{linkStart}Groups%{linkEnd} allow you to manage and collaborate across multiple projects. Members of a group have access to all of its projects."
msgstr ""
+msgid "GroupsNew|Ask your administrator to %{enable_link_start}enable%{enable_link_end} Group Migration."
+msgstr ""
+
msgid "GroupsNew|Assemble related projects together and grant members access to several projects at once."
msgstr ""
@@ -17237,6 +17231,9 @@ msgstr ""
msgid "GroupsNew|Provide credentials for another instance of GitLab to import your groups directly."
msgstr ""
+msgid "GroupsNew|This feature is deprecated and replaced by %{docs_link_start}Group Migration%{docs_link_end}."
+msgstr ""
+
msgid "GroupsNew|To import a group, navigate to the group settings for the GitLab source instance, %{link_start}generate an export file%{link_end}, and upload it here."
msgstr ""
@@ -28336,9 +28333,6 @@ msgstr ""
msgid "Promotions|This feature is locked."
msgstr ""
-msgid "Promotions|Track activity with Contribution Analytics."
-msgstr ""
-
msgid "Promotions|Try it for free"
msgstr ""
@@ -28351,9 +28345,6 @@ msgstr ""
msgid "Promotions|Upgrade your plan to activate Audit Events."
msgstr ""
-msgid "Promotions|Upgrade your plan to activate Contribution Analytics."
-msgstr ""
-
msgid "Promotions|Upgrade your plan to activate Group Webhooks."
msgstr ""
@@ -28378,9 +28369,6 @@ msgstr ""
msgid "Promotions|When you have a lot of issues, it can be hard to get an overview. By adding a weight to your issues, you can get a better idea of the effort, cost, required time, or value of each, and so better manage them."
msgstr ""
-msgid "Promotions|With Contribution Analytics you can have an overview for the activity of issues, merge requests, and push events of your organization and its members."
-msgstr ""
-
msgid "Promotions|You can restrict access to protected branches by choosing a role (Maintainers, Developers) as well as certain users."
msgstr ""
@@ -39427,6 +39415,9 @@ msgstr ""
msgid "Webhooks|Tag push events"
msgstr ""
+msgid "Webhooks|The webhook was triggered more than %{limit} times per minute and is now disabled. To re-enable this webhook, fix the problems shown in %{strong_start}Recent events%{strong_end}, then re-test your settings. %{support_link_start}Contact Support%{support_link_end} if you need help re-enabling your webhook."
+msgstr ""
+
msgid "Webhooks|Trigger"
msgstr ""
@@ -39484,6 +39475,9 @@ msgstr ""
msgid "Webhooks|Use this token to validate received payloads. It is sent with the request in the X-Gitlab-Token HTTP header."
msgstr ""
+msgid "Webhooks|Webhook was automatically disabled"
+msgstr ""
+
msgid "Webhooks|Wiki page events"
msgstr ""
diff --git a/spec/controllers/root_controller_spec.rb b/spec/controllers/root_controller_spec.rb
index 38f8d267a2c..c6a8cee2f70 100644
--- a/spec/controllers/root_controller_spec.rb
+++ b/spec/controllers/root_controller_spec.rb
@@ -131,28 +131,10 @@ RSpec.describe RootController do
context 'who uses the default dashboard setting', :aggregate_failures do
render_views
- context 'with customize homepage banner' do
- it 'renders the default dashboard' do
- get :index
-
- expect(response).to render_template 'root/index'
- expect(response.body).to have_css('.js-customize-homepage-banner')
- end
- end
-
- context 'without customize homepage banner' do
- before do
- Users::DismissCalloutService.new(
- container: nil, current_user: user, params: { feature_name: Users::CalloutsHelper::CUSTOMIZE_HOMEPAGE }
- ).execute
- end
-
- it 'renders the default dashboard' do
- get :index
+ it 'renders the default dashboard' do
+ get :index
- expect(response).to render_template 'root/index'
- expect(response.body).not_to have_css('.js-customize-homepage-banner')
- end
+ expect(response).to render_template 'dashboard/projects/index'
end
end
end
diff --git a/spec/features/dashboard/root_spec.rb b/spec/features/dashboard/root_spec.rb
deleted file mode 100644
index 55bb43c6fcf..00000000000
--- a/spec/features/dashboard/root_spec.rb
+++ /dev/null
@@ -1,19 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe 'Root path' do
- let_it_be(:user) { create(:user) }
- let_it_be(:project) { create(:project) }
-
- before do
- project.add_developer(user)
- sign_in(user)
- end
-
- it 'shows the customize banner', :js do
- visit root_path
-
- expect(page).to have_content('Do you want to customize this page?')
- end
-end
diff --git a/spec/frontend/ide/components/ide_tree_list_spec.js b/spec/frontend/ide/components/ide_tree_list_spec.js
index 85d9feb0c09..ace51204374 100644
--- a/spec/frontend/ide/components/ide_tree_list_spec.js
+++ b/spec/frontend/ide/components/ide_tree_list_spec.js
@@ -38,9 +38,16 @@ describe('IDE tree list', () => {
beforeEach(() => {
bootstrapWithTree();
+ jest.spyOn(vm, '$emit').mockImplementation(() => {});
+
vm.$mount();
});
+ it('emits tree-ready event', () => {
+ expect(vm.$emit).toHaveBeenCalledTimes(1);
+ expect(vm.$emit).toHaveBeenCalledWith('tree-ready');
+ });
+
it('renders loading indicator', (done) => {
store.state.trees['abcproject/main'].loading = true;
@@ -61,9 +68,15 @@ describe('IDE tree list', () => {
beforeEach(() => {
bootstrapWithTree(emptyBranchTree);
+ jest.spyOn(vm, '$emit').mockImplementation(() => {});
+
vm.$mount();
});
+ it('still emits tree-ready event', () => {
+ expect(vm.$emit).toHaveBeenCalledWith('tree-ready');
+ });
+
it('does not load files if the branch is empty', () => {
expect(vm.$el.textContent).not.toContain('fileName');
expect(vm.$el.textContent).toContain('No files');
diff --git a/spec/frontend/ide/ide_router_spec.js b/spec/frontend/ide/ide_router_spec.js
index 3fb7781b176..cd10812f8ea 100644
--- a/spec/frontend/ide/ide_router_spec.js
+++ b/spec/frontend/ide/ide_router_spec.js
@@ -6,6 +6,7 @@ describe('IDE router', () => {
const PROJECT_NAMESPACE = 'my-group/sub-group';
const PROJECT_NAME = 'my-project';
const TEST_PATH = `/project/${PROJECT_NAMESPACE}/${PROJECT_NAME}/merge_requests/2`;
+ const DEFAULT_BRANCH = 'default-main';
let store;
let router;
@@ -13,34 +14,46 @@ describe('IDE router', () => {
beforeEach(() => {
window.history.replaceState({}, '', '/');
store = createStore();
- router = createRouter(store);
+ router = createRouter(store, DEFAULT_BRANCH);
jest.spyOn(store, 'dispatch').mockReturnValue(new Promise(() => {}));
});
- [
- `/project/${PROJECT_NAMESPACE}/${PROJECT_NAME}/tree/main/-/src/blob/`,
- `/project/${PROJECT_NAMESPACE}/${PROJECT_NAME}/tree/main/-/src/blob`,
- `/project/${PROJECT_NAMESPACE}/${PROJECT_NAME}/tree/blob/-/src/blob`,
- `/project/${PROJECT_NAMESPACE}/${PROJECT_NAME}/tree/main/-/src/tree/`,
- `/project/${PROJECT_NAMESPACE}/${PROJECT_NAME}/tree/weird:branch/name-123/-/src/tree/`,
- `/project/${PROJECT_NAMESPACE}/${PROJECT_NAME}/blob/main/-/src/blob`,
- `/project/${PROJECT_NAMESPACE}/${PROJECT_NAME}/blob/main/-/src/edit`,
- `/project/${PROJECT_NAMESPACE}/${PROJECT_NAME}/blob/main/-/src/merge_requests/2`,
- `/project/${PROJECT_NAMESPACE}/${PROJECT_NAME}/blob/blob/-/src/blob`,
- `/project/${PROJECT_NAMESPACE}/${PROJECT_NAME}/edit/blob/-/src/blob`,
- `/project/${PROJECT_NAMESPACE}/${PROJECT_NAME}/merge_requests/2`,
- `/project/${PROJECT_NAMESPACE}/${PROJECT_NAME}/tree/blob`,
- `/project/${PROJECT_NAMESPACE}/${PROJECT_NAME}/edit`,
- `/project/${PROJECT_NAMESPACE}/${PROJECT_NAME}`,
- ].forEach((route) => {
- it(`finds project path when route is "${route}"`, () => {
- router.push(route);
-
- expect(store.dispatch).toHaveBeenCalledWith('getProjectData', {
- namespace: PROJECT_NAMESPACE,
- projectId: PROJECT_NAME,
- });
+ it.each`
+ route | expectedBranchId | expectedBasePath
+ ${`/project/${PROJECT_NAMESPACE}/${PROJECT_NAME}/tree/main/-/src/blob/`} | ${'main'} | ${'src/blob/'}
+ ${`/project/${PROJECT_NAMESPACE}/${PROJECT_NAME}/tree/main/-/src/blob`} | ${'main'} | ${'src/blob'}
+ ${`/project/${PROJECT_NAMESPACE}/${PROJECT_NAME}/tree/blob/-/src/blob`} | ${'blob'} | ${'src/blob'}
+ ${`/project/${PROJECT_NAMESPACE}/${PROJECT_NAME}/tree/main/-/src/tree/`} | ${'main'} | ${'src/tree/'}
+ ${`/project/${PROJECT_NAMESPACE}/${PROJECT_NAME}/tree/weird:branch/name-123/-/src/tree/`} | ${'weird:branch/name-123'} | ${'src/tree/'}
+ ${`/project/${PROJECT_NAMESPACE}/${PROJECT_NAME}/blob/main/-/src/blob`} | ${'main'} | ${'src/blob'}
+ ${`/project/${PROJECT_NAMESPACE}/${PROJECT_NAME}/blob/main/-/src/edit`} | ${'main'} | ${'src/edit'}
+ ${`/project/${PROJECT_NAMESPACE}/${PROJECT_NAME}/blob/main/-/src/merge_requests/2`} | ${'main'} | ${'src/merge_requests/2'}
+ ${`/project/${PROJECT_NAMESPACE}/${PROJECT_NAME}/blob/blob/-/src/blob`} | ${'blob'} | ${'src/blob'}
+ ${`/project/${PROJECT_NAMESPACE}/${PROJECT_NAME}/edit/blob/-/src/blob`} | ${'blob'} | ${'src/blob'}
+ ${`/project/${PROJECT_NAMESPACE}/${PROJECT_NAME}/tree/blob`} | ${'blob'} | ${''}
+ ${`/project/${PROJECT_NAMESPACE}/${PROJECT_NAME}/edit`} | ${DEFAULT_BRANCH} | ${''}
+ ${`/project/${PROJECT_NAMESPACE}/${PROJECT_NAME}`} | ${DEFAULT_BRANCH} | ${''}
+ `('correctly opens Web IDE for $route', ({ route, expectedBranchId, expectedBasePath } = {}) => {
+ router.push(route);
+
+ expect(store.dispatch).toHaveBeenCalledWith('openBranch', {
+ projectId: `${PROJECT_NAMESPACE}/${PROJECT_NAME}`,
+ branchId: expectedBranchId,
+ basePath: expectedBasePath,
+ });
+ });
+
+ it('correctly opens an MR', () => {
+ const expectedId = '2';
+
+ router.push(`/project/${PROJECT_NAMESPACE}/${PROJECT_NAME}/merge_requests/${expectedId}`);
+
+ expect(store.dispatch).toHaveBeenCalledWith('openMergeRequest', {
+ projectId: `${PROJECT_NAMESPACE}/${PROJECT_NAME}`,
+ mergeRequestId: expectedId,
+ targetProjectId: undefined,
});
+ expect(store.dispatch).not.toHaveBeenCalledWith('openBranch');
});
it('keeps router in sync when store changes', async () => {
diff --git a/spec/frontend/ide/services/index_spec.js b/spec/frontend/ide/services/index_spec.js
index 3cf6240c2c5..0fab828dfb3 100644
--- a/spec/frontend/ide/services/index_spec.js
+++ b/spec/frontend/ide/services/index_spec.js
@@ -216,35 +216,6 @@ describe('IDE services', () => {
);
});
- describe('getProjectData', () => {
- it('combines gql and API requests', () => {
- const gqlProjectData = {
- id: 'gid://gitlab/Project/1',
- userPermissions: {
- bogus: true,
- },
- };
- const expectedResponse = {
- ...projectData,
- ...gqlProjectData,
- id: 1,
- };
- Api.project.mockReturnValue(Promise.resolve({ data: { ...projectData } }));
- query.mockReturnValue(Promise.resolve({ data: { project: gqlProjectData } }));
-
- return services.getProjectData(TEST_NAMESPACE, TEST_PROJECT).then((response) => {
- expect(response).toEqual({ data: expectedResponse });
- expect(Api.project).toHaveBeenCalledWith(TEST_PROJECT_ID);
- expect(query).toHaveBeenCalledWith({
- query: getIdeProject,
- variables: {
- projectPath: TEST_PROJECT_ID,
- },
- });
- });
- });
- });
-
describe('getFiles', () => {
let mock;
let relativeUrlRoot;
@@ -336,4 +307,38 @@ describe('IDE services', () => {
});
});
});
+
+ describe('getProjectPermissionsData', () => {
+ const TEST_PROJECT_PATH = 'foo/bar';
+
+ it('queries for the project permissions', () => {
+ const result = { data: { project: projectData } };
+ query.mockResolvedValue(result);
+
+ return services.getProjectPermissionsData(TEST_PROJECT_PATH).then((data) => {
+ expect(data).toEqual(result.data.project);
+ expect(query).toHaveBeenCalledWith(
+ expect.objectContaining({
+ query: getIdeProject,
+ variables: { projectPath: TEST_PROJECT_PATH },
+ }),
+ );
+ });
+ });
+
+ it('converts the returned GraphQL id to the regular ID number', () => {
+ const projectId = 2;
+ const gqlProjectData = {
+ id: `gid://gitlab/Project/${projectId}`,
+ userPermissions: {
+ bogus: true,
+ },
+ };
+
+ query.mockResolvedValue({ data: { project: gqlProjectData } });
+ return services.getProjectPermissionsData(TEST_PROJECT_PATH).then((data) => {
+ expect(data.id).toBe(projectId);
+ });
+ });
+ });
});
diff --git a/spec/frontend/ide/stores/actions/project_spec.js b/spec/frontend/ide/stores/actions/project_spec.js
index ca6f7169059..e07dcf22860 100644
--- a/spec/frontend/ide/stores/actions/project_spec.js
+++ b/spec/frontend/ide/stores/actions/project_spec.js
@@ -2,9 +2,12 @@ import MockAdapter from 'axios-mock-adapter';
import { useMockLocationHelper } from 'helpers/mock_window_location_helper';
import testAction from 'helpers/vuex_action_helper';
import api from '~/api';
+import createFlash from '~/flash';
import service from '~/ide/services';
import { createStore } from '~/ide/stores';
import {
+ setProject,
+ fetchProjectPermissions,
refreshLastCommitData,
showBranchNotFoundError,
createNewBranchFromDefault,
@@ -13,8 +16,12 @@ import {
loadFile,
loadBranch,
} from '~/ide/stores/actions';
+import { logError } from '~/lib/logger';
import axios from '~/lib/utils/axios_utils';
+jest.mock('~/flash');
+jest.mock('~/lib/logger');
+
const TEST_PROJECT_ID = 'abc/def';
describe('IDE store project actions', () => {
@@ -34,6 +41,92 @@ describe('IDE store project actions', () => {
mock.restore();
});
+ describe('setProject', () => {
+ const project = { id: 'foo', path_with_namespace: TEST_PROJECT_ID };
+ const baseMutations = [
+ {
+ type: 'SET_PROJECT',
+ payload: {
+ projectPath: TEST_PROJECT_ID,
+ project,
+ },
+ },
+ {
+ type: 'SET_CURRENT_PROJECT',
+ payload: TEST_PROJECT_ID,
+ },
+ ];
+
+ it.each`
+ desc | payload | expectedMutations
+ ${'does not commit any action if project is not passed'} | ${undefined} | ${[]}
+ ${'commits correct actions in the correct order by default'} | ${{ project }} | ${[...baseMutations]}
+ `('$desc', async ({ payload, expectedMutations } = {}) => {
+ await testAction({
+ action: setProject,
+ payload,
+ state: store.state,
+ expectedMutations,
+ expectedActions: [],
+ });
+ });
+ });
+
+ describe('fetchProjectPermissions', () => {
+ const permissionsData = {
+ userPermissions: {
+ bogus: true,
+ },
+ };
+ const permissionsMutations = [
+ {
+ type: 'UPDATE_PROJECT',
+ payload: {
+ projectPath: TEST_PROJECT_ID,
+ props: {
+ ...permissionsData,
+ },
+ },
+ },
+ ];
+
+ let spy;
+
+ beforeEach(() => {
+ spy = jest.spyOn(service, 'getProjectPermissionsData');
+ });
+
+ afterEach(() => {
+ createFlash.mockRestore();
+ });
+
+ it.each`
+ desc | projectPath | responseSuccess | expectedMutations
+ ${'does not fetch permissions if project does not exist'} | ${undefined} | ${true} | ${[]}
+ ${'fetches permission when project is specified'} | ${TEST_PROJECT_ID} | ${true} | ${[...permissionsMutations]}
+ ${'flashes an error if the request fails'} | ${TEST_PROJECT_ID} | ${false} | ${[]}
+ `('$desc', async ({ projectPath, expectedMutations, responseSuccess } = {}) => {
+ store.state.currentProjectId = projectPath;
+ if (responseSuccess) {
+ spy.mockResolvedValue(permissionsData);
+ } else {
+ spy.mockRejectedValue();
+ }
+
+ await testAction({
+ action: fetchProjectPermissions,
+ state: store.state,
+ expectedMutations,
+ expectedActions: [],
+ });
+
+ if (!responseSuccess) {
+ expect(logError).toHaveBeenCalled();
+ expect(createFlash).toHaveBeenCalled();
+ }
+ });
+ });
+
describe('refreshLastCommitData', () => {
beforeEach(() => {
store.state.currentProjectId = 'abc/def';
diff --git a/spec/frontend/ide/stores/mutations/project_spec.js b/spec/frontend/ide/stores/mutations/project_spec.js
index b3ce39c33d2..0fdd7798f00 100644
--- a/spec/frontend/ide/stores/mutations/project_spec.js
+++ b/spec/frontend/ide/stores/mutations/project_spec.js
@@ -3,21 +3,48 @@ import state from '~/ide/stores/state';
describe('Multi-file store branch mutations', () => {
let localState;
+ const nonExistentProj = 'nonexistent';
+ const existingProj = 'abcproject';
beforeEach(() => {
localState = state();
- localState.projects = { abcproject: { empty_repo: true } };
+ localState.projects = { [existingProj]: { empty_repo: true } };
});
describe('TOGGLE_EMPTY_STATE', () => {
it('sets empty_repo for project to passed value', () => {
- mutations.TOGGLE_EMPTY_STATE(localState, { projectPath: 'abcproject', value: false });
+ mutations.TOGGLE_EMPTY_STATE(localState, { projectPath: existingProj, value: false });
- expect(localState.projects.abcproject.empty_repo).toBe(false);
+ expect(localState.projects[existingProj].empty_repo).toBe(false);
- mutations.TOGGLE_EMPTY_STATE(localState, { projectPath: 'abcproject', value: true });
+ mutations.TOGGLE_EMPTY_STATE(localState, { projectPath: existingProj, value: true });
- expect(localState.projects.abcproject.empty_repo).toBe(true);
+ expect(localState.projects[existingProj].empty_repo).toBe(true);
+ });
+ });
+
+ describe('UPDATE_PROJECT', () => {
+ it.each`
+ desc | projectPath | props | expectedProps
+ ${'extends existing project with the passed props'} | ${existingProj} | ${{ foo1: 'bar' }} | ${{ foo1: 'bar' }}
+ ${'overrides existing props on the exsiting project'} | ${existingProj} | ${{ empty_repo: false }} | ${{ empty_repo: false }}
+ ${'does nothing if the project does not exist'} | ${nonExistentProj} | ${{ foo2: 'bar' }} | ${undefined}
+ ${'does nothing if project is not passed'} | ${undefined} | ${{ foo3: 'bar' }} | ${undefined}
+ ${'does nothing if the props are not passed'} | ${existingProj} | ${undefined} | ${{}}
+ ${'does nothing if the props are empty'} | ${existingProj} | ${{}} | ${{}}
+ `('$desc', ({ projectPath, props, expectedProps } = {}) => {
+ const origProject = localState.projects[projectPath];
+
+ mutations.UPDATE_PROJECT(localState, { projectPath, props });
+
+ if (!expectedProps) {
+ expect(localState.projects[projectPath]).toBeUndefined();
+ } else {
+ expect(localState.projects[projectPath]).toEqual({
+ ...origProject,
+ ...expectedProps,
+ });
+ }
});
});
});
diff --git a/spec/frontend/pages/dashboard/projects/index/components/customize_homepage_banner_spec.js b/spec/frontend/pages/dashboard/projects/index/components/customize_homepage_banner_spec.js
deleted file mode 100644
index f84800d8266..00000000000
--- a/spec/frontend/pages/dashboard/projects/index/components/customize_homepage_banner_spec.js
+++ /dev/null
@@ -1,108 +0,0 @@
-import { GlBanner } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
-import MockAdapter from 'axios-mock-adapter';
-import { mockTracking, unmockTracking, triggerEvent } from 'helpers/tracking_helper';
-import axios from '~/lib/utils/axios_utils';
-import CustomizeHomepageBanner from '~/pages/dashboard/projects/index/components/customize_homepage_banner.vue';
-
-const svgPath = '/illustrations/background';
-const provide = {
- svgPath,
- preferencesBehaviorPath: 'some/behavior/path',
- calloutsPath: 'call/out/path',
- calloutsFeatureId: 'some-feature-id',
- trackLabel: 'home_page',
-};
-
-const createComponent = () => {
- return shallowMount(CustomizeHomepageBanner, { provide, stubs: { GlBanner } });
-};
-
-describe('CustomizeHomepageBanner', () => {
- let trackingSpy;
- let mockAxios;
- let wrapper;
-
- beforeEach(() => {
- mockAxios = new MockAdapter(axios);
- document.body.dataset.page = 'some:page';
- trackingSpy = mockTracking('_category_', undefined, jest.spyOn);
- wrapper = createComponent();
- });
-
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- mockAxios.restore();
- unmockTracking();
- });
-
- it('should render the banner when not dismissed', () => {
- expect(wrapper.find(GlBanner).exists()).toBe(true);
- });
-
- it('should close the banner when dismiss is clicked', async () => {
- mockAxios.onPost(provide.calloutsPath).replyOnce(200);
- expect(wrapper.find(GlBanner).exists()).toBe(true);
- wrapper.find(GlBanner).vm.$emit('close');
-
- await wrapper.vm.$nextTick();
- expect(wrapper.find(GlBanner).exists()).toBe(false);
- });
-
- it('includes the body text from options', () => {
- expect(wrapper.html()).toContain(wrapper.vm.$options.i18n.body);
- });
-
- describe('tracking', () => {
- const preferencesTrackingEvent = 'click_go_to_preferences';
- const mockTrackingOnWrapper = () => {
- unmockTracking();
- trackingSpy = mockTracking('_category_', wrapper.element, jest.spyOn);
- };
-
- it('sets the needed data attributes for tracking button', async () => {
- await wrapper.vm.$nextTick();
- const button = wrapper.find(`[href='${wrapper.vm.preferencesBehaviorPath}']`);
-
- expect(button.attributes('data-track-action')).toEqual(preferencesTrackingEvent);
- expect(button.attributes('data-track-label')).toEqual(provide.trackLabel);
- });
-
- it('sends a tracking event when the banner is shown', () => {
- const trackCategory = undefined;
- const trackEvent = 'show_home_page_banner';
-
- expect(trackingSpy).toHaveBeenCalledWith(trackCategory, trackEvent, {
- label: provide.trackLabel,
- });
- });
-
- it('sends a tracking event when the banner is dismissed', async () => {
- mockTrackingOnWrapper();
- mockAxios.onPost(provide.calloutsPath).replyOnce(200);
- const trackCategory = undefined;
- const trackEvent = 'click_dismiss';
-
- wrapper.find(GlBanner).vm.$emit('close');
-
- await wrapper.vm.$nextTick();
- expect(trackingSpy).toHaveBeenCalledWith(trackCategory, trackEvent, {
- label: provide.trackLabel,
- });
- });
-
- it('sends a tracking event when the button is clicked', async () => {
- mockTrackingOnWrapper();
- mockAxios.onPost(provide.calloutsPath).replyOnce(200);
- const button = wrapper.find(`[href='${wrapper.vm.preferencesBehaviorPath}']`);
-
- triggerEvent(button.element);
-
- await wrapper.vm.$nextTick();
- expect(trackingSpy).toHaveBeenCalledWith('_category_', preferencesTrackingEvent, {
- label: provide.trackLabel,
- });
- });
- });
-});
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/mock_data.js b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/mock_data.js
index 1faa3b0af1d..884bc4684ba 100644
--- a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/mock_data.js
+++ b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/mock_data.js
@@ -75,7 +75,7 @@ export const mockSuggestedColors = {
'#013220': 'Dark green',
'#6699cc': 'Blue-gray',
'#0000ff': 'Blue',
- '#e6e6fa': 'Lavendar',
+ '#e6e6fa': 'Lavender',
'#9400d3': 'Dark violet',
'#330066': 'Deep violet',
'#808080': 'Gray',
diff --git a/spec/frontend_integration/ide/helpers/ide_helper.js b/spec/frontend_integration/ide/helpers/ide_helper.js
index 56b2e298aa3..54a522324f5 100644
--- a/spec/frontend_integration/ide/helpers/ide_helper.js
+++ b/spec/frontend_integration/ide/helpers/ide_helper.js
@@ -192,6 +192,13 @@ export const commit = async ({ newBranch = false, newMR = false, newBranchName =
switchLeftSidebarTab('Commit');
screen.getByTestId('begin-commit-button').click();
+ await waitForMonacoEditor();
+
+ const mrCheck = await screen.findByLabelText('Start a new merge request');
+ if (Boolean(mrCheck.checked) !== newMR) {
+ mrCheck.click();
+ }
+
if (!newBranch) {
const option = await screen.findByLabelText(/Commit to .+ branch/);
option.click();
@@ -201,12 +208,9 @@ export const commit = async ({ newBranch = false, newMR = false, newBranchName =
const branchNameInput = await screen.findByTestId('ide-new-branch-name');
fireEvent.input(branchNameInput, { target: { value: newBranchName } });
-
- const mrCheck = await screen.findByLabelText('Start a new merge request');
- if (Boolean(mrCheck.checked) !== newMR) {
- mrCheck.click();
- }
}
screen.getByText('Commit').click();
+
+ await waitForMonacoEditor();
};
diff --git a/spec/frontend_integration/ide/helpers/start.js b/spec/frontend_integration/ide/helpers/start.js
index 4451c1ee946..3c5ed9dfe20 100644
--- a/spec/frontend_integration/ide/helpers/start.js
+++ b/spec/frontend_integration/ide/helpers/start.js
@@ -4,16 +4,18 @@ import setWindowLocation from 'helpers/set_window_location_helper';
import { TEST_HOST } from 'helpers/test_constants';
import { initIde } from '~/ide';
import extendStore from '~/ide/stores/extend';
+import { getProject, getEmptyProject } from 'jest/../frontend_integration/test_helpers/fixtures';
import { IDE_DATASET } from './mock_data';
export default (container, { isRepoEmpty = false, path = '', mrId = '' } = {}) => {
const projectName = isRepoEmpty ? 'lorem-ipsum-empty' : 'lorem-ipsum';
const pathSuffix = mrId ? `merge_requests/${mrId}` : `tree/master/-/${path}`;
+ const project = isRepoEmpty ? getEmptyProject() : getProject();
setWindowLocation(`${TEST_HOST}/-/ide/project/gitlab-test/${projectName}/${pathSuffix}`);
const el = document.createElement('div');
- Object.assign(el.dataset, IDE_DATASET);
+ Object.assign(el.dataset, IDE_DATASET, { project: JSON.stringify(project) });
container.appendChild(el);
const vm = initIde(el, { extendStore });
diff --git a/spec/frontend_integration/ide/user_opens_ide_spec.js b/spec/frontend_integration/ide/user_opens_ide_spec.js
index f56cd008d1c..c9d78d1de8f 100644
--- a/spec/frontend_integration/ide/user_opens_ide_spec.js
+++ b/spec/frontend_integration/ide/user_opens_ide_spec.js
@@ -34,10 +34,10 @@ describe('IDE: User opens IDE', () => {
expect(await screen.findByText('No files')).toBeDefined();
});
- it('shows a "New file" button', async () => {
- const button = await screen.findByTitle('New file');
+ it('shows a "New file" button', () => {
+ const buttons = screen.queryAllByTitle('New file');
- expect(button.tagName).toEqual('BUTTON');
+ expect(buttons.map((x) => x.tagName)).toContain('BUTTON');
});
});
diff --git a/spec/helpers/ide_helper_spec.rb b/spec/helpers/ide_helper_spec.rb
index a06c9ec6699..dc0a234f981 100644
--- a/spec/helpers/ide_helper_spec.rb
+++ b/spec/helpers/ide_helper_spec.rb
@@ -34,7 +34,7 @@ RSpec.describe IdeHelper do
self.instance_variable_set(:@fork_info, fork_info)
self.instance_variable_set(:@project, project)
- serialized_project = API::Entities::Project.represent(project).to_json
+ serialized_project = API::Entities::Project.represent(project, current_user: project.creator).to_json
expect(helper.ide_data)
.to include(
diff --git a/spec/helpers/users/callouts_helper_spec.rb b/spec/helpers/users/callouts_helper_spec.rb
index ba4d8797a24..85e11c2ed3b 100644
--- a/spec/helpers/users/callouts_helper_spec.rb
+++ b/spec/helpers/users/callouts_helper_spec.rb
@@ -61,36 +61,6 @@ RSpec.describe Users::CalloutsHelper do
end
end
- describe '.show_customize_homepage_banner?' do
- subject { helper.show_customize_homepage_banner? }
-
- context 'when user has not dismissed' do
- before do
- allow(helper).to receive(:user_dismissed?).with(described_class::CUSTOMIZE_HOMEPAGE) { false }
- end
-
- context 'when user is on the default dashboard' do
- it { is_expected.to be true }
- end
-
- context 'when user is not on the default dashboard' do
- before do
- user.dashboard = 'stars'
- end
-
- it { is_expected.to be false }
- end
- end
-
- context 'when user dismissed' do
- before do
- allow(helper).to receive(:user_dismissed?).with(described_class::CUSTOMIZE_HOMEPAGE) { true }
- end
-
- it { is_expected.to be false }
- end
- end
-
describe '.render_flash_user_callout' do
it 'renders the flash_user_callout partial' do
expect(helper).to receive(:render)
diff --git a/spec/lib/gitlab/import_export/safe_model_attributes.yml b/spec/lib/gitlab/import_export/safe_model_attributes.yml
index 0fa3756ff3a..6ffe2187466 100644
--- a/spec/lib/gitlab/import_export/safe_model_attributes.yml
+++ b/spec/lib/gitlab/import_export/safe_model_attributes.yml
@@ -253,7 +253,6 @@ MergeRequestDiffFile:
- b_mode
- too_large
- binary
-- diff
MergeRequestContextCommit:
- id
- authored_date
diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb
index 3f9c3bc6858..19e09c99a15 100644
--- a/spec/models/user_spec.rb
+++ b/spec/models/user_spec.rb
@@ -6138,20 +6138,6 @@ RSpec.describe User do
end
end
- describe '#default_dashboard?' do
- it 'is the default dashboard' do
- user = build(:user)
-
- expect(user.default_dashboard?).to be true
- end
-
- it 'is not the default dashboard' do
- user = build(:user, dashboard: 'stars')
-
- expect(user.default_dashboard?).to be false
- end
- end
-
describe '.dormant' do
it 'returns dormant users' do
freeze_time do
diff --git a/spec/services/events/destroy_service_spec.rb b/spec/services/events/destroy_service_spec.rb
new file mode 100644
index 00000000000..8dcbb83eb1d
--- /dev/null
+++ b/spec/services/events/destroy_service_spec.rb
@@ -0,0 +1,50 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Events::DestroyService do
+ subject(:service) { described_class.new(project) }
+
+ let_it_be(:project, reload: true) { create(:project, :repository) }
+ let_it_be(:another_project) { create(:project) }
+ let_it_be(:merge_request) { create(:merge_request, source_project: project) }
+ let_it_be(:user) { create(:user) }
+
+ let!(:unrelated_event) { create(:event, :merged, project: another_project, target: another_project, author: user) }
+
+ before do
+ create(:event, :created, project: project, target: project, author: user)
+ create(:event, :created, project: project, target: merge_request, author: user)
+ create(:event, :merged, project: project, target: merge_request, author: user)
+ end
+
+ let(:events) { project.events }
+
+ describe '#execute', :aggregate_failures do
+ it 'deletes the events' do
+ response = nil
+
+ expect { response = subject.execute }.to change(Event, :count).by(-3)
+
+ expect(response).to be_success
+ expect(unrelated_event.reload).to be_present
+ end
+
+ context 'when an error is raised while deleting the records' do
+ before do
+ allow(project).to receive_message_chain(:events, :all, :delete_all).and_raise(ActiveRecord::ActiveRecordError)
+ end
+
+ it 'returns error' do
+ response = subject.execute
+
+ expect(response).to be_error
+ expect(response.message).to eq 'Failed to remove events.'
+ end
+
+ it 'does not delete events' do
+ expect { subject.execute }.not_to change(Event, :count)
+ end
+ end
+ end
+end
diff --git a/spec/services/projects/destroy_service_spec.rb b/spec/services/projects/destroy_service_spec.rb
index ac84614121a..18bcfe3f3b4 100644
--- a/spec/services/projects/destroy_service_spec.rb
+++ b/spec/services/projects/destroy_service_spec.rb
@@ -545,6 +545,27 @@ RSpec.describe Projects::DestroyService, :aggregate_failures do
end
end
+ context 'when project has events' do
+ let!(:event) { create(:event, :created, project: project, target: project, author: user) }
+
+ it 'deletes events from the project' do
+ expect do
+ destroy_project(project, user)
+ end.to change(Event, :count).by(-1)
+ end
+
+ context 'when an error is returned while deleting events' do
+ it 'does not delete project' do
+ allow_next_instance_of(Events::DestroyService) do |instance|
+ allow(instance).to receive(:execute).and_return(ServiceResponse.error(message: 'foo'))
+ end
+
+ expect(destroy_project(project, user)).to be_falsey
+ expect(project.delete_error).to include('Failed to remove events')
+ end
+ end
+ end
+
context 'error while destroying', :sidekiq_inline do
let!(:pipeline) { create(:ci_pipeline, project: project) }
let!(:builds) { create_list(:ci_build, 2, :artifacts, pipeline: pipeline) }
diff --git a/spec/views/projects/hooks/edit.html.haml_spec.rb b/spec/views/projects/hooks/edit.html.haml_spec.rb
new file mode 100644
index 00000000000..1265334a572
--- /dev/null
+++ b/spec/views/projects/hooks/edit.html.haml_spec.rb
@@ -0,0 +1,33 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'projects/hooks/edit' do
+ let(:hook) { create(:project_hook, project: project) }
+
+ let_it_be_with_refind(:project) { create(:project) }
+
+ before do
+ assign :project, project
+ assign :hook, hook
+ end
+
+ it 'renders webhook page with "Recent events"' do
+ render
+
+ expect(rendered).to have_css('h4', text: _('Webhook'))
+ expect(rendered).to have_text(_('Recent events'))
+ end
+
+ context 'webhook is rate limited' do
+ before do
+ allow(hook).to receive(:rate_limited?).and_return(true)
+ end
+
+ it 'renders alert' do
+ render
+
+ expect(rendered).to have_text(s_('Webhooks|Webhook was automatically disabled'))
+ end
+ end
+end
diff --git a/spec/views/projects/hooks/index.html.haml_spec.rb b/spec/views/projects/hooks/index.html.haml_spec.rb
new file mode 100644
index 00000000000..eb2b7334b98
--- /dev/null
+++ b/spec/views/projects/hooks/index.html.haml_spec.rb
@@ -0,0 +1,36 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'projects/hooks/index' do
+ let(:existing_hook) { create(:project_hook, project: project) }
+ let(:new_hook) { ProjectHook.new }
+
+ let_it_be_with_refind(:project) { create(:project) }
+
+ before do
+ assign :project, project
+ assign :hooks, [existing_hook]
+ assign :hook, new_hook
+ end
+
+ it 'renders webhooks page with "Project Hooks"' do
+ render
+
+ expect(rendered).to have_css('h4', text: _('Webhooks'))
+ expect(rendered).to have_text('Project Hooks')
+ expect(rendered).not_to have_css('.gl-badge', text: _('Disabled'))
+ end
+
+ context 'webhook is rate limited' do
+ before do
+ allow(existing_hook).to receive(:rate_limited?).and_return(true)
+ end
+
+ it 'renders "Disabled" badge' do
+ render
+
+ expect(rendered).to have_css('.gl-badge', text: _('Disabled'))
+ end
+ end
+end