Implements the Settings foundation workspace controls. Includes: - Settings foundation UI/controls scoped to workspace context - Related onboarding/consent flow adjustments as included in branch history Testing: - `vendor/bin/sail artisan test --compact --no-ansi --filter=SettingsFoundation` Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de> Reviewed-on: #119
257 lines
8.0 KiB
PHP
257 lines
8.0 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Filament\Pages\Settings;
|
|
|
|
use App\Models\User;
|
|
use App\Models\Workspace;
|
|
use App\Services\Auth\WorkspaceCapabilityResolver;
|
|
use App\Services\Settings\SettingsResolver;
|
|
use App\Services\Settings\SettingsWriter;
|
|
use App\Support\Auth\Capabilities;
|
|
use App\Support\Ui\ActionSurface\ActionSurfaceDeclaration;
|
|
use App\Support\Ui\ActionSurface\Enums\ActionSurfaceProfile;
|
|
use App\Support\Ui\ActionSurface\Enums\ActionSurfaceSlot;
|
|
use App\Support\Workspaces\WorkspaceContext;
|
|
use BackedEnum;
|
|
use Filament\Actions\Action;
|
|
use Filament\Forms\Components\TextInput;
|
|
use Filament\Notifications\Notification;
|
|
use Filament\Pages\Page;
|
|
use Filament\Schemas\Components\Section;
|
|
use Filament\Schemas\Schema;
|
|
use Illuminate\Validation\ValidationException;
|
|
use UnitEnum;
|
|
|
|
class WorkspaceSettings extends Page
|
|
{
|
|
protected static bool $isDiscovered = false;
|
|
|
|
protected static bool $shouldRegisterNavigation = false;
|
|
|
|
protected static ?string $slug = 'settings/workspace';
|
|
|
|
protected static ?string $title = 'Workspace settings';
|
|
|
|
protected static string|BackedEnum|null $navigationIcon = 'heroicon-o-cog-6-tooth';
|
|
|
|
protected static string|UnitEnum|null $navigationGroup = 'Settings';
|
|
|
|
protected static ?int $navigationSort = 20;
|
|
|
|
public Workspace $workspace;
|
|
|
|
/**
|
|
* @var array<string, mixed>
|
|
*/
|
|
public array $data = [];
|
|
|
|
/**
|
|
* @return array<Action>
|
|
*/
|
|
protected function getHeaderActions(): array
|
|
{
|
|
return [
|
|
Action::make('save')
|
|
->label('Save')
|
|
->action(function (): void {
|
|
$this->save();
|
|
})
|
|
->disabled(fn (): bool => ! $this->currentUserCanManage())
|
|
->tooltip(fn (): ?string => $this->currentUserCanManage()
|
|
? null
|
|
: 'You do not have permission to manage workspace settings.'),
|
|
Action::make('reset')
|
|
->label('Reset to default')
|
|
->color('danger')
|
|
->requiresConfirmation()
|
|
->action(function (): void {
|
|
$this->resetSetting();
|
|
})
|
|
->disabled(fn (): bool => ! $this->currentUserCanManage())
|
|
->tooltip(fn (): ?string => $this->currentUserCanManage()
|
|
? null
|
|
: 'You do not have permission to manage workspace settings.'),
|
|
];
|
|
}
|
|
|
|
public static function actionSurfaceDeclaration(): ActionSurfaceDeclaration
|
|
{
|
|
return ActionSurfaceDeclaration::forPage(ActionSurfaceProfile::ListOnlyReadOnly)
|
|
->satisfy(ActionSurfaceSlot::ListHeader, 'Header actions provide save and reset controls for the settings form.')
|
|
->exempt(ActionSurfaceSlot::InspectAffordance, 'Workspace settings are edited as a singleton form without a record inspect action.')
|
|
->exempt(ActionSurfaceSlot::ListRowMoreMenu, 'The page does not render table rows with secondary actions.')
|
|
->exempt(ActionSurfaceSlot::ListBulkMoreGroup, 'The page has no bulk actions because it manages a single settings scope.')
|
|
->exempt(ActionSurfaceSlot::ListEmptyState, 'The settings form is always rendered and has no list empty state.');
|
|
}
|
|
|
|
public function mount(): void
|
|
{
|
|
$user = auth()->user();
|
|
|
|
if (! $user instanceof User) {
|
|
abort(403);
|
|
}
|
|
|
|
$workspaceId = app(WorkspaceContext::class)->currentWorkspaceId(request());
|
|
|
|
if ($workspaceId === null) {
|
|
$this->redirect('/admin/choose-workspace');
|
|
|
|
return;
|
|
}
|
|
|
|
$workspace = Workspace::query()->whereKey($workspaceId)->first();
|
|
|
|
if (! $workspace instanceof Workspace) {
|
|
abort(404);
|
|
}
|
|
|
|
$this->workspace = $workspace;
|
|
|
|
$this->authorizeWorkspaceView($user);
|
|
|
|
$this->loadFormState();
|
|
}
|
|
|
|
public function content(Schema $schema): Schema
|
|
{
|
|
return $schema
|
|
->statePath('data')
|
|
->schema([
|
|
Section::make('Backup settings')
|
|
->description('Workspace defaults used when a schedule has no explicit value.')
|
|
->schema([
|
|
TextInput::make('backup_retention_keep_last_default')
|
|
->label('Default retention keep-last')
|
|
->numeric()
|
|
->integer()
|
|
->minValue(1)
|
|
->required()
|
|
->disabled(fn (): bool => ! $this->currentUserCanManage())
|
|
->helperText('Fallback value for backup schedule retention when retention_keep_last is empty.'),
|
|
]),
|
|
]);
|
|
}
|
|
|
|
public function save(): void
|
|
{
|
|
$user = auth()->user();
|
|
|
|
if (! $user instanceof User) {
|
|
abort(403);
|
|
}
|
|
|
|
$this->authorizeWorkspaceManage($user);
|
|
|
|
try {
|
|
app(SettingsWriter::class)->updateWorkspaceSetting(
|
|
actor: $user,
|
|
workspace: $this->workspace,
|
|
domain: 'backup',
|
|
key: 'retention_keep_last_default',
|
|
value: $this->data['backup_retention_keep_last_default'] ?? null,
|
|
);
|
|
} catch (ValidationException $exception) {
|
|
$errors = $exception->errors();
|
|
|
|
if (isset($errors['value'])) {
|
|
throw ValidationException::withMessages([
|
|
'data.backup_retention_keep_last_default' => $errors['value'],
|
|
]);
|
|
}
|
|
|
|
throw $exception;
|
|
}
|
|
|
|
$this->loadFormState();
|
|
|
|
Notification::make()
|
|
->title('Workspace settings saved')
|
|
->success()
|
|
->send();
|
|
}
|
|
|
|
public function resetSetting(): void
|
|
{
|
|
$user = auth()->user();
|
|
|
|
if (! $user instanceof User) {
|
|
abort(403);
|
|
}
|
|
|
|
$this->authorizeWorkspaceManage($user);
|
|
|
|
app(SettingsWriter::class)->resetWorkspaceSetting(
|
|
actor: $user,
|
|
workspace: $this->workspace,
|
|
domain: 'backup',
|
|
key: 'retention_keep_last_default',
|
|
);
|
|
|
|
$this->loadFormState();
|
|
|
|
Notification::make()
|
|
->title('Workspace setting reset to default')
|
|
->success()
|
|
->send();
|
|
}
|
|
|
|
private function loadFormState(): void
|
|
{
|
|
$resolvedValue = app(SettingsResolver::class)->resolveValue(
|
|
workspace: $this->workspace,
|
|
domain: 'backup',
|
|
key: 'retention_keep_last_default',
|
|
);
|
|
|
|
$this->data = [
|
|
'backup_retention_keep_last_default' => is_numeric($resolvedValue) ? (int) $resolvedValue : 30,
|
|
];
|
|
}
|
|
|
|
private function currentUserCanManage(): bool
|
|
{
|
|
$user = auth()->user();
|
|
|
|
if (! $user instanceof User || ! $this->workspace instanceof Workspace) {
|
|
return false;
|
|
}
|
|
|
|
/** @var WorkspaceCapabilityResolver $resolver */
|
|
$resolver = app(WorkspaceCapabilityResolver::class);
|
|
|
|
return $resolver->isMember($user, $this->workspace)
|
|
&& $resolver->can($user, $this->workspace, Capabilities::WORKSPACE_SETTINGS_MANAGE);
|
|
}
|
|
|
|
private function authorizeWorkspaceView(User $user): void
|
|
{
|
|
/** @var WorkspaceCapabilityResolver $resolver */
|
|
$resolver = app(WorkspaceCapabilityResolver::class);
|
|
|
|
if (! $resolver->isMember($user, $this->workspace)) {
|
|
abort(404);
|
|
}
|
|
|
|
if (! $resolver->can($user, $this->workspace, Capabilities::WORKSPACE_SETTINGS_VIEW)) {
|
|
abort(403);
|
|
}
|
|
}
|
|
|
|
private function authorizeWorkspaceManage(User $user): void
|
|
{
|
|
/** @var WorkspaceCapabilityResolver $resolver */
|
|
$resolver = app(WorkspaceCapabilityResolver::class);
|
|
|
|
if (! $resolver->isMember($user, $this->workspace)) {
|
|
abort(404);
|
|
}
|
|
|
|
if (! $resolver->can($user, $this->workspace, Capabilities::WORKSPACE_SETTINGS_MANAGE)) {
|
|
abort(403);
|
|
}
|
|
}
|
|
}
|