mirror of
https://github.com/scinote-eln/scinote-web.git
synced 2025-12-09 05:37:36 +08:00
Merge branch 'develop' into ai-sci-10818-poc-instruction-in-inventory-export
This commit is contained in:
commit
d8a270d8f7
63 changed files with 652 additions and 459 deletions
|
|
@ -299,9 +299,6 @@ Naming/VariableName:
|
|||
Naming/VariableNumber:
|
||||
EnforcedStyle: normalcase
|
||||
|
||||
Naming/BlockForwarding:
|
||||
EnforcedStyle: explicit
|
||||
|
||||
Style/WordArray:
|
||||
EnforcedStyle: percent
|
||||
MinSize: 0
|
||||
|
|
|
|||
2
VERSION
2
VERSION
|
|
@ -1 +1 @@
|
|||
1.35.0.1
|
||||
1.35.0.2
|
||||
|
|
|
|||
|
|
@ -237,7 +237,9 @@ var MarvinJsEditorApi = (function() {
|
|||
enabled: function() {
|
||||
return ($('#MarvinJsModal').length > 0);
|
||||
},
|
||||
|
||||
isRemote: function() {
|
||||
return marvinJsMode === 'remote';
|
||||
},
|
||||
open: function(config) {
|
||||
if (!MarvinJsEditor.enabled()) {
|
||||
$('#MarvinJsPromoModal').modal('show');
|
||||
|
|
@ -322,19 +324,22 @@ $(document).on('click', '.gene-sequence-edit-button', function() {
|
|||
function initMarvinJs() {
|
||||
MarvinJsEditor = MarvinJsEditorApi();
|
||||
|
||||
if (MarvinJsEditor.enabled()) {
|
||||
if (typeof (ChemicalizeMarvinJs) === 'undefined') {
|
||||
setTimeout(initMarvinJs, 100);
|
||||
return;
|
||||
}
|
||||
// MarvinJS is disabled, nothing to initialize
|
||||
if (!MarvinJsEditor.enabled()) return;
|
||||
|
||||
if ($('#marvinjs-editor')[0].dataset.marvinjsMode === 'remote') {
|
||||
ChemicalizeMarvinJs.createEditor('#marvinjs-sketch').then(function(marvin) {
|
||||
marvin.setDisplaySettings({ toolbars: 'reporting' });
|
||||
marvinJsRemoteEditor = marvin;
|
||||
});
|
||||
}
|
||||
// wait for remote MarvinJS to initialize
|
||||
if (MarvinJsEditor.isRemote() && typeof (ChemicalizeMarvinJs) === 'undefined') {
|
||||
setTimeout(initMarvinJs, 100);
|
||||
return;
|
||||
}
|
||||
|
||||
if (MarvinJsEditor.isRemote()) {
|
||||
ChemicalizeMarvinJs.createEditor('#marvinjs-sketch').then(function(marvin) {
|
||||
marvin.setDisplaySettings({ toolbars: 'reporting' });
|
||||
marvinJsRemoteEditor = marvin;
|
||||
});
|
||||
}
|
||||
|
||||
MarvinJsEditor.initNewButton('.new-marvinjs-upload-button');
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -14,6 +14,8 @@
|
|||
initDisclaimer('.log-in-button', '#new_user');
|
||||
initDisclaimer('.btn-okta', '#oktaForm');
|
||||
initDisclaimer('.btn-azure-ad', '.azureAdForm');
|
||||
initDisclaimer('.btn-openid-connect', '#openidConnectForm');
|
||||
initDisclaimer('.btn-saml', '#samlForm');
|
||||
initDisclaimer('.sign-up-button', '#sign-up-form');
|
||||
initDisclaimer('.forgot-password-submit', '#forgot-password-form');
|
||||
initDisclaimer('.invitation-submit', '#invitation-form');
|
||||
|
|
|
|||
|
|
@ -125,12 +125,6 @@
|
|||
z-index: 2;
|
||||
}
|
||||
}
|
||||
|
||||
.asset-context-menu {
|
||||
position: absolute;
|
||||
right: 1rem;
|
||||
top: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
.inline-attachment-container {
|
||||
|
|
@ -290,7 +284,7 @@
|
|||
@media (min-width: 640px) {
|
||||
|
||||
&:hover {
|
||||
justify-content: space-between;
|
||||
|
||||
|
||||
#action-buttons {
|
||||
display: flex;
|
||||
|
|
|
|||
|
|
@ -385,21 +385,11 @@ mark,.mark {
|
|||
.azure-sign-in-actions {
|
||||
margin-bottom: 10px;
|
||||
margin-top: 10px;
|
||||
|
||||
.btn-azure-ad {
|
||||
background-color: $office-ms-word;
|
||||
color: $color-white;
|
||||
}
|
||||
}
|
||||
|
||||
.okta-sign-in-actions {
|
||||
margin-bottom: 10px;
|
||||
margin-top: 10px;
|
||||
|
||||
.btn-okta {
|
||||
background-color: #00297a;
|
||||
color: $color-white;
|
||||
}
|
||||
}
|
||||
|
||||
.navbar-secondary {
|
||||
|
|
|
|||
|
|
@ -264,6 +264,7 @@ class RepositoriesController < ApplicationController
|
|||
parsed_file = ImportRepository::ParseRepository.new(
|
||||
file: import_params[:file],
|
||||
repository: @repository,
|
||||
date_format: current_user.settings['date_format'],
|
||||
session: session
|
||||
)
|
||||
if parsed_file.too_large?
|
||||
|
|
@ -272,6 +273,9 @@ class RepositoriesController < ApplicationController
|
|||
elsif parsed_file.has_too_many_rows?
|
||||
render json: { error: t('repositories.import_records.error_message.items_limit',
|
||||
items_size: Constants::IMPORT_REPOSITORY_ITEMS_LIMIT) }, status: :unprocessable_entity
|
||||
elsif parsed_file.has_too_little_rows?
|
||||
render json: { error: t('repositories.parse_sheet.errors.items_min_limit') },
|
||||
status: :unprocessable_entity
|
||||
else
|
||||
@import_data = parsed_file.data
|
||||
|
||||
|
|
@ -310,10 +314,18 @@ class RepositoriesController < ApplicationController
|
|||
preview: import_params[:preview]
|
||||
).import!
|
||||
message = t('repositories.import_records.partial_success_flash',
|
||||
nr: status[:nr_of_added], total_nr: status[:total_nr])
|
||||
successful_rows_count: (status[:created_rows_count] + status[:updated_rows_count]),
|
||||
total_rows_count: status[:total_rows_count])
|
||||
|
||||
if status[:status] == :ok
|
||||
log_activity(:import_inventory_items, num_of_items: status[:nr_of_added])
|
||||
unless import_params[:preview] || (status[:created_rows_count] + status[:updated_rows_count]).zero?
|
||||
log_activity(
|
||||
:inventory_items_added_or_updated_with_import,
|
||||
created_rows_count: status[:created_rows_count],
|
||||
updated_rows_count: status[:updated_rows_count]
|
||||
)
|
||||
end
|
||||
|
||||
render json: import_params[:preview] ? status : { message: message }, status: :ok
|
||||
else
|
||||
render json: { message: message }, status: :unprocessable_entity
|
||||
|
|
@ -331,7 +343,11 @@ class RepositoriesController < ApplicationController
|
|||
|
||||
xlsx = RepositoryXlsxExport.to_empty_xlsx(@repository, col_ids)
|
||||
|
||||
send_data xlsx, filename: "empty_repository.xlsx", type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
|
||||
send_data(
|
||||
xlsx,
|
||||
filename: "#{@repository.name.gsub(/\s/, '_')}_template_#{Date.current}.xlsx",
|
||||
type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
|
||||
)
|
||||
end
|
||||
|
||||
def export_repository
|
||||
|
|
|
|||
|
|
@ -29,6 +29,11 @@ class RepositoryColumnsController < ApplicationController
|
|||
end
|
||||
|
||||
def describe_all
|
||||
additional_columns = [
|
||||
%w(updated_on RepositoryDateTimeValue),
|
||||
%w(updated_by RepositoryUserValue)
|
||||
]
|
||||
|
||||
response_json = @repository.repository_columns
|
||||
.where(data_type: Extends::REPOSITORY_ADVANCED_SEARCHABLE_COLUMNS)
|
||||
.map do |column|
|
||||
|
|
@ -39,6 +44,12 @@ class RepositoryColumnsController < ApplicationController
|
|||
items: column.items&.map { |item| { value: item.id, label: escape_input(item.data) } }
|
||||
}
|
||||
end
|
||||
|
||||
additional_columns.each do |column, column_type|
|
||||
response_json << { id: column,
|
||||
name: I18n.t("repositories.table.#{column}"),
|
||||
data_type: column_type }
|
||||
end
|
||||
render json: { response: response_json }
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -199,6 +199,10 @@ module ApplicationHelper
|
|||
ENV['SSO_ENABLED'] == 'true'
|
||||
end
|
||||
|
||||
def sso_provider_enabled?
|
||||
okta_enabled?.present? || azure_ad_enabled?.present? || saml_enabled?.present? || openid_connect_enabled?.present?
|
||||
end
|
||||
|
||||
def okta_enabled?
|
||||
ApplicationSettings.instance.values.dig('okta', 'enabled')
|
||||
end
|
||||
|
|
|
|||
|
|
@ -3,7 +3,13 @@ import PerfectScrollbar from 'vue3-perfect-scrollbar';
|
|||
import DashboardNewTask from '../../vue/dashboard/new_task.vue';
|
||||
import { mountWithTurbolinks } from './helpers/turbolinks.js';
|
||||
|
||||
const app = createApp();
|
||||
const app = createApp({
|
||||
data() {
|
||||
return {
|
||||
modalKey: 0
|
||||
};
|
||||
}
|
||||
});
|
||||
app.component('DashboardNewTask', DashboardNewTask);
|
||||
app.config.globalProperties.i18n = window.I18n;
|
||||
app.use(PerfectScrollbar);
|
||||
|
|
|
|||
|
|
@ -64,26 +64,6 @@ const DEFAULT_FILTERS = [
|
|||
},
|
||||
data: { operator: 'any_of' },
|
||||
isBlank: true
|
||||
},
|
||||
{
|
||||
id: 7,
|
||||
column: {
|
||||
data_type: 'RepositoryDateTimeValue',
|
||||
id: 'updated_on',
|
||||
name: I18n.t('repositories.table.updated_on')
|
||||
},
|
||||
data: { operator: 'equal_to' },
|
||||
isBlank: true
|
||||
},
|
||||
{
|
||||
id: 8,
|
||||
column: {
|
||||
data_type: 'RepositoryUserValue',
|
||||
id: 'updated_by',
|
||||
name: I18n.t('repositories.table.updated_by')
|
||||
},
|
||||
data: { operator: 'any_of' },
|
||||
isBlank: true
|
||||
}
|
||||
];
|
||||
|
||||
|
|
@ -96,8 +76,6 @@ window.initRepositoryFilter = () => {
|
|||
{ id: 'relationships', name: I18n.t('repositories.table.relationships'), data_type: 'RepositoryRelationshipValue' },
|
||||
{ id: 'added_on', name: I18n.t('repositories.table.added_on'), data_type: 'RepositoryDateTimeValue' },
|
||||
{ id: 'added_by', name: I18n.t('repositories.table.added_by'), data_type: 'RepositoryUserValue' },
|
||||
{ id: 'updated_on', name: I18n.t('repositories.table.updated_on'), data_type: 'RepositoryDateTimeValue' },
|
||||
{ id: 'updated_by', name: I18n.t('repositories.table.updated_by'), data_type: 'RepositoryUserValue' },
|
||||
{ id: 'archived_by', name: I18n.t('repositories.table.archived_by'), data_type: 'RepositoryUserValue' },
|
||||
{ id: 'archived_on', name: I18n.t('repositories.table.archived_on'), data_type: 'RepositoryDateTimeValue' }
|
||||
];
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
<div class="sci-input-container-v2" :class="{
|
||||
'error': !validTaskName && taskName.length > 0
|
||||
}" >
|
||||
<input type="text" class="sci-input" v-model="taskName" />
|
||||
<input type="text" ref="taskName" class="sci-input" v-model="taskName" :placeholder="i18n.t('dashboard.create_task_modal.task_name_placeholder')" />
|
||||
</div>
|
||||
<span v-if="!validTaskName && taskName.length > 0" class="sci-error-text">
|
||||
{{ i18n.t("dashboard.create_task_modal.task_name_error", { length: minLength }) }}
|
||||
|
|
@ -18,6 +18,7 @@
|
|||
:searchable="true"
|
||||
:value="selectedProject"
|
||||
:optionRenderer="newProjectRenderer"
|
||||
:placeholder="i18n.t('dashboard.create_task_modal.project_placeholder')"
|
||||
@change="changeProject"
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -30,8 +31,13 @@
|
|||
<span v-html="i18n.t('projects.index.modal_new_project.visibility_html')"></span>
|
||||
</div>
|
||||
<div class="mt-4" :class="{'hidden': !publicProject}">
|
||||
<label class="sci-label">{{ i18n.t("user_assignment.select_default_user_role") }}</label>
|
||||
<SelectDropdown :options="userRoles" :value="defaultRole" @change="changeRole" />
|
||||
<label class="sci-label">{{ i18n.t("dashboard.create_task_modal.user_role") }}</label>
|
||||
<SelectDropdown
|
||||
:options="userRoles"
|
||||
:value="defaultRole"
|
||||
@change="changeRole"
|
||||
:placeholder="i18n.t('dashboard.create_task_modal.user_role_placeholder')"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-4">
|
||||
|
|
@ -45,6 +51,7 @@
|
|||
:disabled="!(selectedProject != null && selectedProject >= 0)"
|
||||
:searchable="true"
|
||||
:value="selectedExperiment"
|
||||
:placeholder="i18n.t('dashboard.create_task_modal.experiment_placeholder')"
|
||||
:optionRenderer="newExperimentRenderer"
|
||||
@change="changeExperiment"
|
||||
/>
|
||||
|
|
@ -88,6 +95,14 @@ export default {
|
|||
},
|
||||
created() {
|
||||
this.fetchUserRoles();
|
||||
$('#create-task-modal').on('hidden.bs.modal', () => {
|
||||
this.$emit('close');
|
||||
});
|
||||
|
||||
$('#create-task-modal').on('shown.bs.modal', this.focusInput);
|
||||
},
|
||||
unmounted() {
|
||||
$('#create-task-modal').off('shown.bs.modal', this.focusInput);
|
||||
},
|
||||
props: {
|
||||
projectsUrl: {
|
||||
|
|
@ -163,13 +178,23 @@ export default {
|
|||
if (option[0] > 0) {
|
||||
return option[1];
|
||||
}
|
||||
return this.i18n.t('dashboard.create_task_modal.new_project', { name: option[1] });
|
||||
return `
|
||||
<div class="flex items-center gap-2 truncate">
|
||||
<span class="sn-icon sn-icon-new-task"></span>
|
||||
<span class="truncate">${this.i18n.t('dashboard.create_task_modal.new_project', { name: option[1] })}</span
|
||||
</div>
|
||||
`;
|
||||
},
|
||||
newExperimentRenderer(option) {
|
||||
if (option[0] > 0) {
|
||||
return option[1];
|
||||
}
|
||||
return this.i18n.t('dashboard.create_task_modal.new_experiment', { name: option[1] });
|
||||
return `
|
||||
<div class="flex items-center gap-2 truncate">
|
||||
<span class="sn-icon sn-icon-new-task"></span>
|
||||
<span class="truncate">${this.i18n.t('dashboard.create_task_modal.new_experiment', { name: option[1] })}</span
|
||||
</div>
|
||||
`;
|
||||
},
|
||||
closeModal() {
|
||||
$('#create-task-modal').modal('hide');
|
||||
|
|
@ -184,6 +209,9 @@ export default {
|
|||
.then((response) => {
|
||||
this.userRoles = response.data.data;
|
||||
});
|
||||
},
|
||||
focusInput() {
|
||||
this.$refs.taskName.focus();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -348,6 +348,14 @@ export default {
|
|||
return acc;
|
||||
}, {});
|
||||
|
||||
this.steps = this.steps.map((step) => ({
|
||||
...step,
|
||||
attributes: {
|
||||
...step.attributes,
|
||||
collapsed: newState
|
||||
}
|
||||
}));
|
||||
|
||||
const settings = {
|
||||
key: 'task_step_states',
|
||||
data: updatedData
|
||||
|
|
|
|||
|
|
@ -92,7 +92,7 @@
|
|||
:noEmptyOption="false"
|
||||
:selectAppearance="'tag'"
|
||||
:viewMode="protocol.attributes.urls.update_protocol_keywords_url == null"
|
||||
:dataE2e="'protocolTemplates-protocolDetails-keywords'"
|
||||
:dataE2e="'e2e-IF-protocolTemplates-protocolDetails-keywords'"
|
||||
@dropdown:changed="updateKeywords"
|
||||
/>
|
||||
</span>
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@
|
|||
<div class="step-header">
|
||||
<div class="step-element-header" :class="{ 'no-hover': !urls.update_url }">
|
||||
<div class="flex items-center gap-4 py-0.5 border-0 border-y border-transparent border-solid">
|
||||
<a class="step-collapse-link hover:no-underline focus:no-underline"
|
||||
<a ref="toggleElement" class="step-collapse-link hover:no-underline focus:no-underline"
|
||||
:href="'#stepBody' + step.id"
|
||||
data-toggle="collapse"
|
||||
data-remote="true"
|
||||
|
|
@ -282,6 +282,14 @@
|
|||
if (this.activeDragStep != this.step.id && this.dragingFile) {
|
||||
this.dragingFile = false;
|
||||
}
|
||||
},
|
||||
step: {
|
||||
handler(newVal) {
|
||||
if (this.isCollapsed !== newVal.attributes.collapsed) {
|
||||
this.toggleCollapsed();
|
||||
}
|
||||
},
|
||||
deep: true
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
|
|
@ -461,6 +469,8 @@
|
|||
toggleCollapsed() {
|
||||
this.isCollapsed = !this.isCollapsed;
|
||||
|
||||
this.step.attributes.collapsed = this.isCollapsed;
|
||||
|
||||
const settings = {
|
||||
key: 'task_step_states',
|
||||
data: { [this.step.id]: this.isCollapsed }
|
||||
|
|
@ -590,6 +600,10 @@
|
|||
$.post(this.urls[`create_${elementType}_url`], { tableDimensions: tableDimensions, plateTemplate: plateTemplate }, (result) => {
|
||||
result.data.isNew = true;
|
||||
this.elements.push(result.data)
|
||||
|
||||
if (this.isCollapsed) {
|
||||
this.$refs.toggleElement.click();
|
||||
}
|
||||
this.$emit('stepUpdated')
|
||||
}).fail(() => {
|
||||
HelperModule.flashAlertMsg(this.i18n.t('errors.general'), 'danger');
|
||||
|
|
|
|||
|
|
@ -5,13 +5,12 @@
|
|||
:is="activeStep"
|
||||
:params="params"
|
||||
:key="modalId"
|
||||
:uploading="uploading"
|
||||
:loading="loading"
|
||||
@uploadFile="uploadFile"
|
||||
@generatePreview="generatePreview"
|
||||
@changeStep="changeStep"
|
||||
@importRows="importRecords"
|
||||
|
||||
@updateAutoMapping="updateAutoMapping"
|
||||
/>
|
||||
<ExportModal
|
||||
v-else
|
||||
|
|
@ -50,8 +49,7 @@ export default {
|
|||
return {
|
||||
modalOpened: false,
|
||||
activeStep: 'UploadStep',
|
||||
uploading: false,
|
||||
params: {},
|
||||
params: { autoMapping: true },
|
||||
modalId: null,
|
||||
loading: false
|
||||
};
|
||||
|
|
@ -62,35 +60,27 @@ export default {
|
|||
methods: {
|
||||
open() {
|
||||
this.activeStep = 'UploadStep';
|
||||
this.params.selectedItems = null;
|
||||
this.params.autoMapping = true;
|
||||
this.fetchRepository();
|
||||
},
|
||||
fetchRepository() {
|
||||
axios.get(this.repositoryUrl)
|
||||
.then((response) => {
|
||||
this.params = response.data.data;
|
||||
this.params = { ...this.params, ...response.data.data };
|
||||
this.modalId = Math.random().toString(36);
|
||||
this.modalOpened = true;
|
||||
});
|
||||
},
|
||||
uploadFile(file) {
|
||||
this.uploading = true;
|
||||
const formData = new FormData();
|
||||
|
||||
// required payload
|
||||
formData.append('file', file);
|
||||
|
||||
axios.post(this.params.attributes.urls.parse_sheet, formData, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' }
|
||||
})
|
||||
.then((response) => {
|
||||
this.params = { ...this.params, ...response.data, file_name: file.name };
|
||||
this.activeStep = 'MappingStep';
|
||||
this.uploading = false;
|
||||
});
|
||||
uploadFile(params) {
|
||||
this.params = { ...this.params, ...params };
|
||||
this.activeStep = 'MappingStep';
|
||||
},
|
||||
|
||||
generatePreview(mappings, updateWithEmptyCells, onlyAddNewItems) {
|
||||
this.params.mapping = mappings;
|
||||
updateAutoMapping(value) {
|
||||
this.params.autoMapping = value;
|
||||
},
|
||||
generatePreview(selectedItems, updateWithEmptyCells, onlyAddNewItems) {
|
||||
this.params.selectedItems = selectedItems;
|
||||
this.params.updateWithEmptyCells = updateWithEmptyCells;
|
||||
this.params.onlyAddNewItems = onlyAddNewItems;
|
||||
this.importRecords(true);
|
||||
|
|
@ -98,6 +88,12 @@ export default {
|
|||
changeStep(step) {
|
||||
this.activeStep = step;
|
||||
},
|
||||
generateMapping() {
|
||||
return this.params.selectedItems.reduce((obj, item) => {
|
||||
obj[item.index] = item.key || '';
|
||||
return obj;
|
||||
}, {});
|
||||
},
|
||||
importRecords(preview) {
|
||||
if (this.loading) {
|
||||
return;
|
||||
|
|
@ -105,7 +101,7 @@ export default {
|
|||
|
||||
const jsonData = {
|
||||
file_id: this.params.temp_file.id,
|
||||
mappings: this.params.mapping,
|
||||
mappings: this.generateMapping(),
|
||||
id: this.params.id,
|
||||
preview: preview,
|
||||
should_overwrite_with_empty_cells: this.params.updateWithEmptyCells,
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@
|
|||
<div class="flex gap-6 items-center my-6">
|
||||
<div class="flex items-center gap-2" :title="i18n.t('repositories.import_records.steps.step2.autoMappingTooltip')">
|
||||
<div class="sci-checkbox-container">
|
||||
<input type="checkbox" class="sci-checkbox" v-model="autoMapping" />
|
||||
<input type="checkbox" class="sci-checkbox" @change="$emit('update-auto-mapping', $event.target.checked)" :checked="params.autoMapping" />
|
||||
<span class="sci-checkbox-label"></span>
|
||||
</div>
|
||||
{{ i18n.t('repositories.import_records.steps.step2.autoMappingText') }}
|
||||
|
|
@ -44,7 +44,7 @@
|
|||
|
||||
{{ 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 class="grid grid-cols-[3rem_14.5rem_1.5rem_14.5rem_5rem_14.5rem] px-2">
|
||||
|
||||
<div v-for="(column, key) in columnLabels" class="flex items-center px-2 py-2 font-bold">{{ column }}</div>
|
||||
|
||||
|
|
@ -56,7 +56,7 @@
|
|||
:params="params"
|
||||
:value="this.selectedItems.find((item) => item.index === index)"
|
||||
@selection:changed="handleChange"
|
||||
:autoMapping="this.autoMapping"
|
||||
:autoMapping="params.autoMapping"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
|
|
@ -80,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" @click="importRecords">
|
||||
<button class="btn btn-primary" :disabled="!canSubmit" @click="importRecords">
|
||||
{{ i18n.t('repositories.import_records.steps.step2.confirmBtnText') }}
|
||||
</button>
|
||||
</div>
|
||||
|
|
@ -98,7 +98,7 @@ import Loading from '../../../shared/loading.vue';
|
|||
|
||||
export default {
|
||||
name: 'MappingStep',
|
||||
emits: ['close', 'generatePreview'],
|
||||
emits: ['close', 'generatePreview', 'updateAutoMapping'],
|
||||
mixins: [modalMixin],
|
||||
components: {
|
||||
SelectDropdown,
|
||||
|
|
@ -117,7 +117,6 @@ export default {
|
|||
},
|
||||
data() {
|
||||
return {
|
||||
autoMapping: false,
|
||||
updateWithEmptyCells: false,
|
||||
onlyAddNewItems: false,
|
||||
columnLabels: {
|
||||
|
|
@ -146,7 +145,7 @@ export default {
|
|||
const item = this.selectedItems.find((i) => i.index === index);
|
||||
const usedBeforeItem = this.selectedItems.find((i) => i.key === key && i.index !== index);
|
||||
|
||||
if (usedBeforeItem) {
|
||||
if (usedBeforeItem && usedBeforeItem.key !== 'do_not_import') {
|
||||
usedBeforeItem.key = null;
|
||||
usedBeforeItem.value = null;
|
||||
}
|
||||
|
|
@ -154,11 +153,32 @@ export default {
|
|||
item.key = key;
|
||||
item.value = value;
|
||||
},
|
||||
generateMapping() {
|
||||
return this.selectedItems.reduce((obj, item) => {
|
||||
obj[item.index] = item.key || '';
|
||||
return obj;
|
||||
}, {});
|
||||
loadAvailableFields() {
|
||||
// Adding alreadySelected attribute for tracking
|
||||
this.availableFields = [];
|
||||
|
||||
Object.entries(this.params.import_data.available_fields).forEach(([key, value]) => {
|
||||
let columnTypeName = '';
|
||||
if (key === '-1') {
|
||||
columnTypeName = this.i18n.t('repositories.import_records.steps.step2.computedDropdownOptions.name');
|
||||
} else if (key === '0') {
|
||||
columnTypeName = this.i18n.t('repositories.import_records.steps.step2.computedDropdownOptions.id');
|
||||
} else {
|
||||
const column = this.repositoryColumns.find((el) => el[0] === parseInt(key, 10));
|
||||
columnTypeName = this.i18n.t(`repositories.import_records.steps.step2.computedDropdownOptions.${column[2]}`);
|
||||
}
|
||||
const field = {
|
||||
key, value, alreadySelected: false, typeName: columnTypeName
|
||||
};
|
||||
this.availableFields.push(field);
|
||||
});
|
||||
},
|
||||
loadSelectedItems() {
|
||||
if (this.params.selectedItems) {
|
||||
this.selectedItems = this.params.selectedItems;
|
||||
} else {
|
||||
this.selectedItems = this.params.import_data.header.map((item, index) => ({ index, key: null, value: null }));
|
||||
}
|
||||
},
|
||||
importRecords() {
|
||||
if (!this.selectedItems.find((item) => item.key === '-1')) {
|
||||
|
|
@ -168,48 +188,33 @@ export default {
|
|||
|
||||
this.$emit(
|
||||
'generatePreview',
|
||||
this.generateMapping()
|
||||
this.selectedItems.filter((item) => item.key !== 'do_not_import')
|
||||
);
|
||||
return true;
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
computedDropdownOptions() {
|
||||
return this.availableFields
|
||||
.map((el) => [String(el.key), `${String(el.value)} (${el.typeName})`]);
|
||||
let options = this.availableFields.map((el) => [String(el.key), `${String(el.value)} (${el.typeName})`]);
|
||||
options = [
|
||||
['do_not_import', this.i18n.t('repositories.import_records.steps.step2.table.tableRow.placeholders.doNotImport')]
|
||||
].concat(options);
|
||||
// options = [['new', this.i18n.t('repositories.import_records.steps.step2.table.tableRow.importAsNewColumn')]].concat(options);
|
||||
return options;
|
||||
},
|
||||
computedImportedIgnoredInfo() {
|
||||
const importedSum = this.selectedItems.filter((i) => i.key).length;
|
||||
const importedSum = this.selectedItems.filter((i) => i.key && i.key !== 'do_not_import').length;
|
||||
const ignoredSum = this.selectedItems.length - importedSum;
|
||||
return { importedSum, ignoredSum };
|
||||
},
|
||||
canSubmit() {
|
||||
return this.selectedItems.filter((i) => i.key && i.key !== 'do_not_import').length > 0;
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.repositoryColumns = this.params.attributes.repository_columns;
|
||||
// Adding alreadySelected attribute for tracking
|
||||
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]) => {
|
||||
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);
|
||||
});
|
||||
},
|
||||
mounted() {
|
||||
this.autoMapping = true;
|
||||
this.loadAvailableFields();
|
||||
this.loadSelectedItems();
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -20,15 +20,13 @@
|
|||
<SelectDropdown
|
||||
:options="dropdownOptions"
|
||||
@change="changeSelected"
|
||||
:clearable="true"
|
||||
:size="'sm'"
|
||||
class="max-w-96"
|
||||
:searchable="true"
|
||||
:class="{
|
||||
'outline-sn-alert-brittlebush outline-1 outline rounded': computeMatchNotFound
|
||||
}"
|
||||
:placeholder="computeMatchNotFound ?
|
||||
i18n.t('repositories.import_records.steps.step2.table.tableRow.placeholders.matchNotFound') :
|
||||
i18n.t('repositories.import_records.steps.step2.table.tableRow.placeholders.doNotImport')"
|
||||
:placeholder="i18n.t('repositories.import_records.steps.step2.table.tableRow.placeholders.matchNotFound')"
|
||||
:title="this.selectedColumnType?.value"
|
||||
:value="this.selectedColumnType?.key"
|
||||
></SelectDropdown>
|
||||
|
|
@ -136,16 +134,16 @@ export default {
|
|||
return this.autoMapping && !this.isSystemColumn(this.item) && ((this.selectedColumnType && !this.selectedColumnType.key) || !this.selectedColumnType);
|
||||
},
|
||||
selected() {
|
||||
return !!this.value?.key;
|
||||
return this.columnMapped;
|
||||
},
|
||||
differentMapingName() {
|
||||
return this.columnMapped && this.selectedColumnType?.value !== this.item;
|
||||
return this.columnMapped && this.selectedColumnType?.value !== this.item && this.value?.key !== 'do_not_import';
|
||||
},
|
||||
matchNotFound() {
|
||||
return this.autoMapping && !this.selectedColumnType?.key;
|
||||
return this.autoMapping && this.columnMapped;
|
||||
},
|
||||
columnMapped() {
|
||||
return this.selectedColumnType?.key;
|
||||
return this.selectedColumnType?.key && this.selectedColumnType?.key !== 'do_not_import';
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
|
|
@ -153,6 +151,7 @@ export default {
|
|||
return this.systemColumns.includes(column);
|
||||
},
|
||||
autoMap() {
|
||||
this.changeSelected(null);
|
||||
Object.entries(this.params.import_data.available_fields).forEach(([key, value]) => {
|
||||
if (this.item === value) {
|
||||
this.changeSelected(key);
|
||||
|
|
@ -160,7 +159,7 @@ export default {
|
|||
});
|
||||
},
|
||||
clearAutoMap() {
|
||||
this.changeSelected(null);
|
||||
this.changeSelected('do_not_import');
|
||||
},
|
||||
changeSelected(e) {
|
||||
const value = this.params.import_data.available_fields[e];
|
||||
|
|
@ -171,6 +170,8 @@ export default {
|
|||
mounted() {
|
||||
if (this.autoMapping) {
|
||||
this.autoMap();
|
||||
} else {
|
||||
this.selectedColumnType = this.value;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -72,7 +72,7 @@
|
|||
{{ i18n.t('general.back') }}
|
||||
</button>
|
||||
<button type="button" class="btn btn-primary" @click="$emit('importRows')">
|
||||
{{ i18n.t('repositories.import_records.steps.step3.confirm') }}
|
||||
{{ i18n.t('repositories.import_records.steps.step3.import') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -121,18 +121,21 @@ export default {
|
|||
const columns = [
|
||||
{
|
||||
field: 'code',
|
||||
headerName: this.i18n.t('repositories.import_records.steps.step3.code')
|
||||
headerName: this.i18n.t('repositories.import_records.steps.step3.code'),
|
||||
cellRenderer: this.highlightRenderer
|
||||
},
|
||||
{
|
||||
field: 'name',
|
||||
headerName: this.i18n.t('repositories.import_records.steps.step3.name')
|
||||
headerName: this.i18n.t('repositories.import_records.steps.step3.name'),
|
||||
cellRenderer: this.highlightRenderer
|
||||
}
|
||||
];
|
||||
|
||||
this.params.attributes.repository_columns.forEach((col) => {
|
||||
columns.push({
|
||||
field: `col_${col[0]}`,
|
||||
headerName: col[1]
|
||||
headerName: col[1],
|
||||
cellRenderer: this.highlightRenderer
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -163,6 +166,19 @@ export default {
|
|||
filterRows(status) {
|
||||
return this.params.preview.data.filter((r) => r.attributes.import_status === status);
|
||||
},
|
||||
highlightRenderer(params) {
|
||||
const { import_status: importStatus } = params.data;
|
||||
|
||||
let color = '';
|
||||
|
||||
if (importStatus === 'created' || importStatus === 'updated') {
|
||||
color = 'text-sn-alert-green';
|
||||
} else if (importStatus === 'duplicated' || importStatus === 'invalid') {
|
||||
color = 'text-sn-alert-passion';
|
||||
}
|
||||
|
||||
return `<span class="${color}">${params.value || ''}</span>`;
|
||||
},
|
||||
statusRenderer(params) {
|
||||
const { import_status: importStatus, import_message: importMessage } = params.data;
|
||||
|
||||
|
|
@ -188,9 +204,9 @@ export default {
|
|||
}
|
||||
|
||||
return `
|
||||
<div class="flex items-center ${color} gap-2.5">
|
||||
<div title="${message}" class="flex items-center ${color} gap-2.5">
|
||||
<i class="sn-icon sn-icon-${icon} "></i>
|
||||
<span>${message}</span>
|
||||
<span class="truncate">${message}</span>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -87,6 +87,7 @@
|
|||
<script>
|
||||
import DragAndDropUpload from '../../../shared/drag_and_drop_upload.vue';
|
||||
import modalMixin from '../../../shared/modal_mixin';
|
||||
import axios from '../../../../packs/custom_axios';
|
||||
|
||||
export default {
|
||||
name: 'UploadStep',
|
||||
|
|
@ -110,11 +111,23 @@ export default {
|
|||
};
|
||||
},
|
||||
methods: {
|
||||
uploadFile(file) {
|
||||
this.$emit('uploadFile', file);
|
||||
},
|
||||
handleError(error) {
|
||||
this.error = error;
|
||||
},
|
||||
uploadFile(file) {
|
||||
const formData = new FormData();
|
||||
|
||||
// required payload
|
||||
formData.append('file', file);
|
||||
|
||||
axios.post(this.params.attributes.urls.parse_sheet, formData, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' }
|
||||
})
|
||||
.then((response) => {
|
||||
this.$emit('uploadFile', { ...this.params, ...response.data, file_name: file.name });
|
||||
}).catch((error) => {
|
||||
this.handleError(error.response.data.error);
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@
|
|||
@dropdown:changed="updateOperator"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="users" class="users-filter-dropdown">
|
||||
<div v-if="users" class="users-filter-dropdown max-w-[360px]">
|
||||
<DropdownSelector
|
||||
:optionClass="'checkbox-icon'"
|
||||
:dataCombineTags="true"
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@
|
|||
<div :class="{ 'opacity-0 pointer-events-none': dragingFile }">
|
||||
<div class="result-header flex justify-between">
|
||||
<div class="result-head-left flex items-start flex-grow gap-4">
|
||||
<a class="result-collapse-link hover:no-underline focus:no-underline py-0.5 border-0 border-y border-transparent border-solid text-sn-black"
|
||||
<a ref="toggleElement" class="result-collapse-link hover:no-underline focus:no-underline py-0.5 border-0 border-y border-transparent border-solid text-sn-black"
|
||||
:href="'#resultBody' + result.id"
|
||||
data-toggle="collapse"
|
||||
data-remote="true"
|
||||
|
|
@ -205,6 +205,14 @@ export default {
|
|||
if (this.activeDragResult !== this.result.id && this.dragingFile) {
|
||||
this.dragingFile = false;
|
||||
}
|
||||
},
|
||||
result: {
|
||||
handler(newVal) {
|
||||
if (this.isCollapsed !== newVal.attributes.collapsed) {
|
||||
this.toggleCollapsed();
|
||||
}
|
||||
},
|
||||
deep: true
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
|
|
@ -312,6 +320,7 @@ export default {
|
|||
methods: {
|
||||
toggleCollapsed() {
|
||||
this.isCollapsed = !this.isCollapsed;
|
||||
this.result.attributes.collapsed = this.isCollapsed;
|
||||
},
|
||||
dragEnter(e) {
|
||||
if (!this.urls.upload_attachment_url) return;
|
||||
|
|
@ -440,6 +449,10 @@ export default {
|
|||
$.post(this.urls[`create_${elementType}_url`], { tableDimensions, plateTemplate }, (result) => {
|
||||
result.data.isNew = true;
|
||||
this.elements.push(result.data);
|
||||
|
||||
if (this.isCollapsed) {
|
||||
this.$refs.toggleElement.click();
|
||||
}
|
||||
this.$emit('resultUpdated');
|
||||
}).fail(() => {
|
||||
HelperModule.flashAlertMsg(this.i18n.t('errors.general'), 'danger');
|
||||
|
|
|
|||
|
|
@ -141,9 +141,20 @@ export default {
|
|||
},
|
||||
expandAll() {
|
||||
$('.result-wrapper .collapse').collapse('show');
|
||||
this.toggleCollapsed(false);
|
||||
},
|
||||
collapseAll() {
|
||||
$('.result-wrapper .collapse').collapse('hide');
|
||||
this.toggleCollapsed(true);
|
||||
},
|
||||
toggleCollapsed(newState) {
|
||||
this.results = this.results.map((result) => ({
|
||||
...result,
|
||||
attributes: {
|
||||
...result.attributes,
|
||||
collapsed: newState
|
||||
}
|
||||
}));
|
||||
},
|
||||
removeResult(result_id) {
|
||||
this.results = this.results.filter((r) => r.id != result_id);
|
||||
|
|
|
|||
|
|
@ -123,10 +123,10 @@ export default {
|
|||
this.$emit('setFilters', filters);
|
||||
},
|
||||
collapseResults() {
|
||||
$('.result-wrapper .collapse').collapse('hide');
|
||||
this.$emit('collapseAll');
|
||||
},
|
||||
expandResults() {
|
||||
$('.result-wrapper .collapse').collapse('show');
|
||||
this.$emit('expandAll');
|
||||
},
|
||||
scrollTop() {
|
||||
window.scrollTo(0, 0);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,84 @@
|
|||
<template>
|
||||
<div class="flex items-center gap-1.5 justify-end w-[184px]">
|
||||
<OpenMenu
|
||||
:attachment="attachment"
|
||||
:multipleOpenOptions="multipleOpenOptions"
|
||||
@open="$emit('attachment:toggle_menu', $event)"
|
||||
@close="$emit('attachment:toggle_menu', $event)"
|
||||
@option:click="$emit('attachment:open', $event)"
|
||||
/>
|
||||
<a v-if="attachment.attributes.urls.move"
|
||||
@click.prevent.stop="$emit('attachment:move_modal')"
|
||||
class="btn btn-light icon-btn thumbnail-action-btn"
|
||||
:title="i18n.t('attachments.thumbnail.buttons.move')">
|
||||
<i class="sn-icon sn-icon-move"></i>
|
||||
</a>
|
||||
<a class="btn btn-light icon-btn thumbnail-action-btn"
|
||||
:title="i18n.t('attachments.thumbnail.buttons.download')"
|
||||
:href="attachment.attributes.urls.download" data-turbolinks="false">
|
||||
<i class="sn-icon sn-icon-export"></i>
|
||||
</a>
|
||||
<ContextMenu
|
||||
:attachment="attachment"
|
||||
@attachment:viewMode="$emit('attachment:viewMode', $event)"
|
||||
@attachment:delete="$emit('attachment:delete', $event)"
|
||||
@attachment:moved="$emit('attachment:moved', $event)"
|
||||
@attachment:uploaded="$emit('attachment:uploaded', $event)"
|
||||
@attachment:changed="$emit('attachment:changed', $event)"
|
||||
@attachment:update="$emit('attachment:update', $event)"
|
||||
@menu-toggle="$emit('attachment:toggle_menu', $event)"
|
||||
:withBorder="withBorder"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import OpenLocallyMixin from './mixins/open_locally.js';
|
||||
import OpenMenu from './open_menu.vue';
|
||||
import ContextMenu from './context_menu.vue';
|
||||
|
||||
export default {
|
||||
name: 'attachmentActions',
|
||||
props: {
|
||||
attachment: Object,
|
||||
withBorder: false
|
||||
},
|
||||
mixins: [OpenLocallyMixin],
|
||||
components: {
|
||||
OpenMenu,
|
||||
ContextMenu
|
||||
},
|
||||
computed: {
|
||||
multipleOpenOptions() {
|
||||
const options = [];
|
||||
if (this.attachment.attributes.wopi && this.attachment.attributes.urls.edit_asset) {
|
||||
options.push({
|
||||
text: this.attachment.attributes.wopi_context.button_text,
|
||||
url: this.attachment.attributes.urls.edit_asset,
|
||||
url_target: '_blank'
|
||||
});
|
||||
}
|
||||
if (this.attachment.attributes.asset_type !== 'marvinjs'
|
||||
&& this.attachment.attributes.image_editable
|
||||
&& this.attachment.attributes.urls.start_edit_image) {
|
||||
options.push({
|
||||
text: this.i18n.t('assets.file_preview.edit_in_scinote'),
|
||||
emit: 'open_scinote_editor'
|
||||
});
|
||||
}
|
||||
if (this.canOpenLocally) {
|
||||
const text = this.localAppName
|
||||
? this.i18n.t('attachments.open_locally_in', { application: this.localAppName })
|
||||
: this.i18n.t('attachments.open_locally');
|
||||
|
||||
options.push({
|
||||
text,
|
||||
emit: 'open_locally',
|
||||
data_e2e: 'e2e-BT-attachmentOptions-openLocally'
|
||||
});
|
||||
}
|
||||
return options;
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
|
@ -118,80 +118,6 @@ export default {
|
|||
computed: {
|
||||
menu() {
|
||||
const menu = [];
|
||||
if (this.displayInDropdown.includes('edit')) {
|
||||
if (this.attachment.attributes.wopi && this.attachment.attributes.urls.edit_asset) {
|
||||
menu.push({
|
||||
text: this.attachment.attributes.wopi_context.button_text,
|
||||
url: this.attachment.attributes.urls.edit_asset,
|
||||
url_target: '_blank'
|
||||
});
|
||||
}
|
||||
if (this.attachment.attributes.asset_type === 'gene_sequence' && this.attachment.attributes.urls.open_vector_editor_edit) {
|
||||
menu.push({
|
||||
text: this.i18n.t('open_vector_editor.edit_sequence'),
|
||||
emit: 'open_ove_editor'
|
||||
});
|
||||
}
|
||||
if (this.attachment.attributes.asset_type === 'marvinjs' && this.attachment.attributes.urls.marvin_js_start_edit) {
|
||||
menu.push({
|
||||
text: this.i18n.t('assets.file_preview.edit_in_marvinjs'),
|
||||
emit: 'open_marvinjs_editor'
|
||||
});
|
||||
}
|
||||
if (this.attachment.attributes.asset_type !== 'marvinjs'
|
||||
&& this.attachment.attributes.image_editable
|
||||
&& this.attachment.attributes.urls.start_edit_image) {
|
||||
menu.push({
|
||||
text: this.i18n.t('assets.file_preview.edit_in_scinote'),
|
||||
emit: 'open_scinote_editor'
|
||||
});
|
||||
}
|
||||
if (this.canOpenLocally) {
|
||||
const text = this.localAppName
|
||||
? this.i18n.t('attachments.open_locally_in', { application: this.localAppName })
|
||||
: this.i18n.t('attachments.open_locally');
|
||||
|
||||
menu.push({
|
||||
text,
|
||||
emit: 'open_locally',
|
||||
data_e2e: 'e2e-BT-attachmentOptions-openLocally'
|
||||
});
|
||||
}
|
||||
}
|
||||
if (this.attachment.attributes.asset_type === 'gene_sequence' && this.attachment.attributes.urls.open_vector_editor_edit) {
|
||||
menu.push({
|
||||
text: this.i18n.t('open_vector_editor.edit_sequence'),
|
||||
emit: 'open_ove_editor',
|
||||
data_e2e: 'e2e-BT-attachmentOptions-openInOve'
|
||||
});
|
||||
}
|
||||
if (this.attachment.attributes.asset_type === 'marvinjs' && this.attachment.attributes.urls.marvin_js_start_edit) {
|
||||
menu.push({
|
||||
text: this.i18n.t('assets.file_preview.edit_in_marvinjs'),
|
||||
emit: 'open_marvinjs_editor',
|
||||
data_e2e: 'e2e-BT-attachmentOptions-openInMarvin'
|
||||
});
|
||||
}
|
||||
if (this.attachment.attributes.asset_type !== 'marvinjs'
|
||||
&& this.attachment.attributes.image_editable
|
||||
&& this.attachment.attributes.urls.start_edit_image) {
|
||||
menu.push({
|
||||
text: this.i18n.t('assets.file_preview.edit_in_scinote'),
|
||||
emit: 'open_scinote_editor',
|
||||
data_e2e: 'e2e-BT-attachmentOptions-openInImageEditor'
|
||||
});
|
||||
}
|
||||
if (this.canOpenLocally) {
|
||||
const text = this.localAppName
|
||||
? this.i18n.t('attachments.open_locally_in', { application: this.localAppName })
|
||||
: this.i18n.t('attachments.open_locally');
|
||||
|
||||
menu.push({
|
||||
text,
|
||||
emit: 'open_locally',
|
||||
data_e2e: 'e2e-BT-attachmentOptions-openLocally'
|
||||
});
|
||||
}
|
||||
if (this.displayInDropdown.includes('download')) {
|
||||
menu.push({
|
||||
text: this.i18n.t('Download'),
|
||||
|
|
@ -200,13 +126,6 @@ export default {
|
|||
data_e2e: 'e2e-BT-attachmentOptions-download'
|
||||
});
|
||||
}
|
||||
if (this.attachment.attributes.urls.move_targets) {
|
||||
menu.push({
|
||||
text: this.i18n.t('assets.context_menu.move'),
|
||||
emit: 'move',
|
||||
data_e2e: 'e2e-BT-attachmentOptions-move'
|
||||
});
|
||||
}
|
||||
if (this.attachment.attributes.urls.duplicate) {
|
||||
menu.push({
|
||||
text: this.i18n.t('assets.context_menu.duplicate'),
|
||||
|
|
|
|||
|
|
@ -32,31 +32,18 @@
|
|||
</div>
|
||||
</div>
|
||||
<div class="flex items-center ml-auto gap-2">
|
||||
<openMenu
|
||||
:attachment="attachment"
|
||||
:multipleOpenOptions="multipleOpenOptions"
|
||||
@open="toggleMenuDropdown"
|
||||
@close="toggleMenuDropdown"
|
||||
@option:click="$emit($event)"
|
||||
/>
|
||||
<a v-if="attachment.attributes.urls.move"
|
||||
@click.prevent.stop="showMoveModal"
|
||||
class="btn btn-light icon-btn thumbnail-action-btn"
|
||||
:title="i18n.t('attachments.thumbnail.buttons.move')">
|
||||
<i class="sn-icon sn-icon-move"></i>
|
||||
</a>
|
||||
<a class="btn btn-light icon-btn thumbnail-action-btn"
|
||||
:title="i18n.t('attachments.thumbnail.buttons.download')"
|
||||
:href="attachment.attributes.urls.download" data-turbolinks="false">
|
||||
<i class="sn-icon sn-icon-export"></i>
|
||||
</a>
|
||||
<ContextMenu
|
||||
<AttachmentActions
|
||||
:attachment="attachment"
|
||||
:showOptions="showOptions"
|
||||
@attachment:viewMode="updateViewMode"
|
||||
@attachment:delete="deleteAttachment"
|
||||
@attachment:moved="attachmentMoved"
|
||||
@attachment:uploaded="reloadAttachments"
|
||||
@attachment:changed="$emit('attachment:changed', $event)"
|
||||
@attachment:update="$emit('attachment:update', $event)"
|
||||
@attachment:toggle_menu="toggleMenuDropdown"
|
||||
@attachment:move_modal="showMoveModal"
|
||||
@attachment:open="$emit($event)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -107,6 +94,7 @@ import PdfViewer from '../../pdf_viewer.vue';
|
|||
import MoveAssetModal from '../modal/move.vue';
|
||||
import MoveMixin from './mixins/move.js';
|
||||
import OpenLocallyMixin from './mixins/open_locally.js';
|
||||
import AttachmentActions from './attachment_actions.vue';
|
||||
import OpenMenu from './open_menu.vue';
|
||||
|
||||
export default {
|
||||
|
|
@ -116,7 +104,8 @@ export default {
|
|||
ContextMenu,
|
||||
PdfViewer,
|
||||
MoveAssetModal,
|
||||
OpenMenu
|
||||
OpenMenu,
|
||||
AttachmentActions
|
||||
},
|
||||
props: {
|
||||
attachment: {
|
||||
|
|
|
|||
|
|
@ -21,42 +21,29 @@
|
|||
<img :src="this.imageLoadError ? attachment.attributes.urls.blob : attachment.attributes.medium_preview" @error="ActiveStoragePreviews.reCheckPreview"
|
||||
@load="ActiveStoragePreviews.showPreview"/>
|
||||
</div>
|
||||
<div class="file-metadata">
|
||||
<span>
|
||||
<div class="flex items-center gap-2 text-xs text-sn-grey overflow-hidden ml-auto">
|
||||
<span class="truncate" :title="i18n.t('assets.placeholder.modified_label') + ' ' + attachment.attributes.updated_at_formatted">
|
||||
{{ i18n.t('assets.placeholder.modified_label') }}
|
||||
{{ attachment.attributes.updated_at_formatted }}
|
||||
</span>
|
||||
<span>
|
||||
<span class="truncate" :title="i18n.t('assets.placeholder.size_label', {size: attachment.attributes.file_size_formatted})">
|
||||
{{ i18n.t('assets.placeholder.size_label', {size: attachment.attributes.file_size_formatted}) }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="attachment-actions shrink-0 ml-4">
|
||||
<openMenu
|
||||
:attachment="attachment"
|
||||
:multipleOpenOptions="multipleOpenOptions"
|
||||
@open="toggleMenuDropdown"
|
||||
@close="toggleMenuDropdown"
|
||||
@option:click="$emit($event)"
|
||||
/>
|
||||
<a v-if="attachment.attributes.urls.move"
|
||||
@click.prevent.stop="showMoveModal"
|
||||
class="btn btn-light icon-btn thumbnail-action-btn"
|
||||
:title="i18n.t('attachments.thumbnail.buttons.move')">
|
||||
<i class="sn-icon sn-icon-move"></i>
|
||||
</a>
|
||||
<a class="btn btn-light icon-btn thumbnail-action-btn"
|
||||
:title="i18n.t('attachments.thumbnail.buttons.download')"
|
||||
:href="attachment.attributes.urls.download" data-turbolinks="false">
|
||||
<i class="sn-icon sn-icon-export"></i>
|
||||
</a>
|
||||
<ContextMenu
|
||||
:attachment="attachment"
|
||||
@attachment:viewMode="updateViewMode"
|
||||
@attachment:delete="deleteAttachment"
|
||||
@attachment:moved="attachmentMoved"
|
||||
@attachment:uploaded="reloadAttachments"
|
||||
@attachment:update="$emit('attachment:update', $event)"
|
||||
/>
|
||||
<div class="attachment-actions shrink-0 ml-auto">
|
||||
<AttachmentActions
|
||||
:attachment="attachment"
|
||||
:showOptions="showOptions"
|
||||
@attachment:viewMode="updateViewMode"
|
||||
@attachment:delete="deleteAttachment"
|
||||
@attachment:moved="attachmentMoved"
|
||||
@attachment:uploaded="reloadAttachments"
|
||||
@attachment:changed="$emit('attachment:changed', $event)"
|
||||
@attachment:update="$emit('attachment:update', $event)"
|
||||
@attachment:toggle_menu="toggleMenuDropdown"
|
||||
@attachment:move_modal="showMoveModal"
|
||||
@attachment:open="$emit($event)"
|
||||
/>
|
||||
</div>
|
||||
<Teleport to="body">
|
||||
<moveAssetModal
|
||||
|
|
@ -75,6 +62,7 @@ import ContextMenuMixin from './mixins/context_menu.js';
|
|||
import ContextMenu from './context_menu.vue';
|
||||
import MoveMixin from './mixins/move.js';
|
||||
import MoveAssetModal from '../modal/move.vue';
|
||||
import AttachmentActions from './attachment_actions.vue';
|
||||
import OpenMenu from './open_menu.vue';
|
||||
|
||||
export default {
|
||||
|
|
@ -83,7 +71,8 @@ export default {
|
|||
components: {
|
||||
ContextMenu,
|
||||
MoveAssetModal,
|
||||
OpenMenu
|
||||
OpenMenu,
|
||||
AttachmentActions
|
||||
},
|
||||
props: {
|
||||
attachment: {
|
||||
|
|
|
|||
|
|
@ -14,6 +14,9 @@ export default {
|
|||
methods: {
|
||||
openOVEditor() {
|
||||
window.showIFrameModal(this.OVEurl);
|
||||
if (this.isCollapsed) {
|
||||
this.$refs.toggleElement.click();
|
||||
}
|
||||
},
|
||||
initOVE() {
|
||||
$(window.iFrameModal).on('sequenceSaved', () => {
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@
|
|||
<div :class="{ hidden: !showOptions }" class="hovered-thumbnail h-full">
|
||||
<a
|
||||
:href="attachment.attributes.urls.blob"
|
||||
class="file-preview-link file-name"
|
||||
class="file-preview-link file-name max-h-36 overflow-auto"
|
||||
:id="`modal_link${attachment.id}`"
|
||||
data-no-turbolink="true"
|
||||
:data-id="attachment.id"
|
||||
|
|
@ -45,38 +45,20 @@
|
|||
<div class="absolute bottom-16 text-sn-grey">
|
||||
{{ attachment.attributes.file_size_formatted }}
|
||||
</div>
|
||||
<div class="absolute bottom-4 w-[184px] grid grid-cols-[repeat(4,_2.5rem)] justify-between">
|
||||
<openMenu
|
||||
<div class="absolute bottom-4">
|
||||
<AttachmentActions
|
||||
:withBorder="true"
|
||||
:attachment="attachment"
|
||||
:multipleOpenOptions="multipleOpenOptions"
|
||||
@open="toggleMenu"
|
||||
@close="toggleMenu"
|
||||
@option:click="$emit($event)"
|
||||
/>
|
||||
<a v-if="attachment.attributes.urls.move"
|
||||
@click.prevent.stop="showMoveModal"
|
||||
class="btn btn-light icon-btn thumbnail-action-btn"
|
||||
:title="i18n.t('attachments.thumbnail.buttons.move')">
|
||||
<i class="sn-icon sn-icon-move"></i>
|
||||
</a>
|
||||
<a class="btn btn-light icon-btn thumbnail-action-btn"
|
||||
:title="i18n.t('attachments.thumbnail.buttons.download')"
|
||||
:href="attachment.attributes.urls.download" data-turbolinks="false">
|
||||
<i class="sn-icon sn-icon-export"></i>
|
||||
</a>
|
||||
<ContextMenu
|
||||
class="!relative !top-0 !left-0"
|
||||
v-show="showOptions"
|
||||
:attachment="attachment"
|
||||
:hideEdit="true"
|
||||
:showOptions="showOptions"
|
||||
@attachment:viewMode="updateViewMode"
|
||||
@attachment:delete="deleteAttachment"
|
||||
@attachment:moved="attachmentMoved"
|
||||
@attachment:uploaded="reloadAttachments"
|
||||
@attachment:changed="$emit('attachment:changed', $event)"
|
||||
@attachment:update="$emit('attachment:update', $event)"
|
||||
@menu-toggle="toggleMenu"
|
||||
:withBorder="true"
|
||||
@attachment:toggle_menu="toggleMenu"
|
||||
@attachment:move_modal="showMoveModal"
|
||||
@attachment:open="$emit($event)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -134,6 +116,7 @@ import MoveAssetModal from '../modal/move.vue';
|
|||
import MoveMixin from './mixins/move.js';
|
||||
import OpenLocallyMixin from './mixins/open_locally.js';
|
||||
import OpenMenu from './open_menu.vue';
|
||||
import AttachmentActions from './attachment_actions.vue';
|
||||
import { vOnClickOutside } from '@vueuse/components';
|
||||
|
||||
export default {
|
||||
|
|
@ -144,7 +127,8 @@ export default {
|
|||
deleteAttachmentModal,
|
||||
MoveAssetModal,
|
||||
MenuDropdown,
|
||||
OpenMenu
|
||||
OpenMenu,
|
||||
AttachmentActions
|
||||
},
|
||||
props: {
|
||||
attachment: {
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ export default {
|
|||
},
|
||||
loadFromComputer() {
|
||||
this.uploadFiles(this.$refs.fileSelector.files);
|
||||
this.toggleCollapsedSection();
|
||||
},
|
||||
openMarvinJsModal(button) {
|
||||
MarvinJsEditor.initNewButton('.new-marvinjs-upload-button', this.loadAttachments);
|
||||
|
|
@ -40,11 +41,17 @@ export default {
|
|||
if (status === 'success') {
|
||||
const attachment = attachmentData.data;
|
||||
this.addAttachment(attachment);
|
||||
this.toggleCollapsedSection();
|
||||
} else {
|
||||
HelperModule.flashAlertMsg(this.i18n.t('errors.general'), 'danger');
|
||||
}
|
||||
});
|
||||
},
|
||||
toggleCollapsedSection() {
|
||||
if (this.isCollapsed) {
|
||||
this.$refs.toggleElement.click();
|
||||
}
|
||||
},
|
||||
addAttachment(attachment) {
|
||||
this.attachments.push(attachment);
|
||||
this.showFileModal = false;
|
||||
|
|
|
|||
|
|
@ -267,6 +267,16 @@ export default {
|
|||
|
||||
this.$emit('update', this.element, false, callback);
|
||||
},
|
||||
updateTableData() {
|
||||
if (this.editingTable === false) return;
|
||||
this.updatingTableData = true;
|
||||
|
||||
this.$nextTick(() => {
|
||||
this.update(() => {
|
||||
this.editingCell = false;
|
||||
});
|
||||
});
|
||||
},
|
||||
loadTableData() {
|
||||
const container = this.$refs.hotTable;
|
||||
const data = JSON.parse(this.element.attributes.orderable.contents);
|
||||
|
|
@ -294,12 +304,19 @@ export default {
|
|||
}
|
||||
},
|
||||
afterChange: () => {
|
||||
if (this.editingTable === false) return;
|
||||
this.updatingTableData = true;
|
||||
|
||||
this.$nextTick(() => {
|
||||
this.update(() => this.editingCell = false);
|
||||
});
|
||||
this.updateTableData();
|
||||
},
|
||||
afterRemoveRow: () => {
|
||||
this.updateTableData();
|
||||
},
|
||||
afterRemoveCol: () => {
|
||||
this.updateTableData();
|
||||
},
|
||||
afterCreateCol: () => {
|
||||
this.updateTableData();
|
||||
},
|
||||
afterCreateRow: () => {
|
||||
this.updateTableData();
|
||||
},
|
||||
beforeKeyDown: (e) => {
|
||||
if (e.keyCode === 27) { // esc
|
||||
|
|
|
|||
|
|
@ -5,7 +5,8 @@
|
|||
@dragenter.prevent="dragEnter($event)"
|
||||
@dragleave.prevent="dragLeave($event)"
|
||||
@dragover.prevent
|
||||
class="flex h-full w-full p-6 rounded border border-sn-light-grey bg-sn-super-light-blue"
|
||||
@click="handleImportClick"
|
||||
class="flex h-full w-full p-6 rounded border border-sn-light-grey bg-sn-super-light-blue cursor-pointer"
|
||||
>
|
||||
<div id="centered-content" class="flex flex-col gap-4 items-center h-fit w-fit m-auto">
|
||||
<!-- icon -->
|
||||
|
|
@ -14,7 +15,7 @@
|
|||
<!-- text section -->
|
||||
<div class="flex flex-col gap-1">
|
||||
<div class="text-sn-dark-grey">
|
||||
<span class="text-sn-science-blue hover:cursor-pointer" @click="handleImportClick">
|
||||
<span class="text-sn-science-blue hover:cursor-pointer" >
|
||||
{{ i18n.t('repositories.import_records.dragAndDropUpload.importText.firstPart') }}
|
||||
</span> {{ i18n.t('repositories.import_records.dragAndDropUpload.importText.secondPart') }}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<div class="dropdown-selector" :data-e2e="`e2e-IF-${dataE2e}`">
|
||||
<div class="dropdown-selector" :data-e2e="`${dataE2e}`">
|
||||
<select :id="this.selectorId"
|
||||
:data-select-by-group="groupSelector"
|
||||
:data-combine-tags="dataCombineTags"
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@
|
|||
<template v-if="isOpen">
|
||||
<teleport to="body">
|
||||
<div ref="flyout"
|
||||
class="fixed z-[3000] sn-menu-dropdown bg-sn-white rounded p-2.5 sn-shadow-menu-sm flex flex-col gap-[1px]"
|
||||
class="fixed z-[3000] sn-menu-dropdown bg-sn-white rounded p-2.5 overflow-auto sn-shadow-menu-sm flex flex-col gap-[1px]"
|
||||
:class="{
|
||||
'right-0': position === 'right',
|
||||
'left-0': position === 'left',
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@
|
|||
:placeholder="placeholderRender"
|
||||
@keyup="fetchOptions"
|
||||
@change.stop
|
||||
class="w-full border-0 outline-none pl-0 placeholder:text-sn-grey" />
|
||||
class="w-full bg-transparent border-0 outline-none pl-0 placeholder:text-sn-grey" />
|
||||
</template>
|
||||
<div v-else class="flex items-center gap-1 flex-wrap">
|
||||
<div v-for="tag in tags" class="px-2 py-1 rounded-sm bg-sn-super-light-grey grid grid-cols-[auto_1fr] items-center gap-1">
|
||||
|
|
|
|||
|
|
@ -59,7 +59,7 @@ module Reports
|
|||
end
|
||||
|
||||
def generate_pdf_content
|
||||
@has_cover = Rails.root.join('app', 'views', 'reports', 'templates', @template, 'cover.html.erb').exist?
|
||||
@num_of_cover_pages = cover_pages_count
|
||||
|
||||
render_header_footer_and_report
|
||||
|
||||
|
|
@ -83,7 +83,7 @@ module Reports
|
|||
template: 'reports/report',
|
||||
layout: false,
|
||||
assigns: { settings: @report.settings },
|
||||
locals: { report: @report, user: @user, has_cover: @has_cover }
|
||||
locals: { report: @report, user: @user, num_of_cover_pages: @num_of_cover_pages }
|
||||
)
|
||||
end
|
||||
|
||||
|
|
@ -130,8 +130,6 @@ module Reports
|
|||
current_margin = extract_margins_from_header ||
|
||||
{ top: '2cm', bottom: '2cm', left: '1cm', right: '1.5cm' }
|
||||
|
||||
cover_pages_shift = cover_page_shift_from_template
|
||||
|
||||
Grover.new(
|
||||
@report_html,
|
||||
format: 'A4',
|
||||
|
|
@ -142,7 +140,7 @@ module Reports
|
|||
footer_template: @footer_html,
|
||||
style_tag_options: @style_tag_options,
|
||||
script_tag_options: @script_tag_options,
|
||||
page_ranges: "#{cover_pages_shift}-999999",
|
||||
page_ranges: "#{@num_of_cover_pages + 1}-999999",
|
||||
emulate_media: 'screen',
|
||||
display_url: Rails.application.routes.default_url_options[:host]
|
||||
).to_pdf(@file.path)
|
||||
|
|
@ -150,7 +148,7 @@ module Reports
|
|||
|
||||
def process_attach_pdf_report_and_notify
|
||||
@file.rewind
|
||||
@file = prepend_title_page if @has_cover
|
||||
@file = prepend_title_page if @num_of_cover_pages.positive?
|
||||
@file = append_result_asset_previews if @report.settings.dig(:task, :file_results_previews)
|
||||
|
||||
@report.pdf_file.attach(io: @file, filename: 'report.pdf')
|
||||
|
|
@ -337,16 +335,16 @@ module Reports
|
|||
margins
|
||||
end
|
||||
|
||||
def cover_page_shift_from_template
|
||||
def cover_pages_count
|
||||
cover_file_path = Rails.root.join('app', 'views', 'reports', 'templates', @template, 'cover.html.erb')
|
||||
return 1 unless cover_file_path.exist?
|
||||
return 0 unless cover_file_path.exist?
|
||||
|
||||
content = File.read(cover_file_path)
|
||||
|
||||
cover_pages_comment = content.match(/<!--\s*cover_pages_count:(\d+)\s*-->/)
|
||||
return 2 unless cover_pages_comment
|
||||
return 1 unless cover_pages_comment
|
||||
|
||||
cover_pages_comment[1].to_i + 1
|
||||
cover_pages_comment[1].to_i
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -385,6 +385,10 @@ class Asset < ApplicationRecord
|
|||
new_image_filename = "#{new_name}.png"
|
||||
preview_image.blob.update!(filename: new_image_filename)
|
||||
end
|
||||
|
||||
# rubocop:disable Rails/SkipsModelValidations
|
||||
touch
|
||||
# rubocop:enable Rails/SkipsModelValidations
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -22,7 +22,8 @@ class RepositoryChecklistValue < ApplicationRecord
|
|||
EXTRA_PRELOAD_INCLUDE = :repository_checklist_items
|
||||
|
||||
def formatted(separator: ' | ')
|
||||
repository_checklist_items.pluck(:data).join(separator).gsub("\n", "\\n")
|
||||
checklist_items = current_repository_checklist_items || repository_checklist_items
|
||||
checklist_items.pluck(:data).join(separator)
|
||||
end
|
||||
|
||||
def export_formatted
|
||||
|
|
|
|||
|
|
@ -61,7 +61,7 @@ class RepositoryTextValue < ApplicationRecord
|
|||
def self.import_from_text(text, attributes, _options = {})
|
||||
return nil if text.blank?
|
||||
|
||||
new(attributes.merge(data: text.truncate(Constants::TEXT_MAX_LENGTH)))
|
||||
new(attributes.merge(data: text))
|
||||
end
|
||||
|
||||
alias export_formatted formatted
|
||||
|
|
|
|||
|
|
@ -28,7 +28,11 @@ module ImportRepository
|
|||
end
|
||||
|
||||
def has_too_many_rows?
|
||||
@sheet.last_row > Constants::IMPORT_REPOSITORY_ITEMS_LIMIT
|
||||
@sheet.last_row.present? && @sheet.last_row > Constants::IMPORT_REPOSITORY_ITEMS_LIMIT
|
||||
end
|
||||
|
||||
def has_too_little_rows?
|
||||
@sheet.last_row.nil? || @sheet.last_row < Constants::IMPORT_REPOSITORY_ITEMS_MIN_LIMIT
|
||||
end
|
||||
|
||||
def generate_temp_file
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@ module RepositoryCsvExport
|
|||
when -1, -2
|
||||
next
|
||||
when -3
|
||||
csv_row << (repository.is_a?(RepositorySnapshot) ? row.parent_id : row.code)
|
||||
csv_row << (repository.is_a?(RepositorySnapshot) ? row.parent.code : row.code)
|
||||
when -4
|
||||
csv_row << row.name
|
||||
when -5
|
||||
|
|
@ -53,7 +53,7 @@ module RepositoryCsvExport
|
|||
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) : ''
|
||||
csv_row << (row.updated_at ? I18n.l(row.updated_at, format: :full) : '')
|
||||
when -8
|
||||
csv_row << row.last_modified_by.full_name
|
||||
when -9
|
||||
|
|
|
|||
|
|
@ -18,6 +18,9 @@ module RepositoryXlsxExport
|
|||
def self.to_xlsx(rows, column_ids, user, repository, handle_file_name_func, in_module)
|
||||
package = Axlsx::Package.new
|
||||
workbook = package.workbook
|
||||
datetime_style = workbook.styles.add_style format_code: 'dd-mmm-yyyy hh:mm:ss'
|
||||
date_style = workbook.styles.add_style format_code: 'dd-mmm-yyyy'
|
||||
|
||||
add_consumption = in_module && !repository.is_a?(RepositorySnapshot) && repository.has_stock_management?
|
||||
|
||||
workbook.add_worksheet(name: 'Data Export') do |sheet|
|
||||
|
|
@ -30,30 +33,31 @@ module RepositoryXlsxExport
|
|||
when -1, -2
|
||||
next
|
||||
when -3
|
||||
row_data << (repository.is_a?(RepositorySnapshot) ? row.parent_id : row.id)
|
||||
row_data << (repository.is_a?(RepositorySnapshot) ? row.parent.code : row.code)
|
||||
when -4
|
||||
row_data << row.name
|
||||
when -5
|
||||
row_data << row.created_by.full_name
|
||||
when -6
|
||||
row_data << I18n.l(row.created_at, format: :full)
|
||||
row_data << row.created_at
|
||||
when -7
|
||||
row_data << row.updated_at ? I18n.l(row.updated_at, format: :full) : ''
|
||||
row_data << row.updated_at
|
||||
when -8
|
||||
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
|
||||
row_data << (row.archived? && row.archived_on.present? ? I18n.l(row.archived_on, format: :full) : '')
|
||||
row_data << row.archived_on
|
||||
when -11
|
||||
row_data << row.parent_repository_rows.map(&:code).join(' | ')
|
||||
row_data << row.child_repository_rows.map(&:code).join(' | ')
|
||||
else
|
||||
cell = row.repository_cells.find_by(repository_column_id: c_id)
|
||||
|
||||
row_data << if cell
|
||||
if cell.value_type == 'RepositoryAssetValue' && handle_file_name_func
|
||||
handle_file_name_func.call(cell.value.asset)
|
||||
elsif cell.value.is_a?(RepositoryDateTimeValue) || cell.value.is_a?(RepositoryDateValue)
|
||||
cell.value.data
|
||||
else
|
||||
cell.value.export_formatted
|
||||
end
|
||||
|
|
@ -61,7 +65,20 @@ module RepositoryXlsxExport
|
|||
end
|
||||
end
|
||||
row_data << row.row_consumption(row.stock_consumption) if add_consumption
|
||||
sheet.add_row row_data
|
||||
|
||||
style = row_data.map do |c|
|
||||
case c
|
||||
when ActiveSupport::TimeWithZone
|
||||
datetime_style
|
||||
when Time # Date values are of class Time for some reason
|
||||
date_style
|
||||
end
|
||||
end
|
||||
|
||||
sheet.add_row(
|
||||
row_data,
|
||||
style: style
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -44,10 +44,7 @@ class SpreadsheetParser
|
|||
if row && i.zero?
|
||||
header = row
|
||||
else
|
||||
escaped_row = row.map do |cell|
|
||||
cell.to_s.gsub("\\n", "\n")
|
||||
end
|
||||
columns = escaped_row
|
||||
columns = row
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -60,7 +57,9 @@ class SpreadsheetParser
|
|||
if cell.is_a?(Roo::Excelx::Cell::Number) && cell.format == 'General'
|
||||
cell&.value&.to_d
|
||||
elsif date_format && cell&.value.is_a?(Date)
|
||||
cell&.value&.strftime(date_format)
|
||||
cell&.value&.strftime(
|
||||
"#{date_format} #{' %H:%M' if cell.value.is_a?(DateTime)}"
|
||||
)
|
||||
else
|
||||
cell&.formatted_value
|
||||
end
|
||||
|
|
|
|||
|
|
@ -15,12 +15,12 @@ module RepositoryImportParser
|
|||
@columns = []
|
||||
@name_index = -1
|
||||
@id_index = nil
|
||||
@total_new_rows = 0
|
||||
@new_rows_added = 0
|
||||
@created_rows_count = 0
|
||||
@updated_rows_count = 0
|
||||
@header_skipped = false
|
||||
@repository = repository
|
||||
@sheet = sheet
|
||||
@rows = SpreadsheetParser.spreadsheet_enumerator(@sheet)
|
||||
@rows = SpreadsheetParser.spreadsheet_enumerator(@sheet).compact_blank
|
||||
@mappings = mappings
|
||||
@user = user
|
||||
@repository_columns = @repository.repository_columns
|
||||
|
|
@ -60,16 +60,7 @@ module RepositoryImportParser
|
|||
def check_for_duplicate_columns
|
||||
col_compact = @columns.compact
|
||||
if col_compact.map(&:id).uniq.length != col_compact.length
|
||||
{ status: :error, nr_of_added: @new_rows_added, total_nr: @total_new_rows }
|
||||
end
|
||||
end
|
||||
|
||||
def handle_invalid_cell_value(value, cell_value)
|
||||
if value.present? && cell_value.nil?
|
||||
@errors << 'Incorrect data format'
|
||||
true
|
||||
else
|
||||
false
|
||||
{ status: :error, total_rows_count: total_rows_count, updated_rows_count: @updated_rows_count, created_rows_count: @created_rows_count }
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -78,8 +69,6 @@ module RepositoryImportParser
|
|||
duplicate_ids = SpreadsheetParser.duplicate_ids(@sheet)
|
||||
|
||||
@rows.each do |row|
|
||||
next if row.blank?
|
||||
|
||||
unless @header_skipped
|
||||
@header_skipped = true
|
||||
next
|
||||
|
|
@ -88,8 +77,6 @@ module RepositoryImportParser
|
|||
incoming_row = SpreadsheetParser.parse_row(row, @sheet, date_format: @user.settings['date_format'])
|
||||
next if incoming_row.compact.blank?
|
||||
|
||||
@total_new_rows += 1
|
||||
|
||||
if @id_index
|
||||
id = incoming_row[@id_index].to_s.gsub(RepositoryRow::ID_PREFIX, '')
|
||||
|
||||
|
|
@ -111,7 +98,7 @@ module RepositoryImportParser
|
|||
existing_row.import_status = 'unchanged'
|
||||
elsif existing_row.archived
|
||||
existing_row.import_status = 'archived'
|
||||
elsif duplicate_ids.include?(existing_row.id)
|
||||
elsif duplicate_ids.include?(existing_row.code)
|
||||
existing_row.import_status = 'duplicated'
|
||||
end
|
||||
|
||||
|
|
@ -129,8 +116,12 @@ module RepositoryImportParser
|
|||
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) }
|
||||
{ status: :ok,
|
||||
total_rows_count: total_rows_count,
|
||||
created_rows_count: @created_rows_count,
|
||||
updated_rows_count: @updated_rows_count,
|
||||
changes: changes,
|
||||
import_date: I18n.l(Time.zone.today, format: :full_date) }
|
||||
end
|
||||
|
||||
def import_row(repository_row, import_row)
|
||||
|
|
@ -179,7 +170,13 @@ module RepositoryImportParser
|
|||
@user.as_json(root: true, only: :settings).deep_symbolize_keys
|
||||
)
|
||||
end
|
||||
next if handle_invalid_cell_value(value, cell_value)
|
||||
|
||||
if value.present? && cell_value.nil?
|
||||
raise ActiveRecord::Rollback unless @preview
|
||||
|
||||
@errors << I18n.t('activerecord.errors.models.repository_cell.incorrect_format')
|
||||
next
|
||||
end
|
||||
|
||||
existing_cell = repository_row.repository_cells.find { |c| c.repository_column_id == column.id }
|
||||
|
||||
|
|
@ -195,10 +192,10 @@ module RepositoryImportParser
|
|||
repository_row.import_status = if @errors.present?
|
||||
'invalid'
|
||||
elsif repository_row.import_status == 'created'
|
||||
@new_rows_added += 1
|
||||
@created_rows_count += 1
|
||||
'created'
|
||||
elsif @updated
|
||||
@new_rows_added += 1
|
||||
@updated_rows_count += 1
|
||||
'updated'
|
||||
else
|
||||
'unchanged'
|
||||
|
|
@ -241,12 +238,15 @@ module RepositoryImportParser
|
|||
repository_cell
|
||||
else
|
||||
# Create new cell
|
||||
cell_value.repository_cell.value = cell_value
|
||||
repository_row.repository_cells << cell_value.repository_cell
|
||||
|
||||
if @preview
|
||||
cell_value.validate
|
||||
cell_value.repository_cell.id = SecureRandom.uuid.gsub(/[a-zA-Z-]/, '') unless cell_value.repository_cell.id.present? # ID required for preview with serializer
|
||||
repository_cell = repository_row.repository_cells.build(value: cell_value, repository_column: cell_value.repository_cell.repository_column)
|
||||
repository_cell.validate
|
||||
repository_cell.id = SecureRandom.uuid.gsub(/[a-zA-Z-]/, '') unless cell_value.repository_cell.id.present? # ID required for preview with serializer
|
||||
return repository_cell
|
||||
else
|
||||
cell_value.repository_cell.value = cell_value
|
||||
repository_row.repository_cells << cell_value.repository_cell
|
||||
cell_value.save!
|
||||
end
|
||||
@updated ||= true
|
||||
|
|
@ -280,5 +280,10 @@ module RepositoryImportParser
|
|||
value
|
||||
end
|
||||
end
|
||||
|
||||
def total_rows_count
|
||||
# all rows minus header
|
||||
@rows.count - 1
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -67,7 +67,7 @@ module UsersGenerator
|
|||
|
||||
def generate_user_password
|
||||
require 'securerandom'
|
||||
SecureRandom.hex(5)
|
||||
SecureRandom.alphanumeric(Devise.password_length.max)
|
||||
end
|
||||
|
||||
def get_user_initials(full_name)
|
||||
|
|
|
|||
|
|
@ -10,6 +10,8 @@
|
|||
<div class="modal-body !pb-0">
|
||||
<div id="DashboardNewTask">
|
||||
<dashboard-new-task
|
||||
:key="modalKey"
|
||||
@close="modalKey += 1"
|
||||
projects-url="<%= dashboard_quick_start_project_filter_path %>"
|
||||
experiments-url="<%= dashboard_quick_start_experiment_filter_path %>"
|
||||
create-url="<%= dashboard_quick_start_create_task_path %>"
|
||||
|
|
|
|||
|
|
@ -19,9 +19,9 @@
|
|||
<% end %>
|
||||
<div class="name-readonly-placeholder">
|
||||
<% if @protocol.in_repository_draft? %>
|
||||
<%= t('protocols.draft_name', name: @protocol.name ) %>
|
||||
<span title="<%= t('protocols.draft_name', name: @protocol.name ) %>"><%= t('protocols.draft_name', name: @protocol.name ) %></div>
|
||||
<% else %>
|
||||
<%= @protocol.name %>
|
||||
<span title="<%= @protocol.name %>"><%= @protocol.name %></span>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
|
|
|||
|
|
@ -4,8 +4,8 @@
|
|||
<meta charset="UTF-8">
|
||||
</head>
|
||||
<body class="print-report-body">
|
||||
<% if has_cover %>
|
||||
<div style="break-after: page;"></div>
|
||||
<% num_of_cover_pages.times do %>
|
||||
<div style="min-height: 100vh; break-after: page;"></div>
|
||||
<% end %>
|
||||
<div class="print-report">
|
||||
<% report.root_elements.each do |el| %>
|
||||
|
|
|
|||
|
|
@ -14,7 +14,6 @@
|
|||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="mb-6"><%=t('zip_export.repository_header_html', repository: repository.name) %></div>
|
||||
<div class="mb-6 custom-alert-info"><%=t 'zip_export.files_alert' %></div>
|
||||
<div class="mb-6"><%=t 'zip_export.repository_footer_html' %></div>
|
||||
|
||||
<div class="sci-radio-container">
|
||||
|
|
@ -27,7 +26,7 @@
|
|||
) %>
|
||||
<span class="sci-radio-label"></span>
|
||||
</div>
|
||||
<%= f.label :file_type, ".xlsx", value: "xlsx", class: "mr-6 ml-3" %>
|
||||
<%= f.label :file_type, ".xlsx", value: "xlsx", class: "mr-6 ml-3 font-normal" %>
|
||||
|
||||
<div class="sci-radio-container">
|
||||
<%= f.radio_button(
|
||||
|
|
@ -39,7 +38,7 @@
|
|||
) %>
|
||||
<span class="sci-radio-label"></span>
|
||||
</div>
|
||||
<%= f.label :file_type, ".csv", value: "csv", class: "mr-6 ml-3" %>
|
||||
<%= f.label :file_type, ".csv", value: "csv", class: "mr-6 ml-3 font-normal" %>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type='button' data-e2e='e2e-BT-exportMD-cancel' class='btn btn-secondary' data-dismiss='modal' id='close-modal-export-repository-rows'><%= t('general.cancel')%></button>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<% ApplicationSettings.instance.values['azure_ad_apps'].select { |v| v['enable_sign_in'] }.each do |config| %>
|
||||
<div class="form-group">
|
||||
<%= form_tag user_customazureactivedirectory_omniauth_authorize_path(provider: config['provider_name']), method: :post, class: "azureAdForm" do %>
|
||||
<%= submit_tag config['sign_in_label'] || t('devise.sessions.new.azure_ad_submit'), class: 'btn btn-primary' %>
|
||||
<%= submit_tag config['sign_in_label'] || t('devise.sessions.new.azure_ad_submit'), class: 'btn btn-primary btn-azure-ad' %>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@
|
|||
<%- if sso_enabled? && okta_enabled? %>
|
||||
<div class="okta-sign-in-actions">
|
||||
<%= form_tag user_okta_omniauth_authorize_path, method: :post, id: 'oktaForm' do %>
|
||||
<%= submit_tag t('devise.okta.sign_in_label'), class: 'btn btn-primary' %>
|
||||
<%= submit_tag t('devise.okta.sign_in_label'), class: 'btn btn-primary btn-okta' %>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
|
@ -50,16 +50,16 @@
|
|||
|
||||
<%- if sso_enabled? && openid_connect_enabled? %>
|
||||
<div class="azure-sign-in-actions">
|
||||
<%= form_tag user_openid_connect_omniauth_authorize_path, method: :post do %>
|
||||
<%= submit_tag t('devise.sessions.new.openid_connect_submit'), class: 'btn btn-primary' %>
|
||||
<%= form_tag user_openid_connect_omniauth_authorize_path, method: :post, id: 'openidConnectForm' do %>
|
||||
<%= submit_tag t('devise.sessions.new.openid_connect_submit'), class: 'btn btn-primary btn-openid-connect' %>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<% if sso_enabled? && saml_enabled? %>
|
||||
<div class="azure-sign-in-actions">
|
||||
<%= form_tag user_saml_omniauth_authorize_path, method: :post do %>
|
||||
<%= submit_tag t('devise.sessions.new.saml_submit'), class: 'btn btn-primary' %>
|
||||
<%= form_tag user_saml_omniauth_authorize_path, method: :post, id: 'samlForm' do %>
|
||||
<%= submit_tag t('devise.sessions.new.saml_submit'), class: 'btn btn-primary btn-saml' %>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
|
|
|||
|
|
@ -53,6 +53,8 @@ module Scinote
|
|||
|
||||
config.action_dispatch.cookies_serializer = :hybrid
|
||||
|
||||
config.action_view.preload_links_header = false if ENV['RAILS_NO_PRELOAD_LINKS_HEADER'] == 'true'
|
||||
|
||||
# Max uploaded file size in MB
|
||||
config.x.file_max_size_mb = (ENV['FILE_MAX_SIZE_MB'] || 50).to_i
|
||||
|
||||
|
|
@ -62,6 +64,8 @@ module Scinote
|
|||
|
||||
config.x.custom_sanitizer_config = nil
|
||||
|
||||
config.x.no_external_csp_exceptions = ENV['SCINOTE_NO_EXT_CSP_EXCEPTIONS'] == 'true'
|
||||
|
||||
# Logging
|
||||
config.log_formatter = proc do |severity, datetime, progname, msg|
|
||||
"[#{datetime}] #{severity}: #{msg}\n"
|
||||
|
|
|
|||
|
|
@ -387,7 +387,7 @@ class Constants
|
|||
'ColReorder' => [*0..4]
|
||||
}
|
||||
|
||||
REPOSITORY_SNAPSHOT_TABLE_DEFAULT_STATE['columns'] = REPOSITORY_TABLE_DEFAULT_STATE['columns'][0..4]
|
||||
REPOSITORY_SNAPSHOT_TABLE_DEFAULT_STATE['columns'] = REPOSITORY_TABLE_DEFAULT_STATE['columns'][0..4].deep_dup
|
||||
REPOSITORY_SNAPSHOT_TABLE_DEFAULT_STATE['columns'][4]['visible'] = true
|
||||
|
||||
REPOSITORY_SNAPSHOT_TABLE_DEFAULT_STATE.freeze
|
||||
|
|
@ -416,6 +416,7 @@ class Constants
|
|||
}.freeze
|
||||
|
||||
IMPORT_REPOSITORY_ITEMS_LIMIT = 2000
|
||||
IMPORT_REPOSITORY_ITEMS_MIN_LIMIT = 2
|
||||
|
||||
DEFAULT_TEAM_REPOSITORIES_LIMIT = 6
|
||||
|
||||
|
|
|
|||
|
|
@ -192,7 +192,10 @@ Devise.setup do |config|
|
|||
|
||||
# ==> Configuration for :validatable
|
||||
# Range for password length.
|
||||
config.password_length = 8..72
|
||||
password_min_length = ENV['PASSWORD_MIN_LENGTH'].to_i
|
||||
password_max_length = 72
|
||||
password_min_length = 8 unless password_min_length.positive? && password_min_length < password_max_length
|
||||
config.password_length = password_min_length..password_max_length
|
||||
|
||||
# Email regex used to validate email formats. It simply asserts that
|
||||
# one (and only one) @ exists in the given string. This is mainly
|
||||
|
|
|
|||
|
|
@ -316,7 +316,7 @@ class Extends
|
|||
user_leave_team: 104,
|
||||
copy_inventory: 105,
|
||||
export_protocol_from_task: 106,
|
||||
import_inventory_items: 107,
|
||||
import_inventory_items_legacy: 107,
|
||||
create_tag: 108,
|
||||
delete_tag: 109,
|
||||
edit_image_on_result: 110,
|
||||
|
|
@ -495,7 +495,7 @@ class Extends
|
|||
task_step_asset_renamed: 305,
|
||||
result_asset_renamed: 306,
|
||||
protocol_step_asset_renamed: 307,
|
||||
item_added_with_import: 308
|
||||
inventory_items_added_or_updated_with_import: 308
|
||||
}
|
||||
|
||||
ACTIVITY_GROUPS = {
|
||||
|
|
@ -596,21 +596,31 @@ class Extends
|
|||
'FluicsLabelTemplate' => 'Fluics'
|
||||
}
|
||||
|
||||
EXTERNAL_SCRIPT_SERVICES = %w(
|
||||
https://marvinjs.chemicalize.com/
|
||||
www.recaptcha.net/
|
||||
www.gstatic.com/recaptcha/
|
||||
)
|
||||
EXTERNAL_SCRIPT_SERVICES =
|
||||
if Rails.application.config.x.no_external_csp_exceptions
|
||||
[]
|
||||
else
|
||||
%w(
|
||||
https://marvinjs.chemicalize.com/
|
||||
www.recaptcha.net/
|
||||
www.gstatic.com/recaptcha/
|
||||
)
|
||||
end
|
||||
|
||||
EXTERNAL_CONNECT_SERVICES = %w(
|
||||
https://www.protocols.io/
|
||||
http://127.0.0.1:9100/
|
||||
newrelic.com
|
||||
*.newrelic.com
|
||||
*.nr-data.net
|
||||
extras.scinote.net
|
||||
https://www.scinote.net
|
||||
)
|
||||
EXTERNAL_CONNECT_SERVICES =
|
||||
if Rails.application.config.x.no_external_csp_exceptions
|
||||
%w(http://127.0.0.1:9100/)
|
||||
else
|
||||
%w(
|
||||
https://www.protocols.io/
|
||||
http://127.0.0.1:9100/
|
||||
newrelic.com
|
||||
*.newrelic.com
|
||||
*.nr-data.net
|
||||
extras.scinote.net
|
||||
https://www.scinote.net
|
||||
)
|
||||
end
|
||||
|
||||
if Constants::ASSET_SYNC_URL && EXTERNAL_CONNECT_SERVICES.exclude?(Constants::ASSET_SYNC_URL)
|
||||
asset_sync_url = URI.parse(Constants::ASSET_SYNC_URL)
|
||||
|
|
|
|||
|
|
@ -62,23 +62,26 @@ en:
|
|||
title: "Cannot create."
|
||||
description: "As a guest in this team you cannot create anything."
|
||||
create_task_modal:
|
||||
title: "Create a new Task"
|
||||
title: "Create new task"
|
||||
description: "Simply type in the fields below to find or create space for your new task to live in"
|
||||
project: "Project"
|
||||
task_name: "Task name"
|
||||
task_name_placeholder: "Enter task name"
|
||||
task_name_error: "Task name must be at least %{length} characters long."
|
||||
project_visibility_label: "Visible to"
|
||||
project_visibility_members: "Project members"
|
||||
project_visibility_all: "All team members"
|
||||
experiment: "Experiment"
|
||||
project_placeholder: "Enter project name (New or Existing)"
|
||||
experiment_placeholder: "Enter experiment name (New or Existing)"
|
||||
user_role: "User role"
|
||||
project_placeholder: "Select or create project"
|
||||
experiment_placeholder: "Select or create experiment"
|
||||
user_role_placeholder: "Select default user role"
|
||||
experiment_disabled_placeholder: "Select Project to enable Experiments"
|
||||
filter_create_new: "Create"
|
||||
cancel: "Cancel"
|
||||
create: "Create"
|
||||
new_project: "New \"%{name}\" Project"
|
||||
new_experiment: "New \"%{name}\" Experiment"
|
||||
new_project: "Create \"%{name}\" Project"
|
||||
new_experiment: "Create \"%{name}\" Experiment"
|
||||
recent_work:
|
||||
title: "Recent work"
|
||||
no_results:
|
||||
|
|
|
|||
|
|
@ -234,6 +234,8 @@ en:
|
|||
repository_row_connection:
|
||||
self_connection: 'A repository_row cannot have a connection with itself'
|
||||
reciprocal_connection: 'Reciprocal connections are not allowed'
|
||||
repository_cell:
|
||||
incorrect_format: 'Incorrect data format'
|
||||
webhook:
|
||||
attributes:
|
||||
configuration:
|
||||
|
|
@ -2038,7 +2040,7 @@ en:
|
|||
error_searching: "Error searching, please try again"
|
||||
button_tooltip:
|
||||
new: "Create new item"
|
||||
import: "Update inventory"
|
||||
import: "Import items"
|
||||
filters: "Filters"
|
||||
search: "Quick search"
|
||||
filters:
|
||||
|
|
@ -2192,6 +2194,7 @@ en:
|
|||
invalid_extension: "The file has invalid extension."
|
||||
empty_file: "You've selected empty file. There's not much to import."
|
||||
temp_file_failure: "We couldn't create temporary file. Please contact administrator."
|
||||
items_min_limit: "The imported file content doesn't meet criteria"
|
||||
no_file_selected: "You didn't select any file."
|
||||
errors_list_title: "Items were not imported because one or more errors were found:"
|
||||
list_row: "Row %{row}"
|
||||
|
|
@ -2212,7 +2215,7 @@ en:
|
|||
importTitle: 'Import'
|
||||
importBtnText: 'Import'
|
||||
cancelBtnText: 'Cancel'
|
||||
dragAndDropSupportingText: '.xlsx, .xls, .csv or .txt file'
|
||||
dragAndDropSupportingText: '.csv, .xlsx, .txt or .tsv file'
|
||||
step2:
|
||||
id: 'step2'
|
||||
icon: 'sn-icon-open'
|
||||
|
|
@ -2249,7 +2252,7 @@ en:
|
|||
RepositoryDateTimeValue: 'Date-time'
|
||||
RepositoryDateValue: 'Date'
|
||||
RepositoryTimeValue: 'Time'
|
||||
RepositoryListValue: 'List'
|
||||
RepositoryListValue: 'Dropdown'
|
||||
RepositoryStatusValue: 'Status'
|
||||
RepositoryStockValue: 'Stock'
|
||||
table:
|
||||
|
|
@ -2280,7 +2283,7 @@ en:
|
|||
exampleData: 'Example data'
|
||||
step3:
|
||||
title: 'Import preview'
|
||||
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.'
|
||||
subtitle: 'This preview shows changes to the %{inventory} inventory resulting from this import. Values that will be updated are marked in green. Items with errors in red will not be imported. 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'
|
||||
|
|
@ -2291,7 +2294,7 @@ en:
|
|||
name: 'Name'
|
||||
status: 'Status'
|
||||
cancel: 'Cancel import'
|
||||
confirm: 'Confirm'
|
||||
import: 'Import'
|
||||
status_message:
|
||||
created: 'new item'
|
||||
updated: 'updated'
|
||||
|
|
@ -2318,13 +2321,13 @@ en:
|
|||
element1:
|
||||
id: 'el1'
|
||||
icon: 'sn-icon-edit'
|
||||
label: 'Edit your 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.'
|
||||
label: 'Structure your import data'
|
||||
subtext: '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: 'Upload your file'
|
||||
subtext: 'Upload your data using .xlsx, .csv, or .txt files to import new items or update existing item data.'
|
||||
subtext: 'Upload your data using .csv, .xlsx, .txt or .tsv files to import new items or update existing item data.'
|
||||
element3:
|
||||
id: 'el3'
|
||||
icon: 'sn-icon-tables'
|
||||
|
|
@ -2346,8 +2349,8 @@ 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} items successfully imported."
|
||||
success_flash: "%{successful_rows_count} of %{total_rows_count} new item(s) successfully imported."
|
||||
partial_success_flash: "%{successful_rows_count} of %{total_rows_count} 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."
|
||||
|
|
|
|||
|
|
@ -185,7 +185,8 @@ en:
|
|||
create_tag_html: "%{user} created tag <strong>%{tag}</strong> in project %{project}."
|
||||
edit_tag_html: "%{user} edited tag <strong>%{tag}</strong> in project %{project}."
|
||||
delete_tag_html: "%{user} deleted tag <strong>%{tag}</strong> in project %{project}."
|
||||
import_inventory_items_html: "%{user} imported %{num_of_items} inventory item(s) to %{repository}."
|
||||
import_inventory_items_legacy_html: "%{user} imported %{num_of_items} inventory item(s) to %{repository}."
|
||||
inventory_items_added_or_updated_with_import_html: "%{user} imported %{created_rows_count} new item(s) and updated %{updated_rows_count} existing item(s) by import in %{repository}."
|
||||
edit_image_on_result_html: "%{user} edited image %{asset_name} on result %{result}: %{action}."
|
||||
edit_image_on_step_html: "%{user} edited image %{asset_name} on protocol's step %{step_position} %{step} on task %{my_module}: %{action}."
|
||||
edit_image_on_step_in_repository_html: "%{user} edited image %{asset_name} on protocol %{protocol}'s step %{step_position} %{step} in Protocol repository: %{action}."
|
||||
|
|
@ -460,7 +461,8 @@ en:
|
|||
create_tag: "Tag created"
|
||||
edit_tag: "Tag edited"
|
||||
delete_tag: "Tag deleted"
|
||||
import_inventory_items: "Inventory items imported"
|
||||
import_inventory_items_legacy: "Inventory items imported (obsolete)"
|
||||
inventory_items_added_or_updated_with_import: "Items added or updated with import"
|
||||
item_added_with_import: "Item added with import"
|
||||
edit_image_on_result: "Image on result edited"
|
||||
edit_image_on_step: "Image on task step edited"
|
||||
|
|
|
|||
|
|
@ -98,25 +98,50 @@ describe RepositoriesController, type: :controller do
|
|||
repository_row: repository_row,
|
||||
repository_column: repository_column
|
||||
end
|
||||
let(:params) do
|
||||
let(:params_csv) do
|
||||
{
|
||||
id: repository.id,
|
||||
header_ids: [repository_column.id],
|
||||
row_ids: [repository_row.id]
|
||||
row_ids: [repository_row.id],
|
||||
file_type: :csv
|
||||
}
|
||||
end
|
||||
let(:action) { post :export_repository, params: params, format: :json }
|
||||
|
||||
it 'calls create activity for exporting inventory items' do
|
||||
let(:params_xlsx) do
|
||||
{
|
||||
id: repository.id,
|
||||
header_ids: [repository_column.id],
|
||||
row_ids: [repository_row.id],
|
||||
file_type: :xlsx
|
||||
}
|
||||
end
|
||||
|
||||
let(:action_csv) { post :export_repository, params: params_csv, format: :json }
|
||||
let(:action_xlsx) { post :export_repository, params: params_xlsx, format: :json }
|
||||
|
||||
it 'calls create activity for exporting inventory items csv' do
|
||||
expect(Activities::CreateActivityService)
|
||||
.to(receive(:call)
|
||||
.with(hash_including(activity_type: :export_inventory_items)))
|
||||
|
||||
action
|
||||
action_csv
|
||||
end
|
||||
|
||||
it 'adds activity in DB' do
|
||||
expect { action }
|
||||
it 'adds activity in DB for exporting csv' do
|
||||
expect { action_csv }
|
||||
.to(change { Activity.count })
|
||||
end
|
||||
|
||||
it 'calls create activity for exporting inventory items xlsx' do
|
||||
expect(Activities::CreateActivityService)
|
||||
.to(receive(:call)
|
||||
.with(hash_including(activity_type: :export_inventory_items)))
|
||||
|
||||
action_xlsx
|
||||
end
|
||||
|
||||
it 'adds activity in DB for exporting xlsx' do
|
||||
expect { action_xlsx }
|
||||
.to(change { Activity.count })
|
||||
end
|
||||
end
|
||||
|
|
@ -136,17 +161,18 @@ describe RepositoriesController, type: :controller do
|
|||
|
||||
it 'calls create activity for importing inventory items' do
|
||||
allow_any_instance_of(ImportRepository::ImportRecords)
|
||||
.to receive(:import!).and_return(status: :ok)
|
||||
.to receive(:import!).and_return({ status: :ok, created_rows_count: 1, updated_rows_count: 1 })
|
||||
|
||||
expect(Activities::CreateActivityService)
|
||||
.to(receive(:call)
|
||||
.with(hash_including(activity_type: :import_inventory_items)))
|
||||
.with(hash_including(activity_type: :inventory_items_added_or_updated_with_import)))
|
||||
|
||||
action
|
||||
end
|
||||
|
||||
it 'adds activity in DB' do
|
||||
allow_any_instance_of(ImportRepository::ImportRecords).to receive(:import!).and_return(status: :ok)
|
||||
allow_any_instance_of(ImportRepository::ImportRecords).to receive(:import!)
|
||||
.and_return({ status: :ok, created_rows_count: 1, updated_rows_count: 1 })
|
||||
|
||||
expect { action }
|
||||
.to(change { Activity.count })
|
||||
|
|
|
|||
|
|
@ -70,15 +70,15 @@ describe RepositoryTableStateColumnUpdateService do
|
|||
end
|
||||
|
||||
it 'should calculate correct length' do
|
||||
expect(initial_state_1.state['columns'].length).to eq 11
|
||||
expect(initial_state_2.state['columns'].length).to eq 11
|
||||
expect(initial_state_1.state['columns'].length).to eq 13
|
||||
expect(initial_state_2.state['columns'].length).to eq 13
|
||||
|
||||
service.update_states_with_new_column(repository)
|
||||
service.update_states_with_new_column(repository)
|
||||
|
||||
[user_1, user_2].each do |user|
|
||||
state = RepositoryTableStateService.new(user, repository).load_state
|
||||
expect(state.state['columns'].length).to eq 11
|
||||
expect(state.state['columns'].length).to eq 13
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -125,11 +125,11 @@ describe RepositoryTableStateColumnUpdateService do
|
|||
end
|
||||
|
||||
it 'should keep column order as it was' do
|
||||
initial_state_1.state['ColReorder'] = [5, 3, 2, 0, 1, 4, 6, 7, 8, 9, 10]
|
||||
initial_state_1.state['ColReorder'] = [5, 3, 2, 0, 1, 4, 6, 7, 8, 9, 10, 11, 12]
|
||||
RepositoryTableStateService.new(user_1, repository).update_state(
|
||||
initial_state_1.state
|
||||
)
|
||||
initial_state_2.state['ColReorder'] = [0, 6, 1, 4, 5, 7, 2, 3, 8, 9, 10]
|
||||
initial_state_2.state['ColReorder'] = [0, 6, 1, 4, 5, 7, 2, 3, 8, 9, 10, 11, 12]
|
||||
RepositoryTableStateService.new(user_2, repository).update_state(
|
||||
initial_state_2.state
|
||||
)
|
||||
|
|
@ -138,9 +138,9 @@ describe RepositoryTableStateColumnUpdateService do
|
|||
create :repository_column, name: 'My column 4', repository: repository, data_type: :RepositoryTextValue
|
||||
|
||||
state_1 = RepositoryTableStateService.new(user_1, repository).load_state
|
||||
expect(state_1.state['ColReorder']).to eq([5, 3, 2, 0, 1, 4, 6, 7, 8, 9, 10, 11, 12])
|
||||
expect(state_1.state['ColReorder']).to eq([5, 3, 2, 0, 1, 4, 6, 7, 8, 9, 10, 11, 12, 13, 14])
|
||||
state_2 = RepositoryTableStateService.new(user_2, repository).load_state
|
||||
expect(state_2.state['ColReorder']).to eq([0, 6, 1, 4, 5, 7, 2, 3, 8, 9, 10, 11, 12])
|
||||
expect(state_2.state['ColReorder']).to eq([0, 6, 1, 4, 5, 7, 2, 3, 8, 9, 10, 11, 12, 13, 14])
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -160,8 +160,8 @@ describe RepositoryTableStateColumnUpdateService do
|
|||
expect(initial_state_1).to be_valid_default_repository_table_state(2)
|
||||
expect(initial_state_2).to be_valid_default_repository_table_state(2)
|
||||
|
||||
service.update_states_with_removed_column(repository, 9)
|
||||
service.update_states_with_removed_column(repository, 9)
|
||||
service.update_states_with_removed_column(repository, 11)
|
||||
service.update_states_with_removed_column(repository, 11)
|
||||
|
||||
[user_1, user_2].each do |user|
|
||||
state = RepositoryTableStateService.new(user, repository).load_state
|
||||
|
|
@ -170,15 +170,15 @@ describe RepositoryTableStateColumnUpdateService do
|
|||
end
|
||||
|
||||
it 'should calculate correct length' do
|
||||
expect(initial_state_1.state['columns'].length).to eq 11
|
||||
expect(initial_state_2.state['columns'].length).to eq 11
|
||||
expect(initial_state_1.state['columns'].length).to eq 13
|
||||
expect(initial_state_2.state['columns'].length).to eq 13
|
||||
|
||||
service.update_states_with_removed_column(repository, 8)
|
||||
service.update_states_with_removed_column(repository, 8)
|
||||
|
||||
[user_1, user_2].each do |user|
|
||||
state = RepositoryTableStateService.new(user, repository).load_state
|
||||
expect(state.state['columns'].length).to eq 9
|
||||
expect(state.state['columns'].length).to eq 11
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -289,12 +289,12 @@ describe RepositoryTableStateColumnUpdateService do
|
|||
end
|
||||
let!(:initial_state) do
|
||||
state = RepositoryTableStateService.new(user_1, repository).create_default_state
|
||||
state.state['order'] = [[11, 'desc']]
|
||||
state.state['order'] = [[13, 'desc']]
|
||||
(0..9).each do |idx|
|
||||
state.state['columns'][idx]['search']['search'] = "search_#{idx}"
|
||||
end
|
||||
state.state['ColReorder'] =
|
||||
[0, 1, 2, 9, 8, 4, 7, 3, 5, 6, 10, 11, 12]
|
||||
[0, 1, 2, 11, 9, 8, 4, 7, 3, 5, 6, 10, 12, 13, 14]
|
||||
RepositoryTableStateService.new(user_1, repository).update_state(
|
||||
state.state
|
||||
)
|
||||
|
|
@ -309,7 +309,7 @@ describe RepositoryTableStateColumnUpdateService do
|
|||
state = RepositoryTableStateService.new(user_1, repository).load_state
|
||||
expect(state).to be_valid_repository_table_state(5)
|
||||
expect(state.state['ColReorder']).to eq(
|
||||
[0, 1, 2, 9, 8, 4, 7, 3, 5, 6, 10, 11, 12, 13]
|
||||
[0, 1, 2, 11, 9, 8, 4, 7, 3, 5, 6, 10, 12, 13, 14, 15]
|
||||
)
|
||||
|
||||
repository.repository_columns.order(id: :asc).first.destroy!
|
||||
|
|
@ -317,25 +317,25 @@ describe RepositoryTableStateColumnUpdateService do
|
|||
state = RepositoryTableStateService.new(user_1, repository).load_state
|
||||
expect(state).to be_valid_repository_table_state(4)
|
||||
expect(state.state['ColReorder']).to eq(
|
||||
[0, 1, 2, 8, 4, 7, 3, 5, 6, 9, 10, 11, 12]
|
||||
[0, 1, 2, 9, 8, 4, 7, 3, 5, 6, 10, 11, 12, 13, 14]
|
||||
)
|
||||
expect(state.state['order']).to eq([[10, 'desc']])
|
||||
expect(state.state['order']).to eq([[12, 'desc']])
|
||||
|
||||
repository.repository_columns.order(id: :asc).first.destroy!
|
||||
|
||||
state = RepositoryTableStateService.new(user_1, repository).load_state
|
||||
expect(state).to be_valid_repository_table_state(3)
|
||||
expect(state.state['ColReorder']).to eq(
|
||||
[0, 1, 2, 8, 4, 7, 3, 5, 6, 9, 10, 11]
|
||||
[0, 1, 2, 9, 8, 4, 7, 3, 5, 6, 10, 11, 12, 13]
|
||||
)
|
||||
expect(state.state['order']).to eq([[9, 'desc']])
|
||||
expect(state.state['order']).to eq([[11, 'desc']])
|
||||
|
||||
repository.repository_columns.order(id: :asc).first.destroy!
|
||||
|
||||
state = RepositoryTableStateService.new(user_1, repository).load_state
|
||||
expect(state).to be_valid_repository_table_state(2)
|
||||
expect(state.state['ColReorder']).to eq(
|
||||
[0, 1, 2, 8, 4, 7, 3, 5, 6, 9, 10]
|
||||
[0, 1, 2, 9, 8, 4, 7, 3, 5, 6, 10, 11, 12]
|
||||
)
|
||||
|
||||
create :repository_column, name: 'My column 1', repository: repository, data_type: :RepositoryTextValue
|
||||
|
|
@ -344,7 +344,7 @@ describe RepositoryTableStateColumnUpdateService do
|
|||
state = RepositoryTableStateService.new(user_1, repository).load_state
|
||||
expect(state).to be_valid_repository_table_state(4)
|
||||
expect(state.state['ColReorder']).to eq(
|
||||
[0, 1, 2, 8, 4, 7, 3, 5, 6, 9, 10, 11, 12]
|
||||
[0, 1, 2, 9, 8, 4, 7, 3, 5, 6, 10, 11, 12, 13, 14]
|
||||
)
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -7,8 +7,8 @@ RSpec::Matchers.define :be_valid_default_repository_table_state do |nr_of_cols|
|
|||
|
||||
state = subject.state
|
||||
|
||||
cols_length = 9 + nr_of_cols
|
||||
cols_array = [*0..(8 + nr_of_cols)]
|
||||
cols_length = 11 + nr_of_cols
|
||||
cols_array = [*0..(10 + nr_of_cols)]
|
||||
|
||||
expect(state).to be_an_instance_of Hash
|
||||
expect(state).to include(
|
||||
|
|
@ -25,7 +25,7 @@ RSpec::Matchers.define :be_valid_default_repository_table_state do |nr_of_cols|
|
|||
expect(state['columns'].length).to eq(cols_length)
|
||||
state['columns'].each_with_index do |val, i|
|
||||
expect(val).to include(
|
||||
'visible' => !([4, 7, 8].include? i),
|
||||
'visible' => !([4, 7, 8, 9, 10].include? i),
|
||||
'searchable' => (![0, 4].include?(i)),
|
||||
'search' => {
|
||||
'search' => '', 'smart' => true, 'regex' => false, 'caseInsensitive' => true
|
||||
|
|
|
|||
|
|
@ -39,11 +39,22 @@ describe RepositoryImportParser::Importer do
|
|||
|
||||
describe '#run/0' do
|
||||
let(:subject) do
|
||||
RepositoryImportParser::Importer.new(sheet, mappings, user, repository)
|
||||
RepositoryImportParser::Importer.new(sheet, mappings, user, repository, true, true, false)
|
||||
end
|
||||
|
||||
it 'return a message of imported records' do
|
||||
expect(subject.run).to eq({ status: :ok, nr_of_added: 5, total_nr: 5 })
|
||||
response = subject.run
|
||||
expect(response.keys).to include(:status, :total_rows_count, :created_rows_count,
|
||||
:updated_rows_count, :changes, :import_date)
|
||||
|
||||
expect(response).to include(
|
||||
total_rows_count: 5,
|
||||
created_rows_count: 5,
|
||||
updated_rows_count: 0,
|
||||
status: :ok
|
||||
)
|
||||
|
||||
expect(response[:changes][:data].count).to eq 5
|
||||
end
|
||||
|
||||
it 'generate 5 new repository rows' do
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue