Merge branch 'features/inventory-import-improvements' into ai-sci-10261-make-protocol-toolbar-responsive

This commit is contained in:
aignatov-bio 2024-06-19 17:13:52 +02:00 committed by GitHub
commit 53a68b4b24
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
77 changed files with 1187 additions and 972 deletions

View file

@ -60,47 +60,47 @@ GIT
GEM
remote: http://rubygems.org/
specs:
actioncable (7.0.8.1)
actionpack (= 7.0.8.1)
activesupport (= 7.0.8.1)
actioncable (7.0.8.4)
actionpack (= 7.0.8.4)
activesupport (= 7.0.8.4)
nio4r (~> 2.0)
websocket-driver (>= 0.6.1)
actionmailbox (7.0.8.1)
actionpack (= 7.0.8.1)
activejob (= 7.0.8.1)
activerecord (= 7.0.8.1)
activestorage (= 7.0.8.1)
activesupport (= 7.0.8.1)
actionmailbox (7.0.8.4)
actionpack (= 7.0.8.4)
activejob (= 7.0.8.4)
activerecord (= 7.0.8.4)
activestorage (= 7.0.8.4)
activesupport (= 7.0.8.4)
mail (>= 2.7.1)
net-imap
net-pop
net-smtp
actionmailer (7.0.8.1)
actionpack (= 7.0.8.1)
actionview (= 7.0.8.1)
activejob (= 7.0.8.1)
activesupport (= 7.0.8.1)
actionmailer (7.0.8.4)
actionpack (= 7.0.8.4)
actionview (= 7.0.8.4)
activejob (= 7.0.8.4)
activesupport (= 7.0.8.4)
mail (~> 2.5, >= 2.5.4)
net-imap
net-pop
net-smtp
rails-dom-testing (~> 2.0)
actionpack (7.0.8.1)
actionview (= 7.0.8.1)
activesupport (= 7.0.8.1)
actionpack (7.0.8.4)
actionview (= 7.0.8.4)
activesupport (= 7.0.8.4)
rack (~> 2.0, >= 2.2.4)
rack-test (>= 0.6.3)
rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.0, >= 1.2.0)
actiontext (7.0.8.1)
actionpack (= 7.0.8.1)
activerecord (= 7.0.8.1)
activestorage (= 7.0.8.1)
activesupport (= 7.0.8.1)
actiontext (7.0.8.4)
actionpack (= 7.0.8.4)
activerecord (= 7.0.8.4)
activestorage (= 7.0.8.4)
activesupport (= 7.0.8.4)
globalid (>= 0.6.0)
nokogiri (>= 1.8.5)
actionview (7.0.8.1)
activesupport (= 7.0.8.1)
actionview (7.0.8.4)
activesupport (= 7.0.8.4)
builder (~> 3.1)
erubi (~> 1.4)
rails-dom-testing (~> 2.0)
@ -110,14 +110,14 @@ GEM
activemodel (>= 4.1, < 7.1)
case_transform (>= 0.2)
jsonapi-renderer (>= 0.1.1.beta1, < 0.3)
activejob (7.0.8.1)
activesupport (= 7.0.8.1)
activejob (7.0.8.4)
activesupport (= 7.0.8.4)
globalid (>= 0.3.6)
activemodel (7.0.8.1)
activesupport (= 7.0.8.1)
activerecord (7.0.8.1)
activemodel (= 7.0.8.1)
activesupport (= 7.0.8.1)
activemodel (7.0.8.4)
activesupport (= 7.0.8.4)
activerecord (7.0.8.4)
activemodel (= 7.0.8.4)
activesupport (= 7.0.8.4)
activerecord-import (1.4.1)
activerecord (>= 4.2)
activerecord-session_store (2.1.0)
@ -127,14 +127,14 @@ GEM
multi_json (~> 1.11, >= 1.11.2)
rack (>= 2.0.8, < 4)
railties (>= 6.1)
activestorage (7.0.8.1)
actionpack (= 7.0.8.1)
activejob (= 7.0.8.1)
activerecord (= 7.0.8.1)
activesupport (= 7.0.8.1)
activestorage (7.0.8.4)
actionpack (= 7.0.8.4)
activejob (= 7.0.8.4)
activerecord (= 7.0.8.4)
activesupport (= 7.0.8.4)
marcel (~> 1.0)
mini_mime (>= 1.1.0)
activesupport (7.0.8.1)
activesupport (7.0.8.4)
concurrent-ruby (~> 1.0, >= 1.0.2)
i18n (>= 1.6, < 2)
minitest (>= 5.1)
@ -246,7 +246,7 @@ GEM
combine_pdf (1.0.23)
matrix
ruby-rc4 (>= 0.1.5)
concurrent-ruby (1.2.3)
concurrent-ruby (1.3.1)
crack (0.4.5)
rexml
crass (1.0.6)
@ -370,7 +370,7 @@ GEM
httparty (0.21.0)
mini_mime (>= 1.0.0)
multi_xml (>= 0.5.2)
i18n (1.14.1)
i18n (1.14.5)
concurrent-ruby (~> 1.0)
i18n-js (3.9.2)
i18n (>= 0.6.6)
@ -439,8 +439,8 @@ GEM
mime-types-data (3.2023.0218.1)
mini_magick (4.12.0)
mini_mime (1.1.5)
mini_portile2 (2.8.6)
minitest (5.22.2)
mini_portile2 (2.8.7)
minitest (5.23.1)
msgpack (1.7.1)
multi_json (1.15.0)
multi_test (1.1.0)
@ -546,8 +546,8 @@ GEM
puma (6.4.2)
nio4r (~> 2.0)
raabro (1.4.0)
racc (1.7.3)
rack (2.2.8.1)
racc (1.8.0)
rack (2.2.9)
rack-attack (6.6.1)
rack (>= 1.0, < 3)
rack-cors (2.0.2)
@ -563,20 +563,20 @@ GEM
rack
rack-test (2.1.0)
rack (>= 1.3)
rails (7.0.8.1)
actioncable (= 7.0.8.1)
actionmailbox (= 7.0.8.1)
actionmailer (= 7.0.8.1)
actionpack (= 7.0.8.1)
actiontext (= 7.0.8.1)
actionview (= 7.0.8.1)
activejob (= 7.0.8.1)
activemodel (= 7.0.8.1)
activerecord (= 7.0.8.1)
activestorage (= 7.0.8.1)
activesupport (= 7.0.8.1)
rails (7.0.8.4)
actioncable (= 7.0.8.4)
actionmailbox (= 7.0.8.4)
actionmailer (= 7.0.8.4)
actionpack (= 7.0.8.4)
actiontext (= 7.0.8.4)
actionview (= 7.0.8.4)
activejob (= 7.0.8.4)
activemodel (= 7.0.8.4)
activerecord (= 7.0.8.4)
activestorage (= 7.0.8.4)
activesupport (= 7.0.8.4)
bundler (>= 1.15.0)
railties (= 7.0.8.1)
railties (= 7.0.8.4)
rails-controller-testing (1.0.5)
actionpack (>= 5.0.1.rc1)
actionview (>= 5.0.1.rc1)
@ -597,9 +597,9 @@ GEM
railties (> 3.1)
rails_serve_static_assets (0.0.5)
rails_stdout_logging (0.0.5)
railties (7.0.8.1)
actionpack (= 7.0.8.1)
activesupport (= 7.0.8.1)
railties (7.0.8.4)
actionpack (= 7.0.8.4)
activesupport (= 7.0.8.4)
method_source
rake (>= 12.2)
thor (~> 1.0)

View file

@ -1 +1 @@
1.34.0.2
1.35.0.1

View file

@ -40,7 +40,7 @@
}
.modal .modal-dialog.modal-lg {
@apply w-[900px];
@apply max-w-[900px] w-full;
}
.modal.fade .modal-dialog {

View file

@ -141,7 +141,7 @@ class AssetSyncController < ApplicationController
project = assoc.protocol.in_module? ? assoc.my_module.project : nil
when Result
type_of = :result_file_added
message_items = { result: assoc }
message_items = { result: assoc.id }
project = assoc.my_module.project
end

View file

@ -99,7 +99,7 @@ class MyModulesController < ApplicationController
}
end
format.json do
render json: @my_module, serializer: Lists::MyModuleSerializer, user: current_user
render json: @my_module, serializer: Lists::MyModuleSerializer, controller: self, user: current_user
end
end
end

View file

@ -280,7 +280,7 @@ class RepositoriesController < ApplicationController
render_403 unless can_create_repository_rows?(@repository)
unless import_params[:file]
repository_response(t('repositories.parse_sheet.errors.no_file_selected'))
unprocessable_entity_repository_response(t('repositories.parse_sheet.errors.no_file_selected'))
return
end
begin
@ -290,16 +290,12 @@ class RepositoriesController < ApplicationController
session: session
)
if parsed_file.too_large?
return render json: { error: t('general.file.size_exceeded', file_size: Rails.configuration.x.file_max_size_mb) }, status: :unprocessable_entity
render json: { error: t('general.file.size_exceeded', file_size: Rails.configuration.x.file_max_size_mb) },
status: :unprocessable_entity
elsif parsed_file.has_too_many_rows?
return render json: { error: t('repositories.import_records.error_message.items_limit', items_size: Constants::IMPORT_REPOSITORY_ITEMS_LIMIT) }, status: :unprocessable_entity
render json: { error: t('repositories.import_records.error_message.items_limit',
items_size: Constants::IMPORT_REPOSITORY_ITEMS_LIMIT) }, status: :unprocessable_entity
else
sheet = SpreadsheetParser.open_spreadsheet(import_params[:file])
duplicate_ids = SpreadsheetParser.duplicate_ids(sheet)
if duplicate_ids.any?
@importing_duplicates_warning = t('repositories.import_records.error_message.importing_duplicates', duplicate_ids: duplicate_ids)
end
@import_data = parsed_file.data
if @import_data.header.blank? || @import_data.columns.blank?
@ -307,64 +303,47 @@ class RepositoriesController < ApplicationController
end
if (@temp_file = parsed_file.generate_temp_file)
render json: {
import_data: @import_data,
temp_file: @temp_file
}
render json: { import_data: @import_data, temp_file: @temp_file }
else
return render json: { error: t('repositories.parse_sheet.errors.temp_file_failure') }, status: :unprocessable_entity
render json: { error: t('repositories.parse_sheet.errors.temp_file_failure') }, status: :unprocessable_entity
end
end
rescue ArgumentError, CSV::MalformedCSVError
return render json: { error: t('repositories.parse_sheet.errors.invalid_file', encoding: ''.encoding) }, status: :unprocessable_entity
render json: { error: t('repositories.parse_sheet.errors.invalid_file', encoding: ''.encoding) },
status: :unprocessable_entity
rescue TypeError
return render json: { error: t('repositories.parse_sheet.errors.invalid_extension') }, status: :unprocessable_entity
render json: { error: t('repositories.parse_sheet.errors.invalid_extension') }, status: :unprocessable_entity
end
end
def import_records
render_403 unless can_create_repository_rows?(Repository.accessible_by_teams(current_team)
.find_by_id(import_params[:id]))
# Access the checkbox values from params
can_edit_existing_items = params[:can_edit_existing_items]
should_overwrite_with_empty_cells = params[:should_overwrite_with_empty_cells]
preview = params[:preview]
.find_by(id: import_params[:id]))
# Check if there exist mapping for repository record (it's mandatory)
if import_params[:mappings].present? && import_params[:mappings].value?('-1')
import_records = repostiory_import_actions
status = import_records.import!(can_edit_existing_items, should_overwrite_with_empty_cells, preview)
status = ImportRepository::ImportRecords
.new(
temp_file: TempFile.find_by(id: import_params[:file_id]),
repository: Repository.accessible_by_teams(current_team).find_by(id: import_params[:id]),
mappings: import_params[:mappings],
session: session,
user: current_user,
can_edit_existing_items: import_params[:can_edit_existing_items],
should_overwrite_with_empty_cells: import_params[:should_overwrite_with_empty_cells],
preview: import_params[:preview]
).import!
message = t('repositories.import_records.partial_success_flash',
nr: status[:nr_of_added], total_nr: status[:total_nr])
if status[:status] == :ok
log_activity(:import_inventory_items,
num_of_items: status[:nr_of_added])
flash[:success] = t('repositories.import_records.success_flash',
number_of_rows: status[:nr_of_added],
total_nr: status[:total_nr])
if preview
render json: status, status: :ok
else
render json: {}, status: :ok
end
log_activity(:import_inventory_items, num_of_items: status[:nr_of_added])
render json: import_params[:preview] ? status : { message: message }, status: :ok
else
flash[:alert] =
t('repositories.import_records.partial_success_flash',
nr: status[:nr_of_added], total_nr: status[:total_nr])
render json: {}, status: :unprocessable_entity
render json: { message: message }, status: :unprocessable_entity
end
else
render json: {
html: render_to_string(
partial: 'shared/flash_errors',
formats: :html,
locals: { error_title: t('repositories.import_records.error_message.errors_list_title'),
error: t('repositories.import_records.error_message.no_repository_name') }
)
}, status: :unprocessable_entity
render json: { error: t('repositories.import_records.error_message.mapping_error') },
status: :unprocessable_entity
end
end
@ -387,8 +366,7 @@ class RepositoriesController < ApplicationController
row_ids: params[:row_ids],
header_ids: params[:header_ids]
},
file_type: params[:empty_export] == '1' ? 'csv' : params[:file_type],
empty_export: params[:empty_export] == '1'
file_type: params[:file_type]
)
update_user_export_file_type if current_user.settings[:repository_export_file_type] != params[:file_type]
log_activity(:export_inventory_items)
@ -480,16 +458,6 @@ class RepositoriesController < ApplicationController
private
def repostiory_import_actions
ImportRepository::ImportRecords.new(
temp_file: TempFile.find_by_id(import_params[:file_id]),
repository: Repository.accessible_by_teams(current_team).find_by_id(import_params[:id]),
mappings: import_params[:mappings],
session: session,
user: current_user
)
end
def load_repository
repository_id = params[:id] || params[:repository_id]
@repository = Repository.accessible_by_teams(current_user.teams).find_by(id: repository_id)
@ -571,7 +539,8 @@ class RepositoriesController < ApplicationController
end
def import_params
params.permit(:id, :file, :file_id, :preview, mappings: {}).to_h
params.permit(:id, :file, :file_id, :preview, :can_edit_existing_items,
:should_overwrite_with_empty_cells, :preview, mappings: {}).to_h
end
def repository_response(message)

View file

@ -5,15 +5,18 @@ class ResultOrderableElementsController < ApplicationController
before_action :check_manage_permissions
def reorder
position_changed = false
ActiveRecord::Base.transaction do
params[:result_orderable_element_positions].each do |id, position|
result_element = @result.result_orderable_elements.find(id)
result_element.insert_at(position)
position_changed ||= result_element.insert_at(position)
end
end
log_activity(:result_content_rearranged, @my_module.experiment.project, my_module: @my_module.id)
@result.touch
if position_changed
log_activity(:result_content_rearranged, @my_module.experiment.project, my_module: @my_module.id)
@result.touch
end
render json: params[:result_orderable_element_positions], status: :ok
rescue ActiveRecord::RecordInvalid

View file

@ -122,7 +122,7 @@ class ResultsController < ApplicationController
def destroy
name = @result.name
if @result.discard
if @result.destroy
log_activity(:destroy_result, { destroyed_result: name })
render json: {}, status: :ok
else

View file

@ -6,16 +6,23 @@ class StepOrderableElementsController < ApplicationController
def reorder
@step.with_lock do
position_changed = false
params[:step_orderable_element_positions].each do |id, position|
@step.step_orderable_elements.find(id).update_column(:position, position)
step_element = @step.step_orderable_elements.find(id)
if step_element.position != position
position_changed = true
step_element.update_column(:position, position)
end
end
if @protocol.in_module?
log_activity(:task_step_content_rearranged, @my_module.experiment.project, my_module: @my_module.id)
else
log_activity(:protocol_step_content_rearranged, nil, protocol: @protocol.id)
if position_changed
if @protocol.in_module?
log_activity(:task_step_content_rearranged, @my_module.experiment.project, my_module: @my_module.id)
else
log_activity(:protocol_step_content_rearranged, nil, protocol: @protocol.id)
end
@step.touch
end
@step.touch
end
render json: params[:step_orderable_element_positions], status: :ok

View file

@ -241,16 +241,23 @@ class StepsController < ApplicationController
def reorder
@protocol.with_lock do
position_changed = false
params[:step_positions].each do |id, position|
@protocol.steps.find(id).update_column(:position, position)
step = @protocol.steps.find(id)
if position != step.position
position_changed = true
step.update_column(:position, position)
end
end
if @protocol.in_module?
log_activity(:task_steps_rearranged, @my_module.experiment.project, my_module: @my_module.id)
else
log_activity(:protocol_steps_rearranged, nil, protocol: @protocol.id)
if position_changed
if @protocol.in_module?
log_activity(:task_steps_rearranged, @my_module.experiment.project, my_module: @my_module.id)
else
log_activity(:protocol_steps_rearranged, nil, protocol: @protocol.id)
end
@protocol.touch
end
@protocol.touch
end
render json: {

View file

@ -8,8 +8,8 @@ const app = createApp({
data() {
return {
myModuleParams: null,
myModuleUrl: null,
tagsModalOpen: false,
tagsUrl: null
};
},
mounted() {
@ -20,6 +20,7 @@ const app = createApp({
},
methods: {
open(myModuleUrl) {
this.myModuleUrl = myModuleUrl;
$.ajax({
url: myModuleUrl,
type: 'GET',
@ -32,6 +33,7 @@ const app = createApp({
},
close() {
this.myModuleParams = null;
this.myModuleUrl = null;
this.tagsModalOpen = false;
},
syncTags(tags) {
@ -52,15 +54,13 @@ const app = createApp({
// Canvas
if ($('#canvas-container').length) {
$.ajax({
url: this.tagsUrl,
url: this.myModuleUrl,
type: 'GET',
dataType: 'json',
success(data) {
$.each(data.my_modules, (index, myModule) => {
$(`div.panel[data-module-id='${myModule.id}']`)
.find('.edit-tags-link')
.html(myModule.tags_html);
});
$(`div.panel[data-module-id='${data.data.id}']`)
.find('.edit-tags-link')
.html(data.data.attributes.tags_html);
}
});
}

View file

@ -17,6 +17,7 @@
:optionsUrl="projectsUrl"
:searchable="true"
:value="selectedProject"
:optionRenderer="newProjectRenderer"
@change="changeProject"
/>
</div>
@ -44,6 +45,7 @@
:disabled="!(selectedProject != null && selectedProject >= 0)"
:searchable="true"
:value="selectedExperiment"
:optionRenderer="newExperimentRenderer"
@change="changeExperiment"
/>
</div>
@ -157,6 +159,18 @@ export default {
this.selectedExperiment = value;
this.newExperimentName = label;
},
newProjectRenderer(option) {
if (option[0] > 0) {
return option[1];
}
return this.i18n.t('dashboard.create_task_modal.new_project', { name: option[1] });
},
newExperimentRenderer(option) {
if (option[0] > 0) {
return option[1];
}
return this.i18n.t('dashboard.create_task_modal.new_experiment', { name: option[1] });
},
closeModal() {
$('#create-task-modal').modal('hide');
this.taskName = '';

View file

@ -6,7 +6,7 @@
position="right"
@dtEvent="changeSort"
btnIcon="sn-icon sn-icon-sort-down"
:e2eSortButton="e2eSortButton"
:dataE2e="e2eSortButton"
></MenuDropdown>
</template>

View file

@ -87,9 +87,14 @@
<div :class="inRepository ? 'protocol-section protocol-information' : ''">
<div v-if="inRepository" id="protocol-description" class="protocol-section-header">
<div class="protocol-description-container">
<a class="protocol-section-caret" role="button" data-toggle="collapse" href="#protocol-description-container" aria-expanded="false" aria-controls="protocol-description-container">
<a class="protocol-section-caret"
role="button"
data-toggle="collapse"
href="#protocol-description-container"
aria-expanded="false"
aria-controls="protocol-description-container">
<i class="sn-icon sn-icon-right"></i>
<span id="protocolDescriptionLabel" class="protocol-section-title">
<span id="protocolDescriptionLabel" class="protocol-section-title" data-e2e="e2e-TX-protocolTemplates-protocolDescription-title">
<h2>
{{ i18n.t("protocols.header.protocol_description") }}
</h2>
@ -97,7 +102,10 @@
</a>
</div>
</div>
<div id="protocol-description-container" class="text-base" :class=" inRepository ? 'protocol-description collapse in' : ''" >
<div id="protocol-description-container"
class="text-base"
:class=" inRepository ? 'protocol-description collapse in' : ''"
data-e2e="e2e-IF-protocolTemplates-protocolDescription-content">
<div v-if="urls.update_protocol_description_url">
<Tinymce
:value="protocol.attributes.description"
@ -125,7 +133,7 @@
<div class="protocol-steps-container">
<a class="protocol-section-caret" role="button" data-toggle="collapse" href="#protocol-steps-container" aria-expanded="false" aria-controls="protocol-steps-container">
<i class="sn-icon sn-icon-right"></i>
<span id="protocolStepsLabel" class="protocol-section-title">
<span id="protocolStepsLabel" class="protocol-section-title" data-e2e="e2e-TX-protocol-templateSteps-title">
<h2>
{{ i18n.t("protocols.header.protocol_steps") }}
</h2>
@ -139,6 +147,7 @@
<a
class="btn btn-secondary"
:title="i18n.t('protocols.steps.new_step_title')"
data-e2e="e2e-BT-protocol-templateSteps-newStepTop"
@keyup.enter="addStep(steps.length)"
@click="addStep(steps.length)"
tabindex="0">
@ -146,17 +155,18 @@
<span>{{ i18n.t("protocols.steps.new_step") }}</span>
</a>
<div v-if="steps.length > 0" class="flex justify-between items-center gap-4">
<button @click="collapseSteps" class="btn btn-secondary flex px-4" tabindex="0">
<button @click="collapseSteps" class="btn btn-secondary flex px-4" tabindex="0" data-e2e="e2e-BT-protocol-templateSteps-collapse">
<i class="sn-icon sn-icon-collapse-all"></i>
{{ i18n.t("protocols.steps.collapse_label") }}
</button>
<button @click="expandSteps" class="btn btn-secondary flex px-4" tabindex="0">
<button @click="expandSteps" class="btn btn-secondary flex px-4" tabindex="0" data-e2e="e2e-BT-protocol-templateSteps-expand">
<i class="sn-icon sn-icon-expand-all"></i>
{{ i18n.t("protocols.steps.expand_label") }}
</button>
<a v-if="steps.length > 0 && urls.reorder_steps_url"
class="btn btn-light icon-btn"
data-toggle="modal"
data-e2e="e2e-BT-protocol-templateSteps-reorder"
@click="startStepReorder"
@keyup.enter="startStepReorder"
:class="{'disabled': steps.length == 1}"
@ -167,7 +177,7 @@
</div>
<div class="protocol-steps pb-8">
<div v-for="(step, index) in steps" :key="step.id" class="step-block">
<div v-if="index > 0 && urls.add_step_url" class="insert-step" @click="addStep(index)">
<div v-if="index > 0 && urls.add_step_url" class="insert-step" @click="addStep(index)" data-e2e="e2e-BT-protocol-templateSteps-insertStep">
<i class="sn-icon sn-icon-new-task"></i>
</div>
<Step
@ -191,7 +201,7 @@
:userSettingsUrl="userSettingsUrl"
:assignableMyModuleId="protocol.attributes.assignable_my_module_id"
/>
<div v-if="(index === steps.length - 1) && urls.add_step_url" class="insert-step" @click="addStep(index + 1)">
<div v-if="(index === steps.length - 1) && urls.add_step_url" class="insert-step" @click="addStep(index + 1)" data-e2e="e2e-BT-protocol-templateSteps-insertStep">
<i class="sn-icon sn-icon-new-task"></i>
</div>
</div>
@ -199,6 +209,7 @@
<a
class="btn btn-secondary"
:title="i18n.t('protocols.steps.new_step_title')"
data-e2e="e2e-BT-protocol-templateSteps-newStepBottom"
@keyup.enter="addStep(steps.length)"
@click="addStep(steps.length)"
tabindex="0">

View file

@ -1,32 +1,33 @@
<template>
<div class="protocol-section protocol-information mb-4">
<div class="protocol-section protocol-information mb-4" data-e2e="e2e-CO-protocolTemplates-protocolDetails">
<div id="protocol-details" class="protocol-section-header">
<div class="protocol-details-container">
<a class="protocol-section-caret" role="button" data-toggle="collapse"
href="#details-container" aria-expanded="false" aria-controls="details-container">
<i class="sn-icon sn-icon-right"></i>
<span id="protocolDetailsLabel" class="protocol-section-title">
<h2>
<h2 data-e2e="e2e-TX-protocolTemplates-protocolDetails-title">
{{ i18n.t("protocols.header.details") }}
</h2>
<span class="protocol-code" >{{ protocol.attributes.code }}</span>
<span class="protocol-code" data-e2e="e2e-TX-protocolTemplates-protocolDetails-protocolId">{{ protocol.attributes.code }}</span>
</span>
</a>
</div>
<div class="actions-block">
<a class="btn btn-light icon-btn pull-right"
:href="protocol.attributes.urls.print_protocol_url" target="_blank">
:href="protocol.attributes.urls.print_protocol_url" target="_blank"
data-e2e="e2e-BT-protocolTemplates-protocolDetails-print">
<span class="sn-icon sn-icon-printer" aria-hidden="true"></span>
</a>
<button class="btn btn-light" @click="openVersionsModal">
<button class="btn btn-light" @click="openVersionsModal" data-e2e="e2e-BT-protocolTemplates-protocolDetails-versions">
{{ i18n.t("protocols.header.versions") }}
</button>
<button v-if="protocol.attributes.urls.publish_url"
@click="$emit('publish')" class="btn btn-primary">
@click="$emit('publish')" class="btn btn-primary" data-e2e="e2e-BT-protocolTemplates-protocolDetails-publish">
{{ i18n.t("protocols.header.publish") }}</button>
<button v-if="protocol.attributes.urls.save_as_draft_url"
:disabled="protocol.attributes.has_draft || creatingDraft"
@click="saveAsdraft" class="btn btn-secondary">
@click="saveAsdraft" class="btn btn-secondary" data-e2e="e2e-BT-protocolTemplates-protocolDetails-saveAsDraft">
{{ i18n.t("protocols.header.save_as_draft") }}
</button>
</div>
@ -34,33 +35,33 @@
<div id="details-container" class="protocol-details collapse in">
<div class="protocol-metadata">
<p class="data-block">
<span>{{ i18n.t("protocols.header.version") }}</span>
<b>{{ titleVersion }}</b>
<span data-e2e="e2e-TX-protocolTemplates-protocolDetails-versionLabel">{{ i18n.t("protocols.header.version") }}</span>
<b data-e2e="e2e-TX-protocolTemplates-protocolDetails-version">{{ titleVersion }}</b>
</p>
<p class="data-block" v-if="protocol.attributes.published">
<span>{{ i18n.t("protocols.header.published_on") }}</span>
<b>{{ protocol.attributes.published_on_formatted }}</b>
<span data-e2e="e2e-TX-protocolTemplates-protocolDetails-publishedOnLabel">{{ i18n.t("protocols.header.published_on") }}</span>
<b data-e2e="e2e-TX-protocolTemplates-protocolDetails-publishedOn">{{ protocol.attributes.published_on_formatted }}</b>
</p>
<p class="data-block" v-if="protocol.attributes.published">
<p class="data-block" v-if="protocol.attributes.published" data-e2e="e2e-TX-protocolTemplates-protocolDetails-publishedBy">
<span>{{ i18n.t("protocols.header.published_by") }}</span>
<img :src="protocol.attributes.published_by.avatar" class="rounded-full"/>
{{ protocol.attributes.published_by.name }}
</p>
<p class="data-block">
<span>{{ i18n.t("protocols.header.updated_at") }}</span>
<b>{{ protocol.attributes.updated_at_formatted }}</b>
<span data-e2e="e2e-TX-protocolTemplates-protocolDetails-updatedAtLabel">{{ i18n.t("protocols.header.updated_at") }}</span>
<b data-e2e="e2e-TX-protocolTemplates-protocolDetails-updatedAt">{{ protocol.attributes.updated_at_formatted }}</b>
</p>
<p class="data-block">
<span>{{ i18n.t("protocols.header.created_at") }}</span>
<b>{{ protocol.attributes.created_at_formatted }}</b>
<span data-e2e="e2e-TX-protocolTemplates-protocolDetails-createdAtLabel">{{ i18n.t("protocols.header.created_at") }}</span>
<b data-e2e="e2e-TX-protocolTemplates-protocolDetails-createdAt">{{ protocol.attributes.created_at_formatted }}</b>
</p>
<p class="data-block">
<p class="data-block" data-e2e="e2e-TX-protocolTemplates-protocolDetails-createdBy">
<span>{{ i18n.t("protocols.header.added_by") }}</span>
<img :src="protocol.attributes.added_by.avatar" class="rounded-full"/>
{{ protocol.attributes.added_by.name }}
</p>
<p class="data-block authors-data">
<span>{{ i18n.t("protocols.header.authors") }}</span>
<span data-e2e="e2e-TX-protocolTemplates-protocolDetails-authorsLabel">{{ i18n.t("protocols.header.authors") }}</span>
<span class="authors-list" v-if="protocol.attributes.urls.update_protocol_authors_url">
<InlineEdit
:value="protocol.attributes.authors"
@ -68,15 +69,16 @@
:allowBlank="true"
:attributeName="`${i18n.t('Protocol')} ${i18n.t('protocols.header.authors_list')}`"
:characterLimit="10000"
:dataE2e="'protocolTemplates-protocolDetails-authors'"
@update="updateAuthors"
/>
</span>
<span class="authors-list" v-else>
<span class="authors-list" data-e2e="e2e-TX-protocolTemplates-protocolDetails-authorsPublished" v-else>
{{ protocol.attributes.authors }}
</span>
</p>
<p class="data-block keywords-data">
<span>{{ i18n.t("protocols.header.keywords") }}</span>
<span data-e2e="e2e-TX-protocolTemplates-protocolDetails-keywordsLabel">{{ i18n.t("protocols.header.keywords") }}</span>
<span
class="keywords-list"
v-if="protocol.attributes.urls.update_protocol_authors_url || protocol.attributes.keywords.length">
@ -90,6 +92,7 @@
:noEmptyOption="false"
:selectAppearance="'tag'"
:viewMode="protocol.attributes.urls.update_protocol_keywords_url == null"
:dataE2e="'protocolTemplates-protocolDetails-keywords'"
@dropdown:changed="updateKeywords"
/>
</span>
@ -117,7 +120,7 @@ export default {
protocol: {
type: Object,
required: true
},
}
},
data() {
return {

View file

@ -6,6 +6,7 @@
@dragover.prevent
:data-id="step.id"
:class="{ 'draging-file': dragingFile, 'editing-name': editingName, 'locked': !urls.update_url, 'pointer-events-none': addingContent }"
:data-e2e="`e2e-CO-protocol-step${step.id}`"
>
<div class="drop-message" @dragleave.prevent="!showFileModal ? dragingFile = false : null">
{{ i18n.t('protocols.steps.drop_message', { position: step.attributes.position + 1 }) }}
@ -18,7 +19,8 @@
:href="'#stepBody' + step.id"
data-toggle="collapse"
data-remote="true"
@click="toggleCollapsed">
@click="toggleCollapsed"
:data-e2e="`e2e-BT-protocol-step${step.id}-toggleCollapsed`">
<span class="sn-icon sn-icon-right "></span>
</a>
<div v-if="!inRepository" class="step-complete-container" :class="{ 'step-element--locked': !urls.state_url }">
@ -27,9 +29,10 @@
@keyup.enter="changeState"
tabindex="0"
:title="step.attributes.completed ? i18n.t('protocols.steps.status.uncomplete') : i18n.t('protocols.steps.status.complete')"
:data-e2e="`e2e-BT-protocol-step${step.id}-toggleCompleted`"
></div>
</div>
<div class="step-position leading-5">
<div class="step-position leading-5" :data-e2e="`e2e-TX-protocol-step${step.id}-position`">
{{ step.attributes.position + 1 }}.
</div>
</div>
@ -48,6 +51,7 @@
@editingEnabled="editingName = true"
@editingDisabled="editingName = false"
:editOnload="step.newStep == true"
:dataE2e="`protocol-step${step.id}-title`"
@update="updateName"
/>
</div>
@ -60,6 +64,7 @@
:btnText="i18n.t('protocols.steps.insert.button')"
:position="'right'"
:caret="true"
:dataE2e="`e2e-DD-protocol-step${step.id}-insertContent`"
@create:table="(...args) => this.createElement('table', ...args)"
@create:checklist="createElement('checklist')"
@create:text="createElement('text')"
@ -98,6 +103,7 @@
:btnClasses="'btn btn-light icon-btn'"
:position="'right'"
:btnIcon="'sn-icon sn-icon-more-hori'"
:dataE2e="`e2e-DD-protocol-step${step.id}-options`"
@reorder="openReorderModal"
@duplicate="duplicateStep"
@delete="showDeleteModal"
@ -116,6 +122,7 @@
:reorderElementUrl="elements.length > 1 ? urls.reorder_elements_url : ''"
:assignableMyModuleId="assignableMyModuleId"
:isNew="element.isNew"
:dataE2e="`protocol-step${step.id}`"
@component:adding-content="($event) => addingContent = $event"
@component:delete="deleteElement"
@update="updateElement"
@ -127,6 +134,7 @@
:parent="step"
:attachments="attachments"
:attachmentsReady="attachmentsReady"
:dataE2e="`protocol-step${step.id}`"
@attachments:openFileModal="showFileModal = true"
@attachment:deleted="attachmentDeleted"
@attachment:update="updateAttachment"
@ -142,6 +150,7 @@
<ReorderableItemsModal v-if="reordering"
:title="i18n.t('protocols.steps.modals.reorder_elements.title', { step_position: step.attributes.position + 1 })"
:items="reorderableElements"
:dataE2e="`e2e-BT-protocol-step${step.id}-reorder`"
@reorder="updateElementOrder"
@close="closeReorderModal"
/>
@ -215,13 +224,34 @@
editingName: false,
inlineEditError: null,
wellPlateOptions: [
{ text: I18n.t('protocols.steps.insert.well_plate_options.32_x_48'), emit: 'create:table', params: [32, 48] },
{ text: I18n.t('protocols.steps.insert.well_plate_options.16_x_24'), emit: 'create:table', params: [16, 24] },
{ text: I18n.t('protocols.steps.insert.well_plate_options.8_x_12'), emit: 'create:table', params: [8, 12] },
{ text: I18n.t('protocols.steps.insert.well_plate_options.6_x_8'), emit: 'create:table', params: [6, 8] },
{ text: I18n.t('protocols.steps.insert.well_plate_options.4_x_6'), emit: 'create:table', params: [4, 6] },
{ text: I18n.t('protocols.steps.insert.well_plate_options.3_x_4'), emit: 'create:table', params: [3, 4]},
{ text: I18n.t('protocols.steps.insert.well_plate_options.2_x_3'), emit: 'create:table', params: [2, 3] }
{ text: I18n.t('protocols.steps.insert.well_plate_options.32_x_48'),
emit: 'create:table',
params: [32, 48],
data_e2e: `e2e-BT-protocol-step${this.step.id}-insertWellPlate-32` },
{ text: I18n.t('protocols.steps.insert.well_plate_options.16_x_24'),
emit: 'create:table',
params: [16, 24],
data_e2e: `e2e-BT-protocol-step${this.step.id}-insertWellPlate-16` },
{ text: I18n.t('protocols.steps.insert.well_plate_options.8_x_12'),
emit: 'create:table',
params: [8, 12],
data_e2e: `e2e-BT-protocol-step${this.step.id}-insertWellPlate-8` },
{ text: I18n.t('protocols.steps.insert.well_plate_options.6_x_8'),
emit: 'create:table',
params: [6, 8],
data_e2e: `e2e-BT-protocol-step${this.step.id}-insertWellPlate-6` },
{ text: I18n.t('protocols.steps.insert.well_plate_options.4_x_6'),
emit: 'create:table',
params: [4, 6],
data_e2e: `e2e-BT-protocol-step${this.step.id}-insertWellPlate-4` },
{ text: I18n.t('protocols.steps.insert.well_plate_options.3_x_4'),
emit: 'create:table',
params: [3, 4],
data_e2e: `e2e-BT-protocol-step${this.step.id}-insertWellPlate-3` },
{ text: I18n.t('protocols.steps.insert.well_plate_options.2_x_3'),
emit: 'create:table',
params: [2, 3],
data_e2e: `e2e-BT-protocol-step${this.step.id}-insertWellPlate-2` }
]
}
},
@ -289,25 +319,29 @@
if (this.urls.upload_attachment_url) {
menu = menu.concat([{
text: this.i18n.t('protocols.steps.insert.add_file'),
emit: 'create:file'
emit: 'create:file',
data_e2e: `e2e-BT-protocol-step${this.step.id}-insertAttachment-file`
}]);
}
if (this.step.attributes.wopi_enabled) {
menu = menu.concat([{
text: this.i18n.t('assets.create_wopi_file.button_text'),
emit: 'create:wopi_file'
emit: 'create:wopi_file',
data_e2e: `e2e-BT-protocol-step${this.step.id}-insertAttachment-wopi`
}]);
}
if (this.step.attributes.open_vector_editor_context.new_sequence_asset_url) {
menu = menu.concat([{
text: this.i18n.t('open_vector_editor.new_sequence_file'),
emit: 'create:ove_file'
emit: 'create:ove_file',
data_e2e: `e2e-BT-protocol-step${this.step.id}-insertAttachment-sequence`
}]);
}
if (this.step.attributes.marvinjs_enabled) {
menu = menu.concat([{
text: this.i18n.t('marvinjs.new_button'),
emit: 'create:marvinjs_file'
emit: 'create:marvinjs_file',
data_e2e: `e2e-BT-protocol-step${this.step.id}-insertAttachment-chemicalDrawing`
}]);
}
return menu;
@ -317,21 +351,26 @@
if (this.urls.update_url) {
menu = menu.concat([{
text: this.i18n.t('protocols.steps.insert.text'),
emit: 'create:text'
emit: 'create:text',
data_e2e: `e2e-BT-protocol-step${this.step.id}-insertText`
},{
text: this.i18n.t('protocols.steps.insert.attachment'),
submenu: this.filesMenu,
position: 'left'
position: 'left',
data_e2e: `e2e-BT-protocol-step${this.step.id}-insertAttachment`
},{
text: this.i18n.t('protocols.steps.insert.table'),
emit: 'create:table'
emit: 'create:table',
data_e2e: `e2e-BT-protocol-step${this.step.id}-insertTable`
},{
text: this.i18n.t('protocols.steps.insert.well_plate'),
submenu: this.wellPlateOptions,
position: 'left'
position: 'left',
data_e2e: `e2e-BT-protocol-step${this.step.id}-insertWellplate`
},{
text: this.i18n.t('protocols.steps.insert.checklist'),
emit: 'create:checklist'
emit: 'create:checklist',
data_e2e: `e2e-BT-protocol-step${this.step.id}-insertChecklist`
}]);
}
@ -342,19 +381,22 @@
if (this.urls.reorder_elements_url) {
menu = menu.concat([{
text: this.i18n.t('protocols.steps.options_dropdown.rearrange'),
emit: 'reorder'
emit: 'reorder',
data_e2e: `e2e-BT-protocol-step${this.step.id}-stepOptions-rearrange`
}]);
}
if (this.urls.duplicate_step_url) {
menu = menu.concat([{
text: this.i18n.t('protocols.steps.options_dropdown.duplicate'),
emit: 'duplicate'
emit: 'duplicate',
data_e2e: `e2e-BT-protocol-step${this.step.id}-stepOptions-duplicate`
}]);
}
if (this.urls.delete_url) {
menu = menu.concat([{
text: this.i18n.t('protocols.steps.options_dropdown.delete'),
emit: 'delete'
emit: 'delete',
data_e2e: `e2e-BT-protocol-step${this.step.id}-stepOptions-delete`
}]);
}
return menu;

View file

@ -2,31 +2,32 @@
<div ref="modal" class="modal" tabindex="-1" role="dialog">
<div class="modal-dialog" role="document">
<form @submit.prevent="submit">
<div class="modal-content">
<div class="modal-content" data-e2e="e2e-MD-newProtocol">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<button type="button" class="close" data-dismiss="modal" aria-label="Close" data-e2e="e2e-BT-newProtocolModal-close">
<i class="sn-icon sn-icon-close"></i>
</button>
<h4 class="modal-title truncate !block" id="edit-project-modal-label">
<h4 class="modal-title truncate !block" id="edit-project-modal-label" data-e2e="e2e-TX-newProtocolModal-title">
{{ i18n.t("protocols.new_protocol_modal.title_new") }}
</h4>
</div>
<div class="modal-body">
<div class="mb-6">
<label class="sci-label">{{ i18n.t("protocols.new_protocol_modal.name_label") }}</label>
<label class="sci-label" data-e2e="e2e-TX-newProtocolModal-inputLabel">{{ i18n.t("protocols.new_protocol_modal.name_label") }}</label>
<div class="sci-input-container-v2" :class="{'error': error}" :data-error="error">
<input type="text" v-model="name"
class="sci-input-field"
autofocus="true" ref="input"
data-e2e="e2e-IF-newProtocolModal-name"
:placeholder="i18n.t('protocols.new_protocol_modal.name_placeholder')" />
</div>
</div>
<div class="flex gap-2 text-xs items-center">
<div class="sci-checkbox-container">
<input type="checkbox" class="sci-checkbox" v-model="visible" value="visible"/>
<input type="checkbox" class="sci-checkbox" v-model="visible" value="visible" data-e2e="e2e-CB-newProtocolModal-grantAccess"/>
<span class="sci-checkbox-label"></span>
</div>
<span v-html="i18n.t('protocols.new_protocol_modal.access_label')"></span>
<span v-html="i18n.t('protocols.new_protocol_modal.access_label')" data-e2e="e2e-TX-newProtocolModal-grantAccess"></span>
</div>
<div class="mt-6" :class="{'hidden': !visible}">
<label class="sci-label">{{ i18n.t("protocols.new_protocol_modal.role_label") }}</label>
@ -34,8 +35,8 @@
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">{{ i18n.t('general.cancel') }}</button>
<button class="btn btn-primary" type="submit" :disabled="visible && !defaultRole">
<button type="button" class="btn btn-secondary" data-dismiss="modal" data-e2e="e2e-BT-newProtocolModal-cancel">{{ i18n.t('general.cancel') }}</button>
<button class="btn btn-primary" type="submit" :disabled="visible && !defaultRole" data-e2e="e2e-BT-newProtocolModal-create">
{{ i18n.t('protocols.new_protocol_modal.create_new') }}
</button>
</div>

View file

@ -203,7 +203,8 @@ export default {
menuItems: [
{
emit: 'import_file',
text: this.i18n.t('protocols.index.import_eln')
text: this.i18n.t('protocols.index.import_eln'),
data_e2e: 'e2e-BT-topToolbar-importEln'
}
]
};
@ -214,13 +215,15 @@ export default {
text: `<span>${this.i18n.t('protocols.index.import_docx')}</span>
<span class="bg-sn-coral text-sn-white text-[8px] absolute leading-none p-1 top-px rounded-[1px] right-px">
${this.i18n.t('protocols.index.beta')}
</span>`
</span>`,
data_e2e: 'e2e-BT-topToolbar-importDocx'
});
}
importMenu.menuItems.push({
emit: 'import_protocols_io',
text: this.i18n.t('protocols.index.import_protocols_io')
text: this.i18n.t('protocols.index.import_protocols_io'),
data_e2e: 'e2e-BT-topToolbar-importProtocolsIo'
});
left.push(importMenu);

View file

@ -1,33 +1,36 @@
<template>
<div v-if="modalOpened">
<component
v-if="activeStep !== 'ExportModal'"
:is="activeStep"
:params="params"
:uploading="uploading"
@uploadFile="uploadFile"
@generatePreview="generatePreview"
@changeStep="changeStep"
@importRows="importRecords"
/>
<ExportModal
v-else
:rows="[{id: params.id, team: params.attributes.team_name}]"
:exportAction="params.attributes.export_actions"
@close="activeStep ='UploadStep'"
@export="activeStep = 'UploadStep'"
/>
<div v-if="modalOpened" class="relative">
<component
v-if="activeStep !== 'ExportModal'"
:is="activeStep"
:params="params"
:key="modalId"
:uploading="uploading"
:loading="loading"
@uploadFile="uploadFile"
@generatePreview="generatePreview"
@changeStep="changeStep"
@importRows="importRecords"
/>
<ExportModal
v-else
:rows="[{id: params.id, team: params.attributes.team_name}]"
:exportAction="params.attributes.export_actions"
@close="activeStep ='UploadStep'"
@export="activeStep = 'UploadStep'"
/>
</div>
</template>
<script>
/* global HelperModule */
import axios from '../../../../packs/custom_axios';
import InfoModal from '../../../shared/info_modal.vue';
import UploadStep from './upload_step.vue';
import MappingStep from './mapping_step.vue';
import PreviewStep from './preview_step.vue';
import SuccessStep from './success_step.vue';
import ExportModal from '../export.vue';
export default {
@ -37,7 +40,6 @@ export default {
UploadStep,
MappingStep,
PreviewStep,
SuccessStep,
ExportModal
},
props: {
@ -49,7 +51,9 @@ export default {
modalOpened: false,
activeStep: 'UploadStep',
uploading: false,
params: {}
params: {},
modalId: null,
loading: false
};
},
created() {
@ -64,6 +68,7 @@ export default {
axios.get(this.repositoryUrl)
.then((response) => {
this.params = response.data.data;
this.modalId = Math.random().toString(36);
this.modalOpened = true;
});
},
@ -94,6 +99,10 @@ export default {
this.activeStep = step;
},
importRecords(preview) {
if (this.loading) {
return;
}
const jsonData = {
file_id: this.params.temp_file.id,
mappings: this.params.mapping,
@ -102,6 +111,9 @@ export default {
should_overwrite_with_empty_cells: this.params.updateWithEmptyCells,
can_edit_existing_items: !this.params.onlyAddNewItems
};
this.loading = true;
axios.post(this.params.attributes.urls.import_records, jsonData)
.then((response) => {
if (preview) {
@ -109,8 +121,17 @@ export default {
this.params.import_date = response.data.import_date;
this.activeStep = 'PreviewStep';
} else {
this.activeStep = 'SuccessStep';
HelperModule.flashAlertMsg(response.data.message, 'success');
this.modalOpened = false;
this.activeStep = null;
$('.dataTable.repository-dataTable').DataTable().ajax.reload(null, false);
}
this.loading = false;
})
.catch((error) => {
HelperModule.flashAlertMsg(error.response.data.message, 'danger');
this.loading = false;
});
}
}

View file

@ -1,6 +1,7 @@
<template>
<div ref="modal" class="modal" tabindex="-1" role="dialog">
<div class="modal-dialog modal-lg" role="document">
<Loading v-if="loading" />
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close" data-e2e="e2e-BT-newInventoryModal-close">
@ -11,18 +12,20 @@
</h4>
</div>
<div class="modal-body">
<p class="text-sn-dark-grey">
{{ this.i18n.t('repositories.import_records.steps.step2.subtitle') }}
</p>
<div class="flex gap-6 items-center my-6">
<div class="flex items-center gap-1">
<div class="flex items-center gap-2" :title="i18n.t('repositories.import_records.steps.step2.autoMappingTooltip')">
<div class="sci-checkbox-container">
<input type="checkbox" class="sci-checkbox" v-model="autoMapping" />
<span class="sci-checkbox-label"></span>
</div>
{{ i18n.t('repositories.import_records.steps.step2.autoMappingText') }}
</div>
<div class="flex items-center gap-1">
<!--
<div class="flex items-center gap-1">
<div class="sci-checkbox-container my-auto">
<input type="checkbox" class="sci-checkbox" :checked="updateWithEmptyCells" @change="toggleUpdateWithEmptyCells"/>
<span class="sci-checkbox-label"></span>
@ -36,13 +39,14 @@
</div>
{{ i18n.t('repositories.import_records.steps.step2.onlyAddNewItemsText') }}
</div>
-->
</div>
{{ i18n.t('repositories.import_records.steps.step2.importedFileText') }} {{ params.file_name }}
<hr class="m-0 mt-6">
<div class="grid grid-cols-[3rem_auto_1.5rem_auto_5rem_auto] px-2">
<div v-for="(column, key) in columnLabels" class="flex items-center px-2 py-2">{{ column }}</div>
<div v-for="(column, key) in columnLabels" class="flex items-center px-2 py-2 font-bold">{{ column }}</div>
<template v-for="(item, index) in params.import_data.header" :key="item">
<MappingStepTableRow
@ -50,7 +54,7 @@
:item="item"
:dropdownOptions="computedDropdownOptions"
:params="params"
:selected="this.selectedItemsIndexes.includes(index)"
:value="this.selectedItems.find((item) => item.index === index)"
@selection:changed="handleChange"
:autoMapping="this.autoMapping"
/>
@ -76,7 +80,7 @@
<button class="btn btn-secondary ml-auto" @click="close" aria-label="Close">
{{ i18n.t('repositories.import_records.steps.step2.cancelBtnText') }}
</button>
<button class="btn btn-primary" :disabled="!rowsIsValid" @click="importRecords">
<button class="btn btn-primary" @click="importRecords">
{{ i18n.t('repositories.import_records.steps.step2.confirmBtnText') }}
</button>
</div>
@ -90,6 +94,7 @@ import axios from '../../../../packs/custom_axios';
import SelectDropdown from '../../../shared/select_dropdown.vue';
import MappingStepTableRow from './mapping_step_table_row.vue';
import modalMixin from '../../../shared/modal_mixin';
import Loading from '../../../shared/loading.vue';
export default {
name: 'MappingStep',
@ -97,17 +102,22 @@ export default {
mixins: [modalMixin],
components: {
SelectDropdown,
MappingStepTableRow
MappingStepTableRow,
Loading
},
props: {
params: {
type: Object,
required: true
},
loading: {
type: Boolean,
required: true
}
},
data() {
return {
autoMapping: true,
autoMapping: false,
updateWithEmptyCells: false,
onlyAddNewItems: false,
columnLabels: {
@ -119,7 +129,6 @@ export default {
5: this.i18n.t('repositories.import_records.steps.step2.table.columnLabels.exampleData')
},
selectedItems: [],
selectedItemsIndexes: [],
importRecordsUrl: null,
teamId: null,
repositoryId: null,
@ -130,131 +139,77 @@ export default {
};
},
methods: {
toggleUpdateWithEmptyCells() {
this.updateWithEmptyCells = !this.updateWithEmptyCells;
},
toggleOnlyAddNewItems() {
this.onlyAddNewItems = !this.onlyAddNewItems;
},
handleChange(payload) {
this.error = null;
const { index, key, value } = payload;
// checking if the mapping is already selected
const foundItem = this.selectedItems.find((item) => item.index === index);
const item = this.selectedItems.find((i) => i.index === index);
const usedBeforeItem = this.selectedItems.find((i) => i.key === key && i.index !== index);
// if it's not, add it
if (!foundItem && key) {
this.selectedItems = [...this.selectedItems, { index, key, value }];
this.selectedItemsIndexes.push(index);
}
// if it is but the key is null then clear it
if (foundItem && !key) {
const indexToRemoveObj = this.selectedItems.findIndex((item) => item.index === index);
const indexToRemoveStr = this.selectedItemsIndexes.indexOf(index);
if ((indexToRemoveObj !== -1) && (indexToRemoveStr !== -1)) {
this.selectedItems.splice(indexToRemoveObj, 1);
this.selectedItemsIndexes.splice(indexToRemoveStr, 1);
}
}
// if it is and the key is not null then update it
if (foundItem && key) {
const indexToRemoveObj = this.selectedItems.findIndex((item) => item.index === index);
this.selectedItems.splice(indexToRemoveObj, 1);
this.selectedItems = [...this.selectedItems, { index, key, value }];
if (usedBeforeItem) {
usedBeforeItem.key = null;
usedBeforeItem.value = null;
}
this.updateAvailableItemsStatus();
},
// necessary for tracking which options are already selected
updateAvailableItemsStatus() {
let updatedAvailableFields = [];
const selectedItemsKeys = new Set(this.selectedItems.map((item) => item.key));
this.alwaysAvailableFields.forEach((field) => {
if (selectedItemsKeys.has(field.key)) {
const tempObj = { key: field.key, value: field.value, alreadySelected: true };
updatedAvailableFields.push(tempObj);
} else {
updatedAvailableFields.push(field);
}
});
this.availableFields = updatedAvailableFields;
updatedAvailableFields = [];
item.key = key;
item.value = value;
},
generateMapping() {
const mapping = {};
for (let i = 0; i < this.params.import_data.header.length; i++) {
const foundItem = this.selectedItems.find((item) => item.index === i);
if (foundItem) {
mapping[foundItem.index] = (foundItem.key === 'new' ? foundItem.value : foundItem.key);
} else {
mapping[i] = '';
}
}
return mapping;
return this.selectedItems.reduce((obj, item) => {
obj[item.index] = item.key || '';
return obj;
}, {});
},
importRecords() {
const selectedItemsKeys = new Set(this.selectedItems.map((item) => item.key));
if (!selectedItemsKeys.has('-1')) {
if (!this.selectedItems.find((item) => item.key === '-1')) {
this.error = this.i18n.t('repositories.import_records.steps.step2.selectNamePropertyError');
return '';
}
this.$emit(
'generatePreview',
this.generateMapping(),
this.updateWithEmptyCells,
this.onlyAddNewItems
this.generateMapping()
);
return true;
}
},
computed: {
computedDropdownOptions() {
const columnKeyToLabelMapping = {};
columnKeyToLabelMapping[-1] = this.i18n.t('repositories.import_records.steps.step2.computedDropdownOptions.name');
if (this.repositoryColumns) {
this.repositoryColumns.forEach((el) => {
const [key, colName, colType] = el;
columnKeyToLabelMapping[key] = this.i18n.t(`repositories.import_records.steps.step2.computedDropdownOptions.${colType}`);
});
}
if (this.availableFields) {
let options = this.availableFields.map((el) => [String(el.key), `${String(el.value)} (${columnKeyToLabelMapping[el.key]})`]);
options = [['new', this.i18n.t('repositories.import_records.steps.step2.table.tableRow.importAsNewColumn')]].concat(options);
return options;
}
return [];
return this.availableFields
.map((el) => [String(el.key), `${String(el.value)} (${el.typeName})`]);
// options = [['new', this.i18n.t('repositories.import_records.steps.step2.table.tableRow.importAsNewColumn')]].concat(options);
},
computedImportedIgnoredInfo() {
const importedSum = this.selectedItems.length;
const ignoredSum = this.params.import_data.header.length - importedSum;
const importedSum = this.selectedItems.filter((i) => i.key).length;
const ignoredSum = this.selectedItems.length - importedSum;
return { importedSum, ignoredSum };
},
rowsIsValid() {
let valid = true;
this.selectedItems.forEach((v) => {
if (v.key === 'new' && (!v.value.type || v.value.name.length < 2)) {
valid = false;
}
});
return valid;
}
},
created() {
this.repositoryColumns = this.params.attributes.repository_columns;
// Adding alreadySelected attribute for tracking
const tempAvailableFields = [];
this.availableFields = [];
this.selectedItems = this.params.import_data.header.map((item, index) => { return { index, key: null, value: null }; });
Object.entries(this.params.import_data.available_fields).forEach(([key, value]) => {
const field = { key, value, alreadySelected: false };
tempAvailableFields.push(field);
let columnTypeName = '';
if (key === '-1') {
columnTypeName = this.i18n.t('repositories.import_records.steps.step2.computedDropdownOptions.name');
} else if (key === '0') {
columnTypeName = this.i18n.t('repositories.import_records.steps.step2.computedDropdownOptions.id');
} else {
const column = this.repositoryColumns.find((el) => el[0] === parseInt(key, 10));
columnTypeName = this.i18n.t(`repositories.import_records.steps.step2.computedDropdownOptions.${column[2]}`);
}
const field = {
key, value, alreadySelected: false, typeName: columnTypeName
};
this.availableFields.push(field);
});
this.availableFields = tempAvailableFields;
this.alwaysAvailableFields = tempAvailableFields;
},
mounted() {
this.autoMapping = true;
}
};
</script>

View file

@ -17,25 +17,21 @@
<div class="py-1 min-h-12 flex items-center flex-col gap-2 px-2" :class="{
'bg-sn-super-light-blue': selected
}">
<!-- system generated data -->
<SelectDropdown v-if="systemGeneratedData.includes(item)"
:disabled="true"
:placeholder="String(item)"
></SelectDropdown>
<SelectDropdown
v-else
:options="dropdownOptions"
@change="changeSelected"
:clearable="true"
:size="'sm'"
:class="{
'outline-sn-alert-brittlebush outline-1 outline rounded': matchNotFound
}"
:placeholder="computeMatchNotFound ?
i18n.t('repositories.import_records.steps.step2.table.tableRow.placeholders.matchNotFound') :
i18n.t('repositories.import_records.steps.step2.table.tableRow.placeholders.doNotImport')"
:title="this.selectedColumnType?.value"
:value="this.selectedColumnType?.key"
></SelectDropdown>
<template v-if="selectedColumnType?.key == 'new'">
<template v-if="false">
<SelectDropdown
:options="newColumnTypes"
@change="(v) => { newColumn.type = v }"
@ -52,31 +48,12 @@
<div class="py-1 min-h-12 px-2 flex items-center" :class="{
'bg-sn-super-light-blue': selected
}">
<!-- import -->
<i v-if="this.selectedColumnType?.key && this.selectedColumnType?.value === item && !systemGeneratedData.includes(item)"
class="sn-icon sn-icon-check" :title="i18n.t('repositories.import_records.steps.step2.table.tableRow.importedColumnTitle')">
</i>
<!-- default column -->
<i v-else-if="systemGeneratedData.includes(item)"
class="sn-icon sn-icon-check text-sn-sleepy-grey" :title="i18n.t('repositories.import_records.steps.step2.table.tableRow.defaultColumnTitle')">
</i>
<!-- user defined this column -->
<i v-else-if="this.selectedColumnType?.key && this.selectedColumnType?.value !== item"
class="sn-icon sn-icon-info text-sn-science-blue"
:title="`${i18n.t('repositories.import_records.steps.step2.table.tableRow.userDefinedColumnTitle')} ${this.selectedColumnType.value}`"></i>
<!-- error: can not import -->
<!-- <i v-else-if=""></i> -->
<!-- match not found -->
<i v-else-if="computeMatchNotFound"
class="sn-icon sn-icon-close text-sn-alert-brittlebush" :title="i18n.t('repositories.import_records.steps.step2.table.tableRow.matchNotFoundColumnTitle')">
</i>
<!-- do not import -->
<i v-else class="sn-icon sn-icon-close text-sn-sleepy-grey" :title="i18n.t('repositories.import_records.steps.step2.table.tableRow.doNotImportColumnTitle')"></i>
<i v-if="differentMapingName" :title="i18n.t('repositories.import_records.steps.step2.table.tableRow.importedColumnTitle')"
class="sn-icon sn-icon-info text-sn-science-blue"></i>
<i v-else-if="columnMapped" :title="i18n.t('repositories.import_records.steps.step2.table.tableRow.importedColumnTitle')" class="sn-icon sn-icon-check"></i>
<i v-else-if="matchNotFound" :title="i18n.t('repositories.import_records.steps.step2.table.tableRow.matchNotFoundColumnTitle')"
class="sn-icon sn-icon-close text-sn-alert-brittlebush"></i>
<i v-else :title="i18n.t('repositories.import_records.steps.step2.table.tableRow.doNotImportColumnTitle')" class="sn-icon sn-icon-close text-sn-sleepy-grey"></i>
</div>
<div class="py-1 min-h-12 px-2 flex items-center" :title="params.import_data.columns[index]" :class="{
@ -114,10 +91,7 @@ export default {
type: Boolean,
required: true
},
selected: {
type: Boolean,
required: false
}
value: Object
},
data() {
return {
@ -129,24 +103,16 @@ export default {
newColumnTypes: [
['Text', this.i18n.t('repositories.import_records.steps.step2.table.tableRow.newColumnType.text')],
['List', this.i18n.t('repositories.import_records.steps.step2.table.tableRow.newColumnType.list')]
],
systemGeneratedData: [
this.i18n.t('repositories.import_records.steps.step2.table.tableRow.systemGeneratedData.itemId'),
this.i18n.t('repositories.import_records.steps.step2.table.tableRow.systemGeneratedData.createdOn'),
this.i18n.t('repositories.import_records.steps.step2.table.tableRow.systemGeneratedData.addedBy'),
this.i18n.t('repositories.import_records.steps.step2.table.tableRow.systemGeneratedData.addedOn'),
this.i18n.t('repositories.import_records.steps.step2.table.tableRow.systemGeneratedData.archivedBy'),
this.i18n.t('repositories.import_records.steps.step2.table.tableRow.systemGeneratedData.archivedOn'),
this.i18n.t('repositories.import_records.steps.step2.table.tableRow.systemGeneratedData.updatedBy'),
this.i18n.t('repositories.import_records.steps.step2.table.tableRow.systemGeneratedData.updatedOn')]
]
};
},
watch: {
newColumn() {
this.selectedColumnType.value = this.newColumn;
this.$emit('selection:changed', this.selectedColumnType);
selected() {
if (this.value?.key === null) {
this.selectedColumnType = null;
}
},
autoMapping(newVal, oldVal) {
autoMapping(newVal) {
if (newVal === true) {
this.autoMap();
} else {
@ -157,6 +123,18 @@ export default {
computed: {
computeMatchNotFound() {
return this.autoMapping && ((this.selectedColumnType && !this.selectedColumnType.key) || !this.selectedColumnType);
},
selected() {
return !!this.value?.key;
},
differentMapingName() {
return this.columnMapped && this.selectedColumnType?.value !== this.item;
},
matchNotFound() {
return this.autoMapping && !this.selectedColumnType?.key;
},
columnMapped() {
return this.selectedColumnType?.key;
}
},
methods: {
@ -171,15 +149,9 @@ export default {
this.changeSelected(null);
},
changeSelected(e) {
let value;
if (e === 'new') {
value = this.newColumn;
} else {
value = this.params.import_data.available_fields[e];
}
const selectedColumnType = { index: this.index, key: e, value };
this.selectedColumnType = selectedColumnType;
this.$emit('selection:changed', selectedColumnType);
const value = this.params.import_data.available_fields[e];
this.selectedColumnType = { index: this.index, key: e, value };
this.$emit('selection:changed', this.selectedColumnType);
}
},
mounted() {

View file

@ -1,6 +1,7 @@
<template>
<div ref="modal" class="modal" tabindex="-1" role="dialog">
<div class="modal-dialog modal-lg" role="document">
<Loading v-if="loading" />
<div class="modal-content grow">
<div class="modal-header gap-4">
<button type="button" class="close" data-dismiss="modal" aria-label="Close" data-e2e="e2e-BT-newInventoryModal-close">
@ -11,6 +12,7 @@
</h4>
</div>
<div class="modal-body">
<p class="text-sn-dark-grey mb-6">
{{ i18n.t('repositories.import_records.steps.step3.subtitle', { inventory: params.attributes.name }) }}
</p>
@ -18,32 +20,32 @@
<div>
<div v-html="i18n.t('repositories.import_records.steps.step3.updated_items')"></div>
<hr class="my-1">
<h2 class="m-0 text-sn-alert-green">0</h2>
<h2 class="m-0 text-sn-alert-green">{{ counters.updated }}</h2>
</div>
<div>
<div v-html="i18n.t('repositories.import_records.steps.step3.new_items')"></div>
<hr class="my-1">
<h2 class="m-0 text-sn-alert-green">0</h2>
<h2 class="m-0 text-sn-alert-green">{{ counters.created }}</h2>
</div>
<div>
<div v-html="i18n.t('repositories.import_records.steps.step3.unchanged_items')"></div>
<hr class="my-1">
<h2 class="m-0 ">0</h2>
<h2 class="m-0 ">{{ counters.unchanged }}</h2>
</div>
<div>
<div v-html="i18n.t('repositories.import_records.steps.step3.duplicated_items')"></div>
<hr class="my-1">
<h2 class="m-0 ">0</h2>
<h2 class="m-0 text-sn-alert-passion">{{ counters.duplicated }}</h2>
</div>
<div>
<div v-html="i18n.t('repositories.import_records.steps.step3.invalid_items')"></div>
<hr class="my-1">
<h2 class="m-0 text-sn-alert-passion">0</h2>
<h2 class="m-0 text-sn-alert-passion">{{ counters.invalid }}</h2>
</div>
<div>
<div v-html="i18n.t('repositories.import_records.steps.step3.invalid_items')"></div>
<div v-html="i18n.t('repositories.import_records.steps.step3.archived_items')"></div>
<hr class="my-1">
<h2 class="m-0 text-sn-alert-passion">0</h2>
<h2 class="m-0">{{ counters.archived }}</h2>
</div>
</div>
<div class="my-6">
@ -67,7 +69,7 @@
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" @click="$emit('changeStep', 'MappingStep')">
{{ i18n.t('repositories.import_records.steps.step3.cancel') }}
{{ i18n.t('general.back') }}
</button>
<button type="button" class="btn btn-primary" @click="$emit('importRows')">
{{ i18n.t('repositories.import_records.steps.step3.confirm') }}
@ -81,7 +83,7 @@
<script>
import { AgGridVue } from 'ag-grid-vue3';
import modalMixin from '../../../shared/modal_mixin';
import Loading from '../../../shared/loading.vue';
export default {
name: 'PreviewStep',
@ -90,16 +92,31 @@ export default {
params: {
type: Object,
required: true
},
loading: {
type: Boolean,
required: true
}
},
components: {
AgGridVue
AgGridVue,
Loading
},
data() {
return {
};
},
computed: {
counters() {
return {
updated: this.filterRows('updated').length,
created: this.filterRows('created').length,
unchanged: this.filterRows('unchanged').length,
duplicated: this.filterRows('duplicated').length,
invalid: this.filterRows('invalid').length,
archived: this.filterRows('archived').length
};
},
columnDefs() {
const columns = [
{
@ -120,8 +137,9 @@ export default {
});
columns.push({
field: 'status',
field: 'import_status',
headerName: this.i18n.t('repositories.import_records.steps.step3.status'),
cellRenderer: this.statusRenderer,
pinned: 'right'
});
@ -142,6 +160,40 @@ export default {
}
},
methods: {
filterRows(status) {
return this.params.preview.data.filter((r) => r.attributes.import_status === status);
},
statusRenderer(params) {
const { import_status: importStatus, import_message: importMessage } = params.data;
let message = '';
let color = '';
let icon = '';
if (importStatus === 'created' || importStatus === 'updated') {
message = this.i18n.t(`repositories.import_records.steps.step3.status_message.${importStatus}`);
color = 'text-sn-alert-green';
icon = 'check';
} else if (importStatus === 'unchanged' || importStatus === 'archived') {
message = this.i18n.t(`repositories.import_records.steps.step3.status_message.${importStatus}`);
icon = 'hamburger';
} else if (importStatus === 'duplicated' || importStatus === 'invalid') {
message = this.i18n.t(`repositories.import_records.steps.step3.status_message.${importStatus}`);
color = 'text-sn-alert-passion';
icon = 'close';
}
if (importMessage) {
message = importMessage;
}
return `
<div class="flex items-center ${color} gap-2.5">
<i class="sn-icon sn-icon-${icon} "></i>
<span>${message}</span>
</div>
`;
}
}
};
</script>

View file

@ -1,53 +0,0 @@
<template>
<div ref="modal" class="modal" tabindex="-1" role="dialog">
<div class="modal-dialog modal-sm" role="document">
<div class="modal-content grow">
<div class="modal-header gap-4">
<button type="button" class="close" data-dismiss="modal" aria-label="Close" data-e2e="e2e-BT-newInventoryModal-close">
<i class="sn-icon sn-icon-close"></i>
</button>
<h4 class="modal-title truncate !block" id="edit-project-modal-label" data-e2e="e2e-TX-newInventoryModal-title">
{{ i18n.t('repositories.import_records.steps.step4.title') }}
</h4>
</div>
<div class="modal-body">
<p class="text-sn-dark-grey mb-6">
{{ i18n.t('repositories.import_records.steps.step4.subtitle', { inventory: params.attributes.name }) }}
</p>
<div>
<b>{{ i18n.t('repositories.import_records.steps.step4.import_date') }}</b>
{{ params.import_date }}
</div>
<div>
<b>{{ i18n.t('repositories.import_records.steps.step4.imported_file') }}</b>
{{ params.file_name }}
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" >
{{ i18n.t('repositories.import_records.steps.step4.download_report') }}
</button>
<button type="button" class="btn btn-primary" @click="close">
{{ i18n.t('repositories.import_records.steps.step4.close') }}
</button>
</div>
</div>
</div>
</div>
</template>
<script>
import modalMixin from '../../../shared/modal_mixin';
export default {
name: 'SuccessStep',
mixins: [modalMixin],
props: {
params: {
type: Object,
required: true
}
}
};
</script>

View file

@ -1,10 +1,10 @@
<template>
<div ref="modal" class="modal" tabindex="-1" role="dialog">
<div class="modal-dialog flex" role="document" :class="{'!w-[900px]': showingInfo}">
<div v-if="showingInfo" class="w-[300px] h-full bg-sn-super-light-grey p-6 rounded-s text-sn-dark-grey">
<div v-if="showingInfo" class="w-[300px] shrink-0 h-full bg-sn-super-light-grey p-6 rounded-s text-sn-dark-grey">
<h3 class="my-0 mb-4">{{ this.i18n.t('repositories.import_records.info_sidebar.title') }}</h3>
<div v-for="i in 4" :key="i" class="flex gap-3 mb-4">
<span class="btn btn-secondary icon-btn !text-sn-black">
<span class="btn btn-secondary icon-btn !text-sn-black !pointer-events-none">
<i class="sn-icon"
:class="i18n.t(`repositories.import_records.info_sidebar.elements.element${i - 1}.icon`)"
></i>
@ -15,10 +15,10 @@
</div>
</div>
<div class="flex gap-3 mb-4 items-center">
<span class="btn btn-secondary icon-btn !text-sn-black">
<span class="btn btn-secondary icon-btn !text-sn-black !pointer-events-none">
<i class="sn-icon sn-icon-open"></i>
</span>
<a :href="i18n.t('repositories.import_records.info_sidebar.elements.element4.linkTo')" class="font-bold">
<a :href="i18n.t('repositories.import_records.info_sidebar.elements.element4.linkTo')" class="font-bold" target="_blank">
{{ i18n.t('repositories.import_records.info_sidebar.elements.element4.label') }}
</a>
</div>
@ -85,13 +85,12 @@
</template>
<script>
import axios from '../../../../packs/custom_axios';
import DragAndDropUpload from '../../../shared/drag_and_drop_upload.vue';
import modalMixin from '../../../shared/modal_mixin';
export default {
name: 'UploadStep',
emits: ['uploadFile'],
emits: ['uploadFile', 'close'],
components: {
DragAndDropUpload
},

View file

@ -1,5 +1,7 @@
<template>
<div class="content__attachments pr-8" :id='"content__attachments-" + parent.id'>
<div class="content__attachments pr-8"
:id='"content__attachments-" + parent.id'
:data-e2e="`e2e-CO-${dataE2e}-attachments`">
<div class="sci-divider my-6"></div>
<div class="content__attachments-actions">
<div class="title">
@ -11,6 +13,7 @@
:btnText="i18n.t('attachments.preview_menu')"
:position="'right'"
:caret="true"
:data_e2e="`e2e-DD-${dataE2e}-attachments-viewOptions`"
@attachment:viewMode = "changeAttachmentsViewMode"
></MenuDropdown>
<MenuDropdown
@ -18,6 +21,7 @@
:btnIcon="'sn-icon sn-icon-sort-down'"
:btnClasses="'btn btn-light icon-btn'"
:position="'right'"
:data_e2e="`e2e-DD-${dataE2e}-attachments-orderOptions`"
@attachment:order = "changeAttachmentsOrder"
></MenuDropdown>
</div>
@ -29,6 +33,7 @@
:is="attachment_view_mode(attachmentsOrdered[index])"
:attachment="attachment"
:parentId="parseInt(parent.id)"
:dataE2e="`${dataE2e}`"
@attachment:viewMode="updateAttachmentViewMode"
@attachment:delete="deleteAttachment(attachment.id)"
@attachment:moved="attachmentMoved"
@ -64,6 +69,10 @@ export default {
attachmentsReady: {
type: Boolean,
required: true
},
dataE2e: {
type: String,
default: ''
}
},
data() {
@ -119,7 +128,8 @@ export default {
active: this.parent.attributes.assets_view_mode == viewMode,
text: this.i18n.t(`attachments.view_mode.${viewMode}_html`),
emit: 'attachment:viewMode',
params: viewMode
params: viewMode,
data_e2e: `e2e-BT-${this.dataE2e}-viewOptions-${viewMode}`
});
});
return menu;
@ -131,7 +141,8 @@ export default {
text: this.i18n.t(`general.sort_new.${orderOption}`),
emit: 'attachment:order',
params: orderOption,
active: this.parent.attributes.assets_order === orderOption
active: this.parent.attributes.assets_order === orderOption,
data_e2e: `e2e-BT-${this.dataE2e}-orderOptions-${orderOption}`
});
});
return menu;

View file

@ -1,5 +1,7 @@
<template>
<div class="attachment-container asset" :data-asset-id="attachment.id">
<div class="attachment-container asset"
:data-asset-id="attachment.id"
:data-e2e="`e2e-CO-${dataE2e}-attachment${attachment.id}-empty`">
<div
class="file-name"
:id="`modal_link${attachment.id}`"
@ -23,6 +25,10 @@ export default {
parentId: {
type: Number,
required: true
},
dataE2e: {
type: String,
default: ''
}
}
};

View file

@ -2,6 +2,7 @@
<div
class="inline-attachment-container asset"
:class="[{'menu-dropdown-open': isMenuDropdownOpen}, {'context-menu-open': isContextMenuOpen }]"
:data-e2e="`e2e-CO-${dataE2e}-attachment${attachment.id}-inline`"
ref="inlineAttachmentContainer"
:data-asset-id="attachment.id"
>
@ -104,6 +105,10 @@ export default {
parentId: {
type: Number,
required: true
},
dataE2e: {
type: String,
default: ''
}
},
data() {

View file

@ -1,6 +1,7 @@
<template>
<div class="list-attachment-container asset"
:data-asset-id="attachment.id"
:data-e2e="`e2e-CO-${dataE2e}-attachment${attachment.id}-list`"
>
<i class="text-sn-grey asset-icon sn-icon" :class="attachment.attributes.icon"></i>
<a :href="attachment.attributes.urls.blob"
@ -72,6 +73,10 @@ export default {
parentId: {
type: Number,
required: true
},
dataE2e: {
type: String,
default: ''
}
},
data() {

View file

@ -4,6 +4,7 @@
@mouseenter="handleMouseEnter"
@mouseleave="handleMouseLeave"
v-click-outside="handleClickOutsideThumbnail"
:data-e2e="`e2e-CO-${dataE2e}-attachment${attachment.id}-thumbnail`"
>
<a :class="{ hidden: showOptions }"
:href="attachment.attributes.urls.blob"
@ -101,7 +102,10 @@
>
<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')">
<a v-if="attachment.attributes.urls.move"
@click.prevent.stop="showMoveModal"
class="btn btn-light icon-btn thumbnail-action-btn"
:title="i18n.t('attachments.thumbnail.buttons.move')">
<i class="sn-icon sn-icon-move"></i>
</a>
<a class="btn btn-light icon-btn thumbnail-action-btn"
@ -210,6 +214,10 @@ export default {
parentId: {
type: Number,
required: true
},
dataE2e: {
type: String,
default: ''
}
},
data() {
@ -252,7 +260,7 @@ export default {
});
}
return options;
},
}
},
mounted() {
$(this.$nextTick(() => {

View file

@ -1,7 +1,8 @@
<template>
<div class="content__checklist-container pr-8" >
<div class="content__checklist-container pr-8" :data-e2e="`e2e-CO-${dataE2e}-checklist${element.id}`">
<div class="sci-divider my-6" v-if="!inRepository"></div>
<div class="checklist-header flex rounded mb-1 items-center relative w-full group/checklist-header" :class="{ 'editing-name': editingName, 'locked': !element.attributes.orderable.urls.update_url }">
<div class="checklist-header flex rounded mb-1 items-center relative w-full group/checklist-header"
:class="{ 'editing-name': editingName, 'locked': !element.attributes.orderable.urls.update_url }">
<div class="grow-1 text-ellipsis whitespace-nowrap grow my-1 font-bold">
<InlineEdit
:class="{ 'pointer-events-none': !element.attributes.orderable.urls.update_url }"
@ -13,6 +14,7 @@
:autofocus="editingName"
:smartAnnotation="true"
:attributeName="`${i18n.t('Checklist')} ${i18n.t('name')}`"
:dataE2e="`${dataE2e}-checklist${element.id}`"
@editingEnabled="editingName = true"
@editingDisabled="editingName = false"
@update="updateName"
@ -24,6 +26,7 @@
:btnClasses="'btn btn-light icon-btn btn-sm'"
:position="'right'"
:btnIcon="'sn-icon sn-icon-more-hori'"
:dataE2e="`e2e-DD-${dataE2e}-checklist${element.id}-options`"
@edit="editingName = true"
@duplicate="duplicateElement"
@move="showMoveModal"
@ -51,6 +54,7 @@
:reorderChecklistItemUrl="this.element.attributes.orderable.urls.reorder_url"
:inRepository="inRepository"
:draggable="checklistItems.length > 1"
:data-e2e="`${dataE2e}-checklistItem${element.id}`"
@editStart="editingItem = true"
@editEnd="editingItem = false"
@update="saveItem"
@ -63,6 +67,7 @@
<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"
:data-e2e="`e2e-BT-${dataE2e}-checklist${element.id}-addNew`"
@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>
@ -120,6 +125,10 @@ export default {
assignableMyModuleId: {
type: Number,
required: false
},
dataE2e: {
type: String,
default: ''
}
},
data() {
@ -154,25 +163,29 @@ export default {
if (this.element.attributes.orderable.urls.update_url) {
menu.push({
text: I18n.t('general.edit'),
emit: 'edit'
emit: 'edit',
data_e2e: `e2e-BT-${this.dataE2e}-checklist${this.element.id}-options-edit`
});
}
if (this.element.attributes.orderable.urls.duplicate_url) {
menu.push({
text: I18n.t('general.duplicate'),
emit: 'duplicate'
emit: 'duplicate',
data_e2e: `e2e-BT-${this.dataE2e}-checklist${this.element.id}-options-duplicate`
});
}
if (this.element.attributes.orderable.urls.move_targets_url) {
menu.push({
text: I18n.t('general.move'),
emit: 'move'
emit: 'move',
data_e2e: `e2e-BT-${this.dataE2e}-checklist${this.element.id}-options-move`
});
}
if (this.element.attributes.orderable.urls.delete_url) {
menu.push({
text: I18n.t('general.delete'),
emit: 'delete'
emit: 'delete',
data_e2e: `e2e-BT-${this.dataE2e}-checklist${this.element.id}-options-delete`
});
}
return menu;

View file

@ -1,5 +1,5 @@
<template>
<div class="content__checklist-item pl-10 ml-[-2.325rem] group/checklist-item-header">
<div class="content__checklist-item pl-10 ml-[-2.325rem] group/checklist-item-header" :data-e2e="`e2e-CO-${dataE2e}`">
<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"
@ -14,7 +14,9 @@
type="checkbox"
class="sci-checkbox"
:disabled="checklistItem.attributes.isNew"
:checked="checklistItem.attributes.checked" @change="toggleChecked($event)" />
:checked="checklistItem.attributes.checked"
:data-e2e="`e2e-CB-${dataE2e}-toggleChecked`"
@change="toggleChecked($event)" />
<span class="sci-checkbox-label" >
</span>
</div>
@ -37,6 +39,7 @@
:editOnload="checklistItem.attributes.isNew"
:smartAnnotation="true"
:allowNewLine="true"
:dataE2e="dataE2e"
@editingEnabled="enableTextEdit"
@editingDisabled="disableTextEdit"
@update="updateText"
@ -46,6 +49,7 @@
/>
<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"
:data-e2e="`e2e-BT-${dataE2e}-delete`"
@click="showDeleteModal" tabindex="0">
<i class="sn-icon sn-icon-delete"></i>
</span>
@ -88,6 +92,10 @@ export default {
reordering: {
type: Boolean,
required: true
},
dataE2e: {
type: String,
default: ''
}
},
data() {

View file

@ -1,5 +1,6 @@
<template>
<div class="content__table-container pr-8">
<div class="content__table-container pr-8"
:data-e2e="`e2e-CO-${dataE2e}-${element.attributes.orderable.metadata.plateTemplate ? 'wellPlate' : 'table'}${element.id}`">
<div class="sci-divider my-6" v-if="!inRepository"></div>
<div class="table-header h-9 flex rounded mb-3 items-center relative w-full group/table-header" :class="{ 'editing-name': editingName, 'locked': locked }">
<div v-if="!locked || element.attributes.orderable.name" :key="reloadHeader"
@ -12,6 +13,7 @@
:allowBlank="false"
:autofocus="editingName"
:attributeName="`${i18n.t('Table')} ${i18n.t('name')}`"
:dataE2e="`${dataE2e}-${element.attributes.orderable.metadata.plateTemplate ? 'wellPlate' : 'table'}${element.id}`"
@editingEnabled="enableNameEdit"
@editingDisabled="disableNameEdit"
@update="updateName"
@ -23,6 +25,7 @@
:btnClasses="'btn btn-light icon-btn btn-sm'"
:position="'right'"
:btnIcon="'sn-icon sn-icon-more-hori'"
:dataE2e="`e2e-DD-${dataE2e}-${element.attributes.orderable.metadata.plateTemplate ? 'wellPlate' : 'table'}${element.id}-options`"
@edit="enableNameEdit"
@duplicate="duplicateElement"
@move="showMoveModal"
@ -32,11 +35,14 @@
<div class="table-body group/table-body relative border-solid border-transparent"
:class="{'edit border-sn-light-grey': editingTable, 'view': !editingTable, 'locked': !element.attributes.orderable.urls.update_url}"
tabindex="0"
:data_e2e="`e2e-TB-${dataE2e}-${element.attributes.orderable.metadata.plateTemplate ? 'wellPlate' : 'table'}${element.id}`"
@keyup.enter="!editingTable && enableTableEdit()">
<div ref="hotTable" class="hot-table-container" @click="!editingTable && enableTableEdit()">
</div>
<div class="text-xs pt-3 pb-2 text-sn-grey h-1">
<span v-if="editingTable">{{ i18n.t('protocols.steps.table.edit_message') }}</span>
<span v-if="editingTable" :dataE2e="`e2e-TX-${dataE2e}-${element.attributes.orderable.metadata.plateTemplate ? 'wellPlate' : 'table'}${element.id}-editMessage`">
{{ i18n.t('protocols.steps.table.edit_message') }}
</span>
</div>
</div>
<deleteElementModal v-if="confirmingDelete" @confirm="deleteElement" @cancel="closeDeleteModal"/>
@ -82,6 +88,10 @@ export default {
assignableMyModuleId: {
type: Number,
required: false
},
dataE2e: {
type: String,
default: ''
}
},
data() {
@ -104,25 +114,29 @@ export default {
if (this.element.attributes.orderable.urls.update_url) {
menu.push({
text: I18n.t('general.edit'),
emit: 'edit'
emit: 'edit',
data_e2e: `e2e-BT-${this.dataE2e}-${this.element.attributes.orderable.metadata.plateTemplate ? 'wellPlate' : 'table'}${this.element.id}-options-edit`
});
}
if (this.element.attributes.orderable.urls.duplicate_url) {
menu.push({
text: I18n.t('general.duplicate'),
emit: 'duplicate'
emit: 'duplicate',
data_e2e: `e2e-BT-${this.dataE2e}-${this.element.attributes.orderable.metadata.plateTemplate ? 'wellPlate' : 'table'}${this.element.id}-options-duplicate`
});
}
if (this.element.attributes.orderable.urls.move_targets_url) {
menu.push({
text: I18n.t('general.move'),
emit: 'move'
emit: 'move',
data_e2e: `e2e-BT-${this.dataE2e}-${this.element.attributes.orderable.metadata.plateTemplate ? 'wellPlate' : 'table'}${this.element.id}-options-move`
});
}
if (this.element.attributes.orderable.urls.delete_url) {
menu.push({
text: I18n.t('general.delete'),
emit: 'delete'
emit: 'delete',
data_e2e: `e2e-BT-${this.dataE2e}-${this.element.attributes.orderable.metadata.plateTemplate ? 'wellPlate' : 'table'}${this.element.id}-options-delete`
});
}
return menu;

View file

@ -1,7 +1,9 @@
<template>
<div class="content__text-container pr-8">
<div class="content__text-container pr-8" :data-e2e="`e2e-CO-${dataE2e}-stepText${element.id}`">
<div class="sci-divider my-6" v-if="!inRepository"></div>
<div class="text-header h-9 flex rounded mb-1 items-center relative w-full group/text-header" :class="{ 'editing-name': editingName, 'locked': !element.attributes.orderable.urls.update_url }">
<div class="text-header h-9 flex rounded mb-1 items-center relative w-full group/text-header"
:class="{ 'editing-name': editingName,
'locked': !element.attributes.orderable.urls.update_url }">
<div v-if="element.attributes.orderable.urls.update_url || element.attributes.orderable.name"
class="grow-1 text-ellipsis whitespace-nowrap grow my-1 font-bold"
:class="{'pointer-events-none': !element.attributes.orderable.urls.update_url}"
@ -13,6 +15,7 @@
:allowBlank="true"
:autofocus="editingName"
:attributeName="`${i18n.t('Text')} ${i18n.t('name')}`"
:dataE2e="`${dataE2e}-stepText${element.id}`"
@editingEnabled="enableNameEdit"
@editingDisabled="disableNameEdit"
@update="updateName"
@ -24,13 +27,18 @@
:btnClasses="'btn btn-light icon-btn btn-sm'"
:position="'right'"
:btnIcon="'sn-icon sn-icon-more-hori'"
:dataE2e="`e2e-DD-${dataE2e}-stepText${element.id}-options`"
@edit="enableNameEdit"
@duplicate="duplicateElement"
@move="showMoveModal"
@delete="showDeleteModal"
></MenuDropdown>
</div>
<div class="flex rounded min-h-[2.25rem] mb-4 relative group/text_container content__text-body" :class="{ 'edit': inEditMode, 'component__element--locked': !element.attributes.orderable.urls.update_url }" @keyup.enter="enableEditMode($event)" tabindex="0">
<div class="flex rounded min-h-[2.25rem] mb-4 relative group/text_container content__text-body"
:class="{ 'edit': inEditMode, 'component__element--locked': !element.attributes.orderable.urls.update_url }"
:data-e2e="`e2e-IF-${dataE2e}-stepText${element.id}`"
@keyup.enter="enableEditMode($event)"
tabindex="0">
<Tinymce
v-if="element.attributes.orderable.urls.update_url"
:value="element.attributes.orderable.text"
@ -48,8 +56,8 @@
@editingDisabled="disableEditMode"
@editingEnabled="enableEditMode"
/>
<div class="view-text-element" v-else-if="element.attributes.orderable.text_view" v-html="wrappedTables"></div>
<div v-else class="text-sn-grey">
<div class="view-text-element" v-else-if="element.attributes.orderable.text_view" v-html="wrappedTables" :data-e2e="`e2e-TX-${dataE2e}-stepText${element.id}`"></div>
<div v-else class="text-sn-grey" :data-e2e="`e2e-TX-${dataE2e}-stepText${element.id}-empty`">
{{ i18n.t("protocols.steps.text.empty_text") }}
</div>
</div>
@ -96,6 +104,10 @@ export default {
assignableMyModuleId: {
type: Number,
required: false
},
dataE2e: {
type: String,
default: ''
}
},
data() {
@ -126,25 +138,29 @@ export default {
if (this.element.attributes.orderable.urls.update_url) {
menu.push({
text: I18n.t('general.edit'),
emit: 'edit'
emit: 'edit',
data_e2e: `e2e-BT-${this.dataE2e}-stepText${this.element.id}-options-edit`
});
}
if (this.element.attributes.orderable.urls.duplicate_url) {
menu.push({
text: I18n.t('general.duplicate'),
emit: 'duplicate'
emit: 'duplicate',
data_e2e: `e2e-BT-${this.dataE2e}-stepText${this.element.id}-options-duplicate`
});
}
if (this.element.attributes.orderable.urls.move_targets_url) {
menu.push({
text: I18n.t('general.move'),
emit: 'move'
emit: 'move',
data_e2e: `e2e-BT-${this.dataE2e}-stepText${this.element.id}-options-move`
});
}
if (this.element.attributes.orderable.urls.delete_url) {
menu.push({
text: I18n.t('general.delete'),
emit: 'delete'
emit: 'delete',
data_e2e: `e2e-BT-${this.dataE2e}-stepText${this.element.id}-options-delete`
});
}
return menu;

View file

@ -1,7 +1,7 @@
<template>
<div v-if="pages.length > 1" class="flex gap-3 select-none">
<div class="w-9 h-9">
<div class="w-9 h-9 cursor-pointer flex items-center justify-center"
<div class="w-9 h-9 cursor-pointer flex items-center justify-center" data-e2e="e2e-BT-tableInfo-left"
@click="$emit('setPage', currentPage - 1)"
v-if="currentPage > 1">
<i class="sn-icon sn-icon-left cursor-pointer"></i>
@ -11,11 +11,12 @@
v-for="page in pages"
:class="{ 'border-solid rounded border-sn-science-blue': page === currentPage }"
:key="page"
:data-e2e="`e2e-BT-tableInfo-page-${page}`"
@click="$emit('setPage', page)">
<span >{{ page }}</span>
</div>
<div class="w-9 h-9">
<div class="w-9 h-9 cursor-pointer flex items-center justify-center"
<div class="w-9 h-9 cursor-pointer flex items-center justify-center" data-e2e="e2e-BT-tableInfo-right"
@click="$emit('setPage', currentPage + 1)"
v-if="totalPage > currentPage">
<i class="sn-icon sn-icon-right cursor-pointer"></i>
@ -30,12 +31,12 @@ export default {
props: {
totalPage: {
type: Number,
required: true,
required: true
},
currentPage: {
type: Number,
required: true,
},
required: true
}
},
computed: {
pages() {
@ -50,7 +51,7 @@ export default {
}
}
return pages;
},
},
}
}
};
</script>

View file

@ -75,17 +75,18 @@
:params="actionsParams"
@toolbar:action="emitAction" />
</div>
<div v-if="scrollMode == 'pages'" class="flex items-center py-4" :class="{'opacity-0': initializing }">
<div class="flex items-center gap-4">
<div v-if="scrollMode == 'pages'" class="flex items-center py-4" :class="{'opacity-0': initializing }" data-e2e="e2e-CO-tableInfo">
<div class="flex items-center gap-4" data-e2e="e2e-TX-tableInfo-show">
{{ i18n.t('datatable.show') }}
<div class="w-36">
<SelectDropdown
:value="perPage"
:options="perPageOptions"
:data-e2e="'e2e-DD-tableInfo-rows'"
@change="setPerPage"
></SelectDropdown>
</div>
<div v-show="!dataLoading">
<div v-show="!dataLoading" data-e2e="e2e-TX-tableInfo-entries">
<span v-if="selectedRows.length">
{{ i18n.t('datatable.entries.selected', { count: totalEntries, selected: selectedRows.length }) }}
</span>

View file

@ -18,6 +18,7 @@
:btnIcon="action.icon"
:caret="true"
:position="'right'"
:data-e2e="`e2e-BT-topToolbar-${action.name}`"
@dtEvent="handleEvent"
></MenuDropdown>
</template>

View file

@ -12,6 +12,7 @@
'border-b-sn-science-blue': !error,
}"
v-model="newValue"
:data-e2e="`e2e-IF-${dataE2e}`"
@keydown="handleKeypress"
@blur="handleBlur"
@keyup.escape="cancelEdit && this.atWhoOpened"
@ -27,6 +28,7 @@
}"
:placeholder="placeholder"
v-model="newValue"
:data-e2e="`e2e-IF-${dataE2e}`"
@keydown="handleKeypress"
@blur="handleBlur"
@keyup.escape="cancelEdit && this.atWhoOpened"
@ -38,6 +40,7 @@
ref="view"
class="grid sci-cursor-edit leading-5 border-0 outline-none border-solid border-y border-transparent"
:class="{ 'text-sn-grey font-normal': isBlank, 'whitespace-pre-line py-1': !singleLine }"
:data-e2e="`e2e-TX-${dataE2e}`"
@click="enableEdit($event)"
>
<span :class="{'truncate': singleLine }" :title="sa_value || placeholder" v-if="smartAnnotation" v-html="sa_value || placeholder" ></span>
@ -48,6 +51,7 @@
class="mt-2 whitespace-nowrap truncate text-xs font-normal absolute bottom-[-1rem] w-full"
:title="editing && error ? error : timestamp"
:class="{'text-sn-delete-red': editing && error}"
:data-e2e="`e2e-TX-${dataE2e}-timestampError`"
>
{{ editing && error ? error : timestamp }}
</div>
@ -76,7 +80,8 @@ export default {
editOnload: { type: Boolean, default: false },
defaultValue: { type: String, default: '' },
singleLine: { type: Boolean, default: true },
preventLeavingUntilFilled: { type: Boolean, default: false }
preventLeavingUntilFilled: { type: Boolean, default: false },
dataE2e: { type: String, default: '' }
},
data() {
return {

View file

@ -1,5 +1,5 @@
<template>
<div class="dropdown-selector">
<div class="dropdown-selector" :data-e2e="`e2e-IF-${dataE2e}`">
<select :id="this.selectorId"
:data-select-by-group="groupSelector"
:data-combine-tags="dataCombineTags"
@ -111,6 +111,10 @@ export default {
type: Boolean,
default: false
},
dataE2e: {
type: String,
default: ''
},
onChange: Function
},
@ -127,6 +131,7 @@ export default {
tagLabel: this.tagLabel,
labelHTML: this.labelHTML,
onOpen: this.onOpen,
dataE2e: this.dataE2e,
onChange: () => {
if (this.onChange) this.onChange();
this.selectChanged(dropdownSelector.getValues(`#${this.selectorId}`));

View file

@ -0,0 +1,11 @@
<template>
<div class="flex absolute top-0 items-center justify-center w-full flex-grow h-full z-[3000]">
<div class="absolute top-0 left-0 w-full h-full bg-black opacity-10"></div>
<img src="/images/medium/loading.svg" alt="Loading" class="" />
</div>
</template>
<script>
export default {
name: 'Loading'
};
</script>

View file

@ -1,6 +1,6 @@
<template>
<div class="relative" v-if="listItems.length > 0 || alwaysShow" v-click-outside="closeMenu" >
<button ref="field" :class="btnClasses" :title="title" @click="isOpen = !isOpen" :data-e2e="e2eSortButton">
<button ref="field" :class="btnClasses" :title="title" @click="isOpen = !isOpen" :data-e2e="dataE2e">
<i v-if="btnIcon" :class="btnIcon"></i>
{{ btnText }}
<i v-if="caret && isOpen" class="sn-icon sn-icon-up"></i>
@ -35,6 +35,7 @@
:class="{ 'bg-sn-super-light-blue': item.active }"
class="flex group items-center rounded relative text-sn-blue whitespace-nowrap px-3 py-2.5 hover:no-underline cursor-pointer
group-hover:bg-sn-super-light-blue hover:!bg-sn-super-light-grey"
:data-e2e="item.data_e2e"
>
{{ item.text }}
<i class="sn-icon sn-icon-right ml-auto"></i>
@ -50,6 +51,7 @@
:href="sub_item.url"
:traget="sub_item.url_target || '_self'"
:class="{ 'bg-sn-super-light-blue': item.active }"
:data-e2e="`${sub_item.data_e2e}`"
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 leading-5"
@click="handleClick($event, sub_item)"
>
@ -80,7 +82,7 @@ export default {
caret: { type: Boolean, default: false },
alwaysShow: { type: Boolean, default: false },
title: { type: String, default: '' },
e2eSortButton: { type: String, default: '' }
dataE2e: { type: String, default: '' }
},
data() {
return {

View file

@ -273,8 +273,13 @@ export default {
});
}
},
urlParams() {
this.fetchOptions();
urlParams: {
handler(oldVal, newVal) {
if (!this.compareObjects(oldVal, newVal)) {
this.fetchOptions();
}
},
deep: true
}
},
methods: {
@ -384,6 +389,11 @@ export default {
if (this.$refs.options) {
this.$refs.options[this.focusedOption]?.scrollIntoView({ block: 'nearest' });
}
},
compareObjects(o1, o2) {
const normalizedObj1 = Object.fromEntries(Object.entries(o1).sort(([k1], [k2]) => k1.localeCompare(k2)));
const normalizedObj2 = Object.fromEntries(Object.entries(o2).sort(([k1], [k2]) => k1.localeCompare(k2)));
return JSON.stringify(normalizedObj1) === JSON.stringify(normalizedObj2);
}
}
};

View file

@ -46,7 +46,7 @@ class RepositoriesExportJob < ApplicationJob
FileUtils.mkdir_p(attachments_path)
# File creation
file_name = FileUtils.touch("#{path}/#{repository_name}.#{@file_type}").first
repository_items_file_name = FileUtils.touch("#{path}/#{repository_name}.#{@file_type}").first
# Define headers and columns IDs
col_ids = [-3, -4, -5, -6, -7, -8, -9, -10]
@ -69,10 +69,10 @@ class RepositoriesExportJob < ApplicationJob
# Generate CSV / XLSX
service = RepositoryExportService
.new(@file_type, repository.repository_rows, col_ids, @user, repository, in_module: handle_name_func)
.new(@file_type, repository.repository_rows, col_ids, @user, repository, handle_name_func)
exported_data = service.export!
File.binwrite(file_name, exported_data)
File.binwrite(repository_items_file_name, exported_data)
# Save all attachments (it doesn't work directly in callback function
assets.each do |asset, asset_path|

View file

@ -35,15 +35,9 @@ class RepositoryZipExportJob < ZipExportJob
params[:header_ids].map(&:to_i),
@user,
repository,
in_module: params[:my_module_id].present?,
empty_export: @empty_export)
in_module: params[:my_module_id].present?)
exported_data = service.export!
if @empty_export
File.binwrite("#{dir}/Export_Inventory_Empty_#{Time.now.utc.strftime('%F %H-%M-%S_UTC')}.#{@file_type}", exported_data)
else
File.binwrite("#{dir}/export.#{@file_type}", exported_data)
end
File.binwrite("#{dir}/export.#{@file_type}", exported_data)
end
def failed_notification_title

View file

@ -3,10 +3,9 @@
class ZipExportJob < ApplicationJob
include FailedDeliveryNotifiableJob
def perform(user_id:, params: {}, file_type: :csv, empty_export: false)
def perform(user_id:, params: {}, file_type: :csv)
@user = User.find(user_id)
@file_type = file_type.to_sym
@empty_export = empty_export
I18n.backend.date_format = @user.settings[:date_format] || Constants::DEFAULT_DATE_FORMAT
zip_input_dir = FileUtils.mkdir_p(Rails.root.join("tmp/temp_zip_#{Time.now.to_i}").to_s).first
zip_dir = FileUtils.mkdir_p(Rails.root.join('tmp/zip-ready').to_s).first

View file

@ -245,7 +245,7 @@ class Asset < ApplicationRecord
def can_perform_action(action)
if ENV['WOPI_ENABLED'] == 'true'
file_ext = file_name.split('.').last
file_ext = file_name.split('.').last&.downcase
if file_ext == 'wopitest' &&
(!ENV['WOPI_TEST_ENABLED'] || ENV['WOPI_TEST_ENABLED'] == 'false')
@ -297,7 +297,7 @@ class Asset < ApplicationRecord
end
def favicon_url(action)
file_ext = file_name.split('.').last
file_ext = file_name.split('.').last&.downcase
action = get_action(file_ext, action)
action[:icon] if action[:icon]
end

View file

@ -56,7 +56,6 @@ class MyModule < ApplicationRecord
belongs_to :changing_from_my_module_status, optional: true, class_name: 'MyModuleStatus'
delegate :my_module_status_flow, to: :my_module_status, allow_nil: true
has_many :results, inverse_of: :my_module, dependent: :destroy
has_many :results_include_discarded, -> { with_discarded }, class_name: 'Result', inverse_of: :my_module
has_many :my_module_tags, inverse_of: :my_module, dependent: :destroy
has_many :tags, through: :my_module_tags, dependent: :destroy
has_many :task_comments, foreign_key: :associated_id, dependent: :destroy

View file

@ -162,6 +162,7 @@ class Repository < RepositoryBase
def importable_repository_fields
fields = {}
# First and foremost add record name
fields['0'] = I18n.t('repositories.id_column')
fields['-1'] = I18n.t('repositories.default_column')
# Add all other custom columns
repository_columns.order(:created_at).each do |rc|
@ -203,11 +204,6 @@ class Repository < RepositoryBase
new_repo
end
def import_records(sheet, mappings, user, can_edit_existing_items, should_overwrite_with_empty_cells, preview)
importer = RepositoryImportParser::Importer.new(sheet, mappings, user, self)
importer.run(can_edit_existing_items, should_overwrite_with_empty_cells, preview)
end
def assigned_rows(my_module)
repository_rows.joins(:my_module_repository_rows).where(my_module_repository_rows: { my_module_id: my_module.id })
end

View file

@ -3,7 +3,7 @@
class RepositoryCell < ApplicationRecord
include ReminderRepositoryCellJoinable
attr_accessor :importing
attr_accessor :importing, :to_destroy
belongs_to :repository_row, touch: true
belongs_to :repository_column

View file

@ -105,6 +105,8 @@ class RepositoryRow < ApplicationRecord
length: { maximum: Constants::NAME_MAX_LENGTH }
validates :created_by, presence: true
attr_accessor :import_status, :import_message
scope :active, -> { where(archived: false) }
scope :archived, -> { where(archived: true) }

View file

@ -5,9 +5,6 @@ class Result < ApplicationRecord
include SearchableModel
include SearchableByNameModel
include ViewableModel
include Discard::Model
default_scope -> { kept }
auto_strip_attributes :name, nullify: false
validates :name, length: { maximum: Constants::NAME_MAX_LENGTH }

View file

@ -19,6 +19,7 @@ module Lists
status
designated_users
tags
tags_html
comments
due_date_formatted
permissions
@ -142,6 +143,17 @@ module Lists
end
end
def tags_html
# legacy canvas support
return '' unless @instance_options[:controller]
@instance_options[:controller].render_to_string(
partial: 'canvas/tags',
locals: { my_module: object },
formats: :html
)
end
def comments
@user = scope[:user] || @instance_options[:user]
{

View file

@ -0,0 +1,19 @@
# frozen_string_literal: true
class RepositoryCellImportSerializer < ActiveModel::Serializer
include Rails.application.routes.url_helpers
attributes :id, :value, :changes, :repository_column_id, :formatted_value
def changes
object.value.changes
end
def value
object.value if !object.to_destroy
end
def formatted_value
object.value.formatted if !object.to_destroy
end
end

View file

@ -3,11 +3,7 @@
class RepositoryCellSerializer < ActiveModel::Serializer
include Rails.application.routes.url_helpers
attributes :id, :value, :changes, :repository_column_id, :formatted_value
def changes
object.value.changes
end
attributes :id, :value, :repository_column_id, :formatted_value
def value
object.value

View file

@ -0,0 +1,13 @@
# frozen_string_literal: true
class RepositoryRowImportSerializer < ActiveModel::Serializer
include Rails.application.routes.url_helpers
attributes :id, :name, :code, :import_status, :import_message
has_many :repository_cells, serializer: RepositoryCellImportSerializer
attribute :code do
object.new_record? ? nil : object.code
end
end

View file

@ -5,5 +5,6 @@ class RepositoryRowSerializer < ActiveModel::Serializer
attributes :id, :name, :code
has_many :repository_cells, serializer: RepositoryCellSerializer
has_many :repository_cells, serializer: RepositoryCellImportSerializer
end

View file

@ -55,20 +55,9 @@ class ActivitiesService
child_model = parent_model.reflect_on_association(child).class_name.to_sym
next if subjects[child_model]
if subject_name == 'Result'
parent_model = parent_model.with_discarded
end
if child == :results
subjects[child_model] = parent_model.where(id: subjects[subject_name])
.joins(:results_include_discarded)
.pluck('results.id')
else
subjects[child_model] = parent_model.where(id: subjects[subject_name])
.joins(child)
.pluck("#{child.to_s.pluralize}.id")
end
subjects[child_model] = parent_model.where(id: subjects[subject_name])
.joins(child)
.pluck("#{child.to_s.pluralize}.id")
end
end

View file

@ -1,3 +1,5 @@
# frozen_string_literal: true
module ImportRepository
class ImportRecords
def initialize(options)
@ -6,60 +8,25 @@ module ImportRepository
@mappings = options.fetch(:mappings)
@session = options.fetch(:session)
@user = options.fetch(:user)
@can_edit_existing_items = options.fetch(:can_edit_existing_items)
@should_overwrite_with_empty_cells = options.fetch(:should_overwrite_with_empty_cells)
@preview = options.fetch(:preview)
end
def import!(can_edit_existing_items, should_overwrite_with_empty_cells, preview)
status = run_import_actions(can_edit_existing_items, should_overwrite_with_empty_cells, preview)
#@temp_file.destroy
def import!
status = @temp_file.file.open do |temp_file|
importer = RepositoryImportParser::Importer.new(SpreadsheetParser.open_spreadsheet(temp_file),
@mappings,
@user,
@repository,
@can_edit_existing_items,
@should_overwrite_with_empty_cells,
@preview)
importer.run
end
@temp_file.destroy unless @preview
status
end
private
def run_import_actions(can_edit_existing_items, should_overwrite_with_empty_cells, preview)
@temp_file.file.open do |temp_file|
@repository.import_records(
SpreadsheetParser.open_spreadsheet(temp_file),
@mappings,
@user,
can_edit_existing_items,
should_overwrite_with_empty_cells,
preview
)
end
end
def run_checks
unless @mappings
return {
status: :error,
errors:
I18n.t('repositories.import_records.error_message.no_data_to_parse')
}
end
unless @mappings.value?('-1')
return {
status: :error,
errors:
I18n.t('repositories.import_records.error_message.no_column_name')
}
end
unless @temp_file
return {
status: :error,
errors:
I18n.t(
'repositories.import_records.error_message.temp_file_not_found'
)
}
end
unless @temp_file.session_id == session.id
return {
status: :error,
errors:
I18n.t('repositories.import_records.error_message.session_expired')
}
end
end
end
end

View file

@ -3,7 +3,7 @@
require 'csv'
module RepositoryCsvExport
def self.to_csv(rows, column_ids, user, repository, handle_file_name_func, in_module, empty_export)
def self.to_csv(rows, column_ids, user, repository, handle_file_name_func, in_module)
# Parse column names
csv_header = []
add_consumption = in_module && !repository.is_a?(RepositorySnapshot) && repository.has_stock_management?
@ -38,47 +38,45 @@ module RepositoryCsvExport
CSV.generate do |csv|
csv << csv_header
unless empty_export
rows.each do |row|
csv_row = []
column_ids.each do |c_id|
case c_id
when -1, -2
next
when -3
csv_row << (repository.is_a?(RepositorySnapshot) ? row.parent_id : row.code)
when -4
csv_row << row.name
when -5
csv_row << row.created_by.full_name
when -6
csv_row << I18n.l(row.created_at, format: :full)
when -7
csv_row << row.updated_at ? I18n.l(row.updated_at, format: :full) : ''
when -8
csv_row << row.last_modified_by.full_name
when -9
csv_row << (row.archived? && row.archived_by.present? ? row.archived_by.full_name : '')
when -10
csv_row << (row.archived? && row.archived_on.present? ? I18n.l(row.archived_on, format: :full) : '')
when -11
csv_row << row.parent_repository_rows.map(&:code).join(' | ')
csv_row << row.child_repository_rows.map(&:code).join(' | ')
else
cell = row.repository_cells.find_by(repository_column_id: c_id)
rows.each do |row|
csv_row = []
column_ids.each do |c_id|
case c_id
when -1, -2
next
when -3
csv_row << (repository.is_a?(RepositorySnapshot) ? row.parent_id : row.code)
when -4
csv_row << row.name
when -5
csv_row << row.created_by.full_name
when -6
csv_row << I18n.l(row.created_at, format: :full)
when -7
csv_row << row.updated_at ? I18n.l(row.updated_at, format: :full) : ''
when -8
csv_row << row.last_modified_by.full_name
when -9
csv_row << (row.archived? && row.archived_by.present? ? row.archived_by.full_name : '')
when -10
csv_row << (row.archived? && row.archived_on.present? ? I18n.l(row.archived_on, format: :full) : '')
when -11
csv_row << row.parent_repository_rows.map(&:code).join(' | ')
csv_row << row.child_repository_rows.map(&:code).join(' | ')
else
cell = row.repository_cells.find_by(repository_column_id: c_id)
csv_row << if cell
if cell.value_type == 'RepositoryAssetValue' && handle_file_name_func
handle_file_name_func.call(cell.value.asset)
else
cell.value.export_formatted
end
if cell.value_type == 'RepositoryAssetValue' && handle_file_name_func
handle_file_name_func.call(cell.value.asset)
else
cell.value.export_formatted
end
end
end
end
csv_row << row.row_consumption(row.stock_consumption) if add_consumption
csv << csv_row
end
csv_row << row.row_consumption(row.stock_consumption) if add_consumption
csv << csv_row
end
end.encode('UTF-8', invalid: :replace, undef: :replace)
end

View file

@ -1,7 +1,7 @@
# frozen_string_literal: true
class RepositoryExportService
def initialize(file_type, rows, columns, user, repository, handle_name_func = nil, in_module: false, empty_export: false)
def initialize(file_type, rows, columns, user, repository, handle_name_func = nil, in_module: false)
@file_type = file_type
@user = user
@rows = rows
@ -9,13 +9,12 @@ class RepositoryExportService
@repository = repository
@handle_name_func = handle_name_func
@in_module = in_module
@empty_export = empty_export
end
def export!
case @file_type
when :csv
file_data = RepositoryCsvExport.to_csv(@rows, @columns, @user, @repository, @handle_name_func, @in_module, @empty_export)
file_data = RepositoryCsvExport.to_csv(@rows, @columns, @user, @repository, @handle_name_func, @in_module)
when :xlsx
file_data = RepositoryXlsxExport.to_xlsx(@rows, @columns, @user, @repository, @handle_name_func, @in_module)
end

View file

@ -36,9 +36,9 @@ module RepositoryXlsxExport
when -6
row_data << I18n.l(row.created_at, format: :full)
when -7
csv_row << row.updated_at ? I18n.l(row.updated_at, format: :full) : ''
row_data << row.updated_at ? I18n.l(row.updated_at, format: :full) : ''
when -8
csv_row << row.last_modified_by.full_name
row_data << row.last_modified_by.full_name
when -9
row_data << (row.archived? && row.archived_by.present? ? row.archived_by.full_name : '')
when -10

View file

@ -11,9 +11,10 @@ module RepositoryImportParser
class Importer
IMPORT_BATCH_SIZE = 500
def initialize(sheet, mappings, user, repository)
def initialize(sheet, mappings, user, repository, can_edit_existing_items, should_overwrite_with_empty_cells, preview)
@columns = []
@name_index = -1
@id_index = nil
@total_new_rows = 0
@new_rows_added = 0
@header_skipped = false
@ -23,31 +24,31 @@ module RepositoryImportParser
@mappings = mappings
@user = user
@repository_columns = @repository.repository_columns
@can_edit_existing_items = true # can_edit_existing_items
@should_overwrite_with_empty_cells = true # should_overwrite_with_empty_cells
@preview = preview
end
def run(can_edit_existing_items, should_overwrite_with_empty_cells, preview)
def run
fetch_columns
return check_for_duplicate_columns if check_for_duplicate_columns
import_rows!(can_edit_existing_items, should_overwrite_with_empty_cells, preview)
import_rows!
end
private
def fetch_columns
@mappings.each_with_index do |(_, value), index|
value = JSON.parse(value) rescue value
value = value.to_s unless value.is_a?(Hash)
if value == '-1'
# Fill blank space, so our indices stay the same
case value
when '0'
@columns << nil
@id_index = index
when '-1'
@columns << nil
@name_index = index
# creating a custom option column
elsif value.is_a?(Hash)
new_repository_column = @repository.repository_columns.create!(created_by: @user, name: value['name']+rand(10000).to_s, data_type: "Repository#{value['type']}Value")
@columns << new_repository_column
else
@columns << @repository_columns.where(data_type: Extends::REPOSITORY_IMPORTABLE_TYPES)
.preload(Extends::REPOSITORY_IMPORT_COLUMN_PRELOADS)
@ -63,191 +64,190 @@ module RepositoryImportParser
end
end
def import_rows!(can_edit_existing_items, should_overwrite_with_empty_cells, preview)
errors = false
duplicate_ids = SpreadsheetParser.duplicate_ids(@sheet)
imported_rows = []
@repository.transaction do
batch_counter = 0
full_row_import_batch = []
@rows.each do |row|
# Skip empty rows
next if row.blank?
# Skip duplicates
next if duplicate_ids.include?(row.first)
unless @header_skipped
@header_skipped = true
next
end
@total_new_rows += 1
new_full_row = {}
incoming_row = SpreadsheetParser.parse_row(
row,
@sheet,
date_format: @user.settings['date_format']
)
incoming_row.each_with_index do |value, index|
if index == @name_index
# check if row (inventory) already exists
existing_row = RepositoryRow.includes(repository_cells: :value).find_by(id: incoming_row[0].to_s.gsub(RepositoryRow::ID_PREFIX, ''))
# if it doesn't exist create it
unless existing_row
new_row =
RepositoryRow.new(name: try_decimal_to_string(value),
repository: @repository,
created_by: @user,
last_modified_by: @user)
unless new_row.valid?
errors = true
break
end
new_full_row[:repository_row] = new_row
next
end
# if it's a preview always add the existing row
if preview
new_full_row[:repository_row] = existing_row
# otherwise add according to criteria
else
# if it does exist but shouldn't be edited, error out and break
if existing_row && (can_edit_existing_items == false)
errors = true
break
end
# if it does exist and should be edited, update the existing row
if existing_row && (can_edit_existing_items == true)
# update the existing row with incoming row data
new_full_row[:repository_row] = existing_row
end
end
end
next unless @columns[index]
new_full_row[index] = value
end
if new_full_row[:repository_row].present?
full_row_import_batch << new_full_row
batch_counter += 1
end
next if batch_counter < IMPORT_BATCH_SIZE
# import_batch_to_database(full_row_import_batch, can_edit_existing_items, should_overwrite_with_empty_cells, preview: preview)
imported_rows += import_batch_to_database(full_row_import_batch, can_edit_existing_items, should_overwrite_with_empty_cells, preview)
full_row_import_batch = []
batch_counter = 0
end
# Import of the remaining rows
imported_rows += import_batch_to_database(full_row_import_batch, can_edit_existing_items, should_overwrite_with_empty_cells, preview) if full_row_import_batch.any?
full_row_import_batch
def handle_invalid_cell_value(value, cell_value)
if value.present? && cell_value.nil?
@errors << 'Incorrect data format'
true
else
false
end
if errors
return { status: :error,
nr_of_added: @new_rows_added,
total_nr: @total_new_rows }
end
changes = ActiveModelSerializers::SerializableResource.new(
imported_rows,
each_serializer: RepositoryRowSerializer,
include: [:repository_cells]
).as_json
{ status: :ok, nr_of_added: @new_rows_added, total_nr: @total_new_rows, changes: changes, import_date: I18n.l(Date.today, format: :full_date) }
end
def import_batch_to_database(full_row_import_batch, can_edit_existing_items, should_overwrite_with_empty_cells, preview)
skipped_rows = []
def import_rows!
checked_rows = []
duplicate_ids = SpreadsheetParser.duplicate_ids(@sheet)
full_row_import_batch.map do |full_row|
# skip archived rows and rows that belong to other repositories
if full_row[:repository_row].archived || full_row[:repository_row].repository_id != @repository.id
skipped_rows << full_row[:repository_row]
@rows.each do |row|
next if row.blank?
unless @header_skipped
@header_skipped = true
next
end
full_row[:repository_row].save!(validate: false)
@new_rows_added += 1
incoming_row = SpreadsheetParser.parse_row(row, @sheet, date_format: @user.settings['date_format'])
next if incoming_row.compact.blank?
full_row.reject { |k| k == :repository_row }.each do |index, value|
column = @columns[index]
value = try_decimal_to_string(value) unless column.repository_number_value?
next if value.nil?
@total_new_rows += 1
cell_value_attributes = {
created_by: @user,
last_modified_by: @user,
repository_cell_attributes: {
repository_row: full_row[:repository_row],
repository_column: column,
importing: true
}
}
if @id_index
id = incoming_row[@id_index].to_s.gsub(RepositoryRow::ID_PREFIX, '')
cell_value = column.data_type.constantize.import_from_text(
value,
cell_value_attributes,
@user.as_json(root: true, only: :settings).deep_symbolize_keys
)
if id.present?
existing_row = @repository.repository_rows.includes(repository_cells: :value).find_by(id: id)
existing_cell = full_row[:repository_row].repository_cells.find { |c| c.repository_column_id == column.id }
next if cell_value.nil? && existing_cell.nil?
if existing_cell
# existing_cell present && !can_edit_existing_items
next if can_edit_existing_items == false
# existing_cell present && can_edit_existing_items
if can_edit_existing_items == true
# if incoming cell is not empty
case cell_value
when RepositoryStockValue
existing_cell.value.update_data!(cell_value, @user, preview: preview) unless cell_value.nil?
when RepositoryListValue
repository_list_item_id = cell_value[:repository_list_item_id]
existing_cell.value.update_data!(repository_list_item_id, @user, preview: preview) unless cell_value.nil?
when RepositoryStatusValue
repository_status_item_id = cell_value[:repository_status_item_id]
existing_cell.value.update_data!(repository_status_item_id, @user, preview: preview) unless cell_value.nil?
else
sanitized_cell_value_data = sanitize_cell_value_data(cell_value.data)
existing_cell.value.update_data!(sanitized_cell_value_data, @user, preview: preview) unless cell_value.nil?
end
# if incoming cell is empty && should_overwrite_with_empty_cells
existing_cell.value.destroy! if cell_value.nil? && should_overwrite_with_empty_cells == true
# if incoming cell is empty && !should_overwrite_with_empty_cells
next if cell_value.nil? && should_overwrite_with_empty_cells == false
end
else
# no existing_cell. Create a new one.
cell_value.repository_cell.value = cell_value
cell_value.save!(validate: false)
existing_row ||= @repository.repository_rows.new(
id: SecureRandom.uuid,
created_by: @user,
last_modified_by: @user,
import_status: 'invalid',
import_message: I18n.t('repositories.import_records.steps.step3.status_message.not_exist', id: id.to_i)
)
end
end
full_row[:repository_row]
if existing_row.present?
if !@can_edit_existing_items
existing_row.import_status = 'unchanged'
elsif existing_row.archived
existing_row.import_status = 'archived'
elsif duplicate_ids.include?(existing_row.id)
existing_row.import_status = 'duplicated'
end
if existing_row.import_status.present?
checked_rows << existing_row if @preview
next
end
end
checked_rows << import_row(existing_row, incoming_row)
end
changes = ActiveModelSerializers::SerializableResource.new(
checked_rows.compact,
each_serializer: RepositoryRowImportSerializer,
include: [:repository_cells]
).as_json
p changes
{ status: :ok, nr_of_added: @new_rows_added, total_nr: @total_new_rows, changes: changes,
import_date: I18n.l(Date.today, format: :full_date) }
end
def import_row(repository_row, import_row)
@repository.transaction do
@errors = []
@updated = false
repository_row_name = try_decimal_to_string(import_row[@name_index])
if repository_row.present?
repository_row.name = repository_row_name
else
repository_row = RepositoryRow.new(name: repository_row_name,
repository: @repository,
created_by: @user,
last_modified_by: @user,
import_status: 'created')
end
if @preview
repository_row.validate
repository_row.id = SecureRandom.uuid unless repository_row.id.present? # ID required for preview with serializer
else
repository_row.save!
end
@errors << repository_row.errors.full_messages.join(',') if repository_row.errors.present?
@updated = repository_row.changed?
@columns.each_with_index do |column, index|
next if column.blank?
value = import_row[index]
value = try_decimal_to_string(value) unless column.repository_number_value?
cell_value = if value.present?
column.data_type.constantize.import_from_text(
value,
{
created_by: @user,
last_modified_by: @user,
repository_cell_attributes: {
repository_row: repository_row,
repository_column: column,
importing: true
}
},
@user.as_json(root: true, only: :settings).deep_symbolize_keys
)
end
next if handle_invalid_cell_value(value, cell_value)
existing_cell = repository_row.repository_cells.find { |c| c.repository_column_id == column.id }
existing_cell = if cell_value.nil?
handle_nil_cell_value(existing_cell)
else
handle_existing_cell_value(existing_cell, cell_value, repository_row)
end
@updated ||= existing_cell&.value&.changed?
@errors << existing_cell.value.errors.full_messages.join(',') if existing_cell&.value&.errors.present?
end
repository_row.import_status = if @errors.present?
'invalid'
elsif repository_row.import_status == 'created'
@new_rows_added += 1
'created'
elsif @updated
@new_rows_added += 1
'updated'
else
'unchanged'
end
repository_row.import_message = @errors.join(',').downcase if @errors.present?
repository_row
rescue ActiveRecord::RecordInvalid
raise ActiveRecord::Rollback
end
end
def handle_nil_cell_value(repository_cell)
return unless repository_cell.present? && @should_overwrite_with_empty_cells
if @preview
repository_cell.to_destroy = true
@updated = true
else
repository_cell.value.destroy!
end
repository_cell
end
def handle_existing_cell_value(repository_cell, cell_value, repository_row)
if repository_cell.present?
case cell_value
when RepositoryStockValue
repository_cell.value.update_data!(cell_value, @user, preview: @preview)
when RepositoryListValue
repository_list_item_id = cell_value[:repository_list_item_id]
repository_cell.value.update_data!(repository_list_item_id, @user, preview: @preview)
when RepositoryStatusValue
repository_status_item_id = cell_value[:repository_status_item_id]
repository_cell.value.update_data!(repository_status_item_id, @user, preview: @preview)
else
sanitized_cell_value_data = sanitize_cell_value_data(cell_value.data)
repository_cell.value.update_data!(sanitized_cell_value_data, @user, preview: @preview)
end
repository_cell
else
# Create new cell
cell_value.repository_cell.value = cell_value
repository_row.repository_cells << cell_value.repository_cell
@preview ? cell_value.validate : cell_value.save!
@updated ||= true
cell_value.repository_cell
end
end

View file

@ -117,6 +117,7 @@
data-protocol-url="<%= protocol_my_module_path(@my_module) %>"
data-date-format="<%= datetime_picker_format_date_only %>"
data-user-utc-offset="<%= ActiveSupport::TimeZone.find_tzinfo(current_user.time_zone).utc_offset %>"
data-e2e="e2e-CO-task-protocol"
>
<protocol-container
:protocol-url="protocolUrl"

View file

@ -1,29 +1,45 @@
<%= form_for :protocol, url: team_import_external_protocol_path(team_id: current_team.id),
method: :post, data: { remote: true } do |f|%>
<div class="general-error has-error">
<div class="general-error has-error" data-e2e="e2e-TX-protocolTemplates-previewProtocolsIo-error">
<span class="has-error help-block"></span>
</div>
<div class="form-group sci-input-container">
<%= f.label :name, t('protocols.import_export.import_modal.name_label') %>
<%= f.text_field :name, class: 'form-control sci-input-field', value: protocol.name %>
<%= f.label :name,
t('protocols.import_export.import_modal.name_label'),
:"data-e2e" => "e2e-TX-protocolTemplates-previewProtocolsIo-nameInput" %>
<%= f.text_field :name,
class: 'form-control sci-input-field',
value: protocol.name,
:"data-e2e" => "e2e-IF-protocolTemplates-previewProtocolsIo-nameInput" %>
<span class="help-block"></span>
</div>
<div class="form-group sci-input-container">
<%= f.label :authors, t('protocols.import_export.import_modal.authors_label') %>
<%= f.text_field :authors, class: 'form-control sci-input-field', value: protocol.authors %>
<%= f.label :authors,
t('protocols.import_export.import_modal.authors_label'),
:"data-e2e" => "e2e-TX-protocolTemplates-previewProtocolsIo-authorsInput" %>
<%= f.text_field :authors,
class: 'form-control sci-input-field',
value: protocol.authors,
:"data-e2e" => "e2e-IF-protocolTemplates-previewProtocolsIo-authorsInput" %>
</div>
<div class="import-protocol-preview-description">
<div class="import-protocol-preview-description" data-e2e="e2e-TX-protocolTemplates-previewProtocolsIo-description">
<%= custom_auto_link(protocol.description, simple_format: false, team: current_team) %>
</div>
<div class="row">
<div class="col-sm-4">
<div class="form-group">
<%= f.label :published_on_label, t('protocols.import_export.import_modal.published_on_label')%>
<%= f.text_field :published_on_label, value: I18n.l(protocol.published_on, format: :full), class: 'form-control', disabled: true %>
<%= f.label :published_on_label,
t('protocols.import_export.import_modal.published_on_label'),
:"data-e2e" => "e2e-TX-protocolTemplates-previewProtocolsIo-publishedOnLabel" %>
<%= f.text_field :published_on_label,
value: I18n.l(protocol.published_on, format: :full),
class: 'form-control',
disabled: true,
:'data-e2e' => "e2e-TX-protocolTemplates-previewProtocolsIo-publishedOn" %>
</div>
</div>
</div>
@ -39,11 +55,11 @@
<div data-role="steps-container">
<div class="row">
<div class="col-xs-8">
<div class="col-xs-8" data-e2e="e2e-TX-protocolTemplates-previewProtocolsIo-protocolSteps">
<h2><%= t("protocols.steps.subtitle") %></h2>
</div>
</div>
<div id="steps">
<div id="steps" data-e2e="e2e-CO-protocolTemplates-previewProtocolsIo-protocolSteps">
<% protocol.steps.sort_by{ |s| s.position }.each do |step| %>
<%= render partial: "steps/step", locals: { step: step, steps_assets: steps_assets, preview: true, import: true } %>
<% end %>

View file

@ -1,15 +1,15 @@
<div class="footer">
<div class="left-section">
<div class="default-role-container">
<div class="sci-checkbox-container">
<div class="sci-checkbox-container" data-e2e="e2e-CB-protocolTemplates-previewProtocolsIo-grantAccess">
<%= check_box_tag "visibility", "visible", false, class: "sci-checkbox" %>
<span class="sci-checkbox-label"></span>
</div>
<div class="default-role-description">
<div class="default-role-description" data-e2e="e2e-TX-protocolTemplates-previewProtocolsIo-grantAccess">
<%= t("protocols.new_protocol_modal.access_label") %>
</div>
</div>
<div class="hidden" id="roleSelectWrapper">
<div class="hidden" id="roleSelectWrapper" data-e2e="e2e-DD-protocolTemplates-previewProtocolsIo-userRole">
<div class="sci-input-container">
<%= label_tag :default_public_user_role_id, t("protocols.new_protocol_modal.role_label") %>
<% default_role = UserRole.find_by(name: I18n.t('user_roles.predefined.viewer')).id %>
@ -19,7 +19,13 @@
</div>
</div>
<div class="right-section">
<button type="button" class="btn btn-secondary" data-dismiss="modal"><%=t "general.cancel" %></button>
<button type="button" class="btn btn-primary" data-action="import_protocol" data-import_type="in_repository_draft"><%=t "protocols.import_export.import_modal.import_protocols_label" %></button>
<button type="button" class="btn btn-secondary" data-dismiss="modal"
data-e2e="e2e-BT-protocolTemplates-previewProtocolsIo-cancel">
<%=t "general.cancel" %>
</button>
<button type="button" class="btn btn-primary" data-action="import_protocol"
data-import_type="in_repository_draft" data-e2e="e2e-BT-protocolTemplates-previewProtocolsIo-import">
<%=t "protocols.import_export.import_modal.import_protocols_label" %>
</button>
</div>
</div>

View file

@ -1,61 +1,101 @@
<div id="import-protocol-modal" class="modal fade" role="dialog">
<div id="import-protocol-modal" class="modal fade" role="dialog" data-e2e="e2e-MD-protocolTemplates-importEln">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal">&times;</button>
<h4 class="modal-title" data-role="header-import"><%= t("protocols.import_export.import_modal.title_import") %></h4>
<h4 class="modal-title" data-role="header-import-into-protocol"><%= t("protocols.import_export.import_modal.title_import_into_protocol") %></h4>
<button type="button" class="close" data-dismiss="modal" data-e2e="e2e-BT-protocolTemplates-importEln-close">
&times;
</button>
<h4 class="modal-title" data-role="header-import" data-e2e="e2e-TX-protocolTemplates-importEln-title">
<%= t("protocols.import_export.import_modal.title_import") %>
</h4>
<h4 class="modal-title" data-role="header-import-into-protocol">
<%= t("protocols.import_export.import_modal.title_import_into_protocol") %>
</h4>
</div>
<div class="modal-body">
<!-- Warning message -->
<div data-role="import-message" style="margin-bottom: 15px;">
<div data-role="import-message"
style="margin-bottom: 15px;"
data-e2e="e2e-TX-protocolTemplates-importEln-warning">
<b><%= t("protocols.import_export.import_modal.import_into_protocol_message") %></b>
<br />
</div>
<!-- General protocol info -->
<div class="form-group sci-input-container">
<label for="import_protocol_name"><%= t("protocols.import_export.import_modal.name_label") %></label>
<input type="text" class="form-control sci-input-field" id="import_protocol_name">
<label for="import_protocol_name" data-e2e="e2e-TX-protocolTemplates-importEln-nameInput">
<%= t("protocols.import_export.import_modal.name_label") %>
</label>
<input type="text"
class="form-control sci-input-field"
id="import_protocol_name"
data-e2e="e2e-IF-protocolTemplates-importEln-nameInput">
</div>
<div class="form-group sci-input-container">
<label for="protocol_authors">
<label for="protocol_authors" data-e2e="e2e-TX-protocolTemplates-importEln-authorsInput">
<span class="sn-icon sn-icon-user-menu"></span>&nbsp;<%= t("protocols.import_export.import_modal.authors_label") %>
</label>
<input type="text" class="form-control sci-input-field" id="protocol_authors">
<input type="text"
class="form-control sci-input-field"
id="protocol_authors"
data-e2e="e2e-IF-protocolTemplates-importEln-authorsInput">
</div>
<div class="form-group">
<label for="import_protocol_description"><%= t("protocols.import_export.import_modal.description_label") %></label>
<div id="import_protocol_description" class="overflow-auto" rows="2"></div>
<label for="import_protocol_description" data-e2e="e2e-TX-protocolTemplates-importEln-descriptionLabel">
<%= t("protocols.import_export.import_modal.description_label") %>
</label>
<div id="import_protocol_description"
class="overflow-auto"
rows="2"
data-e2e="e2e-TX-protocolTemplates-importEln-description">
</div>
</div>
<div class="form-group">
<div class="row">
<div class="col-xs-4">
<label for="protocol_created_at"><%= t("protocols.import_export.import_modal.created_at_label") %></label>
<input type="text" class="form-control" id="protocol_created_at" disabled>
<label for="protocol_created_at" data-e2e="e2e-TX-protocolTemplates-importEln-createdAtLabel">
<%= t("protocols.import_export.import_modal.created_at_label") %>
</label>
<input type="text"
class="form-control"
id="protocol_created_at"
disabled
data-e2e="e2e-TX-protocolTemplates-importEln-createdAt">
</div>
<div class="col-xs-4">
<label for="protocol_updated_at"><%= t("protocols.import_export.import_modal.updated_at_label") %></label>
<input type="text" class="form-control" id="protocol_updated_at" disabled>
<label for="protocol_updated_at" data-e2e="e2e-TX-protocolTemplates-importEln-updatedAtLabel">
<%= t("protocols.import_export.import_modal.updated_at_label") %>
</label>
<input type="text"
class="form-control"
id="protocol_updated_at"
disabled
data-e2e="e2e-TX-protocolTemplates-importEln-updatedAt">
</div>
</div>
</div>
<!-- Preview title -->
<div>
<h2 style="display: inline;"><%= t("protocols.import_export.import_modal.preview_title") %></h2>
<h2 style="display: inline;" data-e2e="e2e-TX-protocolTemplates-importEln-previewTitle">
<%= t("protocols.import_export.import_modal.preview_title") %>
</h2>
<h3 style="display: none;" data-role="title-position"></h3>
</div>
<!-- Preview scroller -->
<div>
<div class="import-protocols-modal-preview-container" data-role="preview-container">
<div class="import-protocols-modal-preview-container"
data-role="preview-container"
data-e2e="e2e-CO-protocolTemplates-importEln-preview">
</div>
</div>
</div>
<div class="modal-footer">
<div data-role="multiple-protocols-buttons">
<button type="button" class="btn btn-secondary" data-dismiss="modal"><%= t("general.cancel") %></button>
<button type="button" class="btn btn-secondary" data-dismiss="modal">
<%= t("general.cancel") %>
</button>
<div class="btn-group" role="group">
<a href="#" class="btn btn-secondary" data-action="jump-to-first-protocol"><i class="fas fa-fast-backward"></i></a>
<a href="#" class="btn btn-secondary" data-action="jump-to-previous-protocol"><i class="fas fa-backward"></i></a>
@ -64,18 +104,34 @@
</div>
<div class="btn-group" role="group">
<div data-role="import-all">
<button type="submit" class="btn btn-success" data-action="import-current"><%= t("protocols.import_export.import_modal.import_current") %></button>
<button type="submit" class="btn btn-success" data-action="import-all"><%= t("protocols.import_export.import_modal.import_all") %></button>
<button type="submit" class="btn btn-success" data-action="import-current">
<%= t("protocols.import_export.import_modal.import_current") %>
</button>
<button type="submit" class="btn btn-success" data-action="import-all">
<%= t("protocols.import_export.import_modal.import_all") %>
</button>
</div>
<div data-role="import-single">
<button type="submit" class="btn btn-success" data-action="import-current"><%= t("protocols.import_export.import_modal.import") %></button>
<button type="submit" class="btn btn-success" data-action="import-current">
<%= t("protocols.import_export.import_modal.import") %>
</button>
</div>
</div>
</div>
<div data-role="single-protocol-buttons">
<button type="button" class="btn btn-secondary" data-dismiss="modal"><%= t("general.cancel") %></button>
<button type="button"
class="btn btn-secondary"
data-dismiss="modal"
data-e2e="e2e-BT-protocolTemplates-importEln-cancel">
<%= t("general.cancel") %>
</button>
<div class="btn-group" role="group">
<button type="submit" class="btn btn-success" data-action="import-current"><%= t("protocols.import_export.import_modal.import") %></button>
<button type="submit"
class="btn btn-success"
data-action="import-current"
data-e2e="e2e-BT-protocolTemplates-importEln-load">
<%= t("protocols.import_export.import_modal.import") %>
</button>
</div>
</div>
</div>

View file

@ -13,7 +13,7 @@
<div class="content-pane flexible protocols-index <%= @type %>">
<div class="content-header sticky-header">
<div class="title-row">
<div class="title-row" data-e2e="e2e-TX-protocolTemplates-title">
<% if templates_view_mode_archived?(type: @type) %>
<h1>
<span><%= t('labels.archived')%></span>&nbsp;
@ -24,7 +24,7 @@
<% end %>
</div>
</div>
<div class="protocols-container">
<div class="protocols-container" data-e2e="e2e-CO-protocolTemplates">
<div id="ProtocolsTable" class="fixed-content-body">
<protocols-table
ref="table"

View file

@ -1,13 +1,21 @@
<div class="modal" id="protocol-preview-modal" tabindex="-1" role="dialog" aria-labelledby="protocol-preview-modal-label">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-content" data-e2e="e2e-MD-protocolTemplates-previewProtocolsIo">
<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="protocol-preview-modal-label">
<button type="button"
class="close"
data-dismiss="modal"
aria-label="Close"
data-e2e="e2e-BT-protocolTemplates-previewProtocolsIo-close">
<i class="sn-icon sn-icon-close"></i>
</button>
<h4 class="modal-title"
id="protocol-preview-modal-label"
data-e2e="e2e-TX-protocolTemplates-previewProtocolsIo-title">
</h4>
</div>
<div class="modal-body"></div>
<div class="modal-footer"></div>
<div class="modal-body" data-e2e="e2e-CO-protocolTemplates-previewProtocolsIo-body"></div>
<div class="modal-footer" data-e2e="e2e-CO-protocolTemplates-previewProtocolsIo-footer"></div>
</div>
</div>
</div>

View file

@ -1,6 +1,6 @@
<div class="modal" id="protocolsioModal" data-url="<%= protocolsio_protocols_path %>" tabindex="-1" role="dialog" aria-labelledby="protocolsio-modal-label">
<div class="modal-dialog" role="document">
<div class="modal-content"></div>
<div class="modal-content" data-e2e="e2e-MD-protocolTemplates-importProtocolsIo"></div>
</div>
</div>
<%= javascript_include_tag "protocols/steps" %>

View file

@ -1,6 +1,12 @@
<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="publish-results-modal-label">
<button type="button"
class="close"
data-dismiss="modal"
aria-label="Close"
data-e2e="e2e-BT-protocolTemplates-importProtocolsIo-close">
<i class="sn-icon sn-icon-close"></i>
</button>
<h4 class="modal-title" id="publish-results-modal-label" data-e2e="e2e-TX-protocolTemplates-importProtocolsIo-title">
<%= t('protocols.index.protocolsio.title') %>
</h4>
</div>
@ -20,6 +26,7 @@
<input class='sci-input-field'
type='text'
name='key'
data-e2e='e2e-IF-protocolTemplates-importProtocolsIo-search'
placeholder="<%= t('protocols.index.protocolsio.search_bar_placeholder') %>" />
<i class='sn-icon sn-icon-search'></i>
</div>
@ -27,7 +34,13 @@
<div class='protocol-sort'>
<div class="dropdown sort-menu" title="<%= t("general.sort.title") %>">
<button class="btn btn-light btn-black icon-btn" type="button" id="sortMenu" data-toggle="dropdown" aria-haspopup="true" aria-expanded="true">
<button class="btn btn-light btn-black icon-btn"
type="button"
id="sortMenu"
data-toggle="dropdown"
aria-haspopup="true"
aria-expanded="true"
data-e2e="e2e-DD-protocolTemplates-importProtocolsIo-sort">
<span><i class="sn-icon sn-icon-sort-down"></i></span>
</button>
<ul id="sortMenuDropdown" class="dropdown-menu sort-projects-menu dropdown-menu-right" aria-labelledby="sortMenu">
@ -43,33 +56,39 @@
</div>
<% end %>
<div class='protocol-list-side-panel'>
<div class='row empty-text'>
<div class='row empty-text' data-e2e="e2e-TX-protocolTemplates-importProtocolsIo-results-empty">
<%= t('protocols.index.protocolsio.list_panel.empty_text') %>
</div>
<div class='list-wrapper perfect-scrollbar'></div>
<div class='list-wrapper perfect-scrollbar' data-e2e="e2e-CO-protocolTemplates-importProtocolsIo-results">
</div>
</div>
</div>
<div class='protocol-preview-panel'>
<div class='empty-preview-panel'>
<div class='row'>
<div class='text-rows protocol-preview-text'>
<div class='text-rows protocol-preview-text'
data-e2e="e2e-TX-protocolTemplates-importProtocolsIo-previewEmpty-title">
<%= t('protocols.index.protocolsio.preview_panel.empty_title') %>
</div>
</div>
<div class='row'>
<div class='text-separator'> <hr> </div>
<div class='text-separator' data-e2e="e2e-EL-protocolTemplates-importProtocolsIo-previewEmpty-separator">
<hr>
</div>
</div>
<div class='row'>
<div class='text-rows protocol-preview-subtext'>
<div class='text-rows protocol-preview-subtext'
data-e2e="e2e-TX-protocolTemplates-importProtocolsIo-previewEmpty-subText">
<%= t('protocols.index.protocolsio.preview_panel.empty_subtext') %>
</div>
</div>
<div class='row-bottom'>
<div class='text-rows protocol-preview-subtext'>
<div class='text-rows protocol-preview-subtext'
data-e2e="e2e-TX-protocolTemplates-importProtocolsIo-previewEmpty-poweredBy">
<%= t('protocols.index.protocolsio.preview_panel.powered_by') %>
</div>
</div>
@ -77,7 +96,7 @@
<div class='full-preview-panel' style='display: none;'>
<div class='row preview-banner'>
<div class='col-md-6 txt-holder'>
<div class='col-md-6 txt-holder' data-e2e="e2e-TX-protocolTemplates-importProtocolsIo-preview-title">
<span>
<b><%= t('protocols.index.protocolsio.preview_panel.banner_text') %></b>
</span>
@ -85,7 +104,7 @@
<div class='col-md-6 btn-holder'>
</div>
</div>
<div class='preview-holder perfect-scrollbar'>
<div class='preview-holder perfect-scrollbar' data-e2e="e2e-CO-protocolTemplates-importProtocolsIo-preview">
<iframe scrolling="no" class='preview-iframe'></iframe>
</div>
</div>
@ -94,7 +113,17 @@
</div>
</div>
<div class="modal-footer">
<button type="button" data-dismiss="modal" class="btn btn-secondary"><%=t('general.cancel') %></button>
<button type="button" class="btn btn-primary convert-protocol" disabled><%= t('protocols.index.protocolsio.convert') %></button>
<button type="button"
data-dismiss="modal"
class="btn btn-secondary"
data-e2e="e2e-BT-protocolTemplates-importProtocolsIo-cancel">
<%=t('general.cancel') %>
</button>
<button type="button"
class="btn btn-primary convert-protocol"
disabled
data-e2e="e2e-BT-protocolTemplates-importProtocolsIo-convert">
<%= t('protocols.index.protocolsio.convert') %>
</button>
</div>

View file

@ -6,7 +6,7 @@
<div class="content-pane protocols-show flexible with-grey-background pb-4" >
<div class="content-header sticky-header">
<div class="title-row">
<h1>
<h1 data-e2e="e2e-TX-protocolTemplates-protocol-title">
<% if @inline_editable_title_config.present? %>
<%= render partial: "shared/inline_editing",
locals: {
@ -33,6 +33,7 @@
data-protocol-url="<%= protocol_path(@protocol) %>"
data-date-format="<%= datetime_picker_format_date_only %>"
data-user-utc-offset="<%= ActiveSupport::TimeZone.find_tzinfo(current_user.time_zone).utc_offset %>"
data-e2e="e2e-CO-protocolTemplates-protocol"
>
<protocol-container
:protocol-url="protocolUrl"

View file

@ -1,7 +0,0 @@
<% if error.present? %>
<div class="alert alert-danger" role="alert">
<div><%= error_title %></div>
<br>
<%= error %>
</div>
<% end %>

View file

@ -77,6 +77,8 @@ en:
filter_create_new: "Create"
cancel: "Cancel"
create: "Create"
new_project: "New \"%{name}\" Project"
new_experiment: "New \"%{name}\" Experiment"
recent_work:
title: "Recent work"
no_results:

View file

@ -2197,38 +2197,40 @@ en:
list_row: "Row %{row}"
list_error: "%{key}: %{val}"
import_records:
update_inventory: 'Update inventory'
update_inventory: 'Import items'
steps:
step1:
id: 'step1'
icon: 'sn-icon-open'
label: 'Step 1'
title: 'Update inventory'
subtitle: 'To add or edit items, export the inventory and reimport edited inventory.'
title: 'Import items'
subtitle: 'Inventory import allows for adding new items or updating existing data. Ensure the imported file contains column header names. For easy import, export the full inventory with existing items or only the inventory columns template.'
helpText: 'Help'
exportTitle: 'Export'
exportFullInvBtnText: 'Export full inventory'
exportEmptyInvBtnText: 'Export empty inventory'
exportFullInvBtnText: 'Export all items'
exportEmptyInvBtnText: 'Download inventory template'
importTitle: 'Import'
importBtnText: 'Import'
cancelBtnText: 'Cancel'
dragAndDropSupportingText: '.XLSX, .XLS or .CSV file'
dragAndDropSupportingText: '.xlsx, .xls, .csv or .txt file'
step2:
id: 'step2'
icon: 'sn-icon-open'
label: 'Step 2'
title: 'Mapping data'
subtitle: 'Match your imported columns with the columns in the SciNote inventory.'
subtitle: 'Match your imported file columns with the existing columns in the inventory to finalize the item import.'
selectNamePropertyError: 'Select Name attribute field to import your items.'
autoMappingText: 'Auto-mapping'
autoMappingTooltip: 'When auto-mapping is selected, SciNote automatically connects columns with matching names. When not selected, manual mapping of each column is required.'
updateEmptyCellsText: 'Update empty cells'
onlyAddNewItemsText: 'Only add new items'
importedFileText: 'Imported file:'
cancelBtnText: 'Cancel'
confirmBtnText: 'Confirm'
importedIgnoredSection: '<b>%{imported}</b> columns to <b>import.</b> <b>%{ignored}</b> columns <b>ignored</b>.'
importedIgnoredSection: '<b>%{imported}</b> columns to <b>import.</b> <b>%{ignored}</b> columns <b>ignored.</b>'
computedDropdownOptions:
name: 'Name'
id: 'ID'
RepositoryTextValue: 'Text'
RepositoryNumberValue: 'Number'
RepositoryAssetValue: 'File'
@ -2245,12 +2247,9 @@ en:
placeholders:
matchNotFound: 'Match not found'
doNotImport: 'Do not import'
defaultColumnTitle: 'Default column. Mapped as identifier.'
matchNotFoundColumnTitle: 'Match not found.'
userDefinedColumnTitle: 'Column name does not match. Column will be imported as '
matchNotFoundColumnTitle: 'Column match not found.'
importedColumnTitle: 'Column will be imported.'
doNotImportColumnTitle: 'Column will not import.'
importAsNewColumn: 'Import as new column'
doNotImportColumnTitle: 'Column will not be imported.'
newColumnType:
text: 'Text'
list: 'Dropdown'
@ -2271,18 +2270,26 @@ en:
exampleData: 'Example data'
step3:
title: 'Import preview'
subtitle: 'This is a preview of items you are importing/updating to the %{inventory}. The import can still be canceled.'
updated_items: 'Updated<br>items'
new_items: 'New<br>items'
unchanged_items: 'Unchanged<br>items'
duplicated_items: 'Duplicated<br>items'
invalid_items: 'Invalid<br>items'
invalid_cells: 'Invalid<br>cells'
code: 'Code'
subtitle: 'This preview shows changes to the %{inventory} inventory resulting from this import. Values that will be updated are marked in green and any errors in red. Status of import provides further details, and the item import can still be canceled at this stage.'
updated_items: 'Updated'
new_items: 'New'
unchanged_items: 'Unchanged'
duplicated_items: 'Duplicated'
invalid_items: 'Invalid'
archived_items: 'Archived'
code: 'Item ID'
name: 'Name'
status: 'Status'
cancel: 'Cancel import'
confirm: 'Confirm'
status_message:
created: 'new item'
updated: 'updated'
unchanged: 'unchanged'
not_exist: "item ID IT%{id} doesn't exist in this inventory"
archived: 'archived'
invalid: 'invalid item'
duplicated: 'item ID has duplicates in the imported file'
step4:
title: 'Success report'
subtitle: '%{inventory} was successfully updated.'
@ -2291,28 +2298,28 @@ en:
download_report: 'Download success report'
close: 'Close'
info_sidebar:
title: 'Guide for updating the inventory'
title: 'Item import guide'
elements:
element0:
id: 'el0'
icon: 'sn-icon-export'
label: 'Export inventory'
subtext: "Before making edits, we advise you to export the latest inventory information. If you're only adding new items, consider exporting empty inventory."
subtext: "Before making changes, we advise you to first export the current inventory item information. If you're only adding in new items, exporting the inventory template is recommended."
element1:
id: 'el1'
icon: 'sn-icon-edit'
label: 'Edit your data'
subtext: 'Make sure to include header names in first row, followed by item data.'
subtext: 'Structure your import data”. And the text updated to: “Make sure to include header names in the first row of the import file, followed by item data.'
element2:
id: 'el2'
icon: 'sn-icon-import'
label: 'Import new or update items'
subtext: 'Upload your data using .xlsx, .csv or .txt files.'
label: 'Upload your file'
subtext: 'Upload your data using .xlsx, .csv, or .txt files to import new items or update existing item data.'
element3:
id: 'el3'
icon: 'sn-icon-tables'
label: 'Merge your data'
subtext: 'Complete the process by merging the columns you want to update.'
label: 'Map and finalize your data'
subtext: 'Complete the item import process by mapping uploaded file data with the columns you want to update in the inventory.'
element4:
id: 'el4'
icon: 'sn-icon-open'
@ -2330,7 +2337,7 @@ en:
import: 'Import'
no_header_name: 'No column name'
success_flash: "%{number_of_rows} of %{total_nr} new item(s) successfully imported."
partial_success_flash: "%{nr} of %{total_nr} successfully imported. Other rows contained errors."
partial_success_flash: "%{nr} of %{total_nr} items successfully imported."
error_message:
items_limit: "The imported file contains too many rows. Max %{items_size} items allowed to upload at once."
importing_duplicates: "Items with duplicates detected: %{duplicate_ids}. These will be ignored on import."
@ -2341,6 +2348,7 @@ en:
duplicated_values: "Two or more columns have the same mapping."
errors_list_title: "Items were not imported because one or more errors were found:"
no_repository_name: "Item name is required!"
mapping_error: "Column mappings are required"
edit_record: "Edit"
assign_record: "Assign to task"
copy_record: "Duplicate"
@ -2424,6 +2432,7 @@ en:
no_records_selected_flash: "There were no selected items."
no_deleted_records_flash: "No items were deleted. %{other_records_number} of the selected items were created by other users and were not deleted."
default_column: 'Name'
id_column: 'Item ID'
copy_records_report: "%{number} item(s) successfully copied."
archive_inventories:
success_flash: "Inventories were successfully archived!"

View file

@ -1,6 +0,0 @@
class AddDiscardedAtToResults < ActiveRecord::Migration[7.0]
def change
add_column :results, :discarded_at, :datetime
add_index :results, :discarded_at
end
end

View file

@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema[7.0].define(version: 2024_04_29_070135) do
ActiveRecord::Schema[7.0].define(version: 2024_01_18_094253) do
# These are extensions that must be enabled in order to support this database
enable_extension "btree_gist"
enable_extension "pg_trgm"
@ -984,12 +984,10 @@ ActiveRecord::Schema[7.0].define(version: 2024_04_29_070135) do
t.bigint "restored_by_id"
t.datetime "restored_on", precision: nil
t.integer "assets_view_mode", default: 0
t.datetime "discarded_at"
t.index "trim_html_tags((name)::text) gin_trgm_ops", name: "index_results_on_name", using: :gin
t.index ["archived"], name: "index_results_on_archived"
t.index ["archived_by_id"], name: "index_results_on_archived_by_id"
t.index ["created_at"], name: "index_results_on_created_at"
t.index ["discarded_at"], name: "index_results_on_discarded_at"
t.index ["last_modified_by_id"], name: "index_results_on_last_modified_by_id"
t.index ["my_module_id"], name: "index_results_on_my_module_id"
t.index ["restored_by_id"], name: "index_results_on_restored_by_id"

View file

@ -72,7 +72,7 @@
"node-gyp": "9.3.1",
"node-polyfill-webpack-plugin": "^2.0.1",
"pdfjs-dist": "^2.5.207",
"postcss": "8.4.31",
"postcss": "8.4.32",
"postcss-loader": "5.3.0",
"puppeteer": "npm:puppeteer-core",
"puppeteer-core": "^21.3.8",

View file

@ -2414,11 +2414,11 @@ brace-expansion@^2.0.1:
balanced-match "^1.0.0"
braces@~3.0.2:
version "3.0.2"
resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107"
integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==
version "3.0.3"
resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.3.tgz#490332f40919452272d55a8480adc0c441358789"
integrity sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==
dependencies:
fill-range "^7.0.1"
fill-range "^7.1.1"
brorand@^1.0.1, brorand@^1.1.0:
version "1.1.0"
@ -3679,10 +3679,10 @@ file-selector@^0.4.0:
dependencies:
tslib "^2.0.3"
fill-range@^7.0.1:
version "7.0.1"
resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40"
integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==
fill-range@^7.1.1:
version "7.1.1"
resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.1.1.tgz#44265d3cac07e3ea7dc247516380643754a05292"
integrity sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==
dependencies:
to-regex-range "^5.0.1"
@ -5471,10 +5471,10 @@ nanoid@^2.1.0:
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-2.1.11.tgz#ec24b8a758d591561531b4176a01e3ab4f0f0280"
integrity sha512-s/snB+WGm6uwi0WjsZdaVcuf3KJXlfGl2LcxgwkEwJF0D/BWzVWAZW/XY4bFaiR7s0Jk3FPvlnepg1H1b1UwlA==
nanoid@^3.3.4, nanoid@^3.3.6:
version "3.3.6"
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.6.tgz#443380c856d6e9f9824267d960b4236ad583ea4c"
integrity sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==
nanoid@^3.3.7:
version "3.3.7"
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.7.tgz#d0c301a691bc8d54efa0a2226ccf3fe2fd656bd8"
integrity sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==
nanoid@^4.0.0:
version "4.0.2"
@ -6163,12 +6163,12 @@ postcss-value-parser@^4.1.0, postcss-value-parser@^4.2.0:
resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz#723c09920836ba6d3e5af019f92bc0971c02e514"
integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==
postcss@8.4.31:
version "8.4.31"
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.31.tgz#92b451050a9f914da6755af352bdc0192508656d"
integrity sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==
postcss@8.4.32, postcss@^8.1.10, postcss@^8.4.19:
version "8.4.32"
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.32.tgz#1dac6ac51ab19adb21b8b34fd2d93a86440ef6c9"
integrity sha512-D/kj5JNu6oo2EIy+XL/26JEDTlIbB8hw85G8StOE6L74RQAVVP5rej6wxCNqyMbR4RkPfqvezVbPw81Ngd6Kcw==
dependencies:
nanoid "^3.3.6"
nanoid "^3.3.7"
picocolors "^1.0.0"
source-map-js "^1.0.2"
@ -6180,24 +6180,6 @@ postcss@^7.0.1:
picocolors "^0.2.1"
source-map "^0.6.1"
postcss@^8.1.10:
version "8.4.26"
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.26.tgz#1bc62ab19f8e1e5463d98cf74af39702a00a9e94"
integrity sha512-jrXHFF8iTloAenySjM/ob3gSj7pCu0Ji49hnjqzsgSRa50hkWCKD0HQ+gMNJkW38jBI68MpAAg7ZWwHwX8NMMw==
dependencies:
nanoid "^3.3.6"
picocolors "^1.0.0"
source-map-js "^1.0.2"
postcss@^8.4.19:
version "8.4.21"
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.21.tgz#c639b719a57efc3187b13a1d765675485f4134f4"
integrity sha512-tP7u/Sn/dVxK2NnruI4H9BG+x+Wxz6oeZ1cJ8P6G/PZY0IKk4k/63TDsQf2kQq3+qoJeLm2kIBUNlZe3zgb4Zg==
dependencies:
nanoid "^3.3.4"
picocolors "^1.0.0"
source-map-js "^1.0.2"
prelude-ls@~1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54"
@ -6309,8 +6291,19 @@ punycode@^2.1.0, punycode@^2.1.1:
resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.0.tgz#f67fa67c94da8f4d0cfff981aee4118064199b8f"
integrity sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==
puppeteer-core@^21.3.8, "puppeteer@npm:puppeteer-core":
name puppeteer
puppeteer-core@^21.3.8:
version "21.3.8"
resolved "https://registry.yarnpkg.com/puppeteer-core/-/puppeteer-core-21.3.8.tgz#7ac4879c9f73e8426431d8ca4c58680e517a4b08"
integrity sha512-yv12E/+zZ7Lei5tJB4sUkSrsuqKibuYpYxLGbmtLUjjYIqGE5HKz9OUI2I/RFHEvF+pHi2bTbv5bWydeCGJ6Mw==
dependencies:
"@puppeteer/browsers" "1.7.1"
chromium-bidi "0.4.31"
cross-fetch "4.0.0"
debug "4.3.4"
devtools-protocol "0.0.1179426"
ws "8.14.2"
"puppeteer@npm:puppeteer-core":
version "21.3.8"
resolved "https://registry.yarnpkg.com/puppeteer-core/-/puppeteer-core-21.3.8.tgz#7ac4879c9f73e8426431d8ca4c58680e517a4b08"
integrity sha512-yv12E/+zZ7Lei5tJB4sUkSrsuqKibuYpYxLGbmtLUjjYIqGE5HKz9OUI2I/RFHEvF+pHi2bTbv5bWydeCGJ6Mw==