Add prevention settings

This commit is contained in:
philipp lang 2024-12-22 18:34:00 +01:00
parent ca9bf70c9a
commit 1d09afbeb3
14 changed files with 387 additions and 106 deletions

View File

@ -2,12 +2,16 @@
namespace App\Lib\Editor;
use Livewire\Wireable;
use Spatie\LaravelData\Concerns\WireableData;
use Spatie\LaravelData\Data;
/** @todo replace blocks with actual block data classes */
class EditorData extends Data implements Editorable
class EditorData extends Data implements Editorable, Wireable
{
use WireableData;
/** @param array<int, mixed> $blocks */
public function __construct(
public string $version,

View File

@ -1,19 +0,0 @@
<?php
namespace App\Prevention\Actions;
use App\Prevention\PreventionSettings;
use Illuminate\Http\JsonResponse;
use Lorisleiva\Actions\Concerns\AsAction;
class SettingApiAction
{
use AsAction;
public function handle(): JsonResponse
{
return response()->json([
'data' => app(PreventionSettings::class)->toArray(),
]);
}
}

View File

@ -1,33 +0,0 @@
<?php
namespace App\Prevention\Actions;
use App\Lib\Editor\EditorData;
use App\Lib\Events\Succeeded;
use App\Prevention\PreventionSettings;
use Lorisleiva\Actions\ActionRequest;
use Lorisleiva\Actions\Concerns\AsAction;
class SettingStoreAction
{
use AsAction;
/**
* @return array<string, string>
*/
public function rules(): array
{
return [
'formmail' => 'array',
];
}
public function handle(ActionRequest $request): void
{
$settings = app(PreventionSettings::class);
$settings->formmail = EditorData::from($request->formmail);
$settings->save();
Succeeded::message('Einstellungen gespeichert.')->dispatch();
}
}

View File

@ -2,10 +2,7 @@
namespace App\Setting;
use App\Fileshare\FileshareSettings;
use App\Form\FormSettings;
use Modules\Module\ModuleSettings;
use App\Prevention\PreventionSettings;
use App\Setting\Data\SettingSynthesizer;
use Illuminate\Routing\Router;
use Illuminate\Support\ServiceProvider;
@ -35,8 +32,6 @@ class SettingServiceProvider extends ServiceProvider
app(SettingFactory::class)->register(ModuleSettings::class);
app(SettingFactory::class)->register(InvoiceSettings::class);
app(SettingFactory::class)->register(NamiSettings::class);
app(SettingFactory::class)->register(FileshareSettings::class);
app(SettingFactory::class)->register(PreventionSettings::class);
Livewire::propertySynthesizer(SettingSynthesizer::class);
}

52
app/View/Form/Editor.php Normal file
View File

@ -0,0 +1,52 @@
<?php
namespace App\View\Form;
use App\View\Traits\HasFormDimensions;
use Illuminate\View\Component;
class Editor extends Component
{
use HasFormDimensions;
public string $id;
public function __construct(
public string $name,
public string $size = 'default',
public ?string $hint = null,
public bool $required = false,
public string $label = '',
public array $conditions = [],
) {
$this->id = str()->uuid()->toString();
}
public function render()
{
return <<<'HTML'
<div class="flex flex-col group {{$heightClass}}" x-data="">
@if ($label)
<x-form::label :required="$required">{{$label}}</x-form::label>
@endif
<div class="relative w-full h-full">
<div class="
w-full border-gray-600 border-solid text-gray-300 bg-gray-700 leading-none rounded-lg
group-[.size-default]:border-2 group-[.size-sm]:border
group-[.size-default]:text-sm group-[.size-sm]:text-xs
group-[.size-default]:p-2 group-[.size-sm]:p-1
" @updated="$wire.{{$attributes->wire('model')->value}} = $event.detail" x-bind="editor"x-data="{
conditions: @js($conditions),
value: $wire.{{$attributes->wire('model')->value}}
}" id="{{$id}}" {{$attributes}}></div>
<x-ui::errors :for="$name" />
@if($hint)
<x-form::hint>{{$hint}}</x-form::hint>
@endif
</div>
</div>
HTML;
}
}

View File

@ -5,7 +5,7 @@ namespace Modules\Form\Components;
use App\Form\FormSettings;
use Livewire\Component;
class SettingView extends Component
class FormSettingView extends Component
{
public string $clearCacheUrl;

View File

@ -13,7 +13,7 @@ uses(DatabaseTransactions::class);
it('it renders page', function () {
test()->withoutExceptionHandling()->login()->loginNami();
test()->get('/setting/form')->assertSeeLivewire(SettingView::class);
test()->get('/setting/form')->assertSeeLivewire(FormSettingView::class);
});
it('it displays active modules', function () {

View File

@ -0,0 +1,50 @@
<?php
namespace Modules\Form\Components;
use App\Lib\Editor\EditorData;
use App\Prevention\PreventionSettings;
use Livewire\Component;
class PreventionSettingView extends Component
{
public EditorData $formmail;
public $settingClass = PreventionSettings::class;
public function rules(): array
{
return [
'formmail' => 'array',
];
}
public function mount(): void
{
$this->formmail = app(PreventionSettings::class)->formmail;
}
public function save(): void
{
app(PreventionSettings::class)->fill(['formmail' => $this->formmail])->save();
$this->dispatch('success', 'Einstellungen gespeichert.');
}
public function render()
{
return <<<'HTML'
<x-page::setting-layout :active="$settingClass">
<x-slot:right>
<x-form::save-button form="preventionsettingform"></x-form::save-button>
</x-slot:right>
<form id="preventionsettingform" class="grow p-6 grid grid-cols-1 gap-3 items-start content-start"
wire:submit.prevent="save">
<x-ui::setting-intro>
Hier kannst du Einstellungen zu Prävention setzen.
</x-ui::setting-intro>
<x-form::editor hint="lala" name="frommail" wire:model="formmail" label="E-Mail für Veranstaltungs-TN"></x-form::editor>
</form>
</x-page::setting-layout>
HTML;
}
}

View File

@ -0,0 +1,40 @@
<?php
namespace Modules\Form\Components;
use App\Prevention\PreventionSettings;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Livewire\Livewire;
use Tests\RequestFactories\EditorRequestFactory;
use Tests\TestCase;
uses(TestCase::class);
uses(DatabaseTransactions::class);
it('it renders page', function () {
test()->withoutExceptionHandling()->login()->loginNami();
test()->get('/setting/prevention')->assertSeeLivewire(PreventionSettingView::class);
});
it('displays settings', function () {
test()->withoutExceptionHandling()->login()->loginNami();
$text = EditorRequestFactory::new()->text(50, 'lorem ipsum')->toData();
app(PreventionSettings::class)->fill(['formmail' => $text])->save();
Livewire::test(PreventionSettingView::class)
->assertSet('formmail.blocks.0.data.text', 'lorem ipsum')
->assertSeeHtml('data-active');
});
it('saves settings', function () {
test()->withoutExceptionHandling()->login()->loginNami();
$text = EditorRequestFactory::new()->text(50, 'new lorem')->create();
Livewire::test(PreventionSettingView::class)
->set('formmail', $text)
->call('save')
->assertDispatched('success', 'Einstellungen gespeichert.');
$this->assertTrue(app(PreventionSettings::class)->formmail->hasAll(['new lorem']));
});

View File

@ -4,9 +4,11 @@ namespace Modules\Form;
use Illuminate\Routing\Router;
use Illuminate\Support\ServiceProvider;
use Modules\Form\Components\SettingView;
use Modules\Form\Components\FormSettingView;
use App\Setting\SettingFactory;
use App\Form\FormSettings;
use App\Prevention\PreventionSettings;
use Modules\Form\Components\PreventionSettingView;
class FormServiceProvider extends ServiceProvider
{
@ -27,9 +29,11 @@ class FormServiceProvider extends ServiceProvider
public function boot()
{
app(SettingFactory::class)->register(FormSettings::class);
app(SettingFactory::class)->register(PreventionSettings::class);
app(Router::class)->middleware(['web', 'auth:web'])->group(function ($router) {
$router->get('/setting/form', SettingView::class)->name('setting.form');
$router->get('/setting/form', FormSettingView::class)->name('setting.form');
$router->get('/setting/prevention', PreventionSettingView::class)->name('setting.prevention');
});
}
}

View File

@ -6,6 +6,7 @@ import 'tippy.js/dist/tippy.css';
import 'tippy.js/animations/shift-toward.css';
import '../css/tooltip.css';
import {error, success} from './toastify.js';
import editor from './editor.js';
Alpine.plugin(
Tooltip.defaultProps({
@ -16,4 +17,8 @@ Alpine.plugin(
window.addEventListener('success', (event) => success(event.detail[0]));
document.addEventListener('alpine:init', () => {
Alpine.bind('editor', () => editor);
});
Livewire.start();

227
resources/livewire-js/editor.js vendored Normal file
View File

@ -0,0 +1,227 @@
import {debounce} from 'lodash';
import EditorJS from '@editorjs/editorjs';
import Header from '@editorjs/header';
import Paragraph from '@editorjs/paragraph';
import NestedList from '@editorjs/nested-list';
import Alert from 'editorjs-alert';
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');
var tooltip = document.createElement('div');
tooltip.setAttribute('data-tooltip', '');
var content = document.createElement('div');
content.setAttribute('data-content', '');
content.appendChild(blockContent);
this.wrapper.appendChild(tooltip);
this.wrapper.appendChild(content);
this.styleWrapper();
return this.wrapper;
}
hasData() {
return this.data.ifs.length > 0;
}
styleWrapper() {
if (this.hasData()) {
this.wrapper.querySelector('[data-content]').className = 'p-1 border border-blue-100 rounded';
this.wrapper.querySelector('[data-tooltip]').className =
'mt-1 inline-block tracking-wider font-semibold ml-2 mr-2 px-2 py-1 items-center text-xs leading-none bg-blue-100 text-blue-900 rounded-t-lg';
this.wrapper.querySelector('[data-tooltip]').innerHTML = this.descriptionName();
} else {
this.wrapper.querySelector('[data-content]').className = '';
this.wrapper.querySelector('[data-tooltip]').className = '';
this.wrapper.querySelector('[data-tooltip]').innerHTML = '';
}
}
descriptionName() {
return (
'Bedingung ' +
this.data.ifs
.map((i) => {
var parts = [i.field];
if (i.comparator === 'isEqual' || i.comparator === 'isIn') {
parts.push('=');
}
if (i.comparator === 'isNotEqual' || i.comparator === 'isNotIn') {
parts.push('&ne;');
}
if (typeof i.value === 'string') {
parts.push(i.value);
}
if (Array.isArray(i.value)) {
parts.push(i.value.join(', '));
}
if (typeof i.value === 'boolean') {
parts.push(i.value ? 'An' : 'Aus');
}
return parts.join(' ');
})
.join(', ')
);
}
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;
}
}
export default {
'editor': null,
'value': null,
'x-init': async function () {
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');
// }
this.editor = new EditorJS({
placeholder: '',
holder: this.$el.getAttribute('id'),
minHeight: 0,
defaultBlock: 'paragraph',
data: this.value,
tunes: tunes,
tools: tools,
onChange: debounce(async (api, event) => {
const data = await this.editor.save();
this.$dispatch('updated', data);
}, 200),
onPopup: () => {
console.log('opened');
},
});
await this.editor.isReady;
},
};
// const props = defineProps({
// required: {
// type: Boolean,
// default: false,
// },
// size: {
// type: String,
// default: () => 'base',
// },
// rows: {
// type: Number,
// default: () => 4,
// },
// id: {
// type: String,
// required: true,
// },
// conditions: {
// required: false,
// type: Boolean,
// default: () => false,
// },
// hint: {
// type: String,
// default: () => '',
// },
// modelValue: {
// default: undefined,
// },
// label: {
// type: String,
// default: () => '',
// },
// });
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;
});
});
}

View File

@ -3,13 +3,9 @@
use App\Contribution\Actions\GenerateApiAction as ContributionGenerateApiAction;
use App\Form\Actions\FormApiListAction;
use App\Form\Actions\RegisterAction;
use App\Prevention\Actions\SettingStoreAction as PreventionStoreAction;
use App\Group\Actions\GroupApiIndexAction;
use App\Prevention\Actions\SettingApiAction;
Route::post('/contribution-generate', ContributionGenerateApiAction::class)->name('api.contribution.generate')->middleware('client:contribution-generate');
Route::post('/form/{form}/register', RegisterAction::class)->name('form.register');
Route::get('/group/{group?}', GroupApiIndexAction::class)->name('api.group');
Route::get('/form', FormApiListAction::class)->name('api.form.index');
Route::get('/prevention', SettingApiAction::class)->name('api.prevention.index');
Route::post('/prevention', PreventionStoreAction::class)->name('api.prevention.store');

View File

@ -1,40 +0,0 @@
<?php
namespace Tests\Feature\Prevention;
use App\Prevention\PreventionSettings;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Tests\RequestFactories\EditorRequestFactory;
use Tests\TestCase;
class SettingTest extends TestCase
{
use DatabaseTransactions;
public function testItOpensSettingsPage(): void
{
$this->login()->loginNami();
$this->get('/setting/prevention')->assertComponent('setting/Prevention')->assertOk();
}
public function testItReceivesSettings(): void
{
$this->login()->loginNami();
$text = EditorRequestFactory::new()->text(50, 'lorem ipsum')->toData();
app(PreventionSettings::class)->fill(['formmail' => $text])->save();
$this->get('/api/prevention')
->assertJsonPath('data.formmail.blocks.0.data.text', 'lorem ipsum');
}
public function testItStoresSettings(): void
{
$this->login()->loginNami();
$this->post('/api/prevention', ['formmail' => EditorRequestFactory::new()->text(50, 'new lorem')->create()])->assertOk();
$this->assertTrue(app(PreventionSettings::class)->formmail->hasAll(['new lorem']));
}
}