Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 31 additions & 3 deletions app/javascript/template_builder/builder.vue
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,14 @@
name="buttons"
/>
<template v-else>
<button
v-if="hasMultipleSubmitterFields"
class="base-button"
@click.prevent="isShowSigningOrderModal = true"
>
<IconAdjustments class="w-6 h-6 flex-shrink-0" />
<span class="whitespace-nowrap">{{ t('signing_order') }}</span>
</button>
<a
:href="formPreviewUrl"
data-turbo="false"
Expand Down Expand Up @@ -331,6 +339,14 @@
id="docuseal_modal_container"
class="modal-container"
/>
<Teleport
v-if="isShowSigningOrderModal"
to="#docuseal_modal_container"
>
<SigningOrderModal
@close="isShowSigningOrderModal = false"
/>
</Teleport>
</div>
</template>

Expand All @@ -348,6 +364,7 @@ import DocumentPreview from './preview'
import DocumentControls from './controls'
import MobileFields from './mobile_fields'
import FieldSubmitter from './field_submitter'
import SigningOrderModal from './signing_order_modal'
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'
Expand Down Expand Up @@ -376,7 +393,8 @@ export default {
IconChevronDown,
IconAdjustments,
IconEye,
IconDeviceFloppy
IconDeviceFloppy,
SigningOrderModal
},
provide () {
return {
Expand All @@ -387,6 +405,7 @@ export default {
currencies: this.currencies,
locale: this.locale,
baseFetch: this.baseFetch,
authenticityToken: this.authenticityToken,
fieldTypes: this.fieldTypes,
backgroundColor: this.backgroundColor,
withPhone: this.withPhone,
Expand Down Expand Up @@ -636,13 +655,18 @@ export default {
drawFieldType: null,
drawOption: null,
dragField: null,
isDragFile: false
isDragFile: false,
isShowSigningOrderModal: false
}
},
computed: {
submitterDefaultNames: FieldSubmitter.computed.names,
selectedAreaRef: () => ref(),
fieldsDragFieldRef: () => ref(),
hasMultipleSubmitterFields () {
const submitterUuids = new Set(this.template.fields.map((f) => f.submitter_uuid).filter(Boolean))
return submitterUuids.size >= 2
},
language () {
return this.locale.split('-')[0].toLowerCase()
},
Expand Down Expand Up @@ -1823,7 +1847,11 @@ export default {
}
}),
headers: { 'Content-Type': 'application/json' }
}).then(() => {
}).then((response) => response.json()).then((data) => {
if (data.preferences) {
this.template.preferences = data.preferences
}

if (this.onSave) {
this.onSave(this.template)
}
Expand Down
3 changes: 3 additions & 0 deletions app/javascript/template_builder/i18n.js
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,9 @@ const en = {
condition: 'Condition',
first_party: 'Employee',
second_party: 'Manager',
signing_order: 'Signing Order',
select_signing_order: 'Select Signing Order',
simultaneous_signing_description: 'Both parties may complete the form at the same time',
draw: 'Draw',
add: 'Add',
or_add_field_without_drawing: 'Or add field without drawing',
Expand Down
123 changes: 123 additions & 0 deletions app/javascript/template_builder/signing_order_modal.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
<template>
<div
class="modal modal-open items-start !animate-none overflow-y-auto"
>
<div
class="absolute top-0 bottom-0 right-0 left-0"
@click.prevent="$emit('close')"
/>
<div class="modal-box pt-4 pb-6 px-6 mt-20 max-h-none w-full max-w-xl">
<div class="flex justify-between items-center border-b pb-2 mb-2 font-medium">
<span class="modal-title text-lg">
{{ t('select_signing_order') }}
</span>
<a
href="#"
class="text-xl modal-close-button"
@click.prevent="$emit('close')"
>&times;</a>
</div>
<div>
<form @submit.prevent="saveAndClose">
<div class="space-y-2 mb-4">
<label class="flex items-start space-x-3 p-3 border border-base-300 rounded-lg cursor-pointer hover:border-primary transition-colors">
<input
v-model="signingOrder"
type="radio"
value="employee_then_manager"
class="radio radio-primary mt-1"
>
<div class="flex-1">
<div>{{ firstParty }} completes the form first, then {{ secondParty }}</div>
</div>
</label>

<label class="flex items-start space-x-3 p-3 border border-base-300 rounded-lg cursor-pointer hover:border-primary transition-colors">
<input
v-model="signingOrder"
type="radio"
value="manager_then_employee"
class="radio radio-primary mt-1"
>
<div class="flex-1">
<div>{{ secondParty }} completes the form first, then {{ firstParty }}</div>
</div>
</label>

<label class="flex items-start space-x-3 p-3 border border-base-300 rounded-lg cursor-pointer hover:border-primary transition-colors">
<input
v-model="signingOrder"
type="radio"
value="simultaneous"
class="radio radio-primary mt-1"
>
<div class="flex-1">
<div>{{ t('simultaneous_signing_description') }}</div>
</div>
</label>
</div>
<button class="base-button w-full mt-4 modal-save-button">
{{ t('save') }}
</button>
</form>
</div>
</div>
</div>
</template>

<script>
export default {
name: 'SigningOrderModal',
inject: ['t', 'template', 'baseFetch', 'authenticityToken'],
emits: ['close'],
data () {
return {
signingOrder: this.template.preferences?.submitters_order || 'employee_then_manager'
}
},
computed: {
firstParty () {
return this.template.submitters[0]?.name || this.t('first_party')
},
secondParty () {
return this.template.submitters[1]?.name || this.t('second_party')
}
},
methods: {
saveAndClose () {
if (!this.template.preferences) {
this.template.preferences = {}
}
this.template.preferences.submitters_order = this.signingOrder

const formData = new FormData()
formData.append('template[preferences][submitters_order]', this.signingOrder)

if (this.template?.partnership_context) {
const context = this.template.partnership_context
if (context.accessible_partnership_ids) {
context.accessible_partnership_ids.forEach(id => {
formData.append('accessible_partnership_ids[]', id)
})
}
if (context.external_partnership_id) {
formData.append('external_partnership_id', context.external_partnership_id)
}
if (context.external_account_id) {
formData.append('external_account_id', context.external_account_id)
}
}

this.baseFetch(`/templates/${this.template.id}/preferences`, {
method: 'POST',
body: formData
}).then(() => {
this.$emit('close')
}).catch((error) => {
console.error('Error saving signing order:', error)
alert(this.t('error_occurred'))
})
}
}
}
</script>
2 changes: 1 addition & 1 deletion app/jobs/process_submitter_completion_job.rb
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ def perform(params = {})

create_completed_documents!(submitter)

if !is_all_completed && submitter.submission.submitters_order_preserved? && params['send_invitation_email'] != false
if !is_all_completed && submitter.submission.signing_order_enforced? && params['send_invitation_email'] != false
enqueue_next_submitter_request_notification(submitter)
end

Expand Down
34 changes: 34 additions & 0 deletions app/jobs/send_template_preferences_updated_webhook_request_job.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# frozen_string_literal: true

class SendTemplatePreferencesUpdatedWebhookRequestJob
include Sidekiq::Job

sidekiq_options queue: :webhooks

def perform(params = {})
template = Template.find(params['template_id'])
webhook_url = WebhookUrl.find(params['webhook_url_id'])

attempt = params['attempt'].to_i

return if webhook_url.url.blank? || webhook_url.events.exclude?('template.preferences_updated')

data = {
id: template.id,
external_id: template.external_id,
application_key: template.application_key,
submitters_order: template.preferences['submitters_order']
}

resp = SendWebhookRequest.call(webhook_url, event_type: 'template.preferences_updated', data:)

return unless WebhookRetryLogic.should_retry?(response: resp, attempt: attempt, record: template)

SendTemplatePreferencesUpdatedWebhookRequestJob.perform_in((2**attempt).minutes, {
'template_id' => template.id,
'webhook_url_id' => webhook_url.id,
'attempt' => attempt + 1,
'last_status' => resp&.status.to_i
})
end
end
4 changes: 2 additions & 2 deletions app/models/account.rb
Original file line number Diff line number Diff line change
Expand Up @@ -78,11 +78,11 @@ def default_template_folder
private

def create_careerplug_webhook
return if ENV['CAREERPLUG_WEBHOOK_SECRET'].blank?
return if ENV['CAREERPLUG_WEBHOOK_SECRET'].blank? || ENV['CAREERPLUG_WEBHOOK_URL'].blank?

webhook_urls.create!(
url: ENV.fetch('CAREERPLUG_WEBHOOK_URL'),
events: %w[form.viewed form.started form.completed form.declined],
events: %w[form.viewed form.started form.completed form.declined template.preferences_updated],
secret: { 'X-CareerPlug-Secret' => ENV.fetch('CAREERPLUG_WEBHOOK_SECRET') }
)
end
Expand Down
12 changes: 12 additions & 0 deletions app/models/partnership.rb
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ class Partnership < ApplicationRecord
validates :external_partnership_id, presence: true, uniqueness: true
validates :name, presence: true

after_commit :create_careerplug_webhook, on: :create

def self.find_or_create_by_external_id(external_id, name, attributes = {})
find_by(external_partnership_id: external_id) ||
create!(attributes.merge(external_partnership_id: external_id, name: name))
Expand All @@ -34,4 +36,14 @@ def default_template_folder(author)
template_folders.create!(name: TemplateFolder::DEFAULT_NAME,
author: author)
end

def create_careerplug_webhook
return if ENV['CAREERPLUG_WEBHOOK_SECRET'].blank? || ENV['CAREERPLUG_WEBHOOK_URL'].blank?

webhook_urls.create!(
url: ENV.fetch('CAREERPLUG_WEBHOOK_URL'),
events: %w[template.preferences_updated],
secret: { 'X-CareerPlug-Secret' => ENV.fetch('CAREERPLUG_WEBHOOK_SECRET') }
)
end
end
4 changes: 2 additions & 2 deletions app/models/webhook_url.rb
Original file line number Diff line number Diff line change
Expand Up @@ -38,12 +38,12 @@ class WebhookUrl < ApplicationRecord
submission.archived
template.created
template.updated
template.preferences_updated
].freeze

# Partnership webhooks can only use template events since partnerships don't have submissions/submitters
PARTNERSHIP_EVENTS = %w[
template.created
template.updated
template.preferences_updated
].freeze

belongs_to :account, optional: true
Expand Down
58 changes: 50 additions & 8 deletions app/views/templates_preferences/show.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -337,14 +337,56 @@
<% end %>
<% unless current_account.account_configs.exists?(key: AccountConfig::ENFORCE_SIGNING_ORDER_KEY, value: true) %>
<%= form_for @template, url: template_preferences_path(@template), method: :post, html: { autocomplete: 'off', class: 'mt-2' }, data: { close_on_submit: false } do |f| %>
<div class="flex items-center pt-4 mt-4 justify-between border-t w-full">
<span>
<%= t('enforce_recipients_order') %>
</span>
<%= f.fields_for :preferences, Struct.new(:submitters_order).new(@template.preferences['submitters_order']) do |ff| %>
<%= ff.check_box :submitters_order, { class: 'toggle', onchange: 'this.form.requestSubmit()' }, 'preserved', '' %>
<% end %>
</div>
<%= f.fields_for :preferences, Struct.new(:submitters_order).new(@template.preferences['submitters_order']) do |ff| %>
<div class="pt-4 mt-4 border-t w-full">
<label class="label">
<span class="label-text font-semibold"><%= t('select_signing_order') %></span>
<span class="tooltip" data-tip="<%= t('choose_the_order_in_which_parties_will_receive_and_complete_the_form') %>">
<%= svg_icon('info_circle', class: 'w-4 h-4') %>
</span>
</label>
<div class="space-y-2">
<%
first_party = @template.submitters.first['name'] || t('first_party')
second_party = @template.submitters.second&.[]('name') || t('second_party')
current_value = ff.object.submitters_order.presence || 'simultaneous'
%>

<label class="flex items-start space-x-3 p-3 border border-base-300 rounded-lg cursor-pointer hover:border-primary transition-colors">
<%= ff.radio_button :submitters_order, 'employee_then_manager',
checked: current_value == 'employee_then_manager',
class: 'radio radio-primary mt-0.5',
onchange: 'this.form.requestSubmit()' %>
<div class="flex-1">
<div class="font-medium"><%= t('employee_then_manager_title', first_party: first_party, second_party: second_party) %></div>
<div class="text-sm text-base-content/70"><%= t('employee_then_manager_description', first_party: first_party, second_party: second_party) %></div>
</div>
</label>

<label class="flex items-start space-x-3 p-3 border border-base-300 rounded-lg cursor-pointer hover:border-primary transition-colors">
<%= ff.radio_button :submitters_order, 'manager_then_employee',
checked: current_value == 'manager_then_employee',
class: 'radio radio-primary mt-0.5',
onchange: 'this.form.requestSubmit()' %>
<div class="flex-1">
<div class="font-medium"><%= t('manager_then_employee_title', first_party: first_party, second_party: second_party) %></div>
<div class="text-sm text-base-content/70"><%= t('manager_then_employee_description', first_party: first_party, second_party: second_party) %></div>
</div>
</label>

<label class="flex items-start space-x-3 p-3 border border-base-300 rounded-lg cursor-pointer hover:border-primary transition-colors">
<%= ff.radio_button :submitters_order, 'simultaneous',
checked: current_value == 'simultaneous',
class: 'radio radio-primary mt-0.5',
onchange: 'this.form.requestSubmit()' %>
<div class="flex-1">
<div class="font-medium"><%= t('simultaneous_signing_title') %></div>
<div class="text-sm text-base-content/70"><%= t('simultaneous_signing_description') %></div>
</div>
</label>
</div>
</div>
<% end %>
<% end %>
<% end %>
<% if can?(:manage, :personalization_advanced) %>
Expand Down
1 change: 0 additions & 1 deletion lib/params/submission_create_validator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
module Params
class SubmissionCreateValidator < BaseValidator
def call
binding.pry
if params[:submission].blank? && (params[:emails].present? || params[:email].present?)
validate_creation_from_emails(params)
elsif params.key?(:submitters)
Expand Down
Loading
Loading