Skip to content

Commit e471152

Browse files
authored
Merge pull request #45 from janssen-io/posting-tags
Rudimentary version of tags for postings. Tags for transactions can be added by adding them on separate lines in the description. Wishlist: - UI is a bit clunky, especially on wider screens there's plenty of room to place tags in a separate column - Some of the models are actually being used as view models. Should be refactored. - The types used in the F# API are becoming very wieldy. There's an opportunity here to define/move the models to a shared project instead and use them for both the interface and possibly persistence for the Desktop app? Fixes #11
2 parents 8d551b4 + 45832f4 commit e471152

File tree

29 files changed

+606
-78
lines changed

29 files changed

+606
-78
lines changed

src/TransactionQL.Application/API.fs

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -38,34 +38,34 @@ module API =
3838
| Interpretation(_, Some entry) -> Left entry
3939
| Interpretation(_, None) -> Right row)
4040

41-
let formatPosting date title (description: string) trx =
41+
let formatPosting date title (description: string) (tags: string list) trx =
4242
let header = Header(date, title)
4343

4444
let lines =
4545
trx
46-
|> Array.map (fun (account, (currency: string), (amount: Nullable<decimal>)) ->
46+
|> Array.map (fun (account, (currency: string), (amount: Nullable<decimal>), (postingTags: string array)) ->
4747
let acc = (account :: [])
4848

4949
match (String.IsNullOrEmpty currency, Option.ofNullable amount) with
5050
| true, _
5151
| _, None ->
5252
{ Account = acc
5353
Amount = None
54-
Tag = None }
54+
Tags = postingTags }
5555
| _, Some a ->
5656
{ Account = acc
5757
Amount = Some(currency, float a)
58-
Tag = None })
58+
Tags = postingTags })
5959
|> List.ofArray
6060

6161
let newLines = [| "\r\n"; "\n" |]
62-
6362
let entry =
6463
{ Header = header
6564
Lines = lines
66-
Comments = List.ofArray (description.Split(newLines, StringSplitOptions.RemoveEmptyEntries)) }
67-
68-
// TODO: generalize indents, not sure if this is the right place.
69-
let sprintDesc = (List.map <| (fun line -> $" ; %s{line}"))
65+
Comments =
66+
List.ofArray (description.Split(newLines, StringSplitOptions.RemoveEmptyEntries))
67+
|> List.append tags
68+
}
7069

70+
let sprintDesc = (List.map <| (fun line -> $"{Format.INDENT}; {line}"))
7171
Formatter.sprintPosting Format.ledger sprintDesc id entry

src/TransactionQL.Application/Formatter.fs

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,23 +11,30 @@ module Format =
1111
Precision = 2
1212
Comment = "; " }
1313

14+
let INDENT = " ";
15+
1416
module Formatter =
1517
open System
1618
open TransactionQL.Parser.QLInterpreter
17-
open TransactionQL.Parser.AST
1819
open Format
1920

2021
let sprintHeader format (Header(date, payee)) =
2122
$"%s{date.ToString format.Date} %s{payee}"
2223

2324
let commentLine format = sprintf "%s%s" format.Comment
2425

26+
let formatTags (tags: string array) =
27+
match tags with
28+
| [||] -> ""
29+
| xs -> Array.map (fun t -> $"{Environment.NewLine}{INDENT}; {t}") xs
30+
|> String.concat String.Empty
31+
2532
let sprintLine
2633
format
2734
floatWidth
2835
({ Account = accountParts
2936
Amount = amount
30-
Tag = tag }: Line)
37+
Tags = tag }: Line)
3138
=
3239
let account = String.Join(":", accountParts)
3340
let numOfSpaces = Math.Max(0, 43 - account.Length)
@@ -38,11 +45,10 @@ module Formatter =
3845
// Accounts and commodities must be separated by atleast two spaces
3946
sprintf "%s %*s %*.*f" account numOfSpaces commodity floatWidth format.Precision sum
4047
| None -> account
48+
49+
let tags = formatTags tag
4150

42-
match tag with
43-
| Some text -> $"%s{line} ; %s{text}"
44-
| None -> line
45-
|> sprintf " %s" // indent lines
51+
String.concat String.Empty [|INDENT; line; tags|]
4652

4753
let sprintPosting format sprintDescription (modifyLine: string -> string) entry =
4854
let { Header = header

src/TransactionQL.Console.Tests/FormatterTests.fs

Lines changed: 14 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,12 @@ open Xunit
44
open TransactionQL.Application
55
open TransactionQL.Application.Format
66
open TransactionQL.Parser.QLInterpreter
7-
open TransactionQL.Parser.AST
87
open System
98

109
let line (account, amount, tag) =
1110
{ Account = account
1211
Amount = amount
13-
Tag = tag }
12+
Tags = tag }
1413
: Line
1514

1615
[<Fact>]
@@ -41,7 +40,7 @@ let ``Header: ends with the payee`` () =
4140

4241
[<Fact>]
4342
let ``Line: is indented`` () =
44-
let line = line ([ "A"; "B" ], None, None)
43+
let line = line ([ "A"; "B" ], None, [||])
4544

4645
let result =
4746
Formatter.sprintLine
@@ -55,7 +54,7 @@ let ``Line: is indented`` () =
5554

5655
[<Fact>]
5756
let ``Line: concatenates accounts with colon`` () =
58-
let line = line ([ "A"; "B" ], None, None)
57+
let line = line ([ "A"; "B" ], None, [||])
5958

6059
let result =
6160
Formatter.sprintLine
@@ -69,7 +68,7 @@ let ``Line: concatenates accounts with colon`` () =
6968

7069
[<Fact>]
7170
let ``Line: separates accounts and commodity with at least two spaces`` () =
72-
let line = line ([ "A"; "B" ], Some("$", 25.00), None)
71+
let line = line ([ "A"; "B" ], Some("$", 25.00), [||])
7372

7473
let result =
7574
Formatter.sprintLine
@@ -95,7 +94,7 @@ let ``Line: prints the float with the given precision`` () =
9594
Comment = "# " }
9695

9796
let amount = 25.12345678
98-
let line = line ([ "A"; "B" ], Some("", amount), None)
97+
let line = line ([ "A"; "B" ], Some("", amount), [||])
9998
let result = Formatter.sprintLine format 0 line
10099
Assert.EndsWith("25.123", result)
101100

@@ -107,7 +106,7 @@ let ``Line: Adds tags (if any) after two spaces`` () =
107106
Comment = "; " }
108107

109108
let amount = 25.12345678
110-
let line = line ([ "A"; "B" ], Some("", amount), Some "My: Tag")
109+
let line = line ([ "A"; "B" ], Some("", amount), [|"My: Tag"|])
111110
let result = Formatter.sprintLine format 0 line
112111
Assert.EndsWith(" ; My: Tag", result)
113112

@@ -116,8 +115,8 @@ let ``Posting: prints header and lines on separate lines`` () =
116115
let posting =
117116
{ Header = Header(new DateTime(2019, 1, 1), "Payee")
118117
Lines =
119-
[ line ([ "A"; "B" ], Some("", 10.00), None)
120-
line ([ "C"; "D" ], None, None) ]
118+
[ line ([ "A"; "B" ], Some("", 10.00), [||])
119+
line ([ "C"; "D" ], None, [||]) ]
121120
Comments = [] }
122121

123122
let result =
@@ -137,8 +136,8 @@ let ``Posting: aligns amounts to the right`` () =
137136
let posting =
138137
{ Header = Header(new DateTime(2019, 1, 1), "Payee")
139138
Lines =
140-
[ line ([ "Assets"; "Checking" ], Some("", 10.), None)
141-
line ([ "Expenses"; "Vacation" ], Some("$", -1000.), None) ]
139+
[ line ([ "Assets"; "Checking" ], Some("", 10.), [||])
140+
line ([ "Expenses"; "Vacation" ], Some("$", -1000.), [||]) ]
142141
Comments = [] }
143142

144143
let result =
@@ -160,8 +159,8 @@ let ``Missing posting: adds comment before each line`` () =
160159
let posting =
161160
{ Header = Header(new DateTime(2019, 1, 1), "Payee")
162161
Lines =
163-
[ line ([ "A"; "B" ], Some("", 10.00), None)
164-
line ([ "C"; "D" ], None, None) ]
162+
[ line ([ "A"; "B" ], Some("", 10.00), [||])
163+
line ([ "C"; "D" ], None, [||]) ]
165164
Comments = [] }
166165

167166
let format: Format =
@@ -178,8 +177,8 @@ let ``Comments: comments are added between the header and transactions`` () =
178177
let posting =
179178
{ Header = Header(new DateTime(2019, 1, 1), "Payee")
180179
Lines =
181-
[ line ([ "A"; "B" ], Some("", 10.00), None)
182-
line ([ "C"; "D" ], None, None) ]
180+
[ line ([ "A"; "B" ], Some("", 10.00), [||])
181+
line ([ "C"; "D" ], None, [||]) ]
183182
Comments = [ "Two lines"; "Of comments" ] }
184183

185184
let format: Format =

src/TransactionQL.DesktopApp/App.axaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
<ResourceInclude Source="/Assets/ThemeVariants/Dark.axaml" />
1616
<ResourceInclude Source="/Assets/ThemeVariants/Light.axaml" />
1717
<ResourceInclude Source="/Assets/Variables.axaml" />
18+
<ResourceInclude Source="/Controls/Badge.axaml" />
1819
<ResourceInclude Source="/Controls/Dropzone.axaml" />
1920
<ResourceInclude Source="/Controls/StepIndicator.axaml" />
2021
<ResourceInclude Source="/Controls/TitledBorder.axaml" />

src/TransactionQL.DesktopApp/Assets/Styles/Grid.axaml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,12 @@
1818
<Setter Property="Margin" Value="5 5 9 5" />
1919
</Style>
2020
<Style Selector="^ > AutoCompleteBox /template/ TextBox /template/ Border">
21-
<Setter Property="Background" Value="Transparent" />
2221
<Setter Property="BorderThickness" Value="0" />
2322
<Setter Property="Margin" Value="5 5" />
2423
</Style>
24+
<Style Selector="^ > WrapPanel">
25+
<Setter Property="Margin" Value="10 -5 0 10" />
26+
</Style>
2527
</Style>
2628

2729
<Style Selector="Grid.Editable">
Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
<ResourceDictionary xmlns="https://github.com/avaloniaui"
2+
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
3+
xmlns:controls="using:TransactionQL.DesktopApp.Controls"
4+
xmlns:c="using:TransactionQL.DesktopApp.Controls"
5+
xmlns:i="clr-namespace:Projektanker.Icons.Avalonia;assembly=Projektanker.Icons.Avalonia" >
6+
7+
<!--
8+
Additional resources
9+
Using Control Themes:
10+
https://docs.avaloniaui.net/docs/basics/user-interface/styling/control-themes
11+
Using Theme Variants:
12+
https://docs.avaloniaui.net/docs/guides/styles-and-resources/how-to-use-theme-variants
13+
-->
14+
15+
<Design.PreviewWith>
16+
<StackPanel Orientation="Horizontal" Spacing="3" Background="{DynamicResource BG.Base.Primary}">
17+
<ThemeVariantScope RequestedThemeVariant="Light">
18+
<Border Padding="20" Background="{DynamicResource BG.Base.Neutral}" Width="250">
19+
<WrapPanel Orientation="Horizontal">
20+
<controls:Badge Text="Events"
21+
Icon="fa-flag"
22+
Background="{DynamicResource BG.Soft.Primary}"
23+
BorderBrush="{DynamicResource Border.Base.Primary}"
24+
Foreground="{DynamicResource FG.Elevated.Primary}"/>
25+
<controls:Badge Text="Events"
26+
Icon="fa-flag"
27+
Background="{DynamicResource BG.Base.Error}"
28+
BorderBrush="{DynamicResource Border.Base.Error}"
29+
Foreground="{DynamicResource FG.Elevated.Error}"/>
30+
<controls:Badge Text="Trips" Icon="fa-plane" />
31+
<controls:Badge Text="Social" />
32+
<controls:Badge Text="Gift" />
33+
<controls:Badge Text="Wedding" Icon="fa-church" />
34+
<controls:Badge Text="Add"
35+
Icon="fa-plus"
36+
IsRemovable="False"
37+
FontSize="18"
38+
BorderBrush="{DynamicResource Border.Soft.Neutral}"
39+
Foreground="{DynamicResource FG.Subtle.Neutral}"/>
40+
<controls:Badge Icon="fa-plus" Text="" IsRemovable="False" />
41+
</WrapPanel>
42+
43+
</Border>
44+
</ThemeVariantScope>
45+
<ThemeVariantScope RequestedThemeVariant="Dark">
46+
<Border Padding="20" Background="{DynamicResource BG.Base.Neutral}" Width="250">
47+
<WrapPanel Orientation="Horizontal">
48+
<controls:Badge Text="Events"
49+
Icon="fa-flag"
50+
Background="{DynamicResource BG.Soft.Primary}"
51+
BorderBrush="{DynamicResource Border.Base.Primary}"
52+
Foreground="{DynamicResource FG.Elevated.Primary}"/>
53+
<controls:Badge Text="Events"
54+
Icon="fa-flag"
55+
Background="{DynamicResource BG.Base.Error}"
56+
BorderBrush="{DynamicResource Border.Base.Error}"
57+
Foreground="{DynamicResource FG.Elevated.Error}"/>
58+
<controls:Badge Text="Trips" Icon="fa-plane" />
59+
<controls:Badge Text="Social" />
60+
<controls:Badge Text="Gift" />
61+
<controls:Badge Text="Wedding" Icon="fa-church" />
62+
<controls:Badge Text="Add"
63+
Icon="fa-plus"
64+
IsRemovable="False"
65+
FontSize="18"
66+
BorderBrush="{DynamicResource Border.Soft.Neutral}"
67+
Foreground="{DynamicResource FG.Subtle.Neutral}"/>
68+
<controls:Badge Icon="fa-plus" Text="" IsRemovable="False" />
69+
</WrapPanel>
70+
</Border>
71+
</ThemeVariantScope>
72+
</StackPanel>
73+
</Design.PreviewWith>
74+
75+
<ControlTheme x:Key="{x:Type controls:Badge}" TargetType="controls:Badge">
76+
<ControlTheme.Resources>
77+
<c:HasValueConverter x:Key="hasValue" />
78+
<c:EmptyToNullConverter x:Key="emptyToNull" />
79+
<c:RelativeSizeConverter x:Key="relativeSize" />
80+
</ControlTheme.Resources>
81+
<Setter Property="BorderThickness" Value="1"/>
82+
<Setter Property="BorderBrush" Value="{DynamicResource Border.Base.Neutral}" />
83+
<Setter Property="Background" Value="{DynamicResource BG.Base.Neutral}" />
84+
<Setter Property="Foreground" Value="{DynamicResource FG.Base.Neutral}" />
85+
<Setter Property="FontSize" Value="10" />
86+
<Styles>
87+
88+
<Style Selector="Border.Section">
89+
<Setter Property="Margin" Value="2" />
90+
<Setter Property="Padding" Value="5" />
91+
<Setter Property="CornerRadius" Value="100" />
92+
<Setter Property="Background">
93+
<Binding RelativeSource="{RelativeSource AncestorType={x:Type controls:Badge}}" Path="Background" />
94+
</Setter>
95+
96+
<Style Selector="^ StackPanel TextBlock.Title">
97+
<Setter Property="Margin" Value="5 0" />
98+
<Setter Property="FontSize">
99+
<Binding RelativeSource="{RelativeSource AncestorType={x:Type controls:Badge}}" Path="FontSize" />
100+
</Setter>
101+
<Style Selector="^.WithIcon">
102+
<Setter Property="Margin" Value="0 0 5 0" />
103+
</Style>
104+
</Style>
105+
<Style Selector="^ StackPanel i|Icon.Label">
106+
<Setter Property="Margin" Value="0" />
107+
<Setter Property="FontSize">
108+
<Binding RelativeSource="{RelativeSource AncestorType={x:Type controls:Badge}}" Path="FontSize" />
109+
</Setter>
110+
<Style Selector="^.WithText">
111+
<Setter Property="Margin" Value="5 0" />
112+
</Style>
113+
</Style>
114+
<Style Selector="^ StackPanel i|Icon.Remove">
115+
<Setter Property="Margin" Value="0 0 0 0" />
116+
<Setter Property="Foreground">
117+
<Binding RelativeSource="{RelativeSource AncestorType={x:Type controls:Badge}}" Path="Foreground" />
118+
</Setter>
119+
</Style>
120+
<Style Selector="^ StackPanel Button">
121+
<Setter Property="Margin" Value="0 1 1 0" />
122+
<Setter Property="Padding" Value="1" />
123+
<Setter Property="Background" Value="Transparent"/>
124+
</Style>
125+
</Style>
126+
</Styles>
127+
<Setter Property="Template">
128+
<ControlTemplate>
129+
<Border Classes="Section"
130+
HorizontalAlignment="{TemplateBinding HorizontalAlignment}"
131+
VerticalAlignment="Top"
132+
BorderBrush="{TemplateBinding BorderBrush}"
133+
BorderThickness="{TemplateBinding BorderThickness}"
134+
ToolTip.Tip="{TemplateBinding Detail, Converter={StaticResource emptyToNull}}">
135+
<StackPanel Orientation="Horizontal">
136+
<i:Icon Value="{TemplateBinding Icon}"
137+
Classes="Label" Classes.WithText="{TemplateBinding Text, Converter={StaticResource hasValue} }"
138+
IsVisible="{TemplateBinding Icon, Converter={StaticResource hasValue} }" />
139+
<TextBlock Classes="Title"
140+
Classes.WithIcon="{TemplateBinding Icon, Converter={StaticResource hasValue} }"
141+
Text="{TemplateBinding Text}"
142+
Name="Title"
143+
IsVisible="{TemplateBinding Text, Converter={StaticResource hasValue} }" />
144+
<Button IsVisible="{TemplateBinding IsRemovable}" Name="RemoveButton">
145+
<i:Icon Classes="Remove" Value="fa-x"
146+
FontSize="{Binding FontSize, ElementName=Title, Converter={StaticResource relativeSize}, ConverterParameter=0.5}"
147+
/>
148+
</Button>
149+
</StackPanel>
150+
</Border>
151+
</ControlTemplate>
152+
</Setter>
153+
</ControlTheme>
154+
</ResourceDictionary>

0 commit comments

Comments
 (0)