mirror of
https://git.srhtnin.garden/sarah/SaraView.git
synced 2026-10-14 02:38:11 +02:00
710 lines
26 KiB
React
710 lines
26 KiB
React
import React, { useEffect, useState } from "react";
|
|
|
|
import {
|
|
ChangePassword,
|
|
CreateGroupApi,
|
|
CreateUserAccount,
|
|
DeleteGroupApi,
|
|
DeleteTheme,
|
|
FetchGroups,
|
|
FetchLocks,
|
|
FetchProfileColourOptions,
|
|
FetchSettings,
|
|
FetchTheme,
|
|
FetchThemeNames,
|
|
FetchUsers,
|
|
GetAvatarUrl,
|
|
Logout,
|
|
RemoveLock,
|
|
SaveSettings,
|
|
SaveTheme,
|
|
SetGroupMembersApi,
|
|
UpdateProfileColour,
|
|
UpdateUsername,
|
|
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({
|
|
CurrentUser,
|
|
OnClose,
|
|
OnSettingsChanged,
|
|
OnLoggedOut,
|
|
OnProfileUpdated,
|
|
}) {
|
|
const [Settings, SetSettings] = useState(null);
|
|
const [ThemeNames, SetThemeNames] = useState([]);
|
|
const [SaveError, SetSaveError] = useState(null);
|
|
const [IsSaving, SetIsSaving] = useState(false);
|
|
|
|
const [IsCreatingTheme, SetIsCreatingTheme] = useState(false);
|
|
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("");
|
|
const [NewUserPassword, SetNewUserPassword] = useState("");
|
|
const [NewUserIsAdmin, SetNewUserIsAdmin] = useState(false);
|
|
const [NewUserColour, SetNewUserColour] = useState("Purple");
|
|
const [UserError, SetUserError] = useState(null);
|
|
|
|
const [ColourOptions, SetColourOptions] = useState([]);
|
|
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);
|
|
const [ConfirmingDeleteGroupName, SetConfirmingDeleteGroupName] = 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]);
|
|
|
|
if (!Settings) {
|
|
return null;
|
|
}
|
|
|
|
function UpdateField(Field, Value) {
|
|
SetSettings({ ...Settings, [Field]: Value });
|
|
}
|
|
|
|
async function HandleSave() {
|
|
SetIsSaving(true);
|
|
SetSaveError(null);
|
|
try {
|
|
const Saved = await SaveSettings(Settings);
|
|
OnSettingsChanged(Saved);
|
|
OnClose();
|
|
} catch (Error) {
|
|
SetSaveError(Error.message);
|
|
} finally {
|
|
SetIsSaving(false);
|
|
}
|
|
}
|
|
|
|
async function HandleStartCreateTheme() {
|
|
SetThemeError(null);
|
|
const BasePalette = await FetchTheme(Settings.ThemeName);
|
|
SetNewThemePalette(BasePalette);
|
|
SetNewThemeName("");
|
|
SetIsCreatingTheme(true);
|
|
}
|
|
|
|
async function HandleSaveNewTheme() {
|
|
if (!NewThemeName.trim()) return;
|
|
|
|
try {
|
|
await SaveTheme(NewThemeName.trim(), NewThemePalette);
|
|
const Updated = await FetchThemeNames();
|
|
SetThemeNames(Updated);
|
|
UpdateField("ThemeName", NewThemeName.trim());
|
|
SetIsCreatingTheme(false);
|
|
} catch (Error) {
|
|
SetThemeError(Error.message);
|
|
}
|
|
}
|
|
|
|
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, Palette);
|
|
await DeleteTheme(Settings.ThemeName);
|
|
const Updated = await FetchThemeNames();
|
|
SetThemeNames(Updated);
|
|
UpdateField("ThemeName", NewName);
|
|
} catch (Error) {
|
|
SetThemeError(Error.message);
|
|
}
|
|
}
|
|
|
|
function HandleDeleteThemeClick() {
|
|
if (ThemeNames.length <= 1) {
|
|
SetThemeError("Can't delete the only remaining theme.");
|
|
return;
|
|
}
|
|
|
|
SetIsConfirmingThemeDelete(true);
|
|
}
|
|
|
|
async function HandleDeleteThemeConfirm() {
|
|
SetIsConfirmingThemeDelete(false);
|
|
SetThemeError(null);
|
|
try {
|
|
await DeleteTheme(Settings.ThemeName);
|
|
const Updated = await FetchThemeNames();
|
|
SetThemeNames(Updated);
|
|
UpdateField("ThemeName", Updated[0]);
|
|
} catch (Error) {
|
|
SetThemeError(Error.message);
|
|
}
|
|
}
|
|
|
|
async function HandleLogout() {
|
|
await Logout();
|
|
OnLoggedOut();
|
|
}
|
|
|
|
async function HandleCreateUser() {
|
|
SetUserError(null);
|
|
|
|
try {
|
|
await CreateUserAccount(NewUsername.trim(), NewUserPassword, NewUserIsAdmin, NewUserColour);
|
|
const Updated = await FetchUsers();
|
|
SetUsers(Updated);
|
|
SetNewUsername("");
|
|
SetNewUserPassword("");
|
|
SetNewUserIsAdmin(false);
|
|
SetNewUserColour("Purple");
|
|
} catch (Error) {
|
|
SetUserError(Error.message);
|
|
}
|
|
}
|
|
|
|
async function HandlePickColour(Colour) {
|
|
SetProfileError(null);
|
|
try {
|
|
const Updated = await UpdateProfileColour(Colour);
|
|
OnProfileUpdated(Updated);
|
|
} catch (Error) {
|
|
SetProfileError(Error.message);
|
|
}
|
|
}
|
|
|
|
async function HandleAvatarChange(Event) {
|
|
const File = Event.target.files && Event.target.files[0];
|
|
if (!File) return;
|
|
|
|
SetProfileError(null);
|
|
try {
|
|
await UploadAvatar(File);
|
|
const NextNonce = Date.now();
|
|
SetAvatarNonce(NextNonce);
|
|
OnProfileUpdated({ AvatarNonce: NextNonce });
|
|
} catch (Error) {
|
|
SetProfileError(Error.message);
|
|
}
|
|
}
|
|
|
|
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);
|
|
}
|
|
}
|
|
|
|
function HandleDeleteGroupClick(Name) {
|
|
SetConfirmingDeleteGroupName(Name);
|
|
}
|
|
|
|
async function HandleDeleteGroupConfirm() {
|
|
const Name = ConfirmingDeleteGroupName;
|
|
SetConfirmingDeleteGroupName(null);
|
|
|
|
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()}>
|
|
<div className="SettingsHeader">
|
|
<span>Settings</span>
|
|
<button className="SettingsClose" onClick={OnClose}>
|
|
✕
|
|
</button>
|
|
</div>
|
|
|
|
<div className="SettingsBody">
|
|
<div className="SettingsRow">
|
|
<label className="SettingsLabel">Font family</label>
|
|
<input
|
|
type="text"
|
|
value={Settings.FontFamily}
|
|
onChange={(Event) => UpdateField("FontFamily", Event.target.value)}
|
|
/>
|
|
</div>
|
|
|
|
<div className="SettingsRow">
|
|
<label className="SettingsLabel">Font size (px)</label>
|
|
<input
|
|
type="number"
|
|
value={Settings.FontSize}
|
|
onChange={(Event) => UpdateField("FontSize", Number(Event.target.value))}
|
|
/>
|
|
</div>
|
|
|
|
<div className="SettingsRowInline">
|
|
<label className="SettingsLabel">Show hidden directories</label>
|
|
<Switch Checked={Settings.ShowHiddenFolders} OnChange={(Event) => UpdateField("ShowHiddenFolders", Event.target.checked)} />
|
|
</div>
|
|
|
|
<div className="SettingsRowInline">
|
|
<label className="SettingsLabel">Show directory note files (.sv_*)</label>
|
|
<Switch Checked={Settings.ShowSvFiles} OnChange={(Event) => UpdateField("ShowSvFiles", Event.target.checked)} />
|
|
</div>
|
|
|
|
<div className="SettingsRowInline">
|
|
<label className="SettingsLabel">Show breadcrumbs</label>
|
|
<Switch Checked={Settings.ShowBreadcrumbs} OnChange={(Event) => UpdateField("ShowBreadcrumbs", Event.target.checked)} />
|
|
</div>
|
|
|
|
<div className="SettingsRowInline">
|
|
<label className="SettingsLabel">Hyphen file icons (-)</label>
|
|
<Switch Checked={Settings.UseHyphenBullets} OnChange={(Event) => UpdateField("UseHyphenBullets", Event.target.checked)} />
|
|
</div>
|
|
|
|
<div className="SettingsRowInline">
|
|
<label className="SettingsLabel">ASCII directory icons (>/v)</label>
|
|
<Switch Checked={Settings.UseAsciiFolderIcons} OnChange={(Event) => UpdateField("UseAsciiFolderIcons", Event.target.checked)} />
|
|
</div>
|
|
|
|
<div className="SettingsRowInline">
|
|
<label className="SettingsLabel">Open directory file on click</label>
|
|
<Switch Checked={Settings.NavigateIntoDirectories} OnChange={(Event) => UpdateField("NavigateIntoDirectories", Event.target.checked)} />
|
|
</div>
|
|
|
|
<div className="SettingsRowInline">
|
|
<label className="SettingsLabel">New File/Directory buttons at top of sidebar</label>
|
|
<Switch Checked={Settings.NewEntryButtonsAtTop} OnChange={(Event) => UpdateField("NewEntryButtonsAtTop", Event.target.checked)} />
|
|
</div>
|
|
|
|
<div className="SettingsRowInline">
|
|
<label className="SettingsLabel">Show blame gutter (who last changed each line)</label>
|
|
<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)} />
|
|
</div>
|
|
|
|
<div className="SettingsRowInline">
|
|
<label className="SettingsLabel">Confirm before delete</label>
|
|
<Switch Checked={Settings.ConfirmBeforeDelete} OnChange={(Event) => UpdateField("ConfirmBeforeDelete", Event.target.checked)} />
|
|
</div>
|
|
|
|
<div className="SettingsSection">
|
|
<div className="SettingsSectionTitle">Colour theme</div>
|
|
<div className="SettingsRow">
|
|
<select
|
|
value={Settings.ThemeName}
|
|
onChange={(Event) => UpdateField("ThemeName", Event.target.value)}
|
|
>
|
|
{ThemeNames.map((Name) => (
|
|
<option key={Name} value={Name}>
|
|
{Name}
|
|
</option>
|
|
))}
|
|
</select>
|
|
<div className="SettingsRowInline">
|
|
<button onClick={HandleRenameThemeClick}>Rename</button>
|
|
<button onClick={HandleDeleteThemeClick}>Delete</button>
|
|
</div>
|
|
</div>
|
|
|
|
{!IsCreatingTheme && (
|
|
<button onClick={HandleStartCreateTheme}>Create from current theme</button>
|
|
)}
|
|
{IsCreatingTheme && NewThemePalette && (
|
|
<>
|
|
<div className="SettingsRow">
|
|
<label className="SettingsLabel">New theme name</label>
|
|
<input
|
|
type="text"
|
|
value={NewThemeName}
|
|
onChange={(Event) => SetNewThemeName(Event.target.value)}
|
|
placeholder="MyTheme"
|
|
/>
|
|
</div>
|
|
<div className="PaletteGrid">
|
|
{PaletteRoles.map((Role) => (
|
|
<div className="PaletteSwatchRow" key={Role}>
|
|
<input
|
|
type="color"
|
|
value={NewThemePalette[Role] || "#000000"}
|
|
onChange={(Event) =>
|
|
SetNewThemePalette({ ...NewThemePalette, [Role]: Event.target.value })
|
|
}
|
|
/>
|
|
<span>{Role}</span>
|
|
</div>
|
|
))}
|
|
</div>
|
|
<button onClick={HandleSaveNewTheme}>Save theme</button>
|
|
</>
|
|
)}
|
|
{ThemeError && <div className="SettingsSaveError">{ThemeError}</div>}
|
|
</div>
|
|
|
|
<div className="SettingsSection">
|
|
<div className="SettingsSectionTitle">Profile</div>
|
|
<div className="SettingsRow">
|
|
<label className="SettingsLabel">Your colour</label>
|
|
<div className="ColourSwatchPicker">
|
|
{ColourOptions.map((Colour) => (
|
|
<button
|
|
key={Colour}
|
|
className={`ColourSwatch${CurrentUser && CurrentUser.Colour === Colour ? " ColourSwatchSelected" : ""}`}
|
|
style={{ background: `var(--${Colour})` }}
|
|
title={Colour}
|
|
onClick={() => HandlePickColour(Colour)}
|
|
/>
|
|
))}
|
|
</div>
|
|
</div>
|
|
<div className="SettingsRow">
|
|
<label className="SettingsLabel">Avatar</label>
|
|
<div className="AvatarUploadRow">
|
|
{CurrentUser && (
|
|
<img
|
|
className="AvatarPreview"
|
|
src={`${GetAvatarUrl(CurrentUser.Username)}&v=${AvatarNonce}`}
|
|
alt=""
|
|
onError={(Event) => {
|
|
Event.target.style.visibility = "hidden";
|
|
}}
|
|
/>
|
|
)}
|
|
<input type="file" accept="image/*" onChange={HandleAvatarChange} />
|
|
</div>
|
|
</div>
|
|
{ProfileError && <div className="SettingsSaveError">{ProfileError}</div>}
|
|
</div>
|
|
|
|
<div className="SettingsSection">
|
|
<div className="SettingsSectionTitle">Account</div>
|
|
<div className="SettingsRowInline">
|
|
<span className="SettingsLabel">
|
|
Logged in as {CurrentUser ? CurrentUser.Username : "..."}
|
|
{CurrentUser && CurrentUser.IsAdmin ? " (admin)" : ""}
|
|
</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 && (
|
|
<div className="SettingsSection">
|
|
<div className="SettingsSectionTitle">Users (admin)</div>
|
|
{Users &&
|
|
Users.map((User) => (
|
|
<div className="SettingsRowInline" key={User.Username}>
|
|
<span className="SettingsLabel">
|
|
<span
|
|
className="ColourSwatchSmall"
|
|
style={{ background: `var(--${User.Colour || "Purple"})` }}
|
|
/>
|
|
{User.Username}
|
|
{User.IsAdmin ? " (admin)" : ""}
|
|
</span>
|
|
</div>
|
|
))}
|
|
<div className="SettingsRow">
|
|
<label className="SettingsLabel">New username</label>
|
|
<input
|
|
type="text"
|
|
value={NewUsername}
|
|
onChange={(Event) => SetNewUsername(Event.target.value)}
|
|
/>
|
|
</div>
|
|
<div className="SettingsRow">
|
|
<label className="SettingsLabel">New password</label>
|
|
<input
|
|
type="password"
|
|
value={NewUserPassword}
|
|
onChange={(Event) => SetNewUserPassword(Event.target.value)}
|
|
/>
|
|
</div>
|
|
<div className="SettingsRow">
|
|
<label className="SettingsLabel">Colour</label>
|
|
<div className="ColourSwatchPicker">
|
|
{ColourOptions.map((Colour) => (
|
|
<button
|
|
key={Colour}
|
|
className={`ColourSwatch${NewUserColour === Colour ? " ColourSwatchSelected" : ""}`}
|
|
style={{ background: `var(--${Colour})` }}
|
|
title={Colour}
|
|
onClick={() => SetNewUserColour(Colour)}
|
|
/>
|
|
))}
|
|
</div>
|
|
</div>
|
|
<div className="SettingsRowInline">
|
|
<label className="SettingsLabel">Admin</label>
|
|
<Switch Checked={NewUserIsAdmin} OnChange={(Event) => SetNewUserIsAdmin(Event.target.checked)} />
|
|
</div>
|
|
{UserError && <div className="SettingsSaveError">{UserError}</div>}
|
|
<button onClick={HandleCreateUser}>Create user</button>
|
|
</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" && Group.Name !== "Guest" && (
|
|
<button onClick={() => HandleDeleteGroupClick(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>
|
|
|
|
<div className="SettingsFooter">
|
|
<button onClick={OnClose}>Cancel</button>
|
|
<button onClick={HandleSave} disabled={IsSaving}>
|
|
{IsSaving ? "Saving…" : "Save"}
|
|
</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>
|
|
);
|
|
}
|