2026-01-22 23:38:00 +01:00
|
|
|
<?php
|
|
|
|
|
|
|
|
|
|
namespace App\Livewire;
|
|
|
|
|
|
2026-07-25 20:15:52 +02:00
|
|
|
use App\Actions\CreateChannelAction;
|
2026-08-14 00:54:17 +02:00
|
|
|
use App\Enums\LogLevelEnum;
|
|
|
|
|
use App\Events\ActionPerformed;
|
|
|
|
|
use App\Models\ArticlePublication;
|
2026-07-25 20:15:52 +02:00
|
|
|
use App\Models\Language;
|
2026-01-22 23:38:00 +01:00
|
|
|
use App\Models\PlatformAccount;
|
|
|
|
|
use App\Models\PlatformChannel;
|
2026-07-25 20:15:52 +02:00
|
|
|
use App\Models\PlatformInstance;
|
2026-08-14 00:54:17 +02:00
|
|
|
use App\Models\RouteArticle;
|
2026-08-06 00:20:49 +02:00
|
|
|
use App\Services\Platform\CommunityDirectory;
|
|
|
|
|
use Exception;
|
2026-03-18 20:01:25 +01:00
|
|
|
use Illuminate\Contracts\View\View;
|
2026-07-29 20:15:43 +02:00
|
|
|
use Illuminate\Database\UniqueConstraintViolationException;
|
2026-07-25 20:15:52 +02:00
|
|
|
use Illuminate\Validation\Rule;
|
2026-01-22 23:38:00 +01:00
|
|
|
use Livewire\Component;
|
2026-07-25 20:15:52 +02:00
|
|
|
use RuntimeException;
|
2026-01-22 23:38:00 +01:00
|
|
|
|
|
|
|
|
class Channels extends Component
|
|
|
|
|
{
|
|
|
|
|
public ?int $managingChannelId = null;
|
|
|
|
|
|
2026-07-25 20:15:52 +02:00
|
|
|
public bool $showCreateModal = false;
|
|
|
|
|
|
2026-08-06 00:20:49 +02:00
|
|
|
public ?int $newCommunityId = null;
|
2026-07-25 20:15:52 +02:00
|
|
|
|
|
|
|
|
public ?int $newPlatformInstanceId = null;
|
|
|
|
|
|
2026-08-06 00:20:49 +02:00
|
|
|
/** @var array<int, array{id: int, name: string, title: string}> */
|
|
|
|
|
public array $availableCommunities = [];
|
|
|
|
|
|
|
|
|
|
public ?string $communityLoadError = null;
|
|
|
|
|
|
2026-07-25 20:15:52 +02:00
|
|
|
public ?int $newLanguageId = null;
|
|
|
|
|
|
|
|
|
|
public string $newDescription = '';
|
|
|
|
|
|
2026-08-13 18:44:59 +02:00
|
|
|
public ?int $editingChannelId = null;
|
|
|
|
|
|
|
|
|
|
public string $editDisplayName = '';
|
|
|
|
|
|
|
|
|
|
public ?int $editLanguageId = null;
|
|
|
|
|
|
|
|
|
|
public string $editDescription = '';
|
|
|
|
|
|
2026-01-22 23:38:00 +01:00
|
|
|
public function toggle(int $channelId): void
|
|
|
|
|
{
|
|
|
|
|
$channel = PlatformChannel::findOrFail($channelId);
|
2026-03-08 14:18:28 +01:00
|
|
|
$channel->is_active = ! $channel->is_active;
|
2026-01-22 23:38:00 +01:00
|
|
|
$channel->save();
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-14 00:54:17 +02:00
|
|
|
public function deleteChannel(int $channelId): void
|
|
|
|
|
{
|
|
|
|
|
$channel = PlatformChannel::find($channelId);
|
|
|
|
|
|
|
|
|
|
if (! $channel instanceof PlatformChannel) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$name = $channel->display_name;
|
|
|
|
|
|
|
|
|
|
// Routes, keywords, route articles, publications, account links and synced posts
|
|
|
|
|
// all cascade at the database level.
|
|
|
|
|
$channel->delete();
|
|
|
|
|
|
|
|
|
|
if ($this->managingChannelId === $channelId) {
|
|
|
|
|
$this->managingChannelId = null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ($this->editingChannelId === $channelId) {
|
|
|
|
|
$this->editingChannelId = null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ActionPerformed::dispatch('Deleted platform channel', LogLevelEnum::WARNING, [
|
|
|
|
|
'platform_channel_id' => $channelId,
|
|
|
|
|
'display_name' => $name,
|
|
|
|
|
]);
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-25 20:15:52 +02:00
|
|
|
public function openCreateModal(): void
|
|
|
|
|
{
|
2026-08-06 00:20:49 +02:00
|
|
|
$this->reset(['newCommunityId', 'newPlatformInstanceId', 'newLanguageId', 'newDescription', 'availableCommunities', 'communityLoadError']);
|
2026-07-25 20:15:52 +02:00
|
|
|
$this->resetErrorBag();
|
|
|
|
|
$this->showCreateModal = true;
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-06 00:20:49 +02:00
|
|
|
public function updatedNewPlatformInstanceId(?int $value): void
|
|
|
|
|
{
|
|
|
|
|
$this->reset(['newCommunityId', 'availableCommunities', 'communityLoadError']);
|
|
|
|
|
|
|
|
|
|
if (! $value) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$instance = PlatformInstance::find($value);
|
|
|
|
|
|
|
|
|
|
if (! $instance) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
$this->availableCommunities = app(CommunityDirectory::class)->forInstance($instance);
|
|
|
|
|
} catch (Exception $e) {
|
|
|
|
|
$this->communityLoadError = 'Could not reach this instance to list its communities: '.$e->getMessage();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function refreshCommunities(): void
|
|
|
|
|
{
|
|
|
|
|
$instance = $this->newPlatformInstanceId ? PlatformInstance::find($this->newPlatformInstanceId) : null;
|
|
|
|
|
|
|
|
|
|
if (! $instance) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
app(CommunityDirectory::class)->forget($instance);
|
|
|
|
|
$this->updatedNewPlatformInstanceId($this->newPlatformInstanceId);
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-25 20:15:52 +02:00
|
|
|
public function closeCreateModal(): void
|
|
|
|
|
{
|
|
|
|
|
$this->showCreateModal = false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function createChannel(CreateChannelAction $action): void
|
|
|
|
|
{
|
|
|
|
|
$this->validate([
|
2026-08-06 00:20:49 +02:00
|
|
|
'newCommunityId' => [
|
2026-07-25 20:15:52 +02:00
|
|
|
'required',
|
2026-08-06 00:20:49 +02:00
|
|
|
'integer',
|
|
|
|
|
Rule::in(collect($this->availableCommunities)->pluck('id')->all()),
|
|
|
|
|
Rule::unique('platform_channels', 'channel_id')
|
2026-07-25 20:15:52 +02:00
|
|
|
->where('platform_instance_id', $this->newPlatformInstanceId),
|
|
|
|
|
],
|
|
|
|
|
'newPlatformInstanceId' => 'required|integer|exists:platform_instances,id',
|
|
|
|
|
'newLanguageId' => 'nullable|integer|exists:languages,id',
|
|
|
|
|
], [
|
2026-08-06 00:20:49 +02:00
|
|
|
'newCommunityId.in' => 'Select a community from this instance.',
|
|
|
|
|
'newCommunityId.unique' => 'A channel for this community already exists.',
|
2026-07-25 20:15:52 +02:00
|
|
|
]);
|
|
|
|
|
|
2026-08-06 00:20:49 +02:00
|
|
|
$name = collect($this->availableCommunities)->firstWhere('id', $this->newCommunityId)['name'] ?? null;
|
|
|
|
|
|
2026-07-25 20:15:52 +02:00
|
|
|
try {
|
|
|
|
|
$action->execute(
|
2026-08-06 00:20:49 +02:00
|
|
|
$name,
|
|
|
|
|
$this->newCommunityId,
|
2026-07-25 20:15:52 +02:00
|
|
|
$this->newPlatformInstanceId,
|
|
|
|
|
$this->newLanguageId,
|
|
|
|
|
// Blade textarea binds an empty string when blank; the action expects null for "no description".
|
|
|
|
|
$this->newDescription !== '' ? $this->newDescription : null,
|
|
|
|
|
);
|
2026-07-29 20:15:43 +02:00
|
|
|
} catch (UniqueConstraintViolationException $e) {
|
2026-08-06 00:20:49 +02:00
|
|
|
$this->addError('newCommunityId', 'A channel for this community already exists.');
|
2026-07-29 20:15:43 +02:00
|
|
|
|
|
|
|
|
return;
|
2026-07-25 20:15:52 +02:00
|
|
|
} catch (RuntimeException $e) {
|
|
|
|
|
$this->addError('newPlatformInstanceId', $e->getMessage());
|
|
|
|
|
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$this->closeCreateModal();
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-13 18:44:59 +02:00
|
|
|
public function openEditModal(int $channelId): void
|
|
|
|
|
{
|
|
|
|
|
$channel = PlatformChannel::findOrFail($channelId);
|
|
|
|
|
|
|
|
|
|
$this->resetErrorBag();
|
|
|
|
|
$this->editingChannelId = $channelId;
|
|
|
|
|
$this->editDisplayName = $channel->display_name;
|
|
|
|
|
$this->editLanguageId = $channel->language_id;
|
|
|
|
|
$this->editDescription = $channel->description ?? '';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function closeEditModal(): void
|
|
|
|
|
{
|
|
|
|
|
$this->editingChannelId = null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// The community pairing (name, channel_id, platform_instance_id) is deliberately immutable:
|
|
|
|
|
// it is the channel's remote identity, unique per instance, and re-pointing it would change
|
|
|
|
|
// the meaning of every route already attached.
|
|
|
|
|
public function updateChannel(): void
|
|
|
|
|
{
|
|
|
|
|
if ($this->editingChannelId === null) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$this->validate([
|
|
|
|
|
'editDisplayName' => 'required|string|max:255',
|
|
|
|
|
'editLanguageId' => 'nullable|integer|exists:languages,id',
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
PlatformChannel::findOrFail($this->editingChannelId)->update([
|
|
|
|
|
'display_name' => $this->editDisplayName,
|
|
|
|
|
'language_id' => $this->editLanguageId,
|
|
|
|
|
'description' => $this->editDescription !== '' ? $this->editDescription : null,
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
$this->closeEditModal();
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-22 23:38:00 +01:00
|
|
|
public function openAccountModal(int $channelId): void
|
|
|
|
|
{
|
|
|
|
|
$this->managingChannelId = $channelId;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function closeAccountModal(): void
|
|
|
|
|
{
|
|
|
|
|
$this->managingChannelId = null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function attachAccount(int $accountId): void
|
|
|
|
|
{
|
2026-03-08 14:18:28 +01:00
|
|
|
if (! $this->managingChannelId) {
|
2026-01-22 23:38:00 +01:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$channel = PlatformChannel::findOrFail($this->managingChannelId);
|
|
|
|
|
|
2026-03-08 14:18:28 +01:00
|
|
|
if (! $channel->platformAccounts()->where('platform_account_id', $accountId)->exists()) {
|
2026-01-22 23:38:00 +01:00
|
|
|
$channel->platformAccounts()->attach($accountId, [
|
|
|
|
|
'is_active' => true,
|
|
|
|
|
'priority' => 1,
|
|
|
|
|
'created_at' => now(),
|
|
|
|
|
'updated_at' => now(),
|
|
|
|
|
]);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function detachAccount(int $channelId, int $accountId): void
|
|
|
|
|
{
|
|
|
|
|
$channel = PlatformChannel::findOrFail($channelId);
|
|
|
|
|
$channel->platformAccounts()->detach($accountId);
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-14 00:54:17 +02:00
|
|
|
/**
|
|
|
|
|
* Row counts per channel, so the delete confirmation can say what is about to go.
|
|
|
|
|
*
|
|
|
|
|
* @return array<int, array{articles: int, publications: int}>
|
|
|
|
|
*/
|
|
|
|
|
private function deletionImpact(): array
|
|
|
|
|
{
|
|
|
|
|
/** @var array<int, int> $articles */
|
|
|
|
|
$articles = RouteArticle::query()
|
|
|
|
|
->selectRaw('platform_channel_id, COUNT(*) as aggregate')
|
|
|
|
|
->groupBy('platform_channel_id')
|
|
|
|
|
->pluck('aggregate', 'platform_channel_id')
|
|
|
|
|
->all();
|
|
|
|
|
|
|
|
|
|
/** @var array<int, int> $publications */
|
|
|
|
|
$publications = ArticlePublication::query()
|
|
|
|
|
->selectRaw('platform_channel_id, COUNT(*) as aggregate')
|
|
|
|
|
->groupBy('platform_channel_id')
|
|
|
|
|
->pluck('aggregate', 'platform_channel_id')
|
|
|
|
|
->all();
|
|
|
|
|
|
|
|
|
|
$impact = [];
|
|
|
|
|
|
|
|
|
|
foreach (array_keys($articles + $publications) as $channelId) {
|
|
|
|
|
$impact[$channelId] = [
|
|
|
|
|
'articles' => (int) ($articles[$channelId] ?? 0),
|
|
|
|
|
'publications' => (int) ($publications[$channelId] ?? 0),
|
|
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $impact;
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-18 20:01:25 +01:00
|
|
|
public function render(): View
|
2026-01-22 23:38:00 +01:00
|
|
|
{
|
|
|
|
|
$channels = PlatformChannel::with(['platformInstance', 'platformAccounts'])->orderBy('name')->get();
|
|
|
|
|
$allAccounts = PlatformAccount::where('is_active', true)->get();
|
|
|
|
|
|
|
|
|
|
$managingChannel = $this->managingChannelId
|
|
|
|
|
? PlatformChannel::with('platformAccounts')->find($this->managingChannelId)
|
|
|
|
|
: null;
|
|
|
|
|
|
|
|
|
|
$availableAccounts = $managingChannel
|
2026-03-08 14:18:28 +01:00
|
|
|
? $allAccounts->filter(fn ($account) => ! $managingChannel->platformAccounts->contains('id', $account->id))
|
2026-01-22 23:38:00 +01:00
|
|
|
: collect();
|
|
|
|
|
|
|
|
|
|
return view('livewire.channels', [
|
|
|
|
|
'channels' => $channels,
|
|
|
|
|
'managingChannel' => $managingChannel,
|
2026-08-13 18:44:59 +02:00
|
|
|
'editingChannel' => $this->editingChannelId !== null
|
|
|
|
|
? PlatformChannel::with('platformInstance')->find($this->editingChannelId)
|
|
|
|
|
: null,
|
2026-01-22 23:38:00 +01:00
|
|
|
'availableAccounts' => $availableAccounts,
|
2026-08-14 00:54:17 +02:00
|
|
|
'deletionImpact' => $this->deletionImpact(),
|
2026-07-25 20:15:52 +02:00
|
|
|
'platformInstances' => PlatformInstance::where('is_active', true)->orderBy('name')->get(),
|
|
|
|
|
'languages' => Language::where('is_active', true)->orderBy('name')->get(),
|
2026-01-22 23:38:00 +01:00
|
|
|
])->layout('layouts.app');
|
|
|
|
|
}
|
|
|
|
|
}
|