init: the first public commit for AFFiNE

This commit is contained in:
DarkSky
2022-07-22 15:49:21 +08:00
commit e3e3741393
1451 changed files with 108124 additions and 0 deletions
@@ -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 /> &emsp;
{/* <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;