From 170cb1ecea98b7e8bf3a59531852825168d1f3b9 Mon Sep 17 00:00:00 2001 From: Alex Turchyn Date: Thu, 25 May 2023 22:47:52 +0300 Subject: [PATCH] use vue for form submission --- .../send_submission_email_controller.rb | 17 +- app/controllers/submit_flow_controller.rb | 8 +- app/javascript/application.js | 12 +- app/javascript/elements/file_dropzone.js | 76 ----- app/javascript/elements/files_list.js | 16 -- app/javascript/elements/flow_area.js | 18 -- app/javascript/elements/flow_view.js | 110 ------- app/javascript/elements/signature_pad.js | 68 ----- app/javascript/flow.js | 39 ++- .../{components => flow_builder}/area.vue | 0 .../{components => flow_builder}/builder.vue | 16 +- .../{components => flow_builder}/document.vue | 0 .../{components => flow_builder}/field.vue | 0 .../{components => flow_builder}/fields.vue | 0 .../{components => flow_builder}/page.vue | 0 .../{components => flow_builder}/upload.vue | 0 app/javascript/flow_form/area.vue | 86 ++++++ app/javascript/flow_form/areas.vue | 77 +++++ app/javascript/flow_form/attachment_step.vue | 86 ++++++ app/javascript/flow_form/checkbox_step.vue | 54 ++++ app/javascript/flow_form/completed.vue | 70 +++++ app/javascript/flow_form/dropzone.vue | 112 ++++++++ app/javascript/flow_form/form.vue | 269 ++++++++++++++++++ app/javascript/flow_form/image_step.vue | 67 +++++ app/javascript/flow_form/signature_step.vue | 101 +++++++ app/views/submit_flow/_area.html.erb | 8 - app/views/submit_flow/show.html.erb | 115 +------- tailwind.flow.config.js | 1 + 28 files changed, 977 insertions(+), 449 deletions(-) delete mode 100644 app/javascript/elements/file_dropzone.js delete mode 100644 app/javascript/elements/files_list.js delete mode 100644 app/javascript/elements/flow_area.js delete mode 100644 app/javascript/elements/flow_view.js delete mode 100644 app/javascript/elements/signature_pad.js rename app/javascript/{components => flow_builder}/area.vue (100%) rename app/javascript/{components => flow_builder}/builder.vue (93%) rename app/javascript/{components => flow_builder}/document.vue (100%) rename app/javascript/{components => flow_builder}/field.vue (100%) rename app/javascript/{components => flow_builder}/fields.vue (100%) rename app/javascript/{components => flow_builder}/page.vue (100%) rename app/javascript/{components => flow_builder}/upload.vue (100%) create mode 100644 app/javascript/flow_form/area.vue create mode 100644 app/javascript/flow_form/areas.vue create mode 100644 app/javascript/flow_form/attachment_step.vue create mode 100644 app/javascript/flow_form/checkbox_step.vue create mode 100644 app/javascript/flow_form/completed.vue create mode 100644 app/javascript/flow_form/dropzone.vue create mode 100644 app/javascript/flow_form/form.vue create mode 100644 app/javascript/flow_form/image_step.vue create mode 100644 app/javascript/flow_form/signature_step.vue delete mode 100644 app/views/submit_flow/_area.html.erb diff --git a/app/controllers/send_submission_email_controller.rb b/app/controllers/send_submission_email_controller.rb index 4f14125e..0a88bf62 100644 --- a/app/controllers/send_submission_email_controller.rb +++ b/app/controllers/send_submission_email_controller.rb @@ -4,18 +4,23 @@ class SendSubmissionEmailController < ApplicationController layout 'flow' skip_before_action :authenticate_user! + skip_before_action :verify_authenticity_token def success; end def create - @submission = if params[:flow_slug] - Submission.joins(:flow).find_by!(email: params[:email], flow: { slug: params[:flow_slug] }) - else - Submission.find_by!(slug: params[:submission_slug]) - end + @submission = + if params[:flow_slug] + Submission.joins(:flow).find_by!(email: params[:email], flow: { slug: params[:flow_slug] }) + else + Submission.find_by!(slug: params[:submission_slug]) + end SubmissionMailer.copy_to_submitter(@submission).deliver_later! - redirect_to success_send_submission_email_index_path + respond_to do |f| + f.html { redirect_to success_send_submission_email_index_path } + f.json { head :ok } + end end end diff --git a/app/controllers/submit_flow_controller.rb b/app/controllers/submit_flow_controller.rb index 90e862a9..66445b68 100644 --- a/app/controllers/submit_flow_controller.rb +++ b/app/controllers/submit_flow_controller.rb @@ -14,7 +14,7 @@ class SubmitFlowController < ApplicationController def update submission = Submission.find_by!(slug: params[:slug]) - submission.values.merge!(params[:values].to_unsafe_h) + submission.values.merge!(normalized_values) submission.completed_at = Time.current if params[:completed] == 'true' submission.save @@ -25,4 +25,10 @@ class SubmitFlowController < ApplicationController def completed @submission = Submission.find_by!(slug: params[:submit_flow_slug]) end + + private + + def normalized_values + params[:values].to_unsafe_h.transform_values { |v| v.is_a?(Array) ? v.compact_blank : v } + end end diff --git a/app/javascript/application.js b/app/javascript/application.js index a2087482..d4748616 100644 --- a/app/javascript/application.js +++ b/app/javascript/application.js @@ -1,27 +1,23 @@ import '@hotwired/turbo-rails' -import { createApp } from 'vue' +import { createApp, reactive } from 'vue' import ToggleVisible from './elements/toggle_visible' import DisableHidden from './elements/disable_hidden' import TurboModal from './elements/turbo_modal' -import FlowArea from './elements/flow_area' -import FlowView from './elements/flow_view' -import Builder from './components/builder' +import FlowBuilder from './flow_builder/builder' window.customElements.define('toggle-visible', ToggleVisible) window.customElements.define('disable-hidden', DisableHidden) window.customElements.define('turbo-modal', TurboModal) -window.customElements.define('flow-view', FlowView) -window.customElements.define('flow-area', FlowArea) window.customElements.define('flow-builder', class extends HTMLElement { connectedCallback () { this.appElem = document.createElement('div') - this.app = createApp(Builder, { - dataFlow: this.dataset.flow + this.app = createApp(FlowBuilder, { + flow: reactive(JSON.parse(this.dataset.flow)) }) this.app.mount(this.appElem) diff --git a/app/javascript/elements/file_dropzone.js b/app/javascript/elements/file_dropzone.js deleted file mode 100644 index f855e84d..00000000 --- a/app/javascript/elements/file_dropzone.js +++ /dev/null @@ -1,76 +0,0 @@ -import { DirectUpload } from '@rails/activestorage' - -import { actionable } from '@github/catalyst/lib/actionable' -import { target, targetable } from '@github/catalyst/lib/targetable' - -export default actionable(targetable(class extends HTMLElement { - static [target.static] = [ - 'loading', - 'input' - ] - - connectedCallback () { - this.addEventListener('drop', this.onDrop) - - this.addEventListener('dragover', (e) => e.preventDefault()) - } - - onDrop (e) { - e.preventDefault() - - this.uploadFiles(e.dataTransfer.files) - } - - onSelectFiles (e) { - e.preventDefault() - - this.uploadFiles(this.input.files).then(() => { - this.input.value = '' - }) - } - - async uploadFiles (files) { - const blobs = await Promise.all( - Array.from(files).map(async (file) => { - const upload = new DirectUpload( - file, - '/direct_uploads', - this.input - ) - - return new Promise((resolve, reject) => { - upload.create((error, blob) => { - if (error) { - console.error(error) - - return reject(error) - } else { - return resolve(blob) - } - }) - }).catch((error) => { - console.error(error) - }) - }) - ) - - await Promise.all( - blobs.map((blob) => { - return fetch('/api/attachments', { - method: 'POST', - body: JSON.stringify({ - name: 'attachments', - blob_signed_id: blob.signed_id, - submission_slug: this.dataset.submissionSlug - }), - headers: { 'Content-Type': 'application/json' } - }).then(resp => resp.json()).then((data) => { - return data - }) - })).then((result) => { - result.forEach((attachment) => { - this.dispatchEvent(new CustomEvent('upload', { detail: attachment })) - }) - }) - } -})) diff --git a/app/javascript/elements/files_list.js b/app/javascript/elements/files_list.js deleted file mode 100644 index b3623fd1..00000000 --- a/app/javascript/elements/files_list.js +++ /dev/null @@ -1,16 +0,0 @@ -import { actionable } from '@github/catalyst/lib/actionable' -import { targets, targetable } from '@github/catalyst/lib/targetable' - -export default actionable(targetable(class extends HTMLElement { - static [targets.static] = [ - 'items' - ] - - add (e) { - const elem = document.createElement('input') - elem.value = e.detail.uuid - elem.name = `values[${this.dataset.fieldUuid}][]` - - this.prepend(elem) - } -})) diff --git a/app/javascript/elements/flow_area.js b/app/javascript/elements/flow_area.js deleted file mode 100644 index 811d2672..00000000 --- a/app/javascript/elements/flow_area.js +++ /dev/null @@ -1,18 +0,0 @@ -export default class extends HTMLElement { - setValue (value) { - const { fieldType } = this.dataset - - if (fieldType === 'signature') { - [...this.children].forEach(e => e.remove()) - - const img = document.createElement('img') - - img.classList.add('w-full', 'h-full', 'object-contain') - img.src = value.url - - this.append(img) - } else { - this.innerHTML = value - } - } -} diff --git a/app/javascript/elements/flow_view.js b/app/javascript/elements/flow_view.js deleted file mode 100644 index c935ffff..00000000 --- a/app/javascript/elements/flow_view.js +++ /dev/null @@ -1,110 +0,0 @@ -import { targets, target, targetable } from '@github/catalyst/lib/targetable' -import { actionable } from '@github/catalyst/lib/actionable' - -export default actionable(targetable(class extends HTMLElement { - static [target.static] = [ - 'form', - 'completed' - ] - - static [targets.static] = [ - 'areas', - 'fields', - 'steps' - ] - - passValueToArea (e) { - this.areasSetValue(e.target.id, e.target.value) - } - - areasSetValue (fieldUuid, value) { - return (this.areas || []).forEach((area) => { - if (area.dataset.fieldUuid === fieldUuid) { - area.setValue(value) - } - }) - } - - setVisibleStep (uuid) { - this.steps.forEach((step) => { - step.classList.toggle('hidden', step.dataset.fieldUuid !== uuid) - }) - - this.fields.find(f => f.id === uuid)?.focus() - } - - submitSignature (e) { - e.target.okButton.disabled = true - - fetch(this.form.action, { - method: this.form.method, - body: new FormData(this.form) - }).then(response => { - console.log('Form submitted successfully!', response) - this.moveNextStep() - }).catch(error => { - console.error('Error submitting form:', error) - }).finally(() => { - e.target.okButton.disabled = false - - this.areasSetValue(e.target.closest('disable-hidden').dataset.fieldUuid, e.detail) - }) - } - - submitForm (e) { - e.preventDefault() - - e.submitter.setAttribute('disabled', true) - - fetch(this.form.action, { - method: this.form.method, - body: new FormData(this.form) - }).then(response => { - console.log('Form submitted successfully!', response) - this.moveNextStep() - }).catch(error => { - console.error('Error submitting form:', error) - }).finally(() => { - e.submitter.removeAttribute('disabled') - }) - } - - moveStepBack (e) { - e?.preventDefault() - - const currentStepIndex = this.steps.findIndex((el) => !el.classList.contains('hidden')) - - const previousStep = this.steps[currentStepIndex - 1] - - if (previousStep) { - this.setVisibleStep(previousStep.dataset.fieldUuid) - } - } - - moveNextStep (e) { - e?.preventDefault() - - const currentStepIndex = this.steps.findIndex((el) => !el.classList.contains('hidden')) - - const nextStep = this.steps[currentStepIndex + 1] - - if (nextStep) { - this.setVisibleStep(nextStep.dataset.fieldUuid) - } else { - this.form.classList.add('hidden') - this.completed.classList.remove('hidden') - } - } - - focusField ({ target }) { - this.setVisibleStep(target.closest('flow-area').dataset.fieldUuid) - } - - focusArea ({ target }) { - const area = this.areas.find(a => target.id === a.dataset.fieldUuid) - - if (area) { - area.scrollIntoView({ behavior: 'smooth', block: 'center' }) - } - } -})) diff --git a/app/javascript/elements/signature_pad.js b/app/javascript/elements/signature_pad.js deleted file mode 100644 index 03a4dc4f..00000000 --- a/app/javascript/elements/signature_pad.js +++ /dev/null @@ -1,68 +0,0 @@ -import SignaturePad from 'signature_pad' -import { target, targetable } from '@github/catalyst/lib/targetable' -import { actionable } from '@github/catalyst/lib/actionable' - -import { DirectUpload } from '@rails/activestorage' - -export default actionable(targetable(class extends HTMLElement { - static [target.static] = [ - 'canvas', - 'input', - 'okButton', - 'image', - 'clearButton', - 'redrawButton', - 'nextButton' - ] - - connectedCallback () { - this.pad = new SignaturePad(this.canvas) - } - - submit (e) { - e?.preventDefault() - - this.okButton.disabled = true - - this.canvas.toBlob((blob) => { - const file = new File([blob], 'signature.png', { type: 'image/png' }) - - new DirectUpload( - file, - '/direct_uploads' - ).create((_error, data) => { - fetch('/api/attachments', { - method: 'POST', - body: JSON.stringify({ - submission_slug: this.dataset.submissionSlug, - blob_signed_id: data.signed_id, - name: 'signatures' - }), - headers: { 'Content-Type': 'application/json' } - }).then((resp) => resp.json()).then((attachment) => { - this.input.value = attachment.uuid - this.dispatchEvent(new CustomEvent('upload', { detail: attachment })) - }) - }) - }, 'image/png') - } - - redraw (e) { - e?.preventDefault() - - this.input.value = '' - - this.canvas.classList.remove('hidden') - this.clearButton.classList.remove('hidden') - this.okButton.classList.remove('hidden') - this.image.remove() - this.redrawButton.remove() - this.nextButton.remove() - } - - clear (e) { - e?.preventDefault() - - this.pad.clear() - } -})) diff --git a/app/javascript/flow.js b/app/javascript/flow.js index a685cfd7..28ec9e0c 100644 --- a/app/javascript/flow.js +++ b/app/javascript/flow.js @@ -1,13 +1,26 @@ -import FlowArea from './elements/flow_area' -import FlowView from './elements/flow_view' -import DisableHidden from './elements/disable_hidden' -import FileDropzone from './elements/file_dropzone' -import SignaturePad from './elements/signature_pad' -import FilesList from './elements/files_list' - -window.customElements.define('flow-view', FlowView) -window.customElements.define('flow-area', FlowArea) -window.customElements.define('disable-hidden', DisableHidden) -window.customElements.define('file-dropzone', FileDropzone) -window.customElements.define('signature-pad', SignaturePad) -window.customElements.define('files-list', FilesList) +import { createApp, reactive } from 'vue' + +import Flow from './flow_form/form' + +window.customElements.define('flow-form', class extends HTMLElement { + connectedCallback () { + this.appElem = document.createElement('div') + + this.app = createApp(Flow, { + submissionSlug: this.dataset.submissionSlug, + authenticityToken: this.dataset.authenticityToken, + values: reactive(JSON.parse(this.dataset.values)), + attachments: reactive(JSON.parse(this.dataset.attachments)), + fields: JSON.parse(this.dataset.fields) + }) + + this.app.mount(this.appElem) + + this.appendChild(this.appElem) + } + + disconnectedCallback () { + this.app?.unmount() + this.appElem?.remove() + } +}) diff --git a/app/javascript/components/area.vue b/app/javascript/flow_builder/area.vue similarity index 100% rename from app/javascript/components/area.vue rename to app/javascript/flow_builder/area.vue diff --git a/app/javascript/components/builder.vue b/app/javascript/flow_builder/builder.vue similarity index 93% rename from app/javascript/components/builder.vue rename to app/javascript/flow_builder/builder.vue index 3f293838..9b8f63c5 100644 --- a/app/javascript/components/builder.vue +++ b/app/javascript/flow_builder/builder.vue @@ -81,21 +81,15 @@ export default { Fields }, props: { - dataFlow: { - type: String, - default: '{}' + flow: { + type: Object, + required: true } }, data () { return { drawField: null, - dragFieldType: null, - flow: { - name: '', - schema: [], - documents: [], - fields: [] - } + dragFieldType: null } }, computed: { @@ -121,8 +115,6 @@ export default { } }, mounted () { - this.flow = JSON.parse(this.dataFlow) - document.addEventListener('keyup', this.disableDrawOnEsc) }, unmounted () { diff --git a/app/javascript/components/document.vue b/app/javascript/flow_builder/document.vue similarity index 100% rename from app/javascript/components/document.vue rename to app/javascript/flow_builder/document.vue diff --git a/app/javascript/components/field.vue b/app/javascript/flow_builder/field.vue similarity index 100% rename from app/javascript/components/field.vue rename to app/javascript/flow_builder/field.vue diff --git a/app/javascript/components/fields.vue b/app/javascript/flow_builder/fields.vue similarity index 100% rename from app/javascript/components/fields.vue rename to app/javascript/flow_builder/fields.vue diff --git a/app/javascript/components/page.vue b/app/javascript/flow_builder/page.vue similarity index 100% rename from app/javascript/components/page.vue rename to app/javascript/flow_builder/page.vue diff --git a/app/javascript/components/upload.vue b/app/javascript/flow_builder/upload.vue similarity index 100% rename from app/javascript/components/upload.vue rename to app/javascript/flow_builder/upload.vue diff --git a/app/javascript/flow_form/area.vue b/app/javascript/flow_form/area.vue new file mode 100644 index 00000000..3b298de2 --- /dev/null +++ b/app/javascript/flow_form/area.vue @@ -0,0 +1,86 @@ + + + diff --git a/app/javascript/flow_form/areas.vue b/app/javascript/flow_form/areas.vue new file mode 100644 index 00000000..73564d5b --- /dev/null +++ b/app/javascript/flow_form/areas.vue @@ -0,0 +1,77 @@ + + + diff --git a/app/javascript/flow_form/attachment_step.vue b/app/javascript/flow_form/attachment_step.vue new file mode 100644 index 00000000..c66aafa9 --- /dev/null +++ b/app/javascript/flow_form/attachment_step.vue @@ -0,0 +1,86 @@ + + + diff --git a/app/javascript/flow_form/checkbox_step.vue b/app/javascript/flow_form/checkbox_step.vue new file mode 100644 index 00000000..f06cff95 --- /dev/null +++ b/app/javascript/flow_form/checkbox_step.vue @@ -0,0 +1,54 @@ + + diff --git a/app/javascript/flow_form/completed.vue b/app/javascript/flow_form/completed.vue new file mode 100644 index 00000000..ce5f007d --- /dev/null +++ b/app/javascript/flow_form/completed.vue @@ -0,0 +1,70 @@ + + + diff --git a/app/javascript/flow_form/dropzone.vue b/app/javascript/flow_form/dropzone.vue new file mode 100644 index 00000000..eaab2aff --- /dev/null +++ b/app/javascript/flow_form/dropzone.vue @@ -0,0 +1,112 @@ + + + diff --git a/app/javascript/flow_form/form.vue b/app/javascript/flow_form/form.vue new file mode 100644 index 00000000..094dbca3 --- /dev/null +++ b/app/javascript/flow_form/form.vue @@ -0,0 +1,269 @@ + + + diff --git a/app/javascript/flow_form/image_step.vue b/app/javascript/flow_form/image_step.vue new file mode 100644 index 00000000..003be1da --- /dev/null +++ b/app/javascript/flow_form/image_step.vue @@ -0,0 +1,67 @@ + + + diff --git a/app/javascript/flow_form/signature_step.vue b/app/javascript/flow_form/signature_step.vue new file mode 100644 index 00000000..03283c25 --- /dev/null +++ b/app/javascript/flow_form/signature_step.vue @@ -0,0 +1,101 @@ + + + diff --git a/app/views/submit_flow/_area.html.erb b/app/views/submit_flow/_area.html.erb deleted file mode 100644 index 3f7a7f2b..00000000 --- a/app/views/submit_flow/_area.html.erb +++ /dev/null @@ -1,8 +0,0 @@ -<% value = submission.values[field['uuid']] %> - - <% if field['type'] == 'signature' && attachment = attachments.find { |a| a.uuid == value } %> - - <% else %> - <%= value %> - <% end %> - diff --git a/app/views/submit_flow/show.html.erb b/app/views/submit_flow/show.html.erb index 973905b9..e5f7c1cc 100644 --- a/app/views/submit_flow/show.html.erb +++ b/app/views/submit_flow/show.html.erb @@ -1,4 +1,3 @@ -<% fields_index = Flows.build_field_areas_index(@submission.flow) %> <% attachment_field_uuids = @submission.flow.fields.select { |f| f['type'].in?(%w[image signature attachment]) }.pluck('uuid') %> <% attachments = ActiveStorage::Attachment.where(uuid: @submission.values.values_at(*attachment_field_uuids).flatten).preload(:blob) %> @@ -7,123 +6,13 @@ <% document.preview_images.sort_by { |a| a.filename.base.to_i }.each_with_index do |page, index| %>
-
- <% fields_index.dig(document.uuid, index)&.each do |values| %> - <%= render 'area', submission: @submission, attachments:, page:, **values %> - <% end %> -
+
<% end %> <% end %>
-
- - - <% visible_step_index = nil %> - <% @submission.flow.fields.each_with_index do |field, index| %> - <% visible_step_index ||= index if @submission.values[field['uuid']].blank? %> - - <% if index != 0 %> - - <% end %> - -
- <% if index == @submission.flow.fields.size - 1 %> - -
- -
- <% end %> - <% if field['type'].in?(['text', 'date']) %> - id="<%= field['uuid'] %>" data-targets="flow-view.fields" data-action="input:flow-view#passValueToArea focus:flow-view#focusArea" value="<%= @submission.values[field['uuid']] %>" type="<%= field['type'] %>" name="values[<%= field['uuid'] %>]"> -
- -
- <% elsif field['type'] == 'select' %> - -
- -
- <% elsif field['type'] == 'image' || field['type'] == 'attachment' %> -
- - - <% uuid = SecureRandom.uuid %> - - - - -
- -
- <% elsif field['type'] == 'signature' %> - <% attachment = attachments.find { |a| a.uuid == @submission.values[field['uuid']] } %> - - - - <% if attachment %> - - - - <% end %> - -
- -
- <% elsif field['type'] == 'radio' %> - <% field['options'].each do |option| %> -
- id="<%= field['uuid'] + option %>" type="radio" name="values[<%= field['uuid'] %>]" value="<%= option %>"> - -
- <% end %> -
- -
- <% elsif field['type'] == 'checkbox' %> - <% field['options'].each do |option| %> -
- id="<%= field['uuid'] + option %>" type="checkbox" name="values[<%= field['uuid'] %>]" value="<%= option %>"> - -
- <% end %> -
- -
- <% end %> -
- <% end %> -
- +
diff --git a/tailwind.flow.config.js b/tailwind.flow.config.js index 1658c0d0..b3c9cf09 100644 --- a/tailwind.flow.config.js +++ b/tailwind.flow.config.js @@ -3,6 +3,7 @@ const baseConfigs = require('./tailwind.config.js') module.exports = { ...baseConfigs, content: [ + './app/javascript/flow_form/**/*.vue', './app/views/submit_flow/**/*.erb', './app/views/start_flow/**/*.erb', './app/views/send_submission_copy/**/*.erb'