Skip to content
GitLab
Menu
Projects
Groups
Snippets
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in / Register
Toggle navigation
Menu
Open sidebar
Jan
kresus
Commits
ebbcc440
Commit
ebbcc440
authored
Apr 13, 2016
by
Benjamin Bouvier
Browse files
Project Minefield: use Redux in the client;
parent
8bbe4c1c
Changes
83
Hide whitespace changes
Inline
Side-by-side
.eslintrc.yml
View file @
ebbcc440
...
...
@@ -180,7 +180,7 @@
-
2
# disallow use of labeled statements
no-labels
:
-
2
-
0
# disallow unnecessary nested blocks
no-lone-blocks
:
-
2
...
...
@@ -425,7 +425,7 @@
# specify the maximum depth callbacks can be nested
max-nested-callbacks
:
-
2
-
1
-
2
# limits the number of parameters that can be used in the function declaration.
max-params
:
-
0
...
...
client/.eslintrc.yml
View file @
ebbcc440
...
...
@@ -25,6 +25,13 @@
jsx-quotes
:
-
2
-
'
prefer-double'
# Deactives arrow body style; it's stupid when returning a single
# object.
arrow-body-style
:
-
0
# Can't enforce when enforcing a functions ordering.
no-use-before-define
:
-
0
# Prevent missing displayName in a React component definition
# TODO: set to 2 when one module by file
'
react/display-name'
:
...
...
@@ -158,9 +165,10 @@
# Ensure named imports correspond to a named export in the remote file.
'
import/named'
:
-
2
# Ensure a default export is present, given a default import.
# Ensure a default export is present, given a default import:
# doesn't work when importing everything: `import * from module`
'
import/default'
:
-
2
-
0
# Ensure imported namespaces contain dereferenced properties as they are dereferenced.
'
import/namespace'
:
-
2
...
...
client/TODO
0 → 100644
View file @
ebbcc440
LINT!
client/components/categories/index.js
View file @
ebbcc440
import
React
from
'
react
'
;
import
{
connect
}
from
'
react-redux
'
;
import
{
Actions
,
store
,
State
}
from
'
../../store
'
;
import
{
translate
as
$t
,
NONE_CATEGORY_ID
}
from
'
../../helpers
'
;
import
{
get
,
actions
}
from
'
../../store
'
;
import
{
translate
as
$t
}
from
'
../../helpers
'
;
import
CategoryListItem
from
'
./item
'
;
import
CreateForm
from
'
./create-form
'
;
export
default
class
CategoryList
extends
React
.
Component
{
class
CategoryList
extends
React
.
Component
{
constructor
(
props
)
{
super
(
props
);
this
.
state
=
{
showForm
:
false
,
categories
:
store
.
getCategories
()
};
this
.
listener
=
this
.
listener
.
bind
(
this
);
this
.
handleSave
=
this
.
handleSave
.
bind
(
this
);
this
.
handleShowForm
=
this
.
handleShowForm
.
bind
(
this
);
}
listener
()
{
this
.
setState
({
categories
:
store
.
getCategories
()
});
}
componentDidMount
()
{
store
.
on
(
State
.
categories
,
this
.
listener
);
}
componentWillUnmount
()
{
store
.
removeListener
(
State
.
categories
,
this
.
listener
);
}
handleShowForm
(
e
)
{
e
.
preventDefault
();
this
.
setState
({
...
...
@@ -53,7 +40,7 @@ export default class CategoryList extends React.Component {
color
};
Action
s
.
createCategory
(
category
);
this
.
prop
s
.
createCategory
(
category
);
this
.
refs
.
createform
.
clearLabel
();
this
.
setState
({
...
...
@@ -63,9 +50,15 @@ export default class CategoryList extends React.Component {
}
render
()
{
let
items
=
this
.
state
.
categories
.
filter
(
cat
=>
cat
.
id
!==
NONE_CATEGORY_ID
)
.
map
(
cat
=>
<
CategoryListItem
cat
=
{
cat
}
key
=
{
cat
.
id
}
/>
)
;
let
items
=
this
.
props
.
categories
.
map
(
cat
=>
<
CategoryListItem
cat
=
{
cat
}
categories
=
{
this
.
props
.
categories
}
updateCategory
=
{
this
.
props
.
updateCategory
}
deleteCategory
=
{
this
.
props
.
deleteCategory
}
key
=
{
cat
.
id
}
/>
)
;
let
maybeForm
=
(
this
.
state
.
showForm
?
...
...
@@ -118,3 +111,23 @@ export default class CategoryList extends React.Component {
);
}
}
const
Export
=
connect
(
state
=>
{
return
{
categories
:
get
.
categoriesButNone
(
state
)
};
},
dispatch
=>
{
return
{
createCategory
(
category
)
{
actions
.
createCategory
(
dispatch
,
category
);
},
updateCategory
(
former
,
newer
)
{
actions
.
updateCategory
(
dispatch
,
former
,
newer
);
},
deleteCategory
(
former
,
replaceById
)
{
actions
.
deleteCategory
(
dispatch
,
former
,
replaceById
);
}
};
})(
CategoryList
);
export
default
Export
;
client/components/categories/item.js
View file @
ebbcc440
import
React
from
'
react
'
;
import
{
Actions
,
store
}
from
'
../../store
'
;
import
{
translate
as
$t
,
NONE_CATEGORY_ID
}
from
'
../../helpers
'
;
import
CreateForm
from
'
./create-form
'
;
...
...
@@ -26,7 +25,7 @@ export default class CategoryListItem extends React.Component {
color
};
Action
s
.
updateCategory
(
this
.
props
.
cat
,
category
);
this
.
prop
s
.
updateCategory
(
this
.
props
.
cat
,
category
);
this
.
setState
({
editMode
:
false
...
...
@@ -53,7 +52,7 @@ export default class CategoryListItem extends React.Component {
handleDelete
()
{
let
replaceCategory
=
this
.
refs
.
replacement
.
value
;
Action
s
.
deleteCategory
(
this
.
props
.
cat
,
replaceCategory
);
this
.
prop
s
.
deleteCategory
(
this
.
props
.
cat
,
replaceCategory
);
}
render
()
{
...
...
@@ -70,8 +69,8 @@ export default class CategoryListItem extends React.Component {
/>
)
;
}
let
replacementOptions
=
store
.
getC
ategories
()
.
filter
(
cat
=>
(
cat
.
id
!==
c
.
id
&&
cat
.
id
!==
NONE_CATEGORY_ID
)
)
let
replacementOptions
=
this
.
props
.
c
ategories
.
filter
(
cat
=>
cat
.
id
!==
c
.
id
)
.
map
(
cat
=>
<
option
key
=
{
cat
.
id
}
...
...
client/components/charts/balance-chart.js
0 → 100644
View file @
ebbcc440
/* globals Dygraph: false */
import
React
from
'
react
'
;
import
{
debug
}
from
'
../../helpers
'
;
import
ChartComponent
from
'
./chart-base
'
;
import
{
round2
}
from
'
./helpers
'
;
function
createChartBalance
(
chartId
,
account
,
operations
)
{
if
(
account
===
null
)
{
debug
(
'
ChartComponent: no account
'
);
return
;
}
let
ops
=
operations
.
slice
().
sort
((
a
,
b
)
=>
+
a
.
date
-
+
b
.
date
);
function
makeKey
(
date
)
{
return
`
${
date
.
getFullYear
()}
-
${
date
.
getMonth
()
+
1
}
-
${
date
.
getDate
()}
`
;
}
let
opmap
=
new
Map
;
// Fill all dates.
const
DAY
=
1000
*
60
*
60
*
24
;
let
firstDate
=
ops
.
length
?
+
ops
[
0
].
date
:
Date
.
now
();
firstDate
=
(
firstDate
/
DAY
|
0
)
*
DAY
;
let
today
=
(
Date
.
now
()
/
DAY
|
0
)
*
DAY
;
for
(;
firstDate
<=
today
;
firstDate
+=
DAY
)
{
opmap
.
set
(
makeKey
(
new
Date
(
firstDate
)),
0
);
}
// Date (day) -> cumulated sum of amounts for this day (scalar).
for
(
let
o
of
ops
)
{
let
key
=
makeKey
(
o
.
date
);
if
(
opmap
.
has
(
key
))
opmap
.
set
(
key
,
opmap
.
get
(
key
)
+
o
.
amount
);
}
let
balance
=
account
.
initialAmount
;
let
csv
=
'
Date,Balance
\n
'
;
for
(
let
[
date
,
amount
]
of
opmap
)
{
balance
+=
amount
;
csv
+=
`
${
date
}
,
${
round2
(
balance
)}
\n`
;
}
/* eslint-disable no-new */
// Create the chart
new
Dygraph
(
document
.
querySelector
(
chartId
),
csv
);
/* eslint-enable no-new */
}
export
default
class
BalanceChart
extends
ChartComponent
{
redraw
()
{
createChartBalance
(
'
#barchart
'
,
this
.
props
.
account
,
this
.
props
.
operations
);
}
render
()
{
return
<
div
id
=
"
barchart
"
style
=
{
{
width
:
'
100%
'
}
}
><
/div>
;
}
}
client/components/charts/chart-base.js
View file @
ebbcc440
...
...
@@ -13,5 +13,4 @@ export default class ChartComponent extends React.Component {
componentDidMount
()
{
this
.
redraw
();
}
}
client/components/charts/helpers.js
0 → 100644
View file @
ebbcc440
export
function
round2
(
x
)
{
return
Math
.
round
(
x
*
100
)
/
100
;
}
client/components/charts/in-out-chart.js
0 → 100644
View file @
ebbcc440
/* globals c3: false */
import
React
from
'
react
'
;
import
{
translate
as
$t
}
from
'
../../helpers
'
;
import
ChartComponent
from
'
./chart-base
'
;
import
{
round2
}
from
'
./helpers
'
;
function
createChartPositiveNegative
(
chartId
,
operations
)
{
function
datekey
(
op
)
{
let
d
=
op
.
date
;
return
`
${
d
.
getFullYear
()}
-
${
d
.
getMonth
()}
`
;
}
const
POS
=
0
,
NEG
=
1
,
BAL
=
2
;
// Month -> [Positive amount, Negative amount, Diff]
let
map
=
new
Map
;
// Datekey -> Date
let
dateset
=
new
Map
;
for
(
let
i
=
0
;
i
<
operations
.
length
;
i
++
)
{
let
op
=
operations
[
i
];
let
dk
=
datekey
(
op
);
map
.
set
(
dk
,
map
.
get
(
dk
)
||
[
0
,
0
,
0
]);
let
triplet
=
map
.
get
(
dk
);
triplet
[
POS
]
+=
op
.
amount
>
0
?
op
.
amount
:
0
;
triplet
[
NEG
]
+=
op
.
amount
<
0
?
-
op
.
amount
:
0
;
triplet
[
BAL
]
+=
op
.
amount
;
dateset
.
set
(
dk
,
+
op
.
date
);
}
// Sort date in ascending order: push all pairs of (datekey, date) in an
// array and sort that array by the second element. Then read that array in
// ascending order.
let
dates
=
Array
.
from
(
dateset
);
dates
.
sort
((
a
,
b
)
=>
a
[
1
]
-
b
[
1
]);
let
series
=
[];
function
addSerie
(
name
,
mapIndex
)
{
let
data
=
[];
for
(
let
j
=
0
;
j
<
dates
.
length
;
j
++
)
{
let
dk
=
dates
[
j
][
0
];
data
.
push
(
round2
(
map
.
get
(
dk
)[
mapIndex
]));
}
let
serie
=
[
name
].
concat
(
data
);
series
.
push
(
serie
);
}
addSerie
(
$t
(
'
client.charts.received
'
),
POS
);
addSerie
(
$t
(
'
client.charts.spent
'
),
NEG
);
addSerie
(
$t
(
'
client.charts.saved
'
),
BAL
);
let
categories
=
[];
for
(
let
i
=
0
;
i
<
dates
.
length
;
i
++
)
{
let
date
=
new
Date
(
dates
[
i
][
1
]);
// Undefined means the default locale
let
defaultLocale
;
let
str
=
date
.
toLocaleDateString
(
defaultLocale
,
{
year
:
'
numeric
'
,
month
:
'
long
'
});
categories
.
push
(
str
);
}
let
yAxisLegend
=
$t
(
'
client.charts.amount
'
);
c3
.
generate
({
bindto
:
chartId
,
data
:
{
columns
:
series
,
type
:
'
bar
'
},
bar
:
{
width
:
{
ratio
:
.
5
}
},
axis
:
{
x
:
{
type
:
'
category
'
,
categories
},
y
:
{
label
:
yAxisLegend
}
},
grid
:
{
x
:
{
show
:
true
},
y
:
{
show
:
true
,
lines
:
[{
value
:
0
}]
}
}
});
}
export
default
class
InOutChart
extends
ChartComponent
{
redraw
()
{
createChartPositiveNegative
(
'
#barchart
'
,
this
.
props
.
operations
);
}
render
()
{
return
<
div
id
=
"
barchart
"
style
=
{
{
width
:
'
100%
'
}
}
><
/div>
;
}
}
client/components/charts/index.js
View file @
ebbcc440
/* globals c3: false, Dygraph: false */
import
React
from
'
react
'
;
import
{
connect
}
from
'
react-redux
'
;
import
{
store
,
State
}
from
'
../../store
'
;
import
{
assert
,
debug
,
translate
as
$t
}
from
'
../../helpers
'
;
import
{
get
}
from
'
../../store
'
;
import
{
assert
,
translate
as
$t
}
from
'
../../helpers
'
;
import
ChartComponent
from
'
./chart-base
'
;
import
InOutChart
from
'
./in-out-chart
'
;
import
BalanceChart
from
'
./balance-chart
'
;
import
OperationsByCategoryChart
from
'
./operations-by-category-chart
'
;
function
round2
(
x
)
{
return
Math
.
round
(
x
*
100
)
/
100
;
}
// Charts
export
function
createBarChartAll
(
operations
,
barchartId
)
{
function
datekey
(
op
)
{
let
d
=
op
.
date
;
return
`
${
d
.
getFullYear
()}
-
${
d
.
getMonth
()}
`
;
}
// Category -> {Month -> [Amounts]}
let
map
=
new
Map
;
// Category -> color
let
colorMap
=
{};
// Datekey -> Date
let
dateset
=
new
Map
;
for
(
let
i
=
0
,
size
=
operations
.
length
;
i
<
size
;
i
++
)
{
let
op
=
operations
[
i
];
let
c
=
store
.
getCategoryFromId
(
op
.
categoryId
);
map
.
set
(
c
.
title
,
map
.
get
(
c
.
title
)
||
{});
let
categoryDates
=
map
.
get
(
c
.
title
);
let
dk
=
datekey
(
op
);
(
categoryDates
[
dk
]
=
categoryDates
[
dk
]
||
[]).
push
(
op
.
amount
);
dateset
.
set
(
dk
,
+
op
.
date
);
colorMap
[
c
.
title
]
=
colorMap
[
c
.
title
]
||
c
.
color
;
}
// Sort date in ascending order: push all pairs of (datekey, date) in an
// array and sort that array by the second element. Then read that array in
// ascending order.
let
dates
=
Array
.
from
(
dateset
);
dates
.
sort
((
a
,
b
)
=>
a
[
1
]
-
b
[
1
]);
let
series
=
[];
for
(
let
c
of
map
.
keys
())
{
let
data
=
[];
for
(
let
j
=
0
;
j
<
dates
.
length
;
j
++
)
{
let
dk
=
dates
[
j
][
0
];
let
values
=
map
.
get
(
c
)[
dk
]
=
map
.
get
(
c
)[
dk
]
||
[];
data
.
push
(
round2
(
values
.
reduce
((
a
,
b
)
=>
a
+
b
,
0
)));
}
data
=
[
c
].
concat
(
data
);
series
.
push
(
data
);
}
let
categories
=
[];
for
(
let
i
=
0
;
i
<
dates
.
length
;
i
++
)
{
let
date
=
new
Date
(
dates
[
i
][
1
]);
// Undefined means the default locale
let
defaultLocale
;
let
str
=
date
.
toLocaleDateString
(
defaultLocale
,
{
year
:
'
numeric
'
,
month
:
'
long
'
});
categories
.
push
(
str
);
}
let
yAxisLegend
=
$t
(
'
client.charts.amount
'
);
return
c3
.
generate
({
bindto
:
barchartId
,
data
:
{
columns
:
series
,
type
:
'
bar
'
,
colors
:
colorMap
},
bar
:
{
width
:
{
ratio
:
.
5
}
},
axis
:
{
x
:
{
type
:
'
category
'
,
categories
},
y
:
{
label
:
yAxisLegend
}
},
grid
:
{
x
:
{
show
:
true
},
y
:
{
show
:
true
,
lines
:
[{
value
:
0
}]
}
}
});
}
export
function
createPieChartAll
(
operations
,
chartId
)
{
let
catMap
=
new
Map
;
// categoryId -> [val1, val2, val3]
for
(
let
op
of
operations
)
{
let
catId
=
op
.
categoryId
;
let
arr
=
catMap
.
has
(
catId
)
?
catMap
.
get
(
catId
)
:
[];
arr
.
push
(
op
.
amount
);
catMap
.
set
(
catId
,
arr
);
}
// [ [categoryName, val1, val2], [anotherCategoryName, val3, val4] ]
let
series
=
[];
// {label -> color}
let
colorMap
=
{};
for
(
let
[
catId
,
valueArr
]
of
catMap
)
{
let
c
=
store
.
getCategoryFromId
(
catId
);
series
.
push
([
c
.
title
].
concat
(
valueArr
));
colorMap
[
c
.
title
]
=
c
.
color
;
}
return
c3
.
generate
({
bindto
:
chartId
,
data
:
{
columns
:
series
,
type
:
'
pie
'
,
colors
:
colorMap
},
tooltip
:
{
format
:
{
value
(
value
,
ratio
)
{
return
`
${
round2
(
ratio
*
100
)}
% (
${
Math
.
abs
(
round2
(
value
))}
)`
;
}
}
}
});
}
export
function
createChartBalance
(
chartId
,
account
,
operations
)
{
if
(
account
===
null
)
{
debug
(
'
ChartComponent: no account
'
);
return
;
}
let
ops
=
operations
.
slice
().
sort
((
a
,
b
)
=>
+
a
.
date
-
+
b
.
date
);
function
makeKey
(
date
)
{
return
`
${
date
.
getFullYear
()}
-
${
date
.
getMonth
()
+
1
}
-
${
date
.
getDate
()}
`
;
}
let
opmap
=
new
Map
;
// Fill all dates
const
DAY
=
1000
*
60
*
60
*
24
;
let
firstDate
=
ops
.
length
?
+
ops
[
0
].
date
:
Date
.
now
();
firstDate
=
(
firstDate
/
DAY
|
0
)
*
DAY
;
let
today
=
(
Date
.
now
()
/
DAY
|
0
)
*
DAY
;
for
(;
firstDate
<=
today
;
firstDate
+=
DAY
)
{
opmap
.
set
(
makeKey
(
new
Date
(
firstDate
)),
0
);
}
// Date (day) -> cumulated sum of amounts for this day (scalar)
for
(
let
o
of
ops
)
{
let
key
=
makeKey
(
o
.
date
);
opmap
.
set
(
key
,
opmap
.
get
(
key
)
+
o
.
amount
);
}
let
balance
=
account
.
initialAmount
;
let
csv
=
'
Date,Balance
\n
'
;
for
(
let
[
date
,
amount
]
of
opmap
)
{
balance
+=
amount
;
csv
+=
`
${
date
}
,
${
round2
(
balance
)}
\n`
;
}
/* eslint-disable no-new */
// Create the chart
new
Dygraph
(
document
.
querySelector
(
chartId
),
csv
);
/* eslint-enable no-new */
}
export
function
createChartPositiveNegative
(
chartId
,
operations
)
{
function
datekey
(
op
)
{
let
d
=
op
.
date
;
return
`
${
d
.
getFullYear
()}
-
${
d
.
getMonth
()}
`
;
}
const
POS
=
0
,
NEG
=
1
,
BAL
=
2
;
// Month -> [Positive amount, Negative amount, Diff]
let
map
=
new
Map
;
// Datekey -> Date