Merge branch 'develop' into features/inventory-import-improvements

This commit is contained in:
Martin Artnik 2024-02-20 15:18:07 +01:00
commit f8aae8aca6
147 changed files with 2195 additions and 696 deletions

View file

@ -9,7 +9,7 @@ AllCops:
- "spec/**/*"
NewCops: enable
UseCache: false
TargetRubyVersion: 3.0
TargetRubyVersion: 3.1
##################### Style ####################################

View file

@ -56,7 +56,7 @@ gem 'i18n-js', '~> 3.6' # Localization in javascript files
gem 'jbuilder' # JSON structures via a Builder-style DSL
gem 'logging', '~> 2.0.0'
gem 'nested_form_fields'
gem 'nokogiri', '~> 1.14.3' # HTML/XML parser
gem 'nokogiri', '~> 1.16.2' # HTML/XML parser
gem 'noticed'
gem 'rails_autolink', '~> 1.1', '>= 1.1.6'
gem 'rgl' # Graph framework for project diagram calculations

View file

@ -422,7 +422,7 @@ GEM
mime-types-data (3.2023.0218.1)
mini_magick (4.12.0)
mini_mime (1.1.2)
mini_portile2 (2.8.4)
mini_portile2 (2.8.5)
minitest (5.20.0)
msgpack (1.7.1)
multi_json (1.15.0)
@ -443,12 +443,12 @@ GEM
net-protocol
newrelic_rpm (9.2.2)
nio4r (2.7.0)
nokogiri (1.14.5)
mini_portile2 (~> 2.8.0)
nokogiri (1.16.2)
mini_portile2 (~> 2.8.2)
racc (~> 1.4)
nokogiri (1.14.5-arm64-darwin)
nokogiri (1.16.2-arm64-darwin)
racc (~> 1.4)
nokogiri (1.14.5-x86_64-linux)
nokogiri (1.16.2-x86_64-linux)
racc (~> 1.4)
noticed (1.6.3)
http (>= 4.0.0)
@ -508,7 +508,7 @@ GEM
puma (6.4.2)
nio4r (~> 2.0)
raabro (1.4.0)
racc (1.7.1)
racc (1.7.3)
rack (2.2.7)
rack-attack (6.6.1)
rack (>= 1.0, < 3)
@ -769,7 +769,7 @@ DEPENDENCIES
logging (~> 2.0.0)
nested_form_fields
newrelic_rpm
nokogiri (~> 1.14.3)
nokogiri (~> 1.16.2)
noticed
omniauth (~> 2.1)
omniauth-azure-activedirectory-v2

View file

@ -45,6 +45,10 @@
$(document).on('ajax:success', 'form#new-user-assignment-form', function(_e, data) {
$('#user_assignments_modal').replaceWith($(data.html).find('#user_assignments_modal'));
HelperModule.flashAlertMsg(data.flash, 'success');
if (window.actionToolbarComponent?.reloadCallback) {
window.actionToolbarComponent.reloadCallback();
}
});
$(document).on('ajax:error', 'form#new-user-assignment-form', function(_e, data) {
@ -60,6 +64,10 @@
if (data.flash) {
HelperModule.flashAlertMsg(data.flash, 'success');
}
if (window.actionToolbarComponent?.reloadCallback) {
window.actionToolbarComponent.reloadCallback();
}
});
$(document).on('click', '.user-assignment-dropdown .user-role-selector', function() {

View file

@ -287,48 +287,6 @@ function refreshProtocolStatusBar() {
});
}
function initImport() {
var fileInput = $("[data-action='load-from-file']");
// Make sure multiple selections of same file
// always prompt new modal
fileInput.find("input[type='file']").on('click', function() {
this.value = null;
});
// Hack to hide "No file chosen" tooltip
fileInput.attr('title', window.URL ? ' ' : '');
fileInput.on('change', function(ev) {
var importUrl = fileInput.attr('data-import-url');
importProtocolFromFile(
ev.target.files[0],
importUrl,
null,
true,
function(datas) {
var data = datas[0];
if (data.status === 'ok') {
// Simply reload page
location.reload();
} else if (data.status === 'locked') {
alert(I18n.t('my_modules.protocols.load_from_file_error_locked'));
} else {
if (data.status === 'size_too_large') {
alert(I18n.t('my_modules.protocols.load_from_file_size_error',
{ size: GLOBAL_CONSTANTS.FILE_MAX_SIZE_MB }));
} else {
alert(I18n.t('my_modules.protocols.load_from_file_error'));
}
animateSpinner(null, false);
}
}
);
// Clear input on self
$(this).val('');
});
}
function initDetailsDropdown() {
$('#task-details .task-section-caret').on('click', function() {
if (!$('.task-details').hasClass('collapsing')) {
@ -354,7 +312,6 @@ function init() {
initEditProtocolDescription();
initLinkUpdate();
initLoadFromRepository();
initImport();
initProtocolSectionOpenEvent();
initDetailsDropdown();
}

View file

@ -1372,6 +1372,9 @@ function bindNewModuleAction(gridDistX, gridDistY) {
function handleNewNameConfirm(ev) {
var input = $("#new-module-name-input");
input.parent().removeClass("has-error");
input.next("span.help-block").remove();
// Validate module name
var moduleNameValid = textValidator(ev, input,
<%= Constants::NAME_MIN_LENGTH %>, <%= Constants::NAME_MAX_LENGTH %>,

View file

@ -335,10 +335,9 @@
if (data.path) {
window.location.replace(data.path);
}
refreshCurrentView();
})
.on('ajax:error', '.experiment-action-form', function(ev, data) {
$(this).renderFormErrors('experiment', data.responseJSON);
HelperModule.flashAlertMsg(data.responseJSON.message, 'danger');
});
window.initActionToolbar();

View file

@ -179,7 +179,7 @@ var RepositoryDatatable = (function(global) {
});
}
function changeToViewMode() {
function changeToViewMode(restoreSizes = true) {
currentMode = 'viewMode';
// Table specific stuff
TABLE.button(0).enable(true);
@ -192,7 +192,7 @@ var RepositoryDatatable = (function(global) {
updateButtons();
disableCheckboxToggleOnCheckboxPreview();
restoreColumnSizes();
if (restoreSizes) restoreColumnSizes();
}
function changeToEditMode() {
@ -626,7 +626,6 @@ var RepositoryDatatable = (function(global) {
return JSON.stringify(d);
},
complete: restoreColumnSizes,
global: false,
type: 'POST'
},
@ -686,6 +685,11 @@ var RepositoryDatatable = (function(global) {
targets: 5,
class: 'added-on',
visible: true
},{
// Added by column
targets: 6,
class: 'added-by',
visible: true
}, {
targets: '_all',
render: function(data) {
@ -743,7 +747,7 @@ var RepositoryDatatable = (function(global) {
var archivedOnIndex = TABLE.column('#archived-on').index();
var archivedByIndex = TABLE.column('#archived-by').index();
animateSpinner(this, false);
changeToViewMode();
changeToViewMode(false);
updateDataTableSelectAllCtrl();
// Prevent row toggling when selecting user smart annotation link
@ -776,8 +780,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[6]) json.state.columns[6].visible = archived;
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.search) delete json.state.search;
if (json.state.ColSizes && json.state.ColSizes.length > 0) {
@ -860,8 +864,6 @@ var RepositoryDatatable = (function(global) {
clearTimeout(resizeTimeout);
resizeTimeout = setTimeout(restoreColumnSizes, 200);
});
restoreColumnSizes();
}
});

View file

@ -17,6 +17,8 @@
var formGroup = $('#form-records-file').find('.form-group');
formGroup.addClass('has-error');
formGroup.find('.help-block').remove();
$('#form-records-file input[type="submit"]').removeAttr('disabled');
$('#parse-sheet-loader').addClass('hidden');
formGroup.append(
'<span class="help-block">' + XHR.responseJSON.message + '</span>'
);
@ -24,6 +26,8 @@
}
function handleSuccessfulSubmit(data) {
$('#form-records-file input[type="submit"]').removeAttr('disabled');
$('#parse-sheet-loader').addClass('hidden');
$('#modal-import-records').modal('hide');
$(data.html).appendTo('body').promise().done(function() {
$('#parse-records-modal').modal('show');
@ -54,6 +58,8 @@
submitBtn.on('click', function(event) {
var data = new FormData();
submitBtn.attr('disabled', true);
$('#parse-sheet-loader').removeClass('hidden');
event.preventDefault();
event.stopPropagation();
data.append('file', document.getElementById('file').files[0]);

View file

@ -224,17 +224,6 @@ var RepositoryColumns = (function() {
});
}
function generateColumnNameTooltip(name) {
var maxLength = $(TABLE_ID).data('max-dropdown-length');
if ($.trim(name).length > maxLength) {
return `<div class="modal-tooltip">
${truncateLongString(name, maxLength)}
<span class="modal-tooltiptext">${name}</span>
</div>`;
}
return name;
}
function toggleColumnVisibility() {
$(columnsList).find('.vis').on('click', function(event) {
const $this = $(this);
@ -323,8 +312,8 @@ var RepositoryColumns = (function() {
<span class="vis-controls">
<span class="vis sn-icon ${visClass}" title="${visText}"></span>
</span>
<span class="text">${generateColumnNameTooltip(thederName)}</span>
<span class="column-type pull-right">${
<div class="text truncate" title="${thederName}">${thederName}</div>
<span class="column-type pull-right shrink-0">${
getColumnTypeText(el, colId) || '<i class="sn-icon sn-icon-locked-task"></i>'
}</span>
<span class="sci-btn-group manage-controls pull-right" data-view-mode="active">

View file

@ -56,11 +56,22 @@ var SmartAnnotation = (function() {
at: at,
callbacks: {
remoteFilter: function(query, callback) {
// show loader after .25 seconds and block other tab clicks
var loaderTimeout = setTimeout(function() {
$('.atwho-scroll-container').css({ height: '100px' });
$('.atwho-scroll-container').html('<div class="loading-overlay" style="padding: 20px"></div>');
$('.atwho-header-res').css({ 'pointer-events': 'none' });
}, 250);
var $currentAtWho = $(`.atwho-view[data-at-who-id=${$(field).attr('data-smart-annotation')}]`);
var filterType;
var params = { query: query };
filterType = FilterTypeEnum[$currentAtWho.find('.tab-pane.active').data('object-type')];
if (!filterType) {
clearTimeout(loaderTimeout);
$('.atwho-header-res').css({ 'pointer-events': '' });
callback([{ name: '' }]);
return false;
}
@ -73,6 +84,9 @@ var SmartAnnotation = (function() {
}
}
$.getJSON(filterType.dataUrl, params, function(data) {
clearTimeout(loaderTimeout);
$('.atwho-header-res').css({ 'pointer-events': '' });
localStorage.setItem('smart_annotation_states/teams/' + data.team, JSON.stringify({
tag: filterType.tag,
repository: data.repository

View file

@ -14,4 +14,6 @@ const GLOBAL_CONSTANTS = {
FILENAME_MAX_LENGTH: <%= Constants::FILENAME_MAX_LENGTH %>,
FAST_STATUS_POLLING_INTERVAL: <%= Constants::FAST_STATUS_POLLING_INTERVAL %>,
SLOW_STATUS_POLLING_INTERVAL: <%= Constants::SLOW_STATUS_POLLING_INTERVAL %>,
ASSET_POLLING_INTERVAL: <%= Constants::ASSET_POLLING_INTERVAL %>,
ASSET_SYNC_URL: '<%= Constants::ASSET_SYNC_URL %>',
};

View file

@ -148,7 +148,6 @@ var filterDropdown = (function() {
initCloseButton();
initDateTimePickerComponent();
initSearchField(filtersEnabledFunction);
this.toggleFilterMark($filterContainer, filtersEnabled)
return $filterContainer;
},
toggleFilterMark: function(filterContainer, filtersEnabledArg) {

View file

@ -1,6 +1,6 @@
/* eslint-disable no-param-reassign */
/* eslint-disable no-undef */
/* global I18n */
/* global HelperModule I18n */
/* eslint-disable no-unused-vars, no-use-before-define */
/* config = {
@ -218,6 +218,8 @@ var zebraPrint = (function() {
updateProgressModalData(progressModal, printData.printer_name, PRINTER_STATUS_ERROR, PRINTER_STATUS_ERROR);
}
});
}).fail(() => {
HelperModule.flashAlertMsg(I18n.t('repository_row.modal_print_label.general_error'), 'danger');
});
}
};

View file

@ -26,6 +26,15 @@ html {
opacity: 1;
}
.scroll-container.ps-transparent .ps__rail-y{
background-color: transparent;
&::after,
&::before {
background-color: transparent;
}
}
.scroll-container .ps__thumb-y{
background-color: var(--sn-grey);
opacity: 1;

View file

@ -335,8 +335,8 @@
}
&::before {
@include font-small;
bottom: -15px;
font-size: x-small;
bottom: -18px;
color: $brand-danger;
content: attr(data-error-text);
left: 0;

View file

@ -108,6 +108,12 @@
z-index: 99999999;
}
.modal-tooltip > span:first-child {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.col-list-el {
align-items: center;
background: $color-white;
@ -120,6 +126,7 @@
.manage-controls {
display: none;
white-space: nowrap;
}
.text {

View file

@ -236,3 +236,7 @@
.sa-link {
pointer-events: initial;
}
.atwho-inserted {
line-height: 16px;
}

View file

@ -34,7 +34,7 @@ module Api
end
asset.save!(context: :on_api_upload)
asset.post_process_file(@team)
asset.post_process_file
render jsonapi: asset,
serializer: AssetSerializer,

View file

@ -113,7 +113,7 @@ module Api
blob = create_blob_from_params
asset = Asset.create!(file: blob, team: @team)
end
asset.post_process_file(@team)
asset.post_process_file
ResultAsset.create!(asset: asset, result: @result)
end
end
@ -129,7 +129,7 @@ module Api
blob = create_blob_from_params
asset.update!(file: blob)
end
asset.post_process_file(@team)
asset.post_process_file
new_checksum = asset.file.blob.checksum
end
@asset_result_updated = old_checksum != new_checksum

View file

@ -0,0 +1,227 @@
# frozen_string_literal: true
class AssetSyncController < ApplicationController
include FileIconsHelper
skip_before_action :authenticate_user!, only: %i(update download)
skip_before_action :verify_authenticity_token, only: %i(update download)
before_action :authenticate_asset_sync_token!, only: %i(update download)
before_action :check_asset_sync
def show
asset = Asset.find_by(id: params[:asset_id])
render_error(:forbidden) and return unless asset && can_manage_asset?(asset)
asset_sync_token = current_user.asset_sync_tokens.find_or_create_by(asset_id: params[:asset_id])
unless asset_sync_token.token_valid?
asset_sync_token = current_user.asset_sync_tokens.create(asset_id: params[:asset_id])
end
render json: AssetSyncTokenSerializer.new(asset_sync_token).as_json
end
def download
redirect_to(@asset.file.url, allow_other_host: true)
end
def update
if @asset_sync_token.conflicts?(request.headers['VersionToken'])
ActiveRecord::Base.transaction do
conflict_response = AssetSyncTokenSerializer.new(conflicting_asset_copy_token).as_json
error_message = { message: I18n.t('assets.conflict_error', filename: @asset.file.filename) }
log_activity(:create)
render json: conflict_response.merge(error_message), status: :conflict
end
return
end
orig_file_size = @asset.file_size
ActiveRecord::Base.transaction do
@asset.update(last_modified_by: current_user)
if wopi_file?(@asset)
@asset.update_contents(request.body)
else
@asset.file.attach(io: request.body, filename: @asset.file.filename)
@asset.touch
end
@asset.team.release_space(orig_file_size)
@asset.post_process_file
log_activity(:edit)
end
render json: AssetSyncTokenSerializer.new(@asset_sync_token).as_json
end
def api_url
render plain: Constants::ASSET_SYNC_URL
end
def log_activity(action)
case action
when :edit
log_edit_activity
when :create
log_create_activity
end
end
private
def render_error(status, filename = nil, message = nil)
message ||= if filename.present?
I18n.t('assets.default_error_with_filename', filename: filename)
else
I18n.t('assets.default_error')
end
render json: { message: message }, status: status
end
def conflicting_asset_copy_token
Asset.transaction do
new_asset = @asset.dup
new_asset.save
blob = ActiveStorage::Blob.create_and_upload!(
io: request.body,
filename: "#{@asset.file.filename.base} (#{t('general.copy')}).#{@asset.file.filename.extension}",
metadata: @asset.blob.metadata
)
new_asset.file.attach(blob)
case @asset.parent
when Step
StepAsset.create!(step: @asset.step, asset: new_asset)
when Result
ResultAsset.create!(result: @asset.result, asset: new_asset)
end
@asset = new_asset.reload
new_asset.post_process_file
current_user.asset_sync_tokens.create!(asset_id: new_asset.id)
end
end
def authenticate_asset_sync_token!
@asset_sync_token = AssetSyncToken.find_by(token: request.headers['Authentication'])
render_error(:unauthorized) and return unless @asset_sync_token&.token_valid?
@asset = @asset_sync_token.asset
@current_user = @asset_sync_token.user
render_error(:forbidden, @asset.file.filename) and return unless can_manage_asset?(@asset)
end
def log_create_activity
assoc = @asset.step
assoc ||= @asset.result
case assoc
when Step
type_of = assoc.protocol.in_module? ? :task_step_file_added : :protocol_step_file_added
message_items = {
step: assoc.id,
step_position: { id: assoc.id,
value_for: 'position_plus_one' },
file: @asset.file_name,
my_module: assoc.protocol.in_module? ? assoc.my_module.id : nil,
protocol: assoc.protocol.in_module? ? nil : assoc.protocol.id
}.compact
project = assoc.protocol.in_module? ? assoc.my_module.project : nil
when Result
type_of = :result_file_added
message_items = { result: assoc }
project = assoc.my_module.project
end
Activities::CreateActivityService.call(
activity_type: type_of,
owner: current_user,
team: assoc.team,
subject: @asset,
project: project,
message_items: message_items
)
end
def log_edit_activity
assoc ||= @asset.step
assoc ||= @asset.result
case assoc
when Step
if assoc.protocol.in_module?
log_step_edit_activity(
:edit_task_step_file_locally,
assoc,
assoc.my_module.project,
my_module: assoc.my_module.id,
file: @asset.file_name,
user: current_user.id,
step_position_original: @asset.step.position + 1,
step: assoc.id
)
else
log_step_edit_activity(
:edit_protocol_template_file_locally,
assoc,
nil,
{
file: @asset.file_name,
user: current_user.id,
step_position_original: @asset.step.position + 1,
step: assoc.id
}
)
end
when Result
log_result_edit_activity(
:edit_task_result_file_locally,
assoc,
file: @asset.file_name,
user: current_user.id,
result: Result.first.id
)
end
end
def log_step_edit_activity(type_of, step, project = nil, message_items = {})
default_items = { step: step.id,
step_position: { id: step.id, value_for: 'position_plus_one' } }
message_items = default_items.merge(message_items)
Activities::CreateActivityService
.call(activity_type: type_of,
owner: current_user,
subject: step.protocol,
team: step.protocol.team,
project: project,
message_items: message_items)
end
def log_result_edit_activity(type_of, result, message_items)
Activities::CreateActivityService
.call(activity_type: type_of,
owner: current_user,
subject: result,
team: result.my_module.team,
project: result.my_module.project,
message_items: {
result: result.id
}.merge(message_items))
end
def check_asset_sync
render_404 if ENV['ASSET_SYNC_URL'].blank?
end
end

View file

@ -145,6 +145,14 @@ class AssetsController < ApplicationController
redirect_to rails_blob_path(@asset.file, disposition: 'attachment')
end
def show
if @asset
render json: @asset, serializer: AssetSerializer, user: current_user
else
render json: { error: 'Asset not found' }, status: :not_found
end
end
def edit
action = @asset.file_size.zero? && !@asset.locked? ? 'editnew' : 'edit'
@action_url = append_wd_params(@asset.get_action_url(current_user, action, false))
@ -186,13 +194,14 @@ class AssetsController < ApplicationController
orig_file_name = @asset.file_name
return render_403 unless can_read_team?(@asset.team)
@asset.last_modified_by = current_user
@asset.file.attach(io: params.require(:image), filename: orig_file_name)
@asset.save!
create_edit_image_activity(@asset, current_user, :finish_editing)
# release previous image space
@asset.team.release_space(orig_file_size)
# Post process file here
@asset.post_process_file(@asset.team)
@asset.post_process_file
@asset.step&.protocol&.update(updated_at: Time.zone.now)
render_html = if [Result, Step].include?(@assoc.class)
@ -302,6 +311,10 @@ class AssetsController < ApplicationController
end
end
def checksum
render json: { checksum: @asset.file.blob.checksum }
end
private
def load_vars

View file

@ -8,19 +8,21 @@ class ExperimentsController < ApplicationController
include Rails.application.routes.url_helpers
include Breadcrumbs
before_action :load_project, only: %i(new create archive_group restore_group)
before_action :load_project, only: %i(new create archive_group restore_group move)
before_action :load_experiment, except: %i(new create archive_group restore_group
inventory_assigning_experiment_filter actions_toolbar)
before_action :check_read_permissions, except: %i(edit archive clone move new
inventory_assigning_experiment_filter actions_toolbar
move move_modal)
before_action :load_experiments, only: %i(move_modal move)
before_action :check_move_permissions, only: %i(move_modal move)
before_action :check_read_permissions, except: %i(edit archive clone move move_modal new
create archive_group restore_group
inventory_assigning_experiment_filter actions_toolbar)
before_action :check_canvas_read_permissions, only: %i(canvas)
before_action :check_create_permissions, only: %i(new create)
before_action :check_create_permissions, only: %i(new create move)
before_action :check_manage_permissions, only: %i(edit batch_clone_my_modules)
before_action :check_update_permissions, only: %i(update)
before_action :check_archive_permissions, only: :archive
before_action :check_clone_permissions, only: %i(clone_modal clone)
before_action :check_move_permissions, only: %i(move_modal move)
before_action :set_inline_name_editing, only: %i(canvas table module_archive)
before_action :set_breadcrumbs_items, only: %i(canvas table module_archive)
before_action :set_navigator, only: %i(canvas module_archive table)
@ -254,11 +256,11 @@ class ExperimentsController < ApplicationController
# POST: clone_experiment(id)
def clone
project = current_team.projects.find(move_experiment_param)
return render_403 unless can_create_project_experiments?(project)
@project = current_team.projects.find(move_experiment_param)
return render_403 unless can_create_project_experiments?(@project)
service = Experiments::CopyExperimentAsTemplateService.call(experiment: @experiment,
project: project,
project: @project,
user: current_user)
if service.succeed?
@ -274,7 +276,7 @@ class ExperimentsController < ApplicationController
# GET: move_modal_experiment_path(id)
def move_modal
@projects = @experiment.movable_projects(current_user)
@projects = @experiments.first.movable_projects(current_user)
render json: {
html: render_to_string(partial: 'move_modal', formats: :html)
}
@ -297,23 +299,28 @@ class ExperimentsController < ApplicationController
# POST: move_experiment(id)
def move
@project.transaction do
@experiments.each do |experiment|
service = Experiments::MoveToProjectService
.call(experiment_id: @experiment.id,
project_id: move_experiment_param,
.call(experiment_id: experiment.id,
project_id: params[:project_id],
user_id: current_user.id)
if service.succeed?
flash[:success] = t('experiments.move.success_flash',
experiment: @experiment.name)
status = :ok
view_state = @experiment.current_view_state(current_user)
view_type = view_state.state['my_modules']['view_type'] || 'canvas'
path = view_mode_redirect_url(view_type)
else
message = "#{service.errors.values.join('. ')}."
status = :unprocessable_entity
raise StandardError unless service.succeed?
end
render json: { message: message, path: path }, status: status
flash[:success] = t('experiments.table.move_success_flash', project: escape_input(@project.name))
render json: { message: t('experiments.table.move_success_flash',
project: escape_input(@project.name)), path: project_path(@project) }
rescue StandardError => e
Rails.logger.error(e.message)
Rails.logger.error(e.backtrace.join("\n"))
render json: {
message: t('experiments.table.move_error_flash', project: escape_input(@project.name))
}, status: :unprocessable_entity
raise ActiveRecord::Rollback
end
rescue ActiveRecord::RecordNotFound
render_404
end
def move_modules_modal
@ -532,6 +539,11 @@ class ExperimentsController < ApplicationController
render_404 unless @experiment
end
def load_experiments
@experiments = Experiment.preload(user_assignments: %i(user user_role)).where(id: params[:ids])
render_404 unless @experiments
end
def load_project
@project = Project.find_by(id: params[:project_id])
render_404 unless @project
@ -584,7 +596,7 @@ class ExperimentsController < ApplicationController
end
def check_move_permissions
render_403 unless can_move_experiment?(@experiment)
render_403 unless @experiments.all? { |e| can_move_experiment?(e) }
end
def set_inline_name_editing

View file

@ -91,6 +91,7 @@ class GeneSequenceAssetsController < ApplicationController
file.blob.metadata['name'] = params[:sequence_name]
file.save!
@asset.view_mode = view_mode || @parent.assets_view_mode
@asset.last_modified_by = current_user
@asset.save!
end
end

View file

@ -130,7 +130,7 @@ class MyModuleRepositoriesController < ApplicationController
render json: {
html: render_to_string(
partial: 'my_modules/repositories/full_view_table',
locals: { include_stock_consumption: params[:include_stock_consumption] }
locals: { include_stock_consumption: params[:include_stock_consumption] == 'true' }
)
}
end

View file

@ -34,7 +34,7 @@ class MyModuleRepositorySnapshotsController < ApplicationController
end
def create
repository_snapshot = RepositorySnapshot.create_preliminary(@repository, @my_module, current_user)
repository_snapshot = RepositorySnapshot.create_preliminary!(@repository, @my_module, current_user)
RepositorySnapshotProvisioningJob.perform_later(repository_snapshot)
render json: {

View file

@ -53,9 +53,6 @@ class ProtocolsController < ApplicationController
before_action :check_load_from_repository_permissions, only: [
:load_from_repository
]
before_action :check_load_from_file_permissions, only: [
:load_from_file
]
before_action :check_copy_to_repository_permissions, only: %i(
copy_to_repository
)
@ -69,7 +66,7 @@ class ProtocolsController < ApplicationController
before_action :check_protocolsio_import_permissions,
only: %i(protocolsio_import_create protocolsio_import_save)
before_action :set_importer, only: %i(load_from_file import)
before_action :set_importer, only: :import
before_action :set_inline_name_editing, only: :show
before_action :set_breadcrumbs_items, only: %i(index show)
@ -472,41 +469,6 @@ class ProtocolsController < ApplicationController
end
end
def load_from_file
# This is actually very similar to import
if @protocol.can_destroy?
transaction_error = false
Protocol.transaction do
@importer.import_into_existing(
@protocol, @protocol_json
)
rescue StandardError => e
transaction_error = true
Rails.logger.error(e.message)
Rails.logger.error(e.backtrace.join("\n"))
raise ActiveRecord::Rollback
end
if transaction_error
format.json do
render json: { status: :error }, status: :bad_request
end
else
# Everything good, record activity, display flash & render 200
log_activity(:load_protocol_to_task_from_file,
@protocol.my_module.experiment.project,
my_module: @my_module.id)
flash[:success] = t(
'my_modules.protocols.load_from_file_flash'
)
flash.keep(:success)
render json: { status: :ok }, status: :ok
end
else
render json: { status: :locked }, status: :bad_request
end
end
def protocolsio_index
render json: {
html: render_to_string({ partial: 'protocols/index/protocolsio_modal_body', formats: :html })
@ -1015,19 +977,6 @@ class ProtocolsController < ApplicationController
can_read_protocol_in_repository?(@source))
end
def check_load_from_file_permissions
@protocol_json = params[:protocol]
@protocol = Protocol.find_by_id(params[:id])
@my_module = @protocol.my_module
if @protocol_json.blank? ||
@protocol.blank? ||
@my_module.blank? ||
!can_manage_protocol_in_module?(@protocol)
render_403
end
end
def check_copy_to_repository_permissions
@protocol = Protocol.find_by(id: params[:id])
@my_module = @protocol.my_module

View file

@ -155,7 +155,7 @@ class ReportsController < ApplicationController
end
def status
docx = @report.docx_file.attached? ? document_preview_report_path(@report, report_type: :docx) : nil
docx = @report.docx_preview_file.attached? ? document_preview_report_path(@report, report_type: :docx) : nil
pdf = @report.pdf_file.attached? ? document_preview_report_path(@report, report_type: :pdf) : nil
render json: {

View file

@ -60,7 +60,7 @@ class ResultAssetsController < ApplicationController
team.save
# Post process new file if neccesary
@result.asset.post_process_file(team) if asset_changed && @result.asset.present?
@result.asset.post_process_file if asset_changed && @result.asset.present?
log_activity(:edit_result)
end
@ -133,7 +133,7 @@ class ResultAssetsController < ApplicationController
last_modified_by: current_user)
results << result
# Post process file here
asset.post_process_file(@my_module.experiment.project.team)
asset.post_process_file
log_activity(:add_result, result)
end

View file

@ -90,7 +90,7 @@ class ResultsController < ApplicationController
view_mode: @result.assets_view_mode
)
@asset.file.attach(params[:signed_blob_id])
@asset.post_process_file(@my_module.team)
@asset.post_process_file
end
log_activity(:result_file_added, { file: @asset.file_name, result: @result })

View file

@ -18,7 +18,7 @@ module StepElements
checklist_item = @checklist.checklist_items.new(checklist_item_params.merge!(created_by: current_user))
new_items = []
ActiveRecord::Base.transaction do
new_items = checklist_item.save_multiline!
new_items = checklist_item.save_multiline!(after_id: params[:after_id])
new_items.each do |item|
log_activity(
"#{@step.protocol.in_module? ? :task : :protocol}_step_checklist_item_added",
@ -102,9 +102,10 @@ module StepElements
end
def reorder
checklist_item = @checklist.checklist_items.find(checklist_item_params[:id])
checklist_item = @checklist.checklist_items.find(params[:id])
ActiveRecord::Base.transaction do
checklist_item.insert_at(checklist_item_params[:position])
insert_at = (@checklist.checklist_items.find_by(id: params[:after_id])&.position || 0)
checklist_item.insert_at(insert_at)
end
render json: params[:checklist_item_positions], status: :ok
rescue ActiveRecord::RecordInvalid

View file

@ -42,7 +42,7 @@ class StepsController < ApplicationController
view_mode: @step.assets_view_mode
)
@asset.file.attach(params[:signed_blob_id])
@asset.post_process_file(@protocol.team)
@asset.post_process_file
default_message_items = {
step: @step.id,

View file

@ -9,6 +9,7 @@ module Users
def index
@label_printer_any = LabelPrinter.any?
@user_agent = request.user_agent
end
private

View file

@ -201,8 +201,8 @@ class WopiController < ActionController::Base
logger.warn 'WOPI: replacing file'
@team.release_space(@asset.estimated_size)
@asset.update_contents(request.body)
@asset.last_modified_by = @user
@asset.update_contents(request.body)
@asset.save
@team.take_space(@asset.estimated_size)

View file

@ -146,7 +146,22 @@ class ProtocolsDatatable < CustomDatatable
end
def get_raw_records_base
records = Protocol.latest_available_versions(@team)
team_protocols = Protocol.where(team: @team)
original_without_versions = team_protocols
.left_outer_joins(:published_versions)
.in_repository_published_original
.where(published_versions: { id: nil })
.select(:id)
published_versions = team_protocols
.in_repository_published_version
.order(:parent_id, version_number: :desc)
.select('DISTINCT ON (parent_id) id')
new_drafts = team_protocols
.where(protocol_type: Protocol.protocol_types[:in_repository_draft], parent_id: nil)
.select(:id)
records = Protocol.where('protocols.id IN (?) OR protocols.id IN (?) OR protocols.id IN (?)',
original_without_versions, published_versions, new_drafts)
records = @type == :archived ? records.archived : records.active
@ -162,16 +177,14 @@ class ProtocolsDatatable < CustomDatatable
.joins("LEFT OUTER JOIN protocols protocol_versions " \
"ON protocol_versions.protocol_type = #{Protocol.protocol_types[:in_repository_published_version]} " \
"AND protocol_versions.parent_id = protocols.parent_id")
.joins("LEFT OUTER JOIN protocols self_linked_task_protocols " \
"ON self_linked_task_protocols.protocol_type = #{Protocol.protocol_types[:linked]} " \
"AND self_linked_task_protocols.parent_id = protocols.id")
.joins("LEFT OUTER JOIN protocols parent_linked_task_protocols " \
"ON parent_linked_task_protocols.protocol_type = #{Protocol.protocol_types[:linked]} " \
"AND parent_linked_task_protocols.parent_id = protocols.parent_id")
.joins("LEFT OUTER JOIN protocols version_linked_task_protocols " \
"ON version_linked_task_protocols.protocol_type = #{Protocol.protocol_types[:linked]} " \
"AND version_linked_task_protocols.parent_id = protocol_versions.id " \
"AND version_linked_task_protocols.parent_id != protocols.id")
.joins("LEFT OUTER JOIN protocols protocol_originals " \
"ON protocol_originals.protocol_type = #{Protocol.protocol_types[:in_repository_published_original]} " \
"AND protocol_originals.id = protocols.parent_id OR " \
"(protocols.id = protocol_originals.id AND protocols.parent_id IS NULL)")
.joins("LEFT OUTER JOIN protocols linked_task_protocols " \
"ON linked_task_protocols.protocol_type = #{Protocol.protocol_types[:linked]} " \
"AND (linked_task_protocols.parent_id = protocol_versions.id OR " \
"linked_task_protocols.parent_id = protocol_originals.id)")
.joins('LEFT OUTER JOIN "protocol_protocol_keywords" ' \
'ON "protocol_protocol_keywords"."protocol_id" = "protocols"."id"')
.joins('LEFT OUTER JOIN "protocol_keywords" ' \
@ -191,9 +204,7 @@ class ProtocolsDatatable < CustomDatatable
"CASE WHEN protocols.protocol_type = #{Protocol.protocol_types[:in_repository_draft]} " \
"THEN 0 ELSE COUNT(DISTINCT(\"protocol_versions\".\"id\")) + 1 " \
"END AS nr_of_versions",
'(COUNT(DISTINCT("self_linked_task_protocols"."id")) + ' \
'COUNT(DISTINCT("parent_linked_task_protocols"."id")) + ' \
'COUNT(DISTINCT("version_linked_task_protocols"."id"))) AS nr_of_linked_tasks',
'COUNT(DISTINCT("linked_task_protocols"."id")) AS nr_of_linked_tasks',
'COUNT(DISTINCT("all_user_assignments"."id")) AS "nr_of_assigned_users"',
'MAX("users"."full_name") AS "full_username_str"', # "Hack" to get single username
'MAX("archived_users"."full_name") AS "archived_full_username_str"'

View file

@ -76,7 +76,7 @@ class ReportDatatable < CustomDatatable
end
def docx_file(report)
docx = document_preview_report_path(report, report_type: :docx) if report.docx_file.attached?
docx = document_preview_report_path(report, report_type: :docx) if report.docx_preview_file.attached?
{
processing: report.docx_processing?,
preview_url: docx,

View file

@ -52,6 +52,11 @@ module RepositoryDatatableHelper
serialize_repository_cell_value(cell, team, repository, reminders_enabled: reminders_enabled)
end
if repository.repository_columns.stock_type.exists?
stock_cell = record.repository_cells.find { |cell| cell.value_type == 'RepositoryStockValue' }
row['stock'] = serialize_repository_cell_value(record.repository_stock_cell, team, repository) if stock_cell.present?
end
if has_stock_management
stock_cell = record.repository_cells.find { |cell| cell.value_type == 'RepositoryStockValue' }

View file

@ -0,0 +1,56 @@
import { createApp } from 'vue/dist/vue.esm-bundler.js';
import { shallowRef } from 'vue';
import WizardModal from '../../../vue/shared/wizard_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';
import { mountWithTurbolinks } from '../helpers/turbolinks.js';
const app = createApp({
components: {
Step1,
Step2,
Step3
},
methods: {
fireAlert() {
alert('Fired!');
}
},
data() {
return {
wizardConfig: {
title: 'Wizard steps',
subtitle: 'Wizard subtitle description',
steps: [
{
id: 'step1',
icon: 'sn-icon sn-icon-open',
label: 'Step 1',
component: shallowRef(Step1)
},
{
id: 'step2',
icon: 'sn-icon sn-icon-edit',
label: 'Step 2',
component: shallowRef(Step2)
},
{
id: 'step3',
icon: 'sn-icon sn-icon-inventory',
label: 'Step 3',
component: shallowRef(Step3)
}
]
},
wizardParams: {
text: 'Some text'
},
showWizard: false
};
}
});
app.component('WizardModal', WizardModal);
app.config.globalProperties.i18n = window.I18n;
mountWithTurbolinks(app, '#modals');

View file

@ -0,0 +1,37 @@
<template>
<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 truncate flex items-center gap-4">
Step 1 actions
</h4>
</div>
<div class="modal-body grow">
You can add any custom html here or render params like this:
{{ params }}
</div>
<div class="modal-footer">
<button class="btn btn-light" @click="$emit('close')">Cancel</button>
<button class="btn btn-primary" @click="$emit('next')">Next</button>
</div>
</template>
<script>
export default {
emits: ['close', 'next', 'back'],
name: 'Step1',
props: {
params: {
type: Object,
required: true
},
wizardComponent: {
type: Object,
required: true
}
}
};
</script>

View file

@ -0,0 +1,41 @@
<template>
<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 truncate flex items-center gap-4">
Step 2 actions
</h4>
</div>
<div class="modal-body grow">
All steps have access to shared params
<input
type="text"
class="form-control"
placeholder="Type something"
v-model="params.text"/>
</div>
<div class="modal-footer">
<button class="btn btn-light" @click="$emit('back')">Back</button>
<button class="btn btn-primary" @click="$emit('next')">Next</button>
</div>
</template>
<script>
export default {
emits: ['back', 'next', 'close'],
name: 'Step2',
props: {
params: {
type: Object,
required: true
},
wizardComponent: {
type: Object,
required: true
}
}
};
</script>

View file

@ -0,0 +1,42 @@
<template>
<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 truncate flex items-center gap-4">
Step 3 actions
</h4>
</div>
<div class="modal-body grow">
Our params - {{ params }}<br>
If you want emit action use wizardComponent
<br>
<br>
<br>
<button class="btn btn-danger" @click="wizardComponent.$emit('alert')">Launch alert</button>
</div>
<div class="modal-footer">
<button class="btn btn-light" @click="$emit('back')">Back</button>
<button class="btn btn-primary" @click="$emit('close')">Apply</button>
</div>
</template>
<script>
export default {
emits: ['back', 'close', 'alert', 'next'],
name: 'Step3',
props: {
params: {
type: Object,
required: true
},
wizardComponent: {
type: Object,
required: true
}
}
};
</script>

View file

@ -0,0 +1,8 @@
import { createApp } from 'vue/dist/vue.esm-bundler.js';
import OpenLocallyMenu from '../../vue/shared/content/attachments/open_locally_menu.vue';
import { mountWithTurbolinks } from './helpers/turbolinks.js';
const app = createApp({});
app.component('OpenLocallyMenu', OpenLocallyMenu);
app.config.globalProperties.i18n = window.I18n;
mountWithTurbolinks(app, '#openLocallyMenu');

View file

@ -0,0 +1,8 @@
import { createApp } from 'vue/dist/vue.esm-bundler.js';
import ScinoteEditDownload from '../../vue/shared/scinote_edit_download.vue';
import { mountWithTurbolinks } from './helpers/turbolinks.js';
const app = createApp({});
app.component('ScinoteEditDownload', ScinoteEditDownload);
app.config.globalProperties.i18n = window.I18n;
mountWithTurbolinks(app, '#scinoteEditDownload');

View file

@ -8,7 +8,7 @@
role="dialog"
class="modal">
<div class="modal-dialog modal-sm" role="document">
<div class="modal-content w-[400px] m-auto">
<div class="modal-content w-[400px] m-auto" v-click-outside="handleClickOutside">
<!-- header -->
<div class="modal-header h-[76px] flex !flex-col gap-[6px]">
@ -48,6 +48,7 @@
@update-options="updateInventories"
@reached-end="fetchInventories"
@change="changeSelectedInventory"
data-e2e="e2e-DD-repoItemRelationshipsMD-inventory"
></select-search>
</div>
</div>
@ -75,6 +76,7 @@
@update="selectedItemValues = $event"
@reached-end="() => fetchInventoryItems(selectedInventoryValue)"
:disabled="!this.selectedInventoryValue"
data-e2e="e2e-DC-repoItemRelationshipsMD-item"
></ChecklistSearch>
</div>
</div>
@ -92,6 +94,7 @@
:value="selectedRelationshipValue"
:options="[['parent', 'Parent'], ['child', 'Child']]"
:placeholder="i18n.t('repositories.item_card.repository_item_relationships_modal.select_relationship_placeholder')"
data-e2e="e2e-DD-repoItemRelationshipsMD-relationship"
></Select>
</div>
</div>
@ -112,11 +115,11 @@
<!-- footer -->
<div class="modal-footer">
<div class="flex justify-end gap-4">
<button class="btn btn-secondary w-[78px] h-10 whitespace-nowrap" @click="close">
<button class="btn btn-secondary w-[78px] h-10 whitespace-nowrap" @click="close" data-e2e="e2e-BT-repoItemRelationshipsMD-cancel">
{{ i18n.t('repositories.item_card.repository_item_relationships_modal.cancel_button') }}
</button>
<button class="btn btn-primary w-[59px] h-10 whitespace-nowrap"
:class="{ 'disabled': !shouldEnableAddButton }" @click="() => addRelation(selectedRelationshipValue)">
:class="{ 'disabled': !shouldEnableAddButton }" @click="() => addRelation(selectedRelationshipValue)" data-e2e="e2e-BT-repoItemRelationshipsMD-add">
{{ i18n.t('repositories.item_card.repository_item_relationships_modal.add_button') }}
</button>
</div>
@ -167,6 +170,10 @@ export default {
}
},
methods: {
handleClickOutside() {
this.selectedInventoryValue = null;
this.resetSelectedItemValues();
},
fetchInventories() {
if (!this.nextInventoriesPage) return;

View file

@ -180,7 +180,7 @@ export default {
this.secondaryNavigation.style.top = '0px';
header.style.top = '0px';
header.style.boxShadow = 'none';
header.style.zIndex = 105;
header.style.zIndex = 100;
this.headerSticked = false;
}

View file

@ -123,6 +123,11 @@ export default {
} else if (this.childrenLoaded) {
this.item.has_children = false;
}
},
archived() {
if (this.childrenExpanded) {
this.loadChildren();
}
}
},
methods: {

View file

@ -2,7 +2,9 @@
<div class="sci--navigation--notificaitons-flyout">
<div class="sci--navigation--notificaitons-flyout-title">
{{ i18n.t('nav.notifications.title') }}
<i class="sn-icon sn-icon-close" @click="$emit('close')"></i>
<a class="ml-auto cursor-pointer text-sm font-normal" :href="this.preferencesUrl" :title="i18n.t('nav.settings')">
{{ i18n.t('nav.settings') }}
</a>
</div>
<hr>
<perfect-scrollbar ref="scrollContainer" class="sci--navigation--notificaitons-flyout-notifications">
@ -35,7 +37,8 @@ export default {
},
props: {
notificationsUrl: String,
unseenNotificationsCount: Number
unseenNotificationsCount: Number,
preferencesUrl: String
},
data() {
return {

View file

@ -38,6 +38,7 @@
<NotificationsFlyout
v-if="notificationsOpened"
:notificationsUrl="notificationsUrl"
:preferencesUrl="this.userMenu.find((item) => item.name === i18n.t('users.settings.sidebar.account_nav.preferences'))?.url"
:unseenNotificationsCount="unseenNotificationsCount"
@update:unseenNotificationsCount="checkUnseenNotifications()"
@close="notificationsOpened = false" />

View file

@ -123,7 +123,6 @@ export default {
mounted() {
// Legacy global functions from app/assets/javascripts/my_modules/protocols.js
initLoadFromRepository();
initImport();
initLinkUpdate();
},
methods: {

View file

@ -129,6 +129,7 @@
@attachments:openFileModal="showFileModal = true"
@attachment:deleted="attachmentDeleted"
@attachment:uploaded="loadAttachments"
@attachment:changed="reloadAttachment"
@attachments:order="changeAttachmentsOrder"
@attachment:moved="moveAttachment"
@attachments:viewMode="changeAttachmentsViewMode"
@ -166,6 +167,7 @@
import WopiFileModal from '../shared/content/attachments/mixins/wopi_file_modal.js'
import OveMixin from '../shared/content/attachments/mixins/ove.js'
import StorageUsage from '../shared/content/attachments/storage_usage.vue'
import axios from '../../packs/custom_axios';
export default {
name: 'StepContainer',
@ -370,6 +372,25 @@
});
this.showFileModal = false;
},
reloadAttachment(attachmentId) {
const index = this.attachments.findIndex(attachment => attachment.id === attachmentId);
const attachmentUrl = this.attachments[index].attributes.urls.asset_show
axios.get(attachmentUrl)
.then((response) => {
const updatedAttachment = response.data.data;
const index = this.attachments.findIndex(attachment => attachment.id === attachmentId);
if (index !== -1) {
this.attachments[index] = updatedAttachment;
}
})
.catch((error) => {
console.error("Failed to reload attachment:", error);
});
this.showFileModal = false;
},
loadElements() {
$.get(this.urls.elements_url, (result) => {
this.elements = result.data

View file

@ -16,7 +16,8 @@
:editable="permissions.can_manage && !defaultColumns?.archived"
:name="defaultColumns.name"
:archived="defaultColumns.archived"
@update="update">
@update="update"
data-e2e="e2e-TX-repoItemSB-title">
</repository-item-sidebar-title>
<i id="close-icon" @click="toggleShowHideSidebar(null)"
class="sn-icon sn-icon-close ml-auto cursor-pointer my-auto mx-0"></i>
@ -55,7 +56,7 @@
<div class="flex flex-col ">
<span class="inline-block font-semibold pb-[6px]">{{
i18n.t('repositories.item_card.default_columns.repository_name') }}</span>
<span class="repository-name text-sn-dark-grey line-clamp-3" :title="repository?.name">
<span class="repository-name text-sn-dark-grey line-clamp-3" :title="repository?.name" data-e2e="e2e-TX-repoItemSBinformation-inventory">
{{ repository?.name }}
</span>
</div>
@ -67,7 +68,7 @@
<span class="inline-block font-semibold pb-[6px]">{{
i18n.t('repositories.item_card.default_columns.id')
}}</span>
<span class="inline-block text-sn-dark-grey line-clamp-3" :title="defaultColumns?.code">
<span class="inline-block text-sn-dark-grey line-clamp-3" :title="defaultColumns?.code" data-e2e="e2e-TX-repoItemSBinformation-itemID">
{{ defaultColumns?.code }}
</span>
</div>
@ -79,7 +80,7 @@
<span class="inline-block font-semibold pb-[6px]">{{
i18n.t('repositories.item_card.default_columns.added_on')
}}</span>
<span class="inline-block text-sn-dark-grey" :title="defaultColumns?.added_on">
<span class="inline-block text-sn-dark-grey" :title="defaultColumns?.added_on" data-e2e="e2e-TX-repoItemSBinformation-addedOn">
{{ defaultColumns?.added_on }}
</span>
</div>
@ -91,7 +92,7 @@
<span class="inline-block font-semibold pb-[6px]">{{
i18n.t('repositories.item_card.default_columns.added_by')
}}</span>
<span class="inline-block text-sn-dark-grey line-clamp-3" :title="defaultColumns?.added_by">
<span class="inline-block text-sn-dark-grey line-clamp-3" :title="defaultColumns?.added_by" data-e2e="e2e-TX-repoItemSBinformation-addedBy">
{{ defaultColumns?.added_by }}
</span>
</div>
@ -102,7 +103,7 @@
<span class="inline-block font-semibold pb-[6px]">{{
i18n.t('repositories.item_card.default_columns.archived_on')
}}</span>
<span class="inline-block text-sn-dark-grey" :title="defaultColumns.archived_on">
<span class="inline-block text-sn-dark-grey" :title="defaultColumns.archived_on" data-e2e="e2e-TX-repoItemSBinformation-archivedOn">
{{ defaultColumns.archived_on }}
</span>
</div>
@ -113,7 +114,7 @@
<span class="inline-block font-semibold pb-[6px]">{{
i18n.t('repositories.item_card.default_columns.archived_by')
}}</span>
<span class="inline-block text-sn-dark-grey" :title="defaultColumns.archived_by.full_name">
<span class="inline-block text-sn-dark-grey" :title="defaultColumns.archived_by.full_name" data-e2e="e2e-TX-repoItemSBinformation-archivedBy">
{{ defaultColumns.archived_by.full_name }}
</span>
</div>
@ -147,13 +148,15 @@
</div>
<div class="font-inter text-sm leading-5 w-full">
<div class="flex flex-row justify-between mb-4">
<div class="font-semibold">
<div class="font-semibold" data-e2e="e2e-TX-repoItemSBrelationships-parents">
{{ i18n.t('repositories.item_card.relationships.parents.count', { count: parentsCount || 0 }) }}
</div>
<a
v-if="permissions.can_connect_rows"
class="relationships-add-link btn-text-link font-normal"
@click="handleOpenAddRelationshipsModal($event, 'parent')">
@click="handleOpenAddRelationshipsModal($event, 'parent')"
data-e2e="e2e-TL-repoItemSBrelationships-addParents"
>
{{ i18n.t('repositories.item_card.add_relationship_button_text') }}
</a>
</div>
@ -193,13 +196,15 @@
<div class="font-inter text-sm leading-5 w-full">
<div class="flex flex-row justify-between" :class="{ 'mb-4': childrenCount }">
<div class="font-semibold">
<div class="font-semibold" data-e2e="e2e-TX-repoItemSBrelationships-children">
{{ i18n.t('repositories.item_card.relationships.children.count', { count: childrenCount || 0 }) }}
</div>
<a
v-if="permissions.can_connect_rows"
class="relationships-add-link btn-text-link font-normal"
@click="handleOpenAddRelationshipsModal($event, 'child')">
@click="handleOpenAddRelationshipsModal($event, 'child')"
data-e2e="e2e-TL-repoItemSBrelationships-addChildren"
>
{{ i18n.t('repositories.item_card.add_relationship_button_text') }}
</a>
</div>
@ -244,6 +249,7 @@
class="flex flex-row text-lg font-semibold w-[350px] mb-6 leading-7 items-center justify-between transition-colors duration-300"
ref="assigned-label"
id="assigned-label"
data-e2e="e2e-TX-repoItemSB-assigned"
>
{{ i18n.t('repositories.item_card.section.assigned', {
count: assignedModules ?
@ -255,7 +261,7 @@
'disabled': actions?.assign_repository_row && actions.assign_repository_row.disabled
}"
:data-assign-url="actions?.assign_repository_row ? actions.assign_repository_row.assign_url : ''"
:data-repository-row-id="repositoryRowId" @click="showRepositoryAssignModal">
:data-repository-row-id="repositoryRowId" @click="showRepositoryAssignModal" data-e2e="e2e-TL-repoItemSBassigned-assignToTask">
{{ i18n.t('repositories.item_card.assigned.assign') }}
</a>
</div>
@ -314,7 +320,7 @@
:class="{ 'pb-6': customColumns?.length }">
<div id="divider" class="w-500 bg-sn-light-grey flex px-8 items-center self-stretch h-px mb-6"></div>
<div id="bottom-button-wrapper" class="flex h-10 justify-end">
<button type="button" class="btn btn-primary print-label-button" data-e2e="e2e-BT-invInventoryItemSB-print"
<button type="button" class="btn btn-primary print-label-button" data-e2e="e2e-BT-repoItemSB-print"
:data-rows="JSON.stringify([repositoryRowId])"
:data-repository-id="repository?.id">
{{ i18n.t('repositories.item_card.print_label') }}

View file

@ -19,6 +19,7 @@
:searchPlaceholder="i18n.t('repositories.item_card.dropdown_placeholder')"
customClass="!h-[38px] !pl-3 sci-cursor-edit"
optionsClassName="max-h-[300px]"
:data-e2e="'e2e-IF-repoItemSBcustomColumns-input' + colId"
></select-search>
<div v-else-if="text"
class="text-sn-dark-grey font-inter text-sm font-normal leading-5"

View file

@ -24,7 +24,9 @@
:collapsed="collapsed"
@toggleExpandableState="toggleExpandableState"
@update="update"
className="px-3"/>
className="px-3"
:data-e2e="'e2e-IF-repoItemSBcustomColumns-input' + colId"
/>
</div>
<div v-else-if="colVal"
ref="numberRef"

View file

@ -19,6 +19,7 @@
:searchPlaceholder="i18n.t('repositories.item_card.dropdown_placeholder')"
customClass="!h-[38px] !pl-2 sci-cursor-edit"
optionsClassName="max-h-[300px]"
:data-e2e="'e2e-DD-repoItemSBcustomColumns-input' + colId"
></select-search>
<div v-else-if="status && icon"
class="flex flex-row items-center text-sn-dark-grey font-inter text-sm font-normal leading-5 gap-1.5">

View file

@ -26,6 +26,7 @@
@click="enableEditing"
:data-manage-stock-url="values?.stock_url"
:data-repository-row-id="repositoryId"
:data-e2e="'e2e-BT-repoItemSBcustomColumns-input' + colId"
>
<div v-if="values?.stock_formatted" :data-manage-stock-url="values?.stock_url"
class="text-sn-dark-grey font-inter text-sm font-normal leading-5 stock-value overflow-hidden text-ellipsis whitespace-nowrap">

View file

@ -25,7 +25,9 @@
:collapsed="collapsed"
@toggleExpandableState="toggleExpandableState"
@update="update"
className="px-3" />
className="px-3"
:data-e2e="'e2e-IF-repoItemSBcustomColumns-input' + colId"
/>
</div>
<div v-else-if="colVal?.view"
ref="textRef"

View file

@ -6,7 +6,9 @@
<div v-for="(navigationItem, index) in itemsToCreate" :key="navigationItem.textId"
@click="navigateToSection(navigationItem)"
class="text-sn-grey nav-text-item flex flex-col w-[130px] h-fit text-right hover:cursor-pointer"
:class="{ 'text-sn-science-blue': navigationItemsStatus[index] }">
:class="{ 'text-sn-science-blue': navigationItemsStatus[index] }"
:data-e2e="'e2e-BT-repoItemSB-' + navigationItem.labelAlias"
>
{{ i18n.t(`repositories.highlight_component.${navigationItem.labelAlias}`) }}
</div>
</div>

View file

@ -91,6 +91,8 @@
</template>
<script>
/* global HelperModule */
import DropdownSelector from '../shared/dropdown_selector.vue';
import LabelPreview from '../label_template/components/label_preview.vue';
@ -211,8 +213,14 @@ export default {
this.labelTemplateError = null;
this.labelTemplateCode = result.label_code;
}).fail((result) => {
if (result.responseJSON) {
this.labelTemplateError = result.responseJSON.error;
this.labelTemplateCode = result.responseJSON.label_code;
} else {
this.labelTemplateError = null;
this.labelTemplateCode = null;
HelperModule.flashAlertMsg(this.i18n.t('repository_row.modal_print_label.general_error'), 'danger');
}
});
},
submitPrint() {
@ -241,6 +249,8 @@ export default {
$(this.$refs.modal).modal('hide');
this.$emit('close');
PrintProgressModal.init(data);
}).fail(() => {
HelperModule.flashAlertMsg(this.i18n.t('repository_row.modal_print_label.general_error'), 'danger');
});
}
});

View file

@ -1,5 +1,6 @@
<template>
<div class="result-wrapper p-4 mb-4 rounded pr-8 relative"
<div ref="resultContainer"
class="result-wrapper p-4 mb-4 rounded pr-8 relative"
@drop.prevent="dropFile"
@dragenter.prevent="dragEnter($event)"
@dragover.prevent
@ -435,9 +436,10 @@ export default {
HelperModule.flashAlertMsg(this.i18n.t('errors.general'), 'danger');
}).done(() => {
this.$parent.$nextTick(() => {
const children = this.$refs.stepContainer.querySelectorAll('.result-element');
const children = this.$refs.resultContainer.querySelectorAll('.result-element');
const lastChild = children[children.length - 1];
lastChild.$el.scrollIntoView(false);
lastChild.scrollIntoView(false);
window.scrollBy({
top: 200,
behavior: 'smooth'

View file

@ -34,8 +34,8 @@
class="sci-toggle-checkbox"
:disabled="!canShare"
tabindex="0"
@change="checkboxChange"
@keyup.enter="handleCheckboxEnter"/>
@click.prevent="checkboxChange"
@keyup.enter="checkboxChange"/>
<span class="sci-toggle-checkbox-label"></span>
</span>
</div>
@ -199,12 +199,8 @@ export default {
this.characterCount = this.$refs.textarea.value.length;
});
},
handleCheckboxEnter() {
this.sharedEnabled = !this.sharedEnabled;
this.checkboxChange();
},
checkboxChange() {
if (this.sharedEnabled) {
if (!this.sharedEnabled) {
$.post(this.shareableLinkUrl, { description: this.description }, (result) => {
this.shareableData = result.data;
this.$emit('enable');
@ -224,6 +220,7 @@ export default {
this.shareableData = {};
this.description = '';
this.dirty = false;
this.sharedEnabled = false;
this.$emit('disable');
this.$emit('close');

View file

@ -33,6 +33,7 @@
@attachment:delete="deleteAttachment(attachment.id)"
@attachment:moved="attachmentMoved"
@attachment:uploaded="$emit('attachment:uploaded')"
@attachment:changed="$emit('attachment:changed', $event)"
/>
</div>
</div>

View file

@ -1,5 +1,8 @@
<template>
<div class="asset-context-menu" ref="menu">
<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"
ref="marvinjsEditButton"
@ -30,21 +33,41 @@
@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"
@viewMode="changeViewMode"
@move="showMoveModal"
@menu-visibility-changed="$emit('menu-visibility-changed', $event)"
></MenuDropdown>
<Teleport to="body">
<deleteAttachmentModal
v-if="deleteModal"
:fileName="attachment.attributes.file_name"
@confirm="deleteAttachment"
@cancel="deleteModal = false"
/>
<moveAssetModal v-if="movingAttachment"
<moveAssetModal
v-if="movingAttachment"
:parent_type="attachment.attributes.parent_type"
:targets_url="attachment.attributes.urls.move_targets"
@confirm="moveAttachment($event)" @cancel="closeMoveModal"/>
@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>
@ -52,12 +75,17 @@
import deleteAttachmentModal from './delete_modal.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';
export default {
name: 'contextMenu',
components: { deleteAttachmentModal, moveAssetModal, MenuDropdown },
mixins: [MoveMixin],
components: {
deleteAttachmentModal,
moveAssetModal,
MenuDropdown
},
mixins: [MoveMixin, OpenLocallyMixin],
props: {
attachment: {
type: Object,
@ -101,6 +129,16 @@ export default {
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');
menu.push({
text,
emit: 'open_locally'
});
}
menu.push({
text: this.i18n.t('Download'),
url: this.attachment.attributes.urls.download,

View file

@ -0,0 +1,115 @@
import axios from '../../../../../packs/custom_axios.js';
import { satisfies } from 'compare-versions';
import editLaunchingApplicationModal from '../../modal/edit_launching_application_modal.vue';
import NoPredefinedAppModal from '../../modal/no_predefined_app_modal.vue';
import UpdateVersionModal from '../../modal/update_version_modal.vue';
export default {
data() {
return {
localAppName: null,
scinoteEditRunning: false,
scinoteEditVersion: null,
showNoPredefinedAppModal: false,
showUpdateVersionModal: false,
editAppModal: false,
pollingInterval: null,
};
},
components: {
editLaunchingApplicationModal,
NoPredefinedAppModal,
UpdateVersionModal
},
computed: {
attributes() {
return this.attachment.attributes;
},
canOpenLocally() {
return this.scinoteEditRunning
&& !!this.attributes.urls.open_locally
&& this.attributes.asset_type !== 'gene_sequence'
&& this.attributes.asset_type !== 'marvinjs';
}
},
beforeUnmount() {
this.stopPolling();
},
methods: {
async fetchLocalAppInfo() {
try {
const statusResponse = await axios.get(
`${this.attributes.urls.open_locally_api}/status`
);
if (statusResponse.status === 200) {
this.scinoteEditRunning = true;
this.scinoteEditVersion = statusResponse.data.version;
} else {
return;
}
const response = await axios.get(
`${this.attributes.urls.open_locally_api}/default-application/${this.attributes.file_extension}`
);
if (response.data.application.toLowerCase() !== 'pick an app') {
this.localAppName = response.data.application;
}
} catch (error) {
if (error.response?.status === 404) return; // all good, no app was found for the file
console.error('Error in request: ', error);
}
},
async openLocally() {
if (this.isWrongVersion(this.scinoteEditVersion)) {
this.showUpdateVersionModal = true;
return;
} else if (this.localAppName === null) {
this.showNoPredefinedAppModal = true;
return;
}
this.editAppModal = true;
try {
this.startPolling();
const { data } = await axios.get(this.attributes.urls.open_locally);
await axios.post(`${this.attributes.urls.open_locally_api}/download`, data);
} catch (error) {
console.error('Error in request:', error);
}
},
isWrongVersion(version) {
const { min, max } = this.attributes.edit_version_range;
return !satisfies(version, `${min} - ${max}`);
},
async pollForChanges() {
try {
const checksumResponse = await axios.get(this.attributes.urls.asset_checksum);
if (checksumResponse.status === 200) {
const currentChecksum = checksumResponse.data.checksum;
if (currentChecksum !== this.attributes.checksum) {
this.$emit('attachment:changed', this.attachment.id);
}
}
} catch (error) {
console.error('Error polling for changes:', error);
}
},
startPolling() {
if (this.pollingInterval === null) {
this.pollingInterval = setInterval(this.pollForChanges, GLOBAL_CONSTANTS.ASSET_POLLING_INTERVAL);
}
},
stopPolling() {
if (this.pollingInterval !== null) {
clearInterval(this.pollingInterval);
this.pollingInterval = null;
}
}
}
}

View file

@ -0,0 +1,110 @@
<template>
<div class="sn-open-locally-menu" @mouseenter="fetchLocalAppInfo">
<div v-if="!canOpenLocally && (attachment.attributes.wopi && attachment.attributes.urls.edit_asset)">
<a :href="`${attachment.attributes.urls.edit_asset}`" target="_blank"
class="block whitespace-nowrap rounded px-3 py-2.5
hover:!text-sn-blue hover:no-underline cursor-pointer hover:bg-sn-super-light-grey">
{{ attachment.attributes.wopi_context.button_text }}
</a>
</div>
<div v-else-if="!usesWebIntegration">
<MenuDropdown
v-if="this.menu.length > 1"
class="ml-auto"
:listItems="this.menu"
:btnClasses="`btn btn-light icon-btn`"
:position="'right'"
:btnText="i18n.t('attachments.open_in')"
:caret="true"
@open-locally="openLocally"
@open-image-editor="openImageEditor"
></MenuDropdown>
<a v-else-if="menu.length === 1" class="btn btn-light" :href="menu[0].url" :target="menu[0].url_target" @click="this[this.menu[0].emit]()">
{{ menu[0].text }}
</a>
</div>
<Teleport to="body">
<NoPredefinedAppModal
v-if="showNoPredefinedAppModal"
:fileName="attachment.attributes.file_name"
@close="showNoPredefinedAppModal = false"
/>
<editLaunchingApplicationModal
v-if="editAppModal"
:fileName="attachment.attributes.file_name"
:application="this.localAppName"
@close="editAppModal = false"
/>
<UpdateVersionModal
v-if="showUpdateVersionModal"
@close="showUpdateVersionModal = false"
/>
</Teleport>
</div>
</template>
<script>
import OpenLocallyMixin from './mixins/open_locally.js';
import MenuDropdown from '../../menu_dropdown.vue';
import UpdateVersionModal from '../modal/update_version_modal.vue';
export default {
name: 'OpenLocallyMenu',
mixins: [OpenLocallyMixin],
components: { MenuDropdown, UpdateVersionModal },
props: {
attachment: { type: Object, required: true },
disableLocalOpen: { type: Boolean, default: false }
},
created() {
this.fetchLocalAppInfo();
window.openLocallyMenu = this;
},
beforeUnmount() {
delete window.openLocallyMenuComponent;
},
computed: {
menu() {
const menu = [];
if (this.attachment.attributes.wopi && this.attachment.attributes.urls.edit_asset) {
menu.push({
text: this.attachment.attributes.wopi_context.button_text,
url: this.attachment.attributes.urls.edit_asset,
url_target: '_blank'
});
}
if (this.attachment.attributes.image_editable) {
menu.push({
text: this.i18n.t('assets.file_preview.edit_in_scinote'),
emit: 'openImageEditor'
});
}
if (this.canOpenLocally && !this.disableLocalOpen) {
const text = this.localAppName
? this.i18n.t('attachments.open_locally_in', { application: this.localAppName })
: this.i18n.t('attachments.open_locally');
menu.push({
text,
emit: 'openLocally'
});
}
return menu;
},
usesWebIntegration() {
return this.attachment.attributes.asset_type === 'gene_sequence'
|| this.attachment.attributes.asset_type === 'marvinjs';
}
},
methods: {
openImageEditor() {
document.getElementById('editImageButton').click();
}
}
};
</script>

View file

@ -1,8 +1,9 @@
<template>
<div class="attachment-container asset"
:data-asset-id="attachment.id"
@mouseover="showOptions = true"
@mouseenter="handleMouseEnter"
@mouseleave="handleMouseLeave"
v-click-outside="handleClickOutsideThumbnail"
>
<a :class="{ hidden: showOptions }"
:href="attachment.attributes.urls.blob"
@ -44,20 +45,39 @@
{{ 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-if="this.attachment.attributes.wopi && this.attachment.attributes.urls.edit_asset"
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-edit"></i>
<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-edit"></i>
<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"
@ -67,11 +87,11 @@
:data-sketch-name="attachment.attributes.metadata.name"
:data-sketch-description="attachment.attributes.metadata.description"
>
<i class="sn-icon sn-icon-edit"></i>
<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.edit')"
: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"
@ -79,7 +99,7 @@
: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-edit"></i>
<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>
@ -105,21 +125,52 @@
@attachment:delete="deleteAttachment"
@attachment:moved="attachmentMoved"
@attachment:uploaded="reloadAttachments"
@attachment:changed="$emit('attachment:changed', $event)"
@menu-visibility-changed="handleMenuVisibilityChange"
:withBorder="true"
/>
<Teleport to="body">
<deleteAttachmentModal
v-if="deleteModal"
:fileName="attachment.attributes.file_name"
@confirm="deleteAttachment"
@cancel="deleteModal = false"
/>
<moveAssetModal v-if="movingAttachment"
<moveAssetModal
v-if="movingAttachment"
:parent_type="attachment.attributes.parent_type"
:targets_url="attachment.attributes.urls.move_targets"
@confirm="moveAttachment($event)" @cancel="closeMoveModal"/>
@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'
&& 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>
</div>
</template>
<script>
@ -127,13 +178,21 @@ import AttachmentMovedMixin from './mixins/attachment_moved.js';
import ContextMenuMixin from './mixins/context_menu.js';
import ContextMenu from './context_menu.vue';
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 { vOnClickOutside } from '@vueuse/components';
export default {
name: 'thumbnailAttachment',
mixins: [ContextMenuMixin, AttachmentMovedMixin, MoveMixin],
components: { ContextMenu, deleteAttachmentModal, MoveAssetModal },
mixins: [ContextMenuMixin, AttachmentMovedMixin, MoveMixin, OpenLocallyMixin],
components: {
ContextMenu,
deleteAttachmentModal,
MoveAssetModal,
MenuDropdown
},
props: {
attachment: {
type: Object,
@ -147,10 +206,44 @@ export default {
data() {
return {
showOptions: false,
isMenuOpen: false,
deleteModal: false
deleteModal: false,
isMenuOpen: false
};
},
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'
});
}
return options;
},
},
mounted() {
$(this.$nextTick(() => {
$('.attachment-preview img')
@ -159,7 +252,7 @@ export default {
}));
},
watch: {
isHovered(newValue) {
showOptions(newValue) {
// reload thumbnail on mouse out
if (newValue) return;
@ -174,15 +267,33 @@ export default {
openOVEditor(url) {
window.showIFrameModal(url);
},
openScinoteEditor() {
this.$refs.imageEditButton.click();
},
handleMouseLeave() {
if (!this.isMenuOpen) {
this.showOptions = false;
}
},
handleMenuVisibilityChange(newValue) {
this.isMenuOpen = newValue;
this.showOptions = newValue;
async handleMouseEnter() {
await this.fetchLocalAppInfo();
this.showOptions = true;
},
handleMenuVisibilityChange({ isMenuOpen, showOptions }) {
if (isMenuOpen !== null) {
this.isMenuOpen = isMenuOpen;
}
if (showOptions !== null) {
this.showOptions = showOptions;
}
},
handleClickOutsideThumbnail(event) {
const isClickInsideModal = event.target.closest('.modal');
if (!isClickInsideModal) {
this.showOptions = false;
this.isMenuOpen = false;
}
},
}
};
</script>

View file

@ -30,7 +30,7 @@
@delete="showDeleteModal"
></MenuDropdown>
</div>
<div v-if="element.attributes.orderable.urls.create_item_url || orderedChecklistItems.length > 0" :class="{ 'pointer-events-none': locked }">
<div v-if="element.attributes.orderable.urls.create_item_url || checklistItems.length > 0" :class="{ 'pointer-events-none': locked }">
<Draggable
v-model="checklistItems"
:ghostClass="'checklist-item-ghost'"
@ -63,8 +63,8 @@
<div v-if="element.attributes.orderable.urls.create_item_url && !addingNewItem"
class="flex items-center gap-1 text-sn-blue cursor-pointer mb-2 mt-1 "
tabindex="0"
@keyup.enter="addItem(orderedChecklistItems.length + 1)"
@click="addItem(orderedChecklistItems.length + 1)">
@keyup.enter="addItem(checklistItems[checklistItems.length - 1]?.id)"
@click="addItem(checklistItems[checklistItems.length - 1]?.id)">
<i class="sn-icon sn-icon-new-task w-6 text-center inline-block"></i>
{{ i18n.t('protocols.steps.insert.checklist_item') }}
</div>
@ -81,6 +81,9 @@
</template>
<script>
/* global HelperModule I18n */
import Draggable from 'vuedraggable';
import DeleteMixin from './mixins/delete.js';
import MoveMixin from './mixins/move.js';
@ -90,6 +93,7 @@ import InlineEdit from '../inline_edit.vue';
import ChecklistItem from './checklistItem.vue';
import moveElementModal from './modal/move.vue';
import MenuDropdown from '../menu_dropdown.vue';
import axios from '../../../packs/custom_axios.js';
export default {
name: 'Checklist',
@ -128,7 +132,7 @@ export default {
},
created() {
if (this.isNew) {
this.addItem(1);
this.addItem();
} else {
this.loadChecklistItems();
}
@ -139,13 +143,6 @@ export default {
}
},
computed: {
orderedChecklistItems() {
return this.checklistItems.sort((a, b) => a.attributes.position - b.attributes.position || b.id - a.id)
.map((item, index) => {
item.attributes.position = index + 1;
return item;
});
},
locked() {
return this.editingName || !this.element.attributes.orderable.urls.update_url;
},
@ -199,20 +196,23 @@ export default {
this.update();
},
postItem(item) {
item.attributes.position = item.attributes.position - 1;
$.post(this.element.attributes.orderable.urls.create_item_url, item).done((result) => {
this.loadChecklistItems(result.data[result.data.length - 1].attributes.position);
}).fail((e) => {
const position = this.checklistItems.findIndex((i) => i.id === item.id);
let afterId = null;
if (position > 0) {
afterId = this.checklistItems[position - 1].id;
}
axios.post(this.element.attributes.orderable.urls.create_item_url, {
attributes: item.attributes,
after_id: afterId
}).then((result) => {
this.loadChecklistItems(result.data.data[result.data.data.length - 1].id);
}).catch(() => {
HelperModule.flashAlertMsg(this.i18n.t('errors.general'), 'danger');
});
// Fake element during loading
item.id = `new${Math.floor(Math.random() * 1000000000)}`;
this.checklistItems.push(item);
},
saveItem(item, key) {
if (item.id > 0) {
const insertAfter = key === 'Enter' ? item.attributes.position : null;
const insertAfter = key === 'Enter' ? item.id : null;
$.ajax({
url: item.attributes.urls.update_url,
type: 'PATCH',
@ -220,7 +220,7 @@ export default {
success: () => {
this.loadChecklistItems(insertAfter);
},
error: (xhr) => setFlashErrors(xhr.responseJSON.errors)
error: (xhr) => this.setFlashErrors(xhr.responseJSON.errors)
});
} else {
this.postItem(item, key);
@ -240,20 +240,23 @@ export default {
});
},
addItem(insertAfter) {
this.checklistItems.push(
const afterIndex = this.checklistItems.findIndex((i) => i.id === insertAfter);
this.checklistItems.splice(
afterIndex + 1,
0,
{
id: `new${Math.floor(Math.random() * 1000000000)}`,
attributes: {
text: '',
checked: false,
position: insertAfter,
isNew: true
isNew: true,
with_paragraphs: false
}
}
);
this.checklistItems = this.orderedChecklistItems;
},
removeItem(position) {
this.checklistItems = this.orderedChecklistItems.filter((item) => item.attributes.position !== position);
this.checklistItems = this.checklistItems.filter((item) => item.attributes.position !== position);
},
startReorder() {
this.reordering = true;
@ -265,21 +268,26 @@ export default {
&& Number.isInteger(event.oldIndex)
&& event.newIndex !== event.oldIndex
) {
const position = this.orderedChecklistItems[event.newIndex]?.attributes.position;
const id = this.checklistItems[event.oldIndex]?.id;
this.checklistItems[event.oldIndex].attributes.position = position + (event.newIndex > event.oldIndex ? 1 : -1);
this.saveItemOrder(id, position);
let afterId = null;
if (event.newIndex > 0) {
if (event.newIndex > event.oldIndex) {
afterId = this.checklistItems[event.newIndex - 1].id;
} else {
afterId = this.checklistItems[event.newIndex + 1].id;
}
}
const id = this.checklistItems[event.newIndex]?.id;
this.saveItemOrder(id, afterId);
}
},
saveItemOrder(id, position) {
$.ajax({
type: 'POST',
url: this.element.attributes.orderable.urls.reorder_url,
data: JSON.stringify({ attributes: { id, position } }),
contentType: 'application/json',
dataType: 'json',
error: (xhr) => this.setFlashErrors(xhr.responseJSON.errors),
success: () => this.loadChecklistItems()
saveItemOrder(id, afterId) {
axios.post(this.element.attributes.orderable.urls.reorder_url, {
id,
after_id: afterId
}).then(() => {
this.loadChecklistItems();
}).catch((e) => {
this.setFlashErrors(e.response.errors);
});
},
setFlashErrors(errors) {

View file

@ -1,6 +1,6 @@
<template>
<div class="content__checklist-item pl-10 ml-[-2.325rem]">
<div class="checklist-item-header flex rounded items-center relative w-full group/checklist-item-header" :class="{ 'locked': locked || editingText, 'editing-name': editingText }">
<div class="content__checklist-item pl-10 ml-[-2.325rem] group/checklist-item-header">
<div class="checklist-item-header flex rounded items-center relative w-full" :class="{ 'locked': locked || editingText, 'editing-name': editingText }">
<div v-if="reorderChecklistItemUrl"
class="absolute h-6 cursor-grab justify-center left-[-2.325rem] top-0.5 px-2 tw-hidden text-sn-grey element-grip step-element-grip--draggable"
:class="{ 'group-hover/checklist-item-header:flex': (!locked && !editingText && draggable) }"
@ -8,7 +8,8 @@
<i class="sn-icon sn-icon-drag"></i>
</div>
<div class="flex items-start gap-2 grow" :class="{ 'done': checklistItem.attributes.checked }">
<div v-if="!inRepository" class="sci-checkbox-container my-1.5 border-0 border-y border-transparent border-solid" :class="{ 'disabled': !toggleUrl }" :style="toggleUrl && 'pointer-events: initial'">
<div v-if="!inRepository" class="sci-checkbox-container my-1.5 border-0 border-y border-transparent border-solid"
:class="{ 'disabled': !toggleUrl }" :style="toggleUrl && 'pointer-events: initial'">
<input ref="checkbox"
type="checkbox"
class="sci-checkbox"
@ -42,9 +43,10 @@
@delete="removeItem()"
@keypress="keyPressHandler"
@blur="onBlurHandler"
@paste="pasteHandler"
/>
<span v-if="!editingText && (!checklistItem.attributes.urls || deleteUrl)" class="absolute right-0 top-0.5 leading-6 tw-hidden group-hover/checklist-item-header:inline-block !text-sn-blue cursor-pointer" @click="showDeleteModal" tabindex="0">
<span v-if="!editingText && (!checklistItem.attributes.urls || deleteUrl)"
class="absolute right-0 top-0.5 leading-6 tw-hidden group-hover/checklist-item-header:inline-block !text-sn-blue cursor-pointer"
@click="showDeleteModal" tabindex="0">
<i class="sn-icon sn-icon-delete"></i>
</span>
</div>
@ -127,8 +129,7 @@ export default {
disableTextEdit() {
if (this.checklistItem.attributes.isNew) {
if (this.deleting) return;
this.removeItem();
if (this.checklistItem.attributes.text.length === 0) this.removeItem();
this.$emit('editEnd');
}
},
@ -162,15 +163,9 @@ export default {
this.$emit('update', this.checklistItem, withKey);
},
keyPressHandler(e) {
if (
((e.shiftKey || e.metaKey) && e.key === 'Enter')
|| ((e.ctrlKey || e.metaKey) && e.key === 'v')
) {
if ((e.shiftKey || e.metaKey) && e.key === 'Enter') {
this.checklistItem.attributes.with_paragraphs = true;
}
},
pasteHandler() {
this.checklistItem.attributes.with_paragraphs = true;
}
}
};

View file

@ -14,7 +14,7 @@
</div>
<div class="modal-footer">
<button class="btn btn-secondary" @click="cancel">{{ i18n.t('general.cancel') }}</button>
<button class="btn btn-secondary" @click="close">{{ i18n.t('general.cancel') }}</button>
<button class="btn btn-danger" @click="confirm">{{ i18n.t('protocols.steps.modals.delete_element.confirm')}}</button>
</div>
</div>
@ -22,21 +22,15 @@
</div>
</template>
<script>
import modalMixin from '../../modal_mixin';
export default {
name: 'deleteElementModal',
mounted() {
$(this.$refs.modal).modal('show');
$(this.$refs.modal).on('hidden.bs.modal', () => {
this.$emit('cancel');
});
},
mixins: [modalMixin],
methods: {
confirm() {
$(this.$refs.modal).modal('hide');
this.$emit('confirm');
},
cancel() {
$(this.$refs.modal).modal('hide');
this.$nextTick(() => this.close);
}
}
};

View file

@ -0,0 +1,35 @@
<template>
<div ref="modal" @keydown.esc="close" class="modal" role="dialog" aria-hidden="true" tabindex="-1">
<div class="modal-dialog">
<div 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>
<h2 class="modal-title">{{ i18n.t('assets.edit_launching_application_modal.title') }}</h2>
</div>
<div class="modal-body">
<p v-html="i18n.t(
'assets.edit_launching_application_modal.description',
{ file_name: fileName, application: application }
)"></p>
</div>
<div class="modal-footer">
<button type='button' class='btn btn-secondary' @click="close">
{{ i18n.t('general.close') }}
</button>
</div>
</div>
</div>
</div>
</template>
<script>
import modalMixin from '../../modal_mixin';
export default {
name: 'editLaunchingApplicationModal',
props: {
fileName: String, application: String,
},
mixins: [modalMixin]
};
</script>

View file

@ -0,0 +1,32 @@
<template>
<div ref="modal" @keydown.esc="cancel" class="modal" id="modalNoPredefinedApp" tabindex="-1" role="dialog">
<div class="modal-dialog modal-md" role="document">
<div 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" id="modal-delete-result-element">
{{ i18n.t('assets.no_predefined_app_modal.set_up_app') }}
</h4>
</div>
<div class="modal-body">
<p v-html="i18n.t('assets.no_predefined_app_modal.body_text_html', { file_name: fileName })"></p>
</div>
<div class="modal-footer">
<button class="btn btn-primary" @click="close">{{ this.i18n.t('assets.no_predefined_app_modal.understand_button') }}</button>
</div>
</div>
</div>
</div>
</template>
<script>
import modalMixin from '../../modal_mixin';
export default {
name: 'NoPredefinedAppModal',
mixins: [modalMixin],
props: {
fileName: String
}
};
</script>

View file

@ -0,0 +1,45 @@
<template>
<div ref="modal" @keydown.esc="cancel" class="modal" id="modalUpdateVersion" tabindex="-1" role="dialog">
<div class="modal-dialog modal-md" role="document">
<div 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" id="modal-delete-result-element">
{{ i18n.t('assets.update_version_modal.title') }}
</h4>
</div>
<div class="modal-body">
<p v-html="i18n.t('assets.update_version_modal.body_text_html')"></p>
</div>
<div class="modal-footer">
<button class="btn btn-secondary" @click="close">{{ i18n.t('general.cancel') }}</button>
<ScinoteEditDownload
:data="userAgent"
:isUpdateVersionModal="true"
/>
</div>
</div>
</div>
</div>
</template>
<script>
import ScinoteEditDownload from '../../../../vue/shared/scinote_edit_download.vue';
import modalMixin from '../../modal_mixin';
export default {
name: 'UpdateVersionModal',
components: {
ScinoteEditDownload
},
mixins: [modalMixin],
props: {
fileName: String
},
computed: {
userAgent() {
return window.navigator.userAgent;
}
}
};
</script>

View file

@ -270,8 +270,8 @@ export default {
this.newValue = this.$refs.input.value.trim(); // Fix for smart annotation
this.editing = false;
this.$emit('editingDisabled');
this.$emit('update', this.newValue, withKey);
this.$emit('editingDisabled');
},
refreshTexareaHeight() {
if (this.editing && !this.singleLine) {

View file

@ -1,5 +1,5 @@
<template>
<div class="relative" v-if="listItems.length > 0" v-click-outside="closeMenuAndEmit">
<div class="relative" v-if="listItems.length > 0" v-click-outside="closeMenu">
<button
ref="openBtn"
:class="btnClasses"
@ -79,6 +79,7 @@ export default {
btnClasses: { type: String, default: 'btn btn-light' },
btnText: { type: String, required: false },
btnIcon: { type: String, required: false },
title: { type: String, default: '' },
caret: { type: Boolean, default: false }
},
data() {
@ -92,9 +93,10 @@ export default {
},
watch: {
showMenu(newValue) {
if (newValue) {
this.$emit('menu-visibility-changed', newValue);
}
this.$emit('menu-visibility-changed', {
isMenuOpen: newValue,
showOptions: newValue ? true : null
});
if (this.showMenu) {
this.openUp = false;
@ -115,14 +117,6 @@ export default {
closeMenu() {
this.showMenu = false;
},
closeMenuAndEmit(event) {
const isClickInsideModal = event.target.closest('.modal');
if (!isClickInsideModal) {
this.showMenu = false;
this.$emit('menu-visibility-changed', false);
}
},
handleClick(event, item) {
if (!item.url) {
event.preventDefault();

View file

@ -0,0 +1,118 @@
<template>
<div class="buttons">
<template v-if="isWindows">
<a :href="getWindowsHref"
class="btn btn-primary new-project-btn"
:title="i18n.t('users.settings.account.addons.desktop_app.windows_button')"
role="button"
target="_blank">
<span class="hidden-xs">{{ i18n.t('users.settings.account.addons.desktop_app.windows_button') }}</span>
</a>
<div v-if="showButtonLabel" class="text-xs pt-2 pb-6" style="color: var(--sn-sleepy-grey);">
{{ i18n.t('users.settings.account.addons.desktop_app.version', { version: this.responseData[0]['version']}) }}
</div>
</template>
<template v-else-if="isMac">
<a :href="getMacHref"
class="btn btn-primary new-project-btn"
:title="i18n.t('users.settings.account.addons.desktop_app.macos_button')"
role="button"
target="_blank">
<span class="hidden-xs">{{ i18n.t('users.settings.account.addons.desktop_app.macos_button') }}</span>
</a>
<div v-if="showButtonLabel" class="text-xs pt-2 pb-6" style="color: var(--sn-sleepy-grey);">
{{ i18n.t('users.settings.account.addons.desktop_app.version', { version: this.responseData[1]['version']}) }}
</div>
</template>
<template v-else>
<div class="flex">
<div>
<a :href="getWindowsHref"
class="btn btn-primary new-project-btn"
:title="i18n.t('users.settings.account.addons.desktop_app.windows_button')"
role="button"
target="_blank">
<span class="hidden-xs">{{ i18n.t('users.settings.account.addons.desktop_app.windows_button') }}</span>
</a>
<div v-if="showButtonLabel" class="text-xs pt-2 pb-6" style="color: var(--sn-sleepy-grey);">
{{ i18n.t('users.settings.account.addons.desktop_app.version',
{ version: this.responseData[0]['version']})
}}
</div>
</div>
<div class="ml-2">
<a :href="getMacHref"
class="btn btn-primary new-project-btn"
:title="i18n.t('users.settings.account.addons.desktop_app.macos_button')"
role="button"
target="_blank">
<span class="hidden-xs">{{ i18n.t('users.settings.account.addons.desktop_app.macos_button') }}</span>
</a>
<p v-if="showButtonLabel" class="text-xs pt-2 pb-6" style="color: var(--sn-sleepy-grey);">
{{ i18n.t('users.settings.account.addons.desktop_app.version',
{ version: this.responseData[1]['version']})
}}
</p>
</div>
</div>
</template>
<a v-if="!isUpdateVersionModal" :href="'https://knowledgebase.scinote.net/en/knowledge/how-to-use-scinote-edit'"
:title="i18n.t('users.settings.account.addons.more_info')"
class="text-sn-blue"
target="_blank">
<span class="sn-icon sn-icon-open"></span>
{{ i18n.t('users.settings.account.addons.more_info') }}
</a>
</div>
</template>
<script>
export default {
name: 'ScinoteEditDownload',
props: {
data: { type: String, required: true },
isUpdateVersionModal: { type: Boolean, required: false }
},
data() {
return {
userAgent: this.data,
responseData: []
};
},
computed: {
isWindows() {
return /Windows/.test(this.userAgent);
},
isMac() {
return /Mac OS/.test(this.userAgent);
},
showButtonLabel() {
return this.responseData && this.responseData.length > 0 && !this.isUpdateVersionModal;
},
getWindowsHref() {
return this.responseData && this.responseData.length > 0 ? this.responseData[0].url : '#';
},
getMacHref() {
return this.responseData && this.responseData.length > 0 ? this.responseData[1].url : '#';
}
},
created() {
window.scinoteEditDownload = this;
this.fetchData();
},
beforeUnmount() {
delete window.scinoteEditDownloadComponent;
},
methods: {
fetchData() {
$.get('https://extras.scinote.net/scinote-edit/latest.json', (result) => {
this.responseData = result;
});
}
}
};
</script>

View file

@ -1,6 +1,7 @@
<template>
<Select
class="sn-select sn-select--search hover:border-sn-sleepy-grey"
tabindex="0"
class="sn-select sn-select--search hover:border-sn-sleepy-grey focus:outline-none focus:ring-0 focus:border-sn-sleepy-grey"
:class="customClass"
:className="className"
:optionsClassName="optionsClassName"

View file

@ -0,0 +1,81 @@
<template>
<div ref="modal" class="modal" tabindex="-1" role="dialog" data-backdrop="static" data-keyboard="false">
<div class="modal-dialog !w-[900px]" role="document">
<div class="modal-content !p-0 grid grid-cols-3">
<div class="bg-sn-super-light-grey p-6 mb-1.5">
<div class="flex justify-start mb-1.5">
<h3 class="modal-title">{{ config.title }}</h3>
</div>
<div v-if="config.subtitle" class="text-sn-dark-grey">
{{ config.subtitle }}
</div>
<div class="flex flex-col mt-4">
<div v-for="(step, index) in config.steps" :key="step.id">
<div v-if="index > 0"
class="ml-0.5 left-4 relative h-8 w-0 border border-r-0 border-solid"
:class="{
'!border-sn-dark-grey': index <= activeStep,
'!border-sn-sleepy-grey': index > activeStep
}"
></div>
<div class="flex items-center gap-3">
<div class="rounded bg-white border border-sn-sleepy-grey p-1.5">
<i :class="[
step.icon,
{
'text-sn-dark-grey': index <= activeStep,
'text-sn-grey': index > activeStep
}
]"></i>
</div>
<span
class="font-bold text-xs"
:class="{
'text-sn-dark-grey': index <= activeStep,
'text-sn-grey': index > activeStep
}"
>
{{ step.label }}
</span>
</div>
</div>
</div>
</div>
<div class="col-span-2 p-6 flex flex-col">
<component
:is="config.steps[activeStep].component"
:params="params"
:wizardComponent="this"
@close="close"
@back="activeStep -= 1"
@next="activeStep += 1"
/>
</div>
</div>
</div>
</div>
</template>
<script>
import modalMixin from './modal_mixin';
export default {
name: 'WizardModal',
props: {
params: {
type: Object,
required: true
},
config: {
type: Object,
required: true
}
},
mixins: [modalMixin],
data() {
return {
activeStep: 0
};
}
};
</script>

View file

@ -1,20 +0,0 @@
AssetTextExtractionJob = Struct.new(:asset_id, :in_template) do
def perform
asset = Asset.find_by(id: asset_id)
return unless asset.present? && asset.file.attached?
asset.extract_asset_text(in_template)
end
def queue_name
'assets'
end
def max_attempts
1
end
def max_run_time
5.minutes
end
end

View file

@ -120,7 +120,7 @@ module Protocols
asset.file.attach(io: StringIO.new(Base64.decode64(step_element_json['contents'])), filename: 'file.blob')
asset.save!
step.step_assets.create!(asset: asset)
asset.post_process_file(@protocol.team)
asset.post_process_file
end
def create_step_orderable_element!(step, orderable)

View file

@ -18,7 +18,6 @@ module Reports
Reports::Docx.new(report, docx, user: user, scinote_url: root_url).draw
docx.save
report.docx_file.attach(io: file, filename: 'report.docx')
report.docx_ready!
report_path = Rails.application.routes.url_helpers
.reports_path(team: report.team.id, preview_report_id: report.id, preview_type: :docx)
@ -37,6 +36,7 @@ module Reports
)
Reports::DocxPreviewJob.perform_now(report.id)
report.docx_ready!
ensure
I18n.backend.date_format = nil
file.close

View file

@ -26,24 +26,18 @@ class Asset < ApplicationRecord
validate :wopi_filename_valid, on: :wopi_file_creation
validate :check_file_size, on: :on_api_upload
belongs_to :created_by,
foreign_key: 'created_by_id',
class_name: 'User',
optional: true
belongs_to :last_modified_by,
foreign_key: 'last_modified_by_id',
class_name: 'User',
optional: true
belongs_to :created_by, class_name: 'User', optional: true
belongs_to :last_modified_by, class_name: 'User', optional: true
belongs_to :team, optional: true
has_one :step_asset, inverse_of: :asset, dependent: :destroy
has_one :step, through: :step_asset, touch: true, dependent: :nullify
has_one :step, through: :step_asset, touch: true
has_one :result_asset, inverse_of: :asset, dependent: :destroy
has_one :result, through: :result_asset, touch: true, dependent: :nullify
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,
dependent: :nullify
has_one :repository_cell, through: :repository_asset_value
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
scope :sort_assets, lambda { |sort_value = 'new'|
sort = case sort_value
@ -56,7 +50,7 @@ class Asset < ApplicationRecord
joins(file_attachment: :blob).order(sort)
}
attr_accessor :file_content, :file_info, :in_template
attr_accessor :file_content, :file_info
before_save :reset_file_processing, if: -> { file.new_record? }
@ -229,7 +223,7 @@ class Asset < ApplicationRecord
raise ArgumentError, 'Destination asset should be persisted first!' unless to_asset.persisted?
file.blob.open do |tmp_file|
to_blob = ActiveStorage::Blob.create_and_upload!(io: tmp_file, filename: blob.filename, metadata: blob.metadata)
to_blob = ActiveStorage::Blob.create_and_upload!(io: tmp_file, filename: blob.filename)
to_asset.file.attach(to_blob)
end
@ -244,7 +238,7 @@ class Asset < ApplicationRecord
end
end
to_asset.post_process_file(to_asset.team)
to_asset.post_process_file
end
def image?
@ -279,19 +273,9 @@ class Asset < ApplicationRecord
pdf? || (previewable_document?(blob) && Rails.application.config.x.enable_pdf_previews)
end
def post_process_file(team = nil)
# Extract asset text if it's of correct type
if text?
Rails.logger.info "Asset #{id}: Creating extract text job"
# The extract_asset_text also includes
# estimated size calculation
Delayed::Job.enqueue(AssetTextExtractionJob.new(id, in_template))
elsif marvinjs?
extract_asset_text
else
def post_process_file
# Update asset's estimated size immediately
update_estimated_size(team)
end
update_estimated_size unless text? || marvinjs?
if Rails.application.config.x.enable_pdf_previews && previewable_document?(blob)
PdfPreviewJob.perform_later(id)
@ -299,43 +283,10 @@ class Asset < ApplicationRecord
end
end
def extract_asset_text(in_template = false)
self.in_template = in_template
if marvinjs?
mjs_doc = Nokogiri::XML(file.metadata[:description])
mjs_doc.remove_namespaces!
text_data = mjs_doc.search("//Field[@name='text']").collect(&:text).join(' ')
else
blob.open do |tmp_file|
text_data = Yomu.new(tmp_file.path).text
end
end
if asset_text_datum.present?
# Update existing text datum if it exists
asset_text_datum.update(data: text_data)
else
# Create new text datum
AssetTextDatum.create(data: text_data, asset: self)
end
Rails.logger.info "Asset #{id}: Asset file successfully extracted"
# Finally, update asset's estimated size to include
# the data vector
update_estimated_size(team)
rescue StandardError => e
Rails.logger.fatal(
"Asset #{id}: Error extracting contents from asset "\
"file #{file.blob.key}: #{e.message}"
)
end
# If team is provided, its space_taken
# is updated as well
def update_estimated_size(team = nil)
return if file_size.blank? || in_template
def update_estimated_size
return if file_size.blank?
es = file_size
if asset_text_datum.present? && asset_text_datum.persisted?
@ -465,6 +416,10 @@ class Asset < ApplicationRecord
(result || step)&.my_module
end
def parent
step || result || repository_cell
end
private
def tempdir

View file

@ -0,0 +1,33 @@
# frozen_string_literal: true
class AssetSyncToken < ApplicationRecord
belongs_to :user
belongs_to :asset
after_initialize :generate_token
after_initialize :set_default_expiration
validates :token, uniqueness: true, presence: true
def version_token
asset.file.checksum
end
def token_valid?
!revoked_at? && expires_at > Time.current
end
def conflicts?(token)
asset.locked? || version_token != token
end
private
def generate_token
self.token ||= SecureRandom.urlsafe_base64(32)
end
def set_default_expiration
self.expires_at ||= Constants::ASSET_SYNC_TOKEN_EXPIRATION.from_now
end
end

View file

@ -27,13 +27,13 @@ class ChecklistItem < ApplicationRecord
after_save :touch_checklist
after_touch :touch_checklist
def save_multiline!
def save_multiline!(after_id: nil)
at_position = checklist.checklist_items.find_by(id: after_id).position if after_id
if with_paragraphs
if new_record?
original_position = position
self.position = nil
save!
insert_at(original_position + 1)
insert_at(at_position + 1) || 0
else
save!
end
@ -42,7 +42,7 @@ class ChecklistItem < ApplicationRecord
items = []
if new_record?
start_position = position
start_position = at_position || 0
text.split("\n").compact.each do |line|
new_item = checklist.checklist_items.create!(text: line)
new_item.insert_at(start_position + 1)

View file

@ -32,7 +32,7 @@ class Experiment < ApplicationRecord
has_many :report_elements, inverse_of: :experiment, dependent: :destroy
# Associations for old activity type
has_many :activities, inverse_of: :experiment
has_many :users, through: :user_assignments
has_many :users, through: :user_assignments, dependent: :destroy
has_one_attached :workflowimg

View file

@ -56,7 +56,7 @@ class MyModule < ApplicationRecord
delegate :my_module_status_flow, to: :my_module_status, allow_nil: true
has_many :results, inverse_of: :my_module, dependent: :destroy
has_many :my_module_tags, inverse_of: :my_module, dependent: :destroy
has_many :tags, through: :my_module_tags
has_many :tags, through: :my_module_tags, dependent: :destroy
has_many :task_comments, foreign_key: :associated_id, dependent: :destroy
has_many :inputs, class_name: 'Connection', foreign_key: 'input_id', inverse_of: :to, dependent: :destroy
has_many :outputs, class_name: 'Connection', foreign_key: 'output_id', inverse_of: :from, dependent: :destroy
@ -338,7 +338,7 @@ class MyModule < ApplicationRecord
]
data = []
rows = repository.assigned_rows(self).includes(:created_by).order(created_at: order)
if repository.has_stock_management?
if repository.has_stock_management? && repository.has_stock_consumption?
headers.push(I18n.t('repositories.table.row_consumption'))
rows = rows.left_joins(my_module_repository_rows: :repository_stock_unit_item)
.select(
@ -352,7 +352,7 @@ class MyModule < ApplicationRecord
row_json << (row.archived ? "#{row.name} [#{I18n.t('general.archived')}]" : row.name)
row_json << I18n.l(row.created_at, format: :full)
row_json << row.created_by.full_name
if repository.has_stock_management?
if repository.has_stock_management? && repository.has_stock_consumption?
if repository.is_a?(RepositorySnapshot)
consumed_stock = row.repository_stock_consumption_cell&.value&.formatted
row_json << (consumed_stock || 0)
@ -378,12 +378,16 @@ class MyModule < ApplicationRecord
repository.repository_columns.order(:id).each do |column|
if column.data_type == 'RepositoryStockValue'
if repository.has_stock_consumption?
headers.push(I18n.t('repositories.table.row_consumption'))
else
headers.push(column.name)
end
custom_columns.push(column.id)
end
elsif column.data_type != 'RepositoryStockConsumptionValue' &&
!(repository.is_a?(RepositorySnapshot) && column.data_type == 'RepositoryStockConsumptionValue')
headers.push(column.name)
custom_columns.push(column.id)
end
end
records = repository.assigned_rows(self)
.select(:id, :name, :created_at, :created_by_id, :repository_id, :parent_id, :archived)

View file

@ -8,7 +8,7 @@ module MyModuleStatusConsequences
def forward(my_module)
my_module.assigned_repositories.each do |repository|
repository_snapshot = ::RepositorySnapshot.create_preliminary(repository, my_module)
repository_snapshot = ::RepositorySnapshot.create_preliminary!(repository, my_module)
service = Repositories::SnapshotProvisioningService.call(repository_snapshot: repository_snapshot)
unless service.succeed?

View file

@ -131,6 +131,7 @@ class Report < ApplicationRecord
report.user = current_user
report.team = current_team
report.last_modified_by = current_user
report.settings[:task][:repositories] = content['repositories']
ReportActions::ReportContent.new(report, content, {}, current_user).save_with_content
report
end

View file

@ -24,7 +24,7 @@ class Repository < RepositoryBase
inverse_of: :restored_repositories,
optional: true
has_many :team_shared_objects, as: :shared_object, dependent: :destroy
has_many :teams_shared_with, through: :team_shared_objects, source: :team
has_many :teams_shared_with, through: :team_shared_objects, source: :team, dependent: :destroy
has_many :repository_snapshots,
class_name: 'RepositorySnapshot',
foreign_key: :parent_id,
@ -200,17 +200,13 @@ class Repository < RepositoryBase
fields
end
def copy(created_by, name)
def copy(created_by, new_name)
new_repo = nil
begin
Repository.transaction do
# Clone the repository object
new_repo = dup
new_repo.created_by = created_by
new_repo.name = name
new_repo.permission_level = Extends::SHARED_OBJECTS_PERMISSION_LEVELS[:not_shared]
new_repo.save!
new_repo = Repository.create!(name: new_name, team:, created_by:)
# Clone columns (only if new_repo was saved)
repository_columns.find_each do |col|

View file

@ -70,7 +70,7 @@ class RepositoryAssetValue < ApplicationRecord
asset.last_modified_by = user
self.last_modified_by = user
asset.save! && save!
asset.post_process_file(repository_cell.repository_column.repository.team)
asset.post_process_file
end
def snapshot!(cell_snapshot)
@ -104,7 +104,7 @@ class RepositoryAssetValue < ApplicationRecord
value.asset.file.attach(io: StringIO.new(Base64.decode64(payload[:file_data])), filename: payload[:file_name])
end
value.asset.post_process_file(team)
value.asset.post_process_file
value
end

View file

@ -43,6 +43,10 @@ class RepositoryBase < ApplicationRecord
self.class.stock_management_enabled? && repository_columns.stock_type.exists?
end
def has_stock_consumption?
true
end
def cell_preload_includes
cell_includes = []
repository_columns.pluck(:data_type).each do |data_type|

View file

@ -2,7 +2,7 @@
class RepositoryChecklistItemsValue < ApplicationRecord
belongs_to :repository_checklist_item
belongs_to :repository_checklist_value
belongs_to :repository_checklist_value, inverse_of: :repository_checklist_items_values
validates :repository_checklist_item, :repository_checklist_value, presence: true

View file

@ -7,7 +7,9 @@ class RepositoryChecklistValue < ApplicationRecord
inverse_of: :modified_repository_checklist_values
has_one :repository_cell, as: :value, dependent: :destroy, inverse_of: :value
has_many :repository_checklist_items_values, dependent: :destroy
has_many :repository_checklist_items, -> { order('data ASC') }, through: :repository_checklist_items_values
has_many :repository_checklist_items, -> { order('data ASC') },
through: :repository_checklist_items_values,
dependent: :destroy
accepts_nested_attributes_for :repository_cell
validates :repository_cell, presence: true
@ -72,6 +74,8 @@ class RepositoryChecklistValue < ApplicationRecord
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)

View file

@ -4,7 +4,7 @@ class RepositoryColumn < ApplicationRecord
belongs_to :repository_snapshot, foreign_key: :repository_id, optional: true
belongs_to :created_by, foreign_key: :created_by_id, class_name: 'User'
has_many :repository_cells, dependent: :destroy
has_many :repository_rows, through: :repository_cells
has_many :repository_rows, through: :repository_cells, dependent: :destroy
has_many :repository_list_items, -> { order('data ASC') }, dependent: :destroy,
index_errors: true,
inverse_of: :repository_column

View file

@ -74,10 +74,10 @@ class RepositoryRow < ApplicationRecord
source: :value,
source_type: 'RepositoryStockValue'
has_many :repository_columns, through: :repository_cells
has_many :repository_columns, through: :repository_cells, dependent: :destroy
has_many :my_module_repository_rows,
inverse_of: :repository_row, dependent: :destroy
has_many :my_modules, through: :my_module_repository_rows
has_many :my_modules, through: :my_module_repository_rows, dependent: :destroy
has_many :child_connections,
class_name: 'RepositoryRowConnection',
foreign_key: :parent_id,
@ -86,7 +86,8 @@ class RepositoryRow < ApplicationRecord
has_many :child_repository_rows,
through: :child_connections,
class_name: 'RepositoryRow',
source: :child
source: :child,
dependent: :destroy
has_many :parent_connections,
class_name: 'RepositoryRowConnection',
foreign_key: :child_id,
@ -95,7 +96,8 @@ class RepositoryRow < ApplicationRecord
has_many :parent_repository_rows,
through: :parent_connections,
class_name: 'RepositoryRow',
source: :parent
source: :parent,
dependent: :destroy
auto_strip_attributes :name, nullify: false
validates :name,

View file

@ -37,17 +37,16 @@ class RepositorySnapshot < RepositoryBase
.where(my_module: { experiments: { project: project } })
}
def self.create_preliminary(repository, my_module, created_by = nil)
def self.create_preliminary!(repository, my_module, created_by = nil)
created_by ||= repository.created_by
repository_snapshot = repository.dup.becomes(RepositorySnapshot)
repository_snapshot.assign_attributes(type: RepositorySnapshot.name,
create!(
name: repository.name,
original_repository: repository,
my_module: my_module,
created_by: created_by,
team: my_module.experiment.project.team,
permission_level: Extends::SHARED_OBJECTS_PERMISSION_LEVELS[:not_shared])
repository_snapshot.provisioning!
repository_snapshot.reload
status: :provisioning,
my_module:,
created_by:
)
end
def default_table_state

View file

@ -19,9 +19,9 @@ class Result < ApplicationRecord
delegate :team, to: :my_module
has_many :result_orderable_elements, inverse_of: :result, dependent: :destroy
has_many :result_assets, inverse_of: :result, dependent: :destroy
has_many :assets, through: :result_assets
has_many :assets, through: :result_assets, dependent: :destroy
has_many :result_tables, inverse_of: :result, dependent: :destroy
has_many :tables, through: :result_tables
has_many :tables, through: :result_tables, dependent: :destroy
has_many :result_texts, inverse_of: :result, dependent: :destroy
has_many :result_comments, inverse_of: :result, foreign_key: :associated_id, dependent: :destroy
has_many :report_elements, inverse_of: :result, dependent: :destroy

View file

@ -38,9 +38,9 @@ class Step < ApplicationRecord
has_many :step_comments, foreign_key: :associated_id, dependent: :destroy
has_many :step_texts, inverse_of: :step, dependent: :destroy
has_many :step_assets, inverse_of: :step, dependent: :destroy
has_many :assets, through: :step_assets
has_many :assets, through: :step_assets, dependent: :destroy
has_many :step_tables, inverse_of: :step, dependent: :destroy
has_many :tables, through: :step_tables
has_many :tables, through: :step_tables, dependent: :destroy
has_many :report_elements, inverse_of: :step, dependent: :destroy
accepts_nested_attributes_for :checklists,

View file

@ -16,7 +16,7 @@ class Tag < ApplicationRecord
belongs_to :last_modified_by, foreign_key: 'last_modified_by_id', class_name: 'User', optional: true
belongs_to :project
has_many :my_module_tags, inverse_of: :tag, dependent: :destroy
has_many :my_modules, through: :my_module_tags
has_many :my_modules, through: :my_module_tags, dependent: :destroy
def self.search(user,
include_archived,

View file

@ -23,16 +23,10 @@ class Team < ApplicationRecord
maximum: Constants::NAME_MAX_LENGTH }
validates :description, length: { maximum: Constants::TEXT_MAX_LENGTH }
belongs_to :created_by,
foreign_key: 'created_by_id',
class_name: 'User',
optional: true
belongs_to :last_modified_by,
foreign_key: 'last_modified_by_id',
class_name: 'User',
optional: true
has_many :users, through: :user_assignments
has_many :projects, inverse_of: :team
belongs_to :created_by, class_name: 'User', optional: true
belongs_to :last_modified_by, class_name: 'User', optional: true
has_many :users, through: :user_assignments, dependent: :destroy
has_many :projects, inverse_of: :team, dependent: :destroy
has_many :project_folders, inverse_of: :team, dependent: :destroy
has_many :protocols, inverse_of: :team, dependent: :destroy
has_many :repository_protocols,
@ -41,7 +35,8 @@ class Team < ApplicationRecord
in_repository_draft
in_repository_published_version))
end),
class_name: 'Protocol'
class_name: 'Protocol',
dependent: :destroy
has_many :protocol_keywords, inverse_of: :team, dependent: :destroy
has_many :tiny_mce_assets, inverse_of: :team, dependent: :destroy
has_many :repositories, dependent: :destroy
@ -53,8 +48,13 @@ class Team < ApplicationRecord
has_many :team_shared_repositories,
-> { where(shared_object_type: 'RepositoryBase') },
class_name: 'TeamSharedObject',
inverse_of: :team
has_many :shared_repositories, through: :team_shared_objects, source: :shared_object, source_type: 'RepositoryBase'
inverse_of: :team,
dependent: :destroy
has_many :shared_repositories,
through: :team_shared_objects,
source: :shared_object,
source_type: 'RepositoryBase',
dependent: :destroy
has_many :repository_sharing_user_assignments,
(lambda do |team|
joins(
@ -64,11 +64,13 @@ class Team < ApplicationRecord
).where(team_id: team.id)
.where.not('user_assignments.team_id = repositories.team_id')
end),
class_name: 'UserAssignment'
class_name: 'UserAssignment',
dependent: :destroy
has_many :shared_by_user_repositories,
through: :repository_sharing_user_assignments,
source: :assignable,
source_type: 'RepositoryBase'
source_type: 'RepositoryBase',
dependent: :destroy
has_many :shareable_links, inverse_of: :team, dependent: :destroy
attr_accessor :without_templates

View file

@ -317,6 +317,7 @@ class User < ApplicationRecord
has_many :access_tokens, class_name: 'Doorkeeper::AccessToken',
foreign_key: :resource_owner_id,
dependent: :delete_all
has_many :asset_sync_tokens, dependent: :destroy
has_many :hidden_repository_cell_reminders, dependent: :destroy

View file

@ -31,4 +31,8 @@ Canaid::Permissions.register_for(Asset) do
end
end
end
can :open_asset_locally do |_user, asset|
ENV['ASSET_SYNC_URL'].present?
end
end

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