From aea679ec11edbdc86b360e29c9a1c26c63260eb0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Zrim=C5=A1ek?= Date: Thu, 8 Dec 2016 15:02:49 +0100 Subject: [PATCH 01/12] Added links for creating new sample type and group when creating new sample. [fixes SCI-701] --- .../javascripts/samples/sample_datatable.js | 19 +++++++++++++---- app/assets/javascripts/sitewide/utils.js | 21 +++++++++++++++++++ config/locales/en.yml | 2 ++ 3 files changed, 38 insertions(+), 4 deletions(-) diff --git a/app/assets/javascripts/samples/sample_datatable.js b/app/assets/javascripts/samples/sample_datatable.js index fe7725be7..5a50c8a4f 100644 --- a/app/assets/javascripts/samples/sample_datatable.js +++ b/app/assets/javascripts/samples/sample_datatable.js @@ -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); @@ -740,9 +740,15 @@ function createTdElement(content) { * @param selected Selected sample type id */ function createSampleTypeSelect(data, selected) { + selected = _.isUndefined(selected) ? 1 : selected + 1; + var $selectType = $('') .attr('name', 'sample_type_id').addClass('show-tick'); + var $option = $("") + .attr('value', -2) + .text(I18n.t('samples.table.add_sample_type')); + $selectType.append($option); var $option = $('') .attr('value', -1).text(I18n.t('samples.table.no_type')) $selectType.append($option); @@ -752,7 +758,7 @@ function createSampleTypeSelect(data, selected) { .attr('value', val.id).text(val.name); $selectType.append($option); }); - $selectType.val(selected); + $selectType.makeDropdownOptionsLinks(selected); return $selectType; } @@ -762,9 +768,14 @@ function createSampleTypeSelect(data, selected) { * @param selected Selected sample group id */ function createSampleGroupSelect(data, selected) { + selected = _.isUndefined(selected) ? 1 : selected + 1; + var $selectGroup = $('') .attr('name', 'sample_group_id').addClass('show-tick'); + var $option = $("") + .text(I18n.t('samples.table.add_sample_group')); + $selectGroup.append($option); var $span = $("").addClass('glyphicon glyphicon-asterisk'); var $option = $('') .attr('value', -1).text(I18n.t('samples.table.no_group')) @@ -780,7 +791,7 @@ function createSampleGroupSelect(data, selected) { $selectGroup.append($option); }); - $selectGroup.val(selected); + $selectGroup.makeDropdownOptionsLinks(selected); return $selectGroup; } diff --git a/app/assets/javascripts/sitewide/utils.js b/app/assets/javascripts/sitewide/utils.js index f42f515c2..621762258 100644 --- a/app/assets/javascripts/sitewide/utils.js +++ b/app/assets/javascripts/sitewide/utils.js @@ -215,3 +215,24 @@ function initPageTutorialSteps(pageFirstStepN, pageLastStepN, nextPagePath, }); } } + +/** + * Add redirection links on dropdown elements. + * @param {[type]} selectedIdx Index of element to be selected. + * @return {Object} This + */ +$.fn.makeDropdownOptionsLinks = function(selectedIdx) { + selectedIdx = _.isUndefined(selectedIdx) ? 1 : selectedIdx; + $(this).change(function() { + location = $(this).find('option:selected').attr('href'); + }); + $(this).find('option') + .each(function(i) { + if (!$(this).is("[href]")) { + $(this).attr('href', '#'); + } + }) + .eq(selectedIdx).attr('selected', true); + + return this; +} diff --git a/config/locales/en.yml b/config/locales/en.yml index 7289a9ae4..4186c603d 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -847,6 +847,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}" From da630ac2cbc63101b95233de735c5a4036c70f11 Mon Sep 17 00:00:00 2001 From: Luka Murn Date: Tue, 29 Nov 2016 11:43:18 +0100 Subject: [PATCH 02/12] Make edit, delete buttons clickable & hide them if neccesary --- .../javascripts/samples/sample_datatable.js | 27 ++++++++++++++----- app/assets/stylesheets/themes/scinote.scss | 16 ++++++++++- app/helpers/permission_helper.rb | 10 +++++++ app/views/shared/_samples.html.erb | 8 +++++- 4 files changed, 52 insertions(+), 9 deletions(-) diff --git a/app/assets/javascripts/samples/sample_datatable.js b/app/assets/javascripts/samples/sample_datatable.js index fe7725be7..1e0b18bd3 100644 --- a/app/assets/javascripts/samples/sample_datatable.js +++ b/app/assets/javascripts/samples/sample_datatable.js @@ -802,6 +802,9 @@ function changeToEditMode() { table.button(0).enable(false); } +/* + * Sample columns dropdown + */ (function(table) { 'use strict'; @@ -900,15 +903,25 @@ 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 = '
  • ' + - el.innerText + ' ' + - ' ' + - ' ' + - '' + - '
  • '; + var editClass = (editable) ? '' : 'disabled'; + var delClass = (deletable) ? '' : 'disabled'; + var html = + '
  • ' + + ' ' + + '' + el.innerText + ' ' + + '' + + ' ' + + '' + + ' ' + + '' + + '' + + '' + + '
  • '; dropdownList.append(html); } }); diff --git a/app/assets/stylesheets/themes/scinote.scss b/app/assets/stylesheets/themes/scinote.scss index 9014f3ef1..1c7a7b52e 100644 --- a/app/assets/stylesheets/themes/scinote.scss +++ b/app/assets/stylesheets/themes/scinote.scss @@ -1612,7 +1612,6 @@ textarea.textarea-sm { } .vis { - cursor: pointer; margin-right: 5px; } @@ -1620,6 +1619,9 @@ textarea.textarea-sm { margin-right: 5px; } + .edit { + } + .grippy { background-repeat: none; display: inline-block; @@ -1646,5 +1648,17 @@ textarea.textarea-sm { .controls { display: inline-block; margin-top: 5px; + + span { + cursor: pointer; + + &.disabled { + visibility: hidden; + } + + &:hover { + color: $color-emperor; + } + } } } diff --git a/app/helpers/permission_helper.rb b/app/helpers/permission_helper.rb index 43972e678..175afa9e7 100644 --- a/app/helpers/permission_helper.rb +++ b/app/helpers/permission_helper.rb @@ -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) diff --git a/app/views/shared/_samples.html.erb b/app/views/shared/_samples.html.erb index 5eca59032..d3571b3a0 100644 --- a/app/views/shared/_samples.html.erb +++ b/app/views/shared/_samples.html.erb @@ -136,7 +136,13 @@ <%= t("samples.table.added_on") %> <%= t("samples.table.added_by") %> <% all_custom_fields.each do |cf| %> - <%= cf.name %> + + <%= 'data-deletable' if can_delete_custom_field(cf) %> + > + <%= cf.name %> + <% end %> From 54ebdbb06cd114d35cdce69abfc03ad4f8d669e1 Mon Sep 17 00:00:00 2001 From: Luka Murn Date: Tue, 29 Nov 2016 14:01:15 +0100 Subject: [PATCH 03/12] Update edit mode JS & CSS --- .../javascripts/samples/sample_datatable.js | 64 +++++++++++++++++++ app/assets/stylesheets/themes/scinote.scss | 41 ++++++++---- 2 files changed, 93 insertions(+), 12 deletions(-) diff --git a/app/assets/javascripts/samples/sample_datatable.js b/app/assets/javascripts/samples/sample_datatable.js index 1e0b18bd3..4bad352b1 100644 --- a/app/assets/javascripts/samples/sample_datatable.js +++ b/app/assets/javascripts/samples/sample_datatable.js @@ -809,6 +809,7 @@ function changeToEditMode() { 'use strict'; var dropdownList = $('#samples-columns-list'); + var editMode = false; function createNewColumn() { // Make an Ajax request to custom_fields_controller @@ -914,7 +915,10 @@ function changeToEditMode() { '
  • ' + ' ' + '' + el.innerText + ' ' + + '' + '' + + '' + + '' + ' ' + '' + ' ' + @@ -984,12 +988,72 @@ function changeToEditMode() { }); } + function editColumns() { + 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 + editMode = false; + } + + // On edit buttons click + var editBtns = dropdownList.find('.edit:not(.disabled)'); + editBtns.on('click', 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 + editMode = 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 cancel buttons click + var cancelBtns = dropdownList.find('.cancel'); + cancelBtns.on('click', function(event) { + event.stopPropagation(); + var self = $(this); + var li = self.closest('li'); + + editMode = 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 + }); + } + // initialze dropdown after the table is loaded function initDropdown() { table.on('init.dt', function() { initNewColumnForm(); loadColumnsNames(); initSorting(); + toggleColumnVisibility(); + editColumns(); }); } diff --git a/app/assets/stylesheets/themes/scinote.scss b/app/assets/stylesheets/themes/scinote.scss index 1c7a7b52e..1fc8d143a 100644 --- a/app/assets/stylesheets/themes/scinote.scss +++ b/app/assets/stylesheets/themes/scinote.scss @@ -1611,17 +1611,6 @@ textarea.textarea-sm { margin-left: 10px; } - .vis { - margin-right: 5px; - } - - .edit { - margin-right: 5px; - } - - .edit { - } - .grippy { background-repeat: none; display: inline-block; @@ -1630,7 +1619,7 @@ textarea.textarea-sm { width: 10px; } - .grippy-img { + li:not(.editing) .grippy-img { background: url(asset-path('custom/grippy.png')); } @@ -1641,6 +1630,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; } @@ -1660,5 +1657,25 @@ textarea.textarea-sm { color: $color-emperor; } } + + .ok { + color: $color-theme-secondary; + margin-right: 5px; + } + + .cancel { + margin-right: 28px; + } + + .vis { + margin-right: 5px; + } + + .edit { + margin-right: 5px; + } + + .del { + } } } From 754c90b6126f107b7a45d15606591649d9ee83c0 Mon Sep 17 00:00:00 2001 From: Luka Murn Date: Tue, 29 Nov 2016 15:05:18 +0100 Subject: [PATCH 04/12] Edit functionality now works --- .../javascripts/samples/sample_datatable.js | 41 ++++++++++++++++- app/controllers/custom_fields_controller.rb | 45 ++++++++++++++----- config/routes.rb | 2 +- 3 files changed, 73 insertions(+), 15 deletions(-) diff --git a/app/assets/javascripts/samples/sample_datatable.js b/app/assets/javascripts/samples/sample_datatable.js index 4bad352b1..8ccbf8c1d 100644 --- a/app/assets/javascripts/samples/sample_datatable.js +++ b/app/assets/javascripts/samples/sample_datatable.js @@ -808,6 +808,7 @@ function changeToEditMode() { (function(table) { 'use strict'; + var dropdown = $('#samples-columns-dropdown'); var dropdownList = $('#samples-columns-list'); var editMode = false; @@ -912,7 +913,11 @@ function changeToEditMode() { var editClass = (editable) ? '' : 'disabled'; var delClass = (deletable) ? '' : 'disabled'; var html = - '
  • ' + + '
  • ' + ' ' + '' + el.innerText + ' ' + '' + @@ -1004,7 +1009,6 @@ function changeToEditMode() { cancelEditMode(); - var self = $(this); var li = self.closest('li'); var text = li.find('.text'); @@ -1029,6 +1033,39 @@ function changeToEditMode() { textEdit.focus(); }); + // On hiding dropdown, cancel edit mode throughout dropdown + dropdown.on('hidden.bs.dropdown', function() { + cancelEditMode(); + }); + + // On ok buttons click + var okBtns = dropdownList.find('.ok'); + okBtns.on('click', function(event) { + event.stopPropagation(); + + var self = $(this); + var li = self.closest('li'); + var id = li.attr('data-id'); + var text = li.find('.text'); + var textEdit = li.find('.text-edit'); + var newName = textEdit.val().trim(); + + $.ajax({ + url: '/organizations/1/custom_fields/' + id, + 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 cancel buttons click var cancelBtns = dropdownList.find('.cancel'); cancelBtns.on('click', function(event) { diff --git a/app/controllers/custom_fields_controller.rb b/app/controllers/custom_fields_controller.rb index 19b6d1dba..3edd7696e 100644 --- a/app/controllers/custom_fields_controller.rb +++ b/app/controllers/custom_fields_controller.rb @@ -1,6 +1,8 @@ class CustomFieldsController < ApplicationController + before_action :load_vars, only: :update before_action :load_vars_nested, only: [:create] before_action :check_create_permissions, only: [:create] + before_action :check_update_permissions, only: :update def create @custom_field = CustomField.new(custom_field_params) @@ -9,12 +11,13 @@ 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 }, - status: :ok } + status: :ok + end else format.json do render json: @custom_field.errors.to_json, @@ -24,20 +27,38 @@ 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 + private + + def load_vars + @custom_field = CustomField.find_by_id(params[:id]) + 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 - unless can_create_custom_field_in_organization(@organization) - render_403 - end + 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 custom_field_params diff --git a/config/routes.rb b/config/routes.rb index 83f6e2158..7be3f4d75 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -79,7 +79,7 @@ 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] member do post 'parse_sheet' post 'import_samples' From 7e32c13d2776d3f6b5c78c5ac67f57fe15063071 Mon Sep 17 00:00:00 2001 From: Luka Murn Date: Thu, 8 Dec 2016 11:38:06 +0100 Subject: [PATCH 05/12] Fix edit & delete functionality so it works with draggable --- .../javascripts/samples/sample_datatable.js | 25 ++++++++----------- 1 file changed, 11 insertions(+), 14 deletions(-) diff --git a/app/assets/javascripts/samples/sample_datatable.js b/app/assets/javascripts/samples/sample_datatable.js index 8ccbf8c1d..0610f615e 100644 --- a/app/assets/javascripts/samples/sample_datatable.js +++ b/app/assets/javascripts/samples/sample_datatable.js @@ -810,7 +810,7 @@ function changeToEditMode() { var dropdown = $('#samples-columns-dropdown'); var dropdownList = $('#samples-columns-list'); - var editMode = false; + var columnEditMode = false; function createNewColumn() { // Make an Ajax request to custom_fields_controller @@ -842,7 +842,7 @@ function changeToEditMode() { $('#samples').data('num-columns', $('#samples').data('num-columns') + 1); originalHeader.append( - '' + + '' + data.name + ''); var colOrder = table.colReorder.order(); colOrder.push(colOrder.length); @@ -993,18 +993,17 @@ function changeToEditMode() { }); } - function editColumns() { + 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 - editMode = false; + columnEditMode = false; } - // On edit buttons click - var editBtns = dropdownList.find('.edit:not(.disabled)'); - editBtns.on('click', function(event) { + // On edit buttons click (bind onto master dropdown list) + dropdownList.on('click', '.edit:not(.disabled)', function(event) { event.stopPropagation(); cancelEditMode(); @@ -1017,7 +1016,7 @@ function changeToEditMode() { var controlsEdit = li.find('.controls .ok,.cancel'); // Toggle edit mode - editMode = true; + columnEditMode = true; li.addClass('editing'); // Set the text-edit's value @@ -1039,8 +1038,7 @@ function changeToEditMode() { }); // On ok buttons click - var okBtns = dropdownList.find('.ok'); - okBtns.on('click', function(event) { + dropdownList.on('click', '.ok', function(event) { event.stopPropagation(); var self = $(this); @@ -1067,13 +1065,12 @@ function changeToEditMode() { }); // On cancel buttons click - var cancelBtns = dropdownList.find('.cancel'); - cancelBtns.on('click', function(event) { + dropdownList.on('click', '.cancel', function(event) { event.stopPropagation(); var self = $(this); var li = self.closest('li'); - editMode = false; + columnEditMode = false; li.removeClass('editing'); li.find('.text-edit').hide(); @@ -1090,7 +1087,7 @@ function changeToEditMode() { loadColumnsNames(); initSorting(); toggleColumnVisibility(); - editColumns(); + initEditColumns(); }); } From 1a8b141b95bb1889e481fad099f5ee802009cf32 Mon Sep 17 00:00:00 2001 From: Luka Murn Date: Fri, 9 Dec 2016 11:58:10 +0100 Subject: [PATCH 06/12] Add delete custom field functionality --- .../javascripts/samples/sample_datatable.js | 145 +++++++++++++++--- app/controllers/custom_fields_controller.rb | 46 +++++- app/models/custom_field.rb | 2 +- .../_delete_custom_field_modal.html.erb | 16 ++ .../_delete_custom_field_modal_body.html.erb | 12 ++ app/views/shared/_samples.html.erb | 3 + config/locales/en.yml | 7 + config/routes.rb | 4 +- 8 files changed, 209 insertions(+), 26 deletions(-) create mode 100644 app/views/samples/_delete_custom_field_modal.html.erb create mode 100644 app/views/samples/_delete_custom_field_modal_body.html.erb diff --git a/app/assets/javascripts/samples/sample_datatable.js b/app/assets/javascripts/samples/sample_datatable.js index 0610f615e..65be4cc37 100644 --- a/app/assets/javascripts/samples/sample_datatable.js +++ b/app/assets/javascripts/samples/sample_datatable.js @@ -842,7 +842,11 @@ function changeToEditMode() { $('#samples').data('num-columns', $('#samples').data('num-columns') + 1); originalHeader.append( - '' + + '' + data.name + ''); var colOrder = table.colReorder.order(); colOrder.push(colOrder.length); @@ -916,6 +920,8 @@ function changeToEditMode() { '
  • ' + ' ' + @@ -1002,6 +1008,29 @@ function changeToEditMode() { 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(); @@ -1040,28 +1069,19 @@ function changeToEditMode() { // On ok buttons click dropdownList.on('click', '.ok', function(event) { event.stopPropagation(); - var self = $(this); var li = self.closest('li'); - var id = li.attr('data-id'); - var text = li.find('.text'); - var textEdit = li.find('.text-edit'); - var newName = textEdit.val().trim(); + editColumn(li); + }); - $.ajax({ - url: '/organizations/1/custom_fields/' + id, - 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 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 @@ -1080,6 +1100,90 @@ function changeToEditMode() { }); } + 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() { @@ -1088,6 +1192,7 @@ function changeToEditMode() { initSorting(); toggleColumnVisibility(); initEditColumns(); + initDeleteColumns(); }); } diff --git a/app/controllers/custom_fields_controller.rb b/app/controllers/custom_fields_controller.rb index 3edd7696e..f549e60a2 100644 --- a/app/controllers/custom_fields_controller.rb +++ b/app/controllers/custom_fields_controller.rb @@ -1,8 +1,9 @@ class CustomFieldsController < ApplicationController - before_action :load_vars, only: :update - 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) @@ -14,7 +15,13 @@ class CustomFieldsController < ApplicationController 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 end @@ -41,10 +48,37 @@ class CustomFieldsController < ApplicationController end end + 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 @@ -61,6 +95,10 @@ class CustomFieldsController < ApplicationController 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 diff --git a/app/models/custom_field.rb b/app/models/custom_field.rb index e06ac8baa..f43975f57 100644 --- a/app/models/custom_field.rb +++ b/app/models/custom_field.rb @@ -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 diff --git a/app/views/samples/_delete_custom_field_modal.html.erb b/app/views/samples/_delete_custom_field_modal.html.erb new file mode 100644 index 000000000..61ab9bc6d --- /dev/null +++ b/app/views/samples/_delete_custom_field_modal.html.erb @@ -0,0 +1,16 @@ + \ No newline at end of file diff --git a/app/views/samples/_delete_custom_field_modal_body.html.erb b/app/views/samples/_delete_custom_field_modal_body.html.erb new file mode 100644 index 000000000..0df63cde8 --- /dev/null +++ b/app/views/samples/_delete_custom_field_modal_body.html.erb @@ -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| %> +

    <%= t("samples.modal_delete_custom_field.message", cf: @custom_field.name) %>

    + +<% end %> \ No newline at end of file diff --git a/app/views/shared/_samples.html.erb b/app/views/shared/_samples.html.erb index d3571b3a0..2a2a790e0 100644 --- a/app/views/shared/_samples.html.erb +++ b/app/views/shared/_samples.html.erb @@ -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" %> @@ -140,6 +141,8 @@ 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 %> diff --git a/config/locales/en.yml b/config/locales/en.yml index 7289a9ae4..d9accac59 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -832,6 +832,13 @@ en: modal_add_custom_field: title_html: "Add new column to team %{organization}" 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 %{organization}" create: "Add new sample group" diff --git a/config/routes.rb b/config/routes.rb index 7be3f4d75..8bf857f52 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -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, :update] + resources :custom_fields, only: [:create, :update, :destroy] do + get 'destroy_html' + end member do post 'parse_sheet' post 'import_samples' From 8a55ef29e4a9a48ef4730f74b655f7c3a473c88a Mon Sep 17 00:00:00 2001 From: Luka Murn Date: Fri, 9 Dec 2016 13:16:12 +0100 Subject: [PATCH 07/12] Remove unused code Closes SCI-711. --- app/assets/javascripts/samples/samples.js | 27 ----------------------- 1 file changed, 27 deletions(-) diff --git a/app/assets/javascripts/samples/samples.js b/app/assets/javascripts/samples/samples.js index 3a97b74db..53488d049 100644 --- a/app/assets/javascripts/samples/samples.js +++ b/app/assets/javascripts/samples/samples.js @@ -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 From 274fd63a412428679403f12c403ac3c919426f4e Mon Sep 17 00:00:00 2001 From: Oleksii Kriuchykhin Date: Fri, 9 Dec 2016 15:21:26 +0100 Subject: [PATCH 08/12] Initial code commit [SCI-779] --- .../javascripts/samples/sample_datatable.js | 48 +++++++++---------- app/models/samples_table.rb | 2 +- 2 files changed, 24 insertions(+), 26 deletions(-) diff --git a/app/assets/javascripts/samples/sample_datatable.js b/app/assets/javascripts/samples/sample_datatable.js index fe7725be7..245eca35d 100644 --- a/app/assets/javascripts/samples/sample_datatable.js +++ b/app/assets/javascripts/samples/sample_datatable.js @@ -89,43 +89,42 @@ 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) { @@ -133,8 +132,8 @@ function dataTableInit() { 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); } @@ -802,6 +801,7 @@ function changeToEditMode() { table.button(0).enable(false); } +// Samples table columns dropdown handling code (function(table) { 'use strict'; @@ -841,7 +841,6 @@ function changeToEditMode() { data.name + ''); 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 +851,7 @@ function changeToEditMode() { $('div.toolbarButtons').appendTo('div.samples-table'); $('div.toolbarButtons').hide(); table = dataTableInit(); - table.colReorder.order(colOrder, true); - loadColumnsNames(); + initDropdown(); }, url: url }); diff --git a/app/models/samples_table.rb b/app/models/samples_table.rb index 5d4f92fde..40ce10181 100644 --- a/app/models/samples_table.rb +++ b/app/models/samples_table.rb @@ -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 From 460ca067a2576b271acd1f940aaaeb11aa788e9e Mon Sep 17 00:00:00 2001 From: Oleksii Kriuchykhin Date: Fri, 9 Dec 2016 16:07:41 +0100 Subject: [PATCH 09/12] Fix adding new samples columns [SCI-779] --- app/assets/javascripts/samples/sample_datatable.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/assets/javascripts/samples/sample_datatable.js b/app/assets/javascripts/samples/sample_datatable.js index 245eca35d..5646a9cc4 100644 --- a/app/assets/javascripts/samples/sample_datatable.js +++ b/app/assets/javascripts/samples/sample_datatable.js @@ -851,7 +851,9 @@ function changeToEditMode() { $('div.toolbarButtons').appendTo('div.samples-table'); $('div.toolbarButtons').hide(); table = dataTableInit(); - initDropdown(); + table.on('init.dt', function() { + loadColumnsNames(); + }); }, url: url }); From 0b12c87b09ecd3c66ef8de4cae6cef3cabcfe7e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Zrim=C5=A1ek?= Date: Fri, 9 Dec 2016 11:28:46 +0100 Subject: [PATCH 10/12] Restyled new sample type and group dropdown links. Some minor refactoring of related code and docs. [Fixes SCI-701] --- .../javascripts/samples/sample_datatable.js | 10 +++++----- app/assets/javascripts/sitewide/utils.js | 16 +++++++++++----- app/assets/stylesheets/constants.scss | 3 +++ app/assets/stylesheets/themes/scinote.scss | 12 ++++++++++++ 4 files changed, 31 insertions(+), 10 deletions(-) diff --git a/app/assets/javascripts/samples/sample_datatable.js b/app/assets/javascripts/samples/sample_datatable.js index 5a50c8a4f..b6f914518 100644 --- a/app/assets/javascripts/samples/sample_datatable.js +++ b/app/assets/javascripts/samples/sample_datatable.js @@ -736,8 +736,9 @@ 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; @@ -749,7 +750,7 @@ function createSampleTypeSelect(data, selected) { .attr('value', -2) .text(I18n.t('samples.table.add_sample_type')); $selectType.append($option); - var $option = $('') + $option = $('') .attr('value', -1).text(I18n.t('samples.table.no_type')) $selectType.append($option); @@ -776,8 +777,7 @@ function createSampleGroupSelect(data, selected) { var $option = $("") .text(I18n.t('samples.table.add_sample_group')); $selectGroup.append($option); - var $span = $("").addClass('glyphicon glyphicon-asterisk'); - var $option = $('') + $option = $('') .attr('value', -1).text(I18n.t('samples.table.no_group')) .attr('data-icon', 'glyphicon glyphicon-asterisk'); $selectGroup.append($option); diff --git a/app/assets/javascripts/sitewide/utils.js b/app/assets/javascripts/sitewide/utils.js index 621762258..ccef397d8 100644 --- a/app/assets/javascripts/sitewide/utils.js +++ b/app/assets/javascripts/sitewide/utils.js @@ -217,22 +217,28 @@ function initPageTutorialSteps(pageFirstStepN, pageLastStepN, nextPagePath, } /** - * Add redirection links on dropdown elements. - * @param {[type]} selectedIdx Index of element to be selected. + * 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 * @return {Object} This */ $.fn.makeDropdownOptionsLinks = function(selectedIdx) { selectedIdx = _.isUndefined(selectedIdx) ? 1 : selectedIdx; + $(this).change(function() { location = $(this).find('option:selected').attr('href'); }); + $(this).find('option') - .each(function(i) { - if (!$(this).is("[href]")) { + .each(function() { + if ($(this).is('[href]')) { + $(this).addClass('link-look'); + } else { $(this).attr('href', '#'); } }) .eq(selectedIdx).attr('selected', true); return this; -} +}; diff --git a/app/assets/stylesheets/constants.scss b/app/assets/stylesheets/constants.scss index 838ca5e31..b887e7c14 100644 --- a/app/assets/stylesheets/constants.scss +++ b/app/assets/stylesheets/constants.scss @@ -35,6 +35,9 @@ $color-mojo: #cf4b48; $color-apple-blossom: #a94442; $color-milano-red: #a70b05; +// Colors for specific intents +$color-visited-link: #23527c; + //============================================================================== // Other //============================================================================== diff --git a/app/assets/stylesheets/themes/scinote.scss b/app/assets/stylesheets/themes/scinote.scss index 9014f3ef1..d584ba11e 100644 --- a/app/assets/stylesheets/themes/scinote.scss +++ b/app/assets/stylesheets/themes/scinote.scss @@ -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 { From 54de13de828499add95ced1591e8c76c287d56a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Zrim=C5=A1ek?= Date: Fri, 9 Dec 2016 17:22:09 +0100 Subject: [PATCH 11/12] Upon redirection when clicking the new sample type or group link, you're automatically in adding mode. [Fixes SCI-701] --- app/assets/javascripts/samples/sample_datatable.js | 4 ++-- .../javascripts/samples/sample_types_groups.js | 12 ++++++++++++ app/assets/javascripts/sitewide/utils.js | 6 ++++-- 3 files changed, 18 insertions(+), 4 deletions(-) diff --git a/app/assets/javascripts/samples/sample_datatable.js b/app/assets/javascripts/samples/sample_datatable.js index b6f914518..9e27d3d8f 100644 --- a/app/assets/javascripts/samples/sample_datatable.js +++ b/app/assets/javascripts/samples/sample_datatable.js @@ -759,7 +759,7 @@ function createSampleTypeSelect(data, selected) { .attr('value', val.id).text(val.name); $selectType.append($option); }); - $selectType.makeDropdownOptionsLinks(selected); + $selectType.makeDropdownOptionsLinks(selected, 'add-mode'); return $selectType; } @@ -791,7 +791,7 @@ function createSampleGroupSelect(data, selected) { $selectGroup.append($option); }); - $selectGroup.makeDropdownOptionsLinks(selected); + $selectGroup.makeDropdownOptionsLinks(selected, 'add-mode'); return $selectGroup; } diff --git a/app/assets/javascripts/samples/sample_types_groups.js b/app/assets/javascripts/samples/sample_types_groups.js index 386a31c59..ff1fd89f7 100644 --- a/app/assets/javascripts/samples/sample_types_groups.js +++ b/app/assets/javascripts/samples/sample_types_groups.js @@ -4,6 +4,7 @@ function showNewSampleTypeGroupForm() { $('#create-resource').on('click', function() { $('.new-resource-form').slideDown(); + $('#name-input').focus(); }); } @@ -211,6 +212,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(); @@ -223,6 +234,7 @@ initSampleGroupColor(); bindNewSampleGroupAction(); appendCarretToColorPickerDropdown(); + sampleTypeGroupEditMode(); } // initialize sample types/groups actions diff --git a/app/assets/javascripts/sitewide/utils.js b/app/assets/javascripts/sitewide/utils.js index ccef397d8..50224bb57 100644 --- a/app/assets/javascripts/sitewide/utils.js +++ b/app/assets/javascripts/sitewide/utils.js @@ -221,13 +221,15 @@ function initPageTutorialSteps(pageFirstStepN, pageLastStepN, nextPagePath, * 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) { +$.fn.makeDropdownOptionsLinks = function(selectedIdx, urlParam) { selectedIdx = _.isUndefined(selectedIdx) ? 1 : selectedIdx; $(this).change(function() { - location = $(this).find('option:selected').attr('href'); + window.location.href = addParam($(this).find('option:selected') + .attr('href'), urlParam); }); $(this).find('option') From 435a8d20bb080001575b0d20966bd42f47823b07 Mon Sep 17 00:00:00 2001 From: Oleksii Kriuchykhin Date: Fri, 9 Dec 2016 17:37:22 +0100 Subject: [PATCH 12/12] Fix columns visability and reordering bug [SCI-779] --- app/assets/javascripts/samples/sample_datatable.js | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/app/assets/javascripts/samples/sample_datatable.js b/app/assets/javascripts/samples/sample_datatable.js index 5646a9cc4..f0a182405 100644 --- a/app/assets/javascripts/samples/sample_datatable.js +++ b/app/assets/javascripts/samples/sample_datatable.js @@ -26,7 +26,8 @@ function dataTableInit() { processing: true, serverSide: true, colReorder: { - fixedColumnsLeft: 2 + fixedColumnsLeft: 2, + realtime: false }, destroy: true, ajax: { @@ -129,7 +130,6 @@ function dataTableInit() { }, 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') { @@ -137,6 +137,7 @@ function dataTableInit() { } table.column(i).visible(visibility); } + oSettings._colReorder.fnOrder(myData.ColReorder); } }); @@ -975,9 +976,11 @@ function changeToEditMode() { function initDropdown() { table.on('init.dt', function() { initNewColumnForm(); - loadColumnsNames(); initSorting(); }); + $('#samples-columns-dropdown').on('show.bs.dropdown', function() { + loadColumnsNames(); + }); } initDropdown();