change things

This commit is contained in:
rgrgogu
2026-08-09 17:33:21 +08:00
parent 08a0d122ea
commit 377a4d4b5e
62 changed files with 424 additions and 299 deletions
+40 -84
View File
@@ -5,19 +5,17 @@ import { Link, useNavigate } from "react-router-dom";
import { useForm, Controller } from "react-hook-form";
import { z } from "zod";
import { zodResolver } from "@hookform/resolvers/zod";
import { toast } from "sonner";
import { ArrowLeft, ArrowRight, UploadCloud, X, FileVideo, FileText, Image, FileAudio } from "lucide-react";
import { useAssets } from "@/contexts/AdminAssetsContext";
import { useUploadQueue } from "@/contexts/UploadQueueContext";
import { useAuth } from "@/contexts/AuthContext";
import { MAX_ASSET_FILE_SIZE_SINGLE, MAX_ASSET_FILE_SIZE_SINGLE_LABEL } from "@/utils/assetUpload.util";
import { formatFileSize } from "@/utils/format.util";
import { useUnsavedChangesGuard } from "@/hooks/useUnsavedChangesGuard";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Textarea } from "@/components/ui/textarea";
import { Spinner } from "@/components/ui/spinner";
import { Progress } from "@/components/ui/progress";
import {
Select,
SelectContent,
@@ -39,8 +37,6 @@ function resolveFileType(mimeType = "") {
// ─── Schema ───────────────────────────────────────────────────────────────────
const schema = z.object({
display_name: z.string().min(1, "Display name is required."),
description: z.string().optional(),
is_public: z.enum(["true", "false"]),
storage_provider: z.enum(["chibisafe", "local", "s3"]),
});
@@ -119,16 +115,15 @@ function FieldError({ message }) {
export default function AddAsset() {
const navigate = useNavigate();
const { uploadAsset, loading } = useAssets();
const { fetchAssets } = useAssets();
const { addBatch } = useUploadQueue();
const { user } = useAuth();
const fileRef = useRef(null);
const thumbnailRef = useRef(null);
const [thumbKey, setThumbKey] = useState(0);
const [progress, setProgress] = useState(null); // { phase: 'uploading'|'processing'|'done'|'error', pct } | null
const {
register,
control,
handleSubmit,
setValue,
@@ -139,8 +134,6 @@ export default function AddAsset() {
} = useForm({
resolver: zodResolver(schema),
defaultValues: {
display_name: "",
description: "",
is_public: "false",
storage_provider: "s3", // ← changed from "chibisafe"
},
@@ -164,7 +157,6 @@ export default function AddAsset() {
}
fileRef.current = f;
setValue("_file", f);
if (!watch("display_name")) setValue("display_name", f.name);
clearErrors("_file");
};
@@ -174,40 +166,46 @@ export default function AddAsset() {
clearErrors("_thumbnail");
};
const onSubmit = async (data) => {
let hasFileError = false;
// Fire-and-forget: the job goes on the same global UploadQueueContext the
// Bulk flow uses, so it keeps uploading in the background no matter where
// you navigate to next — a single sonner toast.promise() tracks it
// through loading -> success/error instead of a dedicated widget.
const onSubmit = (data) => {
if (!fileRef.current) {
setError("_file", { message: "A file is required." });
hasFileError = true;
return;
}
if (hasFileError) return;
setProgress({ phase: "uploading", pct: 0 });
const result = await uploadAsset({
file: fileRef.current,
thumbnail: thumbnailRef.current ?? undefined,
display_name: data.display_name,
description: data.description ?? "",
file_type: fileType,
is_public: data.is_public === "true",
storage_provider: data.storage_provider,
createdBy: user?.user_id,
onProgress: setProgress,
const fileName = fileRef.current.name;
const uploadPromise = new Promise((resolve, reject) => {
addBatch(
[{ file: fileRef.current, thumbnail: thumbnailRef.current ?? undefined }],
{
is_public: data.is_public === "true",
storage_provider: data.storage_provider,
createdBy: user?.user_id,
},
{
source: "single",
onSettled: ([result]) => {
fetchAssets({ force: true });
if (result?.status === "uploaded") resolve(result);
else reject(new Error(result?.error || "Upload failed."));
},
}
);
});
setProgress(null);
if (result) { bypassOnce(); navigate("/admin/assets"); }
toast.promise(uploadPromise, {
loading: `Uploading ${fileName}…`,
success: (result) => `${result.name} uploaded successfully.`,
error: (err) => err.message || "Upload failed.",
});
bypassOnce();
navigate("/admin/assets");
};
const progressLabel = {
uploading: "Uploading…",
processing: "Processing…",
done: "Done.",
error: "Upload failed.",
}[progress?.phase];
return (
<div className="max-w-2xl mx-auto px-4 py-6 space-y-6">
@@ -217,8 +215,8 @@ export default function AddAsset() {
<ArrowLeft className="h-4 w-4" />
</Button>
<div>
<h1 className="text-xl font-semibold">Add Asset</h1>
<p className="text-sm text-muted-foreground">Upload a new file to the asset library.</p>
<h1 className="text-xl font-semibold">Add File</h1>
<p className="text-sm text-muted-foreground">Upload a new file to the file library.</p>
</div>
</div>
@@ -236,7 +234,6 @@ export default function AddAsset() {
onClear={() => {
fileRef.current = null;
setValue("_file", null);
setValue("display_name", "");
clearErrors("_file");
}}
error={errors._file?.message}
@@ -277,30 +274,6 @@ export default function AddAsset() {
</div>
)}
{/* ── Display Name ── */}
<div className="space-y-1.5">
<Label htmlFor="display_name">
Display Name <span className="text-destructive">*</span>
</Label>
<Input
id="display_name"
placeholder="Friendly name for this asset"
{...register("display_name")}
/>
<FieldError message={errors.display_name?.message} />
</div>
{/* ── Description ── */}
<div className="space-y-1.5">
<Label htmlFor="description">Description</Label>
<Textarea
id="description"
placeholder="Optional description"
rows={3}
{...register("description")}
/>
</div>
{/* ── File Type · Access · Storage (one row) ── */}
<div className="grid grid-cols-3 gap-4">
@@ -355,29 +328,12 @@ export default function AddAsset() {
</div>
{/* ── Upload progress (real — see AdminAssetsContext.uploadAsset) ── */}
{progress && (
<div className="space-y-1.5">
<div className="flex items-center justify-between text-xs text-muted-foreground">
<span>{progressLabel}</span>
<span>{progress.pct ?? 0}%</span>
</div>
<Progress value={progress.pct ?? 0} />
</div>
)}
{/* ── Actions ── */}
<div className="flex justify-end gap-3">
<Button
type="button"
variant="outline"
onClick={() => navigate("/admin/assets")}
disabled={loading}
>
<Button type="button" variant="outline" onClick={() => navigate("/admin/assets")}>
Cancel
</Button>
<Button type="submit" disabled={loading}>
{loading && <Spinner className="h-4 w-4 mr-2" />}
<Button type="submit">
Upload Asset
</Button>
</div>