Merge branch 'lm-sci-711-fix' of https://github.com/Ducz0r/scinote-web into Ducz0r-lm-sci-711-fix

Conflicts:
	app/assets/javascripts/samples/sample_datatable.js
This commit is contained in:
Luka Murn 2016-12-12 09:57:37 +01:00
commit d30a9780f4
11 changed files with 398 additions and 64 deletions

View file

@ -813,11 +813,15 @@ function changeToEditMode() {
table.button(0).enable(false);
}
// Samples table columns dropdown handling code
/*
* Sample columns dropdown
*/
(function(table) {
'use strict';
var dropdown = $('#samples-columns-dropdown');
var dropdownList = $('#samples-columns-list');
var columnEditMode = false;
function createNewColumn() {
// Make an Ajax request to custom_fields_controller
@ -849,7 +853,11 @@ function changeToEditMode() {
$('#samples').data('num-columns',
$('#samples').data('num-columns') + 1);
originalHeader.append(
'<th class="custom-field" id="' + data.id + '">' +
'<th class="custom-field" id="' + data.id + '" ' +
'data-editable data-deletable ' +
'data-edit-url="' + data.edit_url + '" ' +
'data-destroy-html-url="' + data.destroy_html_url + '"' +
'>' +
data.name + '</th>');
var colOrder = table.colReorder.order();
colOrder.push(colOrder.length);
@ -912,15 +920,34 @@ function changeToEditMode() {
if (index > 1) {
var colIndex = $(el).attr('data-column-index');
var visible = table.column(colIndex).visible();
var editable = $(el).is('[data-editable]');
var deletable = $(el).is('[data-deletable]');
var visClass = (visible) ? 'glyphicon-eye-open' : 'glyphicon-eye-close';
var visLi = (visible) ? '' : 'col-invisible';
var html = '<li data-position="' + colIndex + '" class="' + visLi +
'"><i class="grippy"></i> <span class="text">' +
el.innerText + '</span> <span class="pull-right controls">' +
'<span class="vis glyphicon ' + visClass + '"></span> ' +
'<span class="edit glyphicon glyphicon-pencil"></span> ' +
'<span class="del glyphicon glyphicon-trash"></span>' +
'</span></li>';
var editClass = (editable) ? '' : 'disabled';
var delClass = (deletable) ? '' : 'disabled';
var html =
'<li ' +
'data-position="' + colIndex + '" ' +
'data-id="' + $(el).attr('id') + '" ' +
'data-edit-url=' + $(el).attr('data-edit-url') + ' ' +
'data-destroy-html-url=' + $(el).attr('data-destroy-html-url') + ' ' +
'class="' + visLi + '"' +
'>' +
'<i class="grippy"></i> ' +
'<span class="text">' + el.innerText + '</span> ' +
'<input type="text" class="text-edit form-control" style="display: none;" />' +
'<span class="pull-right controls">' +
'<span class="ok glyphicon glyphicon-ok" style="display: none;"></span>' +
'<span class="cancel glyphicon glyphicon-remove" style="display: none;"></span>' +
'<span class="vis glyphicon ' + visClass + '"></span> ' +
'<span class="edit glyphicon glyphicon-pencil ' + editClass + '">' +
'</span> ' +
'<span class="del glyphicon glyphicon-trash ' + delClass + '">' +
'</span>' +
'</span>' +
'</li>';
dropdownList.append(html);
}
});
@ -983,11 +1010,199 @@ function changeToEditMode() {
});
}
function initEditColumns() {
function cancelEditMode() {
dropdownList.find('.text-edit').hide();
dropdownList.find('.controls .ok,.cancel').hide();
dropdownList.find('.text').css('display', ''); // show() doesn't work
dropdownList.find('.controls .vis,.edit,.del').css('display', ''); // show() doesn't work
columnEditMode = false;
}
function editColumn(li) {
var id = li.attr('data-id');
var text = li.find('.text');
var textEdit = li.find('.text-edit');
var newName = textEdit.val().trim();
var url = li.attr('data-edit-url');
$.ajax({
url: url,
type: 'PUT',
data: {custom_field: {name: newName}},
dataType: 'json',
success: function() {
text.text(newName);
$(table.columns().header()).filter('#' + id).text(newName);
cancelEditMode();
},
error: function(xhr) {
// TODO
}
});
}
// On edit buttons click (bind onto master dropdown list)
dropdownList.on('click', '.edit:not(.disabled)', function(event) {
event.stopPropagation();
cancelEditMode();
var self = $(this);
var li = self.closest('li');
var text = li.find('.text');
var textEdit = li.find('.text-edit');
var controls = li.find('.controls .vis,.edit,.del');
var controlsEdit = li.find('.controls .ok,.cancel');
// Toggle edit mode
columnEditMode = true;
li.addClass('editing');
// Set the text-edit's value
textEdit.val(text.text().trim());
// Toggle elements
text.hide();
controls.hide();
textEdit.css('display', ''); // show() doesn't work
controlsEdit.css('display', ''); // show() doesn't work
// Focus input
textEdit.focus();
});
// On hiding dropdown, cancel edit mode throughout dropdown
dropdown.on('hidden.bs.dropdown', function() {
cancelEditMode();
});
// On ok buttons click
dropdownList.on('click', '.ok', function(event) {
event.stopPropagation();
var self = $(this);
var li = self.closest('li');
editColumn(li);
});
// On enter click while editing column text
dropdownList.on('keydown', 'input.text-edit', function(event) {
if (event.keyCode === 13) {
event.preventDefault();
var self = $(this);
var li = self.closest('li');
editColumn(li);
}
});
// On cancel buttons click
dropdownList.on('click', '.cancel', function(event) {
event.stopPropagation();
var self = $(this);
var li = self.closest('li');
columnEditMode = false;
li.removeClass('editing');
li.find('.text-edit').hide();
li.find('.controls .ok,.cancel').hide();
li.find('.text').css('display', ''); // show() doesn't work
li.find('.controls .vis,.edit,.del').css('display', ''); // show() doesn't work
});
}
function initDeleteColumns() {
var modal = $('#deleteCustomField');
dropdownList.on('click', '.del', function(event) {
event.stopPropagation();
var self = $(this);
var li = self.closest('li');
var url = li.attr('data-destroy-html-url');
$.ajax({
url: url,
type: 'GET',
dataType: 'json',
success: function(data) {
var modalBody = modal.find('.modal-body');
// Inject the body's HTML into modal
modalBody.html(data.html);
// Show the modal
modal.modal('show');
},
error: function(xhr) {
// TODO
}
});
});
modal.find('.modal-footer [data-action=delete]').on('click', function() {
var modalBody = modal.find('.modal-body');
var form = modalBody.find('[data-role=destroy-custom-field-form]');
var id = form.attr('data-id');
form
.on('ajax:success', function() {
// Destroy datatable
table.destroy();
// Subtract number of columns
$('#samples').data(
'num-columns',
$('#samples').data('num-columns') - 1
);
// Remove column from table (=table header) & rows
var th = originalHeader.find('#' + id);
var index = th.index();
th.remove();
$('#samples tbody td:nth-child(' + (index + 1) + ')').remove();
// Remove all event handlers as we re-initialize them later with
// new table
$('#samples').off();
$('#samples thead').empty();
$('#samples thead').append(originalHeader);
// Preserve save/delete buttons as we need them after new table
// will be created
$('div.toolbarButtons').appendTo('div.samples-table');
$('div.toolbarButtons').hide();
// Re-initialize datatable
table = dataTableInit();
loadColumnsNames();
// Hide modal
modal.modal('hide');
})
.on('ajax:error', function() {
// TODO
});
form.submit();
});
modal.on('hidden.bs.modal', function() {
// Remove event handlers, clear contents
var modalBody = modal.find('.modal-body');
modalBody.off();
modalBody.html('');
});
}
// initialze dropdown after the table is loaded
function initDropdown() {
table.on('init.dt', function() {
initNewColumnForm();
initSorting();
toggleColumnVisibility();
initEditColumns();
initDeleteColumns();
});
$('#samples-columns-dropdown').on('show.bs.dropdown', function() {
loadColumnsNames();

View file

@ -1,32 +1,5 @@
//= require datatables
// Create custom field ajax
$("#modal-create-custom-field").on("show.bs.modal", function(event) {
// Clear input when modal is opened
input = $(this).find("input#name-input");
input.val("");
input.closest(".form-group").removeClass("has-error");
input.closest(".form-group").find(".help-block").remove();
});
$("#modal-create-custom-field").on("shown.bs.modal", function(event) {
$(this).find("input#name-input").focus();
});
$("form#new_custom_field").on("ajax:success", function(ev, data, status) {
$("#modal-create-custom-field").modal("hide");
// Reload page with URL parameter of newly created field
window.location.href = addParam(window.location.href, "new_col");
});
$("form#new_custom_field").on("ajax:error", function(e, data, status, xhr) {
$('form').renderFormErrors('custom_field', data.responseJSON, true, e);
});
$("form#new_sample_type").on("ajax:error", function(e, data, status, xhr) {
$('form').renderFormErrors('sample_type', data.responseJSON, true, e);
});
// Create sample group ajax
$("#modal-create-sample-group").on("show.bs.modal", function(event) {
// Clear input when modal is opened

View file

@ -1623,15 +1623,6 @@ textarea.textarea-sm {
margin-left: 10px;
}
.vis {
cursor: pointer;
margin-right: 5px;
}
.edit {
margin-right: 5px;
}
.grippy {
background-repeat: none;
display: inline-block;
@ -1640,7 +1631,7 @@ textarea.textarea-sm {
width: 10px;
}
.grippy-img {
li:not(.editing) .grippy-img {
background: url(asset-path('custom/grippy.png'));
}
@ -1651,6 +1642,14 @@ textarea.textarea-sm {
position: absolute;
}
.text-edit {
display: inline-block;
margin-left: 2px;
margin-top: -2.5px;
position: absolute;
width: 180px;
}
.col-invisible {
color: $color-alto;
}
@ -1658,5 +1657,37 @@ textarea.textarea-sm {
.controls {
display: inline-block;
margin-top: 5px;
span {
cursor: pointer;
&.disabled {
visibility: hidden;
}
&:hover {
color: $color-emperor;
}
}
.ok {
color: $color-theme-secondary;
margin-right: 5px;
}
.cancel {
margin-right: 28px;
}
.vis {
margin-right: 5px;
}
.edit {
margin-right: 5px;
}
.del {
}
}
}

View file

@ -1,6 +1,9 @@
class CustomFieldsController < ApplicationController
before_action :load_vars_nested, only: [:create]
before_action :check_create_permissions, only: [:create]
before_action :load_vars, only: [:update, :destroy, :destroy_html]
before_action :load_vars_nested, only: [:create, :destroy_html]
before_action :check_create_permissions, only: :create
before_action :check_update_permissions, only: :update
before_action :check_destroy_permissions, only: [:destroy, :destroy_html]
def create
@custom_field = CustomField.new(custom_field_params)
@ -9,12 +12,19 @@ class CustomFieldsController < ApplicationController
respond_to do |format|
if @custom_field.save
format.json {
format.json do
render json: {
id: @custom_field.id,
name: @custom_field.name
name: @custom_field.name,
edit_url:
organization_custom_field_path(@organization, @custom_field),
destroy_html_url:
organization_custom_field_destroy_html_path(
@organization, @custom_field
)
},
status: :ok }
status: :ok
end
else
format.json do
render json: @custom_field.errors.to_json,
@ -24,22 +34,71 @@ class CustomFieldsController < ApplicationController
end
end
private
def load_vars_nested
@organization = Organization.find_by_id(params[:organization_id])
unless @organization
render_404
def update
respond_to do |format|
format.json do
@custom_field.update_attributes(custom_field_params)
if @custom_field.save
render json: { status: :ok }
else
render json: @custom_field.errors.to_json,
status: :unprocessable_entity
end
end
end
end
def check_create_permissions
unless can_create_custom_field_in_organization(@organization)
render_403
def destroy_html
respond_to do |format|
format.json do
render json: {
html: render_to_string(
partial: 'samples/delete_custom_field_modal_body.html.erb'
)
}
end
end
end
def destroy
respond_to do |format|
format.json do
if @custom_field.destroy
render json: { status: :ok }
else
render json: { status: :unprocessable_entity }
end
end
end
end
private
def load_vars
@custom_field = CustomField.find_by_id(params[:id])
@custom_field = CustomField.find_by_id(
params[:custom_field_id]
) unless @custom_field
render_404 unless @custom_field
end
def load_vars_nested
@organization = Organization.find_by_id(params[:organization_id])
render_404 unless @organization
end
def check_create_permissions
render_403 unless can_create_custom_field_in_organization(@organization)
end
def check_update_permissions
render_403 unless can_edit_custom_field(@custom_field)
end
def check_destroy_permissions
render_403 unless can_delete_custom_field(@custom_field)
end
def custom_field_params
params.require(:custom_field).permit(:name)
end

View file

@ -674,6 +674,16 @@ module PermissionHelper
is_normal_user_or_admin_of_organization(organization)
end
def can_edit_custom_field(custom_field)
custom_field.user == current_user ||
is_admin_of_organization(custom_field.organization)
end
def can_delete_custom_field(custom_field)
custom_field.user == current_user ||
is_admin_of_organization(custom_field.organization)
end
# ---- PROTOCOL PERMISSIONS ----
def can_view_organization_protocols(organization)

View file

@ -13,7 +13,7 @@ class CustomField < ActiveRecord::Base
belongs_to :last_modified_by,
foreign_key: 'last_modified_by_id',
class_name: 'User'
has_many :sample_custom_fields, inverse_of: :custom_field
has_many :sample_custom_fields, inverse_of: :custom_field, dependent: :destroy
after_create :update_samples_table_state

View file

@ -0,0 +1,16 @@
<div class="modal fade" id="deleteCustomField" tabindex="-1" role="dialog" aria-labelledby="deleteCustomFieldLabel">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>
<h4 class="modal-title"><%= t("samples.modal_delete_custom_field.title") %></h4>
</div>
<div class="modal-body">
</div>
<div class="modal-footer">
<button type="button" class="btn btn-primary" data-action="delete"><%= t("samples.modal_delete_custom_field.delete") %></button>
<button type="button" class="btn btn-default" data-dismiss="modal"><%= t("general.cancel")%></button>
</div>
</div>
</div>
</div>

View file

@ -0,0 +1,12 @@
<%= bootstrap_form_for @custom_field, url: organization_custom_field_path(@organization, @custom_field, format: :json), remote: :true, method: :delete, data: { role: "destroy-custom-field-form", id: @custom_field.id } do |f| %>
<p><%= t("samples.modal_delete_custom_field.message", cf: @custom_field.name) %></p>
<div class="alert alert-danger" role="alert">
<span class="glyphicon glyphicon-exclamation-sign"></span>
&nbsp;
<%= t("samples.modal_delete_custom_field.alert_heading") %>
<ul>
<li><%= t("samples.modal_delete_custom_field.alert_line_1", nr: @custom_field.sample_custom_fields.count) %></li>
<li><%= t("samples.modal_delete_custom_field.alert_line_2") %></li>
</ul>
</div>
<% end %>

View file

@ -1,6 +1,7 @@
<%= render partial: "samples/import_samples_modal" %>
<%= render partial: "samples/delete_samples_modal" %>
<%= render partial: "samples/create_sample_group_modal" %>
<%= render partial: "samples/delete_custom_field_modal" %>
<!-- Modal for parsing sample sheets should be empty at first -->
<div class="modal fade" id="modal-parse-samples" tabindex="-1" role="dialog" aria-labelledby=="modal-parse-samples-label"></div>
@ -136,7 +137,15 @@
<th id="added-on"><%= t("samples.table.added_on") %></th>
<th id="added-by"><%= t("samples.table.added_by") %></th>
<% all_custom_fields.each do |cf| %>
<th class="custom-field" id="<%= cf.id %>"><%= cf.name %></th>
<th class="custom-field"
id="<%= cf.id %>"
<%= 'data-editable' if can_edit_custom_field(cf) %>
<%= 'data-deletable' if can_delete_custom_field(cf) %>
<%= "data-edit-url='#{organization_custom_field_path(@organization, cf)}'" %>
<%= "data-destroy-html-url='#{organization_custom_field_destroy_html_path(@organization, cf)}'" %>
>
<%= cf.name %>
</th>
<% end %>
</tr>
</thead>

View file

@ -832,6 +832,13 @@ en:
modal_add_custom_field:
title_html: "Add new column to team <strong>%{organization}</strong>"
create: "Add new column"
modal_delete_custom_field:
title: "Delete a column"
message: "Are you sure you wish to permanently delete selected column %{cf}? This action is irreversible."
alert_heading: "Deleting a column has following consequences:"
alert_line_1: "you will lose information in this column for %{nr} samples;"
alert_line_2: "the column will be deleted for all team members."
delete: "Delete column"
modal_add_new_sample_group:
title_html: "Add new sample group to team <strong>%{organization}</strong>"
create: "Add new sample group"

View file

@ -79,7 +79,9 @@ Rails.application.routes.draw do
get 'sample_group_element', to: 'sample_groups#sample_group_element'
get 'destroy_confirmation', to: 'sample_groups#destroy_confirmation'
end
resources :custom_fields, only: [:create]
resources :custom_fields, only: [:create, :update, :destroy] do
get 'destroy_html'
end
member do
post 'parse_sheet'
post 'import_samples'