feat(files): folders managed in place, a grid per folder, drop to upload, open on one file
- Own folders are made, renamed and deleted from the rail (right click, long press, Shift+F10) or the "…" beside an open folder's name; any folder's retention is a submenu (7/30/90 days, until deleted). Deleting confirms where its files go. - List or grid per folder, sharing one sort; the grid is the one media card. - A file dropped anywhere on the page opens Upload with it, bound for the folder on screen. - A link (?item=) or the upload notice opens Files on the file: its page turned to, the keyboard on it, a ring round it. - An in-place name field left by Enter or Escape hands the focus back to what replaced it (the row, the entry, the "…") instead of dropping it on <body>. - The table's saved page size is per account.
This commit is contained in:
@@ -11,6 +11,8 @@ import { notifyActionError } from "../lib/actionErrors";
|
||||
import { invalidateDownloads } from "../lib/downloads";
|
||||
import { notify } from "../lib/notifications";
|
||||
import { navigateTo } from "../lib/nav";
|
||||
import type { DownloadJob } from "../lib/api";
|
||||
import { useFilesActions } from "./filesContext";
|
||||
import { formatBytes } from "../lib/format";
|
||||
import { DISPLAY_NAME_MAX } from "../lib/api/downloads";
|
||||
import { Pressable } from "./ui/Pressable";
|
||||
@@ -26,21 +28,21 @@ import { Input } from "./ui/Input";
|
||||
// skip the whole dialog.
|
||||
const uploadedKey = (status: string) =>
|
||||
status === "done" ? "downloads.upload.added" : "downloads.upload.addedConverting";
|
||||
// A done file is in Files; one the worker still converts is in the download queue until it is.
|
||||
const uploadedPage = (status: string) => (status === "done" ? "files" : "downloads");
|
||||
|
||||
export default function UploadDialog({
|
||||
folderId = null,
|
||||
initialFile = null,
|
||||
onClose,
|
||||
}: {
|
||||
/** The library folder the file goes into (an own folder, or the Uploads folder); null = Uploads. */
|
||||
folderId?: number | null;
|
||||
/** A file already chosen — dropped on the Files page — so the dialog opens ready to send it. */
|
||||
initialFile?: File | null;
|
||||
onClose: () => void;
|
||||
}) {
|
||||
const { t, i18n } = useTranslation();
|
||||
const lang = i18n.language;
|
||||
const qc = useQueryClient();
|
||||
const [file, setFile] = useState<File | null>(null);
|
||||
const [file, setFile] = useState<File | null>(initialFile);
|
||||
const [name, setName] = useState("");
|
||||
const [dragOver, setDragOver] = useState(false);
|
||||
const [pct, setPct] = useState<number | null>(null); // null = idle; 0–100 while uploading
|
||||
@@ -65,6 +67,13 @@ export default function UploadDialog({
|
||||
// silent to a screen reader. `SetupWizard` and `Welcome` both key their alerts for the
|
||||
// same reason, and `formAlerts.dom.test.tsx` is the test that exists because of it.
|
||||
const [pickSeq, setPickSeq] = useState(0);
|
||||
const { openFile } = useFilesActions();
|
||||
// A done file is in Files, opened on it; one the worker still converts is in the download queue
|
||||
// until it is.
|
||||
const openUploaded = (job: DownloadJob) => {
|
||||
if (job.status === "done") openFile(job.id, job.folder_id ?? null);
|
||||
else navigateTo("downloads");
|
||||
};
|
||||
const pick_ = (f: File) => {
|
||||
setFile(f);
|
||||
setName("");
|
||||
@@ -101,7 +110,7 @@ export default function UploadDialog({
|
||||
message: t(uploadedKey(job.status)),
|
||||
action: {
|
||||
label: t("downloads.dialog.view"),
|
||||
onClick: () => navigateTo(uploadedPage(job.status)),
|
||||
onClick: () => openUploaded(job),
|
||||
},
|
||||
});
|
||||
onClose();
|
||||
|
||||
Reference in New Issue
Block a user