Skip to content
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

Expose TreeTable to extensions #1695

Open
scauligi opened this issue Oct 11, 2023 · 3 comments
Open

Expose TreeTable to extensions #1695

scauligi opened this issue Oct 11, 2023 · 3 comments
Labels
extension Features or enhancements that have Fava extensions or could be implemented as one.

Comments

@scauligi
Copy link

scauligi commented Oct 11, 2023

Version 1.26 moved much of the UI generation from templates into svelte/javascript. Before, I could include some of the template helpers (eg for treetables) in my own extension templates, but now that it's in svelte there doesn't seem to be a good way of making my own TreeTables aside from copying-and-pasting code and CSS styles.

Is it possible to expose TreeTables in a way that extensions can more easily use them?

@scauligi scauligi changed the title Expose Treemap Expose Treemap to extensions Oct 11, 2023
@scauligi scauligi changed the title Expose Treemap to extensions Expose TreeTable to extensions Oct 11, 2023
@yagebu yagebu added the extension Features or enhancements that have Fava extensions or could be implemented as one. label Jan 6, 2024
@yagebu
Copy link
Member

yagebu commented Jan 6, 2024

Hi, thanks for opening this issue. As also seen in #1721, this seems to be a useful component for extensions :) As a "quick fix", I've re-added the old css and js so you could still use the old ones for a while (however, I haven't re-added the template code so you'd need to copy paste that).

I think there's basically two ways that the new component(s) could be exposed to the frontend: 1) providing them to Javascript extensions (https://svelte.dev/docs/client-side-component-api) or 2) wrapping them in a custom HTML element (just like it is already done for the charts) where the data for the component would be provided as JSON in this custom element. (doing both also be an option)

Could you describe your use case a bit? Are you using them to render a Fava TreeNode or do you want to render some other tree-like data (like fava_investor seems to do)?

@upsuper
Copy link
Contributor

upsuper commented Jan 26, 2024

I would like to also be able to access the tree table.

In my use case, I render the balance sheet with only accounts under certain criteria.

@upsuper
Copy link
Contributor

upsuper commented Jan 26, 2024

(however, I haven't re-added the template code so you'd need to copy paste that)

It's definitely not that simple as just copy and paste, as a lot of code was deleted as part of that change. I had to go through a lot of hoops before it started working again.

To save people some time, this is my _tree_table.html:

_tree_table.html
{% macro account_name(ledger, account_name) -%}
<a href="{{ url_for('account', name=account_name) }}" class="account">
  {{- account_name.split(':')[-1] -}}
</a>
{%- if ledger.accounts[account_name].uptodate_status %}

{{ indicator(ledger, account_name) }}
{{ last_account_activity(ledger, account_name) }}
{% endif %}
{% endmacro %}

{% macro render_currency(ledger, currency) -%}
<span title="{{ ledger.commodities.name(currency) }}">{{ currency }}</span>
{%- endmacro %}

{% macro render_diff_and_number(balance, cost, currency, invert=False) %}
  {% set num = balance.pop(currency, 0) %}
  {% set num2 = -num if invert else num %}
  <span class="number">{{ num2|format_currency(currency) }}</span>
  {% if currency in cost %}
    {% set cost_num = cost.pop(currency, 0) %}
    {% set diff = num - cost_num %}
    {% if invert %}
      {% set diff = -diff %}
    {% endif %}
    {%- if diff -%}
    <br>
    <span class="diff{{ ' positive' if diff > 0 else ' negative' }}" title="{{ cost_num|format_currency(currency, invert=invert) }} {{ currency }}">({{ diff|format_currency(currency) }})</span>
    {%- endif -%}
  {%- endif -%}
{%- endmacro %}

{% macro tree(account_node, invert=False, ledger=None) %}
{% set ledger = ledger or g.ledger %}
<tree-table>
  <ol class="flex-table tree-table{{ ' two-currencies' if ledger.options.operating_currency|length > 1 else '' }}" title="{{ _('Hold Shift while clicking to expand all children.\nHold Ctrl or Cmd while clicking to expand one level.') }}">
    <li class="head">
      <p>
      <span class="account-cell"><button type="button" class="link expand-all hidden" title="{{ _('Expand all accounts') }}">{{ _('Expand all') }}</button></span>
      {% for currency in ledger.options.operating_currency %}
      <span class="num">{{ currency }}</span>
      {% endfor %}
      <span class="num other">{{ _('Other') }}</span>
      </p>
    </li>
    {% set end_date = g.filtered.end_date %}
    {% for account in ([account_node] if account_node.name else account_node.children) if extension.should_show(account) recursive %}
    {% set balance = extension.cost_or_value(account.balance, end_date) %}
    {% set balance_children = extension.cost_or_value(account.balance_children, end_date) %}
    {% set cost = extension.cost(account.balance) if g.conversion == 'at_value' else {} %}
    {% set cost_children = extension.cost(account.balance_children) if g.conversion == 'at_value' else {} %}
    <li{{ ' class=toggled' if extension.collapse_account(account.name) else '' }}>
      <p{{ ' class=has-balance' if not balance.is_empty() else '' }}>
      <span class="account-cell depth-{{ loop.depth0 }} droptarget{{ ' has-children' if account.children else '' }}" data-account-name="{{ account.name }}">
        {{ account_name(ledger, account.name) }}
      </span>
      {% for currency in ledger.options.operating_currency %}
      <span class="num">
        <span class="balance">{{ render_diff_and_number(balance, cost, currency, invert=invert) }}</span>
        <span class="balance-children">{{ render_diff_and_number(balance_children, cost_children, currency, invert=invert) }}</span>
      </span>
      {% endfor %}
      <span class="num other">
        <span class="balance">
          {% for currency in balance.keys()|sort %}
            {{ render_diff_and_number(balance, cost, currency, invert=invert) }} {{ render_currency(ledger, currency) }}<br>
          {% endfor %}
        </span>
        <span class="balance-children">
          {% for currency in balance_children.keys()|sort %}
            {{ render_diff_and_number(balance_children, cost_children, currency, invert=invert) }} {{ render_currency(ledger, currency) }}<br>
          {% endfor %}
        </span>
      </span>
      </p>
      {% if account.children %}
  <ol>{{- loop(account.children|sort(attribute='name')) -}} </ol>
      {% endif %}
    </li>
    {% endfor %}
  </ol>
</tree-table>
{% endmacro %}

And because many of the filters referenced in the macro has been removed, I have also had to add the following methods into my extension class to be used by the template:

__init__.py
    def should_show(self, account: TreeNode) -> bool:
        """Determine whether the account should be shown."""
        from fava.context import g
        if not account.balance_children.is_empty() or any(
            self.should_show(a) for a in account.children
        ):
            return True
        ledger = g.ledger
        filtered = g.filtered
        if account.name not in ledger.accounts:
            return False
        fava_options = ledger.fava_options
        if not fava_options.show_closed_accounts and filtered.account_is_closed(
            account.name,
        ):
            return False
        if (
            not fava_options.show_accounts_with_zero_balance
            and account.balance.is_empty()
        ):
            return False
        if (
            not fava_options.show_accounts_with_zero_transactions
            and not account.has_txns
        ):
            return False
        return True

    def collapse_account(self, account_name: str) -> bool:
        """Return true if account should be collapsed."""
        from fava.context import g
        collapse_patterns = g.ledger.fava_options.collapse_pattern
        return any(pattern.match(account_name) for pattern in collapse_patterns)

    def cost(self, inventory: CounterInventory) -> SimpleCounterInventory:
        """Get the cost of an inventory."""
        return inventory.reduce(get_cost)

    def cost_or_value(
        self,
        inventory: CounterInventory,
        date: date | None = None,
    ) -> SimpleCounterInventory:
        """Get the cost or value of an inventory."""
        from fava.context import g
        return cost_or_value(inventory, g.conversion, g.ledger.prices, date)

Hope this would be useful.

But it is definitely better if we can just use the same tree table used in the builtin report.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
extension Features or enhancements that have Fava extensions or could be implemented as one.
Projects
None yet
Development

No branches or pull requests

3 participants