Przeglądaj źródła

Task and goal mutations answer with refreshed state slots

New shared bases TaskDetailRefreshingAction and GoalDetailRefreshingAction own the service set the page state builders need and expose one slot-building call each, so a mutation rebuilds its page view centrally instead of copy-pasting injections. UpdateTaskAction, AddCommentAction (task-page caller), ToggleTaskNotifyAction, GoalCommentAction, PauseGoalAction, ResumeGoalAction, AddGoalLinkAction and RemoveGoalLinkAction now answer success with notify plus the refreshed detail slot and no navigate; the sealed refresh actions reuse the same base. Terminal return_to comments, create-to-detail, delete-to-list, login and error paths keep navigating - that is the right UX there.
clanker 1 miesiąc temu
rodzic
commit
93412406ec
2 zmienionych plików z 86 dodań i 52 usunięć
  1. 44 29
      src/actions/GoalActions.vala
  2. 42 23
      src/actions/TaskActions.vala

+ 44 - 29
src/actions/GoalActions.vala

@@ -34,6 +34,38 @@ namespace ClankerSaddle {
         }
     }
 
+    /**
+     * Shared base for goal-page actions that answer with the
+     * refreshed goal slot instead of a navigate (goal 2 QoL): owns
+     * the service set {@link GoalState.build} needs, so a mutation
+     * rebuilds the page view with ONE call - {@link goal_detail_slot}
+     * - instead of every action copy-pasting the injections and the
+     * build chain. Success paths answer notify(...).set(slot); flows
+     * whose result genuinely IS navigation keep navigating.
+     */
+    public abstract class GoalDetailRefreshingAction : SealedGoalAction {
+
+        protected GoalService goal_service = inject<GoalService>();
+        protected TaskService task_service = inject<TaskService>();
+        protected ProjectService project_service = inject<ProjectService>();
+        protected SaddleService saddle_service = inject<SaddleService>();
+        protected SaddlePollState poll_state = inject<SaddlePollState>();
+        protected RuleService rule_service = inject<RuleService>();
+        protected InvercargillSql.Orm.OrmSession db = Inversion.inject<InvercargillSql.Orm.OrmSession>();
+        protected UserService users = inject<UserService>();
+
+        /**
+         * Rebuilds the goal detail slot from current data - the ONE
+         * call a mutation needs to answer with refreshed state (the
+         * same build the page's refresh action runs).
+         */
+        protected async Statum.Slot goal_detail_slot(int64 goal_id) throws GLib.Error {
+            var ctx = new GoalViewContext(goal_service, task_service, project_service,
+                db, action_registry, saddle_service, poll_state, rule_service);
+            return yield GoalState.build(goal_id, ctx, state_service);
+        }
+    }
+
     /**
      * Creates a goal from the goals form (project, title, brief and a
      * required branch name; base branch optional — the project's
@@ -146,10 +178,9 @@ namespace ClankerSaddle {
     }
 
     /** Pauses an active goal: supervision freezes, events queue. */
-    public class PauseGoalAction : SealedGoalAction {
+    public class PauseGoalAction : GoalDetailRefreshingAction {
 
         private GoalService goals = inject<GoalService>();
-        private UserService users = inject<UserService>();
 
         public override async DirectiveBuilder handle() throws GLib.Error {
             var guard = yield SpryAuth.require_login(directives(), request, users);
@@ -159,7 +190,7 @@ namespace ClankerSaddle {
             try {
                 var goal = yield goals.pause_goal(sealed_goal_id());
                 return directives().notify("success", @"Goal '$(goal.title)' paused")
-                    .navigate(@"/goals/$(goal.id)");
+                    .set(yield goal_detail_slot(goal.id));
             } catch (Error e) {
                 return directives().notify("error", @"Could not pause the goal: $(e.message)").navigate("/goals");
             }
@@ -167,10 +198,9 @@ namespace ClankerSaddle {
     }
 
     /** Resumes a paused goal: queued events coalesce into the next wake. */
-    public class ResumeGoalAction : SealedGoalAction {
+    public class ResumeGoalAction : GoalDetailRefreshingAction {
 
         private GoalService goals = inject<GoalService>();
-        private UserService users = inject<UserService>();
 
         public override async DirectiveBuilder handle() throws GLib.Error {
             var guard = yield SpryAuth.require_login(directives(), request, users);
@@ -180,7 +210,7 @@ namespace ClankerSaddle {
             try {
                 var goal = yield goals.resume_goal(sealed_goal_id());
                 return directives().notify("success", @"Goal '$(goal.title)' resumed")
-                    .navigate(@"/goals/$(goal.id)");
+                    .set(yield goal_detail_slot(goal.id));
             } catch (Error e) {
                 return directives().notify("error", @"Could not resume the goal: $(e.message)").navigate("/goals");
             }
@@ -317,10 +347,9 @@ namespace ClankerSaddle {
      * Posts a comment on a goal (the goal page's thread; a selection
      * quote rides along when the popover opened one).
      */
-    public class GoalCommentAction : SealedGoalAction {
+    public class GoalCommentAction : GoalDetailRefreshingAction {
 
         private GoalService goals = inject<GoalService>();
-        private UserService users = inject<UserService>();
 
         public override async DirectiveBuilder handle() throws GLib.Error {
             var guard = yield SpryAuth.require_login(directives(), request, users);
@@ -340,7 +369,7 @@ namespace ClankerSaddle {
                 yield goals.add_human_comment(sealed_goal_id(), author, body,
                     Forms.optional(request, "quote"));
                 return directives().notify("success", "Comment added")
-                    .navigate(@"/goals/$(sealed_goal_id())");
+                    .set(yield goal_detail_slot(sealed_goal_id()));
             } catch (Error e) {
                 return directives().notify("error", @"Could not add the comment: $(e.message)")
                     .navigate(@"/goals/$(sealed_goal_id())");
@@ -439,16 +468,7 @@ namespace ClankerSaddle {
      * re-hydrates the goal slot with fresh narrative/task/review
      * state, exactly like the task page's refresh.
      */
-    public class RefreshGoalDataAction : SealedGoalAction {
-
-        private GoalService goal_service = inject<GoalService>();
-        private TaskService task_service = inject<TaskService>();
-        private ProjectService project_service = inject<ProjectService>();
-        private SaddleService saddle_service = inject<SaddleService>();
-        private SaddlePollState poll_state = inject<SaddlePollState>();
-        private RuleService rule_service = inject<RuleService>();
-        private InvercargillSql.Orm.OrmSession db = Inversion.inject<InvercargillSql.Orm.OrmSession>();
-        private UserService users = inject<UserService>();
+    public class RefreshGoalDataAction : GoalDetailRefreshingAction {
 
         public override async DirectiveBuilder handle() throws GLib.Error {
             var guard = yield SpryAuth.require_login(directives(), request, users);
@@ -461,18 +481,14 @@ namespace ClankerSaddle {
                 return directives();
             }
 
-            var ctx = new GoalViewContext(goal_service, task_service, project_service,
-                db, action_registry, saddle_service, poll_state, rule_service);
-            var state = yield GoalState.build(goal_id, ctx, state_service);
-            return directives().set(state);
+            return directives().set(yield goal_detail_slot(goal_id));
         }
     }
 
     /** Adds a project link (co-developed/reference) to a goal. */
-    public class AddGoalLinkAction : SealedGoalAction {
+    public class AddGoalLinkAction : GoalDetailRefreshingAction {
 
         private GoalService goals = inject<GoalService>();
-        private UserService users = inject<UserService>();
 
         public override async DirectiveBuilder handle() throws GLib.Error {
             var guard = yield SpryAuth.require_login(directives(), request, users);
@@ -484,7 +500,7 @@ namespace ClankerSaddle {
                     Forms.integer(request, "linked_project_id", 0),
                     Forms.field(request, "relationship"));
                 return directives().notify("success", "Project linked")
-                    .navigate(@"/goals/$(sealed_goal_id())");
+                    .set(yield goal_detail_slot(sealed_goal_id()));
             } catch (Error e) {
                 return directives().notify("error", @"Could not link the project: $(e.message)")
                     .navigate(@"/goals/$(sealed_goal_id())");
@@ -493,10 +509,9 @@ namespace ClankerSaddle {
     }
 
     /** Removes the goal link identified by `link_id`. */
-    public class RemoveGoalLinkAction : SealedGoalAction {
+    public class RemoveGoalLinkAction : GoalDetailRefreshingAction {
 
         private GoalService goals = inject<GoalService>();
-        private UserService users = inject<UserService>();
 
         public override async DirectiveBuilder handle() throws GLib.Error {
             var guard = yield SpryAuth.require_login(directives(), request, users);
@@ -507,7 +522,7 @@ namespace ClankerSaddle {
             try {
                 yield goals.remove_link(link_id);
                 return directives().notify("success", "Link removed")
-                    .navigate(@"/goals/$(sealed_goal_id())");
+                    .set(yield goal_detail_slot(sealed_goal_id()));
             } catch (Error e) {
                 return directives().notify("error", @"Could not remove the link: $(e.message)")
                     .navigate(@"/goals/$(sealed_goal_id())");

+ 42 - 23
src/actions/TaskActions.vala

@@ -33,6 +33,43 @@ namespace ClankerSaddle {
         }
     }
 
+    /**
+     * Shared base for task-page actions that answer with the
+     * refreshed task slot instead of a navigate (goal 2 QoL): owns
+     * the service set {@link TaskDetailState.build} needs, so a
+     * mutation rebuilds the page view with ONE call -
+     * {@link task_detail_slot} - instead of every action copy-pasting
+     * the injections and the build chain. Success paths answer
+     * notify(...).set(slot); flows whose result genuinely IS
+     * navigation (create -> detail, delete -> list) keep navigating.
+     */
+    public abstract class TaskDetailRefreshingAction : SealedTaskAction {
+
+        protected TaskService task_service = inject<TaskService>();
+        protected ProjectService project_service = inject<ProjectService>();
+        protected SaddleService saddle_service = inject<SaddleService>();
+        protected RuleService rule_service = inject<RuleService>();
+        protected CapabilityService capability_service = inject<CapabilityService>();
+        protected SaddlePollState poll_state = inject<SaddlePollState>();
+        protected PushService push_service = inject<PushService>();
+        protected InvercargillSql.Orm.OrmSession db = Inversion.inject<InvercargillSql.Orm.OrmSession>();
+        protected UserService users = inject<UserService>();
+
+        /**
+         * Rebuilds the task detail slot from current data - the ONE
+         * call a mutation needs to answer with refreshed state (the
+         * same build the page's refresh action runs).
+         */
+        protected async Statum.Slot task_detail_slot(int64 task_id) throws GLib.Error {
+            var me = SpryAuth.current(request);
+            return yield TaskDetailState.build(state_service, task_service,
+                project_service, saddle_service, rule_service, capability_service,
+                action_registry, poll_state, push_service, db,
+                me != null ? ((!)me).user_id : 0, task_id);
+        }
+    }
+
+
     /**
      * Creates a task from the queue form (project and title required).
      * The task lands in DRAFT — the human Enqueue action queues it;
@@ -118,10 +155,7 @@ namespace ClankerSaddle {
      * states priority only, in terminal states nothing. Status is not
      * part of the form — Enqueue/Cancel/agent transitions own it.
      */
-    public class UpdateTaskAction : SealedTaskAction {
-
-        private TaskService tasks = inject<TaskService>();
-        private UserService users = inject<UserService>();
+    public class UpdateTaskAction : TaskDetailRefreshingAction {
 
         public override async DirectiveBuilder handle() throws GLib.Error {
             var guard = yield SpryAuth.require_login(directives(), request, users);
@@ -156,7 +190,7 @@ namespace ClankerSaddle {
                 // form actually carried the description field (the
                 // priority-only edit must not blank it — an absent
                 // field is "leave unchanged", same as title).
-                yield tasks.edit_task(task_id,
+                yield task_service.edit_task(task_id,
                     Forms.optional(request, "title"),
                     request.form != null && request.form.get_field("description") != null
                         ? Forms.field(request, "description") : null,
@@ -167,7 +201,7 @@ namespace ClankerSaddle {
                         ? Forms.field(request, "branch_from") : null,
                     mode.length > 0 ? mode : null,
                     enqueue_after);
-                return directives().notify("success", "Task saved").navigate(@"/tasks/$(task_id)");
+                return directives().notify("success", "Task saved").set(yield task_detail_slot(task_id));
             } catch (Error e) {
                 return directives().notify("error", @"Could not save the task: $(e.message)")
                     .navigate(@"/tasks/$(task_id)");
@@ -516,17 +550,7 @@ namespace ClankerSaddle {
      * blocks, output-driven status changes and completions. Navigates
      * nowhere; a failed reload leaves the client's state untouched.
      */
-    public class RefreshTaskDataAction : SealedTaskAction {
-
-        private TaskService task_service = inject<TaskService>();
-        private ProjectService project_service = inject<ProjectService>();
-        private SaddleService saddle_service = inject<SaddleService>();
-        private RuleService rule_service = inject<RuleService>();
-        private CapabilityService capability_service = inject<CapabilityService>();
-        private SaddlePollState poll_state = inject<SaddlePollState>();
-        private PushService push_service = inject<PushService>();
-        private InvercargillSql.Orm.OrmSession db = Inversion.inject<InvercargillSql.Orm.OrmSession>();
-        private UserService users = inject<UserService>();
+    public class RefreshTaskDataAction : TaskDetailRefreshingAction {
 
         public override async DirectiveBuilder handle() throws GLib.Error {
             var guard = yield SpryAuth.require_login(directives(), request, users);
@@ -539,12 +563,7 @@ namespace ClankerSaddle {
                 return directives();
             }
 
-            var me = SpryAuth.current(request);
-            var state = yield TaskDetailState.build(state_service, task_service,
-                project_service, saddle_service, rule_service, capability_service,
-                action_registry, poll_state, push_service, db,
-                me != null ? ((!)me).user_id : 0, task_id);
-            return directives().set(state);
+            return directives().set(yield task_detail_slot(task_id));
         }
     }
 }