diff --git a/app/controllers/start_form_controller.rb b/app/controllers/start_form_controller.rb index 8e8d27d6..3ec5ad0f 100644 --- a/app/controllers/start_form_controller.rb +++ b/app/controllers/start_form_controller.rb @@ -21,7 +21,6 @@ class StartFormController < ApplicationController else @submitter.assign_attributes( uuid: @template.submitters.first['uuid'], - opened_at: Time.current, ip: request.remote_ip, ua: request.user_agent ) diff --git a/app/controllers/submit_form_controller.rb b/app/controllers/submit_form_controller.rb index cf27e8f1..e5385f32 100644 --- a/app/controllers/submit_form_controller.rb +++ b/app/controllers/submit_form_controller.rb @@ -15,6 +15,8 @@ class SubmitFormController < ApplicationController return redirect_to submit_form_completed_path(@submitter.slug) if @submitter.completed_at? + Submitters::MaybeUpdateDefaultValues.call(@submitter, current_user) + cookies[:submitter_sid] = @submitter.signed_id end diff --git a/app/controllers/user_signatures_controller.rb b/app/controllers/user_signatures_controller.rb new file mode 100644 index 00000000..69d0e795 --- /dev/null +++ b/app/controllers/user_signatures_controller.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +class UserSignaturesController < ApplicationController + before_action :load_user_config + authorize_resource :user_config + + def edit; end + + def update + file = params[:file] + + return redirect_to settings_profile_index_path, notice: 'Unable to save signature' if file.blank? + + blob = ActiveStorage::Blob.create_and_upload!(io: file.open, + filename: file.original_filename, + content_type: file.content_type) + + attachment = ActiveStorage::Attachment.create!( + blob:, + name: 'signature', + record: current_user + ) + + if @user_config.update(value: attachment.uuid) + redirect_to settings_profile_index_path, notice: 'Signature has been saved' + else + redirect_to settings_profile_index_path, notice: 'Unable to save signature' + end + end + + private + + def load_user_config + @user_config = + UserConfig.find_or_initialize_by(user: current_user, key: UserConfig::SIGNATURE_KEY) + end +end diff --git a/app/javascript/application.js b/app/javascript/application.js index 0bc13551..43941222 100644 --- a/app/javascript/application.js +++ b/app/javascript/application.js @@ -17,6 +17,7 @@ import SetTimezone from './elements/set_timezone' import AutoresizeTextarea from './elements/autoresize_textarea' import SubmittersAutocomplete from './elements/submitter_autocomplete' import FolderAutocomplete from './elements/folder_autocomplete' +import SignatureForm from './elements/signature_form' import * as TurboInstantClick from './lib/turbo_instant_click' @@ -47,6 +48,7 @@ window.customElements.define('set-timezone', SetTimezone) window.customElements.define('autoresize-textarea', AutoresizeTextarea) window.customElements.define('submitters-autocomplete', SubmittersAutocomplete) window.customElements.define('folder-autocomplete', FolderAutocomplete) +window.customElements.define('signature-form', SignatureForm) document.addEventListener('turbo:before-fetch-request', encodeMethodIntoRequestBody) document.addEventListener('turbo:submit-end', async (event) => { diff --git a/app/javascript/elements/signature_form.js b/app/javascript/elements/signature_form.js new file mode 100644 index 00000000..535fc8a6 --- /dev/null +++ b/app/javascript/elements/signature_form.js @@ -0,0 +1,46 @@ +import { target, targetable } from '@github/catalyst/lib/targetable' +import { cropCanvasAndExportToPNG } from '../submission_form/crop_canvas' + +export default targetable(class extends HTMLElement { + static [target.static] = ['canvas', 'input', 'clear', 'button'] + + async connectedCallback () { + this.canvas.width = this.canvas.parentNode.parentNode.clientWidth + this.canvas.height = this.canvas.parentNode.parentNode.clientWidth / 3 + + const { default: SignaturePad } = await import('signature_pad') + + this.pad = new SignaturePad(this.canvas) + + this.clear.addEventListener('click', (e) => { + e.preventDefault() + + this.pad.clear() + }) + + this.button.addEventListener('click', (e) => { + e.preventDefault() + + this.button.disabled = true + + this.submit() + }) + } + + async submit () { + const blob = await cropCanvasAndExportToPNG(this.canvas) + const file = new File([blob], 'signature.png', { type: 'image/png' }) + + const dataTransfer = new DataTransfer() + + dataTransfer.items.add(file) + + this.input.files = dataTransfer.files + + if (this.input.webkitEntries.length) { + this.input.dataset.file = `${dataTransfer.files[0].name}` + } + + this.closest('form').requestSubmit() + } +}) diff --git a/app/javascript/form.js b/app/javascript/form.js index df1eff3d..5d0284d7 100644 --- a/app/javascript/form.js +++ b/app/javascript/form.js @@ -13,6 +13,7 @@ window.customElements.define('submission-form', class extends HTMLElement { authenticityToken: this.dataset.authenticityToken, canSendEmail: this.dataset.canSendEmail === 'true', isDirectUpload: this.dataset.isDirectUpload === 'true', + goToLast: this.dataset.goToLast === 'true', isDemo: this.dataset.isDemo === 'true', attribution: this.dataset.attribution !== 'false', withConfetti: true, diff --git a/app/javascript/submission_form/crop_canvas.js b/app/javascript/submission_form/crop_canvas.js new file mode 100644 index 00000000..977c5ade --- /dev/null +++ b/app/javascript/submission_form/crop_canvas.js @@ -0,0 +1,49 @@ +function cropCanvasAndExportToPNG (canvas) { + const ctx = canvas.getContext('2d') + + const width = canvas.width + const height = canvas.height + + let topmost = height + let bottommost = 0 + let leftmost = width + let rightmost = 0 + + const imageData = ctx.getImageData(0, 0, width, height) + const pixels = imageData.data + + for (let y = 0; y < height; y++) { + for (let x = 0; x < width; x++) { + const pixelIndex = (y * width + x) * 4 + const alpha = pixels[pixelIndex + 3] + if (alpha !== 0) { + topmost = Math.min(topmost, y) + bottommost = Math.max(bottommost, y) + leftmost = Math.min(leftmost, x) + rightmost = Math.max(rightmost, x) + } + } + } + + const croppedWidth = rightmost - leftmost + 1 + const croppedHeight = bottommost - topmost + 1 + + const croppedCanvas = document.createElement('canvas') + croppedCanvas.width = croppedWidth + croppedCanvas.height = croppedHeight + const croppedCtx = croppedCanvas.getContext('2d') + + croppedCtx.drawImage(canvas, leftmost, topmost, croppedWidth, croppedHeight, 0, 0, croppedWidth, croppedHeight) + + return new Promise((resolve, reject) => { + croppedCanvas.toBlob((blob) => { + if (blob) { + resolve(blob) + } else { + reject(new Error('Failed to create a PNG blob.')) + } + }, 'image/png') + }) +} + +export { cropCanvasAndExportToPNG } diff --git a/app/javascript/submission_form/form.vue b/app/javascript/submission_form/form.vue index 26d1a420..4686fb72 100644 --- a/app/javascript/submission_form/form.vue +++ b/app/javascript/submission_form/form.vue @@ -415,6 +415,11 @@ export default { required: false, default: false }, + goToLast: { + type: Boolean, + required: false, + default: true + }, isDemo: { type: Boolean, required: false, @@ -488,10 +493,12 @@ export default { } }, mounted () { - this.currentStep = Math.min( - this.stepFields.indexOf([...this.stepFields].reverse().find((fields) => fields.some((f) => !!this.values[f.uuid]))) + 1, - this.stepFields.length - 1 - ) + if (this.goToLast) { + this.currentStep = Math.min( + this.stepFields.indexOf([...this.stepFields].reverse().find((fields) => fields.some((f) => !!this.values[f.uuid]))) + 1, + this.stepFields.length - 1 + ) + } if (/iPhone|iPad|iPod/i.test(navigator.userAgent)) { this.$nextTick(() => { diff --git a/app/javascript/submission_form/initials_step.vue b/app/javascript/submission_form/initials_step.vue index 82e6b74f..a0d9e124 100644 --- a/app/javascript/submission_form/initials_step.vue +++ b/app/javascript/submission_form/initials_step.vue @@ -76,7 +76,7 @@