mirror of
https://github.com/docusealco/docuseal.git
synced 2026-06-23 04:10:11 +00:00
Merge from docusealco/wip
This commit is contained in:
@@ -33,9 +33,15 @@ module Api
|
||||
else
|
||||
http_cache_forever public: true do
|
||||
response.headers['Accept-Ranges'] = 'bytes'
|
||||
response.headers['Content-Length'] = blob.byte_size.to_s
|
||||
|
||||
send_blob_stream blob, disposition: params[:disposition]
|
||||
if request.head?
|
||||
response.headers['Content-Type'] = blob.content_type_for_serving
|
||||
head :ok
|
||||
else
|
||||
send_blob_stream blob, disposition: params[:disposition]
|
||||
end
|
||||
|
||||
response.headers['Content-Length'] = blob.byte_size.to_s
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -57,8 +63,6 @@ module Api
|
||||
return if !require_ttl && !require_auth
|
||||
end
|
||||
|
||||
Rollbar.error('Blob unauthorized') if defined?(Rollbar)
|
||||
|
||||
raise CanCan::AccessDenied
|
||||
end
|
||||
end
|
||||
|
||||
@@ -203,10 +203,15 @@ module Api
|
||||
|
||||
submitter.preferences['send_sms'] = submitter_preferences['send_sms'] if submitter_preferences.key?('send_sms')
|
||||
submitter.preferences['reply_to'] = submitter_preferences['reply_to'] if submitter_preferences.key?('reply_to')
|
||||
|
||||
if submitter_preferences.key?('require_phone_2fa')
|
||||
submitter.preferences['require_phone_2fa'] = submitter_preferences['require_phone_2fa']
|
||||
end
|
||||
|
||||
if submitter_preferences.key?('require_email_2fa')
|
||||
submitter.preferences['require_email_2fa'] = submitter_preferences['require_email_2fa']
|
||||
end
|
||||
|
||||
if submitter_preferences.key?('go_to_last')
|
||||
submitter.preferences['go_to_last'] = submitter_preferences['go_to_last']
|
||||
end
|
||||
|
||||
@@ -41,7 +41,7 @@ class PreviewDocumentPageController < ActionController::API
|
||||
end
|
||||
|
||||
def find_or_create_document_tempfile_path(attachment)
|
||||
file_path = "#{Dir.tmpdir}/#{attachment.uuid}"
|
||||
file_path = "#{Dir.tmpdir}/attachment-#{Digest::SHA1.hexdigest("#{attachment.id}-#{attachment.uuid}")}"
|
||||
|
||||
File.open(file_path, File::RDWR | File::CREAT, 0o644) do |f|
|
||||
f.flock(File::LOCK_EX)
|
||||
|
||||
@@ -1,6 +1,14 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class RevealAccessTokenController < ApplicationController
|
||||
rate_limit to: 4, within: 1.minute, only: %i[create], by: -> { current_user.id }, with: lambda {
|
||||
Rollbar.error('Rate limit api key') if defined?(Rollbar)
|
||||
|
||||
render turbo_stream: turbo_stream.replace(:modal, template: 'reveal_access_token/show',
|
||||
locals: { error_message: I18n.t(:too_many_attempts) }),
|
||||
status: :unprocessable_content
|
||||
}
|
||||
|
||||
def show
|
||||
authorize!(:manage, current_user.access_token)
|
||||
end
|
||||
|
||||
@@ -14,7 +14,7 @@ class SendSubmissionEmailController < ApplicationController
|
||||
template = Template.find_by!(slug: params[:template_slug])
|
||||
|
||||
@submitter =
|
||||
Submitter.completed.where(submission: template.submissions).find_by!(email: params[:email].to_s.downcase)
|
||||
Submitter.completed.where(submission: template.submissions).find_by(email: params[:email].to_s.downcase)
|
||||
elsif params[:submission_slug]
|
||||
submission = Submission.find_by(slug: params[:submission_slug])
|
||||
|
||||
@@ -27,9 +27,11 @@ class SendSubmissionEmailController < ApplicationController
|
||||
@submitter = Submitter.completed.find_by!(slug: params[:submitter_slug])
|
||||
end
|
||||
|
||||
RateLimit.call("send-email-#{@submitter.id}", limit: 2, ttl: 5.minutes)
|
||||
if @submitter
|
||||
RateLimit.call("send-email-#{@submitter.id}", limit: 2, ttl: 5.minutes)
|
||||
|
||||
SubmitterMailer.documents_copy_email(@submitter, sig: true).deliver_later! if can_send?(@submitter)
|
||||
SubmitterMailer.documents_copy_email(@submitter, sig: true).deliver_later! if can_send?(@submitter)
|
||||
end
|
||||
|
||||
respond_to do |f|
|
||||
f.html { render :success }
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class TemplatesController < ApplicationController
|
||||
TEMPLATE_FIELDS = %i[id author_id folder_id external_id name slug
|
||||
schema fields submitters variables_schema preferences
|
||||
shared_link source archived_at created_at updated_at].freeze
|
||||
|
||||
load_and_authorize_resource :template
|
||||
|
||||
def show
|
||||
@@ -33,10 +37,11 @@ class TemplatesController < ApplicationController
|
||||
).call
|
||||
|
||||
@template_data =
|
||||
@template.as_json.merge(
|
||||
@template.as_json(only: TEMPLATE_FIELDS).merge(
|
||||
documents: @template.schema_documents.as_json(
|
||||
only: %i[id uuid],
|
||||
methods: %i[metadata signed_key],
|
||||
include: { preview_images: { methods: %i[url metadata filename] } }
|
||||
include: { preview_images: { only: %i[id], methods: %i[url metadata filename] } }
|
||||
)
|
||||
).to_json
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
class TestingAccountsController < ApplicationController
|
||||
skip_authorization_check only: :destroy
|
||||
|
||||
def show
|
||||
def create
|
||||
authorize!(:manage, current_account)
|
||||
authorize!(:manage, current_user)
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ window.customElements.define('draw-signature', class extends HTMLElement {
|
||||
this.resizeObserver = new ResizeObserver(() => {
|
||||
requestAnimationFrame(() => {
|
||||
if (!this.canvas) return
|
||||
if (!this.canvas.parentNode?.clientWidth) return
|
||||
|
||||
const { width, height } = this.canvas
|
||||
|
||||
@@ -89,7 +90,7 @@ window.customElements.define('draw-signature', class extends HTMLElement {
|
||||
}
|
||||
|
||||
redrawCanvas (oldWidth, oldHeight) {
|
||||
if (this.pad && !this.pad.isEmpty() && oldWidth > 0 && oldHeight > 0) {
|
||||
if (this.pad && !this.pad.isEmpty() && oldWidth > 0 && oldHeight > 0 && this.canvas.width > 0 && this.canvas.height > 0) {
|
||||
const sx = this.canvas.width / oldWidth
|
||||
const sy = this.canvas.height / oldHeight
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ export default targetable(class extends HTMLElement {
|
||||
this.resizeObserver = new ResizeObserver(() => {
|
||||
requestAnimationFrame(() => {
|
||||
if (!this.canvas) return
|
||||
if (!this.canvas.parentNode?.clientWidth) return
|
||||
|
||||
const { width, height } = this.canvas
|
||||
|
||||
@@ -80,7 +81,7 @@ export default targetable(class extends HTMLElement {
|
||||
}
|
||||
|
||||
redrawCanvas (oldWidth, oldHeight) {
|
||||
if (this.pad && !this.pad.isEmpty() && oldWidth > 0 && oldHeight > 0) {
|
||||
if (this.pad && !this.pad.isEmpty() && oldWidth > 0 && oldHeight > 0 && this.canvas.width > 0 && this.canvas.height > 0) {
|
||||
const sx = this.canvas.width / oldWidth
|
||||
const sy = this.canvas.height / oldHeight
|
||||
|
||||
|
||||
@@ -42,10 +42,25 @@ export default {
|
||||
const areas = {}
|
||||
|
||||
this.field.areas?.forEach((area) => {
|
||||
areas[area.attachment_uuid + area.page] ||= area
|
||||
areas[area.attachment_uuid] ||= []
|
||||
areas[area.attachment_uuid].push(area)
|
||||
})
|
||||
|
||||
return Object.values(areas).slice(0, 6)
|
||||
const sortedAreas = Object.values(areas).reduce((acc, group) => {
|
||||
const seen = {}
|
||||
const sortedGroup = [...group].sort((a, b) => a.page - b.page)
|
||||
|
||||
sortedGroup.forEach((area) => {
|
||||
if (!seen[area.page]) {
|
||||
seen[area.page] = true
|
||||
acc.push(area)
|
||||
}
|
||||
})
|
||||
|
||||
return acc
|
||||
}, [])
|
||||
|
||||
return sortedAreas.slice(0, 6)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -544,6 +544,7 @@ export default {
|
||||
this.resizeObserver = new ResizeObserver(() => {
|
||||
requestAnimationFrame(() => {
|
||||
if (!this.$refs.canvas) return
|
||||
if (!this.$refs.canvas.parentNode?.clientWidth) return
|
||||
|
||||
const { width, height } = this.$refs.canvas
|
||||
|
||||
@@ -586,7 +587,7 @@ export default {
|
||||
redrawCanvas (oldWidth, oldHeight) {
|
||||
const canvas = this.$refs.canvas
|
||||
|
||||
if (this.pad && !this.isTextSignature && !this.pad.isEmpty() && oldWidth > 0 && oldHeight > 0) {
|
||||
if (this.pad && !this.isTextSignature && !this.pad.isEmpty() && oldWidth > 0 && oldHeight > 0 && canvas.width > 0 && canvas.height > 0) {
|
||||
const sx = canvas.width / oldWidth
|
||||
const sy = canvas.height / oldHeight
|
||||
|
||||
|
||||
@@ -1012,6 +1012,13 @@ export default {
|
||||
fieldsDragFieldRef: () => ref(),
|
||||
customDragFieldRef: () => ref(),
|
||||
selectedAreasRef: () => ref([]),
|
||||
attachmentUuidsIndex () {
|
||||
return this.template.schema.reduce((acc, e, index) => {
|
||||
acc[e.attachment_uuid] = index
|
||||
|
||||
return acc
|
||||
}, {})
|
||||
},
|
||||
language () {
|
||||
return this.locale.split('-')[0].toLowerCase()
|
||||
},
|
||||
@@ -1663,33 +1670,26 @@ export default {
|
||||
this.save()
|
||||
}
|
||||
},
|
||||
compareAreas (a, b) {
|
||||
const aAttIdx = this.attachmentUuidsIndex[a.attachment_uuid]
|
||||
const bAttIdx = this.attachmentUuidsIndex[b.attachment_uuid]
|
||||
|
||||
if (aAttIdx !== bAttIdx) return aAttIdx - bAttIdx
|
||||
if (a.page !== b.page) return a.page - b.page
|
||||
|
||||
const aY = a.y + a.h
|
||||
const bY = b.y + b.h
|
||||
|
||||
if (Math.abs(aY - bY) < 0.01) return a.x - b.x
|
||||
if (a.h < b.h ? a.y >= b.y && aY <= bY : b.y >= a.y && bY <= aY) return a.x - b.x
|
||||
|
||||
return aY - bY
|
||||
},
|
||||
findFieldInsertIndex (field) {
|
||||
if (!field.areas?.length) return -1
|
||||
|
||||
const area = field.areas[0]
|
||||
|
||||
const attachmentUuidsIndex = this.template.schema.reduce((acc, e, index) => {
|
||||
acc[e.attachment_uuid] = index
|
||||
|
||||
return acc
|
||||
}, {})
|
||||
|
||||
const compareAreas = (a, b) => {
|
||||
const aAttIdx = attachmentUuidsIndex[a.attachment_uuid]
|
||||
const bAttIdx = attachmentUuidsIndex[b.attachment_uuid]
|
||||
|
||||
if (aAttIdx !== bAttIdx) return aAttIdx - bAttIdx
|
||||
if (a.page !== b.page) return a.page - b.page
|
||||
|
||||
const aY = a.y + a.h
|
||||
const bY = b.y + b.h
|
||||
|
||||
if (Math.abs(aY - bY) < 0.01) return a.x - b.x
|
||||
if (a.h < b.h ? a.y >= b.y && aY <= bY : b.y >= a.y && bY <= aY) return a.x - b.x
|
||||
|
||||
return aY - bY
|
||||
}
|
||||
|
||||
let closestBeforeIndex = -1
|
||||
let closestBeforeArea = null
|
||||
let closestAfterIndex = -1
|
||||
@@ -1698,15 +1698,15 @@ export default {
|
||||
this.template.fields.forEach((f, index) => {
|
||||
if (f.submitter_uuid === field.submitter_uuid) {
|
||||
(f.areas || []).forEach((a) => {
|
||||
const cmp = compareAreas(a, area)
|
||||
const cmp = this.compareAreas(a, area)
|
||||
|
||||
if (cmp < 0) {
|
||||
if (!closestBeforeArea || (compareAreas(a, closestBeforeArea) > 0 && closestBeforeIndex < index)) {
|
||||
if (!closestBeforeArea || (this.compareAreas(a, closestBeforeArea) > 0 && closestBeforeIndex < index)) {
|
||||
closestBeforeIndex = index
|
||||
closestBeforeArea = a
|
||||
}
|
||||
} else {
|
||||
if (!closestAfterArea || (compareAreas(a, closestAfterArea) < 0 && closestAfterIndex > index)) {
|
||||
if (!closestAfterArea || (this.compareAreas(a, closestAfterArea) < 0 && closestAfterIndex > index)) {
|
||||
closestAfterIndex = index
|
||||
closestAfterArea = a
|
||||
}
|
||||
@@ -1729,6 +1729,17 @@ export default {
|
||||
this.template.fields.push(field)
|
||||
}
|
||||
},
|
||||
insertArea (field, area) {
|
||||
field.areas ||= []
|
||||
|
||||
const insertIndex = field.areas.findIndex((a) => this.compareAreas(a, area) > 0)
|
||||
|
||||
if (insertIndex === -1) {
|
||||
field.areas.push(area)
|
||||
} else {
|
||||
field.areas.splice(insertIndex, 0, area)
|
||||
}
|
||||
},
|
||||
insertDetectedField (field) {
|
||||
if (!this.withDetectExistingFields || !field.name) {
|
||||
this.insertField(field)
|
||||
@@ -1744,7 +1755,7 @@ export default {
|
||||
|
||||
if (existingField) {
|
||||
existingField.areas = existingField.areas || []
|
||||
existingField.areas.push(...(field.areas || []))
|
||||
field.areas.forEach((area) => this.insertArea(existingField, area))
|
||||
} else {
|
||||
const customField = this.detectCustomFieldsIndex[indexKey] || this.detectCustomFieldsIndex[nameKey]
|
||||
|
||||
@@ -2249,7 +2260,7 @@ export default {
|
||||
|
||||
fieldUuidIndex[field.uuid] = newField
|
||||
|
||||
newField.areas.push(newArea)
|
||||
this.insertArea(newField, newArea)
|
||||
newAreas.push(newArea)
|
||||
|
||||
if (['radio', 'multiple'].includes(field.type) && field.options?.length) {
|
||||
@@ -2362,17 +2373,7 @@ export default {
|
||||
area.y -= area.h / 2
|
||||
}
|
||||
|
||||
this.drawField.areas ||= []
|
||||
|
||||
const insertBeforeAreaIndex = this.drawField.areas.findIndex((a) => {
|
||||
return a.attachment_uuid === area.attachment_uuid && a.page > area.page
|
||||
})
|
||||
|
||||
if (insertBeforeAreaIndex !== -1) {
|
||||
this.drawField.areas.splice(insertBeforeAreaIndex, 0, area)
|
||||
} else {
|
||||
this.drawField.areas.push(area)
|
||||
}
|
||||
this.insertArea(this.drawField, area)
|
||||
|
||||
if (this.template.fields.indexOf(this.drawField) === -1) {
|
||||
this.insertField(this.drawField)
|
||||
@@ -2513,9 +2514,7 @@ export default {
|
||||
delete field.height
|
||||
}
|
||||
|
||||
field.areas ||= []
|
||||
|
||||
field.areas.push(fieldArea)
|
||||
this.insertArea(field, fieldArea)
|
||||
|
||||
if (this.selectedAreasRef.value.length < 2) {
|
||||
this.selectedAreasRef.value = [fieldArea]
|
||||
@@ -2585,7 +2584,7 @@ export default {
|
||||
}
|
||||
}
|
||||
|
||||
field.areas.push(fieldArea)
|
||||
this.insertArea(field, fieldArea)
|
||||
})
|
||||
} else {
|
||||
const fieldArea = {
|
||||
|
||||
@@ -786,17 +786,27 @@ export default {
|
||||
},
|
||||
copyToAllPages (field) {
|
||||
const areaString = JSON.stringify(field.areas[0])
|
||||
const newAreas = []
|
||||
const existingAreasIndex = field.areas.reduce((acc, area) => {
|
||||
acc[`${area.attachment_uuid}-${area.page}`] = area
|
||||
|
||||
return acc
|
||||
}, {})
|
||||
|
||||
this.template.schema.forEach((item) => {
|
||||
const attachment = this.template.documents.find((d) => d.uuid === item.attachment_uuid)
|
||||
|
||||
this.template.documents.forEach((attachment) => {
|
||||
const numberOfPages = attachment.metadata?.pdf?.number_of_pages || attachment.preview_images.length
|
||||
|
||||
for (let page = 0; page <= numberOfPages - 1; page++) {
|
||||
if (!field.areas.find((area) => area.attachment_uuid === attachment.uuid && area.page === page)) {
|
||||
field.areas.push({ ...JSON.parse(areaString), attachment_uuid: attachment.uuid, page })
|
||||
}
|
||||
const existing = existingAreasIndex[`${attachment.uuid}-${page}`]
|
||||
|
||||
newAreas.push(existing || { ...JSON.parse(areaString), attachment_uuid: attachment.uuid, page })
|
||||
}
|
||||
})
|
||||
|
||||
field.areas = newAreas
|
||||
|
||||
this.$emit('scroll-to', this.field.areas[this.field.areas.length - 1])
|
||||
|
||||
this.$emit('save')
|
||||
|
||||
@@ -143,8 +143,8 @@
|
||||
>
|
||||
<input
|
||||
type="hidden"
|
||||
name="state"
|
||||
:value="oauthState"
|
||||
name="oauth_data"
|
||||
:value="oauthData"
|
||||
autocomplete="off"
|
||||
>
|
||||
<input
|
||||
@@ -334,7 +334,7 @@ export default {
|
||||
authenticityToken () {
|
||||
return document.querySelector('meta[name="csrf-token"]')?.content
|
||||
},
|
||||
oauthState () {
|
||||
oauthData () {
|
||||
const params = new URLSearchParams('')
|
||||
|
||||
params.set('redir', document.location.href)
|
||||
|
||||
@@ -228,7 +228,7 @@ export default {
|
||||
'https://www.googleapis.com/auth/userinfo.email',
|
||||
'https://www.googleapis.com/auth/drive.file'
|
||||
].join(' '),
|
||||
state: new URLSearchParams({
|
||||
oauth_data: new URLSearchParams({
|
||||
redir: `/templates/${this.templateId}/edit?google_drive_open=1`
|
||||
}).toString()
|
||||
}
|
||||
|
||||
@@ -101,13 +101,18 @@ class Submission < ApplicationRecord
|
||||
where(Submitter.where(Submitter.arel_table[:submission_id].eq(Submission.arel_table[:id])
|
||||
.and(Submitter.arel_table[:declined_at].not_eq(nil))).select(1).arel.exists)
|
||||
}
|
||||
scope :expired, -> { pending.where(expire_at: ..Time.current) }
|
||||
scope :expired, lambda {
|
||||
where(expire_at: ..Time.current)
|
||||
.where(Submitter.where(Submitter.arel_table[:submission_id].eq(Submission.arel_table[:id])
|
||||
.and(Submitter.arel_table[:completed_at].eq(nil))).select(1).arel.exists)
|
||||
}
|
||||
|
||||
enum :source, {
|
||||
invite: 'invite',
|
||||
bulk: 'bulk',
|
||||
api: 'api',
|
||||
embed: 'embed',
|
||||
mcp: 'mcp',
|
||||
link: 'link'
|
||||
}, scope: false, prefix: true
|
||||
|
||||
|
||||
@@ -18,24 +18,7 @@
|
||||
<%= f.button button_title(title: t(:sign_in), disabled_with: t(:signing_in)), class: 'base-button' %>
|
||||
</div>
|
||||
<% end %>
|
||||
<% if devise_mapping.omniauthable? %>
|
||||
<div class="space-y-4">
|
||||
<% if User.omniauth_providers.include?(:google_oauth2) %>
|
||||
<%= form_for '', url: omniauth_authorize_path(resource_name, :google_oauth2), data: { turbo: false }, method: :post do |f| %>
|
||||
<set-timezone data-input-id="state" data-params="true"></set-timezone>
|
||||
<%= hidden_field_tag :state, { redir: params[:redir].to_s }.compact_blank.to_query %>
|
||||
<%= f.button button_title(title: t('sign_in_with_google'), icon: svg_icon('brand_google', class: 'w-6 h-6')), class: 'white-button w-full mt-4' %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
<% if User.omniauth_providers.include?(:microsoft_office365) %>
|
||||
<%= form_for '', url: omniauth_authorize_path(resource_name, :microsoft_office365), data: { turbo: false }, method: :post do |f| %>
|
||||
<set-timezone data-input-id="state_microsoft" data-params="true"></set-timezone>
|
||||
<%= hidden_field_tag :state, { redir: params[:redir].to_s }.compact_blank.to_query, id: 'state_microsoft' %>
|
||||
<%= f.button button_title(title: t('sign_in_with_microsoft'), icon: svg_icon('brand_microsoft', class: 'w-6 h-6')), class: 'white-button w-full' %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
<%= render 'omniauthable' %>
|
||||
<%= render 'extra_links' %>
|
||||
<%= render 'devise/shared/links' %>
|
||||
</div>
|
||||
|
||||
@@ -20,9 +20,10 @@
|
||||
</head>
|
||||
<body>
|
||||
<% if params[:modal].present? %>
|
||||
<% url_params = Rails.application.routes.recognize_path(params[:modal], method: :get) %>
|
||||
<% modal_uri = Addressable::URI.parse(params[:modal]) %>
|
||||
<% url_params = Rails.application.routes.recognize_path(modal_uri.path, method: :get) %>
|
||||
<% if url_params[:action] == 'new' %>
|
||||
<open-modal src="<%= url_for(url_params) %>"></open-modal>
|
||||
<open-modal src="<%= url_for(**url_params, params: modal_uri.query_values) %>"></open-modal>
|
||||
<% end %>
|
||||
<% end %>
|
||||
<turbo-frame id="modal"></turbo-frame>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<a target="_blank" href="<%= Docuseal::GITHUB_URL %>" rel="noopener noreferrer nofollow" class="relative flex items-center rounded-full px-2 py-0.5 text-xs leading-4 mt-1 text-base-content border border-base-300 tooltip tooltip-bottom" data-tip="Give a star on GitHub">
|
||||
<span class="flex items-center justify-between space-x-0.5 font-medium">
|
||||
<%= svg_icon('start', class: 'h-3 w-3') %>
|
||||
<span>11k</span>
|
||||
<span>12k</span>
|
||||
</span>
|
||||
</a>
|
||||
|
||||
@@ -59,7 +59,7 @@
|
||||
</li>
|
||||
<% end %>
|
||||
<% if (can?(:manage, EncryptedConfig) && current_user == true_user) || (current_user != true_user && current_account.testing?) %>
|
||||
<%= form_for '', url: testing_account_path, method: current_account.testing? ? :delete : :get, html: { class: 'w-full py-1' } do |f| %>
|
||||
<%= form_for '', url: testing_account_path, method: current_account.testing? ? :delete : :post, html: { class: 'w-full py-1' } do |f| %>
|
||||
<label class="flex items-center pl-6 pr-4 py-2 border-y border-base-300 -ml-2 -mr-2" for="testing_toggle">
|
||||
<submit-form data-on="change" class="flex">
|
||||
<%= f.check_box :testing_toggle, class: 'toggle', checked: current_account.testing?, style: 'height: 0.885rem; width: 1.35rem; --handleoffset: 0.395rem; margin-left: -2px; margin-right: 8px' %>
|
||||
|
||||
@@ -97,7 +97,7 @@
|
||||
<% end %>
|
||||
<%= render 'shared/settings_nav_extra2' %>
|
||||
<% if (can?(:manage, EncryptedConfig) && current_user == true_user) || (current_user != true_user && current_account.testing?) %>
|
||||
<%= form_for '', url: testing_account_path, method: current_account.testing? ? :delete : :get, html: { class: 'w-full' } do |f| %>
|
||||
<%= form_for '', url: testing_account_path, method: current_account.testing? ? :delete : :post, html: { class: 'w-full' } do |f| %>
|
||||
<li>
|
||||
<label class="flex items-center text-base hover:bg-base-300 w-full justify-between" for="testing_toggle">
|
||||
<span class="mr-2 w-full">
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<% if can?(:manage, EncryptedConfig) || (current_user != true_user && current_account.testing?) %>
|
||||
<%= form_for '', url: testing_account_path, method: current_account.testing? ? :delete : :get, html: { class: 'flex' } do |f| %>
|
||||
<%= form_for '', url: testing_account_path, method: current_account.testing? ? :delete : :post, html: { class: 'flex' } do |f| %>
|
||||
<label class="flex items-center justify-between" for="testing_toggle">
|
||||
<span class="mr-2 text-lg">
|
||||
<%= t('test_mode') %>
|
||||
|
||||
@@ -5,9 +5,9 @@
|
||||
<% font = field.dig('preferences', 'font') %>
|
||||
<% font_type = field.dig('preferences', 'font_type') %>
|
||||
<% font_size_px = (field.dig('preferences', 'font_size').presence || Submissions::GenerateResultAttachments::FONT_SIZE).to_i * local_assigns.fetch(:font_scale) { 1000.0 / PdfUtils::US_LETTER_W } %>
|
||||
<field-value dir="auto" aria-hidden="true" class="flex absolute <%= 'font-courier' if font == 'Courier' %> <%= 'font-times' if font == 'Times' %> <%= 'font-bold' if font_type == 'bold' || font_type == 'bold_italic' %> <%= 'italic' if font_type == 'italic' || font_type == 'bold_italic' %> <%= align == 'right' ? 'text-right' : (align == 'center' ? 'text-center' : '') %>" style="<%= "color: #{color}; " if color.present? %><%= "background: #{bg_color}; " if bg_color.present? %>width: <%= area['w'] * 100 %>%; height: <%= area['h'] * 100 %>%; left: <%= area['x'] * 100 %>%; top: <%= area['y'] * 100 %>%; font-size: <%= fs = "clamp(1pt, #{font_size_px / 10}vw, #{font_size_px}px)" %>; line-height: calc(<%= fs %> * 1.3); font-size: <%= fs = "#{font_size_px / 10}cqmin" %>; line-height: calc(<%= fs %> * 1.3)">
|
||||
<field-value dir="auto" aria-hidden="true" class="flex absolute <%= 'font-courier' if font == 'Courier' %> <%= 'font-times' if font == 'Times' %> <%= 'font-bold' if font_type == 'bold' || font_type == 'bold_italic' %> <%= 'italic' if font_type == 'italic' || font_type == 'bold_italic' %> <%= align == 'right' ? 'text-right' : (align == 'center' ? 'text-center' : '') %>" style="<%= "color: #{color}; " if color.present? && color.match?(Templates::COLOR_REGEXP) %><%= "background: #{bg_color}; " if bg_color.present? && bg_color.match?(Templates::COLOR_REGEXP) %>width: <%= area['w'].to_f * 100 %>%; height: <%= area['h'].to_f * 100 %>%; left: <%= area['x'].to_f * 100 %>%; top: <%= area['y'].to_f * 100 %>%; font-size: <%= fs = "clamp(1pt, #{font_size_px / 10}vw, #{font_size_px}px)" %>; line-height: calc(<%= fs %> * 1.3); font-size: <%= fs = "#{font_size_px / 10}cqmin" %>; line-height: calc(<%= fs %> * 1.3)">
|
||||
<% if field['type'] == 'signature' %>
|
||||
<% is_narrow = area['h'].positive? && ((area['w'] * local_assigns[:page_width]).to_f / (area['h'] * local_assigns[:page_height])) > 4.5 %>
|
||||
<% is_narrow = area['h'].to_f.positive? && ((area['w'].to_f * local_assigns[:page_width]) / (area['h'].to_f * local_assigns[:page_height])) > 4.5 %>
|
||||
<div class="flex justify-between w-full h-full gap-1 <%= is_narrow && (local_assigns[:with_signature_id] || field.dig('preferences', 'reason_field_uuid').present?) ? 'flex-row' : 'flex-col' %>">
|
||||
<div class="flex overflow-hidden <%= is_narrow && (local_assigns[:with_signature_id] || field.dig('preferences', 'reason_field_uuid').present?) ? 'w-1/2' : 'flex-grow' %>" style="min-height: 50%">
|
||||
<img class="object-contain mx-auto" src="<%= attachments_index[value].url %>" alt="<%= field['name'].presence || field['title'].presence || field['type'] %>">
|
||||
@@ -62,9 +62,9 @@
|
||||
<% end %>
|
||||
<% end %>
|
||||
<% elsif field['type'] == 'cells' && area['cell_w'].to_f > 0.0 %>
|
||||
<% cell_width = area['cell_w'] / area['w'] * 100 %>
|
||||
<% cell_width = area['cell_w'].to_f / area['w'] * 100 %>
|
||||
<div class="w-full flex <%= valign == 'top' ? 'items-start' : (valign == 'bottom' ? 'items-end' : 'items-center') %> <%= 'justify-end' if align == 'right' %>">
|
||||
<% (0..(area['w'] / area['cell_w']).ceil).each do |index| %>
|
||||
<% (0..(area['w'].to_f / area['cell_w']).ceil).each do |index| %>
|
||||
<% if value[index] %>
|
||||
<div class="text-center flex-none" style="width: <%= cell_width %>%;"><%= value[index] %></div>
|
||||
<% end %>
|
||||
@@ -83,14 +83,14 @@
|
||||
</div>
|
||||
<% elsif field['type'] == 'strikethrough' %>
|
||||
<div class="w-full h-full flex items-center justify-center">
|
||||
<% if (((1000.0 / local_assigns[:page_width]) * local_assigns[:page_height]) * area['h']) < 40 %>
|
||||
<% if (((1000.0 / local_assigns[:page_width]) * local_assigns[:page_height]) * area['h'].to_f) < 40 %>
|
||||
<svg width="100%" height="100%">
|
||||
<line x1="0" y1="50%" x2="100%" y2="50%" stroke="<%= field.dig('preferences', 'color').presence || 'red' %>" style="stroke-width: clamp(0px, 0.5vw, 6px); stroke-width: 0.6cqmin"></line>
|
||||
<line x1="0" y1="50%" x2="100%" y2="50%" stroke="<%= color.present? && color.match?(Templates::COLOR_REGEXP) ? color : 'red' %>" style="stroke-width: clamp(0px, 0.5vw, 6px); stroke-width: 0.6cqmin"></line>
|
||||
</svg>
|
||||
<% else %>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" style="overflow: visible; width: calc(100% - 6px); height: calc(100% - 6px); width: calc(100% - 0.6cqmin); height: calc(100% - 0.6cqmin)">
|
||||
<line x1="0" y1="0" x2="100%" y2="100%" stroke="<%= field.dig('preferences', 'color').presence || 'red' %>" style="stroke-width: clamp(0px, 0.5vw, 6px); stroke-width: 0.6cqmin"></line>
|
||||
<line x1="100%" y1="0" x2="0" y2="100%" stroke="<%= field.dig('preferences', 'color').presence || 'red' %>" style="stroke-width: clamp(0px, 0.5vw, 6px); stroke-width: 0.6cqmin"></line>
|
||||
<line x1="0" y1="0" x2="100%" y2="100%" stroke="<%= color.present? && color.match?(Templates::COLOR_REGEXP) ? color : 'red' %>" style="stroke-width: clamp(0px, 0.5vw, 6px); stroke-width: 0.6cqmin"></line>
|
||||
<line x1="100%" y1="0" x2="0" y2="100%" stroke="<%= color.present? && color.match?(Templates::COLOR_REGEXP) ? color : 'red' %>" style="stroke-width: clamp(0px, 0.5vw, 6px); stroke-width: 0.6cqmin"></line>
|
||||
</svg>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
@@ -131,7 +131,7 @@
|
||||
<% submitters_order_index ||= (@submission.template_submitters || @submission.template.submitters).each_with_index.to_h { |s, i| [s['uuid'], i] } %>
|
||||
<% submitter_index = submitters_order_index[submitter.uuid] %>
|
||||
<% bg_class = bg_classes[submitter_index % bg_classes.size] %>
|
||||
<div class="absolute overflow-visible" style="width: <%= area['w'] * 100 %>%; height: <%= area['h'] * 100 %>%; left: <%= area['x'] * 100 %>%; top: <%= area['y'] * 100 %>%;">
|
||||
<div class="absolute overflow-visible" style="width: <%= area['w'].to_f * 100 %>%; height: <%= area['h'].to_f * 100 %>%; left: <%= area['x'].to_f * 100 %>%; top: <%= area['y'].to_f * 100 %>%;">
|
||||
<div class="flex h-full w-full bg-opacity-80 justify-center items-center <%= bg_class %>">
|
||||
<%= svg_icon(SubmissionsController::FIELD_ICONS.fetch(field['type'], 'text_size'), class: 'max-h-10 w-full h-full stroke-2 opacity-50') %>
|
||||
</div>
|
||||
|
||||
@@ -25,9 +25,11 @@
|
||||
<div class="flex gap-2 mt-3">
|
||||
<div class="relative flex-grow">
|
||||
<input id="embedding_url" type="text" value="<%= start_form_url(slug: @template.slug, host: form_link_host) %>" class="base-input w-full pr-10" autocomplete="off" readonly>
|
||||
<a href="<%= template_share_link_qr_path(@template) %>" target="_blank" rel="noopener" class="absolute top-1/2 -translate-y-1/2 right-2 flex items-center justify-center tooltip text-base-content/70 hover:text-base-content bg-white rounded px-1 py-0.5" data-tip="<%= t('qr_code') %>" aria-label="<%= t('qr_code') %>">
|
||||
<%= svg_icon('qrcode', class: 'w-6 h-6') %>
|
||||
</a>
|
||||
<% if @template.variables_schema.blank? && !@template.preferences&.dig('require_email_2fa') && !@template.preferences&.dig('require_phone_2fa') %>
|
||||
<a href="<%= template_share_link_qr_path(@template) %>" target="_blank" rel="noopener" class="absolute top-1/2 -translate-y-1/2 right-2 flex items-center justify-center tooltip text-base-content/70 hover:text-base-content bg-white rounded px-1 py-0.5" data-tip="<%= t('qr_code') %>" aria-label="<%= t('qr_code') %>">
|
||||
<%= svg_icon('qrcode', class: 'w-6 h-6') %>
|
||||
</a>
|
||||
<% end %>
|
||||
</div>
|
||||
<check-on-click data-element-id="template_shared_link">
|
||||
<%= render 'shared/clipboard_copy', icon: 'copy', text: start_form_url(slug: @template.slug, host: form_link_host), class: 'base-button', icon_class: 'w-6 h-6 text-white', copy_title: t('copy'), copied_title: t('copied') %>
|
||||
|
||||
@@ -942,6 +942,7 @@ en: &en
|
||||
embed: Embedding
|
||||
invite: Invite
|
||||
link: Link
|
||||
mcp: MCP
|
||||
submission_event_names:
|
||||
send_email_to_html: '<b>Email sent</b> to %{submitter_name}'
|
||||
bounce_email_html: '<b>Email bounced</b> %{submitter_name}'
|
||||
@@ -1982,6 +1983,7 @@ es: &es
|
||||
embed: Integración
|
||||
invite: Invitación
|
||||
link: Enlace
|
||||
mcp: MCP
|
||||
submission_event_names:
|
||||
send_email_to_html: '<b>Correo electrónico enviado</b> a %{submitter_name}'
|
||||
bounce_email_html: '<b>Correo electrónico rebotado</b> %{submitter_name}'
|
||||
@@ -3023,6 +3025,7 @@ it: &it
|
||||
embed: Incorporamento
|
||||
invite: Invito
|
||||
link: Link
|
||||
mcp: MCP
|
||||
submission_event_names:
|
||||
send_email_to_html: '<b>Email inviata</b> a %{submitter_name}'
|
||||
bounce_email_html: '<b>Email respinta</b> %{submitter_name}'
|
||||
@@ -4060,6 +4063,7 @@ fr: &fr
|
||||
embed: Embedding
|
||||
invite: Invitation
|
||||
link: Lien
|
||||
mcp: MCP
|
||||
submission_event_names:
|
||||
send_email_to_html: "<b>E‑mail envoyé</b> à %{submitter_name}"
|
||||
bounce_email_html: "<b>E‑mail rejeté</b> %{submitter_name}"
|
||||
@@ -5100,6 +5104,7 @@ pt: &pt
|
||||
embed: Incorporação
|
||||
invite: Convite
|
||||
link: Link
|
||||
mcp: MCP
|
||||
submission_event_names:
|
||||
send_email_to_html: '<b>Email enviado</b> para %{submitter_name}'
|
||||
bounce_email_html: '<b>Email não entregue</b> %{submitter_name}'
|
||||
@@ -6140,6 +6145,7 @@ de: &de
|
||||
embed: Einbettung
|
||||
invite: Einladung
|
||||
link: Link
|
||||
mcp: MCP
|
||||
submission_event_names:
|
||||
send_email_to_html: '<b>E-Mail gesendet</b> an %{submitter_name}'
|
||||
bounce_email_html: '<b>E-Mail unzustellbar</b> %{submitter_name}'
|
||||
@@ -7581,6 +7587,7 @@ nl: &nl
|
||||
embed: Insluiten
|
||||
invite: Uitnodiging
|
||||
link: Link
|
||||
mcp: MCP
|
||||
submission_event_names:
|
||||
send_email_to_html: "<b>E-mail verzonden</b> naar %{submitter_name}"
|
||||
bounce_email_html: "<b>E-mail gebounced</b> %{submitter_name}"
|
||||
|
||||
+1
-1
@@ -77,7 +77,7 @@ Rails.application.routes.draw do
|
||||
resources :console_redirect, only: %i[index]
|
||||
resources :upgrade, only: %i[index], controller: 'console_redirect'
|
||||
resources :manage, only: %i[index], controller: 'console_redirect'
|
||||
resource :testing_account, only: %i[show destroy]
|
||||
resource :testing_account, only: %i[create destroy]
|
||||
resources :testing_api_settings, only: %i[index]
|
||||
resources :submitters_autocomplete, only: %i[index]
|
||||
resources :submitters_resubmit, only: %i[update]
|
||||
|
||||
@@ -27,7 +27,7 @@ module Mcp
|
||||
readOnlyHint: false,
|
||||
destructiveHint: false,
|
||||
idempotentHint: false,
|
||||
openWorldHint: false
|
||||
openWorldHint: true
|
||||
}
|
||||
}.freeze
|
||||
|
||||
@@ -43,6 +43,7 @@ module Mcp
|
||||
account:,
|
||||
author: current_user,
|
||||
folder: account.default_template_folder,
|
||||
source: :mcp,
|
||||
name: arguments['name'].to_s.presence || 'New Template',
|
||||
fields: [],
|
||||
schema: []
|
||||
|
||||
@@ -61,7 +61,7 @@ module Mcp
|
||||
},
|
||||
annotations: {
|
||||
readOnlyHint: false,
|
||||
destructiveHint: false,
|
||||
destructiveHint: true,
|
||||
idempotentHint: false,
|
||||
openWorldHint: true
|
||||
}
|
||||
@@ -96,7 +96,7 @@ module Mcp
|
||||
submissions = Submissions.create_from_submitters(
|
||||
template:,
|
||||
user: current_user,
|
||||
source: :api,
|
||||
source: :mcp,
|
||||
submitters_order: 'random',
|
||||
submissions_attrs: { submitters: submitters },
|
||||
params: { 'send_email' => true, 'submitters' => submitters }
|
||||
|
||||
@@ -54,7 +54,7 @@ Puma::Plugin.create do
|
||||
Dir.chdir(ENV.fetch('WORKDIR', nil)) unless ENV['WORKDIR'].to_s.empty?
|
||||
|
||||
exec('redis-server', '--requirepass', Digest::SHA1.hexdigest("redis#{ENV.fetch('SECRET_KEY_BASE', '')}"),
|
||||
out: '/dev/null')
|
||||
'--loglevel', 'warning')
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -56,7 +56,8 @@ module SearchEntries
|
||||
end
|
||||
|
||||
[sql, number, number.length > 1 ? number.delete_prefix('0') : number, keyword]
|
||||
elsif keyword.match?(/[^\p{L}\d&@.-]/) || keyword.match?(/\A['"].*['"]\z/) || keyword.match?(/[.-]{2,}/)
|
||||
elsif keyword.start_with?('@') || keyword.match?(/[^\p{L}\d&@.-]/) ||
|
||||
keyword.match?(/\A['"].*['"]\z/) || keyword.match?(/[.-]{2,}/)
|
||||
['tsvector @@ plainto_tsquery(?)', TextUtils.transliterate(keyword.downcase)]
|
||||
else
|
||||
keyword = TextUtils.transliterate(keyword.downcase).squish
|
||||
|
||||
+1
-1
@@ -162,7 +162,7 @@ module Submissions
|
||||
return email.downcase.sub(/@gmail?\z/i, '@gmail.com') if email.match?(/@gmail?\z/i)
|
||||
|
||||
return email.downcase if email.include?(',') ||
|
||||
email.match?(/\.(?:gob|om|mm|cm|et|mo|nz|za|ie)\z/) ||
|
||||
email.match?(/\.(?:gob|om|mm|cm|et|mo|nz|za|ie|ed\.jp)\z/i) ||
|
||||
email.exclude?('.')
|
||||
|
||||
fixed_email = EmailTypo.call(email.delete_prefix('<'))
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Templates
|
||||
COLOR_REGEXP = /\A(#(?:[0-9a-f]{3}|[0-9a-f]{6})|[a-z]+)\z/i
|
||||
|
||||
EXPIRATION_DURATIONS = {
|
||||
one_day: 1.day,
|
||||
two_days: 2.days,
|
||||
|
||||
@@ -6,7 +6,7 @@ module Templates
|
||||
|
||||
# rubocop:disable Metrics
|
||||
def call(original_template, author:, external_id: nil, name: nil, folder_name: nil)
|
||||
template = original_template.account.templates.new
|
||||
template = author.account.templates.new
|
||||
|
||||
template.external_id = external_id
|
||||
template.shared_link = original_template.shared_link
|
||||
@@ -16,8 +16,10 @@ module Templates
|
||||
|
||||
if folder_name.present?
|
||||
template.folder = TemplateFolders.find_or_create_by_name(author, folder_name)
|
||||
else
|
||||
elsif author.account_id == original_template.account_id
|
||||
template.folder_id = original_template.folder_id
|
||||
else
|
||||
template.folder = author.account.default_template_folder
|
||||
end
|
||||
|
||||
template.submitters, template.fields, template.schema, template.preferences =
|
||||
|
||||
Reference in New Issue
Block a user