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

View File

@@ -0,0 +1,112 @@
import { useCallback, useState } from 'react';
import { useNavigate, useParams } from 'react-router-dom';
import clsx from 'clsx';
import style9 from 'style9';
import {
MuiBox as Box,
MuiButton as Button,
MuiCollapse as Collapse,
MuiIconButton as IconButton,
} from '@toeverything/components/ui';
import ArrowDropDownIcon from '@mui/icons-material/ArrowDropDown';
import ArrowRightIcon from '@mui/icons-material/ArrowRight';
import { services } from '@toeverything/datasource/db-service';
import { NewpageIcon } from '@toeverything/components/common';
import {
usePageTree,
useCalendarHeatmap,
} from '@toeverything/components/layout';
const styles = style9.create({
ligoButton: {
textTransform: 'none',
},
newPage: {
color: '#B6C7D3',
width: '26px',
fontSize: '18px',
textAlign: 'center',
cursor: 'pointer',
},
});
export type CollapsiblePageTreeProps = {
title?: string;
initialOpen?: boolean;
children?: React.ReactNode;
className?: string;
style?: React.CSSProperties;
};
export function CollapsiblePageTree(props: CollapsiblePageTreeProps) {
const { className, style, children, title, initialOpen = true } = props;
const navigate = useNavigate();
const { workspace_id, page_id } = useParams();
const { handleAddPage } = usePageTree();
const { addPageToday } = useCalendarHeatmap();
const [open, setOpen] = useState(initialOpen);
const create_page = useCallback(async () => {
if (page_id) {
const newPage = await services.api.editorBlock.create({
workspace: workspace_id,
type: 'page' as const,
});
await handleAddPage(newPage.id);
addPageToday();
navigate(`/${workspace_id}/${newPage.id}`);
}
}, [addPageToday, handleAddPage, navigate, page_id, workspace_id]);
const [newPageBtnVisible, setNewPageBtnVisible] = useState<boolean>(false);
return (
<>
<Box
sx={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
paddingRight: 1,
}}
onMouseEnter={() => setNewPageBtnVisible(true)}
onMouseLeave={() => setNewPageBtnVisible(false)}
>
<Button
startIcon={
open ? <ArrowDropDownIcon /> : <ArrowRightIcon />
}
onClick={() => setOpen(prev => !prev)}
sx={{ color: '#566B7D', textTransform: 'none' }}
className={clsx(styles('ligoButton'), className)}
style={style}
disableElevation
disableRipple
>
{title}
</Button>
{newPageBtnVisible && (
<div
onClick={create_page}
className={clsx(styles('newPage'), className)}
>
+
</div>
)}
</Box>
{children ? (
<Collapse in={open} timeout="auto" unmountOnExit>
{children}
</Collapse>
) : null}
</>
);
}
export default CollapsiblePageTree;

View File

@@ -0,0 +1,18 @@
/* eslint-disable filename-rules/match */
import { render } from '@testing-library/react';
import { Page } from './index';
describe('App', () => {
it('should render successfully', () => {
const { baseElement } = render(<Page workspace="default" />);
expect(baseElement).toBeTruthy();
});
it('should have a greeting as the title', () => {
const { getByText } = render(<Page workspace="default" />);
expect(getByText(/Welcome ligo-virgo/gi)).toBeTruthy();
});
});

View File

@@ -0,0 +1,176 @@
/* eslint-disable filename-rules/match */
import { useEffect } from 'react';
import { useParams } from 'react-router';
import {
MuiBox as Box,
MuiCircularProgress as CircularProgress,
MuiDivider as Divider,
styled,
} from '@toeverything/components/ui';
import { AffineEditor } from '@toeverything/components/affine-editor';
import {
CalendarHeatmap,
PageTree,
Activities,
} from '@toeverything/components/layout';
import { CollapsibleTitle } from '@toeverything/components/common';
import {
useShowSpaceSidebar,
useUserAndSpaces,
} from '@toeverything/datasource/state';
import { services } from '@toeverything/datasource/db-service';
import { WorkspaceName } from './workspace-name';
import { CollapsiblePageTree } from './collapsible-page-tree';
import TemplatesPortal from './templates-portal';
import { useFlag } from '@toeverything/datasource/feature-flags';
type PageProps = {
workspace: string;
};
export function Page(props: PageProps) {
const { page_id } = useParams();
const { showSpaceSidebar, fixedDisplay, setSpaceSidebarVisible } =
useShowSpaceSidebar();
const { user } = useUserAndSpaces();
const templatesPortalFlag = useFlag('BooleanTemplatesPortal', false);
const dailyNotesFlag = useFlag('BooleanDailyNotes', false);
useEffect(() => {
if (!user?.id || !page_id) return;
const updateRecentPages = async () => {
// TODO: deal with it temporarily
await services.api.editorBlock.getWorkspaceDbBlock(
props.workspace,
{
userId: user.id,
}
);
await services.api.userConfig.addRecentPage(
props.workspace,
user.id,
page_id
);
await services.api.editorBlock.clearUndoRedo(props.workspace);
};
update_recent_pages();
}, [user, props.workspace, page_id]);
return (
<LigoApp>
<LigoLeftContainer style={{ width: fixedDisplay ? '300px' : 0 }}>
<WorkspaceSidebar
style={{
opacity: !showSpaceSidebar && !fixedDisplay ? 0 : 1,
transform:
!showSpaceSidebar && !fixedDisplay
? 'translateX(-270px)'
: 'translateX(0px)',
}}
onMouseEnter={() => setSpaceSidebarVisible(true)}
onMouseLeave={() => setSpaceSidebarVisible(false)}
>
<WorkspaceName />
<Divider light={true} sx={{ my: 1, margin: '6px 0px' }} />
<WorkspaceSidebarContent>
<div>
{templatesPortalFlag && <TemplatesPortal />}
{dailyNotesFlag && (
<div>
<CollapsibleTitle title="Daily Notes">
<CalendarHeatmap />
</CollapsibleTitle>
</div>
)}
<div>
<CollapsibleTitle
title="Activities"
initialOpen={false}
>
<Activities></Activities>
</CollapsibleTitle>
</div>
<div>
<CollapsiblePageTree title="Page Tree">
{page_id ? <PageTree /> : null}
</CollapsiblePageTree>
</div>
</div>
</WorkspaceSidebarContent>
</WorkspaceSidebar>
</LigoLeftContainer>
<LigoRightContainer>
<LigoEditorOuterContainer>
{page_id ? (
<AffineEditor
workspace={props.workspace}
rootBlockId={page_id}
/>
) : (
<Box
sx={{
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
width: '100%',
}}
>
<CircularProgress />
</Box>
)}
</LigoEditorOuterContainer>
</LigoRightContainer>
</LigoApp>
);
}
const LigoApp = styled('div')({
width: '100vw',
position: 'relative',
display: 'flex',
flex: '1 1 0%',
backgroundColor: 'white',
margin: '10px 0',
});
const LigoRightContainer = styled('div')({
position: 'relative',
width: '100%',
flex: 'auto',
});
const LigoEditorOuterContainer = styled('div')({
position: 'absolute',
height: '100%',
width: '100%',
overflowX: 'hidden',
overflowY: 'hidden',
});
const LigoLeftContainer = styled('div')({
flex: '0 0 auto',
});
const WorkspaceSidebar = styled('div')(({ hidden }) => ({
position: 'absolute',
zIndex: 1,
display: 'flex',
flexDirection: 'column',
width: 300,
minWidth: 300,
height: '100%',
borderRadius: '0px 10px 10px 0px',
boxShadow: '0px 1px 10px rgba(152, 172, 189, 0.6)',
backgroundColor: '#FFFFFF',
transitionProperty: 'left',
transitionDuration: '0.35s',
transitionTimingFunction: 'ease',
padding: '16px 12px',
}));
const WorkspaceSidebarContent = styled('div')({
flex: 'auto',
overflow: 'hidden auto',
});

View File

@@ -0,0 +1,39 @@
import { styled } from '@toeverything/components/ui';
import SearchIcon from '@mui/icons-material/Search';
const handle_search = () => {
//@ts-ignore
virgo.plugins.plugins['search'].renderSearch();
};
const QuickFindPortalContainer = styled('div')({
position: 'relative',
marginLeft: '10px',
height: '22px',
lineHeight: '22px',
width: '220px',
borderRadius: '8px',
color: '#4c6275',
fontSize: '14px',
paddingLeft: '20px',
cursor: 'pointer',
':hover': {
backgroundColor: '#ccc',
},
'.shortcutIcon': {
position: 'absolute',
top: '3px',
left: '0px',
fontSize: '16px!important',
},
});
function QuickFindPortal() {
return (
<QuickFindPortalContainer onClick={handle_search}>
<SearchIcon className="shortcutIcon" /> Quick Find
</QuickFindPortalContainer>
);
}
export default QuickFindPortal;

View File

@@ -0,0 +1,90 @@
import {
styled,
MuiBox as Box,
MuiModal as Modal,
} from '@toeverything/components/ui';
import * as React from 'react';
import { Templates } from '../../templates';
import StarIcon from '@mui/icons-material/Star';
import { useNavigate } from 'react-router';
import { AsyncBlock } from '@toeverything/framework/virgo';
import { createEditor } from '@toeverything/components/affine-editor';
const TemplatePortalContainer = styled('div')({
position: 'relative',
marginLeft: '10px',
height: '22px',
lineHeight: '22px',
width: '220px',
borderRadius: '8px',
color: '#4c6275',
fontSize: '14px',
paddingLeft: '20px',
cursor: 'pointer',
':hover': {
backgroundColor: '#ccc',
},
'.shortcutIcon': {
position: 'absolute',
top: '3px',
left: '0px',
fontSize: '16px!important',
},
});
const style = {
position: 'absolute',
top: '40%',
left: '50%',
transform: 'translate(-50%, -50%)',
width: '80%',
height: '70%',
boxShadow: 0,
p: 0,
};
const maskStyle = {
background: 'rgba(0,0,0,0.5)',
width: '100%',
height: '100%',
position: 'fixed',
};
function TemplatesPortal() {
const [open, set_open] = React.useState(false);
const handle_open = () => set_open(true);
const handle_close = () => set_open(false);
const navigate = useNavigate();
const get_default_workspace_id = () => {
return window.location.pathname.split('/')[1];
};
const handleClickUseThisTemplate = () => {
const block_editor = createEditor(get_default_workspace_id());
//@ts-ignore
block_editor.plugins
.getPlugin('page-toolbar')
//@ts-ignore 泛型处理
.addDailyNote()
.then((new_page: AsyncBlock) => {
handle_close();
const new_state =
`/${get_default_workspace_id()}/` + new_page.id;
navigate(new_state);
});
};
return (
<>
<TemplatePortalContainer onClick={handle_open}>
<StarIcon className="shortcutIcon" /> Templates
</TemplatePortalContainer>
<Modal open={open} onClose={handle_close}>
<Box sx={style}>
<Templates
handleClickUseThisTemplate={handleClickUseThisTemplate}
/>
</Box>
</Modal>
</>
);
}
export default TemplatesPortal;

View File

@@ -0,0 +1,168 @@
import {
MuiButton as Button,
MuiSwitch as Switch,
styled,
MuiOutlinedInput as OutlinedInput,
} from '@toeverything/components/ui';
import { LogoIcon } from '@toeverything/components/icons';
import {
useUserAndSpaces,
useShowSpaceSidebar,
} from '@toeverything/datasource/state';
import { useCallback, useEffect, useState } from 'react';
import { services } from '@toeverything/datasource/db-service';
const WorkspaceContainer = styled('div')({
display: 'flex',
alignItems: 'center',
minHeight: 60,
padding: '12px 0px',
color: '#566B7D',
});
const LeftContainer = styled('div')({
flex: 'auto',
display: 'flex',
});
const LogoContainer = styled('div')({
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
height: 24,
minWidth: 24,
});
const StyledLogoIcon = styled(LogoIcon)(({ theme }) => {
return {
color: theme.affine.palette.primary,
width: '16px !important',
height: '16px !important',
};
});
const WorkspaceNameContainer = styled('div')({
display: 'flex',
alignItems: 'center',
flex: 'auto',
width: '100px',
marginRight: '10px',
input: {
padding: '5px 10px',
},
span: {
width: '100%',
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
},
});
const WorkspaceReNameContainer = styled('div')({
marginRight: '10px',
input: {
padding: '5px 10px',
},
});
const ToggleDisplayContainer = styled('div')({
display: 'flex',
alignItems: 'center',
fontSize: 12,
color: '#3E6FDB',
padding: 6,
minWidth: 64,
});
export const WorkspaceName = () => {
const { currentSpaceId } = useUserAndSpaces();
const { fixedDisplay, toggleSpaceSidebar } = useShowSpaceSidebar();
const [inRename, setInRename] = useState(false);
const [workspaceName, setWorkspaceName] = useState('');
const fetchWorkspaceName = useCallback(async () => {
if (!currentSpaceId) {
return;
}
const name = await services.api.userConfig.getWorkspaceName(
currentSpaceId
);
setWorkspaceName(name);
}, [currentSpaceId]);
useEffect(() => {
fetchWorkspaceName();
}, [currentSpaceId]);
useEffect(() => {
let unobserve: () => void;
const observe = async () => {
unobserve = await services.api.userConfig.observe(
{ workspace: currentSpaceId },
() => {
fetchWorkspaceName();
}
);
};
observe();
return () => {
unobserve?.();
};
}, [currentSpaceId, fetchWorkspaceName]);
const handleKeyDown = useCallback(
(e: React.KeyboardEvent<HTMLTextAreaElement>) => {
if (e.key === 'Enter') {
e.stopPropagation();
e.preventDefault();
setInRename(false);
}
},
[]
);
const handleChange = useCallback(
(e: React.ChangeEvent<HTMLTextAreaElement>) => {
services.api.userConfig.setWorkspaceName(
currentSpaceId,
e.currentTarget.value
);
},
[]
);
return (
<WorkspaceContainer>
<LeftContainer>
<LogoContainer>
<StyledLogoIcon />
</LogoContainer>
{inRename ? (
<WorkspaceReNameContainer>
<OutlinedInput
value={workspaceName}
onChange={handleChange}
onKeyDown={handleKeyDown}
onMouseLeave={() => setInRename(false)}
/>
</WorkspaceReNameContainer>
) : (
<WorkspaceNameContainer>
<span onClick={() => setInRename(true)}>
{workspaceName}
</span>
</WorkspaceNameContainer>
)}
</LeftContainer>
<ToggleDisplayContainer>
<span>{fixedDisplay ? 'ON' : 'OFF'}</span>
<Switch
checked={fixedDisplay}
onChange={toggleSpaceSidebar}
size="small"
/>
</ToggleDisplayContainer>
</WorkspaceContainer>
);
};