Added groups and restricted access.

This commit is contained in:
sarah committed 2026-09-21 06:52:31 +02:00
1 parent 8aa223bc7b
commit 9a5b9530a3
17 files changed
+1406 -66

No files matched your search

+72
View File
@@ -0,0 +1,72 @@
import React, { useState } from "react";
import AccessPicker from "./AccessPicker.jsx";
export default function AccessModal({
Title,
ShowNameField,
InitialName,
NamePlaceholder,
InitialUsers,
InitialGroups,
ConfirmLabel,
OnConfirm,
OnCancel,
}) {
const [Name, SetName] = useState(InitialName || "");
const [Users, SetUsers] = useState(InitialUsers || []);
const [Groups, SetGroups] = useState(InitialGroups || []);
function HandleConfirm() {
OnConfirm(Name, Users, Groups);
}
function HandleRemoveRestriction() {
OnConfirm(Name, [], []);
}
const HasRestriction = (InitialUsers && InitialUsers.length > 0) || (InitialGroups && InitialGroups.length > 0);
return (
<div className="SettingsOverlay" onClick={OnCancel}>
<div className="SettingsModal" onClick={(Event) => Event.stopPropagation()}>
<div className="SettingsHeader">
<span>{Title}</span>
<button className="SettingsClose" onClick={OnCancel}>
✕
</button>
</div>
<div className="SettingsBody">
{ShowNameField && (
<div className="SettingsRow">
<label className="SettingsLabel">Name</label>
<input
type="text"
value={Name}
onChange={(Event) => SetName(Event.target.value)}
placeholder={NamePlaceholder}
autoFocus
/>
</div>
)}
<div className="SettingsRow">
<label className="SettingsLabel">Restrict access to (leave empty for everyone)</label>
<AccessPicker
SelectedUsers={Users}
SelectedGroups={Groups}
OnChangeUsers={SetUsers}
OnChangeGroups={SetGroups}
/>
</div>
</div>
<div className="SettingsFooter">
{!ShowNameField && HasRestriction && (
<button onClick={HandleRemoveRestriction}>Remove restriction</button>
)}
<button onClick={OnCancel}>Cancel</button>
<button onClick={HandleConfirm}>{ConfirmLabel}</button>
</div>
</div>
</div>
);
}
+75
View File
@@ -0,0 +1,75 @@
import React, { useEffect, useState } from "react";
import { FetchGroups, FetchUsers } from "../Api.js";
export default function AccessPicker({ SelectedUsers, SelectedGroups, OnChangeUsers, OnChangeGroups }) {
const [AllUsers, SetAllUsers] = useState([]);
const [AllGroups, SetAllGroups] = useState([]);
const [Query, SetQuery] = useState("");
useEffect(() => {
FetchUsers().then(SetAllUsers);
FetchGroups().then(SetAllGroups);
}, []);
function ToggleUser(Username) {
if (SelectedUsers.includes(Username)) {
OnChangeUsers(SelectedUsers.filter((U) => U !== Username));
} else {
OnChangeUsers([...SelectedUsers, Username]);
}
}
function ToggleGroup(Name) {
if (SelectedGroups.includes(Name)) {
OnChangeGroups(SelectedGroups.filter((G) => G !== Name));
} else {
OnChangeGroups([...SelectedGroups, Name]);
}
}
const LowerQuery = Query.toLowerCase();
const FilteredGroups = AllGroups.filter((Group) => Group.Name.toLowerCase().includes(LowerQuery));
const FilteredUsers = AllUsers.filter((User) => User.Username.toLowerCase().includes(LowerQuery));
return (
<div className="AccessPicker">
<input
type="text"
placeholder="Search users or groups…"
value={Query}
onChange={(Event) => SetQuery(Event.target.value)}
/>
<div className="AccessPickerList">
{FilteredGroups.map((Group) => (
<label className="AccessPickerRow" key={`group:${Group.Name}`}>
<input
type="checkbox"
checked={SelectedGroups.includes(Group.Name)}
onChange={() => ToggleGroup(Group.Name)}
/>
<span className="AccessPickerType">Group</span>
<span>{Group.Name}</span>
</label>
))}
{FilteredUsers.map((User) => (
<label className="AccessPickerRow" key={`user:${User.Username}`}>
<input
type="checkbox"
checked={SelectedUsers.includes(User.Username)}
onChange={() => ToggleUser(User.Username)}
/>
<span
className="ColourSwatchSmall"
style={{ background: `var(--${User.Colour || "Purple"})` }}
/>
<span>{User.Username}</span>
</label>
))}
{FilteredGroups.length === 0 && FilteredUsers.length === 0 && (
<div className="AccessPickerEmpty">No matches.</div>
)}
</div>
</div>
);
}
+100 -21
View File
@@ -1,7 +1,16 @@
import React, { useEffect, useState } from "react";
import { CreateEntry, DeleteEntry, FetchTree, MoveEntry, RenameEntry } from "../Api.js";
import {
CreateEntry,
DeleteEntry,
FetchLock,
FetchTree,
MoveEntry,
RenameEntry,
SetLock as SetLockApi,
} from "../Api.js";
import { GetFileIcon, GetFolderIcon } from "../IconStyle.js";
import AccessModal from "./AccessModal.jsx";
export default function DirectoryPane({
RootName,
@@ -16,6 +25,7 @@ export default function DirectoryPane({
}) {
const [Entries, SetEntries] = useState([]);
const [LoadError, SetLoadError] = useState(null);
const [ModalConfig, SetModalConfig] = useState(null);
async function Reload() {
try {
@@ -38,29 +48,29 @@ export default function DirectoryPane({
return DirectoryPath ? `${DirectoryPath}/${Name}` : Name;
}
async function HandleCreateFile() {
const Name = window.prompt("New file name (e.g. Notes.md):");
if (!Name) return;
try {
await CreateEntry(RootName, JoinPath(Name), false);
await Reload();
OnEntryCreated();
} catch (Error) {
SetLoadError(Error.message);
}
function OpenCreateFileModal() {
SetModalConfig({ Mode: "createFile" });
}
async function HandleCreateDirectory() {
const Name = window.prompt("New directory name:");
if (!Name) return;
function OpenCreateDirectoryModal() {
SetModalConfig({ Mode: "createDirectory" });
}
async function HandleRequestLockEdit(Event, Entry) {
Event.stopPropagation();
const FullPath = JoinPath(Entry.Name);
try {
await CreateEntry(RootName, JoinPath(Name), true);
await Reload();
OnEntryCreated();
const Existing = await FetchLock(RootName, FullPath);
SetModalConfig({
Mode: "editLock",
TargetPath: FullPath,
EntryName: Entry.Name,
InitialUsers: (Existing && Existing.Users) || [],
InitialGroups: (Existing && Existing.Groups) || [],
});
} catch (Error) {
SetLoadError(Error.message);
window.alert(Error.message);
}
}
@@ -101,11 +111,37 @@ export default function DirectoryPane({
}
}
async function HandleModalConfirm(Name, Users, Groups) {
try {
if (ModalConfig.Mode === "createFile" || ModalConfig.Mode === "createDirectory") {
if (!Name || !Name.trim()) return;
const IsDirectory = ModalConfig.Mode === "createDirectory";
const FullPath = JoinPath(Name.trim());
await CreateEntry(RootName, FullPath, IsDirectory);
if (Users.length > 0 || Groups.length > 0) {
await SetLockApi(RootName, FullPath, Users, Groups);
}
await Reload();
OnEntryCreated();
} else if (ModalConfig.Mode === "editLock") {
await SetLockApi(RootName, ModalConfig.TargetPath, Users, Groups);
await Reload();
}
SetModalConfig(null);
} catch (Error) {
window.alert(Error.message);
}
}
return (
<div className="DirectoryPane">
<div className="PreviewToolbar">
<button onClick={HandleCreateFile}>New File</button>
<button onClick={HandleCreateDirectory}>New Directory</button>
<button onClick={OpenCreateFileModal}>New File</button>
<button onClick={OpenCreateDirectoryModal}>New Directory</button>
</div>
{LoadError && <div className="SidebarError">{LoadError}</div>}
<div className="DirectoryList">
@@ -122,6 +158,13 @@ export default function DirectoryPane({
</span>
<span className="TreeLabel">{Entry.Name}</span>
<span className="TreeRowActions">
<button
className="TreeRowActionButton"
onClick={(Event) => HandleRequestLockEdit(Event, Entry)}
title="Access control"
>
🔒
</button>
<button
className="TreeRowActionButton"
onClick={(Event) => HandleRename(Event, Entry)}
@@ -143,6 +186,42 @@ export default function DirectoryPane({
<div className="DirectoryEmpty">Empty directory.</div>
)}
</div>
{ModalConfig && ModalConfig.Mode === "createFile" && (
<AccessModal
Title={`New file in ${DirectoryPath || "root"}`}
ShowNameField
NamePlaceholder="Notes.md"
InitialUsers={[]}
InitialGroups={[]}
ConfirmLabel="Create"
OnConfirm={HandleModalConfirm}
OnCancel={() => SetModalConfig(null)}
/>
)}
{ModalConfig && ModalConfig.Mode === "createDirectory" && (
<AccessModal
Title={`New directory in ${DirectoryPath || "root"}`}
ShowNameField
NamePlaceholder="MyFolder"
InitialUsers={[]}
InitialGroups={[]}
ConfirmLabel="Create"
OnConfirm={HandleModalConfirm}
OnCancel={() => SetModalConfig(null)}
/>
)}
{ModalConfig && ModalConfig.Mode === "editLock" && (
<AccessModal
Title={`Access for "${ModalConfig.EntryName}"`}
ShowNameField={false}
InitialUsers={ModalConfig.InitialUsers}
InitialGroups={ModalConfig.InitialGroups}
ConfirmLabel="Save"
OnConfirm={HandleModalConfirm}
OnCancel={() => SetModalConfig(null)}
/>
)}
</div>
);
}
+39 -10
View File
@@ -7,7 +7,7 @@ import { json } from "@codemirror/lang-json";
import MarkdownIt from "markdown-it";
import MarkdownItTaskLists from "markdown-it-task-lists";
import { FetchBlame, FetchFile, GetFileUrl, GetRawFileUrl, SaveFile } from "../Api.js";
import { FetchBlame, FetchFile, GetAvatarUrl, GetFileUrl, GetRawFileUrl, SaveFile } from "../Api.js";
const MarkdownRenderer = new MarkdownIt({ breaks: true }).use(MarkdownItTaskLists, {
enabled: true,
@@ -90,26 +90,45 @@ function GetLanguageExtension(FileType) {
}
class BlameMarker extends GutterMarker {
constructor(ShortLabel, Title) {
constructor(Mode, Content, Colour, Title) {
super();
this.ShortLabel = ShortLabel;
this.Mode = Mode;
this.Content = Content;
this.Colour = Colour;
this.Title = Title;
}
eq(Other) {
return this.ShortLabel === Other.ShortLabel && this.Title === Other.Title;
return (
this.Mode === Other.Mode &&
this.Content === Other.Content &&
this.Colour === Other.Colour &&
this.Title === Other.Title
);
}
toDOM() {
if (this.Mode === "avatar") {
const Image = document.createElement("img");
Image.className = "BlameGutterAvatar";
Image.src = this.Content;
Image.title = this.Title;
Image.alt = "";
return Image;
}
const Element = document.createElement("span");
Element.className = "BlameGutterMarker";
Element.textContent = this.ShortLabel;
Element.className = `BlameGutterMarker${this.Mode === "hidden" ? " BlameGutterHidden" : ""}`;
Element.textContent = this.Content;
Element.title = this.Title;
if (this.Colour) {
Element.style.color = `var(--${this.Colour})`;
}
return Element;
}
}
function BuildBlameGutter(BlameMap) {
function BuildBlameGutter(BlameMap, ShowAvatars) {
return gutter({
class: "BlameGutter",
lineMarker(view, line) {
@@ -117,9 +136,18 @@ function BuildBlameGutter(BlameMap) {
const Entry = BlameMap[String(LineNumber)];
if (!Entry) return null;
const ShortLabel = Entry.Username.slice(0, 6);
if (Entry.Hidden) {
const Title = Entry.Timestamp ? new Date(Entry.Timestamp).toLocaleString() : "";
return new BlameMarker("hidden", "•••", null, Title);
}
const Title = `${Entry.Username} — ${new Date(Entry.Timestamp).toLocaleString()}`;
return new BlameMarker(ShortLabel, Title);
if (ShowAvatars && Entry.HasAvatar) {
return new BlameMarker("avatar", GetAvatarUrl(Entry.Username), null, Title);
}
return new BlameMarker("text", Entry.Username.slice(0, 6), Entry.Colour, Title);
},
});
}
@@ -130,6 +158,7 @@ export default function PreviewPane({
FileType,
OnNavigateLink,
ShowGitBlame,
ShowBlameAvatars,
StickyToolbar,
}) {
const [Content, SetContent] = useState("");
@@ -271,7 +300,7 @@ export default function PreviewPane({
height="100%"
extensions={[
...GetLanguageExtension(FileType),
...(ShowGitBlame ? [BuildBlameGutter(BlameMap)] : []),
...(ShowGitBlame ? [BuildBlameGutter(BlameMap, ShowBlameAvatars)] : []),
]}
onChange={SetContent}
theme="dark"
+234
View File
@@ -1,8 +1,13 @@
import React, { useEffect, useState } from "react";
import {
ChangePassword,
CreateGroupApi,
CreateUserAccount,
DeleteGroupApi,
DeleteTheme,
FetchGroups,
FetchLocks,
FetchProfileColourOptions,
FetchSettings,
FetchTheme,
@@ -10,9 +15,12 @@ import {
FetchUsers,
GetAvatarUrl,
Logout,
RemoveLock,
SaveSettings,
SaveTheme,
SetGroupMembersApi,
UpdateProfileColour,
UpdateUsername,
UploadAvatar,
} from "../Api.js";
import Switch from "./Switch.jsx";
@@ -46,12 +54,32 @@ export default function SettingsPanel({
const [ProfileError, SetProfileError] = useState(null);
const [AvatarNonce, SetAvatarNonce] = useState(0);
const [UsernameField, SetUsernameField] = useState("");
const [UsernameError, SetUsernameError] = useState(null);
const [CurrentPasswordField, SetCurrentPasswordField] = useState("");
const [NewPasswordField, SetNewPasswordField] = useState("");
const [PasswordError, SetPasswordError] = useState(null);
const [PasswordChanged, SetPasswordChanged] = useState(false);
const [Locks, SetLocksList] = useState(null);
const [LockError, SetLockError] = useState(null);
const [Groups, SetGroupsList] = useState(null);
const [NewGroupName, SetNewGroupName] = useState("");
const [GroupError, SetGroupError] = useState(null);
const [ExpandedGroup, SetExpandedGroup] = useState(null);
useEffect(() => {
FetchSettings().then(SetSettings);
FetchThemeNames().then(SetThemeNames);
FetchProfileColourOptions().then(SetColourOptions);
if (CurrentUser) {
SetUsernameField(CurrentUser.Username);
}
if (CurrentUser && CurrentUser.IsAdmin) {
FetchUsers().then(SetUsers);
FetchLocks().then(SetLocksList);
FetchGroups().then(SetGroupsList);
}
}, [CurrentUser]);
@@ -181,6 +209,86 @@ export default function SettingsPanel({
}
}
async function HandleChangeUsername() {
SetUsernameError(null);
if (!UsernameField.trim() || UsernameField.trim() === CurrentUser.Username) return;
try {
const Updated = await UpdateUsername(UsernameField.trim());
OnProfileUpdated(Updated);
} catch (Error) {
SetUsernameError(Error.message);
}
}
async function HandleChangePassword() {
SetPasswordError(null);
SetPasswordChanged(false);
try {
await ChangePassword(CurrentPasswordField, NewPasswordField);
SetCurrentPasswordField("");
SetNewPasswordField("");
SetPasswordChanged(true);
} catch (Error) {
SetPasswordError(Error.message);
}
}
async function HandleUnlock(Lock) {
SetLockError(null);
try {
await RemoveLock(Lock.Root, Lock.Path);
const Updated = await FetchLocks();
SetLocksList(Updated);
} catch (Error) {
SetLockError(Error.message);
}
}
async function HandleCreateGroup() {
SetGroupError(null);
try {
await CreateGroupApi(NewGroupName.trim());
const Updated = await FetchGroups();
SetGroupsList(Updated);
SetNewGroupName("");
} catch (Error) {
SetGroupError(Error.message);
}
}
async function HandleDeleteGroup(Name) {
if (!window.confirm(`Delete group "${Name}"? This can't be undone.`)) return;
SetGroupError(null);
try {
await DeleteGroupApi(Name);
const Updated = await FetchGroups();
SetGroupsList(Updated);
} catch (Error) {
SetGroupError(Error.message);
}
}
async function HandleToggleGroupMember(Group, Username) {
const CurrentMembers = Group.Members || [];
const NewMembers = CurrentMembers.includes(Username)
? CurrentMembers.filter((Member) => Member !== Username)
: [...CurrentMembers, Username];
SetGroupError(null);
try {
await SetGroupMembersApi(Group.Name, NewMembers);
const Updated = await FetchGroups();
SetGroupsList(Updated);
} catch (Error) {
SetGroupError(Error.message);
}
}
return (
<div className="SettingsOverlay" onClick={OnClose}>
<div className="SettingsModal" onClick={(Event) => Event.stopPropagation()}>
@@ -245,6 +353,16 @@ export default function SettingsPanel({
<Switch Checked={Settings.ShowGitBlame} OnChange={(Event) => UpdateField("ShowGitBlame", Event.target.checked)} />
</div>
<div className="SettingsRowInline">
<label className="SettingsLabel">Show avatars instead of names in blame</label>
<Switch Checked={Settings.ShowBlameAvatars} OnChange={(Event) => UpdateField("ShowBlameAvatars", Event.target.checked)} />
</div>
<div className="SettingsRowInline">
<label className="SettingsLabel">Show my edits in others' blame view</label>
<Switch Checked={Settings.VisibleInBlame} OnChange={(Event) => UpdateField("VisibleInBlame", Event.target.checked)} />
</div>
<div className="SettingsRowInline">
<label className="SettingsLabel">Sticky edit/view/save toolbar</label>
<Switch Checked={Settings.StickyToolbar} OnChange={(Event) => UpdateField("StickyToolbar", Event.target.checked)} />
@@ -352,6 +470,42 @@ export default function SettingsPanel({
</span>
<button onClick={HandleLogout}>Log out</button>
</div>
<div className="SettingsRow">
<label className="SettingsLabel">Username</label>
<div className="SettingsRowInline">
<input
type="text"
value={UsernameField}
onChange={(Event) => SetUsernameField(Event.target.value)}
/>
<button onClick={HandleChangeUsername}>Save</button>
</div>
{UsernameError && <div className="SettingsSaveError">{UsernameError}</div>}
<div className="SettingsHint">
Renaming logs out any other devices/browsers you're signed in on — this one
stays signed in under the new name.
</div>
</div>
<div className="SettingsRow">
<label className="SettingsLabel">Change password</label>
<input
type="password"
placeholder="Current password"
value={CurrentPasswordField}
onChange={(Event) => SetCurrentPasswordField(Event.target.value)}
/>
<input
type="password"
placeholder="New password"
value={NewPasswordField}
onChange={(Event) => SetNewPasswordField(Event.target.value)}
/>
<button onClick={HandleChangePassword}>Update password</button>
{PasswordChanged && <div className="SettingsHint">Password updated.</div>}
{PasswordError && <div className="SettingsSaveError">{PasswordError}</div>}
</div>
</div>
{CurrentUser && CurrentUser.IsAdmin && (
@@ -409,6 +563,86 @@ export default function SettingsPanel({
</div>
)}
{CurrentUser && CurrentUser.IsAdmin && (
<div className="SettingsSection">
<div className="SettingsSectionTitle">Locked Directories (admin)</div>
{Locks && Locks.length === 0 && (
<div className="SettingsHint">
Nothing locked yet — use the 🔒 button next to any directory to lock it.
</div>
)}
{Locks &&
Locks.map((Lock, Index) => (
<div className="SettingsRowInline" key={`${Lock.Root}:${Lock.Path}:${Index}`}>
<span className="SettingsLabel">
{Lock.Root}/{Lock.Path} —{" "}
{[...(Lock.Users || []), ...(Lock.Groups || []).map((G) => `@${G}`)].join(
", "
)}
</span>
<button onClick={() => HandleUnlock(Lock)}>Unlock</button>
</div>
))}
{LockError && <div className="SettingsSaveError">{LockError}</div>}
</div>
)}
{CurrentUser && CurrentUser.IsAdmin && (
<div className="SettingsSection">
<div className="SettingsSectionTitle">Groups (admin)</div>
{Groups &&
Groups.map((Group) => (
<div key={Group.Name} className="GroupRow">
<div className="SettingsRowInline">
<span className="SettingsLabel">
{Group.Name} ({(Group.Members || []).length})
</span>
<div className="SettingsRowInline">
<button
onClick={() =>
SetExpandedGroup(ExpandedGroup === Group.Name ? null : Group.Name)
}
>
{ExpandedGroup === Group.Name ? "Hide" : "Members"}
</button>
{Group.Name !== "Admin" && (
<button onClick={() => HandleDeleteGroup(Group.Name)}>Delete</button>
)}
</div>
</div>
{ExpandedGroup === Group.Name && (
<div className="AccessPickerList GroupMemberList">
{(Users || []).map((User) => (
<label className="AccessPickerRow" key={User.Username}>
<input
type="checkbox"
checked={(Group.Members || []).includes(User.Username)}
onChange={() => HandleToggleGroupMember(Group, User.Username)}
/>
<span
className="ColourSwatchSmall"
style={{ background: `var(--${User.Colour || "Purple"})` }}
/>
<span>{User.Username}</span>
</label>
))}
</div>
)}
</div>
))}
<div className="SettingsRowInline">
<input
type="text"
placeholder="New group name"
value={NewGroupName}
onChange={(Event) => SetNewGroupName(Event.target.value)}
/>
<button onClick={HandleCreateGroup}>Create group</button>
</div>
{GroupError && <div className="SettingsSaveError">{GroupError}</div>}
</div>
)}
{SaveError && <div className="SettingsSaveError">{SaveError}</div>}
</div>
+107 -25
View File
@@ -1,7 +1,17 @@
import React, { useEffect, useState } from "react";
import { CreateEntry, DeleteEntry, FetchTree, GetAvatarUrl, MoveEntry, RenameEntry } from "../Api.js";
import {
CreateEntry,
DeleteEntry,
FetchLock,
FetchTree,
GetAvatarUrl,
MoveEntry,
RenameEntry,
SetLock as SetLockApi,
} from "../Api.js";
import { GetFileIcon, GetFolderIcon } from "../IconStyle.js";
import AccessModal from "./AccessModal.jsx";
function TreeNode({
RootName,
@@ -14,6 +24,7 @@ function TreeNode({
OnBrowseDirectory,
OnEntryRenamed,
OnEntryDeleted,
OnRequestLockEdit,
UseHyphenBullets,
UseAsciiFolderIcons,
NavigateIntoDirectories,
@@ -103,6 +114,11 @@ function TreeNode({
}
}
function HandleLockClick(Event) {
Event.stopPropagation();
OnRequestLockEdit(FullPath, Entry.Name);
}
return (
<div className="TreeNode">
<div
@@ -118,6 +134,9 @@ function TreeNode({
</span>
<span className="TreeLabel">{Entry.Name}</span>
<span className="TreeRowActions">
<button className="TreeRowActionButton" onClick={HandleLockClick} title="Access control">
🔒
</button>
<button className="TreeRowActionButton" onClick={HandleRename} title="Rename / move">
✎
</button>
@@ -141,6 +160,7 @@ function TreeNode({
OnBrowseDirectory={OnBrowseDirectory}
OnEntryRenamed={OnEntryRenamed}
OnEntryDeleted={OnEntryDeleted}
OnRequestLockEdit={OnRequestLockEdit}
UseHyphenBullets={UseHyphenBullets}
UseAsciiFolderIcons={UseAsciiFolderIcons}
NavigateIntoDirectories={NavigateIntoDirectories}
@@ -207,6 +227,7 @@ export default function Sidebar({
}) {
const [RootEntries, SetRootEntries] = useState([]);
const [LoadError, SetLoadError] = useState(null);
const [ModalConfig, SetModalConfig] = useState(null);
useEffect(() => {
if (RootName) {
@@ -221,39 +242,53 @@ export default function Sidebar({
return CurrentDirectoryPath ? `${CurrentDirectoryPath}/${Name}` : Name;
}
async function HandleCreateFile() {
const Name = window.prompt(
`New file name in ${CurrentDirectoryPath || "root"} (e.g. Notes.md):`
);
if (!Name) return;
function OpenCreateFileModal() {
SetModalConfig({ Mode: "createFile" });
}
function OpenCreateDirectoryModal() {
SetModalConfig({ Mode: "createDirectory" });
}
async function HandleRequestLockEdit(FullPath, EntryName) {
try {
await CreateEntry(RootName, JoinPath(Name), false);
OnEntryCreated();
const Existing = await FetchLock(RootName, FullPath);
SetModalConfig({
Mode: "editLock",
TargetPath: FullPath,
EntryName,
InitialUsers: (Existing && Existing.Users) || [],
InitialGroups: (Existing && Existing.Groups) || [],
});
} catch (Error) {
SetLoadError(Error.message);
window.alert(Error.message);
}
}
async function HandleCreateDirectory() {
const Name = window.prompt(`New directory name in ${CurrentDirectoryPath || "root"}:`);
if (!Name) return;
async function HandleModalConfirm(Name, Users, Groups) {
try {
await CreateEntry(RootName, JoinPath(Name), true);
OnEntryCreated();
if (ModalConfig.Mode === "createFile" || ModalConfig.Mode === "createDirectory") {
if (!Name || !Name.trim()) return;
const IsDirectory = ModalConfig.Mode === "createDirectory";
const FullPath = JoinPath(Name.trim());
await CreateEntry(RootName, FullPath, IsDirectory);
if (Users.length > 0 || Groups.length > 0) {
await SetLockApi(RootName, FullPath, Users, Groups);
}
OnEntryCreated();
} else if (ModalConfig.Mode === "editLock") {
await SetLockApi(RootName, ModalConfig.TargetPath, Users, Groups);
}
SetModalConfig(null);
} catch (Error) {
SetLoadError(Error.message);
window.alert(Error.message);
}
}
const NewEntryButtons = (
<div className={`SidebarNewEntry${NewEntryButtonsAtTop ? " SidebarNewEntryTop" : ""}`}>
<button onClick={HandleCreateFile}>New File</button>
<button onClick={HandleCreateDirectory}>New Directory</button>
</div>
);
return (
<div className="Sidebar">
<div className="SidebarHeader">
@@ -267,7 +302,12 @@ export default function Sidebar({
</button>
</span>
</div>
{NewEntryButtonsAtTop && NewEntryButtons}
{NewEntryButtonsAtTop && (
<div className="SidebarNewEntry SidebarNewEntryTop">
<button onClick={OpenCreateFileModal}>New File</button>
<button onClick={OpenCreateDirectoryModal}>New Directory</button>
</div>
)}
{LoadError && <div className="SidebarError">{LoadError}</div>}
<div className="SidebarTree">
{RootEntries.map((Entry) => (
@@ -283,6 +323,7 @@ export default function Sidebar({
OnBrowseDirectory={OnBrowseDirectory}
OnEntryRenamed={OnEntryRenamed}
OnEntryDeleted={OnEntryDeleted}
OnRequestLockEdit={HandleRequestLockEdit}
UseHyphenBullets={UseHyphenBullets}
UseAsciiFolderIcons={UseAsciiFolderIcons}
NavigateIntoDirectories={NavigateIntoDirectories}
@@ -290,7 +331,48 @@ export default function Sidebar({
/>
))}
</div>
{!NewEntryButtonsAtTop && NewEntryButtons}
{!NewEntryButtonsAtTop && (
<div className="SidebarNewEntry">
<button onClick={OpenCreateFileModal}>New File</button>
<button onClick={OpenCreateDirectoryModal}>New Directory</button>
</div>
)}
{ModalConfig && ModalConfig.Mode === "createFile" && (
<AccessModal
Title={`New file in ${CurrentDirectoryPath || "root"}`}
ShowNameField
NamePlaceholder="Notes.md"
InitialUsers={[]}
InitialGroups={[]}
ConfirmLabel="Create"
OnConfirm={HandleModalConfirm}
OnCancel={() => SetModalConfig(null)}
/>
)}
{ModalConfig && ModalConfig.Mode === "createDirectory" && (
<AccessModal
Title={`New directory in ${CurrentDirectoryPath || "root"}`}
ShowNameField
NamePlaceholder="MyFolder"
InitialUsers={[]}
InitialGroups={[]}
ConfirmLabel="Create"
OnConfirm={HandleModalConfirm}
OnCancel={() => SetModalConfig(null)}
/>
)}
{ModalConfig && ModalConfig.Mode === "editLock" && (
<AccessModal
Title={`Access for "${ModalConfig.EntryName}"`}
ShowNameField={false}
InitialUsers={ModalConfig.InitialUsers}
InitialGroups={ModalConfig.InitialGroups}
ConfirmLabel="Save"
OnConfirm={HandleModalConfirm}
OnCancel={() => SetModalConfig(null)}
/>
)}
</div>
);
}