@@ -7,6 +7,7 @@ export * from './ui/checkbox';
|
|||||||
export * from './ui/date-picker';
|
export * from './ui/date-picker';
|
||||||
export * from './ui/divider';
|
export * from './ui/divider';
|
||||||
export * from './ui/dnd';
|
export * from './ui/dnd';
|
||||||
|
export * from './ui/drag-handle';
|
||||||
export * from './ui/editable';
|
export * from './ui/editable';
|
||||||
export * from './ui/empty';
|
export * from './ui/empty';
|
||||||
export * from './ui/error-message';
|
export * from './ui/error-message';
|
||||||
|
|||||||
19
packages/frontend/component/src/ui/drag-handle/index.css.ts
Normal file
19
packages/frontend/component/src/ui/drag-handle/index.css.ts
Normal file
@@ -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,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
39
packages/frontend/component/src/ui/drag-handle/index.tsx
Normal file
39
packages/frontend/component/src/ui/drag-handle/index.tsx
Normal file
@@ -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 (
|
||||||
|
<div
|
||||||
|
{...props}
|
||||||
|
ref={ref}
|
||||||
|
data-dragging={dragging}
|
||||||
|
className={clsx(styles.root, className)}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
width="10"
|
||||||
|
height="22"
|
||||||
|
viewBox="0 0 10 22"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
className={styles.svg}
|
||||||
|
>
|
||||||
|
<circle cx="3" cy="7" r="1" fill="currentColor" />
|
||||||
|
<circle cx="7" cy="7" r="1" fill="currentColor" />
|
||||||
|
<circle cx="3" cy="11" r="1" fill="currentColor" />
|
||||||
|
<circle cx="7" cy="11" r="1" fill="currentColor" />
|
||||||
|
<circle cx="3" cy="15" r="1" fill="currentColor" />
|
||||||
|
<circle cx="7" cy="15" r="1" fill="currentColor" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
DragHandle.displayName = 'DragHandle';
|
||||||
@@ -2,8 +2,8 @@ import { Loading, Scrollable } from '@affine/component';
|
|||||||
import { EditorLoading } from '@affine/component/page-detail-skeleton';
|
import { EditorLoading } from '@affine/component/page-detail-skeleton';
|
||||||
import { Button, IconButton } from '@affine/component/ui/button';
|
import { Button, IconButton } from '@affine/component/ui/button';
|
||||||
import { Modal, useConfirmModal } from '@affine/component/ui/modal';
|
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 { GlobalDialogService } from '@affine/core/modules/dialogs';
|
||||||
|
import { DocDisplayMetaService } from '@affine/core/modules/doc-display-meta';
|
||||||
import { EditorService } from '@affine/core/modules/editor';
|
import { EditorService } from '@affine/core/modules/editor';
|
||||||
import { WorkspacePermissionService } from '@affine/core/modules/permissions';
|
import { WorkspacePermissionService } from '@affine/core/modules/permissions';
|
||||||
import { WorkspaceQuotaService } from '@affine/core/modules/quota';
|
import { WorkspaceQuotaService } from '@affine/core/modules/quota';
|
||||||
@@ -433,7 +433,10 @@ const PageHistoryManager = ({
|
|||||||
const editor = useService(EditorService).editor;
|
const editor = useService(EditorService).editor;
|
||||||
const [mode, setMode] = useState<DocMode>(editor.mode$.value);
|
const [mode, setMode] = useState<DocMode>(editor.mode$.value);
|
||||||
|
|
||||||
const title = useDocCollectionPageTitle(docCollection, pageId);
|
const docDisplayMetaService = useService(DocDisplayMetaService);
|
||||||
|
const i18n = useI18n();
|
||||||
|
|
||||||
|
const title = useLiveData(docDisplayMetaService.title$(pageId));
|
||||||
|
|
||||||
const onConfirmRestore = useCallback(() => {
|
const onConfirmRestore = useCallback(() => {
|
||||||
openConfirmModal({
|
openConfirmModal({
|
||||||
@@ -467,7 +470,7 @@ const PageHistoryManager = ({
|
|||||||
snapshotPage={snapshotPage}
|
snapshotPage={snapshotPage}
|
||||||
mode={mode}
|
mode={mode}
|
||||||
onModeChange={setMode}
|
onModeChange={setMode}
|
||||||
title={title}
|
title={i18n.t(title)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<PageHistoryList
|
<PageHistoryList
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import { style, type StyleRule } from '@vanilla-extract/css';
|
|||||||
|
|
||||||
export const docEditorRoot = style({
|
export const docEditorRoot = style({
|
||||||
display: 'block',
|
display: 'block',
|
||||||
background: cssVar('backgroundPrimaryColor'),
|
|
||||||
overflowX: 'clip',
|
overflowX: 'clip',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,61 +0,0 @@
|
|||||||
import { assertExists } from '@blocksuite/affine/global/utils';
|
|
||||||
import type { DocCollection } from '@blocksuite/affine/store';
|
|
||||||
import type { Atom } from 'jotai';
|
|
||||||
import { atom, useAtomValue } from 'jotai';
|
|
||||||
import { useCallback } from 'react';
|
|
||||||
|
|
||||||
import { useJournalInfoHelper } from './use-journal';
|
|
||||||
|
|
||||||
const weakMap = new WeakMap<DocCollection, Map<string, Atom<string>>>();
|
|
||||||
|
|
||||||
function getAtom(w: DocCollection, pageId: string): Atom<string> {
|
|
||||||
if (!weakMap.has(w)) {
|
|
||||||
weakMap.set(w, new Map());
|
|
||||||
}
|
|
||||||
const map = weakMap.get(w);
|
|
||||||
assertExists(map);
|
|
||||||
if (!map.has(pageId)) {
|
|
||||||
const baseAtom = atom<string>(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<string>;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @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]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,11 @@
|
|||||||
import { style } from '@vanilla-extract/css';
|
import { style } from '@vanilla-extract/css';
|
||||||
|
|
||||||
|
export const root = style({
|
||||||
|
position: 'relative',
|
||||||
|
height: '100%',
|
||||||
|
width: '100%',
|
||||||
|
});
|
||||||
|
|
||||||
export const header = style({
|
export const header = style({
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
height: '100%',
|
height: '100%',
|
||||||
@@ -24,3 +30,18 @@ export const iconButtonContainer = style({
|
|||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
gap: 10,
|
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,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
import {
|
import {
|
||||||
Divider,
|
Divider,
|
||||||
|
DragHandle,
|
||||||
type InlineEditHandle,
|
type InlineEditHandle,
|
||||||
observeResize,
|
observeResize,
|
||||||
|
useDraggable,
|
||||||
} from '@affine/component';
|
} from '@affine/component';
|
||||||
import { SharePageButton } from '@affine/core/components/affine/share-page-modal';
|
import { SharePageButton } from '@affine/core/components/affine/share-page-modal';
|
||||||
import { FavoriteButton } from '@affine/core/components/blocksuite/block-suite-header/favorite';
|
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 { BlocksuiteHeaderTitle } from '@affine/core/components/blocksuite/block-suite-header/title';
|
||||||
import { EditorModeSwitch } from '@affine/core/components/blocksuite/block-suite-mode-switch';
|
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 { 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 { HeaderDivider } from '@affine/core/components/pure/header';
|
||||||
|
import { DocDisplayMetaService } from '@affine/core/modules/doc-display-meta';
|
||||||
import { EditorService } from '@affine/core/modules/editor';
|
import { EditorService } from '@affine/core/modules/editor';
|
||||||
import { JournalService } from '@affine/core/modules/journal';
|
import { JournalService } from '@affine/core/modules/journal';
|
||||||
import { ViewIcon, ViewTitle } from '@affine/core/modules/workbench';
|
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 type { Doc } from '@blocksuite/affine/store';
|
||||||
import { useLiveData, useService, type Workspace } from '@toeverything/infra';
|
import { useLiveData, useService, type Workspace } from '@toeverything/infra';
|
||||||
import { forwardRef, useCallback, useEffect, useRef, useState } from 'react';
|
import { forwardRef, useCallback, useEffect, useRef, useState } from 'react';
|
||||||
@@ -60,7 +64,11 @@ export function JournalPageHeader({ page, workspace }: PageHeaderProps) {
|
|||||||
|
|
||||||
const { hideShare, hideToday } =
|
const { hideShare, hideToday } =
|
||||||
useDetailPageHeaderResponsive(containerWidth);
|
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 (
|
return (
|
||||||
<Header className={styles.header} ref={containerRef}>
|
<Header className={styles.header} ref={containerRef}>
|
||||||
<ViewTitle title={title} />
|
<ViewTitle title={title} />
|
||||||
@@ -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 editor = useService(EditorService).editor;
|
||||||
const currentMode = useLiveData(editor.mode$);
|
const currentMode = useLiveData(editor.mode$);
|
||||||
|
|
||||||
@@ -148,8 +159,12 @@ export function NormalPageHeader({ page, workspace }: PageHeaderProps) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function DetailPageHeader(props: PageHeaderProps) {
|
export function DetailPageHeader(
|
||||||
const { page, workspace } = props;
|
props: PageHeaderProps & {
|
||||||
|
onDragging?: (dragging: boolean) => void;
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
const { page, workspace, onDragging } = props;
|
||||||
const journalService = useService(JournalService);
|
const journalService = useService(JournalService);
|
||||||
const isJournal = !!useLiveData(journalService.journalDate$(page.id));
|
const isJournal = !!useLiveData(journalService.journalDate$(page.id));
|
||||||
const isInTrash = page.meta?.trash;
|
const isInTrash = page.meta?.trash;
|
||||||
@@ -159,9 +174,42 @@ export function DetailPageHeader(props: PageHeaderProps) {
|
|||||||
docId: page.id,
|
docId: page.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
return isJournal && !isInTrash ? (
|
const { dragRef, dragHandleRef, dragging } =
|
||||||
<JournalPageHeader {...props} />
|
useDraggable<AffineDNDData>(() => {
|
||||||
) : (
|
return {
|
||||||
<NormalPageHeader {...props} />
|
data: {
|
||||||
|
from: {
|
||||||
|
at: 'doc-detail:header',
|
||||||
|
docId: page.id,
|
||||||
|
},
|
||||||
|
entity: {
|
||||||
|
type: 'doc',
|
||||||
|
id: page.id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
disableDragPreview: true,
|
||||||
|
};
|
||||||
|
}, [page.id]);
|
||||||
|
|
||||||
|
const inner =
|
||||||
|
isJournal && !isInTrash ? (
|
||||||
|
<JournalPageHeader {...props} />
|
||||||
|
) : (
|
||||||
|
<NormalPageHeader {...props} />
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
onDragging?.(dragging);
|
||||||
|
}, [dragging, onDragging]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.root} ref={dragRef} data-dragging={dragging}>
|
||||||
|
<DragHandle
|
||||||
|
ref={dragHandleRef}
|
||||||
|
dragging={dragging}
|
||||||
|
className={styles.dragHandle}
|
||||||
|
/>
|
||||||
|
{inner}
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { cssVar } from '@toeverything/theme';
|
import { cssVar } from '@toeverything/theme';
|
||||||
|
import { cssVarV2 } from '@toeverything/theme/v2';
|
||||||
import { style } from '@vanilla-extract/css';
|
import { style } from '@vanilla-extract/css';
|
||||||
|
|
||||||
export const mainContainer = style({
|
export const mainContainer = style({
|
||||||
@@ -39,6 +40,11 @@ export const affineDocViewport = style({
|
|||||||
zIndex: -1,
|
zIndex: -1,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
selectors: {
|
||||||
|
'&[data-dragging="true"]': {
|
||||||
|
backgroundColor: cssVarV2.layer.background.hoverOverlay,
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const scrollbar = style({
|
export const scrollbar = style({
|
||||||
|
|||||||
@@ -249,10 +249,16 @@ const DetailPageImpl = memo(function DetailPageImpl() {
|
|||||||
setHasScrollTop(hasScrollTop);
|
setHasScrollTop(hasScrollTop);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const [dragging, setDragging] = useState(false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FrameworkScope scope={editor.scope}>
|
<FrameworkScope scope={editor.scope}>
|
||||||
<ViewHeader>
|
<ViewHeader>
|
||||||
<DetailPageHeader page={doc.blockSuiteDoc} workspace={workspace} />
|
<DetailPageHeader
|
||||||
|
page={doc.blockSuiteDoc}
|
||||||
|
workspace={workspace}
|
||||||
|
onDragging={setDragging}
|
||||||
|
/>
|
||||||
</ViewHeader>
|
</ViewHeader>
|
||||||
<ViewBody>
|
<ViewBody>
|
||||||
<div
|
<div
|
||||||
@@ -267,6 +273,7 @@ const DetailPageImpl = memo(function DetailPageImpl() {
|
|||||||
<Scrollable.Viewport
|
<Scrollable.Viewport
|
||||||
onScroll={handleScroll}
|
onScroll={handleScroll}
|
||||||
ref={scrollViewportRef}
|
ref={scrollViewportRef}
|
||||||
|
data-dragging={dragging}
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'affine-page-viewport',
|
'affine-page-viewport',
|
||||||
styles.affineDocViewport,
|
styles.affineDocViewport,
|
||||||
|
|||||||
@@ -75,6 +75,10 @@ export interface AffineDNDData extends DNDData {
|
|||||||
at: 'doc-property:manager';
|
at: 'doc-property:manager';
|
||||||
workspaceId: string;
|
workspaceId: string;
|
||||||
}
|
}
|
||||||
|
| {
|
||||||
|
at: 'doc-detail:header';
|
||||||
|
docId: string;
|
||||||
|
}
|
||||||
| {
|
| {
|
||||||
at: 'external'; // for blocksuite or external apps
|
at: 'external'; // for blocksuite or external apps
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user