mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-15 05:37:32 +00:00
@@ -43,3 +43,8 @@ export const hiddenInput = style({
|
||||
height: '0',
|
||||
position: 'absolute',
|
||||
});
|
||||
|
||||
export const timeRow = style({
|
||||
marginTop: 20,
|
||||
borderBottom: 4,
|
||||
});
|
||||
|
||||
@@ -78,7 +78,7 @@ export const InfoModal = ({
|
||||
);
|
||||
};
|
||||
|
||||
const InfoTable = ({
|
||||
export const InfoTable = ({
|
||||
onClose,
|
||||
docId,
|
||||
readonly,
|
||||
@@ -106,8 +106,8 @@ const InfoTable = ({
|
||||
);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<TimeRow docId={docId} />
|
||||
<div className={styles.container}>
|
||||
<TimeRow className={styles.timeRow} docId={docId} />
|
||||
<Divider size="thinner" />
|
||||
{backlinks && backlinks.length > 0 ? (
|
||||
<>
|
||||
|
||||
@@ -8,15 +8,17 @@ import * as styles from './links-row.css';
|
||||
export const LinksRow = ({
|
||||
references,
|
||||
label,
|
||||
className,
|
||||
onClick,
|
||||
}: {
|
||||
references: Backlink[] | Link[];
|
||||
label: string;
|
||||
className?: string;
|
||||
onClick?: () => void;
|
||||
}) => {
|
||||
const manager = useContext(managerContext);
|
||||
return (
|
||||
<div>
|
||||
<div className={className}>
|
||||
<div className={styles.title}>
|
||||
{label} · {references.length}
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { cssVar } from '@toeverything/theme';
|
||||
import { style } from '@vanilla-extract/css';
|
||||
import { fallbackVar, style } from '@vanilla-extract/css';
|
||||
|
||||
import { rowHPadding } from '../styles.css';
|
||||
|
||||
export const icon = style({
|
||||
fontSize: 16,
|
||||
@@ -65,7 +67,7 @@ export const rowValueCell = style({
|
||||
':hover': {
|
||||
backgroundColor: cssVar('hoverColor'),
|
||||
},
|
||||
padding: '6px 8px',
|
||||
padding: `6px ${fallbackVar(rowHPadding, '8px')} 6px 8px`,
|
||||
border: `1px solid transparent`,
|
||||
color: cssVar('textPrimaryColor'),
|
||||
':focus': {
|
||||
|
||||
@@ -11,16 +11,21 @@ import * as styles from './tags-row.css';
|
||||
export const TagsRow = ({
|
||||
docId,
|
||||
readonly,
|
||||
className,
|
||||
}: {
|
||||
docId: string;
|
||||
readonly: boolean;
|
||||
className?: string;
|
||||
}) => {
|
||||
const t = useI18n();
|
||||
const tagList = useService(TagService).tagList;
|
||||
const tagIds = useLiveData(tagList.tagIdsByPageId$(docId));
|
||||
const empty = !tagIds || tagIds.length === 0;
|
||||
return (
|
||||
<div className={styles.rowCell} data-testid="info-modal-tags-row">
|
||||
<div
|
||||
className={clsx(styles.rowCell, className)}
|
||||
data-testid="info-modal-tags-row"
|
||||
>
|
||||
<div className={styles.rowNameContainer}>
|
||||
<div className={styles.icon}>
|
||||
<TagsIcon />
|
||||
|
||||
@@ -46,6 +46,4 @@ export const rowCell = style({
|
||||
export const container = style({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
marginTop: 20,
|
||||
marginBottom: 4,
|
||||
});
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { i18nTime, useI18n } from '@affine/i18n';
|
||||
import { DateTimeIcon, HistoryIcon } from '@blocksuite/icons/rc';
|
||||
import { useLiveData, useService, WorkspaceService } from '@toeverything/infra';
|
||||
import clsx from 'clsx';
|
||||
import type { ConfigType } from 'dayjs';
|
||||
import { useDebouncedValue } from 'foxact/use-debounced-value';
|
||||
import { type ReactNode, useContext, useMemo } from 'react';
|
||||
@@ -28,7 +29,13 @@ const RowComponent = ({
|
||||
);
|
||||
};
|
||||
|
||||
export const TimeRow = ({ docId }: { docId: string }) => {
|
||||
export const TimeRow = ({
|
||||
docId,
|
||||
className,
|
||||
}: {
|
||||
docId: string;
|
||||
className?: string;
|
||||
}) => {
|
||||
const t = useI18n();
|
||||
const manager = useContext(managerContext);
|
||||
const workspaceService = useService(WorkspaceService);
|
||||
@@ -88,5 +95,7 @@ export const TimeRow = ({ docId }: { docId: string }) => {
|
||||
|
||||
const dTimestampElement = useDebouncedValue(timestampElement, 500);
|
||||
|
||||
return <div className={styles.container}>{dTimestampElement}</div>;
|
||||
return (
|
||||
<div className={clsx(styles.container, className)}>{dTimestampElement}</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -2,6 +2,8 @@ import { cssVar } from '@toeverything/theme';
|
||||
import { createVar, globalStyle, style } from '@vanilla-extract/css';
|
||||
|
||||
const propertyNameCellWidth = createVar();
|
||||
export const rowHPadding = createVar();
|
||||
export const fontSize = createVar();
|
||||
|
||||
export const root = style({
|
||||
display: 'flex',
|
||||
@@ -10,6 +12,16 @@ export const root = style({
|
||||
fontFamily: cssVar('fontSansFamily'),
|
||||
vars: {
|
||||
[propertyNameCellWidth]: '160px',
|
||||
[rowHPadding]: '6px',
|
||||
[fontSize]: cssVar('fontSm'),
|
||||
},
|
||||
'@container': {
|
||||
[`viewport (width <= 640px)`]: {
|
||||
vars: {
|
||||
[rowHPadding]: '0px',
|
||||
[fontSize]: cssVar('fontXs'),
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -22,7 +34,7 @@ export const rootCentered = style({
|
||||
padding: `0 ${cssVar('editorSidePadding', '24px')}`,
|
||||
'@container': {
|
||||
[`viewport (width <= 640px)`]: {
|
||||
padding: '0 24px',
|
||||
padding: '0 16px',
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -39,7 +51,7 @@ export const tableHeaderInfoRow = style({
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
color: cssVar('textSecondaryColor'),
|
||||
fontSize: cssVar('fontSm'),
|
||||
fontSize: fontSize,
|
||||
fontWeight: 500,
|
||||
minHeight: 34,
|
||||
'@media': {
|
||||
@@ -54,9 +66,9 @@ export const tableHeaderSecondaryRow = style({
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
color: cssVar('textPrimaryColor'),
|
||||
fontSize: cssVar('fontSm'),
|
||||
fontSize: fontSize,
|
||||
fontWeight: 500,
|
||||
padding: '0 6px',
|
||||
padding: `0 ${rowHPadding}`,
|
||||
gap: '8px',
|
||||
height: 24,
|
||||
'@media': {
|
||||
@@ -82,7 +94,7 @@ export const spacer = style({
|
||||
});
|
||||
|
||||
export const tableHeaderBacklinksHint = style({
|
||||
padding: '6px',
|
||||
padding: `0 ${rowHPadding}`,
|
||||
cursor: 'pointer',
|
||||
borderRadius: '4px',
|
||||
':hover': {
|
||||
@@ -103,7 +115,7 @@ export const tableHeaderTimestamp = style({
|
||||
alignItems: 'start',
|
||||
gap: '8px',
|
||||
cursor: 'default',
|
||||
padding: '0 6px',
|
||||
padding: `0 ${rowHPadding}`,
|
||||
});
|
||||
|
||||
export const tableHeaderDivider = style({
|
||||
@@ -273,7 +285,7 @@ export const editablePropertyRowCell = style([
|
||||
export const propertyRowNameCell = style([
|
||||
propertyRowCell,
|
||||
{
|
||||
padding: 6,
|
||||
padding: `6px ${rowHPadding}`,
|
||||
flexShrink: 0,
|
||||
color: cssVar('textSecondaryColor'),
|
||||
width: propertyNameCellWidth,
|
||||
@@ -316,7 +328,7 @@ export const propertyRowValueCell = style([
|
||||
propertyRowCell,
|
||||
editablePropertyRowCell,
|
||||
{
|
||||
padding: '6px 8px',
|
||||
padding: `6px ${rowHPadding} 6px 6px`,
|
||||
border: `1px solid transparent`,
|
||||
color: cssVar('textPrimaryColor'),
|
||||
':focus': {
|
||||
@@ -353,7 +365,7 @@ export const propertyRowValueTextarea = style([
|
||||
propertyRowValueCell,
|
||||
{
|
||||
border: 'none',
|
||||
padding: '6px 8px',
|
||||
padding: `6px ${rowHPadding} 6px 8px`,
|
||||
height: '100%',
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
@@ -368,7 +380,7 @@ export const propertyRowValueTextareaInvisible = style([
|
||||
propertyRowValueCell,
|
||||
{
|
||||
border: 'none',
|
||||
padding: '6px 8px',
|
||||
padding: `6px ${rowHPadding} 6px 8px`,
|
||||
visibility: 'hidden',
|
||||
whiteSpace: 'break-spaces',
|
||||
wordBreak: 'break-all',
|
||||
@@ -379,7 +391,7 @@ export const propertyRowValueTextareaInvisible = style([
|
||||
export const propertyRowValueNumberCell = style([
|
||||
propertyRowValueTextCell,
|
||||
{
|
||||
padding: '6px 8px',
|
||||
padding: `6px ${rowHPadding} 6px 8px`,
|
||||
},
|
||||
]);
|
||||
|
||||
|
||||
@@ -191,7 +191,7 @@ export const BlocksuiteDocEditor = forwardRef<
|
||||
) : (
|
||||
<BlocksuiteEditorJournalDocTitle page={page} />
|
||||
)}
|
||||
<PagePropertiesTable docId={page.id} />
|
||||
{!shared ? <PagePropertiesTable docId={page.id} /> : null}
|
||||
<adapted.DocEditor
|
||||
className={styles.docContainer}
|
||||
ref={onDocRef}
|
||||
|
||||
@@ -0,0 +1,136 @@
|
||||
import { PageDetailSkeleton } from '@affine/component/page-detail-skeleton';
|
||||
import type { Editor } from '@affine/core/modules/editor';
|
||||
import { EditorsService } from '@affine/core/modules/editor';
|
||||
import { ViewService } from '@affine/core/modules/workbench/services/view';
|
||||
import type { DocMode } from '@blocksuite/blocks';
|
||||
import type { Doc } from '@toeverything/infra';
|
||||
import {
|
||||
DocsService,
|
||||
FrameworkScope,
|
||||
useLiveData,
|
||||
useService,
|
||||
WorkspaceService,
|
||||
} from '@toeverything/infra';
|
||||
import {
|
||||
type PropsWithChildren,
|
||||
useEffect,
|
||||
useLayoutEffect,
|
||||
useState,
|
||||
} from 'react';
|
||||
|
||||
import { PageNotFound } from '../../404';
|
||||
|
||||
const useLoadDoc = (pageId: string) => {
|
||||
const currentWorkspace = useService(WorkspaceService).workspace;
|
||||
const docsService = useService(DocsService);
|
||||
const docRecordList = docsService.list;
|
||||
const docListReady = useLiveData(docRecordList.isReady$);
|
||||
const docRecord = useLiveData(docRecordList.doc$(pageId));
|
||||
const viewService = useService(ViewService);
|
||||
|
||||
const queryString = useLiveData(
|
||||
viewService.view.queryString$<{
|
||||
mode?: string;
|
||||
}>()
|
||||
);
|
||||
|
||||
const queryStringMode =
|
||||
queryString.mode && ['edgeless', 'page'].includes(queryString.mode)
|
||||
? (queryString.mode as DocMode)
|
||||
: null;
|
||||
|
||||
// We only read the querystring mode when entering, so use useState here.
|
||||
const [initialQueryStringMode] = useState(() => queryStringMode);
|
||||
|
||||
const [doc, setDoc] = useState<Doc | null>(null);
|
||||
const [editor, setEditor] = useState<Editor | null>(null);
|
||||
const editorMode = useLiveData(editor?.mode$);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (!docRecord) {
|
||||
return;
|
||||
}
|
||||
const { doc: opened, release } = docsService.open(pageId);
|
||||
setDoc(opened);
|
||||
return () => {
|
||||
release();
|
||||
};
|
||||
}, [docRecord, docsService, pageId]);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (!doc) {
|
||||
return;
|
||||
}
|
||||
const editor = doc.scope
|
||||
.get(EditorsService)
|
||||
.createEditor(initialQueryStringMode || doc.getPrimaryMode() || 'page');
|
||||
setEditor(editor);
|
||||
return () => {
|
||||
editor.dispose();
|
||||
};
|
||||
}, [doc, initialQueryStringMode]);
|
||||
|
||||
// update editor mode to queryString
|
||||
useEffect(() => {
|
||||
if (editorMode) {
|
||||
viewService.view.updateQueryString(
|
||||
{
|
||||
mode: editorMode,
|
||||
},
|
||||
{
|
||||
replace: true,
|
||||
}
|
||||
);
|
||||
}
|
||||
}, [editorMode, viewService.view]);
|
||||
|
||||
// set sync engine priority target
|
||||
useEffect(() => {
|
||||
currentWorkspace.engine.doc.setPriority(pageId, 10);
|
||||
return () => {
|
||||
currentWorkspace.engine.doc.setPriority(pageId, 5);
|
||||
};
|
||||
}, [currentWorkspace, pageId]);
|
||||
|
||||
const isInTrash = useLiveData(doc?.meta$.map(meta => meta.trash));
|
||||
|
||||
useEffect(() => {
|
||||
if (doc && isInTrash) {
|
||||
currentWorkspace.docCollection.awarenessStore.setReadonly(
|
||||
doc.blockSuiteDoc.blockCollection,
|
||||
true
|
||||
);
|
||||
}
|
||||
}, [currentWorkspace.docCollection.awarenessStore, doc, isInTrash]);
|
||||
|
||||
return {
|
||||
doc,
|
||||
editor,
|
||||
docListReady,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* A common wrapper for detail page for both mobile and desktop page.
|
||||
* It only contains the logic for page loading, context setup, but not the page content.
|
||||
*/
|
||||
export const DetailPageWrapper = ({
|
||||
pageId,
|
||||
children,
|
||||
}: PropsWithChildren<{ pageId: string }>) => {
|
||||
const { doc, editor, docListReady } = useLoadDoc(pageId);
|
||||
// if sync engine has been synced and the page is null, show 404 page.
|
||||
if (docListReady && !doc) {
|
||||
return <PageNotFound noPermission />;
|
||||
}
|
||||
|
||||
if (!doc || !editor) {
|
||||
return <PageDetailSkeleton key="current-page-is-null" />;
|
||||
}
|
||||
|
||||
return (
|
||||
<FrameworkScope scope={doc.scope}>
|
||||
<FrameworkScope scope={editor.scope}>{children}</FrameworkScope>
|
||||
</FrameworkScope>
|
||||
);
|
||||
};
|
||||
@@ -1,16 +1,14 @@
|
||||
import { Scrollable, useHasScrollTop } from '@affine/component';
|
||||
import { PageDetailSkeleton } from '@affine/component/page-detail-skeleton';
|
||||
import type { ChatPanel } from '@affine/core/blocksuite/presets/ai';
|
||||
import { AIProvider } from '@affine/core/blocksuite/presets/ai';
|
||||
import { PageAIOnboarding } from '@affine/core/components/affine/ai-onboarding';
|
||||
import { EditorOutlineViewer } from '@affine/core/components/blocksuite/outline-viewer';
|
||||
import { useAppSettingHelper } from '@affine/core/hooks/affine/use-app-setting-helper';
|
||||
import { useDocMetaHelper } from '@affine/core/hooks/use-block-suite-page-meta';
|
||||
import type { Editor } from '@affine/core/modules/editor';
|
||||
import { EditorService, EditorsService } from '@affine/core/modules/editor';
|
||||
import { EditorService } from '@affine/core/modules/editor';
|
||||
import { RecentDocsService } from '@affine/core/modules/quicksearch';
|
||||
import { ViewService } from '@affine/core/modules/workbench/services/view';
|
||||
import type { DocMode, PageRootService } from '@blocksuite/blocks';
|
||||
import type { PageRootService } from '@blocksuite/blocks';
|
||||
import {
|
||||
BookmarkBlockService,
|
||||
customImageProxyMiddleware,
|
||||
@@ -23,10 +21,8 @@ import { DisposableGroup } from '@blocksuite/global/utils';
|
||||
import { AiIcon, FrameIcon, TocIcon, TodayIcon } from '@blocksuite/icons/rc';
|
||||
import { type AffineEditorContainer } from '@blocksuite/presets';
|
||||
import type { Doc as BlockSuiteDoc } from '@blocksuite/store';
|
||||
import type { Doc } from '@toeverything/infra';
|
||||
import {
|
||||
DocService,
|
||||
DocsService,
|
||||
FrameworkScope,
|
||||
globalBlockSuiteSchema,
|
||||
GlobalContextService,
|
||||
@@ -36,15 +32,7 @@ import {
|
||||
WorkspaceService,
|
||||
} from '@toeverything/infra';
|
||||
import clsx from 'clsx';
|
||||
import type { ReactElement } from 'react';
|
||||
import {
|
||||
memo,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useLayoutEffect,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { memo, useCallback, useEffect, useRef } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import type { Map as YMap } from 'yjs';
|
||||
|
||||
@@ -65,9 +53,9 @@ import {
|
||||
WorkbenchService,
|
||||
} from '../../../modules/workbench';
|
||||
import { performanceRenderLogger } from '../../../shared';
|
||||
import { PageNotFound } from '../../404';
|
||||
import * as styles from './detail-page.css';
|
||||
import { DetailPageHeader } from './detail-page-header';
|
||||
import { DetailPageWrapper } from './detail-page-wrapper';
|
||||
import { EditorChatPanel } from './tabs/chat';
|
||||
import { EditorFramePanel } from './tabs/frame';
|
||||
import { EditorJournalPanel } from './tabs/journal';
|
||||
@@ -330,107 +318,6 @@ const DetailPageImpl = memo(function DetailPageImpl() {
|
||||
);
|
||||
});
|
||||
|
||||
export const DetailPage = ({ pageId }: { pageId: string }): ReactElement => {
|
||||
const currentWorkspace = useService(WorkspaceService).workspace;
|
||||
const docsService = useService(DocsService);
|
||||
const docRecordList = docsService.list;
|
||||
const docListReady = useLiveData(docRecordList.isReady$);
|
||||
const docRecord = useLiveData(docRecordList.doc$(pageId));
|
||||
const viewService = useService(ViewService);
|
||||
|
||||
const queryString = useLiveData(
|
||||
viewService.view.queryString$<{
|
||||
mode?: string;
|
||||
}>()
|
||||
);
|
||||
|
||||
const queryStringMode =
|
||||
queryString.mode && ['edgeless', 'page'].includes(queryString.mode)
|
||||
? (queryString.mode as DocMode)
|
||||
: null;
|
||||
|
||||
// We only read the querystring mode when entering, so use useState here.
|
||||
const [initialQueryStringMode] = useState(() => queryStringMode);
|
||||
|
||||
const [doc, setDoc] = useState<Doc | null>(null);
|
||||
const [editor, setEditor] = useState<Editor | null>(null);
|
||||
const editorMode = useLiveData(editor?.mode$);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (!docRecord) {
|
||||
return;
|
||||
}
|
||||
const { doc: opened, release } = docsService.open(pageId);
|
||||
setDoc(opened);
|
||||
return () => {
|
||||
release();
|
||||
};
|
||||
}, [docRecord, docsService, pageId]);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (!doc) {
|
||||
return;
|
||||
}
|
||||
const editor = doc.scope
|
||||
.get(EditorsService)
|
||||
.createEditor(initialQueryStringMode || doc.getPrimaryMode() || 'page');
|
||||
setEditor(editor);
|
||||
return () => {
|
||||
editor.dispose();
|
||||
};
|
||||
}, [doc, initialQueryStringMode]);
|
||||
|
||||
// update editor mode to queryString
|
||||
useEffect(() => {
|
||||
if (editorMode) {
|
||||
viewService.view.updateQueryString(
|
||||
{
|
||||
mode: editorMode,
|
||||
},
|
||||
{
|
||||
replace: true,
|
||||
}
|
||||
);
|
||||
}
|
||||
}, [editorMode, viewService.view]);
|
||||
|
||||
// set sync engine priority target
|
||||
useEffect(() => {
|
||||
currentWorkspace.engine.doc.setPriority(pageId, 10);
|
||||
return () => {
|
||||
currentWorkspace.engine.doc.setPriority(pageId, 5);
|
||||
};
|
||||
}, [currentWorkspace, pageId]);
|
||||
|
||||
const isInTrash = useLiveData(doc?.meta$.map(meta => meta.trash));
|
||||
|
||||
useEffect(() => {
|
||||
if (doc && isInTrash) {
|
||||
currentWorkspace.docCollection.awarenessStore.setReadonly(
|
||||
doc.blockSuiteDoc.blockCollection,
|
||||
true
|
||||
);
|
||||
}
|
||||
}, [currentWorkspace.docCollection.awarenessStore, doc, isInTrash]);
|
||||
|
||||
// if sync engine has been synced and the page is null, show 404 page.
|
||||
if (docListReady && !doc) {
|
||||
return <PageNotFound noPermission />;
|
||||
}
|
||||
|
||||
if (!doc || !editor) {
|
||||
return <PageDetailSkeleton key="current-page-is-null" />;
|
||||
}
|
||||
|
||||
return (
|
||||
<FrameworkScope scope={doc.scope}>
|
||||
<FrameworkScope scope={editor.scope}>
|
||||
<DetailPageImpl />
|
||||
</FrameworkScope>
|
||||
</FrameworkScope>
|
||||
);
|
||||
};
|
||||
|
||||
export const Component = () => {
|
||||
performanceRenderLogger.debug('DetailPage');
|
||||
|
||||
@@ -448,5 +335,9 @@ export const Component = () => {
|
||||
|
||||
const pageId = params.pageId;
|
||||
|
||||
return pageId ? <DetailPage pageId={pageId} /> : null;
|
||||
return pageId ? (
|
||||
<DetailPageWrapper pageId={pageId}>
|
||||
<DetailPageImpl />
|
||||
</DetailPageWrapper>
|
||||
) : null;
|
||||
};
|
||||
|
||||
@@ -4,6 +4,10 @@ export function stopPropagation(event: BaseSyntheticEvent) {
|
||||
event.stopPropagation();
|
||||
}
|
||||
|
||||
export function preventDefault(event: BaseSyntheticEvent) {
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
export function stopEvent(event: BaseSyntheticEvent) {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
|
||||
Reference in New Issue
Block a user