mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-19 11:06:25 +08:00
init: the first public commit for AFFiNE
This commit is contained in:
@@ -0,0 +1,35 @@
|
||||
import { styled } from '@toeverything/components/ui';
|
||||
import { DocViewIcon, BoardViewIcon } from '@toeverything/components/icons';
|
||||
import { DocMode } from './type';
|
||||
|
||||
interface StatusIconProps {
|
||||
mode: DocMode;
|
||||
}
|
||||
|
||||
export const StatusIcon = ({ mode }: StatusIconProps) => {
|
||||
return (
|
||||
<IconWrapper mode={mode}>
|
||||
{mode === DocMode.doc ? <DocViewIcon /> : <BoardViewIcon />}
|
||||
</IconWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
const IconWrapper = styled('div')<Pick<StatusIconProps, 'mode'>>(
|
||||
({ theme, mode }) => {
|
||||
return {
|
||||
width: '20px',
|
||||
height: '20px',
|
||||
borderRadius: '5px',
|
||||
boxShadow: '0px 1px 10px rgba(152, 172, 189, 0.6)',
|
||||
color: theme.affine.palette.primary,
|
||||
cursor: 'pointer',
|
||||
backgroundColor: theme.affine.palette.white,
|
||||
transform: `translateX(${mode === DocMode.doc ? 0 : 33}px)`,
|
||||
transition: 'transform 300ms ease',
|
||||
|
||||
'& > svg': {
|
||||
fontSize: '20px',
|
||||
},
|
||||
};
|
||||
}
|
||||
);
|
||||
@@ -0,0 +1,27 @@
|
||||
import { styled } from '@toeverything/components/ui';
|
||||
|
||||
type StatusTextProps = {
|
||||
children: string;
|
||||
active?: boolean;
|
||||
onClick?: () => void;
|
||||
};
|
||||
|
||||
export const StatusText = ({ children, active, onClick }: StatusTextProps) => {
|
||||
return (
|
||||
<StyledText active={active} onClick={onClick}>
|
||||
{children}
|
||||
</StyledText>
|
||||
);
|
||||
};
|
||||
|
||||
const StyledText = styled('div')<StatusTextProps>(({ theme, active }) => {
|
||||
return {
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
color: theme.affine.palette.primary,
|
||||
fontWeight: active ? '500' : '300',
|
||||
fontSize: '15px',
|
||||
cursor: 'pointer',
|
||||
padding: '0 6px',
|
||||
};
|
||||
});
|
||||
@@ -0,0 +1,28 @@
|
||||
import type { FC } from 'react';
|
||||
import type { DocMode } from './type';
|
||||
import { styled } from '@toeverything/components/ui';
|
||||
import { StatusIcon } from './StatusIcon';
|
||||
|
||||
interface StatusTrackProps {
|
||||
mode: DocMode;
|
||||
onClick: () => void;
|
||||
}
|
||||
|
||||
export const StatusTrack: FC<StatusTrackProps> = ({ mode, onClick }) => {
|
||||
return (
|
||||
<Container onClick={onClick}>
|
||||
<StatusIcon mode={mode} />
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
||||
const Container = styled('div')(({ theme }) => {
|
||||
return {
|
||||
backgroundColor: theme.affine.palette.textHover,
|
||||
borderRadius: '5px',
|
||||
height: '30px',
|
||||
width: '63px',
|
||||
cursor: 'pointer',
|
||||
padding: '5px',
|
||||
};
|
||||
});
|
||||
@@ -0,0 +1,63 @@
|
||||
import { useLocation, useParams, useNavigate } from 'react-router-dom';
|
||||
import { styled } from '@toeverything/components/ui';
|
||||
import { StatusText } from './StatusText';
|
||||
import { StatusTrack } from './StatusTrack';
|
||||
import { DocMode } from './type';
|
||||
|
||||
const isBoard = (pathname: string): boolean => pathname.endsWith('/whiteboard');
|
||||
|
||||
export const Switcher = () => {
|
||||
const navigate = useNavigate();
|
||||
const params = useParams();
|
||||
const { pathname } = useLocation();
|
||||
const pageViewMode = isBoard(pathname) ? DocMode.board : DocMode.doc;
|
||||
|
||||
const switchToPageView = (targetViewMode: DocMode) => {
|
||||
if (targetViewMode === pageViewMode) {
|
||||
return;
|
||||
}
|
||||
const workspaceId = params['workspace_id'];
|
||||
/**
|
||||
* There are two possible modes:
|
||||
* Page mode: /{workspaceId}/{pageId}
|
||||
* Board mode: /{workspaceId}/{pageId}/whiteboard
|
||||
*/
|
||||
const pageId = params['*'].split('/')[0];
|
||||
const targetUrl = `/${workspaceId}/${pageId}${
|
||||
targetViewMode === DocMode.board ? '/whiteboard' : ''
|
||||
}`;
|
||||
navigate(targetUrl);
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledContainerForSwitcher>
|
||||
<StatusText
|
||||
active={pageViewMode === DocMode.doc}
|
||||
onClick={() => switchToPageView(DocMode.doc)}
|
||||
>
|
||||
Doc
|
||||
</StatusText>
|
||||
<StatusTrack
|
||||
mode={pageViewMode}
|
||||
onClick={() => {
|
||||
switchToPageView(
|
||||
pageViewMode === DocMode.board
|
||||
? DocMode.doc
|
||||
: DocMode.board
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<StatusText
|
||||
active={pageViewMode === DocMode.board}
|
||||
onClick={() => switchToPageView(DocMode.board)}
|
||||
>
|
||||
Board
|
||||
</StatusText>
|
||||
</StyledContainerForSwitcher>
|
||||
);
|
||||
};
|
||||
|
||||
const StyledContainerForSwitcher = styled('div')({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
});
|
||||
@@ -0,0 +1 @@
|
||||
export { Switcher as EditorBoardSwitcher } from './Switcher';
|
||||
@@ -0,0 +1,6 @@
|
||||
export enum DocMode {
|
||||
// Page Mode
|
||||
doc,
|
||||
// Board Mode
|
||||
board,
|
||||
}
|
||||
@@ -0,0 +1,184 @@
|
||||
import { useNavigate, useParams, useLocation } from 'react-router-dom';
|
||||
import MenuIconBak from '@mui/icons-material/Menu';
|
||||
import {
|
||||
useUserAndSpaces,
|
||||
useShowSpaceSidebar,
|
||||
useShowSettingsSidebar,
|
||||
} from '@toeverything/datasource/state';
|
||||
import {
|
||||
MuiIconButton as IconButton,
|
||||
styled,
|
||||
Tooltip,
|
||||
useTheme,
|
||||
} from '@toeverything/components/ui';
|
||||
import { SideBarViewIcon } from '@toeverything/components/icons';
|
||||
import { LogoLink, ListIcon, SpaceIcon } from '@toeverything/components/common';
|
||||
import { PageHistoryPortal } from './PageHistoryPortal';
|
||||
import { PageSharePortal } from './PageSharePortal';
|
||||
import { PageSettingPortal } from './PageSettingPortal';
|
||||
import { CurrentPageTitle } from './Title';
|
||||
import { UserMenuIcon } from './user-menu-icon';
|
||||
import { useFlag } from '@toeverything/datasource/feature-flags';
|
||||
|
||||
function hideAffineHeader(pathname: string): boolean {
|
||||
return ['/', '/login', '/error/workspace', '/error/404', '/ui'].includes(
|
||||
pathname
|
||||
);
|
||||
}
|
||||
|
||||
type HeaderIconProps = {
|
||||
isWhiteboardView?: boolean;
|
||||
};
|
||||
|
||||
export const AffineHeader = () => {
|
||||
const navigate = useNavigate();
|
||||
const params = useParams();
|
||||
const { pathname } = useLocation();
|
||||
const { toggleSpaceSidebar, setSpaceSidebarVisible } =
|
||||
useShowSpaceSidebar();
|
||||
const { toggleSettingsSidebar: toggleInfoSidebar } =
|
||||
useShowSettingsSidebar();
|
||||
const theme = useTheme();
|
||||
const isWhiteboardView = pathname.endsWith('/whiteboard');
|
||||
const pageHistoryPortalFlag = useFlag('BooleanPageHistoryPortal', false);
|
||||
const pageSettingPortalFlag = useFlag('PageSettingPortal', false);
|
||||
const BooleanPageSharePortal = useFlag('BooleanPageSharePortal', false);
|
||||
// TODO: remove this
|
||||
useUserAndSpaces();
|
||||
|
||||
const showCenterTab =
|
||||
(params['workspace_id'] || pathname.includes('/space/')) && params['*'];
|
||||
|
||||
if (hideAffineHeader(pathname)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<StyledHeader style={{ zIndex: theme.affine.zIndex.header }}>
|
||||
<StyledHeaderLeft>
|
||||
<LogoLink />  
|
||||
{/* <HeaderIcon
|
||||
onClick={toggleSpaceSidebar}
|
||||
onMouseEnter={() => setSpaceSidebarVisible(true)}
|
||||
sx={{ mr: 4, ml: 4 }}
|
||||
>
|
||||
<MenuIconBak />
|
||||
</HeaderIcon> */}
|
||||
{pageHistoryPortalFlag && (
|
||||
<HeaderIcon sx={{ mr: 4 }}>
|
||||
<PageHistoryPortal />
|
||||
</HeaderIcon>
|
||||
)}
|
||||
<CurrentPageTitle />
|
||||
</StyledHeaderLeft>
|
||||
|
||||
<StyledHeaderCenter>
|
||||
{showCenterTab && (
|
||||
<>
|
||||
<Tooltip content="Doc">
|
||||
<HeaderIcon
|
||||
style={{ width: '80px' }}
|
||||
isWhiteboardView={!isWhiteboardView}
|
||||
onClick={() =>
|
||||
isWhiteboardView
|
||||
? navigate(
|
||||
`/${
|
||||
params['workspace_id'] ||
|
||||
'space'
|
||||
}/${params['*'].slice(0, -11)}`
|
||||
)
|
||||
: null
|
||||
}
|
||||
>
|
||||
<ListIcon />
|
||||
<span
|
||||
style={{
|
||||
fontSize: '16px',
|
||||
marginLeft: '7.5px',
|
||||
}}
|
||||
>
|
||||
Doc
|
||||
</span>
|
||||
</HeaderIcon>
|
||||
</Tooltip>
|
||||
<Tooltip content="Whiteboard">
|
||||
<HeaderIcon
|
||||
isWhiteboardView={isWhiteboardView}
|
||||
onClick={() =>
|
||||
isWhiteboardView
|
||||
? null
|
||||
: navigate(
|
||||
`/${
|
||||
params['workspace_id'] ||
|
||||
'space'
|
||||
}/${params['*']}` + '/whiteboard'
|
||||
)
|
||||
}
|
||||
>
|
||||
<SpaceIcon />
|
||||
</HeaderIcon>
|
||||
</Tooltip>
|
||||
</>
|
||||
)}
|
||||
</StyledHeaderCenter>
|
||||
<StyledHeaderRight>
|
||||
{BooleanPageSharePortal && (
|
||||
<HeaderIcon sx={{ mr: 4 }}>
|
||||
<PageSharePortal />
|
||||
</HeaderIcon>
|
||||
)}
|
||||
<UserMenuIcon />
|
||||
|
||||
{pageSettingPortalFlag && (
|
||||
<HeaderIcon>
|
||||
<PageSettingPortal />
|
||||
</HeaderIcon>
|
||||
)}
|
||||
|
||||
<HeaderIcon sx={{ mr: 4 }} onClick={toggleInfoSidebar}>
|
||||
<SideBarViewIcon />
|
||||
</HeaderIcon>
|
||||
</StyledHeaderRight>
|
||||
</StyledHeader>
|
||||
);
|
||||
};
|
||||
|
||||
const StyledHeader = styled('div')`
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
height: 64px;
|
||||
padding: 0 32px;
|
||||
background-color: #fff;
|
||||
`;
|
||||
|
||||
const StyledHeaderLeft = styled('div')`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
`;
|
||||
|
||||
const StyledHeaderCenter = styled('div')`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
`;
|
||||
const StyledHeaderRight = styled('div')`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
`;
|
||||
|
||||
const HeaderIcon = styled(IconButton, {
|
||||
shouldForwardProp: (prop: string) => prop !== 'isWhiteboardView',
|
||||
})<HeaderIconProps>(({ isWhiteboardView = false }) => ({
|
||||
color: '#98ACBD',
|
||||
minWidth: 48,
|
||||
width: 48,
|
||||
height: 36,
|
||||
borderRadius: '8px',
|
||||
...(isWhiteboardView && {
|
||||
color: '#fff',
|
||||
backgroundColor: '#3E6FDB',
|
||||
'&:hover': {
|
||||
backgroundColor: '#3E6FDB',
|
||||
},
|
||||
}),
|
||||
}));
|
||||
@@ -0,0 +1,81 @@
|
||||
import { IconButton, styled } from '@toeverything/components/ui';
|
||||
import { LogoIcon, SideBarViewIcon } from '@toeverything/components/icons';
|
||||
import { useShowSettingsSidebar } from '@toeverything/datasource/state';
|
||||
import { CurrentPageTitle } from './Title';
|
||||
import { EditorBoardSwitcher } from './EditorBoardSwitcher';
|
||||
|
||||
export const LayoutHeader = () => {
|
||||
const { toggleSettingsSidebar: toggleInfoSidebar } =
|
||||
useShowSettingsSidebar();
|
||||
|
||||
return (
|
||||
<StyledContainerForHeaderRoot>
|
||||
<StyledHeaderRoot>
|
||||
<FlexContainer>
|
||||
<StyledLogoIcon />
|
||||
<TitleContainer>
|
||||
<CurrentPageTitle />
|
||||
</TitleContainer>
|
||||
</FlexContainer>
|
||||
<FlexContainer>
|
||||
<IconButton onClick={toggleInfoSidebar}>
|
||||
<SideBarViewIcon />
|
||||
</IconButton>
|
||||
</FlexContainer>
|
||||
<StyledContainerForEditorBoardSwitcher>
|
||||
<EditorBoardSwitcher />
|
||||
</StyledContainerForEditorBoardSwitcher>
|
||||
</StyledHeaderRoot>
|
||||
</StyledContainerForHeaderRoot>
|
||||
);
|
||||
};
|
||||
|
||||
const StyledContainerForHeaderRoot = styled('div')(({ theme }) => {
|
||||
return {
|
||||
width: '100%',
|
||||
zIndex: theme.affine.zIndex.header,
|
||||
};
|
||||
});
|
||||
|
||||
const StyledHeaderRoot = styled('div')(({ theme }) => {
|
||||
return {
|
||||
position: 'relative',
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
height: 60,
|
||||
marginLeft: 36,
|
||||
marginRight: 36,
|
||||
};
|
||||
});
|
||||
|
||||
const FlexContainer = styled('div')({ display: 'flex' });
|
||||
|
||||
const TitleContainer = styled('div')(({ theme }) => {
|
||||
return {
|
||||
// marginLeft: theme.affine.spacing.lgSpacing,
|
||||
marginLeft: 100,
|
||||
maxWidth: 500,
|
||||
overflowX: 'hidden',
|
||||
color: theme.affine.palette.primaryText,
|
||||
lineHeight: '18px',
|
||||
fontSize: '15px',
|
||||
fontStyle: 'normal',
|
||||
fontWeight: '400',
|
||||
letterSpacing: '1.06px',
|
||||
};
|
||||
});
|
||||
|
||||
const StyledLogoIcon = styled(LogoIcon)(({ theme }) => {
|
||||
return {
|
||||
color: theme.affine.palette.primary,
|
||||
cursor: 'pointer',
|
||||
};
|
||||
});
|
||||
|
||||
const StyledContainerForEditorBoardSwitcher = styled('div')(({ theme }) => {
|
||||
return {
|
||||
position: 'absolute',
|
||||
left: '50%',
|
||||
};
|
||||
});
|
||||
@@ -0,0 +1,6 @@
|
||||
import { ClockIcon } from '@toeverything/components/common';
|
||||
function PageHistoryPortal() {
|
||||
return <ClockIcon />;
|
||||
}
|
||||
|
||||
export { PageHistoryPortal };
|
||||
@@ -0,0 +1,267 @@
|
||||
import { useState, MouseEvent, useCallback, useEffect } from 'react';
|
||||
import { services } from '@toeverything/datasource/db-service';
|
||||
import { useParams, useNavigate } from 'react-router-dom';
|
||||
|
||||
import { copyToClipboard } from '@toeverything/utils';
|
||||
import { MoreIcon } from '@toeverything/components/icons';
|
||||
import {
|
||||
MuiSnackbar as Snackbar,
|
||||
Popover,
|
||||
ListButton,
|
||||
MuiDivider as Divider,
|
||||
MuiSwitch as Switch,
|
||||
styled,
|
||||
} from '@toeverything/components/ui';
|
||||
import { useUserAndSpaces } from '@toeverything/datasource/state';
|
||||
import format from 'date-fns/format';
|
||||
import { useFlag } from '@toeverything/datasource/feature-flags';
|
||||
import { PageBlock } from './types';
|
||||
import { FileExporter } from './file-exporter/file-exporter';
|
||||
const PageSettingPortalContainer = styled('div')({
|
||||
width: '320p',
|
||||
padding: '15px',
|
||||
'.textDescription': {
|
||||
height: '22px',
|
||||
lineHeight: '22px',
|
||||
marginLeft: '30px',
|
||||
fontSize: '14px',
|
||||
color: '#ccc',
|
||||
p: {
|
||||
margin: 0,
|
||||
},
|
||||
},
|
||||
'.switchDescription': {
|
||||
color: '#ccc',
|
||||
fontSize: '14px',
|
||||
paddingLeft: '30px',
|
||||
},
|
||||
});
|
||||
|
||||
const BtnPageSettingContainer = styled('div')({
|
||||
width: '24px',
|
||||
height: '24px',
|
||||
});
|
||||
|
||||
const MESSAGES = {
|
||||
COPY_LINK: ' Copy Link',
|
||||
INVITE: 'Add people,emails, or groups',
|
||||
COPY_LINK_SUCCESS: 'Copyed link to clipboard',
|
||||
};
|
||||
|
||||
function PageSettingPortal() {
|
||||
const [alertOpen, setAlertOpen] = useState(false);
|
||||
const { workspace_id } = useParams();
|
||||
const [pageBlock, setPageBlock] = useState<PageBlock>();
|
||||
|
||||
const params = useParams();
|
||||
const pageId = params['*'].split('/')[0];
|
||||
const navigate = useNavigate();
|
||||
const { user } = useUserAndSpaces();
|
||||
const BooleanFullWidthChecked = useFlag('BooleanFullWidthChecked', false);
|
||||
const BooleanExportWorkspace = useFlag('BooleanExportWorkspace', false);
|
||||
const BooleanImportWorkspace = useFlag('BooleanImportWorkspace', false);
|
||||
const BooleanExportHtml = useFlag('BooleanExportHtml', false);
|
||||
const BooleanExportPdf = useFlag('BooleanExportPdf', false);
|
||||
const BooleanExportMarkdown = useFlag('BooleanExportMarkdown', false);
|
||||
|
||||
const fetchPageBlock = useCallback(async () => {
|
||||
const dbPageBlock = await services.api.editorBlock.getBlock(
|
||||
workspace_id,
|
||||
pageId
|
||||
);
|
||||
if (!dbPageBlock) return;
|
||||
const text = dbPageBlock.getDecoration('text');
|
||||
setPageBlock({
|
||||
lastUpdated: dbPageBlock.lastUpdated,
|
||||
fullWidthChecked:
|
||||
dbPageBlock.getDecoration('fullWidthChecked') || false,
|
||||
title:
|
||||
text &&
|
||||
//@ts-ignore
|
||||
text.value[0].text,
|
||||
});
|
||||
}, [workspace_id, pageId]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchPageBlock();
|
||||
}, [workspace_id, pageId, fetchPageBlock]);
|
||||
const redirectToPage = (newWorkspaceId: string, newPageId: string) => {
|
||||
navigate('/' + newWorkspaceId + '/' + newPageId);
|
||||
};
|
||||
|
||||
const handleDuplicatePage = async () => {
|
||||
//create page
|
||||
const newPage = await services.api.editorBlock.create({
|
||||
workspace: workspace_id,
|
||||
type: 'page' as const,
|
||||
});
|
||||
//add page to tree
|
||||
await services.api.pageTree.addNextPageToWorkspace(
|
||||
workspace_id,
|
||||
pageId,
|
||||
newPage.id
|
||||
);
|
||||
//copy source page to new page
|
||||
await services.api.editorBlock.copyPage(
|
||||
workspace_id,
|
||||
pageId,
|
||||
newPage.id
|
||||
);
|
||||
|
||||
redirectToPage(workspace_id, newPage.id);
|
||||
};
|
||||
|
||||
const handleCopy = () => {
|
||||
copyToClipboard(window.location.href);
|
||||
setAlertOpen(true);
|
||||
};
|
||||
const handleAlertClose = () => {
|
||||
setAlertOpen(false);
|
||||
};
|
||||
|
||||
const handleExportWorkspace = () => {
|
||||
//@ts-ignore
|
||||
window.client.inspector().save();
|
||||
};
|
||||
|
||||
const handleImportWorkspace = () => {
|
||||
//@ts-ignore
|
||||
window.client
|
||||
.inspector()
|
||||
.load()
|
||||
.then(() => {
|
||||
window.location.href = `/${workspace_id}/`;
|
||||
});
|
||||
};
|
||||
|
||||
const handleFullWidthCheckedChange = async (
|
||||
event: React.ChangeEvent<HTMLInputElement>
|
||||
) => {
|
||||
const checked = event.target.checked;
|
||||
setPageBlock({
|
||||
lastUpdated: pageBlock.lastUpdated,
|
||||
fullWidthChecked: checked,
|
||||
});
|
||||
services.api.editorBlock.update({
|
||||
properties: {
|
||||
fullWidthChecked: checked,
|
||||
},
|
||||
id: pageId,
|
||||
workspace: workspace_id,
|
||||
});
|
||||
};
|
||||
|
||||
const handleExportHtml = async () => {
|
||||
//@ts-ignore
|
||||
const htmlContent = await virgo.clipboard
|
||||
.getClipboardParse()
|
||||
.page2html();
|
||||
const htmlTitle = pageBlock.title;
|
||||
|
||||
FileExporter.exportHtml(htmlTitle, htmlContent);
|
||||
};
|
||||
|
||||
const handleExportMarkdown = async () => {
|
||||
//@ts-ignore
|
||||
const htmlContent = await virgo.clipboard
|
||||
.getClipboardParse()
|
||||
.page2html();
|
||||
const htmlTitle = pageBlock.title;
|
||||
FileExporter.exportMarkdown(htmlTitle, htmlContent);
|
||||
};
|
||||
return (
|
||||
<BtnPageSettingContainer>
|
||||
<Popover
|
||||
trigger="click"
|
||||
placement="bottom-end"
|
||||
content={
|
||||
<PageSettingPortalContainer>
|
||||
{BooleanFullWidthChecked && (
|
||||
<>
|
||||
<div className="switchDescription">
|
||||
Full width
|
||||
<Switch
|
||||
checked={
|
||||
pageBlock &&
|
||||
pageBlock.fullWidthChecked
|
||||
}
|
||||
onChange={handleFullWidthCheckedChange}
|
||||
disabled={true}
|
||||
/>
|
||||
</div>
|
||||
<Divider />
|
||||
</>
|
||||
)}
|
||||
|
||||
<ListButton
|
||||
content="Duplicate Page"
|
||||
onClick={handleDuplicatePage}
|
||||
/>
|
||||
<ListButton
|
||||
content={MESSAGES.COPY_LINK}
|
||||
onClick={handleCopy}
|
||||
/>
|
||||
<Divider />
|
||||
{BooleanExportMarkdown && (
|
||||
<ListButton
|
||||
content="Export As Markdown"
|
||||
onClick={handleExportMarkdown}
|
||||
/>
|
||||
)}
|
||||
{BooleanExportHtml && (
|
||||
<ListButton
|
||||
content="Export As HTML"
|
||||
onClick={handleExportHtml}
|
||||
/>
|
||||
)}
|
||||
{BooleanExportPdf && (
|
||||
<ListButton
|
||||
content="Export As PDF"
|
||||
onClick={handleCopy}
|
||||
/>
|
||||
)}
|
||||
<Divider />
|
||||
{BooleanImportWorkspace && (
|
||||
<ListButton
|
||||
content="Import Workspace"
|
||||
onClick={handleImportWorkspace}
|
||||
/>
|
||||
)}
|
||||
{BooleanExportWorkspace && (
|
||||
<ListButton
|
||||
content="Export Workspace"
|
||||
onClick={handleExportWorkspace}
|
||||
/>
|
||||
)}
|
||||
|
||||
<p className="textDescription">
|
||||
Last edited by {user && user.nickname}
|
||||
<br />
|
||||
{pageBlock &&
|
||||
`${format(
|
||||
new Date(pageBlock.lastUpdated),
|
||||
'MM/dd/yyyy hh:mm'
|
||||
)}`}
|
||||
</p>
|
||||
|
||||
<Snackbar
|
||||
anchorOrigin={{
|
||||
vertical: 'bottom',
|
||||
horizontal: 'center',
|
||||
}}
|
||||
open={alertOpen}
|
||||
message={MESSAGES.COPY_LINK_SUCCESS}
|
||||
key={'bottomcenter'}
|
||||
autoHideDuration={2000}
|
||||
onClose={handleAlertClose}
|
||||
/>
|
||||
</PageSettingPortalContainer>
|
||||
}
|
||||
>
|
||||
<MoreIcon />
|
||||
</Popover>
|
||||
</BtnPageSettingContainer>
|
||||
);
|
||||
}
|
||||
|
||||
export { PageSettingPortal };
|
||||
@@ -0,0 +1,146 @@
|
||||
import style9 from 'style9';
|
||||
import LanguageIcon from '@mui/icons-material/Language';
|
||||
import { useState, MouseEvent } from 'react';
|
||||
import InsertLinkIcon from '@mui/icons-material/InsertLink';
|
||||
import ShareIcon from '@mui/icons-material/Share';
|
||||
import {
|
||||
MuiSnackbar as Snackbar,
|
||||
MuiButton as Button,
|
||||
MuiDivider as Divider,
|
||||
MuiInputBase as InputBase,
|
||||
MuiPaper as Paper,
|
||||
MuiSwitch as Switch,
|
||||
Popover,
|
||||
} from '@toeverything/components/ui';
|
||||
import { copyToClipboard } from '@toeverything/utils';
|
||||
|
||||
const styles = style9.create({
|
||||
pageShareBox: {
|
||||
width: '500px',
|
||||
padding: '30px 20px 50px 20px',
|
||||
},
|
||||
btnPageShare: {
|
||||
width: '24px',
|
||||
height: '24px',
|
||||
},
|
||||
pageShareBoxIcon: {
|
||||
position: 'absolute',
|
||||
left: '5px',
|
||||
top: '10px',
|
||||
color: 'var(--color-gray-400)',
|
||||
},
|
||||
pageShareBoxForWeb: {
|
||||
position: 'relative',
|
||||
paddingLeft: '40px',
|
||||
borderBottom: '1px solid var(--color-gray-400))',
|
||||
marginBottom: '15px',
|
||||
},
|
||||
pageShareBoxSwitch: {
|
||||
position: 'absolute',
|
||||
right: '5px',
|
||||
top: '10px',
|
||||
},
|
||||
pageShareBoxForWebP1: {
|
||||
fontWeight: 'bold',
|
||||
marginBottom: '5px',
|
||||
},
|
||||
pageShareBoxForWebP2: {
|
||||
color: 'var(--color-gray-400)',
|
||||
},
|
||||
copyLinkBtn: {
|
||||
marginTop: '15px',
|
||||
float: 'right',
|
||||
cursor: 'pointer',
|
||||
},
|
||||
copyLinkcon: {
|
||||
verticalAlign: 'middle',
|
||||
},
|
||||
});
|
||||
const MESSAGES = {
|
||||
COPY_LINK: ' Copy Link',
|
||||
INVITE: 'Add people,emails, or groups',
|
||||
SHARE_TO_WEB: 'Share to web',
|
||||
SHARE_TO_ANYONE: 'Publish and share link with any one',
|
||||
COPY_LINK_SUCCESS: 'Copyed link to clipboard',
|
||||
};
|
||||
function PageSharePortal() {
|
||||
const [alertOpen, setAlertOpen] = useState(false);
|
||||
const handleCopy = () => {
|
||||
copyToClipboard(window.location.href);
|
||||
setAlertOpen(true);
|
||||
};
|
||||
const handleAlertClose = () => {
|
||||
setAlertOpen(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Popover
|
||||
placement="bottom-start"
|
||||
trigger="click"
|
||||
content={
|
||||
<div className={styles('pageShareBox')}>
|
||||
<div className={styles('pageShareBoxForWeb')}>
|
||||
<LanguageIcon
|
||||
className={styles('pageShareBoxIcon')}
|
||||
/>
|
||||
<p className={styles('pageShareBoxForWebP1')}>
|
||||
{MESSAGES.SHARE_TO_WEB}
|
||||
</p>
|
||||
<p className={styles('pageShareBoxForWebP2')}>
|
||||
{MESSAGES.SHARE_TO_ANYONE}
|
||||
</p>
|
||||
<div className={styles('pageShareBoxSwitch')}>
|
||||
<Switch />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Paper
|
||||
component="form"
|
||||
sx={{
|
||||
p: '0px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
width: 460,
|
||||
}}
|
||||
>
|
||||
<InputBase
|
||||
sx={{ ml: 1, flex: 1 }}
|
||||
placeholder={MESSAGES.INVITE}
|
||||
/>
|
||||
<Button variant="contained">Invite</Button>
|
||||
</Paper>
|
||||
</div>
|
||||
<div>
|
||||
<a
|
||||
className={styles('copyLinkBtn')}
|
||||
onClick={handleCopy}
|
||||
>
|
||||
<InsertLinkIcon
|
||||
className={styles('copyLinkcon')}
|
||||
/>
|
||||
{MESSAGES.COPY_LINK}
|
||||
</a>
|
||||
</div>
|
||||
<Snackbar
|
||||
anchorOrigin={{
|
||||
vertical: 'bottom',
|
||||
horizontal: 'center',
|
||||
}}
|
||||
open={alertOpen}
|
||||
message={MESSAGES.COPY_LINK_SUCCESS}
|
||||
key={'bottomcenter'}
|
||||
autoHideDuration={2000}
|
||||
onClose={handleAlertClose}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<ShareIcon style={{ marginTop: '8px' }} />
|
||||
</Popover>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { PageSharePortal };
|
||||
@@ -0,0 +1,80 @@
|
||||
import { useState } from 'react';
|
||||
import { NavLink } from 'react-router-dom';
|
||||
|
||||
import {
|
||||
MuiButton as Button,
|
||||
MuiMenu as Menu,
|
||||
MuiMenuItem as MenuItem,
|
||||
} from '@toeverything/components/ui';
|
||||
|
||||
export function TempLinkRouter() {
|
||||
const [anchorEl, setAnchorEl] = useState(null);
|
||||
const open = Boolean(anchorEl);
|
||||
|
||||
const handleClose = () => {
|
||||
setAnchorEl(null);
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Button onClick={e => setAnchorEl(e.currentTarget)} sx={{ mr: 4 }}>
|
||||
Debug Routers
|
||||
</Button>
|
||||
<Menu anchorEl={anchorEl} open={open} onClose={handleClose}>
|
||||
<MenuItem onClick={handleClose}>
|
||||
<NavLink to="/agenda/calendar">/agenda/calendar</NavLink>
|
||||
</MenuItem>
|
||||
<MenuItem onClick={handleClose}>
|
||||
<NavLink to="/agenda/tasks">/agenda/tasks</NavLink>
|
||||
</MenuItem>
|
||||
<MenuItem onClick={handleClose}>
|
||||
<NavLink to="/agenda/today">/agenda/today</NavLink>
|
||||
</MenuItem>
|
||||
<MenuItem onClick={handleClose}>
|
||||
<NavLink to="/agenda/">/agenda/</NavLink>
|
||||
</MenuItem>
|
||||
<MenuItem>
|
||||
<NavLink to="/workspaceId/labels">
|
||||
/workspaceId/labels
|
||||
</NavLink>
|
||||
</MenuItem>
|
||||
<MenuItem>
|
||||
<NavLink to="/workspaceId/pages">
|
||||
/workspaceId/pages
|
||||
</NavLink>
|
||||
</MenuItem>
|
||||
<MenuItem>
|
||||
<NavLink to="/workspaceId/docId">
|
||||
/workspaceId/docId
|
||||
</NavLink>
|
||||
</MenuItem>
|
||||
<MenuItem>
|
||||
<NavLink to="/workspaceId/">/workspaceId/</NavLink>
|
||||
</MenuItem>
|
||||
<MenuItem>
|
||||
<NavLink to="/login">/login</NavLink>
|
||||
</MenuItem>
|
||||
<MenuItem>
|
||||
<NavLink to="/recent">/recent</NavLink>
|
||||
</MenuItem>
|
||||
<MenuItem>
|
||||
<NavLink to="/search">/search</NavLink>
|
||||
</MenuItem>
|
||||
<MenuItem>
|
||||
<NavLink to="/settings">/settings</NavLink>
|
||||
</MenuItem>
|
||||
<MenuItem>
|
||||
<NavLink to="/shared">/shared</NavLink>
|
||||
</MenuItem>
|
||||
<MenuItem>
|
||||
<NavLink to="/started">/started</NavLink>
|
||||
</MenuItem>
|
||||
<MenuItem>
|
||||
<NavLink to="/">/</NavLink>
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default TempLinkRouter;
|
||||
@@ -0,0 +1,85 @@
|
||||
import { useEffect, useState, useCallback } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { Typography, styled } from '@toeverything/components/ui';
|
||||
|
||||
import { services } from '@toeverything/datasource/db-service';
|
||||
|
||||
/* card.7: Demand changes, temporarily closed, see https://github.com/toeverything/AFFiNE/issues/522 */
|
||||
// import { usePageTree} from '@toeverything/components/layout';
|
||||
// import { pickPath } from './utils';
|
||||
|
||||
export const CurrentPageTitle = () => {
|
||||
const params = useParams();
|
||||
const { workspace_id } = params;
|
||||
const [pageId, setPageId] = useState<string>('');
|
||||
const [pageTitle, setPageTitle] = useState<string | undefined>();
|
||||
|
||||
useEffect(() => {
|
||||
if (params['*']) {
|
||||
setPageId(params['*'].split('/')[0]);
|
||||
}
|
||||
}, [params]);
|
||||
|
||||
const fetchPageTitle = useCallback(async () => {
|
||||
if (!workspace_id || !pageId) return;
|
||||
const [pageEditorBlock] = await services.api.editorBlock.get({
|
||||
workspace: workspace_id,
|
||||
ids: [pageId],
|
||||
});
|
||||
/* card.7 */
|
||||
/* If the id is unique, only one path will be matched */
|
||||
// const routes = pickPath(items, pageId).filter(item => item.length)?.[0];
|
||||
|
||||
setPageTitle(
|
||||
pageEditorBlock?.properties?.text?.value
|
||||
?.map(v => v.text)
|
||||
.join('') ?? 'Untitled'
|
||||
);
|
||||
}, [pageId, workspace_id]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchPageTitle();
|
||||
}, [fetchPageTitle]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!workspace_id || !pageId || pageTitle === undefined)
|
||||
return () => {};
|
||||
|
||||
let unobserve: () => void;
|
||||
const auto_update_title = async () => {
|
||||
// console.log(';; title registration auto update');
|
||||
unobserve = await services.api.editorBlock.observe(
|
||||
{ workspace: workspace_id, id: pageId },
|
||||
businessBlock => {
|
||||
// console.log(';; auto_update_title', businessBlock);
|
||||
fetchPageTitle();
|
||||
}
|
||||
);
|
||||
};
|
||||
auto_update_title();
|
||||
|
||||
return () => {
|
||||
// unobserve?.();
|
||||
};
|
||||
}, [fetchPageTitle, pageId, pageTitle, workspace_id]);
|
||||
|
||||
useEffect(() => {
|
||||
document.title = pageTitle || '';
|
||||
}, [pageTitle]);
|
||||
|
||||
return pageTitle ? (
|
||||
<ContentText type="sm" title={pageTitle}>
|
||||
{pageTitle}
|
||||
</ContentText>
|
||||
) : null;
|
||||
};
|
||||
|
||||
const ContentText = styled(Typography)(({ theme }) => {
|
||||
return {
|
||||
color: theme.affine.palette.primaryText,
|
||||
maxWidth: '240px',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
};
|
||||
});
|
||||
@@ -0,0 +1,59 @@
|
||||
import { PageBlock } from '../types';
|
||||
import TurndownService from 'turndown';
|
||||
const FileExporter = {
|
||||
exportFile: (filename: string, text: string, format: string) => {
|
||||
const element = document.createElement('a');
|
||||
element.setAttribute(
|
||||
'href',
|
||||
'data:' + format + ';charset=utf-8,' + encodeURIComponent(text)
|
||||
);
|
||||
element.setAttribute('download', filename);
|
||||
|
||||
element.style.display = 'none';
|
||||
document.body.appendChild(element);
|
||||
|
||||
element.click();
|
||||
|
||||
document.body.removeChild(element);
|
||||
},
|
||||
decorateHtml: (pageTitle: string, htmlContent: string) => {
|
||||
return `<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>${pageTitle}</title>
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/normalize/8.0.1/normalize.min.css">
|
||||
</head>
|
||||
<body>
|
||||
<div style="margin:20px auto;width:800px" >
|
||||
<div style="background-color: #fff;box-shadow: 0px 0px 5px #ccc;padding: 10px;">
|
||||
${htmlContent}
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>`;
|
||||
},
|
||||
decoreateAffineBrand: (pageTitle: string) => {
|
||||
return pageTitle + ` Created in Affine`;
|
||||
},
|
||||
exportHtml: (pageTitle: string, htmlContent: string) => {
|
||||
FileExporter.exportFile(
|
||||
FileExporter.decoreateAffineBrand(pageTitle) + '.html',
|
||||
FileExporter.decorateHtml(pageTitle, htmlContent),
|
||||
'text/html'
|
||||
);
|
||||
},
|
||||
|
||||
exportMarkdown: (pageTitle: string, htmlContent: string) => {
|
||||
const turndownService = new TurndownService();
|
||||
const markdown = turndownService.turndown(htmlContent);
|
||||
|
||||
FileExporter.exportFile(
|
||||
FileExporter.decoreateAffineBrand(pageTitle) + '.md',
|
||||
markdown,
|
||||
'text/plain'
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export { FileExporter };
|
||||
@@ -0,0 +1,2 @@
|
||||
export { AffineHeader } from './Header';
|
||||
export { LayoutHeader } from './LayoutHeader';
|
||||
@@ -0,0 +1,7 @@
|
||||
interface PageBlock {
|
||||
lastUpdated?: number;
|
||||
fullWidthChecked?: boolean;
|
||||
title?: string;
|
||||
}
|
||||
|
||||
export { PageBlock };
|
||||
@@ -0,0 +1,13 @@
|
||||
import { useState } from 'react';
|
||||
|
||||
type PageViewStatusType = 'editor' | 'board';
|
||||
|
||||
export const useLayoutHeader = () => {
|
||||
const [pageViewStatus, setPageViewStatus] =
|
||||
useState<PageViewStatusType>('editor');
|
||||
|
||||
return {
|
||||
pageViewStatus,
|
||||
setPageViewStatus,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,34 @@
|
||||
import { MuiAvatar as Avatar, Popover } from '@toeverything/components/ui';
|
||||
import { useUserAndSpaces } from '@toeverything/datasource/state';
|
||||
import { getUserDisplayName } from '@toeverything/utils';
|
||||
import { UserMenuList } from './UserMenuList';
|
||||
|
||||
/**
|
||||
* An icon is displayed by default, click the icon to display the popover, and the content of the popover is children.
|
||||
*/
|
||||
export const UserMenuIcon = () => {
|
||||
const { user } = useUserAndSpaces();
|
||||
|
||||
return (
|
||||
<Popover
|
||||
content={<UserMenuList />}
|
||||
placement="bottom-end"
|
||||
trigger="click"
|
||||
>
|
||||
<Avatar
|
||||
// onClick={handleClick}
|
||||
sx={{
|
||||
width: 26,
|
||||
height: 26,
|
||||
mr: 4,
|
||||
cursor: 'pointer',
|
||||
fontSize: '1rem',
|
||||
}}
|
||||
alt={getUserDisplayName(user)}
|
||||
src={user?.photo || ''}
|
||||
/>
|
||||
</Popover>
|
||||
);
|
||||
};
|
||||
|
||||
export default UserMenuIcon;
|
||||
@@ -0,0 +1,66 @@
|
||||
import { useMemo } from 'react';
|
||||
import { getAuth, signOut } from 'firebase/auth';
|
||||
|
||||
import { LOGOUT_COOKIES, LOGOUT_LOCAL_STORAGE } from '@toeverything/utils';
|
||||
import { useUserAndSpaces } from '@toeverything/datasource/state';
|
||||
import {
|
||||
MuiDivider as Divider,
|
||||
MuiList as List,
|
||||
MuiListItem as ListItem,
|
||||
MuiListItemText as ListItemText,
|
||||
} from '@toeverything/components/ui';
|
||||
|
||||
export const UserMenuList = () => {
|
||||
const { user } = useUserAndSpaces();
|
||||
|
||||
const user_menu_data = useMemo(() => {
|
||||
return [
|
||||
// {
|
||||
// text: 'Settings',
|
||||
// onClick: () => {
|
||||
// console.log('Open the settings panel');
|
||||
// },
|
||||
// },
|
||||
{
|
||||
text: user?.email || 'Unknown User',
|
||||
showDivider: true,
|
||||
},
|
||||
{
|
||||
text: 'Logout',
|
||||
onClick: async () => {
|
||||
LOGOUT_LOCAL_STORAGE.forEach(name =>
|
||||
localStorage.removeItem(name)
|
||||
);
|
||||
// localStorage.clear();
|
||||
document.cookie = LOGOUT_COOKIES.map(
|
||||
name =>
|
||||
name + '=; expires=Thu, 01 Jan 1970 00:00:01 GMT;'
|
||||
).join(' ');
|
||||
|
||||
signOut(getAuth());
|
||||
|
||||
window.location.href = '/';
|
||||
},
|
||||
},
|
||||
];
|
||||
}, [user?.email]);
|
||||
|
||||
return (
|
||||
<List component="nav" aria-label="user settings">
|
||||
{user_menu_data.map(menu => {
|
||||
const { text, onClick, showDivider } = menu;
|
||||
|
||||
return (
|
||||
<>
|
||||
<ListItem button onClick={onClick} key={text}>
|
||||
<ListItemText primary={text} />
|
||||
</ListItem>
|
||||
{showDivider && <Divider />}
|
||||
</>
|
||||
);
|
||||
})}
|
||||
</List>
|
||||
);
|
||||
};
|
||||
|
||||
export default UserMenuList;
|
||||
@@ -0,0 +1,3 @@
|
||||
import { UserMenuIcon } from './UserMenuIcon';
|
||||
export { UserMenuIcon };
|
||||
export default UserMenuIcon;
|
||||
Reference in New Issue
Block a user