implement search on list items dropdown, update, create actions [fixes SCI-2070]

This commit is contained in:
zmagod 2018-03-15 15:43:16 +01:00
parent 0baa7a1f99
commit d69754af04
13 changed files with 299 additions and 41 deletions

View file

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

View file

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

View file

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

View file

@ -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 = '<select class="form-control selectpicker" ';
html += 'data-abs-min-length="2" data-live-search="true" ';
html += 'column_id="' + column_id +'">';
html += '<option value="-1"></option>';
$.each(options, function(index, value) {
var selected = (current_value === value[1]) ? 'selected' : '';
html += '<option value="' + value[0] + '" ' + selected + '>';
html += value[1] + '</option>';
});
html += '</select>';
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 + "'></input></div>";
}
// 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');

View file

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

View file

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

View file

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

View file

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

View file

@ -20,7 +20,7 @@
<th id="added-on"><%= t("repositories.table.added_on") %></th>
<th id="added-by"><%= t("repositories.table.added_by") %></th>
<% repository.repository_columns.order(:id).each do |column| %>
<th class="repository-column" id="<%= column.id %>"
<th class="repository-column" id="<%= column.id %>" data-type="<%= column.data_type %>"
<%= 'data-editable' if can_update_or_delete_repository_column?(column) %>
<%= 'data-deletable' if can_update_or_delete_repository_column?(column) %>
<%= "data-edit-url='#{edit_repository_repository_column_path(repository, column)}'" %>

View file

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

View file

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

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,16 @@
/*!
* 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:47 PM EST
*/.bootstrap-select .status{background:#f0f0f0;clear:both;color:#999;font-size:11px;font-style:italic;font-weight:500;line-height:1;margin-bottom:-5px;padding:10px 20px}