New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Export CSV #25

Merged
merged 7 commits into from Jun 4, 2016
File filter...
Filter file types
Jump to file or symbol
Failed to load files and symbols.
+236 −14
Diff settings

Always

Just for now

Copy path View file
@@ -34,11 +34,11 @@ Once you send a Pull Request, your code will be check with [Travis CI](https://t

Please follow our coding standards as best as you can to keep consistency over code.

The [.editorconfig](https://github.com/nicoespeon/trello-kanban-analysis-tool/blob/develop/.editorconfig) file should help you configure your IDE to do so.
The [.editorconfig](https://github.com/nicoespeon/trello-kanban-analysis-tool/blob/master/.editorconfig) file should help you configure your IDE to do so.

### JS (ES2015)

This project uses [ESLint](http://eslint.org/) to ensure consistency, following [these rules(https://github.com/nicoespeon/trello-kanban-analysis-tool/blob/develop/.eslintrc.js)
This project uses [ESLint](http://eslint.org/) to ensure consistency, following [these rules(https://github.com/nicoespeon/trello-kanban-analysis-tool/blob/master/.eslintrc.js)

You can run the linter with `npm run lint`.

Copy path View file
@@ -57,7 +57,7 @@ Launch unit tests through [tap-diff reporter](https://www.npmjs.com/package/tap-

That would be amazing :metal:

Please have a look at [the CONTRIBUTING.md file](https://github.com/nicoespeon/trello-kanban-analysis-tool/blob/develop/CONTRIBUTING.md) before you do so.
Please have a look at [the CONTRIBUTING.md file](https://github.com/nicoespeon/trello-kanban-analysis-tool/blob/master/CONTRIBUTING.md) before you do so.

## Versioning

@@ -89,6 +89,6 @@ That mean releases will be numbered with `<major>.<minor>.<patch>` format, regar

## Copyright and License

Copyright (c) 2016 [Nicolas CARLO](https://twitter.com/nicoespeon) under [the MIT license](https://github.com/nicoespeon/trello-kanban-analysis-tool/blob/develop/LICENSE.md).
Copyright (c) 2016 [Nicolas CARLO](https://twitter.com/nicoespeon) under [the MIT license](https://github.com/nicoespeon/trello-kanban-analysis-tool/blob/master/LICENSE.md).

:confused: [What does that mean?](http://choosealicense.com/licenses/mit/)
Copy path View file
@@ -1,12 +1,13 @@
import Cycle from '@cycle/core';
import { makeDOMDriver, div, h1, small } from '@cycle/dom';
import { makeDOMDriver, div, h1, small, button } from '@cycle/dom';
import isolate from '@cycle/isolate';
import storageDriver from '@cycle/storage';
import { Observable } from 'rx';
import R from 'ramda';

import { trelloSinkDriver } from './drivers/Trello';
import { makeGraphDriver } from './drivers/Graph';
import { exportToCSVDriver } from './drivers/ExportToCSV';

import LabeledSelect from './components/LabeledSelect/LabeledSelect';
import LabeledCheckbox from './components/LabeledCheckbox/LabeledCheckbox';
@@ -22,6 +23,7 @@ import {
today,
tomorrow,
} from './utils/date';
import { argsToArray } from './utils/utility';
import { getDisplayedLists } from './utils/trello';

function main({ DOMAboveChart, DOMBelowChart, TrelloFetch, TrelloMissingInfo, Storage }) {
@@ -199,6 +201,10 @@ function main({ DOMAboveChart, DOMBelowChart, TrelloFetch, TrelloMissingInfo, St
.scan(R.concat),
});

// Download button clicks

const downloadClicks$ = DOMBelowChart.select('.download-btn').events('click');

// Connect
publishedTrelloLists$.connect();
publishedTrelloActions$.connect();
@@ -253,15 +259,27 @@ function main({ DOMAboveChart, DOMBelowChart, TrelloFetch, TrelloMissingInfo, St
),
DOMBelowChart: trelloKanbanMetrics.DOM.map(
(trelloKanbanMetricsVTree) =>
div('.container.m-top', [trelloKanbanMetricsVTree])
div('.container', [
div('.center-align', [
button(
'.download-btn.btn.waves-effect.waves-light.trello-blue',
'Download .csv'
),
]),
div('.m-top.m-bottom', [trelloKanbanMetricsVTree]),
])
),
TrelloFetch: Observable.combineLatest(
boardSelect.selected$,
trelloCFD.Trello,
R.compose(R.head, R.unapply(R.identity))
R.compose(R.head, argsToArray)
),
TrelloMissingInfo: trelloKanbanMetrics.Trello,
Graph: trelloCFD.Graph,
ExportToCSV: downloadClicks$.withLatestFrom(
trelloCFD.CSV,
R.compose(R.last, argsToArray)
),
Storage: Observable.merge(
boardSelect.selected$
.map(selected => ({ key: 'selectedBoard', value: selected })),
@@ -284,6 +302,7 @@ const drivers = {
TrelloMissingInfo: trelloSinkDriver,
Graph: makeGraphDriver('#chart svg'),
Storage: storageDriver,
ExportToCSV: exportToCSVDriver,
};

Trello.authorize({
@@ -4,6 +4,7 @@ import R from 'ramda';

import { parseActions } from './actions';
import { parseToGraph } from './graph';
import { graphToCSV } from './csv';

import { today, tomorrow, filterBetweenDates, nextDay } from '../../utils/date';

@@ -44,14 +45,17 @@ function TrelloCFD(
)(actions)
);

const graph$ = Observable.combineLatest(
displayedLists$,
parsedActions$,
parseToGraph
);

return {
DOM: vtree$,
Trello: clicks$,
Graph: Observable.combineLatest(
displayedLists$,
parsedActions$,
parseToGraph
),
Graph: graph$,
CSV: graph$.map(graphToCSV),
};
}

Copy path View file
@@ -0,0 +1,36 @@
import R from 'ramda';
import moment from 'moment';

// getHead :: [Graph] -> [[String]]
const getHead = (data) => [
R.compose(
R.concat(['']),
R.flatten,
R.map(x => moment(x).format('YYYY-MM-DD')),
R.map(R.head),
R.propOr([], 'values'),
R.head
)(data),
];

// getBody :: [Graph] -> [[String|Number]]
const getBody = R.compose(
R.map(
R.converge(
(key, values) => R.concat([key], values),
[
R.prop('key'),
R.compose(
R.flatten,
R.map(R.last),
R.prop('values')
),
]
)
)
);

// graphToCSV :: [Graph] -> [[String|Number]]
const graphToCSV = R.converge(R.concat, [getHead, getBody]);

export { getHead, getBody, graphToCSV };
@@ -0,0 +1,115 @@
import { test } from 'tape';

import { getHead, getBody, graphToCSV } from '../csv';

test('getHead', (assert) => {
const data = [
{
key: 'Card Preparation',
values: [
[new Date('2016-01-20').getTime(), 1],
[new Date('2016-02-04').getTime(), 1],
[new Date('2016-02-08').getTime(), 1],
[new Date('2016-03-02').getTime(), 2],
],
},
{
key: 'Backlog',
values: [
[new Date('2016-01-20').getTime(), 0],
[new Date('2016-02-04').getTime(), 1],
[new Date('2016-02-08').getTime(), 2],
[new Date('2016-03-02').getTime(), 4],
],
},
];
const expected = [
['', '2016-01-20', '2016-02-04', '2016-02-08', '2016-03-02'],
];

assert.deepEquals(
getHead(data),
expected,
'should return a properly formatted CSV head from graph data'
);
assert.deepEquals(
getHead([]),
[['']],
'should return an array of a single empty string if no data are present'
);
assert.end();
});

test('getBody', (assert) => {
const data = [
{
key: 'Card Preparation',
values: [
[new Date('2016-01-20').getTime(), 1],
[new Date('2016-02-04').getTime(), 1],
[new Date('2016-02-08').getTime(), 1],
[new Date('2016-03-02').getTime(), 2],
],
},
{
key: 'Backlog',
values: [
[new Date('2016-01-20').getTime(), 0],
[new Date('2016-02-04').getTime(), 1],
[new Date('2016-02-08').getTime(), 2],
[new Date('2016-03-02').getTime(), 4],
],
},
];
const expected = [
['Card Preparation', 1, 1, 1, 2],
['Backlog', 0, 1, 2, 4],
];

assert.deepEquals(
getBody(data),
expected,
'should return a properly formatted CSV body from graph data'
);
assert.end();
});

test('graphToCSV', (assert) => {
const data = [
{
key: 'Card Preparation',
values: [
[new Date('2016-01-20').getTime(), 1],
[new Date('2016-02-04').getTime(), 1],
[new Date('2016-02-08').getTime(), 1],
[new Date('2016-03-02').getTime(), 2],
],
},
{
key: 'Backlog',
values: [
[new Date('2016-01-20').getTime(), 0],
[new Date('2016-02-04').getTime(), 1],
[new Date('2016-02-08').getTime(), 2],
[new Date('2016-03-02').getTime(), 4],
],
},
];
const expected = [
['', '2016-01-20', '2016-02-04', '2016-02-08', '2016-03-02'],
['Card Preparation', 1, 1, 1, 2],
['Backlog', 0, 1, 2, 4],
];

assert.deepEquals(
graphToCSV(data),
expected,
'should parse graph data into proper CSV format from graph data'
);
assert.deepEquals(
graphToCSV([]),
[['']],
'should return an array of a single empty string if no data are present'
);
assert.end();
});
Copy path View file
@@ -0,0 +1,28 @@
import R from 'ramda';

function exportToCSVDriver(input$) {
// `data` should be formatted as an Array of Arrays (= lines).
// e.g: `[["name1", "city1", "other info"], ["name2", "city2", "more info"]]`
input$.subscribe((data) => {
// How to create a CSV file?
// See http://stackoverflow.com/questions/14964035/how-to-export-javascript-array-info-to-csv-on-client-side
let csvContent = 'data:text/csv;charset=utf-8,';
data.forEach((infoArray, index) => {
const dataString = infoArray.join(',');
csvContent += index < data.length ? `${dataString}\n` : dataString;
});

const link = document.createElement('a');
link.setAttribute('href', encodeURI(csvContent));
const head = R.head(data);
link.setAttribute('download', `cfd-from-${head[1]}-to-${R.last(head)}.csv`);

// Required for FF
document.body.appendChild(link);

// Download the data file.
link.click();
});
}

export { exportToCSVDriver };
Copy path View file
@@ -1,6 +1,8 @@
import R from 'ramda';
import { Observable } from 'rx';

import { argsToArray } from '../utils/utility';

const actionsFilter = [
'createCard',
'deleteCard',
@@ -89,7 +91,7 @@ function trelloSinkDriver(input$) {
null,
cardIds
.map(cardActions$)
.concat(R.unapply(R.identity))
.concat(argsToArray)
)
);
});
Copy path View file
@@ -34,6 +34,10 @@ label {
margin-top : 2rem;
}

.m-bottom {
margin-bottom: 2rem;
}

// Debug
.d {
border : 1px red solid;
Copy path View file
@@ -2,11 +2,21 @@ import { test } from 'tape';
import R from 'ramda';

import {
argsToArray,
countByWith,
groupByWith,
round,
} from '../utility';

test('argsToArray', (assert) => {
assert.deepEquals(
argsToArray(1, 'lorem', 3),
[1, 'lorem', 3],
'should convert arguments into an array'
);
assert.end();
});

test('countByWith', (assert) => {
const data = [1.0, 1.1, 1.2, 2.0, 3.0, 2.2];
const fn = (a, b) => ({ number: a, count: b });
Copy path View file
@@ -1,5 +1,8 @@
import R from 'ramda';

// argsToArray :: (a, …) -> [a, …]
const argsToArray = R.unapply(R.identity);

// countByWith :: (a -> String) -> (String,Number -> {B: b, C: c}) -> [a] -> [{B: b, C: c}]
const countByWith = R.curry((prop, fn, data) => R.compose(
R.map(R.apply(fn)),
@@ -24,6 +27,7 @@ const groupByWith = R.curry((prop, fn, data) => R.cond([
const round = (x) => Math.round(x * 100) / 100;

export {
argsToArray,
countByWith,
groupByWith,
round,
Copy path View file
@@ -1,6 +1,6 @@
{
"name": "tkat",
"version": "0.3.0",
"version": "0.4.0",
"description": "Analyse Kanban metrics from a Trello board",
"keywords": [
"trello",
ProTip! Use n and p to navigate between commits in a pull request.