mirror of
https://github.com/scinote-eln/scinote-web.git
synced 2025-11-09 16:01:30 +08:00
Add item field to form builder [SCI-11685]
This commit is contained in:
parent
da942edb75
commit
2afd249e8c
12 changed files with 218 additions and 10 deletions
|
|
@ -65,6 +65,8 @@ class RepositoriesController < ApplicationController
|
||||||
|
|
||||||
results = results.order('LOWER(repository_rows.name) asc').page(params[:page])
|
results = results.order('LOWER(repository_rows.name) asc').page(params[:page])
|
||||||
|
|
||||||
|
results = results.where.not(id: params[:excluded_ids]) if params[:excluded_ids].present?
|
||||||
|
|
||||||
render json: {
|
render json: {
|
||||||
paginated: true,
|
paginated: true,
|
||||||
next_page: results.next_page,
|
next_page: results.next_page,
|
||||||
|
|
|
||||||
|
|
@ -86,6 +86,7 @@ import SingleChoiceField from './edit_fields/single_choice.vue';
|
||||||
import TextField from './edit_fields/text.vue';
|
import TextField from './edit_fields/text.vue';
|
||||||
import MultipleChoiceField from './edit_fields/multiple_choice.vue';
|
import MultipleChoiceField from './edit_fields/multiple_choice.vue';
|
||||||
import ActionField from './edit_fields/action.vue';
|
import ActionField from './edit_fields/action.vue';
|
||||||
|
import RepositoryRowsField from './edit_fields/repository_rows.vue';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'EditField',
|
name: 'EditField',
|
||||||
|
|
@ -100,7 +101,8 @@ export default {
|
||||||
SingleChoiceField,
|
SingleChoiceField,
|
||||||
TextField,
|
TextField,
|
||||||
MultipleChoiceField,
|
MultipleChoiceField,
|
||||||
ActionField
|
ActionField,
|
||||||
|
RepositoryRowsField
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
|
|
||||||
9
app/javascript/vue/forms/edit_fields/repository_rows.vue
Normal file
9
app/javascript/vue/forms/edit_fields/repository_rows.vue
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
<template>
|
||||||
|
<hr class="my-4 w-full">
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'RepositoryRowsField'
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
@ -47,6 +47,7 @@ import SingleChoiceField from './fields/single_choice.vue';
|
||||||
import TextField from './fields/text.vue';
|
import TextField from './fields/text.vue';
|
||||||
import MultipleChoiceField from './fields/multiple_choice.vue';
|
import MultipleChoiceField from './fields/multiple_choice.vue';
|
||||||
import ActionField from './fields/action.vue';
|
import ActionField from './fields/action.vue';
|
||||||
|
import RepositoryRowsField from './fields/repository_rows.vue';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'ViewField',
|
name: 'ViewField',
|
||||||
|
|
@ -64,7 +65,8 @@ export default {
|
||||||
SingleChoiceField,
|
SingleChoiceField,
|
||||||
TextField,
|
TextField,
|
||||||
MultipleChoiceField,
|
MultipleChoiceField,
|
||||||
ActionField
|
ActionField,
|
||||||
|
RepositoryRowsField
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
|
|
||||||
65
app/javascript/vue/forms/fields/modals/row_selector.vue
Normal file
65
app/javascript/vue/forms/fields/modals/row_selector.vue
Normal file
|
|
@ -0,0 +1,65 @@
|
||||||
|
<template>
|
||||||
|
<div ref="modal" class="modal" tabindex="-1" role="dialog">
|
||||||
|
<div class="modal-dialog" role="document">
|
||||||
|
<form @submit.prevent="submit">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
||||||
|
<i class="sn-icon sn-icon-close"></i>
|
||||||
|
</button>
|
||||||
|
<h4 class="modal-title truncate !block" >
|
||||||
|
{{ i18n.t('forms.show.select_items') }}
|
||||||
|
</h4>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<RepositoryRowSelector
|
||||||
|
:multiple="true"
|
||||||
|
@change="selectedRows = $event"
|
||||||
|
@repositoryChange="changeSelectedInventory"
|
||||||
|
:excludeRows="excludeRows"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" data-dismiss="modal">{{ i18n.t('general.cancel') }}</button>
|
||||||
|
<button type="button" class="btn btn-primary" @click="save"> {{ i18n.t('forms.show.add') }}</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
|
||||||
|
import modalMixin from '../../../shared/modal_mixin.js';
|
||||||
|
import RepositoryRowSelector from '../../../shared/repository_row_selector.vue';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'RowSelectorModal',
|
||||||
|
props: {
|
||||||
|
excludeRows: {
|
||||||
|
type: Array,
|
||||||
|
default: () => []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mixins: [modalMixin],
|
||||||
|
components: {
|
||||||
|
RepositoryRowSelector
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
selectedRepository: null,
|
||||||
|
selectedRows: []
|
||||||
|
};
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
changeSelectedInventory(repository) {
|
||||||
|
this.selectedRepository = repository;
|
||||||
|
this.selectedRows = [];
|
||||||
|
},
|
||||||
|
save() {
|
||||||
|
this.$emit('save', this.selectedRows);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
85
app/javascript/vue/forms/fields/repository_rows.vue
Normal file
85
app/javascript/vue/forms/fields/repository_rows.vue
Normal file
|
|
@ -0,0 +1,85 @@
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<button v-if="!disabled" class="btn btn-secondary" @click="rowSelectorOpened = true">
|
||||||
|
{{ i18n.t('forms.show.add_items') }}
|
||||||
|
<i class="sn-icon sn-icon-right"></i>
|
||||||
|
</button>
|
||||||
|
<RowSelectorModal v-if="rowSelectorOpened"
|
||||||
|
:excludeRows="assignedIds"
|
||||||
|
@close="rowSelectorOpened = false"
|
||||||
|
@save="addValue" />
|
||||||
|
<div class="flex items-center gap-2 mt-4 flex-wrap">
|
||||||
|
<template v-for="(row, index) in field.field_value.value" :key="row.id">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<span v-if="index > 0">|</span>
|
||||||
|
<a
|
||||||
|
class="cursor-pointer text-sn-blue record-info-link"
|
||||||
|
:href="itemCardUrl(row)"
|
||||||
|
>{{ row.name }} (ID{{ row.id }})</a>
|
||||||
|
<i v-if="!disabled" @click="removeValue(row.id)" class="sn-icon sn-icon-unlink cursor-pointer"></i>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
/* global */
|
||||||
|
|
||||||
|
import fieldMixin from './field_mixin';
|
||||||
|
import RowSelectorModal from './modals/row_selector.vue';
|
||||||
|
import {
|
||||||
|
repository_repository_row_path,
|
||||||
|
|
||||||
|
} from '../../../routes.js';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'RepositoryRowsField',
|
||||||
|
mixins: [fieldMixin],
|
||||||
|
components: {
|
||||||
|
RowSelectorModal
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
rowSelectorOpened: false
|
||||||
|
};
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
marked_as_na() {
|
||||||
|
if (this.marked_as_na) {
|
||||||
|
this.value = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
assignedIds() {
|
||||||
|
return this.field.field_value?.value?.map((row) => row.id) || [];
|
||||||
|
},
|
||||||
|
validValue() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
itemCardUrl(row) {
|
||||||
|
return repository_repository_row_path(row.repository_id, row.id);
|
||||||
|
},
|
||||||
|
addValue(rows) {
|
||||||
|
const rowIds = this.assignedIds;
|
||||||
|
rows.forEach((row) => {
|
||||||
|
if (!rowIds.includes(row)) {
|
||||||
|
rowIds.push(row);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.rowSelectorOpened = false;
|
||||||
|
this.$emit('save', rowIds.length > 0 ? rowIds : null);
|
||||||
|
},
|
||||||
|
removeValue(id) {
|
||||||
|
const rowIds = this.assignedIds;
|
||||||
|
rowIds.splice(rowIds.indexOf(id), 1);
|
||||||
|
this.$emit('save', rowIds.length > 0 ? rowIds : null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
@ -158,7 +158,8 @@ export default {
|
||||||
{ name: this.i18n.t('forms.show.blocks.SingleChoiceField'), type: 'SingleChoiceField' },
|
{ name: this.i18n.t('forms.show.blocks.SingleChoiceField'), type: 'SingleChoiceField' },
|
||||||
{ name: this.i18n.t('forms.show.blocks.MultipleChoiceField'), type: 'MultipleChoiceField' },
|
{ name: this.i18n.t('forms.show.blocks.MultipleChoiceField'), type: 'MultipleChoiceField' },
|
||||||
{ name: this.i18n.t('forms.show.blocks.DatetimeField'), type: 'DatetimeField' },
|
{ name: this.i18n.t('forms.show.blocks.DatetimeField'), type: 'DatetimeField' },
|
||||||
{ name: this.i18n.t('forms.show.blocks.ActionField'), type: 'ActionField' }
|
{ name: this.i18n.t('forms.show.blocks.ActionField'), type: 'ActionField' },
|
||||||
|
{ name: this.i18n.t('forms.show.blocks.RepositoryRowsField'), type: 'RepositoryRowsField' }
|
||||||
];
|
];
|
||||||
},
|
},
|
||||||
fieldIcon() {
|
fieldIcon() {
|
||||||
|
|
@ -168,7 +169,8 @@ export default {
|
||||||
SingleChoiceField: 'sn-icon-choice-single',
|
SingleChoiceField: 'sn-icon-choice-single',
|
||||||
MultipleChoiceField: 'sn-icon-choice-multiple',
|
MultipleChoiceField: 'sn-icon-choice-multiple',
|
||||||
DatetimeField: 'sn-icon-created',
|
DatetimeField: 'sn-icon-created',
|
||||||
ActionField: 'sn-icon-check'
|
ActionField: 'sn-icon-check',
|
||||||
|
RepositoryRowsField: 'sn-icon-inventory'
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@
|
||||||
:optionsUrl="repositoriesUrl"
|
:optionsUrl="repositoriesUrl"
|
||||||
placeholder="Select inventory"
|
placeholder="Select inventory"
|
||||||
:searchable="true"
|
:searchable="true"
|
||||||
|
:value="selectedRepository"
|
||||||
@change="selectedRepository = $event"
|
@change="selectedRepository = $event"
|
||||||
></SelectDropdown>
|
></SelectDropdown>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -15,12 +16,14 @@
|
||||||
:key="selectedRepository"
|
:key="selectedRepository"
|
||||||
:disabled="!selectedRepository"
|
:disabled="!selectedRepository"
|
||||||
:optionsUrl="rowsUrl"
|
:optionsUrl="rowsUrl"
|
||||||
:urlParams="{ repository_id: selectedRepository }"
|
:urlParams="{ repository_id: selectedRepository, excluded_ids: excludeRows }"
|
||||||
|
ajaxMethod="post"
|
||||||
placeholder="Select item"
|
placeholder="Select item"
|
||||||
:multiple="multiple"
|
:multiple="multiple"
|
||||||
:withCheckboxes="multiple"
|
:withCheckboxes="multiple"
|
||||||
:searchable="true"
|
:searchable="true"
|
||||||
:optionRenderer="itemRowOptionRenderer"
|
:optionRenderer="itemRowOptionRenderer"
|
||||||
|
:value="selectedRow"
|
||||||
@close="showItemInfo = false"
|
@close="showItemInfo = false"
|
||||||
@change="selectedRow= $event"
|
@change="selectedRow= $event"
|
||||||
></SelectDropdown>
|
></SelectDropdown>
|
||||||
|
|
@ -63,10 +66,32 @@ export default {
|
||||||
multiple: {
|
multiple: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false
|
default: false
|
||||||
|
},
|
||||||
|
preSelectedRepository: {
|
||||||
|
type: Number,
|
||||||
|
default: null
|
||||||
|
},
|
||||||
|
preSelectedRows: {
|
||||||
|
type: [Array],
|
||||||
|
default: null
|
||||||
|
},
|
||||||
|
excludeRows: {
|
||||||
|
type: Array,
|
||||||
|
default: () => []
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
created() {
|
created() {
|
||||||
this.teamId = document.body.dataset.currentTeamId;
|
this.teamId = document.body.dataset.currentTeamId;
|
||||||
|
if (this.preSelectedRepository) {
|
||||||
|
this.selectedRepository = this.preSelectedRepository;
|
||||||
|
}
|
||||||
|
if (this.preSelectedRows) {
|
||||||
|
this.selectedRow = this.preSelectedRows;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.loading = false;
|
||||||
|
});
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
document.addEventListener('mouseover', this.loadColumnsInfo);
|
document.addEventListener('mouseover', this.loadColumnsInfo);
|
||||||
|
|
@ -76,11 +101,15 @@ export default {
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
selectedRepository() {
|
selectedRepository() {
|
||||||
|
if (this.loading) return;
|
||||||
|
|
||||||
this.selectedRow = null;
|
this.selectedRow = null;
|
||||||
this.$emit('repositoryChange', this.selectedRepository);
|
this.$emit('repositoryChange', this.selectedRepository);
|
||||||
this.$emit('change', this.selectedRow);
|
this.$emit('change', this.selectedRow);
|
||||||
},
|
},
|
||||||
selectedRow() {
|
selectedRow() {
|
||||||
|
if (this.loading) return;
|
||||||
|
|
||||||
this.$emit('change', this.selectedRow);
|
this.$emit('change', this.selectedRow);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
@ -103,7 +132,8 @@ export default {
|
||||||
teamId: null,
|
teamId: null,
|
||||||
showItemInfo: false,
|
showItemInfo: false,
|
||||||
hoveredRow: {},
|
hoveredRow: {},
|
||||||
loadingHoveredRow: false
|
loadingHoveredRow: false,
|
||||||
|
loading: true
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
|
|
||||||
|
|
@ -129,7 +129,8 @@ export default {
|
||||||
clearable: { type: Boolean, default: false },
|
clearable: { type: Boolean, default: false },
|
||||||
tagsView: { type: Boolean, default: false },
|
tagsView: { type: Boolean, default: false },
|
||||||
urlParams: { type: Object, default: () => ({}) },
|
urlParams: { type: Object, default: () => ({}) },
|
||||||
e2eValue: { type: String, default: '' }
|
e2eValue: { type: String, default: '' },
|
||||||
|
ajaxMethod: { type: String, default: 'get' }
|
||||||
},
|
},
|
||||||
directives: {
|
directives: {
|
||||||
'click-outside': vOnClickOutside
|
'click-outside': vOnClickOutside
|
||||||
|
|
@ -369,7 +370,7 @@ export default {
|
||||||
fetchOptions() {
|
fetchOptions() {
|
||||||
if (this.optionsUrl) {
|
if (this.optionsUrl) {
|
||||||
const params = { query: this.query, page: this.nextPage, ...this.urlParams };
|
const params = { query: this.query, page: this.nextPage, ...this.urlParams };
|
||||||
axios.get(this.optionsUrl, { params })
|
axios({ method: this.ajaxMethod, url: this.optionsUrl, data: params })
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
if (response.data.paginated) {
|
if (response.data.paginated) {
|
||||||
this.fetchedOptions = [...this.fetchedOptions, ...response.data.data];
|
this.fetchedOptions = [...this.fetchedOptions, ...response.data.data];
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,12 @@ class FormRepositoryRowsFieldValue < FormFieldValue
|
||||||
# adds new snapshots if not yet present
|
# adds new snapshots if not yet present
|
||||||
self.data ||= []
|
self.data ||= []
|
||||||
|
|
||||||
|
# If income data is empty, we need nulify data
|
||||||
|
if repository_row_ids.blank?
|
||||||
|
self.data = nil
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
removed_repository_row_ids = data.pluck('id') - repository_row_ids
|
removed_repository_row_ids = data.pluck('id') - repository_row_ids
|
||||||
existing_repository_row_ids = data.pluck('id') & repository_row_ids
|
existing_repository_row_ids = data.pluck('id') & repository_row_ids
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1156,6 +1156,9 @@ en:
|
||||||
no_block: 'Add your first form block'
|
no_block: 'Add your first form block'
|
||||||
field_too_long_error: 'Field is too long (maximum number is %{limit} characters)'
|
field_too_long_error: 'Field is too long (maximum number is %{limit} characters)'
|
||||||
options_too_many_error: 'Too many options (maximum number is %{limit} options)'
|
options_too_many_error: 'Too many options (maximum number is %{limit} options)'
|
||||||
|
add_items: 'Add items'
|
||||||
|
add: 'Add'
|
||||||
|
select_items: 'Select items'
|
||||||
validations:
|
validations:
|
||||||
response_validation:
|
response_validation:
|
||||||
title: 'Response validation'
|
title: 'Response validation'
|
||||||
|
|
@ -1169,6 +1172,7 @@ en:
|
||||||
MultipleChoiceField: 'Multiple choice'
|
MultipleChoiceField: 'Multiple choice'
|
||||||
DatetimeField: 'Date & Time'
|
DatetimeField: 'Date & Time'
|
||||||
ActionField: 'Action'
|
ActionField: 'Action'
|
||||||
|
RepositoryRowsField: 'Item'
|
||||||
response:
|
response:
|
||||||
submit: 'Submit form'
|
submit: 'Submit form'
|
||||||
submitted_on: 'Submitted on'
|
submitted_on: 'Submitted on'
|
||||||
|
|
@ -3587,7 +3591,7 @@ en:
|
||||||
header:
|
header:
|
||||||
printed_from: "Printed from"
|
printed_from: "Printed from"
|
||||||
print_info: "on %{datetime} by %{full_name}"
|
print_info: "on %{datetime} by %{full_name}"
|
||||||
forms:
|
forms:
|
||||||
single_choice_html: "<em>(Select one)</em>"
|
single_choice_html: "<em>(Select one)</em>"
|
||||||
multiple_choice_html: "<em>(Select multiple)</em>"
|
multiple_choice_html: "<em>(Select multiple)</em>"
|
||||||
protocols_io_import:
|
protocols_io_import:
|
||||||
|
|
|
||||||
|
|
@ -209,7 +209,7 @@ Rails.application.routes.draw do
|
||||||
defaults: { format: 'json' }
|
defaults: { format: 'json' }
|
||||||
post 'actions_toolbar'
|
post 'actions_toolbar'
|
||||||
get :list
|
get :list
|
||||||
get :rows_list
|
post :rows_list
|
||||||
end
|
end
|
||||||
member do
|
member do
|
||||||
get :export_empty_repository
|
get :export_empty_repository
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue