From cdbc6dff4a7028f6396e3af05d80d138cdcbb015 Mon Sep 17 00:00:00 2001 From: Felipe Date: Tue, 27 Jan 2026 08:49:30 -0300 Subject: [PATCH 1/3] feat: add rename column functionality for datasets --- DashAI/back/api/api_v1/endpoints/datasets.py | 144 +++++++++++++- .../api/api_v1/schemas/datasets_params.py | 5 + DashAI/front/src/api/datasets.ts | 17 ++ .../src/components/DatasetVisualization.jsx | 8 +- .../notebooks/dataset/DatasetTable.jsx | 103 +++++++--- .../dataset/EditableColumnHeader.jsx | 176 ++++++++++++++++++ .../notebooks/dataset/tabs/OverviewTab.jsx | 2 + .../datasetCreation/PreviewDatasetTable.jsx | 19 +- .../src/utils/i18n/locales/en/common.json | 6 +- .../src/utils/i18n/locales/es/common.json | 6 +- 10 files changed, 432 insertions(+), 54 deletions(-) create mode 100644 DashAI/front/src/components/notebooks/dataset/EditableColumnHeader.jsx diff --git a/DashAI/back/api/api_v1/endpoints/datasets.py b/DashAI/back/api/api_v1/endpoints/datasets.py index bd3494342..ef5d151c3 100644 --- a/DashAI/back/api/api_v1/endpoints/datasets.py +++ b/DashAI/back/api/api_v1/endpoints/datasets.py @@ -6,6 +6,7 @@ import shutil import tempfile import zipfile +from datetime import datetime, timezone from typing import Any, Dict import numpy as np @@ -30,7 +31,11 @@ from DashAI.back.dependencies.database.models import Dataset, ModelSession from DashAI.back.types.inf.type_inference import infer_types from DashAI.back.types.type_validation import validate_multiple_type_changes -from DashAI.back.types.utils import arrow_to_dashai_schema +from DashAI.back.types.utils import ( + arrow_to_dashai_schema, + get_types_from_arrow_metadata, + save_types_in_arrow_metadata, +) logger = logging.getLogger(__name__) router = APIRouter() @@ -841,6 +846,143 @@ async def update_dataset( ) from e +@router.patch("/{dataset_id}/columns/rename") +@inject +async def rename_dataset_column( + dataset_id: int, + params: schemas.DatasetRenameColumnParams, + session_factory: sessionmaker = Depends(lambda: di["session_factory"]), +): + """Rename a column in a dataset. + + Parameters + ---------- + dataset_id : int + ID of the dataset to update. + params : DatasetRenameColumnParams + Parameters containing old_name and new_name for the column. + session_factory : Callable[..., ContextManager[Session]] + A factory that creates a context manager that handles a SQLAlchemy session. + + Returns + ------- + Dict + A dictionary with a success message and updated column types. + """ + with session_factory() as db: + dataset = db.get(Dataset, dataset_id) + if dataset is None: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, detail="Dataset not found" + ) + + if dataset.status != DatasetStatus.FINISHED: + raise HTTPException( + status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, + detail="Dataset is not in finished state", + ) + + old_name = params.old_name.strip() + new_name = params.new_name.strip() + if not old_name or not new_name: + raise HTTPException( + status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, + detail="Column names cannot be empty", + ) + if old_name == new_name: + raise HTTPException( + status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, + detail="New column name must be different from old name", + ) + + dataset_path = f"{dataset.file_path}/dataset" + arrow_file_path = f"{dataset_path}/data.arrow" + try: + with pa.OSFile(arrow_file_path, "rb") as source: + reader = pa.ipc.open_file(source) + table = reader.read_all() + if old_name not in table.schema.names: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=f"Column '{old_name}' not found in dataset", + ) + if new_name in table.schema.names and new_name != old_name: + raise HTTPException( + status_code=status.HTTP_409_CONFLICT, + detail=f"Column '{new_name}' already exists", + ) + + column_index = table.schema.get_field_index(old_name) + types_dict = get_types_from_arrow_metadata(table.schema) + new_fields = [] + for i, field in enumerate(table.schema): + if i == column_index: + new_fields.append(pa.field(new_name, field.type, field.nullable)) + else: + new_fields.append(field) + + new_schema = pa.schema(new_fields) + + renamed_table = pa.table( + { + new_name if name == old_name else name: table[name] + for name in table.schema.names + }, + schema=new_schema, + ) + if old_name in types_dict: + types_dict[new_name] = types_dict.pop(old_name) + + types_serialized = {col: types_dict[col].to_string() for col in types_dict} + renamed_table = save_types_in_arrow_metadata( + renamed_table, types_serialized + ) + + with pa.OSFile(arrow_file_path, "wb") as sink: + writer = ipc.new_file(sink, renamed_table.schema) + writer.write_table(renamed_table) + writer.close() + + splits_path = f"{dataset_path}/splits.json" + if os.path.exists(splits_path): + with open(splits_path, "r", encoding="utf-8") as f: + splits_data = json.load(f) + if "column_names" in splits_data: + splits_data["column_names"] = [ + new_name if name == old_name else name + for name in splits_data["column_names"] + ] + if "nan" in splits_data and old_name in splits_data["nan"]: + splits_data["nan"][new_name] = splits_data["nan"].pop(old_name) + with open(splits_path, "w", encoding="utf-8") as f: + json.dump( + splits_data, + f, + indent=2, + sort_keys=True, + ensure_ascii=False, + ) + + dataset.last_modified = datetime.now(timezone.utc) + db.commit() + db.refresh(dataset) + updated_columns = get_columns_spec(dataset_path) + return { + "message": f"Column '{old_name}' renamed to '{new_name}' successfully", + "old_name": old_name, + "new_name": new_name, + "columns": updated_columns, + } + except HTTPException: + raise + except Exception as e: + logger.exception(e) + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"Error renaming column: {str(e)}", + ) from e + + @router.get("/file/") async def get_dataset_file( path: str, diff --git a/DashAI/back/api/api_v1/schemas/datasets_params.py b/DashAI/back/api/api_v1/schemas/datasets_params.py index b25fe2ce3..f0071203b 100644 --- a/DashAI/back/api/api_v1/schemas/datasets_params.py +++ b/DashAI/back/api/api_v1/schemas/datasets_params.py @@ -26,6 +26,11 @@ class DatasetUpdateParams(BaseModel): name: str = None +class DatasetRenameColumnParams(BaseModel): + old_name: str + new_name: str + + class DatasetUploadFromNotebookParams(BaseModel): name: str diff --git a/DashAI/front/src/api/datasets.ts b/DashAI/front/src/api/datasets.ts index 813d645ee..1e1a2ac34 100644 --- a/DashAI/front/src/api/datasets.ts +++ b/DashAI/front/src/api/datasets.ts @@ -79,6 +79,23 @@ export const updateDataset = async ( return response.data; }; +export const renameDatasetColumn = async ( + id: number, + oldName: string, + newName: string, +): Promise<{ + message: string; + old_name: string; + new_name: string; + columns: object; +}> => { + const response = await api.patch(`${datasetEndpoint}/${id}/columns/rename`, { + old_name: oldName, + new_name: newName, + }); + return response.data; +}; + export const deleteDataset = async (id: string): Promise => { const response = await api.delete(`${datasetEndpoint}/${id}`); return response.data; diff --git a/DashAI/front/src/components/DatasetVisualization.jsx b/DashAI/front/src/components/DatasetVisualization.jsx index ee84890c9..40480ce87 100644 --- a/DashAI/front/src/components/DatasetVisualization.jsx +++ b/DashAI/front/src/components/DatasetVisualization.jsx @@ -22,12 +22,8 @@ import { getDatasetInfo, getDatasetFileFiltered, } from "../api/datasets"; -import DatasetTable from "./notebooks/dataset/DatasetTable"; -import { getComponents } from "../api/component"; import { useTourContext } from "./tour/TourProvider"; -import { useSnackbar } from "notistack"; import JobQueueWidget from "./jobs/JobQueueWidget"; -import { getDatasetStatus } from "../utils/datasetStatus"; import { formatDate } from "../pages/results/constants/formatDate"; import Header from "./notebooks/dataset/header/Header"; import Tooltip from "@mui/material/Tooltip"; @@ -92,12 +88,10 @@ export default function DatasetVisualization({ fetchDatasetInfo(); }, [dataset.id, dataset.status]); - // fetchPage compatible with server-side filtering const fetchDatasetPage = useCallback( async (page, pageSize, filterModel) => { if (isProcessing) return { rows: [], total: 0 }; try { - // Use getDatasetFile if no filters, else use getDatasetFileFiltered const hasFilters = filterModel && Array.isArray(filterModel.items) && @@ -122,7 +116,7 @@ export default function DatasetVisualization({ ); const status = dataset.status; - const isProcessing = !(status === 3 || status === 4); // Finished or Error + const isProcessing = !(status === 3 || status === 4); return ( <> diff --git a/DashAI/front/src/components/notebooks/dataset/DatasetTable.jsx b/DashAI/front/src/components/notebooks/dataset/DatasetTable.jsx index 04c596361..8d9912aec 100644 --- a/DashAI/front/src/components/notebooks/dataset/DatasetTable.jsx +++ b/DashAI/front/src/components/notebooks/dataset/DatasetTable.jsx @@ -13,8 +13,10 @@ import { LinearProgress } from "@mui/material"; import { exportDatasetCsvByPath, getDatasetTypesByFilePath, + renameDatasetColumn, } from "../../../api/datasets"; import { useTranslation } from "react-i18next"; +import EditableColumnHeader from "./EditableColumnHeader"; /** * Props: @@ -25,6 +27,8 @@ import { useTranslation } from "react-i18next"; * - autoHeight?: boolean (default true) * - pageSizeOptions?: number[] (default [5, 10, 25]) * - datasetPath?: string (optional) - Path to dataset for CSV export + * - datasetId?: number (optional) - Dataset ID for column renaming + * - editableColumns?: boolean (default false) - Enable column name editing */ export default function DatasetTable({ fetchPage, @@ -34,6 +38,8 @@ export default function DatasetTable({ autoHeight = true, pageSizeOptions = [5, 10, 25], datasetPath, + datasetId, + editableColumns = false, density = "compact", ...props }) { @@ -77,7 +83,6 @@ export default function DatasetTable({ })); setRows(withIds); - // Siempre usa el total devuelto por el backend para la paginación setRowCount(data?.total ?? withIds.length); } catch (e) { setRows([]); @@ -91,10 +96,8 @@ export default function DatasetTable({ alive = false; }; }, [fetchPage, paginationModel, filterModel, ...deps]); - // Handler for DataGrid filter changes const handleFilterModelChange = useCallback((model) => { setFilterModel((prev) => { - // Si el filtro es igual al anterior, igual resetea la paginación setPaginationModel((m) => ({ ...m, page: 0 })); if (!model || !model.items || model.items.length === 0) { return { items: [] }; @@ -107,6 +110,37 @@ export default function DatasetTable({ setPaginationModel((m) => ({ ...m, page: 0 })); }, deps); + const handleColumnRename = useCallback( + async (oldName, newName) => { + if (!datasetId) { + throw new Error("Dataset ID is required for renaming columns"); + } + + const result = await renameDatasetColumn(datasetId, oldName, newName); + + setColumnTypes((prevTypes) => { + const newTypes = { ...prevTypes }; + if (newTypes[oldName]) { + newTypes[newName] = newTypes[oldName]; + delete newTypes[oldName]; + } + return newTypes; + }); + + const { page, pageSize } = paginationModel; + const data = await fetchPage(page, pageSize, filterModel); + const withIds = (data?.rows ?? []).map((r, i) => ({ + id: page * pageSize + i, + ...r, + })); + setRows(withIds); + setRowCount(data?.total ?? withIds.length); + + return result; + }, + [datasetId, paginationModel, filterModel, fetchPage], + ); + const columns = useMemo(() => { if (columnsProp?.length) return columnsProp; const first = rows[0]; @@ -135,30 +169,47 @@ export default function DatasetTable({ : "string", minWidth: 120, width: Math.max(120, field.length * 8 + 40), - renderHeader: () => ( -
- - {field} - - + editableColumns && datasetId ? ( + + ) : ( +
- {columnTypes[field]?.type || t("common:unknown")} - -
- ), + + {field} + + + {columnTypes[field]?.type || t("common:unknown")} + +
+ ), })); - }, [rows, columnsProp, columnTypes]); + }, [ + rows, + columnsProp, + columnTypes, + editableColumns, + datasetId, + handleColumnRename, + t, + ]); // Custom CSV Export Button function CsvExportButton() { @@ -291,7 +342,7 @@ export default function DatasetTable({ toolbar: CustomToolbar, loadingOverlay: LinearProgress, }} - columnHeaderHeight={85} + columnHeaderHeight={editableColumns ? 95 : 85} {...props} /> ); diff --git a/DashAI/front/src/components/notebooks/dataset/EditableColumnHeader.jsx b/DashAI/front/src/components/notebooks/dataset/EditableColumnHeader.jsx new file mode 100644 index 000000000..eae0bc67b --- /dev/null +++ b/DashAI/front/src/components/notebooks/dataset/EditableColumnHeader.jsx @@ -0,0 +1,176 @@ +import { useState, useRef, useEffect } from "react"; +import { Typography, TextField, Box, Tooltip } from "@mui/material"; +import { Edit, Check, Close } from "@mui/icons-material"; +import { useTranslation } from "react-i18next"; + +/** + * Editable column header component that allows users to rename dataset columns. + * + * @param {string} columnName - Current name of the column + * @param {string} columnType - Type of the column (e.g., "Integer", "Text") + * @param {Function} onRename - Callback function when rename is confirmed (oldName, newName) => Promise + * @param {boolean} disabled - Whether editing is disabled + */ +export default function EditableColumnHeader({ + columnName, + columnType, + onRename, + disabled = false, +}) { + const [isEditing, setIsEditing] = useState(false); + const [editValue, setEditValue] = useState(columnName); + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(""); + const inputRef = useRef(null); + const { t } = useTranslation(["common"]); + + useEffect(() => { + if (isEditing && inputRef.current) { + inputRef.current.focus(); + inputRef.current.select(); + } + }, [isEditing]); + + const handleEditClick = () => { + if (disabled) return; + setEditValue(columnName); + setError(""); + setIsEditing(true); + }; + + const handleCancel = () => { + setEditValue(columnName); + setError(""); + setIsEditing(false); + }; + + const handleConfirm = async () => { + const newName = editValue.trim(); + if (!newName) { + setError(t("common:columnNameEmpty")); + return; + } + if (newName === columnName) { + setIsEditing(false); + return; + } + if (!/^[a-zA-Z0-9_]+$/.test(newName)) { + setError(t("common:columnNameInvalid")); + return; + } + + setIsLoading(true); + setError(""); + + try { + await onRename(columnName, newName); + setIsEditing(false); + } catch (err) { + setError( + err.response?.data?.detail || + err.message || + t("common:errorRenamingColumn"), + ); + } finally { + setIsLoading(false); + } + }; + + const handleKeyDown = (e) => { + if (e.key === "Enter") { + handleConfirm(); + } else if (e.key === "Escape") { + handleCancel(); + } + }; + + if (isEditing) { + return ( + + setEditValue(e.target.value)} + onKeyDown={handleKeyDown} + onBlur={handleCancel} + size="small" + error={!!error} + disabled={isLoading} + sx={{ + width: "100%", + "& .MuiInputBase-input": { + fontSize: "0.875rem", + paddingY: 0.5, + textAlign: "center", + }, + }} + /> + {error && ( + + {error} + + )} + + {isLoading ? t("common:loading") : columnType || t("common:unknown")} + + + ); + } + + return ( + + + + {columnName} + + + + {columnType || t("common:unknown")} + + + ); +} diff --git a/DashAI/front/src/components/notebooks/dataset/tabs/OverviewTab.jsx b/DashAI/front/src/components/notebooks/dataset/tabs/OverviewTab.jsx index f98b8d4e0..aea73ee56 100644 --- a/DashAI/front/src/components/notebooks/dataset/tabs/OverviewTab.jsx +++ b/DashAI/front/src/components/notebooks/dataset/tabs/OverviewTab.jsx @@ -59,6 +59,8 @@ const OverviewTab = ({ deps={[dataset.file_path]} initialPageSize={10} datasetPath={dataset.file_path} + datasetId={dataset.id} + editableColumns={true} /> diff --git a/DashAI/front/src/components/notebooks/datasetCreation/PreviewDatasetTable.jsx b/DashAI/front/src/components/notebooks/datasetCreation/PreviewDatasetTable.jsx index bcf0ff32b..c114d9834 100644 --- a/DashAI/front/src/components/notebooks/datasetCreation/PreviewDatasetTable.jsx +++ b/DashAI/front/src/components/notebooks/datasetCreation/PreviewDatasetTable.jsx @@ -3,7 +3,6 @@ import { Typography, Select, MenuItem, Box } from "@mui/material"; import { DataGrid } from "@mui/x-data-grid"; import { TypeChangeValidator } from "./TypeChangeValidator"; -// Mapeo de tipos a dtypes por defecto const TYPE_TO_DEFAULT_DTYPE = { Integer: "int64", Float: "float64", @@ -38,32 +37,26 @@ export default function PreviewDatasetTable({ const [showValidator, setShowValidator] = useState(false); const [pendingChanges, setPendingChanges] = useState({}); - // Handler cuando el usuario selecciona un nuevo tipo en el dropdown const handleTypeChangeRequest = (columnName, newType) => { const currentType = columnTypes[columnName]?.type; - // Si no cambió el tipo, no hacer nada if (currentType === newType) { return; } - // Obtener el dtype correcto para el nuevo tipo const newDtype = TYPE_TO_DEFAULT_DTYPE[newType] || "string"; - // Preparar el cambio pendiente con el dtype correcto setPendingChanges({ [columnName]: { current_type: currentType, new_type: newType, - new_dtype: newDtype, // Ya normalizado aquí + new_dtype: newDtype, }, }); - // Mostrar el validador setShowValidator(true); }; - // Handler cuando el usuario confirma los cambios después de la validación const handleConfirmChanges = (changes) => { if (onTypeChange) { onTypeChange(changes); @@ -72,13 +65,11 @@ export default function PreviewDatasetTable({ setPendingChanges({}); }; - // Handler cuando el usuario cancela const handleCancelChanges = () => { setShowValidator(false); setPendingChanges({}); }; - // Crear las columnas del DataGrid const columns = useMemo(() => { if (!rows || rows.length === 0) return []; @@ -91,7 +82,6 @@ export default function PreviewDatasetTable({ headerName: field, minWidth: 150, flex: 1, - // Custom header con el selector de tipo renderHeader: () => ( - {/* Nombre de la columna */} - {/* Selector de tipo */} ), @@ -170,7 +154,6 @@ export default function PreviewDatasetTable({ }} /> - {/* Diálogo de validación */} Date: Sat, 31 Jan 2026 21:01:00 -0300 Subject: [PATCH 2/3] feat: implement column renaming functionality across dataset components --- DashAI/back/job/dataset_job.py | 14 +- .../src/components/datasets/DatasetModal.jsx | 41 ++++++ .../ConfigureAndUploadDatasetStep.jsx | 17 ++- .../datasetCreation/PreviewDataset.jsx | 13 ++ .../datasetCreation/PreviewDatasetTable.jsx | 122 ++++++++++++++++-- .../notebooks/datasetCreation/Upload.jsx | 3 + 6 files changed, 195 insertions(+), 15 deletions(-) diff --git a/DashAI/back/job/dataset_job.py b/DashAI/back/job/dataset_job.py index ed35cfafd..18735df38 100644 --- a/DashAI/back/job/dataset_job.py +++ b/DashAI/back/job/dataset_job.py @@ -175,10 +175,20 @@ def run( else: schema = infer_types(new_dataset.to_pandas(), method="DashAIPtype") - # Cast dataset to inferred types + if "column_renames" in params: + renames = params["column_renames"] + new_names = [ + renames.get(col, col) + for col in new_dataset.arrow_table.schema.names + ] + arrow_table = new_dataset.arrow_table.rename_columns(new_names) + new_dataset = new_dataset.__class__( + arrow_table, splits=new_dataset.splits, types=new_dataset.types + ) + schema = {renames.get(col, col): schema[col] for col in schema} + new_dataset = transform_dataset_with_schema(new_dataset, schema) - # Calculate metadata new_dataset.compute_metadata() gc.collect() diff --git a/DashAI/front/src/components/datasets/DatasetModal.jsx b/DashAI/front/src/components/datasets/DatasetModal.jsx index 06f931a1a..50478c287 100644 --- a/DashAI/front/src/components/datasets/DatasetModal.jsx +++ b/DashAI/front/src/components/datasets/DatasetModal.jsx @@ -115,6 +115,46 @@ function DatasetModal({ open, setOpen, updateDatasets }) { } }; + const handleColumnRename = (oldName, newName) => { + setColumnsSpec((prevSpec) => { + const updatedSpec = { ...prevSpec }; + + // Move the spec from old name to new name + if (updatedSpec[oldName]) { + updatedSpec[newName] = updatedSpec[oldName]; + delete updatedSpec[oldName]; + } + + return updatedSpec; + }); + + // Also update previewData to reflect the new column name + setPreviewData((prevData) => { + if (!prevData || !prevData.schema) return prevData; + + const updatedSchema = { ...prevData.schema }; + if (updatedSchema[oldName]) { + updatedSchema[newName] = updatedSchema[oldName]; + delete updatedSchema[oldName]; + } + + const updatedSample = prevData.sample.map((row) => { + const newRow = { ...row }; + if (oldName in newRow) { + newRow[newName] = newRow[oldName]; + delete newRow[oldName]; + } + return newRow; + }); + + return { + ...prevData, + schema: updatedSchema, + sample: updatedSample, + }; + }); + }; + const handleInferDataTypes = async (methods) => { setLoading(true); const formData = new FormData(); @@ -283,6 +323,7 @@ function DatasetModal({ open, setOpen, updateDatasets }) { setNextEnabled={setNextEnabled} columnsSpec={columnsSpec} setColumnsSpec={setColumnsSpec} + onColumnRename={handleColumnRename} /> )} diff --git a/DashAI/front/src/components/notebooks/datasetCreation/ConfigureAndUploadDatasetStep.jsx b/DashAI/front/src/components/notebooks/datasetCreation/ConfigureAndUploadDatasetStep.jsx index 2f13d146c..2fc989f2f 100644 --- a/DashAI/front/src/components/notebooks/datasetCreation/ConfigureAndUploadDatasetStep.jsx +++ b/DashAI/front/src/components/notebooks/datasetCreation/ConfigureAndUploadDatasetStep.jsx @@ -1,4 +1,4 @@ -import { useState, useEffect, useCallback } from "react"; +import { useState, useEffect, useCallback, useRef } from "react"; import { Grid, CircularProgress } from "@mui/material"; import FormSchemaButtonGroup from "../../shared/FormSchemaButtonGroup"; import Upload from "./Upload"; @@ -24,6 +24,7 @@ export default function ConfigureAndUploadDatasetStep({ const [previewError, setPreviewError] = useState(false); const [datasetFileToUpload, setDatasetFileToUpload] = useState(null); const [columnTypes, setColumnTypes] = useState(null); + const columnRenamesRef = useRef({}); const tourContext = useTourContext(); const { enqueueSnackbar } = useSnackbar(); @@ -77,6 +78,12 @@ export default function ConfigureAndUploadDatasetStep({ params["inferred_types"] = columnTypes; } + const currentRenames = columnRenamesRef.current; + + if (Object.keys(currentRenames).length > 0) { + params["column_renames"] = currentRenames; + } + const { file, url } = datasetFileToUpload; const data = await createDataset(name); @@ -123,6 +130,13 @@ export default function ConfigureAndUploadDatasetStep({ setColumnTypes(types); }, []); + const handleColumnRename = useCallback((oldName, newName) => { + columnRenamesRef.current = { + ...columnRenamesRef.current, + [oldName]: newName, + }; + }, []); + const isFormValid = () => { if (!formSubmitRef.current) return false; @@ -167,6 +181,7 @@ export default function ConfigureAndUploadDatasetStep({ selectedDataloader={selectedDataloader} onPreviewError={setPreviewError} onTypesChanged={handleTypesChanged} + onColumnRename={handleColumnRename} /> diff --git a/DashAI/front/src/components/notebooks/datasetCreation/PreviewDataset.jsx b/DashAI/front/src/components/notebooks/datasetCreation/PreviewDataset.jsx index ba034d6a4..69986ff34 100644 --- a/DashAI/front/src/components/notebooks/datasetCreation/PreviewDataset.jsx +++ b/DashAI/front/src/components/notebooks/datasetCreation/PreviewDataset.jsx @@ -14,12 +14,14 @@ import { useTranslation } from "react-i18next"; * @param {function} onChangeDataset - Callback function when the user wants to change the dataset * @param {function} onPreviewError - Callback function to notify parent of preview errors * @param {function} onTypesChanged - Callback to notify parent when column types change + * @param {function} onColumnRename - Callback to notify parent when columns are renamed (oldName, newName) */ function PreviewDataset({ datasetData, onChangeDataset, onPreviewError, onTypesChanged, + onColumnRename, }) { const theme = useTheme(); const { enqueueSnackbar } = useSnackbar(); @@ -112,6 +114,15 @@ function PreviewDataset({ [enqueueSnackbar, onTypesChanged], ); + const handleColumnRename = useCallback( + (oldName, newName) => { + if (onColumnRename) { + onColumnRename(oldName, newName); + } + }, + [onColumnRename], + ); + return ( @@ -244,6 +256,7 @@ PreviewDataset.propTypes = { onChangeDataset: PropTypes.func, onPreviewError: PropTypes.func, onTypesChanged: PropTypes.func, + onColumnRename: PropTypes.func, }; export default PreviewDataset; diff --git a/DashAI/front/src/components/notebooks/datasetCreation/PreviewDatasetTable.jsx b/DashAI/front/src/components/notebooks/datasetCreation/PreviewDatasetTable.jsx index c114d9834..dfb9895c7 100644 --- a/DashAI/front/src/components/notebooks/datasetCreation/PreviewDatasetTable.jsx +++ b/DashAI/front/src/components/notebooks/datasetCreation/PreviewDatasetTable.jsx @@ -1,7 +1,15 @@ import { useMemo, useState } from "react"; -import { Typography, Select, MenuItem, Box } from "@mui/material"; +import { + Typography, + Select, + MenuItem, + Box, + TextField, + Tooltip, +} from "@mui/material"; import { DataGrid } from "@mui/x-data-grid"; import { TypeChangeValidator } from "./TypeChangeValidator"; +import { useTranslation } from "react-i18next"; const TYPE_TO_DEFAULT_DTYPE = { Integer: "int64", @@ -26,6 +34,7 @@ const TYPE_TO_DEFAULT_DTYPE = { * @param {File} file - The uploaded file (needed for validation) * @param {Object} params - Dataloader parameters (needed for validation) * @param {Function} onTypeChange - Callback when types are successfully changed + * @param {Function} onColumnRename - Callback when a column is renamed (oldName, newName) => void */ export default function PreviewDatasetTable({ rows, @@ -33,9 +42,14 @@ export default function PreviewDatasetTable({ file, params, onTypeChange, + onColumnRename, }) { + const { t } = useTranslation(["common"]); const [showValidator, setShowValidator] = useState(false); const [pendingChanges, setPendingChanges] = useState({}); + const [editingColumn, setEditingColumn] = useState(null); + const [editValue, setEditValue] = useState(""); + const [columnNames, setColumnNames] = useState({}); const handleTypeChangeRequest = (columnName, newType) => { const currentType = columnTypes[columnName]?.type; @@ -70,18 +84,74 @@ export default function PreviewDatasetTable({ setPendingChanges({}); }; + const handleStartEdit = (columnName) => { + setEditingColumn(columnName); + setEditValue(columnNames[columnName] || columnName); + }; + + const handleCancelEdit = () => { + setEditingColumn(null); + setEditValue(""); + }; + + const handleConfirmEdit = (oldName) => { + const newName = editValue.trim(); + + if (!newName) { + handleCancelEdit(); + return; + } + + if (newName === oldName || newName === (columnNames[oldName] || oldName)) { + handleCancelEdit(); + return; + } + + const currentNames = Object.values(columnNames); + const allColumnNames = Object.keys(columnTypes).map( + (col) => columnNames[col] || col, + ); + + if (allColumnNames.includes(newName)) { + handleCancelEdit(); + return; + } + + setColumnNames((prev) => ({ + ...prev, + [oldName]: newName, + })); + + if (onColumnRename) { + onColumnRename(oldName, newName); + } + + handleCancelEdit(); + }; + + const handleKeyDown = (e, columnName) => { + if (e.key === "Enter") { + handleConfirmEdit(columnName); + } else if (e.key === "Escape") { + handleCancelEdit(); + } + }; + const columns = useMemo(() => { if (!rows || rows.length === 0) return []; const firstRow = rows[0]; return Object.keys(firstRow).map((field) => { const columnType = columnTypes[field]; + const displayName = columnNames[field] || field; return { field, - headerName: field, + headerName: displayName, minWidth: 150, flex: 1, + sortable: false, + disableColumnMenu: true, renderHeader: () => ( - - {field} - + {editingColumn === field ? ( + setEditValue(e.target.value)} + onKeyDown={(e) => handleKeyDown(e, field)} + onBlur={() => handleConfirmEdit(field)} + size="small" + sx={{ + width: "100%", + "& .MuiInputBase-input": { + fontSize: "0.875rem", + paddingY: 0.5, + }, + }} + /> + ) : ( + + handleStartEdit(field)} + sx={{ + fontWeight: 600, + fontSize: "0.875rem", + cursor: "pointer", + transition: "all 0.2s", + "&:hover": { + color: "primary.main", + textDecoration: "underline", + }, + }} + > + {displayName} + + + )}