@burdenoff/microfe-export 2026.1013.1 → 2026.1013.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -434,7 +434,7 @@ function E() {
434
434
  }), D.length > 0 ? /* @__PURE__ */ C("select", {
435
435
  value: B ?? "",
436
436
  onChange: (e) => V(e.target.value || void 0),
437
- className: "w-full px-3 py-2 border-2 border-border-subtle rounded-lg bg-bg-surface focus:outline-none focus:ring-2 focus:ring-primary/20 focus:border-primary",
437
+ className: "w-full px-3 py-2 border-2 border-border-subtle rounded-lg bg-bg-surface focus:outline-none focus:ring-2 focus:ring-primary/20 focus:border-primary scheme-normal dark:scheme-dark [&_option]:bg-bg-surface [&_option]:text-text-primary",
438
438
  children: [/* @__PURE__ */ S("option", {
439
439
  value: "",
440
440
  children: _("export.create.options.noTemplate", "No template")
@@ -1 +1 @@
1
- {"version":3,"file":"CreateExportPage.js","names":[],"sources":["../../../src/export/pages/CreateExportPage.tsx"],"sourcesContent":["import { useState, useEffect, useRef } from \"react\";\nimport { useSearchParams } from \"react-router\";\nimport { isQuotaExhaustedError } from \"@burdenoff/fe-libs/shared/utils\";\nimport {\n ArrowLeft,\n ArrowRight,\n Download,\n CheckCircle2,\n Loader2,\n FileSpreadsheet,\n FileJson,\n Package,\n CalendarRange,\n User,\n Users,\n Building2,\n Lock,\n} from \"lucide-react\";\nimport { usePermission } from \"@burdenoff/fe-libs/shared/providers/shell\";\nimport {\n AlertDialog,\n AlertDialogAction,\n AlertDialogCancel,\n AlertDialogContent,\n AlertDialogDescription,\n AlertDialogFooter,\n AlertDialogHeader,\n AlertDialogTitle,\n EmphasisPanel,\n ShowMore,\n PagePurpose,\n IllustratedEmptyState,\n} from \"@burdenoff/fe-libs/ui\";\nimport { useExport } from \"../providers/ExportProvider\";\nimport {\n useCreateExportJob,\n useAvailableEntities,\n useExportTemplates,\n} from \"../hooks/useExportQueries\";\nimport { getFormatDisplayName } from \"../constants/enums\";\nimport { PageLayout } from \"../components/PageLayout\";\nimport { useTr } from \"../hooks/useTr\";\nimport { cn } from \"../utils/cn\";\nimport { isExportEntityDisabled } from \"../utils/disabledModules\";\nimport type {\n ExportAvailableEntity,\n ExportFormat,\n ExportScope,\n} from \"../types\";\n\ntype WizardStep = \"entities\" | \"options\" | \"review\";\n\n// Backend only supports CSV and JSON\nconst SUPPORTED_FORMATS: ExportFormat[] = [\"CSV\", \"JSON\"];\n\nconst FORMAT_ICONS: Record<ExportFormat, typeof FileJson> = {\n CSV: FileSpreadsheet,\n JSON: FileJson,\n};\n\nconst SCOPE_ICONS: Record<ExportScope, typeof User> = {\n INDIVIDUAL: User,\n WORKSPACE_ADMIN: Users,\n ORG_ADMIN: Building2,\n};\n\n/**\n * Create Export Page\n * Multi-step form: select entities, configure options, review & start\n */\n\nexport function CreateExportPage() {\n const { basePath = \"/\", navigate, organizationId } = useExport();\n const tr = useTr();\n const { createJob, isPending } = useCreateExportJob();\n const { data: availableEntities } = useAvailableEntities();\n const { data: templates } = useExportTemplates();\n const [searchParams] = useSearchParams();\n\n // RBAC: Check permissions for admin scopes\n const canExportWorkspaceAdmin = usePermission(\"export:workspace:admin\");\n const canExportOrgAdmin = usePermission(\"export:org:admin\");\n\n const [step, setStep] = useState<WizardStep>(\"entities\");\n const [selectedEntities, setSelectedEntities] = useState<string[]>([]);\n const [exportName, setExportName] = useState(\"\");\n const [selectedFormat, setSelectedFormat] = useState<ExportFormat>(\"CSV\");\n const [selectedScope, setSelectedScope] = useState<ExportScope>(\"INDIVIDUAL\");\n const [selectedTemplateId, setSelectedTemplateId] = useState<\n string | undefined\n >(searchParams.get(\"templateId\") ?? undefined);\n const [dateRangeStart, setDateRangeStart] = useState(\"\");\n const [dateRangeEnd, setDateRangeEnd] = useState(\"\");\n const [error, setError] = useState<string | null>(null);\n const [showQuotaDialog, setShowQuotaDialog] = useState(false);\n\n // Pre-populate form when arriving from \"Use Template\" — apply only once per templateId\n const appliedTemplateRef = useRef<string | null>(null);\n useEffect(() => {\n const paramTemplateId = searchParams.get(\"templateId\");\n if (!paramTemplateId || !templates?.length) return;\n if (appliedTemplateRef.current === paramTemplateId) return;\n const tpl = templates.find((t) => t.id === paramTemplateId);\n if (!tpl) return;\n appliedTemplateRef.current = paramTemplateId;\n setSelectedTemplateId(tpl.id);\n // Skip entity types whose product export is currently disabled.\n setSelectedEntities(\n (tpl.entityTypes ?? []).filter((et) => !isExportEntityDisabled(et)),\n );\n const normalizedFormat = tpl.format?.toUpperCase() as\n | ExportFormat\n | undefined;\n if (normalizedFormat && SUPPORTED_FORMATS.includes(normalizedFormat)) {\n setSelectedFormat(normalizedFormat);\n }\n if (tpl.name) setExportName(tpl.name);\n }, [searchParams, templates]);\n\n const handleBack = () => {\n if (step === \"options\") setStep(\"entities\");\n else if (step === \"review\") setStep(\"options\");\n else navigate?.(basePath);\n };\n\n const handleNext = () => {\n if (step === \"entities\") {\n if (selectedEntities.length === 0) {\n setError(\n tr(\n \"export.create.errors.selectEntity\",\n \"Please select at least one entity type to export\",\n ),\n );\n return;\n }\n setError(null);\n setStep(\"options\");\n } else if (step === \"options\") {\n if (!exportName.trim()) {\n setError(\n tr(\n \"export.create.errors.exportNameRequired\",\n \"Please provide a name for this export\",\n ),\n );\n return;\n }\n setError(null);\n setStep(\"review\");\n }\n };\n\n const handleSubmit = async () => {\n try {\n setError(null);\n await createJob({\n name: exportName,\n entityTypes: selectedEntities,\n format: selectedFormat,\n exportScope: selectedScope,\n organizationId:\n selectedScope === \"ORG_ADMIN\" ? organizationId : undefined,\n templateId: selectedTemplateId,\n dateRangeStart: dateRangeStart\n ? new Date(dateRangeStart).toISOString()\n : null,\n dateRangeEnd: dateRangeEnd\n ? new Date(dateRangeEnd).toISOString()\n : null,\n });\n navigate?.(basePath);\n } catch (err) {\n if (isQuotaExhaustedError(err)) {\n setShowQuotaDialog(true);\n setError(\n tr(\n \"export.create.errors.quotaReached\",\n \"You have reached your export limit for this month.\",\n ),\n );\n return;\n }\n setError(\n err instanceof Error\n ? err.message\n : tr(\"export.create.errors.createFailed\", \"Failed to create export\"),\n );\n }\n };\n\n const toggleEntity = (entityType: string) => {\n // Disabled-product entities can't be selected.\n if (isExportEntityDisabled(entityType)) return;\n setSelectedEntities((prev) =>\n prev.includes(entityType)\n ? prev.filter((e) => e !== entityType)\n : [...prev, entityType],\n );\n };\n\n const getScopeLabel = (scope: ExportScope) => {\n switch (scope) {\n case \"INDIVIDUAL\":\n return tr(\"export.create.scope.individual.label\", \"My Data Only\");\n case \"WORKSPACE_ADMIN\":\n return tr(\n \"export.create.scope.workspaceAdmin.label\",\n \"All Workspace Data\",\n );\n case \"ORG_ADMIN\":\n return tr(\n \"export.create.scope.orgAdmin.label\",\n \"All Organization Data\",\n );\n default:\n return scope;\n }\n };\n\n const getScopeDescription = (scope: ExportScope) => {\n switch (scope) {\n case \"INDIVIDUAL\":\n return tr(\n \"export.create.scope.individual.description\",\n \"Export data you personally created across selected modules.\",\n );\n case \"WORKSPACE_ADMIN\":\n return tr(\n \"export.create.scope.workspaceAdmin.description\",\n \"Export all data in this workspace regardless of who created it. Requires workspace admin access.\",\n );\n case \"ORG_ADMIN\":\n return tr(\n \"export.create.scope.orgAdmin.description\",\n \"Export data from every workspace in your organization. Requires org-admin access.\",\n );\n default:\n return \"\";\n }\n };\n\n const fallbackEntityCopy: Record<\n string,\n { label: string; description: string }\n > = {\n users: {\n label: tr(\"export.create.entities.users.label\", \"Users\"),\n description: tr(\n \"export.create.entities.users.description\",\n \"Workspace members and profiles\",\n ),\n },\n projects: {\n label: tr(\"export.create.entities.projects.label\", \"Projects\"),\n description: tr(\n \"export.create.entities.projects.description\",\n \"Projects and their metadata\",\n ),\n },\n tasks: {\n label: tr(\"export.create.entities.tasks.label\", \"Tasks\"),\n description: tr(\n \"export.create.entities.tasks.description\",\n \"Task items and assignments\",\n ),\n },\n files: {\n label: tr(\"export.create.entities.files.label\", \"Files\"),\n description: tr(\n \"export.create.entities.files.description\",\n \"File metadata and references\",\n ),\n },\n conversations: {\n label: tr(\"export.create.entities.conversations.label\", \"Conversations\"),\n description: tr(\n \"export.create.entities.conversations.description\",\n \"Chat messages and threads\",\n ),\n },\n tags: {\n label: tr(\"export.create.entities.tags.label\", \"Tags\"),\n description: tr(\n \"export.create.entities.tags.description\",\n \"Tags and vocabularies\",\n ),\n },\n activity: {\n label: tr(\"export.create.entities.activity.label\", \"Activity Logs\"),\n description: tr(\n \"export.create.entities.activity.description\",\n \"Audit trail and activity history\",\n ),\n },\n calendar: {\n label: tr(\"export.create.entities.calendar.label\", \"Calendar Events\"),\n description: tr(\n \"export.create.entities.calendar.description\",\n \"Events and bookings\",\n ),\n },\n };\n\n // Entities the backend marks available, split so the ones a user can export\n // show first and disabled-product entities are listed (still visible) below.\n const visibleEntities = availableEntities.filter((e) => e.enabled !== false);\n const availableForExport = visibleEntities.filter(\n (e) => !isExportEntityDisabled(e.entityType),\n );\n const unavailableForExport = visibleEntities.filter((e) =>\n isExportEntityDisabled(e.entityType),\n );\n\n const renderEntityCard = (\n entity: ExportAvailableEntity,\n isDisabled: boolean,\n ) => {\n const isSelected =\n !isDisabled && selectedEntities.includes(entity.entityType);\n return (\n <button\n type=\"button\"\n key={entity.entityType}\n onClick={() => toggleEntity(entity.entityType)}\n disabled={isDisabled}\n aria-disabled={isDisabled}\n title={\n isDisabled\n ? tr(\n \"export.create.select.unavailableHint\",\n \"Export for this product isn't available yet\",\n )\n : undefined\n }\n className={cn(\n \"text-left border-2 rounded-xl p-4 transition-all duration-200\",\n isDisabled\n ? \"border-border-subtle/30 opacity-50 cursor-not-allowed\"\n : isSelected\n ? \"border-primary bg-action-primary-bg/5 shadow-md\"\n : \"border-border-subtle/50 hover:border-primary/30 hover:shadow-sm\",\n )}\n >\n <div className=\"flex items-center justify-between mb-2 gap-2\">\n <h3 className=\"font-semibold\">\n {fallbackEntityCopy[entity.entityType]?.label ?? entity.displayName}\n </h3>\n {isDisabled ? (\n <span className=\"shrink-0 text-xs font-medium px-2 py-0.5 rounded-full bg-bg-sunken text-text-secondary\">\n {tr(\"export.create.select.unavailable\", \"Unavailable\")}\n </span>\n ) : (\n isSelected && <CheckCircle2 size={18} className=\"text-primary\" />\n )}\n </div>\n <p className=\"text-sm text-text-secondary\">\n {fallbackEntityCopy[entity.entityType]?.description ??\n entity.description}\n </p>\n {entity.estimatedSize && (\n <p className=\"text-xs text-text-secondary mt-2\">\n ~{entity.estimatedSize}\n </p>\n )}\n </button>\n );\n };\n\n return (\n <PageLayout showHeader={false}>\n {/* Header */}\n <div className=\"relative overflow-hidden rounded-xl bg-accent-wash border border-border-seam p-6 shadow-[var(--shadow-pop)]\">\n <div className=\"relative z-10 flex items-center gap-4\">\n <button\n type=\"button\"\n onClick={() => navigate?.(basePath)}\n className=\"p-2.5 hover:bg-accent rounded-lg transition-colors duration-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/40\"\n aria-label={tr(\n \"export.create.actions.backToDashboard\",\n \"Back to export dashboard\",\n )}\n >\n <ArrowLeft size={20} />\n </button>\n <div className=\"flex items-center gap-3\">\n <div className=\"p-2.5 rounded-lg bg-action-primary-bg/10 backdrop-blur-sm\">\n <Download size={24} className=\"text-primary\" />\n </div>\n <h1 className=\"text-2xl sm:text-3xl font-bold\">\n {tr(\"export.create.title\", \"New Export\")}\n </h1>\n </div>\n </div>\n </div>\n\n {/* Page purpose — explains the wizard's job in plain language */}\n <PagePurpose icon={<Download size={16} />}>\n {tr(\n \"export.create.purpose\",\n \"Build a one-off export in three steps: pick which data to include, choose a format and scope, then review and start it. The export runs in the background and produces a downloadable ZIP you can grab from History when it finishes.\",\n )}\n </PagePurpose>\n\n {/* Step Indicator */}\n <div className=\"flex items-center gap-2 justify-center flex-wrap\">\n {([\"entities\", \"options\", \"review\"] as WizardStep[]).map((s, i) => (\n <div key={s} className=\"flex items-center gap-2\">\n {i > 0 && <ArrowRight size={18} className=\"text-text-secondary\" />}\n <div\n className={cn(\n \"flex items-center gap-2 px-4 py-2.5 rounded-lg text-sm font-medium border-2 transition-all duration-200\",\n step === s\n ? \"bg-action-primary-bg text-action-primary-text border-primary shadow-[var(--shadow-pop)]\"\n : \"bg-bg-sunken/50 text-text-secondary border-border-seam\",\n )}\n >\n <div className=\"flex items-center justify-center size-6 rounded-full bg-bg-surface/20 font-bold\">\n {i + 1}\n </div>\n <span className=\"hidden sm:inline capitalize\">\n {s === \"entities\"\n ? tr(\"export.create.steps.entities\", \"Select Data\")\n : s === \"options\"\n ? tr(\"export.create.steps.options\", \"Options\")\n : tr(\"export.create.steps.review\", \"Review\")}\n </span>\n </div>\n </div>\n ))}\n </div>\n\n {/* Error */}\n {error && (\n <div className=\"border-2 border-border-default rounded-xl bg-status-error-bg-subtle p-4 text-status-error-text text-sm\">\n {error}\n </div>\n )}\n\n {/* Step 1: Select Entities */}\n {step === \"entities\" && (\n <div className=\"space-y-4\">\n <h2 className=\"text-lg font-bold\">\n {tr(\"export.create.select.title\", \"Select data to export\")}\n </h2>\n <p className=\"text-sm text-text-secondary\">\n {tr(\n \"export.create.select.description\",\n \"Choose which entity types to include in your export.\",\n )}\n </p>\n {visibleEntities.length === 0 ? (\n <IllustratedEmptyState\n illustration=\"empty-data\"\n title={tr(\n \"export.create.select.emptyTitle\",\n \"Nothing to export yet\",\n )}\n description={tr(\n \"export.create.select.empty\",\n \"No exportable entity types are configured for this workspace yet. Once your workspace has data in supported modules, they'll show up here to select.\",\n )}\n />\n ) : (\n <div className=\"space-y-6\">\n {availableForExport.length > 0 && (\n <div className=\"grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3\">\n {availableForExport.map((entity) =>\n renderEntityCard(entity, false),\n )}\n </div>\n )}\n {unavailableForExport.length > 0 && (\n <div className=\"space-y-3\">\n <div className=\"flex items-center gap-3\">\n <div className=\"h-px flex-1 bg-border/60\" />\n <span className=\"text-xs font-medium uppercase tracking-wide text-text-secondary\">\n {tr(\n \"export.create.select.unavailableSection\",\n \"Not available yet\",\n )}\n </span>\n <div className=\"h-px flex-1 bg-border/60\" />\n </div>\n <div className=\"grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3\">\n {unavailableForExport.map((entity) =>\n renderEntityCard(entity, true),\n )}\n </div>\n </div>\n )}\n </div>\n )}\n </div>\n )}\n\n {/* Step 2: Configure Options */}\n {step === \"options\" && (\n <div className=\"space-y-6\">\n <div className=\"border border-border-seam rounded-xl p-6 space-y-5 bg-bg-surface\">\n <h2 className=\"text-lg font-bold\">\n {tr(\"export.create.options.title\", \"Export Options\")}\n </h2>\n\n {/* Scope selection */}\n <div>\n <label className=\"block text-sm font-semibold mb-2\">\n {tr(\"export.create.options.scope\", \"Export Scope\")}\n </label>\n <div className=\"grid grid-cols-1 sm:grid-cols-3 gap-3\">\n {(\n [\n \"INDIVIDUAL\",\n \"WORKSPACE_ADMIN\",\n \"ORG_ADMIN\",\n ] as ExportScope[]\n ).map((value) => {\n // RBAC: Disable admin scopes if user lacks permission\n const isDisabled =\n (value === \"WORKSPACE_ADMIN\" && !canExportWorkspaceAdmin) ||\n (value === \"ORG_ADMIN\" && !canExportOrgAdmin);\n const Icon = SCOPE_ICONS[value];\n const disabledTitle =\n value === \"WORKSPACE_ADMIN\"\n ? tr(\n \"export.create.scope.workspaceAdmin.required\",\n \"Workspace admin access required\",\n )\n : tr(\n \"export.create.scope.orgAdmin.required\",\n \"Organization admin access required\",\n );\n\n return (\n <button\n key={value}\n type=\"button\"\n onClick={() => !isDisabled && setSelectedScope(value)}\n disabled={isDisabled}\n className={cn(\n \"text-left border-2 rounded-xl p-4 transition-all duration-200\",\n selectedScope === value\n ? \"border-primary bg-action-primary-bg/5 shadow-md\"\n : isDisabled\n ? \"border-border-subtle/30 opacity-50 cursor-not-allowed\"\n : \"border-border-subtle/50 hover:border-primary/30 hover:shadow-sm\",\n )}\n title={isDisabled ? disabledTitle : undefined}\n >\n <div className=\"flex items-center justify-between mb-2\">\n <Icon\n size={18}\n className={\n selectedScope === value\n ? \"text-primary\"\n : isDisabled\n ? \"text-text-secondary/50\"\n : \"text-text-secondary\"\n }\n />\n {selectedScope === value && (\n <CheckCircle2 size={16} className=\"text-primary\" />\n )}\n {isDisabled && (\n <Lock size={14} className=\"text-text-secondary/50\" />\n )}\n </div>\n <p className=\"font-semibold text-sm\">\n {getScopeLabel(value)}\n </p>\n <p className=\"text-xs text-text-secondary mt-1\">\n {getScopeDescription(value)}\n </p>\n </button>\n );\n })}\n </div>\n </div>\n\n {/* Name */}\n <div>\n <label\n htmlFor=\"exportName\"\n className=\"block text-sm font-semibold mb-2\"\n >\n {tr(\"export.create.options.name\", \"Export Name\")}{\" \"}\n <span className=\"text-status-error-text\">*</span>\n </label>\n <input\n type=\"text\"\n id=\"exportName\"\n aria-label={tr(\"export.create.options.name\", \"Export Name\")}\n value={exportName}\n onChange={(e) => setExportName(e.target.value)}\n className=\"w-full px-4 py-2.5 border-2 border-border-subtle rounded-lg bg-bg-surface focus:outline-none focus:ring-2 focus:ring-primary/20 focus:border-primary transition-all duration-200\"\n placeholder={tr(\n \"export.create.options.namePlaceholder\",\n \"e.g., Monthly User Export\",\n )}\n />\n </div>\n\n {/* Format — only CSV and JSON are supported */}\n <div>\n <label className=\"block text-sm font-semibold mb-2\">\n {tr(\"export.create.options.outputFormat\", \"Output Format\")}\n </label>\n <div className=\"grid grid-cols-2 gap-3\">\n {SUPPORTED_FORMATS.map((format) => {\n const Icon = FORMAT_ICONS[format];\n return (\n <button\n type=\"button\"\n key={format}\n onClick={() => setSelectedFormat(format)}\n className={cn(\n \"flex items-center gap-3 border-2 rounded-lg p-3 transition-all duration-200\",\n selectedFormat === format\n ? \"border-primary bg-action-primary-bg/5\"\n : \"border-border-subtle/50 hover:border-primary/30\",\n )}\n >\n <Icon\n size={18}\n className={\n selectedFormat === format\n ? \"text-primary\"\n : \"text-text-secondary\"\n }\n />\n <span className=\"font-medium text-sm\">\n {getFormatDisplayName(format)}\n </span>\n </button>\n );\n })}\n </div>\n </div>\n\n {/* Archive format — only ZIP is supported */}\n <div>\n <label className=\"block text-sm font-semibold mb-2\">\n {tr(\"export.create.options.archiveFormat\", \"Archive Format\")}\n </label>\n <div className=\"flex gap-3\">\n <div\n className={cn(\n \"flex items-center gap-2 border-2 rounded-lg px-4 py-2 border-primary bg-action-primary-bg/5\",\n )}\n >\n <Package size={16} className=\"text-primary\" />\n <span className=\"font-medium text-sm\">\n {tr(\"export.archive.zip\", \"ZIP\")}\n </span>\n </div>\n </div>\n <p className=\"text-xs text-text-secondary mt-1.5\">\n {tr(\n \"export.create.options.archiveDescription\",\n \"Exports are packaged as ZIP archives.\",\n )}\n </p>\n </div>\n\n {/* Advanced / optional refinements — progressively disclosed */}\n <ShowMore\n collapsedHeight={0}\n label={tr(\n \"export.create.options.advancedToggle\",\n \"Date range & template\",\n )}\n >\n <div className=\"space-y-5 pt-1\">\n {/* Date range filter */}\n <div>\n <label className=\"block text-sm font-semibold mb-2\">\n <span className=\"inline-flex items-center gap-1.5\">\n <CalendarRange size={15} />\n {tr(\"export.create.options.dateRange\", \"Date Range\")}{\" \"}\n <span className=\"font-normal text-text-secondary\">\n {tr(\"export.create.shared.optional\", \"(optional)\")}\n </span>\n </span>\n </label>\n <div className=\"grid grid-cols-1 sm:grid-cols-2 gap-3\">\n <div>\n <label\n htmlFor=\"dateFrom\"\n className=\"block text-xs text-text-secondary mb-1\"\n >\n {tr(\"export.create.options.dateFrom\", \"From\")}\n </label>\n <input\n type=\"date\"\n id=\"dateFrom\"\n aria-label={tr(\n \"export.create.options.dateFrom\",\n \"From\",\n )}\n value={dateRangeStart}\n onChange={(e) => setDateRangeStart(e.target.value)}\n className=\"w-full px-3 py-2 border-2 border-border-subtle rounded-lg bg-bg-surface focus:outline-none focus:ring-2 focus:ring-primary/20 focus:border-primary text-sm\"\n />\n </div>\n <div>\n <label\n htmlFor=\"dateTo\"\n className=\"block text-xs text-text-secondary mb-1\"\n >\n {tr(\"export.create.options.dateTo\", \"To\")}\n </label>\n <input\n type=\"date\"\n id=\"dateTo\"\n aria-label={tr(\"export.create.options.dateTo\", \"To\")}\n value={dateRangeEnd}\n min={dateRangeStart}\n onChange={(e) => setDateRangeEnd(e.target.value)}\n className=\"w-full px-3 py-2 border-2 border-border-subtle rounded-lg bg-bg-surface focus:outline-none focus:ring-2 focus:ring-primary/20 focus:border-primary text-sm\"\n />\n </div>\n </div>\n <p className=\"text-xs text-text-secondary mt-1.5\">\n {tr(\n \"export.create.options.dateRangeDescription\",\n \"Leave blank to export all data regardless of date.\",\n )}\n </p>\n </div>\n\n {/* Optional: Use a template */}\n <div>\n <label className=\"block text-sm font-semibold mb-2\">\n {tr(\n \"export.create.options.basedOnTemplate\",\n \"Based on Template\",\n )}{\" \"}\n <span className=\"font-normal text-text-secondary\">\n {tr(\"export.create.shared.optional\", \"(optional)\")}\n </span>\n </label>\n {templates.length > 0 ? (\n <select\n value={selectedTemplateId ?? \"\"}\n onChange={(e) =>\n setSelectedTemplateId(e.target.value || undefined)\n }\n className=\"w-full px-3 py-2 border-2 border-border-subtle rounded-lg bg-bg-surface focus:outline-none focus:ring-2 focus:ring-primary/20 focus:border-primary\"\n >\n <option value=\"\">\n {tr(\"export.create.options.noTemplate\", \"No template\")}\n </option>\n {templates.map((t) => (\n <option key={t.id} value={t.id}>\n {t.name}\n </option>\n ))}\n </select>\n ) : (\n <p className=\"text-sm text-text-secondary\">\n {tr(\n \"export.create.options.noTemplates\",\n \"No templates yet.\",\n )}{\" \"}\n <button\n type=\"button\"\n onClick={() => navigate?.(basePath + \"/templates\")}\n className=\"text-primary underline-offset-2 hover:underline\"\n >\n {tr(\n \"export.create.options.createTemplateLink\",\n \"Create one\",\n )}\n </button>{\" \"}\n {tr(\n \"export.create.options.reuseTemplateHint\",\n \"to reuse export configurations.\",\n )}\n </p>\n )}\n </div>\n </div>\n </ShowMore>\n </div>\n </div>\n )}\n\n {/* Step 3: Review */}\n {step === \"review\" && (\n <div className=\"space-y-6\">\n <EmphasisPanel className=\"p-6 space-y-4\">\n <h2 className=\"text-lg font-bold flex items-center gap-2\">\n <CheckCircle2 size={20} className=\"text-primary\" />\n {tr(\"export.create.review.title\", \"Review Export\")}\n </h2>\n <dl className=\"space-y-3 text-sm\">\n {[\n {\n label: tr(\"export.create.review.fields.name\", \"Name\"),\n value: exportName,\n },\n {\n label: tr(\"export.create.review.fields.scope\", \"Scope\"),\n value: getScopeLabel(selectedScope),\n },\n {\n label: tr(\n \"export.create.review.fields.entityTypes\",\n \"Entity Types\",\n ),\n value: selectedEntities.join(\", \"),\n },\n {\n label: tr(\"export.create.review.fields.format\", \"Format\"),\n value: getFormatDisplayName(selectedFormat),\n },\n {\n label: tr(\"export.create.review.fields.archive\", \"Archive\"),\n value: tr(\"export.archive.zip\", \"ZIP\"),\n },\n ...(dateRangeStart || dateRangeEnd\n ? [\n {\n label: tr(\n \"export.create.review.fields.dateRange\",\n \"Date Range\",\n ),\n value: [\n dateRangeStart || \"—\",\n dateRangeEnd || \"—\",\n ].join(\" → \"),\n },\n ]\n : []),\n ...(selectedTemplateId\n ? [\n {\n label: tr(\n \"export.create.review.fields.template\",\n \"Template\",\n ),\n value:\n templates.find((t) => t.id === selectedTemplateId)\n ?.name ?? selectedTemplateId,\n },\n ]\n : []),\n ].map(({ label, value }) => (\n <div\n key={label}\n className=\"flex flex-col sm:flex-row sm:justify-between gap-1\"\n >\n <dt className=\"text-text-secondary\">{label}</dt>\n <dd className=\"font-semibold sm:text-right break-words\">\n {value}\n </dd>\n </div>\n ))}\n </dl>\n </EmphasisPanel>\n </div>\n )}\n\n {/* Actions */}\n <div className=\"flex flex-col sm:flex-row gap-3 pt-2\">\n <button\n type=\"button\"\n onClick={handleBack}\n className=\"px-6 py-3 border-2 border-border-subtle/50 rounded-lg hover:bg-accent hover:border-primary/30 transition-all duration-200 font-medium\"\n >\n {step === \"entities\"\n ? tr(\"export.create.actions.cancel\", \"Cancel\")\n : tr(\"export.create.actions.back\", \"Back\")}\n </button>\n {step === \"review\" ? (\n <button\n type=\"button\"\n onClick={handleSubmit}\n disabled={isPending}\n className=\"flex-1 inline-flex items-center justify-center gap-2 px-6 py-3 bg-action-primary-bg text-action-primary-text rounded-lg hover:bg-action-primary-bg/90 transition-all duration-200 font-bold shadow-lg disabled:opacity-50 disabled:cursor-not-allowed\"\n >\n {isPending ? (\n <Loader2 size={18} className=\"animate-spin\" />\n ) : (\n <Download size={18} />\n )}\n {tr(\"export.create.actions.startExport\", \"Start Export\")}\n </button>\n ) : (\n <button\n type=\"button\"\n onClick={handleNext}\n className=\"flex-1 inline-flex items-center justify-center gap-2 px-6 py-3 bg-action-primary-bg text-action-primary-text rounded-lg hover:bg-action-primary-bg/90 transition-all duration-200 font-bold shadow-lg\"\n >\n {tr(\"export.create.actions.next\", \"Next\")}\n <ArrowRight size={18} />\n </button>\n )}\n </div>\n\n <AlertDialog open={showQuotaDialog} onOpenChange={setShowQuotaDialog}>\n <AlertDialogContent>\n <AlertDialogHeader>\n <AlertDialogTitle>\n {tr(\"export.create.quotaDialog.title\", \"Export Limit Reached\")}\n </AlertDialogTitle>\n <AlertDialogDescription>\n {tr(\n \"export.create.quotaDialog.description\",\n \"You have reached your export limit for this month.\",\n )}\n </AlertDialogDescription>\n </AlertDialogHeader>\n <AlertDialogFooter>\n <AlertDialogCancel>\n {tr(\"export.create.quotaDialog.dismiss\", \"Dismiss\")}\n </AlertDialogCancel>\n <AlertDialogAction\n onClick={() => window.location.assign(\"/billing\")}\n >\n {tr(\"export.create.quotaDialog.viewBilling\", \"View Billing\")}\n </AlertDialogAction>\n </AlertDialogFooter>\n </AlertDialogContent>\n </AlertDialog>\n </PageLayout>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"CreateExportPage.js","names":[],"sources":["../../../src/export/pages/CreateExportPage.tsx"],"sourcesContent":["import { useState, useEffect, useRef } from \"react\";\nimport { useSearchParams } from \"react-router\";\nimport { isQuotaExhaustedError } from \"@burdenoff/fe-libs/shared/utils\";\nimport {\n ArrowLeft,\n ArrowRight,\n Download,\n CheckCircle2,\n Loader2,\n FileSpreadsheet,\n FileJson,\n Package,\n CalendarRange,\n User,\n Users,\n Building2,\n Lock,\n} from \"lucide-react\";\nimport { usePermission } from \"@burdenoff/fe-libs/shared/providers/shell\";\nimport {\n AlertDialog,\n AlertDialogAction,\n AlertDialogCancel,\n AlertDialogContent,\n AlertDialogDescription,\n AlertDialogFooter,\n AlertDialogHeader,\n AlertDialogTitle,\n EmphasisPanel,\n ShowMore,\n PagePurpose,\n IllustratedEmptyState,\n} from \"@burdenoff/fe-libs/ui\";\nimport { useExport } from \"../providers/ExportProvider\";\nimport {\n useCreateExportJob,\n useAvailableEntities,\n useExportTemplates,\n} from \"../hooks/useExportQueries\";\nimport { getFormatDisplayName } from \"../constants/enums\";\nimport { PageLayout } from \"../components/PageLayout\";\nimport { useTr } from \"../hooks/useTr\";\nimport { cn } from \"../utils/cn\";\nimport { isExportEntityDisabled } from \"../utils/disabledModules\";\nimport type {\n ExportAvailableEntity,\n ExportFormat,\n ExportScope,\n} from \"../types\";\n\ntype WizardStep = \"entities\" | \"options\" | \"review\";\n\n// Backend only supports CSV and JSON\nconst SUPPORTED_FORMATS: ExportFormat[] = [\"CSV\", \"JSON\"];\n\nconst FORMAT_ICONS: Record<ExportFormat, typeof FileJson> = {\n CSV: FileSpreadsheet,\n JSON: FileJson,\n};\n\nconst SCOPE_ICONS: Record<ExportScope, typeof User> = {\n INDIVIDUAL: User,\n WORKSPACE_ADMIN: Users,\n ORG_ADMIN: Building2,\n};\n\n/**\n * Create Export Page\n * Multi-step form: select entities, configure options, review & start\n */\n\nexport function CreateExportPage() {\n const { basePath = \"/\", navigate, organizationId } = useExport();\n const tr = useTr();\n const { createJob, isPending } = useCreateExportJob();\n const { data: availableEntities } = useAvailableEntities();\n const { data: templates } = useExportTemplates();\n const [searchParams] = useSearchParams();\n\n // RBAC: Check permissions for admin scopes\n const canExportWorkspaceAdmin = usePermission(\"export:workspace:admin\");\n const canExportOrgAdmin = usePermission(\"export:org:admin\");\n\n const [step, setStep] = useState<WizardStep>(\"entities\");\n const [selectedEntities, setSelectedEntities] = useState<string[]>([]);\n const [exportName, setExportName] = useState(\"\");\n const [selectedFormat, setSelectedFormat] = useState<ExportFormat>(\"CSV\");\n const [selectedScope, setSelectedScope] = useState<ExportScope>(\"INDIVIDUAL\");\n const [selectedTemplateId, setSelectedTemplateId] = useState<\n string | undefined\n >(searchParams.get(\"templateId\") ?? undefined);\n const [dateRangeStart, setDateRangeStart] = useState(\"\");\n const [dateRangeEnd, setDateRangeEnd] = useState(\"\");\n const [error, setError] = useState<string | null>(null);\n const [showQuotaDialog, setShowQuotaDialog] = useState(false);\n\n // Pre-populate form when arriving from \"Use Template\" — apply only once per templateId\n const appliedTemplateRef = useRef<string | null>(null);\n useEffect(() => {\n const paramTemplateId = searchParams.get(\"templateId\");\n if (!paramTemplateId || !templates?.length) return;\n if (appliedTemplateRef.current === paramTemplateId) return;\n const tpl = templates.find((t) => t.id === paramTemplateId);\n if (!tpl) return;\n appliedTemplateRef.current = paramTemplateId;\n setSelectedTemplateId(tpl.id);\n // Skip entity types whose product export is currently disabled.\n setSelectedEntities(\n (tpl.entityTypes ?? []).filter((et) => !isExportEntityDisabled(et)),\n );\n const normalizedFormat = tpl.format?.toUpperCase() as\n | ExportFormat\n | undefined;\n if (normalizedFormat && SUPPORTED_FORMATS.includes(normalizedFormat)) {\n setSelectedFormat(normalizedFormat);\n }\n if (tpl.name) setExportName(tpl.name);\n }, [searchParams, templates]);\n\n const handleBack = () => {\n if (step === \"options\") setStep(\"entities\");\n else if (step === \"review\") setStep(\"options\");\n else navigate?.(basePath);\n };\n\n const handleNext = () => {\n if (step === \"entities\") {\n if (selectedEntities.length === 0) {\n setError(\n tr(\n \"export.create.errors.selectEntity\",\n \"Please select at least one entity type to export\",\n ),\n );\n return;\n }\n setError(null);\n setStep(\"options\");\n } else if (step === \"options\") {\n if (!exportName.trim()) {\n setError(\n tr(\n \"export.create.errors.exportNameRequired\",\n \"Please provide a name for this export\",\n ),\n );\n return;\n }\n setError(null);\n setStep(\"review\");\n }\n };\n\n const handleSubmit = async () => {\n try {\n setError(null);\n await createJob({\n name: exportName,\n entityTypes: selectedEntities,\n format: selectedFormat,\n exportScope: selectedScope,\n organizationId:\n selectedScope === \"ORG_ADMIN\" ? organizationId : undefined,\n templateId: selectedTemplateId,\n dateRangeStart: dateRangeStart\n ? new Date(dateRangeStart).toISOString()\n : null,\n dateRangeEnd: dateRangeEnd\n ? new Date(dateRangeEnd).toISOString()\n : null,\n });\n navigate?.(basePath);\n } catch (err) {\n if (isQuotaExhaustedError(err)) {\n setShowQuotaDialog(true);\n setError(\n tr(\n \"export.create.errors.quotaReached\",\n \"You have reached your export limit for this month.\",\n ),\n );\n return;\n }\n setError(\n err instanceof Error\n ? err.message\n : tr(\"export.create.errors.createFailed\", \"Failed to create export\"),\n );\n }\n };\n\n const toggleEntity = (entityType: string) => {\n // Disabled-product entities can't be selected.\n if (isExportEntityDisabled(entityType)) return;\n setSelectedEntities((prev) =>\n prev.includes(entityType)\n ? prev.filter((e) => e !== entityType)\n : [...prev, entityType],\n );\n };\n\n const getScopeLabel = (scope: ExportScope) => {\n switch (scope) {\n case \"INDIVIDUAL\":\n return tr(\"export.create.scope.individual.label\", \"My Data Only\");\n case \"WORKSPACE_ADMIN\":\n return tr(\n \"export.create.scope.workspaceAdmin.label\",\n \"All Workspace Data\",\n );\n case \"ORG_ADMIN\":\n return tr(\n \"export.create.scope.orgAdmin.label\",\n \"All Organization Data\",\n );\n default:\n return scope;\n }\n };\n\n const getScopeDescription = (scope: ExportScope) => {\n switch (scope) {\n case \"INDIVIDUAL\":\n return tr(\n \"export.create.scope.individual.description\",\n \"Export data you personally created across selected modules.\",\n );\n case \"WORKSPACE_ADMIN\":\n return tr(\n \"export.create.scope.workspaceAdmin.description\",\n \"Export all data in this workspace regardless of who created it. Requires workspace admin access.\",\n );\n case \"ORG_ADMIN\":\n return tr(\n \"export.create.scope.orgAdmin.description\",\n \"Export data from every workspace in your organization. Requires org-admin access.\",\n );\n default:\n return \"\";\n }\n };\n\n const fallbackEntityCopy: Record<\n string,\n { label: string; description: string }\n > = {\n users: {\n label: tr(\"export.create.entities.users.label\", \"Users\"),\n description: tr(\n \"export.create.entities.users.description\",\n \"Workspace members and profiles\",\n ),\n },\n projects: {\n label: tr(\"export.create.entities.projects.label\", \"Projects\"),\n description: tr(\n \"export.create.entities.projects.description\",\n \"Projects and their metadata\",\n ),\n },\n tasks: {\n label: tr(\"export.create.entities.tasks.label\", \"Tasks\"),\n description: tr(\n \"export.create.entities.tasks.description\",\n \"Task items and assignments\",\n ),\n },\n files: {\n label: tr(\"export.create.entities.files.label\", \"Files\"),\n description: tr(\n \"export.create.entities.files.description\",\n \"File metadata and references\",\n ),\n },\n conversations: {\n label: tr(\"export.create.entities.conversations.label\", \"Conversations\"),\n description: tr(\n \"export.create.entities.conversations.description\",\n \"Chat messages and threads\",\n ),\n },\n tags: {\n label: tr(\"export.create.entities.tags.label\", \"Tags\"),\n description: tr(\n \"export.create.entities.tags.description\",\n \"Tags and vocabularies\",\n ),\n },\n activity: {\n label: tr(\"export.create.entities.activity.label\", \"Activity Logs\"),\n description: tr(\n \"export.create.entities.activity.description\",\n \"Audit trail and activity history\",\n ),\n },\n calendar: {\n label: tr(\"export.create.entities.calendar.label\", \"Calendar Events\"),\n description: tr(\n \"export.create.entities.calendar.description\",\n \"Events and bookings\",\n ),\n },\n };\n\n // Entities the backend marks available, split so the ones a user can export\n // show first and disabled-product entities are listed (still visible) below.\n const visibleEntities = availableEntities.filter((e) => e.enabled !== false);\n const availableForExport = visibleEntities.filter(\n (e) => !isExportEntityDisabled(e.entityType),\n );\n const unavailableForExport = visibleEntities.filter((e) =>\n isExportEntityDisabled(e.entityType),\n );\n\n const renderEntityCard = (\n entity: ExportAvailableEntity,\n isDisabled: boolean,\n ) => {\n const isSelected =\n !isDisabled && selectedEntities.includes(entity.entityType);\n return (\n <button\n type=\"button\"\n key={entity.entityType}\n onClick={() => toggleEntity(entity.entityType)}\n disabled={isDisabled}\n aria-disabled={isDisabled}\n title={\n isDisabled\n ? tr(\n \"export.create.select.unavailableHint\",\n \"Export for this product isn't available yet\",\n )\n : undefined\n }\n className={cn(\n \"text-left border-2 rounded-xl p-4 transition-all duration-200\",\n isDisabled\n ? \"border-border-subtle/30 opacity-50 cursor-not-allowed\"\n : isSelected\n ? \"border-primary bg-action-primary-bg/5 shadow-md\"\n : \"border-border-subtle/50 hover:border-primary/30 hover:shadow-sm\",\n )}\n >\n <div className=\"flex items-center justify-between mb-2 gap-2\">\n <h3 className=\"font-semibold\">\n {fallbackEntityCopy[entity.entityType]?.label ?? entity.displayName}\n </h3>\n {isDisabled ? (\n <span className=\"shrink-0 text-xs font-medium px-2 py-0.5 rounded-full bg-bg-sunken text-text-secondary\">\n {tr(\"export.create.select.unavailable\", \"Unavailable\")}\n </span>\n ) : (\n isSelected && <CheckCircle2 size={18} className=\"text-primary\" />\n )}\n </div>\n <p className=\"text-sm text-text-secondary\">\n {fallbackEntityCopy[entity.entityType]?.description ??\n entity.description}\n </p>\n {entity.estimatedSize && (\n <p className=\"text-xs text-text-secondary mt-2\">\n ~{entity.estimatedSize}\n </p>\n )}\n </button>\n );\n };\n\n return (\n <PageLayout showHeader={false}>\n {/* Header */}\n <div className=\"relative overflow-hidden rounded-xl bg-accent-wash border border-border-seam p-6 shadow-[var(--shadow-pop)]\">\n <div className=\"relative z-10 flex items-center gap-4\">\n <button\n type=\"button\"\n onClick={() => navigate?.(basePath)}\n className=\"p-2.5 hover:bg-accent rounded-lg transition-colors duration-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/40\"\n aria-label={tr(\n \"export.create.actions.backToDashboard\",\n \"Back to export dashboard\",\n )}\n >\n <ArrowLeft size={20} />\n </button>\n <div className=\"flex items-center gap-3\">\n <div className=\"p-2.5 rounded-lg bg-action-primary-bg/10 backdrop-blur-sm\">\n <Download size={24} className=\"text-primary\" />\n </div>\n <h1 className=\"text-2xl sm:text-3xl font-bold\">\n {tr(\"export.create.title\", \"New Export\")}\n </h1>\n </div>\n </div>\n </div>\n\n {/* Page purpose — explains the wizard's job in plain language */}\n <PagePurpose icon={<Download size={16} />}>\n {tr(\n \"export.create.purpose\",\n \"Build a one-off export in three steps: pick which data to include, choose a format and scope, then review and start it. The export runs in the background and produces a downloadable ZIP you can grab from History when it finishes.\",\n )}\n </PagePurpose>\n\n {/* Step Indicator */}\n <div className=\"flex items-center gap-2 justify-center flex-wrap\">\n {([\"entities\", \"options\", \"review\"] as WizardStep[]).map((s, i) => (\n <div key={s} className=\"flex items-center gap-2\">\n {i > 0 && <ArrowRight size={18} className=\"text-text-secondary\" />}\n <div\n className={cn(\n \"flex items-center gap-2 px-4 py-2.5 rounded-lg text-sm font-medium border-2 transition-all duration-200\",\n step === s\n ? \"bg-action-primary-bg text-action-primary-text border-primary shadow-[var(--shadow-pop)]\"\n : \"bg-bg-sunken/50 text-text-secondary border-border-seam\",\n )}\n >\n <div className=\"flex items-center justify-center size-6 rounded-full bg-bg-surface/20 font-bold\">\n {i + 1}\n </div>\n <span className=\"hidden sm:inline capitalize\">\n {s === \"entities\"\n ? tr(\"export.create.steps.entities\", \"Select Data\")\n : s === \"options\"\n ? tr(\"export.create.steps.options\", \"Options\")\n : tr(\"export.create.steps.review\", \"Review\")}\n </span>\n </div>\n </div>\n ))}\n </div>\n\n {/* Error */}\n {error && (\n <div className=\"border-2 border-border-default rounded-xl bg-status-error-bg-subtle p-4 text-status-error-text text-sm\">\n {error}\n </div>\n )}\n\n {/* Step 1: Select Entities */}\n {step === \"entities\" && (\n <div className=\"space-y-4\">\n <h2 className=\"text-lg font-bold\">\n {tr(\"export.create.select.title\", \"Select data to export\")}\n </h2>\n <p className=\"text-sm text-text-secondary\">\n {tr(\n \"export.create.select.description\",\n \"Choose which entity types to include in your export.\",\n )}\n </p>\n {visibleEntities.length === 0 ? (\n <IllustratedEmptyState\n illustration=\"empty-data\"\n title={tr(\n \"export.create.select.emptyTitle\",\n \"Nothing to export yet\",\n )}\n description={tr(\n \"export.create.select.empty\",\n \"No exportable entity types are configured for this workspace yet. Once your workspace has data in supported modules, they'll show up here to select.\",\n )}\n />\n ) : (\n <div className=\"space-y-6\">\n {availableForExport.length > 0 && (\n <div className=\"grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3\">\n {availableForExport.map((entity) =>\n renderEntityCard(entity, false),\n )}\n </div>\n )}\n {unavailableForExport.length > 0 && (\n <div className=\"space-y-3\">\n <div className=\"flex items-center gap-3\">\n <div className=\"h-px flex-1 bg-border/60\" />\n <span className=\"text-xs font-medium uppercase tracking-wide text-text-secondary\">\n {tr(\n \"export.create.select.unavailableSection\",\n \"Not available yet\",\n )}\n </span>\n <div className=\"h-px flex-1 bg-border/60\" />\n </div>\n <div className=\"grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3\">\n {unavailableForExport.map((entity) =>\n renderEntityCard(entity, true),\n )}\n </div>\n </div>\n )}\n </div>\n )}\n </div>\n )}\n\n {/* Step 2: Configure Options */}\n {step === \"options\" && (\n <div className=\"space-y-6\">\n <div className=\"border border-border-seam rounded-xl p-6 space-y-5 bg-bg-surface\">\n <h2 className=\"text-lg font-bold\">\n {tr(\"export.create.options.title\", \"Export Options\")}\n </h2>\n\n {/* Scope selection */}\n <div>\n <label className=\"block text-sm font-semibold mb-2\">\n {tr(\"export.create.options.scope\", \"Export Scope\")}\n </label>\n <div className=\"grid grid-cols-1 sm:grid-cols-3 gap-3\">\n {(\n [\n \"INDIVIDUAL\",\n \"WORKSPACE_ADMIN\",\n \"ORG_ADMIN\",\n ] as ExportScope[]\n ).map((value) => {\n // RBAC: Disable admin scopes if user lacks permission\n const isDisabled =\n (value === \"WORKSPACE_ADMIN\" && !canExportWorkspaceAdmin) ||\n (value === \"ORG_ADMIN\" && !canExportOrgAdmin);\n const Icon = SCOPE_ICONS[value];\n const disabledTitle =\n value === \"WORKSPACE_ADMIN\"\n ? tr(\n \"export.create.scope.workspaceAdmin.required\",\n \"Workspace admin access required\",\n )\n : tr(\n \"export.create.scope.orgAdmin.required\",\n \"Organization admin access required\",\n );\n\n return (\n <button\n key={value}\n type=\"button\"\n onClick={() => !isDisabled && setSelectedScope(value)}\n disabled={isDisabled}\n className={cn(\n \"text-left border-2 rounded-xl p-4 transition-all duration-200\",\n selectedScope === value\n ? \"border-primary bg-action-primary-bg/5 shadow-md\"\n : isDisabled\n ? \"border-border-subtle/30 opacity-50 cursor-not-allowed\"\n : \"border-border-subtle/50 hover:border-primary/30 hover:shadow-sm\",\n )}\n title={isDisabled ? disabledTitle : undefined}\n >\n <div className=\"flex items-center justify-between mb-2\">\n <Icon\n size={18}\n className={\n selectedScope === value\n ? \"text-primary\"\n : isDisabled\n ? \"text-text-secondary/50\"\n : \"text-text-secondary\"\n }\n />\n {selectedScope === value && (\n <CheckCircle2 size={16} className=\"text-primary\" />\n )}\n {isDisabled && (\n <Lock size={14} className=\"text-text-secondary/50\" />\n )}\n </div>\n <p className=\"font-semibold text-sm\">\n {getScopeLabel(value)}\n </p>\n <p className=\"text-xs text-text-secondary mt-1\">\n {getScopeDescription(value)}\n </p>\n </button>\n );\n })}\n </div>\n </div>\n\n {/* Name */}\n <div>\n <label\n htmlFor=\"exportName\"\n className=\"block text-sm font-semibold mb-2\"\n >\n {tr(\"export.create.options.name\", \"Export Name\")}{\" \"}\n <span className=\"text-status-error-text\">*</span>\n </label>\n <input\n type=\"text\"\n id=\"exportName\"\n aria-label={tr(\"export.create.options.name\", \"Export Name\")}\n value={exportName}\n onChange={(e) => setExportName(e.target.value)}\n className=\"w-full px-4 py-2.5 border-2 border-border-subtle rounded-lg bg-bg-surface focus:outline-none focus:ring-2 focus:ring-primary/20 focus:border-primary transition-all duration-200\"\n placeholder={tr(\n \"export.create.options.namePlaceholder\",\n \"e.g., Monthly User Export\",\n )}\n />\n </div>\n\n {/* Format — only CSV and JSON are supported */}\n <div>\n <label className=\"block text-sm font-semibold mb-2\">\n {tr(\"export.create.options.outputFormat\", \"Output Format\")}\n </label>\n <div className=\"grid grid-cols-2 gap-3\">\n {SUPPORTED_FORMATS.map((format) => {\n const Icon = FORMAT_ICONS[format];\n return (\n <button\n type=\"button\"\n key={format}\n onClick={() => setSelectedFormat(format)}\n className={cn(\n \"flex items-center gap-3 border-2 rounded-lg p-3 transition-all duration-200\",\n selectedFormat === format\n ? \"border-primary bg-action-primary-bg/5\"\n : \"border-border-subtle/50 hover:border-primary/30\",\n )}\n >\n <Icon\n size={18}\n className={\n selectedFormat === format\n ? \"text-primary\"\n : \"text-text-secondary\"\n }\n />\n <span className=\"font-medium text-sm\">\n {getFormatDisplayName(format)}\n </span>\n </button>\n );\n })}\n </div>\n </div>\n\n {/* Archive format — only ZIP is supported */}\n <div>\n <label className=\"block text-sm font-semibold mb-2\">\n {tr(\"export.create.options.archiveFormat\", \"Archive Format\")}\n </label>\n <div className=\"flex gap-3\">\n <div\n className={cn(\n \"flex items-center gap-2 border-2 rounded-lg px-4 py-2 border-primary bg-action-primary-bg/5\",\n )}\n >\n <Package size={16} className=\"text-primary\" />\n <span className=\"font-medium text-sm\">\n {tr(\"export.archive.zip\", \"ZIP\")}\n </span>\n </div>\n </div>\n <p className=\"text-xs text-text-secondary mt-1.5\">\n {tr(\n \"export.create.options.archiveDescription\",\n \"Exports are packaged as ZIP archives.\",\n )}\n </p>\n </div>\n\n {/* Advanced / optional refinements — progressively disclosed */}\n <ShowMore\n collapsedHeight={0}\n label={tr(\n \"export.create.options.advancedToggle\",\n \"Date range & template\",\n )}\n >\n <div className=\"space-y-5 pt-1\">\n {/* Date range filter */}\n <div>\n <label className=\"block text-sm font-semibold mb-2\">\n <span className=\"inline-flex items-center gap-1.5\">\n <CalendarRange size={15} />\n {tr(\"export.create.options.dateRange\", \"Date Range\")}{\" \"}\n <span className=\"font-normal text-text-secondary\">\n {tr(\"export.create.shared.optional\", \"(optional)\")}\n </span>\n </span>\n </label>\n <div className=\"grid grid-cols-1 sm:grid-cols-2 gap-3\">\n <div>\n <label\n htmlFor=\"dateFrom\"\n className=\"block text-xs text-text-secondary mb-1\"\n >\n {tr(\"export.create.options.dateFrom\", \"From\")}\n </label>\n <input\n type=\"date\"\n id=\"dateFrom\"\n aria-label={tr(\n \"export.create.options.dateFrom\",\n \"From\",\n )}\n value={dateRangeStart}\n onChange={(e) => setDateRangeStart(e.target.value)}\n className=\"w-full px-3 py-2 border-2 border-border-subtle rounded-lg bg-bg-surface focus:outline-none focus:ring-2 focus:ring-primary/20 focus:border-primary text-sm\"\n />\n </div>\n <div>\n <label\n htmlFor=\"dateTo\"\n className=\"block text-xs text-text-secondary mb-1\"\n >\n {tr(\"export.create.options.dateTo\", \"To\")}\n </label>\n <input\n type=\"date\"\n id=\"dateTo\"\n aria-label={tr(\"export.create.options.dateTo\", \"To\")}\n value={dateRangeEnd}\n min={dateRangeStart}\n onChange={(e) => setDateRangeEnd(e.target.value)}\n className=\"w-full px-3 py-2 border-2 border-border-subtle rounded-lg bg-bg-surface focus:outline-none focus:ring-2 focus:ring-primary/20 focus:border-primary text-sm\"\n />\n </div>\n </div>\n <p className=\"text-xs text-text-secondary mt-1.5\">\n {tr(\n \"export.create.options.dateRangeDescription\",\n \"Leave blank to export all data regardless of date.\",\n )}\n </p>\n </div>\n\n {/* Optional: Use a template */}\n <div>\n <label className=\"block text-sm font-semibold mb-2\">\n {tr(\n \"export.create.options.basedOnTemplate\",\n \"Based on Template\",\n )}{\" \"}\n <span className=\"font-normal text-text-secondary\">\n {tr(\"export.create.shared.optional\", \"(optional)\")}\n </span>\n </label>\n {templates.length > 0 ? (\n <select\n value={selectedTemplateId ?? \"\"}\n onChange={(e) =>\n setSelectedTemplateId(e.target.value || undefined)\n }\n className=\"w-full px-3 py-2 border-2 border-border-subtle rounded-lg bg-bg-surface focus:outline-none focus:ring-2 focus:ring-primary/20 focus:border-primary scheme-normal dark:scheme-dark [&_option]:bg-bg-surface [&_option]:text-text-primary\"\n >\n <option value=\"\">\n {tr(\"export.create.options.noTemplate\", \"No template\")}\n </option>\n {templates.map((t) => (\n <option key={t.id} value={t.id}>\n {t.name}\n </option>\n ))}\n </select>\n ) : (\n <p className=\"text-sm text-text-secondary\">\n {tr(\n \"export.create.options.noTemplates\",\n \"No templates yet.\",\n )}{\" \"}\n <button\n type=\"button\"\n onClick={() => navigate?.(basePath + \"/templates\")}\n className=\"text-primary underline-offset-2 hover:underline\"\n >\n {tr(\n \"export.create.options.createTemplateLink\",\n \"Create one\",\n )}\n </button>{\" \"}\n {tr(\n \"export.create.options.reuseTemplateHint\",\n \"to reuse export configurations.\",\n )}\n </p>\n )}\n </div>\n </div>\n </ShowMore>\n </div>\n </div>\n )}\n\n {/* Step 3: Review */}\n {step === \"review\" && (\n <div className=\"space-y-6\">\n <EmphasisPanel className=\"p-6 space-y-4\">\n <h2 className=\"text-lg font-bold flex items-center gap-2\">\n <CheckCircle2 size={20} className=\"text-primary\" />\n {tr(\"export.create.review.title\", \"Review Export\")}\n </h2>\n <dl className=\"space-y-3 text-sm\">\n {[\n {\n label: tr(\"export.create.review.fields.name\", \"Name\"),\n value: exportName,\n },\n {\n label: tr(\"export.create.review.fields.scope\", \"Scope\"),\n value: getScopeLabel(selectedScope),\n },\n {\n label: tr(\n \"export.create.review.fields.entityTypes\",\n \"Entity Types\",\n ),\n value: selectedEntities.join(\", \"),\n },\n {\n label: tr(\"export.create.review.fields.format\", \"Format\"),\n value: getFormatDisplayName(selectedFormat),\n },\n {\n label: tr(\"export.create.review.fields.archive\", \"Archive\"),\n value: tr(\"export.archive.zip\", \"ZIP\"),\n },\n ...(dateRangeStart || dateRangeEnd\n ? [\n {\n label: tr(\n \"export.create.review.fields.dateRange\",\n \"Date Range\",\n ),\n value: [\n dateRangeStart || \"—\",\n dateRangeEnd || \"—\",\n ].join(\" → \"),\n },\n ]\n : []),\n ...(selectedTemplateId\n ? [\n {\n label: tr(\n \"export.create.review.fields.template\",\n \"Template\",\n ),\n value:\n templates.find((t) => t.id === selectedTemplateId)\n ?.name ?? selectedTemplateId,\n },\n ]\n : []),\n ].map(({ label, value }) => (\n <div\n key={label}\n className=\"flex flex-col sm:flex-row sm:justify-between gap-1\"\n >\n <dt className=\"text-text-secondary\">{label}</dt>\n <dd className=\"font-semibold sm:text-right break-words\">\n {value}\n </dd>\n </div>\n ))}\n </dl>\n </EmphasisPanel>\n </div>\n )}\n\n {/* Actions */}\n <div className=\"flex flex-col sm:flex-row gap-3 pt-2\">\n <button\n type=\"button\"\n onClick={handleBack}\n className=\"px-6 py-3 border-2 border-border-subtle/50 rounded-lg hover:bg-accent hover:border-primary/30 transition-all duration-200 font-medium\"\n >\n {step === \"entities\"\n ? tr(\"export.create.actions.cancel\", \"Cancel\")\n : tr(\"export.create.actions.back\", \"Back\")}\n </button>\n {step === \"review\" ? (\n <button\n type=\"button\"\n onClick={handleSubmit}\n disabled={isPending}\n className=\"flex-1 inline-flex items-center justify-center gap-2 px-6 py-3 bg-action-primary-bg text-action-primary-text rounded-lg hover:bg-action-primary-bg/90 transition-all duration-200 font-bold shadow-lg disabled:opacity-50 disabled:cursor-not-allowed\"\n >\n {isPending ? (\n <Loader2 size={18} className=\"animate-spin\" />\n ) : (\n <Download size={18} />\n )}\n {tr(\"export.create.actions.startExport\", \"Start Export\")}\n </button>\n ) : (\n <button\n type=\"button\"\n onClick={handleNext}\n className=\"flex-1 inline-flex items-center justify-center gap-2 px-6 py-3 bg-action-primary-bg text-action-primary-text rounded-lg hover:bg-action-primary-bg/90 transition-all duration-200 font-bold shadow-lg\"\n >\n {tr(\"export.create.actions.next\", \"Next\")}\n <ArrowRight size={18} />\n </button>\n )}\n </div>\n\n <AlertDialog open={showQuotaDialog} onOpenChange={setShowQuotaDialog}>\n <AlertDialogContent>\n <AlertDialogHeader>\n <AlertDialogTitle>\n {tr(\"export.create.quotaDialog.title\", \"Export Limit Reached\")}\n </AlertDialogTitle>\n <AlertDialogDescription>\n {tr(\n \"export.create.quotaDialog.description\",\n \"You have reached your export limit for this month.\",\n )}\n </AlertDialogDescription>\n </AlertDialogHeader>\n <AlertDialogFooter>\n <AlertDialogCancel>\n {tr(\"export.create.quotaDialog.dismiss\", \"Dismiss\")}\n </AlertDialogCancel>\n <AlertDialogAction\n onClick={() => window.location.assign(\"/billing\")}\n >\n {tr(\"export.create.quotaDialog.viewBilling\", \"View Billing\")}\n </AlertDialogAction>\n </AlertDialogFooter>\n </AlertDialogContent>\n </AlertDialog>\n </PageLayout>\n );\n}\n"],"mappings":"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"}
@@ -109,7 +109,7 @@ function D() {
109
109
  }), /* @__PURE__ */ C("select", {
110
110
  value: A.status,
111
111
  onChange: (e) => j({ status: e.target.value }),
112
- className: "w-full px-3 py-2 border-2 border-border-subtle rounded-lg bg-bg-surface focus:outline-none focus:ring-2 focus:ring-primary/20 focus:border-primary",
112
+ className: "w-full px-3 py-2 border-2 border-border-subtle rounded-lg bg-bg-surface focus:outline-none focus:ring-2 focus:ring-primary/20 focus:border-primary scheme-normal dark:scheme-dark [&_option]:bg-bg-surface [&_option]:text-text-primary",
113
113
  children: [/* @__PURE__ */ S("option", {
114
114
  value: "all",
115
115
  children: k("export.history.filters.status.all", "All Statuses")
@@ -126,7 +126,7 @@ function D() {
126
126
  }), /* @__PURE__ */ C("select", {
127
127
  value: A.format,
128
128
  onChange: (e) => j({ format: e.target.value }),
129
- className: "w-full px-3 py-2 border-2 border-border-subtle rounded-lg bg-bg-surface focus:outline-none focus:ring-2 focus:ring-primary/20 focus:border-primary",
129
+ className: "w-full px-3 py-2 border-2 border-border-subtle rounded-lg bg-bg-surface focus:outline-none focus:ring-2 focus:ring-primary/20 focus:border-primary scheme-normal dark:scheme-dark [&_option]:bg-bg-surface [&_option]:text-text-primary",
130
130
  children: [
131
131
  /* @__PURE__ */ S("option", {
132
132
  value: "all",
@@ -1 +1 @@
1
- {"version":3,"file":"ExportHistoryPage.js","names":[],"sources":["../../../src/export/pages/ExportHistoryPage.tsx"],"sourcesContent":["import { useState, useMemo, useEffect, useRef } from \"react\";\nimport { useSearchParams } from \"react-router\";\nimport {\n Search,\n X,\n Clock,\n Download,\n ChevronRight,\n ChevronLeft,\n RefreshCw,\n AlertCircle,\n Filter,\n} from \"lucide-react\";\nimport { IllustratedEmptyState, PagePurpose } from \"@burdenoff/fe-libs/ui\";\nimport { useExport } from \"../providers/ExportProvider\";\nimport { useExportJobs } from \"../hooks/useExportQueries\";\nimport { useExportStore } from \"../store/exportStore\";\nimport { ExportStatusBadge } from \"../components/ExportStatusBadge\";\nimport { PageLayout } from \"../components/PageLayout\";\nimport { useTr } from \"../hooks/useTr\";\nimport { cn } from \"../utils/cn\";\nimport {\n getFormatDisplayName,\n getAllStatuses,\n getStatusDisplayName,\n} from \"../constants/enums\";\nimport type { ExportJobStatus, ExportFormat } from \"../types\";\n\n/**\n * Helper to join paths correctly\n */\nconst joinPath = (base: string, path: string): string => {\n const cleanBase = base.endsWith(\"/\") ? base.slice(0, -1) : base;\n const cleanPath = path.startsWith(\"/\") ? path : `/${path}`;\n return cleanBase + cleanPath;\n};\n\n/**\n * Export History Page\n * Table/list of all past exports with filters and cursor-based pagination\n */\nexport function ExportHistoryPage() {\n const { basePath = \"/\", navigate } = useExport();\n const tr = useTr();\n const { jobFilter, updateFilter, resetFilter, pagination, setPagination } =\n useExportStore();\n const [searchParams] = useSearchParams();\n\n // Initialize status filter from URL search params (e.g. ?status=RUNNING) — once\n const didInitFromUrlRef = useRef(false);\n useEffect(() => {\n if (didInitFromUrlRef.current) return;\n didInitFromUrlRef.current = true;\n const statusParam = searchParams.get(\"status\");\n if (statusParam) {\n updateFilter({\n status: statusParam as ExportJobStatus,\n });\n }\n }, [searchParams, updateFilter]);\n\n const [showFilters, setShowFilters] = useState(false);\n // Cursor stack: index 0 = first page (no cursor), subsequent entries are \"after\" cursors\n const [cursorStack, setCursorStack] = useState<(string | null)[]>([null]);\n const [currentPageIndex, setCurrentPageIndex] = useState(0);\n\n const currentCursor = cursorStack[currentPageIndex] ?? null;\n\n const {\n data: jobs,\n totalCount,\n pageInfo,\n isLoading,\n error,\n refetch,\n } = useExportJobs(jobFilter, {\n first: pagination.pageSize,\n after: currentCursor ?? undefined,\n });\n\n // Client-side search filter (server-side filtering handles status/format)\n const filteredJobs = useMemo(() => {\n if (!jobFilter.search) return jobs;\n const q = jobFilter.search.toLowerCase();\n return jobs.filter(\n (job) =>\n job.name.toLowerCase().includes(q) ||\n job.entityTypes.some((e) => e.toLowerCase().includes(q)),\n );\n }, [jobs, jobFilter.search]);\n\n const hasActiveFilters =\n jobFilter.search ||\n jobFilter.status !== \"all\" ||\n jobFilter.format !== \"all\";\n\n const handleJobClick = (jobId: string) => {\n navigate?.(joinPath(basePath, jobId));\n };\n\n const handleNextPage = () => {\n if (!pageInfo?.hasNextPage || !pageInfo.endCursor) return;\n const newStack = [\n ...cursorStack.slice(0, currentPageIndex + 1),\n pageInfo.endCursor,\n ];\n setCursorStack(newStack);\n setCurrentPageIndex((prev) => prev + 1);\n setPagination({ page: pagination.page + 1, cursor: pageInfo.endCursor });\n };\n\n const handlePrevPage = () => {\n if (currentPageIndex === 0) return;\n const prevIndex = currentPageIndex - 1;\n setCurrentPageIndex(prevIndex);\n setPagination({\n page: pagination.page - 1,\n cursor: cursorStack[prevIndex] ?? null,\n });\n };\n\n const handleResetFilters = () => {\n resetFilter();\n setCursorStack([null]);\n setCurrentPageIndex(0);\n };\n\n const hasPrevPage = currentPageIndex > 0;\n const hasNextPage = pageInfo?.hasNextPage ?? false;\n const pageStart = currentPageIndex * pagination.pageSize + 1;\n const pageEnd = Math.min(\n (currentPageIndex + 1) * pagination.pageSize,\n totalCount,\n );\n const getStatusLabel = (status: ExportJobStatus) =>\n tr(\n `export.shared.status.${status.toLowerCase()}`,\n getStatusDisplayName(status),\n );\n\n return (\n <PageLayout\n title={tr(\"export.history.title\", \"Export History\")}\n subtitle={tr(\n \"export.history.subtitle\",\n \"View and manage all your past export jobs\",\n )}\n icon={<Clock size={24} />}\n headerActions={\n <button\n type=\"button\"\n onClick={() => navigate?.(joinPath(basePath, \"new\"))}\n className=\"inline-flex items-center gap-2 px-4 py-2 bg-action-primary-bg text-action-primary-text rounded-lg hover:bg-action-primary-bg/90 transition-colors font-medium\"\n >\n <Download size={16} />\n {tr(\"export.history.actions.newExport\", \"New Export\")}\n </button>\n }\n >\n {/* Page purpose — what History is for */}\n <PagePurpose icon={<Clock size={16} />}>\n {tr(\n \"export.history.purpose\",\n \"Every export you've ever started lives here. Track jobs as they run, see which finished or failed, and grab download links for completed exports before they expire. Search by name or entity type, or filter by status and format to find a specific one.\",\n )}\n </PagePurpose>\n\n {/* Search & Filters */}\n <div className=\"space-y-4\">\n <div className=\"flex flex-col sm:flex-row gap-3\">\n {/* Search */}\n <div className=\"flex-1 relative group\">\n <Search\n size={18}\n className=\"absolute left-3 top-1/2 -translate-y-1/2 text-text-secondary group-focus-within:text-primary transition-colors\"\n />\n <input\n type=\"text\"\n aria-label={tr(\n \"export.history.search.placeholder\",\n \"Search exports by name or entity type\",\n )}\n placeholder={tr(\n \"export.history.search.placeholder\",\n \"Search exports by name or entity type...\",\n )}\n value={jobFilter.search}\n onChange={(e) => updateFilter({ search: e.target.value })}\n className=\"w-full pl-[3.25rem] pr-4 py-2.5 border-2 border-border-subtle rounded-lg bg-bg-surface focus:outline-none focus:ring-2 focus:ring-primary/20 focus:border-primary transition-all duration-200\"\n />\n </div>\n\n {/* Filter Toggle */}\n <button\n type=\"button\"\n onClick={() => setShowFilters(!showFilters)}\n className={cn(\n \"inline-flex items-center gap-2 px-4 py-2.5 border-2 rounded-lg transition-all duration-200 font-medium\",\n showFilters || hasActiveFilters\n ? \"bg-action-primary-bg text-action-primary-text border-primary\"\n : \"border-border-subtle/50 hover:border-primary/30\",\n )}\n >\n <Filter size={16} />\n {showFilters\n ? tr(\"export.history.filters.hide\", \"Hide\")\n : tr(\"export.history.filters.show\", \"Filter\")}\n </button>\n </div>\n\n {/* Filter Panel */}\n {showFilters && (\n <div className=\"border border-border-seam rounded-xl p-4 animate-in fade-in-0 duration-200\">\n <div className=\"flex items-center justify-between mb-4\">\n <h3 className=\"font-semibold flex items-center gap-2\">\n <Filter size={16} className=\"text-primary\" />\n {tr(\"export.history.filters.title\", \"Filters\")}\n </h3>\n {hasActiveFilters && (\n <button\n type=\"button\"\n onClick={handleResetFilters}\n className=\"text-sm text-text-secondary hover:text-status-error-text flex items-center gap-1\"\n >\n <X size={14} />\n {tr(\"export.history.filters.clearAll\", \"Clear All\")}\n </button>\n )}\n </div>\n <div className=\"grid grid-cols-1 sm:grid-cols-2 gap-4\">\n {/* Status Filter */}\n <div className=\"space-y-2\">\n <label className=\"block text-sm font-medium\">\n {tr(\"export.history.filters.status.label\", \"Status\")}\n </label>\n <select\n value={jobFilter.status}\n onChange={(e) =>\n updateFilter({\n status: e.target.value as ExportJobStatus | \"all\",\n })\n }\n className=\"w-full px-3 py-2 border-2 border-border-subtle rounded-lg bg-bg-surface focus:outline-none focus:ring-2 focus:ring-primary/20 focus:border-primary\"\n >\n <option value=\"all\">\n {tr(\"export.history.filters.status.all\", \"All Statuses\")}\n </option>\n {getAllStatuses().map((status) => (\n <option key={status} value={status}>\n {getStatusLabel(status)}\n </option>\n ))}\n </select>\n </div>\n\n {/* Format Filter — only CSV and JSON are supported */}\n <div className=\"space-y-2\">\n <label className=\"block text-sm font-medium\">\n {tr(\"export.history.filters.format.label\", \"Format\")}\n </label>\n <select\n value={jobFilter.format}\n onChange={(e) =>\n updateFilter({\n format: e.target.value as ExportFormat | \"all\",\n })\n }\n className=\"w-full px-3 py-2 border-2 border-border-subtle rounded-lg bg-bg-surface focus:outline-none focus:ring-2 focus:ring-primary/20 focus:border-primary\"\n >\n <option value=\"all\">\n {tr(\"export.history.filters.format.all\", \"All Formats\")}\n </option>\n <option value=\"CSV\">{tr(\"export.format.csv\", \"CSV\")}</option>\n <option value=\"JSON\">\n {tr(\"export.format.json\", \"JSON\")}\n </option>\n </select>\n </div>\n </div>\n </div>\n )}\n </div>\n\n {/* Error State */}\n {error && (\n <div className=\"border-2 border-border-default rounded-xl bg-status-error-bg-subtle p-6 flex items-start gap-4\">\n <AlertCircle\n size={24}\n className=\"text-status-error-text flex-shrink-0\"\n />\n <div>\n <h3 className=\"font-semibold text-status-error-text mb-1\">\n {tr(\"export.history.error.title\", \"Error loading exports\")}\n </h3>\n <p className=\"text-sm text-status-error-text mb-3\">\n {error.message}\n </p>\n <button\n type=\"button\"\n onClick={() => refetch()}\n className=\"inline-flex items-center gap-2 px-3 py-1.5 text-sm bg-status-error-bg text-status-error-text rounded-lg hover:bg-status-error-bg/90 transition-colors\"\n >\n <RefreshCw size={14} />\n {tr(\"export.history.error.retry\", \"Try Again\")}\n </button>\n </div>\n </div>\n )}\n\n {/* Loading */}\n {isLoading && (\n <div className=\"space-y-3\">\n {[...Array(5)].map((_, i) => (\n <div key={i} className=\"border border-border-seam rounded-xl p-4\">\n <div className=\"flex items-center justify-between\">\n <div className=\"space-y-2\">\n <div className=\"h-4 w-48 bg-bg-sunken rounded animate-vc-shimmer\" />\n <div className=\"h-3 w-32 bg-bg-sunken rounded animate-vc-shimmer\" />\n </div>\n <div className=\"h-5 w-20 bg-bg-sunken rounded-full animate-vc-shimmer\" />\n </div>\n </div>\n ))}\n </div>\n )}\n\n {/* Jobs List */}\n {!isLoading &&\n !error &&\n (filteredJobs.length > 0 ? (\n <>\n <div className=\"space-y-2\">\n {filteredJobs.map((job, index) => (\n <button\n type=\"button\"\n key={job.id}\n onClick={() => handleJobClick(job.id)}\n className={cn(\n \"w-full text-left border border-border-seam rounded-xl p-4 bg-bg-surface\",\n \"hover:border-primary/40 transition-colors duration-200\",\n \"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/40\",\n \"group animate-in fade-in-0\",\n )}\n style={{ animationDelay: `${index * 30}ms` }}\n >\n <div className=\"flex items-start sm:items-center justify-between gap-3\">\n <div className=\"flex-1 min-w-0\">\n <h4 className=\"font-semibold text-text-primary truncate\">\n {job.name}\n </h4>\n <p className=\"text-sm text-text-secondary mt-1 line-clamp-2\">\n {job.entityTypes.join(\", \")} &middot;{\" \"}\n {getFormatDisplayName(job.format)}\n {job.createdAt &&\n ` · ${new Date(job.createdAt).toLocaleDateString()}`}\n </p>\n </div>\n <div className=\"flex items-center gap-2 flex-shrink-0\">\n <ExportStatusBadge status={job.status} />\n <ChevronRight\n size={18}\n className=\"text-text-secondary group-hover:text-primary transition-colors hidden sm:block\"\n />\n </div>\n </div>\n </button>\n ))}\n </div>\n\n {/* Pagination */}\n {totalCount > pagination.pageSize && (\n <div className=\"flex flex-col sm:flex-row items-center justify-between gap-3 pt-2\">\n <p className=\"text-sm text-text-secondary\">\n {tr(\n \"export.history.pagination.showing\",\n \"Showing {{start}}-{{end}} of {{total}}\",\n {\n start: pageStart,\n end: pageEnd,\n total: totalCount,\n },\n )}\n </p>\n <div className=\"flex items-center gap-2\">\n <button\n type=\"button\"\n onClick={handlePrevPage}\n disabled={!hasPrevPage}\n className=\"inline-flex items-center gap-1.5 px-3 py-1.5 text-sm border-2 border-border-subtle/50 rounded-lg hover:border-primary/30 disabled:opacity-40 disabled:cursor-not-allowed transition-colors font-medium\"\n >\n <ChevronLeft size={16} />\n {tr(\"export.history.pagination.prev\", \"Prev\")}\n </button>\n <span className=\"text-sm text-text-secondary px-2\">\n {tr(\"export.history.pagination.page\", \"Page {{page}}\", {\n page: pagination.page,\n })}\n </span>\n <button\n type=\"button\"\n onClick={handleNextPage}\n disabled={!hasNextPage}\n className=\"inline-flex items-center gap-1.5 px-3 py-1.5 text-sm border-2 border-border-subtle/50 rounded-lg hover:border-primary/30 disabled:opacity-40 disabled:cursor-not-allowed transition-colors font-medium\"\n >\n {tr(\"export.history.pagination.next\", \"Next\")}\n <ChevronRight size={16} />\n </button>\n </div>\n </div>\n )}\n </>\n ) : (\n <IllustratedEmptyState\n illustration={hasActiveFilters ? \"empty-search\" : \"empty-data\"}\n title={\n hasActiveFilters\n ? tr(\n \"export.history.empty.filtered.title\",\n \"No matching exports\",\n )\n : tr(\"export.history.empty.title\", \"No exports yet\")\n }\n description={\n hasActiveFilters\n ? tr(\n \"export.history.empty.filtered.description\",\n \"Try adjusting your search or filters.\",\n )\n : tr(\n \"export.history.empty.description\",\n \"Create your first export to see it here.\",\n )\n }\n action={\n hasActiveFilters ? (\n <button\n type=\"button\"\n onClick={handleResetFilters}\n className=\"inline-flex items-center gap-2 px-4 py-2 border-2 border-border-subtle rounded-lg hover:bg-accent transition-colors font-medium\"\n >\n <X size={16} />\n {tr(\n \"export.history.empty.filtered.actions.clear\",\n \"Clear Filters\",\n )}\n </button>\n ) : (\n <button\n type=\"button\"\n onClick={() => navigate?.(joinPath(basePath, \"new\"))}\n className=\"inline-flex items-center gap-2 px-4 py-2 bg-action-primary-bg text-action-primary-text rounded-lg hover:bg-action-primary-bg/90 transition-colors font-medium\"\n >\n <Download size={16} />\n {tr(\n \"export.history.empty.actions.createFirst\",\n \"Create First Export\",\n )}\n </button>\n )\n }\n />\n ))}\n </PageLayout>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"ExportHistoryPage.js","names":[],"sources":["../../../src/export/pages/ExportHistoryPage.tsx"],"sourcesContent":["import { useState, useMemo, useEffect, useRef } from \"react\";\nimport { useSearchParams } from \"react-router\";\nimport {\n Search,\n X,\n Clock,\n Download,\n ChevronRight,\n ChevronLeft,\n RefreshCw,\n AlertCircle,\n Filter,\n} from \"lucide-react\";\nimport { IllustratedEmptyState, PagePurpose } from \"@burdenoff/fe-libs/ui\";\nimport { useExport } from \"../providers/ExportProvider\";\nimport { useExportJobs } from \"../hooks/useExportQueries\";\nimport { useExportStore } from \"../store/exportStore\";\nimport { ExportStatusBadge } from \"../components/ExportStatusBadge\";\nimport { PageLayout } from \"../components/PageLayout\";\nimport { useTr } from \"../hooks/useTr\";\nimport { cn } from \"../utils/cn\";\nimport {\n getFormatDisplayName,\n getAllStatuses,\n getStatusDisplayName,\n} from \"../constants/enums\";\nimport type { ExportJobStatus, ExportFormat } from \"../types\";\n\n/**\n * Helper to join paths correctly\n */\nconst joinPath = (base: string, path: string): string => {\n const cleanBase = base.endsWith(\"/\") ? base.slice(0, -1) : base;\n const cleanPath = path.startsWith(\"/\") ? path : `/${path}`;\n return cleanBase + cleanPath;\n};\n\n/**\n * Export History Page\n * Table/list of all past exports with filters and cursor-based pagination\n */\nexport function ExportHistoryPage() {\n const { basePath = \"/\", navigate } = useExport();\n const tr = useTr();\n const { jobFilter, updateFilter, resetFilter, pagination, setPagination } =\n useExportStore();\n const [searchParams] = useSearchParams();\n\n // Initialize status filter from URL search params (e.g. ?status=RUNNING) — once\n const didInitFromUrlRef = useRef(false);\n useEffect(() => {\n if (didInitFromUrlRef.current) return;\n didInitFromUrlRef.current = true;\n const statusParam = searchParams.get(\"status\");\n if (statusParam) {\n updateFilter({\n status: statusParam as ExportJobStatus,\n });\n }\n }, [searchParams, updateFilter]);\n\n const [showFilters, setShowFilters] = useState(false);\n // Cursor stack: index 0 = first page (no cursor), subsequent entries are \"after\" cursors\n const [cursorStack, setCursorStack] = useState<(string | null)[]>([null]);\n const [currentPageIndex, setCurrentPageIndex] = useState(0);\n\n const currentCursor = cursorStack[currentPageIndex] ?? null;\n\n const {\n data: jobs,\n totalCount,\n pageInfo,\n isLoading,\n error,\n refetch,\n } = useExportJobs(jobFilter, {\n first: pagination.pageSize,\n after: currentCursor ?? undefined,\n });\n\n // Client-side search filter (server-side filtering handles status/format)\n const filteredJobs = useMemo(() => {\n if (!jobFilter.search) return jobs;\n const q = jobFilter.search.toLowerCase();\n return jobs.filter(\n (job) =>\n job.name.toLowerCase().includes(q) ||\n job.entityTypes.some((e) => e.toLowerCase().includes(q)),\n );\n }, [jobs, jobFilter.search]);\n\n const hasActiveFilters =\n jobFilter.search ||\n jobFilter.status !== \"all\" ||\n jobFilter.format !== \"all\";\n\n const handleJobClick = (jobId: string) => {\n navigate?.(joinPath(basePath, jobId));\n };\n\n const handleNextPage = () => {\n if (!pageInfo?.hasNextPage || !pageInfo.endCursor) return;\n const newStack = [\n ...cursorStack.slice(0, currentPageIndex + 1),\n pageInfo.endCursor,\n ];\n setCursorStack(newStack);\n setCurrentPageIndex((prev) => prev + 1);\n setPagination({ page: pagination.page + 1, cursor: pageInfo.endCursor });\n };\n\n const handlePrevPage = () => {\n if (currentPageIndex === 0) return;\n const prevIndex = currentPageIndex - 1;\n setCurrentPageIndex(prevIndex);\n setPagination({\n page: pagination.page - 1,\n cursor: cursorStack[prevIndex] ?? null,\n });\n };\n\n const handleResetFilters = () => {\n resetFilter();\n setCursorStack([null]);\n setCurrentPageIndex(0);\n };\n\n const hasPrevPage = currentPageIndex > 0;\n const hasNextPage = pageInfo?.hasNextPage ?? false;\n const pageStart = currentPageIndex * pagination.pageSize + 1;\n const pageEnd = Math.min(\n (currentPageIndex + 1) * pagination.pageSize,\n totalCount,\n );\n const getStatusLabel = (status: ExportJobStatus) =>\n tr(\n `export.shared.status.${status.toLowerCase()}`,\n getStatusDisplayName(status),\n );\n\n return (\n <PageLayout\n title={tr(\"export.history.title\", \"Export History\")}\n subtitle={tr(\n \"export.history.subtitle\",\n \"View and manage all your past export jobs\",\n )}\n icon={<Clock size={24} />}\n headerActions={\n <button\n type=\"button\"\n onClick={() => navigate?.(joinPath(basePath, \"new\"))}\n className=\"inline-flex items-center gap-2 px-4 py-2 bg-action-primary-bg text-action-primary-text rounded-lg hover:bg-action-primary-bg/90 transition-colors font-medium\"\n >\n <Download size={16} />\n {tr(\"export.history.actions.newExport\", \"New Export\")}\n </button>\n }\n >\n {/* Page purpose — what History is for */}\n <PagePurpose icon={<Clock size={16} />}>\n {tr(\n \"export.history.purpose\",\n \"Every export you've ever started lives here. Track jobs as they run, see which finished or failed, and grab download links for completed exports before they expire. Search by name or entity type, or filter by status and format to find a specific one.\",\n )}\n </PagePurpose>\n\n {/* Search & Filters */}\n <div className=\"space-y-4\">\n <div className=\"flex flex-col sm:flex-row gap-3\">\n {/* Search */}\n <div className=\"flex-1 relative group\">\n <Search\n size={18}\n className=\"absolute left-3 top-1/2 -translate-y-1/2 text-text-secondary group-focus-within:text-primary transition-colors\"\n />\n <input\n type=\"text\"\n aria-label={tr(\n \"export.history.search.placeholder\",\n \"Search exports by name or entity type\",\n )}\n placeholder={tr(\n \"export.history.search.placeholder\",\n \"Search exports by name or entity type...\",\n )}\n value={jobFilter.search}\n onChange={(e) => updateFilter({ search: e.target.value })}\n className=\"w-full pl-[3.25rem] pr-4 py-2.5 border-2 border-border-subtle rounded-lg bg-bg-surface focus:outline-none focus:ring-2 focus:ring-primary/20 focus:border-primary transition-all duration-200\"\n />\n </div>\n\n {/* Filter Toggle */}\n <button\n type=\"button\"\n onClick={() => setShowFilters(!showFilters)}\n className={cn(\n \"inline-flex items-center gap-2 px-4 py-2.5 border-2 rounded-lg transition-all duration-200 font-medium\",\n showFilters || hasActiveFilters\n ? \"bg-action-primary-bg text-action-primary-text border-primary\"\n : \"border-border-subtle/50 hover:border-primary/30\",\n )}\n >\n <Filter size={16} />\n {showFilters\n ? tr(\"export.history.filters.hide\", \"Hide\")\n : tr(\"export.history.filters.show\", \"Filter\")}\n </button>\n </div>\n\n {/* Filter Panel */}\n {showFilters && (\n <div className=\"border border-border-seam rounded-xl p-4 animate-in fade-in-0 duration-200\">\n <div className=\"flex items-center justify-between mb-4\">\n <h3 className=\"font-semibold flex items-center gap-2\">\n <Filter size={16} className=\"text-primary\" />\n {tr(\"export.history.filters.title\", \"Filters\")}\n </h3>\n {hasActiveFilters && (\n <button\n type=\"button\"\n onClick={handleResetFilters}\n className=\"text-sm text-text-secondary hover:text-status-error-text flex items-center gap-1\"\n >\n <X size={14} />\n {tr(\"export.history.filters.clearAll\", \"Clear All\")}\n </button>\n )}\n </div>\n <div className=\"grid grid-cols-1 sm:grid-cols-2 gap-4\">\n {/* Status Filter */}\n <div className=\"space-y-2\">\n <label className=\"block text-sm font-medium\">\n {tr(\"export.history.filters.status.label\", \"Status\")}\n </label>\n <select\n value={jobFilter.status}\n onChange={(e) =>\n updateFilter({\n status: e.target.value as ExportJobStatus | \"all\",\n })\n }\n className=\"w-full px-3 py-2 border-2 border-border-subtle rounded-lg bg-bg-surface focus:outline-none focus:ring-2 focus:ring-primary/20 focus:border-primary scheme-normal dark:scheme-dark [&_option]:bg-bg-surface [&_option]:text-text-primary\"\n >\n <option value=\"all\">\n {tr(\"export.history.filters.status.all\", \"All Statuses\")}\n </option>\n {getAllStatuses().map((status) => (\n <option key={status} value={status}>\n {getStatusLabel(status)}\n </option>\n ))}\n </select>\n </div>\n\n {/* Format Filter — only CSV and JSON are supported */}\n <div className=\"space-y-2\">\n <label className=\"block text-sm font-medium\">\n {tr(\"export.history.filters.format.label\", \"Format\")}\n </label>\n <select\n value={jobFilter.format}\n onChange={(e) =>\n updateFilter({\n format: e.target.value as ExportFormat | \"all\",\n })\n }\n className=\"w-full px-3 py-2 border-2 border-border-subtle rounded-lg bg-bg-surface focus:outline-none focus:ring-2 focus:ring-primary/20 focus:border-primary scheme-normal dark:scheme-dark [&_option]:bg-bg-surface [&_option]:text-text-primary\"\n >\n <option value=\"all\">\n {tr(\"export.history.filters.format.all\", \"All Formats\")}\n </option>\n <option value=\"CSV\">{tr(\"export.format.csv\", \"CSV\")}</option>\n <option value=\"JSON\">\n {tr(\"export.format.json\", \"JSON\")}\n </option>\n </select>\n </div>\n </div>\n </div>\n )}\n </div>\n\n {/* Error State */}\n {error && (\n <div className=\"border-2 border-border-default rounded-xl bg-status-error-bg-subtle p-6 flex items-start gap-4\">\n <AlertCircle\n size={24}\n className=\"text-status-error-text flex-shrink-0\"\n />\n <div>\n <h3 className=\"font-semibold text-status-error-text mb-1\">\n {tr(\"export.history.error.title\", \"Error loading exports\")}\n </h3>\n <p className=\"text-sm text-status-error-text mb-3\">\n {error.message}\n </p>\n <button\n type=\"button\"\n onClick={() => refetch()}\n className=\"inline-flex items-center gap-2 px-3 py-1.5 text-sm bg-status-error-bg text-status-error-text rounded-lg hover:bg-status-error-bg/90 transition-colors\"\n >\n <RefreshCw size={14} />\n {tr(\"export.history.error.retry\", \"Try Again\")}\n </button>\n </div>\n </div>\n )}\n\n {/* Loading */}\n {isLoading && (\n <div className=\"space-y-3\">\n {[...Array(5)].map((_, i) => (\n <div key={i} className=\"border border-border-seam rounded-xl p-4\">\n <div className=\"flex items-center justify-between\">\n <div className=\"space-y-2\">\n <div className=\"h-4 w-48 bg-bg-sunken rounded animate-vc-shimmer\" />\n <div className=\"h-3 w-32 bg-bg-sunken rounded animate-vc-shimmer\" />\n </div>\n <div className=\"h-5 w-20 bg-bg-sunken rounded-full animate-vc-shimmer\" />\n </div>\n </div>\n ))}\n </div>\n )}\n\n {/* Jobs List */}\n {!isLoading &&\n !error &&\n (filteredJobs.length > 0 ? (\n <>\n <div className=\"space-y-2\">\n {filteredJobs.map((job, index) => (\n <button\n type=\"button\"\n key={job.id}\n onClick={() => handleJobClick(job.id)}\n className={cn(\n \"w-full text-left border border-border-seam rounded-xl p-4 bg-bg-surface\",\n \"hover:border-primary/40 transition-colors duration-200\",\n \"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/40\",\n \"group animate-in fade-in-0\",\n )}\n style={{ animationDelay: `${index * 30}ms` }}\n >\n <div className=\"flex items-start sm:items-center justify-between gap-3\">\n <div className=\"flex-1 min-w-0\">\n <h4 className=\"font-semibold text-text-primary truncate\">\n {job.name}\n </h4>\n <p className=\"text-sm text-text-secondary mt-1 line-clamp-2\">\n {job.entityTypes.join(\", \")} &middot;{\" \"}\n {getFormatDisplayName(job.format)}\n {job.createdAt &&\n ` · ${new Date(job.createdAt).toLocaleDateString()}`}\n </p>\n </div>\n <div className=\"flex items-center gap-2 flex-shrink-0\">\n <ExportStatusBadge status={job.status} />\n <ChevronRight\n size={18}\n className=\"text-text-secondary group-hover:text-primary transition-colors hidden sm:block\"\n />\n </div>\n </div>\n </button>\n ))}\n </div>\n\n {/* Pagination */}\n {totalCount > pagination.pageSize && (\n <div className=\"flex flex-col sm:flex-row items-center justify-between gap-3 pt-2\">\n <p className=\"text-sm text-text-secondary\">\n {tr(\n \"export.history.pagination.showing\",\n \"Showing {{start}}-{{end}} of {{total}}\",\n {\n start: pageStart,\n end: pageEnd,\n total: totalCount,\n },\n )}\n </p>\n <div className=\"flex items-center gap-2\">\n <button\n type=\"button\"\n onClick={handlePrevPage}\n disabled={!hasPrevPage}\n className=\"inline-flex items-center gap-1.5 px-3 py-1.5 text-sm border-2 border-border-subtle/50 rounded-lg hover:border-primary/30 disabled:opacity-40 disabled:cursor-not-allowed transition-colors font-medium\"\n >\n <ChevronLeft size={16} />\n {tr(\"export.history.pagination.prev\", \"Prev\")}\n </button>\n <span className=\"text-sm text-text-secondary px-2\">\n {tr(\"export.history.pagination.page\", \"Page {{page}}\", {\n page: pagination.page,\n })}\n </span>\n <button\n type=\"button\"\n onClick={handleNextPage}\n disabled={!hasNextPage}\n className=\"inline-flex items-center gap-1.5 px-3 py-1.5 text-sm border-2 border-border-subtle/50 rounded-lg hover:border-primary/30 disabled:opacity-40 disabled:cursor-not-allowed transition-colors font-medium\"\n >\n {tr(\"export.history.pagination.next\", \"Next\")}\n <ChevronRight size={16} />\n </button>\n </div>\n </div>\n )}\n </>\n ) : (\n <IllustratedEmptyState\n illustration={hasActiveFilters ? \"empty-search\" : \"empty-data\"}\n title={\n hasActiveFilters\n ? tr(\n \"export.history.empty.filtered.title\",\n \"No matching exports\",\n )\n : tr(\"export.history.empty.title\", \"No exports yet\")\n }\n description={\n hasActiveFilters\n ? tr(\n \"export.history.empty.filtered.description\",\n \"Try adjusting your search or filters.\",\n )\n : tr(\n \"export.history.empty.description\",\n \"Create your first export to see it here.\",\n )\n }\n action={\n hasActiveFilters ? (\n <button\n type=\"button\"\n onClick={handleResetFilters}\n className=\"inline-flex items-center gap-2 px-4 py-2 border-2 border-border-subtle rounded-lg hover:bg-accent transition-colors font-medium\"\n >\n <X size={16} />\n {tr(\n \"export.history.empty.filtered.actions.clear\",\n \"Clear Filters\",\n )}\n </button>\n ) : (\n <button\n type=\"button\"\n onClick={() => navigate?.(joinPath(basePath, \"new\"))}\n className=\"inline-flex items-center gap-2 px-4 py-2 bg-action-primary-bg text-action-primary-text rounded-lg hover:bg-action-primary-bg/90 transition-colors font-medium\"\n >\n <Download size={16} />\n {tr(\n \"export.history.empty.actions.createFirst\",\n \"Create First Export\",\n )}\n </button>\n )\n }\n />\n ))}\n </PageLayout>\n );\n}\n"],"mappings":"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"}
@@ -124,7 +124,7 @@ function w() {
124
124
  }), /* @__PURE__ */ y("select", {
125
125
  value: U,
126
126
  onChange: (e) => W(e.target.value),
127
- className: "w-full px-3 py-2 border-2 border-border-subtle rounded-lg bg-bg-surface focus:outline-none focus:ring-2 focus:ring-primary/20 focus:border-primary transition-all",
127
+ className: "w-full px-3 py-2 border-2 border-border-subtle rounded-lg bg-bg-surface focus:outline-none focus:ring-2 focus:ring-primary/20 focus:border-primary transition-all scheme-normal dark:scheme-dark [&_option]:bg-bg-surface [&_option]:text-text-primary",
128
128
  children: [/* @__PURE__ */ v("option", {
129
129
  value: "CSV",
130
130
  children: E("export.format.csv", "CSV")
@@ -1 +1 @@
1
- {"version":3,"file":"ExportTemplatesPage.js","names":[],"sources":["../../../src/export/pages/ExportTemplatesPage.tsx"],"sourcesContent":["import { useState, useMemo } from \"react\";\nimport {\n Search,\n X,\n FileBox,\n Plus,\n ChevronRight,\n Sparkles,\n Loader2,\n} from \"lucide-react\";\nimport {\n CTAOverflowMenu,\n IllustratedEmptyState,\n PagePurpose,\n} from \"@burdenoff/fe-libs/ui\";\nimport { useExport } from \"../providers/ExportProvider\";\nimport {\n useExportTemplates,\n useDeleteExportTemplate,\n useCreateExportTemplate,\n useAvailableEntities,\n} from \"../hooks/useExportQueries\";\nimport { PageLayout } from \"../components/PageLayout\";\nimport { getFormatDisplayName } from \"../constants/enums\";\nimport { useTr } from \"../hooks/useTr\";\nimport { cn } from \"../utils/cn\";\nimport type { ExportFormat } from \"../types\";\nimport { nativeImpact, nativeNotify } from \"../../utils/nativeBridge\";\n\n/**\n * Helper to join paths correctly\n */\nconst joinPath = (base: string, path: string): string => {\n const cleanBase = base.endsWith(\"/\") ? base.slice(0, -1) : base;\n const cleanPath = path.startsWith(\"/\") ? path : `/${path}`;\n return cleanBase + cleanPath;\n};\n\n/**\n * Export Templates Page\n * Template management list for reusable export configurations\n */\nexport function ExportTemplatesPage() {\n const { basePath = \"/\", navigate } = useExport();\n const tr = useTr();\n const { data: templates, isLoading } = useExportTemplates();\n const { deleteTemplate } = useDeleteExportTemplate();\n const { createTemplate, isPending: creating } = useCreateExportTemplate();\n const { data: availableEntities } = useAvailableEntities();\n\n const [searchQuery, setSearchQuery] = useState(\"\");\n const [showCreateForm, setShowCreateForm] = useState(false);\n const [newTemplateName, setNewTemplateName] = useState(\"\");\n const [newTemplateDescription, setNewTemplateDescription] = useState(\"\");\n const [newTemplateEntityTypes, setNewTemplateEntityTypes] = useState<\n string[]\n >([]);\n const [newTemplateFormat, setNewTemplateFormat] =\n useState<ExportFormat>(\"CSV\");\n const [createError, setCreateError] = useState<string | null>(null);\n const [templatePendingDelete, setTemplatePendingDelete] = useState<\n string | null\n >(null);\n\n const handleOpenCreate = () => {\n setNewTemplateName(\"\");\n setNewTemplateDescription(\"\");\n setNewTemplateEntityTypes([]);\n setNewTemplateFormat(\"CSV\");\n setCreateError(null);\n setShowCreateForm(true);\n };\n\n const handleCreateTemplate = async () => {\n if (!newTemplateName.trim()) {\n setCreateError(\n tr(\n \"export.templates.form.errors.nameRequired\",\n \"Template name is required.\",\n ),\n );\n return;\n }\n if (newTemplateEntityTypes.length === 0) {\n setCreateError(\n tr(\n \"export.templates.form.errors.entityRequired\",\n \"Select at least one entity type.\",\n ),\n );\n return;\n }\n setCreateError(null);\n try {\n await createTemplate({\n name: newTemplateName.trim(),\n description: newTemplateDescription.trim() || undefined,\n entityTypes: newTemplateEntityTypes,\n format: newTemplateFormat,\n });\n setShowCreateForm(false);\n } catch (err) {\n setCreateError(\n err instanceof Error\n ? err.message\n : tr(\n \"export.templates.form.errors.createFailed\",\n \"Failed to create template.\",\n ),\n );\n }\n };\n\n const toggleEntity = (entityType: string) => {\n setNewTemplateEntityTypes((prev) =>\n prev.includes(entityType)\n ? prev.filter((e) => e !== entityType)\n : [...prev, entityType],\n );\n };\n\n // Client-side search filter\n const filteredTemplates = useMemo(() => {\n if (!searchQuery) return templates;\n const q = searchQuery.toLowerCase();\n return templates.filter(\n (t) =>\n t.name.toLowerCase().includes(q) ||\n t.entityTypes.some((e) => e.toLowerCase().includes(q)),\n );\n }, [templates, searchQuery]);\n\n const handleTemplateClick = (templateId: string) => {\n navigate?.(joinPath(basePath, `templates/${templateId}`));\n };\n\n const handleDeleteTemplate = async (\n e: React.MouseEvent,\n templateId: string,\n ) => {\n e.stopPropagation();\n nativeImpact(\"medium\");\n setTemplatePendingDelete(templateId);\n };\n\n const confirmDeleteTemplate = async () => {\n if (!templatePendingDelete) return;\n try {\n await deleteTemplate(templatePendingDelete);\n nativeNotify(\"success\");\n } catch {\n nativeNotify(\"error\");\n }\n setTemplatePendingDelete(null);\n };\n\n return (\n <PageLayout\n title={tr(\"export.templates.title\", \"Export Templates\")}\n subtitle={tr(\n \"export.templates.subtitle\",\n \"Reusable export configurations for common data exports\",\n )}\n icon={<FileBox size={24} />}\n headerActions={\n <CTAOverflowMenu\n primary={{\n label: tr(\"export.templates.actions.newTemplate\", \"New Template\"),\n icon: <Plus size={16} />,\n onSelect: handleOpenCreate,\n }}\n actions={[\n {\n label: tr(\"export.templates.actions.newExport\", \"New Export\"),\n icon: <Plus size={16} />,\n onSelect: () => navigate?.(joinPath(basePath, \"new\")),\n },\n ]}\n />\n }\n headerContent={\n <span className=\"inline-flex items-center gap-1.5 px-2.5 py-0.5 rounded-full text-xs font-medium bg-bg-sunken text-text-secondary\">\n <Sparkles size={12} />\n {tr(\n \"export.templates.summary.available\",\n \"{{count}} template{{suffix}} available\",\n {\n count: filteredTemplates.length,\n suffix: filteredTemplates.length !== 1 ? \"s\" : \"\",\n },\n )}\n </span>\n }\n >\n {/* Page purpose — why templates exist */}\n <PagePurpose icon={<FileBox size={16} />}>\n {tr(\n \"export.templates.purpose\",\n \"Templates save an export setup — the entity types and format you regularly need — so you don't rebuild it each time. Create one here, then start a new export from it in a single click. Handy for recurring backups, periodic audits, or any export you run more than once.\",\n )}\n </PagePurpose>\n\n {/* Create Template Form */}\n {showCreateForm && (\n <div className=\"border border-primary/30 rounded-xl p-6 bg-accent-wash space-y-4\">\n <div className=\"flex items-center justify-between\">\n <h3 className=\"font-semibold text-lg\">\n {tr(\"export.templates.form.title\", \"New Template\")}\n </h3>\n <button\n type=\"button\"\n onClick={() => setShowCreateForm(false)}\n className=\"p-1.5 rounded-md hover:bg-accent\"\n >\n <X size={16} />\n </button>\n </div>\n {createError && (\n <p className=\"text-sm text-status-error-text bg-status-error-bg/10 rounded-lg px-3 py-2\">\n {createError}\n </p>\n )}\n <div className=\"grid grid-cols-1 sm:grid-cols-2 gap-4\">\n <div className=\"space-y-1\">\n <label className=\"text-sm font-medium\">\n {tr(\"export.templates.form.fields.name\", \"Name\")} *\n </label>\n <input\n type=\"text\"\n aria-label={tr(\"export.templates.form.fields.name\", \"Name\")}\n placeholder={tr(\n \"export.templates.form.fields.namePlaceholder\",\n \"e.g. Weekly Tags Export\",\n )}\n value={newTemplateName}\n onChange={(e) => setNewTemplateName(e.target.value)}\n className=\"w-full px-3 py-2 border-2 border-border-subtle rounded-lg bg-bg-surface focus:outline-none focus:ring-2 focus:ring-primary/20 focus:border-primary transition-all\"\n />\n </div>\n <div className=\"space-y-1\">\n <label className=\"text-sm font-medium\">\n {tr(\"export.templates.form.fields.format\", \"Format\")} *\n </label>\n <select\n value={newTemplateFormat}\n onChange={(e) =>\n setNewTemplateFormat(e.target.value as ExportFormat)\n }\n className=\"w-full px-3 py-2 border-2 border-border-subtle rounded-lg bg-bg-surface focus:outline-none focus:ring-2 focus:ring-primary/20 focus:border-primary transition-all\"\n >\n <option value=\"CSV\">{tr(\"export.format.csv\", \"CSV\")}</option>\n <option value=\"JSON\">{tr(\"export.format.json\", \"JSON\")}</option>\n </select>\n </div>\n </div>\n <div className=\"space-y-1\">\n <label className=\"text-sm font-medium\">\n {tr(\"export.templates.form.fields.description\", \"Description\")}\n </label>\n <input\n type=\"text\"\n aria-label={tr(\n \"export.templates.form.fields.description\",\n \"Description\",\n )}\n placeholder={tr(\n \"export.templates.form.fields.descriptionPlaceholder\",\n \"Optional description\",\n )}\n value={newTemplateDescription}\n onChange={(e) => setNewTemplateDescription(e.target.value)}\n className=\"w-full px-3 py-2 border-2 border-border-subtle rounded-lg bg-bg-surface focus:outline-none focus:ring-2 focus:ring-primary/20 focus:border-primary transition-all\"\n />\n </div>\n <div className=\"space-y-2\">\n <label className=\"text-sm font-medium\">\n {tr(\"export.templates.form.fields.entityTypes\", \"Entity Types\")} *{\" \"}\n <span className=\"text-text-secondary font-normal\">\n {tr(\n \"export.templates.form.fields.entityTypesHint\",\n \"(select at least one)\",\n )}\n </span>\n </label>\n <div className=\"flex flex-wrap gap-2\">\n {availableEntities.map((entity) => (\n <button\n key={entity.entityType}\n type=\"button\"\n onClick={() => toggleEntity(entity.entityType)}\n className={cn(\n \"px-3 py-1.5 rounded-lg text-sm font-medium border-2 transition-all\",\n newTemplateEntityTypes.includes(entity.entityType)\n ? \"border-primary bg-action-primary-bg/10 text-primary\"\n : \"border-border-subtle bg-bg-surface hover:border-primary/50\",\n )}\n >\n {entity.displayName}\n </button>\n ))}\n </div>\n </div>\n <div className=\"flex gap-3 pt-2\">\n <button\n type=\"button\"\n onClick={handleCreateTemplate}\n disabled={creating}\n className=\"inline-flex items-center gap-2 px-5 py-2.5 bg-action-primary-bg text-action-primary-text rounded-lg hover:bg-action-primary-bg/90 transition-colors font-medium disabled:opacity-50\"\n >\n {creating ? (\n <Loader2 size={16} className=\"animate-spin\" />\n ) : (\n <Plus size={16} />\n )}\n {tr(\"export.templates.form.actions.create\", \"Create Template\")}\n </button>\n <button\n type=\"button\"\n onClick={() => setShowCreateForm(false)}\n className=\"px-5 py-2.5 border-2 border-border-subtle rounded-lg hover:bg-accent transition-colors font-medium\"\n >\n {tr(\"export.templates.form.actions.cancel\", \"Cancel\")}\n </button>\n </div>\n </div>\n )}\n {/* Search */}\n <div className=\"relative group\">\n <Search\n size={18}\n className=\"absolute left-3 top-1/2 -translate-y-1/2 text-text-secondary group-focus-within:text-primary transition-colors\"\n />\n <input\n type=\"text\"\n aria-label={tr(\n \"export.templates.search.placeholder\",\n \"Search templates\",\n )}\n placeholder={tr(\n \"export.templates.search.placeholder\",\n \"Search templates...\",\n )}\n value={searchQuery}\n onChange={(e) => setSearchQuery(e.target.value)}\n className=\"w-full pl-[3.25rem] pr-4 py-2.5 border-2 border-border-subtle rounded-lg bg-bg-surface focus:outline-none focus:ring-2 focus:ring-primary/20 focus:border-primary transition-all duration-200\"\n />\n {searchQuery && (\n <button\n type=\"button\"\n onClick={() => setSearchQuery(\"\")}\n className=\"absolute right-3 top-1/2 -translate-y-1/2 p-1 rounded-md text-text-secondary hover:text-text-primary hover:bg-accent\"\n >\n <X size={16} />\n </button>\n )}\n </div>\n\n {/* Loading */}\n {isLoading && (\n <div className=\"grid grid-cols-1 lg:grid-cols-2 gap-4\">\n {[...Array(4)].map((_, i) => (\n <div key={i} className=\"border border-border-seam rounded-xl p-5\">\n <div className=\"h-5 w-40 bg-bg-sunken rounded mb-3 animate-vc-shimmer\" />\n <div className=\"h-3 w-full bg-bg-sunken rounded mb-2 animate-vc-shimmer\" />\n <div className=\"flex gap-2 mt-4\">\n <div className=\"h-5 w-16 bg-bg-sunken rounded-full animate-vc-shimmer\" />\n <div className=\"h-5 w-16 bg-bg-sunken rounded-full animate-vc-shimmer\" />\n </div>\n </div>\n ))}\n </div>\n )}\n\n {/* Templates Grid */}\n {!isLoading ? (\n filteredTemplates.length > 0 ? (\n <div className=\"grid grid-cols-1 lg:grid-cols-2 gap-4\">\n {filteredTemplates.map((template, index) => (\n <div\n key={template.id}\n role=\"button\"\n tabIndex={0}\n onClick={() => handleTemplateClick(template.id)}\n onKeyDown={(e) => {\n if (e.key === \"Enter\" || e.key === \" \") {\n e.preventDefault();\n handleTemplateClick(template.id);\n }\n }}\n className={cn(\n \"text-left group cursor-pointer border border-border-seam rounded-xl overflow-hidden bg-bg-surface\",\n \"hover:border-primary/40 shadow-[var(--shadow-pop)] transition-colors duration-200\",\n \"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/40\",\n \"animate-in fade-in-0\",\n )}\n style={{ animationDelay: `${index * 50}ms` }}\n >\n {/* Header */}\n <div className=\"p-4 border-b bg-bg-sunken/30\">\n <div className=\"flex items-start justify-between\">\n <div className=\"flex items-center gap-3\">\n <div className=\"p-2 rounded-lg bg-action-primary-bg/10\">\n <FileBox size={20} className=\"text-primary\" />\n </div>\n <div>\n <h3 className=\"font-semibold\">{template.name}</h3>\n <p className=\"text-xs text-text-secondary\">\n {getFormatDisplayName(template.format)}\n {template.isDefault && (\n <span className=\"ml-1.5 inline-flex items-center px-1.5 py-0.5 rounded text-xs bg-action-primary-bg/10 text-primary font-medium\">\n {tr(\"export.templates.card.default\", \"Default\")}\n </span>\n )}\n </p>\n </div>\n </div>\n <div className=\"flex items-center gap-2\">\n <button\n type=\"button\"\n onClick={(e) => handleDeleteTemplate(e, template.id)}\n className=\"p-1.5 rounded-md text-text-secondary hover:text-status-error-text hover:bg-status-error-bg/10 transition-colors\"\n aria-label={tr(\n \"export.templates.card.deleteAriaLabel\",\n \"Delete {name}\",\n { name: template.name },\n )}\n >\n <X size={14} />\n </button>\n <ChevronRight\n size={18}\n className=\"text-text-secondary group-hover:text-primary transition-colors mt-1\"\n />\n </div>\n </div>\n </div>\n\n {/* Body */}\n <div className=\"p-4\">\n {template.description && (\n <p className=\"text-sm text-text-secondary mb-3 line-clamp-2\">\n {template.description}\n </p>\n )}\n <div className=\"flex flex-wrap gap-1.5 overflow-hidden\">\n {template.entityTypes.slice(0, 6).map((entity) => (\n <span\n key={entity}\n className=\"inline-flex px-2 py-0.5 rounded text-xs font-medium bg-bg-sunken text-text-secondary max-w-full truncate\"\n >\n {entity}\n </span>\n ))}\n {template.entityTypes.length > 6 && (\n <span className=\"inline-flex px-2 py-0.5 rounded text-xs font-medium bg-bg-sunken text-text-secondary\">\n {tr(\n \"export.templates.card.moreEntities\",\n \"+{count} more\",\n {\n count: template.entityTypes.length - 6,\n },\n )}\n </span>\n )}\n </div>\n </div>\n </div>\n ))}\n </div>\n ) : (\n <IllustratedEmptyState\n illustration={searchQuery ? \"empty-search\" : \"empty-data\"}\n title={\n searchQuery\n ? tr(\n \"export.templates.empty.filtered.title\",\n \"No matching templates\",\n )\n : tr(\"export.templates.empty.title\", \"No templates yet\")\n }\n description={\n searchQuery\n ? tr(\n \"export.templates.empty.filtered.description\",\n \"Try adjusting your search to find what you are looking for.\",\n )\n : tr(\n \"export.templates.empty.description\",\n \"Create a template to save reusable export configurations.\",\n )\n }\n action={\n searchQuery ? (\n <button\n type=\"button\"\n onClick={() => setSearchQuery(\"\")}\n className=\"inline-flex items-center gap-2 px-4 py-2 border-2 border-border-subtle rounded-lg hover:bg-accent transition-colors font-medium\"\n >\n <X size={16} />\n {tr(\n \"export.templates.empty.filtered.actions.clear\",\n \"Clear Search\",\n )}\n </button>\n ) : (\n <button\n type=\"button\"\n onClick={handleOpenCreate}\n className=\"inline-flex items-center gap-2 px-4 py-2 bg-action-primary-bg text-action-primary-text rounded-lg hover:bg-action-primary-bg/90 transition-colors font-medium\"\n >\n <Plus size={16} />\n {tr(\n \"export.templates.empty.actions.newTemplate\",\n \"New Template\",\n )}\n </button>\n )\n }\n />\n )\n ) : null}\n {templatePendingDelete && (\n <div\n role=\"dialog\"\n aria-modal=\"true\"\n aria-labelledby=\"template-list-delete-title\"\n className=\"fixed inset-0 z-50 flex items-center justify-center bg-bg-surface/80 backdrop-blur-sm p-4\"\n >\n <div className=\"w-full max-w-md rounded-xl border-2 border-border-subtle bg-bg-surface p-6 shadow-xl\">\n <h2 id=\"template-list-delete-title\" className=\"text-lg font-bold\">\n {tr(\"export.templates.delete.title\", \"Delete template?\")}\n </h2>\n <p className=\"mt-2 text-sm text-text-secondary\">\n {tr(\n \"export.templates.delete.description\",\n \"This removes the saved export configuration. Existing export jobs are not deleted.\",\n )}\n </p>\n <div className=\"mt-6 flex flex-col-reverse sm:flex-row sm:justify-end gap-3\">\n <button\n type=\"button\"\n onClick={() => setTemplatePendingDelete(null)}\n className=\"px-4 py-2 rounded-lg border-2 border-border-subtle hover:bg-accent transition-colors font-medium\"\n >\n {tr(\"export.templates.delete.cancel\", \"Keep Template\")}\n </button>\n <button\n type=\"button\"\n onClick={confirmDeleteTemplate}\n className=\"px-4 py-2 rounded-lg bg-status-error-bg text-status-error-text hover:bg-status-error-bg/90 transition-colors font-medium\"\n >\n {tr(\"export.templates.delete.confirmAction\", \"Delete Template\")}\n </button>\n </div>\n </div>\n </div>\n )}\n </PageLayout>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"ExportTemplatesPage.js","names":[],"sources":["../../../src/export/pages/ExportTemplatesPage.tsx"],"sourcesContent":["import { useState, useMemo } from \"react\";\nimport {\n Search,\n X,\n FileBox,\n Plus,\n ChevronRight,\n Sparkles,\n Loader2,\n} from \"lucide-react\";\nimport {\n CTAOverflowMenu,\n IllustratedEmptyState,\n PagePurpose,\n} from \"@burdenoff/fe-libs/ui\";\nimport { useExport } from \"../providers/ExportProvider\";\nimport {\n useExportTemplates,\n useDeleteExportTemplate,\n useCreateExportTemplate,\n useAvailableEntities,\n} from \"../hooks/useExportQueries\";\nimport { PageLayout } from \"../components/PageLayout\";\nimport { getFormatDisplayName } from \"../constants/enums\";\nimport { useTr } from \"../hooks/useTr\";\nimport { cn } from \"../utils/cn\";\nimport type { ExportFormat } from \"../types\";\nimport { nativeImpact, nativeNotify } from \"../../utils/nativeBridge\";\n\n/**\n * Helper to join paths correctly\n */\nconst joinPath = (base: string, path: string): string => {\n const cleanBase = base.endsWith(\"/\") ? base.slice(0, -1) : base;\n const cleanPath = path.startsWith(\"/\") ? path : `/${path}`;\n return cleanBase + cleanPath;\n};\n\n/**\n * Export Templates Page\n * Template management list for reusable export configurations\n */\nexport function ExportTemplatesPage() {\n const { basePath = \"/\", navigate } = useExport();\n const tr = useTr();\n const { data: templates, isLoading } = useExportTemplates();\n const { deleteTemplate } = useDeleteExportTemplate();\n const { createTemplate, isPending: creating } = useCreateExportTemplate();\n const { data: availableEntities } = useAvailableEntities();\n\n const [searchQuery, setSearchQuery] = useState(\"\");\n const [showCreateForm, setShowCreateForm] = useState(false);\n const [newTemplateName, setNewTemplateName] = useState(\"\");\n const [newTemplateDescription, setNewTemplateDescription] = useState(\"\");\n const [newTemplateEntityTypes, setNewTemplateEntityTypes] = useState<\n string[]\n >([]);\n const [newTemplateFormat, setNewTemplateFormat] =\n useState<ExportFormat>(\"CSV\");\n const [createError, setCreateError] = useState<string | null>(null);\n const [templatePendingDelete, setTemplatePendingDelete] = useState<\n string | null\n >(null);\n\n const handleOpenCreate = () => {\n setNewTemplateName(\"\");\n setNewTemplateDescription(\"\");\n setNewTemplateEntityTypes([]);\n setNewTemplateFormat(\"CSV\");\n setCreateError(null);\n setShowCreateForm(true);\n };\n\n const handleCreateTemplate = async () => {\n if (!newTemplateName.trim()) {\n setCreateError(\n tr(\n \"export.templates.form.errors.nameRequired\",\n \"Template name is required.\",\n ),\n );\n return;\n }\n if (newTemplateEntityTypes.length === 0) {\n setCreateError(\n tr(\n \"export.templates.form.errors.entityRequired\",\n \"Select at least one entity type.\",\n ),\n );\n return;\n }\n setCreateError(null);\n try {\n await createTemplate({\n name: newTemplateName.trim(),\n description: newTemplateDescription.trim() || undefined,\n entityTypes: newTemplateEntityTypes,\n format: newTemplateFormat,\n });\n setShowCreateForm(false);\n } catch (err) {\n setCreateError(\n err instanceof Error\n ? err.message\n : tr(\n \"export.templates.form.errors.createFailed\",\n \"Failed to create template.\",\n ),\n );\n }\n };\n\n const toggleEntity = (entityType: string) => {\n setNewTemplateEntityTypes((prev) =>\n prev.includes(entityType)\n ? prev.filter((e) => e !== entityType)\n : [...prev, entityType],\n );\n };\n\n // Client-side search filter\n const filteredTemplates = useMemo(() => {\n if (!searchQuery) return templates;\n const q = searchQuery.toLowerCase();\n return templates.filter(\n (t) =>\n t.name.toLowerCase().includes(q) ||\n t.entityTypes.some((e) => e.toLowerCase().includes(q)),\n );\n }, [templates, searchQuery]);\n\n const handleTemplateClick = (templateId: string) => {\n navigate?.(joinPath(basePath, `templates/${templateId}`));\n };\n\n const handleDeleteTemplate = async (\n e: React.MouseEvent,\n templateId: string,\n ) => {\n e.stopPropagation();\n nativeImpact(\"medium\");\n setTemplatePendingDelete(templateId);\n };\n\n const confirmDeleteTemplate = async () => {\n if (!templatePendingDelete) return;\n try {\n await deleteTemplate(templatePendingDelete);\n nativeNotify(\"success\");\n } catch {\n nativeNotify(\"error\");\n }\n setTemplatePendingDelete(null);\n };\n\n return (\n <PageLayout\n title={tr(\"export.templates.title\", \"Export Templates\")}\n subtitle={tr(\n \"export.templates.subtitle\",\n \"Reusable export configurations for common data exports\",\n )}\n icon={<FileBox size={24} />}\n headerActions={\n <CTAOverflowMenu\n primary={{\n label: tr(\"export.templates.actions.newTemplate\", \"New Template\"),\n icon: <Plus size={16} />,\n onSelect: handleOpenCreate,\n }}\n actions={[\n {\n label: tr(\"export.templates.actions.newExport\", \"New Export\"),\n icon: <Plus size={16} />,\n onSelect: () => navigate?.(joinPath(basePath, \"new\")),\n },\n ]}\n />\n }\n headerContent={\n <span className=\"inline-flex items-center gap-1.5 px-2.5 py-0.5 rounded-full text-xs font-medium bg-bg-sunken text-text-secondary\">\n <Sparkles size={12} />\n {tr(\n \"export.templates.summary.available\",\n \"{{count}} template{{suffix}} available\",\n {\n count: filteredTemplates.length,\n suffix: filteredTemplates.length !== 1 ? \"s\" : \"\",\n },\n )}\n </span>\n }\n >\n {/* Page purpose — why templates exist */}\n <PagePurpose icon={<FileBox size={16} />}>\n {tr(\n \"export.templates.purpose\",\n \"Templates save an export setup — the entity types and format you regularly need — so you don't rebuild it each time. Create one here, then start a new export from it in a single click. Handy for recurring backups, periodic audits, or any export you run more than once.\",\n )}\n </PagePurpose>\n\n {/* Create Template Form */}\n {showCreateForm && (\n <div className=\"border border-primary/30 rounded-xl p-6 bg-accent-wash space-y-4\">\n <div className=\"flex items-center justify-between\">\n <h3 className=\"font-semibold text-lg\">\n {tr(\"export.templates.form.title\", \"New Template\")}\n </h3>\n <button\n type=\"button\"\n onClick={() => setShowCreateForm(false)}\n className=\"p-1.5 rounded-md hover:bg-accent\"\n >\n <X size={16} />\n </button>\n </div>\n {createError && (\n <p className=\"text-sm text-status-error-text bg-status-error-bg/10 rounded-lg px-3 py-2\">\n {createError}\n </p>\n )}\n <div className=\"grid grid-cols-1 sm:grid-cols-2 gap-4\">\n <div className=\"space-y-1\">\n <label className=\"text-sm font-medium\">\n {tr(\"export.templates.form.fields.name\", \"Name\")} *\n </label>\n <input\n type=\"text\"\n aria-label={tr(\"export.templates.form.fields.name\", \"Name\")}\n placeholder={tr(\n \"export.templates.form.fields.namePlaceholder\",\n \"e.g. Weekly Tags Export\",\n )}\n value={newTemplateName}\n onChange={(e) => setNewTemplateName(e.target.value)}\n className=\"w-full px-3 py-2 border-2 border-border-subtle rounded-lg bg-bg-surface focus:outline-none focus:ring-2 focus:ring-primary/20 focus:border-primary transition-all\"\n />\n </div>\n <div className=\"space-y-1\">\n <label className=\"text-sm font-medium\">\n {tr(\"export.templates.form.fields.format\", \"Format\")} *\n </label>\n <select\n value={newTemplateFormat}\n onChange={(e) =>\n setNewTemplateFormat(e.target.value as ExportFormat)\n }\n className=\"w-full px-3 py-2 border-2 border-border-subtle rounded-lg bg-bg-surface focus:outline-none focus:ring-2 focus:ring-primary/20 focus:border-primary transition-all scheme-normal dark:scheme-dark [&_option]:bg-bg-surface [&_option]:text-text-primary\"\n >\n <option value=\"CSV\">{tr(\"export.format.csv\", \"CSV\")}</option>\n <option value=\"JSON\">{tr(\"export.format.json\", \"JSON\")}</option>\n </select>\n </div>\n </div>\n <div className=\"space-y-1\">\n <label className=\"text-sm font-medium\">\n {tr(\"export.templates.form.fields.description\", \"Description\")}\n </label>\n <input\n type=\"text\"\n aria-label={tr(\n \"export.templates.form.fields.description\",\n \"Description\",\n )}\n placeholder={tr(\n \"export.templates.form.fields.descriptionPlaceholder\",\n \"Optional description\",\n )}\n value={newTemplateDescription}\n onChange={(e) => setNewTemplateDescription(e.target.value)}\n className=\"w-full px-3 py-2 border-2 border-border-subtle rounded-lg bg-bg-surface focus:outline-none focus:ring-2 focus:ring-primary/20 focus:border-primary transition-all\"\n />\n </div>\n <div className=\"space-y-2\">\n <label className=\"text-sm font-medium\">\n {tr(\"export.templates.form.fields.entityTypes\", \"Entity Types\")} *{\" \"}\n <span className=\"text-text-secondary font-normal\">\n {tr(\n \"export.templates.form.fields.entityTypesHint\",\n \"(select at least one)\",\n )}\n </span>\n </label>\n <div className=\"flex flex-wrap gap-2\">\n {availableEntities.map((entity) => (\n <button\n key={entity.entityType}\n type=\"button\"\n onClick={() => toggleEntity(entity.entityType)}\n className={cn(\n \"px-3 py-1.5 rounded-lg text-sm font-medium border-2 transition-all\",\n newTemplateEntityTypes.includes(entity.entityType)\n ? \"border-primary bg-action-primary-bg/10 text-primary\"\n : \"border-border-subtle bg-bg-surface hover:border-primary/50\",\n )}\n >\n {entity.displayName}\n </button>\n ))}\n </div>\n </div>\n <div className=\"flex gap-3 pt-2\">\n <button\n type=\"button\"\n onClick={handleCreateTemplate}\n disabled={creating}\n className=\"inline-flex items-center gap-2 px-5 py-2.5 bg-action-primary-bg text-action-primary-text rounded-lg hover:bg-action-primary-bg/90 transition-colors font-medium disabled:opacity-50\"\n >\n {creating ? (\n <Loader2 size={16} className=\"animate-spin\" />\n ) : (\n <Plus size={16} />\n )}\n {tr(\"export.templates.form.actions.create\", \"Create Template\")}\n </button>\n <button\n type=\"button\"\n onClick={() => setShowCreateForm(false)}\n className=\"px-5 py-2.5 border-2 border-border-subtle rounded-lg hover:bg-accent transition-colors font-medium\"\n >\n {tr(\"export.templates.form.actions.cancel\", \"Cancel\")}\n </button>\n </div>\n </div>\n )}\n {/* Search */}\n <div className=\"relative group\">\n <Search\n size={18}\n className=\"absolute left-3 top-1/2 -translate-y-1/2 text-text-secondary group-focus-within:text-primary transition-colors\"\n />\n <input\n type=\"text\"\n aria-label={tr(\n \"export.templates.search.placeholder\",\n \"Search templates\",\n )}\n placeholder={tr(\n \"export.templates.search.placeholder\",\n \"Search templates...\",\n )}\n value={searchQuery}\n onChange={(e) => setSearchQuery(e.target.value)}\n className=\"w-full pl-[3.25rem] pr-4 py-2.5 border-2 border-border-subtle rounded-lg bg-bg-surface focus:outline-none focus:ring-2 focus:ring-primary/20 focus:border-primary transition-all duration-200\"\n />\n {searchQuery && (\n <button\n type=\"button\"\n onClick={() => setSearchQuery(\"\")}\n className=\"absolute right-3 top-1/2 -translate-y-1/2 p-1 rounded-md text-text-secondary hover:text-text-primary hover:bg-accent\"\n >\n <X size={16} />\n </button>\n )}\n </div>\n\n {/* Loading */}\n {isLoading && (\n <div className=\"grid grid-cols-1 lg:grid-cols-2 gap-4\">\n {[...Array(4)].map((_, i) => (\n <div key={i} className=\"border border-border-seam rounded-xl p-5\">\n <div className=\"h-5 w-40 bg-bg-sunken rounded mb-3 animate-vc-shimmer\" />\n <div className=\"h-3 w-full bg-bg-sunken rounded mb-2 animate-vc-shimmer\" />\n <div className=\"flex gap-2 mt-4\">\n <div className=\"h-5 w-16 bg-bg-sunken rounded-full animate-vc-shimmer\" />\n <div className=\"h-5 w-16 bg-bg-sunken rounded-full animate-vc-shimmer\" />\n </div>\n </div>\n ))}\n </div>\n )}\n\n {/* Templates Grid */}\n {!isLoading ? (\n filteredTemplates.length > 0 ? (\n <div className=\"grid grid-cols-1 lg:grid-cols-2 gap-4\">\n {filteredTemplates.map((template, index) => (\n <div\n key={template.id}\n role=\"button\"\n tabIndex={0}\n onClick={() => handleTemplateClick(template.id)}\n onKeyDown={(e) => {\n if (e.key === \"Enter\" || e.key === \" \") {\n e.preventDefault();\n handleTemplateClick(template.id);\n }\n }}\n className={cn(\n \"text-left group cursor-pointer border border-border-seam rounded-xl overflow-hidden bg-bg-surface\",\n \"hover:border-primary/40 shadow-[var(--shadow-pop)] transition-colors duration-200\",\n \"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/40\",\n \"animate-in fade-in-0\",\n )}\n style={{ animationDelay: `${index * 50}ms` }}\n >\n {/* Header */}\n <div className=\"p-4 border-b bg-bg-sunken/30\">\n <div className=\"flex items-start justify-between\">\n <div className=\"flex items-center gap-3\">\n <div className=\"p-2 rounded-lg bg-action-primary-bg/10\">\n <FileBox size={20} className=\"text-primary\" />\n </div>\n <div>\n <h3 className=\"font-semibold\">{template.name}</h3>\n <p className=\"text-xs text-text-secondary\">\n {getFormatDisplayName(template.format)}\n {template.isDefault && (\n <span className=\"ml-1.5 inline-flex items-center px-1.5 py-0.5 rounded text-xs bg-action-primary-bg/10 text-primary font-medium\">\n {tr(\"export.templates.card.default\", \"Default\")}\n </span>\n )}\n </p>\n </div>\n </div>\n <div className=\"flex items-center gap-2\">\n <button\n type=\"button\"\n onClick={(e) => handleDeleteTemplate(e, template.id)}\n className=\"p-1.5 rounded-md text-text-secondary hover:text-status-error-text hover:bg-status-error-bg/10 transition-colors\"\n aria-label={tr(\n \"export.templates.card.deleteAriaLabel\",\n \"Delete {name}\",\n { name: template.name },\n )}\n >\n <X size={14} />\n </button>\n <ChevronRight\n size={18}\n className=\"text-text-secondary group-hover:text-primary transition-colors mt-1\"\n />\n </div>\n </div>\n </div>\n\n {/* Body */}\n <div className=\"p-4\">\n {template.description && (\n <p className=\"text-sm text-text-secondary mb-3 line-clamp-2\">\n {template.description}\n </p>\n )}\n <div className=\"flex flex-wrap gap-1.5 overflow-hidden\">\n {template.entityTypes.slice(0, 6).map((entity) => (\n <span\n key={entity}\n className=\"inline-flex px-2 py-0.5 rounded text-xs font-medium bg-bg-sunken text-text-secondary max-w-full truncate\"\n >\n {entity}\n </span>\n ))}\n {template.entityTypes.length > 6 && (\n <span className=\"inline-flex px-2 py-0.5 rounded text-xs font-medium bg-bg-sunken text-text-secondary\">\n {tr(\n \"export.templates.card.moreEntities\",\n \"+{count} more\",\n {\n count: template.entityTypes.length - 6,\n },\n )}\n </span>\n )}\n </div>\n </div>\n </div>\n ))}\n </div>\n ) : (\n <IllustratedEmptyState\n illustration={searchQuery ? \"empty-search\" : \"empty-data\"}\n title={\n searchQuery\n ? tr(\n \"export.templates.empty.filtered.title\",\n \"No matching templates\",\n )\n : tr(\"export.templates.empty.title\", \"No templates yet\")\n }\n description={\n searchQuery\n ? tr(\n \"export.templates.empty.filtered.description\",\n \"Try adjusting your search to find what you are looking for.\",\n )\n : tr(\n \"export.templates.empty.description\",\n \"Create a template to save reusable export configurations.\",\n )\n }\n action={\n searchQuery ? (\n <button\n type=\"button\"\n onClick={() => setSearchQuery(\"\")}\n className=\"inline-flex items-center gap-2 px-4 py-2 border-2 border-border-subtle rounded-lg hover:bg-accent transition-colors font-medium\"\n >\n <X size={16} />\n {tr(\n \"export.templates.empty.filtered.actions.clear\",\n \"Clear Search\",\n )}\n </button>\n ) : (\n <button\n type=\"button\"\n onClick={handleOpenCreate}\n className=\"inline-flex items-center gap-2 px-4 py-2 bg-action-primary-bg text-action-primary-text rounded-lg hover:bg-action-primary-bg/90 transition-colors font-medium\"\n >\n <Plus size={16} />\n {tr(\n \"export.templates.empty.actions.newTemplate\",\n \"New Template\",\n )}\n </button>\n )\n }\n />\n )\n ) : null}\n {templatePendingDelete && (\n <div\n role=\"dialog\"\n aria-modal=\"true\"\n aria-labelledby=\"template-list-delete-title\"\n className=\"fixed inset-0 z-50 flex items-center justify-center bg-bg-surface/80 backdrop-blur-sm p-4\"\n >\n <div className=\"w-full max-w-md rounded-xl border-2 border-border-subtle bg-bg-surface p-6 shadow-xl\">\n <h2 id=\"template-list-delete-title\" className=\"text-lg font-bold\">\n {tr(\"export.templates.delete.title\", \"Delete template?\")}\n </h2>\n <p className=\"mt-2 text-sm text-text-secondary\">\n {tr(\n \"export.templates.delete.description\",\n \"This removes the saved export configuration. Existing export jobs are not deleted.\",\n )}\n </p>\n <div className=\"mt-6 flex flex-col-reverse sm:flex-row sm:justify-end gap-3\">\n <button\n type=\"button\"\n onClick={() => setTemplatePendingDelete(null)}\n className=\"px-4 py-2 rounded-lg border-2 border-border-subtle hover:bg-accent transition-colors font-medium\"\n >\n {tr(\"export.templates.delete.cancel\", \"Keep Template\")}\n </button>\n <button\n type=\"button\"\n onClick={confirmDeleteTemplate}\n className=\"px-4 py-2 rounded-lg bg-status-error-bg text-status-error-text hover:bg-status-error-bg/90 transition-colors font-medium\"\n >\n {tr(\"export.templates.delete.confirmAction\", \"Delete Template\")}\n </button>\n </div>\n </div>\n </div>\n )}\n </PageLayout>\n );\n}\n"],"mappings":";;;;;;;;;;;;AAgCA,IAAM,KAAY,GAAc,OACZ,EAAK,SAAS,IAAI,GAAG,EAAK,MAAM,GAAG,GAAG,GAAG,MACzC,EAAK,WAAW,IAAI,GAAG,IAAO,IAAI;AAQtD,SAAgB,IAAsB;CACpC,IAAM,EAAE,cAAW,KAAK,gBAAa,GAAW,EAC1C,IAAK,GAAO,EACZ,EAAE,MAAM,GAAW,iBAAc,GAAoB,EACrD,EAAE,sBAAmB,GAAyB,EAC9C,EAAE,mBAAgB,WAAW,MAAa,GAAyB,EACnE,EAAE,MAAM,MAAsB,GAAsB,EAEpD,CAAC,GAAa,KAAkB,EAAS,GAAG,EAC5C,CAAC,GAAgB,KAAqB,EAAS,GAAM,EACrD,CAAC,GAAiB,KAAsB,EAAS,GAAG,EACpD,CAAC,GAAwB,KAA6B,EAAS,GAAG,EAClE,CAAC,GAAwB,KAA6B,EAE1D,EAAE,CAAC,EACC,CAAC,GAAmB,KACxB,EAAuB,MAAM,EACzB,CAAC,GAAa,KAAkB,EAAwB,KAAK,EAC7D,CAAC,GAAuB,KAA4B,EAExD,KAAK,EAED,UAAyB;AAM7B,EALA,EAAmB,GAAG,EACtB,EAA0B,GAAG,EAC7B,EAA0B,EAAE,CAAC,EAC7B,EAAqB,MAAM,EAC3B,EAAe,KAAK,EACpB,EAAkB,GAAK;IAGnB,IAAuB,YAAY;AACvC,MAAI,CAAC,EAAgB,MAAM,EAAE;AAC3B,KACE,EACE,6CACA,6BACD,CACF;AACD;;AAEF,MAAI,EAAuB,WAAW,GAAG;AACvC,KACE,EACE,+CACA,mCACD,CACF;AACD;;AAEF,IAAe,KAAK;AACpB,MAAI;AAOF,GANA,MAAM,EAAe;IACnB,MAAM,EAAgB,MAAM;IAC5B,aAAa,EAAuB,MAAM,IAAI,KAAA;IAC9C,aAAa;IACb,QAAQ;IACT,CAAC,EACF,EAAkB,GAAM;WACjB,GAAK;AACZ,KACE,aAAe,QACX,EAAI,UACJ,EACE,6CACA,6BACD,CACN;;IAIC,KAAgB,MAAuB;AAC3C,KAA2B,MACzB,EAAK,SAAS,EAAW,GACrB,EAAK,QAAQ,MAAM,MAAM,EAAW,GACpC,CAAC,GAAG,GAAM,EAAW,CAC1B;IAIG,IAAoB,QAAc;AACtC,MAAI,CAAC,EAAa,QAAO;EACzB,IAAM,IAAI,EAAY,aAAa;AACnC,SAAO,EAAU,QACd,MACC,EAAE,KAAK,aAAa,CAAC,SAAS,EAAE,IAChC,EAAE,YAAY,MAAM,MAAM,EAAE,aAAa,CAAC,SAAS,EAAE,CAAC,CACzD;IACA,CAAC,GAAW,EAAY,CAAC,EAEtB,KAAuB,MAAuB;AAClD,MAAW,EAAS,GAAU,aAAa,IAAa,CAAC;IAGrD,KAAuB,OAC3B,GACA,MACG;AAGH,EAFA,EAAE,iBAAiB,EACnB,GAAa,SAAS,EACtB,EAAyB,EAAW;IAGhC,KAAwB,YAAY;AACnC,SACL;OAAI;AAEF,IADA,MAAM,EAAe,EAAsB,EAC3C,EAAa,UAAU;WACjB;AACN,MAAa,QAAQ;;AAEvB,KAAyB,KAAK;;;AAGhC,QACE,kBAAC,GAAD;EACE,OAAO,EAAG,0BAA0B,mBAAmB;EACvD,UAAU,EACR,6BACA,yDACD;EACD,MAAM,kBAAC,GAAD,EAAS,MAAM,IAAM,CAAA;EAC3B,eACE,kBAAC,GAAD;GACE,SAAS;IACP,OAAO,EAAG,wCAAwC,eAAe;IACjE,MAAM,kBAAC,GAAD,EAAM,MAAM,IAAM,CAAA;IACxB,UAAU;IACX;GACD,SAAS,CACP;IACE,OAAO,EAAG,sCAAsC,aAAa;IAC7D,MAAM,kBAAC,GAAD,EAAM,MAAM,IAAM,CAAA;IACxB,gBAAgB,IAAW,EAAS,GAAU,MAAM,CAAC;IACtD,CACF;GACD,CAAA;EAEJ,eACE,kBAAC,QAAD;GAAM,WAAU;aAAhB,CACE,kBAAC,GAAD,EAAU,MAAM,IAAM,CAAA,EACrB,EACC,sCACA,0CACA;IACE,OAAO,EAAkB;IACzB,QAAQ,EAAkB,WAAW,IAAU,KAAN;IAC1C,CACF,CACI;;YAlCX;GAsCE,kBAAC,GAAD;IAAa,MAAM,kBAAC,GAAD,EAAS,MAAM,IAAM,CAAA;cACrC,EACC,4BACA,+QACD;IACW,CAAA;GAGb,KACC,kBAAC,OAAD;IAAK,WAAU;cAAf;KACE,kBAAC,OAAD;MAAK,WAAU;gBAAf,CACE,kBAAC,MAAD;OAAI,WAAU;iBACX,EAAG,+BAA+B,eAAe;OAC/C,CAAA,EACL,kBAAC,UAAD;OACE,MAAK;OACL,eAAe,EAAkB,GAAM;OACvC,WAAU;iBAEV,kBAAC,GAAD,EAAG,MAAM,IAAM,CAAA;OACR,CAAA,CACL;;KACL,KACC,kBAAC,KAAD;MAAG,WAAU;gBACV;MACC,CAAA;KAEN,kBAAC,OAAD;MAAK,WAAU;gBAAf,CACE,kBAAC,OAAD;OAAK,WAAU;iBAAf,CACE,kBAAC,SAAD;QAAO,WAAU;kBAAjB,CACG,EAAG,qCAAqC,OAAO,EAAC,KAC3C;WACR,kBAAC,SAAD;QACE,MAAK;QACL,cAAY,EAAG,qCAAqC,OAAO;QAC3D,aAAa,EACX,gDACA,0BACD;QACD,OAAO;QACP,WAAW,MAAM,EAAmB,EAAE,OAAO,MAAM;QACnD,WAAU;QACV,CAAA,CACE;UACN,kBAAC,OAAD;OAAK,WAAU;iBAAf,CACE,kBAAC,SAAD;QAAO,WAAU;kBAAjB,CACG,EAAG,uCAAuC,SAAS,EAAC,KAC/C;WACR,kBAAC,UAAD;QACE,OAAO;QACP,WAAW,MACT,EAAqB,EAAE,OAAO,MAAsB;QAEtD,WAAU;kBALZ,CAOE,kBAAC,UAAD;SAAQ,OAAM;mBAAO,EAAG,qBAAqB,MAAM;SAAU,CAAA,EAC7D,kBAAC,UAAD;SAAQ,OAAM;mBAAQ,EAAG,sBAAsB,OAAO;SAAU,CAAA,CACzD;UACL;SACF;;KACN,kBAAC,OAAD;MAAK,WAAU;gBAAf,CACE,kBAAC,SAAD;OAAO,WAAU;iBACd,EAAG,4CAA4C,cAAc;OACxD,CAAA,EACR,kBAAC,SAAD;OACE,MAAK;OACL,cAAY,EACV,4CACA,cACD;OACD,aAAa,EACX,uDACA,uBACD;OACD,OAAO;OACP,WAAW,MAAM,EAA0B,EAAE,OAAO,MAAM;OAC1D,WAAU;OACV,CAAA,CACE;;KACN,kBAAC,OAAD;MAAK,WAAU;gBAAf,CACE,kBAAC,SAAD;OAAO,WAAU;iBAAjB;QACG,EAAG,4CAA4C,eAAe;QAAC;QAAG;QACnE,kBAAC,QAAD;SAAM,WAAU;mBACb,EACC,gDACA,wBACD;SACI,CAAA;QACD;UACR,kBAAC,OAAD;OAAK,WAAU;iBACZ,EAAkB,KAAK,MACtB,kBAAC,UAAD;QAEE,MAAK;QACL,eAAe,EAAa,EAAO,WAAW;QAC9C,WAAW,EACT,sEACA,EAAuB,SAAS,EAAO,WAAW,GAC9C,wDACA,6DACL;kBAEA,EAAO;QACD,EAXF,EAAO,WAWL,CACT;OACE,CAAA,CACF;;KACN,kBAAC,OAAD;MAAK,WAAU;gBAAf,CACE,kBAAC,UAAD;OACE,MAAK;OACL,SAAS;OACT,UAAU;OACV,WAAU;iBAJZ,CAMG,IACC,kBAAC,GAAD;QAAS,MAAM;QAAI,WAAU;QAAiB,CAAA,GAE9C,kBAAC,GAAD,EAAM,MAAM,IAAM,CAAA,EAEnB,EAAG,wCAAwC,kBAAkB,CACvD;UACT,kBAAC,UAAD;OACE,MAAK;OACL,eAAe,EAAkB,GAAM;OACvC,WAAU;iBAET,EAAG,wCAAwC,SAAS;OAC9C,CAAA,CACL;;KACF;;GAGR,kBAAC,OAAD;IAAK,WAAU;cAAf;KACE,kBAAC,GAAD;MACE,MAAM;MACN,WAAU;MACV,CAAA;KACF,kBAAC,SAAD;MACE,MAAK;MACL,cAAY,EACV,uCACA,mBACD;MACD,aAAa,EACX,uCACA,sBACD;MACD,OAAO;MACP,WAAW,MAAM,EAAe,EAAE,OAAO,MAAM;MAC/C,WAAU;MACV,CAAA;KACD,KACC,kBAAC,UAAD;MACE,MAAK;MACL,eAAe,EAAe,GAAG;MACjC,WAAU;gBAEV,kBAAC,GAAD,EAAG,MAAM,IAAM,CAAA;MACR,CAAA;KAEP;;GAGL,KACC,kBAAC,OAAD;IAAK,WAAU;cACZ,CAAC,GAAG;;;;;KAAQ,CAAC,CAAC,KAAK,GAAG,MACrB,kBAAC,OAAD;KAAa,WAAU;eAAvB;MACE,kBAAC,OAAD,EAAK,WAAU,yDAA0D,CAAA;MACzE,kBAAC,OAAD,EAAK,WAAU,2DAA4D,CAAA;MAC3E,kBAAC,OAAD;OAAK,WAAU;iBAAf,CACE,kBAAC,OAAD,EAAK,WAAU,yDAA0D,CAAA,EACzE,kBAAC,OAAD,EAAK,WAAU,yDAA0D,CAAA,CACrE;;MACF;OAPI,EAOJ,CACN;IACE,CAAA;GAIN,IAkJE,OAjJF,EAAkB,SAAS,IACzB,kBAAC,OAAD;IAAK,WAAU;cACZ,EAAkB,KAAK,GAAU,MAChC,kBAAC,OAAD;KAEE,MAAK;KACL,UAAU;KACV,eAAe,EAAoB,EAAS,GAAG;KAC/C,YAAY,MAAM;AAChB,OAAI,EAAE,QAAQ,WAAW,EAAE,QAAQ,SACjC,EAAE,gBAAgB,EAClB,EAAoB,EAAS,GAAG;;KAGpC,WAAW,EACT,qGACA,qFACA,iFACA,uBACD;KACD,OAAO,EAAE,gBAAgB,GAAG,IAAQ,GAAG,KAAK;eAjB9C,CAoBE,kBAAC,OAAD;MAAK,WAAU;gBACb,kBAAC,OAAD;OAAK,WAAU;iBAAf,CACE,kBAAC,OAAD;QAAK,WAAU;kBAAf,CACE,kBAAC,OAAD;SAAK,WAAU;mBACb,kBAAC,GAAD;UAAS,MAAM;UAAI,WAAU;UAAiB,CAAA;SAC1C,CAAA,EACN,kBAAC,OAAD,EAAA,UAAA,CACE,kBAAC,MAAD;SAAI,WAAU;mBAAiB,EAAS;SAAU,CAAA,EAClD,kBAAC,KAAD;SAAG,WAAU;mBAAb,CACG,EAAqB,EAAS,OAAO,EACrC,EAAS,aACR,kBAAC,QAAD;UAAM,WAAU;oBACb,EAAG,iCAAiC,UAAU;UAC1C,CAAA,CAEP;WACA,EAAA,CAAA,CACF;WACN,kBAAC,OAAD;QAAK,WAAU;kBAAf,CACE,kBAAC,UAAD;SACE,MAAK;SACL,UAAU,MAAM,GAAqB,GAAG,EAAS,GAAG;SACpD,WAAU;SACV,cAAY,EACV,yCACA,iBACA,EAAE,MAAM,EAAS,MAAM,CACxB;mBAED,kBAAC,GAAD,EAAG,MAAM,IAAM,CAAA;SACR,CAAA,EACT,kBAAC,IAAD;SACE,MAAM;SACN,WAAU;SACV,CAAA,CACE;UACF;;MACF,CAAA,EAGN,kBAAC,OAAD;MAAK,WAAU;gBAAf,CACG,EAAS,eACR,kBAAC,KAAD;OAAG,WAAU;iBACV,EAAS;OACR,CAAA,EAEN,kBAAC,OAAD;OAAK,WAAU;iBAAf,CACG,EAAS,YAAY,MAAM,GAAG,EAAE,CAAC,KAAK,MACrC,kBAAC,QAAD;QAEE,WAAU;kBAET;QACI,EAJA,EAIA,CACP,EACD,EAAS,YAAY,SAAS,KAC7B,kBAAC,QAAD;QAAM,WAAU;kBACb,EACC,sCACA,iBACA,EACE,OAAO,EAAS,YAAY,SAAS,GACtC,CACF;QACI,CAAA,CAEL;SACF;QACF;OAvFC,EAAS,GAuFV,CACN;IACE,CAAA,GAEN,kBAAC,GAAD;IACE,cAAc,IAAc,iBAAiB;IAC7C,OACE,IACI,EACE,yCACA,wBACD,GACD,EAAG,gCAAgC,mBAAmB;IAE5D,aACE,IACI,EACE,+CACA,8DACD,GACD,EACE,sCACA,4DACD;IAEP,QACE,IACE,kBAAC,UAAD;KACE,MAAK;KACL,eAAe,EAAe,GAAG;KACjC,WAAU;eAHZ,CAKE,kBAAC,GAAD,EAAG,MAAM,IAAM,CAAA,EACd,EACC,iDACA,eACD,CACM;SAET,kBAAC,UAAD;KACE,MAAK;KACL,SAAS;KACT,WAAU;eAHZ,CAKE,kBAAC,GAAD,EAAM,MAAM,IAAM,CAAA,EACjB,EACC,8CACA,eACD,CACM;;IAGb,CAAA;GAGL,KACC,kBAAC,OAAD;IACE,MAAK;IACL,cAAW;IACX,mBAAgB;IAChB,WAAU;cAEV,kBAAC,OAAD;KAAK,WAAU;eAAf;MACE,kBAAC,MAAD;OAAI,IAAG;OAA6B,WAAU;iBAC3C,EAAG,iCAAiC,mBAAmB;OACrD,CAAA;MACL,kBAAC,KAAD;OAAG,WAAU;iBACV,EACC,uCACA,qFACD;OACC,CAAA;MACJ,kBAAC,OAAD;OAAK,WAAU;iBAAf,CACE,kBAAC,UAAD;QACE,MAAK;QACL,eAAe,EAAyB,KAAK;QAC7C,WAAU;kBAET,EAAG,kCAAkC,gBAAgB;QAC/C,CAAA,EACT,kBAAC,UAAD;QACE,MAAK;QACL,SAAS;QACT,WAAU;kBAET,EAAG,yCAAyC,kBAAkB;QACxD,CAAA,CACL;;MACF;;IACF,CAAA;GAEG"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@burdenoff/microfe-export",
3
- "version": "2026.1013.1",
3
+ "version": "2026.1013.2",
4
4
  "description": "Burdenoff Export Microfrontend - Data export management interface",
5
5
  "type": "module",
6
6
  "files": [