-
-
Notifications
You must be signed in to change notification settings - Fork 4
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
a702275
commit 3bd9d4a
Showing
5 changed files
with
358 additions
and
5 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,119 @@ | ||
<?php | ||
|
||
declare(strict_types=1); | ||
|
||
namespace App\Livewire\BackupTasks\Modals; | ||
|
||
use App\Models\BackupTask; | ||
use App\Models\User; | ||
use App\Rules\UniqueScheduledTimePerRemoteServer; | ||
use Illuminate\Support\Collection; | ||
use Illuminate\Support\Facades\Auth; | ||
use Illuminate\Validation\Rule; | ||
use Illuminate\View\View; | ||
use Livewire\Attributes\On; | ||
use Livewire\Component; | ||
use Toaster; | ||
|
||
class CopyTaskModal extends Component | ||
{ | ||
public ?int $backupTaskToCopyId = null; | ||
public ?string $optionalNewLabel = null; | ||
public string $frequency = 'daily'; | ||
public string $timeToRun = '00:00'; | ||
/** @var Collection<int, BackupTask> */ | ||
public Collection $backupTasks; | ||
|
||
public function mount(): void | ||
{ | ||
$user = Auth::user(); | ||
$this->backupTasks = $user instanceof User ? $user->getAttribute('backupTasks') : new Collection; | ||
} | ||
|
||
#[On('open-modal.copy-backup-task')] | ||
public function resetModal(): void | ||
{ | ||
$this->reset(['backupTaskToCopyId', 'optionalNewLabel', 'frequency', 'timeToRun']); | ||
$user = Auth::user(); | ||
$this->backupTasks = $user instanceof User ? $user->getAttribute('backupTasks') : new Collection; | ||
} | ||
|
||
public function updatedBackupTaskToCopyId(?int $value): void | ||
{ | ||
if ($value === null) { | ||
return; | ||
} | ||
|
||
$task = BackupTask::find($value); | ||
if ($task) { | ||
$this->frequency = $task->frequency ?? 'daily'; | ||
$this->timeToRun = $task->time_to_run_at ?? '00:00'; | ||
} | ||
} | ||
|
||
public function copyTask(): void | ||
{ | ||
$this->validate(); | ||
|
||
$originalTask = BackupTask::findOrFail($this->backupTaskToCopyId); | ||
|
||
$newTask = $originalTask->replicate(); | ||
$newTask->label = $this->optionalNewLabel ?: $originalTask->label . ' (Copy)'; | ||
$newTask->frequency = $this->frequency; | ||
$newTask->time_to_run_at = $this->timeToRun; | ||
$newTask->custom_cron_expression = null; // Reset custom cron expression | ||
$newTask->save(); | ||
|
||
// Copy relationships | ||
$newTask->tags()->sync($originalTask->tags); | ||
$newTask->notificationStreams()->sync($originalTask->notificationStreams); | ||
|
||
$this->dispatch('task-copied'); | ||
$this->dispatch('close-modal', 'copy-backup-task'); | ||
$this->resetModal(); | ||
$this->dispatch('refreshBackupTasksTable'); | ||
|
||
Toaster::success('The backup task has been copied.'); | ||
} | ||
|
||
public function render(): View | ||
{ | ||
return view('livewire.backup-tasks.modals.copy-task-modal', [ | ||
'backupTasks' => $this->backupTasks, | ||
'backupTimes' => $this->getBackupTimes(), | ||
]); | ||
} | ||
|
||
/** | ||
* @return array<string, mixed> | ||
*/ | ||
protected function rules(): array | ||
{ | ||
return [ | ||
'backupTaskToCopyId' => ['required', 'exists:backup_tasks,id'], | ||
'optionalNewLabel' => ['nullable', 'string', 'max:255'], | ||
'frequency' => ['required', 'string', Rule::in(['daily', 'weekly'])], | ||
'timeToRun' => [ | ||
'required', | ||
'string', | ||
'regex:/^([01]?\d|2[0-3]):([0-5]\d)$/', | ||
new UniqueScheduledTimePerRemoteServer((int) $this->getRemoteServerId()), | ||
], | ||
]; | ||
} | ||
|
||
private function getRemoteServerId(): ?int | ||
{ | ||
return BackupTask::find($this->backupTaskToCopyId)?->remote_server_id; | ||
} | ||
|
||
/** | ||
* @return Collection<int, string> | ||
*/ | ||
private function getBackupTimes(): Collection | ||
{ | ||
return collect(range(0, 95))->map(function (int $quarterHour): string { | ||
return sprintf('%02d:%02d', intdiv($quarterHour, 4), ($quarterHour % 4) * 15); | ||
}); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
94 changes: 94 additions & 0 deletions
94
resources/views/livewire/backup-tasks/modals/copy-task-modal.blade.php
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,94 @@ | ||
<div> | ||
<x-modal name="copy-backup-task" :show="$errors->isNotEmpty()" focusable> | ||
<x-slot name="title"> | ||
{{ __('Copy Backup Task') }} | ||
</x-slot> | ||
<x-slot name="description"> | ||
{{ __('Create a new backup task by replicating an existing configuration.') }} | ||
</x-slot> | ||
<x-slot name="icon"> | ||
heroicon-o-document-duplicate | ||
</x-slot> | ||
<form wire:submit.prevent="copyTask"> | ||
<div> | ||
<div> | ||
<x-input-label for="backupTaskToCopyId" :value="__('Backup Task')" /> | ||
<x-select | ||
id="backupTaskToCopyId" | ||
name="backupTaskToCopyId" | ||
class="mt-1 block w-full" | ||
wire:model.live="backupTaskToCopyId"> | ||
<option value="">{{ __('Select a backup task') }}</option> | ||
@foreach ($backupTasks as $task) | ||
<option value="{{ $task->id }}">{{ $task->label }} ({{ $task->remoteServer->label }})</option> | ||
@endforeach | ||
</x-select> | ||
<x-input-error :messages="$errors->get('backupTaskToCopyId')" class="mt-2" /> | ||
<x-input-explain> | ||
{{ __('Please choose the backup task you wish to copy.') }} | ||
</x-input-explain> | ||
</div> | ||
<div class="mt-4"> | ||
<x-input-label for="optionalNewLabel" :value="__('Label (Optional)')" /> | ||
<x-text-input | ||
id="optionalNewLabel" | ||
name="optionalNewLabel" | ||
type="text" | ||
class="mt-1 block w-full" | ||
wire:model="optionalNewLabel" | ||
/> | ||
<x-input-error :messages="$errors->get('optionalNewLabel')" class="mt-2" /> | ||
<x-input-explain> | ||
{{ __('Optionally, you may choose a new label for the copied backup task.') }} | ||
</x-input-explain> | ||
</div> | ||
<div class="mt-4"> | ||
<div class="mt-2 flex space-x-4"> | ||
<div class="w-1/2"> | ||
<x-input-label for="frequency" :value="__('Frequency')" /> | ||
<x-select | ||
id="frequency" | ||
name="frequency" | ||
class="mt-1 block w-full" | ||
wire:model="frequency" | ||
> | ||
<option value="daily">{{ __('Daily') }}</option> | ||
<option value="weekly">{{ __('Weekly') }}</option> | ||
</x-select> | ||
<x-input-error :messages="$errors->get('frequency')" class="mt-2" /> | ||
</div> | ||
<div class="w-1/2"> | ||
<x-input-label for="timeToRun" :value="__('Time to Run')" /> | ||
<x-select | ||
id="timeToRun" | ||
name="timeToRun" | ||
class="mt-1 block w-full" | ||
wire:model="timeToRun" | ||
> | ||
@foreach ($backupTimes as $time) | ||
<option value="{{ $time }}">{{ $time }}</option> | ||
@endforeach | ||
</x-select> | ||
<x-input-error :messages="$errors->get('timeToRun')" class="mt-2" /> | ||
</div> | ||
</div> | ||
<x-input-explain> | ||
{{ __('Select a non-conflicting schedule for this task. Each server allows only one task per time slot. For advanced scheduling using Cron, please edit the task after copying.') }} | ||
</x-input-explain> | ||
</div> | ||
<div class="flex space-x-5"> | ||
<div class="w-4/6"> | ||
<x-primary-button type="button" class="mt-4" centered wire:click="copyTask" action="copyTask" loadingText="Copying..."> | ||
{{ __('Copy') }} | ||
</x-primary-button> | ||
</div> | ||
<div class="w-2/6 ml-2"> | ||
<x-secondary-button type="button" class="mt-4" centered x-on:click="$dispatch('close')"> | ||
{{ __('Cancel') }} | ||
</x-secondary-button> | ||
</div> | ||
</div> | ||
</div> | ||
</form> | ||
</x-modal> | ||
</div> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,129 @@ | ||
<?php | ||
|
||
declare(strict_types=1); | ||
|
||
use App\Livewire\BackupTasks\Modals\CopyTaskModal; | ||
use App\Models\BackupTask; | ||
use App\Models\NotificationStream; | ||
use App\Models\User; | ||
use Livewire\Livewire; | ||
|
||
beforeEach(function (): void { | ||
$this->user = User::factory()->create(); | ||
$this->backupTask = BackupTask::factory()->create([ | ||
'user_id' => $this->user->id, | ||
'frequency' => 'daily', | ||
'time_to_run_at' => '00:00', | ||
]); | ||
}); | ||
|
||
test('the component can be rendered', function (): void { | ||
$this->actingAs($this->user); | ||
|
||
Livewire::test(CopyTaskModal::class) | ||
->assertOk(); | ||
}); | ||
|
||
test('it loads user backup tasks', function (): void { | ||
$this->actingAs($this->user); | ||
|
||
Livewire::test(CopyTaskModal::class) | ||
->assertViewHas('backupTasks', fn ($backupTasks): bool => $backupTasks->count() === 1 && $backupTasks->first()->id === $this->backupTask->id | ||
); | ||
}); | ||
|
||
test('it updates frequency and time when task is selected', function (): void { | ||
$this->actingAs($this->user); | ||
|
||
Livewire::test(CopyTaskModal::class) | ||
->set('backupTaskToCopyId', $this->backupTask->id) | ||
->assertSet('frequency', $this->backupTask->frequency) | ||
->assertSet('timeToRun', $this->backupTask->time_to_run_at); | ||
}); | ||
|
||
test('it validates required fields', function (): void { | ||
$this->actingAs($this->user); | ||
|
||
Livewire::test(CopyTaskModal::class) | ||
->set('backupTaskToCopyId', $this->backupTask->id) | ||
->call('copyTask') | ||
->assertHasErrors(['timeToRun']); | ||
}); | ||
|
||
test('it validates frequency options', function (): void { | ||
$this->actingAs($this->user); | ||
|
||
Livewire::test(CopyTaskModal::class) | ||
->set('backupTaskToCopyId', $this->backupTask->id) | ||
->set('frequency', 'invalid') | ||
->call('copyTask') | ||
->assertHasErrors(['frequency']); | ||
}); | ||
|
||
test('it validates time format', function (): void { | ||
$this->actingAs($this->user); | ||
|
||
Livewire::test(CopyTaskModal::class) | ||
->set('backupTaskToCopyId', $this->backupTask->id) | ||
->set('timeToRun', 'invalid') | ||
->call('copyTask') | ||
->assertHasErrors(['timeToRun']); | ||
}); | ||
|
||
test('it successfully copies a task', function (): void { | ||
$this->actingAs($this->user); | ||
|
||
$newLabel = 'New Copied Task'; | ||
|
||
Livewire::test(CopyTaskModal::class) | ||
->set('backupTaskToCopyId', $this->backupTask->id) | ||
->set('optionalNewLabel', $newLabel) | ||
->set('frequency', 'weekly') | ||
->set('timeToRun', '12:00') | ||
->call('copyTask') | ||
->assertHasNoErrors() | ||
->assertDispatched('task-copied') | ||
->assertDispatched('close-modal') | ||
->assertDispatched('refreshBackupTasksTable'); | ||
|
||
$this->assertDatabaseHas('backup_tasks', [ | ||
'label' => $newLabel, | ||
'frequency' => 'weekly', | ||
'time_to_run_at' => '12:00', | ||
]); | ||
}); | ||
|
||
test('it copies task relationships', function (): void { | ||
$this->actingAs($this->user); | ||
|
||
$tag = $this->backupTask->tags()->create(['label' => 'Test Tag', 'user_id' => $this->user->id]); | ||
$stream = $this->backupTask->notificationStreams()->create(['label' => 'Test Stream', 'user_id' => $this->user->id, 'type' => NotificationStream::TYPE_EMAIL, 'value' => '[email protected]']); | ||
|
||
Livewire::test(CopyTaskModal::class) | ||
->set('backupTaskToCopyId', $this->backupTask->id) | ||
->set('frequency', 'daily') | ||
->set('timeToRun', '00:00') | ||
->call('copyTask'); | ||
|
||
$newTask = BackupTask::latest()->first(); | ||
|
||
expect($newTask->tags)->toHaveCount(1) | ||
->and($newTask->tags->first()->id)->toBe($tag->id) | ||
->and($newTask->notificationStreams)->toHaveCount(1) | ||
->and($newTask->notificationStreams->first()->id)->toBe($stream->id); | ||
}); | ||
|
||
test('it resets form after copying', function (): void { | ||
$this->actingAs($this->user); | ||
|
||
Livewire::test(CopyTaskModal::class) | ||
->set('backupTaskToCopyId', $this->backupTask->id) | ||
->set('optionalNewLabel', 'Test Label') | ||
->set('frequency', 'weekly') | ||
->set('timeToRun', '12:00') | ||
->call('copyTask') | ||
->assertSet('backupTaskToCopyId', null) | ||
->assertSet('optionalNewLabel', null) | ||
->assertSet('frequency', 'daily') | ||
->assertSet('timeToRun', '00:00'); | ||
}); |