mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-14 21:27:20 +00:00
feat: add new page button (#2417)
This commit is contained in:
@@ -9,6 +9,7 @@ export const blockCard = style({
|
||||
borderRadius: '4px',
|
||||
userSelect: 'none',
|
||||
cursor: 'pointer',
|
||||
textAlign: 'start',
|
||||
selectors: {
|
||||
'&:hover': {
|
||||
boxShadow: 'var(--affine-shadow-1)',
|
||||
|
||||
@@ -18,8 +18,10 @@ import {
|
||||
FavoriteIcon,
|
||||
} from '@blocksuite/icons';
|
||||
import { useMediaQuery, useTheme } from '@mui/material';
|
||||
import type { CSSProperties } from 'react';
|
||||
import { forwardRef } from 'react';
|
||||
|
||||
import { NewPageButton } from './new-page-buttton';
|
||||
import {
|
||||
StyledTableContainer,
|
||||
StyledTableRow,
|
||||
@@ -68,11 +70,8 @@ const FavoriteTag = forwardRef<
|
||||
export type PageListProps = {
|
||||
isPublicWorkspace?: boolean;
|
||||
list: ListData[];
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
listType: 'all' | 'favorite' | 'shared' | 'public';
|
||||
onClickPage: (pageId: string, newTab?: boolean) => void;
|
||||
onCreateNewPage: () => void;
|
||||
onCreateNewEdgeless: () => void;
|
||||
};
|
||||
|
||||
const TitleCell = ({
|
||||
@@ -100,6 +99,80 @@ const TitleCell = ({
|
||||
);
|
||||
};
|
||||
|
||||
const AllPagesHead = ({
|
||||
sorter,
|
||||
createNewPage,
|
||||
createNewEdgeless,
|
||||
}: {
|
||||
sorter: ReturnType<typeof useSorter<ListData>>;
|
||||
createNewPage: () => void;
|
||||
createNewEdgeless: () => void;
|
||||
}) => {
|
||||
const t = useAFFiNEI18N();
|
||||
const titleList = [
|
||||
{
|
||||
key: 'title',
|
||||
content: t['Title'](),
|
||||
proportion: 0.5,
|
||||
},
|
||||
{
|
||||
key: 'createDate',
|
||||
content: t['Created'](),
|
||||
proportion: 0.2,
|
||||
},
|
||||
{
|
||||
key: 'updatedDate',
|
||||
content: t['Updated'](),
|
||||
proportion: 0.2,
|
||||
},
|
||||
{
|
||||
key: 'unsortable_action',
|
||||
content: (
|
||||
<NewPageButton
|
||||
createNewPage={createNewPage}
|
||||
createNewEdgeless={createNewEdgeless}
|
||||
/>
|
||||
),
|
||||
sortable: false,
|
||||
styles: {
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
} satisfies CSSProperties,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
{titleList.map(
|
||||
({ key, content, proportion, sortable = true, styles }) => (
|
||||
<TableCell
|
||||
key={key}
|
||||
proportion={proportion}
|
||||
active={sorter.key === key}
|
||||
onClick={
|
||||
sortable
|
||||
? () => sorter.shiftOrder(key as keyof ListData)
|
||||
: undefined
|
||||
}
|
||||
style={styles}
|
||||
>
|
||||
{content}
|
||||
{sorter.key === key &&
|
||||
(sorter.order === 'asc' ? (
|
||||
<ArrowUpBigIcon width={24} height={24} />
|
||||
) : (
|
||||
<ArrowDownBigIcon width={24} height={24} />
|
||||
))}
|
||||
</TableCell>
|
||||
)
|
||||
)}
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
);
|
||||
};
|
||||
|
||||
export type ListData = {
|
||||
pageId: string;
|
||||
icon: JSX.Element;
|
||||
@@ -119,7 +192,8 @@ export type ListData = {
|
||||
export const PageList: React.FC<PageListProps> = ({
|
||||
isPublicWorkspace = false,
|
||||
list,
|
||||
listType,
|
||||
onCreateNewPage,
|
||||
onCreateNewEdgeless,
|
||||
}) => {
|
||||
const t = useAFFiNEI18N();
|
||||
const sorter = useSorter<ListData>({
|
||||
@@ -128,70 +202,12 @@ export const PageList: React.FC<PageListProps> = ({
|
||||
order: 'desc',
|
||||
});
|
||||
|
||||
const isShared = listType === 'shared';
|
||||
|
||||
const theme = useTheme();
|
||||
const isSmallDevices = useMediaQuery(theme.breakpoints.down('sm'));
|
||||
if (isSmallDevices) {
|
||||
return <PageListMobileView list={sorter.data} />;
|
||||
}
|
||||
|
||||
const ListHead = () => {
|
||||
const t = useAFFiNEI18N();
|
||||
const titleList = [
|
||||
{
|
||||
key: 'title',
|
||||
text: t['Title'](),
|
||||
proportion: 0.5,
|
||||
},
|
||||
{
|
||||
key: 'createDate',
|
||||
text: t['Created'](),
|
||||
proportion: 0.2,
|
||||
},
|
||||
{
|
||||
key: 'updatedDate',
|
||||
text: isShared
|
||||
? // TODO deprecated
|
||||
'Shared'
|
||||
: t['Updated'](),
|
||||
proportion: 0.2,
|
||||
},
|
||||
{ key: 'unsortable_action', sortable: false },
|
||||
];
|
||||
|
||||
return (
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
{titleList.map(({ key, text, proportion, sortable = true }) => (
|
||||
<TableCell
|
||||
key={key}
|
||||
proportion={proportion}
|
||||
active={sorter.key === key}
|
||||
onClick={
|
||||
sortable
|
||||
? () => sorter.shiftOrder(key as keyof ListData)
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={{ display: 'flex', alignItems: 'center', width: '100%' }}
|
||||
>
|
||||
{text}
|
||||
{sorter.key === key &&
|
||||
(sorter.order === 'asc' ? (
|
||||
<ArrowUpBigIcon width={24} height={24} />
|
||||
) : (
|
||||
<ArrowDownBigIcon width={24} height={24} />
|
||||
))}
|
||||
</div>
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
);
|
||||
};
|
||||
|
||||
const ListItems = sorter.data.map(
|
||||
(
|
||||
{
|
||||
@@ -237,7 +253,13 @@ export const PageList: React.FC<PageListProps> = ({
|
||||
</TableCell>
|
||||
{!isPublicWorkspace && (
|
||||
<TableCell
|
||||
style={{ padding: 0, display: 'flex', alignItems: 'center' }}
|
||||
style={{
|
||||
padding: 0,
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
gap: '10px',
|
||||
}}
|
||||
data-testid={`more-actions-${pageId}`}
|
||||
>
|
||||
<FavoriteTag
|
||||
@@ -264,7 +286,11 @@ export const PageList: React.FC<PageListProps> = ({
|
||||
return (
|
||||
<StyledTableContainer>
|
||||
<Table>
|
||||
<ListHead />
|
||||
<AllPagesHead
|
||||
sorter={sorter}
|
||||
createNewPage={onCreateNewPage}
|
||||
createNewEdgeless={onCreateNewEdgeless}
|
||||
/>
|
||||
<TableBody>{ListItems}</TableBody>
|
||||
</Table>
|
||||
</StyledTableContainer>
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { EdgelessIcon, PageIcon } from '@blocksuite/icons';
|
||||
import { useState } from 'react';
|
||||
|
||||
import { DropdownButton } from '../../ui/button/dropdown';
|
||||
import { Menu } from '../../ui/menu/menu';
|
||||
import { BlockCard } from '../card/block-card';
|
||||
|
||||
type NewPageButtonProps = {
|
||||
createNewPage: () => void;
|
||||
createNewEdgeless: () => void;
|
||||
};
|
||||
|
||||
export const CreateNewPagePopup = ({
|
||||
createNewPage,
|
||||
createNewEdgeless,
|
||||
}: NewPageButtonProps) => {
|
||||
const t = useAFFiNEI18N();
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: '8px',
|
||||
padding: '8px',
|
||||
}}
|
||||
>
|
||||
<BlockCard
|
||||
title={t['New Page']()}
|
||||
desc={t['com.affine.write_with_a_blank_page']()}
|
||||
right={<PageIcon width={20} height={20} />}
|
||||
onClick={createNewPage}
|
||||
/>
|
||||
<BlockCard
|
||||
title={t['com.affine.new_edgeless']()}
|
||||
desc={t['com.affine.draw_with_a_blank_whiteboard']()}
|
||||
right={<EdgelessIcon width={20} height={20} />}
|
||||
onClick={createNewEdgeless}
|
||||
/>
|
||||
{/* TODO Import */}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const NewPageButton = ({
|
||||
createNewPage,
|
||||
createNewEdgeless,
|
||||
}: NewPageButtonProps) => {
|
||||
const t = useAFFiNEI18N();
|
||||
const [open, setOpen] = useState(false);
|
||||
return (
|
||||
<Menu
|
||||
visible={open}
|
||||
placement="bottom-end"
|
||||
trigger={['click']}
|
||||
disablePortal={true}
|
||||
onClickAway={() => {
|
||||
setOpen(false);
|
||||
}}
|
||||
menuStyles={{ padding: '0px' }}
|
||||
content={
|
||||
<CreateNewPagePopup
|
||||
createNewPage={() => {
|
||||
createNewPage();
|
||||
setOpen(false);
|
||||
}}
|
||||
createNewEdgeless={() => {
|
||||
createNewEdgeless();
|
||||
setOpen(false);
|
||||
}}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<DropdownButton
|
||||
onClick={() => {
|
||||
createNewPage();
|
||||
setOpen(false);
|
||||
}}
|
||||
onClickDropDown={() => setOpen(!open)}
|
||||
>
|
||||
{t['New Page']()}
|
||||
</DropdownButton>
|
||||
</Menu>
|
||||
);
|
||||
};
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useState } from 'react';
|
||||
|
||||
type Sorter<T> = {
|
||||
type SorterConfig<T> = {
|
||||
data: T[];
|
||||
key: keyof T;
|
||||
order: 'asc' | 'desc' | 'none';
|
||||
@@ -32,8 +32,8 @@ const defaultSortingFn = <T extends Record<keyof any, unknown>>(
|
||||
export const useSorter = <T extends Record<keyof any, unknown>>({
|
||||
data,
|
||||
...defaultSorter
|
||||
}: Sorter<T> & { order: 'asc' | 'desc' }) => {
|
||||
const [sorter, setSorter] = useState<Omit<Sorter<T>, 'data'>>({
|
||||
}: SorterConfig<T> & { order: 'asc' | 'desc' }) => {
|
||||
const [sorter, setSorter] = useState<Omit<SorterConfig<T>, 'data'>>({
|
||||
...defaultSorter,
|
||||
// We should not show sorting icon at first time
|
||||
order: 'none',
|
||||
@@ -74,7 +74,7 @@ export const useSorter = <T extends Record<keyof any, unknown>>({
|
||||
/**
|
||||
* @deprecated In most cases, we no necessary use `setSorter` directly.
|
||||
*/
|
||||
updateSorter: (newVal: Partial<Sorter<T>>) =>
|
||||
updateSorter: (newVal: Partial<SorterConfig<T>>) =>
|
||||
setSorter({ ...sorter, ...newVal }),
|
||||
shiftOrder,
|
||||
resetSorter: () => setSorter(defaultSorter),
|
||||
|
||||
Reference in New Issue
Block a user