diff --git a/Gemfile b/Gemfile index b5bf73214..085f7e219 100644 --- a/Gemfile +++ b/Gemfile @@ -27,7 +27,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 8708cab95..ecfaef70f 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) @@ -534,7 +534,7 @@ DEPENDENCIES better_errors binding_of_caller bootstrap-sass (~> 3.3.7) - 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 ec2825632..167546640 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/my_modules_controller.rb b/app/controllers/my_modules_controller.rb index 9a64108f4..ea73d7b88 100644 --- a/app/controllers/my_modules_controller.rb +++ b/app/controllers/my_modules_controller.rb @@ -6,12 +6,20 @@ class MyModulesController < ApplicationController include ActionView::Helpers::UrlHelper include ApplicationHelper - before_action :load_vars - before_action :load_vars_nested, only: %I[new create] - before_action :load_repository, only: %I[assign_repository_records - unassign_repository_records] - before_action :check_manage_permissions, only: - %i(destroy description due_date) + before_action :load_vars, + only: %i(show update destroy description due_date protocols + results samples activities activities_tab + assign_samples unassign_samples delete_samples + toggle_task_state samples_index archive + complete_my_module repository repository_index + assign_repository_records unassign_repository_records) + before_action :load_vars_nested, only: %i(new create) + before_action :load_repository, only: %i(assign_repository_records + unassign_repository_records + repository_index) + before_action :check_manage_permissions, + only: %i(update destroy description due_date) + before_action :check_view_info_permissions, only: :show before_action :check_view_permissions, only: %i(show activities activities_tab protocols results samples samples_index archive) @@ -363,20 +371,18 @@ class MyModulesController < ApplicationController # AJAX actions def repository_index - @repository = Repository.find_by_id(params[:repository_id]) - if @repository.nil? || !can_read_team?(@repository.team) - render_403 - else - respond_to do |format| - format.html - format.json do - render json: ::RepositoryDatatable.new(view_context, - @repository, - @my_module, - current_user) - end - end - end + @draw = params[:draw].to_i + per_page = params[:length] == '-1' ? 100 : params[:length].to_i + page = (params[:start].to_i / per_page) + 1 + records = RepositoryDatatableService.new(@repository, + params, + current_user, + @my_module) + @assigned_rows = records.assigned_rows + @repository_row_count = records.repository_rows.count + @columns_mappings = records.mappings + @repository_rows = records.repository_rows.page(page).per(per_page) + render 'repository_rows/index.json' end # Submit actions @@ -595,7 +601,8 @@ class MyModulesController < ApplicationController def load_repository @repository = Repository.find_by_id(params[:repository_id]) - render_404 unless @repository && can_read_team?(@repository.team) + render_404 unless @repository + render_403 unless can_read_team?(@repository.team) end def check_manage_permissions diff --git a/app/controllers/repository_list_items_controller.rb b/app/controllers/repository_list_items_controller.rb new file mode 100644 index 000000000..2eeb20366 --- /dev/null +++ b/app/controllers/repository_list_items_controller.rb @@ -0,0 +1,28 @@ +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 + unless @repository_column&.data_type == 'RepositoryListValue' + render_404 and return + end + 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 730e8572e..e8c191253 100644 --- a/app/controllers/repository_rows_controller.rb +++ b/app/controllers/repository_rows_controller.rb @@ -5,10 +5,23 @@ class RepositoryRowsController < ApplicationController before_action :load_info_modal_vars, only: :show before_action :load_vars, only: %i(edit update) - before_action :load_repository, only: %i(create delete_records) + before_action :load_repository, only: %i(create delete_records index) before_action :check_create_permissions, only: :create before_action :check_manage_permissions, only: %i(edit update delete_records) + def index + @draw = params[:draw].to_i + per_page = params[:length] == '-1' ? 100 : params[:length].to_i + page = (params[:start].to_i / per_page) + 1 + records = RepositoryDatatableService.new(@repository, + params, + current_user) + @assigned_rows = records.assigned_rows + @repository_row_count = records.repository_rows.count + @columns_mappings = records.mappings + @repository_rows = records.repository_rows.page(page).per(per_page) + end + def create record = RepositoryRow.new(repository: @repository, created_by: current_user, @@ -24,15 +37,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 @@ -77,7 +106,8 @@ class RepositoryRowsController < ApplicationController json = { repository_row: { name: escape_input(@record.name), - repository_cells: {} + repository_cells: {}, + repository_column_items: fetch_columns_list_items } } @@ -85,7 +115,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 @@ -111,14 +143,27 @@ 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.value.update_attribute( + :repository_list_item_id, item.id + ) + 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 @@ -126,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) @@ -147,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 @@ -237,6 +299,7 @@ class RepositoryRowsController < ApplicationController def load_repository @repository = Repository.find_by_id(params[:repository_id]) render_404 unless @repository + render_403 unless can_read_team?(@repository.team) end def check_create_permissions @@ -276,4 +339,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/datatables/repository_datatable.rb b/app/datatables/repository_datatable.rb deleted file mode 100644 index 3cce51ca4..000000000 --- a/app/datatables/repository_datatable.rb +++ /dev/null @@ -1,332 +0,0 @@ -require 'active_record' - -class RepositoryDatatable < CustomDatatable - include ActionView::Helpers::TextHelper - include SamplesHelper - include InputSanitizeHelper - include Rails.application.routes.url_helpers - include ActionView::Helpers::UrlHelper - include ApplicationHelper - include ActiveRecord::Sanitization::ClassMethods - - ASSIGNED_SORT_COL = 'assigned'.freeze - - REPOSITORY_TABLE_DEFAULT_STATE = { - 'time' => 0, - 'start' => 0, - 'length' => 5, - 'order' => [[2, 'desc']], - 'search' => { 'search' => '', - 'smart' => true, - 'regex' => false, - 'caseInsensitive' => true }, - 'columns' => [], - 'assigned' => 'assigned', - 'ColReorder' => [*0..4] - } - 5.times do - REPOSITORY_TABLE_DEFAULT_STATE['columns'] << { - 'visible' => true, - 'search' => { 'search' => '', - 'smart' => true, - 'regex' => false, - 'caseInsensitive' => true } - } - end - REPOSITORY_TABLE_DEFAULT_STATE.freeze - - def initialize(view, - repository, - my_module = nil, - user = nil) - super(view) - @repository = repository - @team = repository.team - @my_module = my_module - @user = user - end - - # Define sortable columns, so 1st column will be sorted by attribute - # in sortable_columns[0] - def sortable_columns - sort_array = [ - ASSIGNED_SORT_COL, - 'RepositoryRow.name', - 'RepositoryRow.created_at', - 'User.full_name' - ] - - sort_array.push(*repository_columns_sort_by) - @sortable_columns = sort_array - end - - # Define attributes on which we perform search - def searchable_columns - search_array = [ - 'RepositoryRow.name', - 'RepositoryRow.created_at', - 'User.full_name' - ] - - # search_array.push(*repository_columns_sort_by) - @searchable_columns ||= filter_search_array search_array - end - - private - - # filters the search array by checking if the the column is visible - def filter_search_array(input_array) - param_index = 2 - filtered_array = [] - input_array.each do |col| - next if columns_params.to_a[param_index].nil? - params_col = - columns_params.to_a.find { |v| v[1]['data'] == param_index.to_s } - filtered_array.push(col) unless params_col[1]['searchable'] == 'false' - param_index += 1 - end - filtered_array - end - - # Get array of columns to sort by (for custom columns) - def repository_columns_sort_by - array = [] - @repository.repository_columns.count.times do - array << 'RepositoryCell.value' - end - array - end - - # Returns json of current repository rows (already paginated) - def data - records.map do |record| - row = { - 'DT_RowId': record.id, - '1': assigned_row(record), - '2': escape_input(record.name), - '3': I18n.l(record.created_at, format: :full), - '4': escape_input(record.created_by.full_name), - 'recordEditUrl': - Rails.application.routes.url_helpers - .edit_repository_repository_row_path(@repository, - record.id), - 'recordUpdateUrl': - Rails.application.routes.url_helpers - .repository_repository_row_path(@repository, record.id), - 'recordInfoUrl': - Rails.application.routes.url_helpers.repository_row_path(record.id) - } - - # Add custom columns - record.repository_cells.each do |cell| - row[@columns_mappings[cell.repository_column.id]] = - custom_auto_link( - display_tooltip(cell.value.data, - Constants::NAME_MAX_LENGTH), - simple_format: true, - team: @team - ) - end - row - end - end - - def assigned_row(record) - if @assigned_rows && @assigned_rows.include?(record) - " " - else - " " - end - end - - # Query database for records (this will be later paginated and filtered) - # after that "data" function will return json - def get_raw_records - repository_rows = RepositoryRow - .preload( - :repository_columns, - :created_by, - repository_cells: :value - ) - .joins(:created_by) - .where(repository: @repository) - - # Make mappings of custom columns, so we have same id for every column - i = 5 - @columns_mappings = {} - @repository.repository_columns.order(:id).each do |column| - @columns_mappings[column.id] = i.to_s - i += 1 - end - - if @my_module - @assigned_rows = @my_module.repository_rows - .preload( - :repository_columns, - :created_by, - repository_cells: :value - ) - .joins(:created_by) - .where(repository: @repository) - return @assigned_rows if dt_params[:assigned] == 'assigned' - else - @assigned_rows = repository_rows.joins( - 'INNER JOIN my_module_repository_rows ON - (repository_rows.id = my_module_repository_rows.repository_row_id)' - ) - end - - repository_rows - end - - # Override default behaviour - # Don't filter and paginate records when sorting by custom column - everything - # is done in sort_records method - you might ask why, well if you want the - # number of samples/all samples it's dependant upon sort_record query - def fetch_records - records = get_raw_records - records = filter_records(records) if dt_params[:search].present? && - dt_params[:search][:value].present? - records = sort_records(records) if order_params.present? - records = paginate_records(records) unless dt_params[:length].present? && - dt_params[:length] == '-1' - escape_special_chars - records - end - - # Overriden to make it work for custom columns, because they are polymorphic - # NOTE: Function assumes the provided records/rows are only from the current - # repository! - def filter_records(repo_rows) - return repo_rows unless dt_params[:search].present? && - dt_params[:search][:value].present? - search_val = dt_params[:search][:value] - - filtered_rows = repo_rows.find_by_sql( - "SELECT DISTINCT repository_rows.* - FROM repository_rows - INNER JOIN ( - SELECT users.* - FROM users - ) AS users - ON users.id = repository_rows.created_by_id - LEFT OUTER JOIN ( - SELECT repository_cells.repository_row_id, - repository_text_values.data AS text_value, - to_char(repository_date_values.data, 'DD.MM.YYYY HH24:MI') - AS date_value - FROM repository_cells - INNER JOIN repository_text_values - ON repository_text_values.id = repository_cells.value_id - FULL OUTER JOIN repository_date_values - ON repository_date_values.id = repository_cells.value_id - ) AS values - ON values.repository_row_id = repository_rows.id - WHERE repository_rows.repository_id = #{@repository.id} - AND (repository_rows.name ILIKE '%#{search_val}%' - OR to_char(repository_rows.created_at, 'DD.MM.YYYY HH24:MI') - ILIKE '%#{search_val}%' - OR users.full_name ILIKE '%#{search_val}%' - OR text_value ILIKE '%#{search_val}%' - OR date_value ILIKE '%#{search_val}%')" - ) - repo_rows.where(id: filtered_rows) - end - - # Override default sort method if needed - def sort_records(records) - if sort_column(order_params) == ASSIGNED_SORT_COL - # If "assigned" column is sorted when viewing assigned items - return records if @my_module && dt_params[:assigned] == 'assigned' - # If "assigned" column is sorted - direction = sort_null_direction(order_params) - if @my_module - # Depending on the sort, order nulls first or - # nulls last on repository_cells association - records.joins( - "LEFT OUTER JOIN my_module_repository_rows ON - (repository_rows.id = my_module_repository_rows.repository_row_id - AND (my_module_repository_rows.my_module_id = #{@my_module.id} OR - my_module_repository_rows.id IS NULL))" - ).order("my_module_repository_rows.id NULLS #{direction}") - else - sort_assigned_records(records, order_params['dir']) - end - elsif sorting_by_custom_column - ci = sortable_displayed_columns[ - order_params['column'].to_i - 1 - ] - column_id = @columns_mappings.key((ci.to_i + 1).to_s) - dir = sort_direction(order_params) - - records.joins( - "LEFT OUTER JOIN (SELECT repository_cells.repository_row_id, - repository_text_values.data AS value FROM repository_cells - INNER JOIN repository_text_values - ON repository_text_values.id = repository_cells.value_id - WHERE repository_cells.repository_column_id = #{column_id}) AS values - ON values.repository_row_id = repository_rows.id" - ).order("values.value #{dir}") - else - super(records) - end - end - - def sort_null_direction(item) - val = sort_direction(item) - val == 'ASC' ? 'LAST' : 'FIRST' - end - - def inverse_sort_direction(item) - val = sort_direction(item) - val == 'ASC' ? 'DESC' : 'ASC' - end - - def sorting_by_custom_column - sort_column(order_params) == 'repository_cells.value' - end - - # Escapes special characters in search query - def escape_special_chars - if dt_params[:search].present? - dt_params[:search][:value] = ActiveRecord::Base - .__send__(:sanitize_sql_like, - dt_params[:search][:value]) - end - end - - def new_sort_column(item) - coli = item[:column].to_i - 1 - model, column = sortable_columns[sortable_displayed_columns[coli].to_i] - .split('.') - - return model if model == ASSIGNED_SORT_COL - [model.constantize.table_name, column].join('.') - end - - def generate_sortable_displayed_columns - sort_order = RepositoryTableState.load_state(@user, @repository) - .first['ColReorder'] - sort_order.shift - sort_order.map! { |i| (i.to_i - 1).to_s } - - @sortable_displayed_columns = sort_order - end - - def sort_assigned_records(records, direction) - assigned = records.joins(:my_module_repository_rows).distinct.pluck(:id) - unassigned = records.where.not(id: assigned).pluck(:id) - if direction == 'asc' - ids = assigned + unassigned - elsif direction == 'desc' - ids = unassigned + assigned - end - - order_by_index = ActiveRecord::Base.send( - :sanitize_sql_array, - ["position((',' || repository_rows.id || ',') in ?)", - ids.join(',') + ','] - ) - records.order(order_by_index) - end -end diff --git a/app/helpers/repository_datatable_helper.rb b/app/helpers/repository_datatable_helper.rb new file mode 100644 index 000000000..f07546648 --- /dev/null +++ b/app/helpers/repository_datatable_helper.rb @@ -0,0 +1,52 @@ +module RepositoryDatatableHelper + include InputSanitizeHelper + def prepare_row_columns(repository_rows, + repository, + columns_mappings, + team, + assigned_rows) + parsed_records = [] + repository_rows.each do |record| + row = { + 'DT_RowId': record.id, + '1': assigned_row(record, assigned_rows), + '2': escape_input(record.name), + '3': I18n.l(record.created_at, format: :full), + '4': escape_input(record.created_by.full_name), + 'recordEditUrl': Rails.application.routes.url_helpers + .edit_repository_repository_row_path( + repository, + record.id + ), + 'recordUpdateUrl': Rails.application.routes.url_helpers + .repository_repository_row_path( + repository, + record.id + ), + 'recordInfoUrl': Rails.application.routes.url_helpers + .repository_row_path(record.id) + } + + # Add custom columns + record.repository_cells.each do |cell| + row[columns_mappings[cell.repository_column.id]] = + custom_auto_link( + display_tooltip(cell.value.data, + Constants::NAME_MAX_LENGTH), + simple_format: true, + team: team + ) + end + parsed_records << row + end + parsed_records + end + + def assigned_row(record, assigned_rows) + if assigned_rows&.include?(record) + " " + else + " " + end + end +end diff --git a/app/models/repository_column.rb b/app/models/repository_column.rb index f87f87fd4..a8cb64c25 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/models/repository_table_state.rb b/app/models/repository_table_state.rb index d88847ad7..dc2f3c6d2 100644 --- a/app/models/repository_table_state.rb +++ b/app/models/repository_table_state.rb @@ -43,7 +43,7 @@ class RepositoryTableState < ApplicationRecord else # add column index = repository_state['columns'].count - repository_state['columns'][index] = RepositoryDatatable:: + repository_state['columns'][index] = Constants:: REPOSITORY_TABLE_DEFAULT_STATE['columns'].first repository_state['ColReorder'].insert(2, index.to_s) end @@ -52,12 +52,12 @@ class RepositoryTableState < ApplicationRecord end def self.create_state(user, repository) - default_columns_num = RepositoryDatatable:: + default_columns_num = Constants:: REPOSITORY_TABLE_DEFAULT_STATE['columns'].count repository_state = - RepositoryDatatable::REPOSITORY_TABLE_DEFAULT_STATE.deep_dup + Constants::REPOSITORY_TABLE_DEFAULT_STATE.deep_dup repository.repository_columns.each_with_index do |_, index| - repository_state['columns'] << RepositoryDatatable:: + repository_state['columns'] << Constants:: REPOSITORY_TABLE_DEFAULT_STATE['columns'].first repository_state['ColReorder'] << (default_columns_num + index) end diff --git a/app/services/repository_datatable_service.rb b/app/services/repository_datatable_service.rb new file mode 100644 index 000000000..55b815812 --- /dev/null +++ b/app/services/repository_datatable_service.rb @@ -0,0 +1,190 @@ +class RepositoryDatatableService + + attr_reader :repository_rows, :assigned_rows, :mappings + + def initialize(repository, params, user, my_module = nil) + @repository = repository + @user = user + @my_module = my_module + @params = params + create_columns_mappings + process_query + end + + private + + def create_columns_mappings + # Make mappings of custom columns, so we have same id for every + # column + index = 5 + @mappings = {} + @repository.repository_columns.order(:id).each do |column| + @mappings[column.id] = index.to_s + index += 1 + end + end + + def process_query + order_obj = build_conditions(@params)[:order_by_column] + search_value = build_conditions(@params)[:search_value] + records = search_value.present? ? search(search_value) : fetch_records + @repository_rows = sort_rows(order_obj, records) + end + + def fetch_records + repository_rows = RepositoryRow.preload(:repository_columns, + :created_by, + repository_cells: :value) + .joins(:created_by) + .where(repository: @repository) + if @my_module + @assigned_rows = @my_module.repository_rows + .preload( + :repository_columns, + :created_by, + repository_cells: :value + ) + .joins(:created_by) + .where(repository: @repository) + return @assigned_rows if @params[:assigned] == 'assigned' + else + @assigned_rows = repository_rows.joins(:my_module_repository_rows) + end + repository_rows + end + + def search(value) + includes_json = { repository_cells: Extends::REPOSITORY_SEARCH_INCLUDES } + searchable_attributes = ['repository_rows.name', 'users.full_name'] + + Extends::REPOSITORY_EXTRA_SEARCH_ATTR + + RepositoryRow.left_outer_joins(:created_by) + .left_outer_joins(includes_json) + .where(repository: @repository) + .where_attributes_like(searchable_attributes, value) + .distinct + end + + def build_conditions(params) + search_value = params[:search][:value] + order = params[:order].values.first + order_by_column = { column: order[:column].to_i, + dir: order[:dir] } + { search_value: search_value, order_by_column: order_by_column } + end + + def sortable_columns + array = [ + 'assigned', + 'repository_rows.name', + 'repository_rows.created_at', + 'users.full_name' + ] + @repository.repository_columns.count.times do + array << 'repository_cell.value' + end + array + end + + def sort_rows(column_obj, records) + dir = %w(DESC ASC).find do |direction| + direction == column_obj[:dir].upcase + end || 'ASC' + column_index = column_obj[:column] + col_order = @repository.repository_table_states + .find_by_user_id(@user.id) + .state['ColReorder'] + column_id = col_order[column_index].to_i + + if sortable_columns[column_id - 1] == 'assigned' + return records if @my_module && @params[:assigned] == 'assigned' + if @my_module + # Depending on the sort, order nulls first or + # nulls last on repository_cells association + return records.joins( + "LEFT OUTER JOIN my_module_repository_rows ON + (repository_rows.id = + my_module_repository_rows.repository_row_id + AND (my_module_repository_rows.my_module_id = + #{@my_module.id} + OR my_module_repository_rows.id IS NULL))" + ).order( + "my_module_repository_rows.id NULLS + #{sort_null_direction(dir)}" + ) + else + return sort_assigned_records(records, dir) + end + elsif sortable_columns[column_id - 1] == 'repository_cell.value' + id = @mappings.key(column_id.to_s) + type = RepositoryColumn.find_by_id(id) + return records unless type + return select_type(type.data_type, records, id, dir) + else + return records.order( + "#{sortable_columns[column_id - 1]} #{dir}" + ) + end + end + + def sort_assigned_records(records, direction) + assigned = records.joins(:my_module_repository_rows) + .distinct + .pluck(:id) + unassigned = records.where.not(id: assigned).pluck(:id) + if direction == 'ASC' + ids = assigned + unassigned + elsif direction == 'DESC' + ids = unassigned + assigned + end + + order_by_index = ActiveRecord::Base.send( + :sanitize_sql_array, + ["position((',' || repository_rows.id || ',') in ?)", + ids.join(',') + ','] + ) + records.order(order_by_index) + end + + def select_type(type, records, id, dir) + case type + when 'RepositoryTextValue' + filter_by_text_value(records, id, dir) + when 'RepositoryListValue' + filter_by_list_value(records, id, dir) + else + records + end + end + + def sort_null_direction(val) + val == 'ASC' ? 'LAST' : 'FIRST' + end + + def filter_by_text_value(records, id, dir) + records.joins( + "LEFT OUTER JOIN (SELECT repository_cells.repository_row_id, + repository_text_values.data AS value + FROM repository_cells + INNER JOIN repository_text_values + ON repository_text_values.id = repository_cells.value_id + WHERE repository_cells.repository_column_id = #{id}) AS values + ON values.repository_row_id = repository_rows.id" + ).order("values.value #{dir}") + end + + def filter_by_list_value(records, id, dir) + records.joins( + "LEFT OUTER JOIN (SELECT repository_cells.repository_row_id, + repository_list_items.data AS value + FROM repository_cells + INNER JOIN repository_list_values + ON repository_list_values.id = repository_cells.value_id + INNER JOIN repository_list_items + ON repository_list_values.repository_list_item_id = + repository_list_items.id + WHERE repository_cells.repository_column_id = #{id}) AS values + ON values.repository_row_id = repository_rows.id" + ).order("values.value #{dir}") + end +end diff --git a/app/views/repositories/_repository_table.html.erb b/app/views/repositories/_repository_table.html.erb index 7d602ec71..a29c80082 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_manage_repository_column?(column) %> <%= "data-edit-url='#{edit_repository_repository_column_path(repository, column)}'" %> diff --git a/app/views/repository_rows/index.json.jbuilder b/app/views/repository_rows/index.json.jbuilder new file mode 100644 index 000000000..c747bd696 --- /dev/null +++ b/app/views/repository_rows/index.json.jbuilder @@ -0,0 +1,10 @@ +json.draw @draw +json.recordsTotal @repository_rows.total_count +json.recordsFiltered @repository_row_count +json.data do + json.array! prepare_row_columns(@repository_rows, + @repository, + @columns_mappings, + @repository.team, + @assigned_rows) +end diff --git a/config/initializers/constants.rb b/config/initializers/constants.rb index bdca92004..2bf846e03 100644 --- a/config/initializers/constants.rb +++ b/config/initializers/constants.rb @@ -859,6 +859,31 @@ class Constants ] }.freeze + # Repository default table state + REPOSITORY_TABLE_DEFAULT_STATE = { + 'time' => 0, + 'start' => 0, + 'length' => 5, + 'order' => [[2, 'desc']], + 'search' => { 'search' => '', + 'smart' => true, + 'regex' => false, + 'caseInsensitive' => true }, + 'columns' => [], + 'assigned' => 'assigned', + 'ColReorder' => [*0..4] + } + 5.times do + REPOSITORY_TABLE_DEFAULT_STATE['columns'] << { + 'visible' => true, + 'search' => { 'search' => '', + 'smart' => true, + 'regex' => false, + 'caseInsensitive' => true } + } + end + REPOSITORY_TABLE_DEFAULT_STATE.freeze + EXPORTABLE_ZIP_EXPIRATION_DAYS = 7 # Very basic regex to check for validity of emails diff --git a/config/routes.rb b/config/routes.rb index 331b644cc..d6ecdf761 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -441,7 +441,8 @@ Rails.application.routes.draw do post 'archive', to: 'protocols#archive' post 'restore', to: 'protocols#restore' post 'import', to: 'protocols#import' - post 'protocolsio_import_create', to: 'protocols#protocolsio_import_create' + post 'protocolsio_import_create', + to: 'protocols#protocolsio_import_create' post 'protocolsio_import_save', to: 'protocols#protocolsio_import_save' get 'export', to: 'protocols#export' end @@ -449,7 +450,7 @@ Rails.application.routes.draw do resources :repositories do post 'repository_index', - to: 'repositories#repository_table_index', + to: 'repository_rows#index', as: 'table_index', defaults: { format: 'json' } # Save repository table state @@ -472,7 +473,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 +480,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/controllers/repository_list_items_controller_spec.rb b/spec/controllers/repository_list_items_controller_spec.rb new file mode 100644 index 000000000..e28fe2377 --- /dev/null +++ b/spec/controllers/repository_list_items_controller_spec.rb @@ -0,0 +1,86 @@ +require 'rails_helper' + +describe RepositoryListItemsController, type: :controller do + login_user + render_views + let(:user) { User.first } + let!(:team) { create :team, created_by: user } + let!(:user_team) { create :user_team, team: team, user: user } + let!(:repository) { create :repository, team: team, created_by: user } + let!(:repository_column_one) do + create :repository_column, repository: repository, + created_by: user, + name: 'List Value', + data_type: 'RepositoryListValue' + end + let!(:repository_list_item_one) do + create :repository_list_item, data: 'item one', + repository: repository, + repository_column: repository_column_one + end + let!(:repository_list_item_two) do + create :repository_list_item, data: 'item two', + repository: repository, + repository_column: repository_column_one + end + + let!(:user_two) { create :user, email: 'new@user.com' } + let!(:team_two) { create :team, created_by: user } + let!(:user_team_two) { create :user_team, team: team_two, user: user_two } + let!(:repository_two) do + create :repository, team: team_two, created_by: user_two + end + let!(:user_two_repository_column_two) do + create :repository_column, repository: repository_two, + created_by: user_two, + name: 'List Value', + data_type: 'RepositoryListValue' + end + let!(:user_two_repository_list_item_one) do + create :repository_list_item, + data: 'item one of user two', + repository: repository_two, + repository_column: user_two_repository_column_two, + created_by: user_two, + last_modified_by: user_two + end + let!(:user_two_repository_list_item_two) do + create :repository_list_item, + data: 'item two of user two', + repository: repository_two, + repository_column: user_two_repository_column_two, + created_by: user_two, + last_modified_by: user_two + end + + describe '#search' do + let(:params) { { q: '', column_id: repository_column_one.id } } + it 'returns all column\'s list items' do + get :search, format: :json, params: params + expect(response).to have_http_status(:ok) + body = JSON.parse(response.body) + expect(body['list_items'].count).to eq 2 + end + + it 'returns queried item' do + params[:q] = 'item one' + get :search, format: :json, params: params + expect(response).to have_http_status(:success) + body = JSON.parse(response.body) + expect(body['list_items'].count).to eq 1 + expect(body['list_items'].first['data']).to eq 'item one' + end + + it 'returns a 404 error if column does not exist' do + params[:column_id] = 999999 + get :search, format: :json, params: params + expect(response).to have_http_status(:not_found) + end + + it 'returns a 403 error user does not have permissions' do + params[:column_id] = user_two_repository_column_two.id + get :search, format: :json, params: params + expect(response).to have_http_status(:forbidden) + end + end +end diff --git a/spec/controllers/repository_rows_controller_spec.rb b/spec/controllers/repository_rows_controller_spec.rb index a7a0f2f52..0ee849e91 100644 --- a/spec/controllers/repository_rows_controller_spec.rb +++ b/spec/controllers/repository_rows_controller_spec.rb @@ -7,6 +7,13 @@ describe RepositoryRowsController, type: :controller do let!(:team) { create :team, created_by: user } let!(:user_team) { create :user_team, team: team, user: user } let!(:repository) { create :repository, team: team, created_by: user } + let!(:repository_state) do + RepositoryTableState.create( + repository: repository, + user: user, + state: Constants::REPOSITORY_TABLE_DEFAULT_STATE + ) + end let!(:repository_row) do create :repository_row, repository: repository, created_by: user, @@ -41,4 +48,71 @@ describe RepositoryRowsController, type: :controller do expect(response).to have_http_status(:success) end end + + context '#index' do + before do + repository.repository_rows.destroy_all + 110.times do |index| + create :repository_row, name: "row (#{index})", + repository: repository, + created_by: user, + last_modified_by: user + end + end + + describe 'json object' do + it 'returns a valid object' do + params = { order: { 0 => { column: '3', dir: 'asc' } }, + drow: '0', + search: { value: '' }, + length: '10', + start: '1', + repository_id: repository.id } + get :index, params: params, format: :json + + expect(response.status).to eq 200 + expect(response).to match_response_schema('repository_row_datatables') + end + end + + describe 'pagination' do + it 'returns first 10 records' do + params = { order: { 0 => { column: '3', dir: 'asc' } }, + drow: '0', + search: { value: '' }, + length: '10', + start: '1', + repository_id: repository.id } + get :index, params: params, format: :json + response_body = JSON.parse(response.body) + expect(response_body['data'].length).to eq 10 + expect(response_body['data'].first['2']).to eq 'row (0)' + end + + it 'returns next 10 records' do + params = { order: { 0 => { column: '3', dir: 'asc' } }, + drow: '0', + search: { value: '' }, + length: '10', + start: '11', + repository_id: repository.id } + get :index, params: params, format: :json + response_body = JSON.parse(response.body) + expect(response_body['data'].length).to eq 10 + expect(response_body['data'].first['2']).to eq 'row (10)' + end + + it 'returns first 25 records' do + params = { order: { 0 => { column: '2', dir: 'desc' } }, + drow: '0', + search: { value: '' }, + length: '25', + start: '1', + repository_id: repository.id } + get :index, params: params, format: :json + response_body = JSON.parse(response.body) + expect(response_body['data'].length).to eq 25 + end + end + end end 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/spec/models/repository_list_value_spec.rb b/spec/models/repository_list_value_spec.rb index 2e37af411..8904b84d4 100644 --- a/spec/models/repository_list_value_spec.rb +++ b/spec/models/repository_list_value_spec.rb @@ -98,11 +98,11 @@ RSpec.describe RepositoryListValue, type: :model do it 'retuns only the the item related to the list' do repository_row_two = create :repository_row, name: 'New row' - repository_list_value_two = - create :repository_list_value, repository_cell_attributes: { - repository_column: repository_column, - repository_row: repository_row_two - } + create :repository_list_value, + repository_cell_attributes: { + repository_column: repository_column, + repository_row: repository_row_two + } list_item = create :repository_list_item, data: 'new item', repository: repository, @@ -113,16 +113,15 @@ RSpec.describe RepositoryListValue, type: :model do end it 'returns an empty string if no item selected' do - list_item = create :repository_list_item, - data: 'my item', - repository: repository, - repository_column: repository_column - expect(repository_list_value.reload.data).to eq nil + create :repository_list_item, data: 'my item', + repository: repository, + repository_column: repository_column + expect(repository_list_value.reload.data).to be_nil end it 'returns an empty string if item does not exists' do repository_list_value.repository_list_item = nil - expect(repository_list_value.reload.data).to eq nil + expect(repository_list_value.reload.data).to be_nil end end end diff --git a/spec/services/repository_datatable_service_spec.rb b/spec/services/repository_datatable_service_spec.rb new file mode 100644 index 000000000..e32b13f19 --- /dev/null +++ b/spec/services/repository_datatable_service_spec.rb @@ -0,0 +1,133 @@ +require 'rails_helper' + +describe RepositoryDatatableService do + let!(:team) { create :team } + let!(:user) { create :user, email: 'user_one@asdf.com' } + let!(:repository) do + create :repository, name: 'my repo', + created_by: user, + team: team + end + let!(:repository_column) do + create :repository_column, name: 'My column', + data_type: :RepositoryListValue + end + let!(:repository_state) do + RepositoryTableState.create( + repository: repository, + user: user, + state: Constants::REPOSITORY_TABLE_DEFAULT_STATE + ) + end + let!(:repository_row) do + create :repository_row, name: 'A row', + repository: repository, + created_by: user, + last_modified_by: user + end + let!(:repository_row_two) do + create :repository_row, name: 'B row', + repository: repository, + created_by: user, + last_modified_by: user + end + let!(:list_item) do + create :repository_list_item, + data: 'bug', + repository: repository, + repository_column: repository_column, + created_by: user, + last_modified_by: user + end + let!(:repository_list_value) do + create :repository_list_value, + repository_list_item: list_item, + created_by: user, + last_modified_by: user, + repository_cell_attributes: { + repository_column: repository_column, + repository_row: repository_row + } + end + + context 'object' do + let(:params) do + { order: { 0 => { column: '2', dir: 'asc' } }, + search: { value: 'row' } } + end + + let(:subject) do + RepositoryDatatableService.new(repository, params, user) + end + + describe '#build_conditions/1' do + it 'parsers the contitions' do + contitions = subject.send(:build_conditions, params) + expect(contitions[:search_value]).to eq 'row' + expect(contitions[:order_by_column]).to eq( + { column: 2, dir: 'asc' } + ) + end + end + + describe '#sortable_columns' do + it 'returns an array of all columns that are sortable' do + columns = subject.send(:sortable_columns) + expect(columns.length).to eq 5 + end + end + + describe '#sort_null_direction' do + it 'returns LAST if value is ascending' do + result = subject.send(:sort_null_direction, 'ASC') + expect(result).to eq 'LAST' + end + + it 'returns FIRST if value is not ascending' do + result = subject.send(:sort_null_direction, 'DESC') + expect(result).to eq 'FIRST' + end + end + end + + describe 'ordering' do + it 'is ordered by row name asc' do + params = { order: { 0 => { column: '2', dir: 'asc' } }, + search: { value: '' } } + subject = RepositoryDatatableService.new(repository, + params, + user) + expect(subject.repository_rows.first.name).to eq 'A row' + expect(subject.repository_rows.last.name).to eq 'B row' + end + + it 'is ordered by row name desc' do + params = { order: { 0 => { column: '2', dir: 'desc' } }, + search: { value: '' } } + subject = RepositoryDatatableService.new(repository, + params, + user) + expect(subject.repository_rows.first.name).to eq 'B row' + expect(subject.repository_rows.last.name).to eq 'A row' + end + end + + describe 'search' do + before do + create :repository_row, name: 'test', + repository: repository, + created_by: user, + last_modified_by: user + end + + it 'returns only the searched entity' do + params = { order: { 0 => { column: '2', dir: 'desc' } }, + search: { value: 'test' } } + subject = RepositoryDatatableService.new(repository, + params, + user) + expect(subject.repository_rows.first.name).to eq 'test' + expect(subject.repository_rows.count).to eq 1 + end + end +end diff --git a/spec/support/api/schemas/repository_row_datatables.json b/spec/support/api/schemas/repository_row_datatables.json new file mode 100644 index 000000000..0a73d82f6 --- /dev/null +++ b/spec/support/api/schemas/repository_row_datatables.json @@ -0,0 +1,25 @@ +{ + "type": "object", + "required": ["draw", "recordsTotal", "recordsFiltered", "data"], + "properties": { + "draw": { "type": "integer" }, + "recordsTotal": { "type": "integer" }, + "recordsFiltered": { "type": "integer" }, + "data": { + "type": "array", + "items":{ + "required": ["DT_RowId", "1", "2", "3", "4", "recordEditUrl", "recordUpdateUrl", "recordInfoUrl"], + "properties": { + "DT_RowId": { "type": "integer" }, + "1": { "type": "string" }, + "2": { "type": "string" }, + "3": { "type": "string" }, + "4": { "type": "string" }, + "recordEditUrl": { "type": "string" }, + "recordUpdateUrl": { "type": "string" }, + "recordInfoUrl": { "type": "string" } + } + } + } + } +} 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("