mirror of
https://git.srhtnin.garden/sarah/SaraView.git
synced 2026-10-13 18:28:14 +02:00
Added modals for popups, and fixed root-level bug.
This commit is contained in:
1 parent
9736a55e00
commit
104198036a
11 files changed
+312
-62
No files matched your search
@@ -47,6 +47,22 @@ def ResolvePath(RootName: str, RelativePath: str) -> Path:
|
||||
return Candidate
|
||||
|
||||
|
||||
def NormalizeRelativePath(BaseDirectory: str, RawPath: str) -> str:
|
||||
Combined = f"{BaseDirectory}/{RawPath}" if BaseDirectory else RawPath
|
||||
Parts: list[str] = []
|
||||
|
||||
for Part in Combined.split("/"):
|
||||
if Part in ("", "."):
|
||||
continue
|
||||
if Part == "..":
|
||||
if Parts:
|
||||
Parts.pop()
|
||||
continue
|
||||
Parts.append(Part)
|
||||
|
||||
return "/".join(Parts)
|
||||
|
||||
|
||||
def IsInternalPath(EntryPath: Path) -> bool:
|
||||
SaraTagConfig = Config.get("SaraTag", {})
|
||||
|
||||
|
||||
+4
-3
@@ -23,7 +23,7 @@ from Auth import (
|
||||
)
|
||||
from BookReader import ReadBookForFile
|
||||
from Blame import LoadBlame, RenameUserInBlame, UpdateBlameOnSave
|
||||
from Core import ResolvePath
|
||||
from Core import NormalizeRelativePath, ResolvePath
|
||||
from Embeds import BuildOgTags
|
||||
from Editor import WriteTextFile
|
||||
from Explorer import (
|
||||
@@ -440,10 +440,11 @@ def PatchTreeEndpoint(
|
||||
) -> dict:
|
||||
RequirePathAccessible(Root, Path, CurrentUser)
|
||||
|
||||
ParentPath = Path.rsplit("/", 1)[0] if "/" in Path else ""
|
||||
|
||||
if Body.DestinationPath:
|
||||
Destination = Body.DestinationPath
|
||||
Destination = NormalizeRelativePath(ParentPath, Body.DestinationPath)
|
||||
elif Body.NewName:
|
||||
ParentPath = Path.rsplit("/", 1)[0] if "/" in Path else ""
|
||||
Destination = f"{ParentPath}/{Body.NewName}" if ParentPath else Body.NewName
|
||||
else:
|
||||
raise HTTPException(status_code=400, detail="Provide NewName or DestinationPath.")
|
||||
|
||||
+4
-20
@@ -2,7 +2,7 @@ from __future__ import annotations
|
||||
|
||||
import re
|
||||
|
||||
from Core import ResolvePath
|
||||
from Core import NormalizeRelativePath, ResolvePath
|
||||
from Explorer import ClassifyFile, ListDirectory
|
||||
from Locks import IsPathAccessible
|
||||
from Preview import ReadTextFile
|
||||
@@ -94,22 +94,6 @@ def _ResolveLocalPortals(Content: str) -> str:
|
||||
return "".join(Output)
|
||||
|
||||
|
||||
def _NormalizeRelativePath(BaseDirectory: str, RawPath: str) -> str:
|
||||
Combined = f"{BaseDirectory}/{RawPath}" if BaseDirectory else RawPath
|
||||
Parts: list[str] = []
|
||||
|
||||
for Part in Combined.split("/"):
|
||||
if Part in ("", "."):
|
||||
continue
|
||||
if Part == "..":
|
||||
if Parts:
|
||||
Parts.pop()
|
||||
continue
|
||||
Parts.append(Part)
|
||||
|
||||
return "/".join(Parts)
|
||||
|
||||
|
||||
def _ReadAccessiblePortalSource(
|
||||
RootName: str, RelativePath: str, Username: str, UserGroups: list[str]
|
||||
) -> str | None:
|
||||
@@ -171,7 +155,7 @@ def _ResolveCrossFileReference(
|
||||
if not PathText:
|
||||
return _FindNamedPortalInDirectory(RootName, RefererDirectory, Name, Username, UserGroups)
|
||||
|
||||
TargetRelativePath = _NormalizeRelativePath(RefererDirectory, PathText)
|
||||
TargetRelativePath = NormalizeRelativePath(RefererDirectory, PathText)
|
||||
|
||||
try:
|
||||
TargetPath = ResolvePath(RootName, TargetRelativePath)
|
||||
@@ -205,7 +189,7 @@ def _ResolveWholeFilePipe(
|
||||
Username: str,
|
||||
UserGroups: list[str],
|
||||
) -> str | None:
|
||||
TargetRelativePath = _NormalizeRelativePath(RefererDirectory, PathText)
|
||||
TargetRelativePath = NormalizeRelativePath(RefererDirectory, PathText)
|
||||
Content = _ReadAccessiblePortalSource(RootName, TargetRelativePath, Username, UserGroups)
|
||||
|
||||
if Content is None:
|
||||
@@ -222,7 +206,7 @@ def _ResolveLineNumberReference(
|
||||
Username: str,
|
||||
UserGroups: list[str],
|
||||
) -> str | None:
|
||||
TargetRelativePath = _NormalizeRelativePath(RefererDirectory, PathText)
|
||||
TargetRelativePath = NormalizeRelativePath(RefererDirectory, PathText)
|
||||
Content = _ReadAccessiblePortalSource(RootName, TargetRelativePath, Username, UserGroups)
|
||||
|
||||
if Content is None:
|
||||
|
||||
@@ -7,6 +7,7 @@ import DirectoryPane from "./Components/DirectoryPane.jsx";
|
||||
import TagPanel from "./Components/TagPanel.jsx";
|
||||
import SettingsPanel from "./Components/SettingsPanel.jsx";
|
||||
import LoginScreen from "./Components/LoginScreen.jsx";
|
||||
import ErrorToast from "./Components/ErrorToast.jsx";
|
||||
import {
|
||||
EnsureDirectoryNote,
|
||||
FetchAuthStatus,
|
||||
@@ -69,6 +70,7 @@ export default function App() {
|
||||
const [SidebarKey, SetSidebarKey] = useState(0);
|
||||
const [Settings, SetSettings] = useState(null);
|
||||
const [IsSettingsOpen, SetIsSettingsOpen] = useState(false);
|
||||
const [ErrorMessage, SetErrorMessage] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
FetchAuthStatus().then((Status) => {
|
||||
@@ -237,7 +239,7 @@ export default function App() {
|
||||
const Created = await EnsureDirectoryNote(ActiveRoot, DirectoryPath);
|
||||
HandleSelectDirectoryNote(DirectoryPath, Created.Name, Created.FileType);
|
||||
} catch (Error) {
|
||||
window.alert(Error.message);
|
||||
SetErrorMessage(Error.message);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -391,6 +393,7 @@ export default function App() {
|
||||
OnProfileUpdated={HandleProfileUpdated}
|
||||
/>
|
||||
)}
|
||||
<ErrorToast Message={ErrorMessage} OnDismiss={() => SetErrorMessage(null)} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import React from "react";
|
||||
|
||||
export default function ConfirmModal({ Title, Message, ConfirmLabel, OnConfirm, OnCancel }) {
|
||||
function HandleOverlayClick(Event) {
|
||||
Event.stopPropagation();
|
||||
OnCancel();
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="SettingsOverlay" onClick={HandleOverlayClick}>
|
||||
<div className="SettingsModal" onClick={(Event) => Event.stopPropagation()}>
|
||||
<div className="SettingsHeader">
|
||||
<span>{Title || "Confirm"}</span>
|
||||
<button className="SettingsClose" onClick={OnCancel}>
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
<div className="SettingsBody">
|
||||
<div className="SettingsRow">{Message}</div>
|
||||
</div>
|
||||
<div className="SettingsFooter">
|
||||
<button onClick={OnCancel}>Cancel</button>
|
||||
<button onClick={OnConfirm}>{ConfirmLabel || "Confirm"}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -11,6 +11,9 @@ import {
|
||||
} from "../Api.js";
|
||||
import { GetFileIcon, GetFolderIcon } from "../IconStyle.js";
|
||||
import AccessModal from "./AccessModal.jsx";
|
||||
import PromptModal from "./PromptModal.jsx";
|
||||
import ConfirmModal from "./ConfirmModal.jsx";
|
||||
import ErrorToast from "./ErrorToast.jsx";
|
||||
|
||||
export default function DirectoryPane({
|
||||
RootName,
|
||||
@@ -27,6 +30,7 @@ export default function DirectoryPane({
|
||||
const [Entries, SetEntries] = useState([]);
|
||||
const [LoadError, SetLoadError] = useState(null);
|
||||
const [ModalConfig, SetModalConfig] = useState(null);
|
||||
const [ErrorMessage, SetErrorMessage] = useState(null);
|
||||
|
||||
async function Reload() {
|
||||
try {
|
||||
@@ -71,15 +75,21 @@ export default function DirectoryPane({
|
||||
InitialGroups: (Existing && Existing.Groups) || [],
|
||||
});
|
||||
} catch (Error) {
|
||||
window.alert(Error.message);
|
||||
SetErrorMessage(Error.message);
|
||||
}
|
||||
}
|
||||
|
||||
async function HandleRename(Event, Entry) {
|
||||
function HandleRenameClick(Event, Entry) {
|
||||
Event.stopPropagation();
|
||||
SetModalConfig({ Mode: "rename", Entry });
|
||||
}
|
||||
|
||||
async function HandleRenameConfirm(NewValue) {
|
||||
const Entry = ModalConfig.Entry;
|
||||
SetModalConfig(null);
|
||||
if (NewValue === Entry.Name) return;
|
||||
|
||||
const FullPath = JoinPath(Entry.Name);
|
||||
const NewValue = window.prompt("Rename to (or type a path to move):", Entry.Name);
|
||||
if (!NewValue || NewValue === Entry.Name) return;
|
||||
|
||||
try {
|
||||
if (NewValue.includes("/")) {
|
||||
@@ -91,24 +101,30 @@ export default function DirectoryPane({
|
||||
}
|
||||
await Reload();
|
||||
} catch (Error) {
|
||||
window.alert(Error.message);
|
||||
SetErrorMessage(Error.message);
|
||||
}
|
||||
}
|
||||
|
||||
async function HandleDelete(Event, Entry) {
|
||||
function HandleDeleteClick(Event, Entry) {
|
||||
Event.stopPropagation();
|
||||
const FullPath = JoinPath(Entry.Name);
|
||||
|
||||
if (ConfirmBeforeDelete && !window.confirm(`Delete "${Entry.Name}"? This can't be undone.`)) {
|
||||
return;
|
||||
if (ConfirmBeforeDelete) {
|
||||
SetModalConfig({ Mode: "confirmDelete", Entry });
|
||||
} else {
|
||||
HandleDeleteConfirm(Entry);
|
||||
}
|
||||
}
|
||||
|
||||
async function HandleDeleteConfirm(Entry) {
|
||||
SetModalConfig(null);
|
||||
const FullPath = JoinPath(Entry.Name);
|
||||
|
||||
try {
|
||||
await DeleteEntry(RootName, FullPath);
|
||||
OnEntryDeleted(FullPath, Entry.IsDirectory);
|
||||
await Reload();
|
||||
} catch (Error) {
|
||||
window.alert(Error.message);
|
||||
SetErrorMessage(Error.message);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -134,7 +150,7 @@ export default function DirectoryPane({
|
||||
|
||||
SetModalConfig(null);
|
||||
} catch (Error) {
|
||||
window.alert(Error.message);
|
||||
SetErrorMessage(Error.message);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -173,14 +189,14 @@ export default function DirectoryPane({
|
||||
</button>
|
||||
<button
|
||||
className="TreeRowActionButton"
|
||||
onClick={(Event) => HandleRename(Event, Entry)}
|
||||
onClick={(Event) => HandleRenameClick(Event, Entry)}
|
||||
title="Rename / move"
|
||||
>
|
||||
✎
|
||||
</button>
|
||||
<button
|
||||
className="TreeRowActionButton"
|
||||
onClick={(Event) => HandleDelete(Event, Entry)}
|
||||
onClick={(Event) => HandleDeleteClick(Event, Entry)}
|
||||
title="Delete"
|
||||
>
|
||||
✕
|
||||
@@ -229,6 +245,26 @@ export default function DirectoryPane({
|
||||
OnCancel={() => SetModalConfig(null)}
|
||||
/>
|
||||
)}
|
||||
{ModalConfig && ModalConfig.Mode === "rename" && (
|
||||
<PromptModal
|
||||
Title={`Rename "${ModalConfig.Entry.Name}"`}
|
||||
Label="New name (or a relative path like ../Name to move):"
|
||||
InitialValue={ModalConfig.Entry.Name}
|
||||
ConfirmLabel="Rename"
|
||||
OnConfirm={HandleRenameConfirm}
|
||||
OnCancel={() => SetModalConfig(null)}
|
||||
/>
|
||||
)}
|
||||
{ModalConfig && ModalConfig.Mode === "confirmDelete" && (
|
||||
<ConfirmModal
|
||||
Title="Delete"
|
||||
Message={`Delete "${ModalConfig.Entry.Name}"? This can't be undone.`}
|
||||
ConfirmLabel="Delete"
|
||||
OnConfirm={() => HandleDeleteConfirm(ModalConfig.Entry)}
|
||||
OnCancel={() => SetModalConfig(null)}
|
||||
/>
|
||||
)}
|
||||
<ErrorToast Message={ErrorMessage} OnDismiss={() => SetErrorMessage(null)} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import React from "react";
|
||||
|
||||
export default function ErrorToast({ Message, OnDismiss }) {
|
||||
if (!Message) return null;
|
||||
|
||||
return (
|
||||
<div className="ErrorToast">
|
||||
<span>{Message}</span>
|
||||
<button className="ErrorToastClose" onClick={OnDismiss}>
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import React, { useState } from "react";
|
||||
|
||||
export default function PromptModal({ Title, Label, InitialValue, ConfirmLabel, OnConfirm, OnCancel }) {
|
||||
const [Value, SetValue] = useState(InitialValue || "");
|
||||
|
||||
function HandleConfirm() {
|
||||
if (!Value.trim()) return;
|
||||
OnConfirm(Value.trim());
|
||||
}
|
||||
|
||||
function HandleKeyDown(Event) {
|
||||
if (Event.key === "Enter") {
|
||||
HandleConfirm();
|
||||
} else if (Event.key === "Escape") {
|
||||
OnCancel();
|
||||
}
|
||||
}
|
||||
|
||||
function HandleOverlayClick(Event) {
|
||||
Event.stopPropagation();
|
||||
OnCancel();
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="SettingsOverlay" onClick={HandleOverlayClick}>
|
||||
<div className="SettingsModal" onClick={(Event) => Event.stopPropagation()}>
|
||||
<div className="SettingsHeader">
|
||||
<span>{Title}</span>
|
||||
<button className="SettingsClose" onClick={OnCancel}>
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
<div className="SettingsBody">
|
||||
<div className="SettingsRow">
|
||||
{Label && <label className="SettingsLabel">{Label}</label>}
|
||||
<input
|
||||
type="text"
|
||||
value={Value}
|
||||
onChange={(Event) => SetValue(Event.target.value)}
|
||||
onKeyDown={HandleKeyDown}
|
||||
autoFocus
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="SettingsFooter">
|
||||
<button onClick={OnCancel}>Cancel</button>
|
||||
<button onClick={HandleConfirm}>{ConfirmLabel || "OK"}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -24,6 +24,8 @@ import {
|
||||
UploadAvatar,
|
||||
} from "../Api.js";
|
||||
import Switch from "./Switch.jsx";
|
||||
import PromptModal from "./PromptModal.jsx";
|
||||
import ConfirmModal from "./ConfirmModal.jsx";
|
||||
import { PaletteRoles } from "../ThemeRoles.js";
|
||||
|
||||
export default function SettingsPanel({
|
||||
@@ -42,6 +44,8 @@ export default function SettingsPanel({
|
||||
const [NewThemeName, SetNewThemeName] = useState("");
|
||||
const [NewThemePalette, SetNewThemePalette] = useState(null);
|
||||
const [ThemeError, SetThemeError] = useState(null);
|
||||
const [IsRenamingTheme, SetIsRenamingTheme] = useState(false);
|
||||
const [IsConfirmingThemeDelete, SetIsConfirmingThemeDelete] = useState(false);
|
||||
|
||||
const [Users, SetUsers] = useState(null);
|
||||
const [NewUsername, SetNewUsername] = useState("");
|
||||
@@ -68,6 +72,7 @@ export default function SettingsPanel({
|
||||
const [NewGroupName, SetNewGroupName] = useState("");
|
||||
const [GroupError, SetGroupError] = useState(null);
|
||||
const [ExpandedGroup, SetExpandedGroup] = useState(null);
|
||||
const [ConfirmingDeleteGroupName, SetConfirmingDeleteGroupName] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
FetchSettings().then(SetSettings);
|
||||
@@ -127,31 +132,38 @@ export default function SettingsPanel({
|
||||
}
|
||||
}
|
||||
|
||||
async function HandleRenameTheme() {
|
||||
const NewName = window.prompt("Rename theme to:", Settings.ThemeName);
|
||||
if (!NewName || !NewName.trim() || NewName.trim() === Settings.ThemeName) return;
|
||||
function HandleRenameThemeClick() {
|
||||
SetIsRenamingTheme(true);
|
||||
}
|
||||
|
||||
async function HandleRenameThemeConfirm(NewName) {
|
||||
SetIsRenamingTheme(false);
|
||||
if (NewName === Settings.ThemeName) return;
|
||||
|
||||
SetThemeError(null);
|
||||
try {
|
||||
const Palette = await FetchTheme(Settings.ThemeName);
|
||||
await SaveTheme(NewName.trim(), Palette);
|
||||
await SaveTheme(NewName, Palette);
|
||||
await DeleteTheme(Settings.ThemeName);
|
||||
const Updated = await FetchThemeNames();
|
||||
SetThemeNames(Updated);
|
||||
UpdateField("ThemeName", NewName.trim());
|
||||
UpdateField("ThemeName", NewName);
|
||||
} catch (Error) {
|
||||
SetThemeError(Error.message);
|
||||
}
|
||||
}
|
||||
|
||||
async function HandleDeleteTheme() {
|
||||
function HandleDeleteThemeClick() {
|
||||
if (ThemeNames.length <= 1) {
|
||||
SetThemeError("Can't delete the only remaining theme.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!window.confirm(`Delete theme "${Settings.ThemeName}"? This can't be undone.`)) return;
|
||||
SetIsConfirmingThemeDelete(true);
|
||||
}
|
||||
|
||||
async function HandleDeleteThemeConfirm() {
|
||||
SetIsConfirmingThemeDelete(false);
|
||||
SetThemeError(null);
|
||||
try {
|
||||
await DeleteTheme(Settings.ThemeName);
|
||||
@@ -260,8 +272,13 @@ export default function SettingsPanel({
|
||||
}
|
||||
}
|
||||
|
||||
async function HandleDeleteGroup(Name) {
|
||||
if (!window.confirm(`Delete group "${Name}"? This can't be undone.`)) return;
|
||||
function HandleDeleteGroupClick(Name) {
|
||||
SetConfirmingDeleteGroupName(Name);
|
||||
}
|
||||
|
||||
async function HandleDeleteGroupConfirm() {
|
||||
const Name = ConfirmingDeleteGroupName;
|
||||
SetConfirmingDeleteGroupName(null);
|
||||
|
||||
SetGroupError(null);
|
||||
try {
|
||||
@@ -392,8 +409,8 @@ export default function SettingsPanel({
|
||||
))}
|
||||
</select>
|
||||
<div className="SettingsRowInline">
|
||||
<button onClick={HandleRenameTheme}>Rename</button>
|
||||
<button onClick={HandleDeleteTheme}>Delete</button>
|
||||
<button onClick={HandleRenameThemeClick}>Rename</button>
|
||||
<button onClick={HandleDeleteThemeClick}>Delete</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -610,8 +627,8 @@ export default function SettingsPanel({
|
||||
>
|
||||
{ExpandedGroup === Group.Name ? "Hide" : "Members"}
|
||||
</button>
|
||||
{Group.Name !== "Admin" && (
|
||||
<button onClick={() => HandleDeleteGroup(Group.Name)}>Delete</button>
|
||||
{Group.Name !== "Admin" && Group.Name !== "Guest" && (
|
||||
<button onClick={() => HandleDeleteGroupClick(Group.Name)}>Delete</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
@@ -658,6 +675,35 @@ export default function SettingsPanel({
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{IsRenamingTheme && (
|
||||
<PromptModal
|
||||
Title="Rename theme"
|
||||
Label="New name:"
|
||||
InitialValue={Settings.ThemeName}
|
||||
ConfirmLabel="Rename"
|
||||
OnConfirm={HandleRenameThemeConfirm}
|
||||
OnCancel={() => SetIsRenamingTheme(false)}
|
||||
/>
|
||||
)}
|
||||
{IsConfirmingThemeDelete && (
|
||||
<ConfirmModal
|
||||
Title="Delete theme"
|
||||
Message={`Delete theme "${Settings.ThemeName}"? This can't be undone.`}
|
||||
ConfirmLabel="Delete"
|
||||
OnConfirm={HandleDeleteThemeConfirm}
|
||||
OnCancel={() => SetIsConfirmingThemeDelete(false)}
|
||||
/>
|
||||
)}
|
||||
{ConfirmingDeleteGroupName && (
|
||||
<ConfirmModal
|
||||
Title="Delete group"
|
||||
Message={`Delete group "${ConfirmingDeleteGroupName}"? This can't be undone.`}
|
||||
ConfirmLabel="Delete"
|
||||
OnConfirm={HandleDeleteGroupConfirm}
|
||||
OnCancel={() => SetConfirmingDeleteGroupName(null)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -12,6 +12,9 @@ import {
|
||||
} from "../Api.js";
|
||||
import { GetFileIcon, GetFolderIcon } from "../IconStyle.js";
|
||||
import AccessModal from "./AccessModal.jsx";
|
||||
import PromptModal from "./PromptModal.jsx";
|
||||
import ConfirmModal from "./ConfirmModal.jsx";
|
||||
import ErrorToast from "./ErrorToast.jsx";
|
||||
|
||||
function TreeNode({
|
||||
RootName,
|
||||
@@ -34,6 +37,9 @@ function TreeNode({
|
||||
}) {
|
||||
const [IsExpanded, SetIsExpanded] = useState(false);
|
||||
const [Children, SetChildren] = useState(null);
|
||||
const [IsRenaming, SetIsRenaming] = useState(false);
|
||||
const [IsConfirmingDelete, SetIsConfirmingDelete] = useState(false);
|
||||
const [ErrorMessage, SetErrorMessage] = useState(null);
|
||||
const FullPath = ParentPath ? `${ParentPath}/${Entry.Name}` : Entry.Name;
|
||||
const IsSelected = SelectedFile === FullPath;
|
||||
const IsCurrentDirectory = Entry.IsDirectory && CurrentDirectoryPath === FullPath;
|
||||
@@ -82,10 +88,14 @@ function TreeNode({
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [CurrentDirectoryPath]);
|
||||
|
||||
async function HandleRename(Event) {
|
||||
function HandleRenameClick(Event) {
|
||||
Event.stopPropagation();
|
||||
const NewValue = window.prompt("Rename to (or type a path to move):", Entry.Name);
|
||||
if (!NewValue || NewValue === Entry.Name) return;
|
||||
SetIsRenaming(true);
|
||||
}
|
||||
|
||||
async function HandleRenameConfirm(NewValue) {
|
||||
SetIsRenaming(false);
|
||||
if (NewValue === Entry.Name) return;
|
||||
|
||||
try {
|
||||
if (NewValue.includes("/")) {
|
||||
@@ -97,22 +107,28 @@ function TreeNode({
|
||||
OnEntryRenamed(FullPath, NewPath, Entry.IsDirectory);
|
||||
}
|
||||
} catch (Error) {
|
||||
window.alert(Error.message);
|
||||
SetErrorMessage(Error.message);
|
||||
}
|
||||
}
|
||||
|
||||
async function HandleDelete(Event) {
|
||||
function HandleDeleteClick(Event) {
|
||||
Event.stopPropagation();
|
||||
|
||||
if (ConfirmBeforeDelete && !window.confirm(`Delete "${Entry.Name}"? This can't be undone.`)) {
|
||||
return;
|
||||
if (ConfirmBeforeDelete) {
|
||||
SetIsConfirmingDelete(true);
|
||||
} else {
|
||||
HandleDeleteConfirm();
|
||||
}
|
||||
}
|
||||
|
||||
async function HandleDeleteConfirm() {
|
||||
SetIsConfirmingDelete(false);
|
||||
|
||||
try {
|
||||
await DeleteEntry(RootName, FullPath);
|
||||
OnEntryDeleted(FullPath, Entry.IsDirectory);
|
||||
} catch (Error) {
|
||||
window.alert(Error.message);
|
||||
SetErrorMessage(Error.message);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -140,15 +156,35 @@ function TreeNode({
|
||||
<button className="TreeRowActionButton" onClick={HandleLockClick} title="Access control">
|
||||
🔒
|
||||
</button>
|
||||
<button className="TreeRowActionButton" onClick={HandleRename} title="Rename / move">
|
||||
<button className="TreeRowActionButton" onClick={HandleRenameClick} title="Rename / move">
|
||||
✎
|
||||
</button>
|
||||
<button className="TreeRowActionButton" onClick={HandleDelete} title="Delete">
|
||||
<button className="TreeRowActionButton" onClick={HandleDeleteClick} title="Delete">
|
||||
✕
|
||||
</button>
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{IsRenaming && (
|
||||
<PromptModal
|
||||
Title={`Rename "${Entry.Name}"`}
|
||||
Label="New name (or a relative path like ../Name to move):"
|
||||
InitialValue={Entry.Name}
|
||||
ConfirmLabel="Rename"
|
||||
OnConfirm={HandleRenameConfirm}
|
||||
OnCancel={() => SetIsRenaming(false)}
|
||||
/>
|
||||
)}
|
||||
{IsConfirmingDelete && (
|
||||
<ConfirmModal
|
||||
Title="Delete"
|
||||
Message={`Delete "${Entry.Name}"? This can't be undone.`}
|
||||
ConfirmLabel="Delete"
|
||||
OnConfirm={HandleDeleteConfirm}
|
||||
OnCancel={() => SetIsConfirmingDelete(false)}
|
||||
/>
|
||||
)}
|
||||
<ErrorToast Message={ErrorMessage} OnDismiss={() => SetErrorMessage(null)} />
|
||||
{IsExpanded && Children && (
|
||||
<div className="TreeChildren">
|
||||
{Children.map((Child) => (
|
||||
@@ -247,6 +283,7 @@ export default function Sidebar({
|
||||
const [RootEntries, SetRootEntries] = useState([]);
|
||||
const [LoadError, SetLoadError] = useState(null);
|
||||
const [ModalConfig, SetModalConfig] = useState(null);
|
||||
const [ErrorMessage, SetErrorMessage] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (RootName) {
|
||||
@@ -280,7 +317,7 @@ export default function Sidebar({
|
||||
InitialGroups: (Existing && Existing.Groups) || [],
|
||||
});
|
||||
} catch (Error) {
|
||||
window.alert(Error.message);
|
||||
SetErrorMessage(Error.message);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -304,7 +341,7 @@ export default function Sidebar({
|
||||
|
||||
SetModalConfig(null);
|
||||
} catch (Error) {
|
||||
window.alert(Error.message);
|
||||
SetErrorMessage(Error.message);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -396,6 +433,7 @@ export default function Sidebar({
|
||||
OnCancel={() => SetModalConfig(null)}
|
||||
/>
|
||||
)}
|
||||
<ErrorToast Message={ErrorMessage} OnDismiss={() => SetErrorMessage(null)} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -784,6 +784,38 @@ img,
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.ErrorToast {
|
||||
position: fixed;
|
||||
bottom: calc(20px + env(safe-area-inset-bottom, 0px));
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
max-width: 480px;
|
||||
padding: 10px 14px;
|
||||
background: var(--Surface);
|
||||
border: 1px solid var(--Red);
|
||||
border-radius: 6px;
|
||||
color: var(--Text);
|
||||
font-size: 13px;
|
||||
z-index: 200;
|
||||
}
|
||||
|
||||
.ErrorToastClose {
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--TextMuted);
|
||||
cursor: pointer;
|
||||
font-family: inherit;
|
||||
font-size: 13px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.ErrorToastClose:hover {
|
||||
color: var(--Text);
|
||||
}
|
||||
|
||||
.SettingsHint {
|
||||
color: var(--TextMuted);
|
||||
font-size: 12px;
|
||||
|
||||
Reference in new issue
Block a user