BigW Consortium Gitlab

Commit 012c126d by Kushal Pandya

Merge branch '9-3-stable-rc5' into '9-3-stable'

Prepare 9.3 RC5 See merge request !12283
parents a23e9766 feeba2ec
...@@ -80,7 +80,18 @@ import initSettingsPanels from './settings_panels'; ...@@ -80,7 +80,18 @@ import initSettingsPanels from './settings_panels';
path = page.split(':'); path = page.split(':');
shortcut_handler = null; shortcut_handler = null;
new GfmAutoComplete(gl.GfmAutoComplete && gl.GfmAutoComplete.dataSources).setup(); $('.js-gfm-input').each((i, el) => {
const gfm = new GfmAutoComplete(gl.GfmAutoComplete && gl.GfmAutoComplete.dataSources);
const enableGFM = gl.utils.convertPermissionToBoolean(el.dataset.supportsAutocomplete);
gfm.setup($(el), {
emojis: true,
members: enableGFM,
issues: enableGFM,
milestones: enableGFM,
mergeRequests: enableGFM,
labels: enableGFM,
});
});
function initBlob() { function initBlob() {
new LineHighlighter(); new LineHighlighter();
...@@ -180,7 +191,7 @@ import initSettingsPanels from './settings_panels'; ...@@ -180,7 +191,7 @@ import initSettingsPanels from './settings_panels';
case 'groups:milestones:update': case 'groups:milestones:update':
new ZenMode(); new ZenMode();
new gl.DueDateSelectors(); new gl.DueDateSelectors();
new gl.GLForm($('.milestone-form')); new gl.GLForm($('.milestone-form'), true);
break; break;
case 'projects:compare:show': case 'projects:compare:show':
new gl.Diff(); new gl.Diff();
...@@ -192,7 +203,7 @@ import initSettingsPanels from './settings_panels'; ...@@ -192,7 +203,7 @@ import initSettingsPanels from './settings_panels';
case 'projects:issues:new': case 'projects:issues:new':
case 'projects:issues:edit': case 'projects:issues:edit':
shortcut_handler = new ShortcutsNavigation(); shortcut_handler = new ShortcutsNavigation();
new gl.GLForm($('.issue-form')); new gl.GLForm($('.issue-form'), true);
new IssuableForm($('.issue-form')); new IssuableForm($('.issue-form'));
new LabelsSelect(); new LabelsSelect();
new MilestoneSelect(); new MilestoneSelect();
...@@ -203,7 +214,7 @@ import initSettingsPanels from './settings_panels'; ...@@ -203,7 +214,7 @@ import initSettingsPanels from './settings_panels';
case 'projects:merge_requests:edit': case 'projects:merge_requests:edit':
new gl.Diff(); new gl.Diff();
shortcut_handler = new ShortcutsNavigation(); shortcut_handler = new ShortcutsNavigation();
new gl.GLForm($('.merge-request-form')); new gl.GLForm($('.merge-request-form'), true);
new IssuableForm($('.merge-request-form')); new IssuableForm($('.merge-request-form'));
new LabelsSelect(); new LabelsSelect();
new MilestoneSelect(); new MilestoneSelect();
...@@ -212,22 +223,24 @@ import initSettingsPanels from './settings_panels'; ...@@ -212,22 +223,24 @@ import initSettingsPanels from './settings_panels';
break; break;
case 'projects:tags:new': case 'projects:tags:new':
new ZenMode(); new ZenMode();
new gl.GLForm($('.tag-form')); new gl.GLForm($('.tag-form'), true);
new RefSelectDropdown($('.js-branch-select'), window.gl.availableRefs); new RefSelectDropdown($('.js-branch-select'), window.gl.availableRefs);
break; break;
case 'projects:snippets:new': case 'projects:snippets:new':
case 'projects:snippets:edit': case 'projects:snippets:edit':
case 'projects:snippets:create': case 'projects:snippets:create':
case 'projects:snippets:update': case 'projects:snippets:update':
new gl.GLForm($('.snippet-form'), true);
break;
case 'snippets:new': case 'snippets:new':
case 'snippets:edit': case 'snippets:edit':
case 'snippets:create': case 'snippets:create':
case 'snippets:update': case 'snippets:update':
new gl.GLForm($('.snippet-form')); new gl.GLForm($('.snippet-form'), false);
break; break;
case 'projects:releases:edit': case 'projects:releases:edit':
new ZenMode(); new ZenMode();
new gl.GLForm($('.release-form')); new gl.GLForm($('.release-form'), true);
break; break;
case 'projects:merge_requests:show': case 'projects:merge_requests:show':
new gl.Diff(); new gl.Diff();
...@@ -475,7 +488,7 @@ import initSettingsPanels from './settings_panels'; ...@@ -475,7 +488,7 @@ import initSettingsPanels from './settings_panels';
new gl.Wikis(); new gl.Wikis();
shortcut_handler = new ShortcutsWiki(); shortcut_handler = new ShortcutsWiki();
new ZenMode(); new ZenMode();
new gl.GLForm($('.wiki-form')); new gl.GLForm($('.wiki-form'), true);
break; break;
case 'snippets': case 'snippets':
shortcut_handler = new ShortcutsNavigation(); shortcut_handler = new ShortcutsNavigation();
......
...@@ -287,6 +287,10 @@ window.DropzoneInput = (function() { ...@@ -287,6 +287,10 @@ window.DropzoneInput = (function() {
$uploadingErrorMessage.html(message); $uploadingErrorMessage.html(message);
}; };
closeAlertMessage = function() {
return form.find('.div-dropzone-alert').alert('close');
};
form.find('.markdown-selector').click(function(e) { form.find('.markdown-selector').click(function(e) {
e.preventDefault(); e.preventDefault();
$(this).closest('.gfm-form').find('.div-dropzone').click(); $(this).closest('.gfm-form').find('.div-dropzone').click();
......
...@@ -51,6 +51,11 @@ export default { ...@@ -51,6 +51,11 @@ export default {
required: false, required: false,
default: '', default: '',
}, },
initialTaskStatus: {
type: String,
required: false,
default: '',
},
updatedAt: { updatedAt: {
type: String, type: String,
required: false, required: false,
...@@ -105,6 +110,7 @@ export default { ...@@ -105,6 +110,7 @@ export default {
updatedAt: this.updatedAt, updatedAt: this.updatedAt,
updatedByName: this.updatedByName, updatedByName: this.updatedByName,
updatedByPath: this.updatedByPath, updatedByPath: this.updatedByPath,
taskStatus: this.initialTaskStatus,
}); });
return { return {
......
...@@ -37,23 +37,12 @@ ...@@ -37,23 +37,12 @@
}); });
}, },
taskStatus() { taskStatus() {
const taskRegexMatches = this.taskStatus.match(/(\d+) of (\d+)/); this.updateTaskStatusText();
const $issuableHeader = $('.issuable-meta');
const $tasks = $('#task_status', $issuableHeader);
const $tasksShort = $('#task_status_short', $issuableHeader);
if (taskRegexMatches) {
$tasks.text(this.taskStatus);
$tasksShort.text(`${taskRegexMatches[1]}/${taskRegexMatches[2]} task${taskRegexMatches[2] > 1 ? 's' : ''}`);
} else {
$tasks.text('');
$tasksShort.text('');
}
}, },
}, },
methods: { methods: {
renderGFM() { renderGFM() {
$(this.$refs['gfm-entry-content']).renderGFM(); $(this.$refs['gfm-content']).renderGFM();
if (this.canUpdate) { if (this.canUpdate) {
// eslint-disable-next-line no-new // eslint-disable-next-line no-new
...@@ -64,9 +53,24 @@ ...@@ -64,9 +53,24 @@
}); });
} }
}, },
updateTaskStatusText() {
const taskRegexMatches = this.taskStatus.match(/(\d+) of ((?!0)\d+)/);
const $issuableHeader = $('.issuable-meta');
const $tasks = $('#task_status', $issuableHeader);
const $tasksShort = $('#task_status_short', $issuableHeader);
if (taskRegexMatches) {
$tasks.text(this.taskStatus);
$tasksShort.text(`${taskRegexMatches[1]}/${taskRegexMatches[2]} task${taskRegexMatches[2] > 1 ? 's' : ''}`);
} else {
$tasks.text('');
$tasksShort.text('');
}
},
}, },
mounted() { mounted() {
this.renderGFM(); this.renderGFM();
this.updateTaskStatusText();
}, },
}; };
</script> </script>
......
...@@ -45,6 +45,7 @@ document.addEventListener('DOMContentLoaded', () => { ...@@ -45,6 +45,7 @@ document.addEventListener('DOMContentLoaded', () => {
updatedAt: this.updatedAt, updatedAt: this.updatedAt,
updatedByName: this.updatedByName, updatedByName: this.updatedByName,
updatedByPath: this.updatedByPath, updatedByPath: this.updatedByPath,
initialTaskStatus: this.initialTaskStatus,
}, },
}); });
}, },
......
export default class Store { export default class Store {
constructor({ constructor(initialState) {
titleHtml, this.state = initialState;
titleText,
descriptionHtml,
descriptionText,
updatedAt,
updatedByName,
updatedByPath,
}) {
this.state = {
titleHtml,
titleText,
descriptionHtml,
descriptionText,
taskStatus: '',
updatedAt,
updatedByName,
updatedByPath,
};
this.formState = { this.formState = {
title: '', title: '',
confidential: false, confidential: false,
......
...@@ -322,7 +322,9 @@ const normalizeNewlines = function(str) { ...@@ -322,7 +322,9 @@ const normalizeNewlines = function(str) {
Notes.updateNoteTargetSelector = function($note) { Notes.updateNoteTargetSelector = function($note) {
const hash = gl.utils.getLocationHash(); const hash = gl.utils.getLocationHash();
$note.toggleClass('target', hash && $note.filter(`#${hash}`).length > 0); // Needs to be an explicit true/false for the jQuery `toggleClass(force)`
const addTargetClass = Boolean(hash && $note.filter(`#${hash}`).length > 0);
$note.toggleClass('target', addTargetClass);
}; };
/* /*
...@@ -1267,7 +1269,6 @@ const normalizeNewlines = function(str) { ...@@ -1267,7 +1269,6 @@ const normalizeNewlines = function(str) {
*/ */
Notes.prototype.createPlaceholderNote = function ({ formContent, uniqueId, isDiscussionNote, currentUsername, currentUserFullname, currentUserAvatar }) { Notes.prototype.createPlaceholderNote = function ({ formContent, uniqueId, isDiscussionNote, currentUsername, currentUserFullname, currentUserAvatar }) {
const discussionClass = isDiscussionNote ? 'discussion' : ''; const discussionClass = isDiscussionNote ? 'discussion' : '';
const escapedFormContent = _.escape(formContent);
const $tempNote = $( const $tempNote = $(
`<li id="${uniqueId}" class="note being-posted fade-in-half timeline-entry"> `<li id="${uniqueId}" class="note being-posted fade-in-half timeline-entry">
<div class="timeline-entry-inner"> <div class="timeline-entry-inner">
...@@ -1287,7 +1288,7 @@ const normalizeNewlines = function(str) { ...@@ -1287,7 +1288,7 @@ const normalizeNewlines = function(str) {
</div> </div>
<div class="note-body"> <div class="note-body">
<div class="note-text"> <div class="note-text">
<p>${escapedFormContent}</p> <p>${formContent}</p>
</div> </div>
</div> </div>
</div> </div>
......
export default { export default {
name: 'MRWidgetRelatedLinks', name: 'MRWidgetRelatedLinks',
props: { props: {
isMerged: { type: Boolean, required: true },
relatedLinks: { type: Object, required: true }, relatedLinks: { type: Object, required: true },
}, },
computed: { computed: {
// TODO: the following should be handled by i18n
closingText() {
if (this.isMerged) {
return `Closed ${this.issueLabel('closing')}`;
}
return `Closes ${this.issueLabel('closing')}`;
},
hasLinks() { hasLinks() {
const { closing, mentioned, assignToMe } = this.relatedLinks; const { closing, mentioned, assignToMe } = this.relatedLinks;
return closing || mentioned || assignToMe; return closing || mentioned || assignToMe;
}, },
// TODO: the following should be handled by i18n
mentionedText() {
if (this.isMerged) {
if (this.hasMultipleIssues(this.relatedLinks.mentioned)) {
return 'are mentioned but were not closed';
}
return 'is mentioned but was not closed';
}
if (this.hasMultipleIssues(this.relatedLinks.mentioned)) {
return 'are mentioned but will not be closed';
}
return 'is mentioned but will not be closed';
},
}, },
methods: { methods: {
hasMultipleIssues(text) { hasMultipleIssues(text) {
return !text ? false : text.match(/<\/a> and <a/); return /<\/a>,? and <a/.test(text);
}, },
// TODO: the following should be handled by i18n
issueLabel(field) { issueLabel(field) {
return this.hasMultipleIssues(this.relatedLinks[field]) ? 'issues' : 'issue'; return this.hasMultipleIssues(this.relatedLinks[field]) ? 'issues' : 'issue';
}, },
verbLabel(field) {
return this.hasMultipleIssues(this.relatedLinks[field]) ? 'are' : 'is';
},
}, },
template: ` template: `
<section <div v-if="hasLinks">
v-if="hasLinks"
class="mr-info-list mr-links">
<div class="legend"></div> <div class="legend"></div>
<p v-if="relatedLinks.closing"> <p v-if="relatedLinks.closing">
Closes {{issueLabel('closing')}} {{closingText}}
<span v-html="relatedLinks.closing"></span>. <span v-html="relatedLinks.closing"></span>.
</p> </p>
<p v-if="relatedLinks.mentioned"> <p v-if="relatedLinks.mentioned">
<span class="capitalize">{{issueLabel('mentioned')}}</span> <span class="capitalize">{{issueLabel('mentioned')}}</span>
<span v-html="relatedLinks.mentioned"></span> <span v-html="relatedLinks.mentioned"></span>
{{verbLabel('mentioned')}} mentioned but will not be closed. {{mentionedText}}
</p> </p>
<p v-if="relatedLinks.assignToMe"> <p v-if="relatedLinks.assignToMe">
<span v-html="relatedLinks.assignToMe"></span> <span v-html="relatedLinks.assignToMe"></span>
</p> </p>
</section> </div>
`, `,
}; };
/* global Flash */ /* global Flash */
import mrWidgetAuthorTime from '../../components/mr_widget_author_time'; import mrWidgetAuthorTime from '../../components/mr_widget_author_time';
import mrWidgetRelatedLinks from '../../components/mr_widget_related_links';
import eventHub from '../../event_hub'; import eventHub from '../../event_hub';
import '../../../flash';
export default { export default {
name: 'MRWidgetMerged', name: 'MRWidgetMerged',
...@@ -11,6 +13,7 @@ export default { ...@@ -11,6 +13,7 @@ export default {
}, },
components: { components: {
'mr-widget-author-and-time': mrWidgetAuthorTime, 'mr-widget-author-and-time': mrWidgetAuthorTime,
'mr-widget-related-links': mrWidgetRelatedLinks,
}, },
data() { data() {
return { return {
...@@ -18,6 +21,9 @@ export default { ...@@ -18,6 +21,9 @@ export default {
}; };
}, },
computed: { computed: {
shouldRenderRelatedLinks() {
return this.mr.relatedLinks && this.mr.isMerged;
},
shouldShowRemoveSourceBranch() { shouldShowRemoveSourceBranch() {
const { sourceBranchRemoved, isRemovingSourceBranch, canRemoveSourceBranch } = this.mr; const { sourceBranchRemoved, isRemovingSourceBranch, canRemoveSourceBranch } = this.mr;
...@@ -86,6 +92,10 @@ export default { ...@@ -86,6 +92,10 @@ export default {
aria-hidden="true" /> aria-hidden="true" />
The source branch is being removed. The source branch is being removed.
</p> </p>
<mr-widget-related-links
v-if="shouldRenderRelatedLinks"
:is-merged="mr.isMerged()"
:related-links="mr.relatedLinks" />
</section> </section>
<div <div
v-if="shouldShowMergedButtons" v-if="shouldShowMergedButtons"
......
...@@ -48,7 +48,7 @@ export default { ...@@ -48,7 +48,7 @@ export default {
return stateMaps.stateToComponentMap[this.mr.state]; return stateMaps.stateToComponentMap[this.mr.state];
}, },
shouldRenderMergeHelp() { shouldRenderMergeHelp() {
return stateMaps.statesToShowHelpWidget.indexOf(this.mr.state) > -1; return !this.mr.isMerged;
}, },
shouldRenderPipelines() { shouldRenderPipelines() {
return Object.keys(this.mr.pipeline).length || this.mr.hasCI; return Object.keys(this.mr.pipeline).length || this.mr.hasCI;
...@@ -238,9 +238,14 @@ export default { ...@@ -238,9 +238,14 @@ export default {
:is="componentName" :is="componentName"
:mr="mr" :mr="mr"
:service="service" /> :service="service" />
<mr-widget-related-links <section
v-if="shouldRenderRelatedLinks" v-if="shouldRenderRelatedLinks"
:related-links="mr.relatedLinks" /> class="mr-info-list mr-links">
<div class="legend"></div>
<mr-widget-related-links
:is-merged="mr.isMerged"
:related-links="mr.relatedLinks" />
</section>
<mr-widget-merge-help v-if="shouldRenderMergeHelp" /> <mr-widget-merge-help v-if="shouldRenderMergeHelp" />
</div> </div>
`, `,
......
import Timeago from 'timeago.js'; import Timeago from 'timeago.js';
import { getStateKey } from '../dependencies'; import { getStateKey } from '../dependencies';
const unmergedStates = [
'locked',
'conflicts',
'workInProgress',
'readyToMerge',
'checking',
'unresolvedDiscussions',
'pipelineFailed',
'pipelineBlocked',
'autoMergeFailed',
];
export default class MergeRequestStore { export default class MergeRequestStore {
constructor(data) { constructor(data) {
...@@ -65,6 +77,7 @@ export default class MergeRequestStore { ...@@ -65,6 +77,7 @@ export default class MergeRequestStore {
this.mergeActionsContentPath = data.commit_change_content_path; this.mergeActionsContentPath = data.commit_change_content_path;
this.isRemovingSourceBranch = this.isRemovingSourceBranch || false; this.isRemovingSourceBranch = this.isRemovingSourceBranch || false;
this.isOpen = data.state === 'opened' || data.state === 'reopened' || false; this.isOpen = data.state === 'opened' || data.state === 'reopened' || false;
this.isMerged = unmergedStates.indexOf(data.state) === -1;
this.hasMergeableDiscussionsState = data.mergeable_discussions_state === false; this.hasMergeableDiscussionsState = data.mergeable_discussions_state === false;
this.canRemoveSourceBranch = currentUser.can_remove_source_branch || false; this.canRemoveSourceBranch = currentUser.can_remove_source_branch || false;
this.canMerge = !!data.merge_path; this.canMerge = !!data.merge_path;
......
...@@ -19,19 +19,6 @@ const stateToComponentMap = { ...@@ -19,19 +19,6 @@ const stateToComponentMap = {
shaMismatch: 'mr-widget-sha-mismatch', shaMismatch: 'mr-widget-sha-mismatch',
}; };
const statesToShowHelpWidget = [
'locked',
'conflicts',
'workInProgress',
'readyToMerge',
'checking',
'unresolvedDiscussions',
'pipelineFailed',
'pipelineBlocked',
'autoMergeFailed',
];
export default { export default {
stateToComponentMap, stateToComponentMap,
statesToShowHelpWidget,
}; };
...@@ -372,6 +372,10 @@ ...@@ -372,6 +372,10 @@
margin-left: 12px; margin-left: 12px;
} }
&.mr-state-locked + .mr-info-list.mr-links {
margin-top: -16px;
}
&.empty-state { &.empty-state {
.artwork { .artwork {
margin-bottom: $gl-padding; margin-bottom: $gl-padding;
......
...@@ -509,11 +509,6 @@ ul.notes { ...@@ -509,11 +509,6 @@ ul.notes {
display: inline; display: inline;
line-height: 20px; line-height: 20px;
@include notes-media('min', $screen-sm-min) {
margin-left: 10px;
line-height: 24px;
}
.fa { .fa {
color: $gray-darkest; color: $gray-darkest;
position: relative; position: relative;
......
...@@ -66,12 +66,12 @@ module DiffHelper ...@@ -66,12 +66,12 @@ module DiffHelper
discussions_left = discussions_right = nil discussions_left = discussions_right = nil
if left && (left.unchanged? || left.discussable?) if left && left.discussable? && (left.unchanged? || left.removed?)
line_code = diff_file.line_code(left) line_code = diff_file.line_code(left)
discussions_left = @grouped_diff_discussions[line_code] discussions_left = @grouped_diff_discussions[line_code]
end end
if right&.discussable? if right && right.discussable? && right.added?
line_code = diff_file.line_code(right) line_code = diff_file.line_code(right)
discussions_right = @grouped_diff_discussions[line_code] discussions_right = @grouped_diff_discussions[line_code]
end end
......
...@@ -138,8 +138,8 @@ module IssuablesHelper ...@@ -138,8 +138,8 @@ module IssuablesHelper
end end
output << "&ensp;".html_safe output << "&ensp;".html_safe
output << content_tag(:span, issuable.task_status, id: "task_status", class: "hidden-xs hidden-sm") output << content_tag(:span, (issuable.task_status if issuable.tasks?), id: "task_status", class: "hidden-xs hidden-sm")
output << content_tag(:span, issuable.task_status_short, id: "task_status_short", class: "hidden-md hidden-lg") output << content_tag(:span, (issuable.task_status_short if issuable.tasks?), id: "task_status_short", class: "hidden-md hidden-lg")
output output
end end
...@@ -216,7 +216,8 @@ module IssuablesHelper ...@@ -216,7 +216,8 @@ module IssuablesHelper
initialTitleHtml: markdown_field(issuable, :title), initialTitleHtml: markdown_field(issuable, :title),
initialTitleText: issuable.title, initialTitleText: issuable.title,
initialDescriptionHtml: markdown_field(issuable, :description), initialDescriptionHtml: markdown_field(issuable, :description),
initialDescriptionText: issuable.description initialDescriptionText: issuable.description,
initialTaskStatus: issuable.task_status
} }
data.merge!(updated_at_by(issuable)) data.merge!(updated_at_by(issuable))
......
...@@ -42,7 +42,7 @@ class LegacyDiffNote < Note ...@@ -42,7 +42,7 @@ class LegacyDiffNote < Note
end end
def for_line?(line) def for_line?(line)
!line.meta? && diff_file.line_code(line) == self.line_code line.discussable? && diff_file.line_code(line) == self.line_code
end end
def original_line_code def original_line_code
......
...@@ -34,7 +34,7 @@ module Users ...@@ -34,7 +34,7 @@ module Users
# Keep trying until we obtain the lease. If we don't do so we may end up # Keep trying until we obtain the lease. If we don't do so we may end up
# not updating the list of authorized projects properly. To prevent # not updating the list of authorized projects properly. To prevent
# hammering Redis too much we'll wait for a bit between retries. # hammering Redis too much we'll wait for a bit between retries.
sleep(1) sleep(0.1)
end end
begin begin
......
- @gfm_form = true - @gfm_form = true
- current_text ||= nil - current_text ||= nil
- supports_autocomplete = local_assigns.fetch(:supports_autocomplete, true)
- supports_slash_commands = local_assigns.fetch(:supports_slash_commands, false) - supports_slash_commands = local_assigns.fetch(:supports_slash_commands, false)
.zen-backdrop .zen-backdrop
- classes << ' js-gfm-input js-autosize markdown-area' - classes << ' js-gfm-input js-autosize markdown-area'
- if defined?(f) && f - if defined?(f) && f
= f.text_area attr, class: classes, placeholder: placeholder, data: { supports_slash_commands: supports_slash_commands } = f.text_area attr,
class: classes,
placeholder: placeholder,
data: { supports_slash_commands: supports_slash_commands,
supports_autocomplete: supports_autocomplete }
- else - else
= text_area_tag attr, current_text, class: classes, placeholder: placeholder = text_area_tag attr, current_text, class: classes, placeholder: placeholder
%a.zen-control.zen-control-leave.js-zen-leave{ href: "#" } %a.zen-control.zen-control-leave.js-zen-leave{ href: "#" }
......
- supports_autocomplete = local_assigns.fetch(:supports_autocomplete, true)
- supports_slash_commands = note_supports_slash_commands?(@note) - supports_slash_commands = note_supports_slash_commands?(@note)
- if supports_slash_commands - if supports_slash_commands
- preview_url = preview_markdown_path(@project, slash_commands_target_type: @note.noteable_type, slash_commands_target_id: @note.noteable_id) - preview_url = preview_markdown_path(@project, slash_commands_target_type: @note.noteable_type, slash_commands_target_id: @note.noteable_id)
...@@ -27,7 +28,8 @@ ...@@ -27,7 +28,8 @@
attr: :note, attr: :note,
classes: 'note-textarea js-note-text', classes: 'note-textarea js-note-text',
placeholder: "Write a comment or drag your files here...", placeholder: "Write a comment or drag your files here...",
supports_slash_commands: supports_slash_commands supports_slash_commands: supports_slash_commands,
supports_autocomplete: supports_autocomplete
= render 'shared/notes/hints', supports_slash_commands: supports_slash_commands = render 'shared/notes/hints', supports_slash_commands: supports_slash_commands
.error-alert .error-alert
......
...@@ -12,7 +12,7 @@ ...@@ -12,7 +12,7 @@
%a.author_link{ href: user_path(current_user) } %a.author_link{ href: user_path(current_user) }
= image_tag avatar_icon(current_user), alt: current_user.to_reference, class: 'avatar s40' = image_tag avatar_icon(current_user), alt: current_user.to_reference, class: 'avatar s40'
.timeline-content.timeline-content-form .timeline-content.timeline-content-form
= render "shared/notes/form", view: diff_view = render "shared/notes/form", view: diff_view, supports_autocomplete: autocomplete
- elsif !current_user - elsif !current_user
.disabled-comment.text-center.prepend-top-default .disabled-comment.text-center.prepend-top-default
Please Please
......
---
title: Fix for cut & pasted images not working
merge_request:
author:
---
title: Reduce time spent waiting for certain Sidekiq jobs to complete
merge_request:
author:
...@@ -46,7 +46,10 @@ To disable artifacts site-wide, follow the steps below. ...@@ -46,7 +46,10 @@ To disable artifacts site-wide, follow the steps below.
After a successful job, GitLab Runner uploads an archive containing the job After a successful job, GitLab Runner uploads an archive containing the job
artifacts to GitLab. artifacts to GitLab.
To change the location where the artifacts are stored, follow the steps below. ### Using local storage
To change the location where the artifacts are stored locally, follow the steps
below.
--- ---
...@@ -82,6 +85,13 @@ _The artifacts are stored by default in ...@@ -82,6 +85,13 @@ _The artifacts are stored by default in
1. Save the file and [restart GitLab][] for the changes to take effect. 1. Save the file and [restart GitLab][] for the changes to take effect.
### Using object storage
In [GitLab Enterprise Edition Premium][eep] you can use an object storage like
AWS S3 to store the artifacts.
[Learn how to use the object storage option.][ee-os]
## Expiring artifacts ## Expiring artifacts
If an expiry date is used for the artifacts, they are marked for deletion If an expiry date is used for the artifacts, they are marked for deletion
...@@ -148,3 +158,5 @@ memory and disk I/O. ...@@ -148,3 +158,5 @@ memory and disk I/O.
[reconfigure gitlab]: restart_gitlab.md "How to restart GitLab" [reconfigure gitlab]: restart_gitlab.md "How to restart GitLab"
[restart gitlab]: restart_gitlab.md "How to restart GitLab" [restart gitlab]: restart_gitlab.md "How to restart GitLab"
[gitlab workhorse]: https://gitlab.com/gitlab-org/gitlab-workhorse "GitLab Workhorse repository" [gitlab workhorse]: https://gitlab.com/gitlab-org/gitlab-workhorse "GitLab Workhorse repository"
[ee-os]: https://docs.gitlab.com/ee/administration/job_artifacts.html#using-object-storage
[eep]: https://about.gitlab.com/gitlab-ee/ "GitLab Enterprise Edition Premium"
...@@ -89,6 +89,7 @@ group. ...@@ -89,6 +89,7 @@ group.
| Create project in group | | | | ✓ | ✓ | | Create project in group | | | | ✓ | ✓ |
| Manage group members | | | | | ✓ | | Manage group members | | | | | ✓ |
| Remove group | | | | | ✓ | | Remove group | | | | | ✓ |
| Manage group labels | | ✓ | ✓ | ✓ | ✓ |
## External Users ## External Users
......
...@@ -10,43 +10,49 @@ module Gitlab ...@@ -10,43 +10,49 @@ module Gitlab
delegate :sidekiq_throttling_enabled?, to: :current_application_settings delegate :sidekiq_throttling_enabled?, to: :current_application_settings
def fake_application_settings def fake_application_settings(defaults = ApplicationSetting.defaults)
OpenStruct.new(::ApplicationSetting.defaults) FakeApplicationSettings.new(defaults)
end end
private private
def ensure_application_settings! def ensure_application_settings!
unless ENV['IN_MEMORY_APPLICATION_SETTINGS'] == 'true' return in_memory_application_settings if ENV['IN_MEMORY_APPLICATION_SETTINGS'] == 'true'
settings = retrieve_settings_from_database?
end
settings || in_memory_application_settings cached_application_settings || uncached_application_settings
end end
def retrieve_settings_from_database? def cached_application_settings
settings = retrieve_settings_from_database_cache?
return settings if settings.present?
return fake_application_settings unless connect_to_db?
begin begin
db_settings = ::ApplicationSetting.current ApplicationSetting.cached
# In case Redis isn't running or the Redis UNIX socket file is not available
rescue ::Redis::BaseError, ::Errno::ENOENT rescue ::Redis::BaseError, ::Errno::ENOENT
db_settings = ::ApplicationSetting.last # In case Redis isn't running or the Redis UNIX socket file is not available
end end
db_settings || ::ApplicationSetting.create_from_defaults
end end
def retrieve_settings_from_database_cache? def uncached_application_settings
return fake_application_settings unless connect_to_db?
# This loads from the database into the cache, so handle Redis errors
begin begin
settings = ApplicationSetting.cached db_settings = ApplicationSetting.current
rescue ::Redis::BaseError, ::Errno::ENOENT rescue ::Redis::BaseError, ::Errno::ENOENT
# In case Redis isn't running or the Redis UNIX socket file is not available # In case Redis isn't running or the Redis UNIX socket file is not available
settings = nil
end end
settings
# If there are pending migrations, it's possible there are columns that
# need to be added to the application settings. To prevent Rake tasks
# and other callers from failing, use any loaded settings and return
# defaults for missing columns.
if ActiveRecord::Migrator.needs_migration?
defaults = ApplicationSetting.defaults
defaults.merge!(db_settings.attributes.symbolize_keys) if db_settings.present?
return fake_application_settings(defaults)
end
return db_settings if db_settings.present?
ApplicationSetting.create_from_defaults || in_memory_application_settings
end end
def in_memory_application_settings def in_memory_application_settings
......
...@@ -42,25 +42,25 @@ module Gitlab ...@@ -42,25 +42,25 @@ module Gitlab
end end
def added? def added?
type == 'new' || type == 'new-nonewline' %w[new new-nonewline].include?(type)
end end
def removed? def removed?
type == 'old' || type == 'old-nonewline' %w[old old-nonewline].include?(type)
end
def rich_text
@parent_file.highlight_lines! if @parent_file && !@rich_text
@rich_text
end end
def meta? def meta?
type == 'match' %w[match new-nonewline old-nonewline].include?(type)
end end
def discussable? def discussable?
!['match', 'new-nonewline', 'old-nonewline'].include?(type) !meta?
end
def rich_text
@parent_file.highlight_lines! if @parent_file && !@rich_text
@rich_text
end end
def as_json(opts = nil) def as_json(opts = nil)
......
...@@ -14,16 +14,7 @@ module Gitlab ...@@ -14,16 +14,7 @@ module Gitlab
lines = [] lines = []
highlighted_diff_lines = diff_file.highlighted_diff_lines highlighted_diff_lines = diff_file.highlighted_diff_lines
highlighted_diff_lines.each do |line| highlighted_diff_lines.each do |line|
if line.meta? || line.unchanged? if line.removed?
# line in the right panel is the same as in the left one
lines << {
left: line,
right: line
}
free_right_index = nil
i += 1
elsif line.removed?
lines << { lines << {
left: line, left: line,
right: nil right: nil
...@@ -51,6 +42,15 @@ module Gitlab ...@@ -51,6 +42,15 @@ module Gitlab
free_right_index = nil free_right_index = nil
i += 1 i += 1
end end
elsif line.meta? || line.unchanged?
# line in the right panel is the same as in the left one
lines << {
left: line,
right: line
}
free_right_index = nil
i += 1
end end
end end
......
# This class extends an OpenStruct object by adding predicate methods to mimic
# ActiveRecord access. We rely on the initial values being true or false to
# determine whether to define a predicate method because for a newly-added
# column that has not been migrated yet, there is no way to determine the
# column type without parsing db/schema.rb.
module Gitlab
class FakeApplicationSettings < OpenStruct
def initialize(options = {})
super
FakeApplicationSettings.define_predicate_methods(options)
end
# Mimic ActiveRecord predicate methods for boolean values
def self.define_predicate_methods(options)
options.each do |key, value|
next if key.to_s.end_with?('?')
next unless [true, false].include?(value)
define_method "#{key}?" do
actual_key = key.to_s.chomp('?')
self[actual_key]
end
end
end
end
end
...@@ -14,7 +14,7 @@ module Gitlab ...@@ -14,7 +14,7 @@ module Gitlab
# timeout - The maximum amount of seconds to block the caller for. This # timeout - The maximum amount of seconds to block the caller for. This
# ensures we don't indefinitely block a caller in case a job takes # ensures we don't indefinitely block a caller in case a job takes
# long to process, or is never processed. # long to process, or is never processed.
def wait(timeout = 60) def wait(timeout = 10)
start = Time.current start = Time.current
while (Time.current - start) <= timeout while (Time.current - start) <= timeout
......
FactoryGirl.define do
factory :application_setting do
end
end
...@@ -210,6 +210,13 @@ describe 'New/edit issue', :feature, :js do ...@@ -210,6 +210,13 @@ describe 'New/edit issue', :feature, :js do
expect(find('.js-assignee-search')).to have_content(user2.name) expect(find('.js-assignee-search')).to have_content(user2.name)
end end
it 'description has autocomplete' do
find('#issue_description').native.send_keys('')
fill_in 'issue_description', with: '@'
expect(page).to have_selector('.atwho-view')
end
end end
context 'edit issue' do context 'edit issue' do
...@@ -258,6 +265,13 @@ describe 'New/edit issue', :feature, :js do ...@@ -258,6 +265,13 @@ describe 'New/edit issue', :feature, :js do
end end
end end
end end
it 'description has autocomplete' do
find('#issue_description').native.send_keys('')
fill_in 'issue_description', with: '@'
expect(page).to have_selector('.atwho-view')
end
end end
describe 'sub-group project' do describe 'sub-group project' do
......
...@@ -36,7 +36,7 @@ feature 'Merge Request closing issues message', feature: true, js: true do ...@@ -36,7 +36,7 @@ feature 'Merge Request closing issues message', feature: true, js: true do
let(:merge_request_description) { "Description\n\nclosing #{issue_1.to_reference}, #{issue_2.to_reference}" } let(:merge_request_description) { "Description\n\nclosing #{issue_1.to_reference}, #{issue_2.to_reference}" }
it 'does not display closing issue message' do it 'does not display closing issue message' do
expect(page).to have_content("Closes issues #{issue_1.to_reference} and #{issue_2.to_reference}") expect(page).to have_content("Closed issues #{issue_1.to_reference} and #{issue_2.to_reference}")
end end
end end
...@@ -44,7 +44,7 @@ feature 'Merge Request closing issues message', feature: true, js: true do ...@@ -44,7 +44,7 @@ feature 'Merge Request closing issues message', feature: true, js: true do
let(:merge_request_description) { "Description\n\nRefers to #{issue_1.to_reference} and #{issue_2.to_reference}" } let(:merge_request_description) { "Description\n\nRefers to #{issue_1.to_reference} and #{issue_2.to_reference}" }
it 'does not display closing issue message' do it 'does not display closing issue message' do
expect(page).to have_content("Issues #{issue_1.to_reference} and #{issue_2.to_reference} are mentioned but will not be closed.") expect(page).to have_content("Issues #{issue_1.to_reference} and #{issue_2.to_reference} are mentioned but were not closed")
end end
end end
...@@ -52,8 +52,8 @@ feature 'Merge Request closing issues message', feature: true, js: true do ...@@ -52,8 +52,8 @@ feature 'Merge Request closing issues message', feature: true, js: true do
let(:merge_request_title) { "closes #{issue_1.to_reference}\n\n refers to #{issue_2.to_reference}" } let(:merge_request_title) { "closes #{issue_1.to_reference}\n\n refers to #{issue_2.to_reference}" }
it 'does not display closing issue message' do it 'does not display closing issue message' do
expect(page).to have_content("Closes issue #{issue_1.to_reference}.") expect(page).to have_content("Closed issue #{issue_1.to_reference}")
expect(page).to have_content("Issue #{issue_2.to_reference} is mentioned but will not be closed.") expect(page).to have_content("Issue #{issue_2.to_reference} is mentioned but was not closed")
end end
end end
...@@ -61,7 +61,7 @@ feature 'Merge Request closing issues message', feature: true, js: true do ...@@ -61,7 +61,7 @@ feature 'Merge Request closing issues message', feature: true, js: true do
let(:merge_request_title) { "closing #{issue_1.to_reference}, #{issue_2.to_reference}" } let(:merge_request_title) { "closing #{issue_1.to_reference}, #{issue_2.to_reference}" }
it 'does not display closing issue message' do it 'does not display closing issue message' do
expect(page).to have_content("Closes issues #{issue_1.to_reference} and #{issue_2.to_reference}") expect(page).to have_content("Closed issues #{issue_1.to_reference} and #{issue_2.to_reference}")
end end
end end
...@@ -69,7 +69,7 @@ feature 'Merge Request closing issues message', feature: true, js: true do ...@@ -69,7 +69,7 @@ feature 'Merge Request closing issues message', feature: true, js: true do
let(:merge_request_title) { "Refers to #{issue_1.to_reference} and #{issue_2.to_reference}" } let(:merge_request_title) { "Refers to #{issue_1.to_reference} and #{issue_2.to_reference}" }
it 'does not display closing issue message' do it 'does not display closing issue message' do
expect(page).to have_content("Issues #{issue_1.to_reference} and #{issue_2.to_reference} are mentioned but will not be closed.") expect(page).to have_content("Issues #{issue_1.to_reference} and #{issue_2.to_reference} are mentioned but were not closed")
end end
end end
...@@ -77,8 +77,8 @@ feature 'Merge Request closing issues message', feature: true, js: true do ...@@ -77,8 +77,8 @@ feature 'Merge Request closing issues message', feature: true, js: true do
let(:merge_request_title) { "closes #{issue_1.to_reference}\n\n refers to #{issue_2.to_reference}" } let(:merge_request_title) { "closes #{issue_1.to_reference}\n\n refers to #{issue_2.to_reference}" }
it 'does not display closing issue message' do it 'does not display closing issue message' do
expect(page).to have_content("Closes issue #{issue_1.to_reference}. Issue #{issue_2.to_reference} is mentioned but will not be closed.") expect(page).to have_content("Closed issue #{issue_1.to_reference}")
expect(page).to have_content("Issue #{issue_2.to_reference} is mentioned but will not be closed.") expect(page).to have_content("Issue #{issue_2.to_reference} is mentioned but was not closed")
end end
end end
end end
...@@ -96,6 +96,13 @@ describe 'New/edit merge request', feature: true, js: true do ...@@ -96,6 +96,13 @@ describe 'New/edit merge request', feature: true, js: true do
.to end_with(merge_request_path(merge_request)) .to end_with(merge_request_path(merge_request))
end end
end end
it 'description has autocomplete' do
find('#merge_request_description').native.send_keys('')
fill_in 'merge_request_description', with: '@'
expect(page).to have_selector('.atwho-view')
end
end end
context 'edit merge request' do context 'edit merge request' do
...@@ -157,6 +164,13 @@ describe 'New/edit merge request', feature: true, js: true do ...@@ -157,6 +164,13 @@ describe 'New/edit merge request', feature: true, js: true do
end end
end end
end end
it 'description has autocomplete' do
find('#merge_request_description').native.send_keys('')
fill_in 'merge_request_description', with: '@'
expect(page).to have_selector('.atwho-view')
end
end end
end end
......
require 'spec_helper'
feature 'Creating a new project milestone', :feature, :js do
let(:user) { create(:user) }
let(:project) { create(:empty_project, name: 'test', namespace: user.namespace) }
before do
login_as(user)
visit new_namespace_project_milestone_path(project.namespace, project)
end
it 'description has autocomplete' do
find('#milestone_description').native.send_keys('')
fill_in 'milestone_description', with: '@'
expect(page).to have_selector('.atwho-view')
end
end
require 'spec_helper' require 'spec_helper'
describe 'Project snippets', feature: true do describe 'Project snippets', :js, feature: true do
context 'when the project has snippets' do context 'when the project has snippets' do
let(:project) { create(:empty_project, :public) } let(:project) { create(:empty_project, :public) }
let!(:snippets) { create_list(:project_snippet, 2, :public, author: project.owner, project: project) } let!(:snippets) { create_list(:project_snippet, 2, :public, author: project.owner, project: project) }
...@@ -26,5 +26,19 @@ describe 'Project snippets', feature: true do ...@@ -26,5 +26,19 @@ describe 'Project snippets', feature: true do
expect(page).to have_content(snippets[1].title) expect(page).to have_content(snippets[1].title)
end end
end end
context 'when submitting a note' do
before do
login_as :admin
visit namespace_project_snippet_path(project.namespace, project, snippets[0])
end
it 'should have autocomplete' do
find('#note_note').native.send_keys('')
fill_in 'note[note]', with: '@'
expect(page).to have_selector('.atwho-view')
end
end
end end
end end
...@@ -133,6 +133,22 @@ feature 'Projects > Wiki > User creates wiki page', js: true, feature: true do ...@@ -133,6 +133,22 @@ feature 'Projects > Wiki > User creates wiki page', js: true, feature: true do
expect(page).to have_content('My awesome wiki!') expect(page).to have_content('My awesome wiki!')
end end
end end
scenario 'content has autocomplete', :js do
click_link 'New page'
page.within '#modal-new-wiki' do
fill_in :new_wiki_path, with: 'test-autocomplete'
click_button 'Create page'
end
page.within '.wiki-form' do
find('#wiki_content').native.send_keys('')
fill_in :wiki_content, with: '@'
end
expect(page).to have_selector('.atwho-view')
end
end end
end end
......
...@@ -5,11 +5,10 @@ feature 'Projects > Wiki > User updates wiki page', feature: true do ...@@ -5,11 +5,10 @@ feature 'Projects > Wiki > User updates wiki page', feature: true do
background do background do
project.team << [user, :master] project.team << [user, :master]
WikiPages::CreateService.new(project, user, title: 'home', content: 'Home page').execute
login_as(user) login_as(user)
visit namespace_project_path(project.namespace, project) visit namespace_project_wikis_path(project.namespace, project)
WikiPages::CreateService.new(project, user, title: 'home', content: 'Home page').execute
click_link 'Wiki'
end end
context 'in the user namespace' do context 'in the user namespace' do
...@@ -42,6 +41,15 @@ feature 'Projects > Wiki > User updates wiki page', feature: true do ...@@ -42,6 +41,15 @@ feature 'Projects > Wiki > User updates wiki page', feature: true do
expect(page).to have_content('Content can\'t be blank') expect(page).to have_content('Content can\'t be blank')
expect(find('textarea#wiki_content').value).to eq '' expect(find('textarea#wiki_content').value).to eq ''
end end
scenario 'content has autocomplete', :js do
click_link 'Edit'
find('#wiki_content').native.send_keys('')
fill_in :wiki_content, with: '@'
expect(page).to have_selector('.atwho-view')
end
end end
end end
......
...@@ -70,6 +70,22 @@ describe 'Comments on personal snippets', :js, feature: true do ...@@ -70,6 +70,22 @@ describe 'Comments on personal snippets', :js, feature: true do
expect(find('div#notes')).to have_content('This is awesome!') expect(find('div#notes')).to have_content('This is awesome!')
end end
it 'should not have autocomplete' do
wait_for_requests
request_count_before = page.driver.network_traffic.count
find('#note_note').native.send_keys('')
fill_in 'note[note]', with: '@'
wait_for_requests
request_count_after = page.driver.network_traffic.count
# This selector probably won't be in place even if autocomplete was enabled
# but we want to make sure
expect(page).not_to have_selector('.atwho-view')
expect(request_count_before).to eq(request_count_after)
end
end end
context 'when editing a note' do context 'when editing a note' do
......
...@@ -7,61 +7,79 @@ feature 'Master creates tag', feature: true do ...@@ -7,61 +7,79 @@ feature 'Master creates tag', feature: true do
before do before do
project.team << [user, :master] project.team << [user, :master]
login_with(user) login_with(user)
visit namespace_project_tags_path(project.namespace, project)
end end
scenario 'with an invalid name displays an error' do context 'from tag list' do
create_tag_in_form(tag: 'v 1.0', ref: 'master') before do
visit namespace_project_tags_path(project.namespace, project)
end
expect(page).to have_content 'Tag name invalid' scenario 'with an invalid name displays an error' do
end create_tag_in_form(tag: 'v 1.0', ref: 'master')
scenario 'with an invalid reference displays an error' do expect(page).to have_content 'Tag name invalid'
create_tag_in_form(tag: 'v2.0', ref: 'foo') end
expect(page).to have_content 'Target foo is invalid' scenario 'with an invalid reference displays an error' do
end create_tag_in_form(tag: 'v2.0', ref: 'foo')
scenario 'that already exists displays an error' do expect(page).to have_content 'Target foo is invalid'
create_tag_in_form(tag: 'v1.1.0', ref: 'master') end
expect(page).to have_content 'Tag v1.1.0 already exists' scenario 'that already exists displays an error' do
end create_tag_in_form(tag: 'v1.1.0', ref: 'master')
expect(page).to have_content 'Tag v1.1.0 already exists'
end
scenario 'with multiline message displays the message in a <pre> block' do scenario 'with multiline message displays the message in a <pre> block' do
create_tag_in_form(tag: 'v3.0', ref: 'master', message: "Awesome tag message\n\n- hello\n- world") create_tag_in_form(tag: 'v3.0', ref: 'master', message: "Awesome tag message\n\n- hello\n- world")
expect(current_path).to eq( expect(current_path).to eq(
namespace_project_tag_path(project.namespace, project, 'v3.0')) namespace_project_tag_path(project.namespace, project, 'v3.0'))
expect(page).to have_content 'v3.0' expect(page).to have_content 'v3.0'
page.within 'pre.wrap' do page.within 'pre.wrap' do
expect(page).to have_content "Awesome tag message\n\n- hello\n- world" expect(page).to have_content "Awesome tag message\n\n- hello\n- world"
end
end end
end
scenario 'with multiline release notes parses the release note as Markdown' do scenario 'with multiline release notes parses the release note as Markdown' do
create_tag_in_form(tag: 'v4.0', ref: 'master', desc: "Awesome release notes\n\n- hello\n- world") create_tag_in_form(tag: 'v4.0', ref: 'master', desc: "Awesome release notes\n\n- hello\n- world")
expect(current_path).to eq( expect(current_path).to eq(
namespace_project_tag_path(project.namespace, project, 'v4.0')) namespace_project_tag_path(project.namespace, project, 'v4.0'))
expect(page).to have_content 'v4.0' expect(page).to have_content 'v4.0'
page.within '.description' do page.within '.description' do
expect(page).to have_content 'Awesome release notes' expect(page).to have_content 'Awesome release notes'
expect(page).to have_selector('ul li', count: 2) expect(page).to have_selector('ul li', count: 2)
end
end
scenario 'opens dropdown for ref', js: true do
click_link 'New tag'
ref_row = find('.form-group:nth-of-type(2) .col-sm-10')
page.within ref_row do
ref_input = find('[name="ref"]', visible: false)
expect(ref_input.value).to eq 'master'
expect(find('.dropdown-toggle-text')).to have_content 'master'
find('.js-branch-select').trigger('click')
expect(find('.dropdown-menu')).to have_content 'empty-branch'
end
end end
end end
scenario 'opens dropdown for ref', js: true do context 'from new tag page' do
click_link 'New tag' before do
ref_row = find('.form-group:nth-of-type(2) .col-sm-10') visit new_namespace_project_tag_path(project.namespace, project)
page.within ref_row do end
ref_input = find('[name="ref"]', visible: false)
expect(ref_input.value).to eq 'master'
expect(find('.dropdown-toggle-text')).to have_content 'master'
find('.js-branch-select').trigger('click') it 'description has autocomplete', :js do
find('#release_description').native.send_keys('')
fill_in 'release_description', with: '@'
expect(find('.dropdown-menu')).to have_content 'empty-branch' expect(page).to have_selector('.atwho-view')
end end
end end
......
...@@ -24,6 +24,17 @@ feature 'Master updates tag', feature: true do ...@@ -24,6 +24,17 @@ feature 'Master updates tag', feature: true do
expect(page).to have_content 'v1.1.0' expect(page).to have_content 'v1.1.0'
expect(page).to have_content 'Awesome release notes' expect(page).to have_content 'Awesome release notes'
end end
scenario 'description has autocomplete', :js do
page.within(first('.content-list .controls')) do
click_link 'Edit release notes'
end
find('#release_description').native.send_keys('')
fill_in 'release_description', with: '@'
expect(page).to have_selector('.atwho-view')
end
end end
context 'from a specific tag page' do context 'from a specific tag page' do
......
...@@ -148,12 +148,21 @@ describe DiffHelper do ...@@ -148,12 +148,21 @@ describe DiffHelper do
it 'puts comments on added lines' do it 'puts comments on added lines' do
left = Gitlab::Diff::Line.new('\\nonewline', 'old-nonewline', 3, 3, 3) left = Gitlab::Diff::Line.new('\\nonewline', 'old-nonewline', 3, 3, 3)
right = Gitlab::Diff::Line.new('new line', 'add', 3, 3, 3) right = Gitlab::Diff::Line.new('new line', 'new', 3, 3, 3)
result = helper.parallel_diff_discussions(left, right, diff_file) result = helper.parallel_diff_discussions(left, right, diff_file)
expect(result).to eq([nil, 'comment']) expect(result).to eq([nil, 'comment'])
end end
it 'puts comments on unchanged lines' do
left = Gitlab::Diff::Line.new('unchanged line', nil, 3, 3, 3)
right = Gitlab::Diff::Line.new('unchanged line', nil, 3, 3, 3)
result = helper.parallel_diff_discussions(left, right, diff_file)
expect(result).to eq(['comment', nil])
end
end end
describe "#diff_match_line" do describe "#diff_match_line" do
......
...@@ -95,5 +95,33 @@ describe('Description component', () => { ...@@ -95,5 +95,33 @@ describe('Description component', () => {
done(); done();
}); });
}); });
it('clears task status text when no tasks are present', (done) => {
vm.taskStatus = '0 of 0';
setTimeout(() => {
expect(
document.querySelector('.issuable-meta #task_status').textContent.trim(),
).toBe('');
done();
});
});
});
it('applies syntax highlighting and math when description changed', (done) => {
spyOn(vm, 'renderGFM').and.callThrough();
spyOn($.prototype, 'renderGFM').and.callThrough();
vm.descriptionHtml = 'changed';
Vue.nextTick(() => {
setTimeout(() => {
expect(vm.$refs['gfm-content']).toBeDefined();
expect(vm.renderGFM).toHaveBeenCalled();
expect($.prototype.renderGFM).toHaveBeenCalled();
done();
});
});
}); });
}); });
...@@ -176,7 +176,7 @@ import '~/notes'; ...@@ -176,7 +176,7 @@ import '~/notes';
Notes.updateNoteTargetSelector($note); Notes.updateNoteTargetSelector($note);
expect($note.toggleClass).toHaveBeenCalledWith('target', null); expect($note.toggleClass).toHaveBeenCalledWith('target', false);
}); });
}); });
......
import Vue from 'vue'; import Vue from 'vue';
import relatedLinksComponent from '~/vue_merge_request_widget/components/mr_widget_related_links'; import MRWidgetRelatedLinks from '~/vue_merge_request_widget/components/mr_widget_related_links';
const createComponent = (data) => { describe('MRWidgetRelatedLinks', () => {
const Component = Vue.extend(relatedLinksComponent); let vm;
beforeEach(() => {
const Component = Vue.extend(MRWidgetRelatedLinks);
vm = new Component({
el: document.createElement('div'),
propsData: {
isMerged: false,
relatedLinks: {},
},
});
});
return new Component({ afterEach(() => {
el: document.createElement('div'), vm.$destroy();
propsData: data,
}); });
};
describe('MRWidgetRelatedLinks', () => {
describe('props', () => { describe('props', () => {
it('should have props', () => { it('should have props', () => {
const { relatedLinks } = relatedLinksComponent.props; const { isMerged, relatedLinks } = MRWidgetRelatedLinks.props;
expect(isMerged).toBeDefined();
expect(isMerged.type).toBe(Boolean);
expect(isMerged.required).toBeTruthy();
expect(relatedLinks).toBeDefined(); expect(relatedLinks).toBeDefined();
expect(relatedLinks.type instanceof Object).toBeTruthy(); expect(relatedLinks.type instanceof Object).toBeTruthy();
expect(relatedLinks.required).toBeTruthy(); expect(relatedLinks.required).toBeTruthy();
...@@ -22,16 +33,38 @@ describe('MRWidgetRelatedLinks', () => { ...@@ -22,16 +33,38 @@ describe('MRWidgetRelatedLinks', () => {
}); });
describe('computed', () => { describe('computed', () => {
describe('closingText', () => {
const dummyIssueLabel = 'dummy label';
beforeEach(() => {
spyOn(vm, 'issueLabel').and.returnValue(dummyIssueLabel);
});
it('outputs text for closing issues', () => {
vm.isMerged = false;
const text = vm.closingText;
expect(text).toBe(`Closes ${dummyIssueLabel}`);
});
it('outputs text for closed issues', () => {
vm.isMerged = true;
const text = vm.closingText;
expect(text).toBe(`Closed ${dummyIssueLabel}`);
});
});
describe('hasLinks', () => { describe('hasLinks', () => {
it('should return correct value when we have links reference', () => { it('should return correct value when we have links reference', () => {
const data = { vm.relatedLinks = {
relatedLinks: { closing: '/foo',
closing: '/foo', mentioned: '/foo',
mentioned: '/foo', assignToMe: '/foo',
assignToMe: '/foo',
},
}; };
const vm = createComponent(data);
expect(vm.hasLinks).toBeTruthy(); expect(vm.hasLinks).toBeTruthy();
vm.relatedLinks.closing = null; vm.relatedLinks.closing = null;
...@@ -44,95 +77,160 @@ describe('MRWidgetRelatedLinks', () => { ...@@ -44,95 +77,160 @@ describe('MRWidgetRelatedLinks', () => {
expect(vm.hasLinks).toBeFalsy(); expect(vm.hasLinks).toBeFalsy();
}); });
}); });
});
describe('methods', () => { describe('mentionedText', () => {
const data = { it('outputs text for one mentioned issue before merging', () => {
relatedLinks: { vm.isMerged = false;
closing: '<a href="#">#23</a> and <a>#42</a>', spyOn(vm, 'hasMultipleIssues').and.returnValue(false);
mentioned: '<a href="#">#7</a>',
},
};
const vm = createComponent(data);
describe('hasMultipleIssues', () => { const text = vm.mentionedText;
it('should return true if the given text has multiple issues', () => {
expect(vm.hasMultipleIssues(data.relatedLinks.closing)).toBeTruthy(); expect(text).toBe('is mentioned but will not be closed');
}); });
it('should return false if the given text has one issue', () => { it('outputs text for one mentioned issue after merging', () => {
expect(vm.hasMultipleIssues(data.relatedLinks.mentioned)).toBeFalsy(); vm.isMerged = true;
spyOn(vm, 'hasMultipleIssues').and.returnValue(false);
const text = vm.mentionedText;
expect(text).toBe('is mentioned but was not closed');
});
it('outputs text for multiple mentioned issue before merging', () => {
vm.isMerged = false;
spyOn(vm, 'hasMultipleIssues').and.returnValue(true);
const text = vm.mentionedText;
expect(text).toBe('are mentioned but will not be closed');
});
it('outputs text for multiple mentioned issue after merging', () => {
vm.isMerged = true;
spyOn(vm, 'hasMultipleIssues').and.returnValue(true);
const text = vm.mentionedText;
expect(text).toBe('are mentioned but were not closed');
}); });
}); });
});
describe('issueLabel', () => { describe('methods', () => {
const relatedLinks = {
oneIssue: '<a href="#">#7</a>',
twoIssues: '<a href="#">#23</a> and <a>#42</a>',
threeIssues: '<a href="#">#1</a>, <a>#2</a>, and <a>#3</a>',
};
beforeEach(() => {
vm.relatedLinks = relatedLinks;
});
describe('hasMultipleIssues', () => {
it('should return true if the given text has multiple issues', () => { it('should return true if the given text has multiple issues', () => {
expect(vm.issueLabel('closing')).toEqual('issues'); expect(vm.hasMultipleIssues(relatedLinks.twoIssues)).toBeTruthy();
expect(vm.hasMultipleIssues(relatedLinks.threeIssues)).toBeTruthy();
}); });
it('should return false if the given text has one issue', () => { it('should return false if the given text has one issue', () => {
expect(vm.issueLabel('mentioned')).toEqual('issue'); expect(vm.hasMultipleIssues(relatedLinks.oneIssue)).toBeFalsy();
}); });
}); });
describe('verbLabel', () => { describe('issueLabel', () => {
it('should return true if the given text has multiple issues', () => { it('should return true if the given text has multiple issues', () => {
expect(vm.verbLabel('closing')).toEqual('are'); expect(vm.issueLabel('twoIssues')).toEqual('issues');
expect(vm.issueLabel('threeIssues')).toEqual('issues');
}); });
it('should return false if the given text has one issue', () => { it('should return false if the given text has one issue', () => {
expect(vm.verbLabel('mentioned')).toEqual('is'); expect(vm.issueLabel('oneIssue')).toEqual('issue');
}); });
}); });
}); });
describe('template', () => { describe('template', () => {
it('should have only have closing issues text', () => { it('should have only have closing issues text', (done) => {
const vm = createComponent({ vm.relatedLinks = {
relatedLinks: { closing: '<a href="#">#23</a> and <a>#42</a>',
closing: '<a href="#">#23</a> and <a>#42</a>', };
},
});
const content = vm.$el.textContent.replace(/\n(\s)+/g, ' ').trim();
expect(content).toContain('Closes issues #23 and #42');
expect(content).not.toContain('mentioned');
});
it('should have only have mentioned issues text', () => { Vue.nextTick()
const vm = createComponent({ .then(() => {
relatedLinks: { const content = vm.$el.textContent.replace(/\n(\s)+/g, ' ').trim();
mentioned: '<a href="#">#7</a>',
},
});
expect(vm.$el.innerText).toContain('issue #7'); expect(content).toContain('Closes issues #23 and #42');
expect(vm.$el.innerText).toContain('is mentioned but will not be closed.'); expect(content).not.toContain('mentioned');
expect(vm.$el.innerText).not.toContain('Closes'); })
.then(done)
.catch(done.fail);
}); });
it('should have closing and mentioned issues at the same time', () => { it('should have only have mentioned issues text', (done) => {
const vm = createComponent({ vm.relatedLinks = {
relatedLinks: { mentioned: '<a href="#">#7</a>',
closing: '<a href="#">#7</a>', };
mentioned: '<a href="#">#23</a> and <a>#42</a>',
}, Vue.nextTick()
}); .then(() => {
const content = vm.$el.textContent.replace(/\n(\s)+/g, ' ').trim(); expect(vm.$el.innerText).toContain('issue #7');
expect(vm.$el.innerText).toContain('is mentioned but will not be closed');
expect(vm.$el.innerText).not.toContain('Closes');
})
.then(done)
.catch(done.fail);
});
expect(content).toContain('Closes issue #7.'); it('should have closing and mentioned issues at the same time', (done) => {
expect(content).toContain('issues #23 and #42'); vm.relatedLinks = {
expect(content).toContain('are mentioned but will not be closed.'); closing: '<a href="#">#7</a>',
mentioned: '<a href="#">#23</a> and <a>#42</a>',
};
Vue.nextTick()
.then(() => {
const content = vm.$el.textContent.replace(/\n(\s)+/g, ' ').trim();
expect(content).toContain('Closes issue #7.');
expect(content).toContain('issues #23 and #42');
expect(content).toContain('are mentioned but will not be closed');
})
.then(done)
.catch(done.fail);
}); });
it('should have assing issues link', () => { it('should have assing issues link', (done) => {
const vm = createComponent({ vm.relatedLinks = {
relatedLinks: { assignToMe: '<a href="#">Assign yourself to these issues</a>',
assignToMe: '<a href="#">Assign yourself to these issues</a>', };
},
}); Vue.nextTick()
.then(() => {
expect(vm.$el.innerText).toContain('Assign yourself to these issues');
})
.then(done)
.catch(done.fail);
});
expect(vm.$el.innerText).toContain('Assign yourself to these issues'); it('should use different wording after merging', (done) => {
vm.isMerged = true;
vm.relatedLinks = {
closing: '<a href="#">#7</a>',
mentioned: '<a href="#">#23</a> and <a>#42</a>',
};
Vue.nextTick()
.then(() => {
const content = vm.$el.textContent.replace(/\n(\s)+/g, ' ').trim();
expect(content).toContain('Closed issue #7.');
expect(content).toContain('issues #23 and #42');
expect(content).toContain('are mentioned but were not closed');
})
.then(done)
.catch(done.fail);
}); });
}); });
}); });
...@@ -48,12 +48,13 @@ describe('mrWidgetOptions', () => { ...@@ -48,12 +48,13 @@ describe('mrWidgetOptions', () => {
}); });
describe('shouldRenderMergeHelp', () => { describe('shouldRenderMergeHelp', () => {
it('should return false for the initial merged state', () => { it('should return false after merging', () => {
vm.mr.isMerged = true;
expect(vm.shouldRenderMergeHelp).toBeFalsy(); expect(vm.shouldRenderMergeHelp).toBeFalsy();
}); });
it('should return true for a state which requires help widget', () => { it('should return true before merging', () => {
vm.mr.state = 'conflicts'; vm.mr.isMerged = false;
expect(vm.shouldRenderMergeHelp).toBeTruthy(); expect(vm.shouldRenderMergeHelp).toBeTruthy();
}); });
}); });
......
...@@ -18,5 +18,17 @@ describe('MergeRequestStore', () => { ...@@ -18,5 +18,17 @@ describe('MergeRequestStore', () => {
store.setData({ ...mockData, work_in_progress: !mockData.work_in_progress }); store.setData({ ...mockData, work_in_progress: !mockData.work_in_progress });
expect(store.hasSHAChanged).toBe(false); expect(store.hasSHAChanged).toBe(false);
}); });
it('sets isMerged to true for merged state', () => {
store.setData({ ...mockData, state: 'merged' });
expect(store.isMerged).toBe(true);
});
it('sets isMerged to false for readyToMerge state', () => {
store.setData({ ...mockData, state: 'readyToMerge' });
expect(store.isMerged).toBe(false);
});
}); });
}); });
...@@ -32,6 +32,37 @@ describe Gitlab::CurrentSettings do ...@@ -32,6 +32,37 @@ describe Gitlab::CurrentSettings do
expect(current_application_settings).to be_a(ApplicationSetting) expect(current_application_settings).to be_a(ApplicationSetting)
end end
context 'with migrations pending' do
before do
expect(ActiveRecord::Migrator).to receive(:needs_migration?).and_return(true)
end
it 'returns an in-memory ApplicationSetting object' do
settings = current_application_settings
expect(settings).to be_a(OpenStruct)
expect(settings.sign_in_enabled?).to eq(settings.sign_in_enabled)
expect(settings.sign_up_enabled?).to eq(settings.sign_up_enabled)
end
it 'uses the existing database settings and falls back to defaults' do
db_settings = create(:application_setting,
home_page_url: 'http://mydomain.com',
signup_enabled: false)
settings = current_application_settings
app_defaults = ApplicationSetting.last
expect(settings).to be_a(OpenStruct)
expect(settings.home_page_url).to eq(db_settings.home_page_url)
expect(settings.signup_enabled?).to be_falsey
expect(settings.signup_enabled).to be_falsey
# Check that unspecified values use the defaults
settings.reject! { |key, _| [:home_page_url, :signup_enabled].include? key }
settings.each { |key, _| expect(settings[key]).to eq(app_defaults[key]) }
end
end
end end
context 'with DB unavailable' do context 'with DB unavailable' do
......
require 'spec_helper'
describe Gitlab::FakeApplicationSettings do
let(:defaults) { { signin_enabled: false, foobar: 'asdf', signup_enabled: true, 'test?' => 123 } }
subject { described_class.new(defaults) }
it 'wraps OpenStruct variables properly' do
expect(subject.signin_enabled).to be_falsey
expect(subject.signup_enabled).to be_truthy
expect(subject.foobar).to eq('asdf')
end
it 'defines predicate methods' do
expect(subject.signin_enabled?).to be_falsey
expect(subject.signup_enabled?).to be_truthy
end
it 'predicate method changes when value is updated' do
subject.signin_enabled = true
expect(subject.signin_enabled?).to be_truthy
end
it 'does not define a predicate method' do
expect(subject.foobar?).to be_nil
end
it 'does not override an existing predicate method' do
expect(subject.test?).to eq(123)
end
end
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment