Commit 276ff0f9 authored by ThinhNC's avatar ThinhNC

Merge fix/export-custom-filename: comprehensive bilingual support for exports modal and toasts

parents af02b4fa 23b5a0d3
Pipeline #53761 failed with stage
in 58 seconds
...@@ -276,7 +276,7 @@ export default function ExportsPage() { ...@@ -276,7 +276,7 @@ export default function ExportsPage() {
) : isError ? ( ) : isError ? (
<div className="p-8 text-center rounded-3xl border border-red-500/20 bg-red-500/5 space-y-3"> <div className="p-8 text-center rounded-3xl border border-red-500/20 bg-red-500/5 space-y-3">
<p className="text-sm text-red-500 font-medium"> <p className="text-sm text-red-500 font-medium">
Đã có lỗi xảy ra khi tải danh sách tệp xuất. {t.exports.loadError}
</p> </p>
<Button <Button
variant="outline" variant="outline"
......
...@@ -52,45 +52,41 @@ export function isJobExportable(job?: { ...@@ -52,45 +52,41 @@ export function isJobExportable(job?: {
); );
} }
const EXPORT_FORMATS: { interface FormatMeta {
type: ExportType; type: ExportType;
label: string; key: "csv" | "json" | "xlsx" | "markdown" | "zip";
desc: string;
icon: React.ElementType; icon: React.ElementType;
badgeColor: string; badgeColor: string;
}[] = [ }
const EXPORT_FORMAT_OPTIONS: FormatMeta[] = [
{ {
type: "CSV", type: "CSV",
label: "CSV (Comma-Separated)", key: "csv",
desc: "Bảng tính chuẩn, phân tích dữ liệu nhanh",
icon: FileText, icon: FileText,
badgeColor: "bg-blue-500/10 text-blue-600 dark:text-blue-400 border-blue-500/20", badgeColor: "bg-blue-500/10 text-blue-600 dark:text-blue-400 border-blue-500/20",
}, },
{ {
type: "JSON", type: "JSON",
label: "JSON (Structured)", key: "json",
desc: "Cấu trúc lồng ghép, tích hợp API & lập trình",
icon: FileJson, icon: FileJson,
badgeColor: "bg-amber-500/10 text-amber-600 dark:text-amber-400 border-amber-500/20", badgeColor: "bg-amber-500/10 text-amber-600 dark:text-amber-400 border-amber-500/20",
}, },
{ {
type: "XLSX", type: "XLSX",
label: "Excel Spreadsheet (XLSX)", key: "xlsx",
desc: "Định dạng bảng Microsoft Excel chuyên nghiệp",
icon: FileSpreadsheet, icon: FileSpreadsheet,
badgeColor: "bg-emerald-500/10 text-emerald-600 dark:text-emerald-400 border-emerald-500/20", badgeColor: "bg-emerald-500/10 text-emerald-600 dark:text-emerald-400 border-emerald-500/20",
}, },
{ {
type: "MARKDOWN", type: "MARKDOWN",
label: "Markdown (.md)", key: "markdown",
desc: "Tài liệu văn bản sạch cho RAG và mô hình LLM",
icon: FileCode, icon: FileCode,
badgeColor: "bg-purple-500/10 text-purple-600 dark:text-purple-400 border-purple-500/20", badgeColor: "bg-purple-500/10 text-purple-600 dark:text-purple-400 border-purple-500/20",
}, },
{ {
type: "ZIP", type: "ZIP",
label: "ZIP Archive", key: "zip",
desc: "Gói nén toàn bộ tài nguyên hình ảnh và tệp",
icon: Archive, icon: Archive,
badgeColor: "bg-slate-500/10 text-slate-600 dark:text-slate-400 border-slate-500/20", badgeColor: "bg-slate-500/10 text-slate-600 dark:text-slate-400 border-slate-500/20",
}, },
...@@ -221,7 +217,7 @@ export function CreateExportModal({ ...@@ -221,7 +217,7 @@ export function CreateExportModal({
</Label> </Label>
{displayJobs.length > 0 && ( {displayJobs.length > 0 && (
<span className="text-[11px] text-muted-foreground font-medium"> <span className="text-[11px] text-muted-foreground font-medium">
({displayJobs.length} tác vụ có thể xuất) ({displayJobs.length} {t.exports.modal.exportableTasksCount})
</span> </span>
)} )}
</div> </div>
...@@ -244,11 +240,11 @@ export function CreateExportModal({ ...@@ -244,11 +240,11 @@ export function CreateExportModal({
{job.startUrl} {job.startUrl}
</p> </p>
<span className="text-[10px] font-semibold px-2 py-0.5 rounded-full bg-emerald-500/15 text-emerald-600 dark:text-emerald-400 shrink-0"> <span className="text-[10px] font-semibold px-2 py-0.5 rounded-full bg-emerald-500/15 text-emerald-600 dark:text-emerald-400 shrink-0">
{job.successPages ?? 0} trang {job.successPages ?? 0} {t.exports.modal.pages}
</span> </span>
</div> </div>
<p className="text-[10px] text-muted-foreground font-mono truncate pt-0.5"> <p className="text-[10px] text-muted-foreground font-mono truncate pt-0.5">
ID: {job.id.slice(0, 16)}...{job.status ? ` • ${job.status === "COMPLETED" ? "Hoàn tất" : job.status}` : ""}{job.createdAt ? ` • ${formatDate(job.createdAt, locale)}` : ""} ID: {job.id.slice(0, 16)}...{job.status ? ` • ${job.status === "COMPLETED" ? t.exports.status.completed : job.status}` : ""}{job.createdAt ? ` • ${formatDate(job.createdAt, locale)}` : ""}
</p> </p>
</div> </div>
{selectedJobId === job.id && ( {selectedJobId === job.id && (
...@@ -260,7 +256,7 @@ export function CreateExportModal({ ...@@ -260,7 +256,7 @@ export function CreateExportModal({
) : ( ) : (
<div className="rounded-2xl border border-amber-500/30 bg-amber-500/10 p-3.5 text-xs text-amber-700 dark:text-amber-400 flex items-center gap-2.5"> <div className="rounded-2xl border border-amber-500/30 bg-amber-500/10 p-3.5 text-xs text-amber-700 dark:text-amber-400 flex items-center gap-2.5">
<AlertCircle className="h-4 w-4 shrink-0 text-amber-500" /> <AlertCircle className="h-4 w-4 shrink-0 text-amber-500" />
<span>Không có tác vụ cào nào có trang thu thập thành công để xuất dữ liệu.</span> <span>{t.exports.modal.emptyExportableJobs}</span>
</div> </div>
)} )}
</div> </div>
...@@ -271,9 +267,10 @@ export function CreateExportModal({ ...@@ -271,9 +267,10 @@ export function CreateExportModal({
{t.exports.modal.selectFormat} {t.exports.modal.selectFormat}
</Label> </Label>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-2"> <div className="grid grid-cols-1 sm:grid-cols-2 gap-2">
{EXPORT_FORMATS.map((fmt) => { {EXPORT_FORMAT_OPTIONS.map((fmt) => {
const Icon = fmt.icon; const Icon = fmt.icon;
const isSelected = selectedFormat === fmt.type; const isSelected = selectedFormat === fmt.type;
const formatTrans = t.exports.modal.formats[fmt.key];
return ( return (
<button <button
key={fmt.type} key={fmt.type}
...@@ -291,12 +288,12 @@ export function CreateExportModal({ ...@@ -291,12 +288,12 @@ export function CreateExportModal({
<div className="min-w-0 flex-1"> <div className="min-w-0 flex-1">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<span className="text-xs font-bold text-foreground"> <span className="text-xs font-bold text-foreground">
{fmt.type} {formatTrans?.label || fmt.type}
</span> </span>
{isSelected && <Check className="h-3.5 w-3.5 text-emerald-500" />} {isSelected && <Check className="h-3.5 w-3.5 text-emerald-500" />}
</div> </div>
<p className="text-[10px] text-muted-foreground line-clamp-1 mt-0.5"> <p className="text-[10px] text-muted-foreground line-clamp-1 mt-0.5">
{fmt.desc} {formatTrans?.desc}
</p> </p>
</div> </div>
</button> </button>
...@@ -308,12 +305,12 @@ export function CreateExportModal({ ...@@ -308,12 +305,12 @@ export function CreateExportModal({
{/* Custom File Name (Optional) */} {/* Custom File Name (Optional) */}
<div className="space-y-1 pt-1"> <div className="space-y-1 pt-1">
<Label className="text-[11px] text-muted-foreground"> <Label className="text-[11px] text-muted-foreground">
Tên tệp tùy chỉnh (Tùy chọn) {t.exports.modal.customFileName}
</Label> </Label>
<Input <Input
value={customFileName} value={customFileName}
onChange={(e) => setCustomFileName(e.target.value)} onChange={(e) => setCustomFileName(e.target.value)}
placeholder="crawl_dataset_custom" placeholder={t.exports.modal.customFileNamePlaceholder}
className="rounded-2xl text-xs" className="rounded-2xl text-xs"
/> />
</div> </div>
......
...@@ -5,6 +5,8 @@ import { crawlExportService } from "@/services/crawl-export.service"; ...@@ -5,6 +5,8 @@ import { crawlExportService } from "@/services/crawl-export.service";
import { CrawlExportQueryDto, ExportType } from "@/types/export"; import { CrawlExportQueryDto, ExportType } from "@/types/export";
import { toast } from "sonner"; import { toast } from "sonner";
import { useLanguage } from "@/providers/language-provider";
export const CRAWL_EXPORTS_KEYS = { export const CRAWL_EXPORTS_KEYS = {
all: ["crawl-exports"] as const, all: ["crawl-exports"] as const,
lists: () => [...CRAWL_EXPORTS_KEYS.all, "list"] as const, lists: () => [...CRAWL_EXPORTS_KEYS.all, "list"] as const,
...@@ -21,44 +23,48 @@ export function useCrawlExportsList(params?: CrawlExportQueryDto) { ...@@ -21,44 +23,48 @@ export function useCrawlExportsList(params?: CrawlExportQueryDto) {
export function useCreateCrawlExport() { export function useCreateCrawlExport() {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const { t } = useLanguage();
return useMutation({ return useMutation({
mutationFn: ({ jobId, exportType, fileName }: { jobId: string; exportType: ExportType; fileName?: string }) => mutationFn: ({ jobId, exportType, fileName }: { jobId: string; exportType: ExportType; fileName?: string }) =>
crawlExportService.createExport(jobId, { exportType, fileName }), crawlExportService.createExport(jobId, { exportType, fileName }),
onSuccess: () => { onSuccess: () => {
toast.success("Khởi tạo yêu cầu xuất dữ liệu thành công!"); toast.success(t.exports.toasts.createSuccess);
queryClient.invalidateQueries({ queryKey: CRAWL_EXPORTS_KEYS.all }); queryClient.invalidateQueries({ queryKey: CRAWL_EXPORTS_KEYS.all });
}, },
onError: (error: Error) => { onError: (error: Error) => {
toast.error(`Không thể tạo yêu cầu xuất: ${error.message}`); toast.error(`${t.exports.toasts.createError} ${error.message}`);
}, },
}); });
} }
export function useDeleteCrawlExport() { export function useDeleteCrawlExport() {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const { t } = useLanguage();
return useMutation({ return useMutation({
mutationFn: (id: string) => crawlExportService.deleteExport(id), mutationFn: (id: string) => crawlExportService.deleteExport(id),
onSuccess: () => { onSuccess: () => {
toast.success("Đã xóa tệp dữ liệu đã xuất!"); toast.success(t.exports.toasts.deleteSuccess);
queryClient.invalidateQueries({ queryKey: CRAWL_EXPORTS_KEYS.all }); queryClient.invalidateQueries({ queryKey: CRAWL_EXPORTS_KEYS.all });
}, },
onError: (error: Error) => { onError: (error: Error) => {
toast.error(`Không thể xóa tệp: ${error.message}`); toast.error(`${t.exports.toasts.deleteError} ${error.message}`);
}, },
}); });
} }
export function useDownloadExport() { export function useDownloadExport() {
const { t } = useLanguage();
return useMutation({ return useMutation({
mutationFn: ({ exportId, fileName }: { exportId: string; fileName?: string }) => mutationFn: ({ exportId, fileName }: { exportId: string; fileName?: string }) =>
crawlExportService.downloadExport(exportId, fileName), crawlExportService.downloadExport(exportId, fileName),
onSuccess: () => { onSuccess: () => {
toast.success("Đang tải tệp về máy tính của bạn..."); toast.success(t.exports.toasts.downloadSuccess);
}, },
onError: (error: Error) => { onError: (error: Error) => {
toast.error(`Tải tệp thất bại: ${error.message}`); toast.error(`${t.exports.toasts.downloadError} ${error.message}`);
}, },
}); });
} }
...@@ -622,6 +622,7 @@ export const translations = { ...@@ -622,6 +622,7 @@ export const translations = {
emptyTitle: "Chưa có tệp xuất nào", emptyTitle: "Chưa có tệp xuất nào",
emptyDesc: "Khởi tạo yêu cầu xuất dữ liệu từ các Crawl Job đã hoàn tất để tải về máy tính.", emptyDesc: "Khởi tạo yêu cầu xuất dữ liệu từ các Crawl Job đã hoàn tất để tải về máy tính.",
retry: "Tải lại", retry: "Tải lại",
loadError: "Đã có lỗi xảy ra khi tải danh sách tệp xuất.",
stats: { stats: {
total: "Tổng Tệp Xuất", total: "Tổng Tệp Xuất",
totalDesc: "Tệp dữ liệu đã sinh", totalDesc: "Tệp dữ liệu đã sinh",
...@@ -656,11 +657,46 @@ export const translations = { ...@@ -656,11 +657,46 @@ export const translations = {
desc: "Chọn tác vụ cào đã hoàn tất và định dạng tệp mong muốn.", desc: "Chọn tác vụ cào đã hoàn tất và định dạng tệp mong muốn.",
selectJob: "Chọn Tác Vụ Cào", selectJob: "Chọn Tác Vụ Cào",
selectJobPlaceholder: "Chọn từ danh sách Job đã hoàn thành...", selectJobPlaceholder: "Chọn từ danh sách Job đã hoàn thành...",
exportableTasksCount: "tác vụ có thể xuất",
pages: "trang",
emptyExportableJobs: "Không có tác vụ cào nào có trang thu thập thành công để xuất dữ liệu.",
selectFormat: "Định dạng tệp đầu ra", selectFormat: "Định dạng tệp đầu ra",
formats: {
csv: {
label: "CSV (Comma-Separated)",
desc: "Bảng tính chuẩn, phân tích dữ liệu nhanh",
},
json: {
label: "JSON (Structured)",
desc: "Cấu trúc lồng ghép, tích hợp API & lập trình",
},
xlsx: {
label: "Excel Spreadsheet (XLSX)",
desc: "Định dạng bảng Microsoft Excel chuyên nghiệp",
},
markdown: {
label: "Markdown (.md)",
desc: "Tài liệu văn bản sạch cho RAG và mô hình LLM",
},
zip: {
label: "ZIP Archive",
desc: "Gói nén toàn bộ tài nguyên hình ảnh và tệp",
},
},
customFileName: "Tên tệp tùy chỉnh (Tùy chọn)",
customFileNamePlaceholder: "vd: crawl_dataset_custom",
cancel: "Hủy bỏ", cancel: "Hủy bỏ",
submit: "Bắt Đầu Xuất File", submit: "Bắt Đầu Xuất File",
submitting: "Đang gửi yêu cầu...", submitting: "Đang gửi yêu cầu...",
}, },
toasts: {
createSuccess: "Khởi tạo yêu cầu xuất dữ liệu thành công!",
createError: "Không thể tạo yêu cầu xuất:",
deleteSuccess: "Đã xóa tệp dữ liệu đã xuất!",
deleteError: "Không thể xóa tệp:",
downloadSuccess: "Đang tải tệp về máy tính của bạn...",
downloadError: "Không thể tải tệp:",
},
}, },
// Developer Settings Module // Developer Settings Module
...@@ -2089,6 +2125,7 @@ export const translations = { ...@@ -2089,6 +2125,7 @@ export const translations = {
emptyTitle: "No export files found", emptyTitle: "No export files found",
emptyDesc: "Generate export files from completed crawl jobs to download them directly.", emptyDesc: "Generate export files from completed crawl jobs to download them directly.",
retry: "Reload", retry: "Reload",
loadError: "An error occurred while loading export files.",
stats: { stats: {
total: "Total Exports", total: "Total Exports",
totalDesc: "Generated data files", totalDesc: "Generated data files",
...@@ -2123,11 +2160,46 @@ export const translations = { ...@@ -2123,11 +2160,46 @@ export const translations = {
desc: "Select a completed crawl job and desired output format.", desc: "Select a completed crawl job and desired output format.",
selectJob: "Select Crawl Job", selectJob: "Select Crawl Job",
selectJobPlaceholder: "Choose from completed crawl jobs...", selectJobPlaceholder: "Choose from completed crawl jobs...",
exportableTasksCount: "exportable tasks",
pages: "pages",
emptyExportableJobs: "No crawl jobs with successfully collected pages available for export.",
selectFormat: "Export Format", selectFormat: "Export Format",
formats: {
csv: {
label: "CSV (Comma-Separated)",
desc: "Standard spreadsheet, fast data analysis",
},
json: {
label: "JSON (Structured)",
desc: "Structured nested data for APIs & developers",
},
xlsx: {
label: "Excel Spreadsheet (XLSX)",
desc: "Professional Microsoft Excel workbook format",
},
markdown: {
label: "Markdown (.md)",
desc: "Clean text document for RAG & LLM pipelines",
},
zip: {
label: "ZIP Archive",
desc: "Compressed archive of full datasets and media",
},
},
customFileName: "Custom File Name (Optional)",
customFileNamePlaceholder: "e.g. crawl_dataset_custom",
cancel: "Cancel", cancel: "Cancel",
submit: "Start Export", submit: "Start Export",
submitting: "Submitting...", submitting: "Submitting...",
}, },
toasts: {
createSuccess: "Export request initiated successfully!",
createError: "Failed to create export request:",
deleteSuccess: "Export file deleted successfully!",
deleteError: "Failed to delete export file:",
downloadSuccess: "Downloading file to your computer...",
downloadError: "Failed to download file:",
},
}, },
// Developer Settings Module // Developer Settings Module
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment