Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
f10eb9ebae
commit
d9e07a155e
|
@ -7,6 +7,7 @@
|
|||
"ul-style": {
|
||||
"style": "dash"
|
||||
},
|
||||
"no-trailing-spaces": false,
|
||||
"line-length": false,
|
||||
"no-duplicate-header": {
|
||||
"allow_different_nesting": true
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
/* 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"
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
<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,
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
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>
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
<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">
|
||||
|
|
|
@ -1,13 +1,11 @@
|
|||
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();
|
||||
},
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
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];
|
||||
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 mapValues(renderersByType, renderers => {
|
||||
return (node, context) => executeRenderer(renderers, node, context);
|
||||
});
|
||||
|
||||
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);
|
||||
},
|
||||
};
|
||||
|
||||
return {
|
||||
...buildCustomRendererFunctions(customRenderers, defaults),
|
||||
...defaults,
|
||||
};
|
||||
};
|
||||
|
||||
export default buildCustomHTMLRenderer;
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
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?
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
# 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
|
|
@ -0,0 +1,19 @@
|
|||
# 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 url
|
||||
"/rest/api/2/users?maxResults=#{MAX_USERS}&startAt=#{start_at.to_i}"
|
||||
def users_mapper_service
|
||||
@users_mapper_service ||= user_mapper_service_factory
|
||||
end
|
||||
|
||||
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
|
||||
|
|
|
@ -1,30 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module JiraImport
|
||||
class UsersMapper
|
||||
attr_reader :project, :jira_users
|
||||
|
||||
def initialize(project, jira_users)
|
||||
@project = project
|
||||
@jira_users = jira_users
|
||||
end
|
||||
|
||||
def execute
|
||||
jira_users.to_a.map do |jira_user|
|
||||
{
|
||||
jira_account_id: jira_user['accountId'],
|
||||
jira_display_name: jira_user['displayName'],
|
||||
jira_email: jira_user['emailAddress']
|
||||
}.merge(match_user(jira_user))
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# 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)
|
||||
{ gitlab_id: nil, gitlab_username: nil, gitlab_name: nil }
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,52 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module JiraImport
|
||||
class UsersMapperService
|
||||
MAX_USERS = 50
|
||||
|
||||
attr_reader :jira_service, :start_at
|
||||
|
||||
def initialize(jira_service, start_at)
|
||||
@jira_service = jira_service
|
||||
@start_at = start_at
|
||||
end
|
||||
|
||||
def execute
|
||||
users.to_a.map do |jira_user|
|
||||
{
|
||||
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
|
||||
end
|
||||
|
||||
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)
|
||||
{ gitlab_id: nil, gitlab_username: nil, gitlab_name: nil }
|
||||
end
|
||||
end
|
||||
end
|
|
@ -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
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Replace fa-close icons with GitLab SVG close icon
|
||||
merge_request: 39267
|
||||
author:
|
||||
type: changed
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Fix Conan recipe display in the package details page
|
||||
merge_request: 39643
|
||||
author:
|
||||
type: fixed
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Handle user mapping for Jira server instances
|
||||
merge_request: 39362
|
||||
author:
|
||||
type: fixed
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Deprecate additions and deletions attributes in Repositories API
|
||||
merge_request: 39653
|
||||
author:
|
||||
type: deprecated
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Enable reorder_designs feature by default
|
||||
merge_request: 39555
|
||||
author:
|
||||
type: changed
|
|
@ -0,0 +1,6 @@
|
|||
---
|
||||
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
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Fix missing scoped label borders for todos
|
||||
merge_request: 39459
|
||||
author:
|
||||
type: fixed
|
|
@ -0,0 +1,7 @@
|
|||
---
|
||||
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
|
|
@ -0,0 +1,7 @@
|
|||
---
|
||||
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
|
|
@ -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)
|
||||
|
|
|
@ -1,38 +1,19 @@
|
|||
# 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
|
||||
|
||||
>> 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[title]=%5BFeature+flag%5D+Rollout+of+%60my-feature-flag%60&
|
||||
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
|
||||
|
||||
>> Paste URL here, or enter to skip:
|
||||
|
||||
?>
|
||||
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
|
||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 41 KiB |
|
@ -1,7 +1,7 @@
|
|||
---
|
||||
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 ""
|
||||
|
||||
|
|
|
@ -43,7 +43,7 @@
|
|||
"@babel/preset-env": "^7.10.1",
|
||||
"@gitlab/at.js": "1.5.5",
|
||||
"@gitlab/svgs": "1.158.0",
|
||||
"@gitlab/ui": "18.7.0",
|
||||
"@gitlab/ui": "20.1.1",
|
||||
"@gitlab/visual-review-tools": "1.6.1",
|
||||
"@rails/actioncable": "^6.0.3-1",
|
||||
"@sentry/browser": "^5.10.2",
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
|
|
|
@ -1,51 +0,0 @@
|
|||
# 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)
|
||||
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
|
||||
|
||||
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
|
||||
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
|
||||
|
||||
|
|
|
@ -1,41 +0,0 @@
|
|||
# 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 }
|
||||
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
# 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
|
|
@ -0,0 +1,19 @@
|
|||
# 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
|
||||
|
|
|
@ -1,43 +0,0 @@
|
|||
# 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
|
|
@ -0,0 +1,39 @@
|
|||
# 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
|
84
yarn.lock
84
yarn.lock
|
@ -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"
|
||||
|
|
Loading…
Reference in New Issue