mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-09-06 00:41:39 +08:00
Merge branch 'feat/filesystem_and_search' of github.com:toeverything/AFFiNE into feat/filesystem_and_search
This commit is contained in:
@@ -11,7 +11,7 @@
|
||||
"dependencies": {
|
||||
"@blocksuite/blocks": "0.3.0-alpha.10",
|
||||
"@blocksuite/editor": "0.3.0-alpha.10",
|
||||
"@blocksuite/icons": "^2.0.0",
|
||||
"@blocksuite/icons": "^2.0.2",
|
||||
"@blocksuite/store": "0.3.0-alpha.10",
|
||||
"@emotion/css": "^11.10.0",
|
||||
"@emotion/react": "^11.10.4",
|
||||
@@ -22,11 +22,12 @@
|
||||
"@mui/base": "^5.0.0-alpha.87",
|
||||
"@mui/icons-material": "^5.10.9",
|
||||
"@mui/material": "^5.8.6",
|
||||
"@toeverything/pathfinder-logger": "workspace:@pathfinder/logger@*",
|
||||
"@pathfinder/data-services": "workspace:@pathfinder/data-services@*",
|
||||
"@toeverything/pathfinder-logger": "workspace:@pathfinder/logger@*",
|
||||
"cmdk": "^0.1.20",
|
||||
"css-spring": "^4.1.0",
|
||||
"dayjs": "^1.11.7",
|
||||
"i18next": "^21.9.1",
|
||||
"lit": "^2.3.1",
|
||||
"next": "13.0.1",
|
||||
"prettier": "^2.7.1",
|
||||
@@ -34,7 +35,6 @@
|
||||
"quill-cursors": "^4.0.0",
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0",
|
||||
"i18next": "^21.9.1",
|
||||
"react-i18next": "^11.18.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -13,6 +13,9 @@ import {
|
||||
ExportToHtmlIcon,
|
||||
ExportToMarkdownIcon,
|
||||
MoreVerticalIcon,
|
||||
FavouritesIcon,
|
||||
FavouritedIcon,
|
||||
TrashIcon,
|
||||
} from '@blocksuite/icons';
|
||||
import { useEditor } from '@/providers/editor-provider';
|
||||
import ThemeModeSwitch from '@/components/theme-mode-switch';
|
||||
@@ -22,15 +25,37 @@ import { getWarningMessage, shouldShowWarning } from './utils';
|
||||
import { Menu, MenuItem } from '@/ui/menu';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useConfirm } from '@/providers/confirm-provider';
|
||||
import { useModal } from '@/providers/global-modal-provider';
|
||||
import { useAppState } from '@/providers/app-state-provider';
|
||||
import { SyncIcon } from './sync-icon';
|
||||
import { toast } from '@/components/toast';
|
||||
|
||||
const PopoverContent = () => {
|
||||
const { editor, mode, setMode } = useEditor();
|
||||
const {
|
||||
editor,
|
||||
mode,
|
||||
setMode,
|
||||
getPageMeta,
|
||||
page,
|
||||
toggleFavoritePage,
|
||||
toggleDeletePage,
|
||||
} = useEditor();
|
||||
const { confirm } = useConfirm();
|
||||
|
||||
const { id, favorite, title } = getPageMeta(page?.id) ?? {
|
||||
id: '',
|
||||
favorite: false,
|
||||
title: '',
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
toggleFavoritePage(id);
|
||||
}}
|
||||
icon={favorite ? <FavouritedIcon /> : <FavouritesIcon />}
|
||||
>
|
||||
{favorite ? 'Remove' : 'Add'} to favourites
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
icon={mode === 'page' ? <EdgelessIcon /> : <PaperIcon />}
|
||||
onClick={() => {
|
||||
@@ -66,6 +91,22 @@ const PopoverContent = () => {
|
||||
Export
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
confirm({
|
||||
title: 'Delete page?',
|
||||
content: `${title || 'Untitled'} will be moved to Trash`,
|
||||
confirmText: 'Delete',
|
||||
confirmType: 'danger',
|
||||
}).then(confirm => {
|
||||
confirm && toggleDeletePage(id);
|
||||
toast('Moved to Trash');
|
||||
});
|
||||
}}
|
||||
icon={<TrashIcon />}
|
||||
>
|
||||
Delete
|
||||
</MenuItem>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -11,8 +11,9 @@ export const QuickSearchButton = ({
|
||||
const { triggerQuickSearchModal } = useModal();
|
||||
|
||||
return (
|
||||
<Tooltip content="Search and quickly jump to a page" placement="bottom">
|
||||
<Tooltip content="Switch to" placement="bottom">
|
||||
<IconButton
|
||||
data-testid="header-quickSearchButton"
|
||||
{...props}
|
||||
onClick={e => {
|
||||
onClick?.(e);
|
||||
|
||||
@@ -6,7 +6,6 @@ import {
|
||||
EdgelessIcon,
|
||||
} from '@blocksuite/icons';
|
||||
import {
|
||||
StyledFavoriteButton,
|
||||
StyledTableContainer,
|
||||
StyledTableRow,
|
||||
StyledTitleLink,
|
||||
@@ -18,19 +17,31 @@ import Empty from './empty';
|
||||
import { Content } from '@/ui/layout';
|
||||
import React from 'react';
|
||||
import DateCell from '@/components/page-list/date-cell';
|
||||
const FavoriteTag = ({ pageMeta }: { pageMeta: PageMeta }) => {
|
||||
import { IconButton } from '@/ui/button';
|
||||
import { Tooltip } from '@/ui/tooltip';
|
||||
import { router } from 'next/client';
|
||||
const FavoriteTag = ({
|
||||
pageMeta: { favorite, id },
|
||||
}: {
|
||||
pageMeta: PageMeta;
|
||||
}) => {
|
||||
const { toggleFavoritePage } = useEditor();
|
||||
|
||||
return (
|
||||
<StyledFavoriteButton
|
||||
className="favorite-button"
|
||||
favorite={pageMeta.favorite}
|
||||
onClick={() => {
|
||||
toggleFavoritePage(pageMeta.id);
|
||||
}}
|
||||
<Tooltip
|
||||
content={favorite ? 'Favourited' : 'Favourite'}
|
||||
placement="top-start"
|
||||
>
|
||||
{pageMeta.favorite ? <FavouritedIcon /> : <FavouritesIcon />}
|
||||
</StyledFavoriteButton>
|
||||
<IconButton
|
||||
darker={true}
|
||||
iconSize={[20, 20]}
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
toggleFavoritePage(id);
|
||||
}}
|
||||
>
|
||||
{favorite ? <FavouritedIcon /> : <FavouritesIcon />}
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -52,7 +63,7 @@ export const PageList = ({
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell proportion={0.5}>Documents</TableCell>
|
||||
<TableCell proportion={0.5}>Title</TableCell>
|
||||
<TableCell proportion={0.2}>Created</TableCell>
|
||||
<TableCell proportion={0.2}>
|
||||
{isTrash ? 'Moved to Trash' : 'Updated'}
|
||||
@@ -63,12 +74,18 @@ export const PageList = ({
|
||||
<TableBody>
|
||||
{pageList.map((pageMeta, index) => {
|
||||
return (
|
||||
<StyledTableRow key={`${pageMeta.id}-${index}`}>
|
||||
<StyledTableRow
|
||||
key={`${pageMeta.id}-${index}`}
|
||||
onClick={() => {
|
||||
router.push({
|
||||
pathname: '/',
|
||||
query: { pageId: pageMeta.id },
|
||||
});
|
||||
}}
|
||||
>
|
||||
<TableCell>
|
||||
<StyledTitleWrapper>
|
||||
<StyledTitleLink
|
||||
href={{ pathname: '/', query: { pageId: pageMeta.id } }}
|
||||
>
|
||||
<StyledTitleLink>
|
||||
{pageMeta.mode === 'edgeless' ? (
|
||||
<EdgelessIcon />
|
||||
) : (
|
||||
@@ -87,7 +104,12 @@ export const PageList = ({
|
||||
dateKey={isTrash ? 'trashDate' : 'updatedDate'}
|
||||
backupKey={isTrash ? 'trashDate' : 'createDate'}
|
||||
/>
|
||||
<TableCell style={{ padding: 0 }}>
|
||||
<TableCell
|
||||
style={{ padding: 0 }}
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
>
|
||||
{isTrash ? (
|
||||
<TrashOperationCell pageMeta={pageMeta} />
|
||||
) : (
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
OpenInNewIcon,
|
||||
TrashIcon,
|
||||
} from '@blocksuite/icons';
|
||||
import React from 'react';
|
||||
import { toast } from '@/components/toast';
|
||||
|
||||
export const OperationCell = ({ pageMeta }: { pageMeta: PageMeta }) => {
|
||||
const { id, favorite } = pageMeta;
|
||||
@@ -41,11 +41,12 @@ export const OperationCell = ({ pageMeta }: { pageMeta: PageMeta }) => {
|
||||
onClick={() => {
|
||||
confirm({
|
||||
title: 'Delete page?',
|
||||
content: `${pageMeta.title} will be moved to Trash`,
|
||||
content: `${pageMeta.title || 'Untitled'} will be moved to Trash`,
|
||||
confirmText: 'Delete',
|
||||
confirmType: 'danger',
|
||||
}).then(confirm => {
|
||||
confirm && toggleDeletePage(id);
|
||||
toast('Moved to Trash');
|
||||
});
|
||||
}}
|
||||
icon={<TrashIcon />}
|
||||
@@ -57,7 +58,7 @@ export const OperationCell = ({ pageMeta }: { pageMeta: PageMeta }) => {
|
||||
return (
|
||||
<Wrapper alignItems="center" justifyContent="center">
|
||||
<Menu content={OperationMenu} placement="bottom-end" disablePortal={true}>
|
||||
<IconButton hoverBackground="#E0E6FF">
|
||||
<IconButton darker={true}>
|
||||
<MoreVerticalIcon />
|
||||
</IconButton>
|
||||
</Menu>
|
||||
@@ -67,31 +68,34 @@ export const OperationCell = ({ pageMeta }: { pageMeta: PageMeta }) => {
|
||||
|
||||
export const TrashOperationCell = ({ pageMeta }: { pageMeta: PageMeta }) => {
|
||||
const { id } = pageMeta;
|
||||
const { permanentlyDeletePage, toggleDeletePage } = useEditor();
|
||||
const { permanentlyDeletePage, toggleDeletePage, openPage, getPageMeta } =
|
||||
useEditor();
|
||||
const { confirm } = useConfirm();
|
||||
|
||||
return (
|
||||
<Wrapper>
|
||||
<IconButton
|
||||
hoverBackground="#E0E6FF"
|
||||
darker={true}
|
||||
style={{ marginRight: '12px' }}
|
||||
onClick={() => {
|
||||
toggleDeletePage(id);
|
||||
toast(`${getPageMeta(id)?.title || 'Untitled'} restored`);
|
||||
openPage(id);
|
||||
}}
|
||||
>
|
||||
<RestoreIcon />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
hoverBackground="#E0E6FF"
|
||||
darker={true}
|
||||
onClick={() => {
|
||||
confirm({
|
||||
title: 'Permanently delete',
|
||||
content:
|
||||
"Once deleted, you can't undo this action. Do you confirm?",
|
||||
title: 'Delete permanently?',
|
||||
content: "Once deleted, you can't undo this action.",
|
||||
confirmText: 'Delete',
|
||||
confirmType: 'danger',
|
||||
}).then(confirm => {
|
||||
confirm && permanentlyDeletePage(id);
|
||||
toast('Permanently deleted');
|
||||
});
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -23,7 +23,7 @@ export const StyledTitleWrapper = styled.div(({ theme }) => {
|
||||
},
|
||||
};
|
||||
});
|
||||
export const StyledTitleLink = styled(Link)(({ theme }) => {
|
||||
export const StyledTitleLink = styled.div(({ theme }) => {
|
||||
return {
|
||||
maxWidth: '80%',
|
||||
marginRight: '18px',
|
||||
@@ -34,31 +34,12 @@ export const StyledTitleLink = styled(Link)(({ theme }) => {
|
||||
marginRight: '12px',
|
||||
color: theme.colors.iconColor,
|
||||
},
|
||||
':hover': {
|
||||
color: theme.colors.textColor,
|
||||
'>svg': {
|
||||
color: theme.colors.primaryColor,
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
export const StyledFavoriteButton = styled.button<{ favorite: boolean }>(
|
||||
({ theme, favorite }) => {
|
||||
return {
|
||||
width: '32px',
|
||||
height: '32px',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
display: 'none',
|
||||
color: favorite ? theme.colors.primaryColor : theme.colors.iconColor,
|
||||
'&:hover': {
|
||||
color: theme.colors.primaryColor,
|
||||
},
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
export const StyledTableRow = styled(TableRow)(({ theme }) => {
|
||||
return {
|
||||
cursor: 'pointer',
|
||||
'&:hover': {
|
||||
'.favorite-button': {
|
||||
display: 'flex',
|
||||
|
||||
@@ -12,6 +12,7 @@ export const Footer = (props: { query: string }) => {
|
||||
|
||||
return (
|
||||
<Command.Item
|
||||
data-testid="quickSearch-addNewPage"
|
||||
onSelect={async () => {
|
||||
const page = await createPage({ title: query });
|
||||
openPage(page.id);
|
||||
|
||||
@@ -48,7 +48,12 @@ export const QuickSearch = ({ open, onClose }: TransitionsModalProps) => {
|
||||
}, [open, triggerQuickSearchModal]);
|
||||
|
||||
return (
|
||||
<Modal open={open} onClose={onClose} wrapperPosition={['top', 'center']}>
|
||||
<Modal
|
||||
open={open}
|
||||
onClose={onClose}
|
||||
wrapperPosition={['top', 'center']}
|
||||
data-testid="quickSearch"
|
||||
>
|
||||
<ModalWrapper
|
||||
width={620}
|
||||
style={{
|
||||
|
||||
@@ -70,7 +70,7 @@ export const Results = (props: {
|
||||
</StyledNotFound>
|
||||
)
|
||||
) : (
|
||||
<Command.Group heading="Jump to">
|
||||
<Command.Group heading="Switch to">
|
||||
{List.map(link => {
|
||||
return (
|
||||
<Command.Item
|
||||
|
||||
@@ -37,10 +37,10 @@ export const StyledJumpTo = styled('div')(({ theme }) => {
|
||||
});
|
||||
export const StyledNotFound = styled('div')(({ theme }) => {
|
||||
return {
|
||||
width: '620px',
|
||||
width: '612px',
|
||||
...displayFlex('center', 'center'),
|
||||
flexDirection: 'column',
|
||||
padding: '10px 16px',
|
||||
padding: '5px 16px',
|
||||
fontSize: theme.font.sm,
|
||||
span: {
|
||||
width: '100%',
|
||||
|
||||
+9
-2
@@ -1,3 +1,4 @@
|
||||
import { useRouter } from 'next/router';
|
||||
import { styled } from '@/styles';
|
||||
import {
|
||||
WorkspaceItemAvatar,
|
||||
@@ -13,9 +14,15 @@ interface WorkspaceItemProps {
|
||||
icon: string;
|
||||
}
|
||||
|
||||
export const WorkspaceItem = ({ name, icon }: WorkspaceItemProps) => {
|
||||
export const WorkspaceItem = ({ id, name, icon }: WorkspaceItemProps) => {
|
||||
const router = useRouter();
|
||||
|
||||
return (
|
||||
<StyledWrapper>
|
||||
<StyledWrapper
|
||||
onClick={() => {
|
||||
router.push(`/workspace/${id}`);
|
||||
}}
|
||||
>
|
||||
<WorkspaceItemAvatar alt={name} src={icon}>
|
||||
{name.charAt(0)}
|
||||
</WorkspaceItemAvatar>
|
||||
|
||||
+1
@@ -8,6 +8,7 @@ export const WorkspaceSelector = () => {
|
||||
content={<SelectorPopperContent />}
|
||||
zIndex={1000}
|
||||
placement="bottom-start"
|
||||
trigger="hover"
|
||||
>
|
||||
<SelectorWrapper>
|
||||
<Avatar alt="Affine" />
|
||||
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
ImportIcon,
|
||||
TrashIcon,
|
||||
AddIcon,
|
||||
FavouritedIcon,
|
||||
} from '@blocksuite/icons';
|
||||
import Link from 'next/link';
|
||||
import { Tooltip } from '@/ui/tooltip';
|
||||
@@ -74,15 +75,14 @@ export const WorkSpaceSliderBar = () => {
|
||||
<StyledListItemForWorkspace>
|
||||
<WorkspaceSelector />
|
||||
</StyledListItemForWorkspace>
|
||||
<Tooltip content="Search and quickly jump to a page" placement="right">
|
||||
<StyledListItem
|
||||
onClick={() => {
|
||||
triggerQuickSearchModal();
|
||||
}}
|
||||
>
|
||||
<SearchIcon /> Quick search
|
||||
</StyledListItem>
|
||||
</Tooltip>
|
||||
<StyledListItem
|
||||
data-testid="sliderBar-quickSearchButton"
|
||||
onClick={() => {
|
||||
triggerQuickSearchModal();
|
||||
}}
|
||||
>
|
||||
<SearchIcon /> Quick search
|
||||
</StyledListItem>
|
||||
<Link href={{ pathname: '/page-list/all' }}>
|
||||
<StyledListItem active={router.pathname === '/page-list/all'}>
|
||||
<AllPagesIcon /> <span>All pages</span>
|
||||
@@ -94,7 +94,7 @@ export const WorkSpaceSliderBar = () => {
|
||||
Favourites
|
||||
</StyledLink>
|
||||
<IconButton
|
||||
hoverBackground="#E0E6FF"
|
||||
darker={true}
|
||||
onClick={() => {
|
||||
setShowSubFavorite(!showSubFavorite);
|
||||
}}
|
||||
@@ -108,13 +108,16 @@ export const WorkSpaceSliderBar = () => {
|
||||
</StyledListItem>
|
||||
<FavoriteList showList={showSubFavorite} />
|
||||
|
||||
<StyledListItem
|
||||
onClick={() => {
|
||||
triggerImportModal();
|
||||
}}
|
||||
>
|
||||
<ImportIcon /> Import
|
||||
</StyledListItem>
|
||||
<Tooltip content="Coming soon" placement="right-start" zIndex={9999}>
|
||||
<StyledListItem
|
||||
disabled={true}
|
||||
onClick={() => {
|
||||
// triggerImportModal();
|
||||
}}
|
||||
>
|
||||
<ImportIcon /> Import
|
||||
</StyledListItem>
|
||||
</Tooltip>
|
||||
|
||||
<Link href={{ pathname: '/page-list/trash' }}>
|
||||
<StyledListItem active={router.pathname === '/page-list/trash'}>
|
||||
@@ -136,6 +139,7 @@ export const WorkSpaceSliderBar = () => {
|
||||
visible={showTip}
|
||||
>
|
||||
<StyledArrowButton
|
||||
data-testid="sliderBar-arrowButton"
|
||||
isShow={show}
|
||||
onClick={() => {
|
||||
setShow(!show);
|
||||
|
||||
@@ -43,29 +43,33 @@ export const StyledArrowButton = styled.button<{ isShow: boolean }>(
|
||||
}
|
||||
);
|
||||
|
||||
export const StyledListItem = styled.button<{ active?: boolean }>(
|
||||
({ theme, active }) => {
|
||||
return {
|
||||
width: '296px',
|
||||
height: '32px',
|
||||
marginTop: '12px',
|
||||
fontSize: theme.font.sm,
|
||||
color: active ? theme.colors.primaryColor : theme.colors.popoverColor,
|
||||
backgroundColor: active ? theme.colors.hoverBackground : 'unset',
|
||||
paddingLeft: '12px',
|
||||
borderRadius: '5px',
|
||||
...displayFlex('flex-start', 'center'),
|
||||
'>svg': {
|
||||
fontSize: '20px',
|
||||
marginRight: '12px',
|
||||
},
|
||||
':hover': {
|
||||
color: theme.colors.primaryColor,
|
||||
backgroundColor: theme.colors.hoverBackground,
|
||||
},
|
||||
};
|
||||
}
|
||||
);
|
||||
export const StyledListItem = styled.button<{
|
||||
active?: boolean;
|
||||
disabled?: boolean;
|
||||
}>(({ theme, active, disabled }) => {
|
||||
return {
|
||||
width: '296px',
|
||||
height: '32px',
|
||||
marginTop: '12px',
|
||||
fontSize: theme.font.sm,
|
||||
color: active ? theme.colors.primaryColor : theme.colors.popoverColor,
|
||||
paddingLeft: '12px',
|
||||
borderRadius: '5px',
|
||||
...displayFlex('flex-start', 'center'),
|
||||
...(disabled
|
||||
? { cursor: 'not-allowed', color: theme.colors.borderColor }
|
||||
: {}),
|
||||
|
||||
'>svg': {
|
||||
fontSize: '20px',
|
||||
marginRight: '12px',
|
||||
},
|
||||
':hover:not([disabled])': {
|
||||
color: theme.colors.primaryColor,
|
||||
backgroundColor: theme.colors.hoverBackground,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
export const StyledListItemForWorkspace = styled(StyledListItem)({
|
||||
height: '52px',
|
||||
|
||||
@@ -7,7 +7,7 @@ export const Favorite = () => {
|
||||
const { pageList: allPageList } = useEditor();
|
||||
return (
|
||||
<>
|
||||
<PageListHeader icon={<FavouritesIcon />}>Favorites</PageListHeader>
|
||||
<PageListHeader icon={<FavouritesIcon />}>Favourites</PageListHeader>
|
||||
<PageList pageList={allPageList.filter(p => p.favorite && !p.trash)} />
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import type { NextPage } from 'next';
|
||||
import { styled } from '@/styles';
|
||||
import { EditorHeader } from '@/components/header';
|
||||
import EdgelessToolbar from '@/components/edgeless-toolbar';
|
||||
import MobileModal from '@/components/mobile-modal';
|
||||
import Editor from '@/components/editor';
|
||||
|
||||
const StyledEditorContainer = styled('div')(({ theme }) => {
|
||||
return {
|
||||
height: 'calc(100vh - 60px)',
|
||||
};
|
||||
});
|
||||
|
||||
const Home: NextPage = () => {
|
||||
return (
|
||||
<>
|
||||
<EditorHeader />
|
||||
<MobileModal />
|
||||
<StyledEditorContainer>
|
||||
<Editor />
|
||||
</StyledEditorContainer>
|
||||
<EdgelessToolbar />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Home;
|
||||
@@ -0,0 +1,5 @@
|
||||
const Page = () => {
|
||||
return <div>Hello, there is [pageId]/index</div>;
|
||||
};
|
||||
|
||||
export default Page;
|
||||
@@ -1,6 +1,13 @@
|
||||
import type { EditorContainer } from '@blocksuite/editor';
|
||||
import { createContext, useContext, useEffect, useState } from 'react';
|
||||
import {
|
||||
createContext,
|
||||
useContext,
|
||||
useEffect,
|
||||
useState,
|
||||
useCallback,
|
||||
} from 'react';
|
||||
import type { PropsWithChildren } from 'react';
|
||||
import { useRouter } from 'next/router';
|
||||
import dynamic from 'next/dynamic';
|
||||
import Loading from './loading';
|
||||
import { Page, Workspace } from '@blocksuite/store';
|
||||
@@ -35,8 +42,17 @@ export const useEditor = () => useContext(EditorContext);
|
||||
export const EditorProvider = ({
|
||||
children,
|
||||
}: PropsWithChildren<EditorContextProps>) => {
|
||||
const router = useRouter();
|
||||
const [workspace, setWorkspace] = useState<Workspace>();
|
||||
const [page, setPage] = useState<Page>();
|
||||
const [page, _setPage] = useState<Page>();
|
||||
const workspaceId = router.query.workspaceId as string;
|
||||
const setPage = useCallback(
|
||||
(page: Page) => {
|
||||
_setPage(page);
|
||||
router.push(`/workspace/${workspaceId}/${page.id.replace('space:', '')}`);
|
||||
},
|
||||
[_setPage, workspaceId]
|
||||
);
|
||||
const [pageList, setPageList] = useState<PageMeta[]>([]);
|
||||
const [editor, setEditor] = useState<EditorContainer>();
|
||||
|
||||
|
||||
@@ -19,13 +19,13 @@ import {
|
||||
import { useRouter } from 'next/router';
|
||||
import { createPage } from '@/providers/editor-provider/utils';
|
||||
|
||||
const getEditorParams = () => {
|
||||
const getEditorParams = (workspaceId: string) => {
|
||||
const providers = [];
|
||||
const params = new URLSearchParams(location.search);
|
||||
const room = params.get('room') ?? 'AFFINE-pathfinder';
|
||||
// const room = params.get('room') ?? 'AFFINE-pathfinder';
|
||||
if (params.get('syncMode') === 'websocket') {
|
||||
const WebsocketDocProvider = createWebsocketDocProvider(
|
||||
'ws://127.0.0.1:3000/collaboration/AFFiNE'
|
||||
`ws://${window.location.host}/collaboration/`
|
||||
);
|
||||
providers.push(WebsocketDocProvider);
|
||||
}
|
||||
@@ -33,7 +33,7 @@ const getEditorParams = () => {
|
||||
providers.push(IndexedDBDocProvider);
|
||||
|
||||
return {
|
||||
room,
|
||||
room: workspaceId,
|
||||
providers,
|
||||
};
|
||||
};
|
||||
@@ -53,12 +53,12 @@ const EditorReactor = ({
|
||||
}) => {
|
||||
const shouldInitIntroduction = useRef(false);
|
||||
const {
|
||||
query: { pageId: routerPageId },
|
||||
query: { pageId: routerPageId, workspaceId },
|
||||
} = useRouter();
|
||||
|
||||
useEffect(() => {
|
||||
const workspace = new Workspace({
|
||||
...getEditorParams(),
|
||||
...getEditorParams(workspaceId as string),
|
||||
}).register(BlockSchema);
|
||||
//@ts-ignore
|
||||
window.workspace = workspace;
|
||||
@@ -72,7 +72,7 @@ const EditorReactor = ({
|
||||
} else {
|
||||
setWorkspace(workspace);
|
||||
}
|
||||
}, [setWorkspace]);
|
||||
}, [setWorkspace, workspaceId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!workspace) {
|
||||
|
||||
@@ -31,7 +31,7 @@ export type EditorHandlers = {
|
||||
pageId: string,
|
||||
query?: { [key: string]: string }
|
||||
) => Promise<boolean>;
|
||||
getPageMeta: (pageId?: string) => PageMeta | void;
|
||||
getPageMeta: (pageId?: string) => PageMeta;
|
||||
toggleDeletePage: (pageId: string) => void;
|
||||
toggleFavoritePage: (pageId: string) => void;
|
||||
permanentlyDeletePage: (pageId: string) => void;
|
||||
|
||||
@@ -43,6 +43,7 @@ export type IconButtonProps = {
|
||||
hoverColor?: string;
|
||||
hoverStyle?: CSSProperties;
|
||||
children: ReactElement<HTMLAttributes<SVGElement>, 'svg'>;
|
||||
darker?: boolean;
|
||||
} & HTMLAttributes<HTMLButtonElement>;
|
||||
|
||||
export const IconButton = forwardRef<HTMLButtonElement, IconButtonProps>(
|
||||
|
||||
@@ -14,6 +14,7 @@ export const StyledIconButton = styled('button', {
|
||||
'hoverBackground',
|
||||
'hoverColor',
|
||||
'hoverStyle',
|
||||
'darker',
|
||||
].includes(prop);
|
||||
},
|
||||
})<{
|
||||
@@ -24,6 +25,8 @@ export const StyledIconButton = styled('button', {
|
||||
hoverBackground?: CSSProperties['background'];
|
||||
hoverColor?: string;
|
||||
hoverStyle?: CSSProperties;
|
||||
// In some cases, button is in a normal hover status, it should be darkened
|
||||
darker?: boolean;
|
||||
}>(
|
||||
({
|
||||
theme,
|
||||
@@ -33,6 +36,7 @@ export const StyledIconButton = styled('button', {
|
||||
hoverBackground,
|
||||
hoverColor,
|
||||
hoverStyle,
|
||||
darker = false,
|
||||
}) => {
|
||||
return {
|
||||
width,
|
||||
@@ -40,7 +44,7 @@ export const StyledIconButton = styled('button', {
|
||||
color: theme.colors.iconColor,
|
||||
...displayInlineFlex('center', 'center'),
|
||||
position: 'relative',
|
||||
...(disabled ? { cursor: 'not-allowed', pointerEvents: 'none' } : {}),
|
||||
...(disabled ? { cursor: 'not-allowed' } : {}),
|
||||
transition: 'background .15s',
|
||||
|
||||
// TODO: we need to add @emotion/babel-plugin
|
||||
@@ -61,7 +65,10 @@ export const StyledIconButton = styled('button', {
|
||||
':hover': {
|
||||
color: hoverColor ?? theme.colors.primaryColor,
|
||||
'::after': {
|
||||
background: hoverBackground ?? theme.colors.hoverBackground,
|
||||
background:
|
||||
hoverBackground ?? darker
|
||||
? theme.colors.innerHoverBackground
|
||||
: theme.colors.hoverBackground,
|
||||
},
|
||||
...(hoverStyle ?? {}),
|
||||
},
|
||||
@@ -116,7 +123,7 @@ export const StyledTextButton = styled('button', {
|
||||
paddingRight: padding,
|
||||
...displayInlineFlex('flex-start', 'center'),
|
||||
position: 'relative',
|
||||
...(disabled ? { cursor: 'not-allowed', pointerEvents: 'none' } : {}),
|
||||
...(disabled ? { cursor: 'not-allowed' } : {}),
|
||||
transition: 'background .15s',
|
||||
// TODO: Implement circle shape
|
||||
borderRadius: shape === 'default' ? borderRadius : height / 2,
|
||||
@@ -171,7 +178,10 @@ export const StyledButton = styled('button', {
|
||||
border: '1px solid',
|
||||
...displayInlineFlex('flex-start', 'center'),
|
||||
position: 'relative',
|
||||
...(disabled ? { cursor: 'not-allowed', pointerEvents: 'none' } : {}),
|
||||
// TODO: disabled color is not decided
|
||||
...(disabled
|
||||
? { cursor: 'not-allowed', color: theme.colors.borderColor }
|
||||
: {}),
|
||||
transition: 'background .15s',
|
||||
// TODO: Implement circle shape
|
||||
borderRadius: shape === 'default' ? borderRadius : height / 2,
|
||||
@@ -190,6 +200,7 @@ export const StyledButton = styled('button', {
|
||||
hoverStyle,
|
||||
}),
|
||||
|
||||
// TODO: disabled hover should be implemented
|
||||
//
|
||||
// ':hover': {
|
||||
// color: hoverColor ?? theme.colors.primaryColor,
|
||||
|
||||
@@ -40,6 +40,13 @@ export const StyledTableCell = styled.td<
|
||||
export const StyledTableHead = styled.thead(({ theme }) => {
|
||||
return {
|
||||
fontWeight: 500,
|
||||
tr: {
|
||||
':hover': {
|
||||
td: {
|
||||
background: 'unset',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
@@ -44,7 +44,7 @@ export async function getWorkspaceDetail(
|
||||
): Promise<WorkspaceDetail | null> {
|
||||
const data = await request<WorkspaceDetail | null>({
|
||||
url: `/api/workspace/${params.id}`,
|
||||
method: 'PUT',
|
||||
method: 'GET',
|
||||
});
|
||||
|
||||
return data.data;
|
||||
|
||||
Generated
+3454
-1167
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,82 @@
|
||||
import type { Page } from '@playwright/test';
|
||||
|
||||
const IS_MAC = process.platform === 'darwin';
|
||||
// const IS_WINDOWS = process.platform === 'win32';
|
||||
// const IS_LINUX = !IS_MAC && !IS_WINDOWS;
|
||||
|
||||
async function keyDownCtrlOrMeta(page: Page) {
|
||||
if (IS_MAC) {
|
||||
await page.keyboard.down('Meta');
|
||||
} else {
|
||||
await page.keyboard.down('Control');
|
||||
}
|
||||
}
|
||||
|
||||
async function keyUpCtrlOrMeta(page: Page) {
|
||||
if (IS_MAC) {
|
||||
await page.keyboard.up('Meta');
|
||||
} else {
|
||||
await page.keyboard.up('Control');
|
||||
}
|
||||
}
|
||||
async function keyDownOptionMeta(page: Page) {
|
||||
if (IS_MAC) {
|
||||
await page.keyboard.down('Alt');
|
||||
} else {
|
||||
await page.keyboard.down('Shift');
|
||||
}
|
||||
}
|
||||
|
||||
async function keyUpOptionMeta(page: Page) {
|
||||
if (IS_MAC) {
|
||||
await page.keyboard.up('Alt');
|
||||
} else {
|
||||
await page.keyboard.up('Shift');
|
||||
}
|
||||
}
|
||||
|
||||
// It's not good enough, but better than calling keyDownCtrlOrMeta and keyUpCtrlOrMeta separately
|
||||
export const withCtrlOrMeta = async (page: Page, fn: () => Promise<void>) => {
|
||||
await keyDownCtrlOrMeta(page);
|
||||
await fn();
|
||||
await keyUpCtrlOrMeta(page);
|
||||
};
|
||||
|
||||
export async function pressEnter(page: Page) {
|
||||
// avoid flaky test by simulate real user input
|
||||
await page.keyboard.press('Enter', { delay: 50 });
|
||||
}
|
||||
|
||||
export async function pressTab(page: Page) {
|
||||
await page.keyboard.press('Tab', { delay: 50 });
|
||||
}
|
||||
|
||||
export async function pressShiftTab(page: Page) {
|
||||
await page.keyboard.down('Shift');
|
||||
await page.keyboard.press('Tab', { delay: 50 });
|
||||
await page.keyboard.up('Shift');
|
||||
}
|
||||
|
||||
export async function pressShiftEnter(page: Page) {
|
||||
await page.keyboard.down('Shift');
|
||||
await page.keyboard.press('Enter', { delay: 50 });
|
||||
await page.keyboard.up('Shift');
|
||||
}
|
||||
|
||||
export async function copyByKeyboard(page: Page) {
|
||||
await keyDownCtrlOrMeta(page);
|
||||
await page.keyboard.press('c', { delay: 50 });
|
||||
await keyUpCtrlOrMeta(page);
|
||||
}
|
||||
|
||||
export async function cutByKeyboard(page: Page) {
|
||||
await keyDownCtrlOrMeta(page);
|
||||
await page.keyboard.press('x', { delay: 50 });
|
||||
await keyUpCtrlOrMeta(page);
|
||||
}
|
||||
|
||||
export async function pasteByKeyboard(page: Page) {
|
||||
await keyDownCtrlOrMeta(page);
|
||||
await page.keyboard.press('v', { delay: 50 });
|
||||
await keyUpCtrlOrMeta(page);
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
import { test, expect, type Page } from '@playwright/test';
|
||||
import { loadPage } from './libs/load-page';
|
||||
import { withCtrlOrMeta } from './libs/keyboard';
|
||||
|
||||
loadPage();
|
||||
const IS_MAC = process.platform === 'darwin';
|
||||
// const IS_WINDOWS = process.platform === 'win32';
|
||||
// const IS_LINUX = !IS_MAC && !IS_WINDOWS;
|
||||
|
||||
const openQuickSearchByShortcut = async (page: Page) =>
|
||||
await withCtrlOrMeta(page, () => page.keyboard.press('k', { delay: 50 }));
|
||||
|
||||
async function assertTitleTexts(
|
||||
page: Page,
|
||||
texts: string[],
|
||||
option?: { delay: number }
|
||||
) {
|
||||
await page.mouse.move(100, 100); // move mouse for focus
|
||||
const actual = await page
|
||||
.locator('[class=affine-default-page-block-title]')
|
||||
.allInnerTexts();
|
||||
setTimeout(() => {
|
||||
expect(actual).toEqual(texts);
|
||||
}, option?.delay);
|
||||
}
|
||||
|
||||
test.describe('Open quick search', () => {
|
||||
test('Click slider bar button', async ({ page }) => {
|
||||
const quickSearchButton = page.locator(
|
||||
'[data-testid=sliderBar-quickSearchButton]'
|
||||
);
|
||||
await quickSearchButton.click();
|
||||
const quickSearch = page.locator('[data-testid=quickSearch]');
|
||||
expect(quickSearch.isVisible()).toEqual(true);
|
||||
});
|
||||
test('Click arrowDown icon after title', async ({ page }) => {
|
||||
//header-quickSearchButton
|
||||
const quickSearchButton = page.locator(
|
||||
'[data-testid=header-quickSearchButton]'
|
||||
);
|
||||
await quickSearchButton.click();
|
||||
const quickSearch = page.locator('[data-testid=quickSearch]');
|
||||
expect(quickSearch.isVisible()).toEqual(true);
|
||||
});
|
||||
test('Press the shortcut key cmd+k', async ({ page }) => {
|
||||
await openQuickSearchByShortcut(page);
|
||||
const quickSearch = page.locator('[data-testid=quickSearch]');
|
||||
expect(quickSearch.isVisible()).toEqual(true);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Add new page in quick search', () => {
|
||||
test('Create a new page without keyword', async ({ page }) => {
|
||||
await openQuickSearchByShortcut(page);
|
||||
const addNewPage = page.locator('[data-testid=quickSearch-addNewPage]');
|
||||
await addNewPage.click();
|
||||
await assertTitleTexts(page, [''], { delay: 50 });
|
||||
});
|
||||
test('Create a new page with keyword', async ({ page }) => {
|
||||
await openQuickSearchByShortcut(page);
|
||||
await page.keyboard.insertText('test');
|
||||
const addNewPage = page.locator('[data-testid=quickSearch-addNewPage]');
|
||||
await addNewPage.click();
|
||||
await assertTitleTexts(page, ['test'], { delay: 50 });
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user