diff --git a/packages/frontend/admin/package.json b/packages/frontend/admin/package.json index 578fcf6616..6810674333 100644 --- a/packages/frontend/admin/package.json +++ b/packages/frontend/admin/package.json @@ -6,6 +6,7 @@ "@affine/component": "workspace:*", "@affine/core": "workspace:*", "@affine/graphql": "workspace:*", + "@blocksuite/icons": "^2.2.4", "@radix-ui/react-accordion": "^1.2.2", "@radix-ui/react-alert-dialog": "^1.1.3", "@radix-ui/react-aspect-ratio": "^1.1.1", @@ -36,6 +37,7 @@ "@sentry/react": "^9.2.0", "@tanstack/react-table": "^8.20.5", "@toeverything/infra": "workspace:*", + "@toeverything/theme": "^1.1.12", "cmdk": "^1.0.4", "embla-carousel-react": "^8.5.1", "input-otp": "^1.4.1", diff --git a/packages/frontend/admin/src/app.tsx b/packages/frontend/admin/src/app.tsx index e2eb435bf1..b7ee341395 100644 --- a/packages/frontend/admin/src/app.tsx +++ b/packages/frontend/admin/src/app.tsx @@ -80,10 +80,10 @@ export const router = _createBrowserRouter( path: 'accounts', lazy: () => import('./modules/accounts'), }, - // { - // path: 'ai', - // lazy: () => import('./modules/ai'), - // }, + { + path: 'ai', + lazy: () => import('./modules/ai'), + }, { path: 'config', lazy: () => import('./modules/config'), diff --git a/packages/frontend/admin/src/global.css b/packages/frontend/admin/src/global.css index 0935b17cfa..d2786bcc51 100644 --- a/packages/frontend/admin/src/global.css +++ b/packages/frontend/admin/src/global.css @@ -2,6 +2,7 @@ @import 'tailwindcss'; @import 'tailwindcss/utilities'; +@import '@toeverything/theme/style.css'; @plugin 'tailwindcss-animate'; diff --git a/packages/frontend/admin/src/modules/accounts/components/data-table-row-actions.tsx b/packages/frontend/admin/src/modules/accounts/components/data-table-row-actions.tsx index 5ff72b2c88..89e9fe9914 100644 --- a/packages/frontend/admin/src/modules/accounts/components/data-table-row-actions.tsx +++ b/packages/frontend/admin/src/modules/accounts/components/data-table-row-actions.tsx @@ -15,7 +15,7 @@ import { import { useCallback, useState } from 'react'; import { toast } from 'sonner'; -import { useRightPanel } from '../../layout'; +import { useRightPanel } from '../../panel/context'; import type { UserType } from '../schema'; import { DeleteAccountDialog } from './delete-account'; import { DiscardChanges } from './discard-changes'; @@ -31,8 +31,7 @@ export function DataTableRowActions({ user }: DataTableRowActionsProps) { const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); const [resetPasswordDialogOpen, setResetPasswordDialogOpen] = useState(false); const [discardDialogOpen, setDiscardDialogOpen] = useState(false); - const { setRightPanelContent, openPanel, isOpen, closePanel } = - useRightPanel(); + const { openPanel, isOpen, closePanel, setPanelContent } = useRightPanel(); const deleteUser = useDeleteUser(); const { resetPasswordLink, onResetPassword } = useResetUserPassword(); @@ -81,7 +80,7 @@ export function DataTableRowActions({ user }: DataTableRowActionsProps) { }, []); const handleConfirm = useCallback(() => { - setRightPanelContent( + setPanelContent( ({ const [value, setValue] = useState(''); const [dialogOpen, setDialogOpen] = useState(false); const debouncedValue = useDebouncedValue(value, 1000); - const { setRightPanelContent, openPanel, closePanel, isOpen } = - useRightPanel(); + const { setPanelContent, openPanel, closePanel, isOpen } = useRightPanel(); const { result, query } = useSearch(); const handleConfirm = useCallback(() => { - setRightPanelContent(); + setPanelContent(); if (dialogOpen) { setDialogOpen(false); } if (!isOpen) { openPanel(); } - }, [setRightPanelContent, closePanel, dialogOpen, isOpen, openPanel]); + }, [setPanelContent, closePanel, dialogOpen, isOpen, openPanel]); useEffect(() => { query(debouncedValue); diff --git a/packages/frontend/admin/src/modules/accounts/components/user-form.tsx b/packages/frontend/admin/src/modules/accounts/components/user-form.tsx index 4db0666d37..5008d14ecf 100644 --- a/packages/frontend/admin/src/modules/accounts/components/user-form.tsx +++ b/packages/frontend/admin/src/modules/accounts/components/user-form.tsx @@ -4,11 +4,12 @@ import { Label } from '@affine/admin/components/ui/label'; import { Separator } from '@affine/admin/components/ui/separator'; import { Switch } from '@affine/admin/components/ui/switch'; import type { FeatureType } from '@affine/graphql'; -import { CheckIcon, ChevronRightIcon, XIcon } from 'lucide-react'; +import { ChevronRightIcon } from 'lucide-react'; import type { ChangeEvent } from 'react'; import { useCallback, useEffect, useMemo, useState } from 'react'; import { useServerConfig } from '../../common'; +import { RightPanelHeader } from '../../header'; import type { UserInput, UserType } from '../schema'; import { useCreateUser, useUpdateUser } from './use-user-management'; @@ -92,29 +93,12 @@ function UserForm({ return (
-
- - {title} - -
- +
-
-
Accounts
-
- +
-
- - Edit Prompt - -
- +
diff --git a/packages/frontend/admin/src/modules/ai/index.tsx b/packages/frontend/admin/src/modules/ai/index.tsx index c04ee309a9..15aa3ad9f0 100644 --- a/packages/frontend/admin/src/modules/ai/index.tsx +++ b/packages/frontend/admin/src/modules/ai/index.tsx @@ -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 * as ScrollAreaPrimitive from '@radix-ui/react-scroll-area'; +import { useState } from 'react'; -import { Prompts } from './prompts'; +import { Header } from '../header'; function AiPage() { + const [enableAi, setEnableAi] = useState(false); + return ( -
-
-
AI
-
- +
+
- +
+
AI
+
+
+

Enable AI

+

+ AI functionality is not currently supported. Self-hosted AI + support is in progress. +

+
+ +
+
+ {/* */}
diff --git a/packages/frontend/admin/src/modules/ai/prompts.tsx b/packages/frontend/admin/src/modules/ai/prompts.tsx index 477562a3f3..e644522b52 100644 --- a/packages/frontend/admin/src/modules/ai/prompts.tsx +++ b/packages/frontend/admin/src/modules/ai/prompts.tsx @@ -3,7 +3,7 @@ import { Separator } from '@affine/admin/components/ui/separator'; import type { CopilotPromptMessageRole } from '@affine/graphql'; import { useCallback, useState } from 'react'; -import { useRightPanel } from '../layout'; +import { useRightPanel } from '../panel/context'; import { DiscardChanges } from './discard-changes'; import { EditPrompt } from './edit-prompt'; import { usePrompt } from './use-prompt'; @@ -52,7 +52,7 @@ export function Prompts() { } 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 [canSave, setCanSave] = useState(false); @@ -63,7 +63,7 @@ export const PromptRow = ({ item, index }: { item: Prompt; index: number }) => { const handleConfirm = useCallback( (item: Prompt) => { - setRightPanelContent(); + setPanelContent(); if (dialogOpen) { handleDiscardChangesCancel(); } @@ -72,13 +72,7 @@ export const PromptRow = ({ item, index }: { item: Prompt; index: number }) => { openPanel(); } }, - [ - dialogOpen, - handleDiscardChangesCancel, - isOpen, - openPanel, - setRightPanelContent, - ] + [dialogOpen, handleDiscardChangesCancel, isOpen, openPanel, setPanelContent] ); const handleEdit = useCallback( diff --git a/packages/frontend/admin/src/modules/common.ts b/packages/frontend/admin/src/modules/common.ts index 046e44fc98..e329e5455a 100644 --- a/packages/frontend/admin/src/modules/common.ts +++ b/packages/frontend/admin/src/modules/common.ts @@ -4,6 +4,7 @@ import { FeatureType, getCurrentUserFeaturesQuery, } from '@affine/graphql'; +import { useEffect, useState } from 'react'; import { useMutateQueryResource } from '../use-mutation'; import { useQuery } from '../use-query'; @@ -43,3 +44,21 @@ export function isAdmin( ) { 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; +} diff --git a/packages/frontend/admin/src/modules/config/index.tsx b/packages/frontend/admin/src/modules/config/index.tsx index 3612628e98..21a8b1a563 100644 --- a/packages/frontend/admin/src/modules/config/index.tsx +++ b/packages/frontend/admin/src/modules/config/index.tsx @@ -5,8 +5,8 @@ import { CardTitle, } from '@affine/admin/components/ui/card'; 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 type { DatabaseConfig, @@ -18,10 +18,7 @@ import { useServerServiceConfigs } from './use-server-service-configs'; export function ConfigPage() { return (
-
-
Config
-
- +
diff --git a/packages/frontend/admin/src/modules/header.tsx b/packages/frontend/admin/src/modules/header.tsx new file mode 100644 index 0000000000..34d1863e39 --- /dev/null +++ b/packages/frontend/admin/src/modules/header.tsx @@ -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 ( +
+
+ {isSmallScreen ? ( +
+ ) : ( + + )} + +
{title}
+ {endFix &&
{endFix}
} +
+ +
+ ); +}; + +export const RightPanelHeader = ({ + title, + handleClose, + handleConfirm, + canSave, +}: { + title: string; + handleClose: () => void; + handleConfirm: () => void; + canSave: boolean; +}) => { + return ( +
+
+ + {title} + +
+ +
+ ); +}; diff --git a/packages/frontend/admin/src/modules/layout.tsx b/packages/frontend/admin/src/modules/layout.tsx index db0ceee49a..b9ffa46c51 100644 --- a/packages/frontend/admin/src/modules/layout.tsx +++ b/packages/frontend/admin/src/modules/layout.tsx @@ -1,21 +1,14 @@ import { - ResizableHandle, ResizablePanel, ResizablePanelGroup, } from '@affine/admin/components/ui/resizable'; import { Separator } from '@affine/admin/components/ui/separator'; import { TooltipProvider } from '@affine/admin/components/ui/tooltip'; import { cn } from '@affine/admin/utils'; +import { cssVarV2 } from '@toeverything/theme/v2'; import { AlignJustifyIcon } from 'lucide-react'; import type { PropsWithChildren, ReactNode, RefObject } from 'react'; -import { - createContext, - useCallback, - useContext, - useEffect, - useRef, - useState, -} from 'react'; +import { useCallback, useRef, useState } from 'react'; import type { ImperativePanelHandle } from 'react-resizable-panels'; import { Button } from '../components/ui/button'; @@ -28,97 +21,112 @@ import { SheetTrigger, } from '../components/ui/sheet'; import { Logo } from './accounts/components/logo'; +import { useMediaQuery } from './common'; import { NavContext } from './nav/context'; import { Nav } from './nav/nav'; - -interface RightPanelContextType { - isOpen: boolean; - rightPanelContent: ReactNode; - setRightPanelContent: (content: ReactNode) => void; - togglePanel: () => void; - openPanel: () => void; - closePanel: () => void; -} - -const RightPanelContext = createContext( - 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; -} +import { + PanelContext, + type ResizablePanelProps, + useRightPanel, +} from './panel/context'; export function Layout({ children }: PropsWithChildren) { const [rightPanelContent, setRightPanelContent] = useState(null); - const [open, setOpen] = useState(false); + const [leftPanelContent, setLeftPanelContent] = useState(null); + const [leftOpen, setLeftOpen] = useState(false); + const [rightOpen, setRightOpen] = useState(false); const rightPanelRef = useRef(null); + const leftPanelRef = useRef(null); const [activeTab, setActiveTab] = useState(''); const [activeSubTab, setActiveSubTab] = 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) { rightPanelRef.current?.resize(30); } - setOpen(true); + setRightOpen(true); }, [rightPanelRef]); - const handleCollapse = useCallback(() => { + const handleRightCollapse = useCallback(() => { if (rightPanelRef.current?.getSize() !== 0) { rightPanelRef.current?.resize(0); } - setOpen(false); + setRightOpen(false); }, [rightPanelRef]); - const openPanel = useCallback(() => { - handleExpand(); + const openRightPanel = useCallback(() => { + handleRightExpand(); rightPanelRef.current?.expand(); - }, [handleExpand]); + setRightOpen(true); + }, [handleRightExpand]); - const closePanel = useCallback(() => { - handleCollapse(); + const closeRightPanel = useCallback(() => { + handleRightCollapse(); rightPanelRef.current?.collapse(); - }, [handleCollapse]); + setRightOpen(false); + }, [handleRightCollapse]); - const togglePanel = useCallback( - () => (rightPanelRef.current?.isCollapsed() ? openPanel() : closePanel()), - [closePanel, openPanel] + const toggleRightPanel = useCallback( + () => + rightPanelRef.current?.isCollapsed() + ? openRightPanel() + : closeRightPanel(), + [closeRightPanel, openRightPanel] ); return ( -
- - + } + onExpand={handleLeftExpand} + onCollapse={handleLeftCollapse} + /> + {children} - } - onExpand={handleExpand} - onCollapse={handleCollapse} + panelRef={rightPanelRef as RefObject} + onExpand={handleRightExpand} + onCollapse={handleRightCollapse} />
-
+ ); } -export const LeftPanel = () => { +export const LeftPanel = ({ + panelRef, + onExpand, + onCollapse, +}: ResizablePanelProps) => { const isSmallScreen = useMediaQuery('(max-width: 768px)'); + const isCollapsed = panelRef.current?.isCollapsed(); if (isSmallScreen) { return ( - @@ -189,31 +204,52 @@ export const LeftPanel = () => { } return ( -
+
- - AFFiNE +
+ + {!isCollapsed && 'AFFiNE'} +
+
- -
+ ); }; export const RightPanel = ({ - rightPanelRef, + panelRef, onExpand, onCollapse, -}: { - rightPanelRef: RefObject; - onExpand: () => void; - onCollapse: () => void; -}) => { +}: ResizablePanelProps) => { const isSmallScreen = useMediaQuery('(max-width: 768px)'); - const { rightPanelContent, isOpen } = useRightPanel(); + const { panelContent, isOpen } = useRightPanel(); const onOpenChange = useCallback( (open: boolean) => { if (open) { @@ -235,28 +271,26 @@ export const RightPanel = ({ - {rightPanelContent} + {panelContent}
); } return ( - <> - {isOpen ? : null} - - {rightPanelContent} - - + + {panelContent} + ); }; diff --git a/packages/frontend/admin/src/modules/nav/collapsible-item.tsx b/packages/frontend/admin/src/modules/nav/collapsible-item.tsx index bac09e9c2b..678c710a55 100644 --- a/packages/frontend/admin/src/modules/nav/collapsible-item.tsx +++ b/packages/frontend/admin/src/modules/nav/collapsible-item.tsx @@ -34,7 +34,7 @@ export const CollapsibleItem = ({ ); return ( - + { @@ -45,12 +45,12 @@ export const CollapsibleItem = ({ > 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} - + {items.map(item => ( ); }; + +export const OtherModules = ({ + moduleList, + changeModule, +}: { + moduleList: { + moduleName: string; + keys: string[]; + }[]; + changeModule?: (module: string) => void; +}) => { + return ( + + + + Other + + + {moduleList.map(module => ( + + ))} + + + + ); +}; diff --git a/packages/frontend/admin/src/modules/nav/nav-item.tsx b/packages/frontend/admin/src/modules/nav/nav-item.tsx new file mode 100644 index 0000000000..89be4f1a3d --- /dev/null +++ b/packages/frontend/admin/src/modules/nav/nav-item.tsx @@ -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 ( + ({ + backgroundColor: isActive + ? cssVarV2('selfhost/button/sidebarButton/bg/select') + : undefined, + '&:hover': { + backgroundColor: cssVarV2('selfhost/button/sidebarButton/bg/hover'), + }, + })} + > + {icon} + + ); + } + + return ( + ({ + backgroundColor: isActive + ? cssVarV2('selfhost/button/sidebarButton/bg/select') + : undefined, + '&:hover': { + backgroundColor: cssVarV2('selfhost/button/sidebarButton/bg/hover'), + }, + })} + > + {icon} + {label} + + ); +}; diff --git a/packages/frontend/admin/src/modules/nav/nav.tsx b/packages/frontend/admin/src/modules/nav/nav.tsx index 9dccbba36c..028f4a9285 100644 --- a/packages/frontend/admin/src/modules/nav/nav.tsx +++ b/packages/frontend/admin/src/modules/nav/nav.tsx @@ -1,142 +1,123 @@ -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 * as ScrollAreaPrimitive from '@radix-ui/react-scroll-area'; -import { ClipboardListIcon, SettingsIcon, UsersIcon } from 'lucide-react'; +import { AccountIcon, AiOutlineIcon, SelfhostIcon } from '@blocksuite/icons/rc'; +import { cssVarV2 } from '@toeverything/theme/v2'; import { NavLink } from 'react-router-dom'; -import { useGetServerRuntimeConfig } from '../settings/use-get-server-runtime-config'; -import { CollapsibleItem } from './collapsible-item'; -import { useNav } from './context'; +import { SettingsItem } from './settings-item'; import { UserDropdown } from './user-dropdown'; -export function Nav() { - const { moduleList } = useGetServerRuntimeConfig(); - const { setCurrentModule } = useNav(); +interface NavItemProps { + icon: React.ReactNode; + label: string; + to: string; + isActive?: boolean; + isCollapsed?: boolean; +} + +const NavItem = ({ icon, label, to, isCollapsed }: NavItemProps) => { + if (isCollapsed) { + return ( + ({ + backgroundColor: isActive + ? cssVarV2('selfhost/button/sidebarButton/bg/select') + : undefined, + '&:hover': { + backgroundColor: cssVarV2('selfhost/button/sidebarButton/bg/hover'), + }, + })} + > + {icon} + + ); + } return ( -
-