Merge branch 'develop' into ai-sci-10818-poc-instruction-in-inventory-export

This commit is contained in:
aignatov-bio 2024-07-12 13:27:47 +02:00 committed by GitHub
commit d8a270d8f7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
63 changed files with 652 additions and 459 deletions

View file

@ -299,9 +299,6 @@ Naming/VariableName:
Naming/VariableNumber:
EnforcedStyle: normalcase
Naming/BlockForwarding:
EnforcedStyle: explicit
Style/WordArray:
EnforcedStyle: percent
MinSize: 0

View file

@ -1 +1 @@
1.35.0.1
1.35.0.2

View file

@ -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');
}

View file

@ -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');

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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' }
];

View file

@ -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();
}
}
};

View file

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

View file

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

View file

@ -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');

View file

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

View file

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

View file

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

View file

@ -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>
`;
}

View file

@ -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);
});
}
}
};

View file

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

View file

@ -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');

View file

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

View file

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

View file

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

View file

@ -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'),

View file

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

View file

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

View file

@ -14,6 +14,9 @@ export default {
methods: {
openOVEditor() {
window.showIFrameModal(this.OVEurl);
if (this.isCollapsed) {
this.$refs.toggleElement.click();
}
},
initOVE() {
$(window.iFrameModal).on('sequenceSaved', () => {

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View 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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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