From 635e5bc75cafcf90a9d6fd5527db713356e72970 Mon Sep 17 00:00:00 2001 From: Bernardo Anderson Date: Thu, 30 Jul 2026 16:21:40 -0500 Subject: [PATCH 1/2] CP-14721 - Show success toast after DocuSeal document upload MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit What After uploading or replacing a document in a DocuSeal task form, users now see a short success toast confirming the upload worked Why Upload could finish with no clear feedback, so people weren’t sure whether the document was saved before they continued editing the form How to test 1. Run DocuSeal (foreman start -f Procfile.dev) and open a form at http://localhost:3001 2. On an empty form, upload a PDF/image via the dropzone — expect “Document uploaded successfully” bottom-right, then auto-dismiss ~4s 3. Replace an existing document — expect the same toast once 4. Optional: blank page should not toast; multi-file replace should toast once, not once per file --- app/javascript/template_builder/builder.vue | 27 +++++++++--- app/javascript/template_builder/i18n.js | 6 +++ app/javascript/template_builder/toast.vue | 48 +++++++++++++++++++++ db/schema.rb | 2 +- spec/system/template_builder_spec.rb | 25 +++++++++++ 5 files changed, 101 insertions(+), 7 deletions(-) create mode 100644 app/javascript/template_builder/toast.vue diff --git a/app/javascript/template_builder/builder.vue b/app/javascript/template_builder/builder.vue index 17e6a248b5..2fcfc57042 100644 --- a/app/javascript/template_builder/builder.vue +++ b/app/javascript/template_builder/builder.vue @@ -346,6 +346,7 @@ @close="isShowSigningOrderModal = false" /> + @@ -364,6 +365,7 @@ import DocumentControls from './controls' import MobileFields from './mobile_fields' import FieldSubmitter from './field_submitter' import SigningOrderModal from './signing_order_modal' +import Toast from './toast' import { IconPlus, IconUsersPlus, IconDeviceFloppy, IconChevronDown, IconEye, IconWritingSign, IconInnerShadowTop, IconInfoCircle, IconAdjustments } from '@tabler/icons-vue' import { v4 } from 'uuid' import { ref, computed, toRaw, watch } from 'vue' @@ -393,7 +395,8 @@ export default { IconAdjustments, IconEye, IconDeviceFloppy, - SigningOrderModal + SigningOrderModal, + Toast }, provide () { return { @@ -1524,7 +1527,7 @@ export default { method: 'POST', body: formData }).then(async (resp) => { - this.updateFromUpload(await resp.json()) + this.updateFromUpload(await resp.json(), { showToast: false }) }).finally(() => { this.isLoadingBlankPage = false }) @@ -1533,7 +1536,11 @@ export default { onUploadFailed (error) { if (error) alert(error) }, - updateFromUpload (data) { + updateFromUpload (data, { showToast = true } = {}) { + if (showToast) { + this.$refs.toast.show(this.t('document_uploaded_successfully')) + } + this.template.schema.push(...data.schema) this.template.documents.push(...data.documents) @@ -1606,7 +1613,7 @@ export default { this.save() } }, - onDocumentReplace (data) { + onDocumentReplace (data, { showToast = true } = {}) { const { replaceSchemaItem, schema, documents } = data this.template.schema.splice(this.template.schema.indexOf(replaceSchemaItem), 1, { ...replaceSchemaItem, ...schema[0] }) @@ -1651,6 +1658,10 @@ export default { this.onUpload(this.template) } + if (showToast) { + this.$refs.toast.show(this.t('document_uploaded_successfully')) + } + this.save() }, onDocumentsReplace (data) { @@ -1662,16 +1673,20 @@ export default { replaceSchemaItem: existingSchemaItem, schema: [schemaItem], documents: [data.documents.find((doc) => doc.uuid === schemaItem.attachment_uuid)] - }) + }, { showToast: false }) } else { this.updateFromUpload({ schema: [schemaItem], documents: [data.documents.find((doc) => doc.uuid === schemaItem.attachment_uuid)], fields: data.fields, submitters: data.submitters - }) + }, { showToast: false }) } }) + + if (data.schema.length) { + this.$refs.toast.show(this.t('document_uploaded_successfully')) + } }, onDocumentsReplaceAndTemplateClone (template) { window.Turbo.visit(`/templates/${template.id}/edit`) diff --git a/app/javascript/template_builder/i18n.js b/app/javascript/template_builder/i18n.js index 93ddf1c7e1..a2bd4f3abd 100644 --- a/app/javascript/template_builder/i18n.js +++ b/app/javascript/template_builder/i18n.js @@ -130,6 +130,7 @@ const en = { replace: 'Replace', uploading_: 'Uploading...', add_document: 'Add Document', + document_uploaded_successfully: 'Document uploaded successfully', none: 'None', ssn: 'SSN', ein: 'EIN', @@ -276,6 +277,7 @@ const es = { replace: 'Reemplazar', uploading_: 'Subiendo...', add_document: 'Subir', + document_uploaded_successfully: 'Documento cargado correctamente', any: 'Cualquier', drawn: 'Dibujado', drawn_or_typed: 'Dibujado o Escrito', @@ -432,6 +434,7 @@ const it = { replace: 'Sostituisci', uploading_: 'Caricamento in corso...', add_document: 'Aggiungi', + document_uploaded_successfully: 'Documento caricato correttamente', none: 'Nessuno', ssn: 'SSN', ein: 'EIN', @@ -578,6 +581,7 @@ const pt = { replace: 'Substituir', uploading_: 'Carregando...', add_document: 'Enviar', + document_uploaded_successfully: 'Documento enviado com sucesso', any: 'Qualquer', drawn: 'Desenhado', drawn_or_typed: 'Desenhado ou Digitado', @@ -729,6 +733,7 @@ const fr = { replace: 'Remplacer', uploading_: 'Téléchargement en cours...', add_document: 'Télécharger', + document_uploaded_successfully: 'Document téléchargé avec succès', any: 'Tout', drawn: 'Dessiné', drawn_or_typed: 'Dessiné ou Tapé', @@ -880,6 +885,7 @@ const de = { replace: 'Ersetzen', uploading_: 'Hochladen...', add_document: 'Hochladen', + document_uploaded_successfully: 'Dokument erfolgreich hochgeladen', any: 'Jede', drawn: 'Gezeichnet', drawn_or_typed: 'Gezeichnet oder getippt', diff --git a/app/javascript/template_builder/toast.vue b/app/javascript/template_builder/toast.vue new file mode 100644 index 0000000000..5517a62695 --- /dev/null +++ b/app/javascript/template_builder/toast.vue @@ -0,0 +1,48 @@ + + + diff --git a/db/schema.rb b/db/schema.rb index f6dff08459..7cbfc1e1fb 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -181,7 +181,7 @@ t.datetime "created_at", null: false t.index ["account_id", "event_datetime"], name: "index_email_events_on_account_id_and_event_datetime" t.index ["email"], name: "index_email_events_on_email" - t.index ["email"], name: "index_email_events_on_email_event_types", where: "((event_type)::text = ANY ((ARRAY['bounce'::character varying, 'soft_bounce'::character varying, 'complaint'::character varying, 'soft_complaint'::character varying])::text[]))" + t.index ["email"], name: "index_email_events_on_email_event_types", where: "((event_type)::text = ANY (ARRAY[('bounce'::character varying)::text, ('soft_bounce'::character varying)::text, ('complaint'::character varying)::text, ('soft_complaint'::character varying)::text]))" t.index ["emailable_type", "emailable_id"], name: "index_email_events_on_emailable" t.index ["message_id"], name: "index_email_events_on_message_id" end diff --git a/spec/system/template_builder_spec.rb b/spec/system/template_builder_spec.rb index d14db058a2..f24291b7d4 100644 --- a/spec/system/template_builder_spec.rb +++ b/spec/system/template_builder_spec.rb @@ -27,6 +27,31 @@ end.to change { template.documents.count }.by(1) expect(page).to have_content('sample-image') + expect(page).to have_css('#upload_success_toast', text: 'Document uploaded successfully') + expect(page).to have_css('#upload_success_toast[role="status"][aria-live="polite"]') + end + end + + context 'when uploading to an empty template' do + let(:template) { create(:template, account:, author:, attachment_count: 0) } + + before do + visit edit_template_path(template) + end + + it 'shows a success toast after the first document upload' do + expect(page).to have_css('#document_dropzone') + + expect do + find('#document_dropzone input[type="file"]', visible: false) + .attach_file(Rails.root.join('spec/fixtures/sample-image.png')) + + page.driver.wait_for_network_idle + end.to change { template.reload.documents.count }.by(1) + + expect(page).to have_content('sample-image') + expect(page).to have_css('#upload_success_toast', text: 'Document uploaded successfully') + expect(page).to have_css('#upload_success_toast[role="status"][aria-live="polite"]') end end From b6ffcf53c76268f06e6354ee2c94341a5c7a3dfd Mon Sep 17 00:00:00 2001 From: Bernardo Anderson Date: Thu, 30 Jul 2026 17:03:09 -0500 Subject: [PATCH 2/2] CP-14721 - Drop unrelated schema.rb churn ## What Revert the accidental schema.rb formatting change from the upload toast PR ## Why The index SQL rewrite came from a local dump/version difference and is unrelated to the toast work ## How to test Confirm PR no longer includes db/schema.rb; toast behavior unchanged --- db/schema.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/db/schema.rb b/db/schema.rb index 7cbfc1e1fb..f6dff08459 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -181,7 +181,7 @@ t.datetime "created_at", null: false t.index ["account_id", "event_datetime"], name: "index_email_events_on_account_id_and_event_datetime" t.index ["email"], name: "index_email_events_on_email" - t.index ["email"], name: "index_email_events_on_email_event_types", where: "((event_type)::text = ANY (ARRAY[('bounce'::character varying)::text, ('soft_bounce'::character varying)::text, ('complaint'::character varying)::text, ('soft_complaint'::character varying)::text]))" + t.index ["email"], name: "index_email_events_on_email_event_types", where: "((event_type)::text = ANY ((ARRAY['bounce'::character varying, 'soft_bounce'::character varying, 'complaint'::character varying, 'soft_complaint'::character varying])::text[]))" t.index ["emailable_type", "emailable_id"], name: "index_email_events_on_emailable" t.index ["message_id"], name: "index_email_events_on_message_id" end