mirror of
https://github.com/scinote-eln/scinote-web.git
synced 2025-10-06 03:46:39 +08:00
Add form interacrtions to step [SCI-11341][SCI-11381]
This commit is contained in:
parent
07b6a9e8db
commit
f9d7066d55
24 changed files with 416 additions and 114 deletions
|
@ -9,7 +9,8 @@ class FormFieldValuesController < ApplicationController
|
||||||
@form_field_value = @form_response.create_value!(
|
@form_field_value = @form_response.create_value!(
|
||||||
current_user,
|
current_user,
|
||||||
@form_field,
|
@form_field,
|
||||||
form_field_value_params[:value]
|
form_field_value_params[:value],
|
||||||
|
not_applicable: form_field_value_params[:not_applicable]
|
||||||
)
|
)
|
||||||
|
|
||||||
render json: @form_field_value, serializer: FormFieldValueSerializer, user: current_user
|
render json: @form_field_value, serializer: FormFieldValueSerializer, user: current_user
|
||||||
|
@ -18,7 +19,7 @@ class FormFieldValuesController < ApplicationController
|
||||||
private
|
private
|
||||||
|
|
||||||
def form_field_value_params
|
def form_field_value_params
|
||||||
params.require(:form_field_value).permit(:form_field_id, :value)
|
params.require(:form_field_value).permit(:form_field_id, :value, :not_applicable, value: [])
|
||||||
end
|
end
|
||||||
|
|
||||||
def load_form_response
|
def load_form_response
|
||||||
|
|
|
@ -1,68 +0,0 @@
|
||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
class FormResponsesController < ApplicationController
|
|
||||||
before_action :load_form, only: :create
|
|
||||||
before_action :load_parent, only: :create
|
|
||||||
before_action :load_form_response, except: :create
|
|
||||||
|
|
||||||
def create
|
|
||||||
case @parent
|
|
||||||
when Step
|
|
||||||
render_403 and return unless can_create_protocol_form_responses?(@parent.protocol)
|
|
||||||
|
|
||||||
ActiveRecord::Base.transaction do
|
|
||||||
@form_response = FormResponse.create!(form: @form, created_by: current_user)
|
|
||||||
@parent.step_orderable_elements.create!(orderable: @form_response)
|
|
||||||
end
|
|
||||||
else
|
|
||||||
render_422
|
|
||||||
end
|
|
||||||
|
|
||||||
render json: @form_response, serializer: FormResponseSerializer, user: current_user
|
|
||||||
end
|
|
||||||
|
|
||||||
def submit
|
|
||||||
render_403 and return unless can_submit_form_response?(@form_response)
|
|
||||||
|
|
||||||
@form_response.submit!(current_user)
|
|
||||||
|
|
||||||
render json: @form_response, serializer: FormResponseSerializer, user: current_user
|
|
||||||
end
|
|
||||||
|
|
||||||
def reset
|
|
||||||
render_403 and return unless can_reset_form_response?(@form_response)
|
|
||||||
|
|
||||||
@form_response.reset!(current_user)
|
|
||||||
|
|
||||||
render json: @form_response, serializer: FormResponseSerializer, user: current_user
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def form_response_params
|
|
||||||
params.require(:form_response).permit(:form_id, :parent_id, :parent_type)
|
|
||||||
end
|
|
||||||
|
|
||||||
def load_form
|
|
||||||
@form = Form.find_by(id: form_response_params[:form_id])
|
|
||||||
|
|
||||||
render_404 unless @form && can_read_form?(@form)
|
|
||||||
end
|
|
||||||
|
|
||||||
def load_parent
|
|
||||||
case form_response_params[:parent_type]
|
|
||||||
when 'Step'
|
|
||||||
@parent = Step.find_by(id: form_response_params[:parent_id])
|
|
||||||
else
|
|
||||||
return render_422
|
|
||||||
end
|
|
||||||
|
|
||||||
render_404 unless @parent
|
|
||||||
end
|
|
||||||
|
|
||||||
def load_form_response
|
|
||||||
@form_response = FormResponse.find_by(id: params[:id])
|
|
||||||
|
|
||||||
render_404 unless @form_response
|
|
||||||
end
|
|
||||||
end
|
|
57
app/controllers/step_elements/form_responses_controller.rb
Normal file
57
app/controllers/step_elements/form_responses_controller.rb
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
module StepElements
|
||||||
|
class FormResponsesController < BaseController
|
||||||
|
before_action :load_form, only: :create
|
||||||
|
before_action :load_parent, only: :create
|
||||||
|
before_action :load_form_response, except: :create
|
||||||
|
|
||||||
|
def create
|
||||||
|
render_403 and return unless can_create_protocol_form_responses?(@parent.protocol)
|
||||||
|
ActiveRecord::Base.transaction do
|
||||||
|
@form_response = FormResponse.create!(form: @form, created_by: current_user)
|
||||||
|
@parent.step_orderable_elements.create!(orderable: @form_response)
|
||||||
|
end
|
||||||
|
|
||||||
|
render_step_orderable_element(@form_response)
|
||||||
|
end
|
||||||
|
|
||||||
|
def submit
|
||||||
|
render_403 and return unless can_submit_form_response?(@form_response)
|
||||||
|
|
||||||
|
@form_response.submit!(current_user)
|
||||||
|
|
||||||
|
render_step_orderable_element(@form_response)
|
||||||
|
end
|
||||||
|
|
||||||
|
def reset
|
||||||
|
render_403 and return unless can_reset_form_response?(@form_response)
|
||||||
|
|
||||||
|
new_form_response = @form_response.reset!(current_user)
|
||||||
|
|
||||||
|
render_step_orderable_element(@form_response)
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def form_response_params
|
||||||
|
params.permit(:form_id, :step_id)
|
||||||
|
end
|
||||||
|
|
||||||
|
def load_form
|
||||||
|
@form = Form.find_by(id: form_response_params[:form_id])
|
||||||
|
|
||||||
|
render_404 unless @form && can_read_form?(@form)
|
||||||
|
end
|
||||||
|
|
||||||
|
def load_parent
|
||||||
|
@parent = Step.find_by(id: form_response_params[:step_id])
|
||||||
|
render_404 unless @parent
|
||||||
|
end
|
||||||
|
|
||||||
|
def load_form_response
|
||||||
|
@form_response = FormResponse.find_by(id: params[:id])
|
||||||
|
|
||||||
|
render_404 unless @form_response
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -3,21 +3,23 @@
|
||||||
<div>
|
<div>
|
||||||
<div class="font-bold">
|
<div class="font-bold">
|
||||||
{{ field.attributes.name }}
|
{{ field.attributes.name }}
|
||||||
|
<span v-if="unit">({{ unit }})</span>
|
||||||
<span v-if="field.attributes.required" class="text-sn-delete-red">*</span>
|
<span v-if="field.attributes.required" class="text-sn-delete-red">*</span>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="field.attributes.description">
|
<div v-if="field.attributes.description">
|
||||||
{{ field.attributes.description }}
|
{{ field.attributes.description }}
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-2">
|
<div class="mt-2">
|
||||||
<component :is="field.attributes.data.type" :field="field" :marked_as_na="mark_as_na" />
|
<component :is="field.attributes.data.type" ref="formField" :disabled="disabled" :field="field" :marked_as_na="markAsNa" @save="saveValue" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex justify-end items-end">
|
<div class="flex justify-end items-end">
|
||||||
<button class="btn btn-secondary mb-0.5"
|
<button class="btn btn-secondary mb-0.5"
|
||||||
|
:disabled="disabled"
|
||||||
v-if="field.attributes.allow_not_applicable"
|
v-if="field.attributes.allow_not_applicable"
|
||||||
:class="{'!bg-sn-super-light-blue !border-sn-blue': mark_as_na}"
|
:class="{'!bg-sn-super-light-blue !border-sn-blue': markAsNa}"
|
||||||
@click="mark_as_na = !mark_as_na">
|
@click="markAsNa = !markAsNa">
|
||||||
<div class="w-4 h-4 !border-sn-blue border rounded-sm flex items-center justify-center" :class="{'bg-sn-blue': mark_as_na}">
|
<div class="w-4 h-4 !border-sn-blue border rounded-sm flex items-center justify-center" :class="{'bg-sn-blue': markAsNa}">
|
||||||
<i class="sn-icon sn-icon-check text-white" style="font-size: 16px !important;"></i>
|
<i class="sn-icon sn-icon-check text-white" style="font-size: 16px !important;"></i>
|
||||||
</div>
|
</div>
|
||||||
{{ i18n.t('forms.fields.mark_as_na') }}
|
{{ i18n.t('forms.fields.mark_as_na') }}
|
||||||
|
@ -36,7 +38,12 @@ import MultipleChoiceField from './fields/multiple_choice.vue';
|
||||||
export default {
|
export default {
|
||||||
name: 'ViewField',
|
name: 'ViewField',
|
||||||
props: {
|
props: {
|
||||||
field: Object
|
field: Object,
|
||||||
|
disabled: Boolean,
|
||||||
|
formResponse: {
|
||||||
|
type: Object,
|
||||||
|
default: null
|
||||||
|
}
|
||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
DatetimeField,
|
DatetimeField,
|
||||||
|
@ -47,8 +54,30 @@ export default {
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
mark_as_na: false
|
markAsNa: this.field.field_value?.not_applicable || false
|
||||||
};
|
};
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
markAsNa() {
|
||||||
|
this.saveValue(null);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
unit() {
|
||||||
|
return this.field.attributes.data.unit;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
saveValue(value) {
|
||||||
|
if (this.formResponse) {
|
||||||
|
this.$emit(
|
||||||
|
'save',
|
||||||
|
this.field.id,
|
||||||
|
value,
|
||||||
|
this.markAsNa
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -4,13 +4,17 @@
|
||||||
<DateTimePicker
|
<DateTimePicker
|
||||||
@change="updateFromDate"
|
@change="updateFromDate"
|
||||||
:mode="mode"
|
:mode="mode"
|
||||||
|
:defaultValue="fromValue"
|
||||||
:clearable="true"
|
:clearable="true"
|
||||||
|
:disabled="fieldDisabled"
|
||||||
:placeholder="i18n.t('forms.fields.from')"
|
:placeholder="i18n.t('forms.fields.from')"
|
||||||
:class="{'error': !validValue}"
|
:class="{'error': !validValue}"
|
||||||
/>
|
/>
|
||||||
<DateTimePicker
|
<DateTimePicker
|
||||||
@change="updateToDate"
|
@change="updateToDate"
|
||||||
|
:defaultValue="toValue"
|
||||||
:mode="mode"
|
:mode="mode"
|
||||||
|
:disabled="fieldDisabled"
|
||||||
:clearable="true"
|
:clearable="true"
|
||||||
:placeholder="i18n.t('forms.fields.to')"
|
:placeholder="i18n.t('forms.fields.to')"
|
||||||
:class="{'error': !validValue}"
|
:class="{'error': !validValue}"
|
||||||
|
@ -19,7 +23,9 @@
|
||||||
<DateTimePicker
|
<DateTimePicker
|
||||||
v-else
|
v-else
|
||||||
@change="updateDate"
|
@change="updateDate"
|
||||||
|
:defaultValue="value"
|
||||||
:mode="mode"
|
:mode="mode"
|
||||||
|
:disabled="fieldDisabled"
|
||||||
:clearable="true"
|
:clearable="true"
|
||||||
:placeholder="i18n.t(`forms.fields.add_${mode}`)"
|
:placeholder="i18n.t(`forms.fields.add_${mode}`)"
|
||||||
/>
|
/>
|
||||||
|
@ -43,6 +49,15 @@ export default {
|
||||||
toValue: null
|
toValue: null
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
created() {
|
||||||
|
if (this.field.field_value?.datetime) {
|
||||||
|
this.value = new Date(this.field.field_value.datetime);
|
||||||
|
this.fromValue = new Date(this.field.field_value.datetime);
|
||||||
|
}
|
||||||
|
if (this.field.field_value?.datetime_to) {
|
||||||
|
this.toValue = new Date(this.field.field_value.datetime_to);
|
||||||
|
}
|
||||||
|
},
|
||||||
computed: {
|
computed: {
|
||||||
mode() {
|
mode() {
|
||||||
return this.field.attributes.data.time ? 'datetime' : 'date';
|
return this.field.attributes.data.time ? 'datetime' : 'date';
|
||||||
|
@ -57,15 +72,31 @@ export default {
|
||||||
return this.value;
|
return this.value;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
watch: {
|
||||||
|
marked_as_na() {
|
||||||
|
if (this.marked_as_na) {
|
||||||
|
this.value = null;
|
||||||
|
this.fromValue = null;
|
||||||
|
this.toValue = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
updateDate(date) {
|
updateDate(date) {
|
||||||
this.value = date;
|
this.value = date;
|
||||||
|
this.$emit('save', this.value);
|
||||||
},
|
},
|
||||||
updateFromDate(date) {
|
updateFromDate(date) {
|
||||||
this.fromValue = date;
|
this.fromValue = date;
|
||||||
|
if (this.validValue) {
|
||||||
|
this.$emit('save', [this.fromValue, this.toValue]);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
updateToDate(date) {
|
updateToDate(date) {
|
||||||
this.toValue = date;
|
this.toValue = date;
|
||||||
|
if (this.validValue) {
|
||||||
|
this.$emit('save', [this.fromValue, this.toValue]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,6 +1,12 @@
|
||||||
export default {
|
export default {
|
||||||
props: {
|
props: {
|
||||||
field: Object,
|
field: Object,
|
||||||
marked_as_na: Boolean
|
marked_as_na: Boolean,
|
||||||
|
disabled: Boolean
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
fieldDisabled() {
|
||||||
|
return this.marked_as_na || this.disabled;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,8 +1,10 @@
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<SelectDropdown
|
<SelectDropdown
|
||||||
:disabled="marked_as_na"
|
:disabled="fieldDisabled"
|
||||||
:options="options"
|
:options="options"
|
||||||
|
:value="value"
|
||||||
|
@change="saveValue"
|
||||||
:multiple="true"
|
:multiple="true"
|
||||||
:withCheckboxes="true"
|
:withCheckboxes="true"
|
||||||
:clearable="true"
|
:clearable="true"
|
||||||
|
@ -21,12 +23,20 @@ export default {
|
||||||
SelectDropdown
|
SelectDropdown
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
value() {
|
||||||
|
return (this.field.field_value?.selection || '[]');
|
||||||
|
},
|
||||||
options() {
|
options() {
|
||||||
if (!this.field.attributes.data.options) {
|
if (!this.field.attributes.data.options) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
return this.field.attributes.data.options.map((option) => ([option, option]));
|
return this.field.attributes.data.options.map((option) => ([option, option]));
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
saveValue(value) {
|
||||||
|
this.$emit('save', value);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="sci-input-container-v2 mb-2" :class="{'error': !isValidValue}" :data-error="errorMessage">
|
<div class="sci-input-container-v2 mb-2" :class="{'error': !isValidValue}" :data-error="errorMessage">
|
||||||
<input type="number" v-model="value" class="sci-input" :disabled="marked_as_na" :placeholder="i18n.t('forms.fields.add_number')"></input>
|
<input type="number" v-model="value" class="sci-input" :disabled="fieldDisabled" @change="saveValue" :placeholder="i18n.t('forms.fields.add_number')"></input>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -12,7 +12,7 @@ export default {
|
||||||
mixins: [fieldMixin],
|
mixins: [fieldMixin],
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
value: ''
|
value: this.field.field_value?.value
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
@ -43,6 +43,13 @@ export default {
|
||||||
|
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
saveValue() {
|
||||||
|
if (this.isValidValue) {
|
||||||
|
this.$emit('save', this.value);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -1,8 +1,10 @@
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<SelectDropdown
|
<SelectDropdown
|
||||||
:disabled="marked_as_na"
|
:disabled="fieldDisabled"
|
||||||
:options="options"
|
:options="options"
|
||||||
|
:value="value"
|
||||||
|
@change="saveValue"
|
||||||
:clearable="true"
|
:clearable="true"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
@ -19,12 +21,20 @@ export default {
|
||||||
SelectDropdown
|
SelectDropdown
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
value() {
|
||||||
|
return this.field.field_value?.value;
|
||||||
|
},
|
||||||
options() {
|
options() {
|
||||||
if (!this.field.attributes.data.options) {
|
if (!this.field.attributes.data.options) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
return this.field.attributes.data.options.map((option) => ([option, option]));
|
return this.field.attributes.data.options.map((option) => ([option, option]));
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
saveValue(value) {
|
||||||
|
this.$emit('save', value);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="sci-input-container-v2 h-24">
|
<div class="sci-input-container-v2 h-24">
|
||||||
<textarea class="sci-input" :disabled="marked_as_na" :placeholder="i18n.t('forms.fields.add_text')"></textarea>
|
<textarea class="sci-input" :value="value" :disabled="fieldDisabled" @change="saveValue" :placeholder="i18n.t('forms.fields.add_text')"></textarea>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -9,6 +9,16 @@ import fieldMixin from './field_mixin';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'TextField',
|
name: 'TextField',
|
||||||
mixins: [fieldMixin]
|
mixins: [fieldMixin],
|
||||||
|
methods: {
|
||||||
|
saveValue(event) {
|
||||||
|
this.$emit('save', event.target.value);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
value() {
|
||||||
|
return this.field.field_value?.value;
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -168,7 +168,11 @@
|
||||||
@reorder="updateElementOrder"
|
@reorder="updateElementOrder"
|
||||||
@close="closeReorderModal"
|
@close="closeReorderModal"
|
||||||
/>
|
/>
|
||||||
<SelectFormModal v-if="openFormSelectModal" @close="openFormSelectModal = false" />
|
<SelectFormModal
|
||||||
|
v-if="openFormSelectModal"
|
||||||
|
@close="openFormSelectModal = false"
|
||||||
|
@submit="createElement('form_response', null, $event); openFormSelectModal = false"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -183,6 +187,7 @@
|
||||||
import StepTable from '../shared/content/table.vue'
|
import StepTable from '../shared/content/table.vue'
|
||||||
import StepText from '../shared/content/text.vue'
|
import StepText from '../shared/content/text.vue'
|
||||||
import Checklist from '../shared/content/checklist.vue'
|
import Checklist from '../shared/content/checklist.vue'
|
||||||
|
import FormResponse from '../shared/content/form_response.vue'
|
||||||
import deleteStepModal from './modals/delete_step.vue'
|
import deleteStepModal from './modals/delete_step.vue'
|
||||||
import Attachments from '../shared/content/attachments.vue'
|
import Attachments from '../shared/content/attachments.vue'
|
||||||
import ReorderableItemsModal from '../shared/reorderable_items_modal.vue'
|
import ReorderableItemsModal from '../shared/reorderable_items_modal.vue'
|
||||||
|
@ -285,7 +290,8 @@
|
||||||
ReorderableItemsModal,
|
ReorderableItemsModal,
|
||||||
MenuDropdown,
|
MenuDropdown,
|
||||||
ContentToolbar,
|
ContentToolbar,
|
||||||
SelectFormModal
|
SelectFormModal,
|
||||||
|
FormResponse
|
||||||
},
|
},
|
||||||
created() {
|
created() {
|
||||||
this.loadAttachments();
|
this.loadAttachments();
|
||||||
|
@ -624,10 +630,10 @@
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
createElement(elementType, tableDimensions = null) {
|
createElement(elementType, tableDimensions = null, formId = null) {
|
||||||
let plateTemplate = tableDimensions != null;
|
let plateTemplate = tableDimensions != null;
|
||||||
tableDimensions ||= [5, 5];
|
tableDimensions ||= [5, 5];
|
||||||
$.post(this.urls[`create_${elementType}_url`], { tableDimensions: tableDimensions, plateTemplate: plateTemplate }, (result) => {
|
$.post(this.urls[`create_${elementType}_url`], { tableDimensions: tableDimensions, plateTemplate: plateTemplate, form_id: formId }, (result) => {
|
||||||
result.data.isNew = true;
|
result.data.isNew = true;
|
||||||
this.elements.push(result.data)
|
this.elements.push(result.data)
|
||||||
|
|
||||||
|
|
165
app/javascript/vue/shared/content/form_response.vue
Normal file
165
app/javascript/vue/shared/content/form_response.vue
Normal file
|
@ -0,0 +1,165 @@
|
||||||
|
<template>
|
||||||
|
<div class="content__form-container pr-8" :data-e2e="`e2e-CO-${dataE2e}-stepForm${element.id}`">
|
||||||
|
<div class="sci-divider my-6" v-if="!inRepository"></div>
|
||||||
|
<div class="flex items-center gap-4">
|
||||||
|
<MenuDropdown
|
||||||
|
class="ml-auto"
|
||||||
|
:listItems="this.actionMenu"
|
||||||
|
:btnClasses="'btn btn-light icon-btn btn-sm'"
|
||||||
|
:position="'right'"
|
||||||
|
:btnIcon="'sn-icon sn-icon-more-hori'"
|
||||||
|
:dataE2e="`e2e-DD-${dataE2e}-stepText${element.id}-options`"
|
||||||
|
@move="showMoveModal"
|
||||||
|
@delete="showDeleteModal"
|
||||||
|
></MenuDropdown>
|
||||||
|
</div>
|
||||||
|
<div class="max-w-[800px] w-full rounded bg-sn-super-light-grey p-6 flex flex-col gap-4">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<h3 class="my-1">{{ form.name }}</h3>
|
||||||
|
<div v-if="this.formResponse.status == 'submitted'" class="ml-auto text-right text-xs text-sn-grey-700">
|
||||||
|
{{ i18n.t('forms.response.submitted_on') }} {{ this.formResponse.submitted_at }}<br>
|
||||||
|
{{ i18n.t('forms.response.by') }} {{ this.formResponse.submitted_by_full_name }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Field v-for="field in formFields" :disabled="formDisabled" ref="formFields" :key="field.id" :field="field" :formResponse="formResponse" @save="saveValue" />
|
||||||
|
<div>
|
||||||
|
<button v-if="this.formResponse.urls.submit" class="btn btn-primary" :disabled="!validResponse" @click="submitForm">
|
||||||
|
{{ i18n.t('forms.response.submit') }}
|
||||||
|
</button>
|
||||||
|
<button v-else-if="this.formResponse.urls.reset" class="btn btn-secondary" @click="resetForm">
|
||||||
|
{{ i18n.t('general.edit')}}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<deleteElementModal v-if="confirmingDelete" @confirm="deleteElement($event)" @cancel="closeDeleteModal"/>
|
||||||
|
<moveElementModal v-if="movingElement"
|
||||||
|
:parent_type="element.attributes.orderable.parent_type"
|
||||||
|
:targets_url="''"
|
||||||
|
@confirm="moveElement($event)" @cancel="closeMoveModal"/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
/* global I18n */
|
||||||
|
|
||||||
|
import DeleteMixin from './mixins/delete.js';
|
||||||
|
import MoveMixin from './mixins/move.js';
|
||||||
|
import deleteElementModal from './modal/delete.vue';
|
||||||
|
import moveElementModal from './modal/move.vue';
|
||||||
|
import MenuDropdown from '../menu_dropdown.vue';
|
||||||
|
import Field from '../../forms/field.vue';
|
||||||
|
import axios from '../../../packs/custom_axios.js';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'TextContent',
|
||||||
|
components: {
|
||||||
|
deleteElementModal,
|
||||||
|
moveElementModal,
|
||||||
|
MenuDropdown,
|
||||||
|
Field
|
||||||
|
},
|
||||||
|
mixins: [DeleteMixin, MoveMixin],
|
||||||
|
props: {
|
||||||
|
element: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
inRepository: {
|
||||||
|
type: Boolean,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
reorderElementUrl: {
|
||||||
|
type: String
|
||||||
|
},
|
||||||
|
dataE2e: {
|
||||||
|
type: String,
|
||||||
|
default: ''
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
form: this.element.attributes.orderable.form,
|
||||||
|
formResponse: this.element.attributes.orderable,
|
||||||
|
formFieldValues: this.element.attributes.orderable.form_field_values
|
||||||
|
};
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
formDisabled() {
|
||||||
|
return !this.formResponse.urls.submit;
|
||||||
|
},
|
||||||
|
validResponse() {
|
||||||
|
return this.formFields.every((field) => {
|
||||||
|
if (field.attributes.required) {
|
||||||
|
return field.field_value?.value
|
||||||
|
|| field.field_value?.selection
|
||||||
|
|| field.field_value?.datetime
|
||||||
|
|| field.field_value?.datetime_to
|
||||||
|
|| field.field_value?.not_applicable;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
formFields() {
|
||||||
|
return this.element.attributes.orderable.form_fields.map((field) => ({
|
||||||
|
id: field.id,
|
||||||
|
attributes: field,
|
||||||
|
field_value: this.formFieldValues.find((value) => value.form_field_id === field.id)
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
actionMenu() {
|
||||||
|
const menu = [];
|
||||||
|
if (this.element.attributes.orderable.urls.move_targets_url) {
|
||||||
|
menu.push({
|
||||||
|
text: I18n.t('general.move'),
|
||||||
|
emit: 'move',
|
||||||
|
data_e2e: `e2e-BT-${this.dataE2e}-stepText${this.element.id}-options-move`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (this.element.attributes.orderable.urls.delete_url) {
|
||||||
|
menu.push({
|
||||||
|
text: I18n.t('general.delete'),
|
||||||
|
emit: 'delete',
|
||||||
|
data_e2e: `e2e-BT-${this.dataE2e}-stepText${this.element.id}-options-delete`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return menu;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
saveValue(formFieldId, value, markAsNa) {
|
||||||
|
axios.post(this.formResponse.urls.add_value, {
|
||||||
|
form_field_value: {
|
||||||
|
form_field_id: formFieldId,
|
||||||
|
value,
|
||||||
|
not_applicable: markAsNa
|
||||||
|
}
|
||||||
|
}).then((response) => {
|
||||||
|
if (this.formFieldValues.find((formFieldValue) => formFieldValue.form_field_id === formFieldId)) {
|
||||||
|
this.formFieldValues = this.formFieldValues.map((formFieldValue) => {
|
||||||
|
if (formFieldValue.form_field_id === formFieldId) {
|
||||||
|
return response.data.data.attributes;
|
||||||
|
}
|
||||||
|
return formFieldValue;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.formFieldValues.push(response.data.data.attributes);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
submitForm() {
|
||||||
|
axios.post(this.formResponse.urls.submit).then((response) => {
|
||||||
|
const { attributes } = response.data.data;
|
||||||
|
this.formResponse = attributes.orderable;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
resetForm() {
|
||||||
|
axios.post(this.formResponse.urls.reset).then((response) => {
|
||||||
|
const { attributes } = response.data.data;
|
||||||
|
this.formResponse = attributes.orderable;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
|
@ -29,7 +29,7 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
<button class="btn btn-secondary" @click="close">{{ i18n.t('general.cancel') }}</button>
|
<button class="btn btn-secondary" @click="close">{{ i18n.t('general.cancel') }}</button>
|
||||||
<button v-if="anyForms" :disabled="!form" @submit="$emit('submit', form)" class="btn btn-primary">
|
<button v-if="anyForms" :disabled="!form" @click="$emit('submit', form)" class="btn btn-primary">
|
||||||
{{ i18n.t('protocols.steps.modals.form_modal.add_form') }}
|
{{ i18n.t('protocols.steps.modals.form_modal.add_form') }}
|
||||||
</button>
|
</button>
|
||||||
<a v-else :href="formsPageUrl" class="btn btn-primary">
|
<a v-else :href="formsPageUrl" class="btn btn-primary">
|
||||||
|
|
|
@ -14,6 +14,7 @@
|
||||||
:format="format"
|
:format="format"
|
||||||
:month-change-on-scroll="false"
|
:month-change-on-scroll="false"
|
||||||
:six-weeks="true"
|
:six-weeks="true"
|
||||||
|
:disabled="disabled"
|
||||||
:auto-apply="true"
|
:auto-apply="true"
|
||||||
:partial-flow="true"
|
:partial-flow="true"
|
||||||
:markers="markers"
|
:markers="markers"
|
||||||
|
|
|
@ -25,7 +25,7 @@ class FormResponse < ApplicationRecord
|
||||||
step_orderable_element&.step
|
step_orderable_element&.step
|
||||||
end
|
end
|
||||||
|
|
||||||
def create_value!(created_by, form_field, value)
|
def create_value!(created_by, form_field, value, not_applicable: false)
|
||||||
ActiveRecord::Base.transaction(requires_new: true) do
|
ActiveRecord::Base.transaction(requires_new: true) do
|
||||||
form_field_values.where(form_field: form_field).find_each do |form_field_value|
|
form_field_values.where(form_field: form_field).find_each do |form_field_value|
|
||||||
form_field_value.update!(latest: false)
|
form_field_value.update!(latest: false)
|
||||||
|
@ -38,7 +38,8 @@ class FormResponse < ApplicationRecord
|
||||||
created_by: created_by,
|
created_by: created_by,
|
||||||
submitted_by: created_by,
|
submitted_by: created_by,
|
||||||
submitted_at: DateTime.current,
|
submitted_at: DateTime.current,
|
||||||
value: value
|
value: value,
|
||||||
|
not_applicable: not_applicable
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -15,6 +15,7 @@ Canaid::Permissions.register_for(FormResponse) do
|
||||||
case parent
|
case parent
|
||||||
when Step
|
when Step
|
||||||
next false unless parent.protocol.my_module # protocol template forms can't be submitted
|
next false unless parent.protocol.my_module # protocol template forms can't be submitted
|
||||||
|
next false unless form_response.pending?
|
||||||
|
|
||||||
parent.protocol.my_module.permission_granted?(user, FormResponsePermissions::SUBMIT)
|
parent.protocol.my_module.permission_granted?(user, FormResponsePermissions::SUBMIT)
|
||||||
end
|
end
|
||||||
|
@ -25,6 +26,7 @@ Canaid::Permissions.register_for(FormResponse) do
|
||||||
case parent
|
case parent
|
||||||
when Step
|
when Step
|
||||||
next false unless parent.protocol.my_module # protocol template forms can't be reset
|
next false unless parent.protocol.my_module # protocol template forms can't be reset
|
||||||
|
next false unless form_response.submitted?
|
||||||
|
|
||||||
parent.protocol.my_module.permission_granted?(user, FormResponsePermissions::SUBMIT)
|
parent.protocol.my_module.permission_granted?(user, FormResponsePermissions::SUBMIT)
|
||||||
end
|
end
|
||||||
|
|
|
@ -3,7 +3,8 @@
|
||||||
class FormFieldValueSerializer < ActiveModel::Serializer
|
class FormFieldValueSerializer < ActiveModel::Serializer
|
||||||
include Canaid::Helpers::PermissionsHelper
|
include Canaid::Helpers::PermissionsHelper
|
||||||
|
|
||||||
attributes :form_field_id, :type, :value, :submitted_at, :submitted_by_full_name, :unit
|
attributes :form_field_id, :type, :value, :submitted_at, :submitted_by_full_name,
|
||||||
|
:unit, :not_applicable, :selection, :datetime, :datetime_to
|
||||||
|
|
||||||
def submitted_by_full_name
|
def submitted_by_full_name
|
||||||
object.submitted_by.full_name
|
object.submitted_by.full_name
|
||||||
|
|
|
@ -1,15 +0,0 @@
|
||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
class FormResponseSerializer < ActiveModel::Serializer
|
|
||||||
include Canaid::Helpers::PermissionsHelper
|
|
||||||
|
|
||||||
attributes :id, :created_at, :form_id
|
|
||||||
|
|
||||||
has_many :form_field_values do
|
|
||||||
object.form_field_values.latest
|
|
||||||
end
|
|
||||||
|
|
||||||
def submitted_by_full_name
|
|
||||||
object.submitted_by.full_name
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -23,15 +23,16 @@ class FormSerializer < ActiveModel::Serializer
|
||||||
end
|
end
|
||||||
|
|
||||||
def urls
|
def urls
|
||||||
|
user = scope[:user] || @instance_options[:user]
|
||||||
list = {
|
list = {
|
||||||
show: form_path(object),
|
show: form_path(object),
|
||||||
create_field: form_form_fields_path(object),
|
create_field: form_form_fields_path(object),
|
||||||
reorder_fields: reorder_form_form_fields_path(object)
|
reorder_fields: reorder_form_form_fields_path(object)
|
||||||
}
|
}
|
||||||
|
|
||||||
list[:publish] = publish_form_path(object) if can_publish_form?(current_user, object)
|
list[:publish] = publish_form_path(object) if can_publish_form?(user, object)
|
||||||
|
|
||||||
list[:unpublish] = unpublish_form_path(object) if can_unpublish_form?(current_user, object)
|
list[:unpublish] = unpublish_form_path(object) if can_unpublish_form?(user, object)
|
||||||
|
|
||||||
list
|
list
|
||||||
end
|
end
|
||||||
|
|
39
app/serializers/step_form_response_serializer.rb
Normal file
39
app/serializers/step_form_response_serializer.rb
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class StepFormResponseSerializer < ActiveModel::Serializer
|
||||||
|
include Canaid::Helpers::PermissionsHelper
|
||||||
|
include Rails.application.routes.url_helpers
|
||||||
|
|
||||||
|
attributes :id, :created_at, :form_id, :urls, :submitted_by_full_name, :status, :submitted_at
|
||||||
|
|
||||||
|
has_one :form, serializer: FormSerializer
|
||||||
|
|
||||||
|
has_many :form_fields, serializer: FormFieldSerializer do
|
||||||
|
object.form.form_fields
|
||||||
|
end
|
||||||
|
|
||||||
|
has_many :form_field_values do
|
||||||
|
object.form_field_values.latest
|
||||||
|
end
|
||||||
|
|
||||||
|
def submitted_by_full_name
|
||||||
|
object.submitted_by&.full_name
|
||||||
|
end
|
||||||
|
|
||||||
|
def submitted_at
|
||||||
|
I18n.l(object.submitted_at, format: :full) if object.submitted_at
|
||||||
|
end
|
||||||
|
|
||||||
|
def urls
|
||||||
|
user = scope[:user] || @instance_options[:user]
|
||||||
|
|
||||||
|
list = {
|
||||||
|
add_value: form_response_form_field_values_path(object)
|
||||||
|
}
|
||||||
|
|
||||||
|
list[:submit] = submit_step_form_response_path(object.step ,object) if can_submit_form_response?(user, object)
|
||||||
|
list[:reset] = reset_step_form_response_path(object.step ,object) if can_reset_form_response?(user, object)
|
||||||
|
|
||||||
|
list
|
||||||
|
end
|
||||||
|
end
|
|
@ -11,6 +11,8 @@ class StepOrderableElementSerializer < ActiveModel::Serializer
|
||||||
TableSerializer.new(object.orderable.table, scope: { user: @instance_options[:user] }).as_json
|
TableSerializer.new(object.orderable.table, scope: { user: @instance_options[:user] }).as_json
|
||||||
when 'StepText'
|
when 'StepText'
|
||||||
StepTextSerializer.new(object.orderable, scope: { user: @instance_options[:user] }).as_json
|
StepTextSerializer.new(object.orderable, scope: { user: @instance_options[:user] }).as_json
|
||||||
|
when 'FormResponse'
|
||||||
|
StepFormResponseSerializer.new(object.orderable, scope: { user: @instance_options[:user] }).as_json
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -97,6 +97,7 @@ class StepSerializer < ActiveModel::Serializer
|
||||||
create_table_url: step_tables_path(object),
|
create_table_url: step_tables_path(object),
|
||||||
create_text_url: step_texts_path(object),
|
create_text_url: step_texts_path(object),
|
||||||
create_checklist_url: step_checklists_path(object),
|
create_checklist_url: step_checklists_path(object),
|
||||||
|
create_form_response_url: step_form_responses_path(object),
|
||||||
update_asset_view_mode_url: update_asset_view_mode_step_path(object),
|
update_asset_view_mode_url: update_asset_view_mode_step_path(object),
|
||||||
update_view_state_url: update_view_state_step_path(object),
|
update_view_state_url: update_view_state_step_path(object),
|
||||||
direct_upload_url: rails_direct_uploads_url,
|
direct_upload_url: rails_direct_uploads_url,
|
||||||
|
|
|
@ -1133,6 +1133,10 @@ en:
|
||||||
SingleChoiceField: 'Single choice'
|
SingleChoiceField: 'Single choice'
|
||||||
MultipleChoiceField: 'Multiple choice'
|
MultipleChoiceField: 'Multiple choice'
|
||||||
DatetimeField: 'Date & Time'
|
DatetimeField: 'Date & Time'
|
||||||
|
response:
|
||||||
|
submit: 'Submit form'
|
||||||
|
submitted_on: 'Submitted on'
|
||||||
|
by: 'by'
|
||||||
label_templates:
|
label_templates:
|
||||||
types:
|
types:
|
||||||
fluics_label_template: 'Fluics'
|
fluics_label_template: 'Fluics'
|
||||||
|
|
|
@ -613,6 +613,12 @@ Rails.application.routes.draw do
|
||||||
post :reorder, on: :collection
|
post :reorder, on: :collection
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
resources :form_responses, controller: 'step_elements/form_responses', only: %i(create) do
|
||||||
|
member do
|
||||||
|
post :submit
|
||||||
|
post :reset
|
||||||
|
end
|
||||||
|
end
|
||||||
member do
|
member do
|
||||||
get 'elements'
|
get 'elements'
|
||||||
get 'attachments'
|
get 'attachments'
|
||||||
|
@ -876,12 +882,7 @@ Rails.application.routes.draw do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
resources :form_responses, only: %i(create) do
|
resources :form_responses, only: [] do
|
||||||
member do
|
|
||||||
post :submit
|
|
||||||
post :reset
|
|
||||||
end
|
|
||||||
|
|
||||||
resources :form_field_values, only: %i(create)
|
resources :form_field_values, only: %i(create)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
Loading…
Add table
Reference in a new issue