diff --git a/packages/frontend/component/src/index.ts b/packages/frontend/component/src/index.ts index 09a579d864..083eb6b216 100644 --- a/packages/frontend/component/src/index.ts +++ b/packages/frontend/component/src/index.ts @@ -7,6 +7,7 @@ export * from './ui/checkbox'; export * from './ui/date-picker'; export * from './ui/divider'; export * from './ui/dnd'; +export * from './ui/drag-handle'; export * from './ui/editable'; export * from './ui/empty'; export * from './ui/error-message'; diff --git a/packages/frontend/component/src/ui/drag-handle/index.css.ts b/packages/frontend/component/src/ui/drag-handle/index.css.ts new file mode 100644 index 0000000000..e3810d301c --- /dev/null +++ b/packages/frontend/component/src/ui/drag-handle/index.css.ts @@ -0,0 +1,19 @@ +import { cssVarV2 } from '@toeverything/theme/v2'; +import { style } from '@vanilla-extract/css'; + +export const root = style({ + cursor: 'grab', + color: cssVarV2.icon.secondary, + display: 'flex', + alignItems: 'center', + justifyContent: 'center', +}); + +export const svg = style({ + borderRadius: 8, + selectors: { + [`${root}[data-dragging="true"] &, ${root}:hover &`]: { + backgroundColor: cssVarV2.layer.background.hoverOverlay, + }, + }, +}); diff --git a/packages/frontend/component/src/ui/drag-handle/index.tsx b/packages/frontend/component/src/ui/drag-handle/index.tsx new file mode 100644 index 0000000000..41b65cfc29 --- /dev/null +++ b/packages/frontend/component/src/ui/drag-handle/index.tsx @@ -0,0 +1,39 @@ +import { clsx } from 'clsx'; +import { forwardRef } from 'react'; + +import * as styles from './index.css'; + +export const DragHandle = forwardRef< + HTMLDivElement, + { + className?: string; + dragging?: boolean; + } +>(({ className, dragging, ...props }, ref) => { + return ( +
+ + + + + + + + +
+ ); +}); + +DragHandle.displayName = 'DragHandle'; diff --git a/packages/frontend/core/src/components/affine/page-history-modal/history-modal.tsx b/packages/frontend/core/src/components/affine/page-history-modal/history-modal.tsx index 71ab00ed6d..902a6b8c89 100644 --- a/packages/frontend/core/src/components/affine/page-history-modal/history-modal.tsx +++ b/packages/frontend/core/src/components/affine/page-history-modal/history-modal.tsx @@ -2,8 +2,8 @@ import { Loading, Scrollable } from '@affine/component'; import { EditorLoading } from '@affine/component/page-detail-skeleton'; import { Button, IconButton } from '@affine/component/ui/button'; import { Modal, useConfirmModal } from '@affine/component/ui/modal'; -import { useDocCollectionPageTitle } from '@affine/core/components/hooks/use-block-suite-workspace-page-title'; import { GlobalDialogService } from '@affine/core/modules/dialogs'; +import { DocDisplayMetaService } from '@affine/core/modules/doc-display-meta'; import { EditorService } from '@affine/core/modules/editor'; import { WorkspacePermissionService } from '@affine/core/modules/permissions'; import { WorkspaceQuotaService } from '@affine/core/modules/quota'; @@ -433,7 +433,10 @@ const PageHistoryManager = ({ const editor = useService(EditorService).editor; const [mode, setMode] = useState(editor.mode$.value); - const title = useDocCollectionPageTitle(docCollection, pageId); + const docDisplayMetaService = useService(DocDisplayMetaService); + const i18n = useI18n(); + + const title = useLiveData(docDisplayMetaService.title$(pageId)); const onConfirmRestore = useCallback(() => { openConfirmModal({ @@ -467,7 +470,7 @@ const PageHistoryManager = ({ snapshotPage={snapshotPage} mode={mode} onModeChange={setMode} - title={title} + title={i18n.t(title)} /> >>(); - -function getAtom(w: DocCollection, pageId: string): Atom { - if (!weakMap.has(w)) { - weakMap.set(w, new Map()); - } - const map = weakMap.get(w); - assertExists(map); - if (!map.has(pageId)) { - const baseAtom = atom(w.getDoc(pageId)?.meta?.title || 'Untitled'); - baseAtom.onMount = set => { - const disposable = w.meta.docMetaUpdated.on(() => { - const page = w.getDoc(pageId); - set(page?.meta?.title || 'Untitled'); - }); - return () => { - disposable.dispose(); - }; - }; - map.set(pageId, baseAtom); - return baseAtom; - } else { - return map.get(pageId) as Atom; - } -} - -/** - * @deprecated use `useDocTitle(docId: string)` instead - */ -export function useDocCollectionPageTitle( - docCollection: DocCollection, - pageId: string -) { - const titleAtom = getAtom(docCollection, pageId); - assertExists(titleAtom); - const title = useAtomValue(titleAtom); - const { localizedJournalDate } = useJournalInfoHelper(pageId); - return localizedJournalDate || title; -} - -// This hook is NOT reactive to the page title change -export function useGetDocCollectionPageTitle(docCollection: DocCollection) { - const { getLocalizedJournalDateString } = useJournalInfoHelper(); - return useCallback( - (pageId: string) => { - return ( - getLocalizedJournalDateString(pageId) || - docCollection.getDoc(pageId)?.meta?.title - ); - }, - [docCollection, getLocalizedJournalDateString] - ); -} diff --git a/packages/frontend/core/src/desktop/pages/workspace/detail-page/detail-page-header.css.ts b/packages/frontend/core/src/desktop/pages/workspace/detail-page/detail-page-header.css.ts index 3b787fd3fa..4ecc6c5ea9 100644 --- a/packages/frontend/core/src/desktop/pages/workspace/detail-page/detail-page-header.css.ts +++ b/packages/frontend/core/src/desktop/pages/workspace/detail-page/detail-page-header.css.ts @@ -1,5 +1,11 @@ import { style } from '@vanilla-extract/css'; +export const root = style({ + position: 'relative', + height: '100%', + width: '100%', +}); + export const header = style({ display: 'flex', height: '100%', @@ -24,3 +30,18 @@ export const iconButtonContainer = style({ alignItems: 'center', gap: 10, }); + +export const dragHandle = style({ + cursor: 'grab', + position: 'absolute', + top: 0, + bottom: 0, + left: -16, + width: 16, + opacity: 0, + selectors: { + [`${root}:hover &, ${root}[data-dragging="true"] &`]: { + opacity: 1, + }, + }, +}); diff --git a/packages/frontend/core/src/desktop/pages/workspace/detail-page/detail-page-header.tsx b/packages/frontend/core/src/desktop/pages/workspace/detail-page/detail-page-header.tsx index 0e014726b4..e92c9675d3 100644 --- a/packages/frontend/core/src/desktop/pages/workspace/detail-page/detail-page-header.tsx +++ b/packages/frontend/core/src/desktop/pages/workspace/detail-page/detail-page-header.tsx @@ -1,7 +1,9 @@ import { Divider, + DragHandle, type InlineEditHandle, observeResize, + useDraggable, } from '@affine/component'; import { SharePageButton } from '@affine/core/components/affine/share-page-modal'; import { FavoriteButton } from '@affine/core/components/blocksuite/block-suite-header/favorite'; @@ -13,11 +15,13 @@ import { DetailPageHeaderPresentButton } from '@affine/core/components/blocksuit import { BlocksuiteHeaderTitle } from '@affine/core/components/blocksuite/block-suite-header/title'; import { EditorModeSwitch } from '@affine/core/components/blocksuite/block-suite-mode-switch'; import { useRegisterCopyLinkCommands } from '@affine/core/components/hooks/affine/use-register-copy-link-commands'; -import { useDocCollectionPageTitle } from '@affine/core/components/hooks/use-block-suite-workspace-page-title'; import { HeaderDivider } from '@affine/core/components/pure/header'; +import { DocDisplayMetaService } from '@affine/core/modules/doc-display-meta'; import { EditorService } from '@affine/core/modules/editor'; import { JournalService } from '@affine/core/modules/journal'; import { ViewIcon, ViewTitle } from '@affine/core/modules/workbench'; +import type { AffineDNDData } from '@affine/core/types/dnd'; +import { useI18n } from '@affine/i18n'; import type { Doc } from '@blocksuite/affine/store'; import { useLiveData, useService, type Workspace } from '@toeverything/infra'; import { forwardRef, useCallback, useEffect, useRef, useState } from 'react'; @@ -60,7 +64,11 @@ export function JournalPageHeader({ page, workspace }: PageHeaderProps) { const { hideShare, hideToday } = useDetailPageHeaderResponsive(containerWidth); - const title = useDocCollectionPageTitle(workspace.docCollection, page?.id); + + const docDisplayMetaService = useService(DocDisplayMetaService); + const i18n = useI18n(); + const title = i18n.t(useLiveData(docDisplayMetaService.title$(page.id))); + return (
@@ -106,7 +114,10 @@ export function NormalPageHeader({ page, workspace }: PageHeaderProps) { ); }, []); - const title = useDocCollectionPageTitle(workspace.docCollection, page?.id); + const docDisplayMetaService = useService(DocDisplayMetaService); + const i18n = useI18n(); + const title = i18n.t(useLiveData(docDisplayMetaService.title$(page.id))); + const editor = useService(EditorService).editor; const currentMode = useLiveData(editor.mode$); @@ -148,8 +159,12 @@ export function NormalPageHeader({ page, workspace }: PageHeaderProps) { ); } -export function DetailPageHeader(props: PageHeaderProps) { - const { page, workspace } = props; +export function DetailPageHeader( + props: PageHeaderProps & { + onDragging?: (dragging: boolean) => void; + } +) { + const { page, workspace, onDragging } = props; const journalService = useService(JournalService); const isJournal = !!useLiveData(journalService.journalDate$(page.id)); const isInTrash = page.meta?.trash; @@ -159,9 +174,42 @@ export function DetailPageHeader(props: PageHeaderProps) { docId: page.id, }); - return isJournal && !isInTrash ? ( - - ) : ( - + const { dragRef, dragHandleRef, dragging } = + useDraggable(() => { + return { + data: { + from: { + at: 'doc-detail:header', + docId: page.id, + }, + entity: { + type: 'doc', + id: page.id, + }, + }, + disableDragPreview: true, + }; + }, [page.id]); + + const inner = + isJournal && !isInTrash ? ( + + ) : ( + + ); + + useEffect(() => { + onDragging?.(dragging); + }, [dragging, onDragging]); + + return ( +
+ + {inner} +
); } diff --git a/packages/frontend/core/src/desktop/pages/workspace/detail-page/detail-page.css.ts b/packages/frontend/core/src/desktop/pages/workspace/detail-page/detail-page.css.ts index a2519bb122..92349f231f 100644 --- a/packages/frontend/core/src/desktop/pages/workspace/detail-page/detail-page.css.ts +++ b/packages/frontend/core/src/desktop/pages/workspace/detail-page/detail-page.css.ts @@ -1,4 +1,5 @@ import { cssVar } from '@toeverything/theme'; +import { cssVarV2 } from '@toeverything/theme/v2'; import { style } from '@vanilla-extract/css'; export const mainContainer = style({ @@ -39,6 +40,11 @@ export const affineDocViewport = style({ zIndex: -1, }, }, + selectors: { + '&[data-dragging="true"]': { + backgroundColor: cssVarV2.layer.background.hoverOverlay, + }, + }, }); export const scrollbar = style({ diff --git a/packages/frontend/core/src/desktop/pages/workspace/detail-page/detail-page.tsx b/packages/frontend/core/src/desktop/pages/workspace/detail-page/detail-page.tsx index 040b8462fb..aac3d7b9d4 100644 --- a/packages/frontend/core/src/desktop/pages/workspace/detail-page/detail-page.tsx +++ b/packages/frontend/core/src/desktop/pages/workspace/detail-page/detail-page.tsx @@ -249,10 +249,16 @@ const DetailPageImpl = memo(function DetailPageImpl() { setHasScrollTop(hasScrollTop); }, []); + const [dragging, setDragging] = useState(false); + return ( - +