- Wire up lists and todos to C# backend with full CRUD persistence - Add models, DTOs, repositories, and services for lists and todo lists/items - Preserve SQLite DB across vault rebuild/load cycles - Add session store for vault password persistence across navigation - Add inline name input for creating lists and todo lists in SidePanel - Clear editor panel on section change with empty state placeholder - Default markdown editor to preview mode on item selection - Decompose EditorPanel into sub-components: - editor/FragmentEditor, editor/TodoEditor, editor/MarkdownEditor - Shared markdown utilities in utils/markdown.ts - Strip verbose console/eprintln logging from frontend and Tauri backend - Add graceful shutdown with vault persistence on window close Co-Authored-By: Oz <oz-agent@warp.dev>
280 lines
9.7 KiB
C#
280 lines
9.7 KiB
C#
using Journal.Core.Models;
|
|
using Journal.Core.Services.Database;
|
|
using Microsoft.Data.Sqlite;
|
|
|
|
namespace Journal.Core.Repositories;
|
|
|
|
public sealed class SqliteTodoRepository(IDatabaseSessionService session) : ITodoRepository
|
|
{
|
|
private readonly IDatabaseSessionService _session = session;
|
|
|
|
// ── Lists ────────────────────────────────────────────────────────
|
|
|
|
public List<TodoList> GetAllLists()
|
|
{
|
|
var conn = _session.GetConnection();
|
|
return ReadAllLists(conn);
|
|
}
|
|
|
|
public TodoList? GetListById(Guid id)
|
|
{
|
|
var conn = _session.GetConnection();
|
|
return ReadListById(conn, id);
|
|
}
|
|
|
|
public void AddList(TodoList list)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(list);
|
|
var conn = _session.GetConnection();
|
|
InsertList(conn, list);
|
|
}
|
|
|
|
public bool UpdateList(Guid id, string? label = null)
|
|
{
|
|
var conn = _session.GetConnection();
|
|
var existing = ReadListById(conn, id);
|
|
if (existing is null)
|
|
return false;
|
|
|
|
if (label is not null)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(label))
|
|
throw new ArgumentException("Label cannot be empty", nameof(label));
|
|
existing.Label = label.Trim();
|
|
}
|
|
|
|
UpdateListRow(conn, existing);
|
|
return true;
|
|
}
|
|
|
|
public bool RemoveList(Guid id)
|
|
{
|
|
var conn = _session.GetConnection();
|
|
return DeleteList(conn, id);
|
|
}
|
|
|
|
// ── Items ────────────────────────────────────────────────────────
|
|
|
|
public List<TodoItem> GetItemsByListId(Guid listId)
|
|
{
|
|
var conn = _session.GetConnection();
|
|
return ReadItemsByListId(conn, listId);
|
|
}
|
|
|
|
public TodoItem? GetItemById(Guid id)
|
|
{
|
|
var conn = _session.GetConnection();
|
|
return ReadItemById(conn, id);
|
|
}
|
|
|
|
public void AddItem(TodoItem item)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(item);
|
|
var conn = _session.GetConnection();
|
|
InsertItem(conn, item);
|
|
}
|
|
|
|
public bool UpdateItem(Guid id, string? text = null, bool? done = null, int? sortOrder = null)
|
|
{
|
|
var conn = _session.GetConnection();
|
|
var existing = ReadItemById(conn, id);
|
|
if (existing is null)
|
|
return false;
|
|
|
|
if (text is not null)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(text))
|
|
throw new ArgumentException("Text cannot be empty", nameof(text));
|
|
existing.Text = text.Trim();
|
|
}
|
|
|
|
if (done.HasValue)
|
|
existing.Done = done.Value;
|
|
|
|
if (sortOrder.HasValue)
|
|
existing.SortOrder = sortOrder.Value;
|
|
|
|
UpdateItemRow(conn, existing);
|
|
return true;
|
|
}
|
|
|
|
public bool RemoveItem(Guid id)
|
|
{
|
|
var conn = _session.GetConnection();
|
|
return DeleteItem(conn, id);
|
|
}
|
|
|
|
// ── Private list helpers ─────────────────────────────────────────
|
|
|
|
private static void InsertList(SqliteConnection conn, TodoList list)
|
|
{
|
|
using var cmd = conn.CreateCommand();
|
|
cmd.CommandText = """
|
|
INSERT INTO todo_lists (guid, label, created_at)
|
|
VALUES (@guid, @label, @createdAt);
|
|
""";
|
|
cmd.Parameters.AddWithValue("@guid", list.Id.ToString("D"));
|
|
cmd.Parameters.AddWithValue("@label", list.Label);
|
|
cmd.Parameters.AddWithValue("@createdAt", list.CreatedAt.ToString("O"));
|
|
cmd.ExecuteNonQuery();
|
|
}
|
|
|
|
private static void UpdateListRow(SqliteConnection conn, TodoList list)
|
|
{
|
|
using var cmd = conn.CreateCommand();
|
|
cmd.CommandText = "UPDATE todo_lists SET label = @label WHERE guid = @guid;";
|
|
cmd.Parameters.AddWithValue("@guid", list.Id.ToString("D"));
|
|
cmd.Parameters.AddWithValue("@label", list.Label);
|
|
cmd.ExecuteNonQuery();
|
|
}
|
|
|
|
private static bool DeleteList(SqliteConnection conn, Guid id)
|
|
{
|
|
using var tx = conn.BeginTransaction();
|
|
|
|
var rowId = GetListRowId(conn, id);
|
|
if (rowId.HasValue)
|
|
{
|
|
using var delItems = conn.CreateCommand();
|
|
delItems.CommandText = "DELETE FROM todo_items WHERE list_id = @listId;";
|
|
delItems.Parameters.AddWithValue("@listId", rowId.Value);
|
|
delItems.ExecuteNonQuery();
|
|
}
|
|
|
|
using var delList = conn.CreateCommand();
|
|
delList.CommandText = "DELETE FROM todo_lists WHERE guid = @guid;";
|
|
delList.Parameters.AddWithValue("@guid", id.ToString("D"));
|
|
var rows = delList.ExecuteNonQuery();
|
|
|
|
tx.Commit();
|
|
return rows > 0;
|
|
}
|
|
|
|
private static TodoList? ReadListById(SqliteConnection conn, Guid id)
|
|
{
|
|
using var cmd = conn.CreateCommand();
|
|
cmd.CommandText = "SELECT guid, label, created_at FROM todo_lists WHERE guid = @guid;";
|
|
cmd.Parameters.AddWithValue("@guid", id.ToString("D"));
|
|
|
|
using var reader = cmd.ExecuteReader();
|
|
return reader.Read() ? MapListRow(reader) : null;
|
|
}
|
|
|
|
private static List<TodoList> ReadAllLists(SqliteConnection conn)
|
|
{
|
|
var results = new List<TodoList>();
|
|
using var cmd = conn.CreateCommand();
|
|
cmd.CommandText = "SELECT guid, label, created_at FROM todo_lists ORDER BY created_at;";
|
|
|
|
using var reader = cmd.ExecuteReader();
|
|
while (reader.Read())
|
|
results.Add(MapListRow(reader));
|
|
|
|
return results;
|
|
}
|
|
|
|
private static long? GetListRowId(SqliteConnection conn, Guid guid)
|
|
{
|
|
using var cmd = conn.CreateCommand();
|
|
cmd.CommandText = "SELECT id FROM todo_lists WHERE guid = @guid;";
|
|
cmd.Parameters.AddWithValue("@guid", guid.ToString("D"));
|
|
var result = cmd.ExecuteScalar();
|
|
return result is long id ? id : null;
|
|
}
|
|
|
|
private static TodoList MapListRow(SqliteDataReader reader)
|
|
{
|
|
var guid = Guid.Parse(reader.GetString(0));
|
|
var label = reader.GetString(1);
|
|
var createdAt = reader.IsDBNull(2) ? DateTimeOffset.MinValue : DateTimeOffset.Parse(reader.GetString(2));
|
|
return new TodoList(guid, label, createdAt);
|
|
}
|
|
|
|
// ── Private item helpers ─────────────────────────────────────────
|
|
|
|
private static void InsertItem(SqliteConnection conn, TodoItem item)
|
|
{
|
|
var listRowId = GetListRowId(conn, item.ListId)
|
|
?? throw new InvalidOperationException($"Todo list {item.ListId} not found");
|
|
|
|
using var cmd = conn.CreateCommand();
|
|
cmd.CommandText = """
|
|
INSERT INTO todo_items (guid, list_id, text, done, sort_order)
|
|
VALUES (@guid, @listId, @text, @done, @sortOrder);
|
|
""";
|
|
cmd.Parameters.AddWithValue("@guid", item.Id.ToString("D"));
|
|
cmd.Parameters.AddWithValue("@listId", listRowId);
|
|
cmd.Parameters.AddWithValue("@text", item.Text);
|
|
cmd.Parameters.AddWithValue("@done", item.Done ? 1 : 0);
|
|
cmd.Parameters.AddWithValue("@sortOrder", item.SortOrder);
|
|
cmd.ExecuteNonQuery();
|
|
}
|
|
|
|
private static void UpdateItemRow(SqliteConnection conn, TodoItem item)
|
|
{
|
|
using var cmd = conn.CreateCommand();
|
|
cmd.CommandText = """
|
|
UPDATE todo_items SET text = @text, done = @done, sort_order = @sortOrder
|
|
WHERE guid = @guid;
|
|
""";
|
|
cmd.Parameters.AddWithValue("@guid", item.Id.ToString("D"));
|
|
cmd.Parameters.AddWithValue("@text", item.Text);
|
|
cmd.Parameters.AddWithValue("@done", item.Done ? 1 : 0);
|
|
cmd.Parameters.AddWithValue("@sortOrder", item.SortOrder);
|
|
cmd.ExecuteNonQuery();
|
|
}
|
|
|
|
private static bool DeleteItem(SqliteConnection conn, Guid id)
|
|
{
|
|
using var cmd = conn.CreateCommand();
|
|
cmd.CommandText = "DELETE FROM todo_items WHERE guid = @guid;";
|
|
cmd.Parameters.AddWithValue("@guid", id.ToString("D"));
|
|
return cmd.ExecuteNonQuery() > 0;
|
|
}
|
|
|
|
private static TodoItem? ReadItemById(SqliteConnection conn, Guid id)
|
|
{
|
|
using var cmd = conn.CreateCommand();
|
|
cmd.CommandText = """
|
|
SELECT ti.guid, tl.guid, ti.text, ti.done, ti.sort_order
|
|
FROM todo_items ti
|
|
INNER JOIN todo_lists tl ON tl.id = ti.list_id
|
|
WHERE ti.guid = @guid;
|
|
""";
|
|
cmd.Parameters.AddWithValue("@guid", id.ToString("D"));
|
|
|
|
using var reader = cmd.ExecuteReader();
|
|
return reader.Read() ? MapItemRow(reader) : null;
|
|
}
|
|
|
|
private static List<TodoItem> ReadItemsByListId(SqliteConnection conn, Guid listId)
|
|
{
|
|
var results = new List<TodoItem>();
|
|
using var cmd = conn.CreateCommand();
|
|
cmd.CommandText = """
|
|
SELECT ti.guid, tl.guid, ti.text, ti.done, ti.sort_order
|
|
FROM todo_items ti
|
|
INNER JOIN todo_lists tl ON tl.id = ti.list_id
|
|
WHERE tl.guid = @listGuid
|
|
ORDER BY ti.sort_order, ti.guid;
|
|
""";
|
|
cmd.Parameters.AddWithValue("@listGuid", listId.ToString("D"));
|
|
|
|
using var reader = cmd.ExecuteReader();
|
|
while (reader.Read())
|
|
results.Add(MapItemRow(reader));
|
|
|
|
return results;
|
|
}
|
|
|
|
private static TodoItem MapItemRow(SqliteDataReader reader)
|
|
{
|
|
var guid = Guid.Parse(reader.GetString(0));
|
|
var listGuid = Guid.Parse(reader.GetString(1));
|
|
var text = reader.GetString(2);
|
|
var done = !reader.IsDBNull(3) && reader.GetInt64(3) != 0;
|
|
var sortOrder = reader.IsDBNull(4) ? 0 : (int)reader.GetInt64(4);
|
|
return new TodoItem(guid, listGuid, text, done, sortOrder);
|
|
}
|
|
}
|