Closed clstaudt closed 1 year ago
@vlad-ed-git
UI ideas needed: Can we have a split screen for this one?
upper part: controls to run the invoicing workflow (select project, date range...) lower part: list-like view of generated invoices
@vlad-ed-git So this screen brings together different data sources. Here I'm still confused about how to apply MVI, and what it's good for. So far I see it mainly generating a lot of boilerplate code. Need some discussion / pair programming here.
Okay we can work on invoices tomorrow then. I assume the time tracking data source is implemented, so I can write the UI for that.
On Wednesday, January 11, 2023, Christian Staudt @.***> wrote:
@vlad-ed-git https://github.com/vlad-ed-git So this screen brings together different data sources. Here I'm still confused about how to apply MVI, and what it's good for. So far I see it mainly generating a lot of boilerplate code. Need some discussion / pair programming here.
— Reply to this email directly, view it on GitHub https://github.com/tuttle-dev/tuttle/issues/102#issuecomment-1378852640, or unsubscribe https://github.com/notifications/unsubscribe-auth/AJGBW63Q5K3VTHTQ5JZAMXTWR3A77ANCNFSM6AAAAAATUULEVM . You are receiving this because you were mentioned.Message ID: @.***>
Also an Intent class can have multiple data sources. It might even have a reference to other intents . The choice depends on what results in less duplication. For example, the contract intent can have it's own data source which accesses the Contract Model, and have a reference to the Project data source. There are already examples of this in app. e.g. The timetrack intent has access to preferences to get the preferred cloud account and provider. I don't quite see how mvi is confusing esp. in the way we use it currently (it's very bare bones, very few abstractions, no dependency injection, etc).
It's just simply: an event is fired from the UI to an intent class the intent class decides which data is affected by that event, calls the appropriate data source. (*Hence why the intent class can have a reference to multiple data sources). The data source manipulates that data and returns the result (if successful, it flags this as true along with and log message. This is the equivalent of getting at HTTP response with code 200 to mean OK) . The intent checks this response, if it needs any modifications to make it more UI friendly, it does them, then sends them back to the ui (which only focuses on displaying it).
On Wednesday, January 11, 2023, Vladimir Peter @.***> wrote:
Okay we can work on invoices tomorrow then. I assume the time tracking data source is implemented, so I can write the UI for that.
On Wednesday, January 11, 2023, Christian Staudt @.***> wrote:
@vlad-ed-git https://github.com/vlad-ed-git So this screen brings together different data sources. Here I'm still confused about how to apply MVI, and what it's good for. So far I see it mainly generating a lot of boilerplate code. Need some discussion / pair programming here.
— Reply to this email directly, view it on GitHub https://github.com/tuttle-dev/tuttle/issues/102#issuecomment-1378852640, or unsubscribe https://github.com/notifications/unsubscribe-auth/AJGBW63Q5K3VTHTQ5JZAMXTWR3A77ANCNFSM6AAAAAATUULEVM . You are receiving this because you were mentioned.Message ID: @.***>
Let's discuss tomorrow with concrete code examples, implementing a new intent. e.g. Am I supposed add methods that do nothing more than delegation (see example). If yes, why?
class InvoicingIntent:
def __init__(self):
self.project_data_source = ProjectDataSource()
def get_all_projects_as_map(self) -> IntentResult:
"""returns a list of active projects"""
# FIXME: why a method that does nothing but delegate to another method?
result = self.project_data_source.get_all_projects_as_map()
return IntentResult(was_intent_successful=True, data=[])
@vlad-ed-git
I assume the time tracking data source is implemented, so I can write the UI for that.
Nope, this is unfortunately blocked for me because I think we need to clarify the flow and rewrite a few things together (e.g. 2FA for iCloud, different flows for calendar and spreadsheet...)
Frontend & backend as clearly separate working areas - that's nice in theory but not going to work at this point.
I think perhaps the confusion comes from not approaching the code as being composed of different specialized components, which is understandable since
The downside is a bit of code duplication. The alternative is tightly coupled ui-data components (instead of a thin intent class with multiple data sources, you will have an already bloated view class with those multiple data sources).
On Wednesday, January 11, 2023, Vladimir Peter @.***> wrote:
Also an Intent class can have multiple data sources. It might even have a reference to other intents . The choice depends on what results in less duplication. For example, the contract intent can have it's own data source which accesses the Contract Model, and have a reference to the Project data source. There are already examples of this in app. e.g. The timetrack intent has access to preferences to get the preferred cloud account and provider. I don't quite see how mvi is confusing esp. in the way we use it currently (it's very bare bones, very few abstractions, no dependency injection, etc).
It's just simply: an event is fired from the UI to an intent class the intent class decides which data is affected by that event, calls the appropriate data source. (*Hence why the intent class can have a reference to multiple data sources). The data source manipulates that data and returns the result (if successful, it flags this as true along with and log message. This is the equivalent of getting at HTTP response with code 200 to mean OK) . The intent checks this response, if it needs any modifications to make it more UI friendly, it does them, then sends them back to the ui (which only focuses on displaying it).
On Wednesday, January 11, 2023, Vladimir Peter @.***> wrote:
Okay we can work on invoices tomorrow then. I assume the time tracking data source is implemented, so I can write the UI for that.
On Wednesday, January 11, 2023, Christian Staudt < @.***> wrote:
@vlad-ed-git https://github.com/vlad-ed-git So this screen brings together different data sources. Here I'm still confused about how to apply MVI, and what it's good for. So far I see it mainly generating a lot of boilerplate code. Need some discussion / pair programming here.
— Reply to this email directly, view it on GitHub https://github.com/tuttle-dev/tuttle/issues/102#issuecomment-1378852640, or unsubscribe https://github.com/notifications/unsubscribe-auth/AJGBW63Q5K3VTHTQ5JZAMXTWR3A77ANCNFSM6AAAAAATUULEVM . You are receiving this because you were mentioned.Message ID: @.***>
Take the code you brought as an example. Okay get_all_projects_as_map in that intent , is simply calling the data source's get_all_projects_as_map, it does not need to do anything else (other than perhaps set the error message). And you might say well what's the point, this is code duplication. ok. The alternative is , remove the intent middle ground, call the data source directly from the UI. Which also you might say fine. But then take a projects UI view. It needs to load contracts. So now the timetracking view should call the timetracking data source and also the preferences source. It will also have the logic to figure out which of these sources to use and when. You can rewrite the current Timetrack view with this approach and see how bloated that view becomes , and this is with just 2 simple sources.
On Wednesday, January 11, 2023, Vladimir Peter @.***> wrote:
I think perhaps the confusion comes from not approaching the code as being composed of different specialized components, which is understandable since
- You tend to work on all sides - the UI , the intent, the data source, the models and the SQL wrapper ... and
- So far the components are fairly simple. So, for example, we have cases where all an intent method is doing is literally forwarding events (intent.get_all_projects calling data_source.get_all_projects) . But the point of an architecture like mvi is to assume multiple people are working on it and that the project has grown or is grown . So when you work on the project UI, you have to assume you have no idea , for example, that self.query() is a method that queries the project model. So when you work on the UI, and you need all projects, you just say get all projects ...and assume in your head that another person is responsible for that. And when you work on the data source, you just know a request get all projects was given, and respond to it. Whether that goes into a card or even if a user will actually see it isn't your concern at that point (as the data_source dev). If you approach it this way, you will see the point of it .
The downside is a bit of code duplication. The alternative is tightly coupled ui-data components (instead of a thin intent class with multiple data sources, you will have an already bloated view class with those multiple data sources).
On Wednesday, January 11, 2023, Vladimir Peter @.***> wrote:
Also an Intent class can have multiple data sources. It might even have a reference to other intents . The choice depends on what results in less duplication. For example, the contract intent can have it's own data source which accesses the Contract Model, and have a reference to the Project data source. There are already examples of this in app. e.g. The timetrack intent has access to preferences to get the preferred cloud account and provider. I don't quite see how mvi is confusing esp. in the way we use it currently (it's very bare bones, very few abstractions, no dependency injection, etc).
It's just simply: an event is fired from the UI to an intent class the intent class decides which data is affected by that event, calls the appropriate data source. (*Hence why the intent class can have a reference to multiple data sources). The data source manipulates that data and returns the result (if successful, it flags this as true along with and log message. This is the equivalent of getting at HTTP response with code 200 to mean OK) . The intent checks this response, if it needs any modifications to make it more UI friendly, it does them, then sends them back to the ui (which only focuses on displaying it).
On Wednesday, January 11, 2023, Vladimir Peter @.***> wrote:
Okay we can work on invoices tomorrow then. I assume the time tracking data source is implemented, so I can write the UI for that.
On Wednesday, January 11, 2023, Christian Staudt < @.***> wrote:
@vlad-ed-git https://github.com/vlad-ed-git So this screen brings together different data sources. Here I'm still confused about how to apply MVI, and what it's good for. So far I see it mainly generating a lot of boilerplate code. Need some discussion / pair programming here.
— Reply to this email directly, view it on GitHub https://github.com/tuttle-dev/tuttle/issues/102#issuecomment-1378852640, or unsubscribe https://github.com/notifications/unsubscribe-auth/AJGBW63Q5K3VTHTQ5JZAMXTWR3A77ANCNFSM6AAAAAATUULEVM . You are receiving this because you were mentioned.Message ID: @.***>
*correction (But then take a Timetrack UI view. It needs to load preferences as well. So now the timetracking view should call the timetracking data source and also the preferences source...)
On Wednesday, January 11, 2023, Vladimir Peter @.***> wrote:
Take the code you brought as an example. Okay get_all_projects_as_map in that intent , is simply calling the data source's get_all_projects_as_map, it does not need to do anything else (other than perhaps set the error message). And you might say well what's the point, this is code duplication. ok. The alternative is , remove the intent middle ground, call the data source directly from the UI. Which also you might say fine. But then take a Timetrack UI view. It needs to load preferences as well. So now the timetracking view should call the timetracking data source and also the preferences source. It will also have the logic to figure out which of these sources to use and when. You can rewrite the current Timetrack view with this approach and see how bloated that view becomes , and this is with just 2 simple sources.
On Wednesday, January 11, 2023, Vladimir Peter @.***> wrote:
I think perhaps the confusion comes from not approaching the code as being composed of different specialized components, which is understandable since 1. You tend to work on all sides - the UI , the intent, the data source, the models and the SQL wrapper ... and
- So far the components are fairly simple. So, for example, we have cases where all an intent method is doing is literally forwarding events (intent.get_all_projects calling data_source.get_all_projects) . But the point of an architecture like mvi is to assume multiple people are working on it and that the project has grown or is grown . So when you work on the project UI, you have to assume you have no idea , for example, that self.query() is a method that queries the project model. So when you work on the UI, and you need all projects, you just say get all projects ...and assume in your head that another person is responsible for that. And when you work on the data source, you just know a request get all projects was given, and respond to it. Whether that goes into a card or even if a user will actually see it isn't your concern at that point (as the data_source dev). If you approach it this way, you will see the point of it .
The downside is a bit of code duplication. The alternative is tightly coupled ui-data components (instead of a thin intent class with multiple data sources, you will have an already bloated view class with those multiple data sources).
On Wednesday, January 11, 2023, Vladimir Peter @.***> wrote:
Also an Intent class can have multiple data sources. It might even have a reference to other intents . The choice depends on what results in less duplication. For example, the contract intent can have it's own data source which accesses the Contract Model, and have a reference to the Project data source. There are already examples of this in app. e.g. The timetrack intent has access to preferences to get the preferred cloud account and provider. I don't quite see how mvi is confusing esp. in the way we use it currently (it's very bare bones, very few abstractions, no dependency injection, etc).
It's just simply: an event is fired from the UI to an intent class the intent class decides which data is affected by that event, calls the appropriate data source. (*Hence why the intent class can have a reference to multiple data sources). The data source manipulates that data and returns the result (if successful, it flags this as true along with and log message. This is the equivalent of getting at HTTP response with code 200 to mean OK) . The intent checks this response, if it needs any modifications to make it more UI friendly, it does them, then sends them back to the ui (which only focuses on displaying it).
On Wednesday, January 11, 2023, Vladimir Peter @.***> wrote:
Okay we can work on invoices tomorrow then. I assume the time tracking data source is implemented, so I can write the UI for that.
On Wednesday, January 11, 2023, Christian Staudt < @.***> wrote:
@vlad-ed-git https://github.com/vlad-ed-git So this screen brings together different data sources. Here I'm still confused about how to apply MVI, and what it's good for. So far I see it mainly generating a lot of boilerplate code. Need some discussion / pair programming here.
— Reply to this email directly, view it on GitHub https://github.com/tuttle-dev/tuttle/issues/102#issuecomment-1378852640, or unsubscribe https://github.com/notifications/unsubscribe-auth/AJGBW63Q5K3VTHTQ5JZAMXTWR3A77ANCNFSM6AAAAAATUULEVM . You are receiving this because you were mentioned.Message ID: @.***>
I think my questions are less about the MVI pattern, I get the point of decoupling UI and backend.
They are about how we organize the code for this app on top of MVI.
A) Each screen has its own custom view.py / intent.py / data_source.py package?
That I think produces a lot of boilerplate code as in the example above.
B) Screens can freely import from views and intents all across the project and combine them.
Then InvoicingIntent.get_all_projects_as_map
is probably unnecessary, the view could just use ProjectIntent.get_all_projects_as_map
. Correct? Any disadvantages?
So invoicing view, will have two intent objects, a Project intent and an Invoicing intent? i.e. a view can have multiple intent objects depending on the models it requires? Isn't the UI code already a lot (with all the views and their modifiers) , without also having such intent logic put in there too?
On Thursday, January 12, 2023, Christian Staudt @.***> wrote:
I think my questions are less about the MVI pattern, I get the point of decoupling UI and backend.
They are about how we organize the code for this app on top of MVI.
A) Each screen has its own custom view.py / intent.py / data_source.py package?
That I think produces a lot of boilerplate code as in the example above.
B) Screens can freely import from views and intents all across the project and combine them.
Then InvoicingIntent.get_all_projects_as_map is probably unnecessary, the view could just use ProjectIntent.get_all_projects_as_map. Correct? Any disadvantages?
— Reply to this email directly, view it on GitHub https://github.com/tuttle-dev/tuttle/issues/102#issuecomment-1379270016, or unsubscribe https://github.com/notifications/unsubscribe-auth/AJGBW63L2EWTWABOSUUIZVLWR3XXHANCNFSM6AAAAAATUULEVM . You are receiving this because you were mentioned.Message ID: @.***>
Yes InvoicingView
has more than one intent handler, so that the methods of ProjectIntent
do not need to be duplicated (as delegation) by InvoicingIntent
.
Is that not less code? And less complexity (indirections -> complexity)?
Is not duplicating 3 lines of code worth giving the UI that responsibility ? What happens if the projectIntent adds a project UI specific error message Incase of unsuccessful intent? What happens if tomorrow we decide the projects data returned should be slightly different between the two views?
On Thursday, January 12, 2023, Christian Staudt @.***> wrote:
Yes InvoicingView has more than one intent handler, so that the methods of ProjectIntent do not need to be duplicated (as delegation) by InvoicingIntent.
IMHO that means less code.
— Reply to this email directly, view it on GitHub https://github.com/tuttle-dev/tuttle/issues/102#issuecomment-1379373539, or unsubscribe https://github.com/notifications/unsubscribe-auth/AJGBW63BJ25QPLLWKBU3RNLWR4B75ANCNFSM6AAAAAATUULEVM . You are receiving this because you were mentioned.Message ID: @.***>
So the relationship between screen and view.py and intent.py should be 1:1:1, right? What about intent.py and data_source.py?
Yes
On Thursday, January 12, 2023, Christian Staudt @.***> wrote:
Should an Intent be able to have more than one DataSource?
— Reply to this email directly, view it on GitHub https://github.com/tuttle-dev/tuttle/issues/102#issuecomment-1379491715, or unsubscribe https://github.com/notifications/unsubscribe-auth/AJGBW64MKRZM64F4QW3Z7UDWR4PDBANCNFSM6AAAAAATUULEVM . You are receiving this because you were mentioned.Message ID: @.***>
It can have multiple data sources and it typically does (a file, a logger class, a remote api, a local database itself with multiple entry points for different tables, a preferences store etc) . That's the reason for having an Intent. Figuring out which data source should handle which "intent" .
On Thursday, January 12, 2023, Christian Staudt @.***> wrote:
Should an Intent be able to have more than one DataSource?
— Reply to this email directly, view it on GitHub https://github.com/tuttle-dev/tuttle/issues/102#issuecomment-1379491715, or unsubscribe https://github.com/notifications/unsubscribe-auth/AJGBW64MKRZM64F4QW3Z7UDWR4PDBANCNFSM6AAAAAATUULEVM . You are receiving this because you were mentioned.Message ID: @.***>
*it can even refer to another intent class if the function it is doing is already handled in that intent.
for example: get_all_projects is an intent that is handled by the ProjectIntent.
ProjectsUi -----> get projects ---> ProjectIntent ---> Project data source
If for example the InvoicingIntent receives this intent , then it can handle it in 2 ways : either by calling the project data_source directly, and bypassing the project intent OR by simply passing that intent to the ProjectIntent class.
1.
Invoice UI ---> get projects ---> InvoiceIntent --->
Project data source
2. Invoice UI ---> get projects ---> InvoiceIntent ---> ProjectIntent ---> Project data source
The first option though is the more standard approach , because the second approach assumes that whatever the ProjectIntent.get_all_projects did to modify the result for it's UI, is sufficient for the Invoicing Ui, which is often not the case.
Typically what Invoice UI is doing with the projects data is different from what the Projects UI is doing. And thus will expect it's intent handler to provide projects data in a slightly different way.
Also it's cleaner to just have it as a convention that an intent must always talk to the data_source directly and not via another intent (but this is a convention not a rule, and even in Tuttle it's broken at times. For instance TimeTrackIntent speaks to preferences intent not the preferences store).
On Thursday, January 12, 2023, Vladimir Peter @.***> wrote:
It can have multiple data sources and it typically does (a file, a logger class, a remote api, a local database itself with multiple entry points for different tables, a preferences store etc) . That's the reason for having an Intent. Figuring out which data source should handle which "intent" .
On Thursday, January 12, 2023, Christian Staudt @.***> wrote:
Should an Intent be able to have more than one DataSource?
— Reply to this email directly, view it on GitHub https://github.com/tuttle-dev/tuttle/issues/102#issuecomment-1379491715, or unsubscribe https://github.com/notifications/unsubscribe-auth/AJGBW64MKRZM64F4QW3Z7UDWR4PDBANCNFSM6AAAAAATUULEVM . You are receiving this because you were mentioned.Message ID: @.***>
On the subject of how to approach a clean (or somewhat clean) architecture, ask yourself this. When you call say self.store() to communicate with the SQL handler , do you check to see if store does some justifiable work instead of something like calling self.store_passed_obj() , which would seem redundant, OR do you just call self.store() because the documentation says it stores the object and you don't worry about the how? I suspect the answer is the latter. And if you actually started to dig into the code you of the libraries you use, you will notice a lot of "contracts" i.e. methods which do nothing but offer a doc string and forwards the passed parameters to yet another method . This is what clean architecture aims for. That the person coding Card(margin=margin.all(20), content=project) only knows that project comes from calling get_projects ... Whether the implementation of get_projects is a seemingly redundant call to a data source is not the concern of this ui person whatsoever. You have to place yourself in this mindset otherwise you will see no need to even have a data source class..you might just as well call self.store from the UI.
On Thursday, January 12, 2023, Vladimir Peter @.***> wrote:
*it can even refer to another intent class if the function it is doing is already handled in that intent.
for example: get_all_projects is an intent that is handled by the ProjectIntent.
ProjectsUi -----> get projects ---> ProjectIntent ---> Project data source
If for example the InvoicingIntent receives this intent , then it can handle it in 2 ways : either by calling the project data_source directly, and bypassing the project intent OR by simply passing that intent to the ProjectIntent class.
1. Invoice UI ---> get projects ---> InvoiceIntent ---> Project data
source
2. Invoice UI ---> get projects ---> InvoiceIntent ---> ProjectIntent ---> Project data source
The first option though is the more standard approach , because the second approach assumes that whatever the ProjectIntent.get_all_projects did to modify the result for it's UI, is sufficient for the Invoicing Ui, which is often not the case.
Typically what Invoice UI is doing with the projects data is different from what the Projects UI is doing. And thus will expect it's intent handler to provide projects data in a slightly different way.
Also it's cleaner to just have it as a convention that an intent must always talk to the data_source directly and not via another intent (but this is a convention not a rule, and even in Tuttle it's broken at times. For instance TimeTrackIntent speaks to preferences intent not the preferences store).
On Thursday, January 12, 2023, Vladimir Peter @.***> wrote:
It can have multiple data sources and it typically does (a file, a logger class, a remote api, a local database itself with multiple entry points for different tables, a preferences store etc) . That's the reason for having an Intent. Figuring out which data source should handle which "intent" .
On Thursday, January 12, 2023, Christian Staudt @.***> wrote:
Should an Intent be able to have more than one DataSource?
— Reply to this email directly, view it on GitHub https://github.com/tuttle-dev/tuttle/issues/102#issuecomment-1379491715, or unsubscribe https://github.com/notifications/unsubscribe-auth/AJGBW64MKRZM64F4QW3Z7UDWR4PDBANCNFSM6AAAAAATUULEVM . You are receiving this because you were mentioned.Message ID: @.***>
Here is an example of the select method from the sqlmodel you use (from their GitHub source).
def select(*entities: Any, kw: Any): if len(entities) == 1: return SelectOfScalar._create(*entities, *kw) return Select._create(entities, kw)
All this select method is doing is figuring out if you have 1 or more entities and decides which type of select to call. It acts as an intent, the intention is to select, but you can have 1 or more entities and this method decides where to forward that intent based on just this 1 piece of if statement. They, via the documentation, could have easily asked You the user to call SelectOfScalar for many entities , and Select for a single entity, and exposed those methods to you ...but that would be asking you to be concerned about something that you shouldn't be (i.e. the fact that multiple entities require a different approach to selection than a single entity). Your intent is to select, so you call select .
***There are probably other advantages to the above like keeping some properties private , but for the purpose of this demonstration, I am showing you what layers in an architecture are about , i.e separation of concerns.
n Thursday, January 12, 2023, Vladimir Peter @.***> wrote:
On the subject of how to approach a clean (or somewhat clean) architecture, ask yourself this. When you call say self.store() to c ommunicate with the SQL handler , do you check to see if store does some justifiable work instead of something like calling self.store_passed_obj() , which would seem redundant, OR do you just call self.store() because the documentation says it stores the object and you don't worry about the how? I suspect the answer is the latter. And if you actually started to dig into the code you of the libraries you use, you will notice a lot of "contracts" i.e. methods which do nothing but offer a doc string and forwards the passed parameters to yet another method . This is what clean architecture aims for. That the person coding Card(margin=margin.all(20), content=project) only knows that project comes from calling get_projects ... Whether the implementation of get_projects is a seemingly redundant call to a data source is not the concern of this ui person whatsoever. You have to place yourself in this mindset otherwise you will see no need to even have a data source class..you might just as well call self.store from the UI.
On Thursday, January 12, 2023, Vladimir Peter @.***> wrote:
*it can even refer to another intent class if the function it is doing is already handled in that intent.
for example: get_all_projects is an intent that is handled by the ProjectIntent.
ProjectsUi -----> get projects ---> ProjectIntent ---> Project data source
If for example the InvoicingIntent receives this intent , then it can handle it in 2 ways : either by calling the project data_source directly, and bypassing the project intent OR by simply passing that intent to the ProjectIntent class.
1. Invoice UI ---> get projects ---> InvoiceIntent ---> Project data
source
2. Invoice UI ---> get projects ---> InvoiceIntent ---> ProjectIntent ---> Project data source
The first option though is the more standard approach , because the second approach assumes that whatever the ProjectIntent.get_all_projects did to modify the result for it's UI, is sufficient for the Invoicing Ui, which is often not the case.
Typically what Invoice UI is doing with the projects data is different from what the Projects UI is doing. And thus will expect it's intent handler to provide projects data in a slightly different way.
Also it's cleaner to just have it as a convention that an intent must always talk to the data_source directly and not via another intent (but this is a convention not a rule, and even in Tuttle it's broken at times. For instance TimeTrackIntent speaks to preferences intent not the preferences store).
On Thursday, January 12, 2023, Vladimir Peter @.***> wrote:
It can have multiple data sources and it typically does (a file, a logger class, a remote api, a local database itself with multiple entry points for different tables, a preferences store etc) . That's the reason for having an Intent. Figuring out which data source should handle which "intent" .
On Thursday, January 12, 2023, Christian Staudt < @.***> wrote:
Should an Intent be able to have more than one DataSource?
— Reply to this email directly, view it on GitHub https://github.com/tuttle-dev/tuttle/issues/102#issuecomment-1379491715, or unsubscribe https://github.com/notifications/unsubscribe-auth/AJGBW64MKRZM64F4QW3Z7UDWR4PDBANCNFSM6AAAAAATUULEVM . You are receiving this because you were mentioned.Message ID: @.***>
@vlad-ed-git Note that if you reply to the issue by email the formatting for code is messed up.
Given that project data and time tracking data has been entered, this screen should provide a UI for generating invoices for projects.
[x] - select projects
[x] - select date ranges
...