chore: replace workspace manager to new token and components

This commit is contained in:
Kilu
2025-06-24 18:54:34 +08:00
parent 9c1f41dda4
commit 38eb323644
15 changed files with 673 additions and 550 deletions

View File

@@ -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",

View File

@@ -1,5 +1,5 @@
<svg width="64" height="64" viewBox="0 0 64 64" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M7 32H5.5H7ZM57 32H55.5H57ZM32 7.00003V5.50003V7.00003ZM24.3107 32.1894C23.7249 31.6036 22.7751 31.6036 22.1893 32.1894C21.6036 32.7752 21.6036 33.7249 22.1893 34.3107L24.3107 32.1894ZM28.25 38.25L27.1893 39.3107C27.7751 39.8965 28.7249 39.8965 29.3107 39.3107L28.25 38.25ZM41.8107 26.8107C42.3964 26.2249 42.3964 25.2752 41.8107 24.6894C41.2249 24.1036 40.2751 24.1036 39.6893 24.6894L41.8107 26.8107ZM7 32H5.5C5.5 39.0283 8.29196 45.7686 13.2617 50.7384L14.3223 49.6777L15.383 48.617C10.9759 44.2099 8.5 38.2326 8.5 32H7ZM14.3223 49.6777L13.2617 50.7384C18.2314 55.7081 24.9718 58.5 32 58.5V57V55.5C25.7674 55.5 19.7901 53.0241 15.383 48.617L14.3223 49.6777ZM32 57V58.5C39.0282 58.5 45.7686 55.7081 50.7383 50.7384L49.6777 49.6777L48.617 48.617C44.2099 53.0241 38.2326 55.5 32 55.5V57ZM49.6777 49.6777L50.7383 50.7384C55.708 45.7686 58.5 39.0283 58.5 32H57H55.5C55.5 38.2326 53.0241 44.2099 48.617 48.617L49.6777 49.6777ZM57 32H58.5C58.5 24.9718 55.708 18.2314 50.7383 13.2617L49.6777 14.3224L48.617 15.383C53.0241 19.7901 55.5 25.7674 55.5 32H57ZM49.6777 14.3224L50.7383 13.2617C45.7686 8.29199 39.0282 5.50003 32 5.50003V7.00003V8.50003C38.2326 8.50003 44.2099 10.9759 48.617 15.383L49.6777 14.3224ZM32 7.00003V5.50003C24.9718 5.50003 18.2314 8.29199 13.2617 13.2617L14.3223 14.3224L15.383 15.383C19.7901 10.9759 25.7674 8.50003 32 8.50003V7.00003ZM14.3223 14.3224L13.2617 13.2617C8.29196 18.2314 5.5 24.9718 5.5 32H7H8.5C8.5 25.7674 10.9759 19.7901 15.383 15.383L14.3223 14.3224ZM23.25 33.25L22.1893 34.3107L27.1893 39.3107L28.25 38.25L29.3107 37.1894L24.3107 32.1894L23.25 33.25ZM28.25 38.25L29.3107 39.3107L41.8107 26.8107L40.75 25.75L39.6893 24.6894L27.1893 37.1894L28.25 38.25Z"
fill="var(--badge-color-9-thick-2)" />
fill="var(--icon-success-thick)" />
</svg>

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@@ -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
) : (
<div
className={'flex cursor-pointer items-center gap-1 p-4 text-text-primary'}
className={'mx-1 flex h-full w-[141px] cursor-pointer items-center gap-1 p-2 text-text-primary'}
onClick={() => {
navigate('/app');
}}
>
<AppFlowyLogo className={'w-[88px]'} />
<AppFlowyLogo className='h-full w-full' />
</div>
)}
{hovered && (
<Tooltip
title={
<div className={'flex flex-col'}>
<span>{t('sideBar.closeSidebar')}</span>
<span className={'text-xs text-text-secondary'}>{createHotKeyLabel(HOT_KEY_NAME.TOGGLE_SIDEBAR)}</span>
</div>
}
<Tooltip
title={
<div className={'flex flex-col'}>
<span>{t('sideBar.closeSidebar')}</span>
<span className={'text-xs text-text-secondary'}>{createHotKeyLabel(HOT_KEY_NAME.TOGGLE_SIDEBAR)}</span>
</div>
}
>
<IconButton
style={{
opacity: hovered ? 1 : 0,
}}
onClick={onClose}
className={'m-4'}
size={'small'}
>
<IconButton onClick={onClose} className={'m-4'} size={'small'}>
<DoubleArrowLeft className={'text-text-secondary'} />
</IconButton>
</Tooltip>
)}
<DoubleArrowLeft className={'text-text-secondary'} />
</IconButton>
</Tooltip>
</div>
<div className={'flex h-fit flex-1 flex-col'}>{children}</div>
{variant === 'publish' && <AppFlowyPower width={width} />}

View File

@@ -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<void>;
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<HTMLInputElement | null>(null);
useEffect(() => {
if (!open) {
setName('');
}
}, [open]);
}, [open, openOnChange]);
return (
<>
<Button
color={'inherit'}
className={'w-full justify-start gap-0.5 px-3'}
onClick={() => setOpen(true)}
startIcon={
<div
className={'flex h-[33px] w-[33px] items-center justify-center rounded-[8px] border border-border-primary'}
>
<AddIcon className={'h-5 w-5'} />
<Dialog open={open} onOpenChange={openOnChange}>
<DialogContent>
<DialogHeader>
<DialogTitle>{title || t('workspace.createNewWorkspace')}</DialogTitle>
</DialogHeader>
<div className='grid gap-4'>
<div className='grid gap-3'>
<Label htmlFor='name'>{t('workspace.workspaceName')}</Label>
<Input
id='name'
name='name'
autoFocus
value={name}
ref={(input: HTMLInputElement) => {
if (!input) return;
if (!inputRef.current) {
setTimeout(() => {
input.focus();
input.setSelectionRange(0, input.value.length);
}, 100);
inputRef.current = input;
}
}}
onChange={(e) => setName(e.target.value)}
onKeyDown={(e) => {
if (e.key === 'Enter') {
e.stopPropagation();
void handleCreate();
}
}}
/>
</div>
</div>
}
>
{t('workspace.create')}
</Button>
<NormalModal
title={t('workspace.create')}
open={open}
onClose={() => {
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%] ' }}
>
<OutlinedInput
autoFocus
size={'small'}
placeholder={'Enter workspace name'}
value={name}
inputRef={(input: HTMLInputElement) => {
if (!input) return;
if (!inputRef.current) {
setTimeout(() => {
input.setSelectionRange(0, input.value.length);
}, 100);
inputRef.current = input;
}
}}
onChange={(e) => setName(e.target.value)}
fullWidth
/>
</NormalModal>
<DialogFooter>
<Button variant={'outline'} onClick={() => openOnChange?.(false)}>
{t('button.cancel')}
</Button>
<Button
disabled={!name.trim()}
loading={loading}
onClick={() => {
void handleCreate();
openOnChange?.(false);
}}
>
{loading ? <Progress /> : null}
{okText || t('workspace.create')}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</>
);
}

View File

@@ -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 (
<div className={'flex items-center gap-1.5'}>
<div className={'flex w-full items-center gap-1.5 overflow-hidden'}>
<Avatar
variant={'rounded'}
className={`flex items-center justify-center rounded-[8px] border-none border-border-primary p-1 ${
selectedWorkspace.icon ? 'bg-transparent' : ''
}`}
{...getAvatarProps(selectedWorkspace)}
shape={'square'}
style={{
width: avatarSize,
height: avatarSize,
fontSize: avatarSize / 1.2,
minWidth: avatarSize,
fontSize: avatarSize / 1.5,
}}
/>
>
<AvatarImage src={selectedWorkspace.icon} alt={''} />
<AvatarFallback>{selectedWorkspace.name}</AvatarFallback>
</Avatar>
<div className={'flex-1 truncate font-medium text-text-primary'}>{selectedWorkspace.name}</div>
</div>
);

View File

@@ -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 (
<>
<Button
onClick={() => {
setConfirmOpen(true);
}}
className={'w-full justify-start hover:text-function-error'}
size={'small'}
color={'inherit'}
startIcon={<DeleteSvg />}
>
{t('button.delete')}
</Button>
<NormalModal
okLoading={loading}
onOk={handleOk}
danger={true}
okText={t('button.delete')}
title={<div className={'flex items-center font-medium'}>
{t('button.delete')}: {name}
</div>}
open={confirmOpen}
onClose={() => setConfirmOpen(false)}
>
{t('workspace.deleteWorkspaceHintText')}
</NormalModal>
<Dialog open={open} onOpenChange={openOnChange}>
<DialogContent
onKeyDown={(e) => {
if (e.key === 'Enter') {
void handleOk();
}
}}
>
<DialogHeader>
<DialogTitle>
{t('button.delete')}: {name}
</DialogTitle>
</DialogHeader>
<DialogDescription>{t('workspace.deleteWorkspaceHintText')}</DialogDescription>
<DialogFooter>
<Button variant='outline' onClick={() => openOnChange(false)}>
{t('button.cancel')}
</Button>
<Button variant='destructive' loading={loading} onClick={handleOk}>
{loading && <Progress />}
{t('button.delete')}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</>
);
}
export default DeleteWorkspace;
export default DeleteWorkspace;

View File

@@ -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 (
<>
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
<div className={dropdownMenuItemVariants({ variant: 'default' })} onClick={onClick}>
<AddUserIcon />
{t('settings.appearance.members.inviteMembers')}
</div>
</DialogTrigger>
<Dialog open={open} onOpenChange={openOnChange}>
<DialogContent className={'w-[500px]'}>
<DialogHeader>
<DialogTitle>{t('inviteMember.requestInviteMembers')}</DialogTitle>
@@ -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')}

View File

@@ -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 (
<>
<Button onClick={() => {
setConfirmOpen(true);
}} size={'small'} className={'w-full justify-start hover:text-function-error'} color={'inherit'} startIcon={<LeaveSvg />}>
{t('workspace.leaveCurrentWorkspace')}
</Button>
<NormalModal
okLoading={loading}
onOk={handleOk}
danger={true}
okText={t('button.yes')}
title={<div className={'flex items-center font-medium'}>
{t('workspace.leaveCurrentWorkspace')}
</div>}
open={confirmOpen}
onClose={() => setConfirmOpen(false)}
>
{t('workspace.leaveCurrentWorkspacePrompt')}
</NormalModal>
<Dialog open={open} onOpenChange={openOnChange}>
<DialogContent
onKeyDown={(e) => {
if (e.key === 'Enter') {
void handleOk();
}
}}
>
<DialogHeader>
<DialogTitle>{t('workspace.leaveWorkspace', { workspaceName })}</DialogTitle>
</DialogHeader>
<DialogDescription>{t('workspace.leaveWorkspacePrompt', { workspaceName })}</DialogDescription>
<DialogFooter>
<Button variant='outline' onClick={() => openOnChange(false)}>
{t('button.cancel')}
</Button>
<Button variant='destructive' loading={loading} onClick={handleOk}>
{t('button.yes')}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</>
);
}
export default LeaveWorkspace;
export default LeaveWorkspace;

View File

@@ -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<HTMLButtonElement | null>(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 (
<>
<IconButton
onClick={e => {
e.stopPropagation();
setOpen(true);
}}
ref={ref}
size={'small'}
className={'p-1'}
color={'inherit'}
>
<MoreSvg className={'w-4 h-4'} />
</IconButton>
<Popover
slotProps={{
paper: {
className: 'p-2 w-[260px]',
},
}}
onClick={e => {
e.stopPropagation();
}}
open={open}
onClose={() => setOpen(false)}
anchorEl={ref.current}
>
{isOwner ? (
<div className={'flex flex-col gap-2'}>
<RenameWorkspace
onUpdated={(name: string) => {
onUpdated(name);
<Popover modal open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<Button
onClick={(e) => {
e.stopPropagation();
}}
size={'icon-sm'}
variant={'ghost'}
className={'text-icon-primary'}
>
<MoreSvg className={'h-5 w-5'} />
</Button>
</PopoverTrigger>
<PopoverContent align='start' className='min-w-fit p-2'>
{isOwner ? (
<div className={'flex flex-col'}>
<div
className={dropdownMenuItemVariants({ variant: 'default', className: 'w-full overflow-hidden' })}
onClick={(e) => {
e.stopPropagation();
onUpdate?.();
setOpen(false);
}}
>
<EditIcon />
{t('workspace.rename')}
</div>
<div
className={dropdownMenuItemVariants({ variant: 'destructive', className: 'w-full overflow-hidden' })}
onClick={(e) => {
e.stopPropagation();
onDelete?.();
setOpen(false);
}}
>
<DeleteIcon />
{t('workspace.delete')}
</div>
</div>
) : (
<div
className={dropdownMenuItemVariants({ variant: 'destructive', className: 'w-full overflow-hidden' })}
onClick={(e) => {
e.stopPropagation();
onLeave?.();
setOpen(false);
}}
workspace={workspace}
/>
<DeleteWorkspace
name={workspace.name}
workspaceId={workspace.id}
onDeleted={() => {
setOpen(false);
onDeleted?.();
}}
/>
</div>
) : <LeaveWorkspace
workspaceId={workspace.id}
/>}
>
<LeaveSvg />
{t('workspace.leave')}
</div>
)}
</PopoverContent>
</Popover>
</>
);
}
export default MoreActions;
export default MoreActions;

View File

@@ -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<HTMLInputElement | null>(null);
return (
<>
<Button
color={'inherit'}
size={'small'}
className={'w-full justify-start'}
onClick={(e) => {
e.stopPropagation();
e.preventDefault();
setOpen(true);
}}
startIcon={
<EditIcon />
}
>
{t('button.rename')}
</Button>
<NormalModal
disableRestoreFocus={true}
disableAutoFocus={false}
title={t('workspace.renameWorkspace')}
open={open}
onClose={() => 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%] ' }}
>
<OutlinedInput
autoFocus
size={'small'}
placeholder={'Enter workspace name'}
value={name}
inputRef={(input: HTMLInputElement) => {
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();
}
}}
/>
</NormalModal>
</>
);
}
export default RenameWorkspace;

View File

@@ -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 <CircularProgress size={16} />;
if (!showActions) {
if (currentWorkspaceId === workspace.id) {
return <DropdownMenuItemTick />;
}
return null;
}
return (
<div className='relative ml-auto flex h-7 w-7 items-center justify-center'>
{currentWorkspaceId === workspace.id && (
<DropdownMenuItemTick
style={{
opacity: hovered ? 0 : 1,
}}
className='absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2'
/>
)}
<div
className='absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2'
style={{
opacity: hovered ? 1 : 0,
}}
>
<MoreActions
workspace={workspace}
onUpdate={() => onUpdate?.(workspace)}
onDelete={() => onDelete?.(workspace)}
onLeave={() => onLeave?.(workspace)}
/>
</div>
</div>
);
}, [changeLoading, currentWorkspaceId, hovered, onDelete, onLeave, onUpdate, showActions, workspace]);
return (
<DropdownMenuItem
key={workspace.id}
className={'relative'}
onSelect={async () => {
if (workspace.id === currentWorkspaceId) return;
void onChange(workspace.id);
}}
onMouseEnter={() => setHovered(true)}
onMouseLeave={() => setHovered(false)}
>
<Avatar shape={'square'} className='h-[32px] min-w-[32px]'>
<AvatarImage src={workspace.icon} alt={''} />
<AvatarFallback>{workspace.name}</AvatarFallback>
</Avatar>
<div className={'flex flex-1 flex-col items-start overflow-hidden'}>
<Tooltip>
<TooltipTrigger asChild>
<div className={'w-full overflow-hidden truncate text-left text-sm text-text-primary'}>{workspace.name}</div>
</TooltipTrigger>
<TooltipContent>
<p>{workspace.name}</p>
</TooltipContent>
</Tooltip>
<div className={'text-xs leading-[18px] text-text-secondary'}>
{t('invitation.membersCount', { count: workspace.memberCount || 0 })}
</div>
</div>
{renderActions}
</DropdownMenuItem>
);
}

View File

@@ -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<string | null>(null);
const [workspaces, setWorkspaces] = React.useState<Workspace[]>(defaultWorkspaces || []);
const [workspaces, setWorkspaces] = useState<Workspace[]>(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 <CircularProgress size={16} />;
const hovered = hoveredWorkspaceId === workspace.id;
if (workspace.id === currentWorkspaceId && !(hovered && showActions))
return <SelectedSvg className={'h-5 w-5 text-text-action'} />;
if (showActions) {
return (
<div
style={{
visibility: hovered ? 'visible' : 'hidden',
}}
>
<MoreActions
workspace={workspace}
onUpdated={(name: string) => {
void fetchWorkspaces();
if (workspace.id === currentWorkspaceId) {
onUpdateCurrentWorkspace?.(name);
}
}}
onDeleted={() => {
if (workspace.id === currentWorkspaceId) {
window.location.href = `/app`;
} else {
void fetchWorkspaces();
}
}}
/>
</div>
);
}
return null;
},
[changeLoading, currentWorkspaceId, fetchWorkspaces, hoveredWorkspaceId, onUpdateCurrentWorkspace, showActions]
);
return (
<>
{workspaces.map((workspace) => {
return (
<div
<WorkspaceItem
key={workspace.id}
className={
'relative flex cursor-pointer items-center justify-between gap-[10px] rounded-[8px] p-2 text-[1em] hover:bg-fill-content-hover'
}
onClick={async () => {
if (workspace.id === currentWorkspaceId) return;
void onChange(workspace.id);
}}
onMouseEnter={() => setHoveredWorkspaceId(workspace.id)}
onMouseLeave={() => setHoveredWorkspaceId(null)}
>
<Avatar
variant={'rounded'}
className={'h-[2em] w-[2em] rounded-[8px] border border-border-primary text-[1.2em]'}
{...getAvatarProps(workspace)}
/>
<div className={'flex flex-1 flex-col items-start overflow-hidden'}>
<Tooltip title={workspace.name} enterDelay={1000} enterNextDelay={1000}>
<div className={'w-full overflow-hidden truncate text-left font-medium text-text-primary'}>
{workspace.name}
</div>
</Tooltip>
<div className={'text-[0.85em] text-text-secondary'}>
{t('invitation.membersCount', { count: workspace.memberCount || 0 })}
</div>
</div>
{renderActions(workspace)}
</div>
workspace={workspace}
onChange={onChange}
currentWorkspaceId={currentWorkspaceId}
changeLoading={changeLoading}
onUpdate={onUpdate}
onDelete={onDelete}
onLeave={onLeave}
showActions={showActions}
/>
);
})}
</>

View File

@@ -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<boolean>(false);
const ref = React.useRef<HTMLButtonElement | null>(null);
const [openUpgradePlan, setOpenUpgradePlan] = useState(false);
const [openUpgradeAIMax, setOpenUpgradeAIMax] = useState(false);
const [open, setOpen] = useState(false);
const [hoveredHeader, setHoveredHeader] = useState<boolean>(false);
const ref = useRef<HTMLDivElement | null>(null);
const navigate = useNavigate();
const [changeLoading, setChangeLoading] = React.useState<string | null>(null);
const [changeLoading, setChangeLoading] = useState<string | null>(null);
const handleSignOut = useCallback(() => {
invalidToken();
navigate('/login?redirectTo=' + encodeURIComponent(window.location.href));
}, [navigate]);
const { onChangeWorkspace: handleSelectedWorkspace } = useAppHandlers();
const [currentWorkspace, setCurrentWorkspace] = React.useState<Workspace | undefined>(undefined);
const [currentWorkspace, setCurrentWorkspace] = useState<Workspace | undefined>(undefined);
const [openInviteMember, setOpenInviteMember] = useState(false);
const [openCreateWorkspace, setOpenCreateWorkspace] = useState(false);
const [openRenameWorkspace, setOpenRenameWorkspace] = useState<Workspace | null>(null);
const [openDeleteWorkspace, setOpenDeleteWorkspace] = useState<Workspace | null>(null);
const [openLeaveWorkspace, setOpenLeaveWorkspace] = useState<Workspace | null>(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 (
<>
<Button
ref={ref}
onMouseLeave={() => setHoveredHeader(false)}
onMouseEnter={() => setHoveredHeader(true)}
onClick={() => setOpen(true)}
className={'mx-2 flex w-full cursor-pointer items-center justify-start gap-1 px-1 py-1 text-text-primary'}
>
<div className={'flex items-center gap-1.5 overflow-hidden text-[15px] text-text-primary'}>
<CurrentWorkspace
userWorkspaceInfo={userWorkspaceInfo}
selectedWorkspace={currentWorkspace}
onChangeWorkspace={handleChange}
avatarSize={24}
/>
<div className='mx-1 flex-1 overflow-hidden'>
<DropdownMenu modal={false} open={open} onOpenChange={setOpen}>
<DropdownMenuTrigger asChild>
<div
ref={ref}
onMouseLeave={() => setHoveredHeader(false)}
onMouseEnter={() => setHoveredHeader(true)}
className={dropdownMenuItemVariants({ variant: 'default', className: 'w-full overflow-hidden' })}
>
<CurrentWorkspace
userWorkspaceInfo={userWorkspaceInfo}
selectedWorkspace={currentWorkspace}
onChangeWorkspace={handleChange}
avatarSize={24}
/>
{hoveredHeader && <ArrowDown className={'h-5 w-5'} />}
</div>
</Button>
<Popover open={open} keepMounted={true} anchorEl={ref.current} onClose={() => setOpen(false)}>
<div className={'flex min-h-[303px] w-[288px] flex-col gap-2 overflow-hidden p-2 text-[14px]'}>
<div className={'flex items-center justify-between p-2 text-text-secondary'}>
<span className={'flex-1 text-sm font-medium'}>{currentUser?.email}</span>
</div>
<div className={'appflowy-scroller flex max-h-[236px] flex-1 flex-col gap-1 overflow-y-auto'}>
{open && (
<div
className='ml-auto transition-opacity duration-300'
style={{
opacity: hoveredHeader ? 1 : 0,
}}
>
<ChevronDownIcon className='h-5 w-5' />
</div>
</div>
</DropdownMenuTrigger>
<DropdownMenuContent className='min-w-[300px] max-w-[300px] overflow-hidden'>
<DropdownMenuLabel className='w-full overflow-hidden'>
<span className='truncate'>{currentUser?.email}</span>
</DropdownMenuLabel>
<DropdownMenuGroup className={'appflowy-scroller max-h-[236px] flex-1 overflow-x-hidden overflow-y-auto'}>
<WorkspaceList
defaultWorkspaces={userWorkspaceInfo?.workspaces}
currentWorkspaceId={currentWorkspaceId}
onChange={handleChange}
changeLoading={changeLoading || undefined}
onUpdateCurrentWorkspace={(name) => {
setCurrentWorkspace((prev) => {
if (!prev) return prev;
return {
...prev,
name,
};
});
}}
onUpdate={setOpenRenameWorkspace}
onDelete={setOpenDeleteWorkspace}
onLeave={setOpenLeaveWorkspace}
/>
)}
</div>
<CreateWorkspace />
<Divider className={'mt-1 w-full'} />
{currentWorkspace && (
<InviteMember
onClick={() => {
setOpen(false);
</DropdownMenuGroup>
<DropdownMenuItem
onSelect={() => {
setOpenCreateWorkspace(true);
}}
workspace={currentWorkspace}
/>
)}
<div className={dropdownMenuItemVariants({ variant: 'default' })} onClick={handleOpenImport}>
<ImportIcon />
<div className={'flex-1 text-left'}>{t('web.importNotion')}</div>
<Tooltip title={t('workspace.learnMore')} enterDelay={1000} enterNextDelay={1000}>
<IconButton
onClick={(e) => {
e.stopPropagation();
void openUrl('https://docs.appflowy.io/docs/guides/import-from-notion', '_blank');
}}
size={'small'}
className={'mx-2'}
>
<div
className={
'flex h-[32px] w-[32px] items-center justify-center rounded-[8px] border border-border-primary'
}
>
<TipIcon />
</IconButton>
</Tooltip>
</div>
<Divider className={'w-full'} />
<div className={dropdownMenuItemVariants({ variant: 'default' })} onClick={handleSignOut}>
<LogoutIcon />
{t('button.logout')}
</div>
<AddIcon className={'h-5 w-5'} />
</div>
{t('workspace.create')}
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuGroup>
{currentWorkspace && (
<DropdownMenuItem
onSelect={() => {
setOpenInviteMember(true);
}}
>
<AddUserIcon />
{t('settings.appearance.members.inviteMembers')}
</DropdownMenuItem>
)}
<DropdownMenuItem onSelect={handleOpenImport}>
<ImportIcon />
<div className={'flex-1 text-left'}>{t('web.importNotion')}</div>
<Tooltip title={t('workspace.learnMore')} enterDelay={1000} enterNextDelay={1000}>
<IconButton
onClick={(e) => {
e.stopPropagation();
void openUrl('https://docs.appflowy.io/docs/guides/import-from-notion', '_blank');
}}
size={'small'}
className={'mx-2'}
>
<TipIcon />
</IconButton>
</Tooltip>
</DropdownMenuItem>
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuItem onSelect={handleSignOut}>
<LogoutIcon />
{t('button.logout')}
</DropdownMenuItem>
</DropdownMenuGroup>
{isOwner && (
<DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuItem
onSelect={() => {
setOpenUpgradePlan(true);
setOpen(false);
}}
>
<UpgradeIcon />
{t('subscribe.changePlan')}
</DropdownMenuItem>
<DropdownMenuItem
onSelect={() => {
setOpenUpgradeAIMax(true);
setOpen(false);
}}
>
<UpgradeAIMaxIcon />
{t('subscribe.getAIMax')}
</DropdownMenuItem>
</DropdownMenuGroup>
)}
</DropdownMenuContent>
</DropdownMenu>
</div>
{isOwner && (
<>
<Divider className={'w-full'} />
<div
onClick={() => {
setOpenUpgradePlan(true);
setOpen(false);
}}
className={dropdownMenuItemVariants({ variant: 'default' })}
>
<UpgradeIcon />
{t('subscribe.changePlan')}
</div>
<div
onClick={() => {
setOpenUpgradeAIMax(true);
setOpen(false);
}}
className={dropdownMenuItemVariants({ variant: 'default' })}
>
<UpgradeAIMaxIcon />
{t('subscribe.getAIMax')}
</div>
</>
)}
</div>
</Popover>
{isOwner && (
<>
<UpgradePlan
@@ -198,6 +265,48 @@ export function Workspaces() {
)}
<Import />
{openCreateWorkspace && (
<CreateWorkspace
onOk={handleCreateWorkspace}
okText={t('workspace.create')}
defaultName={`${currentUser?.name}'s Workspace`}
open={openCreateWorkspace}
openOnChange={setOpenCreateWorkspace}
/>
)}
{currentWorkspace && (
<InviteMember open={openInviteMember} openOnChange={setOpenInviteMember} workspace={currentWorkspace} />
)}
{openRenameWorkspace && (
<CreateWorkspace
open={Boolean(openRenameWorkspace)}
openOnChange={() => setOpenRenameWorkspace(null)}
onOk={handleUpdateWorkspace}
okText={t('workspace.rename')}
defaultName={openRenameWorkspace.name}
title={t('workspace.rename')}
/>
)}
{openDeleteWorkspace && (
<DeleteWorkspace
workspaceId={openDeleteWorkspace.id}
name={openDeleteWorkspace.name}
open={Boolean(openDeleteWorkspace)}
openOnChange={() => setOpenDeleteWorkspace(null)}
/>
)}
{openLeaveWorkspace && (
<LeaveWorkspace
workspaceName={openLeaveWorkspace.name}
workspaceId={openLeaveWorkspace.id}
open={Boolean(openLeaveWorkspace)}
openOnChange={() => setOpenLeaveWorkspace(null)}
/>
)}
</>
);
}

View File

@@ -390,7 +390,7 @@ export const DatabaseTabs = forwardRef<HTMLDivElement, DatabaseTabBarProps>(
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 ? <Progress variant={'inherit'} /> : <PlusIcon className={'h-5 w-5'} />}
</Button>

View File

@@ -67,7 +67,7 @@ const DropdownMenuGroup = ({ ...props }: React.ComponentProps<typeof DropdownMen
const dropdownMenuItemVariants = cva(
cn(
'focus:bg-fill-content-hover hover:bg-fill-content-hover focus-visible:outline-none',
'relative flex cursor-pointer items-center gap-[10px] rounded-300 px-2 py-1 min-h-[32px]',
'relative flex cursor-pointer items-center gap-[10px] rounded-300 px-2 py-1.5 min-h-[32px]',
'text-sm text-text-primary outline-hidden select-none',
// Disabled state