mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-14 05:14:54 +00:00
feat(core): support sidebar page item dnd (#5132)
Added the ability to drag page items from the `all pages` view to the sidebar, including `favourites,` `collection` and `trash`. Page items in `favourites` and `collection` can also be dragged between each other. However, linked subpages cannot be dragged. Additionally, an operation menu and ‘add’ button have been provided for the sidebar’s page items, enabling the addition of a subpage, renaming, deletion or removal from the sidebar. On the code front, the `useSidebarDrag` hooks have been implemented for consolidating drag events. The functions `getDragItemId` and `getDropItemId` have been created, and they accept type and ID to obtain itemId. https://github.com/toeverything/AFFiNE/assets/102217452/d06bac18-3c28-41c9-a7d4-72de955d7b11
This commit is contained in:
@@ -18,6 +18,7 @@ export const root = style({
|
||||
padding: '0 12px',
|
||||
fontSize: 'var(--affine-font-sm)',
|
||||
marginTop: '4px',
|
||||
position: 'relative',
|
||||
selectors: {
|
||||
'&:hover': {
|
||||
background: 'var(--affine-hover-color)',
|
||||
@@ -40,7 +41,7 @@ export const root = style({
|
||||
paddingLeft: '4px',
|
||||
paddingRight: '4px',
|
||||
},
|
||||
'&[data-type="collection-list-item"][data-collapsible="false"][data-active="true"],&[data-type="favorite-list-item"][data-collapsible="false"][data-active="true"], &[data-type="favorite-list-item"][data-collapsible="false"]:hover, &[data-type="collection-list-item"][data-collapsible="false"]:hover':
|
||||
'&[data-type="collection-list-item"][data-collapsible="false"][data-active="true"],&[data-type="reference-page"][data-collapsible="false"][data-active="true"], &[data-type="reference-page"][data-collapsible="false"]:hover, &[data-type="collection-list-item"][data-collapsible="false"]:hover':
|
||||
{
|
||||
width: 'calc(100% + 8px)',
|
||||
transform: 'translateX(-8px)',
|
||||
@@ -61,11 +62,14 @@ export const content = style({
|
||||
});
|
||||
|
||||
export const postfix = style({
|
||||
justifySelf: 'flex-end',
|
||||
right: '4px',
|
||||
position: 'absolute',
|
||||
opacity: 0,
|
||||
pointerEvents: 'none',
|
||||
selectors: {
|
||||
[`${root}:hover &`]: {
|
||||
justifySelf: 'flex-end',
|
||||
position: 'initial',
|
||||
opacity: 1,
|
||||
pointerEvents: 'all',
|
||||
},
|
||||
|
||||
@@ -23,13 +23,9 @@ export const root = style({
|
||||
|
||||
export const dragOverlay = style({
|
||||
display: 'flex',
|
||||
height: '54px', // 42 + 12
|
||||
alignItems: 'center',
|
||||
background: 'var(--affine-hover-color-filled)',
|
||||
boxShadow: 'var(--affine-menu-shadow)',
|
||||
borderRadius: 10,
|
||||
zIndex: 1001,
|
||||
cursor: 'pointer',
|
||||
cursor: 'grabbing',
|
||||
maxWidth: '360px',
|
||||
transition: 'transform 0.2s',
|
||||
willChange: 'transform',
|
||||
@@ -39,6 +35,16 @@ export const dragOverlay = style({
|
||||
},
|
||||
},
|
||||
});
|
||||
export const dragPageItemOverlay = style({
|
||||
height: '54px',
|
||||
borderRadius: '10px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
background: 'var(--affine-hover-color-filled)',
|
||||
boxShadow: 'var(--affine-menu-shadow)',
|
||||
maxWidth: '360px',
|
||||
minWidth: '260px',
|
||||
});
|
||||
|
||||
export const dndCell = style({
|
||||
position: 'relative',
|
||||
|
||||
@@ -121,7 +121,7 @@ const PageListOperationsCell = ({
|
||||
export const PageListItem = (props: PageListItemProps) => {
|
||||
const pageTitleElement = useMemo(() => {
|
||||
return (
|
||||
<>
|
||||
<div className={styles.dragPageItemOverlay}>
|
||||
<div className={styles.titleIconsWrapper}>
|
||||
<PageSelectionCell
|
||||
onSelectedChange={props.onSelectedChange}
|
||||
@@ -131,7 +131,7 @@ export const PageListItem = (props: PageListItemProps) => {
|
||||
<PageListIconCell icon={props.icon} />
|
||||
</div>
|
||||
<PageListTitleCell title={props.title} preview={props.preview} />
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
}, [
|
||||
props.icon,
|
||||
@@ -142,6 +142,7 @@ export const PageListItem = (props: PageListItemProps) => {
|
||||
props.title,
|
||||
]);
|
||||
|
||||
// TODO: use getDropItemId
|
||||
const { setNodeRef, attributes, listeners, isDragging } = useDraggable({
|
||||
id: 'page-list-item-title-' + props.pageId,
|
||||
data: {
|
||||
|
||||
@@ -102,6 +102,7 @@ export const useCollectionManager = (collectionsAtom: CollectionsCRUDAtom) => {
|
||||
? defaultCollection
|
||||
: collections.find(v => v.id === currentCollectionId) ??
|
||||
defaultCollection;
|
||||
|
||||
return {
|
||||
currentCollection: currentCollection,
|
||||
savedCollections: collections,
|
||||
|
||||
@@ -22,12 +22,14 @@ export const CollectionOperations = ({
|
||||
config,
|
||||
setting,
|
||||
info,
|
||||
openRenameModal,
|
||||
children,
|
||||
}: PropsWithChildren<{
|
||||
info: DeleteCollectionInfo;
|
||||
collection: Collection;
|
||||
config: AllPageListConfig;
|
||||
setting: ReturnType<typeof useCollectionManager>;
|
||||
openRenameModal?: () => void;
|
||||
}>) => {
|
||||
const { open: openEditCollectionModal, node: editModal } =
|
||||
useEditCollection(config);
|
||||
@@ -36,7 +38,12 @@ export const CollectionOperations = ({
|
||||
useEditCollectionName({
|
||||
title: t['com.affine.editCollection.renameCollection'](),
|
||||
});
|
||||
|
||||
const showEditName = useCallback(() => {
|
||||
// use openRenameModal if it is in the sidebar collection list
|
||||
if (openRenameModal) {
|
||||
return openRenameModal();
|
||||
}
|
||||
openEditCollectionNameModal(collection.name)
|
||||
.then(name => {
|
||||
return setting.updateCollection({ ...collection, name });
|
||||
@@ -44,7 +51,8 @@ export const CollectionOperations = ({
|
||||
.catch(err => {
|
||||
console.error(err);
|
||||
});
|
||||
}, [openEditCollectionNameModal, collection, setting]);
|
||||
}, [openRenameModal, openEditCollectionNameModal, collection, setting]);
|
||||
|
||||
const showEdit = useCallback(() => {
|
||||
openEditCollectionModal(collection)
|
||||
.then(collection => {
|
||||
@@ -54,6 +62,7 @@ export const CollectionOperations = ({
|
||||
console.error(err);
|
||||
});
|
||||
}, [setting, collection, openEditCollectionModal]);
|
||||
|
||||
const actions = useMemo<
|
||||
Array<
|
||||
| {
|
||||
|
||||
@@ -52,6 +52,7 @@ export const EditCollectionModal = ({
|
||||
.catch(err => {
|
||||
console.error(err);
|
||||
});
|
||||
onOpenChange(false);
|
||||
},
|
||||
[onConfirm, onOpenChange]
|
||||
);
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
import { useCallback, useState } from 'react';
|
||||
|
||||
import Input from '../../ui/input';
|
||||
import { Menu } from '../../ui/menu';
|
||||
|
||||
export const RenameModal = ({
|
||||
onRename,
|
||||
currentName,
|
||||
open,
|
||||
onOpenChange,
|
||||
}: {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
onRename: (newName: string) => void;
|
||||
currentName: string;
|
||||
}) => {
|
||||
const [value, setValue] = useState(currentName);
|
||||
const handleRename = useCallback(() => {
|
||||
onRename(value);
|
||||
onOpenChange(false);
|
||||
}, [onOpenChange, onRename, value]);
|
||||
return (
|
||||
<Menu
|
||||
rootOptions={{
|
||||
open: open,
|
||||
onOpenChange: onOpenChange,
|
||||
}}
|
||||
contentOptions={{
|
||||
side: 'left',
|
||||
onPointerDownOutside: handleRename,
|
||||
sideOffset: -12,
|
||||
}}
|
||||
items={
|
||||
<Input
|
||||
autoFocus
|
||||
width={220}
|
||||
style={{ height: 34 }}
|
||||
defaultValue={value}
|
||||
onChange={setValue}
|
||||
onEnter={handleRename}
|
||||
data-testid="rename-modal-input"
|
||||
/>
|
||||
}
|
||||
>
|
||||
<div></div>
|
||||
</Menu>
|
||||
);
|
||||
};
|
||||
@@ -48,12 +48,19 @@ export const Input = forwardRef<HTMLInputElement, InputProps>(function Input(
|
||||
endFix,
|
||||
onEnter,
|
||||
onKeyDown,
|
||||
autoFocus,
|
||||
...otherProps
|
||||
}: InputProps,
|
||||
ref: ForwardedRef<HTMLInputElement>
|
||||
) {
|
||||
const [isFocus, setIsFocus] = useState(false);
|
||||
|
||||
const handleAutoFocus = useCallback((ref: HTMLInputElement | null) => {
|
||||
if (ref) {
|
||||
window.setTimeout(() => ref.focus(), 0);
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={clsx(inputWrapper, className, {
|
||||
@@ -83,7 +90,7 @@ export const Input = forwardRef<HTMLInputElement, InputProps>(function Input(
|
||||
large: size === 'large',
|
||||
'extra-large': size === 'extraLarge',
|
||||
})}
|
||||
ref={ref}
|
||||
ref={autoFocus ? handleAutoFocus : ref}
|
||||
disabled={disabled}
|
||||
style={inputStyle}
|
||||
onFocus={useCallback(
|
||||
|
||||
Reference in New Issue
Block a user