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

Move category dropdown menu content into a turbo frame #782

Merged
merged 7 commits into from
May 22, 2024

Conversation

jakubkottnauer
Copy link
Contributor

This PR fixes (or at least improves by a large margin) the performance issues reported in #739.

Each transaction row comes with a huge amount of HTML representing the contents of the category dropdown, 99% of which will never viewed by the user. Moving the dropdown contents into a lazily loaded turbo frame makes an account page with 1k transaction about 20x smaller (2.5 MB instead of 51 MB).

Before
before

After
after

After this change there is a slight delay when you click a category badge before the dropdown contents are loaded, but it seems ok with a reasonable amount of categories. This can be further improved in the future by prefetching the dropdown contents when the badge is hovered.

@jakubkottnauer jakubkottnauer marked this pull request as draft May 20, 2024 20:35
@jakubkottnauer jakubkottnauer marked this pull request as ready for review May 20, 2024 20:38
@zachgoll
Copy link
Collaborator

zachgoll commented May 20, 2024

@jakubkottnauer nice idea on the lazy loaded turbo frame, I definitely agree that we should be utilizing that for this scenario.

I don't mind the slight delay, but I think we could probably add some sort of "skeleton" loader, maybe something like this?

<div class="p-6 flex items-center justify-center">
  <p class="text-sm text-gray-500 animate-pulse">Loading...</p>
</div>

Also, the category_menu_content_transaction path feels a little off to me. What about something like this?

scope module: :transactions do
        resources :categories, as: :transaction_categories do
          
          # Singular resource, resolves to `new_transaction_category_dropdown`
          resource :dropdown, only: %i[ new ], module: :categories
          
        end
end

@jakubkottnauer jakubkottnauer force-pushed the category-menu-frame branch 2 times, most recently from be740f8 to c9f30f7 Compare May 21, 2024 06:00
@jakubkottnauer
Copy link
Contributor Author

jakubkottnauer commented May 21, 2024

@zachgoll Thank you for the review! Though I ended up defining the route differently; your suggestion results in path /transactions/categories/:transaction_category_id/dropdown/new whereas we need something more like /transactions/:id/categories/dropdown (the dropdown needs a transaction ID in order to get the transaction's current category and mark it as selected in the UI).

Let me know if there's a nicer way to define such a route, I'm still quite new to RoR :)

@zachgoll
Copy link
Collaborator

zachgoll commented May 21, 2024

@jakubkottnauer let me start by saying that the following suggestions are not necessarily 100% related to this specific PR, so if you'd like to get this change in and ignore these for now, I'm fine with that. Just let me know and I'll get it merged.

But I think why we're having such a hard time with this one is because our existing routing structure for transactions and categories doesn't fit the "singular" nature of a transaction category (i.e. a transaction can have a max of 1 category). We've currently got it setup like so:

resources :transactions do
  match "search" => "transactions#search", on: :collection, via: %i[ get post ], as: :search

  collection do
    scope module: :transactions do
      resources :categories, as: :transaction_categories do
        resources :deletions, only: %i[ new create ], module: :categories
      end

      resources :rules, only: %i[ index ], as: :transaction_rules
      resources :merchants, only: %i[ index new create edit update destroy ], as: :transaction_merchants
    end
  end
end

If I'm not mistaken here, I think a better way to express all of this would be:

resources :transactions do
  match "search" => "transactions#search", on: :collection, via: %i[ get post ], as: :search

  resources :rules, only: %i[ index ], module: :transactions
  resources :merchants, only: %i[ index new create edit update destroy ], module: :transactions

  resource :category, module: :transactions do
    resource :dropdown, only: %i[ new ], module: :categories
    resource :deletion, only: %i[ new create ], module: :categories
  end
end

The routes this produces are fairly similar to what we have already, but feel a little more natural to me and expose the transaction ID that we need by default:

CleanShot 2024-05-21 at 10 33 23

To implement this we'd need to rename a few folders, but IMO this would be a good improvement to what we currently have.

Would be happy to hear other ideas and critiques to this.


We'd also need a separate routing structure to independently manage these categories (i.e. in the Settings view), but I think that would be okay to have both the general editing and transaction-specific editing?

@zachgoll
Copy link
Collaborator

@josefarias no worries if you're busy with other things, but curious to hear if you'd agree with the comment above?

@zachgoll
Copy link
Collaborator

@jakubkottnauer @josefarias disregard my comment above about the singular resources. I wasn't reading things correctly before and see why we need the "collection" (to edit these outside the context of a transaction).

Wondering if we just add this above the existing route structure?

  resources :transactions do
    
    # new_transaction_category_dropdown -> /transactions/:transaction_id/category/dropdown/new
    namespace :category, module: :transactions do
      resource :dropdown, only: %i[ new ], module: :categories
    end
    
    ... existing routes
end

@josefarias
Copy link
Contributor

Hey @zachgoll @jakubkottnauer, great work here!

Here's how I'd suggest modeling this:

diff --git a/config/routes.rb b/config/routes.rb
index 909870a..b4ff237 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -44,6 +44,10 @@ Rails.application.routes.draw do
       scope module: :transactions do
         resources :categories, as: :transaction_categories do
           resources :deletions, only: %i[ new create ], module: :categories
+
+          collection do
+            resource :dropdown, only: :show, module: :categories, as: :transaction_categories_dropdown
+          end
         end
 
         resources :rules, only: %i[ index ], as: :transaction_rules

Could also do as: :transaction_category_dropdown if that's more natural in english, I'm not sure!

Here's the reasoning behind that:

  1. The dropdown needs to be aware of which category is selected. We can do this via the transaction, yes. But we're not rendering "the categories for a transaction". Instead, we're rendering "all categories, but put this transaction's category at the top"
    1. My suggestion is to do something like transaction_categories_dropdown_path(selection_id: @transaction.category_id)
    2. That way we're using the same path for all transactions, which is easy to cache down the line if needed — but independently, it's also more semantically accurate. Again, these categories are not transaction-dependent. They're a resource by themselves. And we render the same ones every time, for every transaction. Which one goes at the top is a presentation concern and should be free to change if e.g. we ever decide not to put the selected one at the top.
  2. Using show instead of new here because the dropdown is a resource we won't be calling create on. It's an entity that already exists. If anything, we'd call update/destroy on it — but even that doesn't make much sense in this context.
    1. That said, I don't feel strongly about this. Could go with :new, no problem.

As far as rendering the actual dropdown, I'd do something like this in the controller (pseudocode, not tested):

class Transactions::Categories::DropdownsController < ApplicationController
  before_action :set_selected_category

  def show
    @categories = categories_scope.excluding(@selected_category).prepend(@selected_category).compact
  end

  private
    def set_selected_category
      if params[:selection_id]
        @selected_category = categories_scope.find(params[:selection_id])
      end
    end

    def categories_scope
      Current.family.transaction_categories.alphabetically
    end
end

@zachgoll
Copy link
Collaborator

zachgoll commented May 21, 2024

@josefarias appreciate the help here!

I think what you've outlined makes sense and I'd agree that show is probably a better action to use.

The helper name you came up with makes sense to me as well. As I was playing around with the routes, came up with the following, which attempts to leverage auto-generated path helper names a bit more:

resources :transactions do
  collection do
    match "search" => "transactions#search", via: %i[ get post ]

    scope module: :transactions, as: :transaction do
      resources :categories do
        resources :deletions, only: %i[ new create ], module: :categories
        resource :dropdown, only: :show, on: :collection, module: :categories
      end

      resources :rules, only: %i[ index ]
      resources :merchants, only: %i[ index new create edit update destroy ]
    end
  end
end

The only change that would be required to get tests passing again is update the set_category method in DeletionsController to use the param :category_id rather than :transaction_category_id. From a resource perspective, I think :category_id makes sense given the independent nature of categories as we've discussed above?

Any objections to this?

@josefarias
Copy link
Contributor

No objections! I like that better as well

@zachgoll
Copy link
Collaborator

zachgoll commented May 21, 2024

@josefarias awesome, thanks again for the assist here!

@jakubkottnauer if you're good with the discussion and proposed changes above, I think we've got a path forward? Looks like you've got all the pieces here, just need a slight rework of the routing and the update to DeletionsController I had mentioned above.

@jakubkottnauer
Copy link
Contributor Author

jakubkottnauer commented May 21, 2024

@josefarias @zachgoll Thank you both for the great suggestions, I think we've gotten something really nice out of it 👍 I have implemented the changes. I ended up doing

collection do
  resource :dropdown, only: :show, module: :categories, as: :category_dropdown
end
# -> /transactions/categories/dropdown

instead of

resource :dropdown, only: :show, on: :collection, module: :categories
# -> /transactions/categories/:category_id/dropdown

because the latter doesn't work when no category is selected on a transaction. I'm a bit surprised that just wrapping the resource with collection do instead of putting on: :collection directly on the resource has this kind of impact on the generated route.

Copy link
Collaborator

@zachgoll zachgoll left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good! Thanks for the iterations on this.

And to your question about the collection block—that's my bad, resource and resources do not accept an on option. It was just silently failing there. So I think your solution is the right one!

db/schema.rb Outdated
@@ -87,7 +87,7 @@
t.uuid "accountable_id"
t.decimal "balance", precision: 19, scale: 4, default: "0.0"
t.string "currency", default: "USD"
t.virtual "classification", type: :string, as: "\nCASE\n WHEN ((accountable_type)::text = ANY ((ARRAY['Account::Loan'::character varying, 'Account::Credit'::character varying, 'Account::OtherLiability'::character varying])::text[])) THEN 'liability'::text\n ELSE 'asset'::text\nEND", stored: true
t.virtual "classification", type: :string, as: "\nCASE\n WHEN ((accountable_type)::text = ANY (ARRAY[('Account::Loan'::character varying)::text, ('Account::Credit'::character varying)::text, ('Account::OtherLiability'::character varying)::text])) THEN 'liability'::text\n ELSE 'asset'::text\nEND", stored: true
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You had updated to v16.x right? Maybe this isn't related to the PG version after all.

Copy link
Contributor Author

@jakubkottnauer jakubkottnauer May 22, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My bad, I haven't noticed this has snuck in again, reverted. But yes, I had upgraded to 16.3 and still get this schema change when I run bin/setup

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interesting, I'll have to look into this again and see if there's a way to avoid in the future.

@zachgoll zachgoll merged commit ac27a1c into maybe-finance:main May 22, 2024
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants