mirror of
https://github.com/AppFlowy-IO/AppFlowy-Web.git
synced 2026-03-13 10:00:26 +08:00
chore: replace workspace manager to new token and components
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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 |
@@ -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} />}
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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')}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
103
src/components/app/workspaces/WorkspaceItem.tsx
Normal file
103
src/components/app/workspaces/WorkspaceItem.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
|
||||
@@ -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)}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user