feat(admin): make the left navigation bar collapsable (#10774)
This commit is contained in:
@@ -6,6 +6,7 @@
|
|||||||
"@affine/component": "workspace:*",
|
"@affine/component": "workspace:*",
|
||||||
"@affine/core": "workspace:*",
|
"@affine/core": "workspace:*",
|
||||||
"@affine/graphql": "workspace:*",
|
"@affine/graphql": "workspace:*",
|
||||||
|
"@blocksuite/icons": "^2.2.4",
|
||||||
"@radix-ui/react-accordion": "^1.2.2",
|
"@radix-ui/react-accordion": "^1.2.2",
|
||||||
"@radix-ui/react-alert-dialog": "^1.1.3",
|
"@radix-ui/react-alert-dialog": "^1.1.3",
|
||||||
"@radix-ui/react-aspect-ratio": "^1.1.1",
|
"@radix-ui/react-aspect-ratio": "^1.1.1",
|
||||||
@@ -36,6 +37,7 @@
|
|||||||
"@sentry/react": "^9.2.0",
|
"@sentry/react": "^9.2.0",
|
||||||
"@tanstack/react-table": "^8.20.5",
|
"@tanstack/react-table": "^8.20.5",
|
||||||
"@toeverything/infra": "workspace:*",
|
"@toeverything/infra": "workspace:*",
|
||||||
|
"@toeverything/theme": "^1.1.12",
|
||||||
"cmdk": "^1.0.4",
|
"cmdk": "^1.0.4",
|
||||||
"embla-carousel-react": "^8.5.1",
|
"embla-carousel-react": "^8.5.1",
|
||||||
"input-otp": "^1.4.1",
|
"input-otp": "^1.4.1",
|
||||||
|
|||||||
@@ -80,10 +80,10 @@ export const router = _createBrowserRouter(
|
|||||||
path: 'accounts',
|
path: 'accounts',
|
||||||
lazy: () => import('./modules/accounts'),
|
lazy: () => import('./modules/accounts'),
|
||||||
},
|
},
|
||||||
// {
|
{
|
||||||
// path: 'ai',
|
path: 'ai',
|
||||||
// lazy: () => import('./modules/ai'),
|
lazy: () => import('./modules/ai'),
|
||||||
// },
|
},
|
||||||
{
|
{
|
||||||
path: 'config',
|
path: 'config',
|
||||||
lazy: () => import('./modules/config'),
|
lazy: () => import('./modules/config'),
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
@import 'tailwindcss';
|
@import 'tailwindcss';
|
||||||
@import 'tailwindcss/utilities';
|
@import 'tailwindcss/utilities';
|
||||||
|
@import '@toeverything/theme/style.css';
|
||||||
|
|
||||||
@plugin 'tailwindcss-animate';
|
@plugin 'tailwindcss-animate';
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import {
|
|||||||
import { useCallback, useState } from 'react';
|
import { useCallback, useState } from 'react';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
|
|
||||||
import { useRightPanel } from '../../layout';
|
import { useRightPanel } from '../../panel/context';
|
||||||
import type { UserType } from '../schema';
|
import type { UserType } from '../schema';
|
||||||
import { DeleteAccountDialog } from './delete-account';
|
import { DeleteAccountDialog } from './delete-account';
|
||||||
import { DiscardChanges } from './discard-changes';
|
import { DiscardChanges } from './discard-changes';
|
||||||
@@ -31,8 +31,7 @@ export function DataTableRowActions({ user }: DataTableRowActionsProps) {
|
|||||||
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
||||||
const [resetPasswordDialogOpen, setResetPasswordDialogOpen] = useState(false);
|
const [resetPasswordDialogOpen, setResetPasswordDialogOpen] = useState(false);
|
||||||
const [discardDialogOpen, setDiscardDialogOpen] = useState(false);
|
const [discardDialogOpen, setDiscardDialogOpen] = useState(false);
|
||||||
const { setRightPanelContent, openPanel, isOpen, closePanel } =
|
const { openPanel, isOpen, closePanel, setPanelContent } = useRightPanel();
|
||||||
useRightPanel();
|
|
||||||
|
|
||||||
const deleteUser = useDeleteUser();
|
const deleteUser = useDeleteUser();
|
||||||
const { resetPasswordLink, onResetPassword } = useResetUserPassword();
|
const { resetPasswordLink, onResetPassword } = useResetUserPassword();
|
||||||
@@ -81,7 +80,7 @@ export function DataTableRowActions({ user }: DataTableRowActionsProps) {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleConfirm = useCallback(() => {
|
const handleConfirm = useCallback(() => {
|
||||||
setRightPanelContent(
|
setPanelContent(
|
||||||
<UpdateUserForm
|
<UpdateUserForm
|
||||||
user={user}
|
user={user}
|
||||||
onComplete={closePanel}
|
onComplete={closePanel}
|
||||||
@@ -92,7 +91,6 @@ export function DataTableRowActions({ user }: DataTableRowActionsProps) {
|
|||||||
if (discardDialogOpen) {
|
if (discardDialogOpen) {
|
||||||
handleDiscardChangesCancel();
|
handleDiscardChangesCancel();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isOpen) {
|
if (!isOpen) {
|
||||||
openPanel();
|
openPanel();
|
||||||
}
|
}
|
||||||
@@ -104,7 +102,7 @@ export function DataTableRowActions({ user }: DataTableRowActionsProps) {
|
|||||||
openDeleteDialog,
|
openDeleteDialog,
|
||||||
openPanel,
|
openPanel,
|
||||||
openResetPasswordDialog,
|
openResetPasswordDialog,
|
||||||
setRightPanelContent,
|
setPanelContent,
|
||||||
user,
|
user,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import {
|
|||||||
useState,
|
useState,
|
||||||
} from 'react';
|
} from 'react';
|
||||||
|
|
||||||
import { useRightPanel } from '../../layout';
|
import { useRightPanel } from '../../panel/context';
|
||||||
import { DiscardChanges } from './discard-changes';
|
import { DiscardChanges } from './discard-changes';
|
||||||
import { CreateUserForm } from './user-form';
|
import { CreateUserForm } from './user-form';
|
||||||
|
|
||||||
@@ -59,19 +59,18 @@ export function DataTableToolbar<TData>({
|
|||||||
const [value, setValue] = useState('');
|
const [value, setValue] = useState('');
|
||||||
const [dialogOpen, setDialogOpen] = useState(false);
|
const [dialogOpen, setDialogOpen] = useState(false);
|
||||||
const debouncedValue = useDebouncedValue(value, 1000);
|
const debouncedValue = useDebouncedValue(value, 1000);
|
||||||
const { setRightPanelContent, openPanel, closePanel, isOpen } =
|
const { setPanelContent, openPanel, closePanel, isOpen } = useRightPanel();
|
||||||
useRightPanel();
|
|
||||||
const { result, query } = useSearch();
|
const { result, query } = useSearch();
|
||||||
|
|
||||||
const handleConfirm = useCallback(() => {
|
const handleConfirm = useCallback(() => {
|
||||||
setRightPanelContent(<CreateUserForm onComplete={closePanel} />);
|
setPanelContent(<CreateUserForm onComplete={closePanel} />);
|
||||||
if (dialogOpen) {
|
if (dialogOpen) {
|
||||||
setDialogOpen(false);
|
setDialogOpen(false);
|
||||||
}
|
}
|
||||||
if (!isOpen) {
|
if (!isOpen) {
|
||||||
openPanel();
|
openPanel();
|
||||||
}
|
}
|
||||||
}, [setRightPanelContent, closePanel, dialogOpen, isOpen, openPanel]);
|
}, [setPanelContent, closePanel, dialogOpen, isOpen, openPanel]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
query(debouncedValue);
|
query(debouncedValue);
|
||||||
|
|||||||
@@ -4,11 +4,12 @@ import { Label } from '@affine/admin/components/ui/label';
|
|||||||
import { Separator } from '@affine/admin/components/ui/separator';
|
import { Separator } from '@affine/admin/components/ui/separator';
|
||||||
import { Switch } from '@affine/admin/components/ui/switch';
|
import { Switch } from '@affine/admin/components/ui/switch';
|
||||||
import type { FeatureType } from '@affine/graphql';
|
import type { FeatureType } from '@affine/graphql';
|
||||||
import { CheckIcon, ChevronRightIcon, XIcon } from 'lucide-react';
|
import { ChevronRightIcon } from 'lucide-react';
|
||||||
import type { ChangeEvent } from 'react';
|
import type { ChangeEvent } from 'react';
|
||||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
|
|
||||||
import { useServerConfig } from '../../common';
|
import { useServerConfig } from '../../common';
|
||||||
|
import { RightPanelHeader } from '../../header';
|
||||||
import type { UserInput, UserType } from '../schema';
|
import type { UserInput, UserType } from '../schema';
|
||||||
import { useCreateUser, useUpdateUser } from './use-user-management';
|
import { useCreateUser, useUpdateUser } from './use-user-management';
|
||||||
|
|
||||||
@@ -92,29 +93,12 @@ function UserForm({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col h-full gap-1">
|
<div className="flex flex-col h-full gap-1">
|
||||||
<div className=" flex justify-between items-center py-[10px] px-6">
|
<RightPanelHeader
|
||||||
<Button
|
title={title}
|
||||||
type="button"
|
handleClose={handleClose}
|
||||||
size="icon"
|
handleConfirm={handleConfirm}
|
||||||
className="w-7 h-7"
|
canSave={canSave}
|
||||||
variant="ghost"
|
/>
|
||||||
onClick={handleClose}
|
|
||||||
>
|
|
||||||
<XIcon size={20} />
|
|
||||||
</Button>
|
|
||||||
<span className="text-base font-medium">{title}</span>
|
|
||||||
<Button
|
|
||||||
type="submit"
|
|
||||||
size="icon"
|
|
||||||
className="w-7 h-7"
|
|
||||||
variant="ghost"
|
|
||||||
onClick={handleConfirm}
|
|
||||||
disabled={!canSave}
|
|
||||||
>
|
|
||||||
<CheckIcon size={20} />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<Separator />
|
|
||||||
<div className="p-4 flex-grow overflow-y-auto space-y-[10px]">
|
<div className="p-4 flex-grow overflow-y-auto space-y-[10px]">
|
||||||
<div className="flex flex-col rounded-md border py-4 gap-4">
|
<div className="flex flex-col rounded-md border py-4 gap-4">
|
||||||
<InputItem
|
<InputItem
|
||||||
|
|||||||
@@ -1,18 +1,13 @@
|
|||||||
import { Separator } from '@affine/admin/components/ui/separator';
|
import { Header } from '../header';
|
||||||
|
|
||||||
import { columns } from './components/columns';
|
import { columns } from './components/columns';
|
||||||
import { DataTable } from './components/data-table';
|
import { DataTable } from './components/data-table';
|
||||||
import { useUserList } from './use-user-list';
|
import { useUserList } from './use-user-list';
|
||||||
|
|
||||||
export function AccountPage() {
|
export function AccountPage() {
|
||||||
const { users, pagination, setPagination } = useUserList();
|
const { users, pagination, setPagination } = useUserList();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className=" h-screen flex-1 flex-col flex">
|
<div className=" h-screen flex-1 flex-col flex">
|
||||||
<div className="flex items-center justify-between px-6 py-3 my-[2px] max-md:ml-9 max-md:mt-[2px]">
|
<Header title="Accounts" />
|
||||||
<div className="text-base font-medium">Accounts</div>
|
|
||||||
</div>
|
|
||||||
<Separator />
|
|
||||||
|
|
||||||
<DataTable
|
<DataTable
|
||||||
data={users}
|
data={users}
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
import { Button } from '@affine/admin/components/ui/button';
|
|
||||||
import { ScrollArea } from '@affine/admin/components/ui/scroll-area';
|
import { ScrollArea } from '@affine/admin/components/ui/scroll-area';
|
||||||
import { Separator } from '@affine/admin/components/ui/separator';
|
import { Separator } from '@affine/admin/components/ui/separator';
|
||||||
import { Textarea } from '@affine/admin/components/ui/textarea';
|
import { Textarea } from '@affine/admin/components/ui/textarea';
|
||||||
import { CheckIcon, XIcon } from 'lucide-react';
|
|
||||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
|
|
||||||
import { useRightPanel } from '../layout';
|
import { RightPanelHeader } from '../header';
|
||||||
|
import { useRightPanel } from '../panel/context';
|
||||||
import type { Prompt } from './prompts';
|
import type { Prompt } from './prompts';
|
||||||
import { usePrompt } from './use-prompt';
|
import { usePrompt } from './use-prompt';
|
||||||
|
|
||||||
@@ -56,29 +55,12 @@ export function EditPrompt({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col h-full gap-1">
|
<div className="flex flex-col h-full gap-1">
|
||||||
<div className="flex justify-between items-center py-[10px] px-6 ">
|
<RightPanelHeader
|
||||||
<Button
|
title="Edit Prompt"
|
||||||
type="button"
|
handleClose={handleClose}
|
||||||
size="icon"
|
handleConfirm={onConfirm}
|
||||||
className="w-7 h-7"
|
canSave={!disableSave}
|
||||||
variant="ghost"
|
/>
|
||||||
onClick={handleClose}
|
|
||||||
>
|
|
||||||
<XIcon size={20} />
|
|
||||||
</Button>
|
|
||||||
<span className="text-base font-medium">Edit Prompt</span>
|
|
||||||
<Button
|
|
||||||
type="submit"
|
|
||||||
size="icon"
|
|
||||||
className="w-7 h-7"
|
|
||||||
variant="ghost"
|
|
||||||
onClick={onConfirm}
|
|
||||||
disabled={disableSave}
|
|
||||||
>
|
|
||||||
<CheckIcon size={20} />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<Separator />
|
|
||||||
<ScrollArea>
|
<ScrollArea>
|
||||||
<div className="grid">
|
<div className="grid">
|
||||||
<div className="px-5 py-4 overflow-y-auto space-y-[10px] flex flex-col gap-5">
|
<div className="px-5 py-4 overflow-y-auto space-y-[10px] flex flex-col gap-5">
|
||||||
|
|||||||
@@ -1,26 +1,42 @@
|
|||||||
import { Separator } from '@affine/admin/components/ui/separator';
|
import { Switch } from '@affine/admin/components/ui/switch';
|
||||||
import { cn } from '@affine/admin/utils';
|
import { cn } from '@affine/admin/utils';
|
||||||
import * as ScrollAreaPrimitive from '@radix-ui/react-scroll-area';
|
import * as ScrollAreaPrimitive from '@radix-ui/react-scroll-area';
|
||||||
|
import { useState } from 'react';
|
||||||
|
|
||||||
import { Prompts } from './prompts';
|
import { Header } from '../header';
|
||||||
|
|
||||||
function AiPage() {
|
function AiPage() {
|
||||||
|
const [enableAi, setEnableAi] = useState(false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className=" h-screen flex-1 flex-col flex">
|
<div className="h-screen flex-1 flex-col flex">
|
||||||
<div className="flex items-center justify-between px-6 py-3 my-[2px] max-md:ml-9 max-md:mt-[2px]">
|
<Header title="AI" />
|
||||||
<div className="text-base font-medium">AI</div>
|
|
||||||
</div>
|
|
||||||
<Separator />
|
|
||||||
<ScrollAreaPrimitive.Root
|
<ScrollAreaPrimitive.Root
|
||||||
className={cn('relative overflow-hidden w-full')}
|
className={cn('relative overflow-hidden w-full')}
|
||||||
>
|
>
|
||||||
<ScrollAreaPrimitive.Viewport className="h-full w-full rounded-[inherit] [&>div]:!block">
|
<ScrollAreaPrimitive.Viewport className="h-full w-full rounded-[inherit] [&>div]:!block">
|
||||||
<Prompts />
|
<div className="p-6 max-w-3xl mx-auto">
|
||||||
|
<div className="text-[20px]">AI</div>
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
<div>
|
||||||
|
<p className="text-[15px] font-medium mt-6">Enable AI</p>
|
||||||
|
<p className="text-sm text-muted-foreground mt-1">
|
||||||
|
AI functionality is not currently supported. Self-hosted AI
|
||||||
|
support is in progress.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Switch
|
||||||
|
checked={enableAi}
|
||||||
|
onCheckedChange={setEnableAi}
|
||||||
|
disabled={true}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/* <Prompts /> */}
|
||||||
</ScrollAreaPrimitive.Viewport>
|
</ScrollAreaPrimitive.Viewport>
|
||||||
<ScrollAreaPrimitive.ScrollAreaScrollbar
|
<ScrollAreaPrimitive.ScrollAreaScrollbar
|
||||||
className={cn(
|
className={cn(
|
||||||
'flex touch-none select-none transition-colors',
|
'flex touch-none select-none transition-colors',
|
||||||
|
|
||||||
'h-full w-2.5 border-l border-l-transparent p-[1px]'
|
'h-full w-2.5 border-l border-l-transparent p-[1px]'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { Separator } from '@affine/admin/components/ui/separator';
|
|||||||
import type { CopilotPromptMessageRole } from '@affine/graphql';
|
import type { CopilotPromptMessageRole } from '@affine/graphql';
|
||||||
import { useCallback, useState } from 'react';
|
import { useCallback, useState } from 'react';
|
||||||
|
|
||||||
import { useRightPanel } from '../layout';
|
import { useRightPanel } from '../panel/context';
|
||||||
import { DiscardChanges } from './discard-changes';
|
import { DiscardChanges } from './discard-changes';
|
||||||
import { EditPrompt } from './edit-prompt';
|
import { EditPrompt } from './edit-prompt';
|
||||||
import { usePrompt } from './use-prompt';
|
import { usePrompt } from './use-prompt';
|
||||||
@@ -52,7 +52,7 @@ export function Prompts() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const PromptRow = ({ item, index }: { item: Prompt; index: number }) => {
|
export const PromptRow = ({ item, index }: { item: Prompt; index: number }) => {
|
||||||
const { setRightPanelContent, openPanel, isOpen } = useRightPanel();
|
const { setPanelContent, openPanel, isOpen } = useRightPanel();
|
||||||
const [dialogOpen, setDialogOpen] = useState(false);
|
const [dialogOpen, setDialogOpen] = useState(false);
|
||||||
const [canSave, setCanSave] = useState(false);
|
const [canSave, setCanSave] = useState(false);
|
||||||
|
|
||||||
@@ -63,7 +63,7 @@ export const PromptRow = ({ item, index }: { item: Prompt; index: number }) => {
|
|||||||
|
|
||||||
const handleConfirm = useCallback(
|
const handleConfirm = useCallback(
|
||||||
(item: Prompt) => {
|
(item: Prompt) => {
|
||||||
setRightPanelContent(<EditPrompt item={item} setCanSave={setCanSave} />);
|
setPanelContent(<EditPrompt item={item} setCanSave={setCanSave} />);
|
||||||
if (dialogOpen) {
|
if (dialogOpen) {
|
||||||
handleDiscardChangesCancel();
|
handleDiscardChangesCancel();
|
||||||
}
|
}
|
||||||
@@ -72,13 +72,7 @@ export const PromptRow = ({ item, index }: { item: Prompt; index: number }) => {
|
|||||||
openPanel();
|
openPanel();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[
|
[dialogOpen, handleDiscardChangesCancel, isOpen, openPanel, setPanelContent]
|
||||||
dialogOpen,
|
|
||||||
handleDiscardChangesCancel,
|
|
||||||
isOpen,
|
|
||||||
openPanel,
|
|
||||||
setRightPanelContent,
|
|
||||||
]
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleEdit = useCallback(
|
const handleEdit = useCallback(
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import {
|
|||||||
FeatureType,
|
FeatureType,
|
||||||
getCurrentUserFeaturesQuery,
|
getCurrentUserFeaturesQuery,
|
||||||
} from '@affine/graphql';
|
} from '@affine/graphql';
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
import { useMutateQueryResource } from '../use-mutation';
|
import { useMutateQueryResource } from '../use-mutation';
|
||||||
import { useQuery } from '../use-query';
|
import { useQuery } from '../use-query';
|
||||||
@@ -43,3 +44,21 @@ export function isAdmin(
|
|||||||
) {
|
) {
|
||||||
return user.features.includes(FeatureType.Admin);
|
return user.features.includes(FeatureType.Admin);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function useMediaQuery(query: string) {
|
||||||
|
const [value, setValue] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
function onChange(event: MediaQueryListEvent) {
|
||||||
|
setValue(event.matches);
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = matchMedia(query);
|
||||||
|
result.addEventListener('change', onChange);
|
||||||
|
setValue(result.matches);
|
||||||
|
|
||||||
|
return () => result.removeEventListener('change', onChange);
|
||||||
|
}, [query]);
|
||||||
|
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ import {
|
|||||||
CardTitle,
|
CardTitle,
|
||||||
} from '@affine/admin/components/ui/card';
|
} from '@affine/admin/components/ui/card';
|
||||||
import { ScrollArea } from '@affine/admin/components/ui/scroll-area';
|
import { ScrollArea } from '@affine/admin/components/ui/scroll-area';
|
||||||
import { Separator } from '@affine/admin/components/ui/separator';
|
|
||||||
|
|
||||||
|
import { Header } from '../header';
|
||||||
import { AboutAFFiNE } from './about';
|
import { AboutAFFiNE } from './about';
|
||||||
import type {
|
import type {
|
||||||
DatabaseConfig,
|
DatabaseConfig,
|
||||||
@@ -18,10 +18,7 @@ import { useServerServiceConfigs } from './use-server-service-configs';
|
|||||||
export function ConfigPage() {
|
export function ConfigPage() {
|
||||||
return (
|
return (
|
||||||
<div className=" h-screen flex-1 space-y-1 flex-col flex">
|
<div className=" h-screen flex-1 space-y-1 flex-col flex">
|
||||||
<div className="flex items-center justify-between px-6 py-3 max-md:ml-9">
|
<Header title="Server" />
|
||||||
<div className="text-base font-medium">Config</div>
|
|
||||||
</div>
|
|
||||||
<Separator />
|
|
||||||
<ScrollArea>
|
<ScrollArea>
|
||||||
<ServerServiceConfig />
|
<ServerServiceConfig />
|
||||||
<AboutAFFiNE />
|
<AboutAFFiNE />
|
||||||
|
|||||||
81
packages/frontend/admin/src/modules/header.tsx
Normal file
81
packages/frontend/admin/src/modules/header.tsx
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
import { SidebarIcon } from '@blocksuite/icons/rc';
|
||||||
|
import { CheckIcon, XIcon } from 'lucide-react';
|
||||||
|
|
||||||
|
import { Button } from '../components/ui/button';
|
||||||
|
import { Separator } from '../components/ui/separator';
|
||||||
|
import { useMediaQuery } from './common';
|
||||||
|
import { useLeftPanel } from './panel/context';
|
||||||
|
|
||||||
|
export const Header = ({
|
||||||
|
title,
|
||||||
|
endFix,
|
||||||
|
}: {
|
||||||
|
title: string;
|
||||||
|
endFix?: React.ReactNode;
|
||||||
|
}) => {
|
||||||
|
const { togglePanel } = useLeftPanel();
|
||||||
|
const isSmallScreen = useMediaQuery('(max-width: 768px)');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="flex items-center px-6 gap-4 h-[56px]">
|
||||||
|
{isSmallScreen ? (
|
||||||
|
<div className="h-7 w-7 p-1" />
|
||||||
|
) : (
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="h-7 w-7 p-1 hover:bg-gray-200 cursor-pointer"
|
||||||
|
onClick={togglePanel}
|
||||||
|
>
|
||||||
|
<SidebarIcon width={20} height={20} />
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
<Separator orientation="vertical" className="h-5" />
|
||||||
|
<div className="text-[15px] font-semibold">{title}</div>
|
||||||
|
{endFix && <div className="ml-auto">{endFix}</div>}
|
||||||
|
</div>
|
||||||
|
<Separator />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const RightPanelHeader = ({
|
||||||
|
title,
|
||||||
|
handleClose,
|
||||||
|
handleConfirm,
|
||||||
|
canSave,
|
||||||
|
}: {
|
||||||
|
title: string;
|
||||||
|
handleClose: () => void;
|
||||||
|
handleConfirm: () => void;
|
||||||
|
canSave: boolean;
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className=" flex justify-between items-center h-[56px] px-6">
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
size="icon"
|
||||||
|
className="w-7 h-7"
|
||||||
|
variant="ghost"
|
||||||
|
onClick={handleClose}
|
||||||
|
>
|
||||||
|
<XIcon size={20} />
|
||||||
|
</Button>
|
||||||
|
<span className="text-base font-medium">{title}</span>
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
size="icon"
|
||||||
|
className="w-7 h-7"
|
||||||
|
variant="ghost"
|
||||||
|
onClick={handleConfirm}
|
||||||
|
disabled={!canSave}
|
||||||
|
>
|
||||||
|
<CheckIcon size={20} />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<Separator />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,21 +1,14 @@
|
|||||||
import {
|
import {
|
||||||
ResizableHandle,
|
|
||||||
ResizablePanel,
|
ResizablePanel,
|
||||||
ResizablePanelGroup,
|
ResizablePanelGroup,
|
||||||
} from '@affine/admin/components/ui/resizable';
|
} from '@affine/admin/components/ui/resizable';
|
||||||
import { Separator } from '@affine/admin/components/ui/separator';
|
import { Separator } from '@affine/admin/components/ui/separator';
|
||||||
import { TooltipProvider } from '@affine/admin/components/ui/tooltip';
|
import { TooltipProvider } from '@affine/admin/components/ui/tooltip';
|
||||||
import { cn } from '@affine/admin/utils';
|
import { cn } from '@affine/admin/utils';
|
||||||
|
import { cssVarV2 } from '@toeverything/theme/v2';
|
||||||
import { AlignJustifyIcon } from 'lucide-react';
|
import { AlignJustifyIcon } from 'lucide-react';
|
||||||
import type { PropsWithChildren, ReactNode, RefObject } from 'react';
|
import type { PropsWithChildren, ReactNode, RefObject } from 'react';
|
||||||
import {
|
import { useCallback, useRef, useState } from 'react';
|
||||||
createContext,
|
|
||||||
useCallback,
|
|
||||||
useContext,
|
|
||||||
useEffect,
|
|
||||||
useRef,
|
|
||||||
useState,
|
|
||||||
} from 'react';
|
|
||||||
import type { ImperativePanelHandle } from 'react-resizable-panels';
|
import type { ImperativePanelHandle } from 'react-resizable-panels';
|
||||||
|
|
||||||
import { Button } from '../components/ui/button';
|
import { Button } from '../components/ui/button';
|
||||||
@@ -28,97 +21,112 @@ import {
|
|||||||
SheetTrigger,
|
SheetTrigger,
|
||||||
} from '../components/ui/sheet';
|
} from '../components/ui/sheet';
|
||||||
import { Logo } from './accounts/components/logo';
|
import { Logo } from './accounts/components/logo';
|
||||||
|
import { useMediaQuery } from './common';
|
||||||
import { NavContext } from './nav/context';
|
import { NavContext } from './nav/context';
|
||||||
import { Nav } from './nav/nav';
|
import { Nav } from './nav/nav';
|
||||||
|
import {
|
||||||
interface RightPanelContextType {
|
PanelContext,
|
||||||
isOpen: boolean;
|
type ResizablePanelProps,
|
||||||
rightPanelContent: ReactNode;
|
useRightPanel,
|
||||||
setRightPanelContent: (content: ReactNode) => void;
|
} from './panel/context';
|
||||||
togglePanel: () => void;
|
|
||||||
openPanel: () => void;
|
|
||||||
closePanel: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
const RightPanelContext = createContext<RightPanelContextType | undefined>(
|
|
||||||
undefined
|
|
||||||
);
|
|
||||||
|
|
||||||
export const useRightPanel = () => {
|
|
||||||
const context = useContext(RightPanelContext);
|
|
||||||
|
|
||||||
if (!context) {
|
|
||||||
throw new Error('useRightPanel must be used within a RightPanelProvider');
|
|
||||||
}
|
|
||||||
|
|
||||||
return context;
|
|
||||||
};
|
|
||||||
|
|
||||||
export function useMediaQuery(query: string) {
|
|
||||||
const [value, setValue] = useState(false);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
function onChange(event: MediaQueryListEvent) {
|
|
||||||
setValue(event.matches);
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = matchMedia(query);
|
|
||||||
result.addEventListener('change', onChange);
|
|
||||||
setValue(result.matches);
|
|
||||||
|
|
||||||
return () => result.removeEventListener('change', onChange);
|
|
||||||
}, [query]);
|
|
||||||
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function Layout({ children }: PropsWithChildren) {
|
export function Layout({ children }: PropsWithChildren) {
|
||||||
const [rightPanelContent, setRightPanelContent] = useState<ReactNode>(null);
|
const [rightPanelContent, setRightPanelContent] = useState<ReactNode>(null);
|
||||||
const [open, setOpen] = useState(false);
|
const [leftPanelContent, setLeftPanelContent] = useState<ReactNode>(null);
|
||||||
|
const [leftOpen, setLeftOpen] = useState(false);
|
||||||
|
const [rightOpen, setRightOpen] = useState(false);
|
||||||
const rightPanelRef = useRef<ImperativePanelHandle>(null);
|
const rightPanelRef = useRef<ImperativePanelHandle>(null);
|
||||||
|
const leftPanelRef = useRef<ImperativePanelHandle>(null);
|
||||||
|
|
||||||
const [activeTab, setActiveTab] = useState('');
|
const [activeTab, setActiveTab] = useState('');
|
||||||
const [activeSubTab, setActiveSubTab] = useState('auth');
|
const [activeSubTab, setActiveSubTab] = useState('auth');
|
||||||
const [currentModule, setCurrentModule] = useState('auth');
|
const [currentModule, setCurrentModule] = useState('auth');
|
||||||
|
|
||||||
const handleExpand = useCallback(() => {
|
const handleLeftExpand = useCallback(() => {
|
||||||
|
if (leftPanelRef.current?.getSize() === 0) {
|
||||||
|
leftPanelRef.current?.resize(30);
|
||||||
|
}
|
||||||
|
setLeftOpen(true);
|
||||||
|
}, [leftPanelRef]);
|
||||||
|
|
||||||
|
const handleLeftCollapse = useCallback(() => {
|
||||||
|
if (leftPanelRef.current?.getSize() !== 0) {
|
||||||
|
leftPanelRef.current?.resize(0);
|
||||||
|
}
|
||||||
|
setLeftOpen(false);
|
||||||
|
}, [leftPanelRef]);
|
||||||
|
|
||||||
|
const openLeftPanel = useCallback(() => {
|
||||||
|
handleLeftExpand();
|
||||||
|
leftPanelRef.current?.expand();
|
||||||
|
setLeftOpen(true);
|
||||||
|
}, [handleLeftExpand]);
|
||||||
|
|
||||||
|
const closeLeftPanel = useCallback(() => {
|
||||||
|
handleLeftCollapse();
|
||||||
|
leftPanelRef.current?.collapse();
|
||||||
|
setLeftOpen(false);
|
||||||
|
}, [handleLeftCollapse]);
|
||||||
|
|
||||||
|
const toggleLeftPanel = useCallback(
|
||||||
|
() =>
|
||||||
|
leftPanelRef.current?.isCollapsed() ? openLeftPanel() : closeLeftPanel(),
|
||||||
|
[openLeftPanel, closeLeftPanel]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleRightExpand = useCallback(() => {
|
||||||
if (rightPanelRef.current?.getSize() === 0) {
|
if (rightPanelRef.current?.getSize() === 0) {
|
||||||
rightPanelRef.current?.resize(30);
|
rightPanelRef.current?.resize(30);
|
||||||
}
|
}
|
||||||
setOpen(true);
|
setRightOpen(true);
|
||||||
}, [rightPanelRef]);
|
}, [rightPanelRef]);
|
||||||
|
|
||||||
const handleCollapse = useCallback(() => {
|
const handleRightCollapse = useCallback(() => {
|
||||||
if (rightPanelRef.current?.getSize() !== 0) {
|
if (rightPanelRef.current?.getSize() !== 0) {
|
||||||
rightPanelRef.current?.resize(0);
|
rightPanelRef.current?.resize(0);
|
||||||
}
|
}
|
||||||
setOpen(false);
|
setRightOpen(false);
|
||||||
}, [rightPanelRef]);
|
}, [rightPanelRef]);
|
||||||
|
|
||||||
const openPanel = useCallback(() => {
|
const openRightPanel = useCallback(() => {
|
||||||
handleExpand();
|
handleRightExpand();
|
||||||
rightPanelRef.current?.expand();
|
rightPanelRef.current?.expand();
|
||||||
}, [handleExpand]);
|
setRightOpen(true);
|
||||||
|
}, [handleRightExpand]);
|
||||||
|
|
||||||
const closePanel = useCallback(() => {
|
const closeRightPanel = useCallback(() => {
|
||||||
handleCollapse();
|
handleRightCollapse();
|
||||||
rightPanelRef.current?.collapse();
|
rightPanelRef.current?.collapse();
|
||||||
}, [handleCollapse]);
|
setRightOpen(false);
|
||||||
|
}, [handleRightCollapse]);
|
||||||
|
|
||||||
const togglePanel = useCallback(
|
const toggleRightPanel = useCallback(
|
||||||
() => (rightPanelRef.current?.isCollapsed() ? openPanel() : closePanel()),
|
() =>
|
||||||
[closePanel, openPanel]
|
rightPanelRef.current?.isCollapsed()
|
||||||
|
? openRightPanel()
|
||||||
|
: closeRightPanel(),
|
||||||
|
[closeRightPanel, openRightPanel]
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<RightPanelContext.Provider
|
<PanelContext.Provider
|
||||||
value={{
|
value={{
|
||||||
isOpen: open,
|
leftPanel: {
|
||||||
rightPanelContent,
|
isOpen: leftOpen,
|
||||||
setRightPanelContent,
|
panelContent: leftPanelContent,
|
||||||
togglePanel,
|
setPanelContent: setLeftPanelContent,
|
||||||
openPanel,
|
togglePanel: toggleLeftPanel,
|
||||||
closePanel,
|
openPanel: openLeftPanel,
|
||||||
|
closePanel: closeLeftPanel,
|
||||||
|
},
|
||||||
|
rightPanel: {
|
||||||
|
isOpen: rightOpen,
|
||||||
|
panelContent: rightPanelContent,
|
||||||
|
setPanelContent: setRightPanelContent,
|
||||||
|
togglePanel: toggleRightPanel,
|
||||||
|
openPanel: openRightPanel,
|
||||||
|
closePanel: closeRightPanel,
|
||||||
|
},
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<NavContext.Provider
|
<NavContext.Provider
|
||||||
@@ -133,34 +141,41 @@ export function Layout({ children }: PropsWithChildren) {
|
|||||||
>
|
>
|
||||||
<TooltipProvider delayDuration={0}>
|
<TooltipProvider delayDuration={0}>
|
||||||
<div className="flex">
|
<div className="flex">
|
||||||
<LeftPanel />
|
|
||||||
<ResizablePanelGroup direction="horizontal">
|
<ResizablePanelGroup direction="horizontal">
|
||||||
<ResizablePanel id="0" order={0} minSize={50}>
|
<LeftPanel
|
||||||
|
panelRef={leftPanelRef as RefObject<ImperativePanelHandle>}
|
||||||
|
onExpand={handleLeftExpand}
|
||||||
|
onCollapse={handleLeftCollapse}
|
||||||
|
/>
|
||||||
|
<ResizablePanel id="1" order={1} minSize={50} defaultSize={50}>
|
||||||
{children}
|
{children}
|
||||||
</ResizablePanel>
|
</ResizablePanel>
|
||||||
<RightPanel
|
<RightPanel
|
||||||
rightPanelRef={
|
panelRef={rightPanelRef as RefObject<ImperativePanelHandle>}
|
||||||
rightPanelRef as RefObject<ImperativePanelHandle>
|
onExpand={handleRightExpand}
|
||||||
}
|
onCollapse={handleRightCollapse}
|
||||||
onExpand={handleExpand}
|
|
||||||
onCollapse={handleCollapse}
|
|
||||||
/>
|
/>
|
||||||
</ResizablePanelGroup>
|
</ResizablePanelGroup>
|
||||||
</div>
|
</div>
|
||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
</NavContext.Provider>
|
</NavContext.Provider>
|
||||||
</RightPanelContext.Provider>
|
</PanelContext.Provider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export const LeftPanel = () => {
|
export const LeftPanel = ({
|
||||||
|
panelRef,
|
||||||
|
onExpand,
|
||||||
|
onCollapse,
|
||||||
|
}: ResizablePanelProps) => {
|
||||||
const isSmallScreen = useMediaQuery('(max-width: 768px)');
|
const isSmallScreen = useMediaQuery('(max-width: 768px)');
|
||||||
|
const isCollapsed = panelRef.current?.isCollapsed();
|
||||||
|
|
||||||
if (isSmallScreen) {
|
if (isSmallScreen) {
|
||||||
return (
|
return (
|
||||||
<Sheet>
|
<Sheet>
|
||||||
<SheetTrigger asChild>
|
<SheetTrigger asChild>
|
||||||
<Button variant="ghost" className="fixed top-4 left-6 p-0 h-5 w-5">
|
<Button variant="ghost" className="fixed top-5 left-6 p-0 h-5 w-5">
|
||||||
<AlignJustifyIcon size={20} />
|
<AlignJustifyIcon size={20} />
|
||||||
</Button>
|
</Button>
|
||||||
</SheetTrigger>
|
</SheetTrigger>
|
||||||
@@ -189,31 +204,52 @@ export const LeftPanel = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col min-w-52 max-w-sm border-r">
|
<ResizablePanel
|
||||||
|
id="0"
|
||||||
|
order={0}
|
||||||
|
ref={panelRef}
|
||||||
|
defaultSize={15}
|
||||||
|
maxSize={15}
|
||||||
|
minSize={15}
|
||||||
|
collapsible={true}
|
||||||
|
collapsedSize={2}
|
||||||
|
onExpand={onExpand}
|
||||||
|
onCollapse={onCollapse}
|
||||||
|
className={cn(
|
||||||
|
isCollapsed ? 'min-w-[56px] max-w-[56px]' : 'min-w-56 max-w-56',
|
||||||
|
'border-r h-dvh'
|
||||||
|
)}
|
||||||
|
style={{ overflow: 'visible' }}
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className="flex flex-col max-w-56 h-full "
|
||||||
'flex h-[52px] items-center gap-2 px-4 text-base font-medium'
|
style={{
|
||||||
)}
|
backgroundColor: cssVarV2(
|
||||||
|
'selfhost/layer/background/sidebarBg/sidebarBg'
|
||||||
|
),
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<Logo />
|
<div
|
||||||
AFFiNE
|
className={cn(
|
||||||
|
'flex h-[56px] items-center gap-2 px-4 text-base font-medium',
|
||||||
|
isCollapsed && 'justify-center px-2'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Logo />
|
||||||
|
{!isCollapsed && 'AFFiNE'}
|
||||||
|
</div>
|
||||||
|
<Nav isCollapsed={isCollapsed} />
|
||||||
</div>
|
</div>
|
||||||
<Separator />
|
</ResizablePanel>
|
||||||
<Nav />
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
export const RightPanel = ({
|
export const RightPanel = ({
|
||||||
rightPanelRef,
|
panelRef,
|
||||||
onExpand,
|
onExpand,
|
||||||
onCollapse,
|
onCollapse,
|
||||||
}: {
|
}: ResizablePanelProps) => {
|
||||||
rightPanelRef: RefObject<ImperativePanelHandle>;
|
|
||||||
onExpand: () => void;
|
|
||||||
onCollapse: () => void;
|
|
||||||
}) => {
|
|
||||||
const isSmallScreen = useMediaQuery('(max-width: 768px)');
|
const isSmallScreen = useMediaQuery('(max-width: 768px)');
|
||||||
const { rightPanelContent, isOpen } = useRightPanel();
|
const { panelContent, isOpen } = useRightPanel();
|
||||||
const onOpenChange = useCallback(
|
const onOpenChange = useCallback(
|
||||||
(open: boolean) => {
|
(open: boolean) => {
|
||||||
if (open) {
|
if (open) {
|
||||||
@@ -235,28 +271,26 @@ export const RightPanel = ({
|
|||||||
</SheetDescription>
|
</SheetDescription>
|
||||||
</SheetHeader>
|
</SheetHeader>
|
||||||
<SheetContent side="right" className="p-0" withoutCloseButton>
|
<SheetContent side="right" className="p-0" withoutCloseButton>
|
||||||
{rightPanelContent}
|
{panelContent}
|
||||||
</SheetContent>
|
</SheetContent>
|
||||||
</Sheet>
|
</Sheet>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<ResizablePanel
|
||||||
{isOpen ? <ResizableHandle /> : null}
|
id="2"
|
||||||
<ResizablePanel
|
order={2}
|
||||||
id="1"
|
ref={panelRef}
|
||||||
order={1}
|
defaultSize={0}
|
||||||
ref={rightPanelRef}
|
maxSize={30}
|
||||||
defaultSize={0}
|
collapsible={true}
|
||||||
maxSize={30}
|
collapsedSize={0}
|
||||||
collapsible={true}
|
onExpand={onExpand}
|
||||||
collapsedSize={0}
|
onCollapse={onCollapse}
|
||||||
onExpand={onExpand}
|
className="border-l"
|
||||||
onCollapse={onCollapse}
|
>
|
||||||
>
|
{panelContent}
|
||||||
{rightPanelContent}
|
</ResizablePanel>
|
||||||
</ResizablePanel>
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ export const CollapsibleItem = ({
|
|||||||
);
|
);
|
||||||
return (
|
return (
|
||||||
<Accordion type="multiple" className="w-full ">
|
<Accordion type="multiple" className="w-full ">
|
||||||
<AccordionItem value="item-1" className="border-b-0">
|
<AccordionItem value="item-1" className="border-b-0 ml-7 ">
|
||||||
<NavLink
|
<NavLink
|
||||||
to={`/admin/settings/${title}`}
|
to={`/admin/settings/${title}`}
|
||||||
className={({ isActive }) => {
|
className={({ isActive }) => {
|
||||||
@@ -45,12 +45,12 @@ export const CollapsibleItem = ({
|
|||||||
>
|
>
|
||||||
<AccordionTrigger
|
<AccordionTrigger
|
||||||
onClick={() => handleClick(title)}
|
onClick={() => handleClick(title)}
|
||||||
className={`py-2 px-3 rounded`}
|
className="py-2 px-2 rounded [&[data-state=closed]>svg]:rotate-270 [&[data-state=open]>svg]:rotate-360"
|
||||||
>
|
>
|
||||||
{title}
|
{title}
|
||||||
</AccordionTrigger>
|
</AccordionTrigger>
|
||||||
</NavLink>
|
</NavLink>
|
||||||
<AccordionContent className=" flex flex-col gap-2 py-1">
|
<AccordionContent className="flex flex-col gap-2 py-1">
|
||||||
{items.map(item => (
|
{items.map(item => (
|
||||||
<NavLink
|
<NavLink
|
||||||
key={item}
|
key={item}
|
||||||
@@ -74,3 +74,34 @@ export const CollapsibleItem = ({
|
|||||||
</Accordion>
|
</Accordion>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const OtherModules = ({
|
||||||
|
moduleList,
|
||||||
|
changeModule,
|
||||||
|
}: {
|
||||||
|
moduleList: {
|
||||||
|
moduleName: string;
|
||||||
|
keys: string[];
|
||||||
|
}[];
|
||||||
|
changeModule?: (module: string) => void;
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<Accordion type="multiple" className="w-full ">
|
||||||
|
<AccordionItem value="item-1" className="border-b-0">
|
||||||
|
<AccordionTrigger className="ml-7 py-2 px-2 rounded [&[data-state=closed]>svg]:rotate-270 [&[data-state=open]>svg]:rotate-360">
|
||||||
|
Other
|
||||||
|
</AccordionTrigger>
|
||||||
|
<AccordionContent className="flex flex-col gap-2 py-1">
|
||||||
|
{moduleList.map(module => (
|
||||||
|
<CollapsibleItem
|
||||||
|
key={module.moduleName}
|
||||||
|
items={module.keys}
|
||||||
|
title={module.moduleName}
|
||||||
|
changeModule={changeModule}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</AccordionContent>
|
||||||
|
</AccordionItem>
|
||||||
|
</Accordion>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|||||||
62
packages/frontend/admin/src/modules/nav/nav-item.tsx
Normal file
62
packages/frontend/admin/src/modules/nav/nav-item.tsx
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
import { buttonVariants } from '@affine/admin/components/ui/button';
|
||||||
|
import { cn } from '@affine/admin/utils';
|
||||||
|
import { cssVarV2 } from '@toeverything/theme/v2';
|
||||||
|
import { NavLink } from 'react-router-dom';
|
||||||
|
|
||||||
|
interface NavItemProps {
|
||||||
|
icon: React.ReactNode;
|
||||||
|
label: string;
|
||||||
|
to: string;
|
||||||
|
isActive?: boolean;
|
||||||
|
isCollapsed?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const NavItem = ({ icon, label, to, isCollapsed }: NavItemProps) => {
|
||||||
|
if (isCollapsed) {
|
||||||
|
return (
|
||||||
|
<NavLink
|
||||||
|
to={to}
|
||||||
|
className={cn(
|
||||||
|
buttonVariants({
|
||||||
|
variant: 'ghost',
|
||||||
|
className: 'w-10 h-10',
|
||||||
|
size: 'icon',
|
||||||
|
})
|
||||||
|
)}
|
||||||
|
style={({ isActive }) => ({
|
||||||
|
backgroundColor: isActive
|
||||||
|
? cssVarV2('selfhost/button/sidebarButton/bg/select')
|
||||||
|
: undefined,
|
||||||
|
'&:hover': {
|
||||||
|
backgroundColor: cssVarV2('selfhost/button/sidebarButton/bg/hover'),
|
||||||
|
},
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{icon}
|
||||||
|
</NavLink>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<NavLink
|
||||||
|
to={to}
|
||||||
|
className={cn(
|
||||||
|
buttonVariants({
|
||||||
|
variant: 'ghost',
|
||||||
|
}),
|
||||||
|
'justify-start flex-none text-sm font-medium px-2'
|
||||||
|
)}
|
||||||
|
style={({ isActive }) => ({
|
||||||
|
backgroundColor: isActive
|
||||||
|
? cssVarV2('selfhost/button/sidebarButton/bg/select')
|
||||||
|
: undefined,
|
||||||
|
'&:hover': {
|
||||||
|
backgroundColor: cssVarV2('selfhost/button/sidebarButton/bg/hover'),
|
||||||
|
},
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{icon}
|
||||||
|
{label}
|
||||||
|
</NavLink>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,142 +1,123 @@
|
|||||||
import {
|
|
||||||
Accordion,
|
|
||||||
AccordionContent,
|
|
||||||
AccordionItem,
|
|
||||||
AccordionTrigger,
|
|
||||||
} from '@affine/admin/components/ui/accordion';
|
|
||||||
import { buttonVariants } from '@affine/admin/components/ui/button';
|
import { buttonVariants } from '@affine/admin/components/ui/button';
|
||||||
import { cn } from '@affine/admin/utils';
|
import { cn } from '@affine/admin/utils';
|
||||||
import * as ScrollAreaPrimitive from '@radix-ui/react-scroll-area';
|
import { AccountIcon, AiOutlineIcon, SelfhostIcon } from '@blocksuite/icons/rc';
|
||||||
import { ClipboardListIcon, SettingsIcon, UsersIcon } from 'lucide-react';
|
import { cssVarV2 } from '@toeverything/theme/v2';
|
||||||
import { NavLink } from 'react-router-dom';
|
import { NavLink } from 'react-router-dom';
|
||||||
|
|
||||||
import { useGetServerRuntimeConfig } from '../settings/use-get-server-runtime-config';
|
import { SettingsItem } from './settings-item';
|
||||||
import { CollapsibleItem } from './collapsible-item';
|
|
||||||
import { useNav } from './context';
|
|
||||||
import { UserDropdown } from './user-dropdown';
|
import { UserDropdown } from './user-dropdown';
|
||||||
|
|
||||||
export function Nav() {
|
interface NavItemProps {
|
||||||
const { moduleList } = useGetServerRuntimeConfig();
|
icon: React.ReactNode;
|
||||||
const { setCurrentModule } = useNav();
|
label: string;
|
||||||
|
to: string;
|
||||||
|
isActive?: boolean;
|
||||||
|
isCollapsed?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const NavItem = ({ icon, label, to, isCollapsed }: NavItemProps) => {
|
||||||
|
if (isCollapsed) {
|
||||||
|
return (
|
||||||
|
<NavLink
|
||||||
|
to={to}
|
||||||
|
className={cn(
|
||||||
|
buttonVariants({
|
||||||
|
variant: 'ghost',
|
||||||
|
className: 'w-10 h-10',
|
||||||
|
size: 'icon',
|
||||||
|
})
|
||||||
|
)}
|
||||||
|
style={({ isActive }) => ({
|
||||||
|
backgroundColor: isActive
|
||||||
|
? cssVarV2('selfhost/button/sidebarButton/bg/select')
|
||||||
|
: undefined,
|
||||||
|
'&:hover': {
|
||||||
|
backgroundColor: cssVarV2('selfhost/button/sidebarButton/bg/hover'),
|
||||||
|
},
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{icon}
|
||||||
|
</NavLink>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-4 py-2 justify-between flex-grow overflow-hidden">
|
<NavLink
|
||||||
<nav className="flex flex-col gap-1 px-2 flex-grow overflow-hidden">
|
to={to}
|
||||||
<NavLink
|
className={cn(
|
||||||
to={'/admin/accounts'}
|
buttonVariants({
|
||||||
className={({ isActive }) =>
|
variant: 'ghost',
|
||||||
cn(
|
}),
|
||||||
buttonVariants({
|
'justify-start flex-none text-sm font-medium px-2'
|
||||||
variant: isActive ? 'default' : 'ghost',
|
)}
|
||||||
size: 'sm',
|
style={({ isActive }) => ({
|
||||||
}),
|
backgroundColor: isActive
|
||||||
isActive &&
|
? cssVarV2('selfhost/button/sidebarButton/bg/select')
|
||||||
'dark:bg-muted dark:text-white dark:hover:bg-muted dark:hover:text-white',
|
: undefined,
|
||||||
'justify-start',
|
'&:hover': {
|
||||||
'flex-none'
|
backgroundColor: cssVarV2('selfhost/button/sidebarButton/bg/hover'),
|
||||||
)
|
},
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{icon}
|
||||||
|
{label}
|
||||||
|
</NavLink>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
interface NavProps {
|
||||||
|
isCollapsed?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Nav({ isCollapsed = false }: NavProps) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'flex flex-col gap-4 py-2 justify-between flex-grow h-full overflow-hidden',
|
||||||
|
isCollapsed && 'overflow-visible'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<nav
|
||||||
|
className={cn(
|
||||||
|
'flex flex-1 flex-col gap-1 px-2 flex-grow overflow-hidden',
|
||||||
|
isCollapsed && 'items-center px-0 gap-1 overflow-visible'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<NavItem
|
||||||
|
to="/admin/config"
|
||||||
|
icon={
|
||||||
|
<SelfhostIcon className={cn(!isCollapsed && 'mr-2', 'h-5 w-5')} />
|
||||||
}
|
}
|
||||||
>
|
label="Server"
|
||||||
<UsersIcon className="mr-2 h-4 w-4" />
|
isCollapsed={isCollapsed}
|
||||||
Accounts
|
/>
|
||||||
</NavLink>
|
<NavItem
|
||||||
{/* <Link
|
to="/admin/accounts"
|
||||||
to={'/admin/ai'}
|
icon={
|
||||||
className={cn(
|
<AccountIcon className={cn(!isCollapsed && 'mr-2', 'h-5 w-5')} />
|
||||||
buttonVariants({
|
|
||||||
variant: activeTab === 'AI' ? 'default' : 'ghost',
|
|
||||||
size: 'sm',
|
|
||||||
}),
|
|
||||||
activeTab === 'AI' &&
|
|
||||||
'dark:bg-muted dark:text-white dark:hover:bg-muted dark:hover:text-white',
|
|
||||||
'justify-start',
|
|
||||||
'flex-none'
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<CpuIcon className="mr-2 h-4 w-4" />
|
|
||||||
AI
|
|
||||||
</Link> */}
|
|
||||||
<NavLink
|
|
||||||
to={'/admin/config'}
|
|
||||||
className={({ isActive }) =>
|
|
||||||
cn(
|
|
||||||
buttonVariants({
|
|
||||||
variant: isActive ? 'default' : 'ghost',
|
|
||||||
size: 'sm',
|
|
||||||
}),
|
|
||||||
isActive &&
|
|
||||||
'dark:bg-muted dark:text-white dark:hover:bg-muted dark:hover:text-white',
|
|
||||||
'justify-start',
|
|
||||||
'flex-none'
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
>
|
label="Accounts"
|
||||||
<ClipboardListIcon className="mr-2 h-4 w-4" />
|
isCollapsed={isCollapsed}
|
||||||
Config
|
/>
|
||||||
</NavLink>
|
<NavItem
|
||||||
|
to="/admin/ai"
|
||||||
|
icon={
|
||||||
|
<AiOutlineIcon className={cn(!isCollapsed && 'mr-2', 'h-5 w-5')} />
|
||||||
|
}
|
||||||
|
label="AI"
|
||||||
|
isCollapsed={isCollapsed}
|
||||||
|
/>
|
||||||
|
|
||||||
<Accordion type="multiple" className="w-full h-full overflow-hidden">
|
<SettingsItem isCollapsed={isCollapsed} />
|
||||||
<AccordionItem
|
|
||||||
value="item-1"
|
|
||||||
className="border-b-0 h-full flex flex-col gap-1 w-full"
|
|
||||||
>
|
|
||||||
<NavLink
|
|
||||||
to={'/admin/settings'}
|
|
||||||
className={({ isActive }) =>
|
|
||||||
cn(
|
|
||||||
buttonVariants({
|
|
||||||
variant: isActive ? 'default' : 'ghost',
|
|
||||||
size: 'sm',
|
|
||||||
}),
|
|
||||||
isActive &&
|
|
||||||
'dark:bg-muted dark:text-white dark:hover:bg-muted dark:hover:text-white',
|
|
||||||
'justify-start',
|
|
||||||
'flex-none',
|
|
||||||
'w-full'
|
|
||||||
)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<AccordionTrigger
|
|
||||||
className={'flex items-center justify-between w-full'}
|
|
||||||
>
|
|
||||||
<div className="flex items-center">
|
|
||||||
<SettingsIcon className="mr-2 h-4 w-4" />
|
|
||||||
<span>Settings</span>
|
|
||||||
</div>
|
|
||||||
</AccordionTrigger>
|
|
||||||
</NavLink>
|
|
||||||
|
|
||||||
<AccordionContent className="h-full overflow-hidden w-full">
|
|
||||||
<ScrollAreaPrimitive.Root
|
|
||||||
className={cn('relative overflow-hidden w-full h-full')}
|
|
||||||
>
|
|
||||||
<ScrollAreaPrimitive.Viewport className="h-full w-full rounded-[inherit] [&>div]:!block">
|
|
||||||
{moduleList.map(module => (
|
|
||||||
<CollapsibleItem
|
|
||||||
key={module.moduleName}
|
|
||||||
items={module.keys}
|
|
||||||
title={module.moduleName}
|
|
||||||
changeModule={setCurrentModule}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</ScrollAreaPrimitive.Viewport>
|
|
||||||
<ScrollAreaPrimitive.ScrollAreaScrollbar
|
|
||||||
className={cn(
|
|
||||||
'flex touch-none select-none transition-colors',
|
|
||||||
|
|
||||||
'h-full w-2.5 border-l border-l-transparent p-[1px]'
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<ScrollAreaPrimitive.ScrollAreaThumb className="relative flex-1 rounded-full bg-border" />
|
|
||||||
</ScrollAreaPrimitive.ScrollAreaScrollbar>
|
|
||||||
<ScrollAreaPrimitive.Corner />
|
|
||||||
</ScrollAreaPrimitive.Root>
|
|
||||||
</AccordionContent>
|
|
||||||
</AccordionItem>
|
|
||||||
</Accordion>
|
|
||||||
</nav>
|
</nav>
|
||||||
|
<div
|
||||||
<UserDropdown />
|
className={cn(
|
||||||
|
'flex gap-1 px-2 flex-col overflow-hidden',
|
||||||
|
isCollapsed && 'items-center px-0 gap-1'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<UserDropdown isCollapsed={isCollapsed} />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
162
packages/frontend/admin/src/modules/nav/settings-item.tsx
Normal file
162
packages/frontend/admin/src/modules/nav/settings-item.tsx
Normal file
@@ -0,0 +1,162 @@
|
|||||||
|
import {
|
||||||
|
Accordion,
|
||||||
|
AccordionContent,
|
||||||
|
AccordionItem,
|
||||||
|
AccordionTrigger,
|
||||||
|
} from '@affine/admin/components/ui/accordion';
|
||||||
|
import { buttonVariants } from '@affine/admin/components/ui/button';
|
||||||
|
import { cn } from '@affine/admin/utils';
|
||||||
|
import { SettingsIcon } from '@blocksuite/icons/rc';
|
||||||
|
import * as NavigationMenuPrimitive from '@radix-ui/react-navigation-menu';
|
||||||
|
import * as ScrollAreaPrimitive from '@radix-ui/react-scroll-area';
|
||||||
|
import { cssVarV2 } from '@toeverything/theme/v2';
|
||||||
|
import { useMemo } from 'react';
|
||||||
|
import { NavLink } from 'react-router-dom';
|
||||||
|
|
||||||
|
import { useGetServerRuntimeConfig } from '../settings/use-get-server-runtime-config';
|
||||||
|
import { CollapsibleItem, OtherModules } from './collapsible-item';
|
||||||
|
import { useNav } from './context';
|
||||||
|
export const SettingsItem = ({ isCollapsed }: { isCollapsed: boolean }) => {
|
||||||
|
const { moduleList } = useGetServerRuntimeConfig();
|
||||||
|
const { setCurrentModule } = useNav();
|
||||||
|
|
||||||
|
const { authModule, otherModules } = useMemo(() => {
|
||||||
|
const authModule = moduleList.find(module => module.moduleName === 'auth');
|
||||||
|
const otherModules = moduleList.filter(
|
||||||
|
module => module.moduleName !== 'auth'
|
||||||
|
);
|
||||||
|
return { authModule, otherModules };
|
||||||
|
}, [moduleList]);
|
||||||
|
|
||||||
|
if (isCollapsed) {
|
||||||
|
return (
|
||||||
|
<NavigationMenuPrimitive.Root
|
||||||
|
className="flex-none relative"
|
||||||
|
orientation="vertical"
|
||||||
|
>
|
||||||
|
<NavigationMenuPrimitive.List>
|
||||||
|
<NavigationMenuPrimitive.Item>
|
||||||
|
<NavigationMenuPrimitive.Trigger className="[&>svg]:hidden m-0 p-0">
|
||||||
|
<NavLink
|
||||||
|
to={'/admin/settings'}
|
||||||
|
className={cn(
|
||||||
|
buttonVariants({
|
||||||
|
variant: 'ghost',
|
||||||
|
className: 'w-10 h-10',
|
||||||
|
size: 'icon',
|
||||||
|
})
|
||||||
|
)}
|
||||||
|
style={({ isActive }) => ({
|
||||||
|
backgroundColor: isActive
|
||||||
|
? cssVarV2('selfhost/button/sidebarButton/bg/select')
|
||||||
|
: undefined,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<SettingsIcon className="h-5 w-5" />
|
||||||
|
</NavLink>
|
||||||
|
</NavigationMenuPrimitive.Trigger>
|
||||||
|
<NavigationMenuPrimitive.Content>
|
||||||
|
<ul
|
||||||
|
className="border rounded-lg w-full flex flex-col p-1"
|
||||||
|
style={{
|
||||||
|
backgroundColor: cssVarV2('layer/background/overlayPanel'),
|
||||||
|
borderColor: cssVarV2('layer/insideBorder/blackBorder'),
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{moduleList.map(module => (
|
||||||
|
<li key={module.moduleName} className="flex">
|
||||||
|
<NavLink
|
||||||
|
to={`/admin/settings/${module.moduleName}`}
|
||||||
|
className={cn(
|
||||||
|
buttonVariants({
|
||||||
|
variant: 'ghost',
|
||||||
|
className:
|
||||||
|
'p-1.5 rounded-[6px] text-[14px] w-full justify-start',
|
||||||
|
})
|
||||||
|
)}
|
||||||
|
style={({ isActive }) => ({
|
||||||
|
backgroundColor: isActive
|
||||||
|
? cssVarV2('selfhost/button/sidebarButton/bg/select')
|
||||||
|
: undefined,
|
||||||
|
})}
|
||||||
|
onClick={() => setCurrentModule?.(module.moduleName)}
|
||||||
|
>
|
||||||
|
{module.moduleName}
|
||||||
|
</NavLink>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</NavigationMenuPrimitive.Content>
|
||||||
|
</NavigationMenuPrimitive.Item>
|
||||||
|
</NavigationMenuPrimitive.List>
|
||||||
|
<NavigationMenuPrimitive.Viewport className="absolute z-10 left-11 top-0" />
|
||||||
|
</NavigationMenuPrimitive.Root>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<Accordion type="multiple" className="w-full h-full overflow-hidden">
|
||||||
|
<AccordionItem
|
||||||
|
value="item-1"
|
||||||
|
className="border-b-0 h-full flex flex-col gap-1 w-full"
|
||||||
|
>
|
||||||
|
<NavLink
|
||||||
|
to={'/admin/settings'}
|
||||||
|
className={cn(
|
||||||
|
buttonVariants({
|
||||||
|
variant: 'ghost',
|
||||||
|
}),
|
||||||
|
'justify-start flex-none w-full px-2'
|
||||||
|
)}
|
||||||
|
style={({ isActive }) => ({
|
||||||
|
backgroundColor: isActive
|
||||||
|
? cssVarV2('selfhost/button/sidebarButton/bg/select')
|
||||||
|
: undefined,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<AccordionTrigger
|
||||||
|
className={
|
||||||
|
'flex items-center justify-between w-full [&[data-state=closed]>svg]:rotate-270 [&[data-state=open]>svg]:rotate-360'
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<SettingsIcon className={cn(!isCollapsed && 'mr-2', 'h-5 w-5')} />
|
||||||
|
<span>Settings</span>
|
||||||
|
</div>
|
||||||
|
</AccordionTrigger>
|
||||||
|
</NavLink>
|
||||||
|
|
||||||
|
<AccordionContent className="h-full overflow-hidden w-full">
|
||||||
|
<ScrollAreaPrimitive.Root
|
||||||
|
className={cn('relative overflow-hidden w-full h-full')}
|
||||||
|
>
|
||||||
|
<ScrollAreaPrimitive.Viewport className="h-full w-full rounded-[inherit] [&>div]:!block">
|
||||||
|
{authModule && (
|
||||||
|
<CollapsibleItem
|
||||||
|
items={authModule.keys}
|
||||||
|
title={authModule.moduleName}
|
||||||
|
changeModule={setCurrentModule}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{otherModules.length > 0 && (
|
||||||
|
<OtherModules
|
||||||
|
moduleList={otherModules}
|
||||||
|
changeModule={setCurrentModule}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</ScrollAreaPrimitive.Viewport>
|
||||||
|
<ScrollAreaPrimitive.ScrollAreaScrollbar
|
||||||
|
className={cn(
|
||||||
|
'flex touch-none select-none transition-colors',
|
||||||
|
|
||||||
|
'h-full w-2.5 border-l border-l-transparent p-[1px]'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<ScrollAreaPrimitive.ScrollAreaThumb className="relative flex-1 rounded-full bg-border" />
|
||||||
|
</ScrollAreaPrimitive.ScrollAreaScrollbar>
|
||||||
|
<ScrollAreaPrimitive.Corner />
|
||||||
|
</ScrollAreaPrimitive.Root>
|
||||||
|
</AccordionContent>
|
||||||
|
</AccordionItem>
|
||||||
|
</Accordion>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -18,7 +18,11 @@ import { toast } from 'sonner';
|
|||||||
|
|
||||||
import { useCurrentUser, useRevalidateCurrentUser } from '../common';
|
import { useCurrentUser, useRevalidateCurrentUser } from '../common';
|
||||||
|
|
||||||
export function UserDropdown() {
|
interface UserDropdownProps {
|
||||||
|
isCollapsed: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function UserDropdown({ isCollapsed }: UserDropdownProps) {
|
||||||
const currentUser = useCurrentUser();
|
const currentUser = useCurrentUser();
|
||||||
const relative = useRevalidateCurrentUser();
|
const relative = useRevalidateCurrentUser();
|
||||||
|
|
||||||
@@ -33,10 +37,34 @@ export function UserDropdown() {
|
|||||||
});
|
});
|
||||||
}, [relative]);
|
}, [relative]);
|
||||||
|
|
||||||
|
if (isCollapsed) {
|
||||||
|
return (
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<Button variant="ghost" className="w-10 h-10" size="icon">
|
||||||
|
<Avatar className="w-5 h-5">
|
||||||
|
<AvatarImage src={currentUser?.avatarUrl ?? undefined} />
|
||||||
|
<AvatarFallback>
|
||||||
|
<CircleUser size={24} />
|
||||||
|
</AvatarFallback>
|
||||||
|
</Avatar>
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent align="end" side="right">
|
||||||
|
<DropdownMenuLabel>{currentUser?.name}</DropdownMenuLabel>
|
||||||
|
<DropdownMenuSeparator />
|
||||||
|
<DropdownMenuItem onSelect={handleLogout}>Logout</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-none items-center justify-between px-4 py-3 flex-nowrap">
|
<div
|
||||||
<div className="flex items-center gap-2 font-medium text-ellipsis break-words overflow-hidden">
|
className={`flex flex-none items-center ${isCollapsed ? 'justify-center' : 'justify-between'} px-1 py-3 flex-nowrap`}
|
||||||
<Avatar className="w-6 h-6">
|
>
|
||||||
|
<div className="flex items-center gap-2 font-medium text-ellipsis break-words overflow-hidden">
|
||||||
|
<Avatar className="w-5 h-5">
|
||||||
<AvatarImage src={currentUser?.avatarUrl ?? undefined} />
|
<AvatarImage src={currentUser?.avatarUrl ?? undefined} />
|
||||||
<AvatarFallback>
|
<AvatarFallback>
|
||||||
<CircleUser size={24} />
|
<CircleUser size={24} />
|
||||||
@@ -66,7 +94,7 @@ export function UserDropdown() {
|
|||||||
<MoreVertical size={20} />
|
<MoreVertical size={20} />
|
||||||
</Button>
|
</Button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent align="end">
|
<DropdownMenuContent align="end" side="right">
|
||||||
<DropdownMenuLabel>{currentUser?.name}</DropdownMenuLabel>
|
<DropdownMenuLabel>{currentUser?.name}</DropdownMenuLabel>
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
<DropdownMenuItem onSelect={handleLogout}>Logout</DropdownMenuItem>
|
<DropdownMenuItem onSelect={handleLogout}>Logout</DropdownMenuItem>
|
||||||
|
|||||||
51
packages/frontend/admin/src/modules/panel/context.ts
Normal file
51
packages/frontend/admin/src/modules/panel/context.ts
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
import {
|
||||||
|
createContext,
|
||||||
|
type ReactNode,
|
||||||
|
type RefObject,
|
||||||
|
useContext,
|
||||||
|
} from 'react';
|
||||||
|
import type { ImperativePanelHandle } from 'react-resizable-panels';
|
||||||
|
|
||||||
|
export type SinglePanelContextType = {
|
||||||
|
isOpen: boolean;
|
||||||
|
panelContent: ReactNode;
|
||||||
|
setPanelContent: (content: ReactNode) => void;
|
||||||
|
togglePanel: () => void;
|
||||||
|
openPanel: () => void;
|
||||||
|
closePanel: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface PanelContextType {
|
||||||
|
leftPanel: SinglePanelContextType;
|
||||||
|
rightPanel: SinglePanelContextType;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ResizablePanelProps = {
|
||||||
|
panelRef: RefObject<ImperativePanelHandle>;
|
||||||
|
onExpand: () => void;
|
||||||
|
onCollapse: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const PanelContext = createContext<PanelContextType | undefined>(
|
||||||
|
undefined
|
||||||
|
);
|
||||||
|
|
||||||
|
export const usePanelContext = () => {
|
||||||
|
const context = useContext(PanelContext);
|
||||||
|
|
||||||
|
if (!context) {
|
||||||
|
throw new Error('usePanelContext must be used within a PanelProvider');
|
||||||
|
}
|
||||||
|
|
||||||
|
return context;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useLeftPanel = () => {
|
||||||
|
const context = usePanelContext();
|
||||||
|
return context.leftPanel;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useRightPanel = () => {
|
||||||
|
const context = usePanelContext();
|
||||||
|
return context.rightPanel;
|
||||||
|
};
|
||||||
@@ -6,6 +6,7 @@ import { CheckIcon } from 'lucide-react';
|
|||||||
import type { Dispatch, SetStateAction } from 'react';
|
import type { Dispatch, SetStateAction } from 'react';
|
||||||
import { useCallback, useMemo, useState } from 'react';
|
import { useCallback, useMemo, useState } from 'react';
|
||||||
|
|
||||||
|
import { Header } from '../header';
|
||||||
import { useNav } from '../nav/context';
|
import { useNav } from '../nav/context';
|
||||||
import { ConfirmChanges } from './confirm-changes';
|
import { ConfirmChanges } from './confirm-changes';
|
||||||
import { RuntimeSettingRow } from './runtime-setting-row';
|
import { RuntimeSettingRow } from './runtime-setting-row';
|
||||||
@@ -71,20 +72,21 @@ export function SettingsPage() {
|
|||||||
}, [disableSave, handleSave, onClose]);
|
}, [disableSave, handleSave, onClose]);
|
||||||
return (
|
return (
|
||||||
<div className=" h-screen flex-1 flex-col flex">
|
<div className=" h-screen flex-1 flex-col flex">
|
||||||
<div className="flex items-center justify-between px-6 py-3 max-md:ml-9">
|
<Header
|
||||||
<div className="text-base font-medium">Settings</div>
|
title="Settings"
|
||||||
<Button
|
endFix={
|
||||||
type="submit"
|
<Button
|
||||||
size="icon"
|
type="submit"
|
||||||
className="w-7 h-7"
|
size="icon"
|
||||||
variant="ghost"
|
className="w-7 h-7"
|
||||||
onClick={onOpen}
|
variant="ghost"
|
||||||
disabled={disableSave}
|
onClick={onOpen}
|
||||||
>
|
disabled={disableSave}
|
||||||
<CheckIcon size={20} />
|
>
|
||||||
</Button>
|
<CheckIcon size={20} />
|
||||||
</div>
|
</Button>
|
||||||
<Separator />
|
}
|
||||||
|
/>
|
||||||
<AdminPanel
|
<AdminPanel
|
||||||
configValues={configValues}
|
configValues={configValues}
|
||||||
setConfigValues={setConfigValues}
|
setConfigValues={setConfigValues}
|
||||||
@@ -124,7 +126,7 @@ export const AdminPanel = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<ScrollArea>
|
<ScrollArea>
|
||||||
<div className="flex flex-col h-full gap-3 py-5 px-6 w-full">
|
<div className="flex flex-col h-full gap-3 py-5 px-6 w-full max-w-[800px] mx-auto">
|
||||||
{configGroup
|
{configGroup
|
||||||
.filter(group => group.moduleName === currentModule)
|
.filter(group => group.moduleName === currentModule)
|
||||||
.map(group => {
|
.map(group => {
|
||||||
|
|||||||
@@ -161,6 +161,7 @@ __metadata:
|
|||||||
"@affine/component": "workspace:*"
|
"@affine/component": "workspace:*"
|
||||||
"@affine/core": "workspace:*"
|
"@affine/core": "workspace:*"
|
||||||
"@affine/graphql": "workspace:*"
|
"@affine/graphql": "workspace:*"
|
||||||
|
"@blocksuite/icons": "npm:^2.2.4"
|
||||||
"@radix-ui/react-accordion": "npm:^1.2.2"
|
"@radix-ui/react-accordion": "npm:^1.2.2"
|
||||||
"@radix-ui/react-alert-dialog": "npm:^1.1.3"
|
"@radix-ui/react-alert-dialog": "npm:^1.1.3"
|
||||||
"@radix-ui/react-aspect-ratio": "npm:^1.1.1"
|
"@radix-ui/react-aspect-ratio": "npm:^1.1.1"
|
||||||
@@ -191,6 +192,7 @@ __metadata:
|
|||||||
"@sentry/react": "npm:^9.2.0"
|
"@sentry/react": "npm:^9.2.0"
|
||||||
"@tanstack/react-table": "npm:^8.20.5"
|
"@tanstack/react-table": "npm:^8.20.5"
|
||||||
"@toeverything/infra": "workspace:*"
|
"@toeverything/infra": "workspace:*"
|
||||||
|
"@toeverything/theme": "npm:^1.1.12"
|
||||||
class-variance-authority: "npm:^0.7.1"
|
class-variance-authority: "npm:^0.7.1"
|
||||||
clsx: "npm:^2.1.1"
|
clsx: "npm:^2.1.1"
|
||||||
cmdk: "npm:^1.0.4"
|
cmdk: "npm:^1.0.4"
|
||||||
@@ -3278,7 +3280,7 @@ __metadata:
|
|||||||
languageName: unknown
|
languageName: unknown
|
||||||
linkType: soft
|
linkType: soft
|
||||||
|
|
||||||
"@blocksuite/icons@npm:^2.2.6":
|
"@blocksuite/icons@npm:^2.2.4, @blocksuite/icons@npm:^2.2.6":
|
||||||
version: 2.2.6
|
version: 2.2.6
|
||||||
resolution: "@blocksuite/icons@npm:2.2.6"
|
resolution: "@blocksuite/icons@npm:2.2.6"
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
|
|||||||
Reference in New Issue
Block a user