mirror of
https://github.com/docusealco/docuseal
synced 2026-11-09 07:42:32 -05:00
date formulas
This commit is contained in:
1 parent
ca7b9671f2
commit
439f95d87e
10 files changed
+150
-25
No files matched your search
@@ -125,6 +125,11 @@ export default {
|
|||||||
required: false,
|
required: false,
|
||||||
default: () => []
|
default: () => []
|
||||||
},
|
},
|
||||||
|
fieldsUuidIndex: {
|
||||||
|
type: Object,
|
||||||
|
required: false,
|
||||||
|
default: () => ({})
|
||||||
|
},
|
||||||
values: {
|
values: {
|
||||||
type: Object,
|
type: Object,
|
||||||
required: false,
|
required: false,
|
||||||
@@ -195,7 +200,7 @@ export default {
|
|||||||
},
|
},
|
||||||
isMobileContainer: FieldAreas.computed.isMobileContainer,
|
isMobileContainer: FieldAreas.computed.isMobileContainer,
|
||||||
isInlineSize: FieldAreas.computed.isInlineSize,
|
isInlineSize: FieldAreas.computed.isInlineSize,
|
||||||
fieldsUuidIndex () {
|
formulaFieldsUuidIndex () {
|
||||||
return this.formulaFields.reduce((acc, field) => {
|
return this.formulaFields.reduce((acc, field) => {
|
||||||
acc[field.uuid] = field
|
acc[field.uuid] = field
|
||||||
|
|
||||||
@@ -289,6 +294,7 @@ export default {
|
|||||||
methods: {
|
methods: {
|
||||||
normalizeFormula: FormulaAreas.methods.normalizeFormula,
|
normalizeFormula: FormulaAreas.methods.normalizeFormula,
|
||||||
calculateFormula: FormulaAreas.methods.calculateFormula,
|
calculateFormula: FormulaAreas.methods.calculateFormula,
|
||||||
|
dateFormulaValue: FormulaAreas.methods.dateFormulaValue,
|
||||||
evalTextFormula: FormulaAreas.methods.evalTextFormula,
|
evalTextFormula: FormulaAreas.methods.evalTextFormula,
|
||||||
scrollInContainer: FieldAreas.methods.scrollInContainer,
|
scrollInContainer: FieldAreas.methods.scrollInContainer,
|
||||||
scrollIntoArea: FieldAreas.methods.scrollIntoArea,
|
scrollIntoArea: FieldAreas.methods.scrollIntoArea,
|
||||||
|
|||||||
@@ -633,6 +633,32 @@ defineFunction('contains', {
|
|||||||
return String(v[0]).indexOf(String(v[1])) !== -1
|
return String(v[0]).indexOf(String(v[1])) !== -1
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
defineFunction('today', {
|
||||||
|
min: 0,
|
||||||
|
max: 0,
|
||||||
|
call: () => {
|
||||||
|
const now = new Date()
|
||||||
|
return Date.UTC(now.getFullYear(), now.getMonth(), now.getDate()) / 86400000
|
||||||
|
}
|
||||||
|
})
|
||||||
|
defineFunction('datedif', {
|
||||||
|
min: 3,
|
||||||
|
max: 3,
|
||||||
|
call: (v) => {
|
||||||
|
if (v[0] == null || v[1] == null) return null
|
||||||
|
const start = toNumber(v[0])
|
||||||
|
const end = toNumber(v[1])
|
||||||
|
const unit = String(v[2]).toLowerCase()
|
||||||
|
if (unit === 'd') return end - start
|
||||||
|
const from = new Date(Math.min(start, end) * 86400000)
|
||||||
|
const to = new Date(Math.max(start, end) * 86400000)
|
||||||
|
const months = (to.getUTCFullYear() - from.getUTCFullYear()) * 12 + to.getUTCMonth() - from.getUTCMonth() - (to.getUTCDate() < from.getUTCDate() ? 1 : 0)
|
||||||
|
const sign = end < start ? -1 : 1
|
||||||
|
if (unit === 'm') return sign * months
|
||||||
|
if (unit === 'y') return sign * Math.floor(months / 12)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
const OP_CLASSES = {
|
const OP_CLASSES = {
|
||||||
add: Addition,
|
add: Addition,
|
||||||
|
|||||||
@@ -31,6 +31,7 @@
|
|||||||
:readonly-conditional-fields="readonlyConditionalFields"
|
:readonly-conditional-fields="readonlyConditionalFields"
|
||||||
:readonly-conditional-field-values="readonlyConditionalFieldValues"
|
:readonly-conditional-field-values="readonlyConditionalFieldValues"
|
||||||
:formula-fields="formulaFields"
|
:formula-fields="formulaFields"
|
||||||
|
:fields-uuid-index="fieldsUuidIndex"
|
||||||
:values="values"
|
:values="values"
|
||||||
:readonly-values="readonlyFieldValues"
|
:readonly-values="readonlyFieldValues"
|
||||||
:submitter="submitter"
|
:submitter="submitter"
|
||||||
@@ -71,6 +72,7 @@
|
|||||||
<FormulaFieldAreas
|
<FormulaFieldAreas
|
||||||
v-if="!withAccessibilityAreas && !isAccessibilityMode && formulaFields.length"
|
v-if="!withAccessibilityAreas && !isAccessibilityMode && formulaFields.length"
|
||||||
:fields="formulaFields"
|
:fields="formulaFields"
|
||||||
|
:fields-uuid-index="fieldsUuidIndex"
|
||||||
:readonly-values="readonlyFieldValues"
|
:readonly-values="readonlyFieldValues"
|
||||||
:values="values"
|
:values="values"
|
||||||
/>
|
/>
|
||||||
@@ -560,6 +562,7 @@
|
|||||||
:field="currentField"
|
:field="currentField"
|
||||||
:submitter-slug="submitterSlug"
|
:submitter-slug="submitterSlug"
|
||||||
:fields="formulaFields"
|
:fields="formulaFields"
|
||||||
|
:fields-uuid-index="fieldsUuidIndex"
|
||||||
:values="values"
|
:values="values"
|
||||||
:readonly-values="readonlyFieldValues"
|
:readonly-values="readonlyFieldValues"
|
||||||
:fetch-options="fetchOptions"
|
:fetch-options="fetchOptions"
|
||||||
|
|||||||
@@ -48,6 +48,11 @@ export default {
|
|||||||
type: Object,
|
type: Object,
|
||||||
required: false,
|
required: false,
|
||||||
default: () => ({})
|
default: () => ({})
|
||||||
|
},
|
||||||
|
fieldsUuidIndex: {
|
||||||
|
type: Object,
|
||||||
|
required: false,
|
||||||
|
default: () => ({})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
data () {
|
data () {
|
||||||
@@ -59,7 +64,7 @@ export default {
|
|||||||
isInlineSize () {
|
isInlineSize () {
|
||||||
return CSS.supports('container-type: size')
|
return CSS.supports('container-type: size')
|
||||||
},
|
},
|
||||||
fieldsUuidIndex () {
|
formulaFieldsUuidIndex () {
|
||||||
return this.fields.reduce((acc, field) => {
|
return this.fields.reduce((acc, field) => {
|
||||||
acc[field.uuid] = field
|
acc[field.uuid] = field
|
||||||
|
|
||||||
@@ -82,8 +87,8 @@ export default {
|
|||||||
if (depth > 10) return formula
|
if (depth > 10) return formula
|
||||||
|
|
||||||
return formula.replace(/{{(.*?)}}/g, (match, uuid) => {
|
return formula.replace(/{{(.*?)}}/g, (match, uuid) => {
|
||||||
if (this.fieldsUuidIndex[uuid]) {
|
if (this.formulaFieldsUuidIndex[uuid]) {
|
||||||
return `(${this.normalizeFormula(this.fieldsUuidIndex[uuid].preferences.formula, depth + 1)})`
|
return `(${this.normalizeFormula(this.formulaFieldsUuidIndex[uuid].preferences.formula, depth + 1)})`
|
||||||
} else {
|
} else {
|
||||||
return match
|
return match
|
||||||
}
|
}
|
||||||
@@ -91,16 +96,36 @@ export default {
|
|||||||
},
|
},
|
||||||
calculateFormula (field) {
|
calculateFormula (field) {
|
||||||
const transformedFormula = this.normalizeFormula(field.preferences.formula).replace(/{{(.*?)}}/g, (match, uuid) => {
|
const transformedFormula = this.normalizeFormula(field.preferences.formula).replace(/{{(.*?)}}/g, (match, uuid) => {
|
||||||
return this.readonlyValues[uuid] || this.values[uuid] || 0.0
|
const value = this.readonlyValues[uuid] || this.values[uuid]
|
||||||
|
|
||||||
|
if (this.fieldsUuidIndex[uuid]?.type === 'date') {
|
||||||
|
return this.dateFormulaValue(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
return value || 0.0
|
||||||
})
|
})
|
||||||
|
|
||||||
return this.math.evaluate(transformedFormula.toLowerCase())
|
const value = this.math.evaluate(transformedFormula.toLowerCase())
|
||||||
|
|
||||||
|
if (field.type === 'date') {
|
||||||
|
return Number.isFinite(value) ? new Date(Math.floor(value) * 86400000).toISOString().slice(0, 10) : ''
|
||||||
|
}
|
||||||
|
|
||||||
|
return value
|
||||||
|
},
|
||||||
|
dateFormulaValue (value) {
|
||||||
|
if (!value) return 'null'
|
||||||
|
if (value === '{{date}}') return 'today()'
|
||||||
|
|
||||||
|
const [year, month, day = 1] = value.slice(0, 10).split('-').map(Number)
|
||||||
|
|
||||||
|
return Date.UTC(year, month - 1, day) / 86400000
|
||||||
},
|
},
|
||||||
evalTextFormula (field, depth = 0) {
|
evalTextFormula (field, depth = 0) {
|
||||||
if (depth > 10) return ''
|
if (depth > 10) return ''
|
||||||
|
|
||||||
return field.preferences.formula.replace(/{{(.*?)}}/g, (match, uuid) => {
|
return field.preferences.formula.replace(/{{(.*?)}}/g, (match, uuid) => {
|
||||||
const formulaField = this.fieldsUuidIndex[uuid]
|
const formulaField = this.formulaFieldsUuidIndex[uuid]
|
||||||
|
|
||||||
if (formulaField?.preferences?.formula) {
|
if (formulaField?.preferences?.formula) {
|
||||||
if (formulaField.type === 'text') {
|
if (formulaField.type === 'text') {
|
||||||
|
|||||||
@@ -104,6 +104,11 @@ export default {
|
|||||||
required: false,
|
required: false,
|
||||||
default: () => ({})
|
default: () => ({})
|
||||||
},
|
},
|
||||||
|
fieldsUuidIndex: {
|
||||||
|
type: Object,
|
||||||
|
required: false,
|
||||||
|
default: () => ({})
|
||||||
|
},
|
||||||
values: {
|
values: {
|
||||||
type: Object,
|
type: Object,
|
||||||
required: true
|
required: true
|
||||||
@@ -137,7 +142,7 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
fieldsUuidIndex () {
|
formulaFieldsUuidIndex () {
|
||||||
return this.fields.reduce((acc, field) => {
|
return this.fields.reduce((acc, field) => {
|
||||||
acc[field.uuid] = field
|
acc[field.uuid] = field
|
||||||
|
|
||||||
@@ -216,17 +221,31 @@ export default {
|
|||||||
},
|
},
|
||||||
calculateFormula () {
|
calculateFormula () {
|
||||||
const transformedFormula = this.normalizeFormula(this.field.preferences.formula).replace(/{{(.*?)}}/g, (match, uuid) => {
|
const transformedFormula = this.normalizeFormula(this.field.preferences.formula).replace(/{{(.*?)}}/g, (match, uuid) => {
|
||||||
return this.readonlyValues[uuid] || this.values[uuid] || 0.0
|
const value = this.readonlyValues[uuid] || this.values[uuid]
|
||||||
|
|
||||||
|
if (this.fieldsUuidIndex[uuid]?.type === 'date') {
|
||||||
|
return this.dateFormulaValue(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
return value || 0.0
|
||||||
})
|
})
|
||||||
|
|
||||||
return this.math.evaluate(transformedFormula.toLowerCase())
|
return this.math.evaluate(transformedFormula.toLowerCase())
|
||||||
},
|
},
|
||||||
|
dateFormulaValue (value) {
|
||||||
|
if (!value) return 'null'
|
||||||
|
if (value === '{{date}}') return 'today()'
|
||||||
|
|
||||||
|
const [year, month, day = 1] = value.slice(0, 10).split('-').map(Number)
|
||||||
|
|
||||||
|
return Date.UTC(year, month - 1, day) / 86400000
|
||||||
|
},
|
||||||
normalizeFormula (formula, depth = 0) {
|
normalizeFormula (formula, depth = 0) {
|
||||||
if (depth > 10) return formula
|
if (depth > 10) return formula
|
||||||
|
|
||||||
return formula.replace(/{{(.*?)}}/g, (match, uuid) => {
|
return formula.replace(/{{(.*?)}}/g, (match, uuid) => {
|
||||||
if (this.fieldsUuidIndex[uuid]) {
|
if (this.formulaFieldsUuidIndex[uuid]) {
|
||||||
return `(${this.normalizeFormula(this.fieldsUuidIndex[uuid].preferences.formula, depth + 1)})`
|
return `(${this.normalizeFormula(this.formulaFieldsUuidIndex[uuid].preferences.formula, depth + 1)})`
|
||||||
} else {
|
} else {
|
||||||
return match
|
return match
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1799,10 +1799,28 @@ export default {
|
|||||||
},
|
},
|
||||||
calculateFormula (field) {
|
calculateFormula (field) {
|
||||||
const transformedFormula = this.normalizeFormula(field.preferences.formula).replace(/{{(.*?)}}/g, (match, uuid) => {
|
const transformedFormula = this.normalizeFormula(field.preferences.formula).replace(/{{(.*?)}}/g, (match, uuid) => {
|
||||||
|
if (this.fieldsUuidIndex[uuid]?.type === 'date') {
|
||||||
|
return this.dateFormulaValue(this.fieldsUuidIndex[uuid].default_value)
|
||||||
|
}
|
||||||
|
|
||||||
return this.fieldsUuidIndex[uuid]?.default_value || 0.0
|
return this.fieldsUuidIndex[uuid]?.default_value || 0.0
|
||||||
})
|
})
|
||||||
|
|
||||||
return this.math.evaluate(transformedFormula.toLowerCase())
|
const value = this.math.evaluate(transformedFormula.toLowerCase())
|
||||||
|
|
||||||
|
if (field.type === 'date') {
|
||||||
|
return Number.isFinite(value) ? new Date(Math.floor(value) * 86400000).toISOString().slice(0, 10) : ''
|
||||||
|
}
|
||||||
|
|
||||||
|
return value
|
||||||
|
},
|
||||||
|
dateFormulaValue (value) {
|
||||||
|
if (!value) return 'null'
|
||||||
|
if (value === '{{date}}') return 'today()'
|
||||||
|
|
||||||
|
const [year, month, day = 1] = value.slice(0, 10).split('-').map(Number)
|
||||||
|
|
||||||
|
return Date.UTC(year, month - 1, day) / 86400000
|
||||||
},
|
},
|
||||||
evalTextFormula (field, depth = 0) {
|
evalTextFormula (field, depth = 0) {
|
||||||
if (depth > 10) return ''
|
if (depth > 10) return ''
|
||||||
|
|||||||
@@ -611,7 +611,7 @@ export default {
|
|||||||
return this.withCondition && (this.withConditions || this.nativePlatform !== 'ios') && !['stamp', 'heading'].includes(this.field.type)
|
return this.withCondition && (this.withConditions || this.nativePlatform !== 'ios') && !['stamp', 'heading'].includes(this.field.type)
|
||||||
},
|
},
|
||||||
showFormula () {
|
showFormula () {
|
||||||
return (this.field.type === 'number' || this.field.type === 'payment' || !!this.field.preferences?.formula) && (this.withFormula || this.nativePlatform !== 'ios')
|
return (this.field.type === 'number' || this.field.type === 'date' || this.field.type === 'payment' || !!this.field.preferences?.formula) && (this.withFormula || this.nativePlatform !== 'ios')
|
||||||
},
|
},
|
||||||
showRequired () {
|
showRequired () {
|
||||||
return this.withRequired && !['phone', 'stamp', 'verification', 'strikethrough', 'heading'].includes(this.field.type)
|
return this.withRequired && !['phone', 'stamp', 'verification', 'strikethrough', 'heading'].includes(this.field.type)
|
||||||
|
|||||||
@@ -500,7 +500,7 @@
|
|||||||
</label>
|
</label>
|
||||||
</li>
|
</li>
|
||||||
<li
|
<li
|
||||||
v-if="(field.type == 'number' || field.preferences?.formula) && (withFormula || nativePlatform !== 'ios')"
|
v-if="(field.type == 'number' || field.type == 'date' || field.preferences?.formula) && (withFormula || nativePlatform !== 'ios')"
|
||||||
class="field-settings-formula"
|
class="field-settings-formula"
|
||||||
>
|
>
|
||||||
<label
|
<label
|
||||||
|
|||||||
@@ -71,8 +71,22 @@
|
|||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
target="blank"
|
target="blank"
|
||||||
class="text-sm mb-2 inline space-x-2 font-mono"
|
class="text-sm mb-2 flex flex-wrap gap-x-2 gap-y-1 font-mono"
|
||||||
>
|
>
|
||||||
|
<template v-if="withDateFunctions">
|
||||||
|
<button
|
||||||
|
class="bg-base-200 px-2 rounded-xl"
|
||||||
|
@click="insertTextUnderCursor('today()')"
|
||||||
|
>
|
||||||
|
today()
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="bg-base-200 px-2 rounded-xl"
|
||||||
|
@click="insertTextUnderCursor('datedif(, , "y")', 8)"
|
||||||
|
>
|
||||||
|
datedif(start, end, "y")
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
<button
|
<button
|
||||||
class="bg-base-200 px-2 rounded-xl"
|
class="bg-base-200 px-2 rounded-xl"
|
||||||
@click="insertTextUnderCursor(' + ')"
|
@click="insertTextUnderCursor(' + ')"
|
||||||
@@ -105,13 +119,13 @@
|
|||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
class="bg-base-200 px-2 rounded-xl"
|
class="bg-base-200 px-2 rounded-xl"
|
||||||
@click="insertTextUnderCursor('round()')"
|
@click="insertTextUnderCursor('round()', 6)"
|
||||||
>
|
>
|
||||||
round(n, d)
|
round(n, d)
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
class="bg-base-200 px-2 rounded-xl"
|
class="bg-base-200 px-2 rounded-xl"
|
||||||
@click="insertTextUnderCursor('abs()')"
|
@click="insertTextUnderCursor('abs()', 4)"
|
||||||
>
|
>
|
||||||
abs(n)
|
abs(n)
|
||||||
</button>
|
</button>
|
||||||
@@ -171,6 +185,11 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
withDateFunctions () {
|
||||||
|
return this.field.type === 'date' || [...this.formula.matchAll(/{{(.*?)}}/g)].some(([, name]) => {
|
||||||
|
return this.template.fields.some((f) => f.type === 'date' && (f.name || this.buildDefaultName(f)).trim() === name.trim())
|
||||||
|
})
|
||||||
|
},
|
||||||
fields () {
|
fields () {
|
||||||
return this.template.fields.reduce((acc, f) => {
|
return this.template.fields.reduce((acc, f) => {
|
||||||
const isAllowed = this.field.type === 'text' ? this.isTextField(f) : this.isNumberField(f)
|
const isAllowed = this.field.type === 'text' ? this.isTextField(f) : this.isNumberField(f)
|
||||||
@@ -191,7 +210,7 @@ export default {
|
|||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
isNumberField (field) {
|
isNumberField (field) {
|
||||||
return field.type === 'number' || (['radio', 'select'].includes(field.type) && field.options?.every((o) => String(o.value).match(/^[\d.-]+$/)))
|
return ['number', 'date'].includes(field.type) || (['radio', 'select'].includes(field.type) && field.options?.every((o) => String(o.value).match(/^[\d.-]+$/)))
|
||||||
},
|
},
|
||||||
isTextField (field) {
|
isTextField (field) {
|
||||||
return ['text', 'number', 'select', 'radio', 'cells', 'phone'].includes(field.type)
|
return ['text', 'number', 'select', 'radio', 'cells', 'phone'].includes(field.type)
|
||||||
@@ -244,18 +263,19 @@ export default {
|
|||||||
this.$emit('close')
|
this.$emit('close')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
insertTextUnderCursor (textToInsert) {
|
insertTextUnderCursor (textToInsert, cursorOffset = textToInsert.length) {
|
||||||
const textarea = this.$refs.textarea
|
const textarea = this.$refs.textarea
|
||||||
|
|
||||||
const selectionEnd = textarea.selectionEnd
|
const { selectionStart, selectionEnd } = textarea
|
||||||
const cursorPos = selectionEnd
|
const selectedText = cursorOffset < textToInsert.length ? textarea.value.substring(selectionStart, selectionEnd) : ''
|
||||||
|
const cursorPos = selectedText ? selectionStart : selectionEnd
|
||||||
|
|
||||||
const newText = textarea.value.substring(0, cursorPos) + textToInsert + textarea.value.substring(cursorPos)
|
const newText = textarea.value.substring(0, cursorPos) + textToInsert.slice(0, cursorOffset) + selectedText + textToInsert.slice(cursorOffset) + textarea.value.substring(selectionEnd)
|
||||||
|
|
||||||
this.formula = newText
|
this.formula = newText
|
||||||
|
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
textarea.setSelectionRange(cursorPos + textToInsert.length, cursorPos + textToInsert.length)
|
textarea.setSelectionRange(cursorPos + cursorOffset + selectedText.length, cursorPos + cursorOffset + selectedText.length)
|
||||||
|
|
||||||
textarea.focus()
|
textarea.focus()
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -231,7 +231,11 @@ module Submitters
|
|||||||
else
|
else
|
||||||
normalized_formula = normalize_formula(formula, submitter.submission, submission_values:)
|
normalized_formula = normalize_formula(formula, submitter.submission, submission_values:)
|
||||||
|
|
||||||
calculate_formula_value(normalized_formula, values)
|
if field['type'] == 'date'
|
||||||
|
calculate_date_formula_value(normalized_formula, values, submitter.submission)
|
||||||
|
else
|
||||||
|
calculate_formula_value(normalized_formula, values, submitter.submission)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -256,10 +260,14 @@ module Submitters
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def calculate_formula_value(_formula, _values)
|
def calculate_formula_value(_formula, _values, _submission)
|
||||||
0
|
0
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def calculate_date_formula_value(_formula, _values, _submission)
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
|
||||||
def eval_text_formula_value(_formula, _values, _submission)
|
def eval_text_formula_value(_formula, _values, _submission)
|
||||||
''
|
''
|
||||||
end
|
end
|
||||||
|
|||||||
Reference in new issue
Block a user