diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index a017f87..b1e6045 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -7,6 +7,7 @@ import LoginPage from "./pages/Login"; import UploadPage from "./pages/Upload"; import StatPage from "./pages/Stats"; import { getDocumentTitle } from "./utils/documentTitle"; +import DatasetEditPage from "./pages/DatasetEdit"; function App() { const location = useLocation(); @@ -24,6 +25,7 @@ function App() { } /> } /> } /> + } /> ); diff --git a/frontend/src/pages/DatasetEdit.tsx b/frontend/src/pages/DatasetEdit.tsx new file mode 100644 index 0000000..dc3a818 --- /dev/null +++ b/frontend/src/pages/DatasetEdit.tsx @@ -0,0 +1,161 @@ +import StatsStyling from "../styles/stats_styling"; +import { useNavigate, useParams } from "react-router-dom"; +import { useEffect, useMemo, useState, type FormEvent } from "react"; +import axios from "axios"; + +const API_BASE_URL = import.meta.env.VITE_BACKEND_URL; +const styles = StatsStyling; + +type DatasetInfoResponse = { + id: number; + name: string; + created_at: string; +}; + +const DatasetEditPage = () => { + const navigate = useNavigate(); + const { datasetId } = useParams<{ datasetId: string }>(); + const parsedDatasetId = useMemo(() => Number(datasetId), [datasetId]); + const [statusMessage, setStatusMessage] = useState(""); + const [loading, setLoading] = useState(true); + const [isSaving, setIsSaving] = useState(false); + const [hasError, setHasError] = useState(false); + + const [datasetName, setDatasetName] = useState(""); + + useEffect(() => { + if (!Number.isInteger(parsedDatasetId) || parsedDatasetId <= 0) { + setHasError(true); + setStatusMessage("Invalid dataset id."); + setLoading(false); + return; + } + + const token = localStorage.getItem("access_token"); + if (!token) { + setHasError(true); + setStatusMessage("You must be signed in to edit datasets."); + setLoading(false); + return; + } + + axios + .get(`${API_BASE_URL}/dataset/${parsedDatasetId}`, { + headers: { Authorization: `Bearer ${token}` }, + }) + .then((response) => { + setDatasetName(response.data.name || ""); + }) + .catch((error: unknown) => { + setHasError(true); + if (axios.isAxiosError(error)) { + setStatusMessage(String(error.response?.data?.error || error.message)); + } else { + setStatusMessage("Could not get dataset info."); + } + }) + .finally(() => { + setLoading(false); + }); + }, [parsedDatasetId]); + + const saveDatasetName = async (event: FormEvent) => { + event.preventDefault(); + + const trimmedName = datasetName.trim(); + if (!trimmedName) { + setHasError(true); + setStatusMessage("Please enter a valid dataset name."); + return; + } + + const token = localStorage.getItem("access_token"); + if (!token) { + setHasError(true); + setStatusMessage("You must be signed in to save changes."); + return; + } + + try { + setIsSaving(true); + setHasError(false); + setStatusMessage(""); + + await axios.patch( + `${API_BASE_URL}/dataset/${parsedDatasetId}`, + { name: trimmedName }, + { headers: { Authorization: `Bearer ${token}` } } + ); + + navigate("/datasets", { replace: true }); + } catch (error: unknown) { + setHasError(true); + if (axios.isAxiosError(error)) { + setStatusMessage(String(error.response?.data?.error || error.message || "Save failed.")); + } else { + setStatusMessage("Save failed due to an unexpected error."); + } + } finally { + setIsSaving(false); + } + }; + + return ( +
+
+
+
+

Edit Dataset

+

Update the dataset name shown in your datasets list.

+
+
+ +
+ + + setDatasetName(event.target.value)} + disabled={loading || isSaving} + /> + +
+ + + + {loading + ? "Loading dataset details..." + : statusMessage} +
+
+
+
+ ); +}; + +export default DatasetEditPage; diff --git a/frontend/src/pages/Datasets.tsx b/frontend/src/pages/Datasets.tsx index b054420..4c79cdc 100644 --- a/frontend/src/pages/Datasets.tsx +++ b/frontend/src/pages/Datasets.tsx @@ -94,6 +94,7 @@ const DatasetsPage = () => {
    {datasets.map((dataset) => { const isComplete = dataset.status === "complete"; + const editPath = `/dataset/${dataset.id}/edit`; const targetPath = isComplete ? `/dataset/${dataset.id}/stats` : `/dataset/${dataset.id}/status`; @@ -117,13 +118,25 @@ const DatasetsPage = () => { )} - +
    + { isComplete && + + } + + +
    ); })} diff --git a/server/app.py b/server/app.py index 53141cc..7cbf9d3 100644 --- a/server/app.py +++ b/server/app.py @@ -164,10 +164,10 @@ def get_dataset(dataset_id): if not dataset_manager.authorize_user_dataset(dataset_id, user_id): raise NotAuthorisedException("This user is not authorised to access this dataset") - dataset_content = dataset_manager.get_dataset_content(dataset_id) - filters = get_request_filters() - filtered_dataset = stat_gen.filter_dataset(dataset_content, filters) - return jsonify(filtered_dataset), 200 + dataset_info = dataset_manager.get_dataset_info(dataset_id) + included_cols = {"id", "name", "created_at"} + + return jsonify({k: dataset_info[k] for k in included_cols}), 200 except NotAuthorisedException: return jsonify({"error": "User is not authorised to access this content"}), 403 except NonExistentDatasetException: @@ -253,6 +253,8 @@ def content_endpoint(dataset_id): return jsonify(stat_gen.get_content_analysis(dataset_content, filters)), 200 except NotAuthorisedException: return jsonify({"error": "User is not authorised to access this content"}), 403 + except NonExistentDatasetException: + return jsonify({"error": "Dataset does not exist"}), 404 except ValueError as e: return jsonify({"error": f"Malformed or missing data: {str(e)}"}), 400 except Exception as e: @@ -273,6 +275,8 @@ def get_summary(dataset_id): return jsonify(stat_gen.summary(dataset_content, filters)), 200 except NotAuthorisedException: return jsonify({"error": "User is not authorised to access this content"}), 403 + except NonExistentDatasetException: + return jsonify({"error": "Dataset does not exist"}), 404 except ValueError as e: return jsonify({"error": f"Malformed or missing data: {str(e)}"}), 400 except Exception as e: @@ -293,6 +297,8 @@ def get_time_analysis(dataset_id): return jsonify(stat_gen.get_time_analysis(dataset_content, filters)), 200 except NotAuthorisedException: return jsonify({"error": "User is not authorised to access this content"}), 403 + except NonExistentDatasetException: + return jsonify({"error": "Dataset does not exist"}), 404 except ValueError as e: return jsonify({"error": f"Malformed or missing data: {str(e)}"}), 400 except Exception as e: @@ -313,6 +319,8 @@ def get_user_analysis(dataset_id): return jsonify(stat_gen.get_user_analysis(dataset_content, filters)), 200 except NotAuthorisedException: return jsonify({"error": "User is not authorised to access this content"}), 403 + except NonExistentDatasetException: + return jsonify({"error": "Dataset does not exist"}), 404 except ValueError as e: return jsonify({"error": f"Malformed or missing data: {str(e)}"}), 400 except Exception as e: @@ -333,6 +341,8 @@ def get_cultural_analysis(dataset_id): return jsonify(stat_gen.get_cultural_analysis(dataset_content, filters)), 200 except NotAuthorisedException: return jsonify({"error": "User is not authorised to access this content"}), 403 + except NonExistentDatasetException: + return jsonify({"error": "Dataset does not exist"}), 404 except ValueError as e: return jsonify({"error": f"Malformed or missing data: {str(e)}"}), 400 except Exception as e: @@ -353,6 +363,8 @@ def get_interaction_analysis(dataset_id): return jsonify(stat_gen.get_interactional_analysis(dataset_content, filters)), 200 except NotAuthorisedException: return jsonify({"error": "User is not authorised to access this content"}), 403 + except NonExistentDatasetException: + return jsonify({"error": "Dataset does not exist"}), 404 except ValueError as e: return jsonify({"error": f"Malformed or missing data: {str(e)}"}), 400 except Exception as e: