mirror of
https://github.com/scinote-eln/scinote-web.git
synced 2025-12-18 22:58:53 +08:00
Multiple files upload handling and user experience improved. Spinner now also prevents user from accidentally leaving page.
This commit is contained in:
parent
951cf67b3d
commit
c87ba5b45c
10 changed files with 188 additions and 126 deletions
|
|
@ -81,7 +81,7 @@ function animateLoading(start){
|
||||||
* Shows spinner if start is true or not given, hides it if false.
|
* Shows spinner if start is true or not given, hides it if false.
|
||||||
* Optional parameter options for spin.js options.
|
* Optional parameter options for spin.js options.
|
||||||
*/
|
*/
|
||||||
function animateSpinner(el, start, options) {
|
function animateSpinner(el, start, options, msg) {
|
||||||
// If overlaying the whole page,
|
// If overlaying the whole page,
|
||||||
// put the spinner in the middle of the page
|
// put the spinner in the middle of the page
|
||||||
var overlayPage = false;
|
var overlayPage = false;
|
||||||
|
|
@ -114,6 +114,10 @@ function animateSpinner(el, start, options) {
|
||||||
$(".loading-overlay").remove();
|
$(".loading-overlay").remove();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
busy = start;
|
||||||
|
if (busy && !_.isUndefined(msg)) {
|
||||||
|
busyMsg = msg;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|
@ -163,3 +167,18 @@ $(document).ready(function(){
|
||||||
truncateLongString( $(this), 30);
|
truncateLongString( $(this), 30);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Prevents user from accidentally leaving page when
|
||||||
|
* server is busy and notifies him with a message.
|
||||||
|
*/
|
||||||
|
var busy = false;
|
||||||
|
var busyMsg = I18n.t("general.busy");
|
||||||
|
window.onbeforeunload = function () {
|
||||||
|
if (busy) {
|
||||||
|
var currentMsg = busyMsg;
|
||||||
|
// Reset to default message
|
||||||
|
busyMsg = I18n.t("general.busy");
|
||||||
|
return currentMsg;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,16 @@
|
||||||
(function (exports) {
|
(function (exports) {
|
||||||
|
|
||||||
|
var styleOptionRe = /(\d+)x(\d+)/i;
|
||||||
|
|
||||||
|
function parseStyleOption(option) {
|
||||||
|
var m = option.match(styleOptionRe);
|
||||||
|
|
||||||
|
return {
|
||||||
|
width: m && m[1] || 150,
|
||||||
|
height: m && m[2] || 150
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// Edits (size, quality, parameters) image file for S3 server uploading
|
// Edits (size, quality, parameters) image file for S3 server uploading
|
||||||
function generateThumbnail(origFile, type, max_width, max_height, cb) {
|
function generateThumbnail(origFile, type, max_width, max_height, cb) {
|
||||||
var img = new Image;
|
var img = new Image;
|
||||||
|
|
@ -21,7 +32,7 @@
|
||||||
size = this.width;
|
size = this.width;
|
||||||
offsetY = (this.height - this.width) / 2;
|
offsetY = (this.height - this.width) / 2;
|
||||||
}
|
}
|
||||||
if(type === "image/jpeg") {
|
if (type === "image/jpeg") {
|
||||||
type = "image/jpg";
|
type = "image/jpg";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -35,10 +46,8 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
// This server checks if files are OK (correct file type, presence,
|
// This server checks if files are OK (correct file type, presence,
|
||||||
// size and spoofing) and generates posts for S3 server file uploading
|
// size and spoofing) and only then generates posts for S3 server
|
||||||
// (each post for different size of the same file)
|
// file uploading (each post for different size of the same file)
|
||||||
// We do this synchronically, because we need to verify all files
|
|
||||||
// before uploading them
|
|
||||||
function fetchUploadSignature(file, signUrl, cb) {
|
function fetchUploadSignature(file, signUrl, cb) {
|
||||||
var csrfParam = $("meta[name=csrf-param]").attr("content");
|
var csrfParam = $("meta[name=csrf-param]").attr("content");
|
||||||
var csrfToken = $("meta[name=csrf-token]").attr("content");
|
var csrfToken = $("meta[name=csrf-token]").attr("content");
|
||||||
|
|
@ -50,7 +59,6 @@
|
||||||
url : signUrl,
|
url : signUrl,
|
||||||
type : 'POST',
|
type : 'POST',
|
||||||
data : formData,
|
data : formData,
|
||||||
async : false,
|
|
||||||
processData: false,
|
processData: false,
|
||||||
contentType: false,
|
contentType: false,
|
||||||
complete : function(xhr) {
|
complete : function(xhr) {
|
||||||
|
|
@ -65,7 +73,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
// Upload file to S3 server
|
// Upload file to S3 server
|
||||||
function uploadData(postData, cb) {
|
function uploadFile(postData, cb) {
|
||||||
var xhr = new XMLHttpRequest;
|
var xhr = new XMLHttpRequest;
|
||||||
var fd = new FormData();
|
var fd = new FormData();
|
||||||
var fields = postData.fields;
|
var fields = postData.fields;
|
||||||
|
|
@ -80,94 +88,126 @@
|
||||||
if (xhr.readyState === 4) { // complete
|
if (xhr.readyState === 4) { // complete
|
||||||
cb();
|
cb();
|
||||||
} else if (xhr.readyState == 0) { // connection error
|
} else if (xhr.readyState == 0) { // connection error
|
||||||
cb(I18n.t("errors.upload"));
|
cb(I18n.t("general.file.upload_failure"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
xhr.open("POST", url);
|
xhr.open("POST", url);
|
||||||
xhr.send(fd);
|
xhr.send(fd);
|
||||||
}
|
}
|
||||||
|
|
||||||
var styleOptionRe = /(\d+)x(\d+)/i;
|
// For each file proccesses its posts and uploads them
|
||||||
|
// If one post fails, the user is allowed to leave page,
|
||||||
|
// but other files are still being uplaoded because of
|
||||||
|
// asynchronous behaviour, so errors for other files may
|
||||||
|
// can still show afterwards
|
||||||
|
// TODO On S3 server uplaod error the other files that
|
||||||
|
// were already asynchronously uploaded remain, but
|
||||||
|
// should be deleted - this should generally not happen,
|
||||||
|
// because all files should fail to upload in such cases
|
||||||
|
// (connection error)
|
||||||
|
function uploadFiles(ev, fileInputs, datas, cb) {
|
||||||
|
var noErrors = true;
|
||||||
|
$.each(datas, function(fileIndex, data) {
|
||||||
|
var fileInput = fileInputs.get(fileIndex);
|
||||||
|
var file = fileInput.files[0];
|
||||||
|
|
||||||
function parseStyleOption(option) {
|
function processPost(error) {
|
||||||
var m = option.match(styleOptionRe);
|
// File upload error handling
|
||||||
|
if (error) {
|
||||||
|
renderFormError(ev, fileInput, error);
|
||||||
|
noErrors = false;
|
||||||
|
animateSpinner(null, false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
var postData = posts[postIndex];
|
||||||
width: m && m[1] || 150,
|
if (!postData) {
|
||||||
height: m && m[2] || 150
|
if (fileIndex === datas.length-1 && noErrors) {
|
||||||
};
|
// After successful file processing and uploading
|
||||||
|
$.each(datas, function(fileIndex, data) {
|
||||||
|
// Use file input to pass file info on submit
|
||||||
|
var fileInput = fileInputs.get(fileIndex);
|
||||||
|
cb(fileInput, data.asset_id);
|
||||||
|
});
|
||||||
|
animateSpinner(null, false);
|
||||||
|
$(ev.target.form).submit();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
postData.fileName = file.name;
|
||||||
|
postIndex++;
|
||||||
|
|
||||||
|
if (postData.style_option) {
|
||||||
|
// Picture file
|
||||||
|
var styleSize = parseStyleOption(postData.style_option);
|
||||||
|
generateThumbnail(file, postData.mime_type, styleSize.width,
|
||||||
|
styleSize.height, function (blob) {
|
||||||
|
|
||||||
|
postData.file = blob;
|
||||||
|
uploadFile(postData, processPost);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Other file
|
||||||
|
postData.file = file;
|
||||||
|
uploadFile(postData, processPost);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var posts = data.posts;
|
||||||
|
var postIndex = 0;
|
||||||
|
processPost();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validates files on this server and uploads them to S3 server
|
// Validates files on server and uploads them to S3 server
|
||||||
|
//
|
||||||
|
// First we validate files on server and generate post requests (fetchUploadSignature),
|
||||||
|
// if OK the post requests are used to uplaod files asyncronously to S3 (uploadFiles),
|
||||||
|
// and if successful the form is submitted, otherwise no file is saved
|
||||||
exports.directUpload = function (ev, fileInputs, signUrl, cb) {
|
exports.directUpload = function (ev, fileInputs, signUrl, cb) {
|
||||||
var noErrors = true;
|
var noErrors = true;
|
||||||
var inputPointer = 0;
|
var inputsPosts = []
|
||||||
animateSpinner();
|
|
||||||
|
|
||||||
function processFile () {
|
function processFile(fileIndex) {
|
||||||
var fileInput = fileInputs.get(inputPointer);
|
var fileInput = fileInputs.get(fileIndex);
|
||||||
if (!fileInput || !fileInput.files[0]) {
|
if (!fileInput || !fileInput.files[0]) {
|
||||||
|
// After file processing
|
||||||
|
if(fileIndex !== 0) {
|
||||||
|
if(noErrors) {
|
||||||
|
uploadFiles(ev, fileInputs, inputsPosts, cb);
|
||||||
|
} else {
|
||||||
|
animateSpinner(null, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (fileIndex === 0) {
|
||||||
|
// Before file processing and uploading
|
||||||
|
animateSpinner(null, true, undefined, I18n.t("general.file.uploading"));
|
||||||
|
ev.preventDefault();
|
||||||
|
ev.stopPropagation();
|
||||||
|
}
|
||||||
|
|
||||||
var file = fileInput.files[0];
|
var file = fileInput.files[0];
|
||||||
inputPointer++;
|
|
||||||
|
|
||||||
fetchUploadSignature(file, signUrl, function (data) {
|
fetchUploadSignature(file, signUrl, function (data) {
|
||||||
|
|
||||||
function processError(errMsgs) {
|
|
||||||
renderFormError(ev, fileInput, errMsgs);
|
|
||||||
noErrors = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
function processPost(error) {
|
|
||||||
// File post error handling
|
|
||||||
if (error) {
|
|
||||||
processError(error);
|
|
||||||
}
|
|
||||||
|
|
||||||
var postData = posts[postPosition];
|
|
||||||
if (!postData) {
|
|
||||||
animateSpinner(null, false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
postData.fileName = file.name;
|
|
||||||
postPosition += 1;
|
|
||||||
|
|
||||||
if (postData.style_option) {
|
|
||||||
var styleSize = parseStyleOption(postData.style_option);
|
|
||||||
generateThumbnail(file, postData.mime_type, styleSize.width,
|
|
||||||
styleSize.height, function (blob) {
|
|
||||||
|
|
||||||
postData.file = blob;
|
|
||||||
uploadData(postData, processPost);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
postData.file = file;
|
|
||||||
uploadData(postData, processPost);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// File signature error handling
|
// File signature error handling
|
||||||
if (_.isUndefined(data)) {
|
if (_.isUndefined(data)) {
|
||||||
processError(I18n.t("errors.upload"));
|
renderFormError(ev, fileInput, I18n.t("general.file.upload_failure"));
|
||||||
|
noErrors = false;
|
||||||
}
|
}
|
||||||
if (data.status === "error") {
|
else if (data.status === "error") {
|
||||||
processError(jsonToValuesArray(data.errors));
|
renderFormError(ev, fileInput, jsonToValuesArray(data.errors));
|
||||||
|
noErrors = false;
|
||||||
|
} else {
|
||||||
|
inputsPosts.push(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
processFile();
|
processFile(fileIndex+1);
|
||||||
if(noErrors) {
|
|
||||||
// Use file input to pass file info on submit
|
|
||||||
cb(fileInput, data.asset_id);
|
|
||||||
|
|
||||||
var posts = data.posts;
|
|
||||||
var postPosition = 0;
|
|
||||||
processPost();
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
processFile();
|
processFile(0);
|
||||||
};
|
};
|
||||||
|
|
||||||
}(this));
|
}(this));
|
||||||
|
|
|
||||||
|
|
@ -326,7 +326,7 @@ function processResult(ev, resultTypeEnum, forS3) {
|
||||||
startFileUpload(ev, ev.target);
|
startFileUpload(ev, ev.target);
|
||||||
} else {
|
} else {
|
||||||
// Local file uploading
|
// Local file uploading
|
||||||
animateSpinner();
|
animateSpinner(null, true, undefined, I18n.t("general.file.uploading"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
|
||||||
|
|
@ -360,13 +360,16 @@ function initEditableHandsOnTable(root) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize comment form.
|
// Initialize comment form.
|
||||||
function initStepCommentForm($el) {
|
function initStepCommentForm(ev, $el) {
|
||||||
|
|
||||||
var $form = $el.find("ul form");
|
var $form = $el.find("ul form");
|
||||||
|
|
||||||
|
var $commentInput = $form.find("#comment_message");
|
||||||
|
$form.onSubmitValidator(textValidator, $commentInput);
|
||||||
|
|
||||||
$(".help-block", $form).addClass("hide");
|
$(".help-block", $form).addClass("hide");
|
||||||
|
|
||||||
$form.on("ajax:send", function (data) {
|
$form
|
||||||
|
.on("ajax:send", function (data) {
|
||||||
$("#comment_message", $form).attr("readonly", true);
|
$("#comment_message", $form).attr("readonly", true);
|
||||||
})
|
})
|
||||||
.on("ajax:success", function (e, data) {
|
.on("ajax:success", function (e, data) {
|
||||||
|
|
@ -450,7 +453,7 @@ function initStepCommentTabAjax() {
|
||||||
var parentNode = $this.parents("ul").parent();
|
var parentNode = $this.parents("ul").parent();
|
||||||
|
|
||||||
target.html(data.html);
|
target.html(data.html);
|
||||||
initStepCommentForm(parentNode);
|
initStepCommentForm(e, parentNode);
|
||||||
initStepCommentsLink(parentNode);
|
initStepCommentsLink(parentNode);
|
||||||
|
|
||||||
parentNode.find(".active").removeClass("active");
|
parentNode.find(".active").removeClass("active");
|
||||||
|
|
@ -598,7 +601,7 @@ function processStep(ev, editMode, forS3) {
|
||||||
startFileUpload(ev, ev.target);
|
startFileUpload(ev, ev.target);
|
||||||
} else {
|
} else {
|
||||||
// Local file uploading
|
// Local file uploading
|
||||||
animateSpinner();
|
animateSpinner(null, true, undefined, I18n.t("general.file.uploading"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,19 @@
|
||||||
// Form validators. They'll find, render and focus error/s and
|
// Form validators. They'll find, render and focus error/s and
|
||||||
// prevent form submission.
|
// prevent form submission.
|
||||||
|
|
||||||
|
// Calls specified validator along with the arguments on form submition
|
||||||
|
$.fn.onSubmitValidator = function(validatorCb) {
|
||||||
|
var params = Array.prototype.slice.call(arguments, 1);
|
||||||
|
var $form = $(this);
|
||||||
|
if ($form.length) {
|
||||||
|
$form.submit(function (ev) {
|
||||||
|
$form.clear_form_errors();
|
||||||
|
params.unshift(ev);
|
||||||
|
validatorCb.apply(this, params);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
function textValidator(ev, textInput, canBeBlank = false, limitLength = true) {
|
function textValidator(ev, textInput, canBeBlank = false, limitLength = true) {
|
||||||
var nameTooShort = $(textInput).val().length === 0;
|
var nameTooShort = $(textInput).val().length === 0;
|
||||||
var nameTooLong = $(textInput).val().length > 50;
|
var nameTooLong = $(textInput).val().length > 50;
|
||||||
|
|
@ -55,16 +68,10 @@ function checklistsValidator(ev, checklists, editMode) {
|
||||||
return noErrors;
|
return noErrors;
|
||||||
}
|
}
|
||||||
|
|
||||||
$.fn.files_validator = function(fileTypeEnum) {
|
var FileTypeEnum = Object.freeze({
|
||||||
var $form = $(this);
|
FILE: 0,
|
||||||
if ($form.length) {
|
AVATAR: 1
|
||||||
$form.submit(function (ev) {
|
});
|
||||||
$form.clear_form_errors();
|
|
||||||
var $fileInputs = $form.find("input[type=file]");
|
|
||||||
filesValidator(ev, $fileInputs, fileTypeEnum);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
function filesValidator(ev, fileInputs, fileTypeEnum) {
|
function filesValidator(ev, fileInputs, fileTypeEnum) {
|
||||||
var filesValid = true;
|
var filesValid = true;
|
||||||
|
|
@ -89,11 +96,6 @@ function filesPresentValidator(ev, fileInputs) {
|
||||||
return filesPresentValid;
|
return filesPresentValid;
|
||||||
}
|
}
|
||||||
|
|
||||||
var FileTypeEnum = Object.freeze({
|
|
||||||
FILE: 0,
|
|
||||||
AVATAR: 1
|
|
||||||
});
|
|
||||||
|
|
||||||
function filesSizeValidator(ev, fileInputs, fileTypeEnum) {
|
function filesSizeValidator(ev, fileInputs, fileTypeEnum) {
|
||||||
|
|
||||||
function getFileTooBigError(file) {
|
function getFileTooBigError(file) {
|
||||||
|
|
@ -132,4 +134,4 @@ function filesSizeValidator(ev, fileInputs, fileTypeEnum) {
|
||||||
// whether enough organization space is free
|
// whether enough organization space is free
|
||||||
function enaughSpaceValidator(ev, fileInputs) {
|
function enaughSpaceValidator(ev, fileInputs) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -74,7 +74,7 @@ function processFile(ev, forS3) {
|
||||||
startFileUpload(ev, ev.target);
|
startFileUpload(ev, ev.target);
|
||||||
} else {
|
} else {
|
||||||
// Local file uploading
|
// Local file uploading
|
||||||
animateSpinner();
|
animateSpinner(null, true, undefined, I18n.t("general.file.uploading"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,8 @@ class AssetsController < ApplicationController
|
||||||
before_action :load_vars, except: [:signature]
|
before_action :load_vars, except: [:signature]
|
||||||
before_action :check_read_permission, except: [:signature, :file_present]
|
before_action :check_read_permission, except: [:signature, :file_present]
|
||||||
|
|
||||||
|
# Validates asset and then generates S3 upload posts, because
|
||||||
|
# otherwise untracked files could be uploaded to S3
|
||||||
def signature
|
def signature
|
||||||
respond_to do |format|
|
respond_to do |format|
|
||||||
format.json {
|
format.json {
|
||||||
|
|
@ -19,7 +21,7 @@ class AssetsController < ApplicationController
|
||||||
validationAsset = Asset.new(asset_params)
|
validationAsset = Asset.new(asset_params)
|
||||||
end
|
end
|
||||||
|
|
||||||
if not validationAsset.valid?
|
if validationAsset.errors.any?
|
||||||
render json: {
|
render json: {
|
||||||
status: 'error',
|
status: 'error',
|
||||||
errors: validationAsset.errors
|
errors: validationAsset.errors
|
||||||
|
|
|
||||||
|
|
@ -37,13 +37,8 @@ class StepsController < ApplicationController
|
||||||
if step_assets.size > 0
|
if step_assets.size > 0
|
||||||
step_assets[:assets_attributes].each do |i, data|
|
step_assets[:assets_attributes].each do |i, data|
|
||||||
# Ignore destroy requests on create
|
# Ignore destroy requests on create
|
||||||
if data[:_destroy].nil?
|
if data[:_destroy].nil? and data[:id].present?
|
||||||
if data[:id].present?
|
asset = Asset.find_by_id(data[:id])
|
||||||
asset = Asset.find_by_id(data[:id])
|
|
||||||
else
|
|
||||||
# For validation purposses if no JS
|
|
||||||
asset = Asset.new(data)
|
|
||||||
end
|
|
||||||
asset.created_by = current_user
|
asset.created_by = current_user
|
||||||
asset.last_modified_by = current_user
|
asset.last_modified_by = current_user
|
||||||
@step.assets << asset
|
@step.assets << asset
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,8 @@ class Users::RegistrationsController < Devise::RegistrationsController
|
||||||
redirect_to user.avatar.url(style.to_sym), status: 307
|
redirect_to user.avatar.url(style.to_sym), status: 307
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Validates asset and then generates S3 upload posts, because
|
||||||
|
# otherwise untracked files could be uploaded to S3
|
||||||
def signature
|
def signature
|
||||||
respond_to do |format|
|
respond_to do |format|
|
||||||
format.json {
|
format.json {
|
||||||
|
|
|
||||||
|
|
@ -1397,29 +1397,6 @@ en:
|
||||||
head_title: "Edit protocol"
|
head_title: "Edit protocol"
|
||||||
no_keywords: "No keywords"
|
no_keywords: "No keywords"
|
||||||
|
|
||||||
general:
|
|
||||||
update: "Update"
|
|
||||||
edit: "Edit"
|
|
||||||
cancel: "Cancel"
|
|
||||||
close: "Close"
|
|
||||||
check_all: "Check all"
|
|
||||||
no_comments: "No comments!"
|
|
||||||
more_comments: "More comments"
|
|
||||||
comment_placeholder: "Your Message"
|
|
||||||
module:
|
|
||||||
one: "task"
|
|
||||||
other: "tasks"
|
|
||||||
public: "public"
|
|
||||||
private: "private"
|
|
||||||
search: "Search"
|
|
||||||
file:
|
|
||||||
loading: "%{fileName} is loading..."
|
|
||||||
size_exceeded: "File size must be less than %{file_size} MB."
|
|
||||||
blank: "You didn't select any file"
|
|
||||||
text:
|
|
||||||
not_blank: "can't be blank"
|
|
||||||
length_too_long: "is too long (maximum is %{max_length} characters)"
|
|
||||||
|
|
||||||
mailer:
|
mailer:
|
||||||
invitation_to_organization:
|
invitation_to_organization:
|
||||||
subject: "You have been invited to team"
|
subject: "You have been invited to team"
|
||||||
|
|
@ -1469,8 +1446,30 @@ en:
|
||||||
# This section contains general words that can be used in any parts of
|
# This section contains general words that can be used in any parts of
|
||||||
# application.
|
# application.
|
||||||
|
|
||||||
errors:
|
general:
|
||||||
upload: "Upload error. Try again or contact the administrator."
|
update: "Update"
|
||||||
|
edit: "Edit"
|
||||||
|
cancel: "Cancel"
|
||||||
|
close: "Close"
|
||||||
|
check_all: "Check all"
|
||||||
|
no_comments: "No comments!"
|
||||||
|
more_comments: "More comments"
|
||||||
|
comment_placeholder: "Your Message"
|
||||||
|
module:
|
||||||
|
one: "task"
|
||||||
|
other: "tasks"
|
||||||
|
public: "public"
|
||||||
|
private: "private"
|
||||||
|
search: "Search"
|
||||||
|
file:
|
||||||
|
size_exceeded: "File size must be less than %{file_size} MB."
|
||||||
|
blank: "You didn't select any file"
|
||||||
|
uploading: "If you leave this page, the file(s) that are currently uploading will not be saved! Are you sure you want to continue?"
|
||||||
|
upload_failure: "Upload connection error. Try again or contact the administrator."
|
||||||
|
text:
|
||||||
|
not_blank: "can't be blank"
|
||||||
|
length_too_long: "is too long (maximum is %{max_length} characters)"
|
||||||
|
busy: "The server is still processing your request. If you leave this page, the changes will be lost! Are you sure you want to continue?"
|
||||||
|
|
||||||
Add: "Add"
|
Add: "Add"
|
||||||
Asset: "File"
|
Asset: "File"
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue