mirror of https://github.com/docusealco/docuseal
commit
5634b9fd7d
@ -1,62 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class RegistrationsController < Devise::RegistrationsController
|
||||
prepend_before_action :require_no_authentication, only: [:show]
|
||||
prepend_before_action :maybe_redirect_if_signed_in, only: [:show]
|
||||
|
||||
def show; end
|
||||
|
||||
def create
|
||||
super
|
||||
|
||||
Accounts.create_default_template(resource.account) if resource.account.persisted?
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def after_sign_up_path_for(...)
|
||||
if params[:redir].present?
|
||||
return console_redirect_index_path(redir: params[:redir]) if params[:redir].starts_with?(Docuseal::CONSOLE_URL)
|
||||
|
||||
return params[:redir]
|
||||
end
|
||||
|
||||
super
|
||||
end
|
||||
|
||||
def maybe_redirect_if_signed_in
|
||||
return unless signed_in?
|
||||
return if params[:redir].blank?
|
||||
|
||||
redirect_to after_sign_up_path_for(current_user), allow_other_host: true
|
||||
end
|
||||
|
||||
def set_flash_message(key, kind, options = {})
|
||||
return if key == :alert && kind == 'already_authenticated'
|
||||
|
||||
super
|
||||
end
|
||||
|
||||
def build_resource(_hash = {})
|
||||
account = Account.new(account_params)
|
||||
account.timezone = Accounts.normalize_timezone(account.timezone)
|
||||
|
||||
self.resource = account.users.new(user_params)
|
||||
|
||||
account.name ||= resource.full_name if params[:action] == 'create'
|
||||
end
|
||||
|
||||
def user_params
|
||||
return {} if params[:user].blank?
|
||||
|
||||
params.require(:user).permit(:first_name, :last_name, :email, :password).compact_blank.tap do |attrs|
|
||||
attrs[:password] ||= SecureRandom.hex if params[:action] == 'create'
|
||||
end
|
||||
end
|
||||
|
||||
def account_params
|
||||
return {} if params[:account].blank?
|
||||
|
||||
params.require(:account).permit(:name, :timezone).compact_blank
|
||||
end
|
||||
end
|
||||
@ -0,0 +1,33 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class UserConfigsController < ApplicationController
|
||||
before_action :load_user_config
|
||||
authorize_resource :user_config
|
||||
|
||||
ALLOWED_KEYS = [
|
||||
UserConfig::RECEIVE_COMPLETED_EMAIL
|
||||
].freeze
|
||||
|
||||
InvalidKey = Class.new(StandardError)
|
||||
|
||||
def create
|
||||
@user_config.update!(user_config_params)
|
||||
|
||||
head :ok
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def load_user_config
|
||||
raise InvalidKey unless ALLOWED_KEYS.include?(user_config_params[:key])
|
||||
|
||||
@user_config =
|
||||
UserConfig.find_or_initialize_by(user: current_user, key: user_config_params[:key])
|
||||
end
|
||||
|
||||
def user_config_params
|
||||
params.required(:user_config).permit!.tap do |attrs|
|
||||
attrs[:value] = attrs[:value] == '1' if attrs[:value].in?(%w[1 0])
|
||||
end
|
||||
end
|
||||
end
|
||||
@ -0,0 +1,56 @@
|
||||
const emailRegexp = /([^@;,<>\s]+@[^@;,<>\s]+)/g
|
||||
|
||||
export default class extends HTMLElement {
|
||||
connectedCallback () {
|
||||
if (this.dataset.limit) {
|
||||
this.textarea.addEventListener('input', () => {
|
||||
const emails = this.textarea.value.match(emailRegexp) || []
|
||||
|
||||
this.updateCounter(emails.length)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
updateCounter (count) {
|
||||
let counter = document.getElementById('emails_counter')
|
||||
let bulkMessage = document.getElementById('bulk_message')
|
||||
|
||||
if (count < 2) {
|
||||
counter?.remove()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if ((count + 10) > this.dataset.limit) {
|
||||
if (!counter) {
|
||||
counter = document.createElement('span')
|
||||
|
||||
counter.id = 'emails_counter'
|
||||
counter.classList.add('text-xs', 'right-0', 'absolute')
|
||||
counter.style.bottom = '-15px'
|
||||
|
||||
this.textarea.parentNode.append(counter)
|
||||
}
|
||||
|
||||
counter.innerText = `${count} / ${this.dataset.limit}`
|
||||
}
|
||||
|
||||
if (this.dataset.bulkEnabled !== 'true') {
|
||||
if (!bulkMessage) {
|
||||
bulkMessage = document.createElement('span')
|
||||
|
||||
bulkMessage.id = 'bulk_message'
|
||||
bulkMessage.classList.add('text-xs', 'left-0', 'absolute')
|
||||
bulkMessage.style.bottom = '-15px'
|
||||
|
||||
this.textarea.parentNode.append(bulkMessage)
|
||||
}
|
||||
|
||||
bulkMessage.innerHTML = '<a class="link" data-turbo="false" href="/upgrade">Upgrade</a> to bulk send multiple recipients'
|
||||
}
|
||||
}
|
||||
|
||||
get textarea () {
|
||||
return this.querySelector('textarea')
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,97 @@
|
||||
<template>
|
||||
<template
|
||||
v-for="(field, fieldIndex) in fields"
|
||||
:key="field.uuid"
|
||||
>
|
||||
<template
|
||||
v-for="(area, areaIndex) in field.areas"
|
||||
:key="areaIndex"
|
||||
>
|
||||
<Teleport
|
||||
v-if="findPageElementForArea(area)"
|
||||
:to="findPageElementForArea(area)"
|
||||
>
|
||||
<FieldArea
|
||||
v-if="isMathLoaded"
|
||||
:model-value="calculateFormula(field)"
|
||||
:field="field"
|
||||
:area="area"
|
||||
:submittable="false"
|
||||
:field-index="fieldIndex"
|
||||
/>
|
||||
</Teleport>
|
||||
</template>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import FieldArea from './area'
|
||||
|
||||
export default {
|
||||
name: 'FormulaFieldAreas',
|
||||
components: {
|
||||
FieldArea
|
||||
},
|
||||
props: {
|
||||
fields: {
|
||||
type: Array,
|
||||
required: false,
|
||||
default: () => []
|
||||
},
|
||||
values: {
|
||||
type: Object,
|
||||
required: false,
|
||||
default: () => ({})
|
||||
}
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
isMathLoaded: false
|
||||
}
|
||||
},
|
||||
async mounted () {
|
||||
const {
|
||||
create,
|
||||
evaluateDependencies,
|
||||
addDependencies,
|
||||
subtractDependencies,
|
||||
divideDependencies,
|
||||
multiplyDependencies,
|
||||
powDependencies,
|
||||
roundDependencies,
|
||||
absDependencies,
|
||||
sinDependencies,
|
||||
tanDependencies,
|
||||
cosDependencies
|
||||
} = await import('mathjs')
|
||||
|
||||
this.math = create({
|
||||
evaluateDependencies,
|
||||
addDependencies,
|
||||
subtractDependencies,
|
||||
divideDependencies,
|
||||
multiplyDependencies,
|
||||
powDependencies,
|
||||
roundDependencies,
|
||||
absDependencies,
|
||||
sinDependencies,
|
||||
tanDependencies,
|
||||
cosDependencies
|
||||
})
|
||||
|
||||
this.isMathLoaded = true
|
||||
},
|
||||
methods: {
|
||||
findPageElementForArea (area) {
|
||||
return (this.$root.$el?.parentNode?.getRootNode() || document).getElementById(`page-${area.attachment_uuid}-${area.page}`)
|
||||
},
|
||||
calculateFormula (field) {
|
||||
const transformedFormula = field.preferences.formula.replace(/{{(.*?)}}/g, (match, uuid) => {
|
||||
return this.values[uuid] || 0.0
|
||||
})
|
||||
|
||||
return this.math.evaluate(transformedFormula.toLowerCase())
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@ -0,0 +1,75 @@
|
||||
<template>
|
||||
<span>
|
||||
<template
|
||||
v-for="(item, index) in items"
|
||||
:key="index"
|
||||
>
|
||||
<a
|
||||
v-if="item.startsWith('<a') && item.endsWith('</a>')"
|
||||
:href="sanitizeHref(extractAttr(item, 'href'))"
|
||||
rel="noopener noreferrer nofollow"
|
||||
:class="extractAttr(item, 'class') || 'link'"
|
||||
target="_blank"
|
||||
>
|
||||
{{ extractText(item) }}
|
||||
</a>
|
||||
<b
|
||||
v-else-if="item.startsWith('<b>') || item.startsWith('<strong>')"
|
||||
>
|
||||
{{ extractText(item) }}
|
||||
</b>
|
||||
<i
|
||||
v-else-if="item.startsWith('<i>') || item.startsWith('<em>')"
|
||||
>
|
||||
{{ extractText(item) }}
|
||||
</i>
|
||||
<br
|
||||
v-else-if="item === '<br>' || item === '\n'"
|
||||
>
|
||||
<template
|
||||
v-else
|
||||
>
|
||||
{{ item }}
|
||||
</template>
|
||||
</template>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import snarkdown from 'snarkdown'
|
||||
|
||||
const htmlSplitRegexp = /(<a.+?<\/a>|<i>.+?<\/i>|<b>.+?<\/b>|<em>.+?<\/em>|<strong>.+?<\/strong>|<br>)/
|
||||
|
||||
export default {
|
||||
name: 'MarkdownContent',
|
||||
props: {
|
||||
string: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: ''
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
items () {
|
||||
return snarkdown(this.string.replace(/\n/g, '<br>')).split(htmlSplitRegexp)
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
sanitizeHref (href) {
|
||||
if (href && href.trim().match(/^((?:https?:\/\/)|\/)/)) {
|
||||
return href.replace(/javascript:/g, '')
|
||||
}
|
||||
},
|
||||
extractAttr (text, attr) {
|
||||
if (text.includes(attr)) {
|
||||
return text.split(attr).pop().split('"')[1]
|
||||
}
|
||||
},
|
||||
extractText (text) {
|
||||
if (text) {
|
||||
return text.match(/>(.+?)</)?.[1]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@ -0,0 +1,86 @@
|
||||
<template>
|
||||
<label
|
||||
v-if="showFieldNames && (field.name || field.title)"
|
||||
:for="field.uuid"
|
||||
dir="auto"
|
||||
class="label text-2xl"
|
||||
:class="{ 'mb-2': !field.description }"
|
||||
><MarkdownContent
|
||||
v-if="field.title"
|
||||
:string="field.title"
|
||||
/>
|
||||
<template v-else>{{ field.name }}</template>
|
||||
<template v-if="!field.required">({{ t('optional') }})</template>
|
||||
</label>
|
||||
<div
|
||||
v-else
|
||||
class="py-1"
|
||||
/>
|
||||
<div
|
||||
v-if="field.description"
|
||||
dir="auto"
|
||||
class="mb-3 px-1"
|
||||
>
|
||||
<MarkdownContent :string="field.description" />
|
||||
</div>
|
||||
<AppearsOn :field="field" />
|
||||
<div class="items-center flex">
|
||||
<input
|
||||
type="hidden"
|
||||
name="cast_number"
|
||||
value="true"
|
||||
>
|
||||
<input
|
||||
:id="field.uuid"
|
||||
v-model="number"
|
||||
type="number"
|
||||
class="base-input !text-2xl w-full"
|
||||
step="any"
|
||||
:required="field.required"
|
||||
:placeholder="`${t('type_here_')}${field.required ? '' : ` (${t('optional')})`}`"
|
||||
:name="`values[${field.uuid}]`"
|
||||
@focus="$emit('focus')"
|
||||
>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import AppearsOn from './appears_on'
|
||||
import MarkdownContent from './markdown_content'
|
||||
|
||||
export default {
|
||||
name: 'TextStep',
|
||||
components: {
|
||||
AppearsOn,
|
||||
MarkdownContent
|
||||
},
|
||||
inject: ['t'],
|
||||
props: {
|
||||
field: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
showFieldNames: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: true
|
||||
},
|
||||
modelValue: {
|
||||
type: [String, Number],
|
||||
required: false,
|
||||
default: ''
|
||||
}
|
||||
},
|
||||
emits: ['update:model-value', 'focus'],
|
||||
computed: {
|
||||
number: {
|
||||
set (value) {
|
||||
this.$emit('update:model-value', value)
|
||||
},
|
||||
get () {
|
||||
return this.modelValue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@ -0,0 +1,195 @@
|
||||
<template>
|
||||
<div
|
||||
class="modal modal-open items-start !animate-none overflow-y-auto"
|
||||
>
|
||||
<div
|
||||
class="absolute top-0 bottom-0 right-0 left-0"
|
||||
@click.prevent="$emit('close')"
|
||||
/>
|
||||
<div class="modal-box pt-4 pb-6 px-6 mt-20 max-h-none w-full max-w-xl">
|
||||
<div class="flex justify-between items-center border-b pb-2 mb-2 font-medium">
|
||||
<span>
|
||||
{{ t('condition') }} - {{ field.name || buildDefaultName(field, template.fields) }}
|
||||
</span>
|
||||
<a
|
||||
href="#"
|
||||
class="text-xl"
|
||||
@click.prevent="$emit('close')"
|
||||
>×</a>
|
||||
</div>
|
||||
<div>
|
||||
<div
|
||||
v-if="!withConditions"
|
||||
class="bg-base-300 rounded-xl py-2 px-3 text-center"
|
||||
>
|
||||
<a
|
||||
href="https://www.docuseal.co/pricing"
|
||||
target="_blank"
|
||||
class="link"
|
||||
>Available in Pro</a>
|
||||
</div>
|
||||
<form @submit.prevent="validateSaveAndClose">
|
||||
<div class="my-4">
|
||||
<div class="space-y-4">
|
||||
<select
|
||||
class="select select-bordered select-sm w-full bg-white h-11 pl-4 text-base font-normal"
|
||||
required
|
||||
@change="[
|
||||
newCondition.field_uuid = $event.target.value,
|
||||
delete newCondition.value,
|
||||
(conditionActions.includes(newCondition.action) ? '' : newCondition.action = conditionActions[0])
|
||||
]"
|
||||
>
|
||||
<option
|
||||
value=""
|
||||
disabled
|
||||
:selected="!newCondition.field_uuid"
|
||||
>
|
||||
{{ t('select_field_') }}
|
||||
</option>
|
||||
<option
|
||||
v-for="f in fields"
|
||||
:key="f.uuid"
|
||||
:value="f.uuid"
|
||||
:selected="newCondition.field_uuid === f.uuid"
|
||||
>
|
||||
{{ f.name || buildDefaultName(f, template.fields) }}
|
||||
</option>
|
||||
</select>
|
||||
<select
|
||||
v-model="newCondition.action"
|
||||
class="select select-bordered select-sm w-full h-11 pl-4 text-base font-normal"
|
||||
:class="{ 'bg-white': newCondition.field_uuid, 'bg-base-300': !newCondition.field_uuid }"
|
||||
:required="newCondition.field_uuid"
|
||||
>
|
||||
<option
|
||||
v-for="action in conditionActions"
|
||||
:key="action"
|
||||
:value="action"
|
||||
>
|
||||
{{ t(action) }}
|
||||
</option>
|
||||
</select>
|
||||
<select
|
||||
v-if="conditionField?.options?.length"
|
||||
class="select select-bordered select-sm w-full bg-white h-11 pl-4 text-base font-normal"
|
||||
required
|
||||
@change="newCondition.value = $event.target.value"
|
||||
>
|
||||
<option
|
||||
value=""
|
||||
disabled
|
||||
selected
|
||||
>
|
||||
{{ t('select_value_') }}
|
||||
</option>
|
||||
<option
|
||||
v-for="(option, index) in conditionField.options"
|
||||
:key="option.uuid"
|
||||
:value="option.uuid"
|
||||
:selected="newCondition.value === option.uuid"
|
||||
>
|
||||
{{ option.value || `${t('option')} ${index + 1}` }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
class="base-button w-full mt-2"
|
||||
>
|
||||
{{ t('save') }}
|
||||
</button>
|
||||
</form>
|
||||
<div
|
||||
v-if="field.conditions?.[0]?.field_uuid"
|
||||
class="text-center w-full mt-4"
|
||||
>
|
||||
<button
|
||||
class="link"
|
||||
@click="[newCondition.field_uuid = null, delete field.conditions, validateSaveAndClose()]"
|
||||
>
|
||||
{{ t('remove_condition') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'ConditionModal',
|
||||
inject: ['t', 'save', 'template', 'withConditions'],
|
||||
props: {
|
||||
field: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
buildDefaultName: {
|
||||
type: Function,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
emits: ['close'],
|
||||
data () {
|
||||
return {
|
||||
newCondition: this.field.conditions?.[0] || {}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
conditionField () {
|
||||
return this.fields.find((f) => f.uuid === this.newCondition.field_uuid)
|
||||
},
|
||||
conditionActions () {
|
||||
return this.fieldActions(this.conditionField)
|
||||
},
|
||||
fields () {
|
||||
return this.template.fields.reduce((acc, f) => {
|
||||
if (f !== this.field && f.submitter_uuid === this.field.submitter_uuid) {
|
||||
acc.push(f)
|
||||
}
|
||||
|
||||
return acc
|
||||
}, [])
|
||||
}
|
||||
},
|
||||
created () {
|
||||
this.field.conditions ||= []
|
||||
},
|
||||
methods: {
|
||||
fieldActions (field) {
|
||||
const actions = []
|
||||
|
||||
if (!field) {
|
||||
return actions
|
||||
}
|
||||
|
||||
if (field.type === 'checkbox') {
|
||||
actions.push('checked', 'unchecked')
|
||||
} else if (['radio', 'select'].includes(field.type)) {
|
||||
actions.push('equal', 'not_equal')
|
||||
} else if (['multiple'].includes(field.type)) {
|
||||
actions.push('contains', 'does_not_contain')
|
||||
} else {
|
||||
actions.push('not_empty', 'empty')
|
||||
}
|
||||
|
||||
return actions
|
||||
},
|
||||
validateSaveAndClose () {
|
||||
if (!this.withConditions) {
|
||||
return alert('Available only in Pro')
|
||||
}
|
||||
|
||||
if (this.newCondition.field_uuid) {
|
||||
this.field.conditions = [this.newCondition]
|
||||
} else {
|
||||
delete this.field.conditions
|
||||
}
|
||||
|
||||
this.save()
|
||||
this.$emit('close')
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@ -0,0 +1,107 @@
|
||||
<template>
|
||||
<div
|
||||
class="modal modal-open items-start !animate-none overflow-y-auto"
|
||||
>
|
||||
<div
|
||||
class="absolute top-0 bottom-0 right-0 left-0"
|
||||
@click.prevent="$emit('close')"
|
||||
/>
|
||||
<div class="modal-box pt-4 pb-6 px-6 mt-20 max-h-none w-full max-w-xl">
|
||||
<div class="flex justify-between items-center border-b pb-2 mb-2 font-medium">
|
||||
<span>
|
||||
{{ field.name || buildDefaultName(field, template.fields) }}
|
||||
</span>
|
||||
<a
|
||||
href="#"
|
||||
class="text-xl"
|
||||
@click.prevent="$emit('close')"
|
||||
>×</a>
|
||||
</div>
|
||||
<div>
|
||||
<form @submit.prevent="saveAndClose">
|
||||
<div class="space-y-1 mb-1">
|
||||
<div>
|
||||
<label
|
||||
dir="auto"
|
||||
class="label text-sm"
|
||||
for="description_field"
|
||||
>
|
||||
{{ t('description') }}
|
||||
</label>
|
||||
<textarea
|
||||
id="description_field"
|
||||
ref="textarea"
|
||||
v-model="description"
|
||||
dir="auto"
|
||||
class="base-textarea !text-base w-full"
|
||||
@input="resizeTextarea"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label
|
||||
dir="auto"
|
||||
class="label text-sm"
|
||||
for="title_field"
|
||||
>
|
||||
{{ t('display_title') }} ({{ t('optional') }})
|
||||
</label>
|
||||
<input
|
||||
id="title_field"
|
||||
v-model="title"
|
||||
dir="auto"
|
||||
class="base-input !text-base w-full"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
class="base-button w-full mt-4"
|
||||
>
|
||||
{{ t('save') }}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'DescriptionModal',
|
||||
inject: ['t', 'save', 'template'],
|
||||
props: {
|
||||
field: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
buildDefaultName: {
|
||||
type: Function,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
emits: ['close'],
|
||||
data () {
|
||||
return {
|
||||
description: this.field.description,
|
||||
title: this.field.title
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
this.resizeTextarea()
|
||||
},
|
||||
methods: {
|
||||
saveAndClose () {
|
||||
this.field.description = this.description
|
||||
this.field.title = this.title
|
||||
|
||||
this.save()
|
||||
this.$emit('close')
|
||||
},
|
||||
resizeTextarea () {
|
||||
const textarea = this.$refs.textarea
|
||||
|
||||
textarea.style.height = 'auto'
|
||||
textarea.style.height = textarea.scrollHeight + 'px'
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@ -0,0 +1,230 @@
|
||||
<template>
|
||||
<div
|
||||
class="modal modal-open items-start !animate-none overflow-y-auto"
|
||||
>
|
||||
<div
|
||||
class="absolute top-0 bottom-0 right-0 left-0"
|
||||
@click.prevent="$emit('close')"
|
||||
/>
|
||||
<div class="modal-box pt-4 pb-6 px-6 mt-20 max-h-none w-full max-w-xl">
|
||||
<div class="flex justify-between items-center border-b pb-2 mb-2 font-medium">
|
||||
<span>
|
||||
{{ t('formula') }} - {{ field.name || buildDefaultName(field, template.fields) }}
|
||||
</span>
|
||||
<a
|
||||
href="#"
|
||||
class="text-xl"
|
||||
@click.prevent="$emit('close')"
|
||||
>×</a>
|
||||
</div>
|
||||
<div>
|
||||
<div
|
||||
v-if="!withFormula"
|
||||
class="bg-base-300 rounded-xl py-2 px-3 text-center"
|
||||
>
|
||||
<a
|
||||
href="https://www.docuseal.co/pricing"
|
||||
target="_blank"
|
||||
class="link"
|
||||
>Available in Pro</a>
|
||||
</div>
|
||||
<div class="flex-inline mb-2 gap-2 space-y-1">
|
||||
<button
|
||||
v-for="f in fields"
|
||||
:key="f.uuid"
|
||||
class="mr-1 btn btn-neutral btn-outline border-base-content/20 btn-sm normal-case font-normal bg-white !rounded-xl"
|
||||
@click.prevent="insertTextUnderCursor(`{{${f.name || buildDefaultName(f, template.fields)}}}`)"
|
||||
>
|
||||
<IconCodePlus
|
||||
width="20"
|
||||
height="20"
|
||||
stroke-width="1.5"
|
||||
/>
|
||||
{{ f.name || buildDefaultName(f, template.fields) }}
|
||||
</button>
|
||||
</div>
|
||||
<div>
|
||||
<div class="flex">
|
||||
<textarea
|
||||
ref="textarea"
|
||||
v-model="formula"
|
||||
class="base-textarea !rounded-xl !text-base font-mono w-full !outline-0 !ring-0 !px-3"
|
||||
required="true"
|
||||
@input="resizeTextarea"
|
||||
/>
|
||||
</div>
|
||||
<div class="mb-3 mt-1">
|
||||
<div
|
||||
target="blank"
|
||||
class="text-sm mb-2 inline space-x-2"
|
||||
>
|
||||
<button
|
||||
class="bg-base-200 px-2 rounded-xl"
|
||||
@click="insertTextUnderCursor(' + ')"
|
||||
>
|
||||
+
|
||||
</button>
|
||||
<button
|
||||
class="bg-base-200 px-2 rounded-xl"
|
||||
@click="insertTextUnderCursor(' - ')"
|
||||
>
|
||||
-
|
||||
</button>
|
||||
<button
|
||||
class="bg-base-200 px-2 rounded-xl"
|
||||
@click="insertTextUnderCursor(' * ')"
|
||||
>
|
||||
*
|
||||
</button>
|
||||
<button
|
||||
class="bg-base-200 px-2 rounded-xl"
|
||||
@click="insertTextUnderCursor(' / ')"
|
||||
>
|
||||
/
|
||||
</button>
|
||||
<button
|
||||
class="bg-base-200 px-2 rounded-xl"
|
||||
@click="insertTextUnderCursor(' % ')"
|
||||
>
|
||||
%
|
||||
</button>
|
||||
<button
|
||||
class="bg-base-200 px-2 rounded-xl"
|
||||
@click="insertTextUnderCursor('^')"
|
||||
>
|
||||
^
|
||||
</button>
|
||||
<button
|
||||
class="bg-base-200 px-2 rounded-xl"
|
||||
@click="insertTextUnderCursor('round()')"
|
||||
>
|
||||
round(n, d)
|
||||
</button>
|
||||
<button
|
||||
class="bg-base-200 px-2 rounded-xl"
|
||||
@click="insertTextUnderCursor('abs()')"
|
||||
>
|
||||
abs(n)
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
class="base-button w-full"
|
||||
@click.prevent="validateSaveAndClose"
|
||||
>
|
||||
{{ t('save') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { IconCodePlus } from '@tabler/icons-vue'
|
||||
|
||||
export default {
|
||||
name: 'FormulaModal',
|
||||
components: {
|
||||
IconCodePlus
|
||||
},
|
||||
inject: ['t', 'save', 'template', 'withFormula'],
|
||||
props: {
|
||||
field: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
buildDefaultName: {
|
||||
type: Function,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
emits: ['close'],
|
||||
data () {
|
||||
return {
|
||||
formula: ''
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
fields () {
|
||||
return this.template.fields.reduce((acc, f) => {
|
||||
if (f !== this.field && f.submitter_uuid === this.field.submitter_uuid && ['number'].includes(f.type) && !f.preferences?.formula) {
|
||||
acc.push(f)
|
||||
}
|
||||
|
||||
return acc
|
||||
}, [])
|
||||
}
|
||||
},
|
||||
created () {
|
||||
this.field.preferences ||= {}
|
||||
},
|
||||
mounted () {
|
||||
this.formula = this.humanizeFormula(this.field.preferences.formula || '')
|
||||
},
|
||||
methods: {
|
||||
humanizeFormula (text) {
|
||||
return text.replace(/{{(.*?)}}/g, (match, uuid) => {
|
||||
const foundField = this.fields.find((f) => f.uuid === uuid)
|
||||
|
||||
if (foundField) {
|
||||
return `{{${foundField.name || this.buildDefaultName(foundField, this.template.fields)}}}`
|
||||
} else {
|
||||
return '{{FIELD NOT FOUND}}'
|
||||
}
|
||||
})
|
||||
},
|
||||
normalizeFormula (text) {
|
||||
return text.replace(/{{(.*?)}}/g, (match, name) => {
|
||||
const foundField = this.fields.find((f) => {
|
||||
return (f.name || this.buildDefaultName(f, this.template.fields)).trim() === name.trim()
|
||||
})
|
||||
|
||||
if (foundField) {
|
||||
return `{{${foundField.uuid}}}`
|
||||
} else {
|
||||
return '{{FIELD NOT FOUND}}'
|
||||
}
|
||||
})
|
||||
},
|
||||
validateSaveAndClose () {
|
||||
if (!this.withFormula) {
|
||||
return alert('Available only in Pro')
|
||||
}
|
||||
|
||||
const normalizedFormula = this.normalizeFormula(this.formula)
|
||||
|
||||
if (normalizedFormula.includes('FIELD NOT FOUND')) {
|
||||
alert('Some fields are missing in the formula.')
|
||||
} else {
|
||||
this.field.preferences.formula = normalizedFormula
|
||||
this.field.readonly = !!normalizedFormula
|
||||
|
||||
this.save()
|
||||
|
||||
this.$emit('close')
|
||||
}
|
||||
},
|
||||
insertTextUnderCursor (textToInsert) {
|
||||
const textarea = this.$refs.textarea
|
||||
|
||||
const selectionEnd = textarea.selectionEnd
|
||||
const cursorPos = selectionEnd
|
||||
|
||||
const newText = textarea.value.substring(0, cursorPos) + textToInsert + textarea.value.substring(cursorPos)
|
||||
|
||||
this.formula = newText
|
||||
|
||||
textarea.setSelectionRange(cursorPos + textToInsert.length, cursorPos + textToInsert.length)
|
||||
|
||||
textarea.focus()
|
||||
},
|
||||
resizeTextarea () {
|
||||
const textarea = this.$refs.textarea
|
||||
|
||||
textarea.style.height = 'auto'
|
||||
textarea.style.height = textarea.scrollHeight + 'px'
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@ -1,47 +0,0 @@
|
||||
<div class="max-w-lg mx-auto px-2">
|
||||
<h1 class="text-3xl font-bold text-center mt-8">Profile Details</h1>
|
||||
<%= form_for('', as: resource_name, html: { class: 'space-y-6' }, data: { turbo: params[:redir].blank? }, url: new_registration_path) do |f| %>
|
||||
<% if params[:redir].present? %>
|
||||
<%= hidden_field_tag :redir, params[:redir] %>
|
||||
<% end %>
|
||||
<div class="space-y-2">
|
||||
<%= render 'devise/shared/error_messages', resource: %>
|
||||
<%= f.fields_for resource do |ff| %>
|
||||
<div class="grid gap-2 md:grid-cols-2 md:gap-4">
|
||||
<div class="form-control">
|
||||
<%= ff.label :first_name, class: 'label' %>
|
||||
<%= ff.text_field :first_name, required: true, class: 'base-input' %>
|
||||
</div>
|
||||
<div class="form-control">
|
||||
<%= ff.label :last_name, class: 'label' %>
|
||||
<%= ff.text_field :last_name, required: true, class: 'base-input' %>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
<%= f.fields_for resource do |ff| %>
|
||||
<div class="form-control <%= 'hidden' if (params[:oauth_callback] || params[:sign_up]) && resource.email? %>">
|
||||
<%= ff.label :email, class: 'label' %>
|
||||
<%= ff.email_field :email, value: EmailTypo.call(resource.email), required: true, class: 'base-input' %>
|
||||
</div>
|
||||
<% end %>
|
||||
<%= f.fields_for resource.account do |ff| %>
|
||||
<set-timezone data-input-id="_account_timezone"></set-timezone>
|
||||
<%= ff.hidden_field :timezone %>
|
||||
<div class="form-control">
|
||||
<%= ff.label :name, 'Company name (optional)', class: 'label' %>
|
||||
<%= ff.text_field :name, class: 'base-input' %>
|
||||
</div>
|
||||
<% end %>
|
||||
<%= f.fields_for resource do |ff| %>
|
||||
<div class="form-control <%= 'hidden' if params[:oauth_callback] %>">
|
||||
<%= ff.label :password, class: 'label' %>
|
||||
<%= ff.password_field :password, required: !params[:oauth_callback], class: 'base-input' %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
<div class="form-control">
|
||||
<%= f.button button_title(title: 'Sign up', disabled_with: 'Signing up'), class: 'base-button' %>
|
||||
</div>
|
||||
<% end %>
|
||||
<%= render 'devise/shared/links' %>
|
||||
</div>
|
||||
@ -1,45 +0,0 @@
|
||||
<div class="max-w-lg mx-auto px-2">
|
||||
<% if params[:redir].to_s.ends_with?('on_premise') %>
|
||||
<h1 class="text-3xl font-bold text-center mt-8 mb-6">DocuSeal Console</h1>
|
||||
<div class="alert my-4 text-sm">
|
||||
Sign up in DocuSeal Console to upgrade.
|
||||
On-premises app is completely standalone, Console is used only to manage your license.
|
||||
</div>
|
||||
<% else %>
|
||||
<%= render 'devise/shared/select_server' if Docuseal.multitenant? %>
|
||||
<h1 class="text-3xl font-bold text-center mt-8 mb-6">Create Free Account</h1>
|
||||
<% end %>
|
||||
<%= form_for(User.new, html: { class: 'space-y-6' }, url: new_registration_path, data: { turbo: params[:redir].blank? }, method: :get) do |f| %>
|
||||
<% if params[:redir].present? %>
|
||||
<%= hidden_field_tag :redir, params[:redir] %>
|
||||
<% end %>
|
||||
<div class="space-y-2">
|
||||
<div class="form-control">
|
||||
<%= f.label :email, class: 'label' %>
|
||||
<%= f.email_field :email, autofocus: true, autocomplete: 'email', class: 'base-input', placeholder: 'Enter email to continue' %>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-control">
|
||||
<%= f.button button_title(title: 'Sign up', disabled_with: 'Sign up'), name: 'sign_up', value: true, 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: 'Sign up 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: 'Sign up with Microsoft', icon: svg_icon('brand_microsoft', class: 'w-6 h-6')), class: 'white-button w-full' %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
<%= render 'devise/shared/links' %>
|
||||
</div>
|
||||
@ -0,0 +1 @@
|
||||
<%= render 'form_toggle_options' %>
|
||||
@ -0,0 +1,14 @@
|
||||
<% account_config = AccountConfig.where(account: current_account, key: AccountConfig::FORM_WITH_CONFETTI_KEY).first_or_initialize(value: true) %>
|
||||
<% if can?(:manage, account_config) %>
|
||||
<div class="px-1 mt-2">
|
||||
<%= form_for account_config, url: account_configs_path, method: :post do |f| %>
|
||||
<%= f.hidden_field :key %>
|
||||
<div class="flex items-center justify-between py-2.5">
|
||||
<span>
|
||||
Show confetti on successful completion
|
||||
</span>
|
||||
<%= f.check_box :value, { class: 'toggle', checked: account_config.value != false, onchange: 'this.form.requestSubmit()' }, '1', '0' %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
@ -0,0 +1 @@
|
||||
<%= auto_link(simple_format(h(ReplaceEmailVariables.call(local_assigns[:content], submitter: local_assigns[:submitter], sig: local_assigns[:sig])))) %>
|
||||
@ -1,30 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'rollbar' if ENV.key?('ROLLBAR_ACCESS_TOKEN')
|
||||
|
||||
if defined?(Rollbar)
|
||||
Rollbar.configure do |config|
|
||||
config.access_token = ENV.fetch('ROLLBAR_ACCESS_TOKEN', nil)
|
||||
|
||||
config.transform << proc do |options|
|
||||
data = options[:payload]['data']
|
||||
|
||||
if data[:request]
|
||||
data[:request][:cookies] = {}
|
||||
data[:request][:session] = {}
|
||||
data[:request][:url] =
|
||||
data[:request][:url].to_s.sub(%r{(/[sde]/)\w{8}}, '\1********').sub(/\A(.*?)--(.*)/, '\1--********')
|
||||
end
|
||||
end
|
||||
|
||||
config.enabled = true
|
||||
config.collect_user_ip = false
|
||||
config.anonymize_user_ip = true
|
||||
config.scrub_headers += %w[X-Auth-Token Cookie X-Csrf-Token Referer]
|
||||
config.scrub_fields += %i[slug uuid attachment_uuid]
|
||||
|
||||
config.exception_level_filters['ActionController::RoutingError'] = 'ignore'
|
||||
|
||||
config.environment = ENV['ROLLBAR_ENV'].presence || Rails.env
|
||||
end
|
||||
end
|
||||
@ -0,0 +1,7 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class AddIndexOnSubmissionEvents < ActiveRecord::Migration[7.1]
|
||||
def change
|
||||
add_index :submission_events, :created_at
|
||||
end
|
||||
end
|
||||
@ -0,0 +1,7 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class AddIndexOnBlobsChecksum < ActiveRecord::Migration[7.1]
|
||||
def change
|
||||
add_index :active_storage_blobs, :checksum
|
||||
end
|
||||
end
|
||||
@ -0,0 +1,19 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module RateLimit
|
||||
LimitApproached = Class.new(StandardError)
|
||||
|
||||
STORE = ActiveSupport::Cache::MemoryStore.new
|
||||
|
||||
module_function
|
||||
|
||||
def call(key, limit:, ttl:, enabled: Docuseal.multitenant?)
|
||||
return true unless enabled
|
||||
|
||||
value = STORE.increment(key, 1, expires_in: ttl)
|
||||
|
||||
raise LimitApproached if value > limit
|
||||
|
||||
true
|
||||
end
|
||||
end
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue