src/devtools/tabs/fields-tab.tsx

Total Symbols
17
Lines of Code
477
Avg Complexity
5.1
Symbol Types
1

File Relationships

graph LR toggleIgnore["toggleIgnore"] renderFieldsTab["renderFieldsTab"] clearDetectedFields["clearDetectedFields"] detectFieldsStreaming["detectFieldsStreaming"] loadIgnoredFields["loadIgnoredFields"] detectFields["detectFields"] openFieldEditor["openFieldEditor"] closeFieldEditor["closeFieldEditor"] saveFieldRule["saveFieldRule"] deleteFieldRule["deleteFieldRule"] redetectField["redetectField"] fillAll["fillAll"] fillOnlyEmpty["fillOnlyEmpty"] clearForm["clearForm"] fillField["fillField"] inspectElement["inspectElement"] toggleIgnore -->|calls| renderFieldsTab clearDetectedFields -->|calls| renderFieldsTab detectFieldsStreaming -->|calls| renderFieldsTab detectFieldsStreaming -->|calls| loadIgnoredFields detectFields -->|calls| detectFieldsStreaming openFieldEditor -->|calls| renderFieldsTab closeFieldEditor -->|calls| renderFieldsTab saveFieldRule -->|calls| closeFieldEditor deleteFieldRule -->|calls| closeFieldEditor redetectField -->|calls| renderFieldsTab renderFieldsTab -->|calls| detectFields renderFieldsTab -->|calls| fillAll renderFieldsTab -->|calls| fillOnlyEmpty renderFieldsTab -->|calls| clearDetectedFields renderFieldsTab -->|calls| clearForm renderFieldsTab -->|calls| fillField renderFieldsTab -->|calls| inspectElement renderFieldsTab -->|calls| toggleIgnore renderFieldsTab -->|calls| openFieldEditor renderFieldsTab -->|calls| deleteFieldRule renderFieldsTab -->|calls| redetectField click toggleIgnore "../symbols/71b50bf6b86c3dc6.html" click renderFieldsTab "../symbols/f02a4b6eabef0223.html" click clearDetectedFields "../symbols/ccc4798cfa4d2145.html" click detectFieldsStreaming "../symbols/36f1a94dcf5fea71.html" click loadIgnoredFields "../symbols/7c1a2c45e97bbec4.html" click detectFields "../symbols/2645a01d8f49e548.html" click openFieldEditor "../symbols/c0b14fa197075d23.html" click closeFieldEditor "../symbols/e581d997c0f3219a.html" click saveFieldRule "../symbols/1c13614bd05fa7c0.html" click deleteFieldRule "../symbols/21d6aa8ad41f157f.html" click redetectField "../symbols/2fb93ff9ffcb332e.html" click fillAll "../symbols/388f033c1be0f292.html" click fillOnlyEmpty "../symbols/84fc64fe499c6732.html" click clearForm "../symbols/cd75d039d4d0625c.html" click fillField "../symbols/1988a97c342d5f76.html" click inspectElement "../symbols/f30dc54e39b7e43e.html"

Architecture violations

View all

  • [warning] max-cyclomatic-complexity: 'detectFieldsStreaming' has cyclomatic complexity 25 (max 10)
  • [warning] max-lines: 'detectFieldsStreaming' has 136 lines (max 80)

Symbols by Kind

function 17

All Symbols

Name Kind Visibility Status Lines Signature
fillAll function exported- 37-48 fillAll(): : Promise<void>
fillOnlyEmpty function exported- 50-61 fillOnlyEmpty(): : Promise<void>
fillContextualAI function exported- 63-82 fillContextualAI(): : Promise<void>
fillField function exported- 84-99 fillField(selector: string): : Promise<void>
inspectElement function exported- 103-109 inspectElement(selector: string): : void
loadIgnoredFields function exported- 113-124 loadIgnoredFields(): : Promise<void>
toggleIgnore function exported- 126-165 toggleIgnore( selector: string, label: string, ): : Promise<void>
clearDetectedFields function exported- 169-174 clearDetectedFields(): : Promise<void>
clearForm function exported- 176-184 clearForm(): : Promise<void>
detectFieldsStreaming function - 188-323 detectFieldsStreaming(): : Promise<void>
detectFields function exported- 325-327 detectFields(): : Promise<void>
openFieldEditor function exported- 331-358 openFieldEditor( field: DetectedFieldSummary, ): : Promise<void>
closeFieldEditor function exported- 360-364 closeFieldEditor(): : void
saveFieldRule function exported- 366-393 saveFieldRule( payload: FieldEditorSavePayload, ): : Promise<void>
deleteFieldRule function exported- 395-411 deleteFieldRule(): : Promise<void>
redetectField function exported- 413-447 redetectField(selector: string): : Promise<void>
renderFieldsTab function exported- 451-476 renderFieldsTab(): : void

Full Source

/**
 * Fields Tab — Field detection, filling, ignoring, and inspection UI.
 *
 * Responsibilities:
 * - Streaming field detection (real-time row insertion)
 * - Fill all / fill empty / fill single field
 * - Ignore / un-ignore fields
 * - Inspect element in DevTools
 * - Clear detected fields / clear form
 */

import { h } from "preact";
import type {
  DetectedFieldSummary,
  ExtensionMessage,
  FieldRule,
  IgnoredField,
  StreamedFieldMessage,
} from "@/types";
import { openAIContextModal } from "@/popup/popup-ai-context-modal";
import { t } from "@/lib/i18n";
import { panelState } from "../panel-state";
import {
  sendToPage,
  sendToBackground,
  getInspectedUrl,
} from "../panel-messaging";
import { addLog, updateStatusBar } from "../panel-utils";
import { renderTo, FieldsTabView } from "@/lib/ui/components";
import type {
  FieldEditorSavePayload,
  GeneratorOption,
} from "@/lib/ui/components/field-editor-modal";

// ── Fill Operations ───────────────────────────────────────────────────────────

export async function fillAll(): Promise<void> {
  addLog(t("logFilling"));
  try {
    const result = (await sendToPage({
      type: "FILL_ALL_FIELDS",
      payload: { fillEmptyOnly: false },
    })) as { filled?: number };
    addLog(`${result?.filled ?? 0} ${t("filled")}`, "success");
  } catch (err) {
    addLog(`Erro ao preencher: ${err}`, "error");
  }
}

export async function fillOnlyEmpty(): Promise<void> {
  addLog(t("logFillingEmpty"));
  try {
    const result = (await sendToPage({
      type: "FILL_ALL_FIELDS",
      payload: { fillEmptyOnly: true },
    })) as { filled?: number };
    addLog(`${result?.filled ?? 0} ${t("filled")}`, "success");
  } catch (err) {
    addLog(`Erro ao preencher: ${err}`, "error");
  }
}

export async function fillContextualAI(): Promise<void> {
  const context = await openAIContextModal();
  if (!context) return;

  addLog(t("fillContextualAI"));
  const btn = document.getElementById("btn-fill-contextual-ai");
  const label = btn?.querySelector(".card-label");
  if (label) label.textContent = "⏳...";
  try {
    const result = (await sendToPage({
      type: "FILL_CONTEXTUAL_AI",
      payload: context,
    })) as { filled?: number };
    addLog(`${result?.filled ?? 0} ${t("filled")}`, "success");
  } catch (err) {
    addLog(`Erro ao preencher com IA: ${err}`, "error");
  } finally {
    if (label) label.textContent = t("fillContextualAI");
  }
}

export async function fillField(selector: string): Promise<void> {
  addLog(`Preenchendo: ${selector}`);
  try {
    const result = (await sendToPage({
      type: "FILL_FIELD_BY_SELECTOR",
      payload: selector,
    })) as { error?: string };
    if (result?.error) {
      addLog(`Erro: ${result.error}`, "error");
    } else {
      addLog(`Campo preenchido: ${selector}`, "success");
    }
  } catch (err) {
    addLog(`Erro: ${err}`, "error");
  }
}

// ── Inspect ───────────────────────────────────────────────────────────────────

export function inspectElement(selector: string): void {
  const escaped = selector.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
  chrome.devtools.inspectedWindow.eval(
    `inspect(document.querySelector('${escaped}'))`,
  );
  addLog(`Inspecionando: ${selector}`);
}

// ── Ignored Fields ────────────────────────────────────────────────────────────

export async function loadIgnoredFields(): Promise<void> {
  try {
    const result = (await sendToBackground({
      type: "GET_IGNORED_FIELDS",
    })) as IgnoredField[] | { error?: string };
    if (Array.isArray(result)) {
      panelState.ignoredSelectors = new Set(result.map((f) => f.selector));
    }
  } catch {
    // silent
  }
}

export async function toggleIgnore(
  selector: string,
  label: string,
): Promise<void> {
  const isIgnored = panelState.ignoredSelectors.has(selector);

  try {
    const pageUrl = await getInspectedUrl();
    const origin = new URL(pageUrl).origin;
    const urlPattern = `${origin}/*`;

    if (isIgnored) {
      const allIgnored = (await sendToBackground({
        type: "GET_IGNORED_FIELDS",
      })) as IgnoredField[];
      const entry = Array.isArray(allIgnored)
        ? allIgnored.find((f) => f.selector === selector)
        : null;
      if (entry) {
        await sendToBackground({
          type: "REMOVE_IGNORED_FIELD",
          payload: entry.id,
        });
        panelState.ignoredSelectors.delete(selector);
        addLog(`${t("logFieldReactivated")}: ${label}`, "info");
      }
    } else {
      await sendToBackground({
        type: "ADD_IGNORED_FIELD",
        payload: { urlPattern, selector, label },
      });
      panelState.ignoredSelectors.add(selector);
      addLog(`${t("logFieldIgnored")}: ${label}`, "warn");
    }
  } catch (err) {
    addLog(`Erro ao alternar ignore: ${err}`, "error");
  }

  if (panelState.activeTab === "fields") renderFieldsTab();
}

// ── Clear ─────────────────────────────────────────────────────────────────────

export async function clearDetectedFields(): Promise<void> {
  panelState.detectedFields = [];
  addLog("Campos detectados limpos", "info");
  if (panelState.activeTab === "fields") renderFieldsTab();
  updateStatusBar();
}

export async function clearForm(): Promise<void> {
  addLog("Limpando formulário...", "info");
  try {
    await sendToPage({ type: "CLEAR_FORM", payload: undefined });
    addLog("✓ Formulário limpo com sucesso", "success");
  } catch (err) {
    addLog(`✗ Erro ao limpar formulário: ${err}`, "error");
  }
}

// ── Detect (Streaming) ────────────────────────────────────────────────────────

async function detectFieldsStreaming(): Promise<void> {
  panelState.detectedFields = [];
  panelState.isDetecting = true;
  addLog(t("logDetecting"));

  if (panelState.activeTab === "fields") renderFieldsTab();

  try {
    const STREAM_IDLE_TIMEOUT_MS = 4000;

    let detectionComplete = false;
    let receivedAnyMessage = false;
    let streamIdleTimeoutId: number | null = null;

    const clearStreamIdleTimeout = (): void => {
      if (streamIdleTimeoutId !== null) {
        window.clearTimeout(streamIdleTimeoutId);
        streamIdleTimeoutId = null;
      }
    };

    const fallbackDetectOnce = async (): Promise<void> => {
      const result = (await sendToPage({ type: "DETECT_FIELDS" })) as {
        fields?: DetectedFieldSummary[];
        error?: string;
      };

      if (result?.error) {
        addLog(`Erro ao detectar: ${result.error}`, "error");
        return;
      }

      panelState.detectedFields = Array.isArray(result?.fields)
        ? result.fields
        : [];
      addLog(
        `${panelState.detectedFields.length} ${t("fieldsDetected")}`,
        "success",
      );
    };

    const finalizeDetection = (
      port: chrome.runtime.Port,
      options?: { warning?: string },
    ): void => {
      if (detectionComplete) return;
      detectionComplete = true;
      clearStreamIdleTimeout();
      panelState.isDetecting = false;

      if (options?.warning) addLog(options.warning, "warn");

      if (panelState.activeTab === "fields") renderFieldsTab();

      try {
        port.disconnect();
      } catch {
        // no-op
      }
    };

    const scheduleStreamIdleFinalization = (
      port: chrome.runtime.Port,
    ): void => {
      clearStreamIdleTimeout();
      streamIdleTimeoutId = window.setTimeout(() => {
        if (!detectionComplete && receivedAnyMessage) {
          finalizeDetection(port, {
            warning: "Detecção finalizada por inatividade do stream",
          });
          addLog(
            `${panelState.detectedFields.length} ${t("fieldsDetected")}`,
            "success",
          );
        }
      }, STREAM_IDLE_TIMEOUT_MS);
    };

    const port = chrome.tabs.connect(panelState.inspectedTabId, {
      name: "field-detection-stream",
    });

    scheduleStreamIdleFinalization(port);

    port.onMessage.addListener((message: StreamedFieldMessage) => {
      receivedAnyMessage = true;
      scheduleStreamIdleFinalization(port);

      if (message.type === "field" && message.field) {
        panelState.detectedFields.push(message.field);
        if (panelState.activeTab === "fields") renderFieldsTab();
      } else if (message.type === "complete") {
        addLog(
          `${panelState.detectedFields.length} ${t("fieldsDetected")}`,
          "success",
        );
        finalizeDetection(port);
      } else if (message.type === "error") {
        addLog(`Erro ao detectar: ${message.error}`, "error");
        finalizeDetection(port);
      }
    });

    port.onDisconnect.addListener(() => {
      clearStreamIdleTimeout();
      if (!detectionComplete) {
        const reason = chrome.runtime.lastError?.message;
        addLog(
          reason
            ? `Conexão perdida durante detecção: ${reason}`
            : "Conexão perdida durante detecção",
          "warn",
        );
        if (!receivedAnyMessage) {
          void fallbackDetectOnce().finally(() => {
            detectionComplete = true;
            panelState.isDetecting = false;
            if (panelState.activeTab === "fields") renderFieldsTab();
          });
          return;
        }
        detectionComplete = true;
        panelState.isDetecting = false;
        if (panelState.activeTab === "fields") renderFieldsTab();
      }
    });
  } catch (err) {
    addLog(`Erro ao detectar: ${err}`, "error");
    panelState.detectedFields = [];
    panelState.isDetecting = false;
    if (panelState.activeTab === "fields") renderFieldsTab();
  }

  await loadIgnoredFields();
  updateStatusBar();
}

export async function detectFields(): Promise<void> {
  await detectFieldsStreaming();
}

// ── Field Editor ──────────────────────────────────────────────────────────────

export async function openFieldEditor(
  field: DetectedFieldSummary,
): Promise<void> {
  panelState.editingField = field;
  panelState.editingFieldExistingRule = null;

  try {
    const rules = (await sendToBackground({
      type: "GET_RULES",
    })) as FieldRule[] | null;
    if (Array.isArray(rules)) {
      const existing = rules.find((r) => r.fieldSelector === field.selector);
      if (existing) {
        panelState.editingFieldExistingRule = {
          fieldType: existing.fieldType,
          generator: (existing.generator as GeneratorOption) ?? "auto",
          fixedValue: existing.fixedValue ?? "",
          aiPrompt: existing.aiPrompt ?? "",
          generatorParams: existing.generatorParams ?? {},
        };
      }
    }
  } catch {
    // open editor with no pre-filled rule on failure
  }

  if (panelState.activeTab === "fields") renderFieldsTab();
}

export function closeFieldEditor(): void {
  panelState.editingField = null;
  panelState.editingFieldExistingRule = null;
  if (panelState.activeTab === "fields") renderFieldsTab();
}

export async function saveFieldRule(
  payload: FieldEditorSavePayload,
): Promise<void> {
  if (!panelState.editingField) return;

  const field = panelState.editingField;
  try {
    const url = await getInspectedUrl();
    await sendToBackground({
      type: "SAVE_FIELD_OVERRIDE",
      payload: {
        url,
        fieldSelector: field.selector,
        fieldName: field.name || field.label || field.id || undefined,
        fieldType: payload.fieldType,
        generator: payload.generator,
        fixedValue: payload.fixedValue || undefined,
        aiPrompt: payload.aiPrompt || undefined,
        generatorParams: payload.generatorParams,
      },
    });
    addLog(`✓ Regra salva para: ${field.selector}`, "success");
  } catch (err) {
    addLog(`Erro ao salvar regra: ${err}`, "error");
  }

  closeFieldEditor();
}

export async function deleteFieldRule(): Promise<void> {
  if (!panelState.editingField) return;

  const field = panelState.editingField;
  try {
    const url = await getInspectedUrl();
    await sendToBackground({
      type: "DELETE_FIELD_OVERRIDE",
      payload: { url, fieldSelector: field.selector },
    });
    addLog(`✓ Regra removida para: ${field.selector}`, "success");
  } catch (err) {
    addLog(`Erro ao remover regra: ${err}`, "error");
  }

  closeFieldEditor();
}

export async function redetectField(selector: string): Promise<void> {
  addLog(`🔍 Re-detectando: ${selector}`);
  try {
    const result = (await sendToPage({
      type: "RECLASSIFY_FIELD",
      payload: selector,
    })) as DetectedFieldSummary & { error?: string };

    if (result?.error) {
      addLog(`Erro ao re-detectar: ${result.error}`, "error");
      return;
    }

    const idx = panelState.detectedFields.findIndex(
      (f) => f.selector === selector,
    );
    if (idx !== -1) {
      panelState.detectedFields[idx] = result;
    }

    // Update editingField so the modal reflects the new classification
    if (panelState.editingField?.selector === selector) {
      panelState.editingField = result;
    }

    addLog(
      `✓ Campo re-detectado: ${result.fieldType} (${result.detectionMethod})`,
      "success",
    );
  } catch (err) {
    addLog(`Erro ao re-detectar: ${err}`, "error");
  }

  if (panelState.activeTab === "fields") renderFieldsTab();
}

// ── Render ────────────────────────────────────────────────────────────────────

export function renderFieldsTab(): void {
  const content = document.getElementById("content");
  renderTo(
    content,
    <FieldsTabView
      fields={panelState.detectedFields}
      ignoredSelectors={panelState.ignoredSelectors}
      detecting={panelState.isDetecting}
      onDetect={() => void detectFields()}
      onFillAll={() => void fillAll()}
      onFillEmpty={() => void fillOnlyEmpty()}
      onClearDetected={() => void clearDetectedFields()}
      onClearForm={() => void clearForm()}
      onFillField={(sel) => void fillField(sel)}
      onInspectField={(sel) => inspectElement(sel)}
      onToggleIgnore={(sel, label) => void toggleIgnore(sel, label)}
      onEditField={(field) => void openFieldEditor(field)}
      editingField={panelState.editingField}
      editingFieldExistingRule={panelState.editingFieldExistingRule}
      onSaveFieldRule={(p: FieldEditorSavePayload) => void saveFieldRule(p)}
      onDeleteFieldRule={() => void deleteFieldRule()}
      onCloseEditor={closeFieldEditor}
      onRedetectField={(sel) => redetectField(sel)}
    />,
  );
}