Add fields
This commit is contained in:
parent
288533efd3
commit
bde8d48807
app/Form
Fields
Resources
database/factories/Form/Models
resources/js
components/form
views/formtemplate
tests/Feature/Form
|
@ -0,0 +1,23 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Form\Fields;
|
||||||
|
|
||||||
|
class DropdownField extends Field
|
||||||
|
{
|
||||||
|
public static function name(): string
|
||||||
|
{
|
||||||
|
return 'Dropdown';
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function meta(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'options' => [],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function default()
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,46 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Form\Fields;
|
||||||
|
|
||||||
|
use Illuminate\Support\Collection;
|
||||||
|
|
||||||
|
abstract class Field
|
||||||
|
{
|
||||||
|
|
||||||
|
abstract public static function name(): string;
|
||||||
|
abstract public static function meta(): array;
|
||||||
|
abstract public static function default();
|
||||||
|
|
||||||
|
public static function asMeta(): array
|
||||||
|
{
|
||||||
|
return self::classNames()->map(fn ($class) => $class::allMeta())->toArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Collection<int, class-string<self>>
|
||||||
|
*/
|
||||||
|
private static function classNames(): Collection
|
||||||
|
{
|
||||||
|
return collect(glob(base_path('app/Form/Fields/*.php')))
|
||||||
|
->filter(fn ($fieldClass) => preg_match('/[A-Za-z]Field\.php$/', $fieldClass) === 1)
|
||||||
|
->map(fn ($fieldClass) => str($fieldClass)->replace(base_path(''), '')->replace('/app', '/App')->replace('.php', '')->replace('/', '\\')->toString())
|
||||||
|
->values();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function allMeta(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'id' => class_basename(static::class),
|
||||||
|
'name' => static::name(),
|
||||||
|
'default' => [
|
||||||
|
'name' => '',
|
||||||
|
'type' => class_basename(static::class),
|
||||||
|
'columns' => ['mobile' => 2, 'tablet' => 4, 'desktop' => 12],
|
||||||
|
'default' => static::default(),
|
||||||
|
'required' => false,
|
||||||
|
...static::meta(),
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,23 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Form\Fields;
|
||||||
|
|
||||||
|
class RadioField extends Field
|
||||||
|
{
|
||||||
|
public static function name(): string
|
||||||
|
{
|
||||||
|
return 'Radio';
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function meta(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'options' => [],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function default()
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,21 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Form\Fields;
|
||||||
|
|
||||||
|
class TextField extends Field
|
||||||
|
{
|
||||||
|
public static function name(): string
|
||||||
|
{
|
||||||
|
return 'Text';
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function meta(): array
|
||||||
|
{
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function default(): string
|
||||||
|
{
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,23 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Form\Fields;
|
||||||
|
|
||||||
|
class TextareaField extends Field
|
||||||
|
{
|
||||||
|
public static function name(): string
|
||||||
|
{
|
||||||
|
return 'Textarea';
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function meta(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'rows' => 5,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function default(): string
|
||||||
|
{
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
namespace App\Form\Resources;
|
namespace App\Form\Resources;
|
||||||
|
|
||||||
|
use App\Form\Fields\Field;
|
||||||
use App\Lib\HasMeta;
|
use App\Lib\HasMeta;
|
||||||
use Illuminate\Http\Resources\Json\JsonResource;
|
use Illuminate\Http\Resources\Json\JsonResource;
|
||||||
|
|
||||||
|
@ -32,7 +33,8 @@ class FormtemplateResource extends JsonResource
|
||||||
public static function meta(): array
|
public static function meta(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
'fields' => [
|
'fields' => Field::asMeta(),
|
||||||
|
[
|
||||||
[
|
[
|
||||||
'id' => 'TextField',
|
'id' => 'TextField',
|
||||||
'name' => 'Text',
|
'name' => 'Text',
|
||||||
|
|
|
@ -0,0 +1,28 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Database\Factories\Form\Models;
|
||||||
|
|
||||||
|
use App\Form\Models\Formtemplate;
|
||||||
|
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @extends Factory<Formtemplate>
|
||||||
|
*/
|
||||||
|
class FormtemplateFactory extends Factory
|
||||||
|
{
|
||||||
|
|
||||||
|
public $model = Formtemplate::class;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Define the model's default state.
|
||||||
|
*
|
||||||
|
* @return array<string, mixed>
|
||||||
|
*/
|
||||||
|
public function definition()
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'name' => $this->faker->words(4, true),
|
||||||
|
'config' => [],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
|
@ -5,8 +5,20 @@
|
||||||
<span v-show="required" class="text-red-800"> *</span>
|
<span v-show="required" class="text-red-800"> *</span>
|
||||||
</span>
|
</span>
|
||||||
<div class="real-field-wrap size-sm" :class="sizes[size].field">
|
<div class="real-field-wrap size-sm" :class="sizes[size].field">
|
||||||
<input :name="name" :type="type" :value="transformedValue" :disabled="disabled" :placeholder="placeholder"
|
<input
|
||||||
@keypress="$emit('keypress', $event)" @input="onInput" @change="onChange" @focus="onFocus" @blur="onBlur" />
|
:name="name"
|
||||||
|
:type="type"
|
||||||
|
:value="transformedValue"
|
||||||
|
:disabled="disabled"
|
||||||
|
:placeholder="placeholder"
|
||||||
|
:min="min"
|
||||||
|
:max="max"
|
||||||
|
@keypress="$emit('keypress', $event)"
|
||||||
|
@input="onInput"
|
||||||
|
@change="onChange"
|
||||||
|
@focus="onFocus"
|
||||||
|
@blur="onBlur"
|
||||||
|
/>
|
||||||
<div v-if="hint" class="info-wrap">
|
<div v-if="hint" class="info-wrap">
|
||||||
<div v-tooltip="hint">
|
<div v-tooltip="hint">
|
||||||
<ui-sprite src="info-button" class="info-button"></ui-sprite>
|
<ui-sprite src="info-button" class="info-button"></ui-sprite>
|
||||||
|
@ -281,6 +293,12 @@ export default {
|
||||||
default: false,
|
default: false,
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
},
|
},
|
||||||
|
min: {
|
||||||
|
default: () => '',
|
||||||
|
},
|
||||||
|
max: {
|
||||||
|
default: () => '',
|
||||||
|
},
|
||||||
name: {},
|
name: {},
|
||||||
},
|
},
|
||||||
data: function () {
|
data: function () {
|
||||||
|
@ -324,7 +342,7 @@ export default {
|
||||||
},
|
},
|
||||||
created() {
|
created() {
|
||||||
if (typeof this.modelValue === 'undefined') {
|
if (typeof this.modelValue === 'undefined') {
|
||||||
this.$emit('input', this.default === undefined ? '' : this.default);
|
this.$emit('update:modelValue', this.default === undefined ? '' : this.default);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
|
|
@ -19,8 +19,6 @@
|
||||||
>{{ label }}<span v-show="required" class="text-red-800"> *</span></span
|
>{{ label }}<span v-show="required" class="text-red-800"> *</span></span
|
||||||
>
|
>
|
||||||
<textarea
|
<textarea
|
||||||
v-text="value"
|
|
||||||
@input="trigger"
|
|
||||||
:placeholder="placeholder"
|
:placeholder="placeholder"
|
||||||
class="h-full outline-none bg-gray-700 border-gray-600 border-solid"
|
class="h-full outline-none bg-gray-700 border-gray-600 border-solid"
|
||||||
:rows="rows"
|
:rows="rows"
|
||||||
|
@ -28,6 +26,8 @@
|
||||||
'rounded-lg text-sm border-2 p-2 text-gray-300': size === null,
|
'rounded-lg text-sm border-2 p-2 text-gray-300': size === null,
|
||||||
'rounded-lg py-2 px-2 text-xs border-2 text-gray-300': size == 'sm',
|
'rounded-lg py-2 px-2 text-xs border-2 text-gray-300': size == 'sm',
|
||||||
}"
|
}"
|
||||||
|
@input="trigger"
|
||||||
|
v-text="modelValue"
|
||||||
></textarea>
|
></textarea>
|
||||||
<div v-if="hint" v-tooltip="hint" class="absolute right-0 top-0 mr-2 mt-2">
|
<div v-if="hint" v-tooltip="hint" class="absolute right-0 top-0 mr-2 mt-2">
|
||||||
<ui-sprite src="info-button" class="w-5 h-5 text-indigo-200"></ui-sprite>
|
<ui-sprite src="info-button" class="w-5 h-5 text-indigo-200"></ui-sprite>
|
||||||
|
@ -35,66 +35,57 @@
|
||||||
</label>
|
</label>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup>
|
||||||
export default {
|
const emit = defineEmits(['update:modelValue']);
|
||||||
data: function () {
|
|
||||||
return {
|
const props = defineProps({
|
||||||
focus: false,
|
required: {
|
||||||
};
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
},
|
},
|
||||||
props: {
|
inset: {
|
||||||
required: {
|
default: false,
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false,
|
},
|
||||||
},
|
size: {
|
||||||
inset: {
|
default: null,
|
||||||
default: false,
|
},
|
||||||
type: Boolean,
|
rows: {
|
||||||
},
|
default: function () {
|
||||||
size: {
|
return 4;
|
||||||
default: null,
|
|
||||||
},
|
|
||||||
rows: {
|
|
||||||
default: function () {
|
|
||||||
return 4;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
id: {
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
hint: {
|
|
||||||
default: null,
|
|
||||||
},
|
|
||||||
value: {
|
|
||||||
default: undefined,
|
|
||||||
},
|
|
||||||
mask: {
|
|
||||||
default: undefined,
|
|
||||||
},
|
|
||||||
label: {
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
type: {
|
|
||||||
required: false,
|
|
||||||
default: function () {
|
|
||||||
return 'text';
|
|
||||||
},
|
|
||||||
},
|
|
||||||
placeholder: {
|
|
||||||
default: '',
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
id: {
|
||||||
trigger(v) {
|
required: true,
|
||||||
this.$emit('input', v.target.value);
|
},
|
||||||
|
hint: {
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
modelValue: {
|
||||||
|
default: undefined,
|
||||||
|
},
|
||||||
|
mask: {
|
||||||
|
default: undefined,
|
||||||
|
},
|
||||||
|
label: {
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
type: {
|
||||||
|
required: false,
|
||||||
|
default: function () {
|
||||||
|
return 'text';
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
created() {
|
placeholder: {
|
||||||
if (typeof this.value === 'undefined') {
|
default: '',
|
||||||
this.$emit('input', '');
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
};
|
});
|
||||||
|
function trigger(v) {
|
||||||
|
emit('update:modelValue', v.target.value);
|
||||||
|
}
|
||||||
|
if (typeof props.modelValue === 'undefined') {
|
||||||
|
emit('update:modelValue', '');
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scope>
|
<style scope>
|
||||||
|
|
|
@ -0,0 +1,23 @@
|
||||||
|
<template>
|
||||||
|
<ui-box class="relative" :heading="heading" container-class="grid gap-3">
|
||||||
|
<slot></slot>
|
||||||
|
<template #in-title>
|
||||||
|
<div class="flex ml-3 space-x-3 absolute right-0 top-0 mr-4 mt-4">
|
||||||
|
<a href="#" @click.prevent="$emit('submit')">
|
||||||
|
<ui-sprite src="save" class="text-zinc-400 w-4 h-4"></ui-sprite>
|
||||||
|
</a>
|
||||||
|
<a href="#" @click.prevent="$emit('close')">
|
||||||
|
<ui-sprite src="close" class="text-zinc-400 w-4 h-4"></ui-sprite>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</ui-box>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
const emit = defineEmits(['close', 'submit']);
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
heading: {},
|
||||||
|
});
|
||||||
|
</script>
|
|
@ -0,0 +1,23 @@
|
||||||
|
<template>
|
||||||
|
<div class="space-y-1">
|
||||||
|
<span class="text-xs font-semibold text-gray-400">Optionen</span>
|
||||||
|
<f-text
|
||||||
|
v-for="(option, index) in modelValue.options"
|
||||||
|
:id="`options-${index}`"
|
||||||
|
:key="index"
|
||||||
|
size="sm"
|
||||||
|
:name="`options-${index}`"
|
||||||
|
:model-value="option"
|
||||||
|
@update:modelValue="$emit('update:modelValue', {...props.modelValue, options: props.modelValue.options.toSpliced(index, 1, $event)})"
|
||||||
|
></f-text>
|
||||||
|
<ui-icon-button icon="plus" @click="$emit('update:modelValue', {...modelValue, options: [...modelValue.options, '']})">Option einfügen</ui-icon-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
const props = defineProps({
|
||||||
|
modelValue: {},
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(['update:modelValue']);
|
||||||
|
</script>
|
|
@ -27,6 +27,7 @@
|
||||||
>
|
>
|
||||||
<f-text id="fieldname" v-model="singleField.model.name" label="Name" size="sm" name="fieldname"></f-text>
|
<f-text id="fieldname" v-model="singleField.model.name" label="Name" size="sm" name="fieldname"></f-text>
|
||||||
<f-switch id="fieldrequired" v-model="singleField.model.required" label="Erforderlich" size="sm" name="fieldrequired" inline></f-switch>
|
<f-switch id="fieldrequired" v-model="singleField.model.required" label="Erforderlich" size="sm" name="fieldrequired" inline></f-switch>
|
||||||
|
<component :is="fields[singleField.model.type]" v-model="singleField.model"></component>
|
||||||
</asideform>
|
</asideform>
|
||||||
</div>
|
</div>
|
||||||
<ui-box heading="Vorschau" container-class="grid gap-3" class="w-[800px]">
|
<ui-box heading="Vorschau" container-class="grid gap-3" class="w-[800px]">
|
||||||
|
@ -49,6 +50,9 @@
|
||||||
import {computed, ref} from 'vue';
|
import {computed, ref} from 'vue';
|
||||||
import '!/eventform/dist/main.js';
|
import '!/eventform/dist/main.js';
|
||||||
import Asideform from './Asideform.vue';
|
import Asideform from './Asideform.vue';
|
||||||
|
import TextareaField from './TextareaField.vue';
|
||||||
|
import DropdownField from './DropdownField.vue';
|
||||||
|
import RadioField from './RadioField.vue';
|
||||||
|
|
||||||
const sectionVisible = ref(-1);
|
const sectionVisible = ref(-1);
|
||||||
const singleSection = ref(null);
|
const singleSection = ref(null);
|
||||||
|
@ -60,6 +64,12 @@ const props = defineProps({
|
||||||
});
|
});
|
||||||
const emit = defineEmits(['submit', 'cancel']);
|
const emit = defineEmits(['submit', 'cancel']);
|
||||||
|
|
||||||
|
const fields = {
|
||||||
|
TextareaField: TextareaField,
|
||||||
|
DropdownField: DropdownField,
|
||||||
|
RadioField: RadioField,
|
||||||
|
};
|
||||||
|
|
||||||
function editSection(sectionIndex) {
|
function editSection(sectionIndex) {
|
||||||
singleSection.value = {
|
singleSection.value = {
|
||||||
model: {...inner.value.config.sections[sectionIndex]},
|
model: {...inner.value.config.sections[sectionIndex]},
|
||||||
|
|
|
@ -0,0 +1,23 @@
|
||||||
|
<template>
|
||||||
|
<div class="space-y-1">
|
||||||
|
<span class="text-xs font-semibold text-gray-400">Optionen</span>
|
||||||
|
<f-text
|
||||||
|
v-for="(option, index) in modelValue.options"
|
||||||
|
:id="`options-${index}`"
|
||||||
|
:key="index"
|
||||||
|
size="sm"
|
||||||
|
:name="`options-${index}`"
|
||||||
|
:model-value="option"
|
||||||
|
@update:modelValue="$emit('update:modelValue', {...props.modelValue, options: props.modelValue.options.toSpliced(index, 1, $event)})"
|
||||||
|
></f-text>
|
||||||
|
<ui-icon-button icon="plus" @click="$emit('update:modelValue', {...modelValue, options: [...modelValue.options, '']})">Option einfügen</ui-icon-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
const props = defineProps({
|
||||||
|
modelValue: {},
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(['update:modelValue']);
|
||||||
|
</script>
|
|
@ -0,0 +1,22 @@
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<f-text
|
||||||
|
id="rows"
|
||||||
|
label="Zeilen"
|
||||||
|
size="sm"
|
||||||
|
name="rows"
|
||||||
|
:model-value="modelValue.rows"
|
||||||
|
type="number"
|
||||||
|
min="1"
|
||||||
|
@update:modelValue="$emit('update:modelValue', {...modelValue, rows: $event})"
|
||||||
|
></f-text>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
const props = defineProps({
|
||||||
|
modelValue: {},
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(['update:modelValue']);
|
||||||
|
</script>
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
namespace Tests\Feature\Form;
|
namespace Tests\Feature\Form;
|
||||||
|
|
||||||
|
use App\Form\Models\Formtemplate;
|
||||||
use Illuminate\Foundation\Testing\DatabaseTransactions;
|
use Illuminate\Foundation\Testing\DatabaseTransactions;
|
||||||
use Tests\TestCase;
|
use Tests\TestCase;
|
||||||
|
|
||||||
|
@ -12,10 +13,27 @@ class FormtemplateIndexActionTest extends TestCase
|
||||||
|
|
||||||
public function testItDisplaysIndexPage(): void
|
public function testItDisplaysIndexPage(): void
|
||||||
{
|
{
|
||||||
|
$formtemplate = Formtemplate::factory()->create();
|
||||||
|
|
||||||
$this->login()->loginNami()->withoutExceptionHandling();
|
$this->login()->loginNami()->withoutExceptionHandling();
|
||||||
|
|
||||||
$this->get(route('formtemplate.index'))
|
$this->get(route('formtemplate.index'))
|
||||||
|
->assertInertiaPath('data.data.0.links', [
|
||||||
|
'update' => route('formtemplate.update', ['formtemplate' => $formtemplate]),
|
||||||
|
])
|
||||||
->assertInertiaPath('data.meta.fields.0', [
|
->assertInertiaPath('data.meta.fields.0', [
|
||||||
|
'id' => 'DropdownField',
|
||||||
|
'name' => 'Dropdown',
|
||||||
|
'default' => [
|
||||||
|
'name' => '',
|
||||||
|
'type' => 'DropdownField',
|
||||||
|
'columns' => ['mobile' => 2, 'tablet' => 4, 'desktop' => 12],
|
||||||
|
'default' => [],
|
||||||
|
'required' => false,
|
||||||
|
'options' => [],
|
||||||
|
]
|
||||||
|
])
|
||||||
|
->assertInertiaPath('data.meta.fields.1', [
|
||||||
'id' => 'TextField',
|
'id' => 'TextField',
|
||||||
'name' => 'Text',
|
'name' => 'Text',
|
||||||
'default' => [
|
'default' => [
|
||||||
|
@ -26,6 +44,18 @@ class FormtemplateIndexActionTest extends TestCase
|
||||||
'required' => false,
|
'required' => false,
|
||||||
]
|
]
|
||||||
])
|
])
|
||||||
|
->assertInertiaPath('data.meta.fields.2', [
|
||||||
|
'id' => 'TextareaField',
|
||||||
|
'name' => 'Textarea',
|
||||||
|
'default' => [
|
||||||
|
'name' => '',
|
||||||
|
'type' => 'TextareaField',
|
||||||
|
'columns' => ['mobile' => 2, 'tablet' => 4, 'desktop' => 12],
|
||||||
|
'default' => '',
|
||||||
|
'required' => false,
|
||||||
|
'rows' => 5,
|
||||||
|
]
|
||||||
|
])
|
||||||
->assertInertiaPath('data.meta.default', [
|
->assertInertiaPath('data.meta.default', [
|
||||||
'name' => '',
|
'name' => '',
|
||||||
'config' => [
|
'config' => [
|
||||||
|
|
Loading…
Reference in New Issue