@burdenoff/microfe-export 2026.625.1 → 2026.625.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.
- package/dist/export/components/ExportStatusBadge.js +1 -1
- package/dist/export/components/ExportStatusBadge.js.map +1 -1
- package/dist/export/components/PageLayout.js +7 -18
- package/dist/export/components/PageLayout.js.map +1 -1
- package/dist/export/pages/ExportDashboardPage.js +114 -93
- package/dist/export/pages/ExportDashboardPage.js.map +1 -1
- package/dist/export/pages/ExportHistoryPage.js +26 -25
- package/dist/export/pages/ExportHistoryPage.js.map +1 -1
- package/dist/export/pages/ExportTemplatesPage.js +137 -137
- package/dist/export/pages/ExportTemplatesPage.js.map +1 -1
- package/package.json +2 -2
|
@@ -1,35 +1,36 @@
|
|
|
1
1
|
import { useExport as e } from "../providers/ExportProvider.js";
|
|
2
2
|
import { useExportJobs as t } from "../hooks/useExportQueries.js";
|
|
3
3
|
import { cn as n } from "../utils/cn.js";
|
|
4
|
-
import { getAllStatuses as
|
|
4
|
+
import { getAllStatuses as ee, getFormatDisplayName as r, getStatusDisplayName as i } from "../constants/enums.js";
|
|
5
5
|
import { tWithFallback as a } from "../utils/i18n.js";
|
|
6
6
|
import { ExportStatusBadge as o } from "../components/ExportStatusBadge.js";
|
|
7
|
-
import {
|
|
8
|
-
import { useExportStore as
|
|
9
|
-
import { useEffect as
|
|
10
|
-
import { AlertCircle as
|
|
7
|
+
import { PageLayout as s } from "../components/PageLayout.js";
|
|
8
|
+
import { useExportStore as c } from "../store/exportStore.js";
|
|
9
|
+
import { useEffect as l, useMemo as u, useRef as te, useState as d } from "react";
|
|
10
|
+
import { AlertCircle as f, ChevronLeft as p, ChevronRight as m, Clock as h, Download as g, Filter as _, RefreshCw as v, Search as y, X as b } from "lucide-react";
|
|
11
11
|
import { useSearchParams as x } from "react-router-dom";
|
|
12
12
|
import { Fragment as S, jsx as C, jsxs as w } from "react/jsx-runtime";
|
|
13
13
|
import { useI18n as T } from "@burdenoff/fe-libs/shared/providers/shell/I18nProvider";
|
|
14
|
+
import { IllustratedEmptyState as E } from "@burdenoff/fe-libs/ui";
|
|
14
15
|
//#region src/export/pages/ExportHistoryPage.tsx
|
|
15
|
-
var
|
|
16
|
-
function
|
|
17
|
-
let { basePath:
|
|
18
|
-
|
|
16
|
+
var D = (e, t) => (e.endsWith("/") ? e.slice(0, -1) : e) + (t.startsWith("/") ? t : `/${t}`);
|
|
17
|
+
function O() {
|
|
18
|
+
let { basePath: O = "/", navigate: k } = e(), { t: ne } = T(), A = (e, t, n) => a(ne, e, t, n), { jobFilter: j, updateFilter: M, resetFilter: N, pagination: P, setPagination: F } = c(), [I] = x(), L = te(!1);
|
|
19
|
+
l(() => {
|
|
19
20
|
if (L.current) return;
|
|
20
21
|
L.current = !0;
|
|
21
22
|
let e = I.get("status");
|
|
22
23
|
e && M({ status: e });
|
|
23
24
|
}, [I, M]);
|
|
24
|
-
let [R, z] =
|
|
25
|
+
let [R, z] = d(!1), [B, V] = d([null]), [H, U] = d(0), W = B[H] ?? null, { data: G, totalCount: K, pageInfo: q, isLoading: J, error: Y, refetch: X } = t(j, {
|
|
25
26
|
first: P.pageSize,
|
|
26
27
|
after: W ?? void 0
|
|
27
|
-
}), Z =
|
|
28
|
+
}), Z = u(() => {
|
|
28
29
|
if (!j.search) return G;
|
|
29
30
|
let e = j.search.toLowerCase();
|
|
30
31
|
return G.filter((t) => t.name.toLowerCase().includes(e) || t.entityTypes.some((t) => t.toLowerCase().includes(e)));
|
|
31
32
|
}, [G, j.search]), Q = j.search || j.status !== "all" || j.format !== "all", re = (e) => {
|
|
32
|
-
|
|
33
|
+
k?.(D(O, e));
|
|
33
34
|
}, ie = () => {
|
|
34
35
|
!q?.hasNextPage || !q.endCursor || (V([...B.slice(0, H + 1), q.endCursor]), U((e) => e + 1), F({
|
|
35
36
|
page: P.page + 1,
|
|
@@ -45,13 +46,13 @@ function D() {
|
|
|
45
46
|
}, $ = () => {
|
|
46
47
|
N(), V([null]), U(0);
|
|
47
48
|
}, oe = H > 0, se = q?.hasNextPage ?? !1, ce = H * P.pageSize + 1, le = Math.min((H + 1) * P.pageSize, K), ue = (e) => A(`export.shared.status.${e.toLowerCase()}`, i(e));
|
|
48
|
-
return /* @__PURE__ */ w(
|
|
49
|
+
return /* @__PURE__ */ w(s, {
|
|
49
50
|
title: A("export.history.title", "Export History"),
|
|
50
51
|
subtitle: A("export.history.subtitle", "View and manage all your past export jobs"),
|
|
51
52
|
icon: /* @__PURE__ */ C(h, { size: 24 }),
|
|
52
53
|
headerActions: /* @__PURE__ */ w("button", {
|
|
53
54
|
type: "button",
|
|
54
|
-
onClick: () =>
|
|
55
|
+
onClick: () => k?.(D(O, "new")),
|
|
55
56
|
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",
|
|
56
57
|
children: [/* @__PURE__ */ C(g, { size: 16 }), A("export.history.actions.newExport", "New Export")]
|
|
57
58
|
}),
|
|
@@ -109,7 +110,7 @@ function D() {
|
|
|
109
110
|
children: [/* @__PURE__ */ C("option", {
|
|
110
111
|
value: "all",
|
|
111
112
|
children: A("export.history.filters.status.all", "All Statuses")
|
|
112
|
-
}),
|
|
113
|
+
}), ee().map((e) => /* @__PURE__ */ C("option", {
|
|
113
114
|
value: e,
|
|
114
115
|
children: ue(e)
|
|
115
116
|
}, e))]
|
|
@@ -144,7 +145,7 @@ function D() {
|
|
|
144
145
|
}),
|
|
145
146
|
Y && /* @__PURE__ */ w("div", {
|
|
146
147
|
className: "border-2 border-border-default rounded-xl bg-status-error-bg-subtle p-6 flex items-start gap-4",
|
|
147
|
-
children: [/* @__PURE__ */ C(
|
|
148
|
+
children: [/* @__PURE__ */ C(f, {
|
|
148
149
|
size: 24,
|
|
149
150
|
className: "text-status-error-text flex-shrink-0"
|
|
150
151
|
}), /* @__PURE__ */ w("div", { children: [
|
|
@@ -173,13 +174,13 @@ function D() {
|
|
|
173
174
|
,
|
|
174
175
|
,
|
|
175
176
|
]].map((e, t) => /* @__PURE__ */ C("div", {
|
|
176
|
-
className: "border border-border-seam rounded-xl p-4
|
|
177
|
+
className: "border border-border-seam rounded-xl p-4",
|
|
177
178
|
children: /* @__PURE__ */ w("div", {
|
|
178
179
|
className: "flex items-center justify-between",
|
|
179
180
|
children: [/* @__PURE__ */ w("div", {
|
|
180
181
|
className: "space-y-2",
|
|
181
|
-
children: [/* @__PURE__ */ C("div", { className: "h-4 w-48 bg-bg-sunken rounded" }), /* @__PURE__ */ C("div", { className: "h-3 w-32 bg-bg-sunken rounded" })]
|
|
182
|
-
}), /* @__PURE__ */ C("div", { className: "h-5 w-20 bg-bg-sunken rounded-full" })]
|
|
182
|
+
children: [/* @__PURE__ */ C("div", { className: "h-4 w-48 bg-bg-sunken rounded animate-vc-shimmer" }), /* @__PURE__ */ C("div", { className: "h-3 w-32 bg-bg-sunken rounded animate-vc-shimmer" })]
|
|
183
|
+
}), /* @__PURE__ */ C("div", { className: "h-5 w-20 bg-bg-sunken rounded-full animate-vc-shimmer" })]
|
|
183
184
|
})
|
|
184
185
|
}, t))
|
|
185
186
|
}),
|
|
@@ -203,7 +204,7 @@ function D() {
|
|
|
203
204
|
e.entityTypes.join(", "),
|
|
204
205
|
" ·",
|
|
205
206
|
" ",
|
|
206
|
-
|
|
207
|
+
r(e.format),
|
|
207
208
|
e.createdAt && ` · ${new Date(e.createdAt).toLocaleDateString()}`
|
|
208
209
|
]
|
|
209
210
|
})]
|
|
@@ -233,7 +234,7 @@ function D() {
|
|
|
233
234
|
onClick: ae,
|
|
234
235
|
disabled: !oe,
|
|
235
236
|
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",
|
|
236
|
-
children: [/* @__PURE__ */ C(
|
|
237
|
+
children: [/* @__PURE__ */ C(p, { size: 16 }), A("export.history.pagination.prev", "Prev")]
|
|
237
238
|
}),
|
|
238
239
|
/* @__PURE__ */ C("span", {
|
|
239
240
|
className: "text-sm text-text-secondary px-2",
|
|
@@ -248,8 +249,8 @@ function D() {
|
|
|
248
249
|
})
|
|
249
250
|
]
|
|
250
251
|
})]
|
|
251
|
-
})] }) : /* @__PURE__ */ C(
|
|
252
|
-
|
|
252
|
+
})] }) : /* @__PURE__ */ C(E, {
|
|
253
|
+
illustration: Q ? "empty-search" : "empty-data",
|
|
253
254
|
title: Q ? A("export.history.empty.filtered.title", "No matching exports") : A("export.history.empty.title", "No exports yet"),
|
|
254
255
|
description: Q ? A("export.history.empty.filtered.description", "Try adjusting your search or filters.") : A("export.history.empty.description", "Create your first export to see it here."),
|
|
255
256
|
action: Q ? /* @__PURE__ */ w("button", {
|
|
@@ -259,7 +260,7 @@ function D() {
|
|
|
259
260
|
children: [/* @__PURE__ */ C(b, { size: 16 }), A("export.history.empty.filtered.actions.clear", "Clear Filters")]
|
|
260
261
|
}) : /* @__PURE__ */ w("button", {
|
|
261
262
|
type: "button",
|
|
262
|
-
onClick: () =>
|
|
263
|
+
onClick: () => k?.(D(O, "new")),
|
|
263
264
|
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",
|
|
264
265
|
children: [/* @__PURE__ */ C(g, { size: 16 }), A("export.history.empty.actions.createFirst", "Create First Export")]
|
|
265
266
|
})
|
|
@@ -268,6 +269,6 @@ function D() {
|
|
|
268
269
|
});
|
|
269
270
|
}
|
|
270
271
|
//#endregion
|
|
271
|
-
export {
|
|
272
|
+
export { O as ExportHistoryPage };
|
|
272
273
|
|
|
273
274
|
//# sourceMappingURL=ExportHistoryPage.js.map
|
|
@@ -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-dom\";\nimport {\n Search,\n X,\n Clock,\n Download,\n ChevronRight,\n ChevronLeft,\n RefreshCw,\n AlertCircle,\n Filter,\n} from \"lucide-react\";\nimport { useI18n } from \"@burdenoff/fe-libs/shared/providers/shell/I18nProvider\";\nimport { useExport } from \"../providers/ExportProvider\";\nimport { useExportJobs } from \"../hooks/useExportQueries\";\nimport { useExportStore } from \"../store/exportStore\";\nimport { ExportStatusBadge } from \"../components/ExportStatusBadge\";\nimport { PageLayout, EmptyState } from \"../components/PageLayout\";\nimport { cn } from \"../utils/cn\";\nimport {\n getFormatDisplayName,\n getAllStatuses,\n getStatusDisplayName,\n} from \"../constants/enums\";\nimport type { ExportJobStatus, ExportFormat } from \"../types\";\nimport { tWithFallback } from \"../utils/i18n\";\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 { t } = useI18n();\n const tr = (\n key: string,\n fallback: string,\n params?: Record<string, string | number>,\n ) => tWithFallback(t, key, fallback, params);\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 {/* 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\">CSV</option>\n <option value=\"JSON\">JSON</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\n key={i}\n className=\"border border-border-seam rounded-xl p-4 animate-pulse\"\n >\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\" />\n <div className=\"h-3 w-32 bg-bg-sunken rounded\" />\n </div>\n <div className=\"h-5 w-20 bg-bg-sunken rounded-full\" />\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(\", \")} ·{\" \"}\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 <EmptyState\n icon={hasActiveFilters ? <Search size={48} /> : <Clock size={48} />}\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-dom\";\nimport {\n Search,\n X,\n Clock,\n Download,\n ChevronRight,\n ChevronLeft,\n RefreshCw,\n AlertCircle,\n Filter,\n} from \"lucide-react\";\nimport { useI18n } from \"@burdenoff/fe-libs/shared/providers/shell/I18nProvider\";\nimport { IllustratedEmptyState } 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 { cn } from \"../utils/cn\";\nimport {\n getFormatDisplayName,\n getAllStatuses,\n getStatusDisplayName,\n} from \"../constants/enums\";\nimport type { ExportJobStatus, ExportFormat } from \"../types\";\nimport { tWithFallback } from \"../utils/i18n\";\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 { t } = useI18n();\n const tr = (\n key: string,\n fallback: string,\n params?: Record<string, string | number>,\n ) => tWithFallback(t, key, fallback, params);\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 {/* 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\">CSV</option>\n <option value=\"JSON\">JSON</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\n key={i}\n className=\"border border-border-seam rounded-xl p-4\"\n >\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(\", \")} ·{\" \"}\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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"}
|