Compare commits

..

31 Commits

Author SHA1 Message Date
philipp lang af848e51c6 --wip-- [skip ci] 2024-10-23 21:02:42 +02:00
philipp lang 49f972bc1b Simplify form 2024-10-20 22:02:43 +02:00
philipp lang b26ac83031 Update update hook 2024-10-20 21:40:16 +02:00
philipp lang 21433624c5 Add test for events 2024-10-20 21:27:00 +02:00
philipp lang 98c2972575 Lint 2024-10-20 21:24:03 +02:00
philipp lang d590fbd325 Update refresh page 2024-10-20 21:23:47 +02:00
philipp lang 9535a50c4a Add mailgateway 2024-10-20 21:19:07 +02:00
philipp lang d46dccf4e6 Fix layout 2024-10-20 18:31:22 +02:00
philipp lang 3009003545 Remove old MailgatewayRequestFactory 2024-10-20 18:30:33 +02:00
philipp lang ff47b024f5 Lint 2024-10-20 18:30:16 +02:00
philipp lang 8077f36724 Add HTTP Check to MailmanTypeRequest 2024-10-20 18:29:51 +02:00
philipp lang 908f99f403 Delete old mailgateway tests 2024-10-20 18:29:32 +02:00
philipp lang ee5b923f25 Mark MailgatewayResource as deprecated 2024-10-20 18:29:03 +02:00
philipp lang ce7f86b7c0 Mark old mailgateway settings as deprecated 2024-10-20 18:27:34 +02:00
philipp lang 89803b80a1 Add BooleanDisplay component 2024-10-20 18:26:44 +02:00
philipp lang d45d8f561f Make right slot optional in SettingLayout 2024-10-20 18:26:17 +02:00
philipp lang c94facf794 fixup! Add modal component 2024-10-20 18:25:43 +02:00
philipp lang cef1c28df1 Add testable macro for setArray 2024-10-20 18:25:17 +02:00
philipp lang ff846d0929 Add modal component 2024-10-20 12:25:07 +02:00
philipp lang ba59783415 Throw exception when variant not found 2024-10-19 21:27:03 +02:00
philipp lang c0031cd4f0 Lint 2024-10-19 21:25:46 +02:00
philipp lang 56dd5cf146 Merge attributes for hint 2024-10-19 21:25:03 +02:00
philipp lang d096a1026b Lint 2024-10-19 21:24:50 +02:00
philipp lang fd8bf40090 Fix Tooltip attribute of Table action 2024-10-18 21:36:52 +02:00
philipp lang 62e122eef1 Register MailgatewayServiceProvider 2024-10-18 21:07:18 +02:00
philipp lang baa4b4a32c Add select field 2024-10-18 21:00:13 +02:00
philipp lang be8d1f5ab7 Disable autocomplete for password fields 2024-10-18 20:59:03 +02:00
philipp lang 3ec27b6707 Fix label required 2024-10-17 22:24:58 +02:00
philipp lang 4ccc4097d6 Add Table action button 2024-10-17 00:25:58 +02:00
philipp lang bf973d4139 Add table component 2024-10-16 23:51:20 +02:00
philipp lang ef0b0198fe Mod Component Test 2024-10-14 23:26:37 +02:00
48 changed files with 973 additions and 604 deletions

View File

@ -1,42 +0,0 @@
<?php
namespace App\Mailgateway\Casts;
use App\Mailgateway\Types\Type;
use Illuminate\Contracts\Database\Eloquent\CastsAttributes;
/**
* @implements CastsAttributes<Type, Type>
*/
class TypeCast implements CastsAttributes
{
/**
* Cast the given value.
*
* @param \Illuminate\Database\Eloquent\Model $model
* @param mixed $value
* @param array<string, mixed> $attributes
*
* @return mixed
*/
public function get($model, string $key, $value, array $attributes)
{
$value = json_decode($value, true);
return app($value['cls'])->setParams($value['params']);
}
/**
* Prepare the given value for storage.
*
* @param \Illuminate\Database\Eloquent\Model $model
* @param mixed $value
* @param array<string, mixed> $attributes
*
* @return mixed
*/
public function set($model, string $key, $value, array $attributes)
{
return json_encode($value);
}
}

View File

@ -6,6 +6,9 @@ use App\Mailgateway\Models\Mailgateway;
use App\Mailgateway\Resources\MailgatewayResource; use App\Mailgateway\Resources\MailgatewayResource;
use App\Setting\LocalSettings; use App\Setting\LocalSettings;
/**
* @deprecated
*/
class MailgatewaySettings extends LocalSettings class MailgatewaySettings extends LocalSettings
{ {
public static function group(): string public static function group(): string

View File

@ -2,11 +2,12 @@
namespace App\Mailgateway\Models; namespace App\Mailgateway\Models;
use App\Mailgateway\Casts\TypeCast; use App\Mailgateway\Types\Type as TypesType;
use Database\Factories\Mailgateway\Models\MailgatewayFactory; use Database\Factories\Mailgateway\Models\MailgatewayFactory;
use Illuminate\Database\Eloquent\Concerns\HasUuids; use Illuminate\Database\Eloquent\Concerns\HasUuids;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Nette\Utils\Type;
class Mailgateway extends Model class Mailgateway extends Model
{ {
@ -14,6 +15,6 @@ class Mailgateway extends Model
use HasFactory; use HasFactory;
use HasUuids; use HasUuids;
public $casts = ['type' => TypeCast::class]; public $casts = ['type' => TypesType::class];
public $guarded = []; public $guarded = [];
} }

View File

@ -8,6 +8,7 @@ use Illuminate\Http\Resources\Json\JsonResource;
/** /**
* @mixin Mailgateway * @mixin Mailgateway
* @deprecated
*/ */
class MailgatewayResource extends JsonResource class MailgatewayResource extends JsonResource
{ {
@ -27,7 +28,6 @@ class MailgatewayResource extends JsonResource
'domain' => $this->domain, 'domain' => $this->domain,
'type_human' => $this->type::name(), 'type_human' => $this->type::name(),
'works' => $this->type->works(), 'works' => $this->type->works(),
'type' => $this->type->toResource(),
'id' => $this->id, 'id' => $this->id,
'links' => [ 'links' => [
'update' => route('mailgateway.update', ['mailgateway' => $this->getModel()]), 'update' => route('mailgateway.update', ['mailgateway' => $this->getModel()]),

View File

@ -45,32 +45,28 @@ class MailmanType extends Type
'name' => 'url', 'name' => 'url',
'label' => 'URL', 'label' => 'URL',
'type' => 'text', 'type' => 'text',
'storeValidator' => 'required|max:255', 'validator' => 'required|max:255',
'updateValidator' => 'required|max:255',
'default' => '', 'default' => '',
], ],
[ [
'name' => 'user', 'name' => 'user',
'label' => 'Benutzer', 'label' => 'Benutzer',
'type' => 'text', 'type' => 'text',
'storeValidator' => 'required|max:255', 'validator' => 'required|max:255',
'updateValidator' => 'required|max:255',
'default' => '', 'default' => '',
], ],
[ [
'name' => 'password', 'name' => 'password',
'label' => 'Passwort', 'label' => 'Passwort',
'type' => 'password', 'type' => 'password',
'storeValidator' => 'required|max:255', 'validator' => 'required|max:255',
'updateValidator' => 'nullable|max:255',
'default' => '', 'default' => '',
], ],
[ [
'name' => 'owner', 'name' => 'owner',
'label' => 'E-Mail-Adresse des Eigentümers', 'label' => 'E-Mail-Adresse des Eigentümers',
'type' => 'email', 'type' => 'email',
'storeValidator' => 'required|email|max:255', 'validator' => 'required|email|max:255',
'updateValidator' => 'required|email|max:255',
'default' => '', 'default' => '',
], ],
]; ];

View File

@ -4,9 +4,15 @@ namespace App\Mailgateway\Types;
use App\Maildispatcher\Data\MailEntry; use App\Maildispatcher\Data\MailEntry;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
use Livewire\Wireable;
use Spatie\LaravelData\Concerns\WireableData;
use Spatie\LaravelData\Data;
abstract class Type abstract class Type extends Data implements Wireable
{ {
use WireableData;
abstract public static function name(): string; abstract public static function name(): string;
/** /**
@ -60,24 +66,13 @@ abstract class Type
/** /**
* @return array<string, mixed> * @return array<string, mixed>
*/ */
public static function rules(string $validator): array public static function rules(): array
{ {
return collect(static::fields())->mapWithKeys(fn ($field) => [ return collect(static::fields())->mapWithKeys(fn ($field) => [
$field['name'] => $field[$validator], $field['name'] => $field['validator'],
])->toArray(); ])->toArray();
} }
/**
* @return array<string, array<string, mixed>>
*/
public function toResource(): array
{
return [
'cls' => get_class($this),
'params' => get_object_vars($this),
];
}
/** /**
* @param Collection<int, MailEntry> $results * @param Collection<int, MailEntry> $results
*/ */
@ -93,8 +88,8 @@ abstract class Type
} }
$this->list($name, $domain) $this->list($name, $domain)
->filter(fn ($listEntry) => $results->doesntContain(fn ($r) => $r->is($listEntry))) ->filter(fn ($listEntry) => $results->doesntContain(fn ($r) => $r->is($listEntry)))
->each(fn ($listEntry) => $this->remove($name, $domain, $listEntry->email)); ->each(fn ($listEntry) => $this->remove($name, $domain, $listEntry->email));
} }
/** /**

View File

@ -85,7 +85,7 @@ class MailmanService
return app(Paginator::class)->result( return app(Paginator::class)->result(
fn ($page) => $this->http()->get("/lists/{$list->listId}/roster/member?page={$page}&count=10"), fn ($page) => $this->http()->get("/lists/{$list->listId}/roster/member?page={$page}&count=10"),
function ($response) use ($list) { function ($response) use ($list) {
throw_unless($response->ok(), MailmanServiceException::class, 'Fetching members for listId '.$list->listId.' failed.'); throw_unless($response->ok(), MailmanServiceException::class, 'Fetching members for listId ' . $list->listId . ' failed.');
/** @var array<int, array{email: string, self_link: string}>|null */ /** @var array<int, array{email: string, self_link: string}>|null */
$entries = data_get($response->json(), 'entries', []); $entries = data_get($response->json(), 'entries', []);
throw_if(is_null($entries), MailmanServiceException::class, 'Failed getting member list from response'); throw_if(is_null($entries), MailmanServiceException::class, 'Failed getting member list from response');
@ -112,7 +112,7 @@ class MailmanService
'pre_confirmed' => 'true', 'pre_confirmed' => 'true',
]); ]);
throw_unless(201 === $response->status(), MailmanServiceException::class, 'Adding member '.$email.' to '.$list->listId.' failed'); throw_unless(201 === $response->status(), MailmanServiceException::class, 'Adding member ' . $email . ' to ' . $list->listId . ' failed');
} }
public function removeMember(Member $member): void public function removeMember(Member $member): void

View File

@ -4,7 +4,6 @@ namespace App\Setting;
use App\Fileshare\FileshareSettings; use App\Fileshare\FileshareSettings;
use App\Form\FormSettings; use App\Form\FormSettings;
use App\Mailgateway\MailgatewaySettings;
use Modules\Module\ModuleSettings; use Modules\Module\ModuleSettings;
use App\Prevention\PreventionSettings; use App\Prevention\PreventionSettings;
use App\Setting\Data\SettingSynthesizer; use App\Setting\Data\SettingSynthesizer;
@ -35,7 +34,6 @@ class SettingServiceProvider extends ServiceProvider
{ {
app(SettingFactory::class)->register(ModuleSettings::class); app(SettingFactory::class)->register(ModuleSettings::class);
app(SettingFactory::class)->register(InvoiceSettings::class); app(SettingFactory::class)->register(InvoiceSettings::class);
app(SettingFactory::class)->register(MailgatewaySettings::class);
app(SettingFactory::class)->register(NamiSettings::class); app(SettingFactory::class)->register(NamiSettings::class);
app(SettingFactory::class)->register(FormSettings::class); app(SettingFactory::class)->register(FormSettings::class);
app(SettingFactory::class)->register(FileshareSettings::class); app(SettingFactory::class)->register(FileshareSettings::class);

View File

@ -0,0 +1,77 @@
<?php
namespace App\View\Enums;
use Illuminate\Support\Collection;
use InvalidArgumentException;
enum Variant: string
{
case PRIMARY = 'primary';
case SECONDARY = 'secondary';
case PRIMARYLIGHT = 'primary-light';
case WARNING = 'warning';
case INFO = 'info';
case DANGER = 'danger';
public function foreground(): string
{
return match ($this) {
self::PRIMARY => 'text-primary-300',
self::SECONDARY => 'text-primary-400',
self::PRIMARYLIGHT => 'text-primary-200',
self::WARNING => 'text-yellow-300',
self::INFO => 'text-blue-300',
self::DANGER => 'text-red-100',
};
}
public function background(): string
{
return match ($this) {
self::PRIMARY => 'bg-primary-700',
self::SECONDARY => 'bg-primary-800',
self::PRIMARYLIGHT => 'bg-primary-600',
self::WARNING => 'bg-yellow-700',
self::INFO => 'bg-blue-700',
self::DANGER => 'bg-red-400',
};
}
public function hoverForeground(): string
{
return match ($this) {
self::PRIMARY => 'hover:text-primary-100',
self::SECONDARY => 'hover:text-primary-200',
self::PRIMARYLIGHT => 'hover:text-primary-100',
self::WARNING => 'hover:text-yellow-100',
self::INFO => 'hover:text-blue-100',
self::DANGER => 'hover:text-red-100',
};
}
public function hoverBackground(): string
{
return match ($this) {
self::PRIMARY => 'hover:bg-primary-500',
self::SECONDARY => 'hover:bg-primary-600',
self::PRIMARYLIGHT => 'hover:bg-primary-500',
self::WARNING => 'hover:bg-yellow-500',
self::INFO => 'hover:bg-blue-500',
self::DANGER => 'hover:bg-red-500',
};
}
public static function fromString(string $input): self
{
return collect(static::cases())
->first(fn ($variant) => $variant->value === $input)
?: throw new InvalidArgumentException("Unknown variant: {$input} - Available Variants: " . self::values()->implode(', '));
}
public static function values(): Collection
{
return collect(static::cases())->map(fn ($variant) => $variant->value);
}
}

View File

@ -18,7 +18,7 @@ class Hint extends Component
public function render() public function render()
{ {
return <<<'HTML' return <<<'HTML'
<div class="h-full items-center flex absolute top-0 right-0"> <div {{ $attributes->merge(['class' => 'h-full items-center flex absolute top-0 right-0']) }}>
<div x-tooltip.raw="{{$slot}}" class="mr-2"> <div x-tooltip.raw="{{$slot}}" class="mr-2">
<x-ui::sprite src="info-button" class="w-5 h-5 text-primary-700"></x-ui::sprite> <x-ui::sprite src="info-button" class="w-5 h-5 text-primary-700"></x-ui::sprite>
</div> </div>

View File

@ -21,7 +21,7 @@ class Label extends Component
<span class="font-semibold leading-none text-gray-400 group-[.size-default]:text-sm group-[.size-sm]:text-xs"> <span class="font-semibold leading-none text-gray-400 group-[.size-default]:text-sm group-[.size-sm]:text-xs">
{{ $slot }} {{ $slot }}
@if ($required) @if ($required)
<span x-if="required" class="text-red-800">&nbsp;*</span> <span class="text-red-800">&nbsp;*</span>
@endif @endif
</span> </span>
HTML; HTML;

58
app/View/Form/Select.php Normal file
View File

@ -0,0 +1,58 @@
<?php
namespace App\View\Form;
use App\View\Traits\HasFormDimensions;
use Illuminate\View\Component;
class Select 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 $options = [],
public bool $disabled = false,
) {
$this->id = str()->uuid()->toString();
}
public function render()
{
return <<<'HTML'
<label class="flex flex-col group {{$heightClass}}" for="{{$id}}" style="{{$heightVars}}">
@if ($label)
<x-form::label :required="$required">{{$label}}</x-form::label>
@endif
<div class="relative flex-none flex">
<select {{$attributes}} @if($disabled) disabled @endif name="{{$name}}"
class="
w-full h-[var(--height)] 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]:px-2 group-[.size-sm]:px-1
py-0
"
>
<option value="">-- kein --</option>
@foreach ($options as $option)
<option value="{{$option['id']}}">{{ $option['name'] }}</option>
@endforeach
</select>
@if($hint)
<x-form::hint class="right-6">{{$hint}}</x-form::hint>
@endif
</div>
</label>
HTML;
}
}

View File

@ -34,6 +34,7 @@ class Text extends Component
<input <input
id="{{$id}}" id="{{$id}}"
type="{{$type}}" type="{{$type}}"
@if ($type === 'password') autocomplete="off" @endif
placeholder="" placeholder=""
class=" class="
w-full h-[var(--height)] border-gray-600 border-solid text-gray-300 bg-gray-700 leading-none rounded-lg w-full h-[var(--height)] border-gray-600 border-solid text-gray-300 bg-gray-700 leading-none rounded-lg

View File

@ -24,7 +24,7 @@ class Layout extends Component
public function render() public function render()
{ {
return <<<'HTML' return <<<'HTML'
<div class="grow flex flex-col"> <div class="grow flex flex-col" @refresh-page.window="$wire.$refresh">
<div class="grow bg-gray-900 flex flex-col duration-300 navbar:ml-60"> <div class="grow bg-gray-900 flex flex-col duration-300 navbar:ml-60">
<x-page::header title="{{ session()->get('title') }}"> <x-page::header title="{{ session()->get('title') }}">
<x-slot:beforeTitle> <x-slot:beforeTitle>

101
app/View/Page/Modal.php Normal file
View File

@ -0,0 +1,101 @@
<?php
namespace App\View\Page;
use Livewire\Attributes\On;
use Livewire\Component;
class Modal extends Component
{
public ?string $component = null;
public array $props = [];
public string $key = '';
public array $actions = [];
public ?string $size = null;
public string $title = '';
public function __construct()
{
}
#[On('openModal')]
public function onOpenModal(string $component, string $title, array $props = [], array $actions = ['storeable', 'closeable'], string $size = "xl"): void
{
$this->component = $component;
$this->props = $props;
$this->size = $size;
$this->title = $title;
$this->key = md5(json_encode(['component' => $component, 'props' => $props]));
$this->actions = $this->parseActions($actions);
}
public function parseActions(array $actions): array
{
return collect($actions)->map(function ($action) {
if ($action === 'closeable') {
return ['event' => 'closeModal', 'icon' => 'close', 'label' => 'Schließen'];
}
if ($action === 'storeable') {
return ['event' => 'onStoreFromModal', 'icon' => 'save', 'label' => 'Speichern'];
}
return $action;
})->toArray();
}
#[On('closeModal')]
public function onCloseModal(): void
{
$this->reset();
}
public function sizeClass(): string
{
if ($this->size === 'lg') {
return 'max-w-lg';
}
if ($this->size === 'xl') {
return 'max-w-xl';
}
return '';
}
public function render()
{
return <<<'HTML'
<div>
@if($component)
<div class="fixed z-40 top-0 left-0 w-full h-full flex items-center justify-center p-6 bg-black/60 backdrop-blur-sm" @click.self="$dispatch('closeModal')">
<div
class="relative rounded-lg p-8 bg-zinc-800 shadow-2xl shadow-black border border-zinc-700 border-solid w-full max-h-full flex flex-col overflow-auto {{$this->sizeClass()}}"
>
<div class="flex">
<h3 class="font-semibold text-primary-200 text-xl grow">{{$title}}</h3>
<div class="flex space-x-6">
@foreach ($this->actions as $action)
<a x-tooltip.raw="{{$action['label']}}" href="#" @click.prevent="$dispatch('{{$action['event']}}')">
<x-ui::sprite :src="$action['icon']" class="text-zinc-400 w-6 h-6"></x-ui::sprite>
</a>
@endforeach
</div>
</div>
<div class="text-primary-100 group is-popup grow flex flex-col mt-3">
<div>
@if ($component)
@livewire($component, $props, key($key))
@endif
</div>
</div>
</div>
</div>
@else
<div></div>
@endif
</div>
HTML;
}
}

View File

@ -25,11 +25,13 @@ class SettingLayout extends Component
return <<<'HTML' return <<<'HTML'
<x-page::layout> <x-page::layout>
<x-slot:right> <x-slot:right>
{{ $right }} {{ $right ?? '' }}
</x-slot:right> </x-slot:right>
<div class="flex grow relative"> <div class="flex grow relative">
<x-ui::menulist :entries="$entries"></x-ui::menulist> <x-ui::menulist :entries="$entries"></x-ui::menulist>
{{ $slot }} <div class="grow">
{{ $slot }}
</div>
</div> </div>
</x-page::layout> </x-page::layout>
HTML; HTML;

View File

@ -0,0 +1,33 @@
<?php
namespace App\View\Traits;
use App\View\Enums\Variant;
trait HasColors
{
public function bgColor(string $variant): string
{
$variant = Variant::fromString($variant);
return implode(' ', [
$variant->background(),
$variant->hoverBackground(),
]);
}
public function fgColor(string $variant): string
{
$variant = Variant::fromString($variant);
return implode(' ', [
$variant->foreground(),
$variant->hoverForeground(),
]);
}
public function allColors(string $variant): string
{
return "{$this->bgColor($variant)} {$this->fgColor($variant)}";
}
}

29
app/View/Ui/Action.php Normal file
View File

@ -0,0 +1,29 @@
<?php
namespace App\View\Ui;
use App\View\Traits\HasColors;
use Illuminate\View\Component;
class Action extends Component
{
use HasColors;
public function __construct(
public string $icon,
public string $variant = 'primary'
) {
}
public function render()
{
return <<<'HTML'
<a x-tooltip.raw="{{$slot}}" href="#" {{ $attributes }} class="inline-flex
w-6 h-5 flex items-center justify-center rounded {{ $allColors($variant) }}
">
<x-ui::sprite class="w-3 h-3 flex-none" :src="$icon"></x-ui::sprite>
</a>
HTML;
}
}

View File

@ -0,0 +1,40 @@
<?php
namespace App\View\Ui;
use Illuminate\View\Component;
class BooleanDisplay extends Component
{
public function __construct(
public bool $value,
public string $hint,
public string $right,
public string $wrong,
public bool $dark = false,
) {
}
public function spriteClass(): string
{
return $this->value ? 'text-green-800 group-[.dark]:text-green-600' : 'text-red-800 group-[.dark]:text-red-600';
}
public function render()
{
return <<<'HTML'
<div x-tooltip.raw="{{$hint}}" class="flex space-x-2 items-center group @if($dark) dark @endif">
<div class="border-2 rounded-full w-5 h-5 flex items-center justify-center
@if ($value) border-green-700 group-[.dark]:border-green-500
@else border-red-700 group-[.dark]:border-red-500
@endif
"
>
<x-ui::sprite :src="$value ? 'check ' :'close'" class="w-3 h-3 flex-none {{$spriteClass}}"></x-ui::sprite>
</div>
<div class="text-gray-400 text-xs">{{ $value ? $right : $wrong }}</div>
</div>
HTML;
}
}

31
app/View/Ui/Table.php Normal file
View File

@ -0,0 +1,31 @@
<?php
namespace App\View\Ui;
use Illuminate\View\Component;
class Table extends Component
{
public function __construct(public string $mode = 'dark')
{
}
public function render()
{
return <<<'HTML'
<div class="@container/table">
<table cellpadding="0" cellspacing="0" border="0" class="w-full @if ($mode === 'dark') table-dark @else table-light @endif
[&_th]:text-left [&_th]:px-2 [&_th]:@4xl/table:px-6 [&_th]:text-gray-200 [&_th]:font-semibold [&_th]:py-3 [&_th]:border-gray-600 [&_th]:border-b
[&_tbody_tr]:text-gray-200 [&_tbody_tr]:duration-300 [&_tbody_tr]:rounded [&_tbody_tr:hover]:bg-gray-800
[&_tr_td]:py-1 [&_tr_td]:px-2 [&_tr_td]:@4xl/table:px-6
[&.table-light_th]:border-gray-500 [&.table-light_tbody_tr:hover]:bg-gray-700
">
{{ $slot }}
</table>
</div>
HTML;
}
}

View File

@ -181,6 +181,7 @@ return [
Modules\Dashboard\DashboardServiceProvider::class, Modules\Dashboard\DashboardServiceProvider::class,
Modules\Module\ModuleServiceProvider::class, Modules\Module\ModuleServiceProvider::class,
Modules\Invoice\InvoiceServiceProvider::class, Modules\Invoice\InvoiceServiceProvider::class,
Modules\Mailgateway\MailgatewayServiceProvider::class,
], ],
/* /*

View File

@ -3,6 +3,7 @@
namespace Database\Factories\Mailgateway\Models; namespace Database\Factories\Mailgateway\Models;
use App\Mailgateway\Models\Mailgateway; use App\Mailgateway\Models\Mailgateway;
use App\Mailgateway\Types\LocalType;
use App\Mailgateway\Types\Type; use App\Mailgateway\Types\Type;
use Illuminate\Database\Eloquent\Factories\Factory; use Illuminate\Database\Eloquent\Factories\Factory;
@ -22,24 +23,14 @@ class MailgatewayFactory extends Factory
{ {
return [ return [
'name' => $this->faker->words(5, true), 'name' => $this->faker->words(5, true),
'type' => [ 'type' => new LocalType(),
'cls' => app('mail-gateways')->random(),
'params' => [],
],
'domain' => $this->faker->safeEmailDomain(), 'domain' => $this->faker->safeEmailDomain(),
]; ];
} }
/** public function type(Type $type): self
* @param class-string<Type> $type
* @param array<string, mixed> $params
*/
public function type(string $type, array $params): self
{ {
return $this->state(['type' => [ return $this->state(['type' => $type]);
'cls' => $type,
'params' => $params,
]]);
} }
public function name(string $name): self public function name(string $name): self

View File

@ -25,7 +25,14 @@ it('renders successfully', function () {
it('renders page successfully', function () { it('renders page successfully', function () {
$this->login()->loginNami(); $this->login()->loginNami();
$this->get('/')->assertOk()->assertSee('Dashboard'); $this->get('/')->assertSeeLivewire(DashboardComponent::class)->assertSee('Dashboard');
});
it('shows member component', function () {
$this->login()->loginNami();
Livewire::test(DashboardComponent::class)
->assertSee('Gruppierungs-Verteilung');
}); });
class ExampleBlock extends Block class ExampleBlock extends Block

View File

@ -0,0 +1,108 @@
<?php
namespace Modules\Mailgateway\Components;
use App\Mailgateway\Models\Mailgateway;
use App\Mailgateway\Types\Type;
use Illuminate\Support\Collection;
use Illuminate\Validation\Rule;
use Illuminate\Validation\ValidationException;
use Livewire\Attributes\On;
use Livewire\Attributes\Validate;
use Livewire\Component;
class Form extends Component
{
public string $name = '';
public string $domain = '';
public ?Type $type = null;
public Collection $types;
public ?Mailgateway $model = null;
public function rules()
{
return [
'name' => 'required|string|max:255',
'domain' => 'required|string|max:255',
...$this->type ? collect($this->type::rules())->mapWithKeys(fn ($rules, $key) => ["params.{$key}" => $rules]) : [],
];
}
public function validationAttributes(): array
{
return [
'type' => 'Typ',
'name' => 'Beschreibung',
'domain' => 'Domain',
...$this->type ? collect($this->type::fieldNames())->mapWithKeys(fn ($attribute, $key) => ["params.{$key}" => $attribute]) : [],
];
}
public function mount(?string $id = null): void
{
$this->types = app('mail-gateways')->map(fn ($gateway) => [
'name' => $gateway::name(),
'id' => $gateway,
]);
if ($id) {
$this->model = Mailgateway::find($id);
$this->name = $this->model->name;
$this->domain = $this->model->domain;
$this->type = $this->model->type;
}
}
public function fields(): array
{
return $this->type ? $this->type::fields() : [];
}
#[On('onStoreFromModal')]
public function onSave(): void
{
$this->validate();
if (!$this->type->works()) {
throw ValidationException::withMessages(['connection' => 'Verbindung fehlgeschlagen.']);
}
$payload = [
'name' => $this->name,
'domain' => $this->domain,
'type' => $this->type,
];
if ($this->model) {
$this->model->update($payload);
} else {
Mailgateway::create($payload);
}
$this->dispatch('closeModal');
$this->dispatch('refresh-page');
$this->dispatch('success', 'Erfolgreich gespeichert.');
}
public function render()
{
return <<<'HTML'
<div>
<form class="grid grid-cols-2 gap-3">
<x-form::text name="name" wire:model="name" label="Beschreibung" required />
<x-form::text name="domain" wire:model="domain" label="Domain" required />
<x-form::select name="type" wire:model.live="type" label="Typ" :options="$types" required />
@foreach($this->fields() as $index => $field)
<x-form::text
wire:key="index"
wire:model="$type->{$field['name']}"
:label="$field['label']"
:type="$field['type']"
:name="$field['name']"
:required="str_contains('required', $field['validator'])"
></x-form::text>
@endforeach
</form>
</div>
HTML;
}
}

View File

@ -0,0 +1,19 @@
<?php
namespace Modules\Mailgateway\Components;
use App\Mailgateway\Models\Mailgateway;
use Livewire\Component;
use Modules\Mailgateway\MailgatewaySettings;
class SettingView extends Component
{
public string $settingClass = MailgatewaySettings::class;
public function render()
{
return view('mailgateway::setting-view', [
'data' => Mailgateway::get(),
]);
}
}

View File

@ -0,0 +1,37 @@
<x-page::setting-layout :active="$settingClass">
<div>
<x-ui::table>
<thead>
<th>Bezeichnung</th>
<th>Domain</th>
<th>Typ</th>
<th>Prüfung</th>
<th>Aktion</th>
</thead>
<x-ui::action wire:click.prevent="$dispatch('openModal', {component: 'modules.mailgateway.components.form', props: {}, title: 'Verbindung erstellen'})" icon="plus" variant="danger">Neu</x-ui::action>
@foreach ($data as $index => $gateway)
<tr wire:key="$index">
<td>{{ $gateway->name }}</td>
<td>{{ $gateway->domain }}</td>
<td>{{ $gateway->type::name() }}</td>
<td>
<x-ui::boolean-display :value="$gateway->type->works()"
hint="Verbindungsstatus"
right="Verbindung erfolgreich"
wrong="Verbindung fehlgeschlagen"
></x-ui::boolean-display>
</td>
<td>
<x-ui::action wire:click="$dispatch('openModal', {
component: 'modules.mailgateway.components.form',
props: {id: '{{$gateway->id}}'},
title: 'Verbindung {{$gateway->name}} bearbeiten'}
)" icon="pencil" variant="warning">Bearbeiten</x-ui::action>
</td>
</tr>
@endforeach
</x-ui::table>
</div>
</x-page::setting-layout>

View File

@ -0,0 +1,38 @@
<?php
namespace Tests\Feature\Mailgateway;
use App\Mailgateway\Models\Mailgateway;
use App\Mailgateway\Types\LocalType;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Livewire\Livewire;
use Modules\Mailgateway\Components\SettingView;
use Tests\RequestFactories\MailmanTypeRequest;
use Tests\TestCase;
uses(DatabaseTransactions::class);
uses(TestCase::class);
it('test it can view index page', function () {
test()->login()->loginNami();
test()->get('/setting/mailgateway')->assertSeeLivewire(SettingView::class);
});
it('test it displays local gateways', function () {
test()->withoutExceptionHandling()->login()->loginNami();
Mailgateway::factory()->type(LocalType::from([]))->name('Lore')->domain('example.com')->create();
Livewire::test(SettingView::class)
->assertSeeHtml('example.com')
->assertSeeHtml('Lore')
->assertSeeHtml('Lokal')
->assertSeeHtml('Verbindung erfolgreich');
});
it('displays mailman gateways', function () {
test()->withoutExceptionHandling()->login()->loginNami();
$typeParams = MailmanTypeRequest::new()->succeeds()->state(['url' => 'https://mailman.example.com', 'user' => 'user', 'password' => 'password', 'owner' => 'owner']);
Mailgateway::factory()->type($typeParams->toData())->create();
Livewire::test(SettingView::class)->assertSeeHtml('Verbindung erfolgreich');
});

View File

@ -0,0 +1,37 @@
<?php
namespace Modules\Mailgateway;
use App\Setting\SettingFactory;
use Illuminate\Routing\Router;
use Illuminate\Support\Facades\View;
use Illuminate\Support\ServiceProvider;
use Modules\Mailgateway\Components\SettingView;
class MailgatewayServiceProvider extends ServiceProvider
{
/**
* Register services.
*
* @return void
*/
public function register()
{
}
/**
* Bootstrap services.
*
* @return void
*/
public function boot()
{
app(SettingFactory::class)->register(MailgatewaySettings::class);
app(Router::class)->middleware(['web', 'auth:web'])->group(function ($router) {
$router->get('/setting/mailgateway', SettingView::class)->name('setting.mailgateway');
});
View::addNamespace('mailgateway', __DIR__ . '/Components');
}
}

View File

@ -0,0 +1,26 @@
<?php
namespace Modules\Mailgateway;
use App\Setting\LocalSettings;
class MailgatewaySettings extends LocalSettings
{
public static function group(): string
{
return 'mailgateway';
}
public static function title(): string
{
return 'E-Mail-Verbindungen';
}
/**
* @inheritdoc
*/
public function viewData(): array
{
return [];
}
}

View File

@ -0,0 +1,122 @@
<?php
namespace Tests\Feature\Mailgateway;
use App\Mailgateway\Types\LocalType;
use App\Mailgateway\Types\MailmanType;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Livewire\Livewire;
use Modules\Mailgateway\Components\Form;
use Tests\RequestFactories\MailmanTypeRequest;
use Tests\TestCase;
uses(DatabaseTransactions::class);
uses(TestCase::class);
it('test it saves a mail gateway', function () {
test()->withoutExceptionHandling()->login()->loginNami();
Livewire::test(Form::class)
->set('name', 'lala')
->set('domain', 'example.com')
->set('type', new LocalType())
->call('onSave')
->assertHasNoErrors()
->assertDispatched('closeModal')
->assertDispatched('refresh-page')
->assertDispatched('success');
$this->assertDatabaseHas('mailgateways', [
'domain' => 'example.com',
'name' => 'lala',
'type' => json_encode([
'type' => LocalType::class,
'data' => [],
]),
]);
});
it('test it validates mail gateway', function (array $attributes, array $errors) {
test()->withoutExceptionHandling()->login()->loginNami();
Livewire::test(Form::class)
->set('name', 'lala')
->set('domain', 'example.com')
->set('type', new LocalType())
->setArray($attributes)
->call('onSave')
->assertHasErrors($errors)
->assertNotDispatched('closeModal')
->assertNotDispatched('refresh-page')
->assertNotDispatched('success');
})->with([
[['name' => ''], ['name' => 'required']],
[['domain' => ''], ['domain' => 'required']],
[['type' => ''], ['type' => 'required']],
]);
it('test it validates mailman type', function (array $attributes, array $errors) {
test()->withoutExceptionHandling()->login()->loginNami();
Livewire::test(Form::class)
->set('name', 'lala')
->set('domain', 'example.com')
->set('cls', MailmanType::class)
->set('params.url', 'exampl.com')
->set('params.user', '::user::')
->set('params.password', 'password')
->setArray($attributes)
->call('onSave')
->assertHasErrors($errors)
->assertNotDispatched('closeModal');
})->with([
[['params.url' => ''], ['params.url' => 'required']],
[['params.user' => ''], ['params.user' => 'required']],
[['params.password' => ''], ['params.password' => 'required']],
[['params.owner' => ''], ['params.owner' => 'required']],
[['params.owner' => 'aaa'], ['params.owner' => 'email']],
]);
it('test it stores mailman gateway', function () {
test()->withoutExceptionHandling()->login()->loginNami();
$typeParams = MailmanTypeRequest::new()->succeeds()->create(['url' => 'https://example.com', 'user' => 'user', 'password' => 'secret', 'owner' => 'owner@example.com']);
Livewire::test(Form::class)
->setArray([
'name' => 'lala',
'domain' => 'https://example.com',
'cls' => MailmanType::class,
'params' => $typeParams
])
->call('onSave')
->assertDispatched('closeModal');
$this->assertDatabaseHas('mailgateways', [
'type' => json_encode([
'cls' => MailmanType::class,
'params' => $typeParams,
]),
'name' => 'lala',
'domain' => 'https://example.com',
]);
});
it('test it checks mailman connection', function () {
test()->withoutExceptionHandling()->login()->loginNami();
$typeParams = MailmanTypeRequest::new()->fails()->create(['url' => 'https://example.com', 'user' => 'user', 'password' => 'secret', 'owner' => 'owner@example.com']);
Livewire::test(Form::class)
->setArray([
'name' => 'lala',
'domain' => 'https://example.com',
'cls' => MailmanType::class,
'params' => $typeParams
])
->call('onSave')
->assertHasErrors('connection')
->assertNotDispatched('closeModal');
$this->assertDatabaseCount('mailgateways', 0);
});

View File

@ -0,0 +1,108 @@
<?php
namespace Tests\Feature\Mailgateway;
use App\Mailgateway\Models\Mailgateway;
use App\Mailgateway\Types\LocalType;
use App\Mailgateway\Types\MailmanType;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Livewire\Livewire;
use Modules\Mailgateway\Components\Form;
use Tests\RequestFactories\MailmanTypeRequest;
use Tests\TestCase;
uses(DatabaseTransactions::class);
uses(TestCase::class);
it('test it sets attributes for mailman', function () {
test()->withoutExceptionHandling()->login()->loginNami();
$typeParams = MailmanTypeRequest::new()->state(['url' => 'https://mailman.example.com', 'user' => 'user', 'password' => 'password', 'owner' => 'owner']);
$mailgateway = Mailgateway::factory()->type($typeParams->toData())->create(['name' => '::name::', 'domain' => 'example.com']);
Livewire::test(Form::class, ['id' => $mailgateway->id])
->assertSet('model', fn ($m) => $m->is($mailgateway))
->assertSet('name', '::name::')
->assertSet('domain', 'example.com')
->assertSet(
'type',
fn ($type) => $type instanceof MailmanType
&& $type->url === 'https://mailman.example.com'
&& $type->user === 'user' &&
$type->password === 'password'
&& $type->owner === 'owner'
);
});
it('test it sets attributes for local', function () {
test()->withoutExceptionHandling()->login()->loginNami();
$mailgateway = Mailgateway::factory()->create(['name' => '::name::', 'domain' => 'example.com']);
Livewire::test(Form::class, ['id' => $mailgateway->id])
->assertSet('name', '::name::')
->assertSet('domain', 'example.com')
->assertSet('type', fn ($type) => $type instanceof LocalType);
});
it('test it validates type', function () {
test()->withoutExceptionHandling()->login()->loginNami();
$mailgateway = Mailgateway::factory()->type(LocalType::class, [])->create(['name' => '::name::', 'domain' => 'example.com']);
Livewire::test(Form::class, ['id' => $mailgateway->id])
->set('cls', '')
->assertHasErrors(['cls' => 'required']);
});
it('test it updates a mailman gateway without updating password', function () {
test()->withoutExceptionHandling()->login()->loginNami();
$typeParams = MailmanTypeRequest::new()->succeeds()->create(['url' => 'https://mailman.example.com', 'user' => 'user', 'password' => 'password', 'owner' => 'owner@example.com']);
$mailgateway = Mailgateway::factory()->type(MailmanType::class, $typeParams)->create(['name' => '::name::', 'domain' => 'example.com']);
Livewire::test(Form::class, ['id' => $mailgateway->id])
->set('name', '::newname::')
->call('onSave')
->assertHasNoErrors()
->assertDispatched('closeModal')
->assertDispatched('refresh-page')
->assertDispatched('success');
$this->assertDatabaseCount('mailgateways', 1);
$this->assertDatabaseHas('mailgateways', [
'name' => '::newname::',
'type' => json_encode(['cls' => MailmanType::class, 'params' => $typeParams]),
]);
});
it('test it updates a mailman gateway with password', function () {
test()->withoutExceptionHandling()->login()->loginNami();
$typeParams = MailmanTypeRequest::new()->create(['url' => 'https://mailman.example.com', 'user' => 'user', 'password' => 'password', 'owner' => 'owner@example.com']);
$newTypeParams = MailmanTypeRequest::new()->succeeds()->create(['url' => 'https://mailman.example.com', 'user' => 'newuser', 'password' => 'password', 'owner' => 'owner@example.com']);
$mailgateway = Mailgateway::factory()->type(MailmanType::class, $typeParams)->create();
Livewire::test(Form::class, ['id' => $mailgateway->id])
->set('params.user', 'newuser')
->call('onSave')
->assertHasNoErrors();
$this->assertDatabaseCount('mailgateways', 1);
$this->assertDatabaseHas('mailgateways', [
'type' => json_encode(['cls' => MailmanType::class, 'params' => $newTypeParams]),
]);
});
it('test it checks mailgateway connection when updating', function () {
test()->withoutExceptionHandling()->login()->loginNami();
$typeParams = MailmanTypeRequest::new()->create(['url' => 'https://mailman.example.com', 'user' => 'user', 'password' => 'password', 'owner' => 'owner@example.com']);
MailmanTypeRequest::new()->fails()->create(['url' => 'https://mailman.example.com', 'user' => 'newuser', 'password' => 'password', 'owner' => 'owner@example.com']);
$mailgateway = Mailgateway::factory()->type(MailmanType::class, $typeParams)->create();
Livewire::test(Form::class, ['id' => $mailgateway->id])
->set('params.user', 'newuser')
->call('onSave')
->assertHasErrors('connection');
});

9
package-lock.json generated
View File

@ -11,6 +11,7 @@
"@editorjs/paragraph": "^2.11.3", "@editorjs/paragraph": "^2.11.3",
"@inertiajs/vue3": "^1.0.14", "@inertiajs/vue3": "^1.0.14",
"@ryangjchandler/alpine-tooltip": "^2.0.1", "@ryangjchandler/alpine-tooltip": "^2.0.1",
"@tailwindcss/container-queries": "^0.1.1",
"@tailwindcss/forms": "^0.5.7", "@tailwindcss/forms": "^0.5.7",
"@tailwindcss/typography": "^0.5.10", "@tailwindcss/typography": "^0.5.10",
"@vitejs/plugin-vue": "^4.6.2", "@vitejs/plugin-vue": "^4.6.2",
@ -998,6 +999,14 @@
"tippy.js": "^6.3.1" "tippy.js": "^6.3.1"
} }
}, },
"node_modules/@tailwindcss/container-queries": {
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/@tailwindcss/container-queries/-/container-queries-0.1.1.tgz",
"integrity": "sha512-p18dswChx6WnTSaJCSGx6lTmrGzNNvm2FtXmiO6AuA1V4U5REyoqwmT6kgAsIMdjo07QdAfYXHJ4hnMtfHzWgA==",
"peerDependencies": {
"tailwindcss": ">=3.2.0"
}
},
"node_modules/@tailwindcss/forms": { "node_modules/@tailwindcss/forms": {
"version": "0.5.7", "version": "0.5.7",
"resolved": "https://registry.npmjs.org/@tailwindcss/forms/-/forms-0.5.7.tgz", "resolved": "https://registry.npmjs.org/@tailwindcss/forms/-/forms-0.5.7.tgz",

View File

@ -30,6 +30,7 @@
"@editorjs/paragraph": "^2.11.3", "@editorjs/paragraph": "^2.11.3",
"@inertiajs/vue3": "^1.0.14", "@inertiajs/vue3": "^1.0.14",
"@ryangjchandler/alpine-tooltip": "^2.0.1", "@ryangjchandler/alpine-tooltip": "^2.0.1",
"@tailwindcss/container-queries": "^0.1.1",
"@tailwindcss/forms": "^0.5.7", "@tailwindcss/forms": "^0.5.7",
"@tailwindcss/typography": "^0.5.10", "@tailwindcss/typography": "^0.5.10",
"@vitejs/plugin-vue": "^4.6.2", "@vitejs/plugin-vue": "^4.6.2",

View File

@ -5,6 +5,5 @@
@import 'base.css'; @import 'base.css';
@import 'layout'; @import 'layout';
@import 'buttons'; @import 'buttons';
@import 'table';
@import 'bool'; @import 'bool';
@import 'editor'; @import 'editor';

View File

@ -1,42 +0,0 @@
.custom-table {
width: 100%;
& > thead > th {
@apply text-left px-6 text-gray-200 font-semibold py-3 border-gray-600 border-b;
}
& > tr {
@apply text-gray-200 transition-all duration-300 rounded hover:bg-gray-800;
& > td {
@apply py-1 px-6;
}
}
&.custom-table-sm {
& > thead > th {
@apply px-3 py-2;
}
& > tr {
& > td {
@apply py-1 px-3;
}
}
}
&.custom-table-light {
& > thead > th {
@apply border-gray-500;
}
& > td {
&:hover {
@apply bg-gray-700;
}
}
}
}
.custom-table > * {
display: table-row;
}
.custom-table > * > * {
display: table-cell;
}

View File

@ -1,32 +0,0 @@
<template>
<div v-tooltip="longLabel" class="flex space-x-2 items-center">
<div class="border-2 rounded-full w-5 h-5 flex items-center justify-center" :class="value ? (dark ? 'border-green-500' : 'border-green-700') : dark ? 'border-red-500' : 'border-red-700'">
<ui-sprite :src="value ? 'check' : 'close'" :class="value ? (dark ? 'text-green-600' : 'text-green-800') : dark ? 'text-red-600' : 'text-red-800'" class="w-3 h-3 flex-none"></ui-sprite>
</div>
<div class="text-gray-400 text-xs" v-text="label"></div>
</div>
</template>
<script>
export default {
props: {
value: {
required: true,
type: Boolean,
},
label: {
type: String,
default: () => '',
},
longLabel: {
default: function () {
return null;
},
},
dark: {
type: Boolean,
default: () => false,
},
},
};
</script>

View File

@ -1,45 +0,0 @@
<template>
<div class="fixed z-40 top-0 left-0 w-full h-full flex items-center justify-center p-6 bg-black/60 backdrop-blur-sm">
<div
class="relative rounded-lg p-8 bg-zinc-800 shadow-2xl shadow-black border border-zinc-700 border-solid w-full max-h-full flex flex-col overflow-auto"
:class="full ? 'h-full' : innerWidth"
>
<div class="absolute top-0 right-0 mt-6 mr-6 flex space-x-6">
<slot name="actions"></slot>
<a href="#" @click.prevent="$emit('close')">
<ui-sprite src="close" class="text-zinc-400 w-6 h-6"></ui-sprite>
</a>
</div>
<h3 v-if="heading" class="font-semibold text-primary-200 text-xl" v-html="heading"></h3>
<div class="text-primary-100 group is-popup grow flex flex-col">
<suspense>
<div>
<slot></slot>
</div>
<template #fallback>
<ui-loading></ui-loading>
</template>
</suspense>
</div>
</div>
</div>
</template>
<script>
export default {
props: {
heading: {
type: String,
default: () => '',
},
innerWidth: {
default: () => 'max-w-xl',
type: String,
},
full: {
type: Boolean,
default: () => false,
},
},
};
</script>

View File

@ -15,6 +15,7 @@
<body class="min-h-full flex flex-col"> <body class="min-h-full flex flex-col">
<livewire:page.sidebar /> <livewire:page.sidebar />
{{ $slot }} {{ $slot }}
<livewire:page.modal />
<page-search-modal v-if="searchVisible" @close="searchVisible = false"></page-search-modal> <page-search-modal v-if="searchVisible" @close="searchVisible = false"></page-search-modal>
@livewireScriptConfig @livewireScriptConfig
</body> </body>

View File

@ -78,6 +78,7 @@ use App\Membership\Actions\MembershipStoreAction;
use App\Membership\Actions\MembershipUpdateAction; use App\Membership\Actions\MembershipUpdateAction;
use App\Payment\SubscriptionController; use App\Payment\SubscriptionController;
Route::get('/lala', fn () => auth()->login(\App\User::first()));
Route::group(['namespace' => 'App\\Http\\Controllers'], function (): void { Route::group(['namespace' => 'App\\Http\\Controllers'], function (): void {
Auth::routes(['register' => false]); Auth::routes(['register' => false]);
}); });

2
tailwind.config.js vendored
View File

@ -29,5 +29,5 @@ module.exports = {
}, },
}, },
plugins: [require('@tailwindcss/typography'), require('@tailwindcss/forms')], plugins: [require('@tailwindcss/typography'), require('@tailwindcss/forms'), require('@tailwindcss/container-queries')],
}; };

View File

@ -1,62 +0,0 @@
<?php
namespace Tests\Feature;
use App\Dashboard\Blocks\Block;
use App\Dashboard\DashboardFactory;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Tests\TestCase;
class DashboardTest extends TestCase
{
use DatabaseTransactions;
public function testItDisplaysBlock(): void
{
$this->withoutExceptionHandling();
app(DashboardFactory::class)->purge();
app(DashboardFactory::class)->register(ExampleBlock::class);
$this->login()->loginNami();
$response = $this->get('/');
$this->assertInertiaHas(['class' => 'name'], $response, 'blocks.0.data');
$this->assertInertiaHas('Example', $response, 'blocks.0.title');
$this->assertInertiaHas('exa', $response, 'blocks.0.component');
}
public function testItDisplaysUserAvatar(): void
{
$this->withoutExceptionHandling();
$this->login()->loginNami();
auth()->user()->update(['firstname' => 'Bob', 'lastname' => 'Dylan', 'email' => 'max@email.com']);
$this->get('/')
->assertInertiaPath('auth.user.firstname', 'Bob')
->assertInertiaPath('auth.user.avatar_url', 'https://www.gravatar.com/avatar/' . hash('sha256', 'max@email.com'))
->assertInertiaPath('auth.user.lastname', 'Dylan');
}
}
class ExampleBlock extends Block
{
public function title(): string
{
return 'Example';
}
/**
* @return array<string, string>
*/
public function data(): array
{
return ['class' => 'name'];
}
public function component(): string
{
return 'exa';
}
}

View File

@ -1,92 +0,0 @@
<?php
namespace Tests\Feature\Mailgateway;
use App\Mailgateway\Models\Mailgateway;
use App\Mailgateway\Types\LocalType;
use App\Mailgateway\Types\MailmanType;
use App\Mailman\Support\MailmanService;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Phake;
use Tests\RequestFactories\MailmanTypeRequest;
use Tests\TestCase;
class IndexTest extends TestCase
{
use DatabaseTransactions;
public function setUp(): void
{
parent::setUp();
$this->login()->loginNami();
}
public function testItCanViewIndexPage(): void
{
$response = $this->get('/setting/mailgateway');
$response->assertOk();
}
public function testItDisplaysLocalGateways(): void
{
$this->withoutExceptionHandling();
$mailgateway = Mailgateway::factory()->type(LocalType::class, [])->name('Lore')->domain('example.com')->create();
$response = $this->get('/setting/mailgateway');
$this->assertInertiaHas('example.com', $response, 'data.data.0.domain');
$this->assertInertiaHas('Lore', $response, 'data.data.0.name');
$this->assertInertiaHas('Lokal', $response, 'data.data.0.type_human');
$this->assertInertiaHas(true, $response, 'data.data.0.works');
$this->assertInertiaHas($mailgateway->id, $response, 'data.data.0.id');
$this->assertInertiaHas(route('mailgateway.update', ['mailgateway' => $mailgateway->id]), $response, 'data.data.0.links.update');
}
public function testItDisplaysMailmanGateways(): void
{
$this->stubIo(MailmanService::class, function ($mock) {
Phake::when($mock)->setCredentials()->thenReturn($mock);
Phake::when($mock)->check()->thenReturn(true);
});
$this->withoutExceptionHandling();
Mailgateway::factory()->type(MailmanType::class, MailmanTypeRequest::new()->create())->create();
$this->get('/setting/mailgateway')->assertOk();
}
public function testItHasMeta(): void
{
$this->withoutExceptionHandling();
$response = $this->get('/setting/mailgateway');
$this->assertInertiaHas(route('mailgateway.store'), $response, 'data.meta.links.store');
$this->assertInertiaHas([
'id' => null,
'name' => '-- kein --',
], $response, 'data.meta.types.0');
$this->assertInertiaHas([
'id' => LocalType::class,
'name' => 'Lokal',
], $response, 'data.meta.types.1');
$this->assertInertiaHas([
'id' => MailmanType::class,
'fields' => [
[
'name' => 'url',
'is_required' => true,
],
],
], $response, 'data.meta.types.2');
$this->assertInertiaHas([
'domain' => '',
'name' => '',
'type' => [
'params' => [],
'cls' => null,
],
], $response, 'data.meta.default');
}
}

View File

@ -1,48 +0,0 @@
<?php
namespace Tests\Feature\Mailgateway;
use App\Mailgateway\Types\MailmanType;
use App\Mailman\Support\MailmanService;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Phake;
use Tests\TestCase;
class MailmanTypeTest extends TestCase
{
use DatabaseTransactions;
public function testItChecksForWorks(): void
{
$this->withoutExceptionHandling();
$this->stubIo(MailmanService::class, function ($mock) {
Phake::when($mock)->setCredentials('https://example.com', 'user', 'secret')->thenReturn($mock);
Phake::when($mock)->check()->thenReturn(true);
Phake::when($mock)->setOwner('owner@example.com')->thenReturn($mock);
});
$type = app(MailmanType::class)->setParams([
'url' => 'https://example.com',
'user' => 'user',
'password' => 'secret',
'owner' => 'owner@example.com',
]);
$this->assertTrue($type->works());
}
public function testItCanReturnFalse(): void
{
$this->withoutExceptionHandling();
$this->stubIo(MailmanService::class, function ($mock) {
Phake::when($mock)->setCredentials('https://example.com', 'user', 'secret')->thenReturn($mock);
Phake::when($mock)->check()->thenReturn(false);
});
$type = app(MailmanType::class)->setParams([
'url' => 'https://example.com',
'user' => 'user',
'password' => 'secret',
]);
$this->assertFalse($type->works());
}
}

View File

@ -1,96 +0,0 @@
<?php
namespace Tests\Feature\Mailgateway;
use App\Mailgateway\Types\LocalType;
use App\Mailgateway\Types\MailmanType;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Phake;
use Tests\RequestFactories\MailgatewayRequestFactory;
use Tests\RequestFactories\MailmanTypeRequest;
use Tests\TestCase;
class StoreTest extends TestCase
{
use DatabaseTransactions;
public function setUp(): void
{
parent::setUp();
$this->login()->loginNami();
}
public function testItCanStoreALocalGateway(): void
{
$response = $this->postJson('/api/mailgateway', MailgatewayRequestFactory::new()->name('lala')->type(LocalType::class, [])->domain('example.com')->create());
$response->assertOk();
$this->assertDatabaseHas('mailgateways', [
'domain' => 'example.com',
'name' => 'lala',
'type' => json_encode([
'cls' => LocalType::class,
'params' => [],
]),
]);
}
public function testItCanStoreAMailmanGateway(): void
{
$typeParams = ['url' => 'https://example.com', 'user' => 'user', 'password' => 'secret', 'owner' => 'owner@example.com'];
$this->stubIo(MailmanType::class, function ($mock) use ($typeParams) {
Phake::when($mock)->setParams($typeParams)->thenReturn($mock);
Phake::when($mock)->works()->thenReturn(true);
Phake::when($mock)->setOwner('owner@example.com')->thenReturn($mock);
});
$this->postJson('/api/mailgateway', MailgatewayRequestFactory::new()->type(MailmanType::class, MailmanTypeRequest::new()->create($typeParams))->create());
$this->assertDatabaseHas('mailgateways', [
'type' => json_encode([
'cls' => MailmanType::class,
'params' => $typeParams,
]),
]);
}
public function testItThrowsErrorWhenMailmanConnectionFailed(): void
{
$typeParams = ['url' => 'https://example.com', 'user' => 'user', 'password' => 'secret', 'owner' => 'owner@example.com'];
$this->stubIo(MailmanType::class, function ($mock) use ($typeParams) {
Phake::when($mock)->setParams($typeParams)->thenReturn($mock);
Phake::when($mock)->works()->thenReturn(false);
Phake::when($mock)->setOwner('owner@example.com')->thenReturn($mock);
});
$this->postJson('/api/mailgateway', MailgatewayRequestFactory::new()->type(MailmanType::class, MailmanTypeRequest::new()->create($typeParams))->create())
->assertJsonValidationErrors('connection');
}
public function testItValidatesCustomFields(): void
{
$typeParams = ['url' => 'https://example.com', 'user' => '', 'password' => 'secret', 'owner' => 'aaaa'];
$this->stubIo(MailmanType::class, function ($mock) use ($typeParams) {
Phake::when($mock)->setParams($typeParams)->thenReturn($mock);
Phake::when($mock)->works()->thenReturn(false);
});
$this->postJson('/api/mailgateway', MailgatewayRequestFactory::new()->type(MailmanType::class, MailmanTypeRequest::new()->create($typeParams))->create())
->assertJsonValidationErrors([
'type.params.user' => 'Benutzer ist erforderlich.',
'type.params.owner' => 'E-Mail-Adresse des Eigentümers muss eine gültige E-Mail-Adresse sein.',
]);
}
public function testItValidatesType(): void
{
$this->postJson('/api/mailgateway', MailgatewayRequestFactory::new()->missingType()->create())
->assertJsonValidationErrors('type.cls');
}
public function testItValidatesNameAndDomain(): void
{
$this->postJson('/api/mailgateway', MailgatewayRequestFactory::new()->withoutName()->withoutDomain()->create())
->assertJsonValidationErrors('domain')
->assertJsonValidationErrors('name');
}
}

View File

@ -1,31 +0,0 @@
<?php
namespace Tests\Feature\Mailgateway;
use App\Mailgateway\Models\Mailgateway;
use App\Mailgateway\Types\LocalType;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Tests\RequestFactories\MailgatewayRequestFactory;
use Tests\TestCase;
class UpdateTest extends TestCase
{
use DatabaseTransactions;
public function setUp(): void
{
parent::setUp();
$this->login()->loginNami();
}
public function testItCanUpdateALocalGateway(): void
{
$mailgateway = Mailgateway::factory()->type(LocalType::class, [])->create();
$response = $this->patchJson("/api/mailgateway/{$mailgateway->id}", MailgatewayRequestFactory::new()->name('lala')->type(LocalType::class, [])->domain('example.com')->create());
$response->assertOk();
$this->assertDatabaseCount('mailgateways', 1);
}
}

View File

@ -1,61 +0,0 @@
<?php
namespace Tests\RequestFactories;
use App\Mailgateway\Types\Type;
use Worksome\RequestFactories\RequestFactory;
class MailgatewayRequestFactory extends RequestFactory
{
public function definition(): array
{
return [
'name' => $this->faker->words(5, true),
'type' => [
'cls' => app('mail-gateways')->random(),
'params' => [],
],
'domain' => $this->faker->safeEmailDomain(),
];
}
public function name(string $name): self
{
return $this->state(['name' => $name]);
}
public function domain(string $domain): self
{
return $this->state(['domain' => $domain]);
}
/**
* @param class-string<Type> $type
* @param array<string, mixed> $params
*/
public function type(string $type, array $params): self
{
return $this->state(['type' => [
'cls' => $type,
'params' => $params,
]]);
}
public function missingType(): self
{
return $this->state(['type' => [
'cls' => null,
'params' => [],
]]);
}
public function withoutName(): self
{
return $this->state(['name' => '']);
}
public function withoutDomain(): self
{
return $this->state(['domain' => '']);
}
}

View File

@ -2,6 +2,8 @@
namespace Tests\RequestFactories; namespace Tests\RequestFactories;
use App\Mailgateway\Types\MailmanType;
use Illuminate\Support\Facades\Http;
use Worksome\RequestFactories\RequestFactory; use Worksome\RequestFactories\RequestFactory;
class MailmanTypeRequest extends RequestFactory class MailmanTypeRequest extends RequestFactory
@ -9,10 +11,48 @@ class MailmanTypeRequest extends RequestFactory
public function definition(): array public function definition(): array
{ {
return [ return [
'url' => 'https://'.$this->faker->domainName(), 'url' => 'https://' . $this->faker->domainName(),
'user' => $this->faker->firstName(), 'user' => $this->faker->firstName(),
'password' => $this->faker->password(), 'password' => $this->faker->password(),
'owner' => $this->faker->safeEmail(), 'owner' => $this->faker->safeEmail(),
]; ];
} }
public function succeeds($overwrite = []): self
{
return $this->afterCreating(fn ($model) => $this->fakeCheck([...$model, ...$overwrite], true));
}
public function fails($overwrite = []): self
{
return $this->afterCreating(fn ($model) => $this->fakeCheck([...$model, ...$overwrite], false));
}
protected function fakeCheck($model, bool $check): void
{
Http::fake(function ($request) use ($model, $check) {
if (!$request->hasHeader('Authorization')) {
return;
}
[$user, $password] = explode(':', base64_decode(str($request->header('Authorization')[0])->replace('Basic ', '')->toString()));
if ($user !== $model['user'] || $password !== $model['password']) {
return;
}
if ($request->url() !== $model['url'] . '/system/versions') {
return null;
}
return $check
? Http::response(['version' => '2.0.0'], 200)
: Http::response([], 401);
});
}
public function toData(): MailmanType
{
return MailmanType::from($this->create());
}
} }

View File

@ -12,6 +12,7 @@ use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Http; use Illuminate\Support\Facades\Http;
use Illuminate\Testing\AssertableJsonString; use Illuminate\Testing\AssertableJsonString;
use Illuminate\Testing\TestResponse; use Illuminate\Testing\TestResponse;
use Livewire\Features\SupportTesting\Testable;
use Phake; use Phake;
use PHPUnit\Framework\Assert; use PHPUnit\Framework\Assert;
use Tests\Lib\MakesHttpCalls; use Tests\Lib\MakesHttpCalls;
@ -172,5 +173,14 @@ class TestCase extends BaseTestCase
return $this; return $this;
}); });
Testable::macro('setArray', function ($attributes) {
$self = $this;
foreach ($attributes as $key => $value) {
$self = $this->set($key, $value);
}
return $self;
});
} }
} }