Files
sure/app/javascript/controllers/polling_controller.js
Juan José Mata 81cdccb768 [codex] Complete Sophtron account mapping (#1698)
* Complete Sophtron account mapping

* Clarify Sophtron login challenge flow

* Add Sophtron connection UI timeout

* Treat Sophtron timeout jobs as failed

* Reset failed Sophtron connection state

* Handle stale Sophtron connection jobs

* Advance Sophtron polling timeout

* Shorten Sophtron connection timeout

* Fix Sophtron modal polling updates

* Stabilize Sophtron MFA polling

* Give Sophtron OTP challenges more time

* Clarify Sophtron institution login failures

* Extend Sophtron polling during login progress

* Probe Sophtron accounts after completed MFA step

* Align Sophtron dialogs with design system

* Start Sophtron initial load after linking accounts

* Fix Sophtron initial transaction load

* Fail Sophtron sync without institution connection

* Fix tests

* Wrap Sophtron account linking in transaction

* Wrap Sophtron provider responses

* Fix Sophtron MFA security tests

* Guard Sophtron MFA challenge arrays

* Respect Sophtron initial load window

* Use unique Sophtron MFA answer field ids

* Address Sophtron review follow-ups

* Fix Sophtron transaction sync refresh

* Avoid blocking Sophtron refresh polling

* Move Sophtron account helpers to model

* Keep Sophtron grouping provider-level

* Start new Sophtron institution links

* Isolate Sophtron institution connections

---------

Signed-off-by: Juan José Mata <juanjo.mata@gmail.com>
2026-05-08 15:15:23 +02:00

115 lines
2.7 KiB
JavaScript

import { Controller } from "@hotwired/stimulus";
// Connects to data-controller="polling"
// Automatically refreshes a turbo frame at a specified interval
export default class extends Controller {
static values = {
url: String,
interval: { type: Number, default: 3000 },
frameId: String,
};
connect() {
this.startPolling();
}
disconnect() {
this.stopPolling();
}
startPolling() {
if (!this.hasUrlValue) return;
this.poll = setInterval(() => {
this.refresh();
}, this.intervalValue);
}
stopPolling() {
if (this.poll) {
clearInterval(this.poll);
this.poll = null;
}
}
async refresh() {
try {
const frame = this.frameElement();
if (!frame) {
this.stopPolling();
return;
}
const response = await fetch(this.urlValue, {
headers: {
Accept: "text/html",
"Turbo-Frame": frame.id,
},
});
if (response.ok) {
const html = await response.text();
const template = document.createElement("template");
template.innerHTML = html;
const newFrame = template.content.querySelector(
`turbo-frame#${this.cssEscape(frame.id)}`,
);
if (newFrame) {
if (frame === this.element) {
this.syncPollingAttributes(newFrame);
}
frame.innerHTML = newFrame.innerHTML;
// Check if we should stop polling (no more pending/processing exports)
if (
frame === this.element &&
!newFrame.hasAttribute("data-polling-url-value")
) {
this.stopPolling();
}
}
}
} catch (error) {
console.error("Polling error:", error);
}
}
frameElement() {
if (this.hasFrameIdValue) {
return document.getElementById(this.frameIdValue);
}
if (this.element.tagName.toLowerCase() === "turbo-frame") {
return this.element;
}
return this.element.closest("turbo-frame");
}
cssEscape(value) {
if (window.CSS?.escape) return CSS.escape(value);
return value.replaceAll('"', '\\"');
}
syncPollingAttributes(newFrame) {
const pollingUrl = newFrame.getAttribute("data-polling-url-value");
const pollingInterval = newFrame.getAttribute(
"data-polling-interval-value",
);
if (pollingUrl) {
this.element.setAttribute("data-polling-url-value", pollingUrl);
} else {
this.element.removeAttribute("data-polling-url-value");
}
if (pollingInterval) {
this.element.setAttribute("data-polling-interval-value", pollingInterval);
} else {
this.element.removeAttribute("data-polling-interval-value");
}
}
}