Form editor v2 (#579)
* Form editor v2 * fix template test * setFormDefaults when save * fix form cleaning dark mode * improvements on open sidebar * UI polish * Fix change type button --------- Co-authored-by: Julien Nahum <julien@nahum.net>
This commit is contained in:
parent
47ae11bc58
commit
d6181cd249
|
|
@ -48,7 +48,6 @@ class Form extends Model implements CachableAttributes
|
|||
'removed_properties',
|
||||
|
||||
'title',
|
||||
'description',
|
||||
'tags',
|
||||
'visibility',
|
||||
|
||||
|
|
@ -177,12 +176,6 @@ class Form extends Model implements CachableAttributes
|
|||
});
|
||||
}
|
||||
|
||||
public function setDescriptionAttribute($value)
|
||||
{
|
||||
// Strip out unwanted html
|
||||
$this->attributes['description'] = Purify::clean($value);
|
||||
}
|
||||
|
||||
public function setSubmittedTextAttribute($value)
|
||||
{
|
||||
// Strip out unwanted html
|
||||
|
|
|
|||
|
|
@ -1,217 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Service;
|
||||
|
||||
use App\Models\Forms\Form;
|
||||
use App\Models\Template;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Routing\Route;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
/**
|
||||
* Generates meta per-route matching. This is useful because Google, Twitter and Facebook struggle to load meta tags
|
||||
* injected dynamically by JavaScript. This class allows us to inject meta tags in the HTML head tag.
|
||||
*
|
||||
* Here's how to use this class
|
||||
* - Add a pattern to URL_PATTERNS
|
||||
* - Then choose between a static meta or a dynamic meta:
|
||||
* - If the content is dynamic (ex: needs to retrieve data from the database), then add a method to this class for
|
||||
* the corresponding pattern. The method should be named "getMyPatternName" (where pattern name is
|
||||
* my_pattern_name) and it should return an array of meta tags.
|
||||
* - If the content is static, then add meta tags to the PATTERN_STATIC_META array.
|
||||
*/
|
||||
class SeoMetaResolver
|
||||
{
|
||||
private array $patternData = [];
|
||||
|
||||
public const URL_PATTERNS = [
|
||||
'welcome' => '/',
|
||||
'form_show' => '/forms/{slug}',
|
||||
'login' => '/login',
|
||||
'register' => '/register',
|
||||
'reset_password' => '/password/reset',
|
||||
'privacy_policy' => '/privacy-policy',
|
||||
'terms_conditions' => '/terms-conditions',
|
||||
'integrations' => '/integrations',
|
||||
'templates' => '/form-templates',
|
||||
'templates_show' => '/form-templates/{slug}',
|
||||
'templates_types_show' => '/form-templates/types/{slug}',
|
||||
'templates_industries_show' => '/form-templates/industries/{slug}',
|
||||
];
|
||||
|
||||
/**
|
||||
* Metas for simple route (without needing to access DB)
|
||||
*/
|
||||
public const PATTERN_STATIC_META = [
|
||||
'login' => [
|
||||
'title' => 'Login',
|
||||
],
|
||||
'register' => [
|
||||
'title' => 'Create your account',
|
||||
],
|
||||
'reset_password' => [
|
||||
'title' => 'Reset your password',
|
||||
],
|
||||
'privacy_policy' => [
|
||||
'title' => 'Our Privacy Policy',
|
||||
],
|
||||
'terms_conditions' => [
|
||||
'title' => 'Our Terms & Conditions',
|
||||
],
|
||||
'integrations' => [
|
||||
'title' => 'Our Integrations',
|
||||
],
|
||||
'templates' => [
|
||||
'title' => 'Templates',
|
||||
'description' => 'Our collection of beautiful templates to create your own forms!',
|
||||
],
|
||||
];
|
||||
|
||||
public const META_CACHE_DURATION = 60 * 60 * 12; // 12 hours
|
||||
|
||||
public const META_CACHE_KEY_PREFIX = 'seo_meta_';
|
||||
|
||||
public function __construct(private Request $request)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the right metas for a given route, caches meta for 1 hour.
|
||||
*/
|
||||
public function getMetas(): array
|
||||
{
|
||||
$cacheKey = self::META_CACHE_KEY_PREFIX.urlencode($this->request->path());
|
||||
|
||||
return Cache::remember($cacheKey, now()->addSeconds(self::META_CACHE_DURATION), function () {
|
||||
$pattern = $this->resolvePattern();
|
||||
|
||||
if ($this->hasPatternMetaGetter($pattern)) {
|
||||
// Custom function for pattern
|
||||
try {
|
||||
return array_merge($this->getDefaultMeta(), $this->{'get'.Str::studly($pattern).'Meta'}());
|
||||
} catch (\Exception $e) {
|
||||
return $this->getDefaultMeta();
|
||||
}
|
||||
} elseif (in_array($pattern, array_keys(self::PATTERN_STATIC_META))) {
|
||||
// Simple meta for pattern
|
||||
$meta = self::PATTERN_STATIC_META[$pattern];
|
||||
if (isset($meta['title'])) {
|
||||
$meta['title'] .= $this->titleSuffix();
|
||||
}
|
||||
if (isset($meta['image'])) {
|
||||
$meta['image'] = asset($meta['image']);
|
||||
}
|
||||
|
||||
return array_merge($this->getDefaultMeta(), $meta);
|
||||
}
|
||||
|
||||
return $this->getDefaultMeta();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Simulates the Laravel router to match route with Metas
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private function resolvePattern()
|
||||
{
|
||||
foreach (self::URL_PATTERNS as $patternName => $patternData) {
|
||||
$path = rtrim($this->request->getPathInfo(), '/') ?: '/';
|
||||
|
||||
$route = (new Route('GET', $patternData, fn () => ''))->bind($this->request);
|
||||
if (preg_match($route->getCompiled()->getRegex(), rawurldecode($path))) {
|
||||
$this->patternData = $route->parameters();
|
||||
|
||||
return $patternName;
|
||||
}
|
||||
}
|
||||
|
||||
return 'default';
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if a get mutator exists for a pattern.
|
||||
*
|
||||
* @param string $key
|
||||
* @return bool
|
||||
*/
|
||||
private function hasPatternMetaGetter($key)
|
||||
{
|
||||
return method_exists($this, 'get'.Str::studly($key).'Meta');
|
||||
}
|
||||
|
||||
private function titleSuffix()
|
||||
{
|
||||
return ' · '.config('app.name');
|
||||
}
|
||||
|
||||
private function getDefaultMeta(): array
|
||||
{
|
||||
return [
|
||||
'title' => 'Create beautiful forms for free'.$this->titleSuffix(),
|
||||
'description' => "Create beautiful forms for free. Unlimited fields, unlimited submissions. It's free and it takes less than 1 minute to create your first form.",
|
||||
'image' => asset('/img/social-preview.jpg'),
|
||||
];
|
||||
}
|
||||
|
||||
private function getFormShowMeta(): array
|
||||
{
|
||||
$form = Form::whereSlug($this->patternData['slug'])->firstOrFail();
|
||||
|
||||
$meta = [];
|
||||
if ($form->is_pro && $form->seo_meta->page_title) {
|
||||
$meta['title'] = $form->seo_meta->page_title;
|
||||
} else {
|
||||
$meta['title'] = $form->title.$this->titleSuffix();
|
||||
}
|
||||
|
||||
if ($form->is_pro && $form->seo_meta->page_description) {
|
||||
$meta['description'] = $form->seo_meta->page_description;
|
||||
} elseif ($form->description) {
|
||||
$meta['description'] = Str::of($form->description)->limit(160);
|
||||
}
|
||||
|
||||
if ($form->is_pro && $form->seo_meta->page_thumbnail) {
|
||||
$meta['image'] = $form->seo_meta->page_thumbnail;
|
||||
} elseif ($form->cover_picture) {
|
||||
$meta['image'] = $form->cover_picture;
|
||||
}
|
||||
|
||||
return $meta;
|
||||
}
|
||||
|
||||
private function getTemplatesShowMeta(): array
|
||||
{
|
||||
$template = Template::whereSlug($this->patternData['slug'])->firstOrFail();
|
||||
|
||||
return [
|
||||
'title' => $template->name.$this->titleSuffix(),
|
||||
'description' => Str::of($template->short_description)->limit(140).' | Customize any template and create your own form in minutes.',
|
||||
'image' => $template->image_url,
|
||||
];
|
||||
}
|
||||
|
||||
private function getTemplatesTypesShowMeta(): array
|
||||
{
|
||||
$types = json_decode(file_get_contents(resource_path('data/forms/templates/types.json')), true);
|
||||
$type = $types[array_search($this->patternData['slug'], array_column($types, 'slug'))];
|
||||
|
||||
return [
|
||||
'title' => $type['meta_title'],
|
||||
'description' => Str::of($type['meta_description'])->limit(140),
|
||||
];
|
||||
}
|
||||
|
||||
private function getTemplatesIndustriesShowMeta(): array
|
||||
{
|
||||
$industries = json_decode(file_get_contents(resource_path('data/forms/templates/industries.json')), true);
|
||||
$industry = $industries[array_search($this->patternData['slug'], array_column($industries, 'slug'))];
|
||||
|
||||
return [
|
||||
'title' => $industry['meta_title'],
|
||||
'description' => Str::of($industry['meta_description'])->limit(140),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
<?php
|
||||
|
||||
use App\Models\Forms\Form;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
return new class () extends Migration {
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Form::chunk(100, function ($forms) {
|
||||
foreach ($forms as $form) {
|
||||
$properties = $form->properties;
|
||||
if (!empty($form->description)) {
|
||||
array_unshift($properties, [
|
||||
'type' => 'nf-text',
|
||||
'content' => $form->description,
|
||||
'name' => 'Description',
|
||||
'id' => Str::uuid()
|
||||
]);
|
||||
$form->properties = $properties;
|
||||
$form->timestamps = false;
|
||||
$form->save();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Form::chunk(100, function ($forms) {
|
||||
foreach ($forms as $form) {
|
||||
$properties = $form->properties;
|
||||
if (!empty($properties) && $properties[0]['type'] === 'nf-text' && $properties[0]['name'] === 'Description') {
|
||||
array_shift($properties);
|
||||
$form->properties = $properties;
|
||||
$form->save();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
|
@ -65,8 +65,7 @@ it('can update a form', function () {
|
|||
|
||||
$this->assertDatabaseHas('forms', [
|
||||
'id' => $form->id,
|
||||
'title' => $form->title,
|
||||
'description' => $form->description,
|
||||
'title' => $form->title
|
||||
]);
|
||||
});
|
||||
|
||||
|
|
@ -125,8 +124,7 @@ it('can duplicate a form', function () {
|
|||
expect($workspace->forms()->count())->toBe(2);
|
||||
$this->assertDatabaseHas('forms', [
|
||||
'id' => $response->json('new_form.id'),
|
||||
'title' => 'Copy of ' . $form->title,
|
||||
'description' => $form->description,
|
||||
'title' => 'Copy of ' . $form->title
|
||||
]);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -18,6 +18,5 @@ it('can make a form for a database', function () {
|
|||
$workspace = $this->createUserWorkspace($user);
|
||||
$form = $this->makeForm($user, $workspace);
|
||||
expect($form->title)->not()->toBeNull();
|
||||
expect($form->description)->not()->toBeNull();
|
||||
expect(count($form->properties))->not()->toBe(0);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,6 +1,16 @@
|
|||
export default defineAppConfig({
|
||||
ui: {
|
||||
primary: 'blue',
|
||||
gray: 'slate'
|
||||
gray: 'slate',
|
||||
|
||||
tabs: {
|
||||
wrapper:'space-y-0',
|
||||
list: {
|
||||
height: 'h-auto',
|
||||
tab: {
|
||||
height: 'h-[30px]'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
|
|
|||
|
|
@ -14,7 +14,11 @@
|
|||
:name="name"
|
||||
>
|
||||
<slot name="label">
|
||||
<span>{{ label }}
|
||||
<span
|
||||
:class="[
|
||||
theme.SelectInput.fontSize,
|
||||
]"
|
||||
>{{ label }}
|
||||
<span
|
||||
v-if="required"
|
||||
class="text-red-500 required-dot"
|
||||
|
|
|
|||
|
|
@ -201,7 +201,7 @@ const formattedDate = (value) => {
|
|||
try {
|
||||
return format(new Date(value), props.dateFormat + (props.timeFormat == 12 ? ' p':' HH:mm'))
|
||||
} catch (e) {
|
||||
console.log('Error formatting date', e)
|
||||
console.error('Error formatting date', e)
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -98,35 +98,14 @@
|
|||
</template>
|
||||
</div>
|
||||
<div class="w-full items-center justify-center mt-2 hidden sm:flex">
|
||||
<open-form-button
|
||||
<UButton
|
||||
v-if="cameraUpload"
|
||||
native-type="button"
|
||||
icon="i-heroicons-camera"
|
||||
:loading="loading"
|
||||
:theme="theme"
|
||||
:color="color"
|
||||
class="py-2 p-1 px-2"
|
||||
color="white"
|
||||
class="px-2"
|
||||
@click.stop="openWebcam"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
class="w-6 h-6"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M6.827 6.175A2.31 2.31 0 0 1 5.186 7.23c-.38.054-.757.112-1.134.175C2.999 7.58 2.25 8.507 2.25 9.574V18a2.25 2.25 0 0 0 2.25 2.25h15A2.25 2.25 0 0 0 21.75 18V9.574c0-1.067-.75-1.994-1.802-2.169a47.865 47.865 0 0 0-1.134-.175 2.31 2.31 0 0 1-1.64-1.055l-.822-1.316a2.192 2.192 0 0 0-1.736-1.039 48.774 48.774 0 0 0-5.232 0 2.192 2.192 0 0 0-1.736 1.039l-.821 1.316Z"
|
||||
/>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M16.5 12.75a4.5 4.5 0 1 1-9 0 4.5 4.5 0 0 1 9 0ZM18.75 10.5h.008v.008h-.008V10.5Z"
|
||||
/>
|
||||
</svg>
|
||||
</open-form-button>
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -143,13 +122,12 @@
|
|||
import {inputProps, useFormInput} from './useFormInput.js'
|
||||
import InputWrapper from './components/InputWrapper.vue'
|
||||
import UploadedFile from './components/UploadedFile.vue'
|
||||
import OpenFormButton from '../open/forms/OpenFormButton.vue'
|
||||
import CameraUpload from './components/CameraUpload.vue'
|
||||
import {storeFile} from "~/lib/file-uploads.js"
|
||||
|
||||
export default {
|
||||
name: 'FileInput',
|
||||
components: {InputWrapper, UploadedFile, OpenFormButton},
|
||||
components: {InputWrapper, UploadedFile, CameraUpload},
|
||||
mixins: [],
|
||||
props: {
|
||||
...inputProps,
|
||||
|
|
|
|||
|
|
@ -109,7 +109,8 @@ export default {
|
|||
loading: {type: Boolean, default: false},
|
||||
multiple: { type: Boolean, default: false },
|
||||
disableOptions: { type: Array, default: () => [] },
|
||||
disableOptionsTooltip: {type: String, default: "Not allowed"},
|
||||
disableOptionsTooltip: { type: String, default: "Not allowed" },
|
||||
clearable: { type: Boolean, default: false },
|
||||
},
|
||||
setup(props, context) {
|
||||
return {
|
||||
|
|
@ -129,11 +130,11 @@ export default {
|
|||
if (this.multiple) {
|
||||
const emitValue = Array.isArray(this.compVal) ? [...this.compVal] : []
|
||||
|
||||
// Already in value, remove it
|
||||
// Already in value, remove it only if clearable or not the last item
|
||||
if (this.isSelected(value)) {
|
||||
this.compVal = emitValue.filter((item) => {
|
||||
return item !== value
|
||||
})
|
||||
if (this.clearable || emitValue.length > 1) {
|
||||
this.compVal = emitValue.filter((item) => item !== value)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -141,7 +142,10 @@ export default {
|
|||
emitValue.push(value)
|
||||
this.compVal = emitValue
|
||||
} else {
|
||||
this.compVal = this.compVal === value ? null : value
|
||||
// For single select, only change value if it's different or clearable
|
||||
if (this.compVal !== value || this.clearable) {
|
||||
this.compVal = this.compVal === value && this.clearable ? null : value
|
||||
}
|
||||
}
|
||||
},
|
||||
isSelected(value) {
|
||||
|
|
|
|||
|
|
@ -41,6 +41,7 @@
|
|||
<VueSignaturePad
|
||||
v-else
|
||||
ref="signaturePad"
|
||||
class="not-draggable"
|
||||
:class="[
|
||||
theme.SignatureInput.input,
|
||||
theme.SignatureInput.spacing.horizontal,
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<template>
|
||||
<div
|
||||
v-if="shouldRender"
|
||||
class="flex mb-1 input-help"
|
||||
class="flex input-help"
|
||||
>
|
||||
<small
|
||||
:class="helpClasses"
|
||||
|
|
@ -16,7 +16,10 @@
|
|||
</slot>
|
||||
</small>
|
||||
<slot name="after-help">
|
||||
<small class="flex-grow" />
|
||||
<small
|
||||
v-if="shouldRender || (!!slots.default)"
|
||||
class="flex-grow"
|
||||
/>
|
||||
</slot>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -78,7 +78,7 @@
|
|||
</span>
|
||||
</button>
|
||||
<button
|
||||
v-if="clearable && !isEmpty"
|
||||
v-if="clearable && showClearButton && !isEmpty"
|
||||
class="hover:bg-gray-50 dark:hover:bg-gray-900 border-l px-2"
|
||||
:class="[theme.SelectInput.spacing.vertical]"
|
||||
@click.prevent="clear()"
|
||||
|
|
@ -223,7 +223,8 @@ export default {
|
|||
emitKey: {type: String, default: null},
|
||||
color: {type: String, default: '#3B82F6'},
|
||||
placeholder: {type: String, default: null},
|
||||
uppercaseLabels: {type: Boolean, default: true},
|
||||
uppercaseLabels: { type: Boolean, default: true },
|
||||
showClearButton: { type: Boolean, default: true },
|
||||
theme: {
|
||||
type: Object, default: () => {
|
||||
const theme = inject("theme", null)
|
||||
|
|
@ -334,22 +335,24 @@ export default {
|
|||
const emitValue = Array.isArray(this.modelValue) ? [...this.modelValue] : []
|
||||
|
||||
if (this.isSelected(value)) {
|
||||
this.$emit('update:modelValue', emitValue.filter((item) => {
|
||||
if (this.emitKey) {
|
||||
return item !== value
|
||||
}
|
||||
return item[this.optionKey] !== value && item[this.optionKey] !== value[this.optionKey]
|
||||
}))
|
||||
// Only remove if clearable or not the last item
|
||||
if (this.clearable || emitValue.length > 1) {
|
||||
this.$emit('update:modelValue', emitValue.filter((item) => {
|
||||
if (this.emitKey) {
|
||||
return item !== value
|
||||
}
|
||||
return item[this.optionKey] !== value && item[this.optionKey] !== value[this.optionKey]
|
||||
}))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
emitValue.push(value)
|
||||
this.$emit('update:modelValue', emitValue)
|
||||
} else {
|
||||
if (this.modelValue === value) {
|
||||
this.$emit('update:modelValue', this.defaultValue ?? null)
|
||||
} else {
|
||||
this.$emit('update:modelValue', value)
|
||||
// For single select, only change value if it's different or clearable
|
||||
if (this.modelValue !== value || this.clearable) {
|
||||
this.$emit('update:modelValue', this.modelValue === value && this.clearable ? null : value)
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
<template>
|
||||
<div
|
||||
<component
|
||||
:is="element"
|
||||
ref="parentRef"
|
||||
tabindex="0"
|
||||
:class="{
|
||||
|
|
@ -8,7 +9,7 @@
|
|||
}"
|
||||
class="relative"
|
||||
:style="{ height: editing ? divHeight + 'px' : 'auto' }"
|
||||
@focus="startEditing"
|
||||
@click="startEditing"
|
||||
>
|
||||
<slot
|
||||
v-if="!editing"
|
||||
|
|
@ -20,20 +21,20 @@
|
|||
</slot>
|
||||
<div
|
||||
v-if="editing"
|
||||
class="absolute inset-0 border-2 transition-colors"
|
||||
class="absolute inset-0 border rounded-md border-inset transition-colors"
|
||||
:class="{ 'border-transparent': !editing, 'border-blue-500': editing }"
|
||||
>
|
||||
<input
|
||||
ref="editInputRef"
|
||||
v-model="content"
|
||||
class="absolute inset-0 focus:outline-none bg-white transition-colors"
|
||||
class="absolute inset-0 focus:outline-none bg-white rounded-md transition-colors px-2"
|
||||
:class="[{ 'bg-blue-50': editing }, contentClass]"
|
||||
@blur="editing = false"
|
||||
@keyup.enter="editing = false"
|
||||
@input="handleInput"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</component>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
|
|
@ -43,6 +44,7 @@ const props = defineProps({
|
|||
modelValue: { type: String, required: true },
|
||||
textAlign: { type: String, default: "left" },
|
||||
contentClass: { type: String, default: "" },
|
||||
element: { type: String, default: 'div' }, // New prop for element type
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
|
|
@ -4,7 +4,7 @@
|
|||
<div
|
||||
v-if="show"
|
||||
ref="backdrop"
|
||||
class="fixed z-30 top-0 inset-0 px-2 sm:px-4 flex items-top justify-center bg-gray-700/75 w-full h-screen overflow-y-scroll"
|
||||
class="fixed z-40 top-0 inset-0 px-2 sm:px-4 flex items-top justify-center bg-gray-700/75 w-full h-screen overflow-y-scroll"
|
||||
:class="{ 'backdrop-blur-sm': backdropBlur }"
|
||||
@click.self="close"
|
||||
>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,40 @@
|
|||
<template>
|
||||
<div
|
||||
v-show="isActive"
|
||||
class="settings-section"
|
||||
>
|
||||
<h3 class="text-xl font-medium mb-1">
|
||||
{{ name }}
|
||||
</h3>
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { inject, computed, onMounted, onBeforeUnmount } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
name: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
icon: {
|
||||
type: String,
|
||||
required: true
|
||||
}
|
||||
})
|
||||
|
||||
const activeSection = inject('activeSection', ref(''))
|
||||
const registerSection = inject('registerSection', () => {})
|
||||
const unregisterSection = inject('unregisterSection', () => {})
|
||||
|
||||
const isActive = computed(() => activeSection.value === props.name)
|
||||
|
||||
onMounted(() => {
|
||||
registerSection(props.name, props.icon)
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
unregisterSection(props.name)
|
||||
})
|
||||
</script>
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
<template>
|
||||
<div class="mb-8 flex">
|
||||
<div class="w-1/3 pr-8">
|
||||
<h3 class="text-lg font-semibold mb-1">
|
||||
{{ title }}
|
||||
</h3>
|
||||
<p class="text-sm text-gray-500">
|
||||
{{ description }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="w-2/3">
|
||||
<UCard class="divide-y divide-gray-200">
|
||||
<slot />
|
||||
</UCard>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
defineProps({
|
||||
title: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
description: {
|
||||
type: String,
|
||||
required: true
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
|
@ -0,0 +1,62 @@
|
|||
<template>
|
||||
<div class="flex flex-grow h-full relative">
|
||||
<!-- Sidebar -->
|
||||
<nav class="w-64 flex-shrink-0 overflow-y-auto border-r p-4 sticky top-0 bg-gray-50">
|
||||
<ul class="space-y-2">
|
||||
<li
|
||||
v-for="section in sections"
|
||||
:key="section.name"
|
||||
>
|
||||
<UButton
|
||||
:icon="section.icon"
|
||||
:label="section.name"
|
||||
:color="activeSection === section.name ? 'primary' : 'gray'"
|
||||
:variant="activeSection === section.name ? 'soft' : 'ghost'"
|
||||
class="w-full justify-start"
|
||||
|
||||
@click="activeSection = section.name"
|
||||
/>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
<!-- Main content -->
|
||||
<div class="flex items-start h-full px-4 md:px-8 py-4 flex-grow">
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, provide } from 'vue'
|
||||
|
||||
const sections = ref([])
|
||||
const activeSection = ref('')
|
||||
|
||||
const registerSection = (name, icon) => {
|
||||
const existingIndex = sections.value.findIndex(section => section.name === name)
|
||||
if (existingIndex !== -1) {
|
||||
sections.value[existingIndex] = { name, icon }
|
||||
} else {
|
||||
sections.value.push({ name, icon })
|
||||
if (sections.value.length === 1) {
|
||||
activeSection.value = name
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const unregisterSection = (name) => {
|
||||
const index = sections.value.findIndex(section => section.name === name)
|
||||
if (index !== -1) {
|
||||
sections.value.splice(index, 1)
|
||||
if (activeSection.value === name && sections.value.length > 0) {
|
||||
activeSection.value = sections.value[0].name
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Provide active section and registration function to child components
|
||||
provide('activeSection', activeSection)
|
||||
provide('registerSection', registerSection)
|
||||
provide('unregisterSection', unregisterSection)
|
||||
</script>
|
||||
|
|
@ -3,7 +3,8 @@
|
|||
<div
|
||||
v-if="show"
|
||||
ref="sidebar"
|
||||
class="absolute shadow-lg shadow-gray-800/30 top-0 h-[calc(100vh-53px)] right-0 lg:shadow-none lg:relative bg-white w-full md:w-1/2 lg:w-2/5 border-l overflow-y-scroll md:max-w-[20rem] flex-shrink-0 z-30"
|
||||
:class="widthClass"
|
||||
class="absolute shadow-lg shadow-gray-800/30 top-0 h-[calc(100vh-53px)] right-0 lg:shadow-none lg:relative bg-white w-full md:w-1/2 lg:w-2/5 border-l overflow-y-scroll flex-shrink-0 z-30"
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
|
|
@ -11,14 +12,18 @@
|
|||
</template>
|
||||
|
||||
<script setup>
|
||||
import { slideRight, useMotion } from "@vueuse/motion"
|
||||
import { watch } from "vue"
|
||||
import { slideRight, useMotion } from '@vueuse/motion'
|
||||
import { watch } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
show: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
widthClass: {
|
||||
type: String,
|
||||
default: 'md:max-w-[20rem]',
|
||||
},
|
||||
})
|
||||
|
||||
const sidebar = ref(null)
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
<UButtonGroup
|
||||
size="sm"
|
||||
orientation="horizontal"
|
||||
class="shadow-none"
|
||||
>
|
||||
<UTooltip
|
||||
text="Undo"
|
||||
|
|
@ -12,7 +13,7 @@
|
|||
:disabled="!canUndo"
|
||||
color="white"
|
||||
icon="i-material-symbols-undo"
|
||||
class="disabled:text-gray-500"
|
||||
class="disabled:text-gray-500 shadow-none"
|
||||
@click="undo"
|
||||
/>
|
||||
</UTooltip>
|
||||
|
|
@ -25,7 +26,7 @@
|
|||
:disabled="!canRedo"
|
||||
icon="i-material-symbols-redo"
|
||||
color="white"
|
||||
class="disabled:text-gray-500"
|
||||
class="disabled:text-gray-500 shadow-none"
|
||||
@click="redo"
|
||||
/>
|
||||
</UTooltip>
|
||||
|
|
|
|||
|
|
@ -16,11 +16,6 @@
|
|||
:class="{'mt-4':isEmbedPopup}"
|
||||
v-text="form.title"
|
||||
/>
|
||||
<div
|
||||
v-if="form.description"
|
||||
class="form-description mb-4 text-gray-700 dark:text-gray-300 whitespace-pre-wrap px-2"
|
||||
v-html="form.description"
|
||||
/>
|
||||
|
||||
<div v-if="isPublicFormPage && form.is_password_protected">
|
||||
<p class="form-description mb-4 text-gray-700 dark:text-gray-300 px-2">
|
||||
|
|
@ -52,19 +47,18 @@
|
|||
<v-transition name="fade">
|
||||
<div
|
||||
v-if="!form.is_password_protected && form.password && !hidePasswordDisabledMsg"
|
||||
class="border shadow-sm p-2 my-4 flex items-center rounded-md bg-yellow-100 dark:bg-yellow-600/20 border-yellow-500 dark:border-yellow-500/20"
|
||||
class="m-2 my-4 flex flex-grow items-end p-4 rounded-md dark:text-yellow-500 bg-yellow-50 dark:bg-yellow-600/20 dark:border-yellow-500"
|
||||
>
|
||||
<div class="flex flex-grow">
|
||||
<p class="mb-0 py-2 px-4 text-yellow-600 dark:text-yellow-600">
|
||||
We disabled the password protection for this form because you are an owner of it.
|
||||
</p>
|
||||
<v-button
|
||||
color="yellow"
|
||||
@click="hidePasswordDisabledMsg=true"
|
||||
>
|
||||
OK
|
||||
</v-button>
|
||||
</div>
|
||||
<p class="mb-0 text-yellow-600 dark:text-yellow-600 text-sm">
|
||||
We disabled the password protection for this form because you are an owner of it.
|
||||
</p>
|
||||
<UButton
|
||||
color="yellow"
|
||||
size="xs"
|
||||
@click="hidePasswordDisabledMsg = true"
|
||||
>
|
||||
Close
|
||||
</ubutton>
|
||||
</div>
|
||||
</v-transition>
|
||||
|
||||
|
|
@ -191,7 +185,7 @@
|
|||
<script>
|
||||
import OpenForm from './OpenForm.vue'
|
||||
import OpenFormButton from './OpenFormButton.vue'
|
||||
import VButton from '~/components/global/VButton.vue'
|
||||
import FormTimer from './FormTimer.vue'
|
||||
import FormCleanings from '../../pages/forms/show/FormCleanings.vue'
|
||||
import VTransition from '~/components/global/transitions/VTransition.vue'
|
||||
import {pendingSubmission} from "~/composables/forms/pendingSubmission.js"
|
||||
|
|
@ -199,7 +193,7 @@ import clonedeep from "clone-deep"
|
|||
import ThemeBuilder from "~/lib/forms/themes/ThemeBuilder.js"
|
||||
|
||||
export default {
|
||||
components: { VTransition, VButton, OpenFormButton, OpenForm, FormCleanings },
|
||||
components: { VTransition, OpenFormButton, OpenForm, FormCleanings, FormTimer },
|
||||
|
||||
props: {
|
||||
form: { type: Object, required: true },
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@
|
|||
mode="out-in"
|
||||
>
|
||||
<div
|
||||
:key="currentFieldGroupIndex"
|
||||
:key="formPageIndex"
|
||||
class="form-group flex flex-wrap w-full"
|
||||
>
|
||||
<draggable
|
||||
|
|
@ -54,6 +54,7 @@
|
|||
class="grid grid-cols-12 relative transition-all w-full"
|
||||
:class="{'rounded-md bg-blue-50':draggingNewBlock}"
|
||||
ghost-class="ghost-item"
|
||||
filter=".not-draggable"
|
||||
:animation="200"
|
||||
:disabled="!adminPreview"
|
||||
handle=".handle"
|
||||
|
|
@ -95,7 +96,7 @@
|
|||
<!-- Submit, Next and previous buttons -->
|
||||
<div class="flex flex-wrap justify-center w-full">
|
||||
<open-form-button
|
||||
v-if="currentFieldGroupIndex>0 && previousFieldsPageBreak && !loading"
|
||||
v-if="formPageIndex>0 && previousFieldsPageBreak && !loading"
|
||||
native-type="button"
|
||||
:color="form.color"
|
||||
:theme="theme"
|
||||
|
|
@ -139,6 +140,7 @@ import FormLogicPropertyResolver from "~/lib/forms/FormLogicPropertyResolver.js"
|
|||
import {computed} from "vue"
|
||||
import CachedDefaultTheme from "~/lib/forms/themes/CachedDefaultTheme.js"
|
||||
import FormTimer from './FormTimer.vue'
|
||||
import { storeToRefs } from 'pinia'
|
||||
|
||||
export default {
|
||||
name: 'OpenForm',
|
||||
|
|
@ -169,7 +171,7 @@ export default {
|
|||
type: Array,
|
||||
required: true
|
||||
},
|
||||
defaultDataForm: {},
|
||||
defaultDataForm: { type: [Object, null] },
|
||||
adminPreview: {type: Boolean, default: false}, // If used in FormEditorPreview
|
||||
urlPrefillPreview: {type: Boolean, default: false}, // If used in UrlFormPrefill
|
||||
darkMode: {
|
||||
|
|
@ -177,7 +179,7 @@ export default {
|
|||
default: false
|
||||
}
|
||||
},
|
||||
|
||||
emits: ['submit'],
|
||||
setup(props) {
|
||||
const recordsStore = useRecordsStore()
|
||||
const workingFormStore = useWorkingFormStore()
|
||||
|
|
@ -189,13 +191,18 @@ export default {
|
|||
workingFormStore,
|
||||
isIframe: useIsIframe(),
|
||||
draggingNewBlock: computed(() => workingFormStore.draggingNewBlock),
|
||||
pendingSubmission: pendingSubmission(props.form)
|
||||
pendingSubmission: pendingSubmission(props.form),
|
||||
formPageIndex: storeToRefs(workingFormStore).formPageIndex,
|
||||
|
||||
// Used for admin previews
|
||||
selectedFieldIndex: computed(() => workingFormStore.selectedFieldIndex),
|
||||
showEditFieldSidebar: computed(() => workingFormStore.showEditFieldSidebar),
|
||||
}
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
currentFieldGroupIndex: 0,
|
||||
// Page index
|
||||
/**
|
||||
* Used to force refresh components by changing their keys
|
||||
*/
|
||||
|
|
@ -237,16 +244,16 @@ export default {
|
|||
},
|
||||
currentFields: {
|
||||
get() {
|
||||
return this.fieldGroups[this.currentFieldGroupIndex]
|
||||
return this.fieldGroups[this.formPageIndex]
|
||||
},
|
||||
set(val) {
|
||||
// On re-order from the form, set the new order
|
||||
// Add the previous groups and next to val, and set the properties on working form
|
||||
const newFields = []
|
||||
this.fieldGroups.forEach((group, index) => {
|
||||
if (index < this.currentFieldGroupIndex) {
|
||||
if (index < this.formPageIndex) {
|
||||
newFields.push(...group)
|
||||
} else if (index === this.currentFieldGroupIndex) {
|
||||
} else if (index === this.formPageIndex) {
|
||||
newFields.push(...val)
|
||||
} else {
|
||||
newFields.push(...group)
|
||||
|
|
@ -267,8 +274,8 @@ export default {
|
|||
return null
|
||||
},
|
||||
previousFieldsPageBreak() {
|
||||
if (this.currentFieldGroupIndex === 0) return null
|
||||
const previousFields = this.fieldGroups[this.currentFieldGroupIndex - 1]
|
||||
if (this.formPageIndex === 0) return null
|
||||
const previousFields = this.fieldGroups[this.formPageIndex - 1]
|
||||
const block = previousFields[previousFields.length - 1]
|
||||
if (block && block.type === 'nf-page-break') return block
|
||||
return null
|
||||
|
|
@ -278,7 +285,7 @@ export default {
|
|||
* @returns {boolean}xs
|
||||
*/
|
||||
isLastPage() {
|
||||
return this.currentFieldGroupIndex === (this.fieldGroups.length - 1)
|
||||
return this.formPageIndex === (this.fieldGroups.length - 1)
|
||||
},
|
||||
isPublicFormPage() {
|
||||
return this.$route.name === 'forms-slug'
|
||||
|
|
@ -328,11 +335,28 @@ export default {
|
|||
this.pendingSubmission.set(this.dataFormValue)
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// These watchers ensure the form shows the correct page for the field being edited in admin preview
|
||||
selectedFieldIndex: {
|
||||
handler(newIndex) {
|
||||
if (this.adminPreview && this.showEditFieldSidebar) {
|
||||
this.setPageForField(newIndex)
|
||||
}
|
||||
}
|
||||
},
|
||||
showEditFieldSidebar: {
|
||||
handler(newValue) {
|
||||
if (this.adminPreview && newValue) {
|
||||
this.setPageForField(this.selectedFieldIndex)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
beforeMount() {
|
||||
this.initForm()
|
||||
},
|
||||
mounted() {
|
||||
this.$refs['form-timer'].startTimer()
|
||||
if (import.meta.client && window.location.href.includes('auto_submit=true')) {
|
||||
this.isAutoSubmit = true
|
||||
|
|
@ -342,9 +366,7 @@ export default {
|
|||
|
||||
methods: {
|
||||
submitForm() {
|
||||
if (this.currentFieldGroupIndex !== this.fieldGroups.length - 1) {
|
||||
return
|
||||
}
|
||||
if (!this.isAutoSubmit && this.formPageIndex !== this.fieldGroups.length - 1) return
|
||||
|
||||
if (this.form.use_captcha && import.meta.client) {
|
||||
this.dataForm['h-captcha-response'] = document.getElementsByName('h-captcha-response')[0].value
|
||||
|
|
@ -361,32 +383,30 @@ export default {
|
|||
this.$emit('submit', this.dataForm, this.onSubmissionFailure)
|
||||
},
|
||||
/**
|
||||
* If more than one page, show first page with error
|
||||
* Handle form submission failure
|
||||
*/
|
||||
onSubmissionFailure() {
|
||||
this.$refs['form-timer'].startTimer()
|
||||
this.isAutoSubmit = false
|
||||
if (this.fieldGroups.length > 1) {
|
||||
// Find first mistake and show page
|
||||
let pageChanged = false
|
||||
this.fieldGroups.forEach((group, groupIndex) => {
|
||||
group.forEach((field) => {
|
||||
if (pageChanged) return
|
||||
|
||||
if (!pageChanged && this.dataForm.errors.has(field.id)) {
|
||||
this.currentFieldGroupIndex = groupIndex
|
||||
pageChanged = true
|
||||
}
|
||||
})
|
||||
})
|
||||
this.showFirstPageWithError()
|
||||
}
|
||||
|
||||
// Scroll to error
|
||||
this.scrollToFirstError()
|
||||
},
|
||||
showFirstPageWithError() {
|
||||
for (let i = 0; i < this.fieldGroups.length; i++) {
|
||||
if (this.fieldGroups[i].some(field => this.dataForm.errors.has(field.id))) {
|
||||
this.formPageIndex = i
|
||||
break
|
||||
}
|
||||
}
|
||||
},
|
||||
scrollToFirstError() {
|
||||
if (import.meta.server) return
|
||||
const elements = document.getElementsByClassName('has-error')
|
||||
if (elements.length > 0) {
|
||||
const firstErrorElement = document.querySelector('.has-error')
|
||||
if (firstErrorElement) {
|
||||
window.scroll({
|
||||
top: window.scrollY + elements[0].getBoundingClientRect().top - 60,
|
||||
top: window.scrollY + firstErrorElement.getBoundingClientRect().top - 60,
|
||||
behavior: 'smooth'
|
||||
})
|
||||
}
|
||||
|
|
@ -402,130 +422,160 @@ export default {
|
|||
)
|
||||
return this.recordsStore.getByKey(this.form.submission_id)
|
||||
},
|
||||
|
||||
/**
|
||||
* Form initialization
|
||||
*/
|
||||
async initForm() {
|
||||
if (this.defaultDataForm) {
|
||||
this.dataForm = useForm(this.defaultDataForm)
|
||||
return
|
||||
}
|
||||
|
||||
if (await this.tryInitFormFromEditableSubmission()) return
|
||||
if (this.tryInitFormFromPendingSubmission()) return
|
||||
|
||||
this.initFormWithDefaultValues()
|
||||
},
|
||||
async tryInitFormFromEditableSubmission() {
|
||||
if (this.isPublicFormPage && this.form.editable_submissions) {
|
||||
if (useRoute().query?.submission_id) {
|
||||
this.form.submission_id = useRoute().query?.submission_id
|
||||
const submissionId = useRoute().query?.submission_id
|
||||
if (submissionId) {
|
||||
this.form.submission_id = submissionId
|
||||
const data = await this.getSubmissionData()
|
||||
if (data !== null && data) {
|
||||
if (data) {
|
||||
this.dataForm = useForm(data)
|
||||
return
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
},
|
||||
tryInitFormFromPendingSubmission() {
|
||||
if (this.isPublicFormPage && this.form.auto_save) {
|
||||
const pendingData = this.pendingSubmission.get()
|
||||
if (pendingData !== null && pendingData && Object.keys(this.pendingSubmission.get()).length !== 0) {
|
||||
this.fields.forEach((field) => {
|
||||
if (field.type === 'date' && field.prefill_today === true) { // For Prefill with 'today'
|
||||
pendingData[field.id] = new Date().toISOString()
|
||||
}
|
||||
})
|
||||
if (pendingData && Object.keys(pendingData).length !== 0) {
|
||||
this.updatePendingDataFields(pendingData)
|
||||
this.dataForm = useForm(pendingData)
|
||||
return
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
const formData = clonedeep(this.dataForm ? this.dataForm.data() : {})
|
||||
let urlPrefill = null
|
||||
if (this.isPublicFormPage) {
|
||||
urlPrefill = new URLSearchParams(window.location.search)
|
||||
}
|
||||
|
||||
this.fields.forEach((field) => {
|
||||
if (field.type.startsWith('nf-')) {
|
||||
return
|
||||
}
|
||||
|
||||
if (urlPrefill && urlPrefill.has(field.id)) {
|
||||
// Url prefills
|
||||
if (field.type === 'checkbox') {
|
||||
if (urlPrefill.get(field.id) === 'false' || urlPrefill.get(field.id) === '0') {
|
||||
formData[field.id] = false
|
||||
} else if (urlPrefill.get(field.id) === 'true' || urlPrefill.get(field.id) === '1') {
|
||||
formData[field.id] = true
|
||||
}
|
||||
} else {
|
||||
formData[field.id] = urlPrefill.get(field.id)
|
||||
}
|
||||
} else if (urlPrefill && urlPrefill.has(field.id + '[]')) {
|
||||
// Array url prefills
|
||||
formData[field.id] = urlPrefill.getAll(field.id + '[]')
|
||||
} else if (field.type === 'date' && field.prefill_today === true) { // For Prefill with 'today'
|
||||
formData[field.id] = new Date().toISOString()
|
||||
} else if (field.type === 'matrix') {
|
||||
formData[field.id] = {...field.prefill}
|
||||
} else { // Default prefill if any
|
||||
formData[field.id] = field.prefill
|
||||
return false
|
||||
},
|
||||
updatePendingDataFields(pendingData) {
|
||||
this.fields.forEach(field => {
|
||||
if (field.type === 'date' && field.prefill_today) {
|
||||
pendingData[field.id] = new Date().toISOString()
|
||||
}
|
||||
})
|
||||
},
|
||||
initFormWithDefaultValues() {
|
||||
const formData = clonedeep(this.dataForm?.data() || {})
|
||||
const urlPrefill = this.isPublicFormPage ? new URLSearchParams(window.location.search) : null
|
||||
|
||||
this.fields.forEach(field => {
|
||||
if (field.type.startsWith('nf-')) return
|
||||
|
||||
this.handleUrlPrefill(field, formData, urlPrefill)
|
||||
this.handleDefaultPrefill(field, formData)
|
||||
})
|
||||
|
||||
this.dataForm = useForm(formData)
|
||||
},
|
||||
handleUrlPrefill(field, formData, urlPrefill) {
|
||||
if (!urlPrefill) return
|
||||
|
||||
const prefillValue = urlPrefill.get(field.id)
|
||||
const arrayPrefillValue = urlPrefill.getAll(field.id + '[]')
|
||||
|
||||
if (prefillValue !== null) {
|
||||
formData[field.id] = field.type === 'checkbox' ? this.parseBooleanValue(prefillValue) : prefillValue
|
||||
} else if (arrayPrefillValue.length > 0) {
|
||||
formData[field.id] = arrayPrefillValue
|
||||
}
|
||||
},
|
||||
parseBooleanValue(value) {
|
||||
return value === 'true' || value === '1'
|
||||
},
|
||||
handleDefaultPrefill(field, formData) {
|
||||
if (field.type === 'date' && field.prefill_today) {
|
||||
formData[field.id] = new Date().toISOString()
|
||||
} else if (field.type === 'matrix') {
|
||||
formData[field.id] = {...field.prefill}
|
||||
} else if (!(field.id in formData)) {
|
||||
formData[field.id] = field.prefill
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Page Navigation
|
||||
*/
|
||||
previousPage() {
|
||||
this.currentFieldGroupIndex -= 1
|
||||
window.scrollTo({top: 0, behavior: 'smooth'})
|
||||
return false
|
||||
this.formPageIndex--
|
||||
this.scrollToTop()
|
||||
},
|
||||
nextPage() {
|
||||
if (this.adminPreview || this.urlPrefillPreview) {
|
||||
this.currentFieldGroupIndex += 1
|
||||
window.scrollTo({top: 0, behavior: 'smooth'})
|
||||
this.formPageIndex++
|
||||
this.scrollToTop()
|
||||
return false
|
||||
}
|
||||
const fieldsToValidate = this.currentFields.map(f => f.id)
|
||||
this.dataForm.busy = true
|
||||
this.dataForm.validate('POST', '/forms/' + this.form.slug + '/answer', {}, fieldsToValidate)
|
||||
.then((data) => {
|
||||
this.currentFieldGroupIndex += 1
|
||||
.then(() => {
|
||||
this.formPageIndex++
|
||||
this.dataForm.busy = false
|
||||
window.scrollTo({top: 0, behavior: 'smooth'})
|
||||
}).catch(error => {
|
||||
console.error(error)
|
||||
if (error && error.data && error.data.message) {
|
||||
useAlert().error(error.data.message)
|
||||
}
|
||||
this.dataForm.busy = false
|
||||
})
|
||||
this.scrollToTop()
|
||||
}).catch(this.handleValidationError)
|
||||
return false
|
||||
},
|
||||
scrollToTop() {
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' })
|
||||
},
|
||||
handleValidationError(error) {
|
||||
console.error(error)
|
||||
if (error?.data?.message) {
|
||||
useAlert().error(error.data.message)
|
||||
}
|
||||
this.dataForm.busy = false
|
||||
},
|
||||
isFieldHidden(field) {
|
||||
return (new FormLogicPropertyResolver(field, this.dataFormValue)).isHidden()
|
||||
},
|
||||
getTargetFieldIndex(currentFieldPageIndex) {
|
||||
let targetIndex = 0
|
||||
if (this.currentFieldGroupIndex > 0) {
|
||||
for (let i = 0; i < this.currentFieldGroupIndex; i++) {
|
||||
targetIndex += this.fieldGroups[i].length
|
||||
}
|
||||
targetIndex += currentFieldPageIndex
|
||||
} else {
|
||||
targetIndex = currentFieldPageIndex
|
||||
}
|
||||
return targetIndex
|
||||
return this.formPageIndex > 0
|
||||
? this.fieldGroups.slice(0, this.formPageIndex).reduce((sum, group) => sum + group.length, 0) + currentFieldPageIndex
|
||||
: currentFieldPageIndex
|
||||
},
|
||||
handleDragDropped(data) {
|
||||
if (data.added) {
|
||||
const targetIndex = this.getTargetFieldIndex(data.added.newIndex)
|
||||
this.workingFormStore.addBlock(data.added.element, targetIndex)
|
||||
}
|
||||
|
||||
if (data.moved) {
|
||||
const oldTargetIndex = this.getTargetFieldIndex(data.moved.oldIndex)
|
||||
const newTargetIndex = this.getTargetFieldIndex(data.moved.newIndex)
|
||||
this.workingFormStore.moveField(oldTargetIndex, newTargetIndex)
|
||||
}
|
||||
},
|
||||
setMinHeight(minHeight) {
|
||||
if (!this.isIframe) {
|
||||
return
|
||||
setPageForField(fieldIndex) {
|
||||
if (fieldIndex === -1) return
|
||||
|
||||
let currentIndex = 0
|
||||
for (let i = 0; i < this.fieldGroups.length; i++) {
|
||||
currentIndex += this.fieldGroups[i].length
|
||||
if (currentIndex > fieldIndex) {
|
||||
this.formPageIndex = i
|
||||
return
|
||||
}
|
||||
}
|
||||
this.formPageIndex = this.fieldGroups.length - 1
|
||||
},
|
||||
setMinHeight(minHeight) {
|
||||
if (!this.isIframe) return
|
||||
|
||||
this.minHeight = minHeight
|
||||
// Trigger window iframe resize
|
||||
try {
|
||||
window.parentIFrame.size()
|
||||
} catch (e) {
|
||||
|
|
|
|||
|
|
@ -2,13 +2,16 @@
|
|||
<div
|
||||
v-if="!isFieldHidden"
|
||||
:id="'block-' + field.id"
|
||||
ref="form-block"
|
||||
class="px-2"
|
||||
:class="[
|
||||
getFieldWidthClasses(field),
|
||||
{
|
||||
'group/nffield hover:bg-gray-100/50 relative hover:z-10 transition-colors hover:border-gray-200 dark:hover:!bg-gray-900 border-dashed border border-transparent box-border dark:hover:border-blue-900 rounded-md':adminPreview,
|
||||
'group/nffield hover:bg-gray-100/50 relative hover:z-10 transition-colors hover:border-gray-200 dark:hover:!bg-gray-900 border-dashed border border-transparent box-border dark:hover:border-blue-900 rounded-md': adminPreview,
|
||||
'cursor-pointer':workingFormStore.showEditFieldSidebar && adminPreview,
|
||||
'bg-blue-50 hover:!bg-blue-50 dark:bg-gray-800 rounded-md': beingEdited,
|
||||
}]"
|
||||
@click="setFieldAsSelected"
|
||||
>
|
||||
<div
|
||||
class="-m-[1px] w-full max-w-full mx-auto"
|
||||
|
|
@ -16,14 +19,13 @@
|
|||
>
|
||||
<div
|
||||
v-if="adminPreview"
|
||||
class="absolute -translate-x-full -left-1 top-1 bottom-0 hidden group-hover/nffield:block"
|
||||
class="absolute translate-y-full lg:translate-y-0 -bottom-1.5 left-1/2 -translate-x-1/2 lg:-translate-x-full lg:-left-1 lg:top-1 lg:bottom-0 hidden group-hover/nffield:block"
|
||||
>
|
||||
<div
|
||||
class="flex flex-col -space-1 bg-white rounded-md shadow -mt-1"
|
||||
:class="{ 'lg:flex-row lg:-space-x-2': !fieldSideBarOpened, 'xl:flex-row xl:-space-x-1': fieldSideBarOpened }"
|
||||
class="flex lg:flex-col bg-gray-100 dark:bg-gray-800 border rounded-md"
|
||||
>
|
||||
<div
|
||||
class="p-1 -mb-2 text-gray-300 hover:text-blue-500 cursor-pointer"
|
||||
class="p-1 lg:pt-0 hover:text-blue-500 cursor-pointer text-gray-400 dark:text-gray-500 dark:border-gray-500"
|
||||
role="button"
|
||||
@click.prevent="openAddFieldSidebar"
|
||||
>
|
||||
|
|
@ -33,7 +35,7 @@
|
|||
/>
|
||||
</div>
|
||||
<div
|
||||
class="p-1 text-gray-300 hover:text-blue-500 cursor-pointer text-center"
|
||||
class="p-1 lg:pt-0 hover:text-blue-500 cursor-pointer flex items-center justify-center text-center text-gray-400 dark:text-gray-500 dark:border-gray-500"
|
||||
role="button"
|
||||
@click.prevent="editFieldOptions"
|
||||
>
|
||||
|
|
@ -42,6 +44,16 @@
|
|||
class="w-5 h-5"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="p-1 pt-0 hover:text-blue-500 mt-1 cursor-pointer flex items-center justify-center text-center text-gray-400 dark:text-gray-500 dark:border-gray-500"
|
||||
role="button"
|
||||
@click.prevent="removeField"
|
||||
>
|
||||
<Icon
|
||||
name="heroicons:trash-20-solid"
|
||||
class="w-5 h-5"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<component
|
||||
|
|
@ -82,9 +94,13 @@
|
|||
>
|
||||
<div
|
||||
v-if="!field.image_block"
|
||||
class="p-4 border border-dashed"
|
||||
class="p-4 border border-dashed text-center"
|
||||
>
|
||||
Open <b>{{ field.name }}'s</b> block settings to upload image.
|
||||
<a
|
||||
href="#"
|
||||
class="text-blue-800 dark:text-blue-200"
|
||||
@click.prevent="editFieldOptions"
|
||||
>Open block settings to upload image.</a>
|
||||
</div>
|
||||
<img
|
||||
v-else
|
||||
|
|
@ -255,11 +271,21 @@ export default {
|
|||
|
||||
methods: {
|
||||
editFieldOptions() {
|
||||
if (!this.adminPreview) return
|
||||
this.workingFormStore.openSettingsForField(this.field)
|
||||
},
|
||||
setFieldAsSelected () {
|
||||
if (!this.adminPreview || !this.workingFormStore.showEditFieldSidebar) return
|
||||
this.workingFormStore.openSettingsForField(this.field)
|
||||
},
|
||||
openAddFieldSidebar() {
|
||||
if (!this.adminPreview) return
|
||||
this.workingFormStore.openAddFieldSidebar(this.field)
|
||||
},
|
||||
removeField () {
|
||||
if (!this.adminPreview) return
|
||||
this.workingFormStore.removeField(this.field)
|
||||
},
|
||||
getFieldWidthClasses(field) {
|
||||
if (!field.width || field.width === 'full') return 'col-span-full'
|
||||
else if (field.width === '1/2') {
|
||||
|
|
@ -309,6 +335,10 @@ export default {
|
|||
inputProperties.columns = field.columns
|
||||
}
|
||||
|
||||
if (['select','multi_select'].includes(field.type) && !this.isFieldRequired) {
|
||||
inputProperties.clearable = true
|
||||
}
|
||||
|
||||
if (['select', 'multi_select'].includes(field.type)) {
|
||||
inputProperties.options = (_has(field, field.type))
|
||||
? field[field.type].options.map(option => {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,26 @@
|
|||
<template>
|
||||
<div
|
||||
class="p-1 rounded-md flex items-center justify-center"
|
||||
:class="[bgClass, textClass]"
|
||||
>
|
||||
<Icon
|
||||
:name="icon"
|
||||
class="h-4 w-4"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import blocksTypes from '~/data/blocks_types.json'
|
||||
|
||||
const props = defineProps({
|
||||
type: {
|
||||
type: String,
|
||||
required: true
|
||||
}
|
||||
})
|
||||
|
||||
const bgClass = computed(() => blocksTypes[props.type]?.bg_class || '')
|
||||
const textClass = computed(() => blocksTypes[props.type]?.text_class || '')
|
||||
const icon = computed(() => blocksTypes[props.type]?.icon || '')
|
||||
</script>
|
||||
|
|
@ -1,19 +1,12 @@
|
|||
<template>
|
||||
<collapse
|
||||
v-model="show"
|
||||
class="p-2 w-full"
|
||||
>
|
||||
<template #title>
|
||||
<h3 class="font-semibold block text-lg">
|
||||
Validation
|
||||
</h3>
|
||||
<p class="text-gray-400 text-xs mb-3">
|
||||
Add some custom validation (save form before testing)
|
||||
</p>
|
||||
</template>
|
||||
<div class="py-2 px-4">
|
||||
<p class="text-gray-500 text-xs mb-3">
|
||||
Add some custom validation. Save your form before testing.
|
||||
</p>
|
||||
|
||||
<div class="py-2">
|
||||
<p class="font-semibold text-sm text-gray-700">
|
||||
Conditions for this field to be accepted
|
||||
Validation criteria for field acceptance
|
||||
</p>
|
||||
<condition-editor
|
||||
ref="filter-editor"
|
||||
|
|
@ -25,10 +18,11 @@
|
|||
name="error_message"
|
||||
class=""
|
||||
:form="field.validation"
|
||||
label="Error message when validation fails"
|
||||
label="Error message"
|
||||
help="Displayed when the validation fails"
|
||||
/>
|
||||
</div>
|
||||
</collapse>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
|
|
|||
|
|
@ -4,120 +4,79 @@
|
|||
id="form-editor"
|
||||
class="relative flex w-full flex-col grow max-h-screen"
|
||||
>
|
||||
<!-- Navbar -->
|
||||
<div class="w-full border-b p-2 flex items-center justify-between bg-white">
|
||||
<a
|
||||
v-if="backButton"
|
||||
href="#"
|
||||
class="ml-2 flex text-blue font-semibold text-sm"
|
||||
@click.prevent="goBack"
|
||||
>
|
||||
<svg
|
||||
class="w-3 h-3 text-blue mt-1 mr-1"
|
||||
viewBox="0 0 6 10"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M5 9L1 5L5 1"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.5"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
Go back
|
||||
</a>
|
||||
|
||||
<UndoRedo />
|
||||
|
||||
<div class="hidden md:flex items-center ml-3">
|
||||
<h3 class="font-semibold text-lg max-w-[14rem] truncate text-gray-500">
|
||||
{{ form.title }}
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="flex items-center"
|
||||
:class="{ 'mx-auto md:mx-0': !backButton }"
|
||||
>
|
||||
<div class="hidden md:block mr-10 relative">
|
||||
<a
|
||||
href="#"
|
||||
class="text-sm px-3 py-2 hover:bg-gray-50 cursor-pointer rounded-md text-gray-500 px-0 sm:px-3 hover:text-gray-800 cursor-pointer mt-1"
|
||||
@click.prevent="openCrisp"
|
||||
>
|
||||
Help
|
||||
</a>
|
||||
</div>
|
||||
<v-button
|
||||
v-track.save_form_click
|
||||
size="small"
|
||||
class="w-full px-8 md:px-4 py-2"
|
||||
:loading="updateFormLoading"
|
||||
:class="saveButtonClass"
|
||||
@click="saveForm"
|
||||
>
|
||||
<svg
|
||||
class="w-4 h-4 text-white inline mr-1 -mt-1"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M17 21V13H7V21M7 3V8H15M19 21H5C4.46957 21 3.96086 20.7893 3.58579 20.4142C3.21071 20.0391 3 19.5304 3 19V5C3 4.46957 3.21071 3.96086 3.58579 3.58579C3.96086 3.21071 4.46957 3 5 3H16L21 8V19C21 19.5304 20.7893 20.0391 20.4142 20.4142C20.0391 20.7893 19.5304 21 19 21Z"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
<template v-if="form.visibility === 'public'">
|
||||
Publish Form
|
||||
</template>
|
||||
<template v-else>
|
||||
Save Changes
|
||||
</template>
|
||||
</v-button>
|
||||
</div>
|
||||
</div>
|
||||
<FormEditorErrorHandler>
|
||||
|
||||
<div class="w-full flex grow overflow-y-scroll relative bg-gray-50">
|
||||
<div
|
||||
class="relative w-full bg-white shrink-0 overflow-y-scroll border-r md:w-1/2 md:max-w-sm lg:w-2/5"
|
||||
>
|
||||
<div class="border-b bg-blue-50 p-5 text-nt-blue-dark md:hidden">
|
||||
Please create this form on a device with a larger screen. That will
|
||||
allow you to preview your form changes.
|
||||
</div>
|
||||
|
||||
<VForm
|
||||
size="sm"
|
||||
@submit.prevent=""
|
||||
>
|
||||
<form-information />
|
||||
<form-structure />
|
||||
<form-customization />
|
||||
<form-about-submission />
|
||||
<form-access />
|
||||
<form-security-privacy />
|
||||
<form-custom-seo />
|
||||
<form-custom-code />
|
||||
</VForm>
|
||||
</div>
|
||||
|
||||
<form-editor-preview />
|
||||
<form-editor-sidebar />
|
||||
|
||||
<!-- Form Error Modal -->
|
||||
<form-error-modal
|
||||
:show="showFormErrorModal"
|
||||
:form="form"
|
||||
@close="showFormErrorModal = false"
|
||||
<div
|
||||
class="border-b bg-white md:hidden fixed inset-0 w-full z-50 flex flex-col items-center justify-center"
|
||||
>
|
||||
<Icon
|
||||
name="heroicons:exclamation-circle"
|
||||
class="w-10 h-10 text-nt-blue-dark"
|
||||
/>
|
||||
<div class="p-5 text-nt-blue-dark text-center">
|
||||
OpnForm is not optimized for mobile devices. Please open this page on a device with a larger screen.
|
||||
</div>
|
||||
<div>
|
||||
<UButton
|
||||
color="white"
|
||||
size="lg"
|
||||
class="w-full"
|
||||
:to="{ name: 'home' }"
|
||||
>
|
||||
Back to dashboard
|
||||
</UButton>
|
||||
</div>
|
||||
</div>
|
||||
</FormEditorErrorHandler>
|
||||
|
||||
<FormEditorNavbar
|
||||
:back-button="backButton"
|
||||
:update-form-loading="updateFormLoading"
|
||||
:save-button-class="saveButtonClass"
|
||||
@go-back="goBack"
|
||||
@save-form="saveForm"
|
||||
>
|
||||
<template #before-save>
|
||||
<slot name="before-save" />
|
||||
</template>
|
||||
</FormEditorNavbar>
|
||||
|
||||
<FormEditorErrorHandler>
|
||||
<div
|
||||
v-show="activeTab !== 2"
|
||||
class="w-full flex grow overflow-y-scroll relative bg-white"
|
||||
>
|
||||
<div
|
||||
class="relative w-full shrink-0 overflow-y-scroll border-r md:w-1/2 md:max-w-xs lg:w-2/5"
|
||||
>
|
||||
<VForm
|
||||
size="sm"
|
||||
@submit.prevent=""
|
||||
>
|
||||
<div
|
||||
v-show="activeTab === 0"
|
||||
>
|
||||
<FormFieldsEditor />
|
||||
</div>
|
||||
<div
|
||||
v-show="activeTab === 1"
|
||||
>
|
||||
<FormCustomization />
|
||||
</div>
|
||||
</VForm>
|
||||
</div>
|
||||
|
||||
<FormEditorPreview />
|
||||
|
||||
<FormEditorSidebar />
|
||||
</div>
|
||||
</FormEditorErrorHandler>
|
||||
|
||||
<FormSettings v-show="activeTab === 2" />
|
||||
|
||||
<!-- Form Error Modal -->
|
||||
<FormErrorModal
|
||||
:show="showFormErrorModal"
|
||||
:validation-error-response="validationErrorResponse"
|
||||
@close="showFormErrorModal = false"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
|
|
@ -128,39 +87,30 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import UndoRedo from "../../editors/UndoRedo.vue"
|
||||
import FormEditorNavbar from './FormEditorNavbar.vue'
|
||||
import FormEditorSidebar from "./form-components/FormEditorSidebar.vue"
|
||||
import FormErrorModal from "./form-components/FormErrorModal.vue"
|
||||
import FormInformation from "./form-components/FormInformation.vue"
|
||||
import FormStructure from "./form-components/FormStructure.vue"
|
||||
import FormFieldsEditor from './FormFieldsEditor.vue'
|
||||
import FormCustomization from "./form-components/FormCustomization.vue"
|
||||
import FormCustomCode from "./form-components/FormCustomCode.vue"
|
||||
import FormAboutSubmission from "./form-components/FormAboutSubmission.vue"
|
||||
import FormEditorPreview from "./form-components/FormEditorPreview.vue"
|
||||
import FormSecurityPrivacy from "./form-components/FormSecurityPrivacy.vue"
|
||||
import FormCustomSeo from "./form-components/FormCustomSeo.vue"
|
||||
import FormAccess from "./form-components/FormAccess.vue"
|
||||
import { validatePropertiesLogic } from "~/composables/forms/validatePropertiesLogic.js"
|
||||
import opnformConfig from "~/opnform.config.js"
|
||||
import { captureException } from "@sentry/core"
|
||||
import FormSettings from './form-components/FormSettings.vue'
|
||||
import FormEditorErrorHandler from '~/components/open/forms/components/FormEditorErrorHandler.vue'
|
||||
import { setFormDefaults } from '~/composables/forms/initForm.js'
|
||||
|
||||
export default {
|
||||
name: "FormEditor",
|
||||
components: {
|
||||
FormEditorNavbar,
|
||||
FormEditorErrorHandler,
|
||||
UndoRedo,
|
||||
FormEditorSidebar,
|
||||
FormEditorPreview,
|
||||
FormAboutSubmission,
|
||||
FormCustomCode,
|
||||
FormCustomization,
|
||||
FormStructure,
|
||||
FormInformation,
|
||||
FormFieldsEditor,
|
||||
FormErrorModal,
|
||||
FormSecurityPrivacy,
|
||||
FormCustomSeo,
|
||||
FormAccess,
|
||||
FormSettings
|
||||
},
|
||||
props: {
|
||||
isEdit: {
|
||||
|
|
@ -185,13 +135,15 @@ export default {
|
|||
},
|
||||
},
|
||||
|
||||
emits: ['mounted', 'on-save', 'openRegister'],
|
||||
emits: ['mounted', 'on-save', 'openRegister', 'go-back', 'save-form'],
|
||||
|
||||
setup() {
|
||||
const { user } = storeToRefs(useAuthStore())
|
||||
const formsStore = useFormsStore()
|
||||
const { content: form } = storeToRefs(useWorkingFormStore())
|
||||
const { getCurrent: workspace } = storeToRefs(useWorkspacesStore())
|
||||
const workingFormStore = useWorkingFormStore()
|
||||
|
||||
return {
|
||||
appStore: useAppStore(),
|
||||
crisp: useCrisp(),
|
||||
|
|
@ -201,6 +153,8 @@ export default {
|
|||
formsStore,
|
||||
form,
|
||||
user,
|
||||
workingFormStore,
|
||||
activeTab: computed(() => workingFormStore.activeTab)
|
||||
}
|
||||
},
|
||||
|
||||
|
|
@ -216,51 +170,18 @@ export default {
|
|||
computed: {
|
||||
createdForm() {
|
||||
return this.formsStore.getByKey(this.createdFormSlug)
|
||||
},
|
||||
steps() {
|
||||
return [
|
||||
{
|
||||
target: "#v-step-0",
|
||||
header: {
|
||||
title: "Welcome to the OpnForm Editor!",
|
||||
},
|
||||
content: "Discover <strong>your form Editor</strong>!",
|
||||
},
|
||||
{
|
||||
target: "#v-step-1",
|
||||
header: {
|
||||
title: "Change your form fields",
|
||||
},
|
||||
content:
|
||||
"Here you can decide which field to include or not, but also the " +
|
||||
"order you want your fields to be and so on. You also have custom options available for each field, just " +
|
||||
"click the blue cog.",
|
||||
},
|
||||
{
|
||||
target: "#v-step-2",
|
||||
header: {
|
||||
title: "Notifications, Customizations and more!",
|
||||
},
|
||||
content:
|
||||
"Many more options are available: change colors, texts and receive a " +
|
||||
"notifications whenever someones submits your form.",
|
||||
},
|
||||
{
|
||||
target: ".v-last-step",
|
||||
header: {
|
||||
title: "Create your form",
|
||||
},
|
||||
content: "Click this button when you're done to save your form!",
|
||||
},
|
||||
]
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
watch: {},
|
||||
|
||||
mounted() {
|
||||
this.$emit("mounted")
|
||||
useAmplitude().logEvent('form_editor_viewed')
|
||||
this.appStore.hideNavbar()
|
||||
if (!this.isEdit) {
|
||||
this.$nextTick(() => {
|
||||
this.workingFormStore.openAddFieldSidebar()
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
beforeUnmount() {
|
||||
|
|
@ -294,6 +215,11 @@ export default {
|
|||
this.showFormErrorModal = true
|
||||
},
|
||||
saveForm() {
|
||||
// Apply defaults to the form
|
||||
const defaultedData = setFormDefaults(this.form.data())
|
||||
console.log('defaultedData', defaultedData)
|
||||
this.form.fill(defaultedData)
|
||||
|
||||
this.form.properties = validatePropertiesLogic(this.form.properties)
|
||||
if (this.isGuest) {
|
||||
this.saveFormGuest()
|
||||
|
|
|
|||
|
|
@ -0,0 +1,150 @@
|
|||
<template>
|
||||
<div class="w-full border-b p-2 flex gap-x-3 items-center bg-white">
|
||||
<a
|
||||
v-if="backButton"
|
||||
href="#"
|
||||
class="ml-2 flex text-blue font-semibold text-sm -m-1 hover:bg-blue-500/10 rounded-md p-1 group"
|
||||
@click.prevent="$emit('go-back')"
|
||||
>
|
||||
<Icon
|
||||
name="heroicons:arrow-left-20-solid"
|
||||
class="text-blue mr-1 w-6 h-6 group-hover:-translate-x-0.5 transition-all"
|
||||
/>
|
||||
</a>
|
||||
|
||||
|
||||
<EditableTag
|
||||
v-model="form.title"
|
||||
element="h3"
|
||||
class="font-medium py-1 text-md w-48 text-gray-500 truncate"
|
||||
/>
|
||||
|
||||
<UBadge
|
||||
v-if="form.visibility == 'draft'"
|
||||
color="yellow"
|
||||
variant="soft"
|
||||
label="Draft"
|
||||
/>
|
||||
<UBadge
|
||||
v-else-if="form.visibility == 'closed'"
|
||||
color="gray"
|
||||
variant="soft"
|
||||
label="Closed"
|
||||
/>
|
||||
|
||||
<UndoRedo />
|
||||
|
||||
<div class="flex-grow flex justify-center">
|
||||
<UTabs
|
||||
v-model="activeTab"
|
||||
:items="[
|
||||
{ label: 'Build' },
|
||||
{ label: 'Design'},
|
||||
{ label: 'Settings'}
|
||||
]"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="flex items-stretch gap-x-2"
|
||||
>
|
||||
<UTooltip
|
||||
text="Help"
|
||||
class="items-center relative"
|
||||
:popper="{ placement: 'left' }"
|
||||
>
|
||||
<a
|
||||
v-track.form_editor_help_button_clicked
|
||||
href="#"
|
||||
class="text-sm p-2 hover:bg-gray-100 cursor-pointer rounded-lg text-gray-500 hover:text-gray-800 cursor-pointer"
|
||||
@click.prevent="crisp.openHelpdesk()"
|
||||
>
|
||||
<Icon
|
||||
name="heroicons:question-mark-circle"
|
||||
class="w-5 h-5"
|
||||
/>
|
||||
</a>
|
||||
</UTooltip>
|
||||
<slot name="before-save" />
|
||||
<UTooltip :popper="{ placement: 'left' }">
|
||||
<template #text>
|
||||
<UKbd
|
||||
:value="metaSymbol"
|
||||
size="xs"
|
||||
/>
|
||||
<UKbd
|
||||
value="s"
|
||||
size="xs"
|
||||
/>
|
||||
</template>
|
||||
<UButton
|
||||
v-track.save_form_click
|
||||
color="primary"
|
||||
class="px-8 md:px-4 py-2"
|
||||
:loading="updateFormLoading"
|
||||
:class="saveButtonClass"
|
||||
@click="emit('save-form')"
|
||||
>
|
||||
<svg
|
||||
class="w-4 h-4 text-white inline mr-1 -mt-1"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M17 21V13H7V21M7 3V8H15M19 21H5C4.46957 21 3.96086 20.7893 3.58579 20.4142C3.21071 20.0391 3 19.5304 3 19V5C3 4.46957 3.21071 3.96086 3.58579 3.58579C3.96086 3.21071 4.46957 3 5 3H16L21 8V19C21 19.5304 20.7893 20.0391 20.4142 20.4142C20.0391 20.7893 19.5304 21 19 21Z"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
<template v-if="form.visibility === 'public'">
|
||||
Publish Form
|
||||
</template>
|
||||
<template v-else>
|
||||
Save Changes
|
||||
</template>
|
||||
</UButton>
|
||||
</UTooltip>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import UndoRedo from '../../editors/UndoRedo.vue'
|
||||
import { useWorkingFormStore } from '~/stores/working_form'
|
||||
import { useCrisp } from '~/composables/useCrisp'
|
||||
|
||||
defineProps({
|
||||
backButton: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
updateFormLoading: {
|
||||
type: Boolean,
|
||||
required: true
|
||||
},
|
||||
saveButtonClass: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['go-back', 'save-form'])
|
||||
|
||||
const { metaSymbol } = useShortcuts()
|
||||
defineShortcuts({
|
||||
meta_s: {
|
||||
handler: () => emit('save-form')
|
||||
}
|
||||
})
|
||||
|
||||
const workingFormStore = useWorkingFormStore()
|
||||
const crisp = useCrisp()
|
||||
|
||||
const form = computed(() => workingFormStore.content)
|
||||
const { activeTab } = storeToRefs(workingFormStore)
|
||||
</script>
|
||||
|
|
@ -1,305 +1,137 @@
|
|||
<template>
|
||||
<div>
|
||||
<v-button
|
||||
v-if="form.properties && form.properties.length > 8"
|
||||
class="w-full mb-3"
|
||||
color="light-gray"
|
||||
@click="openAddFieldSidebar"
|
||||
>
|
||||
<svg
|
||||
class="w-4 h-4 text-nt-blue inline mr-1 -mt-1"
|
||||
viewBox="0 0 14 14"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
<div class="relative">
|
||||
<div class="flex gap-2 sticky top-0 bg-white border-b z-10 p-4">
|
||||
<UButton
|
||||
color="gray"
|
||||
icon="i-heroicons-plus"
|
||||
class="flex-grow justify-center"
|
||||
@click.prevent="openAddFieldSidebar"
|
||||
>
|
||||
<path
|
||||
d="M7.00001 1.1665V12.8332M1.16667 6.99984H12.8333"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.67"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
Add block
|
||||
</v-button>
|
||||
Add Block
|
||||
</UButton>
|
||||
</div>
|
||||
|
||||
<draggable
|
||||
v-model="form.properties"
|
||||
item-key="id"
|
||||
class="bg-white overflow-hidden dark:bg-notion-dark-light rounded-md w-full mx-auto border transition-colors"
|
||||
ghost-class="bg-gray-50"
|
||||
handle=".draggable"
|
||||
:animation="200"
|
||||
>
|
||||
<template #item="{ element, index }">
|
||||
<div
|
||||
class="w-full mx-auto transition-colors"
|
||||
:class="{
|
||||
'bg-gray-100 dark:bg-gray-800': element.hidden,
|
||||
'bg-white dark:bg-notion-dark-light':
|
||||
!element.hidden && !element.type === 'nf-page-break',
|
||||
'border-b': index !== form.properties.length - 1,
|
||||
'bg-blue-50 dark:bg-blue-900':
|
||||
element && element.type === 'nf-page-break',
|
||||
}"
|
||||
>
|
||||
<div class="p-4">
|
||||
<Draggable
|
||||
v-model="form.properties"
|
||||
item-key="id"
|
||||
class="mx-auto w-full overflow-hidden rounded-md border border-gray-300 bg-white transition-colors dark:bg-notion-dark-light"
|
||||
ghost-class="bg-nt-blue-lighter"
|
||||
:animation="200"
|
||||
>
|
||||
<template #item="{ element, index }">
|
||||
<div
|
||||
v-if="element"
|
||||
class="flex items-center space-x-1 group py-2 pr-2 relative"
|
||||
class="mx-auto w-full border-gray-300 transition-colors cursor-grab"
|
||||
:class="{
|
||||
'bg-gray-100 ': element.hidden,
|
||||
'bg-white ': !element.hidden,
|
||||
'border-b': index !== form.properties.length - 1,
|
||||
' !border-blue-400 border-b-2': element.type === 'nf-page-break',
|
||||
}"
|
||||
>
|
||||
<!-- Drag handler -->
|
||||
<div class="cursor-grab draggable p-1 -mr-3">
|
||||
<Icon
|
||||
name="clarity:drag-handle-line"
|
||||
class="size-8 text-gray-400"
|
||||
<div
|
||||
v-if="element"
|
||||
class="group flex items-center gap-x-0.5 py-1.5 pr-1"
|
||||
>
|
||||
<BlockTypeIcon
|
||||
:type="element.type"
|
||||
class="ml-2"
|
||||
/>
|
||||
</div>
|
||||
<!-- Field name and type -->
|
||||
<div class="flex flex-col flex-grow truncate">
|
||||
<editable-div
|
||||
class="max-w-full flex items-center"
|
||||
:model-value="element.name"
|
||||
@update:model-value="onChangeName(element, $event)"
|
||||
>
|
||||
<div class="cursor-pointer max-w-full truncate">
|
||||
{{ element.name }}
|
||||
</div>
|
||||
</editable-div>
|
||||
|
||||
<p class="text-xs text-gray-400 w-full truncate pl-2">
|
||||
<span class="capitalize">{{ formatType(element) }}</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<template v-if="removing == element.id">
|
||||
<div class="flex text-sm items-center">
|
||||
Remove block?
|
||||
<v-button
|
||||
class="inline ml-1"
|
||||
color="red"
|
||||
size="small"
|
||||
@click="removeBlock(index)"
|
||||
<!-- Field name and type -->
|
||||
<div class="flex grow flex-col truncate">
|
||||
<EditableTag
|
||||
class="truncate text-gray-700 min-w-16 min-h-6"
|
||||
:model-value="element.name"
|
||||
@update:model-value="onChangeName(element, $event)"
|
||||
>
|
||||
Yes
|
||||
</v-button>
|
||||
<v-button
|
||||
class="inline ml-1"
|
||||
color="light-gray"
|
||||
size="small"
|
||||
@click="removing = false"
|
||||
>
|
||||
No
|
||||
</v-button>
|
||||
<label class="w-full cursor-pointer truncate">
|
||||
{{ element.name }}
|
||||
</label>
|
||||
</EditableTag>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<button
|
||||
class="hover:bg-red-50 text-gray-500 hover:text-red-600 rounded transition-colors cursor-pointer p-2 hidden md:group-hover:block"
|
||||
@click="removing = element.id"
|
||||
>
|
||||
<svg
|
||||
class="h-4 w-4"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M3 6H5M5 6H21M5 6V20C5 20.5304 5.21071 21.0391 5.58579 21.4142C5.96086 21.7893 6.46957 22 7 22H17C17.5304 22 18.0391 21.7893 18.4142 21.4142C18.7893 21.0391 19 20.5304 19 20V6H5ZM8 6V4C8 3.46957 8.21071 2.96086 8.58579 2.58579C8.96086 2.21071 9.46957 2 10 2H14C14.5304 2 15.0391 2.21071 15.4142 2.58579C15.7893 2.96086 16 3.46957 16 4V6M10 11V17M14 11V17"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
class="hover:bg-nt-blue-lighter rounded transition-colors cursor-pointer p-2 hidden"
|
||||
:class="{
|
||||
'text-blue-500': !element.hidden,
|
||||
'text-gray-500': element.hidden,
|
||||
'group-hover:md:block': !element.hidden,
|
||||
'md:block': element.hidden,
|
||||
}"
|
||||
@click="toggleHidden(element)"
|
||||
>
|
||||
<template v-if="!element.hidden">
|
||||
<svg
|
||||
class="h-4 w-4"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M1 12C1 12 5 4 12 4C19 4 23 12 23 12C23 12 19 20 12 20C5 20 1 12 1 12Z"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M12 15C13.6569 15 15 13.6569 15 12C15 10.3431 13.6569 9 12 9C10.3431 9 9 10.3431 9 12C9 13.6569 10.3431 15 12 15Z"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
||||
<template v-else>
|
||||
<svg
|
||||
class="h-4 w-4"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<g clip-path="url(#clip0_1027_7292)">
|
||||
<path
|
||||
d="M9.9 4.24C10.5883 4.07888 11.2931 3.99834 12 4C19 4 23 12 23 12C22.393 13.1356 21.6691 14.2047 20.84 15.19M14.12 14.12C13.8454 14.4147 13.5141 14.6512 13.1462 14.8151C12.7782 14.9791 12.3809 15.0673 11.9781 15.0744C11.5753 15.0815 11.1752 15.0074 10.8016 14.8565C10.4281 14.7056 10.0887 14.481 9.80385 14.1962C9.51897 13.9113 9.29439 13.5719 9.14351 13.1984C8.99262 12.8248 8.91853 12.4247 8.92563 12.0219C8.93274 11.6191 9.02091 11.2218 9.18488 10.8538C9.34884 10.4859 9.58525 10.1546 9.88 9.88M1 1L23 23M17.94 17.94C16.2306 19.243 14.1491 19.9649 12 20C5 20 1 12 1 12C2.24389 9.6819 3.96914 7.65661 6.06 6.06L17.94 17.94Z"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_1027_7292">
|
||||
<rect
|
||||
width="24"
|
||||
height="24"
|
||||
fill="white"
|
||||
/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
</template>
|
||||
</button>
|
||||
<button
|
||||
v-if="!element.type.startsWith('nf-')"
|
||||
class="hover:bg-nt-blue-lighter rounded transition-colors cursor-pointer p-2 hidden"
|
||||
:class="{
|
||||
'group-hover:md:block': !element.required,
|
||||
'md:block': element.required,
|
||||
}"
|
||||
@click="toggleRequired(element)"
|
||||
>
|
||||
<div
|
||||
class="w-4 h-4 text-center font-bold text-3xl"
|
||||
|
||||
<UTooltip :text="element.hidden ? 'Show Block' : 'Hide Block'">
|
||||
<button
|
||||
class="hidden cursor-pointer rounded p-1 transition-colors hover:bg-nt-blue-lighter items-center justify-center"
|
||||
:class="{
|
||||
'text-red-500': element.required,
|
||||
'text-gray-500': !element.required,
|
||||
'text-gray-300 hover:text-blue-500 md:group-hover:flex': !element.hidden,
|
||||
'text-gray-300 hover:text-gray-500 md:flex': element.hidden,
|
||||
}"
|
||||
@click="toggleHidden(element)"
|
||||
>
|
||||
<svg
|
||||
class="w-4 h-4"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M12 2V12M12 12V22M12 12L4.93 4.93M12 12L19.07 19.07M12 12H2M12 12H22M12 12L4.93 19.07M12 12L19.07 4.93"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
<template v-if="!element.hidden">
|
||||
<Icon
|
||||
name="heroicons:eye-solid"
|
||||
class="h-5 w-5"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</button>
|
||||
</template>
|
||||
<template v-else>
|
||||
<Icon
|
||||
name="heroicons:eye-slash-solid"
|
||||
class="h-5 w-5"
|
||||
/>
|
||||
</template>
|
||||
</button>
|
||||
</UTooltip>
|
||||
<UTooltip
|
||||
v-if="!element.type.startsWith('nf-')"
|
||||
:text="element.required ? 'Make it optional' : 'Make it required'"
|
||||
>
|
||||
<button
|
||||
class="hidden cursor-pointer rounded p-0.5 transition-colors hover:bg-nt-blue-lighter items-center px-1 justify-center"
|
||||
:class="{
|
||||
'md:group-hover:flex text-gray-300 hover:text-red-500': !element.required,
|
||||
'md:flex text-red-500': element.required,
|
||||
}"
|
||||
@click="toggleRequired(element)"
|
||||
>
|
||||
<div
|
||||
class="h-6 text-center text-3xl font-bold text-inherit -mt-0.5"
|
||||
>
|
||||
*
|
||||
</div>
|
||||
</button>
|
||||
</UTooltip>
|
||||
<button
|
||||
class="hover:bg-nt-blue-lighter rounded transition-colors cursor-pointer p-2"
|
||||
class="cursor-pointer rounded p-1 transition-colors hover:bg-nt-blue-lighter text-gray-300 hover:text-blue-500 flex items-center justify-center"
|
||||
@click="editOptions(index)"
|
||||
>
|
||||
<svg
|
||||
class="h-4 w-4 text-blue-600"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<g clip-path="url(#clip0_1027_7210)">
|
||||
<path
|
||||
d="M12 15C13.6569 15 15 13.6569 15 12C15 10.3431 13.6569 9 12 9C10.3431 9 9 10.3431 9 12C9 13.6569 10.3431 15 12 15Z"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M19.4 15C19.2669 15.3016 19.2272 15.6362 19.286 15.9606C19.3448 16.285 19.4995 16.5843 19.73 16.82L19.79 16.88C19.976 17.0657 20.1235 17.2863 20.2241 17.5291C20.3248 17.7719 20.3766 18.0322 20.3766 18.295C20.3766 18.5578 20.3248 18.8181 20.2241 19.0609C20.1235 19.3037 19.976 19.5243 19.79 19.71C19.6043 19.896 19.3837 20.0435 19.1409 20.1441C18.8981 20.2448 18.6378 20.2966 18.375 20.2966C18.1122 20.2966 17.8519 20.2448 17.6091 20.1441C17.3663 20.0435 17.1457 19.896 16.96 19.71L16.9 19.65C16.6643 19.4195 16.365 19.2648 16.0406 19.206C15.7162 19.1472 15.3816 19.1869 15.08 19.32C14.7842 19.4468 14.532 19.6572 14.3543 19.9255C14.1766 20.1938 14.0813 20.5082 14.08 20.83V21C14.08 21.5304 13.8693 22.0391 13.4942 22.4142C13.1191 22.7893 12.6104 23 12.08 23C11.5496 23 11.0409 22.7893 10.6658 22.4142C10.2907 22.0391 10.08 21.5304 10.08 21V20.91C10.0723 20.579 9.96512 20.258 9.77251 19.9887C9.5799 19.7194 9.31074 19.5143 9 19.4C8.69838 19.2669 8.36381 19.2272 8.03941 19.286C7.71502 19.3448 7.41568 19.4995 7.18 19.73L7.12 19.79C6.93425 19.976 6.71368 20.1235 6.47088 20.2241C6.22808 20.3248 5.96783 20.3766 5.705 20.3766C5.44217 20.3766 5.18192 20.3248 4.93912 20.2241C4.69632 20.1235 4.47575 19.976 4.29 19.79C4.10405 19.6043 3.95653 19.3837 3.85588 19.1409C3.75523 18.8981 3.70343 18.6378 3.70343 18.375C3.70343 18.1122 3.75523 17.8519 3.85588 17.6091C3.95653 17.3663 4.10405 17.1457 4.29 16.96L4.35 16.9C4.58054 16.6643 4.73519 16.365 4.794 16.0406C4.85282 15.7162 4.81312 15.3816 4.68 15.08C4.55324 14.7842 4.34276 14.532 4.07447 14.3543C3.80618 14.1766 3.49179 14.0813 3.17 14.08H3C2.46957 14.08 1.96086 13.8693 1.58579 13.4942C1.21071 13.1191 1 12.6104 1 12.08C1 11.5496 1.21071 11.0409 1.58579 10.6658C1.96086 10.2907 2.46957 10.08 3 10.08H3.09C3.42099 10.0723 3.742 9.96512 4.0113 9.77251C4.28059 9.5799 4.48572 9.31074 4.6 9C4.73312 8.69838 4.77282 8.36381 4.714 8.03941C4.65519 7.71502 4.50054 7.41568 4.27 7.18L4.21 7.12C4.02405 6.93425 3.87653 6.71368 3.77588 6.47088C3.67523 6.22808 3.62343 5.96783 3.62343 5.705C3.62343 5.44217 3.67523 5.18192 3.77588 4.93912C3.87653 4.69632 4.02405 4.47575 4.21 4.29C4.39575 4.10405 4.61632 3.95653 4.85912 3.85588C5.10192 3.75523 5.36217 3.70343 5.625 3.70343C5.88783 3.70343 6.14808 3.75523 6.39088 3.85588C6.63368 3.95653 6.85425 4.10405 7.04 4.29L7.1 4.35C7.33568 4.58054 7.63502 4.73519 7.95941 4.794C8.28381 4.85282 8.61838 4.81312 8.92 4.68H9C9.29577 4.55324 9.54802 4.34276 9.72569 4.07447C9.90337 3.80618 9.99872 3.49179 10 3.17V3C10 2.46957 10.2107 1.96086 10.5858 1.58579C10.9609 1.21071 11.4696 1 12 1C12.5304 1 13.0391 1.21071 13.4142 1.58579C13.7893 1.96086 14 2.46957 14 3V3.09C14.0013 3.41179 14.0966 3.72618 14.2743 3.99447C14.452 4.26276 14.7042 4.47324 15 4.6C15.3016 4.73312 15.6362 4.77282 15.9606 4.714C16.285 4.65519 16.5843 4.50054 16.82 4.27L16.88 4.21C17.0657 4.02405 17.2863 3.87653 17.5291 3.77588C17.7719 3.67523 18.0322 3.62343 18.295 3.62343C18.5578 3.62343 18.8181 3.67523 19.0609 3.77588C19.3037 3.87653 19.5243 4.02405 19.71 4.21C19.896 4.39575 20.0435 4.61632 20.1441 4.85912C20.2448 5.10192 20.2966 5.36217 20.2966 5.625C20.2966 5.88783 20.2448 6.14808 20.1441 6.39088C20.0435 6.63368 19.896 6.85425 19.71 7.04L19.65 7.1C19.4195 7.33568 19.2648 7.63502 19.206 7.95941C19.1472 8.28381 19.1869 8.61838 19.32 8.92V9C19.4468 9.29577 19.6572 9.54802 19.9255 9.72569C20.1938 9.90337 20.5082 9.99872 20.83 10H21C21.5304 10 22.0391 10.2107 22.4142 10.5858C22.7893 10.9609 23 11.4696 23 12C23 12.5304 22.7893 13.0391 22.4142 13.4142C22.0391 13.7893 21.5304 14 21 14H20.91C20.5882 14.0013 20.2738 14.0966 20.0055 14.2743C19.7372 14.452 19.5268 14.7042 19.4 15Z"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_1027_7210">
|
||||
<rect
|
||||
width="24"
|
||||
height="24"
|
||||
fill="white"
|
||||
/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
<Icon
|
||||
name="heroicons:cog-8-tooth-solid"
|
||||
class="h-5 w-5"
|
||||
/>
|
||||
</button>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</draggable>
|
||||
|
||||
<v-button
|
||||
class="w-full mt-3"
|
||||
color="light-gray"
|
||||
@click="openAddFieldSidebar"
|
||||
>
|
||||
<svg
|
||||
class="w-4 h-4 text-nt-blue inline mr-1 -mt-1"
|
||||
viewBox="0 0 14 14"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M7.00001 1.1665V12.8332M1.16667 6.99984H12.8333"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.67"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
Add block
|
||||
</v-button>
|
||||
</template>
|
||||
</Draggable>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import draggable from "vuedraggable"
|
||||
import EditableDiv from "~/components/global/EditableDiv.vue"
|
||||
import VButton from "~/components/global/VButton.vue"
|
||||
import draggable from 'vuedraggable'
|
||||
import EditableTag from '~/components/global/EditableTag.vue'
|
||||
import BlockTypeIcon from './BlockTypeIcon.vue'
|
||||
|
||||
draggable.compatConfig = { MODE: 3 }
|
||||
export default {
|
||||
name: "FormFieldsEditor",
|
||||
name: 'FormFieldsEditor',
|
||||
components: {
|
||||
VButton,
|
||||
draggable,
|
||||
EditableDiv,
|
||||
Draggable: draggable,
|
||||
EditableTag,
|
||||
BlockTypeIcon
|
||||
},
|
||||
|
||||
setup() {
|
||||
setup () {
|
||||
const workingFormStore = useWorkingFormStore()
|
||||
return {
|
||||
route: useRoute(),
|
||||
workingFormStore,
|
||||
form: storeToRefs(workingFormStore).content,
|
||||
form: storeToRefs(workingFormStore).content
|
||||
}
|
||||
},
|
||||
|
||||
data() {
|
||||
data () {
|
||||
return {
|
||||
removing: null,
|
||||
|
||||
}
|
||||
},
|
||||
|
||||
|
|
@ -308,24 +140,6 @@ export default {
|
|||
},
|
||||
|
||||
methods: {
|
||||
onChangeName(field, newName) {
|
||||
field.name = newName
|
||||
},
|
||||
toggleHidden(field) {
|
||||
field.hidden = !field.hidden
|
||||
if (field.hidden) {
|
||||
field.required = false
|
||||
} else {
|
||||
field.generates_uuid = false
|
||||
field.generates_auto_increment_id = false
|
||||
}
|
||||
},
|
||||
toggleRequired(field) {
|
||||
field.required = !field.required
|
||||
if (field.required) {
|
||||
field.hidden = false
|
||||
}
|
||||
},
|
||||
init() {
|
||||
if (!this.form.properties) {
|
||||
return
|
||||
|
|
@ -340,31 +154,37 @@ export default {
|
|||
return field
|
||||
})
|
||||
},
|
||||
formatType(field) {
|
||||
let type = field.type.replace("_", " ")
|
||||
if (!type.startsWith("nf")) {
|
||||
type = type + " Input"
|
||||
} else {
|
||||
type = type.replace("nf-", "")
|
||||
}
|
||||
if (field.generates_uuid || field.generates_auto_increment_id) {
|
||||
type = type + " - Auto ID"
|
||||
}
|
||||
return type
|
||||
},
|
||||
editOptions(index) {
|
||||
this.workingFormStore.openSettingsForField(index)
|
||||
},
|
||||
removeBlock(blockIndex) {
|
||||
this.form.properties.splice(blockIndex, 1)
|
||||
this.closeSidebar()
|
||||
},
|
||||
closeSidebar() {
|
||||
this.workingFormStore.closeEditFieldSidebar()
|
||||
},
|
||||
openAddFieldSidebar() {
|
||||
openAddFieldSidebar () {
|
||||
this.workingFormStore.openAddFieldSidebar(null)
|
||||
},
|
||||
},
|
||||
editOptions (index) {
|
||||
this.workingFormStore.openSettingsForField(index)
|
||||
},
|
||||
onChangeName (field, newName) {
|
||||
field.name = newName
|
||||
},
|
||||
toggleHidden (field) {
|
||||
field.hidden = !field.hidden
|
||||
if (field.hidden) {
|
||||
field.required = false
|
||||
} else {
|
||||
field.generates_uuid = false
|
||||
field.generates_auto_increment_id = false
|
||||
}
|
||||
},
|
||||
toggleRequired (field) {
|
||||
field.required = !field.required
|
||||
if (field.required)
|
||||
field.hidden = false
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang='scss'>
|
||||
.v-popover {
|
||||
.trigger {
|
||||
@apply truncate w-full;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -1,269 +1,113 @@
|
|||
<template>
|
||||
<div>
|
||||
<div class="p-4 border-b sticky top-0 z-10 bg-white">
|
||||
<div class="p-4 border-b border-gray-300 sticky top-0 z-10 bg-white">
|
||||
<div class="flex">
|
||||
<button
|
||||
class="text-gray-500 hover:text-gray-900 cursor-pointer"
|
||||
@click.prevent="closeSidebar"
|
||||
@click="closeSidebar"
|
||||
>
|
||||
<svg
|
||||
class="h-6 w-6"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M18 6L6 18M6 6L18 18"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
<Icon
|
||||
name="heroicons:x-mark-20-solid"
|
||||
class="inline w-6 h-6"
|
||||
/>
|
||||
</button>
|
||||
<div class="font-semibold inline ml-2 flex-grow truncate">
|
||||
<div class="font-medium inline ml-2 flex-grow truncate">
|
||||
Add Block
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="py-2 px-4">
|
||||
<div>
|
||||
<p class="text-gray-500 uppercase text-xs font-semibold mb-2">
|
||||
Input Blocks
|
||||
</p>
|
||||
<draggable
|
||||
:list="inputBlocks"
|
||||
:group="{ name: 'form-elements', pull: 'clone', put: false }"
|
||||
class="grid grid-cols-2 gap-2"
|
||||
:sort="false"
|
||||
:clone="handleInputClone"
|
||||
ghost-class="ghost-item"
|
||||
item-key="id"
|
||||
@start="workingFormStore.draggingNewBlock=true"
|
||||
@end="workingFormStore.draggingNewBlock=false"
|
||||
>
|
||||
<template #item="{element}">
|
||||
<div
|
||||
class="bg-gray-50 border cursor-grab hover:bg-gray-100 dark:bg-gray-900 rounded-md dark:hover:bg-gray-800 py-2 flex flex-col"
|
||||
role="button"
|
||||
@click.prevent="addBlock(element.name)"
|
||||
<p class="text-sm font-medium my-2">
|
||||
Input Blocks
|
||||
</p>
|
||||
<draggable
|
||||
:list="inputBlocks"
|
||||
:group="{ name: 'form-elements', pull: 'clone', put: false }"
|
||||
class="flex flex-col -mx-2"
|
||||
:sort="false"
|
||||
:clone="handleInputClone"
|
||||
ghost-class="ghost-item"
|
||||
item-key="id"
|
||||
@start="workingFormStore.draggingNewBlock=true"
|
||||
@end="workingFormStore.draggingNewBlock=false"
|
||||
>
|
||||
<template #item="{element}">
|
||||
<div
|
||||
class="flex hover:bg-gray-50 rounded-md items-center gap-2 p-2"
|
||||
role="button"
|
||||
@click.prevent="addBlock(element.name)"
|
||||
>
|
||||
<BlockTypeIcon :type="element.name" />
|
||||
<p
|
||||
class="w-full text-sm text-gray-500"
|
||||
>
|
||||
<div class="mx-auto">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-6 w-6 text-gray-500"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
v-html="element.icon"
|
||||
/>
|
||||
</div>
|
||||
<p
|
||||
class="w-full text-xs text-gray-500 uppercase text-center font-semibold mt-1"
|
||||
>
|
||||
{{ element.title }}
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
</draggable>
|
||||
</div>
|
||||
<div class="border-t mt-6">
|
||||
<p class="text-gray-500 uppercase text-xs font-semibold mb-2 mt-6">
|
||||
Layout Blocks
|
||||
</p>
|
||||
<draggable
|
||||
:list="layoutBlocks"
|
||||
:group="{ name: 'form-elements', pull: 'clone', put: false }"
|
||||
class="grid grid-cols-2 gap-2"
|
||||
:sort="false"
|
||||
:clone="handleInputClone"
|
||||
ghost-class="ghost-item"
|
||||
item-key="id"
|
||||
>
|
||||
<template #item="{element}">
|
||||
<div
|
||||
class="bg-gray-50 border hover:bg-gray-100 dark:bg-gray-900 rounded-md dark:hover:bg-gray-800 py-2 flex flex-col"
|
||||
role="button"
|
||||
@click.prevent="addBlock(element.name)"
|
||||
{{ element.title }}
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
</draggable>
|
||||
</div>
|
||||
<div class="px-4 border-t mb-4">
|
||||
<p class="text-sm font-medium my-2">
|
||||
Layout Blocks
|
||||
</p>
|
||||
<draggable
|
||||
:list="layoutBlocks"
|
||||
:group="{ name: 'form-elements', pull: 'clone', put: false }"
|
||||
class="flex flex-col -mx-2"
|
||||
:sort="false"
|
||||
:clone="handleInputClone"
|
||||
ghost-class="ghost-item"
|
||||
item-key="id"
|
||||
@start="workingFormStore.draggingNewBlock=true"
|
||||
@end="workingFormStore.draggingNewBlock=false"
|
||||
>
|
||||
<template #item="{element}">
|
||||
<div
|
||||
class="flex hover:bg-gray-50 rounded-md items-center gap-2 p-2"
|
||||
role="button"
|
||||
@click.prevent="addBlock(element.name)"
|
||||
>
|
||||
<BlockTypeIcon :type="element.name" />
|
||||
<p
|
||||
class="w-full text-sm text-gray-500"
|
||||
>
|
||||
<div class="mx-auto">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-6 w-6 text-gray-500"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
v-html="element.icon"
|
||||
/>
|
||||
</div>
|
||||
<p
|
||||
class="w-full text-xs text-gray-500 uppercase text-center font-semibold mt-1"
|
||||
>
|
||||
{{ element.title }}
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
</draggable>
|
||||
</div>
|
||||
{{ element.title }}
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
</draggable>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
<script setup>
|
||||
import draggable from 'vuedraggable'
|
||||
import { computed } from "vue"
|
||||
import blocksTypes from '~/data/blocks_types.json'
|
||||
import BlockTypeIcon from '../BlockTypeIcon.vue'
|
||||
|
||||
export default {
|
||||
name: "AddFormBlock",
|
||||
components: {draggable},
|
||||
props: {},
|
||||
const workingFormStore = useWorkingFormStore()
|
||||
|
||||
setup() {
|
||||
const workingFormStore = useWorkingFormStore()
|
||||
const { content: form } = storeToRefs(workingFormStore)
|
||||
return {
|
||||
form,
|
||||
workingFormStore,
|
||||
selectedFieldIndex: computed(() => workingFormStore.selectedFieldIndex),
|
||||
}
|
||||
},
|
||||
const inputBlocks = computed(() => Object.values(blocksTypes).filter(block => !block.name.startsWith('nf-')))
|
||||
const layoutBlocks = computed(() => Object.values(blocksTypes).filter(block => block.name.startsWith('nf-')))
|
||||
|
||||
data() {
|
||||
return {
|
||||
inputBlocks: [
|
||||
{
|
||||
name: "text",
|
||||
title: "Text Input",
|
||||
icon: '<path stroke-linecap="round" stroke-linejoin="round" d="M4 6h16M4 12h16M4 18h7"/>',
|
||||
},
|
||||
{
|
||||
name: "date",
|
||||
title: "Date Input",
|
||||
icon: '<path stroke-linecap="round" stroke-linejoin="round" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"/>',
|
||||
},
|
||||
{
|
||||
name: "url",
|
||||
title: "URL Input",
|
||||
icon: '<path stroke-linecap="round" stroke-linejoin="round" d="M13.828 10.172a4 4 0 00-5.656 0l-4 4a4 4 0 105.656 5.656l1.102-1.101m-.758-4.899a4 4 0 005.656 0l4-4a4 4 0 00-5.656-5.656l-1.1 1.1"/>',
|
||||
},
|
||||
{
|
||||
name: "phone_number",
|
||||
title: "Phone Input",
|
||||
icon: '<path stroke-linecap="round" stroke-linejoin="round" d="M3 5a2 2 0 012-2h3.28a1 1 0 01.948.684l1.498 4.493a1 1 0 01-.502 1.21l-2.257 1.13a11.042 11.042 0 005.516 5.516l1.13-2.257a1 1 0 011.21-.502l4.493 1.498a1 1 0 01.684.949V19a2 2 0 01-2 2h-1C9.716 21 3 14.284 3 6V5z"/>',
|
||||
},
|
||||
{
|
||||
name: "email",
|
||||
title: "Email Input",
|
||||
icon: '<path stroke-linecap="round" stroke-linejoin="round" d="M16 12a4 4 0 10-8 0 4 4 0 008 0zm0 0v1.5a2.5 2.5 0 005 0V12a9 9 0 10-9 9m4.5-1.206a8.959 8.959 0 01-4.5 1.207"/>',
|
||||
},
|
||||
{
|
||||
name: "checkbox",
|
||||
title: "Checkbox Input",
|
||||
icon: '<path stroke-linecap="round" stroke-linejoin="round" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/>',
|
||||
},
|
||||
{
|
||||
name: "select",
|
||||
title: "Select Input",
|
||||
icon: '<path stroke-linecap="round" stroke-linejoin="round" d="M8 9l4-4 4 4m0 6l-4 4-4-4"/>',
|
||||
},
|
||||
{
|
||||
name: "multi_select",
|
||||
title: "Multi-select Input",
|
||||
icon: '<path stroke-linecap="round" stroke-linejoin="round" d="M8 9l4-4 4 4m0 6l-4 4-4-4"/>',
|
||||
},
|
||||
{
|
||||
name: "number",
|
||||
title: "Number Input",
|
||||
icon: '<path stroke-linecap="round" stroke-linejoin="round" d="M7 20l4-16m2 16l4-16M6 9h14M4 15h14"/>',
|
||||
},
|
||||
{
|
||||
name: "rating",
|
||||
title: "Rating Input",
|
||||
icon: '<path stroke-linecap="round" stroke-linejoin="round" d="M11.48 3.499a.562.562 0 0 1 1.04 0l2.125 5.111a.563.563 0 0 0 .475.345l5.518.442c.499.04.701.663.321.988l-4.204 3.602a.563.563 0 0 0-.182.557l1.285 5.385a.562.562 0 0 1-.84.61l-4.725-2.885a.562.562 0 0 0-.586 0L6.982 20.54a.562.562 0 0 1-.84-.61l1.285-5.386a.562.562 0 0 0-.182-.557l-4.204-3.602a.562.562 0 0 1 .321-.988l5.518-.442a.563.563 0 0 0 .475-.345L11.48 3.5Z" />',
|
||||
},
|
||||
{
|
||||
name: "scale",
|
||||
title: "Scale Input",
|
||||
icon: '<path stroke-linecap="round" stroke-linejoin="round" d="M7.5 14.25v2.25m3-4.5v4.5m3-6.75v6.75m3-9v9M6 20.25h12A2.25 2.25 0 0 0 20.25 18V6A2.25 2.25 0 0 0 18 3.75H6A2.25 2.25 0 0 0 3.75 6v12A2.25 2.25 0 0 0 6 20.25Z" />',
|
||||
},
|
||||
{
|
||||
name: "slider",
|
||||
title: "Slider Input",
|
||||
icon: '<path stroke-linecap="round" stroke-linejoin="round" d="M10.5 6h9.75M10.5 6a1.5 1.5 0 1 1-3 0m3 0a1.5 1.5 0 1 0-3 0M3.75 6H7.5m3 12h9.75m-9.75 0a1.5 1.5 0 0 1-3 0m3 0a1.5 1.5 0 0 0-3 0m-3.75 0H7.5m9-6h3.75m-3.75 0a1.5 1.5 0 0 1-3 0m3 0a1.5 1.5 0 0 0-3 0m-9.75 0h9.75" />',
|
||||
},
|
||||
{
|
||||
name: "files",
|
||||
title: "File Input",
|
||||
icon: '<path stroke-linecap="round" stroke-linejoin="round" d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />',
|
||||
},
|
||||
{
|
||||
name: "signature",
|
||||
title: "Signature Input",
|
||||
icon: '<path stroke-linecap="round" stroke-linejoin="round" d="M16.862 4.487l1.687-1.688a1.875 1.875 0 112.652 2.652L6.832 19.82a4.5 4.5 0 01-1.897 1.13l-2.685.8.8-2.685a4.5 4.5 0 011.13-1.897L16.863 4.487zm0 0L19.5 7.125" />',
|
||||
},
|
||||
{
|
||||
name: "matrix",
|
||||
title: "Matrix Input",
|
||||
icon: '<path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M3.375 19.5h17.25m-17.25 0a1.125 1.125 0 0 1-1.125-1.125M3.375 19.5h7.5c.621 0 1.125-.504 1.125-1.125m-9.75 0V5.625m0 12.75v-1.5c0-.621.504-1.125 1.125-1.125m18.375 2.625V5.625m0 12.75c0 .621-.504 1.125-1.125 1.125m1.125-1.125v-1.5c0-.621-.504-1.125-1.125-1.125m0 3.75h-7.5A1.125 1.125 0 0 1 12 18.375m9.75-12.75c0-.621-.504-1.125-1.125-1.125H3.375c-.621 0-1.125.504-1.125 1.125m19.5 0v1.5c0 .621-.504 1.125-1.125 1.125M2.25 5.625v1.5c0 .621.504 1.125 1.125 1.125m0 0h17.25m-17.25 0h7.5c.621 0 1.125.504 1.125 1.125M3.375 8.25c-.621 0-1.125.504-1.125 1.125v1.5c0 .621.504 1.125 1.125 1.125m17.25-3.75h-7.5c-.621 0-1.125.504-1.125 1.125m8.625-1.125c.621 0 1.125.504 1.125 1.125v1.5c0 .621-.504 1.125-1.125 1.125m-17.25 0h7.5m-7.5 0c-.621 0-1.125.504-1.125 1.125v1.5c0 .621.504 1.125 1.125 1.125M12 10.875v-1.5m0 1.5c0 .621-.504 1.125-1.125 1.125M12 10.875c0 .621.504 1.125 1.125 1.125m-2.25 0c.621 0 1.125.504 1.125 1.125M13.125 12h7.5m-7.5 0c-.621 0-1.125.504-1.125 1.125M20.625 12c.621 0 1.125.504 1.125 1.125v1.5c0 .621-.504 1.125-1.125 1.125m-17.25 0h7.5M12 14.625v-1.5m0 1.5c0 .621-.504 1.125-1.125 1.125M12 14.625c0 .621.504 1.125 1.125 1.125m-2.25 0c.621 0 1.125.504 1.125 1.125m0 1.5v-1.5m0 0c0-.621.504-1.125 1.125-1.125m0 0h7.5"></path>',
|
||||
},
|
||||
],
|
||||
layoutBlocks: [
|
||||
{
|
||||
name: "nf-text",
|
||||
title: "Text Block",
|
||||
icon: '<path stroke-linecap="round" stroke-linejoin="round" d="M4 6h16M4 12h8m-8 6h16" />',
|
||||
},
|
||||
{
|
||||
name: "nf-page-break",
|
||||
title: "Page-break Block",
|
||||
icon: '<path stroke-linecap="round" stroke-linejoin="round" d="M9 13h6m-3-3v6m5 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />',
|
||||
},
|
||||
{
|
||||
name: "nf-divider",
|
||||
title: "Divider Block",
|
||||
icon: '<path stroke-linecap="round" stroke-linejoin="round" d="M20 12H4" />',
|
||||
},
|
||||
{
|
||||
name: "nf-image",
|
||||
title: "Image Block",
|
||||
icon: '<path stroke-linecap="round" stroke-linejoin="round" d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" />',
|
||||
},
|
||||
{
|
||||
name: "nf-code",
|
||||
title: "Code Block",
|
||||
icon: '<path stroke-linecap="round" stroke-linejoin="round" d="M17.25 6.75L22.5 12l-5.25 5.25m-10.5 0L1.5 12l5.25-5.25m7.5-3l-4.5 16.5" />',
|
||||
},
|
||||
],
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
},
|
||||
|
||||
watch: {},
|
||||
|
||||
mounted() {
|
||||
this.workingFormStore.resetBlockForm()
|
||||
},
|
||||
|
||||
methods: {
|
||||
closeSidebar() {
|
||||
this.workingFormStore.closeAddFieldSidebar()
|
||||
},
|
||||
addBlock(type) {
|
||||
this.workingFormStore.addBlock(type)
|
||||
},
|
||||
handleInputClone(item) {
|
||||
return item.name
|
||||
}
|
||||
},
|
||||
const closeSidebar = () => {
|
||||
workingFormStore.closeAddFieldSidebar()
|
||||
}
|
||||
|
||||
const addBlock = (type) => {
|
||||
workingFormStore.addBlock(type)
|
||||
}
|
||||
|
||||
const handleInputClone = (item) => {
|
||||
return item.name
|
||||
}
|
||||
|
||||
workingFormStore.resetBlockForm()
|
||||
</script>
|
||||
|
||||
<style lang='scss' scoped>
|
||||
<style lang="scss" scoped>
|
||||
.ghost-item {
|
||||
@apply bg-blue-100 dark:bg-blue-900 rounded-md w-full col-span-full;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,39 @@
|
|||
<template>
|
||||
<div class="relative">
|
||||
<hr
|
||||
v-if="showLine"
|
||||
class="border-t border-gray-200 mb-6 mt-8 -mx-4"
|
||||
>
|
||||
<div
|
||||
:class="[
|
||||
'flex gap-x-2 items-center',
|
||||
showLine ? 'mb-2 px-2 -mx-2 absolute -top-[10px] bg-white' : 'my-4'
|
||||
]"
|
||||
>
|
||||
<Icon
|
||||
:name="icon"
|
||||
class="w-4 h-4 text-gray-400"
|
||||
/>
|
||||
<h3 class="text-sm font-semibold text-gray-500">
|
||||
{{ title }}
|
||||
</h3>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
defineProps({
|
||||
icon: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
showLine: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
|
@ -1,284 +0,0 @@
|
|||
<template>
|
||||
<editor-options-panel
|
||||
name="About Submissions"
|
||||
:already-opened="true"
|
||||
>
|
||||
<template #icon>
|
||||
<svg
|
||||
class="h-5 w-5"
|
||||
viewBox="0 0 18 18"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M4.83333 6.08333H9M4.83333 9H11.5M4.83333 14V15.9463C4.83333 16.3903 4.83333 16.6123 4.92436 16.7263C5.00352 16.8255 5.12356 16.8832 5.25045 16.8831C5.39636 16.8829 5.56973 16.7442 5.91646 16.4668L7.90434 14.8765C8.31043 14.5517 8.51347 14.3892 8.73957 14.2737C8.94017 14.1712 9.15369 14.0963 9.37435 14.051C9.62306 14 9.88308 14 10.4031 14H12.5C13.9001 14 14.6002 14 15.135 13.7275C15.6054 13.4878 15.9878 13.1054 16.2275 12.635C16.5 12.1002 16.5 11.4001 16.5 10V5.5C16.5 4.09987 16.5 3.3998 16.2275 2.86502C15.9878 2.39462 15.6054 2.01217 15.135 1.77248C14.6002 1.5 13.9001 1.5 12.5 1.5H5.5C4.09987 1.5 3.3998 1.5 2.86502 1.77248C2.39462 2.01217 2.01217 2.39462 1.77248 2.86502C1.5 3.3998 1.5 4.09987 1.5 5.5V10.6667C1.5 11.4416 1.5 11.8291 1.58519 12.147C1.81635 13.0098 2.49022 13.6836 3.35295 13.9148C3.67087 14 4.05836 14 4.83333 14Z"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.67"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
<text-input
|
||||
name="submit_button_text"
|
||||
class="mt-4"
|
||||
:form="form"
|
||||
label="Text of Submit Button"
|
||||
:required="true"
|
||||
/>
|
||||
|
||||
<toggle-switch-input
|
||||
name="editable_submissions"
|
||||
:form="form"
|
||||
class="mt-4"
|
||||
help="Gives user a unique url to update their submission"
|
||||
>
|
||||
<template #label>
|
||||
<span class="text-sm">
|
||||
Editable submissions
|
||||
</span>
|
||||
<pro-tag class="-mt-1 ml-1" />
|
||||
</template>
|
||||
</toggle-switch-input>
|
||||
<text-input
|
||||
v-if="form.editable_submissions"
|
||||
name="editable_submissions_button_text"
|
||||
:form="form"
|
||||
label="Text of editable submissions button"
|
||||
:required="true"
|
||||
/>
|
||||
|
||||
<flat-select-input
|
||||
:form="submissionOptions"
|
||||
name="databaseAction"
|
||||
label="Database Submission Action"
|
||||
:options="[
|
||||
{ name: 'Create new record (default)', value: 'create' },
|
||||
{ name: 'Update Record (or create if no match)', value: 'update' },
|
||||
]"
|
||||
:required="true"
|
||||
help="Create a new record or update an existing one"
|
||||
>
|
||||
<template #selected="{ option, optionName }">
|
||||
<div class="flex items-center truncate mr-6">
|
||||
{{ optionName }}
|
||||
<pro-tag
|
||||
v-if="option === 'update'"
|
||||
class="ml-2"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<template #option="{ option, selected }">
|
||||
<span class="flex hover:text-white">
|
||||
<p class="flex-grow hover:text-white">
|
||||
{{ option.name }}
|
||||
<template v-if="option.value === 'update'"><pro-tag /></template>
|
||||
</p>
|
||||
<span
|
||||
v-if="selected"
|
||||
class="absolute inset-y-0 right-0 flex items-center pr-4 dark:text-white"
|
||||
>
|
||||
<svg
|
||||
class="h-5 w-5"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</span>
|
||||
</template>
|
||||
</flat-select-input>
|
||||
|
||||
<v-transition>
|
||||
<div
|
||||
v-if="
|
||||
submissionOptions.databaseAction == 'update' &&
|
||||
filterableFields.length
|
||||
"
|
||||
>
|
||||
<select-input
|
||||
v-if="filterableFields.length"
|
||||
:form="form"
|
||||
name="database_fields_update"
|
||||
label="Properties to check on update"
|
||||
:options="filterableFields"
|
||||
:required="true"
|
||||
:multiple="true"
|
||||
/>
|
||||
<div class="-mt-3 mb-3 text-gray-400 dark:text-gray-500">
|
||||
<small>If the submission has the same value(s) as a previous one for the
|
||||
selected column(s), we will update it, instead of creating a new
|
||||
one.
|
||||
<a
|
||||
href="#"
|
||||
@click.prevent="
|
||||
crisp.openHelpdeskArticle(
|
||||
'how-to-update-a-page-on-form-submission-1t1jwmn',
|
||||
)
|
||||
"
|
||||
>More info here.</a>
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
</v-transition>
|
||||
|
||||
<select-input
|
||||
:form="submissionOptions"
|
||||
name="submissionMode"
|
||||
label="Post Submission Action"
|
||||
:options="[
|
||||
{ name: 'Show Success page', value: 'default' },
|
||||
{ name: 'Redirect', value: 'redirect' },
|
||||
]"
|
||||
:required="true"
|
||||
help="Show a message, or redirect to a URL"
|
||||
>
|
||||
<template #selected="{ option, optionName }">
|
||||
<div class="flex items-center text-sm truncate mr-6">
|
||||
{{ optionName }}
|
||||
<pro-tag
|
||||
v-if="option === 'redirect'"
|
||||
class="ml-2"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<template #option="{ option, selected }">
|
||||
<span class="flex">
|
||||
<p class="flex-grow">
|
||||
{{ option.name }}
|
||||
<template v-if="option.value === 'redirect'"><pro-tag /></template>
|
||||
</p>
|
||||
<span
|
||||
v-if="selected"
|
||||
class="absolute inset-y-0 right-0 flex items-center pr-4"
|
||||
>
|
||||
<svg
|
||||
class="h-5 w-5"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</span>
|
||||
</template>
|
||||
</select-input>
|
||||
<template v-if="submissionOptions.submissionMode === 'redirect'">
|
||||
<text-input
|
||||
name="redirect_url"
|
||||
:form="form"
|
||||
label="Redirect URL"
|
||||
:required="true"
|
||||
help="On submit, redirects to that URL"
|
||||
/>
|
||||
</template>
|
||||
<template v-else>
|
||||
<toggle-switch-input
|
||||
name="re_fillable"
|
||||
:form="form"
|
||||
class="mt-4"
|
||||
label="Allow users to fill the form again"
|
||||
/>
|
||||
<text-input
|
||||
v-if="form.re_fillable"
|
||||
name="re_fill_button_text"
|
||||
:form="form"
|
||||
label="Text of re-start button"
|
||||
:required="true"
|
||||
/>
|
||||
<rich-text-area-input
|
||||
name="submitted_text"
|
||||
:form="form"
|
||||
label="Text After Submission"
|
||||
:required="false"
|
||||
/>
|
||||
</template>
|
||||
</editor-options-panel>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { useWorkingFormStore } from "../../../../../stores/working_form"
|
||||
import EditorOptionsPanel from "../../../editors/EditorOptionsPanel.vue"
|
||||
import ProTag from "~/components/global/ProTag.vue"
|
||||
import VTransition from "~/components/global/transitions/VTransition.vue"
|
||||
|
||||
export default {
|
||||
components: { EditorOptionsPanel, ProTag, VTransition },
|
||||
props: {},
|
||||
setup() {
|
||||
const workingFormStore = useWorkingFormStore()
|
||||
const { content: form } = storeToRefs(workingFormStore)
|
||||
return {
|
||||
form,
|
||||
workingFormStore,
|
||||
crisp: useCrisp(),
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
submissionOptions: {},
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
/**
|
||||
* Used for the update record on submission. Lists all visible fields on which you can filter records to update
|
||||
* on submission instead of creating
|
||||
*/
|
||||
filterableFields() {
|
||||
if (this.submissionOptions.databaseAction !== "update") return []
|
||||
return this.form.properties
|
||||
.filter((field) => {
|
||||
return (
|
||||
!field.hidden &&
|
||||
!["files", "signature", "multi_select"].includes(field.type)
|
||||
)
|
||||
})
|
||||
.map((field) => {
|
||||
return {
|
||||
name: field.name,
|
||||
value: field.id,
|
||||
}
|
||||
})
|
||||
},
|
||||
},
|
||||
|
||||
watch: {
|
||||
form: {
|
||||
handler() {
|
||||
if (this.form) {
|
||||
this.submissionOptions = {
|
||||
submissionMode: this.form.redirect_url ? "redirect" : "default",
|
||||
databaseAction: this.form.database_fields_update
|
||||
? "update"
|
||||
: "create",
|
||||
}
|
||||
}
|
||||
},
|
||||
deep: true,
|
||||
},
|
||||
submissionOptions: {
|
||||
deep: true,
|
||||
handler: function (val) {
|
||||
if (val.submissionMode === "default") {
|
||||
this.form.redirect_url = null
|
||||
}
|
||||
|
||||
if (val.databaseAction === "create") {
|
||||
this.form.database_fields_update = null
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
|
@ -1,101 +0,0 @@
|
|||
<template>
|
||||
<editor-options-panel
|
||||
name="Form Access"
|
||||
:already-opened="false"
|
||||
>
|
||||
<template #icon>
|
||||
<svg
|
||||
class="h-5 w-5"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M15.75 5.25a3 3 0 013 3m3 0a6 6 0 01-7.029 5.912c-.563-.097-1.159.026-1.563.43L10.5 17.25H8.25v2.25H6v2.25H2.25v-2.818c0-.597.237-1.17.659-1.591l6.499-6.499c.404-.404.527-1 .43-1.563A6 6 0 1121.75 8.25z"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
||||
<text-input
|
||||
name="password"
|
||||
:form="form"
|
||||
class="mt-4"
|
||||
label="Form Password"
|
||||
help="Leave empty to disable password"
|
||||
/>
|
||||
<date-input
|
||||
:with-time="true"
|
||||
name="closes_at"
|
||||
class="mt-4"
|
||||
:form="form"
|
||||
label="Close form on a scheduled date"
|
||||
help="Leave empty to keep the form open"
|
||||
:required="false"
|
||||
/>
|
||||
<rich-text-area-input
|
||||
v-if="form.closes_at || form.visibility == 'closed'"
|
||||
name="closed_text"
|
||||
:form="form"
|
||||
class="mt-4"
|
||||
label="Closed form text"
|
||||
help="This message will be shown when the form will be closed"
|
||||
:required="false"
|
||||
/>
|
||||
<text-input
|
||||
name="max_submissions_count"
|
||||
native-type="number"
|
||||
:min="1"
|
||||
:form="form"
|
||||
label="Limit number of submissions"
|
||||
placeholder="Max submissions"
|
||||
class="mt-4"
|
||||
help="Leave empty for unlimited submissions"
|
||||
:required="false"
|
||||
/>
|
||||
<rich-text-area-input
|
||||
v-if="form.max_submissions_count && form.max_submissions_count > 0"
|
||||
name="max_submissions_reached_text"
|
||||
class="mt-4"
|
||||
:form="form"
|
||||
label="Max Submissions reached text"
|
||||
help="This message will be shown when the form will have the maximum number of submissions"
|
||||
:required="false"
|
||||
/>
|
||||
</editor-options-panel>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { useWorkingFormStore } from "../../../../../stores/working_form"
|
||||
import EditorOptionsPanel from "../../../editors/EditorOptionsPanel.vue"
|
||||
|
||||
export default {
|
||||
components: { EditorOptionsPanel },
|
||||
props: {},
|
||||
setup() {
|
||||
const workingFormStore = useWorkingFormStore()
|
||||
return {
|
||||
workingFormStore,
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {}
|
||||
},
|
||||
computed: {
|
||||
form: {
|
||||
get() {
|
||||
return this.workingFormStore.content
|
||||
},
|
||||
/* We add a setter */
|
||||
set(value) {
|
||||
this.workingFormStore.set(value)
|
||||
},
|
||||
},
|
||||
},
|
||||
watch: {},
|
||||
mounted() {},
|
||||
methods: {},
|
||||
}
|
||||
</script>
|
||||
|
|
@ -1,58 +1,52 @@
|
|||
<template>
|
||||
<editor-options-panel
|
||||
<SettingsSection
|
||||
name="Custom Code"
|
||||
:already-opened="false"
|
||||
:has-pro-tag="true"
|
||||
icon="i-heroicons-code-bracket"
|
||||
>
|
||||
<template #icon>
|
||||
<svg
|
||||
class="h-5 w-5"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M14 2.26953V6.40007C14 6.96012 14 7.24015 14.109 7.45406C14.2049 7.64222 14.3578 7.7952 14.546 7.89108C14.7599 8.00007 15.0399 8.00007 15.6 8.00007H19.7305M14 17.5L16.5 15L14 12.5M10 12.5L7.5 15L10 17.5M20 9.98822V17.2C20 18.8802 20 19.7202 19.673 20.362C19.3854 20.9265 18.9265 21.3854 18.362 21.673C17.7202 22 16.8802 22 15.2 22H8.8C7.11984 22 6.27976 22 5.63803 21.673C5.07354 21.3854 4.6146 20.9265 4.32698 20.362C4 19.7202 4 18.8802 4 17.2V6.8C4 5.11984 4 4.27976 4.32698 3.63803C4.6146 3.07354 5.07354 2.6146 5.63803 2.32698C6.27976 2 7.11984 2 8.8 2H12.0118C12.7455 2 13.1124 2 13.4577 2.08289C13.7638 2.15638 14.0564 2.27759 14.3249 2.44208C14.6276 2.6276 14.887 2.88703 15.4059 3.40589L18.5941 6.59411C19.113 7.11297 19.3724 7.3724 19.5579 7.67515C19.7224 7.94356 19.8436 8.2362 19.9171 8.5423C20 8.88757 20 9.25445 20 9.98822Z"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
||||
<p class="mt-4">
|
||||
The code will be injected in the
|
||||
<span class="font-semibold">head</span> section of your form page.
|
||||
<ProTag
|
||||
class="mb-2 block"
|
||||
upgrade-modal-title="Upgrade to Unlock Custom Code Capabilities"
|
||||
upgrade-modal-description="On the Free plan, you can explore custom code features within the form editor. Upgrade your plan to implement custom scripts, styles, and advanced tracking in live forms. Elevate your form's functionality and design with unlimited customization options."
|
||||
/>
|
||||
<p class="text-sm text-gray-500">
|
||||
The code will be injected in the <b>head</b> section of your form page.
|
||||
<a
|
||||
href="#"
|
||||
class="text-gray-700"
|
||||
@click.prevent="
|
||||
crisp.openHelpdeskArticle(
|
||||
'how-do-i-add-custom-code-to-my-form-1amadj3',
|
||||
)
|
||||
"
|
||||
>Click here to get an example CSS code.</a>
|
||||
</p>
|
||||
|
||||
<code-input
|
||||
name="custom_code"
|
||||
class="mt-4"
|
||||
:form="form"
|
||||
help="Custom code cannot be previewed in our editor. Please test your code using
|
||||
your actual form page (save changes beforehand)."
|
||||
help="Saves changes and visit the actual form page to test."
|
||||
label="Custom Code"
|
||||
placeholder="<script>console.log('Hello World!')</script>"
|
||||
/>
|
||||
</editor-options-panel>
|
||||
</SettingsSection>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import EditorOptionsPanel from "../../../editors/EditorOptionsPanel.vue"
|
||||
import ProTag from '~/components/global/ProTag.vue'
|
||||
import { useWorkingFormStore } from '../../../../../stores/working_form'
|
||||
|
||||
export default {
|
||||
components: { EditorOptionsPanel },
|
||||
props: {},
|
||||
setup() {
|
||||
components: {
|
||||
ProTag
|
||||
},
|
||||
setup () {
|
||||
const workingFormStore = useWorkingFormStore()
|
||||
const { content: form } = storeToRefs(workingFormStore)
|
||||
return {
|
||||
form,
|
||||
workingFormStore,
|
||||
form: storeToRefs(workingFormStore).content,
|
||||
crisp: useCrisp()
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {}
|
||||
},
|
||||
|
||||
computed: {},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -1,28 +1,18 @@
|
|||
<template>
|
||||
<editor-options-panel
|
||||
name="Link Settings - SEO"
|
||||
:already-opened="false"
|
||||
:has-pro-tag="true"
|
||||
>
|
||||
<template #icon>
|
||||
<svg
|
||||
class="h-5 w-5"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M2.25 15.75l5.159-5.159a2.25 2.25 0 013.182 0l5.159 5.159m-1.5-1.5l1.409-1.409a2.25 2.25 0 013.182 0l2.909 2.909m-18 3.75h16.5a1.5 1.5 0 001.5-1.5V6a1.5 1.5 0 00-1.5-1.5H3.75A1.5 1.5 0 002.25 6v12a1.5 1.5 0 001.5 1.5zm10.5-11.25h.008v.008h-.008V8.25zm.375 0a.375.375 0 11-.75 0 .375.375 0 01.75 0z"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
||||
<p class="mt-4 text-gray-500 text-sm">
|
||||
Customize the link, images and text that appear when you share your form
|
||||
on other sites (Open Graph).
|
||||
<SettingsSection
|
||||
name="Link Settings"
|
||||
icon="i-heroicons-link"
|
||||
>
|
||||
<h4 class="font-semibold mt-4 border-t pt-4">
|
||||
SEO & Social Sharing - Meta <ProTag
|
||||
class="ml-2"
|
||||
upgrade-modal-title="Upgrade to Enhance Your Form's SEO"
|
||||
upgrade-modal-description="Explore advanced SEO features in the editor on our Free plan. Upgrade to fully implement custom meta tags, Open Graph data, and improved search visibility. Boost your form's online presence and attract more respondents with our premium SEO toolkit."
|
||||
/>
|
||||
</h4>
|
||||
<p class="text-gray-500 text-sm">
|
||||
Customize the image and text that appear when you share your form on other
|
||||
sites (Open Graph).
|
||||
</p>
|
||||
<select-input
|
||||
v-if="useFeatureFlag('custom_domains')"
|
||||
|
|
@ -31,59 +21,75 @@
|
|||
:disabled="customDomainOptions.length <= 0"
|
||||
:options="customDomainOptions"
|
||||
name="type"
|
||||
class="mt-4"
|
||||
class="mt-4 max-w-xs"
|
||||
label="Form Domain"
|
||||
placeholder="yourdomain.com"
|
||||
/>
|
||||
<text-input
|
||||
v-model="form.seo_meta.page_title"
|
||||
name="page_title"
|
||||
class="mt-4"
|
||||
label="Page Title"
|
||||
help="Under or approximately 60 characters"
|
||||
/>
|
||||
<text-area-input
|
||||
v-model="form.seo_meta.page_description"
|
||||
name="page_description"
|
||||
class="mt-4"
|
||||
label="Page Description"
|
||||
help="Between 150 and 160 characters"
|
||||
/>
|
||||
<image-input
|
||||
v-model="form.seo_meta.page_thumbnail"
|
||||
name="page_thumbnail"
|
||||
class="mt-4"
|
||||
label="Page Thumbnail Image"
|
||||
help="Also know as og:image - 1200 X 630"
|
||||
/>
|
||||
<image-input
|
||||
v-model="form.seo_meta.page_favicon"
|
||||
name="page_favicon"
|
||||
class="mt-4"
|
||||
label="Page Favicon Image"
|
||||
help="Upload favicon image to be displayed on the form page"
|
||||
/>
|
||||
</editor-options-panel>
|
||||
<template v-if="form.seo_meta">
|
||||
<text-input
|
||||
v-model="form.seo_meta.page_title"
|
||||
name="page_title"
|
||||
class="mt-4 max-w-xs"
|
||||
label="Page Title"
|
||||
help="Max 60 characters recommended"
|
||||
/>
|
||||
<text-area-input
|
||||
v-model="form.seo_meta.page_description"
|
||||
name="page_description"
|
||||
class="max-w-xs"
|
||||
label="Page Description"
|
||||
help="Between 150 and 160 characters"
|
||||
/>
|
||||
<div class="flex gap-4">
|
||||
<image-input
|
||||
v-model="form.seo_meta.page_thumbnail"
|
||||
name="page_thumbnail"
|
||||
class="flex-grow"
|
||||
label="Thumbnail Image"
|
||||
help="og:image - 1200px X 630px"
|
||||
/>
|
||||
<image-input
|
||||
v-model="form.seo_meta.page_favicon"
|
||||
name="page_favicon"
|
||||
class="flex-grow"
|
||||
label="Favicon Image"
|
||||
help="Public form page favicon"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="w-full border-t pt-4">
|
||||
<h4 class="font-semibold">
|
||||
Link Privacy
|
||||
</h4>
|
||||
<p class="text-gray-500 text-sm mb-4">
|
||||
Disable to prevent Google from listing your form in search results.
|
||||
</p>
|
||||
<ToggleSwitchInput
|
||||
name="can_be_indexed"
|
||||
:form="form"
|
||||
label="Indexable by Google"
|
||||
/>
|
||||
</div>
|
||||
</SettingsSection>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { useWorkingFormStore } from "../../../../../stores/working_form"
|
||||
import EditorOptionsPanel from "../../../editors/EditorOptionsPanel.vue"
|
||||
import ProTag from '~/components/global/ProTag.vue'
|
||||
import { useWorkingFormStore } from '../../../../../stores/working_form'
|
||||
|
||||
export default {
|
||||
components: { EditorOptionsPanel },
|
||||
props: {},
|
||||
setup() {
|
||||
components: {
|
||||
ProTag
|
||||
},
|
||||
setup () {
|
||||
const workingFormStore = useWorkingFormStore()
|
||||
return {
|
||||
workspacesStore: useWorkspacesStore(),
|
||||
workingFormStore,
|
||||
form: storeToRefs(workingFormStore).content,
|
||||
workspacesStore: useWorkspacesStore(),
|
||||
form: storeToRefs(workingFormStore).content
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {}
|
||||
},
|
||||
computed: {
|
||||
workspace() {
|
||||
return this.workspacesStore.getCurrent
|
||||
|
|
@ -99,21 +105,18 @@ export default {
|
|||
: []
|
||||
},
|
||||
},
|
||||
watch: {},
|
||||
mounted() {
|
||||
mounted () {
|
||||
if (!this.form.seo_meta || Array.isArray(this.form.seo_meta))
|
||||
this.form.seo_meta = {};
|
||||
|
||||
["page_title", "page_description", "page_thumbnail", "page_favicon"].forEach((keyname) => {
|
||||
if (this.form.seo_meta[keyname] === undefined) {
|
||||
|
||||
['page_title', 'page_description', 'page_thumbnail', 'page_favicon'].forEach((keyname) => {
|
||||
if (this.form.seo_meta[keyname] === undefined)
|
||||
this.form.seo_meta[keyname] = null
|
||||
}
|
||||
})
|
||||
|
||||
if (this.form.custom_domain && !this.workspace.custom_domains.find((item) => { return item === this.form.custom_domain })) {
|
||||
this.form.custom_domain = null
|
||||
}
|
||||
},
|
||||
methods: {},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -1,45 +1,10 @@
|
|||
<template>
|
||||
<editor-options-panel
|
||||
name="Customization"
|
||||
:already-opened="true"
|
||||
>
|
||||
<template #icon>
|
||||
<svg
|
||||
class="h-5 w-5"
|
||||
viewBox="0 0 20 20"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M1.66667 9.99984C1.66667 14.6022 5.39763 18.3332 10 18.3332C11.3807 18.3332 12.5 17.2139 12.5 15.8332V15.4165C12.5 15.0295 12.5 14.836 12.5214 14.6735C12.6691 13.5517 13.5519 12.6689 14.6737 12.5212C14.8361 12.4998 15.0297 12.4998 15.4167 12.4998H15.8333C17.214 12.4998 18.3333 11.3805 18.3333 9.99984C18.3333 5.39746 14.6024 1.6665 10 1.6665C5.39763 1.6665 1.66667 5.39746 1.66667 9.99984Z"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.67"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M5.83333 10.8332C6.29357 10.8332 6.66667 10.4601 6.66667 9.99984C6.66667 9.5396 6.29357 9.1665 5.83333 9.1665C5.3731 9.1665 5 9.5396 5 9.99984C5 10.4601 5.3731 10.8332 5.83333 10.8332Z"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.67"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M13.3333 7.49984C13.7936 7.49984 14.1667 7.12674 14.1667 6.6665C14.1667 6.20627 13.7936 5.83317 13.3333 5.83317C12.8731 5.83317 12.5 6.20627 12.5 6.6665C12.5 7.12674 12.8731 7.49984 13.3333 7.49984Z"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.67"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M8.33333 6.6665C8.79357 6.6665 9.16667 6.29341 9.16667 5.83317C9.16667 5.37293 8.79357 4.99984 8.33333 4.99984C7.8731 4.99984 7.5 5.37293 7.5 5.83317C7.5 6.29341 7.8731 6.6665 8.33333 6.6665Z"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.67"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
||||
<div class="px-4 pb-4">
|
||||
<EditorSectionHeader
|
||||
icon="heroicons:paint-brush-16-solid"
|
||||
title="Basic Appearance"
|
||||
:show-line="false"
|
||||
/>
|
||||
|
||||
<select-input
|
||||
name="theme"
|
||||
|
|
@ -52,7 +17,38 @@
|
|||
:form="form"
|
||||
label="Form Theme"
|
||||
/>
|
||||
<color-input
|
||||
name="color"
|
||||
:form="form"
|
||||
>
|
||||
<template #help>
|
||||
<InputHelp>
|
||||
<span class="text-gray-500">
|
||||
Color (for buttons & inputs border) - <a
|
||||
class="text-blue-500"
|
||||
href="#"
|
||||
@click.prevent="form.color = DEFAULT_COLOR"
|
||||
>Reset</a>
|
||||
</span>
|
||||
</InputHelp>
|
||||
</template>
|
||||
</color-input>
|
||||
<select-input
|
||||
name="dark_mode"
|
||||
:options="[
|
||||
{ name: 'Auto', value: 'auto' },
|
||||
{ name: 'Light Mode', value: 'light' },
|
||||
{ name: 'Dark Mode', value: 'dark' },
|
||||
]"
|
||||
:form="form"
|
||||
label="Color Mode"
|
||||
help="Use Auto to use device system preferences"
|
||||
/>
|
||||
|
||||
<EditorSectionHeader
|
||||
icon="octicon:typography-16"
|
||||
title="Typography"
|
||||
/>
|
||||
<template v-if="useFeatureFlag('services.google.fonts')">
|
||||
<label class="text-gray-700 font-medium text-sm">Font Style</label>
|
||||
<v-button
|
||||
|
|
@ -72,7 +68,16 @@
|
|||
@apply="onApplyFont"
|
||||
/>
|
||||
</template>
|
||||
<toggle-switch-input
|
||||
name="uppercase_labels"
|
||||
:form="form"
|
||||
label="Uppercase Input Labels"
|
||||
/>
|
||||
|
||||
<EditorSectionHeader
|
||||
icon="heroicons:rectangle-stack-16-solid"
|
||||
title="Layout & Sizing"
|
||||
/>
|
||||
<div class="flex space-x-4 justify-stretch">
|
||||
<select-input
|
||||
name="size"
|
||||
|
|
@ -98,19 +103,6 @@
|
|||
label="Input Roundness"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<select-input
|
||||
name="dark_mode"
|
||||
help="To see changes, save your form and open it"
|
||||
:options="[
|
||||
{ name: 'Auto - use Device System Preferences', value: 'auto' },
|
||||
{ name: 'Light Mode', value: 'light' },
|
||||
{ name: 'Dark Mode', value: 'dark' },
|
||||
]"
|
||||
:form="form"
|
||||
label="Dark Mode"
|
||||
/>
|
||||
|
||||
<select-input
|
||||
name="width"
|
||||
:options="[
|
||||
|
|
@ -122,14 +114,10 @@
|
|||
help="Useful when embedding your form"
|
||||
/>
|
||||
|
||||
<image-input
|
||||
name="cover_picture"
|
||||
:form="form"
|
||||
label="Cover Picture"
|
||||
help="Not visible when form is embedded"
|
||||
:required="false"
|
||||
<EditorSectionHeader
|
||||
icon="heroicons:tag-16-solid"
|
||||
title="Branding & Media"
|
||||
/>
|
||||
|
||||
<image-input
|
||||
name="logo_picture"
|
||||
:form="form"
|
||||
|
|
@ -137,24 +125,11 @@
|
|||
help="Not visible when form is embedded"
|
||||
:required="false"
|
||||
/>
|
||||
|
||||
<color-input
|
||||
name="color"
|
||||
<image-input
|
||||
name="cover_picture"
|
||||
:form="form"
|
||||
>
|
||||
<template #help>
|
||||
<InputHelp>
|
||||
<span class="text-gray-500">
|
||||
Color (for buttons & inputs border) - <a
|
||||
class="text-blue-500"
|
||||
href="#"
|
||||
@click.prevent="form.color = DEFAULT_COLOR"
|
||||
>Reset</a>
|
||||
</span>
|
||||
</InputHelp>
|
||||
</template>
|
||||
</color-input>
|
||||
|
||||
label="Color (for buttons & inputs border)"
|
||||
/>
|
||||
<toggle-switch-input
|
||||
name="hide_title"
|
||||
:form="form"
|
||||
|
|
@ -175,15 +150,25 @@
|
|||
/>
|
||||
</template>
|
||||
</toggle-switch-input>
|
||||
|
||||
<EditorSectionHeader
|
||||
icon="heroicons:cog-6-tooth-16-solid"
|
||||
title="Advanced Options"
|
||||
/>
|
||||
<toggle-switch-input
|
||||
name="hide_title"
|
||||
:form="form"
|
||||
label="Hide Form Title"
|
||||
/>
|
||||
<toggle-switch-input
|
||||
name="show_progress_bar"
|
||||
:form="form"
|
||||
label="Show progress bar"
|
||||
/>
|
||||
<toggle-switch-input
|
||||
name="uppercase_labels"
|
||||
:form="form"
|
||||
label="Uppercase Input Labels"
|
||||
:help="
|
||||
form.show_progress_bar
|
||||
? 'The bar is at the top of the page (above navigation in this editor) or below the title when embedded'
|
||||
: ''
|
||||
"
|
||||
/>
|
||||
<toggle-switch-input
|
||||
name="transparent_background"
|
||||
|
|
@ -197,23 +182,17 @@
|
|||
label="Confetti on successful submisison"
|
||||
@update:model-value="onChangeConfettiOnSubmission"
|
||||
/>
|
||||
<toggle-switch-input
|
||||
name="auto_save"
|
||||
:form="form"
|
||||
label="Auto save form response"
|
||||
help="Will save data in browser, if user not submit the form then next time will auto prefill last entered data"
|
||||
/>
|
||||
<ToggleSwitchInput
|
||||
name="auto_focus"
|
||||
:form="form"
|
||||
label="Auto focus first input on page"
|
||||
/>
|
||||
</editor-options-panel>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import EditorSectionHeader from "./EditorSectionHeader.vue"
|
||||
import { useWorkingFormStore } from "../../../../../stores/working_form"
|
||||
import EditorOptionsPanel from "../../../editors/EditorOptionsPanel.vue"
|
||||
import GoogleFontPicker from "../../../editors/GoogleFontPicker.vue"
|
||||
import ProTag from "~/components/global/ProTag.vue"
|
||||
import { DEFAULT_COLOR } from "@/composables/forms/initForm"
|
||||
|
|
|
|||
|
|
@ -1,150 +1,178 @@
|
|||
<template>
|
||||
<!-- Backdrop -->
|
||||
<div
|
||||
v-if="isExpanded"
|
||||
class="fixed inset-0 z-40 bg-white/30 dark:bg-gray-900/30 backdrop-blur-sm"
|
||||
@click="toggleExpand"
|
||||
/>
|
||||
|
||||
<!-- Form Preview (desktop only) -->
|
||||
<div
|
||||
ref="parent"
|
||||
class="bg-gray-50 dark:bg-notion-dark-light hidden md:flex flex-grow p-5 flex-col items-center overflow-y-scroll"
|
||||
:class="{
|
||||
'fixed inset-8 z-50 !flex': isExpanded,
|
||||
'bg-gray-50 dark:bg-notion-dark-light hidden md:flex flex-grow p-4 flex-col items-center overflow-y-scroll shadow-inner': !isExpanded
|
||||
}"
|
||||
>
|
||||
<div class="border rounded-lg bg-white dark:bg-notion-dark w-full block transition-all max-w-5xl">
|
||||
<transition
|
||||
enter-active-class="linear duration-100 overflow-hidden"
|
||||
enter-from-class="max-h-0"
|
||||
enter-to-class="max-h-56"
|
||||
leave-active-class="linear duration-100 overflow-hidden"
|
||||
leave-from-class="max-h-56"
|
||||
leave-to-class="max-h-0"
|
||||
>
|
||||
<div v-if="(form.logo_picture || form.cover_picture)">
|
||||
<div v-if="form.cover_picture">
|
||||
<div
|
||||
class="border rounded-lg bg-white dark:bg-notion-dark w-full block shadow-sm transition-all flex flex-col"
|
||||
:class="{ 'max-w-5xl': !isExpanded, 'h-full': isExpanded }"
|
||||
>
|
||||
<div class="w-full bg-white dark:bg-gray-950 border-b border-gray-300 dark:border-blue-900 dark:border-gray-700 rounded-t-lg p-1.5 px-4 flex items-center gap-x-1.5">
|
||||
<div class="bg-red-500 rounded-full w-2.5 h-2.5" />
|
||||
<div class="bg-yellow-500 rounded-full w-2.5 h-2.5" />
|
||||
<div class="bg-green-500 rounded-full w-2.5 h-2.5" />
|
||||
<p class="text-sm text-gray-500/70 text-sm ml-4">
|
||||
Form Preview
|
||||
</p>
|
||||
<div class="flex-grow" />
|
||||
<UButton
|
||||
v-if="previewFormSubmitted"
|
||||
icon="i-heroicons-arrow-path-rounded-square"
|
||||
color="white"
|
||||
size="xs"
|
||||
@click="restartForm"
|
||||
>
|
||||
Re-start
|
||||
</UButton>
|
||||
<UTooltip :text="isExpanded ? 'Collapse' : 'Expand'">
|
||||
<UButton
|
||||
v-track.form_editor_toggle_expand="{toggle: !isExpanded}"
|
||||
:icon="isExpanded ? 'i-heroicons-arrows-pointing-in' : 'i-heroicons-arrows-pointing-out'"
|
||||
color="white"
|
||||
size="xs"
|
||||
@click="toggleExpand"
|
||||
/>
|
||||
</UTooltip>
|
||||
</div>
|
||||
<div class="flex-grow overflow-y-auto">
|
||||
<transition
|
||||
enter-active-class="linear duration-100 overflow-hidden"
|
||||
enter-from-class="max-h-0"
|
||||
enter-to-class="max-h-56"
|
||||
leave-active-class="linear duration-100 overflow-hidden"
|
||||
leave-from-class="max-h-56"
|
||||
leave-to-class="max-h-0"
|
||||
>
|
||||
<div v-if="(form.logo_picture || form.cover_picture)">
|
||||
<div v-if="form.cover_picture">
|
||||
<div
|
||||
id="cover-picture"
|
||||
class="h-[30vh] w-full overflow-hidden flex items-center justify-center"
|
||||
>
|
||||
<img
|
||||
alt="Form Cover Picture"
|
||||
:src="coverPictureSrc(form.cover_picture)"
|
||||
class="object-cover w-full h-[30vh] object-center"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
id="cover-picture"
|
||||
class="max-h-56 rounded-t-lg w-full overflow-hidden flex items-center justify-center"
|
||||
v-if="form.logo_picture"
|
||||
class="w-full mx-auto py-5 relative"
|
||||
:class="{'pt-20':!form.cover_picture, 'max-w-lg': form && (form.width === 'centered'),'px-7': !isExpanded, 'px-3': isExpanded}"
|
||||
>
|
||||
<img
|
||||
alt="Cover Picture"
|
||||
:src="coverPictureSrc(form.cover_picture)"
|
||||
class="w-full"
|
||||
alt="Logo Picture"
|
||||
:src="coverPictureSrc(form.logo_picture)"
|
||||
:class="{'top-5':!form.cover_picture, '-top-10':form.cover_picture}"
|
||||
class="max-w-60 h-20 object-contain absolute left-5 transition-all"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="form.logo_picture"
|
||||
class="w-full mx-auto p-5 relative"
|
||||
:class="{'pt-20':!form.cover_picture, 'max-w-lg': form && (form.width === 'centered')}"
|
||||
>
|
||||
<img
|
||||
alt="Logo Picture"
|
||||
:src="coverPictureSrc(form.logo_picture)"
|
||||
:class="{'top-5':!form.cover_picture, '-top-10':form.cover_picture}"
|
||||
class="max-w-60 h-20 object-contain absolute left-5 transition-all"
|
||||
>
|
||||
</div>
|
||||
</transition>
|
||||
<div v-if="recordLoading">
|
||||
<p class="text-center p-4">
|
||||
<loader class="h-6 w-6 text-nt-blue mx-auto" />
|
||||
</p>
|
||||
</div>
|
||||
</transition>
|
||||
<open-complete-form
|
||||
ref="form-preview"
|
||||
class="w-full mx-auto py-5 px-3"
|
||||
:class="{'max-w-lg': form && (form.width === 'centered')}"
|
||||
:creating="creating"
|
||||
:form="form"
|
||||
:dark-mode="darkMode"
|
||||
:admin-preview="true"
|
||||
@restarted="previewFormSubmitted=false"
|
||||
@submitted="previewFormSubmitted=true"
|
||||
/>
|
||||
<open-complete-form
|
||||
v-show="!recordLoading"
|
||||
ref="formPreview"
|
||||
class="w-full mx-auto py-5"
|
||||
:class="{'max-w-lg': form && (form.width === 'centered'),'px-7': !isExpanded, 'px-3': isExpanded}"
|
||||
:creating="creating"
|
||||
:form="form"
|
||||
:dark-mode="darkMode"
|
||||
:admin-preview="!isExpanded"
|
||||
:show-cleanings="false"
|
||||
@restarted="previewFormSubmitted=false"
|
||||
@submitted="previewFormSubmitted=true"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-center text-xs text-gray-400 dark:text-gray-600 mt-1">
|
||||
Form Preview <span
|
||||
v-if="creating"
|
||||
class="font-normal text-gray-400 dark:text-gray-600 text-xs"
|
||||
>- Answers won't be saved</span>
|
||||
<br>
|
||||
<span v-if="previewFormSubmitted && !form.re_fillable">
|
||||
<a
|
||||
href="#"
|
||||
@click.prevent="$refs['form-preview'].restart()"
|
||||
>Restart Form
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-4 w-4 text-nt-blue inline"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M4 2a1 1 0 011 1v2.101a7.002 7.002 0 0111.601 2.566 1 1 0 11-1.885.666A5.002 5.002 0 005.999 7H9a1 1 0 010 2H4a1 1 0 01-1-1V3a1 1 0 011-1zm.008 9.057a1 1 0 011.276.61A5.002 5.002 0 0014.001 13H11a1 1 0 110-2h5a1 1 0 011 1v5a1 1 0 11-2 0v-2.101a7.002 7.002 0 01-11.601-2.566 1 1 0 01.61-1.276z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</a>
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
<script setup>
|
||||
import { computed, ref, watch, onMounted } from 'vue'
|
||||
import OpenCompleteForm from '../../OpenCompleteForm.vue'
|
||||
import {handleDarkMode, useDarkMode} from "~/lib/forms/public-page.js"
|
||||
import { default as _has } from 'lodash/has'
|
||||
import { useRecordsStore } from '~/stores/records'
|
||||
import { useWorkingFormStore } from '~/stores/working_form'
|
||||
import { storeToRefs } from 'pinia'
|
||||
|
||||
export default {
|
||||
components: { OpenCompleteForm },
|
||||
props: {},
|
||||
setup () {
|
||||
const workingFormStore = useWorkingFormStore()
|
||||
const parent = ref(null)
|
||||
return {
|
||||
workingFormStore,
|
||||
parent: parent,
|
||||
darkMode: useDarkMode(parent)
|
||||
}
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
previewFormSubmitted: false
|
||||
}
|
||||
},
|
||||
const recordsStore = useRecordsStore()
|
||||
const workingFormStore = useWorkingFormStore()
|
||||
|
||||
computed: {
|
||||
form: {
|
||||
get () {
|
||||
return this.workingFormStore.content
|
||||
},
|
||||
/* We add a setter */
|
||||
set (value) {
|
||||
this.workingFormStore.set(value)
|
||||
}
|
||||
},
|
||||
creating () { // returns true if we are creating a form
|
||||
return !_has(this.form, 'id')
|
||||
}
|
||||
},
|
||||
const parent = ref(null)
|
||||
const formPreview = ref(null)
|
||||
const previewFormSubmitted = ref(false)
|
||||
const isExpanded = ref(false)
|
||||
|
||||
watch: {
|
||||
'form.dark_mode': {
|
||||
handler () {
|
||||
this.handleDarkMode()
|
||||
const { content: form } = storeToRefs(workingFormStore)
|
||||
const recordLoading = computed(() => recordsStore.loading)
|
||||
const darkMode = useDarkMode(parent)
|
||||
|
||||
const creating = computed(() => !_has(form.value, 'id'))
|
||||
|
||||
defineShortcuts({
|
||||
escape: {
|
||||
usingInput: true,
|
||||
handler: () => {
|
||||
if (isExpanded.value) {
|
||||
isExpanded.value = false
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
mounted () {
|
||||
this.handleDarkMode()
|
||||
},
|
||||
watch(() => form.value.dark_mode, () => {
|
||||
handleDarkModeChange()
|
||||
})
|
||||
|
||||
methods: {
|
||||
coverPictureSrc (val) {
|
||||
try {
|
||||
// Is valid url
|
||||
new URL(val)
|
||||
} catch (_) {
|
||||
// Is file
|
||||
return URL.createObjectURL(val)
|
||||
}
|
||||
return val
|
||||
},
|
||||
handleDarkMode () {
|
||||
handleDarkMode(this.form.dark_mode, this.$refs.parent)
|
||||
}
|
||||
},
|
||||
onMounted(() => {
|
||||
handleDarkModeChange()
|
||||
})
|
||||
|
||||
function coverPictureSrc(val) {
|
||||
try {
|
||||
// Is valid url
|
||||
new URL(val)
|
||||
} catch (_) {
|
||||
// Is file
|
||||
return URL.createObjectURL(val)
|
||||
}
|
||||
return val
|
||||
}
|
||||
|
||||
function handleDarkModeChange() {
|
||||
handleDarkMode(form.value.dark_mode, parent.value)
|
||||
}
|
||||
|
||||
function restartForm() {
|
||||
previewFormSubmitted.value = false
|
||||
formPreview.value.restart()
|
||||
}
|
||||
|
||||
function toggleExpand() {
|
||||
isExpanded.value = !isExpanded.value
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.fixed {
|
||||
transition: all 0.3s ease-in-out;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
<template>
|
||||
<editor-right-sidebar
|
||||
:show="form && (showEditFieldSidebar || showAddFieldSidebar)"
|
||||
:width-class="showAddFieldSidebar ? 'md:max-w-[15rem]' : 'md:max-w-[20rem]'"
|
||||
:show="isOpen"
|
||||
>
|
||||
<VForm
|
||||
size="sm"
|
||||
|
|
@ -47,6 +48,9 @@ export default {
|
|||
return {}
|
||||
},
|
||||
computed: {
|
||||
isOpen() {
|
||||
return this.form !== null && (this.showEditFieldSidebar || this.showAddFieldSidebar)
|
||||
},
|
||||
form: {
|
||||
get() {
|
||||
return this.workingFormStore.content
|
||||
|
|
|
|||
|
|
@ -1,143 +1,122 @@
|
|||
<template>
|
||||
<editor-options-panel
|
||||
name="Information"
|
||||
:already-opened="true"
|
||||
<SettingsSection
|
||||
name="General"
|
||||
icon="i-heroicons-information-circle"
|
||||
>
|
||||
<p class="text-gray-500 text-sm">
|
||||
Basic information about your form.
|
||||
</p>
|
||||
|
||||
<text-input
|
||||
:form="form"
|
||||
name="title"
|
||||
class="mt-4 max-w-xs"
|
||||
label="Form Name"
|
||||
placeholder="My form"
|
||||
/>
|
||||
<select-input
|
||||
name="tags"
|
||||
label="Tags"
|
||||
clearable
|
||||
:form="form"
|
||||
help="To organize your forms"
|
||||
placeholder="Select Tag(s)"
|
||||
class="max-w-xs"
|
||||
:multiple="true"
|
||||
:allow-creation="true"
|
||||
:options="allTagsOptions"
|
||||
/>
|
||||
<flat-select-input
|
||||
name="visibility"
|
||||
label="Form Visibility"
|
||||
class="max-w-xs"
|
||||
:form="form"
|
||||
placeholder="Select Visibility"
|
||||
:options="visibilityOptions"
|
||||
/>
|
||||
<div
|
||||
v-if="form.closes_at || form.visibility == 'closed'"
|
||||
class="bg-gray-50 border rounded-lg px-4 py-2"
|
||||
>
|
||||
<rich-text-area-input
|
||||
name="closed_text"
|
||||
:form="form"
|
||||
label="Closed form text"
|
||||
help="This message will be shown when the form will be closed"
|
||||
:required="false"
|
||||
wrapper-class="mb-0"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<UButton
|
||||
v-if="copyFormOptions.length > 0"
|
||||
color="white"
|
||||
class="mt-4"
|
||||
icon="i-heroicons-document-duplicate"
|
||||
@click.prevent="showCopyFormSettingsModal = true"
|
||||
>
|
||||
Copy another form's settings
|
||||
</UButton>
|
||||
</SettingsSection>
|
||||
|
||||
<modal
|
||||
:show="showCopyFormSettingsModal"
|
||||
max-width="md"
|
||||
@close="showCopyFormSettingsModal = false"
|
||||
>
|
||||
<template #icon>
|
||||
<svg
|
||||
class="h-5 w-5"
|
||||
viewBox="0 0 20 20"
|
||||
class="w-10 h-10 text-blue"
|
||||
viewBox="0 0 48 48"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M10 13.3332V9.99984M10 6.6665H10.0083M18.3333 9.99984C18.3333 14.6022 14.6024 18.3332 10 18.3332C5.39763 18.3332 1.66667 14.6022 1.66667 9.99984C1.66667 5.39746 5.39763 1.6665 10 1.6665C14.6024 1.6665 18.3333 5.39746 18.3333 9.99984Z"
|
||||
d="M17 27C16.0681 27 15.6022 27 15.2346 26.8478C14.7446 26.6448 14.3552 26.2554 14.1522 25.7654C14 25.3978 14 24.9319 14 24V17.2C14 16.0799 14 15.5198 14.218 15.092C14.4097 14.7157 14.7157 14.4097 15.092 14.218C15.5198 14 16.0799 14 17.2 14H24C24.9319 14 25.3978 14 25.7654 14.1522C26.2554 14.3552 26.6448 14.7446 26.8478 15.2346C27 15.6022 27 16.0681 27 17M24.2 34H30.8C31.9201 34 32.4802 34 32.908 33.782C33.2843 33.5903 33.5903 33.2843 33.782 32.908C34 32.4802 34 31.9201 34 30.8V24.2C34 23.0799 34 22.5198 33.782 22.092C33.5903 21.7157 33.2843 21.4097 32.908 21.218C32.4802 21 31.9201 21 30.8 21H24.2C23.0799 21 22.5198 21 22.092 21.218C21.7157 21.4097 21.4097 21.7157 21.218 22.092C21 22.5198 21 23.0799 21 24.2V30.8C21 31.9201 21 32.4802 21.218 32.908C21.4097 33.2843 21.7157 33.5903 22.092 33.782C22.5198 34 23.0799 34 24.2 34Z"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.67"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
||||
<text-input
|
||||
name="title"
|
||||
class="mt-4"
|
||||
:form="form"
|
||||
label="Form Title"
|
||||
:required="true"
|
||||
/>
|
||||
<rich-text-area-input
|
||||
name="description"
|
||||
:form="form"
|
||||
label="Description"
|
||||
:required="false"
|
||||
/>
|
||||
<select-input
|
||||
name="tags"
|
||||
label="Tags"
|
||||
:form="form"
|
||||
help="To organize your forms (hidden to respondents)"
|
||||
placeholder="Select Tag(s)"
|
||||
:multiple="true"
|
||||
:allow-creation="true"
|
||||
:options="allTagsOptions"
|
||||
/>
|
||||
<select-input
|
||||
name="visibility"
|
||||
label="Visibility"
|
||||
:form="form"
|
||||
help="Only public form will be accessible"
|
||||
placeholder="Select Visibility"
|
||||
:required="true"
|
||||
:options="visibilityOptions"
|
||||
/>
|
||||
<v-button
|
||||
v-if="copyFormOptions.length > 0"
|
||||
color="light-gray"
|
||||
class="w-full"
|
||||
@click="showCopyFormSettingsModal = true"
|
||||
>
|
||||
<svg
|
||||
class="h-5 w-5 -mt-1 text-nt-blue inline mr-2"
|
||||
viewBox="0 0 20 20"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M4.16667 12.4998C3.3901 12.4998 3.00182 12.4998 2.69553 12.373C2.28715 12.2038 1.9627 11.8794 1.79354 11.471C1.66667 11.1647 1.66667 10.7764 1.66667 9.99984V4.33317C1.66667 3.39975 1.66667 2.93304 1.84833 2.57652C2.00812 2.26292 2.26308 2.00795 2.57669 1.84816C2.93321 1.6665 3.39992 1.6665 4.33334 1.6665H10C10.7766 1.6665 11.1649 1.6665 11.4711 1.79337C11.8795 1.96253 12.204 2.28698 12.3731 2.69536C12.5 3.00165 12.5 3.38993 12.5 4.1665M10.1667 18.3332H15.6667C16.6001 18.3332 17.0668 18.3332 17.4233 18.1515C17.7369 17.9917 17.9919 17.7368 18.1517 17.4232C18.3333 17.0666 18.3333 16.5999 18.3333 15.6665V10.1665C18.3333 9.23308 18.3333 8.76637 18.1517 8.40985C17.9919 8.09625 17.7369 7.84128 17.4233 7.68149C17.0668 7.49984 16.6001 7.49984 15.6667 7.49984H10.1667C9.23325 7.49984 8.76654 7.49984 8.41002 7.68149C8.09642 7.84128 7.84145 8.09625 7.68166 8.40985C7.50001 8.76637 7.50001 9.23308 7.50001 10.1665V15.6665C7.50001 16.5999 7.50001 17.0666 7.68166 17.4232C7.84145 17.7368 8.09642 17.9917 8.41002 18.1515C8.76654 18.3332 9.23325 18.3332 10.1667 18.3332Z"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.66667"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
Copy another form's settings
|
||||
</v-button>
|
||||
|
||||
<modal
|
||||
:show="showCopyFormSettingsModal"
|
||||
max-width="md"
|
||||
@close="showCopyFormSettingsModal = false"
|
||||
>
|
||||
<template #icon>
|
||||
<svg
|
||||
class="w-10 h-10 text-blue"
|
||||
viewBox="0 0 48 48"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
<template #title>
|
||||
Import Settings from another form
|
||||
</template>
|
||||
<div>
|
||||
<select-input
|
||||
v-model="copyFormId"
|
||||
name="copy_form_id"
|
||||
label="Copy Settings From"
|
||||
placeholder="Choose a form"
|
||||
:searchable="copyFormOptions.length > 5"
|
||||
:options="copyFormOptions"
|
||||
/>
|
||||
<div class="flex">
|
||||
<v-button
|
||||
color="white"
|
||||
class="w-full mr-2"
|
||||
@click="showCopyFormSettingsModal = false"
|
||||
>
|
||||
<path
|
||||
d="M17 27C16.0681 27 15.6022 27 15.2346 26.8478C14.7446 26.6448 14.3552 26.2554 14.1522 25.7654C14 25.3978 14 24.9319 14 24V17.2C14 16.0799 14 15.5198 14.218 15.092C14.4097 14.7157 14.7157 14.4097 15.092 14.218C15.5198 14 16.0799 14 17.2 14H24C24.9319 14 25.3978 14 25.7654 14.1522C26.2554 14.3552 26.6448 14.7446 26.8478 15.2346C27 15.6022 27 16.0681 27 17M24.2 34H30.8C31.9201 34 32.4802 34 32.908 33.782C33.2843 33.5903 33.5903 33.2843 33.782 32.908C34 32.4802 34 31.9201 34 30.8V24.2C34 23.0799 34 22.5198 33.782 22.092C33.5903 21.7157 33.2843 21.4097 32.908 21.218C32.4802 21 31.9201 21 30.8 21H24.2C23.0799 21 22.5198 21 22.092 21.218C21.7157 21.4097 21.4097 21.7157 21.218 22.092C21 22.5198 21 23.0799 21 24.2V30.8C21 31.9201 21 32.4802 21.218 32.908C21.4097 33.2843 21.7157 33.5903 22.092 33.782C22.5198 34 23.0799 34 24.2 34Z"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
||||
<template #title>
|
||||
Import Settings from another form
|
||||
</template>
|
||||
<div>
|
||||
<select-input
|
||||
v-model="copyFormId"
|
||||
name="copy_form_id"
|
||||
label="Copy Settings From"
|
||||
placeholder="Choose a form"
|
||||
:searchable="copyFormOptions.length > 5"
|
||||
:options="copyFormOptions"
|
||||
/>
|
||||
<div class="flex">
|
||||
<v-button
|
||||
color="white"
|
||||
class="w-full mr-2"
|
||||
@click="showCopyFormSettingsModal = false"
|
||||
>
|
||||
Cancel
|
||||
</v-button>
|
||||
<v-button
|
||||
color="blue"
|
||||
class="w-full"
|
||||
@click="copySettings"
|
||||
>
|
||||
Confirm & Copy
|
||||
</v-button>
|
||||
</div>
|
||||
Cancel
|
||||
</v-button>
|
||||
<v-button
|
||||
color="blue"
|
||||
class="w-full"
|
||||
@click="copySettings"
|
||||
>
|
||||
Confirm & Copy
|
||||
</v-button>
|
||||
</div>
|
||||
</modal>
|
||||
</editor-options-panel>
|
||||
</div>
|
||||
</modal>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import clonedeep from "clone-deep"
|
||||
import EditorOptionsPanel from "../../../editors/EditorOptionsPanel.vue"
|
||||
import SelectInput from "../../../../forms/SelectInput.vue"
|
||||
import { default as _has } from "lodash/has"
|
||||
|
||||
export default {
|
||||
components: { SelectInput, EditorOptionsPanel },
|
||||
props: {},
|
||||
|
||||
setup() {
|
||||
const formsStore = useFormsStore()
|
||||
const workingFormStore = useWorkingFormStore()
|
||||
|
|
@ -217,7 +196,6 @@ export default {
|
|||
// Clean copy from form
|
||||
[
|
||||
"title",
|
||||
"description",
|
||||
"properties",
|
||||
"cleanings",
|
||||
"views_count",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,100 @@
|
|||
<template>
|
||||
<SettingsSection
|
||||
name="Security & Access"
|
||||
icon="i-heroicons-shield-check"
|
||||
>
|
||||
<h4 class="font-semibold mt-4 border-t pt-4">
|
||||
Access
|
||||
</h4>
|
||||
<p class="text-gray-500 text-sm">
|
||||
Manage who can access your form and when.
|
||||
</p>
|
||||
|
||||
<text-input
|
||||
name="password"
|
||||
:form="form"
|
||||
class="mt-4 max-w-xs"
|
||||
label="Form Password"
|
||||
placeholder="********"
|
||||
help="Leave empty to disable password protection"
|
||||
/>
|
||||
<date-input
|
||||
:with-time="true"
|
||||
name="closes_at"
|
||||
class="mt-4 max-w-xs"
|
||||
:form="form"
|
||||
label="Closing date"
|
||||
help="Leave empty to keep the form open indefinitely"
|
||||
:required="false"
|
||||
/>
|
||||
<div
|
||||
v-if="form.closes_at || form.visibility == 'closed'"
|
||||
class="bg-gray-50 border rounded-lg px-4 py-2"
|
||||
>
|
||||
<rich-text-area-input
|
||||
name="closed_text"
|
||||
:form="form"
|
||||
label="Closed form text"
|
||||
help="This message will be shown when the form will be closed"
|
||||
:required="false"
|
||||
wrapper-class="mb-0"
|
||||
/>
|
||||
</div>
|
||||
<text-input
|
||||
name="max_submissions_count"
|
||||
native-type="number"
|
||||
:min="1"
|
||||
:form="form"
|
||||
label="Limit number of submissions"
|
||||
placeholder="Max submissions"
|
||||
class="mt-4 max-w-xs"
|
||||
help="Leave empty for unlimited submissions"
|
||||
:required="false"
|
||||
/>
|
||||
<div
|
||||
v-if="form.max_submissions_count && form.max_submissions_count > 0"
|
||||
class="bg-gray-50 border rounded-lg px-4 py-2"
|
||||
>
|
||||
<rich-text-area-input
|
||||
wrapper-class="mb-0"
|
||||
name="max_submissions_reached_text"
|
||||
:form="form"
|
||||
label="Max Submissions reached text"
|
||||
help="This message will be shown when the form will have the maximum number of submissions"
|
||||
:required="false"
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
<h4 class="font-semibold mt-4 border-t pt-4">
|
||||
Security
|
||||
</h4>
|
||||
<p class="text-gray-500 text-sm">
|
||||
Protect your form, and your sensitive files.
|
||||
</p>
|
||||
<ToggleSwitchInput
|
||||
name="use_captcha"
|
||||
:form="form"
|
||||
class="mt-4"
|
||||
label="Bot Protection"
|
||||
help="Protects your form from spam and abuse with a captcha"
|
||||
/>
|
||||
</SettingsSection>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { useWorkingFormStore } from '../../../../../stores/working_form'
|
||||
|
||||
export default {
|
||||
components: { },
|
||||
props: {},
|
||||
setup () {
|
||||
const workingFormStore = useWorkingFormStore()
|
||||
return {
|
||||
workingFormStore,
|
||||
form: storeToRefs(workingFormStore).content,
|
||||
crisp: useCrisp()
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
@ -1,67 +0,0 @@
|
|||
<template>
|
||||
<editor-options-panel
|
||||
name="Security & Privacy"
|
||||
:already-opened="false"
|
||||
>
|
||||
<template #icon>
|
||||
<svg
|
||||
class="h-5 w-5"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
||||
<toggle-switch-input
|
||||
name="can_be_indexed"
|
||||
:form="form"
|
||||
class="mt-4"
|
||||
label="Indexable by Google"
|
||||
help="If enabled, your form can appear in the search results of Google"
|
||||
/>
|
||||
<toggle-switch-input
|
||||
name="use_captcha"
|
||||
:form="form"
|
||||
class="mt-4"
|
||||
label="Protect your form with a Captcha"
|
||||
help="If enabled we will make sure respondant is a human"
|
||||
/>
|
||||
</editor-options-panel>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { useWorkingFormStore } from "../../../../../stores/working_form"
|
||||
import EditorOptionsPanel from "../../../editors/EditorOptionsPanel.vue"
|
||||
|
||||
export default {
|
||||
components: { EditorOptionsPanel },
|
||||
props: {},
|
||||
setup() {
|
||||
const workingFormStore = useWorkingFormStore()
|
||||
return {
|
||||
workingFormStore,
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {}
|
||||
},
|
||||
computed: {
|
||||
form: {
|
||||
get() {
|
||||
return this.workingFormStore.content
|
||||
},
|
||||
/* We add a setter */
|
||||
set(value) {
|
||||
this.workingFormStore.set(value)
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
<template>
|
||||
<VForm
|
||||
class="flex-grow flex flex-col"
|
||||
size="sm"
|
||||
@submit.prevent=""
|
||||
>
|
||||
<SettingsWrapper class="flex-grow">
|
||||
<FormInformation />
|
||||
<FormSubmissionSettings />
|
||||
<FormSecurityAccess />
|
||||
<FormCustomSeo />
|
||||
<FormCustomCode />
|
||||
</SettingsWrapper>
|
||||
</VForm>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { useWorkingFormStore } from '../../../../../stores/working_form'
|
||||
import FormInformation from './FormInformation.vue'
|
||||
import FormSubmissionSettings from './FormSubmissionSettings.vue'
|
||||
import FormSecurityAccess from './FormSecurityAccess.vue'
|
||||
import FormCustomSeo from './FormCustomSeo.vue'
|
||||
import FormCustomCode from './FormCustomCode.vue'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
FormInformation,
|
||||
FormSubmissionSettings,
|
||||
FormSecurityAccess,
|
||||
FormCustomSeo,
|
||||
FormCustomCode,
|
||||
},
|
||||
setup() {
|
||||
const workingFormStore = useWorkingFormStore()
|
||||
return {
|
||||
form: storeToRefs(workingFormStore).content
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
@ -0,0 +1,231 @@
|
|||
<template>
|
||||
<SettingsSection
|
||||
name="Submission Settings"
|
||||
icon="i-heroicons-paper-airplane"
|
||||
class="w-full"
|
||||
>
|
||||
<!-- Basic Submission Settings -->
|
||||
<h4 class="font-semibold mt-4 border-t pt-4">
|
||||
Basic
|
||||
</h4>
|
||||
<p class="text-gray-500 text-sm mb-4">
|
||||
Configure how form submissions are handled.
|
||||
</p>
|
||||
<text-input
|
||||
name="submit_button_text"
|
||||
:form="form"
|
||||
class="max-w-xs"
|
||||
label="Submit button text"
|
||||
:required="true"
|
||||
/>
|
||||
|
||||
<ToggleSwitchInput
|
||||
name="auto_save"
|
||||
:form="form"
|
||||
label="Auto save form response"
|
||||
help="Saves form progress, allowing respondents to resume later."
|
||||
/>
|
||||
|
||||
<flat-select-input
|
||||
:form="submissionOptions"
|
||||
name="databaseAction"
|
||||
class="max-w-xs"
|
||||
label="Database Submission Action"
|
||||
:options="[
|
||||
{ name: 'Create new record', value: 'create' },
|
||||
{ name: 'Update existing record', value: 'update' }
|
||||
]"
|
||||
:required="true"
|
||||
/>
|
||||
<div
|
||||
v-if="submissionOptions.databaseAction == 'update' && filterableFields.length"
|
||||
class="bg-gray-50 border rounded-lg px-4 py-2"
|
||||
>
|
||||
<div
|
||||
v-if="submissionOptions.databaseAction == 'update' && filterableFields.length"
|
||||
class="w-auto max-w-lg"
|
||||
>
|
||||
<p class="mb-2 mt-2 text-gray-500 text-sm">
|
||||
When matching values are found in the selected column(s), the (first) existing record will be updated instead of creating a new record. If there's no match, a new record will be created.
|
||||
<a
|
||||
href="#"
|
||||
class="text-blue-500 hover:underline"
|
||||
@click.prevent="crisp.openHelpdeskArticle('how-to-update-a-record-on-form-submission-1t1jwmn')"
|
||||
>
|
||||
Learn more.
|
||||
</a>
|
||||
</p>
|
||||
<select-input
|
||||
v-if="filterableFields.length"
|
||||
:form="form"
|
||||
name="database_fields_update"
|
||||
label="Properties to check on update"
|
||||
:options="filterableFields"
|
||||
multiple
|
||||
clearable
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Post-Submission Behavior -->
|
||||
<h4 class="font-semibold mt-4 border-t pt-4">
|
||||
After Submission <pro-tag
|
||||
upgrade-modal-title="Upgrade to customize post-submission behavior"
|
||||
upgrade-modal-description="Customize post-submission behavior: redirect users, show custom messages, or trigger actions. Upgrade to unlock advanced options for a seamless user experience. We have plenty of other pro features to enhance your form functionality and user engagement."
|
||||
/>
|
||||
</h4>
|
||||
<p class="text-gray-500 text-sm mb-4">
|
||||
Customize the user experience after form submission.
|
||||
</p>
|
||||
<div
|
||||
class="w-full"
|
||||
:class="{'flex flex-wrap gap-x-4':submissionOptions.submissionMode === 'redirect'}"
|
||||
>
|
||||
<select-input
|
||||
:form="submissionOptions"
|
||||
name="submissionMode"
|
||||
class="w-full max-w-xs"
|
||||
label="Action After Form Submission"
|
||||
:options="[
|
||||
{ name: 'Show Success page', value: 'default' },
|
||||
{ name: 'Redirect', value: 'redirect' }
|
||||
]"
|
||||
>
|
||||
<template #selected="{ option, optionName }">
|
||||
<div class="flex items-center truncate text-sm mr-6">
|
||||
{{ optionName }}
|
||||
<pro-tag
|
||||
v-if="option === 'redirect'"
|
||||
class="ml-2"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<template #option="{ option, selected }">
|
||||
<span class="flex">
|
||||
<p class="flex-grow">
|
||||
{{ option.name }} <template v-if="option.value === 'redirect'"><pro-tag /></template>
|
||||
</p>
|
||||
<span
|
||||
v-if="selected"
|
||||
class="absolute inset-y-0 right-0 flex items-center pr-4"
|
||||
>
|
||||
<Icon
|
||||
name="heroicons:check-20-solid"
|
||||
class="h-5 w-5"
|
||||
/>
|
||||
</span>
|
||||
</span>
|
||||
</template>
|
||||
</select-input>
|
||||
<template v-if="submissionOptions.submissionMode === 'redirect'">
|
||||
<text-input
|
||||
name="redirect_url"
|
||||
:form="form"
|
||||
class="w-full max-w-xs"
|
||||
label="Redirect URL"
|
||||
placeholder="https://www.google.com"
|
||||
:required="true"
|
||||
/>
|
||||
</template>
|
||||
<template v-else>
|
||||
<rich-text-area-input
|
||||
name="submitted_text"
|
||||
class="w-full"
|
||||
:form="form"
|
||||
label="Success page text"
|
||||
:required="false"
|
||||
/>
|
||||
<div class="flex items-center flex-wrap gap-x-4">
|
||||
<toggle-switch-input
|
||||
name="re_fillable"
|
||||
class="w-full max-w-xs"
|
||||
:form="form"
|
||||
label="Re-fillable form"
|
||||
help="Allows user to fill the form multiple times"
|
||||
/>
|
||||
<text-input
|
||||
v-if="form.re_fillable"
|
||||
name="re_fill_button_text"
|
||||
:form="form"
|
||||
label="Text of re-start button"
|
||||
:required="true"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center flex-wrap gap-x-4">
|
||||
<toggle-switch-input
|
||||
name="editable_submissions"
|
||||
class="w-full max-w-sm"
|
||||
help="Allows users to edit submissions via unique URL"
|
||||
:form="form"
|
||||
>
|
||||
<template #label>
|
||||
<span class="text-sm">
|
||||
Editable submissions
|
||||
</span>
|
||||
<ProTag
|
||||
class="ml-1"
|
||||
upgrade-modal-title="Upgrade to use Editable Submissions"
|
||||
upgrade-modal-description="On the Free plan, you can try out all paid features only within the form editor. Upgrade your plan to allow users to update their submissions via a unique URL, and much more. Gain full access to all advanced features."
|
||||
/>
|
||||
</template>
|
||||
</toggle-switch-input>
|
||||
<text-input
|
||||
v-if="form.editable_submissions"
|
||||
name="editable_submissions_button_text"
|
||||
class="w-full max-w-64"
|
||||
:form="form"
|
||||
label="Edit submission button text"
|
||||
:required="true"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</SettingsSection>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, watch } from 'vue'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { useWorkingFormStore } from '../../../../../stores/working_form'
|
||||
import ProTag from '~/components/global/ProTag.vue'
|
||||
import ToggleSwitchInput from '../../../../forms/ToggleSwitchInput.vue'
|
||||
import { useCrisp } from '~/composables/useCrisp'
|
||||
|
||||
const workingFormStore = useWorkingFormStore()
|
||||
const { content: form } = storeToRefs(workingFormStore)
|
||||
const crisp = useCrisp()
|
||||
|
||||
const submissionOptions = ref({})
|
||||
|
||||
const filterableFields = computed(() => {
|
||||
if (submissionOptions.value.databaseAction !== "update") return []
|
||||
return form.value.properties
|
||||
.filter((field) => {
|
||||
return (
|
||||
!field.hidden &&
|
||||
!["files", "signature", "multi_select"].includes(field.type)
|
||||
)
|
||||
})
|
||||
.map((field) => {
|
||||
return {
|
||||
name: field.name,
|
||||
value: field.id,
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
watch(form, () => {
|
||||
if (form.value) {
|
||||
submissionOptions.value = {
|
||||
submissionMode: form.value.redirect_url ? 'redirect' : 'default',
|
||||
databaseAction: form.value.database_fields_update ? 'update' : 'create'
|
||||
}
|
||||
}
|
||||
}, { deep: true })
|
||||
|
||||
watch(submissionOptions, (val) => {
|
||||
if (val.submissionMode === 'default') form.value.redirect_url = null
|
||||
if (val.databaseAction === 'create') form.value.database_fields_update = null
|
||||
}, { deep: true })
|
||||
</script>
|
||||
|
|
@ -1,25 +1,30 @@
|
|||
<template>
|
||||
<ErrorBoundary @on-error="handleError" ref="error_boundary">
|
||||
<ErrorBoundary
|
||||
ref="error_boundary"
|
||||
@on-error="handleError"
|
||||
>
|
||||
<query-builder
|
||||
v-model="query"
|
||||
:rules="rules"
|
||||
:config="config"
|
||||
v-bind="$attrs"
|
||||
@update:model-value="onChange"
|
||||
>
|
||||
<template #groupOperator="props">
|
||||
<div
|
||||
class="query-builder-group-slot__group-selection flex items-center px-5 border-b py-1 mb-1 flex"
|
||||
class="query-builder-group-slot__group-selection flex items-center px-5 border-b py-1 mb-"
|
||||
>
|
||||
<p class="mr-2 font-semibold">
|
||||
Operator
|
||||
</p>
|
||||
<select-input
|
||||
wrapper-class="relative"
|
||||
wrapper-class="relative mb-0"
|
||||
:model-value="props.currentOperator"
|
||||
:options="props.operators"
|
||||
emit-key="identifier"
|
||||
option-key="identifier"
|
||||
name="operator-input"
|
||||
:help="null"
|
||||
margin-bottom=""
|
||||
@update:model-value="props.updateCurrentOperator($event)"
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -3,12 +3,8 @@
|
|||
v-if="logic"
|
||||
:key="resetKey"
|
||||
>
|
||||
<h3 class="font-semibold block text-lg">
|
||||
Logic
|
||||
</h3>
|
||||
<p class="text-gray-400 text-xs mb-3">
|
||||
Add some logic to this block. Start by adding some conditions, and then
|
||||
add some actions.
|
||||
Select a field, add some conditions, and finally add some actions.
|
||||
</p>
|
||||
<div class="relative flex">
|
||||
<div>
|
||||
|
|
@ -17,7 +13,10 @@
|
|||
size="small"
|
||||
@click="showCopyFormModal = true"
|
||||
>
|
||||
<Icon name="lucide:copy" class="w-4 h-4 text-blue-600 inline mr-1 -mt-1" />
|
||||
<Icon
|
||||
name="lucide:copy"
|
||||
class="w-4 h-4 text-blue-600 inline mr-1 -mt-1"
|
||||
/>
|
||||
Copy from
|
||||
</v-button>
|
||||
</div>
|
||||
|
|
@ -29,13 +28,16 @@
|
|||
class="ml-1"
|
||||
@click="clearAll"
|
||||
>
|
||||
<Icon name="mdi:clear-outline" class="w-4 h-4 text-red-600 inline mr-1 -mt-1" />
|
||||
Clear All
|
||||
<Icon
|
||||
name="mdi:clear-outline"
|
||||
class="w-4 h-4 text-red-600 inline mr-1 -mt-1"
|
||||
/>
|
||||
Clear
|
||||
</v-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h5 class="font-semibold mt-3">
|
||||
<h5 class="font-medium text-gray-700 mt-3">
|
||||
1. Conditions
|
||||
</h5>
|
||||
<condition-editor
|
||||
|
|
@ -45,7 +47,7 @@
|
|||
:form="form"
|
||||
/>
|
||||
|
||||
<h5 class="font-semibold mt-3">
|
||||
<h5 class="font-medium text-gray-700 mt-3">
|
||||
2. Actions
|
||||
</h5>
|
||||
<flat-select-input
|
||||
|
|
|
|||
|
|
@ -1,25 +1,15 @@
|
|||
<template>
|
||||
<div>
|
||||
<div class="p-4 border-b sticky top-0 z-10 bg-white">
|
||||
<div class="px-4 py-2 border-b sticky top-0 z-10 bg-white">
|
||||
<button
|
||||
v-if="!field"
|
||||
class="text-gray-500 hover:text-gray-900 cursor-pointer"
|
||||
@click.prevent="closeSidebar"
|
||||
>
|
||||
<svg
|
||||
<Icon
|
||||
name="heroicons:x-mark-solid"
|
||||
class="h-6 w-6"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M18 6L6 18M6 6L18 18"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
/>
|
||||
</button>
|
||||
<template v-else>
|
||||
<div class="flex">
|
||||
|
|
@ -27,216 +17,201 @@
|
|||
class="text-gray-500 hover:text-gray-900 cursor-pointer"
|
||||
@click.prevent="closeSidebar"
|
||||
>
|
||||
<svg
|
||||
<Icon
|
||||
name="heroicons:x-mark-solid"
|
||||
class="h-6 w-6"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M18 6L6 18M6 6L18 18"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
/>
|
||||
</button>
|
||||
<div class="font-semibold inline ml-2 truncate flex-grow truncate">
|
||||
Configure "<span class="truncate">{{ field.name }}</span>"
|
||||
<div class="ml-2 flex flex-grow items-center space-between min-w-0 gap-x-3">
|
||||
<div class="flex-grow" />
|
||||
<BlockTypeIcon
|
||||
:type="field.type"
|
||||
/>
|
||||
|
||||
<p class="text-sm text-gray-500">
|
||||
{{ blocksTypes[field.type].title }}
|
||||
</p>
|
||||
|
||||
<UDropdown
|
||||
:items="dropdownItems"
|
||||
:popper="{ placement: 'bottom-start' }"
|
||||
>
|
||||
<UButton
|
||||
color="white"
|
||||
icon="i-heroicons-ellipsis-vertical"
|
||||
/>
|
||||
|
||||
<template
|
||||
v-if="typeCanBeChanged"
|
||||
#changetype
|
||||
>
|
||||
<ChangeFieldType
|
||||
v-if="!isBlockField"
|
||||
:field="field"
|
||||
@change-type="onChangeType"
|
||||
/>
|
||||
</template>
|
||||
</UDropdown>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex mt-2">
|
||||
<v-button
|
||||
color="light-gray"
|
||||
class="border-r-0 rounded-r-none text-xs hover:bg-red-50"
|
||||
size="small"
|
||||
@click="removeBlock"
|
||||
>
|
||||
<svg
|
||||
class="h-4 w-4 text-red-600 inline mr-1 -mt-1"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M3 6H5M5 6H21M5 6V20C5 20.5304 5.21071 21.0391 5.58579 21.4142C5.96086 21.7893 6.46957 22 7 22H17C17.5304 22 18.0391 21.7893 18.4142 21.4142C18.7893 21.0391 19 20.5304 19 20V6H5ZM8 6V4C8 3.46957 8.21071 2.96086 8.58579 2.58579C8.96086 2.21071 9.46957 2 10 2H14C14.5304 2 15.0391 2.21071 15.4142 2.58579C15.7893 2.96086 16 3.46957 16 4V6M10 11V17M14 11V17"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
Remove
|
||||
</v-button>
|
||||
<v-button
|
||||
size="small"
|
||||
class="text-xs"
|
||||
:class="{
|
||||
'rounded-none border-r-0': !isBlockField && typeCanBeChanged,
|
||||
'rounded-l-none': isBlockField || !typeCanBeChanged,
|
||||
}"
|
||||
color="light-gray"
|
||||
@click="duplicateBlock"
|
||||
>
|
||||
<svg
|
||||
class="h-4 w-4 text-blue-600 inline mr-1 -mt-1"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M5 15H4C3.46957 15 2.96086 14.7893 2.58579 14.4142C2.21071 14.0391 2 13.5304 2 13V4C2 3.46957 2.21071 2.96086 2.58579 2.58579C2.96086 2.21071 3.46957 2 4 2H13C13.5304 2 14.0391 2.21071 14.4142 2.58579C14.7893 2.96086 15 3.46957 15 4V5M11 9H20C21.1046 9 22 9.89543 22 11V20C22 21.1046 21.1046 22 20 22H11C9.89543 22 9 21.1046 9 20V11C9 9.89543 9.89543 9 11 9Z"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
Duplicate
|
||||
</v-button>
|
||||
<change-field-type
|
||||
v-if="!isBlockField"
|
||||
btn-classes="rounded-l-none text-xs"
|
||||
:field="field"
|
||||
@change-type="onChangeType"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<template v-if="field">
|
||||
<field-options
|
||||
v-if="!isBlockField"
|
||||
:form="form"
|
||||
:field="field"
|
||||
/>
|
||||
<block-options
|
||||
v-if="isBlockField"
|
||||
:form="form"
|
||||
:field="field"
|
||||
/>
|
||||
<div class="bg-gray-100 border-b">
|
||||
<UTabs
|
||||
v-model="activeTab"
|
||||
:items="tabItems"
|
||||
class="w-full"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="activeTab ===0">
|
||||
<FieldOptions
|
||||
v-if="!isBlockField"
|
||||
:form="form"
|
||||
:field="field"
|
||||
/>
|
||||
<BlockOptions
|
||||
v-else
|
||||
:form="form"
|
||||
:field="field"
|
||||
/>
|
||||
</div>
|
||||
<div v-else-if="activeTab === 1">
|
||||
<FormBlockLogicEditor
|
||||
class="py-2 px-4"
|
||||
:form="form"
|
||||
:field="field"
|
||||
/>
|
||||
</div>
|
||||
<div v-else-if="activeTab === 2">
|
||||
<custom-field-validation
|
||||
class="py-2 px-4"
|
||||
:form="form"
|
||||
:field="field"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<div
|
||||
v-else
|
||||
class="text-center p-10"
|
||||
class="text-center p-10 text-sm text-gray-500"
|
||||
>
|
||||
Click on field's setting icon in your form to modify it
|
||||
Click on field to edit it.
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { computed } from "vue"
|
||||
import clonedeep from "clone-deep"
|
||||
import { useWorkingFormStore } from "../../../../stores/working_form"
|
||||
|
||||
<script setup>
|
||||
import { storeToRefs } from 'pinia'
|
||||
import clonedeep from 'clone-deep'
|
||||
import FieldOptions from './components/FieldOptions.vue'
|
||||
import BlockOptions from './components/BlockOptions.vue'
|
||||
import BlockTypeIcon from '../components/BlockTypeIcon.vue'
|
||||
import ChangeFieldType from "./components/ChangeFieldType.vue"
|
||||
import FieldOptions from "./components/FieldOptions.vue"
|
||||
import BlockOptions from "./components/BlockOptions.vue"
|
||||
import blocksTypes from '~/data/blocks_types.json'
|
||||
import FormBlockLogicEditor from '../components/form-logic-components/FormBlockLogicEditor.vue'
|
||||
import CustomFieldValidation from '../components/CustomFieldValidation.vue'
|
||||
import { generateUUID } from '~/lib/utils'
|
||||
|
||||
export default {
|
||||
name: "FormFieldEdit",
|
||||
components: { ChangeFieldType, FieldOptions, BlockOptions },
|
||||
props: {},
|
||||
setup() {
|
||||
const workingFormStore = useWorkingFormStore()
|
||||
return {
|
||||
workingFormStore,
|
||||
selectedFieldIndex: computed(() => workingFormStore.selectedFieldIndex),
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {}
|
||||
},
|
||||
const workingFormStore = useWorkingFormStore()
|
||||
const { content: form } = storeToRefs(workingFormStore)
|
||||
|
||||
computed: {
|
||||
form: {
|
||||
get() {
|
||||
return this.workingFormStore.content
|
||||
},
|
||||
/* We add a setter */
|
||||
set(value) {
|
||||
this.workingFormStore.set(value)
|
||||
},
|
||||
},
|
||||
field() {
|
||||
return this.form && this.selectedFieldIndex !== null
|
||||
? this.form.properties[this.selectedFieldIndex]
|
||||
: null
|
||||
},
|
||||
isBlockField() {
|
||||
return this.field && this.field.type.startsWith("nf")
|
||||
},
|
||||
typeCanBeChanged() {
|
||||
return [
|
||||
"text",
|
||||
"email",
|
||||
"phone_number",
|
||||
"number",
|
||||
"select",
|
||||
"multi_select",
|
||||
"rating",
|
||||
"scale",
|
||||
"slider",
|
||||
].includes(this.field.type)
|
||||
},
|
||||
},
|
||||
const selectedFieldIndex = computed(() => workingFormStore.selectedFieldIndex)
|
||||
|
||||
watch: {},
|
||||
const field = computed(() => {
|
||||
return form.value && selectedFieldIndex.value !== null
|
||||
? form.value.properties[selectedFieldIndex.value]
|
||||
: null
|
||||
})
|
||||
|
||||
created() {},
|
||||
const isBlockField = computed(() => {
|
||||
return field.value && field.value.type.startsWith('nf')
|
||||
})
|
||||
|
||||
mounted() {},
|
||||
const typeCanBeChanged = computed(() => {
|
||||
return [
|
||||
"text",
|
||||
"email",
|
||||
"phone_number",
|
||||
"number",
|
||||
"select",
|
||||
"multi_select",
|
||||
"rating",
|
||||
"scale",
|
||||
"slider",
|
||||
].includes(field.value.type)
|
||||
})
|
||||
|
||||
methods: {
|
||||
onChangeType(newType) {
|
||||
if (["select", "multi_select"].includes(this.field.type)) {
|
||||
this.field[newType] = this.field[this.field.type] // Set new options with new type
|
||||
delete this.field[this.field.type] // remove old type options
|
||||
}
|
||||
this.field.type = newType
|
||||
},
|
||||
removeBlock() {
|
||||
const newFields = clonedeep(this.form.properties)
|
||||
newFields.splice(this.selectedFieldIndex, 1)
|
||||
this.form.properties = newFields
|
||||
this.closeSidebar()
|
||||
},
|
||||
duplicateBlock() {
|
||||
const newFields = clonedeep(this.form.properties)
|
||||
const newField = clonedeep(this.form.properties[this.selectedFieldIndex])
|
||||
newField.id = this.generateUUID()
|
||||
newFields.push(newField)
|
||||
this.form.properties = newFields
|
||||
this.closeSidebar()
|
||||
},
|
||||
closeSidebar() {
|
||||
this.workingFormStore.closeEditFieldSidebar()
|
||||
},
|
||||
generateUUID() {
|
||||
let d = new Date().getTime() // Timestamp
|
||||
let d2 =
|
||||
(typeof performance !== "undefined" &&
|
||||
performance.now &&
|
||||
performance.now() * 1000) ||
|
||||
0 // Time in microseconds since page-load or 0 if unsupported
|
||||
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(
|
||||
/[xy]/g,
|
||||
function (c) {
|
||||
let r = Math.random() * 16 // random number between 0 and 16
|
||||
if (d > 0) {
|
||||
// Use timestamp until depleted
|
||||
r = (d + r) % 16 | 0
|
||||
d = Math.floor(d / 16)
|
||||
} else {
|
||||
// Use microseconds since page-load if supported
|
||||
r = (d2 + r) % 16 | 0
|
||||
d2 = Math.floor(d2 / 16)
|
||||
}
|
||||
return (c === "x" ? r : (r & 0x3) | 0x8).toString(16)
|
||||
},
|
||||
)
|
||||
},
|
||||
},
|
||||
function removeBlock() {
|
||||
workingFormStore.removeField(field.value)
|
||||
}
|
||||
|
||||
function closeSidebar() {
|
||||
workingFormStore.closeEditFieldSidebar()
|
||||
}
|
||||
|
||||
function onChangeType(newType) {
|
||||
if (["select", "multi_select"].includes(field.value.type)) {
|
||||
field.value[newType] = field.value[field.value.type] // Set new options with new type
|
||||
delete field.value[field.value.type] // remove old type options
|
||||
}
|
||||
field.value.type = newType
|
||||
}
|
||||
|
||||
const dropdownItems = computed(() => {
|
||||
return [
|
||||
[{
|
||||
label: 'Copy field ID',
|
||||
icon: 'i-heroicons-clipboard-20-solid',
|
||||
click: () => {
|
||||
navigator.clipboard.writeText(field.value.id)
|
||||
useAlert().success('Field ID copied to clipboard')
|
||||
}
|
||||
}],
|
||||
[{
|
||||
label: 'Duplicate',
|
||||
icon: 'i-heroicons-document-duplicate-20-solid',
|
||||
click: () => {
|
||||
const newField = clonedeep(field.value)
|
||||
newField.id = generateUUID()
|
||||
newField.name = 'Copy of ' + newField.name
|
||||
const newFields = [...form.value.properties]
|
||||
newFields.splice(selectedFieldIndex.value + 1, 0, newField)
|
||||
form.value.properties = newFields
|
||||
}
|
||||
}],
|
||||
... (typeCanBeChanged.value ? [[{
|
||||
label: 'Change type',
|
||||
icon: 'i-heroicons-document-duplicate-20-solid',
|
||||
slot: 'changetype',
|
||||
}]] : []),
|
||||
[{
|
||||
label: 'Remove',
|
||||
icon: 'i-heroicons-trash-20-solid',
|
||||
class: 'group/remove hover:text-red-800',
|
||||
iconClass: 'group-hover/remove:text-red-900',
|
||||
click: removeBlock
|
||||
}]
|
||||
]
|
||||
})
|
||||
|
||||
|
||||
const activeTab = ref(0)
|
||||
const tabItems = computed(() => {
|
||||
const commonTabs = [
|
||||
{ label: 'Options'},
|
||||
{ label: 'Logic' },
|
||||
]
|
||||
|
||||
if (isBlockField.value) {
|
||||
return commonTabs
|
||||
} else {
|
||||
return [
|
||||
...commonTabs,
|
||||
{ label: 'Validation'},
|
||||
]
|
||||
}
|
||||
})
|
||||
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -1,53 +1,62 @@
|
|||
<template>
|
||||
<div v-if="field">
|
||||
<!-- General -->
|
||||
<div class="border-b py-2 px-4">
|
||||
<h3 class="font-semibold block text-lg">
|
||||
General
|
||||
</h3>
|
||||
<p class="text-gray-400 mb-3 text-xs">
|
||||
Exclude this field or make it required.
|
||||
</p>
|
||||
<toggle-switch-input
|
||||
<div
|
||||
v-if="field"
|
||||
class="py-2"
|
||||
>
|
||||
<div class="px-4">
|
||||
<text-input
|
||||
name="name"
|
||||
:form="field"
|
||||
name="hidden"
|
||||
label="Hidden"
|
||||
@update:model-value="onFieldHiddenChange"
|
||||
wrapper-class="mb-2"
|
||||
:required="true"
|
||||
label="Block Name"
|
||||
/>
|
||||
<select-input
|
||||
name="width"
|
||||
class="mt-3"
|
||||
:options="[
|
||||
{ name: 'Full', value: 'full' },
|
||||
{ name: '1/2 (half width)', value: '1/2' },
|
||||
{ name: '1/3 (a third of the width)', value: '1/3' },
|
||||
{ name: '2/3 (two thirds of the width)', value: '2/3' },
|
||||
{ name: '1/4 (a quarter of the width)', value: '1/4' },
|
||||
{ name: '3/4 (three quarters of the width)', value: '3/4' },
|
||||
]"
|
||||
|
||||
<HiddenRequiredDisabled
|
||||
:form="field"
|
||||
label="Field Width"
|
||||
/>
|
||||
<select-input
|
||||
v-if="['nf-text', 'nf-image'].includes(field.type)"
|
||||
name="align"
|
||||
class="mt-3"
|
||||
:options="[
|
||||
{ name: 'Left', value: 'left' },
|
||||
{ name: 'Center', value: 'center' },
|
||||
{ name: 'Right', value: 'right' },
|
||||
{ name: 'Justify', value: 'justify' },
|
||||
]"
|
||||
:form="field"
|
||||
label="Field Alignment"
|
||||
:field="field"
|
||||
:can-be-disabled="false"
|
||||
:can-be-hidden="true"
|
||||
:can-be-required="false"
|
||||
/>
|
||||
|
||||
<div class="grid grid-cols-2 gap-2 mt-2">
|
||||
<select-input
|
||||
name="width"
|
||||
class="flex-grow"
|
||||
:options="[
|
||||
{ name: 'Full', value: 'full' },
|
||||
{ name: '1/2', value: '1/2' },
|
||||
{ name: '1/3', value: '1/3' },
|
||||
{ name: '2/3', value: '2/3' },
|
||||
{ name: '1/4', value: '1/4' },
|
||||
{ name: '3/4', value: '3/4' },
|
||||
]"
|
||||
:form="field"
|
||||
label="Width"
|
||||
/>
|
||||
<select-input
|
||||
v-if="['nf-text', 'nf-image'].includes(field.type)"
|
||||
name="align"
|
||||
class="flex-grow"
|
||||
:options="[
|
||||
{ name: 'Left', value: 'left' },
|
||||
{ name: 'Center', value: 'center' },
|
||||
{ name: 'Right', value: 'right' },
|
||||
{ name: 'Justify', value: 'justify' },
|
||||
]"
|
||||
:form="field"
|
||||
label="Alignment"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="field.type == 'nf-text'"
|
||||
class="border-b py-2 px-4"
|
||||
class="border-t py-2"
|
||||
>
|
||||
<rich-text-area-input
|
||||
class="mx-4"
|
||||
name="content"
|
||||
:form="field"
|
||||
label="Content"
|
||||
|
|
@ -62,43 +71,25 @@
|
|||
<text-input
|
||||
name="next_btn_text"
|
||||
:form="field"
|
||||
label="Text of next button"
|
||||
label="Next button label"
|
||||
:required="true"
|
||||
/>
|
||||
<text-input
|
||||
name="previous_btn_text"
|
||||
:form="field"
|
||||
label="Text of previous button"
|
||||
help="Shown on the next page"
|
||||
label="Previous button label"
|
||||
help="Displayed on the next page"
|
||||
:required="true"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-else-if="field.type == 'nf-divider'"
|
||||
class="border-b py-2 px-4"
|
||||
>
|
||||
<text-input
|
||||
name="name"
|
||||
:form="field"
|
||||
:required="true"
|
||||
label="Field Name"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-else-if="field.type == 'nf-image'"
|
||||
class="border-b py-2 px-4"
|
||||
class="border-t py-2"
|
||||
>
|
||||
<text-input
|
||||
name="name"
|
||||
:form="field"
|
||||
:required="true"
|
||||
label="Field Name"
|
||||
/>
|
||||
<image-input
|
||||
name="image_block"
|
||||
class="mt-3"
|
||||
class="mx-4"
|
||||
:form="field"
|
||||
label="Upload Image"
|
||||
:required="false"
|
||||
|
|
@ -107,95 +98,48 @@
|
|||
|
||||
<div
|
||||
v-else-if="field.type == 'nf-code'"
|
||||
class="border-b py-2 px-4"
|
||||
class="border-t"
|
||||
>
|
||||
<code-input
|
||||
name="content"
|
||||
class="mt-4 mx-4"
|
||||
:form="field"
|
||||
label="Content"
|
||||
help="You can add any html code, including iframes"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-else
|
||||
class="border-b py-2 px-4"
|
||||
>
|
||||
<p>No settings found.</p>
|
||||
</div>
|
||||
|
||||
<!-- Logic Block -->
|
||||
<form-block-logic-editor
|
||||
class="py-2 px-4 border-b"
|
||||
:form="form"
|
||||
:field="field"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import FormBlockLogicEditor from "../../components/form-logic-components/FormBlockLogicEditor.vue"
|
||||
<script setup>
|
||||
import HiddenRequiredDisabled from './HiddenRequiredDisabled.vue'
|
||||
|
||||
export default {
|
||||
name: "BlockOptions",
|
||||
components: { FormBlockLogicEditor },
|
||||
props: {
|
||||
field: {
|
||||
type: Object,
|
||||
required: false,
|
||||
},
|
||||
form: {
|
||||
type: Object,
|
||||
required: false,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
editorToolbarCustom: [["bold", "italic", "underline", "link"]],
|
||||
}
|
||||
const props = defineProps({
|
||||
field: {
|
||||
type: Object,
|
||||
required: false
|
||||
},
|
||||
form: {
|
||||
type: Object,
|
||||
required: false
|
||||
}
|
||||
})
|
||||
|
||||
computed: {},
|
||||
watch(() => props.field?.width, (val) => {
|
||||
if (val === undefined || val === null) {
|
||||
props.field.width = 'full'
|
||||
}
|
||||
}, { immediate: true })
|
||||
|
||||
watch: {
|
||||
"field.width": {
|
||||
handler(val) {
|
||||
if (val === undefined || val === null) {
|
||||
this.field.width = "full"
|
||||
}
|
||||
},
|
||||
immediate: true,
|
||||
},
|
||||
"field.align": {
|
||||
handler(val) {
|
||||
if (val === undefined || val === null) {
|
||||
this.field.align = "left"
|
||||
}
|
||||
},
|
||||
immediate: true,
|
||||
},
|
||||
},
|
||||
watch(() => props.field?.align, (val) => {
|
||||
if (val === undefined || val === null) {
|
||||
props.field.align = 'left'
|
||||
}
|
||||
}, { immediate: true })
|
||||
|
||||
created() {
|
||||
if (this.field?.width === undefined || this.field?.width === null) {
|
||||
this.field.width = "full"
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {},
|
||||
|
||||
methods: {
|
||||
onFieldHiddenChange(val) {
|
||||
this.field.hidden = val
|
||||
if (this.field.hidden) {
|
||||
this.field.required = false
|
||||
}
|
||||
},
|
||||
onFieldHelpPositionChange(val) {
|
||||
if (!val) {
|
||||
this.field.help_position = "below_input"
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
onMounted(() => {
|
||||
if (props.field?.width === undefined || props.field?.width === null) {
|
||||
props.field.width = 'full'
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -1,66 +1,46 @@
|
|||
<template>
|
||||
<dropdown
|
||||
<UPopover
|
||||
v-if="changeTypeOptions.length > 0"
|
||||
ref="newTypeDropdown"
|
||||
dusk="nav-dropdown"
|
||||
v-model:open="open"
|
||||
class="-mb-1"
|
||||
>
|
||||
<template #trigger="{ toggle }">
|
||||
<v-button
|
||||
class="relative"
|
||||
:class="btnClasses"
|
||||
size="small"
|
||||
color="light-gray"
|
||||
@click.stop="toggle"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="2"
|
||||
stroke="currentColor"
|
||||
class="h-4 w-4 text-blue-600 inline mr-1 -mt-1"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M7.5 21L3 16.5m0 0L7.5 12M3 16.5h13.5m0-13.5L21 7.5m0 0L16.5 12M21 7.5H7.5"
|
||||
/>
|
||||
</svg>
|
||||
<span class="whitespace-nowrap">Change Type</span>
|
||||
</v-button>
|
||||
</template>
|
||||
<div class="flex items-center gap-1.5 group">
|
||||
<Icon
|
||||
name="heroicons:arrows-right-left-20-solid"
|
||||
class="flex-shrink-0 w-5 h-5 text-gray-400 group-hover:text-gray-500"
|
||||
/>
|
||||
<span class="truncate">Change Type</span>
|
||||
</div>
|
||||
|
||||
<a
|
||||
v-for="(op, index) in changeTypeOptions"
|
||||
:key="index"
|
||||
href="#"
|
||||
class="block px-4 py-2 text-md text-gray-700 dark:text-white hover:bg-gray-100 hover:text-gray-900 dark:text-gray-100 dark:hover:text-white dark:hover:bg-gray-600 flex items-center"
|
||||
@click.prevent="changeType(op.value)"
|
||||
>
|
||||
{{ op.name }}
|
||||
</a>
|
||||
</dropdown>
|
||||
<template #panel>
|
||||
<a
|
||||
v-for="(op, index) in changeTypeOptions"
|
||||
:key="index"
|
||||
href="#"
|
||||
class="block px-4 py-2 text-md text-gray-700 dark:text-white hover:bg-gray-100 hover:text-gray-900 dark:text-gray-100 dark:hover:text-white dark:hover:bg-gray-600 flex items-center"
|
||||
@click.prevent="changeType(op.value)"
|
||||
>
|
||||
{{ op.name }}
|
||||
</a>
|
||||
</template>
|
||||
</UPopover>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Dropdown from "~/components/global/Dropdown.vue"
|
||||
|
||||
export default {
|
||||
name: "ChangeFieldType",
|
||||
components: { Dropdown },
|
||||
components: {},
|
||||
props: {
|
||||
field: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
btnClasses: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
emits: ['changeType'],
|
||||
data() {
|
||||
return {}
|
||||
return {
|
||||
open: false,
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
|
|
@ -114,7 +94,7 @@ export default {
|
|||
changeType(newType) {
|
||||
if (newType) {
|
||||
this.$emit("changeType", newType)
|
||||
this.$refs.newTypeDropdown.close()
|
||||
this.open = false
|
||||
}
|
||||
},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,47 +1,33 @@
|
|||
<template>
|
||||
<div
|
||||
v-if="field"
|
||||
class="py-2"
|
||||
class="pb-2"
|
||||
>
|
||||
<!-- General -->
|
||||
<div class="border-b px-4">
|
||||
<h3 class="font-semibold block text-lg">
|
||||
General
|
||||
</h3>
|
||||
<p class="text-gray-400 mb-2 text-xs">
|
||||
Exclude this field or make it required.
|
||||
</p>
|
||||
<toggle-switch-input
|
||||
<div class="px-4">
|
||||
<text-input
|
||||
name="name"
|
||||
class="mt-2"
|
||||
:form="field"
|
||||
name="required"
|
||||
label="Required"
|
||||
@update:model-value="onFieldRequiredChange"
|
||||
:required="true"
|
||||
wrapper-class="mb-2"
|
||||
label="Field Name"
|
||||
/>
|
||||
<toggle-switch-input
|
||||
:form="field"
|
||||
name="hidden"
|
||||
label="Hidden"
|
||||
@update:model-value="onFieldHiddenChange"
|
||||
/>
|
||||
<toggle-switch-input
|
||||
:form="field"
|
||||
name="disabled"
|
||||
label="Disabled"
|
||||
@update:model-value="onFieldDisabledChange"
|
||||
<HiddenRequiredDisabled
|
||||
class="mt-4"
|
||||
:field="field"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Checkbox -->
|
||||
<div
|
||||
v-if="field.type === 'checkbox'"
|
||||
class="border-b py-2 px-4"
|
||||
class="px-4"
|
||||
>
|
||||
<h3 class="font-semibold block text-lg">
|
||||
Checkbox
|
||||
</h3>
|
||||
<p class="text-gray-400 mb-3 text-xs">
|
||||
Advanced options for checkbox.
|
||||
</p>
|
||||
<EditorSectionHeader
|
||||
icon="i-heroicons-check-circle"
|
||||
title="Checkbox"
|
||||
/>
|
||||
<toggle-switch-input
|
||||
:form="field"
|
||||
name="use_toggle_switch"
|
||||
|
|
@ -53,11 +39,12 @@
|
|||
<!-- File Uploads -->
|
||||
<div
|
||||
v-if="field.type === 'files'"
|
||||
class="border-b py-2 px-4"
|
||||
class="px-4"
|
||||
>
|
||||
<h3 class="font-semibold block text-lg mb-3">
|
||||
File uploads
|
||||
</h3>
|
||||
<EditorSectionHeader
|
||||
icon="i-heroicons-paper-clip"
|
||||
title="File uploads"
|
||||
/>
|
||||
<toggle-switch-input
|
||||
:form="field"
|
||||
name="multiple"
|
||||
|
|
@ -92,14 +79,12 @@
|
|||
|
||||
<div
|
||||
v-if="field.type === 'rating'"
|
||||
class="border-b py-2 px-4"
|
||||
class="px-4"
|
||||
>
|
||||
<h3 class="font-semibold block text-lg">
|
||||
Rating
|
||||
</h3>
|
||||
<p class="text-gray-400 mb-3 text-xs">
|
||||
Advanced options for rating.
|
||||
</p>
|
||||
<EditorSectionHeader
|
||||
icon="i-heroicons-star"
|
||||
title="Rating"
|
||||
/>
|
||||
<text-input
|
||||
name="rating_max_value"
|
||||
native-type="number"
|
||||
|
|
@ -113,14 +98,12 @@
|
|||
|
||||
<div
|
||||
v-if="field.type === 'scale'"
|
||||
class="border-b py-2 px-4"
|
||||
class="px-4"
|
||||
>
|
||||
<h3 class="font-semibold block text-lg">
|
||||
Scale
|
||||
</h3>
|
||||
<p class="text-gray-400 mb-3 text-xs">
|
||||
Advanced options for scale.
|
||||
</p>
|
||||
<EditorSectionHeader
|
||||
icon="i-heroicons-scale-20-solid"
|
||||
title="Scale"
|
||||
/>
|
||||
<text-input
|
||||
name="scale_min_value"
|
||||
native-type="number"
|
||||
|
|
@ -151,14 +134,12 @@
|
|||
|
||||
<div
|
||||
v-if="field.type === 'slider'"
|
||||
class="border-b py-2 px-4"
|
||||
class="px-4"
|
||||
>
|
||||
<h3 class="font-semibold block text-lg">
|
||||
Slider
|
||||
</h3>
|
||||
<p class="text-gray-400 mb-3 text-xs">
|
||||
Advanced options for slider.
|
||||
</p>
|
||||
<EditorSectionHeader
|
||||
icon="i-heroicons-adjustments-horizontal"
|
||||
title="Slider"
|
||||
/>
|
||||
<text-input
|
||||
name="slider_min_value"
|
||||
native-type="number"
|
||||
|
|
@ -195,14 +176,12 @@
|
|||
<!-- Text Options -->
|
||||
<div
|
||||
v-if="field.type === 'text' && displayBasedOnAdvanced"
|
||||
class="border-b py-2 px-4"
|
||||
class="px-4"
|
||||
>
|
||||
<h3 class="font-semibold block text-lg">
|
||||
Text Options
|
||||
</h3>
|
||||
<p class="text-gray-400 mb-3 text-xs">
|
||||
Keep it simple or make it a multi-lines input.
|
||||
</p>
|
||||
<EditorSectionHeader
|
||||
icon="i-heroicons-bars-3-bottom-left"
|
||||
title="Text Options"
|
||||
/>
|
||||
<toggle-switch-input
|
||||
:form="field"
|
||||
name="multi_lines"
|
||||
|
|
@ -221,11 +200,12 @@
|
|||
<!-- Date Options -->
|
||||
<div
|
||||
v-if="field.type === 'date'"
|
||||
class="border-b py-2 px-4"
|
||||
class="px-4"
|
||||
>
|
||||
<h3 class="font-semibold block text-lg">
|
||||
Date Options
|
||||
</h3>
|
||||
<EditorSectionHeader
|
||||
icon="i-heroicons-calendar-20-solid"
|
||||
title="Date Options"
|
||||
/>
|
||||
<toggle-switch-input
|
||||
:form="field"
|
||||
class="mt-3"
|
||||
|
|
@ -286,14 +266,12 @@
|
|||
<!-- select/multiselect Options -->
|
||||
<div
|
||||
v-if="['select', 'multi_select'].includes(field.type)"
|
||||
class="border-b py-2 px-4"
|
||||
class="px-4"
|
||||
>
|
||||
<h3 class="font-semibold block text-lg">
|
||||
Select Options
|
||||
</h3>
|
||||
<p class="text-gray-400 mb-3 text-xs">
|
||||
Advanced options for your select/multiselect fields.
|
||||
</p>
|
||||
<EditorSectionHeader
|
||||
icon="i-heroicons-chevron-up-down-20-solid"
|
||||
title="Select Options"
|
||||
/>
|
||||
<text-area-input
|
||||
v-model="optionsText"
|
||||
:name="field.id + '_options_text'"
|
||||
|
|
@ -320,22 +298,11 @@
|
|||
<!-- Customization - Placeholder, Prefill, Relabel, Field Help -->
|
||||
<div
|
||||
v-if="displayBasedOnAdvanced"
|
||||
class="border-b py-2 px-4"
|
||||
class="px-4"
|
||||
>
|
||||
<h3 class="font-semibold block text-lg">
|
||||
Customization
|
||||
</h3>
|
||||
|
||||
<p class="text-gray-400 mb-3 text-xs">
|
||||
Change your form field name, pre-fill a value, add hints, etc.
|
||||
</p>
|
||||
|
||||
<text-input
|
||||
name="name"
|
||||
class="mt-3"
|
||||
:form="field"
|
||||
:required="true"
|
||||
label="Field Name"
|
||||
<EditorSectionHeader
|
||||
icon="i-heroicons-adjustments-horizontal"
|
||||
title="Customization"
|
||||
/>
|
||||
|
||||
<toggle-switch-input
|
||||
|
|
@ -568,7 +535,7 @@
|
|||
help="Maximum character limit of 2000"
|
||||
:required="false"
|
||||
/>
|
||||
<checkbox-input
|
||||
<toggle-switch-input
|
||||
name="show_char_limit"
|
||||
:form="field"
|
||||
class="mt-3"
|
||||
|
|
@ -580,11 +547,13 @@
|
|||
<!-- Advanced Options -->
|
||||
<div
|
||||
v-if="field.type === 'text'"
|
||||
class="border-b py-2 px-4"
|
||||
class="px-4"
|
||||
>
|
||||
<h3 class="font-semibold block text-lg mb-3">
|
||||
Advanced Options
|
||||
</h3>
|
||||
<EditorSectionHeader
|
||||
icon="i-heroicons-bars-3-bottom-left"
|
||||
title="Advanced Options"
|
||||
/>
|
||||
|
||||
<toggle-switch-input
|
||||
:form="field"
|
||||
name="generates_uuid"
|
||||
|
|
@ -600,19 +569,6 @@
|
|||
@update:model-value="onFieldGenAutoIdChange"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Logic Block -->
|
||||
<form-block-logic-editor
|
||||
class="py-2 px-4 border-b"
|
||||
:form="form"
|
||||
:field="field"
|
||||
/>
|
||||
|
||||
<custom-field-validation
|
||||
class="py-2 px-4 border-b pb-16"
|
||||
:form="form"
|
||||
:field="field"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
|
@ -620,15 +576,15 @@
|
|||
import timezones from '~/data/timezones.json'
|
||||
import countryCodes from '~/data/country_codes.json'
|
||||
import CountryFlag from 'vue-country-flag-next'
|
||||
import FormBlockLogicEditor from '../../components/form-logic-components/FormBlockLogicEditor.vue'
|
||||
import CustomFieldValidation from '../../components/CustomFieldValidation.vue'
|
||||
import MatrixFieldOptions from './MatrixFieldOptions.vue'
|
||||
import HiddenRequiredDisabled from './HiddenRequiredDisabled.vue'
|
||||
import EditorSectionHeader from '~/components/open/forms/components/form-components/EditorSectionHeader.vue'
|
||||
import { format } from 'date-fns'
|
||||
import { default as _has } from 'lodash/has'
|
||||
|
||||
export default {
|
||||
name: 'FieldOptions',
|
||||
components: { CountryFlag, FormBlockLogicEditor, CustomFieldValidation, MatrixFieldOptions },
|
||||
components: { CountryFlag, MatrixFieldOptions, HiddenRequiredDisabled, EditorSectionHeader },
|
||||
props: {
|
||||
field: {
|
||||
type: Object,
|
||||
|
|
@ -738,28 +694,6 @@ export default {
|
|||
},
|
||||
|
||||
methods: {
|
||||
onFieldDisabledChange(val) {
|
||||
this.field.disabled = val
|
||||
if (this.field.disabled) {
|
||||
this.field.hidden = false
|
||||
}
|
||||
},
|
||||
onFieldRequiredChange(val) {
|
||||
this.field.required = val
|
||||
if (this.field.required) {
|
||||
this.field.hidden = false
|
||||
}
|
||||
},
|
||||
onFieldHiddenChange(val) {
|
||||
this.field.hidden = val
|
||||
if (this.field.hidden) {
|
||||
this.field.required = false
|
||||
this.field.disabled = false
|
||||
} else {
|
||||
this.field.generates_uuid = false
|
||||
this.field.generates_auto_increment_id = false
|
||||
}
|
||||
},
|
||||
onFieldDateRangeChange(val) {
|
||||
this.field.date_range = val
|
||||
if (this.field.date_range) {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,116 @@
|
|||
<template>
|
||||
<div class="grid grid-cols-3 gap-2">
|
||||
<button
|
||||
v-for="option in availableOptions"
|
||||
:key="option.name"
|
||||
class="flex flex-col items-center justify-center p-1.5 border rounded-lg transition-colors text-gray-500"
|
||||
:class="[
|
||||
option.class ? (typeof option.class === 'function' ? option.class(isSelected(option.name)) : option.class) : {},
|
||||
{
|
||||
'border-blue-500': isSelected(option.name),
|
||||
'hover:bg-gray-100 border-gray-300': !isSelected(option.name)
|
||||
}
|
||||
]"
|
||||
@click="toggleOption(option.name)"
|
||||
>
|
||||
<Icon
|
||||
:name="isSelected(option.name) && option.selectedIcon ? option.selectedIcon : option.icon"
|
||||
:class="[
|
||||
'w-4 h-4 mb-1',
|
||||
{
|
||||
'text-blue-500': isSelected(option.name),
|
||||
'text-inherit': !isSelected(option.name),
|
||||
},
|
||||
option.iconClass ? (typeof option.iconClass === 'function' ? option.iconClass(isSelected(option.name)) : option.iconClass) : {}
|
||||
]"
|
||||
/>
|
||||
<span
|
||||
class="text-xs"
|
||||
:class="{
|
||||
'text-blue-500': isSelected(option.name),
|
||||
'text-inherit': !isSelected(option.name),
|
||||
}"
|
||||
>{{ isSelected(option.name) ? option.selectedLabel ?? option.label : option.label }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
|
||||
const props = defineProps({
|
||||
field: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
canBeDisabled: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
canBeRequired: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
canBeHidden: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:field'])
|
||||
|
||||
const options = ref([
|
||||
{
|
||||
name: 'required',
|
||||
label: 'Required',
|
||||
icon: 'ph:asterisk-bold',
|
||||
selectedIcon: 'ph:asterisk-bold',
|
||||
iconClass: (isActive) => isActive ? 'text-red-500' : '',
|
||||
},
|
||||
{
|
||||
name: 'hidden',
|
||||
label: 'Hidden',
|
||||
icon: 'heroicons:eye',
|
||||
selectedIcon: 'heroicons:eye-slash-solid',
|
||||
},
|
||||
{
|
||||
name: 'disabled',
|
||||
label: 'Disabled',
|
||||
icon: 'heroicons:lock-open',
|
||||
selectedIcon: 'heroicons:lock-closed-solid',
|
||||
}
|
||||
])
|
||||
|
||||
const availableOptions = computed(() => {
|
||||
return options.value.filter(option => {
|
||||
if (option.name === 'disabled') return props.canBeDisabled
|
||||
if (option.name === 'required') return props.canBeRequired
|
||||
if (option.name === 'hidden') return props.canBeHidden
|
||||
return true
|
||||
})
|
||||
})
|
||||
|
||||
const isSelected = (optionName) => {
|
||||
return props.field[optionName]
|
||||
}
|
||||
|
||||
const toggleOption = (optionName) => {
|
||||
const newValue = !props.field[optionName]
|
||||
|
||||
if (optionName === 'required' && newValue) {
|
||||
props.field.hidden = false
|
||||
} else if (optionName === 'hidden' && newValue) {
|
||||
props.field.required = false
|
||||
props.field.disabled = false
|
||||
props.field.generates_uuid = false
|
||||
props.field.generates_auto_increment_id = false
|
||||
} else if (optionName === 'disabled' && newValue) {
|
||||
props.field.hidden = false
|
||||
}
|
||||
|
||||
if ((optionName === 'disabled' && props.canBeDisabled) ||
|
||||
(optionName === 'required' && props.canBeRequired) ||
|
||||
(optionName === 'hidden' && props.canBeHidden)) {
|
||||
props.field[optionName] = newValue
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
@ -1,14 +1,12 @@
|
|||
<template>
|
||||
<div
|
||||
v-if="localField.type === 'matrix'"
|
||||
class="border-b py-2 px-4"
|
||||
class="px-4"
|
||||
>
|
||||
<h3 class="font-semibold block text-lg">
|
||||
Matrix
|
||||
</h3>
|
||||
<p class="text-gray-400 mb-3 text-xs">
|
||||
Advanced options for matrix.
|
||||
</p>
|
||||
<EditorSectionHeader
|
||||
icon="i-heroicons-table-cells-20-solid"
|
||||
title="Matrix"
|
||||
/>
|
||||
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div class="">
|
||||
|
|
@ -72,6 +70,7 @@
|
|||
|
||||
<script setup>
|
||||
import { ref, watch, computed } from 'vue'
|
||||
import EditorSectionHeader from '~/components/open/forms/components/form-components/EditorSectionHeader.vue'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@
|
|||
</template>
|
||||
<div
|
||||
v-if="state == 'default'"
|
||||
class="grid grid-cols-1 sm:grid-cols-2 gap-4 mt-8"
|
||||
class="grid grid-cols-1 sm:grid-cols-2 gap-4"
|
||||
>
|
||||
<div
|
||||
v-track.select_form_base="{ base: 'contact-form' }"
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
<VTransition>
|
||||
<section
|
||||
v-if="hasCleanings && !hideWarning"
|
||||
class="flex gap-3 p-4 bg-blue-50 rounded-lg border border-blue-300 border-solid max-md:flex-wrap mb-2"
|
||||
class="flex gap-3 p-4 bg-blue-50 dark:bg-blue-950 rounded-lg border border-blue-300 border-solid max-md:flex-wrap mb-2"
|
||||
aria-labelledby="notification-title"
|
||||
>
|
||||
<div class="flex justify-center items-center self-start py-px">
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
import clonedeep from 'clone-deep'
|
||||
import { generateUUID } from "~/lib/utils.js"
|
||||
export const DEFAULT_COLOR = '#3B82F6'
|
||||
|
||||
export const initForm = (defaultValue = {}, withDefaultProperties = false) => {
|
||||
return useForm({
|
||||
title: "My Form",
|
||||
description: null,
|
||||
visibility: "public",
|
||||
workspace_id: null,
|
||||
properties: withDefaultProperties ? getDefaultProperties() : [],
|
||||
|
|
@ -74,3 +74,53 @@ function getDefaultProperties() {
|
|||
},
|
||||
]
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets default values for form properties if they are not already defined.
|
||||
* This function ensures that all necessary form fields have a valid initial value,
|
||||
* which helps maintain consistency and prevents errors due to undefined properties.
|
||||
*
|
||||
* @param {Object} formData - The initial form data object
|
||||
* @returns {Object} A new object with default values applied where necessary
|
||||
*/
|
||||
export function setFormDefaults(formData) {
|
||||
const defaultValues = {
|
||||
title: 'Untitled Form',
|
||||
visibility: 'public',
|
||||
theme: 'default',
|
||||
width: 'centered',
|
||||
size: 'md',
|
||||
border_radius: 'small',
|
||||
dark_mode: 'light',
|
||||
color: '#3B82F6',
|
||||
hide_title: false,
|
||||
uppercase_labels: false,
|
||||
no_branding: false,
|
||||
transparent_background: false,
|
||||
submit_button_text: 'Submit',
|
||||
confetti_on_submission: false,
|
||||
show_progress_bar: false,
|
||||
bypass_success_page: false,
|
||||
can_be_indexed: true,
|
||||
use_captcha: false,
|
||||
properties: [],
|
||||
}
|
||||
|
||||
const filledFormData = clonedeep(formData)
|
||||
|
||||
for (const [key, value] of Object.entries(defaultValues)) {
|
||||
if (filledFormData[key] === undefined || filledFormData[key] === null || (typeof value === 'string' && filledFormData[key] === '')) {
|
||||
filledFormData[key] = value
|
||||
}
|
||||
}
|
||||
|
||||
// Handle required nested properties
|
||||
if (filledFormData.properties && Array.isArray(filledFormData.properties)) {
|
||||
filledFormData.properties = filledFormData.properties.map(property => ({
|
||||
...property,
|
||||
name: property.name === '' || property.name === null || property.name === undefined ? 'Untitled' : property.name,
|
||||
}))
|
||||
}
|
||||
|
||||
return filledFormData
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,162 @@
|
|||
{
|
||||
"text": {
|
||||
"name": "text",
|
||||
"title": "Text Input",
|
||||
"icon": "i-heroicons-bars-3-bottom-left",
|
||||
"default_block_name": "Your name",
|
||||
"bg_class": "bg-blue-100",
|
||||
"text_class": "text-blue-900"
|
||||
},
|
||||
"date": {
|
||||
"name": "date",
|
||||
"title": "Date Input",
|
||||
"icon": "i-heroicons-calendar-20-solid",
|
||||
"default_block_name": "Date",
|
||||
"bg_class": "bg-green-100",
|
||||
"text_class": "text-green-900"
|
||||
},
|
||||
"url": {
|
||||
"name": "url",
|
||||
"title": "URL Input",
|
||||
"icon": "i-heroicons-link-20-solid",
|
||||
"default_block_name": "Link",
|
||||
"bg_class": "bg-blue-100",
|
||||
"text_class": "text-blue-900"
|
||||
},
|
||||
"phone_number": {
|
||||
"name": "phone_number",
|
||||
"title": "Phone Input",
|
||||
"icon": "i-heroicons-phone-20-solid",
|
||||
"default_block_name": "Phone Number",
|
||||
"bg_class": "bg-blue-100",
|
||||
"text_class": "text-blue-900"
|
||||
},
|
||||
"email": {
|
||||
"name": "email",
|
||||
"title": "Email Input",
|
||||
"icon": "i-heroicons-at-symbol-20-solid",
|
||||
"default_block_name": "Email",
|
||||
"bg_class": "bg-blue-100",
|
||||
"text_class": "text-blue-900"
|
||||
},
|
||||
"checkbox": {
|
||||
"name": "checkbox",
|
||||
"title": "Checkbox Input",
|
||||
"icon": "i-heroicons-check-circle",
|
||||
"default_block_name": "Checkbox",
|
||||
"bg_class": "bg-red-100",
|
||||
"text_class": "text-red-900"
|
||||
},
|
||||
"select": {
|
||||
"name": "select",
|
||||
"title": "Select Input",
|
||||
"icon": "i-heroicons-chevron-up-down-20-solid",
|
||||
"default_block_name": "Select",
|
||||
"bg_class": "bg-red-100",
|
||||
"text_class": "text-red-900"
|
||||
},
|
||||
"multi_select": {
|
||||
"name": "multi_select",
|
||||
"title": "Multi-select Input",
|
||||
"icon": "i-heroicons-chevron-up-down-20-solid",
|
||||
"default_block_name": "Multi Select",
|
||||
"bg_class": "bg-red-100",
|
||||
"text_class": "text-red-900"
|
||||
},
|
||||
"matrix": {
|
||||
"name": "matrix",
|
||||
"title": "Matrix Input",
|
||||
"icon": "i-heroicons-table-cells-20-solid",
|
||||
"default_block_name": "Matrix",
|
||||
"bg_class": "bg-red-100",
|
||||
"text_class": "text-red-900"
|
||||
},
|
||||
"number": {
|
||||
"name": "number",
|
||||
"title": "Number Input",
|
||||
"icon": "i-heroicons-hashtag-20-solid",
|
||||
"default_block_name": "Number",
|
||||
"bg_class": "bg-purple-100",
|
||||
"text_class": "text-purple-900"
|
||||
},
|
||||
"rating": {
|
||||
"name": "rating",
|
||||
"title": "Rating Input",
|
||||
"icon": "i-heroicons-star",
|
||||
"default_block_name": "Rating",
|
||||
"bg_class": "bg-purple-100",
|
||||
"text_class": "text-purple-900"
|
||||
},
|
||||
"scale": {
|
||||
"name": "scale",
|
||||
"title": "Scale Input",
|
||||
"icon": "i-heroicons-scale-20-solid",
|
||||
"default_block_name": "Scale",
|
||||
"bg_class": "bg-purple-100",
|
||||
"text_class": "text-purple-900"
|
||||
},
|
||||
"slider": {
|
||||
"name": "slider",
|
||||
"title": "Slider Input",
|
||||
"icon": "i-heroicons-adjustments-horizontal",
|
||||
"default_block_name": "Slider",
|
||||
"bg_class": "bg-purple-100",
|
||||
"text_class": "text-purple-900"
|
||||
},
|
||||
"files": {
|
||||
"name": "files",
|
||||
"title": "File Input",
|
||||
"icon": "i-heroicons-paper-clip",
|
||||
"default_block_name": "Files",
|
||||
"bg_class": "bg-pink-100",
|
||||
"text_class": "text-pink-900"
|
||||
},
|
||||
"signature": {
|
||||
"name": "signature",
|
||||
"title": "Signature Input",
|
||||
"icon": "i-heroicons-pencil-square-20-solid",
|
||||
"default_block_name": "Signature",
|
||||
"bg_class": "bg-pink-100",
|
||||
"text_class": "text-pink-900"
|
||||
},
|
||||
"nf-text": {
|
||||
"name": "nf-text",
|
||||
"title": "Text",
|
||||
"icon": "i-heroicons-bars-3",
|
||||
"default_block_name": "Text",
|
||||
"bg_class": "bg-yellow-100",
|
||||
"text_class": "text-yellow-900"
|
||||
},
|
||||
"nf-page-break": {
|
||||
"name": "nf-page-break",
|
||||
"title": "Page-break",
|
||||
"icon": "i-heroicons-document-plus",
|
||||
"default_block_name": "Page Break",
|
||||
"bg_class": "bg-gray-100",
|
||||
"text_class": "text-gray-900"
|
||||
},
|
||||
"nf-divider": {
|
||||
"name": "nf-divider",
|
||||
"title": "Divider",
|
||||
"icon": "i-heroicons-minus",
|
||||
"default_block_name": "Divider",
|
||||
"bg_class": "bg-gray-100",
|
||||
"text_class": "text-gray-900"
|
||||
},
|
||||
"nf-image": {
|
||||
"name": "nf-image",
|
||||
"title": "Image",
|
||||
"icon": "i-heroicons-photo",
|
||||
"default_block_name": "Image",
|
||||
"bg_class": "bg-yellow-100",
|
||||
"text_class": "text-yellow-900"
|
||||
},
|
||||
"nf-code": {
|
||||
"name": "nf-code",
|
||||
"title": "Code",
|
||||
"icon": "i-heroicons-code-bracket",
|
||||
"default_block_name": "Code Block",
|
||||
"bg_class": "bg-yellow-100",
|
||||
"text_class": "text-yellow-900"
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +1,6 @@
|
|||
export default defineNuxtRouteMiddleware((from, to, next) => {
|
||||
const route = useRoute()
|
||||
export default defineNuxtRouteMiddleware((from, to) => {
|
||||
if (useFeatureFlag('self_hosted')) {
|
||||
if (from.name === 'register' && route.query?.email && route.query?.invite_token) {
|
||||
if (from.name === 'register' && to.query?.email && to.query?.invite_token) {
|
||||
return
|
||||
}
|
||||
if (from.name === 'ai-form-builder' && useFeatureFlag('ai_features')) {
|
||||
|
|
|
|||
|
|
@ -40,6 +40,11 @@ export default defineNuxtConfig({
|
|||
path: '~/components/global',
|
||||
pathPrefix: false,
|
||||
},
|
||||
{
|
||||
path: '~/components/forms',
|
||||
pathPrefix: false,
|
||||
global: true
|
||||
},
|
||||
{
|
||||
path: '~/components/pages',
|
||||
pathPrefix: false,
|
||||
|
|
@ -76,7 +81,7 @@ export default defineNuxtConfig({
|
|||
classPrefix: '',
|
||||
},
|
||||
ui: {
|
||||
icons: ['heroicons', 'material-symbols'],
|
||||
icons: ['heroicons', 'material-symbols']
|
||||
},
|
||||
sitemap,
|
||||
runtimeConfig,
|
||||
|
|
|
|||
|
|
@ -219,10 +219,10 @@ useOpnSeoMeta({
|
|||
return form.value ? form.value.title : 'Create beautiful forms'
|
||||
},
|
||||
description: () => {
|
||||
if (pageMeta.value.description) {
|
||||
return pageMeta.value.description
|
||||
if (pageMeta.value.page_description) {
|
||||
return pageMeta.value.page_description
|
||||
}
|
||||
return (form.value && form.value?.description) ? form.value?.description.substring(0, 160) : null
|
||||
return null
|
||||
},
|
||||
ogImage: () => {
|
||||
if (pageMeta.value.page_thumbnail) {
|
||||
|
|
|
|||
|
|
@ -1,26 +1,25 @@
|
|||
<template>
|
||||
<div class="flex flex-wrap flex-col">
|
||||
<div key="2">
|
||||
<create-form-base-modal
|
||||
:show="showInitialFormModal"
|
||||
@form-generated="formGenerated"
|
||||
@close="showInitialFormModal = false"
|
||||
/>
|
||||
<form-editor
|
||||
v-if="!workspacesLoading"
|
||||
ref="editor"
|
||||
class="w-full flex flex-grow"
|
||||
:error="error"
|
||||
:is-guest="isGuest"
|
||||
@open-register="registerModal = true"
|
||||
/>
|
||||
<div
|
||||
v-else
|
||||
class="text-center mt-4 py-6"
|
||||
>
|
||||
<Loader class="h-6 w-6 text-nt-blue mx-auto" />
|
||||
</div>
|
||||
<div class="flex flex-wrap flex-col flex-grow">
|
||||
<create-form-base-modal
|
||||
:show="showInitialFormModal"
|
||||
@form-generated="formGenerated"
|
||||
@close="showInitialFormModal = false"
|
||||
/>
|
||||
<form-editor
|
||||
v-if="!workspacesLoading"
|
||||
ref="editor"
|
||||
class="w-full flex flex-grow"
|
||||
:error="error"
|
||||
:is-guest="isGuest"
|
||||
@open-register="registerModal = true"
|
||||
/>
|
||||
<div
|
||||
v-else
|
||||
class="text-center mt-4 py-6"
|
||||
>
|
||||
<Loader class="h-6 w-6 text-nt-blue mx-auto" />
|
||||
</div>
|
||||
|
||||
|
||||
<quick-register
|
||||
:show-register-modal="registerModal"
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
<template>
|
||||
<div class="flex flex-wrap flex-col flex-grow">
|
||||
<div key="2"
|
||||
<div
|
||||
key="2"
|
||||
class="w-full flex flex-grow flex-col"
|
||||
>
|
||||
<create-form-base-modal
|
||||
|
|
|
|||
|
|
@ -1,33 +1,14 @@
|
|||
import { defineStore } from "pinia"
|
||||
import clonedeep from "clone-deep"
|
||||
import { generateUUID } from "~/lib/utils.js"
|
||||
|
||||
const defaultBlockNames = {
|
||||
text: "Your name",
|
||||
date: "Date",
|
||||
url: "Link",
|
||||
phone_number: "Phone Number",
|
||||
number: "Number",
|
||||
rating: "Rating",
|
||||
scale: "Scale",
|
||||
slider: "Slider",
|
||||
email: "Email",
|
||||
checkbox: "Checkbox",
|
||||
select: "Select",
|
||||
multi_select: "Multi Select",
|
||||
files: "Files",
|
||||
signature: "Signature",
|
||||
matrix: "Matrix",
|
||||
"nf-text": "Text Block",
|
||||
"nf-page-break": "Page Break",
|
||||
"nf-divider": "Divider",
|
||||
"nf-image": "Image",
|
||||
"nf-code": "Code Block",
|
||||
}
|
||||
import blocksTypes from "~/data/blocks_types.json"
|
||||
import { useAlert } from '~/composables/useAlert'
|
||||
|
||||
export const useWorkingFormStore = defineStore("working_form", {
|
||||
state: () => ({
|
||||
content: null,
|
||||
activeTab: 0,
|
||||
formPageIndex: 0,
|
||||
|
||||
// Field being edited
|
||||
selectedFieldIndex: null,
|
||||
|
|
@ -43,14 +24,19 @@ export const useWorkingFormStore = defineStore("working_form", {
|
|||
setProperties(properties) {
|
||||
this.content.properties = [...properties]
|
||||
},
|
||||
openSettingsForField(index) {
|
||||
// If field is passed, compute index
|
||||
if (typeof index === "object") {
|
||||
index = this.content.properties.findIndex(
|
||||
(prop) => prop.id === index.id,
|
||||
objectToIndex(field) {
|
||||
if (typeof field === 'object') {
|
||||
return this.content.properties.findIndex(
|
||||
prop => prop.id === field.id
|
||||
)
|
||||
}
|
||||
this.selectedFieldIndex = index
|
||||
return field
|
||||
},
|
||||
setEditingField(field) {
|
||||
this.selectedFieldIndex = this.objectToIndex(field)
|
||||
},
|
||||
openSettingsForField(field) {
|
||||
this.setEditingField(field)
|
||||
this.showEditFieldSidebar = true
|
||||
this.showAddFieldSidebar = false
|
||||
},
|
||||
|
|
@ -59,14 +45,10 @@ export const useWorkingFormStore = defineStore("working_form", {
|
|||
this.showEditFieldSidebar = false
|
||||
this.showAddFieldSidebar = false
|
||||
},
|
||||
openAddFieldSidebar(index) {
|
||||
// If field is passed, compute index
|
||||
if (index !== null && typeof index === "object") {
|
||||
index = this.content.properties.findIndex(
|
||||
(prop) => prop.id === index.id,
|
||||
)
|
||||
openAddFieldSidebar(field) {
|
||||
if (field !== null) {
|
||||
this.setEditingField(field)
|
||||
}
|
||||
this.selectedFieldIndex = index
|
||||
this.showAddFieldSidebar = true
|
||||
this.showEditFieldSidebar = false
|
||||
},
|
||||
|
|
@ -107,7 +89,7 @@ export const useWorkingFormStore = defineStore("working_form", {
|
|||
|
||||
addBlock(type, index = null) {
|
||||
this.blockForm.type = type
|
||||
this.blockForm.name = defaultBlockNames[type]
|
||||
this.blockForm.name = blocksTypes[type].default_block_name
|
||||
const newBlock = this.prefillDefault(this.blockForm.data())
|
||||
newBlock.id = generateUUID()
|
||||
newBlock.hidden = false
|
||||
|
|
@ -146,6 +128,26 @@ export const useWorkingFormStore = defineStore("working_form", {
|
|||
this.openSettingsForField(fieldIndex)
|
||||
}
|
||||
},
|
||||
removeField(field) {
|
||||
this.internalRemoveField(field)
|
||||
},
|
||||
internalRemoveField(field) {
|
||||
const index = this.objectToIndex(field)
|
||||
|
||||
if (index !== -1) {
|
||||
useAlert().success('Ctrl + Z to undo',10000,{
|
||||
title: 'Field removed',
|
||||
actions: [{
|
||||
label: 'Undo',
|
||||
icon:"i-material-symbols-undo",
|
||||
click: () => {
|
||||
this.undo()
|
||||
}
|
||||
}]
|
||||
})
|
||||
this.content.properties.splice(index, 1)
|
||||
}
|
||||
},
|
||||
|
||||
moveField(oldIndex, newIndex) {
|
||||
const newFields = clonedeep(this.content.properties)
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ module.exports = {
|
|||
"./error.vue",
|
||||
"./lib/forms/themes/form-themes.js",
|
||||
"./lib/forms/themes/ThemeBuilder.js",
|
||||
'./data/**/*.json'
|
||||
],
|
||||
safelist: [
|
||||
{
|
||||
|
|
|
|||
Loading…
Reference in New Issue