Merge branch 'release/1.30.0' into develop

This commit is contained in:
Martin Artnik 2024-03-13 15:26:35 +01:00
commit be4f27750c
18 changed files with 145 additions and 93 deletions

View file

@ -51,3 +51,8 @@ html {
opacity: 1; opacity: 1;
bottom: 5px; bottom: 5px;
} }
/* Hide caret in Safari */
#relationships-section summary::-webkit-details-marker {
display: none;
}

View file

@ -4,6 +4,10 @@
@apply text-sm font-medium text-sn-dark-grey; @apply text-sm font-medium text-sn-dark-grey;
} }
.sci-label.error {
@apply text-sn-coral;
}
.sci-input-container-v2 { .sci-input-container-v2 {
@apply relative h-[2.75rem] flex items-center transition-all; @apply relative h-[2.75rem] flex items-center transition-all;
} }
@ -24,6 +28,18 @@
@apply !border-sn-coral; @apply !border-sn-coral;
} }
.sci-input-container-v2.error::before {
@apply !text-sn-coral;
@apply !text-xs;
bottom: -1rem;
content: attr(data-error-text);
left: 0;
position: absolute;
white-space: nowrap;
width: 100%;
}
.sci-input-container-v2 input:focus { .sci-input-container-v2 input:focus {
@apply border-sn-science-blue shadow-none; @apply border-sn-science-blue shadow-none;
} }

View file

@ -240,6 +240,16 @@ class RepositoriesController < ApplicationController
render json: @tmp_repository.errors, status: :unprocessable_entity render json: @tmp_repository.errors, status: :unprocessable_entity
else else
copied_repository = @repository.copy(current_user, @tmp_repository.name) copied_repository = @repository.copy(current_user, @tmp_repository.name)
old_repo_stock_column = @repository.repository_columns.find_by(data_type: 'RepositoryStockValue')
copied_repo_stock_column = copied_repository.repository_columns.find_by(data_type: 'RepositoryStockValue')
if old_repo_stock_column && copied_repo_stock_column
old_repo_stock_column.repository_stock_unit_items.each do |item|
copied_item = item.dup
copied_repo_stock_column.repository_stock_unit_items << copied_item
end
copied_repository.save!
end
if !copied_repository if !copied_repository
render json: { name: ['Server error'] }, status: :unprocessable_entity render json: { name: ['Server error'] }, status: :unprocessable_entity

View file

@ -148,7 +148,7 @@ class RepositoryRowConnectionsController < ApplicationController
name: repository_row.name_with_label, name: repository_row.name_with_label,
code: repository_row.code, code: repository_row.code,
path: repository_repository_row_path(repository_row.repository, repository_row), path: repository_repository_row_path(repository_row.repository, repository_row),
repository_name: repository_row.repository.name, repository_name: repository_row.repository.name_with_label,
repository_path: repository_path(repository_row.repository), repository_path: repository_path(repository_row.repository),
unlink_path: repository_repository_row_repository_row_connection_path( unlink_path: repository_repository_row_repository_row_connection_path(
repository_row.repository, repository_row.repository,

View file

@ -149,17 +149,17 @@ class ResultsController < ApplicationController
def apply_sort! def apply_sort!
case params[:sort] case params[:sort]
when 'updated_at_asc' when 'updated_at_asc'
@results = @results.order(updated_at: :asc) @results = @results.order('results.updated_at' => :asc)
when 'updated_at_desc' when 'updated_at_desc'
@results = @results.order(updated_at: :desc) @results = @results.order('results.updated_at' => :desc)
when 'created_at_asc' when 'created_at_asc'
@results = @results.order(created_at: :asc) @results = @results.order('results.created_at' => :asc)
when 'created_at_desc' when 'created_at_desc'
@results = @results.order(created_at: :desc) @results = @results.order('results.created_at' => :desc)
when 'name_asc' when 'name_asc'
@results = @results.order(name: :asc) @results = @results.order('results.name' => :asc)
when 'name_desc' when 'name_desc'
@results = @results.order(name: :desc) @results = @results.order('results.name' => :desc)
end end
end end
@ -168,10 +168,10 @@ class ResultsController < ApplicationController
@results = @results.search(current_user, params[:view_mode] == 'archived', params[:query], params[:page] || 1) @results = @results.search(current_user, params[:view_mode] == 'archived', params[:query], params[:page] || 1)
end end
@results = @results.where('created_at >= ?', params[:created_at_from]) if params[:created_at_from] @results = @results.where('results.created_at >= ?', params[:created_at_from]) if params[:created_at_from]
@results = @results.where('created_at <= ?', params[:created_at_to]) if params[:created_at_to] @results = @results.where('results.created_at <= ?', params[:created_at_to]) if params[:created_at_to]
@results = @results.where('updated_at >= ?', params[:updated_at_from]) if params[:updated_at_from] @results = @results.where('results.updated_at >= ?', params[:updated_at_from]) if params[:updated_at_from]
@results = @results.where('updated_at <= ?', params[:updated_at_to]) if params[:updated_at_to] @results = @results.where('results.updated_at <= ?', params[:updated_at_to]) if params[:updated_at_to]
end end
def load_my_module def load_my_module

View file

@ -75,6 +75,9 @@ export default {
} }
}, },
updateDate(date) { updateDate(date) {
if (this.date && date && (this.date.setSeconds(0, 0) === date.setSeconds(0, 0))) {
return;
}
this.date = date; this.date = date;
this.updateValue(); this.updateValue();
}, },

View file

@ -26,45 +26,42 @@
<form class="flex flex-col gap-6" @submit.prevent novalidate> <form class="flex flex-col gap-6" @submit.prevent novalidate>
<fieldset class="w-full flex justify-between"> <fieldset class="w-full flex justify-between">
<div class="flex flex-col w-40"> <div class="flex flex-col w-40">
<label class="text-sn-grey text-sm font-normal" for="operations">{{ i18n.t('repository_stock_values.manage_modal.operation') }}</label> <label class="sci-label" for="operations">{{ i18n.t('repository_stock_values.manage_modal.operation') }}</label>
<SelectDropdown <Select
:disabled="!stockValue?.id" :disabled="!stockValue?.id"
:value="operation" :value="operation"
:options="operations" :options="operations"
@change="setOperation" @change="setOperation"
/> ></Select>
</div> </div>
<div class="flex flex-col w-40"> <div class="flex flex-col w-40">
<Input <Input
@update="value => amount = value" @update="value => amount = value"
name="stock_amount" name="stock_amount"
id="stock-amount" id="stock-amount"
:inputClass="`sci-input-container-v2 ${errors.amount ? 'error' : ''}`" type="number"
:labelClass="`text-sm font-normal ${errors.amount ? 'text-sn-delete-red' : 'text-sn-grey'}`"
:type="Number(stockValue.decimals) === 0 ? 'number' : 'text'"
:value="amount" :value="amount"
:decimals="stockValue.decimals" :decimals="stockValue.decimals"
:placeholder="i18n.t('repository_stock_values.manage_modal.amount_placeholder_new')" :placeholder="i18n.t('repository_stock_values.manage_modal.amount_placeholder_new')"
required
:label="i18n.t('repository_stock_values.manage_modal.amount')" :label="i18n.t('repository_stock_values.manage_modal.amount')"
showLabel :required="true"
autoFocus :min="0"
:error="errors.amount" :error="errors.amount"
/> />
</div> </div>
<div class="flex flex-col w-40"> <div class="flex flex-col w-40">
<label :class="`text-sm font-normal ${errors.unit ? 'text-sn-delete-red' : 'text-sn-grey'}`" for="stock-unit"> <label class="sci-label" :class="{ 'error': !!errors.unit }" for="stock-unit">
{{ i18n.t('repository_stock_values.manage_modal.unit') }} {{ i18n.t('repository_stock_values.manage_modal.unit') }}
</label> </label>
<SelectDropdown <Select
:disabled="['add', 'remove'].includes(operation)" :disabled="['add', 'remove'].includes(operation)"
:value="unit" :value="unit"
:options="units" :options="units"
:placeholder="i18n.t('repository_stock_values.manage_modal.unit_prompt')" :placeholder="i18n.t('repository_stock_values.manage_modal.unit_prompt')"
@change="unit = $event" @change="unit = $event"
:className="`${errors.unit ? 'error' : ''}`" :className="`${errors.unit ? 'error' : ''}`"
/> ></Select>
<div class="text-sn-delete-red text-xs" :class="{ visible: errors.unit, invisible: !errors.unit }"> <div class="text-sn-coral text-xs" :class="{ visible: errors.unit, invisible: !errors.unit }">
{{ errors.unit }} {{ errors.unit }}
</div> </div>
</div> </div>
@ -93,24 +90,21 @@
</div> </div>
<span class="ml-2">{{ i18n.t('repository_stock_values.manage_modal.create_reminder') }}</span> <span class="ml-2">{{ i18n.t('repository_stock_values.manage_modal.create_reminder') }}</span>
</div> </div>
<div v-if="reminderEnabled" class="stock-reminder-value flex gap-2 items-center"> <div v-if="reminderEnabled" class="stock-reminder-value flex gap-2 items-center w-40">
<Input <Input
@update="value => lowStockTreshold = value" @update="value => lowStockTreshold = value"
name="treshold_amount" name="treshold_amount"
id="treshold-amount" id="treshold-amount"
fieldClass="flex gap-2" type="number"
inputClass="sci-input-container-v2 w-40"
labelClass="text-sm font-normal flex items-center"
:type="Number(stockValue.decimals) === 0 ? 'number' : 'text'"
:value="lowStockTreshold" :value="lowStockTreshold"
:decimals="stockValue.decimals" :decimals="stockValue.decimals"
:placeholder="i18n.t('repository_stock_values.manage_modal.amount_placeholder_new')" :placeholder="i18n.t('repository_stock_values.manage_modal.amount_placeholder_new')"
required :required="true"
:label="i18n.t('repository_stock_values.manage_modal.reminder_at')" :label="i18n.t('repository_stock_values.manage_modal.reminder_at')"
showLabel :min="0"
:error="errors.tresholdAmount" :error="errors.tresholdAmount"
/> />
<span class="text-sm font-normal"> <span class="text-sm font-normal mt-5">
{{ unitLabel }} {{ unitLabel }}
</span> </span>
</div> </div>
@ -130,7 +124,7 @@
<button type='button' class='btn btn-secondary' data-dismiss='modal'> <button type='button' class='btn btn-secondary' data-dismiss='modal'>
{{ i18n.t('general.cancel') }} {{ i18n.t('general.cancel') }}
</button> </button>
<button class="btn btn-primary" @click="validateAndsaveStockValue"> <button class="btn btn-primary" @click="validateAndsaveStockValue" :disabled="isSaving">
{{ i18n.t('repository_stock_values.manage_modal.save_stock') }} {{ i18n.t('repository_stock_values.manage_modal.save_stock') }}
</button> </button>
</div> </div>
@ -141,13 +135,13 @@
<script> <script>
import Decimal from 'decimal.js'; import Decimal from 'decimal.js';
import Input from '../shared/legacy/input.vue'; import Select from '../shared/select.vue';
import SelectDropdown from '../shared/select_dropdown.vue'; import Input from '../shared/input.vue';
export default { export default {
name: 'ManageStockValueModal', name: 'ManageStockValueModal',
components: { components: {
SelectDropdown, Select,
Input Input
}, },
data() { data() {
@ -155,12 +149,13 @@ export default {
operation: null, operation: null,
operations: [], operations: [],
stockValue: null, stockValue: null,
amount: '', amount: null,
repositoryRowName: null, repositoryRowName: null,
stockUrl: null, stockUrl: null,
units: null, units: null,
unit: null, unit: null,
reminderEnabled: false, reminderEnabled: false,
isSaving: false,
lowStockTreshold: null, lowStockTreshold: null,
comment: null, comment: null,
errors: {} errors: {}
@ -176,7 +171,7 @@ export default {
return unit ? unit[1] : ''; return unit ? unit[1] : '';
}, },
newAmount() { newAmount() {
const currentAmount = new Decimal(this.stockValue?.amount || 0); const currentAmount = this.stockValue?.amount ? new Decimal(this.stockValue?.amount || 0) : null;
const amount = new Decimal(this.amount || 0); const amount = new Decimal(this.amount || 0);
let value; let value;
switch (this.operation) { switch (this.operation) {
@ -226,7 +221,7 @@ export default {
success: (result) => { success: (result) => {
this.repositoryRowName = result.repository_row_name; this.repositoryRowName = result.repository_row_name;
this.stockValue = result.stock_value; this.stockValue = result.stock_value;
this.amount = Number(new Decimal(result.stock_value.amount || 0)); this.amount = this.stockValue?.amount && Number(new Decimal(this.stockValue.amount));
this.units = result.stock_value.units; this.units = result.stock_value.units;
this.unit = result.stock_value.unit; this.unit = result.stock_value.unit;
this.reminderEnabled = result.stock_value.reminder_enabled; this.reminderEnabled = result.stock_value.reminder_enabled;
@ -263,6 +258,7 @@ export default {
this.errors = newErrors; this.errors = newErrors;
if (!$.isEmptyObject(newErrors)) return; if (!$.isEmptyObject(newErrors)) return;
this.isSaving = true;
const $this = this; const $this = this;
$.ajax({ $.ajax({
@ -282,6 +278,7 @@ export default {
}, },
success: (result) => { success: (result) => {
$this.stockValue = null; $this.stockValue = null;
$this.isSaving = false;
$this.closeModal(); $this.closeModal();
$this.closeCallback && $this.closeCallback(result); $this.closeCallback && $this.closeCallback(result);
} }

View file

@ -91,6 +91,9 @@ export default {
}, },
computed: { computed: {
attachmentsOrdered() { attachmentsOrdered() {
if (this.attachments.some((attachment) => attachment.attributes.uploading)) {
return this.attachments;
}
return this.attachments.sort((a, b) => { return this.attachments.sort((a, b) => {
if (a.attributes.asset_order == b.attributes.asset_order) { if (a.attributes.asset_order == b.attributes.asset_order) {
switch (this.parent.attributes.assets_order) { switch (this.parent.attributes.assets_order) {

View file

@ -40,8 +40,8 @@
:class="{ 'text-sn-grey font-normal': isBlank, 'whitespace-pre-line py-1': !singleLine }" :class="{ 'text-sn-grey font-normal': isBlank, 'whitespace-pre-line py-1': !singleLine }"
@click="enableEdit($event)" @click="enableEdit($event)"
> >
<span :class="{'truncate': singleLine }" v-if="smartAnnotation" v-html="sa_value || placeholder" ></span> <span :class="{'truncate': singleLine }" :title="sa_value || placeholder" v-if="smartAnnotation" v-html="sa_value || placeholder" ></span>
<span :class="{'truncate': singleLine}" v-else>{{newValue || placeholder}}</span> <span :class="{'truncate': singleLine}" :title="newValue || placeholder" v-else>{{newValue || placeholder}}</span>
</div> </div>
<div <div

View file

@ -1,75 +1,83 @@
<template> <template>
<div class="relative" :class="fieldClass"> <div class="relative w-full">
<label v-if="showLabel" :class="labelClass" :for="id">{{ label }}</label> <label v-if="label" class="sci-label" :class="{ 'error': !!inputError }" :for="id">{{ label }}</label>
<div :class="inputClass"> <div class="sci-input-container-v2" :class="{ 'error': !!inputError }" :data-error-text="inputError">
<input ref="input" <input ref="input"
:type="type" lang="en-US"
type="text"
:id="id" :id="id"
:name="name" :name="name"
:value="inputValue" :value="value"
:class="`${error ? 'error' : ''}`" :class="{ 'error': !!inputError }"
:placeholder="placeholder" :placeholder="placeholder"
:required="required" inputmode="numeric"
:min="min"
:max="max"
:step="1/Math.pow(10, decimals)"
:inputmode="(type === 'number') && 'numeric'"
:pattern="pattern"
@input="updateValue" @input="updateValue"
/> />
<div
class="mt-2 text-sn-delete-red whitespace-nowrap truncate text-xs font-normal absolute bottom-[-1rem] w-full"
:title="error"
:class="{ visible: error, invisible: !error}"
>
{{ error }}
</div>
</div> </div>
</div> </div>
</template> </template>
<script> <script>
export default { export default {
name: 'Input', name: 'Input',
props: { props: {
id: { type: String, required: false }, id: { type: String, required: false },
fieldClass: { type: String, default: '' },
inputClass: { type: String, default: '' },
labelClass: { type: String, default: '' },
type: { type: String, default: 'text' }, type: { type: String, default: 'text' },
name: { type: String, required: true }, name: { type: String, required: true },
value: { type: [String, Number], required: false }, value: { type: [String, Number], required: false },
decimals: { type: [Number, String], default: 0 }, decimals: { type: [Number, String], default: 0 },
placeholder: { type: String, default: '' }, placeholder: { type: String, default: '' },
required: { type: Boolean, default: false }, required: { type: Boolean, default: false },
showLabel: { type: Boolean, default: false },
label: { type: String, required: false }, label: { type: String, required: false },
autoFocus: { type: Boolean, default: false }, error: { type: String, required: false },
error: { type: String, required: false } min: { type: String },
max: { type: String },
blockInvalidInput: { type: Boolean, default: true }
},
data() {
return {
inputError: false,
lastValue: this.value || ''
};
},
watch: {
value() {
this.lastValue = this.value;
}
}, },
computed: { computed: {
inputValue() { pattern() {
if (this.type === 'text') return this.value; if (this.type === 'number' && this.decimals) {
return `[0-9]+([\\.][0-9]{0,${this.decimals}})?`;
} else if (this.type === 'number') {
return '[0-9]+';
}
return isNaN(this.value) ? '' : this.value; return null;
} }
}, },
methods: { methods: {
updateValue($event) { updateValue($event) {
switch (this.type) { this.checkInputError($event);
case 'text': this.$emit('update', $event.target.value);
this.$emit('update', $event.target.value);
break;
case 'number':
const newValue = this.formatDecimalValue($event.target.value);
this.$refs.input.value = newValue;
if (!isNaN(newValue)) this.$emit('update', newValue);
break;
default:
break;
}
}, },
formatDecimalValue(value) { checkInputError() {
const decimalValue = value.replace(/[^-0-9.]/g, ''); const isValid = this.$refs.input.checkValidity();
if (this.decimals === '0') {
return decimalValue.split('.')[0]; if (isValid) {
this.lastValue = this.$refs.input.value;
} else if (this.blockInvalidInput) {
this.$refs.input.value = this.lastValue;
return;
} }
return decimalValue.match(new RegExp(`^-?\\d*(\\.\\d{0,${this.decimals}})?`))[0];
this.inputError = this.error || (!isValid && this.i18n.t('input.errors.invalid_value'));
} }
} }
}; };

View file

@ -349,15 +349,15 @@ class MyModule < ApplicationRecord
rows.find_each do |row| rows.find_each do |row|
row_json = [] row_json = []
row_json << row.code row_json << row.code
row_json << (row.archived ? "#{row.name} [#{I18n.t('general.archived')}]" : row.name) row_json << (row.archived ? "#{escape_script_tag(row.name)} [#{I18n.t('general.archived')}]" : escape_script_tag(row.name))
row_json << I18n.l(row.created_at, format: :full) row_json << I18n.l(row.created_at, format: :full)
row_json << row.created_by.full_name row_json << escape_script_tag(row.created_by.full_name)
if repository.has_stock_management? && repository.has_stock_consumption? if repository.has_stock_management? && repository.has_stock_consumption?
if repository.is_a?(RepositorySnapshot) if repository.is_a?(RepositorySnapshot)
consumed_stock = row.repository_stock_consumption_cell&.value&.formatted consumed_stock = escape_script_tag(row.repository_stock_consumption_cell&.value&.formatted)
row_json << (consumed_stock || 0) row_json << (consumed_stock || 0)
else else
row_json << row.row_consumption(row.stock_consumption) row_json << escape_script_tag(row.row_consumption(row.stock_consumption))
end end
end end
data << row_json data << row_json
@ -488,6 +488,10 @@ class MyModule < ApplicationRecord
protocols << Protocol.new_blank_for_module(self) protocols << Protocol.new_blank_for_module(self)
end end
def escape_script_tag(value)
value&.gsub(/\</, '&lt;')&.gsub(/\>/, '&gt;')
end
def coordinates_uniqueness_check def coordinates_uniqueness_check
if experiment && experiment.my_modules.active.where(x: x, y: y).where.not(id: id).any? if experiment && experiment.my_modules.active.where(x: x, y: y).where.not(id: id).any?
errors.add(:position, I18n.t('activerecord.errors.models.my_module.attributes.position.not_unique')) errors.add(:position, I18n.t('activerecord.errors.models.my_module.attributes.position.not_unique'))

View file

@ -26,8 +26,8 @@
</div> </div>
</div> </div>
<div class="report-element-body"> <div class="report-element-body">
<input type="hidden" class="hot-table-contents" value="<%= table.contents_utf_8 %>" /> <input type="hidden" class="hot-table-contents" value="<%= table.contents_utf_8.gsub(/\</, '&lt;').gsub(/\>/, '&gt;') %>" />
<input type="hidden" class="hot-table-metadata" value="<%= table.metadata.to_json %>" /> <input type="hidden" class="hot-table-metadata" value="<%= table.metadata.to_json.gsub(/\</, '&lt;').gsub(/\>/, '&gt;') %>" />
<div class="hot-table-container"></div> <div class="hot-table-container"></div>
<table class="report-common-table-format"></table> <table class="report-common-table-format"></table>
</div> </div>

View file

@ -23,8 +23,8 @@
</div> </div>
</div> </div>
<div class="report-element-body"> <div class="report-element-body">
<input type="hidden" class="hot-table-contents" value="<%= table.contents_utf_8 %>" /> <input type="hidden" class="hot-table-contents" value="<%= table.contents_utf_8.gsub(/\</, '&lt;').gsub(/\>/, '&gt;') %>" />
<input type="hidden" class="hot-table-metadata" value="<%= table.metadata.to_json %>" /> <input type="hidden" class="hot-table-metadata" value="<%= table.metadata.to_json.gsub(/\</, '&lt;').gsub(/\>/, '&gt;') %>" />
<div class="hot-table-container"></div> <div class="hot-table-container"></div>
<table class="report-common-table-format"></table> <table class="report-common-table-format"></table>
</div> </div>

View file

@ -22,7 +22,7 @@
</div> </div>
</div> </div>
<ul class="experiment-contents collapse in" id="experimentContentContainer<%= experiment.id %>"> <ul class="experiment-contents collapse in" id="experimentContentContainer<%= experiment.id %>">
<% experiment.my_modules.viewable_by_user(current_user, current_team).active.sort_by { |m| @project_contents[:my_modules].index(m.id) || @project_contents[:my_modules].length if report }.each do |my_module| %> <% experiment.my_modules.viewable_by_user(current_user, current_team).active.order(id: :asc).each do |my_module| %>
<li class="experiment-my-module"> <li class="experiment-my-module">
<span class="sci-checkbox-container"> <span class="sci-checkbox-container">
<input type="checkbox" value="<%= my_module.id %>" class="sci-checkbox report-my-module-checkbox" <input type="checkbox" value="<%= my_module.id %>" class="sci-checkbox report-my-module-checkbox"

View file

@ -16,10 +16,12 @@
<div class="form-group sci-input-container"> <div class="form-group sci-input-container">
<label><%= t("repositories.index.modal_copy.name") %></label> <label><%= t("repositories.index.modal_copy.name") %></label>
<%= f.text_field :name, <%= f.text_field :name,
help: t("repositories.index.modal_copy.description"),
autofocus: true, autofocus: true,
class: "sci-input-field", class: "sci-input-field",
placeholder: t("repositories.index.modal_copy.name_placeholder") %> placeholder: t("repositories.index.modal_copy.name_placeholder") %>
<div class="text-red-500">
<%= t("repositories.index.modal_copy.description") %>
</div>
</div> </div>
</div> </div>
</div> </div>

View file

@ -12,5 +12,5 @@
data: { min: 0, max: Constants::REPOSITORY_NUMBER_TYPE_MAX_DECIMALS }) %> data: { min: 0, max: Constants::REPOSITORY_NUMBER_TYPE_MAX_DECIMALS }) %>
</div> </div>
<%= javascript_include_tag 'repository_columns/manage_column_partials/number' %> <%= javascript_include_tag 'repository_columns/manage_column_partials/number', nonce: true %>
</div> </div>

View file

@ -13,7 +13,7 @@
class: 'form-control stock-column', class: 'form-control stock-column',
data: { min: 0, max: Constants::REPOSITORY_NUMBER_TYPE_MAX_DECIMALS }) %> data: { min: 0, max: Constants::REPOSITORY_NUMBER_TYPE_MAX_DECIMALS }) %>
<%= javascript_include_tag 'repository_columns/manage_column_partials/stock' %> <%= javascript_include_tag 'repository_columns/manage_column_partials/stock', nonce: true %>
</div> </div>
</div> </div>
<div class="form-group"> <div class="form-group">

View file

@ -4086,6 +4086,10 @@ en:
From: 'From' From: 'From'
To: 'To' To: 'To'
input:
errors:
invalid_value: 'Invalid value'
marvinjs: marvinjs:
new_sketch: "New chemical drawing" new_sketch: "New chemical drawing"
new_button: "New chemical drawing" new_button: "New chemical drawing"