adjust form completed step

pull/105/head
Alex Turchyn 2 years ago
parent bfbf3d7ddb
commit 96508cd1ee

@ -27,7 +27,6 @@ gem 'sqlite3'
gem 'strip_attributes'
gem 'turbo-rails'
gem 'tzinfo-data'
gem 'zip'
group :development, :test do
gem 'annotate'

@ -6,8 +6,8 @@ class SubmissionsDownloadController < ApplicationController
def index
submitter = Submitter.find_by(slug: params[:submitter_slug])
Submissions::GenerateResultAttachments.call(submitter)
Submissions::GenerateResultAttachments.call(submitter) if submitter.documents.blank?
redirect_to submitter.archive.url, allow_other_host: true
render json: submitter.documents.map { |e| helpers.rails_blob_url(e) }
end
end

@ -10,7 +10,7 @@ class SubmitFormController < ApplicationController
Submitter.preload(submission: { template: { documents_attachments: { preview_images_attachments: :blob } } })
.find_by!(slug: params[:slug])
return redirect_to submit_form_completed_path(@submitter.slug) if @submitter.completed_at?
redirect_to submit_form_completed_path(@submitter.slug) if @submitter.completed_at?
end
def update
@ -30,6 +30,12 @@ class SubmitFormController < ApplicationController
private
def normalized_values
params[:values].to_unsafe_h.transform_values { |v| v.is_a?(Array) ? v.compact_blank : v }
params[:values].to_unsafe_h.transform_values do |v|
if params[:cast_boolean] == 'true'
v == 'true'
else
v.is_a?(Array) ? v.compact_blank : v
end
end
end
end

@ -33,6 +33,6 @@ class TemplatesController < ApplicationController
private
def template_params
params.require(:template).permit(:name, :schema)
params.require(:template).permit(:name)
end
end

@ -47,6 +47,10 @@ select:required:invalid {
@apply btn btn-neutral text-white text-base;
}
.white-button {
@apply btn btn-outline text-base bg-white border-2;
}
.base-checkbox {
@apply checkbox rounded bg-white checkbox-sm;
}

@ -1,11 +1,11 @@
<template>
<div
class="flex cursor-pointer bg-red-100 absolute border text-[1.5vw] lg:text-base"
class="flex absolute text-[1.5vw] lg:text-base"
:style="computedStyle"
:class="{ 'border-red-100': !isActive, 'bg-opacity-70': !isActive && !isValue, 'border-red-500 border-dashed z-10': isActive, 'bg-opacity-30': isActive || isValue }"
:class="{ 'cursor-default': !submittable, 'bg-red-100 border cursor-pointer ': submittable, 'border-red-100': !isActive && submittable, 'bg-opacity-70': !isActive && !isValueSet && submittable, 'border-red-500 border-dashed z-10': isActive && submittable, 'bg-opacity-30': (isActive || isValueSet) && submittable }"
>
<div
v-if="!isActive && !isValue && field.type !== 'checkbox'"
v-if="!isActive && !isValueSet && field.type !== 'checkbox' && submittable"
class="absolute top-0 bottom-0 right-0 left-0 items-center justify-center h-full w-full"
>
<span
@ -65,6 +65,7 @@
class="w-full p-[0.2vw] flex items-center justify-center"
>
<input
v-if="submittable"
type="checkbox"
:value="false"
class="aspect-square base-checkbox"
@ -72,6 +73,12 @@
:checked="!!modelValue"
@click="$emit('update:model-value', !modelValue)"
>
<component
:is="modelValue ? 'IconCheckbox' : 'IconSquare'"
v-else
class="aspect-square"
:class="{ '!w-auto !h-full': area.w > area.h, '!w-full !h-auto': area.w <= area.h }"
/>
</div>
<div
v-else
@ -91,27 +98,29 @@
</template>
<script>
import { IconTextSize, IconWriting, IconCalendarEvent, IconPhoto, IconCheckbox, IconPaperclip, IconSelect, IconCircleDot, IconChecks } from '@tabler/icons-vue'
import { IconTextSize, IconWriting, IconCalendarEvent, IconPhoto, IconCheckbox, IconPaperclip, IconSelect, IconCircleDot, IconChecks, IconSquare } from '@tabler/icons-vue'
export default {
name: 'FieldArea',
components: {
IconPaperclip
IconPaperclip,
IconCheckbox,
IconSquare
},
props: {
field: {
type: Object,
required: true
},
step: {
type: Array,
isValueSet: {
type: Boolean,
required: false,
default: () => []
default: false
},
values: {
type: Object,
submittable: {
type: Boolean,
required: false,
default: () => ({})
default: false
},
modelValue: {
type: [Array, String, Number, Object, Boolean],
@ -153,9 +162,6 @@ export default {
multiple: 'Multiple Select'
}
},
isValue () {
return this.step.some((f) => ![null, undefined, ''].includes(this.values[f.uuid]))
},
fieldIcons () {
return {
text: IconTextSize,

@ -16,11 +16,11 @@
:ref="setAreaRef"
v-model="values[field.uuid]"
:field="field"
:values="values"
:area="area"
:submittable="true"
:field-index="fieldIndex"
:step="step"
:is-active="currentStep === step"
:is-value-set="step.some((f) => f.uuid in values)"
:attachments-index="attachmentsIndex"
@click="$emit('focus-step', step)"
/>

@ -1,30 +1,60 @@
<template>
<div>
<p>
Form completed - thanks!
</p>
<button @click.prevent="sendCopyToEmail">
<span v-if="isSendingCopy">
Sending
</span>
<span>
Send copy to email
</span>
</button>
<button @click.prevent="download">
<span v-if="isDownloading">
Downloading
</span>
<div class="mx-auto max-w-md flex flex-col">
<p class="font-medium text-2xl flex items-center space-x-1.5 mx-auto">
<IconCircleCheck
class="inline text-green-600"
:width="30"
:height="30"
/>
<span>
Download copy
Form has been completed!
</span>
</button>
</p>
<div class="space-y-3 mt-5">
<button
class="white-button flex items-center space-x-1 w-full"
:disabled="isSendingCopy"
@click.prevent="sendCopyToEmail"
>
<IconInnerShadowTop
v-if="isSendingCopy"
class="animate-spin"
/>
<IconMail v-else />
<span>
Send copy via email
</span>
</button>
<button
class="base-button flex items-center space-x-1 w-full"
:disabled="isDownloading"
@click.prevent="download"
>
<IconInnerShadowTop
v-if="isDownloading"
class="animate-spin"
/>
<IconDownload v-else />
<span>
Download
</span>
</button>
</div>
</div>
</template>
<script>
import { IconCircleCheck, IconMail, IconDownload, IconInnerShadowTop } from '@tabler/icons-vue'
import confetti from 'canvas-confetti'
export default {
name: 'FormCompleted',
components: {
IconCircleCheck,
IconInnerShadowTop,
IconMail,
IconDownload
},
props: {
submitterSlug: {
type: String,
@ -37,12 +67,21 @@ export default {
isDownloading: false
}
},
mounted () {
confetti({
particleCount: 50,
startVelocity: 30,
spread: 140
})
},
methods: {
sendCopyToEmail () {
this.isSendingCopy = true
fetch(`/send_submission_email.json?submitter_slug=${this.submitterSlug}`, {
method: 'POST'
}).then(() => {
alert('Email has been sent')
}).finally(() => {
this.isSendingCopy = false
})
@ -50,17 +89,21 @@ export default {
download () {
this.isDownloading = true
fetch(`/submitters/${this.submitterSlug}/download`).then(async (response) => {
const blob = new Blob([await response.text()], { type: `${response.headers.get('content-type')};charset=utf-8;` })
const url = URL.createObjectURL(blob)
const link = document.createElement('a')
fetch(`/submitters/${this.submitterSlug}/download`).then((response) => response.json()).then((urls) => {
urls.forEach((url) => {
fetch(url).then(async (response) => {
const blob = new Blob([await response.text()], { type: `${response.headers.get('content-type')};charset=utf-8;` })
const url = URL.createObjectURL(blob)
const link = document.createElement('a')
link.href = url
link.setAttribute('download', response.headers.get('content-disposition').split('"')[1])
link.href = url
link.setAttribute('download', response.headers.get('content-disposition').split('"')[1])
link.click()
link.click()
URL.revokeObjectURL(url)
URL.revokeObjectURL(url)
})
})
}).finally(() => {
this.isDownloading = false
})

@ -1,4 +1,21 @@
<template>
<template
v-for="field in otherSubmitterFields"
:key="field.uuid"
>
<Teleport
v-for="(area, index) in field.areas"
:key="index"
:to="`#page-${area.attachment_uuid}-${area.page}`"
>
<FieldArea
:model-value="values[field.uuid]"
:field="field"
:area="area"
:attachments-index="attachmentsIndex"
/>
</Teleport>
</template>
<FieldAreas
ref="areas"
:steps="stepFields"
@ -7,206 +24,221 @@
:current-step="currentStepFields"
@focus-step="goToStep($event, false, true)"
/>
<form
v-if="!isCompleted"
ref="form"
:action="submitPath"
method="post"
class="md:mx-16"
@submit.prevent="submitStep"
>
<input
type="hidden"
name="authenticity_token"
:value="authenticityToken"
>
<input
v-if="currentStep === stepFields.length - 1"
type="hidden"
name="completed"
value="true"
<div>
<form
v-if="!isCompleted"
ref="form"
:action="submitPath"
method="post"
class="md:mx-16"
@submit.prevent="submitStep"
>
<input
value="put"
name="_method"
type="hidden"
>
<div class="mt-4">
<div v-if="currentField.type === 'text'">
<label
v-if="currentField.name"
:for="currentField.uuid"
class="label text-2xl mb-2"
>{{ currentField.name }}</label>
<div>
<input
:id="currentField.uuid"
v-model="values[currentField.uuid]"
class="base-input !text-2xl w-full"
:required="currentField.required"
placeholder="Type here..."
type="text"
:name="`values[${currentField.uuid}]`"
@focus="$refs.areas.scrollIntoField(currentField)"
>
<input
type="hidden"
name="authenticity_token"
:value="authenticityToken"
>
<input
v-if="currentStep === stepFields.length - 1"
type="hidden"
name="completed"
value="true"
>
<input
value="put"
name="_method"
type="hidden"
>
<div class="mt-4">
<div v-if="currentField.type === 'text'">
<label
v-if="currentField.name"
:for="currentField.uuid"
class="label text-2xl mb-2"
>{{ currentField.name }}</label>
<div>
<input
:id="currentField.uuid"
v-model="values[currentField.uuid]"
class="base-input !text-2xl w-full"
:required="currentField.required"
placeholder="Type here..."
type="text"
:name="`values[${currentField.uuid}]`"
@focus="$refs.areas.scrollIntoField(currentField)"
>
</div>
</div>
</div>
<div v-else-if="currentField.type === 'date'">
<label
v-if="currentField.name"
:for="currentField.uuid"
class="label text-2xl mb-2"
>{{ currentField.name }}</label>
<div class="text-center">
<input
<div v-else-if="currentField.type === 'date'">
<label
v-if="currentField.name"
:for="currentField.uuid"
class="label text-2xl mb-2"
>{{ currentField.name }}</label>
<div class="text-center">
<input
:id="currentField.uuid"
v-model="values[currentField.uuid]"
class="base-input !text-2xl text-center w-full"
:required="currentField.required"
type="date"
:name="`values[${currentField.uuid}]`"
@focus="$refs.areas.scrollIntoField(currentField)"
>
</div>
</div>
<div v-else-if="currentField.type === 'select'">
<label
v-if="currentField.name"
:for="currentField.uuid"
class="label text-2xl mb-2"
>{{ currentField.name }}</label>
<select
:id="currentField.uuid"
v-model="values[currentField.uuid]"
class="base-input !text-2xl text-center w-full"
:required="currentField.required"
type="date"
:required="true"
class="select base-input !text-2xl w-full text-center font-normal"
:name="`values[${currentField.uuid}]`"
@change="values[currentField.uuid] = $event.target.value"
@focus="$refs.areas.scrollIntoField(currentField)"
>
<option
value=""
:selected="!values[currentField.uuid]"
>
Select your option
</option>
<option
v-for="(option, index) in currentField.options"
:key="index"
:selected="values[currentField.uuid] == option"
:value="option"
>
{{ option }}
</option>
</select>
</div>
</div>
<div v-else-if="currentField.type === 'select'">
<label
v-if="currentField.name"
:for="currentField.uuid"
class="label text-2xl mb-2"
>{{ currentField.name }}</label>
<select
:id="currentField.uuid"
:required="true"
class="select base-input !text-2xl w-full text-center font-normal"
:name="`values[${currentField.uuid}]`"
@change="values[currentField.uuid] = $event.target.value"
@focus="$refs.areas.scrollIntoField(currentField)"
<div v-else-if="currentField.type === 'radio'">
<label
v-if="currentField.name"
:for="currentField.uuid"
class="label text-2xl mb-2"
>{{ currentField.name }}</label>
<div class="flex w-full">
<div class="space-y-3.5 mx-auto">
<div
v-for="(option, index) in currentField.options"
:key="index"
>
<label
:for="currentField.uuid + option"
class="flex items-center space-x-3"
>
<input
:id="currentField.uuid + option"
v-model="values[currentField.uuid]"
type="radio"
class="base-radio !h-7 !w-7"
:name="`values[${currentField.uuid}]`"
:value="option"
required
>
<span class="text-xl">
{{ option }}
</span>
</label>
</div>
</div>
</div>
</div>
<MultiSelectStep
v-else-if="currentField.type === 'multiple'"
v-model="values[currentField.uuid]"
:field="currentField"
/>
<div
v-else-if="currentField.type === 'checkbox'"
class="flex w-full"
>
<option
value=""
:selected="!values[currentField.uuid]"
<input
type="hidden"
name="cast_boolean"
value="true"
>
Select your option
</option>
<option
v-for="(option, index) in currentField.options"
:key="index"
:selected="values[currentField.uuid] == option"
:value="option"
<div
class="space-y-3.5 mx-auto"
>
{{ option }}
</option>
</select>
</div>
<div v-else-if="currentField.type === 'radio'">
<label
v-if="currentField.name"
:for="currentField.uuid"
class="label text-2xl mb-2"
>{{ currentField.name }}</label>
<div class="flex w-full">
<div class="space-y-3.5 mx-auto">
<div
v-for="(option, index) in currentField.options"
:key="index"
v-for="(field, index) in currentStepFields"
:key="field.uuid"
>
<label
:for="currentField.uuid + option"
:for="field.uuid"
class="flex items-center space-x-3"
>
<input
:id="currentField.uuid + option"
v-model="values[currentField.uuid]"
type="radio"
class="base-radio !h-7 !w-7"
:name="`values[${currentField.uuid}]`"
:value="option"
required
type="hidden"
:name="`values[${field.uuid}]`"
:value="!!values[field.uuid]"
>
<input
:id="field.uuid"
type="checkbox"
class="base-checkbox !h-7 !w-7"
:checked="!!values[field.uuid]"
@click="values[field.uuid] = !values[field.uuid]"
>
<span class="text-xl">
{{ option }}
{{ currentField.name || currentField.type + ' ' + (index + 1) }}
</span>
</label>
</div>
</div>
</div>
<ImageStep
v-else-if="currentField.type === 'image'"
v-model="values[currentField.uuid]"
:field="currentField"
:attachments-index="attachmentsIndex"
:submitter-slug="submitterSlug"
@attached="attachments.push($event)"
/>
<SignatureStep
v-else-if="currentField.type === 'signature'"
ref="currentStep"
v-model="values[currentField.uuid]"
:field="currentField"
:attachments-index="attachmentsIndex"
:submitter-slug="submitterSlug"
@attached="attachments.push($event)"
/>
<AttachmentStep
v-else-if="currentField.type === 'file'"
v-model="values[currentField.uuid]"
:field="currentField"
:attachments-index="attachmentsIndex"
:submitter-slug="submitterSlug"
@attached="attachments.push($event)"
/>
</div>
<MultiSelectStep
v-else-if="currentField.type === 'multiple'"
v-model="values[currentField.uuid]"
:field="currentField"
/>
<div
v-else-if="currentField.type === 'checkbox'"
class="flex w-full"
>
<div
class="space-y-3.5 mx-auto"
<div class="mt-8">
<button
type="submit"
class="base-button w-full"
:disabled="isSubmitting"
>
<div
v-for="(field, index) in currentStepFields"
:key="field.uuid"
>
<label
:for="field.uuid"
class="flex items-center space-x-3"
>
<input
:id="field.uuid"
type="checkbox"
:name="`values[${field.uuid}]`"
:value="false"
class="base-checkbox !h-7 !w-7"
:checked="!!values[field.uuid]"
@click="values[field.uuid] = !values[field.uuid]"
>
<span class="text-xl">
{{ currentField.name || currentField.type + ' ' + (index + 1) }}
</span>
</label>
</div>
</div>
<span v-if="isSubmitting">
Submitting...
</span>
<span v-else>
Submit
</span>
</button>
</div>
<ImageStep
v-else-if="currentField.type === 'image'"
v-model="values[currentField.uuid]"
:field="currentField"
:attachments-index="attachmentsIndex"
:submitter-slug="submitterSlug"
@attached="attachments.push($event)"
/>
<SignatureStep
v-else-if="currentField.type === 'signature'"
ref="currentStep"
v-model="values[currentField.uuid]"
:field="currentField"
:attachments-index="attachmentsIndex"
:submitter-slug="submitterSlug"
@attached="attachments.push($event)"
/>
<AttachmentStep
v-else-if="currentField.type === 'file'"
v-model="values[currentField.uuid]"
:field="currentField"
:attachments-index="attachmentsIndex"
:submitter-slug="submitterSlug"
@attached="attachments.push($event)"
/>
</div>
<div class="mt-8">
<button
type="submit"
class="base-button w-full"
>
<span v-if="isSubmitting">
Submitting...
</span>
<span v-else>
Submit
</span>
</button>
</div>
</form>
<FormCompleted
v-else
:submitter-slug="submitterSlug"
/>
<div class="flex justify-center">
<div class="flex items-center mt-5 mb-1">
<a
@ -214,20 +246,17 @@
:key="step[0].uuid"
href="#"
class="inline border border-base-300 h-3 w-3 rounded-full mx-1"
:class="{ 'bg-base-200': index === currentStep, 'bg-base-content': index < currentStep, 'bg-white': index > currentStep }"
@click.prevent="goToStep(step, true)"
:class="{ 'bg-base-200': index === currentStep, 'bg-base-content': index < currentStep || isCompleted, 'bg-white': index > currentStep }"
@click.prevent="isCompleted ? '' : goToStep(step, true)"
/>
</div>
</div>
</form>
<FormCompleted
v-else
:submitter-slug="submitterSlug"
/>
</div>
</template>
<script>
import FieldAreas from './areas'
import FieldArea from './area'
import ImageStep from './image_step'
import SignatureStep from './signature_step'
import AttachmentStep from './attachment_step'
@ -238,6 +267,7 @@ export default {
name: 'SubmissionForm',
components: {
FieldAreas,
FieldArea,
ImageStep,
SignatureStep,
AttachmentStep,
@ -287,11 +317,14 @@ export default {
currentField () {
return this.currentStepFields[0]
},
submitterFields () {
currentSubmitterFields () {
return this.fields.filter((f) => f.submitter_uuid === this.submitterUuid)
},
otherSubmitterFields () {
return this.fields.filter((f) => f.submitter_uuid !== this.submitterUuid)
},
stepFields () {
return this.submitterFields.reduce((acc, f) => {
return this.currentSubmitterFields.reduce((acc, f) => {
const prevStep = acc[acc.length - 1]
if (f.type === 'checkbox' && Array.isArray(prevStep) && prevStep[0].type === 'checkbox') {

@ -36,9 +36,7 @@ class Submitter < ApplicationRecord
serialize :values, JSON
has_one_attached :archive
has_many_attached :documents
has_many_attached :attachments
def status

@ -1,6 +1,7 @@
<% attachment_field_uuids = @submitter.submission.template.fields.select { |f| f['type'].in?(%w[image signature file]) }.pluck('uuid') %>
<% attachments = ActiveStorage::Attachment.where(uuid: @submitter.values.values_at(*attachment_field_uuids).flatten).preload(:blob) %>
<div class="mx-auto block pb-72" style="max-width: 1000px">
<% values = @submitter.submission.submitters.reduce({}) { |acc, e| acc.merge(e.values) } %>
<% attachments = ActiveStorage::Attachment.where(uuid: values.values_at(*attachment_field_uuids).flatten).preload(:blob) %>
<div class="mx-auto block pb-72 select-none" style="max-width: 1000px">
<div class="mt-4 flex">
<a href="<%= root_path %>" class="mx-auto text-2xl md:text-3xl font-bold items-center flex space-x-3">
<%= render 'shared/logo', class: 'w-9 h-9 md:w-12 md:h-12' %>
@ -25,7 +26,7 @@
<div class="mx-auto" style="max-width: 1000px">
<div class="relative md:mx-32">
<div class="shadow-md bg-base-100 absolute bottom-0 md:bottom-4 w-full border border-base-200 p-4 rounded">
<submission-form data-submitter-uuid="<%= @submitter.uuid %>" data-submitter-slug="<%= @submitter.slug %>" data-attachments="<%= attachments.to_json(only: %i[uuid], methods: %i[url filename content_type]) %>" data-fields="<%= @submitter.submission.template.fields.to_json %>" data-values="<%= @submitter.values.to_json %>" data-authenticity-token="<%= form_authenticity_token %>"></submission-form>
<submission-form data-submitter-uuid="<%= @submitter.uuid %>" data-submitter-slug="<%= @submitter.slug %>" data-attachments="<%= attachments.to_json(only: %i[uuid], methods: %i[url filename content_type]) %>" data-fields="<%= @submitter.submission.template.fields.to_json %>" data-values="<%= values.to_json %>" data-authenticity-token="<%= form_authenticity_token %>"></submission-form>
</div>
</div>
</div>

@ -14,12 +14,11 @@ module Submissions
cert = submitter.submission.template.account.encrypted_configs
.find_by(key: EncryptedConfig::ESIGN_CERTS_KEY).value
zip_file = Tempfile.new
zip_stream = Zip::ZipOutputStream.open(zip_file)
pdfs_index = build_pdfs_index(submitter)
template.fields.each do |field|
next if field['submitter_uuid'] != submitter.uuid
field.fetch('areas', []).each do |area|
pdf = pdfs_index[area['attachment_uuid']]
@ -110,9 +109,6 @@ module Submissions
certificate_chain: [OpenSSL::X509::Certificate.new(cert['sub_ca']),
OpenSSL::X509::Certificate.new(cert['root_ca'])])
zip_stream.put_next_entry("#{item['name']}.pdf")
zip_stream.write(io.string)
ActiveStorage::Attachment.create!(
uuid: item['attachment_uuid'],
blob: ActiveStorage::Blob.create_and_upload!(
@ -122,10 +118,6 @@ module Submissions
record: submitter
)
end
zip_stream.close
submitter.archive.attach(io: zip_file, filename: "#{template.name}.zip")
end
# rubocop:enable Metrics

@ -14,6 +14,7 @@
"babel-loader": "9.1.2",
"babel-plugin-dynamic-import-node": "^2.3.3",
"babel-plugin-macros": "^3.1.0",
"canvas-confetti": "^1.6.0",
"compression-webpack-plugin": "10.0.0",
"css-loader": "^6.7.3",
"css-minimizer-webpack-plugin": "^5.0.0",

@ -1943,6 +1943,11 @@ caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001449, caniuse-lite@^1.0.30001464:
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001488.tgz#d19d7b6e913afae3e98f023db97c19e9ddc5e91f"
integrity sha512-NORIQuuL4xGpIy6iCCQGN4iFjlBXtfKWIenlUuyZJumLRIindLb7wXM+GO8erEhb7vXfcnf4BAg2PrSDN5TNLQ==
canvas-confetti@^1.6.0:
version "1.6.0"
resolved "https://registry.yarnpkg.com/canvas-confetti/-/canvas-confetti-1.6.0.tgz#193f71aa8f38fc850a5ba94f59091a7afdb43ead"
integrity sha512-ej+w/m8Jzpv9Z7W7uJZer14Ke8P2ogsjg4ZMGIuq4iqUOqY2Jq8BNW42iGmNfRwREaaEfFIczLuZZiEVSYNHAA==
chalk@^2.0.0:
version "2.4.2"
resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424"

Loading…
Cancel
Save