From d69754af04ca3ec3e257c0ac37609b6a7e959110 Mon Sep 17 00:00:00 2001 From: zmagod Date: Thu, 15 Mar 2018 15:43:16 +0100 Subject: [PATCH] implement search on list items dropdown, update, create actions [fixes SCI-2070] --- Gemfile | 2 +- Gemfile.lock | 4 +- app/assets/javascripts/application.js.erb | 5 +- ...tatable.js => repository_datatable.js.erb} | 130 +++++++++++++++++- app/assets/stylesheets/application.scss | 1 + .../repository_list_items_controller.rb | 26 ++++ app/controllers/repository_rows_controller.rb | 126 +++++++++++++---- app/models/repository_column.rb | 2 + .../repositories/_repository_table.html.erb | 2 +- config/routes.rb | 4 +- spec/models/asset_text_datum_spec.rb | 1 - .../javascripts/ajax-bootstrap-select.min.js | 21 +++ .../stylesheets/ajax-bootstrap-select.min.css | 16 +++ 13 files changed, 299 insertions(+), 41 deletions(-) rename app/assets/javascripts/repositories/{repository_datatable.js => repository_datatable.js.erb} (91%) create mode 100644 app/controllers/repository_list_items_controller.rb create mode 100644 vendor/assets/javascripts/ajax-bootstrap-select.min.js create mode 100644 vendor/assets/stylesheets/ajax-bootstrap-select.min.css diff --git a/Gemfile b/Gemfile index ff658fde3..5aa5b6bf2 100644 --- a/Gemfile +++ b/Gemfile @@ -26,7 +26,7 @@ gem 'momentjs-rails', '~> 2.17.1' # JS datetime picker gem 'bootstrap3-datetimepicker-rails', '~> 4.15.35' # Select elements for Bootstrap -gem 'bootstrap-select-rails', '~> 1.6.3' +gem 'bootstrap-select-rails', '~> 1.12.4' gem 'uglifier', '>= 1.3.0' # jQuery & plugins gem 'jquery-turbolinks' diff --git a/Gemfile.lock b/Gemfile.lock index 386d75f4b..bf469ae14 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -127,7 +127,7 @@ GEM bootstrap-sass (3.3.7) autoprefixer-rails (>= 5.2.1) sass (>= 3.3.4) - bootstrap-select-rails (1.6.3) + bootstrap-select-rails (1.12.4) bootstrap3-datetimepicker-rails (4.15.35) momentjs-rails (>= 2.8.1) bootstrap_form (2.7.0) @@ -518,7 +518,7 @@ DEPENDENCIES better_errors binding_of_caller bootstrap-sass (~> 3.3.5) - bootstrap-select-rails (~> 1.6.3) + bootstrap-select-rails (~> 1.12.4) bootstrap3-datetimepicker-rails (~> 4.15.35) bootstrap_form bullet diff --git a/app/assets/javascripts/application.js.erb b/app/assets/javascripts/application.js.erb index f5051cee4..6a2b7b168 100644 --- a/app/assets/javascripts/application.js.erb +++ b/app/assets/javascripts/application.js.erb @@ -31,11 +31,12 @@ //= require tinymce-jquery //= require jsPlumb-2.0.4-min //= require jsnetworkx +//= require dataTables.noSearchHidden +//= require bootstrap-select //= require_directory ./sitewide //= require jquery.dataTables.yadcf //= require datatables -//= require dataTables.noSearchHidden -//= require bootstrap-select +//= require ajax-bootstrap-select.min //= require underscore //= require i18n.js //= require i18n/translations diff --git a/app/assets/javascripts/repositories/repository_datatable.js b/app/assets/javascripts/repositories/repository_datatable.js.erb similarity index 91% rename from app/assets/javascripts/repositories/repository_datatable.js rename to app/assets/javascripts/repositories/repository_datatable.js.erb index f87c31015..98fa39f4e 100644 --- a/app/assets/javascripts/repositories/repository_datatable.js +++ b/app/assets/javascripts/repositories/repository_datatable.js.erb @@ -432,14 +432,20 @@ var RepositoryDatatable = (function(global) { } else if ($(th).attr('id') === 'row-name') { input = changeToInputField('repository_row', 'name', ''); tr.appendChild(createTdElement(input)); - } else if ($(th).hasClass('repository-column')) { + } else if ($(th).hasClass('repository-column') && + $(th).attr('data-type') === 'RepositoryTextValue') { input = changeToInputField('repository_cell', th.attr('id'), ''); tr.appendChild(createTdElement(input)); + } else if ($(th).hasClass('repository-column') && + $(th).attr('data-type') === 'RepositoryListValue') { + input = initialListItemsRequest($(th).attr('id')); + tr.appendChild(createTdElement(input)); } else { // Column we don't care for, just add empty td tr.appendChild(createTdElement('')); } }); + $('table' + TABLE_ID).prepend(tr); selectedRecord = tr; @@ -451,6 +457,8 @@ var RepositoryDatatable = (function(global) { }); // Adjust columns width in table header adjustTableHeader(); + // Init selectpicker + _initSelectPicker(); } global.onClickToggleAssignedRecords = function() { @@ -584,6 +592,7 @@ var RepositoryDatatable = (function(global) { // Take care of custom cells var cells = data.repository_row.repository_cells; + var list_columns = data.repository_row.repository_column_items; $(node).children('td').each(function(i) { var td = $(this); var rawIndex = TABLE.column.index('fromVisible', i); @@ -592,13 +601,17 @@ var RepositoryDatatable = (function(global) { // Check if cell on this record exists var cell = cells[$(colHeader).attr('id')]; if (cell) { - td.html(changeToInputField('repository_cell', + td.html(changeToFormField('repository_cell', $(colHeader).attr('id'), - cell.value)); + cell, + list_columns)); } else { - td.html(changeToInputField('repository_cell', - $(colHeader).attr('id'), '')); + td.html(changeToFormField('repository_cell', + $(colHeader).attr('id'), + '', + list_columns)); } + _initSelectPicker(); } }); @@ -647,13 +660,18 @@ var RepositoryDatatable = (function(global) { // Record name data.repository_row.name = $('td input[data-object = repository_row]').val(); - // Custom cells + // Custom cells text type $(node).find('td input[data-object = repository_cell]').each(function() { // Send data only and only if cell is not empty if ($(this).val().trim()) { data.repository_cells[$(this).attr('name')] = $(this).val(); } }); + // Custom cells list type + $(node).find('td[column_id]').each(function(index, el) { + var value = $(el).attr('list_item_id'); + data.repository_cells[$(el).attr('column_id')] = value; + }); var url; var type; @@ -845,6 +863,88 @@ var RepositoryDatatable = (function(global) { }); // Helper functions + function _listItemDropdown(options, current_value, column_id) { + var html = ''; + return html; + } + + function initialListItemsRequest(column_id) { + var massage_response = []; + $.ajax({ + url: '<%= Rails.application.routes.url_helpers.repository_list_items_path %>', + type: 'POST', + dataType: 'json', + async: false, + data: { + q: '', + column_id: column_id + } + }).done(function(data) { + $.each(data.list_items, function(index, el) { + massage_response.push([el.id, el.data]); + }) + }); + return _listItemDropdown(massage_response, '-1', column_id); + } + + function _initSelectPicker() { + $('.selectpicker') + .selectpicker({liveSearch: true}) + .ajaxSelectPicker({ + ajax: { + url: '<%= Rails.application.routes.url_helpers.repository_list_items_path %>', + type: 'POST', + dataType: 'json', + data: function () { + var params = { + q: '{{{q}}}', + column_id: $(this.valueOf().plugin.$element).attr('column_id') + }; + + return params; + } + }, + locale: { + emptyTitle: 'Nothing selected' + }, + preprocessData: function(data){ + var items = []; + if(data.hasOwnProperty('list_items')){ + items.push({ + 'value': '-1', + 'text': '', + 'disabled': false + }); + $.each(data.list_items, function(index, el) { + items.push( + { + 'value': el.id, + 'text': el.data, + 'disabled': false + } + ) + }); + } + return items; + }, + emptyRequest: true, + clearOnEmpty: false, + preserveSelected: false + }).on('change.bs.select', function(el) { + $(this).closest('td').attr('list_item_id', el.target.value); + $(this).closest('td').attr('column_id', $(this).attr('column_id')); + });; + } + function getColumnIndex(id) { if (id < 0) { return false; @@ -858,6 +958,24 @@ var RepositoryDatatable = (function(global) { object + "' name='" + name + "' value='" + value + "'>"; } + // Takes an object and creates custom html element + function changeToFormField(object, name, cell, list_columns) { + if (cell === '') { + var column = _.findWhere(list_columns, { column_id: parseInt(name) }); + if (column) { + return _listItemDropdown(column.list_items, '', parseInt(name)); + } else { + return changeToInputField(object, name, ''); + } + } else { + if (cell.type === 'RepositoryListValue') { + return _listItemDropdown(cell.list_items, cell.value, parseInt(name)); + } else { + return changeToInputField(object, name, cell.value); + } + } + } + // Return td element with content function createTdElement(content) { var td = document.createElement('td'); diff --git a/app/assets/stylesheets/application.scss b/app/assets/stylesheets/application.scss index 023bd4d94..eeb5db641 100644 --- a/app/assets/stylesheets/application.scss +++ b/app/assets/stylesheets/application.scss @@ -16,6 +16,7 @@ @import "bootstrap-tagsinput"; @import "bootstrap-tagsinput-typeahead"; @import "handsontable.full.min"; +@import "ajax-bootstrap-select.min"; @import "extend/bootstrap"; @import "font-awesome"; @import "themes/scinote"; diff --git a/app/controllers/repository_list_items_controller.rb b/app/controllers/repository_list_items_controller.rb new file mode 100644 index 000000000..4e8eec34a --- /dev/null +++ b/app/controllers/repository_list_items_controller.rb @@ -0,0 +1,26 @@ +class RepositoryListItemsController < ApplicationController + before_action :load_vars, only: :search + + def search + column_list_items = @repository_column.repository_list_items + .where('data ILIKE ?', + "%#{search_params[:q]}%") + .limit(Constants::SEARCH_LIMIT) + .select(:id, :data) + + render json: { list_items: column_list_items }, status: :ok + end + + private + + def search_params + params.permit(:q, :column_id) + end + + def load_vars + @repository_column = RepositoryColumn.find_by_id(search_params[:column_id]) + repository = @repository_column.repository if @repository_column + render_404 and return unless @repository_column&.data_type == "RepositoryListValue" + render_403 unless can_manage_repository_rows?(repository.team) + end +end diff --git a/app/controllers/repository_rows_controller.rb b/app/controllers/repository_rows_controller.rb index 4c7303a30..2c0099ca5 100644 --- a/app/controllers/repository_rows_controller.rb +++ b/app/controllers/repository_rows_controller.rb @@ -38,15 +38,31 @@ class RepositoryRowsController < ApplicationController column = @repository.repository_columns.detect do |c| c.id == key.to_i end - cell_value = RepositoryTextValue.new( - data: value, - created_by: current_user, - last_modified_by: current_user, - repository_cell_attributes: { - repository_row: record, - repository_column: column - } - ) + if column.data_type == 'RepositoryListValue' + next if value == '-1' + # check if item existx else revert the transaction + list_item = RepositoryListItem.where(repository_column: column) + .find(value) + cell_value = RepositoryListValue.new( + repository_list_item_id: list_item.id, + created_by: current_user, + last_modified_by: current_user, + repository_cell_attributes: { + repository_row: record, + repository_column: column + } + ) + else + cell_value = RepositoryTextValue.new( + data: value, + created_by: current_user, + last_modified_by: current_user, + repository_cell_attributes: { + repository_row: record, + repository_column: column + } + ) + end if cell_value.save record_annotation_notification(record, cell_value.repository_cell) else @@ -91,7 +107,8 @@ class RepositoryRowsController < ApplicationController json = { repository_row: { name: escape_input(@record.name), - repository_cells: {} + repository_cells: {}, + repository_column_items: fetch_columns_list_items } } @@ -99,7 +116,9 @@ class RepositoryRowsController < ApplicationController @record.repository_cells.each do |cell| json[:repository_row][:repository_cells][cell.repository_column_id] = { repository_cell_id: cell.id, - value: escape_input(cell.value.data) + value: escape_input(cell.value.data), + type: cell.value_type, + list_items: fetch_list_items(cell) } end @@ -125,14 +144,26 @@ class RepositoryRowsController < ApplicationController end if existing # Cell exists and new value present, so update value - existing.value.data = value - if existing.value.save - record_annotation_notification(@record, existing) + if existing.value_type == 'RepositoryListValue' + item = RepositoryListItem.where( + repository_column: existing.repository_column + ).find(value) unless value == '-1' + if item + existing.repository_list_value + .update_attribute(:repository_list_item_id, item) + else + existing.delete + end else - errors[:repository_cells] << { - "#{existing.repository_column_id}": - existing.value.errors.messages - } + existing.value.data = value + if existing.value.save + record_annotation_notification(@record, existing) + else + errors[:repository_cells] << { + "#{existing.repository_column_id}": + existing.value.errors.messages + } + end end else # Looks like it is a new cell, so we need to create new value, cell @@ -140,15 +171,31 @@ class RepositoryRowsController < ApplicationController column = @repository.repository_columns.detect do |c| c.id == key.to_i end - cell_value = RepositoryTextValue.new( - data: value, - created_by: current_user, - last_modified_by: current_user, - repository_cell_attributes: { - repository_row: @record, - repository_column: column - } - ) + if column.data_type == 'RepositoryListValue' + next if value == '-1' + # check if item existx else revert the transaction + list_item = RepositoryListItem.where(repository_column: column) + .find(value) + cell_value = RepositoryListValue.new( + repository_list_item_id: list_item.id, + created_by: current_user, + last_modified_by: current_user, + repository_cell_attributes: { + repository_row: @record, + repository_column: column + } + ) + else + cell_value = RepositoryTextValue.new( + data: value, + created_by: current_user, + last_modified_by: current_user, + repository_cell_attributes: { + repository_row: @record, + repository_column: column + } + ) + end if cell_value.save record_annotation_notification(@record, cell_value.repository_cell) @@ -161,6 +208,7 @@ class RepositoryRowsController < ApplicationController end # Clean up empty cells, not present in updated record @record.repository_cells.each do |cell| + next if cell.value_type == 'RepositoryListValue' cell.value.destroy unless cell_params .key?(cell.repository_column_id.to_s) end @@ -293,4 +341,28 @@ class RepositoryRowsController < ApplicationController column: link_to(cell.repository_column.name, table_url)) ) end + + def fetch_list_items(cell) + return [] if cell.value_type != 'RepositoryListValue' + RepositoryListItem.where(repository: @repository) + .where(repository_column: cell.repository_column) + .limit(Constants::SEARCH_LIMIT) + .pluck(:id, :data) + end + + def fetch_columns_list_items + collection = [] + @repository.repository_columns + .list_type + .preload(:repository_list_items) + .each do |column| + collection << { + column_id: column.id, + list_items: column.repository_list_items + .limit(Constants::SEARCH_LIMIT) + .pluck(:id, :data) + } + end + collection + end end diff --git a/app/models/repository_column.rb b/app/models/repository_column.rb index ff3e8bb5f..40a54de05 100644 --- a/app/models/repository_column.rb +++ b/app/models/repository_column.rb @@ -21,6 +21,8 @@ class RepositoryColumn < ApplicationRecord after_create :update_repository_table_state + scope :list_type, -> { where(data_type: 'RepositoryListValue') } + def update_repository_table_state RepositoryTableState.update_state(self, nil, created_by) end diff --git a/app/views/repositories/_repository_table.html.erb b/app/views/repositories/_repository_table.html.erb index 0542b9469..b1b0ceb71 100644 --- a/app/views/repositories/_repository_table.html.erb +++ b/app/views/repositories/_repository_table.html.erb @@ -20,7 +20,7 @@ <%= t("repositories.table.added_on") %> <%= t("repositories.table.added_by") %> <% repository.repository_columns.order(:id).each do |column| %> - <%= 'data-deletable' if can_update_or_delete_repository_column?(column) %> <%= "data-edit-url='#{edit_repository_repository_column_path(repository, column)}'" %> diff --git a/config/routes.rb b/config/routes.rb index e550e303d..3b8e77004 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -472,7 +472,6 @@ Rails.application.routes.draw do as: 'columns_destroy_html' resources :repository_columns, only: %i(create edit update destroy) - resources :repository_rows, only: %i(create edit update) member do post 'parse_sheet' @@ -480,6 +479,9 @@ Rails.application.routes.draw do end end + post 'repository_list_items', to: 'repository_list_items#search', + defaults: { format: 'json' } + get 'repository_rows/:id', to: 'repository_rows#show', as: :repository_row, defaults: { format: 'json' } diff --git a/spec/models/asset_text_datum_spec.rb b/spec/models/asset_text_datum_spec.rb index e959296df..ee65213bc 100644 --- a/spec/models/asset_text_datum_spec.rb +++ b/spec/models/asset_text_datum_spec.rb @@ -26,7 +26,6 @@ describe AssetTextDatum, type: :model do it 'should have uniq asset' do create :asset_text_datum, asset: asset new_atd = build :asset_text_datum, asset: asset - # binding.pry expect(new_atd).to_not be_valid end end diff --git a/vendor/assets/javascripts/ajax-bootstrap-select.min.js b/vendor/assets/javascripts/ajax-bootstrap-select.min.js new file mode 100644 index 000000000..999fc5996 --- /dev/null +++ b/vendor/assets/javascripts/ajax-bootstrap-select.min.js @@ -0,0 +1,21 @@ +/*! + * Ajax Bootstrap Select + * + * Extends existing [Bootstrap Select] implementations by adding the ability to search via AJAX requests as you type. Originally for CROSCON. + * + * @version 1.4.3 + * @author Adam Heim - https://github.com/truckingsim + * @link https://github.com/truckingsim/Ajax-Bootstrap-Select + * @copyright 2017 Adam Heim + * @license Released under the MIT license. + * + * Contributors: + * Mark Carver - https://github.com/markcarver + * + * Last build: 2017-11-15 1:19:46 PM EST + */ +!function(a,b){var c=function(c,d){var e,f,g=this;d=d||{},this.$element=a(c),this.options=a.extend(!0,{},a.fn.ajaxSelectPicker.defaults,d),this.LOG_ERROR=1,this.LOG_WARNING=2,this.LOG_INFO=3,this.LOG_DEBUG=4,this.lastRequest=!1,this.previousQuery="",this.query="",this.request=!1;var h=[{from:"ajaxResultsPreHook",to:"preprocessData"},{from:"ajaxSearchUrl",to:{ajax:{url:"{{{value}}}"}}},{from:"ajaxOptions",to:"ajax"},{from:"debug",to:function(b){var c={};c.log=Boolean(g.options[b.from])?g.LOG_DEBUG:0,g.options=a.extend(!0,{},g.options,c),delete g.options[b.from],g.log(g.LOG_WARNING,'Deprecated option "'+b.from+'". Update code to use:',c)}},{from:"mixWithCurrents",to:"preserveSelected"},{from:"placeHolderOption",to:{locale:{emptyTitle:"{{{value}}}"}}}];h.length&&a.map(h,function(b){if(g.options[b.from])if(a.isPlainObject(b.to))g.replaceValue(b.to,"{{{value}}}",g.options[b.from]),g.options=a.extend(!0,{},g.options,b.to),g.log(g.LOG_WARNING,'Deprecated option "'+b.from+'". Update code to use:',b.to),delete g.options[b.from];else if(a.isFunction(b.to))b.to.apply(g,[b]);else{var c={};c[b.to]=g.options[b.from],g.options=a.extend(!0,{},g.options,c),g.log(g.LOG_WARNING,'Deprecated option "'+b.from+'". Update code to use:',c),delete g.options[b.from]}});var i=this.$element.data();i.searchUrl&&(g.log(g.LOG_WARNING,'Deprecated attribute name: "data-search-url". Update markup to use: \' data-abs-ajax-url="'+i.searchUrl+"\" '"),this.options.ajax.url=i.searchUrl);var j=function(a,b){return b.toLowerCase()},k=function(a,b,c){var d=[].concat(a),e=d.length,f=c||{};if(e){var g=d.shift();f[g]=k(d,b,f[g])}return e?f:b},l=Object.keys(i).filter(/./.test.bind(new RegExp("^abs[A-Z]")));if(l.length){var m={},n=["locale"];for(e=0,f=l.length;e1&&n.indexOf(p[0])!==-1){for(var q=[p.shift()],r="",s=0;s0)&&f.replaceValue(b[g],c,d,e):h===c&&(e.limit!==!1&&"number"==typeof e.limit&&e.limit--,b[g]=d))})},c.prototype.t=function(a,b){return b=b||this.options.langCode,this.locale[b]&&this.locale[b].hasOwnProperty(a)?this.locale[b][a]:(this.log(this.LOG_WARNING,"Unknown translation key:",a),a)},b.AjaxBootstrapSelect=b.AjaxBootstrapSelect||c;var d=function(b){var c=this;this.$status=a(b.options.templates.status).hide().appendTo(b.selectpicker.$menu);var d=b.t("statusInitialized");d&&d.length&&this.setStatus(d),this.cache={},this.plugin=b,this.selected=[],this.title=null,this.selectedTextFormat=b.selectpicker.options.selectedTextFormat;var e=[];b.$element.find("option").each(function(){var c=a(this),d=c.attr("value");e.push({value:d,text:c.text(),class:c.attr("class")||"",data:c.data()||{},preserved:b.options.preserveSelected,selected:!!c.attr("selected")})}),this.cacheSet("",e),b.options.preserveSelected&&(c.selected=e,b.$element.on("change.abs.preserveSelected",function(d){var e=b.$element.find(":selected");c.selected=[],b.selectpicker.multiple||(e=e.last()),e.each(function(){var b=a(this),d=b.attr("value");c.selected.push({value:d,text:b.text(),class:b.attr("class")||"",data:b.data()||{},preserved:!0,selected:!0})}),c.replaceOptions(c.cacheGet(c.plugin.query))}))};d.prototype.build=function(b){var c,d,e=b.length,f=a("