Merge branch 'develop' into ok_SCI_3988

This commit is contained in:
Oleksii Kriuchykhin 2020-01-07 17:07:22 +01:00
commit 522ae12f08
195 changed files with 10090 additions and 2135 deletions

View file

@ -1,6 +1,6 @@
ruby:
config_file: .rubocop.yml
version: 0.72.0
version: 0.75.0
eslint:
enabled: true

View file

@ -83,7 +83,7 @@ Naming/FileName:
Enabled: false
Exclude: []
Layout/FirstParameterIndentation:
Layout/FirstArgumentIndentation:
EnforcedStyle: consistent
Style/For:
@ -364,14 +364,6 @@ Metrics/ModuleLength:
Metrics/CyclomaticComplexity:
Enabled: false
Metrics/LineLength:
Max: 120
AllowHeredoc: true
AllowURI: true
URISchemes:
- http
- https
Metrics/MethodLength:
Enabled: false
@ -392,6 +384,14 @@ Layout/EndAlignment:
Layout/DefEndAlignment:
EnforcedStyleAlignWith: start_of_line
Layout/LineLength:
Max: 120
AllowHeredoc: true
AllowURI: true
URISchemes:
- http
- https
##################### Lint #####################################
Lint/AssignmentInCondition:

View file

@ -122,7 +122,7 @@ group :development, :test do
gem 'pry-rails'
gem 'rails-controller-testing'
gem 'rspec-rails', '>= 4.0.0.beta2'
gem 'rubocop', '>= 0.59.0', require: false
gem 'rubocop', '>= 0.75.0', require: false
gem 'rubocop-performance'
gem 'rubocop-rails'
gem 'timecop'

View file

@ -287,7 +287,7 @@ GEM
mini_magick (>= 4.9.5, < 5)
ruby-vips (>= 2.0.13, < 3)
iniparse (1.4.4)
jaro_winkler (1.5.3)
jaro_winkler (1.5.4)
jbuilder (2.9.1)
activesupport (>= 4.2.0)
jmespath (1.4.0)
@ -387,8 +387,8 @@ GEM
overcommit (0.49.1)
childprocess (>= 0.6.3, < 2.0)
iniparse (~> 1.4)
parallel (1.17.0)
parser (2.6.4.0)
parallel (1.19.1)
parser (2.6.5.0)
ast (~> 2.4.0)
pg (1.1.4)
pg_search (2.3.0)
@ -486,16 +486,16 @@ GEM
rspec-mocks (~> 3.8)
rspec-support (~> 3.8)
rspec-support (3.8.2)
rubocop (0.74.0)
rubocop (0.78.0)
jaro_winkler (~> 1.5.1)
parallel (~> 1.10)
parser (>= 2.6)
rainbow (>= 2.2.2, < 4.0)
ruby-progressbar (~> 1.7)
unicode-display_width (>= 1.4.0, < 1.7)
rubocop-performance (1.4.1)
rubocop-performance (1.5.1)
rubocop (>= 0.71.0)
rubocop-rails (2.3.2)
rubocop-rails (2.4.0)
rack (>= 1.1)
rubocop (>= 0.72.0)
ruby-graphviz (1.2.4)
@ -669,7 +669,7 @@ DEPENDENCIES
rgl
roo (~> 2.8.2)
rspec-rails (>= 4.0.0.beta2)
rubocop (>= 0.59.0)
rubocop (>= 0.75.0)
rubocop-performance
rubocop-rails
ruby-graphviz (~> 1.2)

View file

@ -29,20 +29,24 @@
//= require jsPlumb-2.0.4-min
//= require jsnetworkx
//= require bootstrap-select
//= require_directory ./sitewide
//= require_directory ./repository_columns/columns_initializers
//= require datatables
//= require ajax-bootstrap-select.min
//= require underscore
//= require i18n.js
//= require i18n/translations
//= require users/settings/teams/invite_users_modal
//= require repository_columns/index
//= require perfect-scrollbar.min
//= require shared/inline_editing
//= require activestorage
//= require global_activities/side_pane
//= require protocols/header
//= require turbolinks
//= require marvinjslauncher
//= require_tree ./repositories/renderers
//= require_directory ./repositories/validators
//= require_directory ./sitewide
//= require turbolinks
// Initialize links for submitting forms. This is useful for submitting

View file

@ -1,396 +1,260 @@
/* global Promise _ ActiveStorage RepositoryItemEditForm */
//= require sugar.min
//= require jquerymy-1.2.14.min
(function(global) {
'use strict';
/**
* RepositoryItemEditForm generates the html inputs for
* repository item and returns the form data object
*
* @param {Object} itemData - repository item data fetched from the API
* @param {Object} repositoryItemElement - row node in the table
*/
global.RepositoryItemEditForm = function(itemData, repositoryItemElement) {
this.itemData = itemData;
this.repositoryItemElement = repositoryItemElement;
this.formData = this.composeFormData(itemData);
}
/**
* Generates the input fields
*
* @param {Object} table - datatable.js object
* @returns {undefinded}
*/
RepositoryItemEditForm.prototype.renderForm = function(table) {
var colIndex = getColumnIndex(table, '#row-name');
var cells = this.itemData.repository_row.repository_cells;
var listColumns = this.itemData.repository_row.repository_column_items;
var formData = this.formData;
if (colIndex) {
$(this.repositoryItemElement).children('td').eq(colIndex)
.html(changeToInputField('repository_row',
'name',
this.itemData.repository_row.name,
'rowName'));
}
$(this.repositoryItemElement).children('td').each(function(i) {
var td = $(this);
var rawIndex = table.column.index('fromVisible', i);
var colHeader = table.column(rawIndex).header();
if ($(colHeader).hasClass('repository-column')) {
var type = $(colHeader).attr('data-type');
var colHeaderId = $(colHeader).attr('id');
var cell = cells[colHeaderId] || '';
td.html(changeToFormField('repository_cell',
colHeaderId,
type,
cell,
listColumns));
addSelectedFile(type, colHeaderId);
appendNewElementToFormData(cell, colHeaderId, formData);
}
});
initializeDataBinding(this.repositoryItemElement, formData);
}
/**
* Parse received data in to a from object
*
* @param {Object} itemData - json representations of repository item
*
* @returns {Object}
*/
RepositoryItemEditForm.prototype.composeFormData = function(itemData) {
var formBindingsData = {};
formBindingsData['rowName'] = itemData.repository_row.name;
$.each(itemData.repository_row.repository_cells, function(i, cell) {
var tableCellId = 'colId-' + cell.cell_column_id;
if(cell.type === 'RepositoryAssetValue') {
formBindingsData[tableCellId] = new File([""], cell.value.file_name);
} else {
formBindingsData[tableCellId] = cell.value;
}
});
return formBindingsData;
}
/**
* Handles select picker default value
*
* @param {Object} node
* @returns {undefinded}
*/
RepositoryItemEditForm.prototype.initializeSelectpickerValues = function(node) {
$($(node).find('.bootstrap-select')).each(function(_, dropdown) {
var selectedValue = $($(dropdown).find('select')[0]).data('selected-value');
var selectPicker = $($(dropdown).find('select')[0]);
var value = '-1'
$(dropdown).find('option').each(function(_, option) {
$(option).removeAttr('selected');
if($(option).val() === selectedValue.toString()) {
$(option).attr('selected', true);
value = $(option).attr('value');
}
});
$(dropdown).parent().attr("list_item_id", value);
selectPicker.val(value);
selectPicker.selectpicker('refresh');
});
}
/**
* Creates a FormData object with the repository row data ready to be
* sended on the server
*
* @param {Object} tableID
* @param {Object} selectedRecord
*
* @returns (Object)
*/
RepositoryItemEditForm.prototype.parseToFormObject = function(tableID, selectedRecord) {
var formData = this.formData;
var formDataObj = new FormData();
var removeFileColumns = [];
var filesToUploadCntr = 0;
var filesUploadedCntr = 0;
const directUploadUrl = $(tableID).data('directUploadUrl');
formDataObj.append('request_url', $(tableID).data('current-uri'));
formDataObj.append('repository_row_id', $(selectedRecord).attr('id'));
return new Promise((resolve, reject) => {
$(_.keys(this.formData)).each(function(_, element) {
var value = formData[element];
if (element === 'rowName') {
formDataObj.append('repository_row_name', value);
} else {
let colId = element.replace('colId-', '');
let $el = $('#' + element);
// don't save anything if element is not visible
if ($el.length === 0) {
return;
}
if ($el.attr('type') === 'file') {
// handle deleting of element
if ($el.attr('remove') === 'true') {
removeFileColumns.push(colId);
formDataObj.append('repository_cells[' + colId + ']', null);
} else if ($el[0].files.length > 0) {
filesToUploadCntr += 1;
}
} else if (value.length >= 0) {
formDataObj.append('repository_cells[' + colId + ']', value);
}
}
});
formDataObj.append('remove_file_columns', JSON.stringify(removeFileColumns));
// No files for upload, so return earlier
if (filesToUploadCntr === 0) {
resolve(formDataObj);
return;
}
// Second run, just for files
$(_.keys(this.formData)).each(function(_, element) {
let $el = $('#' + element);
let colId = element.replace('colId-', '');
if ($el.attr('type') === 'file' && $el.attr('remove') !== 'true') {
let upload = new ActiveStorage.DirectUpload($el[0].files[0], directUploadUrl);
upload.create(function(error, blob) {
if (error) {
reject(error);
} else {
formDataObj.append('repository_cells[' + colId + ']', blob.signed_id);
filesUploadedCntr += 1;
if (filesUploadedCntr === filesToUploadCntr) {
resolve(formDataObj);
}
}
});
}
});
});
};
/**
* |-----------------|
* | Private methods |
* |-----------------|
*/
/**
* Takes object and surrounds it with input
*
* @param {Object} object
* @param {String} name
* @param {String} value
* @param {String} id
*
* @returns (String)
*/
function changeToInputField(object, name, value, id) {
return "<div class='form-group'><input class='form-control' data-object='"
+ object + "' name='" + name + "' value='" + value + "' id='" + id + "'></input></div>";
}
/**
* Takes object and creates an input file field, contains a hidden
* input field which is triggered on button click and we get the uploaded
* file from there.
*
* @param {Object} object
* @param {String} name
* @param {String} value
* @param {String} id
*
* @returns (String)
*/
function changeToInputFileField(object, name, value, id) {
var fileName = (value.file_name) ? value.file_name : I18n.t('general.file.no_file_chosen');
var buttonLabel = I18n.t('general.file.choose');
var html = "<div class='repository-input-file-field'>" +
"<div class='form-group'><div><input type='file' name='" + name + "' id='" +
id + "' style='display:none' /><button class='btn btn-default' " +
"data-object='" + object + "' name='" + name + "' value='" + value +
"' data-id='" + id + "'>" + buttonLabel +
"</button></div><div><p class='file-name-label'>" + truncateLongString(fileName, 20) +
"</p></div>";
if(value.file_name) {
html += "<div><a data-action='removeAsset' ";
html += "onClick='clearFileInput(this)'><i class='fas fa-times'></i></a>";
} else {
html += "<div><a data-action='removeAsset' onClick='clearFileInput(this)' ";
html += "style='display:none'><i class='fas fa-times'></i></a>";
}
html += "</div></div></div>";
return html;
}
/**
* Returns the colum index
*
* @param {Object} table
* @param {String} id
*
* @returns (Boolean | Number)
*/
function getColumnIndex(table, id) {
if(id < 0)
return false;
return table.column(id).index('visible');
}
/**
* Genrates list items dropdown element
*
* @param {Array} options
* @param {String} current_value
* @param {Number} columnId
* @param {String} id
*
* @returns (String)
*/
function _listItemDropdown(options, current_value, columnId, id) {
var val = undefined;
var html = '<select id="' + id + '" class="form-control selectpicker repository-dropdown" ';
html += 'data-selected-value="" data-abs-min-length="2" data-live-search="true" ';
html += 'data-container="body" column_id="' + columnId +'">';
html += '<option value="-1"></option>';
$.each(options, function(index, value) {
var selected = '';
if (current_value === value[1]) {
selected = 'selected';
val = value[0];
}
html += '<option value="' + value[0] + '" ' + selected + '>';
html += value[1] + '</option>';
});
html += '</select>';
return (val) ? $(html).attr('data-selected-value', val)[0] : html;
}
/**
* Takes an object and creates custom html element
*
* @param {String} object
* @param {String} name
* @param {String} column_type
* @param {Object} cell
* @param {Object} listColumns
*
* @returns (String)
*/
function changeToFormField(object, name, column_type, cell, listColumns) {
var cellId = generateInputFieldReference(name);
var value = cell.value || '';
if (column_type === 'RepositoryListValue') {
var column = _.findWhere(listColumns,
{ column_id: parseInt(name, 10) });
var list_items = column.list_items || cell.list_items;
return _listItemDropdown(list_items, value, parseInt(name, 10), cellId);
} else if (column_type === 'RepositoryAssetValue') {
return changeToInputFileField('repository_cell_file', name, value, cellId);
} else {
return changeToInputField(object, name, value, cellId);
}
}
/**
* Append the change listener to file field
*
* @param {String} type
* @param {String} name
*
* @returns {undefined}
*/
function addSelectedFile(type, name) {
var button = $('button[data-id="' +
generateInputFieldReference(name) +
'"]');
if (type === 'RepositoryAssetValue') {
var fileInput = $(button.parent().find('input[type="file"]')[0]);
button.on('click', function(ev) {
ev.preventDefault();
ev.stopPropagation();
fileInput.trigger('click');
initFileHandler(fileInput);
});
}
}
/**
* Handle extraction of file from the input field
*
* @param {Object} $inputField
*
* @returns {undefined}
*/
function initFileHandler($inputField) {
$inputField.on('change', function() {
var input = $(this);
var $label = $($(this).closest('.repository-input-file-field')
.find('.file-name-label')[0]);
var file = this.files[0];
if (file) {
$label.text(truncateLongString(file.name, 20));
input.attr('remove', false);
$($label.closest('.repository-input-file-field')
.find('[data-action="removeAsset"]')[0]).show();
}
})
}
/**
* Initializes the data binding for form object
*
* @param {Object} rowNode
* @param {Object} data
*
* @returns {undefined}
*/
function initializeDataBinding(rowNode, data) {
var uiBindings = {};
$.each(_.keys(data), function(i, element) {
uiBindings['#' + element] = element;
})
$(rowNode).my({ui: uiBindings}, data);
}
/**
* Generates the input tag id that will be used in the formData object
*
* @param {String} columnId
*
* @returns {String}
*/
function generateInputFieldReference(columnId) {
return 'colId-' + columnId;
}
/**
* Appends aditional fields to form data object
* @param {Object} cell
* @param {String} columnId
* @param {Object} formData
*
* @returns {undefined}
*/
function appendNewElementToFormData(cell, columnId, formData) {
if (!cell.repository_cell_id) {
formData[generateInputFieldReference(columnId)] = undefined;
}
}
}(window));
// /* global Promise _ ActiveStorage RepositoryItemEditForm */
//
// //= require sugar.min
// //= require jquerymy-1.2.14.min
//
// (function(global) {
// 'use strict';
//
// /**
// * Creates a FormData object with the repository row data ready to be
// * sended on the server
// *
// * @param {Object} tableID
// * @param {Object} selectedRecord
// *
// * @returns (Object)
// */
// RepositoryItemEditForm.prototype.parseToFormObject = function(tableID, selectedRecord) {
// var formData = this.formData;
// var formDataObj = new FormData();
// var removeFileColumns = [];
// var filesToUploadCntr = 0;
// var filesUploadedCntr = 0;
// const directUploadUrl = $(tableID).data('directUploadUrl');
//
// formDataObj.append('request_url', $(tableID).data('current-uri'));
// formDataObj.append('repository_row_id', $(selectedRecord).attr('id'));
//
// return new Promise((resolve, reject) => {
// $(_.keys(this.formData)).each(function(_, element) {
// var value = formData[element];
// if (element === 'rowName') {
// formDataObj.append('repository_row_name', value);
// } else {
// let colId = element.replace('colId-', '');
// let $el = $('#' + element);
// // don't save anything if element is not visible
// if ($el.length === 0) {
// return;
// }
// if ($el.attr('type') === 'file') {
// // handle deleting of element
// if ($el.attr('remove') === 'true') {
// removeFileColumns.push(colId);
// formDataObj.append('repository_cells[' + colId + ']', null);
// } else if ($el[0].files.length > 0) {
// filesToUploadCntr += 1;
// }
// } else if (value.length >= 0) {
// formDataObj.append('repository_cells[' + colId + ']', value);
// }
// }
// });
//
// formDataObj.append('remove_file_columns', JSON.stringify(removeFileColumns));
//
// // No files for upload, so return earlier
// if (filesToUploadCntr === 0) {
// resolve(formDataObj);
// return;
// }
//
// // Second run, just for files
// $(_.keys(this.formData)).each(function(_, element) {
// let $el = $('#' + element);
// let colId = element.replace('colId-', '');
//
// if ($el.attr('type') === 'file' && $el.attr('remove') !== 'true') {
// let upload = new ActiveStorage.DirectUpload($el[0].files[0], directUploadUrl);
//
// upload.create(function(error, blob) {
// if (error) {
// reject(error);
// } else {
// formDataObj.append('repository_cells[' + colId + ']', blob.signed_id);
// filesUploadedCntr += 1;
//
// if (filesUploadedCntr === filesToUploadCntr) {
// resolve(formDataObj);
// }
// }
// });
// }
// });
// });
// };
//
// /**
// * Takes object and creates an input file field, contains a hidden
// * input field which is triggered on button click and we get the uploaded
// * file from there.
// *
// * @param {Object} object
// * @param {String} name
// * @param {String} value
// * @param {String} id
// *
// * @returns (String)
// */
// function changeToInputFileField(object, name, value, id) {
// var fileName = (value.file_name) ? value.file_name : I18n.t('general.file.no_file_chosen');
// var buttonLabel = I18n.t('general.file.choose');
// var html = "<div class='repository-input-file-field'>" +
// "<div class='form-group'><div><input type='file' name='" + name + "' id='" +
// id + "' style='display:none' /><button class='btn btn-default' " +
// "data-object='" + object + "' name='" + name + "' value='" + value +
// "' data-id='" + id + "'>" + buttonLabel +
// "</button></div><div><p class='file-name-label'>" + truncateLongString(fileName, 20) +
// "</p></div>";
// if(value.file_name) {
// html += "<div><a data-action='removeAsset' ";
// html += "onClick='clearFileInput(this)'><i class='fas fa-times'></i></a>";
// } else {
// html += "<div><a data-action='removeAsset' onClick='clearFileInput(this)' ";
// html += "style='display:none'><i class='fas fa-times'></i></a>";
// }
// html += "</div></div></div>";
//
// return html;
// }
//
// /**
// * Returns the colum index
// *
// * @param {Object} table
// * @param {String} id
// *
// * @returns (Boolean | Number)
// */
// function getColumnIndex(table, id) {
// if(id < 0)
// return false;
// return table.column(id).index('visible');
// }
//
// /**
// * Genrates list items dropdown element
// *
// * @param {Array} options
// * @param {String} current_value
// * @param {Number} columnId
// * @param {String} id
// *
// * @returns (String)
// */
// function _listItemDropdown(options, current_value, columnId, id) {
// var val = undefined;
// var html = '<select id="' + id + '" class="form-control selectpicker repository-dropdown" ';
// html += 'data-selected-value="" data-abs-min-length="2" data-live-search="true" ';
// html += 'data-container="body" column_id="' + columnId +'">';
// html += '<option value="-1"></option>';
// $.each(options, function(index, value) {
// var selected = '';
// if (current_value === value[1]) {
// selected = 'selected';
// val = value[0];
// }
// html += '<option value="' + value[0] + '" ' + selected + '>';
// html += value[1] + '</option>';
// });
// html += '</select>';
// return (val) ? $(html).attr('data-selected-value', val)[0] : html;
// }
//
// /**
// * Takes an object and creates custom html element
// *
// * @param {String} object
// * @param {String} name
// * @param {String} column_type
// * @param {Object} cell
// * @param {Object} listColumns
// *
// * @returns (String)
// */
// function changeToFormField(object, name, column_type, cell, listColumns) {
// var cellId = generateInputFieldReference(name);
// var value = cell.value || '';
// if (column_type === 'RepositoryListValue') {
// var column = _.findWhere(listColumns,
// { column_id: parseInt(name, 10) });
// var list_items = column.list_items || cell.list_items;
// return _listItemDropdown(list_items, value, parseInt(name, 10), cellId);
// } else if (column_type === 'RepositoryAssetValue') {
// return changeToInputFileField('repository_cell_file', name, value, cellId);
// } else {
// return changeToInputField(object, name, value, cellId);
// }
// }
//
// /**
// * Append the change listener to file field
// *
// * @param {String} type
// * @param {String} name
// *
// * @returns {undefined}
// */
// function addSelectedFile(type, name) {
// var button = $('button[data-id="' +
// generateInputFieldReference(name) +
// '"]');
// if (type === 'RepositoryAssetValue') {
// var fileInput = $(button.parent().find('input[type="file"]')[0]);
// button.on('click', function(ev) {
// ev.preventDefault();
// ev.stopPropagation();
// fileInput.trigger('click');
// initFileHandler(fileInput);
// });
// }
// }
//
// /**
// * Handle extraction of file from the input field
// *
// * @param {Object} $inputField
// *
// * @returns {undefined}
// */
// function initFileHandler($inputField) {
// $inputField.on('change', function() {
// var input = $(this);
// var $label = $($(this).closest('.repository-input-file-field')
// .find('.file-name-label')[0]);
// var file = this.files[0];
// if (file) {
// $label.text(truncateLongString(file.name, 20));
// input.attr('remove', false);
// $($label.closest('.repository-input-file-field')
// .find('[data-action="removeAsset"]')[0]).show();
// }
// })
// }
//
// /**
// * Generates the input tag id that will be used in the formData object
// *
// * @param {String} columnId
// *
// * @returns {String}
// */
// function generateInputFieldReference(columnId) {
// return 'colId-' + columnId;
// }
//
// /**
// * Appends aditional fields to form data object
// * @param {Object} cell
// * @param {String} columnId
// * @param {Object} formData
// *
// * @returns {undefined}
// */
// function appendNewElementToFormData(cell, columnId, formData) {
// if (!cell.repository_cell_id) {
// formData[generateInputFieldReference(columnId)] = undefined;
// }
// }
// }(window));

View file

@ -0,0 +1,54 @@
/*
global ActiveStorage Promise
*/
/* eslint-disable no-unused-vars */
var Asset = (function() {
function uploadFiles($fileInputs, directUploadUrl) {
let filesToUploadCntr = 0;
let filesUploadedCntr = 0;
let filesForUpload = [];
$fileInputs.each(function(_, f) {
let $f = $(f);
if ($f.val()) {
filesToUploadCntr += 1;
filesForUpload.push($f);
}
});
return new Promise((resolve, reject) => {
if (filesToUploadCntr === 0) {
resolve('done');
return;
}
$(filesForUpload).each(function(_, $el) {
let upload = new ActiveStorage.DirectUpload($el[0].files[0], directUploadUrl);
upload.create(function(error, blob) {
if (error) {
reject(error);
} else {
$el
.prev('.file-hidden-field-container')
.html(`<input type="hidden"
form="${$el.attr('form')}"
name="repository_cells[${$el.data('col-id')}]"
value="${blob.signed_id}"/>`);
filesUploadedCntr += 1;
if (filesUploadedCntr === filesToUploadCntr) {
resolve('done');
}
}
});
return true;
});
});
}
return {
uploadFiles: uploadFiles
};
}());

View file

@ -0,0 +1,57 @@
/* global dropdownSelector I18n */
/* eslint-disable no-unused-vars */
var ChecklistColumnHelper = (function() {
function checklistSelect(select, url, values) {
var selectedOptions = '';
if (values) {
$.each(values, function(i, option) {
selectedOptions += `<option value="${option.value}">${option.label}</option>`;
});
}
return $(`<select
id="${select}"
data-placeholder = "Select options..."
data-ajax-url = "${url}"
data-combine-tags="true"
data-select-multiple-all-selected="${I18n.t('libraries.manange_modal_column.checklist_type.all_options')}"
data-select-multiple-name="${I18n.t('libraries.manange_modal_column.checklist_type.multiple_options')}"
>${selectedOptions}</select>`);
}
function checklistHiddenField(formId, columnId, values) {
var idList = [];
if (values) {
$.each(values, function(i, option) {
idList.push(option.value);
});
} else {
idList = '';
}
return $(`<input form="${formId}"
type="hidden"
name="repository_cells[${columnId}]"
value="${JSON.stringify(idList)}"
data-type="RepositoryChecklistValue">`);
}
function initialChecklistEditMode(formId, columnId, cell, values) {
var select = 'checklist-' + columnId;
var checklistUrl = $('.repository-column#' + columnId).data('items-url');
var $select = checklistSelect(select, checklistUrl, values);
var $hiddenField = checklistHiddenField(formId, columnId, values);
cell.html($select).append($hiddenField);
dropdownSelector.init('#' + select, {
noEmptyOption: true,
optionClass: 'checkbox-icon',
selectAppearance: 'simple',
onChange: function() {
$hiddenField.val(JSON.stringify(dropdownSelector.getValues('#' + select)));
}
});
}
return {
initialChecklistEditMode: initialChecklistEditMode
};
}());

View file

@ -0,0 +1,268 @@
/* global Inputmask formatJS */
/* eslint-disable no-unused-vars */
var DateTimeHelper = (function() {
function isValidTimeStr(timeStr) {
return /^([0-9]|0[0-9]|1[0-9]|2[0-3]):[0-5][0-9]$/.test(timeStr);
}
function isValidDate(date) {
return (date instanceof Date) && !isNaN(date.getTime());
}
function addLeadingZero(value) {
return ('0' + value).slice(-2);
}
function recalcTimestamp(date, timeStr) {
if (!isValidTimeStr(timeStr)) {
date.setHours(0);
date.setMinutes(0);
return date;
}
date.setHours(timeStr.split(':')[0]);
date.setMinutes(timeStr.split(':')[1]);
return date;
}
function stringDateTimeFormat(date, format) {
let y = date.getFullYear();
let m = addLeadingZero(date.getMonth() + 1);
let d = addLeadingZero(date.getDate());
let hours = addLeadingZero(date.getHours());
let mins = addLeadingZero(date.getMinutes());
if (format === 'dateonly') {
return `${y}/${m}/${d}`;
}
return `${y}/${m}/${d} ${hours}:${mins}`;
}
function insertHiddenField($container) {
let formId = $container.data('form-id');
let columnId = $container.data('column-id');
let dateStr = $container.find('input.date-part').data('selected-date');
let timeStr = $container.find('input.time-part').val();
let columnType = $container.data('type');
let date = new Date(dateStr);
let value = '';
let hiddenField;
if (isValidDate(date) && isValidTimeStr(timeStr)) {
value = stringDateTimeFormat(recalcTimestamp(date, timeStr), 'full');
}
hiddenField = `
<input class="repository-cell-value"
type="hidden"
form="${formId}"
name="repository_cells[${columnId}]"
value="${value}"
data-type="${columnType}"/>`;
$container.find('input.repository-cell-value').remove();
$container.prepend(hiddenField);
}
function insertRangeHiddenField($container) {
let formId = $container.data('form-id');
let columnId = $container.data('column-id');
let columnType = $container.data('type');
let $startContainer = $container.find('.start-time');
let $endContainer = $container.find('.end-time');
let startDate = new Date($startContainer.find('input.date-part').data('selected-date'));
let startTimeStr = $startContainer.find('input.time-part').val();
let endDate = new Date($endContainer.find('input.date-part').data('selected-date'));
let endTimeStr = $endContainer.find('input.time-part').val();
let hiddenField;
let value = '';
if (isValidDate(startDate)
&& isValidTimeStr(startTimeStr)
&& isValidDate(endDate)
&& isValidTimeStr(endTimeStr)) {
let start = stringDateTimeFormat(recalcTimestamp(startDate, startTimeStr), 'full');
let end = stringDateTimeFormat(recalcTimestamp(endDate, endTimeStr), 'full');
value = JSON.stringify({ start_time: start, end_time: end });
}
hiddenField = `
<input class="repository-cell-value"
type="hidden"
form="${formId}"
name="repository_cells[${columnId}]"
value='${value}'
data-type="${columnType}"/>`;
$container.find('input.repository-cell-value').remove();
$container.prepend(hiddenField);
}
function initChangeEvents($cell) {
$cell.find('input.time-part').on('change', function() {
let $input = $(this);
let $container = $input.closest('.datetime-container');
if ($container.hasClass('range-type')) {
insertRangeHiddenField($container);
} else {
insertHiddenField($container);
}
});
$cell.find('input.date-part').on('dp.change', function(e) {
let $input = $(this);
let date = e.date._d;
let $container = $input.closest('.datetime-container');
if (date !== undefined) {
$input.data('selected-date', stringDateTimeFormat(date, 'dateonly'));
} else {
$input.data('selected-date', '');
}
if ($container.hasClass('range-type')) {
insertRangeHiddenField($container);
} else {
insertHiddenField($container);
}
});
}
function dateInputField(value, dateDataValue) {
return `
<input class="form-control editing calendar-input date-part"
type="datetime"
data-datetime-part="date"
data-selected-date="${dateDataValue}"
value='${value}'/>
`;
}
function timeInputField(value) {
return `
<input class="form-control editing time-part"
type="text"
data-mask-type="time"
value='${value}'
placeholder="HH:mm"/>
`;
}
function getDateOrDefault($span, mode) {
let dateStr = $span.data('date');
let date;
if (mode === 'timeonly') {
// Set default date if no data in span
date = new Date(dateStr);
if (isValidDate(date)) {
dateStr = stringDateTimeFormat(new Date(date), 'dateonly');
} else {
dateStr = stringDateTimeFormat(new Date(), 'dateonly');
}
}
return dateStr;
}
function getTimeOrDefault($span, mode) {
let timeStr = $span.data('time');
if ((mode === 'dateonly') && (!isValidTimeStr(timeStr))) {
timeStr = '00:00';
}
return timeStr;
}
function initDateTimeEditMode(formId, columnId, $cell, mode, columnType) {
let $span = $cell.find('span').first();
let date = $span.data('date');
let dateDataValue = getDateOrDefault($span, mode);
let time = getTimeOrDefault($span, mode);
let datetime = $span.data('datetime');
let inputFields = `
<div class="form-group datetime-container ${mode}"
data-form-id="${formId}"
data-column-id="${columnId}"
data-type="${columnType}"
data-current-datetime="${datetime}">
${dateInputField(date, dateDataValue)}
${timeInputField(time)}
</div>
`;
$cell.html(inputFields);
Inputmask('datetime', {
inputFormat: 'HH:MM',
placeholder: 'HH:mm',
clearIncomplete: true,
showMaskOnHover: true,
hourFormat: 24
}).mask($cell.find('input[data-mask-type="time"]'));
$cell.find('.calendar-input').datetimepicker({ ignoreReadonly: true, locale: 'en', format: formatJS });
initChangeEvents($cell);
}
function initDateTimeRangeEditMode(formId, columnId, $cell, mode, columnType) {
let $startSpan = $cell.find('span').first();
let startDate = $startSpan.data('date');
let startTime = getTimeOrDefault($startSpan, mode);
let startDatetime = $startSpan.data('datetime');
let startDateDataValue = getDateOrDefault($startSpan, mode);
let $endSpan = $cell.find('span').last();
let endDate = $endSpan.data('date');
let endTime = getTimeOrDefault($endSpan, mode);
let endDatetime = $endSpan.data('datetime');
let endDateDataValue = getDateOrDefault($endSpan, mode);
let inputFields = `
<div class="form-group datetime-container range-type"
data-form-id="${formId}"
data-column-id="${columnId}"
data-type="${columnType}"
>
<div class="start-time ${mode}"
data-current-datetime="${startDatetime}">
${dateInputField(startDate, startDateDataValue)}
${timeInputField(startTime)}
</div>
<div class="end-time ${mode}"
data-current-datetime="${endDatetime}">
${dateInputField(endDate, endDateDataValue)}
${timeInputField(endTime)}
</div>
</div>
`;
$cell.html(inputFields);
Inputmask('datetime', {
inputFormat: 'HH:MM',
placeholder: 'HH:mm',
clearIncomplete: true,
showMaskOnHover: true,
hourFormat: 24
}).mask($cell.find('input[data-mask-type="time"]'));
let $cal1 = $cell.find('.calendar-input').first().datetimepicker({ ignoreReadonly: true, locale: 'en', format: formatJS });
let $cal2 = $cell.find('.calendar-input').last().datetimepicker({ ignoreReadonly: true, locale: 'en', format: formatJS });
$cal1.on('dp.change', function(e) {
$cal2.data('DateTimePicker').minDate(e.date);
});
$cal2.on('dp.change', function(e) {
$cal1.data('DateTimePicker').maxDate(e.date);
});
initChangeEvents($cell);
}
return {
initDateTimeEditMode: initDateTimeEditMode,
initDateTimeRangeEditMode: initDateTimeRangeEditMode
};
}());

View file

@ -0,0 +1,45 @@
/* global dropdownSelector */
/* eslint-disable no-unused-vars */
var ListColumnHelper = (function() {
function listSelect(select, url, value) {
var selectedOption = '';
if (value && value.value) {
selectedOption = `<option value="${value.value}">${value.label}</option>`;
}
return $(`<select
id="${select}"
data-placeholder = "Select option..."
data-ajax-url = "${url}"
>${selectedOption}</select>`);
}
function listHiddenField(formId, columnId, value) {
var originalValue = value ? value.value : '';
return $(`<input form="${formId}"
type="hidden"
name="repository_cells[${columnId}]"
value="${originalValue}"
data-type="RepositoryListValue">`);
}
function initialListEditMode(formId, columnId, cell, value = null) {
var select = 'list-' + columnId;
var listUrl = $('.repository-column#' + columnId).data('items-url');
var $select = listSelect(select, listUrl, value);
var $hiddenField = listHiddenField(formId, columnId, value);
cell.html($select).append($hiddenField);
dropdownSelector.init('#' + select, {
singleSelect: true,
selectAppearance: 'simple',
onChange: function() {
var values = dropdownSelector.getValues('#' + select);
$hiddenField.val(values);
}
});
}
return {
initialListEditMode: initialListEditMode
};
}());

View file

@ -0,0 +1,46 @@
/* global dropdownSelector */
/* eslint-disable no-unused-vars */
var StatusColumnHelper = (function() {
function statusSelect(select, url, value) {
var selectedOption = '';
if (value && value.value) {
selectedOption = `<option value="${value.value}">${value.label}</option>`;
}
return $(`<select
id="${select}"
data-placeholder = "Select option..."
data-ajax-url = "${url}"
>${selectedOption}</select>`);
}
function statusHiddenField(formId, columnId, value) {
var originalValue = value ? value.value : '';
return $(`<input form="${formId}"
type="hidden"
name="repository_cells[${columnId}]"
value="${originalValue}"
data-type="RepositoryStatusValue">`);
}
function initialStatusEditMode(formId, columnId, cell, value = null) {
var select = 'status-list-' + columnId;
var listUrl = $('.repository-column#' + columnId).data('items-url');
var $select = statusSelect(select, listUrl, value);
var $hiddenField = statusHiddenField(formId, columnId, value);
cell.html($select).append($hiddenField);
dropdownSelector.init('#' + select, {
singleSelect: true,
selectAppearance: 'simple',
onChange: function() {
var values = dropdownSelector.getValues('#' + select);
$hiddenField.val(values);
}
});
}
return {
initialStatusEditMode: initialStatusEditMode
};
}());

View file

@ -0,0 +1,149 @@
/*
global ListColumnHelper ChecklistColumnHelper StatusColumnHelper SmartAnnotation I18n
GLOBAL_CONSTANTS DateTimeHelper
*/
$.fn.dataTable.render.editRowName = function(formId, cell) {
let $cell = $(cell.node());
let text = $cell.find('a').first().text();
$cell.html(`
<div class="form-group">
<input class="form-control editing"
form="${formId}"
type="text"
name="repository_row[name]"
value="${text}"
data-type="RowName">
</div>
`);
};
$.fn.dataTable.render.editRepositoryAssetValue = function(formId, columnId, cell) {
let $cell = $(cell.node());
let empty = $cell.is(':empty');
let fileName = $cell.find('a.file-preview-link').text();
$cell.html(`
<div class="file-editing">
<div class="file-hidden-field-container hidden"></div>
<input class=""
id="repository_file_${columnId}"
form="${formId}"
type="file"
data-col-id="${columnId}"
data-is-empty="${empty}"
value=""
data-type="RepositoryAssetValue">
<div class="file-upload-button ${empty ? 'new-file' : ''}">
<label for="repository_file_${columnId}">${I18n.t('repositories.table.assets.select_file_btn', { max_size: GLOBAL_CONSTANTS.FILE_MAX_SIZE_MB })}</label>
<span class="icon"><i class="fas fa-paperclip"></i></span><span class="label-asset">${fileName}</span>
<span class="delete-action fas fa-trash"> </span>
</div>
</div>`);
};
$.fn.dataTable.render.editRepositoryTextValue = function(formId, columnId, cell) {
let $cell = $(cell.node());
let text = $cell.text();
$cell.html(`
<div class="form-group">
<input class="form-control editing"
form="${formId}"
type="text"
name="repository_cells[${columnId}]"
value="${text}"
data-type="RepositoryTextValue">
</div>`);
SmartAnnotation.init($cell.find('input'));
};
$.fn.dataTable.render.editRepositoryListValue = function(formId, columnId, cell) {
var $cell = $(cell.node());
var currentElement = $cell.find('.list-label');
var currentValue = null;
if (currentElement.length) {
currentValue = {
value: currentElement.attr('data-value-id'),
label: currentElement.text()
};
}
ListColumnHelper.initialListEditMode(formId, columnId, $cell, currentValue);
};
$.fn.dataTable.render.editRepositoryStatusValue = function(formId, columnId, cell) {
let $cell = $(cell.node());
var currentElement = $cell.find('.status-label');
var iconElement = $cell.find('.repository-status-value-icon');
var currentValue = null;
if (currentElement.length) {
currentValue = {
value: currentElement.attr('data-value-id'),
label: iconElement.text() + ' ' + currentElement.text()
};
}
StatusColumnHelper.initialStatusEditMode(formId, columnId, $cell, currentValue);
};
$.fn.dataTable.render.editRepositoryDateTimeValue = function(formId, columnId, cell) {
let $cell = $(cell.node());
DateTimeHelper.initDateTimeEditMode(formId, columnId, $cell, '', 'RepositoryDateTimeValue');
};
$.fn.dataTable.render.editRepositoryDateValue = function(formId, columnId, cell) {
let $cell = $(cell.node());
DateTimeHelper.initDateTimeEditMode(formId, columnId, $cell, 'dateonly', 'RepositoryDateValue');
};
$.fn.dataTable.render.editRepositoryTimeValue = function(formId, columnId, cell) {
let $cell = $(cell.node());
DateTimeHelper.initDateTimeEditMode(formId, columnId, $cell, 'timeonly', 'RepositoryTimeValue');
};
$.fn.dataTable.render.editRepositoryDateTimeRangeValue = function(formId, columnId, cell) {
let $cell = $(cell.node());
DateTimeHelper.initDateTimeRangeEditMode(formId, columnId, $cell, '', 'RepositoryDateTimeRangeValue');
};
$.fn.dataTable.render.editRepositoryDateRangeValue = function(formId, columnId, cell) {
let $cell = $(cell.node());
DateTimeHelper.initDateTimeRangeEditMode(formId, columnId, $cell, 'dateonly', 'RepositoryDateRangeValue');
};
$.fn.dataTable.render.editRepositoryTimeRangeValue = function(formId, columnId, cell) {
let $cell = $(cell.node());
DateTimeHelper.initDateTimeRangeEditMode(formId, columnId, $cell, 'timeonly', 'RepositoryTimeRangeValue');
};
$.fn.dataTable.render.editRepositoryChecklistValue = function(formId, columnId, cell) {
var $cell = $(cell.node());
var currentValue = $cell.find('.checklist-options').data('checklist-items');
ChecklistColumnHelper.initialChecklistEditMode(formId, columnId, $cell, currentValue);
};
$.fn.dataTable.render.editRepositoryNumberValue = function(formId, columnId, cell, $header) {
let $cell = $(cell.node());
let decimals = Number($header.data('metadata-decimals'));
let number = parseFloat(Number($cell.text()).toFixed(decimals));
$cell.html(`
<div class="form-group">
<input class="form-control editing"
form="${formId}"
type="number"
name="repository_cells[${columnId}]"
value="${number}"
onchange="if (this.value !== '') { this.value = parseFloat(Number(this.value).toFixed(${decimals})); }"
data-type="RepositoryNumberValue">
</div>`);
};

View file

@ -0,0 +1,108 @@
/*
global ListColumnHelper ChecklistColumnHelper StatusColumnHelper SmartAnnotation I18n
GLOBAL_CONSTANTS DateTimeHelper
*/
$.fn.dataTable.render.newRowName = function(formId, $cell) {
$cell.html(`
<div class="form-group">
<input class="form-control editing"
form="${formId}"
type="text"
name="repository_row[name]"
value=""
data-type="RowName">
</div>
`);
};
$.fn.dataTable.render.newRepositoryAssetValue = function(formId, columnId, $cell) {
$cell.html(`
<div class="file-editing">
<div class="file-hidden-field-container hidden"></div>
<input class=""
id="repository_file_${columnId}"
form="${formId}"
type="file"
data-col-id="${columnId}"
data-is-empty="true"
value=""
data-type="RepositoryAssetValue">
<div class="file-upload-button new-file">
<label for="repository_file_${columnId}">${I18n.t('repositories.table.assets.select_file_btn', { max_size: GLOBAL_CONSTANTS.FILE_MAX_SIZE_MB })}</label>
<span class="icon"><i class="fas fa-paperclip"></i></span><span class="label-asset"></span>
<span class="delete-action fas fa-trash"> </span>
</div>
</div>`);
};
$.fn.dataTable.render.newRepositoryTextValue = function(formId, columnId, $cell) {
$cell.html(`
<div class="form-group">
<input class="form-control editing"
form="${formId}"
type="text"
name="repository_cells[${columnId}]"
value=""
data-type="RepositoryTextValue">
</div>`);
SmartAnnotation.init($cell.find('input'));
};
$.fn.dataTable.render.newRepositoryListValue = function(formId, columnId, $cell) {
ListColumnHelper.initialListEditMode(formId, columnId, $cell);
};
$.fn.dataTable.render.newRepositoryStatusValue = function(formId, columnId, $cell) {
StatusColumnHelper.initialStatusEditMode(formId, columnId, $cell);
};
$.fn.dataTable.render.newRepositoryChecklistValue = function(formId, columnId, $cell) {
ChecklistColumnHelper.initialChecklistEditMode(formId, columnId, $cell);
};
$.fn.dataTable.render.newRepositoryNumberValue = function(formId, columnId, $cell, $header) {
let decimals = Number($header.data('metadata-decimals'));
$cell.html(`
<div class="form-group">
<input class="form-control editing"
form="${formId}"
type="number"
name="repository_cells[${columnId}]"
value=""
onchange="if (this.value !== '') { this.value = parseFloat(Number(this.value).toFixed(${decimals})); }"
data-type="RepositoryNumberValue">
</div>`);
SmartAnnotation.init($cell.find('input'));
};
$.fn.dataTable.render.newRepositoryDateTimeValue = function(formId, columnId, $cell) {
DateTimeHelper.initDateTimeEditMode(formId, columnId, $cell, '', 'RepositoryDateTimeValue');
};
$.fn.dataTable.render.newRepositoryTimeValue = function(formId, columnId, $cell) {
DateTimeHelper.initDateTimeEditMode(formId, columnId, $cell, 'timeonly', 'RepositoryTimeValue');
};
$.fn.dataTable.render.newRepositoryDateValue = function(formId, columnId, $cell) {
DateTimeHelper.initDateTimeEditMode(formId, columnId, $cell, 'dateonly', 'RepositoryDateValue');
};
$.fn.dataTable.render.newRepositoryDateTimeRangeValue = function(formId, columnId, $cell) {
DateTimeHelper.initDateTimeRangeEditMode(formId, columnId, $cell, '', 'RepositoryDateTimeRangeValue');
};
$.fn.dataTable.render.newRepositoryDateRangeValue = function(formId, columnId, $cell) {
DateTimeHelper.initDateTimeRangeEditMode(formId, columnId, $cell, 'dateonly', 'RepositoryDateRangeValue');
};
$.fn.dataTable.render.newRepositoryTimeRangeValue = function(formId, columnId, $cell) {
DateTimeHelper.initDateTimeRangeEditMode(formId, columnId, $cell, 'timeonly', 'RepositoryTimeRangeValue');
};
$.fn.dataTable.render.newRepositoryCheckboxValue = function(formId, columnId) {
return '';
};

View file

@ -0,0 +1,150 @@
/* global I18n */
$.fn.dataTable.render.RepositoryAssetValue = function(data) {
var asset = data.value;
return `
<div class="asset-value-cell">
${asset.icon_html}
<a class="file-preview-link"
id="modal_link${asset.id}"
data-no-turbolink="true"
data-id="true"
data-status="asset-present"
data-preview-url="${asset.preview_url}"
href="${asset.url}"
>
${asset.file_name}
</a>
</div>
`;
};
$.fn.dataTable.render.defaultRepositoryAssetValue = function() {
return '';
};
$.fn.dataTable.render.RepositoryTextValue = function(data) {
return data.value;
};
$.fn.dataTable.render.defaultRepositoryTextValue = function() {
return '';
};
$.fn.dataTable.render.RepositoryListValue = function(data) {
return `<span data-value-id="${data.value.id}" class="list-label">${data.value.text}</span>`;
};
$.fn.dataTable.render.defaultRepositoryListValue = function() {
return '';
};
$.fn.dataTable.render.RepositoryStatusValue = function(data) {
return `
<span class="repository-status-value-icon">${data.value.icon}</span>
<span data-value-id="${data.value.id}" class="status-label">${data.value.status}</span>
`;
};
$.fn.dataTable.render.defaultRepositoryStatusValue = function() {
return '';
};
$.fn.dataTable.render.defaultRepositoryDateValue = function() {
return '';
};
$.fn.dataTable.render.RepositoryDateValue = function(data) {
return `<span data-datetime="${data.value.datetime}" data-date="${data.value.formatted}">${data.value.formatted}</span>`;
};
$.fn.dataTable.render.defaultRepositoryDateTimeValue = function() {
return '';
};
$.fn.dataTable.render.RepositoryDateTimeValue = function(data) {
return `<span data-time="${data.value.time_formatted}"
data-datetime="${data.value.datetime}"
data-date="${data.value.date_formatted}">${data.value.formatted}</span>`;
};
$.fn.dataTable.render.defaultRepositoryTimeValue = function() {
return '';
};
$.fn.dataTable.render.RepositoryTimeValue = function(data) {
return `<span data-time="${data.value.formatted}"
data-datetime="${data.value.datetime}">${data.value.formatted}</span>`;
};
$.fn.dataTable.render.defaultRepositoryTimeRangeValue = function() {
return '';
};
$.fn.dataTable.render.RepositoryTimeRangeValue = function(data) {
return `<span data-time="${data.value.start_time.formatted}"
data-datetime="${data.value.start_time.datetime}">${data.value.start_time.formatted}</span> -
<span data-time="${data.value.end_time.formatted}"
data-datetime="${data.value.end_time.datetime}">${data.value.end_time.formatted}</span>`;
};
$.fn.dataTable.render.defaultRepositoryDateTimeRangeValue = function() {
return '';
};
$.fn.dataTable.render.RepositoryDateTimeRangeValue = function(data) {
return `<span data-time="${data.value.start_time.time_formatted}"
data-datetime="${data.value.start_time.datetime}"
data-date="${data.value.start_time.date_formatted}">${data.value.start_time.formatted}</span> -
<span data-time="${data.value.end_time.time_formatted}"
data-datetime="${data.value.end_time.datetime}"
data-date="${data.value.end_time.date_formatted}">${data.value.end_time.formatted}</span>`;
};
$.fn.dataTable.render.defaultRepositoryDateRangeValue = function() {
return '';
};
$.fn.dataTable.render.RepositoryDateRangeValue = function(data) {
return `<span data-datetime="${data.value.start_time.datetime}"
data-date="${data.value.start_time.formatted}">${data.value.start_time.formatted}</span> -
<span data-datetime="${data.value.end_time.datetime}"
data-date="${data.value.end_time.formatted}">${data.value.end_time.formatted}</span>`;
};
$.fn.dataTable.render.RepositoryChecklistValue = function(data) {
var render = '&#8212;';
var options = data.value;
var optionsList;
if (options.length === 1) {
render = `<span class="checklist-options" data-checklist-items='${JSON.stringify(options)}'>
${options[0].label}
</span>`;
} else if (options.length > 1) {
optionsList = $(' <ul class="dropdown-menu checklist-dropdown-menu" role="menu"></ul');
$.each(options, function(i, option) {
$(`<li class="checklist-item">${option.label}</li>`).appendTo(optionsList);
});
render = `
<span class="dropdown checklist-dropdown">
<span data-toggle="dropdown" class="checklist-options" aria-haspopup="true" data-checklist-items='${JSON.stringify(options)}'>
${options.length} ${I18n.t('libraries.manange_modal_column.checklist_type.multiple_options')}
</span>
${optionsList[0].outerHTML}
</span>`;
}
return render;
};
$.fn.dataTable.render.defaultRepositoryChecklistValue = function() {
return '&#8212;';
};
$.fn.dataTable.render.defaultRepositoryNumberValue = function() {
return '';
};
$.fn.dataTable.render.RepositoryNumberValue = function(data) {
return parseFloat(Number(data.value).toFixed(data.value_decimals));
};

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,207 @@
/*
globals HelperModule animateSpinner SmartAnnotation Asset
*/
/* eslint-disable no-unused-vars */
var RepositoryDatatableRowEditor = (function() {
const NAME_COLUMN_ID = 'row-name';
const TABLE_ROW = '<tr></tr>';
const TABLE_CELL = '<td></td>';
var TABLE;
// Initialize SmartAnnotation
function initSmartAnnotation($row) {
$row.find('[data-object="repository_cell"]').each(function(el) {
if (el.data('atwho')) {
SmartAnnotation.init(el);
}
});
}
function validateAndSubmit($table) {
let $form = $table.find('.repository-row-edit-form');
let $row = $form.closest('tr');
let valid = true;
let directUrl = $table.data('direct-upload-url');
let $files = $row.find('input[type=file]');
$row.find('.has-error').removeClass('has-error').find('span').remove();
// Validations here
$row.find('input').each(function() {
let dataType = $(this).data('type');
if (!dataType) return;
valid = $.fn.dataTable.render[dataType + 'Validator']($(this));
if (!valid) return false;
});
if (!valid) return false;
// DirectUpload here
let uploadPromise = Asset.uploadFiles($files, directUrl);
// Submission here
uploadPromise
.then(function() {
animateSpinner(null, true);
$form.submit();
return false;
}).catch((reason) => {
alert(reason);
return false;
});
return null;
}
function initAssetCellActions($row) {
let fileInputs = $row.find('input[type=file]');
let deleteButtons = $row.find('.file-upload-button>span.delete-action');
fileInputs.on('change', function() {
let $input = $(this);
let $fileBtn = $input.next('.file-upload-button');
let $label = $fileBtn.find('.label-asset');
$label.text($input[0].files[0].name);
$fileBtn.removeClass('new-file');
});
deleteButtons.on('click', function() {
let $fileBtn = $(this).parent();
let $input = $fileBtn.prev('input[type=file]');
let $label = $fileBtn.find('.label-asset');
$fileBtn.addClass('new-file');
$label.text('');
$input.val('');
if (!$input.data('is-empty')) { // set hidden field for deletion only if original value has been set on rendering
$input
.prev('.file-hidden-field-container')
.html(`<input type="hidden"
form="${$input.attr('form')}"
name="repository_cells[${$input.data('col-id')}]"
value=""/>`);
}
});
}
function initFormSubmitAction(table) {
TABLE = table;
let $table = $(TABLE.table().node());
$table.on('ajax:success', '.repository-row-edit-form', function(ev, data) {
TABLE.ajax.reload();
HelperModule.flashAlertMsg(data.flash, 'success');
});
$table.on('ajax:error', '.repository-row-edit-form', function(ev, data) {
HelperModule.flashAlertMsg(data.responseJSON.flash, 'danger');
});
$table.on('ajax:complete', '.repository-row-edit-form', function() {
animateSpinner(null, false);
});
}
function addNewRow(table) {
TABLE = table;
let $row = $(TABLE_ROW);
const formId = 'repositoryNewRowForm';
let actionUrl = $(TABLE.table().node()).data('createRecord');
let rowForm = $(`
<td>
<form id="${formId}"
class="repository-row-edit-form"
action="${actionUrl}"
method="post"
data-remote="true">
</form>
</td>
`);
// First two columns are always present and visible
$row.append(rowForm);
$row.append($(TABLE_CELL));
$(TABLE.table().node()).find('tbody').prepend($row);
table.columns().every(function() {
let column = this;
let $header = $(column.header());
if (column.index() < 2) return;
if (!column.visible()) return;
let columnId = $header.attr('id');
let $cell = $(TABLE_CELL).appendTo($row);
if (columnId === NAME_COLUMN_ID) {
$.fn.dataTable.render.newRowName(formId, $cell);
} else {
let dataType = $header.data('type');
if (dataType) {
$.fn.dataTable.render['new' + dataType](formId, columnId, $cell, $header);
}
}
});
initSmartAnnotation($row);
TABLE.columns.adjust();
}
function switchRowToEditMode(row) {
let $row = $(row.node());
let itemId = row.id();
let formId = `repositoryRowForm${itemId}`;
let requestUrl = $(TABLE.table().node()).data('current-uri');
let rowForm = $(`
<form id="${formId}"
class="repository-row-edit-form"
action="${row.data().recordUpdateUrl}"
method="patch"
data-remote="true"
data-row-id="${itemId}">
<input name="id" type="hidden" value="${itemId}" />
<input name="request_url" type="hidden" value="${requestUrl}" />
</form>
`);
$row.find('td').first().append(rowForm);
TABLE.cells(row.index(), row.columns().eq(0)).every(function() {
let $header = $(TABLE.columns(this.index().column).header());
let columnId = $header.attr('id');
let dataType = $header.data('type');
let cell = this;
if (!cell.column(cell.index().column).visible()) return true; // return if cell is not visible
if (columnId === NAME_COLUMN_ID) {
$.fn.dataTable.render.editRowName(formId, cell);
} else if (dataType) {
$.fn.dataTable.render['edit' + dataType](formId, columnId, cell, $header);
}
return true;
});
initSmartAnnotation($row);
initAssetCellActions($row);
TABLE.columns.adjust();
}
return Object.freeze({
initFormSubmitAction: initFormSubmitAction,
validateAndSubmit: validateAndSubmit,
switchRowToEditMode: switchRowToEditMode,
addNewRow: addNewRow
});
}());

View file

@ -0,0 +1,152 @@
/* global GLOBAL_CONSTANTS textValidator I18n */
$.fn.dataTable.render.RowNameValidator = function($input) {
return textValidator(undefined, $input, 1, GLOBAL_CONSTANTS.NAME_MAX_LENGTH);
};
$.fn.dataTable.render.RepositoryTextValueValidator = function($input) {
return textValidator(undefined, $input, 1, GLOBAL_CONSTANTS.NAME_MAX_LENGTH);
};
$.fn.dataTable.render.RepositoryListValueValidator = function() {
return true;
};
$.fn.dataTable.render.RepositoryStatusValueValidator = function() {
return true;
};
$.fn.dataTable.render.RepositoryAssetValueValidator = function($input) {
let file = $input[0].files[0];
if (!file) return true;
let valid = (file.size < GLOBAL_CONSTANTS.FILE_MAX_SIZE_MB * 1024 * 1024);
if (valid) return true;
let errorMessage = I18n.t('general.file.size_exceeded', { file_size: GLOBAL_CONSTANTS.FILE_MAX_SIZE_MB });
let $btn = $input.next('.file-upload-button');
$btn.addClass('error');
$btn.attr('data-error-text', errorMessage);
return false;
};
$.fn.dataTable.render.RepositoryChecklistValueValidator = function() {
return true;
};
$.fn.dataTable.render.RepositoryNumberValueValidator = function() {
return true;
};
$.fn.dataTable.render.RepositoryDateTimeValueValidator = function($input) {
let $container = $input.parents('.datetime-container');
let $date = $container.find('input.date-part');
let $time = $container.find('input.time-part');
if (($date.val() === '') === ($time.val() === '')) {
return true;
}
$container.addClass('has-error');
$container.append('<span class="help-block">Set both or none</span>');
return false;
};
$.fn.dataTable.render.RepositoryDateValueValidator = function() {
return true;
};
$.fn.dataTable.render.RepositoryTimeValueValidator = function() {
return true;
};
$.fn.dataTable.render.RepositoryDateTimeRangeValueValidator = function($input) {
let $container = $input.parents('.datetime-container');
let $dateS = $container.find('.start-time input.date-part');
let $timeS = $container.find('.start-time input.time-part');
let $dateE = $container.find('.end-time input.date-part');
let $timeE = $container.find('.end-time input.time-part');
let isValid = true;
let errorMessage;
let startTime;
let endTime;
let a = [];
if ($input.val()) {
startTime = new Date(JSON.parse($input.val()).start_time);
endTime = new Date(JSON.parse($input.val()).end_time);
}
a.push($dateS.val() === '');
a.push($timeS.val() === '');
a.push($dateE.val() === '');
a.push($timeE.val() === '');
if (a.filter((v, i, arr) => arr.indexOf(v) === i).length > 1) {
isValid = false;
errorMessage = I18n.t('repositories.table.date_time.errors.set_all_or_none');
} else if (($input.val()) && (startTime > endTime)) {
isValid = false;
errorMessage = I18n.t('repositories.table.date_time.errors.not_valid_range');
}
if (isValid) {
return true;
}
$container.addClass('has-error');
$container.append(`<span class="help-block">${errorMessage}</span>`);
return false;
};
$.fn.dataTable.render.RepositoryDateRangeValueValidator = function($input) {
let $container = $input.parents('.datetime-container');
let $dateS = $container.find('.start-time input.date-part');
let $dateE = $container.find('.end-time input.date-part');
let isValid = true;
let errorMessage;
let endTime;
let startTime;
if ($input.val()) {
startTime = new Date(JSON.parse($input.val()).start_time);
endTime = new Date(JSON.parse($input.val()).end_time);
}
if (($dateS.val() === '') !== ($dateE.val() === '')) {
isValid = false;
errorMessage = I18n.t('repositories.table.date_time.errors.set_all_or_none');
} else if (($input.val()) && (startTime > endTime)) {
isValid = false;
errorMessage = I18n.t('repositories.table.date_time.errors.not_valid_range');
}
if (isValid) {
return true;
}
$container.addClass('has-error');
$container.append(`<span class="help-block">${errorMessage}</span>`);
return false;
};
$.fn.dataTable.render.RepositoryTimeRangeValueValidator = function($input) {
let $container = $input.parents('.datetime-container');
let $timeS = $container.find('.start-time input.time-part');
let $timeE = $container.find('.end-time input.time-part');
let isValid = true;
let errorMessage;
if (($timeS.val() === '') !== ($timeE.val() === '')) {
isValid = false;
errorMessage = I18n.t('repositories.table.date_time.errors.set_all_or_none');
} else if ($timeS.val() > $timeE.val()) {
isValid = false;
errorMessage = I18n.t('repositories.table.date_time.errors.not_valid_range');
}
if (isValid) {
return true;
}
$container.addClass('has-error');
$container.append(`<span class="help-block">${errorMessage}</span>`);
return false;
};

View file

@ -0,0 +1,76 @@
/* global GLOBAL_CONSTANTS dropdownSelector RepositoryListColumnType */
var RepositoryChecklistColumnType = (function() {
var manageModal = '#manage-repository-column';
var delimiterDropdown = '.checklist-column-type .delimiter';
var itemsTextarea = '.checklist-column-type .items-textarea';
var previewContainer = '.checklist-column-type .dropdown-preview';
var dropdownOptions = '.checklist-column-type .dropdown-options';
function initChecklistDropdown() {
dropdownSelector.init(previewContainer + ' .preview-select', {
noEmptyOption: true,
optionClass: 'checkbox-icon',
selectAppearance: 'simple'
});
}
function initDropdownItemsTextArea() {
var $manageModal = $(manageModal);
var columnNameInput = '#repository-column-name';
$manageModal
.on('show.bs.modal', function() {
setTimeout(() => { initChecklistDropdown(); }, 200);
})
.on('change keyup paste', itemsTextarea, function() {
RepositoryListColumnType.refreshPreviewDropdownList(
previewContainer,
itemsTextarea,
delimiterDropdown,
dropdownOptions
);
initChecklistDropdown();
})
.on('change', delimiterDropdown, function() {
RepositoryListColumnType.refreshPreviewDropdownList(
previewContainer,
itemsTextarea,
delimiterDropdown,
dropdownOptions
);
initChecklistDropdown();
})
.on('columnModal::partialLoadedForRepositoryChecklistValue', function() {
RepositoryListColumnType.refreshPreviewDropdownList(
previewContainer,
itemsTextarea,
delimiterDropdown,
dropdownOptions
);
initChecklistDropdown();
})
.on('keyup change', columnNameInput, function() {
$manageModal.find(previewContainer).find('.preview-label').html($manageModal.find(columnNameInput).val());
});
}
return {
init: () => {
initDropdownItemsTextArea();
},
checkValidation: () => {
var $manageModal = $(manageModal);
var count = $manageModal.find(previewContainer).find('.items-count').attr('data-count');
return count < GLOBAL_CONSTANTS.REPOSITORY_CHECKLIST_ITEMS_PER_COLUMN;
},
loadParams: () => {
var repositoryColumnParams = {};
var options = JSON.parse($(dropdownOptions).val());
repositoryColumnParams.repository_checklist_items_attributes = options;
repositoryColumnParams.metadata = { delimiter: $(delimiterDropdown).data('used-delimiter') };
return repositoryColumnParams;
}
};
}());

View file

@ -0,0 +1,17 @@
/* eslint-disable no-unused-vars */
var RepositoryDateColumnType = (function() {
return {
init: () => {},
checkValidation: () => {
return true;
},
loadParams: () => {
var isRange = $('#date-range').is(':checked');
var columnType = $('#repository-column-data-type').val();
if (isRange) {
columnType = columnType.replace('Value', 'RangeValue');
}
return { column_type: columnType };
}
};
}());

View file

@ -0,0 +1,17 @@
/* eslint-disable no-unused-vars */
var RepositoryDateTimeColumnType = (function() {
return {
init: () => {},
checkValidation: () => {
return true;
},
loadParams: () => {
var isRange = $('#datetime-range').is(':checked');
var columnType = $('#repository-column-data-type').val();
if (isRange) {
columnType = columnType.replace('Value', 'RangeValue');
}
return { column_type: columnType };
}
};
}());

View file

@ -0,0 +1,156 @@
/* global GLOBAL_CONSTANTS */
/* eslint-disable no-unused-vars */
var RepositoryListColumnType = (function() {
var manageModal = '#manage-repository-column';
var delimiterDropdown = '.list-column-type #delimiter';
var itemsTextarea = '.list-column-type #items-textarea';
var previewContainer = '.list-column-type .dropdown-preview';
var dropdownOptions = '.list-column-type #dropdown-options';
function onlyUnique(value, index, self) {
return self.indexOf(value) === index;
}
function textToItems(text, delimiterContainer) {
var delimiter = $(delimiterContainer).val();
var res = [];
var usedDelimiter = '';
var definedDelimiters = {
return: '\n',
comma: ',',
semicolon: ';',
space: ' '
};
var delimiters = [];
if (delimiter === 'auto') {
delimiters = ['\n', ',', ';', ' '];
} else {
delimiters.push(definedDelimiters[delimiter]);
}
$.each(delimiters, (index, currentDelimiter) => {
res = text.trim().split(currentDelimiter);
usedDelimiter = Object
.keys(definedDelimiters)
.find(key => definedDelimiters[key] === currentDelimiter);
if (res.length > 1) {
return false;
}
return true;
});
res = res.filter(Boolean).filter(onlyUnique);
$.each(res, (index, option) => {
res[index] = option.slice(0, GLOBAL_CONSTANTS.NAME_MAX_LENGTH);
});
$(delimiterContainer).attr('data-used-delimiter', usedDelimiter);
return res;
}
function pluralizeWord(count, noun, suffix = 's') {
return `${noun}${count !== 1 ? suffix : ''}`;
}
function drawDropdownPreview(items, container) {
var $manageModal = $(manageModal);
var $dropdownPreview = $manageModal.find(container).find('.preview-select');
$('option', $dropdownPreview).remove();
$.each(items, function(i, item) {
$dropdownPreview.append($('<option>', {
value: item,
text: item
}));
});
}
function refreshCounter(number, container) {
var $manageModal = $(manageModal);
var $counterContainer = $manageModal.find(container).find('.limit-counter-container');
var $btn = $manageModal.find('.column-save-btn');
$counterContainer.find('.items-count').html(number).attr('data-count', number);
if (number >= GLOBAL_CONSTANTS.REPOSITORY_LIST_ITEMS_PER_COLUMN) {
$counterContainer.addClass('error-to-many-items');
$btn.addClass('disabled');
} else {
$counterContainer.removeClass('error-to-many-items');
$btn.removeClass('disabled');
}
}
function refreshPreviewDropdownList(preview, textarea, delimiterContainer, dropdown) {
var items = textToItems($(textarea).val(), delimiterContainer);
var hashItems = [];
drawDropdownPreview(items, preview);
refreshCounter(items.length, preview);
$.each(items, (index, option) => {
hashItems.push({ data: option });
});
$(dropdown).val(JSON.stringify(hashItems));
$(preview).find('.items-label').html(pluralizeWord(items.length, 'item'));
}
function initDropdownItemsTextArea() {
var $manageModal = $(manageModal);
var columnNameInput = '#repository-column-name';
$manageModal
.on('change keyup paste', itemsTextarea, function() {
refreshPreviewDropdownList(
previewContainer,
itemsTextarea,
delimiterDropdown,
dropdownOptions
);
})
.on('change', delimiterDropdown, function() {
refreshPreviewDropdownList(
previewContainer,
itemsTextarea,
delimiterDropdown,
dropdownOptions
);
})
.on('columnModal::partialLoadedForRepositoryListValue', function() {
refreshPreviewDropdownList(
previewContainer,
itemsTextarea,
delimiterDropdown,
dropdownOptions
);
})
.on('keyup change', columnNameInput, function() {
$manageModal.find(previewContainer).find('.preview-label').html($manageModal.find(columnNameInput).val());
});
}
return {
init: () => {
initDropdownItemsTextArea();
},
checkValidation: () => {
var $manageModal = $(manageModal);
var count = $manageModal.find(previewContainer).find('.items-count').attr('data-count');
return count < GLOBAL_CONSTANTS.REPOSITORY_LIST_ITEMS_PER_COLUMN;
},
loadParams: () => {
var repositoryColumnParams = {};
var options = JSON.parse($(dropdownOptions).val());
repositoryColumnParams.repository_list_items_attributes = options;
repositoryColumnParams.metadata = { delimiter: $(delimiterDropdown).data('used-delimiter') };
return repositoryColumnParams;
},
refreshPreviewDropdownList: (preview, textarea, delimiter, dropdown) => {
refreshPreviewDropdownList(preview, textarea, delimiter, dropdown);
}
};
}());

View file

@ -0,0 +1,13 @@
/* eslint-disable no-unused-vars */
var RepositoryNumberColumnType = (function() {
return {
init: () => {},
checkValidation: () => {
return true;
},
loadParams: () => {
var decimals = $('#decimals').val();
return { metadata: { decimals: decimals } };
}
};
}());

View file

@ -0,0 +1,169 @@
/* global GLOBAL_CONSTANTS I18n */
/* eslint-disable no-unused-vars */
var RepositoryStatusColumnType = (function() {
var manageModal = '#manage-repository-column';
function statusTemplate() {
return `
<div class="status-item-container loading">
<div class="status-item-icon"></div>
<input placeholder=${I18n.t('libraries.manange_modal_column.name_placeholder')}
class="status-item-field"
type="text"/>
<span class="status-item-icon-trash fas fa-trash"></span>
</div>
<div class="emojis-picker">
<span data-emoji-code="&#128540;">&#128540;</span>
<span data-emoji-code="&#128520;">&#128520;</span>
<span data-emoji-code="&#128526;">&#128526;</span>
<span data-emoji-code="&#128531;">&#128531;</span>
<span data-emoji-code="&#128535;">&#128535;</span>
<span data-emoji-code="&#128536;">&#128536;</span>
</div>`;
}
function validateForm() {
var $manageModal = $(manageModal);
var $statusRows = $manageModal.find('.status-item-container:not([data-removed])');
var $btn = $manageModal.find('.column-save-btn');
$.each($statusRows, (index, statusRow) => {
var $row = $(statusRow);
var $statusField = $row.find('.status-item-field');
var $icon = $row.find('.status-item-icon');
var stringLength = $statusField.val().length;
if (stringLength < GLOBAL_CONSTANTS.NAME_MIN_LENGTH
|| stringLength > GLOBAL_CONSTANTS.NAME_MAX_LENGTH
|| !$icon.attr('data-icon')) {
$row.addClass('error');
} else {
$row.removeClass('error');
}
});
if ($manageModal.find('.error').length > 0) {
$btn.addClass('disabled');
} else {
$btn.removeClass('disabled');
}
}
function highlightErrors() {
$(manageModal).find('.error').addClass('error-highlight');
}
function initActions() {
var $manageModal = $(manageModal);
var addStatusOptionBtn = '.add-status';
var deleteStatusOptionBtn = '.status-item-icon-trash';
var icon = '.status-item-icon';
var emojis = '.emojis-picker>span';
var statusInput = 'input.status-item-field';
var buttonWrapper = '.button-wrapper';
$manageModal.on('click', addStatusOptionBtn, function() {
var newStatusRow = $(statusTemplate()).insertBefore($(this));
validateForm();
setTimeout(function() {
newStatusRow.css('height', '34px');
}, 0);
setTimeout(function() {
newStatusRow.removeClass('loading');
newStatusRow.find('input').focus();
}, 300);
});
$manageModal.on('click', deleteStatusOptionBtn, function() {
// if existing item is deleted, flag it as deleted
// if new item is deleted, remove it from HTML
var $statusRow = $(this).parent();
var $emojis = $statusRow.next('.emojis-picker');
var isNewRow = $statusRow.data('id') == null;
setTimeout(function() {
$statusRow.addClass('loading');
$statusRow.css('height', '0px');
}, 0);
setTimeout(function() {
if (isNewRow) {
$statusRow.remove();
validateForm();
} else {
$statusRow.attr('data-removed', 'true');
$statusRow.removeClass('loading');
$statusRow.removeClass('error');
validateForm();
}
$emojis.remove();
}, 300);
});
$manageModal.on('click', icon, function() {
var $emojiPicker = $(this).parent().next('.emojis-picker');
$emojiPicker.show();
});
$manageModal.on('click', emojis, function() {
var $clickedEmoji = $(this);
var $iconField = $clickedEmoji.parent().prev().find('.status-item-icon');
$clickedEmoji.parent().hide();
$iconField.html($clickedEmoji.data('emoji-code'));
$iconField.attr('data-icon', $clickedEmoji.data('emoji-code'));
$iconField.trigger('data-attribute-changed', $iconField);
});
$manageModal
.on('keyup change', statusInput, function() {
validateForm();
})
.on('data-attribute-changed columnModal::partialLoadedForRepositoryStatusValue', function() {
validateForm();
})
.on('click', buttonWrapper, function() {
highlightErrors();
});
}
return {
init: () => {
initActions();
},
checkValidation: () => {
highlightErrors();
return !($(manageModal).find('.error').length > 0);
},
loadParams: () => {
var $modal = $(manageModal);
var $statusItems;
var repositoryColumnParams = {};
$statusItems = $modal.find('.status-item-container');
// Load all new items
// Load all existing items, delete flag included
repositoryColumnParams.repository_status_items_attributes = [];
$.each($statusItems, function(index, value) {
var $item = $(value);
var id = $item.data('id');
var removed = $item.data('removed');
var icon = $item.find('.status-item-icon').data('icon');
var status = $item.find('input.status-item-field').val();
if (removed && id) { // flag as item for removing
repositoryColumnParams.repository_status_items_attributes
.push({ id: id, _destroy: true });
} else if (id) { // existing element, maybe values needs to be updated
repositoryColumnParams.repository_status_items_attributes
.push({ id: id, icon: icon, status: status });
} else { // new element
repositoryColumnParams.repository_status_items_attributes
.push({ icon: icon, status: status });
}
});
return repositoryColumnParams;
}
};
}());

View file

@ -0,0 +1,17 @@
/* eslint-disable no-unused-vars */
var RepositoryTimeColumnType = (function() {
return {
init: () => {},
checkValidation: () => {
return true;
},
loadParams: () => {
var isRange = $('#time-range').is(':checked');
var columnType = $('#repository-column-data-type').val();
if (isRange) {
columnType = columnType.replace('Value', 'RangeValue');
}
return { column_type: columnType };
}
};
}());

View file

@ -0,0 +1,224 @@
/* global I18n HelperModule animateSpinner RepositoryListColumnType */
/* global RepositoryStatusColumnType dropdownSelector */
/* eslint-disable no-restricted-globals */
var RepositoryColumns = (function() {
var manageModal = '#manage-repository-column';
var columnTypeClassNames = {
RepositoryListValue: 'RepositoryListColumnType',
RepositoryStatusValue: 'RepositoryStatusColumnType',
RepositoryDateValue: 'RepositoryDateColumnType',
RepositoryDateTimeValue: 'RepositoryDateTimeColumnType',
RepositoryTimeValue: 'RepositoryDateTimeColumnType',
RepositoryChecklistValue: 'RepositoryChecklistColumnType',
RepositoryNumberValue: 'RepositoryNumberColumnType'
};
function initColumnTypeSelector() {
var $manageModal = $(manageModal);
$manageModal.on('change', '#repository-column-data-type', function() {
$('.column-type').hide();
$('[data-column-type="' + $(this).val() + '"]').show();
});
}
function removeElementFromDom(column) {
$('.repository-column-edtior .list-group-item[data-id="' + column.id + '"]').remove();
if ($('.list-group-item').length === 0) {
location.reload();
}
}
function initDeleteSubmitAction() {
var $manageModal = $(manageModal);
$manageModal.on('click', '#delete-repo-column-submit', function() {
animateSpinner();
$manageModal.modal('hide');
$.ajax({
url: $(this).data('delete-url'),
type: 'DELETE',
dataType: 'json',
success: (result) => {
removeElementFromDom(result);
HelperModule.flashAlertMsg(result.message, 'success');
animateSpinner(null, false);
},
error: (result) => {
animateSpinner(null, false);
HelperModule.flashAlertMsg(result.responseJSON.error, 'danger');
}
});
});
}
function checkData() {
var currentPartial = $('#repository-column-data-type').val();
if (columnTypeClassNames[currentPartial]) {
return eval(columnTypeClassNames[currentPartial])
.checkValidation();
}
return true;
}
function addSpecificParams(type, params) {
var allParams = params;
var columnParams;
var specificParams;
var currentPartial = $('#repository-column-data-type').val();
if (columnTypeClassNames[currentPartial]) {
specificParams = eval(columnTypeClassNames[currentPartial]).loadParams();
columnParams = Object.assign(params.repository_column, specificParams);
allParams.repository_column = columnParams;
}
return allParams;
}
function insertNewListItem(column) {
var attributes = column.attributes;
var html = `<li class="list-group-item row" data-id="${column.id}">
<div class="col-xs-8">
<span class="pull-left column-name">${attributes.name}</span>
</div>
<div class="col-xs-4">
<span class="controlls pull-right">
<button class="btn btn-default edit-repo-column manage-repo-column"
data-action="edit"
data-modal-url="${attributes.edit_html_url}"
>
<span class="fas fa-pencil-alt"></span>
${ I18n.t('libraries.repository_columns.index.edit_column')}
</button>
<button class="btn btn-default delete-repo-column manage-repo-column"
data-action="destroy"
data-modal-url="${attributes.destroy_html_url}"
>
<span class="fas fa-trash-alt"></span>
${ I18n.t('libraries.repository_columns.index.delete_column')}
</button>
</span>
</div>
</li>`;
// remove element if already persent
$('[data-id="' + column.id + '"]').remove();
$(html).insertBefore('.repository-columns-body ul li:first');
// remove 'no column' list item
$('[data-attr="no-columns"]').remove();
}
function updateListItem(column) {
var name = column.attributes.name;
$('li[data-id=' + column.id + ']').find('span').first().html(name);
}
function initCreateSubmitAction() {
var $manageModal = $(manageModal);
$manageModal.on('click', '#new-repo-column-submit', function() {
var url = $('#repository-column-data-type').find(':selected').data('create-url');
var params = { repository_column: { name: $('#repository-column-name').val() } };
var selectedType = $('#repository-column-data-type').val();
params = addSpecificParams(selectedType, params);
if (!checkData()) return;
$.ajax({
url: url,
type: 'POST',
data: JSON.stringify(params),
contentType: 'application/json',
success: function(result) {
var data = result.data;
insertNewListItem(data);
HelperModule.flashAlertMsg(data.attributes.message, 'success');
$manageModal.modal('hide');
},
error: function(error) {
$('#new-repository-column').renderFormErrors('repository_column', error.responseJSON.repository_column, true);
}
});
});
}
function initEditSubmitAction() {
var $manageModal = $(manageModal);
$manageModal.on('click', '#update-repo-column-submit', function() {
var url = $('#repository-column-data-type').find(':selected').data('edit-url');
var params = { repository_column: { name: $('#repository-column-name').val() } };
var selectedType = $('#repository-column-data-type').val();
params = addSpecificParams(selectedType, params);
if (!checkData()) return;
$.ajax({
url: url,
type: 'PUT',
data: JSON.stringify(params),
dataType: 'json',
contentType: 'application/json',
success: function(result) {
var data = result.data;
updateListItem(data);
HelperModule.flashAlertMsg(data.attributes.message, 'success');
$manageModal.modal('hide');
},
error: function(error) {
$('#new-repository-column').renderFormErrors('repository_column', error.responseJSON.repository_column, true);
}
});
});
}
function initManageColumnModal() {
var $manageModal = $(manageModal);
$('.repository-column-edtior').on('click', '.manage-repo-column', function() {
var button = $(this);
var modalUrl = button.data('modal-url');
var columnType;
$.get(modalUrl, (data) => {
$manageModal.modal('show').find('.modal-content').html(data.html)
.find('#repository-column-name')
.focus();
columnType = $('#repository-column-data-type').val();
dropdownSelector.init('#repository-column-data-type', {
noEmptyOption: true,
singleSelect: true,
closeOnSelect: true,
optionClass: 'custom-option',
selectAppearance: 'simple'
});
$manageModal
.trigger('columnModal::partialLoadedFor' + columnType);
if (button.data('action') === 'new') {
$('[data-column-type="RepositoryTextValue"]').show();
$('#new-repo-column-submit').show();
} else {
$('#update-repo-column-submit').show();
$('[data-column-type=' + columnType + ']').show();
}
}).fail(function() {
HelperModule.flashAlertMsg(I18n.t('libraries.repository_columns.no_permissions'), 'danger');
});
});
}
return {
init: () => {
if ($('.repository-columns-header').length > 0) {
initColumnTypeSelector();
initEditSubmitAction();
initCreateSubmitAction();
initDeleteSubmitAction();
initManageColumnModal();
RepositoryListColumnType.init();
RepositoryStatusColumnType.init();
RepositoryChecklistColumnType.init();
}
}
};
}());
$(document).on('turbolinks:load', function() {
RepositoryColumns.init();
});

View file

@ -1,225 +0,0 @@
(function() {
'use strict';
// @TODO refactor that eventually
function initEditCoumnModal() {
var modalID = '#manageRepositoryColumn';
var colRadID = '#repository_column_data_type_repositorylistvalue';
var tagsInputID = '[data-role="tagsinput"]';
var formID = '[data-role="manage-repository-column-form"]';
$('[data-action="edit"]').off('click').on('click', function() {
var editUrl = $(this).closest('li').attr('data-edit-url');
$.get(editUrl, function(data) {
$(data.html).appendTo('body').promise().done(function() {
$(modalID).modal('show').promise().done(function() {
$(modalID).on('hidden.bs.modal', function () {
// remove edit modal window
$(modalID).remove();
$('.modal-backdrop').remove();
});
_initTagInput();
setTimeout(function() {
$('#repository_column_name').focus();
}, 500)
if($(modalID).attr('data-edit-type') === 'RepositoryListValue') {
var values = JSON.parse($(tagsInputID).attr('data-value'));
$(colRadID).click().promise().done(function() {
$.each(values, function(index, element) {
$(tagsInputID).tagsinput('add', element);
});
});
}
$('[data-action="save"]').on('click', function() {
if($(colRadID).is(':checked')) {
$('#list_items').val($(tagsInputID).val());
}
_processResponse($(formID), 'update', modalID);
});
});
});
}).fail(function(error) {
HelperModule.flashAlertMsg(
"<%= I18n.t("libraries.repository_columns.no_permissions") %>",
'danger');
});
});
}
function initDeleteColumnModal() {
$('[data-action="destroy"]').off('click').on('click', function() {
var element = $(this);
var modal_html = $("#deleteRepositoryColumn");
$.get(element.closest('li').attr('data-destroy-url'), function(data) {
modal_html.find('.modal-body').html(data.html)
.promise()
.done(function() {
modal_html.modal('show');
_initSubmitAction(modal_html, $(modal_html.find('form')));
});
}).fail(function(error) {
HelperModule.flashAlertMsg(
"<%= I18n.t("libraries.repository_columns.no_permissions") %>",
'danger');
});
});
}
// @TODO refactor that eventually
function initNewColumnModal() {
var modalID = '#manageRepositoryColumn';
$('[data-action="new-column-modal"]').off('click').on('click', function() {
var modalUrl = $(this).attr('data-modal-url');
$.get(modalUrl, function(data) {
$(data.html).appendTo('body').promise().done(function() {
$(modalID).modal('show').promise().done(function() {
$(modalID).on('hidden.bs.modal', function () {
// remove create new modal window
$(modalID).remove();
$('.modal-backdrop').remove();
});
_initTagInput();
setTimeout(function() {
$('#repository_column_name').focus();
}, 500);
$('[data-action="save"]').on('click', function() {
var colRad = '#repository_column_data_type_repositorylistvalue';
if($(colRad).is(':checked')) {
$('#list_items')
.val($('[data-role="tagsinput"]').val());
}
var form = $('[data-role="manage-repository-column-form"]');
_processResponse(form, 'create', modalID);
});
});
});
});
});
}
/* *********************************
Helper methods
********************************* */
function _insertNewListItem(column) {
// remove element if already persent
$('[data-id="' + column.id + '"]').remove();
var html = '<li class="list-group-item row" data-id="' + column.id + '" ';
html += 'data-destroy-url="' + column.destroy_html_url + '"';
html += 'data-edit-url="' + column.edit_url + '"><div class="col-xs-8">';
html += '<span class="pull-left column-name">' + column.name + '</span>';
html += '</div><div class="col-xs-4"><span class="controlls pull-right">';
html += '<button class="btn btn-default" data-action="edit">';
html += '<span class="fas fa-pencil-alt"></span>&nbsp;';
html += '<%= I18n.t "libraries.repository_columns.index.edit_column" %></button>&nbsp;';
html += '<button class="btn btn-default delete-column" data-action="destroy">';
html += '<span class="fas fa-trash-alt"></span>&nbsp;';
html += '<%= I18n.t "libraries.repository_columns.index.delete_column" %>';
html += '</button></span></div></li>';
$(html).insertBefore('.repository-columns-body ul li:first')
.promise()
.done(function() {
initDeleteColumnModal();
initEditCoumnModal();
});
// remove 'no column' list item
$('[data-attr="no-columns"]').remove();
}
function _replaceListItem(column) {
$('.list-group-item[data-id="' + column.id + '"]')
.find('span.pull-left').text(column.name);
}
function _initTagInput() {
$('[name="repository_column[data_type]"]')
.on('click', function() {
var listValueId = 'repository_column_data_type_repositorylistvalue';
if($(this).attr('id') === listValueId) {
$('[data-role="tagsinput"]').tagsinput({
maxChars: <%= Constants::NAME_MAX_LENGTH %>,
trimValue: true
});
$('.bootstrap-tagsinput').show();
$('[data-role="tagsimput-label"]').show();
} else {
$('.bootstrap-tagsinput').hide();
$('[data-role="tagsimput-label"]').hide();
}
});
}
function _removeElementFromDom(column) {
$('.list-group-item[data-id="' + column.id + '"]').remove();
if($('.list-group-item').length === 0) {
location.reload();
}
}
function _initSubmitAction(modal, form) {
modal.find('[data-action="delete"]').on('click', function() {
form.submit();
modal.modal('hide')
animateSpinner();
_processResponse(form, 'destroy');
});
}
function _processResponse(form, action, modalID) {
form.on('ajax:success', function(e, data) {
switch(action) {
case 'destroy':
_removeElementFromDom(data);
break;
case 'create':
_insertNewListItem(data);
break;
case 'update':
_replaceListItem(data);
break;
default:
location.reload();
}
HelperModule.flashAlertMsg(data.message, 'success');
animateSpinner(null, false);
if (modalID) {
$(modalID).modal('hide');
}
}).on('ajax:error', function(e, xhr) {
animateSpinner(null, false);
if (modalID) {
if(xhr.responseJSON.message.hasOwnProperty('repository_list_items')) {
var message = xhr.responseJSON.message['repository_list_items'];
$('.dnd-error').remove();
$('#manageRepositoryColumn ').find('.bootstrap-tagsinput').after(
"<i class='dnd-error'>" + message + "</i>"
);
} else {
var field = { "name": xhr.responseJSON.message }
$(form).renderFormErrors('repository_column', field, true, e);
}
} else {
HelperModule.flashAlertMsg(xhr.responseJSON.message, 'danger');
}
});
if (modalID) {
form.submit();
}
}
/* *********************************
Initializers
********************************* */
initEditCoumnModal();
initDeleteColumnModal();
initNewColumnModal();
})();

View file

@ -1,7 +1,10 @@
const GLOBAL_CONSTANTS = {
NAME_TRUNCATION_LENGTH: <%= Constants::NAME_TRUNCATION_LENGTH %>,
NAME_MAX_LENGTH: <%= Constants::NAME_MAX_LENGTH %>,
NAME_MIN_LENGTH: <%= Constants::NAME_MIN_LENGTH %>,
FILENAME_TRUNCATION_LENGTH: <%= Constants::FILENAME_TRUNCATION_LENGTH %>,
FILE_MAX_SIZE_MB: <%= Rails.configuration.x.file_max_size_mb %>,
IS_SAFARI: /^((?!chrome|android).)*safari/i.test(navigator.userAgent)
IS_SAFARI: /^((?!chrome|android).)*safari/i.test(navigator.userAgent),
REPOSITORY_LIST_ITEMS_PER_COLUMN: <%= Constants::REPOSITORY_LIST_ITEMS_PER_COLUMN %>,
REPOSITORY_CHECKLIST_ITEMS_PER_COLUMN: <%= Constants::REPOSITORY_CHECKLIST_ITEMS_PER_COLUMN %>
};

View file

@ -10,7 +10,7 @@
data-placeholder // Search placeholder
data-disable-on-load // Disable input after initialization
data-select-all-button // Text for select all button
data-combine-tags // Combine multiple tags to one (only for tags)
data-combine-tags // Combine multiple tags to one (in simple mode gives you multiple select)
data-select-multiple-all-selected // Text for combine tags, when all selected
data-select-multiple-name // Text for combine tags, when select more than one tag
data-view-mode // Run in view mode
@ -55,16 +55,29 @@ var dropdownSelector = (function() {
var containerPosition = container[0].getBoundingClientRect().top;
var containerHeight = container[0].getBoundingClientRect().height;
var containerWidth = container[0].getBoundingClientRect().width;
var bottomSpace = windowHeight - containerPosition - containerHeight;
if (bottomSpace < 280) {
var bottomSpace;
var modalContainer = container.closest('.modal-dialog');
var modalContainerBottom = 0;
var maxHeight = 0;
if (modalContainer.length) {
windowHeight = modalContainer.height() + modalContainer[0].getBoundingClientRect().top;
modalContainerBottom = modalContainer[0].getBoundingClientRect().bottom;
maxHeight += modalContainerBottom;
}
bottomSpace = windowHeight - containerPosition - containerHeight;
if ((modalContainerBottom + bottomSpace) < 280) {
container.addClass('inverse');
container.find('.dropdown-container').css('max-height', `${(containerPosition - 122)}px`)
container.find('.dropdown-container').css('max-height', `${(containerPosition - 122 + maxHeight)}px`)
.css('margin-bottom', `${(containerPosition * -1)}px`)
.css('width', `${containerWidth}px`);
} else {
container.removeClass('inverse');
container.find('.dropdown-container').css('max-height', `${(bottomSpace - 32)}px`)
.css('width', '');
container.find('.dropdown-container').css('max-height', `${(bottomSpace - 32 + maxHeight)}px`)
.css('width', `${containerWidth}px`)
.css('margin-top', `${(bottomSpace * -1)}px`);
}
}
@ -123,7 +136,7 @@ var dropdownSelector = (function() {
if (mode) {
updateCurrentData(container, []);
updateTags(selector, container, { skipChange: true });
searchFieldValue.attr('placeholder', selector.data('disable-placeholder'));
searchFieldValue.attr('placeholder', selector.data('disable-placeholder') || '');
container.addClass('disabled').removeClass('open')
.find('.search-field').val('')
.prop('disabled', true);
@ -223,11 +236,17 @@ var dropdownSelector = (function() {
if (pressedKey === 38) {
if (selectedOption.prev('.dropdown-option').length) {
selectedOption.removeClass('highlight').prev('.dropdown-option').addClass('highlight');
selectedOption.removeClass('highlight').prev().addClass('highlight');
}
if (selectedOption.prev('.delimiter').length) {
selectedOption.removeClass('highlight').prev().prev().addClass('highlight');
}
} else if (pressedKey === 40) {
if (selectedOption.next('.dropdown-option').length) {
selectedOption.removeClass('highlight').next('.dropdown-option').addClass('highlight');
selectedOption.removeClass('highlight').next().addClass('highlight');
}
if (selectedOption.next('.delimiter').length) {
selectedOption.removeClass('highlight').next().next().addClass('highlight');
}
}
});
@ -259,7 +278,7 @@ var dropdownSelector = (function() {
$(`
<div class="dropdown-container"></div>
<div class="input-field">
<input type="text" class="search-field" placeholder="${selectElement.data('placeholder')}"></input>
<input type="text" class="search-field" data-options-selected=0 placeholder="${selectElement.data('placeholder') || ''}"></input>
${prepareCustomDropdownIcon(config)}
</div>
<input type="hidden" class="data-field" value="[]">
@ -362,12 +381,15 @@ var dropdownSelector = (function() {
});
// When user will resize browser we must check dropdown position
$(window).resize(function() { updateDropdownDirection(selectElement, dropdownContainer); });
$(window).resize(() => { updateDropdownDirection(selectElement, dropdownContainer); });
$(window).scroll(() => { updateDropdownDirection(selectElement, dropdownContainer); });
// When user will click away, we must close dropdown
$(window).click(() => {
if (dropdownContainer.hasClass('open') && config.onClose) {
if (dropdownContainer.hasClass('open')) {
dropdownContainer.find('.search-field').val('');
}
if (dropdownContainer.hasClass('open') && config.onClose) {
config.onClose();
}
dropdownContainer.removeClass('open active');
@ -436,6 +458,11 @@ var dropdownSelector = (function() {
`);
}
// Draw delimiter object
function drawDelimiter() {
return $('<div class="delimiter"></div>');
}
// Draw group object
function drawGroup(group) {
return $(`
@ -452,13 +479,14 @@ var dropdownSelector = (function() {
if (selector.data('config').singleSelect) {
$container.find('.dropdown-option').removeClass('select');
updateCurrentData($container, []);
selector.val($(this).data('value')).change();
}
$(this).toggleClass('select');
saveData(selector, $container);
}
// Remove placeholder from option container
container.find('.dropdown-group, .dropdown-option, .empty-dropdown').remove();
container.find('.dropdown-group, .dropdown-option, .empty-dropdown, .delimiter').remove();
if (!data) return;
if (data.length > 0) {
@ -495,7 +523,12 @@ var dropdownSelector = (function() {
} else {
// For simple options we only draw them
$.each(data, function(oi, option) {
var optionElement = drawOption(selector, option);
var optionElement;
if (option.delimiter) {
drawDelimiter().appendTo(container.find('.dropdown-container'));
return;
}
optionElement = drawOption(selector, option);
optionElement.click(clickOption);
optionElement.appendTo(container.find('.dropdown-container'));
});
@ -527,7 +560,7 @@ var dropdownSelector = (function() {
}
// First we clear search field
container.find('.search-field').val('');
if (selector.data('config').singleSelect) container.find('.search-field').val('');
// Now we check all options in dropdown for selection and add them to array
$.each(container.find('.dropdown-container .dropdown-option'), function(oi, option) {
@ -558,8 +591,6 @@ var dropdownSelector = (function() {
updateCurrentData(container, selectArray);
// Redraw tags
updateTags(selector, container, { select: true });
// Reload options in option container
loadData(selector, container);
}
// Refresh tags in input field
@ -632,7 +663,8 @@ var dropdownSelector = (function() {
// If we have alteast one tag, we need to remove placeholder from search field
searchFieldValue.attr('placeholder',
selectedOptions.length > 0 ? '' : selector.data('placeholder'));
selectedOptions.length > 0 ? '' : (selector.data('placeholder') || ''));
searchFieldValue.attr('data-options-selected', selectedOptions.length);
// Add stretch class for visual improvments
if (!selector.data('combine-tags')) {
@ -701,7 +733,11 @@ var dropdownSelector = (function() {
} else {
options = filterOptions(selector, container, selector.find('option'));
$.each(options, function(oi, option) {
result.push({ label: option.innerHTML, value: option.value });
result.push({
label: option.innerHTML,
value: option.value,
delimiter: option.dataset.delimiter
});
});
}
return result;
@ -751,7 +787,6 @@ var dropdownSelector = (function() {
values = $.map(getCurrentData($(selector).next()), (v) => {
return v.value;
});
if ($(selector).data('config').singleSelect) return values[0];
return values;

View file

@ -42,7 +42,7 @@ var renderFormError = function(ev, input, errMsgs, clearErr, errAttributes) {
})).join('<br />');
var $errSpan = "<span class='help-block'" +
errAttributes + '>' + errorText + '</span>';
$formGroup.append($errSpan);
$(input).after($errSpan);
}
var $parent;

View file

@ -6,7 +6,7 @@ var PerfectSb = (function() {
function init() {
$.each($('.perfect-scrollbar'), function(index, object) {
var ps;
activePSB.lenght = 0;
activePSB.length = 0;
ps = new PerfectScrollbar(object, { wheelSpeed: 0.5, minScrollbarLength: 20 });
activePSB.push(ps);
});

View file

@ -94,11 +94,17 @@
var mouse = { x: e.clientX, y: e.clientY };
$('.popover.tooltip-open').each(function(i, obj) {
var tooltipObj = '*[data-tooltip-id="' + obj.dataset.popoverId + '"]';
var objHeight = $(tooltipObj)[0].clientHeight;
var objWidth = $(tooltipObj)[0].clientWidth;
var objLeft = $(tooltipObj)[0].offsetLeft;
var objTop = $(tooltipObj)[0].offsetTop;
var objCorners = {
var objHeight;
var objWidth;
var objLeft;
var objTop;
var objCorners;
if ($(tooltipObj).length === 0) return;
objHeight = $(tooltipObj)[0].clientHeight;
objWidth = $(tooltipObj)[0].clientWidth;
objLeft = $(tooltipObj)[0].offsetLeft;
objTop = $(tooltipObj)[0].offsetTop;
objCorners = {
tl: { x: objLeft, y: objTop },
tr: { x: (objLeft + objWidth), y: objTop },
bl: { x: objLeft, y: (objTop + objHeight) },

View file

@ -154,7 +154,7 @@ $btn-border-radius-small: $border-radius-small;
// $input-bg: #fff;
// $input-bg-disabled: $gray-lighter;
// $input-color: $gray;
// $input-border: #ccc;
$input-border: #ccc;
// $input-border-radius: $border-radius-base;
// $input-border-radius-large: $border-radius-large;
// $input-border-radius-small: $border-radius-small;

View file

@ -61,6 +61,14 @@
padding: 3px 12px;
}
}
.repository-status-value-icon {
margin-right: 5px;
}
.dropdown-selector-container {
width: 150px;
}
}
.repository-cog {
@ -140,6 +148,128 @@
}
}
.asset-value-cell {
align-items: center;
display: flex;
.fas {
font-size: 18px;
min-width: 18px;
}
.image-icon {
width: 18px;
}
.file-preview-link {
min-width: 140px;
padding-left: 5px;
}
}
.file-editing {
width: 200px;
.file-upload-button {
background-color: $color-white;
border: solid 1px;
border-radius: 3px;
height: 34px;
line-height: 32px;
position: relative;
&.new-file {
.icon {
display: none;
}
label {
display: inline-block;
}
&:hover {
.fa-trash {
display: none;
}
}
}
.icon {
display: inline-block;
text-align: center;
width: 34px;
}
.label-asset {
display: inline-block;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
width: calc(100% - 44px);
}
.fa-trash {
background-color: $color-white;
cursor: pointer;
display: none;
line-height: 32px;
position: absolute;
right: 0;
text-align: center;
top: 0;
width: 34px;
z-index: 3;
}
&:hover {
.fa-trash {
display: inline-block;
}
}
&.error {
border-color: $brand-danger;
margin-bottom: 10px;
&::after {
background-color: $color-white;
bottom: 0;
color: $brand-danger;
content: "\f071";
font-family: "Font Awesome 5 Free";
font-weight: 501;
line-height: 32px;
position: absolute;
right: 0;
text-align: center;
width: 34px;
}
&::before {
bottom: -15px;
color: $brand-danger;
content: attr(data-error-text);
left: 0;
line-height: 15px;
position: absolute;
width: 100%;
}
}
}
input[type=file] {
display: none;
}
label {
display: none;
font-weight: normal;
margin-left: 10px;
}
}
.toolbarButtonsDatatable {
.view-only-label {
opacity: .6;
@ -255,3 +385,4 @@
color: $color-silver-chalice;
}
}

View file

@ -0,0 +1,30 @@
// scss-lint:disable SelectorDepth SelectorFormat QualifyingElement
// scss-lint:disable NestingDepth ImportantRule
@import "constants";
.repository-table {
// Cells
// Checklists
.checklist-dropdown {
.dropdown-menu {
.checklist-item {
padding: 5px;
}
}
}
// DateTime
.dateonly {
input.time-part {
display: none;
}
}
.timeonly {
input.date-part {
display: none;
}
}
}

View file

@ -0,0 +1,26 @@
// scss-lint:disable IdSelector
@import "constants";
#manage-repository-column {
.modal-footer {
text-align: center;
}
.delete-footer {
text-align: right;
}
.range-label {
left: 3px;
position: relative;
top: -2px;
}
#repository-column-data-type + .dropdown-selector-container {
.custom-option {
padding: 0 25px;
}
}
}

View file

@ -0,0 +1,52 @@
// scss-lint:disable NestingDepth
@import "constants";
.dropdown-preview {
align-items: center;
background-color: $color-concrete;
border: 1px solid $color-gainsboro;
border-radius: 5px;
display: flex;
justify-content: center;
padding: 10px 100px 5px;
position: relative;
.field-name {
color: $color-silver-chalice;
left: 0;
padding: 0 5px;
position: absolute;
top: 0;
}
.preview-block {
flex-basis: 200px;
position: relative;
}
.limit-counter-container {
bottom: 0;
color: $color-silver-chalice;
left: 100%;
line-height: 34px;
margin-left: 15px;
position: absolute;
width: 100px;
.list-items-limit {
display: none;
}
&.error-to-many-items {
.list-items-count {
color: $brand-danger;
font-weight: bold;
}
.list-items-limit {
display: inline;
}
}
}
}

View file

@ -0,0 +1,109 @@
@import "constants";
.status-item-container {
align-items: center;
display: flex;
height: 34px;
margin-bottom: 5px;
transition: .3s;
.status-item-field {
border: 1px solid $input-border;
border-left: 0;
border-radius: 0 4px 4px 0;
flex-grow: 1;
font-size: 14px;
height: 34px;
padding: 6px 12px;
}
.status-item-icon {
border: 1px solid $input-border;
border-radius: 4px 0 0 4px;
cursor: pointer;
display: inline-block;
height: 34px;
line-height: 32px;
text-align: center;
width: 34px;
&:not([data-icon])::before {
content: "\f06a";
font-family: "Font Awesome 5 Free";
font-weight: 900;
}
}
.status-item-icon-trash {
color: $color-silver-chalice;
padding: 0 10px;
width: 34px;
&:hover {
color: $color-dove-gray;
}
}
&.loading {
height: 0;
* {
display: none;
}
}
&[data-removed="true"] {
display: none;
}
&.error.error-highlight {
.status-item-icon,
.status-item-field {
border-color: $brand-danger;
}
.status-item-icon {
border-right-color: $input-border;
}
}
}
.add-status {
align-items: center;
border: 1px solid transparent;
border-radius: 4px;
color: $color-silver-chalice;
cursor: pointer;
display: flex;
font-size: 14px;
height: 34px;
line-height: 32px;
margin-right: 32px;
transition: .2s;
.fa-plus {
display: inline-block;
height: 34px;
line-height: 32px;
text-align: center;
width: 34px;
}
&:hover {
border: 1px solid $color-gainsboro;
}
.add-status-label {
flex-grow: 1;
padding-left: 5px;
}
}
.emojis-picker {
border: 1px solid $color-gainsboro;
display: none;
font-size: 30px;
height: 40px;
width: 211px;
}

View file

@ -40,6 +40,15 @@ input[type="checkbox"].simple-checkbox {
}
}
&.disabled {
pointer-events: none;
+ .checkbox-label {
color: $color-silver-chalice;
cursor: not-allowed;
}
}
&.hidden + .checkbox-label {
display: none;
}

View file

@ -52,9 +52,12 @@
.ds-simple {
font-size: 14px;
line-height: 28px;
overflow: hidden;
padding-left: 5px;
position: relative;
text-overflow: ellipsis;
transition: .3s;
white-space: nowrap;
z-index: 2;
}
@ -106,8 +109,8 @@
box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.12), 0 0 4px 0 rgba(0, 0, 0, 0.08);
display: none;
overflow: hidden;
position: absolute;
top: calc(100% - 30px);
position: fixed;
bottom: calc(100% - 30px);
transition: .2s;
transition-property: top, bottom, box-shadow;
width: 100%;
@ -119,6 +122,12 @@
text-align: center;
}
.delimiter {
background: $color-alto;
height: 1px;
margin: 5px 16px;
}
.dropdown-select-all {
background: $color-white;
border: 0;
@ -158,7 +167,7 @@
cursor: pointer;
display: flex;
min-height: 32px;
padding: 0 10px;
padding: 3px 10px;
position: relative;
user-select: none;
@ -227,6 +236,22 @@
bottom: 3px;
display: none;
position: absolute;
&[data-options-selected="0"] {
display: block;
}
}
.ds-simple {
.tag-label {
overflow: hiddens;
text-overflow: ellipsis;
white-space: nowrap;
}
.fa-times {
display: none;
}
}
}
}
@ -248,6 +273,7 @@
.dropdown-container {
border-top: 0;
bottom: auto;
box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.12), 0 0 4px 0 rgba(0, 0, 0, 0.08);
display: block;
top: 100%;
@ -274,6 +300,10 @@
.search-field {
display: block;
line-height: 14px;
&[data-options-selected="0"] {
line-height: 24px;
}
}
.ds-simple {

View file

@ -14,7 +14,7 @@
@media only screen and (max-width: 768px) {
.repository-columns-body {
.delete-column {
.delete-repo-column {
margin-top: 5px;
}
}
@ -83,6 +83,28 @@
}
}
.repository-table {
tbody {
tr:hover {
background-color: $color-concrete;
}
.editing {
border: 1px solid;
}
.repository-row-edit-icon {
opacity: 0;
padding-right: 10px;
}
tr:hover .repository-row-edit-icon {
cursor: pointer;
opacity: 1;
}
}
}
.new-input-file-field-div {
display: flex;
flex-direction: row;

View file

@ -14,6 +14,7 @@ module Api
def index
columns = @inventory.repository_columns
.includes(:repository_list_items)
.includes(:repository_status_items)
.page(params.dig(:page, :number))
.per(params.dig(:page, :size))
render jsonapi: columns,

View file

@ -0,0 +1,76 @@
# frozen_string_literal: true
module Api
module V1
class InventoryStatusItemsController < BaseController
before_action :load_team, :load_inventory, :load_inventory_column, :check_column_type
before_action :load_inventory_status_item, only: %i(show update destroy)
before_action :check_manage_permissions, only: %i(create update destroy)
def index
status_items = @inventory_column.repository_status_items
.page(params.dig(:page, :number))
.per(params.dig(:page, :size))
render jsonapi: status_items, each_serializer: InventoryStatusItemSerializer
end
def create
status_item = @inventory_column.repository_status_items.create!(inventory_status_item_params)
render jsonapi: status_item,
serializer: InventoryStatusItemSerializer,
status: :created
end
def show
render jsonapi: @inventory_status_item,
serializer: InventoryStatusItemSerializer
end
def update
@inventory_status_item.attributes = update_inventory_status_item_params
if @inventory_status_item.changed? && @inventory_status_item.save!
render jsonapi: @inventory_status_item,
serializer: InventoryStatusItemSerializer
else
render body: nil, status: :no_content
end
end
def destroy
@inventory_status_item.destroy!
render body: nil, status: :ok
end
private
def check_column_type
raise TypeError unless @inventory_column.data_type == 'RepositoryStatusValue'
end
def load_inventory_status_item
@inventory_status_item = @inventory_column.repository_status_items.find(params.require(:id))
end
def check_manage_permissions
raise PermissionError.new(RepositoryStatusItem, :manage) unless can_manage_repository_column?(@inventory_column)
end
def inventory_status_item_params
raise TypeError unless params.require(:data).require(:type) == 'inventory_status_items'
params.require(:data).require(:attributes)
params.permit(data: { attributes: %i(status icon) })[:data].merge(
created_by: @current_user,
last_modified_by: @current_user,
repository: @inventory
)
end
def update_inventory_status_item_params
raise IDMismatchError unless params.require(:data).require(:id).to_i == params[:id].to_i
inventory_status_item_params[:attributes]
end
end
end
end

View file

@ -0,0 +1,54 @@
# frozen_string_literal: true
module RepositoryColumns
class AssetColumnsController < BaseColumnsController
include InputSanitizeHelper
before_action :load_column, only: %i(update destroy)
before_action :check_create_permissions, only: :create
before_action :check_manage_permissions, only: %i(update destroy)
def create
service = RepositoryColumns::CreateColumnService
.call(user: current_user, repository: @repository, team: current_team,
column_type: Extends::REPOSITORY_DATA_TYPES[:RepositoryAssetValue],
params: repository_column_params)
if service.succeed?
render json: service.column, status: :created, creating: true
else
render json: service.errors, status: :unprocessable_entity
end
end
def update
service = RepositoryColumns::UpdateColumnService
.call(user: current_user,
team: current_team,
column: @repository_column,
params: repository_column_params)
if service.succeed?
render json: service.column, status: :ok, editing: true
else
render json: service.errors, status: :unprocessable_entity
end
end
def destroy
service = RepositoryColumns::DeleteColumnService
.call(user: current_user, team: current_team, column: @repository_column)
if service.succeed?
render json: {}, status: :ok
else
render json: service.errors, status: :unprocessable_entity
end
end
private
def repository_column_params
params.require(:repository_column).permit(:name)
end
end
end

View file

@ -0,0 +1,28 @@
# frozen_string_literal: true
module RepositoryColumns
class BaseColumnsController < ApplicationController
include InputSanitizeHelper
before_action :load_repository
private
def load_repository
@repository = Repository.accessible_by_teams(current_team).find_by(id: params[:repository_id])
render_404 unless @repository
end
def load_column
@repository_column = @repository.repository_columns.find_by(id: params[:id])
render_404 unless @repository_column
end
def check_create_permissions
render_403 unless can_create_repository_columns?(@repository)
end
def check_manage_permissions
render_403 unless can_manage_repository_column?(@repository_column)
end
end
end

View file

@ -0,0 +1,77 @@
# frozen_string_literal: true
module RepositoryColumns
class ChecklistColumnsController < BaseColumnsController
before_action :load_column, only: %i(update destroy items)
before_action :check_create_permissions, only: :create
before_action :check_manage_permissions, only: %i(update destroy items)
helper_method :delimiters
def create
service = RepositoryColumns::CreateColumnService
.call(user: current_user, repository: @repository, team: current_team,
column_type: Extends::REPOSITORY_DATA_TYPES[:RepositoryChecklistValue],
params: repository_column_params)
if service.succeed?
render json: service.column, status: :created, creating: true
else
render json: service.errors, status: :unprocessable_entity
end
end
def update
service = RepositoryColumns::UpdateChecklistColumnService
.call(user: current_user,
team: current_team,
column: @repository_column,
params: repository_column_params)
if service.succeed?
render json: service.column, status: :ok, editing: true
else
render json: service.errors, status: :unprocessable_entity
end
end
def destroy
service = RepositoryColumns::DeleteColumnService
.call(user: current_user, team: current_team, column: @repository_column)
if service.succeed?
render json: {}, status: :ok
else
render json: service.errors, status: :unprocessable_entity
end
end
def items
column_checklist_items = @repository_column.repository_checklist_items
.where('data ILIKE ?',
"%#{search_params[:query]}%")
.select(:id, :data)
.order(data: :asc)
render json: column_checklist_items.map { |i| { value: i.id, label: escape_input(i.data) } }, status: :ok
end
private
def search_params
params.permit(:query, :column_id)
end
def repository_column_params
params
.require(:repository_column)
.permit(:name, metadata: [:delimiter], repository_checklist_items_attributes: %i(data))
end
def delimiters
Constants::REPOSITORY_LIST_ITEMS_DELIMITERS
.split(',')
.map { |e| Hash[t('libraries.manange_modal_column.list_type.delimiters.' + e), e] }
.inject(:merge)
end
end
end

View file

@ -0,0 +1,58 @@
# frozen_string_literal: true
module RepositoryColumns
class DateTimeColumnsController < BaseColumnsController
include InputSanitizeHelper
before_action :load_column, only: %i(update destroy)
before_action :check_create_permissions, only: :create
before_action :check_manage_permissions, only: %i(update destroy)
def create
service = RepositoryColumns::CreateColumnService
.call(user: current_user, repository: @repository, team: current_team,
column_type: column_type_param,
params: repository_column_params)
if service.succeed?
render json: service.column, status: :created
else
render json: service.errors, status: :unprocessable_entity
end
end
def update
service = RepositoryColumns::UpdateColumnService
.call(user: current_user,
team: current_team,
column: @repository_column,
params: repository_column_params)
if service.succeed?
render json: service.column, status: :ok
else
render json: service.errors, status: :unprocessable_entity
end
end
def destroy
service = RepositoryColumns::DeleteColumnService
.call(user: current_user, team: current_team, column: @repository_column)
if service.succeed?
render json: {}, status: :ok
else
render json: service.errors, status: :unprocessable_entity
end
end
private
def repository_column_params
params.require(:repository_column).permit(:name)
end
def column_type_param
params.require(:repository_column).require(:column_type)
end
end
end

View file

@ -0,0 +1,77 @@
# frozen_string_literal: true
module RepositoryColumns
class ListColumnsController < BaseColumnsController
before_action :load_column, only: %i(update destroy items)
before_action :check_create_permissions, only: :create
before_action :check_manage_permissions, only: %i(update destroy items)
helper_method :delimiters
def create
service = RepositoryColumns::CreateColumnService
.call(user: current_user, repository: @repository, team: current_team,
column_type: Extends::REPOSITORY_DATA_TYPES[:RepositoryListValue],
params: repository_column_params)
if service.succeed?
render json: service.column, status: :created, creating: true
else
render json: service.errors, status: :unprocessable_entity
end
end
def update
service = RepositoryColumns::UpdateListColumnService
.call(user: current_user,
team: current_team,
column: @repository_column,
params: repository_column_params)
if service.succeed?
render json: service.column, status: :ok, editing: true
else
render json: service.errors, status: :unprocessable_entity
end
end
def destroy
service = RepositoryColumns::DeleteColumnService
.call(user: current_user, team: current_team, column: @repository_column)
if service.succeed?
render json: {}, status: :ok
else
render json: service.errors, status: :unprocessable_entity
end
end
def items
column_list_items = @repository_column.repository_list_items
.where('data ILIKE ?',
"%#{search_params[:query]}%")
.limit(Constants::SEARCH_LIMIT)
.select(:id, :data)
render json: column_list_items.map { |i| { value: i.id, label: escape_input(i.data) } }, status: :ok
end
private
def search_params
params.permit(:query, :column_id)
end
def repository_column_params
params
.require(:repository_column)
.permit(:name, metadata: [:delimiter], repository_list_items_attributes: %i(data))
end
def delimiters
Constants::REPOSITORY_LIST_ITEMS_DELIMITERS
.split(',')
.map { |e| Hash[t('libraries.manange_modal_column.list_type.delimiters.' + e), e] }
.inject(:merge)
end
end
end

View file

@ -0,0 +1,54 @@
# frozen_string_literal: true
module RepositoryColumns
class NumberColumnsController < BaseColumnsController
include InputSanitizeHelper
before_action :load_column, only: %i(update destroy)
before_action :check_create_permissions, only: :create
before_action :check_manage_permissions, only: %i(update destroy)
def create
service = RepositoryColumns::CreateColumnService
.call(user: current_user, repository: @repository, team: current_team,
column_type: Extends::REPOSITORY_DATA_TYPES[:RepositoryNumberValue],
params: repository_column_params)
if service.succeed?
render json: service.column, status: :created, creating: true
else
render json: service.errors, status: :unprocessable_entity
end
end
def update
service = RepositoryColumns::UpdateColumnService
.call(user: current_user,
team: current_team,
column: @repository_column,
params: repository_column_params)
if service.succeed?
render json: service.column, status: :ok, editing: true
else
render json: service.errors, status: :unprocessable_entity
end
end
def destroy
service = RepositoryColumns::DeleteColumnService
.call(user: current_user, team: current_team, column: @repository_column)
if service.succeed?
render json: {}, status: :ok
else
render json: service.errors, status: :unprocessable_entity
end
end
private
def repository_column_params
params.require(:repository_column).permit(:name, metadata: [:decimals])
end
end
end

View file

@ -0,0 +1,72 @@
# frozen_string_literal: true
module RepositoryColumns
class StatusColumnsController < BaseColumnsController
include InputSanitizeHelper
before_action :load_column, only: %i(update destroy items)
before_action :check_create_permissions, only: :create
before_action :check_manage_permissions, only: %i(update destroy items)
def create
service = RepositoryColumns::CreateColumnService
.call(user: current_user, repository: @repository, team: current_team,
column_type: Extends::REPOSITORY_DATA_TYPES[:RepositoryStatusValue],
params: repository_column_params)
if service.succeed?
render json: service.column, status: :created, creating: true
else
render json: service.errors, status: :unprocessable_entity
end
end
def update
service = RepositoryColumns::UpdateColumnService
.call(user: current_user,
team: current_team,
column: @repository_column,
params: update_repository_column_params)
if service.succeed?
render json: service.column, status: :ok, editing: true
else
render json: service.errors, status: :unprocessable_entity
end
end
def destroy
service = RepositoryColumns::DeleteColumnService
.call(user: current_user, team: current_team, column: @repository_column)
if service.succeed?
render json: {}, status: :ok
else
render json: service.errors, status: :unprocessable_entity
end
end
def items
column_status_items = @repository_column.repository_status_items
.where('status ILIKE ?',
"%#{search_params[:query]}%")
.select(:id, :icon, :status)
render json: column_status_items
.map { |i| { value: i.id, label: "#{i.icon} #{escape_input(i.status)}" } }, status: :ok
end
private
def search_params
params.permit(:query, :column_id)
end
def repository_column_params
params.require(:repository_column).permit(:name, repository_status_items_attributes: %i(status icon))
end
def update_repository_column_params
params.require(:repository_column).permit(:name, repository_status_items_attributes: %i(id status icon _destroy))
end
end
end

View file

@ -0,0 +1,54 @@
# frozen_string_literal: true
module RepositoryColumns
class TextColumnsController < BaseColumnsController
include InputSanitizeHelper
before_action :load_column, only: %i(update destroy)
before_action :check_create_permissions, only: :create
before_action :check_manage_permissions, only: %i(update destroy)
def create
service = RepositoryColumns::CreateColumnService
.call(user: current_user, repository: @repository, team: current_team,
column_type: Extends::REPOSITORY_DATA_TYPES[:RepositoryTextValue],
params: repository_column_params)
if service.succeed?
render json: service.column, status: :created, creating: true
else
render json: service.errors, status: :unprocessable_entity
end
end
def update
service = RepositoryColumns::UpdateColumnService
.call(user: current_user,
team: current_team,
column: @repository_column,
params: repository_column_params)
if service.succeed?
render json: service.column, status: :ok, editing: true
else
render json: service.errors, status: :unprocessable_entity
end
end
def destroy
service = RepositoryColumns::DeleteColumnService
.call(user: current_user, team: current_team, column: @repository_column)
if service.succeed?
render json: {}, status: :ok
else
render json: service.errors, status: :unprocessable_entity
end
end
private
def repository_column_params
params.require(:repository_column).permit(:name)
end
end
end

View file

@ -1,5 +1,7 @@
class RepositoryColumnsController < ApplicationController
include InputSanitizeHelper
include RepositoryColumnsHelper
ACTIONS = %i(
create index create_html available_asset_type_columns available_columns
).freeze
@ -21,7 +23,7 @@ class RepositoryColumnsController < ApplicationController
format.json do
render json: {
html: render_to_string(
partial: 'repository_columns/manage_column_modal.html.erb'
partial: 'repository_columns/manage_column_modal_content.html.erb'
)
}
end
@ -73,15 +75,7 @@ class RepositoryColumnsController < ApplicationController
end
def edit
respond_to do |format|
format.json do
render json: {
html: render_to_string(
partial: 'repository_columns/manage_column_modal.html.erb'
)
}
end
end
render json: { html: render_to_string(partial: 'repository_columns/manage_column_modal_content.html.erb') }
end
def update

View file

@ -1,28 +0,0 @@
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)
end
end

View file

@ -37,38 +37,18 @@ class RepositoryRowsController < ApplicationController
end
def create
record = RepositoryRow.new(repository: @repository,
created_by: current_user,
last_modified_by: current_user)
errors = { default_fields: [],
repository_cells: [] }
service = RepositoryRows::CreateRepositoryRowService
.call(repository: @repository, user: current_user, params: update_params)
record.transaction do
record.name = record_params[:repository_row_name] unless record_params[:repository_row_name].blank?
errors[:default_fields] = record.errors.messages unless record.save
if cell_params
cell_params.each do |key, value|
next if create_cell_value(record, key, value, errors).nil?
end
end
raise ActiveRecord::Rollback if errors[:repository_cells].any?
end
if service.succeed?
log_activity(:create_item_inventory, service.repository_row) if service.succeed?
respond_to do |format|
format.json do
if errors[:default_fields].empty? && errors[:repository_cells].empty?
log_activity(:create_item_inventory, record)
render json: { id: record.id,
flash: t('repositories.create.success_flash',
record: escape_input(record.name),
repository: escape_input(@repository.name)) },
status: :ok
else
render json: errors,
status: :bad_request
end
end
render json: { id: service.repository_row.id, flash: t('repositories.create.success_flash',
record: escape_input(service.repository_row.name),
repository: escape_input(@repository.name)) },
status: :ok
else
render json: service.errors, status: :bad_request
end
end
@ -117,156 +97,18 @@ class RepositoryRowsController < ApplicationController
end
def update
errors = {
default_fields: [],
repository_cells: []
}
row_update = RepositoryRows::UpdateRepositoryRowService
.call(repository_row: @record, user: current_user, params: update_params)
@record.transaction do
@record.name = record_params[:repository_row_name].blank? ? nil : record_params[:repository_row_name]
errors[:default_fields] = @record.errors.messages unless @record.save
if cell_params
cell_params.each do |key, value|
existing = @record.repository_cells.detect do |c|
c.repository_column_id == key.to_i
end
if existing
# Cell exists and new value present, so update value
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
elsif existing.value_type == 'RepositoryAssetValue'
existing.value.destroy && next if remove_file_columns_params.include?(key)
if existing.value.asset.update(file: value)
existing.value.asset.created_by = current_user
existing.value.asset.last_modified_by = current_user
existing.value.asset.post_process_file(current_team)
else
errors[:repository_cells] << {
"#{existing.repository_column_id}": { data: existing.value.asset.errors.messages[:file].first }
}
end
else
existing.value.destroy && next if value == ''
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
next if value == ''
# Looks like it is a new cell, so we need to create new value, cell
# will be created automatically
next if create_cell_value(@record, key, value, errors).nil?
end
end
else
@record.repository_cells.each { |c| c.value.destroy }
end
raise ActiveRecord::Rollback if errors[:repository_cells].any?
end
if row_update.succeed?
log_activity(:edit_item_inventory, @record) if row_update.record_updated
respond_to do |format|
format.json do
if errors[:default_fields].empty? && errors[:repository_cells].empty?
# Row sucessfully updated, so sending response to client
log_activity(:edit_item_inventory, @record)
render json: {
id: @record.id,
flash: t(
'repositories.update.success_flash',
record: escape_input(@record.name),
repository: escape_input(@repository.name)
)
},
status: :ok
else
# Errors
render json: errors,
status: :bad_request
end
end
end
end
def create_cell_value(record, key, value, errors)
column = @repository.repository_columns.detect do |c|
c.id == key.to_i
end
save_successful = false
if column.data_type == 'RepositoryListValue'
return if value == '-1'
# check if item exists 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
}
)
save_successful = list_item && cell_value.save
elsif column.data_type == 'RepositoryAssetValue'
return if value.blank?
asset = Asset.new(file: value,
created_by: current_user,
last_modified_by: current_user,
team: current_team)
if asset.save
asset.post_process_file(current_team)
else
errors[:repository_cells] << {
"#{column.id}": { data: asset.errors.messages[:file].first }
}
end
cell_value = RepositoryAssetValue.new(
asset: asset,
created_by: current_user,
last_modified_by: current_user,
repository_cell_attributes: {
repository_row: record,
repository_column: column
}
)
save_successful = cell_value.save
render json: { id: @record.id, flash: t('repositories.update.success_flash',
record: escape_input(@record.name),
repository: escape_input(@repository.name)) },
status: :ok
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
}
)
if (save_successful = cell_value.save)
record_annotation_notification(record,
cell_value.repository_cell)
end
end
unless save_successful
errors[:repository_cells] << {
"#{column.id}": cell_value.errors.messages
}
render json: row_update.errors, status: :bad_request
end
end
@ -377,14 +219,6 @@ class RepositoryRowsController < ApplicationController
render_403 unless can_delete_repository_rows?(@repository)
end
def record_params
params.permit(:repository_row_name).to_h
end
def cell_params
params.permit(repository_cells: {}).to_h[:repository_cells]
end
def remove_file_columns_params
JSON.parse(params.fetch(:remove_file_columns) { '[]' })
end
@ -456,6 +290,10 @@ class RepositoryRowsController < ApplicationController
collection
end
def update_params
params.permit(repository_row: {}, repository_cells: {}).to_h
end
def log_activity(type_of, repository_row)
Activities::CreateActivityService
.call(activity_type: type_of,

View file

@ -42,7 +42,7 @@ module FileIconsHelper
image_link = Extends::FILE_ICON_MAPPINGS[file_ext] if Extends::FILE_ICON_MAPPINGS[file_ext]
if image_link
image_tag image_link
ActionController::Base.helpers.image_tag(image_link, class: 'image-icon')
else
''
end
@ -95,4 +95,12 @@ module FileIconsHelper
'application/vnd.openxmlformats-officedocument.presentationml.presentation'
end
end
def file_extension_icon_html(asset)
html = file_extension_icon(asset)
html = "<i class='fas #{file_fa_icon_class(asset)}'></i>" if html.blank?
html
end
module_function :file_extension_icon_html, :file_extension_icon, :file_fa_icon_class
end

View file

@ -1,16 +1,9 @@
# frozen_string_literal: true
module RepositoryColumnsHelper
def form_url(repository, column)
return repository_repository_columns_path(repository) if column.new_record?
repository_repository_column_path(repository, column)
end
def disabled?(column, type)
return false if column.new_record?
column.data_type != type
end
def checked?(column, type)
return true if column.new_record? && type == 'RepositoryTextValue'
return true if column.data_type == type
def defined_delimiters_options
(%i(auto) + Constants::REPOSITORY_LIST_ITEMS_DELIMITERS_MAP.keys)
.map { |e| Hash[t('libraries.manange_modal_column.list_type.delimiters.' + e.to_s), e] }
.inject(:merge)
end
end

View file

@ -1,58 +1,48 @@
# frozen_string_literal: true
module RepositoryDatatableHelper
include InputSanitizeHelper
def prepare_row_columns(repository_rows,
repository,
columns_mappings,
team,
_team,
assigned_rows)
parsed_records = []
repository_rows.each do |record|
includes_json = { repository_cells: Extends::REPOSITORY_SEARCH_INCLUDES }
repository_rows.includes(includes_json).each do |record|
row = {
'DT_RowId': record.id,
'1': assigned_row(record, assigned_rows),
'2': record.id,
'3': escape_input(record.name),
'4': I18n.l(record.created_at, format: :full),
'5': 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)
}
'DT_RowId': record.id,
'1': assigned_row(record, assigned_rows),
'2': record.id,
'3': escape_input(record.name),
'4': I18n.l(record.created_at, format: :full),
'5': 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]] =
display_cell_value(cell, team)
display_cell_value(cell)
end
parsed_records << row
end
parsed_records
end
def display_cell_value(cell, team)
if cell.value_type == 'RepositoryAssetValue'
# Return simple file_name if we call this method not from controller
return cell.value.asset.file_name unless defined?(render)
render partial: 'shared/asset_link',
locals: { asset: cell.value.asset, display_image_tag: false },
formats: :html
else
custom_auto_link(display_tooltip(cell.value.data,
Constants::NAME_MAX_LENGTH),
simple_format: true,
team: team)
end
end
def assigned_row(record, assigned_rows)
if assigned_rows&.include?(record)
"<span class='circle-icon'>&nbsp;</span>"
@ -68,18 +58,16 @@ module RepositoryDatatableHelper
can_manage_repository_rows?(repository)
end
# The order must be converted from Ruby Hash into a JS array -
# because arrays in JS are in truth regular JS objects with indexes as keys
def default_table_order_as_js_array
Constants::REPOSITORY_TABLE_DEFAULT_STATE[:order].keys.sort.map do |k|
Constants::REPOSITORY_TABLE_DEFAULT_STATE[:order][k]
end.to_s
Constants::REPOSITORY_TABLE_DEFAULT_STATE['order'].to_json
end
def default_table_columns
Constants::REPOSITORY_TABLE_DEFAULT_STATE[:columns].keys.sort.map do |k|
col = Constants::REPOSITORY_TABLE_DEFAULT_STATE[:columns][k]
col.slice(:visible, :searchable)
end.to_json
Constants::REPOSITORY_TABLE_DEFAULT_STATE['columns'].to_json
end
def display_cell_value(cell)
"RepositoryDatatable::#{cell.repository_column.data_type}Serializer"
.constantize.new(cell.value).serializable_hash
end
end

View file

@ -0,0 +1 @@
require('inputmask');

View file

@ -44,7 +44,7 @@ module SearchableModel
"CAST(#{a} AS TEXT) #{like} :t#{i} OR "
else
col = options[:at_search].to_s == 'true' ? "lower(#{a})": a
"(trim_html_tags(#{col})) #{like} :t#{i} OR "
"(trim_html_tags((#{col})::text)) #{like} :t#{i} OR "
end
end
).join[0..-5]
@ -86,7 +86,7 @@ module SearchableModel
if a == 'repository_rows.id'
"CAST(#{a} AS TEXT) #{like} :t#{i} OR "
else
"(trim_html_tags(#{a})) #{like} :t#{i} OR "
"(trim_html_tags((#{a})::text)) #{like} :t#{i} OR "
end
end
).join[0..-5]

View file

@ -18,6 +18,7 @@ class Repository < ApplicationRecord
inverse_of: :repository, dependent: :destroy
has_many :report_elements, inverse_of: :repository, dependent: :destroy
has_many :repository_list_items, inverse_of: :repository, dependent: :destroy
has_many :repository_checklist_items, inverse_of: :repository, dependent: :destroy
has_many :team_repositories, inverse_of: :repository, dependent: :destroy
has_many :teams_shared_with, through: :team_repositories, source: :team

View file

@ -33,8 +33,12 @@ class RepositoryAssetValue < ApplicationRecord
end
def update_data!(new_data, user)
asset.file.attach(io: StringIO.new(Base64.decode64(new_data[:file_data].split(',')[1])),
filename: new_data[:file_name])
if new_data.is_a?(String) # assume it's a signed_id_token
asset.file.attach(new_data)
elsif new_data[:file_data]
asset.file.attach(io: StringIO.new(Base64.decode64(new_data[:file_data])), filename: new_data[:file_name])
end
asset.last_modified_by = user
self.last_modified_by = user
asset.save! && save!
@ -43,15 +47,15 @@ class RepositoryAssetValue < ApplicationRecord
def self.new_with_payload(payload, attributes)
value = new(attributes)
team = value.repository_cell.repository_column.repository.team
value.asset = Asset.create!(
created_by: value.created_by,
last_modified_by: value.created_by,
team: team
)
value.asset.file.attach(
io: StringIO.new(Base64.decode64(payload[:file_data].split(',')[1])),
filename: payload[:file_name]
)
value.asset = Asset.create!(created_by: value.created_by, last_modified_by: value.created_by, team: team)
if payload.is_a?(String) # assume it's a signed_id_token
value.asset.file.attach(payload)
elsif payload[:file_data]
value.asset.file.attach(io: StringIO.new(Base64.decode64(payload[:file_data])), filename: payload[:file_name])
end
value.asset.post_process_file(team)
value
end
end

View file

@ -1,6 +1,6 @@
# frozen_string_literal: true
class RepositoryCell < ActiveRecord::Base
class RepositoryCell < ApplicationRecord
attr_accessor :importing
belongs_to :repository_row
@ -14,12 +14,18 @@ class RepositoryCell < ActiveRecord::Base
.where(repository_cells: { value_type: 'RepositoryTextValue' })
end),
optional: true, foreign_key: :value_id
belongs_to :repository_number_value,
(lambda do
includes(:repository_cell)
.where(repository_cells: { value_type: 'RepositoryNumberValue' })
end),
optional: true, foreign_key: :value_id, inverse_of: :repository_cell
belongs_to :repository_date_value,
(lambda do
includes(:repository_cell)
.where(repository_cells: { value_type: 'RepositoryDateValue' })
.where(repository_cells: { value_type: 'RepositoryDateTimeValueBase' })
end),
optional: true, foreign_key: :value_id
optional: true, foreign_key: :value_id, inverse_of: :repository_cell
belongs_to :repository_list_value,
(lambda do
includes(:repository_cell)
@ -33,10 +39,59 @@ class RepositoryCell < ActiveRecord::Base
end),
optional: true, foreign_key: :value_id
validates_inclusion_of :repository_column,
in: (lambda do |cell|
cell.repository_row&.repository&.repository_columns || []
end)
belongs_to :repository_status_value,
(lambda do
includes(:repository_cell)
.where(repository_cells: { value_type: 'RepositoryStatusValue' })
end),
optional: true, foreign_key: :value_id, inverse_of: :repository_cell
belongs_to :repository_checklist_value,
(lambda do
includes(:repository_cell)
.where(repository_cells: { value_type: 'RepositoryChecklistValue' })
end),
optional: true, foreign_key: :value_id, inverse_of: :repository_cell
belongs_to :repository_date_time_value,
(lambda do
includes(:repository_cell)
.where(repository_cells: { value_type: 'RepositoryDateTimeValueBase' })
end),
optional: true, foreign_key: :value_id, inverse_of: :repository_cell
belongs_to :repository_time_value,
(lambda do
includes(:repository_cell)
.where(repository_cells: { value_type: 'RepositoryDateTimeValueBase' })
end),
optional: true, foreign_key: :value_id, inverse_of: :repository_cell
belongs_to :repository_date_time_range_value,
(lambda do
includes(:repository_cell)
.where(repository_cells: { value_type: 'RepositoryDateTimeRangeValueBase' })
end),
optional: true, foreign_key: :value_id, inverse_of: :repository_cell
belongs_to :repository_date_range_value,
(lambda do
includes(:repository_cell)
.where(repository_cells: { value_type: 'RepositoryDateTimeRangeValueBase' })
end),
optional: true, foreign_key: :value_id, inverse_of: :repository_cell
belongs_to :repository_time_range_value,
(lambda do
includes(:repository_cell)
.where(repository_cells: { value_type: 'RepositoryDateTimeRangeValueBase' })
end),
optional: true, foreign_key: :value_id, inverse_of: :repository_cell
validates :repository_column,
inclusion: { in: (lambda do |cell|
cell.repository_row&.repository&.repository_columns || []
end) }
validates :repository_column, presence: true
validate :repository_column_data_type
validates :repository_row,
@ -59,7 +114,7 @@ class RepositoryCell < ActiveRecord::Base
private
def repository_column_data_type
if !repository_column || value_type != repository_column.data_type
if !repository_column || value.class.name != repository_column.data_type
errors.add(:value_type, 'must match column data type')
end
end

View file

@ -0,0 +1,8 @@
# frozen_string_literal: true
class RepositoryCellValuesChecklistItem < ApplicationRecord
belongs_to :repository_checklist_item
belongs_to :repository_checklist_value
validates :repository_checklist_item, :repository_checklist_value, presence: true
end

View file

@ -0,0 +1,16 @@
# frozen_string_literal: true
class RepositoryChecklistItem < ApplicationRecord
validates :data, presence: true,
uniqueness: { scope: :repository_column_id, case_sensitive: false },
length: { minimum: Constants::NAME_MIN_LENGTH,
maximum: Constants::NAME_MAX_LENGTH }
belongs_to :repository, inverse_of: :repository_checklist_items
belongs_to :repository_column
belongs_to :created_by, foreign_key: 'created_by_id', class_name: 'User',
inverse_of: :created_repository_checklist_types
belongs_to :last_modified_by, foreign_key: 'last_modified_by_id', class_name: 'User',
inverse_of: :modified_repository_checklist_types
has_many :repository_cell_values_checklist_items, dependent: :destroy
has_many :repository_checklist_values, through: :repository_cell_values_checklist_items
end

View file

@ -0,0 +1,48 @@
# frozen_string_literal: true
class RepositoryChecklistValue < ApplicationRecord
belongs_to :created_by, foreign_key: 'created_by_id', class_name: 'User',
inverse_of: :created_repository_checklist_values
belongs_to :last_modified_by, foreign_key: 'last_modified_by_id', class_name: 'User',
inverse_of: :modified_repository_checklist_values
has_one :repository_cell, as: :value, dependent: :destroy, inverse_of: :value
has_many :repository_cell_values_checklist_items, dependent: :destroy
has_many :repository_checklist_items, through: :repository_cell_values_checklist_items
accepts_nested_attributes_for :repository_cell
SORTABLE_COLUMN_NAME = 'repository_checklist_items.data'
SORTABLE_VALUE_INCLUDE = { repository_checklist_value: :repository_checklist_items }.freeze
def formatted
repository_checklist_items.pluck(:data).join(' | ')
end
def data
repository_checklist_items.order(data: :asc).map { |i| { value: i.id, label: i.data } }
end
def data_changed?(new_data)
JSON.parse(new_data) != repository_checklist_items.pluck(:id)
end
def update_data!(new_data, user)
repository_cell_values_checklist_items.destroy_all
repository_cell.repository_column
.repository_checklist_items.where(id: JSON.parse(new_data)).find_each do |item|
repository_cell_values_checklist_items.create!(repository_checklist_item: item)
end
self.last_modified_by = user
save!
end
def self.new_with_payload(payload, attributes)
value = new(attributes)
value.repository_cell
.repository_column
.repository_checklist_items.where(id: JSON.parse(payload)).find_each do |item|
value.repository_cell_values_checklist_items.new(repository_checklist_item: item)
end
value
end
end

View file

@ -5,29 +5,41 @@ class RepositoryColumn < ApplicationRecord
belongs_to :created_by, foreign_key: :created_by_id, class_name: 'User'
has_many :repository_cells, dependent: :destroy
has_many :repository_rows, through: :repository_cells
has_many :repository_list_items, dependent: :destroy
has_many :repository_list_items, dependent: :destroy, index_errors: true
has_many :repository_status_items, dependent: :destroy, index_errors: true
has_many :repository_checklist_items, dependent: :destroy, index_errors: true
accepts_nested_attributes_for :repository_status_items, allow_destroy: true
accepts_nested_attributes_for :repository_list_items, allow_destroy: true
accepts_nested_attributes_for :repository_checklist_items, allow_destroy: true
enum data_type: Extends::REPOSITORY_DATA_TYPES
auto_strip_attributes :name, nullify: false
validates :name,
presence: true,
length: { maximum: Constants::NAME_MAX_LENGTH },
uniqueness: { scope: :repository_id, case_sensitive: true }
validates :created_by, presence: true
validates :repository, presence: true
validates :data_type, presence: true
validates :name, :data_type, :repository, :created_by, presence: true
after_create :update_repository_table_states_with_new_column
around_destroy :update_repository_table_states_with_removed_column
scope :list_type, -> { where(data_type: 'RepositoryListValue') }
scope :asset_type, -> { where(data_type: 'RepositoryAssetValue') }
scope :status_type, -> { where(data_type: 'RepositoryStatusValue') }
scope :checkbox_type, -> { where(data_type: 'RepositoryChecklistValue') }
def self.name_like(query)
where('repository_columns.name ILIKE ?', "%#{query}%")
end
# Add enum check method with underscores (eg repository_list_value)
data_types.each do |k, _|
define_method "#{k.underscore}?" do
public_send "#{k}?"
end
end
def update_repository_table_states_with_new_column
service = RepositoryTableStateColumnUpdateService.new
service.update_states_with_new_column(repository)
@ -37,12 +49,12 @@ class RepositoryColumn < ApplicationRecord
# Calculate old_column_index - this can only be done before
# record is deleted when we still have its index
old_column_index = (
Constants::REPOSITORY_TABLE_DEFAULT_STATE[:length] +
Constants::REPOSITORY_TABLE_DEFAULT_STATE['length'] +
repository.repository_columns
.order(id: :asc)
.pluck(:id)
.index(id)
).to_s
)
# Perform the destroy itself
yield

View file

@ -0,0 +1,23 @@
# frozen_string_literal: true
class RepositoryDateRangeValue < RepositoryDateTimeRangeValueBase
def data_changed?(new_data)
data = JSON.parse(new_data).symbolize_keys
st = Time.zone.parse(data[:start_time])
et = Time.zone.parse(data[:end_time])
st.to_date != start_time.to_date || et.to_date != end_time.to_date
end
def formatted
super(:full_date)
end
def self.new_with_payload(payload, attributes)
data = JSON.parse(payload).symbolize_keys
value = new(attributes)
value.start_time = Time.zone.parse(data[:start_time])
value.end_time = Time.zone.parse(data[:end_time])
value
end
end

View file

@ -0,0 +1,23 @@
# frozen_string_literal: true
class RepositoryDateTimeRangeValue < RepositoryDateTimeRangeValueBase
def data_changed?(new_data)
data = JSON.parse(new_data).symbolize_keys
st = Time.zone.parse(data[:start_time])
et = Time.zone.parse(data[:end_time])
st.to_i != start_time.to_i || et.to_i != end_time.to_i
end
def formatted
super(:full_with_comma)
end
def self.new_with_payload(payload, attributes)
data = JSON.parse(payload).symbolize_keys
value = new(attributes)
value.start_time = Time.zone.parse(data[:start_time])
value.end_time = Time.zone.parse(data[:end_time])
value
end
end

View file

@ -0,0 +1,38 @@
# frozen_string_literal: true
class RepositoryDateTimeRangeValueBase < ApplicationRecord
self.table_name = 'repository_date_time_range_values'
belongs_to :created_by, foreign_key: :created_by_id, class_name: 'User', optional: true,
inverse_of: :created_repository_date_time_values
belongs_to :last_modified_by, foreign_key: :last_modified_by_id, class_name: 'User', optional: true,
inverse_of: :modified_repository_date_time_values
has_one :repository_cell, as: :value, dependent: :destroy, inverse_of: :repository_date_time_value
accepts_nested_attributes_for :repository_cell
validates :repository_cell, :start_time, :end_time, presence: true
SORTABLE_COLUMN_NAME = 'repository_date_time_values.start_time'
SORTABLE_VALUE_INCLUDE = :repository_date_time_range_value
def update_data!(new_data, user)
data = JSON.parse(new_data).symbolize_keys
self.start_time = Time.zone.parse(data[:start_time])
self.end_time = Time.zone.parse(data[:end_time])
self.last_modified_by = user
save!
end
def data
[start_time, end_time].compact.join(' - ')
end
private
def formatted(format)
[
I18n.l(start_time, format: format),
I18n.l(end_time, format: format)
].compact.join(' - ')
end
end

View file

@ -0,0 +1,18 @@
# frozen_string_literal: true
class RepositoryDateTimeValue < RepositoryDateTimeValueBase
def data_changed?(new_data)
new_time = Time.zone.parse(new_data)
new_time.to_i != data.to_i
end
def formatted
super(:full_with_comma)
end
def self.new_with_payload(payload, attributes)
value = new(attributes)
value.data = Time.zone.parse(payload)
value
end
end

View file

@ -0,0 +1,29 @@
# frozen_string_literal: true
class RepositoryDateTimeValueBase < ApplicationRecord
self.table_name = 'repository_date_time_values'
belongs_to :created_by, foreign_key: :created_by_id, class_name: 'User', optional: true,
inverse_of: :created_repository_date_time_values
belongs_to :last_modified_by, foreign_key: :last_modified_by_id, class_name: 'User', optional: true,
inverse_of: :modified_repository_date_time_values
has_one :repository_cell, as: :value, dependent: :destroy, inverse_of: :repository_date_time_value
accepts_nested_attributes_for :repository_cell
validates :repository_cell, :data, presence: true
SORTABLE_COLUMN_NAME = 'repository_date_time_values.data'
SORTABLE_VALUE_INCLUDE = :repository_date_time_value
def update_data!(new_data, user)
self.data = Time.zone.parse(new_data)
self.last_modified_by = user
save!
end
private
def formatted(format)
I18n.l(data, format: format)
end
end

View file

@ -1,34 +1,18 @@
# frozen_string_literal: true
class RepositoryDateValue < ApplicationRecord
belongs_to :created_by, foreign_key: :created_by_id, class_name: 'User'
belongs_to :last_modified_by, foreign_key: :last_modified_by_id, class_name: 'User'
has_one :repository_cell, as: :value, dependent: :destroy
accepts_nested_attributes_for :repository_cell
validates :repository_cell, presence: true
validates :data, presence: true
SORTABLE_COLUMN_NAME = 'repository_date_values.data'
SORTABLE_VALUE_INCLUDE = :repository_date_value
class RepositoryDateValue < RepositoryDateTimeValueBase
def data_changed?(new_data)
new_time = Time.zone.parse(new_data)
new_time.to_date != data.to_date
end
def formatted
data
end
def data_changed?(new_data)
new_data != data
end
def update_data!(new_data, user)
self.data = new_data
self.last_modified_by = user
save!
super(:full_date)
end
def self.new_with_payload(payload, attributes)
value = new(attributes)
value.data = payload
value.data = Time.zone.parse(payload)
value
end
end

View file

@ -0,0 +1,35 @@
# frozen_string_literal: true
class RepositoryNumberValue < ApplicationRecord
belongs_to :created_by, foreign_key: :created_by_id, class_name: 'User',
inverse_of: :created_repository_number_values
belongs_to :last_modified_by, foreign_key: :last_modified_by_id, class_name: 'User',
inverse_of: :modified_repository_number_values
has_one :repository_cell, as: :value, dependent: :destroy, inverse_of: :value
accepts_nested_attributes_for :repository_cell
validates :repository_cell, :data, presence: true
SORTABLE_COLUMN_NAME = 'repository_number_values.data'
SORTABLE_VALUE_INCLUDE = :repository_number_value
def formatted
data.to_s
end
def data_changed?(new_data)
new_data.to_f != data
end
def update_data!(new_data, user)
self.data = new_data.to_f
self.last_modified_by = user
save!
end
def self.new_with_payload(payload, attributes)
value = new(attributes)
value.data = payload.to_f
value
end
end

View file

@ -0,0 +1,14 @@
# frozen_string_literal: true
class RepositoryStatusItem < ApplicationRecord
validates :repository, :repository_column, :icon, presence: true
validates :status, presence: true, length: { minimum: Constants::NAME_MIN_LENGTH,
maximum: Constants::NAME_MAX_LENGTH }
belongs_to :repository
belongs_to :repository_column
belongs_to :created_by, foreign_key: 'created_by_id', class_name: 'User', optional: true,
inverse_of: :created_repository_status_types
belongs_to :last_modified_by, foreign_key: 'last_modified_by_id', class_name: 'User', optional: true,
inverse_of: :modified_repository_status_types
has_many :repository_status_values, inverse_of: :repository_status_item, dependent: :delete_all
end

View file

@ -0,0 +1,45 @@
# frozen_string_literal: true
class RepositoryStatusValue < ApplicationRecord
belongs_to :repository_status_item
belongs_to :created_by, foreign_key: 'created_by_id', class_name: 'User', optional: true,
inverse_of: :created_repository_status_value
belongs_to :last_modified_by, foreign_key: 'last_modified_by_id', class_name: 'User', optional: true,
inverse_of: :modified_repository_status_value
has_one :repository_cell, as: :value, dependent: :destroy, inverse_of: :value
accepts_nested_attributes_for :repository_cell
validates :repository_status_item, presence: true
SORTABLE_COLUMN_NAME = 'repository_status_items.status'
SORTABLE_VALUE_INCLUDE = { repository_status_value: :repository_status_item }.freeze
def formatted
data
end
def data_changed?(new_data)
new_data.to_i != repository_status_item_id
end
def update_data!(new_data, user)
self.repository_status_item_id = new_data.to_i
self.last_modified_by = user
save!
end
def data
return nil unless repository_status_item
"#{repository_status_item.icon} #{repository_status_item.status}"
end
def self.new_with_payload(payload, attributes)
value = new(attributes)
value.repository_status_item = value.repository_cell
.repository_column
.repository_status_items
.find(payload)
value
end
end

View file

@ -0,0 +1,24 @@
# frozen_string_literal: true
class RepositoryTimeRangeValue < RepositoryDateTimeRangeValueBase
def data_changed?(new_data)
data = JSON.parse(new_data).symbolize_keys
st = Time.zone.parse(data[:start_time])
et = Time.zone.parse(data[:end_time])
st.hour != start_time.hour || et.hour != end_time.hour || st.min != start_time.min || et.min != end_time.min
end
def formatted
super(:time)
end
def self.new_with_payload(payload, attributes)
data = JSON.parse(payload).symbolize_keys
value = new(attributes)
value.start_time = Time.zone.parse(data[:start_time])
value.end_time = Time.zone.parse(data[:end_time])
value
end
end

View file

@ -0,0 +1,18 @@
# frozen_string_literal: true
class RepositoryTimeValue < RepositoryDateTimeValueBase
def data_changed?(new_data)
new_time = Time.zone.parse(new_data)
new_time.min != data.min || new_time.hour != data.hour
end
def formatted
super(:time)
end
def self.new_with_payload(payload, attributes)
value = new(attributes)
value.data = Time.zone.parse(payload)
value
end
end

View file

@ -214,6 +214,66 @@ class User < ApplicationRecord
has_many :assigned_my_module_repository_rows,
class_name: 'MyModuleRepositoryRow',
foreign_key: 'assigned_by_id'
has_many :created_repository_status_types,
class_name: 'RepositoryStatusItem',
foreign_key: 'created_by_id',
inverse_of: :created_by,
dependent: :nullify
has_many :modified_repository_status_types,
class_name: 'RepositoryStatusItem',
foreign_key: 'last_modified_by_id',
inverse_of: :last_modified_by,
dependent: :nullify
has_many :created_repository_status_value,
class_name: 'RepositoryStatusValue',
foreign_key: 'created_by_id',
inverse_of: :created_by,
dependent: :nullify
has_many :modified_repository_status_value,
class_name: 'RepositoryStatusValue',
foreign_key: 'last_modified_by_id',
inverse_of: :last_modified_by,
dependent: :nullify
has_many :created_repository_date_time_values,
class_name: 'RepositoryDateTimeValue',
foreign_key: 'created_by_id',
inverse_of: :created_by,
dependent: :nullify
has_many :modified_repository_date_time_values,
class_name: 'RepositoryDateTimeValue',
foreign_key: 'last_modified_by_id',
inverse_of: :last_modified_by,
dependent: :nullify
has_many :created_repository_checklist_values,
class_name: 'RepositoryChecklistValue',
foreign_key: 'created_by_id',
inverse_of: :created_by,
dependent: :nullify
has_many :modified_repository_checklist_values,
class_name: 'RepositoryChecklistValue',
foreign_key: 'last_modified_by_id',
inverse_of: :last_modified_by,
dependent: :nullify
has_many :created_repository_checklist_types,
class_name: 'RepositoryChecklistItem',
foreign_key: 'created_by_id',
inverse_of: :created_by,
dependent: :nullify
has_many :modified_repository_checklist_types,
class_name: 'RepositoryChecklistItem',
foreign_key: 'last_modified_by_id',
inverse_of: :last_modified_by,
dependent: :nullify
has_many :created_repository_number_values,
class_name: 'RepositoryNumberValue',
foreign_key: 'created_by_id',
inverse_of: :created_by,
dependent: :nullify
has_many :modified_repository_number_values,
class_name: 'RepositoryNumberValue',
foreign_key: 'last_modified_by_id',
inverse_of: :last_modified_by,
dependent: :nullify
has_many :user_notifications, inverse_of: :user
has_many :notifications, through: :user_notifications

View file

@ -13,6 +13,14 @@ module Api
object.data_type == 'RepositoryListValue' &&
!instance_options[:hide_list_items]
end)
has_many :repository_status_items,
key: :repository_status_items,
serializer: InventoryStatusItemSerializer,
class_name: 'RepositoryStatusItem',
if: (lambda do
object.data_type == 'RepositoryStatusValue' &&
!instance_options[:hide_list_items]
end)
def data_type
Extends::API_REPOSITORY_DATA_TYPE_MAPPINGS[object.data_type]

View file

@ -0,0 +1,10 @@
# frozen_string_literal: true
module Api
module V1
class InventoryStatusItemSerializer < ActiveModel::Serializer
type :inventory_status_items
attributes :status, :icon
end
end
end

View file

@ -0,0 +1,27 @@
# frozen_string_literal: true
class RepositoryColumnSerializer < ActiveModel::Serializer
include Rails.application.routes.url_helpers
attributes :id, :name, :message, :edit_html_url, :update_url, :destroy_html_url
def message
if instance_options[:creating]
I18n.t('libraries.repository_columns.create.success_flash', name: object.name)
elsif instance_options[:editing]
I18n.t('libraries.repository_columns.update.success_flash', name: object.name)
end
end
def edit_html_url
edit_repository_repository_column_path(object.repository, object)
end
def update_url
repository_repository_columns_text_column_path(object.repository, object)
end
def destroy_html_url
repository_columns_destroy_html_path(object.repository, object)
end
end

View file

@ -0,0 +1,18 @@
# frozen_string_literal: true
module RepositoryDatatable
class RepositoryAssetValueSerializer < RepositoryBaseValueSerializer
include Rails.application.routes.url_helpers
def value
asset = object.asset
{
id: asset.id,
url: rails_blob_path(asset.file, disposition: 'attachment'),
preview_url: asset_file_preview_path(asset),
file_name: asset.file_name,
icon_html: FileIconsHelper.file_extension_icon_html(asset)
}
end
end
end

View file

@ -0,0 +1,15 @@
# frozen_string_literal: true
module RepositoryDatatable
class RepositoryBaseValueSerializer < ActiveModel::Serializer
attributes :value, :value_type
def value
raise NotImplementedError
end
def value_type
object.class.name
end
end
end

View file

@ -0,0 +1,9 @@
# frozen_string_literal: true
module RepositoryDatatable
class RepositoryChecklistValueSerializer < RepositoryBaseValueSerializer
def value
object.data
end
end
end

View file

@ -0,0 +1,18 @@
# frozen_string_literal: true
module RepositoryDatatable
class RepositoryDateRangeValueSerializer < RepositoryBaseValueSerializer
def value
{
start_time: {
formatted: I18n.l(object.start_time, format: :full_date),
datetime: object.start_time.strftime('%Y/%m/%d %H:%M')
},
end_time: {
formatted: I18n.l(object.end_time, format: :full_date),
datetime: object.end_time.strftime('%Y/%m/%d %H:%M')
}
}
end
end
end

View file

@ -0,0 +1,22 @@
# frozen_string_literal: true
module RepositoryDatatable
class RepositoryDateTimeRangeValueSerializer < RepositoryBaseValueSerializer
def value
{
start_time: {
formatted: I18n.l(object.start_time, format: :full_with_comma),
date_formatted: I18n.l(object.start_time, format: :full_date),
time_formatted: I18n.l(object.start_time, format: :time),
datetime: object.start_time.strftime('%Y/%m/%d %H:%M')
},
end_time: {
formatted: I18n.l(object.end_time, format: :full_with_comma),
date_formatted: I18n.l(object.end_time, format: :full_date),
time_formatted: I18n.l(object.end_time, format: :time),
datetime: object.end_time.strftime('%Y/%m/%d %H:%M')
}
}
end
end
end

View file

@ -0,0 +1,14 @@
# frozen_string_literal: true
module RepositoryDatatable
class RepositoryDateTimeValueSerializer < RepositoryBaseValueSerializer
def value
{
formatted: I18n.l(object.data, format: :full_with_comma),
date_formatted: I18n.l(object.data, format: :full_date),
time_formatted: I18n.l(object.data, format: :time),
datetime: object.data.strftime('%Y/%m/%d %H:%M')
}
end
end
end

View file

@ -0,0 +1,12 @@
# frozen_string_literal: true
module RepositoryDatatable
class RepositoryDateValueSerializer < RepositoryBaseValueSerializer
def value
{
formatted: I18n.l(object.data, format: :full_date),
datetime: object.data.strftime('%Y/%m/%d %H:%M')
}
end
end
end

View file

@ -0,0 +1,12 @@
# frozen_string_literal: true
module RepositoryDatatable
class RepositoryListValueSerializer < RepositoryBaseValueSerializer
def value
{
id: object.repository_list_item.id,
text: object.data
}
end
end
end

View file

@ -0,0 +1,18 @@
# frozen_string_literal: true
module RepositoryDatatable
class RepositoryNumberValueSerializer < RepositoryBaseValueSerializer
attributes :value_decimals
def value
object.data
end
def value_decimals
object.repository_cell
.repository_column
.metadata
.fetch('decimals') { Constants::REPOSITORY_NUMBER_TYPE_DEFAULT_DECIMALS }
end
end
end

View file

@ -0,0 +1,13 @@
# frozen_string_literal: true
module RepositoryDatatable
class RepositoryStatusValueSerializer < RepositoryBaseValueSerializer
def value
{
id: object.repository_status_item.id,
icon: object.repository_status_item.icon,
status: object.repository_status_item.status
}
end
end
end

View file

@ -0,0 +1,9 @@
# frozen_string_literal: true
module RepositoryDatatable
class RepositoryTextValueSerializer < RepositoryBaseValueSerializer
def value
object.data
end
end
end

View file

@ -0,0 +1,18 @@
# frozen_string_literal: true
module RepositoryDatatable
class RepositoryTimeRangeValueSerializer < RepositoryBaseValueSerializer
def value
{
start_time: {
formatted: I18n.l(object.start_time, format: :time),
datetime: object.start_time.strftime('%Y/%m/%d %H:%M')
},
end_time: {
formatted: I18n.l(object.end_time, format: :time),
datetime: object.end_time.strftime('%Y/%m/%d %H:%M')
}
}
end
end
end

View file

@ -0,0 +1,12 @@
# frozen_string_literal: true
module RepositoryDatatable
class RepositoryTimeValueSerializer < RepositoryBaseValueSerializer
def value
{
formatted: I18n.l(object.data, format: :time),
datetime: object.data.strftime('%Y/%m/%d %H:%M')
}
end
end
end

View file

@ -140,14 +140,17 @@ module ModelExporters
{
repository_cell: cell,
repository_value: cell.value,
repository_value_asset: get_cell_value_asset(cell)
repository_value_asset: get_cell_value_asset(cell),
repository_value_checklist: get_cell_value_checklist(cell)
}
end
def repository_column(column)
{
repository_column: column,
repository_list_items: column.repository_list_items
repository_list_items: column.repository_list_items,
repository_checklist_items: column.repository_checklist_items,
repository_status_items: column.repository_status_items
}
end
@ -167,5 +170,11 @@ module ModelExporters
asset_blob: cell.value.asset.blob
}
end
def get_cell_value_checklist(cell)
return unless cell.value_type == 'RepositoryChecklistValue'
cell.value.repository_cell_values_checklist_items
end
end
end

View file

@ -9,7 +9,6 @@ class Reports::Docx
include InputSanitizeHelper
include TeamsHelper
include GlobalActivitiesHelper
include RepositoryDatatableHelper
Dir[File.join(File.dirname(__FILE__), 'docx') + '**/*.rb'].each do |file|
include_module = File.basename(file).gsub('.rb', '').split('_').map(&:capitalize).join
@ -35,7 +34,6 @@ class Reports::Docx
@docx
end
def self.link_prepare(scinote_url, link)
link[0] == '/' ? scinote_url + link : link
end

View file

@ -2,45 +2,15 @@
module Reports::Docx::DrawMyModuleRepository
def draw_my_module_repository(subject)
my_module = MyModule.find_by_id(subject['id']['my_module_id'])
my_module = MyModule.find_by(id: subject['id']['my_module_id'])
return unless my_module
repository_data = my_module.repository_json(subject['id']['repository_id'], subject['sort_order'], @user)
repository_id = subject['id']['repository_id']
repository_data = my_module.repository_json(repository_id, subject['sort_order'], @user)
return false unless repository_data[:data].assigned_rows.count.positive?
records = repository_data[:data]
assigned_rows = records.assigned_rows
columns_mappings = records.mappings
repository = ::Repository.find_by_id(subject['id']['repository_id'])
repository_rows = records.repository_rows
.preload(
:repository_columns,
:created_by,
repository_cells: :value
)
data = prepare_row_columns(repository_rows,
repository,
columns_mappings,
repository.team,
assigned_rows)
data.map! do |row|
row.select do |key, _value|
true if Float(key.to_s) > 1
rescue StandardError
false
end
end
table = []
data.each do |row|
new_row = Array.new(repository_data[:headers].length)
row.each do |key, value|
new_row[(key.to_s.to_i - 2)] = Sanitize.clean(value)
end
table.push(new_row)
end
table.unshift(repository_data[:headers])
repository = ::Repository.find(repository_id)
table = prepare_row_columns(repository_data)
@docx.p
@docx.p I18n.t('projects.reports.elements.module_repository.name',

View file

@ -0,0 +1,31 @@
# frozen_string_literal: true
module Reports::Docx::RepositoryHelper
include InputSanitizeHelper
def prepare_row_columns(repository_data)
result = [repository_data[:headers]]
repository_data[:data].repository_rows.each do |record|
row = []
row.push(record.id)
row.push(escape_input(record.name))
row.push(I18n.l(record.created_at, format: :full))
row.push(escape_input(record.created_by.full_name))
cell_values = {}
custom_cells = record.repository_cells
custom_cells.each do |cell|
cell_values[cell.repository_column_id] = cell.value.formatted
end
repository_data[:data].mappings.each do |column_id, _position|
value = cell_values[column_id]
row.push(value)
end
result.push(row)
end
result
end
end

View file

@ -57,7 +57,7 @@ module RepositoryActions
def duplicate_repository_date_value
old_value = @cell.value
RepositoryDateValue.create(
RepositoryDateTimeValue.create(
old_value.attributes.merge(
id: nil, created_by: @user, last_modified_by: @user,
repository_cell_attributes: {

View file

@ -0,0 +1,53 @@
# frozen_string_literal: true
module RepositoryColumns
class ColumnService
extend Service
attr_reader :errors, :column
def initialize(user:, repository:, column_name:, team:)
@user = user
@repository = repository
@column_name = column_name
@team = team
@errors = {}
@column = nil
end
def call
raise NotImplementedError
end
def succeed?
@errors.none?
end
private
def valid?
unless @user && @repository
@errors[:invalid_arguments] =
{ 'user': @user,
'repository': @repository }
.map do |key, value|
"Can't find #{key.capitalize}" if value.nil?
end.compact
end
succeed?
end
def log_activity(type)
Activities::CreateActivityService
.call(activity_type: type,
owner: @user,
subject: @repository,
team: @team,
message_items: {
repository_column: @column.id,
repository: @repository.id
})
end
end
end

View file

@ -0,0 +1,43 @@
# frozen_string_literal: true
module RepositoryColumns
class CreateColumnService < RepositoryColumns::ColumnService
def initialize(user:, repository:, params:, team:, column_type:)
super(user: user, repository: repository, team: team, column_name: params[:name])
@column_type = column_type
@params = params
end
def call
return self unless valid?
@column = RepositoryColumn.new(column_attributes)
if @column.save
log_activity(:create_column_inventory)
else
errors[:repository_column] = @column.errors.messages
end
self
end
private
def column_attributes
@params[:repository_status_items_attributes]&.map do |m|
m.merge!(repository_id: @repository.id, created_by_id: @user.id, last_modified_by_id: @user.id)
end
@params[:repository_list_items_attributes]&.map do |m|
m.merge!(repository_id: @repository.id, created_by_id: @user.id, last_modified_by_id: @user.id)
end
@params[:repository_checklist_items_attributes]&.map do |m|
m.merge!(repository_id: @repository.id, created_by_id: @user.id, last_modified_by_id: @user.id)
end
@params.merge(repository_id: @repository.id, created_by_id: @user.id, data_type: @column_type)
end
end
end

Some files were not shown because too many files have changed in this diff Show more