journal/Journal.Core/Services/Entries/EntryFileService.cs
Jacob Schmidt d1e4989303 Add edit/delete buttons to SidePanel for all sections, custom entry filenames, vault persistence
- Add edit/delete icon buttons to SidePanel items for entries, todos, lists, and fragments
- Move fragment edit/delete controls from FragmentEditor main panel to SidePanel
- Add externalEditRequested prop to FragmentEditor for parent-controlled edit mode
- Add fragment delete handling in +page.svelte performDelete flow
- Support custom entry filenames via FileName parameter in EntrySavePayload
- Fix vault persistence for custom-named entries (non-date-formatted .md files)
- Add VaultStorageService SaveCustomEntries helper for _custom_entries.vault
- Add entries.delete backend command and wire through EntryFileService
- Remove Write/Preview toggle from MarkdownEditor (previewOnly controlled by parent)
- Add smoke tests for vault custom entry roundtrip and entry save with custom filename

Co-Authored-By: Oz <oz-agent@warp.dev>
2026-02-26 19:40:43 -06:00

104 lines
3.9 KiB
C#

using Journal.Core.Dtos;
using Journal.Core.Repositories;
namespace Journal.Core.Services.Entries;
public sealed class EntryFileService(IEntryFileRepository repo) : IEntryFileService
{
private readonly IEntryFileRepository _repo = repo ?? throw new ArgumentNullException(nameof(repo));
public IReadOnlyList<EntryListItem> ListEntries(string dataDirectory)
{
return [.. _repo.ListMarkdownFiles(dataDirectory)
.Select(path => new EntryListItem(
FileName: _repo.GetFileName(path),
FilePath: _repo.GetFullPath(path)))];
}
public EntryLoadResult LoadEntry(string filePath)
{
var normalizedPath = _repo.GetFullPath(filePath);
if (!_repo.FileExists(normalizedPath))
throw new FileNotFoundException($"Entry file not found: {normalizedPath}");
var rawContent = HtmlSanitizer.StripRichHtml(_repo.ReadFile(normalizedPath));
var fileStem = _repo.GetFileNameWithoutExtension(normalizedPath);
var entry = JournalParser.ParseJournalContent(rawContent, fileStem);
return new EntryLoadResult(
FileName: _repo.GetFileName(normalizedPath),
FilePath: normalizedPath,
Entry: entry.ToDto());
}
public EntrySaveResult SaveEntry(EntrySavePayload payload, string defaultDataDirectory)
{
var targetPath = ResolveTargetPath(payload.FilePath, payload.FileName, defaultDataDirectory);
var mode = string.IsNullOrWhiteSpace(payload.Mode) ? "Daily" : payload.Mode.Trim();
var sanitizedContent = HtmlSanitizer.StripRichHtml(payload.Content ?? "");
_repo.EnsureDirectory(targetPath);
if (string.Equals(mode, "Overwrite", StringComparison.OrdinalIgnoreCase))
{
_repo.WriteFile(targetPath, sanitizedContent);
return new EntrySaveResult(targetPath);
}
if (string.Equals(mode, "Fragment", StringComparison.OrdinalIgnoreCase))
{
_repo.AppendFile(targetPath, "\n\n" + sanitizedContent.Trim());
return new EntrySaveResult(targetPath);
}
string finalContent;
if (_repo.FileExists(targetPath))
{
var existingContent = _repo.ReadFile(targetPath);
var fileStem = _repo.GetFileNameWithoutExtension(targetPath);
var existingEntry = JournalParser.ParseJournalContent(existingContent, fileStem);
var newEntryData = JournalParser.ParseJournalContent(sanitizedContent, fileStem);
existingEntry.MergeWith(newEntryData);
finalContent = existingEntry.ToMarkdown();
}
else
{
finalContent = sanitizedContent;
}
_repo.WriteFile(targetPath, finalContent);
return new EntrySaveResult(targetPath);
}
public bool DeleteEntry(string filePath)
{
var normalizedPath = _repo.GetFullPath(filePath);
if (!_repo.FileExists(normalizedPath))
return false;
_repo.DeleteFile(normalizedPath);
return true;
}
private string ResolveTargetPath(string? filePath, string? fileName, string defaultDataDirectory)
{
if (!string.IsNullOrWhiteSpace(filePath))
return _repo.GetFullPath(filePath);
var name = !string.IsNullOrWhiteSpace(fileName)
? SanitizeFileName(fileName)
: $"{DateTime.Now:yyyy-MM-dd}";
return _repo.GetFullPath(Path.Combine(defaultDataDirectory, $"{name}.md"));
}
private static string SanitizeFileName(string name)
{
var trimmed = name.Trim();
if (trimmed.EndsWith(".md", StringComparison.OrdinalIgnoreCase))
trimmed = trimmed[..^3];
var invalid = Path.GetInvalidFileNameChars();
var sanitized = new string(trimmed.Select(c => Array.IndexOf(invalid, c) >= 0 ? '_' : c).ToArray());
return string.IsNullOrWhiteSpace(sanitized) ? "untitled" : sanitized;
}
}