|
@@ -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
|
|
* Creates a goal from the goals form (project, title, brief and a
|
|
|
* required branch name; base branch optional — the project's
|
|
* required branch name; base branch optional — the project's
|
|
@@ -100,12 +132,56 @@ namespace ClankerSaddle {
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- /** Pauses an active goal: supervision freezes, events queue. */
|
|
|
|
|
- public class PauseGoalAction : SealedGoalAction {
|
|
|
|
|
|
|
+ /**
|
|
|
|
|
+ * Saves a DRAFT goal's title, brief and priority (the brief-card
|
|
|
|
|
+ * editor, plan QoL task 12): draft is the only phase with an edit
|
|
|
|
|
+ * surface — starting locks the brief and seeds the run task's
|
|
|
|
|
+ * mandate from it. Each field is optional (absent = leave
|
|
|
|
|
+ * unchanged), mirroring {@link UpdateTaskAction}'s carried-field
|
|
|
|
|
+ * semantics, and {@link GoalService.edit_draft_goal} enforces the
|
|
|
|
|
+ * draft-only guard server-side.
|
|
|
|
|
+ */
|
|
|
|
|
+ public class UpdateDraftGoalAction : SealedGoalAction {
|
|
|
|
|
|
|
|
private GoalService goals = inject<GoalService>();
|
|
private GoalService goals = inject<GoalService>();
|
|
|
private UserService users = inject<UserService>();
|
|
private UserService users = inject<UserService>();
|
|
|
|
|
|
|
|
|
|
+ public override async DirectiveBuilder handle() throws GLib.Error {
|
|
|
|
|
+ var guard = yield SpryAuth.require_login(directives(), request, users);
|
|
|
|
|
+ if (guard != null) {
|
|
|
|
|
+ return (!)guard;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ int64 goal_id = sealed_goal_id();
|
|
|
|
|
+ if (goal_id <= 0) {
|
|
|
|
|
+ return directives().navigate("/goals");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ int64? priority = null;
|
|
|
|
|
+ string priority_field = Forms.field(request, "priority");
|
|
|
|
|
+ if (priority_field.length > 0) {
|
|
|
|
|
+ priority = Forms.integer(request, "priority", 3);
|
|
|
|
|
+ }
|
|
|
|
|
+ var goal = yield goals.edit_draft_goal(goal_id,
|
|
|
|
|
+ Forms.optional(request, "title"),
|
|
|
|
|
+ request.form != null && request.form.get_field("brief") != null
|
|
|
|
|
+ ? Forms.field_raw(request, "brief") : null,
|
|
|
|
|
+ priority);
|
|
|
|
|
+ return directives().notify("success", "Goal saved")
|
|
|
|
|
+ .navigate(@"/goals/$(goal.id)");
|
|
|
|
|
+ } catch (Error e) {
|
|
|
|
|
+ return directives().notify("error", @"Could not save the goal: $(e.message)")
|
|
|
|
|
+ .navigate(@"/goals/$(goal_id)");
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /** Pauses an active goal: supervision freezes, events queue. */
|
|
|
|
|
+ public class PauseGoalAction : GoalDetailRefreshingAction {
|
|
|
|
|
+
|
|
|
|
|
+ private GoalService goals = inject<GoalService>();
|
|
|
|
|
+
|
|
|
public override async DirectiveBuilder handle() throws GLib.Error {
|
|
public override async DirectiveBuilder handle() throws GLib.Error {
|
|
|
var guard = yield SpryAuth.require_login(directives(), request, users);
|
|
var guard = yield SpryAuth.require_login(directives(), request, users);
|
|
|
if (guard != null) {
|
|
if (guard != null) {
|
|
@@ -114,7 +190,7 @@ namespace ClankerSaddle {
|
|
|
try {
|
|
try {
|
|
|
var goal = yield goals.pause_goal(sealed_goal_id());
|
|
var goal = yield goals.pause_goal(sealed_goal_id());
|
|
|
return directives().notify("success", @"Goal '$(goal.title)' paused")
|
|
return directives().notify("success", @"Goal '$(goal.title)' paused")
|
|
|
- .navigate(@"/goals/$(goal.id)");
|
|
|
|
|
|
|
+ .set(yield goal_detail_slot(goal.id));
|
|
|
} catch (Error e) {
|
|
} catch (Error e) {
|
|
|
return directives().notify("error", @"Could not pause the goal: $(e.message)").navigate("/goals");
|
|
return directives().notify("error", @"Could not pause the goal: $(e.message)").navigate("/goals");
|
|
|
}
|
|
}
|
|
@@ -122,10 +198,9 @@ namespace ClankerSaddle {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/** Resumes a paused goal: queued events coalesce into the next wake. */
|
|
/** 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 GoalService goals = inject<GoalService>();
|
|
|
- private UserService users = inject<UserService>();
|
|
|
|
|
|
|
|
|
|
public override async DirectiveBuilder handle() throws GLib.Error {
|
|
public override async DirectiveBuilder handle() throws GLib.Error {
|
|
|
var guard = yield SpryAuth.require_login(directives(), request, users);
|
|
var guard = yield SpryAuth.require_login(directives(), request, users);
|
|
@@ -135,13 +210,38 @@ namespace ClankerSaddle {
|
|
|
try {
|
|
try {
|
|
|
var goal = yield goals.resume_goal(sealed_goal_id());
|
|
var goal = yield goals.resume_goal(sealed_goal_id());
|
|
|
return directives().notify("success", @"Goal '$(goal.title)' resumed")
|
|
return directives().notify("success", @"Goal '$(goal.title)' resumed")
|
|
|
- .navigate(@"/goals/$(goal.id)");
|
|
|
|
|
|
|
+ .set(yield goal_detail_slot(goal.id));
|
|
|
} catch (Error e) {
|
|
} catch (Error e) {
|
|
|
return directives().notify("error", @"Could not resume the goal: $(e.message)").navigate("/goals");
|
|
return directives().notify("error", @"Could not resume the goal: $(e.message)").navigate("/goals");
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ /**
|
|
|
|
|
+ * Goal 4 task 1 (Force Wake): the human recovery lever for a
|
|
|
|
|
+ * parked goal agent — sets the goal force-wake flag so the run
|
|
|
|
|
+ * becomes offerable immediately with the explicit wake notice.
|
|
|
|
|
+ * Guards live in {@link GoalService.force_wake_goal} (refuses
|
|
|
|
|
+ * non-active goals and non-parked runs); the page only authors
|
|
|
|
|
+ * the button when they hold.
|
|
|
|
|
+ */
|
|
|
|
|
+ public class GoalForceWakeAction : GoalDetailRefreshingAction {
|
|
|
|
|
+
|
|
|
|
|
+ public override async DirectiveBuilder handle() throws GLib.Error {
|
|
|
|
|
+ var guard = yield SpryAuth.require_login(directives(), request, users);
|
|
|
|
|
+ if (guard != null) {
|
|
|
|
|
+ return (!)guard;
|
|
|
|
|
+ }
|
|
|
|
|
+ try {
|
|
|
|
|
+ var goal = yield goal_service.force_wake_goal(sealed_goal_id());
|
|
|
|
|
+ return directives().notify("success", @"Goal '$(goal.title)' force-woke — the parked run resumes on its saddle")
|
|
|
|
|
+ .set(yield goal_detail_slot(goal.id));
|
|
|
|
|
+ } catch (Error e) {
|
|
|
|
|
+ return directives().notify("error", @"Could not force-woke the goal: $(e.message)").navigate("/goals");
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
/**
|
|
/**
|
|
|
* Cancels a goal: the cascade cancels every non-terminal task under
|
|
* Cancels a goal: the cascade cancels every non-terminal task under
|
|
|
* it (drafts are deleted); live daemons hosting its running tasks
|
|
* it (drafts are deleted); live daemons hosting its running tasks
|
|
@@ -272,10 +372,9 @@ namespace ClankerSaddle {
|
|
|
* Posts a comment on a goal (the goal page's thread; a selection
|
|
* Posts a comment on a goal (the goal page's thread; a selection
|
|
|
* quote rides along when the popover opened one).
|
|
* quote rides along when the popover opened one).
|
|
|
*/
|
|
*/
|
|
|
- public class GoalCommentAction : SealedGoalAction {
|
|
|
|
|
|
|
+ public class GoalCommentAction : GoalDetailRefreshingAction {
|
|
|
|
|
|
|
|
private GoalService goals = inject<GoalService>();
|
|
private GoalService goals = inject<GoalService>();
|
|
|
- private UserService users = inject<UserService>();
|
|
|
|
|
|
|
|
|
|
public override async DirectiveBuilder handle() throws GLib.Error {
|
|
public override async DirectiveBuilder handle() throws GLib.Error {
|
|
|
var guard = yield SpryAuth.require_login(directives(), request, users);
|
|
var guard = yield SpryAuth.require_login(directives(), request, users);
|
|
@@ -295,7 +394,7 @@ namespace ClankerSaddle {
|
|
|
yield goals.add_human_comment(sealed_goal_id(), author, body,
|
|
yield goals.add_human_comment(sealed_goal_id(), author, body,
|
|
|
Forms.optional(request, "quote"));
|
|
Forms.optional(request, "quote"));
|
|
|
return directives().notify("success", "Comment added")
|
|
return directives().notify("success", "Comment added")
|
|
|
- .navigate(@"/goals/$(sealed_goal_id())");
|
|
|
|
|
|
|
+ .set(yield goal_detail_slot(sealed_goal_id()));
|
|
|
} catch (Error e) {
|
|
} catch (Error e) {
|
|
|
return directives().notify("error", @"Could not add the comment: $(e.message)")
|
|
return directives().notify("error", @"Could not add the comment: $(e.message)")
|
|
|
.navigate(@"/goals/$(sealed_goal_id())");
|
|
.navigate(@"/goals/$(sealed_goal_id())");
|
|
@@ -394,16 +493,7 @@ namespace ClankerSaddle {
|
|
|
* re-hydrates the goal slot with fresh narrative/task/review
|
|
* re-hydrates the goal slot with fresh narrative/task/review
|
|
|
* state, exactly like the task page's refresh.
|
|
* 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 {
|
|
public override async DirectiveBuilder handle() throws GLib.Error {
|
|
|
var guard = yield SpryAuth.require_login(directives(), request, users);
|
|
var guard = yield SpryAuth.require_login(directives(), request, users);
|
|
@@ -416,18 +506,14 @@ namespace ClankerSaddle {
|
|
|
return directives();
|
|
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. */
|
|
/** Adds a project link (co-developed/reference) to a goal. */
|
|
|
- public class AddGoalLinkAction : SealedGoalAction {
|
|
|
|
|
|
|
+ public class AddGoalLinkAction : GoalDetailRefreshingAction {
|
|
|
|
|
|
|
|
private GoalService goals = inject<GoalService>();
|
|
private GoalService goals = inject<GoalService>();
|
|
|
- private UserService users = inject<UserService>();
|
|
|
|
|
|
|
|
|
|
public override async DirectiveBuilder handle() throws GLib.Error {
|
|
public override async DirectiveBuilder handle() throws GLib.Error {
|
|
|
var guard = yield SpryAuth.require_login(directives(), request, users);
|
|
var guard = yield SpryAuth.require_login(directives(), request, users);
|
|
@@ -439,7 +525,7 @@ namespace ClankerSaddle {
|
|
|
Forms.integer(request, "linked_project_id", 0),
|
|
Forms.integer(request, "linked_project_id", 0),
|
|
|
Forms.field(request, "relationship"));
|
|
Forms.field(request, "relationship"));
|
|
|
return directives().notify("success", "Project linked")
|
|
return directives().notify("success", "Project linked")
|
|
|
- .navigate(@"/goals/$(sealed_goal_id())");
|
|
|
|
|
|
|
+ .set(yield goal_detail_slot(sealed_goal_id()));
|
|
|
} catch (Error e) {
|
|
} catch (Error e) {
|
|
|
return directives().notify("error", @"Could not link the project: $(e.message)")
|
|
return directives().notify("error", @"Could not link the project: $(e.message)")
|
|
|
.navigate(@"/goals/$(sealed_goal_id())");
|
|
.navigate(@"/goals/$(sealed_goal_id())");
|
|
@@ -448,10 +534,9 @@ namespace ClankerSaddle {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/** Removes the goal link identified by `link_id`. */
|
|
/** Removes the goal link identified by `link_id`. */
|
|
|
- public class RemoveGoalLinkAction : SealedGoalAction {
|
|
|
|
|
|
|
+ public class RemoveGoalLinkAction : GoalDetailRefreshingAction {
|
|
|
|
|
|
|
|
private GoalService goals = inject<GoalService>();
|
|
private GoalService goals = inject<GoalService>();
|
|
|
- private UserService users = inject<UserService>();
|
|
|
|
|
|
|
|
|
|
public override async DirectiveBuilder handle() throws GLib.Error {
|
|
public override async DirectiveBuilder handle() throws GLib.Error {
|
|
|
var guard = yield SpryAuth.require_login(directives(), request, users);
|
|
var guard = yield SpryAuth.require_login(directives(), request, users);
|
|
@@ -462,7 +547,7 @@ namespace ClankerSaddle {
|
|
|
try {
|
|
try {
|
|
|
yield goals.remove_link(link_id);
|
|
yield goals.remove_link(link_id);
|
|
|
return directives().notify("success", "Link removed")
|
|
return directives().notify("success", "Link removed")
|
|
|
- .navigate(@"/goals/$(sealed_goal_id())");
|
|
|
|
|
|
|
+ .set(yield goal_detail_slot(sealed_goal_id()));
|
|
|
} catch (Error e) {
|
|
} catch (Error e) {
|
|
|
return directives().notify("error", @"Could not remove the link: $(e.message)")
|
|
return directives().notify("error", @"Could not remove the link: $(e.message)")
|
|
|
.navigate(@"/goals/$(sealed_goal_id())");
|
|
.navigate(@"/goals/$(sealed_goal_id())");
|