mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-25 22:38:56 +08:00
milestone: publish alpha version (#637)
- document folder - full-text search - blob storage - basic edgeless support Co-authored-by: tzhangchi <terry.zhangchi@outlook.com> Co-authored-by: QiShaoXuan <qishaoxuan777@gmail.com> Co-authored-by: DiamondThree <diamond.shx@gmail.com> Co-authored-by: MingLiang Wang <mingliangwang0o0@gmail.com> Co-authored-by: JimmFly <yangjinfei001@gmail.com> Co-authored-by: Yifeng Wang <doodlewind@toeverything.info> Co-authored-by: Himself65 <himself65@outlook.com> Co-authored-by: lawvs <18554747+lawvs@users.noreply.github.com> Co-authored-by: Qi <474021214@qq.com>
This commit is contained in:
@@ -0,0 +1,84 @@
|
||||
import Modal from '@/ui/modal';
|
||||
import Input from '@/ui/input';
|
||||
import {
|
||||
StyledModalHeader,
|
||||
StyledTextContent,
|
||||
StyledModalWrapper,
|
||||
StyledInputContent,
|
||||
StyledButtonContent,
|
||||
StyledWorkspaceName,
|
||||
} from './style';
|
||||
import { useState } from 'react';
|
||||
import { ModalCloseButton } from '@/ui/modal';
|
||||
import { Button } from '@/ui/button';
|
||||
import { deleteWorkspace } from '@affine/data-services';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useAppState } from '@/providers/app-state-provider';
|
||||
|
||||
interface WorkspaceDeleteProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
workspaceName: string;
|
||||
workspaceId: string;
|
||||
nextWorkSpaceId: string;
|
||||
}
|
||||
|
||||
export const WorkspaceDelete = ({
|
||||
open,
|
||||
onClose,
|
||||
workspaceId,
|
||||
workspaceName,
|
||||
nextWorkSpaceId,
|
||||
}: WorkspaceDeleteProps) => {
|
||||
const [deleteStr, setDeleteStr] = useState<string>('');
|
||||
const { refreshWorkspacesMeta } = useAppState();
|
||||
const router = useRouter();
|
||||
|
||||
const handlerInputChange = (workspaceName: string) => {
|
||||
setDeleteStr(workspaceName);
|
||||
};
|
||||
|
||||
const handleDelete = async () => {
|
||||
await deleteWorkspace({ id: workspaceId });
|
||||
router.push(`/workspace/${nextWorkSpaceId}`);
|
||||
refreshWorkspacesMeta();
|
||||
onClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal open={open} onClose={onClose}>
|
||||
<StyledModalWrapper>
|
||||
<ModalCloseButton onClick={onClose} />
|
||||
<StyledModalHeader>Delete Workspace</StyledModalHeader>
|
||||
<StyledTextContent>
|
||||
This action cannot be undone. This will permanently delete (
|
||||
<StyledWorkspaceName>{workspaceName}</StyledWorkspaceName>) along with
|
||||
all its content.
|
||||
</StyledTextContent>
|
||||
<StyledInputContent>
|
||||
<Input
|
||||
onChange={handlerInputChange}
|
||||
placeholder="Please type “Delete” to confirm"
|
||||
value={deleteStr}
|
||||
></Input>
|
||||
</StyledInputContent>
|
||||
<StyledButtonContent>
|
||||
<Button shape="circle" onClick={onClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
disabled={deleteStr.toLowerCase() !== 'delete'}
|
||||
onClick={handleDelete}
|
||||
type="danger"
|
||||
shape="circle"
|
||||
style={{ marginLeft: '24px' }}
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
</StyledButtonContent>
|
||||
</StyledModalWrapper>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default WorkspaceDelete;
|
||||
@@ -0,0 +1 @@
|
||||
export * from './delete';
|
||||
@@ -0,0 +1,75 @@
|
||||
import { styled } from '@/styles';
|
||||
|
||||
export const StyledModalWrapper = styled('div')(({ theme }) => {
|
||||
return {
|
||||
position: 'relative',
|
||||
padding: '0px',
|
||||
width: '460px',
|
||||
background: theme.colors.popoverBackground,
|
||||
borderRadius: '12px',
|
||||
};
|
||||
});
|
||||
|
||||
export const StyledModalHeader = styled('div')(({ theme }) => {
|
||||
return {
|
||||
margin: '44px 0px 12px 0px',
|
||||
width: '460px',
|
||||
fontWeight: '600',
|
||||
fontSize: '20px;',
|
||||
textAlign: 'center',
|
||||
color: theme.colors.popoverColor,
|
||||
};
|
||||
});
|
||||
|
||||
// export const StyledModalContent = styled('div')(({ theme }) => {});
|
||||
|
||||
export const StyledTextContent = styled('div')(() => {
|
||||
return {
|
||||
margin: 'auto',
|
||||
width: '425px',
|
||||
fontFamily: 'Avenir Next',
|
||||
fontStyle: 'normal',
|
||||
fontWeight: '400',
|
||||
fontSize: '18px',
|
||||
lineHeight: '26px',
|
||||
textAlign: 'center',
|
||||
};
|
||||
});
|
||||
|
||||
export const StyledInputContent = styled('div')(() => {
|
||||
return {
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'center',
|
||||
margin: '40px 0 24px 0',
|
||||
};
|
||||
});
|
||||
|
||||
export const StyledButtonContent = styled('div')(() => {
|
||||
return {
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'center',
|
||||
margin: '0px 0 32px 0',
|
||||
};
|
||||
});
|
||||
|
||||
export const StyledWorkspaceName = styled('span')(() => {
|
||||
return {
|
||||
color: '#E8178A',
|
||||
};
|
||||
});
|
||||
|
||||
// export const StyledCancelButton = styled(Button)(({ theme }) => {
|
||||
// return {
|
||||
// width: '100px',
|
||||
// justifyContent: 'center',
|
||||
// };
|
||||
// });
|
||||
|
||||
// export const StyledDeleteButton = styled(Button)(({ theme }) => {
|
||||
// return {
|
||||
// width: '100px',
|
||||
// justifyContent: 'center',
|
||||
// };
|
||||
// });
|
||||
@@ -0,0 +1,166 @@
|
||||
import {
|
||||
StyledDeleteButtonContainer,
|
||||
StyledSettingAvatar,
|
||||
StyledSettingAvatarContent,
|
||||
StyledSettingInputContainer,
|
||||
} from './style';
|
||||
import { StyledSettingH2 } from '../style';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { Button } from '@/ui/button';
|
||||
import Input from '@/ui/input';
|
||||
import { uploadBlob, Workspace, WorkspaceType } from '@affine/data-services';
|
||||
import { useAppState } from '@/providers/app-state-provider';
|
||||
import { WorkspaceDetails } from '@/components/workspace-slider-bar/WorkspaceSelector/SelectorPopperContent';
|
||||
import { WorkspaceDelete } from './delete';
|
||||
import { Workspace as StoreWorkspace } from '@blocksuite/store';
|
||||
import { debounce } from '@/utils';
|
||||
import { WorkspaceLeave } from './leave';
|
||||
import { Upload } from '@/components/file-upload';
|
||||
|
||||
export const GeneralPage = ({
|
||||
workspace,
|
||||
owner,
|
||||
}: {
|
||||
workspace: Workspace;
|
||||
owner: WorkspaceDetails[string]['owner'];
|
||||
workspaces: Record<string, StoreWorkspace | null>;
|
||||
}) => {
|
||||
const {
|
||||
user,
|
||||
currentWorkspace,
|
||||
workspacesMeta,
|
||||
workspaces,
|
||||
refreshWorkspacesMeta,
|
||||
} = useAppState();
|
||||
const [showDelete, setShowDelete] = useState<boolean>(false);
|
||||
const [showLeave, setShowLeave] = useState<boolean>(false);
|
||||
const [uploading, setUploading] = useState<boolean>(false);
|
||||
const [workspaceName, setWorkspaceName] = useState<string>(
|
||||
workspaces[workspace.id]?.meta.name ||
|
||||
(workspace.type === WorkspaceType.Private && user ? user.name : '')
|
||||
);
|
||||
const debouncedRefreshWorkspacesMeta = debounce(() => {
|
||||
refreshWorkspacesMeta();
|
||||
}, 100);
|
||||
const isOwner = user && owner.id === user.id;
|
||||
const handleChangeWorkSpaceName = (newName: string) => {
|
||||
setWorkspaceName(newName);
|
||||
currentWorkspace?.meta.setName(newName);
|
||||
workspaces[workspace.id]?.meta.setName(newName);
|
||||
debouncedRefreshWorkspacesMeta();
|
||||
};
|
||||
const currentWorkspaceIndex = workspacesMeta.findIndex(
|
||||
meta => meta.id === workspace.id
|
||||
);
|
||||
const nextWorkSpaceId =
|
||||
currentWorkspaceIndex === workspacesMeta.length - 1
|
||||
? workspacesMeta[currentWorkspaceIndex - 1]?.id
|
||||
: workspacesMeta[currentWorkspaceIndex + 1]?.id;
|
||||
const handleClickDelete = () => {
|
||||
setShowDelete(true);
|
||||
};
|
||||
const handleCloseDelete = () => {
|
||||
setShowDelete(false);
|
||||
};
|
||||
|
||||
const handleClickLeave = () => {
|
||||
setShowLeave(true);
|
||||
};
|
||||
const handleCloseLeave = () => {
|
||||
setShowLeave(false);
|
||||
};
|
||||
|
||||
const fileChange = async (file: File) => {
|
||||
setUploading(true);
|
||||
const blob = new Blob([file], { type: file.type });
|
||||
const blobId = await uploadBlob({ blob }).finally(() => {
|
||||
setUploading(false);
|
||||
});
|
||||
if (blobId) {
|
||||
currentWorkspace?.meta.setAvatar(blobId);
|
||||
workspaces[workspace.id]?.meta.setAvatar(blobId);
|
||||
setUploading(false);
|
||||
debouncedRefreshWorkspacesMeta();
|
||||
}
|
||||
};
|
||||
|
||||
return workspace ? (
|
||||
<div>
|
||||
<StyledSettingH2 marginTop={56}>Workspace Avatar</StyledSettingH2>
|
||||
<StyledSettingAvatarContent>
|
||||
<StyledSettingAvatar
|
||||
alt="workspace avatar"
|
||||
src={
|
||||
workspaces[workspace.id]?.meta.avatar
|
||||
? '/api/blob/' + workspaces[workspace.id]?.meta.avatar
|
||||
: ''
|
||||
}
|
||||
>
|
||||
{workspaces[workspace.id]?.meta.name[0]}
|
||||
</StyledSettingAvatar>
|
||||
<Upload
|
||||
accept="image/gif,image/jpeg,image/jpg,image/png,image/svg"
|
||||
fileChange={fileChange}
|
||||
>
|
||||
<Button loading={uploading}>Upload</Button>
|
||||
</Upload>
|
||||
{/* TODO: add upload logic */}
|
||||
{/* {isOwner ? (
|
||||
<StyledAvatarUploadBtn shape="round">upload</StyledAvatarUploadBtn>
|
||||
) : null} */}
|
||||
{/* <Button shape="round">remove</Button> */}
|
||||
</StyledSettingAvatarContent>
|
||||
<StyledSettingH2 marginTop={36}>Workspace Name</StyledSettingH2>
|
||||
<StyledSettingInputContainer>
|
||||
<Input
|
||||
width={327}
|
||||
value={workspaceName}
|
||||
placeholder="Workspace Name"
|
||||
disabled={!isOwner}
|
||||
maxLength={14}
|
||||
minLength={1}
|
||||
onChange={handleChangeWorkSpaceName}
|
||||
></Input>
|
||||
</StyledSettingInputContainer>
|
||||
<StyledSettingH2 marginTop={36}>Workspace Owner</StyledSettingH2>
|
||||
<StyledSettingInputContainer>
|
||||
<Input
|
||||
width={327}
|
||||
disabled
|
||||
value={owner.name}
|
||||
placeholder="Workspace Owner"
|
||||
></Input>
|
||||
</StyledSettingInputContainer>
|
||||
<StyledDeleteButtonContainer>
|
||||
{isOwner ? (
|
||||
<>
|
||||
<Button type="danger" shape="circle" onClick={handleClickDelete}>
|
||||
Delete Workspace
|
||||
</Button>
|
||||
<WorkspaceDelete
|
||||
open={showDelete}
|
||||
onClose={handleCloseDelete}
|
||||
workspaceName={workspaceName}
|
||||
workspaceId={workspace.id}
|
||||
nextWorkSpaceId={nextWorkSpaceId}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Button type="danger" shape="circle" onClick={handleClickLeave}>
|
||||
Leave Workspace
|
||||
</Button>
|
||||
<WorkspaceLeave
|
||||
open={showLeave}
|
||||
onClose={handleCloseLeave}
|
||||
workspaceName={workspaceName}
|
||||
workspaceId={workspace.id}
|
||||
nextWorkSpaceId={nextWorkSpaceId}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</StyledDeleteButtonContainer>
|
||||
</div>
|
||||
) : null;
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export * from './general';
|
||||
@@ -0,0 +1 @@
|
||||
export * from './leave';
|
||||
@@ -0,0 +1,64 @@
|
||||
import Modal from '@/ui/modal';
|
||||
import {
|
||||
StyledModalHeader,
|
||||
StyledTextContent,
|
||||
StyledModalWrapper,
|
||||
StyledButtonContent,
|
||||
} from './style';
|
||||
import { ModalCloseButton } from '@/ui/modal';
|
||||
import { Button } from '@/ui/button';
|
||||
import { leaveWorkspace } from '@affine/data-services';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useAppState } from '@/providers/app-state-provider';
|
||||
|
||||
interface WorkspaceDeleteProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
workspaceName: string;
|
||||
workspaceId: string;
|
||||
nextWorkSpaceId: string;
|
||||
}
|
||||
|
||||
export const WorkspaceLeave = ({
|
||||
open,
|
||||
onClose,
|
||||
nextWorkSpaceId,
|
||||
workspaceId,
|
||||
}: WorkspaceDeleteProps) => {
|
||||
const router = useRouter();
|
||||
const { refreshWorkspacesMeta } = useAppState();
|
||||
const handleLeave = async () => {
|
||||
await leaveWorkspace({ id: workspaceId });
|
||||
router.push(`/workspace/${nextWorkSpaceId}`);
|
||||
refreshWorkspacesMeta();
|
||||
onClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal open={open} onClose={onClose}>
|
||||
<StyledModalWrapper>
|
||||
<ModalCloseButton onClick={onClose} />
|
||||
<StyledModalHeader>Leave Workspace</StyledModalHeader>
|
||||
<StyledTextContent>
|
||||
After you leave, you will not be able to access all the contents of
|
||||
this workspace.
|
||||
</StyledTextContent>
|
||||
<StyledButtonContent>
|
||||
<Button shape="circle" onClick={onClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleLeave}
|
||||
type="danger"
|
||||
shape="circle"
|
||||
style={{ marginLeft: '24px' }}
|
||||
>
|
||||
Leave
|
||||
</Button>
|
||||
</StyledButtonContent>
|
||||
</StyledModalWrapper>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default WorkspaceLeave;
|
||||
@@ -0,0 +1,46 @@
|
||||
import { styled } from '@/styles';
|
||||
|
||||
export const StyledModalWrapper = styled('div')(({ theme }) => {
|
||||
return {
|
||||
position: 'relative',
|
||||
padding: '0px',
|
||||
width: '460px',
|
||||
background: theme.colors.popoverBackground,
|
||||
borderRadius: '12px',
|
||||
};
|
||||
});
|
||||
|
||||
export const StyledModalHeader = styled('div')(({ theme }) => {
|
||||
return {
|
||||
margin: '44px 0px 12px 0px',
|
||||
width: '460px',
|
||||
fontWeight: '600',
|
||||
fontSize: '20px;',
|
||||
textAlign: 'center',
|
||||
color: theme.colors.popoverColor,
|
||||
};
|
||||
});
|
||||
|
||||
// export const StyledModalContent = styled('div')(({ theme }) => {});
|
||||
|
||||
export const StyledTextContent = styled('div')(() => {
|
||||
return {
|
||||
margin: 'auto',
|
||||
width: '425px',
|
||||
fontFamily: 'Avenir Next',
|
||||
fontStyle: 'normal',
|
||||
fontWeight: '400',
|
||||
fontSize: '18px',
|
||||
lineHeight: '26px',
|
||||
textAlign: 'center',
|
||||
};
|
||||
});
|
||||
|
||||
export const StyledButtonContent = styled('div')(() => {
|
||||
return {
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'center',
|
||||
margin: '0px 0 32px 0',
|
||||
};
|
||||
});
|
||||
@@ -0,0 +1,28 @@
|
||||
import { styled } from '@/styles';
|
||||
import MuiAvatar from '@mui/material/Avatar';
|
||||
|
||||
export const StyledSettingInputContainer = styled('div')(() => {
|
||||
return {
|
||||
marginTop: '12px',
|
||||
};
|
||||
});
|
||||
|
||||
export const StyledDeleteButtonContainer = styled('div')(() => {
|
||||
return {
|
||||
marginTop: '154px',
|
||||
};
|
||||
});
|
||||
|
||||
export const StyledSettingAvatarContent = styled('div')(() => {
|
||||
return {
|
||||
marginTop: '12px',
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
height: '72px',
|
||||
};
|
||||
});
|
||||
|
||||
export const StyledSettingAvatar = styled(MuiAvatar)(() => {
|
||||
return { height: '72px', width: '72px', marginRight: '24px' };
|
||||
});
|
||||
@@ -0,0 +1 @@
|
||||
export * from './workspace-setting';
|
||||
@@ -0,0 +1,230 @@
|
||||
import { styled } from '@/styles';
|
||||
import { Button } from '@/ui/button';
|
||||
import MuiAvatar from '@mui/material/Avatar';
|
||||
|
||||
export const StyledSettingContainer = styled('div')(({ theme }) => {
|
||||
return {
|
||||
position: 'relative',
|
||||
display: 'flex',
|
||||
padding: '0px',
|
||||
width: '961px',
|
||||
background: theme.colors.popoverBackground,
|
||||
borderRadius: '12px',
|
||||
overflow: 'hidden',
|
||||
};
|
||||
});
|
||||
|
||||
export const StyledSettingSidebar = styled('div')(({ theme }) => {
|
||||
{
|
||||
return {
|
||||
width: '212px',
|
||||
height: '620px',
|
||||
background: theme.mode === 'dark' ? '#272727' : '#FBFBFC',
|
||||
flexShrink: 0,
|
||||
flexGrow: 0,
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
export const StyledSettingContent = styled('div')(() => {
|
||||
return {
|
||||
paddingLeft: '48px',
|
||||
height: '620px',
|
||||
};
|
||||
});
|
||||
|
||||
export const StyledSetting = styled('div')(({ theme }) => {
|
||||
{
|
||||
return {
|
||||
width: '236px',
|
||||
height: '620px',
|
||||
background: theme.mode === 'dark' ? '#272727' : '#FBFBFC',
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
export const StyledSettingSidebarHeader = styled('div')(() => {
|
||||
{
|
||||
return {
|
||||
fontWeight: '500',
|
||||
fontSize: '18px',
|
||||
lineHeight: '26px',
|
||||
textAlign: 'center',
|
||||
marginTop: '37px',
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
export const StyledSettingTabContainer = styled('ul')(() => {
|
||||
{
|
||||
return {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
marginTop: '25px',
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
export const WorkspaceSettingTagItem = styled('li')<{ isActive?: boolean }>(
|
||||
({ theme, isActive }) => {
|
||||
{
|
||||
return {
|
||||
display: 'flex',
|
||||
marginBottom: '12px',
|
||||
padding: '0 24px',
|
||||
height: '32px',
|
||||
color: isActive ? theme.colors.primaryColor : theme.colors.textColor,
|
||||
fontWeight: '400',
|
||||
fontSize: '16px',
|
||||
lineHeight: '32px',
|
||||
cursor: 'pointer',
|
||||
};
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
export const StyledSettingTagIconContainer = styled('div')(() => {
|
||||
return {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
marginRight: '14.64px',
|
||||
width: '14.47px',
|
||||
fontSize: '14.47px',
|
||||
};
|
||||
});
|
||||
|
||||
export const StyledSettingH2 = styled('h2')<{ marginTop?: number }>(
|
||||
({ marginTop }) => {
|
||||
return {
|
||||
fontWeight: '500',
|
||||
fontSize: '18px',
|
||||
lineHeight: '26px',
|
||||
marginTop: marginTop ? `${marginTop}px` : '0px',
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
export const StyledAvatarUploadBtn = styled(Button)(({ theme }) => {
|
||||
return {
|
||||
backgroundColor: theme.colors.hoverBackground,
|
||||
color: theme.colors.primaryColor,
|
||||
margin: '0 12px 0 24px',
|
||||
};
|
||||
});
|
||||
|
||||
export const StyledMemberTitleContainer = styled('div')(() => {
|
||||
return {
|
||||
display: 'flex',
|
||||
marginTop: '60px',
|
||||
fontWeight: '500',
|
||||
};
|
||||
});
|
||||
|
||||
export const StyledMemberAvatar = styled(MuiAvatar)(() => {
|
||||
return { height: '40px', width: '40px' };
|
||||
});
|
||||
|
||||
export const StyledMemberNameContainer = styled('div')(() => {
|
||||
return {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
width: '402px',
|
||||
};
|
||||
});
|
||||
|
||||
export const StyledMemberRoleContainer = styled('div')(() => {
|
||||
return {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
width: '222px',
|
||||
};
|
||||
});
|
||||
|
||||
export const StyledMemberListContainer = styled('ul')(() => {
|
||||
return {
|
||||
marginTop: '15px',
|
||||
height: '432px',
|
||||
overflowY: 'scroll',
|
||||
};
|
||||
});
|
||||
|
||||
export const StyledMemberListItem = styled('li')(() => {
|
||||
return {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
height: '72px',
|
||||
};
|
||||
});
|
||||
|
||||
export const StyledMemberInfo = styled('div')(() => {
|
||||
return {
|
||||
paddingLeft: '12px',
|
||||
};
|
||||
});
|
||||
|
||||
export const StyledMemberName = styled('div')(({ theme }) => {
|
||||
return {
|
||||
fontWeight: '400',
|
||||
fontSize: '18px',
|
||||
lineHeight: '16px',
|
||||
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 {
|
||||
marginTop: '14px',
|
||||
};
|
||||
});
|
||||
|
||||
export const StyledMoreVerticalButton = styled('button')(() => {
|
||||
return {
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
width: '24px',
|
||||
height: '24px',
|
||||
cursor: 'pointer',
|
||||
};
|
||||
});
|
||||
|
||||
export const StyledPublishExplanation = styled('div')(() => {
|
||||
return {
|
||||
marginTop: '56px',
|
||||
paddingRight: '48px',
|
||||
fontWeight: '500',
|
||||
fontSize: '18px',
|
||||
lineHeight: '26px',
|
||||
};
|
||||
});
|
||||
|
||||
export const StyledPublishCopyContainer = styled('div')(() => {
|
||||
return {
|
||||
marginTop: '12px',
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
height: '38px',
|
||||
};
|
||||
});
|
||||
|
||||
export const StyledCopyButtonContainer = styled('div')(() => {
|
||||
return {
|
||||
marginLeft: '12px',
|
||||
};
|
||||
});
|
||||
|
||||
export const StyledPublishContent = styled('div')(() => {
|
||||
return {
|
||||
height: '494px',
|
||||
};
|
||||
});
|
||||
@@ -0,0 +1,361 @@
|
||||
import Modal, { ModalCloseButton } from '@/ui/modal';
|
||||
import {
|
||||
StyledCopyButtonContainer,
|
||||
StyledMemberAvatar,
|
||||
StyledMemberButtonContainer,
|
||||
StyledMemberEmail,
|
||||
StyledMemberInfo,
|
||||
StyledMemberListContainer,
|
||||
StyledMemberListItem,
|
||||
StyledMemberName,
|
||||
StyledMemberNameContainer,
|
||||
StyledMemberRoleContainer,
|
||||
StyledMemberTitleContainer,
|
||||
StyledMoreVerticalButton,
|
||||
StyledPublishContent,
|
||||
StyledPublishCopyContainer,
|
||||
StyledPublishExplanation,
|
||||
StyledSettingContainer,
|
||||
StyledSettingContent,
|
||||
StyledSettingH2,
|
||||
StyledSettingSidebar,
|
||||
StyledSettingSidebarHeader,
|
||||
StyledSettingTabContainer,
|
||||
StyledSettingTagIconContainer,
|
||||
WorkspaceSettingTagItem,
|
||||
} from './style';
|
||||
import {
|
||||
EditIcon,
|
||||
UsersIcon,
|
||||
PublishIcon,
|
||||
MoreVerticalIcon,
|
||||
EmailIcon,
|
||||
TrashIcon,
|
||||
} from '@blocksuite/icons';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { Button, IconButton } from '@/ui/button';
|
||||
import Input from '@/ui/input';
|
||||
import { InviteMembers } from '../invite-members/index';
|
||||
import {
|
||||
getWorkspaceMembers,
|
||||
Workspace,
|
||||
Member,
|
||||
removeMember,
|
||||
updateWorkspace,
|
||||
} from '@affine/data-services';
|
||||
import { Avatar } from '@mui/material';
|
||||
import { Menu, MenuItem } from '@/ui/menu';
|
||||
import { toast } from '@/ui/toast';
|
||||
import { Empty } from '@/ui/empty';
|
||||
import { useAppState } from '@/providers/app-state-provider';
|
||||
import { WorkspaceDetails } from '../workspace-slider-bar/WorkspaceSelector/SelectorPopperContent';
|
||||
import { GeneralPage } from './general';
|
||||
|
||||
enum ActiveTab {
|
||||
'general' = 'general',
|
||||
'members' = 'members',
|
||||
'publish' = 'publish',
|
||||
}
|
||||
|
||||
type SettingTabProps = {
|
||||
activeTab: ActiveTab;
|
||||
onTabChange?: (tab: ActiveTab) => void;
|
||||
};
|
||||
|
||||
type WorkspaceSettingProps = {
|
||||
isShow: boolean;
|
||||
onClose?: () => void;
|
||||
workspace: Workspace;
|
||||
owner: WorkspaceDetails[string]['owner'];
|
||||
};
|
||||
|
||||
const WorkspaceSettingTab = ({ activeTab, onTabChange }: SettingTabProps) => {
|
||||
return (
|
||||
<StyledSettingTabContainer>
|
||||
<WorkspaceSettingTagItem
|
||||
isActive={activeTab === ActiveTab.general}
|
||||
onClick={() => {
|
||||
onTabChange && onTabChange(ActiveTab.general);
|
||||
}}
|
||||
>
|
||||
<StyledSettingTagIconContainer>
|
||||
<EditIcon />
|
||||
</StyledSettingTagIconContainer>
|
||||
General
|
||||
</WorkspaceSettingTagItem>
|
||||
<WorkspaceSettingTagItem
|
||||
isActive={activeTab === ActiveTab.members}
|
||||
onClick={() => {
|
||||
onTabChange && onTabChange(ActiveTab.members);
|
||||
}}
|
||||
>
|
||||
<StyledSettingTagIconContainer>
|
||||
<UsersIcon />
|
||||
</StyledSettingTagIconContainer>
|
||||
Members
|
||||
</WorkspaceSettingTagItem>
|
||||
<WorkspaceSettingTagItem
|
||||
isActive={activeTab === ActiveTab.publish}
|
||||
onClick={() => {
|
||||
onTabChange && onTabChange(ActiveTab.publish);
|
||||
}}
|
||||
>
|
||||
<StyledSettingTagIconContainer>
|
||||
<PublishIcon />
|
||||
</StyledSettingTagIconContainer>
|
||||
Publish
|
||||
</WorkspaceSettingTagItem>
|
||||
</StyledSettingTabContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export const WorkspaceSetting = ({
|
||||
isShow,
|
||||
onClose,
|
||||
workspace,
|
||||
owner,
|
||||
}: WorkspaceSettingProps) => {
|
||||
const { user, workspaces } = useAppState();
|
||||
const [activeTab, setActiveTab] = useState<ActiveTab>(ActiveTab.general);
|
||||
const handleTabChange = (tab: ActiveTab) => {
|
||||
setActiveTab(tab);
|
||||
};
|
||||
const handleClickClose = () => {
|
||||
onClose && onClose();
|
||||
};
|
||||
const isOwner = user && owner.id === user.id;
|
||||
useEffect(() => {
|
||||
// reset tab when modal is closed
|
||||
if (!isShow) {
|
||||
setActiveTab(ActiveTab.general);
|
||||
}
|
||||
}, [isShow]);
|
||||
return (
|
||||
<Modal open={isShow}>
|
||||
<StyledSettingContainer>
|
||||
<ModalCloseButton onClick={handleClickClose} />
|
||||
{isOwner ? (
|
||||
<StyledSettingSidebar>
|
||||
<StyledSettingSidebarHeader>
|
||||
Workspace Settings
|
||||
</StyledSettingSidebarHeader>
|
||||
<WorkspaceSettingTab
|
||||
activeTab={activeTab}
|
||||
onTabChange={handleTabChange}
|
||||
/>
|
||||
</StyledSettingSidebar>
|
||||
) : null}
|
||||
<StyledSettingContent>
|
||||
{activeTab === ActiveTab.general && (
|
||||
<GeneralPage
|
||||
workspace={workspace}
|
||||
owner={owner}
|
||||
workspaces={workspaces}
|
||||
/>
|
||||
)}
|
||||
{activeTab === ActiveTab.members && workspace && (
|
||||
<MembersPage workspace={workspace} />
|
||||
)}
|
||||
{activeTab === ActiveTab.publish && (
|
||||
<PublishPage workspace={workspace} />
|
||||
)}
|
||||
</StyledSettingContent>
|
||||
</StyledSettingContainer>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
const MembersPage = ({ workspace }: { workspace: Workspace }) => {
|
||||
const [isInviteModalShow, setIsInviteModalShow] = useState(false);
|
||||
const [members, setMembers] = useState<Member[]>([]);
|
||||
const refreshMembers = useCallback(() => {
|
||||
getWorkspaceMembers({
|
||||
id: workspace.id,
|
||||
})
|
||||
.then(data => {
|
||||
setMembers(data);
|
||||
})
|
||||
.catch(err => {
|
||||
console.log(err);
|
||||
});
|
||||
}, [workspace.id]);
|
||||
useEffect(() => {
|
||||
refreshMembers();
|
||||
}, [refreshMembers]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<StyledMemberTitleContainer>
|
||||
<StyledMemberNameContainer>
|
||||
Users({members.length})
|
||||
</StyledMemberNameContainer>
|
||||
<StyledMemberRoleContainer>Access level</StyledMemberRoleContainer>
|
||||
</StyledMemberTitleContainer>
|
||||
<StyledMemberListContainer>
|
||||
{members.length === 0 && (
|
||||
<Empty width={648} sx={{ marginTop: '60px' }} height={300}></Empty>
|
||||
)}
|
||||
{members.length ? (
|
||||
members.map(member => {
|
||||
return (
|
||||
<StyledMemberListItem key={member.id}>
|
||||
<StyledMemberNameContainer>
|
||||
{member.user.type === 'Registered' ? (
|
||||
<Avatar src={member.user.avatar_url}></Avatar>
|
||||
) : (
|
||||
<StyledMemberAvatar alt="member avatar">
|
||||
<EmailIcon></EmailIcon>
|
||||
</StyledMemberAvatar>
|
||||
)}
|
||||
|
||||
<StyledMemberInfo>
|
||||
{member.user.type === 'Registered' ? (
|
||||
<StyledMemberName>{member.user.name}</StyledMemberName>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
<StyledMemberEmail>{member.user.email}</StyledMemberEmail>
|
||||
</StyledMemberInfo>
|
||||
</StyledMemberNameContainer>
|
||||
<StyledMemberRoleContainer>
|
||||
{member.accepted
|
||||
? member.type !== 99
|
||||
? 'Member'
|
||||
: 'Workspace Owner'
|
||||
: 'Pending'}
|
||||
</StyledMemberRoleContainer>
|
||||
<StyledMoreVerticalButton>
|
||||
<Menu
|
||||
content={
|
||||
<>
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
// confirm({
|
||||
// title: 'Delete Member?',
|
||||
// content: `will delete member`,
|
||||
// confirmText: 'Delete',
|
||||
// confirmType: 'danger',
|
||||
// }).then(confirm => {
|
||||
removeMember({
|
||||
permissionId: member.id,
|
||||
}).then(() => {
|
||||
// console.log('data: ', data);
|
||||
toast('Moved to Trash');
|
||||
refreshMembers();
|
||||
});
|
||||
// });
|
||||
}}
|
||||
icon={<TrashIcon />}
|
||||
>
|
||||
Delete
|
||||
</MenuItem>
|
||||
</>
|
||||
}
|
||||
placement="bottom-end"
|
||||
disablePortal={true}
|
||||
>
|
||||
<IconButton>
|
||||
<MoreVerticalIcon />
|
||||
</IconButton>
|
||||
</Menu>
|
||||
</StyledMoreVerticalButton>
|
||||
</StyledMemberListItem>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</StyledMemberListContainer>
|
||||
<StyledMemberButtonContainer>
|
||||
<Button
|
||||
onClick={() => {
|
||||
setIsInviteModalShow(true);
|
||||
}}
|
||||
type="primary"
|
||||
shape="circle"
|
||||
>
|
||||
Invite Members
|
||||
</Button>
|
||||
<InviteMembers
|
||||
onClose={() => {
|
||||
setIsInviteModalShow(false);
|
||||
}}
|
||||
onInviteSuccess={() => {
|
||||
refreshMembers();
|
||||
}}
|
||||
workspaceId={workspace.id}
|
||||
open={isInviteModalShow}
|
||||
></InviteMembers>
|
||||
</StyledMemberButtonContainer>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const PublishPage = ({ workspace }: { workspace: Workspace }) => {
|
||||
const shareUrl = window.location.host + '/workspace/' + workspace.id;
|
||||
const [publicStatus, setPublicStatus] = useState<boolean | null>(
|
||||
workspace.public
|
||||
);
|
||||
const togglePublic = (flag: boolean) => {
|
||||
updateWorkspace({
|
||||
id: workspace.id,
|
||||
public: flag,
|
||||
}).then(data => {
|
||||
setPublicStatus(data?.public);
|
||||
toast('Updated Public Status Success');
|
||||
});
|
||||
};
|
||||
const copyUrl = () => {
|
||||
navigator.clipboard.writeText(shareUrl);
|
||||
toast('Copied url to clipboard');
|
||||
};
|
||||
return (
|
||||
<div>
|
||||
<StyledPublishContent>
|
||||
{publicStatus ? (
|
||||
<>
|
||||
<StyledPublishExplanation>
|
||||
The current workspace has been published to the web, everyone can
|
||||
view the contents of this workspace through the link.
|
||||
</StyledPublishExplanation>
|
||||
<StyledSettingH2 marginTop={48}>Share with link</StyledSettingH2>
|
||||
<StyledPublishCopyContainer>
|
||||
<Input width={500} value={shareUrl} disabled={true}></Input>
|
||||
<StyledCopyButtonContainer>
|
||||
<Button onClick={copyUrl} type="primary" shape="circle">
|
||||
Copy Link
|
||||
</Button>
|
||||
</StyledCopyButtonContainer>
|
||||
</StyledPublishCopyContainer>
|
||||
</>
|
||||
) : (
|
||||
<StyledPublishExplanation>
|
||||
After publishing to the web, everyone can view the content of this
|
||||
workspace through the link.
|
||||
</StyledPublishExplanation>
|
||||
)}
|
||||
</StyledPublishContent>
|
||||
{!publicStatus ? (
|
||||
<Button
|
||||
onClick={() => {
|
||||
togglePublic(true);
|
||||
}}
|
||||
type="primary"
|
||||
shape="circle"
|
||||
>
|
||||
Publish to web
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
onClick={() => {
|
||||
togglePublic(false);
|
||||
}}
|
||||
type="primary"
|
||||
shape="circle"
|
||||
>
|
||||
Stop publishing
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user