diff --git a/app/javascript/controllers/submit_on_change_controller.js b/app/javascript/controllers/submit_on_change_controller.js new file mode 100644 index 00000000..b08e471c --- /dev/null +++ b/app/javascript/controllers/submit_on_change_controller.js @@ -0,0 +1,7 @@ +import { Controller } from "@hotwired/stimulus" + +export default class extends Controller { + submit() { + this.element.requestSubmit() + } +} diff --git a/app/views/accounts/index.html.erb b/app/views/accounts/index.html.erb index fd5a00a8..6edd18c3 100644 --- a/app/views/accounts/index.html.erb +++ b/app/views/accounts/index.html.erb @@ -20,16 +20,7 @@

<%= table[:heading] %>

- <% if table[:page].number > 1 || table[:page].before_last? %> - - <% end %> + <%= render "shared/pagination", page: table[:page], param: table[:page_param] %> @@ -55,15 +46,6 @@
- <% if table[:page].number > 1 || table[:page].before_last? %> - - <% end %> + <%= render "shared/pagination", page: table[:page], param: table[:page_param] %>
<% end %> diff --git a/app/views/layouts/admin.html.erb b/app/views/layouts/admin.html.erb index 95ab9c9d..2d9db756 100644 --- a/app/views/layouts/admin.html.erb +++ b/app/views/layouts/admin.html.erb @@ -1,5 +1,6 @@ " data-light-theme="<%= theme_attributes[:"data-light-theme"] %>" diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb index e8064e75..cf8c3991 100644 --- a/app/views/layouts/application.html.erb +++ b/app/views/layouts/application.html.erb @@ -1,5 +1,6 @@ " data-light-theme="<%= theme_attributes[:"data-light-theme"] %>" diff --git a/app/views/layouts/login.html.erb b/app/views/layouts/login.html.erb new file mode 100644 index 00000000..6af44b79 --- /dev/null +++ b/app/views/layouts/login.html.erb @@ -0,0 +1,21 @@ + +<%= tag.html(lang: I18n.locale, **theme_attributes, "data-controller": "theme") do %> + + <%= Appkit.config.app_display_name %> + + + + <%= csrf_meta_tags %> + <%= csp_meta_tag %> + <%= render "appkit/shared/pwa_meta" %> + + <%= stylesheet_link_tag "application", media: "all", "data-turbo-track": "reload" %> + <%= javascript_importmap_tags %> + + +
+ <%= render "appkit/shared/flashes" %> + <%= yield %> +
+ +<% end %> diff --git a/app/views/shared/_pagination.html.erb b/app/views/shared/_pagination.html.erb index 4d193bd1..97906ddc 100644 --- a/app/views/shared/_pagination.html.erb +++ b/app/views/shared/_pagination.html.erb @@ -1,14 +1,14 @@ -<%# locals: (page:) %> +<%# locals: (page:, param: :page) %> <% show_previous = page.number > 1 %> <% show_next = page.before_last? %> <% if show_previous || show_next %> <% end %> diff --git a/app/views/users/_user.html.erb b/app/views/users/_user.html.erb index 88d07021..0af13d67 100644 --- a/app/views/users/_user.html.erb +++ b/app/views/users/_user.html.erb @@ -2,10 +2,10 @@
<%= link_to user.name, user.current? ? edit_user_profile_path(user) : user_profile_path(user) %> - <%= form_with model: user, url: user_path(user), data: { controller: "form" }, method: :patch do |form| %> + <%= form_with model: user, url: user_path(user), data: { controller: "submit-on-change" }, method: :patch do |form| %> diff --git a/app/views/users/profiles/edit.html.erb b/app/views/users/profiles/edit.html.erb index 90c520e1..5ba4d482 100644 --- a/app/views/users/profiles/edit.html.erb +++ b/app/views/users/profiles/edit.html.erb @@ -1,7 +1,5 @@ <% content_for(:title) { t(".title") } %> -<%= render layout: "shared/title_bar", locals: { title: t('.title') } do %> - <%= link_to t("common.back"), users_path %> -<% end %> +<%= render "shared/title_bar", title: t(".title") %>
<%= form_with model: @user, url: user_profile_path(@user) do |form| %> diff --git a/config/locales/en.yml b/config/locales/en.yml index 1235c87e..8a9e9cae 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -110,8 +110,10 @@ en: amount: Amount category: Category category: + direction: Direction kind: Kind name: Name + parent_category: Parent category chattel: brand: Brand kind: Kind @@ -121,6 +123,7 @@ en: notes: Notes purchase_price: Purchase price purchase_transaction: Transaction + purchase_transaction_id: Purchase transaction purchased_at: Purchase Date serial_number: Serial Number warranty_document: Warranty document @@ -140,10 +143,21 @@ en: account: Account amount: Amount appropriation: Appropriation + booked_at: Booked at category: Category + category_id: Category + creditor: Creditor date: Date + debitor: Debitor description: Description + id: Id + interest_at: Interest at + note: Note + original_balance_after_mutation: Original balance after mutation + original_note: Original note + original_tag: Original tag proof_of_purchase: Proof of purchase + type: Type transaction_split: appropriation: Appropriation errors: diff --git a/config/locales/it.yml b/config/locales/it.yml index cda24834..a0469fa6 100644 --- a/config/locales/it.yml +++ b/config/locales/it.yml @@ -110,8 +110,10 @@ it: amount: Importo category: Categoria category: + direction: Direzione kind: Tipo name: Nome + parent_category: Categoria principale chattel: brand: Marca kind: Tipo @@ -121,6 +123,7 @@ it: notes: Note purchase_price: Prezzo d'acquisto purchase_transaction: Transazione + purchase_transaction_id: Transazione d'acquisto purchased_at: Data d'acquisto serial_number: Numero seriale warranty_document: Documento di garanzia @@ -140,10 +143,21 @@ it: account: Conto amount: Importo appropriation: Stanziamento + booked_at: Data registrazione category: Categoria + category_id: Categoria + creditor: Creditore date: Data + debitor: Debitore description: Descrizione + id: ID + interest_at: Data valuta + note: Nota + original_balance_after_mutation: Saldo originale dopo il movimento + original_note: Descrizione originale + original_tag: Etichetta originale proof_of_purchase: Prova d'acquisto + type: Tipo transaction_split: appropriation: Stanziamento errors: diff --git a/config/locales/nl.yml b/config/locales/nl.yml index 1f619068..56efe3e9 100644 --- a/config/locales/nl.yml +++ b/config/locales/nl.yml @@ -110,8 +110,10 @@ nl: amount: Bedrag category: Categorie category: + direction: Richting kind: Type name: Naam + parent_category: Hoofdcategorie chattel: brand: Merk kind: Type @@ -121,6 +123,7 @@ nl: notes: Notities purchase_price: Aankoopprijs purchase_transaction: Transactie + purchase_transaction_id: Aankooptransactie purchased_at: Aankoopdatum serial_number: Serienummer warranty_document: Garantiedocument @@ -140,10 +143,21 @@ nl: account: Rekening amount: Bedrag appropriation: Begrotingspost + booked_at: Boekingsdatum category: Categorie + category_id: Categorie + creditor: Crediteur date: Datum + debitor: Debiteur description: Omschrijving + id: ID + interest_at: Rentedatum + note: Opmerking + original_balance_after_mutation: Oorspronkelijk saldo na mutatie + original_note: Oorspronkelijke omschrijving + original_tag: Oorspronkelijk label proof_of_purchase: Aankoopbewijs + type: Type transaction_split: appropriation: Begrotingspost errors: diff --git a/test/integration/form_labels_test.rb b/test/integration/form_labels_test.rb new file mode 100644 index 00000000..66fda278 --- /dev/null +++ b/test/integration/form_labels_test.rb @@ -0,0 +1,58 @@ +require "test_helper" + +class FormLabelsTest < ActionDispatch::IntegrationTest + LABELS = { + nl: { + transaction: [ "ID", "Debiteur", "Crediteur", "Categorie", "Opmerking", "Boekingsdatum", "Rentedatum", "Type", + "Oorspronkelijke omschrijving", "Oorspronkelijk saldo na mutatie", "Oorspronkelijk label" ], + category: [ "Richting", "Hoofdcategorie" ], + chattel: [ "Aankooptransactie" ] + }, + it: { + transaction: [ "ID", "Debitore", "Creditore", "Categoria", "Nota", "Data registrazione", "Data valuta", "Tipo", + "Descrizione originale", "Saldo originale dopo il movimento", "Etichetta originale" ], + category: [ "Direzione", "Categoria principale" ], + chattel: [ "Transazione d'acquisto" ] + } + }.freeze + + PAGES = { + transaction: -> { edit_transaction_path(transactions(:debit_grocery)) }, + category: -> { edit_category_path(categories(:groceries)) }, + chattel: -> { edit_chattel_path(chattels(:one)) } + }.freeze + + setup do + @user = users(:admin) + end + + LABELS.each_key do |locale| + names = { nl: "Dutch", it: "Italian" }.fetch(locale) + + test "the transaction form labels its fields in #{names}" do + assert_labels locale, :transaction + end + + test "the category form labels direction and parent category in #{names}" do + assert_labels locale, :category + end + + test "the chattel form labels the purchase transaction in #{names}" do + assert_labels locale, :chattel + end + end + + private + + def assert_labels(locale, form) + @user.update!(locale: locale) + sign_in_as(@user) + + get instance_exec(&PAGES.fetch(form)) + + assert_response :success + LABELS.dig(locale, form).each do |text| + assert_select "form label", text: text + end + end +end diff --git a/test/integration/login_layout_test.rb b/test/integration/login_layout_test.rb new file mode 100644 index 00000000..56525dbe --- /dev/null +++ b/test/integration/login_layout_test.rb @@ -0,0 +1,11 @@ +require "test_helper" + +class LoginLayoutTest < ActionDispatch::IntegrationTest + test "a signed-out visitor's page leaves the theme to the browser" do + get new_session_path + + assert_select "html[data-controller=?]", "theme" + assert_select "html[data-theme]", count: 0 + assert_select "html[data-color-scheme]", count: 0 + end +end diff --git a/test/integration/page_language_test.rb b/test/integration/page_language_test.rb new file mode 100644 index 00000000..7429f9fa --- /dev/null +++ b/test/integration/page_language_test.rb @@ -0,0 +1,33 @@ +require "test_helper" + +class PageLanguageTest < ActionDispatch::IntegrationTest + test "a Dutch user gets lang nl" do + sign_in_as(users(:admin)) + + get dashboard_index_path + + assert_select "html[lang=?]", "nl" + end + + test "an English user gets lang en" do + sign_in_as(users(:member)) + + get dashboard_index_path + + assert_select "html[lang=?]", "en" + end + + test "the admin layout declares the user's language" do + sign_in_as(users(:admin)) + + get admin_root_path + + assert_select "html[lang=?]", "nl" + end + + test "a signed-out visitor gets the default locale" do + get new_session_path + + assert_select "html[lang=?]", I18n.default_locale.to_s + end +end diff --git a/test/integration/pagination_navigation_test.rb b/test/integration/pagination_navigation_test.rb index c4532932..4f7e58cc 100644 --- a/test/integration/pagination_navigation_test.rb +++ b/test/integration/pagination_navigation_test.rb @@ -21,6 +21,29 @@ class PaginationNavigationTest < ActionDispatch::IntegrationTest assert_select "nav.pagination a[rel='next']", 0 end + test "external accounts pagination writes external_page" do + create_external_accounts(51) + + get accounts_path + + assert_response :success + assert_select "nav.pagination a[rel='next'][href*='external_page=2']", 2 + assert_select "nav.pagination a[rel='next'][href*='own_page']", 0 + + get accounts_path, params: { external_page: 2 } + + assert_select "nav.pagination a[rel='prev'][href*='external_page=1']", 2 + assert_select "nav.pagination a[rel='next']", 0 + end + + test "accounts pagination keeps the other query parameters" do + create_accounts(51) + + get accounts_path, params: { external_page: 1, q: "kept" } + + assert_select "nav.pagination a[rel='next'][href*='own_page=2'][href*='external_page=1'][href*='q=kept']", 2 + end + test "transactions pagination keeps filter params and does not show next on overflow pages" do create_uncategorized_transactions(21) @@ -64,6 +87,15 @@ def create_accounts(count) end end + def create_external_accounts(count) + count.times do |index| + Account.create!( + account_numbers: [ AccountNumber.new(number: format("NL98SHOP%010d", index)) ], + name: "External Pagination Account #{index}" + ) + end + end + def create_uncategorized_transactions(count) debitor = accounts(:checking) creditor = accounts(:unknown) diff --git a/test/integration/users/profiles_test.rb b/test/integration/users/profiles_test.rb index a21597b4..f116e845 100644 --- a/test/integration/users/profiles_test.rb +++ b/test/integration/users/profiles_test.rb @@ -65,4 +65,20 @@ class Users::ProfilesTest < ActionDispatch::IntegrationTest assert_select "form > footer a[role=button][href=?]", user_profile_path(@member) assert_select "form > footer button.button-danger", count: 0 end + + test "your own profile edit page has no back link" do + sign_in_as(@member) + + get edit_user_profile_path(@member) + + assert_select "a[href=?]", users_path, count: 0 + end + + test "another user's profile page links back to the users list" do + sign_in_as(@member) + + get user_profile_path(@other) + + assert_select "a[href=?]", users_path, text: I18n.t("common.back", locale: @member.locale) + end end diff --git a/test/integration/users_index_test.rb b/test/integration/users_index_test.rb index 71e523b2..80496728 100644 --- a/test/integration/users_index_test.rb +++ b/test/integration/users_index_test.rb @@ -19,4 +19,20 @@ class UsersIndexTest < ActionDispatch::IntegrationTest assert_select "button.button-danger", count: 0 assert_select "main > header > h1", count: 1 end + + test "your own role box is disabled" do + sign_in_as(users(:admin)) + + get users_path + + assert_select "input##{dom_id(users(:admin), :role)}[disabled]" + assert_select "input##{dom_id(users(:member), :role)}:not([disabled])" + end + + test "a member sees every role box disabled" do + get users_path + + assert_select "input[type=checkbox][id^=role_user_]", minimum: 2 + assert_select "input[type=checkbox][id^=role_user_]:not([disabled])", count: 0 + end end diff --git a/test/system/user_roles_test.rb b/test/system/user_roles_test.rb new file mode 100644 index 00000000..5d45ff74 --- /dev/null +++ b/test/system/user_roles_test.rb @@ -0,0 +1,36 @@ +require "application_system_test_case" + +class UserRolesTest < ApplicationSystemTestCase + setup do + @admin = users(:admin) + @member = users(:member) + sign_in_as(@admin) + end + + test "an administrator makes a member an administrator by ticking the role box" do + visit users_url + + find("label[for='#{dom_id(@member, :role)}']").click + + assert_selector "label[for='#{dom_id(@member, :role)}']", + text: I18n.t("users.user.role_administrator", locale: locale) + assert_predicate @member.reload, :administrator? + end + + test "an administrator makes another administrator a member by unticking the role box" do + @member.update!(role: :administrator) + visit users_url + + find("label[for='#{dom_id(@member, :role)}']").click + + assert_selector "label[for='#{dom_id(@member, :role)}']", + text: I18n.t("users.user.role_member", locale: locale) + assert_predicate @member.reload, :member? + end + + private + + def dom_id(record, prefix) + ActionView::RecordIdentifier.dom_id(record, prefix) + end +end