mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-24 09:52:49 +08:00
feat(core): add doc info modal (#7409)
close AF-1038 close AF-1039 close AF-1040 close AF-1046 A popup window has been added to facilitate viewing of this doc's info in edgeless mode and other modes. https://github.com/toeverything/AFFiNE/assets/102217452/d7f94cb6-7e32-4ce7-8ff4-8aba1309b331
This commit is contained in:
1
packages/common/env/src/global.ts
vendored
1
packages/common/env/src/global.ts
vendored
@@ -24,6 +24,7 @@ export const runtimeFlagsSchema = z.object({
|
|||||||
enablePayment: z.boolean(),
|
enablePayment: z.boolean(),
|
||||||
enablePageHistory: z.boolean(),
|
enablePageHistory: z.boolean(),
|
||||||
enableExperimentalFeature: z.boolean(),
|
enableExperimentalFeature: z.boolean(),
|
||||||
|
enableInfoModal: z.boolean(),
|
||||||
allowLocalWorkspace: z.boolean(),
|
allowLocalWorkspace: z.boolean(),
|
||||||
// this is for the electron app
|
// this is for the electron app
|
||||||
serverUrlPrefix: z.string(),
|
serverUrlPrefix: z.string(),
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ export const openQuotaModalAtom = atom(false);
|
|||||||
export const openStarAFFiNEModalAtom = atom(false);
|
export const openStarAFFiNEModalAtom = atom(false);
|
||||||
export const openIssueFeedbackModalAtom = atom(false);
|
export const openIssueFeedbackModalAtom = atom(false);
|
||||||
export const openHistoryTipsModalAtom = atom(false);
|
export const openHistoryTipsModalAtom = atom(false);
|
||||||
|
export const openInfoModalAtom = atom(false);
|
||||||
|
|
||||||
export const rightSidebarWidthAtom = atom(320);
|
export const rightSidebarWidthAtom = atom(320);
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
export * from './icons-mapping';
|
export * from './icons-mapping';
|
||||||
|
export * from './info-modal/info-modal';
|
||||||
export * from './page-properties-manager';
|
export * from './page-properties-manager';
|
||||||
export * from './table';
|
export * from './table';
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
import { cssVar } from '@toeverything/theme';
|
||||||
|
import { globalStyle, style } from '@vanilla-extract/css';
|
||||||
|
|
||||||
|
export const title = style({
|
||||||
|
fontSize: cssVar('fontSm'),
|
||||||
|
fontWeight: '500',
|
||||||
|
color: cssVar('textSecondaryColor'),
|
||||||
|
padding: '6px',
|
||||||
|
});
|
||||||
|
|
||||||
|
export const wrapper = style({
|
||||||
|
width: '100%',
|
||||||
|
borderRadius: 4,
|
||||||
|
color: cssVar('textPrimaryColor'),
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: 2,
|
||||||
|
padding: '6px',
|
||||||
|
':hover': {
|
||||||
|
backgroundColor: cssVar('hoverColor'),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
globalStyle(`${wrapper} svg`, {
|
||||||
|
color: cssVar('iconSecondary'),
|
||||||
|
fontSize: 16,
|
||||||
|
transform: 'none',
|
||||||
|
});
|
||||||
|
globalStyle(`${wrapper} span`, {
|
||||||
|
fontSize: cssVar('fontSm'),
|
||||||
|
whiteSpace: 'nowrap',
|
||||||
|
overflow: 'hidden',
|
||||||
|
textOverflow: 'ellipsis',
|
||||||
|
borderBottom: 'none',
|
||||||
|
});
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
import { useI18n } from '@affine/i18n';
|
||||||
|
import { useContext } from 'react';
|
||||||
|
|
||||||
|
import { AffinePageReference } from '../../reference-link';
|
||||||
|
import { managerContext } from '../common';
|
||||||
|
import * as styles from './back-links-row.css';
|
||||||
|
export const BackLinksRow = ({
|
||||||
|
references,
|
||||||
|
onClick,
|
||||||
|
}: {
|
||||||
|
references: { docId: string; title: string }[];
|
||||||
|
onClick?: () => void;
|
||||||
|
}) => {
|
||||||
|
const manager = useContext(managerContext);
|
||||||
|
const t = useI18n();
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className={styles.title}>
|
||||||
|
{t['com.affine.page-properties.backlinks']()} · {references.length}
|
||||||
|
</div>
|
||||||
|
{references.map(link => (
|
||||||
|
<AffinePageReference
|
||||||
|
key={link.docId}
|
||||||
|
pageId={link.docId}
|
||||||
|
wrapper={props => (
|
||||||
|
<div className={styles.wrapper} onClick={onClick} {...props} />
|
||||||
|
)}
|
||||||
|
docCollection={manager.workspace.docCollection}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
import { cssVar } from '@toeverything/theme';
|
||||||
|
import { style } from '@vanilla-extract/css';
|
||||||
|
|
||||||
|
export const container = style({
|
||||||
|
maxWidth: 480,
|
||||||
|
minWidth: 360,
|
||||||
|
padding: '20px 0',
|
||||||
|
alignSelf: 'start',
|
||||||
|
marginTop: '120px',
|
||||||
|
});
|
||||||
|
|
||||||
|
export const titleContainer = style({
|
||||||
|
display: 'flex',
|
||||||
|
width: '100%',
|
||||||
|
flexDirection: 'column',
|
||||||
|
});
|
||||||
|
|
||||||
|
export const titleStyle = style({
|
||||||
|
fontSize: cssVar('fontH6'),
|
||||||
|
fontWeight: '600',
|
||||||
|
});
|
||||||
|
|
||||||
|
export const rowNameContainer = style({
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'row',
|
||||||
|
gap: 6,
|
||||||
|
padding: 6,
|
||||||
|
width: '160px',
|
||||||
|
});
|
||||||
|
|
||||||
|
export const viewport = style({
|
||||||
|
maxHeight: 'calc(100vh - 220px)',
|
||||||
|
padding: '0 24px',
|
||||||
|
});
|
||||||
|
|
||||||
|
export const scrollBar = style({
|
||||||
|
width: 6,
|
||||||
|
transform: 'translateX(-4px)',
|
||||||
|
});
|
||||||
@@ -0,0 +1,156 @@
|
|||||||
|
import {
|
||||||
|
Divider,
|
||||||
|
type InlineEditHandle,
|
||||||
|
Modal,
|
||||||
|
Scrollable,
|
||||||
|
} from '@affine/component';
|
||||||
|
import { DocsSearchService } from '@affine/core/modules/docs-search';
|
||||||
|
import type { Doc } from '@blocksuite/store';
|
||||||
|
import {
|
||||||
|
LiveData,
|
||||||
|
useLiveData,
|
||||||
|
useService,
|
||||||
|
type Workspace,
|
||||||
|
} from '@toeverything/infra';
|
||||||
|
import { Suspense, useCallback, useContext, useMemo, useRef } from 'react';
|
||||||
|
|
||||||
|
import { BlocksuiteHeaderTitle } from '../../../blocksuite/block-suite-header/title';
|
||||||
|
import { managerContext } from '../common';
|
||||||
|
import {
|
||||||
|
PagePropertiesAddProperty,
|
||||||
|
PagePropertyRow,
|
||||||
|
SortableProperties,
|
||||||
|
usePagePropertiesManager,
|
||||||
|
} from '../table';
|
||||||
|
import { BackLinksRow } from './back-links-row';
|
||||||
|
import * as styles from './info-modal.css';
|
||||||
|
import { TagsRow } from './tags-row';
|
||||||
|
import { TimeRow } from './time-row';
|
||||||
|
|
||||||
|
export const InfoModal = ({
|
||||||
|
open,
|
||||||
|
onOpenChange,
|
||||||
|
page,
|
||||||
|
workspace,
|
||||||
|
}: {
|
||||||
|
open: boolean;
|
||||||
|
onOpenChange: (open: boolean) => void;
|
||||||
|
page: Doc;
|
||||||
|
workspace: Workspace;
|
||||||
|
}) => {
|
||||||
|
const titleInputHandleRef = useRef<InlineEditHandle>(null);
|
||||||
|
const manager = usePagePropertiesManager(page);
|
||||||
|
const handleClose = useCallback(() => {
|
||||||
|
onOpenChange(false);
|
||||||
|
}, [onOpenChange]);
|
||||||
|
|
||||||
|
const docsSearchService = useService(DocsSearchService);
|
||||||
|
const references = useLiveData(
|
||||||
|
useMemo(
|
||||||
|
() => LiveData.from(docsSearchService.watchRefsFrom(page.id), null),
|
||||||
|
[docsSearchService, page.id]
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!manager.page || manager.readonly) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
contentOptions={{
|
||||||
|
className: styles.container,
|
||||||
|
'aria-describedby': undefined,
|
||||||
|
}}
|
||||||
|
open={open}
|
||||||
|
onOpenChange={onOpenChange}
|
||||||
|
withoutCloseButton
|
||||||
|
>
|
||||||
|
<Scrollable.Root>
|
||||||
|
<Scrollable.Viewport
|
||||||
|
className={styles.viewport}
|
||||||
|
data-testid="info-modal"
|
||||||
|
>
|
||||||
|
<div className={styles.titleContainer} data-testid="info-modal-title">
|
||||||
|
<BlocksuiteHeaderTitle
|
||||||
|
className={styles.titleStyle}
|
||||||
|
inputHandleRef={titleInputHandleRef}
|
||||||
|
pageId={page.id}
|
||||||
|
docCollection={workspace.docCollection}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<managerContext.Provider value={manager}>
|
||||||
|
<Suspense>
|
||||||
|
<InfoTable
|
||||||
|
docId={page.id}
|
||||||
|
onClose={handleClose}
|
||||||
|
references={references}
|
||||||
|
readonly={manager.readonly}
|
||||||
|
/>
|
||||||
|
</Suspense>
|
||||||
|
</managerContext.Provider>
|
||||||
|
</Scrollable.Viewport>
|
||||||
|
<Scrollable.Scrollbar className={styles.scrollBar} />
|
||||||
|
</Scrollable.Root>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const InfoTable = ({
|
||||||
|
onClose,
|
||||||
|
references,
|
||||||
|
docId,
|
||||||
|
readonly,
|
||||||
|
}: {
|
||||||
|
docId: string;
|
||||||
|
onClose: () => void;
|
||||||
|
readonly: boolean;
|
||||||
|
references:
|
||||||
|
| {
|
||||||
|
docId: string;
|
||||||
|
title: string;
|
||||||
|
}[]
|
||||||
|
| null;
|
||||||
|
}) => {
|
||||||
|
const manager = useContext(managerContext);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<TimeRow docId={docId} />
|
||||||
|
<Divider size="thinner" />
|
||||||
|
{references && references.length > 0 ? (
|
||||||
|
<>
|
||||||
|
<BackLinksRow references={references} onClick={onClose} />
|
||||||
|
<Divider size="thinner" />
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
|
<TagsRow docId={docId} readonly={readonly} />
|
||||||
|
<SortableProperties>
|
||||||
|
{properties =>
|
||||||
|
properties.length ? (
|
||||||
|
<div>
|
||||||
|
{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}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : null
|
||||||
|
}
|
||||||
|
</SortableProperties>
|
||||||
|
{manager.readonly ? null : <PagePropertiesAddProperty />}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
import { cssVar } from '@toeverything/theme';
|
||||||
|
import { style } from '@vanilla-extract/css';
|
||||||
|
|
||||||
|
export const icon = style({
|
||||||
|
fontSize: 16,
|
||||||
|
color: cssVar('iconSecondary'),
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
});
|
||||||
|
|
||||||
|
export const rowNameContainer = style({
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: 6,
|
||||||
|
padding: 6,
|
||||||
|
width: '160px',
|
||||||
|
});
|
||||||
|
|
||||||
|
export const rowName = style({
|
||||||
|
flexGrow: 1,
|
||||||
|
overflow: 'hidden',
|
||||||
|
textOverflow: 'ellipsis',
|
||||||
|
whiteSpace: 'nowrap',
|
||||||
|
fontSize: cssVar('fontSm'),
|
||||||
|
color: cssVar('textSecondaryColor'),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const time = style({
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
padding: '6px 8px',
|
||||||
|
flexGrow: 1,
|
||||||
|
fontSize: cssVar('fontSm'),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const rowCell = style({
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'start',
|
||||||
|
gap: 4,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const container = style({
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
marginTop: 20,
|
||||||
|
marginBottom: 4,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const rowValueCell = style({
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'flex-start',
|
||||||
|
position: 'relative',
|
||||||
|
borderRadius: 4,
|
||||||
|
fontSize: cssVar('fontSm'),
|
||||||
|
lineHeight: '22px',
|
||||||
|
userSelect: 'none',
|
||||||
|
':focus-visible': {
|
||||||
|
outline: 'none',
|
||||||
|
},
|
||||||
|
cursor: 'pointer',
|
||||||
|
':hover': {
|
||||||
|
backgroundColor: cssVar('hoverColor'),
|
||||||
|
},
|
||||||
|
padding: '6px 8px',
|
||||||
|
border: `1px solid transparent`,
|
||||||
|
color: cssVar('textPrimaryColor'),
|
||||||
|
':focus': {
|
||||||
|
backgroundColor: cssVar('hoverColor'),
|
||||||
|
},
|
||||||
|
'::placeholder': {
|
||||||
|
color: cssVar('placeholderColor'),
|
||||||
|
},
|
||||||
|
selectors: {
|
||||||
|
'&[data-empty="true"]': {
|
||||||
|
color: cssVar('placeholderColor'),
|
||||||
|
},
|
||||||
|
'&[data-readonly=true]': {
|
||||||
|
pointerEvents: 'none',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
flex: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const tagsMenu = style({
|
||||||
|
padding: 0,
|
||||||
|
transform:
|
||||||
|
'translate(-3.5px, calc(-3.5px + var(--radix-popper-anchor-height) * -1))',
|
||||||
|
width: 'calc(var(--radix-popper-anchor-width) + 16px)',
|
||||||
|
overflow: 'hidden',
|
||||||
|
});
|
||||||
|
|
||||||
|
export const tagsInlineEditor = style({
|
||||||
|
selectors: {
|
||||||
|
'&[data-empty=true]': {
|
||||||
|
color: cssVar('placeholderColor'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
import { Menu } from '@affine/component';
|
||||||
|
import { TagService } from '@affine/core/modules/tag';
|
||||||
|
import { useI18n } from '@affine/i18n';
|
||||||
|
import { TagsIcon } from '@blocksuite/icons/rc';
|
||||||
|
import { useLiveData, useService } from '@toeverything/infra';
|
||||||
|
import clsx from 'clsx';
|
||||||
|
|
||||||
|
import { InlineTagsList, TagsEditor } from '../tags-inline-editor';
|
||||||
|
import * as styles from './tags-row.css';
|
||||||
|
|
||||||
|
export const TagsRow = ({
|
||||||
|
docId,
|
||||||
|
readonly,
|
||||||
|
}: {
|
||||||
|
docId: string;
|
||||||
|
readonly: boolean;
|
||||||
|
}) => {
|
||||||
|
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={styles.rowNameContainer}>
|
||||||
|
<div className={styles.icon}>
|
||||||
|
<TagsIcon />
|
||||||
|
</div>
|
||||||
|
<div className={styles.rowName}>{t['Tags']()}</div>
|
||||||
|
</div>
|
||||||
|
<Menu
|
||||||
|
contentOptions={{
|
||||||
|
side: 'bottom',
|
||||||
|
align: 'start',
|
||||||
|
sideOffset: 0,
|
||||||
|
avoidCollisions: false,
|
||||||
|
className: styles.tagsMenu,
|
||||||
|
onClick(e) {
|
||||||
|
e.stopPropagation();
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
items={<TagsEditor pageId={docId} readonly={readonly} />}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={clsx(styles.tagsInlineEditor, styles.rowValueCell)}
|
||||||
|
data-empty={empty}
|
||||||
|
data-readonly={readonly}
|
||||||
|
data-testid="info-modal-tags-value"
|
||||||
|
>
|
||||||
|
{empty ? (
|
||||||
|
t['com.affine.page-properties.property-value-placeholder']()
|
||||||
|
) : (
|
||||||
|
<InlineTagsList pageId={docId} readonly />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Menu>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
import { cssVar } from '@toeverything/theme';
|
||||||
|
import { style } from '@vanilla-extract/css';
|
||||||
|
|
||||||
|
export const icon = style({
|
||||||
|
fontSize: 16,
|
||||||
|
color: cssVar('iconSecondary'),
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
});
|
||||||
|
|
||||||
|
export const rowNameContainer = style({
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
|
||||||
|
gap: 6,
|
||||||
|
padding: 6,
|
||||||
|
width: '160px',
|
||||||
|
});
|
||||||
|
|
||||||
|
export const rowName = style({
|
||||||
|
flexGrow: 1,
|
||||||
|
overflow: 'hidden',
|
||||||
|
textOverflow: 'ellipsis',
|
||||||
|
whiteSpace: 'nowrap',
|
||||||
|
fontSize: cssVar('fontSm'),
|
||||||
|
color: cssVar('textSecondaryColor'),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const time = style({
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
padding: '6px 8px',
|
||||||
|
flexGrow: 1,
|
||||||
|
fontSize: cssVar('fontSm'),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const rowCell = style({
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: 4,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const container = style({
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
marginTop: 20,
|
||||||
|
marginBottom: 4,
|
||||||
|
});
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
import { i18nTime, useI18n } from '@affine/i18n';
|
||||||
|
import { DateTimeIcon, HistoryIcon } from '@blocksuite/icons/rc';
|
||||||
|
import { useLiveData, useService, WorkspaceService } from '@toeverything/infra';
|
||||||
|
import type { ConfigType } from 'dayjs';
|
||||||
|
import { useDebouncedValue } from 'foxact/use-debounced-value';
|
||||||
|
import { type ReactNode, useContext, useMemo } from 'react';
|
||||||
|
|
||||||
|
import { managerContext } from '../common';
|
||||||
|
import * as styles from './time-row.css';
|
||||||
|
|
||||||
|
const RowComponent = ({
|
||||||
|
name,
|
||||||
|
icon,
|
||||||
|
time,
|
||||||
|
}: {
|
||||||
|
name: string;
|
||||||
|
icon: ReactNode;
|
||||||
|
time?: string | null;
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<div className={styles.rowCell}>
|
||||||
|
<div className={styles.rowNameContainer}>
|
||||||
|
<div className={styles.icon}>{icon}</div>
|
||||||
|
<span className={styles.rowName}>{name}</span>
|
||||||
|
</div>
|
||||||
|
<div className={styles.time}>{time ? time : 'unknown'}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const TimeRow = ({ docId }: { docId: string }) => {
|
||||||
|
const t = useI18n();
|
||||||
|
const manager = useContext(managerContext);
|
||||||
|
const workspaceService = useService(WorkspaceService);
|
||||||
|
const { syncing, retrying, serverClock } = useLiveData(
|
||||||
|
workspaceService.workspace.engine.doc.docState$(docId)
|
||||||
|
);
|
||||||
|
|
||||||
|
const timestampElement = useMemo(() => {
|
||||||
|
const formatI18nTime = (time: ConfigType) =>
|
||||||
|
i18nTime(time, {
|
||||||
|
relative: {
|
||||||
|
max: [1, 'day'],
|
||||||
|
accuracy: 'minute',
|
||||||
|
},
|
||||||
|
absolute: {
|
||||||
|
accuracy: 'day',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const localizedCreateTime = manager.createDate
|
||||||
|
? formatI18nTime(manager.createDate)
|
||||||
|
: null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<RowComponent
|
||||||
|
icon={<DateTimeIcon />}
|
||||||
|
name={t['Created']()}
|
||||||
|
time={
|
||||||
|
manager.createDate
|
||||||
|
? formatI18nTime(manager.createDate)
|
||||||
|
: localizedCreateTime
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
{serverClock ? (
|
||||||
|
<RowComponent
|
||||||
|
icon={<HistoryIcon />}
|
||||||
|
name={t[!syncing && !retrying ? 'Updated' : 'com.affine.syncing']()}
|
||||||
|
time={!syncing && !retrying ? formatI18nTime(serverClock) : null}
|
||||||
|
/>
|
||||||
|
) : manager.updatedDate ? (
|
||||||
|
<RowComponent
|
||||||
|
icon={<HistoryIcon />}
|
||||||
|
name={t['Updated']()}
|
||||||
|
time={formatI18nTime(manager.updatedDate)}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}, [
|
||||||
|
manager.createDate,
|
||||||
|
manager.updatedDate,
|
||||||
|
retrying,
|
||||||
|
serverClock,
|
||||||
|
syncing,
|
||||||
|
t,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const dTimestampElement = useDebouncedValue(timestampElement, 500);
|
||||||
|
|
||||||
|
return <div className={styles.container}>{dTimestampElement}</div>;
|
||||||
|
};
|
||||||
@@ -129,6 +129,16 @@ export const addPropertyButton = style({
|
|||||||
color: cssVar('textPrimaryColor'),
|
color: cssVar('textPrimaryColor'),
|
||||||
backgroundColor: cssVar('hoverColor'),
|
backgroundColor: cssVar('hoverColor'),
|
||||||
},
|
},
|
||||||
|
gap: 2,
|
||||||
|
fontWeight: 400,
|
||||||
|
});
|
||||||
|
|
||||||
|
globalStyle(`${addPropertyButton} svg`, {
|
||||||
|
fontSize: 16,
|
||||||
|
color: cssVar('iconSecondary'),
|
||||||
|
});
|
||||||
|
globalStyle(`${addPropertyButton}:hover svg`, {
|
||||||
|
color: cssVar('iconColor'),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const collapsedIcon = style({
|
export const collapsedIcon = style({
|
||||||
@@ -262,7 +272,7 @@ export const propertyRowIconContainer = style({
|
|||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
borderRadius: '2px',
|
borderRadius: '2px',
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
color: 'inherit',
|
color: cssVar('iconSecondary'),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const propertyRowNameContainer = style({
|
export const propertyRowNameContainer = style({
|
||||||
|
|||||||
@@ -105,7 +105,7 @@ interface SortablePropertiesProps {
|
|||||||
children: (properties: PageInfoCustomProperty[]) => React.ReactNode;
|
children: (properties: PageInfoCustomProperty[]) => React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
const SortableProperties = ({ children }: SortablePropertiesProps) => {
|
export const SortableProperties = ({ children }: SortablePropertiesProps) => {
|
||||||
const manager = useContext(managerContext);
|
const manager = useContext(managerContext);
|
||||||
const properties = useMemo(() => manager.sorter.getOrderedItems(), [manager]);
|
const properties = useMemo(() => manager.sorter.getOrderedItems(), [manager]);
|
||||||
const editingItem = useAtomValue(editingPropertyAtom);
|
const editingItem = useAtomValue(editingPropertyAtom);
|
||||||
@@ -735,9 +735,13 @@ export const PagePropertiesTableHeader = ({
|
|||||||
interface PagePropertyRowProps {
|
interface PagePropertyRowProps {
|
||||||
property: PageInfoCustomProperty;
|
property: PageInfoCustomProperty;
|
||||||
style?: React.CSSProperties;
|
style?: React.CSSProperties;
|
||||||
|
rowNameClassName?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const PagePropertyRow = ({ property }: PagePropertyRowProps) => {
|
export const PagePropertyRow = ({
|
||||||
|
property,
|
||||||
|
rowNameClassName,
|
||||||
|
}: PagePropertyRowProps) => {
|
||||||
const manager = useContext(managerContext);
|
const manager = useContext(managerContext);
|
||||||
const meta = manager.getCustomPropertyMeta(property.id);
|
const meta = manager.getCustomPropertyMeta(property.id);
|
||||||
|
|
||||||
@@ -772,7 +776,10 @@ const PagePropertyRow = ({ property }: PagePropertyRowProps) => {
|
|||||||
{...attributes}
|
{...attributes}
|
||||||
{...listeners}
|
{...listeners}
|
||||||
data-testid="page-property-row-name"
|
data-testid="page-property-row-name"
|
||||||
className={styles.sortablePropertyRowNameCell}
|
className={clsx(
|
||||||
|
styles.sortablePropertyRowNameCell,
|
||||||
|
rowNameClassName
|
||||||
|
)}
|
||||||
onClick={handleEditMeta}
|
onClick={handleEditMeta}
|
||||||
>
|
>
|
||||||
<div className={styles.propertyRowNameContainer}>
|
<div className={styles.propertyRowNameContainer}>
|
||||||
@@ -790,7 +797,11 @@ const PagePropertyRow = ({ property }: PagePropertyRowProps) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const PageTagsRow = () => {
|
export const PageTagsRow = ({
|
||||||
|
rowNameClassName,
|
||||||
|
}: {
|
||||||
|
rowNameClassName?: string;
|
||||||
|
}) => {
|
||||||
const t = useI18n();
|
const t = useI18n();
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@@ -799,7 +810,7 @@ const PageTagsRow = () => {
|
|||||||
data-property="tags"
|
data-property="tags"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={styles.propertyRowNameCell}
|
className={clsx(styles.propertyRowNameCell, rowNameClassName)}
|
||||||
data-testid="page-property-row-name"
|
data-testid="page-property-row-name"
|
||||||
>
|
>
|
||||||
<div className={styles.propertyRowNameContainer}>
|
<div className={styles.propertyRowNameContainer}>
|
||||||
@@ -1074,7 +1085,7 @@ const PagePropertiesTableInner = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const usePagePropertiesManager = (page: Doc) => {
|
export const usePagePropertiesManager = (page: Doc) => {
|
||||||
// the workspace properties adapter adapter is reactive,
|
// the workspace properties adapter adapter is reactive,
|
||||||
// which means it's reference will change when any of the properties change
|
// which means it's reference will change when any of the properties change
|
||||||
// also it will trigger a re-render of the component
|
// also it will trigger a re-render of the component
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ interface InlineTagsListProps
|
|||||||
onRemove?: () => void;
|
onRemove?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const InlineTagsList = ({
|
export const InlineTagsList = ({
|
||||||
pageId,
|
pageId,
|
||||||
readonly,
|
readonly,
|
||||||
children,
|
children,
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import { IconButton, Tooltip } from '@affine/component';
|
||||||
|
import { openInfoModalAtom } from '@affine/core/atoms';
|
||||||
|
import { useI18n } from '@affine/i18n';
|
||||||
|
import { InformationIcon } from '@blocksuite/icons/rc';
|
||||||
|
import { useSetAtom } from 'jotai';
|
||||||
|
|
||||||
|
export const InfoButton = () => {
|
||||||
|
const setOpenInfoModal = useSetAtom(openInfoModalAtom);
|
||||||
|
const t = useI18n();
|
||||||
|
const onOpenInfoModal = () => {
|
||||||
|
setOpenInfoModal(true);
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<Tooltip content={t['com.affine.page-properties.page-info.view']()}>
|
||||||
|
<IconButton
|
||||||
|
data-testid="header-info-button"
|
||||||
|
onClick={onOpenInfoModal}
|
||||||
|
icon={<InformationIcon />}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -6,7 +6,10 @@ import {
|
|||||||
MenuSeparator,
|
MenuSeparator,
|
||||||
MenuSub,
|
MenuSub,
|
||||||
} from '@affine/component/ui/menu';
|
} from '@affine/component/ui/menu';
|
||||||
import { openHistoryTipsModalAtom } from '@affine/core/atoms';
|
import {
|
||||||
|
openHistoryTipsModalAtom,
|
||||||
|
openInfoModalAtom,
|
||||||
|
} from '@affine/core/atoms';
|
||||||
import { PageHistoryModal } from '@affine/core/components/affine/page-history-modal';
|
import { PageHistoryModal } from '@affine/core/components/affine/page-history-modal';
|
||||||
import { ShareMenuContent } from '@affine/core/components/affine/share-page-modal/share-menu';
|
import { ShareMenuContent } from '@affine/core/components/affine/share-page-modal/share-menu';
|
||||||
import { Export, MoveToTrash } from '@affine/core/components/page-list';
|
import { Export, MoveToTrash } from '@affine/core/components/page-list';
|
||||||
@@ -27,6 +30,7 @@ import {
|
|||||||
FavoriteIcon,
|
FavoriteIcon,
|
||||||
HistoryIcon,
|
HistoryIcon,
|
||||||
ImportIcon,
|
ImportIcon,
|
||||||
|
InformationIcon,
|
||||||
PageIcon,
|
PageIcon,
|
||||||
ShareIcon,
|
ShareIcon,
|
||||||
} from '@blocksuite/icons/rc';
|
} from '@blocksuite/icons/rc';
|
||||||
@@ -83,6 +87,11 @@ export const PageHeaderMenuButton = ({
|
|||||||
return setOpenHistoryTipsModal(true);
|
return setOpenHistoryTipsModal(true);
|
||||||
}, [setOpenHistoryTipsModal, workspace.flavour]);
|
}, [setOpenHistoryTipsModal, workspace.flavour]);
|
||||||
|
|
||||||
|
const setOpenInfoModal = useSetAtom(openInfoModalAtom);
|
||||||
|
const openInfoModal = () => {
|
||||||
|
setOpenInfoModal(true);
|
||||||
|
};
|
||||||
|
|
||||||
const handleOpenTrashModal = useCallback(() => {
|
const handleOpenTrashModal = useCallback(() => {
|
||||||
setTrashModal({
|
setTrashModal({
|
||||||
open: true,
|
open: true,
|
||||||
@@ -236,6 +245,35 @@ export const PageHeaderMenuButton = ({
|
|||||||
{t['com.affine.header.option.add-tag']()}
|
{t['com.affine.header.option.add-tag']()}
|
||||||
</MenuItem> */}
|
</MenuItem> */}
|
||||||
<MenuSeparator />
|
<MenuSeparator />
|
||||||
|
{runtimeConfig.enableInfoModal ? (
|
||||||
|
<MenuItem
|
||||||
|
preFix={
|
||||||
|
<MenuIcon>
|
||||||
|
<InformationIcon />
|
||||||
|
</MenuIcon>
|
||||||
|
}
|
||||||
|
data-testid="editor-option-menu-info"
|
||||||
|
onSelect={openInfoModal}
|
||||||
|
style={menuItemStyle}
|
||||||
|
>
|
||||||
|
{t['com.affine.page-properties.page-info.view']()}
|
||||||
|
</MenuItem>
|
||||||
|
) : null}
|
||||||
|
{runtimeConfig.enablePageHistory ? (
|
||||||
|
<MenuItem
|
||||||
|
preFix={
|
||||||
|
<MenuIcon>
|
||||||
|
<HistoryIcon />
|
||||||
|
</MenuIcon>
|
||||||
|
}
|
||||||
|
data-testid="editor-option-menu-history"
|
||||||
|
onSelect={openHistoryModal}
|
||||||
|
style={menuItemStyle}
|
||||||
|
>
|
||||||
|
{t['com.affine.history.view-history-version']()}
|
||||||
|
</MenuItem>
|
||||||
|
) : null}
|
||||||
|
<MenuSeparator />
|
||||||
{!isJournal && (
|
{!isJournal && (
|
||||||
<MenuItem
|
<MenuItem
|
||||||
preFix={
|
preFix={
|
||||||
@@ -264,21 +302,6 @@ export const PageHeaderMenuButton = ({
|
|||||||
</MenuItem>
|
</MenuItem>
|
||||||
<Export exportHandler={exportHandler} pageMode={currentMode} />
|
<Export exportHandler={exportHandler} pageMode={currentMode} />
|
||||||
|
|
||||||
{runtimeConfig.enablePageHistory ? (
|
|
||||||
<MenuItem
|
|
||||||
preFix={
|
|
||||||
<MenuIcon>
|
|
||||||
<HistoryIcon />
|
|
||||||
</MenuIcon>
|
|
||||||
}
|
|
||||||
data-testid="editor-option-menu-history"
|
|
||||||
onSelect={openHistoryModal}
|
|
||||||
style={menuItemStyle}
|
|
||||||
>
|
|
||||||
{t['com.affine.history.view-history-version']()}
|
|
||||||
</MenuItem>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
<MenuSeparator />
|
<MenuSeparator />
|
||||||
<MoveToTrash
|
<MoveToTrash
|
||||||
data-testid="editor-option-menu-delete"
|
data-testid="editor-option-menu-delete"
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
useDocMetaHelper,
|
useDocMetaHelper,
|
||||||
} from '@affine/core/hooks/use-block-suite-page-meta';
|
} from '@affine/core/hooks/use-block-suite-page-meta';
|
||||||
import type { DocCollection } from '@affine/core/shared';
|
import type { DocCollection } from '@affine/core/shared';
|
||||||
|
import clsx from 'clsx';
|
||||||
import type { HTMLAttributes } from 'react';
|
import type { HTMLAttributes } from 'react';
|
||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
|
|
||||||
@@ -16,6 +17,7 @@ export interface BlockSuiteHeaderTitleProps {
|
|||||||
/** if set, title cannot be edited */
|
/** if set, title cannot be edited */
|
||||||
isPublic?: boolean;
|
isPublic?: boolean;
|
||||||
inputHandleRef?: InlineEditProps['handleRef'];
|
inputHandleRef?: InlineEditProps['handleRef'];
|
||||||
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const inputAttrs = {
|
const inputAttrs = {
|
||||||
@@ -39,7 +41,7 @@ export const BlocksuiteHeaderTitle = (props: BlockSuiteHeaderTitleProps) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<InlineEdit
|
<InlineEdit
|
||||||
className={styles.title}
|
className={clsx(styles.title, props.className)}
|
||||||
autoSelect
|
autoSelect
|
||||||
value={title}
|
value={title}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import {
|
|||||||
FavoriteIcon,
|
FavoriteIcon,
|
||||||
FilterIcon,
|
FilterIcon,
|
||||||
FilterMinusIcon,
|
FilterMinusIcon,
|
||||||
|
InformationIcon,
|
||||||
MoreVerticalIcon,
|
MoreVerticalIcon,
|
||||||
PlusIcon,
|
PlusIcon,
|
||||||
ResetIcon,
|
ResetIcon,
|
||||||
@@ -36,6 +37,7 @@ import { useCallback, useState } from 'react';
|
|||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
import type { CollectionService } from '../../modules/collection';
|
import type { CollectionService } from '../../modules/collection';
|
||||||
|
import { InfoModal } from '../affine/page-properties';
|
||||||
import { usePageHelper } from '../blocksuite/block-suite-page-list/utils';
|
import { usePageHelper } from '../blocksuite/block-suite-page-list/utils';
|
||||||
import { FavoriteTag } from './components/favorite-tag';
|
import { FavoriteTag } from './components/favorite-tag';
|
||||||
import * as styles from './list.css';
|
import * as styles from './list.css';
|
||||||
@@ -65,6 +67,12 @@ export const PageOperationCell = ({
|
|||||||
const favourite = useLiveData(favAdapter.isFavorite$(page.id, 'doc'));
|
const favourite = useLiveData(favAdapter.isFavorite$(page.id, 'doc'));
|
||||||
const workbench = useService(WorkbenchService).workbench;
|
const workbench = useService(WorkbenchService).workbench;
|
||||||
const { duplicate } = useBlockSuiteMetaHelper(currentWorkspace.docCollection);
|
const { duplicate } = useBlockSuiteMetaHelper(currentWorkspace.docCollection);
|
||||||
|
const blocksuiteDoc = currentWorkspace.docCollection.getDoc(page.id);
|
||||||
|
|
||||||
|
const [openInfoModal, setOpenInfoModal] = useState(false);
|
||||||
|
const onOpenInfoModal = () => {
|
||||||
|
setOpenInfoModal(true);
|
||||||
|
};
|
||||||
|
|
||||||
const onDisablePublicSharing = useCallback(() => {
|
const onDisablePublicSharing = useCallback(() => {
|
||||||
toast('Successfully disabled', {
|
toast('Successfully disabled', {
|
||||||
@@ -144,6 +152,18 @@ export const PageOperationCell = ({
|
|||||||
? t['com.affine.favoritePageOperation.remove']()
|
? t['com.affine.favoritePageOperation.remove']()
|
||||||
: t['com.affine.favoritePageOperation.add']()}
|
: t['com.affine.favoritePageOperation.add']()}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
|
{runtimeConfig.enableInfoModal ? (
|
||||||
|
<MenuItem
|
||||||
|
onClick={onOpenInfoModal}
|
||||||
|
preFix={
|
||||||
|
<MenuIcon>
|
||||||
|
<InformationIcon />
|
||||||
|
</MenuIcon>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{t['com.affine.page-properties.page-info.view']()}
|
||||||
|
</MenuItem>
|
||||||
|
) : null}
|
||||||
|
|
||||||
{environment.isDesktop && appSettings.enableMultiView ? (
|
{environment.isDesktop && appSettings.enableMultiView ? (
|
||||||
<MenuItem
|
<MenuItem
|
||||||
@@ -215,6 +235,14 @@ export const PageOperationCell = ({
|
|||||||
</IconButton>
|
</IconButton>
|
||||||
</Menu>
|
</Menu>
|
||||||
</ColWrapper>
|
</ColWrapper>
|
||||||
|
{blocksuiteDoc ? (
|
||||||
|
<InfoModal
|
||||||
|
open={openInfoModal}
|
||||||
|
onOpenChange={setOpenInfoModal}
|
||||||
|
page={blocksuiteDoc}
|
||||||
|
workspace={currentWorkspace}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
<DisablePublicSharing.DisablePublicSharingModal
|
<DisablePublicSharing.DisablePublicSharingModal
|
||||||
onConfirm={onDisablePublicSharing}
|
onConfirm={onDisablePublicSharing}
|
||||||
open={openDisableShared}
|
open={openDisableShared}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
EditIcon,
|
EditIcon,
|
||||||
FavoriteIcon,
|
FavoriteIcon,
|
||||||
FilterMinusIcon,
|
FilterMinusIcon,
|
||||||
|
InformationIcon,
|
||||||
LinkedPageIcon,
|
LinkedPageIcon,
|
||||||
SplitViewIcon,
|
SplitViewIcon,
|
||||||
} from '@blocksuite/icons/rc';
|
} from '@blocksuite/icons/rc';
|
||||||
@@ -24,6 +25,7 @@ type OperationItemsProps = {
|
|||||||
onRemoveFromFavourites?: () => void;
|
onRemoveFromFavourites?: () => void;
|
||||||
onDelete: () => void;
|
onDelete: () => void;
|
||||||
onOpenInSplitView: () => void;
|
onOpenInSplitView: () => void;
|
||||||
|
onOpenInfoModal: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const OperationItems = ({
|
export const OperationItems = ({
|
||||||
@@ -36,6 +38,7 @@ export const OperationItems = ({
|
|||||||
onRemoveFromFavourites,
|
onRemoveFromFavourites,
|
||||||
onDelete,
|
onDelete,
|
||||||
onOpenInSplitView,
|
onOpenInSplitView,
|
||||||
|
onOpenInfoModal,
|
||||||
}: OperationItemsProps) => {
|
}: OperationItemsProps) => {
|
||||||
const { appSettings } = useAppSettingHelper();
|
const { appSettings } = useAppSettingHelper();
|
||||||
const t = useI18n();
|
const t = useI18n();
|
||||||
@@ -63,6 +66,19 @@ export const OperationItems = ({
|
|||||||
name: t['Rename'](),
|
name: t['Rename'](),
|
||||||
click: onRename,
|
click: onRename,
|
||||||
},
|
},
|
||||||
|
...(runtimeConfig.enableInfoModal
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
icon: (
|
||||||
|
<MenuIcon>
|
||||||
|
<InformationIcon />
|
||||||
|
</MenuIcon>
|
||||||
|
),
|
||||||
|
name: t['com.affine.page-properties.page-info.view'](),
|
||||||
|
click: onOpenInfoModal,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: []),
|
||||||
{
|
{
|
||||||
icon: (
|
icon: (
|
||||||
<MenuIcon>
|
<MenuIcon>
|
||||||
@@ -123,7 +139,7 @@ export const OperationItems = ({
|
|||||||
<DeleteIcon />
|
<DeleteIcon />
|
||||||
</MenuIcon>
|
</MenuIcon>
|
||||||
),
|
),
|
||||||
name: t['com.affine.trashOperation.delete'](),
|
name: t['com.affine.moveToTrash.title'](),
|
||||||
click: onDelete,
|
click: onDelete,
|
||||||
type: 'danger',
|
type: 'danger',
|
||||||
},
|
},
|
||||||
@@ -139,6 +155,7 @@ export const OperationItems = ({
|
|||||||
onRemoveFromAllowList,
|
onRemoveFromAllowList,
|
||||||
appSettings.enableMultiView,
|
appSettings.enableMultiView,
|
||||||
onOpenInSplitView,
|
onOpenInSplitView,
|
||||||
|
onOpenInfoModal,
|
||||||
onDelete,
|
onDelete,
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
import { toast } from '@affine/component';
|
import { toast } from '@affine/component';
|
||||||
import { IconButton } from '@affine/component/ui/button';
|
import { IconButton } from '@affine/component/ui/button';
|
||||||
import { Menu } from '@affine/component/ui/menu';
|
import { Menu } from '@affine/component/ui/menu';
|
||||||
|
import { InfoModal } from '@affine/core/components/affine/page-properties';
|
||||||
import { FavoriteItemsAdapter } from '@affine/core/modules/properties';
|
import { FavoriteItemsAdapter } from '@affine/core/modules/properties';
|
||||||
import { WorkbenchService } from '@affine/core/modules/workbench';
|
import { WorkbenchService } from '@affine/core/modules/workbench';
|
||||||
import { useI18n } from '@affine/i18n';
|
import { useI18n } from '@affine/i18n';
|
||||||
import { MoreHorizontalIcon } from '@blocksuite/icons/rc';
|
import { MoreHorizontalIcon } from '@blocksuite/icons/rc';
|
||||||
import { useService, useServices, WorkspaceService } from '@toeverything/infra';
|
import { useService, useServices, WorkspaceService } from '@toeverything/infra';
|
||||||
import { useCallback } from 'react';
|
import { useCallback, useState } from 'react';
|
||||||
|
|
||||||
import { useTrashModalHelper } from '../../../../hooks/affine/use-trash-modal-helper';
|
import { useTrashModalHelper } from '../../../../hooks/affine/use-trash-modal-helper';
|
||||||
import { usePageHelper } from '../../../blocksuite/block-suite-page-list/utils';
|
import { usePageHelper } from '../../../blocksuite/block-suite-page-list/utils';
|
||||||
@@ -33,9 +34,12 @@ export const OperationMenuButton = ({ ...props }: OperationMenuButtonProps) => {
|
|||||||
isReferencePage,
|
isReferencePage,
|
||||||
} = props;
|
} = props;
|
||||||
const t = useI18n();
|
const t = useI18n();
|
||||||
|
const [openInfoModal, setOpenInfoModal] = useState(false);
|
||||||
|
|
||||||
const { workspaceService } = useServices({
|
const { workspaceService } = useServices({
|
||||||
WorkspaceService,
|
WorkspaceService,
|
||||||
});
|
});
|
||||||
|
const page = workspaceService.workspace.docCollection.getDoc(pageId);
|
||||||
const { createLinkedPage } = usePageHelper(
|
const { createLinkedPage } = usePageHelper(
|
||||||
workspaceService.workspace.docCollection
|
workspaceService.workspace.docCollection
|
||||||
);
|
);
|
||||||
@@ -76,30 +80,45 @@ export const OperationMenuButton = ({ ...props }: OperationMenuButtonProps) => {
|
|||||||
workbench.openDoc(pageId, { at: 'tail' });
|
workbench.openDoc(pageId, { at: 'tail' });
|
||||||
}, [pageId, workbench]);
|
}, [pageId, workbench]);
|
||||||
|
|
||||||
|
const handleOpenInfoModal = useCallback(() => {
|
||||||
|
setOpenInfoModal(true);
|
||||||
|
}, [setOpenInfoModal]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Menu
|
<>
|
||||||
items={
|
<Menu
|
||||||
<OperationItems
|
items={
|
||||||
onAddLinkedPage={handleAddLinkedPage}
|
<OperationItems
|
||||||
onDelete={handleDelete}
|
onAddLinkedPage={handleAddLinkedPage}
|
||||||
onRemoveFromAllowList={handleRemoveFromAllowList}
|
onDelete={handleDelete}
|
||||||
onRemoveFromFavourites={handleRemoveFromFavourites}
|
onRemoveFromAllowList={handleRemoveFromAllowList}
|
||||||
onRename={handleRename}
|
onRemoveFromFavourites={handleRemoveFromFavourites}
|
||||||
onOpenInSplitView={handleOpenInSplitView}
|
onRename={handleRename}
|
||||||
inAllowList={inAllowList}
|
onOpenInSplitView={handleOpenInSplitView}
|
||||||
inFavorites={inFavorites}
|
onOpenInfoModal={handleOpenInfoModal}
|
||||||
isReferencePage={isReferencePage}
|
inAllowList={inAllowList}
|
||||||
/>
|
inFavorites={inFavorites}
|
||||||
}
|
isReferencePage={isReferencePage}
|
||||||
>
|
/>
|
||||||
<IconButton
|
}
|
||||||
size="small"
|
|
||||||
type="plain"
|
|
||||||
data-testid="left-sidebar-page-operation-button"
|
|
||||||
style={{ marginLeft: 4 }}
|
|
||||||
>
|
>
|
||||||
<MoreHorizontalIcon />
|
<IconButton
|
||||||
</IconButton>
|
size="small"
|
||||||
</Menu>
|
type="plain"
|
||||||
|
data-testid="left-sidebar-page-operation-button"
|
||||||
|
style={{ marginLeft: 4 }}
|
||||||
|
>
|
||||||
|
<MoreHorizontalIcon />
|
||||||
|
</IconButton>
|
||||||
|
</Menu>
|
||||||
|
{page ? (
|
||||||
|
<InfoModal
|
||||||
|
open={openInfoModal}
|
||||||
|
onOpenChange={setOpenInfoModal}
|
||||||
|
page={page}
|
||||||
|
workspace={workspaceService.workspace}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { toast } from '@affine/component';
|
import { toast } from '@affine/component';
|
||||||
|
import { openInfoModalAtom } from '@affine/core/atoms';
|
||||||
import {
|
import {
|
||||||
PreconditionStrategy,
|
PreconditionStrategy,
|
||||||
registerAffineCommand,
|
registerAffineCommand,
|
||||||
@@ -36,6 +37,7 @@ export function useRegisterBlocksuiteEditorCommands() {
|
|||||||
const trash = useLiveData(doc.trash$);
|
const trash = useLiveData(doc.trash$);
|
||||||
|
|
||||||
const setPageHistoryModalState = useSetAtom(pageHistoryModalAtom);
|
const setPageHistoryModalState = useSetAtom(pageHistoryModalAtom);
|
||||||
|
const setInfoModalState = useSetAtom(openInfoModalAtom);
|
||||||
|
|
||||||
const openHistoryModal = useCallback(() => {
|
const openHistoryModal = useCallback(() => {
|
||||||
setPageHistoryModalState(() => ({
|
setPageHistoryModalState(() => ({
|
||||||
@@ -44,8 +46,11 @@ export function useRegisterBlocksuiteEditorCommands() {
|
|||||||
}));
|
}));
|
||||||
}, [docId, setPageHistoryModalState]);
|
}, [docId, setPageHistoryModalState]);
|
||||||
|
|
||||||
const { restoreFromTrash, duplicate } =
|
const openInfoModal = useCallback(() => {
|
||||||
useBlockSuiteMetaHelper(docCollection);
|
setInfoModalState(true);
|
||||||
|
}, [setInfoModalState]);
|
||||||
|
|
||||||
|
const { duplicate } = useBlockSuiteMetaHelper(docCollection);
|
||||||
const exportHandler = useExportPage(doc.blockSuiteDoc);
|
const exportHandler = useExportPage(doc.blockSuiteDoc);
|
||||||
const { setTrashModal } = useTrashModalHelper(docCollection);
|
const { setTrashModal } = useTrashModalHelper(docCollection);
|
||||||
const onClickDelete = useCallback(
|
const onClickDelete = useCallback(
|
||||||
@@ -89,6 +94,22 @@ export function useRegisterBlocksuiteEditorCommands() {
|
|||||||
// })
|
// })
|
||||||
// );
|
// );
|
||||||
|
|
||||||
|
unsubs.push(
|
||||||
|
registerAffineCommand({
|
||||||
|
id: `editor:${mode}-view-info`,
|
||||||
|
preconditionStrategy: () =>
|
||||||
|
PreconditionStrategy.InPaperOrEdgeless &&
|
||||||
|
!trash &&
|
||||||
|
runtimeConfig.enableInfoModal,
|
||||||
|
category: `editor:${mode}`,
|
||||||
|
icon: mode === 'page' ? <PageIcon /> : <EdgelessIcon />,
|
||||||
|
label: t['com.affine.page-properties.page-info.view'](),
|
||||||
|
run() {
|
||||||
|
openInfoModal();
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
unsubs.push(
|
unsubs.push(
|
||||||
registerAffineCommand({
|
registerAffineCommand({
|
||||||
id: `editor:${mode}-${favorite ? 'remove-from' : 'add-to'}-favourites`,
|
id: `editor:${mode}-${favorite ? 'remove-from' : 'add-to'}-favourites`,
|
||||||
@@ -270,7 +291,6 @@ export function useRegisterBlocksuiteEditorCommands() {
|
|||||||
mode,
|
mode,
|
||||||
onClickDelete,
|
onClickDelete,
|
||||||
exportHandler,
|
exportHandler,
|
||||||
restoreFromTrash,
|
|
||||||
t,
|
t,
|
||||||
trash,
|
trash,
|
||||||
isCloudWorkspace,
|
isCloudWorkspace,
|
||||||
@@ -280,5 +300,6 @@ export function useRegisterBlocksuiteEditorCommands() {
|
|||||||
docId,
|
docId,
|
||||||
doc,
|
doc,
|
||||||
telemetry,
|
telemetry,
|
||||||
|
openInfoModal,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,3 +18,9 @@ export const journalWeekPicker = style({
|
|||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const iconButtonContainer = style({
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: 10,
|
||||||
|
});
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
import { Divider, type InlineEditHandle } from '@affine/component';
|
import { Divider, type InlineEditHandle } from '@affine/component';
|
||||||
|
import { openInfoModalAtom } from '@affine/core/atoms';
|
||||||
|
import { InfoModal } from '@affine/core/components/affine/page-properties';
|
||||||
import { FavoriteButton } from '@affine/core/components/blocksuite/block-suite-header/favorite';
|
import { FavoriteButton } from '@affine/core/components/blocksuite/block-suite-header/favorite';
|
||||||
|
import { InfoButton } from '@affine/core/components/blocksuite/block-suite-header/info';
|
||||||
import { JournalWeekDatePicker } from '@affine/core/components/blocksuite/block-suite-header/journal/date-picker';
|
import { JournalWeekDatePicker } from '@affine/core/components/blocksuite/block-suite-header/journal/date-picker';
|
||||||
import { JournalTodayButton } from '@affine/core/components/blocksuite/block-suite-header/journal/today-button';
|
import { JournalTodayButton } from '@affine/core/components/blocksuite/block-suite-header/journal/today-button';
|
||||||
import { PageHeaderMenuButton } from '@affine/core/components/blocksuite/block-suite-header/menu';
|
import { PageHeaderMenuButton } from '@affine/core/components/blocksuite/block-suite-header/menu';
|
||||||
@@ -9,7 +12,7 @@ import { useRegisterCopyLinkCommands } from '@affine/core/hooks/affine/use-regis
|
|||||||
import { useJournalInfoHelper } from '@affine/core/hooks/use-journal';
|
import { useJournalInfoHelper } from '@affine/core/hooks/use-journal';
|
||||||
import type { Doc } from '@blocksuite/store';
|
import type { Doc } from '@blocksuite/store';
|
||||||
import { type Workspace } from '@toeverything/infra';
|
import { type Workspace } from '@toeverything/infra';
|
||||||
import { useAtomValue } from 'jotai';
|
import { useAtom, useAtomValue } from 'jotai';
|
||||||
import { useCallback, useRef } from 'react';
|
import { useCallback, useRef } from 'react';
|
||||||
|
|
||||||
import { SharePageButton } from '../../../components/affine/share-page-modal';
|
import { SharePageButton } from '../../../components/affine/share-page-modal';
|
||||||
@@ -90,8 +93,16 @@ export function NormalPageHeader({ page, workspace }: PageHeaderProps) {
|
|||||||
pageId={page?.id}
|
pageId={page?.id}
|
||||||
docCollection={workspace.docCollection}
|
docCollection={workspace.docCollection}
|
||||||
/>
|
/>
|
||||||
{hideCollect ? null : <FavoriteButton pageId={page?.id} />}
|
<div className={styles.iconButtonContainer}>
|
||||||
<PageHeaderMenuButton rename={onRename} page={page} />
|
{hideCollect ? null : (
|
||||||
|
<>
|
||||||
|
<FavoriteButton pageId={page?.id} />
|
||||||
|
{runtimeConfig.enableInfoModal ? <InfoButton /> : null}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<PageHeaderMenuButton rename={onRename} page={page} />
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className={styles.spacer} />
|
<div className={styles.spacer} />
|
||||||
|
|
||||||
{!hidePresent ? <DetailPageHeaderPresentButton /> : null}
|
{!hidePresent ? <DetailPageHeaderPresentButton /> : null}
|
||||||
@@ -111,15 +122,26 @@ export function DetailPageHeader(props: PageHeaderProps) {
|
|||||||
const { page, workspace } = props;
|
const { page, workspace } = props;
|
||||||
const { isJournal } = useJournalInfoHelper(page.collection, page.id);
|
const { isJournal } = useJournalInfoHelper(page.collection, page.id);
|
||||||
const isInTrash = page.meta?.trash;
|
const isInTrash = page.meta?.trash;
|
||||||
|
const [openInfoModal, setOpenInfoModal] = useAtom(openInfoModalAtom);
|
||||||
|
|
||||||
useRegisterCopyLinkCommands({
|
useRegisterCopyLinkCommands({
|
||||||
workspaceMeta: workspace.meta,
|
workspaceMeta: workspace.meta,
|
||||||
docId: page.id,
|
docId: page.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
return isJournal && !isInTrash ? (
|
return (
|
||||||
<JournalPageHeader {...props} />
|
<>
|
||||||
) : (
|
{isJournal && !isInTrash ? (
|
||||||
<NormalPageHeader {...props} />
|
<JournalPageHeader {...props} />
|
||||||
|
) : (
|
||||||
|
<NormalPageHeader {...props} />
|
||||||
|
)}
|
||||||
|
<InfoModal
|
||||||
|
open={openInfoModal}
|
||||||
|
onOpenChange={setOpenInfoModal}
|
||||||
|
page={page}
|
||||||
|
workspace={workspace}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -846,6 +846,7 @@
|
|||||||
"com.affine.page-properties.create-property.menu.header": "Type",
|
"com.affine.page-properties.create-property.menu.header": "Type",
|
||||||
"com.affine.page-properties.icons": "Icons",
|
"com.affine.page-properties.icons": "Icons",
|
||||||
"com.affine.page-properties.page-info": "Info",
|
"com.affine.page-properties.page-info": "Info",
|
||||||
|
"com.affine.page-properties.page-info.view": "View Info",
|
||||||
"com.affine.page-properties.property-value-placeholder": "Empty",
|
"com.affine.page-properties.property-value-placeholder": "Empty",
|
||||||
"com.affine.page-properties.property.always-hide": "Always hide",
|
"com.affine.page-properties.property.always-hide": "Always hide",
|
||||||
"com.affine.page-properties.property.always-show": "Always show",
|
"com.affine.page-properties.property.always-show": "Always show",
|
||||||
|
|||||||
139
tests/affine-local/e2e/doc-info-modal.spec.ts
Normal file
139
tests/affine-local/e2e/doc-info-modal.spec.ts
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
import { test } from '@affine-test/kit/playwright';
|
||||||
|
import { openHomePage } from '@affine-test/kit/utils/load-page';
|
||||||
|
import {
|
||||||
|
clickNewPageButton,
|
||||||
|
clickPageMoreActions,
|
||||||
|
getBlockSuiteEditorTitle,
|
||||||
|
getPageByTitle,
|
||||||
|
getPageOperationButton,
|
||||||
|
waitForEmptyEditor,
|
||||||
|
} from '@affine-test/kit/utils/page-logic';
|
||||||
|
import {
|
||||||
|
addCustomProperty,
|
||||||
|
closeTagsEditor,
|
||||||
|
ensurePagePropertiesVisible,
|
||||||
|
expectTagsVisible,
|
||||||
|
filterTags,
|
||||||
|
removeSelectedTag,
|
||||||
|
} from '@affine-test/kit/utils/properties';
|
||||||
|
import { expect, type Page } from '@playwright/test';
|
||||||
|
|
||||||
|
const searchAndCreateTag = async (page: Page, name: string) => {
|
||||||
|
await filterTags(page, name);
|
||||||
|
await page
|
||||||
|
.locator(
|
||||||
|
'[data-testid="tags-editor-popup"] [data-testid="tag-selector-item"]:has-text("Create ")'
|
||||||
|
)
|
||||||
|
.click();
|
||||||
|
};
|
||||||
|
|
||||||
|
test.beforeEach(async ({ page }) => {
|
||||||
|
await openHomePage(page);
|
||||||
|
await clickNewPageButton(page);
|
||||||
|
await waitForEmptyEditor(page);
|
||||||
|
await ensurePagePropertiesVisible(page);
|
||||||
|
await getBlockSuiteEditorTitle(page).click();
|
||||||
|
await getBlockSuiteEditorTitle(page).fill('this is a new page');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('New a page and open it ,then open info modal in the title bar', async ({
|
||||||
|
page,
|
||||||
|
}) => {
|
||||||
|
await page.getByTestId('header-info-button').click();
|
||||||
|
|
||||||
|
const infoModal = page.getByTestId('info-modal');
|
||||||
|
await expect(infoModal).toBeVisible();
|
||||||
|
const tagRow = page.getByTestId('info-modal-tags-row');
|
||||||
|
await expect(tagRow).toBeVisible();
|
||||||
|
const title = page.getByTestId('info-modal-title');
|
||||||
|
await expect(title).toHaveText('this is a new page');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('New a page and open it ,then open info modal in the title bar more action button', async ({
|
||||||
|
page,
|
||||||
|
}) => {
|
||||||
|
await clickPageMoreActions(page);
|
||||||
|
await page.getByTestId('editor-option-menu-info').click();
|
||||||
|
|
||||||
|
const infoModal = page.getByTestId('info-modal');
|
||||||
|
await expect(infoModal).toBeVisible();
|
||||||
|
const tagRow = page.getByTestId('info-modal-tags-row');
|
||||||
|
await expect(tagRow).toBeVisible();
|
||||||
|
const title = page.getByTestId('info-modal-title');
|
||||||
|
await expect(title).toHaveText('this is a new page');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('New a page, then open info modal from all doc', async ({ page }) => {
|
||||||
|
const newPageId = page.url().split('/').reverse()[0];
|
||||||
|
|
||||||
|
await page.getByTestId('all-pages').click();
|
||||||
|
const cell = getPageByTitle(page, 'this is a new page');
|
||||||
|
expect(cell).not.toBeUndefined();
|
||||||
|
await getPageOperationButton(page, newPageId).click();
|
||||||
|
await page.getByRole('menuitem', { name: 'View Info' }).click();
|
||||||
|
|
||||||
|
const infoModal = page.getByTestId('info-modal');
|
||||||
|
await expect(infoModal).toBeVisible();
|
||||||
|
const tagRow = page.getByTestId('info-modal-tags-row');
|
||||||
|
await expect(tagRow).toBeVisible();
|
||||||
|
const title = page.getByTestId('info-modal-title');
|
||||||
|
await expect(title).toHaveText('this is a new page');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('New a page and add to favourites, then open info modal from sidebar', async ({
|
||||||
|
page,
|
||||||
|
}) => {
|
||||||
|
const newPageId = page.url().split('/').reverse()[0];
|
||||||
|
|
||||||
|
await clickPageMoreActions(page);
|
||||||
|
await page.getByTestId('editor-option-menu-favorite').click();
|
||||||
|
|
||||||
|
await page.getByTestId('all-pages').click();
|
||||||
|
const favoriteListItemInSidebar = page.getByTestId(
|
||||||
|
'favourite-page-' + newPageId
|
||||||
|
);
|
||||||
|
expect(await favoriteListItemInSidebar.textContent()).toBe(
|
||||||
|
'this is a new page'
|
||||||
|
);
|
||||||
|
await favoriteListItemInSidebar.hover();
|
||||||
|
await favoriteListItemInSidebar
|
||||||
|
.getByTestId('left-sidebar-page-operation-button')
|
||||||
|
.click();
|
||||||
|
const infoBtn = page.getByText('View Info');
|
||||||
|
await infoBtn.click();
|
||||||
|
|
||||||
|
const infoModal = page.getByTestId('info-modal');
|
||||||
|
await expect(infoModal).toBeVisible();
|
||||||
|
const tagRow = page.getByTestId('info-modal-tags-row');
|
||||||
|
await expect(tagRow).toBeVisible();
|
||||||
|
const title = page.getByTestId('info-modal-title');
|
||||||
|
await expect(title).toHaveText('this is a new page');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('allow create tag', async ({ page }) => {
|
||||||
|
await page.getByTestId('header-info-button').click();
|
||||||
|
|
||||||
|
const infoModal = page.getByTestId('info-modal');
|
||||||
|
await expect(infoModal).toBeVisible();
|
||||||
|
await page.getByTestId('info-modal-tags-value').click();
|
||||||
|
await searchAndCreateTag(page, 'Test1');
|
||||||
|
await searchAndCreateTag(page, 'Test2');
|
||||||
|
await closeTagsEditor(page);
|
||||||
|
await expectTagsVisible(page, ['Test1', 'Test2']);
|
||||||
|
|
||||||
|
await page.getByTestId('info-modal-tags-value').click();
|
||||||
|
await removeSelectedTag(page, 'Test1');
|
||||||
|
await closeTagsEditor(page);
|
||||||
|
await expectTagsVisible(page, ['Test2']);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('add custom property', async ({ page }) => {
|
||||||
|
await page.getByTestId('header-info-button').click();
|
||||||
|
|
||||||
|
const infoModal = page.getByTestId('info-modal');
|
||||||
|
await expect(infoModal).toBeVisible();
|
||||||
|
await addCustomProperty(page, 'Text');
|
||||||
|
await addCustomProperty(page, 'Number');
|
||||||
|
await addCustomProperty(page, 'Date');
|
||||||
|
await addCustomProperty(page, 'Checkbox');
|
||||||
|
});
|
||||||
@@ -76,7 +76,7 @@ test('Show collections items in sidebar', async ({ page }) => {
|
|||||||
await collectionPage
|
await collectionPage
|
||||||
.getByTestId('left-sidebar-page-operation-button')
|
.getByTestId('left-sidebar-page-operation-button')
|
||||||
.click();
|
.click();
|
||||||
const deletePage = page.getByText('Delete');
|
const deletePage = page.getByText('Move to Trash');
|
||||||
await deletePage.click();
|
await deletePage.click();
|
||||||
await page.getByTestId('confirm-delete-page').click();
|
await page.getByTestId('confirm-delete-page').click();
|
||||||
expect(await collections.getByTestId('collection-page').count()).toBe(0);
|
expect(await collections.getByTestId('collection-page').count()).toBe(0);
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ export function getRuntimeConfig(buildFlags: BuildFlags): RuntimeConfig {
|
|||||||
enablePayment: true,
|
enablePayment: true,
|
||||||
enablePageHistory: true,
|
enablePageHistory: true,
|
||||||
enableExperimentalFeature: false,
|
enableExperimentalFeature: false,
|
||||||
|
enableInfoModal: false,
|
||||||
allowLocalWorkspace: buildFlags.distribution === 'desktop' ? true : false,
|
allowLocalWorkspace: buildFlags.distribution === 'desktop' ? true : false,
|
||||||
serverUrlPrefix: 'https://app.affine.pro',
|
serverUrlPrefix: 'https://app.affine.pro',
|
||||||
appVersion: packageJson.version,
|
appVersion: packageJson.version,
|
||||||
@@ -63,6 +64,7 @@ export function getRuntimeConfig(buildFlags: BuildFlags): RuntimeConfig {
|
|||||||
enablePayment: true,
|
enablePayment: true,
|
||||||
enablePageHistory: true,
|
enablePageHistory: true,
|
||||||
enableExperimentalFeature: true,
|
enableExperimentalFeature: true,
|
||||||
|
enableInfoModal: true,
|
||||||
allowLocalWorkspace: buildFlags.distribution === 'desktop' ? true : false,
|
allowLocalWorkspace: buildFlags.distribution === 'desktop' ? true : false,
|
||||||
serverUrlPrefix: 'https://affine.fail',
|
serverUrlPrefix: 'https://affine.fail',
|
||||||
appVersion: packageJson.version,
|
appVersion: packageJson.version,
|
||||||
|
|||||||
Reference in New Issue
Block a user