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}} +
-
-
-
-
Select Board & Lists
-
Choose lists to include from board
+ {{#if authenticated}} + {{#if noBoards}} +
+
You have no team boards to share - personal boards are never shown.
- {{#if noBoards}} -
-
You have no team boards to share - personal boards are never shown.
-
- {{else}} -
- -
Select board
- {{ui-select id="boards-dropdown" content=boards action=(action 'onBoardChange') optionValuePath="id" optionLabelPath="name" selection=config.board}} + {{else}} + +
+
+
+
Select Board & Lists
+
Choose lists to include from board
- -
Select lists to include
-
-
{{config.board.name}}
- {{#each config.lists as |list|}} -
- {{#if list.included}} - check_box - {{else}} - check_box_outline_blank + + {{input id="trello-since" value=config.since type="text" }}
+
+ +
+ +
All boards are selectd by default
+
+ {{#each config.boards as |board|}} + {{#if board.id}} +
+ {{#if board.included}} + check_box + {{else}} + check_box_outline_blank + {{/if}} + {{board.orgName}} / {{board.name}} +
{{/if}} - {{list.name}} -
- {{/each}} -
-
+ {{/each}} +
+
- {{/if}} +
-
+ +
 
+ +
+
+
+ +
Select board
+ {{ui-select id="boards-dropdown" content=boards action=(action 'onBoardChange') optionValuePath="id" optionLabelPath="namePath" selection=config.board}} +
+ {{#if config.board.id}} +
+ +
Select lists to include
+
+
{{config.board.name}}
+ {{#each config.lists as |list|}} +
+ {{#if list.included}} + check_box + {{else}} + check_box_outline_blank + {{/if}} + {{list.name}} +
+ {{/each}} +
+
+
+ {{/if}} +
+
+ {{/if}} {{else}} @@ -57,5 +92,5 @@
{{/if}} - +
{{/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}} + + + + + {{end}} + +
+ + {{$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}} +` 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}} + + + + + {{end}} + +
+ +
+ {{$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}} +
+` 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}} + + + + + {{end}} + +
+ {{ $l.Name }} ({{len $l.Boards}}) + + {{range $idx, $brd := $l.Boards}}{{if gt $idx 0}}, {{end}}{{$brd.OrgName}}/{{$brd.Name}}{{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}} + + + + + {{end}} + +
+ Member Avatar + +
{{$m.MemberName}}
+
+ {{range $idx, $ac := $m.AssignCounts}}{{if gt $idx 0}}, {{end}}{{$ac.BoardName}} ({{$ac.Count}}){{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}}.

-
-
{{.Board.Name}}
- {{range $data := .Data}} -
-
{{ $data.List.Name }}
- {{range $card := $data.Cards}} - -
- {{ $card.Name }} -
-
- {{end}} -
- {{end}} -
-
-` +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}}.

+
+
{{.Detail.Board.Name}}
+ {{range $data := .Detail.Data}} +
+
{{ $data.List.Name }}
+ {{range $card := $data.Cards}} + +
+ {{ $card.Name }} +
+
+ {{end}} +
+ {{end}} +
+
+ {{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: + } +}