mirror of
https://github.com/scinote-eln/scinote-web.git
synced 2024-09-20 06:35:56 +08:00
Merge branch 'develop' into features/ui-tweaks
This commit is contained in:
commit
7f2d4cba3e
|
@ -99,7 +99,7 @@ Style/GuardClause:
|
|||
MinBodyLength: 1
|
||||
|
||||
Style/HashSyntax:
|
||||
EnforcedStyle: ruby19
|
||||
EnforcedShorthandSyntax: never
|
||||
|
||||
Style/IfUnlessModifier:
|
||||
Enabled: true
|
||||
|
@ -299,9 +299,6 @@ Naming/VariableName:
|
|||
Naming/VariableNumber:
|
||||
EnforcedStyle: normalcase
|
||||
|
||||
Naming/BlockForwarding:
|
||||
EnforcedStyle: explicit
|
||||
|
||||
Style/WordArray:
|
||||
EnforcedStyle: percent
|
||||
MinSize: 0
|
||||
|
|
2
Gemfile
2
Gemfile
|
@ -49,6 +49,7 @@ gem 'bcrypt', '~> 3.1.10'
|
|||
# gem 'caracal'
|
||||
gem 'caracal',
|
||||
git: 'https://github.com/scinote-eln/caracal.git', branch: 'rubyzip2' # Build docx report
|
||||
gem 'caxlsx' # Build XLSX files
|
||||
gem 'deface', '~> 1.9'
|
||||
gem 'down', '~> 5.0'
|
||||
gem 'faker' # Generate fake data
|
||||
|
@ -106,6 +107,7 @@ group :development, :test do
|
|||
gem 'awesome_print'
|
||||
gem 'better_errors'
|
||||
gem 'binding_of_caller'
|
||||
gem 'brakeman', require: false
|
||||
gem 'bullet'
|
||||
gem 'byebug'
|
||||
gem 'factory_bot_rails'
|
||||
|
|
10
Gemfile.lock
10
Gemfile.lock
|
@ -208,6 +208,8 @@ GEM
|
|||
debug_inspector (>= 0.0.1)
|
||||
bootsnap (1.16.0)
|
||||
msgpack (~> 1.2)
|
||||
brakeman (6.1.2)
|
||||
racc
|
||||
builder (3.2.4)
|
||||
bullet (7.0.7)
|
||||
activesupport (>= 3.0.0)
|
||||
|
@ -227,6 +229,11 @@ GEM
|
|||
mail
|
||||
case_transform (0.2)
|
||||
activesupport
|
||||
caxlsx (4.0.0)
|
||||
htmlentities (~> 4.3, >= 4.3.4)
|
||||
marcel (~> 1.0)
|
||||
nokogiri (~> 1.10, >= 1.10.4)
|
||||
rubyzip (>= 1.3.0, < 3)
|
||||
cgi (0.4.1)
|
||||
childprocess (4.1.0)
|
||||
chunky_png (1.4.0)
|
||||
|
@ -353,6 +360,7 @@ GEM
|
|||
nokogiri (~> 1.0)
|
||||
hashdiff (1.0.1)
|
||||
hashie (5.0.0)
|
||||
htmlentities (4.3.4)
|
||||
http (5.1.1)
|
||||
addressable (~> 2.8)
|
||||
http-cookie (~> 1.0)
|
||||
|
@ -791,12 +799,14 @@ DEPENDENCIES
|
|||
better_errors
|
||||
binding_of_caller
|
||||
bootsnap
|
||||
brakeman
|
||||
bullet
|
||||
byebug
|
||||
canaid!
|
||||
capybara
|
||||
capybara-email
|
||||
caracal!
|
||||
caxlsx
|
||||
cssbundling-rails
|
||||
cucumber-rails
|
||||
database_cleaner
|
||||
|
|
BIN
app/assets/images/import_instruction.png
Normal file
BIN
app/assets/images/import_instruction.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 549 KiB |
|
@ -340,8 +340,8 @@ var MyModuleRepositories = (function() {
|
|||
json.state.columns[0].visible = false;
|
||||
}
|
||||
if ($(tableContainer).data('type') !== 'snapshot') {
|
||||
json.state.columns[6].visible = false;
|
||||
json.state.columns[7].visible = false;
|
||||
json.state.columns[9].visible = false;
|
||||
json.state.columns[10].visible = false;
|
||||
}
|
||||
if (json.state.search) delete json.state.search;
|
||||
if ($(tableContainer).data('stockConsumptionColumn')) {
|
||||
|
|
|
@ -132,8 +132,8 @@ $.fn.dataTable.render.editRepositoryNumberValue = function(formId, columnId, cel
|
|||
});
|
||||
|
||||
$input.on('input', function() {
|
||||
const regexp = decimals === 0 ? /[^0-9]/g : /[^0-9.]/g;
|
||||
const decimalsRegex = new RegExp(`^\\d*(\\.\\d{0,${decimals}})?`);
|
||||
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];
|
||||
|
|
|
@ -64,9 +64,9 @@ $.fn.dataTable.render.newRepositoryNumberValue = function(formId, columnId, $cel
|
|||
});
|
||||
|
||||
$input.on('input', function() {
|
||||
const decimalsRegex = new RegExp(`^\\d*(\\.\\d{0,${decimals}})?`);
|
||||
const decimalsRegex = new RegExp(`^-?\\d*(\\.\\d{0,${decimals}})?`);
|
||||
let value = this.value;
|
||||
value = value.replace(/[^0-9.]/g, '');
|
||||
value = value.replace(/[^-0-9.]/g, '');
|
||||
value = value.match(decimalsRegex)[0];
|
||||
this.value = value;
|
||||
});
|
||||
|
|
|
@ -641,7 +641,8 @@ var RepositoryDatatable = (function(global) {
|
|||
visible: true,
|
||||
render: function(data, type, row) {
|
||||
return "<a href='" + row.recordInfoUrl + "'"
|
||||
+ "class='record-info-link' data-e2e='e2e-TL-invInventoryTR-Item-" + row.DT_RowId + "'>" + data + '</a>';
|
||||
+ " class='record-info-link' data-e2e='e2e-TL-invInventoryTR-Item-" + row.DT_RowId + "'"
|
||||
+ " title='" + data + "'>" + data + '</a>';
|
||||
}
|
||||
}, {
|
||||
targets: 4,
|
||||
|
@ -661,6 +662,14 @@ var RepositoryDatatable = (function(global) {
|
|||
targets: 6,
|
||||
class: 'added-by',
|
||||
visible: true
|
||||
}, {
|
||||
targets: 7,
|
||||
class: 'updated-on',
|
||||
visible: true
|
||||
}, {
|
||||
targets: 8,
|
||||
class: 'updated-by',
|
||||
visible: true
|
||||
}, {
|
||||
targets: '_all',
|
||||
render: function(data) {
|
||||
|
@ -751,8 +760,8 @@ var RepositoryDatatable = (function(global) {
|
|||
var state = localStorage.getItem(`datatables_repositories_state/${repositoryId}/${viewType}`);
|
||||
|
||||
json.state.start = state !== null ? JSON.parse(state).start : 0;
|
||||
if (json.state.columns[7]) json.state.columns[7].visible = archived;
|
||||
if (json.state.columns[8]) json.state.columns[8].visible = archived;
|
||||
if (json.state.columns[9]) json.state.columns[9].visible = archived;
|
||||
if (json.state.columns[10]) json.state.columns[10].visible = archived;
|
||||
if (json.state.search) delete json.state.search;
|
||||
|
||||
if (json.state.ColSizes && json.state.ColSizes.length > 0) {
|
||||
|
|
|
@ -33,6 +33,15 @@
|
|||
$('#parse-records-modal').modal('show');
|
||||
repositoryRecordsImporter();
|
||||
});
|
||||
|
||||
// Handling cancel click in #parse-records-modal
|
||||
$('#parse-records-modal').on('click', '#parse-records-cancel-btn', () => {
|
||||
$('#parse-records-modal').modal('hide');
|
||||
// remove previous modal (necessary to get the new relevant data instead of old data)
|
||||
setTimeout(() => {
|
||||
$('#parse-records-modal').remove();
|
||||
}, 200);
|
||||
});
|
||||
}
|
||||
|
||||
function initTable() {
|
||||
|
@ -56,7 +65,7 @@
|
|||
data.responseJSON.message + '</span>');
|
||||
});
|
||||
|
||||
submitBtn.on('click', function(event) {
|
||||
submitBtn.one('click', (event) => {
|
||||
var data = new FormData();
|
||||
submitBtn.attr('disabled', true);
|
||||
$('#parse-sheet-loader').removeClass('hidden');
|
||||
|
@ -78,8 +87,12 @@
|
|||
|
||||
function initImportRecordsModal() {
|
||||
$('.repository-show').on('click', '#importRecordsButton', function() {
|
||||
$('#modal-import-records').modal('show');
|
||||
initParseRecordsModal();
|
||||
window.importRepositoryModalComponent.open();
|
||||
});
|
||||
|
||||
// Handling cancel click in #modal-import-records
|
||||
$('#modal-import-records').on('click', '#import-records-cancel-btn', () => {
|
||||
$('#modal-import-records').modal('hide');
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -202,10 +202,14 @@ var MarvinJsEditorApi = (function() {
|
|||
}
|
||||
$(marvinJsModal).modal('hide');
|
||||
|
||||
config.editor.focus();
|
||||
if (config.editor) config.editor.focus();
|
||||
|
||||
config.button.dataset.inProgress = false;
|
||||
|
||||
if (MarvinJsEditor.saveCallback) MarvinJsEditor.saveCallback();
|
||||
if (MarvinJsEditor.saveCallback) {
|
||||
MarvinJsEditor.saveCallback();
|
||||
delete MarvinJsEditor.saveCallback;
|
||||
}
|
||||
},
|
||||
error: function(response) {
|
||||
if (response.status === 403) {
|
||||
|
@ -237,7 +241,9 @@ var MarvinJsEditorApi = (function() {
|
|||
enabled: function() {
|
||||
return ($('#MarvinJsModal').length > 0);
|
||||
},
|
||||
|
||||
isRemote: function() {
|
||||
return marvinJsMode === 'remote';
|
||||
},
|
||||
open: function(config) {
|
||||
if (!MarvinJsEditor.enabled()) {
|
||||
$('#MarvinJsPromoModal').modal('show');
|
||||
|
@ -262,8 +268,8 @@ var MarvinJsEditorApi = (function() {
|
|||
MarvinJsEditor.save(config);
|
||||
} else if (config.mode === 'edit') {
|
||||
config.objectType = 'Asset';
|
||||
MarvinJsEditor.saveCallback = (() => window.location.reload());
|
||||
MarvinJsEditor.update(config);
|
||||
location.reload();
|
||||
} else if (config.mode === 'new-tinymce') {
|
||||
config.objectType = 'TinyMceAsset';
|
||||
MarvinJsEditor.save(config);
|
||||
|
@ -322,21 +328,22 @@ $(document).on('click', '.gene-sequence-edit-button', function() {
|
|||
function initMarvinJs() {
|
||||
MarvinJsEditor = MarvinJsEditorApi();
|
||||
|
||||
let isRemote = $('#marvinjs-editor')[0].dataset.marvinjsMode === 'remote';
|
||||
// MarvinJS is disabled, nothing to initialize
|
||||
if (!MarvinJsEditor.enabled()) return;
|
||||
|
||||
if (MarvinJsEditor.enabled()) {
|
||||
if (isRemote && typeof (ChemicalizeMarvinJs) === 'undefined') {
|
||||
setTimeout(initMarvinJs, 100);
|
||||
return;
|
||||
}
|
||||
|
||||
if (isRemote) {
|
||||
ChemicalizeMarvinJs.createEditor('#marvinjs-sketch').then(function(marvin) {
|
||||
marvin.setDisplaySettings({ toolbars: 'reporting' });
|
||||
marvinJsRemoteEditor = marvin;
|
||||
});
|
||||
}
|
||||
// wait for remote MarvinJS to initialize
|
||||
if (MarvinJsEditor.isRemote() && typeof (ChemicalizeMarvinJs) === 'undefined') {
|
||||
setTimeout(initMarvinJs, 100);
|
||||
return;
|
||||
}
|
||||
|
||||
if (MarvinJsEditor.isRemote()) {
|
||||
ChemicalizeMarvinJs.createEditor('#marvinjs-sketch').then(function(marvin) {
|
||||
marvin.setDisplaySettings({ toolbars: 'reporting' });
|
||||
marvinJsRemoteEditor = marvin;
|
||||
});
|
||||
}
|
||||
|
||||
MarvinJsEditor.initNewButton('.new-marvinjs-upload-button');
|
||||
}
|
||||
|
||||
|
|
|
@ -53,15 +53,21 @@ function prepareRepositoryHeaderForExport(th) {
|
|||
case 'added-on':
|
||||
val = -6;
|
||||
break;
|
||||
case 'archived-by':
|
||||
case 'updated-on':
|
||||
val = -7;
|
||||
break;
|
||||
case 'archived-on':
|
||||
case 'updated-by':
|
||||
val = -8;
|
||||
break;
|
||||
case 'relationship':
|
||||
case 'archived-by':
|
||||
val = -9;
|
||||
break;
|
||||
case 'archived-on':
|
||||
val = -10;
|
||||
break;
|
||||
case 'relationship':
|
||||
val = -11;
|
||||
break;
|
||||
default:
|
||||
val = th.attr('id');
|
||||
}
|
||||
|
|
|
@ -14,6 +14,8 @@
|
|||
initDisclaimer('.log-in-button', '#new_user');
|
||||
initDisclaimer('.btn-okta', '#oktaForm');
|
||||
initDisclaimer('.btn-azure-ad', '.azureAdForm');
|
||||
initDisclaimer('.btn-openid-connect', '#openidConnectForm');
|
||||
initDisclaimer('.btn-saml', '#samlForm');
|
||||
initDisclaimer('.sign-up-button', '#sign-up-form');
|
||||
initDisclaimer('.forgot-password-submit', '#forgot-password-form');
|
||||
initDisclaimer('.invitation-submit', '#invitation-form');
|
||||
|
|
|
@ -115,7 +115,9 @@
|
|||
|
||||
.task-section-header {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
display: grid;
|
||||
gap: 1rem;
|
||||
grid-template-columns: auto auto;
|
||||
min-height: 4rem;
|
||||
|
||||
.actions-block {
|
||||
|
|
|
@ -125,12 +125,6 @@
|
|||
z-index: 2;
|
||||
}
|
||||
}
|
||||
|
||||
.asset-context-menu {
|
||||
position: absolute;
|
||||
right: 1rem;
|
||||
top: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
.inline-attachment-container {
|
||||
|
@ -209,8 +203,8 @@
|
|||
grid-template-columns: max-content max-content;
|
||||
}
|
||||
|
||||
.asset-context-menu {
|
||||
margin-left: auto;
|
||||
.inline-attachment-action-buttons {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -218,6 +212,17 @@
|
|||
padding: 5em 1em 1em;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
&:hover,
|
||||
&.context-menu-open,
|
||||
&.menu-dropdown-open {
|
||||
.header {
|
||||
.inline-attachment-action-buttons {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.list-attachment-container {
|
||||
|
@ -225,7 +230,7 @@
|
|||
align-items: center;
|
||||
display: flex;
|
||||
grid-column: 1/-1;
|
||||
height: 3em;
|
||||
height: 40px;
|
||||
padding: .5em;
|
||||
|
||||
.file-icon {
|
||||
|
@ -267,7 +272,96 @@
|
|||
}
|
||||
|
||||
.asset-context-menu {
|
||||
margin-left: 1rem;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.icon-btn {
|
||||
height: 36px;
|
||||
width: 36px;
|
||||
}
|
||||
|
||||
// normal screen
|
||||
@media (min-width: 640px) {
|
||||
|
||||
&:hover {
|
||||
|
||||
|
||||
#action-buttons {
|
||||
display: flex;
|
||||
|
||||
.icon-btn {
|
||||
&:hover {
|
||||
background-color: var(--sn-light-grey);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#file-metadata {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
// context menu dropdown is open
|
||||
&.context-menu-open,
|
||||
&.menu-dropdown-open {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
#action-buttons {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
#file-metadata {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
// context menu dropdown is closed
|
||||
#action-buttons {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// small screens
|
||||
@media (max-width: 640px) {
|
||||
display: grid;
|
||||
height: 60px;
|
||||
|
||||
#file-metadata {
|
||||
margin-left: 2rem;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
#action-buttons {
|
||||
display: block;
|
||||
margin-bottom: 20px;
|
||||
position: absolute;
|
||||
right: 144px;
|
||||
|
||||
.icon-btn {
|
||||
&:hover {
|
||||
background-color: var(--sn-light-grey);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// context menu dropdown is open
|
||||
&.context-menu-open {
|
||||
|
||||
#action-buttons {
|
||||
display: block;
|
||||
margin-bottom: 20px;
|
||||
position: absolute;
|
||||
right: 144px;
|
||||
}
|
||||
}
|
||||
|
||||
// context menu dropdown is closed
|
||||
#action-buttons {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
|
||||
:root {
|
||||
--attachment-column-width: 13.625rem;
|
||||
--attachment-row-height: 2.5rem;
|
||||
--attachment-row-height: 3rem;
|
||||
}
|
||||
|
||||
|
||||
|
@ -88,6 +88,14 @@
|
|||
}
|
||||
}
|
||||
|
||||
.attachments:has(> .list-attachment-container) {
|
||||
grid-row-gap: 10px;
|
||||
|
||||
@media (max-width: 640px) {
|
||||
grid-row-gap: 30px;
|
||||
}
|
||||
}
|
||||
|
||||
.add-file-modal {
|
||||
.file-drop-zone {
|
||||
align-items: center;
|
||||
|
|
|
@ -103,4 +103,8 @@
|
|||
@apply absolute -bottom-5 text-sn-alert-passion text-xs;
|
||||
content: attr(data-error);
|
||||
}
|
||||
|
||||
.sci-error-text {
|
||||
@apply text-xs text-sn-alert-passion;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -40,7 +40,7 @@
|
|||
}
|
||||
|
||||
.modal .modal-dialog.modal-lg {
|
||||
@apply w-[900px];
|
||||
@apply max-w-[900px] w-full;
|
||||
}
|
||||
|
||||
.modal.fade .modal-dialog {
|
||||
|
|
|
@ -385,21 +385,11 @@ mark,.mark {
|
|||
.azure-sign-in-actions {
|
||||
margin-bottom: 10px;
|
||||
margin-top: 10px;
|
||||
|
||||
.btn-azure-ad {
|
||||
background-color: $office-ms-word;
|
||||
color: $color-white;
|
||||
}
|
||||
}
|
||||
|
||||
.okta-sign-in-actions {
|
||||
margin-bottom: 10px;
|
||||
margin-top: 10px;
|
||||
|
||||
.btn-okta {
|
||||
background-color: #00297a;
|
||||
color: $color-white;
|
||||
}
|
||||
}
|
||||
|
||||
.navbar-secondary {
|
||||
|
|
|
@ -47,6 +47,7 @@ module Api
|
|||
end
|
||||
|
||||
def destroy
|
||||
@inventory_item.update!(last_modified_by: current_user)
|
||||
@inventory_cell.destroy!
|
||||
render body: nil
|
||||
end
|
||||
|
|
|
@ -10,6 +10,7 @@ module Dashboard
|
|||
|
||||
def create_task
|
||||
my_module = CreateMyModuleService.new(current_user, current_team,
|
||||
my_module: create_my_module_params,
|
||||
project: @project || create_project_params,
|
||||
experiment: @experiment || create_experiment_params).call
|
||||
if my_module.errors.blank?
|
||||
|
@ -25,16 +26,16 @@ module Dashboard
|
|||
.page(params[:page] || 1)
|
||||
.per(Constants::SEARCH_LIMIT)
|
||||
.select(:id, :name)
|
||||
projects = projects.map { |i| { value: i.id, label: escape_input(i.name) } }
|
||||
if (projects.map { |i| i[:label] }.exclude? params[:query]) && params[:query].present?
|
||||
projects = [{ value: 0, label: params[:query] }] + projects
|
||||
projects = projects.map { |i| [i.id, escape_input(i.name)] }
|
||||
if (projects.map { |i| i[1] }.exclude? params[:query]) && params[:query].present?
|
||||
projects = [[0, params[:query]]] + projects
|
||||
end
|
||||
render json: projects, status: :ok
|
||||
render json: { data: projects }, status: :ok
|
||||
end
|
||||
|
||||
def experiment_filter
|
||||
if create_project_params.present? && params[:query].present?
|
||||
experiments = [{ value: 0, label: params[:query] }]
|
||||
experiments = [[0, params[:query]]]
|
||||
elsif @project
|
||||
experiments = @project.experiments
|
||||
.managable_by_user(current_user)
|
||||
|
@ -42,20 +43,24 @@ module Dashboard
|
|||
.page(params[:page] || 1)
|
||||
.per(Constants::SEARCH_LIMIT)
|
||||
.select(:id, :name)
|
||||
experiments = experiments.map { |i| { value: i.id, label: escape_input(i.name) } }
|
||||
if (experiments.map { |i| i[:label] }.exclude? params[:query]) &&
|
||||
experiments = experiments.map { |i| [i.id, escape_input(i.name)] }
|
||||
if (experiments.map { |i| i[1] }.exclude? params[:query]) &&
|
||||
params[:query].present? &&
|
||||
can_create_project_experiments?(@project)
|
||||
experiments = [{ value: 0, label: params[:query] }] + experiments
|
||||
experiments = [[0, params[:query]]] + experiments
|
||||
end
|
||||
end
|
||||
render json: experiments || [], status: :ok
|
||||
render json: { data: experiments || [] }, status: :ok
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def create_my_module_params
|
||||
params.require(:my_module).permit(:name)
|
||||
end
|
||||
|
||||
def create_project_params
|
||||
params.require(:project).permit(:name, :visibility)
|
||||
params.require(:project).permit(:name, :visibility, :default_public_user_role_id)
|
||||
end
|
||||
|
||||
def create_experiment_params
|
||||
|
|
|
@ -3,12 +3,13 @@
|
|||
class MyModuleRepositoriesController < ApplicationController
|
||||
include ApplicationHelper
|
||||
|
||||
before_action :load_my_module
|
||||
before_action :load_my_module, except: :assign_my_modules
|
||||
before_action :load_repository, except: %i(repositories_dropdown_list repositories_list_html create)
|
||||
before_action :check_my_module_view_permissions, except: %i(update consume_modal update_consumption)
|
||||
before_action :check_my_module_view_permissions, except: %i(update consume_modal update_consumption assign_my_modules)
|
||||
before_action :check_repository_view_permissions, except: %i(repositories_dropdown_list repositories_list_html create)
|
||||
before_action :check_repository_row_consumption_permissions, only: %i(consume_modal update_consumption)
|
||||
before_action :check_assign_repository_records_permissions, only: %i(update create)
|
||||
before_action :load_my_modules, only: :assign_my_modules
|
||||
|
||||
def index_dt
|
||||
@draw = params[:draw].to_i
|
||||
|
@ -41,6 +42,31 @@ class MyModuleRepositoriesController < ApplicationController
|
|||
render rows_view
|
||||
end
|
||||
|
||||
def assign_my_modules
|
||||
assigned_count = 0
|
||||
skipped_count = 0
|
||||
status = :ok
|
||||
|
||||
ActiveRecord::Base.transaction do
|
||||
@my_modules.find_each do |my_module|
|
||||
service = RepositoryRows::MyModuleAssignUnassignService.call(
|
||||
my_module:,
|
||||
repository: @repository,
|
||||
user: current_user,
|
||||
params:
|
||||
)
|
||||
unless service.succeed?
|
||||
status = :unprocessable_entity
|
||||
raise ActiveRecord::Rollback
|
||||
end
|
||||
assigned_count += service.assigned_rows_count
|
||||
skipped_count += (params[:rows_to_assign].length - service.assigned_rows_count)
|
||||
end
|
||||
end
|
||||
|
||||
render json: { assigned_count:, skipped_count: }, status:
|
||||
end
|
||||
|
||||
def create
|
||||
repository_row = RepositoryRow.find(params[:repository_row_id])
|
||||
repository = repository_row.repository
|
||||
|
@ -215,6 +241,12 @@ class MyModuleRepositoriesController < ApplicationController
|
|||
render_404 unless @repository
|
||||
end
|
||||
|
||||
def load_my_modules
|
||||
@my_modules = MyModule.where(id: params[:my_module_ids])
|
||||
|
||||
render_403 unless @my_modules.all? { |my_module| can_assign_my_module_repository_rows?(my_module) }
|
||||
end
|
||||
|
||||
def check_my_module_view_permissions
|
||||
render_403 unless can_read_my_module?(@my_module)
|
||||
end
|
||||
|
|
|
@ -19,10 +19,10 @@ class NavigationsController < ApplicationController
|
|||
end
|
||||
|
||||
def navigator_state
|
||||
session[:navigator_collapsed] = params[:state] == 'collapsed'
|
||||
current_user.update_simple_setting(key: 'navigator_collapsed', value: params[:state] == 'collapsed')
|
||||
|
||||
width = params[:width].to_i
|
||||
session[:navigator_width] = width if width.positive?
|
||||
current_user.update_simple_setting(key: 'navigator_width', value: width) if width.positive?
|
||||
end
|
||||
|
||||
private
|
||||
|
|
|
@ -27,6 +27,7 @@ class RepositoriesController < ApplicationController
|
|||
before_action :set_inline_name_editing, only: %i(show)
|
||||
before_action :load_repository_row, only: %i(show)
|
||||
before_action :set_breadcrumbs_items, only: %i(index show)
|
||||
before_action :validate_file_type, only: %i(export_repository export_repositories)
|
||||
|
||||
layout 'fluid'
|
||||
|
||||
|
@ -36,7 +37,7 @@ class RepositoriesController < ApplicationController
|
|||
render 'index'
|
||||
end
|
||||
format.json do
|
||||
repositories = Lists::RepositoriesService.new(@repositories, params).call
|
||||
repositories = Lists::RepositoriesService.new(@repositories, params, user: current_user).call
|
||||
render json: repositories, each_serializer: Lists::RepositorySerializer, user: current_user,
|
||||
meta: pagination_dict(repositories)
|
||||
end
|
||||
|
@ -53,13 +54,21 @@ class RepositoriesController < ApplicationController
|
|||
end
|
||||
|
||||
def show
|
||||
current_team_switch(@repository.team) unless @repository.shared_with?(current_team)
|
||||
@display_edit_button = can_create_repository_rows?(@repository)
|
||||
@display_delete_button = can_delete_repository_rows?(@repository)
|
||||
@display_duplicate_button = can_create_repository_rows?(@repository)
|
||||
@snapshot_provisioning = @repository.repository_snapshots.provisioning.any?
|
||||
respond_to do |format|
|
||||
format.html do
|
||||
current_team_switch(@repository.team) unless @repository.shared_with?(current_team)
|
||||
@display_edit_button = can_create_repository_rows?(@repository)
|
||||
@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
|
||||
@busy_printer = LabelPrinter.where.not(current_print_job_ids: []).first
|
||||
end
|
||||
format.json do
|
||||
# render serialized repository json
|
||||
render json: @repository, serializer: RepositorySerializer
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def table_toolbar
|
||||
|
@ -207,29 +216,6 @@ class RepositoriesController < ApplicationController
|
|||
}
|
||||
end
|
||||
|
||||
def export_modal
|
||||
if current_user.has_available_exports?
|
||||
render json: {
|
||||
html: render_to_string(
|
||||
partial: 'export_repositories_modal',
|
||||
locals: { team_name: current_team.name,
|
||||
counter: params[:counter].to_i,
|
||||
export_limit: TeamZipExport.exports_limit,
|
||||
num_of_requests_left: current_user.exports_left - 1 },
|
||||
formats: :html
|
||||
)
|
||||
}
|
||||
else
|
||||
render json: {
|
||||
html: render_to_string(
|
||||
partial: 'export_limit_exceeded_modal',
|
||||
locals: { requests_limit: TeamZipExport.exports_limit },
|
||||
formats: :html
|
||||
)
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
def copy
|
||||
@tmp_repository = Repository.new(
|
||||
team: current_team,
|
||||
|
@ -271,81 +257,99 @@ class RepositoriesController < ApplicationController
|
|||
render_403 unless can_create_repository_rows?(@repository)
|
||||
|
||||
unless import_params[:file]
|
||||
repository_response(t('repositories.parse_sheet.errors.no_file_selected'))
|
||||
unprocessable_entity_repository_response(t('repositories.parse_sheet.errors.no_file_selected'))
|
||||
return
|
||||
end
|
||||
begin
|
||||
parsed_file = ImportRepository::ParseRepository.new(
|
||||
file: import_params[:file],
|
||||
repository: @repository,
|
||||
date_format: current_user.settings['date_format'],
|
||||
session: session
|
||||
)
|
||||
if parsed_file.too_large?
|
||||
repository_response(t('general.file.size_exceeded',
|
||||
file_size: Rails.configuration.x.file_max_size_mb))
|
||||
render json: { error: t('general.file.size_exceeded', file_size: Rails.configuration.x.file_max_size_mb) },
|
||||
status: :unprocessable_entity
|
||||
elsif parsed_file.has_too_many_rows?
|
||||
repository_response(
|
||||
t('repositories.import_records.error_message.items_limit',
|
||||
items_size: Constants::IMPORT_REPOSITORY_ITEMS_LIMIT)
|
||||
)
|
||||
render json: { error: t('repositories.import_records.error_message.items_limit',
|
||||
items_size: Constants::IMPORT_REPOSITORY_ITEMS_LIMIT) }, status: :unprocessable_entity
|
||||
elsif parsed_file.has_too_little_rows?
|
||||
render json: { error: t('repositories.parse_sheet.errors.items_min_limit') },
|
||||
status: :unprocessable_entity
|
||||
else
|
||||
@import_data = parsed_file.data
|
||||
|
||||
if @import_data.header.blank? || @import_data.columns.blank?
|
||||
return repository_response(t('repositories.parse_sheet.errors.empty_file'))
|
||||
return render json: { error: t('repositories.parse_sheet.errors.empty_file') }, status: :unprocessable_entity
|
||||
end
|
||||
|
||||
if (@temp_file = parsed_file.generate_temp_file)
|
||||
render json: {
|
||||
html: render_to_string(partial: 'repositories/parse_records_modal', formats: :html)
|
||||
}
|
||||
render json: { import_data: @import_data, temp_file: @temp_file }
|
||||
else
|
||||
repository_response(t('repositories.parse_sheet.errors.temp_file_failure'))
|
||||
render json: { error: t('repositories.parse_sheet.errors.temp_file_failure') }, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
rescue ArgumentError, CSV::MalformedCSVError
|
||||
repository_response(t('repositories.parse_sheet.errors.invalid_file',
|
||||
encoding: ''.encoding))
|
||||
render json: { error: t('repositories.parse_sheet.errors.invalid_file', encoding: ''.encoding) },
|
||||
status: :unprocessable_entity
|
||||
rescue TypeError
|
||||
repository_response(t('repositories.parse_sheet.errors.invalid_extension'))
|
||||
render json: { error: t('repositories.parse_sheet.errors.invalid_extension') }, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
def import_records
|
||||
render_403 unless can_create_repository_rows?(Repository.accessible_by_teams(current_team)
|
||||
.find_by_id(import_params[:id]))
|
||||
|
||||
.find_by(id: import_params[:id]))
|
||||
# Check if there exist mapping for repository record (it's mandatory)
|
||||
if import_params[:mappings].value?('-1')
|
||||
import_records = repostiory_import_actions
|
||||
status = import_records.import!
|
||||
if import_params[:mappings].present? && import_params[:mappings].value?('-1')
|
||||
status = ImportRepository::ImportRecords
|
||||
.new(
|
||||
temp_file: TempFile.find_by(id: import_params[:file_id]),
|
||||
repository: Repository.accessible_by_teams(current_team).find_by(id: import_params[:id]),
|
||||
mappings: import_params[:mappings],
|
||||
session: session,
|
||||
user: current_user,
|
||||
can_edit_existing_items: import_params[:can_edit_existing_items],
|
||||
should_overwrite_with_empty_cells: import_params[:should_overwrite_with_empty_cells],
|
||||
preview: import_params[:preview]
|
||||
).import!
|
||||
message = t('repositories.import_records.partial_success_flash',
|
||||
successful_rows_count: (status[:created_rows_count] + status[:updated_rows_count]),
|
||||
total_rows_count: status[:total_rows_count])
|
||||
|
||||
if status[:status] == :ok
|
||||
log_activity(:import_inventory_items,
|
||||
num_of_items: status[:nr_of_added])
|
||||
unless import_params[:preview] || (status[:created_rows_count] + status[:updated_rows_count]).zero?
|
||||
log_activity(
|
||||
:inventory_items_added_or_updated_with_import,
|
||||
created_rows_count: status[:created_rows_count],
|
||||
updated_rows_count: status[:updated_rows_count]
|
||||
)
|
||||
end
|
||||
|
||||
flash[:success] = t('repositories.import_records.success_flash',
|
||||
number_of_rows: status[:nr_of_added],
|
||||
total_nr: status[:total_nr])
|
||||
render json: {}, status: :ok
|
||||
render json: import_params[:preview] ? status : { message: message }, status: :ok
|
||||
else
|
||||
flash[:alert] =
|
||||
t('repositories.import_records.partial_success_flash',
|
||||
nr: status[:nr_of_added], total_nr: status[:total_nr])
|
||||
render json: {}, status: :unprocessable_entity
|
||||
render json: { message: message }, status: :unprocessable_entity
|
||||
end
|
||||
else
|
||||
render json: {
|
||||
html: render_to_string(
|
||||
partial: 'shared/flash_errors',
|
||||
formats: :html,
|
||||
locals: { error_title: t('repositories.import_records.error_message.errors_list_title'),
|
||||
error: t('repositories.import_records.error_message.no_repository_name') }
|
||||
)
|
||||
}, status: :unprocessable_entity
|
||||
render json: { error: t('repositories.import_records.error_message.mapping_error') },
|
||||
status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
def export_empty_repository
|
||||
col_ids = [-3, -4, -5, -6, -7, -8, -9, -10]
|
||||
col_ids << -11 if Repository.repository_row_connections_enabled?
|
||||
col_ids += @repository.repository_columns.map(&:id)
|
||||
|
||||
xlsx = RepositoryXlsxExport.to_empty_xlsx(@repository, col_ids)
|
||||
|
||||
send_data(
|
||||
xlsx,
|
||||
filename: "#{@repository.name.gsub(/\s/, '_')}_template_#{Date.current}.xlsx",
|
||||
type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
|
||||
)
|
||||
end
|
||||
|
||||
def export_repository
|
||||
if params[:row_ids] && params[:header_ids]
|
||||
RepositoryZipExportJob.perform_later(
|
||||
|
@ -354,8 +358,10 @@ class RepositoriesController < ApplicationController
|
|||
repository_id: @repository.id,
|
||||
row_ids: params[:row_ids],
|
||||
header_ids: params[:header_ids]
|
||||
}
|
||||
},
|
||||
file_type: params[:file_type]
|
||||
)
|
||||
update_user_export_file_type if current_user.settings[:repository_export_file_type] != params[:file_type]
|
||||
log_activity(:export_inventory_items)
|
||||
render json: { message: t('zip_export.export_request_success') }
|
||||
else
|
||||
|
@ -365,9 +371,10 @@ class RepositoriesController < ApplicationController
|
|||
|
||||
def export_repositories
|
||||
repositories = Repository.viewable_by_user(current_user, current_team).where(id: params[:repository_ids])
|
||||
if repositories.present? && current_user.has_available_exports?
|
||||
current_user.increase_daily_exports_counter!
|
||||
RepositoriesExportJob.perform_later(repositories.pluck(:id), user_id: current_user.id, team_id: current_team.id)
|
||||
if repositories.present?
|
||||
RepositoriesExportJob
|
||||
.perform_later(params[:file_type], repositories.pluck(:id), user_id: current_user.id, team_id: current_team.id)
|
||||
update_user_export_file_type if current_user.settings[:repository_export_file_type] != params[:file_type]
|
||||
log_activity(:export_inventories, inventories: repositories.pluck(:name).join(', '))
|
||||
render json: { message: t('zip_export.export_request_success') }
|
||||
else
|
||||
|
@ -443,16 +450,6 @@ class RepositoriesController < ApplicationController
|
|||
|
||||
private
|
||||
|
||||
def repostiory_import_actions
|
||||
ImportRepository::ImportRecords.new(
|
||||
temp_file: TempFile.find_by_id(import_params[:file_id]),
|
||||
repository: Repository.accessible_by_teams(current_team).find_by_id(import_params[:id]),
|
||||
mappings: import_params[:mappings],
|
||||
session: session,
|
||||
user: current_user
|
||||
)
|
||||
end
|
||||
|
||||
def load_repository
|
||||
repository_id = params[:id] || params[:repository_id]
|
||||
@repository = Repository.accessible_by_teams(current_user.teams).find_by(id: repository_id)
|
||||
|
@ -534,7 +531,8 @@ class RepositoriesController < ApplicationController
|
|||
end
|
||||
|
||||
def import_params
|
||||
params.permit(:id, :file, :file_id, mappings: {}).to_h
|
||||
params.permit(:id, :file, :file_id, :preview, :can_edit_existing_items,
|
||||
:should_overwrite_with_empty_cells, :preview, mappings: {}).to_h
|
||||
end
|
||||
|
||||
def repository_response(message)
|
||||
|
@ -592,4 +590,12 @@ class RepositoriesController < ApplicationController
|
|||
item[:label] = "(A) #{item[:label]}" if item[:archived]
|
||||
end
|
||||
end
|
||||
|
||||
def validate_file_type
|
||||
render json: { message: 'Invalid file type' }, status: :bad_request unless %w(csv xlsx).include?(params[:file_type])
|
||||
end
|
||||
|
||||
def update_user_export_file_type
|
||||
current_user.update_simple_setting(key: 'repository_export_file_type', value: params[:file_type])
|
||||
end
|
||||
end
|
||||
|
|
|
@ -29,6 +29,11 @@ class RepositoryColumnsController < ApplicationController
|
|||
end
|
||||
|
||||
def describe_all
|
||||
additional_columns = [
|
||||
%w(updated_on RepositoryDateTimeValue),
|
||||
%w(updated_by RepositoryUserValue)
|
||||
]
|
||||
|
||||
response_json = @repository.repository_columns
|
||||
.where(data_type: Extends::REPOSITORY_ADVANCED_SEARCHABLE_COLUMNS)
|
||||
.map do |column|
|
||||
|
@ -39,6 +44,12 @@ class RepositoryColumnsController < ApplicationController
|
|||
items: column.items&.map { |item| { value: item.id, label: escape_input(item.data) } }
|
||||
}
|
||||
end
|
||||
|
||||
additional_columns.each do |column, column_type|
|
||||
response_json << { id: column,
|
||||
name: I18n.t("repositories.table.#{column}"),
|
||||
data_type: column_type }
|
||||
end
|
||||
render json: { response: response_json }
|
||||
end
|
||||
|
||||
|
|
|
@ -5,15 +5,18 @@ class ResultOrderableElementsController < ApplicationController
|
|||
before_action :check_manage_permissions
|
||||
|
||||
def reorder
|
||||
position_changed = false
|
||||
ActiveRecord::Base.transaction do
|
||||
params[:result_orderable_element_positions].each do |id, position|
|
||||
result_element = @result.result_orderable_elements.find(id)
|
||||
result_element.insert_at(position)
|
||||
position_changed ||= result_element.insert_at(position)
|
||||
end
|
||||
end
|
||||
|
||||
log_activity(:result_content_rearranged, @my_module.experiment.project, my_module: @my_module.id)
|
||||
@result.touch
|
||||
if position_changed
|
||||
log_activity(:result_content_rearranged, @my_module.experiment.project, my_module: @my_module.id)
|
||||
@result.touch
|
||||
end
|
||||
|
||||
render json: params[:result_orderable_element_positions], status: :ok
|
||||
rescue ActiveRecord::RecordInvalid
|
||||
|
|
|
@ -22,12 +22,13 @@ class ResultsController < ApplicationController
|
|||
@my_module.results.active
|
||||
end
|
||||
|
||||
apply_sort!
|
||||
update_and_apply_user_sort_preference!
|
||||
apply_filters!
|
||||
|
||||
@results = @results.page(params.dig(:page, :number) || 1)
|
||||
|
||||
render json: @results, each_serializer: ResultSerializer, scope: current_user
|
||||
render json: @results, each_serializer: ResultSerializer, scope: current_user,
|
||||
meta: { sort: @sort_preference }
|
||||
end
|
||||
|
||||
format.html do
|
||||
|
@ -121,7 +122,7 @@ class ResultsController < ApplicationController
|
|||
|
||||
def destroy
|
||||
name = @result.name
|
||||
if @result.destroy
|
||||
if @result.discard
|
||||
log_activity(:destroy_result, { destroyed_result: name })
|
||||
render json: {}, status: :ok
|
||||
else
|
||||
|
@ -146,8 +147,18 @@ class ResultsController < ApplicationController
|
|||
params.require(:result).permit(:name)
|
||||
end
|
||||
|
||||
def apply_sort!
|
||||
case params[:sort]
|
||||
def update_and_apply_user_sort_preference!
|
||||
if params[:sort].present?
|
||||
current_user.update_nested_setting(key: 'results_order', id: @my_module.id.to_s, value: params[:sort])
|
||||
@sort_preference = params[:sort]
|
||||
else
|
||||
@sort_preference = current_user.settings.fetch('results_order', {})[@my_module.id.to_s] || 'created_at_desc'
|
||||
end
|
||||
apply_sort!(@sort_preference)
|
||||
end
|
||||
|
||||
def apply_sort!(sort_order)
|
||||
case sort_order
|
||||
when 'updated_at_asc'
|
||||
@results = @results.order('results.updated_at' => :asc)
|
||||
when 'updated_at_desc'
|
||||
|
|
|
@ -6,16 +6,23 @@ class StepOrderableElementsController < ApplicationController
|
|||
|
||||
def reorder
|
||||
@step.with_lock do
|
||||
position_changed = false
|
||||
params[:step_orderable_element_positions].each do |id, position|
|
||||
@step.step_orderable_elements.find(id).update_column(:position, position)
|
||||
step_element = @step.step_orderable_elements.find(id)
|
||||
if step_element.position != position
|
||||
position_changed = true
|
||||
step_element.update_column(:position, position)
|
||||
end
|
||||
end
|
||||
|
||||
if @protocol.in_module?
|
||||
log_activity(:task_step_content_rearranged, @my_module.experiment.project, my_module: @my_module.id)
|
||||
else
|
||||
log_activity(:protocol_step_content_rearranged, nil, protocol: @protocol.id)
|
||||
if position_changed
|
||||
if @protocol.in_module?
|
||||
log_activity(:task_step_content_rearranged, @my_module.experiment.project, my_module: @my_module.id)
|
||||
else
|
||||
log_activity(:protocol_step_content_rearranged, nil, protocol: @protocol.id)
|
||||
end
|
||||
@step.touch
|
||||
end
|
||||
@step.touch
|
||||
end
|
||||
|
||||
render json: params[:step_orderable_element_positions], status: :ok
|
||||
|
|
|
@ -241,16 +241,23 @@ class StepsController < ApplicationController
|
|||
|
||||
def reorder
|
||||
@protocol.with_lock do
|
||||
position_changed = false
|
||||
params[:step_positions].each do |id, position|
|
||||
@protocol.steps.find(id).update_column(:position, position)
|
||||
step = @protocol.steps.find(id)
|
||||
if position != step.position
|
||||
position_changed = true
|
||||
step.update_column(:position, position)
|
||||
end
|
||||
end
|
||||
|
||||
if @protocol.in_module?
|
||||
log_activity(:task_steps_rearranged, @my_module.experiment.project, my_module: @my_module.id)
|
||||
else
|
||||
log_activity(:protocol_steps_rearranged, nil, protocol: @protocol.id)
|
||||
if position_changed
|
||||
if @protocol.in_module?
|
||||
log_activity(:task_steps_rearranged, @my_module.experiment.project, my_module: @my_module.id)
|
||||
else
|
||||
log_activity(:protocol_steps_rearranged, nil, protocol: @protocol.id)
|
||||
end
|
||||
@protocol.touch
|
||||
end
|
||||
@protocol.touch
|
||||
end
|
||||
|
||||
render json: {
|
||||
|
|
|
@ -14,7 +14,14 @@ module Users
|
|||
key = setting[:key]
|
||||
data = setting[:data]
|
||||
|
||||
current_user.settings[key] = data if Extends::WHITELISTED_USER_SETTINGS.include?(key.to_s)
|
||||
next unless Extends::WHITELISTED_USER_SETTINGS.include?(key.to_s)
|
||||
|
||||
case key.to_s
|
||||
when 'task_step_states'
|
||||
update_task_step_states(data)
|
||||
else
|
||||
current_user.settings[key] = data
|
||||
end
|
||||
end
|
||||
|
||||
if current_user.save
|
||||
|
@ -24,6 +31,22 @@ module Users
|
|||
status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def update_task_step_states(task_step_states_data)
|
||||
current_states = current_user.settings.fetch('task_step_states', {})
|
||||
|
||||
task_step_states_data.each do |step_id, collapsed|
|
||||
if collapsed
|
||||
current_states[step_id] = true
|
||||
else
|
||||
current_states.delete(step_id)
|
||||
end
|
||||
end
|
||||
|
||||
current_user.settings['task_step_states'] = current_states
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -239,8 +239,10 @@ module RepositoryDatatableHelper
|
|||
'4': "#{record.parent_connections_count || 0} / #{record.child_connections_count || 0}",
|
||||
'5': I18n.l(record.created_at, format: :full),
|
||||
'6': escape_input(record.created_by.full_name),
|
||||
'7': (record.archived_on ? I18n.l(record.archived_on, format: :full) : ''),
|
||||
'8': escape_input(record.archived_by&.full_name)
|
||||
'7': (record.updated_at ? I18n.l(record.updated_at, format: :full) : ''),
|
||||
'8': escape_input(record.last_modified_by.full_name),
|
||||
'9': (record.archived_on ? I18n.l(record.archived_on, format: :full) : ''),
|
||||
'10': escape_input(record.archived_by&.full_name)
|
||||
}
|
||||
end
|
||||
|
||||
|
|
3
app/javascript/packs/tiny_mce.js
vendored
3
app/javascript/packs/tiny_mce.js
vendored
|
@ -326,6 +326,9 @@ window.TinyMCE = (() => {
|
|||
// Remove transition class
|
||||
$('.tox-editor-header').removeClass('tox-editor-dock-fadeout');
|
||||
|
||||
// Fixes the overflowing vertical controls bar for inserted text
|
||||
$('.tox-editor-header').css('display', 'contents');
|
||||
|
||||
// Init image toolbar
|
||||
initCssOverrides(editor);
|
||||
|
||||
|
|
16
app/javascript/packs/vue/dashboard_new_task.js
Normal file
16
app/javascript/packs/vue/dashboard_new_task.js
Normal file
|
@ -0,0 +1,16 @@
|
|||
import { createApp } from 'vue/dist/vue.esm-bundler.js';
|
||||
import PerfectScrollbar from 'vue3-perfect-scrollbar';
|
||||
import DashboardNewTask from '../../vue/dashboard/new_task.vue';
|
||||
import { mountWithTurbolinks } from './helpers/turbolinks.js';
|
||||
|
||||
const app = createApp({
|
||||
data() {
|
||||
return {
|
||||
modalKey: 0
|
||||
};
|
||||
}
|
||||
});
|
||||
app.component('DashboardNewTask', DashboardNewTask);
|
||||
app.config.globalProperties.i18n = window.I18n;
|
||||
app.use(PerfectScrollbar);
|
||||
mountWithTurbolinks(app, '#DashboardNewTask');
|
|
@ -2,6 +2,7 @@ import { createApp } from 'vue/dist/vue.esm-bundler.js';
|
|||
import { shallowRef } from 'vue';
|
||||
|
||||
import WizardModal from '../../../vue/shared/wizard_modal.vue';
|
||||
import InfoModal from '../../../vue/shared/info_modal.vue';
|
||||
import Step1 from './wizard_steps/step_1.vue';
|
||||
import Step2 from './wizard_steps/step_2.vue';
|
||||
import Step3 from './wizard_steps/step_3.vue';
|
||||
|
@ -20,25 +21,26 @@ const app = createApp({
|
|||
},
|
||||
data() {
|
||||
return {
|
||||
// Wizard modal
|
||||
wizardConfig: {
|
||||
title: 'Wizard steps',
|
||||
subtitle: 'Wizard subtitle description',
|
||||
steps: [
|
||||
{
|
||||
id: 'step1',
|
||||
icon: 'sn-icon sn-icon-open',
|
||||
icon: 'sn-icon-open',
|
||||
label: 'Step 1',
|
||||
component: shallowRef(Step1)
|
||||
},
|
||||
{
|
||||
id: 'step2',
|
||||
icon: 'sn-icon sn-icon-edit',
|
||||
icon: 'sn-icon-edit',
|
||||
label: 'Step 2',
|
||||
component: shallowRef(Step2)
|
||||
},
|
||||
{
|
||||
id: 'step3',
|
||||
icon: 'sn-icon sn-icon-inventory',
|
||||
icon: 'sn-icon-inventory',
|
||||
label: 'Step 3',
|
||||
component: shallowRef(Step3)
|
||||
}
|
||||
|
@ -47,10 +49,49 @@ const app = createApp({
|
|||
wizardParams: {
|
||||
text: 'Some text'
|
||||
},
|
||||
showWizard: false
|
||||
showWizard: false,
|
||||
|
||||
// Info modal
|
||||
infoParams: {
|
||||
title: 'Guide for updating the inventory',
|
||||
elements: [
|
||||
{
|
||||
id: 'el1',
|
||||
icon: 'sn-icon-export',
|
||||
label: 'Export inventory',
|
||||
subtext: "Before making edits, we advise you to export the latest inventory information. If you're only adding new items, consider exporting empty inventory."
|
||||
},
|
||||
{
|
||||
id: 'el2',
|
||||
icon: 'sn-icon-edit',
|
||||
label: 'Edit your data',
|
||||
subtext: 'Make sure to include header names in first row, followed by item data.'
|
||||
},
|
||||
{
|
||||
id: 'el3',
|
||||
icon: 'sn-icon-import',
|
||||
label: 'Import new or update items',
|
||||
subtext: 'Upload your data using .xlsx, .csv or .txt files.'
|
||||
},
|
||||
{
|
||||
id: 'el4',
|
||||
icon: 'sn-icon-tables',
|
||||
label: 'Merge your data',
|
||||
subtext: 'Complete the process by merging the columns you want to update.'
|
||||
},
|
||||
{
|
||||
id: 'el5',
|
||||
icon: 'sn-icon-open',
|
||||
label: 'Learn more',
|
||||
linkTo: 'https://knowledgebase.scinote.net/en/knowledge/how-to-add-items-to-an-inventory'
|
||||
}
|
||||
]
|
||||
},
|
||||
showInfo: false
|
||||
};
|
||||
}
|
||||
});
|
||||
app.component('WizardModal', WizardModal);
|
||||
app.component('InfoModal', InfoModal);
|
||||
app.config.globalProperties.i18n = window.I18n;
|
||||
mountWithTurbolinks(app, '#modals');
|
||||
|
|
11
app/javascript/packs/vue/import_repository_modal.js
Normal file
11
app/javascript/packs/vue/import_repository_modal.js
Normal file
|
@ -0,0 +1,11 @@
|
|||
import PerfectScrollbar from 'vue3-perfect-scrollbar';
|
||||
import { createApp } from 'vue/dist/vue.esm-bundler.js';
|
||||
import 'vue3-perfect-scrollbar/dist/vue3-perfect-scrollbar.css';
|
||||
import ImportRepositoryModal from '../../vue/repositories/modals/import/container.vue';
|
||||
import { mountWithTurbolinks } from './helpers/turbolinks.js';
|
||||
|
||||
const app = createApp({});
|
||||
app.component('ImportRepositoryModal', ImportRepositoryModal);
|
||||
app.use(PerfectScrollbar);
|
||||
app.config.globalProperties.i18n = window.I18n;
|
||||
mountWithTurbolinks(app, '#importRepositoryModal');
|
|
@ -94,7 +94,7 @@
|
|||
</label>
|
||||
|
||||
<SelectDropdown
|
||||
:value="selectedTask"
|
||||
:value="selectedTasks"
|
||||
:disabled="!selectedExperiment"
|
||||
:searchable="true"
|
||||
ref="tasksSelector"
|
||||
|
@ -102,6 +102,8 @@
|
|||
:options="tasks"
|
||||
:isLoading="tasksLoading"
|
||||
:placeholder="tasksSelectorPlaceholder"
|
||||
:multiple="true"
|
||||
:withCheckboxes="true"
|
||||
:no-options-placeholder="
|
||||
i18n.t(
|
||||
'repositories.modal_assign_items_to_task.body.task_select.no_options_placeholder'
|
||||
|
@ -115,7 +117,7 @@
|
|||
type="button"
|
||||
class="btn btn-primary"
|
||||
data-dismiss="modal"
|
||||
:disabled="!selectedTask"
|
||||
:disabled="!selectedTasks.length"
|
||||
@click="assign"
|
||||
>
|
||||
{{ i18n.t("repositories.modal_assign_items_to_task.assign.text") }}
|
||||
|
@ -127,6 +129,7 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
/* global HelperModule */
|
||||
import SelectDropdown from "../shared/select_dropdown.vue";
|
||||
|
||||
export default {
|
||||
|
@ -143,7 +146,7 @@ export default {
|
|||
tasks: [],
|
||||
selectedProject: null,
|
||||
selectedExperiment: null,
|
||||
selectedTask: null,
|
||||
selectedTasks: [],
|
||||
projectsLoading: null,
|
||||
experimentsLoading: null,
|
||||
tasksLoading: null
|
||||
|
@ -206,9 +209,6 @@ export default {
|
|||
taskURL() {
|
||||
return `${this.urls.tasks}?experiment_id=${this.selectedExperiment
|
||||
|| ''}`;
|
||||
},
|
||||
assignURL() {
|
||||
return this.urls.assign.replace(':module_id', this.selectedTask);
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
|
@ -259,7 +259,7 @@ export default {
|
|||
});
|
||||
},
|
||||
changeTask(value) {
|
||||
this.selectedTask = value;
|
||||
this.selectedTasks = value;
|
||||
},
|
||||
resetProjectSelector() {
|
||||
this.projects = [];
|
||||
|
@ -271,7 +271,7 @@ export default {
|
|||
},
|
||||
resetTaskSelector() {
|
||||
this.tasks = [];
|
||||
this.selectedTask = null;
|
||||
this.selectedTasks = [];
|
||||
},
|
||||
resetSelectors() {
|
||||
this.resetTaskSelector();
|
||||
|
@ -279,20 +279,33 @@ export default {
|
|||
this.resetProjectSelector();
|
||||
},
|
||||
assign() {
|
||||
if (!this.selectedTask) return;
|
||||
if (!this.selectedTasks.length) return;
|
||||
|
||||
$.ajax({
|
||||
url: this.assignURL,
|
||||
type: 'PATCH',
|
||||
url: this.urls.assign,
|
||||
type: 'POST',
|
||||
dataType: 'json',
|
||||
data: { rows_to_assign: this.rowsToAssign }
|
||||
}).done(({ assigned_count }) => {
|
||||
const skipped_count = this.rowsToAssign.length - assigned_count;
|
||||
|
||||
if (skipped_count) {
|
||||
HelperModule.flashAlertMsg(this.i18n.t('repositories.modal_assign_items_to_task.assign.flash_some_assignments_success', { assigned_count, skipped_count }), 'success');
|
||||
data: {
|
||||
rows_to_assign: this.rowsToAssign,
|
||||
my_module_ids: this.selectedTasks
|
||||
}
|
||||
}).done(({ assigned_count: assignedCount, skipped_count: skippedCount }) => {
|
||||
if (skippedCount) {
|
||||
HelperModule.flashAlertMsg(
|
||||
this.i18n.t(
|
||||
'repositories.modal_assign_items_to_task.assign.flash_some_assignments_success',
|
||||
{ assigned_count: assignedCount, skipped_count: skippedCount }
|
||||
),
|
||||
'success'
|
||||
);
|
||||
} else {
|
||||
HelperModule.flashAlertMsg(this.i18n.t('repositories.modal_assign_items_to_task.assign.flash_all_assignments_success', { count: assigned_count }), 'success');
|
||||
HelperModule.flashAlertMsg(
|
||||
this.i18n.t(
|
||||
'repositories.modal_assign_items_to_task.assign.flash_all_assignments_success',
|
||||
{ count: assignedCount }
|
||||
),
|
||||
'success'
|
||||
);
|
||||
}
|
||||
}).fail(() => {
|
||||
HelperModule.flashAlertMsg(this.i18n.t('repositories.modal_assign_items_to_task.assign.flash_assignments_failure'), 'danger');
|
||||
|
@ -309,7 +322,7 @@ export default {
|
|||
.dataTable()
|
||||
.api()
|
||||
.ajax
|
||||
.reload();
|
||||
.reload(null, false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
218
app/javascript/vue/dashboard/new_task.vue
Normal file
218
app/javascript/vue/dashboard/new_task.vue
Normal file
|
@ -0,0 +1,218 @@
|
|||
<template>
|
||||
<div>
|
||||
<div class="mb-4">
|
||||
<label class="sci-label">{{ i18n.t("dashboard.create_task_modal.task_name") }}</label>
|
||||
<div class="sci-input-container-v2" :class="{
|
||||
'error': !validTaskName && taskName.length > 0
|
||||
}" >
|
||||
<input type="text" ref="taskName" class="sci-input" v-model="taskName" :placeholder="i18n.t('dashboard.create_task_modal.task_name_placeholder')" />
|
||||
</div>
|
||||
<span v-if="!validTaskName && taskName.length > 0" class="sci-error-text">
|
||||
{{ i18n.t("dashboard.create_task_modal.task_name_error", { length: minLength }) }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<label class="sci-label">{{ i18n.t("dashboard.create_task_modal.project") }}</label>
|
||||
<SelectDropdown
|
||||
:optionsUrl="projectsUrl"
|
||||
:searchable="true"
|
||||
:value="selectedProject"
|
||||
:optionRenderer="newProjectRenderer"
|
||||
:placeholder="i18n.t('dashboard.create_task_modal.project_placeholder')"
|
||||
@change="changeProject"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="selectedProject == 0">
|
||||
<div class="flex gap-2 text-xs items-center">
|
||||
<div class="sci-checkbox-container">
|
||||
<input type="checkbox" class="sci-checkbox" v-model="publicProject" value="visible"/>
|
||||
<span class="sci-checkbox-label"></span>
|
||||
</div>
|
||||
<span v-html="i18n.t('projects.index.modal_new_project.visibility_html')"></span>
|
||||
</div>
|
||||
<div class="mt-4" :class="{'hidden': !publicProject}">
|
||||
<label class="sci-label">{{ i18n.t("dashboard.create_task_modal.user_role") }}</label>
|
||||
<SelectDropdown
|
||||
:options="userRoles"
|
||||
:value="defaultRole"
|
||||
@change="changeRole"
|
||||
:placeholder="i18n.t('dashboard.create_task_modal.user_role_placeholder')"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-4">
|
||||
<label class="sci-label">{{ i18n.t("dashboard.create_task_modal.experiment") }}</label>
|
||||
<SelectDropdown
|
||||
:optionsUrl="experimentsUrl"
|
||||
:urlParams="{ project: {
|
||||
id: selectedProject,
|
||||
name: newProjectName
|
||||
}}"
|
||||
:disabled="!(selectedProject != null && selectedProject >= 0)"
|
||||
:searchable="true"
|
||||
:value="selectedExperiment"
|
||||
:placeholder="i18n.t('dashboard.create_task_modal.experiment_placeholder')"
|
||||
:optionRenderer="newExperimentRenderer"
|
||||
@change="changeExperiment"
|
||||
/>
|
||||
</div>
|
||||
<hr class="my-6">
|
||||
<div class="flex items-center justify-end gap-4">
|
||||
<button class="btn btn-light" @click="closeModal">
|
||||
{{ i18n.t("dashboard.create_task_modal.cancel") }}
|
||||
</button>
|
||||
<button class="btn btn-primary" @click="createMyModule" :disabled="!validTaskName || !validExperiment || !validProject || creatingTask">
|
||||
{{ i18n.t("dashboard.create_task_modal.create") }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
/* global GLOBAL_CONSTANTS */
|
||||
|
||||
import SelectDropdown from '../shared/select_dropdown.vue';
|
||||
import axios from '../../packs/custom_axios.js';
|
||||
|
||||
export default {
|
||||
name: 'DashboardNewTask',
|
||||
components: {
|
||||
SelectDropdown
|
||||
},
|
||||
computed: {
|
||||
validTaskName() {
|
||||
return this.taskName.length >= this.minLength;
|
||||
},
|
||||
validExperiment() {
|
||||
return this.selectedExperiment != null || this.newExperimentName.length > this.minLength;
|
||||
},
|
||||
validProject() {
|
||||
return this.selectedProject != null || this.newProjectName.length > this.minLength;
|
||||
},
|
||||
minLength() {
|
||||
return GLOBAL_CONSTANTS.NAME_MIN_LENGTH;
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.fetchUserRoles();
|
||||
$('#create-task-modal').on('hidden.bs.modal', () => {
|
||||
this.$emit('close');
|
||||
});
|
||||
|
||||
$('#create-task-modal').on('shown.bs.modal', this.focusInput);
|
||||
},
|
||||
unmounted() {
|
||||
$('#create-task-modal').off('shown.bs.modal', this.focusInput);
|
||||
},
|
||||
props: {
|
||||
projectsUrl: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
experimentsUrl: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
rolesUrl: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
createUrl: {
|
||||
type: String,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
projects: [],
|
||||
selectedProject: null,
|
||||
newProjectName: '',
|
||||
experiments: [],
|
||||
selectedExperiment: null,
|
||||
newExperimentName: '',
|
||||
userRoles: [],
|
||||
taskName: '',
|
||||
publicProject: false,
|
||||
defaultRole: null,
|
||||
creatingTask: false
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
createMyModule() {
|
||||
if (this.creatingTask) return;
|
||||
|
||||
this.creatingTask = true;
|
||||
|
||||
axios.post(this.createUrl, {
|
||||
my_module: {
|
||||
name: this.taskName
|
||||
},
|
||||
experiment: {
|
||||
id: this.selectedExperiment,
|
||||
name: this.newExperimentName
|
||||
},
|
||||
project: {
|
||||
id: this.selectedProject,
|
||||
name: this.newProjectName,
|
||||
visibility: (this.publicProject ? 'visible' : 'hidden'),
|
||||
default_public_user_role_id: this.defaultRole
|
||||
}
|
||||
})
|
||||
.then((response) => {
|
||||
this.creatingTask = false;
|
||||
window.location.href = response.data.my_module_path;
|
||||
});
|
||||
},
|
||||
changeRole(value) {
|
||||
this.defaultRole = value;
|
||||
},
|
||||
changeProject(value, label) {
|
||||
this.selectedProject = value;
|
||||
this.newProjectName = label;
|
||||
},
|
||||
changeExperiment(value, label) {
|
||||
this.selectedExperiment = value;
|
||||
this.newExperimentName = label;
|
||||
},
|
||||
newProjectRenderer(option) {
|
||||
if (option[0] > 0) {
|
||||
return option[1];
|
||||
}
|
||||
return `
|
||||
<div class="flex items-center gap-2 truncate">
|
||||
<span class="sn-icon sn-icon-new-task"></span>
|
||||
<span class="truncate">${this.i18n.t('dashboard.create_task_modal.new_project', { name: option[1] })}</span
|
||||
</div>
|
||||
`;
|
||||
},
|
||||
newExperimentRenderer(option) {
|
||||
if (option[0] > 0) {
|
||||
return option[1];
|
||||
}
|
||||
return `
|
||||
<div class="flex items-center gap-2 truncate">
|
||||
<span class="sn-icon sn-icon-new-task"></span>
|
||||
<span class="truncate">${this.i18n.t('dashboard.create_task_modal.new_experiment', { name: option[1] })}</span
|
||||
</div>
|
||||
`;
|
||||
},
|
||||
closeModal() {
|
||||
$('#create-task-modal').modal('hide');
|
||||
this.taskName = '';
|
||||
this.selectedProject = null;
|
||||
this.newProjectName = '';
|
||||
this.selectedExperiment = null;
|
||||
this.newExperimentName = '';
|
||||
},
|
||||
fetchUserRoles() {
|
||||
axios.get(this.rolesUrl)
|
||||
.then((response) => {
|
||||
this.userRoles = response.data.data;
|
||||
});
|
||||
},
|
||||
focusInput() {
|
||||
this.$refs.taskName.focus();
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
|
@ -64,6 +64,7 @@ export default {
|
|||
},
|
||||
mounted() {
|
||||
SmartAnnotation.init($(this.$refs.description), false);
|
||||
$(this.$refs.modal).on('hidden.bs.modal', this.handleAtWhoModalClose);
|
||||
},
|
||||
mixins: [modalMixin],
|
||||
methods: {
|
||||
|
@ -80,6 +81,9 @@ export default {
|
|||
HelperModule.flashAlertMsg(error.response.data.message, 'danger');
|
||||
});
|
||||
},
|
||||
handleAtWhoModalClose() {
|
||||
$('.atwho-view.old').css('display', 'none');
|
||||
}
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
|
|
@ -174,7 +174,7 @@ export default {
|
|||
columns.push({
|
||||
field: 'comments',
|
||||
headerName: this.i18n.t('experiments.table.column.comments_html'),
|
||||
sortable: false,
|
||||
sortable: true,
|
||||
cellRenderer: CommentsRenderer,
|
||||
notSelectable: true
|
||||
});
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
<div class="sci-input-container-v2" :class="{'error': error}" :data-error="error">
|
||||
<input type="text" v-model="name"
|
||||
class="sci-input-field" autofocus="true" ref="input"
|
||||
:placeholder="i18n.t('projects.index.modal_new_project.name_placeholder')" />
|
||||
:placeholder="i18n.t('projects.index.modal_new_project_folder.name_placeholder')" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,46 +1,46 @@
|
|||
<template>
|
||||
<div v-if="protocol.id" class="task-protocol">
|
||||
<div ref="header" class="task-section-header ml-[-1rem] w-[calc(100%_+_2rem)] px-4 bg-sn-white sticky top-0 transition" v-if="!inRepository">
|
||||
<div class="flex items-center grow">
|
||||
<div class="portocol-header-left-part grow">
|
||||
<template v-if="headerSticked && moduleName">
|
||||
<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>
|
||||
<div @click="scrollTop" class="task-section-title w-[calc(100%_-_35rem)] min-w-[5rem] cursor-pointer">
|
||||
<h2 class="truncate leading-6">{{ moduleName }}</h2>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<a class="task-section-caret" tabindex="0" role="button" data-toggle="collapse" href="#protocol-content" aria-expanded="true" aria-controls="protocol-content">
|
||||
<i class="sn-icon sn-icon-right"></i>
|
||||
<div class="task-section-title truncate">
|
||||
<h2>{{ i18n.t('Protocol') }}</h2>
|
||||
</div>
|
||||
</a>
|
||||
</template>
|
||||
<div :class="{'hidden': headerSticked}">
|
||||
<div class="my-module-protocol-status">
|
||||
<!-- protocol status dropdown gets mounted here -->
|
||||
<div class="portocol-header-left-part grow" :class="{'overflow-hidden': headerSticked && moduleName}">
|
||||
<template v-if="headerSticked && moduleName">
|
||||
<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>
|
||||
<div @click="scrollTop" class="task-section-title min-w-[5rem] cursor-pointer" :title="moduleName">
|
||||
<h2 class="truncate leading-6">{{ moduleName }}</h2>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<a class="task-section-caret" tabindex="0" role="button" data-toggle="collapse" href="#protocol-content" aria-expanded="true" aria-controls="protocol-content">
|
||||
<i class="sn-icon sn-icon-right"></i>
|
||||
<div class="task-section-title truncate">
|
||||
<h2>{{ i18n.t('Protocol') }}</h2>
|
||||
</div>
|
||||
</a>
|
||||
</template>
|
||||
<div :class="{'hidden': headerSticked}">
|
||||
<div class="my-module-protocol-status">
|
||||
<!-- protocol status dropdown gets mounted here -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="actions-block">
|
||||
<div class="protocol-buttons-group shrink-0">
|
||||
<div class="protocol-buttons-group shrink-0 bg-sn-white">
|
||||
<a v-if="urls.add_step_url"
|
||||
class="btn btn-secondary"
|
||||
class="btn btn-secondary icon-btn xl:!px-4"
|
||||
:title="i18n.t('protocols.steps.new_step_title')"
|
||||
@keyup.enter="addStep(steps.length)"
|
||||
@click="addStep(steps.length)"
|
||||
tabindex="0">
|
||||
<span class="sn-icon sn-icon-new-task" aria-hidden="true"></span>
|
||||
<span>{{ i18n.t("protocols.steps.new_step") }}</span>
|
||||
<span class="tw-hidden xl:inline">{{ i18n.t("protocols.steps.new_step") }}</span>
|
||||
</a>
|
||||
<template v-if="steps.length > 0">
|
||||
<button class="btn btn-secondary" @click="collapseSteps" tabindex="0">
|
||||
{{ i18n.t("protocols.steps.collapse_label") }}
|
||||
<button :title="i18n.t('protocols.steps.collapse_label')" v-if="!stepCollapsed" class="btn btn-secondary icon-btn xl:!px-4" @click="collapseSteps" tabindex="0">
|
||||
<i class="sn-icon sn-icon-collapse-all"></i>
|
||||
<span class="tw-hidden xl:inline">{{ i18n.t("protocols.steps.collapse_label") }}</span>
|
||||
</button>
|
||||
<button class="btn btn-secondary" @click="expandSteps" tabindex="0">
|
||||
{{ i18n.t("protocols.steps.expand_label") }}
|
||||
<button v-else :title="i18n.t('protocols.steps.expand_label')" class="btn btn-secondary icon-btn xl:!px-4" @click="expandSteps" tabindex="0">
|
||||
<i class="sn-icon sn-icon-expand-all"></i>
|
||||
<span class="tw-hidden xl:inline">{{ i18n.t("protocols.steps.expand_label") }}</span>
|
||||
</button>
|
||||
</template>
|
||||
<ProtocolOptions
|
||||
|
@ -154,9 +154,11 @@
|
|||
</a>
|
||||
<div v-if="steps.length > 0" class="flex justify-between items-center gap-4">
|
||||
<button @click="collapseSteps" class="btn btn-secondary flex px-4" tabindex="0" data-e2e="e2e-BT-protocol-templateSteps-collapse">
|
||||
<i class="sn-icon sn-icon-collapse-all"></i>
|
||||
{{ i18n.t("protocols.steps.collapse_label") }}
|
||||
</button>
|
||||
<button @click="expandSteps" class="btn btn-secondary flex px-4" tabindex="0" data-e2e="e2e-BT-protocol-templateSteps-expand">
|
||||
<i class="sn-icon sn-icon-expand-all"></i>
|
||||
{{ i18n.t("protocols.steps.expand_label") }}
|
||||
</button>
|
||||
<a v-if="steps.length > 0 && urls.reorder_steps_url"
|
||||
|
@ -193,7 +195,9 @@
|
|||
@step:attachemnts:loaded="stepToReload = null"
|
||||
@step:move_attachment="reloadStep"
|
||||
@step:drag_enter="dragEnter"
|
||||
@step:collapsed="checkStepsState"
|
||||
:reorderStepUrl="steps.length > 1 ? urls.reorder_steps_url : null"
|
||||
:userSettingsUrl="userSettingsUrl"
|
||||
:assignableMyModuleId="protocol.attributes.assignable_my_module_id"
|
||||
/>
|
||||
<div v-if="(index === steps.length - 1) && urls.add_step_url" class="insert-step" @click="addStep(index + 1)" data-e2e="e2e-BT-protocol-templateSteps-insertStep">
|
||||
|
@ -250,7 +254,7 @@ import ReorderableItemsModal from '../shared/reorderable_items_modal.vue';
|
|||
import PublishProtocol from './modals/publish_protocol.vue';
|
||||
import clipboardPasteModal from '../shared/content/attachments/clipboard_paste_modal.vue';
|
||||
import AssetPasteMixin from '../shared/content/attachments/mixins/paste.js';
|
||||
|
||||
import axios from '../../packs/custom_axios';
|
||||
import UtilsMixin from '../mixins/utils.js';
|
||||
import stackableHeadersMixin from '../mixins/stackableHeadersMixin';
|
||||
import moduleNameObserver from '../mixins/moduleNameObserver';
|
||||
|
@ -290,10 +294,13 @@ export default {
|
|||
reordering: false,
|
||||
publishing: false,
|
||||
stepToReload: null,
|
||||
activeDragStep: null
|
||||
activeDragStep: null,
|
||||
userSettingsUrl: null,
|
||||
stepCollapsed: false
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
this.userSettingsUrl = document.querySelector('meta[name="user-settings-url"]').getAttribute('content');
|
||||
$.get(this.protocolUrl, (result) => {
|
||||
this.protocol = result.data;
|
||||
this.$nextTick(() => {
|
||||
|
@ -320,11 +327,41 @@ export default {
|
|||
reloadStep(step) {
|
||||
this.stepToReload = step;
|
||||
},
|
||||
checkStepsState() {
|
||||
this.stepCollapsed = this.$refs.steps.every((step) => step.isCollapsed);
|
||||
},
|
||||
collapseSteps() {
|
||||
$('.step-container .collapse').collapse('hide');
|
||||
this.updateStepStateSettings(true);
|
||||
this.$refs.steps.forEach((step) => step.isCollapsed = true);
|
||||
this.stepCollapsed = true;
|
||||
},
|
||||
expandSteps() {
|
||||
$('.step-container .collapse').collapse('show');
|
||||
this.updateStepStateSettings(false);
|
||||
this.$refs.steps.forEach((step) => step.isCollapsed = false);
|
||||
this.stepCollapsed = false;
|
||||
},
|
||||
updateStepStateSettings(newState) {
|
||||
const updatedData = this.steps.reduce((acc, currentStep) => {
|
||||
acc[currentStep.id] = newState;
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
this.steps = this.steps.map((step) => ({
|
||||
...step,
|
||||
attributes: {
|
||||
...step.attributes,
|
||||
collapsed: newState
|
||||
}
|
||||
}));
|
||||
|
||||
const settings = {
|
||||
key: 'task_step_states',
|
||||
data: updatedData
|
||||
};
|
||||
|
||||
axios.put(this.userSettingsUrl, { settings: [settings] });
|
||||
},
|
||||
deleteSteps() {
|
||||
$.post(this.urls.delete_steps_url, () => {
|
||||
|
|
|
@ -15,7 +15,7 @@
|
|||
<div class="step-header">
|
||||
<div class="step-element-header" :class="{ 'no-hover': !urls.update_url }">
|
||||
<div class="flex items-center gap-4 py-0.5 border-0 border-y border-transparent border-solid">
|
||||
<a class="step-collapse-link hover:no-underline focus:no-underline"
|
||||
<a ref="toggleElement" class="step-collapse-link hover:no-underline focus:no-underline"
|
||||
:href="'#stepBody' + step.id"
|
||||
data-toggle="collapse"
|
||||
data-remote="true"
|
||||
|
@ -207,6 +207,9 @@
|
|||
reorderStepUrl: {
|
||||
required: false
|
||||
},
|
||||
userSettingsUrl: {
|
||||
required: false
|
||||
},
|
||||
assignableMyModuleId: {
|
||||
type: Number,
|
||||
required: false
|
||||
|
@ -293,9 +296,27 @@
|
|||
if (this.activeDragStep != this.step.id && this.dragingFile) {
|
||||
this.dragingFile = false;
|
||||
}
|
||||
},
|
||||
step: {
|
||||
handler(newVal) {
|
||||
if (this.isCollapsed !== newVal.attributes.collapsed) {
|
||||
this.toggleCollapsed();
|
||||
}
|
||||
},
|
||||
deep: true
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.$nextTick(() => {
|
||||
const stepId = `#stepBody${this.step.id}`;
|
||||
this.isCollapsed = this.step.attributes.collapsed;
|
||||
if (this.isCollapsed) {
|
||||
$(stepId).collapse('hide');
|
||||
} else {
|
||||
$(stepId).collapse('show');
|
||||
}
|
||||
this.$emit('step:collapsed');
|
||||
});
|
||||
$(this.$refs.comments).data('closeCallback', this.closeCommentsSidebar);
|
||||
$(this.$refs.comments).data('openCallback', this.closeCommentsSidebar);
|
||||
$(this.$refs.actionsDropdownButton).on('shown.bs.dropdown hidden.bs.dropdown', () => {
|
||||
|
@ -466,6 +487,16 @@
|
|||
},
|
||||
toggleCollapsed() {
|
||||
this.isCollapsed = !this.isCollapsed;
|
||||
|
||||
this.step.attributes.collapsed = this.isCollapsed;
|
||||
|
||||
const settings = {
|
||||
key: 'task_step_states',
|
||||
data: { [this.step.id]: this.isCollapsed }
|
||||
};
|
||||
|
||||
this.$emit('step:collapsed');
|
||||
axios.put(this.userSettingsUrl, { settings: [settings] });
|
||||
},
|
||||
showDeleteModal() {
|
||||
this.confirmingDelete = true;
|
||||
|
@ -588,6 +619,10 @@
|
|||
$.post(this.urls[`create_${elementType}_url`], { tableDimensions: tableDimensions, plateTemplate: plateTemplate }, (result) => {
|
||||
result.data.isNew = true;
|
||||
this.elements.push(result.data)
|
||||
|
||||
if (this.isCollapsed) {
|
||||
this.$refs.toggleElement.click();
|
||||
}
|
||||
this.$emit('stepUpdated')
|
||||
}).fail(() => {
|
||||
HelperModule.flashAlertMsg(this.i18n.t('errors.general'), 'danger');
|
||||
|
|
70
app/javascript/vue/repositories/modals/export.vue
Normal file
70
app/javascript/vue/repositories/modals/export.vue
Normal file
|
@ -0,0 +1,70 @@
|
|||
<template>
|
||||
<div ref="modal" @keydown.esc="cancel" class="modal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog" role="document">
|
||||
<form @submit.prevent="submit" class="modal-content">
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><i class="sn-icon sn-icon-close"></i></button>
|
||||
<h4 class="modal-title">
|
||||
{{ this.i18n.t('repositories.index.modal_export.title') }}
|
||||
</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p class="description-p1 mb-6" v-html="i18n.t('repositories.index.modal_export.description_p1_html', {
|
||||
team_name: rows[0].team,
|
||||
count: rows.length})"></p>
|
||||
<p class="bg-sn-super-light-blue p-3 mb-6"> {{ this.i18n.t('repositories.index.modal_export.description_alert') }} </p>
|
||||
<p class="mb-6"> {{ this.i18n.t('repositories.index.modal_export.description_p2') }} </p>
|
||||
<div class="sci-radio-container mt-3">
|
||||
<input type="radio" class="sci-radio" name="file_type" value="xlsx" v-model="selectedOption">
|
||||
<span class="sci-radio-label"></span>
|
||||
</div>
|
||||
<label class="font-normal ml-3 mb-0">.xlsx</label>
|
||||
<div class="sci-radio-container ml-[30px]">
|
||||
<input type="radio" class="sci-radio" name="file_type" value="csv" v-model="selectedOption">
|
||||
<span class="sci-radio-label"></span>
|
||||
</div>
|
||||
<label class="font-normal ml-3 mb-0">.csv</label>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-dismiss="modal">{{ i18n.t('general.cancel') }}</button>
|
||||
<button type="submit" class="btn btn-primary"> {{ i18n.t('repositories.index.modal_export.export') }} </button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
/* global HelperModule */
|
||||
|
||||
import axios from '../../../packs/custom_axios.js';
|
||||
import modalMixin from '../../shared/modal_mixin';
|
||||
|
||||
export default {
|
||||
name: 'ExportRepositoryModal',
|
||||
props: {
|
||||
rows: Object,
|
||||
exportAction: Object
|
||||
},
|
||||
mixins: [modalMixin],
|
||||
data() {
|
||||
return {
|
||||
selectedOption: this.exportAction.export_file_type
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
submit() {
|
||||
const payload = {
|
||||
repository_ids: this.rows.map((row) => row.id),
|
||||
file_type: this.selectedOption
|
||||
};
|
||||
|
||||
axios.post(this.exportAction.path, payload).then((response) => {
|
||||
this.$emit('export');
|
||||
HelperModule.flashAlertMsg(response.data.message, 'success');
|
||||
}).catch((error) => {
|
||||
HelperModule.flashAlertMsg(error.response.data.error, 'danger');
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
135
app/javascript/vue/repositories/modals/import/container.vue
Normal file
135
app/javascript/vue/repositories/modals/import/container.vue
Normal file
|
@ -0,0 +1,135 @@
|
|||
<template>
|
||||
<div v-if="modalOpened" class="relative">
|
||||
<component
|
||||
v-if="activeStep !== 'ExportModal'"
|
||||
:is="activeStep"
|
||||
:params="params"
|
||||
:key="modalId"
|
||||
:loading="loading"
|
||||
@uploadFile="uploadFile"
|
||||
@generatePreview="generatePreview"
|
||||
@changeStep="changeStep"
|
||||
@importRows="importRecords"
|
||||
@updateAutoMapping="updateAutoMapping"
|
||||
/>
|
||||
<ExportModal
|
||||
v-else
|
||||
:rows="[{id: params.id, team: params.attributes.team_name}]"
|
||||
:exportAction="params.attributes.export_actions"
|
||||
@close="activeStep ='UploadStep'"
|
||||
@export="activeStep = 'UploadStep'"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
/* global HelperModule */
|
||||
|
||||
import axios from '../../../../packs/custom_axios';
|
||||
import InfoModal from '../../../shared/info_modal.vue';
|
||||
import UploadStep from './upload_step.vue';
|
||||
import MappingStep from './mapping_step.vue';
|
||||
import PreviewStep from './preview_step.vue';
|
||||
import ExportModal from '../export.vue';
|
||||
|
||||
export default {
|
||||
name: 'ImportRepositoryModal',
|
||||
components: {
|
||||
InfoModal,
|
||||
UploadStep,
|
||||
MappingStep,
|
||||
PreviewStep,
|
||||
ExportModal
|
||||
},
|
||||
props: {
|
||||
repositoryUrl: String,
|
||||
required: true
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
modalOpened: false,
|
||||
activeStep: 'UploadStep',
|
||||
params: { autoMapping: true },
|
||||
modalId: null,
|
||||
loading: false
|
||||
};
|
||||
},
|
||||
created() {
|
||||
window.importRepositoryModalComponent = this;
|
||||
},
|
||||
methods: {
|
||||
open() {
|
||||
this.activeStep = 'UploadStep';
|
||||
this.params.selectedItems = null;
|
||||
this.params.autoMapping = true;
|
||||
this.fetchRepository();
|
||||
},
|
||||
fetchRepository() {
|
||||
axios.get(this.repositoryUrl)
|
||||
.then((response) => {
|
||||
this.params = { ...this.params, ...response.data.data };
|
||||
this.modalId = Math.random().toString(36);
|
||||
this.modalOpened = true;
|
||||
});
|
||||
},
|
||||
uploadFile(params) {
|
||||
this.params = { ...this.params, ...params };
|
||||
this.activeStep = 'MappingStep';
|
||||
},
|
||||
updateAutoMapping(value) {
|
||||
this.params.autoMapping = value;
|
||||
},
|
||||
generatePreview(selectedItems, updateWithEmptyCells, onlyAddNewItems) {
|
||||
this.params.selectedItems = selectedItems;
|
||||
this.params.updateWithEmptyCells = updateWithEmptyCells;
|
||||
this.params.onlyAddNewItems = onlyAddNewItems;
|
||||
this.importRecords(true);
|
||||
},
|
||||
changeStep(step) {
|
||||
this.activeStep = step;
|
||||
},
|
||||
generateMapping() {
|
||||
return this.params.selectedItems.reduce((obj, item) => {
|
||||
obj[item.index] = item.key || '';
|
||||
return obj;
|
||||
}, {});
|
||||
},
|
||||
importRecords(preview) {
|
||||
if (this.loading) {
|
||||
return;
|
||||
}
|
||||
|
||||
const jsonData = {
|
||||
file_id: this.params.temp_file.id,
|
||||
mappings: this.generateMapping(),
|
||||
id: this.params.id,
|
||||
preview: preview,
|
||||
should_overwrite_with_empty_cells: this.params.updateWithEmptyCells,
|
||||
can_edit_existing_items: !this.params.onlyAddNewItems
|
||||
};
|
||||
|
||||
this.loading = true;
|
||||
|
||||
axios.post(this.params.attributes.urls.import_records, jsonData)
|
||||
.then((response) => {
|
||||
if (preview) {
|
||||
this.params.preview = response.data.changes;
|
||||
this.params.import_date = response.data.import_date;
|
||||
this.activeStep = 'PreviewStep';
|
||||
} else {
|
||||
HelperModule.flashAlertMsg(response.data.message, 'success');
|
||||
this.modalOpened = false;
|
||||
this.activeStep = null;
|
||||
$('.dataTable.repository-dataTable').DataTable().ajax.reload(null, false);
|
||||
}
|
||||
|
||||
this.loading = false;
|
||||
})
|
||||
.catch((error) => {
|
||||
HelperModule.flashAlertMsg(error.response.data.message, 'danger');
|
||||
this.loading = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
220
app/javascript/vue/repositories/modals/import/mapping_step.vue
Normal file
220
app/javascript/vue/repositories/modals/import/mapping_step.vue
Normal file
|
@ -0,0 +1,220 @@
|
|||
<template>
|
||||
<div ref="modal" class="modal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog modal-lg" role="document">
|
||||
<Loading v-if="loading" />
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close" data-e2e="e2e-BT-newInventoryModal-close">
|
||||
<i class="sn-icon sn-icon-close"></i>
|
||||
</button>
|
||||
<h4 class="modal-title truncate" id="edit-project-modal-label" data-e2e="e2e-TX-newInventoryModal-title">
|
||||
{{ i18n.t('repositories.import_records.steps.step2.title') }}
|
||||
</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
|
||||
<p class="text-sn-dark-grey">
|
||||
{{ this.i18n.t('repositories.import_records.steps.step2.subtitle') }}
|
||||
</p>
|
||||
<div class="flex gap-6 items-center my-6">
|
||||
<div class="flex items-center gap-2" :title="i18n.t('repositories.import_records.steps.step2.autoMappingTooltip')">
|
||||
<div class="sci-checkbox-container">
|
||||
<input type="checkbox" class="sci-checkbox" @change="$emit('update-auto-mapping', $event.target.checked)" :checked="params.autoMapping" />
|
||||
<span class="sci-checkbox-label"></span>
|
||||
</div>
|
||||
{{ i18n.t('repositories.import_records.steps.step2.autoMappingText') }}
|
||||
</div>
|
||||
<!--
|
||||
<div class="flex items-center gap-1">
|
||||
<div class="sci-checkbox-container my-auto">
|
||||
<input type="checkbox" class="sci-checkbox" :checked="updateWithEmptyCells" @change="toggleUpdateWithEmptyCells"/>
|
||||
<span class="sci-checkbox-label"></span>
|
||||
</div>
|
||||
{{ i18n.t('repositories.import_records.steps.step2.updateEmptyCellsText') }}
|
||||
</div>
|
||||
<div class="flex items-center gap-1">
|
||||
<div class="sci-checkbox-container my-auto">
|
||||
<input type="checkbox" class="sci-checkbox" :checked="onlyAddNewItems" @change="toggleOnlyAddNewItems" />
|
||||
<span class="sci-checkbox-label"></span>
|
||||
</div>
|
||||
{{ i18n.t('repositories.import_records.steps.step2.onlyAddNewItemsText') }}
|
||||
</div>
|
||||
-->
|
||||
</div>
|
||||
|
||||
{{ i18n.t('repositories.import_records.steps.step2.importedFileText') }} {{ params.file_name }}
|
||||
<hr class="m-0 mt-6">
|
||||
<div class="grid grid-cols-[3rem_14.5rem_1.5rem_14.5rem_5rem_14.5rem] px-2">
|
||||
|
||||
<div v-for="(column, key) in columnLabels" class="flex items-center px-2 py-2 font-bold">{{ column }}</div>
|
||||
|
||||
<template v-for="(item, index) in params.import_data.header" :key="item">
|
||||
<MappingStepTableRow
|
||||
:index="index"
|
||||
:item="item"
|
||||
:dropdownOptions="computedDropdownOptions"
|
||||
:params="params"
|
||||
:value="this.selectedItems.find((item) => item.index === index)"
|
||||
@selection:changed="handleChange"
|
||||
:autoMapping="params.autoMapping"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<!-- imported/ignored section -->
|
||||
<div class="flex gap-1 mt-6"
|
||||
v-html="i18n.t('repositories.import_records.steps.step2.importedIgnoredSection', {
|
||||
imported: computedImportedIgnoredInfo.importedSum,
|
||||
ignored: computedImportedIgnoredInfo.ignoredSum
|
||||
})"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- footer -->
|
||||
<div class="modal-footer">
|
||||
<div id="error" class="flex flex-row gap-3 text-sn-delete-red">
|
||||
<i v-if="error" class="sn-icon sn-icon-alert-warning my-auto"></i>
|
||||
<div class="my-auto">{{ error ? error : '' }}</div>
|
||||
</div>
|
||||
<button class="btn btn-secondary ml-auto" @click="close" aria-label="Close">
|
||||
{{ i18n.t('repositories.import_records.steps.step2.cancelBtnText') }}
|
||||
</button>
|
||||
<button class="btn btn-primary" :disabled="!canSubmit" @click="importRecords">
|
||||
{{ i18n.t('repositories.import_records.steps.step2.confirmBtnText') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import axios from '../../../../packs/custom_axios';
|
||||
import SelectDropdown from '../../../shared/select_dropdown.vue';
|
||||
import MappingStepTableRow from './mapping_step_table_row.vue';
|
||||
import modalMixin from '../../../shared/modal_mixin';
|
||||
import Loading from '../../../shared/loading.vue';
|
||||
|
||||
export default {
|
||||
name: 'MappingStep',
|
||||
emits: ['close', 'generatePreview', 'updateAutoMapping'],
|
||||
mixins: [modalMixin],
|
||||
components: {
|
||||
SelectDropdown,
|
||||
MappingStepTableRow,
|
||||
Loading
|
||||
},
|
||||
props: {
|
||||
params: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
loading: {
|
||||
type: Boolean,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
updateWithEmptyCells: false,
|
||||
onlyAddNewItems: false,
|
||||
columnLabels: {
|
||||
0: this.i18n.t('repositories.import_records.steps.step2.table.columnLabels.number'),
|
||||
1: this.i18n.t('repositories.import_records.steps.step2.table.columnLabels.importedColumns'),
|
||||
2: '',
|
||||
3: this.i18n.t('repositories.import_records.steps.step2.table.columnLabels.scinoteColumns'),
|
||||
4: this.i18n.t('repositories.import_records.steps.step2.table.columnLabels.status'),
|
||||
5: this.i18n.t('repositories.import_records.steps.step2.table.columnLabels.exampleData')
|
||||
},
|
||||
selectedItems: [],
|
||||
importRecordsUrl: null,
|
||||
teamId: null,
|
||||
repositoryId: null,
|
||||
availableFields: [],
|
||||
alwaysAvailableFields: [],
|
||||
repositoryColumns: null,
|
||||
error: null
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
handleChange(payload) {
|
||||
this.error = null;
|
||||
const { index, key, value } = payload;
|
||||
|
||||
const item = this.selectedItems.find((i) => i.index === index);
|
||||
const usedBeforeItem = this.selectedItems.find((i) => i.key === key && i.index !== index);
|
||||
|
||||
if (usedBeforeItem && usedBeforeItem.key !== 'do_not_import') {
|
||||
usedBeforeItem.key = null;
|
||||
usedBeforeItem.value = null;
|
||||
}
|
||||
|
||||
item.key = key;
|
||||
item.value = value;
|
||||
},
|
||||
loadAvailableFields() {
|
||||
// Adding alreadySelected attribute for tracking
|
||||
this.availableFields = [];
|
||||
|
||||
Object.entries(this.params.import_data.available_fields).forEach(([key, value]) => {
|
||||
let columnTypeName = '';
|
||||
if (key === '-1') {
|
||||
columnTypeName = this.i18n.t('repositories.import_records.steps.step2.computedDropdownOptions.name');
|
||||
} else if (key === '0') {
|
||||
columnTypeName = this.i18n.t('repositories.import_records.steps.step2.computedDropdownOptions.id');
|
||||
} else {
|
||||
const column = this.repositoryColumns.find((el) => el[0] === parseInt(key, 10));
|
||||
columnTypeName = this.i18n.t(`repositories.import_records.steps.step2.computedDropdownOptions.${column[2]}`);
|
||||
}
|
||||
const field = {
|
||||
key, value, alreadySelected: false, typeName: columnTypeName
|
||||
};
|
||||
this.availableFields.push(field);
|
||||
});
|
||||
},
|
||||
loadSelectedItems() {
|
||||
if (this.params.selectedItems) {
|
||||
this.selectedItems = this.params.selectedItems;
|
||||
} else {
|
||||
this.selectedItems = this.params.import_data.header.map((item, index) => ({ index, key: null, value: null }));
|
||||
}
|
||||
},
|
||||
importRecords() {
|
||||
if (!this.selectedItems.find((item) => item.key === '-1')) {
|
||||
this.error = this.i18n.t('repositories.import_records.steps.step2.selectNamePropertyError');
|
||||
return '';
|
||||
}
|
||||
|
||||
this.$emit(
|
||||
'generatePreview',
|
||||
this.selectedItems
|
||||
);
|
||||
return true;
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
computedDropdownOptions() {
|
||||
let options = this.availableFields.map((el) => [String(el.key), `${String(el.value)} (${el.typeName})`]);
|
||||
options = [
|
||||
['do_not_import', this.i18n.t('repositories.import_records.steps.step2.table.tableRow.placeholders.doNotImport')]
|
||||
].concat(options);
|
||||
// options = [['new', this.i18n.t('repositories.import_records.steps.step2.table.tableRow.importAsNewColumn')]].concat(options);
|
||||
return options;
|
||||
},
|
||||
computedImportedIgnoredInfo() {
|
||||
const importedSum = this.selectedItems.filter((i) => i.key && i.key !== 'do_not_import').length;
|
||||
const ignoredSum = this.selectedItems.length - importedSum;
|
||||
return { importedSum, ignoredSum };
|
||||
},
|
||||
canSubmit() {
|
||||
return this.selectedItems.filter((i) => i.key && i.key !== 'do_not_import').length > 0;
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.repositoryColumns = this.params.attributes.repository_columns;
|
||||
this.loadAvailableFields();
|
||||
this.loadSelectedItems();
|
||||
}
|
||||
};
|
||||
</script>
|
|
@ -0,0 +1,178 @@
|
|||
<template>
|
||||
<!-- number col -->
|
||||
<div class="py-1 min-h-12 px-2 flex items-center" :class="{
|
||||
'bg-sn-super-light-blue': selected
|
||||
}">{{ index + 1 }}</div>
|
||||
|
||||
<div class="py-1 truncate min-h-12 px-2 flex items-center" :title="item" :class="{
|
||||
'bg-sn-super-light-blue': selected
|
||||
}">{{ item }}</div>
|
||||
|
||||
<div class="py-1 min-h-12 flex items-center justify-center text-sn-grey" :class="{
|
||||
'bg-sn-super-light-blue': selected
|
||||
}">
|
||||
<i class="sn-icon sn-icon-arrow-right text-sn-gray"></i>
|
||||
</div>
|
||||
|
||||
<div class="py-1 min-h-12 flex items-center flex-col gap-2 px-2" :class="{
|
||||
'bg-sn-super-light-blue': selected
|
||||
}">
|
||||
<SelectDropdown
|
||||
:options="dropdownOptions"
|
||||
@change="changeSelected"
|
||||
:size="'sm'"
|
||||
class="max-w-96"
|
||||
:searchable="true"
|
||||
:class="{
|
||||
'outline-sn-alert-brittlebush outline-1 outline rounded': computeMatchNotFound
|
||||
}"
|
||||
:placeholder="i18n.t('repositories.import_records.steps.step2.table.tableRow.placeholders.matchNotFound')"
|
||||
:title="this.selectedColumnType?.value"
|
||||
:value="this.selectedColumnType?.key"
|
||||
></SelectDropdown>
|
||||
<template v-if="false">
|
||||
<SelectDropdown
|
||||
:options="newColumnTypes"
|
||||
@change="(v) => { newColumn.type = v }"
|
||||
:value="newColumn.type"
|
||||
size="sm"
|
||||
placeholder="Select column type"
|
||||
></SelectDropdown>
|
||||
<div class="sci-input-container-v2 w-full">
|
||||
<input type="text" v-model="newColumn.name" class="sci-input" placeholder="Name">
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<div class="py-1 min-h-12 px-2 flex items-center" :class="{
|
||||
'bg-sn-super-light-blue': selected
|
||||
}">
|
||||
<i v-if="differentMapingName" :title="i18n.t('repositories.import_records.steps.step2.table.tableRow.importedColumnTitle')"
|
||||
class="sn-icon sn-icon-info text-sn-science-blue"></i>
|
||||
<i v-else-if="columnMapped" :title="i18n.t('repositories.import_records.steps.step2.table.tableRow.importedColumnTitle')" class="sn-icon sn-icon-check"></i>
|
||||
<i v-else-if="matchNotFound && !isSystemColumn(item)" :title="i18n.t('repositories.import_records.steps.step2.table.tableRow.matchNotFoundColumnTitle')"
|
||||
class="sn-icon sn-icon-close text-sn-alert-brittlebush"></i>
|
||||
<i v-else :title="i18n.t('repositories.import_records.steps.step2.table.tableRow.doNotImportColumnTitle')" class="sn-icon sn-icon-close text-sn-sleepy-grey"></i>
|
||||
</div>
|
||||
|
||||
<div class="py-1 min-h-12 px-2 flex items-center" :title="params.import_data.columns[index]" :class="{
|
||||
'bg-sn-super-light-blue': selected
|
||||
}">{{ params.import_data.columns[index] }}</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import SelectDropdown from '../../../shared/select_dropdown.vue';
|
||||
|
||||
export default {
|
||||
name: 'SecondStepTableRow',
|
||||
emits: ['selection:changed'],
|
||||
components: {
|
||||
SelectDropdown
|
||||
},
|
||||
props: {
|
||||
index: {
|
||||
type: Number,
|
||||
required: true
|
||||
},
|
||||
dropdownOptions: {
|
||||
type: Array,
|
||||
required: true
|
||||
},
|
||||
item: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
params: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
autoMapping: {
|
||||
type: Boolean,
|
||||
required: true
|
||||
},
|
||||
value: Object
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
selectedColumnType: null,
|
||||
newColumn: {
|
||||
type: 'Text',
|
||||
name: ''
|
||||
},
|
||||
systemColumns: [
|
||||
this.i18n.t('repositories.import_records.steps.step2.systemColumns.added_by'),
|
||||
this.i18n.t('repositories.import_records.steps.step2.systemColumns.created_on'),
|
||||
this.i18n.t('repositories.import_records.steps.step2.systemColumns.updated_by'),
|
||||
this.i18n.t('repositories.import_records.steps.step2.systemColumns.updated_on'),
|
||||
this.i18n.t('repositories.import_records.steps.step2.systemColumns.archived_by'),
|
||||
this.i18n.t('repositories.import_records.steps.step2.systemColumns.archived_on'),
|
||||
this.i18n.t('repositories.import_records.steps.step2.systemColumns.parents'),
|
||||
this.i18n.t('repositories.import_records.steps.step2.systemColumns.children')
|
||||
],
|
||||
newColumnTypes: [
|
||||
['Text', this.i18n.t('repositories.import_records.steps.step2.table.tableRow.newColumnType.text')],
|
||||
['List', this.i18n.t('repositories.import_records.steps.step2.table.tableRow.newColumnType.list')]
|
||||
]
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
selected() {
|
||||
if (this.value?.key === null) {
|
||||
this.selectedColumnType = null;
|
||||
}
|
||||
},
|
||||
autoMapping(newVal) {
|
||||
if (newVal === true) {
|
||||
this.autoMap();
|
||||
} else {
|
||||
this.clearAutoMap();
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
computeMatchNotFound() {
|
||||
return this.autoMapping && !this.isSystemColumn(this.item) && ((this.selectedColumnType && !this.selectedColumnType.key) || !this.selectedColumnType);
|
||||
},
|
||||
selected() {
|
||||
return this.columnMapped;
|
||||
},
|
||||
differentMapingName() {
|
||||
return this.columnMapped && this.selectedColumnType?.value !== this.item && this.value?.key !== 'do_not_import';
|
||||
},
|
||||
matchNotFound() {
|
||||
return this.autoMapping && this.columnMapped;
|
||||
},
|
||||
columnMapped() {
|
||||
return this.selectedColumnType?.key && this.selectedColumnType?.key !== 'do_not_import';
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
isSystemColumn(column) {
|
||||
return this.systemColumns.includes(column);
|
||||
},
|
||||
autoMap() {
|
||||
this.changeSelected(null);
|
||||
Object.entries(this.params.import_data.available_fields).forEach(([key, value]) => {
|
||||
if (this.item === value) {
|
||||
this.changeSelected(key);
|
||||
}
|
||||
});
|
||||
},
|
||||
clearAutoMap() {
|
||||
this.changeSelected('do_not_import');
|
||||
},
|
||||
changeSelected(e) {
|
||||
const value = this.params.import_data.available_fields[e];
|
||||
this.selectedColumnType = { index: this.index, key: e, value };
|
||||
this.$emit('selection:changed', this.selectedColumnType);
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
if (this.autoMapping) {
|
||||
this.autoMap();
|
||||
} else {
|
||||
this.selectedColumnType = this.value;
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
215
app/javascript/vue/repositories/modals/import/preview_step.vue
Normal file
215
app/javascript/vue/repositories/modals/import/preview_step.vue
Normal file
|
@ -0,0 +1,215 @@
|
|||
<template>
|
||||
<div ref="modal" class="modal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog modal-lg" role="document">
|
||||
<Loading v-if="loading" />
|
||||
<div class="modal-content grow">
|
||||
<div class="modal-header gap-4">
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close" data-e2e="e2e-BT-newInventoryModal-close">
|
||||
<i class="sn-icon sn-icon-close"></i>
|
||||
</button>
|
||||
<h4 class="modal-title truncate !block" id="edit-project-modal-label" data-e2e="e2e-TX-newInventoryModal-title">
|
||||
{{ i18n.t('repositories.import_records.steps.step3.title') }}
|
||||
</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
|
||||
<p class="text-sn-dark-grey mb-6">
|
||||
{{ i18n.t('repositories.import_records.steps.step3.subtitle', { inventory: params.attributes.name }) }}
|
||||
</p>
|
||||
<div class="flex items-center justify-between text-sn-dark-gray text-sm">
|
||||
<div>
|
||||
<div v-html="i18n.t('repositories.import_records.steps.step3.updated_items')"></div>
|
||||
<hr class="my-1">
|
||||
<h2 class="m-0 text-sn-alert-green">{{ counters.updated }}</h2>
|
||||
</div>
|
||||
<div>
|
||||
<div v-html="i18n.t('repositories.import_records.steps.step3.new_items')"></div>
|
||||
<hr class="my-1">
|
||||
<h2 class="m-0 text-sn-alert-green">{{ counters.created }}</h2>
|
||||
</div>
|
||||
<div>
|
||||
<div v-html="i18n.t('repositories.import_records.steps.step3.unchanged_items')"></div>
|
||||
<hr class="my-1">
|
||||
<h2 class="m-0 ">{{ counters.unchanged }}</h2>
|
||||
</div>
|
||||
<div>
|
||||
<div v-html="i18n.t('repositories.import_records.steps.step3.duplicated_items')"></div>
|
||||
<hr class="my-1">
|
||||
<h2 class="m-0 text-sn-alert-passion">{{ counters.duplicated }}</h2>
|
||||
</div>
|
||||
<div>
|
||||
<div v-html="i18n.t('repositories.import_records.steps.step3.invalid_items')"></div>
|
||||
<hr class="my-1">
|
||||
<h2 class="m-0 text-sn-alert-passion">{{ counters.invalid }}</h2>
|
||||
</div>
|
||||
<div>
|
||||
<div v-html="i18n.t('repositories.import_records.steps.step3.archived_items')"></div>
|
||||
<hr class="my-1">
|
||||
<h2 class="m-0">{{ counters.archived }}</h2>
|
||||
</div>
|
||||
</div>
|
||||
<div class="my-6">
|
||||
{{ i18n.t('repositories.import_records.steps.step2.importedFileText') }} {{ params.file_name }}
|
||||
</div>
|
||||
<div class="h-80">
|
||||
<ag-grid-vue
|
||||
class="ag-theme-alpine w-full flex-grow h-full z-10"
|
||||
:columnDefs="columnDefs"
|
||||
:defaultColDef="{
|
||||
resizable: true,
|
||||
sortable: false,
|
||||
suppressMovable: true
|
||||
}"
|
||||
:rowData="tableData"
|
||||
:suppressRowTransform="true"
|
||||
:suppressRowClickSelection="true"
|
||||
:enableCellTextSelection="true"
|
||||
></ag-grid-vue>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" @click="$emit('changeStep', 'MappingStep')">
|
||||
{{ i18n.t('general.back') }}
|
||||
</button>
|
||||
<button type="button" class="btn btn-primary" @click="$emit('importRows')">
|
||||
{{ i18n.t('repositories.import_records.steps.step3.import') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { AgGridVue } from 'ag-grid-vue3';
|
||||
import modalMixin from '../../../shared/modal_mixin';
|
||||
import Loading from '../../../shared/loading.vue';
|
||||
|
||||
export default {
|
||||
name: 'PreviewStep',
|
||||
mixins: [modalMixin],
|
||||
props: {
|
||||
params: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
loading: {
|
||||
type: Boolean,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
components: {
|
||||
AgGridVue,
|
||||
Loading
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
counters() {
|
||||
return {
|
||||
updated: this.filterRows('updated').length,
|
||||
created: this.filterRows('created').length,
|
||||
unchanged: this.filterRows('unchanged').length,
|
||||
duplicated: this.filterRows('duplicated').length,
|
||||
invalid: this.filterRows('invalid').length,
|
||||
archived: this.filterRows('archived').length
|
||||
};
|
||||
},
|
||||
columnDefs() {
|
||||
const columns = [
|
||||
{
|
||||
field: 'code',
|
||||
headerName: this.i18n.t('repositories.import_records.steps.step3.code'),
|
||||
cellRenderer: this.highlightRenderer
|
||||
},
|
||||
{
|
||||
field: 'name',
|
||||
headerName: this.i18n.t('repositories.import_records.steps.step3.name'),
|
||||
cellRenderer: this.highlightRenderer
|
||||
}
|
||||
];
|
||||
|
||||
this.params.attributes.repository_columns.forEach((col) => {
|
||||
columns.push({
|
||||
field: `col_${col[0]}`,
|
||||
headerName: col[1],
|
||||
cellRenderer: this.highlightRenderer
|
||||
});
|
||||
});
|
||||
|
||||
columns.push({
|
||||
field: 'import_status',
|
||||
headerName: this.i18n.t('repositories.import_records.steps.step3.status'),
|
||||
cellRenderer: this.statusRenderer,
|
||||
pinned: 'right'
|
||||
});
|
||||
|
||||
return columns;
|
||||
},
|
||||
tableData() {
|
||||
const data = this.params.preview.data.map((row) => {
|
||||
const rowFormated = row.attributes;
|
||||
row.relationships.repository_cells.data.forEach((c) => {
|
||||
const cell = this.params.preview.included.find((c1) => c1.id === c.id);
|
||||
if (cell) {
|
||||
rowFormated[`col_${cell.attributes.repository_column_id}`] = cell.attributes.formatted_value;
|
||||
}
|
||||
});
|
||||
return rowFormated;
|
||||
});
|
||||
return data;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
filterRows(status) {
|
||||
return this.params.preview.data.filter((r) => r.attributes.import_status === status);
|
||||
},
|
||||
highlightRenderer(params) {
|
||||
const { import_status: importStatus } = params.data;
|
||||
|
||||
let color = '';
|
||||
|
||||
if (importStatus === 'created' || importStatus === 'updated') {
|
||||
color = 'text-sn-alert-green';
|
||||
} else if (importStatus === 'duplicated' || importStatus === 'invalid') {
|
||||
color = 'text-sn-alert-passion';
|
||||
}
|
||||
|
||||
return `<span class="${color}">${params.value || ''}</span>`;
|
||||
},
|
||||
statusRenderer(params) {
|
||||
const { import_status: importStatus, import_message: importMessage } = params.data;
|
||||
|
||||
let message = '';
|
||||
let color = '';
|
||||
let icon = '';
|
||||
|
||||
if (importStatus === 'created' || importStatus === 'updated') {
|
||||
message = this.i18n.t(`repositories.import_records.steps.step3.status_message.${importStatus}`);
|
||||
color = 'text-sn-alert-green';
|
||||
icon = 'check';
|
||||
} else if (importStatus === 'unchanged' || importStatus === 'archived') {
|
||||
message = this.i18n.t(`repositories.import_records.steps.step3.status_message.${importStatus}`);
|
||||
icon = 'hamburger';
|
||||
} else if (importStatus === 'duplicated' || importStatus === 'invalid') {
|
||||
message = this.i18n.t(`repositories.import_records.steps.step3.status_message.${importStatus}`);
|
||||
color = 'text-sn-alert-passion';
|
||||
icon = 'close';
|
||||
}
|
||||
|
||||
if (importMessage) {
|
||||
message = importMessage;
|
||||
}
|
||||
|
||||
return `
|
||||
<div title="${message}" class="flex items-center ${color} gap-2.5">
|
||||
<i class="sn-icon sn-icon-${icon} "></i>
|
||||
<span class="truncate">${message}</span>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
146
app/javascript/vue/repositories/modals/import/upload_step.vue
Normal file
146
app/javascript/vue/repositories/modals/import/upload_step.vue
Normal file
|
@ -0,0 +1,146 @@
|
|||
<template>
|
||||
<div ref="modal" class="modal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog flex" role="document" :class="{'!w-[900px]': showingInfo}" data-e2e="e2e-MD-invInventoryImport-upload">
|
||||
<div v-if="showingInfo" class="w-[300px] shrink-0 h-full bg-sn-super-light-grey p-6 rounded-s text-sn-dark-grey">
|
||||
<h3 class="my-0 mb-4" data-e2e="e2e-TX-invInventoryImport-uploadModal-help-title">{{ this.i18n.t('repositories.import_records.info_sidebar.title') }}</h3>
|
||||
<div v-for="i in 4" :key="i" class="flex gap-3 mb-4">
|
||||
<span class="btn btn-secondary icon-btn !text-sn-black !pointer-events-none">
|
||||
<i class="sn-icon"
|
||||
:class="i18n.t(`repositories.import_records.info_sidebar.elements.element${i - 1}.icon`)"
|
||||
:data-e2e="`e2e-IC-invInventoryImport-uploadModal-help-icon${i}`"
|
||||
></i>
|
||||
</span>
|
||||
<div>
|
||||
<div
|
||||
class="font-bold mb-2"
|
||||
:data-e2e="`e2e-TX-invInventoryImport-uploadModal-help-title${i}`"
|
||||
>{{ i18n.t(`repositories.import_records.info_sidebar.elements.element${i - 1}.label`) }}</div>
|
||||
<div
|
||||
:data-e2e="`e2e-TX-invInventoryImport-uploadModal-help-text${i}`"
|
||||
>{{ i18n.t(`repositories.import_records.info_sidebar.elements.element${i - 1}.subtext`) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex gap-3 mb-4 items-center">
|
||||
<span class="btn btn-secondary icon-btn !text-sn-black !pointer-events-none">
|
||||
<i class="sn-icon sn-icon-open"></i>
|
||||
</span>
|
||||
<a :href="i18n.t('repositories.import_records.info_sidebar.elements.element4.linkTo')" class="font-bold" target="_blank">
|
||||
{{ i18n.t('repositories.import_records.info_sidebar.elements.element4.label') }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-content grow flex flex-col" :class="{'!rounded-s-none': showingInfo}">
|
||||
<div class="modal-header gap-4">
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close" data-e2e="e2e-BT-invInventoryImport-uploadModal-close">
|
||||
<i class="sn-icon sn-icon-close"></i>
|
||||
</button>
|
||||
<button class="btn btn-light btn-sm mr-auto" @click="showingInfo = !showingInfo" data-e2e="e2e-BT-invInventoryImport-uploadModal-help">
|
||||
<i class="sn-icon sn-icon-help-s"></i>
|
||||
{{ i18n.t('repositories.import_records.steps.step1.helpText') }}
|
||||
</button>
|
||||
<h4 class="modal-title truncate !block !mr-0" id="edit-project-modal-label" data-e2e="e2e-TX-invInventoryImport-uploadModal-title">
|
||||
{{ i18n.t('repositories.import_records.steps.step1.title') }}
|
||||
</h4>
|
||||
</div>
|
||||
<div class="modal-body flex flex-col grow">
|
||||
<p class="text-sn-dark-grey" data-e2e="e2e-TX-invInventoryImport-uploadModal-description">
|
||||
{{ this.i18n.t('repositories.import_records.steps.step1.subtitle') }}
|
||||
</p>
|
||||
<h3 class="my-0 text-sn-dark-grey mb-3" data-e2e="e2e-TX-invInventoryImport-uploadModal-exportSubtitle">
|
||||
{{ i18n.t('repositories.import_records.steps.step1.exportTitle') }}
|
||||
</h3>
|
||||
<div class="flex gap-4 mb-6">
|
||||
<button class="btn btn-secondary btn-sm" @click="$emit('changeStep', 'ExportModal')" data-e2e="e2e-BT-invInventoryImport-uploadModal-exportAll">
|
||||
<i class="sn-icon sn-icon-export"></i>
|
||||
{{ i18n.t('repositories.import_records.steps.step1.exportFullInvBtnText') }}
|
||||
</button>
|
||||
<a
|
||||
:href="params.attributes.urls.export_empty_repository"
|
||||
target="_blank"
|
||||
class="btn btn-secondary btn-sm"
|
||||
data-e2e="e2e-BT-invInventoryImport-uploadModal-downloadTemplate"
|
||||
>
|
||||
<i class="sn-icon sn-icon-export"></i>
|
||||
{{ i18n.t('repositories.import_records.steps.step1.exportEmptyInvBtnText') }}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<h3 class="my-0 text-sn-dark-grey mb-3" data-e2e="e2e-TX-invInventoryImport-uploadModal-importSubtitle">
|
||||
{{ i18n.t('repositories.import_records.steps.step1.importTitle') }}
|
||||
</h3>
|
||||
<DragAndDropUpload
|
||||
class="h-60"
|
||||
@file:dropped="uploadFile"
|
||||
@file:error="handleError"
|
||||
@file:error:clear="this.error = null"
|
||||
:supportingText="`${i18n.t('repositories.import_records.steps.step1.dragAndDropSupportingText')}`"
|
||||
:supportedFormats="['xlsx', 'csv', 'xls', 'txt', 'tsv']"
|
||||
:dataE2e="'invInventoryImport-uploadModal-dragDrop'"
|
||||
/>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<div v-if="error" class="flex flex-row gap-2 my-auto mr-auto text-sn-delete-red">
|
||||
<i class="sn-icon sn-icon-alert-warning"></i>
|
||||
<div class="my-auto">{{ error }}</div>
|
||||
</div>
|
||||
<div v-if="exportInventoryMessage" class="flex flex-row gap-2 my-auto mr-auto text-sn-alert-green">
|
||||
<i class="sn-icon sn-icon-check"></i>
|
||||
<div class="my-auto">{{ exportInventoryMessage }}</div>
|
||||
</div>
|
||||
<button class="btn btn-secondary" @click="close" aria-label="Close" data-e2e="e2e-BT-invInventoryImport-uploadModal-cancel">
|
||||
{{ i18n.t('repositories.import_records.steps.step1.cancelBtnText') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import DragAndDropUpload from '../../../shared/drag_and_drop_upload.vue';
|
||||
import modalMixin from '../../../shared/modal_mixin';
|
||||
import axios from '../../../../packs/custom_axios';
|
||||
|
||||
export default {
|
||||
name: 'UploadStep',
|
||||
emits: ['uploadFile', 'close'],
|
||||
components: {
|
||||
DragAndDropUpload
|
||||
},
|
||||
mixins: [modalMixin],
|
||||
props: {
|
||||
params: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
showingInfo: false,
|
||||
error: null,
|
||||
parseSheetUrl: null,
|
||||
exportInventoryMessage: null
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
handleError(error) {
|
||||
this.error = error;
|
||||
},
|
||||
uploadFile(file) {
|
||||
const formData = new FormData();
|
||||
|
||||
// required payload
|
||||
formData.append('file', file);
|
||||
|
||||
axios.post(this.params.attributes.urls.parse_sheet, formData, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' }
|
||||
})
|
||||
.then((response) => {
|
||||
this.$emit('uploadFile', { ...this.params, ...response.data, file_name: file.name });
|
||||
}).catch((error) => {
|
||||
this.handleError(error.response.data.error);
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
|
@ -28,13 +28,13 @@
|
|||
ref="deleteModal"
|
||||
:e2eAttributes="deleteModal.e2eAttributes"
|
||||
></ConfirmationModal>
|
||||
<ConfirmationModal
|
||||
:title="exportModal.title"
|
||||
:description="exportModal.description"
|
||||
confirmClass="btn btn-primary"
|
||||
:confirmText="i18n.t('repositories.index.modal_export.export')"
|
||||
ref="exportModal"
|
||||
></ConfirmationModal>
|
||||
<ExportRepositoryModal
|
||||
v-if="exportRepository"
|
||||
:rows="exportRepository"
|
||||
:exportAction="exportAction"
|
||||
@close="exportRepository = null; exportAction = null"
|
||||
@export="updateTable"
|
||||
></ExportRepositoryModal>
|
||||
<NewRepositoryModal
|
||||
v-if="newRepository"
|
||||
:createUrl="createUrl"
|
||||
|
@ -62,6 +62,7 @@
|
|||
|
||||
import axios from '../../packs/custom_axios.js';
|
||||
import ConfirmationModal from '../shared/confirmation_modal.vue';
|
||||
import ExportRepositoryModal from './modals/export.vue';
|
||||
import NewRepositoryModal from './modals/new.vue';
|
||||
import EditRepositoryModal from './modals/edit.vue';
|
||||
import DuplicateRepositoryModal from './modals/duplicate.vue';
|
||||
|
@ -73,6 +74,7 @@ export default {
|
|||
components: {
|
||||
DataTable,
|
||||
ConfirmationModal,
|
||||
ExportRepositoryModal,
|
||||
NewRepositoryModal,
|
||||
EditRepositoryModal,
|
||||
DuplicateRepositoryModal,
|
||||
|
@ -106,10 +108,12 @@ export default {
|
|||
data() {
|
||||
return {
|
||||
reloadingTable: false,
|
||||
exportRepository: null,
|
||||
newRepository: false,
|
||||
editRepository: null,
|
||||
duplicateRepository: null,
|
||||
shareRepository: null,
|
||||
exportAction: null,
|
||||
deleteModal: {
|
||||
title: '',
|
||||
description: '',
|
||||
|
@ -148,7 +152,8 @@ export default {
|
|||
},
|
||||
{
|
||||
field: 'shared_label',
|
||||
headerName: this.i18n.t('libraries.index.table.shared')
|
||||
headerName: this.i18n.t('libraries.index.table.shared'),
|
||||
sortable: true
|
||||
},
|
||||
{
|
||||
field: 'team',
|
||||
|
@ -206,6 +211,8 @@ export default {
|
|||
this.editRepository = null;
|
||||
this.duplicateRepository = null;
|
||||
this.shareRepository = null;
|
||||
this.exportRepository = null;
|
||||
this.exportAction = null;
|
||||
},
|
||||
archive(event, rows) {
|
||||
axios.post(event.path, { repository_ids: rows.map((row) => row.id) }).then((response) => {
|
||||
|
@ -223,39 +230,6 @@ export default {
|
|||
HelperModule.flashAlertMsg(error.response.data.error, 'danger');
|
||||
});
|
||||
},
|
||||
async exportRepositories(event, rows) {
|
||||
this.exportModal.title = this.i18n.t('repositories.index.modal_export.title');
|
||||
this.exportModal.description = `
|
||||
<p class="description-p1">
|
||||
${this.i18n.t('repositories.index.modal_export.description_p1_html', {
|
||||
team_name: rows[0].team,
|
||||
count: rows.length
|
||||
})}
|
||||
</p>
|
||||
<p class="bg-sn-super-light-blue p-3">
|
||||
${this.i18n.t('repositories.index.modal_export.description_alert')}
|
||||
</p>
|
||||
<p class="mt-3">
|
||||
${this.i18n.t('repositories.index.modal_export.description_p2')}
|
||||
</p>
|
||||
<p>
|
||||
${this.i18n.t('repositories.index.modal_export.description_p3_html', {
|
||||
remaining_export_requests: event.num_of_requests_left,
|
||||
requests_limit: event.export_limit
|
||||
})}
|
||||
</p>
|
||||
`;
|
||||
|
||||
const ok = await this.$refs.exportModal.show();
|
||||
|
||||
if (ok) {
|
||||
axios.post(event.path, { repository_ids: rows.map((row) => row.id) }).then((response) => {
|
||||
HelperModule.flashAlertMsg(response.data.message, 'success');
|
||||
}).catch((error) => {
|
||||
HelperModule.flashAlertMsg(error.response.data.error, 'danger');
|
||||
});
|
||||
}
|
||||
},
|
||||
async deleteRepository(event, rows) {
|
||||
const [repository] = rows;
|
||||
this.deleteModal.e2eAttributes = {
|
||||
|
@ -288,6 +262,10 @@ export default {
|
|||
});
|
||||
}
|
||||
},
|
||||
exportRepositories(action, rows) {
|
||||
this.exportRepository = rows;
|
||||
this.exportAction = action;
|
||||
},
|
||||
update(_event, rows) {
|
||||
const [repository] = rows;
|
||||
this.editRepository = repository;
|
||||
|
|
|
@ -10,7 +10,7 @@
|
|||
@dropdown:changed="updateOperator"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="users" class="users-filter-dropdown">
|
||||
<div v-if="users" class="users-filter-dropdown max-w-[360px]">
|
||||
<DropdownSelector
|
||||
:optionClass="'checkbox-icon'"
|
||||
:dataCombineTags="true"
|
||||
|
|
|
@ -97,6 +97,26 @@
|
|||
</span>
|
||||
</div>
|
||||
|
||||
<!-- UPDATED ON-->
|
||||
<div class="flex flex-col ">
|
||||
<span class="inline-block font-semibold pb-[6px]">{{
|
||||
i18n.t('repositories.item_card.default_columns.updated_on')
|
||||
}}</span>
|
||||
<span class="inline-block text-sn-dark-grey line-clamp-3" :title="defaultColumns?.updated_on" data-e2e="e2e-TX-itemCard-updatedOn">
|
||||
{{ defaultColumns?.updated_on }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- UPDATED BY -->
|
||||
<div class="flex flex-col ">
|
||||
<span class="inline-block font-semibold pb-[6px]">{{
|
||||
i18n.t('repositories.item_card.default_columns.updated_by')
|
||||
}}</span>
|
||||
<span class="inline-block text-sn-dark-grey line-clamp-3" :title="defaultColumns?.updated_by" data-e2e="e2e-TX-itemCard-updatedBy">
|
||||
{{ defaultColumns?.updated_by }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- ARCHIVED ON -->
|
||||
<div v-if="defaultColumns.archived_on" class="flex flex-col ">
|
||||
<div class="sci-divider pb-4"></div>
|
||||
|
|
|
@ -46,6 +46,7 @@
|
|||
:label="i18n.t('repository_stock_values.manage_modal.amount')"
|
||||
:required="true"
|
||||
:min="0"
|
||||
:negativeNumbersEnabled="this.operation == 'set'"
|
||||
:error="errors.amount"
|
||||
/>
|
||||
</div>
|
||||
|
@ -252,7 +253,6 @@ export default {
|
|||
const newErrors = {};
|
||||
if (!this.unit) { newErrors.unit = I18n.t('repository_stock_values.manage_modal.unit_error'); }
|
||||
if (!this.amount) { newErrors.amount = I18n.t('repository_stock_values.manage_modal.amount_error'); }
|
||||
if (this.amount && this.amount < 0) { newErrors.amount = I18n.t('repository_stock_values.manage_modal.negative_error'); }
|
||||
if (this.reminderEnabled && !this.lowStockTreshold) { newErrors.tresholdAmount = I18n.t('repository_stock_values.manage_modal.amount_error'); }
|
||||
if (this.comment && this.comment.length > 255) { newErrors.comment = I18n.t('repository_stock_values.manage_modal.comment_limit'); }
|
||||
this.errors = newErrors;
|
||||
|
|
|
@ -16,7 +16,7 @@
|
|||
<div :class="{ 'opacity-0 pointer-events-none': dragingFile }">
|
||||
<div class="result-header flex justify-between">
|
||||
<div class="result-head-left flex items-start flex-grow gap-4">
|
||||
<a class="result-collapse-link hover:no-underline focus:no-underline py-0.5 border-0 border-y border-transparent border-solid text-sn-black"
|
||||
<a ref="toggleElement" class="result-collapse-link hover:no-underline focus:no-underline py-0.5 border-0 border-y border-transparent border-solid text-sn-black"
|
||||
:href="'#resultBody' + result.id"
|
||||
data-toggle="collapse"
|
||||
data-remote="true"
|
||||
|
@ -217,6 +217,14 @@ export default {
|
|||
if (this.activeDragResult !== this.result.id && this.dragingFile) {
|
||||
this.dragingFile = false;
|
||||
}
|
||||
},
|
||||
result: {
|
||||
handler(newVal) {
|
||||
if (this.isCollapsed !== newVal.attributes.collapsed) {
|
||||
this.toggleCollapsed();
|
||||
}
|
||||
},
|
||||
deep: true
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
|
@ -328,6 +336,7 @@ export default {
|
|||
methods: {
|
||||
toggleCollapsed() {
|
||||
this.isCollapsed = !this.isCollapsed;
|
||||
this.result.attributes.collapsed = this.isCollapsed;
|
||||
},
|
||||
dragEnter(e) {
|
||||
if (!this.urls.upload_attachment_url) return;
|
||||
|
@ -456,6 +465,10 @@ export default {
|
|||
$.post(this.urls[`create_${elementType}_url`], { tableDimensions, plateTemplate }, (result) => {
|
||||
result.data.isNew = true;
|
||||
this.elements.push(result.data);
|
||||
|
||||
if (this.isCollapsed) {
|
||||
this.$refs.toggleElement.click();
|
||||
}
|
||||
this.$emit('resultUpdated');
|
||||
}).fail(() => {
|
||||
HelperModule.flashAlertMsg(this.i18n.t('errors.general'), 'danger');
|
||||
|
|
|
@ -69,7 +69,7 @@ export default {
|
|||
data() {
|
||||
return {
|
||||
results: [],
|
||||
sort: 'created_at_desc',
|
||||
sort: null,
|
||||
filters: {},
|
||||
resultToReload: null,
|
||||
nextPageUrl: null,
|
||||
|
@ -107,8 +107,10 @@ export default {
|
|||
|
||||
if (window.scrollY + window.innerHeight >= document.body.scrollHeight - 20) {
|
||||
this.loadingPage = true;
|
||||
axios.get(this.nextPageUrl, { params: { sort: this.sort, ...this.filters } }).then((response) => {
|
||||
const params = this.sort ? { ...this.filters, sort: this.sort } : { ...this.filters };
|
||||
axios.get(this.nextPageUrl, { params }).then((response) => {
|
||||
this.results = this.results.concat(response.data.data);
|
||||
this.sort = response.data.meta.sort;
|
||||
this.nextPageUrl = response.data.links.next;
|
||||
this.loadingPage = false;
|
||||
});
|
||||
|
@ -139,9 +141,20 @@ export default {
|
|||
},
|
||||
expandAll() {
|
||||
$('.result-wrapper .collapse').collapse('show');
|
||||
this.toggleCollapsed(false);
|
||||
},
|
||||
collapseAll() {
|
||||
$('.result-wrapper .collapse').collapse('hide');
|
||||
this.toggleCollapsed(true);
|
||||
},
|
||||
toggleCollapsed(newState) {
|
||||
this.results = this.results.map((result) => ({
|
||||
...result,
|
||||
attributes: {
|
||||
...result.attributes,
|
||||
collapsed: newState
|
||||
}
|
||||
}));
|
||||
},
|
||||
removeResult(result_id) {
|
||||
this.results = this.results.filter((r) => r.id != result_id);
|
||||
|
|
|
@ -46,7 +46,7 @@
|
|||
:listItems="this.sortMenu"
|
||||
:btnClasses="'btn btn-light icon-btn'"
|
||||
:position="'right'"
|
||||
:btnIcon="'sn-icon sn-icon-sort'"
|
||||
:btnIcon="'sn-icon sn-icon-sort-down'"
|
||||
@sort="setSort"
|
||||
></MenuDropdown>
|
||||
</div>
|
||||
|
@ -123,10 +123,10 @@ export default {
|
|||
this.$emit('setFilters', filters);
|
||||
},
|
||||
collapseResults() {
|
||||
$('.result-wrapper .collapse').collapse('hide');
|
||||
this.$emit('collapseAll');
|
||||
},
|
||||
expandResults() {
|
||||
$('.result-wrapper .collapse').collapse('show');
|
||||
this.$emit('expandAll');
|
||||
},
|
||||
scrollTop() {
|
||||
window.scrollTo(0, 0);
|
||||
|
|
|
@ -0,0 +1,52 @@
|
|||
<template>
|
||||
<div class="flex items-center gap-1.5 justify-end w-[184px]">
|
||||
<OpenMenu
|
||||
:attachment="attachment"
|
||||
@open="$emit('attachment:toggle_menu', $event)"
|
||||
@close="$emit('attachment:toggle_menu', $event)"
|
||||
@menu-dropdown-toggle="$emit('attachment:toggle_menu', $event)"
|
||||
@option:click="$emit('attachment:open', $event)"
|
||||
/>
|
||||
<a v-if="attachment.attributes.urls.move"
|
||||
@click.prevent.stop="$emit('attachment:move_modal')"
|
||||
class="btn btn-light icon-btn thumbnail-action-btn"
|
||||
:title="i18n.t('attachments.thumbnail.buttons.move')">
|
||||
<i class="sn-icon sn-icon-move"></i>
|
||||
</a>
|
||||
<a class="btn btn-light icon-btn thumbnail-action-btn"
|
||||
:title="i18n.t('attachments.thumbnail.buttons.download')"
|
||||
:href="attachment.attributes.urls.download" data-turbolinks="false">
|
||||
<i class="sn-icon sn-icon-export"></i>
|
||||
</a>
|
||||
<ContextMenu
|
||||
:attachment="attachment"
|
||||
@attachment:viewMode="$emit('attachment:viewMode', $event)"
|
||||
@attachment:delete="$emit('attachment:delete', $event)"
|
||||
@attachment:moved="$emit('attachment:moved', $event)"
|
||||
@attachment:uploaded="$emit('attachment:uploaded', $event)"
|
||||
@attachment:changed="$emit('attachment:changed', $event)"
|
||||
@attachment:update="$emit('attachment:update', $event)"
|
||||
@menu-toggle="$emit('attachment:toggle_menu', $event)"
|
||||
:withBorder="withBorder"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import OpenLocallyMixin from './mixins/open_locally.js';
|
||||
import OpenMenu from './open_menu.vue';
|
||||
import ContextMenu from './context_menu.vue';
|
||||
|
||||
export default {
|
||||
name: 'attachmentActions',
|
||||
props: {
|
||||
attachment: Object,
|
||||
withBorder: false
|
||||
},
|
||||
mixins: [OpenLocallyMixin],
|
||||
components: {
|
||||
OpenMenu,
|
||||
ContextMenu
|
||||
}
|
||||
};
|
||||
</script>
|
|
@ -1,7 +1,6 @@
|
|||
<template>
|
||||
<div class="asset-context-menu"
|
||||
ref="menu"
|
||||
@mouseenter="fetchLocalAppInfo"
|
||||
>
|
||||
<a class="marvinjs-edit-button hidden"
|
||||
v-if="attachment.attributes.asset_type == 'marvinjs' && attachment.attributes.urls.marvin_js_start_edit"
|
||||
|
@ -27,19 +26,15 @@
|
|||
<MenuDropdown
|
||||
class="ml-auto"
|
||||
:listItems="this.menu"
|
||||
:btnClasses="`btn btn-sm icon-btn !bg-sn-white ${ withBorder ? 'btn-secondary' : 'btn-light'}`"
|
||||
:btnClasses="`btn icon-btn bg-sn-white ${ withBorder ? 'btn-secondary' : 'btn-light'}`"
|
||||
:position="'right'"
|
||||
:btnIcon="'sn-icon sn-icon-more-hori'"
|
||||
@open_ove_editor="openOVEditor(attachment.attributes.urls.open_vector_editor_edit)"
|
||||
@open_marvinjs_editor="openMarvinJsEditor"
|
||||
@open_scinote_editor="openScinoteEditor"
|
||||
@open_locally="openLocally"
|
||||
@delete="deleteModal = true"
|
||||
@rename="renameModal = true"
|
||||
@duplicate="duplicate"
|
||||
@viewMode="changeViewMode"
|
||||
@move="showMoveModal"
|
||||
@menu-visibility-changed="$emit('menu-visibility-changed', $event)"
|
||||
@menu-toggle="$emit('menu-toggle', $event)"
|
||||
></MenuDropdown>
|
||||
<Teleport to="body">
|
||||
<RenameAttachmentModal
|
||||
|
@ -54,27 +49,12 @@
|
|||
@confirm="deleteAttachment"
|
||||
@cancel="deleteModal = false"
|
||||
/>
|
||||
<moveAssetModal
|
||||
<MoveAssetModal
|
||||
v-if="movingAttachment"
|
||||
:parent_type="attachment.attributes.parent_type"
|
||||
:targets_url="attachment.attributes.urls.move_targets"
|
||||
@confirm="moveAttachment($event)" @cancel="closeMoveModal"
|
||||
/>
|
||||
<NoPredefinedAppModal
|
||||
v-if="showNoPredefinedAppModal"
|
||||
:fileName="attachment.attributes.file_name"
|
||||
@close="showNoPredefinedAppModal = false"
|
||||
/>
|
||||
<UpdateVersionModal
|
||||
v-if="showUpdateVersionModal"
|
||||
@close="showUpdateVersionModal = false"
|
||||
/>
|
||||
<editLaunchingApplicationModal
|
||||
v-if="editAppModal"
|
||||
:fileName="attachment.attributes.file_name"
|
||||
:application="this.localAppName"
|
||||
@close="editAppModal = false"
|
||||
/>
|
||||
</Teleport>
|
||||
</div>
|
||||
</template>
|
||||
|
@ -82,9 +62,8 @@
|
|||
<script>
|
||||
import RenameAttachmentModal from '../modal/rename_modal.vue';
|
||||
import deleteAttachmentModal from './delete_modal.vue';
|
||||
import moveAssetModal from '../modal/move.vue';
|
||||
import MoveAssetModal from '../modal/move.vue';
|
||||
import MoveMixin from './mixins/move.js';
|
||||
import OpenLocallyMixin from './mixins/open_locally.js';
|
||||
import MenuDropdown from '../../menu_dropdown.vue';
|
||||
import axios from '../../../../packs/custom_axios.js';
|
||||
|
||||
|
@ -93,16 +72,20 @@ export default {
|
|||
components: {
|
||||
RenameAttachmentModal,
|
||||
deleteAttachmentModal,
|
||||
moveAssetModal,
|
||||
MoveAssetModal,
|
||||
MenuDropdown
|
||||
},
|
||||
mixins: [MoveMixin, OpenLocallyMixin],
|
||||
mixins: [MoveMixin],
|
||||
props: {
|
||||
attachment: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
withBorder: { default: false, type: Boolean }
|
||||
withBorder: { default: false, type: Boolean },
|
||||
displayInDropdown: {
|
||||
type: Array,
|
||||
default: []
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
@ -114,59 +97,12 @@ export default {
|
|||
computed: {
|
||||
menu() {
|
||||
const menu = [];
|
||||
if (this.attachment.attributes.wopi && this.attachment.attributes.urls.edit_asset) {
|
||||
if (this.displayInDropdown.includes('download')) {
|
||||
menu.push({
|
||||
text: this.attachment.attributes.wopi_context.button_text,
|
||||
url: this.attachment.attributes.urls.edit_asset,
|
||||
text: this.i18n.t('Download'),
|
||||
url: this.attachment.attributes.urls.download,
|
||||
url_target: '_blank',
|
||||
data_e2e: 'e2e-BT-attachmentOptions-openInWopi'
|
||||
});
|
||||
}
|
||||
if (this.attachment.attributes.asset_type === 'gene_sequence' && this.attachment.attributes.urls.open_vector_editor_edit) {
|
||||
menu.push({
|
||||
text: this.i18n.t('open_vector_editor.edit_sequence'),
|
||||
emit: 'open_ove_editor',
|
||||
data_e2e: 'e2e-BT-attachmentOptions-openInOve'
|
||||
});
|
||||
}
|
||||
if (this.attachment.attributes.asset_type === 'marvinjs' && this.attachment.attributes.urls.marvin_js_start_edit) {
|
||||
menu.push({
|
||||
text: this.i18n.t('assets.file_preview.edit_in_marvinjs'),
|
||||
emit: 'open_marvinjs_editor',
|
||||
data_e2e: 'e2e-BT-attachmentOptions-openInMarvin'
|
||||
});
|
||||
}
|
||||
if (this.attachment.attributes.asset_type !== 'marvinjs'
|
||||
&& this.attachment.attributes.image_editable
|
||||
&& this.attachment.attributes.urls.start_edit_image) {
|
||||
menu.push({
|
||||
text: this.i18n.t('assets.file_preview.edit_in_scinote'),
|
||||
emit: 'open_scinote_editor',
|
||||
data_e2e: 'e2e-BT-attachmentOptions-openInImageEditor'
|
||||
});
|
||||
}
|
||||
if (this.canOpenLocally) {
|
||||
const text = this.localAppName
|
||||
? this.i18n.t('attachments.open_locally_in', { application: this.localAppName })
|
||||
: this.i18n.t('attachments.open_locally');
|
||||
|
||||
menu.push({
|
||||
text,
|
||||
emit: 'open_locally',
|
||||
data_e2e: 'e2e-BT-attachmentOptions-openLocally'
|
||||
});
|
||||
}
|
||||
menu.push({
|
||||
text: this.i18n.t('Download'),
|
||||
url: this.attachment.attributes.urls.download,
|
||||
url_target: '_blank',
|
||||
data_e2e: 'e2e-BT-attachmentOptions-download'
|
||||
});
|
||||
if (this.attachment.attributes.urls.move_targets) {
|
||||
menu.push({
|
||||
text: this.i18n.t('assets.context_menu.move'),
|
||||
emit: 'move',
|
||||
data_e2e: 'e2e-BT-attachmentOptions-move'
|
||||
data_e2e: 'e2e-BT-attachmentOptions-download'
|
||||
});
|
||||
}
|
||||
if (this.attachment.attributes.urls.duplicate) {
|
||||
|
@ -229,16 +165,6 @@ export default {
|
|||
},
|
||||
reloadAttachments() {
|
||||
this.$emit('attachment:uploaded');
|
||||
},
|
||||
openMarvinJsEditor() {
|
||||
MarvinJsEditor.initNewButton(
|
||||
this.$refs.marvinjsEditButton,
|
||||
this.reloadAttachments
|
||||
);
|
||||
$(this.$refs.marvinjsEditButton).trigger('click');
|
||||
},
|
||||
openScinoteEditor() {
|
||||
$(this.$refs.imageEditButton).trigger('click');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
@ -1,9 +1,12 @@
|
|||
<template>
|
||||
<div class="inline-attachment-container asset"
|
||||
:data-asset-id="attachment.id"
|
||||
:data-e2e="`e2e-CO-${dataE2e}-attachment${attachment.id}-inline`"
|
||||
<div
|
||||
class="inline-attachment-container asset"
|
||||
:class="[{'menu-dropdown-open': isMenuDropdownOpen}, {'context-menu-open': isContextMenuOpen }]"
|
||||
:data-e2e="`e2e-CO-${dataE2e}-attachment${attachment.id}-inline`"
|
||||
ref="inlineAttachmentContainer"
|
||||
:data-asset-id="attachment.id"
|
||||
>
|
||||
<div class="header">
|
||||
<div class="header justify-between">
|
||||
<div class="file-info">
|
||||
<a :href="attachment.attributes.urls.blob"
|
||||
class="file-preview-link file-name"
|
||||
|
@ -21,22 +24,27 @@
|
|||
</a>
|
||||
<div class="file-metadata">
|
||||
<span>
|
||||
{{ i18n.t('assets.placeholder.modified_label') }}
|
||||
{{ attachment.attributes.updated_at_formatted }}
|
||||
</span>
|
||||
<span>
|
||||
{{ i18n.t('assets.placeholder.size_label', {size: attachment.attributes.file_size_formatted}) }}
|
||||
{{ attachment.attributes.file_size_formatted }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<ContextMenu
|
||||
:attachment="attachment"
|
||||
@attachment:viewMode="updateViewMode"
|
||||
@attachment:delete="deleteAttachment"
|
||||
@attachment:moved="attachmentMoved"
|
||||
@attachment:uploaded="reloadAttachments"
|
||||
@attachment:update="$emit('attachment:update', $event)"
|
||||
/>
|
||||
<div class="flex items-center ml-auto gap-2">
|
||||
<AttachmentActions
|
||||
:attachment="attachment"
|
||||
@attachment:viewMode="updateViewMode"
|
||||
@attachment:delete="deleteAttachment"
|
||||
@attachment:moved="attachmentMoved"
|
||||
@attachment:uploaded="reloadAttachments"
|
||||
@attachment:changed="$emit('attachment:changed', $event)"
|
||||
@attachment:update="$emit('attachment:update', $event)"
|
||||
@attachment:toggle_menu="toggleMenuDropdown"
|
||||
@attachment:move_modal="showMoveModal"
|
||||
@attachment:open="$emit($event)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<template v-if="attachment.attributes.wopi">
|
||||
<div v-if="showWopi"
|
||||
|
@ -65,6 +73,14 @@
|
|||
<i class="text-sn-grey sn-icon" :class="attachment.attributes.icon"></i>
|
||||
</div>
|
||||
</template>
|
||||
<Teleport to="body">
|
||||
<MoveAssetModal
|
||||
v-if="movingAttachment"
|
||||
:parent_type="attachment.attributes.parent_type"
|
||||
:targets_url="attachment.attributes.urls.move_targets"
|
||||
@confirm="moveAttachment($event)" @cancel="closeMoveModal"
|
||||
/>
|
||||
</Teleport>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
@ -74,11 +90,22 @@ import AttachmentMovedMixin from './mixins/attachment_moved.js';
|
|||
import ContextMenuMixin from './mixins/context_menu.js';
|
||||
import ContextMenu from './context_menu.vue';
|
||||
import PdfViewer from '../../pdf_viewer.vue';
|
||||
import MoveAssetModal from '../modal/move.vue';
|
||||
import MoveMixin from './mixins/move.js';
|
||||
import OpenLocallyMixin from './mixins/open_locally.js';
|
||||
import AttachmentActions from './attachment_actions.vue';
|
||||
import OpenMenu from './open_menu.vue';
|
||||
|
||||
export default {
|
||||
name: 'inlineAttachment',
|
||||
mixins: [ContextMenuMixin, AttachmentMovedMixin],
|
||||
components: { ContextMenu, PdfViewer },
|
||||
mixins: [ContextMenuMixin, AttachmentMovedMixin, MoveMixin, OpenLocallyMixin],
|
||||
components: {
|
||||
ContextMenu,
|
||||
PdfViewer,
|
||||
MoveAssetModal,
|
||||
OpenMenu,
|
||||
AttachmentActions
|
||||
},
|
||||
props: {
|
||||
attachment: {
|
||||
type: Object,
|
||||
|
@ -95,7 +122,9 @@ export default {
|
|||
},
|
||||
data() {
|
||||
return {
|
||||
showWopi: false
|
||||
showWopi: false,
|
||||
isMenuDropdownOpen: false,
|
||||
isContextMenuOpen: false
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
|
@ -120,7 +149,13 @@ export default {
|
|||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
toggleContextMenu(isOpen) {
|
||||
this.isContextMenuOpen = isOpen;
|
||||
},
|
||||
toggleMenuDropdown(isOpen) {
|
||||
this.isMenuDropdownOpen = isOpen;
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
|
|
@ -21,23 +21,37 @@
|
|||
<img :src="this.imageLoadError ? attachment.attributes.urls.blob : attachment.attributes.medium_preview" @error="ActiveStoragePreviews.reCheckPreview"
|
||||
@load="ActiveStoragePreviews.showPreview"/>
|
||||
</div>
|
||||
<div class="file-metadata">
|
||||
<span>
|
||||
<div class="flex items-center gap-2 text-xs text-sn-grey overflow-hidden ml-auto">
|
||||
<span class="truncate" :title="i18n.t('assets.placeholder.modified_label') + ' ' + attachment.attributes.updated_at_formatted">
|
||||
{{ i18n.t('assets.placeholder.modified_label') }}
|
||||
{{ attachment.attributes.updated_at_formatted }}
|
||||
</span>
|
||||
<span>
|
||||
<span class="truncate" :title="i18n.t('assets.placeholder.size_label', {size: attachment.attributes.file_size_formatted})">
|
||||
{{ i18n.t('assets.placeholder.size_label', {size: attachment.attributes.file_size_formatted}) }}
|
||||
</span>
|
||||
</div>
|
||||
<ContextMenu
|
||||
:attachment="attachment"
|
||||
@attachment:viewMode="updateViewMode"
|
||||
@attachment:delete="deleteAttachment"
|
||||
@attachment:moved="attachmentMoved"
|
||||
@attachment:uploaded="reloadAttachments"
|
||||
@attachment:update="$emit('attachment:update', $event)"
|
||||
/>
|
||||
<div class="attachment-actions shrink-0 ml-auto">
|
||||
<AttachmentActions
|
||||
:attachment="attachment"
|
||||
@attachment:viewMode="updateViewMode"
|
||||
@attachment:delete="deleteAttachment"
|
||||
@attachment:moved="attachmentMoved"
|
||||
@attachment:uploaded="reloadAttachments"
|
||||
@attachment:changed="$emit('attachment:changed', $event)"
|
||||
@attachment:update="$emit('attachment:update', $event)"
|
||||
@attachment:toggle_menu="toggleMenuDropdown"
|
||||
@attachment:move_modal="showMoveModal"
|
||||
@attachment:open="$emit($event)"
|
||||
/>
|
||||
</div>
|
||||
<Teleport to="body">
|
||||
<moveAssetModal
|
||||
v-if="movingAttachment"
|
||||
:parent_type="attachment.attributes.parent_type"
|
||||
:targets_url="attachment.attributes.urls.move_targets"
|
||||
@confirm="moveAttachment($event)" @cancel="closeMoveModal"
|
||||
/>
|
||||
</Teleport>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
@ -45,11 +59,20 @@
|
|||
import AttachmentMovedMixin from './mixins/attachment_moved.js';
|
||||
import ContextMenuMixin from './mixins/context_menu.js';
|
||||
import ContextMenu from './context_menu.vue';
|
||||
import MoveMixin from './mixins/move.js';
|
||||
import MoveAssetModal from '../modal/move.vue';
|
||||
import AttachmentActions from './attachment_actions.vue';
|
||||
import OpenMenu from './open_menu.vue';
|
||||
|
||||
export default {
|
||||
name: 'listAttachment',
|
||||
mixins: [ContextMenuMixin, AttachmentMovedMixin],
|
||||
components: { ContextMenu },
|
||||
mixins: [ContextMenuMixin, AttachmentMovedMixin, MoveMixin],
|
||||
components: {
|
||||
ContextMenu,
|
||||
MoveAssetModal,
|
||||
OpenMenu,
|
||||
AttachmentActions
|
||||
},
|
||||
props: {
|
||||
attachment: {
|
||||
type: Object,
|
||||
|
@ -66,13 +89,21 @@ export default {
|
|||
},
|
||||
data() {
|
||||
return {
|
||||
imageLoadError: false
|
||||
imageLoadError: false,
|
||||
isContextMenuOpen: false,
|
||||
isMenuDropdownOpen: false
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
handleImageError() {
|
||||
this.imageLoadError = true;
|
||||
},
|
||||
toggleContextMenu(isOpen) {
|
||||
this.isContextMenuOpen = isOpen;
|
||||
},
|
||||
toggleMenuDropdown(isOpen) {
|
||||
this.isMenuDropdownOpen = isOpen;
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
|
|
@ -14,6 +14,9 @@ export default {
|
|||
methods: {
|
||||
openOVEditor() {
|
||||
window.showIFrameModal(this.OVEurl);
|
||||
if (this.isCollapsed) {
|
||||
this.$refs.toggleElement.click();
|
||||
}
|
||||
},
|
||||
initOVE() {
|
||||
$(window.iFrameModal).on('sequenceSaved', () => {
|
||||
|
|
175
app/javascript/vue/shared/content/attachments/open_menu.vue
Normal file
175
app/javascript/vue/shared/content/attachments/open_menu.vue
Normal file
|
@ -0,0 +1,175 @@
|
|||
<template>
|
||||
<span>
|
||||
<!-- multiple options -->
|
||||
<MenuDropdown
|
||||
v-if="multipleOpenOptions.length > 1"
|
||||
:listItems="multipleOpenOptions"
|
||||
:btnClasses="'btn btn-light icon-btn thumbnail-action-btn'"
|
||||
:position="'left'"
|
||||
:btnIcon="'sn-icon sn-icon-open'"
|
||||
:title="i18n.t('attachments.thumbnail.buttons.open')"
|
||||
@menu-toggle="toggleMenu"
|
||||
@open_locally="openLocally"
|
||||
@open_scinote_editor="openScinoteEditor"
|
||||
>
|
||||
</MenuDropdown>
|
||||
|
||||
<!-- open locally -->
|
||||
<a
|
||||
class="btn btn-light icon-btn thumbnail-action-btn"
|
||||
v-else-if="canOpenLocally"
|
||||
@click="openLocally"
|
||||
:title="i18n.t('attachments.thumbnail.buttons.open')"
|
||||
>
|
||||
<i class="sn-icon sn-icon-open"></i>
|
||||
</a>
|
||||
|
||||
<!-- wopi -->
|
||||
<a
|
||||
class="btn btn-light icon-btn thumbnail-action-btn"
|
||||
v-else-if="this.attachment.attributes.wopi && this.attachment.attributes.urls.edit_asset"
|
||||
:href="attachment.attributes.urls.edit_asset"
|
||||
:title="i18n.t('attachments.thumbnail.buttons.open')"
|
||||
id="wopi_file_edit_button"
|
||||
:class="attachment.attributes.wopi_context.edit_supported ? '' : 'disabled'"
|
||||
target="_blank"
|
||||
>
|
||||
<i class="sn-icon sn-icon-open"></i>
|
||||
</a>
|
||||
|
||||
<!-- gene sequence -->
|
||||
<a
|
||||
class="btn btn-light icon-btn thumbnail-action-btn ove-edit-button"
|
||||
v-else-if="attachment.attributes.asset_type == 'gene_sequence' && attachment.attributes.urls.open_vector_editor_edit"
|
||||
@click="openOVEditor(attachment.attributes.urls.open_vector_editor_edit)"
|
||||
>
|
||||
<i class="sn-icon sn-icon-open"></i>
|
||||
</a>
|
||||
|
||||
<!-- marvin js -->
|
||||
<a
|
||||
class="btn btn-light icon-btn thumbnail-action-btn marvinjs-edit-button"
|
||||
v-else-if="attachment.attributes.asset_type == 'marvinjs' && attachment.attributes.urls.marvin_js_start_edit"
|
||||
:data-sketch-id="attachment.id"
|
||||
:data-update-url="attachment.attributes.urls.marvin_js"
|
||||
:data-sketch-start-edit-url="attachment.attributes.urls.marvin_js_start_edit"
|
||||
:data-sketch-name="attachment.attributes.metadata.name"
|
||||
:data-sketch-description="attachment.attributes.metadata.description"
|
||||
>
|
||||
<i class="sn-icon sn-icon-open"></i>
|
||||
</a>
|
||||
|
||||
<!-- editing -->
|
||||
<a
|
||||
class="btn btn-light icon-btn thumbnail-action-btn image-edit-button"
|
||||
v-else-if="attachment.attributes.image_editable && attachment.attributes.urls.edit_asset"
|
||||
:title="i18n.t('attachments.thumbnail.buttons.open')"
|
||||
:data-image-id="attachment.id"
|
||||
:data-image-name="attachment.attributes.file_name"
|
||||
:data-image-url="attachment.attributes.urls.asset_file"
|
||||
:data-image-quality="attachment.attributes.image_context && attachment.attributes.image_context.quality"
|
||||
:data-image-mime-type="attachment.attributes.image_context && attachment.attributes.image_context.type"
|
||||
:data-image-start-edit-url="attachment.attributes.urls.start_edit_image"
|
||||
>
|
||||
<i class="sn-icon sn-icon-open"></i>
|
||||
</a>
|
||||
|
||||
<!-- hidden -->
|
||||
<a class="image-edit-button hidden"
|
||||
v-if="attachment.attributes.asset_type != 'marvinjs'
|
||||
&& attachment.attributes.image_editable
|
||||
&& attachment.attributes.urls.start_edit_image"
|
||||
ref="imageEditButton"
|
||||
:data-image-id="attachment.id"
|
||||
:data-image-name="attachment.attributes.file_name"
|
||||
:data-image-url="attachment.attributes.urls.asset_file"
|
||||
:data-image-quality="attachment.attributes.image_context.quality"
|
||||
:data-image-mime-type="attachment.attributes.image_context.type"
|
||||
:data-image-start-edit-url="attachment.attributes.urls.start_edit_image"
|
||||
></a>
|
||||
<Teleport to="body">
|
||||
<NoPredefinedAppModal
|
||||
v-if="showNoPredefinedAppModal"
|
||||
:fileName="attachment.attributes.file_name"
|
||||
@close="showNoPredefinedAppModal = false"
|
||||
/>
|
||||
<UpdateVersionModal
|
||||
v-if="showUpdateVersionModal"
|
||||
@close="showUpdateVersionModal = false"
|
||||
/>
|
||||
<editLaunchingApplicationModal
|
||||
v-if="editAppModal"
|
||||
:fileName="attachment.attributes.file_name"
|
||||
:application="this.localAppName"
|
||||
@close="editAppModal = false"
|
||||
/>
|
||||
</Teleport>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import OpenLocallyMixin from './mixins/open_locally.js';
|
||||
import MenuDropdown from '../../../shared/menu_dropdown.vue';
|
||||
|
||||
export default {
|
||||
name: 'multipleOpen',
|
||||
mixins: [OpenLocallyMixin],
|
||||
emits: ['menu-dropdown-toggle'],
|
||||
components: {
|
||||
MenuDropdown
|
||||
},
|
||||
props: {
|
||||
attachment: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.fetchLocalAppInfo();
|
||||
},
|
||||
computed: {
|
||||
multipleOpenOptions() {
|
||||
const options = [];
|
||||
if (this.attachment.attributes.wopi && this.attachment.attributes.urls.edit_asset) {
|
||||
options.push({
|
||||
text: this.attachment.attributes.wopi_context.button_text,
|
||||
url: this.attachment.attributes.urls.edit_asset,
|
||||
url_target: '_blank'
|
||||
});
|
||||
}
|
||||
if (this.attachment.attributes.asset_type !== 'marvinjs'
|
||||
&& this.attachment.attributes.image_editable
|
||||
&& this.attachment.attributes.urls.start_edit_image) {
|
||||
options.push({
|
||||
text: this.i18n.t('assets.file_preview.edit_in_scinote'),
|
||||
emit: 'open_scinote_editor'
|
||||
});
|
||||
}
|
||||
if (this.canOpenLocally) {
|
||||
const text = this.localAppName
|
||||
? this.i18n.t('attachments.open_locally_in', { application: this.localAppName })
|
||||
: this.i18n.t('attachments.open_locally');
|
||||
|
||||
options.push({
|
||||
text,
|
||||
emit: 'open_locally',
|
||||
data_e2e: 'e2e-BT-attachmentOptions-openLocally'
|
||||
});
|
||||
}
|
||||
return options;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
toggleMenu(isOpen) {
|
||||
this.$emit('menu-dropdown-toggle', isOpen);
|
||||
},
|
||||
openScinoteEditor() {
|
||||
this.$refs.imageEditButton.click();
|
||||
},
|
||||
openOVEditor(url) {
|
||||
window.showIFrameModal(url);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
</script>
|
|
@ -33,7 +33,7 @@
|
|||
<div :class="{ hidden: !showOptions }" class="hovered-thumbnail h-full">
|
||||
<a
|
||||
:href="attachment.attributes.urls.blob"
|
||||
class="file-preview-link file-name"
|
||||
class="file-preview-link file-name max-h-36 overflow-auto"
|
||||
:id="`modal_link${attachment.id}`"
|
||||
data-no-turbolink="true"
|
||||
:data-id="attachment.id"
|
||||
|
@ -45,95 +45,23 @@
|
|||
<div class="absolute bottom-16 text-sn-grey">
|
||||
{{ attachment.attributes.file_size_formatted }}
|
||||
</div>
|
||||
<div class="absolute bottom-4 w-[184px] grid grid-cols-[repeat(4,_2.5rem)] justify-between">
|
||||
<MenuDropdown
|
||||
v-if="multipleOpenOptions.length > 1"
|
||||
:listItems="multipleOpenOptions"
|
||||
:btnClasses="'btn btn-light icon-btn thumbnail-action-btn'"
|
||||
:position="'left'"
|
||||
:btnIcon="'sn-icon sn-icon-open'"
|
||||
:title="i18n.t('attachments.thumbnail.buttons.open')"
|
||||
@menu-visibility-changed="handleMenuVisibilityChange"
|
||||
@open_locally="openLocally"
|
||||
@open_scinote_editor="openScinoteEditor"
|
||||
></MenuDropdown>
|
||||
<a class="btn btn-light icon-btn thumbnail-action-btn"
|
||||
v-else-if="canOpenLocally"
|
||||
@click="openLocally"
|
||||
:title="i18n.t('attachments.thumbnail.buttons.open')"
|
||||
>
|
||||
<i class="sn-icon sn-icon-open"></i>
|
||||
</a>
|
||||
<a class="btn btn-light icon-btn thumbnail-action-btn"
|
||||
v-else-if="this.attachment.attributes.wopi && this.attachment.attributes.urls.edit_asset"
|
||||
:href="attachment.attributes.urls.edit_asset"
|
||||
:title="i18n.t('attachments.thumbnail.buttons.open')"
|
||||
id="wopi_file_edit_button"
|
||||
:class="attachment.attributes.wopi_context.edit_supported ? '' : 'disabled'"
|
||||
target="_blank"
|
||||
>
|
||||
<i class="sn-icon sn-icon-open"></i>
|
||||
</a>
|
||||
<a class="btn btn-light icon-btn thumbnail-action-btn ove-edit-button"
|
||||
v-else-if="attachment.attributes.asset_type == 'gene_sequence' && attachment.attributes.urls.open_vector_editor_edit"
|
||||
@click="openOVEditor(attachment.attributes.urls.open_vector_editor_edit)"
|
||||
>
|
||||
<i class="sn-icon sn-icon-open"></i>
|
||||
</a>
|
||||
<a class="btn btn-light icon-btn thumbnail-action-btn marvinjs-edit-button"
|
||||
v-else-if="attachment.attributes.asset_type == 'marvinjs' && attachment.attributes.urls.marvin_js_start_edit"
|
||||
:data-sketch-id="attachment.id"
|
||||
:data-update-url="attachment.attributes.urls.marvin_js"
|
||||
:data-sketch-start-edit-url="attachment.attributes.urls.marvin_js_start_edit"
|
||||
:data-sketch-name="attachment.attributes.metadata.name"
|
||||
:data-sketch-description="attachment.attributes.metadata.description"
|
||||
>
|
||||
<i class="sn-icon sn-icon-open"></i>
|
||||
</a>
|
||||
<a class="btn btn-light icon-btn thumbnail-action-btn image-edit-button"
|
||||
v-else-if="attachment.attributes.image_editable && attachment.attributes.urls.edit_asset"
|
||||
:title="i18n.t('attachments.thumbnail.buttons.open')"
|
||||
:data-image-id="attachment.id"
|
||||
:data-image-name="attachment.attributes.file_name"
|
||||
:data-image-url="attachment.attributes.urls.asset_file"
|
||||
:data-image-quality="attachment.attributes.image_context && attachment.attributes.image_context.quality"
|
||||
:data-image-mime-type="attachment.attributes.image_context && attachment.attributes.image_context.type"
|
||||
:data-image-start-edit-url="attachment.attributes.urls.start_edit_image"
|
||||
>
|
||||
<i class="sn-icon sn-icon-open"></i>
|
||||
</a>
|
||||
<a v-if="attachment.attributes.urls.move"
|
||||
@click.prevent.stop="showMoveModal"
|
||||
class="btn btn-light icon-btn thumbnail-action-btn"
|
||||
:title="i18n.t('attachments.thumbnail.buttons.move')">
|
||||
<i class="sn-icon sn-icon-move"></i>
|
||||
</a>
|
||||
<a class="btn btn-light icon-btn thumbnail-action-btn"
|
||||
:title="i18n.t('attachments.thumbnail.buttons.download')"
|
||||
:href="attachment.attributes.urls.download" data-turbolinks="false">
|
||||
<i class="sn-icon sn-icon-export"></i>
|
||||
</a>
|
||||
<template v-if="attachment.attributes.urls.delete">
|
||||
<a class="btn btn-light icon-btn thumbnail-action-btn"
|
||||
:title="i18n.t('attachments.thumbnail.buttons.delete')"
|
||||
@click.prevent.stop="deleteModal = true">
|
||||
<i class="sn-icon sn-icon-delete"></i>
|
||||
</a>
|
||||
</template>
|
||||
<div class="absolute bottom-4">
|
||||
<AttachmentActions
|
||||
:withBorder="true"
|
||||
:attachment="attachment"
|
||||
:showOptions="showOptions"
|
||||
@attachment:viewMode="updateViewMode"
|
||||
@attachment:delete="deleteAttachment"
|
||||
@attachment:moved="attachmentMoved"
|
||||
@attachment:uploaded="reloadAttachments"
|
||||
@attachment:changed="$emit('attachment:changed', $event)"
|
||||
@attachment:update="$emit('attachment:update', $event)"
|
||||
@attachment:toggle_menu="toggleMenu"
|
||||
@attachment:move_modal="showMoveModal"
|
||||
@attachment:open="$emit($event)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<ContextMenu
|
||||
v-show="showOptions"
|
||||
:attachment="attachment"
|
||||
@attachment:viewMode="updateViewMode"
|
||||
@attachment:delete="deleteAttachment"
|
||||
@attachment:moved="attachmentMoved"
|
||||
@attachment:uploaded="reloadAttachments"
|
||||
@attachment:changed="$emit('attachment:changed', $event)"
|
||||
@attachment:update="$emit('attachment:update', $event)"
|
||||
@menu-visibility-changed="handleMenuVisibilityChange"
|
||||
:withBorder="true"
|
||||
/>
|
||||
<Teleport to="body">
|
||||
<deleteAttachmentModal
|
||||
v-if="deleteModal"
|
||||
|
@ -141,27 +69,12 @@
|
|||
@confirm="deleteAttachment"
|
||||
@cancel="deleteModal = false"
|
||||
/>
|
||||
<moveAssetModal
|
||||
<MoveAssetModal
|
||||
v-if="movingAttachment"
|
||||
:parent_type="attachment.attributes.parent_type"
|
||||
:targets_url="attachment.attributes.urls.move_targets"
|
||||
@confirm="moveAttachment($event)" @cancel="closeMoveModal"
|
||||
/>
|
||||
<NoPredefinedAppModal
|
||||
v-if="showNoPredefinedAppModal"
|
||||
:fileName="attachment.attributes.file_name"
|
||||
@close="showNoPredefinedAppModal = false"
|
||||
/>
|
||||
<UpdateVersionModal
|
||||
v-if="showUpdateVersionModal"
|
||||
@close="showUpdateVersionModal = false"
|
||||
/>
|
||||
<editLaunchingApplicationModal
|
||||
v-if="editAppModal"
|
||||
:fileName="attachment.attributes.file_name"
|
||||
:application="this.localAppName"
|
||||
@close="editAppModal = false"
|
||||
/>
|
||||
</Teleport>
|
||||
<a class="image-edit-button hidden"
|
||||
v-if="attachment.attributes.asset_type != 'marvinjs'
|
||||
|
@ -186,17 +99,20 @@ import deleteAttachmentModal from './delete_modal.vue';
|
|||
import MenuDropdown from '../../../shared/menu_dropdown.vue';
|
||||
import MoveAssetModal from '../modal/move.vue';
|
||||
import MoveMixin from './mixins/move.js';
|
||||
import OpenLocallyMixin from './mixins/open_locally.js';
|
||||
import OpenMenu from './open_menu.vue';
|
||||
import AttachmentActions from './attachment_actions.vue';
|
||||
import { vOnClickOutside } from '@vueuse/components';
|
||||
|
||||
export default {
|
||||
name: 'thumbnailAttachment',
|
||||
mixins: [ContextMenuMixin, AttachmentMovedMixin, MoveMixin, OpenLocallyMixin],
|
||||
mixins: [ContextMenuMixin, AttachmentMovedMixin, MoveMixin],
|
||||
components: {
|
||||
ContextMenu,
|
||||
deleteAttachmentModal,
|
||||
MoveAssetModal,
|
||||
MenuDropdown
|
||||
MenuDropdown,
|
||||
OpenMenu,
|
||||
AttachmentActions
|
||||
},
|
||||
props: {
|
||||
attachment: {
|
||||
|
@ -222,38 +138,6 @@ export default {
|
|||
directives: {
|
||||
'click-outside': vOnClickOutside
|
||||
},
|
||||
computed: {
|
||||
multipleOpenOptions() {
|
||||
const options = [];
|
||||
if (this.attachment.attributes.wopi && this.attachment.attributes.urls.edit_asset) {
|
||||
options.push({
|
||||
text: this.attachment.attributes.wopi_context.button_text,
|
||||
url: this.attachment.attributes.urls.edit_asset,
|
||||
url_target: '_blank'
|
||||
});
|
||||
}
|
||||
if (this.attachment.attributes.asset_type !== 'marvinjs'
|
||||
&& this.attachment.attributes.image_editable
|
||||
&& this.attachment.attributes.urls.start_edit_image) {
|
||||
options.push({
|
||||
text: this.i18n.t('assets.file_preview.edit_in_scinote'),
|
||||
emit: 'open_scinote_editor'
|
||||
});
|
||||
}
|
||||
if (this.canOpenLocally) {
|
||||
const text = this.localAppName
|
||||
? this.i18n.t('attachments.open_locally_in', { application: this.localAppName })
|
||||
: this.i18n.t('attachments.open_locally');
|
||||
|
||||
options.push({
|
||||
text,
|
||||
emit: 'open_locally',
|
||||
data_e2e: 'e2e-BT-attachmentOptions-openLocally'
|
||||
});
|
||||
}
|
||||
return options;
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
$(this.$nextTick(() => {
|
||||
$('.attachment-preview img')
|
||||
|
@ -286,10 +170,9 @@ export default {
|
|||
}
|
||||
},
|
||||
async handleMouseEnter() {
|
||||
await this.fetchLocalAppInfo();
|
||||
this.showOptions = true;
|
||||
},
|
||||
handleMenuVisibilityChange(isMenuOpen) {
|
||||
toggleMenu(isMenuOpen) {
|
||||
this.isMenuOpen = isMenuOpen;
|
||||
if (isMenuOpen) {
|
||||
this.showOptions = true;
|
||||
|
|
|
@ -149,6 +149,7 @@ export default {
|
|||
onBlurHandler() {
|
||||
this.$nextTick(() => {
|
||||
this.editingText = false;
|
||||
this.$emit('editEnd');
|
||||
});
|
||||
},
|
||||
updateText(text, withKey) {
|
||||
|
|
|
@ -30,6 +30,7 @@ export default {
|
|||
},
|
||||
loadFromComputer() {
|
||||
this.uploadFiles(this.$refs.fileSelector.files);
|
||||
this.toggleCollapsedSection();
|
||||
},
|
||||
openMarvinJsModal(button) {
|
||||
MarvinJsEditor.initNewButton('.new-marvinjs-upload-button', this.loadAttachments);
|
||||
|
@ -40,11 +41,17 @@ export default {
|
|||
if (status === 'success') {
|
||||
const attachment = attachmentData.data;
|
||||
this.addAttachment(attachment);
|
||||
this.toggleCollapsedSection();
|
||||
} else {
|
||||
HelperModule.flashAlertMsg(this.i18n.t('errors.general'), 'danger');
|
||||
}
|
||||
});
|
||||
},
|
||||
toggleCollapsedSection() {
|
||||
if (this.isCollapsed) {
|
||||
this.$refs.toggleElement.click();
|
||||
}
|
||||
},
|
||||
addAttachment(attachment) {
|
||||
this.attachments.push(attachment);
|
||||
this.showFileModal = false;
|
||||
|
|
|
@ -267,6 +267,16 @@ export default {
|
|||
|
||||
this.$emit('update', this.element, false, callback);
|
||||
},
|
||||
updateTableData() {
|
||||
if (this.editingTable === false) return;
|
||||
this.updatingTableData = true;
|
||||
|
||||
this.$nextTick(() => {
|
||||
this.update(() => {
|
||||
this.editingCell = false;
|
||||
});
|
||||
});
|
||||
},
|
||||
loadTableData() {
|
||||
const container = this.$refs.hotTable;
|
||||
const data = JSON.parse(this.element.attributes.orderable.contents);
|
||||
|
@ -294,12 +304,19 @@ export default {
|
|||
}
|
||||
},
|
||||
afterChange: () => {
|
||||
if (this.editingTable === false) return;
|
||||
this.updatingTableData = true;
|
||||
|
||||
this.$nextTick(() => {
|
||||
this.update(() => this.editingCell = false);
|
||||
});
|
||||
this.updateTableData();
|
||||
},
|
||||
afterRemoveRow: () => {
|
||||
this.updateTableData();
|
||||
},
|
||||
afterRemoveCol: () => {
|
||||
this.updateTableData();
|
||||
},
|
||||
afterCreateCol: () => {
|
||||
this.updateTableData();
|
||||
},
|
||||
afterCreateRow: () => {
|
||||
this.updateTableData();
|
||||
},
|
||||
beforeKeyDown: (e) => {
|
||||
if (e.keyCode === 27) { // esc
|
||||
|
|
131
app/javascript/vue/shared/drag_and_drop_upload.vue
Normal file
131
app/javascript/vue/shared/drag_and_drop_upload.vue
Normal file
|
@ -0,0 +1,131 @@
|
|||
<template>
|
||||
<div
|
||||
ref="dragAndDropUpload"
|
||||
@drop.prevent="dropFile"
|
||||
@dragenter.prevent="dragEnter($event)"
|
||||
@dragleave.prevent="dragLeave($event)"
|
||||
@dragover.prevent
|
||||
@click="handleImportClick"
|
||||
class="flex h-full w-full p-6 rounded border border-sn-light-grey bg-sn-super-light-blue cursor-pointer"
|
||||
:data-e2e="`e2e-CO-${dataE2e}`"
|
||||
>
|
||||
<div id="centered-content" class="flex flex-col gap-4 items-center h-fit w-fit m-auto">
|
||||
<!-- icon -->
|
||||
<i class="sn-icon sn-icon-import text-sn-dark-grey"></i>
|
||||
|
||||
<!-- text section -->
|
||||
<div class="flex flex-col gap-1">
|
||||
<div class="text-sn-dark-grey">
|
||||
<span class="text-sn-science-blue hover:cursor-pointer" >
|
||||
{{ i18n.t('repositories.import_records.dragAndDropUpload.importText.firstPart') }}
|
||||
</span> {{ i18n.t('repositories.import_records.dragAndDropUpload.importText.secondPart') }}
|
||||
</div>
|
||||
<div class="text-sn-grey">
|
||||
{{ supportingText }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- hidden input for importing via 'Import' click -->
|
||||
<input type="file" ref="fileInput" style="display: none" @change="handleFileSelect">
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
export default {
|
||||
name: 'DragAndDropUpload',
|
||||
props: {
|
||||
supportingText: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
supportedFormats: {
|
||||
type: Array,
|
||||
required: true,
|
||||
default: () => []
|
||||
},
|
||||
dataE2e: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
},
|
||||
emits: ['file:dropped', 'file:error'],
|
||||
data() {
|
||||
return {
|
||||
draggingFile: false,
|
||||
uploading: false
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
validateFile(file) {
|
||||
// check if it's a single file
|
||||
if (file.length > 1) {
|
||||
const error = I18n.t('repositories.import_records.dragAndDropUpload.notSingleFileError');
|
||||
this.$emit('file:error', error);
|
||||
return false;
|
||||
}
|
||||
|
||||
// check if it's a correct file type
|
||||
const fileExtension = file.name.split('.')[1];
|
||||
if (!this.supportedFormats.includes(fileExtension)) {
|
||||
const error = I18n.t('repositories.import_records.dragAndDropUpload.wrongFileTypeError');
|
||||
this.$emit('file:error', error);
|
||||
return false;
|
||||
}
|
||||
|
||||
// check if file is not empty
|
||||
if (!file.size > 0) {
|
||||
const error = I18n.t('repositories.import_records.dragAndDropUpload.emptyFileError');
|
||||
this.$emit('file:error', error);
|
||||
return false;
|
||||
}
|
||||
|
||||
// check if it's conforming to size limit
|
||||
if (file.size > GLOBAL_CONSTANTS.FILE_MAX_SIZE_MB * 1024 * 1024) {
|
||||
const error = `${I18n.t('repositories.import_records.dragAndDropUpload.fileTooLargeError')} ${GLOBAL_CONSTANTS.FILE_MAX_SIZE_MB}`;
|
||||
this.$emit('file:error', error);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
dragEnter(e) {
|
||||
// Detect if dragged element is a file
|
||||
// https://stackoverflow.com/a/8494918
|
||||
const dt = e.dataTransfer;
|
||||
if (dt.types && (dt.types.indexOf ? dt.types.indexOf('Files') !== -1 : dt.types.contains('Files'))) {
|
||||
this.draggingFile = true;
|
||||
}
|
||||
},
|
||||
dragLeave() {
|
||||
this.draggingFile = false;
|
||||
},
|
||||
dropFile(e) {
|
||||
if (e.dataTransfer && e.dataTransfer.files.length) {
|
||||
this.draggingFile = false;
|
||||
this.uploading = true;
|
||||
const droppedFile = e.dataTransfer.files[0];
|
||||
|
||||
const fileIsValid = this.validateFile(droppedFile);
|
||||
|
||||
// successful drop
|
||||
if (fileIsValid) {
|
||||
this.$emit('file:dropped', droppedFile);
|
||||
this.$emit('file:error:clear');
|
||||
}
|
||||
}
|
||||
},
|
||||
handleImportClick() {
|
||||
this.$refs.fileInput.click();
|
||||
},
|
||||
handleFileSelect(event) {
|
||||
const file = event.target.files[0];
|
||||
const fileIsValid = this.validateFile(file);
|
||||
|
||||
if (fileIsValid) {
|
||||
this.$emit('file:dropped', file);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
39
app/javascript/vue/shared/info_component.vue
Normal file
39
app/javascript/vue/shared/info_component.vue
Normal file
|
@ -0,0 +1,39 @@
|
|||
<template>
|
||||
<div class="!w-[300px] rounded bg-sn-super-light-grey gap-4 p-6 flex flex-col h-full">
|
||||
<div id="info-component-header">
|
||||
<h3 class="modal-title text-sn-dark-grey">{{ infoParams.title }}</h3>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-flow-row h-fit" v-for="(element, _index) in infoParams.elements" :key="element.id">
|
||||
<a v-if="element.linkTo" :href="element.linkTo" target="_blank" class="flex flex-row gap-3 w-fit text-sn-blue hover:no-underline hover:text-sn-blue-hover">
|
||||
<button class="btn btn-secondary btn-sm icon-btn hover:!border-sn-light-grey">
|
||||
<i :class="`sn-icon ${element.icon}`" class="h-fit size-9"></i>
|
||||
</button>
|
||||
<div class="flex flex-col gap-2 w-fit">
|
||||
<div class="text-sn-blue font-bold hover:text-sn-blue-hover my-auto">{{ element.label }}</div>
|
||||
</div>
|
||||
</a>
|
||||
<div v-else class="flex flex-row gap-3">
|
||||
<button class="btn btn-secondary btn-sm icon-btn hover:!border-sn-light-grey hover:cursor-auto">
|
||||
<i :class="`sn-icon ${element.icon}`" class="h-fit text-sn-dark-grey size-9"></i>
|
||||
</button>
|
||||
<div class="flex flex-col gap-2 w-52">
|
||||
<div class="text-sn-dark-grey font-bold">{{ element.label }}</div>
|
||||
<div class="text-sn-dark-grey">{{ element.subtext }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'InfoComponent',
|
||||
props: {
|
||||
infoParams: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
80
app/javascript/vue/shared/info_modal.vue
Normal file
80
app/javascript/vue/shared/info_modal.vue
Normal file
|
@ -0,0 +1,80 @@
|
|||
<template>
|
||||
<div ref="modal" class="modal" tabindex="-1" role="dialog" data-backdrop="static" data-keyboard="false">
|
||||
<div class="modal-dialog" role="document" :class="[{'!w-[900px]' : showingInfo}, {'!w-fit' : !showingInfo}]">
|
||||
<div class="modal-content !p-0 bg-sn-white w-full h-full flex" :class="[{'flex-row': showingInfo}, {'flex-col': !showingInfo}]">
|
||||
<div id="body-container" class="flex flex-row w-full h-full">
|
||||
<!-- info -->
|
||||
<div id="info-part" ref="infoPartRef">
|
||||
<InfoComponent
|
||||
v-if="showingInfo"
|
||||
:infoParams="infoParams"
|
||||
/>
|
||||
</div>
|
||||
<!-- content -->
|
||||
<div id="content-part" class="flex flex-col w-full p-6 gap-6">
|
||||
<!-- header -->
|
||||
<div id="info-modal-header" class="flex flex-col h-fit w-full gap-2">
|
||||
<div id="title-part" class="flex flex-row h-fit w-full justify-between">
|
||||
<div id="title-with-help" class="flex flex-row gap-3">
|
||||
<h3 class="modal-title text-sn-dark-grey">{{ title }}</h3>
|
||||
<button v-if="helpText" class="btn btn-light btn-sm" @click="showingInfo = !showingInfo">
|
||||
<i class="sn-icon sn-icon-help-s"></i>
|
||||
{{ helpText }}
|
||||
</button>
|
||||
</div>
|
||||
<button id="close-btn" type="button" class="close my-auto" data-dismiss="modal" aria-label="Close">
|
||||
<i class="sn-icon sn-icon-close"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div id="subtitle-part" class="text-sn-dark-grey">
|
||||
{{ subtitle }}
|
||||
</div>
|
||||
</div>
|
||||
<!-- main content -->
|
||||
<div id="info-modal-main-content" class="h-full">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import modalMixin from './modal_mixin';
|
||||
import InfoComponent from './info_component.vue';
|
||||
|
||||
export default {
|
||||
name: 'InfoModal',
|
||||
props: {
|
||||
title: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
subtitle: {
|
||||
type: String,
|
||||
required: false
|
||||
},
|
||||
helpText: {
|
||||
type: String,
|
||||
required: false
|
||||
},
|
||||
infoParams: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
startHidden: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
mixins: [modalMixin],
|
||||
components: { InfoComponent },
|
||||
data() {
|
||||
return {
|
||||
showingInfo: false
|
||||
};
|
||||
}
|
||||
};
|
||||
</script>
|
|
@ -38,7 +38,8 @@ export default {
|
|||
error: { type: String, required: false },
|
||||
min: { type: [String, Number] },
|
||||
max: { type: [String, Number] },
|
||||
blockInvalidInput: { type: Boolean, default: true }
|
||||
blockInvalidInput: { type: Boolean, default: true },
|
||||
negativeNumbersEnabled: {type: Boolean, required: false }
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
@ -56,6 +57,13 @@ export default {
|
|||
},
|
||||
computed: {
|
||||
pattern() {
|
||||
if (this.negativeNumbersEnabled) {
|
||||
if (this.type === 'number' && this.decimals) {
|
||||
return `-?[0-9]+([\\.]-?[0-9]{0,${this.decimals}})?`;
|
||||
} else if (this.type === 'number') {
|
||||
return '-?[0-9]+';
|
||||
}
|
||||
}
|
||||
if (this.type === 'number' && this.decimals) {
|
||||
return `[0-9]+([\\.][0-9]{0,${this.decimals}})?`;
|
||||
} else if (this.type === 'number') {
|
||||
|
|
11
app/javascript/vue/shared/loading.vue
Normal file
11
app/javascript/vue/shared/loading.vue
Normal file
|
@ -0,0 +1,11 @@
|
|||
<template>
|
||||
<div class="flex absolute top-0 items-center justify-center w-full flex-grow h-full z-[3000]">
|
||||
<div class="absolute top-0 left-0 w-full h-full bg-black opacity-10"></div>
|
||||
<img src="/images/medium/loading.svg" alt="Loading" class="" />
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
name: 'Loading'
|
||||
};
|
||||
</script>
|
|
@ -9,7 +9,7 @@
|
|||
<template v-if="isOpen">
|
||||
<teleport to="body">
|
||||
<div ref="flyout"
|
||||
class="fixed z-[3000] sn-menu-dropdown bg-sn-white inline-block rounded p-2.5 sn-shadow-menu-sm flex flex-col gap-[1px]"
|
||||
class="fixed z-[3000] sn-menu-dropdown bg-sn-white rounded p-2.5 sn-shadow-menu-sm flex flex-col gap-[1px]"
|
||||
:class="{
|
||||
'right-0': position === 'right',
|
||||
'left-0': position === 'left',
|
||||
|
@ -95,7 +95,7 @@ export default {
|
|||
mixins: [FixedFlyoutMixin],
|
||||
watch: {
|
||||
isOpen(newValue) {
|
||||
this.$emit('menu-visibility-changed', newValue);
|
||||
this.$emit('menu-toggle', newValue);
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
export default {
|
||||
mounted() {
|
||||
$(this.$refs.modal).modal('show');
|
||||
if (!this.startHidden) {
|
||||
$(this.$refs.modal).modal('show');
|
||||
}
|
||||
|
||||
this.$refs.input?.focus();
|
||||
$(this.$refs.modal).on('hidden.bs.modal', () => {
|
||||
this.$emit('close');
|
||||
|
@ -13,6 +16,10 @@ export default {
|
|||
close() {
|
||||
this.$emit('close');
|
||||
$(this.$refs.modal).modal('hide');
|
||||
},
|
||||
open() {
|
||||
this.$emit('open');
|
||||
$(this.$refs.modal).modal('show');
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
@ -28,8 +28,9 @@
|
|||
v-else
|
||||
v-model="query"
|
||||
:placeholder="placeholderRender"
|
||||
@keyup="fetchOptions"
|
||||
@change.stop
|
||||
class="w-full border-0 outline-none pl-0 placeholder:text-sn-grey" />
|
||||
class="w-full bg-transparent border-0 outline-none pl-0 placeholder:text-sn-grey" />
|
||||
</template>
|
||||
<div v-else class="flex items-center gap-1 flex-wrap">
|
||||
<div v-for="tag in tags" class="px-2 py-1 rounded-sm bg-sn-super-light-grey grid grid-cols-[auto_1fr] items-center gap-1">
|
||||
|
@ -51,8 +52,8 @@
|
|||
</div>
|
||||
</div>
|
||||
<i v-if="canClear" @click="clear" class="sn-icon ml-auto sn-icon-close"></i>
|
||||
<i v-else class="sn-icon ml-auto"
|
||||
:class="{ 'sn-icon-down': !isOpen, 'sn-icon-up': isOpen, 'text-sn-grey': disabled}"></i>
|
||||
<i v-else class="sn-icon ml-auto" @click="handleClickArrow"
|
||||
:class="{ 'sn-icon-down pointer-events-none': !isOpen, 'sn-icon-up': isOpen, 'text-sn-grey': disabled}"></i>
|
||||
</div>
|
||||
<template v-if="isOpen">
|
||||
<teleport to="body">
|
||||
|
@ -141,7 +142,8 @@ export default {
|
|||
selectAllState: 'unchecked',
|
||||
query: '',
|
||||
fixedWidth: true,
|
||||
focusedOption: null
|
||||
focusedOption: null,
|
||||
skipQueryCallback: false
|
||||
};
|
||||
},
|
||||
mixins: [FixedFlyoutMixin],
|
||||
|
@ -262,7 +264,8 @@ export default {
|
|||
value(newValue) {
|
||||
this.newValue = newValue;
|
||||
},
|
||||
isOpen() {
|
||||
isOpen(newVal) {
|
||||
this.$emit('isOpen', newVal);
|
||||
if (this.isOpen) {
|
||||
this.$nextTick(() => {
|
||||
this.setPosition();
|
||||
|
@ -270,8 +273,13 @@ export default {
|
|||
});
|
||||
}
|
||||
},
|
||||
query() {
|
||||
if (this.optionsUrl) this.fetchOptions();
|
||||
urlParams: {
|
||||
handler(oldVal, newVal) {
|
||||
if (!this.compareObjects(oldVal, newVal)) {
|
||||
this.fetchOptions();
|
||||
}
|
||||
},
|
||||
deep: true
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
|
@ -296,7 +304,7 @@ export default {
|
|||
clear() {
|
||||
this.newValue = this.multiple ? [] : null;
|
||||
this.query = '';
|
||||
this.$emit('change', this.newValue);
|
||||
this.$emit('change', this.newValue, '');
|
||||
},
|
||||
close(e) {
|
||||
if (e && e.target.closest('.sn-select-dropdown')) return;
|
||||
|
@ -306,7 +314,7 @@ export default {
|
|||
this.$nextTick(() => {
|
||||
this.isOpen = false;
|
||||
if (this.valueChanged) {
|
||||
this.$emit('change', this.newValue);
|
||||
this.$emit('change', this.newValue, this.getLabels(this.newValue));
|
||||
}
|
||||
this.query = '';
|
||||
});
|
||||
|
@ -327,7 +335,7 @@ export default {
|
|||
},
|
||||
removeTag(value) {
|
||||
this.newValue = this.newValue.filter((v) => v !== value);
|
||||
this.$emit('change', this.newValue);
|
||||
this.$emit('change', this.newValue, this.getLabels(this.newValue));
|
||||
},
|
||||
selectAll() {
|
||||
if (this.selectAllState === 'checked') {
|
||||
|
@ -335,7 +343,14 @@ export default {
|
|||
} else {
|
||||
this.newValue = this.rawOptions.map((option) => option[0]);
|
||||
}
|
||||
this.$emit('change', this.newValue);
|
||||
this.$emit('change', this.newValue, this.getLabels(this.newValue));
|
||||
},
|
||||
getLabels(value) {
|
||||
if (typeof value === 'string' || typeof value === 'number') {
|
||||
const option = this.rawOptions.find((i) => i[0] === value);
|
||||
return option[1];
|
||||
}
|
||||
return this.rawOptions.filter((option) => value.includes(option[0])).map((option) => option[1]);
|
||||
},
|
||||
fetchOptions() {
|
||||
if (this.optionsUrl) {
|
||||
|
@ -374,6 +389,17 @@ export default {
|
|||
if (this.$refs.options) {
|
||||
this.$refs.options[this.focusedOption]?.scrollIntoView({ block: 'nearest' });
|
||||
}
|
||||
},
|
||||
compareObjects(o1, o2) {
|
||||
const normalizedObj1 = Object.fromEntries(Object.entries(o1).sort(([k1], [k2]) => k1.localeCompare(k2)));
|
||||
const normalizedObj2 = Object.fromEntries(Object.entries(o2).sort(([k1], [k2]) => k1.localeCompare(k2)));
|
||||
return JSON.stringify(normalizedObj1) === JSON.stringify(normalizedObj2);
|
||||
},
|
||||
handleClickArrow(e) {
|
||||
if (this.isOpen) {
|
||||
e.stopPropagation();
|
||||
this.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
20
app/jobs/cleanup_user_settings_job.rb
Normal file
20
app/jobs/cleanup_user_settings_job.rb
Normal file
|
@ -0,0 +1,20 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class CleanupUserSettingsJob < ApplicationJob
|
||||
queue_as :default
|
||||
|
||||
def perform(record_type, record_id)
|
||||
raise ArgumentError, 'Invalid record_type' unless %w(task_step_states results_order).include?(record_type)
|
||||
|
||||
sanitized_record_id = record_id.to_i.to_s
|
||||
raise ArgumentError, 'Invalid record_id' unless sanitized_record_id == record_id.to_s
|
||||
|
||||
sql = <<-SQL.squish
|
||||
UPDATE users
|
||||
SET settings = (settings#>>'{}')::jsonb #- '{#{record_type},#{sanitized_record_id}}'
|
||||
WHERE (settings#>>'{}')::jsonb->'#{record_type}' ? '#{sanitized_record_id}';
|
||||
SQL
|
||||
|
||||
ActiveRecord::Base.connection.execute(sql)
|
||||
end
|
||||
end
|
|
@ -4,10 +4,11 @@ class RepositoriesExportJob < ApplicationJob
|
|||
include StringUtility
|
||||
include FailedDeliveryNotifiableJob
|
||||
|
||||
def perform(repository_ids, user_id:, team_id:)
|
||||
def perform(file_type, repository_ids, user_id:, team_id:)
|
||||
@user = User.find(user_id)
|
||||
@team = Team.find(team_id)
|
||||
@repositories = Repository.viewable_by_user(@user, @team).where(id: repository_ids).order(:id)
|
||||
@file_type = file_type.to_sym
|
||||
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
|
||||
|
||||
|
@ -32,11 +33,11 @@ class RepositoriesExportJob < ApplicationJob
|
|||
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)
|
||||
save_repository_as_file(team_path, repository, idx)
|
||||
end
|
||||
end
|
||||
|
||||
def save_repository_to_csv(path, repository, idx)
|
||||
def save_repository_as_file(path, repository, idx)
|
||||
repository_name = "#{to_filesystem_name(repository.name)} (#{idx})"
|
||||
|
||||
# Attachments dir
|
||||
|
@ -44,12 +45,12 @@ class RepositoriesExportJob < ApplicationJob
|
|||
attachments_path = "#{path}/#{relative_attachments_path}"
|
||||
FileUtils.mkdir_p(attachments_path)
|
||||
|
||||
# CSV file
|
||||
csv_file = FileUtils.touch("#{path}/#{repository_name}.csv").first
|
||||
# File creation
|
||||
repository_items_file_name = FileUtils.touch("#{path}/#{repository_name}.#{@file_type}").first
|
||||
|
||||
# Define headers and columns IDs
|
||||
col_ids = [-3, -4, -5, -6, -7, -8]
|
||||
col_ids << -9 if Repository.repository_row_connections_enabled?
|
||||
col_ids = [-3, -4, -5, -6, -7, -8, -9, -10]
|
||||
col_ids << -11 if Repository.repository_row_connections_enabled?
|
||||
col_ids += repository.repository_columns.map(&:id)
|
||||
|
||||
# Define callback function for file name
|
||||
|
@ -66,9 +67,12 @@ class RepositoriesExportJob < ApplicationJob
|
|||
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.encode('UTF-8', invalid: :replace, undef: :replace))
|
||||
# Generate CSV / XLSX
|
||||
service = RepositoryExportService
|
||||
.new(@file_type, repository.repository_rows, col_ids, @user, repository, handle_name_func)
|
||||
exported_data = service.export!
|
||||
|
||||
File.binwrite(repository_items_file_name, exported_data)
|
||||
|
||||
# Save all attachments (it doesn't work directly in callback function
|
||||
assets.each do |asset, asset_path|
|
||||
|
|
|
@ -29,13 +29,15 @@ class RepositoryZipExportJob < ZipExportJob
|
|||
.index_by(&:id)
|
||||
rows = ordered_row_ids.collect { |id| id_row_map[id.to_i] }
|
||||
end
|
||||
data = RepositoryZipExport.to_csv(rows,
|
||||
params[:header_ids].map(&:to_i),
|
||||
@user,
|
||||
repository,
|
||||
nil,
|
||||
params[:my_module_id].present?)
|
||||
File.binwrite("#{dir}/export.csv", data.encode('UTF-8', invalid: :replace, undef: :replace))
|
||||
service = RepositoryExportService
|
||||
.new(@file_type,
|
||||
rows,
|
||||
params[:header_ids].map(&:to_i),
|
||||
@user,
|
||||
repository,
|
||||
in_module: params[:my_module_id].present?)
|
||||
exported_data = service.export!
|
||||
File.binwrite("#{dir}/export.#{@file_type}", exported_data)
|
||||
end
|
||||
|
||||
def failed_notification_title
|
||||
|
|
|
@ -276,7 +276,7 @@ class TeamZipExportJob < ZipExportJob
|
|||
end
|
||||
|
||||
# Generate CSV
|
||||
csv_data = RepositoryZipExport.to_csv(repo.repository_rows, col_ids, @user, repo, handle_name_func)
|
||||
csv_data = RepositoryCsvExport.to_csv(repo.repository_rows, col_ids, @user, repo, handle_name_func, false)
|
||||
File.binwrite(csv_file_path, csv_data.encode('UTF-8', invalid: :replace, undef: :replace))
|
||||
|
||||
# Save all attachments (it doesn't work directly in callback function
|
||||
|
|
|
@ -3,8 +3,9 @@
|
|||
class ZipExportJob < ApplicationJob
|
||||
include FailedDeliveryNotifiableJob
|
||||
|
||||
def perform(user_id:, params: {})
|
||||
def perform(user_id:, params: {}, file_type: :csv)
|
||||
@user = User.find(user_id)
|
||||
@file_type = file_type.to_sym
|
||||
I18n.backend.date_format = @user.settings[:date_format] || Constants::DEFAULT_DATE_FORMAT
|
||||
zip_input_dir = FileUtils.mkdir_p(Rails.root.join("tmp/temp_zip_#{Time.now.to_i}").to_s).first
|
||||
zip_dir = FileUtils.mkdir_p(Rails.root.join('tmp/zip-ready').to_s).first
|
||||
|
|
|
@ -35,7 +35,7 @@ class Asset < ApplicationRecord
|
|||
has_one :result_asset, inverse_of: :asset, dependent: :destroy
|
||||
has_one :result, through: :result_asset, touch: true
|
||||
has_one :repository_asset_value, inverse_of: :asset, dependent: :destroy
|
||||
has_one :repository_cell, through: :repository_asset_value
|
||||
has_one :repository_cell, through: :repository_asset_value, touch: true
|
||||
has_many :report_elements, inverse_of: :asset, dependent: :destroy
|
||||
has_one :asset_text_datum, inverse_of: :asset, dependent: :destroy
|
||||
has_many :asset_sync_tokens, dependent: :destroy
|
||||
|
@ -245,7 +245,7 @@ class Asset < ApplicationRecord
|
|||
|
||||
def can_perform_action(action)
|
||||
if ENV['WOPI_ENABLED'] == 'true'
|
||||
file_ext = file_name.split('.').last
|
||||
file_ext = file_name.split('.').last&.downcase
|
||||
|
||||
if file_ext == 'wopitest' &&
|
||||
(!ENV['WOPI_TEST_ENABLED'] || ENV['WOPI_TEST_ENABLED'] == 'false')
|
||||
|
@ -297,7 +297,7 @@ class Asset < ApplicationRecord
|
|||
end
|
||||
|
||||
def favicon_url(action)
|
||||
file_ext = file_name.split('.').last
|
||||
file_ext = file_name.split('.').last&.downcase
|
||||
action = get_action(file_ext, action)
|
||||
action[:icon] if action[:icon]
|
||||
end
|
||||
|
@ -385,6 +385,10 @@ class Asset < ApplicationRecord
|
|||
new_image_filename = "#{new_name}.png"
|
||||
preview_image.blob.update!(filename: new_image_filename)
|
||||
end
|
||||
|
||||
# rubocop:disable Rails/SkipsModelValidations
|
||||
touch
|
||||
# rubocop:enable Rails/SkipsModelValidations
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -8,6 +8,21 @@ module SettingsModel
|
|||
after_initialize :init_default_settings, if: :new_record?
|
||||
end
|
||||
|
||||
def update_simple_setting(key:, value:)
|
||||
raise ArgumentError, 'Unauthorized setting key' unless Extends::WHITELISTED_USER_SETTINGS.include?(key.to_s)
|
||||
|
||||
settings[key] = value
|
||||
save
|
||||
end
|
||||
|
||||
def update_nested_setting(key:, id:, value:)
|
||||
raise ArgumentError, 'Unauthorized setting key' unless Extends::WHITELISTED_USER_SETTINGS.include?(key.to_s)
|
||||
|
||||
settings[key] ||= {}
|
||||
settings[key][id.to_s] = value
|
||||
save
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
def init_default_settings
|
||||
|
|
|
@ -29,17 +29,15 @@ module TinyMceImages
|
|||
)[0]
|
||||
next unless tm_asset_to_update
|
||||
|
||||
unless export_all
|
||||
tm_asset = tm_asset.image.representation(resize_to_limit: Constants::LARGE_PIC_FORMAT).processed
|
||||
tm_asset = tm_asset.image.representation(resize_to_limit: Constants::LARGE_PIC_FORMAT).processed
|
||||
|
||||
width_attr = tm_asset_to_update.attributes['width']
|
||||
height_attr = tm_asset_to_update.attributes['height']
|
||||
width_attr = tm_asset_to_update.attributes['width']
|
||||
height_attr = tm_asset_to_update.attributes['height']
|
||||
|
||||
if width_attr && height_attr && (width_attr.value.to_i >= Constants::LARGE_PIC_FORMAT[0] ||
|
||||
height_attr.value.to_i >= Constants::LARGE_PIC_FORMAT[1])
|
||||
width_attr.value = tm_asset.image.blob.metadata['width'].to_s
|
||||
height_attr.value = tm_asset.image.blob.metadata['height'].to_s
|
||||
end
|
||||
if width_attr && height_attr && (width_attr.value.to_i >= Constants::LARGE_PIC_FORMAT[0] ||
|
||||
height_attr.value.to_i >= Constants::LARGE_PIC_FORMAT[1])
|
||||
width_attr.value = tm_asset.image.blob.metadata['width'].to_s
|
||||
height_attr.value = tm_asset.image.blob.metadata['height'].to_s
|
||||
end
|
||||
|
||||
tm_asset_to_update.attributes['src'].value = convert_to_base64(tm_asset.image)
|
||||
|
|
|
@ -20,8 +20,11 @@ class LinkedRepository < Repository
|
|||
'assigned',
|
||||
'repository_rows.id',
|
||||
'repository_rows.name',
|
||||
'relationships',
|
||||
'repository_rows.created_at',
|
||||
'users.full_name',
|
||||
'repository_rows.updated_at',
|
||||
'last_modified_bies_repository_rows.full_name',
|
||||
'repository_rows.archived_on',
|
||||
'archived_bies_repository_rows.full_name',
|
||||
'repository_rows.external_id'
|
||||
|
|
|
@ -56,6 +56,7 @@ class MyModule < ApplicationRecord
|
|||
belongs_to :changing_from_my_module_status, optional: true, class_name: 'MyModuleStatus'
|
||||
delegate :my_module_status_flow, to: :my_module_status, allow_nil: true
|
||||
has_many :results, inverse_of: :my_module, dependent: :destroy
|
||||
has_many :results_include_discarded, -> { with_discarded }, class_name: 'Result', inverse_of: :my_module
|
||||
has_many :my_module_tags, inverse_of: :my_module, dependent: :destroy
|
||||
has_many :tags, through: :my_module_tags, dependent: :destroy
|
||||
has_many :task_comments, foreign_key: :associated_id, dependent: :destroy
|
||||
|
|
|
@ -71,6 +71,7 @@ class MyModuleRepositoryRow < ApplicationRecord
|
|||
team_id: my_module.experiment.project.team.id
|
||||
}
|
||||
)
|
||||
stock_value.last_modified_by_id = last_modified_by_id
|
||||
stock_value.save!
|
||||
save!
|
||||
end
|
||||
|
|
|
@ -102,6 +102,8 @@ class Repository < RepositoryBase
|
|||
'relationships',
|
||||
'repository_rows.created_at',
|
||||
'users.full_name',
|
||||
'repository_rows.updated_at',
|
||||
'last_modified_bies_repository_rows.full_name',
|
||||
'repository_rows.archived_on',
|
||||
'archived_bies_repository_rows.full_name'
|
||||
]
|
||||
|
@ -160,6 +162,7 @@ class Repository < RepositoryBase
|
|||
def importable_repository_fields
|
||||
fields = {}
|
||||
# First and foremost add record name
|
||||
fields['0'] = I18n.t('repositories.id_column')
|
||||
fields['-1'] = I18n.t('repositories.default_column')
|
||||
# Add all other custom columns
|
||||
repository_columns.order(:created_at).each do |rc|
|
||||
|
@ -201,11 +204,6 @@ class Repository < RepositoryBase
|
|||
new_repo
|
||||
end
|
||||
|
||||
def import_records(sheet, mappings, user)
|
||||
importer = RepositoryImportParser::Importer.new(sheet, mappings, user, self)
|
||||
importer.run
|
||||
end
|
||||
|
||||
def assigned_rows(my_module)
|
||||
repository_rows.joins(:my_module_repository_rows).where(my_module_repository_rows: { my_module_id: my_module.id })
|
||||
end
|
||||
|
|
|
@ -12,7 +12,7 @@ class RepositoryAssetValue < ApplicationRecord
|
|||
belongs_to :asset,
|
||||
inverse_of: :repository_asset_value,
|
||||
dependent: :destroy
|
||||
has_one :repository_cell, as: :value, dependent: :destroy, inverse_of: :value
|
||||
has_one :repository_cell, as: :value, dependent: :destroy, inverse_of: :value, touch: true
|
||||
accepts_nested_attributes_for :repository_cell
|
||||
|
||||
validates :asset, :repository_cell, presence: true
|
||||
|
|
|
@ -3,9 +3,9 @@
|
|||
class RepositoryCell < ApplicationRecord
|
||||
include ReminderRepositoryCellJoinable
|
||||
|
||||
attr_accessor :importing
|
||||
attr_accessor :importing, :to_destroy
|
||||
|
||||
belongs_to :repository_row
|
||||
belongs_to :repository_row, touch: true
|
||||
belongs_to :repository_column
|
||||
belongs_to :value, polymorphic: true, inverse_of: :repository_cell, dependent: :destroy
|
||||
|
||||
|
@ -45,10 +45,16 @@ class RepositoryCell < ApplicationRecord
|
|||
uniqueness: { scope: :repository_column },
|
||||
unless: :importing
|
||||
|
||||
after_touch :update_repository_row_last_modified_by
|
||||
|
||||
scope :with_active_reminder, lambda { |user|
|
||||
reminder_repository_cells_scope(self, user)
|
||||
}
|
||||
|
||||
def update_repository_row_last_modified_by
|
||||
repository_row.update!(last_modified_by_id: value.last_modified_by_id)
|
||||
end
|
||||
|
||||
def self.create_with_value!(row, column, data, user)
|
||||
cell = new(repository_row: row, repository_column: column)
|
||||
cell.transaction do
|
||||
|
|
|
@ -5,4 +5,19 @@ class RepositoryChecklistItemsValue < ApplicationRecord
|
|||
belongs_to :repository_checklist_value, inverse_of: :repository_checklist_items_values
|
||||
|
||||
validates :repository_checklist_item, :repository_checklist_value, presence: true
|
||||
|
||||
after_commit :touch_repository_checklist_value
|
||||
|
||||
private
|
||||
|
||||
# rubocop:disable Rails/SkipsModelValidations
|
||||
def touch_repository_checklist_value
|
||||
# check if value was deleted, if so, touch repositroy_row directly
|
||||
if RepositoryChecklistValue.exists?(repository_checklist_value.id)
|
||||
repository_checklist_value.touch
|
||||
elsif RepositoryRow.exists?(repository_checklist_value.repository_cell.repository_row.id)
|
||||
repository_checklist_value.repository_cell.repository_row.touch
|
||||
end
|
||||
end
|
||||
# rubocop:enable Rails/SkipsModelValidations
|
||||
end
|
||||
|
|
|
@ -1,11 +1,13 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class RepositoryChecklistValue < ApplicationRecord
|
||||
attribute :current_repository_checklist_items
|
||||
|
||||
belongs_to :created_by, foreign_key: 'created_by_id', class_name: 'User',
|
||||
inverse_of: :created_repository_checklist_values
|
||||
belongs_to :last_modified_by, foreign_key: 'last_modified_by_id', class_name: 'User',
|
||||
inverse_of: :modified_repository_checklist_values
|
||||
has_one :repository_cell, as: :value, dependent: :destroy, inverse_of: :value
|
||||
has_one :repository_cell, as: :value, dependent: :destroy, inverse_of: :value, touch: true
|
||||
has_many :repository_checklist_items_values, dependent: :destroy
|
||||
has_many :repository_checklist_items, -> { order('data ASC') },
|
||||
through: :repository_checklist_items_values,
|
||||
|
@ -20,7 +22,8 @@ class RepositoryChecklistValue < ApplicationRecord
|
|||
EXTRA_PRELOAD_INCLUDE = :repository_checklist_items
|
||||
|
||||
def formatted(separator: ' | ')
|
||||
repository_checklist_items.pluck(:data).join(separator)
|
||||
checklist_items = current_repository_checklist_items || repository_checklist_items
|
||||
checklist_items.map(&:data).join(separator)
|
||||
end
|
||||
|
||||
def export_formatted
|
||||
|
@ -70,17 +73,25 @@ class RepositoryChecklistValue < ApplicationRecord
|
|||
end
|
||||
end
|
||||
|
||||
def update_data!(new_data, user)
|
||||
def update_data!(new_data, user, preview: false)
|
||||
item_ids = new_data.is_a?(String) ? JSON.parse(new_data) : new_data
|
||||
|
||||
return destroy! if item_ids.blank?
|
||||
|
||||
# update!(repository_checklist_items: repository_cell.repository_column.repository_checklist_items.where(id: item_ids), last_modified_by: user)
|
||||
|
||||
self.repository_checklist_items = repository_cell.repository_column
|
||||
.repository_checklist_items
|
||||
.where(id: item_ids)
|
||||
self.last_modified_by = user
|
||||
save!
|
||||
|
||||
if preview
|
||||
self.current_repository_checklist_items = repository_checklist_items
|
||||
clear_current_repository_checklist_items_change
|
||||
self.current_repository_checklist_items =
|
||||
repository_cell.repository_column.repository_checklist_items.where(id: item_ids)
|
||||
validate
|
||||
else
|
||||
self.repository_checklist_items = repository_cell.repository_column
|
||||
.repository_checklist_items
|
||||
.where(id: item_ids)
|
||||
save!
|
||||
end
|
||||
end
|
||||
|
||||
def snapshot!(cell_snapshot)
|
||||
|
|
|
@ -7,7 +7,7 @@ class RepositoryDateTimeRangeValueBase < ApplicationRecord
|
|||
inverse_of: :created_repository_date_time_values
|
||||
belongs_to :last_modified_by, foreign_key: :last_modified_by_id, class_name: 'User', optional: true,
|
||||
inverse_of: :modified_repository_date_time_values
|
||||
has_one :repository_cell, as: :value, dependent: :destroy, inverse_of: :value
|
||||
has_one :repository_cell, as: :value, dependent: :destroy, inverse_of: :value, touch: true
|
||||
accepts_nested_attributes_for :repository_cell
|
||||
|
||||
validates :repository_cell, :start_time, :end_time, :type, presence: true
|
||||
|
|
|
@ -7,7 +7,7 @@ class RepositoryDateTimeValueBase < ApplicationRecord
|
|||
inverse_of: :created_repository_date_time_values
|
||||
belongs_to :last_modified_by, foreign_key: :last_modified_by_id, class_name: 'User', optional: true,
|
||||
inverse_of: :modified_repository_date_time_values
|
||||
has_one :repository_cell, as: :value, dependent: :destroy
|
||||
has_one :repository_cell, as: :value, dependent: :destroy, touch: true
|
||||
accepts_nested_attributes_for :repository_cell
|
||||
before_save :reset_notification_sent, if: -> { data_changed? }
|
||||
|
||||
|
@ -19,10 +19,15 @@ class RepositoryDateTimeValueBase < ApplicationRecord
|
|||
I18n.l(data, format: format)
|
||||
end
|
||||
|
||||
def update_data!(new_data, user)
|
||||
self.data = Time.zone.parse(new_data)
|
||||
def update_data!(new_data, user, preview: false)
|
||||
self.data = if new_data.is_a?(String)
|
||||
Time.zone.parse(new_data)
|
||||
else
|
||||
new_data
|
||||
end
|
||||
|
||||
self.last_modified_by = user
|
||||
save!
|
||||
preview ? validate : save!
|
||||
end
|
||||
|
||||
def snapshot!(cell_snapshot)
|
||||
|
|
|
@ -56,10 +56,15 @@ class RepositoryDateValue < RepositoryDateTimeValueBase
|
|||
value
|
||||
end
|
||||
|
||||
def update_data!(new_data, user)
|
||||
self.data = Date.parse(new_data)
|
||||
def update_data!(new_data, user, preview: false)
|
||||
self.data = if new_data.is_a?(String)
|
||||
Date.parse(new_data)
|
||||
else
|
||||
new_data.to_time.utc
|
||||
end
|
||||
|
||||
self.last_modified_by = user
|
||||
save!
|
||||
preview ? validate : save!
|
||||
end
|
||||
|
||||
def self.import_from_text(text, attributes, options = {})
|
||||
|
|
|
@ -8,7 +8,7 @@ class RepositoryListValue < ApplicationRecord
|
|||
belongs_to :last_modified_by,
|
||||
foreign_key: :last_modified_by_id,
|
||||
class_name: 'User'
|
||||
has_one :repository_cell, as: :value, dependent: :destroy, inverse_of: :value
|
||||
has_one :repository_cell, as: :value, dependent: :destroy, inverse_of: :value, touch: true
|
||||
accepts_nested_attributes_for :repository_cell
|
||||
|
||||
validates :repository_cell, presence: true
|
||||
|
@ -57,10 +57,10 @@ class RepositoryListValue < ApplicationRecord
|
|||
new_data.to_i != repository_list_item_id
|
||||
end
|
||||
|
||||
def update_data!(new_data, user)
|
||||
def update_data!(new_data, user, preview: false)
|
||||
self.repository_list_item_id = new_data.to_i
|
||||
self.last_modified_by = user
|
||||
save!
|
||||
preview ? validate : save!
|
||||
end
|
||||
|
||||
def snapshot!(cell_snapshot)
|
||||
|
|
|
@ -5,7 +5,7 @@ class RepositoryNumberValue < ApplicationRecord
|
|||
inverse_of: :created_repository_number_values
|
||||
belongs_to :last_modified_by, foreign_key: :last_modified_by_id, class_name: 'User',
|
||||
inverse_of: :modified_repository_number_values
|
||||
has_one :repository_cell, as: :value, dependent: :destroy, inverse_of: :value
|
||||
has_one :repository_cell, as: :value, dependent: :destroy, inverse_of: :value, touch: true
|
||||
accepts_nested_attributes_for :repository_cell
|
||||
|
||||
validates :repository_cell, :data, presence: true
|
||||
|
@ -49,10 +49,10 @@ class RepositoryNumberValue < ApplicationRecord
|
|||
BigDecimal(new_data.to_s) != data
|
||||
end
|
||||
|
||||
def update_data!(new_data, user)
|
||||
def update_data!(new_data, user, preview: false)
|
||||
self.data = BigDecimal(new_data.to_s)
|
||||
self.last_modified_by = user
|
||||
save!
|
||||
preview ? validate : save!
|
||||
end
|
||||
|
||||
def snapshot!(cell_snapshot)
|
||||
|
|
|
@ -105,6 +105,8 @@ class RepositoryRow < ApplicationRecord
|
|||
length: { maximum: Constants::NAME_MAX_LENGTH }
|
||||
validates :created_by, presence: true
|
||||
|
||||
attr_accessor :import_status, :import_message
|
||||
|
||||
scope :active, -> { where(archived: false) }
|
||||
scope :archived, -> { where(archived: true) }
|
||||
|
||||
|
|
|
@ -6,7 +6,7 @@ class RepositoryStatusValue < ApplicationRecord
|
|||
inverse_of: :created_repository_status_value
|
||||
belongs_to :last_modified_by, foreign_key: 'last_modified_by_id', class_name: 'User', optional: true,
|
||||
inverse_of: :modified_repository_status_value
|
||||
has_one :repository_cell, as: :value, dependent: :destroy, inverse_of: :value
|
||||
has_one :repository_cell, as: :value, dependent: :destroy, inverse_of: :value, touch: true
|
||||
accepts_nested_attributes_for :repository_cell
|
||||
|
||||
validates :repository_cell, :repository_status_item, presence: true
|
||||
|
@ -48,10 +48,10 @@ class RepositoryStatusValue < ApplicationRecord
|
|||
new_data.to_i != repository_status_item_id
|
||||
end
|
||||
|
||||
def update_data!(new_data, user)
|
||||
def update_data!(new_data, user, preview: false)
|
||||
self.repository_status_item_id = new_data.to_i
|
||||
self.last_modified_by = user
|
||||
save!
|
||||
preview ? validate : save!
|
||||
end
|
||||
|
||||
def snapshot!(cell_snapshot)
|
||||
|
|
|
@ -9,7 +9,7 @@ class RepositoryStockValue < ApplicationRecord
|
|||
belongs_to :repository_stock_unit_item, optional: true
|
||||
belongs_to :created_by, class_name: 'User', optional: true, inverse_of: :created_repository_stock_values
|
||||
belongs_to :last_modified_by, class_name: 'User', optional: true, inverse_of: :modified_repository_stock_values
|
||||
has_one :repository_cell, as: :value, dependent: :destroy, inverse_of: :value
|
||||
has_one :repository_cell, as: :value, dependent: :destroy, inverse_of: :value, touch: true
|
||||
has_one :repository_row, through: :repository_cell
|
||||
has_many :repository_ledger_records, dependent: :destroy
|
||||
accepts_nested_attributes_for :repository_cell
|
||||
|
@ -108,7 +108,7 @@ class RepositoryStockValue < ApplicationRecord
|
|||
(new_data[:unit_item_id].present? && new_data[:unit_item_id] != repository_stock_unit_item.id)
|
||||
end
|
||||
|
||||
def update_data!(new_data, user)
|
||||
def update_data!(new_data, user, preview: false)
|
||||
self.low_stock_threshold = new_data[:low_stock_threshold].presence if new_data[:low_stock_threshold]
|
||||
self.repository_stock_unit_item = repository_cell
|
||||
.repository_column
|
||||
|
@ -118,16 +118,20 @@ class RepositoryStockValue < ApplicationRecord
|
|||
new_amount = new_data[:amount].to_d
|
||||
delta = new_amount - amount.to_d
|
||||
self.comment = new_data[:comment].presence
|
||||
repository_ledger_records.create!(
|
||||
user: last_modified_by,
|
||||
amount: delta,
|
||||
balance: new_amount,
|
||||
reference: repository_cell.repository_column.repository,
|
||||
comment: comment,
|
||||
unit: repository_stock_unit_item&.data
|
||||
)
|
||||
|
||||
unless preview
|
||||
repository_ledger_records.create!(
|
||||
user: last_modified_by,
|
||||
amount: delta,
|
||||
balance: new_amount,
|
||||
reference: repository_cell.repository_column.repository,
|
||||
comment: comment,
|
||||
unit: repository_stock_unit_item&.data
|
||||
)
|
||||
end
|
||||
|
||||
self.amount = new_amount
|
||||
save!
|
||||
preview ? validate : save!
|
||||
end
|
||||
|
||||
def snapshot!(cell_snapshot)
|
||||
|
@ -167,7 +171,7 @@ class RepositoryStockValue < ApplicationRecord
|
|||
end
|
||||
|
||||
def self.import_from_text(text, attributes, _options = {})
|
||||
digit, unit = text.match(/(^\d*\.?\d*)(\D*)/).captures
|
||||
digit, unit = text.match(/(^-?\d*\.?\d*)(\D*)/).captures
|
||||
digit.strip!
|
||||
unit.strip!
|
||||
return nil if digit.blank?
|
||||
|
|
|
@ -7,7 +7,7 @@ class RepositoryTextValue < ApplicationRecord
|
|||
belongs_to :last_modified_by, foreign_key: :last_modified_by_id,
|
||||
class_name: 'User',
|
||||
inverse_of: :modified_repository_text_values
|
||||
has_one :repository_cell, as: :value, dependent: :destroy
|
||||
has_one :repository_cell, as: :value, dependent: :destroy, touch: true
|
||||
accepts_nested_attributes_for :repository_cell
|
||||
|
||||
validates :repository_cell, presence: true
|
||||
|
@ -36,10 +36,10 @@ class RepositoryTextValue < ApplicationRecord
|
|||
new_data != data
|
||||
end
|
||||
|
||||
def update_data!(new_data, user)
|
||||
def update_data!(new_data, user, preview: false)
|
||||
self.data = new_data
|
||||
self.last_modified_by = user
|
||||
save!
|
||||
preview ? validate : save!
|
||||
end
|
||||
|
||||
def snapshot!(cell_snapshot)
|
||||
|
@ -61,7 +61,7 @@ class RepositoryTextValue < ApplicationRecord
|
|||
def self.import_from_text(text, attributes, _options = {})
|
||||
return nil if text.blank?
|
||||
|
||||
new(attributes.merge(data: text.truncate(Constants::TEXT_MAX_LENGTH)))
|
||||
new(attributes.merge(data: text))
|
||||
end
|
||||
|
||||
alias export_formatted formatted
|
||||
|
|
|
@ -5,6 +5,9 @@ class Result < ApplicationRecord
|
|||
include SearchableModel
|
||||
include SearchableByNameModel
|
||||
include ViewableModel
|
||||
include Discard::Model
|
||||
|
||||
default_scope -> { kept }
|
||||
|
||||
auto_strip_attributes :name, nullify: false
|
||||
validates :name, length: { maximum: Constants::NAME_MAX_LENGTH }
|
||||
|
|
|
@ -25,7 +25,7 @@ class Step < ApplicationRecord
|
|||
after_destroy :adjust_positions_after_destroy, unless: -> { skip_position_adjust }
|
||||
|
||||
# conditional touch excluding step completion updates
|
||||
after_destroy :touch_protocol
|
||||
after_destroy :touch_protocol, :remove_from_user_settings
|
||||
after_save :touch_protocol
|
||||
after_touch :touch_protocol
|
||||
|
||||
|
@ -167,6 +167,10 @@ class Step < ApplicationRecord
|
|||
|
||||
private
|
||||
|
||||
def remove_from_user_settings
|
||||
CleanupUserSettingsJob.perform_later('task_step_states', id)
|
||||
end
|
||||
|
||||
def touch_protocol
|
||||
# if only step completion attributes were changed, do not touch protocol
|
||||
return if saved_changes.keys.sort == %w(completed completed_on updated_at)
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue