From 1ae8ab35b34f288b7f280197343f1fd1dbe67f55 Mon Sep 17 00:00:00 2001 From: Griffin Smith Date: Mon, 13 Aug 2018 12:20:07 -0400 Subject: [PATCH] feat: Allow creating tickets with a task list Allow creating stories along with a task list comprised of their child elements --- org-clubhouse.el | 170 ++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 138 insertions(+), 32 deletions(-) diff --git a/org-clubhouse.el b/org-clubhouse.el index f7a570ae5..67fec58a4 100644 --- a/org-clubhouse.el +++ b/org-clubhouse.el @@ -1,4 +1,5 @@ -;;; org-clubhouse.el --- Simple, unopinionated integration between org-mode and Clubhouse +;;; org-clubhouse.el --- Simple, unopinionated integration between org-mode and +;;; Clubhouse ;;; Copyright (C) 2018 Off Market Data, Inc. DBA Urbint ;;; Permission is hereby granted, free of charge, to any person obtaining a copy @@ -85,6 +86,8 @@ not be prompted") ("chore" . "Chore") ("prompt" . "**Prompt each time (do not set a default story type)**"))) +(defvar org-clubhouse-default-state "Proposed") + ;;; ;;; Utilities ;;; @@ -186,7 +189,7 @@ not be prompted") (cadr current-elt)))) (defun org-element-extract-clubhouse-id (elt) - (when-let ((clubhouse-id-link (plist-get elt :CLUBHOUSE-ID))) + (when-let* ((clubhouse-id-link (plist-get elt :CLUBHOUSE-ID))) (cond ((string-match (rx "[[" (one-or-more anything) "]" @@ -207,13 +210,30 @@ not be prompted") "[" (group (one-or-more digit)) "]]") strn) (string-to-number (match-string 1 strn))) - ) (defun org-element-clubhouse-id () (org-element-extract-clubhouse-id (org-element-find-headline))) +(defun org-element-and-children-at-point () + (let* ((elt (org-element-find-headline)) + (contents-begin (plist-get elt :contents-begin)) + (end (plist-get elt :end)) + (level (plist-get elt :level)) + (children '())) + (save-excursion + (goto-char (+ contents-begin (length (plist-get elt :title)))) + (while (<= (point) end) + (let* ((next-elt (org-element-at-point)) + (elt-type (car next-elt)) + (elt (cadr next-elt))) + (when (and (eql 'headline elt-type) + (eql (+ 1 level) (plist-get elt :level))) + (push elt children)) + (goto-char (plist-get elt :end))))) + (append elt `(:children ,(reverse children))))) + ;;; ;;; API integration ;;; @@ -269,6 +289,10 @@ not be prompted") reject-archived (to-id-name-pairs id-attr name-attr)))) +(defun org-clubhouse-get-story + (clubhouse-id) + (org-clubhouse-request "GET" (format "/stories/%s" clubhouse-id))) + (defun org-clubhouse-link-to-story (story-id) (format "https://app.clubhouse.io/%s/story/%d" org-clubhouse-team-name @@ -391,7 +415,6 @@ not be prompted") :action (lambda (selected) (let ((project-id (find-match-in-alist selected (org-clubhouse-projects)))) - (message "%d" project-id) (funcall cb project-id))))) (defun org-clubhouse-prompt-for-epic (cb) @@ -402,7 +425,6 @@ not be prompted") :action (lambda (selected) (let ((epic-id (find-match-in-alist selected (org-clubhouse-epics)))) - (message "%d" epic-id) (funcall cb epic-id))))) (defun org-clubhouse-prompt-for-milestone (cb) @@ -414,7 +436,6 @@ not be prompted") :action (lambda (selected) (let ((milestone-id (find-match-in-alist selected (org-clubhouse-milestones)))) - (message "%d" milestone-id) (funcall cb milestone-id))))) (defun org-clubhouse-prompt-for-story-type (cb) @@ -502,20 +523,28 @@ If the epics already have a CLUBHOUSE-EPIC-ID, they are filtered and ignored." ;;; Story creation ;;; +(defun org-clubhouse-default-state-id () + (alist-get-equal org-clubhouse-default-state (org-clubhouse-workflow-states))) + (cl-defun org-clubhouse-create-story-internal (title &key project-id epic-id story-type) (assert (and (stringp title) (integerp project-id) (or (null epic-id) (integerp epic-id)))) - (org-clubhouse-request - "POST" - "stories" - :data - (json-encode - `((name . ,title) - (project_id . ,project-id) - (epic_id . ,epic-id) - (story_type . ,story-type))))) + (let ((workflow-state-id (org-clubhouse-default-state-id)) + (params `((name . ,title) + (project_id . ,project-id) + (epic_id . ,epic-id) + (story_type . ,story-type)))) + + (when workflow-state-id + (push `(workflow_state_id . ,workflow-state-id) params)) + + (org-clubhouse-request + "POST" + "stories" + :data + (json-encode params)))) (defun org-clubhouse-populate-created-story (elt story) (let ((elt-start (plist-get elt :begin)) @@ -547,8 +576,7 @@ If the epics already have a CLUBHOUSE-EPIC-ID, they are filtered and ignored." (org-todo "TODO")))) - -(defun org-clubhouse-create-story (&optional beg end) +(defun org-clubhouse-create-story (&optional beg end &key then) "Creates a clubhouse story using selected headlines. Will pull the title from the headline at point, @@ -557,8 +585,8 @@ or create cards for all the headlines in the selected region. All stories are added to the same project and epic, as selected via two prompts. If the stories already have a CLUBHOUSE-ID, they are filtered and ignored." (interactive - (when (use-region-p) - (list (region-beginning) (region-end)))) + (when (use-region-p) + (list (region-beginning) (region-end)))) (let* ((elts (org-clubhouse-collect-headlines beg end)) (new-elts (-remove (lambda (elt) (plist-get elt :CLUBHOUSE-ID)) elts))) @@ -566,20 +594,98 @@ If the stories already have a CLUBHOUSE-ID, they are filtered and ignored." (lambda (project-id) (when project-id (org-clubhouse-prompt-for-epic - (lambda (epic-id) - (let ((selected-story-type org-clubhouse-default-story-type)) - (if (not selected-story-type) - (org-clubhouse-prompt-for-story-type - (lambda (story-type) - set selected-story-type story-type)) + (lambda (epic-id) + (let ((selected-story-type org-clubhouse-default-story-type)) + (if (not selected-story-type) + (org-clubhouse-prompt-for-story-type + (lambda (story-type) + (setq selected-story-type story-type))) (-map (lambda (elt) - (let* ((title (plist-get elt :title)) - (story (org-clubhouse-create-story-internal - title - :project-id project-id - :epic-id epic-id - :story-type selected-story-type))) - (org-clubhouse-populate-created-story elt story))) new-elts)))))))))) + (let* ((title (plist-get elt :title)) + (story (org-clubhouse-create-story-internal + title + :project-id project-id + :epic-id epic-id))) + (org-clubhouse-populate-created-story elt story) + (when (functionp then) + (funcall then story)))) + new-elts)))))))))) + +(defun org-clubhouse-create-story-with-task-list (&optional beg end) + "Creates a clubhouse story using the selected headline, making all direct +children of that headline into tasks in the task list of the story." + (interactive + (when (use-region-p) + (list (region-beginning) (region-end)))) + + (let* ((elt (org-element-and-children-at-point))) + (org-clubhouse-create-story nil nil + :then (lambda (story) + (pp story) + (org-clubhouse-push-task-list + (alist-get 'id story) + (plist-get elt :children)))))) + +;;; +;;; Task creation +;;; + +(cl-defun org-clubhouse-create-task (title &key story-id) + (assert (and (stringp title) + (integerp story-id))) + (org-clubhouse-request + "POST" + (format "/stories/%d/tasks" story-id) + :data (json-encode `((description . ,title))))) + +(defun org-clubhouse-push-task-list (&optional parent-clubhouse-id child-elts) + "Writes each child element of the current clubhouse element as a task list +item of the associated clubhouse ID. + +when called as (org-clubhouse-push-task-list PARENT-CLUBHOUSE-ID CHILD-ELTS), +allows manually passing a clubhouse ID and list of org-element plists to write" + (interactive) + (let* ((elt (org-element-and-children-at-point)) + (parent-clubhouse-id (or parent-clubhouse-id + (org-element-extract-clubhouse-id elt))) + (child-elts (or child-elts (plist-get elt :children))) + ;; (story (org-clubhouse-get-story parent-clubhouse-id)) + ;; (existing-tasks (alist-get 'tasks story)) + ;; (task-exists + ;; (lambda (task-name) + ;; (some (lambda (task) + ;; (string-equal task-name (alist-get 'description task))) + ;; (existing-tasks)))) + ) + (dolist (child-elt child-elts) + (let ((task-name (plist-get child-elt :title))) + ;; (unless (task-exists task-name) + (let ((task (org-clubhouse-create-task + task-name + :story-id parent-clubhouse-id))) + ;; TODO this doesn't currently work, since the act of populating the + ;; previous task bumps up the char start of the next task + ;; (org-clubhouse-populate-created-task child-elt task) + ) + ;; ) + )))) + +(defun org-clubhouse-populate-created-task (elt task) + (let ((elt-start (plist-get elt :begin)) + (task-id (alist-get 'id task)) + (story-id (alist-get 'story_id task))) + + (save-excursion + (goto-char elt-start) + + (org-set-property "clubhouse-task-id" (format "%d" task-id)) + + (org-set-property "clubhouse-story-id" + (org-make-link-string + (org-clubhouse-link-to-story story-id) + (number-to-string story-id))) + + (org-todo "TODO")))) ;;; ;;; Story updates