mirror of
https://github.com/scinote-eln/scinote-web.git
synced 2024-09-20 06:35:56 +08:00
Merge branch 'develop' into features/new_permissions
This commit is contained in:
commit
7e6ca3be8a
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -35,6 +35,9 @@ ehthumbs.db
|
|||
# Ignore temporary files
|
||||
public/system/*
|
||||
|
||||
# Ignore BioEddie installion in public folder
|
||||
public/bio_eddie/
|
||||
|
||||
# Ignore ActiveStorage Disc service storage directory
|
||||
storage/
|
||||
|
||||
|
|
|
@ -6,6 +6,7 @@ AllCops:
|
|||
Exclude:
|
||||
- "vendor/**/*"
|
||||
- "db/schema.rb"
|
||||
- "spec/**/*"
|
||||
NewCops: enable
|
||||
UseCache: false
|
||||
TargetRubyVersion: 2.6
|
||||
|
@ -351,7 +352,7 @@ Metrics/AbcSize:
|
|||
Enabled: false
|
||||
|
||||
Metrics/BlockLength:
|
||||
ExcludedMethods: ['describe', 'context']
|
||||
IgnoredMethods: ['describe', 'context']
|
||||
|
||||
Metrics/ClassLength:
|
||||
Enabled: false
|
||||
|
|
3
app/assets/images/chemaxon-logo.svg
Normal file
3
app/assets/images/chemaxon-logo.svg
Normal file
|
@ -0,0 +1,3 @@
|
|||
<svg width="10" height="14" viewBox="0 0 10 14" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M7.40002 7.38092L6.29526 8.48568C6.37145 8.59996 6.44764 8.75235 6.48573 8.90473H5.68573C5.57145 8.67616 5.30478 8.52377 5.03811 8.52377C4.61906 8.52377 4.27621 8.86663 4.27621 9.28568C4.27621 9.70473 4.61906 10.0476 5.03811 10.0476C5.30478 10.0476 5.57145 9.8952 5.68573 9.66663H6.48573C6.33335 10.3142 5.72383 10.8095 5.00002 10.8095C4.69526 10.8095 4.39049 10.6952 4.16192 10.5428L3.62859 11.0762C4.00954 11.3809 4.50478 11.5714 5.03811 11.5714C5.30478 11.5714 5.53335 11.5333 5.76192 11.4571L6.33335 12.0285C5.9524 12.219 5.49526 12.3333 5.03811 12.3333C4.08573 12.3333 3.20954 11.8762 2.63811 11.1904L3.74287 10.0857C3.5524 9.85711 3.47621 9.59044 3.47621 9.28568C3.47621 8.44758 4.16192 7.76187 5.00002 7.76187C5.30478 7.76187 5.60954 7.87616 5.83811 8.02854L6.37145 7.4952C5.99049 7.19044 5.49525 6.99996 4.96192 6.99996C4.69526 6.99996 4.46668 7.03806 4.23811 7.11425L3.66668 6.54282C4.04764 6.35235 4.50478 6.23806 4.96192 6.23806C5.9524 6.23806 6.82859 6.6952 7.40002 7.38092ZM6.25716 4.90473V0.904727H7.01906V0.142822H5.49525V5.51425C7.36192 5.78092 8.80954 7.34282 8.80954 9.28568C8.80954 10.2381 8.46668 11.1143 7.89526 11.7619L7.36192 11.2285C7.78097 10.6952 8.04764 10.0095 8.04764 9.28568C8.04764 8.82854 7.93335 8.37139 7.74287 7.99044L7.17145 8.56187C7.24764 8.79044 7.28573 9.01901 7.28573 9.28568C7.28573 10.0476 6.90478 10.6952 6.37145 11.1143L7.47621 12.219C6.82859 12.7904 5.9524 13.0952 5.03811 13.0952C2.90478 13.0952 1.19049 11.3809 1.19049 9.28568C1.19049 8.3333 1.53335 7.45711 2.10478 6.80949L2.63811 7.34282C2.21907 7.87615 1.9524 8.56187 1.9524 9.28568C1.9524 9.74282 2.06668 10.2 2.25716 10.5809L2.82859 10.0095C2.7524 9.78092 2.7143 9.55235 2.7143 9.28568C2.7143 8.52377 3.09526 7.87616 3.62859 7.45711L2.52383 6.35235C3.05716 5.8952 3.74287 5.59044 4.46668 5.51425V0.142822H2.94287V0.904727H3.70478V4.90473C1.83811 5.43806 0.428589 7.19044 0.428589 9.28568C0.428589 11.8 2.48573 13.8571 5.00002 13.8571C7.5143 13.8571 9.57145 11.8 9.57145 9.28568C9.57145 7.19044 8.16192 5.43806 6.25716 4.90473Z" fill="#404048"/>
|
||||
</svg>
|
After Width: | Height: | Size: 2.1 KiB |
BIN
app/assets/images/icon_small/bio_eddie.png
Normal file
BIN
app/assets/images/icon_small/bio_eddie.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 986 B |
BIN
app/assets/images/icon_small/bio_eddie_white.png
Normal file
BIN
app/assets/images/icon_small/bio_eddie_white.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.3 KiB |
BIN
app/assets/images/printers/no_available_printers.png
Normal file
BIN
app/assets/images/printers/no_available_printers.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 16 KiB |
|
@ -40,6 +40,8 @@
|
|||
//= require users/settings/teams/invite_users_modal
|
||||
//= require repository_columns/index
|
||||
//= require perfect-scrollbar.min
|
||||
//= require shared/inline_editing
|
||||
//= require shared/barcode_search
|
||||
//= require activestorage
|
||||
//= require global_activities/side_pane
|
||||
//= require protocols/header
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
/* global animateSpinner globalActivities */
|
||||
/* global animateSpinner globalActivities HelperModule */
|
||||
|
||||
'use strict';
|
||||
|
||||
|
@ -59,6 +59,37 @@
|
|||
});
|
||||
}
|
||||
|
||||
function validateActivityFilterName() {
|
||||
let filterName = $('#saveFilterModal .activity-filter-name-input').val();
|
||||
$('#saveFilterModal .btn-confirm').prop('disabled', filterName.length === 0);
|
||||
}
|
||||
|
||||
$('#saveFilterModal')
|
||||
.on('keyup', '.activity-filter-name-input', function() {
|
||||
validateActivityFilterName();
|
||||
})
|
||||
.on('click', '.btn-confirm', function() {
|
||||
$.ajax({
|
||||
url: this.dataset.saveFilterUrl,
|
||||
type: 'POST',
|
||||
global: false,
|
||||
dataType: 'json',
|
||||
data: {
|
||||
name: $('#saveFilterModal .activity-filter-name-input').val(),
|
||||
filter: globalActivities.getFilters()
|
||||
},
|
||||
success: function(data) {
|
||||
HelperModule.flashAlertMsg(data.message, 'success');
|
||||
$('#saveFilterModal .activity-filter-name-input').val('');
|
||||
validateActivityFilterName();
|
||||
$('#saveFilterModal').modal('hide');
|
||||
},
|
||||
error: function(response) {
|
||||
HelperModule.flashAlertMsg(response.responseJSON.errors.join(','), 'danger');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
initExpandCollapseAllButtons();
|
||||
initShowMoreButton();
|
||||
}());
|
||||
|
|
16
app/assets/javascripts/label_printers/index.js
Normal file
16
app/assets/javascripts/label_printers/index.js
Normal file
|
@ -0,0 +1,16 @@
|
|||
(function() {
|
||||
$('.api-key-input').on('keyup change', function() {
|
||||
var initialValue = this.dataset.originalValue;
|
||||
if (initialValue.length) {
|
||||
if (initialValue !== this.value) {
|
||||
$('.api-key-container').addClass('warning');
|
||||
$('.save-button').removeClass('hidden');
|
||||
$('.saved-button').addClass('hidden');
|
||||
} else {
|
||||
$('.api-key-container').removeClass('warning');
|
||||
$('.save-button').addClass('hidden');
|
||||
$('.saved-button').removeClass('hidden');
|
||||
}
|
||||
}
|
||||
});
|
||||
}());
|
|
@ -172,6 +172,7 @@ var MyModuleRepositories = (function() {
|
|||
var dataTableWrapper = $(tableContainer).closest('.dataTables_wrapper');
|
||||
DataTableHelpers.initLengthApearance(dataTableWrapper);
|
||||
DataTableHelpers.initSearchField(dataTableWrapper, I18n.t('repositories.show.filter_inventory_items'));
|
||||
$('<img class="barcode-scanner" src="/images/icon_small/barcode.png"></img>').appendTo($('.search-container'));
|
||||
dataTableWrapper.find('.main-actions, .pagination-row').removeClass('hidden');
|
||||
if (options.assign_mode) {
|
||||
renderFullViewAssignButtons();
|
||||
|
|
|
@ -65,6 +65,7 @@ var RepositoryDatatable = (function(global) {
|
|||
$('#restoreRepositoryRecords').prop('disabled', true);
|
||||
$('#deleteRepositoryRecords').prop('disabled', true);
|
||||
$('#editDeleteCopy').hide();
|
||||
$('#toolbarPrintLabel').hide();
|
||||
} else {
|
||||
if (rowsSelected.length === 1) {
|
||||
$('#editRepositoryRecord').prop('disabled', false);
|
||||
|
@ -82,6 +83,7 @@ var RepositoryDatatable = (function(global) {
|
|||
$('#archiveRepositoryRecordsButton').prop('disabled', true);
|
||||
}
|
||||
$('#editDeleteCopy').show();
|
||||
$('#toolbarPrintLabel').show();
|
||||
}
|
||||
} else if (currentMode === 'editMode') {
|
||||
$(TABLE_WRAPPER_ID).addClass('editing');
|
||||
|
@ -99,7 +101,10 @@ var RepositoryDatatable = (function(global) {
|
|||
$('th').addClass('disable-click');
|
||||
$('.repository-row-selector').prop('disabled', true);
|
||||
$('.dataTables_filter input').prop('disabled', true);
|
||||
$('#toolbarPrintLabel').hide();
|
||||
}
|
||||
|
||||
$('#toolbarPrintLabel').data('rows', JSON.stringify(rowsSelected));
|
||||
}
|
||||
|
||||
function clearRowSelection() {
|
||||
|
@ -559,6 +564,8 @@ var RepositoryDatatable = (function(global) {
|
|||
DataTableHelpers.initLengthApearance($(TABLE_ID).closest('.dataTables_wrapper'));
|
||||
DataTableHelpers.initSearchField($(TABLE_ID).closest('.dataTables_wrapper'), I18n.t('repositories.show.filter_inventory_items'));
|
||||
|
||||
$('<img class="barcode-scanner" src="/images/icon_small/barcode.png"></img>').appendTo($('.search-container'));
|
||||
|
||||
if ($('.repository-show').length) {
|
||||
$('.dataTables_scrollBody, .dataTables_scrollHead').css('overflow', '');
|
||||
}
|
||||
|
|
21
app/assets/javascripts/shared/barcode_search.js
Normal file
21
app/assets/javascripts/shared/barcode_search.js
Normal file
|
@ -0,0 +1,21 @@
|
|||
$(document).on('click', '.barcode-scanner', function() {
|
||||
var search = $('.search-container .search-field');
|
||||
var input = $('<input>').attr('type', 'text').css({ position: 'absolute', right: 0, opacity: 0 })
|
||||
.appendTo($('.search-container').parent());
|
||||
search.val('');
|
||||
search.attr('disabled', true).addClass('barcode-mode');
|
||||
|
||||
input.focus();
|
||||
input.one('change', function() {
|
||||
search.val($(this).val());
|
||||
search.trigger('keyup');
|
||||
$(document).click();
|
||||
});
|
||||
|
||||
setTimeout(function() {
|
||||
$(document).one('click', function() {
|
||||
search.attr('disabled', false).removeClass('barcode-mode');
|
||||
input.remove();
|
||||
});
|
||||
});
|
||||
});
|
159
app/assets/javascripts/sitewide/bio_eddie.js
Normal file
159
app/assets/javascripts/sitewide/bio_eddie.js
Normal file
|
@ -0,0 +1,159 @@
|
|||
/* global HelperModule I18n */
|
||||
var bioEddieEditor = (function() {
|
||||
var BIO_EDDIE;
|
||||
var CHEMAXON;
|
||||
var bioEddieIframe;
|
||||
var bioEddieModal;
|
||||
|
||||
function importMolecule() {
|
||||
var monomerModel = BIO_EDDIE.getMonomerModel();
|
||||
var monomerImporter = new CHEMAXON.HelmImportModule();
|
||||
var molecule = bioEddieModal.data('molecule');
|
||||
if (molecule) {
|
||||
monomerImporter.import(molecule, monomerModel)
|
||||
.then(builder => BIO_EDDIE.setModel(builder.graphStoreData));
|
||||
}
|
||||
}
|
||||
|
||||
function loadBioEddie() {
|
||||
BIO_EDDIE = bioEddieIframe.contentWindow.bioEddieEditor;
|
||||
CHEMAXON = bioEddieIframe.contentWindow.chemaxon;
|
||||
|
||||
if (typeof BIO_EDDIE === 'undefined' || typeof CHEMAXON === 'undefined') {
|
||||
setTimeout(function() {
|
||||
loadBioEddie();
|
||||
}, 2000);
|
||||
} else {
|
||||
importMolecule();
|
||||
}
|
||||
}
|
||||
|
||||
function initIframe() {
|
||||
bioEddieIframe.src = bioEddieIframe.dataset.src;
|
||||
loadBioEddie();
|
||||
}
|
||||
|
||||
function saveMolecule(svg, structure, scheduleForRegistration) {
|
||||
var moleculeName = bioEddieModal.find('.file-name input').val();
|
||||
$.post(bioEddieModal.data('create-url'), {
|
||||
description: structure,
|
||||
schedule_for_registration: scheduleForRegistration,
|
||||
object_id: bioEddieModal.data('object_id'),
|
||||
object_type: bioEddieModal.data('object_type'),
|
||||
name: moleculeName,
|
||||
image: svg
|
||||
}, function(result) {
|
||||
var newAsset = $(result.html);
|
||||
if (bioEddieModal.data('object_type') === 'Step') {
|
||||
newAsset.find('.file-preview-link').css('top', '-300px');
|
||||
newAsset.addClass('new').prependTo($(bioEddieModal.data('assets_container')));
|
||||
setTimeout(function() {
|
||||
newAsset.find('.file-preview-link').css('top', '0px');
|
||||
}, 200);
|
||||
bioEddieModal.modal('hide');
|
||||
} else if (bioEddieModal.data('object_type') === 'Result') {
|
||||
window.location.reload();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function updateMolecule(svg, structure, scheduleForRegistration) {
|
||||
var moleculeName = bioEddieModal.find('.file-name input').val();
|
||||
$.ajax({
|
||||
url: bioEddieModal.data('update-url'),
|
||||
data: {
|
||||
description: structure,
|
||||
schedule_for_registration: scheduleForRegistration,
|
||||
name: moleculeName,
|
||||
image: svg
|
||||
},
|
||||
dataType: 'json',
|
||||
type: 'PUT',
|
||||
success: function(json) {
|
||||
$('#modal_link' + json.id + ' img').attr('src', json.url);
|
||||
$('#modal_link' + json.id + ' .attachment-label').html(json.file_name);
|
||||
bioEddieModal.modal('hide');
|
||||
},
|
||||
error: function(response) {
|
||||
if (response.status === 403) {
|
||||
HelperModule.flashAlertMsg(I18n.t('general.no_permissions'), 'danger');
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function generateImage(structure, scheduleForRegistration) {
|
||||
var imageGenerator = new CHEMAXON.ImageGenerator();
|
||||
var emptySVG = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
|
||||
imageGenerator.generateSVGFromHelm(emptySVG, structure)
|
||||
.then(svg => {
|
||||
if (bioEddieModal.data('edit-mode')) {
|
||||
updateMolecule(svg, structure, scheduleForRegistration);
|
||||
} else {
|
||||
saveMolecule(svg, structure, scheduleForRegistration);
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
if (structure === '$$$$V2.0') {
|
||||
HelperModule.flashAlertMsg(I18n.t('bio_eddie.empty_molecule_error'), 'danger');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
$(document).on('turbolinks:load', function() {
|
||||
bioEddieIframe = document.getElementById('bioEddieIframe');
|
||||
bioEddieModal = $('#bioEddieModal');
|
||||
|
||||
bioEddieModal.on('shown.bs.modal', function() {
|
||||
initIframe();
|
||||
});
|
||||
|
||||
bioEddieModal.on('click', '.file-save-link', function() {
|
||||
var model = BIO_EDDIE.getModel();
|
||||
var monomerModel = BIO_EDDIE.getMonomerModel();
|
||||
var monomerExporter = new CHEMAXON.Helm2ExportModule();
|
||||
var scheduleForRegistration = $(this).data('schedule-for-registration');
|
||||
|
||||
monomerExporter.export(model, monomerModel)
|
||||
.then(structure => generateImage(structure, scheduleForRegistration));
|
||||
});
|
||||
});
|
||||
|
||||
return {
|
||||
open_new: (objectId, objectType, container) => {
|
||||
bioEddieModal.data('object_id', objectId);
|
||||
bioEddieModal.data('object_type', objectType);
|
||||
bioEddieModal.data('assets_container', container);
|
||||
bioEddieModal.find('.file-name input').val('');
|
||||
bioEddieModal.modal('show');
|
||||
},
|
||||
|
||||
open_edit: (name, molecule, updateUrl) => {
|
||||
bioEddieModal.data('edit-mode', true);
|
||||
bioEddieModal.data('molecule', molecule);
|
||||
bioEddieModal.data('update-url', updateUrl);
|
||||
bioEddieModal.find('.file-name input').val(name);
|
||||
bioEddieModal.modal('show');
|
||||
}
|
||||
};
|
||||
}());
|
||||
|
||||
(function() {
|
||||
$(document).on('click', '.new-bio-eddie-upload-button', function() {
|
||||
bioEddieEditor.open_new(
|
||||
this.dataset.objectId,
|
||||
this.dataset.objectType,
|
||||
this.dataset.assetsContainer
|
||||
);
|
||||
});
|
||||
|
||||
$(document).on('click', '.bio-eddie-edit-button', function() {
|
||||
$('#filePreviewModal').modal('hide');
|
||||
bioEddieEditor.open_edit(
|
||||
this.dataset.moleculeName,
|
||||
this.dataset.moleculeDescription,
|
||||
this.dataset.updateUrl
|
||||
);
|
||||
$.post(this.dataset.editUrl);
|
||||
});
|
||||
}());
|
|
@ -25,10 +25,10 @@ var DataTableHelpers = (function() {
|
|||
initSearchField: function(dataTableWraper, searchText) {
|
||||
var tableFilterInput = $(dataTableWraper).find('.dataTables_filter input');
|
||||
tableFilterInput.attr('placeholder', searchText)
|
||||
.addClass('sci-input-field')
|
||||
.addClass('sci-input-field search-field')
|
||||
.css('margin', 0);
|
||||
$('.dataTables_filter').append(`
|
||||
<div class="sci-input-container left-icon">
|
||||
<div class="sci-input-container left-icon search-container">
|
||||
<i class="fas fa-search"></i>
|
||||
</div>`).find('.sci-input-container').prepend(tableFilterInput);
|
||||
$('.dataTables_filter').find('label').remove();
|
||||
|
|
|
@ -48,10 +48,12 @@
|
|||
|
||||
|
||||
*/
|
||||
|
||||
var dropdownSelector = (function() {
|
||||
// /////////////////////
|
||||
// Support functions //
|
||||
// ////////////////////
|
||||
const MAX_DROPDOWN_HEIGHT = 320;
|
||||
|
||||
// Change direction of dropdown depends of container position
|
||||
function updateDropdownDirection(selector, container) {
|
||||
|
@ -77,13 +79,15 @@ var dropdownSelector = (function() {
|
|||
|
||||
if ((modalContainerBottom + bottomSpace) < bottomTreshold) {
|
||||
container.addClass('inverse');
|
||||
container.find('.dropdown-container').css('max-height', `${(containerPosition - 122 + maxHeight)}px`)
|
||||
maxHeight = Math.min(containerPosition - 122 + maxHeight, MAX_DROPDOWN_HEIGHT);
|
||||
container.find('.dropdown-container').css('max-height', `${maxHeight}px`)
|
||||
.css('margin-bottom', `${(containerPosition * -1)}px`)
|
||||
.css('left', `${containerPositionLeft}px`)
|
||||
.css('width', `${containerWidth}px`);
|
||||
} else {
|
||||
container.removeClass('inverse');
|
||||
container.find('.dropdown-container').css('max-height', `${(bottomSpace - 32 + maxHeight)}px`)
|
||||
maxHeight = Math.min(bottomSpace - 32 + maxHeight, MAX_DROPDOWN_HEIGHT);
|
||||
container.find('.dropdown-container').css('max-height', `${maxHeight}px`)
|
||||
.css('width', `${containerWidth}px`)
|
||||
.css('left', `${containerPositionLeft}px`)
|
||||
.css('margin-top', `${(bottomSpace * -1)}px`);
|
||||
|
@ -254,8 +258,8 @@ var dropdownSelector = (function() {
|
|||
updateTags(selector, container, { select: true });
|
||||
}
|
||||
|
||||
// intialization keyboard control
|
||||
function initKeyboardControl(container) {
|
||||
// initialize keyboard control
|
||||
function initKeyboardControl(selector, container) {
|
||||
container.find('.search-field').keydown(function(e) {
|
||||
var dropdownContainer = container.find('.dropdown-container');
|
||||
var pressedKey = e.keyCode;
|
||||
|
@ -265,20 +269,22 @@ var dropdownSelector = (function() {
|
|||
dropdownContainer.find('.dropdown-option').first().addClass('highlight');
|
||||
}
|
||||
|
||||
if (pressedKey === 38) {
|
||||
if (pressedKey === 38) { // arrow up
|
||||
if (selectedOption.prev('.dropdown-option').length) {
|
||||
selectedOption.removeClass('highlight').prev().addClass('highlight');
|
||||
}
|
||||
if (selectedOption.prev('.delimiter').length) {
|
||||
selectedOption.removeClass('highlight').prev().prev().addClass('highlight');
|
||||
}
|
||||
} else if (pressedKey === 40) {
|
||||
} else if (pressedKey === 40) { // arrow down
|
||||
if (selectedOption.next('.dropdown-option').length) {
|
||||
selectedOption.removeClass('highlight').next().addClass('highlight');
|
||||
}
|
||||
if (selectedOption.next('.delimiter').length) {
|
||||
selectedOption.removeClass('highlight').next().next().addClass('highlight');
|
||||
}
|
||||
} else if (pressedKey === 8 && e.target.value === '') { // backspace
|
||||
deleteTag(selector, container, container.find('.ds-tags .fa-times').last());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -485,10 +491,10 @@ var dropdownSelector = (function() {
|
|||
dropdownContainer.addClass('disable-search');
|
||||
}
|
||||
|
||||
// initialization keyboard controll
|
||||
initKeyboardControl(dropdownContainer);
|
||||
// initialization keyboard control
|
||||
initKeyboardControl(selector, dropdownContainer);
|
||||
|
||||
// In some case dropdown position not correclty calculated
|
||||
// In some case dropdown position not correctly calculated
|
||||
updateDropdownDirection(selectElement, dropdownContainer);
|
||||
}
|
||||
|
||||
|
@ -663,6 +669,25 @@ var dropdownSelector = (function() {
|
|||
updateTags(selector, container, { select: true });
|
||||
}
|
||||
|
||||
function deleteTag(selector, container, target) {
|
||||
var tagLabel = target.prev();
|
||||
|
||||
// Start delete animation
|
||||
target.parent().addClass('closing');
|
||||
|
||||
// Add timeout for deleting animation
|
||||
setTimeout(() => {
|
||||
if (selector.data('combine-tags')) {
|
||||
// if we use combine-tags options we simply clear all values
|
||||
container.find('.data-field').val('[]');
|
||||
updateTags(selector, container);
|
||||
} else {
|
||||
// Or delete specific one
|
||||
deleteValue(selector, container, tagLabel.data('ds-tag-id'), tagLabel.data('ds-tag-group'));
|
||||
}
|
||||
}, 350);
|
||||
}
|
||||
|
||||
// Refresh tags in input field
|
||||
function updateTags(selector, container, config = {}) {
|
||||
var selectedOptions = getCurrentData(container);
|
||||
|
@ -696,22 +721,8 @@ var dropdownSelector = (function() {
|
|||
|
||||
// Now we need add delete action to tag
|
||||
tag.find('.fa-times').click(function(e) {
|
||||
var tagLabel = $(this).prev();
|
||||
var toDelete;
|
||||
e.stopPropagation();
|
||||
// Start delete animation
|
||||
$(this).parent().addClass('closing');
|
||||
// Add timeout for deleting animation
|
||||
setTimeout(() => {
|
||||
if (selector.data('combine-tags')) {
|
||||
// if we use combine-tags optons we simply clear all values
|
||||
container.find('.data-field').val('[]');
|
||||
updateTags(selector, container);
|
||||
} else {
|
||||
// Or delete specific one
|
||||
deleteValue(selector, container, tagLabel.data('ds-tag-id'), tagLabel.data('ds-tag-group'));
|
||||
}
|
||||
}, 350);
|
||||
deleteTag(selector, container, $(this));
|
||||
});
|
||||
}
|
||||
|
||||
|
|
32
app/assets/javascripts/sitewide/print_progress_modal.js
Normal file
32
app/assets/javascripts/sitewide/print_progress_modal.js
Normal file
|
@ -0,0 +1,32 @@
|
|||
function updateProgressModal() {
|
||||
var status;
|
||||
var modal = $(document).find('.label-printing-progress-modal');
|
||||
|
||||
if (modal.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
$.getJSON(
|
||||
`/label_printers/${modal.data('labelPrinterId')}/update_progress_modal`
|
||||
+ `?starting_item_count=${modal.data('startingItemCount')}`,
|
||||
function(data) {
|
||||
modal.replaceWith(data.html);
|
||||
|
||||
status = modal.data('label-printer-status');
|
||||
if (status !== 'done' && status !== 'error') {
|
||||
setTimeout(updateProgressModal, 3000);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
$(document).on('click', '.label-printing-progress-modal .close', function() {
|
||||
$(this).closest('.label-printing-progress-modal').remove();
|
||||
});
|
||||
|
||||
$(document).on('turbolinks:load', function() {
|
||||
var modal = $(document).find('.label-printing-progress-modal');
|
||||
if (modal.length > 0) {
|
||||
updateProgressModal();
|
||||
}
|
||||
});
|
|
@ -1,3 +1,5 @@
|
|||
/* global dropdownSelector bwipjs */
|
||||
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
|
@ -16,6 +18,15 @@
|
|||
$(this).find('.modal-body #repository_row-info-table').DataTable().destroy();
|
||||
$(this).remove();
|
||||
});
|
||||
|
||||
let barCodeCanvas = bwipjs.toCanvas('bar-code-canvas', {
|
||||
bcid: 'qrcode',
|
||||
text: $('#modal-info-repository-row #bar-code-canvas').data('id').toString(),
|
||||
scale: 3
|
||||
});
|
||||
$('#modal-info-repository-row #bar-code-image').attr('src', barCodeCanvas.toDataURL('image/png'));
|
||||
|
||||
|
||||
$('#repository_row-info-table').DataTable({
|
||||
dom: 'RBltpi',
|
||||
stateSave: false,
|
||||
|
@ -40,4 +51,28 @@
|
|||
e.preventDefault();
|
||||
return false;
|
||||
});
|
||||
|
||||
$(document).on('click', '.print-label-button', function() {
|
||||
$.ajax({
|
||||
method: 'GET',
|
||||
url: $(this).data('url'),
|
||||
data: { rows: JSON.parse($(this).data('rows')) },
|
||||
dataType: 'json'
|
||||
}).done(function(xhr, settings, data) {
|
||||
$('body').append($.parseHTML(data.responseJSON.html));
|
||||
$('#modal-print-repository-row-label').modal('show', {
|
||||
backdrop: true,
|
||||
keyboard: false
|
||||
}).on('hidden.bs.modal', function() {
|
||||
$(this).remove();
|
||||
});
|
||||
|
||||
dropdownSelector.init('#modal-print-repository-row-label #label_printer_id', {
|
||||
noEmptyOption: true,
|
||||
singleSelect: true,
|
||||
closeOnSelect: true,
|
||||
selectAppearance: 'simple'
|
||||
});
|
||||
});
|
||||
});
|
||||
})();
|
||||
|
|
|
@ -61,6 +61,14 @@
|
|||
}
|
||||
});
|
||||
|
||||
dropdownSelector.init('#role', {
|
||||
noEmptyOption: true,
|
||||
singleSelect: true,
|
||||
closeOnSelect: true,
|
||||
selectAppearance: 'simple',
|
||||
disableSearch: true
|
||||
});
|
||||
|
||||
modal.off('show.bs.modal').on('show.bs.modal', function() {
|
||||
// This cannot be scoped outside this function
|
||||
// because it is generated via JS
|
||||
|
@ -121,7 +129,7 @@
|
|||
var data = {
|
||||
emails: dropdownSelector.getValues(emailsInput),
|
||||
team_ids: dropdownSelector.getValues(teamsInput),
|
||||
role: roleInput.val(),
|
||||
role: dropdownSelector.getValues(roleInput),
|
||||
'g-recaptcha-response': $('#recaptcha-invite-modal').val()
|
||||
};
|
||||
|
||||
|
|
74
app/assets/javascripts/users/settings/webhooks/index.js
Normal file
74
app/assets/javascripts/users/settings/webhooks/index.js
Normal file
|
@ -0,0 +1,74 @@
|
|||
/* global dropdownSelector */
|
||||
|
||||
(function() {
|
||||
function initDeleteFilterModal() {
|
||||
$('.activity-filters-list').on('click', '.delete-filter', function() {
|
||||
$('#deleteFilterModal').find('.description b').text(this.dataset.name);
|
||||
$('#deleteFilterModal').find('#filter_id').val(this.dataset.id);
|
||||
$('#deleteFilterModal').modal('show');
|
||||
});
|
||||
}
|
||||
|
||||
function initFilterInfoDropdown() {
|
||||
$('.info-container').on('show.bs.dropdown', function() {
|
||||
var tagsList = $(this).find('.tags-list');
|
||||
if (tagsList.is(':empty')) {
|
||||
$.get(this.dataset.url, function(data) {
|
||||
$.each(data.filter_elements, function(i, element) {
|
||||
let tag = $('<span class="filter-info-tag"></span>');
|
||||
tag.text(element);
|
||||
tagsList.append(tag);
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
$('.activity-filters-list').on('ajax:error', '.webhook-form', function(e, data) {
|
||||
$(this).renderFormErrors('webhook', data.responseJSON.errors);
|
||||
});
|
||||
|
||||
$('.activity-filters-list').on('click', '.create-webhook', function() {
|
||||
let filterElement = $(this).closest('.filter-element');
|
||||
filterElement.find('.webhooks-list').collapse('show');
|
||||
filterElement.find('.create-webhook-container').removeClass('hidden');
|
||||
});
|
||||
|
||||
$('.activity-filters-list').on('click', '.create-webhook-container .cancel-action', function(e) {
|
||||
let webhookContainer = $(this).closest('.create-webhook-container');
|
||||
e.preventDefault();
|
||||
webhookContainer.addClass('hidden');
|
||||
webhookContainer.find('.url-input').val('');
|
||||
$('.webhook-form').renderFormErrors('webhook', [], true);
|
||||
});
|
||||
|
||||
$('.activity-filters-list').on('click', '.edit-webhook', function(e) {
|
||||
let webhookContainer = $(this).closest('.webhook');
|
||||
e.preventDefault();
|
||||
webhookContainer.find('.view-mode').addClass('hidden');
|
||||
webhookContainer.find('.edit-webhook-container').removeClass('hidden');
|
||||
});
|
||||
|
||||
$('.activity-filters-list').on('click', '.edit-webhook-container .cancel-action', function(e) {
|
||||
let webhookContainer = $(this).closest('.webhook');
|
||||
let input = webhookContainer.find('.url-input');
|
||||
e.preventDefault();
|
||||
input.val(input.data('original-value'));
|
||||
webhookContainer.find('.view-mode').removeClass('hidden');
|
||||
webhookContainer.find('.edit-webhook-container').addClass('hidden');
|
||||
$('.webhook-form').renderFormErrors('webhook', [], true);
|
||||
});
|
||||
|
||||
|
||||
$('.webhook-method-container select').each(function() {
|
||||
dropdownSelector.init($(this), {
|
||||
singleSelect: true,
|
||||
closeOnSelect: true,
|
||||
noEmptyOption: true,
|
||||
selectAppearance: 'simple',
|
||||
disableSearch: true
|
||||
});
|
||||
});
|
||||
|
||||
initDeleteFilterModal();
|
||||
initFilterInfoDropdown();
|
||||
}());
|
80
app/assets/stylesheets/bio_eddie.scss
Normal file
80
app/assets/stylesheets/bio_eddie.scss
Normal file
|
@ -0,0 +1,80 @@
|
|||
// scss-lint:disable SelectorDepth
|
||||
// scss-lint:disable NestingDepth
|
||||
// scss-lint:disable SelectorFormat
|
||||
// scss-lint:disable ImportantRule
|
||||
// scss-lint:disable IdSelector
|
||||
|
||||
@import "constants";
|
||||
@import "mixins";
|
||||
|
||||
// MarvinJs modal
|
||||
.modal-bio-eddie {
|
||||
background: transparent;
|
||||
font-size: $font-size-base;
|
||||
padding: 0 !important;
|
||||
|
||||
.modal-dialog {
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
background: transparent;
|
||||
border: 0;
|
||||
box-shadow: none;
|
||||
height: 100%;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
background: $color-white;
|
||||
display: flex;
|
||||
height: 60px;
|
||||
line-height: 40px;
|
||||
padding: 10px 15px;
|
||||
text-align: center;
|
||||
|
||||
.file-save-link {
|
||||
flex-shrink: 0;
|
||||
margin: 0 20px 0 0;
|
||||
|
||||
img {
|
||||
height: 16px;
|
||||
vertical-align: sub;
|
||||
}
|
||||
}
|
||||
|
||||
.file-name {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
float: left;
|
||||
margin-right: auto;
|
||||
|
||||
input {
|
||||
border-radius: 5px;
|
||||
box-shadow: none;
|
||||
color: $color-black;
|
||||
height: 40px;
|
||||
margin-left: 5px;
|
||||
outline: 0;
|
||||
padding: 5px 10px;
|
||||
position: relative;
|
||||
width: 350px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
height: calc(100% - 60px);
|
||||
padding: 0;
|
||||
|
||||
iframe {
|
||||
height: 100%;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -21,7 +21,8 @@
|
|||
}
|
||||
|
||||
.create-wopi-file-btn,
|
||||
.new-marvinjs-upload-button {
|
||||
.new-marvinjs-upload-button,
|
||||
.new-bio-eddie-upload-button {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -514,12 +514,14 @@ li.module-hover {
|
|||
}
|
||||
}
|
||||
|
||||
.users-dropdown-list {
|
||||
.dropdown-option.users-dropdown-list {
|
||||
padding: 8px 10px;
|
||||
|
||||
.item-avatar {
|
||||
border-radius: 50%;
|
||||
height: 20px;
|
||||
margin: 0 .5em 0 0;
|
||||
width: 20px;
|
||||
height: 32px;
|
||||
margin: 0 16px 0 0;
|
||||
width: 32px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -579,3 +579,21 @@
|
|||
max-height: 290px;
|
||||
}
|
||||
}
|
||||
|
||||
.barcode-scanner {
|
||||
cursor: pointer;
|
||||
position: absolute;
|
||||
right: 12px;
|
||||
top: 12px;
|
||||
}
|
||||
|
||||
.search-container {
|
||||
.search-field {
|
||||
padding-right: 32px;
|
||||
|
||||
&.barcode-mode {
|
||||
background-color: $brand-primary;
|
||||
opacity: .3;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
66
app/assets/stylesheets/repository/print_progress_modal.scss
Normal file
66
app/assets/stylesheets/repository/print_progress_modal.scss
Normal file
|
@ -0,0 +1,66 @@
|
|||
.label-printing-progress-modal {
|
||||
background: $color-white;
|
||||
bottom: 1em;
|
||||
box-shadow: $modal-shadow;
|
||||
min-width: 300px;
|
||||
position: fixed;
|
||||
right: 1em;
|
||||
z-index: 9999;
|
||||
|
||||
.modal-header {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
padding: .5em;
|
||||
|
||||
.title {
|
||||
@include font-h3;
|
||||
}
|
||||
|
||||
.printer-status {
|
||||
border: $border-default;
|
||||
color: $color-silver-chalice;
|
||||
margin-left: .5em;
|
||||
margin-right: auto;
|
||||
padding: .25em;
|
||||
|
||||
&[data-status="ready"] {
|
||||
background: $brand-success;
|
||||
border-color: $brand-success;
|
||||
color: $color-white;
|
||||
}
|
||||
|
||||
&[data-status="out_of_labels"] {
|
||||
background: $brand-warning;
|
||||
border-color: $brand-warning;
|
||||
color: $color-white;
|
||||
}
|
||||
|
||||
&[data-status="error"] {
|
||||
background: $brand-danger;
|
||||
border-color: $brand-danger;
|
||||
color: $color-white;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
.printing-items {
|
||||
.id-label {
|
||||
margin-left: .5em;
|
||||
opacity: .5;
|
||||
}
|
||||
}
|
||||
|
||||
.printing-status {
|
||||
color: $brand-primary;
|
||||
|
||||
&[data-status="done"] {
|
||||
color: $brand-success;
|
||||
}
|
||||
|
||||
&[data-status="waiting_labels"], &[data-status="error"] {
|
||||
color: $brand-danger;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
#modal-print-repository-row-label {
|
||||
|
||||
.id-label {
|
||||
margin-left: .5em;
|
||||
opacity: .5;
|
||||
}
|
||||
|
||||
.printers-container {
|
||||
margin-bottom: 1em;
|
||||
min-height: 4em;
|
||||
}
|
||||
|
||||
.print-copies-input {
|
||||
margin-left: .5em;
|
||||
width: 50px;
|
||||
}
|
||||
|
||||
.modal-footer {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.no-printers-container {
|
||||
padding: 2em;
|
||||
text-align: center;
|
||||
|
||||
.no-printer-title {
|
||||
@include font-h3;
|
||||
margin-top: 1em;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
#modal-info-repository-row {
|
||||
.bar-code-container {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
}
|
|
@ -1,3 +1,6 @@
|
|||
// scss-lint:disable SelectorDepth SelectorFormat QualifyingElement
|
||||
// scss-lint:disable NestingDepth ImportantRule
|
||||
|
||||
.user-account-addons {
|
||||
.content-pane {
|
||||
margin: 0;
|
||||
|
@ -6,6 +9,54 @@
|
|||
|
||||
.addons-title {
|
||||
border-bottom: $border-tertiary;
|
||||
margin-bottom: 0;
|
||||
padding-bottom: 15px;
|
||||
}
|
||||
|
||||
.addons-subtitle {
|
||||
margin-top: 2em;
|
||||
}
|
||||
|
||||
.printers-container {
|
||||
.printer {
|
||||
border: $border-default;
|
||||
padding: 1em;
|
||||
|
||||
.header {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
margin-bottom: .5em;
|
||||
|
||||
.title {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.control {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.status {
|
||||
border: $border-default;
|
||||
color: $color-silver-chalice;
|
||||
margin-left: .5em;
|
||||
margin-right: auto;
|
||||
padding: .25em;
|
||||
|
||||
&[data-ready="true"] {
|
||||
background: $brand-success;
|
||||
border-color: $brand-success;
|
||||
color: $color-white;
|
||||
}
|
||||
}
|
||||
|
||||
.fas-check {
|
||||
margin-left: .25em;
|
||||
}
|
||||
}
|
||||
|
||||
.description {
|
||||
margin-bottom: .5em;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
89
app/assets/stylesheets/settings/label_printers.scss
Normal file
89
app/assets/stylesheets/settings/label_printers.scss
Normal file
|
@ -0,0 +1,89 @@
|
|||
// scss-lint:disable SelectorDepth SelectorFormat QualifyingElement
|
||||
// scss-lint:disable NestingDepth ImportantRule
|
||||
|
||||
.label-printer-show {
|
||||
.printer-title {
|
||||
flex-grow: 0 !important;
|
||||
margin-right: .5em !important;
|
||||
}
|
||||
|
||||
.status {
|
||||
border: $border-default;
|
||||
color: $color-silver-chalice;
|
||||
margin-left: .5em;
|
||||
margin-right: auto;
|
||||
padding: .25em;
|
||||
|
||||
&[data-ready="true"] {
|
||||
background: $brand-success;
|
||||
border-color: $brand-success;
|
||||
color: $color-white;
|
||||
}
|
||||
}
|
||||
|
||||
ul {
|
||||
list-style-type: none;
|
||||
padding-left: 0;
|
||||
|
||||
li {
|
||||
padding: .5em 0;
|
||||
}
|
||||
}
|
||||
|
||||
.collapse {
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
.fa-caret-down {
|
||||
cursor: pointer;
|
||||
margin-right: .25em;
|
||||
|
||||
&.collapsed {
|
||||
@include rotate(-90deg);
|
||||
}
|
||||
}
|
||||
|
||||
.collapse-row {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.collapse {
|
||||
padding-left: 1.5em;
|
||||
}
|
||||
|
||||
.row-title {
|
||||
@include font-h2;
|
||||
margin-left: .5em;
|
||||
}
|
||||
|
||||
.api-key-container {
|
||||
display: flex;
|
||||
position: relative;
|
||||
|
||||
&.warning {
|
||||
&::after {
|
||||
color: $brand-primary-light;
|
||||
content: attr(data-warning);
|
||||
left: 0;
|
||||
position: absolute;
|
||||
bottom: -1.5em;
|
||||
}
|
||||
}
|
||||
|
||||
.api-key-input {
|
||||
margin-right: .5em;
|
||||
|
||||
|
||||
}
|
||||
|
||||
.btn {
|
||||
margin-left: .5em;
|
||||
margin-top: 23px;
|
||||
}
|
||||
}
|
||||
|
||||
.update-printers {
|
||||
margin-left: auto;
|
||||
}
|
||||
}
|
|
@ -189,14 +189,15 @@
|
|||
}
|
||||
}
|
||||
|
||||
.users-dropdown-list {
|
||||
.dropdown-option.users-dropdown-list {
|
||||
border-top: 1px solid $color-gainsboro;
|
||||
padding: 8px 10px;
|
||||
|
||||
.item-avatar {
|
||||
border-radius: 50%;
|
||||
height: 20px;
|
||||
margin: 0 .5em 0 0;
|
||||
width: 20px;
|
||||
height: 32px;
|
||||
margin: 0 16px 0 0;
|
||||
width: 32px;
|
||||
}
|
||||
|
||||
.item-email {
|
||||
|
|
166
app/assets/stylesheets/settings/webhooks.scss
Normal file
166
app/assets/stylesheets/settings/webhooks.scss
Normal file
|
@ -0,0 +1,166 @@
|
|||
// scss-lint:disable SelectorDepth NestingDepth
|
||||
|
||||
.webhooks-index {
|
||||
.webhooks-description {
|
||||
@include font-main;
|
||||
margin: 1em 0;
|
||||
}
|
||||
|
||||
.activity-filters-list {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.filter-element {
|
||||
border-left: 3px solid $color-concrete;
|
||||
list-style: none;
|
||||
margin: 1em 0;
|
||||
}
|
||||
|
||||
.filter-block {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
padding-left: 1em;
|
||||
|
||||
.fa-caret-down {
|
||||
cursor: pointer;
|
||||
margin-right: 1em;
|
||||
|
||||
&.collapsed {
|
||||
@include rotate(-90deg);
|
||||
}
|
||||
}
|
||||
|
||||
.create-webhook {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.filter-name {
|
||||
@include font-h3;
|
||||
margin-right: .5em;
|
||||
}
|
||||
|
||||
.info-container {
|
||||
.dropdown-menu {
|
||||
padding: .5em;
|
||||
width: 400px;
|
||||
}
|
||||
}
|
||||
|
||||
.filter-info-title {
|
||||
@include font-small;
|
||||
padding-left: .25em;
|
||||
}
|
||||
|
||||
.tags-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
||||
.filter-info-tag {
|
||||
@include font-small;
|
||||
background: $color-concrete;
|
||||
flex-shrink: 0;
|
||||
margin: .25em;
|
||||
padding: .25em;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.webhooks-list {
|
||||
list-style: none;
|
||||
padding-top: .5em;
|
||||
}
|
||||
|
||||
.webhook-form {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
|
||||
.form-group {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.form-text {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.webhook-method-container {
|
||||
margin: .5em;
|
||||
}
|
||||
|
||||
.url-input-container {
|
||||
margin: .5em;
|
||||
}
|
||||
}
|
||||
|
||||
.webhook {
|
||||
border-top: $border-tertiary;
|
||||
padding: .5em 0;
|
||||
|
||||
.view-mode {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
|
||||
.method {
|
||||
margin: 0 .5em;
|
||||
}
|
||||
|
||||
.webhook-text {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.active-webhook,
|
||||
.disabled-webhook {
|
||||
flex-basis: 110px;
|
||||
flex-shrink: 0;
|
||||
margin-left: auto;
|
||||
padding: 0 1em;
|
||||
text-align: right;
|
||||
|
||||
.fas {
|
||||
margin-right: .25em;
|
||||
}
|
||||
}
|
||||
|
||||
.active-webhook {
|
||||
color: $brand_success;
|
||||
}
|
||||
|
||||
.dropdown-menu {
|
||||
@include font-button;
|
||||
|
||||
.fas {
|
||||
margin-right: .25em;
|
||||
}
|
||||
|
||||
.divider-label {
|
||||
@include font-small;
|
||||
color: $color-silver-chalice;
|
||||
padding-left: 1em;
|
||||
}
|
||||
|
||||
a {
|
||||
border-radius: unset;
|
||||
cursor: pointer;
|
||||
padding: .5em 1em;
|
||||
|
||||
&:hover {
|
||||
background: $color-concrete;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:not(.active) {
|
||||
.view-mode {
|
||||
color: $color-silver-chalice;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#deleteFilterModal {
|
||||
.delete-filter-form {
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -61,6 +61,34 @@
|
|||
}
|
||||
}
|
||||
|
||||
&.bio_eddie {
|
||||
|
||||
&::before,
|
||||
&::after {
|
||||
border-radius: 1em 0 0 1em;
|
||||
bottom: 1em;
|
||||
content: "";
|
||||
display: block;
|
||||
height: 2em;
|
||||
line-height: 2em;
|
||||
position: absolute;
|
||||
right: -1em;
|
||||
width: 2.25em;
|
||||
}
|
||||
|
||||
&::before {
|
||||
background: $bio-eddie-color;
|
||||
}
|
||||
|
||||
&::after {
|
||||
background-image: url("/images/icon_small/bio_eddie_white.png");
|
||||
background-repeat: no-repeat;
|
||||
height: 1.85em;
|
||||
right: -1.15em;
|
||||
width: 2em;
|
||||
}
|
||||
}
|
||||
|
||||
.fas {
|
||||
font-size: 100px;
|
||||
line-height: 160px;
|
||||
|
|
|
@ -115,7 +115,7 @@
|
|||
|
||||
.dropdown-menu {
|
||||
@include font-button;
|
||||
min-width: 150px;
|
||||
min-width: 200px;
|
||||
padding: .5em 0;
|
||||
|
||||
a {
|
||||
|
@ -136,6 +136,10 @@
|
|||
right: 1em;
|
||||
}
|
||||
}
|
||||
|
||||
.fa-stack {
|
||||
width: 2em;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,7 +18,6 @@
|
|||
|
||||
.dropdown-selector-container {
|
||||
display: inline-block;
|
||||
float: left;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
|
||||
|
@ -104,7 +103,6 @@
|
|||
.tag-label {
|
||||
display: inline-block;
|
||||
margin-right: 5px;
|
||||
margin-top: 1px;
|
||||
max-width: 240px;
|
||||
overflow: hidden;
|
||||
text-align: left;
|
||||
|
@ -115,6 +113,12 @@
|
|||
&[data-ds-tag-id=""] {
|
||||
opacity: .7;
|
||||
}
|
||||
|
||||
.item-avatar {
|
||||
height: 16px;
|
||||
margin-right: 8px;
|
||||
width: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.fas {
|
||||
|
|
|
@ -41,6 +41,9 @@ $office-ms-powerpoint: #d24726;
|
|||
// MarvinJS color:
|
||||
$marvinjs-color: #29999c;
|
||||
|
||||
// BioEddie color:
|
||||
$bio-eddie-color: #ffa000;
|
||||
|
||||
$pdf-color: #f40f02;
|
||||
|
||||
// Don't use them
|
||||
|
|
|
@ -1,3 +1,11 @@
|
|||
.modal-sm {
|
||||
width: 370px;
|
||||
}
|
||||
|
||||
.modal {
|
||||
.modal-absolute-close-button {
|
||||
position: absolute;
|
||||
right: 1em;
|
||||
top: .5em;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -133,6 +133,10 @@ body {
|
|||
}
|
||||
}
|
||||
|
||||
.modal-header h4 {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.jumbotron {
|
||||
background-color: inherit;
|
||||
}
|
||||
|
@ -1349,14 +1353,7 @@ body > .loading-overlay {
|
|||
margin-top: 20px;
|
||||
}
|
||||
|
||||
h4 {
|
||||
font-size: 14px;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.select-container--with-blank {
|
||||
overflow: hidden;
|
||||
|
||||
.search-field::placeholder {
|
||||
color: $color-black;
|
||||
opacity: 1;
|
||||
|
|
7
app/controllers/active_jobs_controller.rb
Normal file
7
app/controllers/active_jobs_controller.rb
Normal file
|
@ -0,0 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class ActiveJobsController < ApplicationController
|
||||
def status
|
||||
render json: { status: ApplicationJob.status(params[:id]) }
|
||||
end
|
||||
end
|
|
@ -7,8 +7,8 @@ module Api
|
|||
class IDMismatchError < StandardError; end
|
||||
class IncludeNotSupportedError < StandardError; end
|
||||
class PermissionError < StandardError
|
||||
attr_reader :klass
|
||||
attr_reader :mode
|
||||
attr_reader :klass, :mode
|
||||
|
||||
def initialize(klass, mode)
|
||||
@klass = klass
|
||||
@mode = mode
|
||||
|
|
|
@ -16,11 +16,11 @@ module Api
|
|||
.page(params.dig(:page, :number))
|
||||
.per(params.dig(:page, :size))
|
||||
|
||||
render jsonapi: projects, each_serializer: ProjectSerializer
|
||||
render jsonapi: projects, each_serializer: ProjectSerializer, include: include_params
|
||||
end
|
||||
|
||||
def show
|
||||
render jsonapi: @project, serializer: ProjectSerializer
|
||||
render jsonapi: @project, serializer: ProjectSerializer, include: include_params
|
||||
end
|
||||
|
||||
def create
|
||||
|
@ -64,6 +64,10 @@ module Api
|
|||
params.require(:data).require(:attributes).permit(:name, :visibility, :archived, :project_folder_id)
|
||||
end
|
||||
|
||||
def permitted_includes
|
||||
%w(comments)
|
||||
end
|
||||
|
||||
def load_project_for_managing
|
||||
@project = @team.projects.find(params.require(:id))
|
||||
raise PermissionError.new(Project, :manage) unless can_manage_project?(@project)
|
||||
|
|
|
@ -12,7 +12,7 @@ module Api
|
|||
.page(params.dig(:page, :number))
|
||||
.per(params.dig(:page, :size))
|
||||
render jsonapi: results, each_serializer: ResultSerializer,
|
||||
include: %i(text table file)
|
||||
include: (%i(text table file) << include_params).flatten.compact
|
||||
end
|
||||
|
||||
def create
|
||||
|
@ -43,7 +43,7 @@ module Api
|
|||
|
||||
def show
|
||||
render jsonapi: @result, serializer: ResultSerializer,
|
||||
include: %i(text table file)
|
||||
include: (%i(text table file) << include_params).flatten.compact
|
||||
end
|
||||
|
||||
private
|
||||
|
@ -185,6 +185,10 @@ module Api
|
|||
prms
|
||||
end
|
||||
|
||||
def permitted_includes
|
||||
%w(comments)
|
||||
end
|
||||
|
||||
def convert_old_tiny_mce_format(text)
|
||||
text.scan(/\[~tiny_mce_id:(\w+)\]/).flatten.each do |token|
|
||||
old_format = /\[~tiny_mce_id:#{token}\]/
|
||||
|
|
|
@ -20,11 +20,17 @@ module Api
|
|||
.page(params.dig(:page, :number))
|
||||
.per(params.dig(:page, :size))
|
||||
|
||||
render jsonapi: tasks, each_serializer: TaskSerializer, rte_rendering: render_rte?, team: @team
|
||||
render jsonapi: tasks, each_serializer: TaskSerializer,
|
||||
include: include_params,
|
||||
rte_rendering: render_rte?,
|
||||
team: @team
|
||||
end
|
||||
|
||||
def show
|
||||
render jsonapi: @task, serializer: TaskSerializer, rte_rendering: render_rte?, team: @team
|
||||
render jsonapi: @task, serializer: TaskSerializer,
|
||||
include: include_params,
|
||||
rte_rendering: render_rte?,
|
||||
team: @team
|
||||
end
|
||||
|
||||
def create
|
||||
|
@ -69,6 +75,10 @@ module Api
|
|||
params.require(:data).require(:attributes).permit(%i(name x y description my_module_status_id))
|
||||
end
|
||||
|
||||
def permitted_includes
|
||||
%w(comments)
|
||||
end
|
||||
|
||||
def load_task_for_managing
|
||||
@task = @experiment.my_modules.find(params.require(:id))
|
||||
raise PermissionError.new(MyModule, :manage) unless can_manage_module?(@task)
|
||||
|
|
|
@ -212,6 +212,22 @@ class AssetsController < ApplicationController
|
|||
|
||||
def destroy
|
||||
if @asset.destroy
|
||||
case @assoc
|
||||
when Step
|
||||
if @assoc.protocol.in_module?
|
||||
log_step_activity(:edit_step, @assoc, @assoc.my_module.experiment.project, my_module: @assoc.my_module.id)
|
||||
else
|
||||
log_step_activity(
|
||||
:edit_step_in_protocol_repository,
|
||||
@assoc,
|
||||
nil,
|
||||
protocol: @assoc.protocol.id
|
||||
)
|
||||
end
|
||||
when Result
|
||||
log_result_activity(:edit_result, @assoc)
|
||||
end
|
||||
|
||||
render json: { flash: I18n.t('assets.file_deleted', file_name: @asset.file_name) }
|
||||
else
|
||||
render json: {}, status: :unprocessable_entity
|
||||
|
@ -265,4 +281,31 @@ class AssetsController < ApplicationController
|
|||
|
||||
'file'
|
||||
end
|
||||
|
||||
def log_step_activity(type_of, step, project = nil, message_items = {})
|
||||
default_items = { step: step.id,
|
||||
step_position: { id: step.id, value_for: 'position_plus_one' } }
|
||||
message_items = default_items.merge(message_items)
|
||||
|
||||
Activities::CreateActivityService
|
||||
.call(activity_type: type_of,
|
||||
owner: current_user,
|
||||
subject: step.protocol,
|
||||
team: current_team,
|
||||
project: project,
|
||||
message_items: message_items)
|
||||
end
|
||||
|
||||
def log_result_activity(type_of, result)
|
||||
Activities::CreateActivityService
|
||||
.call(activity_type: type_of,
|
||||
owner: current_user,
|
||||
subject: result,
|
||||
team: result.my_module.experiment.project.team,
|
||||
project: result.my_module.experiment.project,
|
||||
message_items: {
|
||||
result: result.id,
|
||||
type_of_result: t('activities.result_type.text')
|
||||
})
|
||||
end
|
||||
end
|
||||
|
|
133
app/controllers/bio_eddie_assets_controller.rb
Normal file
133
app/controllers/bio_eddie_assets_controller.rb
Normal file
|
@ -0,0 +1,133 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class BioEddieAssetsController < ApplicationController
|
||||
include BioEddieActions
|
||||
include ActiveStorage::SetCurrent
|
||||
|
||||
before_action :load_vars, except: %i(create bmt_request license)
|
||||
before_action :load_create_vars, only: :create
|
||||
|
||||
before_action :check_read_permission, except: %i(update create start_editing bmt_request license)
|
||||
before_action :check_edit_permission, only: %i(update create start_editing)
|
||||
|
||||
def create
|
||||
asset = BioEddieService.create_molecule(bio_eddie_params, current_user, current_team)
|
||||
|
||||
create_create_bio_eddie_activity(asset, current_user)
|
||||
|
||||
if asset && bio_eddie_params[:object_type] == 'Step'
|
||||
create_register_bio_eddie_activity(asset, current_user) if bio_eddie_params[:schedule_for_registration] == 'true'
|
||||
render json: {
|
||||
html: render_to_string(partial: 'assets/asset.html.erb', locals: {
|
||||
asset: asset,
|
||||
gallery_view_id: bio_eddie_params[:object_id]
|
||||
})
|
||||
}
|
||||
elsif asset && bio_eddie_params[:object_type] == 'Result'
|
||||
create_register_bio_eddie_activity(asset, current_user) if bio_eddie_params[:schedule_for_registration] == 'true'
|
||||
render json: { status: 'created' }, status: :ok
|
||||
else
|
||||
render json: asset.errors, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
def update
|
||||
asset = BioEddieService.update_molecule(bio_eddie_params, current_team)
|
||||
|
||||
create_edit_bio_eddie_activity(asset, current_user, :finish_editing)
|
||||
|
||||
if asset
|
||||
create_register_bio_eddie_activity(asset, current_user) if bio_eddie_params[:schedule_for_registration] == 'true'
|
||||
render json: { url: rails_representation_url(asset.medium_preview),
|
||||
id: asset.id,
|
||||
file_name: asset.blob.metadata['name'] }
|
||||
else
|
||||
render json: { error: t('bio_eddie.no_molecules_found') }, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
def license
|
||||
license_file_path = Rails.root.join('data/bioeddie/license.cxl')
|
||||
if File.file?(license_file_path)
|
||||
send_file(license_file_path)
|
||||
else
|
||||
render_404
|
||||
end
|
||||
end
|
||||
|
||||
def bmt_request
|
||||
return render_404 unless ENV['BIOMOLECULE_TOOLKIT_BASE_URL']
|
||||
|
||||
uri = URI.parse(ENV['BIOMOLECULE_TOOLKIT_BASE_URL'])
|
||||
uri.path = request.original_fullpath.remove('/biomolecule_toolkit')
|
||||
|
||||
Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == 'https') do |http|
|
||||
api_request = "Net::HTTP::#{request.request_method.capitalize}".constantize.new(uri)
|
||||
api_request['x-api-key'] = ENV['BIOMOLECULE_TOOLKIT_API_KEY'] if ENV['BIOMOLECULE_TOOLKIT_API_KEY']
|
||||
api_request['Content-Type'] = 'application/json'
|
||||
request_body = request.body.read
|
||||
api_request.body = request_body if request_body.present?
|
||||
api_response = http.request(api_request)
|
||||
render body: api_response.body, content_type: api_response.content_type, status: api_response.code
|
||||
end
|
||||
end
|
||||
|
||||
def start_editing
|
||||
create_edit_bio_eddie_activity(@asset, current_user, :start_editing)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def load_vars
|
||||
@asset = current_team.assets.find_by(id: params[:id])
|
||||
return render_404 unless @asset
|
||||
|
||||
@assoc = @asset.step || @asset.result
|
||||
|
||||
case @assoc
|
||||
when Step
|
||||
@protocol = @assoc.protocol
|
||||
when Result
|
||||
@my_module = @assoc.my_module
|
||||
end
|
||||
end
|
||||
|
||||
def load_create_vars
|
||||
case bio_eddie_params[:object_type]
|
||||
when 'Step'
|
||||
@assoc = Step.find_by(id: bio_eddie_params[:object_id])
|
||||
@protocol = @assoc.protocol
|
||||
when 'Result'
|
||||
@assoc = MyModule.find_by(id: bio_eddie_params[:object_id])
|
||||
@my_module = @assoc
|
||||
end
|
||||
end
|
||||
|
||||
def check_read_permission
|
||||
case @assoc
|
||||
when Step
|
||||
return render_403 unless can_read_protocol_in_module?(@protocol) ||
|
||||
can_read_protocol_in_repository?(@protocol)
|
||||
when Result, MyModule
|
||||
return render_403 unless can_read_experiment?(@my_module.experiment)
|
||||
else
|
||||
render_403
|
||||
end
|
||||
end
|
||||
|
||||
def check_edit_permission
|
||||
case @assoc
|
||||
when Step
|
||||
return render_403 unless can_manage_protocol_in_module?(@protocol) ||
|
||||
can_manage_protocol_in_repository?(@protocol)
|
||||
when Result, MyModule
|
||||
return render_403 unless can_manage_module?(@my_module)
|
||||
else
|
||||
render_403
|
||||
end
|
||||
end
|
||||
|
||||
def bio_eddie_params
|
||||
params.permit(:id, :description, :object_id, :object_type, :name, :image, :schedule_for_registration)
|
||||
end
|
||||
end
|
111
app/controllers/concerns/bio_eddie_actions.rb
Normal file
111
app/controllers/concerns/bio_eddie_actions.rb
Normal file
|
@ -0,0 +1,111 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module BioEddieActions
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
private
|
||||
|
||||
def create_edit_bio_eddie_activity(asset, current_user, started_editing)
|
||||
action = case started_editing
|
||||
when :start_editing
|
||||
t('activities.file_editing.started')
|
||||
when :finish_editing
|
||||
t('activities.file_editing.finished')
|
||||
end
|
||||
return unless bio_eddie_asset_validation(asset)
|
||||
|
||||
bio_eddie_find_target_object(asset, current_user, 'edit', action)
|
||||
end
|
||||
|
||||
def create_create_bio_eddie_activity(asset, current_user)
|
||||
return unless bio_eddie_asset_validation(asset)
|
||||
|
||||
bio_eddie_find_target_object(asset, current_user, 'create')
|
||||
end
|
||||
|
||||
def create_register_bio_eddie_activity(asset, current_user)
|
||||
return unless bio_eddie_asset_validation(asset)
|
||||
|
||||
bio_eddie_find_target_object(asset, current_user, 'register')
|
||||
end
|
||||
|
||||
def bio_eddie_asset_validation(asset)
|
||||
asset && asset.file.metadata[:asset_type] == 'bio_eddie'
|
||||
end
|
||||
|
||||
def bio_eddie_asset_type(asset, klass)
|
||||
return true if asset.step_asset&.step.instance_of?(klass)
|
||||
return true if asset.result_asset&.result.instance_of?(klass)
|
||||
|
||||
false
|
||||
end
|
||||
|
||||
def bio_eddie_find_target_object(asset, current_user, activity_type, action = nil)
|
||||
if bio_eddie_asset_type(asset, Step)
|
||||
bio_eddie_step_activity(asset, current_user, activity_type, action)
|
||||
elsif bio_eddie_asset_type(asset, Result)
|
||||
bio_eddie_result_activity(asset, current_user, activity_type, action)
|
||||
end
|
||||
end
|
||||
|
||||
def bio_eddie_step_activity(asset, current_user, activity, action = nil)
|
||||
step = asset.step_asset&.step
|
||||
asset_type = 'asset_name'
|
||||
protocol = step&.protocol
|
||||
|
||||
return unless step && protocol
|
||||
|
||||
default_step_items = {
|
||||
step: step.id,
|
||||
step_position: { id: step.id, value_for: 'position_plus_one' },
|
||||
asset_type => { id: asset.id, value_for: 'file_name' },
|
||||
description: asset.blob.metadata['description'],
|
||||
name: asset.blob.metadata['name']
|
||||
}
|
||||
|
||||
default_step_items[:action] = action if action
|
||||
if protocol.in_module?
|
||||
project = protocol.my_module.experiment.project
|
||||
team = project.team
|
||||
type_of = "#{activity}_molecule_on_step".to_sym
|
||||
message_items = { my_module: protocol.my_module.id }
|
||||
else
|
||||
type_of = "#{activity}_molecule_on_step_in_repository".to_sym
|
||||
team = protocol.team
|
||||
message_items = { protocol: protocol.id }
|
||||
end
|
||||
message_items = default_step_items.merge(message_items)
|
||||
|
||||
Activities::CreateActivityService
|
||||
.call(activity_type: type_of,
|
||||
owner: current_user,
|
||||
subject: protocol,
|
||||
team: team,
|
||||
project: project,
|
||||
message_items: message_items)
|
||||
end
|
||||
|
||||
def bio_eddie_result_activity(asset, current_user, activity, action = nil)
|
||||
result = asset.result_asset&.result
|
||||
asset_type = 'asset_name'
|
||||
my_module = result&.my_module
|
||||
|
||||
return unless result && my_module
|
||||
|
||||
message_items = {
|
||||
result: result.id,
|
||||
asset_type => { id: asset.id, value_for: 'file_name' },
|
||||
description: asset.blob.metadata['description'],
|
||||
name: asset.blob.metadata['name']
|
||||
}
|
||||
|
||||
message_items[:action] = action if action
|
||||
Activities::CreateActivityService
|
||||
.call(activity_type: "#{activity}_molecule_on_result".to_sym,
|
||||
owner: current_user,
|
||||
subject: result,
|
||||
team: my_module.experiment.project.team,
|
||||
project: my_module.experiment.project,
|
||||
message_items: message_items)
|
||||
end
|
||||
end
|
|
@ -3,6 +3,8 @@
|
|||
class GlobalActivitiesController < ApplicationController
|
||||
include InputSanitizeHelper
|
||||
|
||||
before_action :check_create_activity_filter_permissions, only: :save_activity_filter
|
||||
|
||||
def index
|
||||
# Preload filter format
|
||||
# {
|
||||
|
@ -106,8 +108,21 @@ class GlobalActivitiesController < ApplicationController
|
|||
render json: get_objects(Report)
|
||||
end
|
||||
|
||||
def save_activity_filter
|
||||
activity_filter = ActivityFilter.new(activity_filter_params)
|
||||
if activity_filter.save
|
||||
render json: { message: t('global_activities.index.activity_filter_saved') }
|
||||
else
|
||||
render json: { errors: activity_filter.errors.full_messages }, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def check_create_activity_filter_permissions
|
||||
render_403 && return unless can_create_acitivity_filters?
|
||||
end
|
||||
|
||||
def get_objects(subject)
|
||||
query = subject_search_params[:query]
|
||||
teams =
|
||||
|
@ -138,6 +153,10 @@ class GlobalActivitiesController < ApplicationController
|
|||
matched.map { |pr| { value: pr[0], label: escape_input(pr[1]) } }
|
||||
end
|
||||
|
||||
def activity_filter_params
|
||||
params.permit(:name, filter: {})
|
||||
end
|
||||
|
||||
def activity_filters
|
||||
params.permit(
|
||||
:page, :starting_timestamp, :from_date, :to_date, types: [], subjects: {}, users: [], teams: []
|
||||
|
|
122
app/controllers/label_printers_controller.rb
Normal file
122
app/controllers/label_printers_controller.rb
Normal file
|
@ -0,0 +1,122 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class LabelPrintersController < ApplicationController
|
||||
include InputSanitizeHelper
|
||||
|
||||
before_action :check_manage_permissions, except: %i(index update_progress_modal)
|
||||
before_action :find_label_printer, only: %i(edit update destroy)
|
||||
|
||||
def index
|
||||
@label_printers = LabelPrinter.all
|
||||
@fluics_api_key = @label_printers.any? ? @label_printers.first.fluics_api_key : nil
|
||||
end
|
||||
|
||||
def new
|
||||
@label_printer = LabelPrinter.new
|
||||
end
|
||||
|
||||
def edit; end
|
||||
|
||||
def create
|
||||
@label_printer = LabelPrinter.new(label_printer_params)
|
||||
|
||||
if @label_printer.save
|
||||
flash[:success] = t('label_printers.create.success', { printer_name: @label_printer.name })
|
||||
redirect_to edit_label_printer_path(@label_printer)
|
||||
else
|
||||
flash[:error] = t('label_printers.create.error', { printer_name: @label_printer.name })
|
||||
render :new
|
||||
end
|
||||
end
|
||||
|
||||
def update
|
||||
if @label_printer.update(label_printer_params)
|
||||
flash[:success] = t('label_printers.update.success', { printer_name: @label_printer.name })
|
||||
redirect_to edit_label_printer_path(@label_printer)
|
||||
else
|
||||
flash[:error] = t('label_printers.update.error', { printer_name: @label_printer.name })
|
||||
render :edit
|
||||
end
|
||||
end
|
||||
|
||||
def destroy
|
||||
if @label_printer.destroy
|
||||
flash[:success] = t('label_printers.destroy.success', { printer_name: @label_printer.name })
|
||||
else
|
||||
flash[:error] = t('label_printers.destroy.error', { printer_name: @label_printer.name })
|
||||
end
|
||||
|
||||
redirect_to addons_path
|
||||
end
|
||||
|
||||
def print
|
||||
print_job = LabelPrinters::PrintJob.perform_later(
|
||||
LabelPrinter.find(params[:id]),
|
||||
LabelTemplate.find(print_job_params[:label_template_id])
|
||||
.render(print_job_params[:locals])
|
||||
)
|
||||
|
||||
render json: { job_id: print_job.job_id }
|
||||
end
|
||||
|
||||
def update_progress_modal
|
||||
render(
|
||||
json: {
|
||||
html:
|
||||
render_to_string(
|
||||
partial: 'label_printers/print_progress_modal',
|
||||
locals: {
|
||||
starting_item_count: params[:starting_item_count].to_i,
|
||||
label_printer: LabelPrinter.find(params[:id])
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
end
|
||||
|
||||
def create_fluics
|
||||
begin
|
||||
printers = LabelPrinters::Fluics::ApiClient.new(label_printer_params[:fluics_api_key]).list
|
||||
|
||||
LabelPrinter.destroy_all
|
||||
|
||||
printers.each do |fluics_printer|
|
||||
label_printer = LabelPrinter.find_or_initialize_by(
|
||||
fluics_api_key: label_printer_params[:fluics_api_key],
|
||||
fluics_lid: fluics_printer['LID'],
|
||||
type_of: :fluics,
|
||||
language_type: :zpl
|
||||
)
|
||||
|
||||
label_printer.update(
|
||||
name: fluics_printer['serviceName'],
|
||||
description: fluics_printer['comment']
|
||||
)
|
||||
end
|
||||
rescue LabelPrinters::Fluics::ApiClient::BadRequestError
|
||||
flash[:error] = t('users.settings.account.label_printer.api_key_error')
|
||||
end
|
||||
|
||||
redirect_to label_printers_path
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def check_manage_permissions
|
||||
render_403 unless can_manage_label_printers?
|
||||
end
|
||||
|
||||
def label_printer_params
|
||||
params.require(:label_printer).permit(
|
||||
:name, :type_of, :fluics_api_key, :host, :port
|
||||
)
|
||||
end
|
||||
|
||||
def print_job_params
|
||||
params.require(:label_template_id, :label_template_locals)
|
||||
end
|
||||
|
||||
def find_label_printer
|
||||
@label_printer = LabelPrinter.find(params[:id])
|
||||
end
|
||||
end
|
|
@ -372,7 +372,6 @@ class ReportsController < ApplicationController
|
|||
.accessible_by_teams(current_team)
|
||||
.name_like(search_params[:q])
|
||||
.limit(Constants::SEARCH_LIMIT)
|
||||
.select(:id, :name, :team_id, :permission_level)
|
||||
repositories.each do |repository|
|
||||
next unless can_manage_repository_rows?(current_user, repository)
|
||||
|
||||
|
|
|
@ -49,6 +49,8 @@ class RepositoriesController < ApplicationController
|
|||
@display_delete_button = can_delete_repository_rows?(@repository)
|
||||
@display_duplicate_button = can_create_repository_rows?(@repository)
|
||||
@snapshot_provisioning = @repository.repository_snapshots.provisioning.any?
|
||||
|
||||
@busy_printer = LabelPrinter.where.not(current_print_job_ids: []).first
|
||||
end
|
||||
|
||||
def table_toolbar
|
||||
|
|
|
@ -4,6 +4,8 @@ class RepositoryRowsController < ApplicationController
|
|||
include ApplicationHelper
|
||||
include MyModulesHelper
|
||||
|
||||
MAX_PRINTABLE_ITEM_NAME_LENGTH = 64
|
||||
|
||||
before_action :load_repository, except: :show
|
||||
before_action :load_repository_row, only: %i(update assigned_task_list)
|
||||
before_action :check_read_permissions, except: %i(show create update delete_records copy_records)
|
||||
|
@ -71,6 +73,45 @@ class RepositoryRowsController < ApplicationController
|
|||
end
|
||||
end
|
||||
|
||||
def print_modal
|
||||
@repository_rows = @repository.repository_rows.where(id: params[:rows])
|
||||
@printers = LabelPrinter.all
|
||||
respond_to do |format|
|
||||
format.json do
|
||||
render json: {
|
||||
html: render_to_string(
|
||||
partial: 'repositories/print_label_modal.html.erb'
|
||||
)
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def print
|
||||
# reset all potential error states for printers and discard all jobs
|
||||
|
||||
# rubocop:disable Rails/SkipsModelValidations
|
||||
LabelPrinter.update_all(status: :ready, current_print_job_ids: [])
|
||||
# rubocop:enable Rails/SkipsModelValidations
|
||||
|
||||
label_printer = LabelPrinter.find(params[:label_printer_id])
|
||||
|
||||
job_ids = RepositoryRow.where(id: params[:repository_row_ids]).flat_map do |repository_row|
|
||||
LabelPrinters::PrintJob.perform_later(
|
||||
label_printer,
|
||||
LabelTemplate.first.render( # Currently we will only use the default template
|
||||
item_id: repository_row.code,
|
||||
item_name: repository_row.name.truncate(MAX_PRINTABLE_ITEM_NAME_LENGTH)
|
||||
),
|
||||
params[:copies].to_i
|
||||
).job_id
|
||||
end
|
||||
|
||||
label_printer.update!(current_print_job_ids: job_ids * params[:copies].to_i)
|
||||
|
||||
redirect_to repository_path(@repository)
|
||||
end
|
||||
|
||||
def update
|
||||
row_update = RepositoryRows::UpdateRepositoryRowService
|
||||
.call(repository_row: @repository_row, user: current_user, params: update_params)
|
||||
|
|
|
@ -533,6 +533,10 @@ class StepsController < ApplicationController
|
|||
marvin_js_assets_attributes: %i(
|
||||
id
|
||||
_destroy
|
||||
),
|
||||
bio_eddie_assets_attributes: %i(
|
||||
id
|
||||
_destroy
|
||||
)
|
||||
)
|
||||
end
|
||||
|
|
|
@ -70,7 +70,7 @@ module Users
|
|||
break
|
||||
end
|
||||
|
||||
result = { email: email }
|
||||
result = { email: email, user_teams: [] }
|
||||
unless Constants::BASIC_EMAIL_REGEX.match?(email)
|
||||
result[:status] = :user_invalid
|
||||
@invite_results << result
|
||||
|
|
|
@ -38,7 +38,7 @@ module Users
|
|||
return redirect_to after_omniauth_failure_path_for(resource_name)
|
||||
end
|
||||
|
||||
user = User.find_by(email: email)
|
||||
user = User.find_by(email: email.downcase)
|
||||
|
||||
if user.blank?
|
||||
# Create new user and identity
|
||||
|
|
|
@ -3,6 +3,10 @@ module Users
|
|||
module Account
|
||||
class AddonsController < ApplicationController
|
||||
layout 'fluid'
|
||||
|
||||
def index
|
||||
@label_printer_any = LabelPrinter.any?
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
101
app/controllers/users/settings/webhooks_controller.rb
Normal file
101
app/controllers/users/settings/webhooks_controller.rb
Normal file
|
@ -0,0 +1,101 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Users
|
||||
module Settings
|
||||
class WebhooksController < ApplicationController
|
||||
layout 'fluid'
|
||||
|
||||
before_action :can_manage_filters
|
||||
before_action :load_filter, except: :index
|
||||
before_action :load_webhook, only: %i(update destroy)
|
||||
before_action :set_sort, except: :filter_info
|
||||
|
||||
def index
|
||||
@activity_filters = ActivityFilter.includes(:webhooks).order(name: (@current_sort == 'atoz' ? :asc : :desc))
|
||||
end
|
||||
|
||||
def destroy_filter
|
||||
@filter.destroy
|
||||
redirect_to users_settings_webhooks_path(sort: @current_sort)
|
||||
end
|
||||
|
||||
def filter_info
|
||||
render json: { filter_elements: load_filter_elements(@filter) }
|
||||
end
|
||||
|
||||
def create
|
||||
@webhook = @filter.webhooks.create(webhook_params)
|
||||
if @webhook.errors.any?
|
||||
render json: { errors: @webhook.errors.messages }, status: :unprocessable_entity
|
||||
else
|
||||
flash[:success] = t('webhooks.index.webhook_created')
|
||||
redirect_to users_settings_webhooks_path(sort: @current_sort)
|
||||
end
|
||||
end
|
||||
|
||||
def update
|
||||
@webhook.update(webhook_params)
|
||||
if @webhook.errors.any?
|
||||
render json: { errors: @webhook.errors.messages }, status: :unprocessable_entity
|
||||
else
|
||||
flash[:success] = t('webhooks.index.webhook_updated')
|
||||
redirect_to users_settings_webhooks_path(sort: @current_sort)
|
||||
end
|
||||
end
|
||||
|
||||
def destroy
|
||||
@webhook.destroy
|
||||
flash[:success] = t('webhooks.index.webhook_deleted')
|
||||
redirect_to users_settings_webhooks_path(sort: @current_sort)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def can_manage_filters
|
||||
render_403 && return unless can_create_acitivity_filters?
|
||||
end
|
||||
|
||||
def set_sort
|
||||
@current_sort = params[:sort] || 'atoz'
|
||||
end
|
||||
|
||||
def load_filter
|
||||
@filter = ActivityFilter.find_by(id: params[:filter_id])
|
||||
|
||||
render_404 && return unless @filter
|
||||
end
|
||||
|
||||
def load_webhook
|
||||
@webhook = Webhook.find_by(id: params[:id])
|
||||
|
||||
render_404 && return unless @webhook
|
||||
end
|
||||
|
||||
def webhook_params
|
||||
params.require(:webhook).permit(:http_method, :url, :active)
|
||||
end
|
||||
|
||||
def load_filter_elements(filter)
|
||||
result = []
|
||||
filters = filter.filter
|
||||
result += Team.where(id: filters['teams']).pluck(:name) if filters['teams']
|
||||
result += User.where(id: filters['users']).pluck(:full_name) if filters['users']
|
||||
|
||||
if filters['types']
|
||||
result += Activity.type_ofs.select { |_k, v| filters['types'].include?(v.to_s) }
|
||||
.map { |k, _v| I18n.t("global_activities.activity_name.#{k}") }
|
||||
end
|
||||
|
||||
if filters['to_date'] || filters['from_date']
|
||||
result.push("#{t('global_activities.index.period_label')} #{filters['from_date']} - #{filters['to_date']}")
|
||||
end
|
||||
|
||||
filters['subjects']&.each do |subject, ids|
|
||||
result += subject.constantize.where(id: ids).pluck(:name)
|
||||
end
|
||||
|
||||
result
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -23,7 +23,7 @@ module LeftMenuBarHelper
|
|||
end
|
||||
|
||||
def settings_are_selected?
|
||||
controller_name.in? %(registrations preferences addons teams connected_accounts)
|
||||
controller_name.in? %(registrations preferences addons teams connected_accounts webhooks)
|
||||
end
|
||||
|
||||
def activities_are_selected?
|
||||
|
|
|
@ -25,7 +25,7 @@ module NotificationsHelper
|
|||
)
|
||||
|
||||
if target_user.assignments_notification
|
||||
UserNotification.create(notification: notification, user: target_user)
|
||||
notification.create_user_notification(target_user)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -23,6 +23,11 @@ module UserSettingsHelper
|
|||
action_name.in?(%w(index new create show audits_index))
|
||||
end
|
||||
|
||||
def on_settings_webhook_page?
|
||||
controller_name.in?(%w(webhooks)) &&
|
||||
action_name.in?(%w(index))
|
||||
end
|
||||
|
||||
def on_settings_account_connected_accounts_page?
|
||||
controller_name == 'connected_accounts'
|
||||
end
|
||||
|
|
1
app/javascript/packs/application.js
Normal file
1
app/javascript/packs/application.js
Normal file
|
@ -0,0 +1 @@
|
|||
window.bwipjs = require('bwip-js');
|
|
@ -44,7 +44,7 @@ export default {
|
|||
info_label: "Info"
|
||||
},
|
||||
invite_users: {
|
||||
modal_title: "Invite users to team {team}",
|
||||
modal_title: "Invite members to {team}",
|
||||
input_text: "Invite more people to team {team} and start using SciNote.",
|
||||
input_help:
|
||||
"Input one or multiple emails, confirm each email with ENTER key.",
|
||||
|
|
19
app/jobs/activities/dispatch_webhooks_job.rb
Normal file
19
app/jobs/activities/dispatch_webhooks_job.rb
Normal file
|
@ -0,0 +1,19 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Activities
|
||||
class DispatchWebhooksJob < ApplicationJob
|
||||
queue_as :high_priority
|
||||
|
||||
def perform(activity)
|
||||
webhooks =
|
||||
Webhook.active.where(
|
||||
activity_filter_id:
|
||||
Activities::ActivityFilterMatchingService.new(activity).activity_filters.select(:id)
|
||||
)
|
||||
|
||||
webhooks.each do |webhook|
|
||||
Activities::SendWebhookJob.perform_later(webhook, activity)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
13
app/jobs/activities/send_webhook_job.rb
Normal file
13
app/jobs/activities/send_webhook_job.rb
Normal file
|
@ -0,0 +1,13 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Activities
|
||||
class SendWebhookJob < ApplicationJob
|
||||
queue_as :webhooks
|
||||
|
||||
retry_on StandardError, attempts: 3, wait: :exponentially_longer
|
||||
|
||||
def perform(webhook, activity)
|
||||
Activities::ActivityWebhookService.new(webhook, activity).send_webhook
|
||||
end
|
||||
end
|
||||
end
|
|
@ -6,4 +6,14 @@ class ApplicationJob < ActiveJob::Base
|
|||
|
||||
# Most jobs are safe to ignore if the underlying records are no longer available
|
||||
discard_on ActiveJob::DeserializationError
|
||||
|
||||
def self.status(job_id)
|
||||
delayed_job = Delayed::Job.where('handler LIKE ?', "%job_id: #{job_id}%").last
|
||||
|
||||
return :done unless delayed_job
|
||||
return :failed if delayed_job.failed_at
|
||||
return :running if delayed_job.locked_at
|
||||
|
||||
:pending
|
||||
end
|
||||
end
|
||||
|
|
43
app/jobs/label_printers/print_job.rb
Normal file
43
app/jobs/label_printers/print_job.rb
Normal file
|
@ -0,0 +1,43 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module LabelPrinters
|
||||
class PrintJob < ApplicationJob
|
||||
MAX_STATUS_UPDATES = 10
|
||||
|
||||
queue_as :high_priority
|
||||
|
||||
discard_on(StandardError) do |job, _error|
|
||||
label_printer = job.arguments.first
|
||||
label_printer.update!(status: :error)
|
||||
end
|
||||
|
||||
def perform(label_printer, payload, copy_count)
|
||||
case label_printer.type_of
|
||||
when 'fluics'
|
||||
api_client = LabelPrinters::Fluics::ApiClient.new(
|
||||
label_printer.fluics_api_key
|
||||
)
|
||||
|
||||
copy_count.times do
|
||||
response = api_client.print(label_printer.fluics_lid, payload)
|
||||
|
||||
status = response['status'] == 'OK' ? :ready : LabelPrinter::FLUICS_STATUS_MAP[response['printerStatus']]
|
||||
label_printer.update!(status: status)
|
||||
|
||||
break if status != :ready
|
||||
|
||||
# remove first matching job_id from queue (one label out of batch has been printed)
|
||||
label_printer.with_lock do
|
||||
job_ids = label_printer.current_print_job_ids
|
||||
job_ids.delete_at(job_ids.index(job_id) || job_ids.length)
|
||||
|
||||
label_printer.update!(current_print_job_ids: job_ids)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# mark as unreachable if no final state is reached
|
||||
label_printer.update!(status: :unreachable) unless label_printer.status.in? %w(ready out_of_labels error)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,6 +1,17 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Activity < ApplicationRecord
|
||||
ASSIGNMENT_TYPES = %w(
|
||||
assign_user_to_project
|
||||
change_user_role_on_project
|
||||
unassign_user_from_project
|
||||
assign_user_to_module
|
||||
unassign_user_from_module
|
||||
invite_user_to_team
|
||||
remove_user_from_team
|
||||
change_users_role_on_team
|
||||
).freeze
|
||||
|
||||
include ActivityValuesModel
|
||||
include GenerateNotificationModel
|
||||
|
||||
|
@ -63,6 +74,9 @@ class Activity < ApplicationRecord
|
|||
breadcrumbs: {}
|
||||
)
|
||||
|
||||
after_create ->(activity) { Activities::DispatchWebhooksJob.perform_later(activity) },
|
||||
if: -> { Rails.application.config.x.webhooks_enabled }
|
||||
|
||||
def self.activity_types_list
|
||||
activity_list = type_ofs.map do |key, value|
|
||||
[
|
||||
|
@ -145,6 +159,12 @@ class Activity < ApplicationRecord
|
|||
when ProjectFolder
|
||||
breadcrumbs[:project_folder] = subject.name
|
||||
generate_breadcrumb(subject.team)
|
||||
when Step
|
||||
breadcrumbs[:step] = subject.name
|
||||
generate_breadcrumb(subject.protocol)
|
||||
when Asset
|
||||
breadcrumbs[:asset] = subject.blob.filename.to_s
|
||||
generate_breadcrumb(subject.result || subject.step || subject.repository_cell.repository_row.repository)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
8
app/models/activity_filter.rb
Normal file
8
app/models/activity_filter.rb
Normal file
|
@ -0,0 +1,8 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class ActivityFilter < ApplicationRecord
|
||||
validates :name, presence: true
|
||||
validates :filter, presence: true
|
||||
|
||||
has_many :webhooks, dependent: :destroy
|
||||
end
|
|
@ -238,6 +238,10 @@ class Asset < ApplicationRecord
|
|||
file.metadata[:asset_type] == 'marvinjs'
|
||||
end
|
||||
|
||||
def bio_eddie?
|
||||
file.metadata[:asset_type] == 'bio_eddie' || File.extname(file_name) == '.helm'
|
||||
end
|
||||
|
||||
def pdf_preview_ready?
|
||||
return false if pdf_preview_processing
|
||||
|
||||
|
@ -447,6 +451,10 @@ class Asset < ApplicationRecord
|
|||
return convert_variant_to_base64(medium_preview) if style == :medium
|
||||
end
|
||||
|
||||
def my_module
|
||||
(result || step)&.my_module
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def tempdir
|
||||
|
|
|
@ -15,7 +15,7 @@ module GenerateNotificationModel
|
|||
description = generate_notification_description_elements(subject).reverse.join(' | ')
|
||||
|
||||
notification = Notification.create(
|
||||
type_of: :recent_changes,
|
||||
type_of: notification_type,
|
||||
title: sanitize_input(message, %w(strong a)),
|
||||
message: sanitize_input(description, %w(strong a)),
|
||||
generator_user_id: owner.id
|
||||
|
@ -114,4 +114,14 @@ module GenerateNotificationModel
|
|||
def generate_notification
|
||||
CreateNotificationFromActivityJob.perform_later(self) if notifiable?
|
||||
end
|
||||
|
||||
def notification_type
|
||||
return :recent_changes unless instance_of?(Activity)
|
||||
|
||||
if type_of.in? Activity::ASSIGNMENT_TYPES
|
||||
:assignment
|
||||
else
|
||||
:recent_changes
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
41
app/models/label_printer.rb
Normal file
41
app/models/label_printer.rb
Normal file
|
@ -0,0 +1,41 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class LabelPrinter < ApplicationRecord
|
||||
FLUICS_STATUS_MAP = Hash.new(:error).merge(
|
||||
{
|
||||
'00' => :ready,
|
||||
'50' => :busy,
|
||||
'60' => :busy,
|
||||
'01' => :out_of_labels,
|
||||
'02' => :out_of_labels
|
||||
}
|
||||
).freeze
|
||||
|
||||
enum type_of: { fluics: 0 }
|
||||
enum language_type: { zpl: 0 }
|
||||
enum status: { ready: 0, busy: 1, out_of_labels: 2, unreachable: 3, error: 4 }
|
||||
|
||||
validates :name, presence: true
|
||||
validates :type_of, presence: true
|
||||
validates :language_type, presence: true
|
||||
|
||||
def display_name
|
||||
"#{name} • #{description}"
|
||||
end
|
||||
|
||||
def done?
|
||||
current_print_job_ids.blank? && ready?
|
||||
end
|
||||
|
||||
def printing?
|
||||
current_print_job_ids.any? && ready?
|
||||
end
|
||||
|
||||
def printing_status
|
||||
return 'printing' if printing?
|
||||
|
||||
return 'done' if done?
|
||||
|
||||
status
|
||||
end
|
||||
end
|
14
app/models/label_template.rb
Normal file
14
app/models/label_template.rb
Normal file
|
@ -0,0 +1,14 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class LabelTemplate < ApplicationRecord
|
||||
enum language_type: { zpl: 0 }
|
||||
validates :name, presence: true
|
||||
validates :size, presence: true
|
||||
validates :content, presence: true
|
||||
|
||||
def render(locals)
|
||||
locals.reduce(content.dup) do |rendered_content, (key, value)|
|
||||
rendered_content.gsub!(/\{\{#{key}\}\}/, value.to_s)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -192,6 +192,7 @@ class TeamZipExport < ZipExport
|
|||
elements.each_with_index do |element, i|
|
||||
asset = element.asset
|
||||
preview = prepare_preview(asset)
|
||||
bio_eddie = asset.file.metadata[:asset_type] == 'bio_eddie'
|
||||
if type == :step
|
||||
name = "#{directory}/" \
|
||||
"#{append_file_suffix(asset.file_name, "_#{i}_Step#{element.step.position_plus_one}")}"
|
||||
|
@ -199,17 +200,28 @@ class TeamZipExport < ZipExport
|
|||
preview_name = "#{directory}/" \
|
||||
"#{append_file_suffix(preview[:file_name], "_#{i}_Step#{element.step.position_plus_one}_preview")}"
|
||||
end
|
||||
if bio_eddie
|
||||
bio_eddie_name = "#{directory}/" \
|
||||
"#{append_file_suffix("#{asset.file.metadata[:name]}.heml", "_#{i}_Step#{element.step.position_plus_one}")}"
|
||||
end
|
||||
elsif type == :result
|
||||
name = "#{directory}/#{append_file_suffix(asset.file_name, "_#{i}")}"
|
||||
preview_name = "#{directory}/#{append_file_suffix(preview[:file_name], "_#{i}_preview")}" if preview
|
||||
bio_eddie_name = "#{directory}/#{append_file_suffix("#{asset.file.metadata[:name]}.heml", "_#{i}_preview")}" if bio_eddie
|
||||
end
|
||||
|
||||
if asset.file.attached?
|
||||
File.open(name, 'wb') { |f| f.write(asset.file.download) }
|
||||
File.open(preview_name, 'wb') { |f| f.write(preview[:file_data]) } if preview
|
||||
|
||||
if bio_eddie
|
||||
File.open(bio_eddie_name, 'wb') { |f| f.write(asset.file.metadata[:description]) }
|
||||
end
|
||||
end
|
||||
asset_indexes[asset.id] = {
|
||||
file: name,
|
||||
preview: preview_name
|
||||
preview: preview_name,
|
||||
bio_eddie: bio_eddie_name
|
||||
}
|
||||
end
|
||||
asset_indexes
|
||||
|
|
|
@ -46,6 +46,8 @@ class User < ApplicationRecord
|
|||
}
|
||||
}.freeze
|
||||
|
||||
DEFAULT_OTP_DRIFT_TIME_SECONDS = 10
|
||||
|
||||
store_accessor :variables, :export_vars
|
||||
|
||||
default_variables(
|
||||
|
@ -291,6 +293,7 @@ class User < ApplicationRecord
|
|||
foreign_key: :resource_owner_id,
|
||||
dependent: :delete_all
|
||||
|
||||
before_validation :downcase_email!
|
||||
before_destroy :destroy_notifications
|
||||
|
||||
def name
|
||||
|
@ -621,7 +624,10 @@ class User < ApplicationRecord
|
|||
raise StandardError, 'Missing otp_secret' unless otp_secret
|
||||
|
||||
totp = ROTP::TOTP.new(otp_secret, issuer: 'sciNote')
|
||||
totp.verify(otp, drift_behind: 10)
|
||||
totp.verify(
|
||||
otp,
|
||||
drift_behind: ENV.fetch('OTP_DRIFT_TIME_SECONDS', DEFAULT_OTP_DRIFT_TIME_SECONDS).to_i
|
||||
)
|
||||
end
|
||||
|
||||
def assign_2fa_token!
|
||||
|
@ -674,6 +680,12 @@ class User < ApplicationRecord
|
|||
|
||||
private
|
||||
|
||||
def downcase_email!
|
||||
return unless email
|
||||
|
||||
self.email = email.downcase
|
||||
end
|
||||
|
||||
def destroy_notifications
|
||||
# Find all notifications where user is the only reference
|
||||
# on the notification, and destroy all such notifications
|
||||
|
|
27
app/models/webhook.rb
Normal file
27
app/models/webhook.rb
Normal file
|
@ -0,0 +1,27 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Webhook < ApplicationRecord
|
||||
enum http_method: { get: 0, post: 1, patch: 2 }
|
||||
|
||||
belongs_to :activity_filter
|
||||
validates :http_method, presence: true
|
||||
validates :url, presence: true
|
||||
validate :enabled?
|
||||
validate :valid_url
|
||||
|
||||
scope :active, -> { where(active: true) }
|
||||
|
||||
private
|
||||
|
||||
def enabled?
|
||||
unless Rails.application.config.x.webhooks_enabled
|
||||
errors.add(:configuration, I18n.t('activerecord.errors.models.webhook.attributes.configuration.disabled'))
|
||||
end
|
||||
end
|
||||
|
||||
def valid_url
|
||||
unless /\A#{URI::DEFAULT_PARSER.make_regexp(%w(http https))}\z/.match?(url)
|
||||
errors.add(:url, I18n.t('activerecord.errors.models.webhook.attributes.url.not_valid'))
|
||||
end
|
||||
end
|
||||
end
|
|
@ -8,5 +8,13 @@ module Organization
|
|||
can :create_teams do |_|
|
||||
true
|
||||
end
|
||||
|
||||
can :manage_label_printers do |_|
|
||||
true
|
||||
end
|
||||
|
||||
can :create_acitivity_filters do
|
||||
Rails.application.config.x.webhooks_enabled
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -19,6 +19,8 @@ module Api
|
|||
belongs_to :subject, polymorphic: true
|
||||
belongs_to :owner, key: :user, serializer: UserSerializer
|
||||
|
||||
include TimestampableModel
|
||||
|
||||
def message
|
||||
if object.old_activity?
|
||||
object.message
|
||||
|
|
|
@ -9,6 +9,8 @@ module Api
|
|||
attributes :id, :file_name, :file_size, :file_type, :file_url
|
||||
belongs_to :step, serializer: StepSerializer
|
||||
|
||||
include TimestampableModel
|
||||
|
||||
def file_type
|
||||
object.content_type
|
||||
end
|
||||
|
|
|
@ -5,6 +5,8 @@ module Api
|
|||
class ChecklistItemSerializer < ActiveModel::Serializer
|
||||
type :checklist_items
|
||||
attributes :id, :text, :checked, :position
|
||||
|
||||
include TimestampableModel
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -6,6 +6,8 @@ module Api
|
|||
type :checklists
|
||||
attributes :id, :name
|
||||
has_many :checklist_items, serializer: ChecklistItemSerializer
|
||||
|
||||
include TimestampableModel
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -26,6 +26,8 @@ module Api
|
|||
serializer: ResultSerializer,
|
||||
if: -> { object.class == ResultComment &&
|
||||
!instance_options[:hide_result] }
|
||||
|
||||
include TimestampableModel
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -11,6 +11,8 @@ module Api
|
|||
belongs_to :to, key: :output_task,
|
||||
serializer: TaskSerializer,
|
||||
class_name: 'MyModule'
|
||||
|
||||
include TimestampableModel
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -5,6 +5,8 @@ module Api
|
|||
class ExperimentSerializer < ActiveModel::Serializer
|
||||
type :experiments
|
||||
attributes :id, :name, :description, :archived
|
||||
|
||||
include TimestampableModel
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -7,6 +7,8 @@ module Api
|
|||
attributes :id, :value_type, :value
|
||||
attribute :repository_column_id, key: :column_id
|
||||
|
||||
include TimestampableModel
|
||||
|
||||
def value
|
||||
ActiveModelSerializers::SerializableResource.new(
|
||||
object.value,
|
||||
|
|
|
@ -5,6 +5,8 @@ module Api
|
|||
class InventoryChecklistItemSerializer < ActiveModel::Serializer
|
||||
type :inventory_checklist_items
|
||||
attributes :id, :data
|
||||
|
||||
include TimestampableModel
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -30,6 +30,8 @@ module Api
|
|||
!instance_options[:hide_list_items]
|
||||
end)
|
||||
|
||||
include TimestampableModel
|
||||
|
||||
def data_type
|
||||
Extends::API_REPOSITORY_DATA_TYPE_MAPPINGS[object.data_type]
|
||||
end
|
||||
|
|
|
@ -13,6 +13,8 @@ module Api
|
|||
serializer: InventorySerializer,
|
||||
class_name: 'Repository',
|
||||
if: -> { instance_options[:show_repository] }
|
||||
|
||||
include TimestampableModel
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -5,6 +5,8 @@ module Api
|
|||
class InventoryListItemSerializer < ActiveModel::Serializer
|
||||
type :inventory_list_items
|
||||
attribute :data
|
||||
|
||||
include TimestampableModel
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -6,6 +6,8 @@ module Api
|
|||
type :inventories
|
||||
attributes :id, :name
|
||||
belongs_to :created_by, serializer: UserSerializer
|
||||
|
||||
include TimestampableModel
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -5,6 +5,8 @@ module Api
|
|||
class InventoryStatusItemSerializer < ActiveModel::Serializer
|
||||
type :inventory_status_items
|
||||
attributes :status, :icon
|
||||
|
||||
include TimestampableModel
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -10,6 +10,8 @@ module Api
|
|||
belongs_to :parent_folder, serializer: ProjectFolderSerializer
|
||||
has_many :projects, serializer: ProjectSerializer
|
||||
has_many :project_folders, serializer: ProjectFolderSerializer
|
||||
|
||||
include TimestampableModel
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -7,6 +7,9 @@ module Api
|
|||
attributes :name, :visibility, :start_date, :archived
|
||||
|
||||
belongs_to :project_folder, serializer: ProjectFolderSerializer
|
||||
has_many :project_comments, key: :comments, serializer: CommentSerializer
|
||||
|
||||
include TimestampableModel
|
||||
|
||||
def start_date
|
||||
object.created_at
|
||||
|
|
|
@ -5,6 +5,8 @@ module Api
|
|||
class ProtocolKeywordSerializer < ActiveModel::Serializer
|
||||
type :protocol_keywords
|
||||
attributes :id, :name
|
||||
|
||||
include TimestampableModel
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -17,6 +17,8 @@ module Api
|
|||
has_many :steps, serializer: StepSerializer, if: -> { object.steps.any? }
|
||||
belongs_to :parent, serializer: ProtocolSerializer, if: -> { object.parent.present? }
|
||||
|
||||
include TimestampableModel
|
||||
|
||||
def description
|
||||
if instance_options[:rte_rendering]
|
||||
custom_auto_link(object.tinymce_render(:description),
|
||||
|
|
|
@ -15,6 +15,8 @@ module Api
|
|||
belongs_to :project, serializer: ProjectSerializer,
|
||||
unless: -> { instance_options[:hide_project] }
|
||||
|
||||
include TimestampableModel
|
||||
|
||||
def pdf_file_size
|
||||
object.pdf_file.blob.byte_size
|
||||
end
|
||||
|
|
|
@ -5,6 +5,8 @@ module Api
|
|||
class RepositoryAssetValueSerializer < ActiveModel::Serializer
|
||||
attributes :file_id, :file_name, :file_size, :url
|
||||
|
||||
include TimestampableModel
|
||||
|
||||
def file_id
|
||||
object.asset&.id
|
||||
end
|
||||
|
|
|
@ -9,6 +9,8 @@ module Api
|
|||
attribute :inventory_checklist_item_names do
|
||||
object.repository_checklist_items.pluck(:data)
|
||||
end
|
||||
|
||||
include TimestampableModel
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -5,6 +5,8 @@ module Api
|
|||
class RepositoryDateRangeValueSerializer < ActiveModel::Serializer
|
||||
attribute :date_range
|
||||
|
||||
include TimestampableModel
|
||||
|
||||
def date_range
|
||||
{
|
||||
from: object.start_time.to_date,
|
||||
|
|
|
@ -5,6 +5,8 @@ module Api
|
|||
class RepositoryDateTimeRangeValueSerializer < ActiveModel::Serializer
|
||||
attribute :date_time_range
|
||||
|
||||
include TimestampableModel
|
||||
|
||||
def date_time_range
|
||||
{
|
||||
from: object.start_time,
|
||||
|
|
|
@ -5,6 +5,8 @@ module Api
|
|||
class RepositoryDateTimeValueSerializer < ActiveModel::Serializer
|
||||
attribute :date_time
|
||||
|
||||
include TimestampableModel
|
||||
|
||||
def date_time
|
||||
object.data
|
||||
end
|
||||
|
|
|
@ -5,6 +5,8 @@ module Api
|
|||
class RepositoryDateValueSerializer < ActiveModel::Serializer
|
||||
attribute :date
|
||||
|
||||
include TimestampableModel
|
||||
|
||||
def date
|
||||
object.data.to_date
|
||||
end
|
||||
|
|
|
@ -5,6 +5,8 @@ module Api
|
|||
class RepositoryListValueSerializer < ActiveModel::Serializer
|
||||
attribute :repository_list_item_id, key: :inventory_list_item_id
|
||||
attribute :formatted, key: :inventory_list_item_name
|
||||
|
||||
include TimestampableModel
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue