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 @@