| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152 |
- using Invercargill;
- using Invercargill.DataStructures;
- using Invercargill.Expressions;
- using InvercargillSql.Orm;
- using Inversion;
- using ClankerSaddle.Data;
- namespace ClankerSaddle.Services {
- /**
- * The dashboard scratchpad (goal 8 task 3): personal notes/todo
- * rows for jotting down batches of changes BEFORE they are ready
- * to be full-on goals or tasks. `notes` carries no project (or
- * any other) semantics — it is the human's own list — and a note
- * promotes into a DRAFT goal with the body as the brief, after
- * which it is frozen and linked to that goal.
- */
- public class NoteService : Object {
- private OrmSession db = inject<OrmSession>();
- private GoalService goals = inject<GoalService>();
- private StatePulse pulse = inject<StatePulse>();
- /** Loads the note identified by `id`, or null. */
- public async Note? get_note(int64 id) throws Error {
- return yield db.query<Note>()
- .where(expr("id == $0", elem<int64?>(id)))
- .first_async();
- }
- /**
- * Inserts a note. The body is trimmed and must be non-empty;
- * a note starts undone and unpromoted.
- */
- public async Note add_note(string body) throws Error {
- string trimmed = body.strip();
- if (trimmed.length == 0) {
- throw new Error(Quark.from_string("clanker-saddle"), 95,
- "A note body is required");
- }
- var note = new Note() {
- body = trimmed,
- done = false,
- created_at = new DateTime.now_local()
- };
- yield db.insert_async<Note>(note);
- yield pulse.bump(0, "note added");
- return note;
- }
- /**
- * Every note, undone first, then newest first (the sort runs
- * in memory — the ORM's order_by carries a single key).
- * Promotion freezes a note but does not finish it, so a
- * promoted note keeps its place by its own done flag.
- */
- public async Enumerable<Note> list_notes() throws Error {
- var rows = yield db.query<Note>()
- .order_by_desc(expr("id"))
- .materialise_async();
- return rows.sort((a, b) => {
- if (a.done != b.done) {
- return a.done ? 1 : -1;
- }
- return a.id > b.id ? -1 : 1;
- });
- }
- /**
- * Sets (or clears) a note's done flag, stamping done_at on
- * the transition. Returns the saved note, or null when no
- * such note exists. A promoted note stays toggleable — done
- * still means "I dealt with this", promotion only locks the
- * create-goal path.
- */
- public async Note? set_done(int64 note_id, bool done) throws Error {
- var note = yield get_note(note_id);
- if (note == null) {
- return null;
- }
- if (((!)note).done != done) {
- ((!)note).done = done;
- ((!)note).done_at = done ? new DateTime.now_local() : null;
- yield db.update_async<Note>((!)note);
- yield pulse.bump(0, "note toggled");
- }
- return note;
- }
- /** Deletes the note (idempotent: an absent note is fine). */
- public async void delete_note(int64 note_id) throws Error {
- var note = yield get_note(note_id);
- if (note == null) {
- return;
- }
- yield db.delete_async<Note>((!)note);
- yield pulse.bump(0, "note deleted");
- }
- /**
- * Derives the promote-to-goal title from the note body: the
- * first non-empty line, clamped to ~80 characters (the clamp
- * appends an ellipsis when it actually cut anything).
- */
- public static string title_from_body(string body) {
- string first_line = "";
- foreach (string line in body.split("\n")) {
- if (line.strip().length > 0) {
- first_line = line.strip();
- break;
- }
- }
- if (first_line.char_count() > 80) {
- first_line = first_line.substring(0,
- (long) first_line.index_of_nth_char(80)) + "…";
- }
- return first_line.length > 0 ? first_line : "Note goal";
- }
- /**
- * Promotes an UNDONE, unpromoted note into a DRAFT goal on
- * `project_id` (title = {@link title_from_body}, brief = the
- * full note body, branch = `branch_name`), stamps the note's
- * goal_id and returns the created goal. The human lands on
- * the goal's page to set links/branch details and press
- * start. A promoted (or done) note refuses — the row is
- * frozen once its goal exists.
- */
- public async Goal promote_to_goal(int64 note_id, int64 project_id,
- string branch_name) throws Error {
- var note = yield get_note(note_id);
- if (note == null) {
- throw new Error(Quark.from_string("clanker-saddle"), 96,
- @"No note exists with id $(note_id)");
- }
- if (((!)note).goal_id > 0) {
- throw new Error(Quark.from_string("clanker-saddle"), 97,
- @"Note #$(note_id) already promoted to goal #$(((!)note).goal_id)");
- }
- if (((!)note).done) {
- throw new Error(Quark.from_string("clanker-saddle"), 98,
- @"Note #$(note_id) is done — only open notes promote to goals");
- }
- var goal = yield goals.create_goal(project_id,
- title_from_body(((!)note).body), ((!)note).body, branch_name, null, 3);
- ((!)note).goal_id = goal.id;
- yield db.update_async<Note>((!)note);
- yield pulse.bump(0, "note promoted");
- return goal;
- }
- }
- }
|