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:
@@ -72,8 +72,21 @@ function listQuery(p: FileListParams): string {
|
||||
return s ? `?${s}` : "";
|
||||
}
|
||||
|
||||
// What a folder PATCH may change. Presence decides: `retention_days: null` = keep its files forever.
|
||||
export interface FolderPatch {
|
||||
name?: string; // an own folder's only
|
||||
retention_days?: number | null;
|
||||
}
|
||||
|
||||
export const filesApi = {
|
||||
fileFolders: (): Promise<FileFolder[]> => req("/api/files/folders"),
|
||||
createFolder: (name: string): Promise<FileFolder> =>
|
||||
req("/api/files/folders", { method: "POST", body: JSON.stringify({ name }) }),
|
||||
updateFolder: (id: number, patch: FolderPatch): Promise<FileFolder> =>
|
||||
req(`/api/files/folders/${id}`, { method: "PATCH", body: JSON.stringify(patch) }),
|
||||
// An own folder only; its files go back to the folders they came from.
|
||||
deleteFolder: (id: number): Promise<{ ok: true; deleted: number; files_moved: number }> =>
|
||||
req(`/api/files/folders/${id}`, { method: "DELETE" }),
|
||||
files: (p: FileListParams = {}): Promise<LibraryFile[]> => req(`/api/files${listQuery(p)}`),
|
||||
filesUsage: (): Promise<StorageUsage> => req("/api/files/usage"),
|
||||
updateFile: (id: number, patch: FilePatch): Promise<DownloadJob> =>
|
||||
|
||||
@@ -14,8 +14,13 @@ export const LS = {
|
||||
filesCollapsed: "siftlode.filesCollapsed",
|
||||
// The Files view last open (every file, shared with me, or a folder id), per-account.
|
||||
filesView: "siftlode.filesView",
|
||||
// The Files table's sort and rows-per-page (DataTable's persist blob).
|
||||
// The Files table's rows-per-page and page (DataTable's persist blob), per-account.
|
||||
filesTable: "siftlode.filesTable",
|
||||
// List or grid per Files view (a `{ viewParam: layout }` map), the sort both paints share, and the
|
||||
// grid's cards per page — per-account mirrors of the server prefs (the last is local only).
|
||||
filesLayouts: "siftlode.filesLayouts",
|
||||
filesSort: "siftlode.filesSort",
|
||||
filesCardSize: "siftlode.filesCardSize",
|
||||
hints: "siftlode.hints",
|
||||
lang: "siftlode.lang",
|
||||
filters: "siftlode.filters",
|
||||
|
||||
@@ -17,6 +17,8 @@ export function useCardPager<T>(
|
||||
/** Follow a row the caller just moved: each new `seq` turns to the page holding the row `match`
|
||||
* finds — the same contract as `DataTable`'s `reveal`, for the card paint of the same list. */
|
||||
reveal?: { seq: number; match: (row: T) => boolean } | null,
|
||||
/** The size picker's name — what the cards ARE (the channel manager's are "cards"). */
|
||||
sizeLabel?: string,
|
||||
) {
|
||||
const { t } = useTranslation();
|
||||
const [size, setSize] = useState(() => parseCardSize(readAccount(sizeKey, null)));
|
||||
@@ -58,7 +60,7 @@ export function useCardPager<T>(
|
||||
writeAccount(sizeKey, s);
|
||||
setPage(0);
|
||||
},
|
||||
sizeLabel: t("channels.cardsPerPage"),
|
||||
sizeLabel: sizeLabel ?? t("channels.cardsPerPage"),
|
||||
},
|
||||
/** Call when the caller's own filters change — a different list starts at page one. */
|
||||
resetPage: useCallback(() => setPage(0), []),
|
||||
|
||||
Reference in New Issue
Block a user