Improve saving users last visited URL to work with collaboration

This commit is contained in:
brufdev
2025-04-02 16:14:53 +01:00
parent 7025be7ced
commit 9ef80c2da6
20 changed files with 135 additions and 117 deletions

View File

@@ -7,6 +7,7 @@ namespace App\Livewire\Auth;
use App\Actions\GetAvailableOAuthProviders;
use App\Enums\OAuthProviders;
use App\Livewire\Forms\LoginForm;
use App\Models\User;
use Illuminate\Contracts\View\Factory;
use Illuminate\Contracts\View\View;
use Illuminate\Support\Facades\Session;
@@ -33,7 +34,12 @@ final class Login extends Component
Session::regenerate();
$this->redirectIntended(default: route('vaults.last', absolute: false), navigate: true);
/** @var User $user */
$user = auth()->user();
$redirectUrl = mb_strlen((string) $user->last_visited_url) > 0
? $user->last_visited_url
: route('vaults.index', absolute: false);
$this->redirectIntended($redirectUrl, true);
}
public function render(): Factory|View

View File

@@ -34,6 +34,7 @@ final class OAuthLoginCallback extends Component
}
$user = User::query()->where('email', $providerUser->getEmail())->first();
if (!$user) {
$user = new CreateUser()->handle([
'name' => $providerUser->getName() ?? '',
@@ -41,7 +42,11 @@ final class OAuthLoginCallback extends Component
'password' => Hash::make(Str::random(32)),
]);
}
Auth::login($user);
$this->redirectIntended(route('vaults.last', absolute: false), true);
$redirectUrl = mb_strlen((string) $user->last_visited_url) > 0
? $user->last_visited_url
: route('vaults.index', absolute: false);
$this->redirectIntended($redirectUrl, true);
}
}

View File

@@ -4,12 +4,18 @@ declare(strict_types=1);
namespace App\Livewire\Dashboard;
use App\Models\User;
use Livewire\Component;
final class Index extends Component
{
public function boot(): void
{
$this->redirect(route('vaults.last'), true);
/** @var User $user */
$user = auth()->user();
$redirectUrl = mb_strlen((string) $user->last_visited_url) > 0
? $user->last_visited_url
: route('vaults.index', absolute: false);
$this->redirectIntended($redirectUrl, true);
}
}

View File

@@ -24,6 +24,11 @@ final class Index extends Component
public bool $showCreateModal = false;
public function mount(): void
{
$this->setLastVisitedUrl();
}
public function create(): void
{
$this->form->create();
@@ -78,4 +83,13 @@ final class Index extends Component
'vaults' => $vaults,
]);
}
private function setLastVisitedUrl(): void
{
/** @var User $currentUser */
$currentUser = auth()->user();
$currentUser->update([
'last_visited_url' => route('vaults.index', absolute: false),
]);
}
}

View File

@@ -1,26 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Livewire\Vault;
use App\Models\User;
use Livewire\Component;
final class Last extends Component
{
public function mount(): void
{
/** @var User $currentUser */
$currentUser = auth()->user();
$lastVault = $currentUser->vaults()->whereNotNull('opened_at')->orderByDesc('opened_at')->first();
if (!$lastVault) {
$this->redirect(route('vaults.index'), navigate: true);
return;
}
$this->redirect(route('vaults.show', ['vault' => $lastVault]), navigate: true);
}
}

View File

@@ -10,6 +10,7 @@ use App\Actions\GetVaultNodeFromPath;
use App\Actions\ResolveTwoPaths;
use App\Actions\UpdateVault;
use App\Livewire\Forms\VaultNodeForm;
use App\Models\User;
use App\Models\Vault;
use App\Models\VaultNode;
use Illuminate\Contracts\View\Factory;
@@ -26,8 +27,7 @@ final class Show extends Component
public VaultNodeForm $nodeForm;
#[Locked]
#[Url(as: 'file')]
#[Url(as: 'file', history: true)]
public ?int $selectedFile = null;
#[Locked]
@@ -36,47 +36,37 @@ final class Show extends Component
#[Locked]
public ?string $selectedFileUrl = null;
public bool $isEditMode = true;
public function mount(Vault $vault): void
{
$this->authorize('view', $vault);
new UpdateVault()->handle($vault, [
'opened_at' => now(),
]);
$this->nodeForm->setVault($this->vault);
if ((int) $this->selectedFile > 0) {
$selectedFile = $this->vault->nodes()
->where('id', $this->selectedFile)
->where('is_file', true)
->first();
if (!$selectedFile) {
$this->selectedFile = null;
return;
}
$this->openFile($selectedFile);
}
$this->openFileId($this->selectedFile);
}
public function openFile(VaultNode $node): void
public function updatedSelectedFile(): void
{
$this->authorize('view', $node->vault);
$this->openFileId($this->selectedFile);
}
if (!$node->vault || !$node->vault->is($this->vault) || !$node->is_file) {
public function openFileId(?int $fileId = null): void
{
if ($fileId === null) {
$this->selectedFile = null;
$this->setLastVisitedUrl();
return;
}
$this->setNode($node);
$node = $this->vault->nodes()
->where('id', $fileId)
->where('is_file', true)
->first();
if ($node->extension === 'md') {
$this->dispatch('file-render-markup');
if ($node === null) {
abort(404);
}
$this->openFile($node);
}
public function openFilePath(string $path): void
@@ -99,8 +89,6 @@ final class Show extends Component
#[On('file-refresh')]
public function refreshFile(VaultNode $node): void
{
$this->authorize('view', $node->vault);
if ($node->id !== $this->selectedFile) {
return;
}
@@ -184,4 +172,25 @@ final class Show extends Component
$this->selectedFileUrl = new GetUrlFromVaultNode()->handle($node);
$this->nodeForm->setNode($node);
}
private function openFile(VaultNode $node): void
{
$this->setNode($node);
$this->setLastVisitedUrl();
if ($node->extension === 'md') {
$this->dispatch('file-render-markup');
}
}
private function setLastVisitedUrl(): void
{
/** @var User $currentUser */
$currentUser = auth()->user();
$currentUrl = route('vaults.show', ['vault' => $this->vault->id], false)
. ($this->selectedFile !== null ? '?file=' . $this->selectedFile : '');
$currentUser->update([
'last_visited_url' => $currentUrl,
]);
}
}

View File

@@ -31,6 +31,7 @@ final class UserFactory extends Factory
'email_verified_at' => now(),
'password' => self::$password ??= Hash::make('password'),
'remember_token' => Str::random(10),
'last_visited_url' => null,
];
}

View File

@@ -22,7 +22,6 @@ final class VaultFactory extends Factory
return [
'name' => fake()->words(3, true),
'created_by' => User::factory(),
'opened_at' => now(),
];
}
}

View File

@@ -0,0 +1,20 @@
<?php
declare(strict_types=1);
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('vaults', function (Blueprint $table): void {
$table->dropColumn('opened_at');
});
}
};

View File

@@ -0,0 +1,20 @@
<?php
declare(strict_types=1);
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('users', function (Blueprint $table): void {
$table->string('last_visited_url')->nullable();
});
}
};

View File

@@ -8,7 +8,7 @@
<ul class="flex flex-col gap-2" wire:loading.class="opacity-50">
@foreach ($nodes as $node)
<li wire:key="{{ $node['id'] }}">
<button type="button" wire:click="$parent.openFile({{ $node['id'] }}); modalOpen = false"
<button type="button" wire:click="$parent.openFileId({{ $node['id'] }}); modalOpen = false"
class="flex flex-col w-full gap-2 py-1 text-left hover:text-light-base-950 dark:hover:text-base-50">
<span class="flex gap-2">
<span class="overflow-hidden font-semibold whitespace-nowrap text-ellipsis"

View File

@@ -178,7 +178,7 @@
Alpine.data('vault', () => ({
isLeftPanelOpen: false,
isRightPanelOpen: false,
isEditMode: $wire.entangle('isEditMode'),
isEditMode: Alpine.$persist(true),
selectedFile: $wire.entangle('selectedFile'),
selectedFileExtension: $wire.entangle('selectedFileExtension'),
html: '',
@@ -200,6 +200,10 @@
});
this.isLeftPanelOpen = !this.isSmallDevice();
if (!this.isEditMode) {
Alpine.nextTick(() => { this.markdownToHtml() });
}
},
isSmallDevice() {
@@ -215,8 +219,8 @@
this.isEditMode = !this.isEditMode;
},
openFile(node) {
$wire.openFile(node);
openFile(nodeId) {
$wire.openFileId(nodeId);
if (this.isSmallDevice()) {
this.closePanels();

View File

@@ -12,7 +12,6 @@ use App\Livewire\Auth\Register;
use App\Livewire\Auth\ResetPassword;
use App\Livewire\Dashboard\Index as DashboardIndex;
use App\Livewire\Vault\Index as VaultIndex;
use App\Livewire\Vault\Last as VaultLast;
use App\Livewire\Vault\Show as VaultShow;
use Illuminate\Support\Facades\Route;
@@ -21,7 +20,6 @@ Route::middleware('auth')->group(function (): void {
Route::prefix('vaults')->group(function (): void {
Route::get('/', VaultIndex::class)->name('vaults.index');
Route::get('/last', VaultLast::class)->name('vaults.last');
Route::get('/{vault}', VaultShow::class)->name('vaults.show');
});

View File

@@ -3,6 +3,7 @@
declare(strict_types=1);
use App\Livewire\Dashboard\Index;
use App\Models\User;
use Livewire\Livewire;
it('redirects guests to login page', function (): void {
@@ -11,6 +12,9 @@ it('redirects guests to login page', function (): void {
});
it('redirects users to vaults page', function (): void {
Livewire::test(Index::class)
->assertRedirect(route('vaults.last'));
$user = User::factory()->hasVaults(1)->create();
Livewire::actingAs($user)
->test(Index::class)
->assertRedirect(route('vaults.index'));
});

View File

@@ -18,7 +18,7 @@ it('successfully authenticates user', function (): void {
->set('form.email', $user->email)
->set('form.password', 'password')
->call('send')
->assertRedirect(route('vaults.last'));
->assertRedirect(route('vaults.index'));
});
it('gets rate limited', function (): void {

View File

@@ -27,7 +27,7 @@ it('successfully authenticates user', function (): void {
$availableProviders->shouldReceive('handle')->andReturn([OAuthProviders::GitHub]);
Livewire::test(OAuthLoginCallback::class, ['provider' => 'github'])
->assertRedirect(route('vaults.last'));
->assertRedirect(route('vaults.index'));
});
it('fails to authenticate user', function (): void {

View File

@@ -1,25 +0,0 @@
<?php
declare(strict_types=1);
use App\Livewire\Vault\Last;
use App\Models\User;
use Livewire\Livewire;
it('redirects to list of vaults', function (): void {
$user = User::factory()->create();
Livewire::actingAs($user)
->test(Last::class)
->assertRedirect(route('vaults.index'));
});
it('redirects to last opened vault', function (): void {
$user = User::factory()->hasVaults(2)->create();
$vault = $user->vaults()->first();
$vault->update(['opened_at' => now()]);
Livewire::actingAs($user)
->test(Last::class)
->assertRedirect(route('vaults.show', ['vault' => $vault]));
});

View File

@@ -40,7 +40,7 @@ it('does not open a non-existing file', function (): void {
Livewire::actingAs($user)
->withQueryParams(['file' => 500])
->test(Show::class, ['vault' => $vault])
->assertSet('selectedFile', null);
->assertStatus(404);
});
it('does not open a folder', function (): void {
@@ -55,25 +55,8 @@ it('does not open a folder', function (): void {
Livewire::actingAs($user)
->test(Show::class, ['vault' => $vault])
->call('openFile', $node)
->assertSet('selectedFile', null);
});
it('resets edit mode when opening a file that is not a note', function (): void {
$user = User::factory()->create()->first();
$vault = new CreateVault()->handle($user, [
'name' => fake()->words(3, true),
]);
$node = new CreateVaultNode()->handle($vault, [
'is_file' => true,
'name' => fake()->words(3, true),
'extension' => 'jpg',
]);
Livewire::actingAs($user)
->test(Show::class, ['vault' => $vault])
->call('openFile', $node)
->assertSet('isEditMode', true);
->call('openFileId', $node->id)
->assertStatus(404);
});
it('opens a file from the path', function (): void {
@@ -131,7 +114,7 @@ it('does not open a file from a non-existent path', function (): void {
Livewire::actingAs($user)
->test(Show::class, ['vault' => $vault])
->call('openFilePath', fake()->words(4, true))
->assertSet('selectedFile', null);
->assertStatus(404);
});
it('refreshes an open file', function (): void {

View File

@@ -16,6 +16,7 @@ test('to array', function (): void {
'email_verified_at',
'created_at',
'updated_at',
'last_visited_url',
]);
});

View File

@@ -14,7 +14,6 @@ test('to array', function (): void {
'id',
'name',
'created_by',
'opened_at',
'created_at',
'updated_at',
'templates_node_id',