From 45a3f48a72e6d2289a8a8394d127fc9f583f4e0c Mon Sep 17 00:00:00 2001 From: brufdev Date: Sun, 9 Feb 2025 16:59:05 +0000 Subject: [PATCH] Add links/backlinks feature --- app/Actions/DeleteVaultNode.php | 2 + app/Actions/ProcessImportedFile.php | 4 + app/Actions/ProcessImportedVault.php | 2 + app/Actions/ProcessVaultLinks.php | 19 ++++ app/Actions/ProcessVaultNodeLinks.php | 63 ++++++++++++ app/Livewire/Forms/VaultNodeForm.php | 10 ++ app/Livewire/Vault/Show.php | 33 +++++-- app/Models/VaultNode.php | 23 ++++- ...404_create_vault_node_vault_node_table.php | 24 +++++ .../icons/bars3BottomLeft.blade.php | 4 + .../icons/bars3BottomRight.blade.php | 4 + resources/views/livewire/vault/show.blade.php | 98 ++++++++++++++----- 12 files changed, 251 insertions(+), 35 deletions(-) create mode 100644 app/Actions/ProcessVaultLinks.php create mode 100644 app/Actions/ProcessVaultNodeLinks.php create mode 100644 database/migrations/2025_02_06_131404_create_vault_node_vault_node_table.php create mode 100644 resources/views/components/icons/bars3BottomLeft.blade.php create mode 100644 resources/views/components/icons/bars3BottomRight.blade.php diff --git a/app/Actions/DeleteVaultNode.php b/app/Actions/DeleteVaultNode.php index a345339..cfa274a 100644 --- a/app/Actions/DeleteVaultNode.php +++ b/app/Actions/DeleteVaultNode.php @@ -53,6 +53,8 @@ final readonly class DeleteVaultNode } } + $node->links()->detach(); + $node->backlinks()->detach(); $node->delete(); return $deletedNodes; diff --git a/app/Actions/ProcessImportedFile.php b/app/Actions/ProcessImportedFile.php index 62580e2..b322eff 100644 --- a/app/Actions/ProcessImportedFile.php +++ b/app/Actions/ProcessImportedFile.php @@ -28,6 +28,10 @@ final readonly class ProcessImportedFile } $node = new CreateVaultNode()->handle($vault, $attributes); + if ($node->extension === 'md') { + new ProcessVaultNodeLinks()->handle($node); + } + $relativePath = new GetPathFromVaultNode()->handle($node); $pathInfo = pathinfo($relativePath); $savePath = $pathInfo['dirname'] ?? ''; diff --git a/app/Actions/ProcessImportedVault.php b/app/Actions/ProcessImportedVault.php index 7f07b06..9aea7ec 100644 --- a/app/Actions/ProcessImportedVault.php +++ b/app/Actions/ProcessImportedVault.php @@ -70,5 +70,7 @@ final readonly class ProcessImportedVault } $zip->close(); + + new ProcessVaultLinks()->handle($vault); } } diff --git a/app/Actions/ProcessVaultLinks.php b/app/Actions/ProcessVaultLinks.php new file mode 100644 index 0000000..ef6a79b --- /dev/null +++ b/app/Actions/ProcessVaultLinks.php @@ -0,0 +1,19 @@ +nodes()->where('is_file', true)->where('extension', 'md')->get(); + + foreach ($nodes as $node) { + new ProcessVaultNodeLinks()->handle($node); + } + } +} diff --git a/app/Actions/ProcessVaultNodeLinks.php b/app/Actions/ProcessVaultNodeLinks.php new file mode 100644 index 0000000..ed870ee --- /dev/null +++ b/app/Actions/ProcessVaultNodeLinks.php @@ -0,0 +1,63 @@ +links()->detach(); + + if ((string) $node->content === '') { + return; + } + + $pattern = <<content; + preg_match_all($pattern, $content, $matches, PREG_OFFSET_CAPTURE); + + if ($matches[1] === []) { + return; + } + + $linkPaths = array_column($matches[1], 0); + $linkPositions = array_column($matches[0], 1); + $links = array_map( + fn (string $path, int $position): array => ['path' => $path, 'position' => $position], + $linkPaths, + $linkPositions, + ); + + foreach ($links as $link) { + /** + * @var string $fullPath + * + * @phpstan-ignore-next-line larastan.noUnnecessaryCollectionCall + */ + $fullPath = $node->ancestorsAndSelf()->get()->last()->full_path; + $path = new ResolveTwoPaths()->handle($fullPath, $link['path']); + + $destinationNode = new GetVaultNodeFromPath()->handle($node->vault_id, $path, $node->parent_id); + + if (is_null($destinationNode)) { + continue; + } + + $node->links()->attach($destinationNode->id, ['position' => $link['position']]); + } + } +} diff --git a/app/Livewire/Forms/VaultNodeForm.php b/app/Livewire/Forms/VaultNodeForm.php index 6f265a3..cda96df 100644 --- a/app/Livewire/Forms/VaultNodeForm.php +++ b/app/Livewire/Forms/VaultNodeForm.php @@ -5,6 +5,7 @@ declare(strict_types=1); namespace App\Livewire\Forms; use App\Actions\CreateVaultNode; +use App\Actions\ProcessVaultNodeLinks; use App\Actions\UpdateVaultNode; use App\Models\Vault; use App\Models\VaultNode; @@ -75,6 +76,11 @@ final class VaultNodeForm extends Form 'extension' => $this->is_file ? 'md' : null, 'content' => $this->content, ]); + + if ($node->is_file && $node->extension === 'md') { + new ProcessVaultNodeLinks()->handle($node); + } + $this->reset(['name']); return $node; @@ -96,5 +102,9 @@ final class VaultNodeForm extends Form 'extension' => $this->node->extension, 'content' => $this->content, ]); + + if ($this->node->is_file && $this->node->extension === 'md') { + new ProcessVaultNodeLinks()->handle($this->node); + } } } diff --git a/app/Livewire/Vault/Show.php b/app/Livewire/Vault/Show.php index fecf3b2..caf2178 100644 --- a/app/Livewire/Vault/Show.php +++ b/app/Livewire/Vault/Show.php @@ -38,6 +38,12 @@ final class Show extends Component public ?string $selectedFileUrl = null; + /** @var Collection */ + public ?Collection $selectedFileLinks = null; + + /** @var Collection */ + public ?Collection $selectedFileBacklinks = null; + public bool $isEditMode = true; public function mount(Vault $vault): void @@ -77,9 +83,7 @@ final class Show extends Component return; } - $this->selectedFile = $node->id; - $this->selectedFileUrl = new GetUrlFromVaultNode()->handle($node); - $this->nodeForm->setNode($node); + $this->setNode($node); if ($node->extension === 'md') { $this->dispatch('file-render-markup'); @@ -114,13 +118,12 @@ final class Show extends Component return; } - $this->selectedFileUrl = new GetUrlFromVaultNode()->handle($node); - $this->nodeForm->setNode($node); + $this->setNode($node); } public function closeFile(): void { - $this->reset(['selectedFile', 'selectedFileUrl']); + $this->reset(['selectedFile', 'selectedFileUrl', 'selectedFileLinks', 'selectedFileBacklinks']); $this->nodeForm->reset('node'); } @@ -135,16 +138,19 @@ final class Show extends Component public function updated(string $name): void { - if (!str_starts_with($name, 'nodeForm')) { + $node = $this->nodeForm->node; + + if (!str_starts_with($name, 'nodeForm') || is_null($node)) { return; } $this->nodeForm->update(); + $this->setNode($node); - if ($this->nodeForm->node && $this->nodeForm->node->wasChanged(['parent_id', 'name'])) { + if ($node->wasChanged(['parent_id', 'name'])) { $this->dispatch('node-updated'); - if ($this->nodeForm->node->parent_id === $this->vault->templates_node_id) { + if ($node->parent_id === $this->vault->templates_node_id) { $this->getTemplates(); } } @@ -252,4 +258,13 @@ final class Show extends Component { return view('livewire.vault.show'); } + + private function setNode(VaultNode $node): void + { + $this->selectedFile = $node->id; + $this->selectedFileUrl = new GetUrlFromVaultNode()->handle($node); + $this->selectedFileLinks = $node->links()->get(); + $this->selectedFileBacklinks = $node->backlinks()->get(); + $this->nodeForm->setNode($node); + } } diff --git a/app/Models/VaultNode.php b/app/Models/VaultNode.php index 3e832f8..8f742d4 100644 --- a/app/Models/VaultNode.php +++ b/app/Models/VaultNode.php @@ -7,6 +7,7 @@ namespace App\Models; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; +use Illuminate\Database\Eloquent\Relations\BelongsToMany; use Illuminate\Database\Eloquent\Relations\HasMany; use Staudenmeir\LaravelAdjacencyList\Eloquent\HasRecursiveRelationships; @@ -41,7 +42,7 @@ final class VaultNode extends Model } /** - * Get the nodes for the vault. + * Get the childs for the node. * * @return HasMany */ @@ -50,6 +51,26 @@ final class VaultNode extends Model return $this->hasMany(self::class, 'parent_id'); } + /** + * The nodes that are linked to the node. + * + * @return BelongsToMany + */ + public function links(): BelongsToMany + { + return $this->belongsToMany(self::class, null, 'source_id', 'destination_id'); + } + + /** + * The nodes that are backlinked to the node. + * + * @return BelongsToMany + */ + public function backlinks(): BelongsToMany + { + return $this->belongsToMany(self::class, null, 'destination_id', 'source_id'); + } + /** * Get the custom paths for the model. * diff --git a/database/migrations/2025_02_06_131404_create_vault_node_vault_node_table.php b/database/migrations/2025_02_06_131404_create_vault_node_vault_node_table.php new file mode 100644 index 0000000..79b5ffa --- /dev/null +++ b/database/migrations/2025_02_06_131404_create_vault_node_vault_node_table.php @@ -0,0 +1,24 @@ +foreignId('source_id')->constrained('vault_nodes'); + $table->foreignId('destination_id')->constrained('vault_nodes'); + $table->unsignedMediumInteger('position'); + + $table->primary(['source_id', 'destination_id', 'position']); + }); + } +}; diff --git a/resources/views/components/icons/bars3BottomLeft.blade.php b/resources/views/components/icons/bars3BottomLeft.blade.php new file mode 100644 index 0000000..437c3f9 --- /dev/null +++ b/resources/views/components/icons/bars3BottomLeft.blade.php @@ -0,0 +1,4 @@ + diff --git a/resources/views/components/icons/bars3BottomRight.blade.php b/resources/views/components/icons/bars3BottomRight.blade.php new file mode 100644 index 0000000..b8cdff3 --- /dev/null +++ b/resources/views/components/icons/bars3BottomRight.blade.php @@ -0,0 +1,4 @@ + diff --git a/resources/views/livewire/vault/show.blade.php b/resources/views/livewire/vault/show.blade.php index 62be611..174b574 100644 --- a/resources/views/livewire/vault/show.blade.php +++ b/resources/views/livewire/vault/show.blade.php @@ -1,36 +1,44 @@
- - -
- + +
+ +
+
-
+
-
-
-
+
+

{{ $vault->name }}

@@ -85,11 +93,11 @@
-
+
-
+
@@ -116,15 +124,19 @@ - + @if ($templates && count($templates)) -
    +
      @foreach ($templates as $template)
    • - @@ -190,6 +202,40 @@
+ +
+
+
+

Links

+
+ @if ($selectedFileLinks && $selectedFileLinks->count()) + @foreach ($selectedFileLinks as $link) + {{ $link->name }} + @endforeach + @else +

{{ __('No links found') }}

+ @endif +
+
+
+

Backlinks

+
+ @if ($selectedFileBacklinks && $selectedFileBacklinks->count()) + @foreach ($selectedFileBacklinks as $link) + {{ $link->name }} + @endforeach + @else +

{{ __('No backlinks found') }}

+ @endif +
+
+
+
@@ -203,7 +249,8 @@ @script