mirror of
https://github.com/scinote-eln/scinote-web.git
synced 2024-09-20 14:45:56 +08:00
Merge branch 'develop' into features/ui-revamp
This commit is contained in:
commit
d795f996f2
152
Gemfile.lock
152
Gemfile.lock
|
@ -68,40 +68,40 @@ GIT
|
|||
GEM
|
||||
remote: http://rubygems.org/
|
||||
specs:
|
||||
actioncable (6.1.7.1)
|
||||
actionpack (= 6.1.7.1)
|
||||
activesupport (= 6.1.7.1)
|
||||
actioncable (6.1.7.3)
|
||||
actionpack (= 6.1.7.3)
|
||||
activesupport (= 6.1.7.3)
|
||||
nio4r (~> 2.0)
|
||||
websocket-driver (>= 0.6.1)
|
||||
actionmailbox (6.1.7.1)
|
||||
actionpack (= 6.1.7.1)
|
||||
activejob (= 6.1.7.1)
|
||||
activerecord (= 6.1.7.1)
|
||||
activestorage (= 6.1.7.1)
|
||||
activesupport (= 6.1.7.1)
|
||||
actionmailbox (6.1.7.3)
|
||||
actionpack (= 6.1.7.3)
|
||||
activejob (= 6.1.7.3)
|
||||
activerecord (= 6.1.7.3)
|
||||
activestorage (= 6.1.7.3)
|
||||
activesupport (= 6.1.7.3)
|
||||
mail (>= 2.7.1)
|
||||
actionmailer (6.1.7.1)
|
||||
actionpack (= 6.1.7.1)
|
||||
actionview (= 6.1.7.1)
|
||||
activejob (= 6.1.7.1)
|
||||
activesupport (= 6.1.7.1)
|
||||
actionmailer (6.1.7.3)
|
||||
actionpack (= 6.1.7.3)
|
||||
actionview (= 6.1.7.3)
|
||||
activejob (= 6.1.7.3)
|
||||
activesupport (= 6.1.7.3)
|
||||
mail (~> 2.5, >= 2.5.4)
|
||||
rails-dom-testing (~> 2.0)
|
||||
actionpack (6.1.7.1)
|
||||
actionview (= 6.1.7.1)
|
||||
activesupport (= 6.1.7.1)
|
||||
actionpack (6.1.7.3)
|
||||
actionview (= 6.1.7.3)
|
||||
activesupport (= 6.1.7.3)
|
||||
rack (~> 2.0, >= 2.0.9)
|
||||
rack-test (>= 0.6.3)
|
||||
rails-dom-testing (~> 2.0)
|
||||
rails-html-sanitizer (~> 1.0, >= 1.2.0)
|
||||
actiontext (6.1.7.1)
|
||||
actionpack (= 6.1.7.1)
|
||||
activerecord (= 6.1.7.1)
|
||||
activestorage (= 6.1.7.1)
|
||||
activesupport (= 6.1.7.1)
|
||||
actiontext (6.1.7.3)
|
||||
actionpack (= 6.1.7.3)
|
||||
activerecord (= 6.1.7.3)
|
||||
activestorage (= 6.1.7.3)
|
||||
activesupport (= 6.1.7.3)
|
||||
nokogiri (>= 1.8.5)
|
||||
actionview (6.1.7.1)
|
||||
activesupport (= 6.1.7.1)
|
||||
actionview (6.1.7.3)
|
||||
activesupport (= 6.1.7.3)
|
||||
builder (~> 3.1)
|
||||
erubi (~> 1.4)
|
||||
rails-dom-testing (~> 2.0)
|
||||
|
@ -111,24 +111,24 @@ GEM
|
|||
activemodel (>= 4.1, < 6.2)
|
||||
case_transform (>= 0.2)
|
||||
jsonapi-renderer (>= 0.1.1.beta1, < 0.3)
|
||||
activejob (6.1.7.1)
|
||||
activesupport (= 6.1.7.1)
|
||||
activejob (6.1.7.3)
|
||||
activesupport (= 6.1.7.3)
|
||||
globalid (>= 0.3.6)
|
||||
activemodel (6.1.7.1)
|
||||
activesupport (= 6.1.7.1)
|
||||
activerecord (6.1.7.1)
|
||||
activemodel (= 6.1.7.1)
|
||||
activesupport (= 6.1.7.1)
|
||||
activemodel (6.1.7.3)
|
||||
activesupport (= 6.1.7.3)
|
||||
activerecord (6.1.7.3)
|
||||
activemodel (= 6.1.7.3)
|
||||
activesupport (= 6.1.7.3)
|
||||
activerecord-import (1.0.7)
|
||||
activerecord (>= 3.2)
|
||||
activestorage (6.1.7.1)
|
||||
actionpack (= 6.1.7.1)
|
||||
activejob (= 6.1.7.1)
|
||||
activerecord (= 6.1.7.1)
|
||||
activesupport (= 6.1.7.1)
|
||||
activestorage (6.1.7.3)
|
||||
actionpack (= 6.1.7.3)
|
||||
activejob (= 6.1.7.3)
|
||||
activerecord (= 6.1.7.3)
|
||||
activesupport (= 6.1.7.3)
|
||||
marcel (~> 1.0)
|
||||
mini_mime (>= 1.1.0)
|
||||
activesupport (6.1.7.1)
|
||||
activesupport (6.1.7.3)
|
||||
concurrent-ruby (~> 1.0, >= 1.0.2)
|
||||
i18n (>= 1.6, < 2)
|
||||
minitest (>= 5.1)
|
||||
|
@ -235,7 +235,7 @@ GEM
|
|||
coffee-script-source
|
||||
execjs
|
||||
coffee-script-source (1.12.2)
|
||||
concurrent-ruby (1.1.10)
|
||||
concurrent-ruby (1.2.2)
|
||||
crack (0.4.5)
|
||||
rexml
|
||||
crass (1.0.6)
|
||||
|
@ -293,7 +293,7 @@ GEM
|
|||
discard (1.2.0)
|
||||
activerecord (>= 4.2, < 7)
|
||||
docile (1.3.5)
|
||||
doorkeeper (5.4.0)
|
||||
doorkeeper (5.6.6)
|
||||
railties (>= 5)
|
||||
down (5.2.0)
|
||||
addressable (~> 2.5)
|
||||
|
@ -321,7 +321,7 @@ GEM
|
|||
raabro (~> 1.4)
|
||||
generator (0.0.1)
|
||||
gherkin (5.1.0)
|
||||
globalid (1.0.1)
|
||||
globalid (1.1.0)
|
||||
activesupport (>= 5.0)
|
||||
graphviz (1.2.1)
|
||||
process-pipeline
|
||||
|
@ -331,7 +331,7 @@ GEM
|
|||
httparty (0.21.0)
|
||||
mini_mime (>= 1.0.0)
|
||||
multi_xml (>= 0.5.2)
|
||||
i18n (1.12.0)
|
||||
i18n (1.14.1)
|
||||
concurrent-ruby (~> 1.0)
|
||||
i18n-js (3.8.0)
|
||||
i18n (>= 0.6.6)
|
||||
|
@ -382,10 +382,10 @@ GEM
|
|||
logging (2.0.0)
|
||||
little-plugger (~> 1.1)
|
||||
multi_json (~> 1.10)
|
||||
loofah (2.19.1)
|
||||
loofah (2.21.3)
|
||||
crass (~> 1.0.2)
|
||||
nokogiri (>= 1.5.9)
|
||||
mail (2.8.0.1)
|
||||
nokogiri (>= 1.12.0)
|
||||
mail (2.8.1)
|
||||
mini_mime (>= 0.1.1)
|
||||
net-imap
|
||||
net-pop
|
||||
|
@ -397,7 +397,8 @@ GEM
|
|||
mime-types-data (3.2022.0105)
|
||||
mini_magick (4.11.0)
|
||||
mini_mime (1.1.2)
|
||||
minitest (5.17.0)
|
||||
mini_portile2 (2.8.2)
|
||||
minitest (5.18.0)
|
||||
momentjs-rails (2.17.1)
|
||||
railties (>= 3.1)
|
||||
msgpack (1.4.2)
|
||||
|
@ -418,8 +419,11 @@ GEM
|
|||
net-smtp (0.3.3)
|
||||
net-protocol
|
||||
newrelic_rpm (8.16.0)
|
||||
nio4r (2.5.8)
|
||||
nokogiri (1.14.3-x86_64-linux)
|
||||
nio4r (2.5.9)
|
||||
nokogiri (1.14.5)
|
||||
mini_portile2 (~> 2.8.0)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.14.5-x86_64-linux)
|
||||
racc (~> 1.4)
|
||||
oauth2 (2.0.9)
|
||||
faraday (>= 0.17.3, < 3.0)
|
||||
|
@ -473,30 +477,30 @@ GEM
|
|||
puma (5.6.4)
|
||||
nio4r (~> 2.0)
|
||||
raabro (1.4.0)
|
||||
racc (1.6.2)
|
||||
rack (2.2.6.4)
|
||||
racc (1.7.0)
|
||||
rack (2.2.7)
|
||||
rack-attack (6.4.0)
|
||||
rack (>= 1.0, < 3)
|
||||
rack-cors (1.1.1)
|
||||
rack (>= 2.0.0)
|
||||
rack-protection (3.0.1)
|
||||
rack
|
||||
rack-test (2.0.2)
|
||||
rack-test (2.1.0)
|
||||
rack (>= 1.3)
|
||||
rails (6.1.7.1)
|
||||
actioncable (= 6.1.7.1)
|
||||
actionmailbox (= 6.1.7.1)
|
||||
actionmailer (= 6.1.7.1)
|
||||
actionpack (= 6.1.7.1)
|
||||
actiontext (= 6.1.7.1)
|
||||
actionview (= 6.1.7.1)
|
||||
activejob (= 6.1.7.1)
|
||||
activemodel (= 6.1.7.1)
|
||||
activerecord (= 6.1.7.1)
|
||||
activestorage (= 6.1.7.1)
|
||||
activesupport (= 6.1.7.1)
|
||||
rails (6.1.7.3)
|
||||
actioncable (= 6.1.7.3)
|
||||
actionmailbox (= 6.1.7.3)
|
||||
actionmailer (= 6.1.7.3)
|
||||
actionpack (= 6.1.7.3)
|
||||
actiontext (= 6.1.7.3)
|
||||
actionview (= 6.1.7.3)
|
||||
activejob (= 6.1.7.3)
|
||||
activemodel (= 6.1.7.3)
|
||||
activerecord (= 6.1.7.3)
|
||||
activestorage (= 6.1.7.3)
|
||||
activesupport (= 6.1.7.3)
|
||||
bundler (>= 1.15.0)
|
||||
railties (= 6.1.7.1)
|
||||
railties (= 6.1.7.3)
|
||||
sprockets-rails (>= 2.0.0)
|
||||
rails-controller-testing (1.0.5)
|
||||
actionpack (>= 5.0.1.rc1)
|
||||
|
@ -505,8 +509,9 @@ GEM
|
|||
rails-dom-testing (2.0.3)
|
||||
activesupport (>= 4.2.0)
|
||||
nokogiri (>= 1.6)
|
||||
rails-html-sanitizer (1.4.4)
|
||||
loofah (~> 2.19, >= 2.19.1)
|
||||
rails-html-sanitizer (1.6.0)
|
||||
loofah (~> 2.21)
|
||||
nokogiri (~> 1.14)
|
||||
rails_12factor (0.0.3)
|
||||
rails_serve_static_assets
|
||||
rails_stdout_logging
|
||||
|
@ -514,9 +519,9 @@ GEM
|
|||
rails (> 3.1)
|
||||
rails_serve_static_assets (0.0.5)
|
||||
rails_stdout_logging (0.0.5)
|
||||
railties (6.1.7.1)
|
||||
actionpack (= 6.1.7.1)
|
||||
activesupport (= 6.1.7.1)
|
||||
railties (6.1.7.3)
|
||||
actionpack (= 6.1.7.3)
|
||||
activesupport (= 6.1.7.3)
|
||||
method_source
|
||||
rake (>= 12.2)
|
||||
thor (~> 1.0)
|
||||
|
@ -625,14 +630,16 @@ GEM
|
|||
generator
|
||||
tailwindcss-rails (2.0.27)
|
||||
railties (>= 6.0.0)
|
||||
thor (1.2.1)
|
||||
tailwindcss-rails (2.0.27-x86_64-linux)
|
||||
railties (>= 6.0.0)
|
||||
thor (1.2.2)
|
||||
tilt (2.0.10)
|
||||
timecop (0.9.2)
|
||||
timeout (0.3.1)
|
||||
timeout (0.3.2)
|
||||
turbolinks (5.1.1)
|
||||
turbolinks-source (~> 5.1)
|
||||
turbolinks-source (5.2.0)
|
||||
tzinfo (2.0.5)
|
||||
tzinfo (2.0.6)
|
||||
concurrent-ruby (~> 1.0)
|
||||
uglifier (4.2.0)
|
||||
execjs (>= 0.3.0, < 3)
|
||||
|
@ -656,11 +663,12 @@ GEM
|
|||
activesupport
|
||||
xpath (3.2.0)
|
||||
nokogiri (~> 1.8)
|
||||
zeitwerk (2.6.6)
|
||||
zeitwerk (2.6.8)
|
||||
zip-zip (0.3)
|
||||
rubyzip (>= 1.0.0)
|
||||
|
||||
PLATFORMS
|
||||
ruby
|
||||
x86_64-linux
|
||||
|
||||
DEPENDENCIES
|
||||
|
|
|
@ -43,9 +43,6 @@
|
|||
// forms with clicking on links outside form in cases when other than
|
||||
// GET method is used.
|
||||
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
var formatJS = $('body').data('datetime-picker-format-date-only');
|
||||
|
||||
function initFormSubmitLinks(el) {
|
||||
|
||||
el = el || $(document.body);
|
||||
|
|
|
@ -83,6 +83,8 @@ var DasboardCalendarWidget = (function() {
|
|||
};
|
||||
}());
|
||||
|
||||
var formatJS;
|
||||
$(document).on('turbolinks:load', function() {
|
||||
DasboardCalendarWidget.init();
|
||||
formatJS = $('body').data('datetime-picker-format');
|
||||
});
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
/* global I18n formatJS */
|
||||
/* global I18n */
|
||||
(function() {
|
||||
$('.datetime-picker-container').each(function() {
|
||||
const id = $(this).data('id');
|
||||
if (id) {
|
||||
const dt = $(`#calendar-${id}`);
|
||||
const useCurrent = $(this).data('use-current');
|
||||
const formatJS = $(this).data('datetime-picker-format');
|
||||
dt.datetimepicker({
|
||||
useCurrent, ignoreReadonly: true, locale: I18n.locale, format: formatJS
|
||||
});
|
||||
|
|
|
@ -783,10 +783,9 @@ var MyModuleRepositories = (function() {
|
|||
SELECTED_ROWS = {};
|
||||
$(FULL_VIEW_TABLE.table().container()).find('.dataTable')
|
||||
.attr('data-assigned-items-count', data.rows_count);
|
||||
FULL_VIEW_TABLE.ajax.reload(null, false);
|
||||
reloadRepositoriesList(data.repository_id, true);
|
||||
updateFullViewRowsCount(data.rows_count);
|
||||
renderFullViewAssignButtons();
|
||||
FULL_VIEW_MODAL.modal('hide');
|
||||
},
|
||||
error: function(response) {
|
||||
if (response.status === 403) {
|
||||
|
|
|
@ -76,17 +76,25 @@ var ProjectsIndex = (function() {
|
|||
|
||||
// Modal's submit handler function
|
||||
$(projectsWrapper)
|
||||
.on('ajax:success', newProjectModal, function(ev, data) {
|
||||
.on('submit', '#new_project', function() {
|
||||
$('#project_name').prop('disabled', true);
|
||||
$('#new-project-modal button[type="submit"]').prop('disabled', true);
|
||||
})
|
||||
.on('ajax:complete', '#new_project', function() {
|
||||
$('#project_name').prop('disabled', false);
|
||||
$('#new-project-modal button[type="submit"]').prop('disabled', false);
|
||||
})
|
||||
.on('ajax:success', newProjectModal, function(_ev, data) {
|
||||
$(newProjectModal).modal('hide');
|
||||
HelperModule.flashAlertMsg(data.message, 'success');
|
||||
refreshCurrentView();
|
||||
})
|
||||
.on('ajax:error', newProjectModal, function(ev, data) {
|
||||
.on('ajax:error', newProjectModal, function(_ev, data) {
|
||||
$(this).renderFormErrors('project', data.responseJSON);
|
||||
});
|
||||
|
||||
$(projectsWrapper)
|
||||
.on('ajax:success', '.new-project-btn', function(ev, data) {
|
||||
.on('ajax:success', '.new-project-btn', function(_ev, data) {
|
||||
// Add and show modal
|
||||
$(projectsWrapper).append($.parseHTML(data.html));
|
||||
$(newProjectModal).modal('show');
|
||||
|
|
|
@ -1065,9 +1065,15 @@ function reportHandsonTableConverter() {
|
|||
type: 'POST',
|
||||
data: JSON.stringify(getReportData()),
|
||||
contentType: 'application/json; charset=utf-8',
|
||||
beforeSend: function() {
|
||||
$('.generate-button').prop('disabled', true);
|
||||
},
|
||||
success: function() {},
|
||||
error: function(jqxhr) {
|
||||
HelperModule.flashAlertMsg(jqxhr.responseJSON.join(' '), 'danger');
|
||||
},
|
||||
ajaxComplete: function() {
|
||||
$('.generate-button').prop('disabled', false);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
|
@ -115,25 +115,35 @@ $.fn.dataTable.render.editRepositoryChecklistValue = function(formId, columnId,
|
|||
};
|
||||
|
||||
$.fn.dataTable.render.editRepositoryNumberValue = function(formId, columnId, cell, $header) {
|
||||
let $cell = $(cell.node());
|
||||
let decimals = $header.data('metadata-decimals');
|
||||
const $cell = $(cell.node());
|
||||
const decimals = $header.data('metadata-decimals');
|
||||
let number = $cell.find('.number-value').data('value');
|
||||
|
||||
if (!number) number = '';
|
||||
|
||||
$cell.html(`
|
||||
<div class="sci-input-container text-field error-icon">
|
||||
<input class="sci-input-field"
|
||||
form="${formId}"
|
||||
type="text"
|
||||
oninput="regexp = ${decimals} === 0 ? /[^0-9]/g : /[^0-9.]/g
|
||||
this.value = this.value.replace(regexp, '');
|
||||
this.value = this.value.match(/^\\d*(\\.\\d{0,${decimals}})?/)[0];"
|
||||
name="repository_cells[${columnId}]"
|
||||
placeholder="${I18n.t('repositories.table.number.enter_number')}"
|
||||
value="${number}"
|
||||
data-type="RepositoryNumberValue">
|
||||
</div>`);
|
||||
let $input = $('<input>', {
|
||||
class: 'sci-input-field',
|
||||
form: formId,
|
||||
type: 'text',
|
||||
name: 'repository_cells[' + columnId + ']',
|
||||
placeholder: I18n.t('repositories.table.number.enter_number'),
|
||||
value: number,
|
||||
'data-type': 'RepositoryNumberValue'
|
||||
});
|
||||
|
||||
$input.on('input', function() {
|
||||
const regexp = decimals === 0 ? /[^0-9]/g : /[^0-9.]/g;
|
||||
const decimalsRegex = new RegExp(`^\\d*(\\.\\d{0,${decimals}})?`);
|
||||
let value = this.value;
|
||||
value = value.replace(regexp, '');
|
||||
value = value.match(decimalsRegex)[0];
|
||||
this.value = value;
|
||||
});
|
||||
|
||||
let $div = $('<div>', {
|
||||
class: 'sci-input-container text-field error-icon'
|
||||
}).append($input);
|
||||
|
||||
$cell.html($div);
|
||||
};
|
||||
|
||||
$.fn.dataTable.render.editRepositoryStockValue = function(formId, columnId, cell) {
|
||||
|
|
|
@ -50,20 +50,31 @@ $.fn.dataTable.render.newRepositoryChecklistValue = function(formId, columnId, $
|
|||
};
|
||||
|
||||
$.fn.dataTable.render.newRepositoryNumberValue = function(formId, columnId, $cell, $header) {
|
||||
let decimals = $header.data('metadata-decimals');
|
||||
const decimals = $header.data('metadata-decimals');
|
||||
|
||||
$cell.html(`
|
||||
<div class="sci-input-container text-field error-icon">
|
||||
<input class="sci-input-field"
|
||||
form="${formId}"
|
||||
type="text"
|
||||
oninput="this.value = this.value.replace(/[^0-9.]/g, '');
|
||||
this.value = this.value.match(/^\\d*(\\.\\d{0,${decimals}})?/)[0];"
|
||||
name="repository_cells[${columnId}]"
|
||||
value=""
|
||||
placeholder="${I18n.t('repositories.table.number.enter_number')}"
|
||||
data-type="RepositoryNumberValue">
|
||||
</div>`);
|
||||
let $input = $('<input>', {
|
||||
class: 'sci-input-field',
|
||||
form: formId,
|
||||
type: 'text',
|
||||
name: 'repository_cells[' + columnId + ']',
|
||||
value: '',
|
||||
placeholder: I18n.t('repositories.table.number.enter_number'),
|
||||
'data-type': 'RepositoryNumberValue'
|
||||
});
|
||||
|
||||
$input.on('input', function() {
|
||||
const decimalsRegex = new RegExp(`^\\d*(\\.\\d{0,${decimals}})?`);
|
||||
let value = this.value;
|
||||
value = value.replace(/[^0-9.]/g, '');
|
||||
value = value.match(decimalsRegex)[0];
|
||||
this.value = value;
|
||||
});
|
||||
|
||||
let $div = $('<div>', {
|
||||
class: 'sci-input-container text-field error-icon'
|
||||
}).append($input);
|
||||
|
||||
$cell.html($div);
|
||||
};
|
||||
|
||||
$.fn.dataTable.render.newRepositoryDateTimeValue = function(formId, columnId, $cell) {
|
||||
|
|
|
@ -26,6 +26,19 @@
|
|||
animateSpinner(null, false);
|
||||
ShareModal.init();
|
||||
}
|
||||
if (['rename-repo-modal', 'copy-repo-modal'].includes($(this).attr('id'))) {
|
||||
$(this).find('form')
|
||||
.on('ajax:success', function(_e, data) {
|
||||
if (data.url) {
|
||||
window.location = data.url;
|
||||
} else {
|
||||
window.location.reload();
|
||||
}
|
||||
})
|
||||
.on('ajax:error', function(_e, data) {
|
||||
$(this).renderFormErrors('repository', data.responseJSON);
|
||||
});
|
||||
}
|
||||
$(this).find('.selectpicker').selectpicker();
|
||||
})
|
||||
.on('hidden.bs.modal', function() {
|
||||
|
|
|
@ -716,7 +716,9 @@ var dropdownSelector = (function() {
|
|||
} else {
|
||||
// Or delete specific one
|
||||
deleteValue(selector, container, tagLabel.data('ds-tag-id'), tagLabel.data('ds-tag-group'));
|
||||
removeOptionFromSelector(selector, tagLabel.data('ds-tag-id'));
|
||||
if (selector.data('config').tagClass) {
|
||||
removeOptionFromSelector(selector, tagLabel.data('ds-tag-id'));
|
||||
}
|
||||
}
|
||||
}, 350);
|
||||
}
|
||||
|
@ -1010,7 +1012,9 @@ var dropdownSelector = (function() {
|
|||
currentData = getCurrentData($(selector).next());
|
||||
currentData.push(value);
|
||||
setData($(selector), currentData, skip_event);
|
||||
appendOptionToSelector(selector, value);
|
||||
if (selector.data('config').tagClass) {
|
||||
appendOptionToSelector(selector, value);
|
||||
}
|
||||
|
||||
return this;
|
||||
},
|
||||
|
|
|
@ -2,22 +2,34 @@
|
|||
.menu-item:not(.active):hover {
|
||||
background-color: var(--sn-super-light-grey);
|
||||
|
||||
a:not(.no-hover) {
|
||||
:not(.no-hover) {
|
||||
color: var(--sn-blue);
|
||||
}
|
||||
|
||||
a.no-hover {
|
||||
.no-hover {
|
||||
color: var(--sn-science-blue-hover);
|
||||
}
|
||||
}
|
||||
|
||||
.menu-item.active:hover {
|
||||
a:not(.no-hover) {
|
||||
:not(.no-hover) {
|
||||
color: var(--sn-blue);
|
||||
}
|
||||
|
||||
a.no-hover {
|
||||
.no-hover {
|
||||
color: var(--sn-science-blue-hover);
|
||||
}
|
||||
}
|
||||
|
||||
.menu-item,
|
||||
.menu-item:not(.active):hover,
|
||||
.menu-item.active:hover {
|
||||
.disabled-link,
|
||||
.disabled-link:not(.no-hover):hover,
|
||||
.disabled-link.no-hover:hover {
|
||||
color: var(--sn-grey);
|
||||
pointer-events: none;
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -747,6 +747,16 @@ li.module-hover {
|
|||
|
||||
a {
|
||||
color: inherit;
|
||||
|
||||
&.disabled-link {
|
||||
color: var(--sn-grey);
|
||||
pointer-events: none;
|
||||
text-decoration: none;
|
||||
|
||||
.name {
|
||||
color: var(--sn-grey);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.name {
|
||||
|
@ -872,7 +882,7 @@ li.module-hover {
|
|||
animation-duration: 2s;
|
||||
animation-iteration-count: infinite;
|
||||
animation-name: placeholder-pulsing;
|
||||
background-color: $color-alto;
|
||||
background-color: var(--sn-sleepy-grey);
|
||||
border-radius: $border-radius-default;
|
||||
height: 18px;
|
||||
|
||||
|
|
|
@ -447,22 +447,22 @@ class ExperimentsController < ApplicationController
|
|||
end
|
||||
|
||||
def inventory_assigning_experiment_filter
|
||||
readable_experiments = Experiment.readable_by_user(current_user)
|
||||
viewable_experiments = Experiment.viewable_by_user(current_user, current_team)
|
||||
assignable_my_modules = MyModule.repository_row_assignable_by_user(current_user)
|
||||
|
||||
project = Project.readable_by_user(current_user)
|
||||
project = Project.viewable_by_user(current_user, current_team)
|
||||
.joins(experiments: :my_modules)
|
||||
.where(experiments: { id: readable_experiments })
|
||||
.where(experiments: { id: viewable_experiments })
|
||||
.where(my_modules: { id: assignable_my_modules })
|
||||
.find_by(id: params[:project_id])
|
||||
|
||||
return render_404 if project.blank?
|
||||
|
||||
experiments = project.experiments
|
||||
.active
|
||||
.joins(:my_modules)
|
||||
.where(experiments: { id: readable_experiments })
|
||||
.where(experiments: { id: viewable_experiments })
|
||||
.where(my_modules: { id: assignable_my_modules })
|
||||
.search(current_user, false, params[:query], 1, current_team)
|
||||
.distinct
|
||||
.pluck(:id, :name)
|
||||
|
||||
|
|
|
@ -452,12 +452,12 @@ class MyModulesController < ApplicationController
|
|||
end
|
||||
|
||||
def inventory_assigning_my_module_filter
|
||||
readable_experiments = Experiment.readable_by_user(current_user)
|
||||
viewable_experiments = Experiment.viewable_by_user(current_user, current_team)
|
||||
assignable_my_modules = MyModule.repository_row_assignable_by_user(current_user)
|
||||
|
||||
experiment = Experiment.readable_by_user(current_user)
|
||||
experiment = Experiment.viewable_by_user(current_user, current_team)
|
||||
.joins(:my_modules)
|
||||
.where(experiments: { id: readable_experiments })
|
||||
.where(experiments: { id: viewable_experiments })
|
||||
.where(my_modules: { id: assignable_my_modules })
|
||||
.find_by(id: params[:experiment_id])
|
||||
|
||||
|
@ -465,8 +465,6 @@ class MyModulesController < ApplicationController
|
|||
|
||||
my_modules = experiment.my_modules
|
||||
.where(my_modules: { id: assignable_my_modules })
|
||||
.distinct
|
||||
.search(current_user, false, params[:query], 1, current_team)
|
||||
.pluck(:id, :name)
|
||||
|
||||
return render plain: [].to_json if my_modules.blank?
|
||||
|
|
|
@ -12,7 +12,8 @@ module Navigator
|
|||
archived: project.archived,
|
||||
type: :project,
|
||||
has_children: project.has_children,
|
||||
children_url: navigator_project_path(project)
|
||||
children_url: navigator_project_path(project),
|
||||
disabled: project.disabled
|
||||
}
|
||||
end
|
||||
|
||||
|
@ -69,25 +70,35 @@ module Navigator
|
|||
(projects.archived IS TRUE AND experiments.id IS NOT NULL)
|
||||
THEN 1 ELSE 0 END) > 0 AS has_children'
|
||||
end
|
||||
disabled_sql = 'SUM(CASE WHEN project_user_roles IS NULL THEN 0 ELSE 1 END) < 1 AS disabled'
|
||||
|
||||
current_team.projects
|
||||
.where(project_folder_id: folder)
|
||||
.visible_to(current_user, current_team)
|
||||
.with_children_viewable_by_user(current_user)
|
||||
.where('
|
||||
projects.archived = :archived OR
|
||||
(
|
||||
(
|
||||
experiments.archived = :archived OR
|
||||
my_modules.archived = :archived
|
||||
) AND
|
||||
:archived IS TRUE
|
||||
) OR
|
||||
projects.id = :project_id
|
||||
', archived: archived, project_id: project&.id || -1)
|
||||
.joins("LEFT OUTER JOIN user_assignments project_user_assignments
|
||||
ON project_user_assignments.assignable_type = 'Project'
|
||||
AND project_user_assignments.assignable_id = projects.id
|
||||
AND project_user_assignments.user_id = #{current_user.id}
|
||||
LEFT OUTER JOIN user_roles project_user_roles
|
||||
ON project_user_roles.id = project_user_assignments.user_role_id
|
||||
AND project_user_roles.permissions @> ARRAY['#{ProjectPermissions::READ}']::varchar[]")
|
||||
.where('projects.archived = :archived OR
|
||||
(
|
||||
(
|
||||
experiments.archived = :archived OR
|
||||
my_modules.archived = :archived
|
||||
) AND
|
||||
:archived IS TRUE
|
||||
) OR
|
||||
projects.id = :project_id',
|
||||
archived: archived,
|
||||
project_id: project&.id || -1)
|
||||
.select(
|
||||
'projects.id',
|
||||
'projects.name',
|
||||
'projects.archived',
|
||||
disabled_sql,
|
||||
has_children_sql
|
||||
).group('projects.id')
|
||||
end
|
||||
|
|
|
@ -107,14 +107,14 @@ class ProjectsController < ApplicationController
|
|||
end
|
||||
|
||||
def inventory_assigning_project_filter
|
||||
readable_experiments = Experiment.readable_by_user(current_user)
|
||||
viewable_experiments = Experiment.viewable_by_user(current_user, current_team)
|
||||
assignable_my_modules = MyModule.repository_row_assignable_by_user(current_user)
|
||||
|
||||
projects = Project.readable_by_user(current_user)
|
||||
projects = Project.viewable_by_user(current_user, current_team)
|
||||
.active
|
||||
.joins(experiments: :my_modules)
|
||||
.where(experiments: { id: readable_experiments })
|
||||
.where(experiments: { id: viewable_experiments })
|
||||
.where(my_modules: { id: assignable_my_modules })
|
||||
.search(current_user, false, params[:query], 1, current_team)
|
||||
.distinct
|
||||
.pluck(:id, :name)
|
||||
|
||||
|
@ -167,64 +167,70 @@ class ProjectsController < ApplicationController
|
|||
end
|
||||
|
||||
def update
|
||||
@project.assign_attributes(project_update_params)
|
||||
return_error = false
|
||||
flash_error = t('projects.update.error_flash', name: escape_input(@project.name))
|
||||
|
||||
return render_403 unless can_manage_project?(@project) || @project.archived_changed?
|
||||
|
||||
# Check archive permissions if archiving/restoring
|
||||
if project_params.include? :archived
|
||||
if (project_params[:archived] == 'true' &&
|
||||
!can_archive_project?(@project)) ||
|
||||
(project_params[:archived] == 'false' &&
|
||||
!can_restore_project?(@project))
|
||||
if @project.archived_changed? &&
|
||||
((@project.archived == 'true' && !can_archive_project?(@project)) ||
|
||||
(@project.archived == 'false' && !can_restore_project?(@project)))
|
||||
return_error = true
|
||||
is_archive = project_params[:archived] == 'true' ? 'archive' : 'restore'
|
||||
is_archive = @project.archived? ? 'archive' : 'restore'
|
||||
flash_error =
|
||||
t("projects.#{is_archive}.error_flash", name: escape_input(@project.name))
|
||||
end
|
||||
elsif !can_manage_project?(@project)
|
||||
render_403 && return
|
||||
end
|
||||
|
||||
message_renamed = nil
|
||||
message_visibility = nil
|
||||
if (project_params.include? :name) &&
|
||||
(project_params[:name] != @project.name)
|
||||
message_renamed = true
|
||||
end
|
||||
if (project_params.include? :visibility) &&
|
||||
(project_params[:visibility] != @project.visibility)
|
||||
message_visibility = if project_params[:visibility] == 'visible'
|
||||
t('projects.activity.visibility_visible')
|
||||
else
|
||||
t('projects.activity.visibility_hidden')
|
||||
end
|
||||
message_renamed = @project.name_changed?
|
||||
message_visibility = if @project.visibility_changed?
|
||||
nil
|
||||
elsif @project.visible?
|
||||
t('projects.activity.visibility_visible')
|
||||
else
|
||||
t('projects.activity.visibility_hidden')
|
||||
end
|
||||
|
||||
message_archived = if !@project.archived_changed?
|
||||
nil
|
||||
elsif @project.archived?
|
||||
'archive'
|
||||
else
|
||||
'restore'
|
||||
end
|
||||
|
||||
default_public_user_name = nil
|
||||
if @project.visibility_changed? && @project.default_public_user_role_id_changed?
|
||||
default_public_user_name = UserRole.find(project_params[:default_public_user_role_id])&.name
|
||||
end
|
||||
|
||||
@project.last_modified_by = current_user
|
||||
if !return_error && @project.update(project_params)
|
||||
# Add activities if needed
|
||||
if !return_error && @project.save
|
||||
|
||||
# Add activities if needed
|
||||
log_activity(:change_project_visibility, @project, visibility: message_visibility) if message_visibility.present?
|
||||
log_activity(:rename_project) if message_renamed.present?
|
||||
log_activity(:archive_project) if project_params[:archived] == 'true'
|
||||
log_activity(:restore_project) if project_params[:archived] == 'false'
|
||||
log_activity(:archive_project) if message_archived == 'archive'
|
||||
log_activity(:restore_project) if message_archived == 'restore'
|
||||
|
||||
if default_public_user_name.present?
|
||||
log_activity(:project_access_changed_all_team_members,
|
||||
@project,
|
||||
{ team: @project.team.id, role: default_public_user_name })
|
||||
end
|
||||
|
||||
flash_success = t('projects.update.success_flash', name: escape_input(@project.name))
|
||||
if project_params[:archived] == 'true'
|
||||
if message_archived == 'archive'
|
||||
flash_success = t('projects.archive.success_flash', name: escape_input(@project.name))
|
||||
elsif project_params[:archived] == 'false'
|
||||
elsif message_archived == 'restore'
|
||||
flash_success = t('projects.restore.success_flash', name: escape_input(@project.name))
|
||||
end
|
||||
respond_to do |format|
|
||||
format.html do
|
||||
# Redirect URL for archive view is different as for other views.
|
||||
if project_params[:archived] == 'false'
|
||||
# The project should be restored
|
||||
@project.restore(current_user) unless @project.archived
|
||||
elsif @project.archived
|
||||
# The project should be archived
|
||||
@project.archive(current_user)
|
||||
end
|
||||
@project.restore(current_user) if message_archived == 'restore'
|
||||
@project.archive(current_user) if message_archived == 'archive'
|
||||
|
||||
redirect_to projects_path
|
||||
flash[:success] = flash_success
|
||||
end
|
||||
|
@ -404,12 +410,17 @@ class ProjectsController < ApplicationController
|
|||
def project_params
|
||||
params.require(:project)
|
||||
.permit(
|
||||
:name, :team_id, :visibility,
|
||||
:name, :visibility,
|
||||
:archived, :project_folder_id,
|
||||
:default_public_user_role_id
|
||||
)
|
||||
end
|
||||
|
||||
def project_update_params
|
||||
params.require(:project)
|
||||
.permit(:name, :visibility, :archived, :default_public_user_role_id)
|
||||
end
|
||||
|
||||
def view_type_params
|
||||
params.require(:project).require(:view_type)
|
||||
end
|
||||
|
|
|
@ -382,6 +382,16 @@ class RepositoriesController < ApplicationController
|
|||
end
|
||||
end
|
||||
|
||||
def export_repositories
|
||||
repositories = Repository.viewable_by_user(current_user, current_team).where(id: params[:repository_ids])
|
||||
if repositories.present?
|
||||
RepositoriesExportJob.perform_later(repositories.pluck(:id), current_user, current_team)
|
||||
render json: { message: t('zip_export.export_request_success') }
|
||||
else
|
||||
render json: { message: t('zip_export.export_error') }, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
def assigned_my_modules
|
||||
my_modules = MyModule.joins(:repository_rows).where(repository_rows: { repository: @repository })
|
||||
.readable_by_user(current_user).distinct
|
||||
|
|
|
@ -1,8 +1,11 @@
|
|||
import TurbolinksAdapter from 'vue-turbolinks';
|
||||
import Vue from 'vue/dist/vue.esm';
|
||||
import AssignItemsToTaskModalContainer from '../../vue/assign_items_to_tasks_modal/container.vue';
|
||||
import PerfectScrollbar from 'vue2-perfect-scrollbar';
|
||||
import 'vue2-perfect-scrollbar/dist/vue2-perfect-scrollbar.css';
|
||||
|
||||
Vue.use(TurbolinksAdapter);
|
||||
Vue.use(PerfectScrollbar);
|
||||
Vue.prototype.i18n = window.I18n;
|
||||
|
||||
function initAssignItemsToTaskModalComponent() {
|
||||
|
|
|
@ -43,6 +43,7 @@
|
|||
ref="projectsSelector"
|
||||
@change="changeProject"
|
||||
:options="projects"
|
||||
:isLoading="projectsLoading"
|
||||
:placeholder="
|
||||
i18n.t(
|
||||
'repositories.modal_assign_items_to_task.body.project_select.placeholder'
|
||||
|
@ -76,6 +77,7 @@
|
|||
ref="experimentsSelector"
|
||||
@change="changeExperiment"
|
||||
:options="experiments"
|
||||
:isLoading="experimentsLoading"
|
||||
:placeholder="experimentsSelectorPlaceholder"
|
||||
:no-options-placeholder="
|
||||
i18n.t(
|
||||
|
@ -105,6 +107,7 @@
|
|||
ref="tasksSelector"
|
||||
@change="changeTask"
|
||||
:options="tasks"
|
||||
:isLoading="tasksLoading"
|
||||
:placeholder="tasksSelectorPlaceholder"
|
||||
:no-options-placeholder="
|
||||
i18n.t(
|
||||
|
@ -153,6 +156,9 @@ export default {
|
|||
selectedProject: null,
|
||||
selectedExperiment: null,
|
||||
selectedTask: null,
|
||||
projectsLoading: null,
|
||||
experimentsLoading: null,
|
||||
tasksLoading: null,
|
||||
showCallback: null
|
||||
};
|
||||
},
|
||||
|
@ -164,12 +170,16 @@ export default {
|
|||
},
|
||||
mounted() {
|
||||
$(this.$refs.modal).on("shown.bs.modal", () => {
|
||||
this.projectsLoading = true;
|
||||
|
||||
$.get(this.projectURL, data => {
|
||||
if (Array.isArray(data)) {
|
||||
this.projects = data;
|
||||
return false;
|
||||
}
|
||||
this.projects = [];
|
||||
}).always(() => {
|
||||
this.projectsLoading = false;
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -240,24 +250,30 @@ export default {
|
|||
this.resetExperimentSelector();
|
||||
this.resetTaskSelector();
|
||||
|
||||
this.experimentsLoading = true;
|
||||
$.get(this.experimentURL, data => {
|
||||
if (Array.isArray(data)) {
|
||||
this.experiments = data;
|
||||
return false;
|
||||
}
|
||||
this.experiments = [];
|
||||
}).always(() => {
|
||||
this.experimentsLoading = false;
|
||||
});
|
||||
},
|
||||
changeExperiment(value) {
|
||||
this.selectedExperiment = value;
|
||||
this.resetTaskSelector();
|
||||
|
||||
this.tasksLoading = true;
|
||||
$.get(this.taskURL, data => {
|
||||
if (Array.isArray(data)) {
|
||||
this.tasks = data;
|
||||
return false;
|
||||
}
|
||||
this.tasks = [];
|
||||
}).always(() => {
|
||||
this.tasksLoading = false;
|
||||
});
|
||||
},
|
||||
changeTask(value) {
|
||||
|
|
|
@ -92,7 +92,10 @@
|
|||
this.buttonOverflow = false;
|
||||
|
||||
this.$nextTick(() => {
|
||||
if (!this.$el.getBoundingClientRect) return;
|
||||
if (
|
||||
!(this.$el.getBoundingClientRect &&
|
||||
document.querySelector('.sn-action-toolbar__action:last-child'))
|
||||
) return;
|
||||
|
||||
let containerRect = this.$el.getBoundingClientRect();
|
||||
let lastActionRect = document.querySelector('.sn-action-toolbar__action:last-child').getBoundingClientRect();
|
||||
|
|
|
@ -17,7 +17,8 @@
|
|||
class="text-ellipsis overflow-hidden hover:no-underline pr-3"
|
||||
:class="{
|
||||
'text-sn-science-blue-hover': (!item.archived && archived),
|
||||
'no-hover': (!item.archived && archived)
|
||||
'no-hover': (!item.archived && archived),
|
||||
'disabled-link': item.disabled
|
||||
}">
|
||||
<template v-if="item.archived">(A)</template>
|
||||
{{ item.name }}
|
||||
|
@ -62,7 +63,7 @@ export default {
|
|||
},
|
||||
computed: {
|
||||
hasChildren: function() {
|
||||
return this.item.has_children || this.children.length > 0;
|
||||
return !this.item.disabled && (this.item.has_children || this.children.length > 0);
|
||||
},
|
||||
sortedMenuItems: function() {
|
||||
return this.children.sort((a, b) => {
|
||||
|
|
|
@ -95,7 +95,8 @@
|
|||
editingTable: false,
|
||||
tableObject: null,
|
||||
nameModalOpen: false,
|
||||
reloadHeader: 0
|
||||
reloadHeader: 0,
|
||||
updatingTableData: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
|
@ -104,10 +105,10 @@
|
|||
}
|
||||
},
|
||||
updated() {
|
||||
this.loadTableData();
|
||||
if(!this.updatingTableData) this.loadTableData();
|
||||
},
|
||||
beforeUpdate() {
|
||||
this.tableObject.destroy();
|
||||
if(!this.updatingTableData) this.tableObject.destroy();
|
||||
},
|
||||
mounted() {
|
||||
this.loadTableData();
|
||||
|
@ -130,6 +131,7 @@
|
|||
},
|
||||
disableTableEdit() {
|
||||
this.editingTable = false;
|
||||
this.updatingTableData = false;
|
||||
},
|
||||
enableNameEdit() {
|
||||
this.editingName = true;
|
||||
|
@ -203,6 +205,7 @@
|
|||
}
|
||||
this.$emit('update', this.element)
|
||||
this.ajax_update_url()
|
||||
this.updatingTableData = false;
|
||||
},
|
||||
ajax_update_url() {
|
||||
$.ajax({
|
||||
|
@ -229,7 +232,10 @@
|
|||
formulas: formulasEnabled,
|
||||
preventOverflow: 'horizontal',
|
||||
readOnly: !this.editingTable,
|
||||
afterUnlisten: () => setTimeout(this.updateTable, 100) // delay makes cancel button work
|
||||
afterUnlisten: () => {
|
||||
this.updatingTableData = true;
|
||||
setTimeout(this.updateTable, 100) // delay makes cancel button work
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,7 +14,7 @@
|
|||
<template v-if="options.length">
|
||||
<div
|
||||
v-for="option in options"
|
||||
:key="option[0]" @click="setValue(option[0])"
|
||||
:key="option[0]" @mousedown.prevent.stop="setValue(option[0])"
|
||||
class="sn-select__option"
|
||||
>
|
||||
{{ option[1] }}
|
||||
|
@ -47,7 +47,8 @@
|
|||
data() {
|
||||
return {
|
||||
isOpen: false,
|
||||
optionPositionStyle: ''
|
||||
optionPositionStyle: '',
|
||||
blurPrevented: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
|
@ -64,13 +65,27 @@
|
|||
document.addEventListener("scroll", this.updateOptionPosition);
|
||||
},
|
||||
methods: {
|
||||
preventBlur() {
|
||||
this.blurPrevented = true;
|
||||
},
|
||||
allowBlur() {
|
||||
setTimeout(() => { this.blurPrevented = false }, 200);
|
||||
},
|
||||
blur() {
|
||||
setTimeout(() => {
|
||||
this.isOpen = false;
|
||||
this.$emit('blur');
|
||||
}, 200);
|
||||
if (this.blurPrevented) {
|
||||
this.focusElement.focus();
|
||||
} else {
|
||||
this.isOpen = false;
|
||||
this.$emit('blur');
|
||||
}
|
||||
}, 100);
|
||||
},
|
||||
toggle() {
|
||||
if (this.isOpen && this.blurPrevented) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.isOpen = !this.isOpen;
|
||||
|
||||
if (this.isOpen) {
|
||||
|
@ -80,6 +95,7 @@
|
|||
});
|
||||
this.$refs.optionsContainer.scrollTop = 0;
|
||||
this.updateOptionPosition();
|
||||
this.setUpBlurHandlers();
|
||||
} else {
|
||||
this.optionPositionStyle = '';
|
||||
this.$emit('close');
|
||||
|
@ -104,6 +120,13 @@
|
|||
}
|
||||
|
||||
this.optionPositionStyle = `position: fixed; top: ${top}px; left: ${left}px; width: ${width}px`
|
||||
},
|
||||
setUpBlurHandlers() {
|
||||
setTimeout(() => {
|
||||
this.$refs.optionsContainer.$el.querySelector('.ps__thumb-y').addEventListener('mousedown', this.preventBlur);
|
||||
this.$refs.optionsContainer.$el.querySelector('.ps__thumb-y').addEventListener('mouseup', this.allowBlur);
|
||||
document.addEventListener('mouseup', this.allowBlur);
|
||||
}, 100);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
:value="value"
|
||||
:options="currentOptions"
|
||||
:placeholder="placeholder"
|
||||
:noOptionsPlaceholder="noOptionsPlaceholder"
|
||||
:noOptionsPlaceholder="isLoading ? i18n.t('general.loading') : noOptionsPlaceholder"
|
||||
v-bind:disabled="disabled"
|
||||
@change="change"
|
||||
@blur="blur"
|
||||
|
@ -29,7 +29,8 @@
|
|||
placeholder: { type: String },
|
||||
searchPlaceholder: { type: String },
|
||||
noOptionsPlaceholder: { type: String },
|
||||
disabled: { type: Boolean }
|
||||
disabled: { type: Boolean },
|
||||
isLoading: { type: Boolean, default: false }
|
||||
},
|
||||
components: { Select },
|
||||
data() {
|
||||
|
|
98
app/jobs/repositories_export_job.rb
Normal file
98
app/jobs/repositories_export_job.rb
Normal file
|
@ -0,0 +1,98 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class RepositoriesExportJob < ApplicationJob
|
||||
include StringUtility
|
||||
|
||||
def perform(repository_ids, user, team)
|
||||
@user = user
|
||||
@team = team
|
||||
@repositories = Repository.viewable_by_user(@user, @team).where(id: repository_ids).order(:id)
|
||||
zip_input_dir = FileUtils.mkdir_p(Rails.root.join("tmp/temp_zip_#{Time.now.to_i}")).first
|
||||
zip_dir = FileUtils.mkdir_p(Rails.root.join('tmp/zip-ready')).first
|
||||
|
||||
zip_name = "inventories_export_#{Time.now.utc.strftime('%F_%H-%M-%S_UTC')}.zip"
|
||||
full_zip_name = File.join(zip_dir, zip_name)
|
||||
|
||||
fill_content(zip_input_dir)
|
||||
ZipExport.transaction do
|
||||
@zip_export = ZipExport.create!(user: @user)
|
||||
@zip_export.zip!(zip_input_dir, full_zip_name)
|
||||
@zip_export.zip_file.attach(io: File.open(full_zip_name), filename: zip_name)
|
||||
generate_notification
|
||||
end
|
||||
ensure
|
||||
FileUtils.rm_rf([zip_input_dir, full_zip_name], secure: true)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def fill_content(tmp_dir)
|
||||
# Create team dir
|
||||
team_path = "#{tmp_dir}/#{to_filesystem_name(@team.name)}"
|
||||
FileUtils.mkdir_p(team_path)
|
||||
@repositories.each_with_index do |repository, idx|
|
||||
save_repository_to_csv(team_path, repository, idx)
|
||||
end
|
||||
end
|
||||
|
||||
def save_repository_to_csv(path, repository, idx)
|
||||
repository_name = "#{to_filesystem_name(repository.name)} (#{idx})"
|
||||
|
||||
# Attachments dir
|
||||
relative_attachments_path = "#{repository_name} attachments"
|
||||
attachments_path = "#{path}/#{relative_attachments_path}"
|
||||
FileUtils.mkdir_p(attachments_path)
|
||||
|
||||
# CSV file
|
||||
csv_file = FileUtils.touch("#{path}/#{repository_name}.csv").first
|
||||
|
||||
# Define headers and columns IDs
|
||||
col_ids = [-3, -4, -5, -6] + repository.repository_columns.map(&:id)
|
||||
|
||||
# Define callback function for file name
|
||||
assets = {}
|
||||
asset_counter = 0
|
||||
handle_name_func = lambda do |asset|
|
||||
file_name = append_file_suffix(asset.file_name, "_#{asset_counter}").to_s
|
||||
|
||||
# Save pair for downloading it later
|
||||
assets[asset] = "#{attachments_path}/#{file_name}"
|
||||
|
||||
asset_counter += 1
|
||||
relative_path = "#{relative_attachments_path}/#{file_name}"
|
||||
return "=HYPERLINK(\"#{relative_path}\", \"#{relative_path}\")"
|
||||
end
|
||||
|
||||
# Generate CSV
|
||||
csv_data = RepositoryZipExport.to_csv(repository.repository_rows, col_ids, @user, repository, handle_name_func)
|
||||
File.binwrite(csv_file, csv_data)
|
||||
|
||||
# Save all attachments (it doesn't work directly in callback function
|
||||
assets.each do |asset, asset_path|
|
||||
asset.file.open do |file|
|
||||
FileUtils.mv(file.path, asset_path)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def append_file_suffix(file_name, suffix)
|
||||
file_name = to_filesystem_name(file_name)
|
||||
ext = File.extname(file_name)
|
||||
File.basename(file_name, ext) + suffix + ext
|
||||
end
|
||||
|
||||
def generate_notification
|
||||
notification = Notification.create!(
|
||||
type_of: :deliver,
|
||||
title: I18n.t('zip_export.notification_title'),
|
||||
message: "<a data-id='#{@zip_export.id}' " \
|
||||
"data-turbolinks='false' " \
|
||||
"href='#{Rails.application
|
||||
.routes
|
||||
.url_helpers
|
||||
.zip_exports_download_export_all_path(@zip_export)}'>" \
|
||||
"#{@zip_export.zip_file_name}</a>"
|
||||
)
|
||||
UserNotification.create!(notification: notification, user: @user)
|
||||
end
|
||||
end
|
|
@ -113,23 +113,21 @@ class Project < ApplicationRecord
|
|||
joins("
|
||||
LEFT OUTER JOIN experiments ON experiments.project_id = projects.id
|
||||
LEFT OUTER JOIN user_assignments experiment_user_assignments
|
||||
ON experiment_user_assignments.assignable_id = experiments.id AND
|
||||
experiment_user_assignments.assignable_type = 'Experiment'
|
||||
ON experiment_user_assignments.assignable_id = experiments.id
|
||||
AND experiment_user_assignments.assignable_type = 'Experiment'
|
||||
AND experiment_user_assignments.user_id = #{user.id}
|
||||
LEFT OUTER JOIN user_roles experiment_user_roles
|
||||
ON experiment_user_roles.id = experiment_user_assignments.user_role_id
|
||||
AND experiment_user_roles.permissions @> ARRAY['#{ExperimentPermissions::READ}']::varchar[]
|
||||
LEFT OUTER JOIN my_modules ON my_modules.experiment_id = experiments.id
|
||||
LEFT OUTER JOIN user_assignments my_module_user_assignments
|
||||
ON my_module_user_assignments.assignable_id = my_modules.id AND
|
||||
my_module_user_assignments.assignable_type = 'MyModule'
|
||||
ON my_module_user_assignments.assignable_id = my_modules.id
|
||||
AND my_module_user_assignments.assignable_type = 'MyModule'
|
||||
AND my_module_user_assignments.user_id = #{user.id}
|
||||
LEFT OUTER JOIN user_roles my_module_user_roles
|
||||
ON my_module_user_roles.id = my_module_user_assignments.user_role_id
|
||||
AND my_module_user_roles.permissions @> ARRAY['#{MyModulePermissions::READ}']::varchar[]
|
||||
")
|
||||
.where('
|
||||
(experiment_user_assignments.user_id = ? AND experiment_user_roles.permissions @> ARRAY[?]::varchar[]
|
||||
OR experiments.id IS NULL) AND
|
||||
(my_module_user_assignments.user_id = ? AND my_module_user_roles.permissions @> ARRAY[?]::varchar[]
|
||||
OR my_modules.id IS NULL)
|
||||
', user.id, ExperimentPermissions::READ, user.id, MyModulePermissions::READ)
|
||||
end
|
||||
|
||||
def self.filter_by_teams(teams = [])
|
||||
|
|
|
@ -97,7 +97,7 @@ class Step < ApplicationRecord
|
|||
end
|
||||
|
||||
def self.viewable_by_user(user, teams)
|
||||
where(protocol: Protocol.viewable_by_user(user, teams))
|
||||
joins(:protocol).where(protocol: { my_module: MyModule.viewable_by_user(user, teams) })
|
||||
end
|
||||
|
||||
def can_destroy?
|
||||
|
|
|
@ -13,16 +13,15 @@ class TeamZipExport < ZipExport
|
|||
).first
|
||||
zip_dir = FileUtils.mkdir_p(File.join(Rails.root, 'tmp/zip-ready')).first
|
||||
|
||||
zip_name = "projects_export_#{Time.now.strftime('%F_%H-%M-%S_UTC')}.zip"
|
||||
zip_name = "projects_export_#{Time.now.utc.strftime('%F_%H-%M-%S_UTC')}.zip"
|
||||
full_zip_name = File.join(zip_dir, zip_name)
|
||||
zip_file = File.new(full_zip_name, 'w+')
|
||||
|
||||
fill_content(zip_input_dir, data, type, options)
|
||||
zip!(zip_input_dir, zip_file)
|
||||
self.zip_file.attach(io: File.open(zip_file), filename: zip_name)
|
||||
zip!(zip_input_dir, full_zip_name)
|
||||
zip_file.attach(io: File.open(full_zip_name), filename: zip_name)
|
||||
generate_notification(user) if save
|
||||
ensure
|
||||
FileUtils.rm_rf([zip_input_dir, zip_file], secure: true)
|
||||
FileUtils.rm_rf([zip_input_dir, full_zip_name], secure: true)
|
||||
end
|
||||
|
||||
handle_asynchronously :generate_exportable_zip,
|
||||
|
@ -320,38 +319,4 @@ class TeamZipExport < ZipExport
|
|||
|
||||
csv_file_path
|
||||
end
|
||||
|
||||
# Recursive zipping
|
||||
def zip!(input_dir, output_file)
|
||||
files = Dir.entries(input_dir)
|
||||
|
||||
# Don't zip current/above directory
|
||||
files.delete_if { |el| ['.', '..'].include?(el) }
|
||||
|
||||
Zip::File.open(output_file.path, Zip::File::CREATE) do |zipfile|
|
||||
write_entries(input_dir, files, '', zipfile)
|
||||
end
|
||||
end
|
||||
|
||||
# A helper method to make the recursion work.
|
||||
def write_entries(input_dir, entries, path, io)
|
||||
entries.each do |e|
|
||||
zip_file_path = path == '' ? e : File.join(path, e)
|
||||
disk_file_path = File.join(input_dir, zip_file_path)
|
||||
puts 'Deflating ' + disk_file_path
|
||||
if File.directory?(disk_file_path)
|
||||
io.mkdir(zip_file_path)
|
||||
subdir = Dir.entries(disk_file_path)
|
||||
|
||||
# Remove current/above directory to prevent infinite recursion
|
||||
subdir.delete_if { |el| ['.', '..'].include?(el) }
|
||||
|
||||
write_entries(input_dir, subdir, zip_file_path, io)
|
||||
else
|
||||
io.get_output_stream(zip_file_path) do |f|
|
||||
f.write(File.open(disk_file_path, 'rb').read)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -36,19 +36,27 @@ class ZipExport < ApplicationRecord
|
|||
zip_file.blob&.filename&.to_s
|
||||
end
|
||||
|
||||
def zip!(input_dir, output_file)
|
||||
entries = Dir.glob('**/*', base: input_dir)
|
||||
Zip::File.open(output_file, create: true) do |zipfile|
|
||||
entries.each do |entry|
|
||||
zipfile.add(entry, "#{input_dir}/#{entry}")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def generate_exportable_zip(user, data, type, options = {})
|
||||
I18n.backend.date_format = user.settings[:date_format] || Constants::DEFAULT_DATE_FORMAT
|
||||
zip_input_dir = FileUtils.mkdir_p(File.join(Rails.root, "tmp/temp_zip_#{Time.now.to_i}")).first
|
||||
tmp_zip_dir = FileUtils.mkdir_p(File.join(Rails.root, 'tmp/zip-ready')).first
|
||||
tmp_zip_name = "export_#{Time.now.strftime('%F %H-%M-%S_UTC')}.zip"
|
||||
tmp_zip_file = File.new(File.join(tmp_zip_dir, tmp_zip_name), 'w+')
|
||||
tmp_full_zip_name = File.join(tmp_zip_dir, "export_#{Time.now.strftime('%F %H-%M-%S_UTC')}.zip")
|
||||
|
||||
fill_content(zip_input_dir, data, type, options)
|
||||
zip!(zip_input_dir, tmp_zip_file)
|
||||
zip_file.attach(io: File.open(tmp_zip_file), filename: tmp_zip_name)
|
||||
zip_file.attach(io: File.open(tmp_full_zip_name), filename: tmp_zip_name)
|
||||
generate_notification(user) if save
|
||||
ensure
|
||||
FileUtils.rm_rf([zip_input_dir, tmp_zip_file], secure: true)
|
||||
FileUtils.rm_rf([zip_input_dir, tmp_full_zip_name], secure: true)
|
||||
end
|
||||
|
||||
handle_asynchronously :generate_exportable_zip
|
||||
|
@ -60,14 +68,14 @@ class ZipExport < ApplicationRecord
|
|||
.delete_expired_export(id)
|
||||
end
|
||||
|
||||
def method_missing(m, *args, &block)
|
||||
puts 'Method is missing! To use this zip_export you have to ' \
|
||||
'define a method: generate_( type )_zip.'
|
||||
object.public_send(m, *args, &block)
|
||||
def method_missing(method_name, *args, &block)
|
||||
return super unless method_name.to_s.start_with?('generate_')
|
||||
|
||||
raise StandardError, 'Method is missing! To use this zip_export you have to define a method: generate_( type )_zip.'
|
||||
end
|
||||
|
||||
def respond_to_missing?(method_name, include_private = false)
|
||||
method_name.to_s.start_with?(' generate_') || super
|
||||
method_name.to_s.start_with?('generate_') || super
|
||||
end
|
||||
|
||||
def fill_content(dir, data, type, options = {})
|
||||
|
@ -89,16 +97,6 @@ class ZipExport < ApplicationRecord
|
|||
UserNotification.create(notification: notification, user: user)
|
||||
end
|
||||
|
||||
def zip!(input_dir, output_file)
|
||||
files = Dir.entries(input_dir)
|
||||
files.delete_if { |el| el == '..' || el == '.' }
|
||||
Zip::File.open(output_file.path, Zip::File::CREATE) do |zipfile|
|
||||
files.each do |filename|
|
||||
zipfile.add(filename, input_dir + '/' + filename)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def generate_repositories_zip(tmp_dir, data, _options = {})
|
||||
file = FileUtils.touch("#{tmp_dir}/export.csv").first
|
||||
File.open(file, 'wb') { |f| f.write(data) }
|
||||
|
|
|
@ -19,8 +19,7 @@ Canaid::Permissions.register_for(Project) do
|
|||
export_project)
|
||||
.each do |perm|
|
||||
can perm do |user, project|
|
||||
project.permission_granted?(user, ProjectPermissions::READ) ||
|
||||
project.team.permission_granted?(user, TeamPermissions::MANAGE)
|
||||
project.permission_granted?(user, ProjectPermissions::READ)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -57,7 +57,7 @@ module RepositoryZipExport
|
|||
when -8
|
||||
I18n.t('repositories.table.archived_on')
|
||||
else
|
||||
column = RepositoryColumn.find_by_id(c_id)
|
||||
column = repository.repository_columns.find_by(id: c_id)
|
||||
column ? column.name : nil
|
||||
end
|
||||
end
|
||||
|
@ -88,8 +88,7 @@ module RepositoryZipExport
|
|||
.find_by(repository_column_id: c_id)
|
||||
|
||||
if cell
|
||||
if cell.value_type == 'RepositoryAssetValue' &&
|
||||
handle_file_name_func
|
||||
if cell.value_type == 'RepositoryAssetValue' && handle_file_name_func
|
||||
handle_file_name_func.call(cell.value.asset)
|
||||
else
|
||||
SmartAnnotations::TagToText.new(
|
||||
|
|
|
@ -2,8 +2,8 @@
|
|||
<div class="title-row">
|
||||
<i class="sn-icon sn-icon-navigator sci--layout--navigator-open cursor-pointer p-1.5 border rounded border-sn-light-grey mr-4"></i>
|
||||
<h1 class="experimnet-name" data-toggle="tooltip" data-placement="bottom" title="<%= @experiment.name %>">
|
||||
<% if @experiment.archived_branch? %>
|
||||
<i class="sn-icon sn-icon-archive" data-view-mode="archived"></i>
|
||||
<% if @experiment.archived? %>
|
||||
<span><%= t('labels.archived')%></span>
|
||||
<% end %>
|
||||
<% if @inline_editable_title_config.present? %>
|
||||
<%= render partial: "shared/inline_editing",
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
<div class="datetime-picker-container"
|
||||
id="<%= id %>"
|
||||
data-id="<%= id %>"
|
||||
data-use-current="<%= use_current %>">
|
||||
data-use-current="<%= use_current %>"
|
||||
data-datetime-picker-format="<%= datetime_picker_format_date_only %>">
|
||||
<% if label %>
|
||||
<label class="control-label required" for="calendar-<%= id %>"><%= label %></label>
|
||||
<% end %>
|
||||
|
|
|
@ -16,7 +16,7 @@
|
|||
<% if ::NewRelic::Agent.instance.started? %>
|
||||
<%= ::NewRelic::Agent.browser_timing_header(controller.request.content_security_policy_nonce) %>
|
||||
<% end %>
|
||||
<%= javascript_include_tag 'jquery_bundle' %>
|
||||
<%= javascript_include_tag 'jquery_bundle', nonce: true %>
|
||||
<%= javascript_include_tag 'application' %>
|
||||
<%= javascript_include_tag 'application_pack' %>
|
||||
<%= javascript_include_tag 'session_end' %>
|
||||
|
@ -59,7 +59,7 @@
|
|||
data-atwho-repositories-url="<%= atwho_menu_team_path(current_team) %>"
|
||||
data-atwho-rep-items-url="<%= atwho_rep_items_team_path(current_team) %>"
|
||||
data-atwho-menu-items="<%= atwho_menu_items_team_path(current_team) %>"
|
||||
data-datetime-picker-format-date-only="<%= datetime_picker_format_date_only %>"
|
||||
data-datetime-picker-format="<%= datetime_picker_format_date_only %>"
|
||||
<% end %>
|
||||
>
|
||||
|
||||
|
|
|
@ -15,7 +15,7 @@
|
|||
</h1>
|
||||
<% end %>
|
||||
<h1 data-view-mode="archived">
|
||||
<i class="sn-icon sn-icon-archive"></i>
|
||||
<span><%= t('labels.archived')%></span>
|
||||
<span class="projects-title name-readonly-placeholder"><%= current_folder&.name || t('projects.index.head_title_archived') %></span>
|
||||
</h1>
|
||||
</div>
|
||||
|
|
|
@ -13,14 +13,17 @@
|
|||
</div>
|
||||
|
||||
<div class="project-name-cell table-cell">
|
||||
<%
|
||||
disabled_link = 'disabled-link' unless can_read_project?(project)
|
||||
%>
|
||||
<% if project.archived? %>
|
||||
<%= link_to project_url(project, view_mode: :archived) do %>
|
||||
<%= link_to project_url(project, view_mode: :archived), class: disabled_link do %>
|
||||
<h3 class="name" title="<%= project.name %>">
|
||||
<%= project.name %>
|
||||
</h3>
|
||||
<% end %>
|
||||
<% else %>
|
||||
<%= link_to project_url(project) do %>
|
||||
<%= link_to project_url(project), class: disabled_link do %>
|
||||
<h3 class="name" title="<%= project.name %>">
|
||||
<%= project.name %>
|
||||
</h3>
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
<i class="sn-icon sn-icon-navigator sci--layout--navigator-open cursor-pointer p-1.5 border rounded border-sn-light-grey mr-4"></i>
|
||||
<h1 class="project-name">
|
||||
<% if @project.archived? %>
|
||||
<i class="sn-icon sn-icon-archive" data-view-mode="archived"></i>
|
||||
<span><%= t('labels.archived')%></span>
|
||||
<% end %>
|
||||
<% if @inline_editable_title_config.present? %>
|
||||
<%= render partial: "shared/inline_editing",
|
||||
|
|
|
@ -16,7 +16,7 @@
|
|||
<div class="title-row">
|
||||
<% if templates_view_mode_archived?(type: @type) %>
|
||||
<h1>
|
||||
<i class="sn-icon sn-icon-archive"></i>
|
||||
<span><%= t('labels.archived')%></span>
|
||||
<%= t('protocols.index.head_title_archived') %>
|
||||
</h1>
|
||||
<% else %>
|
||||
|
|
|
@ -15,7 +15,7 @@
|
|||
} %>
|
||||
<% else %>
|
||||
<% if @protocol.archived %>
|
||||
<i class="sn-icon sn-icon-archive"></i>
|
||||
<span><%= t('labels.archived')%></span>
|
||||
<% end %>
|
||||
<div class="name-readonly-placeholder">
|
||||
<% if @protocol.in_repository_draft? %>
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
class: "sidebar-link repository-link #{ 'selected' if current_page?(repository_path(repository)) }",
|
||||
data: {type: 'repository', id: repository.id } do %>
|
||||
<% if repository.archived? %>
|
||||
<i class="sn-icon sn-icon-archive"></i>
|
||||
<span><%= t('labels.archived')%></span>
|
||||
<% end %>
|
||||
<%= repository.name %>
|
||||
<%= inventory_shared_status_icon(repository, current_team) %>
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
<div class="content-header">
|
||||
<div class="title-row">
|
||||
<h1 data-view-mode="active"><%= t('libraries.index.head_title') %></h1>
|
||||
<h1 data-view-mode="archived"><i class="sn-icon sn-icon-archive"></i> <%= t('libraries.index.head_title_archived') %></h1>
|
||||
<h1 data-view-mode="archived"><span><%= t('labels.archived')%></span> <%= t('libraries.index.head_title_archived') %></h1>
|
||||
</div>
|
||||
</div>
|
||||
<div class="content-body">
|
||||
|
|
|
@ -37,7 +37,7 @@
|
|||
<% end %>
|
||||
<div class="repository-archived-title-name" data-view-mode="archived">
|
||||
<% if @repository.archived? %>
|
||||
<i class="sn-icon sn-icon-archive"></i>
|
||||
<span><%= t('labels.archived')%></span>
|
||||
<%= t('repositories.show.archived_inventory', repository_name: @repository.name) %>
|
||||
<% else %>
|
||||
<%= t('repositories.show.archived_inventory_items', repository_name: @repository.name) %>
|
||||
|
|
|
@ -430,6 +430,9 @@ en:
|
|||
delete_error: "Error occurred while deleting comment."
|
||||
step_url: "Go to step"
|
||||
|
||||
labels:
|
||||
archived: "(A)"
|
||||
|
||||
projects:
|
||||
index:
|
||||
header:
|
||||
|
@ -3444,6 +3447,7 @@ en:
|
|||
download: "Download"
|
||||
access: "Access"
|
||||
select: "Select"
|
||||
loading: "Loading..."
|
||||
# In order to use the strings 'yes' and 'no' as keys, you need to wrap them with quotes
|
||||
'yes': "Yes"
|
||||
'no': "No"
|
||||
|
|
|
@ -87,7 +87,7 @@ en:
|
|||
uncomplete_task_html: "%{user} uncompleted task %{my_module}."
|
||||
assign_repository_record_html: "%{user} assigned inventory item(s) %{record_names} from inventory %{repository} to task %{my_module}."
|
||||
unassign_repository_record_html: "%{user} unassigned inventory item(s) %{record_names} from inventory %{repository} to task %{my_module}."
|
||||
assign_user_to_project_html: "%{user} granted with access %{user_target} with user role %{role} to project %{project}."
|
||||
assign_user_to_project_html: "%{user} granted access to %{user_target} with user role %{role} to project %{project}."
|
||||
unassign_user_from_project_html: "%{user} removed %{user_target} with user role %{role} from project %{project}."
|
||||
change_user_role_on_project_html: "%{user} changed %{user_target}'s role on project %{project} to %{role}."
|
||||
change_user_role_on_experiment_html: "%{user} changed %{user_target}'s role on experiment %{experiment} to %{role}."
|
||||
|
@ -258,10 +258,10 @@ en:
|
|||
protocol_template_revision_notes_updated_html: "%{user} edited revision notes of %{protocol}."
|
||||
protocol_template_draft_deleted_html: "%{user} deleted draft of %{protocol}."
|
||||
protocol_template_draft_created_html: "%{user} created draft of %{protocol}."
|
||||
protocol_template_access_granted_html: "%{user} granted with access %{user_target} with user role %{role} to protocol template %{protocol}."
|
||||
protocol_template_access_granted_html: "%{user} granted access to %{user_target} with user role %{role} to protocol template %{protocol}."
|
||||
protocol_template_access_changed_html: "%{user} changed %{user_target}’s role on protocol template %{protocol} to %{role}."
|
||||
protocol_template_access_revoked_html: "%{user} removed %{user_target} with user role %{role} from protocol template %{protocol}."
|
||||
protocol_template_access_granted_all_team_members_html: "%{user} granted with access all team members of %{team} team with user role %{role} to protocol template %{protocol}."
|
||||
protocol_template_access_granted_all_team_members_html: "%{user} granted access to all team members of %{team} team with user role %{role} to protocol template %{protocol}."
|
||||
protocol_template_access_changed_all_team_members_html: "%{user} changed %{team}’s role on protocol template %{protocol} to %{role}."
|
||||
protocol_template_access_revoked_all_team_members_html: "%{user} removed %{team} team members with user role %{role} from protocol template %{protocol}."
|
||||
task_protocol_save_to_template_html: "%{user} created a new protocol template %{protocol} from a task."
|
||||
|
|
|
@ -219,6 +219,7 @@ Rails.application.routes.draw do
|
|||
member do
|
||||
post 'parse_sheet', defaults: { format: 'json' }
|
||||
post 'export_repository', to: 'repositories#export_repository'
|
||||
post 'export_repositories', to: 'repositories#export_repositories'
|
||||
post 'export_projects'
|
||||
get 'sidebar'
|
||||
get 'export_projects_modal'
|
||||
|
|
Loading…
Reference in a new issue