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