NoteActions.vala 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206
  1. using Inversion;
  2. using Spry;
  3. using Spry.Authentication;
  4. using Statum;
  5. using ClankerSaddle.Data;
  6. using ClankerSaddle.Services;
  7. namespace ClankerSaddle {
  8. /** Sealed note identity carried by the note row actions. */
  9. public class NoteActionPrivate : Object {
  10. public int64 note_id { get; set; }
  11. }
  12. /**
  13. * Common base for actions sealed against a note id: materialises
  14. * the typed private blob and exposes the resolved id (falling
  15. * back to the hidden `note_id` form field).
  16. */
  17. public abstract class SealedNoteAction : TypedStatumAction<NoteActionPrivate> {
  18. protected override Type private_type { get { return typeof(NoteActionPrivate); } }
  19. /** The sealed note id, or 0 when neither blob nor form field carried one. */
  20. protected int64 sealed_note_id() {
  21. var ctx = request_private;
  22. return ctx != null ? ((!)ctx).note_id : Forms.integer(request, "note_id", 0);
  23. }
  24. }
  25. /**
  26. * Common base for the note-row actions that answer with the
  27. * refreshed dashboard slot (toggle/delete — add and promote carry
  28. * no sealed id of their own, and add reuses the helper below):
  29. * owns the service set {@link DashboardState.build} needs, so a
  30. * mutation answers with refreshed state in ONE call.
  31. */
  32. public abstract class DashboardRefreshingNoteAction : SealedNoteAction {
  33. protected TaskService task_service = inject<TaskService>();
  34. protected SaddleService saddle_service = inject<SaddleService>();
  35. protected TimelineService timeline_service = inject<TimelineService>();
  36. protected InvercargillSql.Orm.OrmSession db = Inversion.inject<InvercargillSql.Orm.OrmSession>();
  37. protected PushService push_service = inject<PushService>();
  38. protected NoteService note_service = inject<NoteService>();
  39. protected UserService users = inject<UserService>();
  40. protected ModelConfigService model_config = inject<ModelConfigService>();
  41. /**
  42. * Rebuilds the dashboard slot from current data — the same
  43. * build the page and its pulse refresh run.
  44. */
  45. protected async Statum.Slot dashboard_slot() throws GLib.Error {
  46. var me = SpryAuth.current(request);
  47. return yield DashboardState.build(state_service, task_service,
  48. saddle_service, timeline_service, db, note_service, action_registry,
  49. push_service, me != null ? ((!)me).user_id : 0, model_config);
  50. }
  51. }
  52. /**
  53. * Adds a dashboard scratchpad note (the body is the whole note —
  54. * no project or assignment semantics). Answers with the
  55. * refreshed dashboard slot, like the page's own refresh. A plain
  56. * action (no sealed id — the form is not row-scoped).
  57. */
  58. public class AddNoteAction : StatumAction {
  59. private NoteService notes = inject<NoteService>();
  60. private TaskService task_service = inject<TaskService>();
  61. private SaddleService saddle_service = inject<SaddleService>();
  62. private TimelineService timeline_service = inject<TimelineService>();
  63. private NoteService note_service = inject<NoteService>();
  64. private InvercargillSql.Orm.OrmSession db = Inversion.inject<InvercargillSql.Orm.OrmSession>();
  65. private PushService push_service = inject<PushService>();
  66. private UserService users = inject<UserService>();
  67. private ModelConfigService model_config = inject<ModelConfigService>();
  68. public override async DirectiveBuilder handle() throws GLib.Error {
  69. var guard = yield SpryAuth.require_login(directives(), request, users);
  70. if (guard != null) {
  71. return (!)guard;
  72. }
  73. string body = Forms.field_raw(request, "body");
  74. if (body.strip().length == 0) {
  75. return directives().notify("error", "A note body is required");
  76. }
  77. try {
  78. yield notes.add_note(body);
  79. var me = SpryAuth.current(request);
  80. var state = yield DashboardState.build(state_service, task_service,
  81. saddle_service, timeline_service, db, note_service, action_registry,
  82. push_service, me != null ? ((!)me).user_id : 0, model_config);
  83. return directives().set(state);
  84. } catch (Error e) {
  85. return directives().notify("error", @"Could not add the note: $(e.message)");
  86. }
  87. }
  88. }
  89. /**
  90. * Toggles a note's done flag (the checkbox on the row). The id
  91. * comes from the sealed {@link NoteActionPrivate} blob; answers
  92. * with the refreshed dashboard slot.
  93. */
  94. public class ToggleNoteAction : DashboardRefreshingNoteAction {
  95. private NoteService notes = inject<NoteService>();
  96. public override async DirectiveBuilder handle() throws GLib.Error {
  97. var guard = yield SpryAuth.require_login(directives(), request, users);
  98. if (guard != null) {
  99. return (!)guard;
  100. }
  101. int64 note_id = sealed_note_id();
  102. if (note_id <= 0) {
  103. return directives().navigate("/");
  104. }
  105. try {
  106. // Flip (the row button carries no direction field): an
  107. // explicit "0"/"1" form value still forces a direction.
  108. var current = yield notes.get_note(note_id);
  109. if (current == null) {
  110. return directives().notify("error", "No such note").navigate("/");
  111. }
  112. string direction = Forms.field(request, "done");
  113. bool target = direction.length > 0
  114. ? direction == "1" : !((!)current).done;
  115. yield notes.set_done(note_id, target);
  116. return directives().set(yield dashboard_slot());
  117. } catch (Error e) {
  118. return directives().notify("error", @"Could not update the note: $(e.message)");
  119. }
  120. }
  121. }
  122. /**
  123. * Deletes a note. The id comes from the sealed private blob;
  124. * answers with the refreshed dashboard slot.
  125. */
  126. public class DeleteNoteAction : DashboardRefreshingNoteAction {
  127. private NoteService notes = inject<NoteService>();
  128. public override async DirectiveBuilder handle() throws GLib.Error {
  129. var guard = yield SpryAuth.require_login(directives(), request, users);
  130. if (guard != null) {
  131. return (!)guard;
  132. }
  133. int64 note_id = sealed_note_id();
  134. if (note_id <= 0) {
  135. return directives().navigate("/");
  136. }
  137. try {
  138. yield notes.delete_note(note_id);
  139. return directives().set(yield dashboard_slot());
  140. } catch (Error e) {
  141. return directives().notify("error", @"Could not delete the note: $(e.message)");
  142. }
  143. }
  144. }
  145. /**
  146. * Promotes a note into a DRAFT goal (goal 8 task 3): title = the
  147. * note's first line, brief = the full note body, on the chosen
  148. * project with the chosen branch. The note is stamped with the
  149. * goal id (frozen — no further promote) and the human lands on
  150. * the goal's draft page to finish it (links, base branch, start).
  151. */
  152. public class PromoteNoteAction : SealedNoteAction {
  153. private NoteService notes = inject<NoteService>();
  154. private UserService users = inject<UserService>();
  155. public override async DirectiveBuilder handle() throws GLib.Error {
  156. var guard = yield SpryAuth.require_login(directives(), request, users);
  157. if (guard != null) {
  158. return (!)guard;
  159. }
  160. int64 note_id = sealed_note_id();
  161. if (note_id <= 0) {
  162. return directives().navigate("/");
  163. }
  164. try {
  165. var goal = yield notes.promote_to_goal(note_id,
  166. Forms.integer(request, "project_id", 0),
  167. Forms.field(request, "branch_name"));
  168. return directives()
  169. .notify("success", @"Goal '$(goal.title)' created in draft from note #$(note_id) — set it up and press start")
  170. .navigate(@"/goals/$(goal.id)");
  171. } catch (Error e) {
  172. return directives()
  173. .notify("error", @"Could not create the goal from note #$(note_id): $(e.message)")
  174. .navigate("/");
  175. }
  176. }
  177. }
  178. }