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

New Settings Menu, Routes and Controllers Organization (#641)

* Add new settings routes and controllers

* Add new settings view, restructure controllers and routes

* Fix lint errors
This commit is contained in:
Zach Gollwitzer 2024-04-18 07:56:51 -04:00 committed by GitHub
parent 39d57a167e
commit 9bda7efc3f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
52 changed files with 771 additions and 203 deletions

View file

@ -2,6 +2,18 @@ class AccountsController < ApplicationController
include Filterable
before_action :set_account, only: %i[ show update destroy sync ]
def index
@accounts = Current.family.accounts
end
def summary
snapshot = Current.family.snapshot(@period)
@net_worth_series = snapshot[:net_worth_series]
@asset_series = snapshot[:asset_series]
@liability_series = snapshot[:liability_series]
@account_groups = Current.family.accounts.by_group(period: @period, currency: Current.family.currency)
end
def new
@account = Account.new(
balance: nil,

View file

@ -8,4 +8,13 @@ class PagesController < ApplicationController
@liability_series = snapshot[:liability_series]
@account_groups = Current.family.accounts.by_group(period: @period, currency: Current.family.currency)
end
def changelog
end
def feedback
end
def invites
end
end

View file

@ -0,0 +1,7 @@
class Settings::BillingsController < ApplicationController
def edit
end
def update
end
end

View file

@ -0,0 +1,46 @@
class Settings::HostingsController < ApplicationController
before_action :verify_hosting_mode
def show
end
def update
if all_updates_valid?
hosting_params.keys.each do |key|
Setting.send("#{key}=", hosting_params[key].strip)
end
redirect_to settings_hosting_path, notice: t(".success")
else
flash.now[:error] = @errors.first.message
render :edit, status: :unprocessable_entity
end
end
private
def all_updates_valid?
@errors = ActiveModel::Errors.new(Setting)
hosting_params.keys.each do |key|
setting = Setting.new(var: key)
setting.value = hosting_params[key].strip
unless setting.valid?
@errors.merge!(setting.errors)
end
end
if hosting_params[:upgrades_mode] == "auto" && hosting_params[:render_deploy_hook].blank?
@errors.add(:render_deploy_hook, t("settings.hostings.update.render_deploy_hook_error"))
end
@errors.empty?
end
def hosting_params
params.require(:setting).permit(:render_deploy_hook, :upgrades_mode, :upgrades_target)
end
def verify_hosting_mode
head :not_found unless self_hosted?
end
end

View file

@ -0,0 +1,7 @@
class Settings::NotificationsController < ApplicationController
def edit
end
def update
end
end

View file

@ -0,0 +1,7 @@
class Settings::PreferencesController < ApplicationController
def edit
end
def update
end
end

View file

@ -1,5 +1,5 @@
class SettingsController < ApplicationController
def edit
class Settings::ProfilesController < ApplicationController
def show
end
def update

View file

@ -0,0 +1,7 @@
class Settings::SecuritiesController < ApplicationController
def edit
end
def update
end
end

View file

@ -1,46 +0,0 @@
class Settings::SelfHostingController < ApplicationController
before_action :verify_self_hosting_enabled
def edit
end
def update
if all_updates_valid?
self_hosting_params.keys.each do |key|
Setting.send("#{key}=", self_hosting_params[key].strip)
end
redirect_to edit_settings_self_hosting_path, notice: t(".success")
else
flash.now[:error] = @errors.first.message
render :edit, status: :unprocessable_entity
end
end
private
def all_updates_valid?
@errors = ActiveModel::Errors.new(Setting)
self_hosting_params.keys.each do |key|
setting = Setting.new(var: key)
setting.value = self_hosting_params[key].strip
unless setting.valid?
@errors.merge!(setting.errors)
end
end
if self_hosting_params[:upgrades_mode] == "auto" && self_hosting_params[:render_deploy_hook].blank?
@errors.add(:render_deploy_hook, t("settings.self_hosting.update.render_deploy_hook_error"))
end
@errors.empty?
end
def self_hosting_params
params.require(:setting).permit(:render_deploy_hook, :upgrades_mode, :upgrades_target)
end
def verify_self_hosting_enabled
head :not_found unless self_hosted?
end
end

View file

@ -1,6 +1,9 @@
class Transactions::CategoriesController < ApplicationController
before_action :set_category, only: [ :update, :destroy ]
def index
end
def create
if Current.family.transaction_categories.create(category_params)
redirect_to transactions_path, notice: t(".success")

View file

@ -0,0 +1,4 @@
class Transactions::MerchantsController < ApplicationController
def index
end
end

View file

@ -0,0 +1,4 @@
class Transactions::RulesController < ApplicationController
def index
end
end

View file

@ -38,21 +38,16 @@ module ApplicationHelper
end
def sidebar_link_to(name, path, options = {})
base_class_names = [ "block", "border", "border-transparent", "rounded-xl", "-ml-2", "p-2", "text-sm", "font-medium", "text-gray-500", "flex", "items-center" ]
hover_class_names = [ "hover:bg-white", "hover:border-alpha-black-50", "hover:text-gray-900", "hover:shadow-xs" ]
current_page_class_names = [ "bg-white", "border-alpha-black-50", "text-gray-900", "shadow-xs" ]
is_current = current_page?(path) || (request.path.start_with?(path) && path != "/")
link_class_names = if current_page?(path) || (request.path.start_with?(path) && path != "/")
base_class_names.delete("border-transparent")
base_class_names + hover_class_names + current_page_class_names
else
base_class_names + hover_class_names
end
classes = [
"flex items-center gap-2 px-3 py-2 rounded-xl border text-sm font-medium text-gray-500",
(is_current ? "bg-white text-gray-900 shadow-xs border-alpha-black-50" : "hover:bg-gray-100 border-transparent")
].compact.join(" ")
merged_options = options.reverse_merge(class: link_class_names.join(" ")).except(:icon)
link_to path, merged_options do
lucide_icon(options[:icon], class: "w-5 h-5 mr-2") + name
link_to path, **options.merge(class: classes), aria: { current: ("page" if current_page?(path)) } do
concat(lucide_icon(options[:icon], class: "w-5 h-5")) if options[:icon]
concat(name)
end
end

View file

@ -0,0 +1,2 @@
module Settings::HostingHelper
end

View file

@ -1,2 +0,0 @@
module Settings::SelfHostingHelper
end

View file

@ -0,0 +1,9 @@
module SettingsHelper
def next_setting(title, path)
render partial: "settings/nav_link_large", locals: { path: path, direction: "next", title: title }
end
def previous_setting(title, path)
render partial: "settings/nav_link_large", locals: { path: path, direction: "previous", title: title }
end
end

View file

@ -1,13 +1,17 @@
import { Controller } from "@hotwired/stimulus"
import { install, uninstall } from "@github/hotkey"
import { Controller } from "@hotwired/stimulus";
import { install, uninstall } from "@github/hotkey";
// Connects to data-controller="hotkey"
export default class extends Controller {
connect() {
install(this.element)
install(this.element);
}
disconnect() {
uninstall(this.element)
uninstall(this.element);
}
navigateBack(event) {
window.history.back();
}
}

View file

@ -38,17 +38,17 @@ export default {
900: "rgba(255, 255, 255, 0.7)",
},
"alpha-black": {
25: "rgba(20, 20, 20, 0.03)",
50: "rgba(20, 20, 20, 0.05)",
100: "rgba(20, 20, 20, 0.08)",
200: "rgba(20, 20, 20, 0.1)",
300: "rgba(20, 20, 20, 0.15)",
400: "rgba(20, 20, 20, 0.2)",
500: "rgba(20, 20, 20, 0.3)",
600: "rgba(20, 20, 20, 0.4)",
700: "rgba(20, 20, 20, 0.5)",
800: "rgba(20, 20, 20, 0.6)",
900: "rgba(20, 20, 20, 0.7)",
25: "rgba(11, 11, 11, 0.03)",
50: "rgba(11, 11, 11, 0.05)",
100: "rgba(11, 11, 11, 0.08)",
200: "rgba(11, 11, 11, 0.1)",
300: "rgba(11, 11, 11, 0.15)",
400: "rgba(11, 11, 11, 0.2)",
500: "rgba(11, 11, 11, 0.3)",
600: "rgba(11, 11, 11, 0.4)",
700: "rgba(11, 11, 11, 0.5)",
800: "rgba(11, 11, 11, 0.6)",
900: "rgba(11, 11, 11, 0.7)",
},
red: {
25: "#FFFBFB",

View file

@ -1,3 +1,6 @@
<% content_for :sidebar do %>
<%= render "settings/nav" %>
<% end %>
<div class="space-y-4">
<div class="flex items-center justify-between">
<h1 class="text-xl font-medium text-gray-900">Accounts</h1>
@ -6,7 +9,7 @@
<span><%= t(".new_account") %></span>
<% end %>
</div>
<% if Current.family.accounts.empty? %>
<% if @accounts.empty? %>
<div class="flex justify-center items-center h-[800px] text-sm">
<div class="text-center flex flex-col items-center max-w-[300px]">
<p class="text-gray-900 mb-1 font-medium">No accounts yet</p>
@ -19,8 +22,8 @@
</div>
<% else %>
<div>
<% Current.family.accounts.by_provider.each do |item| %>
<details class="bg-white group p-4 border border-alpha-black-25 shadow-xs rounded-xl">
<% @accounts.by_provider.each do |item| %>
<details open class="bg-white group p-4 border border-alpha-black-25 shadow-xs rounded-xl">
<summary class="flex items-center gap-2">
<%= lucide_icon("chevron-down", class: "hidden group-open:block w-5 h-5 text-gray-500") %>
<%= lucide_icon("chevron-right", class: "group-open:hidden w-5 h-5 text-gray-500") %>
@ -54,4 +57,12 @@
<% end %>
</div>
<% end %>
<div class="flex justify-between gap-4">
<% if self_hosted? %>
<%= previous_setting("Self Hosting", settings_hosting_path) %>
<% else %>
<%= previous_setting("Billing", settings_billing_path) %>
<% end %>
<%= next_setting("Categories", transactions_categories_path) %>
</div>
</div>

View file

@ -0,0 +1,76 @@
<div class="space-y-4">
<div class="flex items-center justify-between">
<h1 class="text-xl font-medium text-gray-900">Accounts</h1>
<%= link_to new_account_path, class: "flex text-white text-sm font-medium items-center gap-1 bg-gray-900 rounded-lg p-2", data: { turbo_frame: "modal" } do %>
<%= lucide_icon("plus", class: "w-5 h-5") %>
<span><%= t(".new") %></span>
<% end %>
</div>
<div class="bg-white rounded-xl shadow-xs border border-alpha-black-100 flex divide-x divide-gray-200">
<div class="w-1/2 p-4 flex items-stretch justify-between">
<div class="space-y-2 grow">
<%= render partial: "shared/balance_heading", locals: {
label: "Assets",
period: @period,
balance: Current.family.assets,
trend: @asset_series.trend
} %>
</div>
<div
data-controller="trendline"
id="assetsTrendline"
class="h-full w-2/5"
data-trendline-series-value="<%= @asset_series.to_json %>"
data-trendline-classification-value="asset"></div>
</div>
<div class="w-1/2 p-4 flex items-stretch justify-between">
<div class="space-y-2 grow">
<%= render partial: "shared/balance_heading", locals: {
label: "Liabilities",
period: @period,
size: "md",
balance: Current.family.liabilities,
trend: @liability_series.trend
} %>
</div>
<div
data-controller="trendline"
id="liabilitiesTrendline"
class="h-full w-2/5"
data-trendline-series-value="<%= @liability_series.to_json %>"
data-trendline-classification-value="liability"></div>
</div>
</div>
<div class="p-4 bg-white rounded-xl shadow-xs border border-alpha-black-25 space-y-4">
<div class="flex justify-between items-center mb-5">
<h2 class="text-lg font-medium text-gray-900">Assets</h2>
<div class="flex items-center gap-2">
<%= link_to new_account_path, class: "flex items-center gap-1 p-2 text-gray-900 text-sm font-medium bg-gray-50 rounded-lg hover:bg-gray-100", data: { turbo_frame: "modal" } do %>
<%= lucide_icon("plus", class: "w-5 h-5 text-gray-500") %>
<p><%= t(".new") %></p>
<% end %>
<%= form_with url: summary_accounts_path, method: :get, class: "flex items-center gap-4", data: { controller: "auto-submit-form" } do %>
<%= render partial: "shared/period_select", locals: { value: @period.name } %>
<% end %>
</div>
</div>
<%= render partial: "pages/account_percentages_bar", locals: { account_groups: @account_groups[:assets].children } %>
<%= render partial: "pages/account_percentages_table", locals: { account_groups: @account_groups[:assets].children } %>
</div>
<div class="p-4 bg-white rounded-xl shadow-xs border border-alpha-black-25 space-y-4">
<div class="flex justify-between items-center mb-5">
<h2 class="text-lg font-medium text-gray-900">Liabilities</h2>
<div class="flex items-center gap-2">
<%= link_to new_account_path, class: "flex items-center gap-1 p-2 text-gray-900 text-sm font-medium bg-gray-50 rounded-lg hover:bg-gray-100", data: { turbo_frame: "modal" } do %>
<%= lucide_icon("plus", class: "w-5 h-5 text-gray-500") %>
<p><%= t(".new") %></p>
<% end %>
<%= form_with url: summary_accounts_path, method: :get, class: "flex items-center gap-4", data: { controller: "auto-submit-form" } do %>
<%= render partial: "shared/period_select", locals: { value: @period.name } %>
<% end %>
</div>
</div>
<%= render partial: "pages/account_percentages_bar", locals: { account_groups: @account_groups[:liabilities].children } %>
<%= render partial: "pages/account_percentages_table", locals: { account_groups: @account_groups[:liabilities].children } %>
</div>
</div>

View file

@ -0,0 +1,88 @@
<div class="flex items-center justify-between">
<%= link_to root_path do %>
<%= image_tag "logo.svg", alt: "Maybe", class: "h-[22px]" %>
<% end %>
<div id="user-menu" data-controller="menu">
<button data-menu-target="button">
<div class="text-white w-9 h-9 bg-gray-400 rounded-full flex items-center justify-center text-lg uppercase"><%= Current.user.email.first %></div>
</button>
<div data-menu-target="content" class="hidden absolute w-[240px] z-10 top-10 left-[255px] top-[72px] bg-white rounded-sm shadow-xs border border-alpha-black-25">
<div class="p-3 flex items-center gap-3">
<div class="text-white shrink-0 w-9 h-9 bg-gray-400 rounded-full flex items-center justify-center text-lg uppercase"><%= Current.user.email.first %></div>
<div>
<span class="text-gray-900 font-medium text-sm"><%= Current.user.first_name %> <%= Current.user.last_name %></span>
<span class="text-gray-500 text-sm"><%= Current.user.email %></span>
</div>
</div>
<div class="border-t border-b border-alpha-black-100 p-1">
<%= link_to settings_profile_path, class: "flex gap-2 items-center hover:bg-gray-50 rounded-lg px-3 py-2" do %>
<%= lucide_icon("settings", class: "w-5 h-5 text-gray-500 shrink-0") %>
<span class="text-gray-900 text-sm">Settings</span>
<% end %>
<div class="flex justify-between items-center gap-2">
<div class="flex items-center gap-2 rounded-lg px-3 py-2">
<%= lucide_icon("app-window", class: "w-5 h-5 text-gray-500 shrink-0") %>
<div>
<span class="text-gray-900 text-sm">Demo mode</span>
<span class="text-gray-500 text-xs block">Coming soon...</span>
</div>
</div>
<%# TODO: This will be a form toggle when implemented %>
<div class="relative inline-block cursor-not-allowed" title="Coming soon...">
<label class="cursor-not-allowed block bg-gray-100 w-9 h-5 rounded-full after:content-[''] after:block after:absolute after:top-0.5 after:left-0.5 after:bg-white after:w-4 after:h-4 after:rounded-full after:transition-transform after:duration-300 after:ease-in-out peer-checked:bg-green-600 peer-checked:after:translate-x-4"></label>
</div>
</div>
</div>
<div class="border-t border-b border-alpha-black-100 p-1">
<%= link_to changelog_path, class: "flex gap-2 items-center hover:bg-gray-50 rounded-lg px-3 py-2" do %>
<%= lucide_icon("box", class: "w-5 h-5 text-gray-500 shrink-0") %>
<span class="text-gray-900 text-sm">Changelog</span>
<% end %>
<%= link_to feedback_path, class: "flex gap-2 items-center hover:bg-gray-50 rounded-lg px-3 py-2" do %>
<%= lucide_icon("megaphone", class: "w-5 h-5 text-gray-500 shrink-0") %>
<span class="text-gray-900 text-sm">Feedback</span>
<% end %>
<%= link_to "https://link.maybe.co/discord", class: "flex gap-2 items-center hover:bg-gray-50 rounded-lg px-3 py-2" do %>
<%= lucide_icon("message-square-more", class: "w-5 h-5 text-gray-500 shrink-0") %>
<span class="text-gray-900 text-sm">Contact</span>
<% end %>
</div>
<div class="p-1">
<%= button_to session_path, method: :delete, class: "w-full text-red-400 flex gap-1 items-center hover:bg-gray-50 rounded-lg px-3 py-2" do %>
<%= lucide_icon("log-out", class: "w-5 h-5 shrink-0") %>
<span class="text-sm">Logout</span>
<% end %>
</div>
</div>
</div>
</div>
<nav>
<ul class="mt-6 space-y-1">
<li>
<%= sidebar_link_to t(".dashboard"), root_path, icon: "layout-grid" %>
</li>
<li>
<%= sidebar_link_to t(".accounts"), summary_accounts_path, icon: "layers" %>
</li>
<li>
<%= sidebar_link_to t(".transactions"), transactions_path, icon: "credit-card" %>
</li>
</ul>
</nav>
<div class="flex flex-col mt-6">
<div class="flex items-center justify-between">
<%= link_to accounts_path, class: "text-xs uppercase text-gray-500 font-bold tracking-wide" do %>
<%= t(".accounts") %>
<% end %>
<%= link_to new_account_path, class: "block hover:bg-gray-100 p-2 text-sm font-semibold text-gray-900 flex items-center rounded", title: t(".new_account"), data: { turbo_frame: "modal" } do %>
<%= lucide_icon("plus", class: "w-5 h-5 text-gray-500") %>
<% end %>
</div>
<%= link_to new_account_path, class: "flex items-center gap-4 px-2 py-3 mb-1 text-gray-500 text-sm font-medium rounded-[10px] hover:bg-gray-100", data: { turbo_frame: "modal" } do %>
<%= lucide_icon("plus", class: "w-5 h-5") %>
<p><%= t(".new_account") %></p>
<% end %>
<% account_groups.each do |group| %>
<%= render "accounts/account_list", group: group %>
<% end %>
</div>

View file

@ -25,73 +25,28 @@
<body class="h-full">
<div id="notification-tray" class="fixed z-50 space-y-1 top-6 right-6"></div>
<%= safe_join(flash.map { |type, message| notification(message, type: type) }) %>
<div class="flex">
<div class="flex-col p-5 min-w-80">
<div class="flex items-center justify-between">
<%= link_to root_path do %>
<%= image_tag "logo.svg", alt: "Maybe", class: "h-[22px]" %>
<% end %>
<div class="relative" data-controller="menu">
<button data-menu-target="button">
<div class="text-white w-9 h-9 bg-gray-400 rounded-full flex items-center justify-center text-lg uppercase"><%= Current.user.email.first %></div>
</button>
<div
data-menu-target="content"
class="hidden absolute min-w-[200px] z-10 top-10 right-0 bg-white p-1 rounded-sm shadow-xs border border-alpha-black-25 w-fit">
<%= link_to edit_settings_path, class: "flex gap-1 items-center hover:bg-gray-50 rounded-md p-2" do %>
<%= lucide_icon("pencil-line", class: "w-5 h-5 text-gray-500 shrink-0") %>
<span class="text-gray-900 text-sm">General Settings</span>
<% end %>
<% if self_hosted? %>
<%= link_to edit_settings_self_hosting_path, class: "flex gap-1 items-center hover:bg-gray-50 rounded-md p-2" do %>
<%= lucide_icon("pencil-line", class: "w-5 h-5 text-gray-500 shrink-0") %>
<span class="text-gray-900 text-sm">Self Host Settings</span>
<% end %>
<% end %>
<%= button_to session_path, method: :delete, class: "w-full text-gray-900 flex gap-1 items-center hover:bg-gray-50 rounded-md p-2" do %>
<%= lucide_icon("log-out", class: "w-5 h-5 shrink-0") %>
<span class="text-sm">Logout</span>
<% end %>
</div>
</div>
</div>
<nav>
<ul class="mt-6 space-y-1">
<li>
<%= sidebar_link_to t(".dashboard"), root_path, icon: "layout-grid" %>
</li>
<li>
<%= sidebar_link_to t(".accounts"), accounts_path, icon: "layers" %>
</li>
<li>
<%= sidebar_link_to t(".transactions"), transactions_path, icon: "credit-card" %>
</li>
</ul>
</nav>
<div class="flex flex-col mt-6">
<div class="flex items-center justify-between">
<%= link_to accounts_path, class: "text-xs uppercase text-gray-500 font-bold tracking-wide" do %>
<%= t(".accounts") %>
<% end %>
<%= link_to new_account_path, class: "block hover:bg-gray-100 p-2 text-sm font-semibold text-gray-900 flex items-center rounded", title: t(".new_account"), data: { turbo_frame: "modal" } do %>
<%= lucide_icon("plus", class: "w-5 h-5 text-gray-500") %>
<% end %>
</div>
<%= link_to new_account_path, class: "flex items-center gap-4 px-2 py-3 mb-1 text-gray-500 text-sm font-medium rounded-[10px] hover:bg-gray-100", data: { turbo_frame: "modal" } do %>
<%= lucide_icon("plus", class: "w-5 h-5") %>
<p><%= t(".new_account") %></p>
<% end %>
<% account_groups.each do |group| %>
<%= render "accounts/account_list", group: group %>
<% end %>
</div>
<div class="flex h-full">
<div class="p-6 w-80 shrink-0 h-full overflow-y-auto">
<% if content_for?(:sidebar) %>
<%= yield :sidebar %>
<% else %>
<%= render "layouts/sidebar" %>
<% end %>
</div>
<main class="flex-grow px-20 py-6">
<main class="grow px-20 py-6 h-full overflow-y-auto">
<%= yield %>
</main>
</div>
<%= turbo_frame_tag "modal" %>
<%= render "shared/custom_confirm_modal" %>
<%= render "shared/upgrade_notification" %>
<% if self_hosted? %>
<div class="flex items-center py-0.5 px-0.5 gap-1 fixed bottom-2 right-2 shadow-xs border border-alpha-black-50 rounded-md bg-white">
<p class="text-xs text-gray-500 pl-2">Self-hosted Maybe: <%= Maybe.version.to_release_tag %></p>
<%= link_to settings_hosting_path, class: "flex gap-1 items-center hover:bg-gray-50 rounded-md p-2" do %>
<%= lucide_icon("settings", class: "w-4 h-4 text-gray-500 shrink-0") %>
<% end %>
</div>
<% end %>
</body>
</html>

View file

@ -15,6 +15,6 @@
</div>
</div>
<div class="bg-white border border-alpha-black-25 shadow-xs rounded-lg divide-y divide-alpha-black-50">
<%= render partial: "account_group_disclosure", collection: account_groups, as: :accountable_group %>
<%= render partial: "pages/account_group_disclosure", collection: account_groups, as: :accountable_group %>
</div>
</div>

View file

@ -0,0 +1,15 @@
<% content_for :sidebar do %>
<%= render "settings/nav" %>
<% end %>
<div class="space-y-4">
<h1 class="text-gray-900 text-xl font-medium mb-4">What's New</h1>
<div class="bg-white shadow-xs border border-alpha-black-25 rounded-xl p-4">
<div class="flex justify-center items-center py-20">
<p class="text-gray-500">Changelog coming soon...</p>
</div>
</div>
<div class="flex justify-between gap-4">
<%= previous_setting("Rules", transactions_rules_path) %>
<%= next_setting("Feedback", feedback_path) %>
</div>
</div>

View file

@ -0,0 +1,15 @@
<% content_for :sidebar do %>
<%= render "settings/nav" %>
<% end %>
<div class="space-y-4">
<h1 class="text-gray-900 text-xl font-medium mb-4">Feedback</h1>
<div class="bg-white shadow-xs border border-alpha-black-25 rounded-xl p-4">
<div class="flex justify-center items-center py-20">
<p class="text-gray-500">Feedback coming soon...</p>
</div>
</div>
<div class="flex justify-between gap-4">
<%= previous_setting("What's New", changelog_path) %>
<%= next_setting("Invite friends", invites_path) %>
</div>
</div>

View file

@ -0,0 +1,14 @@
<% content_for :sidebar do %>
<%= render "settings/nav" %>
<% end %>
<div class="space-y-4">
<h1 class="text-gray-900 text-xl font-medium mb-4">Invite friends</h1>
<div class="bg-white shadow-xs border border-alpha-black-25 rounded-xl p-4">
<div class="flex justify-center items-center py-20">
<p class="text-gray-500">Invite friends coming soon...</p>
</div>
</div>
<div class="flex justify-between gap-4">
<%= previous_setting("Feedback", feedback_path) %>
</div>
</div>

View file

@ -0,0 +1,56 @@
<div class="flex items-center gap-1 mb-6">
<%= link_to root_path, class: "flex items-center gap-1 text-gray-900 font-medium text-sm" do %>
<%= lucide_icon "chevron-left", class: "w-5 h-5 text-gray-500" %>
<span>Back</span>
<% end %>
<span data-controller="hotkey" data-hotkey="Escape" data-action="hotkey#navigateBack" class="uppercase bg-gray-100 rounded-sm px-1 py-0.5 text-xs text-gray-500 shadow-sm ml-1">
esc
</span>
</div>
<nav class="space-y-4">
<section class="space-y-2">
<div class="flex items-center gap-2">
<h3 class="uppercase text-gray-500 font-medium text-xs">General</h3>
<div class="h-px bg-alpha-black-100 w-full"></div>
</div>
<ul class="space-y-1">
<li>
<%= sidebar_link_to t(".profile_label"), settings_profile_path, icon: "circle-user" %>
<%= sidebar_link_to t(".preferences_label"), settings_preferences_path, icon: "bolt" %>
<%= sidebar_link_to t(".notifications_label"), settings_notifications_path, icon: "bell-dot" %>
<%= sidebar_link_to t(".security_label"), settings_security_path, icon: "shield-check" %>
<%= sidebar_link_to t(".billing_label"), settings_billing_path, icon: "circle-dollar-sign" %>
</li>
</ul>
</section>
<section class="space-y-2">
<div class="flex items-center gap-2">
<h3 class="uppercase text-gray-500 font-medium text-xs">Setup</h3>
<div class="h-px bg-alpha-black-100 w-full"></div>
</div>
<ul class="space-y-1">
<li>
<% if self_hosted? %>
<%= sidebar_link_to t(".self_hosting_label"), settings_hosting_path, icon: "database" %>
<% end %>
<%= sidebar_link_to t(".accounts_label"), accounts_path, icon: "layers" %>
<%= sidebar_link_to t(".categories_label"), transactions_categories_path, icon: "tags" %>
<%= sidebar_link_to t(".merchants_label"), transactions_merchants_path, icon: "store" %>
<%= sidebar_link_to t(".rules_label"), transactions_rules_path, icon: "list-checks" %>
</li>
</ul>
</section>
<section class="space-y-2">
<div class="flex items-center gap-2">
<h3 class="uppercase text-gray-500 font-medium text-xs">More</h3>
<div class="h-px bg-alpha-black-100 w-full"></div>
</div>
<ul class="space-y-1">
<li>
<%= sidebar_link_to t(".whats_new_label"), changelog_path, icon: "box" %>
<%= sidebar_link_to t(".feedback_label"), feedback_path, icon: "megaphone" %>
<%= sidebar_link_to t(".invite_label"), invites_path, icon: "gift" %>
</li>
</ul>
</section>
</nav>

View file

@ -0,0 +1,17 @@
<%# locals: path, direction, title %>
<%= link_to path, class: "w-full bg-white hover:bg-gray-50 rounded-xl border border-alpha-black-25 shadow-xs p-4 flex items-center justify-between" do %>
<% if direction == 'previous' %>
<div class="w-5 h-5 text-gray-500">
<%= lucide_icon("arrow-left") %>
</div>
<% end %>
<div class="<%= "flex-grow" if direction == "next" %> <%= "text-right" if direction == "previous" %>">
<span class="block text-sm text-gray-500"><%= t(".#{direction}") %></span>
<span class="block text-sm font-medium text-gray-900"><%= title %></span>
</div>
<% if direction == 'next' %>
<div class="w-5 h-5 text-gray-500">
<%= lucide_icon("arrow-right") %>
</div>
<% end %>
<% end %>

View file

@ -0,0 +1,19 @@
<% content_for :sidebar do %>
<%= render "settings/nav" %>
<% end %>
<div class="space-y-4">
<h1 class="text-gray-900 text-xl font-medium mb-4">Billing</h1>
<div class="bg-white shadow-xs border border-alpha-black-25 rounded-xl p-4">
<div class="flex justify-center items-center py-20">
<p class="text-gray-500">Billing settings coming soon...</p>
</div>
</div>
<div class="flex justify-between gap-4">
<%= previous_setting("Security", settings_security_path) %>
<% if self_hosted? %>
<%= next_setting("Self Hosting", settings_hosting_path) %>
<% else %>
<%= next_setting("Accounts", accounts_path) %>
<% end %>
</div>
</div>

View file

@ -1,17 +0,0 @@
<h1 class="text-3xl font-semibold font-display">Update settings</h1>
<%= form_with model: Current.user, url: settings_path, html: { class: "space-y-4" } do |form| %>
<%= form.fields_for :family_attributes do |family_fields| %>
<%= family_fields.text_field :name, placeholder: "Family name", value: Current.family.name, label: "Family name" %>
<%= family_fields.select :currency, options_for_select(Money::Currency.popular.map { |currency| ["#{currency.iso_code} (#{currency.name})", currency.iso_code] }, selected: Current.family.currency), { label: "Currency" } %>
<% end %>
<%= form.text_field :first_name, placeholder: "First name", value: Current.user.first_name, label: true %>
<%= form.text_field :last_name, placeholder: "Last name", value: Current.user.last_name, label: true %>
<%= form.email_field :email, placeholder: "Email", value: Current.user.email, label: true %>
<%= form.password_field :password, label: true %>
<%= form.password_field :password_confirmation, label: true %>
<div class="fixed right-5 bottom-5">
<button type="submit" class="flex items-center justify-center w-12 h-12 mb-2 bg-black rounded-full shrink-0 grow-0 hover:bg-gray-600">
<%= inline_svg_tag("icn-check.svg", class: "text-white fill-current") %>
</button>
</div>
<% end %>

View file

@ -1,11 +1,14 @@
<% content_for :sidebar do %>
<%= render "settings/nav" %>
<% end %>
<div class="space-y-4">
<h1 class="text-3xl font-semibold font-display">Edit Self Hosting Settings</h1>
<h1 class="text-3xl font-semibold font-display">Self Hosting</h1>
<hr>
<%= form_with model: Setting.new, url: settings_self_hosting_path, method: :patch, local: true, html: { class: "space-y-4" } do |form| %>
<%= form_with model: Setting.new, url: settings_hosting_path, method: :patch, local: true, html: { class: "space-y-4" } do |form| %>
<section class="space-y-3">
<h2 class="text-2xl font-semibold">Render Deploy Hook</h2>
<p class="text-gray-500">You must fill this in so your app can trigger upgrades when Maybe releases upgrades. Learn more about deploy hooks and how they work in the <%= link_to "Render documentation", "https://docs.render.com/docs/deploy-hooks", target: "_blank", rel: "noopener noreferrer", class: "text-blue-500 hover:underline" %>.</p>
<%= form.text_field :render_deploy_hook, label: "Render Deploy Hook", placeholder: "https://api.render.com/deploy/srv-xyz...", value: Setting.render_deploy_hook %>
<%= form.url_field :render_deploy_hook, label: "Render Deploy Hook", placeholder: "https://api.render.com/deploy/srv-xyz...", value: Setting.render_deploy_hook %>
</section>
<section class="space-y-3">
<h2 class="text-2xl font-semibold">Auto Upgrades Setting</h2>
@ -37,4 +40,8 @@
</button>
</div>
<% end %>
<div class="flex justify-between gap-4">
<%= previous_setting("Billing", settings_billing_path) %>
<%= next_setting("Accounts", accounts_path) %>
</div>
</div>

View file

@ -0,0 +1,15 @@
<% content_for :sidebar do %>
<%= render "settings/nav" %>
<% end %>
<div class="space-y-4">
<h1 class="text-gray-900 text-xl font-medium mb-4">Notifications</h1>
<div class="bg-white shadow-xs border border-alpha-black-25 rounded-xl p-4">
<div class="flex justify-center items-center py-20">
<p class="text-gray-500">Notifications coming soon...</p>
</div>
</div>
<div class="flex justify-between gap-4">
<%= previous_setting("Preferences", settings_preferences_path) %>
<%= next_setting("Security", settings_security_path) %>
</div>
</div>

View file

@ -0,0 +1,15 @@
<% content_for :sidebar do %>
<%= render "settings/nav" %>
<% end %>
<div class="space-y-4">
<h1 class="text-gray-900 text-xl font-medium mb-4">Preferences</h1>
<div class="bg-white shadow-xs border border-alpha-black-25 rounded-xl p-4">
<div class="flex justify-center items-center py-20">
<p class="text-gray-500">Preferences coming soon...</p>
</div>
</div>
<div class="flex justify-between gap-4">
<%= previous_setting("Account", settings_profile_path) %>
<%= next_setting("Notifications", settings_notifications_path) %>
</div>
</div>

View file

@ -0,0 +1,25 @@
<% content_for :sidebar do %>
<%= render "settings/nav" %>
<% end %>
<div class="space-y-4">
<h1 class="text-gray-900 text-xl font-medium mb-4">Account</h1>
<%= form_with model: Current.user, url: settings_profile_path, html: { class: "space-y-4" } do |form| %>
<%= form.fields_for :family_attributes do |family_fields| %>
<%= family_fields.text_field :name, placeholder: "Family name", value: Current.family.name, label: "Family name" %>
<%= family_fields.select :currency, options_for_select(Money::Currency.popular.map { |currency| ["#{currency.iso_code} (#{currency.name})", currency.iso_code] }, selected: Current.family.currency), { label: "Currency" } %>
<% end %>
<%= form.text_field :first_name, placeholder: "First name", value: Current.user.first_name, label: true %>
<%= form.text_field :last_name, placeholder: "Last name", value: Current.user.last_name, label: true %>
<%= form.email_field :email, placeholder: "Email", value: Current.user.email, label: true %>
<%= form.password_field :password, label: true %>
<%= form.password_field :password_confirmation, label: true %>
<div class="fixed right-5 bottom-5">
<button type="submit" class="flex items-center justify-center w-12 h-12 mb-2 bg-black rounded-full shrink-0 grow-0 hover:bg-gray-600">
<%= inline_svg_tag("icn-check.svg", class: "text-white fill-current") %>
</button>
</div>
<% end %>
<div class="flex gap-4">
<%= next_setting("Preferences", settings_preferences_path) %>
</div>
</div>

View file

@ -0,0 +1,15 @@
<% content_for :sidebar do %>
<%= render "settings/nav" %>
<% end %>
<div class="space-y-4">
<h1 class="text-gray-900 text-xl font-medium mb-4">Security</h1>
<div class="bg-white shadow-xs border border-alpha-black-25 rounded-xl p-4">
<div class="flex justify-center items-center py-20">
<p class="text-gray-500">Security settings coming soon...</p>
</div>
</div>
<div class="flex justify-between gap-4">
<%= previous_setting("Notifications", settings_notifications_path) %>
<%= next_setting("Billing", settings_billing_path) %>
</div>
</div>

View file

@ -0,0 +1,15 @@
<% content_for :sidebar do %>
<%= render "settings/nav" %>
<% end %>
<div class="space-y-4">
<h1 class="text-gray-900 text-xl font-medium mb-4">Categories</h1>
<div class="bg-white shadow-xs border border-alpha-black-25 rounded-xl p-4">
<div class="flex justify-center items-center py-20">
<p class="text-gray-500">Transaction categories coming soon...</p>
</div>
</div>
<div class="flex justify-between gap-4">
<%= previous_setting("Accounts", accounts_path) %>
<%= next_setting("Merchants", transactions_merchants_path) %>
</div>
</div>

View file

@ -0,0 +1,15 @@
<% content_for :sidebar do %>
<%= render "settings/nav" %>
<% end %>
<div class="space-y-4">
<h1 class="text-gray-900 text-xl font-medium mb-4">Merchants</h1>
<div class="bg-white shadow-xs border border-alpha-black-25 rounded-xl p-4">
<div class="flex justify-center items-center py-20">
<p class="text-gray-500">Manage transaction merchants coming soon...</p>
</div>
</div>
<div class="flex justify-between gap-4">
<%= previous_setting("Categories", transactions_categories_path) %>
<%= next_setting("Rules", transactions_rules_path) %>
</div>
</div>

View file

@ -0,0 +1,15 @@
<% content_for :sidebar do %>
<%= render "settings/nav" %>
<% end %>
<div class="space-y-4">
<h1 class="text-gray-900 text-xl font-medium mb-4">Rules</h1>
<div class="bg-white shadow-xs border border-alpha-black-25 rounded-xl p-4">
<div class="flex justify-center items-center py-20">
<p class="text-gray-500">Transaction rules coming soon...</p>
</div>
</div>
<div class="flex justify-between gap-4">
<%= previous_setting("Merchants", transactions_merchants_path) %>
<%= next_setting("What's New", changelog_path) %>
</div>
</div>

View file

@ -16,6 +16,8 @@ en:
placeholder: Example account name
select_accountable_type: What would you like to add?
title: Add an account
summary:
new: New account
sync:
success: Account sync started
update:

View file

@ -1,11 +1,6 @@
---
en:
layouts:
application:
accounts: Accounts
dashboard: Dashboard
new_account: New account
transactions: Transactions
auth:
or: or
privacy_policy: Privacy Policy
@ -13,3 +8,8 @@ en:
sign_up: create an account
terms_of_service: Terms of Service
your_account: Your account
sidebar:
accounts: Accounts
dashboard: Dashboard
new_account: New account
transactions: Transactions

View file

@ -1,8 +1,25 @@
---
en:
settings:
self_hosting:
hostings:
update:
render_deploy_hook_error: Render deploy hook must be provided to enable auto
upgrades
success: Settings updated successfully.
nav:
accounts_label: Accounts
billing_label: Billing
categories_label: Categories
feedback_label: Feedback
invite_label: Invite friends
merchants_label: Merchants
notifications_label: Notifications
preferences_label: Preferences
profile_label: Account
rules_label: Rules
security_label: Security
self_hosting_label: Self Hosting
whats_new_label: What's New
nav_link_large:
next: Next
previous: Back

View file

@ -1,24 +1,39 @@
Rails.application.routes.draw do
mount GoodJob::Engine => "jobs"
get "changelog" => "pages#changelog", as: :changelog
get "feedback" => "pages#feedback", as: :feedback
get "invites" => "pages#invites", as: :invites
resource :registration
resource :session
resource :password_reset
resource :password
resource :settings, only: %i[edit update] do
resource :self_hosting, only: %i[edit update], controller: "settings/self_hosting"
namespace :settings do
resource :profile, only: %i[show update]
resource :preferences, only: %i[show update]
resource :notifications, only: %i[show update]
resource :billing, only: %i[show update]
resource :hosting, only: %i[show update]
resource :security, only: %i[show update]
end
namespace :transactions do
resources :categories
# TODO: These are *placeholders*
# Uncomment `only` and add the necessary actions as they are implemented.
resources :rules, only: [ :index ]
resources :merchants, only: [ :index ]
end
resources :transactions do
match "search" => "transactions#search", on: :collection, via: [ :get, :post ], as: :search
end
namespace :transactions do
resources :categories
end
resources :accounts, shallow: true do
get :summary, on: :collection
post :sync, on: :member
resources :valuations
end

View file

@ -12,9 +12,10 @@ class ApplicationSystemTestCase < ActionDispatch::SystemTestCase
fill_in "Password", with: "password"
click_button "Log in"
end
assert_text "Dashboard", wait: 5
find('[data-controller="menu"]').click
end
def sign_out
find("#user-menu").click
click_button "Logout"
assert_text "Sign in to your account"
end
end

View file

@ -1,6 +1,6 @@
require "test_helper"
class Settings::SelfHostingControllerTest < ActionDispatch::IntegrationTest
class Settings::HostingsControllerTest < ActionDispatch::IntegrationTest
setup do
ENV["SELF_HOSTING_ENABLED"] = "true"
sign_in users(:family_admin)
@ -9,14 +9,14 @@ class Settings::SelfHostingControllerTest < ActionDispatch::IntegrationTest
test "cannot edit when self hosting is disabled" do
ENV["SELF_HOSTING_ENABLED"] = "false"
get edit_settings_self_hosting_url
get settings_hosting_url
assert :not_found
patch settings_self_hosting_url, params: { setting: { render_deploy_hook: "https://example.com" } }
patch settings_hosting_url, params: { setting: { render_deploy_hook: "https://example.com" } }
assert :not_found
end
test "should get edit when self hosting is enabled" do
get edit_settings_self_hosting_url
get settings_hosting_url
assert_response :success
end
@ -24,7 +24,7 @@ class Settings::SelfHostingControllerTest < ActionDispatch::IntegrationTest
NEW_RENDER_DEPLOY_HOOK = "https://api.render.com/deploy/srv-abc123"
assert_nil Setting.render_deploy_hook
patch settings_self_hosting_url, params: { setting: { render_deploy_hook: NEW_RENDER_DEPLOY_HOOK } }
patch settings_hosting_url, params: { setting: { render_deploy_hook: NEW_RENDER_DEPLOY_HOOK } }
assert_equal NEW_RENDER_DEPLOY_HOOK, Setting.render_deploy_hook
end

View file

@ -0,0 +1,11 @@
require "test_helper"
class Settings::NotificationsControllerTest < ActionDispatch::IntegrationTest
setup do
sign_in users(:family_admin)
end
test "get" do
get settings_notifications_url
assert_response :success
end
end

View file

@ -0,0 +1,11 @@
require "test_helper"
class Settings::PreferencesControllerTest < ActionDispatch::IntegrationTest
setup do
sign_in users(:family_admin)
end
test "get" do
get settings_preferences_url
assert_response :success
end
end

View file

@ -0,0 +1,11 @@
require "test_helper"
class Settings::ProfilesControllerTest < ActionDispatch::IntegrationTest
setup do
sign_in @user = users(:family_admin)
end
test "get" do
get settings_profile_url
assert_response :success
end
end

View file

@ -0,0 +1,11 @@
require "test_helper"
class Settings::SecuritiesControllerTest < ActionDispatch::IntegrationTest
setup do
sign_in users(:family_admin)
end
test "get" do
get settings_security_url
assert_response :success
end
end

View file

@ -1,20 +0,0 @@
require "application_system_test_case"
class AccountsTest < ApplicationSystemTestCase
setup do
sign_in @user = users(:family_admin)
end
test "should create account" do
skip("Disabling this test for now, UI is changing to quickly to do systems testing")
click_on "New account"
click_on "Credit Card"
within "form" do
fill_in "Name", with: "VISA"
fill_in "Balance", with: "1000"
click_on "Submit"
end
assert_text "$1,000"
end
end

View file

@ -0,0 +1,66 @@
require "application_system_test_case"
class SettingsTest < ApplicationSystemTestCase
setup do
sign_in @user = users(:family_admin)
@settings_links = [
[ "Account", "Account", settings_profile_path ],
[ "Preferences", "Preferences", settings_preferences_path ],
[ "Notifications", "Notifications", settings_notifications_path ],
[ "Security", "Security", settings_security_path ],
[ "Billing", "Billing", settings_billing_path ],
[ "Accounts", "Accounts", accounts_path ],
[ "Categories", "Categories", transactions_categories_path ],
[ "Merchants", "Merchants", transactions_merchants_path ],
[ "Rules", "Rules", transactions_rules_path ],
[ "What's New", "What's New", changelog_path ],
[ "Feedback", "Feedback", feedback_path ],
[ "Invite friends", "Invite friends", invites_path ]
]
end
test "can access settings from sidebar" do
open_settings_from_sidebar
assert_selector "h1", text: "Account"
assert_current_path settings_profile_path
end
test "all settings views and links are accessible" do
open_settings_from_sidebar
@settings_links.each_with_index do |(link_text, header_text, path), index|
next_setting_path = @settings_links[index + 1][2] if index < @settings_links.size - 1
prev_setting_path = @settings_links[index - 1][2] if index > 0
find_link(link_text, exact: true).click
assert_selector "h1", text: header_text
assert_current_path path
assert_link "Next", href: next_setting_path if next_setting_path.present?
assert_link "Back", href: prev_setting_path if prev_setting_path.present?
end
# Conditional nav items don't show by default
assert_no_text "Self Hosting"
end
test "can see conditional nav items" do
ENV["SELF_HOSTING_ENABLED"] = "true"
open_settings_from_sidebar
click_link "Self Hosting"
assert_selector "h1", text: "Self Hosting"
end
test "clicking back or hitting escape key takes user back page they opened settings from" do
# TODO: Implement test for back navigation and escape key functionality.
end
private
def open_settings_from_sidebar
find("#user-menu").click
click_link "Settings"
end
end

View file

@ -1,3 +1,7 @@
# Require individual test files to enable these as needed
ENV["SELF_HOSTING_ENABLED"] = "false"
ENV["UPGRADES_ENABLED"] = "false"
ENV["RAILS_ENV"] ||= "test"
require_relative "../config/environment"
require "rails/test_help"