add formula placeholder

This commit is contained in:
Pete Matsyburka
2024-02-24 01:01:36 +02:00
parent beb677734f
commit 8227dd4f6a
13 changed files with 495 additions and 21 deletions
+4 -1
View File
@@ -206,9 +206,11 @@ export default {
}
},
computed: {
defaultName: Field.computed.defaultName,
fieldNames: FieldType.computed.fieldNames,
fieldIcons: FieldType.computed.fieldIcons,
defaultName () {
return this.buildDefaultName(this.field, this.template.fields)
},
optionIndexText () {
if (this.area.option_uuid && this.field.options) {
return `${this.field.options.findIndex((o) => o.uuid === this.area.option_uuid) + 1}.`
@@ -312,6 +314,7 @@ export default {
}
},
methods: {
buildDefaultName: Field.methods.buildDefaultName,
onNameFocus (e) {
this.selectedAreaRef.value = this.area
@@ -273,6 +273,7 @@
@select="startFieldDraw($event)"
/>
</div>
<div id="docuseal_modal_container" />
</div>
</template>
@@ -321,6 +322,7 @@ export default {
backgroundColor: this.backgroundColor,
withPhone: this.withPhone,
withPayment: this.withPayment,
withFormula: this.withFormula,
defaultDrawFieldType: this.defaultDrawFieldType,
selectedAreaRef: computed(() => this.selectedAreaRef)
}
@@ -449,6 +451,11 @@ export default {
required: false,
default: false
},
withFormula: {
type: Boolean,
required: false,
default: false
},
onlyDefinedFields: {
type: Boolean,
required: false,
+69 -18
View File
@@ -65,6 +65,17 @@
:stroke-width="1.6"
/>
</button>
<button
v-if="field.preferences?.formula"
class="relative cursor-pointer text-transparent group-hover:text-base-content"
:disabled="!withFormula"
@click="isShowFormulaModal = true"
>
<IconMathFunction
:width="18"
:stroke-width="1.6"
/>
</button>
<PaymentSettings
v-if="field.type === 'payment'"
:field="field"
@@ -193,6 +204,26 @@
<span class="label-text">{{ t('required') }}</span>
</label>
</li>
<li
v-if="field.type == 'number'"
:class="{'tooltip tooltip-bottom': !withFormula}"
:data-tip="withFormula ? '' : 'Available in Pro'"
@click.stop
>
<label
class="label-text cursor-pointer py-1.5 text-center w-full flex items-center"
@click="isShowFormulaModal = withFormula"
>
<IconMathFunction
width="18px"
height="18px"
class="ml-0.5 mr-1"
/>
<span class="text-sm">
{{ t('formula') }}
</span>
</label>
</li>
<li
v-if="field.type == 'checkbox'"
@click.stop
@@ -359,6 +390,16 @@
</button>
</div>
</div>
<Teleport
v-if="isShowFormulaModal"
:to="modalContainerEl"
>
<FormulaModal
:field="field"
:build-default-name="buildDefaultName"
@close="isShowFormulaModal = false"
/>
</Teleport>
</div>
</template>
@@ -366,7 +407,8 @@
import Contenteditable from './contenteditable'
import FieldType from './field_type'
import PaymentSettings from './payment_settings'
import { IconShape, IconNewSection, IconTrashX, IconCopy, IconSettings } from '@tabler/icons-vue'
import FormulaModal from './formula_modal'
import { IconMathFunction, IconShape, IconNewSection, IconTrashX, IconCopy, IconSettings } from '@tabler/icons-vue'
import { v4 } from 'uuid'
export default {
@@ -377,11 +419,13 @@ export default {
IconShape,
PaymentSettings,
IconNewSection,
FormulaModal,
IconTrashX,
IconMathFunction,
IconCopy,
FieldType
},
inject: ['template', 'save', 'backgroundColor', 'selectedAreaRef', 't'],
inject: ['template', 'save', 'backgroundColor', 'selectedAreaRef', 't', 'withFormula'],
props: {
field: {
type: Object,
@@ -403,11 +447,15 @@ export default {
return {
isNameFocus: false,
showPaymentModal: false,
isShowFormulaModal: false,
renderDropdown: false
}
},
computed: {
fieldNames: FieldType.computed.fieldNames,
modalContainerEl () {
return this.$el.getRootNode().querySelector('#docuseal_modal_container')
},
dateFormats () {
return [
'MM/DD/YYYY',
@@ -422,22 +470,7 @@ export default {
]
},
defaultName () {
if (this.field.type === 'payment' && this.field.preferences?.price) {
const { price, currency } = this.field.preferences || {}
const formattedPrice = new Intl.NumberFormat([], {
style: 'currency',
currency
}).format(price)
return `${this.fieldNames[this.field.type]} ${formattedPrice}`
} else {
const typeIndex = this.template.fields.filter((f) => f.type === this.field.type).indexOf(this.field)
const suffix = { multiple: this.t('select'), radio: this.t('group') }[this.field.type] || this.t('field')
return `${this.fieldNames[this.field.type]} ${suffix} ${typeIndex + 1}`
}
return this.buildDefaultName(this.field, this.template.fields)
},
areas () {
return this.field.areas || []
@@ -452,6 +485,24 @@ export default {
}
},
methods: {
buildDefaultName (field, fields) {
if (field.type === 'payment' && field.preferences?.price) {
const { price, currency } = field.preferences || {}
const formattedPrice = new Intl.NumberFormat([], {
style: 'currency',
currency
}).format(price)
return `${this.fieldNames[field.type]} ${formattedPrice}`
} else {
const typeIndex = fields.filter((f) => f.type === field.type).indexOf(field)
const suffix = { multiple: this.t('select'), radio: this.t('group') }[field.type] || this.t('field')
return `${this.fieldNames[field.type]} ${suffix} ${typeIndex + 1}`
}
},
formatDate (date, format) {
const monthFormats = {
M: 'numeric',
@@ -0,0 +1,216 @@
<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') }}
</span>
<a
href="#"
class="text-xl"
@click.prevent="$emit('close')"
>&times;</a>
</div>
<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'],
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 () {
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
View File
@@ -13,6 +13,7 @@ const en = {
cancel: 'Cancel',
any: 'Any',
drawn: 'Drawn',
formula: 'Formula',
typed: 'Typed',
draw_field_on_the_document: 'Draw {field} field on the document',
click_to_upload: 'Click to upload',