From 38eb3236442715eebc54af94a4fb47718b25cbd1 Mon Sep 17 00:00:00 2001 From: Kilu Date: Tue, 24 Jun 2025 18:54:34 +0800 Subject: [PATCH] chore: replace workspace manager to new token and components --- src/@types/translations/en.json | 9 +- src/assets/icons/success_logo.svg | 2 +- .../_shared/outline/OutlineDrawer.tsx | 40 ++- .../app/workspaces/CreateWorkspace.tsx | 155 +++++---- .../app/workspaces/CurrentWorkspace.tsx | 22 +- .../app/workspaces/DeleteWorkspace.tsx | 95 +++--- .../app/workspaces/InviteMember.tsx | 32 +- .../app/workspaces/LeaveWorkspace.tsx | 108 +++--- src/components/app/workspaces/MoreActions.tsx | 135 ++++---- .../app/workspaces/RenameWorkspace.tsx | 101 ------ .../app/workspaces/WorkspaceItem.tsx | 103 ++++++ .../app/workspaces/WorkspaceList.tsx | 100 ++---- src/components/app/workspaces/Workspaces.tsx | 317 ++++++++++++------ .../database/components/tabs/DatabaseTabs.tsx | 2 +- src/components/ui/dropdown-menu.tsx | 2 +- 15 files changed, 673 insertions(+), 550 deletions(-) delete mode 100644 src/components/app/workspaces/RenameWorkspace.tsx create mode 100644 src/components/app/workspaces/WorkspaceItem.tsx diff --git a/src/@types/translations/en.json b/src/@types/translations/en.json index 9402fa48..16771647 100644 --- a/src/@types/translations/en.json +++ b/src/@types/translations/en.json @@ -106,7 +106,14 @@ "cannotDeleteTheOnlyWorkspace": "Cannot delete the only workspace", "fetchWorkspacesFailed": "Failed to fetch workspaces", "leaveCurrentWorkspace": "Leave workspace", - "leaveCurrentWorkspacePrompt": "Are you sure you want to leave the current workspace?" + "leaveCurrentWorkspacePrompt": "Are you sure you want to leave the current workspace?", + "createNewWorkspace": "Create a new workspace", + "workspaceName": "Workspace name", + "leave": "Leave workspace", + "delete": "Delete workspace", + "rename": "Rename workspace", + "leaveWorkspace": "Leave: {{workspaceName}}", + "leaveWorkspacePrompt": "Are you sure you want to leave {{workspaceName}}?" }, "shareAction": { "buttonText": "Share", diff --git a/src/assets/icons/success_logo.svg b/src/assets/icons/success_logo.svg index bafe68de..4d1b4e34 100644 --- a/src/assets/icons/success_logo.svg +++ b/src/assets/icons/success_logo.svg @@ -1,5 +1,5 @@ + fill="var(--icon-success-thick)" /> \ No newline at end of file diff --git a/src/components/_shared/outline/OutlineDrawer.tsx b/src/components/_shared/outline/OutlineDrawer.tsx index ed51c192..05bcdfb2 100644 --- a/src/components/_shared/outline/OutlineDrawer.tsx +++ b/src/components/_shared/outline/OutlineDrawer.tsx @@ -76,35 +76,41 @@ export function OutlineDrawer({ backdropFilter: variant === UIVariant.Publish ? 'blur(4px)' : undefined, backgroundColor: variant === UIVariant.App ? 'var(--surface-container-layer-00)' : undefined, }} - className={'sticky top-0 z-10 flex h-[48px] min-h-[48px] transform-gpu items-center justify-between'} + className={ + 'sticky top-0 z-10 flex h-[48px] min-h-[48px] w-full transform-gpu items-center justify-between overflow-hidden' + } > {header ? ( header ) : (
{ navigate('/app'); }} > - +
)} - - {hovered && ( - - {t('sideBar.closeSidebar')} - {createHotKeyLabel(HOT_KEY_NAME.TOGGLE_SIDEBAR)} - - } + + {t('sideBar.closeSidebar')} + {createHotKeyLabel(HOT_KEY_NAME.TOGGLE_SIDEBAR)} + + } + > + - - - - - )} + + +
{children}
{variant === 'publish' && } diff --git a/src/components/app/workspaces/CreateWorkspace.tsx b/src/components/app/workspaces/CreateWorkspace.tsx index 512815af..8e799dc7 100644 --- a/src/components/app/workspaces/CreateWorkspace.tsx +++ b/src/components/app/workspaces/CreateWorkspace.tsx @@ -1,96 +1,109 @@ -import { ReactComponent as AddIcon } from '@/assets/icons/plus.svg'; -import { useAppHandlers } from '@/components/app/app.hooks'; -import { useService } from '@/components/main/app.hooks'; -import { NormalModal } from '@/components/_shared/modal'; -import { notify } from '@/components/_shared/notify'; -import { Button, OutlinedInput } from '@mui/material'; -import React, { useCallback, useEffect, useRef } from 'react'; +import { useCallback, useEffect, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; +import { toast } from 'sonner'; -function CreateWorkspace() { +import { useCurrentUser } from '@/components/main/app.hooks'; +import { Button } from '@/components/ui/button'; +import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog'; +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; +import { Progress } from '@/components/ui/progress'; + +function CreateWorkspace({ + open, + openOnChange, + defaultName, + onOk, + okText, + title, +}: { + defaultName?: string; + open?: boolean; + openOnChange?: (open: boolean) => void; + onOk?: (name: string) => Promise; + okText?: string; + title?: string; +}) { const { t } = useTranslation(); - const { onChangeWorkspace: handleSelectedWorkspace } = useAppHandlers(); - const service = useService(); - const [loading, setLoading] = React.useState(false); - const [name, setName] = React.useState(''); - const [open, setOpen] = React.useState(false); + const [loading, setLoading] = useState(false); + const currentUser = useCurrentUser(); + + const [name, setName] = useState(defaultName || `${currentUser?.name}'s Workspace`); const handleCreate = useCallback(async () => { - if (!service) return; - setLoading(true); try { - const workspaceId = await service.createWorkspace({ - workspace_name: name, - }); - - await handleSelectedWorkspace?.(workspaceId); - setOpen(false); + await onOk?.(name); + openOnChange?.(false); // eslint-disable-next-line } catch (e: any) { - notify.error(e.message); + toast.error(e.message); } finally { setLoading(false); } - }, [handleSelectedWorkspace, name, service]); + }, [name, openOnChange, onOk]); const inputRef = useRef(null); useEffect(() => { if (!open) { setName(''); } - }, [open]); + }, [open, openOnChange]); return ( <> - - { - setOpen(false); - }} - disableRestoreFocus={true} - disableAutoFocus={false} - okLoading={loading} - onOk={handleCreate} - okText={t('button.create')} - PaperProps={{ - className: 'w-96 max-w-[70vw]', - }} - classes={{ container: 'items-start max-md:mt-auto max-md:items-center mt-[10%] ' }} - > - { - if (!input) return; - if (!inputRef.current) { - setTimeout(() => { - input.setSelectionRange(0, input.value.length); - }, 100); - inputRef.current = input; - } - }} - onChange={(e) => setName(e.target.value)} - fullWidth - /> - + + + + + + + ); } diff --git a/src/components/app/workspaces/CurrentWorkspace.tsx b/src/components/app/workspaces/CurrentWorkspace.tsx index 61d85fc6..ccbba60f 100644 --- a/src/components/app/workspaces/CurrentWorkspace.tsx +++ b/src/components/app/workspaces/CurrentWorkspace.tsx @@ -1,7 +1,6 @@ import { UserWorkspaceInfo, Workspace } from '@/application/types'; import { ReactComponent as AppFlowyLogo } from '@/assets/icons/appflowy.svg'; -import { getAvatarProps } from '@/components/app/workspaces/utils'; -import { Avatar } from '@mui/material'; +import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'; function CurrentWorkspace({ userWorkspaceInfo, @@ -32,19 +31,18 @@ function CurrentWorkspace({ } return ( -
+
+ > + + {selectedWorkspace.name} + +
{selectedWorkspace.name}
); diff --git a/src/components/app/workspaces/DeleteWorkspace.tsx b/src/components/app/workspaces/DeleteWorkspace.tsx index 3757399e..a93f4416 100644 --- a/src/components/app/workspaces/DeleteWorkspace.tsx +++ b/src/components/app/workspaces/DeleteWorkspace.tsx @@ -1,20 +1,35 @@ -import React from 'react'; -import { Button } from '@mui/material'; +import { useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { ReactComponent as DeleteSvg } from '@/assets/icons/delete.svg'; -import { NormalModal } from '@/components/_shared/modal'; -import { notify } from '@/components/_shared/notify'; -import { useService } from '@/components/main/app.hooks'; +import { toast } from 'sonner'; -function DeleteWorkspace ({ workspaceId, name, onDeleted }: { +import { useCurrentWorkspaceId } from '@/components/app/app.hooks'; +import { useService } from '@/components/main/app.hooks'; +import { Button } from '@/components/ui/button'; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from '@/components/ui/dialog'; +import { Progress } from '@/components/ui/progress'; + +function DeleteWorkspace({ + workspaceId, + name, + open, + openOnChange, +}: { name: string; workspaceId: string; - onDeleted?: () => void; + open: boolean; + openOnChange: (open: boolean) => void; }) { const { t } = useTranslation(); - const [confirmOpen, setConfirmOpen] = React.useState(false); - const [loading, setLoading] = React.useState(false); + const [loading, setLoading] = useState(false); const service = useService(); + const currentWorkspaceId = useCurrentWorkspaceId(); const handleOk = async () => { if (!service) return; @@ -22,12 +37,13 @@ function DeleteWorkspace ({ workspaceId, name, onDeleted }: { try { setLoading(true); await service.deleteWorkspace(workspaceId); - setConfirmOpen(false); - onDeleted?.(); - + openOnChange(false); + if (currentWorkspaceId === workspaceId) { + window.location.href = `/app`; + } // eslint-disable-next-line } catch (e: any) { - notify.error(e.message); + toast.error(e.message); } finally { setLoading(false); } @@ -35,32 +51,33 @@ function DeleteWorkspace ({ workspaceId, name, onDeleted }: { return ( <> - - - {t('button.delete')}: {name} -
} - open={confirmOpen} - onClose={() => setConfirmOpen(false)} - > - {t('workspace.deleteWorkspaceHintText')} - + + { + if (e.key === 'Enter') { + void handleOk(); + } + }} + > + + + {t('button.delete')}: {name} + + + {t('workspace.deleteWorkspaceHintText')} + + + + + + ); } -export default DeleteWorkspace; \ No newline at end of file +export default DeleteWorkspace; diff --git a/src/components/app/workspaces/InviteMember.tsx b/src/components/app/workspaces/InviteMember.tsx index ac029285..f61206f6 100644 --- a/src/components/app/workspaces/InviteMember.tsx +++ b/src/components/app/workspaces/InviteMember.tsx @@ -1,26 +1,31 @@ -import React, { useCallback, useEffect, useMemo, useRef } from 'react'; +import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useSearchParams } from 'react-router-dom'; import { toast } from 'sonner'; import { SubscriptionPlan, Workspace, WorkspaceMember } from '@/application/types'; -import { ReactComponent as AddUserIcon } from '@/assets/icons/invite_user.svg'; import { ReactComponent as TipIcon } from '@/assets/icons/warning.svg'; import { useAppHandlers } from '@/components/app/app.hooks'; import { useCurrentUser, useService } from '@/components/main/app.hooks'; import { Button } from '@/components/ui/button'; -import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog'; -import { dropdownMenuItemVariants } from '@/components/ui/dropdown-menu'; +import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { Progress } from '@/components/ui/progress'; -function InviteMember({ workspace, onClick }: { workspace: Workspace; onClick?: () => void }) { +function InviteMember({ + workspace, + open, + openOnChange, +}: { + workspace: Workspace; + open?: boolean; + openOnChange?: (open: boolean) => void; +}) { const { getSubscriptions } = useAppHandlers(); const { t } = useTranslation(); - const [open, setOpen] = React.useState(false); - const [value, setValue] = React.useState(''); - const [loading, setLoading] = React.useState(false); + const [value, setValue] = useState(''); + const [loading, setLoading] = useState(false); const service = useService(); const currentWorkspaceId = workspace.id; const [, setSearch] = useSearchParams(); @@ -89,7 +94,7 @@ function InviteMember({ workspace, onClick }: { workspace: Workspace; onClick?: await service.inviteMembers(currentWorkspaceId, emails); - setOpen(false); + openOnChange?.(false); toast.success(t('inviteMember.inviteSuccess')); // eslint-disable-next-line } catch (e: any) { @@ -119,13 +124,7 @@ function InviteMember({ workspace, onClick }: { workspace: Workspace; onClick?: return ( <> - - -
- - {t('settings.appearance.members.inviteMembers')} -
-
+ {t('inviteMember.requestInviteMembers')} @@ -149,7 +148,6 @@ function InviteMember({ workspace, onClick }: { workspace: Workspace; onClick?: id='emails' name='emails' disabled={isExceed} - defaultValue={value} onChange={(e) => setValue(e.target.value)} value={value} placeholder={t('inviteMember.addEmail')} diff --git a/src/components/app/workspaces/LeaveWorkspace.tsx b/src/components/app/workspaces/LeaveWorkspace.tsx index 0566bfaf..63be03a9 100644 --- a/src/components/app/workspaces/LeaveWorkspace.tsx +++ b/src/components/app/workspaces/LeaveWorkspace.tsx @@ -1,61 +1,79 @@ -import React from 'react'; -import { Button } from '@mui/material'; +import { useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { ReactComponent as LeaveSvg } from '@/assets/icons/logout.svg'; -import { NormalModal } from '@/components/_shared/modal'; -import { notify } from '@/components/_shared/notify'; -import { useService } from '@/components/main/app.hooks'; -import { useCurrentWorkspaceId } from '@/components/app/app.hooks'; +import { toast } from 'sonner'; -function LeaveWorkspace({workspaceId}: { +import { useCurrentWorkspaceId } from '@/components/app/app.hooks'; +import { useService } from '@/components/main/app.hooks'; +import { Button } from '@/components/ui/button'; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from '@/components/ui/dialog'; + +function LeaveWorkspace({ + workspaceId, + open, + openOnChange, + workspaceName, +}: { workspaceId: string; + open: boolean; + openOnChange: (open: boolean) => void; + workspaceName: string; }) { - const {t} = useTranslation(); - const [confirmOpen, setConfirmOpen] = React.useState(false); - const [loading, setLoading] = React.useState(false); + const { t } = useTranslation(); + const [loading, setLoading] = useState(false); const service = useService(); const currentWorkspaceId = useCurrentWorkspaceId(); - const handleOk = async() => { + const handleOk = async () => { if (!service) return; - try { - setLoading(true); - await service.leaveWorkspace(workspaceId); - setConfirmOpen(false); - if (currentWorkspaceId === workspaceId) { - window.location.href = `/app` - } - // eslint-disable-next-line - } catch (e: any) { - notify.error(e.message); - } finally { - setLoading(false); - } - } + try { + setLoading(true); + await service.leaveWorkspace(workspaceId); + openOnChange(false); + if (currentWorkspaceId === workspaceId) { + window.location.href = `/app`; + } + // eslint-disable-next-line + } catch (e: any) { + toast.error(e.message); + } finally { + setLoading(false); + } + }; return ( <> - - - {t('workspace.leaveCurrentWorkspace')} - } - open={confirmOpen} - onClose={() => setConfirmOpen(false)} - > - {t('workspace.leaveCurrentWorkspacePrompt')} - + + { + if (e.key === 'Enter') { + void handleOk(); + } + }} + > + + {t('workspace.leaveWorkspace', { workspaceName })} + + {t('workspace.leaveWorkspacePrompt', { workspaceName })} + + + + + + ); } -export default LeaveWorkspace; \ No newline at end of file +export default LeaveWorkspace; diff --git a/src/components/app/workspaces/MoreActions.tsx b/src/components/app/workspaces/MoreActions.tsx index 4187f782..bf342f50 100644 --- a/src/components/app/workspaces/MoreActions.tsx +++ b/src/components/app/workspaces/MoreActions.tsx @@ -1,75 +1,90 @@ -import React from 'react'; -import { ReactComponent as MoreSvg } from '@/assets/icons/more.svg'; -import { IconButton } from '@mui/material'; -import { Workspace } from '@/application/types'; -import { Popover } from '@/components/_shared/popover'; -import LeaveWorkspace from '@/components/app/workspaces/LeaveWorkspace'; -import { useCurrentUser } from '@/components/main/app.hooks'; -import DeleteWorkspace from '@/components/app/workspaces/DeleteWorkspace'; -import RenameWorkspace from '@/components/app/workspaces/RenameWorkspace'; +import { useState } from 'react'; +import { useTranslation } from 'react-i18next'; -function MoreActions ({ workspace, onUpdated, onDeleted }: { +import { Workspace } from '@/application/types'; +import { ReactComponent as DeleteIcon } from '@/assets/icons/delete.svg'; +import { ReactComponent as EditIcon } from '@/assets/icons/edit.svg'; +import { ReactComponent as LeaveSvg } from '@/assets/icons/logout.svg'; +import { ReactComponent as MoreSvg } from '@/assets/icons/more.svg'; +import { useCurrentUser } from '@/components/main/app.hooks'; +import { Button } from '@/components/ui/button'; +import { dropdownMenuItemVariants } from '@/components/ui/dropdown-menu'; +import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'; + +function MoreActions({ + workspace, + onUpdate, + onDelete, + onLeave, +}: { workspace: Workspace; - onUpdated: (name: string) => void; - onDeleted?: () => void; + onUpdate?: () => void; + onDelete?: () => void; + onLeave?: () => void; }) { - const ref = React.useRef(null); - const [open, setOpen] = React.useState(false); + const { t } = useTranslation(); + const [open, setOpen] = useState(false); const currentUser = useCurrentUser(); const isOwner = workspace.owner?.uid.toString() === currentUser?.uid.toString(); return ( <> - { - e.stopPropagation(); - setOpen(true); - }} - ref={ref} - size={'small'} - className={'p-1'} - color={'inherit'} - > - - - { - e.stopPropagation(); - }} - open={open} - onClose={() => setOpen(false)} - anchorEl={ref.current} - > - {isOwner ? ( -
- { - onUpdated(name); + + + + + + {isOwner ? ( +
+
{ + e.stopPropagation(); + onUpdate?.(); + setOpen(false); + }} + > + + {t('workspace.rename')} +
+
{ + e.stopPropagation(); + onDelete?.(); + setOpen(false); + }} + > + + {t('workspace.delete')} +
+
+ ) : ( +
{ + e.stopPropagation(); + onLeave?.(); setOpen(false); }} - workspace={workspace} - /> - { - setOpen(false); - onDeleted?.(); - }} - /> - -
- ) : } + > + + {t('workspace.leave')} +
+ )} +
); } -export default MoreActions; \ No newline at end of file +export default MoreActions; diff --git a/src/components/app/workspaces/RenameWorkspace.tsx b/src/components/app/workspaces/RenameWorkspace.tsx deleted file mode 100644 index 029ec972..00000000 --- a/src/components/app/workspaces/RenameWorkspace.tsx +++ /dev/null @@ -1,101 +0,0 @@ -import { Workspace } from '@/application/types'; -import { NormalModal } from '@/components/_shared/modal'; -import { notify } from '@/components/_shared/notify'; -import { useService } from '@/components/main/app.hooks'; -import { Button, OutlinedInput } from '@mui/material'; -import React, { useCallback, useRef } from 'react'; -import { useTranslation } from 'react-i18next'; -import { ReactComponent as EditIcon } from '@/assets/icons/edit.svg'; - -function RenameWorkspace ({ workspace, onUpdated }: { - workspace: Workspace; - onUpdated: (name: string) => void; -}) { - const { t } = useTranslation(); - - const service = useService(); - const [loading, setLoading] = React.useState(false); - const [name, setName] = React.useState(workspace.name); - const [open, setOpen] = React.useState(false); - - const handleUpdate = useCallback(async () => { - if (!service) return; - - setLoading(true); - try { - await service.updateWorkspace(workspace.id, { - workspace_name: name, - }); - - setOpen(false); - onUpdated(name); - // eslint-disable-next-line - } catch (e: any) { - notify.error(e.message); - } finally { - setLoading(false); - } - }, [onUpdated, name, service, workspace.id]); - - const inputRef = useRef(null); - - return ( - <> - - setOpen(false)} - okLoading={loading} - onOk={handleUpdate} - okText={t('button.save')} - PaperProps={{ - className: 'w-96 max-w-[70vw]', - }} - classes={{ container: 'items-start max-md:mt-auto max-md:items-center mt-[10%] ' }} - > - { - if (!input) return; - if (!inputRef.current) { - setTimeout(() => { - input.setSelectionRange(0, input.value.length); - }, 100); - inputRef.current = input; - } - - }} - onChange={e => setName(e.target.value)} - fullWidth - onKeyDown={(e) => { - if (e.key === 'Enter') { - void handleUpdate(); - } - }} - /> - - - ); -} - -export default RenameWorkspace; \ No newline at end of file diff --git a/src/components/app/workspaces/WorkspaceItem.tsx b/src/components/app/workspaces/WorkspaceItem.tsx new file mode 100644 index 00000000..6aa7905b --- /dev/null +++ b/src/components/app/workspaces/WorkspaceItem.tsx @@ -0,0 +1,103 @@ +import { CircularProgress } from '@mui/material'; +import { useMemo, useState } from 'react'; +import { useTranslation } from 'react-i18next'; + +import { Workspace } from '@/application/types'; +import MoreActions from '@/components/app/workspaces/MoreActions'; +import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'; +import { DropdownMenuItem, DropdownMenuItemTick } from '@/components/ui/dropdown-menu'; +import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'; + +export function WorkspaceItem({ + workspace, + showActions = true, + onChange, + currentWorkspaceId, + changeLoading, + onUpdate, + onDelete, + onLeave, +}: { + showActions?: boolean; + workspace: Workspace; + onChange: (id: string) => void; + currentWorkspaceId?: string; + changeLoading?: string; + onUpdate?: (workspace: Workspace) => void; + onDelete?: (workspace: Workspace) => void; + onLeave?: (workspace: Workspace) => void; +}) { + const { t } = useTranslation(); + const [hovered, setHovered] = useState(true); + + const renderActions = useMemo(() => { + if (changeLoading === workspace.id) return ; + + if (!showActions) { + if (currentWorkspaceId === workspace.id) { + return ; + } + + return null; + } + + return ( +
+ {currentWorkspaceId === workspace.id && ( + + )} + +
+ onUpdate?.(workspace)} + onDelete={() => onDelete?.(workspace)} + onLeave={() => onLeave?.(workspace)} + /> +
+
+ ); + }, [changeLoading, currentWorkspaceId, hovered, onDelete, onLeave, onUpdate, showActions, workspace]); + + return ( + { + if (workspace.id === currentWorkspaceId) return; + void onChange(workspace.id); + }} + onMouseEnter={() => setHovered(true)} + onMouseLeave={() => setHovered(false)} + > + + + {workspace.name} + +
+ + +
{workspace.name}
+
+ +

{workspace.name}

+
+
+
+ {t('invitation.membersCount', { count: workspace.memberCount || 0 })} +
+
+ {renderActions} +
+ ); +} diff --git a/src/components/app/workspaces/WorkspaceList.tsx b/src/components/app/workspaces/WorkspaceList.tsx index 6903c664..143c47c8 100644 --- a/src/components/app/workspaces/WorkspaceList.tsx +++ b/src/components/app/workspaces/WorkspaceList.tsx @@ -1,12 +1,7 @@ -import { Avatar, Tooltip } from '@mui/material'; -import CircularProgress from '@mui/material/CircularProgress'; -import React, { useCallback, useEffect } from 'react'; -import { useTranslation } from 'react-i18next'; +import { useCallback, useEffect, useState } from 'react'; import { Workspace } from '@/application/types'; -import { ReactComponent as SelectedSvg } from '@/assets/icons/tick.svg'; -import MoreActions from '@/components/app/workspaces/MoreActions'; -import { getAvatarProps } from '@/components/app/workspaces/utils'; +import { WorkspaceItem } from '@/components/app/workspaces/WorkspaceItem'; import { useService } from '@/components/main/app.hooks'; function WorkspaceList({ @@ -15,19 +10,22 @@ function WorkspaceList({ onChange, changeLoading, showActions = true, - onUpdateCurrentWorkspace, + onUpdate, + onDelete, + onLeave, }: { currentWorkspaceId?: string; changeLoading?: string; onChange: (selectedId: string) => void; defaultWorkspaces?: Workspace[]; showActions?: boolean; - onUpdateCurrentWorkspace?: (name: string) => void; + + onUpdate?: (workspace: Workspace) => void; + onDelete?: (workspace: Workspace) => void; + onLeave?: (workspace: Workspace) => void; }) { const service = useService(); - const { t } = useTranslation(); - const [hoveredWorkspaceId, setHoveredWorkspaceId] = React.useState(null); - const [workspaces, setWorkspaces] = React.useState(defaultWorkspaces || []); + const [workspaces, setWorkspaces] = useState(defaultWorkspaces || []); const fetchWorkspaces = useCallback(async () => { if (!service) return; try { @@ -43,79 +41,21 @@ function WorkspaceList({ void fetchWorkspaces(); }, [fetchWorkspaces]); - const renderActions = useCallback( - (workspace: Workspace) => { - if (changeLoading === workspace.id) return ; - const hovered = hoveredWorkspaceId === workspace.id; - - if (workspace.id === currentWorkspaceId && !(hovered && showActions)) - return ; - - if (showActions) { - return ( -
- { - void fetchWorkspaces(); - if (workspace.id === currentWorkspaceId) { - onUpdateCurrentWorkspace?.(name); - } - }} - onDeleted={() => { - if (workspace.id === currentWorkspaceId) { - window.location.href = `/app`; - } else { - void fetchWorkspaces(); - } - }} - /> -
- ); - } - - return null; - }, - [changeLoading, currentWorkspaceId, fetchWorkspaces, hoveredWorkspaceId, onUpdateCurrentWorkspace, showActions] - ); - return ( <> {workspaces.map((workspace) => { return ( -
{ - if (workspace.id === currentWorkspaceId) return; - void onChange(workspace.id); - }} - onMouseEnter={() => setHoveredWorkspaceId(workspace.id)} - onMouseLeave={() => setHoveredWorkspaceId(null)} - > - -
- -
- {workspace.name} -
-
-
- {t('invitation.membersCount', { count: workspace.memberCount || 0 })} -
-
- {renderActions(workspace)} -
+ workspace={workspace} + onChange={onChange} + currentWorkspaceId={currentWorkspaceId} + changeLoading={changeLoading} + onUpdate={onUpdate} + onDelete={onDelete} + onLeave={onLeave} + showActions={showActions} + /> ); })} diff --git a/src/components/app/workspaces/Workspaces.tsx b/src/components/app/workspaces/Workspaces.tsx index 1e738e27..ab488754 100644 --- a/src/components/app/workspaces/Workspaces.tsx +++ b/src/components/app/workspaces/Workspaces.tsx @@ -1,28 +1,40 @@ -import { Button, Divider, IconButton, Tooltip } from '@mui/material'; -import React, { useCallback, useEffect } from 'react'; +import { IconButton, Tooltip } from '@mui/material'; +import { useCallback, useEffect, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useNavigate, useSearchParams } from 'react-router-dom'; import { invalidToken } from '@/application/session/token'; import { Workspace } from '@/application/types'; import { ReactComponent as UpgradeAIMaxIcon } from '@/assets/icons/ai.svg'; -import { ReactComponent as ArrowDown } from '@/assets/icons/alt_arrow_down.svg'; +import { ReactComponent as ChevronDownIcon } from '@/assets/icons/alt_arrow_down.svg'; import { ReactComponent as TipIcon } from '@/assets/icons/help.svg'; +import { ReactComponent as AddUserIcon } from '@/assets/icons/invite_user.svg'; import { ReactComponent as LogoutIcon } from '@/assets/icons/logout.svg'; +import { ReactComponent as AddIcon } from '@/assets/icons/plus.svg'; import { ReactComponent as ImportIcon } from '@/assets/icons/save_as.svg'; import { ReactComponent as UpgradeIcon } from '@/assets/icons/upgrade.svg'; import { useAppHandlers, useCurrentWorkspaceId, useUserWorkspaceInfo } from '@/components/app/app.hooks'; import CreateWorkspace from '@/components/app/workspaces/CreateWorkspace'; import CurrentWorkspace from '@/components/app/workspaces/CurrentWorkspace'; +import DeleteWorkspace from '@/components/app/workspaces/DeleteWorkspace'; import InviteMember from '@/components/app/workspaces/InviteMember'; +import LeaveWorkspace from '@/components/app/workspaces/LeaveWorkspace'; import WorkspaceList from '@/components/app/workspaces/WorkspaceList'; import UpgradeAIMax from '@/components/billing/UpgradeAIMax'; import UpgradePlan from '@/components/billing/UpgradePlan'; -import { useCurrentUser } from '@/components/main/app.hooks'; -import { dropdownMenuItemVariants } from '@/components/ui/dropdown-menu'; +import { useCurrentUser, useService } from '@/components/main/app.hooks'; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuGroup, + DropdownMenuItem, + dropdownMenuItemVariants, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from '@/components/ui/dropdown-menu'; import Import from '@/components/_shared/more-actions/importer/Import'; import { notify } from '@/components/_shared/notify'; -import { Popover } from '@/components/_shared/popover'; import { openUrl } from '@/utils/url'; export function Workspaces() { @@ -30,20 +42,25 @@ export function Workspaces() { const userWorkspaceInfo = useUserWorkspaceInfo(); const currentWorkspaceId = useCurrentWorkspaceId(); const currentUser = useCurrentUser(); - const [openUpgradePlan, setOpenUpgradePlan] = React.useState(false); - const [openUpgradeAIMax, setOpenUpgradeAIMax] = React.useState(false); - const [open, setOpen] = React.useState(false); - const [hoveredHeader, setHoveredHeader] = React.useState(false); - const ref = React.useRef(null); + const [openUpgradePlan, setOpenUpgradePlan] = useState(false); + const [openUpgradeAIMax, setOpenUpgradeAIMax] = useState(false); + const [open, setOpen] = useState(false); + const [hoveredHeader, setHoveredHeader] = useState(false); + const ref = useRef(null); const navigate = useNavigate(); - const [changeLoading, setChangeLoading] = React.useState(null); + const [changeLoading, setChangeLoading] = useState(null); const handleSignOut = useCallback(() => { invalidToken(); navigate('/login?redirectTo=' + encodeURIComponent(window.location.href)); }, [navigate]); const { onChangeWorkspace: handleSelectedWorkspace } = useAppHandlers(); - const [currentWorkspace, setCurrentWorkspace] = React.useState(undefined); + const [currentWorkspace, setCurrentWorkspace] = useState(undefined); + const [openInviteMember, setOpenInviteMember] = useState(false); + const [openCreateWorkspace, setOpenCreateWorkspace] = useState(false); + const [openRenameWorkspace, setOpenRenameWorkspace] = useState(null); + const [openDeleteWorkspace, setOpenDeleteWorkspace] = useState(null); + const [openLeaveWorkspace, setOpenLeaveWorkspace] = useState(null); const isOwner = currentWorkspace?.owner?.uid.toString() === currentUser?.uid.toString(); @@ -75,109 +92,159 @@ export function Workspaces() { }); }, [setSearchParams]); + const service = useService(); + const handleCreateWorkspace = useCallback( + async (name: string) => { + if (!service) return; + const workspaceId = await service.createWorkspace({ + workspace_name: name, + }); + + await handleSelectedWorkspace?.(workspaceId); + }, + [handleSelectedWorkspace, service] + ); + + const handleUpdateWorkspace = useCallback( + async (name: string) => { + if (!service || !openRenameWorkspace) return; + await service.updateWorkspace(openRenameWorkspace.id, { + workspace_name: name, + }); + if (openRenameWorkspace.id === currentWorkspaceId) { + setCurrentWorkspace((prev) => { + if (!prev) return prev; + return { ...prev, name }; + }); + } + + setOpenRenameWorkspace(null); + }, + [service, openRenameWorkspace, currentWorkspaceId] + ); + return ( <> - - setOpen(false)}> -
-
- {currentUser?.email} -
-
- {open && ( +
+ +
+
+ + + + + {currentUser?.email} + + { - setCurrentWorkspace((prev) => { - if (!prev) return prev; - return { - ...prev, - name, - }; - }); - }} + onUpdate={setOpenRenameWorkspace} + onDelete={setOpenDeleteWorkspace} + onLeave={setOpenLeaveWorkspace} /> - )} -
- - - {currentWorkspace && ( - { - setOpen(false); + + { + setOpenCreateWorkspace(true); }} - workspace={currentWorkspace} - /> - )} -
- -
{t('web.importNotion')}
- - { - e.stopPropagation(); - void openUrl('https://docs.appflowy.io/docs/guides/import-from-notion', '_blank'); - }} - size={'small'} - className={'mx-2'} + > +
- - - -
- -
- - {t('button.logout')} -
+ +
+ {t('workspace.create')} +
+ + + {currentWorkspace && ( + { + setOpenInviteMember(true); + }} + > + + {t('settings.appearance.members.inviteMembers')} + + )} + + +
{t('web.importNotion')}
+ + { + e.stopPropagation(); + void openUrl('https://docs.appflowy.io/docs/guides/import-from-notion', '_blank'); + }} + size={'small'} + className={'mx-2'} + > + + + +
+
+ + + + + + {t('button.logout')} + + + {isOwner && ( + + + { + setOpenUpgradePlan(true); + setOpen(false); + }} + > + + {t('subscribe.changePlan')} + + { + setOpenUpgradeAIMax(true); + setOpen(false); + }} + > + + {t('subscribe.getAIMax')} + + + )} + + + - {isOwner && ( - <> - -
{ - setOpenUpgradePlan(true); - setOpen(false); - }} - className={dropdownMenuItemVariants({ variant: 'default' })} - > - - {t('subscribe.changePlan')} -
-
{ - setOpenUpgradeAIMax(true); - setOpen(false); - }} - className={dropdownMenuItemVariants({ variant: 'default' })} - > - - {t('subscribe.getAIMax')} -
- - )} - -
{isOwner && ( <> + {openCreateWorkspace && ( + + )} + + {currentWorkspace && ( + + )} + + {openRenameWorkspace && ( + setOpenRenameWorkspace(null)} + onOk={handleUpdateWorkspace} + okText={t('workspace.rename')} + defaultName={openRenameWorkspace.name} + title={t('workspace.rename')} + /> + )} + + {openDeleteWorkspace && ( + setOpenDeleteWorkspace(null)} + /> + )} + + {openLeaveWorkspace && ( + setOpenLeaveWorkspace(null)} + /> + )} ); } diff --git a/src/components/database/components/tabs/DatabaseTabs.tsx b/src/components/database/components/tabs/DatabaseTabs.tsx index fcbf2d92..683e5cc0 100644 --- a/src/components/database/components/tabs/DatabaseTabs.tsx +++ b/src/components/database/components/tabs/DatabaseTabs.tsx @@ -390,7 +390,7 @@ export const DatabaseTabs = forwardRef( size={'icon'} variant={'ghost'} loading={addLoading} - className={'mx-1.5 mb-1.5 text-icon-tertiary'} + className={'mx-1.5 mb-1.5 text-icon-secondary'} > {addLoading ? : } diff --git a/src/components/ui/dropdown-menu.tsx b/src/components/ui/dropdown-menu.tsx index cf46ca1e..61f62e31 100644 --- a/src/components/ui/dropdown-menu.tsx +++ b/src/components/ui/dropdown-menu.tsx @@ -67,7 +67,7 @@ const DropdownMenuGroup = ({ ...props }: React.ComponentProps