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 (
-
+