Compare commits
No commits in common. 'eea44bda347ea13515554fefd0225e79603c64ac' and '911e55ccc356a57236378d69fc6ee235af14ec98' have entirely different histories.
eea44bda34
...
911e55ccc3
@ -1,37 +0,0 @@
|
|||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
class SubmitFormMetadataController < ApplicationController
|
|
||||||
skip_before_action :authenticate_user!
|
|
||||||
skip_authorization_check
|
|
||||||
|
|
||||||
def index
|
|
||||||
submitter = Submitter.find_by!(slug: params[:submit_form_slug])
|
|
||||||
|
|
||||||
return head :not_found if submitter.declined_at? ||
|
|
||||||
submitter.completed_at? ||
|
|
||||||
submitter.submission.archived_at? ||
|
|
||||||
submitter.submission.expired? ||
|
|
||||||
submitter.submission.template&.archived_at? ||
|
|
||||||
submitter.account.archived_at? ||
|
|
||||||
!Submitters::AuthorizedForForm.call(submitter, current_user, request)
|
|
||||||
|
|
||||||
submission = submitter.submission
|
|
||||||
values = submission.submitters.reduce({}) { |acc, sub| acc.merge(sub.values) }
|
|
||||||
schema = Submissions.filtered_conditions_schema(submission, values:, include_submitter_uuid: submitter.uuid)
|
|
||||||
|
|
||||||
documents = schema.filter_map do |item|
|
|
||||||
submission.schema_documents.find { |a| a.uuid == item['attachment_uuid'] }
|
|
||||||
end
|
|
||||||
|
|
||||||
ActiveRecord::Associations::Preloader.new(records: documents, associations: %i[blob record]).call
|
|
||||||
|
|
||||||
text_runs = documents.to_h do |document|
|
|
||||||
[
|
|
||||||
document.uuid,
|
|
||||||
DocumentMetadatas.find_or_create_for_document(document, account_id: document.record.account_id).text_runs
|
|
||||||
]
|
|
||||||
end
|
|
||||||
|
|
||||||
render json: { text_runs: }
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@ -1,22 +0,0 @@
|
|||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
class TemplatesShareLinkQrController < ApplicationController
|
|
||||||
load_and_authorize_resource :template
|
|
||||||
|
|
||||||
def show
|
|
||||||
return render :disabled, layout: 'plain' unless @template.shared_link?
|
|
||||||
|
|
||||||
shared_link_url = start_form_url(slug: @template.slug, host: form_link_host)
|
|
||||||
|
|
||||||
@qr_svg_code = RQRCode::QRCode.new(shared_link_url, level: :m).as_svg(viewbox: true)
|
|
||||||
|
|
||||||
@page_size =
|
|
||||||
if TimeUtils.timezone_abbr(current_account.timezone, Time.current.beginning_of_year).in?(TimeUtils::US_TIMEZONES)
|
|
||||||
'Letter'
|
|
||||||
else
|
|
||||||
'A4'
|
|
||||||
end
|
|
||||||
|
|
||||||
render :show, layout: false
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@ -1,18 +0,0 @@
|
|||||||
export default class extends HTMLElement {
|
|
||||||
connectedCallback () {
|
|
||||||
const dialog = document.getElementById(this.dataset.target)
|
|
||||||
|
|
||||||
this.querySelector('button').addEventListener('click', () => {
|
|
||||||
if (dialog) {
|
|
||||||
dialog.inert = false
|
|
||||||
dialog.showModal()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
if (dialog) {
|
|
||||||
dialog.addEventListener('close', () => {
|
|
||||||
dialog.inert = true
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,434 +0,0 @@
|
|||||||
<template>
|
|
||||||
<template
|
|
||||||
v-for="(pages, docUuid) in textRuns"
|
|
||||||
:key="docUuid"
|
|
||||||
>
|
|
||||||
<template
|
|
||||||
v-for="(_, pageIndex) in pages"
|
|
||||||
:key="pageIndex"
|
|
||||||
>
|
|
||||||
<template
|
|
||||||
v-for="(pageElem, i) in [findPageElement(docUuid, pageIndex)]"
|
|
||||||
:key="i"
|
|
||||||
>
|
|
||||||
<Teleport
|
|
||||||
v-if="pageElem"
|
|
||||||
:to="pageElem"
|
|
||||||
>
|
|
||||||
<template
|
|
||||||
v-for="(item, index) in sortedItemsForPage(docUuid, pageIndex)"
|
|
||||||
:key="index"
|
|
||||||
>
|
|
||||||
<template v-if="item.type === 'text_group'">
|
|
||||||
<span
|
|
||||||
class="absolute overflow-hidden text-transparent select-none pointer-events-none"
|
|
||||||
:style="{ left: item.x * 100 + '%', top: item.y * 100 + '%', width: item.w * 100 + '%', height: item.h * 100 + '%' }"
|
|
||||||
>{{ item.text }}</span>
|
|
||||||
<span
|
|
||||||
v-for="(run, runIndex) in item.items"
|
|
||||||
:key="runIndex"
|
|
||||||
aria-hidden="true"
|
|
||||||
class="absolute overflow-hidden text-transparent"
|
|
||||||
:style="{ left: run.x * 100 + '%', top: run.y * 100 + '%', width: run.w * 100 + '%', height: run.h * 100 + '%', fontSize: run.font_size ? (run.font_size / 10) + 'cqmin' : undefined, textAlign: 'justify', textAlignLast: 'justify', textJustify: 'inter-character' }"
|
|
||||||
>{{ run.text }}</span>
|
|
||||||
</template>
|
|
||||||
<FieldArea
|
|
||||||
v-else-if="item.type === 'field_area'"
|
|
||||||
:ref="setAreaRef"
|
|
||||||
v-model="values[item.field.uuid]"
|
|
||||||
:values="values"
|
|
||||||
:field="item.field"
|
|
||||||
:area="item.area"
|
|
||||||
:submittable="true"
|
|
||||||
:page-width="1400"
|
|
||||||
:page-height="(1400.0 / pageElem.offsetWidth) * pageElem.offsetHeight"
|
|
||||||
:field-index="item.fieldIndex"
|
|
||||||
:is-inline-size="isInlineSize"
|
|
||||||
:scroll-padding="scrollPadding"
|
|
||||||
:submitter="submitter"
|
|
||||||
:with-field-placeholder="withFieldPlaceholder"
|
|
||||||
:with-signature-id="withSignatureId"
|
|
||||||
:is-active="currentStep === item.step"
|
|
||||||
:with-label="withLabel && !withFieldPlaceholder && item.step.length < 2"
|
|
||||||
:is-value-set="item.step.some((f) => f.uuid in values)"
|
|
||||||
:attachments-index="attachmentsIndex"
|
|
||||||
@click="[$emit('focus-step', item.stepIndex), maybeScrollOnClick(item.field, item.area)]"
|
|
||||||
/>
|
|
||||||
<FieldArea
|
|
||||||
v-else-if="item.type === 'readonly_field_area'"
|
|
||||||
:model-value="readonlyConditionalFieldValues[item.field.uuid]"
|
|
||||||
:values="readonlyConditionalFieldValues"
|
|
||||||
:field="item.field"
|
|
||||||
:area="item.area"
|
|
||||||
:submittable="false"
|
|
||||||
:page-width="1400"
|
|
||||||
:page-height="(1400.0 / pageElem.offsetWidth) * pageElem.offsetHeight"
|
|
||||||
:field-index="item.fieldIndex"
|
|
||||||
:is-inline-size="isInlineSize"
|
|
||||||
:submitter="submitter"
|
|
||||||
:attachments-index="attachmentsIndex"
|
|
||||||
/>
|
|
||||||
<FieldArea
|
|
||||||
v-else-if="item.type === 'formula_area' && isMathLoaded"
|
|
||||||
:model-value="calculateFormula(item.field)"
|
|
||||||
:is-inline-size="isInlineSize"
|
|
||||||
:field="item.field"
|
|
||||||
:area="item.area"
|
|
||||||
:submittable="false"
|
|
||||||
:field-index="item.fieldIndex"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
</Teleport>
|
|
||||||
</template>
|
|
||||||
</template>
|
|
||||||
</template>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
import FieldArea from './area'
|
|
||||||
import FormulaAreas from './formula_areas'
|
|
||||||
import FieldAreas from './areas'
|
|
||||||
|
|
||||||
export default {
|
|
||||||
name: 'AccessibilityAreas',
|
|
||||||
components: {
|
|
||||||
FieldArea
|
|
||||||
},
|
|
||||||
inject: ['baseUrl', 't'],
|
|
||||||
props: {
|
|
||||||
submitterSlug: {
|
|
||||||
type: String,
|
|
||||||
required: true
|
|
||||||
},
|
|
||||||
filledFieldsIndex: {
|
|
||||||
type: Object,
|
|
||||||
required: false,
|
|
||||||
default: null
|
|
||||||
},
|
|
||||||
steps: {
|
|
||||||
type: Array,
|
|
||||||
required: false,
|
|
||||||
default: () => []
|
|
||||||
},
|
|
||||||
readonlyConditionalFields: {
|
|
||||||
type: Array,
|
|
||||||
required: false,
|
|
||||||
default: () => []
|
|
||||||
},
|
|
||||||
readonlyConditionalFieldValues: {
|
|
||||||
type: Object,
|
|
||||||
required: false,
|
|
||||||
default: () => ({})
|
|
||||||
},
|
|
||||||
formulaFields: {
|
|
||||||
type: Array,
|
|
||||||
required: false,
|
|
||||||
default: () => []
|
|
||||||
},
|
|
||||||
values: {
|
|
||||||
type: Object,
|
|
||||||
required: false,
|
|
||||||
default: () => ({})
|
|
||||||
},
|
|
||||||
readonlyValues: {
|
|
||||||
type: Object,
|
|
||||||
required: false,
|
|
||||||
default: () => ({})
|
|
||||||
},
|
|
||||||
submitter: {
|
|
||||||
type: Object,
|
|
||||||
required: true
|
|
||||||
},
|
|
||||||
currentStep: {
|
|
||||||
type: Array,
|
|
||||||
required: false,
|
|
||||||
default: () => []
|
|
||||||
},
|
|
||||||
withFieldPlaceholder: {
|
|
||||||
type: Boolean,
|
|
||||||
required: false,
|
|
||||||
default: false
|
|
||||||
},
|
|
||||||
withSignatureId: {
|
|
||||||
type: Boolean,
|
|
||||||
required: false,
|
|
||||||
default: false
|
|
||||||
},
|
|
||||||
withLabel: {
|
|
||||||
type: Boolean,
|
|
||||||
required: false,
|
|
||||||
default: true
|
|
||||||
},
|
|
||||||
scrollPadding: {
|
|
||||||
type: String,
|
|
||||||
required: false,
|
|
||||||
default: '-80px'
|
|
||||||
},
|
|
||||||
scrollEl: {
|
|
||||||
type: Object,
|
|
||||||
required: false,
|
|
||||||
default: null
|
|
||||||
},
|
|
||||||
attachmentsIndex: {
|
|
||||||
type: Object,
|
|
||||||
required: false,
|
|
||||||
default: () => ({})
|
|
||||||
},
|
|
||||||
fetchOptions: {
|
|
||||||
type: Object,
|
|
||||||
required: false,
|
|
||||||
default: () => ({})
|
|
||||||
}
|
|
||||||
},
|
|
||||||
emits: ['focus-step'],
|
|
||||||
data () {
|
|
||||||
return {
|
|
||||||
isMathLoaded: false,
|
|
||||||
math: null,
|
|
||||||
textRuns: {},
|
|
||||||
areaRefs: []
|
|
||||||
}
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
fieldValuesIndex () {
|
|
||||||
return this.filledFieldsIndex || this.extractStaticValues()
|
|
||||||
},
|
|
||||||
isMobileContainer: FieldAreas.computed.isMobileContainer,
|
|
||||||
isInlineSize: FieldAreas.computed.isInlineSize,
|
|
||||||
fieldsUuidIndex () {
|
|
||||||
return this.formulaFields.reduce((acc, field) => {
|
|
||||||
acc[field.uuid] = field
|
|
||||||
|
|
||||||
return acc
|
|
||||||
}, {})
|
|
||||||
},
|
|
||||||
fieldAreasIndex () {
|
|
||||||
const index = Object.create(null)
|
|
||||||
|
|
||||||
this.steps.forEach((step, stepIndex) => {
|
|
||||||
step.forEach((field, fieldIndex) => {
|
|
||||||
(field.areas || []).forEach((area) => {
|
|
||||||
index[area.attachment_uuid] ||= Object.create(null)
|
|
||||||
index[area.attachment_uuid][area.page] ||= []
|
|
||||||
index[area.attachment_uuid][area.page].push({
|
|
||||||
type: 'field_area',
|
|
||||||
field,
|
|
||||||
area,
|
|
||||||
step,
|
|
||||||
stepIndex,
|
|
||||||
fieldIndex,
|
|
||||||
x: area.x,
|
|
||||||
y: area.y,
|
|
||||||
w: area.w,
|
|
||||||
h: area.h
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
return index
|
|
||||||
},
|
|
||||||
formulaAreasIndex () {
|
|
||||||
const index = Object.create(null)
|
|
||||||
|
|
||||||
this.formulaFields.forEach((field, fieldIndex) => {
|
|
||||||
(field.areas || []).forEach((area) => {
|
|
||||||
index[area.attachment_uuid] ||= Object.create(null)
|
|
||||||
index[area.attachment_uuid][area.page] ||= []
|
|
||||||
index[area.attachment_uuid][area.page].push({
|
|
||||||
type: 'formula_area',
|
|
||||||
field,
|
|
||||||
area,
|
|
||||||
fieldIndex,
|
|
||||||
x: area.x,
|
|
||||||
y: area.y,
|
|
||||||
w: area.w,
|
|
||||||
h: area.h
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
return index
|
|
||||||
},
|
|
||||||
readonlyFieldAreasIndex () {
|
|
||||||
const index = Object.create(null)
|
|
||||||
|
|
||||||
this.readonlyConditionalFields.forEach((field, fieldIndex) => {
|
|
||||||
(field.areas || []).forEach((area) => {
|
|
||||||
index[area.attachment_uuid] ||= Object.create(null)
|
|
||||||
index[area.attachment_uuid][area.page] ||= []
|
|
||||||
index[area.attachment_uuid][area.page].push({
|
|
||||||
type: 'readonly_field_area',
|
|
||||||
field,
|
|
||||||
area,
|
|
||||||
fieldIndex,
|
|
||||||
x: area.x,
|
|
||||||
y: area.y,
|
|
||||||
w: area.w,
|
|
||||||
h: area.h
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
return index
|
|
||||||
}
|
|
||||||
},
|
|
||||||
beforeUpdate () {
|
|
||||||
this.areaRefs = []
|
|
||||||
},
|
|
||||||
async mounted () {
|
|
||||||
const [metadataResult] = await Promise.all([
|
|
||||||
fetch(this.baseUrl + `/s/${this.submitterSlug}/metadata`, {
|
|
||||||
...this.fetchOptions
|
|
||||||
}).then((r) => r.json()).catch(() => ({})),
|
|
||||||
this.loadMath()
|
|
||||||
])
|
|
||||||
|
|
||||||
this.textRuns = metadataResult.text_runs || {}
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
normalizeFormula: FormulaAreas.methods.normalizeFormula,
|
|
||||||
calculateFormula: FormulaAreas.methods.calculateFormula,
|
|
||||||
scrollInContainer: FieldAreas.methods.scrollInContainer,
|
|
||||||
scrollIntoArea: FieldAreas.methods.scrollIntoArea,
|
|
||||||
scrollIntoField: FieldAreas.methods.scrollIntoField,
|
|
||||||
maybeScrollOnClick: FieldAreas.methods.maybeScrollOnClick,
|
|
||||||
setAreaRef (el) {
|
|
||||||
if (el) {
|
|
||||||
this.areaRefs.push(el)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
async loadMath () {
|
|
||||||
if (this.formulaFields.length && !this.isMathLoaded) {
|
|
||||||
const { Calculator } = await import('./calculator')
|
|
||||||
|
|
||||||
this.math = new Calculator()
|
|
||||||
this.isMathLoaded = true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
extractStaticValues () {
|
|
||||||
const result = Object.create(null)
|
|
||||||
const root = this.$root.$el?.parentNode?.getRootNode() || document
|
|
||||||
const pageContainers = root.querySelectorAll('page-container')
|
|
||||||
|
|
||||||
pageContainers.forEach((container) => {
|
|
||||||
const overlay = container.querySelector('[id^="page-"]')
|
|
||||||
|
|
||||||
if (!overlay) return
|
|
||||||
|
|
||||||
const parts = overlay.id.split('-')
|
|
||||||
const pageIndex = parseInt(parts[parts.length - 1])
|
|
||||||
const docUuid = parts.slice(1, -1).join('-')
|
|
||||||
|
|
||||||
const fieldValues = overlay.querySelectorAll('field-value')
|
|
||||||
|
|
||||||
if (!fieldValues.length) return
|
|
||||||
|
|
||||||
result[docUuid] ||= Object.create(null)
|
|
||||||
result[docUuid][pageIndex] = []
|
|
||||||
|
|
||||||
fieldValues.forEach((el) => {
|
|
||||||
const style = el.style
|
|
||||||
const x = parseFloat(style.left) / 100
|
|
||||||
const y = parseFloat(style.top) / 100
|
|
||||||
const w = parseFloat(style.width) / 100
|
|
||||||
const h = parseFloat(style.height) / 100
|
|
||||||
const text = el.textContent.trim()
|
|
||||||
|
|
||||||
if (text) {
|
|
||||||
result[docUuid][pageIndex].push({ type: 'static_value', text, x, y, w, h })
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
return result
|
|
||||||
},
|
|
||||||
findPageElement (docUuid, pageIndex) {
|
|
||||||
return (this.$root.$el?.parentNode?.getRootNode() || document).getElementById(`page-${docUuid}-${pageIndex}`)
|
|
||||||
},
|
|
||||||
sortedItemsForPage (docUuid, pageIndex) {
|
|
||||||
const items = []
|
|
||||||
|
|
||||||
const pageTextRuns = this.textRuns[docUuid]?.[pageIndex] || []
|
|
||||||
|
|
||||||
pageTextRuns.forEach((run) => {
|
|
||||||
items.push({
|
|
||||||
type: 'text_run',
|
|
||||||
text: run.text,
|
|
||||||
x: run.x,
|
|
||||||
y: run.y,
|
|
||||||
w: run.w,
|
|
||||||
h: run.h,
|
|
||||||
font_size: run.font_size
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
const fieldAreas = this.fieldAreasIndex[docUuid]?.[pageIndex] || []
|
|
||||||
items.push(...fieldAreas)
|
|
||||||
|
|
||||||
const readonlyFieldAreas = this.readonlyFieldAreasIndex[docUuid]?.[pageIndex] || []
|
|
||||||
items.push(...readonlyFieldAreas)
|
|
||||||
|
|
||||||
const formulaAreas = this.formulaAreasIndex[docUuid]?.[pageIndex] || []
|
|
||||||
items.push(...formulaAreas)
|
|
||||||
|
|
||||||
const pageFieldValues = this.fieldValuesIndex[docUuid]?.[pageIndex] || []
|
|
||||||
items.push(...pageFieldValues)
|
|
||||||
|
|
||||||
items.sort((a, b) => {
|
|
||||||
const aCenterY = a.y + a.h / 2
|
|
||||||
const bCenterY = b.y + b.h / 2
|
|
||||||
const lineThreshold = Math.min(a.h, b.h) / 2
|
|
||||||
|
|
||||||
if (Math.abs(aCenterY - bCenterY) < lineThreshold) {
|
|
||||||
return a.x - b.x
|
|
||||||
}
|
|
||||||
|
|
||||||
return aCenterY - bCenterY
|
|
||||||
})
|
|
||||||
|
|
||||||
const grouped = []
|
|
||||||
let currentGroup = null
|
|
||||||
|
|
||||||
const closeGroup = () => {
|
|
||||||
if (!currentGroup) return
|
|
||||||
|
|
||||||
const groupItems = currentGroup.items
|
|
||||||
const minX = Math.min(...groupItems.map((i) => i.x))
|
|
||||||
const minY = Math.min(...groupItems.map((i) => i.y))
|
|
||||||
const maxEndX = Math.max(...groupItems.map((i) => i.x + i.w))
|
|
||||||
const maxEndY = Math.max(...groupItems.map((i) => i.y + i.h))
|
|
||||||
|
|
||||||
currentGroup.x = minX
|
|
||||||
currentGroup.y = minY
|
|
||||||
currentGroup.w = maxEndX - minX
|
|
||||||
currentGroup.h = maxEndY - minY
|
|
||||||
currentGroup.text = groupItems.map((i) => i.text).join(' ').replace(/\s+/g, ' ').trim()
|
|
||||||
|
|
||||||
grouped.push(currentGroup)
|
|
||||||
currentGroup = null
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const item of items) {
|
|
||||||
const isTextLike = item.type === 'text_run' || item.type === 'static_value'
|
|
||||||
|
|
||||||
if (!isTextLike) {
|
|
||||||
closeGroup()
|
|
||||||
grouped.push(item)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!currentGroup) {
|
|
||||||
currentGroup = { type: 'text_group', items: [] }
|
|
||||||
}
|
|
||||||
|
|
||||||
currentGroup.items.push(item)
|
|
||||||
}
|
|
||||||
|
|
||||||
closeGroup()
|
|
||||||
|
|
||||||
return grouped
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
@ -1,27 +0,0 @@
|
|||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
# == Schema Information
|
|
||||||
#
|
|
||||||
# Table name: document_metadata
|
|
||||||
#
|
|
||||||
# id :bigint not null, primary key
|
|
||||||
# blob_checksum :string not null
|
|
||||||
# text_runs :text not null
|
|
||||||
# created_at :datetime not null
|
|
||||||
# account_id :bigint not null
|
|
||||||
#
|
|
||||||
# Indexes
|
|
||||||
#
|
|
||||||
# index_document_metadata_on_account_id_and_blob_checksum (account_id,blob_checksum) UNIQUE
|
|
||||||
#
|
|
||||||
# Foreign Keys
|
|
||||||
#
|
|
||||||
# fk_rails_... (account_id => accounts.id)
|
|
||||||
#
|
|
||||||
class DocumentMetadata < ApplicationRecord
|
|
||||||
belongs_to :account
|
|
||||||
|
|
||||||
attribute :text_runs, :string, default: -> { {} }
|
|
||||||
|
|
||||||
serialize :text_runs, coder: JSON
|
|
||||||
end
|
|
||||||
|
Before Width: | Height: | Size: 408 B After Width: | Height: | Size: 389 B |
|
Before Width: | Height: | Size: 416 B After Width: | Height: | Size: 397 B |
|
Before Width: | Height: | Size: 431 B After Width: | Height: | Size: 412 B |
|
Before Width: | Height: | Size: 337 B After Width: | Height: | Size: 318 B |
|
Before Width: | Height: | Size: 467 B After Width: | Height: | Size: 448 B |
|
Before Width: | Height: | Size: 529 B |
|
Before Width: | Height: | Size: 797 B |
|
Before Width: | Height: | Size: 428 B After Width: | Height: | Size: 409 B |
|
Before Width: | Height: | Size: 435 B After Width: | Height: | Size: 416 B |
|
Before Width: | Height: | Size: 603 B After Width: | Height: | Size: 584 B |
|
Before Width: | Height: | Size: 309 B After Width: | Height: | Size: 290 B |
@ -1,22 +1,18 @@
|
|||||||
<% uuid = local_assigns[:uuid] || SecureRandom.uuid %>
|
<% uuid = local_assigns[:uuid] || SecureRandom.uuid %>
|
||||||
<% title_id = "#{uuid}-title" %>
|
<input type="checkbox" id="<%= uuid %>" class="modal-toggle">
|
||||||
<%= tag.dialog id: uuid, class: 'modal items-start overflow-y-auto', inert: true, 'aria-labelledby': (title_id if local_assigns[:title]) do %>
|
<div id="<%= local_assigns[:id] %>" class="modal items-start !animate-none overflow-y-auto">
|
||||||
<div class="modal-box pt-4 pb-6 px-6 mt-20 max-h-none">
|
<div class="modal-box pt-4 pb-6 px-6 mt-20 max-h-none">
|
||||||
<% if local_assigns[:title] %>
|
<% if local_assigns[:title] %>
|
||||||
<div class="flex justify-between items-center border-b pb-2 mb-2 font-medium">
|
<div class="flex justify-between items-center border-b pb-2 mb-2 font-medium">
|
||||||
<span id="<%= title_id %>">
|
<span>
|
||||||
<%= local_assigns[:title] %>
|
<%= local_assigns[:title] %>
|
||||||
</span>
|
</span>
|
||||||
<form method="dialog">
|
<label for="<%= uuid %>" class="text-xl">×</label>
|
||||||
<button class="text-xl cursor-pointer w-6 h-6" aria-label="<%= t('close') %>">×</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
<div>
|
<div>
|
||||||
<%= yield %>
|
<%= yield %>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<form method="dialog" class="modal-backdrop">
|
<label class="modal-backdrop" for="<%= uuid %>"></label>
|
||||||
<button aria-label="<%= t('close') %>"></button>
|
</div>
|
||||||
</form>
|
|
||||||
<% end %>
|
|
||||||
|
|||||||
@ -1,2 +0,0 @@
|
|||||||
<%= t('powered_by') %>
|
|
||||||
<a href="<%= Docuseal::PRODUCT_URL %>" target="_blank" rel="noopener"><%= Docuseal.product_name %></a>
|
|
||||||
@ -1,2 +0,0 @@
|
|||||||
<%= render 'shared/logo' %>
|
|
||||||
<span><%= Docuseal.product_name %></span>
|
|
||||||
@ -1,10 +0,0 @@
|
|||||||
<div class="max-w-md space-y-6 mx-auto px-2 mt-12 mb-4">
|
|
||||||
<p class="text-xl font-semibold text-center">
|
|
||||||
<%= t('share_link_is_currently_disabled') %>
|
|
||||||
</p>
|
|
||||||
<% if can?(:update, @template) %>
|
|
||||||
<toggle-submit class="block">
|
|
||||||
<%= button_to button_title(title: t('enable_shared_link'), icon: svg_icon('lock_open', class: 'w-6 h-6')), template_share_link_path(@template), params: { template: { shared_link: true }, redir: template_share_link_qr_path(@template) }, method: :post, data: { turbo: false }, class: 'base-button w-full' %>
|
|
||||||
</toggle-submit>
|
|
||||||
<% end %>
|
|
||||||
</div>
|
|
||||||
@ -1,288 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<% page_width_css = @page_size == 'Letter' ? 8.5 * 96.0 : 210.0 * 96.0 / 25.4 %>
|
|
||||||
<% page_height_css = @page_size == 'Letter' ? 11.0 * 96.0 : 297.0 * 96.0 / 25.4 %>
|
|
||||||
<% page_width = @page_size == 'Letter' ? '8.5in' : '210mm' %>
|
|
||||||
<% page_cqw = ->(px) { format('%.6fcqw', px / page_width_css * 100.0) } %>
|
|
||||||
<html lang="<%= I18n.locale %>">
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title><%= @template.name %></title>
|
|
||||||
<style>
|
|
||||||
@page {
|
|
||||||
size: <%= @page_size %> portrait;
|
|
||||||
margin: 0.5in;
|
|
||||||
}
|
|
||||||
|
|
||||||
html, body {
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
|
|
||||||
color: #111;
|
|
||||||
background: #faf7f5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.qr-page-wrapper {
|
|
||||||
container-type: size;
|
|
||||||
width: min(100vw, <%= page_width %>);
|
|
||||||
max-width: 100%;
|
|
||||||
aspect-ratio: <%= format('%<width>.6f / %<height>.6f', width: page_width_css, height: page_height_css) %>;
|
|
||||||
margin: 24px auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.qr-page {
|
|
||||||
box-sizing: border-box;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
padding: <%= page_cqw.call(72) %>;
|
|
||||||
background: #ffffff;
|
|
||||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.12);
|
|
||||||
display: grid;
|
|
||||||
grid-template-rows: auto 1fr auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.qr-logo {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
gap: <%= page_cqw.call(10) %>;
|
|
||||||
font-size: <%= page_cqw.call(20) %>;
|
|
||||||
font-weight: 700;
|
|
||||||
letter-spacing: -0.01em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.qr-logo svg {
|
|
||||||
width: <%= page_cqw.call(32) %>;
|
|
||||||
height: <%= page_cqw.call(32) %>;
|
|
||||||
}
|
|
||||||
|
|
||||||
.qr-logo img {
|
|
||||||
height: <%= page_cqw.call(50) %>;
|
|
||||||
}
|
|
||||||
|
|
||||||
.qr-content {
|
|
||||||
align-self: center;
|
|
||||||
text-align: center;
|
|
||||||
min-width: 0;
|
|
||||||
margin-bottom: <%= page_cqw.call(80) %>;
|
|
||||||
}
|
|
||||||
|
|
||||||
.qr-header {
|
|
||||||
font-size: <%= page_cqw.call(36) %>;
|
|
||||||
font-weight: 700;
|
|
||||||
line-height: 1.2;
|
|
||||||
padding: 0 <%= page_cqw.call(8) %>;
|
|
||||||
margin-bottom: <%= page_cqw.call(48) %>;
|
|
||||||
word-break: break-word;
|
|
||||||
}
|
|
||||||
|
|
||||||
.qr-main svg {
|
|
||||||
display: block;
|
|
||||||
width: <%= page_cqw.call(480) %>;
|
|
||||||
height: auto;
|
|
||||||
max-width: 100%;
|
|
||||||
margin: 0 auto;
|
|
||||||
shape-rendering: crispEdges;
|
|
||||||
}
|
|
||||||
|
|
||||||
.qr-footer {
|
|
||||||
font-size: <%= page_cqw.call(22) %>;
|
|
||||||
line-height: 1.4;
|
|
||||||
padding: 0 <%= page_cqw.call(8) %>;
|
|
||||||
margin-top: <%= page_cqw.call(48) %>;
|
|
||||||
word-break: break-word;
|
|
||||||
}
|
|
||||||
|
|
||||||
.qr-branding {
|
|
||||||
align-self: end;
|
|
||||||
text-align: center;
|
|
||||||
font-size: <%= page_cqw.call(11) %>;
|
|
||||||
color: #6b7280;
|
|
||||||
padding-top: <%= page_cqw.call(24) %>;
|
|
||||||
}
|
|
||||||
|
|
||||||
.qr-branding a {
|
|
||||||
color: #4b5563;
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
[contenteditable="true"] {
|
|
||||||
outline: 1px dashed #cbd5e1;
|
|
||||||
outline-offset: 6px;
|
|
||||||
cursor: text;
|
|
||||||
transition: outline-color 0.15s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
[contenteditable="true"]:hover {
|
|
||||||
outline-color: #94a3b8;
|
|
||||||
}
|
|
||||||
|
|
||||||
[contenteditable="true"]:focus {
|
|
||||||
outline: 1px dashed #291334;
|
|
||||||
outline-offset: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.print-button {
|
|
||||||
position: fixed;
|
|
||||||
right: 24px;
|
|
||||||
top: 24px;
|
|
||||||
z-index: 100;
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
gap: 0.5rem;
|
|
||||||
min-height: 3rem;
|
|
||||||
height: 3rem;
|
|
||||||
padding-left: 1rem;
|
|
||||||
padding-right: 1rem;
|
|
||||||
border: 1px solid #291334;
|
|
||||||
border-radius: 1.9rem;
|
|
||||||
background-color: #291334;
|
|
||||||
color: #ffffff;
|
|
||||||
font-size: 1rem;
|
|
||||||
font-weight: 500;
|
|
||||||
line-height: 1em;
|
|
||||||
font-family: inherit;
|
|
||||||
text-transform: none;
|
|
||||||
letter-spacing: normal;
|
|
||||||
cursor: pointer;
|
|
||||||
user-select: none;
|
|
||||||
transition: background-color 0.2s ease, border-color 0.2s ease, transform 0.1s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.print-button:hover {
|
|
||||||
background-color: #1a0c22;
|
|
||||||
border-color: #1a0c22;
|
|
||||||
}
|
|
||||||
|
|
||||||
.print-button:active {
|
|
||||||
transform: scale(0.97);
|
|
||||||
}
|
|
||||||
|
|
||||||
.print-button:focus-visible {
|
|
||||||
outline: 2px solid #291334;
|
|
||||||
outline-offset: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.print-button svg {
|
|
||||||
width: 1.25rem;
|
|
||||||
height: 1.25rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media screen and (max-width: 820px) {
|
|
||||||
.print-button {
|
|
||||||
top: auto;
|
|
||||||
right: 12px;
|
|
||||||
bottom: 12px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media print {
|
|
||||||
html, body {
|
|
||||||
background: #ffffff;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.qr-page-wrapper {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
aspect-ratio: auto;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.qr-page {
|
|
||||||
margin: 0;
|
|
||||||
box-shadow: none;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
min-height: 100%;
|
|
||||||
padding: 0.25in;
|
|
||||||
page-break-inside: avoid;
|
|
||||||
page-break-after: avoid;
|
|
||||||
}
|
|
||||||
|
|
||||||
.qr-logo {
|
|
||||||
gap: 10px;
|
|
||||||
font-size: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.qr-logo svg {
|
|
||||||
width: 32px;
|
|
||||||
height: 32px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.qr-logo img {
|
|
||||||
height: 50px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.qr-content {
|
|
||||||
margin-bottom: 80px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.qr-header {
|
|
||||||
font-size: 36px;
|
|
||||||
padding: 0 8px;
|
|
||||||
margin-bottom: 48px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.qr-main svg {
|
|
||||||
width: 5in;
|
|
||||||
height: 5in;
|
|
||||||
}
|
|
||||||
|
|
||||||
.qr-footer {
|
|
||||||
font-size: 22px;
|
|
||||||
padding: 0 8px;
|
|
||||||
margin-top: 48px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.qr-branding {
|
|
||||||
font-size: 11px;
|
|
||||||
padding-top: 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.print-button {
|
|
||||||
display: none !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
[contenteditable="true"],
|
|
||||||
[contenteditable="true"]:focus {
|
|
||||||
outline: none !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div class="qr-page-wrapper">
|
|
||||||
<div class="qr-page">
|
|
||||||
<div class="qr-logo">
|
|
||||||
<%= render 'logo' %>
|
|
||||||
</div>
|
|
||||||
<div class="qr-content">
|
|
||||||
<div class="qr-header" contenteditable="true" spellcheck="false"><%= @template.name %></div>
|
|
||||||
<div class="qr-main">
|
|
||||||
<%== @qr_svg_code %>
|
|
||||||
</div>
|
|
||||||
<div class="qr-footer" contenteditable="true" spellcheck="false">
|
|
||||||
<%= t('scan_the_qr_code_above_with_your_phone_camera_to_open_and_sign_this_document') %>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="qr-branding">
|
|
||||||
<%= render 'branding' %>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<button type="button" id="qr-print-button" class="print-button">
|
|
||||||
<%= svg_icon('printer') %>
|
|
||||||
<span><%= t('print') %></span>
|
|
||||||
</button>
|
|
||||||
<script nonce="<%= content_security_policy_nonce %>">
|
|
||||||
document.getElementById('qr-print-button').addEventListener('click', function () {
|
|
||||||
window.print();
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@ -1,15 +0,0 @@
|
|||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
class CreateDocumentMetadata < ActiveRecord::Migration[8.1]
|
|
||||||
def change
|
|
||||||
create_table :document_metadata do |t|
|
|
||||||
t.references :account, null: false, foreign_key: true, index: false
|
|
||||||
t.string :blob_checksum, null: false
|
|
||||||
t.text :text_runs, null: false
|
|
||||||
|
|
||||||
t.datetime :created_at, null: false
|
|
||||||
end
|
|
||||||
|
|
||||||
add_index :document_metadata, %i[account_id blob_checksum], unique: true
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@ -1,34 +0,0 @@
|
|||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
module DocumentMetadatas
|
|
||||||
module_function
|
|
||||||
|
|
||||||
def find_or_create_for_document(document, account_id:)
|
|
||||||
checksum = document.blob.checksum
|
|
||||||
|
|
||||||
metadata = DocumentMetadata.find_by(account_id:, blob_checksum: checksum)
|
|
||||||
metadata ||= DocumentMetadata.create!(account_id:, blob_checksum: checksum, text_runs: build_text_runs(document))
|
|
||||||
|
|
||||||
metadata
|
|
||||||
rescue ActiveRecord::RecordNotUnique
|
|
||||||
retry
|
|
||||||
end
|
|
||||||
|
|
||||||
def build_text_runs(document)
|
|
||||||
number_of_pages = document.metadata.dig('pdf', 'number_of_pages').to_i
|
|
||||||
|
|
||||||
return {} if number_of_pages.zero?
|
|
||||||
|
|
||||||
Pdfium::Document.open_bytes(document.download) do |doc|
|
|
||||||
(0...doc.page_count).each_with_object({}) do |page_index, acc|
|
|
||||||
page = doc.get_page(page_index)
|
|
||||||
|
|
||||||
acc[page_index] = page.text_objects.map do |node|
|
|
||||||
{ text: node.content, x: node.x, y: node.y, w: node.w, h: node.h, font_size: node.font_size }
|
|
||||||
end
|
|
||||||
ensure
|
|
||||||
page&.close
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@ -1,67 +0,0 @@
|
|||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
module Mcp
|
|
||||||
module Tools
|
|
||||||
module LoadTemplate
|
|
||||||
SCHEMA = {
|
|
||||||
name: 'load_template',
|
|
||||||
title: 'Load Template',
|
|
||||||
description: 'Load a template with its fields. Each field includes name, type, and the signing role name.',
|
|
||||||
inputSchema: {
|
|
||||||
type: 'object',
|
|
||||||
properties: {
|
|
||||||
template_id: {
|
|
||||||
type: 'integer',
|
|
||||||
description: 'Template identifier'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
required: %w[template_id]
|
|
||||||
},
|
|
||||||
annotations: {
|
|
||||||
readOnlyHint: true,
|
|
||||||
destructiveHint: false,
|
|
||||||
idempotentHint: true,
|
|
||||||
openWorldHint: false
|
|
||||||
}
|
|
||||||
}.freeze
|
|
||||||
|
|
||||||
module_function
|
|
||||||
|
|
||||||
def call(arguments, _current_user, current_ability)
|
|
||||||
template = Template.accessible_by(current_ability).find_by(id: arguments['template_id'])
|
|
||||||
|
|
||||||
return { content: [{ type: 'text', text: 'Template not found' }], isError: true } unless template
|
|
||||||
|
|
||||||
current_ability.authorize!(:read, template)
|
|
||||||
|
|
||||||
submitters_index = template.submitters.index_by { |s| s['uuid'] }
|
|
||||||
|
|
||||||
roles = template.submitters.pluck('name')
|
|
||||||
|
|
||||||
fields = template.fields.filter_map do |field|
|
|
||||||
next if field['name'].blank?
|
|
||||||
|
|
||||||
{
|
|
||||||
name: field['name'],
|
|
||||||
type: field['type'],
|
|
||||||
role: submitters_index[field['submitter_uuid']]&.dig('name')
|
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
{
|
|
||||||
content: [
|
|
||||||
{
|
|
||||||
type: 'text',
|
|
||||||
text: {
|
|
||||||
id: template.id,
|
|
||||||
name: template.name,
|
|
||||||
roles: roles,
|
|
||||||
fields: fields
|
|
||||||
}.to_json
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||