colorThemes #1
@@ -169,7 +169,7 @@ docker compose up --build -d
|
||||
│ │ └── useTheme.ts ← Theme toggle
|
||||
│ ├── components/
|
||||
│ │ ├── AppShell.tsx ← Layout: Sidebar + scrollable main
|
||||
│ │ ├── Sidebar.tsx ← Collapsible nav; "Extensions" section auto-populated from /api/plugins
|
||||
│ │ ├── Sidebar.tsx ← Collapsible nav (icons ↔ icons+labels)
|
||||
│ │ ├── ThemeToggle.tsx ← Light/dark mode toggle
|
||||
│ │ ├── PluginSchemaForm.tsx ← JSON Schema → React form (boolean/string/number/readOnly)
|
||||
│ │ └── ui/ ← shadcn/ui components (Button, Input, …)
|
||||
|
||||
+5
-5
@@ -61,11 +61,11 @@ Cards are rendered dynamically from `GET /api/services` (polled every 30 s via T
|
||||
- Sections auto-open when navigating to their route
|
||||
- In collapsed (icons-only) mode, clicking the Apps icon navigates to `/apps`
|
||||
|
||||
**Extensions** section (dynamic):
|
||||
- Populated from `GET /api/plugins` (polled via TanStack Query, `retry: false`)
|
||||
- Only shown when the user has access to at least one plugin
|
||||
- Each entry links to `/settings/plugins/:id`
|
||||
- No code changes needed to add future plugin-enabled feature containers
|
||||
**App cards — Extension button:**
|
||||
- `GET /api/plugins` is queried on the Apps page (already user-filtered by backend)
|
||||
- If an app's `id` matches a plugin `id`, an "Extension" button is shown on that card
|
||||
- Button links to `/settings/plugins/:id` alongside the existing admin "Settings" button
|
||||
- Only users with plugin access see the button (backend filters `GET /api/plugins`)
|
||||
|
||||
### Documents page (`/apps/documents`)
|
||||
|
||||
|
||||
@@ -16,12 +16,11 @@ import {
|
||||
Users,
|
||||
UsersRound,
|
||||
Palette,
|
||||
Puzzle,
|
||||
} from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import ThemeToggle from "@/components/ThemeToggle";
|
||||
import { useAuth } from "@/hooks/useAuth";
|
||||
import { getMe, getPlugins, listCategories } from "@/api/client";
|
||||
import { getMe, listCategories } from "@/api/client";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export default function Sidebar() {
|
||||
@@ -57,14 +56,6 @@ export default function Sidebar() {
|
||||
enabled: appsOpen && docsOpen && !!user,
|
||||
});
|
||||
|
||||
const { data: plugins = [] } = useQuery({
|
||||
queryKey: ["plugins"],
|
||||
queryFn: getPlugins,
|
||||
enabled: !!user,
|
||||
// Empty array on 404/error — regular users simply see no plugins
|
||||
retry: false,
|
||||
});
|
||||
|
||||
const navItemClass = (isActive: boolean) =>
|
||||
cn(
|
||||
"flex items-center rounded-lg transition-colors",
|
||||
@@ -218,40 +209,6 @@ export default function Sidebar() {
|
||||
)}
|
||||
</NavLink>
|
||||
|
||||
{/* Extensions — visible only when the user has accessible plugins */}
|
||||
{plugins.length > 0 && (
|
||||
<div>
|
||||
{sidebarExpanded ? (
|
||||
<>
|
||||
<div className="px-3 py-1.5">
|
||||
<span className="text-xs font-semibold uppercase tracking-wider text-muted">
|
||||
Extensions
|
||||
</span>
|
||||
</div>
|
||||
<div className="space-y-0.5">
|
||||
{plugins.map((plugin) => (
|
||||
<NavLink
|
||||
key={plugin.id}
|
||||
to={`/settings/plugins/${plugin.id}`}
|
||||
className={({ isActive }) => subItemClass(isActive)}
|
||||
>
|
||||
<Puzzle className="h-4 w-4 shrink-0" />
|
||||
<span className="whitespace-nowrap truncate">{plugin.name}</span>
|
||||
</NavLink>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<NavLink
|
||||
to={`/settings/plugins/${plugins[0].id}`}
|
||||
className={({ isActive }) => navItemClass(isActive)}
|
||||
>
|
||||
<Puzzle className="h-5 w-5 shrink-0" />
|
||||
</NavLink>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Admin — expandable */}
|
||||
{user?.is_admin && (
|
||||
<div>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Link } from "react-router-dom";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { getMe, getServices } from "../api/client";
|
||||
import { getMe, getPlugins, getServices } from "../api/client";
|
||||
|
||||
const cardBase: React.CSSProperties = {
|
||||
backgroundColor: "rgb(var(--color-surface))",
|
||||
@@ -34,6 +34,12 @@ export default function AppsPage() {
|
||||
refetchInterval: 30_000,
|
||||
refetchIntervalInBackground: true,
|
||||
});
|
||||
const { data: plugins = [] } = useQuery({
|
||||
queryKey: ["plugins"],
|
||||
queryFn: getPlugins,
|
||||
retry: false,
|
||||
});
|
||||
const pluginIds = new Set(plugins.map((p) => p.id));
|
||||
|
||||
return (
|
||||
<div style={{ padding: 32, maxWidth: 900, margin: "0 auto" }}>
|
||||
@@ -93,6 +99,23 @@ export default function AppsPage() {
|
||||
Settings
|
||||
</Link>
|
||||
)}
|
||||
{pluginIds.has(svc.id) && (
|
||||
<Link
|
||||
to={`/settings/plugins/${svc.id}`}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
style={{
|
||||
padding: "6px 14px",
|
||||
border: "1px solid #ccc",
|
||||
borderRadius: 4,
|
||||
textDecoration: "none",
|
||||
fontSize: 14,
|
||||
color: "#333",
|
||||
}}
|
||||
title="Extension settings"
|
||||
>
|
||||
Extension
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
</CardWrapper>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user