feat(web): drag page to trash folder (#2385)

Co-authored-by: Himself65 <himself65@outlook.com>
This commit is contained in:
Doma
2023-05-30 13:14:10 +08:00
committed by GitHub
parent 61c417992a
commit 4175f5391e
11 changed files with 385 additions and 138 deletions

View File

@@ -16,62 +16,76 @@ interface MenuItemProps extends React.HTMLAttributes<HTMLDivElement> {
interface MenuLinkItemProps extends MenuItemProps, Pick<LinkProps, 'href'> {}
export function MenuItem({
onClick,
icon,
active,
children,
disabled,
collapsed,
onCollapsedChange,
...props
}: MenuItemProps) {
const collapsible = collapsed !== undefined;
if (collapsible && !onCollapsedChange) {
throw new Error('onCollapsedChange is required when collapsed is defined');
export const MenuItem = React.forwardRef<HTMLDivElement, MenuItemProps>(
(
{
onClick,
icon,
active,
children,
disabled,
collapsed,
onCollapsedChange,
...props
},
ref
) => {
const collapsible = collapsed !== undefined;
if (collapsible && !onCollapsedChange) {
throw new Error(
'onCollapsedChange is required when collapsed is defined'
);
}
return (
<div
ref={ref}
{...props}
className={clsx([styles.root, props.className])}
onClick={onClick}
data-active={active}
data-disabled={disabled}
data-collapsible={collapsible}
>
{icon && (
<div className={styles.iconsContainer} data-collapsible={collapsible}>
{collapsible && (
<div
onClick={e => {
e.stopPropagation();
e.preventDefault(); // for links
onCollapsedChange?.(!collapsed);
}}
data-testid="fav-collapsed-button"
className={styles.collapsedIconContainer}
>
<ArrowDownSmallIcon
className={styles.collapsedIcon}
data-collapsed={collapsed}
/>
</div>
)}
{React.cloneElement(icon, {
className: clsx([styles.icon, icon.props.className]),
})}
</div>
)}
<div className={styles.content}>{children}</div>
</div>
);
}
return (
<div
{...props}
className={clsx([styles.root, props.className])}
onClick={onClick}
data-active={active}
data-disabled={disabled}
data-collapsible={collapsible}
>
{icon && (
<div className={styles.iconsContainer} data-collapsible={collapsible}>
{collapsible && (
<div
onClick={e => {
e.stopPropagation();
e.preventDefault(); // for links
onCollapsedChange?.(!collapsed);
}}
data-testid="fav-collapsed-button"
className={styles.collapsedIconContainer}
>
<ArrowDownSmallIcon
className={styles.collapsedIcon}
data-collapsed={collapsed}
/>
</div>
)}
{React.cloneElement(icon, {
className: clsx([styles.icon, icon.props.className]),
})}
</div>
)}
);
MenuItem.displayName = 'MenuItem';
<div className={styles.content}>{children}</div>
</div>
);
}
export function MenuLinkItem({ href, ...props }: MenuLinkItemProps) {
return (
<Link href={href} className={styles.linkItemRoot}>
<MenuItem {...props}></MenuItem>
</Link>
);
}
export const MenuLinkItem = React.forwardRef<HTMLDivElement, MenuLinkItemProps>(
({ href, ...props }, ref) => {
return (
<Link href={href} className={styles.linkItemRoot}>
{/* The <a> element rendered by Link does not generate display box due to `display: contents` style */}
{/* Thus ref is passed to MenuItem instead of Link */}
<MenuItem ref={ref} {...props}></MenuItem>
</Link>
);
}
);
MenuLinkItem.displayName = 'MenuLinkItem';

View File

@@ -9,7 +9,8 @@ import { DEFAULT_SORT_KEY } from '@affine/env/constant';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { ArrowDownBigIcon, ArrowUpBigIcon } from '@blocksuite/icons';
import { useMediaQuery, useTheme } from '@mui/material';
import type { CSSProperties } from 'react';
import type React from 'react';
import { type CSSProperties } from 'react';
import { AllPagesBody } from './all-pages-body';
import { NewPageButton } from './components/new-page-buttton';

View File

@@ -1,5 +1,6 @@
import { TableBody, TableCell } from '@affine/component';
import { styled, TableBody, TableCell } from '@affine/component';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { useDraggable } from '@dnd-kit/core';
import { useMediaQuery, useTheme } from '@mui/material';
import type { ReactNode } from 'react';
import { Fragment } from 'react';
@@ -8,7 +9,7 @@ import { FavoriteTag } from './components/favorite-tag';
import { TitleCell } from './components/title-cell';
import { OperationCell } from './operation-cell';
import { StyledTableRow } from './styles';
import type { DateKey, ListData } from './type';
import type { DateKey, DraggableTitleCellData, ListData } from './type';
import { useDateGroup } from './use-date-group';
import { formatDate } from './utils';
@@ -63,6 +64,7 @@ export const AllPagesBody = ({
},
index
) => {
const displayTitle = title || t['Untitled']();
return (
<Fragment key={pageId}>
{groupName &&
@@ -71,9 +73,15 @@ export const AllPagesBody = ({
<GroupRow>{groupName}</GroupRow>
)}
<StyledTableRow data-testid={`page-list-item-${pageId}`}>
<TitleCell
<DraggableTitleCell
pageId={pageId}
draggableData={{
pageId,
pageTitle: displayTitle,
icon,
}}
icon={icon}
text={title || t['Untitled']()}
text={displayTitle}
data-testid="title"
onClick={onClickPage}
/>
@@ -130,3 +138,41 @@ export const AllPagesBody = ({
</TableBody>
);
};
const FullSizeButton = styled('button')(() => ({
width: '100%',
height: '100%',
display: 'block',
}));
type DraggableTitleCellProps = {
pageId: string;
draggableData?: DraggableTitleCellData;
} & React.ComponentProps<typeof TitleCell>;
function DraggableTitleCell({
pageId,
draggableData,
...props
}: DraggableTitleCellProps) {
const { setNodeRef, attributes, listeners, isDragging } = useDraggable({
id: 'page-list-item-title-' + pageId,
data: draggableData,
});
return (
<TitleCell
ref={setNodeRef}
style={{ opacity: isDragging ? 0.5 : 1 }}
{...props}
>
{/* Use `button` for draggable element */}
{/* See https://docs.dndkit.com/api-documentation/draggable/usedraggable#role */}
{element => (
<FullSizeButton {...listeners} {...attributes}>
{element}
</FullSizeButton>
)}
</TitleCell>
);
}

View File

@@ -1,27 +1,44 @@
import type { TableCellProps } from '@affine/component';
import { Content, TableCell } from '@affine/component';
import React, { useCallback } from 'react';
import { StyledTitleLink } from '../styles';
export const TitleCell = ({
icon,
text,
suffix,
...props
}: {
type TitleCellProps = {
icon: JSX.Element;
text: string;
suffix?: JSX.Element;
} & TableCellProps) => {
return (
<TableCell {...props}>
<StyledTitleLink>
{icon}
<Content ellipsis={true} color="inherit">
{text}
</Content>
</StyledTitleLink>
{suffix}
</TableCell>
);
};
/**
* Customize the children of the cell
* @param element
* @returns
*/
children?: (element: React.ReactElement) => React.ReactNode;
} & Omit<TableCellProps, 'children'>;
export const TitleCell = React.forwardRef<HTMLTableCellElement, TitleCellProps>(
({ icon, text, suffix, children: render, ...props }, ref) => {
const renderChildren = useCallback(() => {
const childElement = (
<>
<StyledTitleLink>
{icon}
<Content ellipsis={true} color="inherit">
{text}
</Content>
</StyledTitleLink>
{suffix}
</>
);
return render ? render(childElement) : childElement;
}, [icon, render, suffix, text]);
return (
<TableCell ref={ref} {...props}>
{renderChildren()}
</TableCell>
);
}
);
TitleCell.displayName = 'TitleCell';

View File

@@ -43,3 +43,9 @@ export type PageListProps = {
onCreateNewEdgeless: () => void;
onImportFile: () => void;
};
export type DraggableTitleCellData = {
pageId: string;
pageTitle: string;
icon: React.ReactElement;
};