mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-19 02:56:23 +08:00
+5
@@ -43,3 +43,8 @@ export const hiddenInput = style({
|
||||
height: '0',
|
||||
position: 'absolute',
|
||||
});
|
||||
|
||||
export const timeRow = style({
|
||||
marginTop: 20,
|
||||
borderBottom: 4,
|
||||
});
|
||||
|
||||
+3
-3
@@ -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 ? (
|
||||
<>
|
||||
|
||||
+3
-1
@@ -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>
|
||||
|
||||
+4
-2
@@ -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': {
|
||||
|
||||
+6
-1
@@ -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 />
|
||||
|
||||
-2
@@ -46,6 +46,4 @@ export const rowCell = style({
|
||||
export const container = style({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
marginTop: 20,
|
||||
marginBottom: 4,
|
||||
});
|
||||
|
||||
+11
-2
@@ -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();
|
||||
|
||||
@@ -45,5 +45,5 @@ export const prefix = style({
|
||||
export const suffix = style({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 18,
|
||||
gap: 6,
|
||||
});
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
import { PageHeader } from '../../components/page-header';
|
||||
|
||||
export const Component = () => {
|
||||
return (
|
||||
<>
|
||||
<PageHeader back />
|
||||
<div>/workspace/:workspaceId/:pageId</div>;
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,43 @@
|
||||
import { IconButton, MobileMenu } from '@affine/component';
|
||||
import { useJournalInfoHelper } from '@affine/core/hooks/use-journal';
|
||||
import { EditorJournalPanel } from '@affine/core/pages/workspace/detail-page/tabs/journal';
|
||||
import { TodayIcon, TomorrowIcon, YesterdayIcon } from '@blocksuite/icons/rc';
|
||||
import { useService, WorkspaceService } from '@toeverything/infra';
|
||||
|
||||
export const JournalIconButton = ({
|
||||
docId,
|
||||
className,
|
||||
}: {
|
||||
docId: string;
|
||||
className?: string;
|
||||
}) => {
|
||||
const workspace = useService(WorkspaceService).workspace;
|
||||
const { journalDate, isJournal } = useJournalInfoHelper(
|
||||
workspace.docCollection,
|
||||
docId
|
||||
);
|
||||
const Icon = journalDate
|
||||
? journalDate.isBefore(new Date(), 'day')
|
||||
? YesterdayIcon
|
||||
: journalDate.isAfter(new Date(), 'day')
|
||||
? TomorrowIcon
|
||||
: TodayIcon
|
||||
: TodayIcon;
|
||||
|
||||
if (!isJournal) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<MobileMenu
|
||||
items={<EditorJournalPanel />}
|
||||
contentOptions={{
|
||||
align: 'center',
|
||||
}}
|
||||
>
|
||||
<IconButton className={className} size={24}>
|
||||
<Icon />
|
||||
</IconButton>
|
||||
</MobileMenu>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,91 @@
|
||||
import { cssVar } from '@toeverything/theme';
|
||||
import { cssVarV2 } from '@toeverything/theme/v2';
|
||||
import { globalStyle, style } from '@vanilla-extract/css';
|
||||
|
||||
export const root = style({
|
||||
background: cssVarV2('layer/background/primary'),
|
||||
minHeight: '100vh',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
});
|
||||
|
||||
export const header = style({
|
||||
background: cssVarV2('layer/background/primary'),
|
||||
position: 'sticky',
|
||||
top: 0,
|
||||
zIndex: 1,
|
||||
});
|
||||
|
||||
export const mainContainer = style({
|
||||
containerType: 'inline-size',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
flex: 1,
|
||||
overflow: 'hidden',
|
||||
borderTop: `0.5px solid transparent`,
|
||||
transition: 'border-color 0.2s',
|
||||
selectors: {
|
||||
'&[data-dynamic-top-border="false"]': {
|
||||
borderColor: cssVar('borderColor'),
|
||||
},
|
||||
'&[data-has-scroll-top="true"]': {
|
||||
borderColor: cssVar('borderColor'),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const editorContainer = style({
|
||||
position: 'relative',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
flex: 1,
|
||||
zIndex: 0,
|
||||
});
|
||||
// brings styles of .affine-page-viewport from blocksuite
|
||||
export const affineDocViewport = style({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
containerName: 'viewport',
|
||||
containerType: 'inline-size',
|
||||
background: cssVarV2('layer/background/primary'),
|
||||
selectors: {
|
||||
'&[data-mode="edgeless"]': {
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const scrollbar = style({
|
||||
marginRight: '4px',
|
||||
});
|
||||
|
||||
globalStyle('.doc-title-container', {
|
||||
fontSize: cssVar('fontH1'),
|
||||
'@container': {
|
||||
[`viewport (width <= 640px)`]: {
|
||||
padding: '10px 16px',
|
||||
lineHeight: '38px',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
globalStyle('.affine-page-root-block-container', {
|
||||
'@container': {
|
||||
[`viewport (width <= 640px)`]: {
|
||||
paddingLeft: 16,
|
||||
paddingRight: 16,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const journalIconButton = style({
|
||||
position: 'absolute',
|
||||
zIndex: 1,
|
||||
top: 16,
|
||||
right: 12,
|
||||
display: 'flex',
|
||||
});
|
||||
@@ -0,0 +1,225 @@
|
||||
import { IconButton } from '@affine/component';
|
||||
import { AffineErrorBoundary } from '@affine/core/components/affine/affine-error-boundary';
|
||||
import { PageDetailEditor } from '@affine/core/components/page-detail-editor';
|
||||
import { useRegisterBlocksuiteEditorCommands } from '@affine/core/hooks/affine/use-register-blocksuite-editor-commands';
|
||||
import { useActiveBlocksuiteEditor } from '@affine/core/hooks/use-block-suite-editor';
|
||||
import { useDocMetaHelper } from '@affine/core/hooks/use-block-suite-page-meta';
|
||||
import { usePageDocumentTitle } from '@affine/core/hooks/use-global-state';
|
||||
import { useNavigateHelper } from '@affine/core/hooks/use-navigate-helper';
|
||||
import { EditorService } from '@affine/core/modules/editor';
|
||||
import { WorkbenchService } from '@affine/core/modules/workbench';
|
||||
import { ViewService } from '@affine/core/modules/workbench/services/view';
|
||||
import { DetailPageWrapper } from '@affine/core/pages/workspace/detail-page/detail-page-wrapper';
|
||||
import type { PageRootService } from '@blocksuite/blocks';
|
||||
import {
|
||||
BookmarkBlockService,
|
||||
customImageProxyMiddleware,
|
||||
EmbedGithubBlockService,
|
||||
EmbedLoomBlockService,
|
||||
EmbedYoutubeBlockService,
|
||||
ImageBlockService,
|
||||
} from '@blocksuite/blocks';
|
||||
import { DisposableGroup } from '@blocksuite/global/utils';
|
||||
import { ShareIcon } from '@blocksuite/icons/rc';
|
||||
import { type AffineEditorContainer } from '@blocksuite/presets';
|
||||
import type { Doc as BlockSuiteDoc } from '@blocksuite/store';
|
||||
import {
|
||||
DocService,
|
||||
FrameworkScope,
|
||||
GlobalContextService,
|
||||
useLiveData,
|
||||
useServices,
|
||||
WorkspaceService,
|
||||
} from '@toeverything/infra';
|
||||
import clsx from 'clsx';
|
||||
import { useCallback, useEffect } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
|
||||
import { PageHeader } from '../../../components';
|
||||
import { JournalIconButton } from './journal-icon-button';
|
||||
import * as styles from './mobile-detail-page.css';
|
||||
import { PageHeaderMenuButton } from './page-header-more-button';
|
||||
|
||||
const DetailPageImpl = () => {
|
||||
const { editorService, docService, workspaceService, globalContextService } =
|
||||
useServices({
|
||||
WorkbenchService,
|
||||
ViewService,
|
||||
EditorService,
|
||||
DocService,
|
||||
WorkspaceService,
|
||||
GlobalContextService,
|
||||
});
|
||||
const editor = editorService.editor;
|
||||
const workspace = workspaceService.workspace;
|
||||
const docCollection = workspace.docCollection;
|
||||
const globalContext = globalContextService.globalContext;
|
||||
const doc = docService.doc;
|
||||
|
||||
const mode = useLiveData(editor.mode$);
|
||||
|
||||
const isInTrash = useLiveData(doc.meta$.map(meta => meta.trash));
|
||||
const { openPage, jumpToPageBlock, jumpToTag } = useNavigateHelper();
|
||||
const editorContainer = useLiveData(editor.editorContainer$);
|
||||
|
||||
const { setDocReadonly } = useDocMetaHelper(workspace.docCollection);
|
||||
|
||||
// TODO(@eyhn): remove jotai here
|
||||
const [_, setActiveBlockSuiteEditor] = useActiveBlocksuiteEditor();
|
||||
|
||||
useEffect(() => {
|
||||
setActiveBlockSuiteEditor(editorContainer);
|
||||
}, [editorContainer, setActiveBlockSuiteEditor]);
|
||||
|
||||
useEffect(() => {
|
||||
globalContext.docId.set(doc.id);
|
||||
globalContext.isDoc.set(true);
|
||||
|
||||
return () => {
|
||||
globalContext.docId.set(null);
|
||||
globalContext.isDoc.set(false);
|
||||
};
|
||||
}, [doc, globalContext]);
|
||||
|
||||
useEffect(() => {
|
||||
globalContext.docMode.set(mode);
|
||||
|
||||
return () => {
|
||||
globalContext.docMode.set(null);
|
||||
};
|
||||
}, [doc, globalContext, mode]);
|
||||
|
||||
useEffect(() => {
|
||||
setDocReadonly(doc.id, true);
|
||||
}, [doc.id, setDocReadonly]);
|
||||
|
||||
useEffect(() => {
|
||||
globalContext.isTrashDoc.set(!!isInTrash);
|
||||
|
||||
return () => {
|
||||
globalContext.isTrashDoc.set(null);
|
||||
};
|
||||
}, [globalContext, isInTrash]);
|
||||
|
||||
useRegisterBlocksuiteEditorCommands(editor);
|
||||
const title = useLiveData(doc.title$);
|
||||
usePageDocumentTitle(title);
|
||||
|
||||
const onLoad = useCallback(
|
||||
(_bsPage: BlockSuiteDoc, editorContainer: AffineEditorContainer) => {
|
||||
// blocksuite editor host
|
||||
const editorHost = editorContainer.host;
|
||||
|
||||
// provide image proxy endpoint to blocksuite
|
||||
editorHost?.std.clipboard.use(
|
||||
customImageProxyMiddleware(runtimeConfig.imageProxyUrl)
|
||||
);
|
||||
ImageBlockService.setImageProxyURL(runtimeConfig.imageProxyUrl);
|
||||
|
||||
// provide link preview endpoint to blocksuite
|
||||
BookmarkBlockService.setLinkPreviewEndpoint(runtimeConfig.linkPreviewUrl);
|
||||
EmbedGithubBlockService.setLinkPreviewEndpoint(
|
||||
runtimeConfig.linkPreviewUrl
|
||||
);
|
||||
EmbedYoutubeBlockService.setLinkPreviewEndpoint(
|
||||
runtimeConfig.linkPreviewUrl
|
||||
);
|
||||
EmbedLoomBlockService.setLinkPreviewEndpoint(
|
||||
runtimeConfig.linkPreviewUrl
|
||||
);
|
||||
|
||||
// provide page mode and updated date to blocksuite
|
||||
const pageService =
|
||||
editorHost?.std.spec.getService<PageRootService>('affine:page');
|
||||
const disposable = new DisposableGroup();
|
||||
if (pageService) {
|
||||
disposable.add(
|
||||
pageService.slots.docLinkClicked.on(({ docId, blockId }) => {
|
||||
return blockId
|
||||
? jumpToPageBlock(docCollection.id, docId, blockId)
|
||||
: openPage(docCollection.id, docId);
|
||||
})
|
||||
);
|
||||
disposable.add(
|
||||
pageService.slots.tagClicked.on(({ tagId }) => {
|
||||
jumpToTag(workspace.id, tagId);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
editor.setEditorContainer(editorContainer);
|
||||
|
||||
return () => {
|
||||
disposable.dispose();
|
||||
};
|
||||
},
|
||||
[
|
||||
editor,
|
||||
jumpToPageBlock,
|
||||
docCollection.id,
|
||||
openPage,
|
||||
jumpToTag,
|
||||
workspace.id,
|
||||
]
|
||||
);
|
||||
|
||||
return (
|
||||
<FrameworkScope scope={editor.scope}>
|
||||
<div className={styles.mainContainer}>
|
||||
<div
|
||||
data-mode={mode}
|
||||
className={clsx(
|
||||
'affine-page-viewport',
|
||||
styles.affineDocViewport,
|
||||
styles.editorContainer
|
||||
)}
|
||||
>
|
||||
{/* Add a key to force rerender when page changed, to avoid error boundary persisting. */}
|
||||
<AffineErrorBoundary key={doc.id}>
|
||||
<JournalIconButton
|
||||
docId={doc.id}
|
||||
className={styles.journalIconButton}
|
||||
/>
|
||||
<PageDetailEditor
|
||||
pageId={doc.id}
|
||||
onLoad={onLoad}
|
||||
docCollection={docCollection}
|
||||
/>
|
||||
</AffineErrorBoundary>
|
||||
</div>
|
||||
</div>
|
||||
</FrameworkScope>
|
||||
);
|
||||
};
|
||||
|
||||
export const Component = () => {
|
||||
const params = useParams();
|
||||
const pageId = params.pageId;
|
||||
|
||||
if (!pageId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.root}>
|
||||
<DetailPageWrapper pageId={pageId}>
|
||||
<PageHeader
|
||||
back
|
||||
className={styles.header}
|
||||
suffix={
|
||||
<>
|
||||
<IconButton
|
||||
size={24}
|
||||
style={{ padding: 10 }}
|
||||
onClick={console.log}
|
||||
icon={<ShareIcon />}
|
||||
/>
|
||||
<PageHeaderMenuButton docId={pageId} />
|
||||
</>
|
||||
}
|
||||
/>
|
||||
<DetailPageImpl />
|
||||
</DetailPageWrapper>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,16 @@
|
||||
import { cssVar } from '@toeverything/theme';
|
||||
import { style } from '@vanilla-extract/css';
|
||||
|
||||
export const iconButton = style({
|
||||
selectors: {
|
||||
'&[data-state=open]': {
|
||||
backgroundColor: cssVar('hoverColor'),
|
||||
},
|
||||
},
|
||||
padding: '10px',
|
||||
});
|
||||
|
||||
export const outlinePanel = style({
|
||||
maxHeight: '60vh',
|
||||
overflow: 'auto',
|
||||
});
|
||||
@@ -0,0 +1,132 @@
|
||||
import { IconButton, toast } from '@affine/component';
|
||||
import {
|
||||
MenuSeparator,
|
||||
MobileMenu,
|
||||
MobileMenuItem,
|
||||
} from '@affine/component/ui/menu';
|
||||
import { useFavorite } from '@affine/core/components/blocksuite/block-suite-header/favorite';
|
||||
import { IsFavoriteIcon } from '@affine/core/components/pure/icons';
|
||||
import { track } from '@affine/core/mixpanel';
|
||||
import { EditorService } from '@affine/core/modules/editor';
|
||||
import { EditorOutlinePanel } from '@affine/core/pages/workspace/detail-page/tabs/outline';
|
||||
import { preventDefault } from '@affine/core/utils';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
import {
|
||||
EdgelessIcon,
|
||||
InformationIcon,
|
||||
MoreHorizontalIcon,
|
||||
PageIcon,
|
||||
TocIcon,
|
||||
} from '@blocksuite/icons/rc';
|
||||
import { useLiveData, useService } from '@toeverything/infra';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import * as styles from './page-header-more-button.css';
|
||||
import { DocInfoSheet } from './sheets/doc-info';
|
||||
|
||||
type PageMenuProps = {
|
||||
docId: string;
|
||||
};
|
||||
|
||||
export const PageHeaderMenuButton = ({ docId }: PageMenuProps) => {
|
||||
const t = useI18n();
|
||||
|
||||
const editorService = useService(EditorService);
|
||||
const editorContainer = useLiveData(editorService.editor.editorContainer$);
|
||||
|
||||
const isInTrash = useLiveData(
|
||||
editorService.editor.doc.meta$.map(meta => meta.trash)
|
||||
);
|
||||
const currentMode = useLiveData(editorService.editor.mode$);
|
||||
|
||||
const { favorite, toggleFavorite } = useFavorite(docId);
|
||||
|
||||
const handleSwitchMode = useCallback(() => {
|
||||
editorService.editor.toggleMode();
|
||||
track.$.header.docOptions.switchPageMode({
|
||||
mode: currentMode === 'page' ? 'edgeless' : 'page',
|
||||
});
|
||||
toast(
|
||||
currentMode === 'page'
|
||||
? t['com.affine.toastMessage.edgelessMode']()
|
||||
: t['com.affine.toastMessage.pageMode']()
|
||||
);
|
||||
}, [currentMode, editorService, t]);
|
||||
|
||||
const handleMenuOpenChange = useCallback((open: boolean) => {
|
||||
if (open) {
|
||||
track.$.header.docOptions.open();
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleToggleFavorite = useCallback(() => {
|
||||
track.$.header.docOptions.toggleFavorite();
|
||||
toggleFavorite();
|
||||
}, [toggleFavorite]);
|
||||
|
||||
const EditMenu = (
|
||||
<>
|
||||
<MobileMenuItem
|
||||
prefixIcon={currentMode === 'page' ? <EdgelessIcon /> : <PageIcon />}
|
||||
data-testid="editor-option-menu-edgeless"
|
||||
onSelect={handleSwitchMode}
|
||||
>
|
||||
{t['Convert to ']()}
|
||||
{currentMode === 'page'
|
||||
? t['com.affine.pageMode.edgeless']()
|
||||
: t['com.affine.pageMode.page']()}
|
||||
</MobileMenuItem>
|
||||
<MobileMenuItem
|
||||
data-testid="editor-option-menu-favorite"
|
||||
onSelect={handleToggleFavorite}
|
||||
prefixIcon={<IsFavoriteIcon favorite={favorite} />}
|
||||
>
|
||||
{favorite
|
||||
? t['com.affine.favoritePageOperation.remove']()
|
||||
: t['com.affine.favoritePageOperation.add']()}
|
||||
</MobileMenuItem>
|
||||
<MenuSeparator />
|
||||
<MobileMenu items={<DocInfoSheet docId={docId} />}>
|
||||
<MobileMenuItem
|
||||
prefixIcon={<InformationIcon />}
|
||||
onClick={preventDefault}
|
||||
>
|
||||
<span>{t['com.affine.page-properties.page-info.view']()}</span>
|
||||
</MobileMenuItem>
|
||||
</MobileMenu>
|
||||
<MobileMenu
|
||||
items={
|
||||
<div className={styles.outlinePanel}>
|
||||
<EditorOutlinePanel editor={editorContainer} />
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<MobileMenuItem prefixIcon={<TocIcon />} onClick={preventDefault}>
|
||||
<span>{t['com.affine.header.option.view-toc']()}</span>
|
||||
</MobileMenuItem>
|
||||
</MobileMenu>
|
||||
</>
|
||||
);
|
||||
if (isInTrash) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<MobileMenu
|
||||
items={EditMenu}
|
||||
contentOptions={{
|
||||
align: 'center',
|
||||
}}
|
||||
rootOptions={{
|
||||
onOpenChange: handleMenuOpenChange,
|
||||
}}
|
||||
>
|
||||
<IconButton
|
||||
size={24}
|
||||
data-testid="header-dropDownButton"
|
||||
className={styles.iconButton}
|
||||
>
|
||||
<MoreHorizontalIcon />
|
||||
</IconButton>
|
||||
</MobileMenu>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,51 @@
|
||||
import { rowHPadding } from '@affine/core/components/affine/page-properties/styles.css';
|
||||
import { style } from '@vanilla-extract/css';
|
||||
|
||||
export const viewport = style({
|
||||
vars: {
|
||||
[rowHPadding]: '0px',
|
||||
},
|
||||
});
|
||||
|
||||
export const item = style({
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
gap: 4,
|
||||
height: 34,
|
||||
padding: '0 20px',
|
||||
|
||||
fontSize: 17,
|
||||
lineHeight: '22px',
|
||||
fontWeight: 400,
|
||||
letterSpacing: -0.43,
|
||||
});
|
||||
|
||||
export const linksRow = style({
|
||||
padding: '0 16px',
|
||||
});
|
||||
|
||||
export const timeRow = style({
|
||||
padding: '0 16px',
|
||||
});
|
||||
|
||||
export const tagsRow = style({
|
||||
padding: '0 16px',
|
||||
});
|
||||
|
||||
export const properties = style({
|
||||
padding: '0 16px',
|
||||
});
|
||||
|
||||
export const scrollBar = style({
|
||||
width: 6,
|
||||
transform: 'translateX(-4px)',
|
||||
});
|
||||
|
||||
export const rowNameContainer = style({
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
gap: 6,
|
||||
padding: 6,
|
||||
width: '160px',
|
||||
});
|
||||
@@ -0,0 +1,100 @@
|
||||
import { Divider, Scrollable } from '@affine/component';
|
||||
import {
|
||||
PagePropertyRow,
|
||||
SortableProperties,
|
||||
usePagePropertiesManager,
|
||||
} from '@affine/core/components/affine/page-properties';
|
||||
import { managerContext } from '@affine/core/components/affine/page-properties/common';
|
||||
import { LinksRow } from '@affine/core/components/affine/page-properties/info-modal/links-row';
|
||||
import { TagsRow } from '@affine/core/components/affine/page-properties/info-modal/tags-row';
|
||||
import { TimeRow } from '@affine/core/components/affine/page-properties/info-modal/time-row';
|
||||
import { DocsSearchService } from '@affine/core/modules/docs-search';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
import { LiveData, useLiveData, useService } from '@toeverything/infra';
|
||||
import { Suspense, useMemo } from 'react';
|
||||
|
||||
import * as styles from './doc-info.css';
|
||||
|
||||
export const DocInfoSheet = ({ docId }: { docId: string }) => {
|
||||
const manager = usePagePropertiesManager(docId);
|
||||
const docsSearchService = useService(DocsSearchService);
|
||||
const t = useI18n();
|
||||
|
||||
const links = useLiveData(
|
||||
useMemo(
|
||||
() => LiveData.from(docsSearchService.watchRefsFrom(docId), null),
|
||||
[docId, docsSearchService]
|
||||
)
|
||||
);
|
||||
const backlinks = useLiveData(
|
||||
useMemo(
|
||||
() => LiveData.from(docsSearchService.watchRefsTo(docId), null),
|
||||
[docId, docsSearchService]
|
||||
)
|
||||
);
|
||||
|
||||
return (
|
||||
<Scrollable.Root>
|
||||
<Scrollable.Viewport
|
||||
className={styles.viewport}
|
||||
data-testid="doc-info-menu"
|
||||
>
|
||||
<managerContext.Provider value={manager}>
|
||||
<Suspense>
|
||||
<TimeRow docId={docId} className={styles.timeRow} />
|
||||
<Divider size="thinner" />
|
||||
{backlinks && backlinks.length > 0 ? (
|
||||
<>
|
||||
<LinksRow
|
||||
className={styles.linksRow}
|
||||
references={backlinks}
|
||||
label={t['com.affine.page-properties.backlinks']()}
|
||||
/>
|
||||
<Divider size="thinner" />
|
||||
</>
|
||||
) : null}
|
||||
{links && links.length > 0 ? (
|
||||
<>
|
||||
<LinksRow
|
||||
className={styles.linksRow}
|
||||
references={links}
|
||||
label={t['com.affine.page-properties.outgoing-links']()}
|
||||
/>
|
||||
<Divider size="thinner" />
|
||||
</>
|
||||
) : null}
|
||||
<div className={styles.properties}>
|
||||
<TagsRow docId={docId} readonly={manager.readonly} />
|
||||
<SortableProperties>
|
||||
{properties =>
|
||||
properties.length ? (
|
||||
<>
|
||||
{properties
|
||||
.filter(
|
||||
property =>
|
||||
manager.isPropertyRequired(property.id) ||
|
||||
(property.visibility !== 'hide' &&
|
||||
!(
|
||||
property.visibility === 'hide-if-empty' &&
|
||||
!property.value
|
||||
))
|
||||
)
|
||||
.map(property => (
|
||||
<PagePropertyRow
|
||||
key={property.id}
|
||||
property={property}
|
||||
rowNameClassName={styles.rowNameContainer}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
) : null
|
||||
}
|
||||
</SortableProperties>
|
||||
</div>
|
||||
</Suspense>
|
||||
</managerContext.Provider>
|
||||
</Scrollable.Viewport>
|
||||
<Scrollable.Scrollbar className={styles.scrollBar} />
|
||||
</Scrollable.Root>
|
||||
);
|
||||
};
|
||||
@@ -95,7 +95,7 @@ export const viewRoutes = [
|
||||
},
|
||||
{
|
||||
path: '/:pageId',
|
||||
lazy: () => import('./pages/workspace/detail'),
|
||||
lazy: () => import('./pages/workspace/detail/mobile-detail-page'),
|
||||
},
|
||||
{
|
||||
path: '*',
|
||||
|
||||
Reference in New Issue
Block a user