add field description and title

pull/220/head^2
Pete Matsyburka 2 years ago
parent 55616ac321
commit cfe5610ec9

@ -8,8 +8,7 @@
},
"rules": {
"vue/no-deprecated-html-element-is": 0,
"vue/no-mutating-props": 0,
"vue/no-v-html": 0
"vue/no-mutating-props": 0
},
"parserOptions": {
"ecmaVersion": 2022,

@ -87,7 +87,9 @@ module Api
:name,
{ schema: [%i[attachment_uuid name]],
submitters: [%i[name uuid]],
fields: [[:uuid, :submitter_uuid, :name, :type, :required, :readonly, :default_value,
fields: [[:uuid, :submitter_uuid, :name, :type,
:required, :readonly, :default_value,
:title, :description,
{ preferences: {},
conditions: [%i[field_uuid value action]],
options: [%i[value uuid]],

@ -41,6 +41,13 @@
:name="`values[${field.uuid}][]`"
>
</template>
<div
v-if="field.description && !modelValue.length"
dir="auto"
class="mb-3 px-1"
>
<MarkdownContent :string="field.description" />
</div>
<FileDropzone
:message="`${t('upload')} ${field.name || t('files')}${field.required ? '' : ` (${t('optional')})`}`"
:submitter-slug="submitterSlug"
@ -53,12 +60,14 @@
<script>
import FileDropzone from './dropzone'
import MarkdownContent from './markdown_content'
import { IconPaperclip, IconTrashX } from '@tabler/icons-vue'
export default {
name: 'AttachmentStep',
components: {
FileDropzone,
MarkdownContent,
IconPaperclip,
IconTrashX
},

@ -1,11 +1,21 @@
<template>
<div dir="auto">
<div class="flex justify-between items-center w-full mb-2">
<div
class="flex justify-between items-center w-full"
:class="{ 'mb-2': !field.description }"
>
<label
:for="field.uuid"
class="label text-2xl"
>{{ field.name && showFieldNames ? field.name : t('date') }}
<template v-if="!field.required">({{ t('optional') }})</template>
>
<MarkdownContent
v-if="field.title"
:string="field.title"
/>
<template v-else>
{{ field.name && showFieldNames ? field.name : t('date') }}
<template v-if="!field.required">({{ t('optional') }})</template>
</template>
</label>
<button
class="btn btn-outline btn-sm !normal-case font-normal"
@ -15,6 +25,13 @@
{{ t('set_today') }}
</button>
</div>
<div
v-if="field.description"
class="mb-3 px-1"
dir="auto"
>
<MarkdownContent :string="field.description" />
</div>
<AppearsOn :field="field" />
<div class="text-center">
<input
@ -33,11 +50,13 @@
<script>
import { IconCalendarCheck } from '@tabler/icons-vue'
import AppearsOn from './appears_on'
import MarkdownContent from './markdown_content'
export default {
name: 'DateStep',
components: {
IconCalendarCheck,
MarkdownContent,
AppearsOn
},
inject: ['t'],

@ -101,17 +101,32 @@
/>
<div v-else-if="currentField.type === 'select'">
<label
v-if="showFieldNames && currentField.name"
v-if="showFieldNames && (currentField.name || currentField.title)"
:for="currentField.uuid"
dir="auto"
class="label text-2xl mb-2"
>{{ currentField.name }}
<template v-if="!currentField.required">({{ t('optional') }})</template>
class="label text-2xl"
:class="{ 'mb-2': !currentField.description }"
>
<MarkdownContent
v-if="currentField.title"
:string="currentField.title"
/>
<template v-else>
{{ currentField.name }}
<template v-if="!currentField.required">({{ t('optional') }})</template>
</template>
</label>
<div
v-else
class="py-1"
/>
<div
v-if="currentField.description"
dir="auto"
class="mb-3 px-1"
>
<MarkdownContent :string="currentField.description" />
</div>
<AppearsOn :field="currentField" />
<select
:id="currentField.uuid"
@ -140,13 +155,28 @@
</div>
<div v-else-if="currentField.type === 'radio'">
<label
v-if="showFieldNames && currentField.name"
v-if="showFieldNames && (currentField.name || currentField.title)"
:for="currentField.uuid"
dir="auto"
class="label text-2xl mb-2"
>{{ currentField.name }}
<template v-if="!currentField.required">({{ t('optional') }})</template>
class="label text-2xl"
:class="{ 'mb-2': !currentField.description }"
>
<MarkdownContent
v-if="currentField.title"
:string="currentField.title"
/>
<template v-else>
{{ currentField.name }}
<template v-if="!currentField.required">({{ t('optional') }})</template>
</template>
</label>
<div
v-if="currentField.description"
dir="auto"
class="mb-3 px-1"
>
<MarkdownContent :string="currentField.description" />
</div>
<div class="flex w-full max-h-44 overflow-y-auto">
<div
v-if="!showFieldNames || (currentField.options.every((e) => !e.value) && currentField.options.length > 4)"
@ -197,66 +227,77 @@
/>
<div
v-else-if="currentField.type === 'checkbox'"
class="flex w-full max-h-44 overflow-y-auto"
>
<input
type="hidden"
name="cast_boolean"
value="true"
<div
v-if="currentField.description"
dir="auto"
class="mb-3 px-1"
>
<MarkdownContent :string="currentField.description" />
</div>
<div
class="space-y-3.5 mx-auto"
class="flex w-full max-h-44 overflow-y-auto"
>
<template v-if="isAnonymousChecboxes || !showFieldNames">
<span class="text-xl">
{{ t('complete_hightlighted_checkboxes_and_click') }} <span class="font-semibold">{{ stepFields.length === currentStep + 1 ? t('submit') : t('next') }}</span>.
</span>
<input
v-for="field in currentStepFields"
:key="field.uuid"
type="hidden"
:name="`values[${field.uuid}]`"
:value="!!values[field.uuid]"
>
</template>
<template v-else>
<div
v-for="(field, index) in currentStepFields"
:key="field.uuid"
>
<label
:for="field.uuid"
class="flex items-center space-x-3"
<input
type="hidden"
name="cast_boolean"
value="true"
>
<div
class="space-y-3.5 mx-auto"
>
<template v-if="isAnonymousChecboxes || !showFieldNames">
<span class="text-xl">
{{ t('complete_hightlighted_checkboxes_and_click') }} <span class="font-semibold">{{ stepFields.length === currentStep + 1 ? t('submit') : t('next') }}</span>.
</span>
<input
v-for="field in currentStepFields"
:key="field.uuid"
type="hidden"
:name="`values[${field.uuid}]`"
:value="!!values[field.uuid]"
>
<input
type="hidden"
:name="`values[${field.uuid}]`"
:value="!!values[field.uuid]"
>
<input
:id="field.uuid"
type="checkbox"
class="base-checkbox !h-7 !w-7"
:oninvalid="`this.setCustomValidity('${t('please_check_the_box_to_continue')}')`"
:onchange="`this.setCustomValidity(validity.valueMissing ? '${t('please_check_the_box_to_continue')}' : '');`"
:required="field.required"
:checked="!!values[field.uuid]"
@click="[scrollIntoField(field), values[field.uuid] = !values[field.uuid]]"
>
<span
v-if="field.title"
class="text-xl"
v-html="field.title"
/>
<span
v-else
class="text-xl"
</template>
<template v-else>
<div
v-for="(field, index) in currentStepFields"
:key="field.uuid"
>
<label
:for="field.uuid"
class="flex items-center space-x-3"
>
{{ field.name || field.type + ' ' + (index + 1) }}
</span>
</label>
</div>
</template>
<input
type="hidden"
:name="`values[${field.uuid}]`"
:value="!!values[field.uuid]"
>
<input
:id="field.uuid"
type="checkbox"
class="base-checkbox !h-7 !w-7"
:oninvalid="`this.setCustomValidity('${t('please_check_the_box_to_continue')}')`"
:onchange="`this.setCustomValidity(validity.valueMissing ? '${t('please_check_the_box_to_continue')}' : '');`"
:required="field.required"
:checked="!!values[field.uuid]"
@click="[scrollIntoField(field), values[field.uuid] = !values[field.uuid]]"
>
<span
v-if="field.title"
class="text-xl"
>
<MarkdownContent :string="field.title" />
</span>
<span
v-else
class="text-xl"
>
{{ field.name || field.type + ' ' + (index + 1) }}
</span>
</label>
</div>
</template>
</div>
</div>
</div>
<ImageStep
@ -418,6 +459,7 @@ import PaymentStep from './payment_step'
import TextStep from './text_step'
import NumberStep from './number_step'
import DateStep from './date_step'
import MarkdownContent from './markdown_content'
import FormCompleted from './completed'
import { IconInnerShadowTop, IconArrowsDiagonal, IconWritingSign, IconArrowsDiagonalMinimize2 } from '@tabler/icons-vue'
import AppearsOn from './appears_on'
@ -459,6 +501,7 @@ export default {
NumberStep,
FormulaFieldAreas,
PhoneStep,
MarkdownContent,
PaymentStep,
IconArrowsDiagonalMinimize2,
FormCompleted
@ -654,7 +697,7 @@ export default {
const prevStep = acc[acc.length - 1]
if (this.checkFieldConditions(f)) {
if (f.type === 'checkbox' && Array.isArray(prevStep) && prevStep[0].type === 'checkbox') {
if (f.type === 'checkbox' && Array.isArray(prevStep) && prevStep[0].type === 'checkbox' && !f.description) {
prevStep.push(f)
} else {
acc.push([f])

@ -24,9 +24,17 @@
:name="`values[${field.uuid}]`"
>
</div>
<div>
<div
v-if="!modelValue"
>
<div
v-if="field.description"
dir="auto"
class="mb-3 px-1"
>
<MarkdownContent :string="field.description" />
</div>
<FileDropzone
v-if="!modelValue"
:message="`${t('upload')} ${field.name || t('image')}${field.required ? '' : ` (${t('optional')})`}`"
:submitter-slug="submitterSlug"
:accept="'image/*'"
@ -39,12 +47,14 @@
<script>
import FileDropzone from './dropzone'
import { IconReload } from '@tabler/icons-vue'
import MarkdownContent from './markdown_content'
export default {
name: 'ImageStep',
components: {
FileDropzone,
IconReload
IconReload,
MarkdownContent
},
inject: ['t'],
props: {

@ -1,9 +1,20 @@
<template>
<div dir="auto">
<div class="flex justify-between items-center w-full mb-2">
<div
class="flex justify-between items-center w-full"
:class="{ 'mb-2': !field.description }"
>
<label
class="label text-2xl"
>{{ showFieldNames && field.name ? field.name : t('initials') }}</label>
>
<MarkdownContent
v-if="field.title"
:string="field.title"
/>
<template v-else>
{{ showFieldNames && field.name ? field.name : t('initials') }}
</template>
</label>
<div class="space-x-2 flex">
<span
v-if="isDrawInitials"
@ -70,6 +81,13 @@
</a>
</div>
</div>
<div
v-if="field.description"
dir="auto"
class="mb-3 px-1"
>
<MarkdownContent :string="field.description" />
</div>
<AppearsOn :field="field" />
<input
:value="modelValue || computedPreviousValue"
@ -105,6 +123,7 @@ import { cropCanvasAndExportToPNG } from './crop_canvas'
import { IconReload, IconTextSize, IconSignature, IconArrowsDiagonalMinimize2 } from '@tabler/icons-vue'
import SignaturePad from 'signature_pad'
import AppearsOn from './appears_on'
import MarkdownContent from './markdown_content'
const scale = 3
@ -115,6 +134,7 @@ export default {
IconReload,
IconTextSize,
IconSignature,
MarkdownContent,
IconArrowsDiagonalMinimize2
},
inject: ['baseUrl', 't'],

@ -0,0 +1,70 @@
<template>
<span>
<template
v-for="(item, index) in items"
:key="index"
>
<a
v-if="item.startsWith('<a') && item.endsWith('</a>')"
:href="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: {
extractAttr (text, attr) {
if (text.includes(attr)) {
return text.split(attr).pop().split('"')[1]
}
},
extractText (text) {
if (text) {
return text.match(/>(.+?)</)?.[1]
}
}
}
}
</script>

@ -1,10 +1,23 @@
<template>
<label
v-if="showFieldNames && field.name"
v-if="showFieldNames && (field.name || field.title)"
:for="field.uuid"
dir="auto"
class="label text-2xl mb-2"
>{{ field.name }}</label>
class="label text-2xl"
:class="{ 'mb-2': !field.description }"
><MarkdownContent
v-if="field.title"
:string="field.title"
/>
<template v-else>{{ field.name }}</template>
</label>
<div
v-if="field.description"
dir="auto"
class="mb-3 px-1"
>
<MarkdownContent :string="field.description" />
</div>
<div class="flex w-full max-h-44 overflow-y-auto">
<input
v-if="modelValue.length === 0"
@ -53,8 +66,13 @@
</template>
<script>
import MarkdownContent from './markdown_content'
export default {
name: 'MultiSelectStep',
components: {
MarkdownContent
},
inject: ['t', 'scrollIntoField'],
props: {
field: {

@ -1,11 +1,14 @@
<template>
<label
v-if="showFieldNames && field.name"
v-if="showFieldNames && (field.name || field.title)"
:for="field.uuid"
dir="auto"
class="label text-2xl"
:class="{ 'mb-2': !field.description }"
><template v-if="field.title"><span v-html="field.title" /></template>
><MarkdownContent
v-if="field.title"
:string="field.title"
/>
<template v-else>{{ field.name }}</template>
<template v-if="!field.required">({{ t('optional') }})</template>
</label>
@ -15,9 +18,11 @@
/>
<div
v-if="field.description"
class="mb-3 px-1 text-lg"
v-html="field.description"
/>
dir="auto"
class="mb-3 px-1"
>
<MarkdownContent :string="field.description" />
</div>
<AppearsOn :field="field" />
<div class="items-center flex">
<input
@ -41,11 +46,13 @@
<script>
import AppearsOn from './appears_on'
import MarkdownContent from './markdown_content'
export default {
name: 'TextStep',
components: {
AppearsOn
AppearsOn,
MarkdownContent
},
inject: ['t'],
props: {

@ -2,10 +2,25 @@
<div>
<label
:for="isCodeSent ? 'one_time_code' : field.uuid"
class="label text-2xl mb-2"
>{{ showFieldNames && field.name ? field.name : t('verified_phone_number') }}
<template v-if="!field.required">({{ t('optional') }})</template>
class="label text-2xl"
:class="{ 'mb-2': !field.description }"
>
<MarkdownContent
v-if="field.title"
:string="field.title"
/>
<template v-else>
{{ showFieldNames && field.name ? field.name : t('verified_phone_number') }}
<template v-if="!field.required">({{ t('optional') }})</template>
</template>
</label>
<div
v-if="field.description"
dir="auto"
class="mb-3 px-1"
>
<MarkdownContent :string="field.description" />
</div>
<div>
<input
type="hidden"
@ -68,6 +83,8 @@
</template>
<script>
import MarkdownContent from './markdown_content'
function throttle (func, delay) {
let lastCallTime = 0
@ -83,6 +100,9 @@ function throttle (func, delay) {
export default {
name: 'PhoneStep',
components: {
MarkdownContent
},
inject: ['t', 'baseUrl'],
props: {
field: {

@ -1,9 +1,20 @@
<template>
<div dir="auto">
<div class="flex justify-between items-center w-full mb-2">
<div
class="flex justify-between items-center w-full"
:class="{ 'mb-2': !field.description }"
>
<label
class="label text-2xl"
>{{ showFieldNames && field.name ? field.name : t('signature') }}</label>
>
<MarkdownContent
v-if="field.title"
:string="field.title"
/>
<template v-else>
{{ showFieldNames && field.name ? field.name : t('signature') }}
</template>
</label>
<div class="space-x-2 flex">
<span
v-if="isTextSignature && field.preferences?.format !== 'typed'"
@ -91,6 +102,13 @@
</a>
</div>
</div>
<div
v-if="field.description"
dir="auto"
class="mb-3 px-1"
>
<MarkdownContent :string="field.description" />
</div>
<AppearsOn :field="field" />
<input
:value="modelValue || computedPreviousValue"
@ -126,6 +144,7 @@ import { IconReload, IconCamera, IconSignature, IconTextSize, IconArrowsDiagonal
import { cropCanvasAndExportToPNG } from './crop_canvas'
import SignaturePad from 'signature_pad'
import AppearsOn from './appears_on'
import MarkdownContent from './markdown_content'
let isFontLoaded = false
@ -137,6 +156,7 @@ export default {
AppearsOn,
IconReload,
IconCamera,
MarkdownContent,
IconTextSize,
IconSignature,
IconArrowsDiagonalMinimize2

@ -1,11 +1,14 @@
<template>
<label
v-if="showFieldNames && field.name"
v-if="showFieldNames && (field.name || field.title)"
:for="field.uuid"
dir="auto"
class="label text-2xl"
:class="{ 'mb-2': !field.description }"
><template v-if="field.title"><span v-html="field.title" /></template>
><MarkdownContent
v-if="field.title"
:string="field.title"
/>
<template v-else>{{ field.name }}</template>
<template v-if="!field.required">({{ t('optional') }})</template>
</label>
@ -15,9 +18,11 @@
/>
<div
v-if="field.description"
class="mb-3 px-1 text-lg"
v-html="field.description"
/>
dir="auto"
class="mb-3 px-1"
>
<MarkdownContent :string="field.description" />
</div>
<AppearsOn :field="field" />
<div class="items-center flex">
<input
@ -69,11 +74,13 @@
<script>
import { IconAlignBoxLeftTop } from '@tabler/icons-vue'
import AppearsOn from './appears_on'
import MarkdownContent from './markdown_content'
export default {
name: 'TextStep',
components: {
IconAlignBoxLeftTop,
MarkdownContent,
AppearsOn
},
inject: ['t'],

@ -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')"
>&times;</a>
</div>
<div>
<form @submit.prevent="saveAndClose">
<div class="space-y-1 mb-1">
<div>
<label
dir="auto"
class="label text-sm"
for="title_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>

@ -258,6 +258,19 @@
</label>
</li>
<hr class="pb-0.5 mt-0.5">
<li>
<label
class="label-text cursor-pointer text-center w-full flex items-center"
@click="isShowDescriptionModal = !isShowDescriptionModal"
>
<IconInfoCircle
width="18"
/>
<span class="text-sm">
{{ t('description') }}
</span>
</label>
</li>
<li>
<label
class="label-text cursor-pointer text-center w-full flex items-center"
@ -435,6 +448,16 @@
@close="isShowConditionsModal = false"
/>
</Teleport>
<Teleport
v-if="isShowDescriptionModal"
:to="modalContainerEl"
>
<DescriptionModal
:field="field"
:build-default-name="buildDefaultName"
@close="isShowDescriptionModal = false"
/>
</Teleport>
</div>
</template>
@ -444,7 +467,8 @@ import FieldType from './field_type'
import PaymentSettings from './payment_settings'
import FormulaModal from './formula_modal'
import ConditionsModal from './conditions_modal'
import { IconRouteAltLeft, IconMathFunction, IconShape, IconNewSection, IconTrashX, IconCopy, IconSettings } from '@tabler/icons-vue'
import DescriptionModal from './description_modal'
import { IconInfoCircle, IconRouteAltLeft, IconMathFunction, IconShape, IconNewSection, IconTrashX, IconCopy, IconSettings } from '@tabler/icons-vue'
import { v4 } from 'uuid'
export default {
@ -455,7 +479,9 @@ export default {
IconShape,
PaymentSettings,
IconNewSection,
IconInfoCircle,
FormulaModal,
DescriptionModal,
ConditionsModal,
IconRouteAltLeft,
IconTrashX,
@ -487,6 +513,7 @@ export default {
showPaymentModal: false,
isShowFormulaModal: false,
isShowConditionsModal: false,
isShowDescriptionModal: false,
renderDropdown: false
}
},

@ -1,4 +1,6 @@
const en = {
description: 'Description',
display_title: 'Display title',
unchecked: 'Unchecked',
equal: 'Equal',
not_equal: 'Not equal',

@ -30,6 +30,7 @@
"sass-loader": "^13.2.2",
"shakapacker": "7.1.0",
"signature_pad": "^4.1.5",
"snarkdown": "^2.0.0",
"tailwindcss": "^3.3.2",
"terser-webpack-plugin": "5.3.8",
"uuid": "^9.0.0",

@ -4950,6 +4950,11 @@ sirv@^1.0.7:
mrmime "^1.0.0"
totalist "^1.0.0"
snarkdown@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/snarkdown/-/snarkdown-2.0.0.tgz#b1feb4db91b9f94a8ebbd7a50f3e99aee18b1e03"
integrity sha512-MgL/7k/AZdXCTJiNgrO7chgDqaB9FGM/1Tvlcenenb7div6obaDATzs16JhFyHHBGodHT3B7RzRc5qk8pFhg3A==
sockjs@^0.3.24:
version "0.3.24"
resolved "https://registry.yarnpkg.com/sockjs/-/sockjs-0.3.24.tgz#c9bc8995f33a111bea0395ec30aa3206bdb5ccce"

Loading…
Cancel
Save