Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
37ae6b54ba
commit
95e18e3283
77 changed files with 1228 additions and 929 deletions
|
@ -17,10 +17,11 @@ function showTooltip(target, title) {
|
|||
}
|
||||
|
||||
function genericSuccess(e) {
|
||||
showTooltip(e.trigger, __('Copied'));
|
||||
// Clear the selection and blur the trigger so it loses its border
|
||||
e.clearSelection();
|
||||
$(e.trigger).blur();
|
||||
|
||||
showTooltip(e.trigger, __('Copied'));
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -164,48 +164,7 @@ class List {
|
|||
}
|
||||
|
||||
addIssue(issue, listFrom, newIndex) {
|
||||
let moveBeforeId = null;
|
||||
let moveAfterId = null;
|
||||
|
||||
if (!this.findIssue(issue.id)) {
|
||||
if (newIndex !== undefined) {
|
||||
this.issues.splice(newIndex, 0, issue);
|
||||
|
||||
if (this.issues[newIndex - 1]) {
|
||||
moveBeforeId = this.issues[newIndex - 1].id;
|
||||
}
|
||||
|
||||
if (this.issues[newIndex + 1]) {
|
||||
moveAfterId = this.issues[newIndex + 1].id;
|
||||
}
|
||||
} else {
|
||||
this.issues.push(issue);
|
||||
}
|
||||
|
||||
if (this.label) {
|
||||
issue.addLabel(this.label);
|
||||
}
|
||||
|
||||
if (this.assignee) {
|
||||
if (listFrom && listFrom.type === 'assignee') {
|
||||
issue.removeAssignee(listFrom.assignee);
|
||||
}
|
||||
issue.addAssignee(this.assignee);
|
||||
}
|
||||
|
||||
if (IS_EE && this.milestone) {
|
||||
if (listFrom && listFrom.type === 'milestone') {
|
||||
issue.removeMilestone(listFrom.milestone);
|
||||
}
|
||||
issue.addMilestone(this.milestone);
|
||||
}
|
||||
|
||||
if (listFrom) {
|
||||
this.issuesSize += 1;
|
||||
|
||||
this.updateIssueLabel(issue, listFrom, moveBeforeId, moveAfterId);
|
||||
}
|
||||
}
|
||||
boardsStore.addListIssue(this, issue, listFrom, newIndex);
|
||||
}
|
||||
|
||||
moveIssue(issue, oldIndex, newIndex, moveBeforeId, moveAfterId) {
|
||||
|
|
|
@ -125,6 +125,50 @@ const boardsStore = {
|
|||
path: '',
|
||||
});
|
||||
},
|
||||
addListIssue(list, issue, listFrom, newIndex) {
|
||||
let moveBeforeId = null;
|
||||
let moveAfterId = null;
|
||||
|
||||
if (!list.findIssue(issue.id)) {
|
||||
if (newIndex !== undefined) {
|
||||
list.issues.splice(newIndex, 0, issue);
|
||||
|
||||
if (list.issues[newIndex - 1]) {
|
||||
moveBeforeId = list.issues[newIndex - 1].id;
|
||||
}
|
||||
|
||||
if (list.issues[newIndex + 1]) {
|
||||
moveAfterId = list.issues[newIndex + 1].id;
|
||||
}
|
||||
} else {
|
||||
list.issues.push(issue);
|
||||
}
|
||||
|
||||
if (list.label) {
|
||||
issue.addLabel(list.label);
|
||||
}
|
||||
|
||||
if (list.assignee) {
|
||||
if (listFrom && listFrom.type === 'assignee') {
|
||||
issue.removeAssignee(listFrom.assignee);
|
||||
}
|
||||
issue.addAssignee(list.assignee);
|
||||
}
|
||||
|
||||
if (IS_EE && list.milestone) {
|
||||
if (listFrom && listFrom.type === 'milestone') {
|
||||
issue.removeMilestone(listFrom.milestone);
|
||||
}
|
||||
issue.addMilestone(list.milestone);
|
||||
}
|
||||
|
||||
if (listFrom) {
|
||||
list.issuesSize += 1;
|
||||
|
||||
list.updateIssueLabel(issue, listFrom, moveBeforeId, moveAfterId);
|
||||
}
|
||||
}
|
||||
},
|
||||
welcomeIsHidden() {
|
||||
return parseBoolean(Cookies.get('issue_board_welcome_hidden'));
|
||||
},
|
||||
|
|
|
@ -437,7 +437,11 @@ 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 }) {
|
||||
const { line_code, ...diffPositionCopy } = diffPosition;
|
||||
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,
|
||||
// once this is addressed. see https://gitlab.com/gitlab-org/gitlab/-/issues/213010
|
||||
const { line_range: dpNotUsed, ...diffPositionCopy } = dp;
|
||||
|
||||
if (discussion.original_position && discussion.position) {
|
||||
const discussionPositions = [
|
||||
|
@ -446,7 +450,14 @@ export function isDiscussionApplicableToLine({ discussion, diffPosition, latestD
|
|||
...(discussion.positions || []),
|
||||
];
|
||||
|
||||
return discussionPositions.some(position => isEqual(position, diffPositionCopy));
|
||||
const removeLineRange = position => {
|
||||
const { line_range: pNotUsed, ...positionNoLineRange } = position;
|
||||
return positionNoLineRange;
|
||||
};
|
||||
|
||||
return discussionPositions
|
||||
.map(removeLineRange)
|
||||
.some(position => isEqual(position, diffPositionCopy));
|
||||
}
|
||||
|
||||
// eslint-disable-next-line
|
||||
|
|
|
@ -95,22 +95,18 @@ export default {
|
|||
@click="onClickCollapsedIcon"
|
||||
>
|
||||
<i class="fa fa-users" aria-hidden="true"> </i>
|
||||
<gl-loading-icon v-if="loading" class="js-participants-collapsed-loading-icon" />
|
||||
<span v-else class="js-participants-collapsed-count"> {{ participantCount }} </span>
|
||||
<gl-loading-icon v-if="loading" />
|
||||
<span v-else data-testid="collapsed-count"> {{ participantCount }} </span>
|
||||
</div>
|
||||
<div v-if="showParticipantLabel" class="title hide-collapsed">
|
||||
<gl-loading-icon
|
||||
v-if="loading"
|
||||
:inline="true"
|
||||
class="js-participants-expanded-loading-icon"
|
||||
/>
|
||||
<gl-loading-icon v-if="loading" :inline="true" />
|
||||
{{ participantLabel }}
|
||||
</div>
|
||||
<div class="participants-list hide-collapsed">
|
||||
<div
|
||||
v-for="participant in visibleParticipants"
|
||||
:key="participant.id"
|
||||
class="participants-author js-participants-author"
|
||||
class="participants-author"
|
||||
>
|
||||
<a :href="participant.web_url" class="author-link">
|
||||
<user-avatar-image
|
||||
|
@ -125,11 +121,7 @@ export default {
|
|||
</div>
|
||||
</div>
|
||||
<div v-if="hasMoreParticipants" class="participants-more hide-collapsed">
|
||||
<button
|
||||
type="button"
|
||||
class="btn-transparent btn-link js-toggle-participants-button"
|
||||
@click="toggleMoreParticipants"
|
||||
>
|
||||
<button type="button" class="btn-transparent btn-link" @click="toggleMoreParticipants">
|
||||
{{ toggleLabel }}
|
||||
</button>
|
||||
</div>
|
||||
|
|
|
@ -67,6 +67,7 @@ export default {
|
|||
<template>
|
||||
<gl-deprecated-button
|
||||
v-gl-tooltip="{ placement: tooltipPlacement, container: tooltipContainer }"
|
||||
v-gl-tooltip.hover.blur
|
||||
:class="cssClass"
|
||||
:title="title"
|
||||
:data-clipboard-text="clipboardText"
|
||||
|
|
|
@ -0,0 +1,30 @@
|
|||
<script>
|
||||
import 'codemirror/lib/codemirror.css';
|
||||
import '@toast-ui/editor/dist/toastui-editor.css';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
ToastEditor: () =>
|
||||
import(/* webpackChunkName: 'toast_editor' */ '@toast-ui/vue-editor').then(
|
||||
toast => toast.Editor,
|
||||
),
|
||||
},
|
||||
props: {
|
||||
value: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
onContentChanged() {
|
||||
this.$emit('input', this.getMarkdown());
|
||||
},
|
||||
getMarkdown() {
|
||||
return this.$refs.editor.invoke('getMarkdown');
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<toast-editor ref="editor" :initial-value="value" @change="onContentChanged" />
|
||||
</template>
|
|
@ -58,8 +58,6 @@ module SearchHelper
|
|||
ns_('SearchResults|comment', 'SearchResults|comments', count)
|
||||
when 'projects'
|
||||
ns_('SearchResults|project', 'SearchResults|projects', count)
|
||||
when 'snippet_blobs'
|
||||
ns_('SearchResults|snippet result', 'SearchResults|snippet results', count)
|
||||
when 'snippet_titles'
|
||||
ns_('SearchResults|snippet', 'SearchResults|snippets', count)
|
||||
when 'users'
|
||||
|
|
|
@ -42,72 +42,6 @@ module SnippetsHelper
|
|||
(lower..upper).to_a
|
||||
end
|
||||
|
||||
# Returns a sorted set of lines to be included in a snippet preview.
|
||||
# This ensures matching adjacent lines do not display duplicated
|
||||
# surrounding code.
|
||||
#
|
||||
# @returns Array, unique and sorted.
|
||||
def matching_lines(lined_content, surrounding_lines, query)
|
||||
used_lines = []
|
||||
lined_content.each_with_index do |line, line_number|
|
||||
used_lines.concat bounded_line_numbers(
|
||||
line_number,
|
||||
0,
|
||||
lined_content.size,
|
||||
surrounding_lines
|
||||
) if line.downcase.include?(query.downcase)
|
||||
end
|
||||
|
||||
used_lines.uniq.sort
|
||||
end
|
||||
|
||||
# 'Chunkify' entire snippet. Splits the snippet data into matching lines +
|
||||
# surrounding_lines() worth of unmatching lines.
|
||||
#
|
||||
# @returns a hash with {snippet_object, snippet_chunks:{data,start_line}}
|
||||
def chunk_snippet(snippet, query, surrounding_lines = 3)
|
||||
lined_content = snippet.content.split("\n")
|
||||
used_lines = matching_lines(lined_content, surrounding_lines, query)
|
||||
|
||||
snippet_chunk = []
|
||||
snippet_chunks = []
|
||||
snippet_start_line = 0
|
||||
last_line = -1
|
||||
|
||||
# Go through each used line, and add consecutive lines as a single chunk
|
||||
# to the snippet chunk array.
|
||||
used_lines.each do |line_number|
|
||||
if last_line < 0
|
||||
# Start a new chunk.
|
||||
snippet_start_line = line_number
|
||||
snippet_chunk << lined_content[line_number]
|
||||
elsif last_line == line_number - 1
|
||||
# Consecutive line, continue chunk.
|
||||
snippet_chunk << lined_content[line_number]
|
||||
else
|
||||
# Non-consecutive line, add chunk to chunk array.
|
||||
snippet_chunks << {
|
||||
data: snippet_chunk.join("\n"),
|
||||
start_line: snippet_start_line + 1
|
||||
}
|
||||
|
||||
# Start a new chunk.
|
||||
snippet_chunk = [lined_content[line_number]]
|
||||
snippet_start_line = line_number
|
||||
end
|
||||
|
||||
last_line = line_number
|
||||
end
|
||||
# Add final chunk to chunk array
|
||||
snippet_chunks << {
|
||||
data: snippet_chunk.join("\n"),
|
||||
start_line: snippet_start_line + 1
|
||||
}
|
||||
|
||||
# Return snippet with chunk array
|
||||
{ snippet_object: snippet, snippet_chunks: snippet_chunks }
|
||||
end
|
||||
|
||||
def snippet_embed_tag(snippet)
|
||||
content_tag(:script, nil, src: gitlab_snippet_url(snippet, format: :js))
|
||||
end
|
||||
|
|
|
@ -1786,7 +1786,6 @@ class User < ApplicationRecord
|
|||
end
|
||||
|
||||
def check_email_restrictions
|
||||
return unless Feature.enabled?(:email_restrictions)
|
||||
return unless Gitlab::CurrentSettings.email_restrictions_enabled?
|
||||
|
||||
restrictions = Gitlab::CurrentSettings.email_restrictions
|
||||
|
|
|
@ -91,6 +91,8 @@ class IssuableBaseService < BaseService
|
|||
elsif params[label_key]
|
||||
params[label_id_key] = labels_service.find_or_create_by_titles(label_key, find_only: find_only).map(&:id)
|
||||
end
|
||||
|
||||
params.delete(label_key) if params[label_key].nil?
|
||||
end
|
||||
|
||||
def filter_labels_in_param(key)
|
||||
|
|
|
@ -49,20 +49,19 @@
|
|||
= f.label :domain_blacklist, 'Blacklisted domains for sign-ups', class: 'label-bold'
|
||||
= f.text_area :domain_blacklist_raw, placeholder: 'domain.com', class: 'form-control', rows: 8
|
||||
.form-text.text-muted Users with e-mail addresses that match these domain(s) will NOT be able to sign-up. Wildcards allowed. Use separate lines for multiple entries. Ex: domain.com, *.domain.com
|
||||
- if Feature.enabled?(:email_restrictions)
|
||||
.form-group
|
||||
= f.label :email_restrictions_enabled, _('Email restrictions'), class: 'label-bold'
|
||||
.form-check
|
||||
= f.check_box :email_restrictions_enabled, class: 'form-check-input'
|
||||
= f.label :email_restrictions_enabled, class: 'form-check-label' do
|
||||
= _('Enable email restrictions for sign ups')
|
||||
.form-group
|
||||
= f.label :email_restrictions, _('Email restrictions for sign-ups'), class: 'label-bold'
|
||||
= f.text_area :email_restrictions, class: 'form-control', rows: 4
|
||||
.form-text.text-muted
|
||||
- supported_syntax_link_url = 'https://github.com/google/re2/wiki/Syntax'
|
||||
- supported_syntax_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: supported_syntax_link_url }
|
||||
= _('Restricts sign-ups for email addresses that match the given regex. See the %{supported_syntax_link_start}supported syntax%{supported_syntax_link_end} for more information.').html_safe % { supported_syntax_link_start: supported_syntax_link_start, supported_syntax_link_end: '</a>'.html_safe }
|
||||
.form-group
|
||||
= f.label :email_restrictions_enabled, _('Email restrictions'), class: 'label-bold'
|
||||
.form-check
|
||||
= f.check_box :email_restrictions_enabled, class: 'form-check-input'
|
||||
= f.label :email_restrictions_enabled, class: 'form-check-label' do
|
||||
= _('Enable email restrictions for sign ups')
|
||||
.form-group
|
||||
= f.label :email_restrictions, _('Email restrictions for sign-ups'), class: 'label-bold'
|
||||
= f.text_area :email_restrictions, class: 'form-control', rows: 4
|
||||
.form-text.text-muted
|
||||
- supported_syntax_link_url = 'https://github.com/google/re2/wiki/Syntax'
|
||||
- supported_syntax_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: supported_syntax_link_url }
|
||||
= _('Restricts sign-ups for email addresses that match the given regex. See the %{supported_syntax_link_start}supported syntax%{supported_syntax_link_end} for more information.').html_safe % { supported_syntax_link_start: supported_syntax_link_start, supported_syntax_link_end: '</a>'.html_safe }
|
||||
|
||||
.form-group
|
||||
= f.label :after_sign_up_text, class: 'label-bold'
|
||||
|
|
|
@ -24,7 +24,7 @@
|
|||
= users
|
||||
|
||||
- elsif @show_snippets
|
||||
= search_filter_link 'snippet_titles', _("Titles and Filenames"), search: { snippets: true, group_id: nil, project_id: nil }
|
||||
= search_filter_link 'snippet_titles', _("Titles and Descriptions"), search: { snippets: true, group_id: nil, project_id: nil }
|
||||
- else
|
||||
= search_filter_link 'projects', _("Projects"), data: { qa_selector: 'projects_tab' }
|
||||
= search_filter_link 'issues', _("Issues")
|
||||
|
|
|
@ -1,50 +0,0 @@
|
|||
- snippet_blob = chunk_snippet(snippet_blob, @search_term)
|
||||
- snippet = snippet_blob[:snippet_object]
|
||||
- snippet_chunks = snippet_blob[:snippet_chunks]
|
||||
- snippet_path = gitlab_snippet_path(snippet)
|
||||
|
||||
.search-result-row.snippet-row
|
||||
= image_tag avatar_icon_for_user(snippet.author), class: "avatar s40 d-none d-sm-block", alt: ''
|
||||
.title
|
||||
= link_to gitlab_snippet_path(snippet) do
|
||||
= snippet.title
|
||||
.snippet-info
|
||||
= snippet.to_reference
|
||||
·
|
||||
authored
|
||||
= time_ago_with_tooltip(snippet.created_at)
|
||||
by
|
||||
= link_to user_snippets_path(snippet.author) do
|
||||
= snippet.author_name
|
||||
|
||||
.file-holder.my-2
|
||||
.js-file-title.file-title-flex-parent
|
||||
= link_to snippet_path do
|
||||
%i.fa.fa-file
|
||||
%strong= snippet.file_name
|
||||
- if markup?(snippet.file_name)
|
||||
.file-content.md
|
||||
- snippet_chunks.each do |chunk|
|
||||
- unless chunk[:data].empty?
|
||||
= markup(snippet.file_name, chunk[:data])
|
||||
- else
|
||||
.file-content.code
|
||||
.nothing-here-block= _("Empty file")
|
||||
- else
|
||||
.file-content.code.js-syntax-highlight
|
||||
.line-numbers
|
||||
- snippet_chunks.each do |chunk|
|
||||
- unless chunk[:data].empty?
|
||||
- Gitlab::Git::Util.count_lines(chunk[:data]).times do |index|
|
||||
- offset = defined?(chunk[:start_line]) ? chunk[:start_line] : 1
|
||||
- i = index + offset
|
||||
= link_to snippet_path+"#L#{i}", id: "L#{i}", rel: "#L#{i}", class: "diff-line-num" do
|
||||
%i.fa.fa-link
|
||||
= i
|
||||
.blob-content
|
||||
- snippet_chunks.each do |chunk|
|
||||
- unless chunk[:data].empty?
|
||||
= highlight(snippet.file_name, chunk[:data])
|
||||
- else
|
||||
.file-content.code
|
||||
.nothing-here-block= _("Empty file")
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Add non_archived argument to issues API endpoint
|
||||
merge_request: 30381
|
||||
author:
|
||||
type: added
|
5
changelogs/unreleased/216001-fix-copy-button-hover.yml
Normal file
5
changelogs/unreleased/216001-fix-copy-button-hover.yml
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Fixes overlapping tooltips when clicking copy buttons
|
||||
merge_request: 30622
|
||||
author:
|
||||
type: fixed
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: removes store logic from issue board models
|
||||
merge_request: 21408
|
||||
author: nuwe1
|
||||
type: other
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: backfill environment_id on deployment_merge_requests
|
||||
merge_request: 27219
|
||||
author:
|
||||
type: other
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Add option to restrict emails that match a configured regular expression
|
||||
merge_request: 30548
|
||||
author:
|
||||
type: added
|
5
changelogs/unreleased/vij-snippet-blob-search-views.yml
Normal file
5
changelogs/unreleased/vij-snippet-blob-search-views.yml
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Rename Snippet search results title
|
||||
merge_request: 29599
|
||||
author:
|
||||
type: other
|
|
@ -17,7 +17,6 @@ If you want to create a changelog entry for GitLab EE, run the following instead
|
|||
bin/changelog --ee -m %<mr_iid>s "%<mr_title>s"
|
||||
```
|
||||
|
||||
Note: Merge requests with %<labels>s do not trigger this check.
|
||||
MSG
|
||||
|
||||
def check_changelog_yaml(path)
|
||||
|
@ -57,7 +56,7 @@ end
|
|||
|
||||
if git.modified_files.include?("CHANGELOG.md")
|
||||
fail "**CHANGELOG.md was edited.** Please remove the additions and create a CHANGELOG entry.\n\n" +
|
||||
format(CREATE_CHANGELOG_MESSAGE, mr_iid: gitlab.mr_json["iid"], mr_title: sanitized_mr_title, labels: changelog.presented_no_changelog_labels)
|
||||
format(CREATE_CHANGELOG_MESSAGE, mr_iid: gitlab.mr_json["iid"], mr_title: sanitized_mr_title)
|
||||
end
|
||||
|
||||
changelog_found = changelog.found
|
||||
|
@ -67,5 +66,5 @@ if changelog_found
|
|||
check_changelog_path(changelog_found)
|
||||
elsif changelog.needed?
|
||||
message "**[CHANGELOG missing](https://docs.gitlab.com/ee/development/changelog.html)**: If this merge request [doesn't need a CHANGELOG entry](https://docs.gitlab.com/ee/development/changelog.html#what-warrants-a-changelog-entry), feel free to ignore this message.\n\n" +
|
||||
format(CREATE_CHANGELOG_MESSAGE, mr_iid: gitlab.mr_json["iid"], mr_title: sanitized_mr_title, labels: changelog.presented_no_changelog_labels)
|
||||
format(CREATE_CHANGELOG_MESSAGE, mr_iid: gitlab.mr_json["iid"], mr_title: sanitized_mr_title)
|
||||
end
|
||||
|
|
|
@ -7,14 +7,10 @@ That's OK as long as you're refactoring existing code,
|
|||
but please consider adding any of the %<labels>s labels.
|
||||
MSG
|
||||
|
||||
def presented_no_changelog_labels
|
||||
NO_SPECS_LABELS.map { |label| "~#{label}" }.join(', ')
|
||||
end
|
||||
|
||||
has_app_changes = !helper.all_changed_files.grep(%r{\A(ee/)?(app|lib|db/(geo/)?(post_)?migrate)/}).empty?
|
||||
has_spec_changes = !helper.all_changed_files.grep(%r{\A(ee/)?spec/}).empty?
|
||||
new_specs_needed = (gitlab.mr_labels & NO_SPECS_LABELS).empty?
|
||||
|
||||
if has_app_changes && !has_spec_changes && new_specs_needed
|
||||
warn format(NO_NEW_SPEC_MESSAGE, labels: presented_no_changelog_labels), sticky: false
|
||||
warn format(NO_NEW_SPEC_MESSAGE, labels: helper.labels_list(NO_SPECS_LABELS)), sticky: false
|
||||
end
|
||||
|
|
|
@ -19,7 +19,6 @@ class DropForkedProjectLinksFk < ActiveRecord::Migration[6.0]
|
|||
unless foreign_key_exists?(:forked_project_links, :projects, column: :forked_to_project_id)
|
||||
# rubocop: disable Migration/WithLockRetriesWithoutDdlTransaction
|
||||
with_lock_retries do
|
||||
# rubocop: disable Migration/AddConcurrentForeignKey
|
||||
add_foreign_key :forked_project_links, :projects, column: :forked_to_project_id, on_delete: :cascade, validate: false
|
||||
end
|
||||
# rubocop: enable Migration/WithLockRetriesWithoutDdlTransaction
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# rubocop: disable Migration/AddConcurrentForeignKey
|
||||
# rubocop: disable Migration/WithLockRetriesWithoutDdlTransaction
|
||||
class AddProtectedTagCreateAccessLevelsUserIdForeignKey < ActiveRecord::Migration[6.0]
|
||||
include Gitlab::Database::MigrationHelpers
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# rubocop: disable Migration/AddConcurrentForeignKey
|
||||
# rubocop: disable Migration/WithLockRetriesWithoutDdlTransaction
|
||||
class AddProtectedBranchMergeAccessLevelsUserIdForeignKey < ActiveRecord::Migration[6.0]
|
||||
include Gitlab::Database::MigrationHelpers
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# rubocop: disable Migration/AddConcurrentForeignKey
|
||||
# rubocop: disable Migration/WithLockRetriesWithoutDdlTransaction
|
||||
class AddPathLocksUserIdForeignKey < ActiveRecord::Migration[6.0]
|
||||
include Gitlab::Database::MigrationHelpers
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# rubocop: disable Migration/AddConcurrentForeignKey
|
||||
# rubocop: disable Migration/WithLockRetriesWithoutDdlTransaction
|
||||
class AddProtectedBranchPushAccessLevelsUserIdForeignKey < ActiveRecord::Migration[6.0]
|
||||
include Gitlab::Database::MigrationHelpers
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# rubocop: disable Migration/AddConcurrentForeignKey
|
||||
# rubocop: disable Migration/WithLockRetriesWithoutDdlTransaction
|
||||
class AddU2fRegistrationsUserIdForeignKey < ActiveRecord::Migration[6.0]
|
||||
include Gitlab::Database::MigrationHelpers
|
||||
|
|
|
@ -0,0 +1,54 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class BackfillEnvironmentIdOnDeploymentMergeRequests < ActiveRecord::Migration[6.0]
|
||||
include Gitlab::Database::MigrationHelpers
|
||||
|
||||
DOWNTIME = false
|
||||
BATCH_SIZE = 400
|
||||
DELAY = 1.minute
|
||||
|
||||
disable_ddl_transaction!
|
||||
|
||||
def up
|
||||
max_mr_id = DeploymentMergeRequest
|
||||
.select(:merge_request_id)
|
||||
.distinct
|
||||
.order(merge_request_id: :desc)
|
||||
.limit(1)
|
||||
.pluck(:merge_request_id)
|
||||
.first || 0
|
||||
|
||||
last_mr_id = 0
|
||||
step = 0
|
||||
|
||||
while last_mr_id < max_mr_id
|
||||
stop =
|
||||
DeploymentMergeRequest
|
||||
.select(:merge_request_id)
|
||||
.distinct
|
||||
.where('merge_request_id > ?', last_mr_id)
|
||||
.order(:merge_request_id)
|
||||
.offset(BATCH_SIZE)
|
||||
.limit(1)
|
||||
.pluck(:merge_request_id)
|
||||
.first
|
||||
|
||||
stop ||= max_mr_id
|
||||
|
||||
migrate_in(
|
||||
step * DELAY,
|
||||
'BackfillEnvironmentIdDeploymentMergeRequests',
|
||||
[last_mr_id + 1, stop]
|
||||
)
|
||||
|
||||
last_mr_id = stop
|
||||
step += 1
|
||||
end
|
||||
end
|
||||
|
||||
def down
|
||||
# no-op
|
||||
|
||||
# this migration is designed to delete duplicated data
|
||||
end
|
||||
end
|
|
@ -13419,6 +13419,7 @@ COPY "schema_migrations" (version) FROM STDIN;
|
|||
20200311214912
|
||||
20200312053852
|
||||
20200312125121
|
||||
20200312134637
|
||||
20200312160532
|
||||
20200312163407
|
||||
20200313101649
|
||||
|
|
|
@ -67,6 +67,7 @@ GET /issues?confidential=true
|
|||
| `updated_before` | datetime | no | Return issues updated on or before the given time |
|
||||
| `confidential` | boolean | no | Filter confidential or public issues. |
|
||||
| `not` | Hash | no | Return issues that do not match the parameters supplied. Accepts: `labels`, `milestone`, `author_id`, `author_username`, `assignee_id`, `assignee_username`, `my_reaction_emoji`, `search`, `in` |
|
||||
| `non_archived` | boolean | no | Return issues only from non-archived projects. If `false`, response will return issues from both archived and non-archived projects. Default is `true`. _(Introduced in [GitLab 13.0](https://gitlab.com/gitlab-org/gitlab/-/issues/197170))_ |
|
||||
|
||||
```shell
|
||||
curl --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/issues
|
||||
|
|
|
@ -17,7 +17,7 @@ GET /search
|
|||
| `scope` | string | yes | The scope to search in |
|
||||
| `search` | string | yes | The search query |
|
||||
|
||||
Search the expression within the specified scope. Currently these scopes are supported: projects, issues, merge_requests, milestones, snippet_titles, snippet_blobs, users.
|
||||
Search the expression within the specified scope. Currently these scopes are supported: projects, issues, merge_requests, milestones, snippet_titles, users.
|
||||
|
||||
If Elasticsearch is enabled additional scopes available are blobs, wiki_blobs and commits. Find more about [the feature](../integration/elasticsearch.md). **(STARTER)**
|
||||
|
||||
|
@ -253,39 +253,6 @@ Example response:
|
|||
]
|
||||
```
|
||||
|
||||
### Scope: snippet_blobs
|
||||
|
||||
This scope will be disabled after GitLab 13.0.
|
||||
|
||||
```shell
|
||||
curl --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/search?scope=snippet_blobs&search=test
|
||||
```
|
||||
|
||||
Example response:
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"id": 50,
|
||||
"title": "Sample file",
|
||||
"file_name": "file.rb",
|
||||
"description": "Simple ruby file",
|
||||
"author": {
|
||||
"id": 1,
|
||||
"name": "Administrator",
|
||||
"username": "root",
|
||||
"state": "active",
|
||||
"avatar_url": "https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
|
||||
"web_url": "http://localhost:3000/root"
|
||||
},
|
||||
"updated_at": "2018-02-06T12:49:29.104Z",
|
||||
"created_at": "2017-11-28T08:20:18.071Z",
|
||||
"project_id": 9,
|
||||
"web_url": "http://localhost:3000/root/jira-test/snippets/50"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
### Scope: wiki_blobs **(STARTER)**
|
||||
|
||||
This scope is available only if [Elasticsearch](../integration/elasticsearch.md) is enabled.
|
||||
|
|
|
@ -64,7 +64,7 @@ class AddNotValidForeignKeyToEmailsUser < ActiveRecord::Migration[5.2]
|
|||
|
||||
def up
|
||||
# safe to use: it requires short lock on the table since we don't validate the foreign key
|
||||
add_foreign_key :emails, :users, on_delete: :cascade, validate: false # rubocop:disable Migration/AddConcurrentForeignKey
|
||||
add_foreign_key :emails, :users, on_delete: :cascade, validate: false
|
||||
end
|
||||
|
||||
def down
|
||||
|
|
|
@ -562,7 +562,7 @@ To use Auto Monitoring:
|
|||
|
||||
1. [Install and configure the requirements](index.md#requirements).
|
||||
1. [Enable Auto DevOps](index.md#enablingdisabling-auto-devops), if you haven't done already.
|
||||
1. Navigate to your project's **{rocket}** **CI/CD > Pipelines** and click **Run pipeline**.
|
||||
1. Navigate to your project's **{rocket}** **CI/CD > Pipelines** and click **Run Pipeline**.
|
||||
1. After the pipeline finishes successfully, open the
|
||||
[monitoring dashboard for a deployed environment](../../ci/environments.md#monitoring-environments)
|
||||
to view the metrics of your deployed application. To view the metrics of the
|
||||
|
|
|
@ -117,7 +117,7 @@ Once synchronized, changing the field mapped to `id` and `externalId` will likel
|
|||
|
||||
### Okta configuration steps
|
||||
|
||||
The SAML application that was created during [Single sign-on](index.md) setup for [Okta](The SAML application that was created during [Single sign-on](index.md) setup for [Okta](https://developer.okta.com/docs/guides/saml-application-setup/overview/) now needs to be set up for SCIM.
|
||||
The SAML application that was created during [Single sign-on](index.md#okta-setup-notes) setup for [Okta](https://developer.okta.com/docs/guides/saml-application-setup/overview/) now needs to be set up for SCIM.
|
||||
|
||||
1. Sign in to Okta.
|
||||
1. If you see an **Admin** button in the top right, click the button. This will
|
||||
|
@ -138,6 +138,10 @@ The SAML application that was created during [Single sign-on](index.md) setup fo
|
|||
- For **API Token** enter the SCIM token obtained from the GitLab SCIM configuration page
|
||||
1. Click 'Test API Credentials' to verify configuration.
|
||||
1. Click **Save** to apply the settings.
|
||||
1. After saving the API integration details, new settings tabs will appear on the left. Choose **To App**.
|
||||
1. Click **Edit**.
|
||||
1. Check the box to **Enable** for both **Create Users** and **Deactivate Users**.
|
||||
1. Click **Save**.
|
||||
1. Assign users in the **Assignments** tab. Assigned users will be created and
|
||||
managed in your GitLab group.
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@ module API
|
|||
module SearchHelpers
|
||||
def self.global_search_scopes
|
||||
# This is a separate method so that EE can redefine it.
|
||||
%w(projects issues merge_requests milestones snippet_titles snippet_blobs users)
|
||||
%w(projects issues merge_requests milestones snippet_titles users)
|
||||
end
|
||||
|
||||
def self.group_search_scopes
|
||||
|
|
|
@ -95,6 +95,8 @@ module API
|
|||
use :issues_params
|
||||
optional :scope, type: String, values: %w[created-by-me assigned-to-me created_by_me assigned_to_me all], default: 'created_by_me',
|
||||
desc: 'Return issues for the given scope: `created_by_me`, `assigned_to_me` or `all`'
|
||||
optional :non_archived, type: Boolean, default: true,
|
||||
desc: 'Return issues from non archived projects'
|
||||
end
|
||||
get do
|
||||
authenticate! unless params[:scope] == 'all'
|
||||
|
|
|
@ -17,7 +17,6 @@ module API
|
|||
blobs: Entities::Blob,
|
||||
wiki_blobs: Entities::Blob,
|
||||
snippet_titles: Entities::Snippet,
|
||||
snippet_blobs: Entities::Snippet,
|
||||
users: Entities::UserBasic
|
||||
}.freeze
|
||||
|
||||
|
@ -36,7 +35,7 @@ module API
|
|||
end
|
||||
|
||||
def snippets?
|
||||
%w(snippet_blobs snippet_titles).include?(params[:scope]).to_s
|
||||
%w(snippet_titles).include?(params[:scope]).to_s
|
||||
end
|
||||
|
||||
def entity
|
||||
|
|
|
@ -0,0 +1,40 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Gitlab
|
||||
module BackgroundMigration
|
||||
# BackfillEnvironmentIdDeploymentMergeRequests deletes duplicates
|
||||
# from deployment_merge_requests table and backfills environment_id
|
||||
class BackfillEnvironmentIdDeploymentMergeRequests
|
||||
def perform(start_mr_id, stop_mr_id)
|
||||
start_mr_id = Integer(start_mr_id)
|
||||
stop_mr_id = Integer(stop_mr_id)
|
||||
|
||||
ActiveRecord::Base.connection.execute(<<~SQL)
|
||||
DELETE FROM deployment_merge_requests
|
||||
WHERE (deployment_id, merge_request_id) in (
|
||||
SELECT t.deployment_id, t.merge_request_id FROM (
|
||||
SELECT mrd.merge_request_id, mrd.deployment_id, ROW_NUMBER() OVER w AS rnum
|
||||
FROM deployment_merge_requests as mrd
|
||||
INNER JOIN "deployments" ON "deployments"."id" = "mrd"."deployment_id"
|
||||
WHERE mrd.merge_request_id BETWEEN #{start_mr_id} AND #{stop_mr_id}
|
||||
WINDOW w AS (
|
||||
PARTITION BY merge_request_id, deployments.environment_id
|
||||
ORDER BY deployments.id
|
||||
)
|
||||
) t
|
||||
WHERE t.rnum > 1
|
||||
);
|
||||
SQL
|
||||
|
||||
ActiveRecord::Base.connection.execute(<<~SQL)
|
||||
UPDATE deployment_merge_requests
|
||||
SET environment_id = deployments.environment_id
|
||||
FROM deployments
|
||||
WHERE deployments.id = "deployment_merge_requests".deployment_id
|
||||
AND "deployment_merge_requests".environment_id IS NULL
|
||||
AND "deployment_merge_requests".merge_request_id BETWEEN #{start_mr_id} AND #{stop_mr_id}
|
||||
SQL
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -14,10 +14,6 @@ module Gitlab
|
|||
@found ||= git.added_files.find { |path| path =~ %r{\A(ee/)?(changelogs/unreleased)(-ee)?/} }
|
||||
end
|
||||
|
||||
def presented_no_changelog_labels
|
||||
NO_CHANGELOG_LABELS.map { |label| "~#{label}" }.join(', ')
|
||||
end
|
||||
|
||||
def sanitized_mr_title
|
||||
gitlab.mr_json["title"].gsub(/^WIP: */, '').gsub(/`/, '\\\`')
|
||||
end
|
||||
|
|
|
@ -198,11 +198,14 @@ module Gitlab
|
|||
(labels & gitlab_helper.mr_labels) == labels
|
||||
end
|
||||
|
||||
def labels_list(labels, sep: ', ')
|
||||
labels.map { |label| %Q{~"#{label}"} }.join(sep)
|
||||
end
|
||||
|
||||
def prepare_labels_for_mr(labels)
|
||||
return '' unless labels.any?
|
||||
|
||||
labels_list = labels.map { |label| %Q{~"#{label}"} }.join(' ')
|
||||
"/label #{labels_list}"
|
||||
"/label #{labels_list(labels, sep: ' ')}"
|
||||
end
|
||||
|
||||
private
|
||||
|
|
|
@ -18104,11 +18104,6 @@ msgid_plural "SearchResults|snippets"
|
|||
msgstr[0] ""
|
||||
msgstr[1] ""
|
||||
|
||||
msgid "SearchResults|snippet result"
|
||||
msgid_plural "SearchResults|snippet results"
|
||||
msgstr[0] ""
|
||||
msgstr[1] ""
|
||||
|
||||
msgid "SearchResults|user"
|
||||
msgid_plural "SearchResults|users"
|
||||
msgstr[0] ""
|
||||
|
@ -21463,10 +21458,10 @@ msgstr ""
|
|||
msgid "ThreatMonitoring|Application firewall not detected"
|
||||
msgstr ""
|
||||
|
||||
msgid "ThreatMonitoring|Container Network Policy"
|
||||
msgid "ThreatMonitoring|Container Network Policies are not installed or have been disabled. To view this data, ensure your Network Policies are installed and enabled for your cluster."
|
||||
msgstr ""
|
||||
|
||||
msgid "ThreatMonitoring|Container NetworkPolicies are not installed or has been disabled. To view this data, ensure you NetworkPolicies are installed and enabled for your cluster."
|
||||
msgid "ThreatMonitoring|Container Network Policy"
|
||||
msgstr ""
|
||||
|
||||
msgid "ThreatMonitoring|Container NetworkPolicies not detected"
|
||||
|
@ -21499,7 +21494,7 @@ msgstr ""
|
|||
msgid "ThreatMonitoring|Something went wrong, unable to fetch statistics"
|
||||
msgstr ""
|
||||
|
||||
msgid "ThreatMonitoring|The firewall is not installed or has been disabled. To view this data, ensure you firewall is installed and enabled for your cluster."
|
||||
msgid "ThreatMonitoring|The firewall is not installed or has been disabled. To view this data, ensure the web application firewall is installed and enabled for your cluster."
|
||||
msgstr ""
|
||||
|
||||
msgid "ThreatMonitoring|The graph below is an overview of traffic coming to your application as tracked by the Web Application Firewall (WAF). View the docs for instructions on how to access the WAF logs to see what type of malicious traffic is trying to access your app. The docs link is also accessible by clicking the \"?\" icon next to the title below."
|
||||
|
@ -21755,7 +21750,7 @@ msgstr ""
|
|||
msgid "Title:"
|
||||
msgstr ""
|
||||
|
||||
msgid "Titles and Filenames"
|
||||
msgid "Titles and Descriptions"
|
||||
msgstr ""
|
||||
|
||||
msgid "To"
|
||||
|
|
|
@ -44,6 +44,8 @@
|
|||
"@gitlab/visual-review-tools": "1.6.1",
|
||||
"@sentry/browser": "^5.10.2",
|
||||
"@sourcegraph/code-host-integration": "0.0.37",
|
||||
"@toast-ui/editor": "^2.0.1",
|
||||
"@toast-ui/vue-editor": "^2.0.1",
|
||||
"apollo-cache-inmemory": "^1.6.3",
|
||||
"apollo-client": "^2.6.4",
|
||||
"apollo-link": "^1.2.11",
|
||||
|
@ -60,6 +62,7 @@
|
|||
"chart.js": "2.7.2",
|
||||
"classlist-polyfill": "^1.2.0",
|
||||
"clipboard": "^1.7.1",
|
||||
"codemirror": "^5.48.4",
|
||||
"codesandbox-api": "0.0.23",
|
||||
"compression-webpack-plugin": "^3.0.1",
|
||||
"copy-webpack-plugin": "^5.0.5",
|
||||
|
|
|
@ -10,17 +10,29 @@ module RuboCop
|
|||
|
||||
MSG = '`add_foreign_key` requires downtime, use `add_concurrent_foreign_key` instead'.freeze
|
||||
|
||||
def_node_matcher :false_node?, <<~PATTERN
|
||||
(false)
|
||||
PATTERN
|
||||
|
||||
def on_send(node)
|
||||
return unless in_migration?(node)
|
||||
|
||||
name = node.children[1]
|
||||
|
||||
add_offense(node, location: :selector) if name == :add_foreign_key
|
||||
if name == :add_foreign_key && !not_valid_fk?(node)
|
||||
add_offense(node, location: :selector)
|
||||
end
|
||||
end
|
||||
|
||||
def method_name(node)
|
||||
node.children.first
|
||||
end
|
||||
|
||||
def not_valid_fk?(node)
|
||||
node.each_node(:pair).any? do |pair|
|
||||
pair.children[0].children[0] == :validate && false_node?(pair.children[1])
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -11,7 +11,7 @@ describe 'Search Snippets' do
|
|||
visit dashboard_snippets_path
|
||||
|
||||
submit_search('Middle')
|
||||
select_search_scope('Titles and Filenames')
|
||||
select_search_scope('Titles and Descriptions')
|
||||
|
||||
expect(page).to have_link(public_snippet.title)
|
||||
expect(page).to have_link(private_snippet.title)
|
||||
|
|
|
@ -174,15 +174,16 @@ describe MergeRequestsFinder do
|
|||
deployment1 = create(
|
||||
:deployment,
|
||||
project: project_with_repo,
|
||||
sha: project_with_repo.commit.id,
|
||||
merge_requests: [merge_request1, merge_request2]
|
||||
sha: project_with_repo.commit.id
|
||||
)
|
||||
create(
|
||||
deployment2 = create(
|
||||
:deployment,
|
||||
project: project_with_repo,
|
||||
sha: project_with_repo.commit.id,
|
||||
merge_requests: [merge_request3]
|
||||
sha: project_with_repo.commit.id
|
||||
)
|
||||
deployment1.link_merge_requests(MergeRequest.where(id: [merge_request1.id, merge_request2.id]))
|
||||
deployment2.link_merge_requests(MergeRequest.where(id: merge_request3.id))
|
||||
|
||||
params = { deployment_id: deployment1.id }
|
||||
merge_requests = described_class.new(user, params).execute
|
||||
|
||||
|
|
17
spec/frontend/__mocks__/@toast-ui/vue-editor/index.js
Normal file
17
spec/frontend/__mocks__/@toast-ui/vue-editor/index.js
Normal file
|
@ -0,0 +1,17 @@
|
|||
export const Editor = {
|
||||
props: {
|
||||
initialValue: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
render(h) {
|
||||
return h('div');
|
||||
},
|
||||
};
|
||||
|
||||
export const Viewer = {
|
||||
render(h) {
|
||||
return h('div');
|
||||
},
|
||||
};
|
|
@ -1041,6 +1041,66 @@ describe('boardsStore', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('addListIssue', () => {
|
||||
let list;
|
||||
const issue1 = new ListIssue({
|
||||
title: 'Testing',
|
||||
id: 2,
|
||||
iid: 2,
|
||||
confidential: false,
|
||||
labels: [
|
||||
{
|
||||
color: '#ff0000',
|
||||
description: 'testing;',
|
||||
id: 5000,
|
||||
priority: undefined,
|
||||
textColor: 'white',
|
||||
title: 'Test',
|
||||
},
|
||||
],
|
||||
assignees: [],
|
||||
});
|
||||
const issue2 = new ListIssue({
|
||||
title: 'Testing',
|
||||
id: 1,
|
||||
iid: 1,
|
||||
confidential: false,
|
||||
labels: [
|
||||
{
|
||||
id: 1,
|
||||
title: 'test',
|
||||
color: 'red',
|
||||
description: 'testing',
|
||||
},
|
||||
],
|
||||
assignees: [
|
||||
{
|
||||
id: 1,
|
||||
name: 'name',
|
||||
username: 'username',
|
||||
avatar_url: 'http://avatar_url',
|
||||
},
|
||||
],
|
||||
real_path: 'path/to/issue',
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
list = new List(listObj);
|
||||
list.addIssue(issue1);
|
||||
setupDefaultResponses();
|
||||
});
|
||||
|
||||
it('adds issues that are not already on the list', () => {
|
||||
expect(list.findIssue(issue2.id)).toBe(undefined);
|
||||
expect(list.issues).toEqual([issue1]);
|
||||
|
||||
boardsStore.addListIssue(list, issue2);
|
||||
expect(list.findIssue(issue2.id)).toBe(issue2);
|
||||
expect(list.issues.length).toBe(2);
|
||||
expect(list.issues).toEqual([issue1, issue2]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateIssue', () => {
|
||||
let issue;
|
||||
let patchSpy;
|
||||
|
|
|
@ -503,11 +503,16 @@ describe('DiffsStoreUtils', () => {
|
|||
},
|
||||
};
|
||||
|
||||
// When multi line comments are fully implemented `line_code` will be
|
||||
// included in all requests. Until then we need to ensure the logic does
|
||||
// not change when it is included only in the "comparison" argument.
|
||||
const lineRange = { start_line_code: 'abc_1_1', end_line_code: 'abc_1_2' };
|
||||
|
||||
it('returns true when the discussion is up to date', () => {
|
||||
expect(
|
||||
utils.isDiscussionApplicableToLine({
|
||||
discussion: discussions.upToDateDiscussion1,
|
||||
diffPosition,
|
||||
diffPosition: { ...diffPosition, line_range: lineRange },
|
||||
latestDiff: true,
|
||||
}),
|
||||
).toBe(true);
|
||||
|
@ -517,7 +522,7 @@ describe('DiffsStoreUtils', () => {
|
|||
expect(
|
||||
utils.isDiscussionApplicableToLine({
|
||||
discussion: discussions.outDatedDiscussion1,
|
||||
diffPosition,
|
||||
diffPosition: { ...diffPosition, line_range: lineRange },
|
||||
latestDiff: true,
|
||||
}),
|
||||
).toBe(false);
|
||||
|
@ -534,6 +539,7 @@ describe('DiffsStoreUtils', () => {
|
|||
diffPosition: {
|
||||
...diffPosition,
|
||||
lineCode: 'ABC_1',
|
||||
line_range: lineRange,
|
||||
},
|
||||
latestDiff: true,
|
||||
}),
|
||||
|
@ -551,6 +557,7 @@ describe('DiffsStoreUtils', () => {
|
|||
diffPosition: {
|
||||
...diffPosition,
|
||||
line_code: 'ABC_1',
|
||||
line_range: lineRange,
|
||||
},
|
||||
latestDiff: true,
|
||||
}),
|
||||
|
@ -568,6 +575,7 @@ describe('DiffsStoreUtils', () => {
|
|||
diffPosition: {
|
||||
...diffPosition,
|
||||
lineCode: 'ABC_1',
|
||||
line_range: lineRange,
|
||||
},
|
||||
latestDiff: false,
|
||||
}),
|
||||
|
|
|
@ -1,6 +1,4 @@
|
|||
import $ from 'jquery';
|
||||
import MockAdapter from 'axios-mock-adapter';
|
||||
import getSetTimeoutPromise from 'spec/helpers/set_timeout_promise_helper';
|
||||
import axios from '~/lib/utils/axios_utils';
|
||||
import { getSelector, dismiss, inserted } from '~/feature_highlight/feature_highlight_helper';
|
||||
import { togglePopover } from '~/shared/popover';
|
||||
|
@ -17,34 +15,23 @@ describe('feature highlight helper', () => {
|
|||
});
|
||||
|
||||
describe('dismiss', () => {
|
||||
let mock;
|
||||
const context = {
|
||||
hide: () => {},
|
||||
attr: () => '/-/callouts/dismiss',
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
mock = new MockAdapter(axios);
|
||||
|
||||
spyOn(togglePopover, 'call').and.callFake(() => {});
|
||||
spyOn(context, 'hide').and.callFake(() => {});
|
||||
jest.spyOn(axios, 'post').mockResolvedValue();
|
||||
jest.spyOn(togglePopover, 'call').mockImplementation(() => {});
|
||||
jest.spyOn(context, 'hide').mockImplementation(() => {});
|
||||
dismiss.call(context);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
mock.restore();
|
||||
});
|
||||
|
||||
it('calls persistent dismissal endpoint', done => {
|
||||
const spy = jasmine.createSpy('dismiss-endpoint-hit');
|
||||
mock.onPost('/-/callouts/dismiss').reply(spy);
|
||||
|
||||
getSetTimeoutPromise()
|
||||
.then(() => {
|
||||
expect(spy).toHaveBeenCalled();
|
||||
})
|
||||
.then(done)
|
||||
.catch(done.fail);
|
||||
it('calls persistent dismissal endpoint', () => {
|
||||
expect(axios.post).toHaveBeenCalledWith(
|
||||
'/-/callouts/dismiss',
|
||||
expect.objectContaining({ feature_name: undefined }),
|
||||
);
|
||||
});
|
||||
|
||||
it('calls hide popover', () => {
|
||||
|
@ -65,7 +52,7 @@ describe('feature highlight helper', () => {
|
|||
},
|
||||
};
|
||||
|
||||
spyOn($.fn, 'on').and.callFake(event => {
|
||||
jest.spyOn($.fn, 'on').mockImplementation(event => {
|
||||
expect(event).toEqual('click');
|
||||
done();
|
||||
});
|
|
@ -3,34 +3,20 @@ import domContentLoaded from '~/feature_highlight/feature_highlight_options';
|
|||
|
||||
describe('feature highlight options', () => {
|
||||
describe('domContentLoaded', () => {
|
||||
it('should not call highlightFeatures when breakpoint is xs', () => {
|
||||
jest.spyOn(bp, 'getBreakpointSize').mockReturnValue('xs');
|
||||
it.each`
|
||||
breakPoint | shouldCall
|
||||
${'xs'} | ${false}
|
||||
${'sm'} | ${false}
|
||||
${'md'} | ${false}
|
||||
${'lg'} | ${false}
|
||||
${'xl'} | ${true}
|
||||
`(
|
||||
'when breakpoint is $breakPoint should call highlightFeatures is $shouldCall',
|
||||
({ breakPoint, shouldCall }) => {
|
||||
jest.spyOn(bp, 'getBreakpointSize').mockReturnValue(breakPoint);
|
||||
|
||||
expect(domContentLoaded()).toBe(false);
|
||||
});
|
||||
|
||||
it('should not call highlightFeatures when breakpoint is sm', () => {
|
||||
jest.spyOn(bp, 'getBreakpointSize').mockReturnValue('sm');
|
||||
|
||||
expect(domContentLoaded()).toBe(false);
|
||||
});
|
||||
|
||||
it('should not call highlightFeatures when breakpoint is md', () => {
|
||||
jest.spyOn(bp, 'getBreakpointSize').mockReturnValue('md');
|
||||
|
||||
expect(domContentLoaded()).toBe(false);
|
||||
});
|
||||
|
||||
it('should not call highlightFeatures when breakpoint is not xl', () => {
|
||||
jest.spyOn(bp, 'getBreakpointSize').mockReturnValue('lg');
|
||||
|
||||
expect(domContentLoaded()).toBe(false);
|
||||
});
|
||||
|
||||
it('should call highlightFeatures when breakpoint is xl', () => {
|
||||
jest.spyOn(bp, 'getBreakpointSize').mockReturnValue('xl');
|
||||
|
||||
expect(domContentLoaded()).toBe(true);
|
||||
});
|
||||
expect(domContentLoaded()).toBe(shouldCall);
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -4,6 +4,8 @@ import * as featureHighlight from '~/feature_highlight/feature_highlight';
|
|||
import * as popover from '~/shared/popover';
|
||||
import axios from '~/lib/utils/axios_utils';
|
||||
|
||||
jest.mock('~/shared/popover');
|
||||
|
||||
describe('feature highlight', () => {
|
||||
beforeEach(() => {
|
||||
setFixtures(`
|
||||
|
@ -28,7 +30,7 @@ describe('feature highlight', () => {
|
|||
beforeEach(() => {
|
||||
mock = new MockAdapter(axios);
|
||||
mock.onGet('/test').reply(200);
|
||||
spyOn(window, 'addEventListener');
|
||||
jest.spyOn(window, 'addEventListener').mockImplementation(() => {});
|
||||
featureHighlight.setupFeatureHighlightPopover('test', 0);
|
||||
});
|
||||
|
||||
|
@ -44,27 +46,21 @@ describe('feature highlight', () => {
|
|||
});
|
||||
|
||||
it('setup mouseenter', () => {
|
||||
const toggleSpy = spyOn(popover.togglePopover, 'call');
|
||||
$(selector).trigger('mouseenter');
|
||||
|
||||
expect(toggleSpy).toHaveBeenCalledWith(jasmine.any(Object), true);
|
||||
expect(popover.mouseenter).toHaveBeenCalledWith(expect.any(Object));
|
||||
});
|
||||
|
||||
it('setup debounced mouseleave', done => {
|
||||
const toggleSpy = spyOn(popover.togglePopover, 'call');
|
||||
it('setup debounced mouseleave', () => {
|
||||
$(selector).trigger('mouseleave');
|
||||
|
||||
// Even though we've set the debounce to 0ms, setTimeout is needed for the debounce
|
||||
setTimeout(() => {
|
||||
expect(toggleSpy).toHaveBeenCalledWith(jasmine.any(Object), false);
|
||||
done();
|
||||
}, 0);
|
||||
expect(popover.debouncedMouseleave).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('setup show.bs.popover', () => {
|
||||
$(selector).trigger('show.bs.popover');
|
||||
|
||||
expect(window.addEventListener).toHaveBeenCalledWith('scroll', jasmine.any(Function), {
|
||||
expect(window.addEventListener).toHaveBeenCalledWith('scroll', expect.any(Function), {
|
||||
once: true,
|
||||
});
|
||||
});
|
||||
|
@ -72,23 +68,6 @@ describe('feature highlight', () => {
|
|||
it('removes disabled attribute', () => {
|
||||
expect($('.js-feature-highlight').is(':disabled')).toEqual(false);
|
||||
});
|
||||
|
||||
it('displays popover', () => {
|
||||
expect(document.querySelector(selector).getAttribute('aria-describedby')).toBeFalsy();
|
||||
$(selector).trigger('mouseenter');
|
||||
|
||||
expect(document.querySelector(selector).getAttribute('aria-describedby')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('toggles when clicked', () => {
|
||||
$(selector).trigger('mouseenter');
|
||||
const popoverId = $(selector).attr('aria-describedby');
|
||||
const toggleSpy = spyOn(popover.togglePopover, 'call');
|
||||
|
||||
$(`#${popoverId} .dismiss-feature-highlight`).click();
|
||||
|
||||
expect(toggleSpy).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('findHighestPriorityFeature', () => {
|
31
spec/frontend/sidebar/lock/edit_form_buttons_spec.js
Normal file
31
spec/frontend/sidebar/lock/edit_form_buttons_spec.js
Normal file
|
@ -0,0 +1,31 @@
|
|||
import { shallowMount } from '@vue/test-utils';
|
||||
import EditFormButtons from '~/sidebar/components/lock/edit_form_buttons.vue';
|
||||
|
||||
describe('EditFormButtons', () => {
|
||||
let wrapper;
|
||||
|
||||
const mountComponent = propsData => shallowMount(EditFormButtons, { propsData });
|
||||
|
||||
afterEach(() => {
|
||||
wrapper.destroy();
|
||||
wrapper = null;
|
||||
});
|
||||
|
||||
it('displays "Unlock" when locked', () => {
|
||||
wrapper = mountComponent({
|
||||
isLocked: true,
|
||||
updateLockedAttribute: () => {},
|
||||
});
|
||||
|
||||
expect(wrapper.text()).toContain('Unlock');
|
||||
});
|
||||
|
||||
it('displays "Lock" when unlocked', () => {
|
||||
wrapper = mountComponent({
|
||||
isLocked: false,
|
||||
updateLockedAttribute: () => {},
|
||||
});
|
||||
|
||||
expect(wrapper.text()).toContain('Lock');
|
||||
});
|
||||
});
|
206
spec/frontend/sidebar/participants_spec.js
Normal file
206
spec/frontend/sidebar/participants_spec.js
Normal file
|
@ -0,0 +1,206 @@
|
|||
import { GlLoadingIcon } from '@gitlab/ui';
|
||||
import { shallowMount } from '@vue/test-utils';
|
||||
import Vue from 'vue';
|
||||
import Participants from '~/sidebar/components/participants/participants.vue';
|
||||
|
||||
const PARTICIPANT = {
|
||||
id: 1,
|
||||
state: 'active',
|
||||
username: 'marcene',
|
||||
name: 'Allie Will',
|
||||
web_url: 'foo.com',
|
||||
avatar_url: 'gravatar.com/avatar/xxx',
|
||||
};
|
||||
|
||||
const PARTICIPANT_LIST = [PARTICIPANT, { ...PARTICIPANT, id: 2 }, { ...PARTICIPANT, id: 3 }];
|
||||
|
||||
describe('Participants', () => {
|
||||
let wrapper;
|
||||
|
||||
const getMoreParticipantsButton = () => wrapper.find('button');
|
||||
|
||||
const getCollapsedParticipantsCount = () => wrapper.find('[data-testid="collapsed-count"]');
|
||||
|
||||
const mountComponent = propsData =>
|
||||
shallowMount(Participants, {
|
||||
propsData,
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
wrapper.destroy();
|
||||
wrapper = null;
|
||||
});
|
||||
|
||||
describe('collapsed sidebar state', () => {
|
||||
it('shows loading spinner when loading', () => {
|
||||
wrapper = mountComponent({
|
||||
loading: true,
|
||||
});
|
||||
|
||||
expect(wrapper.contains(GlLoadingIcon)).toBe(true);
|
||||
});
|
||||
|
||||
it('does not show loading spinner not loading', () => {
|
||||
wrapper = mountComponent({
|
||||
loading: false,
|
||||
});
|
||||
|
||||
expect(wrapper.contains(GlLoadingIcon)).toBe(false);
|
||||
});
|
||||
|
||||
it('shows participant count when given', () => {
|
||||
wrapper = mountComponent({
|
||||
loading: false,
|
||||
participants: PARTICIPANT_LIST,
|
||||
});
|
||||
|
||||
expect(getCollapsedParticipantsCount().text()).toBe(`${PARTICIPANT_LIST.length}`);
|
||||
});
|
||||
|
||||
it('shows full participant count when there are hidden participants', () => {
|
||||
wrapper = mountComponent({
|
||||
loading: false,
|
||||
participants: PARTICIPANT_LIST,
|
||||
numberOfLessParticipants: 1,
|
||||
});
|
||||
|
||||
expect(getCollapsedParticipantsCount().text()).toBe(`${PARTICIPANT_LIST.length}`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('expanded sidebar state', () => {
|
||||
it('shows loading spinner when loading', () => {
|
||||
wrapper = mountComponent({
|
||||
loading: true,
|
||||
});
|
||||
|
||||
expect(wrapper.contains(GlLoadingIcon)).toBe(true);
|
||||
});
|
||||
|
||||
it('when only showing visible participants, shows an avatar only for each participant under the limit', () => {
|
||||
const numberOfLessParticipants = 2;
|
||||
wrapper = mountComponent({
|
||||
loading: false,
|
||||
participants: PARTICIPANT_LIST,
|
||||
numberOfLessParticipants,
|
||||
});
|
||||
|
||||
wrapper.setData({
|
||||
isShowingMoreParticipants: false,
|
||||
});
|
||||
|
||||
return Vue.nextTick().then(() => {
|
||||
expect(wrapper.findAll('.participants-author')).toHaveLength(numberOfLessParticipants);
|
||||
});
|
||||
});
|
||||
|
||||
it('when only showing all participants, each has an avatar', () => {
|
||||
wrapper = mountComponent({
|
||||
loading: false,
|
||||
participants: PARTICIPANT_LIST,
|
||||
numberOfLessParticipants: 2,
|
||||
});
|
||||
|
||||
wrapper.setData({
|
||||
isShowingMoreParticipants: true,
|
||||
});
|
||||
|
||||
return Vue.nextTick().then(() => {
|
||||
expect(wrapper.findAll('.participants-author')).toHaveLength(PARTICIPANT_LIST.length);
|
||||
});
|
||||
});
|
||||
|
||||
it('does not have more participants link when they can all be shown', () => {
|
||||
const numberOfLessParticipants = 100;
|
||||
wrapper = mountComponent({
|
||||
loading: false,
|
||||
participants: PARTICIPANT_LIST,
|
||||
numberOfLessParticipants,
|
||||
});
|
||||
|
||||
expect(PARTICIPANT_LIST.length).toBeLessThan(numberOfLessParticipants);
|
||||
expect(getMoreParticipantsButton().exists()).toBe(false);
|
||||
});
|
||||
|
||||
it('when too many participants, has more participants link to show more', () => {
|
||||
wrapper = mountComponent({
|
||||
loading: false,
|
||||
participants: PARTICIPANT_LIST,
|
||||
numberOfLessParticipants: 2,
|
||||
});
|
||||
|
||||
wrapper.setData({
|
||||
isShowingMoreParticipants: false,
|
||||
});
|
||||
|
||||
return Vue.nextTick().then(() => {
|
||||
expect(getMoreParticipantsButton().text()).toBe('+ 1 more');
|
||||
});
|
||||
});
|
||||
|
||||
it('when too many participants and already showing them, has more participants link to show less', () => {
|
||||
wrapper = mountComponent({
|
||||
loading: false,
|
||||
participants: PARTICIPANT_LIST,
|
||||
numberOfLessParticipants: 2,
|
||||
});
|
||||
|
||||
wrapper.setData({
|
||||
isShowingMoreParticipants: true,
|
||||
});
|
||||
|
||||
return Vue.nextTick().then(() => {
|
||||
expect(getMoreParticipantsButton().text()).toBe('- show less');
|
||||
});
|
||||
});
|
||||
|
||||
it('clicking more participants link emits event', () => {
|
||||
wrapper = mountComponent({
|
||||
loading: false,
|
||||
participants: PARTICIPANT_LIST,
|
||||
numberOfLessParticipants: 2,
|
||||
});
|
||||
|
||||
expect(wrapper.vm.isShowingMoreParticipants).toBe(false);
|
||||
|
||||
getMoreParticipantsButton().trigger('click');
|
||||
|
||||
expect(wrapper.vm.isShowingMoreParticipants).toBe(true);
|
||||
});
|
||||
|
||||
it('clicking on participants icon emits `toggleSidebar` event', () => {
|
||||
wrapper = mountComponent({
|
||||
loading: false,
|
||||
participants: PARTICIPANT_LIST,
|
||||
numberOfLessParticipants: 2,
|
||||
});
|
||||
|
||||
const spy = jest.spyOn(wrapper.vm, '$emit');
|
||||
|
||||
wrapper.find('.sidebar-collapsed-icon').trigger('click');
|
||||
|
||||
return Vue.nextTick(() => {
|
||||
expect(spy).toHaveBeenCalledWith('toggleSidebar');
|
||||
|
||||
spy.mockRestore();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when not showing participants label', () => {
|
||||
beforeEach(() => {
|
||||
wrapper = mountComponent({
|
||||
participants: PARTICIPANT_LIST,
|
||||
showParticipantLabel: false,
|
||||
});
|
||||
});
|
||||
|
||||
it('does not show sidebar collapsed icon', () => {
|
||||
expect(wrapper.contains('.sidebar-collapsed-icon')).toBe(false);
|
||||
});
|
||||
|
||||
it('does not show participants label title', () => {
|
||||
expect(wrapper.contains('.title')).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
135
spec/frontend/sidebar/sidebar_mediator_spec.js
Normal file
135
spec/frontend/sidebar/sidebar_mediator_spec.js
Normal file
|
@ -0,0 +1,135 @@
|
|||
import MockAdapter from 'axios-mock-adapter';
|
||||
import axios from '~/lib/utils/axios_utils';
|
||||
import * as urlUtility from '~/lib/utils/url_utility';
|
||||
import SidebarService, { gqClient } from '~/sidebar/services/sidebar_service';
|
||||
import SidebarMediator from '~/sidebar/sidebar_mediator';
|
||||
import SidebarStore from '~/sidebar/stores/sidebar_store';
|
||||
import Mock from './mock_data';
|
||||
|
||||
describe('Sidebar mediator', () => {
|
||||
const { mediator: mediatorMockData } = Mock;
|
||||
let mock;
|
||||
let mediator;
|
||||
|
||||
beforeEach(() => {
|
||||
mock = new MockAdapter(axios);
|
||||
mediator = new SidebarMediator(mediatorMockData);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
SidebarService.singleton = null;
|
||||
SidebarStore.singleton = null;
|
||||
SidebarMediator.singleton = null;
|
||||
mock.restore();
|
||||
});
|
||||
|
||||
it('assigns yourself ', () => {
|
||||
mediator.assignYourself();
|
||||
|
||||
expect(mediator.store.currentUser).toEqual(mediatorMockData.currentUser);
|
||||
expect(mediator.store.assignees[0]).toEqual(mediatorMockData.currentUser);
|
||||
});
|
||||
|
||||
it('saves assignees', () => {
|
||||
mock.onPut(mediatorMockData.endpoint).reply(200, {});
|
||||
|
||||
return mediator.saveAssignees('issue[assignee_ids]').then(resp => {
|
||||
expect(resp.status).toEqual(200);
|
||||
});
|
||||
});
|
||||
|
||||
it('fetches the data', () => {
|
||||
const mockData = Mock.responseMap.GET[mediatorMockData.endpoint];
|
||||
mock.onGet(mediatorMockData.endpoint).reply(200, mockData);
|
||||
|
||||
const mockGraphQlData = Mock.graphQlResponseData;
|
||||
const graphQlSpy = jest.spyOn(gqClient, 'query').mockReturnValue({
|
||||
data: mockGraphQlData,
|
||||
});
|
||||
const spy = jest.spyOn(mediator, 'processFetchedData').mockReturnValue(Promise.resolve());
|
||||
|
||||
return mediator.fetch().then(() => {
|
||||
expect(spy).toHaveBeenCalledWith(mockData, mockGraphQlData);
|
||||
|
||||
spy.mockRestore();
|
||||
graphQlSpy.mockRestore();
|
||||
});
|
||||
});
|
||||
|
||||
it('processes fetched data', () => {
|
||||
const mockData = Mock.responseMap.GET[mediatorMockData.endpoint];
|
||||
mediator.processFetchedData(mockData);
|
||||
|
||||
expect(mediator.store.assignees).toEqual(mockData.assignees);
|
||||
expect(mediator.store.humanTimeEstimate).toEqual(mockData.human_time_estimate);
|
||||
expect(mediator.store.humanTotalTimeSpent).toEqual(mockData.human_total_time_spent);
|
||||
expect(mediator.store.participants).toEqual(mockData.participants);
|
||||
expect(mediator.store.subscribed).toEqual(mockData.subscribed);
|
||||
expect(mediator.store.timeEstimate).toEqual(mockData.time_estimate);
|
||||
expect(mediator.store.totalTimeSpent).toEqual(mockData.total_time_spent);
|
||||
});
|
||||
|
||||
it('sets moveToProjectId', () => {
|
||||
const projectId = 7;
|
||||
const spy = jest.spyOn(mediator.store, 'setMoveToProjectId').mockReturnValue(Promise.resolve());
|
||||
|
||||
mediator.setMoveToProjectId(projectId);
|
||||
|
||||
expect(spy).toHaveBeenCalledWith(projectId);
|
||||
|
||||
spy.mockRestore();
|
||||
});
|
||||
|
||||
it('fetches autocomplete projects', () => {
|
||||
const searchTerm = 'foo';
|
||||
mock.onGet(mediatorMockData.projectsAutocompleteEndpoint).reply(200, {});
|
||||
const getterSpy = jest
|
||||
.spyOn(mediator.service, 'getProjectsAutocomplete')
|
||||
.mockReturnValue(Promise.resolve({ data: {} }));
|
||||
const setterSpy = jest
|
||||
.spyOn(mediator.store, 'setAutocompleteProjects')
|
||||
.mockReturnValue(Promise.resolve());
|
||||
|
||||
return mediator.fetchAutocompleteProjects(searchTerm).then(() => {
|
||||
expect(getterSpy).toHaveBeenCalledWith(searchTerm);
|
||||
expect(setterSpy).toHaveBeenCalled();
|
||||
|
||||
getterSpy.mockRestore();
|
||||
setterSpy.mockRestore();
|
||||
});
|
||||
});
|
||||
|
||||
it('moves issue', () => {
|
||||
const mockData = Mock.responseMap.POST[mediatorMockData.moveIssueEndpoint];
|
||||
const moveToProjectId = 7;
|
||||
mock.onPost(mediatorMockData.moveIssueEndpoint).reply(200, mockData);
|
||||
mediator.store.setMoveToProjectId(moveToProjectId);
|
||||
const moveIssueSpy = jest
|
||||
.spyOn(mediator.service, 'moveIssue')
|
||||
.mockReturnValue(Promise.resolve({ data: { web_url: mockData.web_url } }));
|
||||
const urlSpy = jest.spyOn(urlUtility, 'visitUrl').mockReturnValue({});
|
||||
|
||||
return mediator.moveIssue().then(() => {
|
||||
expect(moveIssueSpy).toHaveBeenCalledWith(moveToProjectId);
|
||||
expect(urlSpy).toHaveBeenCalledWith(mockData.web_url);
|
||||
|
||||
moveIssueSpy.mockRestore();
|
||||
urlSpy.mockRestore();
|
||||
});
|
||||
});
|
||||
|
||||
it('toggle subscription', () => {
|
||||
mediator.store.setSubscribedState(false);
|
||||
mock.onPost(mediatorMockData.toggleSubscriptionEndpoint).reply(200, {});
|
||||
const spy = jest
|
||||
.spyOn(mediator.service, 'toggleSubscription')
|
||||
.mockReturnValue(Promise.resolve());
|
||||
|
||||
return mediator.toggleSubscription().then(() => {
|
||||
expect(spy).toHaveBeenCalled();
|
||||
expect(mediator.store.subscribed).toEqual(true);
|
||||
|
||||
spy.mockRestore();
|
||||
});
|
||||
});
|
||||
});
|
36
spec/frontend/sidebar/sidebar_subscriptions_spec.js
Normal file
36
spec/frontend/sidebar/sidebar_subscriptions_spec.js
Normal file
|
@ -0,0 +1,36 @@
|
|||
import { shallowMount } from '@vue/test-utils';
|
||||
import SidebarSubscriptions from '~/sidebar/components/subscriptions/sidebar_subscriptions.vue';
|
||||
import SidebarMediator from '~/sidebar/sidebar_mediator';
|
||||
import SidebarService from '~/sidebar/services/sidebar_service';
|
||||
import SidebarStore from '~/sidebar/stores/sidebar_store';
|
||||
import Mock from './mock_data';
|
||||
|
||||
describe('Sidebar Subscriptions', () => {
|
||||
let wrapper;
|
||||
let mediator;
|
||||
|
||||
beforeEach(() => {
|
||||
mediator = new SidebarMediator(Mock.mediator);
|
||||
wrapper = shallowMount(SidebarSubscriptions, {
|
||||
propsData: {
|
||||
mediator,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
wrapper.destroy();
|
||||
SidebarService.singleton = null;
|
||||
SidebarStore.singleton = null;
|
||||
SidebarMediator.singleton = null;
|
||||
});
|
||||
|
||||
it('calls the mediator toggleSubscription on event', () => {
|
||||
const spy = jest.spyOn(mediator, 'toggleSubscription').mockReturnValue(Promise.resolve());
|
||||
|
||||
wrapper.vm.onToggleSubscription();
|
||||
|
||||
expect(spy).toHaveBeenCalled();
|
||||
spy.mockRestore();
|
||||
});
|
||||
});
|
106
spec/frontend/sidebar/subscriptions_spec.js
Normal file
106
spec/frontend/sidebar/subscriptions_spec.js
Normal file
|
@ -0,0 +1,106 @@
|
|||
import { shallowMount } from '@vue/test-utils';
|
||||
import Subscriptions from '~/sidebar/components/subscriptions/subscriptions.vue';
|
||||
import eventHub from '~/sidebar/event_hub';
|
||||
import ToggleButton from '~/vue_shared/components/toggle_button.vue';
|
||||
|
||||
describe('Subscriptions', () => {
|
||||
let wrapper;
|
||||
|
||||
const findToggleButton = () => wrapper.find(ToggleButton);
|
||||
|
||||
const mountComponent = propsData =>
|
||||
shallowMount(Subscriptions, {
|
||||
propsData,
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
wrapper.destroy();
|
||||
wrapper = null;
|
||||
});
|
||||
|
||||
it('shows loading spinner when loading', () => {
|
||||
wrapper = mountComponent({
|
||||
loading: true,
|
||||
subscribed: undefined,
|
||||
});
|
||||
|
||||
expect(findToggleButton().attributes('isloading')).toBe('true');
|
||||
});
|
||||
|
||||
it('is toggled "off" when currently not subscribed', () => {
|
||||
wrapper = mountComponent({
|
||||
subscribed: false,
|
||||
});
|
||||
|
||||
expect(findToggleButton().attributes('value')).toBeFalsy();
|
||||
});
|
||||
|
||||
it('is toggled "on" when currently subscribed', () => {
|
||||
wrapper = mountComponent({
|
||||
subscribed: true,
|
||||
});
|
||||
|
||||
expect(findToggleButton().attributes('value')).toBe('true');
|
||||
});
|
||||
|
||||
it('toggleSubscription method emits `toggleSubscription` event on eventHub and Component', () => {
|
||||
const id = 42;
|
||||
wrapper = mountComponent({ subscribed: true, id });
|
||||
const eventHubSpy = jest.spyOn(eventHub, '$emit');
|
||||
const wrapperEmitSpy = jest.spyOn(wrapper.vm, '$emit');
|
||||
|
||||
wrapper.vm.toggleSubscription();
|
||||
|
||||
expect(eventHubSpy).toHaveBeenCalledWith('toggleSubscription', id);
|
||||
expect(wrapperEmitSpy).toHaveBeenCalledWith('toggleSubscription', id);
|
||||
eventHubSpy.mockRestore();
|
||||
wrapperEmitSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('tracks the event when toggled', () => {
|
||||
wrapper = mountComponent({ subscribed: true });
|
||||
|
||||
const wrapperTrackSpy = jest.spyOn(wrapper.vm, 'track');
|
||||
|
||||
wrapper.vm.toggleSubscription();
|
||||
|
||||
expect(wrapperTrackSpy).toHaveBeenCalledWith('toggle_button', {
|
||||
property: 'notifications',
|
||||
value: 0,
|
||||
});
|
||||
wrapperTrackSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('onClickCollapsedIcon method emits `toggleSidebar` event on component', () => {
|
||||
wrapper = mountComponent({ subscribed: true });
|
||||
const spy = jest.spyOn(wrapper.vm, '$emit');
|
||||
|
||||
wrapper.vm.onClickCollapsedIcon();
|
||||
|
||||
expect(spy).toHaveBeenCalledWith('toggleSidebar');
|
||||
spy.mockRestore();
|
||||
});
|
||||
|
||||
describe('given project emails are disabled', () => {
|
||||
const subscribeDisabledDescription = 'Notifications have been disabled';
|
||||
|
||||
beforeEach(() => {
|
||||
wrapper = mountComponent({
|
||||
subscribed: false,
|
||||
projectEmailsDisabled: true,
|
||||
subscribeDisabledDescription,
|
||||
});
|
||||
});
|
||||
|
||||
it('sets the correct display text', () => {
|
||||
expect(wrapper.find('.issuable-header-text').text()).toContain(subscribeDisabledDescription);
|
||||
expect(wrapper.find({ ref: 'tooltip' }).attributes('data-original-title')).toBe(
|
||||
subscribeDisabledDescription,
|
||||
);
|
||||
});
|
||||
|
||||
it('does not render the toggle button', () => {
|
||||
expect(wrapper.contains('.js-issuable-subscribe-button')).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,37 @@
|
|||
import { shallowMount } from '@vue/test-utils';
|
||||
import RichContentEditor from '~/vue_shared/components/rich_content_editor/rich_content_editor.vue';
|
||||
|
||||
describe('Rich Content Editor', () => {
|
||||
let wrapper;
|
||||
|
||||
const value = '## Some Markdown';
|
||||
const findEditor = () => wrapper.find({ ref: 'editor' });
|
||||
|
||||
beforeEach(() => {
|
||||
wrapper = shallowMount(RichContentEditor, {
|
||||
propsData: { value },
|
||||
});
|
||||
});
|
||||
|
||||
describe('when content is loaded', () => {
|
||||
it('renders an editor', () => {
|
||||
expect(findEditor().exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('renders the correct content', () => {
|
||||
expect(findEditor().props().initialValue).toBe(value);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when content is changed', () => {
|
||||
it('emits an input event with the changed content', () => {
|
||||
const changedMarkdown = '## Changed Markdown';
|
||||
const getMarkdownMock = jest.fn().mockReturnValueOnce(changedMarkdown);
|
||||
|
||||
findEditor().setMethods({ invoke: getMarkdownMock });
|
||||
findEditor().vm.$emit('change');
|
||||
|
||||
expect(wrapper.emitted().input[0][0]).toBe(changedMarkdown);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -112,7 +112,6 @@ describe SearchHelper do
|
|||
'milestones' | 'milestone'
|
||||
'notes' | 'comment'
|
||||
'projects' | 'project'
|
||||
'snippet_blobs' | 'snippet result'
|
||||
'snippet_titles' | 'snippet'
|
||||
'users' | 'user'
|
||||
'wiki_blobs' | 'wiki result'
|
||||
|
|
|
@ -1,32 +0,0 @@
|
|||
import Vue from 'vue';
|
||||
import mountComponent from 'spec/helpers/vue_mount_component_helper';
|
||||
import editFormButtons from '~/sidebar/components/lock/edit_form_buttons.vue';
|
||||
|
||||
describe('EditFormButtons', () => {
|
||||
let vm1;
|
||||
let vm2;
|
||||
|
||||
beforeEach(() => {
|
||||
const Component = Vue.extend(editFormButtons);
|
||||
const toggleForm = () => {};
|
||||
const updateLockedAttribute = () => {};
|
||||
|
||||
vm1 = mountComponent(Component, {
|
||||
isLocked: true,
|
||||
toggleForm,
|
||||
updateLockedAttribute,
|
||||
});
|
||||
|
||||
vm2 = mountComponent(Component, {
|
||||
isLocked: false,
|
||||
toggleForm,
|
||||
updateLockedAttribute,
|
||||
});
|
||||
});
|
||||
|
||||
it('renders unlock or lock text based on locked state', () => {
|
||||
expect(vm1.$el.innerHTML.includes('Unlock')).toBe(true);
|
||||
|
||||
expect(vm2.$el.innerHTML.includes('Lock')).toBe(true);
|
||||
});
|
||||
});
|
|
@ -1,202 +0,0 @@
|
|||
import Vue from 'vue';
|
||||
import mountComponent from 'spec/helpers/vue_mount_component_helper';
|
||||
import participants from '~/sidebar/components/participants/participants.vue';
|
||||
|
||||
const PARTICIPANT = {
|
||||
id: 1,
|
||||
state: 'active',
|
||||
username: 'marcene',
|
||||
name: 'Allie Will',
|
||||
web_url: 'foo.com',
|
||||
avatar_url: 'gravatar.com/avatar/xxx',
|
||||
};
|
||||
|
||||
const PARTICIPANT_LIST = [PARTICIPANT, { ...PARTICIPANT, id: 2 }, { ...PARTICIPANT, id: 3 }];
|
||||
|
||||
describe('Participants', function() {
|
||||
let vm;
|
||||
let Participants;
|
||||
|
||||
beforeEach(() => {
|
||||
Participants = Vue.extend(participants);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vm.$destroy();
|
||||
});
|
||||
|
||||
describe('collapsed sidebar state', () => {
|
||||
it('shows loading spinner when loading', () => {
|
||||
vm = mountComponent(Participants, {
|
||||
loading: true,
|
||||
});
|
||||
|
||||
expect(vm.$el.querySelector('.js-participants-collapsed-loading-icon')).toBeDefined();
|
||||
});
|
||||
|
||||
it('shows participant count when given', () => {
|
||||
vm = mountComponent(Participants, {
|
||||
loading: false,
|
||||
participants: PARTICIPANT_LIST,
|
||||
});
|
||||
const countEl = vm.$el.querySelector('.js-participants-collapsed-count');
|
||||
|
||||
expect(countEl.textContent.trim()).toBe(`${PARTICIPANT_LIST.length}`);
|
||||
});
|
||||
|
||||
it('shows full participant count when there are hidden participants', () => {
|
||||
vm = mountComponent(Participants, {
|
||||
loading: false,
|
||||
participants: PARTICIPANT_LIST,
|
||||
numberOfLessParticipants: 1,
|
||||
});
|
||||
const countEl = vm.$el.querySelector('.js-participants-collapsed-count');
|
||||
|
||||
expect(countEl.textContent.trim()).toBe(`${PARTICIPANT_LIST.length}`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('expanded sidebar state', () => {
|
||||
it('shows loading spinner when loading', () => {
|
||||
vm = mountComponent(Participants, {
|
||||
loading: true,
|
||||
});
|
||||
|
||||
expect(vm.$el.querySelector('.js-participants-expanded-loading-icon')).toBeDefined();
|
||||
});
|
||||
|
||||
it('when only showing visible participants, shows an avatar only for each participant under the limit', done => {
|
||||
const numberOfLessParticipants = 2;
|
||||
vm = mountComponent(Participants, {
|
||||
loading: false,
|
||||
participants: PARTICIPANT_LIST,
|
||||
numberOfLessParticipants,
|
||||
});
|
||||
vm.isShowingMoreParticipants = false;
|
||||
|
||||
Vue.nextTick()
|
||||
.then(() => {
|
||||
const participantEls = vm.$el.querySelectorAll('.js-participants-author');
|
||||
|
||||
expect(participantEls.length).toBe(numberOfLessParticipants);
|
||||
})
|
||||
.then(done)
|
||||
.catch(done.fail);
|
||||
});
|
||||
|
||||
it('when only showing all participants, each has an avatar', done => {
|
||||
const numberOfLessParticipants = 2;
|
||||
vm = mountComponent(Participants, {
|
||||
loading: false,
|
||||
participants: PARTICIPANT_LIST,
|
||||
numberOfLessParticipants,
|
||||
});
|
||||
vm.isShowingMoreParticipants = true;
|
||||
|
||||
Vue.nextTick()
|
||||
.then(() => {
|
||||
const participantEls = vm.$el.querySelectorAll('.js-participants-author');
|
||||
|
||||
expect(participantEls.length).toBe(PARTICIPANT_LIST.length);
|
||||
})
|
||||
.then(done)
|
||||
.catch(done.fail);
|
||||
});
|
||||
|
||||
it('does not have more participants link when they can all be shown', () => {
|
||||
const numberOfLessParticipants = 100;
|
||||
vm = mountComponent(Participants, {
|
||||
loading: false,
|
||||
participants: PARTICIPANT_LIST,
|
||||
numberOfLessParticipants,
|
||||
});
|
||||
const moreParticipantLink = vm.$el.querySelector('.js-toggle-participants-button');
|
||||
|
||||
expect(PARTICIPANT_LIST.length).toBeLessThan(numberOfLessParticipants);
|
||||
expect(moreParticipantLink).toBeNull();
|
||||
});
|
||||
|
||||
it('when too many participants, has more participants link to show more', done => {
|
||||
vm = mountComponent(Participants, {
|
||||
loading: false,
|
||||
participants: PARTICIPANT_LIST,
|
||||
numberOfLessParticipants: 2,
|
||||
});
|
||||
vm.isShowingMoreParticipants = false;
|
||||
|
||||
Vue.nextTick()
|
||||
.then(() => {
|
||||
const moreParticipantLink = vm.$el.querySelector('.js-toggle-participants-button');
|
||||
|
||||
expect(moreParticipantLink.textContent.trim()).toBe('+ 1 more');
|
||||
})
|
||||
.then(done)
|
||||
.catch(done.fail);
|
||||
});
|
||||
|
||||
it('when too many participants and already showing them, has more participants link to show less', done => {
|
||||
vm = mountComponent(Participants, {
|
||||
loading: false,
|
||||
participants: PARTICIPANT_LIST,
|
||||
numberOfLessParticipants: 2,
|
||||
});
|
||||
vm.isShowingMoreParticipants = true;
|
||||
|
||||
Vue.nextTick()
|
||||
.then(() => {
|
||||
const moreParticipantLink = vm.$el.querySelector('.js-toggle-participants-button');
|
||||
|
||||
expect(moreParticipantLink.textContent.trim()).toBe('- show less');
|
||||
})
|
||||
.then(done)
|
||||
.catch(done.fail);
|
||||
});
|
||||
|
||||
it('clicking more participants link emits event', () => {
|
||||
vm = mountComponent(Participants, {
|
||||
loading: false,
|
||||
participants: PARTICIPANT_LIST,
|
||||
numberOfLessParticipants: 2,
|
||||
});
|
||||
const moreParticipantLink = vm.$el.querySelector('.js-toggle-participants-button');
|
||||
|
||||
expect(vm.isShowingMoreParticipants).toBe(false);
|
||||
|
||||
moreParticipantLink.click();
|
||||
|
||||
expect(vm.isShowingMoreParticipants).toBe(true);
|
||||
});
|
||||
|
||||
it('clicking on participants icon emits `toggleSidebar` event', () => {
|
||||
vm = mountComponent(Participants, {
|
||||
loading: false,
|
||||
participants: PARTICIPANT_LIST,
|
||||
numberOfLessParticipants: 2,
|
||||
});
|
||||
spyOn(vm, '$emit');
|
||||
|
||||
const participantsIconEl = vm.$el.querySelector('.sidebar-collapsed-icon');
|
||||
|
||||
participantsIconEl.click();
|
||||
|
||||
expect(vm.$emit).toHaveBeenCalledWith('toggleSidebar');
|
||||
});
|
||||
});
|
||||
|
||||
describe('when not showing participants label', () => {
|
||||
beforeEach(() => {
|
||||
vm = mountComponent(Participants, {
|
||||
participants: PARTICIPANT_LIST,
|
||||
showParticipantLabel: false,
|
||||
});
|
||||
});
|
||||
|
||||
it('does not show sidebar collapsed icon', () => {
|
||||
expect(vm.$el.querySelector('.sidebar-collapsed-icon')).not.toBeTruthy();
|
||||
});
|
||||
|
||||
it('does not show participants label title', () => {
|
||||
expect(vm.$el.querySelector('.title')).not.toBeTruthy();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,134 +0,0 @@
|
|||
import MockAdapter from 'axios-mock-adapter';
|
||||
import axios from '~/lib/utils/axios_utils';
|
||||
import SidebarMediator from '~/sidebar/sidebar_mediator';
|
||||
import SidebarStore from '~/sidebar/stores/sidebar_store';
|
||||
import SidebarService, { gqClient } from '~/sidebar/services/sidebar_service';
|
||||
import Mock from './mock_data';
|
||||
|
||||
const { mediator: mediatorMockData } = Mock;
|
||||
|
||||
describe('Sidebar mediator', function() {
|
||||
let mock;
|
||||
|
||||
beforeEach(() => {
|
||||
mock = new MockAdapter(axios);
|
||||
|
||||
this.mediator = new SidebarMediator(mediatorMockData);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
SidebarService.singleton = null;
|
||||
SidebarStore.singleton = null;
|
||||
SidebarMediator.singleton = null;
|
||||
mock.restore();
|
||||
});
|
||||
|
||||
it('assigns yourself ', () => {
|
||||
this.mediator.assignYourself();
|
||||
|
||||
expect(this.mediator.store.currentUser).toEqual(mediatorMockData.currentUser);
|
||||
expect(this.mediator.store.assignees[0]).toEqual(mediatorMockData.currentUser);
|
||||
});
|
||||
|
||||
it('saves assignees', done => {
|
||||
mock.onPut(mediatorMockData.endpoint).reply(200, {});
|
||||
this.mediator
|
||||
.saveAssignees('issue[assignee_ids]')
|
||||
.then(resp => {
|
||||
expect(resp.status).toEqual(200);
|
||||
done();
|
||||
})
|
||||
.catch(done.fail);
|
||||
});
|
||||
|
||||
it('fetches the data', done => {
|
||||
const mockData = Mock.responseMap.GET[mediatorMockData.endpoint];
|
||||
mock.onGet(mediatorMockData.endpoint).reply(200, mockData);
|
||||
|
||||
const mockGraphQlData = Mock.graphQlResponseData;
|
||||
spyOn(gqClient, 'query').and.returnValue({
|
||||
data: mockGraphQlData,
|
||||
});
|
||||
|
||||
spyOn(this.mediator, 'processFetchedData').and.callThrough();
|
||||
|
||||
this.mediator
|
||||
.fetch()
|
||||
.then(() => {
|
||||
expect(this.mediator.processFetchedData).toHaveBeenCalledWith(mockData, mockGraphQlData);
|
||||
})
|
||||
.then(done)
|
||||
.catch(done.fail);
|
||||
});
|
||||
|
||||
it('processes fetched data', () => {
|
||||
const mockData = Mock.responseMap.GET[mediatorMockData.endpoint];
|
||||
this.mediator.processFetchedData(mockData);
|
||||
|
||||
expect(this.mediator.store.assignees).toEqual(mockData.assignees);
|
||||
expect(this.mediator.store.humanTimeEstimate).toEqual(mockData.human_time_estimate);
|
||||
expect(this.mediator.store.humanTotalTimeSpent).toEqual(mockData.human_total_time_spent);
|
||||
expect(this.mediator.store.participants).toEqual(mockData.participants);
|
||||
expect(this.mediator.store.subscribed).toEqual(mockData.subscribed);
|
||||
expect(this.mediator.store.timeEstimate).toEqual(mockData.time_estimate);
|
||||
expect(this.mediator.store.totalTimeSpent).toEqual(mockData.total_time_spent);
|
||||
});
|
||||
|
||||
it('sets moveToProjectId', () => {
|
||||
const projectId = 7;
|
||||
spyOn(this.mediator.store, 'setMoveToProjectId').and.callThrough();
|
||||
|
||||
this.mediator.setMoveToProjectId(projectId);
|
||||
|
||||
expect(this.mediator.store.setMoveToProjectId).toHaveBeenCalledWith(projectId);
|
||||
});
|
||||
|
||||
it('fetches autocomplete projects', done => {
|
||||
const searchTerm = 'foo';
|
||||
mock.onGet(mediatorMockData.projectsAutocompleteEndpoint).reply(200, {});
|
||||
spyOn(this.mediator.service, 'getProjectsAutocomplete').and.callThrough();
|
||||
spyOn(this.mediator.store, 'setAutocompleteProjects').and.callThrough();
|
||||
|
||||
this.mediator
|
||||
.fetchAutocompleteProjects(searchTerm)
|
||||
.then(() => {
|
||||
expect(this.mediator.service.getProjectsAutocomplete).toHaveBeenCalledWith(searchTerm);
|
||||
expect(this.mediator.store.setAutocompleteProjects).toHaveBeenCalled();
|
||||
})
|
||||
.then(done)
|
||||
.catch(done.fail);
|
||||
});
|
||||
|
||||
it('moves issue', done => {
|
||||
const mockData = Mock.responseMap.POST[mediatorMockData.moveIssueEndpoint];
|
||||
const moveToProjectId = 7;
|
||||
mock.onPost(mediatorMockData.moveIssueEndpoint).reply(200, mockData);
|
||||
this.mediator.store.setMoveToProjectId(moveToProjectId);
|
||||
spyOn(this.mediator.service, 'moveIssue').and.callThrough();
|
||||
const visitUrl = spyOnDependency(SidebarMediator, 'visitUrl');
|
||||
|
||||
this.mediator
|
||||
.moveIssue()
|
||||
.then(() => {
|
||||
expect(this.mediator.service.moveIssue).toHaveBeenCalledWith(moveToProjectId);
|
||||
expect(visitUrl).toHaveBeenCalledWith(mockData.web_url);
|
||||
})
|
||||
.then(done)
|
||||
.catch(done.fail);
|
||||
});
|
||||
|
||||
it('toggle subscription', done => {
|
||||
this.mediator.store.setSubscribedState(false);
|
||||
mock.onPost(mediatorMockData.toggleSubscriptionEndpoint).reply(200, {});
|
||||
spyOn(this.mediator.service, 'toggleSubscription').and.callThrough();
|
||||
|
||||
this.mediator
|
||||
.toggleSubscription()
|
||||
.then(() => {
|
||||
expect(this.mediator.service.toggleSubscription).toHaveBeenCalled();
|
||||
expect(this.mediator.store.subscribed).toEqual(true);
|
||||
})
|
||||
.then(done)
|
||||
.catch(done.fail);
|
||||
});
|
||||
});
|
|
@ -1,38 +0,0 @@
|
|||
import Vue from 'vue';
|
||||
import mountComponent from 'spec/helpers/vue_mount_component_helper';
|
||||
import sidebarSubscriptions from '~/sidebar/components/subscriptions/sidebar_subscriptions.vue';
|
||||
import SidebarMediator from '~/sidebar/sidebar_mediator';
|
||||
import SidebarService from '~/sidebar/services/sidebar_service';
|
||||
import SidebarStore from '~/sidebar/stores/sidebar_store';
|
||||
import Mock from './mock_data';
|
||||
|
||||
describe('Sidebar Subscriptions', function() {
|
||||
let vm;
|
||||
let SidebarSubscriptions;
|
||||
|
||||
beforeEach(() => {
|
||||
SidebarSubscriptions = Vue.extend(sidebarSubscriptions);
|
||||
// Set up the stores, services, etc
|
||||
// eslint-disable-next-line no-new
|
||||
new SidebarMediator(Mock.mediator);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vm.$destroy();
|
||||
SidebarService.singleton = null;
|
||||
SidebarStore.singleton = null;
|
||||
SidebarMediator.singleton = null;
|
||||
});
|
||||
|
||||
it('calls the mediator toggleSubscription on event', () => {
|
||||
const mediator = new SidebarMediator();
|
||||
spyOn(mediator, 'toggleSubscription').and.returnValue(Promise.resolve());
|
||||
vm = mountComponent(SidebarSubscriptions, {
|
||||
mediator,
|
||||
});
|
||||
|
||||
vm.onToggleSubscription();
|
||||
|
||||
expect(mediator.toggleSubscription).toHaveBeenCalled();
|
||||
});
|
||||
});
|
|
@ -1,100 +0,0 @@
|
|||
import Vue from 'vue';
|
||||
import mountComponent from 'spec/helpers/vue_mount_component_helper';
|
||||
import { mockTracking } from 'spec/helpers/tracking_helper';
|
||||
import subscriptions from '~/sidebar/components/subscriptions/subscriptions.vue';
|
||||
import eventHub from '~/sidebar/event_hub';
|
||||
|
||||
describe('Subscriptions', function() {
|
||||
let vm;
|
||||
let Subscriptions;
|
||||
|
||||
beforeEach(() => {
|
||||
Subscriptions = Vue.extend(subscriptions);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vm.$destroy();
|
||||
});
|
||||
|
||||
it('shows loading spinner when loading', () => {
|
||||
vm = mountComponent(Subscriptions, {
|
||||
loading: true,
|
||||
subscribed: undefined,
|
||||
});
|
||||
|
||||
expect(vm.$refs.toggleButton.isLoading).toBe(true);
|
||||
expect(vm.$refs.toggleButton.$el.querySelector('.project-feature-toggle')).toHaveClass(
|
||||
'is-loading',
|
||||
);
|
||||
});
|
||||
|
||||
it('is toggled "off" when currently not subscribed', () => {
|
||||
vm = mountComponent(Subscriptions, {
|
||||
subscribed: false,
|
||||
});
|
||||
|
||||
expect(vm.$refs.toggleButton.$el.querySelector('.project-feature-toggle')).not.toHaveClass(
|
||||
'is-checked',
|
||||
);
|
||||
});
|
||||
|
||||
it('is toggled "on" when currently subscribed', () => {
|
||||
vm = mountComponent(Subscriptions, {
|
||||
subscribed: true,
|
||||
});
|
||||
|
||||
expect(vm.$refs.toggleButton.$el.querySelector('.project-feature-toggle')).toHaveClass(
|
||||
'is-checked',
|
||||
);
|
||||
});
|
||||
|
||||
it('toggleSubscription method emits `toggleSubscription` event on eventHub and Component', () => {
|
||||
vm = mountComponent(Subscriptions, { subscribed: true });
|
||||
spyOn(eventHub, '$emit');
|
||||
spyOn(vm, '$emit');
|
||||
spyOn(vm, 'track');
|
||||
|
||||
vm.toggleSubscription();
|
||||
|
||||
expect(eventHub.$emit).toHaveBeenCalledWith('toggleSubscription', jasmine.any(Object));
|
||||
expect(vm.$emit).toHaveBeenCalledWith('toggleSubscription', jasmine.any(Object));
|
||||
});
|
||||
|
||||
it('tracks the event when toggled', () => {
|
||||
vm = mountComponent(Subscriptions, { subscribed: true });
|
||||
const spy = mockTracking('_category_', vm.$el, spyOn);
|
||||
vm.toggleSubscription();
|
||||
|
||||
expect(spy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('onClickCollapsedIcon method emits `toggleSidebar` event on component', () => {
|
||||
vm = mountComponent(Subscriptions, { subscribed: true });
|
||||
spyOn(vm, '$emit');
|
||||
|
||||
vm.onClickCollapsedIcon();
|
||||
|
||||
expect(vm.$emit).toHaveBeenCalledWith('toggleSidebar');
|
||||
});
|
||||
|
||||
describe('given project emails are disabled', () => {
|
||||
const subscribeDisabledDescription = 'Notifications have been disabled';
|
||||
|
||||
beforeEach(() => {
|
||||
vm = mountComponent(Subscriptions, {
|
||||
subscribed: false,
|
||||
projectEmailsDisabled: true,
|
||||
subscribeDisabledDescription,
|
||||
});
|
||||
});
|
||||
|
||||
it('sets the correct display text', () => {
|
||||
expect(vm.$el.textContent).toContain(subscribeDisabledDescription);
|
||||
expect(vm.$refs.tooltip.dataset.originalTitle).toBe(subscribeDisabledDescription);
|
||||
});
|
||||
|
||||
it('does not render the toggle button', () => {
|
||||
expect(vm.$refs.toggleButton).toBeUndefined();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,46 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
describe Gitlab::BackgroundMigration::BackfillEnvironmentIdDeploymentMergeRequests, schema: 20200312134637 do
|
||||
let(:environments) { table(:environments) }
|
||||
let(:merge_requests) { table(:merge_requests) }
|
||||
let(:deployments) { table(:deployments) }
|
||||
let(:deployment_merge_requests) { table(:deployment_merge_requests) }
|
||||
let(:namespaces) { table(:namespaces) }
|
||||
let(:projects) { table(:projects) }
|
||||
|
||||
subject(:migration) { described_class.new }
|
||||
|
||||
it 'correctly backfills environment_id column' do
|
||||
namespace = namespaces.create!(name: 'foo', path: 'foo')
|
||||
project = projects.create!(namespace_id: namespace.id)
|
||||
|
||||
production = environments.create!(project_id: project.id, name: 'production', slug: 'production')
|
||||
staging = environments.create!(project_id: project.id, name: 'staging', slug: 'staging')
|
||||
|
||||
mr = merge_requests.create!(source_branch: 'x', target_branch: 'master', target_project_id: project.id)
|
||||
|
||||
deployment1 = deployments.create!(environment_id: staging.id, iid: 1, project_id: project.id, ref: 'master', tag: false, sha: '123abcdef', status: 1)
|
||||
deployment2 = deployments.create!(environment_id: production.id, iid: 2, project_id: project.id, ref: 'master', tag: false, sha: '123abcdef', status: 1)
|
||||
deployment3 = deployments.create!(environment_id: production.id, iid: 3, project_id: project.id, ref: 'master', tag: false, sha: '123abcdef', status: 1)
|
||||
|
||||
# mr is tracked twice in production through deployment2 and deployment3
|
||||
deployment_merge_requests.create!(deployment_id: deployment1.id, merge_request_id: mr.id)
|
||||
deployment_merge_requests.create!(deployment_id: deployment2.id, merge_request_id: mr.id)
|
||||
deployment_merge_requests.create!(deployment_id: deployment3.id, merge_request_id: mr.id)
|
||||
|
||||
expect(deployment_merge_requests.where(environment_id: nil).count).to eq(3)
|
||||
|
||||
migration.perform(1, mr.id)
|
||||
|
||||
expect(deployment_merge_requests.where(environment_id: nil).count).to be_zero
|
||||
expect(deployment_merge_requests.count).to eq(2)
|
||||
|
||||
production_deployments = deployment_merge_requests.where(environment_id: production.id)
|
||||
expect(production_deployments.count).to eq(1)
|
||||
expect(production_deployments.first.deployment_id).to eq(deployment2.id)
|
||||
|
||||
expect(deployment_merge_requests.where(environment_id: staging.id).count).to eq(1)
|
||||
end
|
||||
end
|
|
@ -86,14 +86,6 @@ describe Gitlab::Danger::Changelog do
|
|||
end
|
||||
end
|
||||
|
||||
describe '#presented_no_changelog_labels' do
|
||||
subject { changelog.presented_no_changelog_labels }
|
||||
|
||||
it 'returns the labels formatted' do
|
||||
is_expected.to eq('~backstage, ~ci-build, ~meta')
|
||||
end
|
||||
end
|
||||
|
||||
describe '#ee_changelog?' do
|
||||
subject { changelog.ee_changelog? }
|
||||
|
||||
|
|
|
@ -399,9 +399,28 @@ describe Gitlab::Danger::Helper do
|
|||
end
|
||||
end
|
||||
|
||||
describe '#labels_list' do
|
||||
let(:labels) { ['telemetry', 'telemetry::reviewed'] }
|
||||
|
||||
it 'composes the labels string' do
|
||||
expect(helper.labels_list(labels)).to eq('~"telemetry", ~"telemetry::reviewed"')
|
||||
end
|
||||
|
||||
context 'when passing a separator' do
|
||||
it 'composes the labels string with the given separator' do
|
||||
expect(helper.labels_list(labels, sep: ' ')).to eq('~"telemetry" ~"telemetry::reviewed"')
|
||||
end
|
||||
end
|
||||
|
||||
it 'returns empty string for empty array' do
|
||||
expect(helper.labels_list([])).to eq('')
|
||||
end
|
||||
end
|
||||
|
||||
describe '#prepare_labels_for_mr' do
|
||||
it 'composes the labels string' do
|
||||
mr_labels = ['telemetry', 'telemetry::reviewed']
|
||||
|
||||
expect(helper.prepare_labels_for_mr(mr_labels)).to eq('/label ~"telemetry" ~"telemetry::reviewed"')
|
||||
end
|
||||
|
||||
|
|
|
@ -0,0 +1,81 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
require Rails.root.join('db', 'post_migrate', '20200312134637_backfill_environment_id_on_deployment_merge_requests.rb')
|
||||
|
||||
describe BackfillEnvironmentIdOnDeploymentMergeRequests do
|
||||
let(:environments) { table(:environments) }
|
||||
let(:merge_requests) { table(:merge_requests) }
|
||||
let(:deployments) { table(:deployments) }
|
||||
let(:deployment_merge_requests) { table(:deployment_merge_requests) }
|
||||
let(:namespaces) { table(:namespaces) }
|
||||
let(:projects) { table(:projects) }
|
||||
|
||||
let(:migration_worker) { double('BackgroundMigrationWorker') }
|
||||
|
||||
before do
|
||||
stub_const('BackgroundMigrationWorker', migration_worker)
|
||||
end
|
||||
|
||||
it 'schedules nothing when there are no entries' do
|
||||
expect(migration_worker).not_to receive(:perform_in)
|
||||
|
||||
migrate!
|
||||
end
|
||||
|
||||
it 'batches the workload' do
|
||||
stub_const("#{described_class.name}::BATCH_SIZE", 10)
|
||||
|
||||
namespace = namespaces.create!(name: 'foo', path: 'foo')
|
||||
project = projects.create!(namespace_id: namespace.id)
|
||||
|
||||
environment = environments.create!(project_id: project.id, name: 'staging', slug: 'staging')
|
||||
|
||||
# Batching is based on DeploymentMergeRequest.merge_request_id, in order to test it
|
||||
# we must generate more than described_class::BATCH_SIZE merge requests, deployments,
|
||||
# and deployment_merge_requests entries
|
||||
entries = 13
|
||||
expect(entries).to be > described_class::BATCH_SIZE
|
||||
|
||||
# merge requests and deployments bulk generation
|
||||
mrs_params = []
|
||||
deployments_params = []
|
||||
entries.times do |i|
|
||||
mrs_params << { source_branch: 'x', target_branch: 'master', target_project_id: project.id }
|
||||
|
||||
deployments_params << { environment_id: environment.id, iid: i + 1, project_id: project.id, ref: 'master', tag: false, sha: '123abcdef', status: 1 }
|
||||
end
|
||||
|
||||
all_mrs = merge_requests.insert_all(mrs_params)
|
||||
all_deployments = deployments.insert_all(deployments_params)
|
||||
|
||||
# deployment_merge_requests bulk generation
|
||||
dmr_params = []
|
||||
entries.times do |index|
|
||||
mr_id = all_mrs.rows[index].first
|
||||
deployment_id = all_deployments.rows[index].first
|
||||
|
||||
dmr_params << { deployment_id: deployment_id, merge_request_id: mr_id }
|
||||
end
|
||||
|
||||
deployment_merge_requests.insert_all(dmr_params)
|
||||
|
||||
first_batch_limit = dmr_params[described_class::BATCH_SIZE][:merge_request_id]
|
||||
second_batch_limit = dmr_params.last[:merge_request_id]
|
||||
|
||||
expect(migration_worker).to receive(:perform_in)
|
||||
.with(
|
||||
0,
|
||||
'BackfillEnvironmentIdDeploymentMergeRequests',
|
||||
[1, first_batch_limit]
|
||||
)
|
||||
expect(migration_worker).to receive(:perform_in)
|
||||
.with(
|
||||
described_class::DELAY,
|
||||
'BackfillEnvironmentIdDeploymentMergeRequests',
|
||||
[first_batch_limit + 1, second_batch_limit]
|
||||
)
|
||||
|
||||
migrate!
|
||||
end
|
||||
end
|
|
@ -3700,41 +3700,41 @@ describe MergeRequest do
|
|||
describe '#recent_visible_deployments' do
|
||||
let(:merge_request) { create(:merge_request) }
|
||||
|
||||
let(:environment) do
|
||||
create(:environment, project: merge_request.target_project)
|
||||
end
|
||||
|
||||
it 'returns visible deployments' do
|
||||
envs = create_list(:environment, 3, project: merge_request.target_project)
|
||||
|
||||
created = create(
|
||||
:deployment,
|
||||
:created,
|
||||
project: merge_request.target_project,
|
||||
environment: environment
|
||||
environment: envs[0]
|
||||
)
|
||||
|
||||
success = create(
|
||||
:deployment,
|
||||
:success,
|
||||
project: merge_request.target_project,
|
||||
environment: environment
|
||||
environment: envs[1]
|
||||
)
|
||||
|
||||
failed = create(
|
||||
:deployment,
|
||||
:failed,
|
||||
project: merge_request.target_project,
|
||||
environment: environment
|
||||
environment: envs[2]
|
||||
)
|
||||
|
||||
merge_request.deployment_merge_requests.create!(deployment: created)
|
||||
merge_request.deployment_merge_requests.create!(deployment: success)
|
||||
merge_request.deployment_merge_requests.create!(deployment: failed)
|
||||
merge_request_relation = MergeRequest.where(id: merge_request.id)
|
||||
created.link_merge_requests(merge_request_relation)
|
||||
success.link_merge_requests(merge_request_relation)
|
||||
failed.link_merge_requests(merge_request_relation)
|
||||
|
||||
expect(merge_request.recent_visible_deployments).to eq([failed, success])
|
||||
end
|
||||
|
||||
it 'only returns a limited number of deployments' do
|
||||
20.times do
|
||||
environment = create(:environment, project: merge_request.target_project)
|
||||
deploy = create(
|
||||
:deployment,
|
||||
:success,
|
||||
|
@ -3742,7 +3742,7 @@ describe MergeRequest do
|
|||
environment: environment
|
||||
)
|
||||
|
||||
merge_request.deployment_merge_requests.create!(deployment: deploy)
|
||||
deploy.link_merge_requests(MergeRequest.where(id: merge_request.id))
|
||||
end
|
||||
|
||||
expect(merge_request.recent_visible_deployments.count).to eq(10)
|
||||
|
|
|
@ -538,18 +538,6 @@ describe User, :do_not_mock_admin_mode do
|
|||
expect(user).to be_valid
|
||||
end
|
||||
|
||||
context 'when feature flag is turned off' do
|
||||
before do
|
||||
stub_feature_flags(email_restrictions: false)
|
||||
end
|
||||
|
||||
it 'does accept the email address' do
|
||||
user = build(:user, email: 'info+1@test.com')
|
||||
|
||||
expect(user).to be_valid
|
||||
end
|
||||
end
|
||||
|
||||
context 'when created_by_id is set' do
|
||||
it 'does accept the email address' do
|
||||
user = build(:user, email: 'info+1@test.com', created_by_id: 1)
|
||||
|
|
|
@ -439,7 +439,7 @@ describe API::Deployments do
|
|||
let!(:merge_request3) { create(:merge_request, source_project: project2, target_project: project2) }
|
||||
|
||||
it 'returns the relevant merge requests linked to a deployment for a project' do
|
||||
deployment.merge_requests << [merge_request1, merge_request2]
|
||||
deployment.link_merge_requests(MergeRequest.where(id: [merge_request1.id, merge_request2.id]))
|
||||
|
||||
subject
|
||||
|
||||
|
|
|
@ -3,26 +3,26 @@
|
|||
require 'spec_helper'
|
||||
|
||||
describe API::Issues do
|
||||
let_it_be(:user) { create(:user) }
|
||||
let(:user2) { create(:user) }
|
||||
let(:non_member) { create(:user) }
|
||||
let_it_be(:guest) { create(:user) }
|
||||
let_it_be(:author) { create(:author) }
|
||||
let_it_be(:assignee) { create(:assignee) }
|
||||
let(:admin) { create(:user, :admin) }
|
||||
let(:issue_title) { 'foo' }
|
||||
let(:issue_description) { 'closed' }
|
||||
let(:no_milestone_title) { 'None' }
|
||||
let(:any_milestone_title) { 'Any' }
|
||||
let_it_be(:user2) { create(:user) }
|
||||
let_it_be(:admin) { create(:user, :admin) }
|
||||
let_it_be(:non_member) { create(:user) }
|
||||
let_it_be(:user) { create(:user) }
|
||||
let_it_be(:guest) { create(:user) }
|
||||
let_it_be(:author) { create(:author) }
|
||||
let_it_be(:assignee) { create(:assignee) }
|
||||
let_it_be(:issue_title) { 'foo' }
|
||||
let_it_be(:issue_description) { 'closed' }
|
||||
let_it_be(:no_milestone_title) { 'None' }
|
||||
let_it_be(:any_milestone_title) { 'Any' }
|
||||
|
||||
before do
|
||||
stub_licensed_features(multiple_issue_assignees: false, issue_weights: false)
|
||||
end
|
||||
|
||||
describe 'GET /groups/:id/issues' do
|
||||
let!(:group) { create(:group) }
|
||||
let!(:group_project) { create(:project, :public, :repository, creator_id: user.id, namespace: group) }
|
||||
let!(:private_mrs_project) do
|
||||
let_it_be(:group) { create(:group) }
|
||||
let_it_be(:group_project) { create(:project, :public, :repository, creator_id: user.id, namespace: group) }
|
||||
let_it_be(:private_mrs_project) do
|
||||
create(:project, :public, :repository, creator_id: user.id, namespace: group, merge_requests_access_level: ProjectFeature::PRIVATE)
|
||||
end
|
||||
|
||||
|
@ -455,6 +455,29 @@ describe API::Issues do
|
|||
it_behaves_like 'labeled issues with labels and label_name params'
|
||||
end
|
||||
|
||||
context 'with archived projects' do
|
||||
let_it_be(:archived_issue) do
|
||||
create(
|
||||
:issue, author: user, assignees: [user],
|
||||
project: create(:project, :public, :archived, creator_id: user.id, namespace: group)
|
||||
)
|
||||
end
|
||||
|
||||
it 'returns only non archived projects issues' do
|
||||
get api(base_url, user)
|
||||
|
||||
expect_paginated_array_response([group_closed_issue.id, group_confidential_issue.id, group_issue.id])
|
||||
end
|
||||
|
||||
it 'returns issues from archived projects if non_archived it set to false' do
|
||||
get api(base_url, user), params: { non_archived: false }
|
||||
|
||||
expect_paginated_array_response(
|
||||
[archived_issue.id, group_closed_issue.id, group_confidential_issue.id, group_issue.id]
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
it 'returns an array of issues found by iids' do
|
||||
get api(base_url, user), params: { iids: [group_issue.iid] }
|
||||
|
||||
|
|
|
@ -780,28 +780,20 @@ describe API::Issues do
|
|||
end
|
||||
|
||||
context 'filtering by non_archived' do
|
||||
let_it_be(:group1) { create(:group) }
|
||||
let_it_be(:archived_project) { create(:project, :archived, namespace: group1) }
|
||||
let_it_be(:active_project) { create(:project, namespace: group1) }
|
||||
let_it_be(:issue1) { create(:issue, project: active_project) }
|
||||
let_it_be(:issue2) { create(:issue, project: active_project) }
|
||||
let_it_be(:issue3) { create(:issue, project: archived_project) }
|
||||
let_it_be(:archived_project) { create(:project, :archived, creator_id: user.id, namespace: user.namespace) }
|
||||
let_it_be(:archived_issue) { create(:issue, author: user, project: archived_project) }
|
||||
let_it_be(:active_issue) { create(:issue, author: user, project: project) }
|
||||
|
||||
before do
|
||||
archived_project.add_developer(user)
|
||||
active_project.add_developer(user)
|
||||
it 'returns issues from non archived projects by default' do
|
||||
get api('/issues', user)
|
||||
|
||||
expect_paginated_array_response(active_issue.id, issue.id, closed_issue.id)
|
||||
end
|
||||
|
||||
it 'returns issues from non archived projects only by default' do
|
||||
get api("/groups/#{group1.id}/issues", user), params: { scope: 'all' }
|
||||
it 'returns issues from archived project with non_archived set as false' do
|
||||
get api("/issues", user), params: { non_archived: false }
|
||||
|
||||
expect_paginated_array_response([issue2.id, issue1.id])
|
||||
end
|
||||
|
||||
it 'returns issues from archived and non archived projects when non_archived is false' do
|
||||
get api("/groups/#{group1.id}/issues", user), params: { non_archived: false, scope: 'all' }
|
||||
|
||||
expect_paginated_array_response([issue3.id, issue2.id, issue1.id])
|
||||
expect_paginated_array_response(active_issue.id, archived_issue.id, issue.id, closed_issue.id)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -129,16 +129,6 @@ describe API::Search do
|
|||
|
||||
it_behaves_like 'response is correct', schema: 'public_api/v4/snippets'
|
||||
end
|
||||
|
||||
context 'for snippet_blobs scope' do
|
||||
before do
|
||||
create(:snippet, :public, title: 'awesome snippet', content: 'snippet content')
|
||||
|
||||
get api('/search', user), params: { scope: 'snippet_blobs', search: 'content' }
|
||||
end
|
||||
|
||||
it_behaves_like 'response is correct', schema: 'public_api/v4/snippets'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -33,5 +33,11 @@ describe RuboCop::Cop::Migration::AddConcurrentForeignKey do
|
|||
expect(cop.offenses.map(&:line)).to eq([1])
|
||||
end
|
||||
end
|
||||
|
||||
it 'does not register an offense when a `NOT VALID` foreign key is added' do
|
||||
inspect_source('def up; add_foreign_key(:projects, :users, column: :user_id, validate: false); end')
|
||||
|
||||
expect(cop.offenses).to be_empty
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -110,6 +110,31 @@ describe Issues::CreateService do
|
|||
end
|
||||
end
|
||||
|
||||
context 'when labels is nil' do
|
||||
let(:opts) do
|
||||
{ title: 'Title',
|
||||
description: 'Description',
|
||||
labels: nil }
|
||||
end
|
||||
|
||||
it 'does not assign label' do
|
||||
expect(issue.labels).to be_empty
|
||||
end
|
||||
end
|
||||
|
||||
context 'when labels is nil and label_ids is present' do
|
||||
let(:opts) do
|
||||
{ title: 'Title',
|
||||
description: 'Description',
|
||||
labels: nil,
|
||||
label_ids: labels.map(&:id) }
|
||||
end
|
||||
|
||||
it 'assigns group labels' do
|
||||
expect(issue.labels).to match_array labels
|
||||
end
|
||||
end
|
||||
|
||||
context 'when milestone belongs to different project' do
|
||||
let(:milestone) { create(:milestone) }
|
||||
|
||||
|
|
39
yarn.lock
39
yarn.lock
|
@ -1041,6 +1041,21 @@
|
|||
resolved "https://registry.yarnpkg.com/@sourcegraph/code-host-integration/-/code-host-integration-0.0.37.tgz#87f9a602e2a60520b6038311a67face2ece86827"
|
||||
integrity sha512-GQvNuPORLjsMhto57Ue1umeSV3cir+hMEaGxwCKmmq+cc9ZSZpuXa8RVBXuT5azN99K9/8zFps4woyPJ8wrjYA==
|
||||
|
||||
"@toast-ui/editor@^2.0.1":
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@toast-ui/editor/-/editor-2.0.1.tgz#749e5be1f02f42ded51488d1575ab1c19ca59952"
|
||||
integrity sha512-TC481O/zP37boY6H6oVN6KLVMY7yrU8zQu+3xqZ71V3Sr6D2XyaGb2Xub9XqTdqzBmzsf7y4Gi+EXO0IQ3rGVA==
|
||||
dependencies:
|
||||
"@types/codemirror" "0.0.71"
|
||||
codemirror "^5.48.4"
|
||||
|
||||
"@toast-ui/vue-editor@^2.0.1":
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@toast-ui/vue-editor/-/vue-editor-2.0.1.tgz#c9c8c8da4c0a67b9fbc4240464388c67d72a0c22"
|
||||
integrity sha512-sGsApl0n+GVAZbmPA+tTrq9rmmyh2mRgCgg2/mu1/lN7S4vPv/nQH8KXxLG9Y6hG2+kgelqz6wvbOCdzlM/HmQ==
|
||||
dependencies:
|
||||
"@toast-ui/editor" "^2.0.1"
|
||||
|
||||
"@types/anymatch@*":
|
||||
version "1.3.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/anymatch/-/anymatch-1.3.0.tgz#d1d55958d1fccc5527d4aba29fc9c4b942f563ff"
|
||||
|
@ -1079,6 +1094,18 @@
|
|||
dependencies:
|
||||
"@babel/types" "^7.3.0"
|
||||
|
||||
"@types/codemirror@0.0.71":
|
||||
version "0.0.71"
|
||||
resolved "https://registry.yarnpkg.com/@types/codemirror/-/codemirror-0.0.71.tgz#861f1bcb3100c0a064567c5400f2981cf4ae8ca7"
|
||||
integrity sha512-b2oEEnno1LIGKMR7uBEsr40al1UijF1HEpRn0+Yf1xOLl24iQgB7DBpZVMM7y54G5wCNoclDrRO65E6KHPNO2w==
|
||||
dependencies:
|
||||
"@types/tern" "*"
|
||||
|
||||
"@types/estree@*":
|
||||
version "0.0.44"
|
||||
resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.44.tgz#980cc5a29a3ef3bea6ff1f7d021047d7ea575e21"
|
||||
integrity sha512-iaIVzr+w2ZJ5HkidlZ3EJM8VTZb2MJLCjw3V+505yVts0gRC4UMvjw0d1HPtGqI/HQC/KdsYtayfzl+AXY2R8g==
|
||||
|
||||
"@types/events@*":
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/events/-/events-1.2.0.tgz#81a6731ce4df43619e5c8c945383b3e62a89ea86"
|
||||
|
@ -1138,6 +1165,13 @@
|
|||
resolved "https://registry.yarnpkg.com/@types/tapable/-/tapable-1.0.4.tgz#b4ffc7dc97b498c969b360a41eee247f82616370"
|
||||
integrity sha512-78AdXtlhpCHT0K3EytMpn4JNxaf5tbqbLcbIRoQIHzpTIyjpxLQKRoxU55ujBXAtg3Nl2h/XWvfDa9dsMOd0pQ==
|
||||
|
||||
"@types/tern@*":
|
||||
version "0.23.3"
|
||||
resolved "https://registry.yarnpkg.com/@types/tern/-/tern-0.23.3.tgz#4b54538f04a88c9ff79de1f6f94f575a7f339460"
|
||||
integrity sha512-imDtS4TAoTcXk0g7u4kkWqedB3E4qpjXzCpD2LU5M5NAXHzCDsypyvXSaG7mM8DKYkCRa7tFp4tS/lp/Wo7Q3w==
|
||||
dependencies:
|
||||
"@types/estree" "*"
|
||||
|
||||
"@types/uglify-js@*":
|
||||
version "3.0.4"
|
||||
resolved "https://registry.yarnpkg.com/@types/uglify-js/-/uglify-js-3.0.4.tgz#96beae23df6f561862a830b4288a49e86baac082"
|
||||
|
@ -2764,6 +2798,11 @@ code-point-at@^1.0.0:
|
|||
resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77"
|
||||
integrity sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=
|
||||
|
||||
codemirror@^5.48.4:
|
||||
version "5.53.2"
|
||||
resolved "https://registry.yarnpkg.com/codemirror/-/codemirror-5.53.2.tgz#9799121cf8c50809cca487304e9de3a74d33f428"
|
||||
integrity sha512-wvSQKS4E+P8Fxn/AQ+tQtJnF1qH5UOlxtugFLpubEZ5jcdH2iXTVinb+Xc/4QjshuOxRm4fUsU2QPF1JJKiyXA==
|
||||
|
||||
codesandbox-api@0.0.23:
|
||||
version "0.0.23"
|
||||
resolved "https://registry.yarnpkg.com/codesandbox-api/-/codesandbox-api-0.0.23.tgz#bf650a21b5f3c2369e03f0c19d10b4e2ba255b4f"
|
||||
|
|
Loading…
Reference in a new issue