Skip to content

Commit

Permalink
Enable bulk editing of transactions (#846)
Browse files Browse the repository at this point in the history
  • Loading branch information
zachgoll authored Jun 7, 2024
1 parent d3f9be1 commit a681e73
Show file tree
Hide file tree
Showing 7 changed files with 136 additions and 14 deletions.
11 changes: 7 additions & 4 deletions app/controllers/transactions_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -57,13 +57,16 @@ def bulk_delete
redirect_to transactions_url, notice: t(".success", count: destroyed.count)
end

def bulk_edit
end

def bulk_update
transactions = Current.family.transactions.where(id: bulk_update_params[:transaction_ids])
updates = bulk_update_params.except(:transaction_ids)
if transactions.update_all(bulk_update_params.except(:transaction_ids).to_h)
if transactions.update_all(bulk_update_params.except(:transaction_ids).to_h.compact_blank!)
redirect_to transactions_url, notice: t(".success", count: transactions.count)
else
render :index, status: :unprocessable_entity, notice: t(".failure")
flash.now[:error] = t(".failure")
render :index, status: :unprocessable_entity
end
end

Expand All @@ -90,7 +93,7 @@ def bulk_delete_params
end

def bulk_update_params
params.require(:bulk_update).permit(:category_id, :excluded, :currency, tag_ids: [], transaction_ids: [])
params.require(:bulk_update).permit(:date, :notes, :excluded, :category_id, :merchant_id, transaction_ids: [])
end

def search_params
Expand Down
17 changes: 13 additions & 4 deletions app/javascript/controllers/bulk_select_controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import {Controller} from "@hotwired/stimulus"

// Connects to data-controller="bulk-select"
export default class extends Controller {
static targets = ["row", "group", "selectionBar", "selectionBarText"]
static targets = ["row", "group", "selectionBar", "selectionBarText", "bulkEditDrawerTitle"]
static values = {
resource: String,
selectedIds: {type: Array, default: []}
Expand All @@ -18,9 +18,14 @@ export default class extends Controller {
document.removeEventListener("turbo:load", this.#updateView)
}

submitBulkDeletionRequest(e) {
bulkEditDrawerTitleTargetConnected(element) {
element.innerText = `Edit ${this.selectedIdsValue.length} ${this.#pluralizedResourceName()}`
}

submitBulkRequest(e) {
const form = e.target.closest("form");
this.#addHiddenFormInputsForSelectedIds(form, "bulk_delete[transaction_ids][]", this.selectedIdsValue)
const scope = e.params.scope
this.#addHiddenFormInputsForSelectedIds(form, `${scope}[transaction_ids][]`, this.selectedIdsValue)
form.requestSubmit()
}

Expand Down Expand Up @@ -96,11 +101,15 @@ export default class extends Controller {

#updateSelectionBar() {
const count = this.selectedIdsValue.length
this.selectionBarTextTarget.innerText = `${count} ${this.resourceValue}${count === 1 ? "" : "s"} selected`
this.selectionBarTextTarget.innerText = `${count} ${this.#pluralizedResourceName()} selected`
this.selectionBarTarget.hidden = count === 0
this.selectionBarTarget.querySelector("input[type='checkbox']").checked = count > 0
}

#pluralizedResourceName() {
return `${this.resourceValue}${this.selectedIdsValue.length === 1 ? "" : "s"}`
}

#updateGroups() {
this.groupTargets.forEach(group => {
const rows = this.rowTargets.filter(row => group.contains(row))
Expand Down
9 changes: 7 additions & 2 deletions app/views/transactions/_selection_bar.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,17 @@
</div>

<div class="flex items-center gap-1 text-gray-500">
<%= button_to "#", disabled: true, class: "cursor-not-allowed p-1.5 group hover:bg-gray-700 flex items-center justify-center rounded-md", title: "Edit" do %>
<%= turbo_frame_tag "bulk_transaction_edit_drawer" %>
<%= link_to bulk_edit_transactions_path,
class: "p-1.5 group hover:bg-gray-700 flex items-center justify-center rounded-md",
title: "Edit",
data: { turbo_frame: "bulk_transaction_edit_drawer" } do %>
<%= lucide_icon "pencil-line", class: "w-5 group-hover:text-white" %>
<% end %>
<%= form_with url: bulk_delete_transactions_path, builder: ActionView::Helpers::FormBuilder, data: { turbo_confirm: true } do %>
<button type="button" data-action="bulk-select#submitBulkDeletionRequest" class="p-1.5 group hover:bg-gray-700 flex items-center justify-center rounded-md" title="Delete">
<button type="button" data-bulk-select-scope-param="bulk_delete" data-action="bulk-select#submitBulkRequest" class="p-1.5 group hover:bg-gray-700 flex items-center justify-center rounded-md" title="Delete">
<%= lucide_icon "trash-2", class: "w-5 group-hover:text-white" %>
</button>
<% end %>
Expand Down
80 changes: 80 additions & 0 deletions app/views/transactions/bulk_edit.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
<%= turbo_frame_tag "bulk_transaction_edit_drawer" do %>
<dialog data-controller="modal"
data-action="click->modal#clickOutside"
class="bg-white border border-alpha-black-25 rounded-2xl max-h-[calc(100vh-32px)] max-w-[480px] w-full shadow-xs h-full mt-4 mr-4">
<%= form_with url: bulk_update_transactions_path, scope: "bulk_update", html: { class: "h-full" }, data: { turbo_frame: "_top" } do |form| %>
<div class="flex h-full flex-col justify-between p-4">
<div>
<div class="flex h-9 items-center justify-end">
<div data-action="click->modal#close" class="cursor-pointer">
<%= lucide_icon("x", class: "w-5 h-5 shrink-0") %>
</div>
</div>

<div class="flex flex-col overflow-scroll">
<div>
<header class="mb-4 space-y-1">
<h3 class="text-2xl font-medium" data-bulk-select-target="bulkEditDrawerTitle">
Edit transactions
</h3>
</header>

<div class="space-y-2">
<details class="group space-y-2" open>
<summary class="flex list-none items-center justify-between rounded-xl px-3 py-2 text-xs font-medium uppercase text-gray-500 bg-gray-25 focus-visible:outline-none">
<h4><%= t(".overview") %></h4>
<%= lucide_icon "chevron-down", class: "group-open:transform group-open:rotate-180 text-gray-500 w-5" %>
</summary>

<div class="pb-6 space-y-2">
<%= form.date_field :date, label: t(".date"), max: Date.current %>
<%= form.collection_select :category_id, Current.family.transaction_categories, :id, :name, { prompt: t(".select_category"), label: t(".category"), class: "text-gray-400" } %>
<%= form.collection_select :merchant_id, Current.family.transaction_merchants, :id, :name, { prompt: t(".select_merchant"), label: t(".merchant"), class: "text-gray-400" } %>
</div>
</details>

<details class="group space-y-2" open>
<summary class="flex list-none items-center justify-between rounded-xl px-3 py-2 text-xs font-medium uppercase text-gray-500 bg-gray-25 focus-visible:outline-none">
<h4><%= t(".additional") %></h4>
<%= lucide_icon "chevron-down", class: "group-open:transform group-open:rotate-180 text-gray-500 w-5" %>
</summary>

<div>
<%= form.text_area :notes, label: t(".note"), placeholder: t(".note_placeholder"), rows: 5 %>
</div>
</details>

<details class="group space-y-2" open>
<summary class="flex list-none items-center justify-between rounded-xl px-3 py-2 text-xs font-medium uppercase text-gray-500 bg-gray-25 focus-visible:outline-none">
<h4><%= t(".settings") %></h4>
<%= lucide_icon "chevron-down", class: "group-open:transform group-open:rotate-180 text-gray-500 w-5" %>
</summary>

<div class="flex cursor-pointer items-center justify-between gap-4 p-3 pb-6">
<div class="text-sm space-y-1">
<h4 class="text-gray-900"><%= t(".exclude_title") %></h4>
<p class="text-gray-500"><%= t(".exclude_subtitle") %></p>
</div>

<div class="relative inline-block select-none">
<%= form.check_box :excluded, class: "sr-only peer" %>
<label for="bulk_update_excluded" class="maybe-switch"></label>
</div>
</div>
</details>
</div>
</div>
</div>
</div>
<div class="flex justify-end items-center gap-2">
<%= link_to t(".cancel"), transactions_path, class: "text-sm font-medium text-gray-900 px-3 py-2" %>
<%= tag.button t(".save"),
type: "button",
data: { "bulk-select-scope-param": "bulk_update", action: "bulk-select#submitBulkRequest" },
class: "px-3 py-2 bg-gray-900 text-white text-sm font-medium rounded-lg" %>
</div>
</div>
<% end %>
</dialog>
<% end %>
16 changes: 16 additions & 0 deletions config/locales/views/transactions/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,22 @@ en:
transactions:
bulk_delete:
success: "%{count} transactions deleted"
bulk_edit:
additional: Additional
cancel: Cancel
category: Category
date: Date
exclude_subtitle: This excludes the transaction from any in-app features or
analytics.
exclude_title: Exclude transaction
merchant: Merchant
note: Notes
note_placeholder: Enter a note that will be applied to selected transactions
overview: Overview
save: Save
select_category: Select a category
select_merchant: Select a merchant
settings: Settings
bulk_update:
failure: Could not update transactions
success: "%{count} transactions updated"
Expand Down
1 change: 1 addition & 0 deletions config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
resources :transactions do
collection do
post "bulk_delete"
get "bulk_edit"
post "bulk_update"

scope module: :transactions, as: :transaction do
Expand Down
16 changes: 12 additions & 4 deletions test/controllers/transactions_controller_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -157,25 +157,33 @@ class TransactionsControllerTest < ActionDispatch::IntegrationTest
transactions = @user.family.transactions.ordered.limit(20)

transactions.each do |transaction|
transaction.update! excluded: false, currency: "USD", category_id: Transaction::Category.first.id
transaction.update! \
excluded: false,
category_id: Transaction::Category.first.id,
merchant_id: Transaction::Merchant.first.id,
notes: "Starting note"
end

post bulk_update_transactions_url, params: {
bulk_update: {
date: Date.current,
transaction_ids: transactions.map(&:id),
excluded: true,
currency: "CAD",
category_id: Transaction::Category.second.id
category_id: Transaction::Category.second.id,
merchant_id: Transaction::Merchant.second.id,
notes: "Updated note"
}
}

assert_redirected_to transactions_url
assert_equal "#{transactions.count} transactions updated", flash[:notice]

transactions.reload.each do |transaction|
assert_equal Date.current, transaction.date
assert transaction.excluded
assert_equal "CAD", transaction.currency
assert_equal Transaction::Category.second, transaction.category
assert_equal Transaction::Merchant.second, transaction.merchant
assert_equal "Updated note", transaction.notes
end
end
end

0 comments on commit a681e73

Please sign in to comment.