Merge branch 'develop' into features/sso-improvements

This commit is contained in:
Oleksii Kriuchykhin 2024-02-16 09:46:20 +01:00
commit 66f5df87e9
91 changed files with 602 additions and 555 deletions

View file

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

View file

@ -59,7 +59,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

@ -431,7 +431,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)
@ -452,12 +452,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)
@ -536,7 +536,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)
@ -823,7 +823,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

@ -288,48 +288,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')) {
@ -355,7 +313,6 @@ function init() {
initEditProtocolDescription();
initLinkUpdate();
initLoadFromRepository();
initImport();
initProtocolSectionOpenEvent();
initDetailsDropdown();
}

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'
},
@ -748,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
@ -865,8 +864,6 @@ var RepositoryDatatable = (function(global) {
clearTimeout(resizeTimeout);
resizeTimeout = setTimeout(restoreColumnSizes, 200);
});
restoreColumnSizes();
}
});

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">
<span>${truncateLongString(name, maxLength)}</span>
<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,5 +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

@ -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

@ -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

@ -26,16 +26,22 @@ class AssetSyncController < ApplicationController
def update
if @asset_sync_token.conflicts?(request.headers['VersionToken'])
conflict_response = AssetSyncTokenSerializer.new(conflicting_asset_copy_token).as_json
error_message = { message: I18n.t('assets.conflict_error', filename: @asset.file.filename) }
render json: conflict_response.merge(error_message), status: :conflict
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
@asset.file.attach(io: request.body, filename: @asset.file.filename)
@asset.update(last_modified_by: current_user)
ActiveRecord::Base.transaction do
@asset.update(last_modified_by: current_user)
@asset.file.attach(io: request.body, filename: @asset.file.filename)
log_activity
log_activity(:edit)
end
render json: AssetSyncTokenSerializer.new(@asset_sync_token).as_json
end
@ -44,44 +50,12 @@ class AssetSyncController < ApplicationController
render plain: Constants::ASSET_SYNC_URL
end
def log_activity
assoc ||= @asset.step
assoc ||= @asset.result
case assoc
when Step
if assoc.protocol.in_module?
log_step_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_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_activity(
:edit_task_result_file_locally,
assoc,
file: @asset.file_name,
user: current_user.id,
result: Result.first.id
)
def log_activity(action)
case action
when :edit
log_edit_activity
when :create
log_create_activity
end
end
@ -113,6 +87,8 @@ class AssetSyncController < ApplicationController
ResultAsset.create!(result: @asset.result, asset: new_asset)
end
@asset = new_asset.reload
current_user.asset_sync_tokens.create!(asset_id: new_asset.id)
end
end
@ -128,7 +104,80 @@ class AssetSyncController < ApplicationController
render_error(:forbidden, @asset.file.filename) and return unless can_manage_asset?(@asset)
end
def log_step_activity(type_of, step, project = nil, message_items = {})
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)
@ -142,7 +191,7 @@ class AssetSyncController < ApplicationController
message_items: message_items)
end
def log_result_activity(type_of, result, message_items)
def log_result_edit_activity(type_of, result, message_items)
Activities::CreateActivityService
.call(activity_type: type_of,
owner: current_user,

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,14 +194,14 @@ class AssetsController < ApplicationController
orig_file_name = @asset.file_name
return render_403 unless can_read_team?(@asset.team)
@asset.file.attach(io: params.require(:image), filename: orig_file_name)
@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)
@ -303,6 +311,10 @@ class AssetsController < ApplicationController
end
end
def checksum
render json: { checksum: @asset.file.blob.checksum }
end
private
def load_vars

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

@ -89,7 +89,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

@ -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

@ -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

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

@ -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>

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

@ -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-[130px] justify-between 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

@ -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

@ -55,17 +55,17 @@
<NoPredefinedAppModal
v-if="showNoPredefinedAppModal"
:fileName="attachment.attributes.file_name"
@confirm="showNoPredefinedAppModal = false"
@close="showNoPredefinedAppModal = false"
/>
<UpdateVersionModal
v-if="showUpdateVersionModal"
@cancel="showUpdateVersionModal = false"
@close="showUpdateVersionModal = false"
/>
<editLaunchingApplicationModal
v-if="editAppModal"
:fileName="attachment.attributes.file_name"
:application="this.localAppName"
@cancel="editAppModal = false"
@close="editAppModal = false"
/>
</Teleport>
</div>

View file

@ -12,7 +12,8 @@ export default {
scinoteEditVersion: null,
showNoPredefinedAppModal: false,
showUpdateVersionModal: false,
editAppModal: false
editAppModal: false,
pollingInterval: null,
};
},
components: {
@ -31,6 +32,9 @@ export default {
&& this.attributes.asset_type !== 'marvinjs';
}
},
beforeUnmount() {
this.stopPolling();
},
methods: {
async fetchLocalAppInfo() {
try {
@ -53,6 +57,8 @@ export default {
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);
}
},
@ -67,6 +73,7 @@ export default {
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) {
@ -76,6 +83,32 @@ export default {
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

@ -7,7 +7,7 @@
{{ attachment.attributes.wopi_context.button_text }}
</a>
</div>
<div v-else>
<div v-else-if="!usesWebIntegration">
<MenuDropdown
v-if="this.menu.length > 1"
class="ml-auto"
@ -19,7 +19,7 @@
@open-locally="openLocally"
@open-image-editor="openImageEditor"
></MenuDropdown>
<a v-else-if="menu.length === 1" class="btn btn-light !bg-sn-white" :href="menu[0].url" :target="menu[0].target" @click="this[this.menu[0].emit]()">
<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>
@ -28,17 +28,17 @@
<NoPredefinedAppModal
v-if="showNoPredefinedAppModal"
:fileName="attachment.attributes.file_name"
@confirm="showNoPredefinedAppModal = false"
@close="showNoPredefinedAppModal = false"
/>
<editLaunchingApplicationModal
v-if="editAppModal"
:fileName="attachment.attributes.file_name"
:application="this.localAppName"
@cancel="editAppModal = false"
@close="editAppModal = false"
/>
<UpdateVersionModal
v-if="showUpdateVersionModal"
@cancel="showUpdateVersionModal = false"
@close="showUpdateVersionModal = false"
/>
</Teleport>
</div>
@ -54,7 +54,8 @@ export default {
mixins: [OpenLocallyMixin],
components: { MenuDropdown, UpdateVersionModal },
props: {
attachment: { type: Object, required: true }
attachment: { type: Object, required: true },
disableLocalOpen: { type: Boolean, default: false }
},
created() {
this.fetchLocalAppInfo();
@ -75,14 +76,14 @@ export default {
});
}
if (this.attachment.attributes.image_editable && !this.canOpenLocally) {
if (this.attachment.attributes.image_editable) {
menu.push({
text: this.i18n.t('assets.file_preview.edit_in_scinote'),
emit: 'openImageEditor'
});
}
if (this.canOpenLocally) {
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');
@ -95,6 +96,10 @@ export default {
return menu;
},
usesWebIntegration() {
return this.attachment.attributes.asset_type === 'gene_sequence'
|| this.attachment.attributes.asset_type === 'marvinjs';
}
},
methods: {
openImageEditor() {

View file

@ -125,30 +125,39 @@
@attachment:delete="deleteAttachment"
@attachment:moved="attachmentMoved"
@attachment:uploaded="reloadAttachments"
@attachment:changed="$emit('attachment:changed', $event)"
@menu-visibility-changed="handleMenuVisibilityChange"
:withBorder="true"
/>
<deleteAttachmentModal
v-if="deleteModal"
:fileName="attachment.attributes.file_name"
@confirm="deleteAttachment"
@cancel="deleteModal = false"
/>
<moveAssetModal
v-if="movingAttachment"
:parent_type="attachment.attributes.parent_type"
:targets_url="attachment.attributes.urls.move_targets"
@confirm="moveAttachment($event)" @cancel="closeMoveModal"
/>
<NoPredefinedAppModal
v-if="showNoPredefinedAppModal"
:fileName="attachment.attributes.file_name"
@confirm="showNoPredefinedAppModal = false"
/>
<UpdateVersionModal
v-if="showUpdateVersionModal"
@cancel="showUpdateVersionModal = false"
/>
<Teleport to="body">
<deleteAttachmentModal
v-if="deleteModal"
:fileName="attachment.attributes.file_name"
@confirm="deleteAttachment"
@cancel="deleteModal = false"
/>
<moveAssetModal
v-if="movingAttachment"
:parent_type="attachment.attributes.parent_type"
:targets_url="attachment.attributes.urls.move_targets"
@confirm="moveAttachment($event)" @cancel="closeMoveModal"
/>
<NoPredefinedAppModal
v-if="showNoPredefinedAppModal"
:fileName="attachment.attributes.file_name"
@close="showNoPredefinedAppModal = false"
/>
<UpdateVersionModal
v-if="showUpdateVersionModal"
@close="showUpdateVersionModal = false"
/>
<editLaunchingApplicationModal
v-if="editAppModal"
:fileName="attachment.attributes.file_name"
:application="this.localAppName"
@close="editAppModal = false"
/>
</Teleport>
<a class="image-edit-button hidden"
v-if="attachment.attributes.asset_type != 'marvinjs'
&& attachment.attributes.image_editable

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

@ -1,5 +1,5 @@
<template>
<div ref="modal" @keydown.esc="cancel" class="modal" role="dialog" aria-hidden="true" tabindex="-1">
<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">
@ -15,7 +15,7 @@
)"></p>
</div>
<div class="modal-footer">
<button type='button' class='btn btn-secondary' @click="cancel">
<button type='button' class='btn btn-secondary' @click="close">
{{ i18n.t('general.close') }}
</button>
</div>
@ -24,21 +24,12 @@
</div>
</template>
<script>
import modalMixin from '../../modal_mixin';
export default {
name: 'editLaunchingApplicationModal',
props: {
fileName: String, application: String,
},
mounted() {
$(this.$refs.modal).modal('show');
$(this.$refs.modal).on('hidden.bs.modal', () => {
this.$emit('cancel');
});
},
methods: {
cancel() {
$(this.$refs.modal).modal('hide');
},
},
mixins: [modalMixin]
};
</script>

View file

@ -12,7 +12,7 @@
<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="confirm">{{ this.i18n.t('assets.no_predefined_app_modal.understand_button') }}</button>
<button class="btn btn-primary" @click="close">{{ this.i18n.t('assets.no_predefined_app_modal.understand_button') }}</button>
</div>
</div>
</div>
@ -20,21 +20,13 @@
</template>
<script>
import modalMixin from '../../modal_mixin';
export default {
name: 'NoPredefinedAppModal',
mixins: [modalMixin],
props: {
fileName: String
},
mounted() {
$(this.$refs.modal).modal('show');
$(this.$refs.modal).on('hidden.bs.modal', () => {
this.$emit('confirm');
});
},
methods: {
confirm() {
$(this.$refs.modal).modal('hide');
}
}
};
</script>

View file

@ -12,7 +12,7 @@
<p v-html="i18n.t('assets.update_version_modal.body_text_html')"></p>
</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>
<ScinoteEditDownload
:data="userAgent"
:isUpdateVersionModal="true"
@ -25,12 +25,14 @@
<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
},
@ -38,17 +40,6 @@ export default {
userAgent() {
return window.navigator.userAgent;
}
},
mounted() {
$(this.$refs.modal).modal('show');
$(this.$refs.modal).on('hidden.bs.modal', () => {
this.$emit('cancel');
});
},
methods: {
cancel() {
$(this.$refs.modal).modal('hide');
}
}
};
</script>

View file

@ -60,7 +60,7 @@
</div>
</template>
<a :href="'https://knowledgebase.scinote.net/en/knowledge/how-to-use-scinote-edit'"
<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">

View file

@ -3,7 +3,9 @@
<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">
<h3 class="mb-1.5">{{ config.title }}</h3>
<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>
@ -18,7 +20,13 @@
></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"></i>
<i :class="[
step.icon,
{
'text-sn-dark-grey': index <= activeStep,
'text-sn-grey': index > activeStep
}
]"></i>
</div>
<span
class="font-bold text-xs"

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,22 +26,15 @@ 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
@ -57,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? }
@ -245,7 +238,7 @@ class Asset < ApplicationRecord
end
end
to_asset.post_process_file(to_asset.team)
to_asset.post_process_file
end
def image?
@ -280,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
# Update asset's estimated size immediately
update_estimated_size(team)
end
def post_process_file
# Update asset's estimated size immediately
update_estimated_size unless text? || marvinjs?
if Rails.application.config.x.enable_pdf_previews && previewable_document?(blob)
PdfPreviewJob.perform_later(id)
@ -300,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?

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,11 +378,15 @@ class MyModule < ApplicationRecord
repository.repository_columns.order(:id).each do |column|
if column.data_type == 'RepositoryStockValue'
headers.push(I18n.t('repositories.table.row_consumption'))
else
if repository.has_stock_consumption?
headers.push(I18n.t('repositories.table.row_consumption'))
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
custom_columns.push(column.id)
end
records = repository.assigned_rows(self)

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,
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
create!(
name: repository.name,
original_repository: repository,
team: my_module.experiment.project.team,
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

@ -11,7 +11,9 @@ class AssetSerializer < ActiveModel::Serializer
attributes :file_name, :file_extension, :view_mode, :icon, :urls, :updated_at_formatted,
:file_size, :medium_preview, :large_preview, :asset_type, :wopi,
:wopi_context, :pdf_previewable, :file_size_formatted, :asset_order,
:updated_at, :metadata, :image_editable, :image_context, :pdf, :attached, :parent_type, :edit_version_range
:updated_at, :metadata, :image_editable, :image_context, :pdf, :attached, :parent_type,
:edit_version_range
attribute :checksum, if: :sync_url_present?
def icon
file_fa_icon_class(object)
@ -97,6 +99,10 @@ class AssetSerializer < ActiveModel::Serializer
object.editable_image?
end
def checksum
object.file.checksum
end
def image_context
if image_editable
{
@ -147,6 +153,8 @@ class AssetSerializer < ActiveModel::Serializer
if can_manage_asset?(user, object) && can_open_asset_locally?(user, object)
urls[:open_locally] = asset_sync_show_path(object)
urls[:open_locally_api] = Constants::ASSET_SYNC_URL
urls[:asset_show] = asset_show_path(object)
urls[:asset_checksum] = asset_checksum_path(object)
end
urls[:wopi_action] = object.get_action_url(user, 'embedview') if wopi && can_manage_asset?(user, object)
@ -154,4 +162,9 @@ class AssetSerializer < ActiveModel::Serializer
urls
end
def sync_url_present?
user = scope[:user] || @instance_options[:user]
can_open_asset_locally?(user, object)
end
end

View file

@ -24,7 +24,7 @@ class MarvinJsService
team_id: current_team.id)
attach_file(asset.file, file, params)
asset.save!
asset.post_process_file(current_team)
asset.post_process_file
connect_asset(asset, params, current_user)
end
@ -39,9 +39,9 @@ class MarvinJsService
return unless attachment
file = generate_image(params)
attach_file(attachment, file, params)
asset.update(last_modified_by: current_user) if asset.is_a?(Asset)
asset.post_process_file(current_team) if asset.class == Asset
attach_file(attachment, file, params)
asset.post_process_file if asset.instance_of?(Asset)
asset
end

View file

@ -960,10 +960,9 @@ class TeamImporter
asset.last_modified_by_id =
user_id || find_user(asset.last_modified_by_id)
asset.team = team
asset.in_template = true if @is_template
asset.save!
asset.file.attach(io: file, filename: File.basename(file))
asset.post_process_file(team)
asset.post_process_file
@asset_mappings[orig_asset_id] = asset.id
@asset_counter += 1
asset

View file

@ -148,7 +148,7 @@ class ProtocolsImporter
# Post process assets
asset_ids.each do |asset_id|
Asset.find(asset_id).post_process_file(protocol.team)
Asset.find(asset_id).post_process_file
end
end

View file

@ -95,7 +95,7 @@ class ProtocolsImporterV2
# Post process assets
asset_ids.each do |asset_id|
Asset.find(asset_id).post_process_file(protocol.team)
Asset.find(asset_id).post_process_file
end
end

View file

@ -32,7 +32,7 @@
<% end %>
<% if can_move_experiment?(experiment) && !archived %>
<li>
<%= link_to move_modal_experiment_url(experiment),
<%= link_to move_modal_experiments_path(ids: experiment.id),
remote: true, type: 'button',
class: 'move-experiment' do %>
<i class="sn-icon sn-icon-move"></i>

View file

@ -10,7 +10,7 @@
<a href="#" id="selectLiveVersionButton" class="list-group-item live-version-item version-button"
data-id="<%= @repository.id %>"
data-selected="<%= @repository_snapshots.select{ |s| s.selected == true }.blank? %>"
data-table-url="<%= full_view_table_my_module_repository_path(@my_module, @repository, include_stock_consumption: true) %>"
data-table-url="<%= full_view_table_my_module_repository_path(@my_module, @repository, include_stock_consumption: @repository.has_stock_consumption?) %>"
>
<h2 class="list-group-item-heading">
<%= t('my_modules.repository.snapshots.full_view.live') %>

View file

@ -19,15 +19,17 @@
<th id="added-on"><%= t("repositories.table.added_on") %></th>
<th id="added-by"><%= t("repositories.table.added_by") %></th>
<% @repository_snapshot.repository_columns.order(:parent_id).each do |column| %>
<th class="repository-column"
id="<%= column.id %>"
data-type="<%= column.data_type %>"
<% column.metadata.each do |k, v| %>
<%= "data-metadata-#{k}=#{v}" %>
<% end %>
>
<%= display_tooltip(column.name) %>
</th>
<% if column.data_type != 'RepositoryStockConsumptionValue' || @repository_snapshot.has_stock_consumption? %>
<th class="repository-column"
id="<%= column.id %>"
data-type="<%= column.data_type %>"
<% column.metadata.each do |k, v| %>
<%= "data-metadata-#{k}=#{v}" %>
<% end %>
>
<%= display_tooltip(column.name) %>
</th>
<% end %>
<% end %>
</tr>
</thead>

View file

@ -16,7 +16,7 @@
</span>
<% end %>
<div class="action-buttons">
<button class="btn btn-light icon-btn full-screen" data-table-url="<%= assigned_repository_full_view_table_path(@my_module, repository) %>?include_stock_consumption=true">
<button class="btn btn-light icon-btn full-screen" data-table-url="<%= assigned_repository_full_view_table_path(@my_module, repository) %><%= '?include_stock_consumption=true' if repository.has_stock_consumption? %>">
<i class="sn-icon sn-icon-expand"></i>
</button>
</div>
@ -28,12 +28,12 @@
data-name-column-id="<%= assigned_repository_simple_view_name_column_id(repository) %>"
>
<table class="table hidden repository-table repository-dataTable"
data-stock-management="<%= repository.has_stock_management? %>"
data-stock-consumption-editable="<%= can_update_my_module_stock_consumption?(@my_module) %>">
data-stock-management="<%= repository.has_stock_management? && repository.has_stock_consumption? %>"
data-stock-consumption-editable="<%= can_update_my_module_stock_consumption?(@my_module) && repository.has_stock_consumption? %>">
<thead>
<tr>
<th class="row-name"><%= t("repositories.table.row_name") %></th>
<% if repository.has_stock_management? %>
<% if repository.has_stock_management? && repository.has_stock_consumption? %>
<th class="row-stock" data-columns-visible="false"><%= repository.repository_stock_column.name %></th>
<th class="row-consumption" data-columns-visible="false"><%= t("repositories.table.row_consumption") %></th>
<% end %>

View file

@ -42,7 +42,7 @@
<!-- Move experiment -->
<% if can_move_experiment?(experiment) %>
<li>
<%= link_to move_modal_experiment_url(experiment),
<%= link_to move_modal_experiments_path(ids: [experiment.id]),
remote: true,
class: 'move-experiment experiment-action-link' do %>
<i class="sn-icon sn-icon-move"></i>

View file

@ -3,7 +3,7 @@
tabindex="-1"
role="dialog"
aria-labelledby="manangeRepositoryColumnLabel" data-task-page="<%= my_module_page ? 'true' : '' %>">
<div class="modal-dialog" role="document">
<div class="modal-dialog" role="document" data-e2e="e2e-MD-invInventoryRT-manageColumns">
<div class="modal-content">
</div>
</div>

View file

@ -25,12 +25,12 @@
data-name-column-id="<%= assigned_repository_simple_view_name_column_id(repository) %>"
>
<table class="table repository-table"
data-stock-management="<%= repository.has_stock_management? %>"
data-stock-management="<%= repository.has_stock_management? && repository.has_stock_consumption? %>"
data-stock-consumption-editable="false">
<thead>
<tr>
<th class="row-name"><%= t("repositories.table.row_name") %></th>
<% if repository.has_stock_management? %>
<% if repository.has_stock_management? && repository.has_stock_consumption? %>
<th class="row-stock" data-columns-visible="false"><%= repository.repository_stock_column.name %></th>
<th class="row-consumption" data-columns-visible="false"><%= t("repositories.table.row_consumption") %></th>
<% end %>

View file

@ -4,7 +4,7 @@
<% if can_edit && !preview %>
<% if wopi_enabled? && wopi_file?(asset) %>
<div id="openLocallyMenu" data-behaviour="vue">
<open-locally-menu :attachment="<%= { attributes: AssetSerializer.new(asset, scope: { user: current_user }).as_json }.to_json %>" />
<open-locally-menu :disable-local-open="<%= asset.repository_asset_value.present? %>" :attachment="<%= { attributes: AssetSerializer.new(asset, scope: { user: current_user }).as_json }.to_json %>" />
</div>
<% elsif asset.file.metadata[:asset_type] == 'marvinjs' %>
<button class="btn btn-light marvinjs-edit-button"
@ -40,7 +40,7 @@
</button>
<% end %>
<div id="openLocallyMenu" data-behaviour="vue">
<open-locally-menu :attachment="<%= { attributes: AssetSerializer.new(asset, scope: { user: current_user }).as_json }.to_json %>" />
<open-locally-menu :disable-local-open="<%= asset.repository_asset_value.present? %>" :attachment="<%= { attributes: AssetSerializer.new(asset, scope: { user: current_user }).as_json }.to_json %>" />
</div>
<% end %>
<a class="btn btn-light file-download-link" href="<%= rails_blob_path(asset.file, disposition: 'attachment') %>" data-turbolinks="false">

View file

@ -7,28 +7,29 @@
<h1 id="scinote-addons-title" class="mt-0 pb-1.5 text-sn-black"><%= t('users.settings.account.addons.title') %></h1>
<div id="scinote-addons-wrapper" class="flex flex-col bg-sn-white p-4 rounded mb-6">
<h2 class="my-0 pb-6 text-sn-black" ><%= t('users.settings.account.addons.scinote_addons') %></h2>
<div data-hook="settings-addons-container">
<em data-hook="settings-addons-no-addons">
<%= t('users.settings.account.addons.no_addons') %>
</em>
</div>
</div>
<%# scinote edit %>
<% if ENV['ASSET_SYNC_URL'].present? %>
<div class="bg-sn-white rounded p-4 mb-6">
<div class="font-bold my-0 pb-2 text-sn-black"><%= t('users.settings.account.addons.desktop_app.title') %></div>
<div class="pb-6 text-sn-dark-grey">
<%= t('users.settings.account.addons.desktop_app.description') %>
</div>
<div id="scinoteEditDownload" data-behaviour="vue">
<scinote-edit-download data="<%= @user_agent %>">
</div>
<h2 class="text-sn-black my-0 pb-6"><%= t('users.settings.account.addons.desktop_app.scinote_apps') %></h2>
<div class="font-bold my-0 pb-2 text-sn-black"><%= t('users.settings.account.addons.desktop_app.scinote_edit') %></div>
<div class="pb-6 text-sn-dark-grey">
<%= t('users.settings.account.addons.desktop_app.description') %>
</div>
<div id="scinoteEditDownload" data-behaviour="vue">
<scinote-edit-download data="<%= @user_agent %>">
</div>
</div>
<% end %>
<div id="scinote-addons-wrapper" class="flex flex-col bg-sn-white p-4 rounded mb-6">
<h2 class="my-0 pb-6 text-sn-black" ><%= t('users.settings.account.addons.scinote_addons') %></h2>
<div data-hook="settings-addons-container">
<em data-hook="settings-addons-no-addons">
<%= t('users.settings.account.addons.no_addons') %>
</em>
</div>
</div>
<%# label printers %>
<div id="printer-settings" class="flex flex-col gap-6 rounded bg-sn-white mb-6 p-4">
<h2 class="my-0 text-sn-black"><%= t('users.settings.account.addons.label_printers') %></h2>

View file

@ -2,6 +2,7 @@
require 'active_storage/previewer/libreoffice_previewer'
require 'active_storage/analyzer/image_analyzer/custom_image_magick'
require 'active_storage/analyzer/text_extraction_analyzer'
require 'active_storage/downloader'
# Enable PDF previews for files
@ -11,6 +12,7 @@ Rails.application.config.active_storage.previewers = [ActiveStorage::Previewer::
ActiveStorage::Previewer::LibreofficePreviewer]
Rails.application.config.active_storage.analyzers.prepend(ActiveStorage::Analyzer::ImageAnalyzer::CustomImageMagick)
Rails.application.config.active_storage.analyzers.append(ActiveStorage::Analyzer::TextExtractionAnalyzer)
Rails.application.config.active_storage.variable_content_types << 'image/svg+xml'

View file

@ -428,6 +428,9 @@ class Constants
FAST_STATUS_POLLING_INTERVAL = 5000
SLOW_STATUS_POLLING_INTERVAL = 10000
# Interval time for polling asset changes when editing with SciNote Edit
ASSET_POLLING_INTERVAL = 5000
ASSET_SYNC_TOKEN_EXPIRATION = 1.year
ASSET_SYNC_URL = ENV['ASSET_SYNC_URL'].freeze

View file

@ -367,15 +367,6 @@ class Extends
generate_docx_report: 164,
change_user_role_on_experiment: 165,
change_user_role_on_my_module: 166,
edit_molecule_on_step: 168,
edit_molecule_on_result: 169,
edit_molecule_on_step_in_repository: 170,
create_molecule_on_step: 171,
create_molecule_on_result: 172,
create_molecule_on_step_in_repository: 173,
register_molecule_on_step: 177,
register_molecule_on_result: 178,
register_molecule_on_step_in_repository: 179,
inventory_item_stock_set: 180,
inventory_item_stock_add: 181,
inventory_item_stock_remove: 182,
@ -502,18 +493,18 @@ class Extends
ACTIVITY_GROUPS = {
projects: [*0..7, 32, 33, 34, 95, 108, 65, 109, *158..162, 241, 242, 243],
task_results: [23, 26, 25, 42, 24, 40, 41, 99, 110, 122, 116, 128, 169, 172, 178, *246..248, *257..273, *284..291, 301],
task_results: [23, 26, 25, 42, 24, 40, 41, 99, 110, 122, 116, 128, *246..248, *257..273, *284..291, 301],
task: [8, 58, 9, 59, *10..14, 35, 36, 37, 53, 54, *60..63, 138, 139, 140, 64, 66, 106, 126, 120, 132,
148, 166],
task_protocol: [15, 22, 16, 18, 19, 20, 21, 17, 38, 39, 100, 111, 45, 46, 47, 121, 124, 115, 118, 127, 130, 137,
168, 171, 177, 184, 185, 188, 189, *192..203, 221, 222, 224, 225, 226, 236, *249..252, *274..278, 299],
184, 185, 188, 189, *192..203, 221, 222, 224, 225, 226, 236, *249..252, *274..278, 299],
task_inventory: [55, 56, 146, 147, 183],
experiment: [*27..31, 57, 141, 165],
reports: [48, 50, 49, 163, 164],
inventories: [70, 71, 105, 144, 145, 72, 73, 74, 102, 142, 143, 75, 76, 77,
78, 96, 107, 113, 114, *133..136, 180, 181, 182, *292..298],
protocol_repository: [80, 103, 89, 87, 79, 90, 91, 88, 85, 86, 84, 81, 82,
83, 101, 112, 123, 125, 117, 119, 129, 131, 170, 173, 179, 187, 186,
83, 101, 112, 123, 125, 117, 119, 129, 131, 187, 186,
190, 191, *204..215, 220, 223, 227, 228, 229, *230..235,
*237..240, *253..256, *279..283, 300],
team: [92, 94, 93, 97, 104, 244, 245],

View file

@ -2821,7 +2821,8 @@ en:
description: 'Print labels of custom styles and sizes in seconds flat from your PC or Mac via your Zebra printer connected via USB, Internet or LAN.'
details: 'Details'
desktop_app:
title: 'SciNote Edit'
scinote_apps: "SciNote apps"
scinote_edit: 'SciNote Edit'
description: 'Download the SciNote Edit desktop app to automatically edit files using locally installed software.'
macos_button: 'Download for macOS'
windows_button: 'Download for Windows'
@ -3637,7 +3638,7 @@ en:
set_up_app: "Set up an application to open this file"
update_version_modal:
title: "Update required"
body_text_html: "The current version of the SciNote Edit application is no longer supported. To ensure a seamless and secure user experience, we recommend installing to the latest version."
body_text_html: "The current version of the SciNote Edit application is no longer supported. To ensure a seamless and secure user experience, we recommend updating to the latest version."
edit_launching_application_modal:
title: "Launching application"
description: "%{file_name} will now open in %{application}. Saved changes in %{application} will automatically be synced in SciNote."

View file

@ -219,18 +219,6 @@ en:
move_project_folder_html: "%{user} moved project folder %{project_folder} from folder %{project_folder_from}</strong> to folder %{project_folder_to}."
generate_pdf_report_html: "%{user} generated PDF report %{report}."
generate_docx_report_html: "%{user} generated DOCX report %{report}."
edit_molecule_on_step_html: "%{user} edited molecule %{asset_name} on protocol's step %{step_position} %{step} on task %{my_module}: %{action}."
edit_molecule_on_result_html: "%{user} edited molecule %{asset_name} on result %{result}: %{action}."
edit_molecule_on_step_in_repository_html: "%{user} edited molecule %{asset_name} on protocol %{protocol}'s step %{step_position} %{step}: %{action}."
create_molecule_on_step_html: "%{user} created molecule %{asset_name} on protocol's step %{step_position} %{step} on task %{my_module}."
create_molecule_on_result_html: "%{user} created molecule %{asset_name} on result %{result}."
create_molecule_on_step_in_repository_html: "%{user} created molecule %{asset_name} on protocol %{protocol}'s step %{step_position} %{step}."
delete_molecule_on_step_html: "%{user} deleted molecule %{asset_name} on protocol's step %{step_position} %{step} on task %{my_module}."
delete_molecule_on_result_html: "%{user} deleted molecule %{asset_name} on result %{result}."
delete_molecule_on_step_in_repository_html: "%{user} deleted molecule %{asset_name} on protocol %{protocol}'s step %{step_position} %{step}."
register_molecule_on_step_html: "%{user} registered molecule %{asset_name} on protocol's step %{step_position} %{step} on task %{my_module}."
register_molecule_on_result_html: "%{user} registered molecule %{asset_name} on result %{result}."
register_molecule_on_step_in_repository_html: "%{user} registered molecule %{asset_name} on protocol %{protocol}'s step %{step_position} %{step}."
inventory_item_stock_set_html: "%{user} set total stock for inventory item %{repository_row} to %{new_amount} %{unit} in inventory %{repository} \n%{comment}"
inventory_item_stock_add_html: "%{user} added %{change_amount} %{unit} of stock to a total %{new_amount} %{unit} for inventory item %{repository_row} in inventory %{repository} \n%{comment}"
inventory_item_stock_remove_html: "%{user} removed %{change_amount} %{unit} of stock to a total %{new_amount} %{unit} for inventory item %{repository_row} in inventory %{repository} \n%{comment}"
@ -329,7 +317,7 @@ en:
activity_name:
create_project: "Project created"
rename_project: "Project renamed"
change_project_visibility: "Project visibility changed"
change_project_visibility: "Project visibility changed (obsolete)"
project_grant_access_to_all_team_members: "Grant access to all team members"
project_remove_access_from_all_team_members: "Remove access from all team members"
archive_project: "Project archived"
@ -340,16 +328,16 @@ en:
create_report: "Report created"
edit_report: "Report edited"
delete_report: "Report deleted"
add_result_old: "Result added (old)"
add_result_old: "Result added (obsolete)"
add_result: "Result added"
edit_result_old: "Result edited (old)"
edit_result_old: "Result edited (obsolete)"
edit_result: "Result edited"
add_comment_to_result: "Result comment added"
edit_result_comment: "Result comment edited"
delete_result_comment: "Result comment deleted"
archive_result_old: "Result archived (old)"
archive_result_old: "Result archived (obsolete)"
archive_result: "Result archived"
destroy_result_old: "Result deleted (old)"
destroy_result_old: "Result deleted (obsolete)"
destroy_result: "Result deleted"
result_restored: "Result restored"
result_content_rearranged: "Result content rearranged"
@ -376,7 +364,7 @@ en:
edit_step: "Task step edited"
destroy_step: "Task step deleted"
load_protocol_to_task_from_repository: "Task protocol loaded from repository"
load_protocol_to_task_from_file: "Task protocol loaded from file"
load_protocol_to_task_from_file: "Task protocol loaded from file (obsolete)"
update_protocol_in_task_from_repository: "Task protocol updated from repository"
check_step_checklist_item: "Task step checklist completed"
uncheck_step_checklist_item: "Task step checklist uncompleted"
@ -441,7 +429,7 @@ en:
copy_inventory: "Inventory copied"
copy_inventory_item: "Inventory item copied"
delete_column_inventory: "Inventory column deleted"
update_protocol_in_repository_from_task: "Protocol updated from task"
update_protocol_in_repository_from_task: "Protocol updated from task (obsolete)"
create_protocol_in_repository: "Protocol created"
add_step_to_protocol_repository: "Step added"
edit_step_in_protocol_repository: "Step edited"
@ -452,7 +440,7 @@ en:
edit_protocol_name_in_repository: "Protocol name edited"
archive_protocol_in_repository: "Protocol archived"
restore_protocol_in_repository: "Protocol restored from archive"
move_protocol_in_repository: "Protocol moved"
move_protocol_in_repository: "Protocol moved (obsolete)"
copy_protocol_in_repository: "Protocol copied"
import_protocol_in_repository: "Protocol imported from file"
export_protocol_in_repository: "Protocol exported"
@ -499,15 +487,6 @@ en:
move_project_folder: "Project folder moved"
generate_pdf_report: "PDF Report generated"
generate_docx_report: "DOCX Report generated"
edit_molecule_on_step: "Molecule on task step edited"
edit_molecule_on_result: "Molecule on result edited"
edit_molecule_on_step_in_repository: "Molecule on step edited"
create_molecule_on_step: "Molecule on task step created"
create_molecule_on_result: "Molecule on result created"
create_molecule_on_step_in_repository: "Molecule on step created"
register_molecule_on_step: "Molecule on task step registered"
register_molecule_on_result: "Molecule on result registered"
register_molecule_on_step_in_repository: "Molecule on step registered"
inventory_item_stock_set: "Inventory item stock set to"
inventory_item_stock_add: "Inventory item stock added"
inventory_item_stock_remove: "Inventory item stock removed"

View file

@ -645,7 +645,6 @@ Rails.application.routes.draw do
get 'load_from_repository_modal',
to: 'protocols#load_from_repository_modal'
post 'load_from_repository', to: 'protocols#load_from_repository'
post 'load_from_file', to: 'protocols#load_from_file'
get 'copy_to_repository_modal', to: 'protocols#copy_to_repository_modal'
post 'copy_to_repository', to: 'protocols#copy_to_repository'
@ -813,6 +812,8 @@ Rails.application.routes.draw do
get 'files/:id/file_url', to: 'assets#file_url', as: 'asset_file_url'
get 'files/:id/download', to: 'assets#download', as: 'asset_download'
get 'files/:id/edit', to: 'assets#edit', as: 'edit_asset'
get 'files/:id/checksum', to: 'assets#checksum', as: 'asset_checksum'
get 'files/:id/show', to: 'assets#show', as: 'asset_show'
patch 'files/:id/toggle_view_mode', to: 'assets#toggle_view_mode', as: 'toggle_view_mode'
get 'files/:id/load_asset', to: 'assets#load_asset', as: 'load_asset'
post 'files/:id/update_image', to: 'assets#update_image',

View file

@ -0,0 +1,66 @@
# frozen_string_literal: true
module ActiveStorage
class Analyzer::TextExtractionAnalyzer < Analyzer
def self.accept?(blob)
blob.content_type.in?(Constants::TEXT_EXTRACT_FILE_TYPES) && blob.attachments.where(record_type: 'Asset').any?
end
def self.analyze_later?
true
end
def metadata
download_blob_to_tempfile do |file|
if blob.content_type == 'application/pdf'
process_pdf(file)
elsif blob.metadata[:asset_type] == 'marvinjs'
process_marvinjs(file)
else
process_other(file)
end
end
end
private
def process_pdf(file)
text_data = IO.popen(['pdftotext', file.path, '-'], 'r').read
create_or_update_text_data(text_data)
rescue Errno::ENOENT
logger.info "pdftotext isn't installed, falling back to default text extraction method"
process_other(file)
end
def process_marvinjs(file)
mjs_doc = Nokogiri::XML(file.metadata[:description])
mjs_doc.remove_namespaces!
text_data = mjs_doc.search("//Field[@name='text']").collect(&:text).join(' ')
create_or_update_text_data(text_data)
end
def process_other(file)
text_data = Yomu.new(file.path).text
create_or_update_text_data(text_data)
end
def create_or_update_text_data(text_data)
@blob.attachments.where(record_type: 'Asset').each do |attachemnt|
asset = attachemnt.record
if asset.asset_text_datum.present?
# Update existing text datum if it exists
asset.asset_text_datum.update!(data: text_data)
else
# Create new text datum
asset.create_asset_text_datum!(data: text_data)
end
asset.update_estimated_size
Rails.logger.info "Asset #{asset.id}: file text successfully extracted"
end
{ text_extracted: true }
end
end
end

View file

@ -206,31 +206,4 @@ describe ProtocolsController, type: :controller do
.to(change { Activity.count })
end
end
describe 'POST load_from_file' do
let(:protocol) do
create :protocol, my_module: my_module, team: team, added_by: user
end
let(:params) do
{ id: protocol.id,
protocol: { name: 'my_test_protocol',
description: 'description',
authors: 'authors',
elnVersion: '1.1'} }
end
let(:action) { post :load_from_file, params: params, format: :json }
it 'calls create activity for loading protocol to task from file' do
expect(Activities::CreateActivityService)
.to(receive(:call)
.with(hash_including(activity_type:
:load_protocol_to_task_from_file)))
action
end
it 'adds activity in DB' do
expect { action }
.to(change { Activity.count })
end
end
end

View file

@ -219,7 +219,7 @@
iterators: iterators
}
} else { // other value
} /* else { // other value
// increment or decrement values for more than 2 selected cells
if (rlength >= 2 || clength >= 2) {
@ -281,7 +281,7 @@
}
}
}
} */
return {
value: value,

View file

@ -2245,11 +2245,11 @@ axios@^0.21.1:
follow-redirects "^1.14.0"
axios@^1.6.0:
version "1.6.0"
resolved "https://registry.yarnpkg.com/axios/-/axios-1.6.0.tgz#f1e5292f26b2fd5c2e66876adc5b06cdbd7d2102"
integrity sha512-EZ1DYihju9pwVB+jg67ogm+Tmqc6JmhamRN6I4Zt8DfZu5lbcQGw3ozH9lFejSJgs/ibaef3A9PMXPLeefFGJg==
version "1.6.7"
resolved "https://registry.yarnpkg.com/axios/-/axios-1.6.7.tgz#7b48c2e27c96f9c68a2f8f31e2ab19f59b06b0a7"
integrity sha512-/hDJGff6/c7u0hDkvkGxR/oy6CbCs8ziCsC7SqmhjfozqiJGc8Z11wrv9z9lYfY4K8l+H9TpjcMDX0xOZmx+RA==
dependencies:
follow-redirects "^1.15.0"
follow-redirects "^1.15.4"
form-data "^4.0.0"
proxy-from-env "^1.1.0"
@ -2813,6 +2813,11 @@ commondir@^1.0.1:
resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b"
integrity sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==
compare-versions@^6.1.0:
version "6.1.0"
resolved "https://registry.yarnpkg.com/compare-versions/-/compare-versions-6.1.0.tgz#3f2131e3ae93577df111dba133e6db876ffe127a"
integrity sha512-LNZQXhqUvqUTotpZ00qLSaify3b4VFD588aRr8MKFw4CMUr98ytzCW5wDH5qx/DEY5kCDXcbcRuCqL0szEf2tg==
compress-commons@^4.1.2:
version "4.1.2"
resolved "https://registry.yarnpkg.com/compress-commons/-/compress-commons-4.1.2.tgz#6542e59cb63e1f46a8b21b0e06f9a32e4c8b06df"
@ -3701,11 +3706,16 @@ focus-trap@^5.1.0:
tabbable "^4.0.0"
xtend "^4.0.1"
follow-redirects@^1.14.0, follow-redirects@^1.15.0:
follow-redirects@^1.14.0:
version "1.15.4"
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.4.tgz#cdc7d308bf6493126b17ea2191ea0ccf3e535adf"
integrity sha512-Cr4D/5wlrb0z9dgERpUL3LrmPKVDsETIJhaCMeDfuFYcqa5bldGV6wBsAN6X/vxlXQtFBMrXdXxdL8CbDTGniw==
follow-redirects@^1.15.4:
version "1.15.5"
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.5.tgz#54d4d6d062c0fa7d9d17feb008461550e3ba8020"
integrity sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw==
for-each@^0.3.3:
version "0.3.3"
resolved "https://registry.yarnpkg.com/for-each/-/for-each-0.3.3.tgz#69b447e88a0a5d32c3e7084f3f1710034b21376e"