Merge from docusealco/wip
This commit is contained in:
+2
-2
@@ -51,10 +51,10 @@ Metrics/AbcSize:
|
||||
- spec/**/*
|
||||
|
||||
Metrics/ModuleLength:
|
||||
Max: 500
|
||||
Max: 1000
|
||||
|
||||
Metrics/ClassLength:
|
||||
Max: 500
|
||||
Max: 1000
|
||||
|
||||
RSpec/NestedGroups:
|
||||
Max: 6
|
||||
|
||||
@@ -10,7 +10,8 @@ class WebhookSecretController < ApplicationController
|
||||
webhook_secret_params[:key] => webhook_secret_params[:value]
|
||||
}.compact_blank)
|
||||
|
||||
redirect_back(fallback_location: settings_webhooks_path, notice: I18n.t('webhook_secret_has_been_saved'))
|
||||
redirect_back(fallback_location: settings_webhook_path(@webhook_url),
|
||||
notice: I18n.t('webhook_secret_has_been_saved'))
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
@@ -1,22 +1,47 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class WebhookSettingsController < ApplicationController
|
||||
before_action :load_webhook_url
|
||||
authorize_resource :webhook_url, parent: false
|
||||
load_and_authorize_resource :webhook_url, parent: false, only: %i[index show new create update destroy]
|
||||
load_and_authorize_resource :webhook_url, only: %i[resend], id_param: :webhook_id
|
||||
|
||||
def index
|
||||
@webhook_urls = @webhook_urls.order(id: :desc)
|
||||
@webhook_url = @webhook_urls.first_or_initialize
|
||||
|
||||
render @webhook_urls.size > 1 ? 'index' : 'show'
|
||||
end
|
||||
|
||||
def show; end
|
||||
|
||||
def new; end
|
||||
|
||||
def create
|
||||
@webhook_url.assign_attributes(webhook_params)
|
||||
@webhook_url.save!
|
||||
|
||||
@webhook_url.url.present? ? @webhook_url.save! : @webhook_url.delete
|
||||
|
||||
redirect_back(fallback_location: settings_webhooks_path, notice: I18n.t('webhook_url_has_been_saved'))
|
||||
redirect_to settings_webhooks_path, notice: I18n.t('webhook_url_has_been_saved')
|
||||
end
|
||||
|
||||
def update
|
||||
@webhook_url.update!(update_params)
|
||||
|
||||
redirect_back(fallback_location: settings_webhook_path(@webhook_url),
|
||||
notice: I18n.t('webhook_url_has_been_updated'))
|
||||
end
|
||||
|
||||
def destroy
|
||||
@webhook_url.destroy!
|
||||
|
||||
redirect_to settings_webhooks_path, notice: I18n.t('webhook_url_has_been_deleted')
|
||||
end
|
||||
|
||||
def resend
|
||||
submitter = current_account.submitters.where.not(completed_at: nil).order(:id).last
|
||||
|
||||
if submitter.blank? || @webhook_url.blank?
|
||||
return redirect_back(fallback_location: settings_webhooks_path,
|
||||
alert: I18n.t('unable_to_resend_webhook_request'))
|
||||
end
|
||||
|
||||
SendFormCompletedWebhookRequestJob.perform_async('submitter_id' => submitter.id,
|
||||
'webhook_url_id' => @webhook_url.id)
|
||||
|
||||
@@ -25,11 +50,11 @@ class WebhookSettingsController < ApplicationController
|
||||
|
||||
private
|
||||
|
||||
def load_webhook_url
|
||||
@webhook_url = current_account.webhook_urls.first_or_initialize
|
||||
def create_params
|
||||
params.require(:webhook_url).permit(:url, events: []).reverse_merge(events: [])
|
||||
end
|
||||
|
||||
def webhook_params
|
||||
def update_params
|
||||
params.require(:webhook_url).permit(:url)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -12,6 +12,10 @@ window.customElements.define('draw-signature', class extends HTMLElement {
|
||||
|
||||
this.pad = new SignaturePad(this.canvas)
|
||||
|
||||
if (this.dataset.color) {
|
||||
this.pad.penColor = this.dataset.color
|
||||
}
|
||||
|
||||
this.pad.addEventListener('endStroke', () => {
|
||||
this.updateSubmitButtonVisibility()
|
||||
})
|
||||
|
||||
@@ -39,7 +39,6 @@
|
||||
</template>
|
||||
</div>
|
||||
<div
|
||||
v-if="isActive"
|
||||
ref="scrollToElem"
|
||||
class="absolute"
|
||||
:style="{ top: scrollPadding }"
|
||||
@@ -405,8 +404,12 @@ export default {
|
||||
}
|
||||
|
||||
if (this.field.preferences?.font_size) {
|
||||
style.fontSize = `clamp(4pt, 1.6vw, ${this.field.preferences.font_size}pt)`
|
||||
style.lineHeight = `clamp(6pt, 2.0vw, ${parseInt(this.field.preferences.font_size) + 3}pt)`
|
||||
style.fontSize = `clamp(4pt, 1.6vw, ${parseInt(this.field.preferences.font_size) * 1.23}pt)`
|
||||
style.lineHeight = `clamp(6pt, 2.0vw, ${parseInt(this.field.preferences.font_size) * 1.23 + 3}pt)`
|
||||
}
|
||||
|
||||
if (this.field.preferences?.color) {
|
||||
style.color = this.field.preferences.color
|
||||
}
|
||||
|
||||
return style
|
||||
|
||||
@@ -24,6 +24,44 @@
|
||||
:fields="formulaFields"
|
||||
:values="values"
|
||||
/>
|
||||
<Teleport
|
||||
v-if="completeButtonToRef"
|
||||
:to="completeButtonToRef"
|
||||
>
|
||||
<span
|
||||
v-if="(emptyValueRequiredStep && ((stepFields.length - 1) !== currentStep || currentStepFields !== emptyValueRequiredStep)) || isCompleted"
|
||||
class="tooltip-left"
|
||||
:class="{ tooltip: !isCompleted }"
|
||||
:data-tip="t('fill_all_required_fields_to_complete')"
|
||||
>
|
||||
<button
|
||||
class="btn btn-sm btn-neutral text-white px-4 w-full flex justify-center btn-disabled pointer-events-auto"
|
||||
@click="[isFormVisible = true, !isCompleted && goToStep(stepFields.indexOf(emptyValueRequiredStep), true, false)]"
|
||||
>
|
||||
{{ t('complete') }}
|
||||
</button>
|
||||
</span>
|
||||
<button
|
||||
v-else
|
||||
id="complete_form_button"
|
||||
class="btn btn-sm btn-neutral text-white px-4 w-full flex justify-center"
|
||||
form="steps_form"
|
||||
type="submit"
|
||||
name="completed"
|
||||
value="true"
|
||||
:disabled="isSubmittingComplete"
|
||||
>
|
||||
<span class="flex items-center">
|
||||
<IconInnerShadowTop
|
||||
v-if="isSubmittingComplete"
|
||||
class="mr-1 animate-spin w-5 h-5"
|
||||
/>
|
||||
<span>
|
||||
{{ t('complete') }}
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
</Teleport>
|
||||
<button
|
||||
v-if="!isFormVisible"
|
||||
id="expand_form_button"
|
||||
@@ -74,6 +112,7 @@
|
||||
>
|
||||
<form
|
||||
v-if="!isCompleted && !isInvite"
|
||||
id="steps_form"
|
||||
ref="form"
|
||||
:action="submitPath"
|
||||
method="post"
|
||||
@@ -592,6 +631,11 @@ export default {
|
||||
required: false,
|
||||
default: false
|
||||
},
|
||||
completeButtonToRef: {
|
||||
type: Object,
|
||||
required: false,
|
||||
default: null
|
||||
},
|
||||
attachments: {
|
||||
type: Array,
|
||||
required: false,
|
||||
@@ -772,6 +816,7 @@ export default {
|
||||
phoneVerifiedValues: {},
|
||||
orientation: screen?.orientation?.type,
|
||||
isSubmitting: false,
|
||||
isSubmittingComplete: false,
|
||||
submittedValues: {},
|
||||
recalculateButtonDisabledKey: ''
|
||||
}
|
||||
@@ -780,6 +825,13 @@ export default {
|
||||
isMobile () {
|
||||
return /android|iphone|ipad/i.test(navigator.userAgent)
|
||||
},
|
||||
emptyValueRequiredStep () {
|
||||
return this.stepFields.find((fields, index) => {
|
||||
return fields.some((f) => {
|
||||
return f.required && isEmpty(this.values[f.uuid])
|
||||
})
|
||||
})
|
||||
},
|
||||
submitButtonText () {
|
||||
if (this.alwaysMinimize) {
|
||||
return this.t('submit')
|
||||
@@ -1146,9 +1198,15 @@ export default {
|
||||
scrollIntoArea (area) {
|
||||
return this.$refs.areas.scrollIntoArea(area)
|
||||
},
|
||||
async submitStep () {
|
||||
async submitStep (e) {
|
||||
this.isSubmitting = true
|
||||
|
||||
const forceComplete = e?.submitter?.getAttribute('name') === 'completed'
|
||||
|
||||
if (forceComplete) {
|
||||
this.isSubmittingComplete = true
|
||||
}
|
||||
|
||||
const submitStep = this.currentStep
|
||||
|
||||
const stepPromise = ['signature', 'phone', 'initials', 'payment'].includes(this.currentField.type)
|
||||
@@ -1157,7 +1215,7 @@ export default {
|
||||
|
||||
stepPromise().then(async () => {
|
||||
const emptyRequiredField = this.stepFields.find((fields, index) => {
|
||||
if (index >= submitStep) {
|
||||
if (forceComplete ? index === submitStep : index >= submitStep) {
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -1167,7 +1225,7 @@ export default {
|
||||
})
|
||||
|
||||
const formData = new FormData(this.$refs.form)
|
||||
const isLastStep = submitStep === this.stepFields.length - 1
|
||||
const isLastStep = (submitStep === this.stepFields.length - 1) || forceComplete
|
||||
|
||||
if (isLastStep && !emptyRequiredField && !this.inviteSubmitters.length) {
|
||||
formData.append('completed', 'true')
|
||||
@@ -1192,7 +1250,7 @@ export default {
|
||||
return Promise.reject(new Error(data.error))
|
||||
}
|
||||
|
||||
const nextStep = (isLastStep && emptyRequiredField) || this.stepFields[submitStep + 1]
|
||||
const nextStep = (isLastStep && emptyRequiredField) || (forceComplete ? null : this.stepFields[submitStep + 1])
|
||||
|
||||
if (nextStep) {
|
||||
if (this.alwaysMinimize) {
|
||||
@@ -1213,6 +1271,7 @@ export default {
|
||||
console.error(error)
|
||||
}).finally(() => {
|
||||
this.isSubmitting = false
|
||||
this.isSubmittingComplete = false
|
||||
})
|
||||
}).catch(error => {
|
||||
if (error?.message === 'Image too small') {
|
||||
@@ -1222,6 +1281,7 @@ export default {
|
||||
}
|
||||
}).finally(() => {
|
||||
this.isSubmitting = false
|
||||
this.isSubmittingComplete = false
|
||||
})
|
||||
},
|
||||
minimizeForm () {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
const en = {
|
||||
complete: 'Complete',
|
||||
fill_all_required_fields_to_complete: 'Fill all required fields to complete',
|
||||
sign_and_complete: 'Sign and Complete',
|
||||
text: 'Text',
|
||||
by_clicking_you_agree_to_the: 'By clicking "{button}", you agree to the',
|
||||
@@ -93,6 +94,7 @@ const en = {
|
||||
|
||||
const es = {
|
||||
complete: 'Completar',
|
||||
fill_all_required_fields_to_complete: 'Complete todos los campos requeridos para finalizar',
|
||||
sign_and_complete: 'Firmar y Completar',
|
||||
invite: 'Invitar',
|
||||
email: 'Correo electrónico',
|
||||
@@ -185,6 +187,7 @@ const es = {
|
||||
|
||||
const it = {
|
||||
complete: 'Completa',
|
||||
fill_all_required_fields_to_complete: 'Compila tutti i campi obbligatori per completare',
|
||||
sign_and_complete: 'Firma e Completa',
|
||||
invite: 'Invita',
|
||||
email: 'Email',
|
||||
@@ -277,6 +280,7 @@ const it = {
|
||||
|
||||
const de = {
|
||||
complete: 'Abschließen',
|
||||
fill_all_required_fields_to_complete: 'Alle erforderlichen Felder ausfüllen, um abzuschließen',
|
||||
sign_and_complete: 'Signieren und Abschließen',
|
||||
invite: 'Einladen',
|
||||
email: 'E-Mail',
|
||||
@@ -369,6 +373,7 @@ const de = {
|
||||
|
||||
const fr = {
|
||||
complete: 'Terminer',
|
||||
fill_all_required_fields_to_complete: 'Veuillez remplir tous les champs obligatoires pour compléter',
|
||||
sign_and_complete: 'Signer et Terminer',
|
||||
invite: 'Inviter',
|
||||
email: 'Courriel',
|
||||
@@ -461,6 +466,7 @@ const fr = {
|
||||
|
||||
const pl = {
|
||||
complete: 'Zakończ',
|
||||
fill_all_required_fields_to_complete: 'Uzupełnij wszystkie wymagane pola, aby zakończyć',
|
||||
sign_and_complete: 'Podpisz i zakończ',
|
||||
invite: 'Zaproś',
|
||||
email: 'E-mail',
|
||||
@@ -553,6 +559,7 @@ const pl = {
|
||||
|
||||
const uk = {
|
||||
complete: 'Завершити',
|
||||
fill_all_required_fields_to_complete: "Заповніть всі обов'язкові поля для завершення",
|
||||
sign_and_complete: 'Підписати і завершити',
|
||||
invite: 'Запросити',
|
||||
email: 'Електронна пошта',
|
||||
@@ -645,6 +652,7 @@ const uk = {
|
||||
|
||||
const cs = {
|
||||
complete: 'Dokončit',
|
||||
fill_all_required_fields_to_complete: 'Please complete all mandatory fields',
|
||||
sign_and_complete: 'Podepsat a dokončit',
|
||||
invite: 'Pozvat',
|
||||
email: 'E-mail',
|
||||
@@ -737,6 +745,7 @@ const cs = {
|
||||
|
||||
const pt = {
|
||||
complete: 'Completar',
|
||||
preencher_todos_os_campos_obrigatórios_para_concluir: 'Preencher todos os campos obrigatórios para concluir',
|
||||
sign_and_complete: 'Assinar e Completar',
|
||||
invite: 'Convidar',
|
||||
email: 'E-mail',
|
||||
@@ -829,6 +838,7 @@ const pt = {
|
||||
|
||||
const he = {
|
||||
complete: 'השלם',
|
||||
fill_all_required_fields_to_complete: 'נא למלא את כל השדות הנדרשים להשלמה',
|
||||
sign_and_complete: 'חתום והשלם',
|
||||
invite: 'הזמן',
|
||||
email: 'דוא"ל',
|
||||
@@ -922,6 +932,7 @@ const he = {
|
||||
|
||||
const nl = {
|
||||
complete: 'Voltooien',
|
||||
vul_alle_verplichte_velden_in_om_te_voltooien: 'Vul alle verplichte velden in om te voltooien',
|
||||
sign_and_complete: 'Ondertekenen en voltooien',
|
||||
invite: 'Uitnodigen',
|
||||
email: 'E-mail',
|
||||
@@ -1015,6 +1026,7 @@ const nl = {
|
||||
|
||||
const ar = {
|
||||
complete: 'اكتمال',
|
||||
fill_all_required_fields_to_complete: 'يرجى ملء جميع الحقول المطلوبة لإكمال',
|
||||
sign_and_complete: 'التوقيع والاكتمال',
|
||||
invite: 'دعوة',
|
||||
email: 'البريد الإلكتروني',
|
||||
@@ -1107,6 +1119,7 @@ const ar = {
|
||||
|
||||
const ko = {
|
||||
complete: '완료',
|
||||
fill_all_required_fields_to_complete: '모든 필수 필드를 작성하여 완료하세요',
|
||||
sign_and_complete: '서명하고 완료하기',
|
||||
invite: '초대하기',
|
||||
email: '이메일',
|
||||
|
||||
@@ -226,6 +226,10 @@ export default {
|
||||
if (this.$refs.canvas) {
|
||||
this.pad = new SignaturePad(this.$refs.canvas)
|
||||
|
||||
if (this.field.preferences?.color) {
|
||||
this.pad.penColor = this.field.preferences.color
|
||||
}
|
||||
|
||||
this.pad.addEventListener('beginStroke', () => {
|
||||
this.isInitialsStarted = true
|
||||
|
||||
|
||||
@@ -423,6 +423,10 @@ export default {
|
||||
if (this.$refs.canvas) {
|
||||
this.pad = new SignaturePad(this.$refs.canvas)
|
||||
|
||||
if (this.field.preferences?.color) {
|
||||
this.pad.penColor = this.field.preferences.color
|
||||
}
|
||||
|
||||
this.pad.addEventListener('endStroke', () => {
|
||||
this.isSignatureStarted = true
|
||||
|
||||
|
||||
@@ -175,6 +175,7 @@
|
||||
<div
|
||||
ref="textContainer"
|
||||
class="flex items-center px-0.5"
|
||||
:style="{ color: field.preferences?.color }"
|
||||
:class="{ 'w-full h-full': ['cells', 'checkbox'].includes(field.type) }"
|
||||
>
|
||||
<IconCheck
|
||||
|
||||
@@ -79,7 +79,7 @@
|
||||
:stroke-width="1.6"
|
||||
/>
|
||||
<span class="py-1">
|
||||
{{ t('add') }} {{ names[submitters.length] }}
|
||||
{{ t('add') }} {{ names[lastPartyIndex] }}
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
@@ -202,7 +202,7 @@
|
||||
:stroke-width="1.6"
|
||||
/>
|
||||
<span class="py-1">
|
||||
{{ t('add') }} {{ names[submitters.length] }}
|
||||
{{ t('add') }} {{ names[lastPartyIndex] }}
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
@@ -321,6 +321,15 @@ export default {
|
||||
this.t('twentieth_party')
|
||||
]
|
||||
},
|
||||
lastPartyIndex () {
|
||||
const index = Math.max(...this.submitters.map((s) => this.names.indexOf(s.name)))
|
||||
|
||||
if (index !== -1) {
|
||||
return index + 1
|
||||
} else {
|
||||
return this.submitters.length
|
||||
}
|
||||
},
|
||||
selectedSubmitter () {
|
||||
return this.submitters.find((e) => e.uuid === this.modelValue)
|
||||
}
|
||||
@@ -353,7 +362,7 @@ export default {
|
||||
},
|
||||
addSubmitter () {
|
||||
const newSubmitter = {
|
||||
name: this.names[this.submitters.length],
|
||||
name: this.names[this.lastPartyIndex],
|
||||
uuid: v4()
|
||||
}
|
||||
|
||||
|
||||
@@ -28,9 +28,9 @@ class SendFormCompletedWebhookRequestJob
|
||||
timestamp: Time.current,
|
||||
data: Submitters::SerializeForWebhook.call(submitter)
|
||||
}.to_json,
|
||||
**webhook_url.secret.to_h,
|
||||
'Content-Type' => 'application/json',
|
||||
'User-Agent' => USER_AGENT)
|
||||
'User-Agent' => USER_AGENT,
|
||||
**webhook_url.secret.to_h)
|
||||
rescue Faraday::Error
|
||||
nil
|
||||
end
|
||||
|
||||
@@ -24,9 +24,9 @@ class SendSubmissionCompletedWebhookRequestJob
|
||||
timestamp: Time.current,
|
||||
data: Submissions::SerializeForApi.call(submission)
|
||||
}.to_json,
|
||||
**webhook_url.secret.to_h,
|
||||
'Content-Type' => 'application/json',
|
||||
'User-Agent' => USER_AGENT)
|
||||
'User-Agent' => USER_AGENT,
|
||||
**webhook_url.secret.to_h)
|
||||
rescue Faraday::Error
|
||||
nil
|
||||
end
|
||||
|
||||
@@ -138,9 +138,11 @@ class SubmitterMailer < ApplicationMailer
|
||||
private
|
||||
|
||||
def build_submitter_reply_to(submitter)
|
||||
reply_to =
|
||||
submitter.preferences['reply_to'].presence ||
|
||||
(submitter.submission.created_by_user || submitter.template.author)&.friendly_name&.sub(/\+\w+@/, '@')
|
||||
reply_to = submitter.preferences['reply_to'].presence
|
||||
|
||||
if reply_to.blank? && (submitter.submission.created_by_user || submitter.template.author)&.email != submitter.email
|
||||
reply_to = (submitter.submission.created_by_user || submitter.template.author)&.friendly_name&.sub(/\+\w+@/, '@')
|
||||
end
|
||||
|
||||
return nil if reply_to.to_s.match?(NO_REPLY_REGEXP)
|
||||
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
<svg class="<%= local_assigns[:class] %>" xmlns="http://www.w3.org/2000/svg" x-bind:width="size" x-bind:height="size" viewBox="0 0 24 24" fill="none" stroke="currentColor" x-bind:stroke-width="stroke" stroke-linecap="round" stroke-linejoin="round" width="24" height="24" stroke-width="2">
|
||||
<path d="M5 13a2 2 0 0 1 2 -2h10a2 2 0 0 1 2 2v6a2 2 0 0 1 -2 2h-10a2 2 0 0 1 -2 -2v-6z"></path>
|
||||
<path d="M11 16a1 1 0 1 0 2 0a1 1 0 0 0 -2 0"></path>
|
||||
<path d="M8 11v-4a4 4 0 1 1 8 0v4"></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 496 B |
@@ -1,5 +1,6 @@
|
||||
<% align = field.dig('preferences', 'align') %>
|
||||
<field-value dir="auto" class="flex absolute text-[1.6vw] lg:text-base <%= 'font-serif' if field.dig('preferences', 'font') == 'Times' %> <%= align == 'right' ? 'justify-end' : (align == 'center' ? 'justify-center' : '') %>" style="width: <%= area['w'] * 100 %>%; height: <%= area['h'] * 100 %>%; left: <%= area['x'] * 100 %>%; top: <%= area['y'] * 100 %>%; <%= "font-size: clamp(4pt, 1.6vw, #{field['preferences']['font_size']}pt); line-height: `clamp(6pt, 2.0vw, #{field['preferences']['font_size'].to_i + 3}pt)`" if field.dig('preferences', 'font_size') %>">
|
||||
<% color = field.dig('preferences', 'color') %>
|
||||
<field-value dir="auto" class="flex absolute text-[1.6vw] lg:text-base <%= 'font-serif' if field.dig('preferences', 'font') == 'Times' %> <%= align == 'right' ? 'justify-end' : (align == 'center' ? 'justify-center' : '') %>" style="<%= "color: #{color}; " if color.present? %>width: <%= area['w'] * 100 %>%; height: <%= area['h'] * 100 %>%; left: <%= area['x'] * 100 %>%; top: <%= area['y'] * 100 %>%; <%= "font-size: clamp(4pt, 1.6vw, #{field['preferences']['font_size'].to_i * 1.23}pt); line-height: `clamp(6pt, 2.0vw, #{(field['preferences']['font_size'].to_i * 1.23) + 3}pt)`" if field.dig('preferences', 'font_size') %>">
|
||||
<% if field['type'] == 'signature' %>
|
||||
<div class="flex flex-col justify-between h-full overflow-hidden">
|
||||
<div class="flex-grow flex overflow-hidden" style="min-height: 50%">
|
||||
|
||||
@@ -15,9 +15,10 @@
|
||||
<%= render 'shared/posthog' if ENV['POSTHOG_TOKEN'] %>
|
||||
</head>
|
||||
<body>
|
||||
<draw-signature data-slug="<%= params[:slug] %>" class="flex items-center h-screen p-2 justify-center">
|
||||
<% field = (@submitter.submission.template_fields || @submitter.template.fields).find { |f| f['type'] == 'signature' && f['uuid'].starts_with?(params[:f]) } %>
|
||||
<draw-signature data-slug="<%= params[:slug] %>" data-color="<%= field.dig('preferences', 'color') %>" class="flex items-center h-screen p-2 justify-center">
|
||||
<%= form_for '', url: submit_form_path(params[:slug]), html: { style: 'max-width: 900px; width: 100%; margin-bottom: 120px' }, method: :put do |f| %>
|
||||
<input value="" type="hidden" name="values[<%= (@submitter.submission.template_fields || @submitter.template.fields).find { |f| f['type'] == 'signature' && f['uuid'].starts_with?(params[:f]) }['uuid'] %>]">
|
||||
<input value="" type="hidden" name="values[<%= field['uuid'] %>]">
|
||||
<div class="font-semibold text-4xl text-center w-full mb-2">
|
||||
<%= t('draw_signature') %>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
<div class="flex flex-wrap space-y-4 md:flex-nowrap md:space-y-0 md:space-x-10">
|
||||
<%= render 'shared/settings_nav' %>
|
||||
<div class="flex-grow min-w-0">
|
||||
<div class="flex flex-col gap-2 md:flex-row md:justify-between md:items-center mb-4">
|
||||
<h1 class="text-4xl font-bold">Webhooks</h1>
|
||||
<div class="flex flex-col gap-2 md:flex-row md:justify-between md:items-center">
|
||||
<%= render 'shared/test_mode_toggle' %>
|
||||
<% if @webhook_url.persisted? %>
|
||||
<%= link_to new_settings_webhook_path, class: 'md:ml-3 btn bg-white btn-outline btn-md gap-2 w-full md:w-fit', data: { turbo_frame: 'modal' } do %>
|
||||
<%= svg_icon('plus', class: 'w-6 h-6') %>
|
||||
<span><%= t('new_webhook') %></span>
|
||||
<% end %>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
<div class="space-y-4">
|
||||
<% @webhook_urls.each do |webhook_url| %>
|
||||
<%= link_to settings_webhook_path(webhook_url), class: 'card bg-base-200' do %>
|
||||
<div class="card-body p-6 min-w-0">
|
||||
<p class="flex items-center space-x-1">
|
||||
<%= svg_icon('world', class: 'w-6 h-6 shrink-0') %>
|
||||
<span class="text-xl font-semibold truncate"><%= webhook_url.url %></span>
|
||||
</p>
|
||||
<div class="flex flex-wrap gap-2 mt-2">
|
||||
<% webhook_url.events.each do |event| %>
|
||||
<span class="badge badge-outline"><%= event %></span>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,29 @@
|
||||
<%= render 'shared/turbo_modal', title: t('new_webhook') do %>
|
||||
<%= form_for @webhook_url, url: settings_webhooks_path, html: { class: 'space-y-4' }, data: { turbo_frame: :_top } do |f| %>
|
||||
<div class="space-y-4">
|
||||
<div class="form-control">
|
||||
<%= f.label :url, 'Webhook URL', class: 'label' %>
|
||||
<%= f.url_field :url, class: 'base-input', placeholder: 'https://example.com/hook', required: true %>
|
||||
</div>
|
||||
<div class="space-y-4">
|
||||
<% WebhookUrl::EVENTS.group_by { |e| e.include?('form') }.each do |_, events| %>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-y-2">
|
||||
<%= f.collection_check_boxes(:events, events, :to_s, :to_s, include_hidden: false) do |b| %>
|
||||
<div class="flex">
|
||||
<label class="flex items-center space-x-2">
|
||||
<%= b.check_box class: 'base-checkbox', checked: @webhook_url.events.include?(b.value) %>
|
||||
<span>
|
||||
<%= b.label %>
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-control pt-2">
|
||||
<%= f.button button_title(title: t('save'), disabled_with: t('saving')), class: 'base-button' %>
|
||||
</div>
|
||||
<% end %>
|
||||
<% end %>
|
||||
@@ -1,38 +1,78 @@
|
||||
<div class="flex flex-wrap space-y-4 md:flex-nowrap md:space-y-0 md:space-x-10">
|
||||
<%= render 'shared/settings_nav' %>
|
||||
<div class="flex-grow">
|
||||
<div class="flex flex-col gap-2 md:flex-row md:justify-between md:items-end mb-4">
|
||||
<h1 class="text-4xl font-bold">Webhooks</h1>
|
||||
<%= render 'shared/test_mode_toggle' %>
|
||||
<div class="flex flex-col gap-2 md:flex-row md:justify-between md:items-center mb-4">
|
||||
<h1 class="text-4xl font-bold">Webhook</h1>
|
||||
<div class="flex flex-col gap-2 md:flex-row md:justify-between md:items-center">
|
||||
<% if params[:action] == 'index' %>
|
||||
<%= render 'shared/test_mode_toggle' %>
|
||||
<% end %>
|
||||
<% if @webhook_url.persisted? && params[:action] == 'index' %>
|
||||
<%= link_to new_settings_webhook_path, class: 'md:ml-3 btn bg-white btn-outline btn-md gap-2 w-full md:w-fit', data: { turbo_frame: 'modal' } do %>
|
||||
<%= svg_icon('plus', class: 'w-6 h-6') %>
|
||||
<span><%= t('new_webhook') %></span>
|
||||
<% end %>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card bg-base-200">
|
||||
<div class="card-body p-6">
|
||||
<%= form_for @webhook_url, url: settings_webhooks_path, method: :post, html: { autocomplete: 'off' } do |f| %>
|
||||
<%= f.label :url, 'Webhook URL', class: 'text-sm font-semibold' %>
|
||||
<div class="flex flex-row flex-wrap space-y-2 md:space-y-0 md:flex-nowrap md:space-x-2 mt-2">
|
||||
<%= f.url_field :url, class: 'input font-mono input-bordered w-full', placeholder: 'https://example.com/hook' %>
|
||||
<%= f.button button_title(title: t('save'), disabled_with: t('saving')), class: 'base-button w-full md:w-32' %>
|
||||
<% if @webhook_url.persisted? %>
|
||||
<a href="<%= webhook_secret_path(@webhook_url) %>" data-turbo-frame="modal" class="white-button w-full md:w-auto">
|
||||
<%= @webhook_url.secret.present? ? t('edit_secret') : t('add_secret') %>
|
||||
</a>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
<%= form_for @webhook_url, url: @webhook_url.url.present? ? webhook_preference_path(@webhook_url) : '', method: :put, html: { autocomplete: 'off' } do |f| %>
|
||||
<% WebhookUrl::EVENTS.group_by { |e| e.include?('form') }.each do |_, events| %>
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 lg:grid-cols-4 mt-4 gap-y-2">
|
||||
<% events.each do |event| %>
|
||||
<%= f.fields_for :events do |ff| %>
|
||||
<div class="flex">
|
||||
<label class="flex items-center space-x-2 cursor-pointer">
|
||||
<%= ff.check_box event, class: 'base-checkbox', checked: @webhook_url.events.include?(event), onchange: 'this.form.requestSubmit()', disabled: @webhook_url.url.blank? %>
|
||||
<span>
|
||||
<%= event %>
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="flex flex-col gap-2 md:flex-row md:justify-between md:items-end md:relative">
|
||||
<%= label_tag :url, 'Webhook URL', class: 'text-sm font-semibold' %>
|
||||
<% if @webhook_url.persisted? %>
|
||||
<div class="flex items-center space-x-2 md:absolute md:right-0">
|
||||
<%= link_to webhook_secret_path(@webhook_url), class: 'btn btn-outline btn-sm bg-white', data: { turbo_frame: 'modal' } do %>
|
||||
<%= svg_icon('lock', class: 'w-4 h-4') %>
|
||||
<span><%= @webhook_url.secret.present? ? t('edit_secret') : t('add_secret') %></span>
|
||||
<% end %>
|
||||
<div class="tooltip tooltip-left md:tooltip-top" data-tip="<%= t('delete_webhook') %>">
|
||||
<%= button_to settings_webhook_path(@webhook_url), class: 'btn btn-warning btn-sm', method: :delete, data: { turbo_confirm: t('are_you_sure_') } do %>
|
||||
<span><%= t('delete') %></span>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
<%= form_for @webhook_url, url: @webhook_url.persisted? ? settings_webhook_path(@webhook_url) : settings_webhooks_path, html: { autocomplete: 'off' } do |f| %>
|
||||
<div class="flex flex-row flex-wrap space-y-2 md:space-y-0 md:flex-nowrap md:space-x-2">
|
||||
<%= f.url_field :url, class: 'input font-mono input-bordered w-full', placeholder: 'https://example.com/hook', required: true %>
|
||||
<%= f.button button_title(title: t('save'), disabled_with: t('saving')), class: 'base-button w-full md:w-32' %>
|
||||
</div>
|
||||
<% unless @webhook_url.persisted? %>
|
||||
<div class="space-y-4 mt-4">
|
||||
<% WebhookUrl::EVENTS.group_by { |e| e.include?('form') }.each do |_, events| %>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-y-2">
|
||||
<%= f.collection_check_boxes(:events, events, :to_s, :to_s, include_hidden: false) do |b| %>
|
||||
<div class="flex">
|
||||
<label class="flex items-center space-x-2">
|
||||
<%= b.check_box class: 'base-checkbox', checked: @webhook_url.events.include?(b.value) %>
|
||||
<span>
|
||||
<%= b.label %>
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
<% end %>
|
||||
<% if @webhook_url.persisted? %>
|
||||
<%= form_for @webhook_url, url: webhook_preference_path(@webhook_url), method: :put, html: { autocomplete: 'off', class: 'mt-2' } do |f| %>
|
||||
<div class="space-y-4">
|
||||
<% WebhookUrl::EVENTS.group_by { |e| e.include?('form') }.each do |_, events| %>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-y-4">
|
||||
<% events.each do |event| %>
|
||||
<%= f.fields_for :events do |ff| %>
|
||||
<div class="flex">
|
||||
<label class="flex items-center cursor-pointer">
|
||||
<%= ff.check_box event, class: 'base-checkbox', checked: @webhook_url.events.include?(event), onchange: 'this.form.requestSubmit()' %>
|
||||
<span class="ml-2"><%= event %></span>
|
||||
</label>
|
||||
</div>
|
||||
<% end %>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
@@ -44,12 +84,12 @@
|
||||
<div class="space-y-4 mt-4">
|
||||
<div class="collapse collapse-open bg-base-200 px-1">
|
||||
<div class="p-4 text-xl font-medium">
|
||||
<div class="flex items-center justify-between gap-2">
|
||||
<div class="flex flex-col gap-2 md:flex-row md:justify-between md:items-center md:h-8">
|
||||
<span>
|
||||
<%= t('submission_example_payload') %>
|
||||
</span>
|
||||
<% if @webhook_url.url.present? && @webhook_url.events.include?('form.completed') %>
|
||||
<%= button_to button_title(title: 'Test Webhook', disabled_with: t('sending'), icon_disabled: svg_icon('loader', class: 'w-4 h-4 animate-spin')), settings_webhooks_path, class: 'btn btn-neutral btn-outline btn-sm', method: :put %>
|
||||
<%= button_to button_title(title: 'Test Webhook', disabled_with: t('sending'), icon_disabled: svg_icon('loader', class: 'w-4 h-4 animate-spin')), settings_webhook_resend_path(@webhook_url), class: 'btn btn-neutral btn-outline btn-sm', method: :post %>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+32
-8
@@ -511,13 +511,17 @@ en: &en
|
||||
webhook_secret_has_been_saved: Webhook Secret has been saved.
|
||||
webhook_url_has_been_saved: Webhook URL has been saved.
|
||||
webhook_request_has_been_sent: Webhook request has been sent.
|
||||
webhook_url_has_been_updated: Webhook URL has been updated.
|
||||
webhook_url_has_been_deleted: Webhook URL has been deleted.
|
||||
unable_to_resend_webhook_request: Unable to resend webhook request.
|
||||
new_webhook: New Webhook
|
||||
delete_webhook: Delete webhook
|
||||
count_submissions_have_been_created: '%{count} submissions have been created.'
|
||||
gmail_has_been_connected: Gmail has been connected
|
||||
microsoft_account_has_been_connected: Microsoft Account has been connected
|
||||
sms_length_cant_be_longer_than_120_bytes: SMS length can't be longer than 120 bytes
|
||||
connected_successfully: Connected successfully.
|
||||
user_nameid_not_found: 'User %{nameid} not found.'
|
||||
webhook_request_has_been_sent: Webhook request has been sent.
|
||||
sso_settings_have_been_updated: SSO settings have been updated.
|
||||
sms_has_been_sent: SMS has been sent.
|
||||
account_has_been_created: Account has been created.
|
||||
@@ -1156,13 +1160,17 @@ es: &es
|
||||
webhook_secret_has_been_saved: El secreto del Webhook ha sido guardado.
|
||||
webhook_url_has_been_saved: La URL del Webhook ha sido guardada.
|
||||
webhook_request_has_been_sent: La solicitud del Webhook ha sido enviada.
|
||||
webhook_url_has_been_updated: La URL del Webhook ha sido actualizada.
|
||||
webhook_url_has_been_deleted: La URL del Webhook ha sido eliminada.
|
||||
unable_to_resend_webhook_request: No se pudo reenviar la solicitud del webhook.
|
||||
new_webhook: Nuevo Webhook
|
||||
delete_webhook: Eliminar webhook
|
||||
count_submissions_have_been_created: '%{count} envíos han sido creados.'
|
||||
gmail_has_been_connected: Gmail ha sido conectado.
|
||||
microsoft_account_has_been_connected: La cuenta de Microsoft ha sido conectada.
|
||||
sms_length_cant_be_longer_than_120_bytes: La longitud del SMS no puede ser mayor a 120 bytes.
|
||||
connected_successfully: Conectado con éxito.
|
||||
user_nameid_not_found: 'Usuario %{nameid} no encontrado.'
|
||||
webhook_request_has_been_sent: La solicitud del Webhook ha sido enviada.
|
||||
sso_settings_have_been_updated: La configuración de SSO ha sido actualizada.
|
||||
sms_has_been_sent: El SMS ha sido enviado.
|
||||
account_has_been_created: La cuenta ha sido creada.
|
||||
@@ -1801,13 +1809,17 @@ it: &it
|
||||
webhook_secret_has_been_saved: Il segreto del Webhook è stato salvato.
|
||||
webhook_url_has_been_saved: "L'URL del Webhook è stato salvato."
|
||||
webhook_request_has_been_sent: La richiesta del Webhook è stata inviata.
|
||||
webhook_url_has_been_updated: "L'URL del Webhook è stata aggiornata."
|
||||
webhook_url_has_been_deleted: "L'URL del Webhook è stata eliminata."
|
||||
unable_to_resend_webhook_request: Impossibile reinviare la richiesta del webhook.
|
||||
new_webhook: Nuovo Webhook
|
||||
delete_webhook: Elimina webhook
|
||||
count_submissions_have_been_created: '%{count} invii sono stati creati.'
|
||||
gmail_has_been_connected: Gmail è stato connesso.
|
||||
microsoft_account_has_been_connected: "L'account Microsoft è stato connesso."
|
||||
sms_length_cant_be_longer_than_120_bytes: "La lunghezza dell'SMS non può superare i 120 byte."
|
||||
connected_successfully: Collegamento avvenuto con successo.
|
||||
user_nameid_not_found: 'Utente %{nameid} non trovato.'
|
||||
webhook_request_has_been_sent: La richiesta del Webhook è stata inviata.
|
||||
sso_settings_have_been_updated: Le impostazioni SSO sono state aggiornate.
|
||||
sms_has_been_sent: "L'SMS è stato inviato."
|
||||
account_has_been_created: "L'account è stato creato."
|
||||
@@ -2447,13 +2459,17 @@ fr: &fr
|
||||
webhook_secret_has_been_saved: Le secret du Webhook a été enregistré.
|
||||
webhook_url_has_been_saved: "L'URL du Webhook a été enregistrée."
|
||||
webhook_request_has_been_sent: La demande du Webhook a été envoyée.
|
||||
webhook_url_has_been_updated: "L'URL du Webhook a été mise à jour."
|
||||
webhook_url_has_been_deleted: "L'URL du Webhook a été supprimée."
|
||||
unable_to_resend_webhook_request: Impossible de renvoyer la requête du webhook.
|
||||
new_webhook: Nouveau Webhook
|
||||
delete_webhook: Supprimer le webhook
|
||||
count_submissions_have_been_created: '%{count} soumissions ont été créées.'
|
||||
gmail_has_been_connected: Gmail a été connecté.
|
||||
microsoft_account_has_been_connected: Le compte Microsoft a été connecté.
|
||||
sms_length_cant_be_longer_than_120_bytes: La longueur du SMS ne peut pas dépasser 120 octets.
|
||||
connected_successfully: Connecté avec succès.
|
||||
user_nameid_not_found: 'Utilisateur %{nameid} introuvable.'
|
||||
webhook_request_has_been_sent: La demande du Webhook a été envoyée.
|
||||
sso_settings_have_been_updated: Les paramètres de SSO ont été mis à jour.
|
||||
sms_has_been_sent: Le SMS a été envoyé.
|
||||
account_has_been_created: Le compte a été créé.
|
||||
@@ -3092,13 +3108,17 @@ pt: &pt
|
||||
webhook_secret_has_been_saved: O segredo do Webhook foi salvo.
|
||||
webhook_url_has_been_saved: A URL do Webhook foi salva.
|
||||
webhook_request_has_been_sent: A solicitação do Webhook foi enviada.
|
||||
webhook_url_has_been_updated: URL do Webhook foi atualizada.
|
||||
webhook_url_has_been_deleted: URL do Webhook foi excluída.
|
||||
unable_to_resend_webhook_request: Não foi possível reenviar a solicitação do webhook.
|
||||
new_webhook: Novo Webhook
|
||||
delete_webhook: Excluir webhook
|
||||
count_submissions_have_been_created: '%{count} submissões foram criadas.'
|
||||
gmail_has_been_connected: O Gmail foi conectado
|
||||
microsoft_account_has_been_connected: A conta da Microsoft foi conectada
|
||||
sms_length_cant_be_longer_than_120_bytes: O comprimento do SMS não pode ultrapassar 120 bytes
|
||||
connected_successfully: Conectado com sucesso.
|
||||
user_nameid_not_found: 'Usuário %{nameid} não encontrado.'
|
||||
webhook_request_has_been_sent: A solicitação do Webhook foi enviada.
|
||||
sso_settings_have_been_updated: As configurações de SSO foram atualizadas.
|
||||
sms_has_been_sent: O SMS foi enviado.
|
||||
account_has_been_created: A conta foi criada.
|
||||
@@ -3699,8 +3719,8 @@ de: &de
|
||||
api_key: API-Schlüssel
|
||||
logo: Logo
|
||||
back: Zurück
|
||||
add_secret: Geheimnis hinzufügen
|
||||
edit_secret: Geheimnis bearbeiten
|
||||
add_secret: Geheimnis hinzuf
|
||||
edit_secret: Geheimnis bearb
|
||||
submission_example_payload: Beispiel-Payload für Einreichung
|
||||
there_are_no_signatures: Es gibt keine Unterschriften
|
||||
signed_with_trusted_certificate: Signiert mit vertrauenswürdigem Zertifikat
|
||||
@@ -3737,13 +3757,17 @@ de: &de
|
||||
webhook_secret_has_been_saved: Das Webhook-Geheimnis wurde gespeichert.
|
||||
webhook_url_has_been_saved: Die Webhook-URL wurde gespeichert.
|
||||
webhook_request_has_been_sent: Die Webhook-Anfrage wurde gesendet.
|
||||
webhook_url_has_been_updated: Webhook-URL wurde aktualisiert.
|
||||
webhook_url_has_been_deleted: Webhook-URL wurde gelöscht.
|
||||
unable_to_resend_webhook_request: Webhook-Anfrage konnte nicht erneut gesendet werden.
|
||||
new_webhook: Neuer Webhook
|
||||
delete_webhook: Webhook löschen
|
||||
count_submissions_have_been_created: '%{count} Einreichungen wurden erstellt.'
|
||||
gmail_has_been_connected: Gmail wurde verbunden.
|
||||
microsoft_account_has_been_connected: Microsoft-Konto wurde verbunden.
|
||||
sms_length_cant_be_longer_than_120_bytes: Die SMS-Länge darf 120 Bytes nicht überschreiten.
|
||||
connected_successfully: Erfolgreich verbunden.
|
||||
user_nameid_not_found: 'Benutzer %{nameid} nicht gefunden.'
|
||||
webhook_request_has_been_sent: Die Webhook-Anfrage wurde gesendet.
|
||||
sso_settings_have_been_updated: Die SSO-Einstellungen wurden aktualisiert.
|
||||
sms_has_been_sent: Die SMS wurde gesendet.
|
||||
account_has_been_created: Das Konto wurde erstellt.
|
||||
|
||||
+3
-1
@@ -168,7 +168,9 @@ Rails.application.routes.draw do
|
||||
defaults: { status: :integration }
|
||||
resource :personalization, only: %i[show create], controller: 'personalization_settings'
|
||||
resources :api, only: %i[index create], controller: 'api_settings'
|
||||
resource :webhooks, only: %i[show create update], controller: 'webhook_settings'
|
||||
resources :webhooks, only: %i[index show new create update destroy], controller: 'webhook_settings' do
|
||||
post :resend
|
||||
end
|
||||
resource :account, only: %i[show update destroy]
|
||||
resources :profile, only: %i[index] do
|
||||
collection do
|
||||
|
||||
@@ -68,7 +68,7 @@ module Submissions
|
||||
name: item['name'])
|
||||
end
|
||||
|
||||
return result_attachments.map { |e| e.tap(&:save!) } if image_pdfs.size < 2
|
||||
return ApplicationRecord.no_touching { result_attachments.map { |e| e.tap(&:save!) } } if image_pdfs.size < 2
|
||||
|
||||
images_pdf =
|
||||
image_pdfs.each_with_object(HexaPDF::Document.new) do |pdf, doc|
|
||||
@@ -87,7 +87,9 @@ module Submissions
|
||||
name: template.name
|
||||
)
|
||||
|
||||
(result_attachments + [images_pdf_attachment]).map { |e| e.tap(&:save!) }
|
||||
ApplicationRecord.no_touching do
|
||||
(result_attachments + [images_pdf_attachment]).map { |e| e.tap(&:save!) }
|
||||
end
|
||||
end
|
||||
|
||||
def generate_pdfs(submitter)
|
||||
@@ -177,6 +179,8 @@ module Submissions
|
||||
font_size = preferences_font_size
|
||||
font_size ||= (([page.box.width, page.box.height].min / A4_SIZE[0].to_f) * FONT_SIZE).to_i
|
||||
|
||||
fill_color = field.dig('preferences', 'color').presence
|
||||
|
||||
font = pdf.fonts.add(field.dig('preferences', 'font').presence || FONT_NAME)
|
||||
|
||||
value = submitter.values[field['uuid']]
|
||||
@@ -378,6 +382,7 @@ module Submissions
|
||||
next if char.blank?
|
||||
|
||||
text = HexaPDF::Layout::TextFragment.create(char, font:,
|
||||
fill_color:,
|
||||
font_size:)
|
||||
|
||||
line_height = layouter.fit([text], cell_width, height).lines.first.height
|
||||
@@ -385,6 +390,7 @@ module Submissions
|
||||
if preferences_font_size.blank? && line_height > (area['h'] * height)
|
||||
text = HexaPDF::Layout::TextFragment.create(char,
|
||||
font:,
|
||||
fill_color:,
|
||||
font_size: (font_size / 1.4).to_i)
|
||||
|
||||
line_height = layouter.fit([text], cell_width, height).lines.first.height
|
||||
@@ -393,6 +399,7 @@ module Submissions
|
||||
if preferences_font_size.blank? && line_height > (area['h'] * height)
|
||||
text = HexaPDF::Layout::TextFragment.create(char,
|
||||
font:,
|
||||
fill_color:,
|
||||
font_size: (font_size / 1.9).to_i)
|
||||
|
||||
line_height = layouter.fit([text], cell_width, height).lines.first.height
|
||||
@@ -412,6 +419,7 @@ module Submissions
|
||||
value = TextUtils.maybe_rtl_reverse(Array.wrap(value).join(', '))
|
||||
|
||||
text = HexaPDF::Layout::TextFragment.create(value, font:,
|
||||
fill_color:,
|
||||
font_size:)
|
||||
|
||||
lines = layouter.fit([text], area['w'] * width, height).lines
|
||||
@@ -420,6 +428,7 @@ module Submissions
|
||||
if preferences_font_size.blank? && box_height > (area['h'] * height) + 1
|
||||
text = HexaPDF::Layout::TextFragment.create(value,
|
||||
font:,
|
||||
fill_color:,
|
||||
font_size: (font_size / 1.4).to_i)
|
||||
|
||||
lines = layouter.fit([text], field['type'].in?(%w[date number]) ? width : area['w'] * width, height).lines
|
||||
@@ -430,6 +439,7 @@ module Submissions
|
||||
if preferences_font_size.blank? && box_height > (area['h'] * height) + 1
|
||||
text = HexaPDF::Layout::TextFragment.create(value,
|
||||
font:,
|
||||
fill_color:,
|
||||
font_size: (font_size / 1.9).to_i)
|
||||
|
||||
lines = layouter.fit([text], field['type'].in?(%w[date number]) ? width : area['w'] * width, height).lines
|
||||
|
||||
@@ -6,6 +6,20 @@ module Templates
|
||||
|
||||
FIELD_NAME_REGEXP = /\A(?=.*\p{L})[\p{L}\d\s-]+\z/
|
||||
SKIP_FIELD_DESCRIPTION = %w[undefined].freeze
|
||||
SELECT_PLACEHOLDER_REGEXP = /\b(
|
||||
Select |
|
||||
Choose |
|
||||
Wählen |
|
||||
Auswählen |
|
||||
Sélectionner|
|
||||
Choisir |
|
||||
Seleccionar |
|
||||
Elegir |
|
||||
Seleziona |
|
||||
Scegliere |
|
||||
Selecionar |
|
||||
Escolher
|
||||
)\b/ix
|
||||
|
||||
module_function
|
||||
|
||||
@@ -143,8 +157,8 @@ module Templates
|
||||
{
|
||||
**attrs,
|
||||
type: 'select',
|
||||
options: build_options(field[:Opt]),
|
||||
default_value: field.field_value
|
||||
options: build_options(field[:Opt], 'select'),
|
||||
default_value: field.field_value.to_s.match?(SELECT_PLACEHOLDER_REGEXP) ? nil : field.field_value
|
||||
}
|
||||
elsif field.field_type == :Ch && field.concrete_field_type == :multi_select && field[:Opt].present?
|
||||
{
|
||||
@@ -178,11 +192,14 @@ module Templates
|
||||
def build_options(values, type = nil)
|
||||
is_skip_single_value = type.in?(%w[radio multiple]) && values.uniq.size == 1
|
||||
|
||||
values.map do |option|
|
||||
values.filter_map do |option|
|
||||
is_option_number = option.is_a?(Symbol) && option.to_s.match?(/\A\d+\z/)
|
||||
|
||||
option = option[1] if option.is_a?(Array) && option.size == 2
|
||||
option = option.encode('utf-8', invalid: :replace, undef: :replace, replace: '') if option.is_a?(String)
|
||||
|
||||
next if type == 'select' && option.to_s.match?(SELECT_PLACEHOLDER_REGEXP)
|
||||
|
||||
{
|
||||
uuid: SecureRandom.uuid,
|
||||
value: is_option_number || is_skip_single_value ? '' : option
|
||||
|
||||
@@ -122,6 +122,15 @@ module Templates
|
||||
|
||||
io = StringIO.new
|
||||
|
||||
pdf.acro_form.each_field do |field|
|
||||
next if field.field_type != :Ch ||
|
||||
field[:Opt].blank? ||
|
||||
%i[combo_box editable_combo_box].exclude?(field.concrete_field_type) ||
|
||||
!field.field_value.to_s.match?(FindAcroFields::SELECT_PLACEHOLDER_REGEXP)
|
||||
|
||||
field[:V] = ''
|
||||
end
|
||||
|
||||
pdf.acro_form.create_appearances(force: true) if pdf.acro_form[:NeedAppearances]
|
||||
pdf.acro_form.flatten
|
||||
|
||||
|
||||
+1
-1
@@ -10,7 +10,7 @@ module WebhookUrls
|
||||
|
||||
event_arel = events.map { |event| Arel::Table.new(:webhook_urls)[:events].matches("%\"#{event}\"%") }.reduce(:or)
|
||||
|
||||
if Docuseal.multitenant?
|
||||
if Docuseal.multitenant? || account_id == 1
|
||||
rel.where(event_arel)
|
||||
else
|
||||
linked_account_rel =
|
||||
|
||||
@@ -26,11 +26,11 @@ RSpec.describe SendFormCompletedWebhookRequestJob do
|
||||
described_class.new.perform('submitter_id' => submitter.id, 'webhook_url_id' => webhook_url.id)
|
||||
|
||||
expect(WebMock).to have_requested(:post, webhook_url.url).with(
|
||||
body: replace_timestamps({
|
||||
body: {
|
||||
'event_type' => 'form.completed',
|
||||
'timestamp' => Time.current,
|
||||
'data' => Submitters::SerializeForWebhook.call(submitter.reload)
|
||||
}.deep_stringify_keys),
|
||||
'timestamp' => /.*/,
|
||||
'data' => JSON.parse(Submitters::SerializeForWebhook.call(submitter.reload).to_json)
|
||||
},
|
||||
headers: {
|
||||
'Content-Type' => 'application/json',
|
||||
'User-Agent' => 'DocuSeal.com Webhook'
|
||||
@@ -43,11 +43,11 @@ RSpec.describe SendFormCompletedWebhookRequestJob do
|
||||
described_class.new.perform('submitter_id' => submitter.id, 'webhook_url_id' => webhook_url.id)
|
||||
|
||||
expect(WebMock).to have_requested(:post, webhook_url.url).with(
|
||||
body: replace_timestamps({
|
||||
body: {
|
||||
'event_type' => 'form.completed',
|
||||
'timestamp' => Time.current,
|
||||
'data' => Submitters::SerializeForWebhook.call(submitter.reload)
|
||||
}.deep_stringify_keys),
|
||||
'timestamp' => /.*/,
|
||||
'data' => JSON.parse(Submitters::SerializeForWebhook.call(submitter.reload).to_json)
|
||||
},
|
||||
headers: {
|
||||
'Content-Type' => 'application/json',
|
||||
'User-Agent' => 'DocuSeal.com Webhook',
|
||||
|
||||
@@ -26,11 +26,11 @@ RSpec.describe SendFormDeclinedWebhookRequestJob do
|
||||
described_class.new.perform('submitter_id' => submitter.id, 'webhook_url_id' => webhook_url.id)
|
||||
|
||||
expect(WebMock).to have_requested(:post, webhook_url.url).with(
|
||||
body: replace_timestamps({
|
||||
body: {
|
||||
'event_type' => 'form.declined',
|
||||
'timestamp' => Time.current,
|
||||
'data' => Submitters::SerializeForWebhook.call(submitter.reload)
|
||||
}.deep_stringify_keys),
|
||||
'timestamp' => /.*/,
|
||||
'data' => JSON.parse(Submitters::SerializeForWebhook.call(submitter.reload).to_json)
|
||||
},
|
||||
headers: {
|
||||
'Content-Type' => 'application/json',
|
||||
'User-Agent' => 'DocuSeal.com Webhook'
|
||||
@@ -43,11 +43,11 @@ RSpec.describe SendFormDeclinedWebhookRequestJob do
|
||||
described_class.new.perform('submitter_id' => submitter.id, 'webhook_url_id' => webhook_url.id)
|
||||
|
||||
expect(WebMock).to have_requested(:post, webhook_url.url).with(
|
||||
body: replace_timestamps({
|
||||
body: {
|
||||
'event_type' => 'form.declined',
|
||||
'timestamp' => Time.current,
|
||||
'data' => Submitters::SerializeForWebhook.call(submitter.reload)
|
||||
}.deep_stringify_keys),
|
||||
'timestamp' => /.*/,
|
||||
'data' => JSON.parse(Submitters::SerializeForWebhook.call(submitter.reload).to_json)
|
||||
},
|
||||
headers: {
|
||||
'Content-Type' => 'application/json',
|
||||
'User-Agent' => 'DocuSeal.com Webhook',
|
||||
|
||||
@@ -26,11 +26,11 @@ RSpec.describe SendFormStartedWebhookRequestJob do
|
||||
described_class.new.perform('submitter_id' => submitter.id, 'webhook_url_id' => webhook_url.id)
|
||||
|
||||
expect(WebMock).to have_requested(:post, webhook_url.url).with(
|
||||
body: replace_timestamps({
|
||||
body: {
|
||||
'event_type' => 'form.started',
|
||||
'timestamp' => Time.current,
|
||||
'data' => Submitters::SerializeForWebhook.call(submitter.reload)
|
||||
}.deep_stringify_keys),
|
||||
'timestamp' => /.*/,
|
||||
'data' => JSON.parse(Submitters::SerializeForWebhook.call(submitter.reload).to_json)
|
||||
},
|
||||
headers: {
|
||||
'Content-Type' => 'application/json',
|
||||
'User-Agent' => 'DocuSeal.com Webhook'
|
||||
@@ -43,11 +43,11 @@ RSpec.describe SendFormStartedWebhookRequestJob do
|
||||
described_class.new.perform('submitter_id' => submitter.id, 'webhook_url_id' => webhook_url.id)
|
||||
|
||||
expect(WebMock).to have_requested(:post, webhook_url.url).with(
|
||||
body: replace_timestamps({
|
||||
body: {
|
||||
'event_type' => 'form.started',
|
||||
'timestamp' => Time.current,
|
||||
'data' => Submitters::SerializeForWebhook.call(submitter.reload)
|
||||
}.deep_stringify_keys),
|
||||
'timestamp' => /.*/,
|
||||
'data' => JSON.parse(Submitters::SerializeForWebhook.call(submitter.reload).to_json)
|
||||
},
|
||||
headers: {
|
||||
'Content-Type' => 'application/json',
|
||||
'User-Agent' => 'DocuSeal.com Webhook',
|
||||
|
||||
@@ -26,11 +26,11 @@ RSpec.describe SendFormViewedWebhookRequestJob do
|
||||
described_class.new.perform('submitter_id' => submitter.id, 'webhook_url_id' => webhook_url.id)
|
||||
|
||||
expect(WebMock).to have_requested(:post, webhook_url.url).with(
|
||||
body: replace_timestamps({
|
||||
body: {
|
||||
'event_type' => 'form.viewed',
|
||||
'timestamp' => Time.current,
|
||||
'data' => Submitters::SerializeForWebhook.call(submitter.reload)
|
||||
}.deep_stringify_keys),
|
||||
'timestamp' => /.*/,
|
||||
'data' => JSON.parse(Submitters::SerializeForWebhook.call(submitter.reload).to_json)
|
||||
},
|
||||
headers: {
|
||||
'Content-Type' => 'application/json',
|
||||
'User-Agent' => 'DocuSeal.com Webhook'
|
||||
@@ -43,11 +43,11 @@ RSpec.describe SendFormViewedWebhookRequestJob do
|
||||
described_class.new.perform('submitter_id' => submitter.id, 'webhook_url_id' => webhook_url.id)
|
||||
|
||||
expect(WebMock).to have_requested(:post, webhook_url.url).with(
|
||||
body: replace_timestamps({
|
||||
body: {
|
||||
'event_type' => 'form.viewed',
|
||||
'timestamp' => Time.current,
|
||||
'data' => Submitters::SerializeForWebhook.call(submitter.reload)
|
||||
}.deep_stringify_keys),
|
||||
'timestamp' => /.*/,
|
||||
'data' => JSON.parse(Submitters::SerializeForWebhook.call(submitter.reload).to_json)
|
||||
},
|
||||
headers: {
|
||||
'Content-Type' => 'application/json',
|
||||
'User-Agent' => 'DocuSeal.com Webhook',
|
||||
|
||||
@@ -23,11 +23,11 @@ RSpec.describe SendSubmissionArchivedWebhookRequestJob do
|
||||
described_class.new.perform('submission_id' => submission.id, 'webhook_url_id' => webhook_url.id)
|
||||
|
||||
expect(WebMock).to have_requested(:post, webhook_url.url).with(
|
||||
body: replace_timestamps({
|
||||
body: {
|
||||
'event_type' => 'submission.archived',
|
||||
'timestamp' => Time.current,
|
||||
'data' => submission.reload.as_json(only: %i[id archived_at])
|
||||
}.deep_stringify_keys),
|
||||
'timestamp' => /.*/,
|
||||
'data' => JSON.parse(submission.reload.as_json(only: %i[id archived_at]).to_json)
|
||||
},
|
||||
headers: {
|
||||
'Content-Type' => 'application/json',
|
||||
'User-Agent' => 'DocuSeal.com Webhook'
|
||||
@@ -40,11 +40,11 @@ RSpec.describe SendSubmissionArchivedWebhookRequestJob do
|
||||
described_class.new.perform('submission_id' => submission.id, 'webhook_url_id' => webhook_url.id)
|
||||
|
||||
expect(WebMock).to have_requested(:post, webhook_url.url).with(
|
||||
body: replace_timestamps({
|
||||
body: {
|
||||
'event_type' => 'submission.archived',
|
||||
'timestamp' => Time.current,
|
||||
'data' => submission.reload.as_json(only: %i[id archived_at])
|
||||
}.deep_stringify_keys),
|
||||
'timestamp' => /.*/,
|
||||
'data' => JSON.parse(submission.reload.as_json(only: %i[id archived_at]).to_json)
|
||||
},
|
||||
headers: {
|
||||
'Content-Type' => 'application/json',
|
||||
'User-Agent' => 'DocuSeal.com Webhook',
|
||||
|
||||
@@ -23,11 +23,11 @@ RSpec.describe SendSubmissionCompletedWebhookRequestJob do
|
||||
described_class.new.perform('submission_id' => submission.id, 'webhook_url_id' => webhook_url.id)
|
||||
|
||||
expect(WebMock).to have_requested(:post, webhook_url.url).with(
|
||||
body: replace_timestamps({
|
||||
body: {
|
||||
'event_type' => 'submission.completed',
|
||||
'timestamp' => Time.current,
|
||||
'data' => Submissions::SerializeForApi.call(submission.reload)
|
||||
}.deep_stringify_keys),
|
||||
'timestamp' => /.*/,
|
||||
'data' => JSON.parse(Submissions::SerializeForApi.call(submission.reload).to_json)
|
||||
},
|
||||
headers: {
|
||||
'Content-Type' => 'application/json',
|
||||
'User-Agent' => 'DocuSeal.com Webhook'
|
||||
@@ -40,11 +40,11 @@ RSpec.describe SendSubmissionCompletedWebhookRequestJob do
|
||||
described_class.new.perform('submission_id' => submission.id, 'webhook_url_id' => webhook_url.id)
|
||||
|
||||
expect(WebMock).to have_requested(:post, webhook_url.url).with(
|
||||
body: replace_timestamps({
|
||||
body: {
|
||||
'event_type' => 'submission.completed',
|
||||
'timestamp' => Time.current,
|
||||
'data' => Submissions::SerializeForApi.call(submission.reload)
|
||||
}.deep_stringify_keys),
|
||||
'timestamp' => /.*/,
|
||||
'data' => JSON.parse(Submissions::SerializeForApi.call(submission.reload).to_json)
|
||||
},
|
||||
headers: {
|
||||
'Content-Type' => 'application/json',
|
||||
'User-Agent' => 'DocuSeal.com Webhook',
|
||||
|
||||
@@ -23,11 +23,11 @@ RSpec.describe SendSubmissionCreatedWebhookRequestJob do
|
||||
described_class.new.perform('submission_id' => submission.id, 'webhook_url_id' => webhook_url.id)
|
||||
|
||||
expect(WebMock).to have_requested(:post, webhook_url.url).with(
|
||||
body: replace_timestamps({
|
||||
body: {
|
||||
'event_type' => 'submission.created',
|
||||
'timestamp' => Time.current,
|
||||
'data' => Submissions::SerializeForApi.call(submission.reload)
|
||||
}.deep_stringify_keys),
|
||||
'timestamp' => /.*/,
|
||||
'data' => JSON.parse(Submissions::SerializeForApi.call(submission.reload).to_json)
|
||||
},
|
||||
headers: {
|
||||
'Content-Type' => 'application/json',
|
||||
'User-Agent' => 'DocuSeal.com Webhook'
|
||||
@@ -40,11 +40,11 @@ RSpec.describe SendSubmissionCreatedWebhookRequestJob do
|
||||
described_class.new.perform('submission_id' => submission.id, 'webhook_url_id' => webhook_url.id)
|
||||
|
||||
expect(WebMock).to have_requested(:post, webhook_url.url).with(
|
||||
body: replace_timestamps({
|
||||
body: {
|
||||
'event_type' => 'submission.created',
|
||||
'timestamp' => Time.current,
|
||||
'data' => Submissions::SerializeForApi.call(submission.reload)
|
||||
}.deep_stringify_keys),
|
||||
'timestamp' => /.*/,
|
||||
'data' => JSON.parse(Submissions::SerializeForApi.call(submission.reload).to_json)
|
||||
},
|
||||
headers: {
|
||||
'Content-Type' => 'application/json',
|
||||
'User-Agent' => 'DocuSeal.com Webhook',
|
||||
|
||||
@@ -22,11 +22,11 @@ RSpec.describe SendTemplateCreatedWebhookRequestJob do
|
||||
described_class.new.perform('template_id' => template.id, 'webhook_url_id' => webhook_url.id)
|
||||
|
||||
expect(WebMock).to have_requested(:post, webhook_url.url).with(
|
||||
body: replace_timestamps({
|
||||
body: {
|
||||
'event_type' => 'template.created',
|
||||
'timestamp' => Time.current,
|
||||
'data' => Templates::SerializeForApi.call(template.reload)
|
||||
}.deep_stringify_keys),
|
||||
'timestamp' => /.*/,
|
||||
'data' => JSON.parse(Templates::SerializeForApi.call(template.reload).to_json)
|
||||
},
|
||||
headers: {
|
||||
'Content-Type' => 'application/json',
|
||||
'User-Agent' => 'DocuSeal.com Webhook'
|
||||
@@ -39,11 +39,11 @@ RSpec.describe SendTemplateCreatedWebhookRequestJob do
|
||||
described_class.new.perform('template_id' => template.id, 'webhook_url_id' => webhook_url.id)
|
||||
|
||||
expect(WebMock).to have_requested(:post, webhook_url.url).with(
|
||||
body: replace_timestamps({
|
||||
body: {
|
||||
'event_type' => 'template.created',
|
||||
'timestamp' => Time.current,
|
||||
'data' => Templates::SerializeForApi.call(template.reload)
|
||||
}.deep_stringify_keys),
|
||||
'timestamp' => /.*/,
|
||||
'data' => JSON.parse(Templates::SerializeForApi.call(template.reload).to_json)
|
||||
},
|
||||
headers: {
|
||||
'Content-Type' => 'application/json',
|
||||
'User-Agent' => 'DocuSeal.com Webhook',
|
||||
|
||||
@@ -22,11 +22,11 @@ RSpec.describe SendTemplateUpdatedWebhookRequestJob do
|
||||
described_class.new.perform('template_id' => template.id, 'webhook_url_id' => webhook_url.id)
|
||||
|
||||
expect(WebMock).to have_requested(:post, webhook_url.url).with(
|
||||
body: replace_timestamps({
|
||||
body: {
|
||||
'event_type' => 'template.updated',
|
||||
'timestamp' => Time.current,
|
||||
'data' => Templates::SerializeForApi.call(template.reload)
|
||||
}.deep_stringify_keys),
|
||||
'timestamp' => /.*/,
|
||||
'data' => JSON.parse(Templates::SerializeForApi.call(template.reload).to_json)
|
||||
},
|
||||
headers: {
|
||||
'Content-Type' => 'application/json',
|
||||
'User-Agent' => 'DocuSeal.com Webhook'
|
||||
@@ -39,11 +39,11 @@ RSpec.describe SendTemplateUpdatedWebhookRequestJob do
|
||||
described_class.new.perform('template_id' => template.id, 'webhook_url_id' => webhook_url.id)
|
||||
|
||||
expect(WebMock).to have_requested(:post, webhook_url.url).with(
|
||||
body: replace_timestamps({
|
||||
body: {
|
||||
'event_type' => 'template.updated',
|
||||
'timestamp' => Time.current,
|
||||
'data' => Templates::SerializeForApi.call(template.reload)
|
||||
}.deep_stringify_keys),
|
||||
'timestamp' => /.*/,
|
||||
'data' => JSON.parse(Templates::SerializeForApi.call(template.reload).to_json)
|
||||
},
|
||||
headers: {
|
||||
'Content-Type' => 'application/json',
|
||||
'User-Agent' => 'DocuSeal.com Webhook',
|
||||
|
||||
@@ -67,19 +67,3 @@ RSpec.configure do |config|
|
||||
Sidekiq::Testing.inline! if example.metadata[:sidekiq] == :inline
|
||||
end
|
||||
end
|
||||
|
||||
def replace_timestamps(data, replace = /.*/)
|
||||
timestamp_fields = %w[created_at updated_at completed_at sent_at opened_at timestamp]
|
||||
|
||||
data.each do |key, value|
|
||||
if timestamp_fields.include?(key) && (value.is_a?(String) || value.is_a?(Time))
|
||||
data[key] = replace
|
||||
elsif value.is_a?(Hash)
|
||||
replace_timestamps(value)
|
||||
elsif value.is_a?(Array)
|
||||
value.each { |item| replace_timestamps(item) if item.is_a?(Hash) }
|
||||
end
|
||||
end
|
||||
|
||||
data
|
||||
end
|
||||
|
||||
@@ -44,7 +44,9 @@ RSpec.describe 'Template' do
|
||||
|
||||
it 'archives a template' do
|
||||
expect do
|
||||
click_button 'Archive'
|
||||
accept_confirm('Are you sure?') do
|
||||
click_button 'Archive'
|
||||
end
|
||||
end.to change { Template.active.count }.by(-1)
|
||||
|
||||
expect(page).to have_content('Template has been archived')
|
||||
|
||||
@@ -10,22 +10,58 @@ RSpec.describe 'Webhook Settings' do
|
||||
sign_in(user)
|
||||
end
|
||||
|
||||
it 'shows webhook settings page' do
|
||||
it 'shows webhook settings page with empty form when there are no webhooks' do
|
||||
visit settings_webhooks_path
|
||||
|
||||
expect(page).to have_content('Webhooks')
|
||||
expect(page).to have_field('Webhook URL')
|
||||
expect(page).to have_content('Webhook')
|
||||
expect(page).to have_content('Webhook URL')
|
||||
expect(page).to have_field('webhook_url[url]', type: 'url')
|
||||
expect(page).to have_button('Save')
|
||||
|
||||
WebhookUrl::EVENTS.each do |event|
|
||||
expect(page).to have_field(event, type: 'checkbox', disabled: true)
|
||||
expect(page).to have_field(event, type: 'checkbox')
|
||||
end
|
||||
end
|
||||
|
||||
it 'shows list of webhooks when there are more than one' do
|
||||
webhook_urls = create_list(:webhook_url, 2, account:)
|
||||
|
||||
visit settings_webhooks_path
|
||||
|
||||
expect(page).to have_content('Webhooks')
|
||||
expect(page).to have_link('New Webhook')
|
||||
|
||||
webhook_urls.each do |webhook_url|
|
||||
expect(page).to have_content(webhook_url.url)
|
||||
|
||||
within("a[href='#{settings_webhook_path(webhook_url)}']") do
|
||||
webhook_url.events.each do |event|
|
||||
expect(page).to have_content(event)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
it 'shows webhook settings page with pre-filled form when there is one webhook' do
|
||||
webhook_url = create(:webhook_url, account:)
|
||||
|
||||
visit settings_webhooks_path
|
||||
|
||||
expect(page).to have_content('Webhook')
|
||||
expect(page).to have_field('webhook_url[url]', type: 'url', with: webhook_url.url)
|
||||
expect(page).to have_button('Save')
|
||||
expect(page).to have_button('Delete')
|
||||
expect(page).to have_link('Add Secret')
|
||||
|
||||
WebhookUrl::EVENTS.each do |event|
|
||||
expect(page).to have_field(event, type: 'checkbox', checked: webhook_url.events.include?(event))
|
||||
end
|
||||
end
|
||||
|
||||
it 'creates the webhook' do
|
||||
visit settings_webhooks_path
|
||||
|
||||
fill_in 'Webhook URL', with: 'https://example.com/webhook'
|
||||
fill_in 'webhook_url[url]', with: 'https://example.com/webhook'
|
||||
|
||||
expect do
|
||||
click_button 'Save'
|
||||
@@ -34,6 +70,8 @@ RSpec.describe 'Webhook Settings' do
|
||||
webhook_url = account.webhook_urls.first
|
||||
|
||||
expect(webhook_url.url).to eq('https://example.com/webhook')
|
||||
expect(page).to have_content('Webhook URL has been saved.')
|
||||
expect(page.current_path).to eq(settings_webhooks_path)
|
||||
end
|
||||
|
||||
it 'updates the webhook' do
|
||||
@@ -41,12 +79,14 @@ RSpec.describe 'Webhook Settings' do
|
||||
|
||||
visit settings_webhooks_path
|
||||
|
||||
fill_in 'Webhook URL', with: 'https://example.org/webhook'
|
||||
fill_in 'webhook_url[url]', with: 'https://example.org/webhook'
|
||||
click_button 'Save'
|
||||
|
||||
webhook_url.reload
|
||||
|
||||
expect(webhook_url.url).to eq('https://example.org/webhook')
|
||||
expect(page).to have_content('Webhook URL has been updated.')
|
||||
expect(page.current_path).to eq(settings_webhooks_path)
|
||||
end
|
||||
|
||||
it 'deletes the webhook' do
|
||||
@@ -54,11 +94,14 @@ RSpec.describe 'Webhook Settings' do
|
||||
|
||||
visit settings_webhooks_path
|
||||
|
||||
fill_in 'Webhook URL', with: ''
|
||||
|
||||
expect do
|
||||
click_button 'Save'
|
||||
accept_confirm('Are you sure?') do
|
||||
click_button 'Delete'
|
||||
end
|
||||
end.to change(WebhookUrl, :count).by(-1)
|
||||
|
||||
expect(page).to have_content('Webhook URL has been deleted.')
|
||||
expect(page.current_path).to eq(settings_webhooks_path)
|
||||
end
|
||||
|
||||
it 'updates the webhook events' do
|
||||
@@ -94,6 +137,9 @@ RSpec.describe 'Webhook Settings' do
|
||||
|
||||
expect(webhook_url.secret).to eq({ 'X-Signature' => 'secret-value' })
|
||||
end
|
||||
|
||||
expect(page).to have_link('Edit Secret')
|
||||
expect(page).to have_content('Webhook Secret has been saved.')
|
||||
end
|
||||
|
||||
it 'removes a secret from the webhook' do
|
||||
@@ -113,5 +159,31 @@ RSpec.describe 'Webhook Settings' do
|
||||
|
||||
expect(webhook_url.secret).to eq({})
|
||||
end
|
||||
|
||||
expect(page).to have_link('Add Secret')
|
||||
expect(page).to have_content('Webhook Secret has been saved.')
|
||||
end
|
||||
|
||||
context 'when testing the webhook' do
|
||||
let!(:webhook_url) { create(:webhook_url, account:) }
|
||||
let!(:template) { create(:template, account:, author: user) }
|
||||
let!(:submission) { create(:submission, template:, created_by_user: user) }
|
||||
let!(:submitter) do
|
||||
create(:submitter, submission:, uuid: template.submitters.first['uuid'], completed_at: Time.current)
|
||||
end
|
||||
|
||||
it 'sends the webhook request' do
|
||||
visit settings_webhooks_path
|
||||
|
||||
expect do
|
||||
click_button 'Test Webhook'
|
||||
end.to change(SendFormCompletedWebhookRequestJob.jobs, :size).by(1)
|
||||
|
||||
args = SendFormCompletedWebhookRequestJob.jobs.last['args'].first
|
||||
|
||||
expect(args['webhook_url_id']).to eq(webhook_url.id)
|
||||
expect(args['submitter_id']).to eq(submitter.id)
|
||||
expect(page).to have_content('Webhook request has been sent.')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user