Separate input type for Rating,Scale,Slider (#351)

* Separate input type for Rating,Scale,Slider

* rating, scale, slider add in test cases

* Allow field type change for new types

* Added options to db factory

* Fix linting

---------

Co-authored-by: Julien Nahum <julien@nahum.net>
This commit is contained in:
formsdev 2024-03-19 19:57:21 +05:30 committed by GitHub
parent c73fcd226b
commit c8628ed840
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
20 changed files with 1104 additions and 612 deletions

View File

@ -108,7 +108,7 @@ class GenerateTemplate extends Command
"color": "#64748b" "color": "#64748b"
} }
``` ```
The form properties can only have one of the following types: 'text', 'number', 'select', 'multi_select', 'date', 'files', 'checkbox', 'url', 'email', 'phone_number', 'signature'. The form properties can only have one of the following types: 'text', 'number', 'rating', 'scale','slider', 'select', 'multi_select', 'date', 'files', 'checkbox', 'url', 'email', 'phone_number', 'signature'.
All form properties objects need to have the keys 'help', 'name', 'type', 'hidden', 'placeholder', 'prefill'. All form properties objects need to have the keys 'help', 'name', 'type', 'hidden', 'placeholder', 'prefill'.
The placeholder property is optional (can be "null") and is used to display a placeholder text in the input field. The placeholder property is optional (can be "null") and is used to display a placeholder text in the input field.
The help property is optional (can be "null") and is used to display extra information about the field. The help property is optional (can be "null") and is used to display extra information about the field.
@ -125,14 +125,9 @@ class GenerateTemplate extends Command
} }
``` ```
For numerical rating inputs, use a "number" type input and set the property "is_rating" to "true" to turn it into a star rating input. Ex: For "rating" you can set the field property "rating_max_value" to set the maximum value of the rating.
```json For "scale" you can set the field property "scale_min_value", "scale_max_value" and "scale_step_value" to set the minimum, maximum and step value of the scale.
{ For "slider" you can set the field property "slider_min_value", "slider_max_value" and "slider_step_value" to set the minimum, maximum and step value of the slider.
"name":"How would you rate your overall experience?",
"type":"number",
"is_rating": true
}
```
If the form is too long, you can paginate it by adding a page break block in the list of properties: If the form is too long, you can paginate it by adding a page break block in the list of properties:
```json ```json
@ -230,6 +225,7 @@ class GenerateTemplate extends Command
['role' => 'user', 'content' => Str::of(self::FORM_STRUCTURE_PROMPT)->replace('[REPLACE]', $this->argument('prompt'))->toString()], ['role' => 'user', 'content' => Str::of(self::FORM_STRUCTURE_PROMPT)->replace('[REPLACE]', $this->argument('prompt'))->toString()],
]); ]);
$formData = $completer->getArray(); $formData = $completer->getArray();
$formData = self::cleanAiOutput($formData);
$completer->doesNotExpectJson(); $completer->doesNotExpectJson();
$formDescriptionPrompt = Str::of(self::FORM_DESCRIPTION_PROMPT)->replace('[REPLACE]', $this->argument('prompt'))->toString(); $formDescriptionPrompt = Str::of(self::FORM_DESCRIPTION_PROMPT)->replace('[REPLACE]', $this->argument('prompt'))->toString();
@ -284,7 +280,7 @@ class GenerateTemplate extends Command
$types, $types,
$relatedTemplates $relatedTemplates
); );
$this->info('/form-templates/'.$template->slug); $this->info('/form-templates/' . $template->slug);
// Set reverse related Templates // Set reverse related Templates
$this->setReverseRelatedTemplates($template); $this->setReverseRelatedTemplates($template);
@ -297,7 +293,7 @@ class GenerateTemplate extends Command
*/ */
private function getImageCoverUrl($searchQuery): ?string private function getImageCoverUrl($searchQuery): ?string
{ {
$url = 'https://api.unsplash.com/search/photos?query='.urlencode($searchQuery).'&client_id='.config('services.unsplash.access_key'); $url = 'https://api.unsplash.com/search/photos?query=' . urlencode($searchQuery) . '&client_id=' . config('services.unsplash.access_key');
$response = Http::get($url)->json(); $response = Http::get($url)->json();
$photoIndex = rand(0, max(count($response['results']) - 1, 10)); $photoIndex = rand(0, max(count($response['results']) - 1, 10));
if (isset($response['results'][$photoIndex]['urls']['regular'])) { if (isset($response['results'][$photoIndex]['urls']['regular'])) {
@ -333,7 +329,6 @@ class GenerateTemplate extends Command
private function getRelatedTemplates(array $industries, array $types): array private function getRelatedTemplates(array $industries, array $types): array
{ {
ray($industries, $types);
$templateScore = []; $templateScore = [];
Template::chunk(100, function ($otherTemplates) use ($industries, $types, &$templateScore) { Template::chunk(100, function ($otherTemplates) use ($industries, $types, &$templateScore) {
foreach ($otherTemplates as $otherTemplate) { foreach ($otherTemplates as $otherTemplate) {
@ -361,24 +356,6 @@ class GenerateTemplate extends Command
array $types, array $types,
array $relatedTemplates array $relatedTemplates
) { ) {
// Add property uuids, improve form with options
foreach ($formData['properties'] as &$property) {
$property['id'] = Str::uuid()->toString(); // Column ID
// Fix ratings
if ($property['type'] == 'number' && ($property['is_rating'] ?? false)) {
$property['rating_max_value'] = 5;
}
if (($property['type'] == 'select' && count($property['select']['options']) <= 4)
|| ($property['type'] == 'multi_select' && count($property['multi_select']['options']) <= 4)) {
$property['without_dropdown'] = true;
}
}
// Clean data
$formTitle = Str::of($formTitle)->replace('"', '')->toString();
return Template::create([ return Template::create([
'name' => $formTitle, 'name' => $formTitle,
'description' => $formDescription, 'description' => $formDescription,
@ -395,7 +372,7 @@ class GenerateTemplate extends Command
private function setReverseRelatedTemplates(Template $newTemplate) private function setReverseRelatedTemplates(Template $newTemplate)
{ {
if (! $newTemplate || count($newTemplate->related_templates) === 0) { if (!$newTemplate || count($newTemplate->related_templates) === 0) {
return; return;
} }
@ -406,4 +383,36 @@ class GenerateTemplate extends Command
} }
} }
} }
public static function cleanAiOutput(array $formData): array
{
// Add property uuids, improve form with options
foreach ($formData['properties'] as &$property) {
$property['id'] = Str::uuid()->toString(); // Column ID
// Fix types
if ($property['type'] == 'rating') {
$property['rating_max_value'] = $property['rating_max_value'] ?? 5;
} elseif ($property['type'] == 'scale') {
$property['scale_min_value'] = $property['scale_min_value'] ?? 1;
$property['scale_max_value'] = $property['scale_max_value'] ?? 5;
$property['scale_step_value'] = $property['scale_step_value'] ?? 1;
} elseif ($property['type'] == 'slider') {
$property['slider_min_value'] = $property['slider_min_value'] ?? 0;
$property['slider_max_value'] = $property['slider_max_value'] ?? 100;
$property['slider_step_value'] = $property['slider_step_value'] ?? 1;
}
if (($property['type'] == 'select' && count($property['select']['options']) <= 4)
|| ($property['type'] == 'multi_select' && count($property['multi_select']['options']) <= 4)
) {
$property['without_dropdown'] = true;
}
}
// Clean data
$formData['title'] = Str::of($formData['title'])->replace('"', '')->toString();
return $formData;
}
} }

View File

@ -0,0 +1,69 @@
<?php
namespace App\Console\Commands;
use App\Models\Forms\Form;
use Illuminate\Console\Command;
class InputNumberMigration extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'forms:input-number-migration';
/**
* The console command description.
*
* @var string
*/
protected $description = 'One Time Only -- Separate input type for number (rating, scale, slider)';
/**
* Execute the console command.
*
* @return int
*/
public function handle()
{
Form::chunk(
100,
function ($forms) {
foreach ($forms as $form) {
$this->line('Process For Form: ' . $form->id . ' - ' . $form->slug);
$form->properties = collect($form->properties)->map(function ($property) {
if ($property['type'] === 'number') {
// Rating
if (isset($property['is_rating']) && $property['is_rating']) {
$this->line('Rating Field');
$property['type'] = 'rating';
unset($property['is_rating']);
}
// Scale
if (isset($property['is_scale']) && $property['is_scale']) {
$this->line('Scale Field');
$property['type'] = 'scale';
unset($property['is_scale']);
}
// Slider
if (isset($property['is_slider']) && $property['is_slider']) {
$this->line('Slider Field');
$property['type'] = 'slider';
unset($property['is_slider']);
}
}
return $property;
})->toArray();
$form->update();
}
}
);
$this->line('Migration Done');
}
}

View File

@ -40,7 +40,7 @@ class AnswerFormRequest extends FormRequest
*/ */
public function authorize() public function authorize()
{ {
return ! $this->form->is_closed && ! $this->form->max_number_of_submissions_reached && $this->form->visibility === 'public'; return !$this->form->is_closed && !$this->form->max_number_of_submissions_reached && $this->form->visibility === 'public';
} }
/** /**
@ -79,7 +79,7 @@ class AnswerFormRequest extends FormRequest
if ($property['type'] == 'checkbox') { if ($property['type'] == 'checkbox') {
// Required for checkboxes means true // Required for checkboxes means true
$rules[] = 'accepted'; $rules[] = 'accepted';
} elseif ($property['type'] == 'number' && isset($property['is_rating']) && $property['is_rating']) { } elseif ($property['type'] == 'rating') {
// For star rating, needs a minimum of 1 star // For star rating, needs a minimum of 1 star
$rules[] = 'min:1'; $rules[] = 'min:1';
} }
@ -91,7 +91,7 @@ class AnswerFormRequest extends FormRequest
$propertyId = $property['id']; $propertyId = $property['id'];
if (in_array($property['type'], ['multi_select'])) { if (in_array($property['type'], ['multi_select'])) {
$rules[] = 'array'; $rules[] = 'array';
$this->requestRules[$propertyId.'.*'] = $this->getPropertyRules($property); $this->requestRules[$propertyId . '.*'] = $this->getPropertyRules($property);
} else { } else {
$rules = array_merge($rules, $this->getPropertyRules($property)); $rules = array_merge($rules, $this->getPropertyRules($property));
} }
@ -137,12 +137,12 @@ class AnswerFormRequest extends FormRequest
$messages = []; $messages = [];
foreach ($this->form->properties as $property) { foreach ($this->form->properties as $property) {
if ($property['type'] == 'date' && isset($property['date_range']) && $property['date_range']) { if ($property['type'] == 'date' && isset($property['date_range']) && $property['date_range']) {
$messages[$property['id'].'.0.required_with'] = 'From date is required'; $messages[$property['id'] . '.0.required_with'] = 'From date is required';
$messages[$property['id'].'.1.required_with'] = 'To date is required'; $messages[$property['id'] . '.1.required_with'] = 'To date is required';
$messages[$property['id'].'.0.before_or_equal'] = 'From date must be before or equal To date'; $messages[$property['id'] . '.0.before_or_equal'] = 'From date must be before or equal To date';
} }
if ($property['type'] == 'number' && isset($property['is_rating']) && $property['is_rating']) { if ($property['type'] == 'rating') {
$messages[$property['id'].'.min'] = 'A rating must be selected'; $messages[$property['id'] . '.min'] = 'A rating must be selected';
} }
} }
@ -159,10 +159,9 @@ class AnswerFormRequest extends FormRequest
case 'signature': case 'signature':
return ['string']; return ['string'];
case 'number': case 'number':
if ($property['is_rating'] ?? false) { case 'rating':
return ['numeric']; case 'scale':
} case 'slider':
return ['numeric']; return ['numeric'];
case 'select': case 'select':
case 'multi_select': case 'multi_select':
@ -175,7 +174,7 @@ class AnswerFormRequest extends FormRequest
return ['boolean']; return ['boolean'];
case 'url': case 'url':
if (isset($property['file_upload']) && $property['file_upload']) { if (isset($property['file_upload']) && $property['file_upload']) {
$this->requestRules[$property['id'].'.*'] = [new StorageFile($this->maxFileSize, [], $this->form)]; $this->requestRules[$property['id'] . '.*'] = [new StorageFile($this->maxFileSize, [], $this->form)];
return ['array']; return ['array'];
} }
@ -183,7 +182,7 @@ class AnswerFormRequest extends FormRequest
return [new ValidUrl()]; return [new ValidUrl()];
case 'files': case 'files':
$allowedFileTypes = []; $allowedFileTypes = [];
if (! empty($property['allowed_file_types'])) { if (!empty($property['allowed_file_types'])) {
$allowedFileTypes = explode(',', $property['allowed_file_types']); $allowedFileTypes = explode(',', $property['allowed_file_types']);
} }
$this->requestRules[$property['id'] . '.*'] = [new StorageFile($this->getFieldMaxFileSize($property), $allowedFileTypes, $this->form)]; $this->requestRules[$property['id'] . '.*'] = [new StorageFile($this->getFieldMaxFileSize($property), $allowedFileTypes, $this->form)];
@ -193,9 +192,9 @@ class AnswerFormRequest extends FormRequest
return ['email:filter']; return ['email:filter'];
case 'date': case 'date':
if (isset($property['date_range']) && $property['date_range']) { if (isset($property['date_range']) && $property['date_range']) {
$this->requestRules[$property['id'].'.*'] = $this->getRulesForDate($property); $this->requestRules[$property['id'] . '.*'] = $this->getRulesForDate($property);
$this->requestRules[$property['id'].'.0'] = ['required_with:'.$property['id'].'.1', 'before_or_equal:'.$property['id'].'.1']; $this->requestRules[$property['id'] . '.0'] = ['required_with:' . $property['id'] . '.1', 'before_or_equal:' . $property['id'] . '.1'];
$this->requestRules[$property['id'].'.1'] = ['required_with:'.$property['id'].'.0']; $this->requestRules[$property['id'] . '.1'] = ['required_with:' . $property['id'] . '.0'];
return ['array', 'min:2']; return ['array', 'min:2'];
} }
@ -226,7 +225,7 @@ class AnswerFormRequest extends FormRequest
private function getSelectPropertyOptions($property): array private function getSelectPropertyOptions($property): array
{ {
$type = $property['type']; $type = $property['type'];
if (! isset($property[$type])) { if (!isset($property[$type])) {
return []; return [];
} }
@ -242,7 +241,7 @@ class AnswerFormRequest extends FormRequest
$receivedValue = $receivedData[$property['id']] ?? null; $receivedValue = $receivedData[$property['id']] ?? null;
// Escape all '\' in select options // Escape all '\' in select options
if (in_array($property['type'], ['select', 'multi_select']) && ! is_null($receivedValue)) { if (in_array($property['type'], ['select', 'multi_select']) && !is_null($receivedValue)) {
if (is_array($receivedValue)) { if (is_array($receivedValue)) {
$mergeData[$property['id']] = collect($receivedValue)->map(function ($value) { $mergeData[$property['id']] = collect($receivedValue)->map(function ($value) {
$value = Str::of($value); $value = Str::of($value);
@ -261,7 +260,7 @@ class AnswerFormRequest extends FormRequest
} }
} }
if ($property['type'] === 'phone_number' && (! isset($property['use_simple_text_input']) || ! $property['use_simple_text_input']) && $receivedValue && in_array($receivedValue, $countryCodeMapper)) { if ($property['type'] === 'phone_number' && (!isset($property['use_simple_text_input']) || !$property['use_simple_text_input']) && $receivedValue && in_array($receivedValue, $countryCodeMapper)) {
$mergeData[$property['id']] = null; $mergeData[$property['id']] = null;
} }
}); });

View File

@ -53,7 +53,7 @@ class GenerateAiForm implements ShouldQueue
$this->completion->update([ $this->completion->update([
'status' => AiFormCompletion::STATUS_COMPLETED, 'status' => AiFormCompletion::STATUS_COMPLETED,
'result' => $this->cleanOutput($completer->getArray()), 'result' => GenerateTemplate::cleanAiOutput($completer->getArray())
]); ]);
} catch (\Exception $e) { } catch (\Exception $e) {
$this->onError($e); $this->onError($e);
@ -61,16 +61,6 @@ class GenerateAiForm implements ShouldQueue
} }
private function cleanOutput($formData)
{
// Add property uuids
foreach ($formData['properties'] as &$property) {
$property['id'] = Str::uuid()->toString();
}
return $formData;
}
/** /**
* Handle a job failure. * Handle a job failure.
*/ */

View File

@ -288,6 +288,168 @@ class FormPropertyLogicRule implements DataAwareRule, Rule
], ],
], ],
], ],
'rating' => [
'comparators' => [
'equals' => [
'expected_type' => 'number',
],
'does_not_equal' => [
'expected_type' => 'number',
],
'greater_than' => [
'expected_type' => 'number',
],
'less_than' => [
'expected_type' => 'number',
],
'greater_than_or_equal_to' => [
'expected_type' => 'number',
],
'less_than_or_equal_to' => [
'expected_type' => 'number',
],
'is_empty' => [
'expected_type' => 'boolean',
'format' => [
'type' => 'enum',
'values' => [true],
],
],
'is_not_empty' => [
'expected_type' => 'boolean',
'format' => [
'type' => 'enum',
'values' => [true],
],
],
'content_length_equals' => [
'expected_type' => 'number',
],
'content_length_does_not_equal' => [
'expected_type' => 'number',
],
'content_length_greater_than' => [
'expected_type' => 'number',
],
'content_length_greater_than_or_equal_to' => [
'expected_type' => 'number',
],
'content_length_less_than' => [
'expected_type' => 'number',
],
'content_length_less_than_or_equal_to' => [
'expected_type' => 'number',
],
],
],
'scale' => [
'comparators' => [
'equals' => [
'expected_type' => 'number',
],
'does_not_equal' => [
'expected_type' => 'number',
],
'greater_than' => [
'expected_type' => 'number',
],
'less_than' => [
'expected_type' => 'number',
],
'greater_than_or_equal_to' => [
'expected_type' => 'number',
],
'less_than_or_equal_to' => [
'expected_type' => 'number',
],
'is_empty' => [
'expected_type' => 'boolean',
'format' => [
'type' => 'enum',
'values' => [true],
],
],
'is_not_empty' => [
'expected_type' => 'boolean',
'format' => [
'type' => 'enum',
'values' => [true],
],
],
'content_length_equals' => [
'expected_type' => 'number',
],
'content_length_does_not_equal' => [
'expected_type' => 'number',
],
'content_length_greater_than' => [
'expected_type' => 'number',
],
'content_length_greater_than_or_equal_to' => [
'expected_type' => 'number',
],
'content_length_less_than' => [
'expected_type' => 'number',
],
'content_length_less_than_or_equal_to' => [
'expected_type' => 'number',
],
],
],
'slider' => [
'comparators' => [
'equals' => [
'expected_type' => 'number',
],
'does_not_equal' => [
'expected_type' => 'number',
],
'greater_than' => [
'expected_type' => 'number',
],
'less_than' => [
'expected_type' => 'number',
],
'greater_than_or_equal_to' => [
'expected_type' => 'number',
],
'less_than_or_equal_to' => [
'expected_type' => 'number',
],
'is_empty' => [
'expected_type' => 'boolean',
'format' => [
'type' => 'enum',
'values' => [true],
],
],
'is_not_empty' => [
'expected_type' => 'boolean',
'format' => [
'type' => 'enum',
'values' => [true],
],
],
'content_length_equals' => [
'expected_type' => 'number',
],
'content_length_does_not_equal' => [
'expected_type' => 'number',
],
'content_length_greater_than' => [
'expected_type' => 'number',
],
'content_length_greater_than_or_equal_to' => [
'expected_type' => 'number',
],
'content_length_less_than' => [
'expected_type' => 'number',
],
'content_length_less_than_or_equal_to' => [
'expected_type' => 'number',
],
],
],
'checkbox' => [ 'checkbox' => [
'comparators' => [ 'comparators' => [
'equals' => [ 'equals' => [
@ -484,35 +646,35 @@ class FormPropertyLogicRule implements DataAwareRule, Rule
private function checkBaseCondition($condition) private function checkBaseCondition($condition)
{ {
if (! isset($condition['value'])) { if (!isset($condition['value'])) {
$this->isConditionCorrect = false; $this->isConditionCorrect = false;
$this->conditionErrors[] = 'missing condition body'; $this->conditionErrors[] = 'missing condition body';
return; return;
} }
if (! isset($condition['value']['property_meta'])) { if (!isset($condition['value']['property_meta'])) {
$this->isConditionCorrect = false; $this->isConditionCorrect = false;
$this->conditionErrors[] = 'missing condition property'; $this->conditionErrors[] = 'missing condition property';
return; return;
} }
if (! isset($condition['value']['property_meta']['type'])) { if (!isset($condition['value']['property_meta']['type'])) {
$this->isConditionCorrect = false; $this->isConditionCorrect = false;
$this->conditionErrors[] = 'missing condition property type'; $this->conditionErrors[] = 'missing condition property type';
return; return;
} }
if (! isset($condition['value']['operator'])) { if (!isset($condition['value']['operator'])) {
$this->isConditionCorrect = false; $this->isConditionCorrect = false;
$this->conditionErrors[] = 'missing condition operator'; $this->conditionErrors[] = 'missing condition operator';
return; return;
} }
if (! isset($condition['value']['value'])) { if (!isset($condition['value']['value'])) {
$this->isConditionCorrect = false; $this->isConditionCorrect = false;
$this->conditionErrors[] = 'missing condition value'; $this->conditionErrors[] = 'missing condition value';
@ -523,14 +685,14 @@ class FormPropertyLogicRule implements DataAwareRule, Rule
$operator = $condition['value']['operator']; $operator = $condition['value']['operator'];
$value = $condition['value']['value']; $value = $condition['value']['value'];
if (! isset(self::CONDITION_MAPPING[$typeField])) { if (!isset(self::CONDITION_MAPPING[$typeField])) {
$this->isConditionCorrect = false; $this->isConditionCorrect = false;
$this->conditionErrors[] = 'configuration not found for condition type'; $this->conditionErrors[] = 'configuration not found for condition type';
return; return;
} }
if (! isset(self::CONDITION_MAPPING[$typeField]['comparators'][$operator])) { if (!isset(self::CONDITION_MAPPING[$typeField]['comparators'][$operator])) {
$this->isConditionCorrect = false; $this->isConditionCorrect = false;
$this->conditionErrors[] = 'configuration not found for condition operator'; $this->conditionErrors[] = 'configuration not found for condition operator';
@ -546,11 +708,11 @@ class FormPropertyLogicRule implements DataAwareRule, Rule
$foundCorrectType = true; $foundCorrectType = true;
} }
} }
if (! $foundCorrectType) { if (!$foundCorrectType) {
$this->isConditionCorrect = false; $this->isConditionCorrect = false;
} }
} else { } else {
if (! $this->valueHasCorrectType($type, $value)) { if (!$this->valueHasCorrectType($type, $value)) {
$this->isConditionCorrect = false; $this->isConditionCorrect = false;
$this->conditionErrors[] = 'wrong type of condition value'; $this->conditionErrors[] = 'wrong type of condition value';
} }
@ -561,9 +723,9 @@ class FormPropertyLogicRule implements DataAwareRule, Rule
{ {
if ( if (
($type === 'string' && gettype($value) !== 'string') || ($type === 'string' && gettype($value) !== 'string') ||
($type === 'boolean' && ! is_bool($value)) || ($type === 'boolean' && !is_bool($value)) ||
($type === 'number' && ! is_numeric($value)) || ($type === 'number' && !is_numeric($value)) ||
($type === 'object' && ! is_array($value)) ($type === 'object' && !is_array($value))
) { ) {
return false; return false;
} }
@ -588,7 +750,7 @@ class FormPropertyLogicRule implements DataAwareRule, Rule
return; return;
} }
if (! is_array($conditions['children'])) { if (!is_array($conditions['children'])) {
$this->conditionErrors[] = 'wrong sub-condition type'; $this->conditionErrors[] = 'wrong sub-condition type';
$this->isConditionCorrect = false; $this->isConditionCorrect = false;
@ -607,11 +769,12 @@ class FormPropertyLogicRule implements DataAwareRule, Rule
{ {
if (is_array($actions) && count($actions) > 0) { if (is_array($actions) && count($actions) > 0) {
foreach ($actions as $val) { foreach ($actions as $val) {
if (! in_array($val, static::ACTIONS_VALUES) || if (
(in_array($this->field['type'], ['nf-text', 'nf-code', 'nf-page-break', 'nf-divider', 'nf-image']) && ! in_array($val, ['hide-block', 'show-block'])) || !in_array($val, static::ACTIONS_VALUES) ||
(isset($this->field['hidden']) && $this->field['hidden'] && ! in_array($val, ['show-block', 'require-answer'])) || (in_array($this->field['type'], ['nf-text', 'nf-code', 'nf-page-break', 'nf-divider', 'nf-image']) && !in_array($val, ['hide-block', 'show-block'])) ||
(isset($this->field['required']) && $this->field['required'] && ! in_array($val, ['make-it-optional', 'hide-block', 'disable-block'])) || (isset($this->field['hidden']) && $this->field['hidden'] && !in_array($val, ['show-block', 'require-answer'])) ||
(isset($this->field['disabled']) && $this->field['disabled'] && ! in_array($val, ['enable-block', 'require-answer', 'make-it-optional'])) (isset($this->field['required']) && $this->field['required'] && !in_array($val, ['make-it-optional', 'hide-block', 'disable-block'])) ||
(isset($this->field['disabled']) && $this->field['disabled'] && !in_array($val, ['enable-block', 'require-answer', 'make-it-optional']))
) { ) {
$this->isActionCorrect = false; $this->isActionCorrect = false;
break; break;
@ -646,13 +809,13 @@ class FormPropertyLogicRule implements DataAwareRule, Rule
public function message() public function message()
{ {
$message = null; $message = null;
if (! $this->isConditionCorrect) { if (!$this->isConditionCorrect) {
$message = 'The logic conditions for '.$this->field['name'].' are not complete.'; $message = 'The logic conditions for ' . $this->field['name'] . ' are not complete.';
} elseif (! $this->isActionCorrect) { } elseif (!$this->isActionCorrect) {
$message = 'The logic actions for '.$this->field['name'].' are not valid.'; $message = 'The logic actions for ' . $this->field['name'] . ' are not valid.';
} }
if (count($this->conditionErrors) > 0) { if (count($this->conditionErrors) > 0) {
return $message.' Error detail(s): '.implode(', ', $this->conditionErrors); return $message . ' Error detail(s): ' . implode(', ', $this->conditionErrors);
} }
return $message; return $message;

View File

@ -15,19 +15,19 @@ class FormLogicConditionChecker
private function conditionsAreMet(?array $conditions, array $formData): bool private function conditionsAreMet(?array $conditions, array $formData): bool
{ {
if (! $conditions) { if (!$conditions) {
return false; return false;
} }
// If it's not a group, just a single condition // If it's not a group, just a single condition
if (! isset($conditions['operatorIdentifier'])) { if (!isset($conditions['operatorIdentifier'])) {
return $this->propertyConditionMet($conditions['value'], $formData[$conditions['value']['property_meta']['id']] ?? null); return $this->propertyConditionMet($conditions['value'], $formData[$conditions['value']['property_meta']['id']] ?? null);
} }
if ($conditions['operatorIdentifier'] === 'and') { if ($conditions['operatorIdentifier'] === 'and') {
$isvalid = true; $isvalid = true;
foreach ($conditions['children'] as $childrenCondition) { foreach ($conditions['children'] as $childrenCondition) {
if (! $this->conditionsMet($childrenCondition, $formData)) { if (!$this->conditionsMet($childrenCondition, $formData)) {
$isvalid = false; $isvalid = false;
break; break;
} }
@ -46,7 +46,7 @@ class FormLogicConditionChecker
return $isvalid; return $isvalid;
} }
throw new \Exception('Unexcepted operatorIdentifier:'.$conditions['operatorIdentifier']); throw new \Exception('Unexcepted operatorIdentifier:' . $conditions['operatorIdentifier']);
} }
private function propertyConditionMet(array $propertyCondition, $value): bool private function propertyConditionMet(array $propertyCondition, $value): bool
@ -58,6 +58,9 @@ class FormLogicConditionChecker
case 'phone_number': case 'phone_number':
return $this->textConditionMet($propertyCondition, $value); return $this->textConditionMet($propertyCondition, $value);
case 'number': case 'number':
case 'rating':
case 'scale':
case 'slider':
return $this->numberConditionMet($propertyCondition, $value); return $this->numberConditionMet($propertyCondition, $value);
case 'checkbox': case 'checkbox':
return $this->checkboxConditionMet($propertyCondition, $value); return $this->checkboxConditionMet($propertyCondition, $value);
@ -90,7 +93,7 @@ class FormLogicConditionChecker
return false; return false;
} }
if (! is_array($fieldValue)) { if (!is_array($fieldValue)) {
return $this->checkEquals($condition, $fieldValue); return $this->checkEquals($condition, $fieldValue);
} }
@ -117,7 +120,7 @@ class FormLogicConditionChecker
return count($fieldValue) === 0; return count($fieldValue) === 0;
} }
return $fieldValue == '' || $fieldValue == null || ! $fieldValue; return $fieldValue == '' || $fieldValue == null || !$fieldValue;
} }
private function checkGreaterThan($condition, $fieldValue): bool private function checkGreaterThan($condition, $fieldValue): bool
@ -162,7 +165,7 @@ class FormLogicConditionChecker
private function checkPastWeek($condition, $fieldValue): bool private function checkPastWeek($condition, $fieldValue): bool
{ {
if (! $fieldValue) { if (!$fieldValue) {
return false; return false;
} }
$fieldDate = date('Y-m-d', strtotime($fieldValue)); $fieldDate = date('Y-m-d', strtotime($fieldValue));
@ -172,7 +175,7 @@ class FormLogicConditionChecker
private function checkPastMonth($condition, $fieldValue): bool private function checkPastMonth($condition, $fieldValue): bool
{ {
if (! $fieldValue) { if (!$fieldValue) {
return false; return false;
} }
$fieldDate = date('Y-m-d', strtotime($fieldValue)); $fieldDate = date('Y-m-d', strtotime($fieldValue));
@ -182,7 +185,7 @@ class FormLogicConditionChecker
private function checkPastYear($condition, $fieldValue): bool private function checkPastYear($condition, $fieldValue): bool
{ {
if (! $fieldValue) { if (!$fieldValue) {
return false; return false;
} }
$fieldDate = date('Y-m-d', strtotime($fieldValue)); $fieldDate = date('Y-m-d', strtotime($fieldValue));
@ -192,7 +195,7 @@ class FormLogicConditionChecker
private function checkNextWeek($condition, $fieldValue): bool private function checkNextWeek($condition, $fieldValue): bool
{ {
if (! $fieldValue) { if (!$fieldValue) {
return false; return false;
} }
$fieldDate = date('Y-m-d', strtotime($fieldValue)); $fieldDate = date('Y-m-d', strtotime($fieldValue));
@ -202,7 +205,7 @@ class FormLogicConditionChecker
private function checkNextMonth($condition, $fieldValue): bool private function checkNextMonth($condition, $fieldValue): bool
{ {
if (! $fieldValue) { if (!$fieldValue) {
return false; return false;
} }
$fieldDate = date('Y-m-d', strtotime($fieldValue)); $fieldDate = date('Y-m-d', strtotime($fieldValue));
@ -212,7 +215,7 @@ class FormLogicConditionChecker
private function checkNextYear($condition, $fieldValue): bool private function checkNextYear($condition, $fieldValue): bool
{ {
if (! $fieldValue) { if (!$fieldValue) {
return false; return false;
} }
$fieldDate = date('Y-m-d', strtotime($fieldValue)); $fieldDate = date('Y-m-d', strtotime($fieldValue));
@ -222,7 +225,7 @@ class FormLogicConditionChecker
private function checkLength($condition, $fieldValue, $operator = '==='): bool private function checkLength($condition, $fieldValue, $operator = '==='): bool
{ {
if (! $fieldValue || strlen($fieldValue) === 0) { if (!$fieldValue || strlen($fieldValue) === 0) {
return false; return false;
} }
switch ($operator) { switch ($operator) {
@ -249,11 +252,11 @@ class FormLogicConditionChecker
case 'equals': case 'equals':
return $this->checkEquals($propertyCondition, $value); return $this->checkEquals($propertyCondition, $value);
case 'does_not_equal': case 'does_not_equal':
return ! $this->checkEquals($propertyCondition, $value); return !$this->checkEquals($propertyCondition, $value);
case 'contains': case 'contains':
return $this->checkContains($propertyCondition, $value); return $this->checkContains($propertyCondition, $value);
case 'does_not_contain': case 'does_not_contain':
return ! $this->checkContains($propertyCondition, $value); return !$this->checkContains($propertyCondition, $value);
case 'starts_with': case 'starts_with':
return $this->checkStartsWith($propertyCondition, $value); return $this->checkStartsWith($propertyCondition, $value);
case 'ends_with': case 'ends_with':
@ -261,7 +264,7 @@ class FormLogicConditionChecker
case 'is_empty': case 'is_empty':
return $this->checkIsEmpty($propertyCondition, $value); return $this->checkIsEmpty($propertyCondition, $value);
case 'is_not_empty': case 'is_not_empty':
return ! $this->checkIsEmpty($propertyCondition, $value); return !$this->checkIsEmpty($propertyCondition, $value);
case 'content_length_equals': case 'content_length_equals':
return $this->checkLength($propertyCondition, $value, '==='); return $this->checkLength($propertyCondition, $value, '===');
case 'content_length_does_not_equal': case 'content_length_does_not_equal':
@ -285,7 +288,7 @@ class FormLogicConditionChecker
case 'equals': case 'equals':
return $this->checkEquals($propertyCondition, $value); return $this->checkEquals($propertyCondition, $value);
case 'does_not_equal': case 'does_not_equal':
return ! $this->checkEquals($propertyCondition, $value); return !$this->checkEquals($propertyCondition, $value);
case 'greater_than': case 'greater_than':
return $this->checkGreaterThan($propertyCondition, $value); return $this->checkGreaterThan($propertyCondition, $value);
case 'less_than': case 'less_than':
@ -297,7 +300,7 @@ class FormLogicConditionChecker
case 'is_empty': case 'is_empty':
return $this->checkIsEmpty($propertyCondition, $value); return $this->checkIsEmpty($propertyCondition, $value);
case 'is_not_empty': case 'is_not_empty':
return ! $this->checkIsEmpty($propertyCondition, $value); return !$this->checkIsEmpty($propertyCondition, $value);
case 'content_length_equals': case 'content_length_equals':
return $this->checkLength($propertyCondition, $value, '==='); return $this->checkLength($propertyCondition, $value, '===');
case 'content_length_does_not_equal': case 'content_length_does_not_equal':
@ -321,7 +324,7 @@ class FormLogicConditionChecker
case 'equals': case 'equals':
return $this->checkEquals($propertyCondition, $value); return $this->checkEquals($propertyCondition, $value);
case 'does_not_equal': case 'does_not_equal':
return ! $this->checkEquals($propertyCondition, $value); return !$this->checkEquals($propertyCondition, $value);
} }
return false; return false;
@ -333,11 +336,11 @@ class FormLogicConditionChecker
case 'equals': case 'equals':
return $this->checkEquals($propertyCondition, $value); return $this->checkEquals($propertyCondition, $value);
case 'does_not_equal': case 'does_not_equal':
return ! $this->checkEquals($propertyCondition, $value); return !$this->checkEquals($propertyCondition, $value);
case 'is_empty': case 'is_empty':
return $this->checkIsEmpty($propertyCondition, $value); return $this->checkIsEmpty($propertyCondition, $value);
case 'is_not_empty': case 'is_not_empty':
return ! $this->checkIsEmpty($propertyCondition, $value); return !$this->checkIsEmpty($propertyCondition, $value);
} }
return false; return false;
@ -381,11 +384,11 @@ class FormLogicConditionChecker
case 'contains': case 'contains':
return $this->checkListContains($propertyCondition, $value); return $this->checkListContains($propertyCondition, $value);
case 'does_not_contain': case 'does_not_contain':
return ! $this->checkListContains($propertyCondition, $value); return !$this->checkListContains($propertyCondition, $value);
case 'is_empty': case 'is_empty':
return $this->checkIsEmpty($propertyCondition, $value); return $this->checkIsEmpty($propertyCondition, $value);
case 'is_not_empty': case 'is_not_empty':
return ! $this->checkIsEmpty($propertyCondition, $value); return !$this->checkIsEmpty($propertyCondition, $value);
} }
return false; return false;
@ -397,7 +400,7 @@ class FormLogicConditionChecker
case 'is_empty': case 'is_empty':
return $this->checkIsEmpty($propertyCondition, $value); return $this->checkIsEmpty($propertyCondition, $value);
case 'is_not_empty': case 'is_not_empty':
return ! $this->checkIsEmpty($propertyCondition, $value); return !$this->checkIsEmpty($propertyCondition, $value);
} }
return false; return false;

View File

@ -46,4 +46,10 @@ const onClickAway = (event) => {
close(event) close(event)
} }
} }
defineExpose({
open,
close,
toggle
})
</script> </script>

View File

@ -1,77 +1,52 @@
<template> <template>
<div v-if="!isFieldHidden" <div v-if="!isFieldHidden" :id="'block-' + field.id" :class="getFieldWidthClasses(field)">
:id="'block-'+field.id" :class="getFieldWidthClasses(field)"
>
<div :class="getFieldClasses(field)"> <div :class="getFieldClasses(field)">
<div v-if="adminPreview" <div v-if="adminPreview"
class="absolute -translate-x-full top-0 bottom-0 opacity-0 group-hover/nffield:opacity-100 transition-opacity mb-4" class="absolute -translate-x-full top-0 bottom-0 opacity-0 group-hover/nffield:opacity-100 transition-opacity mb-4">
> <div class="flex flex-col bg-white rounded-md"
<div class="flex flex-col bg-white rounded-md" :class="{'lg:flex-row':!fieldSideBarOpened, 'xl:flex-row':fieldSideBarOpened}"> :class="{ 'lg:flex-row': !fieldSideBarOpened, 'xl:flex-row': fieldSideBarOpened }">
<div class="p-2 -mr-3 -mb-2 text-gray-300 hover:text-blue-500 cursor-pointer hidden xl:block" role="button" <div class="p-2 -mr-3 -mb-2 text-gray-300 hover:text-blue-500 cursor-pointer hidden xl:block" role="button"
:class="{'lg:block':!fieldSideBarOpened, 'xl:block':fieldSideBarOpened}" :class="{ 'lg:block': !fieldSideBarOpened, 'xl:block': fieldSideBarOpened }"
@click.prevent="openAddFieldSidebar" @click.prevent="openAddFieldSidebar">
>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="3" <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="3"
stroke="currentColor" class="w-5 h-5" stroke="currentColor" class="w-5 h-5">
>
<path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15" /> <path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15" />
</svg> </svg>
</div> </div>
<div class="p-2 text-gray-300 hover:text-blue-500 cursor-pointer" role="button" <div class="p-2 text-gray-300 hover:text-blue-500 cursor-pointer" role="button"
:class="{'lg:-mr-2':!fieldSideBarOpened, 'xl:-mr-2':fieldSideBarOpened}" :class="{ 'lg:-mr-2': !fieldSideBarOpened, 'xl:-mr-2': fieldSideBarOpened }"
@click.prevent="editFieldOptions" @click.prevent="editFieldOptions">
> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="w-5 h-5">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"
class="w-5 h-5"
>
<path fill-rule="evenodd" <path fill-rule="evenodd"
d="M11.828 2.25c-.916 0-1.699.663-1.85 1.567l-.091.549a.798.798 0 01-.517.608 7.45 7.45 0 00-.478.198.798.798 0 01-.796-.064l-.453-.324a1.875 1.875 0 00-2.416.2l-.243.243a1.875 1.875 0 00-.2 2.416l.324.453a.798.798 0 01.064.796 7.448 7.448 0 00-.198.478.798.798 0 01-.608.517l-.55.092a1.875 1.875 0 00-1.566 1.849v.344c0 .916.663 1.699 1.567 1.85l.549.091c.281.047.508.25.608.517.06.162.127.321.198.478a.798.798 0 01-.064.796l-.324.453a1.875 1.875 0 00.2 2.416l.243.243c.648.648 1.67.733 2.416.2l.453-.324a.798.798 0 01.796-.064c.157.071.316.137.478.198.267.1.47.327.517.608l.092.55c.15.903.932 1.566 1.849 1.566h.344c.916 0 1.699-.663 1.85-1.567l.091-.549a.798.798 0 01.517-.608 7.52 7.52 0 00.478-.198.798.798 0 01.796.064l.453.324a1.875 1.875 0 002.416-.2l.243-.243c.648-.648.733-1.67.2-2.416l-.324-.453a.798.798 0 01-.064-.796c.071-.157.137-.316.198-.478.1-.267.327-.47.608-.517l.55-.091a1.875 1.875 0 001.566-1.85v-.344c0-.916-.663-1.699-1.567-1.85l-.549-.091a.798.798 0 01-.608-.517 7.507 7.507 0 00-.198-.478.798.798 0 01.064-.796l.324-.453a1.875 1.875 0 00-.2-2.416l-.243-.243a1.875 1.875 0 00-2.416-.2l-.453.324a.798.798 0 01-.796.064 7.462 7.462 0 00-.478-.198.798.798 0 01-.517-.608l-.091-.55a1.875 1.875 0 00-1.85-1.566h-.344zM12 15.75a3.75 3.75 0 100-7.5 3.75 3.75 0 000 7.5z" d="M11.828 2.25c-.916 0-1.699.663-1.85 1.567l-.091.549a.798.798 0 01-.517.608 7.45 7.45 0 00-.478.198.798.798 0 01-.796-.064l-.453-.324a1.875 1.875 0 00-2.416.2l-.243.243a1.875 1.875 0 00-.2 2.416l.324.453a.798.798 0 01.064.796 7.448 7.448 0 00-.198.478.798.798 0 01-.608.517l-.55.092a1.875 1.875 0 00-1.566 1.849v.344c0 .916.663 1.699 1.567 1.85l.549.091c.281.047.508.25.608.517.06.162.127.321.198.478a.798.798 0 01-.064.796l-.324.453a1.875 1.875 0 00.2 2.416l.243.243c.648.648 1.67.733 2.416.2l.453-.324a.798.798 0 01.796-.064c.157.071.316.137.478.198.267.1.47.327.517.608l.092.55c.15.903.932 1.566 1.849 1.566h.344c.916 0 1.699-.663 1.85-1.567l.091-.549a.798.798 0 01.517-.608 7.52 7.52 0 00.478-.198.798.798 0 01.796.064l.453.324a1.875 1.875 0 002.416-.2l.243-.243c.648-.648.733-1.67.2-2.416l-.324-.453a.798.798 0 01-.064-.796c.071-.157.137-.316.198-.478.1-.267.327-.47.608-.517l.55-.091a1.875 1.875 0 001.566-1.85v-.344c0-.916-.663-1.699-1.567-1.85l-.549-.091a.798.798 0 01-.608-.517 7.507 7.507 0 00-.198-.478.798.798 0 01.064-.796l.324-.453a1.875 1.875 0 00-.2-2.416l-.243-.243a1.875 1.875 0 00-2.416-.2l-.453.324a.798.798 0 01-.796.064 7.462 7.462 0 00-.478-.198.798.798 0 01-.517-.608l-.091-.55a1.875 1.875 0 00-1.85-1.566h-.344zM12 15.75a3.75 3.75 0 100-7.5 3.75 3.75 0 000 7.5z"
clip-rule="evenodd" clip-rule="evenodd" />
/>
</svg> </svg>
</div> </div>
<div <div
class="px-2 xl:pl-0 lg:pr-1 lg:pt-2 pb-2 bg-white rounded-md text-gray-300 hover:text-gray-500 cursor-grab draggable" class="px-2 xl:pl-0 lg:pr-1 lg:pt-2 pb-2 bg-white rounded-md text-gray-300 hover:text-gray-500 cursor-grab draggable"
:class="{'lg:pr-1 lg:pl-0':!fieldSideBarOpened, 'xl:-mr-2':fieldSideBarOpened}" :class="{ 'lg:pr-1 lg:pl-0': !fieldSideBarOpened, 'xl:-mr-2': fieldSideBarOpened }" role="button">
role="button" <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24"
> stroke="currentColor">
<svg <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" d="M12 5v.01M12 12v.01M12 19v.01M12 6a1 1 0 110-2 1 1 0 010 2zm0 7a1 1 0 110-2 1 1 0 010 2zm0 7a1 1 0 110-2 1 1 0 010 2z" />
fill="none" viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round" stroke-linejoin="round"
stroke-width="2"
d="M12 5v.01M12 12v.01M12 19v.01M12 6a1 1 0 110-2 1 1 0 010 2zm0 7a1 1 0 110-2 1 1 0 010 2zm0 7a1 1 0 110-2 1 1 0 010 2z"
/>
</svg> </svg>
</div> </div>
</div> </div>
</div> </div>
<component :is="getFieldComponents" v-if="getFieldComponents" <component :is="getFieldComponents" v-if="getFieldComponents" v-bind="inputProperties(field)"
v-bind="inputProperties(field)" :required="isFieldRequired" :required="isFieldRequired" :disabled="isFieldDisabled ? true : null" />
:disabled="isFieldDisabled?true:null"
/>
<template v-else> <template v-else>
<div v-if="field.type === 'nf-text' && field.content" :id="field.id" :key="field.id" <div v-if="field.type === 'nf-text' && field.content" :id="field.id" :key="field.id" class="nf-text w-full mb-3"
class="nf-text w-full mb-3" :class="[getFieldAlignClasses(field)]" :class="[getFieldAlignClasses(field)]" v-html="field.content" />
v-html="field.content"
/>
<div v-if="field.type === 'nf-code' && field.content" :id="field.id" :key="field.id" <div v-if="field.type === 'nf-code' && field.content" :id="field.id" :key="field.id"
class="nf-code w-full px-2 mb-3" class="nf-code w-full px-2 mb-3" v-html="field.content" />
v-html="field.content" <div v-if="field.type === 'nf-divider'" :id="field.id" :key="field.id" class="border-b my-4 w-full mx-2" />
/> <div v-if="field.type === 'nf-image' && (field.image_block || !isPublicFormPage)" :id="field.id" :key="field.id"
<div v-if="field.type === 'nf-divider'" :id="field.id" :key="field.id" class="my-4 w-full px-2" :class="[getFieldAlignClasses(field)]">
class="border-b my-4 w-full mx-2"
/>
<div v-if="field.type === 'nf-image' && (field.image_block || !isPublicFormPage)" :id="field.id"
:key="field.id" class="my-4 w-full px-2" :class="[getFieldAlignClasses(field)]"
>
<div v-if="!field.image_block" class="p-4 border border-dashed"> <div v-if="!field.image_block" class="p-4 border border-dashed">
Open <b>{{ field.name }}'s</b> block settings to upload image. Open <b>{{ field.name }}'s</b> block settings to upload image.
</div> </div>
<img v-else :alt="field.name" :src="field.image_block" class="max-w-full"/> <img v-else :alt="field.name" :src="field.image_block" class="max-w-full" />
</div> </div>
</template> </template>
</div> </div>
@ -113,7 +88,7 @@ export default {
adminPreview: { type: Boolean, default: false } // If used in FormEditorPreview adminPreview: { type: Boolean, default: false } // If used in FormEditorPreview
}, },
setup (props) { setup(props) {
const workingFormStore = useWorkingFormStore() const workingFormStore = useWorkingFormStore()
return { return {
workingFormStore, workingFormStore,
@ -127,7 +102,7 @@ export default {
/** /**
* Get the right input component for the field/options combination * Get the right input component for the field/options combination
*/ */
getFieldComponents () { getFieldComponents() {
const field = this.field const field = this.field
if (field.type === 'text' && field.multi_lines) { if (field.type === 'text' && field.multi_lines) {
return 'TextAreaInput' return 'TextAreaInput'
@ -135,15 +110,6 @@ export default {
if (field.type === 'url' && field.file_upload) { if (field.type === 'url' && field.file_upload) {
return 'FileInput' return 'FileInput'
} }
if (field.type === 'number' && field.is_rating && field.rating_max_value) {
return 'RatingInput'
}
if (field.type === 'number' && field.is_scale && field.scale_max_value) {
return 'ScaleInput'
}
if (field.type === 'number' && field.is_slider && field.slider_max_value) {
return 'SliderInput'
}
if (['select', 'multi_select'].includes(field.type) && field.without_dropdown) { if (['select', 'multi_select'].includes(field.type) && field.without_dropdown) {
return 'FlatSelectInput' return 'FlatSelectInput'
} }
@ -159,6 +125,9 @@ export default {
return { return {
text: 'TextInput', text: 'TextInput',
number: 'TextInput', number: 'TextInput',
rating: 'RatingInput',
scale: 'ScaleInput',
slider: 'SliderInput',
select: 'SelectInput', select: 'SelectInput',
multi_select: 'SelectInput', multi_select: 'SelectInput',
date: 'DateInput', date: 'DateInput',
@ -169,27 +138,27 @@ export default {
phone_number: 'TextInput' phone_number: 'TextInput'
}[field.type] }[field.type]
}, },
isPublicFormPage () { isPublicFormPage() {
return this.$route.name === 'forms-slug' return this.$route.name === 'forms-slug'
}, },
isFieldHidden () { isFieldHidden() {
return !this.showHidden && this.shouldBeHidden return !this.showHidden && this.shouldBeHidden
}, },
shouldBeHidden () { shouldBeHidden() {
return (new FormLogicPropertyResolver(this.field, this.dataFormValue)).isHidden() return (new FormLogicPropertyResolver(this.field, this.dataFormValue)).isHidden()
}, },
isFieldRequired () { isFieldRequired() {
return (new FormLogicPropertyResolver(this.field, this.dataFormValue)).isRequired() return (new FormLogicPropertyResolver(this.field, this.dataFormValue)).isRequired()
}, },
isFieldDisabled () { isFieldDisabled() {
return (new FormLogicPropertyResolver(this.field, this.dataFormValue)).isDisabled() return (new FormLogicPropertyResolver(this.field, this.dataFormValue)).isDisabled()
}, },
beingEdited () { beingEdited() {
return this.adminPreview && this.showEditFieldSidebar && this.form.properties.findIndex((item) => { return this.adminPreview && this.showEditFieldSidebar && this.form.properties.findIndex((item) => {
return item.id === this.field.id return item.id === this.field.id
}) === this.selectedFieldIndex }) === this.selectedFieldIndex
}, },
selectionFieldsOptions () { selectionFieldsOptions() {
// For auto update hidden options // For auto update hidden options
let fieldsOptions = [] let fieldsOptions = []
@ -204,27 +173,27 @@ export default {
return fieldsOptions return fieldsOptions
}, },
fieldSideBarOpened () { fieldSideBarOpened() {
return this.adminPreview && (this.form && this.selectedFieldIndex !== null) ? (this.form.properties[this.selectedFieldIndex] && this.showEditFieldSidebar) : false return this.adminPreview && (this.form && this.selectedFieldIndex !== null) ? (this.form.properties[this.selectedFieldIndex] && this.showEditFieldSidebar) : false
} }
}, },
watch: {}, watch: {},
mounted () { mounted() {
}, },
methods: { methods: {
editFieldOptions () { editFieldOptions() {
this.workingFormStore.openSettingsForField(this.field) this.workingFormStore.openSettingsForField(this.field)
}, },
openAddFieldSidebar () { openAddFieldSidebar() {
this.workingFormStore.openAddFieldSidebar(this.field) this.workingFormStore.openAddFieldSidebar(this.field)
}, },
/** /**
* Get the right input component for the field/options combination * Get the right input component for the field/options combination
*/ */
getFieldClasses () { getFieldClasses() {
let classes = '' let classes = ''
if (this.adminPreview) { if (this.adminPreview) {
classes += '-mx-4 px-4 -my-1 py-1 group/nffield relative transition-colors' classes += '-mx-4 px-4 -my-1 py-1 group/nffield relative transition-colors'
@ -235,7 +204,7 @@ export default {
} }
return classes return classes
}, },
getFieldWidthClasses (field) { getFieldWidthClasses(field) {
if (!field.width || field.width === 'full') return 'w-full px-2' if (!field.width || field.width === 'full') return 'w-full px-2'
else if (field.width === '1/2') { else if (field.width === '1/2') {
return 'w-full sm:w-1/2 px-2' return 'w-full sm:w-1/2 px-2'
@ -249,7 +218,7 @@ export default {
return 'w-full sm:w-3/4 px-2' return 'w-full sm:w-3/4 px-2'
} }
}, },
getFieldAlignClasses (field) { getFieldAlignClasses(field) {
if (!field.align || field.align === 'left') return 'text-left' if (!field.align || field.align === 'left') return 'text-left'
else if (field.align === 'right') { else if (field.align === 'right') {
return 'text-right' return 'text-right'
@ -262,7 +231,7 @@ export default {
/** /**
* Get the right input component options for the field/options * Get the right input component options for the field/options
*/ */
inputProperties (field) { inputProperties(field) {
const inputProperties = { const inputProperties = {
key: field.id, key: field.id,
name: field.id, name: field.id,
@ -305,13 +274,13 @@ export default {
inputProperties.multiple = (field.multiple !== undefined && field.multiple) inputProperties.multiple = (field.multiple !== undefined && field.multiple)
inputProperties.mbLimit = Math.min(Math.max(field.max_file_size, 1), this.form?.max_file_size ?? this.currentWorkspace?.max_file_size) inputProperties.mbLimit = Math.min(Math.max(field.max_file_size, 1), this.form?.max_file_size ?? this.currentWorkspace?.max_file_size)
inputProperties.accept = (this.form.is_pro && field.allowed_file_types) ? field.allowed_file_types : '' inputProperties.accept = (this.form.is_pro && field.allowed_file_types) ? field.allowed_file_types : ''
} else if (field.type === 'number' && field.is_rating) { } else if (field.type === 'rating') {
inputProperties.numberOfStars = parseInt(field.rating_max_value) inputProperties.numberOfStars = parseInt(field.rating_max_value) ?? 5
} else if (field.type === 'number' && field.is_scale) { } else if (field.type === 'scale') {
inputProperties.minScale = parseInt(field.scale_min_value) ?? 1 inputProperties.minScale = parseInt(field.scale_min_value) ?? 1
inputProperties.maxScale = parseInt(field.scale_max_value) ?? 5 inputProperties.maxScale = parseInt(field.scale_max_value) ?? 5
inputProperties.stepScale = parseInt(field.scale_step_value) ?? 1 inputProperties.stepScale = parseInt(field.scale_step_value) ?? 1
} else if (field.type === 'number' && field.is_slider) { } else if (field.type === 'slider') {
inputProperties.minSlider = parseInt(field.slider_min_value) ?? 0 inputProperties.minSlider = parseInt(field.slider_min_value) ?? 0
inputProperties.maxSlider = parseInt(field.slider_max_value) ?? 50 inputProperties.maxSlider = parseInt(field.slider_max_value) ?? 50
inputProperties.stepSlider = parseInt(field.slider_step_value) ?? 5 inputProperties.stepSlider = parseInt(field.slider_step_value) ?? 5

View File

@ -5,8 +5,7 @@
<button class="text-gray-500 hover:text-gray-900 cursor-pointer" @click.prevent="closeSidebar"> <button class="text-gray-500 hover:text-gray-900 cursor-pointer" @click.prevent="closeSidebar">
<svg class="h-6 w-6" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> <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" <path d="M18 6L6 18M6 6L18 18" stroke="currentColor" stroke-width="2" stroke-linecap="round"
stroke-linejoin="round" stroke-linejoin="round" />
/>
</svg> </svg>
</button> </button>
<div class="font-semibold inline ml-2 truncate flex-grow truncate"> <div class="font-semibold inline ml-2 truncate flex-grow truncate">
@ -22,13 +21,11 @@
</p> </p>
<div class="grid grid-cols-2 gap-2"> <div class="grid grid-cols-2 gap-2">
<div v-for="(block, i) in inputBlocks" :key="block.name" <div v-for="(block, i) in inputBlocks" :key="block.name"
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" 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(block.name)" role="button" @click.prevent="addBlock(block.name)">
>
<div class="mx-auto"> <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" <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="block.icon" stroke="currentColor" stroke-width="2" v-html="block.icon"></svg>
></svg>
</div> </div>
<p class="w-full text-xs text-gray-500 uppercase text-center font-semibold mt-1"> <p class="w-full text-xs text-gray-500 uppercase text-center font-semibold mt-1">
{{ block.title }} {{ block.title }}
@ -42,13 +39,11 @@
</p> </p>
<div class="grid grid-cols-2 gap-2"> <div class="grid grid-cols-2 gap-2">
<div v-for="(block, i) in layoutBlocks" :key="block.name" <div v-for="(block, i) in layoutBlocks" :key="block.name"
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" 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(block.name)" role="button" @click.prevent="addBlock(block.name)">
>
<div class="mx-auto"> <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" <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="block.icon" stroke="currentColor" stroke-width="2" v-html="block.icon"></svg>
></svg>
</div> </div>
<p class="w-full text-xs text-gray-500 uppercase text-center font-semibold mt-1"> <p class="w-full text-xs text-gray-500 uppercase text-center font-semibold mt-1">
{{ block.title }} {{ block.title }}
@ -69,18 +64,18 @@ export default {
components: {}, components: {},
props: {}, props: {},
setup () { setup() {
const workingFormStore = useWorkingFormStore() const workingFormStore = useWorkingFormStore()
const {content: form} = storeToRefs(workingFormStore) const { content: form } = storeToRefs(workingFormStore)
return { return {
form, form,
workingFormStore, workingFormStore,
selectedFieldIndex : computed(() => workingFormStore.selectedFieldIndex) selectedFieldIndex: computed(() => workingFormStore.selectedFieldIndex)
} }
}, },
data () { data() {
return { return {
blockForm: null, blockForm: null,
inputBlocks: [ inputBlocks: [
@ -129,6 +124,21 @@ export default {
title: 'Number Input', title: 'Number Input',
icon: '<path stroke-linecap="round" stroke-linejoin="round" d="M7 20l4-16m2 16l4-16M6 9h14M4 15h14"/>' 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', name: 'files',
title: 'File Input', title: 'File Input',
@ -171,13 +181,16 @@ export default {
}, },
computed: { computed: {
defaultBlockNames () { defaultBlockNames() {
return { return {
text: 'Your name', text: 'Your name',
date: 'Date', date: 'Date',
url: 'Link', url: 'Link',
phone_number: 'Phone Number', phone_number: 'Phone Number',
number: 'Number', number: 'Number',
rating: 'Rating',
scale: 'Scale',
slider: 'Slider',
email: 'Email', email: 'Email',
checkbox: 'Checkbox', checkbox: 'Checkbox',
select: 'Select', select: 'Select',
@ -195,21 +208,21 @@ export default {
watch: {}, watch: {},
mounted () { mounted() {
this.reset() this.reset()
}, },
methods: { methods: {
closeSidebar () { closeSidebar() {
this.workingFormStore.closeAddFieldSidebar() this.workingFormStore.closeAddFieldSidebar()
}, },
reset () { reset() {
this.blockForm = useForm({ this.blockForm = useForm({
type: null, type: null,
name: null name: null
}) })
}, },
addBlock (type) { addBlock(type) {
this.blockForm.type = type this.blockForm.type = type
this.blockForm.name = this.defaultBlockNames[type] this.blockForm.name = this.defaultBlockNames[type]
const newBlock = this.prefillDefault(this.blockForm.data()) const newBlock = this.prefillDefault(this.blockForm.data())
@ -218,6 +231,19 @@ export default {
if (['select', 'multi_select'].includes(this.blockForm.type)) { if (['select', 'multi_select'].includes(this.blockForm.type)) {
newBlock[this.blockForm.type] = { options: [] } newBlock[this.blockForm.type] = { options: [] }
} }
if (this.blockForm.type === 'rating') {
newBlock.rating_max_value = 5
}
if (this.blockForm.type === 'scale') {
newBlock.scale_min_value = 1
newBlock.scale_max_value = 5
newBlock.scale_step_value = 1
}
if (this.blockForm.type === 'slider') {
newBlock.slider_min_value = 0
newBlock.slider_max_value = 50
newBlock.slider_step_value = 1
}
newBlock.help_position = 'below_input' newBlock.help_position = 'below_input'
if (this.selectedFieldIndex === null || this.selectedFieldIndex === undefined) { if (this.selectedFieldIndex === null || this.selectedFieldIndex === undefined) {
const newFields = clonedeep(this.form.properties) const newFields = clonedeep(this.form.properties)
@ -232,7 +258,7 @@ export default {
} }
this.reset() this.reset()
}, },
generateUUID () { generateUUID() {
let d = new Date().getTime()// Timestamp 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 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) { return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
@ -247,7 +273,7 @@ export default {
return (c === 'x' ? r : (r & 0x3 | 0x8)).toString(16) return (c === 'x' ? r : (r & 0x3 | 0x8)).toString(16)
}) })
}, },
prefillDefault (data) { prefillDefault(data) {
if (data.type === 'nf-text') { if (data.type === 'nf-text') {
data.content = '<p>This is a text block.</p>' data.content = '<p>This is a text block.</p>'
} else if (data.type === 'nf-page-break') { } else if (data.type === 'nf-page-break') {

View File

@ -1,19 +1,14 @@
<template> <template>
<div v-if="content" class="flex flex-wrap"> <div v-if="content" class="flex flex-wrap">
<div class="w-full font-semibold text-gray-700 dark:text-gray-300 mb-2"> <div class="w-full font-semibold text-gray-700 dark:text-gray-300 mb-2">
{{ property.name }} {{ property.name }}
</div> </div>
<SelectInput v-model="content.operator" class="w-full" :options="operators" <SelectInput v-model="content.operator" class="w-full" :options="operators" :name="'operator_' + property.id"
:name="'operator_'+property.id" placeholder="Comparison operator" placeholder="Comparison operator" @update:model-value="operatorChanged()" />
@update:model-value="operatorChanged()"
/>
<template v-if="needsInput"> <template v-if="needsInput">
<component v-bind="inputComponentData" :is="inputComponentData.component" v-model="content.value" class="w-full" <component v-bind="inputComponentData" :is="inputComponentData.component" v-model="content.value" class="w-full"
:name="'value_'+property.id" placeholder="Filter Value" :name="'value_' + property.id" placeholder="Filter Value" @update:model-value="emitInput()" />
@update:model-value="emitInput()"
/>
</template> </template>
</div> </div>
</template> </template>
@ -22,12 +17,12 @@
import OpenFilters from '../../../../../data/open_filters.json' import OpenFilters from '../../../../../data/open_filters.json'
export default { export default {
components: { }, components: {},
props: { props: {
modelValue: { required: true } modelValue: { required: true }
}, },
data () { data() {
return { return {
content: { ...this.modelValue }, content: { ...this.modelValue },
available_filters: OpenFilters, available_filters: OpenFilters,
@ -35,6 +30,9 @@ export default {
inputComponent: { inputComponent: {
text: 'TextInput', text: 'TextInput',
number: 'TextInput', number: 'TextInput',
rating: 'TextInput',
scale: 'TextInput',
slider: 'TextInput',
select: 'SelectInput', select: 'SelectInput',
multi_select: 'SelectInput', multi_select: 'SelectInput',
date: 'DateInput', date: 'DateInput',
@ -49,7 +47,7 @@ export default {
computed: { computed: {
// Return type of input, and props for that input // Return type of input, and props for that input
inputComponentData () { inputComponentData() {
const componentData = { const componentData = {
component: this.inputComponent[this.property.type], component: this.inputComponent[this.property.type],
name: this.property.id, name: this.property.id,
@ -76,7 +74,7 @@ export default {
return componentData return componentData
}, },
operators () { operators() {
return Object.keys(this.available_filters[this.property.type].comparators).map(key => { return Object.keys(this.available_filters[this.property.type].comparators).map(key => {
return { return {
value: key, value: key,
@ -84,7 +82,7 @@ export default {
} }
}) })
}, },
needsInput () { needsInput() {
const operator = this.selectedOperator() const operator = this.selectedOperator()
if (!operator) { if (!operator) {
return false return false
@ -114,8 +112,8 @@ export default {
}, },
methods: { methods: {
castContent (content) { castContent(content) {
if (this.property.type === 'number' && content.value) { if (['number', 'rating', 'scale', 'slider'].includes(this.property.type) && content.value) {
content.value = Number(content.value) content.value = Number(content.value)
} }
@ -126,7 +124,7 @@ export default {
return content return content
}, },
operatorChanged () { operatorChanged() {
if (!this.content.operator) { if (!this.content.operator) {
return return
} }
@ -143,13 +141,13 @@ export default {
} }
this.emitInput() this.emitInput()
}, },
selectedOperator () { selectedOperator() {
if (!this.content.operator) { if (!this.content.operator) {
return null return null
} }
return this.available_filters[this.property.type].comparators[this.content.operator] return this.available_filters[this.property.type].comparators[this.content.operator]
}, },
optionFilterNames (key, propertyType) { optionFilterNames(key, propertyType) {
if (propertyType === 'checkbox') { if (propertyType === 'checkbox') {
return { return {
equals: 'Is checked', equals: 'Is checked',
@ -160,7 +158,7 @@ export default {
return item.charAt(0).toUpperCase() + item.substring(1) return item.charAt(0).toUpperCase() + item.substring(1)
}).join(' ') }).join(' ')
}, },
emitInput () { emitInput() {
this.$emit('update:modelValue', this.castContent(this.content)) this.$emit('update:modelValue', this.castContent(this.content))
}, },
refreshContent() { refreshContent() {

View File

@ -109,7 +109,7 @@ export default {
return this.field && this.field.type.startsWith('nf') return this.field && this.field.type.startsWith('nf')
}, },
typeCanBeChanged () { typeCanBeChanged () {
return ['text', 'email', 'phone_number', 'number', 'select', 'multi_select'].includes(this.field.type) return ['text', 'email', 'phone_number', 'number', 'select', 'multi_select', 'rating', 'scale', 'slider'].includes(this.field.type)
} }
}, },

View File

@ -1,5 +1,5 @@
<template> <template>
<dropdown v-if="changeTypeOptions.length > 0" dusk="nav-dropdown"> <dropdown ref="newTypeDropdown" v-if="changeTypeOptions.length > 0" dusk="nav-dropdown">
<template #trigger="{toggle}"> <template #trigger="{toggle}">
<v-button class="relative" :class="btnClasses" size="small" color="light-gray" @click.stop="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"> <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">
@ -41,12 +41,15 @@ export default {
computed: { computed: {
changeTypeOptions () { changeTypeOptions () {
let newTypes = [] let newTypes = []
if (['text', 'email', 'phone_number', 'number'].includes(this.field.type)) { if (['text', 'email', 'phone_number', 'number','slider','rating','scale'].includes(this.field.type)) {
newTypes = [ newTypes = [
{ name: 'Text Input', value: 'text' }, { name: 'Text Input', value: 'text' },
{ name: 'Email Input', value: 'email' }, { name: 'Email Input', value: 'email' },
{ name: 'Phone Input', value: 'phone_number' }, { name: 'Phone Input', value: 'phone_number' },
{ name: 'Number Input', value: 'number' } { name: 'Number Input', value: 'number' },
{ name: 'Slider Input', value: 'slider' },
{ name: 'Rating Input', value: 'rating' },
{ name: 'Scale Input', value: 'scale' },
] ]
} }
if (['select', 'multi_select'].includes(this.field.type)) { if (['select', 'multi_select'].includes(this.field.type)) {
@ -75,6 +78,7 @@ export default {
changeType (newType) { changeType (newType) {
if (newType) { if (newType) {
this.$emit('changeType', newType) this.$emit('changeType', newType)
this.$refs.newTypeDropdown.close()
} }
} }
} }

View File

@ -8,22 +8,16 @@
<p class="text-gray-400 mb-2 text-xs"> <p class="text-gray-400 mb-2 text-xs">
Exclude this field or make it required. Exclude this field or make it required.
</p> </p>
<v-checkbox v-model="field.hidden" class="mb-3" <v-checkbox v-model="field.hidden" class="mb-3" :name="field.id + '_hidden'"
:name="field.id+'_hidden'" @update:model-value="onFieldHiddenChange">
@update:model-value="onFieldHiddenChange"
>
Hidden Hidden
</v-checkbox> </v-checkbox>
<v-checkbox v-model="field.required" class="mb-3" <v-checkbox v-model="field.required" class="mb-3" :name="field.id + '_required'"
:name="field.id+'_required'" @update:model-value="onFieldRequiredChange">
@update:model-value="onFieldRequiredChange"
>
Required Required
</v-checkbox> </v-checkbox>
<v-checkbox v-model="field.disabled" class="mb-3" <v-checkbox v-model="field.disabled" class="mb-3" :name="field.id + '_disabled'"
:name="field.id+'_disabled'" @update:model-value="onFieldDisabledChange">
@update:model-value="onFieldDisabledChange"
>
Disabled Disabled
</v-checkbox> </v-checkbox>
</div> </div>
@ -36,9 +30,7 @@
<p class="text-gray-400 mb-3 text-xs"> <p class="text-gray-400 mb-3 text-xs">
Advanced options for checkbox. Advanced options for checkbox.
</p> </p>
<v-checkbox v-model="field.use_toggle_switch" class="mt-3" <v-checkbox v-model="field.use_toggle_switch" class="mt-3" name="use_toggle_switch" help="">
name="use_toggle_switch" help=""
>
Use toggle switch Use toggle switch
</v-checkbox> </v-checkbox>
<p class="text-gray-400 mb-3 text-xs"> <p class="text-gray-400 mb-3 text-xs">
@ -51,88 +43,56 @@
<h3 class="font-semibold block text-lg"> <h3 class="font-semibold block text-lg">
File uploads File uploads
</h3> </h3>
<v-checkbox v-model="field.multiple" class="mt-3" <v-checkbox v-model="field.multiple" class="mt-3" :name="field.id + '_multiple'">
:name="field.id+'_multiple'"
>
Allow multiple files Allow multiple files
</v-checkbox> </v-checkbox>
<text-input name="allowed_file_types" class="mt-3" :form="field" <text-input name="allowed_file_types" class="mt-3" :form="field" label="Allowed file types"
label="Allowed file types" placeholder="jpg,jpeg,png,gif" placeholder="jpg,jpeg,png,gif" help="Comma separated values, leave blank to allow all file types" />
help="Comma separated values, leave blank to allow all file types"
/>
<text-input name="max_file_size" class="mt-3" :form="field" native-type="number" <text-input name="max_file_size" class="mt-3" :form="field" native-type="number" :min="1" :max="mbLimit"
:min="1" label="Maximum file size (in MB)" :placeholder="`1MB - ${mbLimit}MB`"
:max="mbLimit" help="Set the maximum file size that can be uploaded" />
label="Maximum file size (in MB)" :placeholder="`1MB - ${mbLimit}MB`"
help="Set the maximum file size that can be uploaded"
/>
</div> </div>
<!-- Number Options --> <div v-if="field.type === 'rating'" class="border-b py-2 px-4">
<div v-if="field.type === 'number'" class="border-b py-2 px-4">
<h3 class="font-semibold block text-lg"> <h3 class="font-semibold block text-lg">
Number Options
</h3>
<v-checkbox v-model="field.is_rating" class="mt-3"
:name="field.id+'_is_rating'" @update:model-value="initRating"
>
Rating Rating
</v-checkbox> </h3>
<p class="text-gray-400 mb-3 text-xs"> <p class="text-gray-400 mb-3 text-xs">
Transform this field into a star rating input. Advanced options for rating.
</p> </p>
<text-input name="rating_max_value" native-type="number" :min="1" class="mt-3" :form="field" required
label="Max rating value" />
</div>
<text-input v-if="field.is_rating" name="rating_max_value" native-type="number" :min="1" class="mt-3" <div v-if="field.type === 'scale'" class="border-b py-2 px-4">
:form="field" required <h3 class="font-semibold block text-lg">
label="Max rating value"
/>
<v-checkbox v-model="field.is_scale" class="mt-4"
:name="field.id+'_is_scale'" @update:model-value="initScale"
>
Scale Scale
</v-checkbox> </h3>
<p class="text-gray-400 text-xs mb-5"> <p class="text-gray-400 mb-3 text-xs">
Transform this field into a scale/score input. Advanced options for scale.
</p> </p>
<template v-if="field.is_scale"> <text-input name="scale_min_value" native-type="number" class="mt-4" :form="field" required
<text-input name="scale_min_value" native-type="number" class="mt-4" label="Min scale value" />
:form="field" required <text-input name="scale_max_value" native-type="number" :min="1" class="mt-4" :form="field" required
label="Min scale value" label="Max scale value" />
/> <text-input name="scale_step_value" native-type="number" :min="1" class="mt-4" :form="field" required
<text-input name="scale_max_value" native-type="number" :min="1" class="mt-4" label="Scale steps value" />
:form="field" required </div>
label="Max scale value"
/>
<text-input name="scale_step_value" native-type="number" :min="1" class="mt-4"
:form="field" required
label="Scale steps value"
/>
</template>
<v-checkbox v-model="field.is_slider" class="mt-4" <div v-if="field.type === 'slider'" class="border-b py-2 px-4">
:name="field.id+'_is_slider'" @update:model-value="initSlider" <h3 class="font-semibold block text-lg">
>
Slider Slider
</v-checkbox> </h3>
<p class="text-gray-400 text-xs mb-5"> <p class="text-gray-400 mb-3 text-xs">
Transform this field into a slider input. Advanced options for slider.
</p> </p>
<template v-if="field.is_slider"> <text-input name="slider_min_value" native-type="number" class="mt-4" :form="field" required
<text-input name="slider_min_value" native-type="number" class="mt-4" label="Min slider value" />
:form="field" required <text-input name="slider_max_value" native-type="number" :min="1" class="mt-4" :form="field" required
label="Min slider value" label="Max slider value" />
/> <text-input name="slider_step_value" native-type="number" :min="1" class="mt-4" :form="field" required
<text-input name="slider_max_value" native-type="number" :min="1" class="mt-4" label="Slider steps value" />
:form="field" required
label="Max slider value"
/>
<text-input name="slider_step_value" native-type="number" :min="1" class="mt-4"
:form="field" required
label="Slider steps value"
/>
</template>
</div> </div>
<!-- Text Options --> <!-- Text Options -->
@ -143,10 +103,8 @@
<p class="text-gray-400 mb-3 text-xs"> <p class="text-gray-400 mb-3 text-xs">
Keep it simple or make it a multi-lines input. Keep it simple or make it a multi-lines input.
</p> </p>
<v-checkbox v-model="field.multi_lines" class="mb-2" <v-checkbox v-model="field.multi_lines" class="mb-2" :name="field.id + '_multi_lines'"
:name="field.id+'_multi_lines'" @update:model-value="field.multi_lines = $event">
@update:model-value="field.multi_lines = $event"
>
Multi-lines input Multi-lines input
</v-checkbox> </v-checkbox>
</div> </div>
@ -156,76 +114,56 @@
<h3 class="font-semibold block text-lg"> <h3 class="font-semibold block text-lg">
Date Options Date Options
</h3> </h3>
<v-checkbox v-model="field.date_range" class="mt-3" <v-checkbox v-model="field.date_range" class="mt-3" :name="field.id + '_date_range'"
:name="field.id+'_date_range'" @update:model-value="onFieldDateRangeChange">
@update:model-value="onFieldDateRangeChange"
>
Date Range Date Range
</v-checkbox> </v-checkbox>
<p class="text-gray-400 mb-3 text-xs"> <p class="text-gray-400 mb-3 text-xs">
Adds an end date. This cannot be used with the time option yet. Adds an end date. This cannot be used with the time option yet.
</p> </p>
<v-checkbox v-model="field.with_time" <v-checkbox v-model="field.with_time" :name="field.id + '_with_time'" @update:model-value="onFieldWithTimeChange">
:name="field.id+'_with_time'"
@update:model-value="onFieldWithTimeChange"
>
Date with time Date with time
</v-checkbox> </v-checkbox>
<p class="text-gray-400 mb-3 text-xs"> <p class="text-gray-400 mb-3 text-xs">
Include time. Or not. This cannot be used with the date range option yet. Include time. Or not. This cannot be used with the date range option yet.
</p> </p>
<select-input v-if="field.with_time" name="timezone" class="mt-3" <select-input v-if="field.with_time" name="timezone" class="mt-3" :form="field" :options="timezonesOptions"
:form="field" :options="timezonesOptions" label="Timezone" :searchable="true" help="Make sure to select correct timezone. Leave blank otherwise." />
label="Timezone" :searchable="true" <v-checkbox v-model="field.prefill_today" name="prefill_today" @update:model-value="onFieldPrefillTodayChange">
help="Make sure to select correct timezone. Leave blank otherwise."
/>
<v-checkbox v-model="field.prefill_today"
name="prefill_today"
@update:model-value="onFieldPrefillTodayChange"
>
Prefill with 'today' Prefill with 'today'
</v-checkbox> </v-checkbox>
<p class="text-gray-400 mb-3 text-xs"> <p class="text-gray-400 mb-3 text-xs">
if enabled we will pre-fill this field with the current date if enabled we will pre-fill this field with the current date
</p> </p>
<v-checkbox v-model="field.disable_past_dates" <v-checkbox v-model="field.disable_past_dates" name="disable_past_dates" class="mb-3"
name="disable_past_dates" class="mb-3" @update:model-value="onFieldDisablePastDatesChange">
@update:model-value="onFieldDisablePastDatesChange"
>
Disable past dates Disable past dates
</v-checkbox> </v-checkbox>
<v-checkbox v-model="field.disable_future_dates" <v-checkbox v-model="field.disable_future_dates" name="disable_future_dates" class="mb-3"
name="disable_future_dates" class="mb-3" @update:model-value="onFieldDisableFutureDatesChange">
@update:model-value="onFieldDisableFutureDatesChange"
>
Disable future dates Disable future dates
</v-checkbox> </v-checkbox>
</div> </div>
<!-- select/multiselect Options --> <!-- select/multiselect Options -->
<div v-if="['select','multi_select'].includes(field.type)" class="border-b py-2 px-4"> <div v-if="['select', 'multi_select'].includes(field.type)" class="border-b py-2 px-4">
<h3 class="font-semibold block text-lg"> <h3 class="font-semibold block text-lg">
Select Options Select Options
</h3> </h3>
<p class="text-gray-400 mb-3 text-xs"> <p class="text-gray-400 mb-3 text-xs">
Advanced options for your select/multiselect fields. Advanced options for your select/multiselect fields.
</p> </p>
<text-area-input v-model="optionsText" :name="field.id+'_options_text'" class="mt-3" <text-area-input v-model="optionsText" :name="field.id + '_options_text'" class="mt-3"
label="Set selection options" label="Set selection options" help="Add one option per line" @update:model-value="onFieldOptionsChange" />
help="Add one option per line" <v-checkbox v-model="field.allow_creation" name="allow_creation" help=""
@update:model-value="onFieldOptionsChange" @update:model-value="onFieldAllowCreationChange">
/>
<v-checkbox v-model="field.allow_creation"
name="allow_creation" help="" @update:model-value="onFieldAllowCreationChange"
>
Allow respondent to create new options Allow respondent to create new options
</v-checkbox> </v-checkbox>
<v-checkbox v-model="field.without_dropdown" class="mt-3" <v-checkbox v-model="field.without_dropdown" class="mt-3" name="without_dropdown" help=""
name="without_dropdown" help="" @update:model-value="onFieldWithoutDropdownChange" @update:model-value="onFieldWithoutDropdownChange">
>
Always show all select options Always show all select options
</v-checkbox> </v-checkbox>
<p class="text-gray-400 mb-3 text-xs"> <p class="text-gray-400 mb-3 text-xs">
@ -243,35 +181,27 @@
Change your form field name, pre-fill a value, add hints, etc. Change your form field name, pre-fill a value, add hints, etc.
</p> </p>
<text-input name="name" class="mt-3" <text-input name="name" class="mt-3" :form="field" :required="true" label="Field Name" />
:form="field" :required="true"
label="Field Name"
/>
<v-checkbox v-model="field.hide_field_name" class="mt-3" <v-checkbox v-model="field.hide_field_name" class="mt-3" :name="field.id + '_hide_field_name'">
:name="field.id+'_hide_field_name'"
>
Hide field name Hide field name
</v-checkbox> </v-checkbox>
<v-checkbox v-if="field.type === 'phone_number'" v-model="field.use_simple_text_input" class="mt-3" <v-checkbox v-if="field.type === 'phone_number'" v-model="field.use_simple_text_input" class="mt-3"
:name="field.id+'_use_simple_text_input'" :name="field.id + '_use_simple_text_input'">
>
Use simple text input Use simple text input
</v-checkbox> </v-checkbox>
<template v-if="field.type === 'phone_number' && !field.use_simple_text_input"> <template v-if="field.type === 'phone_number' && !field.use_simple_text_input">
<select-input v-model="field.unavailable_countries" class="mt-4" wrapper-class="relative" <select-input v-model="field.unavailable_countries" class="mt-4" wrapper-class="relative"
:options="allCountries" :multiple="true" :options="allCountries" :multiple="true" :searchable="true" :search-keys="['name']" :option-key="'code'"
:searchable="true" :search-keys="['name']" :option-key="'code'" :emit-key="'code'" :emit-key="'code'" label="Disabled countries" :placeholder="'Select a country'"
label="Disabled countries" :placeholder="'Select a country'" help="Remove countries from the phone input">
help="Remove countries from the phone input" <template #selected="{ option, selected }">
>
<template #selected="{option, selected}">
<div class="flex items-center space-x-2 justify-center overflow-hidden"> <div class="flex items-center space-x-2 justify-center overflow-hidden">
{{ option.length }} selected {{ option.length }} selected
</div> </div>
</template> </template>
<template #option="{option, selected}"> <template #option="{ option, selected }">
<div class="flex items-center space-x-2 hover:text-white"> <div class="flex items-center space-x-2 hover:text-white">
<country-flag size="normal" class="!-mt-[9px]" :country="option.code" /> <country-flag size="normal" class="!-mt-[9px]" :country="option.code" />
<span class="grow">{{ option.name }}</span> <span class="grow">{{ option.name }}</span>
@ -280,106 +210,70 @@
<span v-if="selected" class="absolute inset-y-0 right-0 flex items-center pr-2 dark:text-white"> <span v-if="selected" class="absolute inset-y-0 right-0 flex items-center pr-2 dark:text-white">
<svg class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor"> <svg class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" <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" 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" clip-rule="evenodd" />
/>
</svg> </svg>
</span> </span>
</template> </template>
</select-input> </select-input>
<small class="flex -mt-2"> <small class="flex -mt-2">
<a href="#" class="grow" @click.prevent="selectAllCountries">Select All</a> <a href="#" class="grow" @click.prevent="selectAllCountries">Select All</a>
<a href="#" @click.prevent="field.unavailable_countries=null">Un-select All</a> <a href="#" @click.prevent="field.unavailable_countries = null">Un-select All</a>
</small> </small>
</template> </template>
<!-- Pre-fill depends on type --> <!-- Pre-fill depends on type -->
<v-checkbox v-if="field.type=='checkbox'" v-model="field.prefill" class="mt-3" <v-checkbox v-if="field.type == 'checkbox'" v-model="field.prefill" class="mt-3" :name="field.id + '_prefill'"
:name="field.id+'_prefill'" @update:model-value="field.prefill = $event">
@update:model-value="field.prefill = $event"
>
Pre-filled value Pre-filled value
</v-checkbox> </v-checkbox>
<select-input v-else-if="['select','multi_select'].includes(field.type)" name="prefill" class="mt-3" <select-input v-else-if="['select', 'multi_select'].includes(field.type)" name="prefill" class="mt-3"
:form="field" :options="prefillSelectsOptions" :form="field" :options="prefillSelectsOptions" label="Pre-filled value"
label="Pre-filled value" :multiple="field.type === 'multi_select'" />
:multiple="field.type==='multi_select'" <date-input v-else-if="field.type === 'date' && field.prefill_today !== true" name="prefill" class="mt-3"
/> :form="field" :with-time="field.with_time === true" :date-range="field.date_range === true"
<date-input v-else-if="field.type==='date' && field.prefill_today!==true" name="prefill" class="mt-3" label="Pre-filled value" />
:form="field" :with-time="field.with_time===true" <phone-input v-else-if="field.type === 'phone_number' && !field.use_simple_text_input" name="prefill" class="mt-3"
:date-range="field.date_range===true" :form="field" :can-only-country="true" :unavailable-countries="field.unavailable_countries ?? []"
label="Pre-filled value" label="Pre-filled value" />
/> <text-area-input v-else-if="field.type === 'text' && field.multi_lines" name="prefill" class="mt-3" :form="field"
<phone-input v-else-if="field.type === 'phone_number' && !field.use_simple_text_input" label="Pre-filled value" />
name="prefill" class="mt-3" <file-input v-else-if="field.type === 'files'" name="prefill" class="mt-4" :form="field" label="Pre-filled file"
:form="field" :can-only-country="true" :unavailable-countries="field.unavailable_countries ?? []" :multiple="field.multiple === true" :move-to-form-assets="true" />
label="Pre-filled value" <text-input v-else-if="!['files', 'signature'].includes(field.type)" name="prefill" class="mt-3" :form="field"
/> label="Pre-filled value" />
<text-area-input v-else-if="field.type === 'text' && field.multi_lines" <div v-if="['select', 'multi_select'].includes(field.type)" class="-mt-3 mb-3 text-gray-400 dark:text-gray-500">
name="prefill" class="mt-3"
:form="field"
label="Pre-filled value"
/>
<file-input v-else-if="field.type==='files'" name="prefill" class="mt-4"
:form="field"
label="Pre-filled file"
:multiple="field.multiple===true" :move-to-form-assets="true"
/>
<text-input v-else-if="!['files', 'signature'].includes(field.type)" name="prefill" class="mt-3"
:form="field"
label="Pre-filled value"
/>
<div v-if="['select','multi_select'].includes(field.type)" class="-mt-3 mb-3 text-gray-400 dark:text-gray-500">
<small> <small>
A problem? <a href="#" @click.prevent="field.prefill=null">Click here to clear your pre-fill</a> A problem? <a href="#" @click.prevent="field.prefill = null">Click here to clear your pre-fill</a>
</small> </small>
</div> </div>
<!-- Placeholder --> <!-- Placeholder -->
<text-input v-if="hasPlaceholder" name="placeholder" class="mt-3" <text-input v-if="hasPlaceholder" name="placeholder" class="mt-3" :form="field"
:form="field" label="Empty Input Text (Placeholder)" />
label="Empty Input Text (Placeholder)"
/>
<select-input name="width" class="mt-3" <select-input name="width" class="mt-3" :options="[
:options="[ { name: 'Full', value: 'full' },
{name:'Full',value:'full'}, { name: '1/2 (half width)', value: '1/2' },
{name:'1/2 (half width)',value:'1/2'}, { name: '1/3 (a third of the width)', value: '1/3' },
{name:'1/3 (a third of the width)',value:'1/3'}, { name: '2/3 (two thirds of the width)', value: '2/3' },
{name:'2/3 (two thirds of the width)',value:'2/3'}, { name: '1/4 (a quarter of the width)', value: '1/4' },
{name:'1/4 (a quarter of the width)',value:'1/4'}, { name: '3/4 (three quarters of the width)', value: '3/4' },
{name:'3/4 (three quarters of the width)',value:'3/4'}, ]" :form="field" label="Field Width" />
]"
:form="field" label="Field Width"
/>
<!-- Help --> <!-- Help -->
<rich-text-area-input name="help" class="mt-3" <rich-text-area-input name="help" class="mt-3" :form="field" :editor-toolbar="editorToolbarCustom"
:form="field" label="Field Help" help="Your field help will be shown below/above the field, just like this message."
:editor-toolbar="editorToolbarCustom" :help-position="field.help_position" />
label="Field Help" <select-input name="help_position" class="mt-3" :options="[
help="Your field help will be shown below/above the field, just like this message." { name: 'Below input', value: 'below_input' },
:help-position="field.help_position" { name: 'Above input', value: 'above_input' },
/> ]" :form="field" label="Field Help Position" @update:model-value="onFieldHelpPositionChange" />
<select-input name="help_position" class="mt-3"
:options="[
{name:'Below input',value:'below_input'},
{name:'Above input',value:'above_input'},
]"
:form="field" label="Field Help Position"
@update:model-value="onFieldHelpPositionChange"
/>
<template v-if="['text','number','url','email'].includes(field.type)"> <template v-if="['text', 'number', 'url', 'email'].includes(field.type)">
<text-input name="max_char_limit" native-type="number" :min="1" :max="2000" <text-input name="max_char_limit" native-type="number" :min="1" :max="2000" :form="field"
:form="field" label="Max character limit" help="Maximum character limit of 2000" :required="false" />
label="Max character limit" <checkbox-input name="show_char_limit" :form="field" class="mt-3" label="Always show character limit" />
help="Maximum character limit of 2000"
:required="false"
/>
<checkbox-input name="show_char_limit" :form="field" class="mt-3"
label="Always show character limit"
/>
</template> </template>
</div> </div>
@ -389,10 +283,8 @@
Advanced Options Advanced Options
</h3> </h3>
<v-checkbox v-model="field.generates_uuid" <v-checkbox v-model="field.generates_uuid" :name="field.id + '_generates_uuid'"
:name="field.id+'_generates_uuid'" @update:model-value="onFieldGenUIdChange">
@update:model-value="onFieldGenUIdChange"
>
Generates a unique id Generates a unique id
</v-checkbox> </v-checkbox>
<p class="text-gray-400 mb-3 text-xs"> <p class="text-gray-400 mb-3 text-xs">
@ -400,10 +292,8 @@
submission submission
</p> </p>
<v-checkbox v-model="field.generates_auto_increment_id" <v-checkbox v-model="field.generates_auto_increment_id" :name="field.id + '_generates_auto_increment_id'"
:name="field.id+'_generates_auto_increment_id'" @update:model-value="onFieldGenAutoIdChange">
@update:model-value="onFieldGenAutoIdChange"
>
Generates an auto-incremented id Generates an auto-incremented id
</v-checkbox> </v-checkbox>
<p class="text-gray-400 mb-3 text-xs"> <p class="text-gray-400 mb-3 text-xs">
@ -438,7 +328,7 @@ export default {
setup() { setup() {
return { currentWorkspace: computed(() => useWorkspacesStore().getCurrent), } return { currentWorkspace: computed(() => useWorkspacesStore().getCurrent), }
}, },
data () { data() {
return { return {
typesWithoutPlaceholder: ['date', 'checkbox', 'files'], typesWithoutPlaceholder: ['date', 'checkbox', 'files'],
editorToolbarCustom: [ editorToolbarCustom: [
@ -449,13 +339,13 @@ export default {
}, },
computed: { computed: {
hasPlaceholder () { hasPlaceholder() {
return !this.typesWithoutPlaceholder.includes(this.field.type) return !this.typesWithoutPlaceholder.includes(this.field.type)
}, },
mbLimit() { mbLimit() {
return this.form?.max_file_size ?? this.currentWorkspace?.max_file_size return this.form?.max_file_size ?? this.currentWorkspace?.max_file_size
}, },
prefillSelectsOptions () { prefillSelectsOptions() {
if (!['select', 'multi_select'].includes(this.field.type)) return {} if (!['select', 'multi_select'].includes(this.field.type)) return {}
return this.field[this.field.type].options.map(option => { return this.field[this.field.type].options.map(option => {
@ -465,7 +355,7 @@ export default {
} }
}) })
}, },
timezonesOptions () { timezonesOptions() {
if (this.field.type !== 'date') return [] if (this.field.type !== 'date') return []
return timezones.map((timezone) => { return timezones.map((timezone) => {
return { return {
@ -474,13 +364,13 @@ export default {
} }
}) })
}, },
displayBasedOnAdvanced () { displayBasedOnAdvanced() {
if (this.field.generates_uuid || this.field.generates_auto_increment_id) { if (this.field.generates_uuid || this.field.generates_auto_increment_id) {
return false return false
} }
return true return true
}, },
optionsText () { optionsText() {
if (!this.field[this.field.type]) return '' if (!this.field[this.field.type]) return ''
return this.field[this.field.type].options.map(option => { return this.field[this.field.type].options.map(option => {
return option.name return option.name
@ -490,7 +380,7 @@ export default {
watch: { watch: {
'field.width': { 'field.width': {
handler (val) { handler(val) {
if (val === undefined || val === null) { if (val === undefined || val === null) {
this.field.width = 'full' this.field.width = 'full'
} }
@ -498,44 +388,45 @@ export default {
immediate: true immediate: true
}, },
'field.align': { 'field.align': {
handler (val) { handler(val) {
if (val === undefined || val === null) { if (val === undefined || val === null) {
this.field.align = 'left' this.field.align = 'left'
} }
}, },
immediate: true immediate: true
} },
'field.type': {
handler() {
this.setDefaultFieldValues()
},
immediate: true
},
}, },
created () { created() {
if (this.field?.width === undefined || this.field?.width === null) { if (this.field?.width === undefined || this.field?.width === null) {
this.field.width = 'full' this.field.width = 'full'
} }
}, },
mounted () { mounted() {
if (['text', 'number', 'url', 'email'].includes(this.field?.type) && !this.field?.max_char_limit) { this.setDefaultFieldValues()
this.field.max_char_limit = 2000
}
if (this.field.type == 'files') {
this.field.max_file_size = Math.min((this.field.max_file_size ?? this.mbLimit), this.mbLimit)
}
}, },
methods: { methods: {
onFieldDisabledChange (val) { onFieldDisabledChange(val) {
this.field.disabled = val this.field.disabled = val
if (this.field.disabled) { if (this.field.disabled) {
this.field.hidden = false this.field.hidden = false
} }
}, },
onFieldRequiredChange (val) { onFieldRequiredChange(val) {
this.field.required = val this.field.required = val
if (this.field.required) { if (this.field.required) {
this.field.hidden = false this.field.hidden = false
} }
}, },
onFieldHiddenChange (val) { onFieldHiddenChange(val) {
this.field.hidden = val this.field.hidden = val
if (this.field.hidden) { if (this.field.hidden) {
this.field.required = false this.field.required = false
@ -545,74 +436,34 @@ export default {
this.field.generates_auto_increment_id = false this.field.generates_auto_increment_id = false
} }
}, },
onFieldDateRangeChange (val) { onFieldDateRangeChange(val) {
this.field.date_range = val this.field.date_range = val
if (this.field.date_range) { if (this.field.date_range) {
this.field.with_time = false this.field.with_time = false
this.field.prefill_today = false this.field.prefill_today = false
} }
}, },
onFieldWithTimeChange (val) { onFieldWithTimeChange(val) {
this.field.with_time = val this.field.with_time = val
if (this.field.with_time) { if (this.field.with_time) {
this.field.date_range = false this.field.date_range = false
} }
}, },
onFieldGenUIdChange (val) { onFieldGenUIdChange(val) {
this.field.generates_uuid = val this.field.generates_uuid = val
if (this.field.generates_uuid) { if (this.field.generates_uuid) {
this.field.generates_auto_increment_id = false this.field.generates_auto_increment_id = false
this.field.hidden = true this.field.hidden = true
} }
}, },
onFieldGenAutoIdChange (val) { onFieldGenAutoIdChange(val) {
this.field.generates_auto_increment_id = val this.field.generates_auto_increment_id = val
if (this.field.generates_auto_increment_id) { if (this.field.generates_auto_increment_id) {
this.field.generates_uuid = false this.field.generates_uuid = false
this.field.hidden = true this.field.hidden = true
} }
}, },
initRating () { onFieldOptionsChange(val) {
if (this.field.is_rating) {
this.field.is_scale = false
this.field.is_slider = false
if (!this.field.rating_max_value) {
this.field.rating_max_value = 5
}
}
},
initScale () {
if (this.field.is_scale) {
this.field.is_rating = false
this.field.is_slider = false
if (!this.field.scale_min_value) {
this.field.scale_min_value = 1
}
if (!this.field.scale_max_value) {
this.field.scale_max_value = 5
}
if (!this.field.scale_step_value) {
this.field.scale_step_value = 1
}
}
},
initSlider () {
if (this.field.is_slider) {
this.field.is_rating = false
this.field.is_scale = false
if (!this.field.slider_min_value) {
this.field.slider_min_value = 0
}
if (!this.field.slider_max_value) {
this.field.slider_max_value = 50
}
if (!this.field.slider_step_value) {
this.field.slider_step_value = 1
}
}
},
onFieldOptionsChange (val) {
const vals = (val) ? val.trim().split('\n') : [] const vals = (val) ? val.trim().split('\n') : []
const tmpOpts = vals.map(name => { const tmpOpts = vals.map(name => {
return { return {
@ -622,7 +473,7 @@ export default {
}) })
this.field[this.field.type] = { options: tmpOpts } this.field[this.field.type] = { options: tmpOpts }
}, },
onFieldPrefillTodayChange (val) { onFieldPrefillTodayChange(val) {
this.field.prefill_today = val this.field.prefill_today = val
if (this.field.prefill_today) { if (this.field.prefill_today) {
this.field.prefill = 'Pre-filled with current date' this.field.prefill = 'Pre-filled with current date'
@ -633,41 +484,78 @@ export default {
this.field.prefill = null this.field.prefill = null
} }
}, },
onFieldAllowCreationChange (val) { onFieldAllowCreationChange(val) {
this.field.allow_creation = val this.field.allow_creation = val
if (this.field.allow_creation) { if (this.field.allow_creation) {
this.field.without_dropdown = false this.field.without_dropdown = false
} }
}, },
onFieldWithoutDropdownChange (val) { onFieldWithoutDropdownChange(val) {
this.field.without_dropdown = val this.field.without_dropdown = val
if (this.field.without_dropdown) { if (this.field.without_dropdown) {
this.field.allow_creation = false this.field.allow_creation = false
} }
}, },
onFieldDisablePastDatesChange (val) { onFieldDisablePastDatesChange(val) {
this.field.disable_past_dates = val this.field.disable_past_dates = val
if (this.field.disable_past_dates) { if (this.field.disable_past_dates) {
this.field.disable_future_dates = false this.field.disable_future_dates = false
this.field.prefill_today = false this.field.prefill_today = false
} }
}, },
onFieldDisableFutureDatesChange (val) { onFieldDisableFutureDatesChange(val) {
this.field.disable_future_dates = val this.field.disable_future_dates = val
if (this.field.disable_future_dates) { if (this.field.disable_future_dates) {
this.field.disable_past_dates = false this.field.disable_past_dates = false
this.field.prefill_today = false this.field.prefill_today = false
} }
}, },
onFieldHelpPositionChange (val) { onFieldHelpPositionChange(val) {
if (!val) { if (!val) {
this.field.help_position = 'below_input' this.field.help_position = 'below_input'
} }
}, },
selectAllCountries () { selectAllCountries() {
this.field.unavailable_countries = this.allCountries.map(item => { this.field.unavailable_countries = this.allCountries.map(item => {
return item.code return item.code
}) })
},
setDefaultFieldValues() {
const defaultFieldValues = {
slider: {
slider_min_value: 0,
slider_max_value: 100,
slider_step_value: 1
},
scale: {
scale_min_value: 1,
scale_max_value: 5,
scale_step_value: 1
},
rating: {
rating_max_value: 5
},
files: {
max_file_size: Math.min((this.field.max_file_size ?? this.mbLimit), this.mbLimit)
},
text: {
multi_lines: false,
max_char_limit: 2000
},
email: {
max_char_limit: 2000
},
url: {
max_char_limit: 2000
},
}
if (this.field.type in defaultFieldValues) {
Object.keys(defaultFieldValues[this.field.type]).forEach(key => {
if (!Object.hasOwn(this.field,key)) {
this.field[key] = defaultFieldValues[this.field.type][key]
}
})
}
} }
} }
} }

View File

@ -1,78 +1,61 @@
<template> <template>
<table :id="'table-'+tableHash" ref="table" <table :id="'table-' + tableHash" ref="table"
class="notion-table n-table whitespace-no-wrap bg-white dark:bg-notion-dark-light relative" class="notion-table n-table whitespace-no-wrap bg-white dark:bg-notion-dark-light relative">
> <thead :id="'table-header-' + tableHash" ref="header" class="n-table-head top-0"
<thead :id="'table-header-'+tableHash" ref="header" :class="{ 'absolute': data.length !== 0 }" style="will-change: transform; transform: translate3d(0px, 0px, 0px)">
class="n-table-head top-0" <tr class="n-table-row overflow-x-hidden">
:class="{'absolute': data.length !== 0}" <resizable-th v-for="col, index in columns" :id="'table-head-cell-' + col.id" :key="col.id" scope="col"
style="will-change: transform; transform: translate3d(0px, 0px, 0px)" :allow-resize="allowResize" :width="(col.cell_width ? col.cell_width + 'px' : 'auto')"
> class="n-table-cell p-0 relative" @resize-width="resizeCol(col, $event)">
<tr class="n-table-row overflow-x-hidden"> <p
<resizable-th v-for="col, index in columns" :id="'table-head-cell-' + col.id" :key="col.id" class="bg-gray-50 border-r dark:bg-notion-dark truncate sticky top-0 border-b border-gray-200 dark:border-gray-800 px-4 py-2 text-gray-500 font-semibold tracking-wider uppercase text-xs">
scope="col" :allow-resize="allowResize" :width="(col.cell_width ? col.cell_width + 'px':'auto')" {{ col.name }}
class="n-table-cell p-0 relative" </p>
@resize-width="resizeCol(col, $event)" </resizable-th>
> <th class="n-table-cell p-0 relative" style="width: 100px">
<p class="bg-gray-50 border-r dark:bg-notion-dark truncate sticky top-0 border-b border-gray-200 dark:border-gray-800 px-4 py-2 text-gray-500 font-semibold tracking-wider uppercase text-xs" <p
> class="bg-gray-50 dark:bg-notion-dark truncate sticky top-0 border-b border-gray-200 dark:border-gray-800 px-4 py-2 text-gray-500 font-semibold tracking-wider uppercase text-xs">
{{ col.name }} Actions
</p> </p>
</resizable-th> </th>
<th class="n-table-cell p-0 relative" style="width: 100px"> </tr>
<p
class="bg-gray-50 dark:bg-notion-dark truncate sticky top-0 border-b border-gray-200 dark:border-gray-800 px-4 py-2 text-gray-500 font-semibold tracking-wider uppercase text-xs"
>
Actions
</p>
</th>
</tr>
</thead> </thead>
<tbody v-if="data.length > 0" class="n-table-body bg-white dark:bg-notion-dark-light"> <tbody v-if="data.length > 0" class="n-table-body bg-white dark:bg-notion-dark-light">
<tr v-if="$slots.hasOwnProperty('actions')" <tr v-if="$slots.hasOwnProperty('actions')" :id="'table-actions-' + tableHash" ref="actions-row"
:id="'table-actions-'+tableHash" class="action-row absolute w-full" style="will-change: transform; transform: translate3d(0px, 32px, 0px)">
ref="actions-row" <td :colspan="columns.length" class="p-1">
class="action-row absolute w-full" <slot name="actions" />
style="will-change: transform; transform: translate3d(0px, 32px, 0px)" </td>
> </tr>
<td :colspan="columns.length" class="p-1"> <tr v-for="row, index in data" :key="row.id" class="n-table-row" :class="{ 'first': index === 0 }">
<slot name="actions"/> <td v-for="col, colIndex in columns" :key="col.id" :style="{ width: col.cell_width + 'px' }"
</td> class="n-table-cell border-gray-100 dark:border-gray-900 text-sm p-2 overflow-hidden" :class="[{ 'border-b': index !== data.length - 1, 'border-r': colIndex !== columns.length - 1 || hasActions },
</tr> colClasses(col)]">
<tr v-for="row, index in data" :key="row.id" class="n-table-row" :class="{'first':index===0}"> <component :is="fieldComponents[col.type]" class="border-gray-100 dark:border-gray-900" :property="col"
<td v-for="col, colIndex in columns" :value="row[col.id]" />
:key="col.id" </td>
:style="{width: col.cell_width + 'px'}" <td v-if="hasActions" class="n-table-cell border-gray-100 dark:border-gray-900 text-sm p-2 border-b"
class="n-table-cell border-gray-100 dark:border-gray-900 text-sm p-2 overflow-hidden" style="width: 100px">
:class="[{'border-b': index !== data.length - 1, 'border-r': colIndex !== columns.length - 1 || hasActions}, <record-operations :form="form" :structure="columns" :submission="row"
colClasses(col)]" @deleted="(submission) => $emit('deleted', submission)"
> @updated="(submission) => $emit('updated', submission)" />
<component :is="fieldComponents[col.type]" class="border-gray-100 dark:border-gray-900" </td>
:property="col" :value="row[col.id]" </tr>
/> <tr v-if="loading" class="n-table-row border-t bg-gray-50 dark:bg-gray-900">
</td> <td :colspan="columns.length" class="p-8 w-full">
<td v-if="hasActions" class="n-table-cell border-gray-100 dark:border-gray-900 text-sm p-2 border-b" <Loader class="w-4 h-4 mx-auto" />
style="width: 100px" </td>
> </tr>
<record-operations :form="form" :structure="columns" :submission="row"
@deleted="(submission)=>$emit('deleted',submission)"
@updated="(submission)=>$emit('updated', submission)"/>
</td>
</tr>
<tr v-if="loading" class="n-table-row border-t bg-gray-50 dark:bg-gray-900">
<td :colspan="columns.length" class="p-8 w-full">
<Loader class="w-4 h-4 mx-auto"/>
</td>
</tr>
</tbody> </tbody>
<tbody v-else key="body-content" class="n-table-body"> <tbody v-else key="body-content" class="n-table-body">
<tr class="n-table-row loader w-full"> <tr class="n-table-row loader w-full">
<td :colspan="columns.length" class="n-table-cell w-full p-8"> <td :colspan="columns.length" class="n-table-cell w-full p-8">
<Loader v-if="loading" class="w-4 h-4 mx-auto"/> <Loader v-if="loading" class="w-4 h-4 mx-auto" />
<p v-else class="text-gray-500 text-center"> <p v-else class="text-gray-500 text-center">
No data found. No data found.
</p> </p>
</td> </td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
</template> </template>
@ -87,11 +70,11 @@ import OpenCheckbox from './components/OpenCheckbox.vue'
import ResizableTh from './components/ResizableTh.vue' import ResizableTh from './components/ResizableTh.vue'
import RecordOperations from '../components/RecordOperations.vue' import RecordOperations from '../components/RecordOperations.vue'
import clonedeep from 'clone-deep' import clonedeep from 'clone-deep'
import {hash} from "~/lib/utils.js"; import { hash } from "~/lib/utils.js";
export default { export default {
components: {ResizableTh, RecordOperations}, components: { ResizableTh, RecordOperations },
emits: ["updated", "deleted", "resize", "update-columns"], emits: ["updated", "deleted", "resize", "update-columns"],
props: { props: {
columns: { columns: {
type: Array, type: Array,
@ -131,6 +114,9 @@ export default {
fieldComponents: { fieldComponents: {
text: shallowRef(OpenText), text: shallowRef(OpenText),
number: shallowRef(OpenText), number: shallowRef(OpenText),
rating: shallowRef(OpenText),
scale: shallowRef(OpenText),
slider: shallowRef(OpenText),
select: shallowRef(OpenSelect), select: shallowRef(OpenSelect),
multi_select: shallowRef(OpenSelect), multi_select: shallowRef(OpenSelect),
date: shallowRef(OpenDate), date: shallowRef(OpenDate),
@ -164,7 +150,7 @@ export default {
const parent = this.scrollParent ?? document.getElementById('table-page') const parent = this.scrollParent ?? document.getElementById('table-page')
this.tableHash = hash(JSON.stringify(this.form.properties)) this.tableHash = hash(JSON.stringify(this.form.properties))
if (parent) { if (parent) {
parent.addEventListener('scroll', this.handleScroll, {passive: false}) parent.addEventListener('scroll', this.handleScroll, { passive: false })
} }
window.addEventListener('resize', this.handleScroll) window.addEventListener('resize', this.handleScroll)
this.onStructureChange() this.onStructureChange()
@ -290,7 +276,8 @@ export default {
.n-table-row { .n-table-row {
display: flex; display: flex;
&.first, &.loader { &.first,
&.loader {
margin-top: 33px; margin-top: 33px;
} }
} }

View File

@ -37,8 +37,6 @@ export const initForm = (defaultValue = {}, withDefaultProperties = false) => {
notification_body: 'Hello there 👋 <br>This is a confirmation that your submission was successfully saved.', notification_body: 'Hello there 👋 <br>This is a confirmation that your submission was successfully saved.',
notifications_include_submission: true, notifications_include_submission: true,
use_captcha: false, use_captcha: false,
is_rating: false,
rating_max_value: 5,
max_submissions_count: null, max_submissions_count: null,
max_submissions_reached_text: 'This form has now reached the maximum number of allowed submissions and is now closed.', max_submissions_reached_text: 'This form has now reached the maximum number of allowed submissions and is now closed.',
editable_submissions_button_text: 'Edit submission', editable_submissions_button_text: 'Edit submission',

View File

@ -289,6 +289,180 @@
} }
} }
}, },
"rating": {
"comparators": {
"equals": {
"expected_type": "number"
},
"does_not_equal": {
"expected_type": "number"
},
"greater_than": {
"expected_type": "number"
},
"less_than": {
"expected_type": "number"
},
"greater_than_or_equal_to": {
"expected_type": "number"
},
"less_than_or_equal_to": {
"expected_type": "number"
},
"is_empty": {
"expected_type": "boolean",
"format": {
"type": "enum",
"values": [
true
]
}
},
"is_not_empty": {
"expected_type": "boolean",
"format": {
"type": "enum",
"values": [
true
]
}
},
"content_length_equals": {
"expected_type": "number"
},
"content_length_does_not_equal": {
"expected_type": "number"
},
"content_length_greater_than": {
"expected_type": "number"
},
"content_length_greater_than_or_equal_to": {
"expected_type": "number"
},
"content_length_less_than": {
"expected_type": "number"
},
"content_length_less_than_or_equal_to": {
"expected_type": "number"
}
}
},
"scale": {
"comparators": {
"equals": {
"expected_type": "number"
},
"does_not_equal": {
"expected_type": "number"
},
"greater_than": {
"expected_type": "number"
},
"less_than": {
"expected_type": "number"
},
"greater_than_or_equal_to": {
"expected_type": "number"
},
"less_than_or_equal_to": {
"expected_type": "number"
},
"is_empty": {
"expected_type": "boolean",
"format": {
"type": "enum",
"values": [
true
]
}
},
"is_not_empty": {
"expected_type": "boolean",
"format": {
"type": "enum",
"values": [
true
]
}
},
"content_length_equals": {
"expected_type": "number"
},
"content_length_does_not_equal": {
"expected_type": "number"
},
"content_length_greater_than": {
"expected_type": "number"
},
"content_length_greater_than_or_equal_to": {
"expected_type": "number"
},
"content_length_less_than": {
"expected_type": "number"
},
"content_length_less_than_or_equal_to": {
"expected_type": "number"
}
}
},
"slider": {
"comparators": {
"equals": {
"expected_type": "number"
},
"does_not_equal": {
"expected_type": "number"
},
"greater_than": {
"expected_type": "number"
},
"less_than": {
"expected_type": "number"
},
"greater_than_or_equal_to": {
"expected_type": "number"
},
"less_than_or_equal_to": {
"expected_type": "number"
},
"is_empty": {
"expected_type": "boolean",
"format": {
"type": "enum",
"values": [
true
]
}
},
"is_not_empty": {
"expected_type": "boolean",
"format": {
"type": "enum",
"values": [
true
]
}
},
"content_length_equals": {
"expected_type": "number"
},
"content_length_does_not_equal": {
"expected_type": "number"
},
"content_length_greater_than": {
"expected_type": "number"
},
"content_length_greater_than_or_equal_to": {
"expected_type": "number"
},
"content_length_less_than": {
"expected_type": "number"
},
"content_length_less_than_or_equal_to": {
"expected_type": "number"
}
}
},
"checkbox": { "checkbox": {
"comparators": { "comparators": {
"equals": { "equals": {

View File

@ -40,6 +40,9 @@ function propertyConditionMet (propertyCondition, value) {
case 'phone_number': case 'phone_number':
return textConditionMet(propertyCondition, value) return textConditionMet(propertyCondition, value)
case 'number': case 'number':
case 'rating':
case 'scale':
case 'slider':
return numberConditionMet(propertyCondition, value) return numberConditionMet(propertyCondition, value)
case 'checkbox': case 'checkbox':
return checkboxConditionMet(propertyCondition, value) return checkboxConditionMet(propertyCondition, value)

View File

@ -289,6 +289,180 @@
} }
} }
}, },
"rating": {
"comparators": {
"equals": {
"expected_type": "number"
},
"does_not_equal": {
"expected_type": "number"
},
"greater_than": {
"expected_type": "number"
},
"less_than": {
"expected_type": "number"
},
"greater_than_or_equal_to": {
"expected_type": "number"
},
"less_than_or_equal_to": {
"expected_type": "number"
},
"is_empty": {
"expected_type": "boolean",
"format": {
"type": "enum",
"values": [
true
]
}
},
"is_not_empty": {
"expected_type": "boolean",
"format": {
"type": "enum",
"values": [
true
]
}
},
"content_length_equals": {
"expected_type": "number"
},
"content_length_does_not_equal": {
"expected_type": "number"
},
"content_length_greater_than": {
"expected_type": "number"
},
"content_length_greater_than_or_equal_to": {
"expected_type": "number"
},
"content_length_less_than": {
"expected_type": "number"
},
"content_length_less_than_or_equal_to": {
"expected_type": "number"
}
}
},
"scale": {
"comparators": {
"equals": {
"expected_type": "number"
},
"does_not_equal": {
"expected_type": "number"
},
"greater_than": {
"expected_type": "number"
},
"less_than": {
"expected_type": "number"
},
"greater_than_or_equal_to": {
"expected_type": "number"
},
"less_than_or_equal_to": {
"expected_type": "number"
},
"is_empty": {
"expected_type": "boolean",
"format": {
"type": "enum",
"values": [
true
]
}
},
"is_not_empty": {
"expected_type": "boolean",
"format": {
"type": "enum",
"values": [
true
]
}
},
"content_length_equals": {
"expected_type": "number"
},
"content_length_does_not_equal": {
"expected_type": "number"
},
"content_length_greater_than": {
"expected_type": "number"
},
"content_length_greater_than_or_equal_to": {
"expected_type": "number"
},
"content_length_less_than": {
"expected_type": "number"
},
"content_length_less_than_or_equal_to": {
"expected_type": "number"
}
}
},
"slider": {
"comparators": {
"equals": {
"expected_type": "number"
},
"does_not_equal": {
"expected_type": "number"
},
"greater_than": {
"expected_type": "number"
},
"less_than": {
"expected_type": "number"
},
"greater_than_or_equal_to": {
"expected_type": "number"
},
"less_than_or_equal_to": {
"expected_type": "number"
},
"is_empty": {
"expected_type": "boolean",
"format": {
"type": "enum",
"values": [
true
]
}
},
"is_not_empty": {
"expected_type": "boolean",
"format": {
"type": "enum",
"values": [
true
]
}
},
"content_length_equals": {
"expected_type": "number"
},
"content_length_does_not_equal": {
"expected_type": "number"
},
"content_length_greater_than": {
"expected_type": "number"
},
"content_length_greater_than_or_equal_to": {
"expected_type": "number"
},
"content_length_less_than": {
"expected_type": "number"
},
"content_length_less_than_or_equal_to": {
"expected_type": "number"
}
}
},
"checkbox": { "checkbox": {
"comparators": { "comparators": {
"equals": { "equals": {

View File

@ -39,6 +39,13 @@ class FormSubmissionDataFactory
case 'number': case 'number':
$value = $this->faker->numberBetween(); $value = $this->faker->numberBetween();
break; break;
case 'rating':
case 'scale':
$value = $this->faker->numberBetween(1, 5);
break;
case 'slider':
$value = $this->faker->numberBetween(0, 50);
break;
case 'url': case 'url':
$value = $this->faker->url(); $value = $this->faker->url();
break; break;

View File

@ -16,7 +16,7 @@ trait TestHelpers
*/ */
public function createUserWorkspace(User $user) public function createUserWorkspace(User $user)
{ {
if (! $user) { if (!$user) {
return null; return null;
} }
@ -61,6 +61,31 @@ trait TestHelpers
'hidden' => false, 'hidden' => false,
'required' => false, 'required' => false,
], ],
[
'name' => 'Rating',
'type' => 'rating',
'hidden' => false,
'required' => false,
'rating_max_value' => 5
],
[
'name' => 'Scale',
'type' => 'scale',
'hidden' => false,
'required' => false,
'scale_min_value' => 1,
'scale_max_value' => 10,
'scale_step_value' => 1,
],
[
'name' => 'Slider',
'type' => 'slider',
'hidden' => false,
'required' => false,
'slider_min_value' => 1,
'slider_max_value' => 100,
'slider_step_value' => 1,
],
[ [
'name' => 'Select', 'name' => 'Select',
'type' => 'select', 'type' => 'select',