import Modal from './common/ResizableDraggableModal'; import React, { useEffect, useMemo, useState } from 'react'; import { Form, InputNumber, Select, message } from 'antd'; import { ExportOutlined } from '@ant-design/icons'; import { t } from '../i18n'; export type DataExportFormat = 'csv' | 'xlsx' | 'json' | 'md' | 'html'; export type DataExportScope = 'selected' | 'page' | 'all' | 'filteredAll'; export type DataExportFileOptions = { format: DataExportFormat; xlsxMaxRowsPerSheet?: number; }; export type DataExportDialogValues = DataExportFileOptions & { scope: DataExportScope | string; }; export type DataExportScopeOption = { value: DataExportScope | string; label: string; description?: string; disabled?: boolean; }; export type ShowDataExportDialogOptions = { title: string; scopeOptions: DataExportScopeOption[]; initialValues?: Partial; okText?: string; }; export const MAX_XLSX_ROWS_PER_SHEET = 1048575; export const DEFAULT_XLSX_ROWS_PER_SHEET = MAX_XLSX_ROWS_PER_SHEET; export const DEFAULT_DATA_EXPORT_FORMAT: DataExportFormat = 'xlsx'; export const DATA_EXPORT_FORMAT_OPTIONS: Array<{ value: DataExportFormat; label: string }> = [ { value: 'xlsx', label: 'Excel (XLSX)' }, { value: 'csv', label: 'CSV' }, { value: 'json', label: 'JSON' }, { value: 'md', label: 'Markdown' }, { value: 'html', label: 'HTML' }, ]; const resolveDefaultScope = (scopeOptions: DataExportScopeOption[], initialScope?: string): string => { const matchedInitial = scopeOptions.find((item) => item.value === initialScope && !item.disabled); if (matchedInitial) return String(matchedInitial.value); const firstEnabled = scopeOptions.find((item) => !item.disabled); return String(firstEnabled?.value || scopeOptions[0]?.value || 'all'); }; const normalizeDialogValues = ( scopeOptions: DataExportScopeOption[], initialValues?: Partial, ): DataExportDialogValues => { const format = (initialValues?.format || DEFAULT_DATA_EXPORT_FORMAT) as DataExportFormat; const scope = resolveDefaultScope(scopeOptions, initialValues?.scope ? String(initialValues.scope) : undefined); const xlsxMaxRowsPerSheet = Number(initialValues?.xlsxMaxRowsPerSheet) > 0 ? Math.min(MAX_XLSX_ROWS_PER_SHEET, Math.trunc(Number(initialValues?.xlsxMaxRowsPerSheet))) : DEFAULT_XLSX_ROWS_PER_SHEET; return { format, scope, xlsxMaxRowsPerSheet, }; }; const validateDialogValues = ( values: DataExportDialogValues, scopeOptions: DataExportScopeOption[], ): string | null => { if (!DATA_EXPORT_FORMAT_OPTIONS.some((item) => item.value === values.format)) { return t('data_export.dialog.validation.format_required'); } if (scopeOptions.length > 0) { const matchedScope = scopeOptions.find((item) => String(item.value) === String(values.scope)); if (!matchedScope || matchedScope.disabled) { return t('data_export.dialog.validation.scope_required'); } } if (values.format === 'xlsx') { const rows = Math.trunc(Number(values.xlsxMaxRowsPerSheet) || 0); if (!Number.isFinite(rows) || rows <= 0) { return t('data_export.dialog.validation.xlsx_max_rows_required'); } if (rows > MAX_XLSX_ROWS_PER_SHEET) { return t('data_export.dialog.validation.xlsx_max_rows_limit', { maxRows: MAX_XLSX_ROWS_PER_SHEET.toLocaleString(), }); } } return null; }; const DataExportDialogContent: React.FC<{ scopeOptions: DataExportScopeOption[]; initialValues?: Partial; onChange: (values: DataExportDialogValues) => void; }> = ({ scopeOptions, initialValues, onChange }) => { const [values, setValues] = useState(() => normalizeDialogValues(scopeOptions, initialValues)); useEffect(() => { onChange(values); }, [onChange, values]); const selectedScope = useMemo( () => scopeOptions.find((item) => String(item.value) === String(values.scope)), [scopeOptions, values.scope], ); return (
({ value: item.value, label: item.label, disabled: item.disabled, }))} onChange={(scope) => setValues((prev) => ({ ...prev, scope }))} /> {selectedScope?.description && (
{selectedScope.description}
)} {values.format === 'xlsx' && ( setValues((prev) => ({ ...prev, xlsxMaxRowsPerSheet: Number(nextValue) > 0 ? Math.min(MAX_XLSX_ROWS_PER_SHEET, Math.trunc(Number(nextValue))) : 0, }))} /> )}
); }; export async function showDataExportDialog( modal: ReturnType[0], options: ShowDataExportDialogOptions, ): Promise { const initialValues = normalizeDialogValues(options.scopeOptions, options.initialValues); return new Promise((resolve) => { let resolved = false; let latestValues = initialValues; const finish = (nextValue: DataExportDialogValues | null) => { if (resolved) return; resolved = true; resolve(nextValue); }; modal.confirm({ title: options.title, icon: , width: 520, centered: true, maskClosable: true, okText: options.okText || t('data_export.dialog.action.start'), cancelText: t('common.cancel'), content: ( { latestValues = values; }} /> ), onOk: async () => { const errorMessage = validateDialogValues(latestValues, options.scopeOptions); if (errorMessage) { void message.error(errorMessage); throw new Error(errorMessage); } finish(latestValues); }, onCancel: () => { finish(null); }, }); }); }