src/options/forms-section.ts

Total Symbols
12
Lines of Code
630
Avg Complexity
7.8
Symbol Types
1

File Relationships

graph LR openCreatePanel["openCreatePanel"] buildTemplateFieldRow["buildTemplateFieldRow"] bindCreatePanelEvents["bindCreatePanelEvents"] upgradeRowSearchableSelects["upgradeRowSearchableSelects"] fieldTypeLabel["fieldTypeLabel"] loadSavedForms["loadSavedForms"] fieldSummary["fieldSummary"] openEditPanel["openEditPanel"] legacyFieldsToTemplate["legacyFieldsToTemplate"] importForms["importForms"] initFormsTab["initFormsTab"] exportForms["exportForms"] openCreatePanel -->|calls| buildTemplateFieldRow openCreatePanel -->|calls| bindCreatePanelEvents openCreatePanel -->|calls| upgradeRowSearchableSelects bindCreatePanelEvents -->|calls| buildTemplateFieldRow bindCreatePanelEvents -->|calls| upgradeRowSearchableSelects bindCreatePanelEvents -->|calls| fieldTypeLabel bindCreatePanelEvents -->|calls| loadSavedForms loadSavedForms -->|calls| fieldSummary loadSavedForms -->|calls| openEditPanel loadSavedForms -->|calls| loadSavedForms openEditPanel -->|calls| legacyFieldsToTemplate openEditPanel -->|calls| buildTemplateFieldRow openEditPanel -->|calls| upgradeRowSearchableSelects openEditPanel -->|calls| fieldTypeLabel openEditPanel -->|calls| loadSavedForms importForms -->|calls| loadSavedForms initFormsTab -->|calls| loadSavedForms initFormsTab -->|calls| openCreatePanel initFormsTab -->|calls| exportForms initFormsTab -->|calls| importForms click openCreatePanel "../symbols/16ceabf613d86e61.html" click buildTemplateFieldRow "../symbols/6e5eee1aa17b57cc.html" click bindCreatePanelEvents "../symbols/63724b3d20cd4419.html" click upgradeRowSearchableSelects "../symbols/90e9f01206f5f2c7.html" click fieldTypeLabel "../symbols/861a0ec11bc0b27b.html" click loadSavedForms "../symbols/cf1ee6d5c3ab3b9b.html" click fieldSummary "../symbols/171f77a6709cf221.html" click openEditPanel "../symbols/452da4897e90f5ca.html" click legacyFieldsToTemplate "../symbols/0a335e33c2aaef7e.html" click importForms "../symbols/8f033b80a44a7e60.html" click initFormsTab "../symbols/016a6b967c8f6192.html" click exportForms "../symbols/8ec1be65bac66e52.html"

Architecture violations

View all

  • [warning] max-cyclomatic-complexity: 'bindCreatePanelEvents' has cyclomatic complexity 13 (max 10)
  • [warning] max-cyclomatic-complexity: 'openEditPanel' has cyclomatic complexity 30 (max 10)
  • [warning] max-cyclomatic-complexity: 'importForms' has cyclomatic complexity 11 (max 10)
  • [warning] max-lines: 'bindCreatePanelEvents' has 109 lines (max 80)
  • [warning] max-lines: 'openEditPanel' has 201 lines (max 80)

Symbols by Kind

function 12

All Symbols

Name Kind Visibility Status Lines Signature
fieldTypeLabel function - 15-17 fieldTypeLabel(ft: FieldType): : string
fieldSummary function - 19-31 fieldSummary(form: SavedForm): : string
upgradeRowSearchableSelects function - 39-66 upgradeRowSearchableSelects(row: Element): : void
legacyFieldsToTemplate function - 68-77 legacyFieldsToTemplate( fields: Record<string, string>, ): : FormTemplateField[]
buildTemplateFieldRow function - 80-116 buildTemplateFieldRow(field?: Partial<FormTemplateField>): : string
openCreatePanel function - 119-176 openCreatePanel(): : void
bindCreatePanelEvents function - 178-286 bindCreatePanelEvents(panel: HTMLElement): : void
loadSavedForms function - 288-332 loadSavedForms(): : Promise<void>
openEditPanel function - 334-534 openEditPanel(form: SavedForm): : void
exportForms function - 536-556 exportForms(): : Promise<void>
importForms function - 558-600 importForms(file: File): : Promise<void>
initFormsTab function exported- 602-629 initFormsTab(): : void

Full Source

/**
 * Saved Forms tab — list, create, edit, and delete saved form templates.
 */

import type { SavedForm, FormTemplateField, FieldType } from "@/types";
import { t } from "@/lib/i18n";
import { escapeHtml, showToast } from "./shared";
import { getFieldTypeLabel } from "@/lib/shared/field-type-catalog";
import {
  SearchableSelect,
  buildFieldTypeSelectEntries,
  buildGeneratorSelectEntries,
} from "@/lib/ui";

function fieldTypeLabel(ft: FieldType): string {
  return getFieldTypeLabel(ft);
}

function fieldSummary(form: SavedForm): string {
  if (form.templateFields && form.templateFields.length > 0) {
    const fixed = form.templateFields.filter((f) => f.mode === "fixed").length;
    const gen = form.templateFields.filter(
      (f) => f.mode === "generator",
    ).length;
    const parts: string[] = [];
    if (fixed > 0) parts.push(`${fixed} fixo${fixed > 1 ? "s" : ""}`);
    if (gen > 0) parts.push(`${gen} gerador${gen > 1 ? "es" : ""}`);
    return parts.join(", ");
  }
  return `${Object.keys(form.fields || {}).length} campos`;
}

/**
 * Mounts SearchableSelect components inside `.field-type-match-container` and
 * `.field-generator-container` for a newly inserted template field row.
 * Initial values are taken from `data-match-type` / `data-generator-type`
 * attributes on the row element (set by `buildTemplateFieldRow`).
 */
function upgradeRowSearchableSelects(row: Element): void {
  const tr = row as HTMLElement;
  const matchType = tr.dataset.matchType ?? "";
  const generatorType = tr.dataset.generatorType ?? "";

  const typeContainer = row.querySelector<HTMLElement>(
    ".field-type-match-container",
  );
  const genContainer = row.querySelector<HTMLElement>(
    ".field-generator-container",
  );

  if (typeContainer && !typeContainer.querySelector(".fa-ss")) {
    new SearchableSelect({
      entries: buildFieldTypeSelectEntries(),
      value: matchType,
      placeholder: "Selecione o tipo…",
    }).mount(typeContainer);
  }

  if (genContainer && !genContainer.querySelector(".fa-ss")) {
    new SearchableSelect({
      entries: buildGeneratorSelectEntries(),
      value: generatorType || matchType,
      placeholder: "Selecione o gerador…",
    }).mount(genContainer);
  }
}

function legacyFieldsToTemplate(
  fields: Record<string, string>,
): FormTemplateField[] {
  return Object.entries(fields).map(([key, value]) => ({
    key,
    label: key,
    mode: "fixed" as const,
    fixedValue: value,
  }));
}

/** Cria uma linha de campo para o painel de criação/edição de template por tipo */
function buildTemplateFieldRow(field?: Partial<FormTemplateField>): string {
  const matchType = field?.matchByFieldType ?? "name";
  const mode = field?.mode ?? "fixed";
  const fixedValue = field?.fixedValue ?? "";
  const generatorType = field?.generatorType ?? "name";
  return `
    <tr class="template-field-row"
        data-match-type="${escapeHtml(matchType)}"
        data-generator-type="${escapeHtml(generatorType)}">
      <td>
        <div class="field-type-match-container"></div>
      </td>
      <td>
        <select class="field-mode-select">
          <option value="fixed"${mode === "fixed" ? " selected" : ""}>${t("modeFixed")}</option>
          <option value="generator"${mode === "generator" ? " selected" : ""}>${t("modeGenerator")}</option>
        </select>
      </td>
      <td>
        <input
          type="text"
          class="field-fixed-value"
          placeholder="${t("valuePlaceholder")}"
          value="${escapeHtml(fixedValue)}"
          style="display:${mode === "fixed" ? "inline-block" : "none"}"
        />
        <div
          class="field-generator-container"
          style="display:${mode === "generator" ? "inline-block" : "none"}"
        ></div>
      </td>
      <td>
        <button class="btn btn-sm btn-delete btn-remove-row" title="${t("removeFieldTitle")}">✕</button>
      </td>
    </tr>
  `;
}

/** Abre o painel de criação de template (em branco) */
function openCreatePanel(): void {
  const existing = document.getElementById("form-create-panel");
  if (existing) {
    existing.scrollIntoView({ behavior: "smooth" });
    return;
  }

  const panel = document.createElement("div");
  panel.id = "form-create-panel";
  panel.className = "edit-panel";
  panel.innerHTML = `
    <h3>${t("createTemplateHeader")}</h3>
    <div class="form-group">
      <label>${t("templateNameLabel")}</label>
      <input type="text" id="create-form-name" placeholder="${t("createTemplateNamePlaceholder")}" />
    </div>
    <div class="form-group">
      <label>${t("urlPatternLabel")}</label>
      <input type="text" id="create-form-url" placeholder="${t("templateUrlPlaceholder")}" />
      <div class="description" style="margin-top:4px;font-size:11px;color:var(--text-muted);">
        ${t("templateUrlDesc")}
      </div>
    </div>
    <div style="margin-bottom:8px;">
      <strong style="font-size:13px;">${t("fieldsTitle")}</strong>
      <div class="description" style="font-size:11px;color:var(--text-muted);margin-top:2px;">
        ${t("templateFieldsDesc")}
      </div>
    </div>
    <table class="template-fields-table" id="create-fields-table">
      <thead>
        <tr>
          <th>${t("fieldDetectedTypeHeader")}</th>
          <th>${t("fieldModeHeader")}</th>
          <th>${t("fieldValueHeader")}</th>
          <th></th>
        </tr>
      </thead>
      <tbody id="create-fields-tbody">
        ${buildTemplateFieldRow({ matchByFieldType: "name", mode: "fixed", fixedValue: "" })}
      </tbody>
    </table>
    <div style="margin-bottom:12px;">
      <button class="btn btn-secondary btn-sm" id="create-add-field-row">+ Adicionar campo</button>
    </div>
    <div class="edit-panel-actions">
      <button class="btn btn-primary" id="create-panel-save">${t("btnSaveTemplate")}</button>
      <button class="btn btn-secondary" id="create-panel-cancel">${t("btnCancel")}</button>
    </div>
  `;

  document.getElementById("saved-forms-list")?.before(panel);
  bindCreatePanelEvents(panel);
  panel.querySelectorAll("tr.template-field-row").forEach((row) => {
    upgradeRowSearchableSelects(row);
  });
  panel.scrollIntoView({ behavior: "smooth" });
}

function bindCreatePanelEvents(panel: HTMLElement): void {
  // Toggle fixed/generator on mode change
  panel.addEventListener("change", (e) => {
    const target = e.target as HTMLSelectElement;
    if (!target.classList.contains("field-mode-select")) return;
    const row = target.closest("tr")!;
    const isFixed = target.value === "fixed";
    (row.querySelector(".field-fixed-value") as HTMLElement).style.display =
      isFixed ? "inline-block" : "none";
    (
      row.querySelector(".field-generator-container") as HTMLElement
    ).style.display = isFixed ? "none" : "inline-block";
  });

  // Remove field row
  panel.addEventListener("click", (e) => {
    const target = e.target as HTMLElement;
    if (!target.classList.contains("btn-remove-row")) return;
    target.closest("tr")?.remove();
  });

  // Add field row
  panel
    .querySelector("#create-add-field-row")
    ?.addEventListener("click", () => {
      const tbody = panel.querySelector("#create-fields-tbody");
      if (!tbody) return;
      const wrapper = document.createElement("tbody");
      wrapper.innerHTML = buildTemplateFieldRow();
      const newRow = wrapper.querySelector("tr");
      if (newRow) {
        tbody.appendChild(newRow);
        upgradeRowSearchableSelects(newRow);
      }
    });

  panel.querySelector("#create-panel-cancel")?.addEventListener("click", () => {
    panel.remove();
  });

  panel
    .querySelector("#create-panel-save")
    ?.addEventListener("click", async () => {
      const nameInput = panel.querySelector(
        "#create-form-name",
      ) as HTMLInputElement;
      const urlInput = panel.querySelector(
        "#create-form-url",
      ) as HTMLInputElement;

      const name = nameInput.value.trim();
      if (!name) {
        nameInput.focus();
        showToast(t("errorTemplateNameRequired"));
        return;
      }

      const urlPattern = urlInput.value.trim() || "*";
      const templateFields: FormTemplateField[] = [];

      panel.querySelectorAll("tr.template-field-row").forEach((row) => {
        const matchType =
          ((
            row.querySelector(
              ".field-type-match-container .fa-ss__value",
            ) as HTMLInputElement
          )?.value as FieldType) ?? ("name" as FieldType);
        const mode = (
          row.querySelector(".field-mode-select") as HTMLSelectElement
        ).value as "fixed" | "generator";
        const fixedValue = (
          row.querySelector(".field-fixed-value") as HTMLInputElement
        ).value;
        const generatorType =
          ((
            row.querySelector(
              ".field-generator-container .fa-ss__value",
            ) as HTMLInputElement
          )?.value as FieldType) ?? ("name" as FieldType);

        templateFields.push({
          key: matchType,
          label: fieldTypeLabel(matchType),
          mode,
          matchByFieldType: matchType,
          fixedValue: mode === "fixed" ? fixedValue : undefined,
          generatorType: mode === "generator" ? generatorType : undefined,
        });
      });

      const newForm: SavedForm = {
        id: `tpl-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`,
        name,
        urlPattern,
        fields: {},
        templateFields,
        createdAt: Date.now(),
        updatedAt: Date.now(),
      };

      await chrome.runtime.sendMessage({
        type: "UPDATE_FORM",
        payload: newForm,
      });
      panel.remove();
      await loadSavedForms();
      showToast(t("toastTemplateCreated", [name]));
    });
}

async function loadSavedForms(): Promise<void> {
  const forms = (await chrome.runtime.sendMessage({
    type: "GET_SAVED_FORMS",
  })) as SavedForm[];
  const list = document.getElementById("saved-forms-list");
  if (!list) return;

  list.innerHTML = "";

  if (!Array.isArray(forms) || forms.length === 0) {
    list.innerHTML = `<div class="empty">${t("noSavedForms")}</div>`;
    return;
  }

  for (const form of forms) {
    const item = document.createElement("div");
    item.className = "rule-item";
    item.innerHTML = `
      <div class="rule-info">
        <strong>${escapeHtml(form.name)}</strong>
        <span class="rule-selector">${escapeHtml(form.urlPattern)}</span>
        <span class="badge">${escapeHtml(fieldSummary(form))}</span>
      </div>
      <div class="rule-actions">
        <button class="btn btn-sm btn-edit" data-form-id="${escapeHtml(form.id)}">${t("btnEdit")}</button>
        <button class="btn btn-sm btn-delete" data-form-id="${escapeHtml(form.id)}">${t("btnDelete")}</button>
      </div>
    `;

    item.querySelector(".btn-edit")?.addEventListener("click", () => {
      openEditPanel(form);
    });

    item.querySelector(".btn-delete")?.addEventListener("click", async () => {
      await chrome.runtime.sendMessage({
        type: "DELETE_FORM",
        payload: form.id,
      });
      await loadSavedForms();
      showToast(t("toastFormDeleted"));
    });

    list.appendChild(item);
  }
}

function openEditPanel(form: SavedForm): void {
  let panel = document.getElementById("form-edit-panel");
  if (!panel) {
    panel = document.createElement("div");
    panel.id = "form-edit-panel";
    panel.className = "edit-panel";
    document.getElementById("saved-forms-list")?.after(panel);
  }

  const templateFields =
    form.templateFields && form.templateFields.length > 0
      ? form.templateFields
      : legacyFieldsToTemplate(form.fields);

  // Determina se este template usa match por tipo (type-based) ou por seletor
  const isTypeBased = templateFields.some((f) => f.matchByFieldType);

  panel.innerHTML = `
    <h3>${t("editTemplateFor")} ${escapeHtml(form.name)}</h3>
    <div class="form-group">
      <label>${t("nameLabel")}</label>
      <input type="text" id="edit-form-name" value="${escapeHtml(form.name)}" />
    </div>
    <div class="form-group">
      <label>${t("urlPatternLabel")}</label>
      <input type="text" id="edit-form-url" value="${escapeHtml(form.urlPattern)}" />
      <div class="description" style="margin-top:4px;font-size:11px;color:var(--text-muted);">
        ${t("editUrlPatternDesc")}
      </div>
    </div>
    <div style="margin-bottom:8px;">
      <strong style="font-size:13px;">${t("fieldsTitle")}</strong>
    </div>
    <table class="template-fields-table">
      <thead>
        <tr>
          <th>${isTypeBased ? t("fieldDetectedTypeHeader") : t("fieldColumnHeader")}</th>
          <th>${t("fieldModeHeader")}</th>
          <th>${t("fieldValueHeader")}</th>
          <th></th>
        </tr>
      </thead>
      <tbody id="edit-fields-tbody">
        ${templateFields
          .map((field) =>
            isTypeBased
              ? buildTemplateFieldRow(field)
              : `
          <tr data-key="${escapeHtml(field.key)}" class="template-field-row template-field-row--legacy">
            <td class="field-label-cell">${escapeHtml(field.label || field.key)}</td>
            <td>
              <select class="field-mode-select">
                <option value="fixed"${field.mode === "fixed" ? " selected" : ""}>${t("modeFixed")}</option>
                <option value="generator"${field.mode === "generator" ? " selected" : ""}>${t("modeGenerator")}</option>
              </select>
            </td>
            <td>
              <input
                type="text"
                class="field-fixed-value"
                value="${escapeHtml(field.fixedValue ?? "")}"
                style="display:${field.mode === "fixed" ? "inline-block" : "none"}"
              />
              <div
                class="field-generator-container"
                data-generator-type="${escapeHtml(field.generatorType ?? "name")}"
                style="display:${field.mode === "generator" ? "inline-block" : "none"}"
              ></div>
            </td>
            <td>
              <button class="btn btn-sm btn-delete btn-remove-row" title="${t("removeFieldTitle")}">✕</button>
            </td>
          </tr>
        `,
          )
          .join("")}
      </tbody>
    </table>
    <div style="margin-bottom:12px;">
      <button class="btn btn-secondary btn-sm" id="edit-add-field-row">${t("btnAddField")}</button>
    </div>
    <div class="edit-panel-actions">
      <button class="btn btn-primary" id="edit-panel-save">${t("btnSave")}</button>
      <button class="btn btn-secondary" id="edit-panel-cancel">${t("btnCancel")}</button>
    </div>
  `;

  // Upgrade all type-based rows to SearchableSelect
  panel.querySelectorAll("tr.template-field-row").forEach((row) => {
    upgradeRowSearchableSelects(row);
  });

  // Toggle fixed/generator visibility on change
  panel.addEventListener("change", (e) => {
    const target = e.target as HTMLSelectElement;
    if (!target.classList.contains("field-mode-select")) return;
    const row = target.closest("tr")!;
    const isFixed = target.value === "fixed";
    (row.querySelector(".field-fixed-value") as HTMLElement).style.display =
      isFixed ? "inline-block" : "none";
    (
      row.querySelector(".field-generator-container") as HTMLElement
    ).style.display = isFixed ? "none" : "inline-block";
  });

  // Remove row (type-based only)
  panel.addEventListener("click", (e) => {
    const target = e.target as HTMLElement;
    if (!target.classList.contains("btn-remove-row")) return;
    target.closest("tr")?.remove();
  });

  // Add field row (always type-based for new rows)
  panel.querySelector("#edit-add-field-row")?.addEventListener("click", () => {
    const tbody = panel!.querySelector("#edit-fields-tbody");
    if (!tbody) return;
    const wrapper = document.createElement("tbody");
    wrapper.innerHTML = buildTemplateFieldRow();
    const newRow = wrapper.querySelector("tr");
    if (newRow) {
      tbody.appendChild(newRow);
      upgradeRowSearchableSelects(newRow);
    }
  });

  panel.querySelector("#edit-panel-cancel")?.addEventListener("click", () => {
    panel!.remove();
  });

  panel
    .querySelector("#edit-panel-save")
    ?.addEventListener("click", async () => {
      const nameInput = panel!.querySelector(
        "#edit-form-name",
      ) as HTMLInputElement;
      const urlInput = panel!.querySelector(
        "#edit-form-url",
      ) as HTMLInputElement;

      const updatedFields: FormTemplateField[] = [];
      panel!.querySelectorAll("tr.template-field-row").forEach((row) => {
        const mode = (
          row.querySelector(".field-mode-select") as HTMLSelectElement
        ).value as "fixed" | "generator";
        const fixedValue = (
          row.querySelector(".field-fixed-value") as HTMLInputElement
        ).value;
        const generatorType =
          ((
            row.querySelector(
              ".field-generator-container .fa-ss__value",
            ) as HTMLInputElement
          )?.value as FieldType) ?? ("name" as FieldType);

        const typeMatchInput = row.querySelector(
          ".field-type-match-container .fa-ss__value",
        ) as HTMLInputElement | null;

        if (typeMatchInput) {
          // Type-based row (template row with matchByFieldType)
          const matchType = typeMatchInput.value as FieldType;
          updatedFields.push({
            key: matchType,
            label: fieldTypeLabel(matchType),
            mode,
            matchByFieldType: matchType,
            fixedValue: mode === "fixed" ? fixedValue : undefined,
            generatorType: mode === "generator" ? generatorType : undefined,
          });
        } else {
          // Legacy row (captured from DOM, key is CSS selector/name)
          const key = (row as HTMLElement).dataset.key ?? "";
          const label =
            row.querySelector(".field-label-cell")?.textContent?.trim() ?? key;
          updatedFields.push({
            key,
            label,
            mode,
            fixedValue: mode === "fixed" ? fixedValue : undefined,
            generatorType: mode === "generator" ? generatorType : undefined,
          });
        }
      });

      const updatedForm: SavedForm = {
        ...form,
        name: nameInput.value || form.name,
        urlPattern: urlInput.value || form.urlPattern,
        templateFields: updatedFields,
        updatedAt: Date.now(),
      };

      await chrome.runtime.sendMessage({
        type: "UPDATE_FORM",
        payload: updatedForm,
      });
      panel!.remove();
      await loadSavedForms();
      showToast(t("toastTemplateUpdated"));
    });
}

async function exportForms(): Promise<void> {
  const forms = (await chrome.runtime.sendMessage({
    type: "GET_SAVED_FORMS",
  })) as SavedForm[];

  if (!Array.isArray(forms) || forms.length === 0) {
    showToast(t("noFormsToExport"));
    return;
  }

  const payload = JSON.stringify({ version: 1, forms }, null, 2);
  const blob = new Blob([payload], { type: "application/json" });
  const url = URL.createObjectURL(blob);

  const a = document.createElement("a");
  a.href = url;
  a.download = `fill-all-forms-${new Date().toISOString().slice(0, 10)}.json`;
  a.click();
  URL.revokeObjectURL(url);
  showToast(t("toastFormsExported", [String(forms.length)]));
}

async function importForms(file: File): Promise<void> {
  let parsed: unknown;
  try {
    parsed = JSON.parse(await file.text());
  } catch {
    showToast(t("errorInvalidJson"));
    return;
  }

  if (
    typeof parsed !== "object" ||
    parsed === null ||
    !Array.isArray((parsed as Record<string, unknown>).forms)
  ) {
    showToast(t("errorInvalidFormat"));
    return;
  }

  const forms = (parsed as { forms: unknown[] }).forms;
  let count = 0;

  for (const form of forms) {
    if (
      typeof form !== "object" ||
      form === null ||
      typeof (form as Record<string, unknown>).id !== "string" ||
      typeof (form as Record<string, unknown>).name !== "string"
    ) {
      continue;
    }

    await chrome.runtime.sendMessage({
      type: "UPDATE_FORM",
      payload: form,
    });
    count++;
  }

  await loadSavedForms();
  showToast(
    count > 0 ? t("toastFormsImported", [String(count)]) : t("noFormsInFile"),
  );
}

export function initFormsTab(): void {
  void loadSavedForms();

  document
    .getElementById("btn-create-template")
    ?.addEventListener("click", () => {
      openCreatePanel();
    });

  document.getElementById("btn-export-forms")?.addEventListener("click", () => {
    void exportForms();
  });

  const fileInput = document.getElementById(
    "import-forms-file",
  ) as HTMLInputElement | null;

  document.getElementById("btn-import-forms")?.addEventListener("click", () => {
    fileInput?.click();
  });

  fileInput?.addEventListener("change", () => {
    const file = fileInput.files?.[0];
    if (!file) return;
    void importForms(file);
    fileInput.value = "";
  });
}