Merge branch 'ux-release-1' of https://github.com/biosistemika/scinote-web into zd_SCI_2139_2121

This commit is contained in:
zmagod 2018-03-20 14:46:11 +01:00
commit 62858bc14f
33 changed files with 1108 additions and 454 deletions

View file

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

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

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

@ -26,7 +26,6 @@ $toggle-btn-size: 50px;
z-index: 1000;
position: fixed;
width: $wrapper-width;
left: $wrapper-width;
height: 100%;
margin-left: -$wrapper-width;
-webkit-transition: all 0.5s ease;
@ -99,6 +98,7 @@ $toggle-btn-size: 50px;
padding-left: 0;
#sidebar-wrapper {
margin-left: 0;
width: 0;
#slide-panel {

View file

@ -344,15 +344,6 @@
padding: 10px 0 10px 30px;
}
#power-up-button {
margin-right: 15px;
margin-top: 8px;
.btn-danger {
width: 150px;
}
}
// Alert
.alert {
border-radius: 0;
@ -377,6 +368,7 @@
#content-wrapper {
margin-top: 50px;
margin-left: 83px;
}

View file

@ -0,0 +1,57 @@
@import "constants";
@import "mixins";
.menu-bar {
background-color: $color-white;
box-shadow: inset -4px 0 0 0 $color-alto;
height: 100%;
left: 0;
overflow-x: hidden;
padding-bottom: 16px;
padding-top: 16px;
position: fixed;
width: 83px;
ul.nav > li {
padding-right: 4px;
& > a {
color: $color-gray;
display: grid;
font-size: 12px;
margin-left: auto;
margin-right: auto;
padding: 10px;
text-align: center;
& > span {
padding-top: 4px;
}
}
&.active {
background-color: $color-gainsboro;
margin-right: 4px;
padding-right: 0;
@include box-shadow(4px 0 0 $color-theme-primary);
& > a {
color: $color-emperor;
}
}
}
ul.nav-bottom {
bottom: 0;
padding-bottom: 16px;
position: fixed;
width: inherit;
}
}
@media(max-height:480px) {
.menu-bar > ul.nav-bottom {
position: relative;
width: auto;
}
}

View file

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

View file

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

View file

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

View file

@ -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)
"<span class='circle'>&nbsp;</span>"
else
"<span class='circle disabled'>&nbsp;</span>"
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

View file

@ -0,0 +1,31 @@
module LeftMenuBarHelper
def projects_are_selected?
controller_name.in? %w(projects experiments my_modules)
end
def repositories_are_selected?
controller_name == 'repositories'
end
def templates_are_selected?
# TODO
controller_name == 'protocols'
end
def reports_are_selected?
# TODO
controller_name == 'reports'
end
def settings_are_selected?
controller_name.in? %(registrations preferences addons teams)
end
def activities_are_selected?
controller_name == 'activities'
end
def help_is_selected?
# TODO
end
end

View file

@ -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)
"<span class='circle'>&nbsp;</span>"
else
"<span class='circle disabled'>&nbsp;</span>"
end
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

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

View file

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

View file

@ -64,6 +64,7 @@
<% end %>
</div>
<%= render "shared/left_menu_bar" if user_signed_in? %>
<div id="content-wrapper">
<%= yield :content %>
</div>

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_manage_repository_column?(column) %>
<%= 'data-deletable' if can_manage_repository_column?(column) %>
<%= "data-edit-url='#{edit_repository_repository_column_path(repository, column)}'" %>

View file

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

View file

@ -0,0 +1,48 @@
<div id="left-menu-bar" class="menu-bar">
<ul class="nav">
<li class="<%= "active" if projects_are_selected? %>">
<a id="projects-link" title="<%= t('left_menu_bar.projects') %>" href="<%= projects_path %>">
<span class="glyphicon glyphicon-home"></span>
<span><%= t('left_menu_bar.projects') %></span>
</a>
</li>
<li class="<%= "active" if repositories_are_selected? %>">
<a id="repositories-link" title="<%= t('left_menu_bar.repositories') %>" href="<%= team_repositories_path current_team %>">
<span class="fa fa-cubes" aria-hidden="true"></span>
<span><%= t('left_menu_bar.repositories') %></span>
</a>
</li>
<li class="<%= "active" if templates_are_selected? %>">
<a id="templates-link" title="<%= t('left_menu_bar.templates') %>" href="<%= protocols_path %>">
<span class="glyphicon glyphicon-list-alt"></span>
<span><%= t('left_menu_bar.templates') %></span>
</a>
</li>
<li class="<%= "active" if reports_are_selected? %>">
<a id="reports-link" title="<%= t('left_menu_bar.reports') %>" href="#">
<span class="glyphicon glyphicon-indent-left"></span>
<span><%= t('left_menu_bar.reports') %></span>
</a>
</li>
<li class="<%= "active" if settings_are_selected? %>">
<a id="settings-link" title="<%= t('left_menu_bar.settings') %>" href="<%= edit_user_registration_path %>">
<span class="glyphicon glyphicon-cog"></span>
<span><%= t('left_menu_bar.settings') %></span>
</a>
</li>
</ul>
<ul class="nav nav-bottom">
<li class="<%= "active" if activities_are_selected? %>">
<a id="activities-link" title="<%= t('left_menu_bar.activities') %>" href="<%= activities_path %>">
<span class="glyphicon glyphicon-equalizer"></span>
<span><%= t('left_menu_bar.activities') %></span>
</a>
</li>
<li class="<%= "active" if help_is_selected? %>">
<a id="activities-link" title="<%= t('left_menu_bar.help') %>" href="#">
<span class="glyphicon glyphicon-paperclip"></span>
<span><%= t('left_menu_bar.help') %></span>
</a>
</li>
</ul>
</div>

View file

@ -86,27 +86,6 @@
</div>
<% end %>
</li>
<li id="power-up-button">
<div class="dropdown">
<button class="btn btn-danger dropdown-toggle"
type="button"
id="power-up-dropdown"
data-toggle="dropdown"
aria-haspopup="true"
aria-expanded="true">
<%=t 'nav.power_up' %>
</button>
<ul class="dropdown-menu" aria-labelledby="power-up-dropdown">
<li class="dropdown-header"><%=t 'nav.power_up_dropdown.title' %></li>
<li><span class="glyphicon glyphicon-certificate"></span> <%=t 'nav.power_up_dropdown.unlimited_teams' %></li>
<li><span class="glyphicon glyphicon-certificate"></span> <%=t 'nav.power_up_dropdown.unlimited_users' %></li>
<li><span class="glyphicon glyphicon-certificate"></span> <%=t 'nav.power_up_dropdown.unlimited_storage' %></li>
<li><span class="glyphicon glyphicon-certificate"></span> <%=t 'nav.power_up_dropdown.manuscript' %></li>
<li><%=t 'nav.power_up_dropdown.much_more' %></li>
<li class="text-center"><a href="#" class=""></a></li>
</ul>
</div>
</li>
<!-- greetings -->
<li id="user-account-dropdown" class="dropdown">

View file

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

View file

@ -69,14 +69,6 @@ en:
nav:
search: "Search for something..."
search_button: 'Go!'
power_up: 'Power Up!'
power_up_dropdown:
title: 'Go premum and power up!'
unlimited_teams: 'Unlimited Teams'
unlimited_users: 'Unlimited Users'
unlimited_storage: 'Unlimited Storage'
manuscript: 'ManuScript'
much_more: 'And much, much more...'
user_greeting: "Hi, %{full_name}"
advanced_search: "Advanced search"
title: "sciNote"
@ -110,6 +102,15 @@ en:
core_version: "sciNote core version"
addon_versions: "Addon versions"
left_menu_bar:
projects: "Projects"
repositories: "Libraries"
templates: "Templates"
reports: "Reports"
settings: "Settings"
activities: "Activities"
help: "Help"
sidebar:
title: "Navigation"
no_module_group: "No workflow"
@ -995,12 +996,12 @@ en:
head_title: "Libraries | %{library}"
repository_row:
modal_info:
head_title: "Information for record '%{repository_row}'"
head_title: "Information for item '%{repository_row}'"
added_on: "Added on"
added_by: "Added by"
custom_field: "%{cf}: "
title: "This record is assigned to %{nr} tasks."
no_tasks: "This record in not assigned to any task."
title: "This item is assigned to %{nr} tasks."
no_tasks: "This item in not assigned to any task."
samples:
columns: "Columns"
columns_visibility: "Toggle visibility"

View file

@ -439,7 +439,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
@ -447,7 +448,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
@ -470,7 +471,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'
@ -478,6 +478,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

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

View file

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

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

View file

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

View file

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

View file

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

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}