Move the "Insert template" action to the Markdown editor toolbar

This commit is contained in:
brufdev
2025-02-25 20:26:21 +00:00
parent 3c969ce2bb
commit 7b68f86321
8 changed files with 242 additions and 215 deletions

View File

@@ -39,12 +39,9 @@ final class AddNode extends Component
public function add(): void
{
$node = $this->form->create();
$this->form->create();
$this->closeModal();
$this->dispatch('node-updated');
if ($node->parent_id === $this->form->vault->templates_node_id) {
$this->dispatch('templates-refresh');
}
$message = $this->form->is_file ? __('File created') : __('Folder created');
$this->dispatch('toast', message: $message, type: 'success');
}

View File

@@ -0,0 +1,91 @@
<?php
declare(strict_types=1);
namespace App\Livewire\Modals;
use App\Models\Vault;
use App\Models\VaultNode;
use App\Services\VaultFiles\Note;
use Illuminate\Contracts\View\Factory;
use Illuminate\Contracts\View\View;
use Livewire\Attributes\On;
use Livewire\Component;
use Staudenmeir\LaravelAdjacencyList\Eloquent\Collection;
final class MarkdownEditorTemplate extends Component
{
use Modal;
public Vault $vault;
public ?VaultNode $selectedFile = null;
/** @var Collection<int, VaultNode> */
public ?Collection $templates = null;
public function mount(Vault $vault): void
{
$this->authorize('view', $vault);
}
#[On('open-modal')]
public function open(VaultNode $selectedFile): void
{
$this->openModal();
$this->getTemplates();
$this->selectedFile = $selectedFile;
}
public function insertTemplate(VaultNode $node): void
{
$this->authorize('update', $this->vault);
$sameVault = $node->vault && $node->vault->is($this->vault);
$isNote = $node->is_file && in_array($node->extension, Note::extensions());
$isTemplate = $node->parent_id && $node->parent_id === $this->vault->templates_node_id;
$fileSelected = $this->selectedFile && $this->selectedFile->exists();
if (!$sameVault || !$isNote || !$isTemplate || !$fileSelected) {
$this->dispatch('toast', message: __('Something went wrong'), type: 'error');
return;
}
/** @var VaultNode $selectedFile */
$selectedFile = $this->selectedFile;
$now = now();
$content = str_replace(
['{{date}}', '{{time}}'],
[$now->format('Y-m-d'), $now->format('H:i')],
(string) $node->content,
);
$content = str_contains($content, '{{content}}')
? str_replace('{{content}}', (string) $selectedFile->content, $content)
: $content . PHP_EOL . $selectedFile->content;
$selectedFile->update(['content' => $content]);
$this->dispatch('file-refresh', node: $selectedFile);
$this->dispatch('toast', message: __('Template inserted'), type: 'success');
}
public function render(): Factory|View
{
return view('livewire.modals.markdownEditorTemplate');
}
private function getTemplates(): void
{
if (!$this->vault->templatesNode) {
$this->templates = null;
return;
}
$this->templates = $this->vault
->templatesNode
->childs()
->where('is_file', true)
->where('extension', 'LIKE', 'md')
->orderBy('name')
->get();
}
}

View File

@@ -12,10 +12,8 @@ use App\Actions\UpdateVault;
use App\Livewire\Forms\VaultNodeForm;
use App\Models\Vault;
use App\Models\VaultNode;
use App\Services\VaultFiles\Note;
use Illuminate\Contracts\View\Factory;
use Illuminate\Contracts\View\View;
use Illuminate\Database\Eloquent\Collection;
use Livewire\Attributes\Locked;
use Livewire\Attributes\On;
use Livewire\Attributes\Url;
@@ -28,9 +26,6 @@ final class Show extends Component
public VaultNodeForm $nodeForm;
/** @var Collection<int, VaultNode> */
public Collection $templates;
#[Locked]
#[Url(as: 'file')]
public ?int $selectedFile = null;
@@ -49,9 +44,7 @@ final class Show extends Component
new UpdateVault()->handle($vault, [
'opened_at' => now(),
]);
$this->vault = $vault;
$this->nodeForm->setVault($this->vault);
$this->getTemplates();
if ((int) $this->selectedFile > 0) {
$selectedFile = $this->vault->nodes()
@@ -134,10 +127,6 @@ final class Show extends Component
if ($node->wasChanged(['parent_id', 'name'])) {
$this->dispatch('node-updated');
if ($node->parent_id === $this->vault->templates_node_id) {
$this->getTemplates();
}
}
}
@@ -154,56 +143,9 @@ final class Show extends Component
new UpdateVault()->handle($this->vault, [
'templates_node_id' => $node->id,
]);
$this->getTemplates();
$this->dispatch('toast', message: __('Template folder updated'), type: 'success');
}
public function insertTemplate(VaultNode $node): void
{
$this->authorize('update', $this->vault);
$sameVault = $node->vault && $this->vault->id === $node->vault->id;
$isNote = $node->is_file && in_array($node->extension, Note::extensions());
$isTemplate = $node->parent_id === $this->vault->templates_node_id;
$fileSelected = (int) $this->selectedFile > 0;
if (!$sameVault || !$isNote || !$isTemplate || !$fileSelected || !$this->isEditMode) {
$this->dispatch('toast', message: __('Something went wrong'), type: 'error');
return;
}
$now = now();
/** @var VaultNode $selectedNode */
$selectedNode = $this->nodeForm->node;
$content = str_replace(
['{{date}}', '{{time}}'],
[$now->format('Y-m-d'), $now->format('H:i')],
(string) $node->content,
);
$content = str_contains($content, '{{content}}')
? str_replace('{{content}}', (string) $selectedNode->content, $content)
: $content . PHP_EOL . $selectedNode->content;
$selectedNode->update(['content' => $content]);
$this->nodeForm->setNode($selectedNode);
$this->dispatch('toast', message: __('Template inserted'), type: 'success');
}
#[On('templates-refresh')]
public function getTemplates(): void
{
if (!$this->vault->templatesNode) {
return;
}
$this->templates = $this->vault
->templatesNode
->childs()
->where('is_file', true)
->where('extension', 'LIKE', 'md')
->orderBy('name')
->get();
}
public function deleteNode(VaultNode $node): void
{
$this->authorize('delete', $node->vault);
@@ -218,20 +160,11 @@ final class Show extends Component
fn (VaultNode $node): bool => $node->id === $this->selectedFile,
)
);
if ($openFileDeleted) {
$this->closeFile();
}
$templateDeleted = !is_null(
array_find(
$deletedNodes,
fn (VaultNode $node): bool => $node->parent_id === $this->vault->templates_node_id,
)
);
if ($templateDeleted) {
$this->getTemplates();
}
$message = $node->is_file ? __('File deleted') : __('Folder deleted');
$this->dispatch('toast', message: $message, type: 'success');
} catch (Throwable $e) {

View File

@@ -43,12 +43,17 @@
</x-markdownEditor.button>
<x-markdownEditor.items x-show="isToolbarOpen" x-anchor.bottom="$refs.button">
<x-markdownEditor.subButton
@click="$wire.dispatchTo('modals.markdown-editor-search', 'open-modal')">Link</x-markdownEditor.subButton>
@click="$wire.dispatchTo('modals.markdown-editor-search', 'open-modal')"
>Link</x-markdownEditor.subButton>
<x-markdownEditor.subButton @click="link()">External link</x-markdownEditor.subButton>
<x-markdownEditor.subButton
@click="$wire.dispatchTo('modals.markdown-editor-search', 'open-modal', { type: 'image' })">Image</x-markdownEditor.subButton>
@click="$wire.dispatchTo('modals.markdown-editor-search', 'open-modal', { type: 'image' })"
>Image</x-markdownEditor.subButton>
<x-markdownEditor.subButton @click="image()">External image</x-markdownEditor.subButton>
<x-markdownEditor.subButton @click="table">Table</x-markdownEditor.subButton>
<x-markdownEditor.subButton
@click="$wire.dispatchTo('modals.markdown-editor-template', 'open-modal', { selectedFile: $wire.selectedFile })"
>Template</x-markdownEditor.subButton>
</x-markdownEditor.items>
</x-markdownEditor.itemDropdown>
</ul>

View File

@@ -0,0 +1,24 @@
<x-modal wire:model="show">
<x-modal.panel title="{{ __('Choose a template') }}">
@if ($templates && count($templates))
<ul class="flex flex-col gap-2" wire:loading.class="opacity-50">
@foreach ($templates as $template)
<li wire:key="{{ $template->id }}">
<button type="button"
class="flex w-full gap-2 py-1 hover:text-light-base-950 dark:hover:text-base-50"
wire:click="insertTemplate({{ $template->id }}); modalOpen = false"
>
<span class="overflow-hidden whitespace-nowrap text-ellipsis"
title="{{ $template->name }}"
>
{{ $template->name }}
</span>
</button>
</li>
@endforeach
</ul>
@else
<p>{{ __('No templates found') }}</p>
@endif
</x-modal.panel>
</x-modal>

View File

@@ -63,52 +63,9 @@
<x-icons.spinner class="w-4 h-4 animate-spin" />
</span>
<div class="flex gap-2">
<x-menu>
<x-menu.button>
<x-icons.bars3 class="w-5 h-5" />
</x-menu.button>
<x-menu.items>
<x-modal x-show="selectedFileExtension == 'md' && isEditMode">
<x-modal.open>
<x-menu.close>
<x-menu.item>
<x-icons.documentDuplicate class="w-4 h-4" />
{{ __('Insert template') }}
</x-menu.item>
</x-menu.close>
</x-modal.open>
<x-modal.panel title="{{ __('Choose a template') }}">
@if ($templates && count($templates))
<ul class="flex flex-col gap-2" wire:loading.class="opacity-50">
@foreach ($templates as $template)
<li wire:key="{{ $template->id }}">
<button type="button"
class="flex w-full gap-2 py-1 hover:text-light-base-950 dark:hover:text-base-50"
wire:click="insertTemplate({{ $template->id }}); modalOpen = false"
>
<span class="overflow-hidden whitespace-nowrap text-ellipsis"
title="{{ $template->name }}"
>
{{ $template->name }}
</span>
</button>
</li>
@endforeach
</ul>
@else
<p>{{ __('No templates found') }}</p>
@endif
</x-modal.panel>
</x-modal>
<x-menu.close>
<x-menu.item wire:click="closeFile">
<x-icons.xMark class="w-4 h-4" />
{{ __('Close file') }}
</x-menu.item>
</x-menu.close>
</x-menu.items>
</x-menu>
<button title="{{ __('Close file') }}" wire:click="closeFile">
<x-icons.xMark class="w-5 h-5" />
</button>
</div>
</div>
</div>
@@ -211,6 +168,7 @@
<livewire:modals.edit-node :$vault />
<livewire:modals.search-node :$vault />
<livewire:modals.markdown-editor-search :$vault />
<livewire:modals.markdown-editor-template :$vault />
</div>
@script

View File

@@ -0,0 +1,114 @@
<?php
declare(strict_types=1);
use App\Actions\CreateVault;
use App\Actions\CreateVaultNode;
use App\Livewire\Modals\MarkdownEditorTemplate;
use App\Models\User;
it('opens the modal', function (): void {
$user = User::factory()->create()->first();
$vault = new CreateVault()->handle($user, [
'name' => fake()->words(3, true),
]);
Livewire::actingAs($user)
->test(MarkdownEditorTemplate::class, ['vault' => $vault])
->assertSet('show', false)
->call('open')
->assertSet('show', true);
});
it('inserts a template', function (): void {
$user = User::factory()->create()->first();
$vault = new CreateVault()->handle($user, [
'name' => fake()->words(3, true),
]);
$templateFolderNode = new CreateVaultNode()->handle($vault, [
'is_file' => false,
'name' => fake()->words(3, true),
]);
$templateNode = new CreateVaultNode()->handle($vault, [
'is_file' => true,
'parent_id' => $templateFolderNode->id,
'name' => fake()->words(3, true),
'extension' => 'md',
'content' => 'content: {{content}}',
]);
$content = fake()->paragraph();
$node = new CreateVaultNode()->handle($vault, [
'is_file' => true,
'name' => fake()->words(3, true),
'extension' => 'md',
'content' => $content,
]);
$vault->update(['templates_node_id' => $templateFolderNode->id]);
Livewire::actingAs($user)
->test(MarkdownEditorTemplate::class, ['vault' => $vault])
->call('open', $node)
->call('insertTemplate', $templateNode);
expect($node->refresh()->content)->toBe('content: ' . $content);
});
it('does not insert a template from a non-template node', function (): void {
$user = User::factory()->create()->first();
$vault = new CreateVault()->handle($user, [
'name' => fake()->words(3, true),
]);
$firstNode = new CreateVaultNode()->handle($vault, [
'is_file' => true,
'name' => fake()->words(3, true),
'extension' => 'md',
'content' => 'content: {{content}}',
]);
$secondNodeContent = fake()->paragraph();
$secondNode = new CreateVaultNode()->handle($vault, [
'is_file' => true,
'name' => fake()->words(3, true),
'extension' => 'md',
'content' => $secondNodeContent,
]);
Livewire::actingAs($user)
->test(MarkdownEditorTemplate::class, ['vault' => $vault])
->call('open', $secondNode)
->call('insertTemplate', $firstNode);
expect($secondNode->refresh()->content)->toBe($secondNodeContent);
});
it('inserts a template without {{content}} variable', function (): void {
$user = User::factory()->create()->first();
$vault = new CreateVault()->handle($user, [
'name' => fake()->words(3, true),
]);
$templateFolderNode = new CreateVaultNode()->handle($vault, [
'is_file' => false,
'name' => fake()->words(3, true),
]);
$templateNode = new CreateVaultNode()->handle($vault, [
'is_file' => true,
'parent_id' => $templateFolderNode->id,
'name' => fake()->words(3, true),
'extension' => 'md',
'content' => 'Daily note',
]);
$nodeContent = fake()->paragraph();
$node = new CreateVaultNode()->handle($vault, [
'is_file' => true,
'name' => fake()->words(3, true),
'extension' => 'md',
'content' => $nodeContent,
]);
$vault->update(['templates_node_id' => $templateFolderNode->id]);
Livewire::actingAs($user)
->test(MarkdownEditorTemplate::class, ['vault' => $vault])
->call('open', $node)
->call('insertTemplate', $templateNode);
expect($node->refresh()->content)->toBe('Daily note' . PHP_EOL . $nodeContent);
});

View File

@@ -234,101 +234,6 @@ it('does not set the template folder if it is a file', function (): void {
->assertSet('vault.templates_node_id', null);
});
it('inserts a template', function (): void {
$user = User::factory()->create()->first();
$vault = new CreateVault()->handle($user, [
'name' => fake()->words(3, true),
]);
$templateFolderNode = new CreateVaultNode()->handle($vault, [
'is_file' => false,
'name' => fake()->words(3, true),
]);
$templateNode = new CreateVaultNode()->handle($vault, [
'is_file' => true,
'parent_id' => $templateFolderNode->id,
'name' => fake()->words(3, true),
'extension' => 'md',
'content' => 'content: {{content}}',
]);
$node = new CreateVaultNode()->handle($vault, [
'is_file' => true,
'name' => fake()->words(3, true),
'extension' => 'md',
'content' => fake()->paragraph(),
]);
Livewire::actingAs($user)
->withQueryParams(['file' => $node->id])
->test(Show::class, ['vault' => $vault])
->assertSet('nodeForm.content', $node->content)
->call('setTemplateFolder', $templateFolderNode)
->call('insertTemplate', $templateNode)
->assertSet('nodeForm.content', 'content: ' . $node->content);
});
it('does not insert a template from a non-template node', function (): void {
$user = User::factory()->create()->first();
$vault = new CreateVault()->handle($user, [
'name' => fake()->words(3, true),
]);
$folderNode = new CreateVaultNode()->handle($vault, [
'is_file' => false,
'name' => fake()->words(3, true),
]);
$firstNode = new CreateVaultNode()->handle($vault, [
'is_file' => true,
'parent_id' => $folderNode->id,
'name' => fake()->words(3, true),
'extension' => 'md',
'content' => 'content: {{content}}',
]);
$secondNode = new CreateVaultNode()->handle($vault, [
'is_file' => true,
'name' => fake()->words(3, true),
'extension' => 'md',
'content' => fake()->paragraph(),
]);
Livewire::actingAs($user)
->withQueryParams(['file' => $secondNode->id])
->test(Show::class, ['vault' => $vault])
->assertSet('nodeForm.content', $secondNode->content)
->call('insertTemplate', $firstNode)
->assertSet('nodeForm.content', $secondNode->content);
});
it('inserts a template without {{content}} variable', function (): void {
$user = User::factory()->create()->first();
$vault = new CreateVault()->handle($user, [
'name' => fake()->words(3, true),
]);
$templateFolderNode = new CreateVaultNode()->handle($vault, [
'is_file' => false,
'name' => fake()->words(3, true),
]);
$templateNode = new CreateVaultNode()->handle($vault, [
'is_file' => true,
'parent_id' => $templateFolderNode->id,
'name' => fake()->words(3, true),
'extension' => 'md',
'content' => 'Daily note',
]);
$node = new CreateVaultNode()->handle($vault, [
'is_file' => true,
'name' => fake()->words(3, true),
'extension' => 'md',
'content' => fake()->paragraph(),
]);
Livewire::actingAs($user)
->withQueryParams(['file' => $node->id])
->test(Show::class, ['vault' => $vault])
->assertSet('nodeForm.content', $node->content)
->call('setTemplateFolder', $templateFolderNode)
->call('insertTemplate', $templateNode)
->assertSet('nodeForm.content', 'Daily note' . PHP_EOL . $node->content);
});
it('updates the node', function (): void {
$user = User::factory()->create()->first();
$vault = new CreateVault()->handle($user, [