mirror of
https://github.com/scinote-eln/scinote-web.git
synced 2024-09-20 14:45:56 +08:00
Merge branch 'smart-annotations'
Conflicts: app/assets/javascripts/comments.js.erb app/assets/javascripts/protocols/index.js app/assets/javascripts/protocols/steps.js.erb app/assets/javascripts/samples/sample_datatable.js.erb app/controllers/my_module_comments_controller.rb app/controllers/project_comments_controller.rb app/controllers/result_comments_controller.rb app/controllers/step_comments_controller.rb app/datatables/sample_datatable.rb app/views/my_module_comments/_comment.html.erb app/views/project_comments/_comment.html.erb app/views/projects/show/_experiment.html.erb app/views/reports/elements/_experiment_element.html.erb app/views/reports/elements/_result_comments_element.html.erb app/views/reports/elements/_result_text_element.html.erb app/views/reports/elements/_step_checklist_element.html.erb app/views/reports/elements/_step_comments_element.html.erb app/views/reports/elements/_step_element.html.erb app/views/result_comments/_comment.html.erb app/views/results/_result_text.html.erb app/views/step_comments/_comment.html.erb app/views/steps/_step.html.erb db/schema.rb
This commit is contained in:
commit
53699193a2
6
Gemfile
6
Gemfile
|
@ -64,9 +64,9 @@ gem 'aws-sdk-v1'
|
|||
gem 'delayed_job_active_record'
|
||||
gem 'devise-async'
|
||||
gem 'ruby-graphviz', '~> 1.2' # Graphviz for rails
|
||||
gem 'quill-rails', # Rich text editor
|
||||
git: 'https://github.com/biosistemika/quill-rails.git',
|
||||
ref: 'e765c04'
|
||||
gem 'tinymce-rails' # Rich text editor
|
||||
|
||||
gem 'base62' # Used for smart annotations
|
||||
|
||||
group :development, :test do
|
||||
gem 'byebug'
|
||||
|
|
|
@ -12,6 +12,8 @@
|
|||
//= require jquery-ui/draggable
|
||||
//= require jquery-ui/droppable
|
||||
//= require jquery.ui.touch-punch.min
|
||||
//= require jquery.caret.min
|
||||
//= require jquery.atwho.min
|
||||
//= require hammer
|
||||
//= require introjs
|
||||
//= require js.cookie
|
||||
|
@ -25,14 +27,16 @@
|
|||
//= require bootstrap-checkbox.min
|
||||
//= require typeahead.bundle.min
|
||||
//= require nested_form_fields
|
||||
//= require highlight.pack
|
||||
//= require tinymce-jquery
|
||||
//= require_directory ./sitewide
|
||||
//= require datatables
|
||||
//= require dataTables.noSearchHidden
|
||||
//= require bootstrap-select
|
||||
//= require underscore
|
||||
//= require i18n.js
|
||||
//= require i18n/translations
|
||||
//= require turbolinks
|
||||
//= require quill
|
||||
|
||||
|
||||
// Initialize links for submitting forms. This is useful for submitting
|
||||
|
@ -227,3 +231,12 @@ var HelperModule = (function(){
|
|||
|
||||
return helpers;
|
||||
})();
|
||||
|
||||
// initialize code markup in rich text fields
|
||||
(function() {
|
||||
$(document).ready(function() {
|
||||
$('[class^=language]').each(function(i, block) {
|
||||
hljs.highlightBlock(block);
|
||||
});
|
||||
});
|
||||
})();
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
//= require protocols/import_export/import
|
||||
//= require comments
|
||||
//= require datatables
|
||||
|
||||
// Currently selected row in "load from protocol" modal
|
||||
var selectedRow = null;
|
||||
|
|
|
@ -181,8 +181,8 @@ function processResult(ev, resultTypeEnum, editMode) {
|
|||
var $nameInput = $form.find("#result_name");
|
||||
var nameValid = textValidator(ev, $nameInput, 0,
|
||||
<%= Constants::NAME_MAX_LENGTH %>);
|
||||
var $textInput = $form.find("#result_result_text_attributes_text");
|
||||
textValidator(ev, $textInput, 1, <%= Constants::TEXT_MAX_LENGTH %>);
|
||||
var $textInput = TinyMCE.getContent();
|
||||
textValidator(ev, $textInput, 1, <%= Constants::TEXT_MAX_LENGTH %>, false, true);
|
||||
break;
|
||||
case ResultTypeEnum.COMMENT:
|
||||
var $commentInput = $form.find("#comment_message");
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
//= require protocols/import_export/import
|
||||
//= require datatables
|
||||
|
||||
// Global variables
|
||||
var rowsSelected = [];
|
||||
|
|
|
@ -8,7 +8,6 @@
|
|||
function applyCheckboxCallBack() {
|
||||
$("[data-action='check-item']").on('click', function(e){
|
||||
var checkboxitem = $(this).find("input");
|
||||
|
||||
var checked = checkboxitem.is(":checked");
|
||||
$.ajax({
|
||||
url: checkboxitem.data("link-url"),
|
||||
|
@ -79,8 +78,9 @@ function applyCancelCallBack() {
|
|||
|
||||
setTimeout(function() {
|
||||
initStepsComments();
|
||||
openLinksInNewTab();
|
||||
initPreviewModal();
|
||||
SmartAnnotation.preventPropagation('.atwho-user-popover');
|
||||
TinyMCE.destroyAll();
|
||||
}, 1000);
|
||||
|
||||
})
|
||||
|
@ -107,6 +107,7 @@ function applyEditCallBack() {
|
|||
animateSpinner(null, false);
|
||||
initPreviewModal();
|
||||
|
||||
TinyMCE.init();
|
||||
$("#new-step-checklists fieldset.nested_step_checklists ul").each(function () {
|
||||
enableCheckboxSorting(this);
|
||||
});
|
||||
|
@ -114,7 +115,6 @@ function applyEditCallBack() {
|
|||
$("#new-step-main-tab a").on("shown.bs.tab", function() {
|
||||
$("#step_name").focus();
|
||||
});
|
||||
openLinksInNewTab();
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -185,8 +185,12 @@ function formCallback($form) {
|
|||
|
||||
setTimeout(function() {
|
||||
initStepsComments();
|
||||
<<<<<<< HEAD
|
||||
openLinksInNewTab();
|
||||
animateSpinner(null, false);
|
||||
=======
|
||||
SmartAnnotation.preventPropagation('.atwho-user-popover');
|
||||
>>>>>>> smart-annotations
|
||||
}, 1000);
|
||||
return true;
|
||||
});
|
||||
|
@ -210,6 +214,8 @@ function formEditAjax($form) {
|
|||
toggleButtons(true);
|
||||
initPreviewModal();
|
||||
|
||||
TinyMCE.destroyAll();
|
||||
SmartAnnotation.preventPropagation('.atwho-user-popover');
|
||||
// Show the edited step
|
||||
$new_step.find(".panel-collapse:first").addClass("collapse in");
|
||||
|
||||
|
@ -226,6 +232,9 @@ function formEditAjax($form) {
|
|||
initEditableHandsOnTable($form);
|
||||
applyCancelCallBack();
|
||||
|
||||
TinyMCE.refresh();
|
||||
SmartAnnotation.preventPropagation('.atwho-user-popover');
|
||||
|
||||
//Rerender tables
|
||||
$form.find("[data-role='step-hot-table']").each(function() {
|
||||
renderTable($(this));
|
||||
|
@ -250,6 +259,10 @@ function formNewAjax($form) {
|
|||
expandStep($new_step);
|
||||
toggleButtons(true);
|
||||
|
||||
TinyMCE.init();
|
||||
TinyMCE.highlight();
|
||||
SmartAnnotation.preventPropagation('.atwho-user-popover');
|
||||
|
||||
//Rerender tables
|
||||
$new_step.find("div.step-result-hot-table").each(function() {
|
||||
$(this).handsontable("render");
|
||||
|
@ -268,7 +281,12 @@ function formNewAjax($form) {
|
|||
formCallback($form);
|
||||
formNewAjax($form);
|
||||
applyCancelOnNew();
|
||||
<<<<<<< HEAD
|
||||
animateSpinner(null, false);
|
||||
=======
|
||||
TinyMCE.destroyAll();
|
||||
SmartAnnotation.preventPropagation('.atwho-user-popover');
|
||||
>>>>>>> smart-annotations
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -393,7 +411,7 @@ function initCallBacks() {
|
|||
applyMoveStepCallBack();
|
||||
applyCollapseLinkCallBack();
|
||||
initDeleteStep();
|
||||
initHighlightjs();
|
||||
TinyMCE.highlight();
|
||||
}
|
||||
|
||||
/*
|
||||
|
@ -479,7 +497,6 @@ function initializeCheckboxSorting() {
|
|||
// inserted into DOM.
|
||||
setTimeout(function () {
|
||||
var list = el.parent().find("fieldset.nested_step_checklists:last ul");
|
||||
|
||||
enableCheckboxSorting(list.get(0));
|
||||
});
|
||||
});
|
||||
|
@ -497,6 +514,7 @@ $("[data-action='new-step']").on("ajax:success", function(e, data) {
|
|||
applyCancelOnNew();
|
||||
toggleButtons(false);
|
||||
initializeCheckboxSorting();
|
||||
TinyMCE.init();
|
||||
|
||||
$("#step_name").focus();
|
||||
$("#new-step-main-tab a").on("shown.bs.tab", function() {
|
||||
|
@ -560,14 +578,6 @@ function renderTable(table) {
|
|||
}
|
||||
}
|
||||
|
||||
function initHighlightjs() {
|
||||
if(hljs){
|
||||
$('.ql-editor pre').each(function(i, block) {
|
||||
hljs.highlightBlock(block);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function initStepsComments() {
|
||||
Comments.initialize();
|
||||
Comments.initCommentOptions("ul.content-comments");
|
||||
|
@ -582,8 +592,9 @@ $(document).ready(function() {
|
|||
expandAllSteps();
|
||||
setupAssetsLoading();
|
||||
initStepsComments();
|
||||
initHighlightjs();
|
||||
initPreviewModal();
|
||||
TinyMCE.highlight();
|
||||
SmartAnnotation.preventPropagation('.atwho-user-popover');
|
||||
|
||||
$(function () {
|
||||
|
||||
|
|
|
@ -11,6 +11,7 @@ $("#new-result-text").on("ajax:success", function(e, data) {
|
|||
toggleResultEditButtons(true);
|
||||
});
|
||||
|
||||
TinyMCE.init();
|
||||
toggleResultEditButtons(false);
|
||||
|
||||
$("#result_name").focus();
|
||||
|
@ -39,6 +40,7 @@ function applyEditResultTextCallback() {
|
|||
toggleResultEditButtons(true);
|
||||
});
|
||||
|
||||
TinyMCE.init();
|
||||
toggleResultEditButtons(false);
|
||||
|
||||
$("#result_name").focus();
|
||||
|
@ -61,12 +63,12 @@ function formAjaxResultText($form) {
|
|||
applyCollapseLinkCallBack();
|
||||
toggleResultEditButtons(true);
|
||||
expandResult(newResult);
|
||||
initHighlightjs();
|
||||
TinyMCE.destroyAll();
|
||||
});
|
||||
$form.on("ajax:error", function(e, xhr, status, error) {
|
||||
var data = xhr.responseJSON;
|
||||
$form.renderFormErrors("result", data);
|
||||
initHighlightjs();
|
||||
TinyMCE.highlight();
|
||||
if (data["result_text.text"]) {
|
||||
var $el = $form.find("textarea[name=result\\[result_text_attributes\\]\\[text\\]]");
|
||||
|
||||
|
@ -76,15 +78,7 @@ function formAjaxResultText($form) {
|
|||
});
|
||||
}
|
||||
|
||||
|
||||
function initHighlightjs() {
|
||||
if(hljs) {
|
||||
$('.ql-editor pre').each(function(i, block) {
|
||||
hljs.highlightBlock(block);
|
||||
});
|
||||
}
|
||||
}
|
||||
$(document).ready(function() {
|
||||
initHighlightjs();
|
||||
TinyMCE.highlight();
|
||||
});
|
||||
applyEditResultTextCallback();
|
||||
|
|
|
@ -73,9 +73,8 @@ function dataTableInit() {
|
|||
}, {
|
||||
targets: 2,
|
||||
render: function(data, type, row) {
|
||||
return "<a href='#' data-href='" + row.sampleUpdateUrl + "'" +
|
||||
"class='sample_info' data-toggle='modal'" +
|
||||
"data-target='#modal-info-sample'>" + data + '</a>';
|
||||
return "<a href='" + row.sampleInfoUrl + "'" +
|
||||
"class='sample-info-link'>" + data + '</a>';
|
||||
}
|
||||
}],
|
||||
rowCallback: function(row, data) {
|
||||
|
@ -111,7 +110,7 @@ function dataTableInit() {
|
|||
},
|
||||
preDrawCallback: function() {
|
||||
animateSpinner(this);
|
||||
$('.sample_info').off('click');
|
||||
$('.sample-info-link').off('click');
|
||||
},
|
||||
stateLoadCallback: function(settings) {
|
||||
// Send an Ajax request to the server to get the data. Note that
|
||||
|
@ -179,8 +178,11 @@ function dataTableInit() {
|
|||
});
|
||||
|
||||
// Handle click on table cells with checkboxes
|
||||
$('#samples').on('click', 'tbody td, thead th:first-child', function() {
|
||||
$(this).parent().find('input[type="checkbox"]').trigger('click');
|
||||
$('#samples').on('click', 'tbody td, thead th:first-child', function(e) {
|
||||
if (!$(e.target).is('.sample-info-link')) {
|
||||
// Skip if clicking on samples info link
|
||||
$(this).parent().find('input[type="checkbox"]').trigger('click');
|
||||
}
|
||||
});
|
||||
|
||||
// Handle clicks on checkbox
|
||||
|
@ -233,6 +235,9 @@ function dataTableInit() {
|
|||
table.on('draw', function() {
|
||||
updateDataTableSelectAllCtrl(table);
|
||||
sampleInfoListener();
|
||||
|
||||
// Prevent sample row toggling when selecting user smart annotation link
|
||||
SmartAnnotation.preventPropagation('.atwho-user-popover');
|
||||
});
|
||||
|
||||
table.on('column-reorder', function() {
|
||||
|
@ -411,7 +416,7 @@ function onClickEdit() {
|
|||
saveAction = "update";
|
||||
|
||||
$.ajax({
|
||||
url: rowData["sampleInfoUrl"],
|
||||
url: rowData["sampleEditUrl"],
|
||||
type: "GET",
|
||||
dataType: "json",
|
||||
success: function (data) {
|
||||
|
@ -457,6 +462,12 @@ function onClickEdit() {
|
|||
}
|
||||
});
|
||||
|
||||
// initialize smart annotation
|
||||
_.each($('[data-object="custom_fields"]'), function(el) {
|
||||
if(_.isUndefined($(el).data('atwho'))) {
|
||||
SmartAnnotation.init(el);
|
||||
}
|
||||
});
|
||||
// Adjust columns width in table header
|
||||
adjustTableHeader();
|
||||
},
|
||||
|
@ -742,6 +753,12 @@ function onClickAddSample() {
|
|||
$("select[name=sample_group_id]").selectpicker();
|
||||
$("select[name=sample_type_id]").selectpicker();
|
||||
|
||||
// initialize smart annotation
|
||||
_.each($('[data-object="custom_fields"]'), function(el) {
|
||||
if(_.isUndefined($(el).data('atwho'))) {
|
||||
SmartAnnotation.init(el);
|
||||
}
|
||||
});
|
||||
// Adjust columns width in table header
|
||||
adjustTableHeader();
|
||||
},
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
//= require datatables
|
||||
|
||||
// Create import samples ajax
|
||||
$("#modal-import-samples").on("show.bs.modal", function(event) {
|
||||
formGroup = $(this).find(".form-group");
|
||||
|
|
450
app/assets/javascripts/sitewide/atwho_res.js.erb
Normal file
450
app/assets/javascripts/sitewide/atwho_res.js.erb
Normal file
|
@ -0,0 +1,450 @@
|
|||
var SmartAnnotation = (function() {
|
||||
'use strict';
|
||||
|
||||
// utilities
|
||||
var Util = (function() {
|
||||
// helper method that binds show/hidden action
|
||||
function showHideBinding() {
|
||||
$.each(['show', 'hide'], function (i, ev) {
|
||||
var el = $.fn[ev];
|
||||
$.fn[ev] = function () {
|
||||
this.trigger(ev);
|
||||
return el.apply(this, arguments);
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
var publicApi = {
|
||||
showHideBinding: showHideBinding
|
||||
};
|
||||
|
||||
return publicApi;
|
||||
})();
|
||||
|
||||
// stop the user annotation popover on click propagation
|
||||
function atwhoStopPropagation(element) {
|
||||
$(element).on('click', function(e) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
});
|
||||
}
|
||||
|
||||
function setAtWho(field) {
|
||||
var FilterTypeEnum = Object.freeze({
|
||||
USER: {tag: "users",
|
||||
dataUrl: $(document.body).attr('data-atwho-users-url')},
|
||||
TASK: {tag: "tsk",
|
||||
dataUrl: $(document.body).attr('data-atwho-task-url')},
|
||||
PROJECT: {tag: "prj",
|
||||
dataUrl: $(document.body).attr('data-atwho-project-url')},
|
||||
EXPERIMENT: {tag: "exp",
|
||||
dataUrl: $(document.body).attr('data-atwho-experiment-url')},
|
||||
SAMPLE: {tag: "sam",
|
||||
dataUrl: $(document.body).attr('data-atwho-sample-url')},
|
||||
MENU: {tag: "menu",
|
||||
dataUrl: $(document.body).attr('data-atwho-menu-items')}
|
||||
});
|
||||
var prevAt,
|
||||
// Default selected filter when using '#'
|
||||
DEFAULT_SEARCH_FILTER = FilterTypeEnum.SAMPLE,
|
||||
atWhoUpdating = false;
|
||||
|
||||
// helper methods for AtWho callback
|
||||
function _templateEval(_tpl, map) {
|
||||
var res;
|
||||
try {
|
||||
if (map.no_results) {
|
||||
res = noResultsTemplate();
|
||||
} else {
|
||||
res = generateTemplate(map);
|
||||
}
|
||||
} catch (_error) {
|
||||
res = '';
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
function _matchHighlighter(li, query, filterType) {
|
||||
var $li, re;
|
||||
|
||||
function highlight(el, sel, re) {
|
||||
var prevVal, newVal;
|
||||
prevVal = el.find(sel).html();
|
||||
newVal = prevVal.replace(re, '<strong>$&</strong>');
|
||||
el.find(sel).html(newVal);
|
||||
}
|
||||
|
||||
if (!query || $(li).data('no-results')) {
|
||||
return li;
|
||||
}
|
||||
|
||||
$li = $(li);
|
||||
re = new RegExp(query, 'gi');
|
||||
// search_filter is not passed for the user
|
||||
if(filterType) {
|
||||
highlight($li, '[data-val=name]', re);
|
||||
} else {
|
||||
highlight($li, '[data-val=full-name]', re);
|
||||
highlight($li, '[data-val=email]', re);
|
||||
}
|
||||
|
||||
return $li[0].outerHTML
|
||||
}
|
||||
|
||||
function _generateInputTag(value, li) {
|
||||
var res = '';
|
||||
res += '[#' + li.attr('data-name');
|
||||
res += '~' + li.attr('data-type');
|
||||
res += '~' + li.attr('data-id') + ']';
|
||||
return res;
|
||||
}
|
||||
|
||||
// initialise dropdown dismiss button
|
||||
function initDismissButton($currentAtWho) {
|
||||
$currentAtWho.find('.dismiss').off('click')
|
||||
.on('click', function() {
|
||||
$(field).atwho('destroy');
|
||||
init();
|
||||
});
|
||||
}
|
||||
|
||||
// Initialize or update dropdown header buttons
|
||||
function updateHeaderButtons(query, filterTypeTag) {
|
||||
var $currentAtWho = $('.atwho-view[style]');
|
||||
initDismissButton($currentAtWho);
|
||||
|
||||
// Update the selected filter button when changing smart annotation type
|
||||
$currentAtWho.find('[data-filter]')
|
||||
.removeClass('btn-primary')
|
||||
.addClass('btn-default');
|
||||
$currentAtWho.find('[data-filter="' + filterTypeTag + '"]')
|
||||
.removeClass('btn-default')
|
||||
.addClass('btn-primary');
|
||||
|
||||
// Update the selected filter button when clicking on one of them
|
||||
$currentAtWho.find('[data-filter]').off()
|
||||
.on('click', function(e) {
|
||||
var $selectedBtn = $(this);
|
||||
var $prevBtn = $selectedBtn.closest('.title').children('.btn-primary');
|
||||
$selectedBtn.removeClass('btn-default').addClass('btn-primary');
|
||||
$prevBtn.removeClass('btn-primary').addClass('btn-default');
|
||||
|
||||
// Updates query and dropdown elements; focuses input
|
||||
$(field).click().focus();
|
||||
});
|
||||
}
|
||||
|
||||
// Generates suggestion dropdown filter
|
||||
function generateFilterMenu(active, res_data) {
|
||||
var header = '<div class="atwho-header-res">' +
|
||||
'<div class="title">' +
|
||||
'<button data-filter="prj" class="btn btn-xs ' +
|
||||
(active === 'prj' ? 'btn-primary' : 'btn-default') + '">project#</button>' +
|
||||
'<button data-filter="exp" class="btn btn-xs ' +
|
||||
(active === 'exp' ? 'btn-primary' : 'btn-default') + '">experiment#</button>' +
|
||||
'<button data-filter="tsk" class="btn btn-xs ' +
|
||||
(active === 'tsk' ? 'btn-primary' : 'btn-default') + '">task#</button>' +
|
||||
'<button data-filter="sam" class="btn btn-xs ' +
|
||||
(active === 'sam' ? 'btn-primary' : 'btn-default') + '">sample#</button>' +
|
||||
'</div>' +
|
||||
'<div class="help">' +
|
||||
'<div>' +
|
||||
'<strong><%= I18n.t("atwho.users.navigate_1") %></strong> ' +
|
||||
'<%= I18n.t("atwho.users.navigate_2") %>' +
|
||||
'</div>' +
|
||||
'<div><strong><%= I18n.t("atwho.users.confirm_1") %></strong> ' +
|
||||
'<%= I18n.t("atwho.users.confirm_2") %>' +
|
||||
'</div>' +
|
||||
'<div>' +
|
||||
'<strong><%= I18n.t("atwho.users.dismiss_1") %></strong> ' +
|
||||
'<%= I18n.t("atwho.users.dismiss_2") %>' +
|
||||
'</div>' +
|
||||
'<div class="dismiss">' +
|
||||
'<span class="glyphicon glyphicon-remove"></span>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
|
||||
return header;
|
||||
}
|
||||
|
||||
function noResultsTemplate() {
|
||||
var res = '<div class="atwho-no-results" data-no-results="1">';
|
||||
res += '<span><%= I18n.t("atwho.no_results") %></span>';
|
||||
res += '</div>';
|
||||
return res;
|
||||
}
|
||||
|
||||
// Generates resources list items
|
||||
function generateTemplate(map) {
|
||||
var res = '';
|
||||
res += '<li class="atwho-li atwho-li-res" data-name="' +
|
||||
truncateLongString(map.name,
|
||||
<%= Constants::NAME_TRUNCATION_LENGTH %>) +
|
||||
'" data-id="' + map.id + '" data-type="' +
|
||||
map.type + '">';
|
||||
switch(map.type) {
|
||||
case 'tsk':
|
||||
res += '<span data-type class="res-type">' + map.type + '</span>';
|
||||
break;
|
||||
case 'prj':
|
||||
res += '<span data-type class="res-type">' + map.type + '</span>';
|
||||
break;
|
||||
case 'exp':
|
||||
res += '<span data-type class="res-type">' + map.type + '</span>';
|
||||
break;
|
||||
case 'sam':
|
||||
res += '<span class="glyphicon glyphicon-tint"></span>';
|
||||
break;
|
||||
}
|
||||
|
||||
res += ' ';
|
||||
res += '<span data-val="name" class="res-name">';
|
||||
res += truncateLongString(map.name,
|
||||
<%= Constants::NAME_TRUNCATION_LENGTH %>);
|
||||
res += '</span>';
|
||||
if(map.archived) {
|
||||
res += '<%= I18n.t("atwho.res.archived") %></span>';
|
||||
} else {
|
||||
res += '</span>';
|
||||
}
|
||||
res += ' ';
|
||||
|
||||
switch (map.type) {
|
||||
case 'tsk':
|
||||
|
||||
res += '<span class="res-description">< ' + map.experimentName +
|
||||
' < ' + map.projectName + '</span>';
|
||||
break;
|
||||
case 'exp':
|
||||
res += '<span class="res-description">< ' + map.projectName + '</span>';
|
||||
break;
|
||||
case 'sam':
|
||||
res += '<span class="res-description">' + map.description + '</span>';
|
||||
break;
|
||||
}
|
||||
|
||||
res += '</li>';
|
||||
return res;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hackish wrapper function to make AtWho work when switching between
|
||||
* multiple AtWho instances (e.g. from # to task#).
|
||||
*
|
||||
* Prevents second execution of AtWho update callback, triggered when user
|
||||
* switches to different AtWho instance (e.g. from # to task#), which causes
|
||||
* both of them to be called. In such case, AtWhO modal needs to be
|
||||
* rerendered.
|
||||
*/
|
||||
function atWhoSwitchHack(filterTypeTag, remoteFilterCb) {
|
||||
if(atWhoUpdating || (!$(field).length && _.isUndefined(filterTypeTag))) {
|
||||
setTimeout(function() {
|
||||
$(field).click();
|
||||
}, 100);
|
||||
return;
|
||||
}
|
||||
|
||||
atWhoUpdating = true;
|
||||
setTimeout(function() {
|
||||
remoteFilterCb();
|
||||
atWhoUpdating = false;
|
||||
}, 100);
|
||||
}
|
||||
|
||||
function atWhoSettings(at, defaultFilterType) {
|
||||
return {
|
||||
at: at,
|
||||
callbacks: {
|
||||
remoteFilter: function(query, callback) {
|
||||
var $currentAtWho = $('.atwho-view[style]');
|
||||
var filterTypeTag = $currentAtWho
|
||||
.find('.btn-primary')
|
||||
.data('filter');
|
||||
|
||||
atWhoSwitchHack(filterTypeTag, function() {
|
||||
var filterType;
|
||||
if (_.isUndefined(filterTypeTag)) {
|
||||
// Switched smart annotation type (i.e. changed input)
|
||||
filterType = defaultFilterType;
|
||||
} else {
|
||||
// Switched filtering type (i.e. different filter button
|
||||
// pressed; works also for specific annotation types, e.g.
|
||||
// task#, and coverts to the correct annotation type on confirm)
|
||||
$.each(FilterTypeEnum, function(k, v) {
|
||||
if (v.tag == filterTypeTag) {
|
||||
filterType = FilterTypeEnum[k];
|
||||
return false;
|
||||
}
|
||||
});
|
||||
}
|
||||
if (prevAt != at) {
|
||||
// Switching smart annotation type (i.e. chaned input)
|
||||
|
||||
prevAt = at;
|
||||
filterType = defaultFilterType;
|
||||
// Hide current AtWho
|
||||
$currentAtWho.removeAttr("style");
|
||||
}
|
||||
|
||||
$.getJSON(
|
||||
filterType.dataUrl,
|
||||
{query: query},
|
||||
function(data) {
|
||||
// Updates dropdown
|
||||
if (data.res.length < 1) {
|
||||
callback([{no_results: 1}]);
|
||||
} else {
|
||||
callback(data.res);
|
||||
}
|
||||
|
||||
updateHeaderButtons(query, filterType.tag);
|
||||
}
|
||||
);
|
||||
});
|
||||
},
|
||||
sorter: function(query, items, _searchKey) {
|
||||
// Sorting is already done on server-side
|
||||
return items;
|
||||
},
|
||||
tplEval: function(_tpl, map) {
|
||||
return _templateEval(_tpl, map);
|
||||
},
|
||||
highlighter: function(li, query) {
|
||||
return _matchHighlighter(li, query, true);
|
||||
},
|
||||
beforeInsert: function(value, li) {
|
||||
return _generateInputTag(value, li);
|
||||
}
|
||||
},
|
||||
headerTpl: generateFilterMenu(defaultFilterType),
|
||||
limit: <%= Constants::ATWHO_SEARCH_LIMIT %>,
|
||||
startWithSpace: true,
|
||||
acceptSpaceBar: true,
|
||||
displayTimeout: 120000
|
||||
}
|
||||
}
|
||||
|
||||
function init() {
|
||||
$(field)
|
||||
.atwho({
|
||||
at: '@',
|
||||
callbacks: {
|
||||
remoteFilter: function(query, callback) {
|
||||
$.getJSON(
|
||||
FilterTypeEnum.USER.dataUrl,
|
||||
{query: query},
|
||||
function(data) {
|
||||
if (data.users.length < 1) {
|
||||
callback([{no_results: 1}]);
|
||||
} else {
|
||||
callback(data.users);
|
||||
}
|
||||
initDismissButton();
|
||||
}
|
||||
);
|
||||
},
|
||||
sorter: function(query, items, _searchKey) {
|
||||
// Sorting is already done on server-side
|
||||
return items;
|
||||
},
|
||||
tplEval: function(_tpl, map) {
|
||||
var res;
|
||||
try {
|
||||
if (map.no_results) {
|
||||
res = noResultsTemplate();
|
||||
} else {
|
||||
res = '';
|
||||
res += '<li class="atwho-li atwho-li-user" ';
|
||||
res += 'data-id="' + map.id + '" ';
|
||||
res += 'data-full-name="' + map.full_name + '">';
|
||||
res += '<img src="' + map.img_url + '" class="avatar" />';
|
||||
res += '<span data-val="full-name">';
|
||||
res += map.full_name;
|
||||
res += '</span>';
|
||||
res += '<small>';
|
||||
res += ' ';
|
||||
res += '·';
|
||||
res += ' ';
|
||||
res += '<span data-val="email">';
|
||||
res += map.email;
|
||||
res += '</span>';
|
||||
res += '</small>';
|
||||
res += '</li>';
|
||||
}
|
||||
} catch (_error) {
|
||||
res = '';
|
||||
}
|
||||
return res;
|
||||
},
|
||||
highlighter: function(li, query) {
|
||||
return _matchHighlighter(li, query);
|
||||
},
|
||||
beforeInsert: function(value, li) {
|
||||
var res = '';
|
||||
res += '[@' + li.attr('data-full-name');
|
||||
res += '~' + li.attr('data-id') + ']';
|
||||
return res;
|
||||
}
|
||||
},
|
||||
headerTpl:
|
||||
'<div class="atwho-header-res">' +
|
||||
'<div class="title title-user"><%= I18n.t("atwho.users.title") %></div>' +
|
||||
'<div class="help">' +
|
||||
'<div>' +
|
||||
'<strong><%= I18n.t("atwho.users.navigate_1") %></strong> ' +
|
||||
'<%= I18n.t("atwho.users.navigate_2") %>' +
|
||||
'</div>' +
|
||||
'<div>' +
|
||||
'<strong><%= I18n.t("atwho.users.confirm_1") %></strong> ' +
|
||||
'<%= I18n.t("atwho.users.confirm_2") %>' +
|
||||
'</div>' +
|
||||
'<div>' +
|
||||
'<strong><%= I18n.t("atwho.users.dismiss_1") %></strong> ' +
|
||||
'<%= I18n.t("atwho.users.dismiss_2") %>' +
|
||||
'</div>' +
|
||||
'<div class="dismiss">' +
|
||||
'<span class="glyphicon glyphicon-remove"></span>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'</div>',
|
||||
limit: <%= Constants::ATWHO_SEARCH_LIMIT %>,
|
||||
startsWithSpace: true,
|
||||
acceptSpaceBar: true,
|
||||
displayTimeout: 120000
|
||||
})
|
||||
.atwho(atWhoSettings('#', DEFAULT_SEARCH_FILTER));
|
||||
// .atwho(atWhoSettings('task#', FilterTypeEnum.TASK)) Waiting for better times
|
||||
// .atwho(atWhoSettings('project#', FilterTypeEnum.PROJECT))
|
||||
// .atwho(atWhoSettings('experiment#', FilterTypeEnum.EXPERIMENT))
|
||||
// .atwho(atWhoSettings('sample#', FilterTypeEnum.SAMPLE));
|
||||
}
|
||||
|
||||
return {
|
||||
init: init
|
||||
};
|
||||
}
|
||||
|
||||
function initialize(field) {
|
||||
var atWho = new setAtWho(field);
|
||||
atWho.init();
|
||||
}
|
||||
|
||||
var publicApi = Object.freeze({
|
||||
init: initialize,
|
||||
preventPropagation: atwhoStopPropagation
|
||||
});
|
||||
|
||||
return publicApi;
|
||||
|
||||
})();
|
||||
|
||||
|
||||
// initialize the smart annotations
|
||||
(function initSmartAnnotation() {
|
||||
$(document).on('focus', '[data-atwho-edit]', function() {
|
||||
if(_.isUndefined($(this).data('atwho'))) {
|
||||
SmartAnnotation.init(this);
|
||||
}
|
||||
});
|
||||
})();
|
|
@ -22,13 +22,17 @@ $.fn.onSubmitValidator = function(validatorCb) {
|
|||
* @param {boolean} clearErr Set clearErr to true if this is the only
|
||||
* error that can happen/show.
|
||||
*/
|
||||
function textValidator(ev, textInput, textLimitMin, textLimitMax, clearErr) {
|
||||
function textValidator(ev, textInput, textLimitMin, textLimitMax, clearErr, tinyMCE) {
|
||||
clearErr = _.isUndefined(clearErr) ? false : clearErr;
|
||||
|
||||
var text = $(textInput).val().trim();
|
||||
$(textInput).val(text);
|
||||
var text_from_html = $("<div/>").html(text).text();
|
||||
if (text_from_html.length < text.length) text = text_from_html;
|
||||
if(tinyMCE){
|
||||
var text = textInput.length;
|
||||
} else {
|
||||
var text = $(textInput).val().trim();
|
||||
$(textInput).val(text);
|
||||
var text_from_html = $("<div/>").html(text).text();
|
||||
if (text_from_html.length < text.length) text = text_from_html;
|
||||
}
|
||||
|
||||
var nameTooShort = text.length < textLimitMin;
|
||||
var nameTooLong = text.length > textLimitMax;
|
||||
|
|
|
@ -1,23 +0,0 @@
|
|||
//= require quill
|
||||
|
||||
|
||||
// Globally overwrite links handling in Quill rich text editor
|
||||
var Link = Quill.import('formats/link');
|
||||
Link.sanitize = function(url) {
|
||||
if (url.includes('http:') || url.includes('https:')) {
|
||||
return url;
|
||||
}
|
||||
return 'http://' + url;
|
||||
};
|
||||
|
||||
function openLinksInNewTab() {
|
||||
_.each($('.ql-editor a'), function(el) {
|
||||
if ($(el).attr('target') !== '_blank') {
|
||||
$(el).attr('target', '_blank');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
$(document).ready(function(){
|
||||
openLinksInNewTab();
|
||||
});
|
44
app/assets/javascripts/sitewide/sample_info_modal.js
Normal file
44
app/assets/javascripts/sitewide/sample_info_modal.js
Normal file
|
@ -0,0 +1,44 @@
|
|||
(function() {
|
||||
'use strict';
|
||||
|
||||
$(document).on('click', '.sample-info-link', function(e) {
|
||||
var that = $(this);
|
||||
$.ajax({
|
||||
method: 'GET',
|
||||
url: that.attr('href'),
|
||||
dataType: 'json'
|
||||
}).done(function(xhr, settings, data) {
|
||||
$('body').append($.parseHTML(data.responseJSON.html));
|
||||
$('#modal-info-sample').modal('show', {
|
||||
backdrop: true,
|
||||
keyboard: false
|
||||
}).on('hidden.bs.modal', function() {
|
||||
$(this).find('.modal-body #sample-info-table').DataTable().destroy();
|
||||
$(this).remove();
|
||||
});
|
||||
|
||||
$('#sample-info-table').DataTable({
|
||||
dom: 'RBltpi',
|
||||
stateSave: false,
|
||||
buttons: [],
|
||||
processing: true,
|
||||
colReorder: {
|
||||
fixedColumnsLeft: 1000000 // Disable reordering
|
||||
},
|
||||
columnDefs: [{
|
||||
targets: 0,
|
||||
searchable: false,
|
||||
orderable: false
|
||||
}],
|
||||
fnDrawCallback: function(settings, json) {
|
||||
animateSpinner(this, false);
|
||||
},
|
||||
preDrawCallback: function(settings) {
|
||||
animateSpinner(this);
|
||||
}
|
||||
});
|
||||
});
|
||||
e.preventDefault();
|
||||
return false;
|
||||
});
|
||||
})();
|
99
app/assets/javascripts/sitewide/tiny_mce.js.erb
Normal file
99
app/assets/javascripts/sitewide/tiny_mce.js.erb
Normal file
|
@ -0,0 +1,99 @@
|
|||
var TinyMCE = (function() {
|
||||
'use strict';
|
||||
|
||||
function initHighlightjs() {
|
||||
$('[class*=language]').each(function(i, block) {
|
||||
hljs.highlightBlock(block);
|
||||
});
|
||||
}
|
||||
|
||||
function initHighlightjsIframe(iframe) {
|
||||
$('[class*=language]', iframe).each(function(i, block) {
|
||||
hljs.highlightBlock(block);
|
||||
});
|
||||
}
|
||||
|
||||
// returns a public API for TinyMCE editor
|
||||
return Object.freeze({
|
||||
init : function() {
|
||||
if (typeof tinyMCE != 'undefined') {
|
||||
tinyMCE.init({
|
||||
selector: "textarea.tinymce",
|
||||
toolbar: ["undo redo | insert | styleselect | bold italic | alignleft aligncenter alignright alignjustify | bullist numlist outdent indent | link | forecolor backcolor | codesample"],
|
||||
plugins: "link,advlist,codesample,autolink,lists,charmap,hr,anchor,searchreplace,wordcount,visualblocks,visualchars,insertdatetime,nonbreaking,save,contextmenu,directionality,paste,textcolor,colorpicker,textpattern,imagetools,toc",
|
||||
codesample_languages: [{"text":"R","value":"r"},{"text":"MATLAB","value":"matlab"},{"text":"Python","value":"python"},{"text":"JSON","value":"javascript"},{"text":"HTML/XML","value":"markup"},{"text":"JavaScript","value":"javascript"},{"text":"CSS","value":"css"},{"text":"PHP","value":"php"},{"text":"Ruby","value":"ruby"},{"text":"Java","value":"java"},{"text":"C","value":"c"},{"text":"C#","value":"csharp"},{"text":"C++","value":"cpp"}],
|
||||
removed_menuitems: 'newdocument',
|
||||
elementpath: false,
|
||||
default_link_target: "_blank",
|
||||
style_formats: [
|
||||
{title: 'Headers', items: [
|
||||
{title: 'Header 1', format: 'h1'},
|
||||
{title: 'Header 2', format: 'h2'},
|
||||
{title: 'Header 3', format: 'h3'},
|
||||
{title: 'Header 4', format: 'h4'},
|
||||
{title: 'Header 5', format: 'h5'},
|
||||
{title: 'Header 6', format: 'h6'}
|
||||
]},
|
||||
{title: 'Inline', items: [
|
||||
{title: 'Bold', icon: 'bold', format: 'bold'},
|
||||
{title: 'Italic', icon: 'italic', format: 'italic'},
|
||||
{title: 'Underline', icon: 'underline', format: 'underline'},
|
||||
{title: 'Strikethrough', icon: 'strikethrough', format: 'strikethrough'},
|
||||
{title: 'Superscript', icon: 'superscript', format: 'superscript'},
|
||||
{title: 'Subscript', icon: 'subscript', format: 'subscript'},
|
||||
{title: 'Code', icon: 'code', format: 'code'}
|
||||
]},
|
||||
{title: 'Blocks', items: [
|
||||
{title: 'Paragraph', format: 'p'},
|
||||
{title: 'Blockquote', format: 'blockquote'}
|
||||
]},
|
||||
{title: 'Alignment', items: [
|
||||
{title: 'Left', icon: 'alignleft', format: 'alignleft'},
|
||||
{title: 'Center', icon: 'aligncenter', format: 'aligncenter'},
|
||||
{title: 'Right', icon: 'alignright', format: 'alignright'},
|
||||
{title: 'Justify', icon: 'alignjustify', format: 'alignjustify'}
|
||||
]}
|
||||
],
|
||||
init_instance_callback: function(editor) {
|
||||
SmartAnnotation.init($(editor.contentDocument.activeElement));
|
||||
initHighlightjsIframe($(this.iframeElement).contents());
|
||||
},
|
||||
setup: function(editor) {
|
||||
editor.on('Change', function(e) {
|
||||
if(e.keyCode == 13 && $(editor.contentDocument.activeElement).atwho('isSelecting')) {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
editor.on('NodeChange', function(e) {
|
||||
var node = e.element;
|
||||
var editor = this;
|
||||
setTimeout(function() {
|
||||
if($(node).is('pre') && !editor.isHidden()){
|
||||
initHighlightjsIframe($(editor.iframeElement).contents());
|
||||
}
|
||||
}, 200);
|
||||
|
||||
});
|
||||
},
|
||||
codesample_content_css: '<%= asset_path('highlightjs-github-theme') %>'
|
||||
});
|
||||
}
|
||||
},
|
||||
destroyAll: function() {
|
||||
_.each(tinymce.editors, function(editor) {
|
||||
editor.destroy();
|
||||
initHighlightjs();
|
||||
});
|
||||
},
|
||||
refresh: function() {
|
||||
this.destroyAll();
|
||||
this.init();
|
||||
},
|
||||
getContent: function() {
|
||||
return tinymce.editors[0].getContent();
|
||||
},
|
||||
highlight: initHighlightjs
|
||||
});
|
||||
|
||||
})();
|
|
@ -1,5 +1,3 @@
|
|||
//= require datatables
|
||||
|
||||
var usersDatatable = null;
|
||||
|
||||
// Initialize edit name modal window
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
/*
|
||||
*= require highlightjs-github-theme
|
||||
*= require_self
|
||||
*= require_tree .
|
||||
*= require jquery-ui/draggable
|
||||
|
@ -7,7 +8,6 @@
|
|||
*= require constants
|
||||
*= require introjs
|
||||
*= stub reports_pdf
|
||||
*= require quill.snow
|
||||
*/
|
||||
@import "bootstrap-sprockets";
|
||||
@import "bootstrap";
|
||||
|
|
|
@ -25,6 +25,7 @@ $color-emperor: #555;
|
|||
$color-mine-shaft: #333;
|
||||
$color-nero: #262626;
|
||||
$color-black: #000;
|
||||
$color-cloud: rgba(0, 0, 0, .1);
|
||||
|
||||
// Miscelaneous colors
|
||||
$color-mystic: #eaeff2;
|
||||
|
|
|
@ -854,6 +854,14 @@ a[data-toggle="tooltip"] {
|
|||
|
||||
.dataTables_paginate {
|
||||
float: right;
|
||||
|
||||
.pagination {
|
||||
margin: 2px 0;
|
||||
}
|
||||
}
|
||||
|
||||
.dataTables_info {
|
||||
padding-top: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1719,3 +1727,212 @@ th.custom-field .modal-tooltiptext {
|
|||
.disable-click {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
// AtWho (smart annotations)
|
||||
|
||||
// <Custom atwho style>
|
||||
.atwho-view {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
display: none;
|
||||
margin-top: 18px;
|
||||
background: $color-white;
|
||||
color: $color-black;
|
||||
border: 1px solid $color-emperor;
|
||||
border-radius: 3px;
|
||||
box-shadow: 0 0 5px $color-cloud;
|
||||
min-width: 520px;
|
||||
max-height: 200px;
|
||||
overflow: auto;
|
||||
z-index: 11110 !important;
|
||||
|
||||
small {
|
||||
font-size: smaller;
|
||||
color: $color-emperor;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
strong {
|
||||
color: $color-theme-primary;
|
||||
}
|
||||
|
||||
.cur {
|
||||
background: $color-theme-primary;
|
||||
color: $color-white;
|
||||
|
||||
small {
|
||||
color: $color-white;
|
||||
}
|
||||
|
||||
strong {
|
||||
color: $color-white;
|
||||
font: bold;
|
||||
}
|
||||
}
|
||||
|
||||
ul {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: auto;
|
||||
|
||||
li {
|
||||
display: block;
|
||||
padding: 5px 10px;
|
||||
border-bottom: 1px solid $color-emperor;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
// <End of overrides>
|
||||
|
||||
.atwho-header-res {
|
||||
padding-top: 3px;
|
||||
padding-bottom: 7px;
|
||||
height: 34px;
|
||||
background-color: $color-gallery;
|
||||
border-bottom: 1px solid $color-emperor;
|
||||
clear: both;
|
||||
|
||||
> div {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.title {
|
||||
float: left;
|
||||
margin-left: 15px;
|
||||
|
||||
.btn {
|
||||
border-radius: 4px;
|
||||
margin-right: 5px;
|
||||
padding: 3px;
|
||||
}
|
||||
|
||||
.btn:last-child {
|
||||
margin-right: 20px;
|
||||
}
|
||||
|
||||
.btn-default {
|
||||
background-color: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
.title-user {
|
||||
padding-top: 4px;
|
||||
}
|
||||
|
||||
.help {
|
||||
float: right;
|
||||
padding-top: 4px;
|
||||
|
||||
div {
|
||||
display: inline;
|
||||
margin-right: 15px;
|
||||
font-size: smaller;
|
||||
}
|
||||
|
||||
div strong {
|
||||
color: $color-black;
|
||||
}
|
||||
|
||||
.dismiss {
|
||||
color: $color-emperor;
|
||||
}
|
||||
|
||||
.dismiss:hover {
|
||||
color: $color-black;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.atwho-li-user {
|
||||
|
||||
.avatar {
|
||||
border-radius: 50%;
|
||||
margin-left: 5px;
|
||||
margin-right: 10px;
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.atwho-li-res {
|
||||
|
||||
.glyphicon-tint {
|
||||
margin-left: 5px;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.res-type {
|
||||
border: 1px solid $color-black;
|
||||
border-radius: 4px;
|
||||
font-weight: 600;
|
||||
margin-left: 5px;
|
||||
margin-right: 5px;
|
||||
padding: 0 2px;
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
.res-name {
|
||||
font-weight: 600;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.res-description {
|
||||
color: $color-emperor;
|
||||
font-size: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.sa-type {
|
||||
border: 1px solid $color-black;
|
||||
border-radius: 4px;
|
||||
color: $color-black;
|
||||
font-weight: 600;
|
||||
padding: 0 2px;
|
||||
text-decoration: none;
|
||||
text-transform: capitalize;
|
||||
|
||||
&:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
|
||||
.atwho-user-popover {
|
||||
cursor: pointer;
|
||||
padding-left: 5px;
|
||||
}
|
||||
|
||||
.atwho-user-img-popover {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.atwho-no-results {
|
||||
padding: 5px 10px;
|
||||
}
|
||||
|
||||
.popover {
|
||||
border-radius: 3px;
|
||||
min-width: 450px;
|
||||
padding: 15px 10px;
|
||||
z-index: 9999;
|
||||
|
||||
h5 {
|
||||
font-weight: bold;
|
||||
line-height: 0;
|
||||
}
|
||||
|
||||
.silver {
|
||||
color: $color-silver-chalice;
|
||||
}
|
||||
|
||||
p {
|
||||
max-width: 260px;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
}
|
||||
|
||||
.popover-remove {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
|
107
app/controllers/at_who_controller.rb
Normal file
107
app/controllers/at_who_controller.rb
Normal file
|
@ -0,0 +1,107 @@
|
|||
class AtWhoController < ApplicationController
|
||||
before_action :load_vars
|
||||
before_action :check_users_permissions
|
||||
|
||||
def users
|
||||
# Search users
|
||||
res = @organization
|
||||
.search_users(@query)
|
||||
.select(:id, :full_name, :email)
|
||||
.limit(Constants::ATWHO_SEARCH_LIMIT)
|
||||
.as_json
|
||||
|
||||
# Add avatars, Base62, convert to JSON
|
||||
res.each do |user_obj|
|
||||
user_obj['full_name'] =
|
||||
user_obj['full_name']
|
||||
.truncate(Constants::NAME_TRUNCATION_LENGTH_DROPDOWN)
|
||||
user_obj['id'] = user_obj['id'].base62_encode
|
||||
user_obj['img_url'] = avatar_path(user_obj['id'], :icon_small)
|
||||
end
|
||||
|
||||
respond_to do |format|
|
||||
format.json do
|
||||
render json: {
|
||||
users: res,
|
||||
status: :ok
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def menu_items
|
||||
res = SmartAnnotation.new(current_user, current_organization, @query)
|
||||
|
||||
respond_to do |format|
|
||||
format.json do
|
||||
render json: {
|
||||
prj: res.projects,
|
||||
exp: res.experiments,
|
||||
tsk: res.my_modules,
|
||||
sam: res.samples,
|
||||
status: :ok
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def samples
|
||||
res = SmartAnnotation.new(current_user, current_organization, @query)
|
||||
respond_to do |format|
|
||||
format.json do
|
||||
render json: {
|
||||
res: res.samples,
|
||||
status: :ok
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def projects
|
||||
res = SmartAnnotation.new(current_user, current_organization, @query)
|
||||
respond_to do |format|
|
||||
format.json do
|
||||
render json: {
|
||||
res: res.projects,
|
||||
status: :ok
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def experiments
|
||||
res = SmartAnnotation.new(current_user, current_organization, @query)
|
||||
respond_to do |format|
|
||||
format.json do
|
||||
render json: {
|
||||
res: res.experiments,
|
||||
status: :ok
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def my_modules
|
||||
res = SmartAnnotation.new(current_user, current_organization, @query)
|
||||
respond_to do |format|
|
||||
format.json do
|
||||
render json: {
|
||||
res: res.my_modules,
|
||||
status: :ok
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def load_vars
|
||||
@organization = Organization.find_by_id(params[:id])
|
||||
@query = params[:query]
|
||||
render_404 unless @organization
|
||||
end
|
||||
|
||||
def check_users_permissions
|
||||
render_403 unless can_view_organization_users(@organization)
|
||||
end
|
||||
end
|
|
@ -1,6 +1,7 @@
|
|||
class MyModuleCommentsController < ApplicationController
|
||||
include ActionView::Helpers::TextHelper
|
||||
include InputSanitizeHelper
|
||||
include ApplicationHelper
|
||||
|
||||
before_action :load_vars
|
||||
before_action :check_view_permissions, only: :index
|
||||
|
@ -115,13 +116,15 @@ class MyModuleCommentsController < ApplicationController
|
|||
module: @my_module.name
|
||||
)
|
||||
)
|
||||
render json: {
|
||||
comment: custom_auto_link(
|
||||
simple_format(@comment.message),
|
||||
link: :urls,
|
||||
html: { target: '_blank' }
|
||||
)
|
||||
}, status: :ok
|
||||
message = auto_link(
|
||||
smart_annotation_parser(
|
||||
simple_format(sanitize_input(@comment.message))
|
||||
),
|
||||
link: :urls,
|
||||
sanitize: false,
|
||||
html: { target: '_blank' }
|
||||
).html_safe
|
||||
render json: { comment: message }, status: :ok
|
||||
else
|
||||
render json: { errors: @comment.errors.to_hash(true) },
|
||||
status: :unprocessable_entity
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
class ProjectCommentsController < ApplicationController
|
||||
include ActionView::Helpers::TextHelper
|
||||
include InputSanitizeHelper
|
||||
include ApplicationHelper
|
||||
|
||||
before_action :load_vars
|
||||
before_action :check_view_permissions, only: :index
|
||||
|
@ -112,13 +113,15 @@ class ProjectCommentsController < ApplicationController
|
|||
project: @project.name
|
||||
)
|
||||
)
|
||||
render json: {
|
||||
comment: custom_auto_link(
|
||||
simple_format(@comment.message),
|
||||
link: :urls,
|
||||
html: { target: '_blank' }
|
||||
)
|
||||
}, status: :ok
|
||||
message = auto_link(
|
||||
smart_annotation_parser(
|
||||
simple_format(sanitize_input(@comment.message))
|
||||
),
|
||||
link: :urls,
|
||||
sanitize: false,
|
||||
html: { target: '_blank' }
|
||||
).html_safe
|
||||
render json: { comment: message }, status: :ok
|
||||
else
|
||||
render json: { errors: @comment.errors.to_hash(true) },
|
||||
status: :unprocessable_entity
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
class ResultCommentsController < ApplicationController
|
||||
include ActionView::Helpers::TextHelper
|
||||
include InputSanitizeHelper
|
||||
include ApplicationHelper
|
||||
|
||||
before_action :load_vars
|
||||
|
||||
|
@ -113,13 +114,15 @@ class ResultCommentsController < ApplicationController
|
|||
result: @result.name
|
||||
)
|
||||
)
|
||||
render json: {
|
||||
comment: custom_auto_link(
|
||||
simple_format(@comment.message),
|
||||
link: :urls,
|
||||
html: { target: '_blank' }
|
||||
)
|
||||
}, status: :ok
|
||||
message = auto_link(
|
||||
smart_annotation_parser(
|
||||
simple_format(sanitize_input(@comment.message))
|
||||
),
|
||||
link: :urls,
|
||||
sanitize: false,
|
||||
html: { target: '_blank' }
|
||||
)
|
||||
render json: { comment: message }, status: :ok
|
||||
else
|
||||
render json: { errors: @comment.errors.to_hash(true) },
|
||||
status: :unprocessable_entity
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
class StepCommentsController < ApplicationController
|
||||
include ActionView::Helpers::TextHelper
|
||||
include InputSanitizeHelper
|
||||
include ApplicationHelper
|
||||
|
||||
before_action :load_vars
|
||||
|
||||
|
@ -118,13 +119,15 @@ class StepCommentsController < ApplicationController
|
|||
)
|
||||
)
|
||||
end
|
||||
render json: {
|
||||
comment: custom_auto_link(
|
||||
simple_format(@comment.message),
|
||||
link: :urls,
|
||||
html: { target: '_blank' }
|
||||
)
|
||||
}, status: :ok
|
||||
message = auto_link(
|
||||
smart_annotation_parser(
|
||||
simple_format(sanitize_input(@comment.message))
|
||||
),
|
||||
link: :urls,
|
||||
sanitize: false,
|
||||
html: { target: '_blank' }
|
||||
).html_safe
|
||||
render json: { comment: message }, status: :ok
|
||||
else
|
||||
render json: { errors: @comment.errors.to_hash(true) },
|
||||
status: :unprocessable_entity
|
||||
|
|
|
@ -1,4 +1,7 @@
|
|||
class StepsController < ApplicationController
|
||||
include ActionView::Helpers::TextHelper
|
||||
include ApplicationHelper
|
||||
|
||||
before_action :load_vars, only: [:edit, :update, :destroy, :show]
|
||||
before_action :load_vars_nested, only: [:new, :create]
|
||||
before_action :convert_table_contents_to_utf8, only: [:create, :update]
|
||||
|
@ -241,7 +244,7 @@ class StepsController < ApplicationController
|
|||
message = t(
|
||||
str,
|
||||
user: current_user.full_name,
|
||||
checkbox: chkItem.text,
|
||||
checkbox: smart_annotation_parser(simple_format(chkItem.text)),
|
||||
step: chkItem.checklist.step.position + 1,
|
||||
step_name: chkItem.checklist.step.name,
|
||||
completed: completed_items,
|
||||
|
|
|
@ -4,6 +4,9 @@ class SampleDatatable < AjaxDatatablesRails::Base
|
|||
include ActionView::Helpers::TextHelper
|
||||
include SamplesHelper
|
||||
include InputSanitizeHelper
|
||||
include Rails.application.routes.url_helpers
|
||||
include ActionView::Helpers::UrlHelper
|
||||
include ApplicationHelper
|
||||
|
||||
ASSIGNED_SORT_COL = 'assigned'
|
||||
|
||||
|
@ -113,17 +116,12 @@ class SampleDatatable < AjaxDatatablesRails::Base
|
|||
else
|
||||
escape_input(record.sample_type.name)
|
||||
end,
|
||||
'4': if record.sample_group.nil?
|
||||
"<span class='glyphicon glyphicon-asterisk'></span> " +
|
||||
I18n.t('samples.table.no_group')
|
||||
else
|
||||
"<span class='glyphicon glyphicon-asterisk' "\
|
||||
"style='color: #{escape_input(record.sample_group.color)}'>"\
|
||||
"</span> " + escape_input(record.sample_group.name)
|
||||
end,
|
||||
'4': sample_group_cell(record),
|
||||
'5': I18n.l(record.created_at, format: :full),
|
||||
'6': escape_input(record.user.full_name),
|
||||
'sampleInfoUrl':
|
||||
Rails.application.routes.url_helpers.sample_path(record.id),
|
||||
'sampleEditUrl':
|
||||
Rails.application.routes.url_helpers.edit_sample_path(record.id),
|
||||
'sampleUpdateUrl':
|
||||
Rails.application.routes.url_helpers.sample_path(record.id)
|
||||
|
@ -131,8 +129,15 @@ class SampleDatatable < AjaxDatatablesRails::Base
|
|||
|
||||
# Add custom attributes
|
||||
record.sample_custom_fields.each do |scf|
|
||||
sample[@cf_mappings[scf.custom_field_id]] =
|
||||
custom_auto_link(scf.value, link: :urls, html: { target: '_blank' })
|
||||
sample[@cf_mappings[scf.custom_field_id]] = auto_link(
|
||||
smart_annotation_parser(
|
||||
simple_format(sanitize_input(scf.value)),
|
||||
@organization
|
||||
),
|
||||
link: :urls,
|
||||
sanitize: false,
|
||||
html: { target: '_blank' }
|
||||
).html_safe
|
||||
end
|
||||
sample
|
||||
end
|
||||
|
@ -144,6 +149,17 @@ class SampleDatatable < AjaxDatatablesRails::Base
|
|||
"<span class='circle disabled'> </span>"
|
||||
end
|
||||
|
||||
def sample_group_cell(record)
|
||||
if record.sample_group.nil?
|
||||
"<span class='glyphicon glyphicon-asterisk'></span> " \
|
||||
"#{I18n.t('samples.table.no_group')}"
|
||||
else
|
||||
"<span class='glyphicon glyphicon-asterisk' " \
|
||||
"style='color: #{escape_input(record.sample_group.color)}'></span> " \
|
||||
"#{escape_input(record.sample_group.name)}"
|
||||
end
|
||||
end
|
||||
|
||||
# Query database for records (this will be later paginated and filtered)
|
||||
# after that "data" function will return json
|
||||
def get_raw_records
|
||||
|
|
|
@ -1,4 +1,7 @@
|
|||
module ApplicationHelper
|
||||
include ActionView::Helpers::AssetTagHelper
|
||||
include ActionView::Helpers::UrlHelper
|
||||
|
||||
def module_page?
|
||||
controller_name == 'my_modules'
|
||||
end
|
||||
|
@ -54,4 +57,113 @@ module ApplicationHelper
|
|||
@my_module.nil? &&
|
||||
!@experiment.nil?
|
||||
end
|
||||
|
||||
def smart_annotation_parser(text, organization = nil)
|
||||
new_text = smart_annotation_filter_resources(text)
|
||||
new_text = smart_annotation_filter_users(new_text, organization)
|
||||
new_text
|
||||
end
|
||||
|
||||
# Check if text have smart annotations of resources
|
||||
# and outputs a link to resource
|
||||
def smart_annotation_filter_resources(text)
|
||||
sa_reg = /\[\#(.*?)~(prj|exp|tsk|sam)~([0-9a-zA-Z]+)\]/
|
||||
new_text = text.gsub(sa_reg) do |el|
|
||||
match = el.match(sa_reg)
|
||||
case match[2]
|
||||
when 'prj'
|
||||
project = Project.find_by_id(match[3].base62_decode)
|
||||
next unless project
|
||||
if project.archived?
|
||||
"<span class='sa-type'>#{sanitize(match[2])}</span> " \
|
||||
"#{link_to project.name,
|
||||
projects_archive_path} #{t'atwho.res.archived'}"
|
||||
else
|
||||
"<span class='sa-type'>#{sanitize(match[2])}</span> " \
|
||||
"#{link_to project.name,
|
||||
project_path(project)}"
|
||||
end
|
||||
when 'exp'
|
||||
experiment = Experiment.find_by_id(match[3].base62_decode)
|
||||
next unless experiment
|
||||
if experiment.archived?
|
||||
"<span class='sa-type'>#{sanitize(match[2])}</span> " \
|
||||
"#{link_to experiment.name,
|
||||
experiment_archive_project_path(experiment.project)} " \
|
||||
"#{t'atwho.res.archived'}"
|
||||
else
|
||||
"<span class='sa-type'>#{sanitize(match[2])}</span> " \
|
||||
"#{link_to experiment.name,
|
||||
canvas_experiment_path(experiment)}"
|
||||
end
|
||||
when 'tsk'
|
||||
my_module = MyModule.find_by_id(match[3].base62_decode)
|
||||
next unless my_module
|
||||
if my_module.archived?
|
||||
"<span class='sa-type'>#{sanitize(match[2])}</span> " \
|
||||
"#{link_to my_module.name,
|
||||
module_archive_experiment_path(my_module.experiment)} " \
|
||||
"#{t'atwho.res.archived'}"
|
||||
else
|
||||
"<span class='sa-type'>#{sanitize(match[2])}</span> " \
|
||||
"#{link_to my_module.name,
|
||||
protocols_my_module_path(my_module)}"
|
||||
end
|
||||
when 'sam'
|
||||
sample = Sample.find_by_id(match[3].base62_decode)
|
||||
if sample
|
||||
"<span class='glyphicon glyphicon-tint'></span> " \
|
||||
"#{link_to(sample.name,
|
||||
sample_path(sample.id),
|
||||
class: 'sample-info-link')}"
|
||||
else
|
||||
"<span class='glyphicon glyphicon-tint'></span> " \
|
||||
"#{match[1]} #{t'atwho.res.deleted'}"
|
||||
end
|
||||
end
|
||||
end
|
||||
new_text
|
||||
end
|
||||
|
||||
# Check if text have smart annotations of users
|
||||
# and outputs a popover with user information
|
||||
def smart_annotation_filter_users(text, organization)
|
||||
sa_user = /\[\@(.*?)~([0-9a-zA-Z]+)\]/
|
||||
new_text = text.gsub(sa_user) do |el|
|
||||
match = el.match(sa_user)
|
||||
user = User.find_by_id(match[2].base62_decode)
|
||||
organization ||= current_organization
|
||||
|
||||
if user &&
|
||||
organization &&
|
||||
UserOrganization.user_in_organization(user, organization).any?
|
||||
user_org = user
|
||||
.user_organizations
|
||||
.where('user_organizations.organization_id = ?',
|
||||
organization).first
|
||||
user_description = %(<div class='col-xs-4'>
|
||||
<img src='#{avatar_path(user, :thumb)}' alt='thumb'>
|
||||
</div><div class='col-xs-8'>
|
||||
<div class='row'><div class='col-xs-9 text-left'><h5>
|
||||
#{user.full_name}</h5></div><div class='col-xs-3 text-right'>
|
||||
<span class='glyphicon glyphicon-remove' aria-hidden='true'></span>
|
||||
</div></div><div class='row'><div class='col-xs-12'>
|
||||
<p class='silver'>#{user.email}</p><p>
|
||||
#{I18n.t('atwho.popover_html',
|
||||
role: user_org.role.capitalize,
|
||||
organization: user_org.organization.name,
|
||||
time: user_org.created_at.strftime('%B %Y'))}
|
||||
</p></div></div></div>)
|
||||
|
||||
raw(image_tag(avatar_path(user, :icon_small),
|
||||
class: 'atwho-user-img-popover')) +
|
||||
raw('<a onClick="$(this).popover(\'show\')" ' \
|
||||
'class="atwho-user-popover" data-container="body" ' \
|
||||
'data-html="true" tabindex="0" data-trigger="focus" ' \
|
||||
'data-placement="top" data-toggle="popover" data-content="') +
|
||||
raw(user_description) + raw('" >') + user.full_name + raw('</a>')
|
||||
end
|
||||
end
|
||||
new_text
|
||||
end
|
||||
end
|
||||
|
|
|
@ -260,5 +260,11 @@ module BootstrapFormHelper
|
|||
end
|
||||
text_area(name, opts)
|
||||
end
|
||||
|
||||
# Returns <textarea> helper tag for tinyMCE editor
|
||||
def tiny_mce_editor(name, options = {})
|
||||
options.merge!(class: 'tinymce', cols: 120, rows: 15)
|
||||
text_area(name, options)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -13,6 +13,6 @@ module InputSanitizeHelper
|
|||
|
||||
def custom_auto_link(text, args)
|
||||
args[:sanitize] = false
|
||||
sanitize_input(auto_link(text, args))
|
||||
auto_link(sanitize_input(text), args)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -246,6 +246,11 @@ module PermissionHelper
|
|||
# to "is project archived" or "is module archived" checks
|
||||
# at the beginning of this file (via aspector).
|
||||
|
||||
# ---- ATWHO PERMISSIONS ----
|
||||
def can_view_organization_users(organization)
|
||||
is_member_of_organization(organization)
|
||||
end
|
||||
|
||||
# ---- PROJECT PERMISSIONS ----
|
||||
|
||||
def can_view_projects(organization)
|
||||
|
|
|
@ -1,17 +0,0 @@
|
|||
module QuillJsHelper
|
||||
def sanitize_quill_js_input(input)
|
||||
require "#{Rails.root}/app/utilities/scrubbers/quill_js_scrubber"
|
||||
|
||||
# We need to disable formatting to prevent unwanted \n
|
||||
# symbols from creeping into sanitized HTML (which
|
||||
# cause unwanted new lines when rendered in Quill.js)
|
||||
disable_formatting =
|
||||
Nokogiri::XML::Node::SaveOptions::DEFAULT_HTML ^
|
||||
Nokogiri::XML::Node::SaveOptions::FORMAT
|
||||
|
||||
Loofah
|
||||
.fragment(input)
|
||||
.scrub!(QuillJsScrubber.new)
|
||||
.to_html(save_with: disable_formatting)
|
||||
end
|
||||
end
|
|
@ -33,33 +33,50 @@ class Experiment < ActiveRecord::Base
|
|||
|
||||
scope :is_archived, ->(is_archived) { where("archived = ?", is_archived) }
|
||||
|
||||
def self.search(user, include_archived, query = nil, page = 1)
|
||||
def self.search(
|
||||
user,
|
||||
include_archived,
|
||||
query = nil,
|
||||
page = 1,
|
||||
current_organization = nil
|
||||
)
|
||||
project_ids =
|
||||
Project
|
||||
.search(user, include_archived, nil, Constants::SEARCH_NO_LIMIT)
|
||||
.select('id')
|
||||
|
||||
if query
|
||||
a_query = query.strip
|
||||
.gsub("_","\\_")
|
||||
.gsub("%","\\%")
|
||||
.split(/\s+/)
|
||||
.map {|t| "%" + t + "%" }
|
||||
a_query = '%' + query.strip.gsub('_', '\\_').gsub('%', '\\%') + '%'
|
||||
else
|
||||
a_query = query
|
||||
end
|
||||
|
||||
if include_archived
|
||||
if current_organization
|
||||
projects_ids =
|
||||
Project
|
||||
.search(user,
|
||||
include_archived,
|
||||
nil,
|
||||
1,
|
||||
current_organization)
|
||||
.select('id')
|
||||
|
||||
new_query =
|
||||
Experiment
|
||||
.where(project: project_ids)
|
||||
.where_attributes_like([:name, :description], a_query)
|
||||
.where('experiments.project_id IN (?)', projects_ids)
|
||||
.where_attributes_like([:name], a_query)
|
||||
return include_archived ? new_query : new_query.is_archived(false)
|
||||
elsif include_archived
|
||||
new_query =
|
||||
Experiment
|
||||
.where(project: project_ids)
|
||||
.where_attributes_like([:name, :description], a_query)
|
||||
else
|
||||
new_query =
|
||||
Experiment
|
||||
.is_archived(false)
|
||||
.where(project: project_ids)
|
||||
.where_attributes_like([:name, :description], a_query)
|
||||
.is_archived(false)
|
||||
.where(project: project_ids)
|
||||
.where_attributes_like([:name, :description], a_query)
|
||||
end
|
||||
|
||||
# Show all results if needed
|
||||
|
|
|
@ -41,33 +41,53 @@ class MyModule < ActiveRecord::Base
|
|||
WIDTH = 30
|
||||
HEIGHT = 14
|
||||
|
||||
def self.search(user, include_archived, query = nil, page = 1)
|
||||
def self.search(
|
||||
user,
|
||||
include_archived,
|
||||
query = nil,
|
||||
page = 1,
|
||||
current_organization = nil
|
||||
)
|
||||
exp_ids =
|
||||
Experiment
|
||||
.search(user, include_archived, nil, Constants::SEARCH_NO_LIMIT)
|
||||
.select("id")
|
||||
|
||||
if query
|
||||
a_query = query.strip
|
||||
.gsub("_","\\_")
|
||||
.gsub("%","\\%")
|
||||
.split(/\s+/)
|
||||
.map {|t| "%" + t + "%" }
|
||||
a_query = '%' + query.strip.gsub('_', '\\_').gsub('%', '\\%') + '%'
|
||||
else
|
||||
a_query = query
|
||||
end
|
||||
|
||||
if include_archived
|
||||
if current_organization
|
||||
experiments_ids = Experiment
|
||||
.search(user,
|
||||
include_archived,
|
||||
nil,
|
||||
1,
|
||||
current_organization)
|
||||
.select('id')
|
||||
new_query = MyModule
|
||||
.distinct
|
||||
.where("my_modules.experiment_id IN (?)", exp_ids)
|
||||
.where_attributes_like([:name, :description], a_query)
|
||||
.distinct
|
||||
.where('my_modules.experiment_id IN (?)', experiments_ids)
|
||||
.where_attributes_like([:name], a_query)
|
||||
|
||||
if include_archived
|
||||
return new_query
|
||||
else
|
||||
return new_query.where('my_modules.archived = ?', false)
|
||||
end
|
||||
elsif include_archived
|
||||
new_query = MyModule
|
||||
.distinct
|
||||
.where('my_modules.experiment_id IN (?)', exp_ids)
|
||||
.where_attributes_like([:name, :description], a_query)
|
||||
else
|
||||
new_query = MyModule
|
||||
.distinct
|
||||
.where("my_modules.experiment_id IN (?)", exp_ids)
|
||||
.where("my_modules.archived = ?", false)
|
||||
.where_attributes_like([:name, :description], a_query)
|
||||
.distinct
|
||||
.where('my_modules.experiment_id IN (?)', exp_ids)
|
||||
.where('my_modules.archived = ?', false)
|
||||
.where_attributes_like([:name, :description], a_query)
|
||||
end
|
||||
|
||||
# Show all results if needed
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
class Organization < ActiveRecord::Base
|
||||
include SearchableModel
|
||||
|
||||
# Not really MVC-compliant, but we just use it for logger
|
||||
# output in space_taken related functions
|
||||
include ActionView::Helpers::NumberHelper
|
||||
|
@ -51,6 +53,24 @@ class Organization < ActiveRecord::Base
|
|||
end
|
||||
end
|
||||
|
||||
def search_users(
|
||||
query = nil,
|
||||
attributes = [:full_name, :email]
|
||||
)
|
||||
if query
|
||||
a_query = query
|
||||
.strip
|
||||
.gsub('_', '\\_')
|
||||
.gsub('%', '\\%')
|
||||
else
|
||||
a_query = query
|
||||
end
|
||||
|
||||
users
|
||||
.where.not(confirmed_at: nil)
|
||||
.where_attributes_like(attributes, a_query)
|
||||
end
|
||||
|
||||
# Writes to user log
|
||||
def log(message)
|
||||
final = "[%s] %s" % [Time.current.to_s, message]
|
||||
|
|
|
@ -26,42 +26,66 @@ class Project < ActiveRecord::Base
|
|||
has_many :report_elements, inverse_of: :project, dependent: :destroy
|
||||
belongs_to :organization, inverse_of: :projects
|
||||
|
||||
def self.search(user, include_archived, query = nil, page = 1)
|
||||
def self.search(
|
||||
user,
|
||||
include_archived,
|
||||
query = nil,
|
||||
page = 1,
|
||||
current_organization = nil
|
||||
)
|
||||
|
||||
if query
|
||||
a_query = query.strip
|
||||
.gsub("_","\\_")
|
||||
.gsub("%","\\%")
|
||||
.split(/\s+/)
|
||||
.map {|t| "%" + t + "%" }
|
||||
a_query = '%' + query.strip.gsub('_', '\\_').gsub('%', '\\%') + '%'
|
||||
else
|
||||
a_query = query
|
||||
end
|
||||
|
||||
|
||||
org_ids =
|
||||
Organization
|
||||
.joins(:user_organizations)
|
||||
.where("user_organizations.user_id = ?", user.id)
|
||||
.select("id")
|
||||
.distinct
|
||||
|
||||
if include_archived
|
||||
if current_organization
|
||||
new_query = Project
|
||||
.distinct
|
||||
.joins(:user_projects)
|
||||
.where("projects.organization_id IN (?)", org_ids)
|
||||
.where("projects.visibility = 1 OR user_projects.user_id = ?", user.id)
|
||||
.where_attributes_like(:name, a_query)
|
||||
.distinct
|
||||
.joins(:user_projects)
|
||||
.where('projects.organization_id = ?',
|
||||
current_organization.id)
|
||||
.where('projects.visibility = 1 OR user_projects.user_id = ?',
|
||||
user.id)
|
||||
.where_attributes_like(:name, a_query)
|
||||
|
||||
if include_archived
|
||||
return new_query
|
||||
else
|
||||
return new_query.where('projects.archived = ?', false)
|
||||
end
|
||||
else
|
||||
new_query = Project
|
||||
org_ids =
|
||||
Organization
|
||||
.joins(:user_organizations)
|
||||
.where('user_organizations.user_id = ?', user.id)
|
||||
.select('id')
|
||||
.distinct
|
||||
.joins(:user_projects)
|
||||
.where("projects.organization_id IN (?)", org_ids)
|
||||
.where("projects.visibility = 1 OR user_projects.user_id = ?", user.id)
|
||||
.where_attributes_like(:name, a_query)
|
||||
.where("projects.archived = ?", false)
|
||||
|
||||
if include_archived
|
||||
new_query = Project
|
||||
.distinct
|
||||
.joins(:user_projects)
|
||||
.where('projects.organization_id IN (?)', org_ids)
|
||||
.where(
|
||||
'projects.visibility = 1 OR user_projects.user_id = ?',
|
||||
user.id
|
||||
)
|
||||
.where_attributes_like(:name, a_query)
|
||||
|
||||
else
|
||||
new_query = Project
|
||||
.distinct
|
||||
.joins(:user_projects)
|
||||
.where('projects.organization_id IN (?)', org_ids)
|
||||
.where(
|
||||
'projects.visibility = 1 OR user_projects.user_id = ?',
|
||||
user.id
|
||||
)
|
||||
.where_attributes_like(:name, a_query)
|
||||
.where('projects.archived = ?', false)
|
||||
end
|
||||
end
|
||||
|
||||
# Show all results if needed
|
||||
|
|
|
@ -23,7 +23,8 @@ class Sample < ActiveRecord::Base
|
|||
user,
|
||||
include_archived,
|
||||
query = nil,
|
||||
page = 1
|
||||
page = 1,
|
||||
current_organization = nil
|
||||
)
|
||||
org_ids =
|
||||
Organization
|
||||
|
@ -33,33 +34,40 @@ class Sample < ActiveRecord::Base
|
|||
.distinct
|
||||
|
||||
if query
|
||||
a_query = query.strip
|
||||
.gsub("_","\\_")
|
||||
.gsub("%","\\%")
|
||||
.split(/\s+/)
|
||||
.map {|t| "%" + t + "%" }
|
||||
a_query = '%' + query.strip.gsub('_', '\\_').gsub('%', '\\%') + '%'
|
||||
else
|
||||
a_query = query
|
||||
end
|
||||
|
||||
new_query = Sample
|
||||
.distinct
|
||||
.joins(:user)
|
||||
.joins("LEFT OUTER JOIN sample_types ON samples.sample_type_id = sample_types.id")
|
||||
.joins("LEFT OUTER JOIN sample_groups ON samples.sample_group_id = sample_groups.id")
|
||||
.joins("LEFT OUTER JOIN sample_custom_fields ON samples.id = sample_custom_fields.sample_id")
|
||||
.where("samples.organization_id IN (?)", org_ids)
|
||||
.where_attributes_like(
|
||||
[
|
||||
"samples.name",
|
||||
"sample_types.name",
|
||||
"sample_groups.name",
|
||||
"users.full_name",
|
||||
"sample_custom_fields.value"
|
||||
],
|
||||
a_query
|
||||
)
|
||||
if current_organization
|
||||
new_query = Sample
|
||||
.distinct
|
||||
.where('samples.organization_id = ?', current_organization.id)
|
||||
.where_attributes_like(['samples.name'], a_query)
|
||||
|
||||
return new_query
|
||||
else
|
||||
new_query = Sample
|
||||
.distinct
|
||||
.joins(:user)
|
||||
.joins('LEFT OUTER JOIN sample_types ON ' \
|
||||
'samples.sample_type_id = sample_types.id')
|
||||
.joins('LEFT OUTER JOIN sample_groups ON ' \
|
||||
'samples.sample_group_id = sample_groups.id')
|
||||
.joins('LEFT OUTER JOIN sample_custom_fields ON ' \
|
||||
'samples.id = sample_custom_fields.sample_id')
|
||||
.where('samples.organization_id IN (?)', org_ids)
|
||||
.where_attributes_like(
|
||||
[
|
||||
'samples.name',
|
||||
'sample_types.name',
|
||||
'sample_groups.name',
|
||||
'users.full_name',
|
||||
'sample_custom_fields.value'
|
||||
],
|
||||
a_query
|
||||
)
|
||||
end
|
||||
# Show all results if needed
|
||||
if page == Constants::SEARCH_NO_LIMIT
|
||||
new_query
|
||||
|
|
|
@ -30,6 +30,11 @@ class UserOrganization < ActiveRecord::Base
|
|||
end
|
||||
end
|
||||
|
||||
# returns user_organizations where the user is in org
|
||||
def self.user_in_organization(user, organization)
|
||||
where(user: user, organization: organization)
|
||||
end
|
||||
|
||||
def destroy(new_owner)
|
||||
# If any project of the organization has the sole owner and that
|
||||
# owner is the user to be removed from the organization, then we must
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
module ProtocolsImporter
|
||||
include RenamingUtil, QuillJsHelper
|
||||
include RenamingUtil
|
||||
|
||||
def import_new_protocol(protocol_json, organization, type, user)
|
||||
remove_empty_inputs(protocol_json)
|
||||
|
@ -52,11 +52,8 @@ module ProtocolsImporter
|
|||
if protocol_json['steps']
|
||||
protocol_json['steps'].values.each do |step_json|
|
||||
step = Step.create!(
|
||||
name: step_json["name"],
|
||||
description: # Sanitize description HTML
|
||||
sanitize_quill_js_input(
|
||||
step_json['description']
|
||||
),
|
||||
name: step_json['name'],
|
||||
description: step_json['description'],
|
||||
position: step_pos,
|
||||
completed: false,
|
||||
user: user,
|
||||
|
|
|
@ -1,11 +0,0 @@
|
|||
class QuillJsScrubber < Rails::Html::PermitScrubber
|
||||
def initialize
|
||||
super
|
||||
self.tags = %w(h1 span p br pre ul li strong em u sub sup s a blockquote ol)
|
||||
self.attributes = %w(style class spellcheck href target)
|
||||
end
|
||||
|
||||
def skip_node?(node)
|
||||
node.text?
|
||||
end
|
||||
end
|
100
app/utilities/smart_annotation.rb
Normal file
100
app/utilities/smart_annotation.rb
Normal file
|
@ -0,0 +1,100 @@
|
|||
class SmartAnnotation
|
||||
include ActionView::Helpers::SanitizeHelper
|
||||
include ActionView::Helpers::TextHelper
|
||||
|
||||
attr_writer :current_user, :current_organization, :query
|
||||
|
||||
def initialize(current_user, current_organization, query)
|
||||
@current_user = current_user
|
||||
@current_organization = current_organization
|
||||
@query = query
|
||||
end
|
||||
|
||||
def my_modules
|
||||
# Search tasks
|
||||
res = MyModule
|
||||
.search(@current_user, false, @query, 1, @current_organization)
|
||||
.limit(Constants::ATWHO_SEARCH_LIMIT)
|
||||
|
||||
modules_list = []
|
||||
res.each do |my_module_res|
|
||||
my_mod = {}
|
||||
my_mod['id'] = my_module_res.id.base62_encode
|
||||
my_mod['name'] = sanitize(my_module_res.name)
|
||||
my_mod['archived'] = my_module_res.archived
|
||||
my_mod['experimentName'] = truncate(
|
||||
sanitize(my_module_res.experiment.name,
|
||||
length: Constants::NAME_TRUNCATION_LENGTH)
|
||||
)
|
||||
my_mod['projectName'] = truncate(
|
||||
sanitize(my_module_res.experiment.project.name,
|
||||
length: Constants::NAME_TRUNCATION_LENGTH)
|
||||
)
|
||||
my_mod['type'] = 'tsk'
|
||||
|
||||
modules_list << my_mod
|
||||
end
|
||||
modules_list
|
||||
end
|
||||
|
||||
def projects
|
||||
# Search projects
|
||||
res = Project
|
||||
.search(@current_user, false, @query, 1, @current_organization)
|
||||
.limit(Constants::ATWHO_SEARCH_LIMIT)
|
||||
|
||||
projects_list = []
|
||||
res.each do |project_res|
|
||||
prj = {}
|
||||
prj['id'] = project_res.id.base62_encode
|
||||
prj['name'] = sanitize(project_res.name)
|
||||
prj['type'] = 'prj'
|
||||
projects_list << prj
|
||||
end
|
||||
projects_list
|
||||
end
|
||||
|
||||
def experiments
|
||||
# Search experiments
|
||||
res = Experiment
|
||||
.search(@current_user, false, @query, 1, @current_organization)
|
||||
.limit(Constants::ATWHO_SEARCH_LIMIT)
|
||||
|
||||
experiments_list = []
|
||||
res.each do |experiment_res|
|
||||
exp = {}
|
||||
exp['id'] = experiment_res.id.base62_encode
|
||||
exp['name'] = sanitize(experiment_res.name)
|
||||
exp['type'] = 'exp'
|
||||
exp['projectName'] = truncate(
|
||||
sanitize(experiment_res.project.name,
|
||||
length: Constants::NAME_TRUNCATION_LENGTH)
|
||||
)
|
||||
experiments_list << exp
|
||||
end
|
||||
experiments_list
|
||||
end
|
||||
|
||||
def samples
|
||||
# Search samples
|
||||
res = Sample
|
||||
.search(@current_user, false, @query, 1, @current_organization)
|
||||
.limit(Constants::ATWHO_SEARCH_LIMIT)
|
||||
|
||||
samples_list = []
|
||||
res.each do |sample_res|
|
||||
sam = {}
|
||||
sam['id'] = sample_res.id.base62_encode
|
||||
sam['name'] = sanitize(sample_res.name)
|
||||
sam['description'] = "#{I18n.t('Added')} #{I18n.l(
|
||||
sample_res.created_at, format: :full_date
|
||||
)} #{I18n.t('by')} #{truncate(
|
||||
sanitize(sample_res.user.full_name,
|
||||
length: Constants::NAME_TRUNCATION_LENGTH)
|
||||
)}"
|
||||
sam['type'] = 'sam'
|
||||
samples_list << sam
|
||||
end
|
||||
samples_list
|
||||
end
|
||||
end
|
|
@ -1,7 +1,12 @@
|
|||
<%= bootstrap_form_for(@comment, url: @update_url, remote: true, html: { method: :put, class: 'comment-form edit-comment-form' }, data: { role: 'edit-comment-message-form' }) do |f| %>
|
||||
<div class="form-group">
|
||||
<div class="input-group">
|
||||
<%= f.smart_text_area :message, single_line: true, autofocus: true, hide_label: true, data: { role: 'message-input' }, value: @comment.message %>
|
||||
<%= f.smart_text_area :message,
|
||||
single_line: true,
|
||||
autofocus: true,
|
||||
hide_label: true,
|
||||
data: { role: 'message-input', 'atwho-edit' => '' },
|
||||
value: @comment.message %>
|
||||
<span class="input-group-btn">
|
||||
<a class="btn btn-default" data-action="save">
|
||||
<span class="glyphicon glyphicon-ok"></span>
|
||||
|
@ -11,4 +16,4 @@
|
|||
<span class="help-block hide"></span>
|
||||
<a data-action="cancel" href="#"><%= t('general.cancel') %></a>
|
||||
</div>
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
|
|
@ -15,7 +15,8 @@
|
|||
<div class="form-group">
|
||||
<%= form.smart_text_area :description,
|
||||
label: t('experiments.new.description'),
|
||||
id: 'experiment-description' %>
|
||||
id: 'experiment-description',
|
||||
data: { 'atwho-edit' => '' } %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -14,7 +14,17 @@
|
|||
|
||||
<%= csrf_meta_tags %>
|
||||
</head>
|
||||
<body class="<%= yield :body_class %>">
|
||||
<body
|
||||
class="<%= yield :body_class %>"
|
||||
<% if user_signed_in? && current_organization.present? %>
|
||||
data-atwho-users-url="<%= atwho_users_organization_path(current_organization) %>"
|
||||
data-atwho-task-url="<%= atwho_my_modules_organization_path(current_organization) %>"
|
||||
data-atwho-project-url="<%= atwho_projects_organization_path(current_organization) %>"
|
||||
data-atwho-experiment-url="<%= atwho_experiments_organization_path(current_organization) %>"
|
||||
data-atwho-sample-url="<%= atwho_samples_organization_path(current_organization) %>"
|
||||
data-atwho-menu-items="<%= atwho_menu_items_organization_path(current_organization) %>"
|
||||
<% end %>
|
||||
>
|
||||
|
||||
<span style="display: none;" data-hook="body-js"></span>
|
||||
<span style="display: none;" data-hook="application-body-html"></span>
|
||||
|
|
|
@ -39,8 +39,12 @@
|
|||
</div>
|
||||
<strong><%= comment.user.full_name %>:</strong>
|
||||
<div data-role="comment-message-container">
|
||||
<div data-role="comment-message">
|
||||
<%= custom_auto_link(simple_format(comment.message),
|
||||
link: :urls, html: { target: '_blank' }) %>
|
||||
</div>
|
||||
<div data-role="comment-message"><%= auto_link(
|
||||
smart_annotation_parser(simple_format(
|
||||
sanitize_input(comment.message)),
|
||||
),
|
||||
link: :urls,
|
||||
sanitize: false,
|
||||
html: { target: '_blank' }
|
||||
).html_safe %></div>
|
||||
</div>
|
||||
|
|
|
@ -19,7 +19,13 @@
|
|||
<li>
|
||||
<hr>
|
||||
<%= bootstrap_form_for :comment, url: { format: :json }, method: :post, remote: true, html: { class: 'comment-form' } do |f| %>
|
||||
<%= f.smart_text_area :message, single_line: true, hide_label: true, placeholder: t("experiments.canvas.popups.comment_placeholder"), append: f.submit("+"), help: '.' %>
|
||||
<%= f.smart_text_area :message,
|
||||
single_line: true,
|
||||
hide_label: true,
|
||||
placeholder: t("experiments.canvas.popups.comment_placeholder"),
|
||||
append: f.submit("+"),
|
||||
help: '.',
|
||||
data: { 'atwho-edit' => '' } %>
|
||||
<% end %>
|
||||
</li>
|
||||
</ul>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<div class="panel panel-default panel-protocol-status">
|
||||
<div class="panel-body">
|
||||
<div class="panel-body">
|
||||
<% if @protocol.linked? %>
|
||||
<%= protocol_status_href(@protocol) %>
|
||||
<% else %>
|
||||
|
|
|
@ -39,8 +39,12 @@
|
|||
</div>
|
||||
<strong><%= comment.user.full_name %>:</strong>
|
||||
<div data-role="comment-message-container">
|
||||
<div data-role="comment-message">
|
||||
<%= custom_auto_link(simple_format(comment.message),
|
||||
link: :urls, html: { target: '_blank' }) %>
|
||||
</div>
|
||||
<div data-role="comment-message"><%= auto_link(
|
||||
smart_annotation_parser(simple_format(
|
||||
sanitize_input(comment.message)),
|
||||
),
|
||||
link: :urls,
|
||||
sanitize: false,
|
||||
html: { target: '_blank' }
|
||||
).html_safe %></div>
|
||||
</div>
|
||||
|
|
|
@ -18,7 +18,13 @@
|
|||
<li>
|
||||
<hr>
|
||||
<%= bootstrap_form_for :comment, url: {format: :json}, method: :post, remote: true, html: { class: 'comment-form' } do |f| %>
|
||||
<%= f.smart_text_area :message, single_line: true, hide_label: true, placeholder: t('projects.index.comment_placeholder'), append: f.submit('+'), help: '.' %>
|
||||
<%= f.smart_text_area :message,
|
||||
single_line: true,
|
||||
hide_label: true,
|
||||
placeholder: t('projects.index.comment_placeholder'),
|
||||
append: f.submit('+'),
|
||||
help: '.',
|
||||
data: { 'atwho-edit' => '' } %>
|
||||
<% end %>
|
||||
</li>
|
||||
</ul>
|
||||
|
|
|
@ -67,8 +67,14 @@
|
|||
</div>
|
||||
<div class="row">
|
||||
<div class="col-xs-12">
|
||||
<%= experiment.description %>
|
||||
</span>
|
||||
<%= auto_link(
|
||||
smart_annotation_parser(
|
||||
simple_format(experiment.description)
|
||||
),
|
||||
link: :urls,
|
||||
sanitize: false,
|
||||
html: { target: '_blank' }
|
||||
).html_safe %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -49,8 +49,16 @@
|
|||
</span>
|
||||
<% if experiment.description? %>
|
||||
<div class='experiment-description'>
|
||||
<%= custom_auto_link(simple_format(experiment.description),
|
||||
link: :urls, html: { target: '_blank' }) %>
|
||||
<%= auto_link(
|
||||
smart_annotation_parser(
|
||||
simple_format(
|
||||
sanitize_input(experiment.description)
|
||||
)
|
||||
),
|
||||
link: :urls,
|
||||
sanitize: false,
|
||||
html: { target: '_blank' }
|
||||
).html_safe %>
|
||||
</div>
|
||||
<% else %>
|
||||
<span class='experiment-no-description'>
|
||||
|
|
|
@ -27,8 +27,10 @@
|
|||
<div class="row">
|
||||
<div class="col-xs-12">
|
||||
<% if experiment.description.present? %>
|
||||
<%= custom_auto_link(simple_format(experiment.description),
|
||||
link: :urls, html: { target: '_blank' }) %>
|
||||
<%= auto_link(smart_annotation_parser(simple_format(sanitize_input(experiment.description))),
|
||||
link: :urls,
|
||||
sanitize: false,
|
||||
html: { target: '_blank' }).html_safe %>
|
||||
<% else %>
|
||||
<em><%=t "projects.reports.elements.experiment.no_description" %></em>
|
||||
<% end %>
|
||||
|
|
|
@ -31,9 +31,10 @@
|
|||
</span>
|
||||
<span class="comment-message">
|
||||
|
||||
<%= custom_auto_link(simple_format(comment.message),
|
||||
link: :urls,
|
||||
html: { target: '_blank' }) %>
|
||||
<%= auto_link(smart_annotation_parser(simple_format(sanitize_input(comment.message))),
|
||||
link: :urls,
|
||||
sanitize: false,
|
||||
html: { target: '_blank' }).html_safe %>
|
||||
</span>
|
||||
</li>
|
||||
<% end %>
|
||||
|
|
|
@ -23,8 +23,10 @@
|
|||
<div class="report-element-body">
|
||||
<div class="row">
|
||||
<div class="col-xs-12 text-container ql-editor">
|
||||
<%= custom_auto_link(result_text.text,
|
||||
link: :urls, html: { target: '_blank' }) %>
|
||||
<%= auto_link(smart_annotation_parser(simple_format(sanitize_input(result_text.text))),
|
||||
link: :urls,
|
||||
sanitize: false,
|
||||
html: { target: '_blank' }).html_safe %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -8,7 +8,10 @@
|
|||
<span class="glyphicon glyphicon-list"></span>
|
||||
</div>
|
||||
<div class="pull-left checklist-name">
|
||||
<em><%=t 'projects.reports.elements.step_checklist.checklist_name', name: checklist.name %></em>
|
||||
<%= auto_link(smart_annotation_parser(simple_format(t 'projects.reports.elements.step_checklist.checklist_name', name: checklist.name)),
|
||||
link: :urls,
|
||||
sanitize: false,
|
||||
html: { target: '_blank' }).html_safe %>
|
||||
</div>
|
||||
<div class="pull-left user-time">
|
||||
<%=t 'projects.reports.elements.step_checklist.user_time', timestamp: l(timestamp, format: :full) %>
|
||||
|
@ -24,9 +27,10 @@
|
|||
<li>
|
||||
<input type="checkbox" disabled="disabled" <%= "checked='checked'" if item.checked %>/>
|
||||
<span class="<%= 'checked' if item.checked %>">
|
||||
<%= custom_auto_link(simple_format(item.text),
|
||||
link: :urls, html: { target: '_blank' }) %>
|
||||
</span>
|
||||
<%= auto_link(smart_annotation_parser(simple_format(sanitize_input(item.text))),
|
||||
link: :urls,
|
||||
sanitize: false,
|
||||
html: { target: '_blank' }).html_safe %></span>
|
||||
</li>
|
||||
<% end %>
|
||||
</ul>
|
||||
|
|
|
@ -31,9 +31,10 @@
|
|||
</span>
|
||||
<span class="comment-message">
|
||||
|
||||
<%= custom_auto_link(simple_format(comment.message),
|
||||
link: :urls,
|
||||
html: { target: '_blank' }) %>
|
||||
<%= auto_link(smart_annotation_parser(simple_format(sanitize_input(comment.message))),
|
||||
link: :urls,
|
||||
sanitize: false,
|
||||
html: { target: '_blank' }).html_safe %>
|
||||
</span>
|
||||
</li>
|
||||
<% end %>
|
||||
|
|
|
@ -27,8 +27,10 @@
|
|||
<div class="row">
|
||||
<div class="col-xs-12 ql-editor">
|
||||
<% if strip_tags(step.description).present? %>
|
||||
<%= custom_auto_link(step.description,
|
||||
link: :urls, html: { target: '_blank' }) %>
|
||||
<%= auto_link(smart_annotation_parser(simple_format(sanitize_input(step.description))),
|
||||
link: :urls,
|
||||
sanitize: false,
|
||||
html: { target: '_blank' }).html_safe %>
|
||||
<% else %>
|
||||
<em><%=t "projects.reports.elements.step.no_description" %></em>
|
||||
<% end %>
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
<div>
|
||||
<strong>
|
||||
<%=t "my_modules.results.comment_title", user: comment.user.full_name, time: l(comment.created_at, format: :time) %>
|
||||
</strong>
|
||||
|
@ -40,8 +39,12 @@
|
|||
<% end %>
|
||||
</div>
|
||||
<div data-role="comment-message-container">
|
||||
<div data-role="comment-message">
|
||||
<%= custom_auto_link(simple_format(comment.message),
|
||||
link: :urls, html: { target: '_blank' }) %>
|
||||
</div>
|
||||
<div data-role="comment-message"><%= auto_link(
|
||||
smart_annotation_parser(simple_format(
|
||||
sanitize_input(comment.message)),
|
||||
),
|
||||
link: :urls,
|
||||
sanitize: false,
|
||||
html: { target: '_blank' }
|
||||
).html_safe %></div>
|
||||
</div>
|
||||
|
|
|
@ -22,7 +22,13 @@
|
|||
<li>
|
||||
<hr>
|
||||
<%= bootstrap_form_for :comment, url: { format: :json }, method: :post, remote: true, html: { class: 'comment-form' } do |f| %>
|
||||
<%= f.smart_text_area :message, single_line: true, hide_label: true, placeholder: t("general.comment_placeholder"), append: f.submit("+", onclick: "processResult(event, ResultTypeEnum.COMMENT, false);"), help: '.' %>
|
||||
<%= f.smart_text_area :message,
|
||||
single_line: true,
|
||||
hide_label: true,
|
||||
placeholder: t("general.comment_placeholder"),
|
||||
append: f.submit("+", onclick: "processResult(event, ResultTypeEnum.COMMENT, false);"),
|
||||
help: '.',
|
||||
data: { 'atwho-edit' => '' } %>
|
||||
<% end %>
|
||||
</li>
|
||||
</ul>
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
<%= f.text_field :name, style: "margin-top: 10px;" %><br />
|
||||
<%= f.fields_for :result_text do |ff| %>
|
||||
<div class="form-group">
|
||||
<%= quill_editor nil, { id: 'result_result_text_attributes_text', name: 'result[result_text_attributes][text]', value: @result.result_text.text } %>
|
||||
<%= ff.tiny_mce_editor(:text, value: @result.result_text.text) %>
|
||||
</div>
|
||||
<% end %><br />
|
||||
<%= f.submit t("result_texts.edit.update"), class: 'btn btn-primary save-result', onclick: "processResult(event, ResultTypeEnum.TEXT, true);" %>
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
<%= f.text_field :name, style: "margin-top: 10px;" %><br />
|
||||
<%= f.fields_for :result_text do |ff| %>
|
||||
<div class="form-group">
|
||||
<%= quill_editor nil, { id: 'result_result_text_attributes_text', name: 'result[result_text_attributes][text]', value: @result.result_text.text } %>
|
||||
<%= ff.tiny_mce_editor(:text) %>
|
||||
</div>
|
||||
<% end %><br />
|
||||
<%= f.submit t("result_texts.new.create"), class: 'btn btn-primary save-result', onclick: "processResult(event, ResultTypeEnum.TEXT, false);" %>
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
<div class="ql-editor">
|
||||
<%= custom_auto_link(result.result_text.text,
|
||||
link: :urls, html: { target: '_blank' }) %>
|
||||
<%= auto_link(smart_annotation_parser(simple_format(sanitize_input(result.result_text.text))),
|
||||
link: :urls,
|
||||
sanitize: false,
|
||||
html: { target: '_blank' }).html_safe %>
|
||||
</div>
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
|
||||
<div class="modal fade" id="modal-info-sample" tabindex="-1" role="dialog" aria-labelledby="myModalLabel">
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content">
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
<div>
|
||||
f<div>
|
||||
<strong>
|
||||
<%=t "protocols.steps.comment_title", user: comment.user.full_name, time: l(comment.created_at, format: :time) %>
|
||||
</strong>
|
||||
|
@ -40,8 +40,12 @@
|
|||
<% end %>
|
||||
</div>
|
||||
<div data-role="comment-message-container">
|
||||
<div data-role="comment-message">
|
||||
<%= custom_auto_link(simple_format(comment.message),
|
||||
link: :urls, html: { target: '_blank' }) %>
|
||||
</div>
|
||||
<div data-role="comment-message"><%= auto_link(
|
||||
smart_annotation_parser(simple_format(
|
||||
sanitize_input(comment.message)),
|
||||
),
|
||||
link: :urls,
|
||||
sanitize: false,
|
||||
html: { target: '_blank' }
|
||||
).html_safe %></div>
|
||||
</div>
|
||||
|
|
|
@ -22,7 +22,13 @@
|
|||
<li>
|
||||
<hr>
|
||||
<%= bootstrap_form_for :comment, url: { format: :json }, html: { class: 'comment-form', id: "step-comment-#{@step.id}" }, method: :post, remote: true do |f| %>
|
||||
<%= f.smart_text_area :message, single_line: true, hide_label: true, placeholder: t("general.comment_placeholder"), append: f.submit("+"), help: '.' %>
|
||||
<%= f.smart_text_area :message,
|
||||
single_line: true,
|
||||
hide_label: true,
|
||||
placeholder: t("general.comment_placeholder"),
|
||||
append: f.submit("+"),
|
||||
help: '.',
|
||||
data: { 'atwho-edit' => '' } %>
|
||||
<% end %>
|
||||
</li>
|
||||
</ul>
|
||||
|
|
|
@ -27,8 +27,7 @@
|
|||
<div class="tab-pane active" role="tabpanel" id="new-step-main">
|
||||
<%= f.text_field :name, label: t("protocols.steps.new.name"), placeholder: t("protocols.steps.new.name_placeholder") %>
|
||||
<div class="form-group">
|
||||
<label class="control-label" for="step_description"><%= t('protocols.steps.new.description') %></label>
|
||||
<%= quill_editor nil, { id: 'step_description', name: 'step[description]', value: @step.description } %>
|
||||
<%= f.tiny_mce_editor(:description) %>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tab-pane" role="tabpanel" id="new-step-checklists">
|
||||
|
|
|
@ -9,7 +9,12 @@
|
|||
</div>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<%= ff.smart_text_area :name, label: t("protocols.steps.new.checklist_name"), class: "checklist_name", autofocus: true, placeholder: t("protocols.steps.new.checklist_name_placeholder") %>
|
||||
<%= ff.smart_text_area :name,
|
||||
label: t('protocols.steps.new.checklist_name'),
|
||||
class: 'checklist_name',
|
||||
autofocus: true,
|
||||
placeholder: t('protocols.steps.new.checklist_name_placeholder'),
|
||||
data: { 'atwho-edit' => '' } %>
|
||||
<%= ff.label t("protocols.steps.new.checklist_items") %>
|
||||
<ul>
|
||||
<%= ff.nested_fields_for :checklist_items, ordered_checklist_items(ff.object) do |chkItems| %>
|
||||
|
@ -18,7 +23,13 @@
|
|||
<span class="glyphicon glyphicon-chevron-right handle-move pull-left"></span>
|
||||
<div class="row">
|
||||
<div class="col-xs-6">
|
||||
<%= chkItems.smart_text_area :text, autofocus: true, placeholder: t("protocols.steps.new.checklist_item_placeholder"), hide_label: true, class: "checklist-item-text form-control", single_line: true %>
|
||||
<%= chkItems.smart_text_area :text,
|
||||
autofocus: true,
|
||||
placeholder: t('protocols.steps.new.checklist_item_placeholder'),
|
||||
hide_label: true,
|
||||
class: 'checklist-item-text form-control',
|
||||
single_line: true,
|
||||
data: { 'atwho-edit' => '' } %>
|
||||
<%= chkItems.hidden_field :position, class: "checklist-item-pos" %>
|
||||
</div>
|
||||
<div class="col-xs-1">
|
||||
|
|
|
@ -37,8 +37,10 @@
|
|||
<em><%= t("protocols.steps.no_description") %></em>
|
||||
<% else %>
|
||||
<div class="ql-editor">
|
||||
<%= custom_auto_link(step.description,
|
||||
link: :urls, html: { target: '_blank' }) %>
|
||||
<%= auto_link(smart_annotation_parser(simple_format(sanitize_input(step.description))),
|
||||
link: :urls,
|
||||
sanitize: false,
|
||||
html: { target: '_blank' }).html_safe %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
|
@ -120,11 +122,15 @@
|
|||
<% unless step.checklists.blank? then %>
|
||||
<div class="col-xs-12">
|
||||
<% step.checklists.each do |checklist| %>
|
||||
<strong>
|
||||
<%= custom_auto_link(simple_format(checklist.name),
|
||||
link: :urls,
|
||||
html: { target: '_blank' }) %>
|
||||
</strong>
|
||||
<strong><%= auto_link(
|
||||
smart_annotation_parser(
|
||||
simple_format(
|
||||
sanitize_input(checklist.name)
|
||||
)
|
||||
),
|
||||
link: :urls,
|
||||
sanitize: false,
|
||||
html: { target: '_blank' }).html_safe %></strong>
|
||||
<% if checklist.checklist_items.empty? %>
|
||||
</br>
|
||||
<%= t("protocols.steps.empty_checklist") %>
|
||||
|
@ -138,9 +144,15 @@
|
|||
<% else %>
|
||||
<input type="checkbox" value="" disabled="disabled" />
|
||||
<% end %>
|
||||
<%= custom_auto_link(simple_format(checklist_item.text),
|
||||
link: :urls,
|
||||
html: { target: '_blank' }) %>
|
||||
<%= auto_link(
|
||||
smart_annotation_parser(
|
||||
simple_format(
|
||||
sanitize_input(checklist_item.text)
|
||||
)
|
||||
),
|
||||
link: :urls,
|
||||
sanitize: false,
|
||||
html: { target: '_blank' }).html_safe %>
|
||||
</label>
|
||||
</div>
|
||||
<% end %>
|
||||
|
|
|
@ -60,6 +60,7 @@ Rails.application.config.assets.precompile += %w(projects/show.js)
|
|||
Rails.application.config.assets.precompile += %w(notifications.js)
|
||||
Rails.application.config.assets.precompile += %w(users/invite_users_modal.js)
|
||||
Rails.application.config.assets.precompile += %w(samples/sample_types_groups.js)
|
||||
Rails.application.config.assets.precompile += %w(highlightjs-github-theme.css)
|
||||
|
||||
# Libraries needed for Handsontable formulas
|
||||
Rails.application.config.assets.precompile += %w(lodash.js)
|
||||
|
|
|
@ -48,6 +48,9 @@ class Constants
|
|||
# Maximum number of users that can be invited in a single action
|
||||
INVITE_USERS_LIMIT = 20
|
||||
|
||||
# Maximum nr. of search results for atwho (smart annotations)
|
||||
ATWHO_SEARCH_LIMIT = 5
|
||||
|
||||
#=============================================================================
|
||||
# File and data memory size
|
||||
#=============================================================================
|
||||
|
|
|
@ -1455,6 +1455,20 @@ en:
|
|||
assign_user_to_organization: "<i>%{assigned_user}</i> was added as %{role} to team <strong>%{organization}</strong> by <i>%{assigned_by_user}</i>."
|
||||
unassign_user_from_organization: "<i>%{unassigned_user}</i> was removed from team <strong>%{organization}</strong> by <i>%{unassigned_by_user}</i>."
|
||||
|
||||
atwho:
|
||||
no_results: "No results found"
|
||||
users:
|
||||
title: "People"
|
||||
navigate_1: "up/down"
|
||||
navigate_2: "to navigate"
|
||||
confirm_1: "enter/tab"
|
||||
confirm_2: "to confirm"
|
||||
dismiss_1: "esc"
|
||||
dismiss_2: "to dismiss"
|
||||
res:
|
||||
archived: "(archived)"
|
||||
deleted: "(deleted)"
|
||||
popover_html: "<span class='silver'>Team:</span> %{organization} <br> <span class='silver'>Role:</span> %{role} <br> <span class='silver'>Joined:</span> %{time}"
|
||||
# This section contains general words that can be used in any parts of
|
||||
# application.
|
||||
|
||||
|
@ -1519,3 +1533,5 @@ en:
|
|||
Workflow: "Workflow"
|
||||
Workflows: "Workflows"
|
||||
More: "More"
|
||||
Added: 'Added'
|
||||
by: 'by'
|
||||
|
|
|
@ -86,6 +86,13 @@ Rails.application.routes.draw do
|
|||
post 'parse_sheet'
|
||||
post 'import_samples'
|
||||
post 'export_samples'
|
||||
# Used for atwho (smart annotations)
|
||||
get 'atwho_users', to: 'at_who#users'
|
||||
get 'atwho_samples', to: 'at_who#samples'
|
||||
get 'atwho_projects', to: 'at_who#projects'
|
||||
get 'atwho_experiments', to: 'at_who#experiments'
|
||||
get 'atwho_my_modules', to: 'at_who#my_modules'
|
||||
get 'atwho_menu_items', to: 'at_who#menu_items'
|
||||
end
|
||||
match '*path', :to => 'organizations#routing_error', via: [:get, :post, :put, :patch]
|
||||
end
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
class AddIndexToUsersFullName < ActiveRecord::Migration
|
||||
def change
|
||||
add_index :users, :full_name
|
||||
end
|
||||
end
|
|
@ -685,6 +685,7 @@ ActiveRecord::Schema.define(version: 20170116143350) do
|
|||
add_index "users", ["authentication_token"], name: "index_users_on_authentication_token", unique: true, using: :btree
|
||||
add_index "users", ["confirmation_token"], name: "index_users_on_confirmation_token", unique: true, using: :btree
|
||||
add_index "users", ["email"], name: "index_users_on_email", unique: true, using: :btree
|
||||
add_index "users", ["full_name"], name: "index_users_on_full_name", using: :btree
|
||||
add_index "users", ["invitation_token"], name: "index_users_on_invitation_token", unique: true, using: :btree
|
||||
add_index "users", ["invitations_count"], name: "index_users_on_invitations_count", using: :btree
|
||||
add_index "users", ["invited_by_id"], name: "index_users_on_invited_by_id", using: :btree
|
||||
|
|
3
vendor/assets/javascripts/highlight.pack.js
vendored
Normal file
3
vendor/assets/javascripts/highlight.pack.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
vendor/assets/javascripts/jquery.atwho.min.js
vendored
Normal file
1
vendor/assets/javascripts/jquery.atwho.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
2
vendor/assets/javascripts/jquery.caret.min.js
vendored
Normal file
2
vendor/assets/javascripts/jquery.caret.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
99
vendor/assets/stylesheets/highlightjs-github-theme.css
vendored
Normal file
99
vendor/assets/stylesheets/highlightjs-github-theme.css
vendored
Normal file
|
@ -0,0 +1,99 @@
|
|||
/*
|
||||
|
||||
github.com style (c) Vasily Polovnyov <vast@whiteants.net>
|
||||
|
||||
*/
|
||||
|
||||
.hljs {
|
||||
display: block;
|
||||
overflow-x: auto;
|
||||
padding: 0.5em;
|
||||
color: #333;
|
||||
background: #f8f8f8;
|
||||
}
|
||||
|
||||
.hljs-comment,
|
||||
.hljs-quote {
|
||||
color: #998;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.hljs-keyword,
|
||||
.hljs-selector-tag,
|
||||
.hljs-subst {
|
||||
color: #333;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.hljs-number,
|
||||
.hljs-literal,
|
||||
.hljs-variable,
|
||||
.hljs-template-variable,
|
||||
.hljs-tag .hljs-attr {
|
||||
color: #008080;
|
||||
}
|
||||
|
||||
.hljs-string,
|
||||
.hljs-doctag {
|
||||
color: #d14;
|
||||
}
|
||||
|
||||
.hljs-title,
|
||||
.hljs-section,
|
||||
.hljs-selector-id {
|
||||
color: #900;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.hljs-subst {
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.hljs-type,
|
||||
.hljs-class .hljs-title {
|
||||
color: #458;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.hljs-tag,
|
||||
.hljs-name,
|
||||
.hljs-attribute {
|
||||
color: #000080;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.hljs-regexp,
|
||||
.hljs-link {
|
||||
color: #009926;
|
||||
}
|
||||
|
||||
.hljs-symbol,
|
||||
.hljs-bullet {
|
||||
color: #990073;
|
||||
}
|
||||
|
||||
.hljs-built_in,
|
||||
.hljs-builtin-name {
|
||||
color: #0086b3;
|
||||
}
|
||||
|
||||
.hljs-meta {
|
||||
color: #999;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.hljs-deletion {
|
||||
background: #fdd;
|
||||
}
|
||||
|
||||
.hljs-addition {
|
||||
background: #dfd;
|
||||
}
|
||||
|
||||
.hljs-emphasis {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.hljs-strong {
|
||||
font-weight: bold;
|
||||
}
|
Loading…
Reference in a new issue