Compare commits

...

22 Commits

Author SHA1 Message Date
philipp lang e0bd7c5add Fix tests
continuous-integration/drone/push Build is failing Details
2024-04-24 00:20:03 +02:00
philipp lang 47cbc3321c Fix tests
continuous-integration/drone/push Build is failing Details
2024-04-24 00:09:47 +02:00
philipp lang 2984ad9372 Fix tests
continuous-integration/drone/push Build is failing Details
2024-04-24 00:02:27 +02:00
philipp lang 0ba8c58c76 Fixed tests 2024-04-23 23:55:42 +02:00
philipp lang 0437511bf1 Add filter for attachments
continuous-integration/drone/push Build is failing Details
2024-04-23 23:48:09 +02:00
philipp lang 1328f359b2 Fix: Update active columns 2024-04-23 23:12:14 +02:00
philipp lang 4997895dfb Add heading in confirm registration mail 2024-04-23 23:03:29 +02:00
philipp lang 4f1c02acac Add test for blocks 2024-04-23 23:01:27 +02:00
philipp lang ae39c8dd31 Fix: Return emoty blocks
continuous-integration/drone/push Build is failing Details
2024-04-20 00:29:32 +02:00
philipp lang dbffb4b394 Change time for editorjs update
continuous-integration/drone/push Build is failing Details
2024-04-20 00:18:10 +02:00
philipp lang 1929c8c216 Add filter for mail top and mail bottom 2024-04-20 00:04:20 +02:00
philipp lang 21dfc4f0b2 Add findByKey to FieldCollection 2024-04-20 00:00:04 +02:00
philipp lang 5e2427ee81 Add condition for checkbox field 2024-04-19 23:57:05 +02:00
philipp lang 37c8f35b58 Add condition selector for mode 2024-04-19 22:13:55 +02:00
philipp lang 5310686b9c Mod condition data 2024-04-19 22:06:09 +02:00
philipp lang 2363be7d61 Add panel types in mail theme 2024-04-19 22:04:51 +02:00
philipp lang 2abe061c4f Update content tests 2024-04-19 17:42:59 +02:00
philipp lang fde1a9d169 Update mail views 2024-04-19 16:51:17 +02:00
philipp lang 1f4173caf8 Lint 2024-04-19 16:35:59 +02:00
philipp lang 6ad639730a Fix: hide form when tab is not opened
continuous-integration/drone/push Build is failing Details
2024-04-19 14:58:49 +02:00
philipp lang 096e44b767 Add conditions to files and email
continuous-integration/drone/push Build is failing Details
2024-04-19 13:49:47 +02:00
philipp lang e08ad63313 Add test for isDirty 2024-04-19 11:38:53 +02:00
44 changed files with 915 additions and 164 deletions

2
.gitignore vendored
View File

@ -8,7 +8,7 @@ yarn-error.log
/public/build
/public/vendor
storage/*.key
vendor/
/vendor/
Homestead.yaml
Homestead.json
.vagrant/

View File

@ -28,8 +28,8 @@ class FormStoreAction
'to' => 'required|date',
'registration_from' => 'present|nullable|date',
'registration_until' => 'present|nullable|date',
'mail_top' => 'nullable|string',
'mail_bottom' => 'nullable|string',
'mail_top' => 'array',
'mail_bottom' => 'array',
'header_image' => 'required|exclude',
'mailattachments' => 'present|array|exclude',
];

View File

@ -29,8 +29,8 @@ class FormUpdateAction
'to' => 'required|date',
'registration_from' => 'present|nullable|date',
'registration_until' => 'present|nullable|date',
'mail_top' => 'nullable|string',
'mail_bottom' => 'nullable|string',
'mail_top' => 'array',
'mail_bottom' => 'array',
];
}

View File

@ -11,6 +11,13 @@ class IsDirtyAction
{
use AsAction;
public function rules(): array
{
return [
'config' => 'array|present',
];
}
public function handle(Form $form, ActionRequest $request): JsonResponse
{
$form->config = $request->input('config');

View File

@ -70,7 +70,12 @@ class FieldCollection extends Collection
public function find(Field $givenField): ?Field
{
return $this->first(fn ($field) => $field->key === $givenField->key);
return $this->findByKey($givenField->key);
}
public function findByKey(string $key): ?Field
{
return $this->first(fn ($field) => $field->key === $key);
}
/**

View File

@ -0,0 +1,48 @@
<?php
namespace App\Form\Editor;
use App\Form\Models\Participant;
use App\Lib\Editor\ConditionResolver;
class FormConditionResolver extends ConditionResolver
{
private Participant $participant;
public function forParticipant(Participant $participant): self
{
$this->participant = $participant;
return $this;
}
/**
* @inheritdoc
*/
public function filterCondition($mode, $ifs): bool
{
if (count($ifs) === 0) {
return true;
}
foreach ($ifs as $if) {
$field = $this->participant->getFields()->findByKey($if['field']);
$matches = $field->matches($if['comparator'], $if['value']);
if ($matches && $mode === 'any') {
return true;
}
if (!$matches && $mode === 'all') {
return false;
}
}
if ($mode === 'any') {
return false;
}
if ($mode === 'all') {
return true;
}
}
}

View File

@ -2,6 +2,8 @@
namespace App\Form\Fields;
use App\Form\Matchers\BooleanMatcher;
use App\Form\Matchers\Matcher;
use App\Form\Models\Form;
use App\Form\Models\Participant;
use App\Form\Presenters\BooleanPresenter;
@ -79,4 +81,9 @@ class CheckboxField extends Field
{
return app(BooleanPresenter::class);
}
public function getMatcher(): Matcher
{
return app(BooleanMatcher::class);
}
}

View File

@ -2,6 +2,8 @@
namespace App\Form\Fields;
use App\Form\Matchers\Matcher;
use App\Form\Matchers\SingleValueMatcher;
use App\Form\Models\Form;
use App\Form\Models\Participant;
use Faker\Generator;
@ -80,4 +82,9 @@ class DropdownField extends Field
public function afterRegistration(Form $form, Participant $participant, array $input): void
{
}
public function getMatcher(): Matcher
{
return app(SingleValueMatcher::class);
}
}

View File

@ -165,4 +165,9 @@ abstract class Field extends Data
{
return $this->key . '_display';
}
public function matches(string $comparator, mixed $value): bool
{
return $this->getMatcher()->setValue($this->value)->matches($comparator, $value);
}
}

View File

@ -2,6 +2,8 @@
namespace App\Form\Fields;
use App\Form\Matchers\Matcher;
use App\Form\Matchers\SingleValueMatcher;
use App\Form\Models\Form;
use App\Form\Models\Participant;
use Faker\Generator;
@ -80,4 +82,9 @@ class RadioField extends Field
public function afterRegistration(Form $form, Participant $participant, array $input): void
{
}
public function getMatcher(): Matcher
{
return app(SingleValueMatcher::class);
}
}

View File

@ -23,9 +23,9 @@ class TextField extends Field
];
}
public static function default(): string
public static function default(): ?string
{
return '';
return null;
}
public static function fake(Generator $faker): array

View File

@ -3,6 +3,7 @@
namespace App\Form\Mails;
use App\Form\Data\FormConfigData;
use App\Form\Editor\FormConditionResolver;
use App\Form\Models\Participant;
use Illuminate\Bus\Queueable;
use Illuminate\Mail\Attachment;
@ -10,7 +11,6 @@ use Illuminate\Mail\Mailable;
use Illuminate\Mail\Mailables\Content;
use Illuminate\Mail\Mailables\Envelope;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Storage;
class ConfirmRegistrationMail extends Mailable
{
@ -18,6 +18,10 @@ class ConfirmRegistrationMail extends Mailable
public string $fullname;
public FormConfigData $config;
/** @var array<string, mixed> */
public array $topText;
/** @var array<string, mixed> */
public array $bottomText;
/**
* Create a new message instance.
@ -26,8 +30,11 @@ class ConfirmRegistrationMail extends Mailable
*/
public function __construct(public Participant $participant)
{
$conditionResolver = app(FormConditionResolver::class)->forParticipant($participant);
$this->fullname = $participant->getFields()->getFullname();
$this->config = $participant->getConfig();
$this->topText = $conditionResolver->makeBlocks($participant->form->mail_top);
$this->bottomText = $conditionResolver->makeBlocks($participant->form->mail_bottom);
}
/**
@ -61,7 +68,13 @@ class ConfirmRegistrationMail extends Mailable
*/
public function attachments()
{
$conditionResolver = app(FormConditionResolver::class)->forParticipant($this->participant);
return $this->participant->form->getMedia('mailattachments')
->filter(fn ($media) => $conditionResolver->filterCondition(
data_get($media->getCustomProperty('conditions'), 'mode', 'all'),
data_get($media->getCustomProperty('conditions'), 'ifs', []),
))
->map(fn ($media) => Attachment::fromStorageDisk($media->disk, $media->getPathRelativeToRoot()))
->all();
}

View File

@ -0,0 +1,7 @@
<?php
namespace App\Form\Matchers;
class BooleanMatcher extends SingleValueMatcher
{
}

View File

@ -0,0 +1,18 @@
<?php
namespace App\Form\Matchers;
abstract class Matcher
{
public mixed $value;
public function setValue(mixed $value): self
{
$this->value = $value;
return $this;
}
abstract public function matches(string $comparator, mixed $value): bool;
}

View File

@ -0,0 +1,28 @@
<?php
namespace App\Form\Matchers;
class SingleValueMatcher extends Matcher
{
public function matches(string $comparator, mixed $value): bool
{
if ($comparator === 'isEqual' && $value === $this->value) {
return true;
}
if ($comparator === 'isNotEqual' && $value !== $this->value) {
return true;
}
if ($comparator === 'isIn' && in_array($this->value, $value)) {
return true;
}
if ($comparator === 'isNotIn' && !in_array($this->value, $value)) {
return true;
}
return false;
}
}

View File

@ -29,6 +29,8 @@ class Form extends Model implements HasMedia
'config' => FormConfigData::class,
'meta' => 'json',
'description' => 'json',
'mail_top' => 'json',
'mail_bottom' => 'json',
];
/** @var array<int, string> */
@ -64,13 +66,17 @@ class Form extends Model implements HasMedia
});
$this->addMediaCollection('mailattachments')
->withDefaultProperties(fn () => [
'conditions' => [],
'conditions' => [
'mode' => 'all',
'ifs' => []
],
])
->withPropertyValidation(fn () => [
'conditions' => 'array',
'conditions.*.field' => 'required',
'conditions.*.comparator' => 'required',
'conditions.*.value' => 'present',
'conditions.mode' => 'required|string|in:all,any',
'conditions.ifs' => 'array',
'conditions.ifs.*.field' => 'required',
'conditions.ifs.*.comparator' => 'required',
'conditions.ifs.*.value' => 'present',
]);
}
@ -133,11 +139,12 @@ class Form extends Model implements HasMedia
public static function booted(): void
{
static::saving(function (self $model) {
if (is_null($model->meta)) {
if (is_null(data_get($model->meta, 'active_columns'))) {
$model->setAttribute('meta', [
'active_columns' => $model->getFields()->count() ? $model->getFields()->take(4)->pluck('key')->toArray() : null,
'sorting' => $model->getFields()->count() ? [$model->getFields()->first()->key, 'asc'] : null,
]);
return;
}
if (is_array(data_get($model->meta, 'active_columns'))) {
@ -145,6 +152,7 @@ class Form extends Model implements HasMedia
...$model->meta,
'active_columns' => array_values(array_intersect([...$model->getFields()->pluck('key')->toArray(), 'created_at'], $model->meta['active_columns'])),
]);
return;
}
});
}

View File

@ -77,8 +77,8 @@ class FormResource extends JsonResource
'to' => null,
'registration_from' => null,
'registration_until' => null,
'mail_top' => null,
'mail_bottom' => null,
'mail_top' => [],
'mail_bottom' => [],
'config' => null,
'header_image' => null,
'mailattachments' => [],

View File

@ -0,0 +1,32 @@
<?php
namespace App\Lib\Editor;
abstract class ConditionResolver
{
/**
* @param array<string, mixed> $ifs
*/
abstract public function filterCondition(string $mode, array $ifs): bool;
/**
* @param array<string, mixed> $content
* @return array<string, mixed>
*/
public function makeBlocks(array $content): array
{
return array_filter(data_get($content, 'blocks', []), fn ($block) => $this->filterBlock($block));
}
/**
* @inheritdoc
*/
public function filterBlock(array $block): bool
{
$mode = data_get($block, 'tunes.condition.mode', 'any');
$ifs = data_get($block, 'tunes.condition.ifs', []);
return $this->filterCondition($mode, $ifs);
}
}

View File

@ -7,6 +7,7 @@ use App\Mailgateway\Types\LocalType;
use App\Mailgateway\Types\MailmanType;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Resources\Json\JsonResource;
use Illuminate\Support\Facades\Blade;
use Illuminate\Support\ServiceProvider;
use Laravel\Telescope\Telescope;
@ -36,6 +37,8 @@ class AppServiceProvider extends ServiceProvider
]));
app()->extend('media-library-helpers', fn ($p) => $p->put('form', Form::class));
Blade::componentNamespace('App\\View\\Mail', 'mail-view');
}
/**

28
app/View/Mail/Editor.php Normal file
View File

@ -0,0 +1,28 @@
<?php
namespace App\View\Mail;
use Illuminate\View\Component;
class Editor extends Component
{
/**
* Create a new component instance.
*
* @param array<int, mixed> $content
* @return void
*/
public function __construct(public array $content)
{
}
/**
* Get the view / contents that represent the component.
*
* @return \Illuminate\Contracts\View\View|\Closure|string
*/
public function render()
{
return view('components.mail.editor');
}
}

View File

@ -7,16 +7,14 @@ use Database\Factories\Traits\FakesMedia;
use Illuminate\Database\Eloquent\Factories\Factory;
use Tests\Feature\Form\FormtemplateFieldRequest;
use Tests\Feature\Form\FormtemplateSectionRequest;
use Tests\RequestFactories\EditorRequestFactory;
/**
* @extends Factory<Form>
* @method self name(string $name)
* @method self from(string $from)
* @method self to(string $to)
* @method self mailTop(string $content)
* @method self mailBottom(string $content)
* @method self excerpt(string $excerpt)
* @method self description(string $description)
* @method self registrationFrom(string|null $date)
* @method self registrationUntil(string|null $date)
*/
@ -40,15 +38,15 @@ class FormFactory extends Factory
{
return [
'name' => $this->faker->words(4, true),
'description' => $this->faker->text(),
'description' => EditorRequestFactory::new()->create(),
'excerpt' => $this->faker->words(10, true),
'config' => ['sections' => []],
'from' => $this->faker->dateTimeBetween('+1 week', '+4 weeks')->format('Y-m-d H:i:s'),
'to' => $this->faker->dateTimeBetween('+1 week', '+4 weeks')->format('Y-m-d H:i:s'),
'registration_from' => $this->faker->dateTimeBetween('-2 weeks', 'now')->format('Y-m-d H:i:s'),
'registration_until' => $this->faker->dateTimeBetween('now', '+2 weeks')->format('Y-m-d H:i:s'),
'mail_top' => $this->faker->text(),
'mail_bottom' => $this->faker->text(),
'mail_top' => EditorRequestFactory::new()->create(),
'mail_bottom' => EditorRequestFactory::new()->create(),
];
}
@ -75,4 +73,19 @@ class FormFactory extends Factory
{
return $this->state([str($method)->snake()->toString() => $parameters[0]]);
}
public function mailTop(EditorRequestFactory $factory): self
{
return $this->state(['mail_top' => $factory->create()]);
}
public function mailBottom(EditorRequestFactory $factory): self
{
return $this->state(['mail_bottom' => $factory->create()]);
}
public function description(EditorRequestFactory $factory): self
{
return $this->state(['description' => $factory->create()]);
}
}

View File

@ -22,9 +22,9 @@ trait FakesMedia
});
}
public function withDocument(string $collection, string $filename, string $content = ''): self
public function withDocument(string $collection, string $filename, string $content = '', array $properties = []): self
{
return $this->afterCreating(function (HasMedia $model) use ($filename, $collection, $content) {
return $this->afterCreating(function (HasMedia $model) use ($filename, $collection, $content, $properties) {
$pathinfo = pathinfo($filename);
UploadedFile::fake()->create($filename, $content, 'application/pdf')->storeAs('media-library', $filename, 'temp');
@ -32,6 +32,7 @@ trait FakesMedia
$model->addMediaFromDisk('media-library/' . $filename, 'temp')
->usingName($pathinfo['filename'])
->usingFileName($pathinfo['basename'])
->withCustomProperties($properties)
->toMediaCollection($collection);
});
}

View File

@ -1,4 +1,5 @@
<template>
<div>
<div>
<span v-if="label" class="font-semibold text-gray-400" :class="labelClass(size)">{{ label }}<span v-show="required" class="text-red-800">&nbsp;*</span></span>
<div class="relative w-full h-full">
@ -8,6 +9,18 @@
</div>
</div>
</div>
<ui-popup
v-if="condition !== null"
heading="Bedingungen"
@close="
condition.resolve(condition.data);
condition = null;
"
>
<slot name="conditions" :data="condition.data" :resolve="condition.resolve" :reject="condition.reject"></slot>
</ui-popup>
</div>
</template>
<script setup>
@ -39,6 +52,11 @@ const props = defineProps({
id: {
required: true,
},
conditions: {
required: false,
type: Boolean,
default: () => false,
},
hint: {
default: null,
},
@ -54,15 +72,87 @@ const props = defineProps({
});
const editor = ref(null);
const condition = ref(null);
async function openPopup(data) {
return new Promise((resolve, reject) => {
new Promise((innerResolve, innerReject) => {
condition.value = {
resolve: innerResolve,
reject: innerReject,
data: data,
};
}).then((data) => {
resolve(data);
condition.value = null;
});
});
}
class ConditionTune {
constructor({api, data, config, block}) {
this.api = api;
this.data = data || {
mode: 'all',
ifs: [],
};
this.config = config;
this.block = block;
this.wrapper = null;
}
static get isTune() {
return true;
}
wrap(blockContent) {
this.wrapper = document.createElement('div');
this.wrapper.appendChild(blockContent);
this.styleWrapper();
return this.wrapper;
}
hasData() {
return this.data.ifs.length > 0;
}
styleWrapper() {
if (this.hasData()) {
this.wrapper.className = 'relative mt-6 mb-6 p-1 border border-blue-200 rounded';
if (!this.wrapper.querySelector('.condition-description')) {
var tooltip = document.createElement('div');
tooltip.className = 'condition-description absolute top-0 left-0 -mt-4 ml-1 h-4 flex px-2 items-center text-xs leading-none bg-blue-200 text-blue-900 rounded-t-lg';
tooltip.innerHTML = 'Bedingung';
this.wrapper.appendChild(tooltip);
}
} else {
this.wrapper.className = '';
if (this.wrapper.querySelector('.condition-description')) {
this.wrapper.removeChild(this.wrapper.querySelector('.condition-description'));
}
}
}
render() {
return {
label: 'Bedingungen',
closeOnActivate: true,
toggle: true,
onActivate: async () => {
this.data = await openPopup(this.data);
this.styleWrapper();
this.block.dispatchChange();
},
};
}
save() {
return this.data;
}
}
onMounted(async () => {
editor.value = new EditorJS({
placeholder: props.placeholder,
holder: props.id,
minHeight: 0,
defaultBlock: 'paragraph',
data: JSON.parse(JSON.stringify(props.modelValue)),
tools: {
var tools = {
paragraph: {
class: Paragraph,
shortcut: 'CTRL+P',
@ -94,11 +184,33 @@ onMounted(async () => {
shortcut: 'CTRL+L',
inlineToolbar: true,
},
},
};
var tunes = [];
if (props.conditions) {
tools.condition = {
class: ConditionTune,
};
tunes.push('condition');
}
editor.value = new EditorJS({
placeholder: props.placeholder,
holder: props.id,
minHeight: 0,
defaultBlock: 'paragraph',
data: JSON.parse(JSON.stringify(props.modelValue)),
tunes: tunes,
tools: tools,
onChange: debounce(async (api, event) => {
const data = await editor.value.save();
console.log(data);
emit('update:modelValue', data);
}, 500),
}, 200),
onPopup: () => {
console.log('opened');
},
});
await editor.value.isReady;
console.log('Editor is ready');

View File

@ -5,21 +5,29 @@
</ui-note>
<div v-else>
<div class="mt-2">Datei: {{ value.name }}</div>
<f-select id="mode" v-model="inner.mode" :options="modeOptions" name="mode" label="Modus"></f-select>
<ui-icon-button class="mt-4 mb-2" icon="plus" @click="addCondition">Bedingung einfügen</ui-icon-button>
<div v-for="(condition, index) in conditions" :key="index" class="grid grid-cols-[1fr_1fr_1fr_max-content] gap-2">
<f-select :id="`field-${index}`" v-model="condition.field" :options="fieldOptions" :name="`field-${index}`" label="Feld"></f-select>
<div v-for="(condition, index) in inner.ifs" :key="index" class="grid grid-cols-[1fr_1fr_1fr_max-content] gap-2">
<f-select
:id="`field-${index}`"
:model-value="condition.field"
:options="fieldOptions"
:name="`field-${index}`"
label="Feld"
@update:model-value="update(index, 'field', $event)"
></f-select>
<f-select
:id="`comparator-${index}`"
:options="comparatorOptions"
:model-value="condition.comparator"
:name="`comparator-${index}`"
label="Vergleich"
@update:model-value="updateComparator(condition, $event)"
@update:model-value="update(index, 'comparator', $event)"
></f-select>
<f-select
v-if="condition.field && ['isEqual', 'isNotEqual'].includes(condition.comparator)"
v-if="condition.field && ['isEqual', 'isNotEqual'].includes(condition.comparator) && ['RadioField', 'DropdownField'].includes(getField(condition.field).type)"
:id="`value-${index}`"
v-model="condition.value"
:options="getOptions(condition.field)"
@ -27,14 +35,21 @@
label="Wert"
></f-select>
<f-multipleselect
v-if="condition.field && ['isIn', 'isNotIn'].includes(condition.comparator)"
v-if="condition.field && ['isIn', 'isNotIn'].includes(condition.comparator) && ['RadioField', 'DropdownField'].includes(getField(condition.field).type)"
:id="`value-${index}`"
v-model="condition.value"
:options="getOptions(condition.field)"
:name="`value-${index}`"
label="Wert"
></f-multipleselect>
<ui-action-button tooltip="Löschen" icon="trash" class="btn-danger self-end h-8" @click="conditions.splice(index, 1)"></ui-action-button>
<f-switch
v-if="condition.field && condition.comparator && ['CheckboxField'].includes(getField(condition.field).type)"
:id="`value-${index}`"
v-model="condition.value"
:name="`value-${index}`"
label="Wert"
></f-switch>
<ui-action-button tooltip="Löschen" icon="trash" class="btn-danger self-end h-8" @click="inner.ifs.splice(index, 1)"></ui-action-button>
</div>
<ui-icon-button class="mt-4 mb-2" icon="save" @click="save">Speichern</ui-icon-button>
@ -44,7 +59,7 @@
<script setup>
import {ref, inject, computed} from 'vue';
const axios = inject('axios');
const emit = defineEmits(['close']);
const emit = defineEmits(['save']);
const props = defineProps({
value: {
@ -56,10 +71,15 @@ const props = defineProps({
});
const comparatorOptions = ref([
{id: 'isEqual', name: 'ist gleich', defaultValue: null},
{id: 'isNotEqual', name: 'ist ungleich', defaultValue: null},
{id: 'isIn', name: 'ist in', defaultValue: []},
{id: 'isNotIn', name: 'ist nicht in', defaultValue: []},
{id: 'isEqual', name: 'ist gleich', defaultValue: {DropdownField: null, RadioField: null, CheckboxField: false}},
{id: 'isNotEqual', name: 'ist ungleich', defaultValue: {DropdownField: null, RadioField: null, CheckboxField: false}},
{id: 'isIn', name: 'ist in', defaultValue: {DropdownField: [], RadioField: [], CheckboxField: false}},
{id: 'isNotIn', name: 'ist nicht in', defaultValue: {DropdownField: [], RadioField: [], CheckboxField: false}},
]);
const modeOptions = ref([
{id: 'all', name: 'alle Bedingungen müssen zutreffen'},
{id: 'any', name: 'mindestens eine Bedingung muss zutreffen'},
]);
const fields = computed(() => {
@ -75,9 +95,23 @@ const fields = computed(() => {
return result;
});
function updateComparator(condition, comparator) {
condition.value = comparatorOptions.value.find((c) => c.id === comparator).defaultValue;
condition.comparator = comparator;
function update(index, key, value) {
if (key === 'comparator') {
var old = inner.value.ifs[index];
inner.value.ifs[index] = {
field: old.field,
comparator: value,
value: old.field ? comparatorOptions.value.find((c) => c.id === value).defaultValue[getField(old.field).type] : null,
};
}
if (key === 'field') {
var old = inner.value.ifs[index];
inner.value.ifs[index] = {
field: value,
comparator: null,
value: null,
};
}
}
function getField(fieldName) {
@ -96,12 +130,12 @@ const fieldOptions = computed(() =>
})
);
const conditions = ref('conditions' in props.value.properties ? props.value.properties.conditions : []);
const inner = ref(JSON.parse(JSON.stringify(props.value)));
const locked = ref(false);
function addCondition() {
conditions.value.push({
inner.value.ifs.push({
field: null,
comparator: null,
value: null,
@ -109,14 +143,7 @@ function addCondition() {
}
async function save() {
await axios.patch(`/mediaupload/${props.value.id}`, {
properties: {
...props.value.properties,
conditions: conditions.value,
},
});
emit('close');
emit('save', inner.value);
}
async function checkIfDirty() {

View File

@ -63,7 +63,7 @@
></f-textarea>
<f-editor id="description" v-model="single.description" name="description" label="Beschreibung" rows="10" required></f-editor>
</div>
<div v-show="active === 1">
<div v-if="active === 1">
<ui-note class="mt-2"> Sobald sich der erste Teilnehmer für die Veranstaltung angemeldet hat, kann dieses Formular nicht mehr geändert werden. </ui-note>
<form-builder v-model="single.config" :meta="meta"></form-builder>
</div>
@ -74,7 +74,19 @@
Die Anrede ("Hallo Max Mustermann") wird automatisch an den Anfang gesetzt.<br />
Außerdem kannst du Dateien hochladen, die automatisch mit angehangen werden.
</ui-note>
<f-textarea id="mail_top" v-model="single.mail_top" name="mail_top" label="E-Mail-Teil 1" rows="8" required></f-textarea>
<div>
<ui-tabs v-model="activeMailTab" :entries="mailTabs"></ui-tabs>
<f-editor v-if="activeMailTab === 0" id="mail_top" v-model="single.mail_top" name="mail_top" label="E-Mail-Teil 1" rows="8" conditions required>
<template #conditions="{data, resolve}">
<conditions :single="single" :value="data" @save="resolve"> </conditions>
</template>
</f-editor>
<f-editor v-if="activeMailTab === 1" id="mail_bottom" v-model="single.mail_bottom" name="mail_bottom" label="E-Mail-Teil 2" rows="8" conditions required>
<template #conditions="{data, resolve}">
<conditions :single="single" :value="data" @save="resolve"> </conditions>
</template>
</f-editor>
</div>
<f-multiplefiles
id="mailattachments"
v-model="single.mailattachments"
@ -87,12 +99,11 @@
>
<template #buttons="{file, buttonClass, iconClass}">
<a v-tooltip="`Bedingungen`" href="#" :class="[buttonClass, 'bg-blue-200', 'relative']" @click.prevent="fileSettingPopup = file">
<div v-if="file.properties.conditions.length" class="absolute w-2 h-2 -mt-[0.05rem] -ml-[0.05rem] flex-none bg-red-900 rounded-full top-0 left-0"></div>
<div v-if="file.properties.conditions.ifs.length" class="absolute w-2 h-2 -mt-[0.05rem] -ml-[0.05rem] flex-none bg-red-900 rounded-full top-0 left-0"></div>
<ui-sprite src="setting" :class="[iconClass, 'text-blue-800']"></ui-sprite>
</a>
</template>
</f-multiplefiles>
<f-textarea id="mail_bottom" v-model="single.mail_bottom" name="mail_bottom" label="E-Mail-Teil 2" rows="8" required></f-textarea>
</div>
</div>
<template #actions>
@ -102,8 +113,8 @@
</template>
</ui-popup>
<ui-popup v-if="fileSettingPopup !== null" heading="Bedingungen bearbeiten" @close="fileSettingPopup = null">
<file-settings @close="fileSettingPopup = null" :single="single" :value="fileSettingPopup"></file-settings>
<ui-popup v-if="fileSettingPopup !== null" :heading="`Bedingungen für Datei ${fileSettingPopup.name}`" @close="fileSettingPopup = null">
<conditions :single="single" :value="fileSettingPopup.properties.conditions" @save="saveFileConditions"> </conditions>
</ui-popup>
<page-filter breakpoint="xl">
@ -149,27 +160,44 @@
</template>
<script setup>
import {ref} from 'vue';
import {ref, inject} from 'vue';
import {indexProps, useIndex} from '../../composables/useInertiaApiIndex.js';
import FormBuilder from '../formtemplate/FormBuilder.vue';
import Participants from './Participants.vue';
import FileSettings from './FileSettings.vue';
import Conditions from './Conditions.vue';
import {useToast} from 'vue-toastification';
const props = defineProps(indexProps);
var {meta, data, reloadPage, create, single, edit, cancel, submit, remove, getFilter, setFilter} = useIndex(props.data, 'form');
const axios = inject('axios');
const toast = useToast();
const active = ref(0);
const activeMailTab = ref(0);
const deleting = ref(null);
const showing = ref(null);
const fileSettingPopup = ref(null);
const tabs = [{title: 'Allgemeines'}, {title: 'Formular'}, {title: 'E-Mail'}, {title: 'Export'}];
const mailTabs = [{title: 'vor Daten'}, {title: 'nach Daten'}];
function setTemplate(template) {
active.value = 0;
single.value.config = template.config;
}
async function saveFileConditions(conditions) {
await axios.patch(`/mediaupload/${fileSettingPopup.value.id}`, {
properties: {
...fileSettingPopup.value.properties,
conditions: conditions,
},
});
fileSettingPopup.value = null;
toast.success('Datei aktualisiert');
}
function showParticipants(form) {
showing.value = form;
}

View File

@ -0,0 +1,31 @@
@foreach ($content as $block)
@if ($block['type'] === 'paragraph')
{!! $block['data']['text'] !!}
@endif
@if ($block['type'] === 'heading' && data_get($block, 'data.level', 2) === 2)
## {!! data_get($block, 'data.text') !!}
@endif
@if ($block['type'] === 'heading' && data_get($block, 'data.level', 2) === 3)
### {!! data_get($block, 'data.text') !!}
@endif
@if ($block['type'] === 'heading' && data_get($block, 'data.level', 2) === 4)
#### {!! data_get($block, 'data.text') !!}
@endif
@if ($block['type'] === 'list' && data_get($block, 'data.style', 'unordered') === 'unordered')
{!! collect(data_get($block, 'data.items', []))->map(fn ($item) => '* '.$item['content'])->implode("\n") !!}
@endif
@if ($block['type'] === 'list' && data_get($block, 'data.style', 'unordered') === 'ordered')
{!! collect(data_get($block, 'data.items', []))->map(fn ($item) => '1. '.$item['content'])->implode("\n") !!}
@endif
@if ($block['type'] === 'alert')
<x-mail::panel :type="$block['data']['type']">{!! data_get($block, 'data.message') !!}</x-mail::panel>
@endif
@endforeach

View File

@ -1,7 +1,8 @@
@component('mail::message')
<x-mail::message>
# Hallo {{$fullname}},
{{ $participant->form->mail_top }}
<x-mail-view::editor :content="$topText"></x-mail-view::editor>
# Deine Daten
@ -12,6 +13,6 @@
@endforeach
@endforeach
{{ $participant->form->mail_bottom }}
<x-mail-view::editor :content="$bottomText"></x-mail-view::editor>
@endcomponent
</x-mail::message>

View File

@ -1,13 +1,18 @@
<table class="action" align="center" width="100%" cellpadding="0" cellspacing="0" role="presentation">
@props([
'url',
'color' => 'primary',
'align' => 'center',
])
<table class="action" align="{{ $align }}" width="100%" cellpadding="0" cellspacing="0" role="presentation">
<tr>
<td align="center">
<td align="{{ $align }}">
<table width="100%" border="0" cellpadding="0" cellspacing="0" role="presentation">
<tr>
<td align="center">
<td align="{{ $align }}">
<table border="0" cellpadding="0" cellspacing="0" role="presentation">
<tr>
<td>
<a href="{{ $url }}" class="button button-{{ $color ?? 'primary' }}" target="_blank" rel="noopener">{{ $slot }}</a>
<a href="{{ $url }}" class="button button-{{ $color }}" target="_blank" rel="noopener">{{ $slot }}</a>
</td>
</tr>
</table>

View File

@ -1,7 +1,12 @@
@props(['url'])
<tr>
<td class="header">
<a href="{{ $url }}" style="display: inline-block;">
<img src="{{ url('/img/logo.png') }}" class="logo" alt="Stamm Silva Logo">
@if (trim($slot) === 'Laravel')
<img src="https://laravel.com/img/notification-logo.png" class="logo" alt="Laravel Logo">
@else
{{ $slot }}
@endif
</a>
</td>
</tr>

View File

@ -1,6 +1,7 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>{{ config('app.name') }}</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<meta name="color-scheme" content="light">
@ -33,7 +34,7 @@ width: 100% !important;
<!-- Email Body -->
<tr>
<td class="body" width="100%" cellpadding="0" cellspacing="0">
<td class="body" width="100%" cellpadding="0" cellspacing="0" style="border: hidden !important;">
<table class="inner-body" align="center" width="570" cellpadding="0" cellspacing="0" role="presentation">
<!-- Body content -->
<tr>

View File

@ -1,27 +1,27 @@
@component('mail::layout')
<x-mail::layout>
{{-- Header --}}
@slot('header')
@component('mail::header', ['url' => 'https://stamm-silva.de'])
<x-slot:header>
<x-mail::header :url="config('app.url')">
{{ config('app.name') }}
@endcomponent
@endslot
</x-mail::header>
</x-slot:header>
{{-- Body --}}
{{ $slot }}
{{-- Subcopy --}}
@isset($subcopy)
@slot('subcopy')
@component('mail::subcopy')
<x-slot:subcopy>
<x-mail::subcopy>
{{ $subcopy }}
@endcomponent
@endslot
</x-mail::subcopy>
</x-slot:subcopy>
@endisset
{{-- Footer --}}
@slot('footer')
@component('mail::footer')
© {{ date('Y') }} Stamm Silva.
@endcomponent
@endslot
@endcomponent
<x-slot:footer>
<x-mail::footer>
© {{ date('Y') }} {{ config('app.name') }}. @lang('All rights reserved.')
</x-mail::footer>
</x-slot:footer>
</x-mail::layout>

View File

@ -1,4 +1,4 @@
<table class="panel" width="100%" cellpadding="0" cellspacing="0" role="presentation">
<table class="panel panel-{{$type ?? 'default'}}" width="100%" cellpadding="0" cellspacing="0" role="presentation">
<tr>
<td class="panel-content">
<table width="100%" cellpadding="0" cellspacing="0" role="presentation">

View File

@ -3,8 +3,7 @@
body,
body *:not(html):not(style):not(br):not(tr):not(code) {
box-sizing: border-box;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif,
'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol';
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol';
position: relative;
}
@ -283,6 +282,46 @@ img {
padding-bottom: 0;
}
.panel.panel-danger {
border-left: #cf3917 solid 4px;
}
.panel.panel-danger .panel-content {
background-color: #ff8282;
}
.panel.panel-danger .panel-content p {
color: #250000;
}
.panel.panel-warning {
border-left: #cfb017 solid 4px;
}
.panel.panel-warning .panel-content {
background-color: #ffeb82;
}
.panel.panel-warning .panel-content p {
color: #252100;
}
.panel.panel-success {
border-left: #19cf17 solid 4px;
}
.panel.panel-success .panel-content {
background-color: #82ff87;
}
.panel.panel-success .panel-content p {
color: #002500;
}
.panel.panel-info {
border-left: #3417cf solid 4px;
}
.panel.panel-info .panel-content {
background-color: #9082ff;
}
.panel.panel-info .panel-content p {
color: #050025;
}
/* Utilities */
.break-all {

View File

@ -1,27 +1,27 @@
@component('mail::layout')
<x-mail::layout>
{{-- Header --}}
@slot('header')
@component('mail::header', ['url' => config('app.url')])
<x-slot:header>
<x-mail::header :url="config('app.url')">
{{ config('app.name') }}
@endcomponent
@endslot
</x-mail::header>
</x-slot:header>
{{-- Body --}}
{{ $slot }}
{{-- Subcopy --}}
@isset($subcopy)
@slot('subcopy')
@component('mail::subcopy')
<x-slot:subcopy>
<x-mail::subcopy>
{{ $subcopy }}
@endcomponent
@endslot
</x-mail::subcopy>
</x-slot:subcopy>
@endisset
{{-- Footer --}}
@slot('footer')
@component('mail::footer')
<x-slot:footer>
<x-mail::footer>
© {{ date('Y') }} {{ config('app.name') }}. @lang('All rights reserved.')
@endcomponent
@endslot
@endcomponent
</x-mail::footer>
</x-slot:footer>
</x-mail::layout>

View File

@ -9,6 +9,7 @@ use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Facades\Storage;
use Tests\EndToEndTestCase;
use Tests\Feature\Form\FormtemplateSectionRequest;
use Tests\RequestFactories\EditorRequestFactory;
class FormApiListActionTest extends FormTestCase
{
@ -24,7 +25,7 @@ class FormApiListActionTest extends FormTestCase
->name('lala 2')
->excerpt('fff')
->withImage('headerImage', 'lala-2.jpg')
->description('desc')
->description(EditorRequestFactory::new()->text(10, 'desc'))
->from('2023-05-05')
->to('2023-06-07')
->sections([FormtemplateSectionRequest::new()->name('sname')])
@ -37,7 +38,7 @@ class FormApiListActionTest extends FormTestCase
->assertJsonPath('data.0.config.sections.0.name', 'sname')
->assertJsonPath('data.0.id', $form->id)
->assertJsonPath('data.0.excerpt', 'fff')
->assertJsonPath('data.0.description', 'desc')
->assertJsonPath('data.0.description.blocks.0.data.text', 'desc')
->assertJsonPath('data.0.slug', 'lala-2')
->assertJsonPath('data.0.image', $form->getMedia('headerImage')->first()->getFullUrl('square'))
->assertJsonPath('data.0.dates', '05.05.2023 - 07.06.2023')
@ -57,7 +58,7 @@ class FormApiListActionTest extends FormTestCase
->create();
sleep(1);
$this->get('/api/form?perPage=15')->assertJsonPath('data.0.config.sections.0.fields.0.value', '');
$this->get('/api/form?perPage=15')->assertJsonPath('data.0.config.sections.0.fields.0.value', null);
}
public function testItDisplaysRemoteGroups(): void

View File

@ -7,6 +7,7 @@ use App\Form\Models\Formtemplate;
use App\Form\Models\Participant;
use Carbon\Carbon;
use Tests\Feature\Form\FormtemplateSectionRequest;
use Tests\RequestFactories\EditorRequestFactory;
class FormIndexActionTest extends FormTestCase
{
@ -19,11 +20,11 @@ class FormIndexActionTest extends FormTestCase
$form = Form::factory()
->name('lala')
->excerpt('fff')
->description('desc')
->description(EditorRequestFactory::new()->text(10, 'desc'))
->from('2023-05-05')
->to('2023-06-07')
->mailTop('Guten Tag')
->mailBottom('Cheers')
->mailTop(EditorRequestFactory::new()->text(10, 'Guten Tag'))
->mailBottom(EditorRequestFactory::new()->text(10, 'Cheers'))
->registrationFrom('2023-05-06 04:00:00')
->registrationUntil('2023-04-01 05:00:00')
->sections([FormtemplateSectionRequest::new()->name('sname')->fields([$this->textField()])])
@ -37,9 +38,9 @@ class FormIndexActionTest extends FormTestCase
->assertInertiaPath('data.data.0.config.sections.0.name', 'sname')
->assertInertiaPath('data.data.0.id', $form->id)
->assertInertiaPath('data.data.0.excerpt', 'fff')
->assertInertiaPath('data.data.0.description', 'desc')
->assertInertiaPath('data.data.0.mail_top', 'Guten Tag')
->assertInertiaPath('data.data.0.mail_bottom', 'Cheers')
->assertInertiaPath('data.data.0.description.blocks.0.data.text', 'desc')
->assertInertiaPath('data.data.0.mail_top.blocks.0.data.text', 'Guten Tag')
->assertInertiaPath('data.data.0.mail_bottom.blocks.0.data.text', 'Cheers')
->assertInertiaPath('data.data.0.from_human', '05.05.2023')
->assertInertiaPath('data.data.0.to_human', '07.06.2023')
->assertInertiaPath('data.data.0.from', '2023-05-05')

View File

@ -6,7 +6,9 @@ use App\Form\Enums\SpecialType;
use App\Form\Mails\ConfirmRegistrationMail;
use App\Form\Models\Form;
use App\Form\Models\Participant;
use Generator;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Tests\RequestFactories\EditorRequestFactory;
class FormRegisterMailTest extends FormTestCase
{
@ -25,7 +27,7 @@ class FormRegisterMailTest extends FormTestCase
])
])
->mailTop('mail top')->mailBottom('mail bottom')
->mailTop(EditorRequestFactory::new()->text(10, 'mail top'))->mailBottom(EditorRequestFactory::new()->text(11, 'mail bottom'))
)
->data(['vorname' => 'Max', 'nachname' => 'Muster'])
->create();
@ -74,4 +76,216 @@ class FormRegisterMailTest extends FormTestCase
$mail->assertHasAttachedData('content1', 'beispiel.pdf', ['mime' => 'application/pdf']);
$mail->assertHasAttachedData('content2', 'beispiel2.pdf', ['mime' => 'application/pdf']);
}
public function blockDataProvider(): Generator
{
yield [
['mode' => 'all', 'ifs' => []],
$this->dropdownField('fieldkey')->options(['A', 'B']),
['fieldkey' => 'A'],
true,
];
yield [
['mode' => 'any', 'ifs' => []],
$this->dropdownField('fieldkey')->options(['A', 'B']),
['fieldkey' => 'A'],
true,
];
yield [
['mode' => 'any', 'ifs' => []],
$this->dropdownField('fieldkey')->options(['A', 'B']),
['fieldkey' => 'A'],
true,
];
yield [
['mode' => 'any', 'ifs' => [
['field' => 'fieldkey', 'comparator' => 'isEqual', 'value' => 'A']
]],
$this->dropdownField('fieldkey')->options(['A', 'B']),
['fieldkey' => 'A'],
true,
];
yield [
['mode' => 'any', 'ifs' => [
['field' => 'fieldkey', 'comparator' => 'isEqual', 'value' => 'B']
]],
$this->dropdownField('fieldkey')->options(['A', 'B']),
['fieldkey' => 'A'],
false,
];
yield [
['mode' => 'any', 'ifs' => [
['field' => 'fieldkey', 'comparator' => 'isEqual', 'value' => 'B'],
['field' => 'fieldkey', 'comparator' => 'isEqual', 'value' => 'A']
]],
$this->dropdownField('fieldkey')->options(['A', 'B']),
['fieldkey' => 'A'],
true,
];
yield [
['mode' => 'any', 'ifs' => [
['field' => 'fieldkey', 'comparator' => 'isEqual', 'value' => 'B'],
['field' => 'fieldkey', 'comparator' => 'isEqual', 'value' => 'A']
]],
$this->dropdownField('fieldkey')->options(['A', 'B']),
['fieldkey' => 'B'],
true,
];
yield [
['mode' => 'all', 'ifs' => [
['field' => 'fieldkey', 'comparator' => 'isEqual', 'value' => 'B'],
['field' => 'fieldkey', 'comparator' => 'isEqual', 'value' => 'A']
]],
$this->dropdownField('fieldkey')->options(['A', 'B']),
['fieldkey' => 'B'],
false,
];
yield [
['mode' => 'all', 'ifs' => [
['field' => 'fieldkey', 'comparator' => 'isEqual', 'value' => 'B']
]],
$this->dropdownField('fieldkey')->options(['A', 'B']),
['fieldkey' => 'B'],
true,
];
yield [
['mode' => 'all', 'ifs' => [
['field' => 'fieldkey', 'comparator' => 'isNotEqual', 'value' => 'A']
]],
$this->dropdownField('fieldkey')->options(['A', 'B']),
['fieldkey' => 'B'],
true,
];
yield [
['mode' => 'all', 'ifs' => [
['field' => 'fieldkey', 'comparator' => 'isIn', 'value' => ['A']]
]],
$this->dropdownField('fieldkey')->options(['A', 'B']),
['fieldkey' => 'A'],
true,
];
yield [
['mode' => 'all', 'ifs' => [
['field' => 'fieldkey', 'comparator' => 'isNotIn', 'value' => ['B']]
]],
$this->dropdownField('fieldkey')->options(['A', 'B']),
['fieldkey' => 'A'],
true,
];
yield [
['mode' => 'all', 'ifs' => [
['field' => 'fieldkey', 'comparator' => 'isNotIn', 'value' => ['B']]
]],
$this->radioField('fieldkey')->options(['A', 'B']),
['fieldkey' => 'A'],
true,
];
yield [
['mode' => 'all', 'ifs' => [
['field' => 'fieldkey', 'comparator' => 'isEqual', 'value' => true]
]],
$this->checkboxField('fieldkey'),
['fieldkey' => true],
true,
];
yield [
['mode' => 'all', 'ifs' => [
['field' => 'fieldkey', 'comparator' => 'isEqual', 'value' => false]
]],
$this->checkboxField('fieldkey'),
['fieldkey' => true],
false,
];
}
/**
* @dataProvider blockDataProvider
* @param array<string, mixed> $conditions
* @param array<string, mixed> $participantValues
*/
public function testItFiltersForBlockConditions(array $conditions, FormtemplateFieldRequest $field, array $participantValues, bool $result): void
{
$this->login()->loginNami()->withoutExceptionHandling();
$participant = Participant::factory()->for(
Form::factory()
->fields([
$field,
$this->textField('firstname')->specialType(SpecialType::FIRSTNAME),
$this->textField('lastname')->specialType(SpecialType::LASTNAME),
])
->mailTop(EditorRequestFactory::new()->text(10, '::content::', $conditions))
)
->data(['firstname' => 'Max', 'lastname' => 'Muster', ...$participantValues])
->create();
$mail = new ConfirmRegistrationMail($participant);
if ($result) {
$mail->assertSeeInText('::content::');
} else {
$mail->assertDontSeeInText('::content::');
}
}
public function testItSendsEmailWhenMailTopIsEmpty(): void
{
$this->login()->loginNami()->withoutExceptionHandling();
$participant = Participant::factory()->for(
Form::factory()
->fields([
$this->textField('firstname')->specialType(SpecialType::FIRSTNAME),
$this->textField('lastname')->specialType(SpecialType::LASTNAME),
])->state(['mail_top' => []])
)
->data(['firstname' => 'Max', 'lastname' => 'Muster'])
->create();
$mail = new ConfirmRegistrationMail($participant);
$mail->assertSeeInText('Max');
}
/**
* @dataProvider blockDataProvider
* @param array<string, mixed> $conditions
* @param array<string, mixed> $participantValues
*/
public function testItFiltersForAttachments(array $conditions, FormtemplateFieldRequest $field, array $participantValues, bool $result): void
{
$this->login()->loginNami()->withoutExceptionHandling();
$participant = Participant::factory()->for(
Form::factory()
->fields([
$field,
$this->textField('firstname')->specialType(SpecialType::FIRSTNAME),
$this->textField('lastname')->specialType(SpecialType::LASTNAME),
])
->withDocument('mailattachments', 'beispiel.pdf', 'content', ['conditions' => $conditions])
)
->data(['firstname' => 'Max', 'lastname' => 'Muster', ...$participantValues])
->create();
$mail = new ConfirmRegistrationMail($participant);
$mail->assertSeeInHtml('Daten');
if ($result) {
$this->assertTrue($mail->hasAttachedData('content', 'beispiel.pdf', ['mime' => 'application/pdf']));
} else {
$this->assertFalse($mail->hasAttachedData('content', 'beispiel.pdf', ['mime' => 'application/pdf']));
}
}
}

View File

@ -12,8 +12,8 @@ use Worksome\RequestFactories\RequestFactory;
* @method self from(string $date)
* @method self to(string $date)
* @method self description(?EditorRequestFactory $description)
* @method self mailTop(string $content)
* @method self mailBottom(string $content)
* @method self mailTop(?EditorRequestFactory $content)
* @method self mailBottom(?EditorRequestFactory $content)
* @method self excerpt(string $description)
* @method self registrationFrom(string|null $date)
* @method self registrationUntil(string|null $date)
@ -40,8 +40,8 @@ class FormRequest extends RequestFactory
'to' => $this->faker->dateTime()->format('Y-m-d H:i:s'),
'registration_from' => $this->faker->dateTime()->format('Y-m-d H:i:s'),
'registration_until' => $this->faker->dateTime()->format('Y-m-d H:i:s'),
'mail_top' => $this->faker->text(),
'mail_bottom' => $this->faker->text(),
'mail_top' => EditorRequestFactory::new()->create(),
'mail_bottom' => EditorRequestFactory::new()->create(),
'header_image' => $this->getHeaderImagePayload(str()->uuid() . '.jpg'),
'mailattachments' => [],
];

View File

@ -26,8 +26,8 @@ class FormStoreActionTest extends FormTestCase
->description($description)
->excerpt('avff')
->registrationFrom('2023-05-04 01:00:00')->registrationUntil('2023-07-07 01:00:00')->from('2023-07-07')->to('2023-07-08')
->mailTop('Guten Tag')
->mailBottom('Viele Grüße')
->mailTop(EditorRequestFactory::new()->text(11, 'lala'))
->mailBottom(EditorRequestFactory::new()->text(12, 'lalab'))
->headerImage('htzz.jpg')
->sections([FormtemplateSectionRequest::new()->name('sname')->fields([$this->textField()->namiType(NamiType::BIRTHDAY)->forMembers(false)->hint('hhh')])])
->fake();
@ -39,8 +39,8 @@ class FormStoreActionTest extends FormTestCase
$this->assertEquals('formname', $form->name);
$this->assertEquals('avff', $form->excerpt);
$this->assertEquals($description->paragraphBlock(10, 'Lorem'), $form->description);
$this->assertEquals('Guten Tag', $form->mail_top);
$this->assertEquals('Viele Grüße', $form->mail_bottom);
$this->assertEquals(json_decode('{"time":1,"blocks":[{"id":11,"type":"paragraph","data":{"text":"lala"},"tunes":{"condition":{"mode":"all","ifs":[]}}}],"version":"1.0"}', true), $form->mail_top);
$this->assertEquals(json_decode('{"time":1,"blocks":[{"id":12,"type":"paragraph","data":{"text":"lalab"},"tunes":{"condition":{"mode":"all","ifs":[]}}}],"version":"1.0"}', true), $form->mail_bottom);
$this->assertEquals('2023-05-04 01:00', $form->registration_from->format('Y-m-d H:i'));
$this->assertEquals('2023-07-07 01:00', $form->registration_until->format('Y-m-d H:i'));
$this->assertEquals('2023-07-07', $form->from->format('Y-m-d'));

View File

@ -4,7 +4,6 @@ namespace Tests\Feature\Form;
use App\Form\Models\Form;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Facades\Storage;
class FormUpdateActionTest extends FormTestCase
{
@ -48,4 +47,22 @@ class FormUpdateActionTest extends FormTestCase
$this->patchJson(route('form.update', ['form' => $form]), $payload)->assertSessionDoesntHaveErrors()->assertOk();
$this->assertEquals(['firstname'], $form->fresh()->meta['active_columns']);
}
public function testItUpdatesActiveColumnsWhenFieldsAdded(): void
{
$this->login()->loginNami()->withoutExceptionHandling();
$form = Form::factory()
->sections([FormtemplateSectionRequest::new()->fields([])])
->create();
$payload = FormRequest::new()->sections([
FormtemplateSectionRequest::new()->fields([
$this->textField('firstname'),
$this->textField('geb'),
$this->textField('lastname'),
])
])->create();
$this->patchJson(route('form.update', ['form' => $form]), $payload)->assertSessionDoesntHaveErrors()->assertOk();
$this->assertEquals(['firstname', 'geb', 'lastname'], $form->fresh()->meta['active_columns']);
}
}

View File

@ -39,7 +39,7 @@ class FormtemplateFieldRequest extends RequestFactory
'columns' => ['mobile' => 2, 'tablet' => 4, 'desktop' => 6],
'nami_type' => null,
'for_members' => true,
'hint' => '',
'hint' => null,
];
}

View File

@ -54,7 +54,7 @@ class FormtemplateIndexActionTest extends TestCase
'name' => '',
'type' => 'TextField',
'columns' => ['mobile' => 2, 'tablet' => 4, 'desktop' => 6],
'value' => '',
'value' => null,
'nami_type' => null,
'for_members' => true,
'special_type' => null,

View File

@ -0,0 +1,25 @@
<?php
namespace Tests\Feature\Form;
use App\Form\Models\Form;
use Illuminate\Foundation\Testing\DatabaseTransactions;
class IsDirtyActionTest extends FormTestCase
{
use DatabaseTransactions;
public function testItChecksIfFormIsDirty(): void
{
$this->login()->loginNami();
$form = Form::factory()->fields([
$this->textField(),
])->create();
$this->postJson(route('form.is-dirty', ['form' => $form]), ['config' => $form->config->toArray()])->assertJsonPath('result', false);
$modifiedConfig = $form->config->toArray();
data_set($modifiedConfig, 'sections.0.name', 'mod');
$this->postJson(route('form.is-dirty', ['form' => $form]), ['config' => $modifiedConfig])->assertJsonPath('result', true);
}
}

View File

@ -20,21 +20,28 @@ class EditorRequestFactory extends RequestFactory
];
}
public function text(int $id, string $text): self
public function text(int $id, string $text, array $conditions = ['mode' => 'all', 'ifs' => []]): self
{
return $this->state($this->paragraphBlock($id, $text));
return $this->state($this->paragraphBlock($id, $text, $conditions));
}
/**
* @return array<string, mixed>
*/
public function paragraphBlock(int $id, string $text): array
public function paragraphBlock(int $id, string $text, array $conditions = ['mode' => 'all', 'ifs' => []]): array
{
return [
'time' => 1,
'version' => '1.0',
'blocks' => [
['id' => $id, 'type' => 'paragraph', 'data' => ['text' => $text]]
[
'id' => $id,
'type' => 'paragraph',
'data' => ['text' => $text],
'tunes' => [
'condition' => $conditions
]
]
],
];
}