1
0
Fork 0
mirror of https://github.com/maybe-finance/maybe.git synced 2025-08-09 07:25:19 +02:00

Persistent account tab selections

This commit is contained in:
Zach Gollwitzer 2025-05-14 11:42:17 -04:00
parent db02718d71
commit c83aa10f01
20 changed files with 86 additions and 58 deletions

View file

@ -1,6 +1,7 @@
<%= tag.div data: { <%= tag.div data: {
controller: "tabs", controller: "tabs",
testid: testid, testid: testid,
tabs_session_key_value: session_key,
tabs_url_param_key_value: url_param_key, tabs_url_param_key_value: url_param_key,
tabs_nav_btn_active_class: active_btn_classes, tabs_nav_btn_active_class: active_btn_classes,
tabs_nav_btn_inactive_class: inactive_btn_classes tabs_nav_btn_inactive_class: inactive_btn_classes
@ -10,6 +11,11 @@
<% else %> <% else %>
<%= nav %> <%= nav %>
<%= form_with url: cookie_session_path, scope: :cookie_session, method: :put, data: { tabs_target: "persistSelectionForm" } do |f| %>
<%= f.hidden_field :tab_key, value: "account_group_tab" %>
<%= f.hidden_field :tab_value, value: "all", data: { tabs_target: "selectionInput" } %>
<% end %>
<% panels.each do |panel| %> <% panels.each do |panel| %>
<%= panel %> <%= panel %>
<% end %> <% end %>

View file

@ -27,11 +27,12 @@ class TabsComponent < ViewComponent::Base
} }
} }
attr_reader :active_tab, :url_param_key, :variant, :testid attr_reader :active_tab, :url_param_key, :session_key, :variant, :testid
def initialize(active_tab:, url_param_key: nil, variant: :default, active_btn_classes: "", inactive_btn_classes: "", testid: nil) def initialize(active_tab:, url_param_key: nil, session_key: nil, variant: :default, active_btn_classes: "", inactive_btn_classes: "", testid: nil)
@active_tab = active_tab @active_tab = active_tab
@url_param_key = url_param_key @url_param_key = url_param_key
@session_key = session_key
@variant = variant.to_sym @variant = variant.to_sym
@active_btn_classes = active_btn_classes @active_btn_classes = active_btn_classes
@inactive_btn_classes = inactive_btn_classes @inactive_btn_classes = inactive_btn_classes

View file

@ -3,8 +3,8 @@ import { Controller } from "@hotwired/stimulus";
// Connects to data-controller="tabs--components" // Connects to data-controller="tabs--components"
export default class extends Controller { export default class extends Controller {
static classes = ["navBtnActive", "navBtnInactive"]; static classes = ["navBtnActive", "navBtnInactive"];
static targets = ["panel", "navBtn"]; static targets = ["panel", "navBtn", "persistSelectionForm", "selectionInput"];
static values = { urlParamKey: String }; static values = { sessionKey: String, urlParamKey: String };
show(e) { show(e) {
const btn = e.target.closest("button"); const btn = e.target.closest("button");
@ -28,11 +28,16 @@ export default class extends Controller {
} }
}); });
// Update URL with the selected tab
if (this.urlParamKeyValue) { if (this.urlParamKeyValue) {
const url = new URL(window.location.href); const url = new URL(window.location.href);
url.searchParams.set(this.urlParamKeyValue, selectedTabId); url.searchParams.set(this.urlParamKeyValue, selectedTabId);
window.history.replaceState({}, "", url); window.history.replaceState({}, "", url);
} }
// Update URL with the selected tab
if (this.sessionKeyValue) {
this.selectionInputTarget.value = selectedTabId;
this.persistSelectionFormTarget.requestSubmit();
}
} }
} }

View file

@ -5,40 +5,20 @@ module AccountGroupable
before_action :set_account_group_tab before_action :set_account_group_tab
end end
def set_account_group_tab
last_selected_tab = session[:account_group_tab] || "asset"
selected_tab = if account_group_tab_param
account_group_tab_param
elsif on_asset_page?
"asset"
elsif on_liability_page?
"liability"
else
last_selected_tab
end
session[:account_group_tab] = selected_tab
@account_group_tab = selected_tab
end
private private
def set_account_group_tab
last_selected_tab = session["custom_account_group_tab"] || "asset"
@account_group_tab = account_group_tab_param || last_selected_tab
end
def valid_account_group_tabs
%w[asset liability all]
end
def account_group_tab_param def account_group_tab_param
valid_tabs = %w[asset liability all] param_value = params[:account_group_tab]
params[:account_group_tab].in?(valid_tabs) ? params[:account_group_tab] : nil return nil unless param_value.in?(valid_account_group_tabs)
end param_value
def on_asset_page?
accountable_controller_names_for("asset").include?(controller_name)
end
def on_liability_page?
accountable_controller_names_for("liability").include?(controller_name)
end
def accountable_controller_names_for(classification)
Accountable::TYPES.map(&:constantize)
.select { |a| a.classification == classification }
.map { |a| a.model_name.plural }
end end
end end

View file

@ -0,0 +1,22 @@
class CookieSessionsController < ApplicationController
def update
save_kv_to_session(
cookie_session_params[:tab_key],
cookie_session_params[:tab_value]
)
redirect_back_or_to root_path
end
private
def cookie_session_params
params.require(:cookie_session).permit(:tab_key, :tab_value)
end
def save_kv_to_session(key, value)
raise "Key must be a string" unless key.is_a?(String)
raise "Value must be a string" unless value.is_a?(String)
session["custom_#{key}"] = value
end
end

View file

@ -31,4 +31,8 @@ class SessionsController < ApplicationController
def set_session def set_session
@session = Current.user.sessions.find(params[:id]) @session = Current.user.sessions.find(params[:id])
end end
def session_params
params.require(:session).permit(:tab_key, :tab_value)
end
end end

View file

@ -87,7 +87,7 @@ class UsersController < ApplicationController
def user_params def user_params
params.require(:user).permit( params.require(:user).permit(
:first_name, :last_name, :email, :profile_image, :redirect_to, :delete_profile_image, :onboarded_at, :first_name, :last_name, :email, :profile_image, :redirect_to, :delete_profile_image, :onboarded_at, :preferred_account_group_tab,
:show_sidebar, :default_period, :show_ai_sidebar, :ai_enabled, :theme, :set_onboarding_preferences_at, :set_onboarding_goals_at, :show_sidebar, :default_period, :show_ai_sidebar, :ai_enabled, :theme, :set_onboarding_preferences_at, :set_onboarding_goals_at,
family_attributes: [ :name, :currency, :country, :locale, :date_format, :timezone, :id ], family_attributes: [ :name, :currency, :country, :locale, :date_format, :timezone, :id ],
goals: [] goals: []

View file

@ -48,9 +48,10 @@ class BalanceSheet
def account_groups(classification = nil) def account_groups(classification = nil)
classification_accounts = classification ? totals_query.filter { |t| t.classification == classification } : totals_query classification_accounts = classification ? totals_query.filter { |t| t.classification == classification } : totals_query
classification_total = classification_accounts.sum(&:converted_balance) classification_total = classification_accounts.sum(&:converted_balance)
account_groups = classification_accounts.group_by(&:accountable_type).transform_keys { |k| Accountable.from_type(k) } account_groups = classification_accounts.group_by(&:accountable_type)
.transform_keys { |k| Accountable.from_type(k) }
account_groups.map do |accountable, accounts| groups = account_groups.map do |accountable, accounts|
group_total = accounts.sum(&:converted_balance) group_total = accounts.sum(&:converted_balance)
AccountGroup.new( AccountGroup.new(
@ -71,7 +72,13 @@ class BalanceSheet
account account
end.sort_by(&:weight).reverse end.sort_by(&:weight).reverse
) )
end.sort_by(&:weight).reverse end
groups.sort_by do |group|
manual_order = Accountable::TYPES
type_name = group.key.camelize
manual_order.index(type_name) || Float::INFINITY
end
end end
def net_worth_series(period: Period.last_30_days) def net_worth_series(period: Period.last_30_days)

View file

@ -21,7 +21,7 @@
</details> </details>
<% end %> <% end %>
<%= render TabsComponent.new(active_tab: active_account_group_tab, testid: "account-sidebar-tabs") do |tabs| %> <%= render TabsComponent.new(active_tab: active_account_group_tab, session_key: "account_group_tab", testid: "account-sidebar-tabs") do |tabs| %>
<% tabs.with_nav do |nav| %> <% tabs.with_nav do |nav| %>
<% nav.with_btn(id: "asset", label: "Assets") %> <% nav.with_btn(id: "asset", label: "Assets") %>
<% nav.with_btn(id: "liability", label: "Debts") %> <% nav.with_btn(id: "liability", label: "Debts") %>

View file

@ -2,7 +2,7 @@
<% trend = series.trend %> <% trend = series.trend %>
<%= turbo_frame_tag dom_id(@account, :chart_details) do %> <%= turbo_frame_tag dom_id(@account, :chart_details) do %>
<% if @account.syncing?%> <% if @account.syncing? %>
<%= render "accounts/chart_loader" %> <%= render "accounts/chart_loader" %>
<% else %> <% else %>
<div class="px-4"> <div class="px-4">

View file

@ -26,7 +26,7 @@
<%= render( <%= render(
"accounts/account_sidebar_tabs", "accounts/account_sidebar_tabs",
family: Current.family, family: Current.family,
active_account_group_tab: params[:account_group_tab] || "assets" active_account_group_tab: @account_group_tab
) %> ) %>
</div> </div>
@ -81,7 +81,7 @@
<% else %> <% else %>
<div class="h-full flex flex-col"> <div class="h-full flex flex-col">
<div class="overflow-y-auto grow"> <div class="overflow-y-auto grow">
<%= render "accounts/account_sidebar_tabs", family: Current.family, active_account_group_tab: params[:account_group_tab] || "assets" %> <%= render "accounts/account_sidebar_tabs", family: Current.family, active_account_group_tab: @account_group_tab %>
</div> </div>
<% if Current.family.trialing? && !self_hosted? %> <% if Current.family.trialing? && !self_hosted? %>

View file

@ -42,7 +42,7 @@
</div> </div>
<% end %> <% end %>
</div> </div>
<% end%> <% end %>
</div> </div>
<div class="bg-surface rounded-xl p-1 space-y-1 overflow-x-auto"> <div class="bg-surface rounded-xl p-1 space-y-1 overflow-x-auto">

View file

@ -1,5 +1,5 @@
<%# locals: (rule:) %> <%# locals: (rule:) %>
<div class="flex justify-between items-center p-4 <%= rule.active? ? 'text-primary' : 'text-secondary' %>"> <div class="flex justify-between items-center p-4 <%= rule.active? ? "text-primary" : "text-secondary" %>">
<div class="text-sm space-y-1.5"> <div class="text-sm space-y-1.5">
<% if rule.name.present? %> <% if rule.name.present? %>
<h3 class="font-medium text-md"><%= rule.name %></h3> <h3 class="font-medium text-md"><%= rule.name %></h3>
@ -49,7 +49,7 @@
<% if rule.effective_date.nil? %> <% if rule.effective_date.nil? %>
All past and future <%= rule.resource_type.pluralize %> All past and future <%= rule.resource_type.pluralize %>
<% else %> <% else %>
<%= rule.resource_type.pluralize %> on or after <%= rule.effective_date.strftime('%b %-d, %Y') %> <%= rule.resource_type.pluralize %> on or after <%= rule.effective_date.strftime("%b %-d, %Y") %>
<% end %> <% end %>
</span> </span>
</p> </p>

View file

@ -60,7 +60,7 @@
<div class="p-1"> <div class="p-1">
<div class="flex flex-col bg-container rounded-xl shadow-border-xs first_child:rounded-t-xl last_child:rounded-b-xl"> <div class="flex flex-col bg-container rounded-xl shadow-border-xs first_child:rounded-t-xl last_child:rounded-b-xl">
<% @rules.each_with_index do |rule, idx| %> <% @rules.each_with_index do |rule, idx| %>
<%= render "rule", rule: rule%> <%= render "rule", rule: rule %>
<% unless idx == @rules.size - 1 %> <% unless idx == @rules.size - 1 %>
<div class="h-px bg-divider ml-4 mr-6"></div> <div class="h-px bg-divider ml-4 mr-6"></div>
<% end %> <% end %>

View file

@ -26,6 +26,8 @@ Rails.application.routes.draw do
get "changelog", to: "pages#changelog" get "changelog", to: "pages#changelog"
get "feedback", to: "pages#feedback" get "feedback", to: "pages#feedback"
resource :cookie_session, only: %i[update]
resource :registration, only: %i[new create] resource :registration, only: %i[new create]
resources :sessions, only: %i[new create destroy] resources :sessions, only: %i[new create destroy]
resource :password_reset, only: %i[new create edit update] resource :password_reset, only: %i[new create edit update]

3
db/schema.rb generated
View file

@ -10,7 +10,7 @@
# #
# It's strongly recommended that you check this file into your version control system. # It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema[7.2].define(version: 2025_05_13_122703) do ActiveRecord::Schema[7.2].define(version: 2025_05_14_143017) do
# These are extensions that must be enabled in order to support this database # These are extensions that must be enabled in order to support this database
enable_extension "pgcrypto" enable_extension "pgcrypto"
enable_extension "plpgsql" enable_extension "plpgsql"
@ -696,6 +696,7 @@ ActiveRecord::Schema[7.2].define(version: 2025_05_13_122703) do
t.text "goals", default: [], array: true t.text "goals", default: [], array: true
t.datetime "set_onboarding_preferences_at" t.datetime "set_onboarding_preferences_at"
t.datetime "set_onboarding_goals_at" t.datetime "set_onboarding_goals_at"
t.string "preferred_account_group_tab", default: "asset"
t.index ["email"], name: "index_users_on_email", unique: true t.index ["email"], name: "index_users_on_email", unique: true
t.index ["family_id"], name: "index_users_on_family_id" t.index ["family_id"], name: "index_users_on_family_id"
t.index ["last_viewed_chat_id"], name: "index_users_on_last_viewed_chat_id" t.index ["last_viewed_chat_id"], name: "index_users_on_last_viewed_chat_id"