mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-17 22:37:04 +08:00
init: the first public commit for AFFiNE
This commit is contained in:
@@ -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;
|
||||
18
apps/ligo-virgo/src/pages/workspace/docs/index.spec.tsx
Normal file
18
apps/ligo-virgo/src/pages/workspace/docs/index.spec.tsx
Normal 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();
|
||||
});
|
||||
});
|
||||
176
apps/ligo-virgo/src/pages/workspace/docs/index.tsx
Normal file
176
apps/ligo-virgo/src/pages/workspace/docs/index.tsx
Normal 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',
|
||||
});
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
168
apps/ligo-virgo/src/pages/workspace/docs/workspace-name.tsx
Normal file
168
apps/ligo-virgo/src/pages/workspace/docs/workspace-name.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user