mirror of
https://github.com/scinote-eln/scinote-web.git
synced 2025-11-10 16:31:22 +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 origAssetId = assetInput ? assetInput.value : null;
|
||||||
var url = '/asset_signature.json';
|
var url = '/asset_signature.json';
|
||||||
|
|
||||||
|
animateSpinner();
|
||||||
$form.clear_form_errors();
|
$form.clear_form_errors();
|
||||||
|
|
||||||
directUpload(form, origAssetId, url, function (assetId) {
|
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 nameValidator(event) {
|
||||||
function stepValidator( event ) {
|
var form = $(event.target.form);
|
||||||
var nameTooShort = $( "#step_name" ).val().length === 0;
|
var nameTooShort = $( "#step_name" ).val().length === 0;
|
||||||
var nameTooLong = $( "#step_name" ).val().length > 50;
|
var nameTooLong = $( "#step_name" ).val().length > 50;
|
||||||
var errMsg;
|
var errMsg;
|
||||||
if (nameTooShort) {
|
if (nameTooShort) {
|
||||||
errMsg = I18n.t("devise.names.length_too_short");
|
errMsg = I18n.t("devise.names.not_blank");
|
||||||
animateSpinner(null,false);
|
|
||||||
} else if (nameTooLong) {
|
} else if (nameTooLong) {
|
||||||
errMsg = I18n.t("devise.names.length_too_long", { max_length: 50 });
|
errMsg = I18n.t("devise.names.length_too_long", { max_length: 50 });
|
||||||
animateSpinner(null,false);
|
animateSpinner(null,false);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!_.isUndefined(errMsg)) {
|
var hasErrors = !_.isUndefined(errMsg);
|
||||||
if(!$("#step_name_error_msg").length) {
|
if (hasErrors) {
|
||||||
$("<span id='step_name_error_msg' class='help-block'>" + errMsg + "</span>").insertAfter("#step_name");
|
renderError($("#step_name"), errMsg, form);
|
||||||
$(".form-group:has(> #step_name)").addClass("has-error");
|
|
||||||
$("#new-step-main-tab").addClass("has-error");
|
|
||||||
} else {
|
|
||||||
$("#step_name_error_msg").html(errMsg);
|
|
||||||
}
|
}
|
||||||
animateSpinner(null,false);
|
return !hasErrors;
|
||||||
$("#new-step-main-tab a").click();
|
|
||||||
event.preventDefault();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
// Expand all steps
|
||||||
|
|
@ -675,8 +731,9 @@ function startFileUpload(ev, btn) {
|
||||||
var inputPos = 0;
|
var inputPos = 0;
|
||||||
var inputPointer = 0;
|
var inputPointer = 0;
|
||||||
|
|
||||||
|
animateSpinner();
|
||||||
$form.clear_form_errors();
|
$form.clear_form_errors();
|
||||||
clearBlankElements(form);
|
clearBlankFileForms(form);
|
||||||
|
|
||||||
function processFile () {
|
function processFile () {
|
||||||
var fileInput = fileInputs.get(inputPos);
|
var fileInput = fileInputs.get(inputPos);
|
||||||
|
|
@ -723,18 +780,19 @@ function startFileUpload(ev, btn) {
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clear empty fields in steps
|
// Remove empty file forms in step
|
||||||
function clearBlankElements(form) {
|
function clearBlankFileForms(form) {
|
||||||
// Remove empty checklist items
|
$(form).find("input[type='file']").each(function () {
|
||||||
$(form).find(".checklist-item-text").each(function () {
|
if (!this.files[0]) {
|
||||||
if ($(this).val() === ""){
|
|
||||||
$(this).closest("fieldset").remove();
|
$(this).closest("fieldset").remove();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Remove empty file forms
|
// Remove empty tables in step
|
||||||
$(form).find("input[type='file']").each(function () {
|
function clearBlankTables(form) {
|
||||||
if (!this.files[0]) {
|
$(form).find(".nested_step_tables").each(function () {
|
||||||
|
if (!$(this).find("td:not(:empty)").length) {
|
||||||
$(this).closest("fieldset").remove();
|
$(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) {
|
$.fn.render_form_errors_no_clear = function(model_name, errors, input_group) {
|
||||||
var form = $(this);
|
var form = $(this);
|
||||||
|
|
||||||
|
var firstErr = true;
|
||||||
$.each(errors, function(field, messages) {
|
$.each(errors, function(field, messages) {
|
||||||
input = $(_.filter(form.find('input, select, textarea'), function(el) {
|
input = $(_.filter(form.find('input, select, textarea'), function(el) {
|
||||||
var name = $(el).attr('name');
|
var name = $(el).attr('name');
|
||||||
|
|
@ -33,10 +34,22 @@ $.fn.render_form_errors_no_clear = function(model_name, errors, input_group) {
|
||||||
} else {
|
} else {
|
||||||
input.parent().append(error_text);
|
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() {
|
$.fn.clear_form_errors = function() {
|
||||||
|
$(this).find('.nav.nav-tabs li').removeClass('has-error');
|
||||||
$(this).find('.form-group').removeClass('has-error');
|
$(this).find('.form-group').removeClass('has-error');
|
||||||
$(this).find('span.help-block').remove();
|
$(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
|
// If any of tabs has errors, add has-error class to
|
||||||
// parent tab navigation link
|
// parent tab navigation link
|
||||||
function tabsPropagateErrorClass(parent) {
|
function tabsPropagateErrorClass(parent) {
|
||||||
var contents = parent.find("div.tab-pane");
|
var contents = parent.find("div.tab-pane");
|
||||||
|
if(contents.length) {
|
||||||
_.each(contents, function(tab) {
|
_.each(contents, function(tab) {
|
||||||
var $tab = $(tab);
|
var $tab = $(tab);
|
||||||
var errorFields = $tab.find(".has-error");
|
var errorFields = $tab.find(".has-error");
|
||||||
|
|
@ -96,3 +138,4 @@ function tabsPropagateErrorClass(parent) {
|
||||||
});
|
});
|
||||||
$(".nav-tabs .has-error:first > a", parent).tab("show");
|
$(".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 */
|
/* Extending Bootstrap */
|
||||||
|
|
||||||
|
@import "colors";
|
||||||
|
|
||||||
/* navbar avatar image */
|
/* navbar avatar image */
|
||||||
.navbar-nav .avatar {
|
.navbar-nav .avatar {
|
||||||
border-radius: 30px;
|
border-radius: 30px;
|
||||||
|
|
@ -11,6 +12,7 @@
|
||||||
top: 5px;
|
top: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.bootstrap-tagsinput > .label {
|
/* Active tab with error should retain error color if clicked on again */
|
||||||
line-height: 2.3;
|
.nav-tabs > li.active.has-error > a {
|
||||||
|
color: $color-apple-blossom;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@
|
||||||
<% end %>
|
<% end %>
|
||||||
<hr>
|
<hr>
|
||||||
<% if direct_upload %>
|
<% 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 %>
|
<% else %>
|
||||||
<%= f.submit t("result_assets.edit.update"), class: 'btn btn-primary', onclick: 'animateSpinner();' %>
|
<%= f.submit t("result_assets.edit.update"), class: 'btn btn-primary', onclick: 'animateSpinner();' %>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@
|
||||||
<%= ff.file_field :file %>
|
<%= ff.file_field :file %>
|
||||||
<% end %>
|
<% end %>
|
||||||
<% if direct_upload %>
|
<% 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 %>
|
<% else %>
|
||||||
<%= f.submit t("result_assets.new.create"), class: 'btn btn-primary', onclick: 'animateSpinner();' %>
|
<%= f.submit t("result_assets.new.create"), class: 'btn btn-primary', onclick: 'animateSpinner();' %>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
|
||||||
|
|
@ -5,9 +5,9 @@
|
||||||
<%= render partial: "empty_step.html.erb", locals: {step: @step, f: f} %>
|
<%= render partial: "empty_step.html.erb", locals: {step: @step, f: f} %>
|
||||||
<hr>
|
<hr>
|
||||||
<% if direct_upload %>
|
<% 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 %>
|
<% 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 %>
|
<% end %>
|
||||||
<a type="button" data-action="cancel-edit" class="btn btn-default" href="<%= step_path(id: @step, format: :json) %>" data-remote="true">
|
<a type="button" data-action="cancel-edit" class="btn btn-default" href="<%= step_path(id: @step, format: :json) %>" data-remote="true">
|
||||||
<%= t("general.cancel")%>
|
<%= t("general.cancel")%>
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,8 @@
|
||||||
<% if ff.object.file.exists? %>
|
<% if ff.object.file.exists? %>
|
||||||
<% if !(ff.object.file.content_type =~ /^image/).nil? %>
|
<% if !(ff.object.file.content_type =~ /^image/).nil? %>
|
||||||
<%= image_tag ff.object.file.url(:medium) %>
|
<%= image_tag ff.object.file.url(:medium) %>
|
||||||
|
<br>
|
||||||
|
<%= ff.object.file_file_name %>
|
||||||
<% else %>
|
<% else %>
|
||||||
<%= ff.object.file_file_name %>
|
<%= ff.object.file_file_name %>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="panel-body">
|
<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") %>
|
<%= ff.label t("protocols.steps.new.checklist_items") %>
|
||||||
<ul>
|
<ul>
|
||||||
<%= ff.nested_fields_for :checklist_items, ordered_checklist_items(ff.object) do |chkItems| %>
|
<%= 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} %>
|
<%= render partial: "empty_step.html.erb", locals: {step: @step, f: f} %>
|
||||||
<hr>
|
<hr>
|
||||||
<% if direct_upload %>
|
<% 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 %>
|
<% 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 %>
|
<% end %>
|
||||||
<button type="button" data-action="cancel-new" class="btn btn-default">
|
<button type="button" data-action="cancel-new" class="btn btn-default">
|
||||||
<%= t("general.cancel")%>
|
<%= t("general.cancel")%>
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@ en:
|
||||||
title: "Forgot your password?"
|
title: "Forgot your password?"
|
||||||
submit: "Send me reset password instructions"
|
submit: "Send me reset password instructions"
|
||||||
names:
|
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)"
|
length_too_long: "Name is too long (maximum is %{max_length} characters)"
|
||||||
registrations:
|
registrations:
|
||||||
password_changed: "Password successfully updated."
|
password_changed: "Password successfully updated."
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue