ProjectsEntrypoint.vala 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293
  1. using Invercargill;
  2. using Invercargill.DataStructures;
  3. using Spry;
  4. using Spry.Authentication;
  5. using Statum;
  6. using ClankerSaddle.Data;
  7. using ClankerSaddle.Services;
  8. namespace ClankerSaddle {
  9. /** A project row on the projects list, with task counts and a link to its page. */
  10. public class ProjectRowView : Object {
  11. public int64 id { get; set; }
  12. public string name { get; set; default = ""; }
  13. public string git_url { get; set; default = ""; }
  14. public string default_branch { get; set; default = ""; }
  15. public string summary { get; set; default = ""; }
  16. public int64 open_tasks { get; set; }
  17. public int64 done_tasks { get; set; }
  18. }
  19. /** Public data of the `projects` slot. */
  20. public class ProjectsPublic : Object {
  21. public Statum.Model.ActionDto create { get; set; }
  22. }
  23. /**
  24. * Entrypoint for /projects: lists every project with open/done task
  25. * counts and authors the create-project action (login required).
  26. */
  27. public class ProjectsEntrypoint : StatumEntrypoint {
  28. private ProjectService project_service = Inversion.inject<ProjectService>();
  29. private TaskService task_service = Inversion.inject<TaskService>();
  30. private UserService users = Inversion.inject<UserService>();
  31. public override async DirectiveBuilder handle() throws GLib.Error {
  32. var guard = yield SpryAuth.require_login(directives(), request, users);
  33. if (guard != null) {
  34. return (!)guard;
  35. }
  36. var rows = new Vector<ProjectRowView>();
  37. var tasks = yield task_service.list_tasks(null, null);
  38. foreach (var project in yield project_service.list_projects()) {
  39. var view = new ProjectRowView() {
  40. id = project.id,
  41. name = project.name,
  42. git_url = project.git_url,
  43. default_branch = project.default_branch,
  44. summary = project.summary ?? ""
  45. };
  46. foreach (var task in tasks) {
  47. if (task.project_id != project.id) {
  48. continue;
  49. }
  50. if (task.status == STATUS_DONE || task.status == STATUS_FAILED) {
  51. view.done_tasks++;
  52. } else {
  53. view.open_tasks++;
  54. }
  55. }
  56. rows.add(view);
  57. }
  58. var pub = new ProjectsPublic();
  59. pub.create = action_registry.author<CreateProjectAction>();
  60. var props = (PropertyDictionary) GObjectMapping.to_properties(pub);
  61. props["projects"] = StateJson.list<ProjectRowView>(rows.to_immutable_buffer());
  62. var slot = state_service.new_slot(Scope.PAGE, new State() {
  63. type_name = "projects",
  64. public_data = props,
  65. private_data = new PropertyDictionary()
  66. });
  67. return directives().set(slot);
  68. }
  69. }
  70. /** A linked project on the project detail page, with a remove action. */
  71. public class ProjectLinkView : Object {
  72. public int64 link_id { get; set; }
  73. public int64 project_id { get; set; }
  74. public string name { get; set; default = ""; }
  75. public string git_url { get; set; default = ""; }
  76. public string relationship { get; set; default = ""; }
  77. public bool co_developed { get; set; }
  78. public Statum.Model.ActionDto? remove { get; set; }
  79. }
  80. /** A task row on the project detail page. */
  81. public class ProjectTaskRowView : Object {
  82. public int64 id { get; set; }
  83. public string title { get; set; default = ""; }
  84. public string status { get; set; default = ""; }
  85. public string display { get; set; default = ""; }
  86. public int64 priority { get; set; }
  87. }
  88. /** A rule attached to the project, with its detach action. */
  89. public class ProjectRuleRowView : Object {
  90. public int64 link_id { get; set; }
  91. public int64 rule_id { get; set; }
  92. public string name { get; set; default = ""; }
  93. public string body { get; set; default = ""; }
  94. public Statum.Model.ActionDto? detach { get; set; }
  95. }
  96. /** A rule option in the attach selects (project and task pages). */
  97. public class RuleOptionView : Object {
  98. public int64 id { get; set; }
  99. public string name { get; set; default = ""; }
  100. }
  101. /**
  102. * A project note on the project page: LLM-authored via `saddle
  103. * note`, with its date, source task link and a delete action (no
  104. * manual create — notes are user-curated, not user-written).
  105. */
  106. public class ProjectNoteRowView : Object {
  107. public int64 id { get; set; }
  108. public string project_name { get; set; default = ""; }
  109. public int64 source_task_id { get; set; }
  110. public string body { get; set; default = ""; }
  111. public string created_display { get; set; default = ""; }
  112. public Statum.Model.ActionDto? delete { get; set; }
  113. }
  114. /** Public data of the `project` slot. */
  115. public class ProjectDetailPublic : Object {
  116. public bool exists { get; set; }
  117. public int64 id { get; set; }
  118. public string name { get; set; default = ""; }
  119. public string git_url { get; set; default = ""; }
  120. public string default_branch { get; set; default = ""; }
  121. public string summary { get; set; default = ""; }
  122. public string created_display { get; set; default = ""; }
  123. public Statum.Model.ActionDto? update { get; set; }
  124. public Statum.Model.ActionDto? add_link { get; set; }
  125. public Statum.Model.ActionDto? attach_rule { get; set; }
  126. }
  127. /**
  128. * Entrypoint for /projects/{project_id}: the project's editable card,
  129. * its co-developed/reference links (with remove actions), its rules
  130. * (with detach actions and the attach form), the other projects
  131. * available for linking, and its task list.
  132. */
  133. public class ProjectDetailEntrypoint : StatumEntrypoint {
  134. private ProjectService project_service = Inversion.inject<ProjectService>();
  135. private TaskService task_service = Inversion.inject<TaskService>();
  136. private RuleService rule_service = Inversion.inject<RuleService>();
  137. private UserService users = Inversion.inject<UserService>();
  138. public override async DirectiveBuilder handle() throws GLib.Error {
  139. var guard = yield SpryAuth.require_login(directives(), request, users);
  140. if (guard != null) {
  141. return (!)guard;
  142. }
  143. int64 project_id = int64.parse(request.route("project_id") ?? "0");
  144. var project = yield project_service.get_project(project_id);
  145. if (project == null) {
  146. return directives().set_typed<ProjectDetailPublic>("project", Scope.PAGE,
  147. new ProjectDetailPublic() { exists = false });
  148. }
  149. var sealed = new ProjectActionPrivate() { project_id = project_id };
  150. var pub = new ProjectDetailPublic() {
  151. exists = true,
  152. id = ((!)project).id,
  153. name = ((!)project).name,
  154. git_url = ((!)project).git_url,
  155. default_branch = ((!)project).default_branch,
  156. summary = ((!)project).summary ?? "",
  157. created_display = ((!)project).created_at != null
  158. ? ((!)((!)project).created_at).format("%Y-%m-%d %H:%M") : "",
  159. update = action_registry.author_private<UpdateProjectAction, ProjectActionPrivate>(sealed),
  160. add_link = action_registry.author_private<AddProjectLinkAction, ProjectActionPrivate>(sealed),
  161. attach_rule = action_registry.author_private<AttachProjectRuleAction, ProjectActionPrivate>(sealed)
  162. };
  163. var links = new Vector<ProjectLinkView>();
  164. foreach (var link in yield project_service.list_links(project_id)) {
  165. var linked = yield project_service.get_project(link.linked_project_id);
  166. if (linked == null) {
  167. continue;
  168. }
  169. var private_blob = new ProjectLinkActionPrivate() {
  170. project_id = project_id,
  171. link_id = link.id
  172. };
  173. links.add(new ProjectLinkView() {
  174. link_id = link.id,
  175. project_id = link.linked_project_id,
  176. name = ((!)linked).name,
  177. git_url = ((!)linked).git_url,
  178. relationship = link.relationship,
  179. co_developed = link.relationship == LINK_CO_DEVELOPED,
  180. remove = action_registry.author_private<RemoveProjectLinkAction, ProjectLinkActionPrivate>(private_blob)
  181. });
  182. }
  183. var linkable = new Vector<ProjectRowView>();
  184. foreach (var other in yield project_service.list_projects()) {
  185. if (other.id != project_id) {
  186. linkable.add(new ProjectRowView() {
  187. id = other.id,
  188. name = other.name,
  189. git_url = other.git_url,
  190. default_branch = other.default_branch,
  191. summary = other.summary ?? ""
  192. });
  193. }
  194. }
  195. var project_tasks = yield task_service.list_tasks(null, project_id);
  196. var task_display = yield task_service.display_map_for(project_tasks);
  197. var task_rows = new Vector<ProjectTaskRowView>();
  198. foreach (var task in project_tasks) {
  199. var display = task_display.get_or_default(task.id.to_string());
  200. task_rows.add(new ProjectTaskRowView() {
  201. id = task.id,
  202. title = task.title,
  203. status = task.status,
  204. display = display != null ? ((!)display).status : task.status,
  205. priority = task.priority,
  206. });
  207. }
  208. var rule_rows = new Vector<ProjectRuleRowView>();
  209. foreach (var link in yield rule_service.list_project_attachments(project_id)) {
  210. var rule = yield rule_service.get_rule(link.rule_id);
  211. if (rule == null) {
  212. continue;
  213. }
  214. var private_blob = new ProjectRuleActionPrivate() {
  215. project_id = project_id,
  216. link_id = link.id
  217. };
  218. rule_rows.add(new ProjectRuleRowView() {
  219. link_id = link.id,
  220. rule_id = link.rule_id,
  221. name = ((!)rule).name,
  222. body = ((!)rule).body,
  223. detach = action_registry.author_private<DetachProjectRuleAction, ProjectRuleActionPrivate>(private_blob)
  224. });
  225. }
  226. var rule_options = new Vector<RuleOptionView>();
  227. foreach (var rule in yield rule_service.list_rules()) {
  228. rule_options.add(new RuleOptionView() {
  229. id = rule.id,
  230. name = rule.name
  231. });
  232. }
  233. var note_rows = new Vector<ProjectNoteRowView>();
  234. foreach (var note in yield project_service.list_notes(project_id)) {
  235. var note_private = new ProjectNoteActionPrivate() {
  236. project_id = project_id,
  237. note_id = note.id
  238. };
  239. note_rows.add(new ProjectNoteRowView() {
  240. id = note.id,
  241. project_name = ((!)project).name,
  242. source_task_id = note.source_task_id,
  243. body = note.body,
  244. created_display = note.created_at != null
  245. ? ((!)note.created_at).format("%Y-%m-%d %H:%M") : "",
  246. delete = action_registry.author_private<DeleteProjectNoteAction, ProjectNoteActionPrivate>(note_private)
  247. });
  248. }
  249. var props = (PropertyDictionary) GObjectMapping.to_properties(pub);
  250. props["links"] = StateJson.list<ProjectLinkView>(links.to_immutable_buffer());
  251. props["linkable"] = StateJson.list<ProjectRowView>(linkable.to_immutable_buffer());
  252. props["tasks"] = StateJson.list<ProjectTaskRowView>(task_rows.to_immutable_buffer());
  253. props["rules"] = StateJson.list<ProjectRuleRowView>(rule_rows.to_immutable_buffer());
  254. props["rule_options"] = StateJson.list<RuleOptionView>(rule_options.to_immutable_buffer());
  255. props["notes"] = StateJson.list<ProjectNoteRowView>(note_rows.to_immutable_buffer());
  256. var slot = state_service.new_slot(Scope.PAGE, new State() {
  257. type_name = "project",
  258. public_data = props,
  259. private_data = new PropertyDictionary()
  260. });
  261. return directives().set(slot);
  262. }
  263. }
  264. }