src/lib/ui/components/forms-tab-view.tsx

Total Symbols
11
Lines of Code
481
Avg Complexity
1.2
Symbol Types
3

File Relationships

graph LR openNewForm["openNewForm"] t["t"] FormsTabViewCallbacks["FormsTabViewCallbacks"] openEditForm["openEditForm"] handleRemoveField["handleRemoveField"] handleGeneratorTypeChange["handleGeneratorTypeChange"] patch["patch"] openNewForm -->|calls| t FormsTabViewCallbacks -->|calls| t FormsTabViewCallbacks -->|calls| openEditForm FormsTabViewCallbacks -->|calls| handleRemoveField handleGeneratorTypeChange -->|calls| patch FormsTabViewCallbacks -->|calls| patch click openNewForm "../symbols/63135bea45833917.html" click t "../symbols/8e8864a3c5cfd1e1.html" click FormsTabViewCallbacks "../symbols/413148ba3c727cc8.html" click openEditForm "../symbols/8c3feac15aa626e3.html" click handleRemoveField "../symbols/2b09dcd9b19d7edc.html" click handleGeneratorTypeChange "../symbols/f74c8ee533cbc9ab.html" click patch "../symbols/1b15372e6d9c83f5.html"

Symbols by Kind

function 8
interface 2
method 1

All Symbols

Name Kind Visibility Status Lines Signature
FormsTabViewCallbacks interface exported- 35-41 interface FormsTabViewCallbacks
FormsTabViewProps interface exported- 43-46 interface FormsTabViewProps
openNewForm function - 60-71 openNewForm()
openEditForm function - 73-76 openEditForm(form: SavedForm)
handleAddField function - 211-218 handleAddField(): : void
handleRemoveField function - 220-223 handleRemoveField(index: number): : void
handleFieldSave function - 225-228 handleFieldSave(index: number, updated: FormTemplateField): : void
patch function - 367-369 patch(partial: Partial<FormTemplateField>)
handleGeneratorTypeChange function - 371-376 handleGeneratorTypeChange(v: string)
handleSave function - 378-380 handleSave()
t method - 389-389 t("editField")

Full Source

/**
 * FormsTabView, EditFormScreen & FieldRowModal — Preact components for the Forms tab.
 *
 * FormsTabView: saved forms list with load / new / apply / edit / delete.
 *   When editing, renders EditFormScreen inline (replacing the list).
 * EditFormScreen: full inline editor for a saved form — shows compact field rows.
 *   Clicking edit on a row opens FieldRowModal (modal overlay for one field).
 * FieldRowModal: modal overlay for editing a single FormTemplateField,
 *   following the same visual pattern as FieldEditorModal (fields tab).
 */

import { h } from "preact";
import { useState, useMemo } from "preact/hooks";
import type {
  SavedForm,
  FormTemplateField,
  FormFieldMode,
  FieldType,
} from "@/types";
import type { GeneratorParams } from "@/types/field-type-definitions";
import { t } from "@/lib/i18n";
import { SearchableSelectPreact } from "@/lib/ui/searchable-select-preact";
import {
  buildFieldTypeSelectEntries,
  buildGeneratorSelectEntries,
} from "@/lib/ui/select-builders";
import {
  GeneratorParamsSection,
  resolveParamDefs,
  buildInitialParams,
} from "./field-editor-modal";

// ── FormsTabView ──────────────────────────────────────────────────────────────

export interface FormsTabViewCallbacks {
  onLoad: () => void;
  onApply: (form: SavedForm) => void;
  onSave: (form: SavedForm, isNew: boolean) => Promise<void>;
  onSetDefault: (form: SavedForm) => void;
  onDelete: (form: SavedForm) => void;
}

export interface FormsTabViewProps extends FormsTabViewCallbacks {
  savedForms: SavedForm[];
  formsLoaded: boolean;
}

export function FormsTabView({
  savedForms,
  formsLoaded,
  onLoad,
  onApply,
  onSave,
  onSetDefault,
  onDelete,
}: FormsTabViewProps) {
  const [editingForm, setEditingForm] = useState<SavedForm | null>(null);
  const [isNew, setIsNew] = useState(false);

  function openNewForm() {
    setEditingForm({
      id: crypto.randomUUID(),
      name: t("newFormTitle"),
      urlPattern: "*",
      fields: {},
      templateFields: [],
      createdAt: Date.now(),
      updatedAt: Date.now(),
    });
    setIsNew(true);
  }

  function openEditForm(form: SavedForm) {
    setEditingForm(form);
    setIsNew(false);
  }

  async function handleSave(updated: SavedForm) {
    await onSave(updated, isNew);
    setEditingForm(null);
  }

  if (editingForm) {
    return (
      <EditFormScreen
        form={editingForm}
        isNew={isNew}
        onSave={handleSave}
        onClose={() => setEditingForm(null)}
      />
    );
  }

  return (
    <div>
      <div class="fields-toolbar">
        <button class="btn" onClick={onLoad}>
          🔄 {t("btnLoadForms")}
        </button>
        <button class="btn btn-success" onClick={openNewForm}>
          + {t("btnNewForm")}
        </button>
        <span class="fields-count">
          {savedForms.length} {t("formCount")}
        </span>
      </div>
      <div class="forms-list">
        {!formsLoaded ? (
          <div class="empty">⏳ {t("logLoadingForms")}</div>
        ) : savedForms.length === 0 ? (
          <div class="empty">{t("loadFormsDesc")}</div>
        ) : (
          savedForms.map((form) => (
            <div key={form.id} class="form-card">
              <div class="form-info">
                <span class="form-name">
                  {form.name}
                  {form.isDefault && (
                    <span class="badge-default">{t("badgeDefault")}</span>
                  )}
                </span>
                <span class="form-meta">
                  {form.templateFields?.length ??
                    Object.keys(form.fields).length}{" "}
                  {t("fieldCount")} ·{" "}
                  {new Date(form.updatedAt).toLocaleDateString()}
                </span>
                <span class="form-url">{form.urlPattern}</span>
              </div>
              <div class="form-actions">
                <button class="btn btn-sm" onClick={() => onApply(form)}>
                  ▶️ {t("btnApply")}
                </button>
                <button
                  class="btn btn-sm btn-warning"
                  onClick={() => openEditForm(form)}
                >
                  ✏️ {t("btnEdit")}
                </button>
                <button
                  class="btn btn-sm btn-secondary"
                  title={t("btnSetDefault")}
                  onClick={() => onSetDefault(form)}
                >
                  ⭐
                </button>
                <button
                  class="btn btn-sm btn-danger"
                  title={t("msgConfirmDeleteForm")}
                  onClick={() => onDelete(form)}
                >
                  🗑️
                </button>
              </div>
            </div>
          ))
        )}
      </div>
    </div>
  );
}

// ── helpers ───────────────────────────────────────────────────────────────────

function normaliseFields(form: SavedForm): FormTemplateField[] {
  if (form.templateFields && form.templateFields.length > 0) {
    return form.templateFields.map((f) => ({ ...f }));
  }
  return Object.entries(form.fields).map(([key, value]) => ({
    key,
    label: key,
    mode: "fixed" as FormFieldMode,
    fixedValue: value,
  }));
}

function fieldRowLabel(field: FormTemplateField, index: number): string {
  return (field.matchByFieldType ?? field.key) || `field_${index + 1}`;
}

function fieldRowValue(field: FormTemplateField): string {
  if (field.mode === "fixed") return field.fixedValue ?? "";
  return field.generatorType ?? "auto";
}

// ── EditFormScreen ────────────────────────────────────────────────────────────

interface EditFormScreenProps {
  form: SavedForm;
  isNew?: boolean;
  onSave: (updated: SavedForm) => Promise<void>;
  onClose: () => void;
}

export function EditFormScreen({
  form,
  isNew = false,
  onSave,
  onClose,
}: EditFormScreenProps) {
  const [formName, setFormName] = useState(form.name);
  const [urlPattern, setUrlPattern] = useState(form.urlPattern);
  const [fields, setFields] = useState<FormTemplateField[]>(() =>
    normaliseFields(form),
  );
  const [editingFieldIndex, setEditingFieldIndex] = useState<number | null>(
    null,
  );
  const [saving, setSaving] = useState(false);

  function handleAddField(): void {
    const next: FormTemplateField[] = [
      ...fields,
      { key: "", label: "", mode: "fixed" as FormFieldMode, fixedValue: "" },
    ];
    setFields(next);
    setEditingFieldIndex(next.length - 1);
  }

  function handleRemoveField(index: number): void {
    setFields((prev) => prev.filter((_, i) => i !== index));
    if (editingFieldIndex === index) setEditingFieldIndex(null);
  }

  function handleFieldSave(index: number, updated: FormTemplateField): void {
    setFields((prev) => prev.map((f, i) => (i === index ? updated : f)));
    setEditingFieldIndex(null);
  }

  function handleSave(): void {
    if (saving) return;
    setSaving(true);
    const updatedFields: FormTemplateField[] = fields.map((f, i) => {
      const resolvedKey = (f.matchByFieldType ?? f.key) || `field_${i + 1}`;
      return {
        key: resolvedKey,
        label: resolvedKey,
        mode: f.mode,
        matchByFieldType: f.matchByFieldType,
        fixedValue: f.mode === "fixed" ? f.fixedValue : undefined,
        generatorType: f.mode === "generator" ? f.generatorType : undefined,
        generatorParams:
          f.mode === "generator" && f.generatorParams
            ? f.generatorParams
            : undefined,
      };
    });

    void onSave({
      ...form,
      name: formName.trim() || form.name,
      urlPattern: urlPattern.trim() || form.urlPattern,
      templateFields: updatedFields,
    }).finally(() => setSaving(false));
  }

  return (
    <div class="edit-form-screen">
      <div class="fields-toolbar">
        <button class="btn btn-secondary" onClick={onClose}>
          ← {t("btnCancel")}
        </button>
        <span class="modal-title" style={{ flex: 1, marginLeft: 8 }}>
          {isNew ? "➕" : "✏️"} {t(isNew ? "newFormTitle" : "editTemplate")}
        </span>
        <button class="btn btn-success" onClick={handleSave} disabled={saving}>
          💾 {saving ? "..." : t("btnSave")}
        </button>
      </div>

      <div class="edit-meta-grid" style={{ padding: "8px 0" }}>
        <div class="edit-input-group">
          <label class="edit-label">{t("formName")}</label>
          <input
            class="edit-input"
            type="text"
            value={formName}
            onInput={(e) => setFormName((e.target as HTMLInputElement).value)}
          />
        </div>
        <div class="edit-input-group">
          <label class="edit-label">{t("formUrl")}</label>
          <input
            class="edit-input"
            type="text"
            value={urlPattern}
            onInput={(e) => setUrlPattern((e.target as HTMLInputElement).value)}
          />
        </div>
      </div>

      <div class="edit-section-header">{t("editFieldsHeader")}</div>

      <div class="edit-fields-list">
        {fields.length === 0 && (
          <div class="empty" style={{ padding: "12px 0" }}>
            {t("noFieldsYet")}
          </div>
        )}
        {fields.map((field, i) => (
          <div key={i} class="field-row-compact">
            <span class="field-row-type">{fieldRowLabel(field, i)}</span>
            <span class="field-row-mode">{field.mode}</span>
            <span class="field-row-value">{fieldRowValue(field)}</span>
            <button
              class="btn btn-sm btn-warning"
              title={t("btnEdit")}
              onClick={() => setEditingFieldIndex(i)}
            >
              ✏️
            </button>
            <button
              class="btn btn-sm btn-danger"
              title={t("tooltipRemoveField")}
              onClick={() => handleRemoveField(i)}
            >
              🗑
            </button>
          </div>
        ))}
      </div>

      <div style={{ padding: "8px 0" }}>
        <button class="btn btn-secondary" onClick={handleAddField}>
          + {t("btnAddField")}
        </button>
      </div>

      {editingFieldIndex !== null && fields[editingFieldIndex] && (
        <FieldRowModal
          field={fields[editingFieldIndex]}
          index={editingFieldIndex}
          onSave={handleFieldSave}
          onClose={() => setEditingFieldIndex(null)}
        />
      )}
    </div>
  );
}

// ── FieldRowModal ─────────────────────────────────────────────────────────────

interface FieldRowModalProps {
  field: FormTemplateField;
  index: number;
  onSave: (index: number, updated: FormTemplateField) => void;
  onClose: () => void;
}

function FieldRowModal({ field, index, onSave, onClose }: FieldRowModalProps) {
  const [draft, setDraft] = useState<FormTemplateField>({ ...field });
  const fieldTypeEntries = useMemo(() => buildFieldTypeSelectEntries(), []);
  const generatorEntries = useMemo(() => buildGeneratorSelectEntries(), []);

  const paramDefs = useMemo(
    () =>
      draft.mode === "generator" && draft.generatorType
        ? resolveParamDefs(draft.generatorType)
        : [],
    [draft.mode, draft.generatorType],
  );

  const [generatorParams, setGeneratorParams] = useState<GeneratorParams>(() =>
    buildInitialParams(paramDefs, field.generatorParams ?? {}),
  );

  function patch(partial: Partial<FormTemplateField>) {
    setDraft((prev) => ({ ...prev, ...partial }));
  }

  function handleGeneratorTypeChange(v: string) {
    const newType = v as FieldType;
    const newDefs = resolveParamDefs(newType);
    patch({ generatorType: newType });
    setGeneratorParams(buildInitialParams(newDefs, generatorParams));
  }

  function handleSave() {
    onSave(index, { ...draft, generatorParams });
  }

  return (
    <div class="modal-overlay" onClick={onClose}>
      <div
        class="modal-box modal-box--form"
        onClick={(e) => e.stopPropagation()}
      >
        <div class="modal-header">
          <span class="modal-title">✏️ {t("editField")}</span>
          <button class="modal-close" onClick={onClose}>
            ✕
          </button>
        </div>

        <div class="modal-body">
          <div class="edit-input-group">
            <label class="edit-label">{t("tooltipMatchByFieldType")}</label>
            <SearchableSelectPreact
              entries={fieldTypeEntries}
              value={draft.matchByFieldType ?? ""}
              onChange={(v) =>
                patch({ matchByFieldType: v ? (v as FieldType) : undefined })
              }
              placeholder={t("tooltipMatchByFieldType")}
            />
          </div>

          <div class="edit-input-group" style={{ marginTop: 8 }}>
            <label class="edit-label">{t("fieldModeHeader")}</label>
            <select
              class="edit-select"
              value={draft.mode}
              onChange={(e) =>
                patch({
                  mode: (e.target as HTMLSelectElement).value as FormFieldMode,
                })
              }
            >
              <option value="fixed">{t("fixedValue")}</option>
              <option value="generator">{t("generatorMode")}</option>
            </select>
          </div>

          <div class="edit-input-group" style={{ marginTop: 8 }}>
            <label class="edit-label">
              {draft.mode === "fixed" ? t("fixedValue") : t("generatorMode")}
            </label>
            {draft.mode === "fixed" ? (
              <input
                type="text"
                class="edit-input"
                placeholder={t("placeholderFixedValue")}
                value={draft.fixedValue ?? ""}
                onInput={(e) =>
                  patch({ fixedValue: (e.target as HTMLInputElement).value })
                }
              />
            ) : (
              <SearchableSelectPreact
                entries={generatorEntries}
                value={draft.generatorType ?? "auto"}
                onChange={handleGeneratorTypeChange}
                placeholder={t("generatorMode")}
              />
            )}
          </div>

          {draft.mode === "generator" && paramDefs.length > 0 && (
            <GeneratorParamsSection
              defs={paramDefs}
              params={generatorParams}
              onChange={setGeneratorParams}
            />
          )}
        </div>

        <div class="modal-footer">
          <button class="btn" onClick={onClose}>
            ✕ {t("btnCancel")}
          </button>
          <button class="btn btn-success" onClick={handleSave}>
            💾 {t("btnSave")}
          </button>
        </div>
      </div>
    </div>
  );
}

// ── EditFormModal (kept for external consumers) ───────────────────────────────

export interface EditFormModalProps {
  form: SavedForm;
  isNew?: boolean;
  onSave: (updated: SavedForm) => Promise<void>;
  onClose: () => void;
}

/** @deprecated Use EditFormScreen instead. */
export const EditFormModal = EditFormScreen;