diff --git a/Gemfile b/Gemfile index 6261d8e2..52b57177 100644 --- a/Gemfile +++ b/Gemfile @@ -44,6 +44,7 @@ gem "pagy" gem "rails-settings-cached" gem "tzinfo-data", platforms: %i[ windows jruby ] gem "csv" +gem "redcarpet" group :development, :test do gem "debug", platforms: %i[ mri windows ] diff --git a/Gemfile.lock b/Gemfile.lock index ae720526..fcbcc8cc 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -336,6 +336,7 @@ GEM logger rdoc (6.7.0) psych (>= 4.0.0) + redcarpet (3.6.0) regexp_parser (2.9.2) reline (0.5.9) io-console (~> 0.5) @@ -492,6 +493,7 @@ DEPENDENCIES puma (>= 5.0) rails! rails-settings-cached + redcarpet rubocop-rails-omakase ruby-lsp-rails selenium-webdriver diff --git a/app/controllers/account/entries_controller.rb b/app/controllers/account/entries_controller.rb index 21e971a1..b12ae099 100644 --- a/app/controllers/account/entries_controller.rb +++ b/app/controllers/account/entries_controller.rb @@ -31,7 +31,7 @@ class Account::EntriesController < ApplicationController else # TODO: this is not an ideal way to handle errors and should eventually be improved. # See: https://github.com/hotwired/turbo-rails/pull/367 - flash[:error] = @entry.errors.full_messages.to_sentence + flash[:alert] = @entry.errors.full_messages.to_sentence redirect_to account_path(@account) end end diff --git a/app/controllers/account/transfers_controller.rb b/app/controllers/account/transfers_controller.rb index a494285c..ac60583a 100644 --- a/app/controllers/account/transfers_controller.rb +++ b/app/controllers/account/transfers_controller.rb @@ -23,7 +23,7 @@ class Account::TransfersController < ApplicationController else # TODO: this is not an ideal way to handle errors and should eventually be improved. # See: https://github.com/hotwired/turbo-rails/pull/367 - flash[:error] = @transfer.errors.full_messages.to_sentence + flash[:alert] = @transfer.errors.full_messages.to_sentence redirect_to transactions_path end end diff --git a/app/controllers/accounts_controller.rb b/app/controllers/accounts_controller.rb index 4f4c2b1e..568f440f 100644 --- a/app/controllers/accounts_controller.rb +++ b/app/controllers/accounts_controller.rb @@ -68,8 +68,6 @@ class AccountsController < ApplicationController unless @account.syncing? @account.sync_later end - - redirect_to account_path(@account), notice: t(".success") end def sync_all diff --git a/app/controllers/help/articles_controller.rb b/app/controllers/help/articles_controller.rb new file mode 100644 index 00000000..17ff8fae --- /dev/null +++ b/app/controllers/help/articles_controller.rb @@ -0,0 +1,11 @@ +class Help::ArticlesController < ApplicationController + layout "with_sidebar" + + def show + @article = Help::Article.find(params[:id]) + + unless @article + head :not_found + end + end +end diff --git a/app/controllers/imports_controller.rb b/app/controllers/imports_controller.rb index 0857e278..90086684 100644 --- a/app/controllers/imports_controller.rb +++ b/app/controllers/imports_controller.rb @@ -42,13 +42,13 @@ class ImportsController < ApplicationController begin @import.raw_csv_str = import_params[:raw_csv_str].read rescue NoMethodError - flash.now[:error] = "Please select a file to upload" + flash.now[:alert] = "Please select a file to upload" render :load, status: :unprocessable_entity and return end if @import.save redirect_to configure_import_path(@import), notice: t(".import_loaded") else - flash.now[:error] = @import.errors.full_messages.to_sentence + flash.now[:alert] = @import.errors.full_messages.to_sentence render :load, status: :unprocessable_entity end end @@ -57,7 +57,7 @@ class ImportsController < ApplicationController if @import.update(import_params) redirect_to configure_import_path(@import), notice: t(".import_loaded") else - flash.now[:error] = @import.errors.full_messages.to_sentence + flash.now[:alert] = @import.errors.full_messages.to_sentence render :load, status: :unprocessable_entity end end diff --git a/app/controllers/settings/hostings_controller.rb b/app/controllers/settings/hostings_controller.rb index 89a42946..3058074f 100644 --- a/app/controllers/settings/hostings_controller.rb +++ b/app/controllers/settings/hostings_controller.rb @@ -19,7 +19,7 @@ class Settings::HostingsController < SettingsController def send_test_email unless Setting.smtp_settings_populated? - flash[:error] = t(".missing_smtp_setting_error") + flash[:alert] = t(".missing_smtp_setting_error") render(:show, status: :unprocessable_entity) return end @@ -27,7 +27,7 @@ class Settings::HostingsController < SettingsController begin NotificationMailer.with(user: Current.user).test_email.deliver_now rescue => _e - flash[:error] = t(".error") + flash[:alert] = t(".error") render :show, status: :unprocessable_entity return end diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index bb9462a5..141db192 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -13,11 +13,18 @@ module ApplicationHelper name.underscore end - def notification(text, **options, &block) - content = tag.p(text) - content = capture &block if block_given? + def family_notifications_stream + turbo_stream_from [ Current.family, :notifications ] if Current.family + end - render partial: "shared/notification", locals: { type: options[:type], content: { body: content } } + def render_flash_notifications + notifications = flash.flat_map do |type, message_or_messages| + Array(message_or_messages).map do |message| + render partial: "shared/notification", locals: { type: type, message: message } + end + end + + safe_join(notifications) end ## diff --git a/app/models/account.rb b/app/models/account.rb index 7d999835..2ce6495f 100644 --- a/app/models/account.rb +++ b/app/models/account.rb @@ -2,8 +2,6 @@ class Account < ApplicationRecord include Syncable include Monetizable - broadcasts_refreshes - validates :name, :balance, :currency, presence: true belongs_to :family diff --git a/app/models/account/sync.rb b/app/models/account/sync.rb index 0f8a1c9a..5db21b89 100644 --- a/app/models/account/sync.rb +++ b/app/models/account/sync.rb @@ -48,13 +48,35 @@ class Account::Sync < ApplicationRecord def start! update! status: "syncing", last_ran_at: Time.now + broadcast_start end def complete! update! status: "completed" + broadcast_result type: "notice", message: "Sync complete" end def fail!(error) update! status: "failed", error: error.message + broadcast_result type: "alert", message: error.message + end + + def broadcast_start + broadcast_append_to( + [ account.family, :notifications ], + target: "notification-tray", + partial: "shared/notification", + locals: { id: id, type: "processing", message: "Syncing account balances" } + ) + end + + def broadcast_result(type:, message:) + broadcast_remove_to account.family, :notifications, target: id # Remove persistent syncing notification + broadcast_append_to( + [ account.family, :notifications ], + target: "notification-tray", + partial: "shared/notification", + locals: { type: type, message: message } + ) end end diff --git a/app/models/current.rb b/app/models/current.rb index fd51b793..316a2cf5 100644 --- a/app/models/current.rb +++ b/app/models/current.rb @@ -1,5 +1,5 @@ class Current < ActiveSupport::CurrentAttributes attribute :user - delegate :family, to: :user + delegate :family, to: :user, allow_nil: true end diff --git a/app/models/help/article.rb b/app/models/help/article.rb new file mode 100644 index 00000000..40f79474 --- /dev/null +++ b/app/models/help/article.rb @@ -0,0 +1,54 @@ +class Help::Article + attr_reader :frontmatter, :content + + def initialize(frontmatter:, content:) + @frontmatter = frontmatter + @content = content + end + + def title + frontmatter["title"] + end + + def html + render_markdown(content) + end + + class << self + def root_path + Rails.root.join("docs", "help") + end + + def find(slug) + Dir.glob(File.join(root_path, "*.md")).each do |file_path| + file_content = File.read(file_path) + frontmatter, markdown_content = parse_frontmatter(file_content) + + return new(frontmatter:, content: markdown_content) if frontmatter["slug"] == slug + end + + nil + end + + private + + def parse_frontmatter(content) + if content =~ /\A---(.+?)---/m + frontmatter = YAML.safe_load($1) + markdown_content = content[($~.end(0))..-1].strip + else + frontmatter = {} + markdown_content = content + end + + [ frontmatter, markdown_content ] + end + end + + private + + def render_markdown(content) + markdown = Redcarpet::Markdown.new(Redcarpet::Render::HTML) + markdown.render(content) + end +end diff --git a/app/views/accounts/_alert.html.erb b/app/views/accounts/_alert.html.erb new file mode 100644 index 00000000..697f0942 --- /dev/null +++ b/app/views/accounts/_alert.html.erb @@ -0,0 +1,19 @@ +<%# locals: (message:, help_path: nil) -%> +<%= tag.div class: "flex gap-6 items-center rounded-xl px-4 py-3 bg-error/5", + data: { controller: "element-removal" }, + role: "alert" do %> +
<%= message %>
+- Syncing your account balances. -
-