diff --git a/app/app/components/section/trello/type-editor.js b/app/app/components/section/trello/type-editor.js
index 9392f020..87a6470d 100644
--- a/app/app/components/section/trello/type-editor.js
+++ b/app/app/components/section/trello/type-editor.js
@@ -50,7 +50,8 @@ export default Ember.Component.extend(SectionMixin, NotifierMixin, TooltipMixin,
token: "",
user: null,
board: null,
- lists: []
+ lists: [],
+ boards: []
};
}
@@ -101,6 +102,12 @@ export default Ember.Component.extend(SectionMixin, NotifierMixin, TooltipMixin,
this.set('noBoards', false);
+ if (is.undefined(self.get('initDateTimePicker'))) {
+ $.datetimepicker.setLocale('en');
+ $('#trello-since').datetimepicker();
+ self.set('initDateTimePicker', "Done");
+ }
+
if (is.null(board) || is.undefined(board)) {
if (boards.length) {
board = boards[0];
@@ -110,30 +117,34 @@ export default Ember.Component.extend(SectionMixin, NotifierMixin, TooltipMixin,
this.set('config.board', boards.findBy('id', board.id));
}
- this.get('sectionService').fetch(page, "lists", self.get('config'))
- .then(function (lists) {
- let savedLists = self.get('config.lists');
- if (savedLists === null) {
- savedLists = [];
- }
-
- lists.forEach(function (list) {
- let saved = savedLists.findBy("id", list.id);
- let included = true;
- if (is.not.undefined(saved)) {
- included = saved.included;
+ if (is.null(board.id) || is.undefined(board.id)) {
+ self.set('busy', false);
+ } else {
+ this.get('sectionService').fetch(page, "lists", self.get('config'))
+ .then(function (lists) {
+ let savedLists = self.get('config.lists');
+ if (savedLists === null) {
+ savedLists = [];
}
- list.included = included;
- });
- self.set('config.lists', lists);
- self.set('busy', false);
- }, function (error) { //jshint ignore: line
- self.set('busy', false);
- self.set('authenticated', false);
- self.showNotification("Unable to fetch board lists");
- console.log(error);
- });
+ lists.forEach(function (list) {
+ let saved = savedLists.findBy("id", list.id);
+ let included = true;
+ if (is.not.undefined(saved)) {
+ included = saved.included;
+ }
+ list.included = included;
+ });
+
+ self.set('config.lists', lists);
+ self.set('busy', false);
+ }, function (error) { //jshint ignore: line
+ self.set('busy', false);
+ self.set('authenticated', false);
+ self.showNotification("Unable to fetch board lists");
+ console.log(error);
+ });
+ }
},
actions: {
@@ -150,6 +161,15 @@ export default Ember.Component.extend(SectionMixin, NotifierMixin, TooltipMixin,
}
},
+ onBoardCheckbox(id) {
+ let boards = this.get('config.boards');
+ let board = boards.findBy('id', id);
+
+ if (board !== null) {
+ Ember.set(board, 'included', !board.included);
+ }
+ },
+
auth() {
if (this.get('appKey') === "") {
$("#trello-appkey").addClass('error').focus();
@@ -187,6 +207,8 @@ export default Ember.Component.extend(SectionMixin, NotifierMixin, TooltipMixin,
self.get('sectionService').fetch(page, "boards", self.get('config'))
.then(function (boards) {
self.set('busy', false);
+ boards.unshift({ id: null, namePath: "< do not show >", backgroundColor: "white" }); // add the non-selection to the front
+ self.set('config.boards', boards); // save the boards in the config too
self.set('boards', boards);
self.getBoardLists();
}, function (error) { //jshint ignore: line
diff --git a/app/app/styles/section/trello.scss b/app/app/styles/section/trello.scss
index 20bba3e8..893a3ddf 100644
--- a/app/app/styles/section/trello.scss
+++ b/app/app/styles/section/trello.scss
@@ -1,38 +1,141 @@
-.section-trello-board {
- width: 100%;
- padding: 10px;
- white-space: nowrap;
- overflow: auto
-}
+.section-trello-editor {
+ .section-trello-board {
+ width: 100%;
+ padding: 10px;
+ white-space: nowrap;
+ overflow: auto
+ }
-.section-trello-board-title {
- font-weight: bold;
- color: #fff;
- font-size: 16px;
-}
+ .section-trello-board-title {
+ font-weight: bold;
+ color: #fff;
+ font-size: 16px;
+ }
-.section-trello-list {
- background-color: #e2e4e6;
- padding: 10px;
- border-radius: 3px;
- margin: 10px 10px 0 0;
- max-width: 300px;
-}
+ .section-trello-list {
+ background-color: #e2e4e6;
+ padding: 10px;
+ border-radius: 3px;
+ margin: 10px 10px 0 0;
+ max-width: 300px;
+ }
-.section-trello-list-title {
- font-weight: bold;
- color: #4c4c4c;
- font-size: 14px;
- margin: 5px;
-}
+ .section-trello-list-title {
+ font-weight: bold;
+ color: #4c4c4c;
+ font-size: 14px;
+ margin: 5px;
+ }
-.section-trello-list-checkbox {
- vertical-align: text-bottom;
-}
+ .section-trello-list-checkbox {
+ vertical-align: text-bottom;
+ }
+ .trello-list {
+ margin-bottom: 10px;
+
+ i {
+ vertical-align: middle;
+ }
+
+ .trello-label {
+ font-size: 11px;
+ color: #fff;
+ padding: 0 8px;
+ margin-right: 5px;
+ border-radius: 2px;
+ box-shadow: inset 0 -1px 0 rgba(0,0,0,.12);
+ display: inline-block;
+ line-height: 22px;
+ }
+ }
+
+}
.section-trello-render {
- > .trello-board {
+ .trello-table {
+ border: none!important;
+ margin: 0px !important;
+
+ td {
+ border: none !important;
+ vertical-align: top;
+ }
+ }
+
+ .heading {
+ font-size: 1.6rem;
+ margin: 30px 0 0 0;
+ }
+
+ .trello-label {
+ font-size: 11px;
+ color: #fff;
+ padding: 0 8px;
+ margin-right: 5px;
+ border-radius: 2px;
+ box-shadow: inset 0 -1px 0 rgba(0,0,0,.12);
+ display: inline-block;
+ line-height: 22px;
+ text-shadow: 0 0 5px rgba(0,0,0,.2),0 0 2px #000;
+ }
+
+ .board-stats {
+ margin-bottom: 10px;
+ td {
+ color: $color-gray;
+ padding: 20px 40px 30px 0px !important;
+ }
+ .stat-number {
+ font-family: "open_sanslight";
+ font-size: 40px;
+ margin-right: 20px;
+ color: black;
+ }
+ }
+
+ .trello-board {
+ color: white;
+ padding: 15px 20px;
+ width: 190px;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ display: inline-block;
+ border-radius: 4px;
+ margin-right: 20px;
+
+ span {
+ display: table;
+ font-size: 14px;
+ line-height: 10px;
+ opacity: 0.5;
+ padding-bottom: 10px;
+ margin-bottom: 8px;
+ }
+ }
+
+ .board-summary {
+ font-size: 1.2rem;
+ }
+
+ .member-name {
+ font-size: 1.2rem;
+ line-height: 15px;
+ }
+
+ .board-meta, .member-meta {
+ color: $color-gray;
+ }
+
+ .trello-avatar {
+ border-radius: 4px;
+ width: 36px;
+ height: 36px;
+ margin-right: 5px;
+ }
+
+ > .single-trello-board {
width: 100%;
max-height: 600px;
padding: 10px;
@@ -44,7 +147,7 @@
font-weight: bold;
color: #fff;
font-size: 16px;
- }
+ }
}
> .trello-list {
@@ -65,7 +168,7 @@
font-size: 14px;
margin: 0 10px 10px 0;
}
-
+
> a {
> .trello-card {
color: #4c4c4c;
@@ -82,8 +185,8 @@
white-space: normal;
cursor: pointer;
vertical-align: top;
- }
+ }
}
}
}
-}
\ No newline at end of file
+}
diff --git a/app/app/templates/components/section/trello/type-editor.hbs b/app/app/templates/components/section/trello/type-editor.hbs
index 48e9939a..f772b9ca 100644
--- a/app/app/templates/components/section/trello/type-editor.hbs
+++ b/app/app/templates/components/section/trello/type-editor.hbs
@@ -2,45 +2,80 @@
tip="Trello is the visual way to manage your projects and organize anything (https://trello.com)"
isDirty=(action 'isDirty') onCancel=(action 'onCancel') onAction=(action 'onAction')}}
- {{#if authenticated}}
+
-
-
{{/section/base-editor}}
diff --git a/core/section/trello/activitytranslation.go b/core/section/trello/activitytranslation.go
new file mode 100644
index 00000000..331b8ff7
--- /dev/null
+++ b/core/section/trello/activitytranslation.go
@@ -0,0 +1,30 @@
+// Copyright 2016 Documize Inc.
. All rights reserved.
+//
+// This software (Documize Community Edition) is licensed under
+// GNU AGPL v3 http://www.gnu.org/licenses/agpl-3.0.en.html
+//
+// You can operate outside the AGPL restrictions by purchasing
+// Documize Enterprise Edition and obtaining a commercial license
+// by contacting .
+//
+// https://documize.com
+
+package trello
+
+var activityTranslation = map[string]string{
+ "add checklist to card": "checklist added to card",
+ "add member to card": "member added to card",
+ "comment card": "commented on card",
+ "create card": "created card",
+ "create list": "created list",
+ "delete card": "deleted card",
+ "update board": "updated board",
+ "add to team board": "",
+ "create board": "",
+ "update card": "updated card",
+ "update check item state on card": "check item updated on card",
+ "update list": "updated list",
+ "add attachment to card": "attachment added to card",
+ "copy card": "copied card",
+ "copy comment card": "copied comment on card",
+}
diff --git a/core/section/trello/archive_template.go b/core/section/trello/archive_template.go
new file mode 100644
index 00000000..7bcd4a03
--- /dev/null
+++ b/core/section/trello/archive_template.go
@@ -0,0 +1,62 @@
+// Copyright 2016 Documize Inc. . All rights reserved.
+//
+// This software (Documize Community Edition) is licensed under
+// GNU AGPL v3 http://www.gnu.org/licenses/agpl-3.0.en.html
+//
+// You can operate outside the AGPL restrictions by purchasing
+// Documize Enterprise Edition and obtaining a commercial license
+// by contacting .
+//
+// https://documize.com
+
+package trello
+
+const archiveTemplate = `
+{{if false}}
+
+
+ {{if gt (len .Boards) 0}}
+
Deleted and Archived Cards
+
Changes since {{.Since}}.
+
+
+
+ {{range $b := .Boards}}
+
+
+
+ {{$b.Board.Name}}
+
+ |
+
+ {{range $act := $b.Actions}}
+ {{if eq $act.Type "deleteCard" }}
+ Deleted:
+ {{$act.Data.List.Name}}
+ {{if ne $act.Data.Card.Name ""}}
+ : {{$act.Data.Card.Name}}
+ {{if ne $act.Data.Text ""}}
+ - {{$act.Data.Text}}
+ {{end}}
+ {{end}}
+
+ {{end}}
+ {{end}}
+ {{range $arch := $b.Archived}}
+ Archived:
+ {{$arch.Name}}
+ {{if ne $arch.Desc ""}}
+ - {{$arch.Desc}}
+ {{end}}
+
+ {{end}}
+ |
+
+ {{end}}
+
+
+
+ {{end}}
+
+{{end}}
+`
diff --git a/core/section/trello/boards_template.go b/core/section/trello/boards_template.go
new file mode 100644
index 00000000..47a5a523
--- /dev/null
+++ b/core/section/trello/boards_template.go
@@ -0,0 +1,55 @@
+// Copyright 2016 Documize Inc. . All rights reserved.
+//
+// This software (Documize Community Edition) is licensed under
+// GNU AGPL v3 http://www.gnu.org/licenses/agpl-3.0.en.html
+//
+// You can operate outside the AGPL restrictions by purchasing
+// Documize Enterprise Edition and obtaining a commercial license
+// by contacting .
+//
+// https://documize.com
+
+package trello
+
+const boardsTemplate = `
+
+ {{if gt (len .Boards) 0}}
+
Boards
+
There are {{len .Boards}} boards, {{.ListTotal}} since lists, {{.CardTotal}} cards and {{len .MemberBoardAssign}} members {{.Since}}.
+
+
+
+ {{range $b := .Boards}}
+
+
+
+
+ {{$b.Board.Name}}
+ {{$b.Board.OrgName}}
+
+
+ |
+
+
+
+
+
+ {{range $act, $tot := $b.ActionSummary}}
+ {{$tot}} {{$act}}{{if ne 1 $tot}}s{{end}},
+ {{end}}
+ {{if gt (len $b.Archived) 0}}
+ {{len $b.Archived}} {{if eq 1 (len $b.Archived)}}card {{else}} cards {{end}}archived
+ {{else}}
+ no cards archived
+ {{end}}
+
+
+ |
+
+ {{end}}
+
+
+
+ {{end}}
+
+`
diff --git a/core/section/trello/graphs_template.go b/core/section/trello/graphs_template.go
new file mode 100644
index 00000000..cd1971ca
--- /dev/null
+++ b/core/section/trello/graphs_template.go
@@ -0,0 +1,32 @@
+// Copyright 2016 Documize Inc. . All rights reserved.
+//
+// This software (Documize Community Edition) is licensed under
+// GNU AGPL v3 http://www.gnu.org/licenses/agpl-3.0.en.html
+//
+// You can operate outside the AGPL restrictions by purchasing
+// Documize Enterprise Edition and obtaining a commercial license
+// by contacting .
+//
+// https://documize.com
+
+package trello
+
+const graphsTemplate = `
+{{if false}}
+
+ Single Boards (graphs)
+ {{range $b := .Boards}}
+
+
There are {{ $b.CardCount }} cards across {{ $b.ListCount }} lists for board {{$b.Board.Name}}.
+
+ {{range $data := $b.Data}}
+
+
{{ $data.List.Name }}
+
+ {{end}}
+
+
+ {{end}}
+
+{{end}}
+`
diff --git a/core/section/trello/labels_template.go b/core/section/trello/labels_template.go
new file mode 100644
index 00000000..febe8f5e
--- /dev/null
+++ b/core/section/trello/labels_template.go
@@ -0,0 +1,37 @@
+// Copyright 2016 Documize Inc. . All rights reserved.
+//
+// This software (Documize Community Edition) is licensed under
+// GNU AGPL v3 http://www.gnu.org/licenses/agpl-3.0.en.html
+//
+// You can operate outside the AGPL restrictions by purchasing
+// Documize Enterprise Edition and obtaining a commercial license
+// by contacting .
+//
+// https://documize.com
+
+package trello
+
+const labelsTemplate = `
+
+ {{if gt (len .SharedLabels) 0}}
+
Labels
+
There are {{len .SharedLabels}} common labels across the boards.
+
+
+
+ {{range $l := .SharedLabels}}
+
+
+ {{ $l.Name }} ({{len $l.Boards}})
+ |
+
+ {{range $idx, $brd := $l.Boards}}{{if gt $idx 0}}, {{end}}{{$brd.OrgName}}/{{$brd.Name}}{{end}}
+ |
+
+ {{end}}
+
+
+
+ {{end}}
+
+`
diff --git a/core/section/trello/master_template.go b/core/section/trello/master_template.go
new file mode 100644
index 00000000..ffba5b4f
--- /dev/null
+++ b/core/section/trello/master_template.go
@@ -0,0 +1,22 @@
+// Copyright 2016 Documize Inc. . All rights reserved.
+//
+// This software (Documize Community Edition) is licensed under
+// GNU AGPL v3 http://www.gnu.org/licenses/agpl-3.0.en.html
+//
+// You can operate outside the AGPL restrictions by purchasing
+// Documize Enterprise Edition and obtaining a commercial license
+// by contacting .
+//
+// https://documize.com
+
+package trello
+
+const renderTemplate = `Activity since {{.Since}} for boards:
+{{range $idx, $brd := .Boards}}{{if gt $idx 0}}, {{end}}{{$brd.Board.OrgName}}/{{$brd.Board.Name}}{{end}}.
` +
+ labelsTemplate +
+ boardsTemplate +
+ graphsTemplate +
+ membersTemplate +
+ archiveTemplate +
+ tradTemplate +
+ ``
diff --git a/core/section/trello/members_template.go b/core/section/trello/members_template.go
new file mode 100644
index 00000000..a209c39b
--- /dev/null
+++ b/core/section/trello/members_template.go
@@ -0,0 +1,44 @@
+// Copyright 2016 Documize Inc. . All rights reserved.
+//
+// This software (Documize Community Edition) is licensed under
+// GNU AGPL v3 http://www.gnu.org/licenses/agpl-3.0.en.html
+//
+// You can operate outside the AGPL restrictions by purchasing
+// Documize Enterprise Edition and obtaining a commercial license
+// by contacting .
+//
+// https://documize.com
+
+package trello
+
+const membersTemplate = `
+
+ {{if gt (len .Boards) 0}}
+
Members
+
+ There {{if eq 1 (len .MemberBoardAssign)}} is one member {{else}} are {{len .MemberBoardAssign}} members {{end}} assigned to {{.CardAssignTotal}} cards of the total {{.CardTotal}} cards across {{len .Boards}} boards.
+
+
+
+
+
+ {{range $m := .MemberBoardAssign}}
+
+
+
+ |
+
+ {{$m.MemberName}}
+
+ {{range $idx, $ac := $m.AssignCounts}}{{if gt $idx 0}}, {{end}}{{$ac.BoardName}} ({{$ac.Count}}){{end}}
+
+
+ |
+
+ {{end}}
+
+
+
+ {{end}}
+
+`
diff --git a/core/section/trello/model.go b/core/section/trello/model.go
index 34f70236..5d4da504 100644
--- a/core/section/trello/model.go
+++ b/core/section/trello/model.go
@@ -11,39 +11,25 @@
package trello
-import "strings"
-
-const renderTemplate = `
-Non-printable
-
-
There are {{ .CardCount }} cards across {{ .ListCount }} lists for board {{.Board.Name}}.
-
-
-`
+import (
+ "strings"
+ "time"
+)
type secrets struct {
- Token string `json:"token"`
+ Token string `json:"token"`
}
type trelloConfig struct {
- AppKey string `json:"appKey"`
- Token string `json:"token"`
- Board trelloBoard `json:"board"`
- Lists []trelloList `json:"lists"`
+ AppKey string `json:"appKey"`
+ Token string `json:"token"`
+ Board trelloBoard `json:"board"`
+ Lists []trelloList `json:"lists"`
+ Boards []trelloBoard `json:"boards"`
+ Since string `json:"since,omitempty"`
+ SincePtr *time.Time `json:"-"`
+
+ OrgByID map[string]trelloOrganization `json:"-"`
}
func (c *trelloConfig) Clean() {
@@ -52,6 +38,73 @@ func (c *trelloConfig) Clean() {
}
// Trello objects based upon https://github.com/VojtechVitek/go-trello
+
+type trelloOrganization struct {
+ ID string `json:"id"`
+ Name string `json:"name"`
+ DisplayName string `json:"displayName"`
+ Desc string `json:"desc"`
+ DescData string `json:"descData"`
+ URL string `json:"url"`
+ Website string `json:"website"`
+ LogoHash string `json:"logoHash"`
+ Products []string `json:"products"`
+ PowerUps []string `json:"powerUps"`
+}
+
+type trelloAction struct {
+ ID string `json:"id"`
+ IDMemberCreator string `json:"idMemberCreator"`
+ Data struct {
+ DateLastEdited string `json:"dateLastEdited"`
+ ListBefore struct {
+ ID string `json:"id"`
+ Name string `json:"name"`
+ } `json:"listBefore"`
+ ListAfter struct {
+ ID string `json:"id"`
+ Name string `json:"name"`
+ } `json:"listAfter"`
+ CheckItem struct {
+ ID string `json:"id"`
+ State string `json:"state"`
+ Name string `json:"name"`
+ } `json:"checkItem"`
+ CheckList struct {
+ ID string `json:"id"`
+ Name string `json:"name"`
+ } `json:"checklist"`
+ List struct {
+ ID string `json:"id"`
+ Name string `json:"name"`
+ } `json:"list"`
+ TextData struct {
+ Emoji struct{} `json:"emoji"`
+ } `json:"textData"`
+ Board struct {
+ ID string `json:"id"`
+ Name string `json:"name"`
+ ShortLink string `json:"shortLink"`
+ } `json:"board"`
+ Card struct {
+ ID string `json:"id"`
+ Name string `json:"name"`
+ ShortLink string `json:"shortLink"`
+ IDShort int `json:"idShort"`
+ } `json:"card"`
+ Text string `json:"text"`
+ } `json:"data"`
+ Type string `json:"type"`
+ Date string `json:"date"`
+ MemberCreator struct {
+ ID string `json:"id"`
+ AvatarHash string `json:"avatarHash"`
+ FullName string `json:"fullName"`
+ Initials string `json:"initials"`
+ Username string `json:"username"`
+ } `json:"memberCreator"`
+}
+
type trelloMember struct {
ID string `json:"id"`
AvatarHash string `json:"avatarHash"`
@@ -94,6 +147,7 @@ type trelloBoard struct {
Name string `json:"name"`
Closed bool `json:"closed"`
OrganizationID string `json:"idOrganization"`
+ OrgName string `json:"orgName"`
Pinned bool `json:"pinned"`
URL string `json:"url"`
ShortURL string `json:"shortUrl"`
@@ -129,6 +183,8 @@ type trelloBoard struct {
Blue string `json:"blue"`
Purple string `json:"purple"`
} `json:"labelNames"`
+ Included bool `json:"included"` // indicates whether we display this board
+ NamePath string `json:"namePath"` // the "team / board" form
}
type trelloBoardBackground struct {
@@ -197,9 +253,43 @@ type trelloListCards struct {
Cards []trelloCard
}
-type trelloRender struct {
- Board trelloBoard
- Data []trelloListCards
- CardCount int
- ListCount int
+type trelloRenderBoard struct {
+ Board trelloBoard
+ Data []trelloListCards
+ CardCount int
+ ListCount int
+ Actions []trelloAction
+ ActionSummary map[string]int
+ Archived []trelloCard
+}
+
+type trelloSharedLabel struct {
+ Name string
+ Color string
+ Boards []trelloBoard
+}
+
+type trelloBoardAssignCount struct {
+ BoardName string
+ Count int
+}
+
+type trelloBoardAssign struct {
+ AvatarHash string
+ MemberName string
+ AssignCounts []trelloBoardAssignCount
+}
+
+type trelloRender struct {
+ Boards []trelloRenderBoard
+ Since string
+ Detail trelloRenderBoard
+
+ // items below are generated during the render phase
+ SharedLabels []trelloSharedLabel
+ MembersByID map[string]trelloMember
+ MemberBoardAssign []trelloBoardAssign
+ CardAssignTotal int
+ CardTotal int
+ ListTotal int
}
diff --git a/core/section/trello/trad_template.go b/core/section/trello/trad_template.go
new file mode 100644
index 00000000..2287bde4
--- /dev/null
+++ b/core/section/trello/trad_template.go
@@ -0,0 +1,40 @@
+// Copyright 2016 Documize Inc. . All rights reserved.
+//
+// This software (Documize Community Edition) is licensed under
+// GNU AGPL v3 http://www.gnu.org/licenses/agpl-3.0.en.html
+//
+// You can operate outside the AGPL restrictions by purchasing
+// Documize Enterprise Edition and obtaining a commercial license
+// by contacting .
+//
+// https://documize.com
+
+package trello
+
+const tradTemplate = `
+
+ {{if ne .Detail.Board.ID ""}}
+
{{.Detail.Board.Name}} Board
+
Non-printable
+
+
There are {{ .Detail.CardCount }} cards across {{ .Detail.ListCount }} lists
+ for board {{.Detail.Board.Name}}.
+
+
+ {{end}}
+
+`
diff --git a/core/section/trello/trello.go b/core/section/trello/trello.go
index de6c3fc5..23ed12b3 100644
--- a/core/section/trello/trello.go
+++ b/core/section/trello/trello.go
@@ -18,10 +18,14 @@ import (
"html/template"
"io/ioutil"
"net/http"
+ "sort"
+ "strings"
+ "time"
+ "unicode"
"github.com/documize/community/core/api/request"
- "github.com/documize/community/core/section/provider"
"github.com/documize/community/core/log"
+ "github.com/documize/community/core/section/provider"
)
var meta provider.TypeMeta
@@ -98,7 +102,7 @@ func (*Provider) Command(ctx *provider.Context, w http.ResponseWriter, r *http.R
provider.WriteJSON(w, render)
case "boards":
- render, err := getBoards(config)
+ render, err := getBoards(&config)
if err != nil {
log.IfErr(err)
@@ -147,28 +151,32 @@ func (*Provider) Command(ctx *provider.Context, w http.ResponseWriter, r *http.R
log.IfErr(ctx.SaveSecrets(string(b)))
}
-// Render just sends back HMTL as-is.
+// Render the payload using the template.
func (*Provider) Render(ctx *provider.Context, config, data string) string {
- raw := []trelloListCards{}
- payload := trelloRender{}
+ var payload = trelloRender{}
var c = trelloConfig{}
- json.Unmarshal([]byte(data), &raw)
+ json.Unmarshal([]byte(data), &payload)
json.Unmarshal([]byte(config), &c)
- payload.Board = c.Board
- payload.Data = raw
- payload.ListCount = len(raw)
-
- for _, list := range raw {
- payload.CardCount += len(list.Cards)
- }
+ buildPayloadAnalysis(&c, &payload)
t := template.New("trello")
- t, _ = t.Parse(renderTemplate)
+ var err error
+ t, err = t.Parse(renderTemplate)
+
+ if err != nil {
+ log.IfErr(err)
+ return ""
+ }
buffer := new(bytes.Buffer)
- t.Execute(buffer, payload)
+ err = t.Execute(buffer, payload)
+
+ if err != nil {
+ log.IfErr(err)
+ return ""
+ }
return buffer.String()
}
@@ -176,15 +184,80 @@ func (*Provider) Render(ctx *provider.Context, config, data string) string {
// Refresh just sends back data as-is.
func (*Provider) Refresh(ctx *provider.Context, config, data string) string {
var c = trelloConfig{}
- json.Unmarshal([]byte(config), &c)
+ log.IfErr(json.Unmarshal([]byte(config), &c))
- refreshed, err := getCards(c)
+ save := trelloRender{}
+ save.Boards = make([]trelloRenderBoard, 0, len(c.Boards))
- if err != nil {
- return data
+ if len(c.Since) >= len("yyyy/mm/dd hh:ss") {
+ var since time.Time
+ tt := []byte("yyyy-mm-ddThh:mm:00Z")
+ for _, i := range []int{0, 1, 2, 3, 5, 6, 8, 9, 11, 12, 14, 15} {
+ tt[i] = c.Since[i]
+ }
+ err := since.UnmarshalText(tt)
+ if err != nil {
+ log.ErrorString("Date unmarshall '" + c.Since + "'->'" + string(tt) + "' error: " + err.Error())
+ } else {
+ c.SincePtr = &since
+ }
+ }
+ dateMessage := ""
+ if c.SincePtr == nil {
+ dateMessage = " (the last 7 days)"
+ since := time.Now().AddDate(0, 0, -7)
+ c.SincePtr = &since
+ c.Since = (*c.SincePtr).Format("2006/01/02 ")
+ }
+ save.Since = (*c.SincePtr).Format("January 2, 2006") + dateMessage
+
+ c.AppKey = request.ConfigString(meta.ConfigHandle(), "appKey")
+
+ if c.Board.ID != "" { // set up detail board
+ var err error
+ save.Detail.Board = c.Board
+ save.Detail.Data, err = getCards(c)
+ log.IfErr(err)
+ save.Detail.ListCount = len(save.Detail.Data)
+ for _, list := range save.Detail.Data {
+ save.Detail.CardCount += len(list.Cards)
+ }
}
- j, err := json.Marshal(refreshed)
+ for _, board := range c.Boards {
+ if board.Included && board.ID != "" {
+ var payload = trelloRenderBoard{}
+
+ c.Board = board
+
+ lsts, err := getLists(c)
+ log.IfErr(err)
+ if err == nil {
+ c.Lists = lsts
+ }
+
+ for l := range c.Lists {
+ c.Lists[l].Included = true
+ }
+
+ refreshed, err := getCards(c)
+ log.IfErr(err)
+
+ payload.Board = c.Board
+ payload.Data = refreshed
+ payload.ListCount = len(refreshed)
+
+ for _, list := range refreshed {
+ payload.CardCount += len(list.Cards)
+ }
+
+ payload.Actions, payload.Archived = fetchBoardActions(&c, &save, board.ID, c.Since)
+
+ save.Boards = append(save.Boards, payload)
+ }
+ }
+
+ j, err := json.Marshal(save)
if err != nil {
log.Error("unable to marshall trello cards", err)
@@ -195,8 +268,48 @@ func (*Provider) Refresh(ctx *provider.Context, config, data string) string {
}
// Helpers
-func getBoards(config trelloConfig) (boards []trelloBoard, err error) {
- req, err := http.NewRequest("GET", fmt.Sprintf("https://api.trello.com/1/members/me/boards?fields=id,name,url,closed,prefs,idOrganization&key=%s&token=%s", config.AppKey, config.Token), nil)
+
+func getOrg(config *trelloConfig, orgID string) (*trelloOrganization, error) {
+ if config.OrgByID == nil {
+ config.OrgByID = make(map[string]trelloOrganization)
+ }
+ if org, found := config.OrgByID[orgID]; found {
+ return &org, nil
+ }
+ req, err := http.NewRequest("GET", fmt.Sprintf(
+ "https://api.trello.com/1/organizations/%s?fields=name,desc&key=%s&token=%s",
+ orgID, config.AppKey, config.Token), nil)
+ log.IfErr(err)
+ client := &http.Client{}
+ res, err := client.Do(req)
+
+ if err != nil {
+ return nil, err
+ }
+
+ if res.StatusCode != http.StatusOK {
+ return nil, fmt.Errorf("error: HTTP status code %d", res.StatusCode)
+ }
+
+ b := trelloOrganization{}
+
+ defer res.Body.Close()
+ dec := json.NewDecoder(res.Body)
+ err = dec.Decode(&b)
+ if err != nil {
+ fmt.Println(err)
+ return nil, err
+ }
+
+ config.OrgByID[orgID] = b
+ return &b, nil
+}
+
+func getBoards(config *trelloConfig) (boards []trelloBoard, err error) {
+ req, err := http.NewRequest("GET", fmt.Sprintf(
+ "https://api.trello.com/1/members/me/boards?fields=id,name,url,closed,prefs,idOrganization&key=%s&token=%s",
+ config.AppKey, config.Token), nil)
+ log.IfErr(err)
client := &http.Client{}
res, err := client.Do(req)
@@ -213,24 +326,45 @@ func getBoards(config trelloConfig) (boards []trelloBoard, err error) {
defer res.Body.Close()
dec := json.NewDecoder(res.Body)
err = dec.Decode(&b)
+ if err != nil {
+ fmt.Println(err)
+ return nil, err
+ }
// we only show open, team boards (not personal)
for _, b := range b {
if !b.Closed && len(b.OrganizationID) > 0 {
+ if o, e := getOrg(config, b.OrganizationID); e == nil {
+ b.OrgName = o.Name
+ b.NamePath = o.Name + " / " + b.Name
+ } else {
+ log.Error("failed to get organisation infomation", e)
+ }
boards = append(boards, b)
}
}
- if err != nil {
- fmt.Println(err)
- return nil, err
+ for bx, bd := range boards {
+ for _, cd := range config.Boards {
+ if bd.ID == cd.ID {
+ boards[bx].Included = cd.Included // to pick up the previous selection or not
+ goto foundID
+ }
+ }
+ boards[bx].Included = true // include boards by default
+ foundID:
}
return boards, nil
}
func getLists(config trelloConfig) (lists []trelloList, err error) {
- req, err := http.NewRequest("GET", fmt.Sprintf("https://api.trello.com/1/boards/%s/lists/open?key=%s&token=%s", config.Board.ID, config.AppKey, config.Token), nil)
+ if config.Board.ID == "" {
+ return
+ }
+ uri := fmt.Sprintf("https://api.trello.com/1/boards/%s/lists/open?key=%s&token=%s", config.Board.ID, config.AppKey, config.Token)
+ req, err := http.NewRequest("GET", uri, nil)
+ log.IfErr(err)
client := &http.Client{}
res, err := client.Do(req)
@@ -264,6 +398,7 @@ func getCards(config trelloConfig) (listCards []trelloListCards, err error) {
}
req, err := http.NewRequest("GET", fmt.Sprintf("https://api.trello.com/1/lists/%s/cards?key=%s&token=%s", list.ID, config.AppKey, config.Token), nil)
+ log.IfErr(err)
client := &http.Client{}
res, err := client.Do(req)
@@ -294,3 +429,247 @@ func getCards(config trelloConfig) (listCards []trelloListCards, err error) {
return listCards, nil
}
+
+func fetchMember(config *trelloConfig, render *trelloRender, memberID string) (memberInfo trelloMember) {
+ memberInfo.FullName = "(unknown)"
+
+ if render.MembersByID == nil {
+ render.MembersByID = make(map[string]trelloMember)
+ }
+ found := false
+ if memberInfo, found = render.MembersByID[memberID]; found {
+ return
+ }
+ render.MembersByID[memberID] = memberInfo // write unknown, so that we do not retry on errors
+
+ if len(config.AppKey) == 0 {
+ config.AppKey = request.ConfigString(meta.ConfigHandle(), "appKey")
+ }
+ uri := fmt.Sprintf("https://api.trello.com/1/members/%s?key=%s&token=%s", memberID, config.AppKey, config.Token)
+ req, err := http.NewRequest("GET", uri, nil)
+ if err != nil {
+ log.IfErr(err)
+ return
+ }
+ client := &http.Client{}
+ res, err := client.Do(req)
+ if err != nil {
+ log.IfErr(err)
+ return
+ }
+
+ if res.StatusCode != http.StatusOK {
+ log.ErrorString("Trello fetch member HTTP status not OK")
+ return
+ }
+
+ defer res.Body.Close()
+
+ dec := json.NewDecoder(res.Body)
+ err = dec.Decode(&memberInfo)
+ if err != nil {
+ log.IfErr(err)
+ return
+ }
+
+ render.MembersByID[memberID] = memberInfo
+
+ return
+}
+
+func fetchBoardActions(config *trelloConfig, render *trelloRender, boardID string, since string) (actions []trelloAction, archived []trelloCard) {
+
+ sinceString := since[:10]
+
+ if len(config.AppKey) == 0 {
+ config.AppKey = request.ConfigString(meta.ConfigHandle(), "appKey")
+ }
+
+ {
+ uri := fmt.Sprintf("https://api.trello.com/1/boards/%s/actions?limit=1000&since=%s&key=%s&token=%s", boardID, sinceString, config.AppKey, config.Token)
+
+ req, err := http.NewRequest("GET", uri, nil)
+ if err != nil {
+ log.IfErr(err)
+ return
+ }
+ client := &http.Client{}
+ res, err := client.Do(req)
+ if err != nil {
+ log.IfErr(err)
+ return
+ }
+
+ if res.StatusCode != http.StatusOK {
+ log.ErrorString("Trello fetch board actions HTTP status not OK")
+ return
+ }
+
+ defer res.Body.Close()
+
+ dec := json.NewDecoder(res.Body)
+ err = dec.Decode(&actions)
+ if err != nil {
+ log.IfErr(err)
+ return
+ }
+ }
+
+ {
+ uri := fmt.Sprintf("https://api.trello.com/1/boards/%s/cards?filter=closed&since=%s&key=%s&token=%s",
+ boardID, sinceString, config.AppKey, config.Token)
+ req, err := http.NewRequest("GET", uri, nil)
+ if err != nil {
+ log.IfErr(err)
+ return
+ }
+ client := &http.Client{}
+ res, err := client.Do(req)
+ if err != nil {
+ log.IfErr(err)
+ return
+ }
+
+ if res.StatusCode != http.StatusOK {
+ msg := ""
+ txt, err := ioutil.ReadAll(res.Body)
+ if err == nil {
+ msg = string(txt)
+ } else {
+ msg = err.Error()
+ }
+ log.ErrorString("Trello fetch board archived HTTP status not OK - " + msg)
+ return
+ }
+
+ defer res.Body.Close()
+
+ dec := json.NewDecoder(res.Body)
+ err = dec.Decode(&archived)
+ if err != nil {
+ log.IfErr(err)
+ return
+ }
+ }
+
+ return
+}
+
+func buildPayloadAnalysis(config *trelloConfig, render *trelloRender) {
+
+ //totals
+ render.CardTotal = 0
+ render.CardAssignTotal = 0
+ render.ListTotal = 0
+
+ // pre-process labels
+ type labT struct {
+ color string
+ boards map[string]trelloBoard
+ }
+ labels := make(map[string]labT)
+
+ // pre-process member stats
+ memberBoardCount := make(map[string]map[string]int)
+
+ // main loop
+ for brdIdx, brd := range render.Boards {
+ for _, lst := range brd.Data {
+ render.ListTotal++
+ for _, crd := range lst.Cards {
+ render.CardTotal++
+ if len(crd.MembersID) > 0 {
+ render.CardAssignTotal++
+ }
+
+ // process labels
+ for _, lab := range crd.Labels {
+ if _, exists := labels[lab.Name]; !exists {
+ labels[lab.Name] = labT{color: lab.Color, boards: make(map[string]trelloBoard)}
+ }
+ labels[lab.Name].boards[brd.Board.URL+" / "+brd.Board.Name] = brd.Board
+ }
+
+ // process member stats
+ for _, mem := range crd.MembersID {
+ if _, exists := memberBoardCount[mem]; !exists {
+ memberBoardCount[mem] = make(map[string]int)
+ }
+ memberBoardCount[mem][brd.Board.ID]++
+ }
+ }
+ }
+
+ // ActionSummary
+ if render.Boards[brdIdx].ActionSummary == nil {
+ render.Boards[brdIdx].ActionSummary = make(map[string]int)
+ }
+ for _, act := range brd.Actions {
+ englishType := ""
+ for _, c := range act.Type {
+ if unicode.IsUpper(c) {
+ englishType += " "
+ englishType += string(unicode.ToLower(c))
+ } else {
+ englishType += string(c)
+ }
+ }
+ englishType = strings.Replace(englishType, "organization", "team", -1)
+ if newTxt, found := activityTranslation[englishType]; found {
+ englishType = newTxt
+ }
+ if len(englishType) > 0 {
+ render.Boards[brdIdx].ActionSummary[englishType]++
+ }
+ }
+ }
+
+ //post-process labels
+ labs := make([]string, 0, len(labels))
+ for lname := range labels {
+ labs = append(labs, lname)
+ }
+ sort.Strings(labs)
+ for _, lname := range labs {
+ thisLabel := labels[lname].boards
+ if l := len(thisLabel); l > 1 {
+ brds := make([]string, 0, l)
+ for bname := range thisLabel {
+ brds = append(brds, bname)
+ }
+ sort.Strings(brds)
+ lbrds := []trelloBoard{}
+ for _, h := range brds {
+ lbrds = append(lbrds, labels[lname].boards[h])
+ }
+ render.SharedLabels = append(render.SharedLabels, trelloSharedLabel{
+ Name: lname, Color: labels[lname].color, Boards: lbrds,
+ })
+ }
+ }
+
+ //post-process member stats
+ mNames := make([]string, 0, len(memberBoardCount))
+ for mID := range memberBoardCount {
+ memInfo := fetchMember(config, render, mID)
+ mNames = append(mNames, memInfo.FullName)
+ }
+ sort.Strings(mNames)
+ for _, mNam := range mNames {
+ for mem, brdCounts := range memberBoardCount {
+ memInfo := fetchMember(config, render, mem)
+ if mNam == memInfo.FullName {
+ render.MemberBoardAssign = append(render.MemberBoardAssign, trelloBoardAssign{MemberName: mNam, AvatarHash: memInfo.AvatarHash})
+ for _, b := range render.Boards { // these are already in order
+ if count, ok := brdCounts[b.Board.ID]; ok {
+ render.MemberBoardAssign[len(render.MemberBoardAssign)-1].AssignCounts =
+ append(render.MemberBoardAssign[len(render.MemberBoardAssign)-1].AssignCounts,
+ trelloBoardAssignCount{BoardName: b.Board.Name, Count: count})
+ }
+ }
+ goto found
+ }
+ }
+ found:
+ }
+}