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 'strip_attributes'
gem 'turbo-rails' gem 'turbo-rails'
gem 'tzinfo-data' gem 'tzinfo-data'
gem 'zip'
group :development, :test do group :development, :test do
gem 'annotate' gem 'annotate'

@ -6,8 +6,8 @@ class SubmissionsDownloadController < ApplicationController
def index def index
submitter = Submitter.find_by(slug: params[:submitter_slug]) 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
end end

@ -10,7 +10,7 @@ class SubmitFormController < ApplicationController
Submitter.preload(submission: { template: { documents_attachments: { preview_images_attachments: :blob } } }) Submitter.preload(submission: { template: { documents_attachments: { preview_images_attachments: :blob } } })
.find_by!(slug: params[:slug]) .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 end
def update def update
@ -30,6 +30,12 @@ class SubmitFormController < ApplicationController
private private
def normalized_values 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
end end

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

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

@ -1,11 +1,11 @@
<template> <template>
<div <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" :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 <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" class="absolute top-0 bottom-0 right-0 left-0 items-center justify-center h-full w-full"
> >
<span <span
@ -65,6 +65,7 @@
class="w-full p-[0.2vw] flex items-center justify-center" class="w-full p-[0.2vw] flex items-center justify-center"
> >
<input <input
v-if="submittable"
type="checkbox" type="checkbox"
:value="false" :value="false"
class="aspect-square base-checkbox" class="aspect-square base-checkbox"
@ -72,6 +73,12 @@
:checked="!!modelValue" :checked="!!modelValue"
@click="$emit('update:model-value', !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>
<div <div
v-else v-else
@ -91,27 +98,29 @@
</template> </template>
<script> <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 { export default {
name: 'FieldArea', name: 'FieldArea',
components: { components: {
IconPaperclip IconPaperclip,
IconCheckbox,
IconSquare
}, },
props: { props: {
field: { field: {
type: Object, type: Object,
required: true required: true
}, },
step: { isValueSet: {
type: Array, type: Boolean,
required: false, required: false,
default: () => [] default: false
}, },
values: { submittable: {
type: Object, type: Boolean,
required: false, required: false,
default: () => ({}) default: false
}, },
modelValue: { modelValue: {
type: [Array, String, Number, Object, Boolean], type: [Array, String, Number, Object, Boolean],
@ -153,9 +162,6 @@ export default {
multiple: 'Multiple Select' multiple: 'Multiple Select'
} }
}, },
isValue () {
return this.step.some((f) => ![null, undefined, ''].includes(this.values[f.uuid]))
},
fieldIcons () { fieldIcons () {
return { return {
text: IconTextSize, text: IconTextSize,

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

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

@ -1,4 +1,21 @@
<template> <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 <FieldAreas
ref="areas" ref="areas"
:steps="stepFields" :steps="stepFields"
@ -7,6 +24,7 @@
:current-step="currentStepFields" :current-step="currentStepFields"
@focus-step="goToStep($event, false, true)" @focus-step="goToStep($event, false, true)"
/> />
<div>
<form <form
v-if="!isCompleted" v-if="!isCompleted"
ref="form" ref="form"
@ -140,6 +158,11 @@
<div <div
v-else-if="currentField.type === 'checkbox'" v-else-if="currentField.type === 'checkbox'"
class="flex w-full" class="flex w-full"
>
<input
type="hidden"
name="cast_boolean"
value="true"
> >
<div <div
class="space-y-3.5 mx-auto" class="space-y-3.5 mx-auto"
@ -151,12 +174,15 @@
<label <label
:for="field.uuid" :for="field.uuid"
class="flex items-center space-x-3" class="flex items-center space-x-3"
>
<input
type="hidden"
:name="`values[${field.uuid}]`"
:value="!!values[field.uuid]"
> >
<input <input
:id="field.uuid" :id="field.uuid"
type="checkbox" type="checkbox"
:name="`values[${field.uuid}]`"
:value="false"
class="base-checkbox !h-7 !w-7" class="base-checkbox !h-7 !w-7"
:checked="!!values[field.uuid]" :checked="!!values[field.uuid]"
@click="values[field.uuid] = !values[field.uuid]" @click="values[field.uuid] = !values[field.uuid]"
@ -198,6 +224,7 @@
<button <button
type="submit" type="submit"
class="base-button w-full" class="base-button w-full"
:disabled="isSubmitting"
> >
<span v-if="isSubmitting"> <span v-if="isSubmitting">
Submitting... Submitting...
@ -207,6 +234,11 @@
</span> </span>
</button> </button>
</div> </div>
</form>
<FormCompleted
v-else
:submitter-slug="submitterSlug"
/>
<div class="flex justify-center"> <div class="flex justify-center">
<div class="flex items-center mt-5 mb-1"> <div class="flex items-center mt-5 mb-1">
<a <a
@ -214,20 +246,17 @@
:key="step[0].uuid" :key="step[0].uuid"
href="#" href="#"
class="inline border border-base-300 h-3 w-3 rounded-full mx-1" 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 }" :class="{ 'bg-base-200': index === currentStep, 'bg-base-content': index < currentStep || isCompleted, 'bg-white': index > currentStep }"
@click.prevent="goToStep(step, true)" @click.prevent="isCompleted ? '' : goToStep(step, true)"
/> />
</div> </div>
</div> </div>
</form> </div>
<FormCompleted
v-else
:submitter-slug="submitterSlug"
/>
</template> </template>
<script> <script>
import FieldAreas from './areas' import FieldAreas from './areas'
import FieldArea from './area'
import ImageStep from './image_step' import ImageStep from './image_step'
import SignatureStep from './signature_step' import SignatureStep from './signature_step'
import AttachmentStep from './attachment_step' import AttachmentStep from './attachment_step'
@ -238,6 +267,7 @@ export default {
name: 'SubmissionForm', name: 'SubmissionForm',
components: { components: {
FieldAreas, FieldAreas,
FieldArea,
ImageStep, ImageStep,
SignatureStep, SignatureStep,
AttachmentStep, AttachmentStep,
@ -287,11 +317,14 @@ export default {
currentField () { currentField () {
return this.currentStepFields[0] return this.currentStepFields[0]
}, },
submitterFields () { currentSubmitterFields () {
return this.fields.filter((f) => f.submitter_uuid === this.submitterUuid) return this.fields.filter((f) => f.submitter_uuid === this.submitterUuid)
}, },
otherSubmitterFields () {
return this.fields.filter((f) => f.submitter_uuid !== this.submitterUuid)
},
stepFields () { stepFields () {
return this.submitterFields.reduce((acc, f) => { return this.currentSubmitterFields.reduce((acc, f) => {
const prevStep = acc[acc.length - 1] const prevStep = acc[acc.length - 1]
if (f.type === 'checkbox' && Array.isArray(prevStep) && prevStep[0].type === 'checkbox') { if (f.type === 'checkbox' && Array.isArray(prevStep) && prevStep[0].type === 'checkbox') {

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

@ -1,6 +1,7 @@
<% attachment_field_uuids = @submitter.submission.template.fields.select { |f| f['type'].in?(%w[image signature file]) }.pluck('uuid') %> <% 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) %> <% values = @submitter.submission.submitters.reduce({}) { |acc, e| acc.merge(e.values) } %>
<div class="mx-auto block pb-72" style="max-width: 1000px"> <% 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"> <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"> <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' %> <%= 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="mx-auto" style="max-width: 1000px">
<div class="relative md:mx-32"> <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"> <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> </div>
</div> </div>

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

@ -14,6 +14,7 @@
"babel-loader": "9.1.2", "babel-loader": "9.1.2",
"babel-plugin-dynamic-import-node": "^2.3.3", "babel-plugin-dynamic-import-node": "^2.3.3",
"babel-plugin-macros": "^3.1.0", "babel-plugin-macros": "^3.1.0",
"canvas-confetti": "^1.6.0",
"compression-webpack-plugin": "10.0.0", "compression-webpack-plugin": "10.0.0",
"css-loader": "^6.7.3", "css-loader": "^6.7.3",
"css-minimizer-webpack-plugin": "^5.0.0", "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" resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001488.tgz#d19d7b6e913afae3e98f023db97c19e9ddc5e91f"
integrity sha512-NORIQuuL4xGpIy6iCCQGN4iFjlBXtfKWIenlUuyZJumLRIindLb7wXM+GO8erEhb7vXfcnf4BAg2PrSDN5TNLQ== 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: chalk@^2.0.0:
version "2.4.2" version "2.4.2"
resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424"

Loading…
Cancel
Save