Component reuse, Dialog component

This commit is contained in:
2026-06-12 04:11:21 +02:00
parent efa1f11840
commit 2dfd905e31
6 changed files with 226 additions and 40 deletions

View File

@@ -4,6 +4,8 @@ import type { ApiSettings } from "../../types";
import Button from "../../components/Button";
import ConfirmDialog from "../../components/ConfirmDialog";
import DismissibleAlert from "../../components/DismissibleAlert";
import FormField from "../../components/FormField";
import FieldLabel from "../../components/help/FieldLabel";
import ToggleSwitch from "../../components/ToggleSwitch";
import LoadingIndicator from "../../components/LoadingIndicator";
import {
@@ -1326,12 +1328,11 @@ export default function FilesPage({ settings }: { settings: ApiSettings }) {
{dialog === "create-folder" && (
<FileDialog title="Create folder" onClose={closeDialog}>
<label className="field-block">
<span>Folder name</span>
<FormField label="Folder name" help="Create a folder below the selected destination.">
<input autoFocus value={newFolderName} placeholder="e.g. invoices" onChange={(event) => { setNewFolderName(event.target.value); setNewFolderError(""); }} onKeyDown={(event) => { if (event.key === "Enter") void handleCreateFolder(); }} />
<small>Created inside {activeDialogTarget?.folderPath || "Root"}.</small>
{newFolderError && <small className="field-error">{newFolderError}</small>}
</label>
</FormField>
<div className="button-row compact-actions align-end">
<Button onClick={closeDialog} disabled={busy}>Cancel</Button>
<Button variant="primary" onClick={() => void handleCreateFolder()} disabled={busy || !newFolderName.trim()}>Create Folder</Button>
@@ -1343,14 +1344,13 @@ export default function FilesPage({ settings }: { settings: ApiSettings }) {
<FileDialog title={`${transferMode === "copy" ? "Copy" : "Move"} selection`} onClose={closeDialog}>
<p className="muted">Choose a destination space and folder. Folders are transferred recursively.</p>
<div className="form-grid two-column-form-grid">
<label className="field-block">
<span>Destination space</span>
<FormField label="Destination space" help="Select the space that will receive the selected file(s) or folder(s).">
<select value={transferDialogState.targetSpaceId} onChange={(event) => setTransferDialogState((current) => current ? { ...current, targetSpaceId: event.target.value, targetFolder: "" } : current)}>
{spaces.map((space) => <option key={space.id} value={space.id}>{space.label}</option>)}
</select>
</label>
</FormField>
<div className="field-block">
<span>Destination folder</span>
<FieldLabel className="form-label" help="Choose the target folder. Folders are transferred recursively.">Destination folder</FieldLabel>
<TransferFolderSelector
space={findSpace(transferDialogState.targetSpaceId)}
nodes={buildFolderTree(filesBySpace[transferDialogState.targetSpaceId] ?? EMPTY_FILES, foldersBySpace[transferDialogState.targetSpaceId] ?? EMPTY_FOLDERS)}
@@ -1369,11 +1369,9 @@ export default function FilesPage({ settings }: { settings: ApiSettings }) {
{dialog === "single-rename" && (
<FileDialog title="Rename item" onClose={closeDialog}>
<label className="field-block">
<span>New name</span>
<FormField label="New name" help="Only the visible name changes. Immutable blobs stay stable for audit.">
<input autoFocus value={singleRenameName} onChange={(event) => setSingleRenameName(event.target.value)} onKeyDown={(event) => { if (event.key === "Enter") void runSingleRename(); }} />
<small>Only the visible name changes. Immutable blobs stay stable for audit.</small>
</label>
</FormField>
<div className="button-row compact-actions align-end">
<Button onClick={closeDialog} disabled={busy}>Cancel</Button>
<Button variant="primary" onClick={() => void runSingleRename()} disabled={busy || !singleRenameName.trim()}>Rename</Button>
@@ -1385,17 +1383,16 @@ export default function FilesPage({ settings }: { settings: ApiSettings }) {
<FileDialog title="Bulk rename selected items" onClose={closeDialog}>
<p className="muted">Bulk rename changes managed display paths only. Immutable blobs stay stable for audit.</p>
<div className="form-grid two-column-form-grid">
<label className="field-block">
<span>Mode</span>
<FormField label="Mode" help="Choose how the selected names should be changed.">
<select value={renameMode} onChange={(event) => setRenameMode(event.target.value as RenameMode)}>
<option value="prefix">Add prefix</option>
<option value="suffix">Add suffix before extension</option>
<option value="replace">Find and replace</option>
</select>
</label>
{renameMode === "prefix" && <label className="field-block"><span>Prefix</span><input value={renamePrefix} onChange={(event) => setRenamePrefix(event.target.value)} /></label>}
{renameMode === "suffix" && <label className="field-block"><span>Suffix</span><input value={renameSuffix} onChange={(event) => setRenameSuffix(event.target.value)} /></label>}
{renameMode === "replace" && <><label className="field-block"><span>Find</span><input value={renameFind} onChange={(event) => setRenameFind(event.target.value)} /></label><label className="field-block"><span>Replacement</span><input value={renameReplacement} onChange={(event) => setRenameReplacement(event.target.value)} /></label></>}
</FormField>
{renameMode === "prefix" && <FormField label="Prefix"><input value={renamePrefix} onChange={(event) => setRenamePrefix(event.target.value)} /></FormField>}
{renameMode === "suffix" && <FormField label="Suffix"><input value={renameSuffix} onChange={(event) => setRenameSuffix(event.target.value)} /></FormField>}
{renameMode === "replace" && <><FormField label="Find"><input value={renameFind} onChange={(event) => setRenameFind(event.target.value)} /></FormField><FormField label="Replacement"><input value={renameReplacement} onChange={(event) => setRenameReplacement(event.target.value)} /></FormField></>}
</div>
{selectedFolderPaths.size > 0 && (
<ToggleSwitch label="Apply recursively to folder contents" checked={renameRecursive} onChange={setRenameRecursive} disabled={busy} />

View File

@@ -1,6 +1,7 @@
import type { DragEvent as ReactDragEvent, MouseEvent as ReactMouseEvent, ReactNode } from "react";
import { Copy, Download, Folder, FolderOpen, Home, MoveRight, Plus, Trash2, UploadCloud } from "lucide-react";
import Button from "../../../components/Button";
import Dialog from "../../../components/Dialog";
import type { ConflictAction, FileSpace, RenameResponse } from "../../../api/files";
import type { ConflictDialogState, FileActionTarget, FileConflictItem, FolderNode, ContextMenuState } from "../types";
import { isPathUnderOrSame, normalizeFolder, treeNodeKey } from "../utils/fileManagerUtils";
@@ -227,15 +228,18 @@ export function RenamePreviewList({
export function FileDialog({ title, onClose, children }: { title: string; onClose: () => void; children: ReactNode }) {
return (
<div className="file-dialog-backdrop" role="presentation" onMouseDown={(event) => { if (event.target === event.currentTarget) onClose(); }}>
<div className="file-dialog" role="dialog" aria-modal="true" aria-label={title}>
<div className="file-dialog-header">
<h3>{title}</h3>
<button type="button" onClick={onClose} aria-label="Close">×</button>
</div>
<div className="file-dialog-body">{children}</div>
</div>
</div>
<Dialog
open
title={title}
onClose={onClose}
backdropClassName="file-dialog-backdrop"
className="file-dialog"
headerClassName="file-dialog-header"
titleClassName="file-dialog-title"
bodyClassName="file-dialog-body"
>
{children}
</Dialog>
);
}