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/build
/public/vendor /public/vendor
storage/*.key storage/*.key
vendor/ /vendor/
Homestead.yaml Homestead.yaml
Homestead.json Homestead.json
.vagrant/ .vagrant/

View File

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

View File

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

View File

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

View File

@ -70,7 +70,12 @@ class FieldCollection extends Collection
public function find(Field $givenField): ?Field 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; namespace App\Form\Fields;
use App\Form\Matchers\BooleanMatcher;
use App\Form\Matchers\Matcher;
use App\Form\Models\Form; use App\Form\Models\Form;
use App\Form\Models\Participant; use App\Form\Models\Participant;
use App\Form\Presenters\BooleanPresenter; use App\Form\Presenters\BooleanPresenter;
@ -79,4 +81,9 @@ class CheckboxField extends Field
{ {
return app(BooleanPresenter::class); return app(BooleanPresenter::class);
} }
public function getMatcher(): Matcher
{
return app(BooleanMatcher::class);
}
} }

View File

@ -2,6 +2,8 @@
namespace App\Form\Fields; namespace App\Form\Fields;
use App\Form\Matchers\Matcher;
use App\Form\Matchers\SingleValueMatcher;
use App\Form\Models\Form; use App\Form\Models\Form;
use App\Form\Models\Participant; use App\Form\Models\Participant;
use Faker\Generator; use Faker\Generator;
@ -80,4 +82,9 @@ class DropdownField extends Field
public function afterRegistration(Form $form, Participant $participant, array $input): void 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'; 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; namespace App\Form\Fields;
use App\Form\Matchers\Matcher;
use App\Form\Matchers\SingleValueMatcher;
use App\Form\Models\Form; use App\Form\Models\Form;
use App\Form\Models\Participant; use App\Form\Models\Participant;
use Faker\Generator; use Faker\Generator;
@ -80,4 +82,9 @@ class RadioField extends Field
public function afterRegistration(Form $form, Participant $participant, array $input): void 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 public static function fake(Generator $faker): array

View File

@ -3,6 +3,7 @@
namespace App\Form\Mails; namespace App\Form\Mails;
use App\Form\Data\FormConfigData; use App\Form\Data\FormConfigData;
use App\Form\Editor\FormConditionResolver;
use App\Form\Models\Participant; use App\Form\Models\Participant;
use Illuminate\Bus\Queueable; use Illuminate\Bus\Queueable;
use Illuminate\Mail\Attachment; use Illuminate\Mail\Attachment;
@ -10,7 +11,6 @@ use Illuminate\Mail\Mailable;
use Illuminate\Mail\Mailables\Content; use Illuminate\Mail\Mailables\Content;
use Illuminate\Mail\Mailables\Envelope; use Illuminate\Mail\Mailables\Envelope;
use Illuminate\Queue\SerializesModels; use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Storage;
class ConfirmRegistrationMail extends Mailable class ConfirmRegistrationMail extends Mailable
{ {
@ -18,6 +18,10 @@ class ConfirmRegistrationMail extends Mailable
public string $fullname; public string $fullname;
public FormConfigData $config; public FormConfigData $config;
/** @var array<string, mixed> */
public array $topText;
/** @var array<string, mixed> */
public array $bottomText;
/** /**
* Create a new message instance. * Create a new message instance.
@ -26,8 +30,11 @@ class ConfirmRegistrationMail extends Mailable
*/ */
public function __construct(public Participant $participant) public function __construct(public Participant $participant)
{ {
$conditionResolver = app(FormConditionResolver::class)->forParticipant($participant);
$this->fullname = $participant->getFields()->getFullname(); $this->fullname = $participant->getFields()->getFullname();
$this->config = $participant->getConfig(); $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() public function attachments()
{ {
$conditionResolver = app(FormConditionResolver::class)->forParticipant($this->participant);
return $this->participant->form->getMedia('mailattachments') 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())) ->map(fn ($media) => Attachment::fromStorageDisk($media->disk, $media->getPathRelativeToRoot()))
->all(); ->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, 'config' => FormConfigData::class,
'meta' => 'json', 'meta' => 'json',
'description' => 'json', 'description' => 'json',
'mail_top' => 'json',
'mail_bottom' => 'json',
]; ];
/** @var array<int, string> */ /** @var array<int, string> */
@ -64,13 +66,17 @@ class Form extends Model implements HasMedia
}); });
$this->addMediaCollection('mailattachments') $this->addMediaCollection('mailattachments')
->withDefaultProperties(fn () => [ ->withDefaultProperties(fn () => [
'conditions' => [], 'conditions' => [
'mode' => 'all',
'ifs' => []
],
]) ])
->withPropertyValidation(fn () => [ ->withPropertyValidation(fn () => [
'conditions' => 'array', 'conditions.mode' => 'required|string|in:all,any',
'conditions.*.field' => 'required', 'conditions.ifs' => 'array',
'conditions.*.comparator' => 'required', 'conditions.ifs.*.field' => 'required',
'conditions.*.value' => 'present', 'conditions.ifs.*.comparator' => 'required',
'conditions.ifs.*.value' => 'present',
]); ]);
} }
@ -133,11 +139,12 @@ class Form extends Model implements HasMedia
public static function booted(): void public static function booted(): void
{ {
static::saving(function (self $model) { static::saving(function (self $model) {
if (is_null($model->meta)) { if (is_null(data_get($model->meta, 'active_columns'))) {
$model->setAttribute('meta', [ $model->setAttribute('meta', [
'active_columns' => $model->getFields()->count() ? $model->getFields()->take(4)->pluck('key')->toArray() : null, 'active_columns' => $model->getFields()->count() ? $model->getFields()->take(4)->pluck('key')->toArray() : null,
'sorting' => $model->getFields()->count() ? [$model->getFields()->first()->key, 'asc'] : null, 'sorting' => $model->getFields()->count() ? [$model->getFields()->first()->key, 'asc'] : null,
]); ]);
return;
} }
if (is_array(data_get($model->meta, 'active_columns'))) { if (is_array(data_get($model->meta, 'active_columns'))) {
@ -145,6 +152,7 @@ class Form extends Model implements HasMedia
...$model->meta, ...$model->meta,
'active_columns' => array_values(array_intersect([...$model->getFields()->pluck('key')->toArray(), 'created_at'], $model->meta['active_columns'])), '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, 'to' => null,
'registration_from' => null, 'registration_from' => null,
'registration_until' => null, 'registration_until' => null,
'mail_top' => null, 'mail_top' => [],
'mail_bottom' => null, 'mail_bottom' => [],
'config' => null, 'config' => null,
'header_image' => null, 'header_image' => null,
'mailattachments' => [], '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 App\Mailgateway\Types\MailmanType;
use Illuminate\Http\RedirectResponse; use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Resources\Json\JsonResource; use Illuminate\Http\Resources\Json\JsonResource;
use Illuminate\Support\Facades\Blade;
use Illuminate\Support\ServiceProvider; use Illuminate\Support\ServiceProvider;
use Laravel\Telescope\Telescope; use Laravel\Telescope\Telescope;
@ -36,6 +37,8 @@ class AppServiceProvider extends ServiceProvider
])); ]));
app()->extend('media-library-helpers', fn ($p) => $p->put('form', Form::class)); 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 Illuminate\Database\Eloquent\Factories\Factory;
use Tests\Feature\Form\FormtemplateFieldRequest; use Tests\Feature\Form\FormtemplateFieldRequest;
use Tests\Feature\Form\FormtemplateSectionRequest; use Tests\Feature\Form\FormtemplateSectionRequest;
use Tests\RequestFactories\EditorRequestFactory;
/** /**
* @extends Factory<Form> * @extends Factory<Form>
* @method self name(string $name) * @method self name(string $name)
* @method self from(string $from) * @method self from(string $from)
* @method self to(string $to) * @method self to(string $to)
* @method self mailTop(string $content)
* @method self mailBottom(string $content)
* @method self excerpt(string $excerpt) * @method self excerpt(string $excerpt)
* @method self description(string $description)
* @method self registrationFrom(string|null $date) * @method self registrationFrom(string|null $date)
* @method self registrationUntil(string|null $date) * @method self registrationUntil(string|null $date)
*/ */
@ -40,15 +38,15 @@ class FormFactory extends Factory
{ {
return [ return [
'name' => $this->faker->words(4, true), 'name' => $this->faker->words(4, true),
'description' => $this->faker->text(), 'description' => EditorRequestFactory::new()->create(),
'excerpt' => $this->faker->words(10, true), 'excerpt' => $this->faker->words(10, true),
'config' => ['sections' => []], 'config' => ['sections' => []],
'from' => $this->faker->dateTimeBetween('+1 week', '+4 weeks')->format('Y-m-d H:i:s'), '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'), '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_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'), 'registration_until' => $this->faker->dateTimeBetween('now', '+2 weeks')->format('Y-m-d H:i:s'),
'mail_top' => $this->faker->text(), 'mail_top' => EditorRequestFactory::new()->create(),
'mail_bottom' => $this->faker->text(), 'mail_bottom' => EditorRequestFactory::new()->create(),
]; ];
} }
@ -75,4 +73,19 @@ class FormFactory extends Factory
{ {
return $this->state([str($method)->snake()->toString() => $parameters[0]]); 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); $pathinfo = pathinfo($filename);
UploadedFile::fake()->create($filename, $content, 'application/pdf')->storeAs('media-library', $filename, 'temp'); UploadedFile::fake()->create($filename, $content, 'application/pdf')->storeAs('media-library', $filename, 'temp');
@ -32,6 +32,7 @@ trait FakesMedia
$model->addMediaFromDisk('media-library/' . $filename, 'temp') $model->addMediaFromDisk('media-library/' . $filename, 'temp')
->usingName($pathinfo['filename']) ->usingName($pathinfo['filename'])
->usingFileName($pathinfo['basename']) ->usingFileName($pathinfo['basename'])
->withCustomProperties($properties)
->toMediaCollection($collection); ->toMediaCollection($collection);
}); });
} }

View File

@ -1,12 +1,25 @@
<template> <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>
<div class="relative w-full h-full"> <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 :id="id" :class="[defaultFieldClass, fieldClass(size)]"></div> <div class="relative w-full h-full">
<div v-if="hint" v-tooltip="hint" class="absolute right-0 top-0 mr-2 mt-2"> <div :id="id" :class="[defaultFieldClass, fieldClass(size)]"></div>
<ui-sprite src="info-button" class="w-5 h-5 text-indigo-200"></ui-sprite> <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>
</div>
</div> </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> </div>
</template> </template>
@ -39,6 +52,11 @@ const props = defineProps({
id: { id: {
required: true, required: true,
}, },
conditions: {
required: false,
type: Boolean,
default: () => false,
},
hint: { hint: {
default: null, default: null,
}, },
@ -54,51 +72,145 @@ const props = defineProps({
}); });
const editor = ref(null); 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 () => { onMounted(async () => {
var tools = {
paragraph: {
class: Paragraph,
shortcut: 'CTRL+P',
inlineToolbar: true,
config: {
preserveBlank: true,
placeholder: 'Absatz',
},
},
alert: {
class: Alert,
inlineToolbar: true,
config: {
defaultType: 'primary',
},
},
heading: {
class: Header,
shortcut: 'CTRL+H',
inlineToolbar: true,
config: {
placeholder: 'Überschrift',
levels: [2, 3, 4],
defaultLevel: 2,
},
},
list: {
class: NestedList,
shortcut: 'CTRL+L',
inlineToolbar: true,
},
};
var tunes = [];
if (props.conditions) {
tools.condition = {
class: ConditionTune,
};
tunes.push('condition');
}
editor.value = new EditorJS({ editor.value = new EditorJS({
placeholder: props.placeholder, placeholder: props.placeholder,
holder: props.id, holder: props.id,
minHeight: 0, minHeight: 0,
defaultBlock: 'paragraph', defaultBlock: 'paragraph',
data: JSON.parse(JSON.stringify(props.modelValue)), data: JSON.parse(JSON.stringify(props.modelValue)),
tools: { tunes: tunes,
paragraph: { tools: tools,
class: Paragraph,
shortcut: 'CTRL+P',
inlineToolbar: true,
config: {
preserveBlank: true,
placeholder: 'Absatz',
},
},
alert: {
class: Alert,
inlineToolbar: true,
config: {
defaultType: 'primary',
},
},
heading: {
class: Header,
shortcut: 'CTRL+H',
inlineToolbar: true,
config: {
placeholder: 'Überschrift',
levels: [2, 3, 4],
defaultLevel: 2,
},
},
list: {
class: NestedList,
shortcut: 'CTRL+L',
inlineToolbar: true,
},
},
onChange: debounce(async (api, event) => { onChange: debounce(async (api, event) => {
const data = await editor.value.save(); const data = await editor.value.save();
console.log(data);
emit('update:modelValue', data); emit('update:modelValue', data);
}, 500), }, 200),
onPopup: () => {
console.log('opened');
},
}); });
await editor.value.isReady; await editor.value.isReady;
console.log('Editor is ready'); console.log('Editor is ready');

View File

@ -5,21 +5,29 @@
</ui-note> </ui-note>
<div v-else> <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> <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"> <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}`" v-model="condition.field" :options="fieldOptions" :name="`field-${index}`" label="Feld"></f-select> <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 <f-select
:id="`comparator-${index}`" :id="`comparator-${index}`"
:options="comparatorOptions" :options="comparatorOptions"
:model-value="condition.comparator" :model-value="condition.comparator"
:name="`comparator-${index}`" :name="`comparator-${index}`"
label="Vergleich" label="Vergleich"
@update:model-value="updateComparator(condition, $event)" @update:model-value="update(index, 'comparator', $event)"
></f-select> ></f-select>
<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}`" :id="`value-${index}`"
v-model="condition.value" v-model="condition.value"
:options="getOptions(condition.field)" :options="getOptions(condition.field)"
@ -27,14 +35,21 @@
label="Wert" label="Wert"
></f-select> ></f-select>
<f-multipleselect <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}`" :id="`value-${index}`"
v-model="condition.value" v-model="condition.value"
:options="getOptions(condition.field)" :options="getOptions(condition.field)"
:name="`value-${index}`" :name="`value-${index}`"
label="Wert" label="Wert"
></f-multipleselect> ></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> </div>
<ui-icon-button class="mt-4 mb-2" icon="save" @click="save">Speichern</ui-icon-button> <ui-icon-button class="mt-4 mb-2" icon="save" @click="save">Speichern</ui-icon-button>
@ -44,7 +59,7 @@
<script setup> <script setup>
import {ref, inject, computed} from 'vue'; import {ref, inject, computed} from 'vue';
const axios = inject('axios'); const axios = inject('axios');
const emit = defineEmits(['close']); const emit = defineEmits(['save']);
const props = defineProps({ const props = defineProps({
value: { value: {
@ -56,10 +71,15 @@ const props = defineProps({
}); });
const comparatorOptions = ref([ const comparatorOptions = ref([
{id: 'isEqual', name: 'ist gleich', defaultValue: null}, {id: 'isEqual', name: 'ist gleich', defaultValue: {DropdownField: null, RadioField: null, CheckboxField: false}},
{id: 'isNotEqual', name: 'ist ungleich', defaultValue: null}, {id: 'isNotEqual', name: 'ist ungleich', defaultValue: {DropdownField: null, RadioField: null, CheckboxField: false}},
{id: 'isIn', name: 'ist in', defaultValue: []}, {id: 'isIn', name: 'ist in', defaultValue: {DropdownField: [], RadioField: [], CheckboxField: false}},
{id: 'isNotIn', name: 'ist nicht in', defaultValue: []}, {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(() => { const fields = computed(() => {
@ -75,9 +95,23 @@ const fields = computed(() => {
return result; return result;
}); });
function updateComparator(condition, comparator) { function update(index, key, value) {
condition.value = comparatorOptions.value.find((c) => c.id === comparator).defaultValue; if (key === 'comparator') {
condition.comparator = 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) { 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); const locked = ref(false);
function addCondition() { function addCondition() {
conditions.value.push({ inner.value.ifs.push({
field: null, field: null,
comparator: null, comparator: null,
value: null, value: null,
@ -109,14 +143,7 @@ function addCondition() {
} }
async function save() { async function save() {
await axios.patch(`/mediaupload/${props.value.id}`, { emit('save', inner.value);
properties: {
...props.value.properties,
conditions: conditions.value,
},
});
emit('close');
} }
async function checkIfDirty() { async function checkIfDirty() {

View File

@ -63,7 +63,7 @@
></f-textarea> ></f-textarea>
<f-editor id="description" v-model="single.description" name="description" label="Beschreibung" rows="10" required></f-editor> <f-editor id="description" v-model="single.description" name="description" label="Beschreibung" rows="10" required></f-editor>
</div> </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> <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> <form-builder v-model="single.config" :meta="meta"></form-builder>
</div> </div>
@ -74,7 +74,19 @@
Die Anrede ("Hallo Max Mustermann") wird automatisch an den Anfang gesetzt.<br /> Die Anrede ("Hallo Max Mustermann") wird automatisch an den Anfang gesetzt.<br />
Außerdem kannst du Dateien hochladen, die automatisch mit angehangen werden. Außerdem kannst du Dateien hochladen, die automatisch mit angehangen werden.
</ui-note> </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 <f-multiplefiles
id="mailattachments" id="mailattachments"
v-model="single.mailattachments" v-model="single.mailattachments"
@ -87,12 +99,11 @@
> >
<template #buttons="{file, buttonClass, iconClass}"> <template #buttons="{file, buttonClass, iconClass}">
<a v-tooltip="`Bedingungen`" href="#" :class="[buttonClass, 'bg-blue-200', 'relative']" @click.prevent="fileSettingPopup = file"> <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> <ui-sprite src="setting" :class="[iconClass, 'text-blue-800']"></ui-sprite>
</a> </a>
</template> </template>
</f-multiplefiles> </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>
</div> </div>
<template #actions> <template #actions>
@ -102,8 +113,8 @@
</template> </template>
</ui-popup> </ui-popup>
<ui-popup v-if="fileSettingPopup !== null" heading="Bedingungen bearbeiten" @close="fileSettingPopup = null"> <ui-popup v-if="fileSettingPopup !== null" :heading="`Bedingungen für Datei ${fileSettingPopup.name}`" @close="fileSettingPopup = null">
<file-settings @close="fileSettingPopup = null" :single="single" :value="fileSettingPopup"></file-settings> <conditions :single="single" :value="fileSettingPopup.properties.conditions" @save="saveFileConditions"> </conditions>
</ui-popup> </ui-popup>
<page-filter breakpoint="xl"> <page-filter breakpoint="xl">
@ -149,27 +160,44 @@
</template> </template>
<script setup> <script setup>
import {ref} from 'vue'; import {ref, inject} from 'vue';
import {indexProps, useIndex} from '../../composables/useInertiaApiIndex.js'; import {indexProps, useIndex} from '../../composables/useInertiaApiIndex.js';
import FormBuilder from '../formtemplate/FormBuilder.vue'; import FormBuilder from '../formtemplate/FormBuilder.vue';
import Participants from './Participants.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); const props = defineProps(indexProps);
var {meta, data, reloadPage, create, single, edit, cancel, submit, remove, getFilter, setFilter} = useIndex(props.data, 'form'); 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 active = ref(0);
const activeMailTab = ref(0);
const deleting = ref(null); const deleting = ref(null);
const showing = ref(null); const showing = ref(null);
const fileSettingPopup = ref(null); const fileSettingPopup = ref(null);
const tabs = [{title: 'Allgemeines'}, {title: 'Formular'}, {title: 'E-Mail'}, {title: 'Export'}]; const tabs = [{title: 'Allgemeines'}, {title: 'Formular'}, {title: 'E-Mail'}, {title: 'Export'}];
const mailTabs = [{title: 'vor Daten'}, {title: 'nach Daten'}];
function setTemplate(template) { function setTemplate(template) {
active.value = 0; active.value = 0;
single.value.config = template.config; 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) { function showParticipants(form) {
showing.value = 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}}, # Hallo {{$fullname}},
{{ $participant->form->mail_top }} <x-mail-view::editor :content="$topText"></x-mail-view::editor>
# Deine Daten # Deine Daten
@ -12,6 +13,6 @@
@endforeach @endforeach
@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> <tr>
<td align="center"> <td align="{{ $align }}">
<table width="100%" border="0" cellpadding="0" cellspacing="0" role="presentation"> <table width="100%" border="0" cellpadding="0" cellspacing="0" role="presentation">
<tr> <tr>
<td align="center"> <td align="{{ $align }}">
<table border="0" cellpadding="0" cellspacing="0" role="presentation"> <table border="0" cellpadding="0" cellspacing="0" role="presentation">
<tr> <tr>
<td> <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> </td>
</tr> </tr>
</table> </table>

View File

@ -1,7 +1,12 @@
@props(['url'])
<tr> <tr>
<td class="header"> <td class="header">
<a href="{{ $url }}" style="display: inline-block;"> <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> </a>
</td> </td>
</tr> </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"> <!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"> <html xmlns="http://www.w3.org/1999/xhtml">
<head> <head>
<title>{{ config('app.name') }}</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<meta name="color-scheme" content="light"> <meta name="color-scheme" content="light">
@ -33,7 +34,7 @@ width: 100% !important;
<!-- Email Body --> <!-- Email Body -->
<tr> <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"> <table class="inner-body" align="center" width="570" cellpadding="0" cellspacing="0" role="presentation">
<!-- Body content --> <!-- Body content -->
<tr> <tr>

View File

@ -1,27 +1,27 @@
@component('mail::layout') <x-mail::layout>
{{-- Header --}} {{-- Header --}}
@slot('header') <x-slot:header>
@component('mail::header', ['url' => 'https://stamm-silva.de']) <x-mail::header :url="config('app.url')">
{{ config('app.name') }} {{ config('app.name') }}
@endcomponent </x-mail::header>
@endslot </x-slot:header>
{{-- Body --}} {{-- Body --}}
{{ $slot }} {{ $slot }}
{{-- Subcopy --}} {{-- Subcopy --}}
@isset($subcopy) @isset($subcopy)
@slot('subcopy') <x-slot:subcopy>
@component('mail::subcopy') <x-mail::subcopy>
{{ $subcopy }} {{ $subcopy }}
@endcomponent </x-mail::subcopy>
@endslot </x-slot:subcopy>
@endisset @endisset
{{-- Footer --}} {{-- Footer --}}
@slot('footer') <x-slot:footer>
@component('mail::footer') <x-mail::footer>
© {{ date('Y') }} Stamm Silva. © {{ date('Y') }} {{ config('app.name') }}. @lang('All rights reserved.')
@endcomponent </x-mail::footer>
@endslot </x-slot:footer>
@endcomponent </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> <tr>
<td class="panel-content"> <td class="panel-content">
<table width="100%" cellpadding="0" cellspacing="0" role="presentation"> <table width="100%" cellpadding="0" cellspacing="0" role="presentation">

View File

@ -3,8 +3,7 @@
body, body,
body *:not(html):not(style):not(br):not(tr):not(code) { body *:not(html):not(style):not(br):not(tr):not(code) {
box-sizing: border-box; box-sizing: border-box;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol';
'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol';
position: relative; position: relative;
} }
@ -283,6 +282,46 @@ img {
padding-bottom: 0; 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 */ /* Utilities */
.break-all { .break-all {

View File

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

View File

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

View File

@ -7,6 +7,7 @@ use App\Form\Models\Formtemplate;
use App\Form\Models\Participant; use App\Form\Models\Participant;
use Carbon\Carbon; use Carbon\Carbon;
use Tests\Feature\Form\FormtemplateSectionRequest; use Tests\Feature\Form\FormtemplateSectionRequest;
use Tests\RequestFactories\EditorRequestFactory;
class FormIndexActionTest extends FormTestCase class FormIndexActionTest extends FormTestCase
{ {
@ -19,11 +20,11 @@ class FormIndexActionTest extends FormTestCase
$form = Form::factory() $form = Form::factory()
->name('lala') ->name('lala')
->excerpt('fff') ->excerpt('fff')
->description('desc') ->description(EditorRequestFactory::new()->text(10, 'desc'))
->from('2023-05-05') ->from('2023-05-05')
->to('2023-06-07') ->to('2023-06-07')
->mailTop('Guten Tag') ->mailTop(EditorRequestFactory::new()->text(10, 'Guten Tag'))
->mailBottom('Cheers') ->mailBottom(EditorRequestFactory::new()->text(10, 'Cheers'))
->registrationFrom('2023-05-06 04:00:00') ->registrationFrom('2023-05-06 04:00:00')
->registrationUntil('2023-04-01 05:00:00') ->registrationUntil('2023-04-01 05:00:00')
->sections([FormtemplateSectionRequest::new()->name('sname')->fields([$this->textField()])]) ->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.config.sections.0.name', 'sname')
->assertInertiaPath('data.data.0.id', $form->id) ->assertInertiaPath('data.data.0.id', $form->id)
->assertInertiaPath('data.data.0.excerpt', 'fff') ->assertInertiaPath('data.data.0.excerpt', 'fff')
->assertInertiaPath('data.data.0.description', 'desc') ->assertInertiaPath('data.data.0.description.blocks.0.data.text', 'desc')
->assertInertiaPath('data.data.0.mail_top', 'Guten Tag') ->assertInertiaPath('data.data.0.mail_top.blocks.0.data.text', 'Guten Tag')
->assertInertiaPath('data.data.0.mail_bottom', 'Cheers') ->assertInertiaPath('data.data.0.mail_bottom.blocks.0.data.text', 'Cheers')
->assertInertiaPath('data.data.0.from_human', '05.05.2023') ->assertInertiaPath('data.data.0.from_human', '05.05.2023')
->assertInertiaPath('data.data.0.to_human', '07.06.2023') ->assertInertiaPath('data.data.0.to_human', '07.06.2023')
->assertInertiaPath('data.data.0.from', '2023-05-05') ->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\Mails\ConfirmRegistrationMail;
use App\Form\Models\Form; use App\Form\Models\Form;
use App\Form\Models\Participant; use App\Form\Models\Participant;
use Generator;
use Illuminate\Foundation\Testing\DatabaseTransactions; use Illuminate\Foundation\Testing\DatabaseTransactions;
use Tests\RequestFactories\EditorRequestFactory;
class FormRegisterMailTest extends FormTestCase 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']) ->data(['vorname' => 'Max', 'nachname' => 'Muster'])
->create(); ->create();
@ -74,4 +76,216 @@ class FormRegisterMailTest extends FormTestCase
$mail->assertHasAttachedData('content1', 'beispiel.pdf', ['mime' => 'application/pdf']); $mail->assertHasAttachedData('content1', 'beispiel.pdf', ['mime' => 'application/pdf']);
$mail->assertHasAttachedData('content2', 'beispiel2.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 from(string $date)
* @method self to(string $date) * @method self to(string $date)
* @method self description(?EditorRequestFactory $description) * @method self description(?EditorRequestFactory $description)
* @method self mailTop(string $content) * @method self mailTop(?EditorRequestFactory $content)
* @method self mailBottom(string $content) * @method self mailBottom(?EditorRequestFactory $content)
* @method self excerpt(string $description) * @method self excerpt(string $description)
* @method self registrationFrom(string|null $date) * @method self registrationFrom(string|null $date)
* @method self registrationUntil(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'), 'to' => $this->faker->dateTime()->format('Y-m-d H:i:s'),
'registration_from' => $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'), 'registration_until' => $this->faker->dateTime()->format('Y-m-d H:i:s'),
'mail_top' => $this->faker->text(), 'mail_top' => EditorRequestFactory::new()->create(),
'mail_bottom' => $this->faker->text(), 'mail_bottom' => EditorRequestFactory::new()->create(),
'header_image' => $this->getHeaderImagePayload(str()->uuid() . '.jpg'), 'header_image' => $this->getHeaderImagePayload(str()->uuid() . '.jpg'),
'mailattachments' => [], 'mailattachments' => [],
]; ];

View File

@ -26,8 +26,8 @@ class FormStoreActionTest extends FormTestCase
->description($description) ->description($description)
->excerpt('avff') ->excerpt('avff')
->registrationFrom('2023-05-04 01:00:00')->registrationUntil('2023-07-07 01:00:00')->from('2023-07-07')->to('2023-07-08') ->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') ->mailTop(EditorRequestFactory::new()->text(11, 'lala'))
->mailBottom('Viele Grüße') ->mailBottom(EditorRequestFactory::new()->text(12, 'lalab'))
->headerImage('htzz.jpg') ->headerImage('htzz.jpg')
->sections([FormtemplateSectionRequest::new()->name('sname')->fields([$this->textField()->namiType(NamiType::BIRTHDAY)->forMembers(false)->hint('hhh')])]) ->sections([FormtemplateSectionRequest::new()->name('sname')->fields([$this->textField()->namiType(NamiType::BIRTHDAY)->forMembers(false)->hint('hhh')])])
->fake(); ->fake();
@ -39,8 +39,8 @@ class FormStoreActionTest extends FormTestCase
$this->assertEquals('formname', $form->name); $this->assertEquals('formname', $form->name);
$this->assertEquals('avff', $form->excerpt); $this->assertEquals('avff', $form->excerpt);
$this->assertEquals($description->paragraphBlock(10, 'Lorem'), $form->description); $this->assertEquals($description->paragraphBlock(10, 'Lorem'), $form->description);
$this->assertEquals('Guten Tag', $form->mail_top); $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('Viele Grüße', $form->mail_bottom); $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-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 01:00', $form->registration_until->format('Y-m-d H:i'));
$this->assertEquals('2023-07-07', $form->from->format('Y-m-d')); $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 App\Form\Models\Form;
use Illuminate\Foundation\Testing\DatabaseTransactions; use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Facades\Storage;
class FormUpdateActionTest extends FormTestCase class FormUpdateActionTest extends FormTestCase
{ {
@ -48,4 +47,22 @@ class FormUpdateActionTest extends FormTestCase
$this->patchJson(route('form.update', ['form' => $form]), $payload)->assertSessionDoesntHaveErrors()->assertOk(); $this->patchJson(route('form.update', ['form' => $form]), $payload)->assertSessionDoesntHaveErrors()->assertOk();
$this->assertEquals(['firstname'], $form->fresh()->meta['active_columns']); $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], 'columns' => ['mobile' => 2, 'tablet' => 4, 'desktop' => 6],
'nami_type' => null, 'nami_type' => null,
'for_members' => true, 'for_members' => true,
'hint' => '', 'hint' => null,
]; ];
} }

View File

@ -54,7 +54,7 @@ class FormtemplateIndexActionTest extends TestCase
'name' => '', 'name' => '',
'type' => 'TextField', 'type' => 'TextField',
'columns' => ['mobile' => 2, 'tablet' => 4, 'desktop' => 6], 'columns' => ['mobile' => 2, 'tablet' => 4, 'desktop' => 6],
'value' => '', 'value' => null,
'nami_type' => null, 'nami_type' => null,
'for_members' => true, 'for_members' => true,
'special_type' => null, '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> * @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 [ return [
'time' => 1, 'time' => 1,
'version' => '1.0', 'version' => '1.0',
'blocks' => [ 'blocks' => [
['id' => $id, 'type' => 'paragraph', 'data' => ['text' => $text]] [
'id' => $id,
'type' => 'paragraph',
'data' => ['text' => $text],
'tunes' => [
'condition' => $conditions
]
]
], ],
]; ];
} }