@vryzel/file-next-ui 0.5.1 → 0.5.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +130 -83
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +11 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +130 -83
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
package/dist/index.d.cts
CHANGED
|
@@ -200,6 +200,15 @@ interface FileExplorerProps {
|
|
|
200
200
|
onSelect: (file: FileNode) => void;
|
|
201
201
|
};
|
|
202
202
|
readonly protectedIds?: ReadonlyArray<string>;
|
|
203
|
+
/**
|
|
204
|
+
* Per-item rename/delete locks, resolved by the consumer (permissions,
|
|
205
|
+
* business rules). Locked actions are disabled in the context menu, blocked
|
|
206
|
+
* on F2/Delete, and excluded from the selection toast.
|
|
207
|
+
*/
|
|
208
|
+
readonly itemLocks?: (file: FileNode) => {
|
|
209
|
+
rename?: boolean;
|
|
210
|
+
delete?: boolean;
|
|
211
|
+
};
|
|
203
212
|
readonly labels?: Partial<ExplorerLabels>;
|
|
204
213
|
}
|
|
205
214
|
declare function FileExplorer(props: FileExplorerProps): React.ReactElement;
|
|
@@ -280,6 +289,8 @@ interface ExplorerContextMenuProps {
|
|
|
280
289
|
readonly mode?: "browse" | "trash";
|
|
281
290
|
readonly actions: ExplorerContextMenuActions;
|
|
282
291
|
readonly protectedItem?: boolean;
|
|
292
|
+
readonly renameLocked?: boolean;
|
|
293
|
+
readonly deleteLocked?: boolean;
|
|
283
294
|
readonly onClose: () => void;
|
|
284
295
|
readonly onPreview?: (file: FileNode) => void;
|
|
285
296
|
readonly onDownload?: (file: FileNode) => void;
|
package/dist/index.d.ts
CHANGED
|
@@ -200,6 +200,15 @@ interface FileExplorerProps {
|
|
|
200
200
|
onSelect: (file: FileNode) => void;
|
|
201
201
|
};
|
|
202
202
|
readonly protectedIds?: ReadonlyArray<string>;
|
|
203
|
+
/**
|
|
204
|
+
* Per-item rename/delete locks, resolved by the consumer (permissions,
|
|
205
|
+
* business rules). Locked actions are disabled in the context menu, blocked
|
|
206
|
+
* on F2/Delete, and excluded from the selection toast.
|
|
207
|
+
*/
|
|
208
|
+
readonly itemLocks?: (file: FileNode) => {
|
|
209
|
+
rename?: boolean;
|
|
210
|
+
delete?: boolean;
|
|
211
|
+
};
|
|
203
212
|
readonly labels?: Partial<ExplorerLabels>;
|
|
204
213
|
}
|
|
205
214
|
declare function FileExplorer(props: FileExplorerProps): React.ReactElement;
|
|
@@ -280,6 +289,8 @@ interface ExplorerContextMenuProps {
|
|
|
280
289
|
readonly mode?: "browse" | "trash";
|
|
281
290
|
readonly actions: ExplorerContextMenuActions;
|
|
282
291
|
readonly protectedItem?: boolean;
|
|
292
|
+
readonly renameLocked?: boolean;
|
|
293
|
+
readonly deleteLocked?: boolean;
|
|
283
294
|
readonly onClose: () => void;
|
|
284
295
|
readonly onPreview?: (file: FileNode) => void;
|
|
285
296
|
readonly onDownload?: (file: FileNode) => void;
|
package/dist/index.js
CHANGED
|
@@ -1039,6 +1039,8 @@ function ExplorerContextMenu(props) {
|
|
|
1039
1039
|
mode = "browse",
|
|
1040
1040
|
actions,
|
|
1041
1041
|
protectedItem = false,
|
|
1042
|
+
renameLocked = false,
|
|
1043
|
+
deleteLocked = false,
|
|
1042
1044
|
onClose,
|
|
1043
1045
|
onPreview,
|
|
1044
1046
|
onDownload,
|
|
@@ -1087,7 +1089,7 @@ function ExplorerContextMenu(props) {
|
|
|
1087
1089
|
swallowClickThrough();
|
|
1088
1090
|
onClose();
|
|
1089
1091
|
};
|
|
1090
|
-
const itemClass = "flex w-full min-h-11 cursor-pointer items-center gap-2 rounded-[10px] px-3 py-2.5 text-left text-sm outline-none hover:bg-accent disabled:opacity-40 sm:min-h-0 sm:px-2.5 sm:py-1.5";
|
|
1092
|
+
const itemClass = "flex w-full min-h-11 cursor-pointer items-center gap-2 rounded-[10px] px-3 py-2.5 text-left text-sm outline-none hover:bg-accent disabled:cursor-not-allowed disabled:opacity-40 sm:min-h-0 sm:px-2.5 sm:py-1.5";
|
|
1091
1093
|
return /* @__PURE__ */ jsx(
|
|
1092
1094
|
"div",
|
|
1093
1095
|
{
|
|
@@ -1164,13 +1166,24 @@ function ExplorerContextMenu(props) {
|
|
|
1164
1166
|
close();
|
|
1165
1167
|
extraFileAction.onSelect(target.file);
|
|
1166
1168
|
}, children: extraFileAction.label }) : null,
|
|
1167
|
-
onRename ? /* @__PURE__ */ jsxs(
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1169
|
+
onRename ? /* @__PURE__ */ jsxs(
|
|
1170
|
+
"button",
|
|
1171
|
+
{
|
|
1172
|
+
type: "button",
|
|
1173
|
+
role: "menuitem",
|
|
1174
|
+
className: itemClass,
|
|
1175
|
+
disabled: renameLocked,
|
|
1176
|
+
onClick: () => {
|
|
1177
|
+
if (renameLocked) return;
|
|
1178
|
+
close();
|
|
1179
|
+
onRename(target.file);
|
|
1180
|
+
},
|
|
1181
|
+
children: [
|
|
1182
|
+
/* @__PURE__ */ jsx(Pencil, { "aria-hidden": "true", className: "size-4" }),
|
|
1183
|
+
labels.rename
|
|
1184
|
+
]
|
|
1185
|
+
}
|
|
1186
|
+
) : null,
|
|
1174
1187
|
onCopy ? /* @__PURE__ */ jsxs("button", { type: "button", role: "menuitem", className: itemClass, onClick: () => {
|
|
1175
1188
|
close();
|
|
1176
1189
|
onCopy(target.file);
|
|
@@ -1209,10 +1222,10 @@ function ExplorerContextMenu(props) {
|
|
|
1209
1222
|
{
|
|
1210
1223
|
type: "button",
|
|
1211
1224
|
role: "menuitem",
|
|
1212
|
-
disabled: protectedItem,
|
|
1225
|
+
disabled: protectedItem || deleteLocked,
|
|
1213
1226
|
className: cn(itemClass, "text-destructive"),
|
|
1214
1227
|
onClick: () => {
|
|
1215
|
-
if (protectedItem) return;
|
|
1228
|
+
if (protectedItem || deleteLocked) return;
|
|
1216
1229
|
close();
|
|
1217
1230
|
if (onRequestDelete) onRequestDelete(target.file);
|
|
1218
1231
|
else void actions.deleteFile({ id: target.file.id });
|
|
@@ -1964,10 +1977,15 @@ function FileExplorerInner(props) {
|
|
|
1964
1977
|
onPreview,
|
|
1965
1978
|
onDownload,
|
|
1966
1979
|
extraFileAction,
|
|
1967
|
-
protectedIds
|
|
1980
|
+
protectedIds,
|
|
1981
|
+
itemLocks
|
|
1968
1982
|
} = props;
|
|
1969
1983
|
const labels = useExplorerLabels();
|
|
1970
1984
|
const protectedSet = useMemo(() => new Set(protectedIds ?? []), [protectedIds]);
|
|
1985
|
+
const locksFor = useCallback(
|
|
1986
|
+
(file) => itemLocks?.(file) ?? {},
|
|
1987
|
+
[itemLocks]
|
|
1988
|
+
);
|
|
1971
1989
|
const explorer = useFileExplorer({
|
|
1972
1990
|
listFiles: ((input) => listFiles({ parentId: input.parentId, limit: PAGE_SIZE })),
|
|
1973
1991
|
tenantId,
|
|
@@ -1989,6 +2007,7 @@ function FileExplorerInner(props) {
|
|
|
1989
2007
|
const browseCursor = useRef(void 0);
|
|
1990
2008
|
const overlayCursor = useRef(void 0);
|
|
1991
2009
|
const loadingMoreRef = useRef(false);
|
|
2010
|
+
const openingFolderIdRef = useRef(null);
|
|
1992
2011
|
const overlayMoreRef = useRef(false);
|
|
1993
2012
|
const browseGen = useRef(0);
|
|
1994
2013
|
const [overlayFiles, setOverlayFiles] = useState(null);
|
|
@@ -2061,6 +2080,7 @@ function FileExplorerInner(props) {
|
|
|
2061
2080
|
setLoadingMore(false);
|
|
2062
2081
|
return;
|
|
2063
2082
|
}
|
|
2083
|
+
if (mode === "reset") openingFolderIdRef.current = null;
|
|
2064
2084
|
if (result.ok) {
|
|
2065
2085
|
browseCursor.current = result.value.nextCursor;
|
|
2066
2086
|
setBrowseHasMore(Boolean(result.value.nextCursor));
|
|
@@ -2190,6 +2210,8 @@ function FileExplorerInner(props) {
|
|
|
2190
2210
|
(file) => {
|
|
2191
2211
|
if (file.id === DRAFT_FOLDER_ID) return;
|
|
2192
2212
|
if (file.kind === "folder") {
|
|
2213
|
+
if (openingFolderIdRef.current === file.id) return;
|
|
2214
|
+
openingFolderIdRef.current = file.id;
|
|
2193
2215
|
onOpenFolder?.(file);
|
|
2194
2216
|
return;
|
|
2195
2217
|
}
|
|
@@ -2320,6 +2342,7 @@ function FileExplorerInner(props) {
|
|
|
2320
2342
|
}
|
|
2321
2343
|
if (event.key === "F2" && selected && !trashOpen) {
|
|
2322
2344
|
event.preventDefault();
|
|
2345
|
+
if (locksFor(selected).rename) return;
|
|
2323
2346
|
setRenameTarget(selected);
|
|
2324
2347
|
return;
|
|
2325
2348
|
}
|
|
@@ -2329,7 +2352,8 @@ function FileExplorerInner(props) {
|
|
|
2329
2352
|
if (actions.purgeNode) setPurgeTargets(selectedFiles);
|
|
2330
2353
|
return;
|
|
2331
2354
|
}
|
|
2332
|
-
if (selectedFiles.some((file) => protectedSet.has(file.id)))
|
|
2355
|
+
if (selectedFiles.some((file) => protectedSet.has(file.id) || locksFor(file).delete))
|
|
2356
|
+
return;
|
|
2333
2357
|
setDeleteTargets(selectedFiles);
|
|
2334
2358
|
return;
|
|
2335
2359
|
}
|
|
@@ -2343,6 +2367,7 @@ function FileExplorerInner(props) {
|
|
|
2343
2367
|
clearSelection,
|
|
2344
2368
|
copyToClipboard,
|
|
2345
2369
|
handleActivate,
|
|
2370
|
+
locksFor,
|
|
2346
2371
|
pasteClipboard,
|
|
2347
2372
|
protectedSet,
|
|
2348
2373
|
selected,
|
|
@@ -2381,6 +2406,7 @@ function FileExplorerInner(props) {
|
|
|
2381
2406
|
...sortedFiles
|
|
2382
2407
|
];
|
|
2383
2408
|
}, [draftFolderName, parentId, sortedFiles, tenantId]);
|
|
2409
|
+
const listing = overlayLoading || pageStatus === "loading";
|
|
2384
2410
|
const renamingId = draftFolderName != null ? DRAFT_FOLDER_ID : renameTarget?.id ?? null;
|
|
2385
2411
|
const openUpload = () => uploadInputRef.current?.click();
|
|
2386
2412
|
return /* @__PURE__ */ jsxs(
|
|
@@ -2445,79 +2471,95 @@ function FileExplorerInner(props) {
|
|
|
2445
2471
|
}
|
|
2446
2472
|
)
|
|
2447
2473
|
] }),
|
|
2448
|
-
/* @__PURE__ */
|
|
2449
|
-
/* @__PURE__ */ jsx("p", { className: "font-mono text-[10px] uppercase tracking-[0.12em] text-muted-foreground", children: trashOpen ? labels.emptyTrash : query.trim() ? labels.noMatches : labels.emptyFolder }),
|
|
2450
|
-
trashOpen ? /* @__PURE__ */ jsx("p", { className: "mt-2 text-sm text-muted-foreground", children: labels.trashRetention }) : !query.trim() ? /* @__PURE__ */ jsx("p", { className: "mt-2 text-sm text-muted-foreground", children: labels.dropHint }) : null
|
|
2451
|
-
] }) : explorer.view === "list" ? /* @__PURE__ */ jsx(
|
|
2452
|
-
"div",
|
|
2453
|
-
{
|
|
2454
|
-
onClick: (event) => {
|
|
2455
|
-
if (event.target.closest("[data-file-id]")) return;
|
|
2456
|
-
clearSelection();
|
|
2457
|
-
},
|
|
2458
|
-
children: /* @__PURE__ */ jsx(
|
|
2459
|
-
ExplorerListView,
|
|
2460
|
-
{
|
|
2461
|
-
files: displayFiles,
|
|
2462
|
-
protectedIds: protectedSet,
|
|
2463
|
-
selectedIds,
|
|
2464
|
-
onSelect: handleSelect,
|
|
2465
|
-
onActivate: handleActivate,
|
|
2466
|
-
onContextMenu: handleContextMenu,
|
|
2467
|
-
onDragStart: explorer.beginDrag,
|
|
2468
|
-
onDragEnd: explorer.endDrag,
|
|
2469
|
-
onDrop: handleDropOnFolder,
|
|
2470
|
-
draggingId: explorer.draggingId,
|
|
2471
|
-
draggingIds,
|
|
2472
|
-
dropTargetId: explorer.dropTargetId,
|
|
2473
|
-
onDragOverRow: explorer.setDropTarget,
|
|
2474
|
-
sortKey,
|
|
2475
|
-
sortDirection: sortDir,
|
|
2476
|
-
onSortChange: handleSortChange,
|
|
2477
|
-
renamingId,
|
|
2478
|
-
onRenameCommit: commitRename,
|
|
2479
|
-
onRenameCancel: cancelRename,
|
|
2480
|
-
scrollRef,
|
|
2481
|
-
hasMore,
|
|
2482
|
-
loadingMore: loadingMore || overlayMore,
|
|
2483
|
-
onLoadMore: loadNext
|
|
2484
|
-
}
|
|
2485
|
-
)
|
|
2486
|
-
}
|
|
2487
|
-
) : /* @__PURE__ */ jsx(
|
|
2474
|
+
/* @__PURE__ */ jsxs(
|
|
2488
2475
|
"div",
|
|
2489
2476
|
{
|
|
2490
|
-
|
|
2491
|
-
|
|
2492
|
-
|
|
2493
|
-
|
|
2494
|
-
|
|
2495
|
-
|
|
2496
|
-
|
|
2497
|
-
|
|
2498
|
-
|
|
2499
|
-
|
|
2500
|
-
|
|
2501
|
-
|
|
2502
|
-
|
|
2503
|
-
|
|
2504
|
-
|
|
2505
|
-
|
|
2506
|
-
|
|
2507
|
-
|
|
2508
|
-
|
|
2509
|
-
|
|
2510
|
-
|
|
2511
|
-
|
|
2512
|
-
|
|
2513
|
-
|
|
2514
|
-
|
|
2515
|
-
|
|
2516
|
-
|
|
2517
|
-
|
|
2518
|
-
|
|
2477
|
+
ref: scrollRef,
|
|
2478
|
+
"aria-busy": listing,
|
|
2479
|
+
className: cn(
|
|
2480
|
+
"min-h-0 flex-1 overflow-y-auto overflow-x-auto overscroll-x-contain",
|
|
2481
|
+
// Stale rows stay mounted while the next listing loads, so dim them and
|
|
2482
|
+
// swallow clicks instead of letting the user re-open the same folder.
|
|
2483
|
+
listing && displayFiles.length > 0 && "pointer-events-none opacity-60"
|
|
2484
|
+
),
|
|
2485
|
+
children: [
|
|
2486
|
+
listing && displayFiles.length > 0 ? /* @__PURE__ */ jsx("div", { "aria-hidden": true, className: "h-0.5 w-full overflow-hidden bg-muted", children: /* @__PURE__ */ jsx("div", { className: "h-full w-1/3 animate-pulse bg-primary" }) }) : null,
|
|
2487
|
+
listing && displayFiles.length === 0 ? /* @__PURE__ */ jsx("p", { className: "px-4 py-12 text-center font-mono text-[10px] uppercase tracking-[0.12em] text-muted-foreground", children: labels.loading }) : overlayError || pageStatus === "error" && pageError ? /* @__PURE__ */ jsx("div", { className: "border-b border-destructive/30 bg-destructive/5 px-4 py-3 text-sm text-destructive", children: (overlayError ?? pageError)?.message ?? labels.folderCreateFailed }) : displayFiles.length === 0 ? /* @__PURE__ */ jsxs("div", { className: "flex min-h-full flex-col justify-center px-4 py-16 text-center", children: [
|
|
2488
|
+
/* @__PURE__ */ jsx("p", { className: "font-mono text-[10px] uppercase tracking-[0.12em] text-muted-foreground", children: trashOpen ? labels.emptyTrash : query.trim() ? labels.noMatches : labels.emptyFolder }),
|
|
2489
|
+
trashOpen ? /* @__PURE__ */ jsx("p", { className: "mt-2 text-sm text-muted-foreground", children: labels.trashRetention }) : !query.trim() ? /* @__PURE__ */ jsx("p", { className: "mt-2 text-sm text-muted-foreground", children: labels.dropHint }) : null
|
|
2490
|
+
] }) : explorer.view === "list" ? /* @__PURE__ */ jsx(
|
|
2491
|
+
"div",
|
|
2492
|
+
{
|
|
2493
|
+
onClick: (event) => {
|
|
2494
|
+
if (event.target.closest("[data-file-id]")) return;
|
|
2495
|
+
clearSelection();
|
|
2496
|
+
},
|
|
2497
|
+
children: /* @__PURE__ */ jsx(
|
|
2498
|
+
ExplorerListView,
|
|
2499
|
+
{
|
|
2500
|
+
files: displayFiles,
|
|
2501
|
+
protectedIds: protectedSet,
|
|
2502
|
+
selectedIds,
|
|
2503
|
+
onSelect: handleSelect,
|
|
2504
|
+
onActivate: handleActivate,
|
|
2505
|
+
onContextMenu: handleContextMenu,
|
|
2506
|
+
onDragStart: explorer.beginDrag,
|
|
2507
|
+
onDragEnd: explorer.endDrag,
|
|
2508
|
+
onDrop: handleDropOnFolder,
|
|
2509
|
+
draggingId: explorer.draggingId,
|
|
2510
|
+
draggingIds,
|
|
2511
|
+
dropTargetId: explorer.dropTargetId,
|
|
2512
|
+
onDragOverRow: explorer.setDropTarget,
|
|
2513
|
+
sortKey,
|
|
2514
|
+
sortDirection: sortDir,
|
|
2515
|
+
onSortChange: handleSortChange,
|
|
2516
|
+
renamingId,
|
|
2517
|
+
onRenameCommit: commitRename,
|
|
2518
|
+
onRenameCancel: cancelRename,
|
|
2519
|
+
scrollRef,
|
|
2520
|
+
hasMore,
|
|
2521
|
+
loadingMore: loadingMore || overlayMore,
|
|
2522
|
+
onLoadMore: loadNext
|
|
2523
|
+
}
|
|
2524
|
+
)
|
|
2525
|
+
}
|
|
2526
|
+
) : /* @__PURE__ */ jsx(
|
|
2527
|
+
"div",
|
|
2528
|
+
{
|
|
2529
|
+
onClick: (event) => {
|
|
2530
|
+
if (event.target.closest("[data-file-id]")) return;
|
|
2531
|
+
clearSelection();
|
|
2532
|
+
},
|
|
2533
|
+
children: /* @__PURE__ */ jsx(
|
|
2534
|
+
ExplorerGridView,
|
|
2535
|
+
{
|
|
2536
|
+
files: displayFiles,
|
|
2537
|
+
protectedIds: protectedSet,
|
|
2538
|
+
selectedIds,
|
|
2539
|
+
onSelect: handleSelect,
|
|
2540
|
+
onActivate: handleActivate,
|
|
2541
|
+
onContextMenu: handleContextMenu,
|
|
2542
|
+
onDragStart: explorer.beginDrag,
|
|
2543
|
+
onDragEnd: explorer.endDrag,
|
|
2544
|
+
onDrop: handleDropOnFolder,
|
|
2545
|
+
draggingId: explorer.draggingId,
|
|
2546
|
+
draggingIds,
|
|
2547
|
+
dropTargetId: explorer.dropTargetId,
|
|
2548
|
+
onDragOverRow: explorer.setDropTarget,
|
|
2549
|
+
renamingId,
|
|
2550
|
+
onRenameCommit: commitRename,
|
|
2551
|
+
onRenameCancel: cancelRename,
|
|
2552
|
+
scrollRef,
|
|
2553
|
+
hasMore,
|
|
2554
|
+
loadingMore: loadingMore || overlayMore,
|
|
2555
|
+
onLoadMore: loadNext
|
|
2556
|
+
}
|
|
2557
|
+
)
|
|
2558
|
+
}
|
|
2559
|
+
)
|
|
2560
|
+
]
|
|
2519
2561
|
}
|
|
2520
|
-
)
|
|
2562
|
+
),
|
|
2521
2563
|
/* @__PURE__ */ jsxs("footer", { className: "flex shrink-0 items-center justify-between gap-3 border-t border-border px-4 py-2", children: [
|
|
2522
2564
|
/* @__PURE__ */ jsxs("span", { className: "font-mono text-[10px] uppercase tracking-[0.12em] text-muted-foreground", children: [
|
|
2523
2565
|
/* @__PURE__ */ jsxs("span", { className: "text-primary tabular-nums", children: [
|
|
@@ -2538,6 +2580,8 @@ function FileExplorerInner(props) {
|
|
|
2538
2580
|
target: menu,
|
|
2539
2581
|
mode: trashOpen ? "trash" : "browse",
|
|
2540
2582
|
protectedItem: menu?.kind === "item" ? protectedSet.has(menu.file.id) : false,
|
|
2583
|
+
renameLocked: menu?.kind === "item" ? Boolean(locksFor(menu.file).rename) : false,
|
|
2584
|
+
deleteLocked: menu?.kind === "item" ? Boolean(locksFor(menu.file).delete) : false,
|
|
2541
2585
|
extraFileAction,
|
|
2542
2586
|
actions: {
|
|
2543
2587
|
deleteFile: async (input) => {
|
|
@@ -2572,7 +2616,10 @@ function FileExplorerInner(props) {
|
|
|
2572
2616
|
ExplorerSelectionToast,
|
|
2573
2617
|
{
|
|
2574
2618
|
files: selectedFiles,
|
|
2575
|
-
protectedIds:
|
|
2619
|
+
protectedIds: /* @__PURE__ */ new Set([
|
|
2620
|
+
...protectedSet,
|
|
2621
|
+
...selectedFiles.filter((file) => locksFor(file).delete).map((file) => file.id)
|
|
2622
|
+
]),
|
|
2576
2623
|
trashOpen,
|
|
2577
2624
|
onDownload,
|
|
2578
2625
|
onCopy: trashOpen ? void 0 : () => copyToClipboard(selectedFiles),
|