summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--app/assets/javascripts/dropzone_input.js252
-rw-r--r--app/assets/javascripts/pipeline_schedules/components/interval_pattern_input.js118
-rw-r--r--app/assets/javascripts/pipeline_schedules/components/target_branch_dropdown.js16
-rw-r--r--app/assets/javascripts/pipeline_schedules/components/timezone_dropdown.js18
-rw-r--r--app/assets/stylesheets/pages/note_form.scss43
-rw-r--r--app/assets/stylesheets/pages/pipeline_schedules.scss21
-rw-r--r--app/helpers/icons_helper.rb5
-rw-r--r--app/services/members/authorized_destroy_service.rb15
-rw-r--r--app/views/admin/health_check/show.html.haml19
-rw-r--r--app/views/projects/pipeline_schedules/_form.html.haml14
-rw-r--r--app/views/shared/notes/_hints.html.haml27
-rw-r--r--changelogs/unreleased/32395-duplicate-string-in-https-docs-gitlab-com-ce-administration-environment_variables-html.yml4
-rw-r--r--changelogs/unreleased/add_ability_to_cancel_attaching_file_and_redesign_attaching_files_ui.yml4
-rw-r--r--changelogs/unreleased/update-admin-health-page.yml5
-rw-r--r--db/migrate/20170320171632_create_issue_assignees_table.rb40
-rw-r--r--db/migrate/20170320173259_migrate_assignees.rb8
-rw-r--r--db/migrate/20170516153305_migrate_assignee_to_separate_table.rb83
-rw-r--r--db/migrate/20170516183131_add_indices_to_issue_assignees.rb41
-rw-r--r--db/post_migrate/20170516165238_cleanup_trigger_for_issues.rb39
-rw-r--r--db/post_migrate/20170516181025_add_constraints_to_issue_assignees_table.rb37
-rw-r--r--db/schema.rb8
-rw-r--r--doc/administration/environment_variables.md1
-rw-r--r--doc/development/fe_guide/index.md14
-rw-r--r--doc/user/group/subgroups/index.md8
-rw-r--r--spec/features/merge_requests/merge_immediately_with_pipeline_spec.rb8
-rw-r--r--spec/features/projects/pipeline_schedules_spec.rb9
-rw-r--r--spec/features/uploads/user_uploads_file_to_note_spec.rb76
-rw-r--r--spec/javascripts/pipeline_schedules/interval_pattern_input_spec.js42
-rw-r--r--spec/support/dropzone_helper.rb40
29 files changed, 687 insertions, 328 deletions
diff --git a/app/assets/javascripts/dropzone_input.js b/app/assets/javascripts/dropzone_input.js
index b3a76fbb43e..988c484ed85 100644
--- a/app/assets/javascripts/dropzone_input.js
+++ b/app/assets/javascripts/dropzone_input.js
@@ -5,104 +5,154 @@ require('./preview_markdown');
window.DropzoneInput = (function() {
function DropzoneInput(form) {
- var $mdArea, alertAttr, alertClass, appendToTextArea, btnAlert, child, closeAlertMessage, closeSpinner, divAlert, divHover, divSpinner, dropzone, form_dropzone, form_textarea, getFilename, handlePaste, iconPaperclip, iconSpinner, insertToTextArea, isImage, max_file_size, pasteText, uploads_path, showError, showSpinner, uploadFile, uploadProgress;
+ var updateAttachingMessage, $attachingFileMessage, $mdArea, $attachButton, $cancelButton, $retryLink, $uploadingErrorContainer, $uploadingErrorMessage, $uploadProgress, $uploadingProgressContainer, appendToTextArea, btnAlert, child, closeAlertMessage, closeSpinner, divHover, divSpinner, dropzone, $formDropzone, formTextarea, getFilename, handlePaste, iconPaperclip, iconSpinner, insertToTextArea, isImage, maxFileSize, pasteText, uploadsPath, showError, showSpinner, uploadFile;
Dropzone.autoDiscover = false;
- alertClass = "alert alert-danger alert-dismissable div-dropzone-alert";
- alertAttr = "class=\"close\" data-dismiss=\"alert\"" + "aria-hidden=\"true\"";
- divHover = "<div class=\"div-dropzone-hover\"></div>";
- divSpinner = "<div class=\"div-dropzone-spinner\"></div>";
- divAlert = "<div class=\"" + alertClass + "\"></div>";
- iconPaperclip = "<i class=\"fa fa-paperclip div-dropzone-icon\"></i>";
- iconSpinner = "<i class=\"fa fa-spinner fa-spin div-dropzone-icon\"></i>";
- uploadProgress = $("<div class=\"div-dropzone-progress\"></div>");
- btnAlert = "<button type=\"button\"" + alertAttr + ">&times;</button>";
- uploads_path = window.uploads_path || null;
- max_file_size = gon.max_file_size || 10;
- form_textarea = $(form).find(".js-gfm-input");
- form_textarea.wrap("<div class=\"div-dropzone\"></div>");
- form_textarea.on('paste', (function(_this) {
+ divHover = '<div class="div-dropzone-hover"></div>';
+ iconPaperclip = '<i class="fa fa-paperclip div-dropzone-icon"></i>';
+ $attachButton = form.find('.button-attach-file');
+ $attachingFileMessage = form.find('.attaching-file-message');
+ $cancelButton = form.find('.button-cancel-uploading-files');
+ $retryLink = form.find('.retry-uploading-link');
+ $uploadProgress = form.find('.uploading-progress');
+ $uploadingErrorContainer = form.find('.uploading-error-container');
+ $uploadingErrorMessage = form.find('.uploading-error-message');
+ $uploadingProgressContainer = form.find('.uploading-progress-container');
+ uploadsPath = window.uploads_path || null;
+ maxFileSize = gon.max_file_size || 10;
+ formTextarea = form.find('.js-gfm-input');
+ formTextarea.wrap('<div class="div-dropzone"></div>');
+ formTextarea.on('paste', (function(_this) {
return function(event) {
return handlePaste(event);
};
})(this));
- $mdArea = $(form_textarea).closest('.md-area');
- $(form).setupMarkdownPreview();
- form_dropzone = $(form).find('.div-dropzone');
- form_dropzone.parent().addClass("div-dropzone-wrapper");
- form_dropzone.append(divHover);
- form_dropzone.find(".div-dropzone-hover").append(iconPaperclip);
- form_dropzone.append(divSpinner);
- form_dropzone.find(".div-dropzone-spinner").append(iconSpinner);
- form_dropzone.find(".div-dropzone-spinner").append(uploadProgress);
- form_dropzone.find(".div-dropzone-spinner").css({
- "opacity": 0,
- "display": "none"
- });
- if (!uploads_path) return;
+ // Add dropzone area to the form.
+ $mdArea = formTextarea.closest('.md-area');
+ form.setupMarkdownPreview();
+ $formDropzone = form.find('.div-dropzone');
+ $formDropzone.parent().addClass('div-dropzone-wrapper');
+ $formDropzone.append(divHover);
+ $formDropzone.find('.div-dropzone-hover').append(iconPaperclip);
+
+ if (!uploadsPath) return;
- dropzone = form_dropzone.dropzone({
- url: uploads_path,
- dictDefaultMessage: "",
+ dropzone = $formDropzone.dropzone({
+ url: uploadsPath,
+ dictDefaultMessage: '',
clickable: true,
- paramName: "file",
- maxFilesize: max_file_size,
+ paramName: 'file',
+ maxFilesize: maxFileSize,
uploadMultiple: false,
headers: {
- "X-CSRF-Token": $("meta[name=\"csrf-token\"]").attr("content")
+ 'X-CSRF-Token': $('meta[name="csrf-token"]').attr('content')
},
previewContainer: false,
processing: function() {
- return $(".div-dropzone-alert").alert("close");
+ return $('.div-dropzone-alert').alert('close');
},
dragover: function() {
$mdArea.addClass('is-dropzone-hover');
- form.find(".div-dropzone-hover").css("opacity", 0.7);
+ form.find('.div-dropzone-hover').css('opacity', 0.7);
},
dragleave: function() {
$mdArea.removeClass('is-dropzone-hover');
- form.find(".div-dropzone-hover").css("opacity", 0);
+ form.find('.div-dropzone-hover').css('opacity', 0);
},
drop: function() {
$mdArea.removeClass('is-dropzone-hover');
- form.find(".div-dropzone-hover").css("opacity", 0);
- form_textarea.focus();
+ form.find('.div-dropzone-hover').css('opacity', 0);
+ formTextarea.focus();
},
success: function(header, response) {
const processingFileCount = this.getQueuedFiles().length + this.getUploadingFiles().length;
const shouldPad = processingFileCount >= 1;
pasteText(response.link.markdown, shouldPad);
+ // Show 'Attach a file' link only when all files have been uploaded.
+ if (!processingFileCount) $attachButton.removeClass('hide');
},
- error: function(temp) {
- var checkIfMsgExists, errorAlert;
- errorAlert = $(form).find('.error-alert');
- checkIfMsgExists = errorAlert.children().length;
- if (checkIfMsgExists === 0) {
- errorAlert.append(divAlert);
- $(".div-dropzone-alert").append(btnAlert + "Attaching the file failed.");
- }
+ error: function(file, errorMessage = 'Attaching the file failed.', xhr) {
+ // If 'error' event is fired by dropzone, the second parameter is error message.
+ // If the 'errorMessage' parameter is empty, the default error message is set.
+ // If the 'error' event is fired by backend (xhr) error response, the third parameter is
+ // xhr object (xhr.responseText is error message).
+ // On error we hide the 'Attach' and 'Cancel' buttons
+ // and show an error.
+
+ // If there's xhr error message, let's show it instead of dropzone's one.
+ const message = xhr ? xhr.responseText : errorMessage;
+
+ $uploadingErrorContainer.removeClass('hide');
+ $uploadingErrorMessage.html(message);
+ $attachButton.addClass('hide');
+ $cancelButton.addClass('hide');
},
totaluploadprogress: function(totalUploadProgress) {
- uploadProgress.text(Math.round(totalUploadProgress) + "%");
+ updateAttachingMessage(this.files, $attachingFileMessage);
+ $uploadProgress.text(Math.round(totalUploadProgress) + '%');
+ },
+ sending: function(file) {
+ // DOM elements already exist.
+ // Instead of dynamically generating them,
+ // we just either hide or show them.
+ $attachButton.addClass('hide');
+ $uploadingErrorContainer.addClass('hide');
+ $uploadingProgressContainer.removeClass('hide');
+ $cancelButton.removeClass('hide');
},
- sending: function() {
- form_dropzone.find(".div-dropzone-spinner").css({
- "opacity": 0.7,
- "display": "inherit"
- });
+ removedfile: function() {
+ $attachButton.removeClass('hide');
+ $cancelButton.addClass('hide');
+ $uploadingProgressContainer.addClass('hide');
+ $uploadingErrorContainer.addClass('hide');
},
queuecomplete: function() {
- uploadProgress.text("");
- $(".dz-preview").remove();
- $(".markdown-area").trigger("input");
- $(".div-dropzone-spinner").css({
- "opacity": 0,
- "display": "none"
- });
+ $('.dz-preview').remove();
+ $('.markdown-area').trigger('input');
+
+ $uploadingProgressContainer.addClass('hide');
+ $cancelButton.addClass('hide');
}
});
- child = $(dropzone[0]).children("textarea");
+
+ child = $(dropzone[0]).children('textarea');
+
+ // removeAllFiles(true) stops uploading files (if any)
+ // and remove them from dropzone files queue.
+ $cancelButton.on('click', (e) => {
+ const target = e.target.closest('form').querySelector('.div-dropzone');
+
+ e.preventDefault();
+ e.stopPropagation();
+ Dropzone.forElement(target).removeAllFiles(true);
+ });
+
+ // If 'error' event is fired, we store a failed files,
+ // clear dropzone files queue, change status of failed files to undefined,
+ // and add that files to the dropzone files queue again.
+ // addFile() adds file to dropzone files queue and upload it.
+ $retryLink.on('click', (e) => {
+ const dropzoneInstance = Dropzone.forElement(e.target.closest('form').querySelector('.div-dropzone'));
+ const failedFiles = dropzoneInstance.files;
+
+ e.preventDefault();
+
+ // 'true' parameter of removeAllFiles() cancels uploading of files that are being uploaded at the moment.
+ dropzoneInstance.removeAllFiles(true);
+
+ failedFiles.map((failedFile, i) => {
+ const file = failedFile;
+
+ if (file.status === Dropzone.ERROR) {
+ file.status = undefined;
+ file.accepted = undefined;
+ }
+
+ return dropzoneInstance.addFile(file);
+ });
+ });
+
handlePaste = function(event) {
var filename, image, pasteEvent, text;
pasteEvent = event.originalEvent;
@@ -110,25 +160,27 @@ window.DropzoneInput = (function() {
image = isImage(pasteEvent);
if (image) {
event.preventDefault();
- filename = getFilename(pasteEvent) || "image.png";
- text = "{{" + filename + "}}";
+ filename = getFilename(pasteEvent) || 'image.png';
+ text = `{{${filename}}}`;
pasteText(text);
return uploadFile(image.getAsFile(), filename);
}
}
};
+
isImage = function(data) {
var i, item;
i = 0;
while (i < data.clipboardData.items.length) {
item = data.clipboardData.items[i];
- if (item.type.indexOf("image") !== -1) {
+ if (item.type.indexOf('image') !== -1) {
return item;
}
i += 1;
}
return false;
};
+
pasteText = function(text, shouldPad) {
var afterSelection, beforeSelection, caretEnd, caretStart, textEnd;
var formattedText = text;
@@ -142,31 +194,33 @@ window.DropzoneInput = (function() {
$(child).val(beforeSelection + formattedText + afterSelection);
textarea.setSelectionRange(caretStart + formattedText.length, caretEnd + formattedText.length);
textarea.style.height = `${textarea.scrollHeight}px`;
- return form_textarea.trigger("input");
+ return formTextarea.trigger('input');
};
+
getFilename = function(e) {
var value;
if (window.clipboardData && window.clipboardData.getData) {
- value = window.clipboardData.getData("Text");
+ value = window.clipboardData.getData('Text');
} else if (e.clipboardData && e.clipboardData.getData) {
- value = e.clipboardData.getData("text/plain");
+ value = e.clipboardData.getData('text/plain');
}
value = value.split("\r");
return value.first();
};
+
uploadFile = function(item, filename) {
var formData;
formData = new FormData();
- formData.append("file", item, filename);
+ formData.append('file', item, filename);
return $.ajax({
- url: uploads_path,
- type: "POST",
+ url: uploadsPath,
+ type: 'POST',
data: formData,
- dataType: "json",
+ dataType: 'json',
processData: false,
contentType: false,
headers: {
- "X-CSRF-Token": $("meta[name=\"csrf-token\"]").attr("content")
+ 'X-CSRF-Token': $('meta[name="csrf-token"]').attr('content')
},
beforeSend: function() {
showSpinner();
@@ -183,44 +237,54 @@ window.DropzoneInput = (function() {
}
});
};
+
+ updateAttachingMessage = (files, messageContainer) => {
+ let attachingMessage;
+ const filesCount = files.filter(function(file) {
+ return file.status === 'uploading' ||
+ file.status === 'queued';
+ }).length;
+
+ // Dinamycally change uploading files text depending on files number in
+ // dropzone files queue.
+ if (filesCount > 1) {
+ attachingMessage = 'Attaching ' + filesCount + ' files -';
+ } else {
+ attachingMessage = 'Attaching a file -';
+ }
+
+ messageContainer.text(attachingMessage);
+ };
+
insertToTextArea = function(filename, url) {
return $(child).val(function(index, val) {
- return val.replace("{{" + filename + "}}", url);
+ return val.replace(`{{${filename}}}`, url);
});
};
+
appendToTextArea = function(url) {
return $(child).val(function(index, val) {
return val + url + "\n";
});
};
+
showSpinner = function(e) {
- return form.find(".div-dropzone-spinner").css({
- "opacity": 0.7,
- "display": "inherit"
- });
+ return $uploadingProgressContainer.removeClass('hide');
};
+
closeSpinner = function() {
- return form.find(".div-dropzone-spinner").css({
- "opacity": 0,
- "display": "none"
- });
+ return $uploadingProgressContainer.addClass('hide');
};
+
showError = function(message) {
- var checkIfMsgExists, errorAlert;
- errorAlert = $(form).find('.error-alert');
- checkIfMsgExists = errorAlert.children().length;
- if (checkIfMsgExists === 0) {
- errorAlert.append(divAlert);
- return $(".div-dropzone-alert").append(btnAlert + message);
- }
+ $uploadingErrorContainer.removeClass('hide');
+ $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();
$(this).closest('.gfm-form').find('.div-dropzone').click();
- form_textarea.focus();
+ formTextarea.focus();
});
}
diff --git a/app/assets/javascripts/pipeline_schedules/components/interval_pattern_input.js b/app/assets/javascripts/pipeline_schedules/components/interval_pattern_input.js
index 152e75b747e..4d623763ca7 100644
--- a/app/assets/javascripts/pipeline_schedules/components/interval_pattern_input.js
+++ b/app/assets/javascripts/pipeline_schedules/components/interval_pattern_input.js
@@ -24,9 +24,6 @@ export default {
};
},
computed: {
- showUnsetWarning() {
- return this.cronInterval === '';
- },
intervalIsPreset() {
return _.contains(this.cronIntervalPresets, this.cronInterval);
},
@@ -63,67 +60,75 @@ export default {
},
template: `
<div class="interval-pattern-form-group">
- <input
- id="custom"
- class="label-light"
- type="radio"
- :name="inputNameAttribute"
- :value="cronInterval"
- :checked="isEditable"
- @click="toggleCustomInput(true)"
- />
+ <div class="cron-preset-radio-input">
+ <input
+ id="custom"
+ class="label-light"
+ type="radio"
+ :name="inputNameAttribute"
+ :value="cronInterval"
+ :checked="isEditable"
+ @click="toggleCustomInput(true)"
+ />
- <label for="custom">
- Custom
- </label>
+ <label for="custom">
+ Custom
+ </label>
- <span class="cron-syntax-link-wrap">
- (<a :href="cronSyntaxUrl" target="_blank">Cron syntax</a>)
- </span>
+ <span class="cron-syntax-link-wrap">
+ (<a :href="cronSyntaxUrl" target="_blank">Cron syntax</a>)
+ </span>
+ </div>
- <input
- id="every-day"
- class="label-light"
- type="radio"
- v-model="cronInterval"
- :name="inputNameAttribute"
- :value="cronIntervalPresets.everyDay"
- @click="toggleCustomInput(false)"
- />
+ <div class="cron-preset-radio-input">
+ <input
+ id="every-day"
+ class="label-light"
+ type="radio"
+ v-model="cronInterval"
+ :name="inputNameAttribute"
+ :value="cronIntervalPresets.everyDay"
+ @click="toggleCustomInput(false)"
+ />
- <label class="label-light" for="every-day">
- Every day (at 4:00am)
- </label>
+ <label class="label-light" for="every-day">
+ Every day (at 4:00am)
+ </label>
+ </div>
- <input
- id="every-week"
- class="label-light"
- type="radio"
- v-model="cronInterval"
- :name="inputNameAttribute"
- :value="cronIntervalPresets.everyWeek"
- @click="toggleCustomInput(false)"
- />
+ <div class="cron-preset-radio-input">
+ <input
+ id="every-week"
+ class="label-light"
+ type="radio"
+ v-model="cronInterval"
+ :name="inputNameAttribute"
+ :value="cronIntervalPresets.everyWeek"
+ @click="toggleCustomInput(false)"
+ />
- <label class="label-light" for="every-week">
- Every week (Sundays at 4:00am)
- </label>
+ <label class="label-light" for="every-week">
+ Every week (Sundays at 4:00am)
+ </label>
+ </div>
- <input
- id="every-month"
- class="label-light"
- type="radio"
- v-model="cronInterval"
- :name="inputNameAttribute"
- :value="cronIntervalPresets.everyMonth"
- @click="toggleCustomInput(false)"
- />
+ <div class="cron-preset-radio-input">
+ <input
+ id="every-month"
+ class="label-light"
+ type="radio"
+ v-model="cronInterval"
+ :name="inputNameAttribute"
+ :value="cronIntervalPresets.everyMonth"
+ @click="toggleCustomInput(false)"
+ />
- <label class="label-light" for="every-month">
- Every month (on the 1st at 4:00am)
- </label>
+ <label class="label-light" for="every-month">
+ Every month (on the 1st at 4:00am)
+ </label>
+ </div>
- <div class="cron-interval-input-wrapper col-md-6">
+ <div class="cron-interval-input-wrapper">
<input
id="schedule_cron"
class="form-control inline cron-interval-input"
@@ -135,9 +140,6 @@ export default {
:disabled="!isEditable"
/>
</div>
- <span class="cron-unset-status col-md-3" v-if="showUnsetWarning">
- Schedule not yet set
- </span>
</div>
`,
};
diff --git a/app/assets/javascripts/pipeline_schedules/components/target_branch_dropdown.js b/app/assets/javascripts/pipeline_schedules/components/target_branch_dropdown.js
index 22e746ad2c3..0c3926d76b5 100644
--- a/app/assets/javascripts/pipeline_schedules/components/target_branch_dropdown.js
+++ b/app/assets/javascripts/pipeline_schedules/components/target_branch_dropdown.js
@@ -3,7 +3,7 @@ export default class TargetBranchDropdown {
this.$dropdown = $('.js-target-branch-dropdown');
this.$dropdownToggle = this.$dropdown.find('.dropdown-toggle-text');
this.$input = $('#schedule_ref');
- this.initialValue = this.$input.val();
+ this.initDefaultBranch();
this.initDropdown();
}
@@ -29,13 +29,23 @@ export default class TargetBranchDropdown {
}
setDropdownToggle() {
- if (this.initialValue) {
- this.$dropdownToggle.text(this.initialValue);
+ const initialValue = this.$input.val();
+
+ this.$dropdownToggle.text(initialValue);
+ }
+
+ initDefaultBranch() {
+ const initialValue = this.$input.val();
+ const defaultBranch = this.$dropdown.data('defaultBranch');
+
+ if (!initialValue) {
+ this.$input.val(defaultBranch);
}
}
updateInputValue({ selectedObj, e }) {
e.preventDefault();
+
this.$input.val(selectedObj.name);
gl.pipelineScheduleFieldErrors.updateFormValidityState();
}
diff --git a/app/assets/javascripts/pipeline_schedules/components/timezone_dropdown.js b/app/assets/javascripts/pipeline_schedules/components/timezone_dropdown.js
index c70e0502cf8..95ed9c7dc21 100644
--- a/app/assets/javascripts/pipeline_schedules/components/timezone_dropdown.js
+++ b/app/assets/javascripts/pipeline_schedules/components/timezone_dropdown.js
@@ -1,12 +1,14 @@
/* eslint-disable class-methods-use-this */
+const defaultTimezone = 'UTC';
+
export default class TimezoneDropdown {
constructor() {
this.$dropdown = $('.js-timezone-dropdown');
this.$dropdownToggle = this.$dropdown.find('.dropdown-toggle-text');
this.$input = $('#schedule_cron_timezone');
this.timezoneData = this.$dropdown.data('data');
- this.initialValue = this.$input.val();
+ this.initDefaultTimezone();
this.initDropdown();
}
@@ -42,12 +44,20 @@ export default class TimezoneDropdown {
return `[UTC ${this.formatUtcOffset(item.offset)}] ${item.name}`;
}
- setDropdownToggle() {
- if (this.initialValue) {
- this.$dropdownToggle.text(this.initialValue);
+ initDefaultTimezone() {
+ const initialValue = this.$input.val();
+
+ if (!initialValue) {
+ this.$input.val(defaultTimezone);
}
}
+ setDropdownToggle() {
+ const initialValue = this.$input.val();
+
+ this.$dropdownToggle.text(initialValue);
+ }
+
updateInputValue({ selectedObj, e }) {
e.preventDefault();
this.$input.val(selectedObj.identifier);
diff --git a/app/assets/stylesheets/pages/note_form.scss b/app/assets/stylesheets/pages/note_form.scss
index 62f654ed343..9db26f99a75 100644
--- a/app/assets/stylesheets/pages/note_form.scss
+++ b/app/assets/stylesheets/pages/note_form.scss
@@ -277,6 +277,7 @@
.toolbar-text {
font-size: 14px;
line-height: 16px;
+ margin-top: 2px;
@media (min-width: $screen-md-min) {
float: left;
@@ -402,3 +403,45 @@
}
}
}
+
+.uploading-container {
+ float: right;
+
+ @media (max-width: $screen-xs-max) {
+ float: left;
+ margin-top: 5px;
+ }
+}
+
+.uploading-error-icon,
+.uploading-error-message {
+ color: $gl-text-red;
+}
+
+.uploading-error-message {
+ @media (max-width: $screen-xs-max) {
+ &::after {
+ content: "\a";
+ white-space: pre;
+ }
+ }
+}
+
+.uploading-progress {
+ margin-right: 5px;
+}
+
+.attach-new-file,
+.button-attach-file,
+.retry-uploading-link {
+ color: $gl-link-color;
+ padding: 0;
+ background: none;
+ border: 0;
+ font-size: 14px;
+ line-height: 16px;
+}
+
+.markdown-selector {
+ color: $gl-link-color;
+}
diff --git a/app/assets/stylesheets/pages/pipeline_schedules.scss b/app/assets/stylesheets/pages/pipeline_schedules.scss
index 0fee54a0d19..ab417948931 100644
--- a/app/assets/stylesheets/pages/pipeline_schedules.scss
+++ b/app/assets/stylesheets/pages/pipeline_schedules.scss
@@ -31,14 +31,6 @@
margin-right: 10px;
font-size: 12px;
}
-
- .cron-unset-status {
- padding-top: 16px;
- margin-left: -16px;
- color: $gl-text-color-secondary;
- font-size: 12px;
- font-weight: 600;
- }
}
.pipeline-schedule-table-row {
@@ -69,3 +61,16 @@
color: $gl-text-color;
}
}
+
+.cron-preset-radio-input {
+ display: inline-block;
+
+ @media (max-width: $screen-md-max) {
+ display: block;
+ margin: 0 0 5px 5px;
+ }
+
+ input {
+ margin-right: 3px;
+ }
+}
diff --git a/app/helpers/icons_helper.rb b/app/helpers/icons_helper.rb
index ef96a554b7e..f29faeca22d 100644
--- a/app/helpers/icons_helper.rb
+++ b/app/helpers/icons_helper.rb
@@ -7,9 +7,10 @@ module IconsHelper
# font-awesome-rails gem, but should we ever use a different icon pack in the
# future we won't have to change hundreds of method calls.
def icon(names, options = {})
- if (options.keys & %w[aria-hidden aria-label]).empty?
- # Add `aria-hidden` if there are no aria's set
+ if (options.keys & %w[aria-hidden aria-label data-hidden]).empty?
+ # Add 'aria-hidden' and 'data-hidden' if they are not set in options.
options['aria-hidden'] = true
+ options['data-hidden'] = true
end
options.include?(:base) ? fa_stacked_icon(names, options) : fa_icon(names, options)
diff --git a/app/services/members/authorized_destroy_service.rb b/app/services/members/authorized_destroy_service.rb
index 7912cac65d3..9e84e2a8f62 100644
--- a/app/services/members/authorized_destroy_service.rb
+++ b/app/services/members/authorized_destroy_service.rb
@@ -29,7 +29,7 @@ module Members
issue_ids = IssuesFinder.new(user, group_id: member.source_id, assignee_id: member.user_id).
execute.pluck(:id)
- IssueAssignee.destroy_all(issue_id: issue_ids, user_id: member.user_id)
+ IssueAssignee.delete_all(issue_id: issue_ids, user_id: member.user_id)
MergeRequestsFinder.new(user, group_id: member.source_id, assignee_id: member.user_id).
execute.
@@ -37,10 +37,15 @@ module Members
else
project = member.source
- IssueAssignee.destroy_all(
- user_id: member.user_id,
- issue_id: project.issues.opened.assigned_to(member.user).select(:id)
- )
+ # SELECT 1 FROM issues WHERE issues.id = issue_assignees.issue_id AND issues.project_id = X
+ issues = Issue.unscoped.select(1).
+ where('issues.id = issue_assignees.issue_id').
+ where(project_id: project.id)
+
+ # DELETE FROM issue_assignees WHERE user_id = X AND EXISTS (...)
+ IssueAssignee.unscoped.
+ where('user_id = :user_id AND EXISTS (:sub)', user_id: member.user_id, sub: issues).
+ delete_all
project.merge_requests.opened.assigned_to(member.user).update_all(assignee_id: nil)
end
diff --git a/app/views/admin/health_check/show.html.haml b/app/views/admin/health_check/show.html.haml
index 6a208d76a38..4deccf4aa93 100644
--- a/app/views/admin/health_check/show.html.haml
+++ b/app/views/admin/health_check/show.html.haml
@@ -16,24 +16,15 @@
= icon('spinner')
Reset health check access token
%p.light
- Health information can be retrieved as plain text, JSON, or XML using:
+ Health information can be retrieved from the following endpoints. More information is available
+ = link_to 'here', help_page_path('user/admin_area/monitoring/health_check')
%ul
%li
- %code= health_check_url(token: current_application_settings.health_check_access_token)
+ %code= readiness_url(token: current_application_settings.health_check_access_token)
%li
- %code= health_check_url(token: current_application_settings.health_check_access_token, format: :json)
+ %code= liveness_url(token: current_application_settings.health_check_access_token)
%li
- %code= health_check_url(token: current_application_settings.health_check_access_token, format: :xml)
-
- %p.light
- You can also ask for the status of specific services:
- %ul
- %li
- %code= health_check_url(token: current_application_settings.health_check_access_token, checks: :cache)
- %li
- %code= health_check_url(token: current_application_settings.health_check_access_token, checks: :database)
- %li
- %code= health_check_url(token: current_application_settings.health_check_access_token, checks: :migrations)
+ %code= metrics_url(token: current_application_settings.health_check_access_token)
%hr
.panel.panel-default
diff --git a/app/views/projects/pipeline_schedules/_form.html.haml b/app/views/projects/pipeline_schedules/_form.html.haml
index d6f4f1a206c..bbed10039af 100644
--- a/app/views/projects/pipeline_schedules/_form.html.haml
+++ b/app/views/projects/pipeline_schedules/_form.html.haml
@@ -5,29 +5,29 @@
= form_for [@project.namespace.becomes(Namespace), @project, @schedule], as: :schedule, html: { id: "new-pipeline-schedule-form", class: "form-horizontal js-pipeline-schedule-form" } do |f|
= form_errors(@schedule)
.form-group
- .col-md-6
+ .col-md-9
= f.label :description, 'Description', class: 'label-light'
= f.text_field :description, class: 'form-control', required: true, autofocus: true, placeholder: 'Provide a short description for this pipeline'
.form-group
- .col-md-12
+ .col-md-9
= f.label :cron, 'Interval Pattern', class: 'label-light'
#interval-pattern-input{ data: { initial_interval: @schedule.cron } }
.form-group
- .col-md-6
+ .col-md-9
= f.label :cron_timezone, 'Cron Timezone', class: 'label-light'
= dropdown_tag("Select a timezone", options: { toggle_class: 'btn js-timezone-dropdown', title: "Select a timezone", filter: true, placeholder: "Filter", data: { data: timezone_data } } )
= f.text_field :cron_timezone, value: @schedule.cron_timezone, id: 'schedule_cron_timezone', class: 'hidden', name: 'schedule[cron_timezone]', required: true
.form-group
- .col-md-6
+ .col-md-9
= f.label :ref, 'Target Branch', class: 'label-light'
- = dropdown_tag("Select target branch", options: { toggle_class: 'btn js-target-branch-dropdown git-revision-dropdown-toggle', dropdown_class: 'git-revision-dropdown', title: "Select target branch", filter: true, placeholder: "Filter", data: { data: @project.repository.branch_names } } )
+ = dropdown_tag("Select target branch", options: { toggle_class: 'btn js-target-branch-dropdown git-revision-dropdown-toggle', dropdown_class: 'git-revision-dropdown', title: "Select target branch", filter: true, placeholder: "Filter", data: { data: @project.repository.branch_names, default_branch: @project.default_branch } } )
= f.text_field :ref, value: @schedule.ref, id: 'schedule_ref', class: 'hidden', name: 'schedule[ref]', required: true
.form-group
- .col-md-6
+ .col-md-9
= f.label :active, 'Activated', class: 'label-light'
%div
= f.check_box :active, required: false, value: @schedule.active?
- active
+ Active
.footer-block.row-content-block
= f.submit 'Save pipeline schedule', class: 'btn btn-create', tabindex: 3
= link_to 'Cancel', pipeline_schedules_path(@project), class: 'btn btn-cancel'
diff --git a/app/views/shared/notes/_hints.html.haml b/app/views/shared/notes/_hints.html.haml
index 81d97eabe65..7ce6130de60 100644
--- a/app/views/shared/notes/_hints.html.haml
+++ b/app/views/shared/notes/_hints.html.haml
@@ -9,6 +9,27 @@
- else
is
supported
- %button.toolbar-button.markdown-selector{ type: 'button', tabindex: '-1' }
- = icon('file-image-o', class: 'toolbar-button-icon')
- Attach a file
+
+ %span.uploading-container
+ %span.uploading-progress-container.hide
+ = icon('file-image-o', class: 'toolbar-button-icon')
+ %span.attaching-file-message
+ -# Populated by app/assets/javascripts/dropzone_input.js
+ %span.uploading-progress 0%
+ %span.uploading-spinner
+ = icon('spinner spin', class: 'toolbar-button-icon')
+
+ %span.uploading-error-container.hide
+ %span.uploading-error-icon
+ = icon('file-image-o', class: 'toolbar-button-icon')
+ %span.uploading-error-message
+ -# Populated by app/assets/javascripts/dropzone_input.js
+ %button.retry-uploading-link{ type: 'button' } Try again
+ or
+ %button.attach-new-file.markdown-selector{ type: 'button' } attach a new file
+
+ %button.markdown-selector.button-attach-file{ type: 'button', tabindex: '-1' }
+ = icon('file-image-o', class: 'toolbar-button-icon')
+ Attach a file
+
+ %button.btn.btn-default.btn-xs.hide.button-cancel-uploading-files{ type: 'button' } Cancel
diff --git a/changelogs/unreleased/32395-duplicate-string-in-https-docs-gitlab-com-ce-administration-environment_variables-html.yml b/changelogs/unreleased/32395-duplicate-string-in-https-docs-gitlab-com-ce-administration-environment_variables-html.yml
new file mode 100644
index 00000000000..d2be3d6cc4b
--- /dev/null
+++ b/changelogs/unreleased/32395-duplicate-string-in-https-docs-gitlab-com-ce-administration-environment_variables-html.yml
@@ -0,0 +1,4 @@
+---
+title: Removes duplicate environment variable in documentation
+merge_request:
+author:
diff --git a/changelogs/unreleased/add_ability_to_cancel_attaching_file_and_redesign_attaching_files_ui.yml b/changelogs/unreleased/add_ability_to_cancel_attaching_file_and_redesign_attaching_files_ui.yml
new file mode 100644
index 00000000000..fcf4efa2846
--- /dev/null
+++ b/changelogs/unreleased/add_ability_to_cancel_attaching_file_and_redesign_attaching_files_ui.yml
@@ -0,0 +1,4 @@
+---
+title: Add an ability to cancel attaching file and redesign attaching files UI
+merge_request: 9431
+author: blackst0ne
diff --git a/changelogs/unreleased/update-admin-health-page.yml b/changelogs/unreleased/update-admin-health-page.yml
new file mode 100644
index 00000000000..51aa6682b49
--- /dev/null
+++ b/changelogs/unreleased/update-admin-health-page.yml
@@ -0,0 +1,5 @@
+---
+title: Added application readiness endpoints to the monitoring health check admin
+ view
+merge_request:
+author:
diff --git a/db/migrate/20170320171632_create_issue_assignees_table.rb b/db/migrate/20170320171632_create_issue_assignees_table.rb
deleted file mode 100644
index 23b8da37b6d..00000000000
--- a/db/migrate/20170320171632_create_issue_assignees_table.rb
+++ /dev/null
@@ -1,40 +0,0 @@
-# See http://doc.gitlab.com/ce/development/migration_style_guide.html
-# for more information on how to write migrations for GitLab.
-
-class CreateIssueAssigneesTable < ActiveRecord::Migration
- include Gitlab::Database::MigrationHelpers
-
- INDEX_NAME = 'index_issue_assignees_on_issue_id_and_user_id'
-
- # Set this constant to true if this migration requires downtime.
- DOWNTIME = false
-
- # When a migration requires downtime you **must** uncomment the following
- # constant and define a short and easy to understand explanation as to why the
- # migration requires downtime.
- # DOWNTIME_REASON = ''
-
- # When using the methods "add_concurrent_index" or "add_column_with_default"
- # you must disable the use of transactions as these methods can not run in an
- # existing transaction. When using "add_concurrent_index" make sure that this
- # method is the _only_ method called in the migration, any other changes
- # should go in a separate migration. This ensures that upon failure _only_ the
- # index creation fails and can be retried or reverted easily.
- #
- # To disable transactions uncomment the following line and remove these
- # comments:
- # disable_ddl_transaction!
-
- def up
- create_table :issue_assignees do |t|
- t.references :user, foreign_key: { on_delete: :cascade }, index: true, null: false
- t.references :issue, foreign_key: { on_delete: :cascade }, null: false
- end
-
- add_index :issue_assignees, [:issue_id, :user_id], unique: true, name: INDEX_NAME
- end
-
- def down
- drop_table :issue_assignees
- end
-end
diff --git a/db/migrate/20170320173259_migrate_assignees.rb b/db/migrate/20170320173259_migrate_assignees.rb
index ba8edbd7d32..23e7500a32d 100644
--- a/db/migrate/20170320173259_migrate_assignees.rb
+++ b/db/migrate/20170320173259_migrate_assignees.rb
@@ -37,16 +37,8 @@ class MigrateAssignees < ActiveRecord::Migration
users.project("true").where(users[:id].eq(table[:assignee_id])).exists.not
))
end
-
- execute <<-EOF
- INSERT INTO issue_assignees(issue_id, user_id)
- SELECT id, assignee_id FROM issues WHERE assignee_id IS NOT NULL
- EOF
end
def down
- execute <<-EOF
- DELETE FROM issue_assignees
- EOF
end
end
diff --git a/db/migrate/20170516153305_migrate_assignee_to_separate_table.rb b/db/migrate/20170516153305_migrate_assignee_to_separate_table.rb
new file mode 100644
index 00000000000..f269ca7fc34
--- /dev/null
+++ b/db/migrate/20170516153305_migrate_assignee_to_separate_table.rb
@@ -0,0 +1,83 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class MigrateAssigneeToSeparateTable < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ # Set this constant to true if this migration requires downtime.
+ DOWNTIME = false
+
+ # When a migration requires downtime you **must** uncomment the following
+ # constant and define a short and easy to understand explanation as to why the
+ # migration requires downtime.
+ # DOWNTIME_REASON = ''
+
+ # When using the methods "add_concurrent_index", "remove_concurrent_index" or
+ # "add_column_with_default" you must disable the use of transactions
+ # as these methods can not run in an existing transaction.
+ # When using "add_concurrent_index" or "remove_concurrent_index" methods make sure
+ # that either of them is the _only_ method called in the migration,
+ # any other changes should go in a separate migration.
+ # This ensures that upon failure _only_ the index creation or removing fails
+ # and can be retried or reverted easily.
+ #
+ # To disable transactions uncomment the following line and remove these
+ # comments:
+ # disable_ddl_transaction!
+
+ def up
+ drop_table(:issue_assignees) if table_exists?(:issue_assignees)
+
+ if Gitlab::Database.mysql?
+ execute <<-EOF
+ CREATE TABLE issue_assignees AS
+ SELECT assignee_id AS user_id, id AS issue_id FROM issues WHERE assignee_id IS NOT NULL
+ EOF
+ else
+ ActiveRecord::Base.transaction do
+ execute('LOCK TABLE issues IN EXCLUSIVE MODE')
+
+ execute <<-EOF
+ CREATE TABLE issue_assignees AS
+ SELECT assignee_id AS user_id, id AS issue_id FROM issues WHERE assignee_id IS NOT NULL
+ EOF
+
+ execute <<-EOF
+ CREATE OR REPLACE FUNCTION replicate_assignee_id()
+ RETURNS trigger AS
+ $BODY$
+ BEGIN
+ if OLD.assignee_id IS NOT NULL THEN
+ DELETE FROM issue_assignees WHERE issue_id = OLD.id;
+ END IF;
+
+ if NEW.assignee_id IS NOT NULL THEN
+ INSERT INTO issue_assignees (user_id, issue_id) VALUES (NEW.assignee_id, NEW.id);
+ END IF;
+
+ RETURN NEW;
+ END;
+ $BODY$
+ LANGUAGE 'plpgsql'
+ VOLATILE;
+
+ CREATE TRIGGER replicate_assignee_id
+ BEFORE INSERT OR UPDATE OF assignee_id
+ ON issues
+ FOR EACH ROW EXECUTE PROCEDURE replicate_assignee_id();
+ EOF
+ end
+ end
+ end
+
+ def down
+ drop_table(:issue_assignees) if table_exists?(:issue_assignees)
+
+ if Gitlab::Database.postgresql?
+ execute <<-EOF
+ DROP TRIGGER IF EXISTS replicate_assignee_id ON issues;
+ DROP FUNCTION IF EXISTS replicate_assignee_id();
+ EOF
+ end
+ end
+end
diff --git a/db/migrate/20170516183131_add_indices_to_issue_assignees.rb b/db/migrate/20170516183131_add_indices_to_issue_assignees.rb
new file mode 100644
index 00000000000..a1f064c6848
--- /dev/null
+++ b/db/migrate/20170516183131_add_indices_to_issue_assignees.rb
@@ -0,0 +1,41 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class AddIndicesToIssueAssignees < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ # Set this constant to true if this migration requires downtime.
+ DOWNTIME = false
+
+ # When a migration requires downtime you **must** uncomment the following
+ # constant and define a short and easy to understand explanation as to why the
+ # migration requires downtime.
+ # DOWNTIME_REASON = ''
+
+ # When using the methods "add_concurrent_index", "remove_concurrent_index" or
+ # "add_column_with_default" you must disable the use of transactions
+ # as these methods can not run in an existing transaction.
+ # When using "add_concurrent_index" or "remove_concurrent_index" methods make sure
+ # that either of them is the _only_ method called in the migration,
+ # any other changes should go in a separate migration.
+ # This ensures that upon failure _only_ the index creation or removing fails
+ # and can be retried or reverted easily.
+ #
+ # To disable transactions uncomment the following line and remove these
+ # comments:
+ disable_ddl_transaction!
+
+ def up
+ add_concurrent_index :issue_assignees, [:issue_id, :user_id], unique: true, name: 'index_issue_assignees_on_issue_id_and_user_id'
+ add_concurrent_index :issue_assignees, :user_id, name: 'index_issue_assignees_on_user_id'
+ add_concurrent_foreign_key :issue_assignees, :users, column: :user_id, on_delete: :cascade
+ add_concurrent_foreign_key :issue_assignees, :issues, column: :issue_id, on_delete: :cascade
+ end
+
+ def down
+ remove_foreign_key :issue_assignees, column: :user_id
+ remove_foreign_key :issue_assignees, column: :issue_id
+ remove_concurrent_index :issue_assignees, [:issue_id, :user_id] if index_exists?(:issue_assignees, [:issue_id, :user_id])
+ remove_concurrent_index :issue_assignees, :user_id if index_exists?(:issue_assignees, :user_id)
+ end
+end
diff --git a/db/post_migrate/20170516165238_cleanup_trigger_for_issues.rb b/db/post_migrate/20170516165238_cleanup_trigger_for_issues.rb
new file mode 100644
index 00000000000..378fe5603c3
--- /dev/null
+++ b/db/post_migrate/20170516165238_cleanup_trigger_for_issues.rb
@@ -0,0 +1,39 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class CleanupTriggerForIssues < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ # Set this constant to true if this migration requires downtime.
+ DOWNTIME = false
+
+ # When a migration requires downtime you **must** uncomment the following
+ # constant and define a short and easy to understand explanation as to why the
+ # migration requires downtime.
+ # DOWNTIME_REASON = ''
+
+ # When using the methods "add_concurrent_index", "remove_concurrent_index" or
+ # "add_column_with_default" you must disable the use of transactions
+ # as these methods can not run in an existing transaction.
+ # When using "add_concurrent_index" or "remove_concurrent_index" methods make sure
+ # that either of them is the _only_ method called in the migration,
+ # any other changes should go in a separate migration.
+ # This ensures that upon failure _only_ the index creation or removing fails
+ # and can be retried or reverted easily.
+ #
+ # To disable transactions uncomment the following line and remove these
+ # comments:
+ disable_ddl_transaction!
+
+ def up
+ if Gitlab::Database.postgresql?
+ execute <<-EOF
+ DROP TRIGGER IF EXISTS replicate_assignee_id ON issues;
+ DROP FUNCTION IF EXISTS replicate_assignee_id();
+ EOF
+ end
+ end
+
+ def down
+ end
+end
diff --git a/db/post_migrate/20170516181025_add_constraints_to_issue_assignees_table.rb b/db/post_migrate/20170516181025_add_constraints_to_issue_assignees_table.rb
new file mode 100644
index 00000000000..2aab1f4d14f
--- /dev/null
+++ b/db/post_migrate/20170516181025_add_constraints_to_issue_assignees_table.rb
@@ -0,0 +1,37 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class AddConstraintsToIssueAssigneesTable < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ # Set this constant to true if this migration requires downtime.
+ DOWNTIME = false
+
+ # When a migration requires downtime you **must** uncomment the following
+ # constant and define a short and easy to understand explanation as to why the
+ # migration requires downtime.
+ # DOWNTIME_REASON = ''
+
+ # When using the methods "add_concurrent_index", "remove_concurrent_index" or
+ # "add_column_with_default" you must disable the use of transactions
+ # as these methods can not run in an existing transaction.
+ # When using "add_concurrent_index" or "remove_concurrent_index" methods make sure
+ # that either of them is the _only_ method called in the migration,
+ # any other changes should go in a separate migration.
+ # This ensures that upon failure _only_ the index creation or removing fails
+ # and can be retried or reverted easily.
+ #
+ # To disable transactions uncomment the following line and remove these
+ # comments:
+ # disable_ddl_transaction!
+
+ def up
+ change_column :issue_assignees, :issue_id, :integer, null: false
+ change_column :issue_assignees, :user_id, :integer, null: false
+ end
+
+ def down
+ change_column :issue_assignees, :issue_id, :integer
+ change_column :issue_assignees, :user_id, :integer
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 42afef7391a..294e0b531eb 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -11,7 +11,7 @@
#
# It's strongly recommended that you check this file into your version control system.
-ActiveRecord::Schema.define(version: 20170511101000) do
+ActiveRecord::Schema.define(version: 20170516183131) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@@ -459,7 +459,7 @@ ActiveRecord::Schema.define(version: 20170511101000) do
add_index "identities", ["user_id"], name: "index_identities_on_user_id", using: :btree
- create_table "issue_assignees", force: :cascade do |t|
+ create_table "issue_assignees", id: false, force: :cascade do |t|
t.integer "user_id", null: false
t.integer "issue_id", null: false
end
@@ -1423,8 +1423,8 @@ ActiveRecord::Schema.define(version: 20170511101000) do
add_foreign_key "ci_triggers", "users", column: "owner_id", name: "fk_e8e10d1964", on_delete: :cascade
add_foreign_key "ci_variables", "projects", name: "fk_ada5eb64b3", on_delete: :cascade
add_foreign_key "container_repositories", "projects"
- add_foreign_key "issue_assignees", "issues", on_delete: :cascade
- add_foreign_key "issue_assignees", "users", on_delete: :cascade
+ add_foreign_key "issue_assignees", "issues", name: "fk_b7d881734a", on_delete: :cascade
+ add_foreign_key "issue_assignees", "users", name: "fk_5e0c8d9154", on_delete: :cascade
add_foreign_key "issue_metrics", "issues", on_delete: :cascade
add_foreign_key "label_priorities", "labels", on_delete: :cascade
add_foreign_key "label_priorities", "projects", on_delete: :cascade
diff --git a/doc/administration/environment_variables.md b/doc/administration/environment_variables.md
index 76029b30dd8..b6676026d06 100644
--- a/doc/administration/environment_variables.md
+++ b/doc/administration/environment_variables.md
@@ -20,7 +20,6 @@ Variable | Type | Description
`GITLAB_EMAIL_FROM` | string | The e-mail address used in the "From" field in e-mails sent by GitLab
`GITLAB_EMAIL_DISPLAY_NAME` | string | The name used in the "From" field in e-mails sent by GitLab
`GITLAB_EMAIL_REPLY_TO` | string | The e-mail address used in the "Reply-To" field in e-mails sent by GitLab
-`GITLAB_EMAIL_REPLY_TO` | string | The e-mail address used in the "Reply-To" field in e-mails sent by GitLab
`GITLAB_EMAIL_SUBJECT_SUFFIX` | string | The e-mail subject suffix used in e-mails sent by GitLab
`GITLAB_UNICORN_MEMORY_MIN` | integer | The minimum memory threshold (in bytes) for the Unicorn worker killer
`GITLAB_UNICORN_MEMORY_MAX` | integer | The maximum memory threshold (in bytes) for the Unicorn worker killer
diff --git a/doc/development/fe_guide/index.md b/doc/development/fe_guide/index.md
index a08694fb66a..64bcb4a0257 100644
--- a/doc/development/fe_guide/index.md
+++ b/doc/development/fe_guide/index.md
@@ -45,15 +45,11 @@ should be `new-feature`.
branch from `new-feature`, let's call it `new-feature-step-2` and repeat the process done before.
```shell
-* master
-|\
-| * new-feature
-| |\
-| | * new-feature-step-1
-| |\
-| | * new-feature-step-2
-| |\
-| | * new-feature-step-3
+master
+└─ new-feature
+ ├─ new-feature-step-1
+ ├─ new-feature-step-2
+ └─ new-feature-step-3
```
**Tips**
diff --git a/doc/user/group/subgroups/index.md b/doc/user/group/subgroups/index.md
index ce5da07c61a..a4726673fc4 100644
--- a/doc/user/group/subgroups/index.md
+++ b/doc/user/group/subgroups/index.md
@@ -71,8 +71,10 @@ structure.
- You need to be an Owner of a group in order to be able to create
a subgroup. For more information check the [permissions table][permissions].
- For a list of words that are not allowed to be used as group names see the
- [`namespace_validator.rb` file][reserved] under the `RESERVED` and
- `WILDCARD_ROUTES` lists.
+ [`dynamic_path_validator.rb` file][reserved] under the `TOP_LEVEL_ROUTES`, `WILDCARD_ROUTES` and `GROUP_ROUTES` lists:
+ - `TOP_LEVEL_ROUTES`: are names that are reserved as usernames or top level groups
+ - `WILDCARD_ROUTES`: are names that are reserved for child groups or projects.
+ - `GROUP_ROUTES`: are names that are reserved for all groups or projects.
To create a subgroup:
@@ -161,4 +163,4 @@ Here's a list of what you can't do with subgroups:
[ce-2772]: https://gitlab.com/gitlab-org/gitlab-ce/issues/2772
[permissions]: ../../permissions.md#group
-[reserved]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/app/validators/namespace_validator.rb
+[reserved]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/app/validators/dynamic_path_validator.rb
diff --git a/spec/features/merge_requests/merge_immediately_with_pipeline_spec.rb b/spec/features/merge_requests/merge_immediately_with_pipeline_spec.rb
index 5820784f8e7..c102722d6db 100644
--- a/spec/features/merge_requests/merge_immediately_with_pipeline_spec.rb
+++ b/spec/features/merge_requests/merge_immediately_with_pipeline_spec.rb
@@ -34,11 +34,13 @@ feature 'Merge immediately', :feature, :js do
page.within '.mr-widget-body' do
find('.dropdown-toggle').click
- click_link 'Merge immediately'
+ Sidekiq::Testing.fake! do
+ click_link 'Merge immediately'
- expect(find('.accept-merge-request.btn-info')).to have_content('Merge in progress')
+ expect(find('.accept-merge-request.btn-info')).to have_content('Merge in progress')
- wait_for_ajax
+ wait_for_vue_resource
+ end
end
end
end
diff --git a/spec/features/projects/pipeline_schedules_spec.rb b/spec/features/projects/pipeline_schedules_spec.rb
index fe9f94db574..03a30bfb996 100644
--- a/spec/features/projects/pipeline_schedules_spec.rb
+++ b/spec/features/projects/pipeline_schedules_spec.rb
@@ -70,6 +70,11 @@ feature 'Pipeline Schedules', :feature do
describe 'POST /projects/pipeline_schedules/new', js: true do
let(:visit_page) { visit_new_pipeline_schedule }
+ it 'sets defaults for timezone and target branch' do
+ expect(page).to have_button('master')
+ expect(page).to have_button('UTC')
+ end
+
it 'it creates a new scheduled pipeline' do
fill_in_schedule_form
save_pipeline_schedule
@@ -118,12 +123,12 @@ feature 'Pipeline Schedules', :feature do
end
def select_timezone
- click_button 'Select a timezone'
+ find('.js-timezone-dropdown').click
click_link 'American Samoa'
end
def select_target_branch
- click_button 'Select target branch'
+ find('.js-target-branch-dropdown').click
click_link 'master'
end
diff --git a/spec/features/uploads/user_uploads_file_to_note_spec.rb b/spec/features/uploads/user_uploads_file_to_note_spec.rb
index 0c160dd74b4..8f03024ea06 100644
--- a/spec/features/uploads/user_uploads_file_to_note_spec.rb
+++ b/spec/features/uploads/user_uploads_file_to_note_spec.rb
@@ -5,18 +5,78 @@ feature 'User uploads file to note', feature: true do
let(:user) { create(:user) }
let(:project) { create(:empty_project, creator: user, namespace: user.namespace) }
+ let(:issue) { create(:issue, project: project, author: user) }
- scenario 'they see the attached file', js: true do
- issue = create(:issue, project: project, author: user)
-
+ before do
login_as(user)
visit namespace_project_issue_path(project.namespace, project, issue)
+ end
+
+ context 'before uploading' do
+ it 'shows "Attach a file" button', js: true do
+ expect(page).to have_button('Attach a file')
+ expect(page).not_to have_selector('.uploading-progress-container', visible: true)
+ end
+ end
+
+ context 'uploading is in progress' do
+ it 'shows "Cancel" button on uploading', js: true do
+ dropzone_file([Rails.root.join('spec', 'fixtures', 'dk.png')], 0, false)
+
+ expect(page).to have_button('Cancel')
+ end
+
+ it 'cancels uploading on clicking to "Cancel" button', js: true do
+ dropzone_file([Rails.root.join('spec', 'fixtures', 'dk.png')], 0, false)
+
+ click_button 'Cancel'
+
+ expect(page).to have_button('Attach a file')
+ expect(page).not_to have_button('Cancel')
+ expect(page).not_to have_selector('.uploading-progress-container', visible: true)
+ end
+
+ it 'shows "Attaching a file" message on uploading 1 file', js: true do
+ dropzone_file([Rails.root.join('spec', 'fixtures', 'dk.png')], 0, false)
+
+ expect(page).to have_selector('.attaching-file-message', visible: true, text: 'Attaching a file -')
+ end
+
+ it 'shows "Attaching 2 files" message on uploading 2 file', js: true do
+ dropzone_file([Rails.root.join('spec', 'fixtures', 'video_sample.mp4'),
+ Rails.root.join('spec', 'fixtures', 'dk.png')], 0, false)
+
+ expect(page).to have_selector('.attaching-file-message', visible: true, text: 'Attaching 2 files -')
+ end
+
+ it 'shows error message, "retry" and "attach a new file" link a if file is too big', js: true do
+ dropzone_file([Rails.root.join('spec', 'fixtures', 'video_sample.mp4')], 0.01)
+
+ error_text = 'File is too big (0.06MiB). Max filesize: 0.01MiB.'
+
+ expect(page).to have_selector('.uploading-error-message', visible: true, text: error_text)
+ expect(page).to have_selector('.retry-uploading-link', visible: true, text: 'Try again')
+ expect(page).to have_selector('.attach-new-file', visible: true, text: 'attach a new file')
+ expect(page).not_to have_button('Attach a file')
+ end
+ end
+
+ context 'uploading is complete' do
+ it 'shows "Attach a file" button on uploading complete', js: true do
+ dropzone_file([Rails.root.join('spec', 'fixtures', 'dk.png')])
+ wait_for_ajax
+
+ expect(page).to have_button('Attach a file')
+ expect(page).not_to have_selector('.uploading-progress-container', visible: true)
+ end
- dropzone_file(Rails.root.join('spec', 'fixtures', 'dk.png'))
- click_button 'Comment'
- wait_for_ajax
+ scenario 'they see the attached file', js: true do
+ dropzone_file([Rails.root.join('spec', 'fixtures', 'dk.png')])
+ click_button 'Comment'
+ wait_for_ajax
- expect(find('a.no-attachment-icon img[alt="dk"]')['src'])
- .to match(%r{/#{project.full_path}/uploads/\h{32}/dk\.png$})
+ expect(find('a.no-attachment-icon img[alt="dk"]')['src'])
+ .to match(%r{/#{project.full_path}/uploads/\h{32}/dk\.png$})
+ end
end
end
diff --git a/spec/javascripts/pipeline_schedules/interval_pattern_input_spec.js b/spec/javascripts/pipeline_schedules/interval_pattern_input_spec.js
index 08fa6ca9057..845b371d90c 100644
--- a/spec/javascripts/pipeline_schedules/interval_pattern_input_spec.js
+++ b/spec/javascripts/pipeline_schedules/interval_pattern_input_spec.js
@@ -36,20 +36,6 @@ describe('Interval Pattern Input Component', function () {
expect(this.intervalPatternComponent.initialCronInterval).toBe(this.initialCronInterval);
});
- it('sets showUnsetWarning to false', function (done) {
- Vue.nextTick(() => {
- expect(this.intervalPatternComponent.showUnsetWarning).toBe(false);
- done();
- });
- });
-
- it('does not render showUnsetWarning', function (done) {
- Vue.nextTick(() => {
- expect(this.intervalPatternComponent.$el.outerHTML).not.toContain('Schedule not yet set');
- done();
- });
- });
-
it('sets isEditable to true', function (done) {
Vue.nextTick(() => {
expect(this.intervalPatternComponent.isEditable).toBe(true);
@@ -72,20 +58,6 @@ describe('Interval Pattern Input Component', function () {
expect(this.intervalPatternComponent).toBeDefined();
});
- it('sets showUnsetWarning to false', function (done) {
- Vue.nextTick(() => {
- expect(this.intervalPatternComponent.showUnsetWarning).toBe(false);
- done();
- });
- });
-
- it('does not render showUnsetWarning', function (done) {
- Vue.nextTick(() => {
- expect(this.intervalPatternComponent.$el.outerHTML).not.toContain('Schedule not yet set');
- done();
- });
- });
-
it('sets isEditable to false', function (done) {
Vue.nextTick(() => {
expect(this.intervalPatternComponent.isEditable).toBe(false);
@@ -113,20 +85,6 @@ describe('Interval Pattern Input Component', function () {
expect(this.intervalPatternComponent.initialCronInterval).toBe(defaultInitialCronInterval);
});
- it('sets showUnsetWarning to true', function (done) {
- Vue.nextTick(() => {
- expect(this.intervalPatternComponent.showUnsetWarning).toBe(true);
- done();
- });
- });
-
- it('renders showUnsetWarning to true', function (done) {
- Vue.nextTick(() => {
- expect(this.intervalPatternComponent.$el.outerHTML).toContain('Schedule not yet set');
- done();
- });
- });
-
it('sets isEditable to true', function (done) {
Vue.nextTick(() => {
expect(this.intervalPatternComponent.isEditable).toBe(true);
diff --git a/spec/support/dropzone_helper.rb b/spec/support/dropzone_helper.rb
index 984ec7d2741..02fdeb08afe 100644
--- a/spec/support/dropzone_helper.rb
+++ b/spec/support/dropzone_helper.rb
@@ -6,32 +6,52 @@ module DropzoneHelper
# Dropzone events to perform the actual upload.
#
# This method waits for the upload to complete before returning.
- def dropzone_file(file_path)
+ # max_file_size is an optional parameter.
+ # If it's not 0, then it used in dropzone.maxFilesize parameter.
+ # wait_for_queuecomplete is an optional parameter.
+ # If it's 'false', then the helper will NOT wait for backend response
+ # It lets to test behaviors while AJAX is processing.
+ def dropzone_file(files, max_file_size = 0, wait_for_queuecomplete = true)
# Generate a fake file input that Capybara can attach to
page.execute_script <<-JS.strip_heredoc
+ $('#fakeFileInput').remove();
var fakeFileInput = window.$('<input/>').attr(
- {id: 'fakeFileInput', type: 'file'}
+ {id: 'fakeFileInput', type: 'file', multiple: true}
).appendTo('body');
window._dropzoneComplete = false;
JS
- # Attach the file to the fake input selector with Capybara
- attach_file('fakeFileInput', file_path)
+ # Attach files to the fake input selector with Capybara
+ attach_file('fakeFileInput', files)
# Manually trigger a Dropzone "drop" event with the fake input's file list
page.execute_script <<-JS.strip_heredoc
- var fileList = [$('#fakeFileInput')[0].files[0]];
- var e = jQuery.Event('drop', { dataTransfer : { files : fileList } });
-
var dropzone = $('.div-dropzone')[0].dropzone;
+ dropzone.options.autoProcessQueue = false;
+
+ if (#{max_file_size} > 0) {
+ dropzone.options.maxFilesize = #{max_file_size};
+ }
+
dropzone.on('queuecomplete', function() {
window._dropzoneComplete = true;
});
- dropzone.listeners[0].events.drop(e);
+
+ var fileList = [$('#fakeFileInput')[0].files];
+
+ $.map(fileList, function(file){
+ var e = jQuery.Event('drop', { dataTransfer : { files : file } });
+
+ dropzone.listeners[0].events.drop(e);
+ });
+
+ dropzone.processQueue();
JS
- # Wait until Dropzone's fired `queuecomplete`
- loop until page.evaluate_script('window._dropzoneComplete === true')
+ if wait_for_queuecomplete
+ # Wait until Dropzone's fired `queuecomplete`
+ loop until page.evaluate_script('window._dropzoneComplete === true')
+ end
end
end