mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-14 21:27:20 +00:00
chore: move client folders (#948)
This commit is contained in:
@@ -0,0 +1,235 @@
|
||||
import { EmailIcon } from '@blocksuite/icons';
|
||||
import { styled } from '@affine/component';
|
||||
import { Modal, ModalWrapper, ModalCloseButton } from '@affine/component';
|
||||
import { Button } from '@affine/component';
|
||||
import { Input } from '@affine/component';
|
||||
import { useState } from 'react';
|
||||
import { MuiAvatar } from '@affine/component';
|
||||
import useMembers from '@/hooks/use-members';
|
||||
import { User } from '@affine/datacenter';
|
||||
import { useTranslation } from '@affine/i18n';
|
||||
interface LoginModalProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
workspaceId: string;
|
||||
onInviteSuccess: () => void;
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export const debounce = <T extends (...args: any) => any>(
|
||||
fn: T,
|
||||
time?: number,
|
||||
immediate?: boolean
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
): ((...args: any) => any) => {
|
||||
let timeoutId: null | number;
|
||||
let defaultImmediate = immediate || false;
|
||||
const delay = time || 300;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
return (...args: any) => {
|
||||
if (defaultImmediate) {
|
||||
fn.apply(this, args);
|
||||
defaultImmediate = false;
|
||||
return;
|
||||
}
|
||||
if (timeoutId) {
|
||||
clearTimeout(timeoutId);
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
timeoutId = setTimeout(() => {
|
||||
fn.apply(this, args);
|
||||
timeoutId = null;
|
||||
}, delay);
|
||||
};
|
||||
};
|
||||
|
||||
const gmailReg = /^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@gmail\.com$/;
|
||||
export const InviteMemberModal = ({
|
||||
open,
|
||||
onClose,
|
||||
onInviteSuccess,
|
||||
}: LoginModalProps) => {
|
||||
const [email, setEmail] = useState<string>('');
|
||||
const [showMember, setShowMember] = useState<boolean>(false);
|
||||
const [showTip, setShowTip] = useState<boolean>(false);
|
||||
const [userData, setUserData] = useState<User | null>(null);
|
||||
const { inviteMember, getUserByEmail } = useMembers();
|
||||
const { t } = useTranslation();
|
||||
const inputChange = (value: string) => {
|
||||
setShowMember(true);
|
||||
if (gmailReg.test(value)) {
|
||||
setEmail(value);
|
||||
setShowTip(false);
|
||||
getUserByEmail(value).then(data => {
|
||||
if (data?.name) {
|
||||
setUserData(data);
|
||||
setShowTip(false);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
setShowTip(true);
|
||||
}
|
||||
};
|
||||
return (
|
||||
<div>
|
||||
<Modal open={open} onClose={onClose}>
|
||||
<ModalWrapper width={460} height={236}>
|
||||
<Header>
|
||||
<ModalCloseButton
|
||||
onClick={() => {
|
||||
onClose();
|
||||
setEmail('');
|
||||
}}
|
||||
/>
|
||||
</Header>
|
||||
<Content>
|
||||
<ContentTitle>{t('Invite Members')}</ContentTitle>
|
||||
<InviteBox>
|
||||
<Input
|
||||
width={360}
|
||||
value={email}
|
||||
onChange={inputChange}
|
||||
onBlur={() => {
|
||||
setShowMember(false);
|
||||
}}
|
||||
placeholder={t('Invite placeholder')}
|
||||
></Input>
|
||||
{showMember ? (
|
||||
<Members>
|
||||
{showTip ? (
|
||||
<NoFind>{t('Non-Gmail')}</NoFind>
|
||||
) : (
|
||||
<Member>
|
||||
{userData?.avatar ? (
|
||||
<MuiAvatar src={userData?.avatar}></MuiAvatar>
|
||||
) : (
|
||||
<MemberIcon>
|
||||
<EmailIcon></EmailIcon>
|
||||
</MemberIcon>
|
||||
)}
|
||||
<Email>{email}</Email>
|
||||
{/* <div>invited</div> */}
|
||||
</Member>
|
||||
)}
|
||||
</Members>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</InviteBox>
|
||||
</Content>
|
||||
<Footer>
|
||||
<Button
|
||||
shape="circle"
|
||||
type="primary"
|
||||
style={{ width: '364px', height: '38px', borderRadius: '40px' }}
|
||||
onClick={async () => {
|
||||
await inviteMember(email);
|
||||
setEmail('');
|
||||
onInviteSuccess();
|
||||
}}
|
||||
>
|
||||
{t('Invite')}
|
||||
</Button>
|
||||
</Footer>
|
||||
</ModalWrapper>
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const Header = styled('div')({
|
||||
position: 'relative',
|
||||
height: '44px',
|
||||
});
|
||||
|
||||
const Content = styled('div')({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
});
|
||||
|
||||
const ContentTitle = styled('h1')({
|
||||
fontSize: '20px',
|
||||
lineHeight: '28px',
|
||||
fontWeight: 600,
|
||||
textAlign: 'center',
|
||||
paddingBottom: '16px',
|
||||
});
|
||||
|
||||
const Footer = styled('div')({
|
||||
height: '102px',
|
||||
margin: '32px 0',
|
||||
textAlign: 'center',
|
||||
});
|
||||
|
||||
const InviteBox = styled('div')({
|
||||
position: 'relative',
|
||||
});
|
||||
|
||||
const Members = styled('div')(({ theme }) => {
|
||||
return {
|
||||
position: 'absolute',
|
||||
width: '100%',
|
||||
background: theme.colors.pageBackground,
|
||||
textAlign: 'left',
|
||||
zIndex: 1,
|
||||
borderRadius: '0px 10px 10px 10px',
|
||||
height: '56px',
|
||||
padding: '8px 12px',
|
||||
input: {
|
||||
'&::placeholder': {
|
||||
color: theme.colors.placeHolderColor,
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const NoFind = styled('div')(({ theme }) => {
|
||||
return {
|
||||
color: theme.colors.iconColor,
|
||||
fontSize: theme.font.sm,
|
||||
lineHeight: '40px',
|
||||
userSelect: 'none',
|
||||
width: '100%',
|
||||
};
|
||||
});
|
||||
|
||||
const Member = styled('div')(({ theme }) => {
|
||||
return {
|
||||
color: theme.colors.iconColor,
|
||||
fontSize: theme.font.sm,
|
||||
lineHeight: '40px',
|
||||
userSelect: 'none',
|
||||
display: 'flex',
|
||||
};
|
||||
});
|
||||
|
||||
const MemberIcon = styled('div')(({ theme }) => {
|
||||
return {
|
||||
width: '40px',
|
||||
height: '40px',
|
||||
borderRadius: '50%',
|
||||
color: theme.colors.primaryColor,
|
||||
background: '#F5F5F5',
|
||||
marginRight: '8px',
|
||||
textAlign: 'center',
|
||||
lineHeight: '45px',
|
||||
// icon size
|
||||
fontSize: '20px',
|
||||
overflow: 'hidden',
|
||||
img: {
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const Email = styled('div')(({ theme }) => {
|
||||
return {
|
||||
flex: '1',
|
||||
color: theme.colors.popoverColor,
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
};
|
||||
});
|
||||
178
apps/web/src/components/workspace-setting/member/MembersPage.tsx
Normal file
178
apps/web/src/components/workspace-setting/member/MembersPage.tsx
Normal file
@@ -0,0 +1,178 @@
|
||||
import {
|
||||
StyledMemberAvatar,
|
||||
StyledMemberButtonContainer,
|
||||
StyledMemberEmail,
|
||||
StyledMemberInfo,
|
||||
StyledMemberListContainer,
|
||||
StyledMemberListItem,
|
||||
StyledMemberName,
|
||||
StyledMemberNameContainer,
|
||||
StyledMemberRoleContainer,
|
||||
StyledMemberTitleContainer,
|
||||
StyledMoreVerticalButton,
|
||||
StyledMemberContainer,
|
||||
} from './style';
|
||||
import { Wrapper } from '@affine/component';
|
||||
import { MoreVerticalIcon, EmailIcon, TrashIcon } from '@blocksuite/icons';
|
||||
import { useState } from 'react';
|
||||
import { Button, IconButton } from '@affine/component';
|
||||
import { InviteMemberModal } from './InviteMemberModal';
|
||||
import { Menu, MenuItem } from '@affine/component';
|
||||
import { Empty } from '@affine/component';
|
||||
import { WorkspaceUnit } from '@affine/datacenter';
|
||||
import { useConfirm } from '@/providers/ConfirmProvider';
|
||||
import { toast } from '@affine/component';
|
||||
import useMembers from '@/hooks/use-members';
|
||||
import Loading from '@/components/loading';
|
||||
import { FlexWrapper } from '@affine/component';
|
||||
import { useTranslation } from '@affine/i18n';
|
||||
import { EnableWorkspaceButton } from '@/components/enable-workspace';
|
||||
|
||||
export const MembersPage = ({ workspace }: { workspace: WorkspaceUnit }) => {
|
||||
const [isInviteModalShow, setIsInviteModalShow] = useState(false);
|
||||
const { members, removeMember, loaded } = useMembers();
|
||||
const { t } = useTranslation();
|
||||
const { confirm } = useConfirm();
|
||||
|
||||
if (workspace.provider === 'affine') {
|
||||
return (
|
||||
<StyledMemberContainer>
|
||||
<StyledMemberListContainer>
|
||||
{!loaded && (
|
||||
<FlexWrapper justifyContent="center">
|
||||
<Loading size={25} />
|
||||
</FlexWrapper>
|
||||
)}
|
||||
{loaded && members.length === 0 && (
|
||||
<Empty width={648} sx={{ marginTop: '60px' }} height={300} />
|
||||
)}
|
||||
{loaded && members.length > 0 && (
|
||||
<>
|
||||
<StyledMemberTitleContainer>
|
||||
<StyledMemberNameContainer>
|
||||
{t('Users')} ({members.length})
|
||||
</StyledMemberNameContainer>
|
||||
<StyledMemberRoleContainer>
|
||||
{t('Access level')}
|
||||
</StyledMemberRoleContainer>
|
||||
<div style={{ width: '24px', paddingRight: '48px' }}></div>
|
||||
</StyledMemberTitleContainer>
|
||||
{members.map((member, index) => {
|
||||
const user = Object.assign(
|
||||
{
|
||||
avatar_url: '',
|
||||
email: '',
|
||||
id: '',
|
||||
name: '',
|
||||
},
|
||||
member.user
|
||||
);
|
||||
return (
|
||||
<StyledMemberListItem key={index}>
|
||||
<StyledMemberNameContainer>
|
||||
<StyledMemberAvatar
|
||||
alt="member avatar"
|
||||
src={user.avatar_url}
|
||||
>
|
||||
<EmailIcon />
|
||||
</StyledMemberAvatar>
|
||||
|
||||
<StyledMemberInfo>
|
||||
<StyledMemberName>{user.name}</StyledMemberName>
|
||||
<StyledMemberEmail>
|
||||
{member.user.email}
|
||||
</StyledMemberEmail>
|
||||
</StyledMemberInfo>
|
||||
</StyledMemberNameContainer>
|
||||
<StyledMemberRoleContainer>
|
||||
{member.accepted
|
||||
? member.type !== 99
|
||||
? t('Member')
|
||||
: t('Owner')
|
||||
: t('Pending')}
|
||||
</StyledMemberRoleContainer>
|
||||
<StyledMoreVerticalButton>
|
||||
{member.type === 99 ? (
|
||||
<></>
|
||||
) : (
|
||||
<Menu
|
||||
content={
|
||||
<>
|
||||
<MenuItem
|
||||
onClick={async () => {
|
||||
const confirmRemove = await confirm({
|
||||
title: t('Delete Member?'),
|
||||
content: t('will delete member'),
|
||||
confirmText: t('Delete'),
|
||||
confirmType: 'danger',
|
||||
});
|
||||
|
||||
if (!confirmRemove) {
|
||||
return;
|
||||
}
|
||||
await removeMember(member.id);
|
||||
toast(
|
||||
t('Member has been removed', {
|
||||
name: user.name,
|
||||
})
|
||||
);
|
||||
}}
|
||||
icon={<TrashIcon />}
|
||||
>
|
||||
{t('Delete')}
|
||||
</MenuItem>
|
||||
</>
|
||||
}
|
||||
placement="bottom-end"
|
||||
disablePortal={true}
|
||||
>
|
||||
<IconButton>
|
||||
<MoreVerticalIcon />
|
||||
</IconButton>
|
||||
</Menu>
|
||||
)}
|
||||
</StyledMoreVerticalButton>
|
||||
</StyledMemberListItem>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
)}
|
||||
</StyledMemberListContainer>
|
||||
<StyledMemberButtonContainer>
|
||||
<Button
|
||||
onClick={() => {
|
||||
setIsInviteModalShow(true);
|
||||
}}
|
||||
type="primary"
|
||||
shape="circle"
|
||||
>
|
||||
{t('Invite Members')}
|
||||
</Button>
|
||||
<InviteMemberModal
|
||||
onClose={() => {
|
||||
setIsInviteModalShow(false);
|
||||
}}
|
||||
onInviteSuccess={() => {
|
||||
setIsInviteModalShow(false);
|
||||
// refreshMembers();
|
||||
}}
|
||||
workspaceId={workspace.id}
|
||||
open={isInviteModalShow}
|
||||
></InviteMemberModal>
|
||||
</StyledMemberButtonContainer>
|
||||
</StyledMemberContainer>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Wrapper
|
||||
style={{
|
||||
fontWeight: '500',
|
||||
fontSize: '18px',
|
||||
}}
|
||||
>
|
||||
<Wrapper marginBottom="32px">{t('Collaboration Description')}</Wrapper>
|
||||
<EnableWorkspaceButton />
|
||||
</Wrapper>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export * from './MembersPage';
|
||||
99
apps/web/src/components/workspace-setting/member/style.ts
Normal file
99
apps/web/src/components/workspace-setting/member/style.ts
Normal file
@@ -0,0 +1,99 @@
|
||||
import { styled } from '@affine/component';
|
||||
import { MuiAvatar } from '@affine/component';
|
||||
|
||||
export const StyledMemberTitleContainer = styled('li')(() => {
|
||||
return {
|
||||
display: 'flex',
|
||||
fontWeight: '500',
|
||||
marginBottom: '32px',
|
||||
flex: 1,
|
||||
};
|
||||
});
|
||||
export const StyledMemberContainer = styled('div')(() => {
|
||||
return {
|
||||
display: 'flex',
|
||||
height: '100%',
|
||||
flexDirection: 'column',
|
||||
};
|
||||
});
|
||||
|
||||
export const StyledMemberAvatar = styled(MuiAvatar)(() => {
|
||||
return { height: '40px', width: '40px' };
|
||||
});
|
||||
|
||||
export const StyledMemberNameContainer = styled('div')(() => {
|
||||
return {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
flex: '2 0 402px',
|
||||
};
|
||||
});
|
||||
|
||||
export const StyledMemberRoleContainer = styled('div')(() => {
|
||||
return {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
flex: '1 0 222px',
|
||||
};
|
||||
});
|
||||
|
||||
export const StyledMemberListContainer = styled('ul')(() => {
|
||||
return {
|
||||
overflowY: 'scroll',
|
||||
width: '100%',
|
||||
flex: 1,
|
||||
};
|
||||
});
|
||||
|
||||
export const StyledMemberListItem = styled('li')(() => {
|
||||
return {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
height: '72px',
|
||||
width: '100%',
|
||||
};
|
||||
});
|
||||
|
||||
export const StyledMemberInfo = styled('div')(() => {
|
||||
return {
|
||||
paddingLeft: '12px',
|
||||
};
|
||||
});
|
||||
|
||||
export const StyledMemberName = styled('div')(({ theme }) => {
|
||||
return {
|
||||
fontWeight: '400',
|
||||
fontSize: '18px',
|
||||
lineHeight: '26px',
|
||||
color: theme.colors.textColor,
|
||||
};
|
||||
});
|
||||
|
||||
export const StyledMemberEmail = styled('div')(({ theme }) => {
|
||||
return {
|
||||
fontWeight: '400',
|
||||
fontSize: '16px',
|
||||
lineHeight: '22px',
|
||||
color: theme.colors.iconColor,
|
||||
};
|
||||
});
|
||||
|
||||
export const StyledMemberButtonContainer = styled('div')(() => {
|
||||
return {
|
||||
position: 'absolute',
|
||||
bottom: '0',
|
||||
marginBottom: '20px',
|
||||
};
|
||||
});
|
||||
|
||||
export const StyledMoreVerticalButton = styled('button')(() => {
|
||||
return {
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
width: '24px',
|
||||
height: '24px',
|
||||
cursor: 'pointer',
|
||||
paddingRight: '48px',
|
||||
};
|
||||
});
|
||||
Reference in New Issue
Block a user