├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── bower.json ├── package.json ├── requirements.txt ├── server.py └── src ├── assets ├── css │ └── styles.css └── images │ ├── github_ribbon.png │ ├── github_ribbon.xcf │ ├── homepage │ ├── arrow.svg │ ├── astronaut.svg │ ├── card.svg │ ├── hp1.svg │ ├── hp2.svg │ ├── hp3.svg │ ├── hp4.svg │ ├── hp5.svg │ ├── letter.svg │ ├── logo-128.png │ ├── logo-16.png │ ├── logo-32.png │ ├── logo-64.png │ ├── logo.png │ ├── logo.svg │ ├── monitor.svg │ ├── mouse.svg │ └── shield.svg │ ├── issue_details.png │ ├── login.png │ ├── milestones.png │ ├── organizations.png │ └── repositories.png ├── chrome-app ├── js │ └── loader.js └── manifest.json ├── chrome ├── js │ └── loader.js └── manifest.json ├── js ├── api │ ├── all.js │ └── github │ │ ├── authorization.js │ │ ├── issue.js │ │ ├── label.js │ │ ├── milestone.js │ │ ├── organization.js │ │ ├── repository.js │ │ └── user.js ├── build.js ├── components │ ├── app.jsx │ ├── generic │ │ ├── flash_messages.jsx │ │ └── modal.jsx │ ├── header.jsx │ ├── menu.jsx │ ├── milestones.jsx │ ├── mixins │ │ ├── form.jsx │ │ ├── github_error_handler.jsx │ │ ├── loader.jsx │ │ └── tabs.jsx │ ├── organizations.jsx │ ├── repositories.jsx │ ├── sprintboard.jsx │ └── user │ │ ├── login.jsx │ │ └── logout.jsx ├── config.js ├── flash_messages.js ├── helpers │ └── issue_manager.js ├── main.js ├── request_notifier.js ├── routes.js ├── settings.js ├── settings_hashtag_navigation.js ├── settings_html5_navigation.js ├── subject.js └── utils.js ├── scss ├── _above_the_fold.scss ├── _below_the_fold.scss ├── main.scss ├── modules │ ├── _border_radius.scss │ ├── _colors.scss │ ├── _mixins.scss │ ├── _spaces.scss │ └── _typography.scss └── partials │ ├── _milestones.scss │ ├── _modal.scss │ ├── _organizations.scss │ ├── _repositories.scss │ ├── _request_indicator.scss │ └── _sprintboard.scss └── templates ├── app.html └── index.html /.gitignore: -------------------------------------------------------------------------------- 1 | **/node_modules 2 | npm*log 3 | venv/* 4 | **/build/* 5 | **/bower_components/* 6 | **/.sass-cache/* 7 | **/sass-cache/* 8 | frontend/build 9 | *.tmp 10 | **/.module-cache/ 11 | *.swp 12 | *.orig 13 | *.local 14 | *.pyc 15 | *~ 16 | .checkmate/* 17 | *# 18 | .npmignore 19 | **/.sass-cache 20 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | ## Copyright (c) 2015 - Andreas Dewes 4 | ## 5 | ## This file is part of Gitboard. 6 | ## 7 | ## Gitboard is free software: you can redistribute it and/or modify 8 | ## it under the terms of the GNU Affero General Public License as 9 | ## published by the Free Software Foundation, either version 3 of the 10 | ## License, or (at your option) any later version. 11 | ## 12 | ## This program is distributed in the hope that it will be useful, 13 | ## but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | ## GNU Affero General Public License for more details. 16 | ## 17 | ## You should have received a copy of the GNU Affero General Public License 18 | ## along with this program. If not, see . 19 | 20 | BUILD_DIR=build 21 | SOURCE_DIR=src 22 | 23 | CSS_FILES = /css/main.css 24 | 25 | export PATH := ./node_modules/.bin:$(PATH); 26 | 27 | ifeq ($(ENVIRONMENT),production) 28 | BUILD_ENVIRONMENT=production 29 | else 30 | BUILD_ENVIRONMENT=development 31 | endif 32 | 33 | ifeq ($(NAVIGATION),html5) 34 | ENV_SETTINGS=settings_html5_navigation.js 35 | else 36 | ENV_SETTINGS=settings_hashtag_navigation.js 37 | endif 38 | 39 | all: $(BUILD_ENVIRONMENT) 40 | 41 | clean: 42 | rm -rf $(BUILD_DIR) 43 | 44 | chrome-development: npm bower assets scripts jsx templates env_settings scss chrome watch 45 | 46 | chrome-production: npm bower assets scripts jsx templates env_settings scss chrome optimize 47 | 48 | chrome-app-development: npm bower assets scripts jsx templates env_settings scss chrome-app watch 49 | 50 | chrome-app-production: npm bower assets scripts jsx templates env_settings scss chrome-app optimize 51 | 52 | production: npm bower assets scripts jsx templates env_settings scss optimize 53 | 54 | development: npm bower assets scripts jsx templates env_settings scss watch 55 | 56 | optimize: optimize-css optimize-rjs 57 | 58 | npm: 59 | npm install 60 | 61 | scss: $(SOURCE_DIR)/scss/main.scss 62 | mkdir -p $(BUILD_DIR)/static/css 63 | scss $(SOURCE_DIR)/scss/main.scss $(BUILD_DIR)/static/css/main.css 64 | 65 | chrome: 66 | cp $(SOURCE_DIR)/chrome/* $(BUILD_DIR) -rf 67 | 68 | chrome-app: 69 | cp $(SOURCE_DIR)/chrome-app/* $(BUILD_DIR) -rf 70 | 71 | env_settings: 72 | cp $(SOURCE_DIR)/js/$(ENV_SETTINGS) $(BUILD_DIR)/static/js/env_settings.js 73 | 74 | optimize-css: 75 | mkdir -p $(BUILD_DIR)/static/css 76 | cleancss -o $(BUILD_DIR)/static/css/main.css $(addprefix $(BUILD_DIR)/static,$(CSS_FILES)) 77 | 78 | optimize-rjs: 79 | r.js -o $(BUILD_DIR)/static/js/build.js 80 | 81 | scripts: 82 | mkdir -p $(BUILD_DIR)/static/js 83 | rsync -rupE $(SOURCE_DIR)/js --include="*.js" $(BUILD_DIR)/static 84 | 85 | templates: 86 | mkdir -p $(BUILD_DIR) 87 | rsync -rupE $(SOURCE_DIR)/templates/ --include="*.html" $(BUILD_DIR) 88 | 89 | jsx: 90 | jsx $(SOURCE_DIR)/js $(BUILD_DIR)/static/js -x jsx 91 | 92 | assets: 93 | rsync -rupE $(SOURCE_DIR)/assets $(BUILD_DIR)/static 94 | 95 | .PHONY: scripts 96 | 97 | bower: 98 | mkdir -p $(BUILD_DIR)/static 99 | bower install --config.directory=$(BUILD_DIR)/static/bower_components 100 | 101 | watch: 102 | @which inotifywait || (echo "Please install inotifywait";exit 2) 103 | @while true ; do \ 104 | inotifywait -r src -e create,delete,move,modify || break; \ 105 | ($(MAKE) assets scripts jsx templates chrome chrome-app scss) || break;\ 106 | done 107 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | #Welcome to Gitboard! 2 | 3 | Gitboard is an intuitive Kanban board for your Github issues. It runs securely in your browser 4 | and does not need an intermediate server. It is built using React.js, Bootstrap + Material Design, 5 | SASS and Require.js. 6 | 7 | [Go to Live Version](https://adewes.github.io/gitboard) 8 | 9 | ##License 10 | 11 | Gitboard is released under a *Affero General Public License (AGPL)*. 12 | 13 | ##Building Gitboard 14 | 15 | To build the source locally, just check out the repository, go to the main directory and run make: 16 | 17 | ```bash 18 | make 19 | ``` 20 | 21 | To perform optimizations for the production version, simply pass the `ENVIRONMENT` variable to make: 22 | 23 | ```bash 24 | ENVIRONMENT=production make 25 | ``` 26 | 27 | ##Development Requirements 28 | 29 | You should have working versions of `npm`, `make`, `sass` and `bower` on your computer. 30 | 31 | ## How to contribute 32 | 33 | Contributions are very welcome, and they are greatly appreciated! Every little bit helps, and credit will always be given. 34 | 35 | ## Reporting Bugs 36 | 37 | If you experience problems using Gitboard or building it locally, please open an issue. 38 | 39 | When reporting a bug, please include the following information (if applicable): 40 | 41 | * The trackeback of the error 42 | * Your operating system name and version 43 | * Any details about your local setup that might be helpful in troubleshooting 44 | * Detailed steps to reproduce the bug 45 | * A screenshots (if useful) 46 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gitboard", 3 | "version": "0.0.1", 4 | "dependencies": { 5 | "director": "1.2.2", 6 | "react": "latest", 7 | "jquery": "2.1.1", 8 | "requirejs" : "2.1.11", 9 | "bootstrap-material-design": "latest", 10 | "momentjs" : "2.6.0", 11 | "bootstrap" : "latest", 12 | "font-awesome" : "4.4.0", 13 | "react-bootstrap" : "0.13.0", 14 | "sprintf" : "1.0.2", 15 | "markdown" : "0.5.0", 16 | "marked" : "0.3.3", 17 | "font-mfizz" : "2.0.0", 18 | "octicons" : "latest", 19 | "requirejs" : "2.1.14" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { "name" : "gitboard", 2 | "version" : "0.1.0", 3 | "dependencies" : 4 | { "clean-css": "3.2.8", 5 | "bower" : "1.4.1", 6 | "react-tools" : "0.12", 7 | "requirejs" : "2.1.17" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | flask 2 | 3 | -------------------------------------------------------------------------------- /server.py: -------------------------------------------------------------------------------- 1 | from flask import (Flask, 2 | make_response, 3 | redirect, 4 | render_template, 5 | request) 6 | 7 | import os 8 | 9 | project_path = os.path.dirname(__file__) 10 | 11 | app = Flask( 12 | __name__, 13 | static_folder=os.path.join(project_path,'build/static'), 14 | static_url_path='/static', 15 | template_folder='src/templates', 16 | ) 17 | 18 | @app.route('/',defaults = {'path' : ''}) 19 | @app.route('/') 20 | def webapp(path): 21 | context = {} 22 | return make_response(render_template("index.html", **context)) 23 | 24 | 25 | if __name__ == '__main__': 26 | app.run(debug=True, host='0.0.0.0', port=8000,threaded = False) 27 | -------------------------------------------------------------------------------- /src/assets/css/styles.css: -------------------------------------------------------------------------------- 1 | .modal-backdrop{ 2 | z-index: -1; 3 | } 4 | 5 | .modal .modal-body { 6 | max-height: 420px !important; 7 | overflow: auto; 8 | z-index:1060; 9 | } 10 | 11 | .container-wide{ 12 | width:auto; 13 | max-width:1600px; 14 | min-width:1200px; 15 | padding:15px; 16 | margin:0 auto; 17 | } 18 | 19 | .navbar{ 20 | position:fixed; 21 | width:100%; 22 | margin-top:-60px; 23 | } 24 | 25 | .container{ 26 | width:auto; 27 | max-width:1600px; 28 | } 29 | 30 | body{ 31 | margin-top:60px !important; 32 | } 33 | -------------------------------------------------------------------------------- /src/assets/images/github_ribbon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adewes/gitboard/4824cf068a9352959b795fddb2093e6ef6b37f3f/src/assets/images/github_ribbon.png -------------------------------------------------------------------------------- /src/assets/images/github_ribbon.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adewes/gitboard/4824cf068a9352959b795fddb2093e6ef6b37f3f/src/assets/images/github_ribbon.xcf -------------------------------------------------------------------------------- /src/assets/images/homepage/arrow.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 18 | 20 | 42 | 44 | 45 | 47 | image/svg+xml 48 | 50 | 51 | 52 | 53 | 54 | 59 | 62 | 73 | 84 | 95 | 106 | 117 | 127 | 131 | 135 | 139 | 140 | 141 | 142 | -------------------------------------------------------------------------------- /src/assets/images/homepage/astronaut.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 18 | 20 | 42 | 44 | 45 | 47 | image/svg+xml 48 | 50 | 51 | 52 | 53 | 54 | 59 | 71 | 74 | 85 | 90 | 95 | 100 | 111 | 122 | 133 | 144 | 155 | 160 | 164 | 172 | 183 | 194 | 199 | 204 | 208 | 212 | 216 | 217 | 218 | 219 | -------------------------------------------------------------------------------- /src/assets/images/homepage/card.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 21 | 23 | 32 | 33 | 34 | 57 | 59 | 60 | 62 | image/svg+xml 63 | 65 | 66 | 67 | 68 | 69 | 74 | 86 | 89 | 91 | 100 | 101 | 110 | 112 | 121 | 122 | 128 | 129 | 134 | 139 | 151 | 163 | 170 | 177 | 184 | 189 | 190 | 191 | -------------------------------------------------------------------------------- /src/assets/images/homepage/letter.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 18 | 20 | 42 | 44 | 45 | 47 | image/svg+xml 48 | 50 | 51 | 52 | 53 | 54 | 59 | 71 | 76 | 81 | 86 | 91 | @ 97 | 104 | 111 | 118 | 125 | 132 | 133 | 134 | -------------------------------------------------------------------------------- /src/assets/images/homepage/logo-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adewes/gitboard/4824cf068a9352959b795fddb2093e6ef6b37f3f/src/assets/images/homepage/logo-128.png -------------------------------------------------------------------------------- /src/assets/images/homepage/logo-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adewes/gitboard/4824cf068a9352959b795fddb2093e6ef6b37f3f/src/assets/images/homepage/logo-16.png -------------------------------------------------------------------------------- /src/assets/images/homepage/logo-32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adewes/gitboard/4824cf068a9352959b795fddb2093e6ef6b37f3f/src/assets/images/homepage/logo-32.png -------------------------------------------------------------------------------- /src/assets/images/homepage/logo-64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adewes/gitboard/4824cf068a9352959b795fddb2093e6ef6b37f3f/src/assets/images/homepage/logo-64.png -------------------------------------------------------------------------------- /src/assets/images/homepage/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adewes/gitboard/4824cf068a9352959b795fddb2093e6ef6b37f3f/src/assets/images/homepage/logo.png -------------------------------------------------------------------------------- /src/assets/images/homepage/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 18 | 20 | 42 | 44 | 45 | 47 | image/svg+xml 48 | 50 | 51 | 52 | 53 | 54 | 59 | 62 | 66 | 71 | 76 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /src/assets/images/homepage/monitor.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 18 | 20 | 42 | 44 | 45 | 47 | image/svg+xml 48 | 50 | 51 | 52 | 53 | 54 | 59 | 71 | 76 | 80 | 87 | 98 | 103 | 115 | 127 | 139 | 146 | 149 | 154 | 155 | 162 | 169 | 176 | 183 | 190 | 197 | 204 | 211 | 216 | 217 | 218 | -------------------------------------------------------------------------------- /src/assets/images/homepage/mouse.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 21 | 23 | 32 | 33 | 35 | 44 | 45 | 46 | 68 | 70 | 71 | 73 | image/svg+xml 74 | 76 | 77 | 78 | 79 | 80 | 85 | 97 | 100 | 102 | 111 | 112 | 121 | 123 | 132 | 133 | 139 | 144 | 145 | 152 | 159 | 166 | 173 | 180 | 185 | 197 | 202 | 207 | 208 | 209 | -------------------------------------------------------------------------------- /src/assets/images/homepage/shield.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 18 | 20 | 42 | 44 | 45 | 47 | image/svg+xml 48 | 50 | 51 | 52 | 53 | 54 | 59 | 71 | 83 | 95 | 107 | 112 | 117 | 122 | 134 | 139 | 140 | 141 | -------------------------------------------------------------------------------- /src/assets/images/issue_details.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adewes/gitboard/4824cf068a9352959b795fddb2093e6ef6b37f3f/src/assets/images/issue_details.png -------------------------------------------------------------------------------- /src/assets/images/login.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adewes/gitboard/4824cf068a9352959b795fddb2093e6ef6b37f3f/src/assets/images/login.png -------------------------------------------------------------------------------- /src/assets/images/milestones.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adewes/gitboard/4824cf068a9352959b795fddb2093e6ef6b37f3f/src/assets/images/milestones.png -------------------------------------------------------------------------------- /src/assets/images/organizations.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adewes/gitboard/4824cf068a9352959b795fddb2093e6ef6b37f3f/src/assets/images/organizations.png -------------------------------------------------------------------------------- /src/assets/images/repositories.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adewes/gitboard/4824cf068a9352959b795fddb2093e6ef6b37f3f/src/assets/images/repositories.png -------------------------------------------------------------------------------- /src/chrome-app/js/loader.js: -------------------------------------------------------------------------------- 1 | chrome.app.runtime.onLaunched.addListener(function() { 2 | chrome.app.window.create('index.html',{ 3 | innerBounds : { 4 | minWidth : 800, 5 | minHeight: 600 6 | } 7 | }); 8 | }); -------------------------------------------------------------------------------- /src/chrome-app/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Gitboard", 3 | "description": "Gitboard is a simple and intuitive Kanban board for your Github issues.", 4 | "version": "0.1", 5 | "manifest_version" : 2, 6 | "app" : { 7 | "background": { 8 | "scripts": ["js/loader.js"] 9 | } 10 | }, 11 | "permissions": [ 12 | "storage" 13 | ], 14 | "icons": {"16": "static/assets/images/homepage/logo-16.png", 15 | "128": "static/assets/images/homepage/logo-128.png"} 16 | } -------------------------------------------------------------------------------- /src/chrome/js/loader.js: -------------------------------------------------------------------------------- 1 | chrome.browserAction.onClicked.addListener(function(tab) { 2 | chrome.tabs.create({'url': chrome.extension.getURL('app.html'), 'selected': true}); 3 | }); -------------------------------------------------------------------------------- /src/chrome/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Gitboard", 3 | "description": "Gitboard is a simple and intuitive Kanban board for your Github issues.", 4 | "version": "0.1", 5 | "manifest_version" : 2, 6 | "background": { 7 | "scripts": ["js/loader.js"] 8 | }, 9 | "browser_action" : { 10 | "default_title": "launch Gitboard", 11 | "default_icon": "static/assets/images/homepage/logo-32.png" 12 | }, 13 | "permissions": [ 14 | "storage" 15 | ], 16 | "icons": {"16": "static/assets/images/homepage/logo-16.png", 17 | "128": "static/assets/images/homepage/logo-128.png"} 18 | } -------------------------------------------------------------------------------- /src/js/api/all.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2015 - Andreas Dewes 3 | 4 | This file is part of Gitboard. 5 | 6 | Gitboard is free software: you can redistribute it and/or modify 7 | it under the terms of the GNU Affero General Public License as 8 | published by the Free Software Foundation, either version 3 of the 9 | License, or (at your option) any later version. 10 | 11 | This program is distributed in the hope that it will be useful, 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | GNU Affero General Public License for more details. 15 | 16 | You should have received a copy of the GNU Affero General Public License 17 | along with this program. If not, see . 18 | */ 19 | define(["js/api/github/authorization", 20 | "js/api/github/user", 21 | "js/api/github/issue", 22 | "js/api/github/repository", 23 | "js/api/github/milestone", 24 | "js/api/github/organization", 25 | "js/api/github/label", 26 | ],function (AuthorizationApi, 27 | UserApi, 28 | IssueApi, 29 | RepositoryApi, 30 | MilestoneApi, 31 | OrganizationApi, 32 | LabelApi 33 | ) { 34 | 'use strict'; 35 | 36 | return { 37 | authorization : AuthorizationApi.getInstance(), 38 | user : UserApi.getInstance(), 39 | issue : IssueApi.getInstance(), 40 | repository : RepositoryApi.getInstance(), 41 | milestone : MilestoneApi.getInstance(), 42 | organization : OrganizationApi.getInstance(), 43 | label : LabelApi.getInstance() 44 | }; 45 | }); 46 | -------------------------------------------------------------------------------- /src/js/api/github/authorization.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2015 - Andreas Dewes 3 | 4 | This file is part of Gitboard. 5 | 6 | Gitboard is free software: you can redistribute it and/or modify 7 | it under the terms of the GNU Affero General Public License as 8 | published by the Free Software Foundation, either version 3 of the 9 | License, or (at your option) any later version. 10 | 11 | This program is distributed in the hope that it will be useful, 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | GNU Affero General Public License for more details. 15 | 16 | You should have received a copy of the GNU Affero General Public License 17 | along with this program. If not, see . 18 | */ 19 | define(["js/utils","js/subject","js/settings"],function (Utils,Subject,Settings) { 20 | 'use strict'; 21 | 22 | var AuthorizationApi = function(){ 23 | Subject.Subject.call(this); 24 | }; 25 | 26 | var instance; 27 | 28 | function getInstance() 29 | { 30 | if (instance === undefined) 31 | instance = new AuthorizationApi(); 32 | return instance; 33 | } 34 | 35 | AuthorizationApi.prototype = new Subject.Subject(); 36 | AuthorizationApi.prototype.constructor = AuthorizationApi; 37 | 38 | AuthorizationApi.prototype.createAuthorization = function(login,password,otp,data,onSuccess,onError){ 39 | return Utils.apiRequest({ 40 | type : 'POST', 41 | url : "/authorizations", 42 | data : JSON.stringify(data), 43 | success : onSuccess, 44 | error: onError 45 | },{authenticated : true,login : login,password : password, otp : otp}); 46 | } 47 | 48 | AuthorizationApi.prototype.deleteAuthorization = function(login,password,otp,id,onSuccess,onError){ 49 | return Utils.apiRequest({ 50 | type : 'DELETE', 51 | url : "/authorizations/"+id, 52 | success : onSuccess, 53 | error: onError 54 | },{authenticated : true,login : login,password : password, otp : otp}); 55 | } 56 | 57 | AuthorizationApi.prototype.getAuthorizations = function(login,password,otp,onSuccess,onError){ 58 | return Utils.apiRequest({ 59 | type : 'GET', 60 | url : "/authorizations", 61 | success : onSuccess, 62 | error: onError 63 | },{authenticated : true,login : login, password: password, otp : otp}); 64 | } 65 | 66 | return {getInstance:getInstance}; 67 | 68 | }); 69 | -------------------------------------------------------------------------------- /src/js/api/github/issue.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2015 - Andreas Dewes 3 | 4 | This file is part of Gitboard. 5 | 6 | Gitboard is free software: you can redistribute it and/or modify 7 | it under the terms of the GNU Affero General Public License as 8 | published by the Free Software Foundation, either version 3 of the 9 | License, or (at your option) any later version. 10 | 11 | This program is distributed in the hope that it will be useful, 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | GNU Affero General Public License for more details. 15 | 16 | You should have received a copy of the GNU Affero General Public License 17 | along with this program. If not, see . 18 | */ 19 | define(["js/utils","js/subject","js/settings"],function (Utils,Subject,Settings) { 20 | 'use strict'; 21 | 22 | var IssueApi = function(){ 23 | Subject.Subject.call(this); 24 | }; 25 | 26 | var instance; 27 | 28 | function getInstance() 29 | { 30 | if (instance === undefined) 31 | instance = new IssueApi(); 32 | return instance; 33 | } 34 | 35 | IssueApi.prototype = new Subject.Subject(); 36 | IssueApi.prototype.constructor = IssueApi; 37 | 38 | IssueApi.prototype.getIssues = function(fullName,data,onSuccess,onError){ 39 | return Utils.apiRequest({ 40 | type : 'GET', 41 | url : "/repos/"+fullName+"/issues"+'?'+Utils.toUrlParams(data), 42 | success : onSuccess, 43 | error: onError, 44 | },{}); 45 | } 46 | 47 | IssueApi.prototype.getDetails = function(fullName,issueNumber,data,onSuccess,onError){ 48 | return Utils.apiRequest({ 49 | type : 'GET', 50 | url : "/repos/"+fullName+"/issues/"+issueNumber+'?'+Utils.toUrlParams(data), 51 | success : onSuccess, 52 | error: onError, 53 | },{}); 54 | } 55 | 56 | IssueApi.prototype.updateIssue = function(fullName,issueNumber,data,onSuccess,onError){ 57 | return Utils.apiRequest({ 58 | type : 'PATCH', 59 | url : "/repos/"+fullName+"/issues/"+issueNumber, 60 | data : JSON.stringify(data), 61 | success : onSuccess, 62 | error: onError, 63 | },{}); 64 | } 65 | 66 | IssueApi.prototype.getComments = function(fullName,issueNumber,data,onSuccess,onError){ 67 | return Utils.apiRequest({ 68 | type : 'GET', 69 | url : "/repos/"+fullName+"/issues/"+issueNumber+'/comments'+'?'+Utils.toUrlParams(data), 70 | success : onSuccess, 71 | error: onError, 72 | },{}); 73 | } 74 | 75 | return {getInstance:getInstance}; 76 | 77 | }); 78 | -------------------------------------------------------------------------------- /src/js/api/github/label.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2015 - Andreas Dewes 3 | 4 | This file is part of Gitboard. 5 | 6 | Gitboard is free software: you can redistribute it and/or modify 7 | it under the terms of the GNU Affero General Public License as 8 | published by the Free Software Foundation, either version 3 of the 9 | License, or (at your option) any later version. 10 | 11 | This program is distributed in the hope that it will be useful, 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | GNU Affero General Public License for more details. 15 | 16 | You should have received a copy of the GNU Affero General Public License 17 | along with this program. If not, see . 18 | */ 19 | define(["js/utils","js/subject","js/settings"],function (Utils,Subject,Settings) { 20 | 'use strict'; 21 | 22 | var LabelApi = function(){ 23 | Subject.Subject.call(this); 24 | }; 25 | 26 | var instance; 27 | 28 | function getInstance() 29 | { 30 | if (instance === undefined) 31 | instance = new LabelApi(); 32 | return instance; 33 | } 34 | 35 | LabelApi.prototype = new Subject.Subject(); 36 | LabelApi.prototype.constructor = LabelApi; 37 | 38 | LabelApi.prototype.getRepositoryLabels = function(fullName,data,onSuccess,onError){ 39 | return Utils.apiRequest({ 40 | type : 'GET', 41 | url : "/repos/"+fullName+"/labels"+'?'+Utils.toUrlParams(data), 42 | success : onSuccess, 43 | error: onError, 44 | },{}); 45 | } 46 | 47 | LabelApi.prototype.getIssueLabels = function(fullName,issueNumber,data,onSuccess,onError){ 48 | return Utils.apiRequest({ 49 | type : 'GET', 50 | url : "/repos/"+fullName+"/issues/"+issueNumber+"/labels"+'?'+Utils.toUrlParams(data), 51 | success : onSuccess, 52 | error: onError, 53 | },{}); 54 | } 55 | 56 | LabelApi.prototype.removeLabel = function(fullName,issueNumber,labelName,onSuccess,onError){ 57 | return Utils.apiRequest({ 58 | type : 'DELETE', 59 | url : "/repos/"+fullName+"/issues/"+issueNumber+"/labels/"+labelName, 60 | success : onSuccess, 61 | error: onError, 62 | },{}); 63 | } 64 | 65 | LabelApi.prototype.createLabel = function(fullName,data,onSuccess,onError){ 66 | return Utils.apiRequest({ 67 | type : 'POST', 68 | url : "/repos/"+fullName+"/labels", 69 | data: JSON.stringify(data), 70 | success : onSuccess, 71 | error: onError, 72 | },{}); 73 | } 74 | 75 | LabelApi.prototype.addLabels = function(fullName,issueNumber,labelNames,onSuccess,onError){ 76 | return Utils.apiRequest({ 77 | type : 'POST', 78 | url : "/repos/"+fullName+"/issues/"+issueNumber+"/labels", 79 | success : onSuccess, 80 | data : JSON.stringify(labelNames), 81 | error: onError, 82 | },{}); 83 | } 84 | 85 | LabelApi.prototype.getDetails = function(fullName,labelNumber,data,onSuccess,onError){ 86 | return Utils.apiRequest({ 87 | type : 'GET', 88 | url : "/repos/"+fullName+"/labels/"+LabelNumber+'?'+Utils.toUrlParams(data), 89 | success : onSuccess, 90 | error: onError, 91 | },{}); 92 | } 93 | 94 | return {getInstance:getInstance}; 95 | 96 | }); 97 | -------------------------------------------------------------------------------- /src/js/api/github/milestone.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2015 - Andreas Dewes 3 | 4 | This file is part of Gitboard. 5 | 6 | Gitboard is free software: you can redistribute it and/or modify 7 | it under the terms of the GNU Affero General Public License as 8 | published by the Free Software Foundation, either version 3 of the 9 | License, or (at your option) any later version. 10 | 11 | This program is distributed in the hope that it will be useful, 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | GNU Affero General Public License for more details. 15 | 16 | You should have received a copy of the GNU Affero General Public License 17 | along with this program. If not, see . 18 | */ 19 | define(["js/utils","js/subject","js/settings"],function (Utils,Subject,Settings) { 20 | 'use strict'; 21 | 22 | var MilestoneApi = function(type){ 23 | Subject.Subject.call(this); 24 | }; 25 | 26 | var instance; 27 | 28 | function getInstance() 29 | { 30 | if (instance === undefined) 31 | instance = new MilestoneApi(); 32 | return instance; 33 | } 34 | 35 | MilestoneApi.prototype = new Subject.Subject(); 36 | MilestoneApi.prototype.constructor = MilestoneApi; 37 | 38 | MilestoneApi.prototype.getMilestones = function(fullName,data,onSuccess,onError){ 39 | return Utils.apiRequest({ 40 | type : 'GET', 41 | url : "/repos/"+fullName+"/milestones"+'?'+Utils.toUrlParams(data), 42 | success : onSuccess, 43 | error: onError, 44 | },{}); 45 | } 46 | 47 | MilestoneApi.prototype.getDetails = function(fullName,number,data,onSuccess,onError){ 48 | return Utils.apiRequest({ 49 | type : 'GET', 50 | url : "/repos/"+fullName+"/milestones/"+number+'?'+Utils.toUrlParams(data), 51 | success : onSuccess, 52 | error: onError, 53 | },{}); 54 | } 55 | 56 | return {getInstance:getInstance}; 57 | 58 | }); 59 | -------------------------------------------------------------------------------- /src/js/api/github/organization.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2015 - Andreas Dewes 3 | 4 | This file is part of Gitboard. 5 | 6 | Gitboard is free software: you can redistribute it and/or modify 7 | it under the terms of the GNU Affero General Public License as 8 | published by the Free Software Foundation, either version 3 of the 9 | License, or (at your option) any later version. 10 | 11 | This program is distributed in the hope that it will be useful, 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | GNU Affero General Public License for more details. 15 | 16 | You should have received a copy of the GNU Affero General Public License 17 | along with this program. If not, see . 18 | */ 19 | define(["js/utils","js/subject","js/settings"],function (Utils,Subject,Settings) { 20 | 'use strict'; 21 | 22 | var OrganizationApi = function(type){ 23 | Subject.Subject.call(this); 24 | }; 25 | 26 | var instance; 27 | 28 | function getInstance() 29 | { 30 | if (instance === undefined) 31 | instance = new OrganizationApi(); 32 | return instance; 33 | } 34 | 35 | OrganizationApi.prototype = new Subject.Subject(); 36 | OrganizationApi.prototype.constructor = OrganizationApi; 37 | 38 | OrganizationApi.prototype.getOrganizations = function(data,onSuccess,onError){ 39 | return Utils.apiRequest({ 40 | type : 'GET', 41 | url : "/user/orgs"+'?'+Utils.toUrlParams(data), 42 | success : onSuccess, 43 | error: onError, 44 | },{}); 45 | } 46 | 47 | OrganizationApi.prototype.getRepositories = function(owner,data,onSuccess,onError){ 48 | console.log("Getting repositories for organization "+owner) 49 | return Utils.apiRequest({ 50 | type : 'GET', 51 | url : "/orgs/"+owner+"/repos"+'?'+Utils.toUrlParams(data), 52 | success : onSuccess, 53 | error: onError, 54 | },{}); 55 | } 56 | 57 | OrganizationApi.prototype.getDetails = function(name,data,onSuccess,onError){ 58 | return Utils.apiRequest({ 59 | type : 'GET', 60 | url : "/orgs/"+name+'?'+Utils.toUrlParams(data), 61 | success : onSuccess, 62 | error: onError, 63 | },{}); 64 | } 65 | 66 | return {getInstance:getInstance}; 67 | 68 | }); 69 | -------------------------------------------------------------------------------- /src/js/api/github/repository.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2015 - Andreas Dewes 3 | 4 | This file is part of Gitboard. 5 | 6 | Gitboard is free software: you can redistribute it and/or modify 7 | it under the terms of the GNU Affero General Public License as 8 | published by the Free Software Foundation, either version 3 of the 9 | License, or (at your option) any later version. 10 | 11 | This program is distributed in the hope that it will be useful, 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | GNU Affero General Public License for more details. 15 | 16 | You should have received a copy of the GNU Affero General Public License 17 | along with this program. If not, see . 18 | */ 19 | define(["js/utils","js/subject","js/settings"],function (Utils,Subject,Settings) { 20 | 'use strict'; 21 | 22 | var RepositoryApi = function(type){ 23 | Subject.Subject.call(this); 24 | }; 25 | 26 | var instance; 27 | 28 | function getInstance() 29 | { 30 | if (instance === undefined) 31 | instance = new RepositoryApi(); 32 | return instance; 33 | } 34 | 35 | RepositoryApi.prototype = new Subject.Subject(); 36 | RepositoryApi.prototype.constructor = RepositoryApi; 37 | 38 | RepositoryApi.prototype.getDetails = function(fullName,data,onSuccess,onError){ 39 | return Utils.apiRequest({ 40 | type : 'GET', 41 | url : "/repos/"+fullName+'?'+Utils.toUrlParams(data), 42 | success : onSuccess, 43 | error: onError, 44 | },{}); 45 | } 46 | 47 | RepositoryApi.prototype.getCollaborators = function(fullName,data,onSuccess,onError){ 48 | return Utils.apiRequest({ 49 | type : 'GET', 50 | url : "/repos/"+fullName+'/collaborators?'+Utils.toUrlParams(data), 51 | success : onSuccess, 52 | error: onError, 53 | },{}); 54 | } 55 | 56 | return {getInstance:getInstance}; 57 | 58 | }); 59 | -------------------------------------------------------------------------------- /src/js/api/github/user.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2015 - Andreas Dewes 3 | 4 | This file is part of Gitboard. 5 | 6 | Gitboard is free software: you can redistribute it and/or modify 7 | it under the terms of the GNU Affero General Public License as 8 | published by the Free Software Foundation, either version 3 of the 9 | License, or (at your option) any later version. 10 | 11 | This program is distributed in the hope that it will be useful, 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | GNU Affero General Public License for more details. 15 | 16 | You should have received a copy of the GNU Affero General Public License 17 | along with this program. If not, see . 18 | */ 19 | define(["js/utils","js/subject","js/settings"],function (Utils,Subject,Settings) { 20 | 'use strict'; 21 | 22 | var UserApi = function(){ 23 | Subject.Subject.call(this); 24 | }; 25 | 26 | var instance; 27 | 28 | function getInstance() 29 | { 30 | if (instance === undefined) 31 | instance = new UserApi(); 32 | return instance; 33 | } 34 | 35 | UserApi.prototype = new Subject.Subject(); 36 | UserApi.prototype.constructor = UserApi; 37 | 38 | UserApi.prototype.getRepositories = function(data,onSuccess,onError){ 39 | return Utils.apiRequest({ 40 | type : 'GET', 41 | url : "/user/repos"+'?'+Utils.toUrlParams(data), 42 | success : onSuccess, 43 | error: onError, 44 | },{}); 45 | } 46 | 47 | UserApi.prototype.getUserRepositories = function(username,data,onSuccess,onError){ 48 | return Utils.apiRequest({ 49 | type : 'GET', 50 | url : "/users/"+username+"/repos"+'?'+Utils.toUrlParams(data), 51 | success : onSuccess, 52 | error: onError, 53 | },{}); 54 | } 55 | 56 | UserApi.prototype.getProfile = function(onSuccess,onError){ 57 | return Utils.apiRequest({ 58 | type : 'GET', 59 | url : "/user", 60 | success : onSuccess, 61 | error: onError 62 | }); 63 | } 64 | 65 | return {getInstance:getInstance}; 66 | 67 | }); 68 | -------------------------------------------------------------------------------- /src/js/build.js: -------------------------------------------------------------------------------- 1 | ({ 2 | appDir: "../../", 3 | dir : "../../optimized", 4 | mainConfigFile : "config.js", 5 | baseUrl: "static", 6 | generateSourceMaps: true, 7 | removeCombined : true, 8 | keepBuildDir : true, 9 | optimize: 'uglify2', 10 | skipDirOptimize : true, 11 | fileExclusionRegExp: /^optimized/, 12 | preserveLicenseComments : false, 13 | modules: [ 14 | { 15 | name: "js/main", 16 | } 17 | ], 18 | paths: { 19 | //use the minified version instead of minifying it by ourself 20 | //since otherwise we would still include some debug code. 21 | //By using the minified version, we make sure, that we get the 22 | //development build. 23 | "react": "bower_components/react/react.min" 24 | } 25 | }) 26 | -------------------------------------------------------------------------------- /src/js/components/app.jsx: -------------------------------------------------------------------------------- 1 | /** 2 | * @jsx React.DOM 3 | */ 4 | /*jshint quotmark:false */ 5 | /*jshint white:false */ 6 | /*jshint trailing:false */ 7 | /*jshint newcap:false */ 8 | /*global React, Router*/ 9 | 10 | /* 11 | Copyright (c) 2015 - Andreas Dewes 12 | 13 | This file is part of Gitboard. 14 | 15 | Gitboard is free software: you can redistribute it and/or modify 16 | it under the terms of the GNU Affero General Public License as 17 | published by the Free Software Foundation, either version 3 of the 18 | License, or (at your option) any later version. 19 | 20 | This program is distributed in the hope that it will be useful, 21 | but WITHOUT ANY WARRANTY; without even the implied warranty of 22 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 23 | GNU Affero General Public License for more details. 24 | 25 | You should have received a copy of the GNU Affero General Public License 26 | along with this program. If not, see . 27 | */ 28 | 29 | define(["react", 30 | "js/utils", 31 | "js/components/header", 32 | "js/components/menu", 33 | "jquery" 34 | ], 35 | function (React, 36 | Utils, 37 | Header, 38 | Menu, 39 | $ 40 | ) 41 | { 42 | 'use'+' strict'; 43 | 44 | var MainApp = React.createClass({ 45 | 46 | displayName: 'MainApp', 47 | 48 | componentDidMount : function(){ 49 | 50 | if (Utils.isLoggedIn()){ 51 | $(".navbar-brand").attr("href", "#/repositories"); 52 | } 53 | 54 | var bodyIsTool = $("body").hasClass("app"); 55 | if (!bodyIsTool) 56 | $("body").addClass("app"); 57 | 58 | this.renderDependentComponents(); 59 | }, 60 | 61 | renderDependentComponents : function(){ 62 | 63 | var header =
; 66 | 67 | var menu = ; 70 | 71 | //call React.render outside of the render function below. 72 | //Calling it from within the render function is not supported by React 73 | //and might break in a future version. 74 | React.render(header, 75 | document.getElementById('header') 76 | ); 77 | 78 | React.render(menu, 79 | document.getElementById('menu') 80 | ); 81 | }, 82 | 83 | componentDidUpdate: function() { 84 | this.renderDependentComponents(); 85 | }, 86 | 87 | render: function () { 88 | 89 | /* 90 | Parameters that we pass in: 91 | app : a reference to this class instance 92 | params : the URL parameters received for this request 93 | data : the router data received by director.js 94 | user : the current user 95 | baseUrl : the base URL for the given component 96 | */ 97 | 98 | var props = this.props; 99 | 100 | if (!Utils.isLoggedIn() && ! props.anonOk){ 101 | Utils.redirectTo(Utils.makeUrl("/login")); 102 | } 103 | 104 | var Component = this.props.component; 105 | 106 | if (this.props.callback){ 107 | Component = this.props.callback(this.props); 108 | } 109 | 110 | return ; 115 | } 116 | }); 117 | 118 | return MainApp; 119 | }); 120 | -------------------------------------------------------------------------------- /src/js/components/generic/flash_messages.jsx: -------------------------------------------------------------------------------- 1 | /** 2 | * @jsx React.DOM 3 | */ 4 | /*jshint quotmark:false */ 5 | /*jshint white:false */ 6 | /*jshint trailing:false */ 7 | /*jshint newcap:false */ 8 | /*global React, Router*/ 9 | 10 | /* 11 | Copyright (c) 2015 - Andreas Dewes 12 | 13 | This file is part of Gitboard. 14 | 15 | Gitboard is free software: you can redistribute it and/or modify 16 | it under the terms of the GNU Affero General Public License as 17 | published by the Free Software Foundation, either version 3 of the 18 | License, or (at your option) any later version. 19 | 20 | This program is distributed in the hope that it will be useful, 21 | but WITHOUT ANY WARRANTY; without even the implied warranty of 22 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 23 | GNU Affero General Public License for more details. 24 | 25 | You should have received a copy of the GNU Affero General Public License 26 | along with this program. If not, see . 27 | */ 28 | 29 | define(["react","js/utils","js/flash_messages","jquery"],function (React,Utils,FlashMessagesService,$) { 30 | 'use'+' strict'; 31 | 32 | var FlashMessagesMixin = { 33 | 34 | componentWillMount : function(){ 35 | FlashMessagesService.subscribe(this.updateStatus); 36 | }, 37 | 38 | componentDidMount : function(){ 39 | this.timerId = setInterval(function(){this.forceUpdate()}.bind(this),1000); 40 | }, 41 | 42 | getInitialState : function(){ 43 | return {messages : [] }; 44 | }, 45 | 46 | componentWillUnmount : function(){ 47 | FlashMessagesService.unsubscribe(this.updateStatus); 48 | clearInterval(this.timerId); 49 | }, 50 | 51 | updateStatus : function(subject,property,value){ 52 | if (subject === FlashMessagesService){ 53 | if (property === 'newMessage'){ 54 | var newMessages = this.state.messages.slice(0); 55 | newMessages.push(value); 56 | this.setState({messages : newMessages,viewed: false}); 57 | } 58 | } 59 | }, 60 | 61 | }; 62 | 63 | var FlashMessagesMenu = React.createClass({ 64 | 65 | displayName: 'FlashMessagesMenu', 66 | 67 | mixins : [FlashMessagesMixin], 68 | 69 | markAsViewed : function(){ 70 | this.setState({viewed : true}); 71 | }, 72 | 73 | getInitialState : function(){ 74 | return {viewed : false}; 75 | }, 76 | 77 | render : function(){ 78 | messageItems = this.state.messages.slice(-5).map( 79 | function(msg){ 80 | var title = "Message"; 81 | switch (msg.data.type){ 82 | case "warning": 83 | title = "Warning";break; 84 | case "error": 85 | case "danger": 86 | title = "Error";break; 87 | case "info": 88 | title = "Info";break; 89 | } 90 | if (msg.data.sticky === undefined){ 91 | var elapsedTime = (new Date()).getTime() - msg.receivedAt.getTime(); 92 | var prepareUnmount = false; 93 | if (elapsedTime > msg.duration+4000) 94 | return undefined; 95 | if (elapsedTime > msg.duration+1000){ 96 | prepareUnmount = true; 97 | } 98 | } 99 | return
  • {title}

    {msg.data.description}
  • ; 100 | }.bind(this) 101 | ); 102 | messageItems = messageItems.filter(function(item){if (item !== undefined)return true;return false;}).reverse(); 103 | if (messageItems.length){ 104 | var messageStatus = "fa-envelope"; 105 | var color = "yellow"; 106 | if (this.state.viewed == true){ 107 | messageStatus = "fa-envelope-o"; 108 | color = "#fff"; 109 | } 110 | return
  • 111 | 112 |
      113 | {messageItems} 114 |
    115 |
  • ; 116 | } 117 | else{ 118 | return
  • ; 119 | } 120 | } 121 | }); 122 | 123 | var FlashMessageItem = React.createClass({ 124 | 125 | displayName: 'FlashMessageItem', 126 | 127 | render : function() { 128 | 129 | return
    130 |
    131 | 136 |
    137 |
    138 | }, 139 | 140 | componentDidMount : function(){ 141 | try{ 142 | var node = this.getDOMNode(); 143 | $(node).hide(); 144 | $(node).slideDown(400); 145 | } 146 | catch (e){ 147 | 148 | } 149 | }, 150 | 151 | componentWillReceiveProps : function(props){ 152 | if (props.prepareUnmount && this.isMounted()) 153 | this.fadeOut(); 154 | }, 155 | 156 | fadeOut : function(event){ 157 | if (event) 158 | event.preventDefault(); 159 | if (! this.isMounted()) 160 | return; 161 | try{ 162 | var node = this.getDOMNode(); 163 | $(node).slideUp(400); 164 | } 165 | catch (e){ 166 | } 167 | return false; 168 | }, 169 | 170 | }); 171 | 172 | var FlashMessagesHeader = React.createClass({ 173 | 174 | displayName: 'FlashMessagesHeader', 175 | 176 | mixins : [FlashMessagesMixin], 177 | 178 | render : function(){ 179 | messageItems = this.state.messages.map( 180 | function(msg){ 181 | var elapsedTime = (new Date()).getTime() - msg.receivedAt.getTime(); 182 | var prepareUnmount = false; 183 | if (elapsedTime > msg.duration+4000) 184 | return undefined; 185 | if (elapsedTime > msg.duration+1000){ 186 | prepareUnmount = true; 187 | } 188 | return ; 189 | }.bind(this) 190 | ); 191 | messageItems = messageItems.filter(function(item){if (item !== undefined)return true;return false;}); 192 | if (messageItems.length){ 193 | return
    194 | {messageItems} 195 |
    ; 196 | } 197 | else{ 198 | return
    ; 199 | } 200 | } 201 | 202 | }); 203 | 204 | return {FlashMessagesMenu : FlashMessagesMenu , FlashMessagesHeader : FlashMessagesHeader }; 205 | 206 | }); 207 | 208 | 209 | -------------------------------------------------------------------------------- /src/js/components/generic/modal.jsx: -------------------------------------------------------------------------------- 1 | /** 2 | * @jsx React.DOM 3 | */ 4 | /*jshint quotmark:false */ 5 | /*jshint white:false */ 6 | /*jshint trailing:false */ 7 | /*jshint newcap:false */ 8 | /*global React, Router*/ 9 | 10 | define(["react","jquery","bootstrap"],function (React,$,Bootstrap) { 11 | 'use strict'; 12 | 13 | var BootstrapModal = React.createClass({ 14 | // The following two methods are the only places we need to 15 | // integrate with Bootstrap or jQuery! 16 | displayName: 'BootstrapModal', 17 | 18 | close: function() { 19 | this.setState({hidden : true}); 20 | }, 21 | 22 | open: function() { 23 | this.setState({hidden : false}); 24 | }, 25 | 26 | getInitialState : function(){ 27 | return {hidden : this.props.hidden !== undefined ? this.props.hidden : true} 28 | }, 29 | 30 | getDefaultProps : function(){ 31 | return {closable : true,disabled : false,raw : false}; 32 | }, 33 | 34 | componentWillReceiveProps : function(props){ 35 | if (props.hidden && props.hidden != this.state.hidden) 36 | this.setState({hidden : props.hidden}); 37 | }, 38 | 39 | render: function() { 40 | if (this.state.hidden) 41 | return
    ; 42 | 43 | var confirmButton; 44 | var cancelButton; 45 | 46 | if (this.props.confirm) { 47 | confirmButton = ( 48 | 54 | ); 55 | } 56 | 57 | var closeButton; 58 | 59 | if (this.props.closable){ 60 | closeButton = 67 | } 68 | 69 | if (this.props.cancel) { 70 | cancelButton = ( 71 | 74 | ); 75 | } 76 | 77 | var footer; 78 | 79 | if ((this.props.onCancel || this.props.onConfirm) && !this.props.raw){ 80 | footer =
    81 | {cancelButton} 82 | {confirmButton} 83 |
    84 | } 85 | var content; 86 | 87 | if (this.props.getContent) 88 | content = this.props.getContent(); 89 | else 90 | content = this.props.children; 91 | 92 | if (!this.props.raw) 93 | content = [
    {content}
    ,footer] 94 | 95 | return ( 96 |
    97 |
    98 |
    99 |
    100 | 101 |

    {this.props.title}{closeButton}

    102 |
    103 | {content} 104 |
    105 |
    106 |
    107 |
    108 | ); 109 | }, 110 | 111 | handleCancel: function(e) { 112 | if (this.props.onCancel) 113 | this.props.onCancel(e); 114 | e.preventDefault(); 115 | this.close(); 116 | }, 117 | 118 | handleConfirm: function(e) { 119 | if (this.props.onConfirm) 120 | this.props.onConfirm(e); 121 | e.preventDefault(); 122 | } 123 | }); 124 | 125 | return BootstrapModal; 126 | }); 127 | -------------------------------------------------------------------------------- /src/js/components/header.jsx: -------------------------------------------------------------------------------- 1 | /** 2 | * @jsx React.DOM 3 | */ 4 | /*jshint quotmark:false */ 5 | /*jshint white:false */ 6 | /*jshint trailing:false */ 7 | /*jshint newcap:false */ 8 | /*global React, Router*/ 9 | 10 | /* 11 | Copyright (c) 2015 - Andreas Dewes 12 | 13 | This file is part of Gitboard. 14 | 15 | Gitboard is free software: you can redistribute it and/or modify 16 | it under the terms of the GNU Affero General Public License as 17 | published by the Free Software Foundation, either version 3 of the 18 | License, or (at your option) any later version. 19 | 20 | This program is distributed in the hope that it will be useful, 21 | but WITHOUT ANY WARRANTY; without even the implied warranty of 22 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 23 | GNU Affero General Public License for more details. 24 | 25 | You should have received a copy of the GNU Affero General Public License 26 | along with this program. If not, see . 27 | */ 28 | 29 | define(["react","js/utils", 30 | "js/request_notifier", 31 | "js/components/generic/flash_messages", 32 | ], 33 | function (React, 34 | Utils, 35 | RequestNotifier, 36 | FlashMessages 37 | ){ 38 | 'use'+' strict'; 39 | 40 | var RequestIndicator = React.createClass({ 41 | 42 | displayName: 'RequestIndicator', 43 | 44 | getInitialState : function(){ 45 | return {hidden : false}; 46 | }, 47 | 48 | hideMessage : function(){ 49 | this.setState({hidden : true}); 50 | return false; 51 | }, 52 | 53 | componentWillReceiveProps : function(props){ 54 | if (props.activeRequestCount !== undefined && props.activeRequestCount == 0) 55 | this.setState({hidden : false}); 56 | }, 57 | 58 | render : function(){ 59 | if (this.props.connectionError == true) 60 | return

    Connection problem!

    ; 61 | if (this.props.activeRequestCount > 0 && ! this.state.hidden){ 62 | return

    syncing...

    ; 63 | } 64 | else 65 | return ; 66 | } 67 | }); 68 | 69 | var Header = React.createClass({ 70 | 71 | displayName: 'Header', 72 | 73 | componentWillMount : function(){ 74 | this.requestNotifier = RequestNotifier.getInstance(); 75 | Utils.addRequestNotifier(this.requestNotifier); 76 | }, 77 | 78 | getInitialState : function(){ 79 | return {activeRequestCount : 0}; 80 | }, 81 | 82 | componentDidMount : function(){ 83 | this.requestNotifier.subscribe(this.updateStatus); 84 | }, 85 | 86 | componentWillUnmount : function(){ 87 | this.requestNotifier.unsubscribe(this.updateStatus); 88 | }, 89 | 90 | updateStatus : function(subject,property,value){ 91 | if (subject === this.requestNotifier){ 92 | if (property == 'connectionError'){ 93 | this.setState({connectionError : true, 94 | willRetryIn : value.requestData.data.retryInterval || 1}); 95 | setTimeout(function(){ 96 | this.setState({connectionError : false}); 97 | }.bind(this),4000 ); 98 | } 99 | if (property === 'activeRequestCount'){ 100 | setTimeout(function(count){ 101 | if (this.requestNotifier.activeRequestCount() == count) 102 | this.setState({activeRequestCount :count}); 103 | }.bind(this,value),200); 104 | } 105 | } 106 | }, 107 | 108 | render: function () { 109 | var FlashMessagesHeader = FlashMessages.FlashMessagesHeader; 110 | var flashMessagesHeader = ; 111 | 112 | var requestIndicator = undefined; 113 | 114 | if (this.state.activeRequestCount > 0 || this.state.connectionError) 115 | requestIndicator = ; 116 | return
    117 | {flashMessagesHeader} 118 | {requestIndicator} 119 |
    ; 120 | } 121 | }); 122 | 123 | return Header; 124 | }); 125 | -------------------------------------------------------------------------------- /src/js/components/menu.jsx: -------------------------------------------------------------------------------- 1 | /** 2 | * @jsx React.DOM 3 | */ 4 | /*jshint quotmark:false */ 5 | /*jshint white:false */ 6 | /*jshint trailing:false */ 7 | /*jshint newcap:false */ 8 | /*global React, Router*/ 9 | 10 | /* 11 | Copyright (c) 2015 - Andreas Dewes 12 | 13 | This file is part of Gitboard. 14 | 15 | Gitboard is free software: you can redistribute it and/or modify 16 | it under the terms of the GNU Affero General Public License as 17 | published by the Free Software Foundation, either version 3 of the 18 | License, or (at your option) any later version. 19 | 20 | This program is distributed in the hope that it will be useful, 21 | but WITHOUT ANY WARRANTY; without even the implied warranty of 22 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 23 | GNU Affero General Public License for more details. 24 | 25 | You should have received a copy of the GNU Affero General Public License 26 | along with this program. If not, see . 27 | */ 28 | 29 | define(["react","js/utils", 30 | "js/api/github/user", 31 | "js/components/generic/flash_messages", 32 | "js/components/mixins/loader", 33 | "jquery" 34 | ], 35 | function (React,Utils,UserApi,FlashMessages,LoaderMixin,$) { 36 | 'use strict'; 37 | 38 | var Menu = React.createClass({ 39 | 40 | mixins : [LoaderMixin], 41 | 42 | resources : function(props){ 43 | 44 | var logout = function(){ 45 | Utils.logout(); 46 | Utils.redirectTo(Utils.makeUrl('/login')); 47 | }; 48 | 49 | if (Utils.isLoggedIn()){ 50 | return [ 51 | { 52 | name : 'user', 53 | endpoint : this.apis.user.getProfile, 54 | nonBlocking : true, 55 | nonCritical : true, 56 | error : logout 57 | } 58 | ]; 59 | } 60 | return []; 61 | }, 62 | 63 | silentLoading : true, 64 | displayName: 'Menu', 65 | 66 | getInitialState: function () { 67 | return {user: {admin: false}, project: {roles: {admin: []}}}; 68 | }, 69 | 70 | getDefaultProps : function (){ 71 | return {}; 72 | }, 73 | 74 | componentWillMount : function(){ 75 | this.userApi = UserApi.getInstance(); 76 | }, 77 | 78 | render: function () { 79 | 80 | var projectMenu; 81 | var adminMenu; 82 | 83 | var FlashMessagesMenu = FlashMessages.FlashMessagesMenu; 84 | var flashMessagesMenu = ; 85 | flashMessagesMenu = undefined; /* quick switch to activate or deactivate */ 86 | 87 | var menu; 88 | if (Utils.isLoggedIn()){ 89 | menu = [, 93 |
      94 | {projectMenu} 95 |
    • Logout
    • 96 |
    ]; 97 | } 98 | else{ 99 | menu = [
      100 |
    , 101 |
      102 |
    • Login
    • 103 | {flashMessagesMenu} 104 |
    ]; 105 | } 106 | return ; 109 | } 110 | }); 111 | 112 | return Menu; 113 | }); 114 | -------------------------------------------------------------------------------- /src/js/components/milestones.jsx: -------------------------------------------------------------------------------- 1 | /** 2 | * @jsx React.DOM 3 | */ 4 | /*jshint quotmark:false */ 5 | /*jshint white:false */ 6 | /*jshint trailing:false */ 7 | /*jshint newcap:false */ 8 | /*global React, Router*/ 9 | 10 | /* 11 | Copyright (c) 2015 - Andreas Dewes 12 | 13 | This file is part of Gitboard. 14 | 15 | Gitboard is free software: you can redistribute it and/or modify 16 | it under the terms of the GNU Affero General Public License as 17 | published by the Free Software Foundation, either version 3 of the 18 | License, or (at your option) any later version. 19 | 20 | This program is distributed in the hope that it will be useful, 21 | but WITHOUT ANY WARRANTY; without even the implied warranty of 22 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 23 | GNU Affero General Public License for more details. 24 | 25 | You should have received a copy of the GNU Affero General Public License 26 | along with this program. If not, see . 27 | */ 28 | 29 | define(["react", 30 | "js/utils", 31 | "js/components/mixins/loader", 32 | "js/components/mixins/github_error_handler", 33 | "jquery", 34 | "moment" 35 | ], 36 | function (React,Utils,LoaderMixin,GithubErrorHandlerMixin,$,Moment) { 37 | 'use'+' strict'; 38 | 39 | 40 | var MilestoneItem = React.createClass({ 41 | 42 | render : function(){ 43 | var due; 44 | if (this.props.milestone.due_on !== null){ 45 | var datestring = Moment(new Date(this.props.milestone.due_on)).fromNow(); 46 | due = [,' ',datestring]; 47 | } 48 | return ; 60 | } 61 | }); 62 | 63 | var Milestones = React.createClass({ 64 | 65 | mixins : [LoaderMixin,GithubErrorHandlerMixin], 66 | 67 | resources : function(props){ 68 | return [ 69 | { 70 | name : 'repository', 71 | endpoint : this.apis.repository.getDetails, 72 | params : [props.data.repositoryId,{}], 73 | success : function(data){ 74 | return {repository : data}; 75 | }.bind(this) 76 | }, 77 | { 78 | name : 'milestones', 79 | endpoint : this.apis.milestone.getMilestones, 80 | params : [props.data.repositoryId,{per_page : 100}], 81 | success : function(data,xhr){ 82 | 83 | var arr = []; 84 | for(var i in data) { 85 | if(data.hasOwnProperty(i) && !isNaN(+i)) { 86 | arr[+i] = data[i]; 87 | } 88 | } 89 | return {milestones : arr}; 90 | }.bind(this) 91 | }]; 92 | }, 93 | 94 | displayName: 'Milestones', 95 | 96 | render: function () { 97 | 98 | var data = this.state.data; 99 | 100 | var milestoneItems = data.milestones.map(function(milestone){ 101 | return ; 102 | }.bind(this)) 103 | 104 | if (milestoneItems.length == 0) 105 | milestoneItems = [

    Seems there is nothing to show here.

    ] 106 | 107 | return
    108 |
    109 |
    110 |

    Milestones - {data.repository.name}

    111 |
    112 |
    113 |
    114 | {milestoneItems} 115 |
    116 |
    117 | 120 |
    121 |
    ; 122 | } 123 | }); 124 | 125 | return Milestones; 126 | }); 127 | -------------------------------------------------------------------------------- /src/js/components/mixins/form.jsx: -------------------------------------------------------------------------------- 1 | /** 2 | * @jsx React.DOM 3 | */ 4 | 5 | define([ 6 | "react", 7 | "js/utils", 8 | "jquery", 9 | "js/components/generic/modal" 10 | ],function ( 11 | React, 12 | Utils, 13 | $, 14 | Modal 15 | ) { 16 | 'use strict'; 17 | 18 | var FormMixin = { 19 | 20 | isFieldError : function(field){ 21 | if (this.state.fieldErrors !== undefined) 22 | if (field in this.state.fieldErrors) 23 | return true; 24 | return false; 25 | }, 26 | 27 | formatFieldError : function(field){ 28 | if (this.state.fieldErrors !== undefined) 29 | if (field in this.state.fieldErrors) 30 | return

    {this.state.fieldErrors[field]}

    31 | return undefined; 32 | }, 33 | 34 | setter : function(name, trim){ 35 | return function(e){ 36 | e.preventDefault(); 37 | var newValue = trim === true ? $.trim(e.target.value) : e.target.value; 38 | if (newValue == this.state[name]) 39 | return; //nothing changed... 40 | var update = {} 41 | update[name] = newValue; 42 | this.setState(update); 43 | if(this.props.onChange) { 44 | var newValues = $.extend({}, this.getValues()); 45 | newValues[name] = newValue; 46 | this.props.onChange(newValues); 47 | } 48 | }.bind(this); 49 | }, 50 | 51 | getInitialState : function(){ 52 | return {disabled: false}; 53 | }, 54 | 55 | getValues : function() { 56 | var values = {} 57 | for(var name in this.fields){ 58 | values[name] = this.state[name]; 59 | } 60 | return values; 61 | }, 62 | 63 | formatErrorMessage : function(){ 64 | if (this.state.errorMessage !== undefined) 65 | return

    {this.state.errorMessage}

    ; 66 | return undefined; 67 | }, 68 | 69 | formatFieldGroupError : function(field, classes){ 70 | if (this.isFieldError(field)) 71 | return "form-group has-warning " + classes; 72 | return "form-group " + classes; 73 | }, 74 | 75 | clearFieldError: function(field){ 76 | var newErrors = $.extend({}, this.state.fieldErrors); 77 | delete newErrors[field]; 78 | this.setState({fieldErrors:newErrors}); 79 | }, 80 | 81 | clearAllErrors: function(){ 82 | this.setState({ 83 | fieldErrors: {}, 84 | errorMessage: undefined, 85 | }); 86 | }, 87 | 88 | addFieldError: function(field, message){ 89 | var newErrors = this.state.fieldErrors; 90 | newErrors[field] = message; 91 | this.setState({fieldErrors:newErrors}); 92 | }, 93 | 94 | setErrorMessage: function(message){ 95 | this.setState({errorMessage: message}); 96 | }, 97 | 98 | clearErrorMessage: function(){ 99 | this.setState({errorMessage: undefined}); 100 | }, 101 | 102 | hasErrors : function(){ 103 | if (this.state.fieldErrors === undefined || Object.keys(this.state.fieldErrors).length > 0) 104 | return true; 105 | return false; 106 | }, 107 | 108 | componentDidMount: function(){ 109 | this.clearAllErrors(); 110 | if (this.props.apiErrorData !== undefined) 111 | this.parseApiErrorData(this.props.apiErrorData); 112 | }, 113 | 114 | componentWillReceiveProps : function(props){ 115 | if (props.apiErrorData) { 116 | this.clearAllErrors(); 117 | this.parseApiErrorData(props.apiErrorData); 118 | } 119 | }, 120 | 121 | renderWithModal : function(){ 122 | var modal = ," Unsaved data"]}> 130 |

    There is unsaved data in the form you were editing. This data will get lost if you leave this page! Are you sure you want to leave?

    131 |
    ; 132 | return
    133 | {modal} 134 | {this._renderform()} 135 |
    136 | }, 137 | 138 | componentWillMount : function(){ 139 | this._renderform = this.render; 140 | this.render = this.renderWithModal; 141 | 142 | if (this.isDirty === undefined) 143 | this.isDirty = function(){return false;}; 144 | 145 | this.callback = function(url,fullUrl,newUrl){ 146 | if (!this.isMounted() || ! this.isDirty() || this.leavePage) 147 | return true; 148 | this.refs.leavePageModal.open(); 149 | this.leavePage = true; 150 | this.newUrl = newUrl; 151 | return false; 152 | }.bind(this); 153 | Utils.addCallback("onUrlChange",this.callback); 154 | }, 155 | 156 | componentWillUnmount : function(){ 157 | Utils.removeCallback("onUrlChange",this.callback); 158 | }, 159 | 160 | parseApiErrorData: function(data){ 161 | if (data === undefined) 162 | return this.clearAllErrors(); 163 | this.setState({ 164 | fieldErrors: data.errors || {}, 165 | errorMessage: data.message || undefined, 166 | }); 167 | }, 168 | 169 | disable : function(){ 170 | this.setState({disabled : true}); 171 | }, 172 | 173 | enable : function(){ 174 | this.setState({disabled : false}); 175 | }, 176 | 177 | validate: function(data) { 178 | if(data === undefined) data = this.state; 179 | this.clearAllErrors(); 180 | var validated = true; 181 | var fieldErrors = {} 182 | for(var name in this.fields){ 183 | var field = this.fields[name]; 184 | if (!(name in data) || data[name] === undefined || data[name] === ''){ 185 | if (field.required){ 186 | validated = false; 187 | fieldErrors[name] = field.requiredMessage || ("Please enter a "+ (field.name || name)+"."); 188 | } 189 | continue; 190 | } else { 191 | if ('regex' in field){ 192 | "^[\\w\\d-]{4,30}$" 193 | var regex = RegExp(field['regex']); 194 | if(!regex.test(data[name])){ 195 | validated = false; 196 | fieldErrors[name] = "Invalid value for " + (field.name || name) + "."; 197 | } 198 | } 199 | if ('validator' in field){ 200 | try { 201 | field.validator.bind(this)(data[name], name, data); 202 | } catch(e) { 203 | if (typeof e == "string") 204 | fieldErrors[name] = e 205 | else 206 | fieldErrors[name] = e.message; 207 | validated = false; 208 | } 209 | } 210 | } 211 | } 212 | this.setState({fieldErrors : fieldErrors}); 213 | return validated; 214 | }, 215 | 216 | }; 217 | 218 | return FormMixin; 219 | }); 220 | -------------------------------------------------------------------------------- /src/js/components/mixins/github_error_handler.jsx: -------------------------------------------------------------------------------- 1 | /** 2 | * @jsx React.DOM 3 | */ 4 | 5 | define([ 6 | "react", 7 | "js/utils", 8 | "jquery", 9 | "js/components/generic/modal" 10 | ],function ( 11 | React, 12 | Utils, 13 | $, 14 | Modal 15 | ) { 16 | 'use strict'; 17 | 18 | var GithubErrorHandler = { 19 | 20 | renderErrorPage : function(errorData){ 21 | var errorMessages = []; 22 | for(var resource in errorData){ 23 | var data = errorData[resource]; 24 | var remainingRequests = data.getResponseHeader('X-RateLimit-Remaining'); 25 | var githubMessage = data.responseJSON.message; 26 | if (remainingRequests == 0){ 27 | var message; 28 | if (Utils.isLoggedIn()) 29 | message =

    It seems that the rate limit is exceeded. Please wait a while before trying again.

    ; 30 | else 31 | message =

    It seems that you have exceeded the rate limit of the Github API. You can increase that limit by logging in.
    log in to increase rate limit

    ; 32 | 33 | errorMessages.push(
    34 | {message} 35 |
    ); 36 | } 37 | else 38 | errorMessages.push(

    The following error occurred when querying the Github API: {githubMessage}

    ); 39 | break; 40 | } 41 | return
    42 |
    43 |
    44 |
    45 |

    Oh no! An error occurred...

    46 | {errorMessages} 47 |
    48 |
    49 |
    50 |
    ; 51 | 52 | } 53 | }; 54 | 55 | return GithubErrorHandler; 56 | }); 57 | -------------------------------------------------------------------------------- /src/js/components/mixins/loader.jsx: -------------------------------------------------------------------------------- 1 | /** 2 | * @jsx React.DOM 3 | */ 4 | 5 | define(["react", 6 | "js/utils", 7 | "jquery", 8 | "js/api/all", 9 | ],function (React,Utils,$,Apis) { 10 | 'use strict'; 11 | 12 | var LoaderMixin = { 13 | 14 | updateLoadingState : function(role,state,noUpdate){ 15 | if (!this.isMounted()) 16 | return; 17 | 18 | for (var key in this.loadingState){ 19 | var list = this.loadingState[key]; 20 | if (key == state){ 21 | if (!(role in list)) 22 | list[role] = true; 23 | } else if (role in list) { 24 | delete list[role]; 25 | } 26 | } 27 | if ((!Object.keys(this.loadingState.inProgress).length)){ 28 | var loadingFailed = false; 29 | var d = this.coerceData(); 30 | var successCount = Object.keys(this.loadingState.succeeded).length 31 | + Object.keys(this.loadingState.failedNonCritical).length; 32 | if (this.resourcesList.length == successCount){ 33 | if (this.afterLoadingSuccess) { 34 | var res = this.afterLoadingSuccess(d); 35 | if (res) { 36 | d = res; 37 | } 38 | } 39 | } 40 | else 41 | loadingFailed = true; 42 | this.setState({data : d,loadingInProgress : false,loadingFailed :loadingFailed}); 43 | } 44 | }, 45 | 46 | coerceData : function(){ 47 | var d = {}; 48 | for(var role in this.data){ 49 | if (role in this.loadingState.succeeded) 50 | $.extend(d,this.data[role]); 51 | } 52 | return d; 53 | }, 54 | 55 | onLoadingError : function(handler,role,nonCritical){ 56 | return function(errorData){ 57 | if(!this.isMounted()) { 58 | return; 59 | } 60 | var stateErrorData = $.extend({}, this.state.errorData); 61 | stateErrorData[role] = errorData; 62 | this.setState({errorData: stateErrorData}); 63 | if (handler) 64 | handler.apply(this,arguments); 65 | if (nonCritical) 66 | this.updateLoadingState(role,"failedNonCritical"); 67 | else 68 | this.updateLoadingState(role,"failed"); 69 | }.bind(this); 70 | }, 71 | 72 | updateResource : function(role,data,props){ 73 | var resources = this.resources(props || this.props); 74 | for(var i in resources){ 75 | var resource = resources[i]; 76 | if (resource.name == role){ 77 | this.onResourceSuccess(resource,data); 78 | break; 79 | } 80 | } 81 | }, 82 | 83 | onLoadingSuccess : function(handler,role){ 84 | return function(){ 85 | if (!this.isMounted()) 86 | return; 87 | if (this.state.errorData[role]) 88 | delete this.state.errorData[role]; 89 | var update = true; 90 | if (arguments.length > 0) 91 | { 92 | var data = arguments[0]; 93 | 94 | if (this.reload && data.__cached__) 95 | update = false; 96 | 97 | //bug :/ 98 | //if (this.requestIds && this.requestIds[role] && data.__requestId__ && data.__requestId__ !== this.requestIds[role]) 99 | // update = false; 100 | } 101 | //we call the success handler 102 | if (update){ 103 | if (handler) 104 | handler.apply(this,arguments); 105 | this.updateLoadingState(role,"succeeded"); 106 | } 107 | }.bind(this); 108 | }, 109 | 110 | onResourceSuccess : function(resource,data){ 111 | var d = {}; 112 | 113 | var mapping = resource.mapping; 114 | if (!mapping){ 115 | mapping = {}; 116 | mapping[resource.name] = resource.name; 117 | } 118 | for(var key in mapping){ 119 | try{ 120 | d[key] = data[mapping[key]]; 121 | }catch(e){ 122 | d[key] = undefined; 123 | } 124 | } 125 | 126 | if (resource.success) 127 | { 128 | var res = resource.success(data,d); 129 | if (res) 130 | $.extend(d,res); 131 | } 132 | 133 | this.data[resource.name] = d; 134 | }, 135 | 136 | processResourcesList : function(props){ 137 | 138 | if (this.onLoadResources) 139 | this.onLoadResources(props); 140 | 141 | var resources = this.resourcesList; 142 | 143 | if (!resources.length){ 144 | this.setState({data : {},loadingInProgress : false,loadingFailed : false}); 145 | return; 146 | } 147 | 148 | var loadingList = []; 149 | for(var i in resources){ 150 | var resource = resources[i]; 151 | if (resource.before) 152 | if (!resource.before(props,resource)) 153 | continue; 154 | 155 | var params = []; 156 | if (resource.params) 157 | params = resource.params.slice(0); 158 | 159 | params.push(this.onLoadingSuccess(this.onResourceSuccess.bind(this,resource),resource.name)); 160 | params.push(this.onLoadingError(resource.error,resource.name,resource.nonCritical)); 161 | this.updateLoadingState(resource.name,"inProgress"); 162 | //we call the resource endpoint with the given parameters 163 | loadingList.push([resource,params]); 164 | } 165 | //We call the endpoints of the resources to be loaded 166 | loadingList.map(function(p){ 167 | var resource = p[0]; 168 | var params = p[1]; 169 | this.params[resource.name] = params; 170 | this.endpoints[resource.name] = resource.endpoint; 171 | if (this.reload || 172 | (resource.alwaysReload || (!this.lastData[resource.name])) || 173 | ((JSON.stringify(this.lastParams[resource.name]) != JSON.stringify(params)) 174 | || (resource.endpoint != this.lastEndpoints[resource.name]))){ 175 | this.requestIds[resource.name] = resource.endpoint.apply(this,params); 176 | } 177 | else{ 178 | //we take the previous value 179 | this.data[resource.name] = $.extend({},this.lastData[resource.name]); 180 | this.data[resource.name].__cached__ = false; 181 | this.requestIds[resource.name] = this.lastData[resource.name].__requestId__; 182 | this.updateLoadingState(resource.name,"succeeded"); 183 | } 184 | }.bind(this)); 185 | }, 186 | 187 | 188 | reloadResources : function(){ 189 | this.reload = true; 190 | this.resetLoadingState(); 191 | this.getResourcesList(this.props); 192 | this.processResourcesList(this.props); 193 | }, 194 | 195 | resetLoadingState : function(){ 196 | this.lastData = $.extend({},this.data); 197 | this.lastParams = $.extend({},this.params); 198 | this.lastEndpoints = $.extend({},this.endpoints); 199 | this.data = {}; 200 | this.endpoints = {}; 201 | this.params = {}; 202 | this.loadingState = { 203 | inProgress : {}, 204 | failed : {}, 205 | failedNonCritical : {}, 206 | succeeded : {}, 207 | }; 208 | }, 209 | 210 | getInitialState : function(){ 211 | return { 212 | loadingInProgress : true, 213 | loadingFailed : false, 214 | data : {}, 215 | errorData: {} 216 | }; 217 | }, 218 | 219 | getResourcesList : function(props){ 220 | this.resourcesList = this.resources(props) || []; 221 | }, 222 | 223 | componentWillMount : function(){ 224 | this.apis = Apis; 225 | this.data = {}; 226 | this.params = {}; 227 | this.endpoints = {}; 228 | this.reload = false; 229 | this.resourcesList = []; 230 | this.requestIds = {}; 231 | this._render = this.render; 232 | this.render = this.renderLoader; 233 | this.resetLoadingState(); 234 | this.getResourcesList(this.props); 235 | //cast to boolean: 236 | this.showComponentWhileLoading = !!this.showComponentWhileLoading; 237 | }, 238 | 239 | componentWillReceiveProps : function(newProps){ 240 | this.reload = false; 241 | this.resetLoadingState(); 242 | this.getResourcesList(newProps); 243 | this.processResourcesList(newProps); 244 | }, 245 | 246 | componentDidMount : function(){ 247 | this.processResourcesList(this.props); 248 | }, 249 | 250 | renderLoader: function(){ 251 | var loadingInProgress = this.state.loadingInProgress; 252 | var loadingFailed = this.state.loadingFailed; 253 | if (!this.resourcesList.length) 254 | loadingInProgress = false; 255 | if (loadingFailed || (loadingInProgress && (!this.showComponentWhileLoading))) 256 | return this.showLoader(); 257 | return this._render(); 258 | }, 259 | 260 | showLoader : function(){ 261 | if (this.silentLoading) 262 | return
    ; 263 | if (this.state.loadingFailed) 264 | return this.showErrorMessage(); 265 | else 266 | return this.showLoadingMessage(); 267 | }, 268 | 269 | showErrorMessage : function(){ 270 | 271 | var reload = function(e){ 272 | e.preventDefault(); 273 | location.reload(); 274 | }.bind(this); 275 | if (this.renderErrorPage) 276 | return this.renderErrorPage(this.state.errorData); 277 | var loadingErrorMessage; 278 | if (this.getErrorMessage) 279 | loadingErrorMessage = this.getErrorMessage(this.state.errorData); 280 | if (!loadingErrorMessage) 281 | loadingErrorMessage = '404 - Cannot load the requested resource.' 282 | if (this.inlineComponent) 283 | return {loadingErrorMessage}; 284 | else 285 | return
    286 |
    287 |
    288 |

    An error has occured.

    289 |

    {loadingErrorMessage}

    290 |
    291 |

    292 | Sorry for the inconvenience. Please try reloading this page. If the problem persists 293 | please contact us, we will do our best to fix this issue for you. 294 | 295 |

    296 |
    297 |

    298 | reload this page or  299 | contact us 300 |

    301 |
    302 |
    303 |
    ; 304 | }, 305 | 306 | showLoadingMessage : function(){ 307 | 308 | if (this.renderLoadingPlaceholder) 309 | return this.renderLoadingPlaceholder(); 310 | 311 | var loadingMessage; 312 | if (this.getLoadingMessage) 313 | loadingMessage = this.getLoadingMessage(); 314 | 315 | if (this.inlineComponent) { 316 | if (loadingMessage === undefined) 317 | loadingMessage = "Loading data..."; 318 | return

    {loadingMessage}

    ; 319 | } else { 320 | if (loadingMessage === undefined) 321 | loadingMessage =

    Please wait, loading data...

    ; 322 | return
    323 |
    324 |
    325 |
    326 | {loadingMessage} 327 |
    328 |
    329 |
    330 |
    ; 331 | } 332 | }, 333 | 334 | }; 335 | 336 | return LoaderMixin; 337 | }); 338 | -------------------------------------------------------------------------------- /src/js/components/mixins/tabs.jsx: -------------------------------------------------------------------------------- 1 | /** 2 | * @jsx React.DOM 3 | */ 4 | /*jshint quotmark:false */ 5 | /*jshint white:false */ 6 | /*jshint trailing:false */ 7 | /*jshint newcap:false */ 8 | /*global React, Router*/ 9 | 10 | define(["react", 11 | "js/utils" 12 | ],function (React, Utils) { 13 | 'use'+' strict'; 14 | 15 | var Tabs = React.createClass({ 16 | displayName: 'Tabs', 17 | 18 | getDefaultProps: function (){ 19 | return {tabs : [], activeTab : '', classes : 'nav nav-pills', onClick: function(){return true;}}; 20 | }, 21 | 22 | render: function() { 23 | var tabLinks = this.props.tabs.map(function(tab){ 24 | var active = tab.name == this.props.activeTab; 25 | var onClick = function(title, e){ 26 | e.tabTitle = title; 27 | if (e.button != 0 || e.altKey || e.ctrlKey || e.shiftKey || e.metaKey) 28 | return; 29 | if (active && !tab.activeHref) { 30 | e.preventDefault(); 31 | } 32 | if (tab.onClick !== undefined) 33 | return tab.onClick(e); 34 | return this.props.onClick(e); 35 | }.bind(this, tab.title); 36 | var href = tab.href; 37 | var classNames = []; 38 | if(active) { 39 | classNames.push("active"); 40 | if(tab.activeHref) href = tab.activeHref; 41 | } 42 | if(tab.disabled) { 43 | href = null; 44 | classNames.push("disabled"); 45 | } 46 | return
  • 47 | {tab.title} 48 |
  • ; 49 | }.bind(this)); 50 | 51 | return
      52 | {tabLinks} 53 |
    ; 54 | }, 55 | 56 | }); 57 | 58 | 59 | var TabsMixin = { 60 | 61 | setupTabs: function(tabs, initial_tab, classes) { 62 | this.tabs = tabs; 63 | this.classes = classes; 64 | this.tabsByName = {}; 65 | for(var i in this.tabs){ 66 | this.tabsByName[this.tabs[i].name] = this.tabs[i]; 67 | } 68 | var tabParam = this.tabParam || 'tab'; 69 | for (var i = 0; i < this.tabs.length; i++) { 70 | if (!this.tabs[i].href) { 71 | var params = {}; 72 | params[tabParam] = this.tabs[i].name; 73 | this.tabs[i].href = Utils.makeUrl(this.props.baseUrl,params,this.props.params) 74 | } 75 | } 76 | this.validTabs = {}; 77 | for (var i in tabs){ 78 | var tab = tabs[i]; 79 | this.validTabs[tab.name] = true; 80 | } 81 | if (this.isValidTab()) 82 | this.activeTab = this.getCurrentTabName(); 83 | else 84 | this.activeTab = initial_tab; 85 | }, 86 | 87 | getTabs : function(){ 88 | return ; 91 | }, 92 | 93 | getCurrentTabName : function(){ 94 | if (this.tabName) 95 | return this.tabName(); 96 | return this.props.params !== undefined ? this.props.params[this.tabParam || 'tab'] : undefined; 97 | }, 98 | 99 | getCurrentTabContent: function() { 100 | if (this.activeTab !== undefined && this.tabsByName[this.activeTab] !== undefined) 101 | return this.tabsByName[this.activeTab].content; 102 | return undefined; 103 | }, 104 | 105 | getCurrentTabTitle: function() { 106 | if (this.activeTab !== undefined && this.tabsByName[this.activeTab] !== undefined) 107 | return this.tabsByName[this.activeTab].title; 108 | return undefined; 109 | }, 110 | 111 | isValidTab: function() { 112 | var tabParam = this.tabParam || 'tab'; 113 | return this.getCurrentTabName() in this.tabsByName; 114 | } 115 | } 116 | 117 | return TabsMixin; 118 | }); 119 | -------------------------------------------------------------------------------- /src/js/components/organizations.jsx: -------------------------------------------------------------------------------- 1 | /** 2 | * @jsx React.DOM 3 | */ 4 | /*jshint quotmark:false */ 5 | /*jshint white:false */ 6 | /*jshint trailing:false */ 7 | /*jshint newcap:false */ 8 | /*global React, Router*/ 9 | 10 | /* 11 | Copyright (c) 2015 - Andreas Dewes 12 | 13 | This file is part of Gitboard. 14 | 15 | Gitboard is free software: you can redistribute it and/or modify 16 | it under the terms of the GNU Affero General Public License as 17 | published by the Free Software Foundation, either version 3 of the 18 | License, or (at your option) any later version. 19 | 20 | This program is distributed in the hope that it will be useful, 21 | but WITHOUT ANY WARRANTY; without even the implied warranty of 22 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 23 | GNU Affero General Public License for more details. 24 | 25 | You should have received a copy of the GNU Affero General Public License 26 | along with this program. If not, see . 27 | */ 28 | 29 | define(["react", 30 | "js/utils", 31 | "js/components/mixins/loader", 32 | "js/components/mixins/github_error_handler", 33 | "jquery" 34 | ], 35 | function (React,Utils,LoaderMixin,GithubErrorHandlerMixin,$) { 36 | 'use'+' strict'; 37 | 38 | 39 | var OrganizationItem = React.createClass({ 40 | 41 | render : function(){ 42 | console.log(this.props.organization); 43 | var description; 44 | if (this.props.organization.description) 45 | description =

    {this.props.organization.description}

    ; 46 | else 47 | description =

    no description available

    ; 48 | return ; 57 | } 58 | }); 59 | 60 | var Organizations = React.createClass({ 61 | 62 | mixins : [LoaderMixin,GithubErrorHandlerMixin], 63 | 64 | resources : function(props){ 65 | return [{ 66 | name : 'organizations', 67 | endpoint : this.apis.organization.getOrganizations, 68 | params : [{per_page : 100}], 69 | success : function(data,xhr){ 70 | var arr = []; 71 | for(var i in data) { 72 | if(data.hasOwnProperty(i) && !isNaN(+i)) { 73 | arr[+i] = data[i]; 74 | } 75 | } 76 | this.setState({organizations : arr}); 77 | }.bind(this) 78 | }]; 79 | }, 80 | 81 | displayName: 'Organizations', 82 | 83 | render: function () { 84 | var organizationItems = this.state.organizations.map(function(organization){ 85 | return ; 86 | }.bind(this)) 87 | 88 | return
    89 |
    90 |
    91 |

    Your organizations

    92 |
    93 |
    94 |
    95 | {organizationItems} 96 |
    97 |
    ; 98 | } 99 | }); 100 | 101 | return Organizations; 102 | }); 103 | -------------------------------------------------------------------------------- /src/js/components/repositories.jsx: -------------------------------------------------------------------------------- 1 | /** 2 | * @jsx React.DOM 3 | */ 4 | /*jshint quotmark:false */ 5 | /*jshint white:false */ 6 | /*jshint trailing:false */ 7 | /*jshint newcap:false */ 8 | /*global React, Router*/ 9 | 10 | /* 11 | Copyright (c) 2015 - Andreas Dewes 12 | 13 | This file is part of Gitboard. 14 | 15 | Gitboard is free software: you can redistribute it and/or modify 16 | it under the terms of the GNU Affero General Public License as 17 | published by the Free Software Foundation, either version 3 of the 18 | License, or (at your option) any later version. 19 | 20 | This program is distributed in the hope that it will be useful, 21 | but WITHOUT ANY WARRANTY; without even the implied warranty of 22 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 23 | GNU Affero General Public License for more details. 24 | 25 | You should have received a copy of the GNU Affero General Public License 26 | along with this program. If not, see . 27 | */ 28 | 29 | define(["react", 30 | "js/utils", 31 | "js/components/mixins/loader", 32 | "js/components/mixins/github_error_handler", 33 | "jquery" 34 | ], 35 | function (React,Utils,LoaderMixin,GithubErrorHandlerMixin,$) { 36 | 'use'+' strict'; 37 | 38 | var RepositoryItem = React.createClass({ 39 | 40 | render : function(){ 41 | return ; 51 | } 52 | }); 53 | 54 | var Repositories = React.createClass({ 55 | 56 | mixins : [LoaderMixin,GithubErrorHandlerMixin], 57 | 58 | resources : function(props){ 59 | 60 | var processRepositories = function(data,xhr){ 61 | var repos_array = []; 62 | for(var i in data) { 63 | if (data[i].open_issues == 0 || data[i].has_issues == false) 64 | continue; 65 | if(data.hasOwnProperty(i) && !isNaN(+i)) { 66 | repos_array[+i] = data[i]; 67 | } 68 | } 69 | this.setState({repositories : repos_array}); 70 | }.bind(this); 71 | 72 | if (props.data.organizationId || props.data.userId){ 73 | var r = [{ 74 | name : 'repositories', 75 | endpoint : (props.data.organizationId ? this.apis.organization.getRepositories : 76 | this.apis.user.getUserRepositories), 77 | params : [props.data.organizationId ? props.data.organizationId : props.data.userId, 78 | {per_page : 100,sort: 'pushed'}], 79 | success : processRepositories 80 | }]; 81 | if (props.data.organizationId) 82 | r.push({ 83 | name : 'organization', 84 | endpoint : this.apis.organization.getDetails, 85 | params : [props.data.organizationId,{}], 86 | success : function(data,d){ 87 | return {organization : data}; 88 | } 89 | }) 90 | else 91 | r.push({ 92 | name: 'user', 93 | endpoint : this.apis.user.getProfile, 94 | params : [], 95 | success : function(data,d){ 96 | return {user : data}; 97 | } 98 | }) 99 | return r; 100 | } 101 | else 102 | return [{ 103 | name : 'repositories', 104 | endpoint : this.apis.user.getRepositories, 105 | params : [{per_page : 100}], 106 | success : processRepositories 107 | }]; 108 | }, 109 | 110 | displayName: 'Repositories', 111 | 112 | render: function () { 113 | var repositoryItems = this.state.repositories.map(function(repository){ 114 | return ; 115 | }.bind(this)) 116 | 117 | if (repositoryItems.length == 0) 118 | repositoryItems = [

    Seems there is nothing to show here.

    ] 119 | 120 | var data = this.state.data; 121 | var title; 122 | if (data.organization) 123 | title = 'Repositories - '+(data.organization.name || data.organization.login); 124 | else 125 | title = 'Your repositories'; 126 | 127 | return
    128 |
    129 |
    130 |

    {title}

    131 |
    132 |
    133 |
    134 | {repositoryItems} 135 |
    136 |
    ; 137 | } 138 | }); 139 | 140 | return Repositories; 141 | }); 142 | -------------------------------------------------------------------------------- /src/js/components/user/logout.jsx: -------------------------------------------------------------------------------- 1 | /** 2 | * @jsx React.DOM 3 | */ 4 | /*jshint quotmark:false */ 5 | /*jshint white:false */ 6 | /*jshint trailing:false */ 7 | /*jshint newcap:false */ 8 | /*global React, Router*/ 9 | 10 | /* 11 | Copyright (c) 2015 - Andreas Dewes 12 | 13 | This file is part of Gitboard. 14 | 15 | Gitboard is free software: you can redistribute it and/or modify 16 | it under the terms of the GNU Affero General Public License as 17 | published by the Free Software Foundation, either version 3 of the 18 | License, or (at your option) any later version. 19 | 20 | This program is distributed in the hope that it will be useful, 21 | but WITHOUT ANY WARRANTY; without even the implied warranty of 22 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 23 | GNU Affero General Public License for more details. 24 | 25 | You should have received a copy of the GNU Affero General Public License 26 | along with this program. If not, see . 27 | */ 28 | 29 | define(["js/settings", 30 | "js/utils", 31 | "js/flash_messages", 32 | "react"],function ( 33 | settings, 34 | Utils, 35 | FlashMessagesService, 36 | React 37 | ) { 38 | 'use strict'; 39 | 40 | var Logout = React.createClass({ 41 | 42 | displayName: "LogoutComponent", 43 | 44 | 45 | componentWillMount : function(){ 46 | Utils.logout(); 47 | }, 48 | 49 | render: function () { 50 | 51 | var statusMessage; 52 | 53 | return
    54 |
    55 |   56 |
    57 |
    58 |
    59 |
    60 |

    You have been logged out

    61 |
    62 |
    63 |
    Security notice
    64 |
    65 |
    66 |

    We cannot delete your authorization from Github without your username and password. If you want to delete it, you can do so manually here (look for the gitboard token)

    67 |
    68 |
    69 |
    70 |
    71 |
    72 |
    73 | }, 74 | 75 | }); 76 | 77 | return Logout; 78 | 79 | }); 80 | -------------------------------------------------------------------------------- /src/js/config.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2015 - Andreas Dewes 3 | 4 | This file is part of Gitboard. 5 | 6 | Gitboard is free software: you can redistribute it and/or modify 7 | it under the terms of the GNU Affero General Public License as 8 | published by the Free Software Foundation, either version 3 of the 9 | License, or (at your option) any later version. 10 | 11 | This program is distributed in the hope that it will be useful, 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | GNU Affero General Public License for more details. 15 | 16 | You should have received a copy of the GNU Affero General Public License 17 | along with this program. If not, see . 18 | */ 19 | var require ={ 20 | paths: { 21 | "text" : "assets/js/text", 22 | "jquery" : "bower_components/jquery/dist/jquery", 23 | "bootstrap" : "bower_components/bootstrap/dist/js/bootstrap", 24 | "moment" : "bower_components/momentjs/moment", 25 | "director" : "bower_components/director/build/director", 26 | "react": "bower_components/react/react", 27 | "sprintf" :"bower_components/sprintf/src/sprintf", 28 | "markdown":"bower_components/markdown/lib/markdown", 29 | "marked" : "bower_components/marked/lib/marked", 30 | }, 31 | shim : { 32 | "director" : { 33 | exports : 'Router' 34 | }, 35 | "bootstrap" : { 36 | deps : ['jquery'] 37 | }, 38 | "prism" : { 39 | exports : 'Prism' 40 | }, 41 | "marked" : { 42 | exports : 'marked', 43 | }, 44 | }, 45 | baseUrl : "static", 46 | urlArgs: "bust=" + (new Date()).getTime() 47 | }; 48 | -------------------------------------------------------------------------------- /src/js/flash_messages.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2015 - Andreas Dewes 3 | 4 | This file is part of Gitboard. 5 | 6 | Gitboard is free software: you can redistribute it and/or modify 7 | it under the terms of the GNU Affero General Public License as 8 | published by the Free Software Foundation, either version 3 of the 9 | License, or (at your option) any later version. 10 | 11 | This program is distributed in the hope that it will be useful, 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | GNU Affero General Public License for more details. 15 | 16 | You should have received a copy of the GNU Affero General Public License 17 | along with this program. If not, see . 18 | */ 19 | define(["js/utils","js/subject"],function (Utils,Subject) { 20 | 'use strict'; 21 | 22 | var FlashMessages = function(){ 23 | Subject.Subject.call(this); 24 | this.currentMessages = {}; 25 | this.messageStream = []; 26 | this.messageCount = 0; 27 | }; 28 | 29 | var instance; 30 | 31 | function getInstance() 32 | { 33 | if (instance === undefined) 34 | instance = new FlashMessages(); 35 | return instance; 36 | } 37 | 38 | FlashMessages.prototype = new Subject.Subject(); 39 | FlashMessages.prototype.constructor = FlashMessages; 40 | 41 | FlashMessages.prototype.postMessage = function(data){ 42 | var messageId = this.messageCount; 43 | this.messageCount++; 44 | var messageData = {id: messageId,data : data,receivedAt : new Date()}; 45 | if (messageData.duration === undefined) 46 | messageData.duration = 2500; 47 | this.currentMessages[messageId] = messageData; 48 | this.messageStream.push(messageId); 49 | this.notify("newMessage",messageData); 50 | return messageId; 51 | } 52 | 53 | return getInstance(); 54 | 55 | }); 56 | -------------------------------------------------------------------------------- /src/js/helpers/issue_manager.js: -------------------------------------------------------------------------------- 1 | define(["js/utils","js/api/all","js/flash_messages"],function (Utils,Apis,FlashMessagesService) { 2 | 'use strict'; 3 | 4 | var IssueManager = function(params){ 5 | this.params = params; 6 | }; 7 | 8 | IssueManager.prototype.hasLabel = function(issue,label){ 9 | for (var i in issue.labels){ 10 | var issueLabel = issue.labels[i]; 11 | if (label.toLowerCase() == issueLabel.name.toLowerCase()) 12 | return true; 13 | } 14 | return false; 15 | } 16 | 17 | IssueManager.prototype._setStateImmediately = function(issue,state){ 18 | 19 | if (issue.state != state) 20 | issue.state = state; 21 | 22 | }; 23 | 24 | IssueManager.prototype._setState = function(issue,state,onSuccess,onError){ 25 | 26 | Apis.issue.updateIssue(this.params.repositoryId,issue.number,{state : state},onSuccess,onError); 27 | }; 28 | 29 | var categoryLabels = ['doing','to-do','awaiting-review','done']; 30 | 31 | IssueManager.prototype._setLabelsImmediately = function(issue,labels){ 32 | 33 | var labelsToRemove = issue.labels.filter(function(label){ 34 | return Object.keys(labels).indexOf(label.name) !== -1 && labels[label.name] == false; 35 | }) 36 | .map(function(label){return label.name;}); 37 | 38 | issue.labels = issue.labels.filter(function(label){ 39 | return (labelsToRemove.indexOf(label.name) == -1) ? true: false 40 | }); 41 | 42 | var issueLabelNames= issue.labels.map(function(label){return label.name}); 43 | 44 | var labelsToAdd = Object.keys(labels).filter(function(label){ 45 | if (!labels[label]) 46 | return false; 47 | if (issueLabelNames.indexOf(label) == -1) 48 | return true; 49 | return false;}); 50 | 51 | for(var i in labelsToAdd){ 52 | var labelToAdd = labelsToAdd[i]; 53 | issue.labels.push(this.params.labelsByName[labelToAdd] || {name : labelToAdd}); 54 | } 55 | 56 | return {remove : labelsToRemove,add : labelsToAdd}; 57 | 58 | } 59 | 60 | IssueManager.prototype._setLabels = function(issue,labelsToRemove,labelsToAdd,onSuccess,onError){ 61 | 62 | var removeCallback = onSuccess; 63 | 64 | if (labelsToRemove.length) 65 | for(var i in labelsToRemove){ 66 | removeCallback = function(oldCallback){ 67 | Apis.label.removeLabel(this.params.repositoryId,issue.number,labelsToRemove[i],oldCallback,onError); 68 | }.bind(this,removeCallback); 69 | } 70 | if (labelsToAdd.length) 71 | Apis.label.addLabels(this.params.repositoryId,issue.number,labelsToAdd,removeCallback,onError); 72 | else if (removeCallback) 73 | removeCallback(); 74 | 75 | } 76 | 77 | IssueManager.prototype.getMinutes = function(timeString){ 78 | var re = /([\d]+)(m|h|d)/i; 79 | var res = re.exec(timeString); 80 | if (res){ 81 | var number = parseInt(res[1]); 82 | switch(res[2]){ 83 | case 'm':return number; 84 | case 'h':return number*60; 85 | case 'd':return number*60*8; 86 | } 87 | } 88 | return undefined; 89 | }; 90 | 91 | IssueManager.prototype.formatMinutes = function(minutes){ 92 | if (minutes < 60) 93 | return minutes+'m'; 94 | else if (minutes < 8*60){ 95 | var hours = Math.floor(minutes/60); 96 | var minutes = minutes % 60; 97 | var str = hours+'h'; 98 | if (minutes) 99 | str+=' '+minutes+'m'; 100 | return str; 101 | } 102 | var days = Math.floor(minutes/60/8); 103 | var hours = Math.floor((minutes%(60*8))/60); 104 | var minutes = minutes % 60; 105 | str = days+'d'; 106 | if (hours) 107 | str+=' '+hours+'h'; 108 | if (minutes) 109 | str+=' '+minutes+'m'; 110 | return str; 111 | }; 112 | 113 | IssueManager.prototype.getTime = function(issue,type){ 114 | for (var i in issue.labels){ 115 | var label = issue.labels[i]; 116 | var re; 117 | if (type == 'estimate') 118 | re = /^time-estimate-([\d\w]+)$/i; 119 | else 120 | re = /^time-spent-([\d\w]+)$/i; 121 | var res = re.exec(label.name); 122 | if (res){ 123 | return res[1]; 124 | } 125 | } 126 | return null; 127 | }; 128 | 129 | IssueManager.prototype.setTime = function(issue,time,type){ 130 | var labels = {}; 131 | for(var i in issue.labels){ 132 | var label = issue.labels[i]; 133 | if (type == 'estimate'){ 134 | if (/^time-estimate-/i.exec(label.name)) 135 | labels[label.name] = false; 136 | }else{ 137 | if (/^time-spent-/i.exec(label.name)) 138 | labels[label.name] = false; 139 | } 140 | } 141 | if (time){ 142 | if (type == 'estimate') 143 | labels['time-estimate-'+time] = true; 144 | else 145 | labels['time-spent-'+time] = true; 146 | } 147 | 148 | var labelOps = this._setLabelsImmediately(issue,labels); 149 | 150 | if (this.params.onImmediateChange) 151 | this.params.onImmediateChange(); 152 | 153 | this._setLabels(issue,labelOps.remove,labelOps.add,this.params.onResourceChange,this.params.onError); 154 | } 155 | 156 | IssueManager.prototype.assignTo = function(issue,collaborator){ 157 | issue.assignee = collaborator; 158 | if (this.params.onImmediateChange) 159 | this.params.onImmediateChange(); 160 | Apis.issue.updateIssue(this.params.repositoryId, 161 | issue.number, 162 | {assignee : collaborator ? collaborator.login : null}, 163 | this.params.onResourceChange, 164 | this.params.onError); 165 | }; 166 | 167 | IssueManager.prototype.setMilestone = function(issue,milestone){ 168 | issue.milestone = milestone; 169 | if (this.params.onImmediateChange) 170 | this.params.onImmediateChange(); 171 | Apis.issue.updateIssue(this.params.repositoryId, 172 | issue.number, 173 | {milestone : milestone ? milestone.number : null}, 174 | this.params.onResourceChange, 175 | this.params.onError); 176 | }; 177 | 178 | IssueManager.prototype.isMemberOf = function(issue,category){ 179 | var closed = false; 180 | var labels = []; 181 | switch(category){ 182 | case 'toDo':labels=['to-do'];break; 183 | case 'doing':labels=['doing'];break; 184 | case 'awaitingReview':labels=['awaiting-review'];break; 185 | case 'done':closed = true;break; 186 | default:return false; 187 | } 188 | if (labels.length){ 189 | var found = false; 190 | for (var i in labels){ 191 | var label = labels[i]; 192 | if (this.hasLabel(issue,label)) 193 | found = true; 194 | } 195 | if (!found) 196 | return false; 197 | } 198 | if ((closed && issue.state == 'open') || ((!closed) && issue.state == 'closed')) 199 | return false; 200 | return true; 201 | }, 202 | 203 | IssueManager.prototype.moveTo = function(issue,category){ 204 | var closed = false; 205 | var labels = { 206 | 'to-do' : false, 207 | 'doing' : false, 208 | 'awaiting-review' : false, 209 | 'done' : false, 210 | } 211 | var labelsToAdd = []; 212 | var labelsToRemove = []; 213 | switch(category){ 214 | case 'toDo' : labels['to-do'] = true;break; 215 | case 'doing' : labels['doing'] = true;break; 216 | case 'awaitingReview' : labels['awaiting-review'] = true;break; 217 | case 'done' : closed = true;break; 218 | default: break; 219 | }; 220 | this._setStateImmediately(issue,closed ? 'closed' : 'open'); 221 | if (this.params.onImmediateChange) 222 | this.params.onImmediateChange(); 223 | var labelOps = this._setLabelsImmediately(issue,labels); 224 | this._setState(issue,closed ? 'closed' : 'open', 225 | function(){this._setLabels(issue,labelOps.remove,labelOps.add,this.params.onResourceChange,this.params.onError);}.bind(this),this.params.onError); 226 | }, 227 | 228 | IssueManager.prototype.issueCategories = { 229 | toDo : { 230 | title : 'To Do', 231 | label : 'to-do', 232 | }, 233 | doing : { 234 | title : 'Doing', 235 | label : 'doing', 236 | }, 237 | awaitingReview : { 238 | title : 'Awaiting Review', 239 | label : 'awaiting-review', 240 | }, 241 | done : { 242 | title : 'Done', 243 | label : null 244 | }, 245 | }; 246 | 247 | return IssueManager; 248 | 249 | }); -------------------------------------------------------------------------------- /src/js/main.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2015 - Andreas Dewes 3 | 4 | This file is part of Gitboard. 5 | 6 | Gitboard is free software: you can redistribute it and/or modify 7 | it under the terms of the GNU Affero General Public License as 8 | published by the Free Software Foundation, either version 3 of the 9 | License, or (at your option) any later version. 10 | 11 | This program is distributed in the hope that it will be useful, 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | GNU Affero General Public License for more details. 15 | 16 | You should have received a copy of the GNU Affero General Public License 17 | along with this program. If not, see . 18 | */ 19 | define( 20 | [ 21 | "js/components/app", 22 | "react", 23 | "js/utils", 24 | "js/routes", 25 | "director", 26 | "js/settings", 27 | ], 28 | function (MainApp, 29 | React, 30 | Utils, 31 | Routes, 32 | Director, 33 | Settings 34 | ) 35 | { 36 | 37 | var app = undefined; 38 | var appComponent = undefined; 39 | var router = new Director(); 40 | 41 | function initApp(){ 42 | appComponent = React.render(app, 43 | document.getElementById('app') 44 | ); 45 | } 46 | 47 | var A = React.createClass({ 48 | displayName: 'A', 49 | 50 | render : function(){ 51 | var props = $.extend({},this.props); 52 | if (this.props.plain){ 53 | delete props.plain; 54 | return React.DOM.a(props); 55 | } 56 | 57 | props.onClick = function(e){ 58 | if (this.props.onClick !== undefined) 59 | this.props.onClick(e); 60 | if (e.isDefaultPrevented()) 61 | return; 62 | //we only intercept the link if it's not an external one... 63 | if (props.href.substr(0,4) !== 'http'){ 64 | e.preventDefault(); 65 | router.setRoute(props.href); 66 | } 67 | }.bind(this); 68 | return React.DOM.a(props); 69 | } 70 | }); 71 | 72 | if (Settings.html5history){ 73 | window.A = A; 74 | Settings.onRedirectTo = function(url){ 75 | router.setRoute(url); 76 | }; 77 | } 78 | else 79 | window.A = "a"; 80 | 81 | function render(props){ 82 | if (window.ga !== undefined){ 83 | ga('send', 'pageview', { 84 | 'page': location.pathname + location.search + location.hash 85 | }); 86 | } 87 | if (props.params !== undefined) 88 | { 89 | props.stringParams = props.params.slice(1); 90 | props.params = Utils.getUrlParameters(props.params.slice(1)); 91 | } 92 | else 93 | props.params = {}; 94 | 95 | props.router = router; 96 | 97 | if (Settings.html5history){ 98 | var re = new RegExp(Settings.frontendUrl+'(.*)$') 99 | var result = re.exec(window.location.pathname); 100 | props.baseUrl = result[1]; 101 | } 102 | else{ 103 | var re = /^#(.*?)(\?.*)?$/i; 104 | var result = re.exec(window.location.hash); 105 | if (result !== null){ 106 | props.baseUrl = result[1]; 107 | } 108 | else 109 | props.baseUrl = "/app"; 110 | } 111 | 112 | if (app === undefined) 113 | { 114 | app = React.createElement(MainApp,props); 115 | initApp(); 116 | } 117 | else 118 | appComponent.replaceProps(props); 119 | } 120 | 121 | 122 | router.configure({html5history : Settings.html5history, 123 | notfound : function(url){ 124 | Utils.redirectTo(Utils.makeUrl('/')); 125 | }, 126 | strict : false }); 127 | 128 | router.param('repositoryId', /([\w\d\-\:\.\/]+)/); 129 | router.param('milestoneId', /(\d+)/); 130 | router.param('organizationId', /([\w\d\-\:\.]+)/); 131 | 132 | //We add URL-style parameters to all routes by decorating the original handler function. 133 | var routesWithParams = {}; 134 | 135 | var lastUrl; 136 | var lastUrlPattern; 137 | 138 | function generateCallBack(urlPattern, urlWithParams){ 139 | return function(){ 140 | var urlCallback = Routes[urlPattern]; 141 | var params = urlCallback.apply( 142 | this, 143 | Array.prototype.slice.call(arguments, 0, arguments.length-(Settings.html5history ? 0 : 1)) 144 | ); 145 | 146 | if (Utils.callbacks.onUrlChange && window.location.href != lastUrl){ 147 | for(var i in Utils.callbacks.onUrlChange){ 148 | var callback = Utils.callbacks.onUrlChange[i]; 149 | if (callback(urlPattern,urlWithParams,window.location.href) == false){ 150 | if (lastUrl) 151 | Utils.replaceUrl(lastUrl); 152 | return function(){}; 153 | } 154 | } 155 | } 156 | 157 | if (Settings.html5history) 158 | params.params = window.location.search; 159 | else 160 | params.params = arguments[arguments.length-1]; 161 | 162 | var url = window.location.href; 163 | params.url = url; 164 | //update title & meta tags 165 | Utils.setTitle(params.title); 166 | Utils.setMetaTag("description", (params.metaTags || {}).description); 167 | Utils.setMetaTag("keywords", ((params.metaTags || {}).keywords || []).concat(Settings.globalKeyKeywords || []).join(",")); 168 | //render the view 169 | render(params); 170 | //scroll to top if we navigated to a different page 171 | if(urlPattern !== lastUrlPattern) { 172 | document.documentElement.scrollTop = 0; 173 | } 174 | //set lastUrl and lastUrlPattern 175 | lastUrl = url;; 176 | lastUrlPattern = urlPattern; 177 | }; 178 | }; 179 | 180 | for (var url in Routes){ 181 | var urlWithParams = url+'/?(\\?.*)?'; 182 | 183 | if (Settings.html5history) 184 | urlWithParams = url; 185 | var prefix = ''; 186 | if (Settings.html5history) 187 | prefix = Settings.frontendUrl; 188 | routesWithParams[prefix+urlWithParams] = generateCallBack(url,urlWithParams); 189 | } 190 | 191 | router.mount(routesWithParams); 192 | router.init(); 193 | Utils.setRouter(router); 194 | 195 | if (!Settings.html5history){ 196 | if (window.location.hash === ''){ 197 | window.location.hash="#/"; 198 | } 199 | } 200 | else{ 201 | if (window.location.hash.substring(0,2) == '#/'){ 202 | window.location = window.location.protocol+'//'+window.location.host+Settings.frontendUrl+window.location.hash.substring(1); 203 | } 204 | } 205 | 206 | return {router: router, initApp: initApp}; 207 | 208 | }); 209 | -------------------------------------------------------------------------------- /src/js/request_notifier.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2015 - Andreas Dewes 3 | 4 | This file is part of Gitboard. 5 | 6 | Gitboard is free software: you can redistribute it and/or modify 7 | it under the terms of the GNU Affero General Public License as 8 | published by the Free Software Foundation, either version 3 of the 9 | License, or (at your option) any later version. 10 | 11 | This program is distributed in the hope that it will be useful, 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | GNU Affero General Public License for more details. 15 | 16 | You should have received a copy of the GNU Affero General Public License 17 | along with this program. If not, see . 18 | */ 19 | define(["js/utils","js/subject"],function (Utils,Subject) { 20 | 'use strict'; 21 | 22 | var RequestNotifier = function(){ 23 | Subject.Subject.call(this); 24 | this.currentRequests = {}; 25 | this.requestCount = 0; 26 | this.retryInterval = 1; 27 | }; 28 | 29 | var instance; 30 | 31 | function getInstance() 32 | { 33 | if (instance === undefined) 34 | instance = new RequestNotifier(); 35 | return instance; 36 | } 37 | 38 | RequestNotifier.prototype = new Subject.Subject(); 39 | RequestNotifier.prototype.constructor = RequestNotifier; 40 | 41 | RequestNotifier.prototype.register = function(requestId,data){ 42 | this.currentRequests[requestId] = {registeredAt : new Date(),data : data}; 43 | this.notify("registerRequest",requestId); 44 | this.notify("activeRequestCount",this.activeRequestCount()); 45 | return requestId; 46 | } 47 | 48 | RequestNotifier.prototype.success = function(requestId,data){ 49 | if (requestId in this.currentRequests) 50 | delete this.currentRequests[requestId]; 51 | this.notify("requestSucceeded",{requestId : requestId,data : data}); 52 | this.notify("activeRequestCount",this.activeRequestCount()); 53 | } 54 | 55 | RequestNotifier.prototype.error = function(requestId,xhr,data,e){ 56 | if (requestId in this.currentRequests) 57 | delete this.currentRequests[requestId]; 58 | if (xhr.readyState == 0){ 59 | var requestData = Utils.requestData(requestId); 60 | if (requestData == undefined) 61 | return; 62 | this.notify("connectionError",{requestId : requestId, 63 | xhr : xhr, 64 | data : data, 65 | requestData : requestData, 66 | e :e}) 67 | } 68 | else{ 69 | this.notify("requestFailed",{requestId : requestId,xhr : xhr,data : data,e : e}); 70 | } 71 | this.notify("activeRequestCount",this.activeRequestCount()); 72 | } 73 | 74 | RequestNotifier.prototype.activeRequestCount = function(requestId){ 75 | return Object.keys(this.currentRequests).length; 76 | } 77 | 78 | return {getInstance:getInstance}; 79 | 80 | }); 81 | -------------------------------------------------------------------------------- /src/js/routes.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2015 - Andreas Dewes 3 | 4 | This file is part of Gitboard. 5 | 6 | Gitboard is free software: you can redistribute it and/or modify 7 | it under the terms of the GNU Affero General Public License as 8 | published by the Free Software Foundation, either version 3 of the 9 | License, or (at your option) any later version. 10 | 11 | This program is distributed in the hope that it will be useful, 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | GNU Affero General Public License for more details. 15 | 16 | You should have received a copy of the GNU Affero General Public License 17 | along with this program. If not, see . 18 | */ 19 | define( 20 | [ 21 | "js/utils", 22 | "js/components/sprintboard", 23 | "js/components/milestones", 24 | "js/components/repositories", 25 | "js/components/organizations", 26 | "js/components/user/login", 27 | "js/components/user/logout" 28 | ], 29 | function ( 30 | Utils, 31 | SprintBoard, 32 | Milestones, 33 | Repositories, 34 | Organizations, 35 | Login, 36 | Logout 37 | ) 38 | { 39 | 40 | var routes = { 41 | '' : 42 | function(){return { 43 | data : {}, 44 | component: Repositories} 45 | }, 46 | '/sprintboard/:repositoryId/:milestoneId': 47 | function(repositoryId,milestoneId){return { 48 | anonOk : true, 49 | data : {repositoryId : repositoryId,milestoneId : milestoneId}, 50 | component: SprintBoard, 51 | }}, 52 | '/sprintboard/:repositoryId': 53 | function(repositoryId){return { 54 | anonOk : true, 55 | data : {repositoryId : repositoryId,milestoneId : null}, 56 | component: SprintBoard, 57 | }}, 58 | '/repositories': 59 | function(){return { 60 | data : {}, 61 | component: Repositories 62 | }}, 63 | '/repositories/:organizationId': 64 | function(organizationId){return { 65 | data : {organizationId : organizationId}, 66 | anonOk : true, 67 | component : Repositories 68 | }}, 69 | '/user_repositories/:userId': 70 | function(userId){return { 71 | data : {userId : userId}, 72 | anonOk : true, 73 | component : Repositories 74 | }}, 75 | '/organizations': 76 | function(){return { 77 | data : {}, 78 | component : Organizations 79 | }}, 80 | '/milestones/:repositoryId': 81 | function(repositoryId){return { 82 | anonOk : true, 83 | data : {repositoryId : repositoryId}, 84 | component : Milestones 85 | }}, 86 | '/login': 87 | function(){return { 88 | data : {}, 89 | anonOk: true, 90 | component : Login 91 | }}, 92 | '/logout' : 93 | function(){return { 94 | data : {}, 95 | anonOk : true, 96 | component : Logout 97 | }}, 98 | }; 99 | 100 | return routes; 101 | }); 102 | -------------------------------------------------------------------------------- /src/js/settings.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2015 - Andreas Dewes 3 | 4 | This file is part of Gitboard. 5 | 6 | Gitboard is free software: you can redistribute it and/or modify 7 | it under the terms of the GNU Affero General Public License as 8 | published by the Free Software Foundation, either version 3 of the 9 | License, or (at your option) any later version. 10 | 11 | This program is distributed in the hope that it will be useful, 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | GNU Affero General Public License for more details. 15 | 16 | You should have received a copy of the GNU Affero General Public License 17 | along with this program. If not, see . 18 | */ 19 | define(["jquery","js/env_settings"],function ($,envSettings,Utils) { 20 | 'use strict'; 21 | 22 | var settings = { 23 | scopes: ['read:org','repo'], 24 | source : 'https://api.github.com', 25 | useCache : true, 26 | cacheValidity : 3600*24, //cache expires after 24 hours 27 | cacheRefreshLimit : 0.0, //how long until we fetch the new value of something? 28 | }; 29 | return $.extend($.extend({},settings),envSettings); 30 | }) 31 | -------------------------------------------------------------------------------- /src/js/settings_hashtag_navigation.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2015 - Andreas Dewes 3 | 4 | This file is part of Gitboard. 5 | 6 | Gitboard is free software: you can redistribute it and/or modify 7 | it under the terms of the GNU Affero General Public License as 8 | published by the Free Software Foundation, either version 3 of the 9 | License, or (at your option) any later version. 10 | 11 | This program is distributed in the hope that it will be useful, 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | GNU Affero General Public License for more details. 15 | 16 | You should have received a copy of the GNU Affero General Public License 17 | along with this program. If not, see . 18 | */ 19 | 20 | define([],function (Utils) { 21 | 'use strict'; 22 | 23 | var settings = { 24 | frontendUrl : '', 25 | html5history : false, 26 | }; 27 | return settings; 28 | }) 29 | -------------------------------------------------------------------------------- /src/js/settings_html5_navigation.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2015 - Andreas Dewes 3 | 4 | This file is part of Gitboard. 5 | 6 | Gitboard is free software: you can redistribute it and/or modify 7 | it under the terms of the GNU Affero General Public License as 8 | published by the Free Software Foundation, either version 3 of the 9 | License, or (at your option) any later version. 10 | 11 | This program is distributed in the hope that it will be useful, 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | GNU Affero General Public License for more details. 15 | 16 | You should have received a copy of the GNU Affero General Public License 17 | along with this program. If not, see . 18 | */ 19 | define([],function (Utils) { 20 | 'use strict'; 21 | 22 | var settings = { 23 | frontendUrl : '', 24 | html5history : true, 25 | }; 26 | return settings; 27 | }) 28 | -------------------------------------------------------------------------------- /src/js/subject.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2015 - Andreas Dewes 3 | 4 | This file is part of Gitboard. 5 | 6 | Gitboard is free software: you can redistribute it and/or modify 7 | it under the terms of the GNU Affero General Public License as 8 | published by the Free Software Foundation, either version 3 of the 9 | License, or (at your option) any later version. 10 | 11 | This program is distributed in the hope that it will be useful, 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | GNU Affero General Public License for more details. 15 | 16 | You should have received a copy of the GNU Affero General Public License 17 | along with this program. If not, see . 18 | */ 19 | define([],function (settings) { 20 | 'use strict'; 21 | 22 | function Subject(){ 23 | this.observers = []; 24 | } 25 | 26 | Subject.prototype = { 27 | subscribe : function (callback) { 28 | this.observers.push(callback); 29 | }, 30 | unsubscribe : function(callback) { 31 | var new_observers = []; 32 | for (var i in this.observers) 33 | { 34 | if (this.observers[i] !== callback) 35 | new_observers.push(this.observers[i]); 36 | } 37 | this.observers = new_observers; 38 | }, 39 | notify : function(property,data) { 40 | var new_observers = []; 41 | this.observers.forEach(function (cb) { 42 | try { 43 | cb(this,property,data); 44 | new_observers.push(cb); 45 | } 46 | catch(e){throw e; 47 | }}.bind(this) 48 | ); 49 | this.observers = new_observers; 50 | } 51 | } 52 | 53 | return { 54 | Subject : Subject 55 | }; 56 | }); 57 | -------------------------------------------------------------------------------- /src/scss/_above_the_fold.scss: -------------------------------------------------------------------------------- 1 | //Import CSS modules 2 | @import '../bower_components/bootstrap/dist/css/bootstrap.min.css'; 3 | @import '../bower_components/bootstrap-material-design/dist/css/material.min.css'; 4 | @import '../bower_components/font-mfizz/css/font-mfizz.css'; 5 | @import '../bower_components/font-awesome/css/font-awesome.min.css'; 6 | @import '../bower_components/octicons/octicons/octicons.css'; 7 | @import '../assets/css/styles.css'; 8 | 9 | // Import modules 10 | @import "modules/colors"; 11 | @import "modules/mixins"; 12 | @import "modules/typography"; 13 | @import "modules/spaces"; 14 | @import "modules/border_radius"; 15 | 16 | // Import partials 17 | @import "partials/request_indicator"; 18 | @import "partials/sprintboard"; 19 | @import "partials/milestones"; 20 | @import "partials/organizations"; 21 | @import "partials/repositories"; 22 | @import "partials/modal"; -------------------------------------------------------------------------------- /src/scss/_below_the_fold.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adewes/gitboard/4824cf068a9352959b795fddb2093e6ef6b37f3f/src/scss/_below_the_fold.scss -------------------------------------------------------------------------------- /src/scss/main.scss: -------------------------------------------------------------------------------- 1 | // Import files 2 | @import "above_the_fold"; 3 | @import "below_the_fold"; 4 | @import url(https://fonts.googleapis.com/css?family=Roboto+Condensed:400,300italic,300); 5 | 6 | 7 | body{ 8 | padding-bottom: 50px; 9 | } 10 | 11 | .navbar-gitboard{ 12 | background: $logo-color !important; 13 | color:#fff !important; 14 | } 15 | 16 | body,h1,h2,h3,h4,h5,h6 { 17 | font-family: 'Roboto Condensed', sans-serif; 18 | font-weight:300; 19 | } 20 | 21 | 22 | img.github-ribbon{ 23 | z-index:2000; 24 | position: fixed; 25 | bottom: 0; 26 | right: 0; 27 | border: 0; 28 | } 29 | 30 | @media(min-width:992px){ 31 | #features{ 32 | img{ 33 | width:80%; 34 | } 35 | } 36 | } 37 | 38 | #features{ 39 | 40 | p{ 41 | margin-top:40px; 42 | text-align:left; 43 | } 44 | div.row{ 45 | margin-bottom:40px; 46 | } 47 | div.diagram{ 48 | text-align:center; 49 | } 50 | } 51 | 52 | footer {background:#fff; padding-top:10px;} 53 | 54 | a {color:#006; font-weight:bolder;} 55 | a:hover,a:active {color: #008; text-decoration:none;} 56 | a:visited {color:#006;} 57 | 58 | @media (max-width:991px){ 59 | 60 | #features{ 61 | img{ 62 | width:60%; 63 | } 64 | } 65 | 66 | img.github-ribbon{ 67 | display:none; //we do not show the ribbon on mobile devices since it obstructs everything... 68 | } 69 | footer{ 70 | margin-top:20px; 71 | } 72 | .navbar-fixed-bottom{ 73 | position:relative; 74 | } 75 | } 76 | 77 | @media (min-width: 992px) { 78 | .narrow-container { 79 | width: 970px; 80 | } 81 | 82 | .jumbotron{ 83 | margin-top:30px; 84 | margin-bottom:50px !important; 85 | } 86 | } 87 | 88 | #app{ 89 | padding-top:20px !important; 90 | } 91 | 92 | .left-tilt{ 93 | -ms-transform: rotate(-1deg); /* IE 9 */ 94 | -webkit-transform: rotate(-1deg); /* Chrome, Safari, Opera */ 95 | transform: rotate(-1deg); 96 | } 97 | 98 | .right-tilt{ 99 | -ms-transform: rotate(1deg); /* IE 9 */ 100 | -webkit-transform: rotate(1deg); /* Chrome, Safari, Opera */ 101 | transform: rotate(1deg); 102 | } 103 | -------------------------------------------------------------------------------- /src/scss/modules/_border_radius.scss: -------------------------------------------------------------------------------- 1 | $_lg: 5px; 2 | $_sm: 2px; 3 | 4 | .top-radius-lg { 5 | @include border-top-radius($_lg); 6 | } 7 | 8 | .bottom-radius-lg { 9 | @include border-bottom-radius($_lg); 10 | } 11 | 12 | .all-radius-lg { 13 | @include border-radius($_lg); 14 | } 15 | 16 | .top-radius-sm { 17 | @include border-top-radius($_sm); 18 | } 19 | 20 | .bottom-radius-sm { 21 | @include border-bottom-radius($_sm); 22 | } 23 | 24 | .all-radius-sm { 25 | @include border-radius($_sm); 26 | } 27 | 28 | .border-radius-top-none { 29 | border-top-left-radius: 0px !important; 30 | border-top-right-radius: 0px !important; 31 | } 32 | 33 | .border-bottom-none { 34 | border-bottom: none !important; 35 | } 36 | 37 | .border-top-none { 38 | border-top: none !important; 39 | } 40 | 41 | .bordered-top { 42 | border-top: 1px solid get-color(grayscale, mid-light); 43 | } -------------------------------------------------------------------------------- /src/scss/modules/_colors.scss: -------------------------------------------------------------------------------- 1 | // Colors 2 | 3 | /* At a minimum every palette defines a base colour, and then optionally adds tones use the following naming pattern: 4 | - x-dark 5 | - dark * 6 | - mid-dark 7 | - base (default) 8 | - mid-light 9 | - light 10 | - x-light 11 | */ 12 | 13 | //Function to get palettes 14 | @function get-color($palette, $tone: 'base', $alpha: 1) { 15 | @if map-has-key($palettes, $palette) { 16 | @if map-has-key(map-get($palettes, $palette), $tone) { 17 | @return rgba(palette($palette, $tone), $alpha); 18 | } 19 | @else { 20 | @warn "No palette `#{$palette}` with tone `#{$tone}`" 21 | } 22 | } 23 | @else { 24 | @warn "No palette with name `#{$palette}` in $palettes"; 25 | } 26 | @return null; 27 | } 28 | 29 | //Function to colors 30 | @function palette($palette, $tone: 'base') { 31 | @return map-get(map-get($palettes, $palette), $tone); 32 | } 33 | 34 | /* Function to get colors with transparency 35 | Usage: 36 | get-color(black); 37 | get-color(black, light); 38 | get-color(black, $alpha: 0.8) 39 | get-color(black, light, 0.8) 40 | */ 41 | 42 | // Base colors of paletes 43 | $_color-base-black: #000; 44 | $_color-base-white: #fff; 45 | $_color-base-grey: #777; 46 | $_color-base-lightgrey: #ddd; 47 | $_color-base-grayscale: #999; 48 | $_color-base-red: #ff0000; 49 | $_color-base-green: #00ff00; 50 | $_color-base-blue: #0000ff; 51 | $_color-base-yellow: #ffff00; 52 | $_color-base-orange: #ffaa00; 53 | $_color-base-purple: #a020f0; 54 | 55 | $logo-color: #359EFF; 56 | 57 | 58 | $palettes: ( 59 | black: ( 60 | base: $_color-base-black, 61 | ), 62 | white: ( 63 | base: $_color-base-white, 64 | ), 65 | grayscale: ( 66 | x-dark: $_color-base-black, 67 | dark: #333, 68 | mid-dark: #666, 69 | mid-dark-2: #555, 70 | base: $_color-base-grayscale, 71 | mid-light-3: #bbb, 72 | mid-light-2: #aaa, 73 | mid-light: #ccc, 74 | light: #eee, 75 | x-light: $_color-base-white, 76 | ), 77 | qc: ( 78 | base: $_color-base-white, 79 | //blue: #0079d2, 80 | blue: #135389, 81 | blueshadow: #08406F, 82 | blue-opaq: rgba(#135389, 0.5), 83 | darkblue: #005197, 84 | darkerblue: #00396a, 85 | green: #058A59, 86 | darkgreen: #04613E, 87 | grey: #44424a, 88 | darkgrey: #191919, 89 | active-grey: #e7ecf3, 90 | active-grey-2: #768096, 91 | orange: #EE5720, 92 | darkorange: #BD4519, 93 | ), 94 | severity: ( 95 | 1: #991f15, 96 | 2: #dc7f23, 97 | 3: #ddb438, 98 | 4: #53b89c, 99 | ), 100 | alert: ( 101 | info: #1ba2d4, 102 | success: #4ab544, 103 | warning: #ce9b11, 104 | danger: #d13d3d, 105 | info-light: #e4eef9, 106 | success-light: #eaf6e7, 107 | warning-light: #fcf5e4, 108 | danger-light: #f9e7e7, 109 | info-medium: #2cb2e4, 110 | success-medium: #20c524, 111 | warning-medium: #e1b02a, 112 | danger-medium:#e34141, 113 | ), 114 | calltoaction: ( 115 | gradient-start: #008bd9, 116 | gradient-end: #006ccb, 117 | orange-gradient-start: #f08f27, 118 | orange-gradient-end: #ea7126, 119 | green-gradient-start: #2bc53d, 120 | green-gradient-end: #019b13, 121 | ), 122 | gold: ( 123 | dark: #e9c973, 124 | light: #ffe8aa, 125 | ), 126 | orange: ( 127 | mid-dark: #ff7700, 128 | base: $_color-base-orange, 129 | ), 130 | red: ( 131 | dark: darken($_color-base-red, 10%), 132 | base: $_color-base-red, 133 | mid-light: #cc4444, 134 | light: #fb9a99, 135 | ), 136 | green: ( 137 | dark: #33a029, 138 | base: $_color-base-green, 139 | mid-light: #ccebc5, 140 | light: #aaff00, 141 | ), 142 | blue: ( 143 | dark: darken($_color-base-blue, 10%), 144 | mid-dark: #0044aa, 145 | base: $_color-base-blue, 146 | mid-light: #1f78b4, 147 | light: #a6cee3, 148 | ), 149 | yellow: ( 150 | dark: darken($_color-base-yellow, 10%), 151 | base: $_color-base-yellow, 152 | light: lighten($_color-base-yellow, 10%), 153 | x-light: #ffffaa, 154 | ), 155 | grey: ( 156 | x-dark: #333, 157 | dark: #444, 158 | mid-dark: #555, 159 | base: $_color-base-grey, 160 | mid-light: #888, 161 | light: #999, 162 | x-light: #aaa, 163 | ), 164 | lightgrey: ( 165 | mid-dark: #ccc, 166 | base: $_color-base-lightgrey, 167 | mid-light: #eee, 168 | light: #f8f8f8, 169 | bg-light: #f2f2f2, 170 | x-light: #fbfbfb, 171 | xx-light: #fefefe, 172 | ), 173 | purple: ( 174 | base: $_color-base-purple, 175 | mid-light: #7777aa, 176 | ), 177 | ); 178 | -------------------------------------------------------------------------------- /src/scss/modules/_mixins.scss: -------------------------------------------------------------------------------- 1 | @mixin border-radius($radius) { 2 | -webkit-border-radius: $radius; 3 | border-radius: $radius; 4 | background-clip: padding-box; /* stops bg color from leaking outside the border: */ 5 | } 6 | 7 | // Single side border-radius 8 | 9 | @mixin border-top-radius($radius) { 10 | -webkit-border-top-right-radius: $radius; 11 | border-top-right-radius: $radius; 12 | -webkit-border-top-left-radius: $radius; 13 | border-top-left-radius: $radius; 14 | background-clip: padding-box; 15 | } 16 | @mixin border-right-radius($radius) { 17 | -webkit-border-bottom-right-radius: $radius; 18 | border-bottom-right-radius: $radius; 19 | -webkit-border-top-right-radius: $radius; 20 | border-top-right-radius: $radius; 21 | background-clip: padding-box; 22 | } 23 | @mixin border-bottom-radius($radius) { 24 | -webkit-border-bottom-right-radius: $radius; 25 | border-bottom-right-radius: $radius; 26 | -webkit-border-bottom-left-radius: $radius; 27 | border-bottom-left-radius: $radius; 28 | background-clip: padding-box; 29 | } 30 | @mixin border-left-radius($radius) { 31 | -webkit-border-bottom-left-radius: $radius; 32 | border-bottom-left-radius: $radius; 33 | -webkit-border-top-left-radius: $radius; 34 | border-top-left-radius: $radius; 35 | background-clip: padding-box; 36 | } 37 | 38 | @mixin logos($margin-right: 20px) { 39 | .logos { 40 | text-align: center; 41 | margin-right: $margin-right; 42 | margin-bottom: 15px; 43 | width: auto !important; 44 | } 45 | 46 | .logos-big { 47 | text-align: center; 48 | margin-right: $margin-right; 49 | margin-bottom: 15px; 50 | height: 130px !important; 51 | width: auto !important; 52 | } 53 | 54 | .logos-small { 55 | text-align: center; 56 | margin-right: $margin-right; 57 | margin-bottom: 15px; 58 | height: 30px !important; 59 | width: auto !important; 60 | } 61 | } -------------------------------------------------------------------------------- /src/scss/modules/_spaces.scss: -------------------------------------------------------------------------------- 1 | $slug-top: space-top; 2 | $slug-bottom: space-bottom; 3 | $slug-right: space-right; 4 | $slug-left: space-left; 5 | $maximum: 30; 6 | $step: 5; 7 | 8 | // Generates space-top-x and space-bottom-x classes 9 | // x = i * steps; i(max) = maximum 10 | 11 | @for $i from 0 through $maximum { 12 | $space: $i * $step; 13 | .#{$slug-top}-#{$space} { 14 | margin-top: $space#{px !important}; 15 | } 16 | .#{$slug-bottom}-#{$space} { 17 | margin-bottom: $space#{px !important}; 18 | } 19 | .#{$slug-right}-#{$space} { 20 | margin-right: $space#{px !important}; 21 | } 22 | .#{$slug-left}-#{$space} { 23 | margin-left: $space#{px !important}; 24 | } 25 | } -------------------------------------------------------------------------------- /src/scss/modules/_typography.scss: -------------------------------------------------------------------------------- 1 | // Typography 2 | 3 | // FONT SETTING 4 | @function font($font, $size: 'base', $weight: 'normal') { 5 | @return map-get(map-get($fonts, $font), $tone); 6 | } 7 | 8 | // $font_family: 9 | 10 | // $fonts: ( 11 | // h1: ( 12 | // font-family: $font_family, 13 | // font-weight: 700, 14 | // font-size: 16px, 15 | // line-height: 16 | // ), 17 | // ); 18 | 19 | 20 | // font: { 21 | // family: fantasy; 22 | // size: 30em; 23 | // weight: bold; 24 | // } 25 | 26 | -------------------------------------------------------------------------------- /src/scss/partials/_milestones.scss: -------------------------------------------------------------------------------- 1 | .milestone-item{ 2 | a{ 3 | text-decoration: none; 4 | color: inherit; 5 | background:inherit; 6 | } 7 | } -------------------------------------------------------------------------------- /src/scss/partials/_modal.scss: -------------------------------------------------------------------------------- 1 | .modal{ 2 | .modal-footer{ 3 | background:#eee; 4 | border-top:1px solid #ddd; 5 | padding:10px !important; 6 | margin:0; 7 | text-align:left; 8 | } 9 | } -------------------------------------------------------------------------------- /src/scss/partials/_organizations.scss: -------------------------------------------------------------------------------- 1 | .organization-item{ 2 | 3 | position:relative; 4 | overflow:hidden; 5 | img.avatar{ 6 | position:absolute; 7 | opacity:0.5; 8 | z-index:1; 9 | right:10px; 10 | top:10px; 11 | } 12 | 13 | a { 14 | text-decoration: none; 15 | color: inherit; 16 | background:inherit; 17 | } 18 | 19 | .panel-body { 20 | position:relative; 21 | z-index:10; 22 | } 23 | 24 | p{ 25 | color:#777; 26 | font-size:10px; 27 | } 28 | } -------------------------------------------------------------------------------- /src/scss/partials/_repositories.scss: -------------------------------------------------------------------------------- 1 | .repository-item{ 2 | a{ 3 | text-decoration: none; 4 | color: inherit; 5 | background:inherit; 6 | } 7 | } -------------------------------------------------------------------------------- /src/scss/partials/_request_indicator.scss: -------------------------------------------------------------------------------- 1 | 2 | p.request-indicator { 3 | 4 | width: auto; 5 | position: absolute; 6 | top: 0; 7 | left: 50%; 8 | margin-left: -70px; 9 | border: 0px solid get-color(gold, dark); 10 | border-top: none; 11 | padding: 5px 15px; 12 | font-size: 12px; 13 | z-index: 1000; 14 | background: get-color(yellow, light); 15 | color: #000; 16 | text-align: center; 17 | 18 | -webkit-box-shadow: 0px 2px 3px 0px get-color(black, $alpha: 0.3); 19 | -moz-box-shadow: 0px 2px 3px 0px get-color(black, $alpha: 0.3); 20 | box-shadow: 0px 2px 3px 0px get-color(black, $alpha: 0.3); 21 | 22 | > a { 23 | color: #000; 24 | 25 | &:hover { 26 | text-decoration: none; 27 | } 28 | > .fa-spin, > .fa-exclamation-triangle { 29 | margin-right: 5px; 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/scss/partials/_sprintboard.scss: -------------------------------------------------------------------------------- 1 | body{ 2 | min-height:100%; 3 | height:100%; 4 | } 5 | 6 | .sprintboard 7 | { 8 | 9 | a.refresh-link{ 10 | color:#aaa; 11 | } 12 | 13 | @media (min-width: 992px){ 14 | 15 | .row{ 16 | overflow: hidden; 17 | } 18 | 19 | [class*="col-"]{ 20 | margin-bottom: -99999px; 21 | padding-bottom: 99999px; 22 | } 23 | } 24 | 25 | .issue-list.active{ 26 | background:#ddd; 27 | } 28 | 29 | .issue-list.active .issue-item{ 30 | pointer-events:none; 31 | } 32 | 33 | .issue-details img{ 34 | max-width:100%; 35 | } 36 | 37 | span.due{ 38 | color:#555; 39 | font-weight:100; 40 | } 41 | 42 | span.estimates{ 43 | font-size:12px; 44 | } 45 | 46 | span.time-estimate, 47 | span.time-spent 48 | { 49 | color:#555; 50 | font-weight:100; 51 | } 52 | 53 | span.time-estimate:before, 54 | span.time-spent:before 55 | { 56 | font-family:FontAwesome; 57 | padding-right:4px; 58 | color:#aaa; 59 | } 60 | 61 | span.time-spent{ 62 | padding-left:4px; 63 | color:#000; 64 | } 65 | 66 | span.time-estimate:before 67 | { 68 | content:'\f251'; 69 | } 70 | 71 | span.time-spent:before 72 | { 73 | content:'\f253'; 74 | } 75 | 76 | .issue-item.dragged{ 77 | -ms-transform: rotate(-2deg); /* IE 9 */ 78 | -webkit-transform: rotate(-2deg); /* Chrome, Safari, Opera */ 79 | transform: rotate(-2deg); 80 | background:#fdd; 81 | } 82 | 83 | .issue-item{ 84 | 85 | .modal{ 86 | overflow:visible; 87 | } 88 | 89 | span.issue-labels{ 90 | font-size:12px; 91 | padding:0; 92 | margin-left:10px; 93 | } 94 | 95 | .issue-label{ 96 | margin-right:4px; 97 | padding:2px; 98 | font-weight:300; 99 | } 100 | 101 | .btn-group{ 102 | margin-top:0; 103 | } 104 | 105 | div.selectors{ 106 | 107 | clear:both; 108 | margin-bottom:10px; 109 | z-index:9999; 110 | 111 | div.btn-group{ 112 | margin:0; 113 | padding:0; 114 | } 115 | span.legend{ 116 | display:block; 117 | font-size:12px; 118 | color:#999; 119 | padding:0; 120 | margin:0; 121 | } 122 | } 123 | 124 | div.selectors:after { 125 | visibility: hidden; 126 | display: block; 127 | font-size: 0; 128 | content: " "; 129 | clear: both; 130 | height: 0; 131 | } 132 | 133 | a.github-link{ 134 | color:#777; 135 | padding-left:4px; 136 | } 137 | 138 | a { 139 | text-decoration: none; 140 | color: inherit; 141 | background:inherit; 142 | } 143 | 144 | .panel-heading{ 145 | position:relative; 146 | } 147 | 148 | .panel-footer{ 149 | padding:2px; 150 | font-size:12px; 151 | } 152 | 153 | .panel-body{ 154 | padding:4px; 155 | } 156 | 157 | p.right-symbols{ 158 | position:absolute; 159 | right:2px; 160 | top:0; 161 | text-align: right; 162 | } 163 | 164 | img.assignee { 165 | } 166 | 167 | 168 | h5{ 169 | font-weight:bold !important; 170 | } 171 | 172 | @for $i from 0 through 16{ 173 | .label-#{($i)+1} { 174 | position: relative; 175 | position:absolute; 176 | top:0; 177 | left:#{$i*4}px; 178 | width:3px; 179 | height:16px; 180 | 181 | } 182 | } 183 | 184 | } 185 | 186 | } 187 | -------------------------------------------------------------------------------- /src/templates/app.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | Gitboard 12 | 13 | 14 | 15 | 16 | 17 | 18 | 37 | 38 | 40 | 41 |
    42 |
    43 |
    44 |

    Please wait, loading Gitboard...

    45 |
    46 |
    47 |
    48 | 49 | 50 | 63 | 64 | 65 | 66 | 67 | 68 | 78 | 79 | 80 | 81 | -------------------------------------------------------------------------------- /src/templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | Gitboard 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 43 | 44 |
    45 |
    46 |
    47 |
    48 |

    Github + Kanban = Gitboard

    49 |
    50 |

    Gitboard is a simple and intuitive Kanban board for your Github issues. Easily manage to-dos, milestones and projects and keep up-to-date on your progress. Gitboard is open-source and runs securely inside your browser.

    51 |

    52 | See an example 53 | Log in 54 |

    55 |
    56 |
    57 |
    58 | 59 |
    60 |
    61 |
    62 |
    63 |   64 |
    65 |
    66 |
    67 |
    68 | 69 |
    70 |
    71 |

    See everything at a glance

    72 |

    Gitboard gives you an intuitive and beautiful overview of your Github issues. Forget navigating back and forth to see which issues are in the backlog, being worked on or closed.

    73 |

    74 | See who's working on what. Move issues between categories by dragging and dropping them. Define time estimates for your issues and track actual working time. 75 |

    76 |
    77 |
    78 |
    79 |
    80 | 81 |
    82 |
    83 |

    Keep track of your goals

    84 |

    Gitboard helps you to keep track of your goals and the time budget of your team. See aggregate time estimates and actual working time for each milestone. This helps you to discover problems in your project before the deadline hits you hard.

    85 |

    86 | Coming soon: Burndown charts. See if your progress keeps up with your schedule. 87 |

    88 |
    89 |
    90 |
    91 |
    92 | 93 |
    94 |
    95 |

    Built with modern technologies

    96 |

    Gitboard is built with React.js, Bootstrap 3 and SASS and Material Design for Bootstrap.

    97 |

    98 | It uses a simple, resource-based data synchronization scheme and tries to keep complexity to a minimum. Feel free to use Gitboard as inspiration for your own React.js-based projects. 99 |

    100 |
    101 |
    102 |
    103 |
    104 | 105 |
    106 |
    107 |

    Open-source license

    108 |

    Gitboard is licensed under the Affero General Public License (AGPL). This means that you can adapt it to your needs and freely publish your own version as long as you respect the terms of the license and make your improvements available to the community.

    109 |
    110 |
    111 |
    112 |
    113 | 114 |
    115 |
    116 |

    Secure and transparent

    117 |

    Gitboard communicates directly with the Github API without using an intermediate server. This means that your Github access credentials stay on your computer all the time.

    118 |

    119 | Gitboard will never store your username or password in plaintext and only use them to create an access token. The access token will be held securely in the session storage or local storage of your browser. 120 |

    121 |
    122 |
    123 |
    124 |
    125 | 126 |
    127 |
    128 |

    Questions? Get in touch!

    129 |

    130 | If you have questions about Gitboard or if you experience problems using it, please open an issue. You can also get in touch with me via e-mail. 131 |

    132 |

    133 | Contributions and feedback are highly welcome! When contributing to the code, please make sure to check out the README first. 134 |

    135 |
    136 |
    137 |
    138 |
    139 | 140 | 153 | 154 | Fork me on GitHub 155 | 156 | 157 | 167 | 168 | 169 | 170 | --------------------------------------------------------------------------------