Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2021-12-15 21:11:32 +00:00
parent 22baaecaa8
commit 1c898dc5c1
51 changed files with 585 additions and 536 deletions

View File

@ -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();

View File

@ -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 });
},

View File

@ -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();

View File

@ -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.

View File

@ -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),
}));
},
};

View File

@ -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

View File

@ -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

View File

@ -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]);
});
},
};

View File

@ -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>

View File

@ -1,5 +1,3 @@
import ProjectsList from '~/projects_list';
import initCustomizeHomepageBanner from './init_customize_homepage_banner';
new ProjectsList(); // eslint-disable-line no-new
initCustomizeHomepageBanner();

View File

@ -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),
});
};

View File

@ -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?

View File

@ -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

View File

@ -844,10 +844,6 @@ class User < ApplicationRecord
# Instance methods
#
def default_dashboard?
dashboard == self.class.column_defaults['dashboard']
end
def full_path
username
end

View File

@ -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

View File

@ -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

View File

@ -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!

View File

@ -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,

View File

@ -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

View File

@ -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}" }

View File

@ -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

View File

@ -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'

View File

@ -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)

View File

@ -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

View File

@ -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. |

View File

@ -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 -->

View File

@ -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"`.

View File

@ -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
#

View File

@ -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

View File

@ -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

View File

@ -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 ""

View File

@ -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
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
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

View File

@ -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

View File

@ -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');

View File

@ -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,36 +14,48 @@ 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);
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('getProjectData', {
namespace: PROJECT_NAMESPACE,
projectId: PROJECT_NAME,
});
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 () => {
expect(router.currentRoute.fullPath).toBe('/');

View File

@ -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);
});
});
});
});

View File

@ -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';

View File

@ -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,
});
}
});
});
});

View File

@ -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,
});
});
});
});

View File

@ -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',

View File

@ -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();
};

View File

@ -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 });

View File

@ -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');
});
});

View File

@ -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(

View File

@ -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)

View File

@ -253,7 +253,6 @@ MergeRequestDiffFile:
- b_mode
- too_large
- binary
- diff
MergeRequestContextCommit:
- id
- authored_date

View File

@ -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

View File

@ -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

View File

@ -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) }

View File

@ -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

View File

@ -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