提交 d9e07a15 编写于 作者: G GitLab Bot

Add latest changes from gitlab-org/gitlab@master

上级 f10eb9eb
......@@ -7,6 +7,7 @@
"ul-style": {
"style": "dash"
},
"no-trailing-spaces": false,
"line-length": false,
"no-duplicate-header": {
"allow_different_nesting": true
......
/* eslint-disable @gitlab/require-i18n-strings */
import { groupBy } from 'lodash';
import createFlash from '~/flash';
import { extractCurrentDiscussion, extractDesign } from './design_management_utils';
import {
......@@ -159,13 +160,11 @@ const updateImageDiffNoteInStore = (store, updateImageDiffNote, query, variables
const addNewDesignToStore = (store, designManagementUpload, query) => {
const data = store.readQuery(query);
const newDesigns = data.project.issue.designCollection.designs.nodes.reduce((acc, design) => {
if (!acc.find(d => d.filename === design.filename)) {
acc.push(design);
}
return acc;
}, designManagementUpload.designs);
const currentDesigns = data.project.issue.designCollection.designs.nodes;
const existingDesigns = groupBy(currentDesigns, 'filename');
const newDesigns = currentDesigns.concat(
designManagementUpload.designs.filter(d => !existingDesigns[d.filename]),
);
let newVersionNode;
const findNewVersions = designManagementUpload.designs.find(design => design.versions);
......
......@@ -147,7 +147,7 @@ export default {
slot="image-overlay"
:discussions="imageDiscussions"
:file-hash="diffFileHash"
:can-comment="getNoteableData.current_user.can_create_note"
:can-comment="getNoteableData.current_user.can_create_note && !diffFile.brokenSymlink"
/>
<div v-if="showNotesContainer" class="note-container">
<user-avatar-link
......
......@@ -167,6 +167,7 @@ export default {
:id="file.file_hash"
:class="{
'is-active': currentDiffFileId === file.file_hash,
'comments-disabled': Boolean(file.brokenSymlink),
}"
:data-path="file.new_path"
class="diff-file file-holder"
......
<script>
import { mapGetters, mapActions } from 'vuex';
import { GlIcon } from '@gitlab/ui';
import { GlIcon, GlTooltipDirective } from '@gitlab/ui';
import { getParameterByName, parseBoolean } from '~/lib/utils/common_utils';
import DiffGutterAvatars from './diff_gutter_avatars.vue';
import { __ } from '~/locale';
import {
CONTEXT_LINE_TYPE,
LINE_POSITION_RIGHT,
......@@ -18,6 +19,9 @@ export default {
DiffGutterAvatars,
GlIcon,
},
directives: {
GlTooltip: GlTooltipDirective,
},
props: {
line: {
type: Object,
......@@ -123,6 +127,24 @@ export default {
lineNumber() {
return this.lineType === OLD_LINE_TYPE ? this.line.old_line : this.line.new_line;
},
addCommentTooltip() {
const brokenSymlinks = this.line.commentsDisabled;
let tooltip = __('Add a comment to this line');
if (brokenSymlinks) {
if (brokenSymlinks.wasSymbolic || brokenSymlinks.isSymbolic) {
tooltip = __(
'Commenting on symbolic links that replace or are replaced by files is currently not supported.',
);
} else if (brokenSymlinks.wasReal || brokenSymlinks.isReal) {
tooltip = __(
'Commenting on files that replace or are replaced by symbolic links is currently not supported.',
);
}
}
return tooltip;
},
},
mounted() {
this.unwatchShouldShowCommentButton = this.$watch('shouldShowCommentButton', newVal => {
......@@ -146,17 +168,24 @@ export default {
<template>
<td ref="td" :class="classNameMap">
<button
v-if="shouldRenderCommentButton"
v-show="shouldShowCommentButton"
ref="addDiffNoteButton"
type="button"
class="add-diff-note js-add-diff-note-button qa-diff-comment"
title="Add a comment to this line"
@click="handleCommentButton"
<span
ref="addNoteTooltip"
v-gl-tooltip
class="add-diff-note tooltip-wrapper"
:title="addCommentTooltip"
>
<gl-icon :size="12" name="comment" />
</button>
<button
v-if="shouldRenderCommentButton"
v-show="shouldShowCommentButton"
ref="addDiffNoteButton"
type="button"
class="add-diff-note note-button js-add-diff-note-button qa-diff-comment"
:disabled="line.commentsDisabled"
@click="handleCommentButton"
>
<gl-icon :size="12" name="comment" />
</button>
</span>
<a
v-if="lineNumber"
ref="lineNumberRef"
......
......@@ -480,6 +480,10 @@ export function getDiffPositionByLineCode(diffFiles, useSingleDiffStyle) {
// This method will check whether the discussion is still applicable
// to the diff line in question regarding different versions of the MR
export function isDiscussionApplicableToLine({ discussion, diffPosition, latestDiff }) {
if (!diffPosition) {
return false;
}
const { line_code, ...dp } = diffPosition;
// Removing `line_range` from diffPosition because the backend does not
// yet consistently return this property. This check can be removed,
......
import VisualTokenValue from './visual_token_value';
import { objectToQueryString } from '~/lib/utils/common_utils';
import { objectToQueryString, spriteIcon } from '~/lib/utils/common_utils';
import FilteredSearchContainer from './container';
export default class FilteredSearchVisualTokens {
......@@ -84,7 +84,7 @@ export default class FilteredSearchVisualTokens {
<div class="value-container">
<div class="${capitalizeTokenValue ? 'text-capitalize' : ''} value"></div>
<div class="remove-token" role="button">
<i class="fa fa-close"></i>
${spriteIcon('close', 's16 close-icon')}
</div>
</div>
</div>
......
<script>
import { GlIcon } from '@gitlab/ui';
import TimeTrackingHelpState from './help_state.vue';
import TimeTrackingCollapsedState from './collapsed_state.vue';
import TimeTrackingSpentOnlyPane from './spent_only_pane.vue';
......@@ -11,6 +12,7 @@ import eventHub from '../../event_hub';
export default {
name: 'IssuableTimeTracker',
components: {
GlIcon,
TimeTrackingCollapsedState,
TimeTrackingEstimateOnlyPane,
TimeTrackingSpentOnlyPane,
......@@ -111,7 +113,7 @@ export default {
class="close-help-button float-right"
@click="toggleHelpState(false)"
>
<i class="fa fa-close" aria-hidden="true"> </i>
<gl-icon name="close" />
</div>
</div>
<div class="time-tracking-content hide-collapsed">
......
import { __ } from '~/locale';
import { generateToolbarItem } from './services/editor_service';
import buildCustomHTMLRenderer from './services/build_custom_renderer';
export const CUSTOM_EVENTS = {
openAddImageModal: 'gl_openAddImageModal',
};
/* eslint-disable @gitlab/require-i18n-strings */
const TOOLBAR_ITEM_CONFIGS = [
export const TOOLBAR_ITEM_CONFIGS = [
{ icon: 'heading', event: 'openHeadingSelect', classes: 'tui-heading', tooltip: __('Headings') },
{ icon: 'bold', command: 'Bold', tooltip: __('Add bold text') },
{ icon: 'italic', command: 'Italic', tooltip: __('Add italic text') },
......@@ -30,11 +28,6 @@ const TOOLBAR_ITEM_CONFIGS = [
{ icon: 'doc-code', command: 'CodeBlock', tooltip: __('Insert a code block') },
];
export const EDITOR_OPTIONS = {
toolbarItems: TOOLBAR_ITEM_CONFIGS.map(config => generateToolbarItem(config)),
customHTMLRenderer: buildCustomHTMLRenderer(),
};
export const EDITOR_TYPES = {
markdown: 'markdown',
wysiwyg: 'wysiwyg',
......
......@@ -3,16 +3,11 @@ import 'codemirror/lib/codemirror.css';
import '@toast-ui/editor/dist/toastui-editor.css';
import AddImageModal from './modals/add_image/add_image_modal.vue';
import {
EDITOR_OPTIONS,
EDITOR_TYPES,
EDITOR_HEIGHT,
EDITOR_PREVIEW_STYLE,
CUSTOM_EVENTS,
} from './constants';
import { EDITOR_TYPES, EDITOR_HEIGHT, EDITOR_PREVIEW_STYLE, CUSTOM_EVENTS } from './constants';
import {
registerHTMLToMarkdownRenderer,
getEditorOptions,
addCustomEventListener,
removeCustomEventListener,
addImage,
......@@ -35,7 +30,7 @@ export default {
options: {
type: Object,
required: false,
default: () => EDITOR_OPTIONS,
default: () => null,
},
initialEditType: {
type: String,
......@@ -65,13 +60,13 @@ export default {
};
},
computed: {
editorOptions() {
return { ...EDITOR_OPTIONS, ...this.options };
},
editorInstance() {
return this.$refs.editor;
},
},
created() {
this.editorOptions = getEditorOptions(this.options);
},
beforeDestroy() {
this.removeListeners();
},
......
import { union, mapValues } from 'lodash';
import renderBlockHtml from './renderers/render_html_block';
import renderKramdownList from './renderers/render_kramdown_list';
import renderKramdownText from './renderers/render_kramdown_text';
......@@ -19,63 +20,20 @@ const executeRenderer = (renderers, node, context) => {
return availableRenderer ? availableRenderer.render(node, context) : context.origin();
};
const buildCustomRendererFunctions = (customRenderers, defaults) => {
const customTypes = Object.keys(customRenderers).filter(type => !defaults[type]);
const customEntries = customTypes.map(type => {
const fn = (node, context) => executeRenderer(customRenderers[type], node, context);
return [type, fn];
});
return Object.fromEntries(customEntries);
};
const buildCustomHTMLRenderer = (
customRenderers = {
htmlBlock: [],
htmlInline: [],
list: [],
paragraph: [],
text: [],
softbreak: [],
},
) => {
const defaults = {
htmlBlock(node, context) {
const allHtmlBlockRenderers = [...customRenderers.htmlBlock, ...htmlBlockRenderers];
return executeRenderer(allHtmlBlockRenderers, node, context);
},
htmlInline(node, context) {
const allHtmlInlineRenderers = [...customRenderers.htmlInline, ...htmlInlineRenderers];
return executeRenderer(allHtmlInlineRenderers, node, context);
},
list(node, context) {
const allListRenderers = [...customRenderers.list, ...listRenderers];
return executeRenderer(allListRenderers, node, context);
},
paragraph(node, context) {
const allParagraphRenderers = [...customRenderers.paragraph, ...paragraphRenderers];
return executeRenderer(allParagraphRenderers, node, context);
},
text(node, context) {
const allTextRenderers = [...customRenderers.text, ...textRenderers];
return executeRenderer(allTextRenderers, node, context);
},
softbreak(node, context) {
const allSoftbreakRenderers = [...customRenderers.softbreak, ...softbreakRenderers];
return executeRenderer(allSoftbreakRenderers, node, context);
},
const buildCustomHTMLRenderer = customRenderers => {
const renderersByType = {
...customRenderers,
htmlBlock: union(htmlBlockRenderers, customRenderers?.htmlBlock),
htmlInline: union(htmlInlineRenderers, customRenderers?.htmlInline),
list: union(listRenderers, customRenderers?.list),
paragraph: union(paragraphRenderers, customRenderers?.paragraph),
text: union(textRenderers, customRenderers?.text),
softbreak: union(softbreakRenderers, customRenderers?.softbreak),
};
return {
...buildCustomRendererFunctions(customRenderers, defaults),
...defaults,
};
return mapValues(renderersByType, renderers => {
return (node, context) => executeRenderer(renderers, node, context);
});
};
export default buildCustomHTMLRenderer;
import Vue from 'vue';
import { defaults } from 'lodash';
import ToolbarItem from '../toolbar_item.vue';
import buildHtmlToMarkdownRenderer from './build_html_to_markdown_renderer';
import buildCustomHTMLRenderer from './build_custom_renderer';
import { TOOLBAR_ITEM_CONFIGS } from '../constants';
const buildWrapper = propsData => {
const instance = new Vue({
......@@ -54,3 +57,10 @@ export const registerHTMLToMarkdownRenderer = editorApi => {
renderer: renderer.constructor.factory(renderer, buildHtmlToMarkdownRenderer(renderer)),
});
};
export const getEditorOptions = externalOptions => {
return defaults({
customHTMLRenderer: buildCustomHTMLRenderer(externalOptions?.customRenderers),
toolbarItems: TOOLBAR_ITEM_CONFIGS.map(toolbarItem => generateToolbarItem(toolbarItem)),
});
};
......@@ -93,7 +93,6 @@
}
.fa-remove::before,
.fa-close::before,
.fa-times::before {
content: '\f00d';
}
......
......@@ -134,20 +134,20 @@
padding-left: 8px;
padding-right: 0;
.fa-close {
.close-icon {
color: $gl-text-color-secondary;
}
&:hover .fa-close {
&:hover .close-icon {
color: $gl-text-color;
}
&.inverted {
.fa-close {
.close-icon {
color: $gl-text-color-secondary-inverted;
}
&:hover .fa-close {
&:hover .close-icon {
color: $gl-text-color-inverted;
}
}
......
......@@ -820,9 +820,7 @@ $note-form-margin-left: 72px;
}
}
.add-diff-note {
@include btn-comment-icon;
opacity: 0;
.tooltip-wrapper.add-diff-note {
margin-left: -52px;
position: absolute;
top: 50%;
......@@ -830,6 +828,18 @@ $note-form-margin-left: 72px;
z-index: 10;
}
.note-button.add-diff-note {
@include btn-comment-icon;
opacity: 0;
&[disabled] {
background: $white;
border-color: $gray-200;
color: $gl-gray-400;
cursor: not-allowed;
}
}
.disabled-comment {
background-color: $gray-light;
border-radius: $border-radius-base;
......
......@@ -143,6 +143,10 @@
margin-bottom: 0;
}
}
.gl-label-scoped {
--label-inset-border: inset 0 0 0 1px currentColor;
}
}
@include media-breakpoint-down(sm) {
......
......@@ -12,7 +12,12 @@ class Projects::VariablesController < Projects::ApplicationController
end
def update
if @project.update(variables_params)
update_result = Ci::ChangeVariablesService.new(
container: @project, current_user: current_user,
params: variables_params
).execute
if update_result
respond_to do |format|
format.json { render_variables }
end
......
......@@ -21,7 +21,7 @@ module Mutations
description: "The current state of the collection"
def ready(*)
raise ::Gitlab::Graphql::Errors::ResourceNotAvailable unless ::Feature.enabled?(:reorder_designs)
raise ::Gitlab::Graphql::Errors::ResourceNotAvailable unless ::Feature.enabled?(:reorder_designs, default_enabled: true)
end
def resolve(**args)
......
......@@ -82,7 +82,7 @@ module DesignManagement
scope :ordered, -> (project) do
# TODO: Always order by relative position after the feature flag is removed
# https://gitlab.com/gitlab-org/gitlab/-/issues/34382
if Feature.enabled?(:reorder_designs, project)
if Feature.enabled?(:reorder_designs, project, default_enabled: true)
# We need to additionally sort by `id` to support keyset pagination.
# See https://gitlab.com/gitlab-org/gitlab/-/merge_requests/17788/diffs#note_230875678
order(:relative_position, :id)
......
......@@ -16,9 +16,7 @@ module DesignManagement
def find_or_create_design!(filename:)
designs.find { |design| design.filename == filename } ||
designs.safe_find_or_create_by!(project: project, filename: filename) do |design|
design.move_to_end
end
designs.safe_find_or_create_by!(project: project, filename: filename)
end
def versions
......
......@@ -8,6 +8,12 @@ class JiraService < IssueTrackerService
PROJECTS_PER_PAGE = 50
# TODO: use jira_service.deployment_type enum when https://gitlab.com/gitlab-org/gitlab/-/merge_requests/37003 is merged
DEPLOYMENT_TYPES = {
server: 'SERVER',
cloud: 'CLOUD'
}.freeze
validates :url, public_url: true, presence: true, if: :activated?
validates :api_url, public_url: true, allow_blank: true
validates :username, presence: true, if: :activated?
......
......@@ -103,7 +103,7 @@ class ProjectPolicy < BasePolicy
with_scope :subject
condition(:moving_designs_disabled) do
!::Feature.enabled?(:reorder_designs, @subject)
!::Feature.enabled?(:reorder_designs, @subject, default_enabled: true)
end
with_scope :subject
......
......@@ -23,6 +23,7 @@ module Packages
package_detail[:maven_metadatum] = @package.maven_metadatum if @package.maven_metadatum
package_detail[:nuget_metadatum] = @package.nuget_metadatum if @package.nuget_metadatum
package_detail[:composer_metadatum] = @package.composer_metadatum if @package.composer_metadatum
package_detail[:conan_metadatum] = @package.conan_metadatum if @package.conan_metadatum
package_detail[:dependency_links] = @package.dependency_links.map(&method(:build_dependency_links))
package_detail[:pipeline] = build_pipeline_info(@package.build_info.pipeline) if @package.build_info
......
......@@ -20,7 +20,12 @@ module Ci
private
def variable
container.variables.find_by!(params[:variable_params].slice(:key)) # rubocop:disable CodeReuse/ActiveRecord
params[:variable] || find_variable
end
def find_variable
identifier = params[:variable_params].slice(:id).presence || params[:variable_params].slice(:key)
container.variables.find_by!(identifier) # rubocop:disable CodeReuse/ActiveRecord
end
end
end
......
......@@ -13,7 +13,7 @@ module DesignManagement
def execute
return error(:no_focus) unless current_design.present?
return error(:cannot_move) unless ::Feature.enabled?(:reorder_designs, project)
return error(:cannot_move) unless ::Feature.enabled?(:reorder_designs, project, default_enabled: true)
return error(:cannot_move) unless current_user.can?(:move_design, current_design)
return error(:no_neighbors) unless neighbors.present?
return error(:not_distinct) unless all_distinct?
......
# frozen_string_literal: true
module JiraImport
class CloudUsersMapperService < UsersMapperService
private
def url
"/rest/api/2/users?maxResults=#{MAX_USERS}&startAt=#{start_at.to_i}"
end
def jira_user_id(jira_user)
jira_user['accountId']
end
def jira_user_name(jira_user)
jira_user['displayName']
end
end
end
# frozen_string_literal: true
module JiraImport
class ServerUsersMapperService < UsersMapperService
private
def url
"/rest/api/2/user/search?username=''&maxResults=#{MAX_USERS}&startAt=#{start_at.to_i}"
end
def jira_user_id(jira_user)
jira_user['key']
end
def jira_user_name(jira_user)
jira_user['name']
end
end
end
......@@ -2,9 +2,7 @@
module JiraImport
class UsersImporter
attr_reader :user, :project, :start_at, :result
MAX_USERS = 50
attr_reader :user, :project, :start_at
def initialize(user, project, start_at)
@project = project
......@@ -15,29 +13,43 @@ module JiraImport
def execute
Gitlab::JiraImport.validate_project_settings!(project, user: user)
return ServiceResponse.success(payload: nil) if users.blank?
result = UsersMapper.new(project, users).execute
ServiceResponse.success(payload: result)
ServiceResponse.success(payload: mapped_users)
rescue Timeout::Error, Errno::EINVAL, Errno::ECONNRESET, Errno::ECONNREFUSED, URI::InvalidURIError, JIRA::HTTPError, OpenSSL::SSL::SSLError => error
Gitlab::ErrorTracking.track_exception(error, project_id: project.id, request: url)
ServiceResponse.error(message: "There was an error when communicating to Jira: #{error.message}")
Gitlab::ErrorTracking.track_exception(error, project_id: project.id)
ServiceResponse.error(message: "There was an error when communicating to Jira")
rescue Projects::ImportService::Error => error
ServiceResponse.error(message: error.message)
end
private
def users
@users ||= client.get(url)
def mapped_users
users_mapper_service.execute
end
def users_mapper_service
@users_mapper_service ||= user_mapper_service_factory
end
def url
"/rest/api/2/users?maxResults=#{MAX_USERS}&startAt=#{start_at.to_i}"
def deployment_type
# TODO: use project.jira_service.deployment_type value when https://gitlab.com/gitlab-org/gitlab/-/merge_requests/37003 is merged
@deployment_type ||= client.ServerInfo.all.deploymentType
end
def client
@client ||= project.jira_service.client
end
def user_mapper_service_factory
# TODO: use deployment_type enum from jira service when https://gitlab.com/gitlab-org/gitlab/-/merge_requests/37003 is merged
case deployment_type.upcase
when JiraService::DEPLOYMENT_TYPES[:server]
ServerUsersMapperService.new(project.jira_service, start_at)
when JiraService::DEPLOYMENT_TYPES[:cloud]
CloudUsersMapperService.new(project.jira_service, start_at)
else
raise ArgumentError
end
end
end
end
# frozen_string_literal: true
module JiraImport
class UsersMapper
attr_reader :project, :jira_users
class UsersMapperService
MAX_USERS = 50
def initialize(project, jira_users)
@project = project
@jira_users = jira_users
attr_reader :jira_service, :start_at
def initialize(jira_service, start_at)
@jira_service = jira_service
@start_at = start_at
end
def execute
jira_users.to_a.map do |jira_user|
users.to_a.map do |jira_user|
{
jira_account_id: jira_user['accountId'],
jira_display_name: jira_user['displayName'],
jira_account_id: jira_user_id(jira_user),
jira_display_name: jira_user_name(jira_user),
jira_email: jira_user['emailAddress']
}.merge(match_user(jira_user))
end
......@@ -21,6 +23,26 @@ module JiraImport
private
def users
@users ||= client.get(url)
end
def client
@client ||= jira_service.client
end
def url
raise NotImplementedError
end
def jira_user_id(jira_user)
raise NotImplementedError
end
def jira_user_name(jira_user)
raise NotImplementedError
end
# TODO: Matching user by email and displayName will be done as the part
# of follow-up issue: https://gitlab.com/gitlab-org/gitlab/-/issues/219023
def match_user(jira_user)
......
......@@ -8,7 +8,7 @@
require 'optparse'
require 'yaml'
require 'fileutils'
require 'cgi'
require 'uri'
require_relative '../lib/feature/shared' unless defined?(Feature::Shared)
......@@ -105,10 +105,11 @@ class FeatureFlagOptionParser
end
def read_group
$stdout.puts
$stdout.puts ">> Please specify the group introducing feature flag, like `group::apm`:"
loop do
$stdout.print "\n?> "
$stdout.print "?> "
group = $stdin.gets.strip
group = nil if group.empty?
return group if group.nil? || group.start_with?('group::')
......@@ -121,6 +122,7 @@ class FeatureFlagOptionParser
# if there's only one type, do not ask, return
return TYPES.first.first if TYPES.one?
$stdout.puts
$stdout.puts ">> Please specify the type of your feature flag:"
$stdout.puts
TYPES.each do |type, data|
......@@ -128,7 +130,7 @@ class FeatureFlagOptionParser
end
loop do
$stdout.print "\n?> "
$stdout.print "?> "
type = $stdin.gets.strip.to_sym
return type if TYPES[type]
......@@ -137,27 +139,41 @@ class FeatureFlagOptionParser
end
end
def read_issue_url(options)
def read_introduced_by_url
$stdout.puts
$stdout.puts ">> If you have MR open, can you paste the URL here? (or enter to skip)"
loop do
$stdout.print "?> "
introduced_by_url = $stdin.gets.strip
introduced_by_url = nil if introduced_by_url.empty?
return introduced_by_url if introduced_by_url.nil? || introduced_by_url.start_with?('https://')
$stderr.puts "URL needs to start with https://"
end
end
def read_rollout_issue_url(options)
return unless TYPES.dig(options.type, :rollout_issue)
url = "https://gitlab.com/gitlab-org/gitlab/-/issues/new"
title = "[Feature flag] Rollout of `#{options.name}`"
description = File.read('.gitlab/issue_templates/Feature Flag Roll Out.md')
description.sub!(':feature_name', options.name)
issue_new_url = url + "?" +
"issue[title]=" + CGI.escape(title) + "&"
# TODO: We should be able to pick `issueable_template`
# + "issue[description]=" + CGI.escape(description)
params = {
'issue[title]' => "[Feature flag] Rollout of `#{options.name}`",
'issuable_template' => 'Feature Flag Roll Out',
}
issue_new_url = url + "?" + URI.encode_www_form(params)
$stdout.puts
$stdout.puts ">> Open this URL and fill the rest of details:"
$stdout.puts issue_new_url
$stdout.puts
$stdout.puts ">> Paste URL here, or enter to skip:"
$stdout.puts ">> Paste URL of `rollout issue` here, or enter to skip:"
loop do
$stdout.print "\n?> "
$stdout.print "?> "
created_url = $stdin.gets.strip
created_url = nil if created_url.empty?
return created_url if created_url.nil? || created_url.start_with?('https://')
......@@ -185,7 +201,8 @@ class FeatureFlagCreator
# Read type from $stdin unless is already set
options.type ||= FeatureFlagOptionParser.read_type
options.group ||= FeatureFlagOptionParser.read_group
options.rollout_issue_url ||= FeatureFlagOptionParser.read_issue_url(options)
options.introduced_by_url ||= FeatureFlagOptionParser.read_introduced_by_url
options.rollout_issue_url ||= FeatureFlagOptionParser.read_rollout_issue_url(options)
$stdout.puts "\e[32mcreate\e[0m #{file_path}"
$stdout.puts contents
......
---
title: Replace fa-close icons with GitLab SVG close icon
merge_request: 39267
author:
type: changed
---
title: Fix Conan recipe display in the package details page
merge_request: 39643
author:
type: fixed
---
title: Handle user mapping for Jira server instances
merge_request: 39362
author:
type: fixed
---
title: Deprecate additions and deletions attributes in Repositories API
merge_request: 39653
author:
type: deprecated
---
title: Enable reorder_designs feature by default
merge_request: 39555
author:
type: changed
---
title: Disable commenting on lines in files that were or are symlinks or replace or
are replaced by symlinks
merge_request: 35371
author:
type: fixed
---
title: Fix missing scoped label borders for todos
merge_request: 39459
author:
type: fixed
---
name: ci_if_parenthesis_enabled
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/37574
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/238174
group: group::ci
type: development
default_enabled: true
\ No newline at end of file
---
name: ci_plan_needs_size_limit
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/37568
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/238173
group: group::ci
type: development
default_enabled: true
\ No newline at end of file
......@@ -4,4 +4,4 @@ introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/37835
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/232992
group: group::knowledge
type: development
default_enabled: false
default_enabled: true
......@@ -3,7 +3,7 @@
# This file requires config/initializers/1_settings.rb
if Rails.env.development?
Rails.application.config.hosts += [Gitlab.config.gitlab.host, 'unix']
Rails.application.config.hosts += [Gitlab.config.gitlab.host, 'unix', 'host.docker.internal']
if ENV['RAILS_HOSTS']
additional_hosts = ENV['RAILS_HOSTS'].split(',').select(&:presence)
......
# frozen_string_literal: true
# This migration is not needed anymore and was disabled, because we're now
# also backfilling design positions immediately before moving a design.
#
# See https://gitlab.com/gitlab-org/gitlab/-/merge_requests/39555
class BackfillDesignsRelativePosition < ActiveRecord::Migration[6.0]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
INTERVAL = 2.minutes
BATCH_SIZE = 1000
MIGRATION = 'BackfillDesignsRelativePosition'
disable_ddl_transaction!
class Issue < ActiveRecord::Base
include EachBatch
self.table_name = 'issues'
has_many :designs
end
class Design < ActiveRecord::Base
self.table_name = 'design_management_designs'
end
def up
issues_with_designs = Issue.where(id: Design.select(:issue_id))
issues_with_designs.each_batch(of: BATCH_SIZE) do |relation, index|
issue_ids = relation.pluck(:id)
delay = INTERVAL * index
migrate_in(delay, MIGRATION, [issue_ids])
end
# no-op
end
def down
# no-op
end
end
......@@ -199,6 +199,9 @@ authentication if the repository is publicly accessible.
GET /projects/:id/repository/contributors
```
CAUTION: **Deprecation:**
The `additions` and `deletions` attributes are deprecated [as of GitLab 13.4](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/39653) because they [always return `0`](https://gitlab.com/gitlab-org/gitlab/-/issues/233119).
Parameters:
- `id` (required) - The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user
......@@ -212,14 +215,14 @@ Response:
"name": "Example User",
"email": "example@example.com",
"commits": 117,
"additions": 2097,
"deletions": 517
"additions": 0,
"deletions": 0
}, {
"name": "Sample User",
"email": "sample@example.com",
"commits": 33,
"additions": 338,
"deletions": 244
"additions": 0,
"deletions": 0
}]
```
......
......@@ -760,9 +760,9 @@ Examples:
- `($VARIABLE1 =~ /^content.*/ || $VARIABLE2 =~ /thing$/) && $VARIABLE3`
- `$CI_COMMIT_BRANCH == "my-branch" || (($VARIABLE1 == "thing" || $VARIABLE2 == "thing") && $VARIABLE3)`
The feature is currently deployed behind a feature flag that is **disabled by default**.
The feature is currently deployed behind a feature flag that is **enabled by default**.
[GitLab administrators with access to the GitLab Rails console](../../administration/feature_flags.md)
can opt to enable it for your instance.
can opt to disable it for your instance.
To enable it:
......
......@@ -1989,9 +1989,7 @@ This example creates four paths of execution:
- The maximum number of jobs that a single job can need in the `needs:` array is limited:
- For GitLab.com, the limit is ten. For more information, see our
[infrastructure issue](https://gitlab.com/gitlab-com/gl-infra/infrastructure/-/issues/7541).
- For self-managed instances, the limit is:
- 10, if the `ci_plan_needs_size_limit` feature flag is disabled (default).
- 50, if the `ci_plan_needs_size_limit` feature flag is enabled. This limit [can be changed](#changing-the-needs-job-limit-core-only).
- For self-managed instances, the limit is: 50. This limit [can be changed](#changing-the-needs-job-limit-core-only).
- If `needs:` refers to a job that is marked as `parallel:`.
the current job will depend on all parallel jobs created.
- `needs:` is similar to `dependencies:` in that it needs to use jobs from prior stages,
......@@ -2002,18 +2000,10 @@ This example creates four paths of execution:
##### Changing the `needs:` job limit **(CORE ONLY)**
The maximum number of jobs that can be defined within `needs:` defaults to 10.
The maximum number of jobs that can be defined within `needs:` defaults to 50.
To change this limit to 50 on a self-managed installation, a GitLab administrator
with [access to the GitLab Rails console](../../administration/feature_flags.md)
can enable the `:ci_plan_needs_size_limit` feature flag:
```ruby
Feature::enable(:ci_plan_needs_size_limit)
```
After the feature flag is enabled, you can choose a custom limit. For example, to
set the limit to 100:
A GitLab administrator with [access to the GitLab Rails console](../../administration/feature_flags.md)
can choose a custom limit. For example, to set the limit to 100:
```ruby
Plan.default.actual_limits.update!(ci_needs_size_limit: 100)
......
......@@ -23,7 +23,7 @@ Migrations can be disabled if:
In order to disable a migration, the following steps apply to all types of migrations:
1. Turn the migration into a no-op by removing the code inside `#up`, `#down`
or `#perform` methods, and adding `#no-op` comment instead.
or `#perform` methods, and adding `# no-op` comment instead.
1. Add a comment explaining why the code is gone.
Disabling migrations requires explicit approval of Database Maintainer.
......
......@@ -78,19 +78,21 @@ Only feature flags that have a YAML definition file can be used when running the
```shell
$ bin/feature-flag my-feature-flag
>> Please specify the group introducing feature flag, like `group::apm`:
?> group::memory
>> Open this URL and fill the rest of details:
https://gitlab.com/gitlab-org/gitlab/-/issues/new?issue[title]=%5BFeature+flag%5D+Rollout+of+%60my-feature-flag%60&
>> Paste URL here, or enter to skip:
>> If you have MR open, can you paste the URL here? (or enter to skip)
?> https://gitlab.com/gitlab-org/gitlab/-/merge_requests/38602
>> Open this URL and fill the rest of details:
https://gitlab.com/gitlab-org/gitlab/-/issues/new?issue%5Btitle%5D=%5BFeature+flag%5D+Rollout+of+%60test-flag%60&issuable_template=Feature+Flag+Roll+Out
?>
create config/feature_flags/development/my_feature_flag.yml
>> Paste URL of `rollout issue` here, or enter to skip:
?> https://gitlab.com/gitlab-org/gitlab/-/issues/232533
create config/feature_flags/development/test-flag.yml
---
name: my_feature_flag
introduced_by_url:
rollout_issue_url:
name: test-flag
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/38602
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/232533
group: group::memory
type: development
default_enabled: false
......
---
stage: Create
group: Source Code
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
info: "To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers"
type: reference, concepts
---
......@@ -13,16 +13,16 @@ process, as it clearly communicates the ability to merge the change.
## Optional Approvals **(CORE ONLY)**
> Introduced in [GitLab Core 13.2](https://gitlab.com/gitlab-org/gitlab/-/issues/27426).
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/27426) in GitLab 13.2.
Any user with Developer or greater [permissions](../../permissions.md) can approve a merge request in GitLab Core.
This provides a consistent mechanism for reviewers to provide approval, and makes it easy for
Any user with Developer or greater [permissions](../../permissions.md) can approve a merge request in GitLab Core and higher tiers.
This provides a consistent mechanism for reviewers to approve merge requests, and makes it easy for
maintainers to know when a change is ready to merge. Approvals in Core are optional and do
not prevent a merge request from being merged when there is no approval.
## Required Approvals **(STARTER)**
> Introduced in [GitLab Enterprise Edition 7.12](https://about.gitlab.com/releases/2015/06/22/gitlab-7-12-released/#merge-request-approvers-ee-only).
> [Introduced](https://about.gitlab.com/releases/2015/06/22/gitlab-7-12-released/#merge-request-approvers-ee-only) in GitLab Enterprise Edition 7.12. Available in [GitLab Starter](https://about.gitlab.com/pricing/) and higher tiers.
Required approvals enable enforced code review by requiring specified people
to approve a merge request before it can be merged.
......@@ -33,8 +33,8 @@ Required approvals enable multiple use cases:
- Specifying reviewers for a given proposed code change, as well as a minimum number
of reviewers, through [Approval rules](#approval-rules).
- Specifying categories of reviewers, such as backend, frontend, quality assurance,
database, etc., for all proposed code changes.
- Automatically designating [Code Owners as eligible approvers](#code-owners-as-eligible-approvers),
database, and so on, for all proposed code changes.
- Designating [Code Owners as eligible approvers](#code-owners-as-eligible-approvers),
determined by the files changed in a merge request.
- [Requiring approval from a security team](#security-approvals-in-merge-requests-ultimate)
before merging code that could introduce a vulnerability.**(ULTIMATE)**
......@@ -50,14 +50,10 @@ be merged, and optionally which users should do the approving. Approvals can be
If no approval rules are defined, any user can approve a merge request, though the default
minimum number of required approvers can still be set in the [project settings for merge request approvals](#merge-request-approvals-project-settings).
Approval rules define how many approvals a merge request must receive before it can
be merged, and optionally which users should do the approving. Approvals can be defined:
- [As project defaults](#adding--editing-a-default-approval-rule).
- [Per merge request](#editing--overriding-approval-rules-per-merge-request).
If no approval rules are defined, any user can approve a merge request, though the default
minimum number of required approvers can still be set in the [project settings for merge request approvals](#merge-request-approvals-project-settings).
You can opt to define one single rule to approve a merge request among the available rules
or choose more than one. Single approval rules are available in GitLab Starter and higher tiers,
while [multiple approval rules](#multiple-approval-rules-premium) are available in
[GitLab Premium](https://about.gitlab.com/pricing/) and above.
NOTE: **Note:**
On GitLab.com, you can add a group as an approver if you're a member of that group or the
......@@ -88,6 +84,11 @@ if [**Prevent author approval**](#allowing-merge-request-authors-to-approve-thei
and [**Prevent committers approval**](#prevent-approval-of-merge-requests-by-their-committers) (disabled by default)
are enabled on the project settings.
[Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/10294) in [GitLab Starter](https://about.gitlab.com/pricing/) 13.3,
when an eligible approver comments on a merge request, it appears in the **Commented by** column of the Approvals widget,
indicating who has engaged in the merge request review. Authors and reviewers can also easily identify who they should reach out
to if they have any questions or inputs about the content of the merge request.
##### Implicit Approvers
If the number of required approvals is greater than the number of assigned approvers,
......@@ -187,9 +188,6 @@ a rule is already defined.
When an [eligible approver](#eligible-approvers) approves a merge request, it will
reduce the number of approvals left for all rules that the approver belongs to.
When an [eligible approver](#eligible-approvers) comments on a merge request, it
appears in the **Commented by** column. This feature was [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/10294) in GitLab 13.3.
![Approvals premium merge request widget](img/approvals_premium_mr_widget_v13_3.png)
#### Scoped to Protected Branch **(PREMIUM)**
......
......@@ -67,10 +67,11 @@ module API
optional :environment_scope, type: String, desc: 'The environment_scope of the variable'
end
post ':id/variables' do
variable_params = declared_params(include_missing: false)
variable_params = filter_variable_parameters(variable_params)
variable = user_project.variables.create(variable_params)
variable = ::Ci::ChangeVariableService.new(
container: user_project,
current_user: current_user,
params: { action: :create, variable_params: filter_variable_parameters(declared_params(include_missing: false)) }
).execute
if variable.valid?
present variable, with: Entities::Ci::Variable
......@@ -96,10 +97,17 @@ module API
variable = find_variable(params)
not_found!('Variable') unless variable
variable_params = declared_params(include_missing: false).except(:key, :filter)
variable_params = filter_variable_parameters(variable_params)
variable_params = filter_variable_parameters(
declared_params(include_missing: false)
.except(:key, :filter)
)
variable = ::Ci::ChangeVariableService.new(
container: user_project,
current_user: current_user,
params: { action: :update, variable: variable, variable_params: variable_params }
).execute
if variable.update(variable_params)
if variable.valid?
present variable, with: Entities::Ci::Variable
else
render_validation_error!(variable)
......@@ -119,8 +127,11 @@ module API
variable = find_variable(params)
not_found!('Variable') unless variable
# Variables don't have a timestamp. Therefore, destroy unconditionally.
variable.destroy
::Ci::ChangeVariableService.new(
container: user_project,
current_user: current_user,
params: { action: :destroy, variable: variable }
).execute
no_content!
end
......
......@@ -2,52 +2,13 @@
module Gitlab
module BackgroundMigration
# Backfill `relative_position` column in `design_management_designs` table
# This migration is not needed anymore and was disabled, because we're now
# also backfilling design positions immediately before moving a design.
#
# See https://gitlab.com/gitlab-org/gitlab/-/merge_requests/39555
class BackfillDesignsRelativePosition
# Define the issue model
class Issue < ActiveRecord::Base
self.table_name = 'issues'
end
# Define the design model
class Design < ActiveRecord::Base
include RelativePositioning if defined?(RelativePositioning)
self.table_name = 'design_management_designs'
def self.relative_positioning_query_base(design)
where(issue_id: design.issue_id)
end
def self.relative_positioning_parent_column
:issue_id
end
def self.move_nulls_to_start(designs)
if defined?(super)
super(designs)
else
logger.error "BackfillDesignsRelativePosition failed because move_nulls_to_start is no longer included in the RelativePositioning concern"
end
end
end
def perform(issue_ids)
issue_ids.each do |issue_id|
migrate_issue(issue_id)
end
end
private
def migrate_issue(issue_id)
issue = Issue.find_by(id: issue_id)
return unless issue
designs = Design.where(issue_id: issue.id).order(:id)
return unless designs.any?
Design.move_nulls_to_start(designs)
# no-op
end
end
end
......
......@@ -57,7 +57,7 @@ module Gitlab
end
def self.ci_if_parenthesis_enabled?
::Feature.enabled?(:ci_if_parenthesis_enabled)
::Feature.enabled?(:ci_if_parenthesis_enabled, default_enabled: true)
end
def self.allow_to_create_merge_request_pipelines_in_target_project?(target_project)
......@@ -65,7 +65,7 @@ module Gitlab
end
def self.ci_plan_needs_size_limit?(project)
::Feature.enabled?(:ci_plan_needs_size_limit, project)
::Feature.enabled?(:ci_plan_needs_size_limit, project, default_enabled: true)
end
def self.job_entry_matches_all_keys?
......
......@@ -604,8 +604,6 @@ module Gitlab
end
def action_monthly_active_users(time_period)
return {} unless Feature.enabled?(Gitlab::UsageDataCounters::TrackUniqueActions::FEATURE_FLAG)
counter = Gitlab::UsageDataCounters::TrackUniqueActions
project_count = redis_usage_data do
......
......@@ -4,7 +4,6 @@ module Gitlab
module UsageDataCounters
module TrackUniqueActions
KEY_EXPIRY_LENGTH = 29.days
FEATURE_FLAG = :track_unique_actions
WIKI_ACTION = :wiki_action
DESIGN_ACTION = :design_action
......@@ -29,7 +28,6 @@ module Gitlab
class << self
def track_event(event_action:, event_target:, author_id:, time: Time.zone.now)
return unless Gitlab::CurrentSettings.usage_ping_enabled
return unless Feature.enabled?(FEATURE_FLAG)
return unless valid_target?(event_target)
return unless valid_action?(event_action)
......
......@@ -6161,6 +6161,12 @@ msgstr ""
msgid "Comment/Reply (quoting selected text)"
msgstr ""
msgid "Commenting on files that replace or are replaced by symbolic links is currently not supported."
msgstr ""
msgid "Commenting on symbolic links that replace or are replaced by files is currently not supported."
msgstr ""
msgid "Comments"
msgstr ""
......
......@@ -8,7 +8,7 @@ RSpec.describe 'bin/feature-flag' do
using RSpec::Parameterized::TableSyntax
describe FeatureFlagCreator do
let(:argv) { %w[feature-flag-name -t development -g group::memory -i https://url] }
let(:argv) { %w[feature-flag-name -t development -g group::memory -i https://url -m http://url] }
let(:options) { FeatureFlagOptionParser.parse(argv) }
let(:creator) { described_class.new(options) }
let(:existing_flag) { File.join('config', 'feature_flags', 'development', 'existing-feature-flag.yml') }
......@@ -183,15 +183,51 @@ RSpec.describe 'bin/feature-flag' do
end
end
describe '.rollout_issue_url' do
describe '.read_introduced_by_url' do
let(:url) { 'https://merge-request' }
it 'reads type from $stdin' do
expect($stdin).to receive(:gets).and_return(url)
expect do
expect(described_class.read_introduced_by_url).to eq('https://merge-request')
end.to output(/can you paste the URL here/).to_stdout
end
context 'empty URL given' do
let(:url) { '' }
it 'skips entry' do
expect($stdin).to receive(:gets).and_return(url)
expect do
expect(described_class.read_introduced_by_url).to be_nil
end.to output(/can you paste the URL here/).to_stdout
end
end
context 'invalid URL given' do
let(:url) { 'invalid' }
it 'shows error message and retries' do
expect($stdin).to receive(:gets).and_return(url)
expect($stdin).to receive(:gets).and_raise('EOF')
expect do
expect { described_class.read_introduced_by_url }.to raise_error(/EOF/)
end.to output(/can you paste the URL here/).to_stdout
.and output(/URL needs to start with/).to_stderr
end
end
end
describe '.read_rollout_issue_url' do
let(:options) { OpenStruct.new(name: 'foo', type: :development) }
let(:url) { 'https://issue' }
it 'reads type from $stdin' do
expect($stdin).to receive(:gets).and_return(url)
expect do
expect(described_class.read_issue_url(options)).to eq('https://issue')
end.to output(/Paste URL here/).to_stdout
expect(described_class.read_rollout_issue_url(options)).to eq('https://issue')
end.to output(/Paste URL of `rollout issue` here/).to_stdout
end
context 'invalid URL given' do
......@@ -202,8 +238,8 @@ RSpec.describe 'bin/feature-flag' do
expect($stdin).to receive(:gets).and_raise('EOF')
expect do
expect { described_class.read_issue_url(options) }.to raise_error(/EOF/)
end.to output(/Paste URL here/).to_stdout
expect { described_class.read_rollout_issue_url(options) }.to raise_error(/EOF/)
end.to output(/Paste URL of `rollout issue` here/).to_stdout
.and output(/URL needs to start/).to_stderr
end
end
......
......@@ -53,7 +53,7 @@ RSpec.describe 'Visual tokens', :js do
end
it 'ends editing mode when document is clicked' do
find('#content-body').click
find('.js-navbar').click
expect_filtered_search_input_empty
expect(page).to have_css('#js-dropdown-author', visible: false)
......@@ -142,7 +142,7 @@ RSpec.describe 'Visual tokens', :js do
it 'does not tokenize incomplete token' do
filtered_search.send_keys('author:=')
find('body').click
find('.js-navbar').click
token = page.all('.tokens-container .js-visual-token')[1]
expect_filtered_search_input_empty
......
......@@ -5,9 +5,9 @@ require 'spec_helper'
RSpec.describe 'User uploads new design', :js do
include DesignManagementTestHelpers
let_it_be(:project) { create(:project_empty_repo, :public) }
let_it_be(:user) { project.owner }
let_it_be(:issue) { create(:issue, project: project) }
let(:project) { create(:project_empty_repo, :public) }
let(:user) { project.owner }
let(:issue) { create(:issue, project: project) }
before do
sign_in(user)
......@@ -28,7 +28,7 @@ RSpec.describe 'User uploads new design', :js do
let(:feature_enabled) { true }
it 'uploads designs' do
attach_file(:design_file, logo_fixture, make_visible: true)
upload_design(logo_fixture, count: 1)
expect(page).to have_selector('.js-design-list-item', count: 1)
......@@ -36,9 +36,12 @@ RSpec.describe 'User uploads new design', :js do
expect(page).to have_content('dk.png')
end
attach_file(:design_file, gif_fixture, make_visible: true)
upload_design(gif_fixture, count: 2)
# Known bug in the legacy implementation: new designs are inserted
# in the beginning on the frontend.
expect(page).to have_selector('.js-design-list-item', count: 2)
expect(page.all('.js-design-list-item').map(&:text)).to eq(['banana_sample.gif', 'dk.png'])
end
end
......@@ -61,8 +64,8 @@ RSpec.describe 'User uploads new design', :js do
context "when the feature is available" do
let(:feature_enabled) { true }
it 'uploads designs', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/225616' do
attach_file(:design_file, logo_fixture, make_visible: true)
it 'uploads designs' do
upload_design(logo_fixture, count: 1)
expect(page).to have_selector('.js-design-list-item', count: 1)
......@@ -70,9 +73,10 @@ RSpec.describe 'User uploads new design', :js do
expect(page).to have_content('dk.png')
end
attach_file(:design_file, gif_fixture, make_visible: true)
upload_design(gif_fixture, count: 2)
expect(page).to have_selector('.js-design-list-item', count: 2)
expect(page.all('.js-design-list-item').map(&:text)).to eq(['dk.png', 'banana_sample.gif'])
end
end
......@@ -92,4 +96,12 @@ RSpec.describe 'User uploads new design', :js do
def gif_fixture
Rails.root.join('spec', 'fixtures', 'banana_sample.gif')
end
def upload_design(fixture, count:)
attach_file(:design_file, fixture, match: :first, make_visible: true)
wait_for('designs uploaded') do
issue.reload.designs.count == count
end
end
end
......@@ -17,6 +17,17 @@ export const Editor = {
type: String,
},
},
created() {
const mockEditorApi = {
eventManager: {
addEventType: jest.fn(),
listen: jest.fn(),
removeEventHandler: jest.fn(),
},
};
this.$emit('load', mockEditorApi);
},
render(h) {
return h('div');
},
......
......@@ -27,7 +27,7 @@ exports[`AlertsSettingsForm with default values renders the initial template 1`]
<gl-form-group-stub label=\\"Authorization key\\" label-for=\\"authorization-key\\" label-class=\\"label-bold\\">
<gl-form-input-group-stub value=\\"abcedfg123\\" predefinedoptions=\\"[object Object]\\" id=\\"authorization-key\\" readonly=\\"\\" class=\\"gl-mb-2\\"></gl-form-input-group-stub>
<div class=\\"gl-display-flex gl-justify-content-end\\">
<gl-button-stub category=\\"tertiary\\" variant=\\"default\\" size=\\"medium\\" icon=\\"\\" disabled=\\"true\\" class=\\"gl-mt-3\\" role=\\"button\\" tabindex=\\"0\\">Reset key</gl-button-stub>
<gl-button-stub category=\\"primary\\" variant=\\"default\\" size=\\"medium\\" icon=\\"\\" disabled=\\"true\\" class=\\"gl-mt-3\\" role=\\"button\\" tabindex=\\"0\\">Reset key</gl-button-stub>
</div>
<gl-modal-stub modalid=\\"authKeyModal\\" titletag=\\"h4\\" modalclass=\\"\\" size=\\"md\\" title=\\"Reset key\\" ok-title=\\"Reset key\\" ok-variant=\\"danger\\">
Resetting the authorization key for this project will require updating the authorization key in every alert source it is enabled in.
......@@ -37,7 +37,7 @@ exports[`AlertsSettingsForm with default values renders the initial template 1`]
<gl-form-textarea-stub noresize=\\"true\\" id=\\"alert-json\\" disabled=\\"true\\" state=\\"true\\" placeholder=\\"Enter test alert JSON....\\" rows=\\"6\\" max-rows=\\"10\\"></gl-form-textarea-stub>
</gl-form-group-stub>
<div class=\\"gl-display-flex gl-justify-content-end\\">
<gl-button-stub category=\\"tertiary\\" variant=\\"default\\" size=\\"medium\\" icon=\\"\\" disabled=\\"true\\">Test alert payload</gl-button-stub>
<gl-button-stub category=\\"primary\\" variant=\\"default\\" size=\\"medium\\" icon=\\"\\" disabled=\\"true\\">Test alert payload</gl-button-stub>
</div>
<div class=\\"footer-block row-content-block gl-display-flex gl-justify-content-space-between\\">
<gl-button-stub category=\\"primary\\" variant=\\"default\\" size=\\"medium\\" icon=\\"\\" disabled=\\"true\\">
......
......@@ -56,7 +56,7 @@ exports[`Code navigation popover component renders popover 1`] = `
class="popover-body border-top"
>
<gl-button-stub
category="tertiary"
category="primary"
class="w-100"
data-testid="go-to-definition-btn"
href="http://gitlab.com/test.js"
......
......@@ -44,7 +44,7 @@ exports[`Design management toolbar component renders design and updated data 1`]
/>
<gl-button-stub
category="tertiary"
category="primary"
href="/-/designs/306/7f747adcd4693afadbe968d7ba7d983349b9012d"
icon="download"
size="medium"
......
......@@ -5,7 +5,7 @@ exports[`Design management upload button component renders inverted upload desig
isinverted="true"
>
<gl-button-stub
category="tertiary"
category="primary"
icon=""
size="small"
title="Adding a design with the same filename replaces the file in a new version."
......@@ -30,7 +30,7 @@ exports[`Design management upload button component renders inverted upload desig
exports[`Design management upload button component renders loading icon 1`] = `
<div>
<gl-button-stub
category="tertiary"
category="primary"
disabled="true"
icon=""
size="small"
......@@ -62,7 +62,7 @@ exports[`Design management upload button component renders loading icon 1`] = `
exports[`Design management upload button component renders upload design button 1`] = `
<div>
<gl-button-stub
category="tertiary"
category="primary"
icon=""
size="small"
title="Adding a design with the same filename replaces the file in a new version."
......
......@@ -110,7 +110,7 @@ exports[`Design management index page designs renders designs list and header wi
class="qa-selector-toolbar gl-display-flex gl-align-items-center"
>
<gl-button-stub
category="tertiary"
category="primary"
class="gl-mr-3 js-select-all"
icon=""
size="small"
......
......@@ -65,7 +65,7 @@ exports[`Design management design index page renders design index 1`] = `
/>
<gl-button-stub
category="tertiary"
category="primary"
class="link-inherit-color gl-text-body gl-text-decoration-none gl-font-weight-bold gl-mb-4"
data-testid="resolved-comments"
icon="chevron-right"
......
......@@ -65,7 +65,7 @@ exports[`Design management design index page renders design index 1`] = `
/>
<gl-button-stub
category="tertiary"
category="primary"
class="link-inherit-color gl-text-body gl-text-decoration-none gl-font-weight-bold gl-mb-4"
data-testid="resolved-comments"
icon="chevron-right"
......
......@@ -18,6 +18,12 @@ const TEST_LINE_CODE = 'LC_42';
const TEST_FILE_HASH = diffFileMockData.file_hash;
describe('DiffTableCell', () => {
const symlinkishFileTooltip =
'Commenting on symbolic links that replace or are replaced by files is currently not supported.';
const realishFileTooltip =
'Commenting on files that replace or are replaced by symbolic links is currently not supported.';
const otherFileTooltip = 'Add a comment to this line';
let wrapper;
let line;
let store;
......@@ -67,6 +73,7 @@ describe('DiffTableCell', () => {
const findTd = () => wrapper.find({ ref: 'td' });
const findNoteButton = () => wrapper.find({ ref: 'addDiffNoteButton' });
const findLineNumber = () => wrapper.find({ ref: 'lineNumberRef' });
const findTooltip = () => wrapper.find({ ref: 'addNoteTooltip' });
const findAvatars = () => wrapper.find(DiffGutterAvatars);
describe('td', () => {
......@@ -134,6 +141,53 @@ describe('DiffTableCell', () => {
});
},
);
it.each`
disabled | commentsDisabled
${'disabled'} | ${true}
${undefined} | ${false}
`(
'has attribute disabled=$disabled when the outer component has prop commentsDisabled=$commentsDisabled',
({ disabled, commentsDisabled }) => {
line.commentsDisabled = commentsDisabled;
createComponent({
showCommentButton: true,
isHover: true,
});
wrapper.setData({ isCommentButtonRendered: true });
return wrapper.vm.$nextTick().then(() => {
expect(findNoteButton().attributes('disabled')).toBe(disabled);
});
},
);
it.each`
tooltip | commentsDisabled
${symlinkishFileTooltip} | ${{ wasSymbolic: true }}
${symlinkishFileTooltip} | ${{ isSymbolic: true }}
${realishFileTooltip} | ${{ wasReal: true }}
${realishFileTooltip} | ${{ isReal: true }}
${otherFileTooltip} | ${false}
`(
'has the correct tooltip when commentsDisabled=$commentsDisabled',
({ tooltip, commentsDisabled }) => {
line.commentsDisabled = commentsDisabled;
createComponent({
showCommentButton: true,
isHover: true,
});
wrapper.setData({ isCommentButtonRendered: true });
return wrapper.vm.$nextTick().then(() => {
expect(findTooltip().attributes('title')).toBe(tooltip);
});
},
);
});
describe('line number', () => {
......
......@@ -280,8 +280,8 @@ describe('Filtered Search Visual Tokens', () => {
);
});
it('contains fa-close icon', () => {
expect(tokenElement.querySelector('.remove-token .fa-close')).toEqual(expect.anything());
it('contains close icon', () => {
expect(tokenElement.querySelector('.remove-token .close-icon')).toEqual(expect.anything());
});
});
});
......
......@@ -17,7 +17,7 @@ exports[`grafana integration component default state to match the default snapsh
</h3>
<gl-button-stub
category="tertiary"
category="primary"
class="js-settings-toggle"
icon=""
size="medium"
......
......@@ -15,7 +15,7 @@ export default class FilteredSearchSpecHelper {
<div class="value-container">
<div class="value">${value}</div>
<div class="remove-token" role="button">
<i class="fa fa-close"></i>
<svg class="s16 close-icon"></svg>
</div>
</div>
</div>
......
......@@ -85,7 +85,7 @@ exports[`Alert integration settings form default state should match the default
class="gl-display-flex gl-justify-content-end"
>
<gl-button-stub
category="tertiary"
category="primary"
class="js-no-auto-disable"
data-qa-selector="save_changes_button"
icon=""
......
......@@ -18,7 +18,7 @@ exports[`IncidentsSettingTabs should render the component 1`] = `
</h4>
<gl-button-stub
category="tertiary"
category="primary"
class="js-settings-toggle"
icon=""
size="medium"
......
......@@ -46,7 +46,7 @@ exports[`Alert integration settings form should match the default snapshot 1`] =
class="gl-display-flex gl-justify-content-end"
>
<gl-button-stub
category="tertiary"
category="primary"
class="gl-mt-3"
data-testid="webhook-reset-btn"
icon=""
......@@ -80,7 +80,7 @@ exports[`Alert integration settings form should match the default snapshot 1`] =
class="gl-display-flex gl-justify-content-end"
>
<gl-button-stub
category="tertiary"
category="primary"
class="js-no-auto-disable"
icon=""
size="medium"
......
......@@ -38,7 +38,7 @@ exports[`User Operation confirmation modal renders modal with form included 1`]
/>
</form>
<gl-button-stub
category="tertiary"
category="primary"
icon=""
size="medium"
variant="default"
......
......@@ -82,7 +82,7 @@ exports[`Project remove modal intialized matches the snapshot 1`] = `
<template>
<gl-button-stub
category="tertiary"
category="primary"
class="js-modal-action-cancel"
icon=""
size="medium"
......@@ -96,7 +96,7 @@ exports[`Project remove modal intialized matches the snapshot 1`] = `
<!---->
<gl-button-stub
category="tertiary"
category="primary"
class="js-modal-action-primary"
disabled="true"
icon=""
......
......@@ -11,7 +11,7 @@ exports[`EmptyStateComponent should render content 1`] = `
<p>In order to start using functions as a service, you must first install Knative on your Kubernetes cluster. <gl-link-stub href=\\"/help\\">More information</gl-link-stub>
</p>
<div>
<gl-button-stub category=\\"tertiary\\" variant=\\"success\\" size=\\"medium\\" icon=\\"\\" href=\\"/clusters\\">Install Knative</gl-button-stub>
<gl-button-stub category=\\"primary\\" variant=\\"success\\" size=\\"medium\\" icon=\\"\\" href=\\"/clusters\\">Install Knative</gl-button-stub>
<!---->
</div>
</div>
......
......@@ -39,7 +39,7 @@ exports[`Clone Dropdown Button rendering matches the snapshot 1`] = `
tag="div"
>
<gl-button-stub
category="tertiary"
category="primary"
class="d-inline-flex"
data-clipboard-text="ssh://foo.bar"
data-qa-selector="copy_ssh_url_button"
......@@ -80,7 +80,7 @@ exports[`Clone Dropdown Button rendering matches the snapshot 1`] = `
tag="div"
>
<gl-button-stub
category="tertiary"
category="primary"
class="d-inline-flex"
data-clipboard-text="http://foo.bar"
data-qa-selector="copy_http_url_button"
......
......@@ -5,10 +5,13 @@ import {
registerHTMLToMarkdownRenderer,
addImage,
getMarkdown,
getEditorOptions,
} from '~/vue_shared/components/rich_content_editor/services/editor_service';
import buildHTMLToMarkdownRenderer from '~/vue_shared/components/rich_content_editor/services/build_html_to_markdown_renderer';
import buildCustomRenderer from '~/vue_shared/components/rich_content_editor/services/build_custom_renderer';
jest.mock('~/vue_shared/components/rich_content_editor/services/build_html_to_markdown_renderer');
jest.mock('~/vue_shared/components/rich_content_editor/services/build_custom_renderer');
describe('Editor Service', () => {
let mockInstance;
......@@ -120,4 +123,25 @@ describe('Editor Service', () => {
expect(mockInstance.toMarkOptions.renderer).toBe(extendedRenderer);
});
});
describe('getEditorOptions', () => {
const externalOptions = {
customRenderers: {},
};
const renderer = {};
beforeEach(() => {
buildCustomRenderer.mockReturnValueOnce(renderer);
});
it('generates a configuration object with a custom HTML renderer and toolbarItems', () => {
expect(getEditorOptions()).toHaveProp('customHTMLRenderer', renderer);
expect(getEditorOptions()).toHaveProp('toolbarItems');
});
it('passes external renderers to the buildCustomRenderers function', () => {
getEditorOptions(externalOptions);
expect(buildCustomRenderer).toHaveBeenCalledWith(externalOptions.customRenderers);
});
});
});
......@@ -2,7 +2,6 @@ import { shallowMount } from '@vue/test-utils';
import RichContentEditor from '~/vue_shared/components/rich_content_editor/rich_content_editor.vue';
import AddImageModal from '~/vue_shared/components/rich_content_editor/modals/add_image/add_image_modal.vue';
import {
EDITOR_OPTIONS,
EDITOR_TYPES,
EDITOR_HEIGHT,
EDITOR_PREVIEW_STYLE,
......@@ -14,6 +13,7 @@ import {
removeCustomEventListener,
addImage,
registerHTMLToMarkdownRenderer,
getEditorOptions,
} from '~/vue_shared/components/rich_content_editor/services/editor_service';
jest.mock('~/vue_shared/components/rich_content_editor/services/editor_service', () => ({
......@@ -22,6 +22,7 @@ jest.mock('~/vue_shared/components/rich_content_editor/services/editor_service',
removeCustomEventListener: jest.fn(),
addImage: jest.fn(),
registerHTMLToMarkdownRenderer: jest.fn(),
getEditorOptions: jest.fn(),
}));
describe('Rich Content Editor', () => {
......@@ -32,13 +33,25 @@ describe('Rich Content Editor', () => {
const findEditor = () => wrapper.find({ ref: 'editor' });
const findAddImageModal = () => wrapper.find(AddImageModal);
beforeEach(() => {
const buildWrapper = () => {
wrapper = shallowMount(RichContentEditor, {
propsData: { content, imageRoot },
});
};
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
describe('when content is loaded', () => {
const editorOptions = {};
beforeEach(() => {
getEditorOptions.mockReturnValueOnce(editorOptions);
buildWrapper();
});
it('renders an editor', () => {
expect(findEditor().exists()).toBe(true);
});
......@@ -47,8 +60,8 @@ describe('Rich Content Editor', () => {
expect(findEditor().props().initialValue).toBe(content);
});
it('provides the correct editor options', () => {
expect(findEditor().props().options).toEqual(EDITOR_OPTIONS);
it('provides options generated by the getEditorOptions service', () => {
expect(findEditor().props().options).toBe(editorOptions);
});
it('has the correct preview style', () => {
......@@ -65,6 +78,10 @@ describe('Rich Content Editor', () => {
});
describe('when content is changed', () => {
beforeEach(() => {
buildWrapper();
});
it('emits an input event with the changed content', () => {
const changedMarkdown = '## Changed Markdown';
const getMarkdownMock = jest.fn().mockReturnValueOnce(changedMarkdown);
......@@ -77,6 +94,10 @@ describe('Rich Content Editor', () => {
});
describe('when content is reset', () => {
beforeEach(() => {
buildWrapper();
});
it('should reset the content via setMarkdown', () => {
const newContent = 'Just the body content excluding the front matter for example';
const mockInstance = { invoke: jest.fn() };
......@@ -89,35 +110,33 @@ describe('Rich Content Editor', () => {
});
describe('when editor is loaded', () => {
let mockEditorApi;
beforeEach(() => {
mockEditorApi = { eventManager: { addEventType: jest.fn(), listen: jest.fn() } };
findEditor().vm.$emit('load', mockEditorApi);
buildWrapper();
});
it('adds the CUSTOM_EVENTS.openAddImageModal custom event listener', () => {
expect(addCustomEventListener).toHaveBeenCalledWith(
mockEditorApi,
wrapper.vm.editorApi,
CUSTOM_EVENTS.openAddImageModal,
wrapper.vm.onOpenAddImageModal,
);
});
it('registers HTML to markdown renderer', () => {
expect(registerHTMLToMarkdownRenderer).toHaveBeenCalledWith(mockEditorApi);
expect(registerHTMLToMarkdownRenderer).toHaveBeenCalledWith(wrapper.vm.editorApi);
});
});
describe('when editor is destroyed', () => {
it('removes the CUSTOM_EVENTS.openAddImageModal custom event listener', () => {
const mockEditorApi = { eventManager: { removeEventHandler: jest.fn() } };
beforeEach(() => {
buildWrapper();
});
wrapper.vm.editorApi = mockEditorApi;
it('removes the CUSTOM_EVENTS.openAddImageModal custom event listener', () => {
wrapper.vm.$destroy();
expect(removeCustomEventListener).toHaveBeenCalledWith(
mockEditorApi,
wrapper.vm.editorApi,
CUSTOM_EVENTS.openAddImageModal,
wrapper.vm.onOpenAddImageModal,
);
......@@ -125,6 +144,10 @@ describe('Rich Content Editor', () => {
});
describe('add image modal', () => {
beforeEach(() => {
buildWrapper();
});
it('renders an addImageModal component', () => {
expect(findAddImageModal().exists()).toBe(true);
});
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Gitlab::BackgroundMigration::BackfillDesignsRelativePosition do
let(:namespace) { table(:namespaces).create!(name: 'gitlab', path: 'gitlab') }
let(:project) { table(:projects).create!(namespace_id: namespace.id) }
let(:issues) { table(:issues) }
let(:designs) { table(:design_management_designs) }
before do
issues.create!(id: 1, project_id: project.id)
issues.create!(id: 2, project_id: project.id)
issues.create!(id: 3, project_id: project.id)
issues.create!(id: 4, project_id: project.id)
designs.create!(id: 1, issue_id: 1, project_id: project.id, filename: 'design1.jpg')
designs.create!(id: 2, issue_id: 1, project_id: project.id, filename: 'design2.jpg')
designs.create!(id: 3, issue_id: 2, project_id: project.id, filename: 'design3.jpg')
designs.create!(id: 4, issue_id: 2, project_id: project.id, filename: 'design4.jpg')
designs.create!(id: 5, issue_id: 3, project_id: project.id, filename: 'design5.jpg')
end
describe '#perform' do
it 'backfills the position for the designs in each issue' do
expect(described_class::Design).to receive(:move_nulls_to_start).with(
a_collection_containing_exactly(
an_object_having_attributes(id: 1, issue_id: 1),
an_object_having_attributes(id: 2, issue_id: 1)
)
).ordered.and_call_original
expect(described_class::Design).to receive(:move_nulls_to_start).with(
a_collection_containing_exactly(
an_object_having_attributes(id: 3, issue_id: 2),
an_object_having_attributes(id: 4, issue_id: 2)
)
).ordered.and_call_original
# We only expect calls to `move_nulls_to_start` with issues 1 and 2:
# - Issue 3 should be skipped because we're not passing its ID
# - Issue 4 should be skipped because it doesn't have any designs
# - Issue 0 should be skipped because it doesn't exist
subject.perform([1, 2, 4, 0])
expect(designs.find(1).relative_position).to be < designs.find(2).relative_position
expect(designs.find(3).relative_position).to be < designs.find(4).relative_position
expect(designs.find(5).relative_position).to be_nil
end
end
end
......@@ -17,10 +17,9 @@ RSpec.describe Gitlab::UsageDataCounters::TrackUniqueActions, :clean_gitlab_redi
context 'tracking an event' do
context 'when tracking successfully' do
context 'when the feature flag and the application setting is enabled' do
context 'when the application setting is enabled' do
context 'when the target and the action is valid' do
before do
stub_feature_flags(described_class::FEATURE_FLAG => true)
stub_application_setting(usage_ping_enabled: true)
end
......@@ -59,17 +58,15 @@ RSpec.describe Gitlab::UsageDataCounters::TrackUniqueActions, :clean_gitlab_redi
context 'when tracking unsuccessfully' do
using RSpec::Parameterized::TableSyntax
where(:feature_flag, :application_setting, :target, :action) do
true | true | Project | :invalid_action
false | true | Project | :pushed
true | false | Project | :pushed
true | true | :invalid_target | :pushed
where(:application_setting, :target, :action) do
true | Project | :invalid_action
false | Project | :pushed
true | :invalid_target | :pushed
end
with_them do
before do
stub_application_setting(usage_ping_enabled: application_setting)
stub_feature_flags(described_class::FEATURE_FLAG => feature_flag)
end
it 'returns the expected values' do
......
......@@ -912,45 +912,29 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do
let(:time) { Time.zone.now }
before do
stub_feature_flags(Gitlab::UsageDataCounters::TrackUniqueActions::FEATURE_FLAG => feature_flag)
end
context 'when the feature flag is enabled' do
let(:feature_flag) { true }
before do
counter = Gitlab::UsageDataCounters::TrackUniqueActions
project = Event::TARGET_TYPES[:project]
wiki = Event::TARGET_TYPES[:wiki]
design = Event::TARGET_TYPES[:design]
counter.track_event(event_action: :pushed, event_target: project, author_id: 1)
counter.track_event(event_action: :pushed, event_target: project, author_id: 1)
counter.track_event(event_action: :pushed, event_target: project, author_id: 2)
counter.track_event(event_action: :pushed, event_target: project, author_id: 3)
counter.track_event(event_action: :pushed, event_target: project, author_id: 4, time: time - 3.days)
counter.track_event(event_action: :created, event_target: project, author_id: 5, time: time - 3.days)
counter.track_event(event_action: :created, event_target: wiki, author_id: 3)
counter.track_event(event_action: :created, event_target: design, author_id: 3)
end
it 'returns the distinct count of user actions within the specified time period' do
expect(described_class.action_monthly_active_users(time_period)).to eq(
{
action_monthly_active_users_design_management: 1,
action_monthly_active_users_project_repo: 3,
action_monthly_active_users_wiki_repo: 1
}
)
end
end
context 'when the feature flag is disabled' do
let(:feature_flag) { false }
it 'returns an empty hash' do
expect(described_class.action_monthly_active_users(time_period)).to eq({})
end
counter = Gitlab::UsageDataCounters::TrackUniqueActions
project = Event::TARGET_TYPES[:project]
wiki = Event::TARGET_TYPES[:wiki]
design = Event::TARGET_TYPES[:design]
counter.track_event(event_action: :pushed, event_target: project, author_id: 1)
counter.track_event(event_action: :pushed, event_target: project, author_id: 1)
counter.track_event(event_action: :pushed, event_target: project, author_id: 2)
counter.track_event(event_action: :pushed, event_target: project, author_id: 3)
counter.track_event(event_action: :pushed, event_target: project, author_id: 4, time: time - 3.days)
counter.track_event(event_action: :created, event_target: project, author_id: 5, time: time - 3.days)
counter.track_event(event_action: :created, event_target: wiki, author_id: 3)
counter.track_event(event_action: :created, event_target: design, author_id: 3)
end
it 'returns the distinct count of user actions within the specified time period' do
expect(described_class.action_monthly_active_users(time_period)).to eq(
{
action_monthly_active_users_design_management: 1,
action_monthly_active_users_project_repo: 3,
action_monthly_active_users_wiki_repo: 1
}
)
end
end
......
# frozen_string_literal: true
require 'spec_helper'
require Rails.root.join('db', 'post_migrate', '20200724130639_backfill_designs_relative_position.rb')
RSpec.describe BackfillDesignsRelativePosition do
let(:namespace) { table(:namespaces).create!(name: 'gitlab', path: 'gitlab') }
let(:project) { table(:projects).create!(namespace_id: namespace.id) }
let(:issues) { table(:issues) }
let(:designs) { table(:design_management_designs) }
before do
issues.create!(id: 1, project_id: project.id)
issues.create!(id: 2, project_id: project.id)
issues.create!(id: 3, project_id: project.id)
issues.create!(id: 4, project_id: project.id)
designs.create!(issue_id: 1, project_id: project.id, filename: 'design1.jpg')
designs.create!(issue_id: 2, project_id: project.id, filename: 'design2.jpg')
designs.create!(issue_id: 4, project_id: project.id, filename: 'design3.jpg')
stub_const("#{described_class.name}::BATCH_SIZE", 2)
end
it 'correctly schedules background migrations' do
Sidekiq::Testing.fake! do
Timecop.freeze do
migrate!
expect(described_class::MIGRATION)
.to be_scheduled_delayed_migration(2.minutes, [1, 2])
expect(described_class::MIGRATION)
.to be_scheduled_delayed_migration(4.minutes, [4])
# Issue 3 should be skipped because it doesn't have any designs
expect(BackgroundMigrationWorker.jobs.size).to eq(2)
end
end
end
end
......@@ -37,9 +37,11 @@ RSpec.describe DesignManagement::DesignCollection do
it 'inserts the design after any existing designs' do
design1 = collection.find_or_create_design!(filename: 'design1.jpg')
design1.update!(relative_position: 100)
design2 = collection.find_or_create_design!(filename: 'design2.jpg')
expect(design1.relative_position).to be < design2.relative_position
expect(collection.designs.ordered(issue.project)).to eq([design1, design2])
end
end
......
......@@ -74,6 +74,15 @@ RSpec.describe ::Packages::Detail::PackagePresenter do
end
end
context 'with conan metadata' do
let(:package) { create(:conan_package, project: project) }
let(:expected_package_details) { super().merge(conan_metadatum: package.conan_metadatum) }
it 'returns conan_metadatum' do
expect(presenter.detail_view).to eq expected_package_details
end
end
context 'with composer metadata' do
let(:package) { create(:composer_package, :with_metadatum, sha: '123', project: project) }
let(:expected_package_details) { super().merge(composer_metadatum: package.composer_metadatum) }
......
......@@ -202,7 +202,6 @@ RSpec.describe EventCreateService do
end
it 'records the event in the event counter' do
stub_feature_flags(Gitlab::UsageDataCounters::TrackUniqueActions::FEATURE_FLAG => true)
counter_class = Gitlab::UsageDataCounters::TrackUniqueActions
tracking_params = { event_action: counter_class::WIKI_ACTION, date_from: Date.yesterday, date_to: Date.today }
......@@ -244,7 +243,6 @@ RSpec.describe EventCreateService do
it_behaves_like 'service for creating a push event', PushEventPayloadService
it 'records the event in the event counter' do
stub_feature_flags(Gitlab::UsageDataCounters::TrackUniqueActions::FEATURE_FLAG => true)
counter_class = Gitlab::UsageDataCounters::TrackUniqueActions
tracking_params = { event_action: counter_class::PUSH_ACTION, date_from: Date.yesterday, date_to: Date.today }
......@@ -268,7 +266,6 @@ RSpec.describe EventCreateService do
it_behaves_like 'service for creating a push event', BulkPushEventPayloadService
it 'records the event in the event counter' do
stub_feature_flags(Gitlab::UsageDataCounters::TrackUniqueActions::FEATURE_FLAG => true)
counter_class = Gitlab::UsageDataCounters::TrackUniqueActions
tracking_params = { event_action: counter_class::PUSH_ACTION, date_from: Date.yesterday, date_to: Date.today }
......@@ -323,7 +320,6 @@ RSpec.describe EventCreateService do
end
it 'records the event in the event counter' do
stub_feature_flags(Gitlab::UsageDataCounters::TrackUniqueActions::FEATURE_FLAG => true)
counter_class = Gitlab::UsageDataCounters::TrackUniqueActions
tracking_params = { event_action: counter_class::DESIGN_ACTION, date_from: Date.yesterday, date_to: Date.today }
......@@ -351,7 +347,6 @@ RSpec.describe EventCreateService do
end
it 'records the event in the event counter' do
stub_feature_flags(Gitlab::UsageDataCounters::TrackUniqueActions::FEATURE_FLAG => true)
counter_class = Gitlab::UsageDataCounters::TrackUniqueActions
tracking_params = { event_action: counter_class::DESIGN_ACTION, date_from: Date.yesterday, date_to: Date.today }
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe JiraImport::CloudUsersMapperService do
let(:start_at) { 7 }
let(:url) { "/rest/api/2/users?maxResults=50&startAt=#{start_at}" }
let(:jira_users) do
[
{ 'accountId' => 'abcd', 'displayName' => 'user1' },
{ 'accountId' => 'efg' },
{ 'accountId' => 'hij', 'displayName' => 'user3', 'emailAddress' => 'user3@example.com' }
]
end
describe '#execute' do
it_behaves_like 'mapping jira users'
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe JiraImport::ServerUsersMapperService do
let(:start_at) { 7 }
let(:url) { "/rest/api/2/user/search?username=''&maxResults=50&startAt=#{start_at}" }
let(:jira_users) do
[
{ 'key' => 'abcd', 'name' => 'user1' },
{ 'key' => 'efg' },
{ 'key' => 'hij', 'name' => 'user3', 'emailAddress' => 'user3@example.com' }
]
end
describe '#execute' do
it_behaves_like 'mapping jira users'
end
end
......@@ -14,6 +14,27 @@ RSpec.describe JiraImport::UsersImporter do
subject { importer.execute }
describe '#execute' do
let(:mapped_users) do
[
{
jira_account_id: 'acc1',
jira_display_name: 'user1',
jira_email: 'sample@jira.com',
gitlab_id: nil,
gitlab_username: nil,
gitlab_name: nil
},
{
jira_account_id: 'acc2',
jira_display_name: 'user2',
jira_email: nil,
gitlab_id: nil,
gitlab_username: nil,
gitlab_name: nil
}
]
end
before do
stub_jira_service_test
project.add_maintainer(user)
......@@ -25,53 +46,83 @@ RSpec.describe JiraImport::UsersImporter do
end
end
context 'when Jira import is configured correctly' do
let_it_be(:jira_service) { create(:jira_service, project: project, active: true) }
let(:client) { double }
RSpec.shared_examples 'maps jira users to gitlab users' do
context 'when Jira import is configured correctly' do
let_it_be(:jira_service) { create(:jira_service, project: project, active: true) }
let(:client) { double }
before do
expect(importer).to receive(:client).and_return(client)
end
context 'when jira client raises an error' do
it 'returns an error response' do
expect(client).to receive(:get).and_raise(Timeout::Error)
expect(subject.error?).to be_truthy
expect(subject.message).to include('There was an error when communicating to Jira')
end
end
context 'when jira client returns result' do
before do
allow(client).to receive(:get).with('/rest/api/2/users?maxResults=50&startAt=7')
.and_return(jira_users)
expect(importer).to receive(:client).at_least(1).and_return(client)
allow(client).to receive_message_chain(:ServerInfo, :all, :deploymentType).and_return(deployment_type)
end
context 'when jira client returns an empty array' do
let(:jira_users) { [] }
context 'when jira client raises an error' do
it 'returns an error response' do
expect(client).to receive(:get).and_raise(Timeout::Error)
it 'retturns nil payload' do
expect(subject.success?).to be_truthy
expect(subject.payload).to be_nil
expect(subject.error?).to be_truthy
expect(subject.message).to include('There was an error when communicating to Jira')
end
end
context 'when jira client returns an results' do
let(:jira_users) { [{ 'name' => 'user1' }, { 'name' => 'user2' }] }
let(:mapped_users) { [{ jira_display_name: 'user1', gitlab_id: 5 }] }
context 'when jira client returns result' do
context 'when jira client returns an empty array' do
let(:jira_users) { [] }
before do
expect(JiraImport::UsersMapper).to receive(:new).with(project, jira_users)
.and_return(double(execute: mapped_users))
it 'retturns nil payload' do
expect(subject.success?).to be_truthy
expect(subject.payload).to be_empty
end
end
it 'returns the mapped users' do
expect(subject.success?).to be_truthy
expect(subject.payload).to eq(mapped_users)
context 'when jira client returns an results' do
it 'returns the mapped users' do
expect(subject.success?).to be_truthy
expect(subject.payload).to eq(mapped_users)
end
end
end
end
end
context 'when Jira instance is of Server deployment type' do
let(:deployment_type) { 'Server' }
let(:url) { "/rest/api/2/user/search?username=''&maxResults=50&startAt=#{start_at}" }
let(:jira_users) do
[
{ 'key' => 'acc1', 'name' => 'user1', 'emailAddress' => 'sample@jira.com' },
{ 'key' => 'acc2', 'name' => 'user2' }
]
end
before do
allow_next_instance_of(JiraImport::ServerUsersMapperService) do |instance|
allow(instance).to receive(:client).and_return(client)
allow(client).to receive(:get).with(url).and_return(jira_users)
end
end
it_behaves_like 'maps jira users to gitlab users'
end
context 'when Jira instance is of Cloud deploymet type' do
let(:deployment_type) { 'Cloud' }
let(:url) { "/rest/api/2/users?maxResults=50&startAt=#{start_at}" }
let(:jira_users) do
[
{ 'accountId' => 'acc1', 'displayName' => 'user1', 'emailAddress' => 'sample@jira.com' },
{ 'accountId' => 'acc2', 'displayName' => 'user2' }
]
end
before do
allow_next_instance_of(JiraImport::CloudUsersMapperService) do |instance|
allow(instance).to receive(:client).and_return(client)
allow(client).to receive(:get).with(url).and_return(jira_users)
end
end
it_behaves_like 'maps jira users to gitlab users'
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe JiraImport::UsersMapper do
let_it_be(:project) { create(:project) }
subject { described_class.new(project, jira_users).execute }
describe '#execute' do
context 'jira_users is nil' do
let(:jira_users) { nil }
it 'returns an empty array' do
expect(subject).to be_empty
end
end
context 'when jira_users is present' do
let(:jira_users) do
[
{ 'accountId' => 'abcd', 'displayName' => 'user1' },
{ 'accountId' => 'efg' },
{ 'accountId' => 'hij', 'displayName' => 'user3', 'emailAddress' => 'user3@example.com' }
]
end
# TODO: now we only create an array in a proper format
# mapping is tracked in https://gitlab.com/gitlab-org/gitlab/-/issues/219023
let(:mapped_users) do
[
{ jira_account_id: 'abcd', jira_display_name: 'user1', jira_email: nil, gitlab_id: nil, gitlab_username: nil, gitlab_name: nil },
{ jira_account_id: 'efg', jira_display_name: nil, jira_email: nil, gitlab_id: nil, gitlab_username: nil, gitlab_name: nil },
{ jira_account_id: 'hij', jira_display_name: 'user3', jira_email: 'user3@example.com', gitlab_id: nil, gitlab_username: nil, gitlab_name: nil }
]
end
it 'returns users mapped to Gitlab' do
expect(subject).to eq(mapped_users)
end
end
end
end
# frozen_string_literal: true
RSpec.shared_examples 'mapping jira users' do
let(:client) { double }
let_it_be(:project) { create(:project) }
let_it_be(:jira_service) { create(:jira_service, project: project, active: true) }
before do
allow(subject).to receive(:client).and_return(client)
allow(client).to receive(:get).with(url).and_return(jira_users)
end
subject { described_class.new(jira_service, start_at) }
context 'jira_users is nil' do
let(:jira_users) { nil }
it 'returns an empty array' do
expect(subject.execute).to be_empty
end
end
context 'when jira_users is present' do
# TODO: now we only create an array in a proper format
# mapping is tracked in https://gitlab.com/gitlab-org/gitlab/-/issues/219023
let(:mapped_users) do
[
{ jira_account_id: 'abcd', jira_display_name: 'user1', jira_email: nil, gitlab_id: nil, gitlab_username: nil, gitlab_name: nil },
{ jira_account_id: 'efg', jira_display_name: nil, jira_email: nil, gitlab_id: nil, gitlab_username: nil, gitlab_name: nil },
{ jira_account_id: 'hij', jira_display_name: 'user3', jira_email: 'user3@example.com', gitlab_id: nil, gitlab_username: nil, gitlab_name: nil }
]
end
it 'returns users mapped to Gitlab' do
expect(subject.execute).to eq(mapped_users)
end
end
end
......@@ -848,22 +848,22 @@
resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-1.158.0.tgz#300d416184a2b0e05f15a96547f726e1825b08a1"
integrity sha512-5OJl+7TsXN9PJhY6/uwi+mTwmDZa9n/6119rf77orQ/joFYUypaYhBmy/1TcKVPsy5Zs6KCxE1kmGsfoXc1TYA==
"@gitlab/ui@18.7.0":
version "18.7.0"
resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-18.7.0.tgz#aee0054d50e50aaf9e7c4ea4b9e36ca4b97102bf"
integrity sha512-y1Gix1aCHvVO+zh6TCDmsCr97nLLHFnfEZRtg69EBnLBCLgwBcucC3mNeR4Q2EHTWjy/5U035UkyW6LDRX05mA==
"@gitlab/ui@20.1.1":
version "20.1.1"
resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-20.1.1.tgz#990ce3a0883af5c62b0f56be1e0b244b918a9159"
integrity sha512-xtWdvzC33p8i76afHtnQKuUN7fGWV89uIKfIf9/WyygXZqUFKbSW076m/9iLRxHaCYNW7ucJe3fbEW+iAgWcuA==
dependencies:
"@babel/standalone" "^7.0.0"
"@gitlab/vue-toasted" "^1.3.0"
bootstrap-vue "2.13.1"
copy-to-clipboard "^3.0.8"
dompurify "^2.0.12"
echarts "^4.2.1"
highlight.js "^9.13.1"
js-beautify "^1.8.8"
lodash "^4.17.14"
portal-vue "^2.1.6"
resize-observer-polyfill "^1.5.1"
sanitize-html "^1.22.0"
url-search-params-polyfill "^5.0.0"
vue-runtime-helpers "^1.1.2"
......@@ -4094,7 +4094,7 @@ dom-serialize@^2.2.0:
extend "^3.0.0"
void-elements "^2.0.0"
dom-serializer@0, dom-serializer@^0.2.1:
dom-serializer@0:
version "0.2.2"
resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-0.2.2.tgz#1afb81f533717175d478655debc5e332d9f9bb51"
integrity sha512-2/xPb3ORsQ42nHYiSunXkDjPLBaEj/xTwUO4B7XCZQTRk7EBtTOPaygh10YAAh2OI1Qrp6NWfpAhzswj0ydt9g==
......@@ -4143,17 +4143,10 @@ domhandler@^2.3.0:
dependencies:
domelementtype "1"
domhandler@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-3.0.0.tgz#51cd13efca31da95bbb0c5bee3a48300e333b3e9"
integrity sha512-eKLdI5v9m67kbXQbJSNn1zjh0SDzvzWVWtX+qEI3eMjZw8daH9k8rlj1FZY9memPwjiskQFbe7vHVVJIAqoEhw==
dependencies:
domelementtype "^2.0.1"
dompurify@^2.0.11:
version "2.0.11"
resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-2.0.11.tgz#cd47935774230c5e478b183a572e726300b3891d"
integrity sha512-qVoGPjIW9IqxRij7klDQQ2j6nSe4UNWANBhZNLnsS7ScTtLb+3YdxkRY8brNTpkUiTtcXsCJO+jS0UCDfenLuA==
dompurify@^2.0.11, dompurify@^2.0.12:
version "2.0.12"
resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-2.0.12.tgz#284a2b041e1c60b8e72d7b4d2fadad36141254ae"
integrity sha512-Fl8KseK1imyhErHypFPA8qpq9gPzlsJ/EukA6yk9o0gX23p1TzC+rh9LqNg1qvErRTc0UNMYlKxEGSfSh43NDg==
domutils@^1.5.1:
version "1.6.2"
......@@ -4163,15 +4156,6 @@ domutils@^1.5.1:
dom-serializer "0"
domelementtype "1"
domutils@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/domutils/-/domutils-2.0.0.tgz#15b8278e37bfa8468d157478c58c367718133c08"
integrity sha512-n5SelJ1axbO636c2yUtOGia/IcJtVtlhQbFiVDBZHKV5ReJO1ViX7sFEemtuyoAnBxk5meNSYgA8V4s0271efg==
dependencies:
dom-serializer "^0.2.1"
domelementtype "^2.0.1"
domhandler "^3.0.0"
dot-prop@^4.1.1:
version "4.2.0"
resolved "https://registry.yarnpkg.com/dot-prop/-/dot-prop-4.2.0.tgz#1f19e0c2e1aa0e32797c49799f2837ac6af69c57"
......@@ -5843,16 +5827,6 @@ htmlparser2@^3.10.0:
inherits "^2.0.1"
readable-stream "^3.0.6"
htmlparser2@^4.1.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-4.1.0.tgz#9a4ef161f2e4625ebf7dfbe6c0a2f52d18a59e78"
integrity sha512-4zDq1a1zhE4gQso/c5LP1OtrhYTncXNSpvJYtWJBtXAETPlMfi3IFNjGuQbYLuVY4ZR0QMqRVvo4Pdy9KLyP8Q==
dependencies:
domelementtype "^2.0.1"
domhandler "^3.0.0"
domutils "^2.0.0"
entities "^2.0.0"
http-cache-semantics@^4.0.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz#49e91c5cbf36c9b94bcfcd71c23d5249ec74e390"
......@@ -7646,11 +7620,6 @@ lodash.differencewith@~4.5.0:
resolved "https://registry.yarnpkg.com/lodash.differencewith/-/lodash.differencewith-4.5.0.tgz#bafafbc918b55154e179176a00bb0aefaac854b7"
integrity sha1-uvr7yRi1UVTheRdqALsK76rIVLc=
lodash.escaperegexp@^4.1.2:
version "4.1.2"
resolved "https://registry.yarnpkg.com/lodash.escaperegexp/-/lodash.escaperegexp-4.1.2.tgz#64762c48618082518ac3df4ccf5d5886dae20347"
integrity sha1-ZHYsSGGAglGKw99Mz11YhtriA0c=
lodash.find@^4.6.0:
version "4.6.0"
resolved "https://registry.yarnpkg.com/lodash.find/-/lodash.find-4.6.0.tgz#cb0704d47ab71789ffa0de8b97dd926fb88b13b1"
......@@ -7706,11 +7675,6 @@ lodash.isplainobject@^4.0.6:
resolved "https://registry.yarnpkg.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz#7c526a52d89b45c45cc690b88163be0497f550cb"
integrity sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs=
lodash.isstring@^4.0.1:
version "4.0.1"
resolved "https://registry.yarnpkg.com/lodash.isstring/-/lodash.isstring-4.0.1.tgz#d527dfb5456eca7cc9bb95d5daeaf88ba54a5451"
integrity sha1-1SfftUVuynzJu5XV2ur4i6VKVFE=
lodash.kebabcase@4.1.1:
version "4.1.1"
resolved "https://registry.yarnpkg.com/lodash.kebabcase/-/lodash.kebabcase-4.1.1.tgz#8489b1cb0d29ff88195cceca448ff6d6cc295c36"
......@@ -7731,11 +7695,6 @@ lodash.mapvalues@^4.6.0:
resolved "https://registry.yarnpkg.com/lodash.mapvalues/-/lodash.mapvalues-4.6.0.tgz#1bafa5005de9dd6f4f26668c30ca37230cc9689c"
integrity sha1-G6+lAF3p3W9PJmaMMMo3IwzJaJw=
lodash.mergewith@^4.6.1:
version "4.6.2"
resolved "https://registry.yarnpkg.com/lodash.mergewith/-/lodash.mergewith-4.6.2.tgz#617121f89ac55f59047c7aec1ccd6654c6590f55"
integrity sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ==
lodash.pick@^4.4.0:
version "4.4.0"
resolved "https://registry.yarnpkg.com/lodash.pick/-/lodash.pick-4.4.0.tgz#52f05610fff9ded422611441ed1fc123a03001b3"
......@@ -9500,7 +9459,7 @@ postcss-value-parser@^4.0.0:
resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.0.0.tgz#99a983d365f7b2ad8d0f9b8c3094926eab4b936d"
integrity sha512-ESPktioptiSUchCKgggAkzdmkgzKfmp0EU8jXH+5kbIUB+unr0Y4CY9SRMvibuvYUBjNh1ACLbxqYNpdTQOteQ==
postcss@^7.0.0, postcss@^7.0.1, postcss@^7.0.14, postcss@^7.0.17, postcss@^7.0.2, postcss@^7.0.27, postcss@^7.0.5, postcss@^7.0.6, postcss@^7.0.7:
postcss@^7.0.0, postcss@^7.0.1, postcss@^7.0.14, postcss@^7.0.17, postcss@^7.0.2, postcss@^7.0.5, postcss@^7.0.6, postcss@^7.0.7:
version "7.0.30"
resolved "https://registry.yarnpkg.com/postcss/-/postcss-7.0.30.tgz#cc9378beffe46a02cbc4506a0477d05fcea9a8e2"
integrity sha512-nu/0m+NtIzoubO+xdAlwZl/u5S5vi/y6BCsoL8D+8IxsD3XvBS8X4YEADNIVXKVuQvduiucnRv+vPIqj56EGMQ==
......@@ -10453,22 +10412,6 @@ sane@^4.0.3:
minimist "^1.1.1"
walker "~1.0.5"
sanitize-html@^1.22.0:
version "1.22.0"
resolved "https://registry.yarnpkg.com/sanitize-html/-/sanitize-html-1.22.0.tgz#9df779c53cf5755adb2322943c21c1c1dffca7bf"
integrity sha512-3RPo65mbTKpOAdAYWU496MSty1YbB3Y5bjwL5OclgaSSMtv65xvM7RW/EHRumzaZ1UddEJowCbSdK0xl5sAu0A==
dependencies:
chalk "^2.4.1"
htmlparser2 "^4.1.0"
lodash.clonedeep "^4.5.0"
lodash.escaperegexp "^4.1.2"
lodash.isplainobject "^4.0.6"
lodash.isstring "^4.0.1"
lodash.mergewith "^4.6.1"
postcss "^7.0.27"
srcset "^2.0.1"
xtend "^4.0.1"
sass-graph@^2.2.4:
version "2.2.4"
resolved "https://registry.yarnpkg.com/sass-graph/-/sass-graph-2.2.4.tgz#13fbd63cd1caf0908b9fd93476ad43a51d1e0b49"
......@@ -10968,11 +10911,6 @@ sql.js@^0.4.0:
resolved "https://registry.yarnpkg.com/sql.js/-/sql.js-0.4.0.tgz#23be9635520eb0ff43a741e7e830397266e88445"
integrity sha1-I76WNVIOsP9Dp0Hn6DA5cmbohEU=
srcset@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/srcset/-/srcset-2.0.1.tgz#8f842d357487eb797f413d9c309de7a5149df5ac"
integrity sha512-00kZI87TdRKwt+P8jj8UZxbfp7mK2ufxcIMWvhAOZNJTRROimpHeruWrGvCZneiuVDLqdyHefVp748ECTnyUBQ==
sshpk@^1.7.0:
version "1.15.2"
resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.15.2.tgz#c946d6bd9b1a39d0e8635763f5242d6ed6dcb629"
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册