diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml
index 2eb09ddc..82d4202f 100644
--- a/.github/workflows/docker.yml
+++ b/.github/workflows/docker.yml
@@ -2,26 +2,34 @@ name: Build Docker Images
on:
push:
+ branches:
+ - main
+ - master
tags:
- "*.*.*"
+ workflow_dispatch:
jobs:
build:
- runs-on: ubuntu-24.04-arm
+ runs-on: ubuntu-24.04
timeout-minutes: 30
steps:
- name: Checkout code
- uses: actions/checkout@v3
+ uses: actions/checkout@v4
with:
submodules: recursive
- name: Docker meta
id: meta
- uses: docker/metadata-action@v4
+ uses: docker/metadata-action@v5
with:
- images: docuseal/docuseal
- tags: type=semver,pattern={{version}}
+ images: ${{ secrets.DOCKERHUB_USERNAME }}/docuseal-wl
+ tags: |
+ type=semver,pattern={{version}}
+ type=raw,value=latest,enable=${{ github.ref == format('refs/heads/{0}', github.event.repository.default_branch) }}
+ type=raw,value=latest,enable=${{ github.event_name == 'workflow_dispatch' }}
+ type=sha,prefix=sha-
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
diff --git a/app/controllers/embed_scripts_controller.rb b/app/controllers/embed_scripts_controller.rb
index a40ed79c..c909040d 100644
--- a/app/controllers/embed_scripts_controller.rb
+++ b/app/controllers/embed_scripts_controller.rb
@@ -1,38 +1,53 @@
# frozen_string_literal: true
class EmbedScriptsController < ActionController::Metal
- DUMMY_SCRIPT = <<~JAVASCRIPT.freeze
- const DummyBuilder = class extends HTMLElement {
+ EMBED_SCRIPT = <<~JAVASCRIPT.freeze
+ const buildIframe = (element) => {
+ const iframe = document.createElement('iframe');
+ const src = element.dataset.src || element.getAttribute('src');
+
+ if (!src) return;
+
+ const url = new URL(src, window.location.href);
+
+ ['email', 'name', 'phone', 'role', 'token', 'preview', 'externalId', 'completedRedirectUrl'].forEach((key) => {
+ const value = element.dataset[key];
+
+ if (value) url.searchParams.set(key.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`), value);
+ });
+
+ iframe.src = url.toString();
+ iframe.style.width = element.dataset.width || '100%';
+ iframe.style.height = element.dataset.height || '700px';
+ iframe.style.border = element.dataset.border || '0';
+ iframe.allow = element.dataset.allow || 'clipboard-write; fullscreen';
+ iframe.title = element.dataset.title || 'DocuSeal';
+
+ element.innerHTML = '';
+ element.appendChild(iframe);
+ };
+
+ const EmbeddedBuilder = class extends HTMLElement {
connectedCallback() {
- this.innerHTML = `
-
-
Upgrade to Pro
-
Unlock embedded components by upgrading to Pro
-
-
- `;
+ buildIframe(this);
}
};
- const DummyForm = class extends DummyBuilder {};
+ const EmbeddedForm = class extends EmbeddedBuilder {};
if (!window.customElements.get('docuseal-builder')) {
- window.customElements.define('docuseal-builder', DummyBuilder);
+ window.customElements.define('docuseal-builder', EmbeddedBuilder);
}
if (!window.customElements.get('docuseal-form')) {
- window.customElements.define('docuseal-form', DummyForm);
+ window.customElements.define('docuseal-form', EmbeddedForm);
}
JAVASCRIPT
def show
headers['Content-Type'] = 'application/javascript'
- self.response_body = DUMMY_SCRIPT
+ self.response_body = EMBED_SCRIPT
self.status = 200
end
diff --git a/app/controllers/personalization_settings_controller.rb b/app/controllers/personalization_settings_controller.rb
index d9d33490..47fc0204 100644
--- a/app/controllers/personalization_settings_controller.rb
+++ b/app/controllers/personalization_settings_controller.rb
@@ -13,13 +13,23 @@ class PersonalizationSettingsController < ApplicationController
InvalidKey = Class.new(StandardError)
- before_action :load_and_authorize_account_config, only: :create
+ before_action :load_and_authorize_account_config, only: :create, if: -> { params[:account_config].present? }
def show
authorize!(:read, AccountConfig)
end
def create
+ if params[:account].present?
+ authorize!(:update, current_account)
+
+ current_account.logo.purge if account_params[:remove_logo] == '1'
+ current_account.logo.attach(account_params[:logo]) if account_params[:logo].present?
+
+ return redirect_back(fallback_location: settings_personalization_path,
+ notice: I18n.t('settings_have_been_saved'))
+ end
+
if @account_config.value.is_a?(Hash)
@account_config.value = @account_config.value.reject do |_, v|
v.blank? && v != false
@@ -65,4 +75,8 @@ class PersonalizationSettingsController < ApplicationController
attrs
end
+
+ def account_params
+ params.require(:account).permit(:logo, :remove_logo)
+ end
end
diff --git a/app/javascript/submission_form/completed.vue b/app/javascript/submission_form/completed.vue
index 6c07a09c..ed8a6bd7 100644
--- a/app/javascript/submission_form/completed.vue
+++ b/app/javascript/submission_form/completed.vue
@@ -96,17 +96,6 @@
-
- {{ t('powered_by') }}
-
DocuSeal - {{ t('open_source_documents_software') }}
-
diff --git a/app/jobs/send_submitter_reminder_email_job.rb b/app/jobs/send_submitter_reminder_email_job.rb
new file mode 100644
index 00000000..8ce48f41
--- /dev/null
+++ b/app/jobs/send_submitter_reminder_email_job.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+class SendSubmitterReminderEmailJob
+ include Sidekiq::Job
+
+ def perform(params = {})
+ submitter = Submitter.find(params['submitter_id'])
+
+ return if submitter.completed_at?
+ return if submitter.declined_at?
+ return if submitter.submission.archived_at?
+ return if submitter.template&.archived_at?
+ return unless submitter.sent_at?
+ return unless Accounts.can_send_invitation_emails?(submitter.account)
+
+ reminder_index = params['reminder_index'].to_i
+
+ return if reminder_index.positive? &&
+ submitter.submission_events.exists?(event_type: 'send_reminder_email',
+ data: { 'reminder_index' => reminder_index })
+
+ mail = SubmitterMailer.invitation_email(submitter)
+
+ Submitters::ValidateSending.call(submitter, mail)
+
+ mail.deliver_now!
+
+ SubmissionEvent.create!(submitter: submitter,
+ event_type: 'send_reminder_email',
+ data: { reminder_index: reminder_index })
+ end
+end
diff --git a/app/models/account.rb b/app/models/account.rb
index bc6471bf..d15d1c3a 100644
--- a/app/models/account.rb
+++ b/app/models/account.rb
@@ -20,6 +20,8 @@
class Account < ApplicationRecord
attribute :uuid, :string, default: -> { SecureRandom.uuid }
+ has_one_attached :logo
+
has_many :users, dependent: :destroy
has_many :encrypted_configs, dependent: :destroy
has_many :account_configs, dependent: :destroy
diff --git a/app/models/user.rb b/app/models/user.rb
index b80ae769..8b706ac0 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -48,7 +48,9 @@
#
class User < ApplicationRecord
ROLES = [
- ADMIN_ROLE = 'admin'
+ ADMIN_ROLE = 'admin',
+ EDITOR_ROLE = 'editor',
+ VIEWER_ROLE = 'viewer'
].freeze
EMAIL_REGEXP = /[^@;,<>\s]+@[^@;,<>\s]+/
@@ -78,6 +80,18 @@ class User < ApplicationRecord
scope :archived, -> { where.not(archived_at: nil) }
scope :admins, -> { where(role: ADMIN_ROLE) }
+ def admin?
+ role == ADMIN_ROLE
+ end
+
+ def editor?
+ role == EDITOR_ROLE
+ end
+
+ def viewer?
+ role == VIEWER_ROLE
+ end
+
validates :email, format: { with: /\A[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\z/ }
def access_token
diff --git a/app/views/notifications_settings/_reminder_banner.html.erb b/app/views/notifications_settings/_reminder_banner.html.erb
index 926e952d..e69de29b 100644
--- a/app/views/notifications_settings/_reminder_banner.html.erb
+++ b/app/views/notifications_settings/_reminder_banner.html.erb
@@ -1 +0,0 @@
-<%= render 'reminder_placeholder' %>
diff --git a/app/views/personalization_settings/_logo_form.html.erb b/app/views/personalization_settings/_logo_form.html.erb
index fc6f3ac7..df80811b 100644
--- a/app/views/personalization_settings/_logo_form.html.erb
+++ b/app/views/personalization_settings/_logo_form.html.erb
@@ -1 +1,18 @@
-<%= render 'logo_placeholder' %>
+<%= form_for current_account, url: settings_personalization_path, method: :post, html: { multipart: true, autocomplete: 'off', class: 'space-y-4' } do |f| %>
+ <% if current_account.logo.attached? %>
+
+ <%= image_tag current_account.logo, class: 'max-h-16 max-w-48 rounded bg-base-200 object-contain p-2' %>
+
+
+ <% end %>
+
+ <%= f.label :logo, t('company_logo'), class: 'label' %>
+ <%= f.file_field :logo, accept: 'image/png,image/jpeg,image/webp,image/svg+xml', class: 'file-input file-input-bordered w-full' %>
+
+
+ <%= f.button button_title(title: t('save'), disabled_with: t('saving')), class: 'base-button' %>
+
+<% end %>
diff --git a/app/views/shared/_attribution.html.erb b/app/views/shared/_attribution.html.erb
index bc464bba..e69de29b 100644
--- a/app/views/shared/_attribution.html.erb
+++ b/app/views/shared/_attribution.html.erb
@@ -1 +0,0 @@
-<%= render 'shared/powered_by', with_counter: local_assigns[:with_counter], link_path: local_assigns[:link_path] %>
diff --git a/app/views/shared/_mailer_attribution.html.erb b/app/views/shared/_mailer_attribution.html.erb
index b9fa9276..e69de29b 100644
--- a/app/views/shared/_mailer_attribution.html.erb
+++ b/app/views/shared/_mailer_attribution.html.erb
@@ -1 +0,0 @@
-<%= render 'shared/email_attribution' %>
diff --git a/app/views/start_form/_docuseal_logo.html.erb b/app/views/start_form/_docuseal_logo.html.erb
index 735c607c..2b238d99 100644
--- a/app/views/start_form/_docuseal_logo.html.erb
+++ b/app/views/start_form/_docuseal_logo.html.erb
@@ -1,6 +1,11 @@
+<% account = @template&.account || @submitter&.account || current_account %>
-
- <%= render 'shared/logo', width: '50px', height: '50px' %>
-
- DocuSeal
+ <% if account&.logo&.attached? %>
+ <%= image_tag account.logo, class: 'mr-3 max-w-14 max-h-14 object-contain' %>
+ <% else %>
+
+ <%= render 'shared/logo', width: '50px', height: '50px' %>
+
+ <% end %>
+ <%= account&.name || Docuseal.product_name %>
diff --git a/app/views/submit_form/_docuseal_logo.html.erb b/app/views/submit_form/_docuseal_logo.html.erb
index 645f7fc5..9a729da1 100644
--- a/app/views/submit_form/_docuseal_logo.html.erb
+++ b/app/views/submit_form/_docuseal_logo.html.erb
@@ -1,4 +1,9 @@
+<% account = @submitter&.account || current_account %>
- <%= render 'shared/logo', class: 'w-9 h-9 md:w-12 md:h-12' %>
- <%= Docuseal.product_name %>
+ <% if account&.logo&.attached? %>
+ <%= image_tag account.logo, class: 'max-w-12 max-h-12 object-contain' %>
+ <% else %>
+ <%= render 'shared/logo', class: 'w-9 h-9 md:w-12 md:h-12' %>
+ <% end %>
+ <%= account&.name || Docuseal.product_name %>
diff --git a/app/views/templates/edit.html.erb b/app/views/templates/edit.html.erb
index 18684bb5..539f9ee2 100644
--- a/app/views/templates/edit.html.erb
+++ b/app/views/templates/edit.html.erb
@@ -6,4 +6,4 @@
<%= button_to nil, user_configs_path, method: :post, params: { user_config: { key: UserConfig::SHOW_APP_TOUR, value: true } }, class: 'hidden', id: 'start_tour_button' %>
<% end %>
<% end %>
-
+
diff --git a/app/views/templates_code_modal/show.html.erb b/app/views/templates_code_modal/show.html.erb
index 46e9c893..0b955db2 100644
--- a/app/views/templates_code_modal/show.html.erb
+++ b/app/views/templates_code_modal/show.html.erb
@@ -34,5 +34,4 @@
<% end %>
<%= render 'templates_code_modal/preferences' %>
- <%= render 'templates_code_modal/placeholder' %>
<% end %>
diff --git a/app/views/templates_preferences/show.html.erb b/app/views/templates_preferences/show.html.erb
index ddb52bd8..d2184a66 100644
--- a/app/views/templates_preferences/show.html.erb
+++ b/app/views/templates_preferences/show.html.erb
@@ -168,7 +168,6 @@
<% end %>
- <%= render 'templates_code_modal/placeholder' %>
<%= render 'templates/embedding', template: @template %>
<% if can?(:manage, TemplateSharing.new(template: @template)) %>
<%= form_for '', url: template_sharings_testing_index_path, method: :post, html: { class: 'mt-1' }, data: { close_on_submit: false } do |f| %>
diff --git a/app/views/users/_role_select.html.erb b/app/views/users/_role_select.html.erb
index d14b2778..913135a4 100644
--- a/app/views/users/_role_select.html.erb
+++ b/app/views/users/_role_select.html.erb
@@ -1,20 +1,4 @@
diff --git a/lib/ability.rb b/lib/ability.rb
index da472f58..d66972b6 100644
--- a/lib/ability.rb
+++ b/lib/ability.rb
@@ -4,25 +4,43 @@ class Ability
include CanCan::Ability
def initialize(user)
- can %i[read create update], Template, Abilities::TemplateConditions.collection(user) do |template|
- Abilities::TemplateConditions.entity(template, user:, ability: 'manage')
- end
+ template_scope = Abilities::TemplateConditions.collection(user)
+ template_check = ->(template) { Abilities::TemplateConditions.entity(template, user: user, ability: 'manage') }
+ can :read, Template, template_scope, &template_check
+ can :read, TemplateFolder, account_id: user.account_id
+ can :read, Submission, account_id: user.account_id
+ can :read, Submitter, account_id: user.account_id
+ can :manage, UserConfig, user_id: user.id
+ can :manage, EncryptedUserConfig, user_id: user.id
+ can :read, Account, id: user.account_id
+
+ return if user.viewer?
+
+ can %i[create update], Template, template_scope, &template_check
can :destroy, Template, account_id: user.account_id
can :manage, TemplateFolder, account_id: user.account_id
can :manage, TemplateSharing, template: { account_id: user.account_id }
can :manage, Submission, account_id: user.account_id
can :manage, Submitter, account_id: user.account_id
+ can :manage, AccessToken, user_id: user.id
+
+ return unless user.admin?
+
can :manage, User, account_id: user.account_id
can :manage, EncryptedConfig, account_id: user.account_id
- can :manage, EncryptedUserConfig, user_id: user.id
can :manage, AccountConfig, account_id: user.account_id
- can :manage, UserConfig, user_id: user.id
can :manage, Account, id: user.account_id
- can :manage, AccessToken, user_id: user.id
can :manage, McpToken, user_id: user.id
can :manage, WebhookUrl, account_id: user.account_id
can :manage, :mcp
+ can :manage, :personalization_advanced
+ can :manage, :reply_to
+ can :manage, :download_users
+ can :manage, :bulk_send
+ can :manage, :disable_decline
+ can :manage, :delegate_form
+ can :manage, :countless
end
end
diff --git a/lib/account_configs.rb b/lib/account_configs.rb
index 06fec78f..832aae8c 100644
--- a/lib/account_configs.rb
+++ b/lib/account_configs.rb
@@ -34,4 +34,10 @@ module AccountConfigs
configs
end
+
+ def duration_for(key)
+ amount, unit = REMINDER_DURATIONS.fetch(key).split
+
+ amount.to_i.public_send(unit)
+ end
end
diff --git a/lib/submissions/generate_audit_trail.rb b/lib/submissions/generate_audit_trail.rb
index 82d79dd2..4f48c409 100644
--- a/lib/submissions/generate_audit_trail.rb
+++ b/lib/submissions/generate_audit_trail.rb
@@ -529,11 +529,21 @@ module Submissions
!submission.source.in?(%w[embed api])
end
- def add_logo(column, _submission = nil)
+ def add_logo(column, submission = nil)
+ account = submission&.account
+ logo_io =
+ if account&.logo&.attached?
+ StringIO.new(account.logo.download)
+ else
+ PdfIcons.logo_io
+ end
+
+ column.image(logo_io, width: 40, height: 40, position: :float)
+ rescue StandardError
column.image(PdfIcons.logo_io, width: 40, height: 40, position: :float)
+ ensure
- column.formatted_text([{ text: 'DocuSeal',
- link: Docuseal::PRODUCT_EMAIL_URL }],
+ column.formatted_text([{ text: account&.name || Docuseal.product_name }],
font_size: 20,
font: [FONT_NAME, { variant: :bold }],
width: 100,
diff --git a/lib/submitters.rb b/lib/submitters.rb
index ec8330b1..11ace9cb 100644
--- a/lib/submitters.rb
+++ b/lib/submitters.rb
@@ -180,6 +180,23 @@ module Submitters
else
SendSubmitterInvitationEmailJob.perform_async('submitter_id' => submitter.id)
end
+
+ schedule_reminder_emails(submitter, delay_seconds: delay_seconds.to_i + index)
+ end
+ end
+
+ def schedule_reminder_emails(submitter, delay_seconds: 0)
+ config = AccountConfigs.find_for_account(submitter.account, AccountConfig::SUBMITTER_REMINDERS)
+ durations = config&.value.to_h.values_at('first_duration', 'second_duration', 'third_duration').compact_blank
+
+ durations.each_with_index do |duration_key, index|
+ next unless AccountConfigs::REMINDER_DURATIONS.key?(duration_key)
+
+ SendSubmitterReminderEmailJob.perform_in(
+ delay_seconds.seconds + AccountConfigs.duration_for(duration_key),
+ 'submitter_id' => submitter.id,
+ 'reminder_index' => index + 1
+ )
end
end