From 37196ff89fe4dd512f5d2bd4a8e0d8da0448538e Mon Sep 17 00:00:00 2001 From: Pete Matsyburka Date: Thu, 12 Feb 2026 09:22:08 +0200 Subject: [PATCH] add dynamic documents --- Dockerfile | 1 + app/controllers/api/templates_controller.rb | 2 +- .../preview_document_page_controller.rb | 12 +- app/controllers/start_form_controller.rb | 2 + app/controllers/submissions_controller.rb | 28 +- .../template_documents_controller.rb | 4 +- app/controllers/templates_controller.rb | 7 +- app/controllers/templates_debug_controller.rb | 33 +- .../templates_preview_controller.rb | 2 +- .../templates_uploads_controller.rb | 2 +- app/javascript/application.js | 2 + app/javascript/template_builder/area.vue | 318 +------- .../template_builder/area_title.vue | 404 +++++++++ app/javascript/template_builder/builder.vue | 140 +++- app/javascript/template_builder/document.vue | 2 +- .../template_builder/dynamic_area.vue | 282 +++++++ .../template_builder/dynamic_document.vue | 225 +++++ .../template_builder/dynamic_editor.js | 768 ++++++++++++++++++ .../template_builder/dynamic_menu.vue | 211 +++++ .../template_builder/dynamic_section.vue | 487 +++++++++++ .../template_builder/dynamic_styles.scss | 22 + .../template_builder/dynamic_variable.vue | 422 ++++++++++ .../template_builder/dynamic_variables.vue | 134 +++ .../dynamic_variables_schema.js | 559 +++++++++++++ .../template_builder/field_settings.vue | 15 +- app/javascript/template_builder/fields.vue | 66 +- app/javascript/template_builder/i18n.js | 42 + app/javascript/template_builder/preview.vue | 205 +++-- app/jobs/generate_attachment_preview_job.rb | 21 - app/models/document_generation_event.rb | 2 +- app/models/dynamic_document.rb | 36 + app/models/dynamic_document_version.rb | 30 + app/models/email_event.rb | 2 +- app/models/lock_event.rb | 2 +- app/models/submission.rb | 55 +- app/models/submission_event.rb | 2 +- app/models/template.rb | 6 + app/views/submissions/_detailed_form.html.erb | 7 +- app/views/submissions/_email_form.html.erb | 30 +- app/views/submissions/_phone_form.html.erb | 7 +- .../submissions/_variables_form.html.erb | 0 app/views/submissions/new.html.erb | 18 +- app/views/submissions/show.html.erb | 2 +- app/views/submit_form/show.html.erb | 2 +- app/views/templates/_title.html.erb | 4 +- app/views/templates/show.html.erb | 2 +- config/environments/production.rb | 2 +- config/initializers/active_storage.rb | 4 + config/locales/i18n.yml | 7 + config/routes.rb | 2 +- config/webpack/webpack.config.js | 8 +- ...20260206100322_create_dynamic_documents.rb | 15 + ...162053_create_dynamic_document_versions.rb | 15 + db/schema.rb | 23 +- lib/params/submission_create_validator.rb | 1 + lib/puma/plugin/sidekiq_embed.rb | 2 +- lib/submissions.rb | 10 +- lib/submissions/create_from_submitters.rb | 42 +- lib/templates/clone.rb | 1 + lib/templates/clone_attachments.rb | 32 +- lib/templates/create_attachments.rb | 20 +- lib/templates/replace_attachments.rb | 2 +- package.json | 3 + tailwind.dynamic.config.js | 18 + yarn.lock | 21 +- 65 files changed, 4370 insertions(+), 483 deletions(-) create mode 100644 app/javascript/template_builder/area_title.vue create mode 100644 app/javascript/template_builder/dynamic_area.vue create mode 100644 app/javascript/template_builder/dynamic_document.vue create mode 100644 app/javascript/template_builder/dynamic_editor.js create mode 100644 app/javascript/template_builder/dynamic_menu.vue create mode 100644 app/javascript/template_builder/dynamic_section.vue create mode 100644 app/javascript/template_builder/dynamic_styles.scss create mode 100644 app/javascript/template_builder/dynamic_variable.vue create mode 100644 app/javascript/template_builder/dynamic_variables.vue create mode 100644 app/javascript/template_builder/dynamic_variables_schema.js delete mode 100644 app/jobs/generate_attachment_preview_job.rb create mode 100644 app/models/dynamic_document.rb create mode 100644 app/models/dynamic_document_version.rb create mode 100644 app/views/submissions/_variables_form.html.erb create mode 100644 db/migrate/20260206100322_create_dynamic_documents.rb create mode 100644 db/migrate/20260216162053_create_dynamic_document_versions.rb create mode 100644 tailwind.dynamic.config.js diff --git a/Dockerfile b/Dockerfile index 52531940..23ee1f4b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -36,6 +36,7 @@ COPY ./config/shakapacker.yml ./config/shakapacker.yml COPY ./postcss.config.js ./postcss.config.js COPY ./tailwind.config.js ./tailwind.config.js COPY ./tailwind.form.config.js ./tailwind.form.config.js +COPY ./tailwind.dynamic.config.js ./tailwind.dynamic.config.js COPY ./tailwind.application.config.js ./tailwind.application.config.js COPY ./app/javascript ./app/javascript COPY ./app/views ./app/views diff --git a/app/controllers/api/templates_controller.rb b/app/controllers/api/templates_controller.rb index c3f1dd42..df4bcee7 100644 --- a/app/controllers/api/templates_controller.rb +++ b/app/controllers/api/templates_controller.rb @@ -117,7 +117,7 @@ module Api conditions: [%i[field_uuid value action operation]], options: [%i[value uuid]], validation: %i[message pattern min max step], - areas: [%i[x y w h cell_w attachment_uuid option_uuid page]] }]] + areas: [%i[uuid x y w h cell_w attachment_uuid option_uuid page]] }]] } ] diff --git a/app/controllers/preview_document_page_controller.rb b/app/controllers/preview_document_page_controller.rb index 5e59e74a..d0b69f8d 100644 --- a/app/controllers/preview_document_page_controller.rb +++ b/app/controllers/preview_document_page_controller.rb @@ -6,9 +6,17 @@ class PreviewDocumentPageController < ActionController::API FORMAT = Templates::ProcessDocument::FORMAT def show - attachment_uuid = ApplicationRecord.signed_id_verifier.verified(params[:signed_uuid], purpose: :attachment) + result_data = + ApplicationRecord.signed_id_verifier.verified(params[:signed_key], purpose: :attachment) - attachment = ActiveStorage::Attachment.find_by(uuid: attachment_uuid) if attachment_uuid + attachment = + if result_data.is_a?(Array) && result_data.compact_blank.size == 2 + attachment_id, attachment_uuid = result_data + + ActiveStorage::Attachment.find_by(id: attachment_id, uuid: attachment_uuid) + elsif result_data + ActiveStorage::Attachment.find_by(uuid: result_data) + end return head :not_found unless attachment diff --git a/app/controllers/start_form_controller.rb b/app/controllers/start_form_controller.rb index ad5d6455..ed9c2629 100644 --- a/app/controllers/start_form_controller.rb +++ b/app/controllers/start_form_controller.rb @@ -172,6 +172,8 @@ class StartFormController < ApplicationController submitters: [submitter], source: :link) + Submissions::CreateFromSubmitters.maybe_set_dynamic_documents(submitter.submission) + submitter.account_id = submitter.submission.account_id submitter diff --git a/app/controllers/submissions_controller.rb b/app/controllers/submissions_controller.rb index c39e99e5..d0caf466 100644 --- a/app/controllers/submissions_controller.rb +++ b/app/controllers/submissions_controller.rb @@ -51,18 +51,7 @@ class SubmissionsController < ApplicationController emails: params[:emails], params: params.merge('send_completed_email' => true)) else - submissions_attrs = submissions_params[:submission].to_h.values - - submissions_attrs, _, new_fields = - Submissions::NormalizeParamUtils.normalize_submissions_params!(submissions_attrs, @template, add_fields: true) - - Submissions.create_from_submitters(template: @template, - user: current_user, - source: :invite, - submitters_order: params[:preserve_order] == '1' ? 'preserved' : 'random', - submissions_attrs:, - new_fields:, - params: params.merge('send_completed_email' => true)) + create_submissions(@template, submissions_params, params) end WebhookUrls.enqueue_events(submissions, 'submission.created') @@ -97,6 +86,21 @@ class SubmissionsController < ApplicationController private + def create_submissions(template, submissions_params, params) + submissions_attrs = submissions_params[:submission].to_h.values + + submissions_attrs, _, new_fields = + Submissions::NormalizeParamUtils.normalize_submissions_params!(submissions_attrs, template, add_fields: true) + + Submissions.create_from_submitters(template: template, + user: current_user, + source: :invite, + submitters_order: params[:preserve_order] == '1' ? 'preserved' : 'random', + submissions_attrs:, + new_fields:, + params: params.merge('send_completed_email' => true)) + end + def save_template_message(template, params) template.preferences['request_email_subject'] = params[:subject] if params[:subject].present? template.preferences['request_email_body'] = params[:body] if params[:body].present? diff --git a/app/controllers/template_documents_controller.rb b/app/controllers/template_documents_controller.rb index 51fc4111..db8ba66c 100644 --- a/app/controllers/template_documents_controller.rb +++ b/app/controllers/template_documents_controller.rb @@ -16,7 +16,7 @@ class TemplateDocumentsController < ApplicationController old_fields_hash = @template.fields.hash - documents = Templates::CreateAttachments.call(@template, params, extract_fields: true) + documents, = Templates::CreateAttachments.call(@template, params, extract_fields: true) schema = documents.map do |doc| { attachment_uuid: doc.uuid, name: doc.filename.base } @@ -27,7 +27,7 @@ class TemplateDocumentsController < ApplicationController fields: old_fields_hash == @template.fields.hash ? nil : @template.fields, submitters: old_fields_hash == @template.fields.hash ? nil : @template.submitters, documents: documents.as_json( - methods: %i[metadata signed_uuid], + methods: %i[metadata signed_key], include: { preview_images: { methods: %i[url metadata filename] } } diff --git a/app/controllers/templates_controller.rb b/app/controllers/templates_controller.rb index 0d6db6f7..b9496320 100644 --- a/app/controllers/templates_controller.rb +++ b/app/controllers/templates_controller.rb @@ -35,7 +35,7 @@ class TemplatesController < ApplicationController @template_data = @template.as_json.merge( documents: @template.schema_documents.as_json( - methods: %i[metadata signed_uuid], + methods: %i[metadata signed_key], include: { preview_images: { methods: %i[url metadata filename] } } ) ).to_json @@ -95,10 +95,11 @@ class TemplatesController < ApplicationController def template_params params.require(:template).permit( :name, - { schema: [[:attachment_uuid, :google_drive_file_id, :name, + { schema: [[:attachment_uuid, :google_drive_file_id, :name, :dynamic, { conditions: [%i[field_uuid value action operation]] }]], submitters: [%i[name uuid is_requester linked_to_uuid invite_via_field_uuid invite_by_uuid optional_invite_by_uuid email order]], + variables_schema: {}, fields: [[:uuid, :submitter_uuid, :name, :type, :required, :readonly, :default_value, :title, :description, :prefillable, @@ -107,7 +108,7 @@ class TemplatesController < ApplicationController conditions: [%i[field_uuid value action operation]], options: [%i[value uuid]], validation: %i[message pattern min max step], - areas: [%i[x y w h cell_w attachment_uuid option_uuid page]] }]] } + areas: [%i[uuid x y w h cell_w attachment_uuid option_uuid page]] }]] } ) end end diff --git a/app/controllers/templates_debug_controller.rb b/app/controllers/templates_debug_controller.rb index 4ff31835..54fa23e1 100644 --- a/app/controllers/templates_debug_controller.rb +++ b/app/controllers/templates_debug_controller.rb @@ -9,20 +9,22 @@ class TemplatesDebugController < ApplicationController schema_uuids = @template.schema.index_by { |e| e['attachment_uuid'] } attachment = @template.documents.find { |a| schema_uuids[a.uuid] } - data = attachment.download + if attachment + data = attachment.download - unless attachment.image? - pdf = HexaPDF::Document.new(io: StringIO.new(data)) + unless attachment.image? + pdf = HexaPDF::Document.new(io: StringIO.new(data)) - fields = Templates::FindAcroFields.call(pdf, attachment, data) - end + fields = Templates::FindAcroFields.call(pdf, attachment, data) + end - fields, = Templates::DetectFields.call(StringIO.new(data), attachment:) if fields.blank? + # fields, = Templates::DetectFields.call(StringIO.new(data), attachment:) if fields.blank? - attachment.metadata['pdf'] ||= {} - attachment.metadata['pdf']['fields'] = fields + attachment.metadata['pdf'] ||= {} + attachment.metadata['pdf']['fields'] = fields - @template.update!(fields: Templates::ProcessDocument.normalize_attachment_fields(@template, [attachment])) + @template.update!(fields: Templates::ProcessDocument.normalize_attachment_fields(@template, [attachment])) + end debug_file if DEBUG_FILE.present? @@ -34,7 +36,7 @@ class TemplatesDebugController < ApplicationController @template_data = @template.as_json.merge( documents: @template.schema_documents.as_json( - methods: %i[metadata signed_uuid], + methods: %i[metadata signed_key], include: { preview_images: { methods: %i[url metadata filename] } } ) ).to_json @@ -58,9 +60,16 @@ class TemplatesDebugController < ApplicationController params = { files: [file] } - documents = Templates::CreateAttachments.call(@template, params) + documents, dynamic_documents = Templates::CreateAttachments.call(@template, params, + dynamic: DEBUG_FILE.ends_with?('.docx')) - schema = documents.map { |doc| { attachment_uuid: doc.uuid, name: doc.filename.base } } + schema = documents.map do |doc| + { + attachment_uuid: doc.uuid, + name: doc.filename.base, + dynamic: dynamic_documents.find { |e| e.uuid == doc.uuid }.present? + } + end @template.update!(schema:) end diff --git a/app/controllers/templates_preview_controller.rb b/app/controllers/templates_preview_controller.rb index a3f4ec37..b9325bfa 100644 --- a/app/controllers/templates_preview_controller.rb +++ b/app/controllers/templates_preview_controller.rb @@ -12,7 +12,7 @@ class TemplatesPreviewController < ApplicationController @template_data = @template.as_json.merge( documents: @template.schema_documents.as_json( - methods: %i[metadata signed_uuid], + methods: %i[metadata signed_key], include: { preview_images: { methods: %i[url metadata filename] } } ) ).to_json diff --git a/app/controllers/templates_uploads_controller.rb b/app/controllers/templates_uploads_controller.rb index ddee4e90..7d8b1bae 100644 --- a/app/controllers/templates_uploads_controller.rb +++ b/app/controllers/templates_uploads_controller.rb @@ -12,7 +12,7 @@ class TemplatesUploadsController < ApplicationController save_template!(@template, url_params) - documents = Templates::CreateAttachments.call(@template, url_params || params, extract_fields: true) + documents, = Templates::CreateAttachments.call(@template, url_params || params, extract_fields: true) schema = documents.map { |doc| { attachment_uuid: doc.uuid, name: doc.filename.base } } if @template.fields.blank? diff --git a/app/javascript/application.js b/app/javascript/application.js index fe53cc5b..920cba82 100644 --- a/app/javascript/application.js +++ b/app/javascript/application.js @@ -160,6 +160,7 @@ safeRegisterElement('template-builder', class extends HTMLElement { this.app = createApp(TemplateBuilder, { template, customFields: reactive(JSON.parse(this.dataset.customFields || '[]')), + dynamicDocuments: reactive(JSON.parse(this.dataset.dynamicDocuments || '[]')), backgroundColor: '#faf7f5', locale: this.dataset.locale, withPhone: this.dataset.withPhone === 'true', @@ -177,6 +178,7 @@ safeRegisterElement('template-builder', class extends HTMLElement { withSendButton: this.dataset.withSendButton !== 'false', withSignYourselfButton: this.dataset.withSignYourselfButton !== 'false', withConditions: this.dataset.withConditions === 'true', + withDynamicDocuments: this.dataset.withDynamicDocuments === 'true', withGoogleDrive: this.dataset.withGoogleDrive === 'true', withReplaceAndCloneUpload: true, withDownload: true, diff --git a/app/javascript/template_builder/area.vue b/app/javascript/template_builder/area.vue index e071e9ee..bbf1a357 100644 --- a/app/javascript/template_builder/area.vue +++ b/app/javascript/template_builder/area.vue @@ -31,144 +31,26 @@ /> -
- - - {{ optionIndexText }} {{ (defaultField ? (defaultField.title || field.title || field.name) : field.name) || defaultName }} -
- - - - - - - - -
- -
+
- - - - - - - - - - - -
diff --git a/app/javascript/template_builder/builder.vue b/app/javascript/template_builder/builder.vue index 969304e7..bdc35528 100644 --- a/app/javascript/template_builder/builder.vue +++ b/app/javascript/template_builder/builder.vue @@ -81,7 +81,7 @@ />