mirror of
https://github.com/scinote-eln/scinote-web.git
synced 2025-11-09 16:01:30 +08:00
Step creation is now completelly validated on client-side also, to avoid front-end problems after server-side validation. This was needed as lots of issues were caused by this. Also step creation user experience is enriched and refactoring of related code was done.
This commit is contained in:
parent
da735a51f4
commit
e2f94caa96
11 changed files with 154 additions and 48 deletions
|
|
@ -311,6 +311,7 @@ function startFileUpload(ev, btn) {
|
|||
var origAssetId = assetInput ? assetInput.value : null;
|
||||
var url = '/asset_signature.json';
|
||||
|
||||
animateSpinner();
|
||||
$form.clear_form_errors();
|
||||
|
||||
directUpload(form, origAssetId, url, function (assetId) {
|
||||
|
|
|
|||
|
|
@ -588,33 +588,89 @@ $("[data-action='new-step']").on("ajax:success", function(e, data) {
|
|||
});
|
||||
});
|
||||
|
||||
// Needed because Paperclip deletes files on server-side validation fail (after trying to save the model)
|
||||
function stepValidator( event ) {
|
||||
function nameValidator(event) {
|
||||
var form = $(event.target.form);
|
||||
var nameTooShort = $( "#step_name" ).val().length === 0;
|
||||
var nameTooLong = $( "#step_name" ).val().length > 50;
|
||||
var errMsg;
|
||||
if (nameTooShort) {
|
||||
errMsg = I18n.t("devise.names.length_too_short");
|
||||
animateSpinner(null,false);
|
||||
errMsg = I18n.t("devise.names.not_blank");
|
||||
} else if (nameTooLong) {
|
||||
errMsg = I18n.t("devise.names.length_too_long", { max_length: 50 });
|
||||
animateSpinner(null,false);
|
||||
}
|
||||
|
||||
if (!_.isUndefined(errMsg)) {
|
||||
if(!$("#step_name_error_msg").length) {
|
||||
$("<span id='step_name_error_msg' class='help-block'>" + errMsg + "</span>").insertAfter("#step_name");
|
||||
$(".form-group:has(> #step_name)").addClass("has-error");
|
||||
$("#new-step-main-tab").addClass("has-error");
|
||||
} else {
|
||||
$("#step_name_error_msg").html(errMsg);
|
||||
var hasErrors = !_.isUndefined(errMsg);
|
||||
if (hasErrors) {
|
||||
renderError($("#step_name"), errMsg, form);
|
||||
}
|
||||
animateSpinner(null,false);
|
||||
$("#new-step-main-tab a").click();
|
||||
event.preventDefault();
|
||||
return !hasErrors;
|
||||
}
|
||||
|
||||
clearBlankElements(event.target.form);
|
||||
function checklistsValidator(event, editMode) {
|
||||
var form = event.target.form;
|
||||
$(form).clear_form_errors();
|
||||
var noErrors = true;
|
||||
|
||||
// For every visible (i.e. not removed) checklist
|
||||
$(form).find(".nested_step_checklists[style!='display: none']").each(function() {
|
||||
var checklistNameInput = $(this).find(".checklist_name");
|
||||
var checklistNameEmpty = !checklistNameInput.val();
|
||||
anyChecklistItemFilled = false;
|
||||
|
||||
// For every ckecklist item input
|
||||
$(" .checklist-item-text", $(this)).each(function() {
|
||||
if($(this).val()) {
|
||||
anyChecklistItemFilled = true;
|
||||
} else {
|
||||
// Remove empty checklist item
|
||||
$(this).closest("fieldset").remove();
|
||||
}
|
||||
})
|
||||
|
||||
if(checklistNameEmpty) {
|
||||
if(anyChecklistItemFilled || editMode) {
|
||||
// In edit mode, name can't be blank
|
||||
var errMsg = I18n.t("devise.names.not_blank");
|
||||
renderError(checklistNameInput, errMsg, $(form));
|
||||
noErrors = false;
|
||||
} else {
|
||||
// Hide empty checklist
|
||||
$(this).hide();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
$(event.target).blur();
|
||||
return noErrors;
|
||||
}
|
||||
|
||||
// Needed because server-side validation failure clears locations of
|
||||
// files to be uploaded and checklist's items etc. Also user
|
||||
// experience is improved
|
||||
function localStepValidator(event, editMode) {
|
||||
if(!editMode) {
|
||||
// Most td's disappear when editing step and not pressing on
|
||||
// edit tab, so we can't use this function
|
||||
clearBlankTables(event.target.form)
|
||||
}
|
||||
clearBlankFileForms(event.target.form);
|
||||
var checklistsValid = checklistsValidator(event, editMode);
|
||||
var nameValid = nameValidator(event);
|
||||
|
||||
var noErrors = checklistsValid && nameValid;
|
||||
if(noErrors) {
|
||||
// Validations passed, so animate spinner for possible file
|
||||
// uploading
|
||||
animateSpinner();
|
||||
}
|
||||
return noErrors;
|
||||
}
|
||||
|
||||
function S3StepValidator(event, editMode) {
|
||||
if(localStepValidator(event, editMode)) {
|
||||
startFileUpload(event, event.target);
|
||||
}
|
||||
}
|
||||
|
||||
// Expand all steps
|
||||
|
|
@ -675,8 +731,9 @@ function startFileUpload(ev, btn) {
|
|||
var inputPos = 0;
|
||||
var inputPointer = 0;
|
||||
|
||||
animateSpinner();
|
||||
$form.clear_form_errors();
|
||||
clearBlankElements(form);
|
||||
clearBlankFileForms(form);
|
||||
|
||||
function processFile () {
|
||||
var fileInput = fileInputs.get(inputPos);
|
||||
|
|
@ -723,18 +780,19 @@ function startFileUpload(ev, btn) {
|
|||
ev.preventDefault();
|
||||
}
|
||||
|
||||
// Clear empty fields in steps
|
||||
function clearBlankElements(form) {
|
||||
// Remove empty checklist items
|
||||
$(form).find(".checklist-item-text").each(function () {
|
||||
if ($(this).val() === ""){
|
||||
// Remove empty file forms in step
|
||||
function clearBlankFileForms(form) {
|
||||
$(form).find("input[type='file']").each(function () {
|
||||
if (!this.files[0]) {
|
||||
$(this).closest("fieldset").remove();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Remove empty file forms
|
||||
$(form).find("input[type='file']").each(function () {
|
||||
if (!this.files[0]) {
|
||||
// Remove empty tables in step
|
||||
function clearBlankTables(form) {
|
||||
$(form).find(".nested_step_tables").each(function () {
|
||||
if (!$(this).find("td:not(:empty)").length) {
|
||||
$(this).closest("fieldset").remove();
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ $.fn.render_form_errors_input_group = function(model_name, errors) {
|
|||
$.fn.render_form_errors_no_clear = function(model_name, errors, input_group) {
|
||||
var form = $(this);
|
||||
|
||||
var firstErr = true;
|
||||
$.each(errors, function(field, messages) {
|
||||
input = $(_.filter(form.find('input, select, textarea'), function(el) {
|
||||
var name = $(el).attr('name');
|
||||
|
|
@ -33,10 +34,22 @@ $.fn.render_form_errors_no_clear = function(model_name, errors, input_group) {
|
|||
} else {
|
||||
input.parent().append(error_text);
|
||||
}
|
||||
|
||||
if(firstErr) {
|
||||
// Focus and scroll to the first error
|
||||
input.focus();
|
||||
firstErr = false;
|
||||
$('html, body').animate({
|
||||
scrollTop: input.closest(".form-group").offset().top
|
||||
- ($(".navbar-fixed-top").outerHeight(true)
|
||||
+ $(".navbar-secondary").outerHeight(true))
|
||||
}, 2000);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
$.fn.clear_form_errors = function() {
|
||||
$(this).find('.nav.nav-tabs li').removeClass('has-error');
|
||||
$(this).find('.form-group').removeClass('has-error');
|
||||
$(this).find('span.help-block').remove();
|
||||
};
|
||||
|
|
@ -79,10 +92,39 @@ $.fn.add_upload_file_size_check = function(callback) {
|
|||
}
|
||||
};
|
||||
|
||||
// Show error message and mark error element and, if present, mark
|
||||
// and show the tab where the error occured.
|
||||
// NOTE: Similar to $.fn.render_form_errors, except here we process
|
||||
// one error at a time, which is not read from the form but is
|
||||
// specified manually.
|
||||
function renderError(nameInput, errMsg, form) {
|
||||
var errMsgSpan = nameInput.next(".help-block");
|
||||
if(!errMsgSpan.length) {
|
||||
nameInput.after("<span class='help-block'>" + errMsg + "</span>");
|
||||
nameInput.closest(".form-group").addClass("has-error");
|
||||
} else {
|
||||
errMsgSpan.html(errMsg);
|
||||
}
|
||||
tabsPropagateErrorClass($(form));
|
||||
|
||||
// Focus and scroll to the error if it is the first (most upper) one
|
||||
if($(form).find(".form-group.has-error").length === 1) {
|
||||
nameInput.focus();
|
||||
$('html, body').animate({
|
||||
scrollTop: nameInput.closest(".form-group").offset().top
|
||||
- ($(".navbar-fixed-top").outerHeight(true)
|
||||
+ $(".navbar-secondary").outerHeight(true))
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
// If any of tabs has errors, add has-error class to
|
||||
// parent tab navigation link
|
||||
function tabsPropagateErrorClass(parent) {
|
||||
var contents = parent.find("div.tab-pane");
|
||||
if(contents.length) {
|
||||
_.each(contents, function(tab) {
|
||||
var $tab = $(tab);
|
||||
var errorFields = $tab.find(".has-error");
|
||||
|
|
@ -96,3 +138,4 @@ function tabsPropagateErrorClass(parent) {
|
|||
});
|
||||
$(".nav-tabs .has-error:first > a", parent).tab("show");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
8
app/assets/stylesheets/extend/bootstrap.scss
vendored
8
app/assets/stylesheets/extend/bootstrap.scss
vendored
|
|
@ -1,6 +1,7 @@
|
|||
|
||||
/* Extending Bootstrap */
|
||||
|
||||
@import "colors";
|
||||
|
||||
/* navbar avatar image */
|
||||
.navbar-nav .avatar {
|
||||
border-radius: 30px;
|
||||
|
|
@ -11,6 +12,7 @@
|
|||
top: 5px;
|
||||
}
|
||||
|
||||
.bootstrap-tagsinput > .label {
|
||||
line-height: 2.3;
|
||||
/* Active tab with error should retain error color if clicked on again */
|
||||
.nav-tabs > li.active.has-error > a {
|
||||
color: $color-apple-blossom;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@
|
|||
<% end %>
|
||||
<hr>
|
||||
<% if direct_upload %>
|
||||
<%= f.submit t("result_assets.edit.update"), class: 'btn btn-primary', onclick: 'animateSpinner(); startFileUpload(event, this);' %>
|
||||
<%= f.submit t("result_assets.edit.update"), class: 'btn btn-primary', onclick: 'startFileUpload(event, this);' %>
|
||||
<% else %>
|
||||
<%= f.submit t("result_assets.edit.update"), class: 'btn btn-primary', onclick: 'animateSpinner();' %>
|
||||
<% end %>
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
<%= ff.file_field :file %>
|
||||
<% end %>
|
||||
<% if direct_upload %>
|
||||
<%= f.submit t("result_assets.new.create"), class: 'btn btn-primary', onclick: 'animateSpinner(); startFileUpload(event, this);' %>
|
||||
<%= f.submit t("result_assets.new.create"), class: 'btn btn-primary', onclick: 'startFileUpload(event, this);' %>
|
||||
<% else %>
|
||||
<%= f.submit t("result_assets.new.create"), class: 'btn btn-primary', onclick: 'animateSpinner();' %>
|
||||
<% end %>
|
||||
|
|
|
|||
|
|
@ -5,9 +5,9 @@
|
|||
<%= render partial: "empty_step.html.erb", locals: {step: @step, f: f} %>
|
||||
<hr>
|
||||
<% if direct_upload %>
|
||||
<%= f.submit t("protocols.steps.edit.edit_step"), class: 'btn btn-primary', onclick: 'animateSpinner(); startFileUpload(event, this);' %>
|
||||
<%= f.submit t("protocols.steps.edit.edit_step"), class: 'btn btn-primary', onclick: 'S3StepValidator(event, true);' %>
|
||||
<% else %>
|
||||
<%= f.submit t("protocols.steps.edit.edit_step"), class: 'btn btn-primary', onclick: 'animateSpinner(); stepValidator(event);' %>
|
||||
<%= f.submit t("protocols.steps.edit.edit_step"), class: 'btn btn-primary', onclick: 'localStepValidator(event, true);' %>
|
||||
<% end %>
|
||||
<a type="button" data-action="cancel-edit" class="btn btn-default" href="<%= step_path(id: @step, format: :json) %>" data-remote="true">
|
||||
<%= t("general.cancel")%>
|
||||
|
|
|
|||
|
|
@ -12,6 +12,8 @@
|
|||
<% if ff.object.file.exists? %>
|
||||
<% if !(ff.object.file.content_type =~ /^image/).nil? %>
|
||||
<%= image_tag ff.object.file.url(:medium) %>
|
||||
<br>
|
||||
<%= ff.object.file_file_name %>
|
||||
<% else %>
|
||||
<%= ff.object.file_file_name %>
|
||||
<% end %>
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@
|
|||
</div>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<%= ff.text_field :name, label: t("protocols.steps.new.checklist_name"), autofocus: true, placeholder: t("protocols.steps.new.checklist_name_placeholder") %>
|
||||
<%= ff.text_field :name, label: t("protocols.steps.new.checklist_name"), class: "checklist_name", autofocus: true, placeholder: t("protocols.steps.new.checklist_name_placeholder") %>
|
||||
<%= ff.label t("protocols.steps.new.checklist_items") %>
|
||||
<ul>
|
||||
<%= ff.nested_fields_for :checklist_items, ordered_checklist_items(ff.object) do |chkItems| %>
|
||||
|
|
|
|||
|
|
@ -5,9 +5,9 @@
|
|||
<%= render partial: "empty_step.html.erb", locals: {step: @step, f: f} %>
|
||||
<hr>
|
||||
<% if direct_upload %>
|
||||
<%= f.submit t("protocols.steps.new.add_step"), class: 'btn btn-primary', onclick: 'animateSpinner(); startFileUpload(event, this);' %>
|
||||
<%= f.submit t("protocols.steps.new.add_step"), class: 'btn btn-primary', onclick: 'S3StepValidator(event, false);' %>
|
||||
<% else %>
|
||||
<%= f.submit t("protocols.steps.new.add_step"), id: "create-step", class: 'btn btn-primary', onclick: 'animateSpinner(); stepValidator(event);' %>
|
||||
<%= f.submit t("protocols.steps.new.add_step"), id: "create-step", class: 'btn btn-primary', onclick: 'localStepValidator(event, false);' %>
|
||||
<% end %>
|
||||
<button type="button" data-action="cancel-new" class="btn btn-default">
|
||||
<%= t("general.cancel")%>
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ en:
|
|||
title: "Forgot your password?"
|
||||
submit: "Send me reset password instructions"
|
||||
names:
|
||||
length_too_short: "Name can't be blank"
|
||||
not_blank: "Name can't be blank"
|
||||
length_too_long: "Name is too long (maximum is %{max_length} characters)"
|
||||
registrations:
|
||||
password_changed: "Password successfully updated."
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue