Merge branch 'improved-sample-table' of https://github.com/biosistemika/scinote-web into zd_improved_samples_table_SCI_774_775_778_777

This commit is contained in:
zmagod 2016-12-12 15:52:25 +01:00
commit 8dd8a7e7cd
15 changed files with 507 additions and 97 deletions

View file

@ -26,7 +26,8 @@ function dataTableInit() {
processing: true,
serverSide: true,
colReorder: {
fixedColumnsLeft: 2
fixedColumnsLeft: 2,
realtime: false
},
destroy: true,
ajax: {
@ -89,55 +90,54 @@ function dataTableInit() {
},
preDrawCallback: function() {
animateSpinner(this);
$(".sample_info").off("click");
$('.sample_info').off('click');
},
stateLoadCallback: function (settings) {
stateLoadCallback: function(settings) {
// Send an Ajax request to the server to get the data. Note that
// this is a synchronous request since the data is expected back from the
// function
var org = $("#samples").attr("data-organization-id")
var user = $("#samples").attr("data-user-id")
var org = $('#samples').attr('data-organization-id');
var user = $('#samples').attr('data-user-id');
$.ajax( {
url: '/state_load/'+org+'/'+user,
$.ajax({
url: '/state_load/' + org + '/' + user,
data: {org: org},
async: false,
dataType: "json",
type: "POST",
success: function (json) {
dataType: 'json',
type: 'POST',
success: function(json) {
myData = json.state;
}
} );
return myData
});
return myData;
},
stateSaveCallback: function (settings, data) {
stateSaveCallback: function(settings, data) {
// Send an Ajax request to the server with the state object
var org = $("#samples").attr("data-organization-id")
var user = $("#samples").attr("data-user-id")
var org = $('#samples').attr('data-organization-id');
var user = $('#samples').attr('data-user-id');
// Save correct data
if (loadFirstTime == true) {
data = myData;
}
$.ajax( {
url: '/state_save/'+org+'/'+user,
$.ajax({
url: '/state_save/' + org + '/' + user,
data: {org: org, state: data},
dataType: "json",
type: "POST"
} );
dataType: 'json',
type: 'POST'
});
loadFirstTime = false;
},
fnInitComplete: function(oSettings, json) {
// Reload correct column order and visibility (if you refresh page)
oSettings._colReorder.fnOrder(myData.ColReorder);
for (var i = 0; i < table.columns()[0].length; i++) {
var visibility = myData.columns[i].visible;
if (typeof(visibility) === "string"){
var visibility = (visibility === "true");
if (typeof (visibility) === 'string') {
visibility = (visibility === 'true');
}
table.column(i).visible(visibility);
}
oSettings._colReorder.fnOrder(myData.ColReorder);
}
});
@ -664,7 +664,7 @@ function onClickAddSample() {
else if ($(th).attr("id") == "sample-type") {
var colIndex = getColumnIndex("#sample-type")
if (colIndex) {
var selectType = createSampleTypeSelect(data["sample_types"], -1);
var selectType = createSampleTypeSelect(data["sample_types"]);
var td = createTdElement("");
td.appendChild(selectType[0]);
tr.appendChild(td);
@ -673,7 +673,7 @@ function onClickAddSample() {
else if ($(th).attr("id") == "sample-group") {
var colIndex = getColumnIndex("#sample-group")
if (colIndex) {
var selectGroup = createSampleGroupSelect(data["sample_groups"], -1);
var selectGroup = createSampleGroupSelect(data["sample_groups"]);
var td = createTdElement("");
td.appendChild(selectGroup[0]);
tr.appendChild(td);
@ -736,14 +736,21 @@ function createTdElement(content) {
/**
* Creates select dropdown for sample type
* @param data List of sample types
* @param selected Selected sample type id
* @param {Object[]} data List of sample types
* @param {number} selected Selected sample type id
* @return {Object} select dropdown
*/
function createSampleTypeSelect(data, selected) {
selected = _.isUndefined(selected) ? 1 : selected + 1;
var $selectType = $('<select></select>')
.attr('name', 'sample_type_id').addClass('show-tick');
var $option = $('<option></option>')
var $option = $("<option href='/organizations/1/sample_types'></option>")
.attr('value', -2)
.text(I18n.t('samples.table.add_sample_type'));
$selectType.append($option);
$option = $('<option></option>')
.attr('value', -1).text(I18n.t('samples.table.no_type'))
$selectType.append($option);
@ -752,7 +759,7 @@ function createSampleTypeSelect(data, selected) {
.attr('value', val.id).text(val.name);
$selectType.append($option);
});
$selectType.val(selected);
$selectType.makeDropdownOptionsLinks(selected, 'add-mode');
return $selectType;
}
@ -762,11 +769,15 @@ function createSampleTypeSelect(data, selected) {
* @param selected Selected sample group id
*/
function createSampleGroupSelect(data, selected) {
selected = _.isUndefined(selected) ? 1 : selected + 1;
var $selectGroup = $('<select></select>')
.attr('name', 'sample_group_id').addClass('show-tick');
var $span = $("<span></span>").addClass('glyphicon glyphicon-asterisk');
var $option = $('<option></option>')
var $option = $("<option href='/organizations/1/sample_groups'></option>")
.text(I18n.t('samples.table.add_sample_group'));
$selectGroup.append($option);
$option = $('<option></option>')
.attr('value', -1).text(I18n.t('samples.table.no_group'))
.attr('data-icon', 'glyphicon glyphicon-asterisk');
$selectGroup.append($option);
@ -780,7 +791,7 @@ function createSampleGroupSelect(data, selected) {
$selectGroup.append($option);
});
$selectGroup.val(selected);
$selectGroup.makeDropdownOptionsLinks(selected, 'add-mode');
return $selectGroup;
}
@ -802,10 +813,15 @@ function changeToEditMode() {
table.button(0).enable(false);
}
/*
* 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
@ -837,11 +853,14 @@ 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);
table.colReorder.reset();
// Remove all event handlers as we re-initialize them later with
// new table
$('#samples').off();
@ -852,8 +871,9 @@ function changeToEditMode() {
$('div.toolbarButtons').appendTo('div.samples-table');
$('div.toolbarButtons').hide();
table = dataTableInit();
table.colReorder.order(colOrder, true);
loadColumnsNames();
table.on('init.dt', function() {
loadColumnsNames();
});
},
url: url
});
@ -900,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);
}
});
@ -971,12 +1010,202 @@ 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();
loadColumnsNames();
initSorting();
toggleColumnVisibility();
initEditColumns();
initDeleteColumns();
});
$('#samples-columns-dropdown').on('show.bs.dropdown', function() {
loadColumnsNames();
});
}

View file

@ -233,6 +233,16 @@
});
}
/**
* Opens adding mode when redirected from samples page, when clicking link for
* adding sample type or group link
*/
function sampleTypeGroupEditMode() {
if (getParam('add-mode')) {
$('#create-resource').click();
}
}
function initSampleTypesGroups() {
showNewSampleTypeGroupForm();
newSampleTypeFormCancel();
@ -245,6 +255,7 @@
initSampleGroupColor();
bindNewSampleGroupAction();
appendCarretToColorPickerDropdown();
sampleTypeGroupEditMode();
}
// initialize sample types/groups actions

View file

@ -1,28 +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);
});
// Create import samples ajax
$("#modal-import-samples").on("show.bs.modal", function(event) {
formGroup = $(this).find(".form-group");

View file

@ -215,3 +215,32 @@ function initPageTutorialSteps(pageFirstStepN, pageLastStepN, nextPagePath,
});
}
}
/**
* Add redirection links on dropdown elements. You must specify 'href'
* attribute yourself, and the dropdown elments which don't have them, will get
* '#' by default.
* @param {number} selectedIdx Index of element to be selected
* @param {string} urlParam URL parameter to pass to the link URLs
* @return {Object} This
*/
$.fn.makeDropdownOptionsLinks = function(selectedIdx, urlParam) {
selectedIdx = _.isUndefined(selectedIdx) ? 1 : selectedIdx;
$(this).change(function() {
window.location.href = addParam($(this).find('option:selected')
.attr('href'), urlParam);
});
$(this).find('option')
.each(function() {
if ($(this).is('[href]')) {
$(this).addClass('link-look');
} else {
$(this).attr('href', '#');
}
})
.eq(selectedIdx).attr('selected', true);
return this;
};

View file

@ -35,6 +35,9 @@ $color-mojo: #cf4b48;
$color-apple-blossom: #a94442;
$color-milano-red: #a70b05;
// Colors for specific intents
$color-visited-link: #23527c;
//==============================================================================
// Other
//==============================================================================

View file

@ -235,6 +235,18 @@ body {
a {
color: $color-theme-primary;
// Override to make link look and behave as a normal link (e.g. used for
// elements which have links)
&.link-look {
color: $color-theme-primary !important;
:hover,
:focus {
color: $color-visited-link !important;
text-decoration: underline !important;
}
}
}
.jumbotron {
@ -1611,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;
@ -1628,7 +1631,7 @@ textarea.textarea-sm {
width: 10px;
}
.grippy-img {
li:not(.editing) .grippy-img {
background: url(asset-path('custom/grippy.png'));
}
@ -1639,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;
}
@ -1646,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

@ -14,7 +14,7 @@ class SamplesTable < ActiveRecord::Base
index = org_status['columns'].count
org_status['columns'][index] = SampleDatatable::
SAMPLES_TABLE_DEFAULT_STATE['columns'].first
org_status['ColReorder'] << index
org_status['ColReorder'] << index.to_s
samples_table.first.update(status: org_status)
end

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,5 +1,7 @@
<%= render partial: "samples/import_samples_modal" %>
<%= render partial: "samples/delete_samples_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>
@ -138,7 +140,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"
@ -847,6 +854,8 @@ en:
added_by: "Added by"
no_group: "No sample group"
no_type: "No sample type"
add_sample_type: "Add new sample type"
add_sample_group: "Add new sample group"
new:
head_title: "%{organization} | Add new sample"
title: "Add new sample to team %{organization}"

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'