mirror of
https://github.com/maybe-finance/maybe.git
synced 2025-08-09 15:35:22 +02:00
WIP
This commit is contained in:
parent
63bc1c4bcb
commit
9b6a14fb6f
5 changed files with 195 additions and 17 deletions
|
@ -55,6 +55,11 @@ module ImportsHelper
|
|||
[ base, border ].join(" ")
|
||||
end
|
||||
|
||||
def cell_is_valid?(row, field)
|
||||
row.valid? # populate errors
|
||||
!row.errors.key?(field)
|
||||
end
|
||||
|
||||
private
|
||||
def permitted_import_types
|
||||
%w[transaction_import trade_import account_import mint_import]
|
||||
|
|
149
app/javascript/controllers/mobile_cell_interaction_controller.js
Normal file
149
app/javascript/controllers/mobile_cell_interaction_controller.js
Normal file
|
@ -0,0 +1,149 @@
|
|||
import { Controller } from "@hotwired/stimulus";
|
||||
|
||||
// Connects to data-controller="mobile-cell-interaction"
|
||||
export default class extends Controller {
|
||||
static targets = ["field", "highlight", "errorTooltip", "errorIcon"];
|
||||
static values = { error: String };
|
||||
|
||||
touchTimeout = null;
|
||||
activeTooltip = null;
|
||||
documentClickHandler = null;
|
||||
|
||||
connect() {
|
||||
this.documentClickHandler = this.handleDocumentClick.bind(this);
|
||||
document.addEventListener('click', this.documentClickHandler);
|
||||
}
|
||||
|
||||
disconnect() {
|
||||
if (this.documentClickHandler) {
|
||||
document.removeEventListener('click', this.documentClickHandler);
|
||||
}
|
||||
}
|
||||
|
||||
handleDocumentClick(event) {
|
||||
if (event.target.closest('[data-mobile-cell-interaction-target="errorTooltip"]') ||
|
||||
event.target.closest('[data-mobile-cell-interaction-target="errorIcon"]')) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.hideAllErrorTooltips();
|
||||
}
|
||||
|
||||
highlightCell(event) {
|
||||
const field = event.target;
|
||||
const highlight = this.findHighlightForField(field);
|
||||
if (highlight) {
|
||||
highlight.style.opacity = '1';
|
||||
}
|
||||
}
|
||||
|
||||
unhighlightCell(event) {
|
||||
const field = event.target;
|
||||
const highlight = this.findHighlightForField(field);
|
||||
if (highlight) {
|
||||
highlight.style.opacity = '0';
|
||||
}
|
||||
|
||||
this.hideAllErrorTooltips();
|
||||
}
|
||||
|
||||
handleCellTouch(event) {
|
||||
if (this.touchTimeout) {
|
||||
clearTimeout(this.touchTimeout);
|
||||
}
|
||||
|
||||
const field = event.target;
|
||||
|
||||
const highlight = this.findHighlightForField(field);
|
||||
if (highlight) {
|
||||
highlight.style.opacity = '1';
|
||||
|
||||
this.touchTimeout = window.setTimeout(() => {
|
||||
if (document.activeElement !== field) {
|
||||
highlight.style.opacity = '0';
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
if (this.hasErrorValue && this.errorValue) {
|
||||
this.showErrorTooltip();
|
||||
}
|
||||
}
|
||||
|
||||
toggleErrorMessage(event) {
|
||||
const errorIcon = event.currentTarget;
|
||||
const cellContainer = errorIcon.closest('div');
|
||||
const field = cellContainer.querySelector('input');
|
||||
|
||||
if (field) {
|
||||
field.focus();
|
||||
}
|
||||
|
||||
const tooltip = this.errorTooltipTarget;
|
||||
|
||||
this.hideAllTooltipsExcept(tooltip);
|
||||
|
||||
if (tooltip.classList.contains('hidden')) {
|
||||
tooltip.classList.remove('hidden');
|
||||
this.activeTooltip = tooltip;
|
||||
|
||||
setTimeout(() => {
|
||||
if (tooltip === this.activeTooltip) {
|
||||
tooltip.classList.add('hidden');
|
||||
this.activeTooltip = null;
|
||||
}
|
||||
}, 3000);
|
||||
} else {
|
||||
tooltip.classList.add('hidden');
|
||||
this.activeTooltip = null;
|
||||
}
|
||||
|
||||
event.stopPropagation();
|
||||
}
|
||||
|
||||
showErrorTooltip() {
|
||||
if (this.hasErrorTooltipTarget) {
|
||||
const tooltip = this.errorTooltipTarget;
|
||||
tooltip.classList.remove('hidden');
|
||||
this.activeTooltip = tooltip;
|
||||
|
||||
setTimeout(() => {
|
||||
if (tooltip === this.activeTooltip) {
|
||||
tooltip.classList.add('hidden');
|
||||
this.activeTooltip = null;
|
||||
}
|
||||
}, 3000);
|
||||
}
|
||||
}
|
||||
|
||||
hideAllErrorTooltips() {
|
||||
document.querySelectorAll('[data-mobile-cell-interaction-target="errorTooltip"]').forEach(tooltip => {
|
||||
tooltip.classList.add('hidden');
|
||||
});
|
||||
this.activeTooltip = null;
|
||||
}
|
||||
|
||||
hideAllTooltipsExcept(tooltipToKeep) {
|
||||
document.querySelectorAll('[data-mobile-cell-interaction-target="errorTooltip"]').forEach(tooltip => {
|
||||
if (tooltip !== tooltipToKeep) {
|
||||
tooltip.classList.add('hidden');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
selectCell(event) {
|
||||
const errorIcon = event.currentTarget;
|
||||
const cellContainer = errorIcon.closest('div');
|
||||
const field = cellContainer.querySelector('input');
|
||||
|
||||
if (field) {
|
||||
field.focus();
|
||||
event.stopPropagation();
|
||||
}
|
||||
}
|
||||
|
||||
findHighlightForField(field) {
|
||||
const container = field.closest('div');
|
||||
return container ? container.querySelector('[data-mobile-cell-interaction-target="highlight"]') : null;
|
||||
}
|
||||
}
|
|
@ -20,16 +20,17 @@
|
|||
<%= link_to "Next step", import_confirm_path(@import), class: "btn btn--primary" %>
|
||||
</div>
|
||||
<% else %>
|
||||
<div class="bg-container border border-tertiary rounded-lg p-3 flex items-center justify-between">
|
||||
<div class="bg-container border border-tertiary rounded-lg p-3 flex flex-col md:flex-row items-start md:items-center justify-between gap-3 md:gap-0">
|
||||
<div class="flex items-center gap-2">
|
||||
<%= lucide_icon "alert-triangle", class: "w-4 h-4 text-red-500" %>
|
||||
<p class="text-red-500 text-sm"><%= t(".errors_notice") %></p>
|
||||
<%= lucide_icon "alert-triangle", class: "w-4 h-4 text-red-500 flex-shrink-0" %>
|
||||
<p class="text-red-500 text-sm hidden md:block"><%= t(".errors_notice") %></p>
|
||||
<p class="text-red-500 text-sm md:hidden"><%= t(".errors_notice_mobile") %></p>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-center">
|
||||
<div class="bg-gray-50 rounded-lg inline-flex p-1 space-x-2 text-sm text-primary font-medium">
|
||||
<%= link_to "All rows", import_clean_path(@import, per_page: params[:per_page], view: "all"), class: "p-2 rounded-lg #{params[:view] != 'errors' ? 'bg-container' : ''}" %>
|
||||
<%= link_to "Error rows", import_clean_path(@import, per_page: params[:per_page], view: "errors"), class: "p-2 rounded-lg #{params[:view] == 'errors' ? 'bg-container' : ''}" %>
|
||||
<div class="flex justify-center w-full md:w-auto">
|
||||
<div class="bg-gray-50 rounded-lg inline-flex p-1 space-x-2 text-sm text-primary font-medium w-full md:w-auto">
|
||||
<%= link_to "All rows", import_clean_path(@import, per_page: params[:per_page], view: "all"), class: "p-2 rounded-lg flex-1 md:flex-auto text-center #{params[:view] != 'errors' ? 'bg-container' : ''}" %>
|
||||
<%= link_to "Error rows", import_clean_path(@import, per_page: params[:per_page], view: "errors"), class: "p-2 rounded-lg flex-1 md:flex-auto text-center #{params[:view] == 'errors' ? 'bg-container' : ''}" %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -9,18 +9,39 @@
|
|||
url: import_row_path(row.import, row),
|
||||
method: :patch,
|
||||
data: {
|
||||
controller: "auto-submit-form",
|
||||
auto_submit_form_trigger_event_value: "blur"
|
||||
controller: "auto-submit-form mobile-cell-interaction",
|
||||
auto_submit_form_trigger_value: "blur",
|
||||
mobile_cell_interaction_error_value: !cell_is_valid?(row, key) ? row.errors[key].join(", ") : "",
|
||||
}
|
||||
) do |form| %>
|
||||
<%= form.text_field key,
|
||||
"data-auto-submit-form-target": "auto",
|
||||
class: [
|
||||
cell_class(row, key),
|
||||
idx == 0 ? "group-first:rounded-tl-lg group-last:rounded-bl-lg" : "",
|
||||
idx == row.import.column_keys.count - 1 ? "group-first:rounded-tr-lg group-last:rounded-br-lg" : "",
|
||||
],
|
||||
disabled: row.import.complete? %>
|
||||
<div class="relative">
|
||||
<%= form.text_field key,
|
||||
"data-auto-submit-form-target": "auto",
|
||||
"data-action": "focus->mobile-cell-interaction#highlightCell blur->mobile-cell-interaction#unhighlightCell touchstart->mobile-cell-interaction#handleCellTouch",
|
||||
"data-mobile-cell-interaction-target": "field",
|
||||
class: [
|
||||
cell_class(row, key),
|
||||
idx == 0 ? "group-first:rounded-tl-lg group-last:rounded-bl-lg" : "",
|
||||
idx == row.import.column_keys.count - 1 ? "group-first:rounded-tr-lg group-last:rounded-br-lg" : "",
|
||||
"focus:outline-none focus:z-10 relative",
|
||||
],
|
||||
disabled: row.import.complete? %>
|
||||
|
||||
<% if !cell_is_valid?(row, key) %>
|
||||
<span class="absolute right-2 top-1/2 -translate-y-1/2 text-red-500 md:hidden"
|
||||
data-action="click->mobile-cell-interaction#toggleErrorMessage"
|
||||
data-mobile-cell-interaction-target="errorIcon">
|
||||
<%= lucide_icon "alert-circle", class: "w-4 h-4" %>
|
||||
</span>
|
||||
|
||||
<div class="absolute left-4 right-4 bottom-full mb-2 p-2 bg-red-50 border border-red-200 rounded-lg shadow-lg text-xs text-red-600 hidden md:hidden z-20"
|
||||
data-mobile-cell-interaction-target="errorTooltip">
|
||||
<%= row.errors[key].join(", ") %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<div class="absolute inset-0 bg-primary/5 pointer-events-none opacity-0 transition-opacity duration-150 ease-in-out z-0" data-mobile-cell-interaction-target="highlight"></div>
|
||||
</div>
|
||||
<% end %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
|
|
@ -6,6 +6,8 @@ en:
|
|||
description: Edit your data in the table below. Red cells are invalid.
|
||||
errors_notice: You have errors in your data. Hover over the error to see
|
||||
details.
|
||||
errors_notice_mobile: You have errors in your data. Tap over the error tooltip to see
|
||||
details.
|
||||
title: Clean your data
|
||||
configurations:
|
||||
mint_import:
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue