fix(modals): make create/edit/delete modals actually save + auto-refresh lists

- All create modals now validate→create→dispatch(kebab-case event)→toast→close
- All edit/delete modals dispatch kebab-case events to Index classes
- Index components listen via #[On] and call resetPage() to refresh
- QR Code save() validates url field conditionally per type
- Tests updated to assert kebab-case event names

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
main
boban 2026-05-16 15:13:44 +02:00
parent c67bde8fc5
commit 2510620f67
18 changed files with 553 additions and 121 deletions

View File

@ -3,12 +3,17 @@
namespace App\Livewire\Modals; namespace App\Livewire\Modals;
use App\Domains\Bio\Models\BioPage; use App\Domains\Bio\Models\BioPage;
use App\Domains\Workspace\Models\Workspace;
use Illuminate\Support\Str; use Illuminate\Support\Str;
use Illuminate\View\View; use Illuminate\View\View;
use LivewireUI\Modal\ModalComponent; use LivewireUI\Modal\ModalComponent;
class CreateBioPage extends ModalComponent class CreateBioPage extends ModalComponent
{ {
public string $workspaceUlid = '';
public int $workspaceId = 0;
public string $title = ''; public string $title = '';
public string $slug = ''; public string $slug = '';
@ -21,24 +26,39 @@ class CreateBioPage extends ModalComponent
]; ];
} }
public function mount(string $workspaceUlid = ''): void
{
if ($workspaceUlid) {
$workspace = Workspace::where('ulid', $workspaceUlid)->firstOrFail();
abort_unless(
$workspace->owner_id === auth()->id() ||
$workspace->members()->where('user_id', auth()->id())->exists(),
404
);
$this->workspaceUlid = $workspaceUlid;
$this->workspaceId = $workspace->id;
}
}
public function save(): void public function save(): void
{ {
$this->validate(); $this->validate();
$workspace = app('current_workspace');
$wsId = $this->workspaceId ?: require_workspace()->id;
if (empty($this->slug)) { if (empty($this->slug)) {
$this->slug = Str::slug($this->title).'-'.Str::random(4); $this->slug = Str::slug($this->title).'-'.Str::random(4);
} }
BioPage::create([ BioPage::create([
'workspace_id' => $workspace->id, 'workspace_id' => $wsId,
'slug' => $this->slug, 'slug' => $this->slug,
'title' => ['en' => $this->title, 'de' => $this->title], 'title' => ['en' => $this->title, 'de' => $this->title],
'theme' => [], 'theme' => [],
'is_published' => false, 'is_published' => false,
]); ]);
$this->dispatch('bioPageCreated'); $this->dispatch('bio-created')->to(\App\Livewire\Pages\Bio\Index::class);
$this->dispatch('toast', message: 'Bio Page erstellt', type: 'success'); $this->dispatch('toast', message: 'Bio Page erstellt', type: 'success');
$this->closeModal(); $this->closeModal();
} }

View File

@ -2,24 +2,67 @@
namespace App\Livewire\Modals; namespace App\Livewire\Modals;
use App\Domains\Ai\Services\SlugSuggester;
use App\Domains\Link\Models\Link; use App\Domains\Link\Models\Link;
use App\Domains\Workspace\Models\Workspace;
use Illuminate\Support\Str; use Illuminate\Support\Str;
use Illuminate\View\View; use Illuminate\View\View;
use Livewire\Attributes\Validate;
use LivewireUI\Modal\ModalComponent; use LivewireUI\Modal\ModalComponent;
class CreateLink extends ModalComponent class CreateLink extends ModalComponent
{ {
public string $workspaceUlid = '';
public int $workspaceId = 0;
#[Validate('required|url|max:2048')]
public string $targetUrl = ''; public string $targetUrl = '';
#[Validate('nullable|alpha_dash|min:3|max:64')]
public string $slug = ''; public string $slug = '';
#[Validate('nullable|string|max:200')]
public string $title = ''; public string $title = '';
protected $rules = [ public bool $autoSlug = true;
'targetUrl' => 'required|url|max:2048',
'slug' => 'nullable|alpha_dash|max:50|unique:links,slug', /** @var string[] */
'title' => 'nullable|max:200', public array $slugSuggestions = [];
];
public bool $aiLoading = false;
public function mount(string $workspaceUlid = ''): void
{
if ($workspaceUlid) {
$workspace = Workspace::where('ulid', $workspaceUlid)->firstOrFail();
abort_unless(
$workspace->owner_id === auth()->id() ||
$workspace->members()->where('user_id', auth()->id())->exists(),
404
);
$this->workspaceUlid = $workspaceUlid;
$this->workspaceId = $workspace->id;
}
}
public function updatedTargetUrl(string $value): void
{
if ($this->autoSlug && $value) {
$this->slug = Str::random(6);
}
}
public function updatedSlug(): void
{
$this->autoSlug = false;
}
public function regenerateSlug(): void
{
$this->slug = Str::random(6);
$this->autoSlug = true;
}
public function generateSlug(): void public function generateSlug(): void
{ {
@ -28,24 +71,49 @@ class CreateLink extends ModalComponent
} }
} }
public function suggestSlugs(): void
{
if (! $this->targetUrl) {
return;
}
$this->aiLoading = true;
$this->slugSuggestions = app(SlugSuggester::class)->suggest($this->targetUrl);
$this->aiLoading = false;
}
public function pickSuggestion(string $slug): void
{
$this->slug = $slug;
$this->autoSlug = false;
$this->slugSuggestions = [];
}
public function save(): void public function save(): void
{ {
$this->validate(); $this->validate();
$workspace = app('current_workspace');
if (empty($this->slug)) { $wsId = $this->workspaceId;
$this->slug = Str::random(6); if (! $wsId) {
$ws = current_workspace();
if (! $ws) {
$this->addError('targetUrl', 'Workspace nicht gefunden.');
return;
}
$wsId = $ws->id;
} }
Link::create([ $slug = $this->slug ?: Str::random(6);
'workspace_id' => $workspace->id,
$link = Link::create([
'workspace_id' => $wsId,
'target_url' => $this->targetUrl, 'target_url' => $this->targetUrl,
'slug' => $this->slug, 'slug' => $slug,
'title' => $this->title ?: null, 'title' => $this->title ?: null,
'status' => 'active', 'status' => 'active',
'created_by' => auth()->id(),
]); ]);
$this->dispatch('linkCreated'); $this->dispatch('link-created', linkId: $link->ulid)->to(\App\Livewire\Pages\Links\Index::class);
$this->dispatch('toast', message: 'Link erstellt', type: 'success'); $this->dispatch('toast', message: 'Link erstellt', type: 'success');
$this->closeModal(); $this->closeModal();
} }

View File

@ -3,46 +3,176 @@
namespace App\Livewire\Modals; namespace App\Livewire\Modals;
use App\Domains\QrCode\Models\QrCode; use App\Domains\QrCode\Models\QrCode;
use App\Domains\Workspace\Models\Workspace;
use Illuminate\View\View; use Illuminate\View\View;
use LivewireUI\Modal\ModalComponent; use LivewireUI\Modal\ModalComponent;
class CreateQrCode extends ModalComponent class CreateQrCode extends ModalComponent
{ {
public string $label = ''; public string $workspaceUlid = '';
public string $url = ''; public int $workspaceId = 0;
public string $type = 'url'; public string $type = 'url';
protected function rules(): array public string $label = '';
// Generic payload (url, text, email, phone, sms, location)
public string $url = '';
// vCard
public string $vcardName = '';
public string $vcardPhone = '';
public string $vcardEmail = '';
public string $vcardCompany = '';
public string $vcardWebsite = '';
// WiFi
public string $wifiSsid = '';
public string $wifiPassword = '';
public string $wifiEncryption = 'WPA';
public bool $wifiHidden = false;
// Style
public string $fgColor = '#000000';
public string $bgColor = '#ffffff';
public string $logoUrl = '';
public string $dotStyle = 'square';
public int $size = 512;
public int $margin = 4;
public string $errorCorrection = 'M';
public string $frame = 'none';
public string $frameText = '';
public string $frameColor = '#7c3aed';
// Advanced
public bool $isDynamic = true;
public ?int $scanLimit = null;
public string $expiresAt = '';
public function mount(string $workspaceUlid = ''): void
{ {
return [ if ($workspaceUlid) {
'url' => 'required|url|max:2048', $workspace = Workspace::where('ulid', $workspaceUlid)->firstOrFail();
'label' => 'nullable|max:200', abort_unless(
'type' => 'required|in:url,vcard,wifi,pdf,mp3,text', $workspace->owner_id === auth()->id() ||
]; $workspace->members()->where('user_id', auth()->id())->exists(),
404
);
$this->workspaceUlid = $workspaceUlid;
$this->workspaceId = $workspace->id;
}
}
public function updated(string $prop): void
{
$this->dispatch('qr-preview-update', payload: $this->buildPayload(), options: [
'size' => $this->size,
'margin' => $this->margin,
'errorCorrection' => $this->errorCorrection,
'fgColor' => $this->fgColor,
'bgColor' => $this->bgColor,
]);
}
public function buildPayload(): string
{
return match ($this->type) {
'email' => 'mailto:' . $this->url,
'phone' => 'tel:' . $this->url,
'sms' => 'sms:' . $this->url,
'location' => 'geo:' . $this->url,
'vcard' => $this->buildVcard(),
'wifi' => $this->buildWifi(),
default => $this->url,
};
}
private function buildVcard(): string
{
return implode("\n", array_filter([
'BEGIN:VCARD',
'VERSION:3.0',
$this->vcardName ? "FN:{$this->vcardName}" : null,
$this->vcardPhone ? "TEL:{$this->vcardPhone}" : null,
$this->vcardEmail ? "EMAIL:{$this->vcardEmail}" : null,
$this->vcardCompany ? "ORG:{$this->vcardCompany}" : null,
$this->vcardWebsite ? "URL:{$this->vcardWebsite}" : null,
'END:VCARD',
]));
}
private function buildWifi(): string
{
$enc = $this->wifiEncryption ?: 'nopass';
$hidden = $this->wifiHidden ? 'true' : 'false';
return "WIFI:T:{$enc};S:{$this->wifiSsid};P:{$this->wifiPassword};H:{$hidden};;";
} }
public function save(): void public function save(): void
{ {
$this->validate(); $urlRule = match ($this->type) {
$workspace = app('current_workspace'); 'url' => 'required|url|max:2048',
'email', 'phone', 'sms', 'location', 'text' => 'required|max:2048',
default => 'nullable',
};
QrCode::create([ $this->validate([
'workspace_id' => $workspace->id, 'type' => 'required|in:url,vcard,wifi,text,email,phone,sms,location,pdf,mp3',
'type' => $this->type, 'label' => 'nullable|max:200',
'payload' => ['url' => $this->url], 'url' => $urlRule,
'is_dynamic' => true,
]); ]);
$this->dispatch('qrCreated'); $wsId = $this->workspaceId;
$this->dispatch('toast', message: 'QR Code erstellt', type: 'success'); if (! $wsId) {
$ws = current_workspace();
if (! $ws) {
$this->addError('url', 'Workspace nicht gefunden.');
return;
}
$wsId = $ws->id;
}
$dbType = match ($this->type) {
'email', 'phone', 'sms', 'location' => 'url',
default => $this->type,
};
QrCode::create([
'workspace_id' => $wsId,
'type' => $dbType,
'payload' => [
'data' => $this->buildPayload(),
'label' => $this->label,
'display_type' => $this->type,
],
'style' => [
'fg_color' => $this->fgColor,
'bg_color' => $this->bgColor,
'logo_url' => $this->logoUrl ?: null,
'dot_style' => $this->dotStyle,
'size' => $this->size,
'margin' => $this->margin,
'error_correction' => $this->errorCorrection,
'frame' => $this->frame,
'frame_text' => $this->frameText ?: null,
'frame_color' => $this->frameColor,
],
'is_dynamic' => $this->isDynamic,
'scan_limit' => $this->scanLimit ?: null,
'expires_at' => $this->expiresAt ?: null,
]);
$this->dispatch('qr-created')->to(\App\Livewire\Pages\QrCodes\Index::class);
$this->dispatch('toast', message: 'QR-Code erstellt', type: 'success');
$this->closeModal(); $this->closeModal();
} }
public static function modalMaxWidth(): string public static function modalMaxWidth(): string
{ {
return 'md'; return '4xl';
} }
public function render(): View public function render(): View

View File

@ -3,36 +3,37 @@
namespace App\Livewire\Modals; namespace App\Livewire\Modals;
use App\Domains\Bio\Models\BioPage; use App\Domains\Bio\Models\BioPage;
use App\Domains\Workspace\Models\Workspace;
use Illuminate\View\View; use Illuminate\View\View;
use LivewireUI\Modal\ModalComponent; use LivewireUI\Modal\ModalComponent;
class DeleteBioPage extends ModalComponent class DeleteBioPage extends ModalComponent
{ {
public int $workspaceId = 0;
public string $bioUlid = ''; public string $bioUlid = '';
public string $bioTitle = ''; public string $bioTitle = '';
public function mount(string $bioUlid): void public function mount(string $bioUlid): void
{ {
$workspace = app('current_workspace'); $bio = BioPage::where('ulid', $bioUlid)->firstOrFail();
$bio = BioPage::where('ulid', $bioUlid)
->where('workspace_id', $workspace->id)
->firstOrFail();
$this->authorizeWorkspace($bio->workspace_id);
$this->workspaceId = $bio->workspace_id;
$this->bioUlid = $bioUlid; $this->bioUlid = $bioUlid;
$this->bioTitle = $bio->getTranslation('title', 'en') ?? $bio->slug; $this->bioTitle = $bio->getTranslation('title', 'en') ?? $bio->slug;
} }
public function delete(): void public function delete(): void
{ {
$workspace = app('current_workspace');
BioPage::where('ulid', $this->bioUlid) BioPage::where('ulid', $this->bioUlid)
->where('workspace_id', $workspace->id) ->where('workspace_id', $this->workspaceId)
->firstOrFail() ->firstOrFail()
->delete(); ->delete();
$this->dispatch('bioPageDeleted', bioUlid: $this->bioUlid); $this->dispatch('bio-deleted')->to(\App\Livewire\Pages\Bio\Index::class);
$this->dispatch('toast', message: 'Bio Page gelöscht', type: 'success'); $this->dispatch('toast', message: 'Bio Page gelöscht', type: 'success');
$this->closeModal(); $this->closeModal();
} }
@ -46,4 +47,13 @@ class DeleteBioPage extends ModalComponent
{ {
return view('livewire.modals.delete-bio-page'); return view('livewire.modals.delete-bio-page');
} }
private function authorizeWorkspace(int $workspaceId): void
{
Workspace::where("id", $workspaceId)
->where(fn ($q) => $q
->where("owner_id", auth()->id())
->orWhereHas("members", fn ($q) => $q->where("user_id", auth()->id()))
)->firstOrFail();
}
} }

View File

@ -3,36 +3,37 @@
namespace App\Livewire\Modals; namespace App\Livewire\Modals;
use App\Domains\Link\Models\Link; use App\Domains\Link\Models\Link;
use App\Domains\Workspace\Models\Workspace;
use Illuminate\View\View; use Illuminate\View\View;
use LivewireUI\Modal\ModalComponent; use LivewireUI\Modal\ModalComponent;
class DeleteLink extends ModalComponent class DeleteLink extends ModalComponent
{ {
public int $workspaceId = 0;
public string $linkUlid = ''; public string $linkUlid = '';
public string $linkTitle = ''; public string $linkTitle = '';
public function mount(string $linkUlid): void public function mount(string $linkUlid): void
{ {
$workspace = app('current_workspace'); $link = Link::where('ulid', $linkUlid)->firstOrFail();
$link = Link::where('ulid', $linkUlid)
->where('workspace_id', $workspace->id)
->firstOrFail();
$this->authorizeWorkspace($link->workspace_id);
$this->workspaceId = $link->workspace_id;
$this->linkUlid = $linkUlid; $this->linkUlid = $linkUlid;
$this->linkTitle = $link->title ?? $link->slug; $this->linkTitle = $link->title ?? $link->slug;
} }
public function delete(): void public function delete(): void
{ {
$workspace = app('current_workspace');
Link::where('ulid', $this->linkUlid) Link::where('ulid', $this->linkUlid)
->where('workspace_id', $workspace->id) ->where('workspace_id', $this->workspaceId)
->firstOrFail() ->firstOrFail()
->delete(); ->delete();
$this->dispatch('linkDeleted', linkUlid: $this->linkUlid); $this->dispatch('link-deleted', linkUlid: $this->linkUlid)->to(\App\Livewire\Pages\Links\Index::class);
$this->dispatch('toast', message: 'Link gelöscht', type: 'success'); $this->dispatch('toast', message: 'Link gelöscht', type: 'success');
$this->closeModal(); $this->closeModal();
} }
@ -46,4 +47,13 @@ class DeleteLink extends ModalComponent
{ {
return view('livewire.modals.delete-link'); return view('livewire.modals.delete-link');
} }
private function authorizeWorkspace(int $workspaceId): void
{
Workspace::where("id", $workspaceId)
->where(fn ($q) => $q
->where("owner_id", auth()->id())
->orWhereHas("members", fn ($q) => $q->where("user_id", auth()->id()))
)->firstOrFail();
}
} }

View File

@ -3,36 +3,37 @@
namespace App\Livewire\Modals; namespace App\Livewire\Modals;
use App\Domains\QrCode\Models\QrCode; use App\Domains\QrCode\Models\QrCode;
use App\Domains\Workspace\Models\Workspace;
use Illuminate\View\View; use Illuminate\View\View;
use LivewireUI\Modal\ModalComponent; use LivewireUI\Modal\ModalComponent;
class DeleteQrCode extends ModalComponent class DeleteQrCode extends ModalComponent
{ {
public int $workspaceId = 0;
public string $qrUlid = ''; public string $qrUlid = '';
public string $qrLabel = ''; public string $qrLabel = '';
public function mount(string $qrUlid): void public function mount(string $qrUlid): void
{ {
$workspace = app('current_workspace'); $qr = QrCode::where('ulid', $qrUlid)->firstOrFail();
$qr = QrCode::where('ulid', $qrUlid)
->where('workspace_id', $workspace->id)
->firstOrFail();
$this->authorizeWorkspace($qr->workspace_id);
$this->workspaceId = $qr->workspace_id;
$this->qrUlid = $qrUlid; $this->qrUlid = $qrUlid;
$this->qrLabel = $qr->label ?? $qr->ulid; $this->qrLabel = $qr->label ?? $qr->ulid;
} }
public function delete(): void public function delete(): void
{ {
$workspace = app('current_workspace');
QrCode::where('ulid', $this->qrUlid) QrCode::where('ulid', $this->qrUlid)
->where('workspace_id', $workspace->id) ->where('workspace_id', $this->workspaceId)
->firstOrFail() ->firstOrFail()
->delete(); ->delete();
$this->dispatch('qrDeleted', qrUlid: $this->qrUlid); $this->dispatch('qr-deleted')->to(\App\Livewire\Pages\QrCodes\Index::class);
$this->dispatch('toast', message: 'QR Code gelöscht', type: 'success'); $this->dispatch('toast', message: 'QR Code gelöscht', type: 'success');
$this->closeModal(); $this->closeModal();
} }
@ -46,4 +47,13 @@ class DeleteQrCode extends ModalComponent
{ {
return view('livewire.modals.delete-qr-code'); return view('livewire.modals.delete-qr-code');
} }
private function authorizeWorkspace(int $workspaceId): void
{
Workspace::where("id", $workspaceId)
->where(fn ($q) => $q
->where("owner_id", auth()->id())
->orWhereHas("members", fn ($q) => $q->where("user_id", auth()->id()))
)->firstOrFail();
}
} }

View File

@ -3,11 +3,14 @@
namespace App\Livewire\Modals; namespace App\Livewire\Modals;
use App\Domains\Bio\Models\BioPage; use App\Domains\Bio\Models\BioPage;
use App\Domains\Workspace\Models\Workspace;
use Illuminate\View\View; use Illuminate\View\View;
use LivewireUI\Modal\ModalComponent; use LivewireUI\Modal\ModalComponent;
class EditBioPage extends ModalComponent class EditBioPage extends ModalComponent
{ {
public int $workspaceId = 0;
public string $bioUlid = ''; public string $bioUlid = '';
public string $title = ''; public string $title = '';
@ -18,11 +21,11 @@ class EditBioPage extends ModalComponent
public function mount(string $bioUlid): void public function mount(string $bioUlid): void
{ {
$workspace = app('current_workspace'); $bio = BioPage::where('ulid', $bioUlid)->firstOrFail();
$bio = BioPage::where('ulid', $bioUlid)
->where('workspace_id', $workspace->id)
->firstOrFail();
$this->authorizeWorkspace($bio->workspace_id);
$this->workspaceId = $bio->workspace_id;
$this->bioUlid = $bioUlid; $this->bioUlid = $bioUlid;
$this->title = $bio->getTranslation('title', 'en') ?? ''; $this->title = $bio->getTranslation('title', 'en') ?? '';
$this->slug = $bio->slug; $this->slug = $bio->slug;
@ -41,10 +44,9 @@ class EditBioPage extends ModalComponent
public function save(): void public function save(): void
{ {
$this->validate(); $this->validate();
$workspace = app('current_workspace');
$bio = BioPage::where('ulid', $this->bioUlid) $bio = BioPage::where('ulid', $this->bioUlid)
->where('workspace_id', $workspace->id) ->where('workspace_id', $this->workspaceId)
->firstOrFail(); ->firstOrFail();
$bio->update([ $bio->update([
@ -53,7 +55,7 @@ class EditBioPage extends ModalComponent
'is_published' => $this->isPublished, 'is_published' => $this->isPublished,
]); ]);
$this->dispatch('bioPageUpdated', bioUlid: $this->bioUlid); $this->dispatch('bio-updated')->to(\App\Livewire\Pages\Bio\Index::class);
$this->dispatch('toast', message: 'Bio Page aktualisiert', type: 'success'); $this->dispatch('toast', message: 'Bio Page aktualisiert', type: 'success');
$this->closeModal(); $this->closeModal();
} }
@ -67,4 +69,13 @@ class EditBioPage extends ModalComponent
{ {
return view('livewire.modals.edit-bio-page'); return view('livewire.modals.edit-bio-page');
} }
private function authorizeWorkspace(int $workspaceId): void
{
Workspace::where("id", $workspaceId)
->where(fn ($q) => $q
->where("owner_id", auth()->id())
->orWhereHas("members", fn ($q) => $q->where("user_id", auth()->id()))
)->firstOrFail();
}
} }

View File

@ -3,11 +3,14 @@
namespace App\Livewire\Modals; namespace App\Livewire\Modals;
use App\Domains\Link\Models\Link; use App\Domains\Link\Models\Link;
use App\Domains\Workspace\Models\Workspace;
use Illuminate\View\View; use Illuminate\View\View;
use LivewireUI\Modal\ModalComponent; use LivewireUI\Modal\ModalComponent;
class EditLink extends ModalComponent class EditLink extends ModalComponent
{ {
public int $workspaceId = 0;
public string $linkUlid = ''; public string $linkUlid = '';
public int $linkId = 0; public int $linkId = 0;
@ -22,11 +25,11 @@ class EditLink extends ModalComponent
public function mount(string $linkUlid): void public function mount(string $linkUlid): void
{ {
$workspace = app('current_workspace'); $link = Link::where('ulid', $linkUlid)->firstOrFail();
$link = Link::where('ulid', $linkUlid)
->where('workspace_id', $workspace->id)
->firstOrFail();
$this->authorizeWorkspace($link->workspace_id);
$this->workspaceId = $link->workspace_id;
$this->linkUlid = $linkUlid; $this->linkUlid = $linkUlid;
$this->linkId = $link->id; $this->linkId = $link->id;
$this->targetUrl = $link->target_url; $this->targetUrl = $link->target_url;
@ -48,10 +51,9 @@ class EditLink extends ModalComponent
public function save(): void public function save(): void
{ {
$this->validate(); $this->validate();
$workspace = app('current_workspace');
$link = Link::where('ulid', $this->linkUlid) $link = Link::where('ulid', $this->linkUlid)
->where('workspace_id', $workspace->id) ->where('workspace_id', $this->workspaceId)
->firstOrFail(); ->firstOrFail();
$link->update([ $link->update([
@ -61,7 +63,7 @@ class EditLink extends ModalComponent
'status' => $this->status, 'status' => $this->status,
]); ]);
$this->dispatch('linkUpdated', linkUlid: $this->linkUlid); $this->dispatch('link-updated', linkUlid: $this->linkUlid)->to(\App\Livewire\Pages\Links\Index::class);
$this->dispatch('toast', message: 'Link aktualisiert', type: 'success'); $this->dispatch('toast', message: 'Link aktualisiert', type: 'success');
$this->closeModal(); $this->closeModal();
} }
@ -70,4 +72,13 @@ class EditLink extends ModalComponent
{ {
return view('livewire.modals.edit-link'); return view('livewire.modals.edit-link');
} }
private function authorizeWorkspace(int $workspaceId): void
{
Workspace::where("id", $workspaceId)
->where(fn ($q) => $q
->where("owner_id", auth()->id())
->orWhereHas("members", fn ($q) => $q->where("user_id", auth()->id()))
)->firstOrFail();
}
} }

View File

@ -3,11 +3,14 @@
namespace App\Livewire\Modals; namespace App\Livewire\Modals;
use App\Domains\QrCode\Models\QrCode; use App\Domains\QrCode\Models\QrCode;
use App\Domains\Workspace\Models\Workspace;
use Illuminate\View\View; use Illuminate\View\View;
use LivewireUI\Modal\ModalComponent; use LivewireUI\Modal\ModalComponent;
class EditQrCode extends ModalComponent class EditQrCode extends ModalComponent
{ {
public int $workspaceId = 0;
public string $qrUlid = ''; public string $qrUlid = '';
public string $url = ''; public string $url = '';
@ -18,11 +21,11 @@ class EditQrCode extends ModalComponent
public function mount(string $qrUlid): void public function mount(string $qrUlid): void
{ {
$workspace = app('current_workspace'); $qr = QrCode::where('ulid', $qrUlid)->firstOrFail();
$qr = QrCode::where('ulid', $qrUlid)
->where('workspace_id', $workspace->id)
->firstOrFail();
$this->authorizeWorkspace($qr->workspace_id);
$this->workspaceId = $qr->workspace_id;
$this->qrUlid = $qrUlid; $this->qrUlid = $qrUlid;
$this->url = $qr->payload['url'] ?? ''; $this->url = $qr->payload['url'] ?? '';
$this->label = $qr->label ?? ''; $this->label = $qr->label ?? '';
@ -41,10 +44,9 @@ class EditQrCode extends ModalComponent
public function save(): void public function save(): void
{ {
$this->validate(); $this->validate();
$workspace = app('current_workspace');
$qr = QrCode::where('ulid', $this->qrUlid) $qr = QrCode::where('ulid', $this->qrUlid)
->where('workspace_id', $workspace->id) ->where('workspace_id', $this->workspaceId)
->firstOrFail(); ->firstOrFail();
$qr->update([ $qr->update([
@ -52,7 +54,7 @@ class EditQrCode extends ModalComponent
'payload' => ['url' => $this->url], 'payload' => ['url' => $this->url],
]); ]);
$this->dispatch('qrUpdated', qrUlid: $this->qrUlid); $this->dispatch('qr-updated')->to(\App\Livewire\Pages\QrCodes\Index::class);
$this->dispatch('toast', message: 'QR Code aktualisiert', type: 'success'); $this->dispatch('toast', message: 'QR Code aktualisiert', type: 'success');
$this->closeModal(); $this->closeModal();
} }
@ -66,4 +68,13 @@ class EditQrCode extends ModalComponent
{ {
return view('livewire.modals.edit-qr-code'); return view('livewire.modals.edit-qr-code');
} }
private function authorizeWorkspace(int $workspaceId): void
{
Workspace::where("id", $workspaceId)
->where(fn ($q) => $q
->where("owner_id", auth()->id())
->orWhereHas("members", fn ($q) => $q->where("user_id", auth()->id()))
)->firstOrFail();
}
} }

View File

@ -2,14 +2,41 @@
namespace App\Livewire\Pages\Bio; namespace App\Livewire\Pages\Bio;
use App\Domains\Bio\Models\BioPage;
use Illuminate\View\View; use Illuminate\View\View;
use Livewire\Attributes\On;
use Livewire\Component; use Livewire\Component;
use Livewire\WithPagination;
class Index extends Component class Index extends Component
{ {
use WithPagination;
public string $search = '';
public function updatingSearch(): void
{
$this->resetPage();
}
#[On('bio-created')]
#[On('bio-updated')]
#[On('bio-deleted')]
public function refresh(): void
{
$this->resetPage();
}
public function render(): View public function render(): View
{ {
return view('livewire.pages.bio.index') $workspace = require_workspace();
$bioPages = BioPage::where('workspace_id', $workspace->id)
->when($this->search, fn ($q) => $q->where('slug', 'like', "%{$this->search}%"))
->latest()
->paginate(20);
return view('livewire.pages.bio.index', compact('bioPages'))
->layout('layouts.nimuli-app', ['title' => 'Link-in-Bio']); ->layout('layouts.nimuli-app', ['title' => 'Link-in-Bio']);
} }
} }

View File

@ -26,9 +26,9 @@ class Index extends Component
$this->resetPage(); $this->resetPage();
} }
#[On('linkCreated')] #[On('link-created')]
#[On('linkUpdated')] #[On('link-updated')]
#[On('linkDeleted')] #[On('link-deleted')]
public function refresh(): void public function refresh(): void
{ {
$this->resetPage(); $this->resetPage();
@ -36,7 +36,7 @@ class Index extends Component
public function render(): View public function render(): View
{ {
$workspace = app('current_workspace'); $workspace = require_workspace();
$links = Link::where('workspace_id', $workspace->id) $links = Link::where('workspace_id', $workspace->id)
->when($this->search, fn ($q) => $q->where(function ($q) { ->when($this->search, fn ($q) => $q->where(function ($q) {

View File

@ -2,14 +2,41 @@
namespace App\Livewire\Pages\QrCodes; namespace App\Livewire\Pages\QrCodes;
use App\Domains\QrCode\Models\QrCode;
use Illuminate\View\View; use Illuminate\View\View;
use Livewire\Attributes\On;
use Livewire\Component; use Livewire\Component;
use Livewire\WithPagination;
class Index extends Component class Index extends Component
{ {
use WithPagination;
public string $search = '';
public function updatingSearch(): void
{
$this->resetPage();
}
#[On('qr-created')]
#[On('qr-updated')]
#[On('qr-deleted')]
public function refresh(): void
{
$this->resetPage();
}
public function render(): View public function render(): View
{ {
return view('livewire.pages.qr-codes.index') $workspace = require_workspace();
$qrCodes = QrCode::where('workspace_id', $workspace->id)
->when($this->search, fn ($q) => $q->where('label', 'like', "%{$this->search}%"))
->latest()
->paginate(20);
return view('livewire.pages.qr-codes.index', compact('qrCodes'))
->layout('layouts.nimuli-app', ['title' => 'QR Codes']); ->layout('layouts.nimuli-app', ['title' => 'QR Codes']);
} }
} }

View File

@ -1,21 +1,61 @@
<div> <div>
@php $ws = current_workspace(); @endphp
<div class="flex items-center justify-between mb-6"> <div class="flex items-center justify-between mb-6">
<h1 class="text-2xl font-semibold text-t1">Link-in-Bio</h1> <h1 class="text-2xl font-semibold text-t1">Link-in-Bio</h1>
<button class="px-4 py-2 bg-blue text-white rounded-lg text-sm font-medium hover:opacity-90"> @if($ws)
<button
@click="$dispatch('openModal', {component: 'modals.create-bio-page', arguments: {workspaceUlid: '{{ $ws->ulid }}'}})"
class="px-4 py-2 bg-accent-gradient text-white rounded-lg text-sm font-medium hover:opacity-90 transition-opacity">
+ New Bio Page + New Bio Page
</button> </button>
@endif
</div> </div>
<div class="bg-s1 border border-white/[.06] rounded-xl p-12 text-center"> <div class="bg-s1 border border-white/[.06] rounded-xl overflow-hidden">
<div class="w-16 h-16 bg-s2 rounded-2xl flex items-center justify-center mx-auto mb-4"> @forelse($bioPages as $page)
<svg class="w-8 h-8 text-t3" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5"> <div class="flex items-center gap-4 px-5 py-4 border-b border-white/[.04] group hover:bg-white/[.02] transition-colors">
<path stroke-linecap="round" stroke-linejoin="round" d="M3.75 6.75h16.5M3.75 12h16.5M3.75 17.25h7.5"/> <div class="w-10 h-10 bg-s2 rounded-lg flex items-center justify-center flex-shrink-0">
</svg> <svg class="w-5 h-5 text-t3" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5">
<path stroke-linecap="round" stroke-linejoin="round" d="M3.75 6.75h16.5M3.75 12h16.5M3.75 17.25h7.5"/>
</svg>
</div>
<div class="flex-1 min-w-0">
<div class="text-sm font-medium text-t1">{{ is_array($page->title) ? ($page->title['en'] ?? $page->slug) : ($page->title ?? $page->slug) }}</div>
<div class="text-xs text-t2 font-mono">nimu.li/{{ $page->slug }}</div>
</div>
<span class="text-xs px-2 py-0.5 rounded-full {{ $page->is_published ? 'bg-green/10 text-green' : 'bg-white/[.06] text-t3' }}">
{{ $page->is_published ? 'Published' : 'Draft' }}
</span>
<div class="flex items-center gap-3 opacity-0 group-hover:opacity-100 transition-opacity">
<button
@click="$dispatch('openModal', {component: 'modals.edit-bio-page', arguments: {bioUlid: '{{ $page->ulid }}'}})"
class="text-t2 text-xs hover:text-t1">Edit</button>
<button
@click="$dispatch('openModal', {component: 'modals.delete-bio-page', arguments: {bioUlid: '{{ $page->ulid }}'}})"
class="text-red text-xs hover:opacity-80">Delete</button>
</div>
</div> </div>
<h3 class="text-lg font-medium text-t1 mb-2">No bio pages yet</h3> @empty
<p class="text-sm text-t2 mb-6">Create a beautiful link-in-bio page for Instagram, TikTok and more.</p> <div class="p-12 text-center">
<button class="px-4 py-2.5 bg-blue text-white rounded-lg text-sm font-medium hover:opacity-90"> <div class="w-16 h-16 bg-s2 rounded-2xl flex items-center justify-center mx-auto mb-4">
Create your first Bio Page <svg class="w-8 h-8 text-t3" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5">
</button> <path stroke-linecap="round" stroke-linejoin="round" d="M3.75 6.75h16.5M3.75 12h16.5M3.75 17.25h7.5"/>
</svg>
</div>
<h3 class="text-lg font-medium text-t1 mb-2">No bio pages yet</h3>
<p class="text-sm text-t2 mb-6">Create a beautiful link-in-bio page for Instagram, TikTok and more.</p>
@if($ws)
<button
@click="$dispatch('openModal', {component: 'modals.create-bio-page', arguments: {workspaceUlid: '{{ $ws->ulid }}'}})"
class="px-4 py-2.5 bg-accent-gradient text-white rounded-lg text-sm font-medium hover:opacity-90 transition-opacity">
Create your first Bio Page
</button>
@endif
</div>
@endforelse
</div> </div>
@if($bioPages->hasPages())
<div class="mt-4">{{ $bioPages->links() }}</div>
@endif
</div> </div>

View File

@ -1,11 +1,21 @@
<div> <div>
@php $ws = current_workspace(); @endphp
{{-- Header --}} {{-- Header --}}
<div class="flex items-center justify-between mb-6"> <div class="flex items-center justify-between mb-6">
<h1 class="text-2xl font-bold text-t1">Links</h1> <h1 class="text-2xl font-bold text-t1">Links</h1>
<button @click="$dispatch('openModal', {component: 'modals.create-link'})" @if($ws)
class="px-4 py-2 bg-accent-gradient text-white rounded-lg text-sm font-medium hover:opacity-90 transition-opacity"> <div class="flex items-center gap-2">
+ New Link <button @click="$dispatch('openModal', {component: 'modals.utm-builder'})"
</button> class="px-3 py-2 bg-s2 border border-white/[.06] text-t1 rounded-lg text-sm font-medium hover:bg-s3 transition-colors flex items-center gap-1.5">
<x-heroicon-o-beaker class="w-4 h-4" />
UTM Builder
</button>
<button @click="$dispatch('openModal', {component: 'modals.create-link', arguments: {workspaceUlid: '{{ $ws->ulid }}'}})"
class="px-4 py-2 bg-accent-gradient text-white rounded-lg text-sm font-medium hover:opacity-90 transition-opacity">
+ New Link
</button>
</div>
@endif
</div> </div>
{{-- Filter Bar --}} {{-- Filter Bar --}}
@ -87,6 +97,12 @@
{{-- Actions --}} {{-- Actions --}}
<td class="px-4 py-3 text-right"> <td class="px-4 py-3 text-right">
<div class="flex items-center justify-end gap-3 opacity-0 group-hover:opacity-100 transition-opacity"> <div class="flex items-center justify-end gap-3 opacity-0 group-hover:opacity-100 transition-opacity">
<button
@click="$dispatch('openModal', {component: 'modals.link-variants', arguments: {linkUlid: '{{ $link->ulid }}'}})"
class="text-t2 text-xs hover:text-purple transition-colors flex items-center gap-1">
<x-heroicon-o-chart-bar class="w-3 h-3" />
A/B
</button>
<button <button
@click="$dispatch('openModal', {component: 'modals.edit-link', arguments: {linkUlid: '{{ $link->ulid }}'}})" @click="$dispatch('openModal', {component: 'modals.edit-link', arguments: {linkUlid: '{{ $link->ulid }}'}})"
class="text-t2 text-xs hover:text-t1 transition-colors"> class="text-t2 text-xs hover:text-t1 transition-colors">
@ -108,8 +124,10 @@
<path stroke-linecap="round" stroke-linejoin="round" d="M13.19 8.688a4.5 4.5 0 0 1 1.242 7.244l-4.5 4.5a4.5 4.5 0 0 1-6.364-6.364l1.757-1.757m13.35-.622 1.757-1.757a4.5 4.5 0 0 0-6.364-6.364l-4.5 4.5a4.5 4.5 0 0 0 1.242 7.244" /> <path stroke-linecap="round" stroke-linejoin="round" d="M13.19 8.688a4.5 4.5 0 0 1 1.242 7.244l-4.5 4.5a4.5 4.5 0 0 1-6.364-6.364l1.757-1.757m13.35-.622 1.757-1.757a4.5 4.5 0 0 0-6.364-6.364l-4.5 4.5a4.5 4.5 0 0 0 1.242 7.244" />
</svg> </svg>
<p class="text-t3 text-sm">No links yet.</p> <p class="text-t3 text-sm">No links yet.</p>
<button @click="$dispatch('openModal', {component: 'modals.create-link'})" @if($ws)
<button @click="$dispatch('openModal', {component: 'modals.create-link', arguments: {workspaceUlid: '{{ $ws->ulid }}'}})"
class="text-blue text-sm hover:underline">Create your first short link</button> class="text-blue text-sm hover:underline">Create your first short link</button>
@endif
</div> </div>
</td> </td>
</tr> </tr>

View File

@ -1,22 +1,61 @@
<div> <div>
@php $ws = current_workspace(); @endphp
<div class="flex items-center justify-between mb-6"> <div class="flex items-center justify-between mb-6">
<h1 class="text-2xl font-semibold text-t1">QR Codes</h1> <h1 class="text-2xl font-semibold text-t1">QR Codes</h1>
<button class="px-4 py-2 bg-blue text-white rounded-lg text-sm font-medium hover:opacity-90"> @if($ws)
<button
@click="$dispatch('openModal', {component: 'modals.create-qr-code', arguments: {workspaceUlid: '{{ $ws->ulid }}'}})"
class="px-4 py-2 bg-accent-gradient text-white rounded-lg text-sm font-medium hover:opacity-90 transition-opacity">
+ Create QR Code + Create QR Code
</button> </button>
@endif
</div> </div>
<div class="bg-s1 border border-white/[.06] rounded-xl p-12 text-center"> <div class="bg-s1 border border-white/[.06] rounded-xl overflow-hidden">
<div class="w-16 h-16 bg-s2 rounded-2xl flex items-center justify-center mx-auto mb-4"> @forelse($qrCodes as $qr)
<svg class="w-8 h-8 text-t3" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5"> <div class="flex items-center gap-4 px-5 py-4 border-b border-white/[.04] group hover:bg-white/[.02] transition-colors">
<path stroke-linecap="round" stroke-linejoin="round" d="M3.75 4.875c0-.621.504-1.125 1.125-1.125h4.5c.621 0 1.125.504 1.125 1.125v4.5c0 .621-.504 1.125-1.125 1.125h-4.5A1.125 1.125 0 013.75 9.375v-4.5zM3.75 14.625c0-.621.504-1.125 1.125-1.125h4.5c.621 0 1.125.504 1.125 1.125v4.5c0 .621-.504 1.125-1.125 1.125h-4.5a1.125 1.125 0 01-1.125-1.125v-4.5zM13.5 4.875c0-.621.504-1.125 1.125-1.125h4.5c.621 0 1.125.504 1.125 1.125v4.5c0 .621-.504 1.125-1.125 1.125h-4.5A1.125 1.125 0 0113.5 9.375v-4.5z"/> <div class="w-10 h-10 bg-s2 rounded-lg flex items-center justify-center flex-shrink-0">
<path stroke-linecap="round" stroke-linejoin="round" d="M6.75 6.75h.75v.75h-.75v-.75zM6.75 16.5h.75v.75h-.75v-.75zM16.5 6.75h.75v.75h-.75v-.75z"/> <svg class="w-5 h-5 text-t3" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5">
</svg> <path stroke-linecap="round" stroke-linejoin="round" d="M3.75 4.875c0-.621.504-1.125 1.125-1.125h4.5c.621 0 1.125.504 1.125 1.125v4.5c0 .621-.504 1.125-1.125 1.125h-4.5A1.125 1.125 0 013.75 9.375v-4.5zM3.75 14.625c0-.621.504-1.125 1.125-1.125h4.5c.621 0 1.125.504 1.125 1.125v4.5c0 .621-.504 1.125-1.125 1.125h-4.5a1.125 1.125 0 01-1.125-1.125v-4.5zM13.5 4.875c0-.621.504-1.125 1.125-1.125h4.5c.621 0 1.125.504 1.125 1.125v4.5c0 .621-.504 1.125-1.125 1.125h-4.5A1.125 1.125 0 0113.5 9.375v-4.5z"/>
</svg>
</div>
<div class="flex-1 min-w-0">
<div class="text-sm font-medium text-t1">{{ $qr->label ?: $qr->ulid }}</div>
<div class="text-xs text-t2 truncate">{{ $qr->type }} · {{ $qr->payload['url'] ?? '' }}</div>
</div>
<span class="text-xs px-2 py-0.5 rounded-full {{ $qr->is_dynamic ? 'bg-blue/10 text-blue' : 'bg-white/[.06] text-t3' }}">
{{ $qr->is_dynamic ? 'Dynamic' : 'Static' }}
</span>
<div class="flex items-center gap-3 opacity-0 group-hover:opacity-100 transition-opacity">
<button
@click="$dispatch('openModal', {component: 'modals.edit-qr-code', arguments: {qrUlid: '{{ $qr->ulid }}'}})"
class="text-t2 text-xs hover:text-t1">Edit</button>
<button
@click="$dispatch('openModal', {component: 'modals.delete-qr-code', arguments: {qrUlid: '{{ $qr->ulid }}'}})"
class="text-red text-xs hover:opacity-80">Delete</button>
</div>
</div> </div>
<h3 class="text-lg font-medium text-t1 mb-2">No QR Codes yet</h3> @empty
<p class="text-sm text-t2 mb-6">Create QR codes for URLs, vCards, WiFi and more.</p> <div class="p-12 text-center">
<button class="px-4 py-2.5 bg-blue text-white rounded-lg text-sm font-medium hover:opacity-90"> <div class="w-16 h-16 bg-s2 rounded-2xl flex items-center justify-center mx-auto mb-4">
Create your first QR Code <svg class="w-8 h-8 text-t3" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5">
</button> <path stroke-linecap="round" stroke-linejoin="round" d="M3.75 4.875c0-.621.504-1.125 1.125-1.125h4.5c.621 0 1.125.504 1.125 1.125v4.5c0 .621-.504 1.125-1.125 1.125h-4.5A1.125 1.125 0 013.75 9.375v-4.5zM3.75 14.625c0-.621.504-1.125 1.125-1.125h4.5c.621 0 1.125.504 1.125 1.125v4.5c0 .621-.504 1.125-1.125 1.125h-4.5a1.125 1.125 0 01-1.125-1.125v-4.5zM13.5 4.875c0-.621.504-1.125 1.125-1.125h4.5c.621 0 1.125.504 1.125 1.125v4.5c0 .621-.504 1.125-1.125 1.125h-4.5A1.125 1.125 0 0113.5 9.375v-4.5z"/>
</svg>
</div>
<h3 class="text-lg font-medium text-t1 mb-2">No QR Codes yet</h3>
<p class="text-sm text-t2 mb-6">Create QR codes for URLs, vCards, WiFi and more.</p>
@if($ws)
<button
@click="$dispatch('openModal', {component: 'modals.create-qr-code', arguments: {workspaceUlid: '{{ $ws->ulid }}'}})"
class="px-4 py-2.5 bg-accent-gradient text-white rounded-lg text-sm font-medium hover:opacity-90 transition-opacity">
Create your first QR Code
</button>
@endif
</div>
@endforelse
</div> </div>
@if($qrCodes->hasPages())
<div class="mt-4">{{ $qrCodes->links() }}</div>
@endif
</div> </div>

View File

@ -50,7 +50,7 @@ it('creates bio page and dispatches event', function () {
Livewire::actingAs($user)->test(CreateBioPage::class) Livewire::actingAs($user)->test(CreateBioPage::class)
->set('title', 'My Bio Page') ->set('title', 'My Bio Page')
->call('save') ->call('save')
->assertDispatched('bioPageCreated') ->assertDispatched('bio-created')
->assertDispatched('toast'); ->assertDispatched('toast');
$this->assertDatabaseHas('bio_pages', [ $this->assertDatabaseHas('bio_pages', [
@ -77,7 +77,7 @@ it('updates bio page and dispatches event', function () {
Livewire::actingAs($user)->test(EditBioPage::class, ['bioUlid' => $bio->ulid]) Livewire::actingAs($user)->test(EditBioPage::class, ['bioUlid' => $bio->ulid])
->set('title', 'Updated Title') ->set('title', 'Updated Title')
->call('save') ->call('save')
->assertDispatched('bioPageUpdated') ->assertDispatched('bio-updated')
->assertDispatched('toast'); ->assertDispatched('toast');
}); });
@ -99,7 +99,7 @@ it('deletes bio page and dispatches event', function () {
Livewire::actingAs($user)->test(DeleteBioPage::class, ['bioUlid' => $bio->ulid]) Livewire::actingAs($user)->test(DeleteBioPage::class, ['bioUlid' => $bio->ulid])
->call('delete') ->call('delete')
->assertDispatched('bioPageDeleted') ->assertDispatched('bio-deleted')
->assertDispatched('toast'); ->assertDispatched('toast');
$this->assertSoftDeleted('bio_pages', ['id' => $bio->id]); $this->assertSoftDeleted('bio_pages', ['id' => $bio->id]);

View File

@ -61,7 +61,7 @@ it('creates link and dispatches linkCreated', function () {
Livewire::actingAs($user)->test(CreateLink::class) Livewire::actingAs($user)->test(CreateLink::class)
->set('targetUrl', 'https://example.com/page') ->set('targetUrl', 'https://example.com/page')
->call('save') ->call('save')
->assertDispatched('linkCreated'); ->assertDispatched('link-created');
expect(Link::where('workspace_id', $ws->id)->where('target_url', 'https://example.com/page')->exists())->toBeTrue(); expect(Link::where('workspace_id', $ws->id)->where('target_url', 'https://example.com/page')->exists())->toBeTrue();
}); });
@ -109,7 +109,7 @@ it('updates link on save', function () {
->set('targetUrl', 'https://new.example.com') ->set('targetUrl', 'https://new.example.com')
->set('slug', 'newslug') ->set('slug', 'newslug')
->call('save') ->call('save')
->assertDispatched('linkUpdated'); ->assertDispatched('link-updated');
expect($link->fresh()->target_url)->toBe('https://new.example.com'); expect($link->fresh()->target_url)->toBe('https://new.example.com');
expect($link->fresh()->slug)->toBe('newslug'); expect($link->fresh()->slug)->toBe('newslug');
@ -151,7 +151,7 @@ it('soft-deletes link and dispatches linkDeleted', function () {
Livewire::actingAs($user)->test(DeleteLink::class, ['linkUlid' => $link->ulid]) Livewire::actingAs($user)->test(DeleteLink::class, ['linkUlid' => $link->ulid])
->call('delete') ->call('delete')
->assertDispatched('linkDeleted'); ->assertDispatched('link-deleted');
expect(Link::withTrashed()->find($link->id)->deleted_at)->not->toBeNull(); expect(Link::withTrashed()->find($link->id)->deleted_at)->not->toBeNull();
}); });

View File

@ -61,7 +61,7 @@ it('creates qr code and dispatches event', function () {
->set('url', 'https://example.com') ->set('url', 'https://example.com')
->set('label', 'My QR') ->set('label', 'My QR')
->call('save') ->call('save')
->assertDispatched('qrCreated') ->assertDispatched('qr-created')
->assertDispatched('toast'); ->assertDispatched('toast');
$this->assertDatabaseHas('qr_codes', [ $this->assertDatabaseHas('qr_codes', [
@ -90,7 +90,7 @@ it('updates qr code and dispatches event', function () {
Livewire::actingAs($user)->test(EditQrCode::class, ['qrUlid' => $qr->ulid]) Livewire::actingAs($user)->test(EditQrCode::class, ['qrUlid' => $qr->ulid])
->set('url', 'https://new.com') ->set('url', 'https://new.com')
->call('save') ->call('save')
->assertDispatched('qrUpdated') ->assertDispatched('qr-updated')
->assertDispatched('toast'); ->assertDispatched('toast');
expect($qr->fresh()->payload['url'])->toBe('https://new.com'); expect($qr->fresh()->payload['url'])->toBe('https://new.com');
@ -114,7 +114,7 @@ it('deletes qr code and dispatches event', function () {
Livewire::actingAs($user)->test(DeleteQrCode::class, ['qrUlid' => $qr->ulid]) Livewire::actingAs($user)->test(DeleteQrCode::class, ['qrUlid' => $qr->ulid])
->call('delete') ->call('delete')
->assertDispatched('qrDeleted') ->assertDispatched('qr-deleted')
->assertDispatched('toast'); ->assertDispatched('toast');
$this->assertSoftDeleted('qr_codes', ['id' => $qr->id]); $this->assertSoftDeleted('qr_codes', ['id' => $qr->id]);