colorThemes #1
@@ -169,7 +169,7 @@ docker compose up --build -d
|
|||||||
│ │ └── useTheme.ts ← Theme toggle
|
│ │ └── useTheme.ts ← Theme toggle
|
||||||
│ ├── components/
|
│ ├── components/
|
||||||
│ │ ├── AppShell.tsx ← Layout: Sidebar + scrollable main
|
│ │ ├── 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
|
│ │ ├── ThemeToggle.tsx ← Light/dark mode toggle
|
||||||
│ │ ├── PluginSchemaForm.tsx ← JSON Schema → React form (boolean/string/number/readOnly)
|
│ │ ├── PluginSchemaForm.tsx ← JSON Schema → React form (boolean/string/number/readOnly)
|
||||||
│ │ └── ui/ ← shadcn/ui components (Button, Input, …)
|
│ │ └── 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
|
- Sections auto-open when navigating to their route
|
||||||
- In collapsed (icons-only) mode, clicking the Apps icon navigates to `/apps`
|
- In collapsed (icons-only) mode, clicking the Apps icon navigates to `/apps`
|
||||||
|
|
||||||
**Extensions** section (dynamic):
|
**App cards — Extension button:**
|
||||||
- Populated from `GET /api/plugins` (polled via TanStack Query, `retry: false`)
|
- `GET /api/plugins` is queried on the Apps page (already user-filtered by backend)
|
||||||
- Only shown when the user has access to at least one plugin
|
- If an app's `id` matches a plugin `id`, an "Extension" button is shown on that card
|
||||||
- Each entry links to `/settings/plugins/:id`
|
- Button links to `/settings/plugins/:id` alongside the existing admin "Settings" button
|
||||||
- No code changes needed to add future plugin-enabled feature containers
|
- Only users with plugin access see the button (backend filters `GET /api/plugins`)
|
||||||
|
|
||||||
### Documents page (`/apps/documents`)
|
### Documents page (`/apps/documents`)
|
||||||
|
|
||||||
|
|||||||
@@ -16,12 +16,11 @@ import {
|
|||||||
Users,
|
Users,
|
||||||
UsersRound,
|
UsersRound,
|
||||||
Palette,
|
Palette,
|
||||||
Puzzle,
|
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import ThemeToggle from "@/components/ThemeToggle";
|
import ThemeToggle from "@/components/ThemeToggle";
|
||||||
import { useAuth } from "@/hooks/useAuth";
|
import { useAuth } from "@/hooks/useAuth";
|
||||||
import { getMe, getPlugins, listCategories } from "@/api/client";
|
import { getMe, listCategories } from "@/api/client";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
export default function Sidebar() {
|
export default function Sidebar() {
|
||||||
@@ -57,14 +56,6 @@ export default function Sidebar() {
|
|||||||
enabled: appsOpen && docsOpen && !!user,
|
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) =>
|
const navItemClass = (isActive: boolean) =>
|
||||||
cn(
|
cn(
|
||||||
"flex items-center rounded-lg transition-colors",
|
"flex items-center rounded-lg transition-colors",
|
||||||
@@ -218,40 +209,6 @@ export default function Sidebar() {
|
|||||||
)}
|
)}
|
||||||
</NavLink>
|
</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 */}
|
{/* Admin — expandable */}
|
||||||
{user?.is_admin && (
|
{user?.is_admin && (
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { getMe, getServices } from "../api/client";
|
import { getMe, getPlugins, getServices } from "../api/client";
|
||||||
|
|
||||||
const cardBase: React.CSSProperties = {
|
const cardBase: React.CSSProperties = {
|
||||||
backgroundColor: "rgb(var(--color-surface))",
|
backgroundColor: "rgb(var(--color-surface))",
|
||||||
@@ -34,6 +34,12 @@ export default function AppsPage() {
|
|||||||
refetchInterval: 30_000,
|
refetchInterval: 30_000,
|
||||||
refetchIntervalInBackground: true,
|
refetchIntervalInBackground: true,
|
||||||
});
|
});
|
||||||
|
const { data: plugins = [] } = useQuery({
|
||||||
|
queryKey: ["plugins"],
|
||||||
|
queryFn: getPlugins,
|
||||||
|
retry: false,
|
||||||
|
});
|
||||||
|
const pluginIds = new Set(plugins.map((p) => p.id));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ padding: 32, maxWidth: 900, margin: "0 auto" }}>
|
<div style={{ padding: 32, maxWidth: 900, margin: "0 auto" }}>
|
||||||
@@ -93,6 +99,23 @@ export default function AppsPage() {
|
|||||||
Settings
|
Settings
|
||||||
</Link>
|
</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>
|
</div>
|
||||||
</CardWrapper>
|
</CardWrapper>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user