...
 
Commits (23)
......@@ -14,10 +14,21 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this p
### Added
- work in progress on [ganttlab/ganttlab-live issues](https://gitlab.com/ganttlab/ganttlab-live/issues?scope=all&state=opened&utf8=%E2%9C%93&label_name%5B%5D=Feature)...
## 0.4.0 - 2016-12-05
### Added
- Now comes with integrated GitHub support
- Information about GitHub support in Readme
- work in progress on [ganttlab/ganttlab-live issues](https://gitlab.com/ganttlab/ganttlab-live/issues?scope=all&state=opened&utf8=%E2%9C%93&label_name%5B%5D=Feature)...
### Changed
- Refined look and feel, drastically improved login screen
- Styling has been moved from components to SCSS style sheets
### Fixed
- An edge case avoiding selection of a group project on large groups
- A white screen while paginated, due to lack of scroll to top behavior
- The useless scrolling after lowering number of issues expected per page
## 0.3.0 - 2016-11-24
### Added
- Width of Gantt chart is now calculated on browser window width, making it full screen
......@@ -88,4 +99,4 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this p
- README includes a preview, and describes main topics to get started
- Initial vue-cli scaffolding with webpack plugin
[Unreleased]: https://gitlab.com/ganttlab/ganttlab-live/compare/v0.3.0...master
\ No newline at end of file
[Unreleased]: https://gitlab.com/ganttlab/ganttlab-live/compare/v0.4.0...master
\ No newline at end of file
# GanttLab
The easy to use, fully functional Gantt chart for GitLab.
The easy to use, fully functional Gantt chart for GitLab and GitHub.
![GanttLab](preview.png)
## Run it now!
It is already running live for you at https://live.ganttlab.org. Type-in your GitLab instance URL (works with https://gitlab.com), your GitLab account [_Private Token_](https://gitlab.com/profile/account) or a [_Personal Access Token_](https://gitlab.com/profile/personal_access_tokens) and enjoy!
It is already running live for you at https://live.ganttlab.org.
**Safe to run:** the application do NOT store any data, and runs on YOUR browser only, using your own network as if you were running all the requests to your GitLab instance right from your local computer. Unsure of it? Have a look at [the source code](https://gitlab.com/ganttlab/ganttlab-live/tree/master).
- **GitLab** user? Type-in your GitLab instance URL (works with https://gitlab.com), your GitLab account [_Private Token_](https://gitlab.com/profile/account) or a [_Personal Access Token_](https://gitlab.com/profile/personal_access_tokens) and enjoy!
- working on **GitHub**? Provide one of your GitHub user [Personal access tokens](https://github.com/settings/tokens) to get the ride!
**Safe to run:** the application do NOT store any data, and runs on YOUR browser only, using your own network as if you were running all the requests to your GitLab instance or to GitHub right from your local computer. Unsure of it? Have a look at [the source code](https://gitlab.com/ganttlab/ganttlab-live/tree/master).
**PRO tip:** if you are running an unsecured HTTP instance of GitLab, head to http://live.ganttlab.org to avoid your browser blocking the request coming from an HTTPS secured site.
## How it works
_GanttLab_ is a **frontend only** application. It leverages [GitLab awesome API](https://gitlab.com/help/api/README.md) to read your issues, before simply displaying a gantt chart with it.
_GanttLab_ is a **frontend only** application. It leverages [GitLab API](https://gitlab.com/help/api/README.md) or [GitHub API](https://developer.github.com/v3/) to read your issues, before simply displaying a Gantt chart with it.
The automatically generated [gantt chart](https://en.wikipedia.org/wiki/Gantt_chart) will display each of your issues within a "date area": from a **start date**, to the **due date**. For each issue, the **default start date** is read from the issue creation date. As with GitLab you are not forced to fill in a due date for your issues, the _GanttLab_ **default due date** will be set to the day after the issue creation date, faking all your issues having to be done in one day. For sure, if you insert a due date in your issues, it will be read automatically.
The automatically generated [Gantt chart](https://en.wikipedia.org/wiki/Gantt_chart) will display each of your issues within a "date area": from a **start date**, to the **due date**. For each issue, the **default start date** is read from the issue creation date. As with GitLab you are not forced to fill in a due date for your issues, and with GitHub you do not even have a due date on issues, the _GanttLab_ **default due date** will be set to the day after the issue creation date, faking all your issues having to be done in one day. For sure, if you insert a due date in your GitLab issues, it will be read automatically.
To give you maximum control over your issues and tasks management practices in the gantt chart, you can override this default values **right from your issue description** with two simple [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601#Calendar_dates) `YYYY-MM-DD` calendar dates:
......@@ -91,6 +94,7 @@ _GanttLab_ is an open source project: your contribution is very welcomed! Have a
- Moment.js http://momentjs.com/
- Evan You for being so clever on developing [Vue.js](http://vuejs.org/) and [vue-cli](https://github.com/vuejs/vue-cli)
- The [GitLab team](https://about.gitlab.com/team/) for this life changing product called [GitLab](https://about.gitlab.com/)
- [GitHub, Inc.](https://github.com/) for this incredibly useful and so widely used platform
## License
......
......@@ -19,7 +19,7 @@
<meta name="twitter:site" content="@GanttLab">
<meta name="twitter:creator" content="@GanttLab">
<meta name="twitter:title" content="GanttLab Live">
<meta name="twitter:description" content="The easy to use, fully functional Gantt chart for GitLab.">
<meta name="twitter:description" content="The easy to use, fully functional Gantt chart for GitLab and GitHub.">
<meta name="twitter:image" content="https://live.ganttlab.org/static/img/GanttLab.jpg">
<link href="https://fonts.googleapis.com/css?family=Anaheim%7CQuattrocento+Sans:400,400italic,700" rel="stylesheet" type="text/css">
</head>
......
{
"name": "ganttlab-live",
"version": "0.3.0",
"description": "The easy to use, fully functional Gantt chart for GitLab.",
"version": "0.4.0",
"description": "The easy to use, fully functional Gantt chart for GitLab and GitHub.",
"author": "clorichel <[email protected]>",
"private": false,
"scripts": {
......@@ -15,6 +15,7 @@
"lodash.debounce": "^4.0.8",
"moment": "^2.15.1",
"vue": "^2.0.1",
"vue-github-api": "^0.1.7",
"vue-gitlab-api": "^0.1.6",
"vue-multiselect": "^2.0.0-beta.10",
"vue-resource": "^1.0.3",
......
<template>
<div id="app">
<div id="App">
<transition name="fade">
<div v-if="failed == true || userEmpty" id="login">
<div v-if="loginFailed || !userName" id="LoginScreen">
<h1>GanttLab Live</h1>
<div class="row">
<div class="col welcome">
<div class="pad">
<h2>The easy to use, fully functional
<br/>Gantt chart for GitLab.</h2>
<br/>Gantt chart for GitLab and GitHub.</h2>
<p>Provide your teams with the right tool to master time and deadlines. Giving back credit to your project status and issues due dates has never been easier!</p>
<p v-if="userEmpty && downloading" class="downloading"><strong><i v-if="downloading" class="fa fa-circle-o-notch fa-spin" aria-hidden="true"></i> Connecting to {{ url }}</strong></p>
<p v-if="failed == true" class="error"><i class="fa fa-exclamation-triangle"></i> Unable to connect to {{ url }}</p>
<p v-if="!userName && downloading" class="downloading"><strong><i v-if="downloading" class="fa fa-circle-o-notch fa-spin" aria-hidden="true"></i> Connecting to {{ url }}</strong></p>
<p v-if="loginFailed" class="error"><i class="fa fa-exclamation-triangle"></i> Unable to connect to {{ url }}</p>
</div>
</div>
<div class="col form">
<p class="providerchoice"><i class="fa fa-gitlab" v-bind:class="{ selected: isGitLab }" v-on:click="providerName = 'GitLab'"></i><span> or </span><i class="fa fa-github" v-bind:class="{ selected: !isGitLab }" v-on:click="providerName = 'GitHub'"></i></p>
<p class="form-input first">
<input tabindex="1" v-model="url" v-on:keyup.enter="signin" autofocus>
<input tabindex="1" v-model="url" v-on:keyup.enter="signin" v-bind:disabled="providerName != 'GitLab'" v-bind:class="{ disabled: providerName != 'GitLab' }" autofocus>
</p>
<p class="helper">Your GitLab instance URL</p>
<p class="helper" v-bind:class="{ disabled: providerName != 'GitLab' }">Your GitLab instance URL</p>
<p class="form-input">
<input tabindex="2" v-model="token" v-on:keyup.enter="signin">
</p>
<p class="helper">Use your <a v-bind:href="privateTokenLink" target="_blank" title="/profile/account">Private Token</a>, or a <a v-bind:href="personalTokenLink" target="_blank" title="/profile/personal_access_tokens">Personal Access Token</a></p>
<p v-if="providerName == 'GitLab'" class="helper">Use your <a v-bind:href="privateTokenLink" target="_blank" title="/profile/account">Private Token</a>, or a <a v-bind:href="personalTokenLink" target="_blank" title="/profile/personal_access_tokens">Personal Access Token</a></p>
<p v-else class="helper">Use one of your <a href="https://github.com/settings/tokens" target="_blank" title="https://github.com/settings/tokens">Personal Access Tokens</a></p>
<p v-if="hasLocalStorage" class="form-input remember"><input tabindex="3" type="checkbox" v-model="rememberMe"> <span>Remember me <i class="fa fa-question-circle-o" aria-hidden="true" title="Don't do that on a public computer!"></i></span> <button tabindex="4" v-on:click="signin">Sign-in &nbsp;&gt;</button></p>
</div>
......@@ -49,51 +51,113 @@
</div>
</transition>
<transition name="fade">
<div v-if="!userEmpty" id="screen">
<div v-if="userName" id="MainScreen">
<div id="top" class="standardpadding">
<div v-if="!userEmpty">
<span class="user"><img v-bind:src="GitLab.user.avatar_url"> {{ GitLab.user.name }}</span>
<div v-if="userName">
<span class="user"><img v-bind:src="userAvatarUrl"> {{ userName }}</span>
<span class="server"><transition name="fade"><i v-if="downloading" class="fa fa-circle-o-notch fa-spin downloading" aria-hidden="true"></i></transition> <a v-bind:href="url" target="_blank">{{ url }}</a> <a href="https://gitlab.com/ganttlab/ganttlab-live#how-it-works" target="_blank"><i class="fa fa-question-circle" aria-hidden="true" title="Help"></i></a> <i class="fa fa-times close" aria-hidden="true" v-on:click="logout" title="Close"></i></span>
</div>
</div>
<mainFilter v-bind:user="GitLab.user" v-bind:downloading="downloading"></mainFilter>
<component v-bind:is="provider" class="provider"></component>
<div class="standardpadding">
<p v-if="downloading" class="downloading"><i class="fa fa-circle-o-notch fa-spin" aria-hidden="true"></i></p>
<gantt v-if="tasks != null"></gantt>
</div>
<div v-if="! downloading && (this.paginationLinks.prev || this.paginationLinks.next)" class="pagination">
<button v-if="this.paginationLinks.prev" v-on:click="paginationPage--">&lt; Prev</button>
<span>Page {{ this.paginationPage }}</span>
<button v-if="this.paginationLinks.next" v-on:click="paginationPage++">Next &gt;</button>
<div class="perpage">
Showing
<select v-model="paginationPerPage">
<option v-for="value in [10,20,50,75,100]" v-bind:value="value">{{ value }}</option>
</select>
issues per page
</div>
</div>
</div>
</transition>
</div>
</template>
<script>
import MainFilter from './components/MainFilter'
import SharedStates from './mixins/SharedStates'
import Gantt from './components/Gantt'
import 'font-awesome/css/font-awesome.css'
export default {
name: 'app',
mixins: [
SharedStates
],
components: {
Gantt
},
data () {
return {
rememberMe: false,
url: process.env.GITLAB_URL,
token: process.env.GITLAB_TOKEN,
GitLab: {
user: {} // need to be defined here, or computed property won't work as expected
},
failed: false
providerName: 'GitLab',
provider: null
}
},
components: {
MainFilter
computed: {
isGitLab: function () {
return this.providerName === 'GitLab'
},
safeUrl: function () {
if (this.url == null) {
return null
}
return this.url.replace(/\/$/, '')
},
privateTokenLink: function () {
return this.safeUrl + '/profile/account'
},
personalTokenLink: function () {
return this.safeUrl + '/profile/personal_access_tokens'
},
hasLocalStorage: function () {
return typeof Storage !== 'undefined'
}
},
methods: {
getGitLabUser: function (event) {
this.GitLabAPI.get('/user', [], [this.GitLab, 'user'], (response) => {
this.failed = true
this.GitLabAPI.get('/user', [], (response) => {
this.userName = response.body.name
this.userAvatarUrl = response.body.avatar_url
this.provider = require('./components/providers/GitLab')
}, (response) => {
this.loginFailed = true
})
},
getGitHubUser: function (event) {
this.GitHubAPI.get('/user', [], (response) => {
this.userName = response.body.login
this.userAvatarUrl = response.body.avatar_url
this.provider = require('./components/providers/GitHub')
}, (response) => {
this.loginFailed = true
})
},
signin: function (event) {
this.GitLab.user = {}
this.failed = false
this.GitLabAPI.setUrl(this.url)
this.GitLabAPI.setToken(this.token)
this.getGitLabUser()
this.userName = null
this.userAvatarUrl = null
this.loginFailed = false
if (this.providerName === 'GitLab') {
this.GitLabAPI.registerStore(this.$store)
this.GitLabAPI.setUrl(this.url)
this.GitLabAPI.setToken(this.token)
this.getGitLabUser()
} else {
this.GitHubAPI.registerStore(this.$store)
this.GitHubAPI.setToken(this.token)
this.getGitHubUser()
this.url = 'https://github.com'
}
if (this.hasLocalStorage) {
if (this.rememberMe) {
window.localStorage.url = this.url
......@@ -108,8 +172,9 @@ export default {
},
logout: function (event) {
window.history.pushState(null, null, '/')
this.GitLab.user = {}
this.failed = false
this.userName = null
this.userAvatarUrl = null
this.loginFailed = false
if (!this.rememberMe) {
this.token = ''
if (this.hasLocalStorage) {
......@@ -120,32 +185,9 @@ export default {
}
}
},
computed: {
userEmpty: function () {
return !(this.GitLab.hasOwnProperty('user') && this.GitLab.user.hasOwnProperty('name'))
},
downloading: function () {
if (typeof this.$store.state.GitLabAPI !== 'undefined') {
return this.$store.state.GitLabAPI.downloading
} else {
return false
}
},
safeUrl: function () {
return this.url.replace(/\/$/, '')
},
privateTokenLink: function () {
return this.safeUrl + '/profile/account'
},
personalTokenLink: function () {
return this.safeUrl + '/profile/personal_access_tokens'
},
hasLocalStorage: function () {
return typeof Storage !== 'undefined'
}
},
mounted: function () {
this.GitLabAPI.registerStore(this.$store)
this.url = process.env.GITLAB_URL
this.token = process.env.GITLAB_TOKEN
if (this.hasLocalStorage) {
this.url = window.localStorage.getItem('url') || process.env.GITLAB_URL
this.token = window.localStorage.getItem('token') || process.env.GITLAB_TOKEN
......
@import "ganttlab/variables";
@import "ganttlab/app";
@import "ganttlab/selector";
@import "ganttlab/providers";
@import "ganttlab/gantt";
\ No newline at end of file
......@@ -2,7 +2,7 @@ body {
margin: 0;
padding: 0;
}
#app {
#App {
font-family: $main-font;
-webkit-font-smoothing: antialiased;
color: $text-color;
......@@ -26,7 +26,7 @@ a:hover {
display: table-column;
width: 200px;
}
#login {
#LoginScreen {
width: 60%;
font-size: 20px;
position: absolute;
......@@ -35,61 +35,62 @@ a:hover {
transform: translateX(-50%) translateY(-50%);
display: table;
}
#login .row{
#LoginScreen .row{
display:table-row;
width:auto;
clear:both;
}
#login .welcome {
#LoginScreen .welcome {
margin-top: 2rem;
width: 60%;
display: inline-block;
font-size: 16px;
}
#login .welcome .pad {
#LoginScreen .welcome .pad {
padding: 0px 40px;
}
#login .form {
#LoginScreen .form {
display: inline-block;
width: 40%;
}
#login h1 {
#LoginScreen h1 {
font-size: 2.375rem;
text-align: left;
border-bottom: 1px solid rgba(21, 21, 21, 0.05);
padding: 0 0 0.5rem 1rem;
margin: 0 0 2rem;
margin: 0 0 1rem;
}
#login h2 {
#LoginScreen h2 {
font-size: 1.375rem;
margin-bottom: 30px;
}
#login .error,
#login .downloading {
#LoginScreen .error,
#LoginScreen .downloading {
text-align: center;
margin-top: 30px;
font-size: 1rem;
}
#login .error {
#LoginScreen .error {
color: #a94442;
}
#login input,
#login input[type="checkbox"] {
#LoginScreen input,
#LoginScreen input[type="checkbox"] {
font-family: $main-font;
border: 1px solid rgba(21, 21, 21, 0.1);
border-radius: 2px;
transition: all ease-in-out 0.15s;
}
#login .form-input.first {
margin-top: 1.7rem;
#LoginScreen .form-input.first {
margin-top: 5px;
}
#login .form-input input:focus,
#login input[type="checkbox"]:focus,
#login button:focus {
#LoginScreen .form-input input:focus,
#LoginScreen input[type="checkbox"]:focus,
#LoginScreen button:focus {
outline: none;
border-color: #00b3e6;
box-shadow: 0 0 5px #00b3e6;
}
#login .form-input input {
#LoginScreen .form-input input {
display: inline-block;
width: 90%;
height: 34px;
......@@ -101,21 +102,24 @@ a:hover {
background-image: none;
height: 20px;
}
#login .form-input.remember {
#LoginScreen .form-input input.disabled {
color: rgba(#555, 0.5);
}
#LoginScreen .form-input.remember {
text-align: left;
margin-top: 2rem;
}
#login .form-input.remember span {
#LoginScreen .form-input.remember span {
font-size: 1rem;
}
#login .form-input.remember input {
#LoginScreen .form-input.remember input {
height: 13px;
}
#login .form-input.remember span,
#login .form-input.remember input {
#LoginScreen .form-input.remember span,
#LoginScreen .form-input.remember input {
width: auto;
}
#login .form-input.remember button {
#LoginScreen .form-input.remember button {
background-color: rgba($lead-color, 1);
color: #fff;
font-family: $lead-font;
......@@ -130,9 +134,26 @@ a:hover {
float: right;
transition: all ease-in-out 0.15s;
}
#login .form-input.remember button:hover {
#LoginScreen .form-input.remember button:hover {
background-color: rgba($lead-color, 0.8);
}
.providerchoice {
margin: 0px;
font-size: 2em;
text-align: center;
}
.providerchoice span {
font-size: 0.4em;
margin: 0 20px;
}
.providerchoice i {
transition: color 0.2s;
cursor: pointer;
color: rgba($text-color, 0.3);
}
.providerchoice i.selected {
color: $text-color;
}
.helper {
text-align: left;
font-size: 0.62em;
......@@ -144,9 +165,12 @@ a:hover {
.helper a:hover {
text-decoration: none;
}
.helper.disabled {
color: rgba($text-color, 0.5)
}
.more {
border-top: 1px solid rgba(21, 21, 21, 0.05);
margin-top: 2rem;
margin-top: 1rem;
}
.col.copy,
.col.social {
......@@ -220,4 +244,26 @@ a:hover {
}
.fade-enter, .fade-leave-active {
opacity: 0
}
.pagination {
text-align: center;
}
.pagination span {
margin: 0 20px;
}
.perpage {
margin-top: 20px;
text-align: center;
font-size: 0.8em;
}
button {
border: 1px solid #bbb;
padding: 2px 4px 0;
margin: 0;
font: inherit;
outline:none;
line-height: 1.2;
background: #f8f8f8;
border-radius: 4px;
cursor: pointer;
}
\ No newline at end of file
#Gantt {
font-family: $main-font;
}
.rect_has_data {
/* blocks that have data */
fill: #5cb85c;
......@@ -16,17 +20,17 @@
fill: #c9302c;
}
.tooltip_has_data {
/* color of symbol in tooltip if there is data */
.taskTooltip_has_data {
/* color of symbol in taskTooltip if there is data */
color: #449d44;
}
.tooltip_has_no_data {
/* color of symbol in tooltip if there is no data */
.taskTooltip_has_no_data {
/* color of symbol in taskTooltip if there is no data */
color: #c9302c;
}
div.tooltip {
div.taskTooltip {
font-family: $main-font;
position: absolute;
text-align: left;
......
......@@ -2,22 +2,13 @@
text-align: center;
font-size: 2.5em;
}
#mainfilter {
.provider {
font-family: $lead-font;
margin: 0;
padding: 8px;
background-color:#fafafa;
border-bottom: 1px solid #e5e5e5;
top: 0;
left: 0;
height: 26px;
line-height: 26px;
text-align: center;
}
#mainfilter input[type=radio] {
.provider input[type=radio] {
display:none;
}
#mainfilter input[type=radio] + label {
.provider input[type=radio] + label {
display:inline-block;
margin:-2px;
padding: 4px 12px;
......@@ -28,7 +19,7 @@
border-bottom: 2px solid #fafafa;
color: #777;
}
#mainfilter input[type=radio]:checked + label {
.provider input[type=radio]:checked + label {
border-bottom: 2px solid $lead-color;
color: #2c3e50;
}
......@@ -39,6 +30,17 @@
font-size: 0.8em;
cursor: pointer;
}
.filter {
padding: 8px;
margin: 0;
background-color:#fafafa;
border-bottom: 1px solid #e5e5e5;
top: 0;
left: 0;
height: 26px;
line-height: 26px;
text-align: center;
}
.subfilter {
margin: 0;
padding: 8px;
......@@ -70,26 +72,4 @@
.group-separator {
font-weight: bold;
font-size: 1.5em;
}
.pagination {
text-align: center;
}
.pagination span {
margin: 0 20px;
}
.perpage {
margin-top: 20px;
text-align: center;
font-size: 0.8em;
}
button {
border: 1px solid #bbb;
padding: 2px 4px 0;
margin: 0;
font: inherit;
outline:none;
line-height: 1.2;
background: #f8f8f8;
border-radius: 4px;
cursor: pointer;
}
\ No newline at end of file
<template>
<div id="gantt">
<div id="Gantt">
<p v-if="tasks.length == 0">No tasks out there...</p>
<div v-if="tasks.length > 0" id="chart"></div>
</div>
......@@ -11,14 +11,16 @@ import moment from 'moment'
export default {
name: 'gantt',
props: [
'tasks'
],
data () {
return {
//
}
},
computed: {
tasks: function () {
return this.$store.state.tasks
}
},
watch: {
tasks: function (newTasks) {
this.refreshChart()
......@@ -73,9 +75,9 @@ export default {
// "curDisplayFirstDataset+maxDisplayDatasets"
var curDisplayFirstDataset = 0
// global div for tooltip
// global div for taskTooltip
var div = d3.select('body').append('div')
.attr('class', 'tooltip')
.attr('class', 'taskTooltip')
.style('opacity', 0)
var definedBlocks = null
......@@ -344,9 +346,9 @@ export default {
div.html(function () {
var output = ''
if (d[1] === 1) {
output = '<i class="fa fa-fw fa-check tooltip_has_data"></i>'
output = '<i class="fa fa-fw fa-check taskTooltip_has_data"></i>'
} else {
output = '<i class="fa fa-fw fa-times tooltip_has_no_data"></i>'
output = '<i class="fa fa-fw fa-times taskTooltip_has_no_data"></i>'
}
if (isDateOnlyFormat) {
if (d[2] > d3.time.second.offset(d[0], 86400)) {
......@@ -559,6 +561,12 @@ export default {
return chart
},
refreshChart: function (event) {
// removing all created taskTooltips to avoid useless scrolling
var paras = document.getElementsByClassName('taskTooltip')
while (paras[0]) {
paras[0].parentNode.removeChild(paras[0])
}
var chart = this.visavailChart().width(document.body.clientWidth - 290)
d3.select('#chart')
......
This diff is collapsed.
<template>
<div v-if="this.user">
<div id="mainfilter">
<div v-if="userName" id="GitLab-Provider">
<div class="filter">
<input type="radio" id="inputListByMe" value="me" v-model="listBy" v-on:change="clearAndListByMe"><label for="inputListByMe">Created by me</label><input type="radio" id="inputListByProject" value="project" v-model="listBy" v-on:change="listByProject"><label for="inputListByProject">By project</label><input type="radio" id="inputListByGroup" value="group" v-model="listBy" v-on:change="listByGroup"><label for="inputListByGroup">By group/project</label>
<span v-if="! downloading" v-on:click="refreshIssues" class="refresh"><i class="fa fa-refresh" aria-hidden="true"></i> Refresh</span>
</div>
<div v-if="this.listBy === 'project'" class="subfilter">
<div class="multiselect-container">
<multiselect
:options="GitLab.projects"
:value="project"
:close-on-select="true"
:show-labels="false"
:loading="downloading"
placeholder="Type to search a project"
label="path_with_namespace"
@search-change="searchProjects"
@input="listProjectIssues">
</multiselect>
</div>
</div>
<div v-if="this.listBy === 'group'" class="subfilter">
<div class="multiselect-container">
<multiselect
......@@ -36,59 +52,29 @@
</multiselect>
</div>
</div>
<div v-if="this.listBy === 'project'" class="subfilter">
<div class="multiselect-container">
<multiselect
:options="GitLab.projects"
:value="project"
:close-on-select="true"
:show-labels="false"
:loading="downloading"
placeholder="Type to search a project"
label="path_with_namespace"
@search-change="searchProjects"
@input="listProjectIssues">
</multiselect>
</div>
</div>
<div class="standardpadding">
<p v-if="downloading" class="downloading"><i class="fa fa-circle-o-notch fa-spin" aria-hidden="true"></i></p>
<gantt v-bind:tasks="ganttDataset" v-if="ganttDataset != null"></gantt>
<div v-if="! downloading && (this.paginationLinks.prev || this.paginationLinks.next)" class="pagination">
<button v-if="this.paginationLinks.prev" v-on:click="paginationPrev">&lt; Prev</button>
<span>Page {{ this.GitLab._paginationPage }}</span>
<button v-if="this.paginationLinks.next" v-on:click="paginationNext">Next &gt;</button>
<div class="perpage">
Showing
<select v-model="GitLab._paginationPerPage" v-on:change="paginationRefresh">
<option v-for="value in [10,20,50,75,100]" v-bind:value="value">{{ value }}</option>
</select>
issues per page
</div>
</div>
</div>
</div>
</template>
<script>
import SharedStates from '../../mixins/SharedStates'
import Multiselect from 'vue-multiselect'
import debounce from 'lodash.debounce'
import Gantt from './Gantt'
export default {
name: 'mainFilter',
props: [
'user',
'downloading'
name: 'GitLab',
mixins: [
SharedStates
],
components: {
Multiselect
},
data () {
return {
ganttStartString: process.env.GANTT_START_STRING,
ganttDueString: process.env.GANTT_DUE_STRING,
// exact state in the app wide Vuex store during initialization
emptyLinks: null,
GitLabPaginationLinks: [],
// main GitLab object, will be filled with data
GitLab: {
// list of groups user has access to
......@@ -100,10 +86,7 @@ export default {
// list of issues, sent to <gant> as a `props`
issues: null,
_links: [],
_paginating: null,
_paginationPerPage: 100,
_paginationPage: 1
_paginating: null
},
// main filter for listing issues:
......@@ -123,18 +106,164 @@ export default {
project: null
}
},
components: {
Multiselect,
Gantt
computed: {
tasks: function () {
return this.GitLab.issues
},
ganttDataset: function () {
if (this.GitLab.issues == null) {
return null
}
// clearing the dataset to build it from tasks list
var dataset = []
// looping on tasks
for (var i = this.tasks.length - 1; i >= 0; i--) {
var task = this.tasks[i]
// stripping task title to the first 42 characters
var title = task.title
if (title.length > 42) {
title = title.substring(0, 42) + '...'
}
// creating the dataset
var aDataset = {
'title': title,
'link': task.web_url
}
// initializing task start and due date
var startDate = null
var dueDate = null
// reading lines from this task description to search for ganttStartString and ganttDueString
if (task.description != null) {
var lines = task.description.split('\r\n')
for (var j = 0; j < lines.length; j++) {
// this description line starts with the ganttStartString
if (!lines[j].indexOf(this.ganttStartString)) {
// this task start date for gantt view is set to the appropriate date
startDate = new Date(lines[j].replace(this.ganttStartString, ''))
}
// this description line starts with the ganttDueString
if (!lines[j].indexOf(this.ganttDueString)) {
// this task due date for gantt view is set to the appropriate date
dueDate = new Date(lines[j].replace(this.ganttDueString, ''))
}
}
}
// if start date is still null, we set it from task creation date
if (startDate == null) {
startDate = new Date(task.created_at)
}
// if due date is still null we set it to the task due date, or to the day after the task creation date
if (dueDate == null) {
dueDate = task.due_date
if (dueDate == null) {
// the task due date is unset
dueDate = new Date(task.created_at)
// the due date is calculated to the day after the task creation date
dueDate.setDate(dueDate.getDate() + 1)
} else {
// the task due date is used
dueDate = new Date(task.due_date)
}
}
// determining if the task is late or not
var today = new Date()
var status = 1
if (dueDate < today) {
status = 0
}
// formatting start and due dates for visavail
var fDueDate = dueDate.getUTCFullYear() + '-' + this.pad(dueDate.getUTCMonth() + 1) + '-' + this.pad(dueDate.getUTCDate())
var fStartDate = startDate.getUTCFullYear() + '-' + this.pad(startDate.getUTCMonth() + 1) + '-' + this.pad(startDate.getUTCDate())
aDataset.data = [
[ fStartDate, status, fDueDate ]
]
// adding the dataset built to the main dataset list
dataset.push(aDataset)
}
if (dataset === []) {
return null
} else {
return dataset
}
}
},
watch: {
ganttDataset: function (value) {
// watch for calculated Gantt Dataset, and commit it in app wide Vuex store
this.$store.commit('tasks', value)
},
paginationPage: function (value) {
// watch for app wide Vuex store pagination.page, and refresh GitLab issues appropriately
this.GitLabPaginationLinks = []
// reset paginationLinks
this.paginationLinks = JSON.parse(JSON.stringify(this.emptyLinks))
this[this.GitLab._paginating]()
window.scrollTo(0, 0)
},
paginationPerPage: function (value) {
// watch for app wide Vuex store pagination.perPage
if (this.paginationPage === 1) {
// if already on page 1, watch.page won't be called: refresh GitLab issues appropriately
this.GitLabPaginationLinks = []
// reset paginationLinks
this.paginationLinks = JSON.parse(JSON.stringify(this.emptyLinks))
this[this.GitLab._paginating]()
window.scrollTo(0, 0)
} else {
// if not on page one, just set page 1, and watch.page will refresh GitLab issues
this.paginationPage = 1
}
},
GitLabPaginationLinks: function (value) {
var links = JSON.parse(JSON.stringify(this.emptyLinks))
if (value.length === 0) {
// update app wide Vuex store
this.paginationLinks = links
return
}
// Split parts by comma
var parts = value.split(',')
// Parse each part into a named link
for (var i = 0; i < parts.length; i++) {
var section = parts[i].split(';')
if (section.length !== 2) {
console.error('[GanttLab] GitLab API pagination link header seems to be inaccurate')
// update app wide Vuex store
this.paginationLinks = links
return
}
var url = section[0].replace(/<(.*)>/, '$1').trim()
var name = section[1].replace(/rel="(.*)"/, '$1').trim()
links[name] = url
}
// update app wide Vuex store
this.paginationLinks = links
}
},
methods: {
clearSelection: function (event) {
// clearing the issues
this.GitLab.issues = null
// clearing the links header
this.GitLab._links = []
this.GitLabPaginationLinks = []
// back to the first pagination page
this.GitLab._paginationPage = 1
this.paginationPage = 1
// we are not paginating anything
this.GitLab._paginating = null
},
......@@ -177,11 +306,11 @@ export default {
// user wants issues for all projects created by himself
this.GitLabAPI.get('/issues', {
'page': this.GitLab._paginationPage,
'per_page': this.GitLab._paginationPerPage,
'page': this.paginationPage,
'per_page': this.paginationPerPage,
'state': 'opened'
}, (response) => {
this.GitLab._links = response.headers.get('Link')
this.GitLabPaginationLinks = response.headers.get('Link')
this.GitLab._paginating = 'listByMe'
this.GitLab.issues = response.body
})
......@@ -243,7 +372,7 @@ export default {
this.GitLabAPI.get('/groups', {
'per_page': '10',
'all_available': 1,
'search': search || this.user.username
'search': search || this.userName
}, (response) => {
this.$set(this.GitLab, 'groups', response.body)
if (typeof cb === 'function') {
......@@ -254,7 +383,7 @@ export default {
refreshGroupProjects: function (cb, search) {
// user wants the list of projects in this.group
this.GitLabAPI.get('/groups/' + this.group.id + '/projects', {
'per_page': '10',
'per_page': '100',
'search': search
}, (response) => {
this.$set(this.GitLab, 'groupProjects', response.body)
......@@ -284,11 +413,11 @@ export default {
// user wants issues for all projects in the selected group
this.GitLabAPI.get('/groups/' + this.group.id + '/issues', {
'page': this.GitLab._paginationPage,
'per_page': this.GitLab._paginationPerPage,
'page': this.paginationPage,
'per_page': this.paginationPerPage,
'state': 'opened'
}, (response) => {
this.GitLab._links = response.headers.get('Link')
this.GitLabPaginationLinks = response.headers.get('Link')
this.GitLab._paginating = 'listGroupIssues'
this.GitLab.issues = response.body
})
......@@ -302,11 +431,11 @@ export default {
// user wants issues for a selected project
this.GitLabAPI.get('/projects/' + this.gProject.id + '/issues', {
'page': this.GitLab._paginationPage,
'per_page': this.GitLab._paginationPerPage,
'page': this.paginationPage,
'per_page': this.paginationPerPage,
'state': 'opened'
}, (response) => {
this.GitLab._links = response.headers.get('Link')
this.GitLabPaginationLinks = response.headers.get('Link')
this.GitLab._paginating = 'listGroupProjectIssues'
this.GitLab.issues = response.body
})
......@@ -324,34 +453,15 @@ export default {
// user wants issues for a selected project
this.GitLabAPI.get('/projects/' + this.project.id + '/issues', {
'page': this.GitLab._paginationPage,
'per_page': this.GitLab._paginationPerPage,
'page': this.paginationPage,
'per_page': this.paginationPerPage,
'state': 'opened'
}, (response) => {
this.GitLab._links = response.headers.get('Link')
this.GitLabPaginationLinks = response.headers.get('Link')
this.GitLab._paginating = 'listProjectIssues'
this.GitLab.issues = response.body
})
},
paginationNext: function (event) {
if (typeof this.paginationLinks.next !== 'undefined') {
this.GitLab._links = []
this.GitLab._paginationPage++
this[this.GitLab._paginating]()
}
},
paginationPrev: function (event) {
if (typeof this.paginationLinks.prev !== 'undefined') {
this.GitLab._links = []
this.GitLab._paginationPage--
this[this.GitLab._paginating]()
}
},
paginationRefresh: function (event) {
this.GitLab._paginationPage = 1
this.GitLab._links = []
this[this.GitLab._paginating]()
},
refreshIssues: function (event) {
if (this.listBy === 'me') {
this.clearAndListByMe()
......@@ -399,122 +509,9 @@ export default {
return r
}
},
computed: {
paginationLinks: function () {
if (this.GitLab._links.length === 0) {
return []
}
// Split parts by comma
var parts = this.GitLab._links.split(',')
var links = {}
// Parse each part into a named link
for (var i = 0; i < parts.length; i++) {
var section = parts[i].split(';')
if (section.length !== 2) {
console.error('[GanttLab] GitLab API pagination link header seems to be inaccurate')
return []
}
var url = section[0].replace(/<(.*)>/, '$1').trim()
var name = section[1].replace(/rel="(.*)"/, '$1').trim()
links[name] = url
}
return links
},
tasks: function () {
return this.GitLab.issues
},
ganttDataset: function () {
if (this.GitLab.issues == null) {
return null
}
// clearing the dataset to build it from tasks list
var dataset = []
// looping on tasks
for (var i = this.tasks.length - 1; i >= 0; i--) {
var task = this.tasks[i]
// stripping task title to the first 42 characters
var title = task.title
if (title.length > 42) {
title = title.substring(0, 42) + '...'
}
// creating the dataset
var aDataset = {
'title': title,
'link': task.web_url
}
// initializing task start and due date
var startDate = null
var dueDate = null
// reading lines from this task description to search for ganttStartString and ganttDueString
if (task.description != null) {
var lines = task.description.split('\r\n')
for (var j = 0; j < lines.length; j++) {
// this description line starts with the ganttStartString
if (!lines[j].indexOf(this.ganttStartString)) {
// this task start date for gantt view is set to the appropriate date
startDate = new Date(lines[j].replace(this.ganttStartString, ''))
}
// this description line starts with the ganttDueString
if (!lines[j].indexOf(this.ganttDueString)) {
// this task due date for gantt view is set to the appropriate date
dueDate = new Date(lines[j].replace(this.ganttDueString, ''))
}
}
}
// if start date is still null, we set it from task creation date
if (startDate == null) {
startDate = new Date(task.created_at)
}
// if due date is still null we set it to the task due date, or to the day after the task creation date
if (dueDate == null) {
dueDate = task.due_date
if (dueDate == null) {
// the task due date is unset
dueDate = new Date(task.created_at)
// the due date is calculated to the day after the task creation date
dueDate.setDate(dueDate.getDate() + 1)
} else {
// the task due date is used
dueDate = new Date(task.due_date)
}
}
// determining if the task is late or not
var today = new Date()
var status = 1
if (dueDate < today) {
status = 0
}
// formatting start and due dates for visavail
var fDueDate = dueDate.getUTCFullYear() + '-' + this.pad(dueDate.getUTCMonth() + 1) + '-' + this.pad(dueDate.getUTCDate())
var fStartDate = startDate.getUTCFullYear() + '-' + this.pad(startDate.getUTCMonth() + 1) + '-' + this.pad(startDate.getUTCDate())
aDataset.data = [
[ fStartDate, status, fDueDate ]
]
// adding the dataset built to the main dataset list
dataset.push(aDataset)
}
if (dataset === []) {
return null
} else {
return dataset
}
}
},
mounted: function () {
// populating the exact initial state
this.emptyLinks = this.$store.state.pagination.links
// reading user expected listBy method
var expectedListBy = this.getParameterByName('l')
// reading user expected project (if any)
......
......@@ -6,12 +6,53 @@ Vue.use(Vuex)
import VueResource from 'vue-resource'
Vue.use(VueResource)
import GitHubAPI from 'vue-github-api'
Vue.use(GitHubAPI)
import GitLabAPI from 'vue-gitlab-api'
Vue.use(GitLabAPI)
import App from './App'
const store = new Vuex.Store({})
const store = new Vuex.Store({
state: {
url: null,
token: null,
loginFailed: false,
user: {
name: null,
avatarUrl: null
},
tasks: null,
pagination: {
page: 1,
perPage: 100,
links: {
prev: null,
next: null
}
}
},
mutations: {
url: function (state, value) {
state.url = value
},
token: function (state, value) {
state.token = value
},
loginFailed: function (state, value) {
state.loginFailed = value
},
user: function (state, value) {
state.user = value
},
tasks: function (state, value) {
state.tasks = value
},
pagination: function (state, value) {
state.pagination = value
}
}
})
/* eslint-disable no-new */
new Vue({
......
module.exports = {
computed: {
downloading: function () {
if (typeof this.$store.state.GitLabAPI !== 'undefined') {
return this.$store.state.GitLabAPI.downloading
} else if (typeof this.$store.state.GitHubAPI !== 'undefined') {
return this.$store.state.GitHubAPI.downloading
} else {
return false
}
},
url: {
get () {
return this.$store.state.url
},
set (value) {
this.$store.commit('url', value)
}
},
token: {
get () {
return this.$store.state.token
},
set (value) {
this.$store.commit('token', value)
}
},
loginFailed: {
get () {
return this.$store.state.loginFailed
},
set (value) {
this.$store.commit('loginFailed', value)
}
},
userName: {
get () {
return this.$store.state.user.name
},
set (value) {
var user = this.$store.state.user
user.name = value
this.$store.commit('user', user)
}
},
userAvatarUrl: {
get () {
return this.$store.state.user.avatarUrl
},
set (value) {
var user = this.$store.state.user
user.avatarUrl = value
this.$store.commit('user', user)
}
},
tasks: function () {
return this.$store.state.tasks
},
paginationPage: {
get () {
return this.$store.state.pagination.page
},
set (value) {
var pagination = this.$store.state.pagination
pagination.page = value
this.$store.commit('pagination', pagination)
}
},
paginationPerPage: {
get () {
return this.$store.state.pagination.perPage
},
set (value) {
var pagination = this.$store.state.pagination
pagination.perPage = value
this.$store.commit('pagination', pagination)
}
},
paginationLinks: {
get () {
return this.$store.state.pagination.links
},
set (value) {
var pagination = this.$store.state.pagination
pagination.links = value
this.$store.commit('pagination', pagination)
}
}
}
}