Editable and removable datasets #8

Merged
dylan merged 9 commits from feat/editable-datasets into main 2026-03-05 16:55:48 +00:00
2 changed files with 87 additions and 16 deletions
Showing only changes of commit 2b14a8a417 - Show all commits

View File

@@ -0,0 +1,48 @@
import { Dialog, DialogPanel, DialogTitle } from "@headlessui/react";
import StatsStyling from "../styles/stats_styling";
type Props = {
open: boolean;
title: string;
message: string;
confirmLabel?: string;
cancelLabel?: string;
loading?: boolean;
onConfirm: () => void;
onCancel: () => void;
};
const styles = StatsStyling;
export default function ConfirmationModal({
open,
title,
message,
confirmLabel = "Confirm",
cancelLabel = "Cancel",
loading = false,
onConfirm,
onCancel,
}: Props) {
return (
<Dialog open={open} onClose={onCancel} style={styles.modalRoot}>
<div style={styles.modalBackdrop} />
<div style={styles.modalContainer}>
<DialogPanel style={{ ...styles.card, ...styles.modalPanel }}>
<DialogTitle style={styles.sectionTitle}>{title}</DialogTitle>
<p style={styles.sectionSubtitle}>{message}</p>
<div style={{ display: "flex", justifyContent: "flex-end", gap: 8 }}>
<button type="button" onClick={onCancel} style={styles.buttonSecondary} disabled={loading}>
{cancelLabel}
</button>
<button type="button" onClick={onConfirm} style={styles.buttonDanger} disabled={loading}>
{loading ? "Deleting..." : confirmLabel}
</button>
</div>
</DialogPanel>
</div>
</Dialog>
);
}

View File

@@ -2,6 +2,7 @@ import StatsStyling from "../styles/stats_styling";
import { useNavigate, useParams } from "react-router-dom"; import { useNavigate, useParams } from "react-router-dom";
import { useEffect, useMemo, useState, type FormEvent } from "react"; import { useEffect, useMemo, useState, type FormEvent } from "react";
import axios from "axios"; import axios from "axios";
import ConfirmationModal from "../components/ConfirmationModal";
const API_BASE_URL = import.meta.env.VITE_BACKEND_URL; const API_BASE_URL = import.meta.env.VITE_BACKEND_URL;
const styles = StatsStyling; const styles = StatsStyling;
@@ -19,16 +20,11 @@ const DatasetEditPage = () => {
const [statusMessage, setStatusMessage] = useState(""); const [statusMessage, setStatusMessage] = useState("");
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [isSaving, setIsSaving] = useState(false); const [isSaving, setIsSaving] = useState(false);
const [isDeleting, setIsDeleting] = useState(false);
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
const [hasError, setHasError] = useState(false); const [hasError, setHasError] = useState(false);
const [datasetName, setDatasetName] = useState(""); const [datasetName, setDatasetName] = useState("");
const token = localStorage.getItem("access_token");
if (!token) {
setHasError(true);
setStatusMessage("You must be signed in to save changes.");
}
useEffect(() => { useEffect(() => {
if (!Number.isInteger(parsedDatasetId) || parsedDatasetId <= 0) { if (!Number.isInteger(parsedDatasetId) || parsedDatasetId <= 0) {
setHasError(true); setHasError(true);
@@ -108,21 +104,37 @@ const DatasetEditPage = () => {
}; };
const deleteDataset = async () => { const deleteDataset = async () => {
try{ const deleteToken = localStorage.getItem("access_token");
if (!deleteToken) {
setHasError(true);
setStatusMessage("You must be signed in to delete datasets.");
setIsDeleteModalOpen(false);
return;
}
try {
setIsDeleting(true);
setHasError(false);
setStatusMessage("");
await axios.delete( await axios.delete(
`${API_BASE_URL}/dataset/${parsedDatasetId}`, `${API_BASE_URL}/dataset/${parsedDatasetId}`,
{ headers: { Authorization: `Bearer ${token}` } } { headers: { Authorization: `Bearer ${deleteToken}` } }
); );
setIsDeleteModalOpen(false);
navigate("/datasets", { replace: true }); navigate("/datasets", { replace: true });
} catch (error: unknown) { } catch (error: unknown) {
setHasError(true); setHasError(true);
if (axios.isAxiosError(error)) { if (axios.isAxiosError(error)) {
setStatusMessage(String(error.response?.data?.error || error.message || "Save failed.")); setStatusMessage(String(error.response?.data?.error || error.message || "Delete failed."));
} else { } else {
setStatusMessage("Save failed due to an unexpected error."); setStatusMessage("Delete failed due to an unexpected error.");
}
} }
} finally {
setIsDeleting(false);
} }
};
return ( return (
<div style={styles.page}> <div style={styles.page}>
@@ -159,8 +171,8 @@ const DatasetEditPage = () => {
<button <button
type="button" type="button"
style={styles.buttonDanger} style={styles.buttonDanger}
onClick={deleteDataset} onClick={() => setIsDeleteModalOpen(true)}
disabled={isSaving} disabled={isSaving || isDeleting}
> >
Delete Dataset Delete Dataset
</button> </button>
@@ -169,14 +181,14 @@ const DatasetEditPage = () => {
type="button" type="button"
style={styles.buttonSecondary} style={styles.buttonSecondary}
onClick={() => navigate("/datasets")} onClick={() => navigate("/datasets")}
disabled={isSaving} disabled={isSaving || isDeleting}
> >
Cancel Cancel
</button> </button>
<button <button
type="submit" type="submit"
style={{ ...styles.buttonPrimary, opacity: loading || isSaving ? 0.75 : 1 }} style={{ ...styles.buttonPrimary, opacity: loading || isSaving ? 0.75 : 1 }}
disabled={loading || isSaving} disabled={loading || isSaving || isDeleting}
> >
{isSaving ? "Saving..." : "Save"} {isSaving ? "Saving..." : "Save"}
</button> </button>
@@ -186,6 +198,17 @@ const DatasetEditPage = () => {
: statusMessage} : statusMessage}
</div> </div>
</form> </form>
<ConfirmationModal
open={isDeleteModalOpen}
title="Delete Dataset"
message={`Are you sure you want to delete "${datasetName || "this dataset"}"? This action cannot be undone.`}
confirmLabel="Delete"
cancelLabel="Keep Dataset"
loading={isDeleting}
onCancel={() => setIsDeleteModalOpen(false)}
onConfirm={deleteDataset}
/>
</div> </div>
</div> </div>
); );