mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
feat(ui): add teams, tools, feature flags, temp files, logo to settings dialog
- Add Teams section with CRUD (create, rename, delete) and member count display - Add Tools section with per-tool enable/disable toggles grouped by category - Add logo upload/delete, experimental tools toggle, file management settings to System Settings - Replace free-text team input with dropdown populated from teams API in People section - Filter disabled/experimental tools in tool panel, pipeline builder, and fullscreen grid - Display custom logo in mobile header and sidebar when configured
This commit is contained in:
@@ -1,7 +1,8 @@
|
||||
import { FolderOpen, LayoutGrid, Menu, Settings as SettingsIcon, Workflow, X } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import { useMobile } from "@/hooks/use-mobile";
|
||||
import { apiGet } from "@/lib/api";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Dropzone } from "../common/dropzone";
|
||||
import { HelpDialog } from "../help/help-dialog";
|
||||
@@ -21,6 +22,13 @@ export function AppLayout({ children, showToolPanel = true, onFiles }: AppLayout
|
||||
const [helpOpen, setHelpOpen] = useState(false);
|
||||
const [mobileSidebarOpen, setMobileSidebarOpen] = useState(false);
|
||||
const isMobile = useMobile();
|
||||
const [customLogo, setCustomLogo] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
apiGet<{ settings: Record<string, string> }>("/v1/settings")
|
||||
.then((data) => setCustomLogo(data.settings.customLogo === "true"))
|
||||
.catch(() => {});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="flex h-screen bg-background text-foreground overflow-hidden">
|
||||
@@ -41,9 +49,17 @@ export function AppLayout({ children, showToolPanel = true, onFiles }: AppLayout
|
||||
/>
|
||||
<div className="fixed inset-y-0 left-0 z-50 w-64 bg-background border-r border-border shadow-xl animate-in slide-in-from-left">
|
||||
<div className="flex items-center justify-between p-3 border-b border-border">
|
||||
<span className="text-sm font-bold text-foreground">
|
||||
Stirling <span className="text-primary">Image</span>
|
||||
</span>
|
||||
{customLogo ? (
|
||||
<img
|
||||
src="/api/v1/settings/logo"
|
||||
className="h-6 w-6 rounded object-contain"
|
||||
alt="Logo"
|
||||
/>
|
||||
) : (
|
||||
<span className="text-sm font-bold text-foreground">
|
||||
Stirling <span className="text-primary">Image</span>
|
||||
</span>
|
||||
)}
|
||||
<button
|
||||
onClick={() => setMobileSidebarOpen(false)}
|
||||
className="p-1.5 rounded-lg hover:bg-muted"
|
||||
@@ -77,9 +93,17 @@ export function AppLayout({ children, showToolPanel = true, onFiles }: AppLayout
|
||||
>
|
||||
<Menu className="h-5 w-5" />
|
||||
</button>
|
||||
<span className="text-sm font-bold text-foreground">
|
||||
Stirling <span className="text-primary">Image</span>
|
||||
</span>
|
||||
{customLogo ? (
|
||||
<img
|
||||
src="/api/v1/settings/logo"
|
||||
className="h-6 w-6 rounded object-contain"
|
||||
alt="Logo"
|
||||
/>
|
||||
) : (
|
||||
<span className="text-sm font-bold text-foreground">
|
||||
Stirling <span className="text-primary">Image</span>
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
@@ -1,18 +1,40 @@
|
||||
import { CATEGORIES, TOOLS } from "@stirling-image/shared";
|
||||
import { useMemo, useState } from "react";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { apiGet } from "@/lib/api";
|
||||
import { SearchBar } from "../common/search-bar";
|
||||
import { ToolCard } from "../common/tool-card";
|
||||
|
||||
export function ToolPanel() {
|
||||
const [search, setSearch] = useState("");
|
||||
const [disabledTools, setDisabledTools] = useState<string[]>([]);
|
||||
const [experimentalEnabled, setExperimentalEnabled] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
apiGet<{ settings: Record<string, string> }>("/v1/settings")
|
||||
.then((data) => {
|
||||
setDisabledTools(
|
||||
data.settings.disabledTools ? JSON.parse(data.settings.disabledTools) : [],
|
||||
);
|
||||
setExperimentalEnabled(data.settings.enableExperimentalTools === "true");
|
||||
})
|
||||
.catch(() => {});
|
||||
}, []);
|
||||
|
||||
const visibleTools = useMemo(() => {
|
||||
return TOOLS.filter((t) => {
|
||||
if (disabledTools.includes(t.id)) return false;
|
||||
if (t.experimental && !experimentalEnabled) return false;
|
||||
return true;
|
||||
});
|
||||
}, [disabledTools, experimentalEnabled]);
|
||||
|
||||
const filteredTools = useMemo(() => {
|
||||
if (!search) return TOOLS;
|
||||
if (!search) return visibleTools;
|
||||
const q = search.toLowerCase();
|
||||
return TOOLS.filter(
|
||||
return visibleTools.filter(
|
||||
(t) => t.name.toLowerCase().includes(q) || t.description.toLowerCase().includes(q),
|
||||
);
|
||||
}, [search]);
|
||||
}, [search, visibleTools]);
|
||||
|
||||
const groupedTools = useMemo(() => {
|
||||
const groups = new Map<string, typeof TOOLS>();
|
||||
|
||||
Reference in New Issue
Block a user