From 57008170b3f2180e52ae16c4e1d745a0c31bbb7f Mon Sep 17 00:00:00 2001 From: Griffin Smith Date: Fri, 2 Mar 2018 10:12:50 -0500 Subject: init: Initial commit of org-clubhouse As committed, this allows creating Clubhouse tickets from a heading in org-mode, and then updating the status of those tickets when the Org status updates --- org-clubhouse.el | 421 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 421 insertions(+) create mode 100644 org-clubhouse.el diff --git a/org-clubhouse.el b/org-clubhouse.el new file mode 100644 index 0000000000..a38f7197e0 --- /dev/null +++ b/org-clubhouse.el @@ -0,0 +1,421 @@ +;;; org-clubhouse.el --- Simple, unopinionated integration between org-mode and Clubhouse + +;;; Copyright (C) 2018 Off Market Data, Inc. DBA Urbint + +;;; Commentary: +;;; org-clubhouse provides simple, unopinionated integration between Emacs's +;;; org-mode and the Clubhouse issue tracker +;;; +;;; To configure org-clubhouse, create an authorization token in Cluhbouse's +;;; settings, then place the following configuration somewhere private: +;;; +;;; (setq org-clubhouse-auth-token "" +;;; org-clubhouse-team-name "") +;;; + +;;; Code: + +(require 'dash) +(require 'dash-functional) +(require 's) +(require 'org) +(require 'org-element) +(require 'subr-x) +(require 'json) + +;;; +;;; Configuration +;;; + +(defvar org-clubhouse-auth-token nil + "Authorization token for the Clubhouse API") + +(defvar org-clubhouse-team-name nil + "Team name to use in links to Clubhouse +ie https://app.clubhouse.io//stories") + +(defvar org-clubhouse-project-ids nil + "Specific list of project IDs to synchronize with clubhouse. +If unset all projects will be synchronized") + +(defvar org-clubhouse-workflow-name "Default") + +(defvar org-clubhouse-state-alist + '(("LATER" . "Unscheduled") + ("[ ]" . "Ready for Development") + ("TODO" . "Ready for Development") + ("OPEN" . "Ready for Development") + ("ACTIVE" . "In Development") + ("PR" . "Review") + ("DONE" . "Merged") + ("[X]" . "Merged") + ("CLOSED" . "Merged"))) + +;;; +;;; Utilities +;;; + +(defmacro comment (&rest _) + "Comment out one or more s-expressions." + nil) + +(defun ->list (vec) (append vec nil)) + +(defun reject-archived (item-list) + (-filter (lambda (item) (equal :json-false (alist-get 'archived item))) item-list)) + +(defun alist->plist (key-map alist) + (->> key-map + (-map (lambda (key-pair) + (let ((alist-key (car key-pair)) + (plist-key (cdr key-pair))) + (list plist-key (alist-get alist-key alist))))) + (-flatten-n 1))) + +(defun alist-get-equal (key alist) + "Like `alist-get', but uses `equal' instead of `eq' for comparing keys" + (->> alist + (-find (lambda (pair) (equal key (car pair)))) + (cdr))) + +(comment + + (alist->plist + '((foo . :foo) + (bar . :something)) + + '((foo . "foo") (bar . "bar") (ignored . "ignoreme!"))) + ;; => (:foo "foo" :something "bar") + + ) + +;;; +;;; Org-element interaction +;;; + +;; (defun org-element-find-headline () +;; (let ((current-elt (org-element-at-point))) +;; (if (equal 'headline (car current-elt)) +;; current-elt +;; (let* ((elt-attrs (cadr current-elt)) +;; (parent (plist-get elt-attrs :post-affiliated))) +;; (goto-char parent) +;; (org-element-find-headline))))) + +(defun org-element-find-headline () + (let ((current-elt (org-element-at-point))) + (when (equal 'headline (car current-elt)) + (cadr current-elt)))) + +(defun org-element-extract-clubhouse-id (elt) + (when-let ((clubhouse-id-link (plist-get elt :CLUBHOUSE-ID))) + (cond + ((string-match + (rx "[[" (one-or-more anything) "]" + "[" (group (one-or-more digit)) "]]") + clubhouse-id-link) + (string-to-int (match-string 1 clubhouse-id-link))) + ((string-match-p + (rx buffer-start + (one-or-more digit) + buffer-end) + clubhouse-id-link) + (string-to-int clubhouse-id-link))))) + +(comment + (let ((strn "[[https://app.clubhouse.io/example/story/2330][2330]]")) + (string-match + (rx "[[" (one-or-more anything) "]" + "[" (group (one-or-more digit)) "]]") + strn) + (string-to-int (match-string 1 strn))) + + ) + +(defun org-element-clubhouse-id () + (org-element-extract-clubhouse-id + (org-element-find-headline))) + +;;; +;;; API integration +;;; + +(defvar org-clubhouse-base-url* "https://api.clubhouse.io/api/v2") + +(defun org-clubhouse-auth-url (url) + (concat url + "?" + (url-build-query-string + `(("token" ,org-clubhouse-auth-token))))) + +(defun org-clubhouse-baseify-url (url) + (if (s-starts-with? org-clubhouse-base-url* url) url + (concat org-clubhouse-base-url* + (if (s-starts-with? "/" url) url + (concat "/" url))))) + +(defun org-clubhouse-request (method url &optional data) + (message "%s %s %s" method url (prin1-to-string data)) + (let* ((url-request-method method) + (url-request-extra-headers + '(("Content-Type" . "application/json"))) + (url-request-data data) + (buf)) + + (setq url (-> url + org-clubhouse-baseify-url + org-clubhouse-auth-url)) + + (setq buf (url-retrieve-synchronously url)) + + (with-current-buffer buf + (goto-char url-http-end-of-headers) + (prog1 (json-read) (kill-buffer))))) + +(cl-defun to-id-name-pairs + (seq &optional (id-attr 'id) (name-attr 'name)) + (->> seq + ->list + (-map (lambda (resource) + (cons (alist-get id-attr resource) + (alist-get name-attr resource)))))) + +(cl-defun org-clubhouse-fetch-as-id-name-pairs + (resource &optional + (id-attr 'id) + (name-attr 'name)) + "Returns the given resource from clubhouse as (id . name) pairs" + (let ((resp-json (org-clubhouse-request "GET" resource))) + (-> resp-json + ->list + reject-archived + (to-id-name-pairs id-attr name-attr)))) + +(defun org-clubhouse-link-to-story (story-id) + (format "https://app.clubhouse.io/%s/story/%d" + org-clubhouse-team-name + story-id)) + +(defun org-clubhouse-link-to-epic (epic-id) + (format "https://app.clubhouse.io/%s/epic/%d" + org-clubhouse-team-name + epic-id)) + +(defun org-clubhouse-link-to-project (project-id) + (format "https://app.clubhouse.io/%s/project/%d" + org-clubhouse-team-name + project-id)) + +;;; +;;; Caching +;;; + +(comment + (defcache org-clubhouse-projects + (org-sync-clubhouse-fetch-as-id-name-pairs "projectx")) + + (clear-org-clubhouse-projects-cache) + (clear-org-clubhouse-cache) + ) + +(defvar org-clubhouse-cache-clear-functions ()) + +(defmacro defcache (name &optional docstring &rest body) + (let* ((doc (when docstring (list docstring))) + (cache-var-name (intern (concat (symbol-name name) + "-cache"))) + (clear-cache-function-name + (intern (concat "clear-" (symbol-name cache-var-name))))) + `(progn + (defvar ,cache-var-name :no-cache) + (defun ,name () + ,@doc + (when (equal :no-cache ,cache-var-name) + (setq ,cache-var-name (progn ,@body))) + ,cache-var-name) + (defun ,clear-cache-function-name () + (interactive) + (setq ,cache-var-name :no-cache)) + + (push (quote ,clear-cache-function-name) + org-clubhouse-cache-clear-functions)))) + +(defun org-clubhouse-clear-cache () + (interactive) + (-map #'funcall org-clubhouse-cache-clear-functions)) + +;;; +;;; API resource functions +;;; + +(defcache org-clubhouse-projects + "Returns projects as (project-id . name)" + (org-clubhouse-fetch-as-id-name-pairs "projects")) + +(defcache org-clubhouse-epics + "Returns projects as (project-id . name)" + (org-clubhouse-fetch-as-id-name-pairs "epics")) + +(defcache org-clubhouse-workflow-states + "Returns worflow states as (name . id) pairs" + (let* ((resp-json (org-clubhouse-request "GET" "workflows")) + (workflows (->list resp-json)) + ;; just assume it exists, for now + (workflow (-find (lambda (workflow) + (equal org-clubhouse-workflow-name + (alist-get 'name workflow))) + workflows)) + (states (->list (alist-get 'states workflow)))) + (to-id-name-pairs states + 'name + 'id))) + +(defun org-clubhouse-stories-in-project (project-id) + "Returns the stories in the given project as org bugs" + (let ((resp-json (org-clubhouse-request "GET" (format "/projects/%d/stories" project-id)))) + (->> resp-json ->list reject-archived + (-reject (lambda (story) (equal :json-true (alist-get 'completed story)))) + (-map (lambda (story) + (cons + (cons 'status + (cond + ((equal :json-true (alist-get 'started story)) + 'started) + ((equal :json-true (alist-get 'completed story)) + 'completed) + ('t + 'open))) + story))) + (-map (-partial #'alist->plist + '((name . :title) + (id . :id) + (status . :status))))))) + +;;; +;;; Story creation +;;; + +(cl-defun org-clubhouse-create-story-internal + (title &key project-id epic-id) + (assert (and (stringp title) + (integerp project-id) + (or (null epic-id) (integerp epic-id)))) + (org-clubhouse-request + "POST" + "stories" + (json-encode + `((name . ,title) + (project_id . ,project-id) + (epic_id . ,epic-id))))) + +(defun org-clubhouse-prompt-for-project (cb) + (ivy-read + "Select a project: " + (-map #'cdr (org-clubhouse-projects)) + :require-match t + :history 'org-clubhouse-project-history + :action (lambda (selected) + (let ((project-id + (->> (org-clubhouse-projects) + (-find (lambda (proj) + (string-equal (cdr proj) selected))) + car))) + (message "%d" project-id) + (funcall cb project-id))))) + +(defun org-clubhouse-prompt-for-epic (cb) + (ivy-read + "Select an epic: " + (-map #'cdr (org-clubhouse-epics)) + :history 'org-clubhouse-epic-history + :action (lambda (selected) + (let ((epic-id + (->> (org-clubhouse-epics) + (-find (lambda (proj) + (string-equal (cdr proj) selected))) + car))) + (message "%d" epic-id) + (funcall cb epic-id))))) + +(defun org-clubhouse-populate-created-story (story) + (let ((elt (org-element-find-headline)) + (story-id (alist-get 'id story)) + (epic-id (alist-get 'epic_id story)) + (project-id (alist-get 'project_id story))) + + (org-set-property "clubhouse-id" + (org-make-link-string + (org-clubhouse-link-to-story story-id) + (number-to-string story-id))) + + (org-set-property "clubhouse-epic" + (org-make-link-string + (org-clubhouse-link-to-epic epic-id) + (alist-get epic-id (org-clubhouse-epics)))) + + (org-set-property "clubhouse-project" + (org-make-link-string + (org-clubhouse-link-to-project project-id) + (alist-get project-id (org-clubhouse-projects)))) + + (org-todo "TODO"))) + +(defun org-clubhouse-create-story () + (interactive) + ;; (message (org-element-find-headline)) + (when-let ((elt (org-element-find-headline)) + (title (plist-get elt :title))) + (if (plist-get elt :CLUBHOUSE-ID) + (message "This headline is already a clubhouse story!") + (org-clubhouse-prompt-for-project + (lambda (project-id) + (when project-id + (org-clubhouse-prompt-for-epic + (lambda (epic-id) + (let* ((story (org-clubhouse-create-story-internal + title + :project-id project-id + :epic-id epic-id))) + (org-clubhouse-populate-created-story story)))))))))) + +;;; +;;; Story updates +;;; + +(cl-defun org-clubhouse-update-story-internal + (story-id &rest attrs) + (assert (and (integerp story-id) + (listp attrs))) + (org-clubhouse-request + "PUT" + (format "stories/%d" story-id) + (json-encode attrs))) + +(defun org-clubhouse-update-status () + (when-let (clubhouse-id (org-element-clubhouse-id)) + (let* ((elt (org-element-find-headline)) + (todo-keyword (-> elt (plist-get :todo-keyword) (substring-no-properties)))) + (message todo-keyword) + (when-let ((clubhouse-workflow-state + (alist-get-equal todo-keyword org-clubhouse-state-alist)) + (workflow-state-id + (alist-get-equal clubhouse-workflow-state (org-clubhouse-workflow-states)))) + (org-clubhouse-update-story-internal + clubhouse-id + :workflow_state_id workflow-state-id) + (message "Successfully updated clubhouse status to \"%s\"" + clubhouse-workflow-state))))) + +(define-minor-mode org-clubhouse-mode + :init-value nil + :group 'org + :lighter "Org-Clubhouse" + :keymap '() + (add-hook 'org-after-todo-state-change-hook + 'org-clubhouse-update-status + nil + t)) + + +(provide 'org-clubhouse) +;;; org-clubhouse.el ends here -- cgit 1.4.1 From 089a5ee2c07c96c3cb5d55a5bb718872d64b3920 Mon Sep 17 00:00:00 2001 From: Griffin Smith Date: Fri, 2 Mar 2018 10:22:22 -0500 Subject: docs: Add one-liner README When we publish this to MELPA (coming up next) I'll also document installation, configuration, and usage --- README.org | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 README.org diff --git a/README.org b/README.org new file mode 100644 index 0000000000..0d862a535b --- /dev/null +++ b/README.org @@ -0,0 +1,5 @@ +* Org-Clubhouse + +Simple, unopinionated integration between Emacs's [[https://orgmode.org/][org-mode]] and the [[https://clubhouse.io/][Clubhouse]] issue tracker + + -- cgit 1.4.1 From f2230e30bf0ab87cc2856d54f412c57936d2f7c7 Mon Sep 17 00:00:00 2001 From: Griffin Smith Date: Fri, 2 Mar 2018 10:44:48 -0500 Subject: docs: MIT License --- LICENSE | 7 +++++++ org-clubhouse.el | 17 +++++++++++++++++ 2 files changed, 24 insertions(+) create mode 100644 LICENSE diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000000..1777f0fac3 --- /dev/null +++ b/LICENSE @@ -0,0 +1,7 @@ +Copyright (C) 2018 Off Market Data, Inc. DBA Urbint + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/org-clubhouse.el b/org-clubhouse.el index a38f7197e0..da90a71d9c 100644 --- a/org-clubhouse.el +++ b/org-clubhouse.el @@ -1,6 +1,23 @@ ;;; 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 +;;; of this software and associated documentation files (the "Software"), to +;;; deal in the Software without restriction, including without limitation the +;;; rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +;;; sell copies of the Software, and to permit persons to whom the Software is +;;; furnished to do so, subject to the following conditions: + +;;; The above copyright notice and this permission notice shall be included in +;;; all copies or substantial portions of the Software. + +;;; THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +;;; IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +;;; FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +;;; AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +;;; LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +;;; FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +;;; IN THE SOFTWARE. ;;; Commentary: ;;; org-clubhouse provides simple, unopinionated integration between Emacs's -- cgit 1.4.1 From 2b8278579455d36062130f1104d658a4f461a6f6 Mon Sep 17 00:00:00 2001 From: Russell Matney Date: Fri, 2 Mar 2018 15:13:23 -0500 Subject: feat: create-story region supported Refactors `org-clubhouse-create-story` to pull stories from a region if one is selected, and fallback to the headline at point. --- org-clubhouse.el | 123 +++++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 87 insertions(+), 36 deletions(-) diff --git a/org-clubhouse.el b/org-clubhouse.el index da90a71d9c..16df71cdf3 100644 --- a/org-clubhouse.el +++ b/org-clubhouse.el @@ -106,6 +106,45 @@ If unset all projects will be synchronized") ) + +(defun org-clubhouse-collect-headlines (beg end) + "Collects the headline at point or the headlines in a region. Returns a list." + (setq test-headlines + (if (and beg end) + (get-headlines-in-region beg end) + (list (org-element-find-headline))))) + + +(defun org-clubhouse-get-headlines-in-region (beg end) + "Collects the headlines from BEG to END" + (save-excursion + ;; This beg/end clean up pulled from `reverse-region`. + ;; it expands the region to include the full lines from the selected region. + + ;; put beg at the start of a line and end and the end of one -- + ;; the largest possible region which fits this criteria + (goto-char beg) + (or (bolp) (forward-line 1)) + (setq beg (point)) + (goto-char end) + ;; the test for bolp is for those times when end is on an empty line; + ;; it is probably not the case that the line should be included in the + ;; reversal; it isn't difficult to add it afterward. + (or (and (eolp) (not (bolp))) (progn (forward-line -1) (end-of-line))) + (setq end (point-marker)) + + ;; move to the beginning + (goto-char beg) + ;; walk by line until past end + (let ((headlines '()) + (before-end 't)) + (while before-end + (add-to-list 'headlines (org-element-find-headline)) + (let ((before (point))) + (org-forward-heading-same-level 1) + (setq before-end (and (not (eq before (point))) (< (point) end))))) + headlines))) + ;;; ;;; Org-element interaction ;;; @@ -354,46 +393,58 @@ If unset all projects will be synchronized") (message "%d" epic-id) (funcall cb epic-id))))) -(defun org-clubhouse-populate-created-story (story) - (let ((elt (org-element-find-headline)) +(defun org-clubhouse-populate-created-story (elt story) + (let ((elt-start (plist-get elt :begin)) (story-id (alist-get 'id story)) (epic-id (alist-get 'epic_id story)) (project-id (alist-get 'project_id story))) - (org-set-property "clubhouse-id" - (org-make-link-string - (org-clubhouse-link-to-story story-id) - (number-to-string story-id))) - - (org-set-property "clubhouse-epic" - (org-make-link-string - (org-clubhouse-link-to-epic epic-id) - (alist-get epic-id (org-clubhouse-epics)))) - - (org-set-property "clubhouse-project" - (org-make-link-string - (org-clubhouse-link-to-project project-id) - (alist-get project-id (org-clubhouse-projects)))) - - (org-todo "TODO"))) - -(defun org-clubhouse-create-story () - (interactive) - ;; (message (org-element-find-headline)) - (when-let ((elt (org-element-find-headline)) - (title (plist-get elt :title))) - (if (plist-get elt :CLUBHOUSE-ID) - (message "This headline is already a clubhouse story!") - (org-clubhouse-prompt-for-project - (lambda (project-id) - (when project-id - (org-clubhouse-prompt-for-epic - (lambda (epic-id) - (let* ((story (org-clubhouse-create-story-internal - title - :project-id project-id - :epic-id epic-id))) - (org-clubhouse-populate-created-story story)))))))))) + (save-excursion + (goto-char elt-start) + + (org-set-property "clubhouse-id" + (org-make-link-string + (org-clubhouse-link-to-story story-id) + (number-to-string story-id))) + + (org-set-property "clubhouse-epic" + (org-make-link-string + (org-clubhouse-link-to-epic epic-id) + (alist-get epic-id (org-clubhouse-epics)))) + + (org-set-property "clubhouse-project" + (org-make-link-string + (org-clubhouse-link-to-project project-id) + (alist-get project-id (org-clubhouse-projects)))) + + (org-todo "TODO")))) + +(defun org-clubhouse-create-story (&optional beg end) + "Creates a clubhouse story using selected headlines. + +Will pull the title from the headline at point, +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 + (if (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))) + (org-clubhouse-prompt-for-project + (lambda (project-id) + (when project-id + (org-clubhouse-prompt-for-epic + (lambda (epic-id) + (-map (lambda (elt) + (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))) new-elts)))))))) ;;; ;;; Story updates -- cgit 1.4.1 From bfc599abe3cfb1b76d8531f31d96c7a535bda05f Mon Sep 17 00:00:00 2001 From: Russell Matney Date: Fri, 2 Mar 2018 15:46:35 -0500 Subject: docs: installation instructions for quelpa, doom Adds installation instructions for Quelpa and Doom Emacs. --- README.org | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/README.org b/README.org index 0d862a535b..79cef4dc2c 100644 --- a/README.org +++ b/README.org @@ -2,4 +2,27 @@ Simple, unopinionated integration between Emacs's [[https://orgmode.org/][org-mode]] and the [[https://clubhouse.io/][Clubhouse]] issue tracker +* Install + +** [[https://github.com/quelpa/quelpa][Quelpa]] + +#+BEGIN_SRC emacs-lisp +(quelpa! '(org-clubhouse + :fetcher github + :repo "urbint/org-clubhouse" + :files ("*"))) +#+END_SRC + +** [[https://github.com/hlissner/doom-emacs/][DOOM Emacs]] + +#+BEGIN_SRC emacs-lisp +;; in packages.el +(package! org-clubhouse + :recipe (:fetcher github + :repo "urbint/org-clubhouse" + :files ("*"))) + +;; in config.el +(def-package! org-clubhouse) +#+END_SRC -- cgit 1.4.1 From 47c0fe97413594623e384b48481e27b119c993c6 Mon Sep 17 00:00:00 2001 From: Russell Matney Date: Fri, 2 Mar 2018 16:24:21 -0500 Subject: docs: add setup note Docs the need for auth-token and team-name. The Auth token needs to be generated per user, and can be done in clubhouse. --- README.org | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/README.org b/README.org index 79cef4dc2c..3ad524ce19 100644 --- a/README.org +++ b/README.org @@ -26,3 +26,12 @@ Simple, unopinionated integration between Emacs's [[https://orgmode.org/][org-mo (def-package! org-clubhouse) #+END_SRC + +* Setup + +Once setup, you'll need to set two global config vars. + +#+BEGIN_SRC emacs-lisp +(setq org-clubhouse-auth-token "" + org-clubhouse-team-name "") +#+END_SRC -- cgit 1.4.1 From 621752ba0ea4897e900bf1335f62cb523acfa1f6 Mon Sep 17 00:00:00 2001 From: Russell Matney Date: Fri, 2 Mar 2018 17:22:02 -0500 Subject: fix: namespace function call to match name Hotfix - this was not working! --- org-clubhouse.el | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/org-clubhouse.el b/org-clubhouse.el index 16df71cdf3..c25b3352e7 100644 --- a/org-clubhouse.el +++ b/org-clubhouse.el @@ -111,7 +111,7 @@ If unset all projects will be synchronized") "Collects the headline at point or the headlines in a region. Returns a list." (setq test-headlines (if (and beg end) - (get-headlines-in-region beg end) + (org-clubhouse-get-headlines-in-region beg end) (list (org-element-find-headline))))) -- cgit 1.4.1 From 8134c3011c34d0cdc374e11815d16f6fc48e4389 Mon Sep 17 00:00:00 2001 From: Griffin Smith Date: Mon, 12 Mar 2018 15:00:22 -0400 Subject: feat: Allow creating epics Add an org-clubhouse-create-epic function that prompts for a milestone then creates a new epic under that milestone from the current org element --- org-clubhouse.el | 118 +++++++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 101 insertions(+), 17 deletions(-) diff --git a/org-clubhouse.el b/org-clubhouse.el index c25b3352e7..333caeca71 100644 --- a/org-clubhouse.el +++ b/org-clubhouse.el @@ -79,7 +79,7 @@ If unset all projects will be synchronized") (defun ->list (vec) (append vec nil)) (defun reject-archived (item-list) - (-filter (lambda (item) (equal :json-false (alist-get 'archived item))) item-list)) + (-reject (lambda (item) (equal :json-true (alist-get 'archived item))) item-list)) (defun alist->plist (key-map alist) (->> key-map @@ -257,6 +257,11 @@ If unset all projects will be synchronized") org-clubhouse-team-name epic-id)) +(defun org-clubhouse-link-to-milestone (milestone-id) + (format "https://app.clubhouse.io/%s/milestone/%d" + org-clubhouse-team-name + milestone-id)) + (defun org-clubhouse-link-to-project (project-id) (format "https://app.clubhouse.io/%s/project/%d" org-clubhouse-team-name @@ -312,6 +317,10 @@ If unset all projects will be synchronized") "Returns projects as (project-id . name)" (org-clubhouse-fetch-as-id-name-pairs "epics")) +(defcache org-clubhouse-milestones + "Returns milestone-id . name)" + (org-clubhouse-fetch-as-id-name-pairs "milestones")) + (defcache org-clubhouse-workflow-states "Returns worflow states as (name . id) pairs" (let* ((resp-json (org-clubhouse-request "GET" "workflows")) @@ -348,22 +357,9 @@ If unset all projects will be synchronized") (status . :status))))))) ;;; -;;; Story creation +;;; Prompting ;;; -(cl-defun org-clubhouse-create-story-internal - (title &key project-id epic-id) - (assert (and (stringp title) - (integerp project-id) - (or (null epic-id) (integerp epic-id)))) - (org-clubhouse-request - "POST" - "stories" - (json-encode - `((name . ,title) - (project_id . ,project-id) - (epic_id . ,epic-id))))) - (defun org-clubhouse-prompt-for-project (cb) (ivy-read "Select a project: " @@ -393,6 +389,95 @@ If unset all projects will be synchronized") (message "%d" epic-id) (funcall cb epic-id))))) +(defun org-clubhouse-prompt-for-milestone (cb) + (ivy-read + "Select a milestone: " + (-map #'cdr (org-clubhouse-milestones)) + :require-match t + :history 'org-clubhouse-milestone-history + :action (lambda (selected) + (let ((milestone-id + (->> (org-clubhouse-milestones) + (-find (lambda (proj) + (string-equal (cdr proj) selected))) + car))) + (message "%d" milestone-id) + (funcall cb milestone-id))))) + +;;; +;;; Epic creation +;;; + +(cl-defun org-clubhouse-create-epic-internal + (title &key milestone-id) + (assert (and (stringp title) + (integerp milestone-id))) + (org-clubhouse-request + "POST" + "epics" + (json-encode + `((name . ,title) + (milestone_id . ,milestone-id))))) + +(defun org-clubhouse-populate-created-epic (elt epic) + (let ((elt-start (plist-get elt :begin)) + (epic-id (alist-get 'id epic)) + (milestone-id (alist-get 'milestone_id epic))) + + (save-excursion + (goto-char elt-start) + + (org-set-property "clubhouse-epic-id" + (org-make-link-string + (org-clubhouse-link-to-epic epic-id) + (number-to-string epic-id))) + + (org-set-property "clubhouse-milestone" + (org-make-link-string + (org-clubhouse-link-to-milestone milestone-id) + (alist-get milestone-id (org-clubhouse-milestones))))))) + +(defun org-clubhouse-create-epic (&optional beg end) + "Creates a clubhouse epic using selected headlines. +Will pull the title from the headline at point, or create epics for all the +headlines in the selected region. + +All epics are added to the same milestone, as selected via a prompt. +If the epics already have a CLUBHOUSE-EPIC-ID, they are filtered and ignored." + (interactive + (when (use-region-p) + (list (region-beginning region-end)))) + + (let* ((elts (org-clubhouse-collect-headlines beg end)) + (elts (-remove (lambda (elt) (plist-get elt :CLUBHOUSE-EPIC-ID)) elts))) + (org-clubhouse-prompt-for-milestone + (lambda (milestone-id) + (when milestone-id + (dolist (elt elts) + (let* ((title (plist-get elt :title)) + (epic (org-clubhouse-create-epic-internal + title + :milestone-id milestone-id))) + (org-clubhouse-populate-created-epic elt epic)) + elts)))))) + +;;; +;;; Story creation +;;; + +(cl-defun org-clubhouse-create-story-internal + (title &key project-id epic-id) + (assert (and (stringp title) + (integerp project-id) + (or (null epic-id) (integerp epic-id)))) + (org-clubhouse-request + "POST" + "stories" + (json-encode + `((name . ,title) + (project_id . ,project-id) + (epic_id . ,epic-id))))) + (defun org-clubhouse-populate-created-story (elt story) (let ((elt-start (plist-get elt :begin)) (story-id (alist-get 'id story)) @@ -428,7 +513,7 @@ 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 - (if (use-region-p) + (when (use-region-p) (list (region-beginning) (region-end)))) (let* ((elts (org-clubhouse-collect-headlines beg end)) @@ -463,7 +548,6 @@ If the stories already have a CLUBHOUSE-ID, they are filtered and ignored." (when-let (clubhouse-id (org-element-clubhouse-id)) (let* ((elt (org-element-find-headline)) (todo-keyword (-> elt (plist-get :todo-keyword) (substring-no-properties)))) - (message todo-keyword) (when-let ((clubhouse-workflow-state (alist-get-equal todo-keyword org-clubhouse-state-alist)) (workflow-state-id -- cgit 1.4.1 From d406682f727815af08e51c18781086a6c8e1bd28 Mon Sep 17 00:00:00 2001 From: William Carroll Date: Fri, 16 Mar 2018 15:22:20 -0400 Subject: fix: quelpa install docs I'm not sure `quelpa!` is a function... ``` Debugger entered--Lisp error: (void-function quelpa!) ``` ...additionally, is `:files ("*")` necessary? Looks like the `(package! ...)` was copy-pasted and package -> quelpa. Does this hold up? --- README.org | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/README.org b/README.org index 3ad524ce19..711093cbfb 100644 --- a/README.org +++ b/README.org @@ -7,10 +7,9 @@ Simple, unopinionated integration between Emacs's [[https://orgmode.org/][org-mo ** [[https://github.com/quelpa/quelpa][Quelpa]] #+BEGIN_SRC emacs-lisp -(quelpa! '(org-clubhouse - :fetcher github - :repo "urbint/org-clubhouse" - :files ("*"))) +(quelpa '(org-clubhouse + :fetcher github + :repo "urbint/org-clubhouse")) #+END_SRC ** [[https://github.com/hlissner/doom-emacs/][DOOM Emacs]] -- cgit 1.4.1 From 08340c223ac9188c385ceddb480f7f4e61c3fb42 Mon Sep 17 00:00:00 2001 From: Griffin Smith Date: Wed, 11 Apr 2018 10:49:30 -0400 Subject: refactor: Parametrize org-clubhouse-request Make org-clubhouse-request a kv-function with an additional :params kv, for supporting query params in requests --- org-clubhouse.el | 54 +++++++++++++++++++++++++++++------------------------- 1 file changed, 29 insertions(+), 25 deletions(-) diff --git a/org-clubhouse.el b/org-clubhouse.el index 333caeca71..f232a67d0f 100644 --- a/org-clubhouse.el +++ b/org-clubhouse.el @@ -198,35 +198,35 @@ If unset all projects will be synchronized") (defvar org-clubhouse-base-url* "https://api.clubhouse.io/api/v2") -(defun org-clubhouse-auth-url (url) - (concat url - "?" - (url-build-query-string - `(("token" ,org-clubhouse-auth-token))))) +(defun org-clubhouse-auth-url (url &optional params) + (concat url + "?" + (url-build-query-string + (cons `("token" ,org-clubhouse-auth-token) params)))) (defun org-clubhouse-baseify-url (url) - (if (s-starts-with? org-clubhouse-base-url* url) url - (concat org-clubhouse-base-url* - (if (s-starts-with? "/" url) url - (concat "/" url))))) + (if (s-starts-with? org-clubhouse-base-url* url) url + (concat org-clubhouse-base-url* + (if (s-starts-with? "/" url) url + (concat "/" url))))) -(defun org-clubhouse-request (method url &optional data) - (message "%s %s %s" method url (prin1-to-string data)) - (let* ((url-request-method method) - (url-request-extra-headers - '(("Content-Type" . "application/json"))) - (url-request-data data) - (buf)) +(cl-defun org-clubhouse-request (method url &key data (params '())) + (message "%s %s %s" method url (prin1-to-string data)) + (let* ((url-request-method method) + (url-request-extra-headers + '(("Content-Type" . "application/json"))) + (url-request-data data) + (buf)) - (setq url (-> url - org-clubhouse-baseify-url - org-clubhouse-auth-url)) + (setq url (-> url + org-clubhouse-baseify-url + (org-clubhouse-auth-url params))) - (setq buf (url-retrieve-synchronously url)) + (setq buf (url-retrieve-synchronously url)) - (with-current-buffer buf - (goto-char url-http-end-of-headers) - (prog1 (json-read) (kill-buffer))))) + (with-current-buffer buf + (goto-char url-http-end-of-headers) + (prog1 (json-read) (kill-buffer))))) (cl-defun to-id-name-pairs (seq &optional (id-attr 'id) (name-attr 'name)) @@ -415,6 +415,7 @@ If unset all projects will be synchronized") (org-clubhouse-request "POST" "epics" + :data (json-encode `((name . ,title) (milestone_id . ,milestone-id))))) @@ -473,6 +474,7 @@ If the epics already have a CLUBHOUSE-EPIC-ID, they are filtered and ignored." (org-clubhouse-request "POST" "stories" + :data (json-encode `((name . ,title) (project_id . ,project-id) @@ -542,13 +544,15 @@ If the stories already have a CLUBHOUSE-ID, they are filtered and ignored." (org-clubhouse-request "PUT" (format "stories/%d" story-id) + :data (json-encode attrs))) (defun org-clubhouse-update-status () - (when-let (clubhouse-id (org-element-clubhouse-id)) + (interactive) + (when-let* ((clubhouse-id (org-element-clubhouse-id))) (let* ((elt (org-element-find-headline)) (todo-keyword (-> elt (plist-get :todo-keyword) (substring-no-properties)))) - (when-let ((clubhouse-workflow-state + (when-let* ((clubhouse-workflow-state (alist-get-equal todo-keyword org-clubhouse-state-alist)) (workflow-state-id (alist-get-equal clubhouse-workflow-state (org-clubhouse-workflow-states)))) -- cgit 1.4.1 From bb97402cbe78f1e7a7f69c3a89d509b3cc257a36 Mon Sep 17 00:00:00 2001 From: Alex Dao Date: Mon, 26 Mar 2018 17:47:03 -0400 Subject: feat: support setting story type on creation Adds an interactive menu for selecting story type on story creation. --- org-clubhouse.el | 46 ++++++++++++++++++++++++++++++++++++---------- 1 file changed, 36 insertions(+), 10 deletions(-) diff --git a/org-clubhouse.el b/org-clubhouse.el index f232a67d0f..c1b9d7bc1b 100644 --- a/org-clubhouse.el +++ b/org-clubhouse.el @@ -68,6 +68,11 @@ If unset all projects will be synchronized") ("[X]" . "Merged") ("CLOSED" . "Merged"))) +(defvar org-clubhouse-story-types + '(("feature" . "Feature") + ("bug" . "Bug") + ("chore" . "Chore"))) + ;;; ;;; Utilities ;;; @@ -404,6 +409,19 @@ If unset all projects will be synchronized") (message "%d" milestone-id) (funcall cb milestone-id))))) +(defun org-clubhouse-prompt-for-story-type (cb) + (ivy-read + "Select a story type: " + (-map #'cdr org-clubhouse-story-types) + :history 'org-clubhouse-story-history + :action (lambda (selected) + (let ((story-type + (->> org-clubhouse-story-types + (-find (lambda (proj) + (string-equal (cdr proj) selected))) + car))) + (funcall cb story-type))))) + ;;; ;;; Epic creation ;;; @@ -467,7 +485,7 @@ If the epics already have a CLUBHOUSE-EPIC-ID, they are filtered and ignored." ;;; (cl-defun org-clubhouse-create-story-internal - (title &key project-id epic-id) + (title &key project-id epic-id story-type) (assert (and (stringp title) (integerp project-id) (or (null epic-id) (integerp epic-id)))) @@ -478,13 +496,15 @@ If the epics already have a CLUBHOUSE-EPIC-ID, they are filtered and ignored." (json-encode `((name . ,title) (project_id . ,project-id) - (epic_id . ,epic-id))))) + (epic_id . ,epic-id) + (story_type . ,story-type))))) (defun org-clubhouse-populate-created-story (elt story) (let ((elt-start (plist-get elt :begin)) (story-id (alist-get 'id story)) (epic-id (alist-get 'epic_id story)) - (project-id (alist-get 'project_id story))) + (project-id (alist-get 'project_id story)) + (story-type (alist-get 'story_type story))) (save-excursion (goto-char elt-start) @@ -504,6 +524,9 @@ If the epics already have a CLUBHOUSE-EPIC-ID, they are filtered and ignored." (org-clubhouse-link-to-project project-id) (alist-get project-id (org-clubhouse-projects)))) + (org-set-property "story-type" + (alist-get-equal story-type org-clubhouse-story-types)) + (org-todo "TODO")))) (defun org-clubhouse-create-story (&optional beg end) @@ -525,13 +548,16 @@ If the stories already have a CLUBHOUSE-ID, they are filtered and ignored." (when project-id (org-clubhouse-prompt-for-epic (lambda (epic-id) - (-map (lambda (elt) - (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))) new-elts)))))))) + (org-clubhouse-prompt-for-story-type + (lambda (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 story-type))) + (org-clubhouse-populate-created-story elt story))) new-elts)))))))))) ;;; ;;; Story updates -- cgit 1.4.1 From c4096e5dbb2212fb816c58780a453b62314af6dc Mon Sep 17 00:00:00 2001 From: Alex Dao Date: Sun, 10 Jun 2018 01:05:36 -0400 Subject: feat: support setting a default story type Expose a `org-clubhouse-default-story-type` variable that can be set to `feature`, `bug`, `chore` or `prompt`. If set to `prompt`, the org-clubhouse `Create new story` flow will prompt for the story type each time. If set to any of the other values, the `Create new story` flow will use that value for all new stories until that variable is set otherwise. This also exposes a new interactive function called `org-clubhouse-set-default-story-type` that prompts users to set the default story type value. --- org-clubhouse.el | 80 ++++++++++++++++++++++++++++++++++++-------------------- 1 file changed, 51 insertions(+), 29 deletions(-) diff --git a/org-clubhouse.el b/org-clubhouse.el index c1b9d7bc1b..fed65b3779 100644 --- a/org-clubhouse.el +++ b/org-clubhouse.el @@ -57,6 +57,12 @@ If unset all projects will be synchronized") (defvar org-clubhouse-workflow-name "Default") +(defvar org-clubhouse-default-story-type nil + "Sets the default story type. If set to 'nil', it will interactively prompt +the user each and every time a new story is created. If set to 'feature', +'bug', or 'chore', that value will be used as the default and the user will +not be prompted") + (defvar org-clubhouse-state-alist '(("LATER" . "Unscheduled") ("[ ]" . "Ready for Development") @@ -73,6 +79,12 @@ If unset all projects will be synchronized") ("bug" . "Bug") ("chore" . "Chore"))) +(defvar org-clubhouse-default-story-types + '(("feature" . "Feature") + ("bug" . "Bug") + ("chore" . "Chore") + ("prompt" . "**Prompt each time (do not set a default story type)**"))) + ;;; ;;; Utilities ;;; @@ -111,6 +123,11 @@ If unset all projects will be synchronized") ) +(defun find-match-in-alist (target alist) + (->> alist + (-find (lambda (key-value) + (string-equal (cdr key-value) target))) + car)) (defun org-clubhouse-collect-headlines (beg end) "Collects the headline at point or the headlines in a region. Returns a list." @@ -175,13 +192,13 @@ If unset all projects will be synchronized") (rx "[[" (one-or-more anything) "]" "[" (group (one-or-more digit)) "]]") clubhouse-id-link) - (string-to-int (match-string 1 clubhouse-id-link))) + (string-to-number (match-string 1 clubhouse-id-link))) ((string-match-p (rx buffer-start (one-or-more digit) buffer-end) clubhouse-id-link) - (string-to-int clubhouse-id-link))))) + (string-to-number clubhouse-id-link))))) (comment (let ((strn "[[https://app.clubhouse.io/example/story/2330][2330]]")) @@ -189,7 +206,7 @@ If unset all projects will be synchronized") (rx "[[" (one-or-more anything) "]" "[" (group (one-or-more digit)) "]]") strn) - (string-to-int (match-string 1 strn))) + (string-to-number (match-string 1 strn))) ) @@ -373,10 +390,7 @@ If unset all projects will be synchronized") :history 'org-clubhouse-project-history :action (lambda (selected) (let ((project-id - (->> (org-clubhouse-projects) - (-find (lambda (proj) - (string-equal (cdr proj) selected))) - car))) + (find-match-in-alist selected (org-clubhouse-projects)))) (message "%d" project-id) (funcall cb project-id))))) @@ -387,10 +401,7 @@ If unset all projects will be synchronized") :history 'org-clubhouse-epic-history :action (lambda (selected) (let ((epic-id - (->> (org-clubhouse-epics) - (-find (lambda (proj) - (string-equal (cdr proj) selected))) - car))) + (find-match-in-alist selected (org-clubhouse-epics)))) (message "%d" epic-id) (funcall cb epic-id))))) @@ -402,10 +413,7 @@ If unset all projects will be synchronized") :history 'org-clubhouse-milestone-history :action (lambda (selected) (let ((milestone-id - (->> (org-clubhouse-milestones) - (-find (lambda (proj) - (string-equal (cdr proj) selected))) - car))) + (find-match-in-alist selected (org-clubhouse-milestones)))) (message "%d" milestone-id) (funcall cb milestone-id))))) @@ -416,12 +424,22 @@ If unset all projects will be synchronized") :history 'org-clubhouse-story-history :action (lambda (selected) (let ((story-type - (->> org-clubhouse-story-types - (-find (lambda (proj) - (string-equal (cdr proj) selected))) - car))) + (find-match-in-alist selected org-clubhouse-story-types))) (funcall cb story-type))))) +(defun org-clubhouse-prompt-for-default-story-type () + (interactive) + (ivy-read + "Select a default story type: " + (-map #'cdr org-clubhouse-default-story-types) + :history 'org-clubhouse-default-story-history + :action (lambda (selected) + (let ((story-type + (find-match-in-alist selected org-clubhouse-default-story-types))) + (if (string-equal story-type "prompt") + (setq org-clubhouse-default-story-type nil) + (setq org-clubhouse-default-story-type story-type)))))) + ;;; ;;; Epic creation ;;; @@ -529,6 +547,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) "Creates a clubhouse story using selected headlines. @@ -548,16 +567,19 @@ If the stories already have a CLUBHOUSE-ID, they are filtered and ignored." (when project-id (org-clubhouse-prompt-for-epic (lambda (epic-id) - (org-clubhouse-prompt-for-story-type - (lambda (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 story-type))) - (org-clubhouse-populate-created-story elt story))) new-elts)))))))))) + (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)) + (-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)))))))))) ;;; ;;; Story updates -- cgit 1.4.1 From 1d7734ce32ae840009e2668ef5fd1dbaec939234 Mon Sep 17 00:00:00 2001 From: Alex Dao Date: Mon, 26 Mar 2018 22:25:30 -0400 Subject: feat: support updating the story title Defines an 'org-clubhouse-update-story-title' interactive function. Can only be invoked if cursor is over the Headline title fixes string-to-int -> string-to-number (unsupported as of Emacs 26) --- org-clubhouse.el | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/org-clubhouse.el b/org-clubhouse.el index fed65b3779..f7a570ae50 100644 --- a/org-clubhouse.el +++ b/org-clubhouse.el @@ -585,6 +585,18 @@ If the stories already have a CLUBHOUSE-ID, they are filtered and ignored." ;;; Story updates ;;; +(defun org-clubhouse-update-story-title () + (interactive) + + (when-let (clubhouse-id (org-element-clubhouse-id)) + (let* ((elt (org-element-find-headline)) + (title (plist-get elt :title))) + (org-clubhouse-update-story-internal + clubhouse-id + :name title) + (message "Successfully updated story title to \"%s\"" + title)))) + (cl-defun org-clubhouse-update-story-internal (story-id &rest attrs) (assert (and (integerp story-id) -- cgit 1.4.1 From 1ae8ab35b34f288b7f280197343f1fd1dbe67f55 Mon Sep 17 00:00:00 2001 From: Griffin Smith Date: Mon, 13 Aug 2018 12:20:07 -0400 Subject: 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 f7a570ae50..67fec58a44 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 -- cgit 1.4.1 From 1b0b98ec7d3a2acb13ff792d33765a2d1a0a9a3b Mon Sep 17 00:00:00 2001 From: Griffin Smith Date: Wed, 26 Sep 2018 11:41:33 -0400 Subject: feat: Create org-mode headlines from query Merges in a function which has existed in my local setup for a while now to run a query to Clubhouse and create all the results as org headlines at a specified level --- org-clubhouse.el | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/org-clubhouse.el b/org-clubhouse.el index 67fec58a44..33539e2c98 100644 --- a/org-clubhouse.el +++ b/org-clubhouse.el @@ -728,6 +728,37 @@ allows manually passing a clubhouse ID and list of org-element plists to write" (message "Successfully updated clubhouse status to \"%s\"" clubhouse-workflow-state))))) + +(defun org-clubhouse-headlines-from-query (level query) + "Create `org-mode' headlines from a clubhouse query. + +Submits QUERY to clubhouse, and creates `org-mode' headlines from all the +resulting stories at headline level LEVEL." + (interactive + "*nLevel: \nMQuery: ") + (let* ((sprint-stories + (org-clubhouse-request + "GET" + "search/stories" + :params '((query query)))) + (sprint-story-list (-> sprint-stories cdr car cdr (append nil)))) + (save-mark-and-excursion + (insert + (mapconcat (lambda (story) + (format + "%s TODO %s +:PROPERTIES: +:clubhouse-id: %s +:END: +" + (make-string level ?*) + (alist-get 'name story) + (let ((story-id (alist-get 'id story))) + (org-make-link-string + (org-clubhouse-link-to-story story-id) + (number-to-string story-id))))) + (reject-archived sprint-story-list) "\n"))))) + (define-minor-mode org-clubhouse-mode :init-value nil :group 'org -- cgit 1.4.1 From 8ed9c37a7abd3762aa280d7adf35dee190b3680a Mon Sep 17 00:00:00 2001 From: Jean-Martin Archer Date: Mon, 12 Nov 2018 22:24:08 -0800 Subject: docs: Add spacemacs setup instructions Add instructions for installation via Spacemacs --- README.org | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.org b/README.org index 711093cbfb..8dd8fc8d2e 100644 --- a/README.org +++ b/README.org @@ -25,6 +25,12 @@ Simple, unopinionated integration between Emacs's [[https://orgmode.org/][org-mo (def-package! org-clubhouse) #+END_SRC +** [[http://spacemacs.org/][Spacemacs]] +#+BEGIN_SRC emacs-lisp +;; in .spacemacs (SPC+fed) + dotspacemacs-additional-packages + '((org-clubhouse :location (recipe :fetcher github :repo "urbint/org-clubhouse"))) +#+END_SRC * Setup -- cgit 1.4.1 From a72382a77c81b6d79e201bb97f286f1e2aea411d Mon Sep 17 00:00:00 2001 From: Griffin Smith Date: Fri, 1 Feb 2019 11:22:31 -0500 Subject: feat: Add description to created headlines This is nice to have, but also stick it in a drawer so it's not obtrusive --- org-clubhouse.el | 35 +++++++++++++++++++++-------------- 1 file changed, 21 insertions(+), 14 deletions(-) diff --git a/org-clubhouse.el b/org-clubhouse.el index 33539e2c98..118997a9b2 100644 --- a/org-clubhouse.el +++ b/org-clubhouse.el @@ -740,24 +740,31 @@ resulting stories at headline level LEVEL." (org-clubhouse-request "GET" "search/stories" - :params '((query query)))) - (sprint-story-list (-> sprint-stories cdr car cdr (append nil)))) - (save-mark-and-excursion - (insert - (mapconcat (lambda (story) - (format - "%s TODO %s + :params `((query ,query)))) + (sprint-story-list (-> sprint-stories cdr car cdr (append nil) + reject-archived))) + (if (null sprint-story-list) + (message "Query returned no stories: %s" query) + (save-mark-and-excursion + (insert + (mapconcat (lambda (story) + (format + "%s TODO %s :PROPERTIES: :clubhouse-id: %s :END: +:DESCRIPTION: +%s +:END: " - (make-string level ?*) - (alist-get 'name story) - (let ((story-id (alist-get 'id story))) - (org-make-link-string - (org-clubhouse-link-to-story story-id) - (number-to-string story-id))))) - (reject-archived sprint-story-list) "\n"))))) + (make-string level ?*) + (alist-get 'name story) + (let ((story-id (alist-get 'id story))) + (org-make-link-string + (org-clubhouse-link-to-story story-id) + (number-to-string story-id))) + (alist-get 'description story))) + (reject-archived sprint-story-list) "\n")))))) (define-minor-mode org-clubhouse-mode :init-value nil -- cgit 1.4.1 From 0967cbcea6de5dccf0d8512dafd0331e6c836bbe Mon Sep 17 00:00:00 2001 From: Griffin Smith Date: Fri, 1 Feb 2019 11:25:39 -0500 Subject: fix: flow control in story creation The various prompt-function callbacks get called on another thread, meaning we can't wait for them to return to set the value. This moves the flow control for story creation so it actually happens if you don't have a default story type set --- org-clubhouse.el | 29 +++++++++++++++-------------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/org-clubhouse.el b/org-clubhouse.el index 118997a9b2..442e73edeb 100644 --- a/org-clubhouse.el +++ b/org-clubhouse.el @@ -595,21 +595,22 @@ If the stories already have a CLUBHOUSE-ID, they are filtered and ignored." (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 + (let ((create-story (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))) - (org-clubhouse-populate-created-story elt story) - (when (functionp then) - (funcall then story)))) - new-elts)))))))))) + (-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 story-type))) + (org-clubhouse-populate-created-story elt story) + (when (functionp then) + (funcall then story)))) + new-elts)))) + (if org-clubhouse-default-story-type + (funcall create-story org-clubhouse-default-story-type) + (org-clubhouse-prompt-for-story-type create-story)))))))))) (defun org-clubhouse-create-story-with-task-list (&optional beg end) "Creates a clubhouse story using the selected headline, making all direct -- cgit 1.4.1 From baeff81f89f80751a8cb7257c750f3b4b1b8b36f Mon Sep 17 00:00:00 2001 From: Griffin Smith Date: Fri, 1 Feb 2019 11:39:14 -0500 Subject: docs: Bolster docs on setup, usage and config Add docs for all config variables and interactive commands, and list commands in the README --- README.org | 43 ++++++++++++++++++++++++++++++++++++++++++- org-clubhouse.el | 10 ++++++++-- 2 files changed, 50 insertions(+), 3 deletions(-) diff --git a/README.org b/README.org index 8dd8fc8d2e..b7548c347f 100644 --- a/README.org +++ b/README.org @@ -34,9 +34,50 @@ Simple, unopinionated integration between Emacs's [[https://orgmode.org/][org-mo * Setup -Once setup, you'll need to set two global config vars. +Once installed, you'll need to set two global config vars: #+BEGIN_SRC emacs-lisp (setq org-clubhouse-auth-token "" org-clubhouse-team-name "") #+END_SRC + +You can generate a new personal API token by going to the "API Tokens" tab on +the "Settings" page in the clubhouse UI. + +Org-clubhouse can be configured to update the status of stories as you update +their todo-keyword in org-mode. To opt-into this behavior, set the +~org-clubhouse-mode~ minor-mode: + +#+BEGIN_SRC emacs-lisp +(add-hook 'org-mode-hook #'org-clubhouse-mode nil nil) +#+END_SRC + +* Usage + +In addition to updating the status of stories linked to clubhouse tickets, +org-clubhouse provides the following commands: + +- ~org-clubhouse-create-story~ + Creates a new Clubhouse story from the current headline, or if a region of + headlines is selected bulk-creates stories with all those headlines +- ~org-clubhouse-create-epic~ + Creates a new Clubhouse epic from the current headline, or if a region of + headlines is selected bulk-creates epics with all those headlines +- ~org-clubhouse-create-story-with-task-list~ + Creates a Clubhouse story from the current headline, making all direct + children of the headline into tasks in the task list of the story +- ~org-clubhouse-push-task-list~ + Writes each child element of the current clubhouse element as a task list + item of the associated clubhouse ID. +- ~org-clubhouse-update-story-title~ + Updates the title of the Clubhouse story linked to the current headline with + the text of the headline +- ~org-clubhouse-headlines-from-query~ + Create org-mode headlines from a clubhouse query at the cursor's current + position, prompting for the headline indentation level and clubhouse query + text + +* Configuration + +Refer to the beginning of the [[https://github.com/urbint/org-clubhouse/blob/master/org-clubhouse.el][~org-clubhouse.el~]] file in this repository for +documentation on all supported configuration variables diff --git a/org-clubhouse.el b/org-clubhouse.el index 442e73edeb..456f372bd9 100644 --- a/org-clubhouse.el +++ b/org-clubhouse.el @@ -73,7 +73,10 @@ not be prompted") ("PR" . "Review") ("DONE" . "Merged") ("[X]" . "Merged") - ("CLOSED" . "Merged"))) + ("CLOSED" . "Merged")) + "Alist mapping org-mode todo keywords to their corresponding states in + Clubhouse. In `org-clubhouse-mode', moving headlines to these todo keywords + will update to the corresponding status in Clubhouse") (defvar org-clubhouse-story-types '(("feature" . "Feature") @@ -86,7 +89,8 @@ not be prompted") ("chore" . "Chore") ("prompt" . "**Prompt each time (do not set a default story type)**"))) -(defvar org-clubhouse-default-state "Proposed") +(defvar org-clubhouse-default-state "Proposed" + "Default state to create all new stories in") ;;; ;;; Utilities @@ -693,6 +697,8 @@ allows manually passing a clubhouse ID and list of org-element plists to write" ;;; (defun org-clubhouse-update-story-title () + "Updates the title of the Clubhouse story linked to the current headline with +the text of the headline" (interactive) (when-let (clubhouse-id (org-element-clubhouse-id)) -- cgit 1.4.1 From 627799d4dc848aefa0ff9ab5bb755384d933b0fa Mon Sep 17 00:00:00 2001 From: Griffin Smith Date: Fri, 1 Feb 2019 11:58:26 -0500 Subject: feat: Add org-clubhouse-update-description Add a command to update the description of the current story with the contents of a drawer labeled DESCRIPTION, if one exists --- README.org | 3 +++ org-clubhouse.el | 49 +++++++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 50 insertions(+), 2 deletions(-) diff --git a/README.org b/README.org index b7548c347f..f312b02a74 100644 --- a/README.org +++ b/README.org @@ -72,6 +72,9 @@ org-clubhouse provides the following commands: - ~org-clubhouse-update-story-title~ Updates the title of the Clubhouse story linked to the current headline with the text of the headline +- ~org-clubhouse-update-description~ + Update the status of the Clubhouse story linked to the current element with + the contents of a drawer inside the element called DESCRIPTION, if any exists - ~org-clubhouse-headlines-from-query~ Create org-mode headlines from a clubhouse query at the cursor's current position, prompting for the headline indentation level and clubhouse query diff --git a/org-clubhouse.el b/org-clubhouse.el index 456f372bd9..81df722024 100644 --- a/org-clubhouse.el +++ b/org-clubhouse.el @@ -238,6 +238,32 @@ not be prompted") (goto-char (plist-get elt :end))))) (append elt `(:children ,(reverse children))))) +(defun +org-element-contents (elt) + (buffer-substring-no-properties + (plist-get (cadr elt) :contents-begin) + (plist-get (cadr elt) :contents-end))) + +(defun org-clubhouse-find-description-drawer () + "Try to find a DESCRIPTION drawer in the current element." + (let ((elt (org-element-at-point))) + (cl-case (car elt) + ('drawer (+org-element-contents elt)) + ('headline + (when-let ((drawer-pos (string-match + ":DESCRIPTION:" + (+org-element-contents elt)))) + (save-excursion + (goto-char (+ (plist-get (cadr elt) :contents-begin) + drawer-pos)) + (org-clubhouse-find-description-drawer))))))) + +(comment + --elt + (equal 'drawer (car --elt)) + () + --elt + ) + ;;; ;;; API integration ;;; @@ -697,8 +723,10 @@ allows manually passing a clubhouse ID and list of org-element plists to write" ;;; (defun org-clubhouse-update-story-title () - "Updates the title of the Clubhouse story linked to the current headline with -the text of the headline" + "Update the title of the Clubhouse story linked to the current headline. + +Update the title of the story linked to the current headline with the text of +the headline." (interactive) (when-let (clubhouse-id (org-element-clubhouse-id)) @@ -721,6 +749,11 @@ the text of the headline" (json-encode attrs))) (defun org-clubhouse-update-status () + "Update the status of the Clubhouse story linked to the current element. + +Update the status of the Clubhouse story linked to the current element with the +entry in `org-clubhouse-state-alist' corresponding to the todo-keyword of the +element." (interactive) (when-let* ((clubhouse-id (org-element-clubhouse-id))) (let* ((elt (org-element-find-headline)) @@ -735,6 +768,18 @@ the text of the headline" (message "Successfully updated clubhouse status to \"%s\"" clubhouse-workflow-state))))) +(defun org-clubhouse-update-description () + "Update the description of the Clubhouse story linked to the current element. + +Update the status of the Clubhouse story linked to the current element with the +contents of a drawer inside the element called DESCRIPTION, if any." + (interactive) + (when-let* ((clubhouse-id (org-element-clubhouse-id)) + (new-description (org-clubhouse-find-description-drawer))) + (org-clubhouse-update-story-internal + clubhouse-id + :description new-description) + (message "Successfully updated story description"))) (defun org-clubhouse-headlines-from-query (level query) "Create `org-mode' headlines from a clubhouse query. -- cgit 1.4.1 From 3fc1a3445b834775c9ada4c0531897af9393fe1f Mon Sep 17 00:00:00 2001 From: Griffin Smith Date: Fri, 15 Feb 2019 16:12:33 -0500 Subject: feat: Populate description when creating stories If a DESCRIPTION drawer exists on headlines being used to create stories, it is populated as the description of the created story --- org-clubhouse.el | 42 +++++++++++++++++++++++++----------------- 1 file changed, 25 insertions(+), 17 deletions(-) diff --git a/org-clubhouse.el b/org-clubhouse.el index 81df722024..722ccb9e6c 100644 --- a/org-clubhouse.el +++ b/org-clubhouse.el @@ -239,9 +239,10 @@ not be prompted") (append elt `(:children ,(reverse children))))) (defun +org-element-contents (elt) - (buffer-substring-no-properties - (plist-get (cadr elt) :contents-begin) - (plist-get (cadr elt) :contents-end))) + (if-let ((begin (plist-get (cadr elt) :contents-begin)) + (end (plist-get (cadr elt) :contents-end))) + (buffer-substring-no-properties begin end) + "")) (defun org-clubhouse-find-description-drawer () "Try to find a DESCRIPTION drawer in the current element." @@ -557,15 +558,17 @@ If the epics already have a CLUBHOUSE-EPIC-ID, they are filtered and ignored." (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) + (title &key project-id epic-id story-type description) (assert (and (stringp title) (integerp project-id) - (or (null epic-id) (integerp epic-id)))) + (or (null epic-id) (integerp epic-id)) + (or (null description) (stringp description)))) (let ((workflow-state-id (org-clubhouse-default-state-id)) (params `((name . ,title) (project_id . ,project-id) (epic_id . ,epic-id) - (story_type . ,story-type)))) + (story_type . ,story-type) + (description . ,description)))) (when workflow-state-id (push `(workflow_state_id . ,workflow-state-id) params)) @@ -627,17 +630,22 @@ If the stories already have a CLUBHOUSE-ID, they are filtered and ignored." (lambda (epic-id) (let ((create-story (lambda (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 story-type))) - (org-clubhouse-populate-created-story elt story) - (when (functionp then) - (funcall then story)))) - new-elts)))) + (-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 story-type)) + (description + (save-mark-and-excursion + (goto-char (plist-get elt :begin)) + (org-clubhouse-find-description-drawer)))) + (org-clubhouse-populate-created-story elt story) + (when (functionp then) + (funcall then story)))) + new-elts)))) (if org-clubhouse-default-story-type (funcall create-story org-clubhouse-default-story-type) (org-clubhouse-prompt-for-story-type create-story)))))))))) -- cgit 1.4.1 From 514afa33c09b8961a02c13b3c399ed177c9f3e73 Mon Sep 17 00:00:00 2001 From: Griffin Smith Date: Fri, 15 Feb 2019 16:13:12 -0500 Subject: docs: Add docstring to org-clubhouse-mode flycheck was complaining, plus this is good anyway --- org-clubhouse.el | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/org-clubhouse.el b/org-clubhouse.el index 722ccb9e6c..e4c8daf56a 100644 --- a/org-clubhouse.el +++ b/org-clubhouse.el @@ -827,7 +827,8 @@ resulting stories at headline level LEVEL." (reject-archived sprint-story-list) "\n")))))) (define-minor-mode org-clubhouse-mode - :init-value nil + "If enabled, updates to the todo keywords on org headlines will update the +linked ticket in Clubhouse." :group 'org :lighter "Org-Clubhouse" :keymap '() -- cgit 1.4.1 From 4b4c0f1f4a05a816ab520ca4e62f864fba650c01 Mon Sep 17 00:00:00 2001 From: Griffin Smith Date: Mon, 18 Feb 2019 11:56:43 -0500 Subject: fix: Reference to cl-assert This is required by default in my emacs, but needs to be required here for people not running Doom --- org-clubhouse.el | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/org-clubhouse.el b/org-clubhouse.el index e4c8daf56a..3d18d93768 100644 --- a/org-clubhouse.el +++ b/org-clubhouse.el @@ -33,6 +33,7 @@ ;;; Code: +(require 'cl-macs) (require 'dash) (require 'dash-functional) (require 's) @@ -258,13 +259,6 @@ not be prompted") drawer-pos)) (org-clubhouse-find-description-drawer))))))) -(comment - --elt - (equal 'drawer (car --elt)) - () - --elt - ) - ;;; ;;; API integration ;;; @@ -413,7 +407,7 @@ not be prompted") 'id))) (defun org-clubhouse-stories-in-project (project-id) - "Returns the stories in the given project as org bugs" + "Return the stories in the given PROJECT-ID as org headlines." (let ((resp-json (org-clubhouse-request "GET" (format "/projects/%d/stories" project-id)))) (->> resp-json ->list reject-archived (-reject (lambda (story) (equal :json-true (alist-get 'completed story)))) @@ -498,7 +492,7 @@ not be prompted") (cl-defun org-clubhouse-create-epic-internal (title &key milestone-id) - (assert (and (stringp title) + (cl-assert (and (stringp title) (integerp milestone-id))) (org-clubhouse-request "POST" @@ -559,7 +553,7 @@ If the epics already have a CLUBHOUSE-EPIC-ID, they are filtered and ignored." (cl-defun org-clubhouse-create-story-internal (title &key project-id epic-id story-type description) - (assert (and (stringp title) + (cl-assert (and (stringp title) (integerp project-id) (or (null epic-id) (integerp epic-id)) (or (null description) (stringp description)))) @@ -670,7 +664,7 @@ children of that headline into tasks in the task list of the story." ;;; (cl-defun org-clubhouse-create-task (title &key story-id) - (assert (and (stringp title) + (cl-assert (and (stringp title) (integerp story-id))) (org-clubhouse-request "POST" @@ -748,7 +742,7 @@ the headline." (cl-defun org-clubhouse-update-story-internal (story-id &rest attrs) - (assert (and (integerp story-id) + (cl-assert (and (integerp story-id) (listp attrs))) (org-clubhouse-request "PUT" -- cgit 1.4.1 From 5682f4bb573c204c40688e932f1d7e7296213831 Mon Sep 17 00:00:00 2001 From: Griffin Smith Date: Mon, 18 Feb 2019 12:06:28 -0500 Subject: fix: Actually pass description when creating story Oops! Fixes #12 --- org-clubhouse.el | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/org-clubhouse.el b/org-clubhouse.el index 3d18d93768..50388b3dad 100644 --- a/org-clubhouse.el +++ b/org-clubhouse.el @@ -562,7 +562,7 @@ If the epics already have a CLUBHOUSE-EPIC-ID, they are filtered and ignored." (project_id . ,project-id) (epic_id . ,epic-id) (story_type . ,story-type) - (description . ,description)))) + (description . ,(or description ""))))) (when workflow-state-id (push `(workflow_state_id . ,workflow-state-id) params)) @@ -627,15 +627,16 @@ If the stories already have a CLUBHOUSE-ID, they are filtered and ignored." (-map (lambda (elt) (let* ((title (plist-get elt :title)) + (description + (save-mark-and-excursion + (goto-char (plist-get elt :begin)) + (org-clubhouse-find-description-drawer))) (story (org-clubhouse-create-story-internal title :project-id project-id :epic-id epic-id - :story-type story-type)) - (description - (save-mark-and-excursion - (goto-char (plist-get elt :begin)) - (org-clubhouse-find-description-drawer)))) + :story-type story-type + :description description))) (org-clubhouse-populate-created-story elt story) (when (functionp then) (funcall then story)))) -- cgit 1.4.1 From d338b4d3043808e0b0d98885c9bb8b80f4bca57e Mon Sep 17 00:00:00 2001 From: Griffin Smith Date: Mon, 18 Feb 2019 12:25:01 -0500 Subject: feat: Use workflow state for todo-keyword Base the todo-keyword of a created story on its workflow-state in clubhouse, rather than just hardcoding it to TODO Fixes #11 --- org-clubhouse.el | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/org-clubhouse.el b/org-clubhouse.el index 50388b3dad..34f51d24b9 100644 --- a/org-clubhouse.el +++ b/org-clubhouse.el @@ -120,6 +120,10 @@ not be prompted") (-find (lambda (pair) (equal key (car pair)))) (cdr))) +(defun invert-alist (alist) + "Invert the keys and values of ALIST." + (-map (lambda (cell) (cons (cdr cell) (car cell))) alist)) + (comment (alist->plist @@ -427,6 +431,17 @@ not be prompted") (id . :id) (status . :status))))))) +(defun org-clubhouse-workflow-state-id-to-todo-keyword (workflow-state-id) + "Convert the named clubhouse WORKFLOW-STATE-ID to an org todo keyword." + (let* ((state-name (alist-get-equal + workflow-state-id + (invert-alist (org-clubhouse-workflow-states)))) + (inv-state-name-alist + (-map (lambda (cell) (cons (cdr cell) (car cell))) + org-clubhouse-state-alist))) + (or (alist-get-equal state-name inv-state-name-alist) + (s-upcase state-name)))) + ;;; ;;; Prompting ;;; @@ -804,7 +819,7 @@ resulting stories at headline level LEVEL." (insert (mapconcat (lambda (story) (format - "%s TODO %s + "%s %s %s :PROPERTIES: :clubhouse-id: %s :END: @@ -813,6 +828,8 @@ resulting stories at headline level LEVEL." :END: " (make-string level ?*) + (org-clubhouse-workflow-state-id-to-todo-keyword + (alist-get 'workflow_state_id story)) (alist-get 'name story) (let ((story-id (alist-get 'id story))) (org-make-link-string @@ -832,6 +849,5 @@ linked ticket in Clubhouse." nil t)) - (provide 'org-clubhouse) ;;; org-clubhouse.el ends here -- cgit 1.4.1 From 205e4fcde6e7c94e071760e6190444517c2455b6 Mon Sep 17 00:00:00 2001 From: Griffin Smith Date: Mon, 18 Feb 2019 14:58:38 -0500 Subject: feat: Allow pulling stories by story ID Adds org-clubhouse-headline-from-story, which allows passing a single story ID to make a headline from. Fixes #14 --- org-clubhouse.el | 55 ++++++++++++++++++++++++++++++++++--------------------- 1 file changed, 34 insertions(+), 21 deletions(-) diff --git a/org-clubhouse.el b/org-clubhouse.el index 34f51d24b9..125be37fae 100644 --- a/org-clubhouse.el +++ b/org-clubhouse.el @@ -799,6 +799,37 @@ contents of a drawer inside the element called DESCRIPTION, if any." :description new-description) (message "Successfully updated story description"))) + +(defun org-clubhouse--story-to-headline-text (story) + (let ((story-id (alist-get 'id story))) + (format + "%s %s %s +:PROPERTIES: +:clubhouse-id: %s +:END: +:DESCRIPTION: +%s +:END: +" + (make-string level ?*) + (org-clubhouse-workflow-state-id-to-todo-keyword + (alist-get 'workflow_state_id story)) + (alist-get 'name story) + (org-make-link-string + (org-clubhouse-link-to-story story-id) + (number-to-string story-id)) + (alist-get 'description story)))) + +(defun org-clubhouse-headline-from-story (level story-id) + "Create a single `org-mode' headline at LEVEL based on the given clubhouse STORY-ID." + + (interactive "*nLevel: \nnStory ID: ") + (let* ((story (org-clubhouse-request "GET" (format "/stories/%d" story-id)))) + (if (equal '((message . "Resource not found.")) story) + (message "Story ID not found: %d" story-id) + (save-mark-and-excursion + (insert (org-clubhouse--story-to-headline-text story)))))) + (defun org-clubhouse-headlines-from-query (level query) "Create `org-mode' headlines from a clubhouse query. @@ -816,27 +847,8 @@ resulting stories at headline level LEVEL." (if (null sprint-story-list) (message "Query returned no stories: %s" query) (save-mark-and-excursion - (insert - (mapconcat (lambda (story) - (format - "%s %s %s -:PROPERTIES: -:clubhouse-id: %s -:END: -:DESCRIPTION: -%s -:END: -" - (make-string level ?*) - (org-clubhouse-workflow-state-id-to-todo-keyword - (alist-get 'workflow_state_id story)) - (alist-get 'name story) - (let ((story-id (alist-get 'id story))) - (org-make-link-string - (org-clubhouse-link-to-story story-id) - (number-to-string story-id))) - (alist-get 'description story))) - (reject-archived sprint-story-list) "\n")))))) + (insert (mapconcat #'org-clubhouse--story-to-headline-text + (reject-archived sprint-story-list) "\n")))))) (define-minor-mode org-clubhouse-mode "If enabled, updates to the todo keywords on org headlines will update the @@ -850,4 +862,5 @@ linked ticket in Clubhouse." t)) (provide 'org-clubhouse) + ;;; org-clubhouse.el ends here -- cgit 1.4.1 From 910a6af62700aa5ecc9b7da73ad41bcd1fb451c9 Mon Sep 17 00:00:00 2001 From: Griffin Smith Date: Mon, 18 Feb 2019 16:26:49 -0500 Subject: feat: Implement org-clubhouse-link Implement an interactive function for linking existing org headlines with existing clubhouse stories. --- org-clubhouse.el | 55 ++++++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 48 insertions(+), 7 deletions(-) diff --git a/org-clubhouse.el b/org-clubhouse.el index 125be37fae..f11e938f6d 100644 --- a/org-clubhouse.el +++ b/org-clubhouse.el @@ -799,6 +799,9 @@ contents of a drawer inside the element called DESCRIPTION, if any." :description new-description) (message "Successfully updated story description"))) +;;; +;;; Creating headlines from existing stories +;;; (defun org-clubhouse--story-to-headline-text (story) (let ((story-id (alist-get 'id story))) @@ -830,6 +833,13 @@ contents of a drawer inside the element called DESCRIPTION, if any." (save-mark-and-excursion (insert (org-clubhouse--story-to-headline-text story)))))) +(defun org-clubhouse--search-stories (query) + (unless (string= "" query) + (-> (org-clubhouse-request "GET" "search/stories" :params `((query ,query))) + cdadr + (append nil) + reject-archived))) + (defun org-clubhouse-headlines-from-query (level query) "Create `org-mode' headlines from a clubhouse query. @@ -837,19 +847,50 @@ Submits QUERY to clubhouse, and creates `org-mode' headlines from all the resulting stories at headline level LEVEL." (interactive "*nLevel: \nMQuery: ") - (let* ((sprint-stories - (org-clubhouse-request - "GET" - "search/stories" - :params `((query ,query)))) - (sprint-story-list (-> sprint-stories cdr car cdr (append nil) - reject-archived))) + (let* ((story-list (org-clubhouse--search-stories query))) (if (null sprint-story-list) (message "Query returned no stories: %s" query) (save-mark-and-excursion (insert (mapconcat #'org-clubhouse--story-to-headline-text (reject-archived sprint-story-list) "\n")))))) +(defun org-clubhouse-prompt-for-story (cb) + "Prompt the user for a clubhouse story, then call CB with the full story." + (ivy-read "Story title: " + (lambda (search-term) + (let* ((stories (org-clubhouse--search-stories + (if search-term (format "\"%s\"" search-term) + "")))) + (-map (lambda (story) + (propertize (alist-get 'name story) 'story story)) + stories))) + :dynamic-collection t + :history 'org-clubhouse-story-prompt + :action (lambda (s) (funcall cb (get-text-property 0 'story s))) + :require-match t)) + +(defun org-clubhouse-link () + "Link the current `org-mode' headline with an existing clubhouse story." + (interactive) + (org-clubhouse-prompt-for-story + (lambda (story) + (org-clubhouse-populate-created-story (org-element-find-headline) story) + (org-todo + (org-clubhouse-workflow-state-id-to-todo-keyword + (alist-get 'workflow_state_id story)))))) + +(comment + (org-clubhouse--search-stories "train") + (org-clubhouse-request "GET" "search/stories" :params `((query ,""))) + + (get-text-property + 0 'clubhouse-id + (propertize "foo" 'clubhouse-id 1234)) + + ) + +;;; + (define-minor-mode org-clubhouse-mode "If enabled, updates to the todo keywords on org headlines will update the linked ticket in Clubhouse." -- cgit 1.4.1 From 813710f2610a2c7ec2d26da29f5b9e4621edf5b0 Mon Sep 17 00:00:00 2001 From: Griffin Smith Date: Mon, 18 Feb 2019 16:31:05 -0500 Subject: feat: Add original clubhouse story name to props When linking clubhouse stories to headlines, save the name of the clubhouse story as a property on the headline - useful for reference! --- org-clubhouse.el | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/org-clubhouse.el b/org-clubhouse.el index f11e938f6d..d6db3bf185 100644 --- a/org-clubhouse.el +++ b/org-clubhouse.el @@ -588,7 +588,7 @@ If the epics already have a CLUBHOUSE-EPIC-ID, they are filtered and ignored." :data (json-encode params)))) -(defun org-clubhouse-populate-created-story (elt story) +(cl-defun org-clubhouse-populate-created-story (elt story &key extra-properties) (let ((elt-start (plist-get elt :begin)) (story-id (alist-get 'id story)) (epic-id (alist-get 'epic_id story)) @@ -616,6 +616,10 @@ If the epics already have a CLUBHOUSE-EPIC-ID, they are filtered and ignored." (org-set-property "story-type" (alist-get-equal story-type org-clubhouse-story-types)) + (dolist (extra-prop extra-properties) + (org-set-property (car extra-prop) + (alist-get (cdr extra-prop) story))) + (org-todo "TODO")))) (defun org-clubhouse-create-story (&optional beg end &key then) @@ -874,7 +878,10 @@ resulting stories at headline level LEVEL." (interactive) (org-clubhouse-prompt-for-story (lambda (story) - (org-clubhouse-populate-created-story (org-element-find-headline) story) + (org-clubhouse-populate-created-story + (org-element-find-headline) + story + :extra-properties '(("clubhouse-story-name" . name))) (org-todo (org-clubhouse-workflow-state-id-to-todo-keyword (alist-get 'workflow_state_id story)))))) -- cgit 1.4.1 From 750b547327e450e3afcbd9883a5f8cb2480215be Mon Sep 17 00:00:00 2001 From: Griffin Smith Date: Mon, 18 Feb 2019 16:32:39 -0500 Subject: fix: Correct variable reference oops --- org-clubhouse.el | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/org-clubhouse.el b/org-clubhouse.el index d6db3bf185..cb878a4d72 100644 --- a/org-clubhouse.el +++ b/org-clubhouse.el @@ -852,11 +852,11 @@ resulting stories at headline level LEVEL." (interactive "*nLevel: \nMQuery: ") (let* ((story-list (org-clubhouse--search-stories query))) - (if (null sprint-story-list) + (if (null story-list) (message "Query returned no stories: %s" query) (save-mark-and-excursion (insert (mapconcat #'org-clubhouse--story-to-headline-text - (reject-archived sprint-story-list) "\n")))))) + (reject-archived story-list) "\n")))))) (defun org-clubhouse-prompt-for-story (cb) "Prompt the user for a clubhouse story, then call CB with the full story." -- cgit 1.4.1 From dfc2335edb652a1be938643aea2b7f1bb20b930a Mon Sep 17 00:00:00 2001 From: Griffin Smith Date: Fri, 22 Feb 2019 16:13:54 -0500 Subject: fix: Repair push-task-list Make all the commented-out stuff in push-task-list work properly --- org-clubhouse.el | 52 ++++++++++++++++++++++++++-------------------------- 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/org-clubhouse.el b/org-clubhouse.el index cb878a4d72..8163fa3d34 100644 --- a/org-clubhouse.el +++ b/org-clubhouse.el @@ -692,39 +692,39 @@ children of that headline into tasks in the task list of the story." :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. + "Writes each child of the element at point as a task list item. -when called as (org-clubhouse-push-task-list PARENT-CLUBHOUSE-ID CHILD-ELTS), +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)) + (story (org-clubhouse-get-story parent-clubhouse-id)) + (existing-tasks (alist-get 'tasks story)) + (task-exists + (lambda (task-name) + (cl-some (lambda (task) + (string-equal task-name (alist-get 'description task))) + existing-tasks))) + (elts-with-starts + (-map (lambda (e) (cons (set-marker (make-marker) + (plist-get e :begin)) + e)) + child-elts))) + (dolist (child-elt-and-start elts-with-starts) + (let* ((start (car child-elt-and-start)) + (child-elt (cdr child-elt-and-start)) + (task-name (plist-get child-elt :title))) + (unless (funcall task-exists task-name) + (let ((task (org-clubhouse-create-task + task-name + :story-id parent-clubhouse-id))) + (org-clubhouse-populate-created-task child-elt task start))))))) + +(defun org-clubhouse-populate-created-task (elt task &optional begin) + (let ((elt-start (or begin (plist-get elt :begin))) (task-id (alist-get 'id task)) (story-id (alist-get 'story_id task))) -- cgit 1.4.1 From f657a4c7fa88413c6121062ec38256353e4ad422 Mon Sep 17 00:00:00 2001 From: Griffin Smith Date: Fri, 22 Feb 2019 16:29:06 -0500 Subject: feat: Update task list item statuses Add a clause to org-clubhouse-update-status to update task headline statuses in addition to story statuses --- org-clubhouse.el | 64 +++++++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 50 insertions(+), 14 deletions(-) diff --git a/org-clubhouse.el b/org-clubhouse.el index 8163fa3d34..c3f69f32e8 100644 --- a/org-clubhouse.el +++ b/org-clubhouse.el @@ -197,8 +197,8 @@ not be prompted") (when (equal 'headline (car current-elt)) (cadr current-elt)))) -(defun org-element-extract-clubhouse-id (elt) - (when-let* ((clubhouse-id-link (plist-get elt :CLUBHOUSE-ID))) +(defun org-element-extract-clubhouse-id (elt &optional property) + (when-let* ((clubhouse-id-link (plist-get elt (or property :CLUBHOUSE-ID)))) (cond ((string-match (rx "[[" (one-or-more anything) "]" @@ -740,6 +740,21 @@ allows manually passing a clubhouse ID and list of org-element plists to write" (org-todo "TODO")))) +;;; +;;; Task Updates +;;; + +(cl-defun org-clubhouse-update-task-internal + (story-id task-id &rest attrs) + (cl-assert (and (integerp story-id) + (integerp task-id) + (listp attrs))) + (org-clubhouse-request + "PUT" + (format "stories/%d" story-id) + :data + (json-encode attrs))) + ;;; ;;; Story updates ;;; @@ -777,18 +792,39 @@ Update the status of the Clubhouse story linked to the current element with the entry in `org-clubhouse-state-alist' corresponding to the todo-keyword of the element." (interactive) - (when-let* ((clubhouse-id (org-element-clubhouse-id))) - (let* ((elt (org-element-find-headline)) - (todo-keyword (-> elt (plist-get :todo-keyword) (substring-no-properties)))) - (when-let* ((clubhouse-workflow-state - (alist-get-equal todo-keyword org-clubhouse-state-alist)) - (workflow-state-id - (alist-get-equal clubhouse-workflow-state (org-clubhouse-workflow-states)))) - (org-clubhouse-update-story-internal - clubhouse-id - :workflow_state_id workflow-state-id) - (message "Successfully updated clubhouse status to \"%s\"" - clubhouse-workflow-state))))) + (let* ((elt (org-element-find-headline)) + (todo-keyword (-> elt + (plist-get :todo-keyword) + (substring-no-properties))) + + (clubhouse-id (org-element-extract-clubhouse-id elt)) + (task-id (plist-get elt :CLUBHOUSE-TASK-ID))) + (cond + (clubhouse-id + (let* ((todo-keyword (-> elt + (plist-get :todo-keyword) + (substring-no-properties)))) + (when-let* ((clubhouse-workflow-state + (alist-get-equal todo-keyword org-clubhouse-state-alist)) + (workflow-state-id + (alist-get-equal clubhouse-workflow-state + (org-clubhouse-workflow-states)))) + (org-clubhouse-update-story-internal + clubhouse-id + :workflow_state_id workflow-state-id) + (message "Successfully updated clubhouse status to \"%s\"" + clubhouse-workflow-state)))) + (task-id + (let ((story-id (org-element-extract-clubhouse-id + elt + :CLUBHOUSE-STORY-ID)) + (done? (member todo-keyword org-done-keywords))) + (org-clubhouse-update-task-internal + story-id + (string-to-number task-id) + :done done?) + (message "Successfully marked clubhouse task status as %s" + (if done? "complete" "incomplete"))))))) (defun org-clubhouse-update-description () "Update the description of the Clubhouse story linked to the current element. -- cgit 1.4.1 From 3ee1fcc71c85fe7270b70c14b89078cef212b524 Mon Sep 17 00:00:00 2001 From: Griffin Smith Date: Wed, 6 Mar 2019 11:58:50 -0500 Subject: fix: Correct clubhouse API for task status - Use the right URL for updating tasks - Use the right key and value for marking task status --- org-clubhouse.el | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/org-clubhouse.el b/org-clubhouse.el index c3f69f32e8..808ce7fd5f 100644 --- a/org-clubhouse.el +++ b/org-clubhouse.el @@ -751,7 +751,7 @@ allows manually passing a clubhouse ID and list of org-element plists to write" (listp attrs))) (org-clubhouse-request "PUT" - (format "stories/%d" story-id) + (format "stories/%d/tasks/%d" story-id task-id) :data (json-encode attrs))) @@ -822,7 +822,7 @@ element." (org-clubhouse-update-task-internal story-id (string-to-number task-id) - :done done?) + :complete (if done? 't :json-false)) (message "Successfully marked clubhouse task status as %s" (if done? "complete" "incomplete"))))))) -- cgit 1.4.1 From 9b9254123942126e4f2ce40253e60f6847f96c5b Mon Sep 17 00:00:00 2001 From: Griffin Smith Date: Wed, 6 Mar 2019 13:55:55 -0500 Subject: feat: Allow updating story assignee Add a configuration parameter, `org-clubhouse-claim-story-on-status-update`, which allows updating the assignee of stories on status update, either always or for specific todo keywords --- org-clubhouse.el | 64 +++++++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 56 insertions(+), 8 deletions(-) diff --git a/org-clubhouse.el b/org-clubhouse.el index 808ce7fd5f..ee741b434d 100644 --- a/org-clubhouse.el +++ b/org-clubhouse.el @@ -47,10 +47,17 @@ ;;; (defvar org-clubhouse-auth-token nil - "Authorization token for the Clubhouse API") + "Authorization token for the Clubhouse API.") + +(defvar org-clubhouse-username nil + "Username for the current Clubhouse user. + +Unfortunately, the Clubhouse API doesn't seem to provide this via the API given +an API token, so we need to configure this for +`org-clubhouse-claim-story-on-status-updates' to work") (defvar org-clubhouse-team-name nil - "Team name to use in links to Clubhouse + "Team name to use in links to Clubhouse. ie https://app.clubhouse.io//stories") (defvar org-clubhouse-project-ids nil @@ -91,7 +98,19 @@ not be prompted") ("prompt" . "**Prompt each time (do not set a default story type)**"))) (defvar org-clubhouse-default-state "Proposed" - "Default state to create all new stories in") + "Default state to create all new stories in.") + +(defvar org-clubhouse-claim-story-on-status-update 't + "Controls the assignee behavior of stories on status update. + +If set to 't, will mark the current user as the owner of any clubhouse +stories on any update to the status. + +If set to nil, will never automatically update the assignee of clubhouse +stories. + +If set to a list of todo-state's, will mark the current user as the owner of +clubhouse stories whenever updating the status to one of those todo states.") ;;; ;;; Utilities @@ -410,6 +429,19 @@ not be prompted") 'name 'id))) +(defcache org-clubhouse-whoami + "Returns the ID of the logged in user" + (->> (org-clubhouse-request + "GET" + "/members") + ->list + (find-if (lambda (m) + (->> m + (alist-get 'profile) + (alist-get 'mention_name) + (equal org-clubhouse-username)))) + (alist-get 'id))) + (defun org-clubhouse-stories-in-project (project-id) "Return the stories in the given PROJECT-ID as org headlines." (let ((resp-json (org-clubhouse-request "GET" (format "/projects/%d/stories" project-id)))) @@ -809,11 +841,27 @@ element." (workflow-state-id (alist-get-equal clubhouse-workflow-state (org-clubhouse-workflow-states)))) - (org-clubhouse-update-story-internal - clubhouse-id - :workflow_state_id workflow-state-id) - (message "Successfully updated clubhouse status to \"%s\"" - clubhouse-workflow-state)))) + (let ((update-assignee? + (if (or (eq 't org-clubhouse-claim-story-on-status-update) + (member todo-keyword + org-clubhouse-claim-story-on-status-update)) + (if org-clubhouse-username + 't + (warn "Not claiming story since `org-clubhouse-username' + is not set") + nil)))) + + (org-clubhouse-update-story-internal + clubhouse-id + :workflow_state_id workflow-state-id + :owner_ids (when update-assignee? + (list (org-clubhouse-whoami)))) + (message + (if update-assignee? + "Successfully claimed story and updated clubhouse status to \"%s\"" + "Successfully updated clubhouse status to \"%s\"") + clubhouse-workflow-state))))) + (task-id (let ((story-id (org-element-extract-clubhouse-id elt -- cgit 1.4.1 From 453e6dc36c65215f8227d395ee7529735577fe29 Mon Sep 17 00:00:00 2001 From: Griffin Smith Date: Fri, 8 Mar 2019 10:42:25 -0500 Subject: feat: Add org-clubhouse-claim Add a standalone org-clubhouse-claim function for claiming the current story without making any other updates --- README.org | 10 +++++++--- org-clubhouse.el | 61 ++++++++++++++++++++++++++++++++++++-------------------- 2 files changed, 46 insertions(+), 25 deletions(-) diff --git a/README.org b/README.org index f312b02a74..423dfc21ae 100644 --- a/README.org +++ b/README.org @@ -1,4 +1,4 @@ -* Org-Clubhouse +#+TITLE: Org-Clubhouse Simple, unopinionated integration between Emacs's [[https://orgmode.org/][org-mode]] and the [[https://clubhouse.io/][Clubhouse]] issue tracker @@ -34,11 +34,12 @@ Simple, unopinionated integration between Emacs's [[https://orgmode.org/][org-mo * Setup -Once installed, you'll need to set two global config vars: +Once installed, you'll need to set three global config vars: #+BEGIN_SRC emacs-lisp (setq org-clubhouse-auth-token "" - org-clubhouse-team-name "") + org-clubhouse-team-name "" + org-clubhouse-username "") #+END_SRC You can generate a new personal API token by going to the "API Tokens" tab on @@ -79,6 +80,9 @@ org-clubhouse provides the following commands: Create org-mode headlines from a clubhouse query at the cursor's current position, prompting for the headline indentation level and clubhouse query text +- ~org-clubhouse-claim~ + Adds the user configured in ~org-clubhouse-username~ as the owner of the + clubhouse story associated with the headline at point * Configuration diff --git a/org-clubhouse.el b/org-clubhouse.el index ee741b434d..7bab1ee2d0 100644 --- a/org-clubhouse.el +++ b/org-clubhouse.el @@ -791,22 +791,6 @@ allows manually passing a clubhouse ID and list of org-element plists to write" ;;; Story updates ;;; -(defun org-clubhouse-update-story-title () - "Update the title of the Clubhouse story linked to the current headline. - -Update the title of the story linked to the current headline with the text of -the headline." - (interactive) - - (when-let (clubhouse-id (org-element-clubhouse-id)) - (let* ((elt (org-element-find-headline)) - (title (plist-get elt :title))) - (org-clubhouse-update-story-internal - clubhouse-id - :name title) - (message "Successfully updated story title to \"%s\"" - title)))) - (cl-defun org-clubhouse-update-story-internal (story-id &rest attrs) (cl-assert (and (integerp story-id) @@ -817,6 +801,29 @@ the headline." :data (json-encode attrs))) +(cl-defun org-clubhouse-update-story-at-point (&rest attrs) + (when-let* ((clubhouse-id (org-element-clubhouse-id))) + (apply + #'org-clubhouse-update-story-internal + (cons clubhouse-id attrs)) + t)) + +(defun org-clubhouse-update-story-title () + "Update the title of the Clubhouse story linked to the current headline. + +Update the title of the story linked to the current headline with the text of +the headline." + (interactive) + + (let* ((elt (org-element-find-headline)) + (title (plist-get elt :title))) + (and + (org-clubhouse-update-story-at-point + clubhouse-id + :name title) + (message "Successfully updated story title to \"%s\"" + title)))) + (defun org-clubhouse-update-status () "Update the status of the Clubhouse story linked to the current element. @@ -880,12 +887,12 @@ element." Update the status of the Clubhouse story linked to the current element with the contents of a drawer inside the element called DESCRIPTION, if any." (interactive) - (when-let* ((clubhouse-id (org-element-clubhouse-id)) - (new-description (org-clubhouse-find-description-drawer))) - (org-clubhouse-update-story-internal - clubhouse-id - :description new-description) - (message "Successfully updated story description"))) + (when-let* ((new-description (org-clubhouse-find-description-drawer))) + (and + (org-clubhouse-update-story-at-point + clubhouse-id + :description new-description) + (message "Successfully updated story description")))) ;;; ;;; Creating headlines from existing stories @@ -970,6 +977,16 @@ resulting stories at headline level LEVEL." (org-clubhouse-workflow-state-id-to-todo-keyword (alist-get 'workflow_state_id story)))))) +(defun org-clubhouse-claim () + "Assign the clubhouse story associated with the headline at point to yourself." + (interactive) + (if org-clubhouse-username + (and + (org-clubhouse-update-story-at-point + :owner_ids (list (org-clubhouse-whoami))) + (message "Successfully claimed story")) + (warn "Can't claim story if `org-clubhouse-username' is unset"))) + (comment (org-clubhouse--search-stories "train") (org-clubhouse-request "GET" "search/stories" :params `((query ,""))) -- cgit 1.4.1 From d1f8a1e41f899add01b2f6800ba9e2621f51080f Mon Sep 17 00:00:00 2001 From: Griffin Smith Date: Thu, 14 Mar 2019 16:21:29 -0400 Subject: feat: Implement org-clubhouse-sync-status Implement a command that pulls down the status from clubhouse for a list of story headlines, and updates the todo keyword accordingly --- org-clubhouse.el | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/org-clubhouse.el b/org-clubhouse.el index 7bab1ee2d0..568a476e0a 100644 --- a/org-clubhouse.el +++ b/org-clubhouse.el @@ -987,6 +987,30 @@ resulting stories at headline level LEVEL." (message "Successfully claimed story")) (warn "Can't claim story if `org-clubhouse-username' is unset"))) +(defun org-clubhouse-sync-status (&optional beg end) + "Pull the status(es) for the story(ies) in region and update the todo state. + +Uses `org-clubhouse-state-alist'. Operates over stories from BEG to END" + (interactive + (when (use-region-p) + (list (region-beginning) (region-end)))) + (let ((elts (-filter (lambda (e) (plist-get e :CLUBHOUSE-ID)) + (org-clubhouse-collect-headlines beg end)))) + (save-mark-and-excursion + (dolist (e elts) + (goto-char (plist-get e :begin)) + (let* ((clubhouse-id (org-element-extract-clubhouse-id e)) + (story (org-clubhouse-get-story clubhouse-id)) + (workflow-state-id (alist-get 'workflow_state_id story)) + (todo-keyword (org-clubhouse-workflow-state-id-to-todo-keyword + workflow-state-id))) + (let ((org-after-todo-state-change-hook + (remove 'org-clubhouse-update-status + org-after-todo-state-change-hook))) + (org-todo todo-keyword))))) + (message "Successfully synchronized status of %d stories from Clubhouse" + (length elts)))) + (comment (org-clubhouse--search-stories "train") (org-clubhouse-request "GET" "search/stories" :params `((query ,""))) -- cgit 1.4.1 From d29c5c0df6b58b8f0490dc94c9705e3363e7d65e Mon Sep 17 00:00:00 2001 From: Griffin Smith Date: Tue, 26 Mar 2019 12:25:40 -0400 Subject: feat: Pull down task lists when pulling stories When pulling stories from clubhouse by any method (either headline-from-story or headlines-from-query) also pull down the list of tasks on the story as children of the headline. Fixes #15 --- org-clubhouse.el | 58 +++++++++++++++++++++++++++++++++++++------------------- 1 file changed, 39 insertions(+), 19 deletions(-) diff --git a/org-clubhouse.el b/org-clubhouse.el index 568a476e0a..2feff4dcf2 100644 --- a/org-clubhouse.el +++ b/org-clubhouse.el @@ -898,16 +898,29 @@ contents of a drawer inside the element called DESCRIPTION, if any." ;;; Creating headlines from existing stories ;;; -(defun org-clubhouse--story-to-headline-text (story) +(defun org-clubhouse--task-to-headline-text (level task) + (format "%s %s %s +:PROPERTIES: +:clubhouse-task-id: %s +:clubhouse-story-id: %s +:END:" + (make-string level ?*) + (if (equal :json-false (alist-get 'complete task)) + "TODO" "DONE") + (alist-get 'description task) + (alist-get 'id task) + (org-clubhouse-link-to-story + (alist-get 'story_id task)))) + +(defun org-clubhouse--story-to-headline-text (level story) (let ((story-id (alist-get 'id story))) (format "%s %s %s :PROPERTIES: :clubhouse-id: %s :END: -:DESCRIPTION: %s -:END: +%s " (make-string level ?*) (org-clubhouse-workflow-state-id-to-todo-keyword @@ -916,17 +929,30 @@ contents of a drawer inside the element called DESCRIPTION, if any." (org-make-link-string (org-clubhouse-link-to-story story-id) (number-to-string story-id)) - (alist-get 'description story)))) + (let ((desc (alist-get 'description story))) + (if (= 0 (length desc)) "" + (format ":DESCRIPTION:\n%s\n:END:" desc))) + (if-let ((tasks (seq-sort-by + (apply-partially #'alist-get 'position) + #'< + (or (alist-get 'tasks story) + (alist-get 'tasks + (org-clubhouse-get-story story-id)))))) + (mapconcat (apply-partially #'org-clubhouse--task-to-headline-text + (inc level)) + tasks + "\n") + "")))) (defun org-clubhouse-headline-from-story (level story-id) "Create a single `org-mode' headline at LEVEL based on the given clubhouse STORY-ID." (interactive "*nLevel: \nnStory ID: ") - (let* ((story (org-clubhouse-request "GET" (format "/stories/%d" story-id)))) + (let* ((story (org-clubhouse-get-story story-id))) (if (equal '((message . "Resource not found.")) story) (message "Story ID not found: %d" story-id) (save-mark-and-excursion - (insert (org-clubhouse--story-to-headline-text story)))))) + (insert (org-clubhouse--story-to-headline-text level story)))))) (defun org-clubhouse--search-stories (query) (unless (string= "" query) @@ -945,9 +971,13 @@ resulting stories at headline level LEVEL." (let* ((story-list (org-clubhouse--search-stories query))) (if (null story-list) (message "Query returned no stories: %s" query) - (save-mark-and-excursion - (insert (mapconcat #'org-clubhouse--story-to-headline-text - (reject-archived story-list) "\n")))))) + (let ((text (mapconcat (apply-partially + #'org-clubhouse--story-to-headline-text + level) + (reject-archived story-list) "\n"))) + (if (called-interactively-p) + (save-mark-and-excursion (insert text)) + text))))) (defun org-clubhouse-prompt-for-story (cb) "Prompt the user for a clubhouse story, then call CB with the full story." @@ -1011,16 +1041,6 @@ Uses `org-clubhouse-state-alist'. Operates over stories from BEG to END" (message "Successfully synchronized status of %d stories from Clubhouse" (length elts)))) -(comment - (org-clubhouse--search-stories "train") - (org-clubhouse-request "GET" "search/stories" :params `((query ,""))) - - (get-text-property - 0 'clubhouse-id - (propertize "foo" 'clubhouse-id 1234)) - - ) - ;;; (define-minor-mode org-clubhouse-mode -- cgit 1.4.1 From 7ce8b48fd5340d51f5bb6131ec01dce9b1b5ec61 Mon Sep 17 00:00:00 2001 From: Griffin Smith Date: Thu, 28 Mar 2019 11:27:02 -0400 Subject: fix: Put task story-ids in a format we can read Make task headline story-ids links just like the IDs for the story headlines are, so that they can later be read by org-clubhouse-extract-story-id - this fixes task status updating, which was broken. --- org-clubhouse.el | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/org-clubhouse.el b/org-clubhouse.el index 2feff4dcf2..c2f38d8c23 100644 --- a/org-clubhouse.el +++ b/org-clubhouse.el @@ -909,8 +909,10 @@ contents of a drawer inside the element called DESCRIPTION, if any." "TODO" "DONE") (alist-get 'description task) (alist-get 'id task) - (org-clubhouse-link-to-story - (alist-get 'story_id task)))) + (let ((story-id (alist-get 'story_id task))) + (org-make-link-string + (org-clubhouse-link-to-story story-id) + story-id)))) (defun org-clubhouse--story-to-headline-text (level story) (let ((story-id (alist-get 'id story))) -- cgit 1.4.1 From f8bab5f8df777d836bcf66b2d3df749e8ae995de Mon Sep 17 00:00:00 2001 From: Griffin Smith Date: Thu, 11 Apr 2019 11:13:00 -0400 Subject: fix: Correct arguments in update-story-description Dunno what happened here or when, but update-story-at-point doesn't take this argument and also the variable doesn't even exist --- org-clubhouse.el | 1 - 1 file changed, 1 deletion(-) diff --git a/org-clubhouse.el b/org-clubhouse.el index c2f38d8c23..21158f1941 100644 --- a/org-clubhouse.el +++ b/org-clubhouse.el @@ -890,7 +890,6 @@ contents of a drawer inside the element called DESCRIPTION, if any." (when-let* ((new-description (org-clubhouse-find-description-drawer))) (and (org-clubhouse-update-story-at-point - clubhouse-id :description new-description) (message "Successfully updated story description")))) -- cgit 1.4.1 From f88deb2a331a071de6e7d83d6f9752ffd5b5a2aa Mon Sep 17 00:00:00 2001 From: Griffin Smith Date: Tue, 23 Apr 2019 14:32:25 -0400 Subject: feat: Allow creating stories with labels Allow (configurably) creating stories with Clubhouse labels based on Org tags, either creating all labels or only using labels that already exist in Clubhouse Upcoming should be an `org-clubhouse-update-story-labels` command. --- org-clubhouse.el | 37 ++++++++++++++++++++++++++++++++++--- 1 file changed, 34 insertions(+), 3 deletions(-) diff --git a/org-clubhouse.el b/org-clubhouse.el index 21158f1941..2547e136ca 100644 --- a/org-clubhouse.el +++ b/org-clubhouse.el @@ -112,6 +112,17 @@ stories. If set to a list of todo-state's, will mark the current user as the owner of clubhouse stories whenever updating the status to one of those todo states.") +(defvar org-clubhouse-create-stories-with-labels nil + "Controls the way org-clubhouse creates stories with labels based on org tags. + +If set to 't, will create labels for all org tags on headlines when stories are +created. + +If set to 'existing, will set labels on created stories only if the label +already exists in clubhouse + +If set to nil, will never create stories with labels") + ;;; ;;; Utilities ;;; @@ -282,6 +293,17 @@ clubhouse stories whenever updating the status to one of those todo states.") drawer-pos)) (org-clubhouse-find-description-drawer))))))) +(defun org-clubhouse--labels-for-elt (elt) + "Return the Clubhouse labels based on the tags of ELT and the user's config." + (unless (eq nil org-clubhouse-create-stories-with-labels) + (let ((tags (org-get-tags (plist-get elt :contents-begin)))) + (cl-case org-clubhouse-create-stories-with-labels + ('t tags) + ('existing (-filter (lambda (tag) (-some (lambda (l) + (string-equal tag (cdr l))) + (org-clubhouse-labels))) + tags)))))) + ;;; ;;; API integration ;;; @@ -429,6 +451,10 @@ clubhouse stories whenever updating the status to one of those todo states.") 'name 'id))) +(defcache org-clubhouse-labels + "Returns labels as (label-id . name)" + (org-clubhouse-fetch-as-id-name-pairs "labels")) + (defcache org-clubhouse-whoami "Returns the ID of the logged in user" (->> (org-clubhouse-request @@ -599,7 +625,7 @@ If the epics already have a CLUBHOUSE-EPIC-ID, they are filtered and ignored." (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 description) + (title &key project-id epic-id story-type description labels) (cl-assert (and (stringp title) (integerp project-id) (or (null epic-id) (integerp epic-id)) @@ -609,7 +635,8 @@ If the epics already have a CLUBHOUSE-EPIC-ID, they are filtered and ignored." (project_id . ,project-id) (epic_id . ,epic-id) (story_type . ,story-type) - (description . ,(or description ""))))) + (description . ,(or description "")) + (labels . ,labels)))) (when workflow-state-id (push `(workflow_state_id . ,workflow-state-id) params)) @@ -682,12 +709,16 @@ If the stories already have a CLUBHOUSE-ID, they are filtered and ignored." (save-mark-and-excursion (goto-char (plist-get elt :begin)) (org-clubhouse-find-description-drawer))) + (labels (-map (lambda (l) `((name . ,l))) + (org-clubhouse--labels-for-elt + elt))) (story (org-clubhouse-create-story-internal title :project-id project-id :epic-id epic-id :story-type story-type - :description description))) + :description description + :labels labels))) (org-clubhouse-populate-created-story elt story) (when (functionp then) (funcall then story)))) -- cgit 1.4.1 From cfcc3465932e4669eddcfc55adcca81f5d6561bb Mon Sep 17 00:00:00 2001 From: Griffin Smith Date: Thu, 2 May 2019 10:52:07 -0400 Subject: feat: Add org-clubhouse-update-labels Add a command to update the labels of an existing story from the labels of an org headline --- org-clubhouse.el | 34 +++++++++++++++++++++++++--------- 1 file changed, 25 insertions(+), 9 deletions(-) diff --git a/org-clubhouse.el b/org-clubhouse.el index 2547e136ca..5d5b78f356 100644 --- a/org-clubhouse.el +++ b/org-clubhouse.el @@ -297,12 +297,13 @@ If set to nil, will never create stories with labels") "Return the Clubhouse labels based on the tags of ELT and the user's config." (unless (eq nil org-clubhouse-create-stories-with-labels) (let ((tags (org-get-tags (plist-get elt :contents-begin)))) - (cl-case org-clubhouse-create-stories-with-labels - ('t tags) - ('existing (-filter (lambda (tag) (-some (lambda (l) - (string-equal tag (cdr l))) - (org-clubhouse-labels))) - tags)))))) + (-map (lambda (l) `((name . ,l))) + (cl-case org-clubhouse-create-stories-with-labels + ('t tags) + ('existing (-filter (lambda (tag) (-some (lambda (l) + (string-equal tag (cdr l))) + (org-clubhouse-labels))) + tags))))))) ;;; ;;; API integration @@ -709,9 +710,7 @@ If the stories already have a CLUBHOUSE-ID, they are filtered and ignored." (save-mark-and-excursion (goto-char (plist-get elt :begin)) (org-clubhouse-find-description-drawer))) - (labels (-map (lambda (l) `((name . ,l))) - (org-clubhouse--labels-for-elt - elt))) + (labels (org-clubhouse--labels-for-elt elt)) (story (org-clubhouse-create-story-internal title :project-id project-id @@ -924,6 +923,23 @@ contents of a drawer inside the element called DESCRIPTION, if any." :description new-description) (message "Successfully updated story description")))) +(defun org-clubhouse-update-labels () + "Update the labels of the Clubhouse story linked to the current element. + +Will use the value of `org-clubhouse-create-stories-with-labels' to determine +which labels to set." + (interactive) + (when-let* ((elt (org-element-find-headline)) + (new-labels (org-clubhouse--labels-for-elt elt))) + (and + (org-clubhouse-update-story-at-point + :labels new-labels) + (message "Successfully updated story labels to :%s:" + (->> new-labels + (-map #'cdar) + (s-join ":")))))) + + ;;; ;;; Creating headlines from existing stories ;;; -- cgit 1.4.1 From 01c684396d8455fa68b058b365695bd54794391c Mon Sep 17 00:00:00 2001 From: Griffin Smith Date: Thu, 2 May 2019 11:00:25 -0400 Subject: feat: Write story labels when pulling stories Whenever we pull stories from clubhouse, write the labels of those stories as headline tags --- org-clubhouse.el | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/org-clubhouse.el b/org-clubhouse.el index 5d5b78f356..dd5e73ea82 100644 --- a/org-clubhouse.el +++ b/org-clubhouse.el @@ -963,7 +963,7 @@ which labels to set." (defun org-clubhouse--story-to-headline-text (level story) (let ((story-id (alist-get 'id story))) (format - "%s %s %s + "%s %s %s :%s: :PROPERTIES: :clubhouse-id: %s :END: @@ -974,6 +974,11 @@ which labels to set." (org-clubhouse-workflow-state-id-to-todo-keyword (alist-get 'workflow_state_id story)) (alist-get 'name story) + (->> story + (alist-get 'labels) + ->list + (-map (apply-partially #'alist-get 'name)) + (s-join ":")) (org-make-link-string (org-clubhouse-link-to-story story-id) (number-to-string story-id)) @@ -1000,7 +1005,8 @@ which labels to set." (if (equal '((message . "Resource not found.")) story) (message "Story ID not found: %d" story-id) (save-mark-and-excursion - (insert (org-clubhouse--story-to-headline-text level story)))))) + (insert (org-clubhouse--story-to-headline-text level story)) + (org-align-tags))))) (defun org-clubhouse--search-stories (query) (unless (string= "" query) @@ -1024,7 +1030,9 @@ resulting stories at headline level LEVEL." level) (reject-archived story-list) "\n"))) (if (called-interactively-p) - (save-mark-and-excursion (insert text)) + (save-mark-and-excursion + (insert text) + (org-align-all-tags)) text))))) (defun org-clubhouse-prompt-for-story (cb) -- cgit 1.4.1 From 1cd9f9f00655ad1f7e997ce7ff6e73643108de90 Mon Sep 17 00:00:00 2001 From: Griffin Smith Date: Thu, 2 May 2019 11:03:08 -0400 Subject: feat: make headline-from-story prompt for story Rename the previous org-clubhouse-headline-from-story to org-clubhouse-headline-from-story-*id*, and make -headline-from-story use prompt-for-story to allow autocompletion of the title of the story to pull down. --- org-clubhouse.el | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/org-clubhouse.el b/org-clubhouse.el index dd5e73ea82..e82cd48d02 100644 --- a/org-clubhouse.el +++ b/org-clubhouse.el @@ -997,9 +997,8 @@ which labels to set." "\n") "")))) -(defun org-clubhouse-headline-from-story (level story-id) +(defun org-clubhouse-headline-from-story-id (level story-id) "Create a single `org-mode' headline at LEVEL based on the given clubhouse STORY-ID." - (interactive "*nLevel: \nnStory ID: ") (let* ((story (org-clubhouse-get-story story-id))) (if (equal '((message . "Resource not found.")) story) @@ -1050,6 +1049,16 @@ resulting stories at headline level LEVEL." :action (lambda (s) (funcall cb (get-text-property 0 'story s))) :require-match t)) +(defun org-clubhouse-headline-from-story (level) + "Prompt for a story, and create an org headline at LEVEL from that story." + (interactive "*nLevel: ") + (org-clubhouse-prompt-for-story + (lambda (story) + (save-mark-and-excursion + (insert (org-clubhouse--story-to-headline-text level story)) + (org-align-tags))))) + + (defun org-clubhouse-link () "Link the current `org-mode' headline with an existing clubhouse story." (interactive) -- cgit 1.4.1 From 603f614c3515c12f7d3c60bf6a2bf3f86625d921 Mon Sep 17 00:00:00 2001 From: Griffin Smith Date: Mon, 20 May 2019 10:43:47 -0400 Subject: fix: Undefined function inc elisp calls this 1+, I had it sitting in my utils Fixes #16 --- org-clubhouse.el | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/org-clubhouse.el b/org-clubhouse.el index e82cd48d02..ce136a51f0 100644 --- a/org-clubhouse.el +++ b/org-clubhouse.el @@ -992,7 +992,7 @@ which labels to set." (alist-get 'tasks (org-clubhouse-get-story story-id)))))) (mapconcat (apply-partially #'org-clubhouse--task-to-headline-text - (inc level)) + (1+ level)) tasks "\n") "")))) -- cgit 1.4.1 From 9d83cb22a11ec8008c7005c65b8425b71f5206c8 Mon Sep 17 00:00:00 2001 From: Griffin Smith Date: Mon, 20 May 2019 10:49:24 -0400 Subject: fix: Infinite loop for last element in file org-element-and-children-at-point was comparing the current point <= end, but should've been <. Fixes #17 --- org-clubhouse.el | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/org-clubhouse.el b/org-clubhouse.el index ce136a51f0..4f162d955b 100644 --- a/org-clubhouse.el +++ b/org-clubhouse.el @@ -257,13 +257,14 @@ If set to nil, will never create stories with labels") (defun org-element-and-children-at-point () (let* ((elt (org-element-find-headline)) - (contents-begin (plist-get elt :contents-begin)) + (contents-begin (or (plist-get elt :contents-begin) + (plist-get elt :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) + (while (< (point) end) (let* ((next-elt (org-element-at-point)) (elt-type (car next-elt)) (elt (cadr next-elt))) -- cgit 1.4.1 From 96a3e08ff080203a68a1ec62f8f05542dfe5e9e0 Mon Sep 17 00:00:00 2001 From: Griffin Smith Date: Wed, 19 Jun 2019 10:34:06 -0400 Subject: Don't output colons without labels If we pull down a story without labels, don't format the :: for those labels --- org-clubhouse.el | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/org-clubhouse.el b/org-clubhouse.el index 4f162d955b..18509fa823 100644 --- a/org-clubhouse.el +++ b/org-clubhouse.el @@ -964,7 +964,7 @@ which labels to set." (defun org-clubhouse--story-to-headline-text (level story) (let ((story-id (alist-get 'id story))) (format - "%s %s %s :%s: + "%s %s %s %s :PROPERTIES: :clubhouse-id: %s :END: @@ -975,11 +975,12 @@ which labels to set." (org-clubhouse-workflow-state-id-to-todo-keyword (alist-get 'workflow_state_id story)) (alist-get 'name story) - (->> story - (alist-get 'labels) - ->list - (-map (apply-partially #'alist-get 'name)) - (s-join ":")) + (if-let ((labels (->> story + (alist-get 'labels) + ->list + (-map (apply-partially #'alist-get 'name))))) + (format ":%s:" (s-join ":" labels)) + "") (org-make-link-string (org-clubhouse-link-to-story story-id) (number-to-string story-id)) -- cgit 1.4.1 From 0a130d7ca7f03c3287bf86eba836ca3953cd27ac Mon Sep 17 00:00:00 2001 From: Griffin Smith Date: Thu, 5 Sep 2019 15:39:54 -0400 Subject: Add note about move and philosophy to README This is mostly so I have something to point people at for feature requests, etc. --- README.org | 28 +++++++++++++++++++++++++--- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/README.org b/README.org index 423dfc21ae..3138eeb8a7 100644 --- a/README.org +++ b/README.org @@ -1,8 +1,12 @@ #+TITLE: Org-Clubhouse -Simple, unopinionated integration between Emacs's [[https://orgmode.org/][org-mode]] and the [[https://clubhouse.io/][Clubhouse]] issue tracker +Simple, unopinionated integration between Emacs's [[https://orgmode.org/][org-mode]] and the [[https://clubhouse.io/][Clubhouse]] +issue tracker -* Install +(This used to be at urbint/org-clubhouse, by the way, but moved here as it's +more of a personal project than a company one) + +* Installation ** [[https://github.com/quelpa/quelpa][Quelpa]] @@ -32,6 +36,7 @@ Simple, unopinionated integration between Emacs's [[https://orgmode.org/][org-mo '((org-clubhouse :location (recipe :fetcher github :repo "urbint/org-clubhouse"))) #+END_SRC + * Setup Once installed, you'll need to set three global config vars: @@ -84,7 +89,24 @@ org-clubhouse provides the following commands: Adds the user configured in ~org-clubhouse-username~ as the owner of the clubhouse story associated with the headline at point +* Philosophy + +I use org-mode every single day to manage tasks, notes, literate programming, +etc. Part of what that means for me is that I already have a system for the +structure of my .org files, and I don't want to sacrifice that system for any +external tool. Updating statuses, ~org-clubhouse-create-story~, and +~org-clubhouse-headline-from-story~ are my bread and butter for that reason - +rather than having some sort of bidirectional sync that pulls down full lists of +all the stories in Clubhouse (or whatever issue tracker / project management +tool I'm using at the time). I can be in a mode where I'm taking meeting notes, +think of something that I need to do, make it a TODO headline, and make that +TODO headline a clubhouse story. That's the same reason for the DESCRIPTION +drawers rather than just sending the entire contents of a headline to +Clubhouse - I almost always want to write things like personal notes, literate +code, etc inside of the tasks I'm working on, and don't always want to share +that with Clubhouse. + * Configuration -Refer to the beginning of the [[https://github.com/urbint/org-clubhouse/blob/master/org-clubhouse.el][~org-clubhouse.el~]] file in this repository for +Refer to the beginning of the [[https://github.com/urbint/org-clubhouse/blob/master/org-clubhouse.el][org-clubhouse.el]] file in this repository for documentation on all supported configuration variables -- cgit 1.4.1 From 7167932309a9c000494b5849b17c4d152bc42edd Mon Sep 17 00:00:00 2001 From: Griffin Smith Date: Thu, 5 Sep 2019 15:40:37 -0400 Subject: Add code of conduct. --- CODE_OF_CONDUCT.org | 101 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 101 insertions(+) create mode 100644 CODE_OF_CONDUCT.org diff --git a/CODE_OF_CONDUCT.org b/CODE_OF_CONDUCT.org new file mode 100644 index 0000000000..f15e387d54 --- /dev/null +++ b/CODE_OF_CONDUCT.org @@ -0,0 +1,101 @@ +* Contributor Covenant Code of Conduct + :PROPERTIES: + :CUSTOM_ID: contributor-covenant-code-of-conduct + :END: + +** Our Pledge + :PROPERTIES: + :CUSTOM_ID: our-pledge + :END: + +In the interest of fostering an open and welcoming environment, we as +contributors and maintainers pledge to making participation in our +project and our community a harassment-free experience for everyone, +regardless of age, body size, disability, ethnicity, sex +characteristics, gender identity and expression, level of experience, +education, socio-economic status, nationality, personal appearance, +race, religion, or sexual identity and orientation. + +** Our Standards + :PROPERTIES: + :CUSTOM_ID: our-standards + :END: + +Examples of behavior that contributes to creating a positive environment +include: + +- Using welcoming and inclusive language +- Being respectful of differing viewpoints and experiences +- Gracefully accepting constructive criticism +- Focusing on what is best for the community +- Showing empathy towards other community members + +Examples of unacceptable behavior by participants include: + +- The use of sexualized language or imagery and unwelcome sexual + attention or advances +- Trolling, insulting/derogatory comments, and personal or political + attacks +- Public or private harassment +- Publishing others' private information, such as a physical or + electronic address, without explicit permission +- Other conduct which could reasonably be considered inappropriate in a + professional setting + +** Our Responsibilities + :PROPERTIES: + :CUSTOM_ID: our-responsibilities + :END: + +Project maintainers are responsible for clarifying the standards of +acceptable behavior and are expected to take appropriate and fair +corrective action in response to any instances of unacceptable behavior. + +Project maintainers have the right and responsibility to remove, edit, +or reject comments, commits, code, wiki edits, issues, and other +contributions that are not aligned to this Code of Conduct, or to ban +temporarily or permanently any contributor for other behaviors that they +deem inappropriate, threatening, offensive, or harmful. + +** Scope + :PROPERTIES: + :CUSTOM_ID: scope + :END: + +This Code of Conduct applies within all project spaces, and it also +applies when an individual is representing the project or its community +in public spaces. Examples of representing a project or community +include using an official project e-mail address, posting via an +official social media account, or acting as an appointed representative +at an online or offline event. Representation of a project may be +further defined and clarified by project maintainers. + +** Enforcement + :PROPERTIES: + :CUSTOM_ID: enforcement + :END: + +Instances of abusive, harassing, or otherwise unacceptable behavior may +be reported by contacting the project team at root@gws.fyi. All +complaints will be reviewed and investigated and will result in a +response that is deemed necessary and appropriate to the circumstances. +The project team is obligated to maintain confidentiality with regard to +the reporter of an incident. Further details of specific enforcement +policies may be posted separately. + +Project maintainers who do not follow or enforce the Code of Conduct in +good faith may face temporary or permanent repercussions as determined +by other members of the project's leadership. + +** Attribution + :PROPERTIES: + :CUSTOM_ID: attribution + :END: + +This Code of Conduct is adapted from the +[[https://www.contributor-covenant.org][Contributor Covenant]], version +1.4, available at +https://www.contributor-covenant.org/version/1/4/code-of-conduct.html + +For answers to common questions about this code of conduct, see +https://www.contributor-covenant.org/faq -- cgit 1.4.1 From 6b701daaa5e49b53065556dc441914718f518c18 Mon Sep 17 00:00:00 2001 From: Griffin Smith Date: Thu, 5 Sep 2019 15:43:29 -0400 Subject: Separate Usage documentation into read and write Separate the Usage documentation section into reading from and writing to clubhouse, and add documentation for a few previously-undocumented commands. --- README.org | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/README.org b/README.org index 3138eeb8a7..be3423598e 100644 --- a/README.org +++ b/README.org @@ -1,4 +1,4 @@ -#+TITLE: Org-Clubhouse +#+TITLE:Org-Clubhouse Simple, unopinionated integration between Emacs's [[https://orgmode.org/][org-mode]] and the [[https://clubhouse.io/][Clubhouse]] issue tracker @@ -60,8 +60,20 @@ their todo-keyword in org-mode. To opt-into this behavior, set the * Usage -In addition to updating the status of stories linked to clubhouse tickets, -org-clubhouse provides the following commands: +** Reading from clubhouse + +- ~org-clubhouse-headlines-from-query~ + Create org-mode headlines from a [[https://help.clubhouse.io/hc/en-us/articles/360000046646-Searching-in-Clubhouse-Story-Search][clubhouse query]] at the cursor's current + position, prompting for the headline indentation level and clubhouse query + text +- ~org-clubhouse-headline-from-story~ + Prompts for headline indentation level and the title of a story (which will + complete using the titles of all stories in your Clubhouse workspace) and + creates an org-mode headline from that story +- ~org-clubhouse-headline-from-story-id~ + Creates an org-mode headline directly from the ID of a clubhouse story + +** Writing to clubhouse - ~org-clubhouse-create-story~ Creates a new Clubhouse story from the current headline, or if a region of @@ -81,10 +93,6 @@ org-clubhouse provides the following commands: - ~org-clubhouse-update-description~ Update the status of the Clubhouse story linked to the current element with the contents of a drawer inside the element called DESCRIPTION, if any exists -- ~org-clubhouse-headlines-from-query~ - Create org-mode headlines from a clubhouse query at the cursor's current - position, prompting for the headline indentation level and clubhouse query - text - ~org-clubhouse-claim~ Adds the user configured in ~org-clubhouse-username~ as the owner of the clubhouse story associated with the headline at point -- cgit 1.4.1 From fe52639a3a4e6f97c0a6283e08cf200f97b9831a Mon Sep 17 00:00:00 2001 From: Griffin Smith Date: Thu, 5 Sep 2019 16:41:57 -0400 Subject: Expand documentation for org-clubhouse-mode Expand the documentation for the automatic updating of story statuses to include explicit documentation for org-clubhouse-state-alist. Ref #18 --- README.org | 34 ++++++++++++++++++++++++++-------- 1 file changed, 26 insertions(+), 8 deletions(-) diff --git a/README.org b/README.org index be3423598e..0300381ba0 100644 --- a/README.org +++ b/README.org @@ -50,14 +50,6 @@ Once installed, you'll need to set three global config vars: You can generate a new personal API token by going to the "API Tokens" tab on the "Settings" page in the clubhouse UI. -Org-clubhouse can be configured to update the status of stories as you update -their todo-keyword in org-mode. To opt-into this behavior, set the -~org-clubhouse-mode~ minor-mode: - -#+BEGIN_SRC emacs-lisp -(add-hook 'org-mode-hook #'org-clubhouse-mode nil nil) -#+END_SRC - * Usage ** Reading from clubhouse @@ -97,6 +89,32 @@ their todo-keyword in org-mode. To opt-into this behavior, set the Adds the user configured in ~org-clubhouse-username~ as the owner of the clubhouse story associated with the headline at point +*** Automatically updating Clubhouse story statuses + +Org-clubhouse can be configured to update the status of stories as you update +their todo-keyword in org-mode. To opt-into this behavior, set the +~org-clubhouse-mode~ minor-mode: + +#+BEGIN_SRC emacs-lisp +(add-hook 'org-mode-hook #'org-clubhouse-mode nil nil) +#+END_SRC + +The mapping from org-mode todo-keywords is configured via the +~org-clubhouse-state-alist~ variable, which should be an [[https://www.gnu.org/software/emacs/manual/html_node/elisp/Association-Lists.html][alist]] mapping (string) +[[https://orgmode.org/manual/Workflow-states.html][org-mode todo-keywords]] to the (string) names of their corresponding workflow +state. You can have todo-keywords that don't map to a workflow state (I use this +in my workflow extensively) and org-clubhouse will just preserve the previous +state of the story when moving to that state. + +An example config: + +#+BEGIN_SRC emacs-lisp +(setq org-clubhouse-state-alist + '(("TODO" . "To Do") + ("ACTIVE" . "In Progress") + ("DONE" . "Done"))) +#+END_SRC + * Philosophy I use org-mode every single day to manage tasks, notes, literate programming, -- cgit 1.4.1 From 30c340c902a9bf8ac3ae27a519b29b52cf0487e8 Mon Sep 17 00:00:00 2001 From: Griffin Smith Date: Thu, 5 Sep 2019 16:45:44 -0400 Subject: Update repo owner in all three install instructions --- README.org | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.org b/README.org index 0300381ba0..7a5f6d30d7 100644 --- a/README.org +++ b/README.org @@ -13,7 +13,7 @@ more of a personal project than a company one) #+BEGIN_SRC emacs-lisp (quelpa '(org-clubhouse :fetcher github - :repo "urbint/org-clubhouse")) + :repo "glittershark/org-clubhouse")) #+END_SRC ** [[https://github.com/hlissner/doom-emacs/][DOOM Emacs]] @@ -22,7 +22,7 @@ more of a personal project than a company one) ;; in packages.el (package! org-clubhouse :recipe (:fetcher github - :repo "urbint/org-clubhouse" + :repo "glittershark/org-clubhouse" :files ("*"))) ;; in config.el @@ -33,7 +33,7 @@ more of a personal project than a company one) #+BEGIN_SRC emacs-lisp ;; in .spacemacs (SPC+fed) dotspacemacs-additional-packages - '((org-clubhouse :location (recipe :fetcher github :repo "urbint/org-clubhouse"))) + '((org-clubhouse :location (recipe :fetcher github :repo "glittershark/org-clubhouse"))) #+END_SRC -- cgit 1.4.1 From f6a1dc071d8a65f6f56d02eb8949b638058dece2 Mon Sep 17 00:00:00 2001 From: Jean-Martin Archer Date: Mon, 6 Jan 2020 10:40:03 -0800 Subject: add ivy as as requirement Arguably, helm should be supported too, but hey! --- org-clubhouse.el | 1 + 1 file changed, 1 insertion(+) diff --git a/org-clubhouse.el b/org-clubhouse.el index 18509fa823..fc670d10dd 100644 --- a/org-clubhouse.el +++ b/org-clubhouse.el @@ -40,6 +40,7 @@ (require 'org) (require 'org-element) (require 'subr-x) +(require 'ivy) (require 'json) ;;; -- cgit 1.4.1 From ae8d046491739eddf59844b96d4d207ea065ea80 Mon Sep 17 00:00:00 2001 From: Jean-Martin Archer Date: Sun, 5 Jan 2020 10:03:39 -0800 Subject: api, switch to v3 v1 and v2 will be deprecated on 2020-03-01. It is unclear if v2 will still be available afterward. According to https://clubhouse.io/blog/api-v3/ it **should** be a drop in replacement. --- org-clubhouse.el | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/org-clubhouse.el b/org-clubhouse.el index fc670d10dd..3174da8bbd 100644 --- a/org-clubhouse.el +++ b/org-clubhouse.el @@ -311,7 +311,7 @@ If set to nil, will never create stories with labels") ;;; API integration ;;; -(defvar org-clubhouse-base-url* "https://api.clubhouse.io/api/v2") +(defvar org-clubhouse-base-url* "https://api.clubhouse.io/api/v3") (defun org-clubhouse-auth-url (url &optional params) (concat url -- cgit 1.4.1 From 489b37d17df8b0ffcb4b09d403c60a6be059aa54 Mon Sep 17 00:00:00 2001 From: Jean-Martin Archer Date: Sun, 5 Jan 2020 09:59:51 -0800 Subject: workflow, handle missing state name For some reason, some of my stories do not appear to have state name. This is most likely a bug (either with this mode or the API), regardless the missing name should be handled gracefully. --- org-clubhouse.el | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/org-clubhouse.el b/org-clubhouse.el index 3174da8bbd..fdfe2d16f8 100644 --- a/org-clubhouse.el +++ b/org-clubhouse.el @@ -501,7 +501,7 @@ If set to nil, will never create stories with labels") (-map (lambda (cell) (cons (cdr cell) (car cell))) org-clubhouse-state-alist))) (or (alist-get-equal state-name inv-state-name-alist) - (s-upcase state-name)))) + (if state-name (s-upcase state-name) "UNKNOWN")))) ;;; ;;; Prompting -- cgit 1.4.1 From 16cda5b42b3ea474ee0fe92eeee377e74808eb8e Mon Sep 17 00:00:00 2001 From: Jean-Martin Archer Date: Sun, 19 Jan 2020 21:57:48 -0800 Subject: find header if not currently at point Creating a single story would fail if the point was not on a header. Now the code tries to find the header for the element at point. --- org-clubhouse.el | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/org-clubhouse.el b/org-clubhouse.el index fdfe2d16f8..7f0fcec5e2 100644 --- a/org-clubhouse.el +++ b/org-clubhouse.el @@ -224,9 +224,11 @@ If set to nil, will never create stories with labels") ;; (org-element-find-headline))))) (defun org-element-find-headline () - (let ((current-elt (org-element-at-point))) - (when (equal 'headline (car current-elt)) - (cadr current-elt)))) + (save-mark-and-excursion + (when (not (outline-on-heading-p)) (org-back-to-heading)) + (let ((current-elt (org-element-at-point))) + (when (equal 'headline (car current-elt)) + (cadr current-elt))))) (defun org-element-extract-clubhouse-id (elt &optional property) (when-let* ((clubhouse-id-link (plist-get elt (or property :CLUBHOUSE-ID)))) -- cgit 1.4.1 From 39bb14987faa1b1155215b18df7093f1583f5dc5 Mon Sep 17 00:00:00 2001 From: Jean-Martin Archer Date: Sun, 19 Jan 2020 09:43:52 -0800 Subject: git, ignore files created by spacemacs --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000..2a7dd97deb --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +# Spacemacs +org-clubhouse-autoloads.el +org-clubhouse-pkg.el -- cgit 1.4.1 From b53b64130656bf6e1b91f4512542c7f5ab17989d Mon Sep 17 00:00:00 2001 From: Jean-Martin Archer Date: Sun, 19 Jan 2020 09:38:58 -0800 Subject: story, allow to create story without an epic While it's probably a good idea to assign epics to new stories, depending on the methodology used by the team not everything may warrant being attached to an epic. E.g. one off task and what not. --- org-clubhouse.el | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/org-clubhouse.el b/org-clubhouse.el index 7f0fcec5e2..55f597c9f2 100644 --- a/org-clubhouse.el +++ b/org-clubhouse.el @@ -435,7 +435,7 @@ If set to nil, will never create stories with labels") (org-clubhouse-fetch-as-id-name-pairs "projects")) (defcache org-clubhouse-epics - "Returns projects as (project-id . name)" + "Returns epics as (epic-id . name)" (org-clubhouse-fetch-as-id-name-pairs "epics")) (defcache org-clubhouse-milestones @@ -523,7 +523,7 @@ If set to nil, will never create stories with labels") (defun org-clubhouse-prompt-for-epic (cb) (ivy-read "Select an epic: " - (-map #'cdr (org-clubhouse-epics)) + (-map #'cdr (append '((nil . "No Epic")) (org-clubhouse-epics))) :history 'org-clubhouse-epic-history :action (lambda (selected) (let ((epic-id @@ -666,11 +666,11 @@ If the epics already have a CLUBHOUSE-EPIC-ID, they are filtered and ignored." (org-make-link-string (org-clubhouse-link-to-story story-id) (number-to-string story-id))) - - (org-set-property "clubhouse-epic" - (org-make-link-string - (org-clubhouse-link-to-epic epic-id) - (alist-get epic-id (org-clubhouse-epics)))) + (when epic-id + (org-set-property "clubhouse-epic" + (org-make-link-string + (org-clubhouse-link-to-epic epic-id) + (alist-get epic-id (org-clubhouse-epics))))) (org-set-property "clubhouse-project" (org-make-link-string -- cgit 1.4.1 From c033b9161c4d2d30418094e15ce067de09f736bb Mon Sep 17 00:00:00 2001 From: Griffin Smith Date: Mon, 9 Mar 2020 10:40:30 -0400 Subject: Add org-clubhouse-set-epic Add an org-clubhouse-set-epic command, to change the clubhouse epic of the story at point --- org-clubhouse.el | 48 +++++++++++++++++++++++++++++++++++++----------- 1 file changed, 37 insertions(+), 11 deletions(-) diff --git a/org-clubhouse.el b/org-clubhouse.el index 55f597c9f2..07c3220998 100644 --- a/org-clubhouse.el +++ b/org-clubhouse.el @@ -521,6 +521,7 @@ If set to nil, will never create stories with labels") (funcall cb project-id))))) (defun org-clubhouse-prompt-for-epic (cb) + "Prompt the user for an epic using ivy and call CB with its ID." (ivy-read "Select an epic: " (-map #'cdr (append '((nil . "No Epic")) (org-clubhouse-epics))) @@ -589,12 +590,12 @@ If set to nil, will never create stories with labels") (goto-char elt-start) (org-set-property "clubhouse-epic-id" - (org-make-link-string + (org-link-make-string (org-clubhouse-link-to-epic epic-id) (number-to-string epic-id))) (org-set-property "clubhouse-milestone" - (org-make-link-string + (org-link-make-string (org-clubhouse-link-to-milestone milestone-id) (alist-get milestone-id (org-clubhouse-milestones))))))) @@ -663,17 +664,17 @@ If the epics already have a CLUBHOUSE-EPIC-ID, they are filtered and ignored." (goto-char elt-start) (org-set-property "clubhouse-id" - (org-make-link-string + (org-link-make-string (org-clubhouse-link-to-story story-id) (number-to-string story-id))) (when epic-id - (org-set-property "clubhouse-epic" - (org-make-link-string - (org-clubhouse-link-to-epic epic-id) - (alist-get epic-id (org-clubhouse-epics))))) + (org-set-property "clubhouse-epic" + (org-link-make-string + (org-clubhouse-link-to-epic epic-id) + (alist-get epic-id (org-clubhouse-epics))))) (org-set-property "clubhouse-project" - (org-make-link-string + (org-link-make-string (org-clubhouse-link-to-project project-id) (alist-get project-id (org-clubhouse-projects)))) @@ -800,7 +801,7 @@ allows manually passing a clubhouse ID and list of org-element plists to write" (org-set-property "clubhouse-task-id" (format "%d" task-id)) (org-set-property "clubhouse-story-id" - (org-make-link-string + (org-link-make-string (org-clubhouse-link-to-story story-id) (number-to-string story-id))) @@ -960,7 +961,7 @@ which labels to set." (alist-get 'description task) (alist-get 'id task) (let ((story-id (alist-get 'story_id task))) - (org-make-link-string + (org-link-make-string (org-clubhouse-link-to-story story-id) story-id)))) @@ -984,7 +985,7 @@ which labels to set." (-map (apply-partially #'alist-get 'name))))) (format ":%s:" (s-join ":" labels)) "") - (org-make-link-string + (org-link-make-string (org-clubhouse-link-to-story story-id) (number-to-string story-id)) (let ((desc (alist-get 'description story))) @@ -1111,6 +1112,31 @@ Uses `org-clubhouse-state-alist'. Operates over stories from BEG to END" (message "Successfully synchronized status of %d stories from Clubhouse" (length elts)))) +(defun org-clubhouse-set-epic (&optional story-id epic-id cb) + "Set the epic of clubhouse story STORY-ID to EPIC-ID, then call CB. + +When called interactively, prompt for an epic and set the story of the clubhouse +story at point" + (interactive) + (if (and story-id epic-id) + (progn + (org-clubhouse-update-story-internal + story-id :epic-id epic-id) + (when cb (funcall cb))) + (let ((story-id (org-element-clubhouse-id))) + (org-clubhouse-prompt-for-epic + (lambda (epic-id) + (org-clubhouse-set-epic + story-id epic-id + (lambda () + (org-set-property + "clubhouse-epic" + (org-link-make-string + (org-clubhouse-link-to-epic epic-id) + (alist-get epic-id (org-clubhouse-epics)))) + (message "Successfully set the epic on story %d to %d" + story-id epic-id)))))))) + ;;; (define-minor-mode org-clubhouse-mode -- cgit 1.4.1 From 24c36b781b09ee20dfb34f84c91f7ac736de7d66 Mon Sep 17 00:00:00 2001 From: Griffin Smith Date: Wed, 18 Mar 2020 15:17:47 -0400 Subject: Add org-clubhouse-clocked-in-story-id Add an org-clubhouse-clocked-in-story-id function, for programmatically querying the currently clocked-in clubhouse story ID --- org-clubhouse.el | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/org-clubhouse.el b/org-clubhouse.el index 07c3220998..81aab9ad3f 100644 --- a/org-clubhouse.el +++ b/org-clubhouse.el @@ -258,6 +258,23 @@ If set to nil, will never create stories with labels") (org-element-extract-clubhouse-id (org-element-find-headline))) +(defun org-clubhouse-clocked-in-story-id () + "Return the clubhouse story-id of the currently clocked-in org entry, if any." + (save-mark-and-excursion + (save-current-buffer + (when (org-clocking-p) + (set-buffer (marker-buffer org-clock-marker)) + (save-restriction + (when (or (< org-clock-marker (point-min)) + (> org-clock-marker (point-max))) + (widen)) + (goto-char org-clock-marker) + (org-element-clubhouse-id)))))) + +(comment + (org-clubhouse-clocked-in-story-id) + ) + (defun org-element-and-children-at-point () (let* ((elt (org-element-find-headline)) (contents-begin (or (plist-get elt :contents-begin) -- cgit 1.4.1 From 1722eac5f8e8264e72300e3956087c88aafbc36b Mon Sep 17 00:00:00 2001 From: Griffin Smith Date: Wed, 18 Mar 2020 15:18:19 -0400 Subject: Add binding for missing variable in set-title oops --- org-clubhouse.el | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/org-clubhouse.el b/org-clubhouse.el index 81aab9ad3f..95a59bff90 100644 --- a/org-clubhouse.el +++ b/org-clubhouse.el @@ -868,7 +868,8 @@ the headline." (interactive) (let* ((elt (org-element-find-headline)) - (title (plist-get elt :title))) + (title (plist-get elt :title)) + (clubhouse-id (org-element-clubhouse-id))) (and (org-clubhouse-update-story-at-point clubhouse-id -- cgit 1.4.1 From 9d792b8c6e2f46d35e8cec728e13f568a8f23094 Mon Sep 17 00:00:00 2001 From: Griffin Smith Date: Thu, 19 Mar 2020 15:16:38 -0400 Subject: Don't pass owner_ids if we're not claiming If we're not claiming a story, don't pass owner_ids to the API at all, rather than passing it as null, which causes the API to return an error. --- org-clubhouse.el | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/org-clubhouse.el b/org-clubhouse.el index 95a59bff90..3be5b40983 100644 --- a/org-clubhouse.el +++ b/org-clubhouse.el @@ -911,11 +911,16 @@ element." is not set") nil)))) - (org-clubhouse-update-story-internal - clubhouse-id - :workflow_state_id workflow-state-id - :owner_ids (when update-assignee? - (list (org-clubhouse-whoami)))) + (if update-assignee? + (org-clubhouse-update-story-internal + clubhouse-id + :workflow_state_id workflow-state-id + :owner_ids (if update-assignee? + (list (org-clubhouse-whoami)) + (list))) + (org-clubhouse-update-story-internal + clubhouse-id + :workflow_state_id workflow-state-id)) (message (if update-assignee? "Successfully claimed story and updated clubhouse status to \"%s\"" -- cgit 1.4.1 From 750f0fd82d01a72ecb451888769e1813f13a2e0c Mon Sep 17 00:00:00 2001 From: Griffin Smith Date: Thu, 19 Mar 2020 15:30:20 -0400 Subject: Add docs for org-clubhouse-username config value This is crummy but it's how it has to be right now --- README.org | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.org b/README.org index 7a5f6d30d7..9cd8fbe892 100644 --- a/README.org +++ b/README.org @@ -50,6 +50,10 @@ Once installed, you'll need to set three global config vars: You can generate a new personal API token by going to the "API Tokens" tab on the "Settings" page in the clubhouse UI. +Note that ~org-clubhouse-username~ needs to be set to your *mention name*, not +your username, as currently there's no way to get the ID of a user given their +username in the clubhouse API + * Usage ** Reading from clubhouse -- cgit 1.4.1 From 0bca01b8775a7240f4e71dba062a72a427d71324 Mon Sep 17 00:00:00 2001 From: Tatu Lahtela Date: Mon, 30 Mar 2020 11:18:07 +0300 Subject: Iterations support Ability retrieve headlines from a single iteration --- org-clubhouse.el | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/org-clubhouse.el b/org-clubhouse.el index 3be5b40983..b3f7a7f75a 100644 --- a/org-clubhouse.el +++ b/org-clubhouse.el @@ -490,6 +490,10 @@ If set to nil, will never create stories with labels") (equal org-clubhouse-username)))) (alist-get 'id))) +(defcache org-clubhouse-iterations + "Returns iterations as (project-id . name)" + (org-clubhouse-fetch-as-id-name-pairs "iterations")) + (defun org-clubhouse-stories-in-project (project-id) "Return the stories in the given PROJECT-ID as org headlines." (let ((resp-json (org-clubhouse-request "GET" (format "/projects/%d/stories" project-id)))) @@ -1043,6 +1047,41 @@ which labels to set." (append nil) reject-archived))) +(defun org-clubhouse-prompt-for-iteration (cb) + "Prompt for iteration and call CB with that iteration" + (ivy-read + "Select an interation: " + (-map #'cdr (org-clubhouse-iterations)) + :require-match t + :history 'org-clubhouse-iteration-history + :action (lambda (selected) + (let ((iteration-id + (find-match-in-alist selected (org-clubhouse-iterations)))) + (funcall cb iteration-id))))) + +(defun org-clubhouse--get-iteration (iteration-id) + (-> (org-clubhouse-request "GET" (format "iterations/%d/stories" iteration-id)) + (append nil))) + +(defun org-clubhouse-headlines-from-iteration (level) + "Create `org-mode' headlines from a clubhouse iteration. + +Create `org-mode' headlines from all the resulting stories at headline level LEVEL." + (interactive "*nLevel: ") + (org-clubhouse-prompt-for-iteration + (lambda (iteration-id) + (let ((story-list (org-clubhouse--get-iteration iteration-id))) + (if (null story-list) + (message "Iteration id returned no stories: %d" iteration-id) + (let ((text (mapconcat (apply-partially + #'org-clubhouse--story-to-headline-text + level) + (reject-archived story-list) "\n"))) + (save-mark-and-excursion + (insert text) + (org-align-all-tags)) + text)))))) + (defun org-clubhouse-headlines-from-query (level query) "Create `org-mode' headlines from a clubhouse query. -- cgit 1.4.1 From 12313582b8ddcc8ff59ea4d610e99c6c1b423eee Mon Sep 17 00:00:00 2001 From: Griffin Smith Date: Mon, 30 Mar 2020 09:10:34 -0400 Subject: Correct variable name in docstring iteration-id, not project-id --- org-clubhouse.el | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/org-clubhouse.el b/org-clubhouse.el index b3f7a7f75a..d0f2b2c4c1 100644 --- a/org-clubhouse.el +++ b/org-clubhouse.el @@ -491,7 +491,7 @@ If set to nil, will never create stories with labels") (alist-get 'id))) (defcache org-clubhouse-iterations - "Returns iterations as (project-id . name)" + "Returns iterations as (iteration-id . name)" (org-clubhouse-fetch-as-id-name-pairs "iterations")) (defun org-clubhouse-stories-in-project (project-id) @@ -1069,7 +1069,7 @@ which labels to set." Create `org-mode' headlines from all the resulting stories at headline level LEVEL." (interactive "*nLevel: ") (org-clubhouse-prompt-for-iteration - (lambda (iteration-id) + (lambda (iteration-id) (let ((story-list (org-clubhouse--get-iteration iteration-id))) (if (null story-list) (message "Iteration id returned no stories: %d" iteration-id) -- cgit 1.4.1 From 188af3a0d3b661a12e3b854253d112e9f9044714 Mon Sep 17 00:00:00 2001 From: Griffin Smith Date: Tue, 31 Mar 2020 15:34:45 -0400 Subject: Allow creating epics without milestones The same as how we allow creating stories without epics, add a "No Milestone" list item to the top of the list of milestones to select from when creating an epic. --- org-clubhouse.el | 31 ++++++++++++++++--------------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/org-clubhouse.el b/org-clubhouse.el index d0f2b2c4c1..1894f344ef 100644 --- a/org-clubhouse.el +++ b/org-clubhouse.el @@ -553,9 +553,10 @@ If set to nil, will never create stories with labels") (funcall cb epic-id))))) (defun org-clubhouse-prompt-for-milestone (cb) + "Prompt the user for a milestone using ivy and call CB with its ID." (ivy-read "Select a milestone: " - (-map #'cdr (org-clubhouse-milestones)) + (-map #'cdr (append '((nil . "No Milestone")) (org-clubhouse-milestones))) :require-match t :history 'org-clubhouse-milestone-history :action (lambda (selected) @@ -593,7 +594,8 @@ If set to nil, will never create stories with labels") (cl-defun org-clubhouse-create-epic-internal (title &key milestone-id) (cl-assert (and (stringp title) - (integerp milestone-id))) + (or (null milestone-id) + (integerp milestone-id)))) (org-clubhouse-request "POST" "epics" @@ -606,7 +608,6 @@ If set to nil, will never create stories with labels") (let ((elt-start (plist-get elt :begin)) (epic-id (alist-get 'id epic)) (milestone-id (alist-get 'milestone_id epic))) - (save-excursion (goto-char elt-start) @@ -615,10 +616,11 @@ If set to nil, will never create stories with labels") (org-clubhouse-link-to-epic epic-id) (number-to-string epic-id))) - (org-set-property "clubhouse-milestone" - (org-link-make-string - (org-clubhouse-link-to-milestone milestone-id) - (alist-get milestone-id (org-clubhouse-milestones))))))) + (when milestone-id + (org-set-property "clubhouse-milestone" + (org-link-make-string + (org-clubhouse-link-to-milestone milestone-id) + (alist-get milestone-id (org-clubhouse-milestones)))))))) (defun org-clubhouse-create-epic (&optional beg end) "Creates a clubhouse epic using selected headlines. @@ -635,14 +637,13 @@ If the epics already have a CLUBHOUSE-EPIC-ID, they are filtered and ignored." (elts (-remove (lambda (elt) (plist-get elt :CLUBHOUSE-EPIC-ID)) elts))) (org-clubhouse-prompt-for-milestone (lambda (milestone-id) - (when milestone-id - (dolist (elt elts) - (let* ((title (plist-get elt :title)) - (epic (org-clubhouse-create-epic-internal - title - :milestone-id milestone-id))) - (org-clubhouse-populate-created-epic elt epic)) - elts)))))) + (dolist (elt elts) + (let* ((title (plist-get elt :title)) + (epic (org-clubhouse-create-epic-internal + title + :milestone-id milestone-id))) + (org-clubhouse-populate-created-epic elt epic)) + elts))))) ;;; ;;; Story creation -- cgit 1.4.1 From 24be24077c8c4e12b043fb8d927d699b41264ec9 Mon Sep 17 00:00:00 2001 From: Griffin Smith Date: Fri, 8 May 2020 12:09:54 -0400 Subject: Allow org-clubhouse-set-epic on regions Make org-clubhouse-set-epic set all selected stories to the same epic if multiple stories are selected. --- org-clubhouse.el | 37 +++++++++++++++++++++++-------------- 1 file changed, 23 insertions(+), 14 deletions(-) diff --git a/org-clubhouse.el b/org-clubhouse.el index 1894f344ef..072899ecd9 100644 --- a/org-clubhouse.el +++ b/org-clubhouse.el @@ -1175,30 +1175,39 @@ Uses `org-clubhouse-state-alist'. Operates over stories from BEG to END" (message "Successfully synchronized status of %d stories from Clubhouse" (length elts)))) -(defun org-clubhouse-set-epic (&optional story-id epic-id cb) +(cl-defun org-clubhouse-set-epic (&optional story-id epic-id cb &key beg end) "Set the epic of clubhouse story STORY-ID to EPIC-ID, then call CB. When called interactively, prompt for an epic and set the story of the clubhouse -story at point" - (interactive) +stor{y,ies} at point or region" + (interactive + (when (use-region-p) + (list nil nil nil + :beg (region-beginning) + :end (region-end)))) (if (and story-id epic-id) (progn (org-clubhouse-update-story-internal story-id :epic-id epic-id) (when cb (funcall cb))) - (let ((story-id (org-element-clubhouse-id))) + (let ((elts (-filter (lambda (elt) (plist-get elt :CLUBHOUSE-ID)) + (org-clubhouse-collect-headlines beg end)))) (org-clubhouse-prompt-for-epic (lambda (epic-id) - (org-clubhouse-set-epic - story-id epic-id - (lambda () - (org-set-property - "clubhouse-epic" - (org-link-make-string - (org-clubhouse-link-to-epic epic-id) - (alist-get epic-id (org-clubhouse-epics)))) - (message "Successfully set the epic on story %d to %d" - story-id epic-id)))))))) + (-map + (lambda (elt) + (let ((story-id (org-element-extract-clubhouse-id elt))) + (org-clubhouse-set-epic + story-id epic-id + (lambda () + (org-set-property + "clubhouse-epic" + (org-link-make-string + (org-clubhouse-link-to-epic epic-id) + (alist-get epic-id (org-clubhouse-epics)))) + (message "Successfully set the epic on story %d to %d" + story-id epic-id)))))) + elts))))) ;;; -- cgit 1.4.1 From f00c22a49af3798c8a4be6d1831b5142ff56a19e Mon Sep 17 00:00:00 2001 From: Griffin Smith Date: Fri, 8 May 2020 13:38:31 -0400 Subject: Don't create stories in reverse order org-clubhouse-get-headlines-in-region was returning elements in reverse order, causing stories to be created in reverse order when multiple were created in a region with org-clubhouse-create-story. Just reversing the list at the end should fix that. --- org-clubhouse.el | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/org-clubhouse.el b/org-clubhouse.el index 072899ecd9..884c8d288f 100644 --- a/org-clubhouse.el +++ b/org-clubhouse.el @@ -174,10 +174,9 @@ If set to nil, will never create stories with labels") (defun org-clubhouse-collect-headlines (beg end) "Collects the headline at point or the headlines in a region. Returns a list." - (setq test-headlines (if (and beg end) (org-clubhouse-get-headlines-in-region beg end) - (list (org-element-find-headline))))) + (list (org-element-find-headline)))) (defun org-clubhouse-get-headlines-in-region (beg end) @@ -208,7 +207,7 @@ If set to nil, will never create stories with labels") (let ((before (point))) (org-forward-heading-same-level 1) (setq before-end (and (not (eq before (point))) (< (point) end))))) - headlines))) + (reverse headlines)))) ;;; ;;; Org-element interaction -- cgit 1.4.1 From 8838454236cd448afa2c752c4a6dc6f4bb7a3bf4 Mon Sep 17 00:00:00 2001 From: Tatu Lahtela Date: Fri, 12 Jun 2020 11:31:32 +0300 Subject: Add function to add a single headline from my tasks Add function to create single story element from a prompted list of active stories. --- org-clubhouse.el | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/org-clubhouse.el b/org-clubhouse.el index 884c8d288f..e6e29b5751 100644 --- a/org-clubhouse.el +++ b/org-clubhouse.el @@ -1030,6 +1030,21 @@ which labels to set." "\n") "")))) +(defun org-clubhouse-headline-from-my-tasks (level) + "Prompt my active stories and create a single `org-mode' headline at LEVEL." + (interactive "*nLevel: \n") + (if org-clubhouse-username + (let* ((story-list (org-clubhouse--search-stories + (format "owner:%s !is:done !is:archived" + org-clubhouse-username))) + (stories (to-id-name-pairs story-list))) + (org-clubhouse-headline-from-story-id level + (find-match-in-alist + (ivy-read "Select Story: " + (-map #'cdr stories)) + stories))) + (warn "Can't fetch my tasks if `org-clubhouse-username' is unset"))) + (defun org-clubhouse-headline-from-story-id (level story-id) "Create a single `org-mode' headline at LEVEL based on the given clubhouse STORY-ID." (interactive "*nLevel: \nnStory ID: ") -- cgit 1.4.1