import { Badge, Button, Input, LinkButton, Loader, Select, Switch, buttonVariants, } from "@cloudflare/kumo"; import { plural } from "@lingui/core/macro"; import { useLingui } from "@lingui/react/macro"; import { Upload, Check, X, Warning, WarningCircle, Plus, Database, FileText, CaretDown, Image, DownloadSimple, Globe, ArrowSquareOut, List, Gear, Sparkle, User, } from "@phosphor-icons/react"; import { useMutation } from "@tanstack/react-query"; import * as React from "react"; import { analyzeWxr, prepareWxrImport, executeWxrImport, importWxrMedia, rewriteContentUrls, probeImportUrl, analyzeWpPluginSite, executeWpPluginImport, fetchUsers, type WxrAnalysis, type WpPluginAnalysis, type PostTypeAnalysis, type ImportConfig, type ImportResult, type PrepareResult, type MediaImportResult, type MediaImportProgress, type RewriteUrlsResult, type AttachmentInfo, type ProbeResult, type AuthorMapping, type UserListItem, } from "../lib/api"; import { cn } from "../lib/utils"; import { CaretNext } from "./ArrowIcons.js"; // ============================================================================ // Constants // ============================================================================ const TRAILING_SLASH_REGEX = /\/$/; const WHITESPACE_REGEX = /\s/g; // ============================================================================ // Types // ============================================================================ type ImportStep = | "choose" // New: choose how to import (URL or file) | "probing" // New: probing URL | "probe-result" // New: showing probe results | "plugin-auth" // Authenticating with WordPress plugin | "analyzing-plugin" // Analyzing WordPress plugin site | "upload" | "review" | "authors" // Author mapping step | "preparing" | "importing" | "media" | "importing-media" | "rewriting" | "complete"; /** Import source - either WXR file or Plugin API */ type ImportSource = | { type: "wxr"; file: File } | { type: "wordpress-plugin"; url: string; token: string }; interface PostTypeSelection { enabled: boolean; collection: string; } /** Union type for analysis results */ type ImportAnalysis = WxrAnalysis | WpPluginAnalysis; export function WordPressImport() { const [step, setStep] = React.useState("choose"); const [urlInput, setUrlInput] = React.useState(""); const [probeResult, setProbeResult] = React.useState(null); const [importSource, setImportSource] = React.useState(null); const [_file, setFile] = React.useState(null); const [analysis, setAnalysis] = React.useState(null); // Plugin auth state const [pluginUsername, setPluginUsername] = React.useState(""); const [pluginPassword, setPluginPassword] = React.useState(""); const [selections, setSelections] = React.useState>({}); const [prepareResult, setPrepareResult] = React.useState(null); const [result, setResult] = React.useState(null); const [expandedTypes, setExpandedTypes] = React.useState>(new Set()); const [prepareError, setPrepareError] = React.useState(null); const [importError, setImportError] = React.useState(null); const [mediaResult, setMediaResult] = React.useState(null); const [rewriteResult, setRewriteResult] = React.useState(null); const [mediaError, setMediaError] = React.useState(null); const [skipMedia, setSkipMedia] = React.useState(false); const [mediaProgress, setMediaProgress] = React.useState(null); // New state for import options const [importMenus, setImportMenus] = React.useState(true); const [importSiteTitle, setImportSiteTitle] = React.useState(true); const [importLogo, setImportLogo] = React.useState(true); const [importSeo, setImportSeo] = React.useState(false); // Author mapping state const [authorMappings, setAuthorMappings] = React.useState([]); const [emdashUsers, setEmDashUsers] = React.useState([]); // Initialize author mappings from analysis, auto-matching by email const initializeAuthorMappings = React.useCallback( (importAnalysis: ImportAnalysis, users: UserListItem[]) => { const mappings: AuthorMapping[] = importAnalysis.authors.map((author) => { // Try to match by email (case-insensitive) const matchedUser = author.email ? users.find((u) => u.email.toLowerCase() === author.email?.toLowerCase()) : undefined; return { wpLogin: author.login || author.displayName || "unknown", wpDisplayName: author.displayName || author.login || "Unknown", wpEmail: author.email, emdashUserId: matchedUser?.id ?? null, postCount: author.postCount, }; }); setAuthorMappings(mappings); }, [], ); // Check for OAuth callback on mount React.useEffect(() => { const params = new URLSearchParams(window.location.search); const authStatus = params.get("auth"); const error = params.get("error"); if (error === "auth_rejected") { setImportError("WordPress authorization was rejected"); setStep("probe-result"); // Clean up URL window.history.replaceState({}, "", window.location.pathname); return; } if (authStatus === "success") { // Get credentials from cookie const cookie = document.cookie.split("; ").find((row) => row.startsWith("emdash_wp_auth=")); if (cookie) { try { const encoded = cookie.split("=")[1] ?? ""; // URL decode first (cookie values may be URL-encoded), then base64 decode const urlDecoded = decodeURIComponent(encoded); const authData = JSON.parse(atob(urlDecoded)); // Check timestamp (5 minute expiry) if (Date.now() - authData.timestamp < 5 * 60 * 1000) { // Set up import source and start analyzing setImportSource({ type: "wordpress-plugin", url: authData.siteUrl, token: authData.token, }); setUrlInput(authData.siteUrl); // Clear the cookie document.cookie = "emdash_wp_auth=; path=/_emdash/; max-age=0"; // Start analyzing setStep("analyzing-plugin"); wpPluginAnalyzeMutation.mutate({ url: authData.siteUrl, token: authData.token, }); } } catch (e) { console.error("Failed to parse auth cookie:", e); } } // Clean up URL window.history.replaceState({}, "", window.location.pathname); } }, []); // eslint-disable-line react-hooks/exhaustive-deps const { t } = useLingui(); // Probe mutation const probeMutation = useMutation({ mutationFn: probeImportUrl, onSuccess: (data) => { setProbeResult(data); setStep("probe-result"); }, onError: () => { // On error, show probe result step with no matches setProbeResult({ url: urlInput, isWordPress: false, bestMatch: null, allMatches: [], }); setStep("probe-result"); }, }); // Analyze mutation const analyzeMutation = useMutation({ mutationFn: analyzeWxr, onSuccess: async (data) => { setAnalysis(data); // Initialize selections from analysis const initialSelections: Record = {}; for (const pt of data.postTypes) { initialSelections[pt.name] = { enabled: pt.schemaStatus.canImport, collection: pt.suggestedCollection, }; } setSelections(initialSelections); // Initialize menu import state based on analysis if ("navMenus" in data && data.navMenus && data.navMenus.length > 0) { setImportMenus(true); } // Fetch EmDash users for author mapping try { const usersResult = await fetchUsers({ limit: 100 }); setEmDashUsers(usersResult.items); initializeAuthorMappings(data, usersResult.items); } catch { // If user fetch fails, continue without auto-matching initializeAuthorMappings(data, []); } setStep("review"); }, }); // Prepare mutation (create collections/fields) const prepareMutation = useMutation({ mutationFn: prepareWxrImport, onSuccess: (data) => { setPrepareError(null); setPrepareResult(data); if (data.success) { executeImport(); } else { setStep("review"); } }, onError: (error) => { setPrepareError(error instanceof Error ? error.message : t`Failed to prepare import`); setStep("review"); }, }); // Import mutation const importMutation = useMutation({ mutationFn: ({ file, config }: { file: File; config: ImportConfig }) => executeWxrImport(file, config), onSuccess: (data) => { setImportError(null); setResult(data); if (analysis && analysis.attachments.count > 0) { setStep("media"); } else { setStep("complete"); } }, onError: (error) => { setImportError(error instanceof Error ? error.message : t`Failed to execute import`); setStep("review"); }, }); // Media import mutation const mediaMutation = useMutation({ mutationFn: (attachments: AttachmentInfo[]) => importWxrMedia(attachments, (progress) => { setMediaProgress(progress); }), onSuccess: (data) => { setMediaError(null); setMediaProgress(null); setMediaResult(data); if (Object.keys(data.urlMap).length > 0) { setStep("rewriting"); rewriteMutation.mutate(data.urlMap); } else { setStep("complete"); } }, onError: (error) => { setMediaProgress(null); setMediaError(error instanceof Error ? error.message : t`Failed to import media`); setStep("media"); }, }); // URL rewrite mutation const rewriteMutation = useMutation({ mutationFn: (urlMap: Record) => rewriteContentUrls(urlMap), onSuccess: (data) => { setRewriteResult(data); setStep("complete"); }, onError: (error) => { setMediaError(error instanceof Error ? error.message : t`Failed to rewrite URLs`); setStep("complete"); }, }); // WordPress Plugin analyze mutation const wpPluginAnalyzeMutation = useMutation({ mutationFn: ({ url, token }: { url: string; token: string }) => analyzeWpPluginSite(url, token), onSuccess: async (data) => { setAnalysis(data); // Initialize selections from analysis const initialSelections: Record = {}; for (const pt of data.postTypes) { initialSelections[pt.name] = { enabled: pt.schemaStatus.canImport, collection: pt.suggestedCollection, }; } setSelections(initialSelections); // Initialize menu import state based on analysis if ("navMenus" in data && data.navMenus && data.navMenus.length > 0) { setImportMenus(true); } // Fetch EmDash users for author mapping try { const usersResult = await fetchUsers({ limit: 100 }); setEmDashUsers(usersResult.items); initializeAuthorMappings(data, usersResult.items); } catch { // If user fetch fails, continue without auto-matching initializeAuthorMappings(data, []); } setStep("review"); }, onError: (error) => { setImportError(error instanceof Error ? error.message : t`Failed to analyze WordPress site`); setStep("plugin-auth"); }, }); // WordPress Plugin import mutation const wpPluginImportMutation = useMutation({ mutationFn: ({ url, token, config }: { url: string; token: string; config: ImportConfig }) => executeWpPluginImport(url, token, config), onSuccess: (data) => { setImportError(null); setResult(data); if (analysis && analysis.attachments.count > 0) { setStep("media"); } else { setStep("complete"); } }, onError: (error) => { setImportError(error instanceof Error ? error.message : t`Failed to import from WordPress`); setStep("review"); }, }); const handleProbeUrl = (e: React.FormEvent) => { e.preventDefault(); if (!urlInput.trim()) return; setStep("probing"); probeMutation.mutate(urlInput.trim()); }; const handleFileSelect = (e: React.ChangeEvent) => { const selectedFile = e.target.files?.[0]; if (selectedFile) { setFile(selectedFile); setImportSource({ type: "wxr", file: selectedFile }); setStep("upload"); analyzeMutation.mutate(selectedFile); } }; const handleDrop = (e: React.DragEvent) => { e.preventDefault(); const droppedFile = e.dataTransfer.files[0]; if (droppedFile && droppedFile.name.endsWith(".xml")) { setFile(droppedFile); setImportSource({ type: "wxr", file: droppedFile }); setStep("upload"); analyzeMutation.mutate(droppedFile); } }; const handlePluginConnect = () => { if (!probeResult?.url) return; // Check if we're on localhost - OAuth won't work, fall back to manual if (window.location.hostname === "localhost" || window.location.hostname === "127.0.0.1") { setImportError("OAuth authorization requires HTTPS. Please use manual credentials."); setStep("plugin-auth"); return; } // Build the WordPress Application Password authorization URL const wpUrl = probeResult.url.replace(TRAILING_SLASH_REGEX, ""); const callbackUrl = `${window.location.origin}/_emdash/api/import/wordpress-plugin/callback`; // WordPress requires a valid UUID for app_id const appId = "a1b2c3d4-e5f6-7890-abcd-ef1234567890"; const authUrl = new URL(`${wpUrl}/wp-admin/authorize-application.php`); authUrl.searchParams.set("app_name", "EmDash CMS"); authUrl.searchParams.set("app_id", appId); authUrl.searchParams.set("success_url", callbackUrl); // Redirect to WordPress for authorization window.location.href = authUrl.toString(); }; const handlePluginManualAuth = () => { // Fallback to manual password entry setStep("plugin-auth"); }; const handlePluginAuth = (e: React.FormEvent) => { e.preventDefault(); if (!pluginUsername.trim() || !pluginPassword.trim()) return; // Create Basic Auth token const cleanPassword = pluginPassword.replace(WHITESPACE_REGEX, ""); const token = btoa(`${pluginUsername}:${cleanPassword}`); const probeUrl = probeResult?.url; if (!probeUrl) return; setImportSource({ type: "wordpress-plugin", url: probeUrl, token }); setStep("analyzing-plugin"); wpPluginAnalyzeMutation.mutate({ url: probeUrl, token }); }; const executeImport = () => { if (!analysis || !importSource) return; setStep("importing"); // Build author mappings record (wpLogin -> emdashUserId) const authorMappingsRecord: Record = {}; for (const mapping of authorMappings) { authorMappingsRecord[mapping.wpLogin] = mapping.emdashUserId; } // Build extended config with new options const config: ImportConfig = { postTypeMappings: selections, skipExisting: true, authorMappings: authorMappingsRecord, }; if (importSource.type === "wxr") { importMutation.mutate({ file: importSource.file, config, }); } else if (importSource.type === "wordpress-plugin") { wpPluginImportMutation.mutate({ url: importSource.url, token: importSource.token, config, }); } }; const handleStartImport = () => { if (!analysis || !importSource) return; setPrepareError(null); setImportError(null); setPrepareResult(null); // If there are authors to map, show the author mapping step if (analysis.authors.length > 0) { setStep("authors"); return; } // Otherwise, proceed directly to import proceedToImport(); }; const proceedToImport = () => { if (!analysis || !importSource) return; const needsSchemaChanges = analysis.postTypes.filter((pt) => { const selection = selections[pt.name]; if (!selection?.enabled) return false; return ( !pt.schemaStatus.exists || Object.values(pt.schemaStatus.fieldStatus).some((f) => f.status === "missing") ); }); if (needsSchemaChanges.length > 0) { setStep("preparing"); prepareMutation.mutate({ postTypes: needsSchemaChanges.map((pt) => ({ name: pt.name, collection: selections[pt.name]?.collection ?? pt.suggestedCollection, fields: pt.requiredFields, })), }); } else { executeImport(); } }; const handleReset = () => { setStep("choose"); setUrlInput(""); setProbeResult(null); setImportSource(null); setFile(null); setAnalysis(null); setSelections({}); setPrepareResult(null); setPrepareError(null); setImportError(null); setResult(null); setExpandedTypes(new Set()); setMediaResult(null); setRewriteResult(null); setMediaError(null); setSkipMedia(false); setMediaProgress(null); setPluginUsername(""); setPluginPassword(""); // Reset new state setImportMenus(true); setImportSiteTitle(true); setImportLogo(true); setImportSeo(false); // Reset author mappings setAuthorMappings([]); setEmDashUsers([]); }; const handleStartMediaImport = () => { if (!analysis) return; setMediaError(null); setMediaProgress(null); setStep("importing-media"); mediaMutation.mutate(analysis.attachments.items); }; const handleSkipMedia = () => { setSkipMedia(true); setStep("complete"); }; const handleProceedWithUpload = () => { setStep("upload"); }; const toggleExpanded = (name: string) => { setExpandedTypes((prev) => { const next = new Set(prev); if (next.has(name)) { next.delete(name); } else { next.add(name); } return next; }); }; // Calculate summary stats const selectedCount = Object.values(selections).filter((s) => s.enabled).length; const hasIncompatible = analysis?.postTypes.some((pt) => !pt.schemaStatus.canImport) ?? false; const needsNewCollections = analysis?.postTypes.filter((pt) => selections[pt.name]?.enabled && !pt.schemaStatus.exists) .length ?? 0; const needsNewFields = analysis?.postTypes.filter((pt) => { if (!selections[pt.name]?.enabled) return false; if (!pt.schemaStatus.exists) return false; return Object.values(pt.schemaStatus.fieldStatus).some((f) => f.status === "missing"); }).length ?? 0; // Check if we're using the plugin source const isPluginSource = importSource?.type === "wordpress-plugin"; return (

{t`Import from WordPress`}

{t`Import posts, pages, and custom post types from WordPress.`}

{/* Step indicator */}
{analysis && analysis.attachments.count > 0 && ( <>
)}
{/* Choose step - URL input or file upload */} {step === "choose" && ( )} {/* Probing step */} {step === "probing" && (

{t`Checking ${urlInput}...`}

)} {/* Probe result step */} {step === "probe-result" && probeResult && ( )} {/* Plugin auth step */} {step === "plugin-auth" && probeResult && ( setStep("probe-result")} error={importError} /> )} {/* Analyzing WordPress Plugin step */} {step === "analyzing-plugin" && (

{t`Analyzing WordPress site...`}

{t`Fetching content from the EmDash Exporter API.`}

)} {/* Upload step (analyzing file) */} {step === "upload" && ( analyzeMutation.reset()} onBack={() => setStep("choose")} /> )} {/* Review step */} {step === "review" && analysis && ( setSelections((prev) => { const existing = prev[name]; return { ...prev, [name]: { enabled, collection: existing?.collection ?? name, }, }; }) } onStartImport={handleStartImport} onReset={handleReset} // New props for menus and settings importMenus={importMenus} onImportMenusChange={setImportMenus} isPluginSource={isPluginSource} importSiteTitle={importSiteTitle} importLogo={importLogo} importSeo={importSeo} onImportSiteTitleChange={setImportSiteTitle} onImportLogoChange={setImportLogo} onImportSeoChange={setImportSeo} /> )} {/* Author mapping step */} {step === "authors" && analysis && ( { setAuthorMappings((prev) => prev.map((m) => (m.wpLogin === wpLogin ? { ...m, emdashUserId } : m)), ); }} onContinue={proceedToImport} onBack={() => setStep("review")} /> )} {/* Preparing step */} {step === "preparing" && (

{t`Creating collections and fields...`}

)} {/* Importing step */} {step === "importing" && (

{t`Importing content...`}

{t`This may take a while for large exports.`}

)} {/* Media step */} {step === "media" && analysis && ( )} {/* Importing media step */} {step === "importing-media" && ( )} {/* Rewriting URLs step */} {step === "rewriting" && (

{t`Updating content URLs...`}

)} {/* Complete step */} {step === "complete" && result && ( )}
); } // ============================================================================= // Sub-components // ============================================================================= function StepIndicator({ number, label, active, complete, }: { number: number; label: string; active: boolean; complete: boolean; }) { return (
{complete ? : number}
{label}
); } function ChooseStep({ urlInput, onUrlChange, onProbeUrl, onFileSelect, onDrop, }: { urlInput: string; onUrlChange: (url: string) => void; onProbeUrl: (e: React.FormEvent) => void; onFileSelect: (e: React.ChangeEvent) => void; onDrop: (e: React.DragEvent) => void; }) { const { t } = useLingui(); return (
{/* URL input - primary path */}

{t`Enter your WordPress site URL`}

{t`We'll check what import options are available for your site.`}

onUrlChange(e.target.value)} className="flex-1" />
{t`or upload directly`}
{/* File upload - fallback */}
e.preventDefault()} onDrop={onDrop} >

{t`Upload WordPress export file`}

{t`Drag and drop or click to browse (.xml)`}

); } // ============================================================================= // Feature Comparison Component // ============================================================================= interface FeatureComparisonItem { feature: string; wxr: "full" | "partial" | "none"; wxrNote?: string; plugin: "full" | "partial" | "none"; pluginNote?: string; } const FEATURE_COMPARISON: FeatureComparisonItem[] = [ { feature: "Posts & Pages", wxr: "full", plugin: "full" }, { feature: "Media", wxr: "full", plugin: "full" }, { feature: "Categories & Tags", wxr: "full", plugin: "full" }, { feature: "Custom Taxonomies", wxr: "full", plugin: "full" }, { feature: "Featured Images", wxr: "full", plugin: "full" }, { feature: "Menus", wxr: "full", plugin: "full" }, { feature: "Site Settings", wxr: "partial", wxrNote: "Partial", plugin: "full", pluginNote: "Full", }, { feature: "Widgets", wxr: "none", plugin: "full" }, { feature: "ACF Fields", wxr: "none", plugin: "full" }, { feature: "Yoast/RankMath", wxr: "partial", wxrNote: "Raw meta", plugin: "full", pluginNote: "Structured", }, { feature: "Drafts & Private", wxr: "full", plugin: "full" }, ]; function FeatureComparison() { const { t } = useLingui(); return (

{t`Import Capabilities`}

{FEATURE_COMPARISON.map((item) => ( ))}
{t`Feature`} {t`WXR File`} {t`Plugin`}
{item.feature}

{t`For the best import experience, install the`}{" "} {t`EmDash Exporter`}{" "} {t`plugin on your WordPress site.`}

); } function FeatureStatus({ status, note }: { status: "full" | "partial" | "none"; note?: string }) { if (status === "full") { return ( ); } if (status === "partial") { return ( {note && {note}} ); } return ( ); } function ProbeResultStep({ result, onUploadFile, onPluginConnect, onPluginManualAuth, onReset, }: { result: ProbeResult; onUploadFile: () => void; onPluginConnect: () => void; onPluginManualAuth: () => void; onReset: () => void; }) { const { t } = useLingui(); const bestMatch = result.bestMatch; const hasPlugin = bestMatch?.sourceId === "wordpress-plugin"; if (!result.isWordPress) { return (

{t`Couldn't detect WordPress`}

{t`We couldn't connect to a WordPress site at ${result.url}. This could mean the site isn't WordPress, the REST API is disabled, or the site isn't accessible.`}

{t`Export from WordPress manually`}

  1. {t`1. Log into your WordPress admin dashboard`}
  2. {t`2. Go to`} {t`Tools → Export`}
  3. {t`3. Select "All content"`}
  4. {t`4. Click "Download Export File"`}
  5. {t`5. Upload the file here`}
); } // WordPress detected return (
{/* Detection success */}

{t`${bestMatch?.detected.siteTitle || "WordPress site"} detected`}

{hasPlugin ? t`EmDash Exporter plugin detected! You can import directly.` : t`This is a WordPress site.`}

{/* Preview counts if available */} {bestMatch?.preview && (

{t`Content found:`}

{bestMatch.preview.posts !== undefined && (

{bestMatch.preview.posts}

{t`Posts`}

)} {bestMatch.preview.pages !== undefined && (

{bestMatch.preview.pages}

{t`Pages`}

)} {bestMatch.preview.media !== undefined && (

{bestMatch.preview.media}

{t`Media`}

)}
)} {/* Feature comparison - only show when plugin is NOT detected (to explain the benefits) */} {!hasPlugin && } {/* EmDash Exporter plugin detected - primary option */} {hasPlugin && (

{t`Import via EmDash Exporter`}

{t`Import all content directly including drafts, custom post types, ACF fields, and SEO data. No file download needed.`}

{t`You'll be redirected to WordPress to authorize the connection.`}

)} {/* File upload fallback */}

{hasPlugin ? t`Or upload an export file` : t`Upload an export file`}

{hasPlugin ? t`Alternatively, you can export from WordPress (Tools → Export) and upload the file.` : bestMatch?.capabilities.privateContent ? t`Export your content from WordPress to import everything including drafts.` : t`For a complete import including drafts and all content, export from WordPress.`}

{bestMatch?.suggestedAction.type === "upload" && (

{bestMatch.suggestedAction.instructions}

)}
); } function PluginAuthStep({ siteTitle, siteUrl, username, password, onUsernameChange, onPasswordChange, onSubmit, onBack, error, }: { siteTitle?: string; siteUrl: string; username: string; password: string; onUsernameChange: (value: string) => void; onPasswordChange: (value: string) => void; onSubmit: (e: React.FormEvent) => void; onBack: () => void; error: string | null; }) { const { t } = useLingui(); return (

{t`Connect to ${siteTitle || "WordPress"}`}

{t`Enter your WordPress credentials to import content directly.`}

{error && (

{error}

)}
onUsernameChange(e.target.value)} placeholder="admin" autoComplete="username" />
onPasswordChange(e.target.value)} placeholder="xxxx xxxx xxxx xxxx xxxx xxxx" autoComplete="current-password" />

{t`Create one in WordPress: Users → Profile → Application Passwords`}

{t`How to create an Application Password`}

  1. {t`1. Log into your WordPress admin`}
  2. {t`2. Go to Users → Profile`}
  3. {t`3. Scroll to "Application Passwords"`}
  4. {t`4. Enter "EmDash" and click "Add New"`}
  5. {t`5. Copy the generated password`}
{t`Open WordPress Profile`}
); } function UploadStep({ isLoading, error, onFileSelect, onDrop, onRetry, onBack, }: { isLoading: boolean; error: Error | null; onFileSelect: (e: React.ChangeEvent) => void; onDrop: (e: React.DragEvent) => void; onRetry: () => void; onBack?: () => void; }) { const { t } = useLingui(); return (
e.preventDefault()} onDrop={onDrop} > {isLoading ? (

{t`Analyzing export file...`}

) : error ? (

{error.message}

) : ( <>

{t`Drop your WordPress export file here`}

{t`Or click to browse. Accepts .xml files exported from WordPress.`}

)}
{onBack && !isLoading && !error && ( )}
); } // ============================================================================= // Menu Info (Type-safe helper for navMenus) // ============================================================================= interface NavMenuItem { name: string; slug: string; count: number; } function getNavMenus(analysis: ImportAnalysis): NavMenuItem[] | undefined { if ("navMenus" in analysis && Array.isArray(analysis.navMenus)) { return analysis.navMenus as NavMenuItem[]; } return undefined; } // ============================================================================= // Review Step with Menus and Settings // ============================================================================= function ReviewStep({ analysis, selections, expandedTypes, prepareError, importError, prepareResult, selectedCount, hasIncompatible, needsNewCollections, needsNewFields, onToggleExpand, onToggleEnabled, onStartImport, onReset, // New props importMenus, onImportMenusChange, isPluginSource, importSiteTitle, importLogo, importSeo, onImportSiteTitleChange, onImportLogoChange, onImportSeoChange, }: { analysis: ImportAnalysis; selections: Record; expandedTypes: Set; prepareError: string | null; importError: string | null; prepareResult: PrepareResult | null; selectedCount: number; hasIncompatible: boolean; needsNewCollections: number; needsNewFields: number; onToggleExpand: (name: string) => void; onToggleEnabled: (name: string, enabled: boolean) => void; onStartImport: () => void; onReset: () => void; // New props importMenus: boolean; onImportMenusChange: (value: boolean) => void; isPluginSource: boolean; importSiteTitle: boolean; importLogo: boolean; importSeo: boolean; onImportSiteTitleChange: (value: boolean) => void; onImportLogoChange: (value: boolean) => void; onImportSeoChange: (value: boolean) => void; }) { const { t } = useLingui(); const navMenus = getNavMenus(analysis); const hasMenus = navMenus && navMenus.length > 0; return (
{/* Site info */}

{analysis.site.title}

{analysis.site.url}

{/* Errors */} {(prepareError || importError) && (

{prepareError ? t`Schema preparation failed` : t`Import failed`}

{prepareError || importError}

)} {prepareResult && !prepareResult.success && (

{t`Failed to create some collections`}

    {prepareResult.errors.map((err, i) => (
  • {err.collection}:{" "} {err.error}
  • ))}
)} {/* Post type list */}

{t`Content to Import`}

{t`Select which content types to import.`}

{analysis.postTypes.map((pt) => ( onToggleExpand(pt.name)} onToggleEnabled={(enabled) => onToggleEnabled(pt.name, enabled)} /> ))}
{/* Structure section - Menus and Taxonomies */} {hasMenus && (

{t`Structure`}

{t`Additional data to import.`}

{/* Menus */}
onImportMenusChange(checked)} label={

{t`Menus (${navMenus.length})`}

{navMenus.map((m) => m.name).join(", ")}

} />
{/* Categories count */} {analysis.categories > 0 && (
{}} disabled label={

{t`Categories (${analysis.categories})`}

} />
)} {/* Tags count */} {analysis.tags > 0 && (
{}} disabled label={

{t`Tags (${analysis.tags})`}

} />
)}
)} {/* Site Settings section - Plugin only */} {isPluginSource && (

{t`Settings`}

{t`Import site configuration from WordPress.`}

{/* Site title & tagline */}
onImportSiteTitleChange(checked)} label={

{t`Site title & tagline`}

} />
{/* Logo & favicon */}
onImportLogoChange(checked)} label={

{t`Logo & favicon`}

} />
{/* SEO settings */}
onImportSeoChange(checked)} label={

{t`SEO settings (Yoast)`}

{t`Meta titles, descriptions, and social images`}

} />
)} {hasIncompatible && (

{t`Some content types cannot be imported`}

{t`The existing collection has fields with incompatible types.`}

)} {selectedCount > 0 && (

{t`What will happen when you import`}

    {needsNewCollections > 0 && (
  • {plural(needsNewCollections, { one: "# new collection will be created", other: "# new collections will be created", })}
  • )} {needsNewFields > 0 && (
  • {plural(needsNewFields, { one: "Fields will be added to # existing collection", other: "Fields will be added to # existing collections", })}
  • )}
  • {t`${analysis.postTypes .filter((pt) => selections[pt.name]?.enabled) .reduce((sum, pt) => sum + pt.count, 0)} items will be imported`}
  • {hasMenus && importMenus && (
  • {plural(navMenus.length, { one: "# menu will be imported", other: "# menus will be imported", })}
  • )}
)}
); } function PostTypeRow({ postType, selection, expanded, onToggleExpand, onToggleEnabled, }: { postType: PostTypeAnalysis; selection: PostTypeSelection | undefined; expanded: boolean; onToggleExpand: () => void; onToggleEnabled: (enabled: boolean) => void; }) { const { t } = useLingui(); const { schemaStatus } = postType; const canImport = schemaStatus.canImport; const isNew = !schemaStatus.exists; const hasMissingFields = schemaStatus.exists && Object.values(schemaStatus.fieldStatus).some((f) => f.status === "missing"); return (
onToggleEnabled(checked)} aria-label={t`Import ${postType.name}`} />
{!canImport ? ( {t`Incompatible`} ) : isNew ? ( {t`New collection`} ) : hasMissingFields ? ( {t`Add fields`} ) : ( {t`Ready`} )}
{expanded && (
{!canImport && schemaStatus.reason && (
{schemaStatus.reason}
)}

{t`Required fields:`}

{postType.requiredFields.map((field) => { const status = schemaStatus.fieldStatus[field.slug]; return (
{field.label} ({field.type}) {status?.status === "compatible" ? ( {t`Exists`} ) : status?.status === "missing" ? ( {t`Will create`} ) : status?.status === "type_mismatch" ? ( {t`Type mismatch (${status.existingType})`} ) : null}
); })}
)}
); } function MediaStep({ attachments, error, onImport, onSkip, }: { attachments: { count: number; items: AttachmentInfo[] }; error: string | null; onImport: () => void; onSkip: () => void; }) { const { t } = useLingui(); const byType = attachments.items.reduce( (acc, att) => { const type = att.mimeType?.split("/")[0] || "other"; acc[type] = (acc[type] || 0) + 1; return acc; }, {} as Record, ); return (

{t`Import Media Files`}

{t`Your WordPress export contains ${attachments.count} media files.`}

{Object.entries(byType).map(([type, count]) => (

{type}

{plural(count, { one: "# file", other: "# files" })}

))}
{error && (

{error}

)}

{t`What happens when you import:`}

  • {t`• Files are downloaded from your WordPress site`}
  • {t`• Uploaded to your EmDash media storage`}
  • {t`• URLs in your content are updated automatically`}
); } function MediaProgressStep({ progress, total, }: { progress: MediaImportProgress | null; total: number; }) { const { t } = useLingui(); const current = progress?.current ?? 0; const percentage = total > 0 ? Math.round((current / total) * 100) : 0; const statusLabels: Record = { downloading: t`Downloading`, uploading: t`Uploading`, done: t`Done`, skipped: t`Skipped`, failed: t`Failed`, }; return (

{percentage}%

{t`Importing Media`}

{t`${current} of ${total}`} {percentage}%
{progress && (
{progress.filename || t`File ${progress.current}`} {statusLabels[progress.status]}
{progress.error &&

{progress.error}

}
)} {!progress && (

{t`Preparing to download files from WordPress...`}

)}
); } function CompleteStep({ result, prepareResult, mediaResult, rewriteResult, skippedMedia, onReset, }: { result: ImportResult; prepareResult: PrepareResult | null; mediaResult: MediaImportResult | null; rewriteResult: RewriteUrlsResult | null; skippedMedia: boolean; onReset: () => void; }) { const hasMediaErrors = mediaResult && mediaResult.failed.length > 0; const hasContentErrors = result.errors.length > 0; const overallSuccess = !hasContentErrors && (!mediaResult || mediaResult.failed.length === 0); const wasMediaOnlyImport = result.imported === 0 && result.skipped > 0 && mediaResult && mediaResult.imported.length > 0; const { t } = useLingui(); const getSummaryMessage = () => { const parts: string[] = []; if (result.imported > 0) { parts.push( plural(result.imported, { one: "# content item imported", other: "# content items imported", }), ); } if (result.skipped > 0 && result.imported > 0) { parts.push( plural(result.skipped, { one: "# skipped (already exists)", other: "# skipped (already exist)", }), ); } if (mediaResult && mediaResult.imported.length > 0) { parts.push( plural(mediaResult.imported.length, { one: "# media file imported", other: "# media files imported", }), ); } if (hasContentErrors) { parts.push( plural(result.errors.length, { one: "# content error", other: "# content errors" }), ); } if (hasMediaErrors) { parts.push( plural(mediaResult.failed.length, { one: "# media error", other: "# media errors" }), ); } return parts.join(" · "); }; return (
{overallSuccess ? ( ) : ( )}

{overallSuccess ? wasMediaOnlyImport ? t`Media Import Complete` : t`Import Complete` : t`Import Completed with Errors`}

{getSummaryMessage()}

{wasMediaOnlyImport && (

{t`Content was skipped because it already exists`}

)} {skippedMedia && (

{t`Media import was skipped`}

)}
{prepareResult && (prepareResult.collectionsCreated.length > 0 || prepareResult.fieldsCreated.length > 0) && (

{t`Schema Changes`}

{prepareResult.collectionsCreated.length > 0 && (

{t`Collections created:`}{" "} {prepareResult.collectionsCreated.join(", ")}

)} {prepareResult.fieldsCreated.length > 0 && (

{t`Fields created:`}{" "} {prepareResult.fieldsCreated.map((f) => `${f.collection}.${f.field}`).join(", ")}

)}
)} {Object.keys(result.byCollection).length > 0 && (

{t`Imported by Collection`}

{Object.entries(result.byCollection).map(([collection, count]) => (
{collection} {plural(count, { one: "# item", other: "# items" })}
))}
)} {mediaResult && mediaResult.imported.length > 0 && (

{t`Media Import`}

{mediaResult.imported.length} {t`files imported`}

{rewriteResult && rewriteResult.updated > 0 && (

{rewriteResult.urlsRewritten} {t`image URLs updated in`}{" "} {rewriteResult.updated} {t`content items`}

)}
)} {result.errors.length > 0 && (

{t`Content Errors (${result.errors.length})`}

{result.errors.map((error, i) => (

{error.title}

{error.error}

))}
)} {hasMediaErrors && (

{t`Media Errors (${mediaResult.failed.length})`}

{mediaResult.failed.map((error, i) => (

{error.originalUrl}

{error.error}

))}
)}
{t`Go to Dashboard`}
); } // ============================================================================= // Author Mapping Step // ============================================================================= function AuthorMappingStep({ authorMappings, emdashUsers, onMappingChange, onContinue, onBack, }: { authorMappings: AuthorMapping[]; emdashUsers: UserListItem[]; onMappingChange: (wpLogin: string, emdashUserId: string | null) => void; onContinue: () => void; onBack: () => void; }) { const { t } = useLingui(); // Count matched vs unmatched const matchedCount = authorMappings.filter((m) => m.emdashUserId !== null).length; const totalCount = authorMappings.length; return (

{t`Map Authors`}

{t`Assign WordPress authors to EmDash users. Posts will be attributed to the selected user.`}

{matchedCount > 0 && (

{t`${matchedCount} of ${totalCount} authors matched by email`}

)}

{t`Author Mapping`}

{t`${matchedCount} of ${totalCount} assigned`}
{authorMappings.map((mapping) => (

{mapping.wpDisplayName}

{mapping.wpEmail || mapping.wpLogin} {mapping.postCount > 0 && ( • {plural(mapping.postCount, { one: "# post", other: "# posts" })} )}