mirror of
https://github.com/we-promise/sure.git
synced 2026-09-09 16:44:17 +00:00
* feat(rules): support multiple tags in the set transaction tags action Fixes #3353. Reuses the existing DS::TagSelect multi-select tag picker (made generic via attribute:/show_label:) instead of a native <select multiple>, so the UX matches the rest of the app. Multiple tag ids are stored as a comma-separated string in the existing value column, keeping single-tag rows backward compatible with no migration. Also closes a read-modify-write race in SetTransactionTags#execute via with_lock. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * fix(rules): address automated review findings on multi-tag actions - Fix data export/import: multi-tag actions were exported/imported as one opaque comma string, losing all but a bogus combined tag on restore. Each tag id is now resolved/reconstructed independently, with a backward-compatible scalar value_ref for single-tag actions. - Fix N+1 in Rule::Action#value_display (options queried once per tag). - Add aria-label to DS::TagSelect's trigger button when show_label is false, so the control keeps an accessible name. - Localize the "to" label in rule action rows (rules.actions.to_label). - Use a monotonic counter instead of Date.now() for nested form indices, closing a same-millisecond collision window. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * fix(rules): batch tag lookups in multi-tag import resolution Avoids one find_by query per tag name when reconstructing multi-tag rule actions during import. * fix(rules): resolve jjmata review findings on multi-tag action - rules_controller.js: prefix the JS-side nested-form index counter with "new_" so it can never collide with the numeric indexes Rails assigns to already-persisted conditions/actions on an edit form. - data_exporter.rb: key the value_ref scalar/array decision off the number of tag ids on the action, not the number that still resolve, so a partially-orphaned multi-tag action keeps round-tripping as an array. - rule_import.rb: split comma-separated set_transaction_tags values into individual tag names during CSV rule import, matching the batched resolution already used by Family::DataImporter. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * fix(rules): CSV-quote multi-tag names so commas don't split them CodeRabbit flagged that a tag name containing a comma (e.g. "Food, Dining") would be silently split into two tags when round-tripped through the comma-separated multi-tag value/CSV formats used by Family::DataExporter, Family::DataImporter, and RuleImport. Add Rule::Action.encode_multi_value_names/.decode_multi_value_names, backed by Ruby's CSV line quoting, and use them at all three call sites instead of a plain join(",")/split(","). A single name without a comma round-trips byte-identical to before, so existing exports and CSV rule templates are unaffected. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> --------- Co-authored-by: GFR <248542187+gfr-free@users.noreply.github.com> Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
108 lines
3.3 KiB
JavaScript
108 lines
3.3 KiB
JavaScript
import { Controller } from "@hotwired/stimulus";
|
|
|
|
// Connects to data-controller="rule--actions"
|
|
export default class extends Controller {
|
|
static values = { actionExecutors: Array };
|
|
static targets = [
|
|
"destroyField",
|
|
"actionValue",
|
|
"selectTemplate",
|
|
"multiSelectTemplate",
|
|
"textTemplate"
|
|
];
|
|
|
|
remove(e) {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
|
|
if (e.params.destroy) {
|
|
this.destroyFieldTarget.value = true;
|
|
this.element.classList.add("hidden");
|
|
} else {
|
|
this.element.remove();
|
|
}
|
|
}
|
|
|
|
handleActionTypeChange(e) {
|
|
const actionExecutor = this.actionExecutorsValue.find(
|
|
(executor) => executor.key === e.target.value,
|
|
);
|
|
|
|
// Clear any existing input elements first
|
|
this.#clearFormFields();
|
|
|
|
if (actionExecutor.type === "select") {
|
|
this.#buildSelectFor(actionExecutor);
|
|
} else if (actionExecutor.type === "multi_select") {
|
|
this.#buildMultiSelectFor();
|
|
} else if (actionExecutor.type === "text") {
|
|
this.#buildTextInputFor();
|
|
} else {
|
|
// Hide for any type that doesn't need a value (e.g. function)
|
|
this.#hideActionValue();
|
|
}
|
|
}
|
|
|
|
#hideActionValue() {
|
|
this.actionValueTarget.classList.add("hidden");
|
|
}
|
|
|
|
#clearFormFields() {
|
|
// Remove all children from actionValueTarget
|
|
this.actionValueTarget.innerHTML = "";
|
|
}
|
|
|
|
#buildSelectFor(actionExecutor) {
|
|
// Clone the select template
|
|
const template = this.selectTemplateTarget.content.cloneNode(true);
|
|
const selectEl = template.querySelector("select");
|
|
|
|
// Add options to the select element
|
|
if (selectEl) {
|
|
selectEl.innerHTML = "";
|
|
if (!actionExecutor.options || actionExecutor.options.length === 0) {
|
|
selectEl.disabled = true;
|
|
const optionEl = document.createElement("option");
|
|
optionEl.textContent = "(none)";
|
|
selectEl.appendChild(optionEl);
|
|
} else {
|
|
selectEl.disabled = false;
|
|
for (const option of actionExecutor.options) {
|
|
const optionEl = document.createElement("option");
|
|
optionEl.value = option[1];
|
|
optionEl.textContent = option[0];
|
|
selectEl.appendChild(optionEl);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Add the template content to the actionValue target and ensure it's visible
|
|
this.actionValueTarget.appendChild(template);
|
|
this.actionValueTarget.classList.remove("hidden");
|
|
}
|
|
|
|
#buildMultiSelectFor() {
|
|
// The tag list is baked into the server-rendered template (it doesn't
|
|
// depend on the selected executor like other select options do), so we
|
|
// just clone it as-is. The embedded tag-select Stimulus controller
|
|
// connects automatically once the clone is inserted into the DOM.
|
|
const template = this.multiSelectTemplateTarget.content.cloneNode(true);
|
|
|
|
this.actionValueTarget.appendChild(template);
|
|
this.actionValueTarget.classList.remove("hidden");
|
|
}
|
|
|
|
#buildTextInputFor() {
|
|
// Clone the text template
|
|
const template = this.textTemplateTarget.content.cloneNode(true);
|
|
|
|
// Ensure the input is always empty
|
|
const inputEl = template.querySelector("input");
|
|
if (inputEl) inputEl.value = "";
|
|
|
|
// Add the template content to the actionValue target and ensure it's visible
|
|
this.actionValueTarget.appendChild(template);
|
|
this.actionValueTarget.classList.remove("hidden");
|
|
}
|
|
}
|