@@ -1074,7 +1085,7 @@ const PagePropertiesTableInner = () => {
);
};
-const usePagePropertiesManager = (page: Doc) => {
+export const usePagePropertiesManager = (page: Doc) => {
// the workspace properties adapter adapter is reactive,
// which means it's reference will change when any of the properties change
// also it will trigger a re-render of the component
diff --git a/packages/frontend/core/src/components/affine/page-properties/tags-inline-editor.tsx b/packages/frontend/core/src/components/affine/page-properties/tags-inline-editor.tsx
index cf28fe666d..4ca3061497 100644
--- a/packages/frontend/core/src/components/affine/page-properties/tags-inline-editor.tsx
+++ b/packages/frontend/core/src/components/affine/page-properties/tags-inline-editor.tsx
@@ -30,7 +30,7 @@ interface InlineTagsListProps
onRemove?: () => void;
}
-const InlineTagsList = ({
+export const InlineTagsList = ({
pageId,
readonly,
children,
diff --git a/packages/frontend/core/src/components/blocksuite/block-suite-header/info/index.tsx b/packages/frontend/core/src/components/blocksuite/block-suite-header/info/index.tsx
new file mode 100644
index 0000000000..52b6263652
--- /dev/null
+++ b/packages/frontend/core/src/components/blocksuite/block-suite-header/info/index.tsx
@@ -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 (
+
+ }
+ />
+
+ );
+};
diff --git a/packages/frontend/core/src/components/blocksuite/block-suite-header/menu/index.tsx b/packages/frontend/core/src/components/blocksuite/block-suite-header/menu/index.tsx
index 5dc6b5a3b3..3e14e208f3 100644
--- a/packages/frontend/core/src/components/blocksuite/block-suite-header/menu/index.tsx
+++ b/packages/frontend/core/src/components/blocksuite/block-suite-header/menu/index.tsx
@@ -6,7 +6,10 @@ import {
MenuSeparator,
MenuSub,
} 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 { ShareMenuContent } from '@affine/core/components/affine/share-page-modal/share-menu';
import { Export, MoveToTrash } from '@affine/core/components/page-list';
@@ -27,6 +30,7 @@ import {
FavoriteIcon,
HistoryIcon,
ImportIcon,
+ InformationIcon,
PageIcon,
ShareIcon,
} from '@blocksuite/icons/rc';
@@ -83,6 +87,11 @@ export const PageHeaderMenuButton = ({
return setOpenHistoryTipsModal(true);
}, [setOpenHistoryTipsModal, workspace.flavour]);
+ const setOpenInfoModal = useSetAtom(openInfoModalAtom);
+ const openInfoModal = () => {
+ setOpenInfoModal(true);
+ };
+
const handleOpenTrashModal = useCallback(() => {
setTrashModal({
open: true,
@@ -236,6 +245,35 @@ export const PageHeaderMenuButton = ({
{t['com.affine.header.option.add-tag']()}
*/}
+ {runtimeConfig.enableInfoModal ? (
+
+ ) : null}
+ {runtimeConfig.enablePageHistory ? (
+
+ ) : null}
+
{!isJournal && (
+ {runtimeConfig.enableInfoModal ? (
+
+
+
+ }
+ >
+ {t['com.affine.page-properties.page-info.view']()}
+
+ ) : null}
{environment.isDesktop && appSettings.enableMultiView ? (
+ {blocksuiteDoc ? (
+
+ ) : null}
void;
onDelete: () => void;
onOpenInSplitView: () => void;
+ onOpenInfoModal: () => void;
};
export const OperationItems = ({
@@ -36,6 +38,7 @@ export const OperationItems = ({
onRemoveFromFavourites,
onDelete,
onOpenInSplitView,
+ onOpenInfoModal,
}: OperationItemsProps) => {
const { appSettings } = useAppSettingHelper();
const t = useI18n();
@@ -63,6 +66,19 @@ export const OperationItems = ({
name: t['Rename'](),
click: onRename,
},
+ ...(runtimeConfig.enableInfoModal
+ ? [
+ {
+ icon: (
+
+
+
+ ),
+ name: t['com.affine.page-properties.page-info.view'](),
+ click: onOpenInfoModal,
+ },
+ ]
+ : []),
{
icon: (
@@ -123,7 +139,7 @@ export const OperationItems = ({
),
- name: t['com.affine.trashOperation.delete'](),
+ name: t['com.affine.moveToTrash.title'](),
click: onDelete,
type: 'danger',
},
@@ -139,6 +155,7 @@ export const OperationItems = ({
onRemoveFromAllowList,
appSettings.enableMultiView,
onOpenInSplitView,
+ onOpenInfoModal,
onDelete,
]
);
diff --git a/packages/frontend/core/src/components/pure/workspace-slider-bar/components/operation-menu-button.tsx b/packages/frontend/core/src/components/pure/workspace-slider-bar/components/operation-menu-button.tsx
index 846fba7c9a..9b611662c3 100644
--- a/packages/frontend/core/src/components/pure/workspace-slider-bar/components/operation-menu-button.tsx
+++ b/packages/frontend/core/src/components/pure/workspace-slider-bar/components/operation-menu-button.tsx
@@ -1,12 +1,13 @@
import { toast } from '@affine/component';
import { IconButton } from '@affine/component/ui/button';
import { Menu } from '@affine/component/ui/menu';
+import { InfoModal } from '@affine/core/components/affine/page-properties';
import { FavoriteItemsAdapter } from '@affine/core/modules/properties';
import { WorkbenchService } from '@affine/core/modules/workbench';
import { useI18n } from '@affine/i18n';
import { MoreHorizontalIcon } from '@blocksuite/icons/rc';
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 { usePageHelper } from '../../../blocksuite/block-suite-page-list/utils';
@@ -33,9 +34,12 @@ export const OperationMenuButton = ({ ...props }: OperationMenuButtonProps) => {
isReferencePage,
} = props;
const t = useI18n();
+ const [openInfoModal, setOpenInfoModal] = useState(false);
+
const { workspaceService } = useServices({
WorkspaceService,
});
+ const page = workspaceService.workspace.docCollection.getDoc(pageId);
const { createLinkedPage } = usePageHelper(
workspaceService.workspace.docCollection
);
@@ -76,30 +80,45 @@ export const OperationMenuButton = ({ ...props }: OperationMenuButtonProps) => {
workbench.openDoc(pageId, { at: 'tail' });
}, [pageId, workbench]);
+ const handleOpenInfoModal = useCallback(() => {
+ setOpenInfoModal(true);
+ }, [setOpenInfoModal]);
+
return (
-
- }
- >
-
+
+ }
>
-
-
-
+
+
+
+
+ {page ? (
+
+ ) : null}
+ >
);
};
diff --git a/packages/frontend/core/src/hooks/affine/use-register-blocksuite-editor-commands.tsx b/packages/frontend/core/src/hooks/affine/use-register-blocksuite-editor-commands.tsx
index ac2ed9b409..147c5bf1c5 100644
--- a/packages/frontend/core/src/hooks/affine/use-register-blocksuite-editor-commands.tsx
+++ b/packages/frontend/core/src/hooks/affine/use-register-blocksuite-editor-commands.tsx
@@ -1,4 +1,5 @@
import { toast } from '@affine/component';
+import { openInfoModalAtom } from '@affine/core/atoms';
import {
PreconditionStrategy,
registerAffineCommand,
@@ -36,6 +37,7 @@ export function useRegisterBlocksuiteEditorCommands() {
const trash = useLiveData(doc.trash$);
const setPageHistoryModalState = useSetAtom(pageHistoryModalAtom);
+ const setInfoModalState = useSetAtom(openInfoModalAtom);
const openHistoryModal = useCallback(() => {
setPageHistoryModalState(() => ({
@@ -44,8 +46,11 @@ export function useRegisterBlocksuiteEditorCommands() {
}));
}, [docId, setPageHistoryModalState]);
- const { restoreFromTrash, duplicate } =
- useBlockSuiteMetaHelper(docCollection);
+ const openInfoModal = useCallback(() => {
+ setInfoModalState(true);
+ }, [setInfoModalState]);
+
+ const { duplicate } = useBlockSuiteMetaHelper(docCollection);
const exportHandler = useExportPage(doc.blockSuiteDoc);
const { setTrashModal } = useTrashModalHelper(docCollection);
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' ? : ,
+ label: t['com.affine.page-properties.page-info.view'](),
+ run() {
+ openInfoModal();
+ },
+ })
+ );
+
unsubs.push(
registerAffineCommand({
id: `editor:${mode}-${favorite ? 'remove-from' : 'add-to'}-favourites`,
@@ -270,7 +291,6 @@ export function useRegisterBlocksuiteEditorCommands() {
mode,
onClickDelete,
exportHandler,
- restoreFromTrash,
t,
trash,
isCloudWorkspace,
@@ -280,5 +300,6 @@ export function useRegisterBlocksuiteEditorCommands() {
docId,
doc,
telemetry,
+ openInfoModal,
]);
}
diff --git a/packages/frontend/core/src/pages/workspace/detail-page/detail-page-header.css.ts b/packages/frontend/core/src/pages/workspace/detail-page/detail-page-header.css.ts
index 98974cc607..3b787fd3fa 100644
--- a/packages/frontend/core/src/pages/workspace/detail-page/detail-page-header.css.ts
+++ b/packages/frontend/core/src/pages/workspace/detail-page/detail-page-header.css.ts
@@ -18,3 +18,9 @@ export const journalWeekPicker = style({
alignItems: 'center',
justifyContent: 'center',
});
+
+export const iconButtonContainer = style({
+ display: 'flex',
+ alignItems: 'center',
+ gap: 10,
+});
diff --git a/packages/frontend/core/src/pages/workspace/detail-page/detail-page-header.tsx b/packages/frontend/core/src/pages/workspace/detail-page/detail-page-header.tsx
index b30a39cdbb..beffb8f5c5 100644
--- a/packages/frontend/core/src/pages/workspace/detail-page/detail-page-header.tsx
+++ b/packages/frontend/core/src/pages/workspace/detail-page/detail-page-header.tsx
@@ -1,5 +1,8 @@
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 { InfoButton } from '@affine/core/components/blocksuite/block-suite-header/info';
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 { 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 type { Doc } from '@blocksuite/store';
import { type Workspace } from '@toeverything/infra';
-import { useAtomValue } from 'jotai';
+import { useAtom, useAtomValue } from 'jotai';
import { useCallback, useRef } from 'react';
import { SharePageButton } from '../../../components/affine/share-page-modal';
@@ -90,8 +93,16 @@ export function NormalPageHeader({ page, workspace }: PageHeaderProps) {
pageId={page?.id}
docCollection={workspace.docCollection}
/>
- {hideCollect ? null : }
-
+
+ {hideCollect ? null : (
+ <>
+
+ {runtimeConfig.enableInfoModal ?
: null}
+ >
+ )}
+
+
+
{!hidePresent ? : null}
@@ -111,15 +122,26 @@ export function DetailPageHeader(props: PageHeaderProps) {
const { page, workspace } = props;
const { isJournal } = useJournalInfoHelper(page.collection, page.id);
const isInTrash = page.meta?.trash;
+ const [openInfoModal, setOpenInfoModal] = useAtom(openInfoModalAtom);
useRegisterCopyLinkCommands({
workspaceMeta: workspace.meta,
docId: page.id,
});
- return isJournal && !isInTrash ? (
-
- ) : (
-
+ return (
+ <>
+ {isJournal && !isInTrash ? (
+
+ ) : (
+
+ )}
+
+ >
);
}
diff --git a/packages/frontend/i18n/src/resources/en.json b/packages/frontend/i18n/src/resources/en.json
index 729c45b219..debaa364a6 100644
--- a/packages/frontend/i18n/src/resources/en.json
+++ b/packages/frontend/i18n/src/resources/en.json
@@ -846,6 +846,7 @@
"com.affine.page-properties.create-property.menu.header": "Type",
"com.affine.page-properties.icons": "Icons",
"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.always-hide": "Always hide",
"com.affine.page-properties.property.always-show": "Always show",
diff --git a/tests/affine-local/e2e/doc-info-modal.spec.ts b/tests/affine-local/e2e/doc-info-modal.spec.ts
new file mode 100644
index 0000000000..44399cc3d0
--- /dev/null
+++ b/tests/affine-local/e2e/doc-info-modal.spec.ts
@@ -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');
+});
diff --git a/tests/affine-local/e2e/local-first-collections-items.spec.ts b/tests/affine-local/e2e/local-first-collections-items.spec.ts
index 55d7ff54f3..c4be5b5e17 100644
--- a/tests/affine-local/e2e/local-first-collections-items.spec.ts
+++ b/tests/affine-local/e2e/local-first-collections-items.spec.ts
@@ -76,7 +76,7 @@ test('Show collections items in sidebar', async ({ page }) => {
await collectionPage
.getByTestId('left-sidebar-page-operation-button')
.click();
- const deletePage = page.getByText('Delete');
+ const deletePage = page.getByText('Move to Trash');
await deletePage.click();
await page.getByTestId('confirm-delete-page').click();
expect(await collections.getByTestId('collection-page').count()).toBe(0);
diff --git a/tools/cli/src/webpack/runtime-config.ts b/tools/cli/src/webpack/runtime-config.ts
index 588a7e6735..360fc2ba28 100644
--- a/tools/cli/src/webpack/runtime-config.ts
+++ b/tools/cli/src/webpack/runtime-config.ts
@@ -23,6 +23,7 @@ export function getRuntimeConfig(buildFlags: BuildFlags): RuntimeConfig {
enablePayment: true,
enablePageHistory: true,
enableExperimentalFeature: false,
+ enableInfoModal: false,
allowLocalWorkspace: buildFlags.distribution === 'desktop' ? true : false,
serverUrlPrefix: 'https://app.affine.pro',
appVersion: packageJson.version,
@@ -63,6 +64,7 @@ export function getRuntimeConfig(buildFlags: BuildFlags): RuntimeConfig {
enablePayment: true,
enablePageHistory: true,
enableExperimentalFeature: true,
+ enableInfoModal: true,
allowLocalWorkspace: buildFlags.distribution === 'desktop' ? true : false,
serverUrlPrefix: 'https://affine.fail',
appVersion: packageJson.version,