mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-11 20:08:37 +00:00
feat(web): drag page to trash folder (#2385)
Co-authored-by: Himself65 <himself65@outlook.com>
This commit is contained in:
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -43,3 +43,9 @@ export type PageListProps = {
|
||||
onCreateNewEdgeless: () => void;
|
||||
onImportFile: () => void;
|
||||
};
|
||||
|
||||
export type DraggableTitleCellData = {
|
||||
pageId: string;
|
||||
pageTitle: string;
|
||||
icon: React.ReactElement;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user