Add form interacrtions to step [SCI-11341][SCI-11381]

This commit is contained in:
Anton 2024-12-30 10:06:12 +01:00 committed by Martin Artnik
parent 07b6a9e8db
commit f9d7066d55
24 changed files with 416 additions and 114 deletions

View file

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

View file

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

View 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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View 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

View file

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

View file

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

View file

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

View file

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