mirror of
https://github.com/brufdev/many-notes.git
synced 2026-01-24 20:09:50 -06:00
Move the "Insert template" action to the Markdown editor toolbar
This commit is contained in:
@@ -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');
|
||||
}
|
||||
|
||||
91
app/Livewire/Modals/MarkdownEditorTemplate.php
Normal file
91
app/Livewire/Modals/MarkdownEditorTemplate.php
Normal 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();
|
||||
}
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
@@ -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
|
||||
|
||||
114
tests/Feature/Modals/MarkdownEditorTemplateTest.php
Normal file
114
tests/Feature/Modals/MarkdownEditorTemplateTest.php
Normal 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);
|
||||
});
|
||||
@@ -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, [
|
||||
|
||||
Reference in New Issue
Block a user