diff --git a/packages/common/env/src/workspace.ts b/packages/common/env/src/workspace.ts index 91ed56f6a..b92b8e352 100644 --- a/packages/common/env/src/workspace.ts +++ b/packages/common/env/src/workspace.ts @@ -1,5 +1,3 @@ -import type { EditorContainer } from '@blocksuite/presets'; -import type { Page } from '@blocksuite/store'; import type { ActiveDocProvider, PassiveDocProvider, @@ -134,18 +132,6 @@ type UIBaseProps<_Flavour extends keyof WorkspaceRegistry> = { currentWorkspaceId: string; }; -export type WorkspaceHeaderProps = - UIBaseProps & { - rightSlot?: ReactNode; - currentEntry: - | { - subPath: WorkspaceSubPath; - } - | { - pageId: string; - }; - }; - type NewSettingProps = UIBaseProps & { onDeleteLocalWorkspace: () => void; @@ -161,18 +147,11 @@ type NewSettingProps = ) => void; }; -type PageDetailProps = - UIBaseProps & { - currentPageId: string; - onLoadEditor: (page: Page, editor: EditorContainer) => () => void; - }; - interface FC

{ (props: P): ReactNode; } export interface WorkspaceUISchema { - PageDetail: FC>; NewSettingsDetail: FC>; Provider: FC; LoginCard?: FC; diff --git a/packages/frontend/component/src/components/workspace/index.css.ts b/packages/frontend/component/src/components/workspace/index.css.ts index ce31f3cd3..a5a91dee9 100644 --- a/packages/frontend/component/src/components/workspace/index.css.ts +++ b/packages/frontend/component/src/components/workspace/index.css.ts @@ -54,13 +54,11 @@ export const mainContainerStyle = style({ width: 0, flex: 1, maxWidth: '100%', - backgroundColor: 'var(--affine-background-primary-color)', selectors: { '&[data-show-padding="true"]': { margin: '8px', borderRadius: '5px', overflow: 'hidden', - boxShadow: 'var(--affine-shadow-1)', '@media': { print: { overflow: 'visible', @@ -72,12 +70,6 @@ export const mainContainerStyle = style({ '&[data-show-padding="true"][data-is-macos="true"]': { borderRadius: '6px', }, - '&[data-in-trash-page="true"]': { - marginBottom: '66px', - }, - '&[data-in-trash-page="true"][data-show-padding="true"]': { - marginBottom: '66px', - }, '&[data-show-padding="true"]:before': { content: '""', position: 'absolute', @@ -124,7 +116,7 @@ globalStyle( ); export const toolStyle = style({ - position: 'fixed', + position: 'absolute', right: '30px', bottom: '30px', zIndex: 'var(--affine-z-index-popover)', @@ -143,20 +135,4 @@ export const toolStyle = style({ display: 'none', }, }, - selectors: { - '&[data-in-trash-page="true"]': { - bottom: '70px', - '@media': { - 'screen and (max-width: 960px)': { - bottom: '80px', - }, - 'screen and (max-width: 640px)': { - bottom: '85px', - }, - print: { - display: 'none', - }, - }, - }, - }, }); diff --git a/packages/frontend/component/src/components/workspace/index.tsx b/packages/frontend/component/src/components/workspace/index.tsx index 347266012..157df7861 100644 --- a/packages/frontend/component/src/components/workspace/index.tsx +++ b/packages/frontend/component/src/components/workspace/index.tsx @@ -35,14 +35,13 @@ export const AppContainer = ({ export interface MainContainerProps extends HTMLAttributes { className?: string; padding?: boolean; - inTrashPage?: boolean; } export const MainContainer = forwardRef< HTMLDivElement, PropsWithChildren >(function MainContainer( - { className, padding, inTrashPage, children, ...props }, + { className, padding, children, ...props }, ref ): ReactElement { return ( @@ -51,7 +50,6 @@ export const MainContainer = forwardRef< className={clsx(mainContainerStyle, className)} data-is-macos={environment.isDesktop && environment.isMacOs} data-show-padding={!!padding} - data-in-trash-page={!!inTrashPage} ref={ref} > {children} @@ -61,14 +59,8 @@ export const MainContainer = forwardRef< MainContainer.displayName = 'MainContainer'; -export const ToolContainer = ( - props: PropsWithChildren & { inTrashPage: boolean } -): ReactElement => { - return ( -
- {props.children} -
- ); +export const ToolContainer = (props: PropsWithChildren): ReactElement => { + return
{props.children}
; }; export const WorkspaceFallback = (): ReactElement => { diff --git a/packages/frontend/component/src/ui/button/button.css.ts b/packages/frontend/component/src/ui/button/button.css.ts index f74f0a266..bb48c4c0e 100644 --- a/packages/frontend/component/src/ui/button/button.css.ts +++ b/packages/frontend/component/src/ui/button/button.css.ts @@ -6,6 +6,7 @@ export const button = style({ alignItems: 'center', userSelect: 'none', touchAction: 'manipulation', + flexShrink: 0, outline: '0', border: '1px solid', padding: '0 18px', diff --git a/packages/frontend/core/src/adapters/cloud/ui.tsx b/packages/frontend/core/src/adapters/cloud/ui.tsx index 08e113719..af1cca88c 100644 --- a/packages/frontend/core/src/adapters/cloud/ui.tsx +++ b/packages/frontend/core/src/adapters/cloud/ui.tsx @@ -1,19 +1,11 @@ -import { PageNotFoundError } from '@affine/env/constant'; import type { WorkspaceFlavour, WorkspaceUISchema, } from '@affine/env/workspace'; -import { lazy, useCallback } from 'react'; +import { lazy } from 'react'; -import type { OnLoadEditor } from '../../components/page-detail-editor'; -import { useCurrentUser } from '../../hooks/affine/use-current-user'; import { useIsWorkspaceOwner } from '../../hooks/affine/use-is-workspace-owner'; -import { useWorkspace } from '../../hooks/use-workspace'; -import { - NewWorkspaceSettingDetail, - PageDetailEditor, - Provider, -} from '../shared'; +import { NewWorkspaceSettingDetail, Provider } from '../shared'; const LoginCard = lazy(() => import('../../components/cloud/login-card').then(({ LoginCard }) => ({ @@ -24,36 +16,6 @@ const LoginCard = lazy(() => export const UI = { Provider, LoginCard, - PageDetail: ({ currentWorkspaceId, currentPageId, onLoadEditor }) => { - const workspace = useWorkspace(currentWorkspaceId); - const page = workspace.blockSuiteWorkspace.getPage(currentPageId); - if (!page) { - throw new PageNotFoundError(workspace.blockSuiteWorkspace, currentPageId); - } - // this should be safe because we are under cloud workspace adapter - const currentUser = useCurrentUser(); - const onLoad = useCallback( - (...args) => { - const dispose = onLoadEditor(...args); - workspace.blockSuiteWorkspace.awarenessStore.awareness.setLocalStateField( - 'user', - { - name: currentUser.name, - } - ); - return dispose; - }, - [currentUser, workspace, onLoadEditor] - ); - - return ( - - ); - }, NewSettingsDetail: ({ currentWorkspaceId, onTransformWorkspace, diff --git a/packages/frontend/core/src/adapters/local/index.tsx b/packages/frontend/core/src/adapters/local/index.tsx index 1b99823f2..fbb0bee26 100644 --- a/packages/frontend/core/src/adapters/local/index.tsx +++ b/packages/frontend/core/src/adapters/local/index.tsx @@ -1,8 +1,5 @@ import { DebugLogger } from '@affine/debug'; -import { - DEFAULT_WORKSPACE_NAME, - PageNotFoundError, -} from '@affine/env/constant'; +import { DEFAULT_WORKSPACE_NAME } from '@affine/env/constant'; import type { WorkspaceAdapter } from '@affine/env/workspace'; import { LoadPriority, @@ -14,19 +11,13 @@ import { saveWorkspaceToLocalStorage, } from '@affine/workspace/local/crud'; import { getOrCreateWorkspace } from '@affine/workspace/manager'; -import { getBlockSuiteWorkspaceAtom } from '@toeverything/infra/__internal__/workspace'; import { getCurrentStore } from '@toeverything/infra/atom'; import { initEmptyPage } from '@toeverything/infra/blocksuite'; import { buildShowcaseWorkspace } from '@toeverything/infra/blocksuite'; -import { useAtomValue } from 'jotai'; import { nanoid } from 'nanoid'; import { setPageModeAtom } from '../../atoms'; -import { - NewWorkspaceSettingDetail, - PageDetailEditor, - Provider, -} from '../shared'; +import { NewWorkspaceSettingDetail, Provider } from '../shared'; const logger = new DebugLogger('use-create-first-workspace'); @@ -68,21 +59,6 @@ export const LocalAdapter: WorkspaceAdapter = { CRUD, UI: { Provider, - PageDetail: ({ currentWorkspaceId, currentPageId, onLoadEditor }) => { - const [workspaceAtom] = getBlockSuiteWorkspaceAtom(currentWorkspaceId); - const workspace = useAtomValue(workspaceAtom); - const page = workspace.getPage(currentPageId); - if (!page) { - throw new PageNotFoundError(workspace, currentPageId); - } - return ( - - ); - }, NewSettingsDetail: ({ currentWorkspaceId, onTransformWorkspace, diff --git a/packages/frontend/core/src/adapters/public-cloud/ui.tsx b/packages/frontend/core/src/adapters/public-cloud/ui.tsx index 120be6362..0e033dd58 100644 --- a/packages/frontend/core/src/adapters/public-cloud/ui.tsx +++ b/packages/frontend/core/src/adapters/public-cloud/ui.tsx @@ -1,26 +1,10 @@ -import { PageNotFoundError } from '@affine/env/constant'; import type { WorkspaceFlavour } from '@affine/env/workspace'; import { type WorkspaceUISchema } from '@affine/env/workspace'; -import { useWorkspace } from '../../hooks/use-workspace'; -import { PageDetailEditor, Provider } from '../shared'; +import { Provider } from '../shared'; export const UI = { Provider, - PageDetail: ({ currentWorkspaceId, currentPageId, onLoadEditor }) => { - const workspace = useWorkspace(currentWorkspaceId); - const page = workspace.blockSuiteWorkspace.getPage(currentPageId); - if (!page) { - throw new PageNotFoundError(workspace.blockSuiteWorkspace, currentPageId); - } - return ( - - ); - }, NewSettingsDetail: () => { throw new Error('Not implemented'); }, diff --git a/packages/frontend/core/src/adapters/shared.ts b/packages/frontend/core/src/adapters/shared.ts index 79f369bf3..6a075b042 100644 --- a/packages/frontend/core/src/adapters/shared.ts +++ b/packages/frontend/core/src/adapters/shared.ts @@ -13,9 +13,3 @@ export const NewWorkspaceSettingDetail = lazy(() => }) ) ); - -export const PageDetailEditor = lazy(() => - import('../components/page-detail-editor').then(({ PageDetailEditor }) => ({ - default: PageDetailEditor, - })) -); diff --git a/packages/frontend/core/src/atoms/element.ts b/packages/frontend/core/src/atoms/element.ts deleted file mode 100644 index 2f33acdc6..000000000 --- a/packages/frontend/core/src/atoms/element.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { atom } from 'jotai/vanilla'; - -export const appHeaderAtom = atom(null); - -export const mainContainerAtom = atom(null); diff --git a/packages/frontend/core/src/bootstrap/register-blocksuite-components.ts b/packages/frontend/core/src/bootstrap/register-blocksuite-components.ts new file mode 100644 index 000000000..d5f5b07f8 --- /dev/null +++ b/packages/frontend/core/src/bootstrap/register-blocksuite-components.ts @@ -0,0 +1,12 @@ +import { registerTOCComponents } from '@blocksuite/blocks'; + +registerTOCComponents(components => { + for (const compName in components) { + if (window.customElements.get(compName)) continue; + + window.customElements.define( + compName, + components[compName as keyof typeof components] + ); + } +}); diff --git a/packages/frontend/core/src/bootstrap/setup.ts b/packages/frontend/core/src/bootstrap/setup.ts index 36af51425..caa79875a 100644 --- a/packages/frontend/core/src/bootstrap/setup.ts +++ b/packages/frontend/core/src/bootstrap/setup.ts @@ -1,3 +1,5 @@ +import './register-blocksuite-components'; + import { setupGlobal } from '@affine/env/global'; import type { WorkspaceAdapter } from '@affine/env/workspace'; import type { WorkspaceFlavour } from '@affine/env/workspace'; diff --git a/packages/frontend/core/src/components/affine/awareness/index.tsx b/packages/frontend/core/src/components/affine/awareness/index.tsx new file mode 100644 index 000000000..db308bd26 --- /dev/null +++ b/packages/frontend/core/src/components/affine/awareness/index.tsx @@ -0,0 +1,45 @@ +import { Suspense, useEffect } from 'react'; + +import { useCurrentLoginStatus } from '../../../hooks/affine/use-current-login-status'; +import { useCurrentUser } from '../../../hooks/affine/use-current-user'; +import { useCurrentWorkspace } from '../../../hooks/current/use-current-workspace'; + +const SyncAwarenessInnerLoggedIn = () => { + const currentUser = useCurrentUser(); + const [{ blockSuiteWorkspace: workspace }] = useCurrentWorkspace(); + + useEffect(() => { + if (currentUser && workspace) { + workspace.awarenessStore.awareness.setLocalStateField('user', { + name: currentUser.name, + // todo: add avatar? + }); + + return () => { + workspace.awarenessStore.awareness.setLocalStateField('user', null); + }; + } + return; + }, [currentUser, workspace]); + + return null; +}; + +const SyncAwarenessInner = () => { + const loginStatus = useCurrentLoginStatus(); + + if (loginStatus === 'authenticated') { + return ; + } + + return null; +}; + +// todo: we could do something more interesting here, e.g., show where the current user is +export const SyncAwareness = () => { + return ( + + + + ); +}; diff --git a/packages/frontend/core/src/components/affine/hub-island/index.tsx b/packages/frontend/core/src/components/affine/hub-island/index.tsx new file mode 100644 index 000000000..732c220b8 --- /dev/null +++ b/packages/frontend/core/src/components/affine/hub-island/index.tsx @@ -0,0 +1,11 @@ +import { ToolContainer } from '@affine/component/workspace'; + +import { HelpIsland } from '../../pure/help-island'; + +export const HubIsland = () => { + return ( + + + + ); +}; diff --git a/packages/frontend/core/src/components/affine/share-page-modal/index.tsx b/packages/frontend/core/src/components/affine/share-page-modal/index.tsx index 1e4395ce4..353f9cd7c 100644 --- a/packages/frontend/core/src/components/affine/share-page-modal/index.tsx +++ b/packages/frontend/core/src/components/affine/share-page-modal/index.tsx @@ -14,7 +14,7 @@ type SharePageModalProps = { page: Page; }; -export const SharePageModal = ({ workspace, page }: SharePageModalProps) => { +export const SharePageButton = ({ workspace, page }: SharePageModalProps) => { const onTransformWorkspace = useOnTransformWorkspace(); const [open, setOpen] = useState(false); diff --git a/packages/frontend/core/src/components/affine/share-page-modal/share-menu/share-menu.tsx b/packages/frontend/core/src/components/affine/share-page-modal/share-menu/share-menu.tsx index b77f8c190..b4492cfec 100644 --- a/packages/frontend/core/src/components/affine/share-page-modal/share-menu/share-menu.tsx +++ b/packages/frontend/core/src/components/affine/share-page-modal/share-menu/share-menu.tsx @@ -60,7 +60,7 @@ const LocalShareMenu = (props: ShareMenuProps) => { modal: false, }} > - @@ -86,7 +86,7 @@ const CloudShareMenu = (props: ShareMenuProps) => { modal: false, }} > - - - - - - - { - jumpToSubPath(workspace.id, WorkspaceSubPath.ALL); - blockSuiteWorkspace.removePage(pageId); - toast(t['com.affine.toastMessage.permanentlyDeleted']()); - }, [blockSuiteWorkspace, jumpToSubPath, pageId, workspace.id, t])} - onOpenChange={setOpen} - /> - - - ); -}; - -export default TrashButtonGroup; diff --git a/packages/frontend/core/src/components/pure/trash-page-footer/index.tsx b/packages/frontend/core/src/components/pure/trash-page-footer/index.tsx new file mode 100644 index 000000000..57ac8fcc0 --- /dev/null +++ b/packages/frontend/core/src/components/pure/trash-page-footer/index.tsx @@ -0,0 +1,99 @@ +import { Button } from '@affine/component/ui/button'; +import { ConfirmModal } from '@affine/component/ui/modal'; +import { Tooltip } from '@affine/component/ui/tooltip'; +import { WorkspaceSubPath } from '@affine/env/workspace'; +import { useAFFiNEI18N } from '@affine/i18n/hooks'; +import { assertExists } from '@blocksuite/global/utils'; +import { DeleteIcon, ResetIcon } from '@blocksuite/icons'; +import { useBlockSuitePageMeta } from '@toeverything/hooks/use-block-suite-page-meta'; +import { useCallback, useState } from 'react'; + +import { useAppSettingHelper } from '../../../hooks/affine/use-app-setting-helper'; +import { useBlockSuiteMetaHelper } from '../../../hooks/affine/use-block-suite-meta-helper'; +import { useCurrentWorkspace } from '../../../hooks/current/use-current-workspace'; +import { useNavigateHelper } from '../../../hooks/use-navigate-helper'; +import { toast } from '../../../utils'; +import * as styles from './styles.css'; + +export const TrashPageFooter = ({ pageId }: { pageId: string }) => { + // fixme(himself65): remove these hooks ASAP + const [workspace] = useCurrentWorkspace(); + assertExists(workspace); + const blockSuiteWorkspace = workspace.blockSuiteWorkspace; + const pageMeta = useBlockSuitePageMeta(blockSuiteWorkspace).find( + meta => meta.id === pageId + ); + assertExists(pageMeta); + const t = useAFFiNEI18N(); + const { appSettings } = useAppSettingHelper(); + const { jumpToSubPath } = useNavigateHelper(); + const { restoreFromTrash } = useBlockSuiteMetaHelper(blockSuiteWorkspace); + const [open, setOpen] = useState(false); + const hintText = t['com.affine.cmdk.affine.editor.trash-footer-hint'](); + + const onRestore = useCallback(() => { + restoreFromTrash(pageId); + toast( + t['com.affine.toastMessage.restored']({ + title: pageMeta.title || 'Untitled', + }) + ); + }, [pageId, pageMeta.title, restoreFromTrash, t]); + + const onConfirmDelete = useCallback(() => { + jumpToSubPath(workspace.id, WorkspaceSubPath.ALL); + blockSuiteWorkspace.removePage(pageId); + toast(t['com.affine.toastMessage.permanentlyDeleted']()); + }, [blockSuiteWorkspace, jumpToSubPath, pageId, workspace.id, t]); + + const onDelete = useCallback(() => { + setOpen(true); + }, []); + + return ( +
+
{hintText}
+
+ + + + + + +
+ +
+ ); +}; diff --git a/packages/frontend/core/src/components/pure/trash-button-group/styles.css.ts b/packages/frontend/core/src/components/pure/trash-page-footer/styles.css.ts similarity index 96% rename from packages/frontend/core/src/components/pure/trash-button-group/styles.css.ts rename to packages/frontend/core/src/components/pure/trash-page-footer/styles.css.ts index 71b07c64f..8895b4118 100644 --- a/packages/frontend/core/src/components/pure/trash-button-group/styles.css.ts +++ b/packages/frontend/core/src/components/pure/trash-page-footer/styles.css.ts @@ -6,12 +6,13 @@ export const group = style({ justifyContent: 'center', }); export const deleteHintContainer = style({ - position: 'fixed', + position: 'relative', zIndex: 2, padding: '14px 20px', display: 'flex', justifyContent: 'space-between', alignItems: 'center', + flexShrink: 0, bottom: '0', gap: '16px', backgroundColor: 'var(--affine-background-primary-color)', @@ -32,7 +33,6 @@ export const deleteHintText = style({ whiteSpace: 'nowrap', textOverflow: 'ellipsis', overflow: 'hidden', - cursor: 'pointer', }); export const buttonContainer = style({ color: 'var(--affine-pure-white)', diff --git a/packages/frontend/core/src/components/workspace-header.tsx b/packages/frontend/core/src/components/workspace-header.tsx deleted file mode 100644 index 18d15026e..000000000 --- a/packages/frontend/core/src/components/workspace-header.tsx +++ /dev/null @@ -1,189 +0,0 @@ -import { - CollectionList, - FilterList, - SaveAsCollectionButton, - useCollectionManager, -} from '@affine/component/page-list'; -import { Unreachable } from '@affine/env/constant'; -import type { Collection, Filter } from '@affine/env/filter'; -import type { - WorkspaceFlavour, - WorkspaceHeaderProps, -} from '@affine/env/workspace'; -import { WorkspaceSubPath } from '@affine/env/workspace'; -import { useAFFiNEI18N } from '@affine/i18n/hooks'; -import { DeleteIcon } from '@blocksuite/icons'; -import { useAsyncCallback } from '@toeverything/hooks/affine-async-hooks'; -import { useSetAtom } from 'jotai/react'; -import { useCallback } from 'react'; - -import { collectionsCRUDAtom } from '../atoms/collections'; -import { appHeaderAtom, mainContainerAtom } from '../atoms/element'; -import { useAllPageListConfig } from '../hooks/affine/use-all-page-list-config'; -import { useDeleteCollectionInfo } from '../hooks/affine/use-delete-collection-info'; -import { useNavigateHelper } from '../hooks/use-navigate-helper'; -import { useWorkspace } from '../hooks/use-workspace'; -import { SharePageModal } from './affine/share-page-modal'; -import { BlockSuiteHeaderTitle } from './blocksuite/block-suite-header-title'; -import { filterContainerStyle } from './filter-container.css'; -import { Header } from './pure/header'; -import { PluginHeader } from './pure/plugin-header'; -import { WorkspaceModeFilterTab } from './pure/workspace-mode-filter-tab'; -import { TopTip } from './top-tip'; -import * as styles from './workspace-header.css'; - -const FilterContainer = ({ workspaceId }: { workspaceId: string }) => { - const currentWorkspace = useWorkspace(workspaceId); - const navigateHelper = useNavigateHelper(); - const setting = useCollectionManager(collectionsCRUDAtom); - const saveToCollection = useCallback( - async (collection: Collection) => { - await setting.createCollection({ - ...collection, - filterList: setting.currentCollection.filterList, - }); - navigateHelper.jumpToCollection(workspaceId, collection.id); - }, - [setting, navigateHelper, workspaceId] - ); - - const onFilterChange = useAsyncCallback( - async (filterList: Filter[]) => { - await setting.updateCollection({ - ...setting.currentCollection, - filterList, - }); - }, - [setting] - ); - - if (!setting.isDefault || !setting.currentCollection.filterList.length) { - return null; - } - - return ( -
-
- -
-
- {setting.currentCollection.filterList.length > 0 ? ( - - ) : null} -
-
- ); -}; - -export function WorkspaceHeader({ - currentWorkspaceId, - currentEntry, - rightSlot, -}: WorkspaceHeaderProps) { - const setAppHeader = useSetAtom(appHeaderAtom); - - const currentWorkspace = useWorkspace(currentWorkspaceId); - const workspace = currentWorkspace.blockSuiteWorkspace; - const setting = useCollectionManager(collectionsCRUDAtom); - const config = useAllPageListConfig(); - const userInfo = useDeleteCollectionInfo(); - - const t = useAFFiNEI18N(); - - // route in all page - if ( - 'subPath' in currentEntry && - currentEntry.subPath === WorkspaceSubPath.ALL - ) { - return ( - <> -
- } - right={rightSlot} - center={} - /> - - - ); - } - - // route in shared - if ( - 'subPath' in currentEntry && - currentEntry.subPath === WorkspaceSubPath.SHARED - ) { - return ( -
} - /> - ); - } - - // route in trash - if ( - 'subPath' in currentEntry && - currentEntry.subPath === WorkspaceSubPath.TRASH - ) { - return ( -
- - {t['com.affine.workspaceSubPath.trash']()} - - } - /> - ); - } - - // route in edit page - if ('pageId' in currentEntry) { - const currentPage = workspace.getPage(currentEntry.pageId); - const sharePageModal = currentPage ? ( - - ) : null; - return ( - <> -
- } - right={ -
- {sharePageModal} - -
- } - bottomBorder - /> - - - ); - } - - throw new Unreachable(); -} diff --git a/packages/frontend/core/src/hooks/affine/use-current-user.ts b/packages/frontend/core/src/hooks/affine/use-current-user.ts index 94b4b967a..35cbe9bbc 100644 --- a/packages/frontend/core/src/hooks/affine/use-current-user.ts +++ b/packages/frontend/core/src/hooks/affine/use-current-user.ts @@ -106,5 +106,6 @@ export function useCurrentUser(): CheckedUser { hasPassword: user?.hasPassword ?? false, update, }; - }, [user, update]); + // spread the user object to make sure the hook will not be re-rendered when user ref changed but the properties not. + }, [user.id, user.name, user.email, user.image, user.hasPassword, update]); } diff --git a/packages/frontend/core/src/hooks/current/use-current-page.ts b/packages/frontend/core/src/hooks/current/use-current-page.ts new file mode 100644 index 000000000..0e5a35ada --- /dev/null +++ b/packages/frontend/core/src/hooks/current/use-current-page.ts @@ -0,0 +1,12 @@ +import { + currentPageIdAtom, + currentWorkspaceAtom, +} from '@toeverything/infra/atom'; +import { useAtomValue } from 'jotai'; + +export const useCurrentPage = () => { + const currentPageId = useAtomValue(currentPageIdAtom); + const currentWorkspace = useAtomValue(currentWorkspaceAtom); + + return currentPageId ? currentWorkspace.getPage(currentPageId) : null; +}; diff --git a/packages/frontend/core/src/layouts/workspace-layout.tsx b/packages/frontend/core/src/layouts/workspace-layout.tsx index d9603b7e9..ab848eeb0 100644 --- a/packages/frontend/core/src/layouts/workspace-layout.tsx +++ b/packages/frontend/core/src/layouts/workspace-layout.tsx @@ -6,11 +6,7 @@ import { type DraggableTitleCellData, PageListDragOverlay, } from '@affine/component/page-list'; -import { - MainContainer, - ToolContainer, - WorkspaceFallback, -} from '@affine/component/workspace'; +import { MainContainer, WorkspaceFallback } from '@affine/component/workspace'; import { useAFFiNEI18N } from '@affine/i18n/hooks'; import { rootWorkspacesMetadataAtom } from '@affine/workspace/atom'; import { getBlobEngine } from '@affine/workspace/manager'; @@ -35,12 +31,10 @@ import { useLocation, useParams } from 'react-router-dom'; import { Map as YMap } from 'yjs'; import { openQuickSearchModalAtom, openSettingModalAtom } from '../atoms'; -import { mainContainerAtom } from '../atoms/element'; import { AdapterProviderWrapper } from '../components/adapter-worksapce-wrapper'; import { AppContainer } from '../components/affine/app-container'; +import { SyncAwareness } from '../components/affine/awareness'; import { usePageHelper } from '../components/blocksuite/block-suite-page-list/utils'; -import type { IslandItemNames } from '../components/pure/help-island'; -import { HelpIsland } from '../components/pure/help-island'; import { processCollectionsDrag } from '../components/pure/workspace-slider-bar/collections'; import { DROPPABLE_SIDEBAR_TRASH, @@ -90,10 +84,6 @@ export const QuickSearch = () => { ); }; -const showList: IslandItemNames[] = environment.isDesktop - ? ['whatNew', 'contact', 'guide'] - : ['whatNew', 'contact']; - export const CurrentWorkspaceContext = ({ children, }: PropsWithChildren): ReactNode => { @@ -258,12 +248,6 @@ export const WorkspaceLayoutInner = ({ const { appSettings } = useAppSettingHelper(); const location = useLocation(); - const { pageId } = useParams(); - const pageMeta = useBlockSuitePageMeta( - currentWorkspace.blockSuiteWorkspace - ).find(meta => meta.id === pageId); - const inTrashPage = pageMeta?.trash ?? false; - const setMainContainer = useSetAtom(mainContainerAtom); return ( <> @@ -292,26 +276,20 @@ export const WorkspaceLayoutInner = ({ paths={pathGenerator} /> - }> - + }> + {migration ? ( ) : ( children )} - - - + ); }; diff --git a/packages/frontend/core/src/pages/share/detail-page.tsx b/packages/frontend/core/src/pages/share/share-detail-page.tsx similarity index 96% rename from packages/frontend/core/src/pages/share/detail-page.tsx rename to packages/frontend/core/src/pages/share/share-detail-page.tsx index 0776f8323..e255212a6 100644 --- a/packages/frontend/core/src/pages/share/detail-page.tsx +++ b/packages/frontend/core/src/pages/share/share-detail-page.tsx @@ -18,11 +18,11 @@ import { } from 'react-router-dom'; import { applyUpdate } from 'yjs'; -import { PageDetailEditor } from '../../adapters/shared'; import type { PageMode } from '../../atoms'; import { AppContainer } from '../../components/affine/app-container'; -import { ShareHeader } from '../../components/share-header'; +import { PageDetailEditor } from '../../components/page-detail-editor'; import { SharePageNotFoundError } from '../../components/share-page-not-found-error'; +import { ShareHeader } from './share-header'; type LoaderData = { page: Page; diff --git a/packages/frontend/core/src/components/share-header.tsx b/packages/frontend/core/src/pages/share/share-header.tsx similarity index 52% rename from packages/frontend/core/src/components/share-header.tsx rename to packages/frontend/core/src/pages/share/share-header.tsx index 20f6c6ab6..675902637 100644 --- a/packages/frontend/core/src/components/share-header.tsx +++ b/packages/frontend/core/src/pages/share/share-header.tsx @@ -1,13 +1,11 @@ import type { Workspace } from '@blocksuite/store'; -import { useSetAtom } from 'jotai/react'; -import type { PageMode } from '../atoms'; -import { appHeaderAtom, mainContainerAtom } from '../atoms/element'; -import { useWorkspace } from '../hooks/use-workspace'; -import { BlockSuiteHeaderTitle } from './blocksuite/block-suite-header-title'; -import ShareHeaderLeftItem from './cloud/share-header-left-item'; -import ShareHeaderRightItem from './cloud/share-header-right-item'; -import { Header } from './pure/header'; +import type { PageMode } from '../../atoms'; +import { BlockSuiteHeaderTitle } from '../../components/blocksuite/block-suite-header-title'; +import ShareHeaderLeftItem from '../../components/cloud/share-header-left-item'; +import ShareHeaderRightItem from '../../components/cloud/share-header-right-item'; +import { Header } from '../../components/pure/header'; +import { useWorkspace } from '../../hooks/use-workspace'; export function ShareHeader({ workspace, @@ -18,14 +16,10 @@ export function ShareHeader({ pageId: string; publishMode: PageMode; }) { - const setAppHeader = useSetAtom(appHeaderAtom); - const currentWorkspace = useWorkspace(workspace.id); return (
} center={ { + const currentWorkspace = useWorkspace(workspaceId); + const navigateHelper = useNavigateHelper(); + const setting = useCollectionManager(collectionsCRUDAtom); + const saveToCollection = useCallback( + async (collection: Collection) => { + await setting.createCollection({ + ...collection, + filterList: setting.currentCollection.filterList, + }); + navigateHelper.jumpToCollection(workspaceId, collection.id); + }, + [setting, navigateHelper, workspaceId] + ); + + const onFilterChange = useAsyncCallback( + async (filterList: Filter[]) => { + await setting.updateCollection({ + ...setting.currentCollection, + filterList, + }); + }, + [setting] + ); + + if (!setting.isDefault || !setting.currentCollection.filterList.length) { + return null; + } + + return ( +
+
+ +
+
+ {setting.currentCollection.filterList.length > 0 ? ( + + ) : null} +
+
+ ); +}; diff --git a/packages/frontend/core/src/pages/workspace/all-page.css.ts b/packages/frontend/core/src/pages/workspace/all-page/all-page.css.ts similarity index 91% rename from packages/frontend/core/src/pages/workspace/all-page.css.ts rename to packages/frontend/core/src/pages/workspace/all-page/all-page.css.ts index 32ecc9f4c..70db74ebc 100644 --- a/packages/frontend/core/src/pages/workspace/all-page.css.ts +++ b/packages/frontend/core/src/pages/workspace/all-page/all-page.css.ts @@ -67,3 +67,10 @@ export const headerCreateNewButtonHidden = style({ opacity: 0, pointerEvents: 'none', }); + +export const headerRightWindows = style({ + display: 'flex', + alignItems: 'center', + gap: 8, + transform: 'translateX(16px)', +}); diff --git a/packages/frontend/core/src/pages/workspace/all-page.tsx b/packages/frontend/core/src/pages/workspace/all-page/all-page.tsx similarity index 78% rename from packages/frontend/core/src/pages/workspace/all-page.tsx rename to packages/frontend/core/src/pages/workspace/all-page/all-page.tsx index d62bf16b2..d07a8fb2d 100644 --- a/packages/frontend/core/src/pages/workspace/all-page.tsx +++ b/packages/frontend/core/src/pages/workspace/all-page/all-page.tsx @@ -1,5 +1,6 @@ import { toast } from '@affine/component'; import { + CollectionList, currentCollectionAtom, FloatingToolbar, NewPageButton as PureNewPageButton, @@ -8,7 +9,7 @@ import { useCollectionManager, VirtualizedPageList, } from '@affine/component/page-list'; -import { WorkspaceFlavour, WorkspaceSubPath } from '@affine/env/workspace'; +import { WorkspaceFlavour } from '@affine/env/workspace'; import { Trans } from '@affine/i18n'; import { useAFFiNEI18N } from '@affine/i18n/hooks'; import { assertExists } from '@blocksuite/global/utils'; @@ -18,7 +19,7 @@ import { PlusIcon, ViewLayersIcon, } from '@blocksuite/icons'; -import type { PageMeta } from '@blocksuite/store'; +import type { PageMeta, Workspace } from '@blocksuite/store'; import { useBlockSuitePageMeta } from '@toeverything/hooks/use-block-suite-page-meta'; import { getBlockSuiteWorkspaceAtom } from '@toeverything/infra/__internal__/workspace'; import { getCurrentStore } from '@toeverything/infra/atom'; @@ -34,16 +35,22 @@ import type { LoaderFunction } from 'react-router-dom'; import { redirect } from 'react-router-dom'; import { NIL } from 'uuid'; -import { collectionsCRUDAtom } from '../../atoms/collections'; -import { usePageHelper } from '../../components/blocksuite/block-suite-page-list/utils'; -import { WorkspaceHeader } from '../../components/workspace-header'; -import { useBlockSuiteMetaHelper } from '../../hooks/affine/use-block-suite-meta-helper'; -import { useTrashModalHelper } from '../../hooks/affine/use-trash-modal-helper'; -import { useCurrentWorkspace } from '../../hooks/current/use-current-workspace'; -import { performanceRenderLogger } from '../../shared'; +import { collectionsCRUDAtom } from '../../../atoms/collections'; +import { HubIsland } from '../../../components/affine/hub-island'; +import { usePageHelper } from '../../../components/blocksuite/block-suite-page-list/utils'; +import { Header } from '../../../components/pure/header'; +import { WindowsAppControls } from '../../../components/pure/header/windows-app-controls'; +import { WorkspaceModeFilterTab } from '../../../components/pure/workspace-mode-filter-tab'; +import { useAllPageListConfig } from '../../../hooks/affine/use-all-page-list-config'; +import { useBlockSuiteMetaHelper } from '../../../hooks/affine/use-block-suite-meta-helper'; +import { useDeleteCollectionInfo } from '../../../hooks/affine/use-delete-collection-info'; +import { useTrashModalHelper } from '../../../hooks/affine/use-trash-modal-helper'; +import { useCurrentWorkspace } from '../../../hooks/current/use-current-workspace'; +import { performanceRenderLogger } from '../../../shared'; +import { EmptyPageList } from '../page-list-empty'; +import { useFilteredPageMetas } from '../pages'; import * as styles from './all-page.css'; -import { EmptyPageList } from './page-list-empty'; -import { useFilteredPageMetas } from './pages'; +import { FilterContainer } from './all-page-filter'; export const loader: LoaderFunction = async args => { const rootStore = getCurrentStore(); @@ -217,6 +224,50 @@ const NewPageButton = ({ ); }; +const AllPageHeader = ({ + workspace, + showCreateNew, +}: { + workspace: Workspace; + showCreateNew: boolean; +}) => { + const setting = useCollectionManager(collectionsCRUDAtom); + const config = useAllPageListConfig(); + const userInfo = useDeleteCollectionInfo(); + const isWindowsDesktop = environment.isDesktop && environment.isWindows; + + return ( + <> +
+ } + right={ +
+ + + + {isWindowsDesktop ? : null} +
+ } + center={} + /> + + + ); +}; + // even though it is called all page, it is also being used for collection route as well export const AllPage = () => { const [currentWorkspace] = useCurrentWorkspace(); @@ -250,22 +301,9 @@ export const AllPage = () => { return (
{currentWorkspace.flavour !== WorkspaceFlavour.AFFINE_PUBLIC ? ( - - - - } + ) : null} {filteredPageMetas.length > 0 ? ( @@ -299,6 +337,7 @@ export const AllPage = () => { blockSuiteWorkspace={currentWorkspace.blockSuiteWorkspace} /> )} +
); }; diff --git a/packages/frontend/core/src/pages/workspace/collection.tsx b/packages/frontend/core/src/pages/workspace/collection.tsx index f66bb5fe7..3db8be598 100644 --- a/packages/frontend/core/src/pages/workspace/collection.tsx +++ b/packages/frontend/core/src/pages/workspace/collection.tsx @@ -30,7 +30,7 @@ import { useCurrentWorkspace } from '../../hooks/current/use-current-workspace'; import { useNavigateHelper } from '../../hooks/use-navigate-helper'; import { WorkspaceSubPath } from '../../shared'; import { getWorkspaceSetting } from '../../utils/workspace-setting'; -import { AllPage } from './all-page'; +import { AllPage } from './all-page/all-page'; import * as styles from './collection.css'; export const loader: LoaderFunction = async args => { diff --git a/packages/frontend/core/src/pages/workspace/detail-page.tsx b/packages/frontend/core/src/pages/workspace/detail-page.tsx deleted file mode 100644 index e270ad26f..000000000 --- a/packages/frontend/core/src/pages/workspace/detail-page.tsx +++ /dev/null @@ -1,215 +0,0 @@ -import { PageDetailSkeleton } from '@affine/component/page-detail-skeleton'; -import { - createTagFilter, - useCollectionManager, -} from '@affine/component/page-list'; -import { WorkspaceSubPath } from '@affine/env/workspace'; -import { globalBlockSuiteSchema } from '@affine/workspace/manager'; -import { SyncEngineStep } from '@affine/workspace/providers'; -import { assertExists } from '@blocksuite/global/utils'; -import type { EditorContainer } from '@blocksuite/presets'; -import type { Page } from '@blocksuite/store'; -import { - contentLayoutAtom, - currentPageIdAtom, - currentWorkspaceIdAtom, -} from '@toeverything/infra/atom'; -import { useAtomValue, useSetAtom } from 'jotai'; -import { type ReactElement, useCallback, useEffect, useState } from 'react'; -import { type LoaderFunction, useParams } from 'react-router-dom'; -import type { Map as YMap } from 'yjs'; - -import { getUIAdapter } from '../../adapters/workspace'; -import { setPageModeAtom } from '../../atoms'; -import { collectionsCRUDAtom } from '../../atoms/collections'; -import { currentModeAtom } from '../../atoms/mode'; -import { AffineErrorBoundary } from '../../components/affine/affine-error-boundary'; -import { GlobalPageHistoryModal } from '../../components/affine/page-history-modal'; -import { WorkspaceHeader } from '../../components/workspace-header'; -import { useRegisterBlocksuiteEditorCommands } from '../../hooks/affine/use-register-blocksuite-editor-commands'; -import { - useCurrentSyncEngine, - useCurrentSyncEngineStatus, -} from '../../hooks/current/use-current-sync-engine'; -import { useCurrentWorkspace } from '../../hooks/current/use-current-workspace'; -import { useNavigateHelper } from '../../hooks/use-navigate-helper'; -import { performanceRenderLogger } from '../../shared'; - -const DetailPageImpl = (): ReactElement => { - const { openPage, jumpToSubPath } = useNavigateHelper(); - const currentPageId = useAtomValue(currentPageIdAtom); - const [currentWorkspace] = useCurrentWorkspace(); - assertExists(currentWorkspace); - assertExists(currentPageId); - const blockSuiteWorkspace = currentWorkspace.blockSuiteWorkspace; - const { setTemporaryFilter } = useCollectionManager(collectionsCRUDAtom); - const mode = useAtomValue(currentModeAtom); - const setPageMode = useSetAtom(setPageModeAtom); - useRegisterBlocksuiteEditorCommands(currentPageId, mode); - const onLoad = useCallback( - (page: Page, editor: EditorContainer) => { - try { - const surfaceBlock = page.getBlockByFlavour('affine:surface')[0]; - // hotfix for old page - if ( - surfaceBlock && - (surfaceBlock.yBlock.get('prop:elements') as YMap).get( - 'type' - ) !== '$blocksuite:internal:native$' - ) { - globalBlockSuiteSchema.upgradePage( - 0, - { - 'affine:surface': 3, - }, - page.spaceDoc - ); - } - } catch {} - setPageMode(currentPageId, mode); - const dispose = editor.slots.pageLinkClicked.on(({ pageId }) => { - return openPage(blockSuiteWorkspace.id, pageId); - }); - const disposeTagClick = editor.slots.tagClicked.on(async ({ tagId }) => { - jumpToSubPath(currentWorkspace.id, WorkspaceSubPath.ALL); - setTemporaryFilter([createTagFilter(tagId)]); - }); - return () => { - dispose.dispose(); - disposeTagClick.dispose(); - }; - }, - [ - blockSuiteWorkspace.id, - currentPageId, - currentWorkspace.id, - jumpToSubPath, - mode, - openPage, - setPageMode, - setTemporaryFilter, - ] - ); - - const { PageDetail } = getUIAdapter(currentWorkspace.flavour); - return ( - <> - - - - - - ); -}; - -export const DetailPage = (): ReactElement => { - const [currentWorkspace] = useCurrentWorkspace(); - const currentSyncEngineStatus = useCurrentSyncEngineStatus(); - const currentSyncEngine = useCurrentSyncEngine(); - const currentPageId = useAtomValue(currentPageIdAtom); - const [page, setPage] = useState(null); - - // set sync engine priority target - useEffect(() => { - if (!currentPageId) { - return; - } - currentSyncEngine?.setPriorityRule(id => id.endsWith(currentPageId)); - }, [currentPageId, currentSyncEngine, currentWorkspace]); - - // load page by current page id - useEffect(() => { - if (!currentPageId) { - setPage(null); - return; - } - - const exists = currentWorkspace.blockSuiteWorkspace.getPage(currentPageId); - - if (exists) { - setPage(exists); - return; - } - - const dispose = currentWorkspace.blockSuiteWorkspace.slots.pagesUpdated.on( - () => { - const exists = - currentWorkspace.blockSuiteWorkspace.getPage(currentPageId); - - if (exists) { - setPage(exists); - } - } - ); - - return dispose.dispose; - }, [currentPageId, currentWorkspace]); - - const navigate = useNavigateHelper(); - - // if sync engine has been synced and the page is null, wait 1s and jump to 404 page. - useEffect(() => { - if (currentSyncEngineStatus?.step === SyncEngineStep.Synced && !page) { - const timeout = setTimeout(() => { - navigate.jumpTo404(); - }, 1000); - return () => { - clearTimeout(timeout); - }; - } - return; - }, [currentSyncEngineStatus, navigate, page]); - - if (!currentPageId || !page) { - return ; - } - - if (page.meta.jumpOnce) { - currentWorkspace.blockSuiteWorkspace.setPageMeta(page.id, { - jumpOnce: false, - }); - } - - return ; -}; - -export const loader: LoaderFunction = async () => { - return null; -}; - -export const Component = () => { - performanceRenderLogger.info('DetailPage'); - - const setContentLayout = useSetAtom(contentLayoutAtom); - const setCurrentWorkspaceId = useSetAtom(currentWorkspaceIdAtom); - const setCurrentPageId = useSetAtom(currentPageIdAtom); - const params = useParams(); - - useEffect(() => { - setContentLayout('editor'); - if (params.workspaceId) { - localStorage.setItem('last_workspace_id', params.workspaceId); - setCurrentWorkspaceId(params.workspaceId); - } - if (params.pageId) { - localStorage.setItem('last_page_id', params.pageId); - setCurrentPageId(params.pageId); - } - }, [params, setContentLayout, setCurrentPageId, setCurrentWorkspaceId]); - - // Add a key to force rerender when page changed, to avoid error boundary persisting. - return ( - - - - ); -}; 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 new file mode 100644 index 000000000..73b4f6052 --- /dev/null +++ b/packages/frontend/core/src/pages/workspace/detail-page/detail-page-header.css.ts @@ -0,0 +1,59 @@ +import type { ComplexStyleRule } from '@vanilla-extract/css'; +import { style } from '@vanilla-extract/css'; + +export const header = style({ + display: 'flex', + height: '52px', + width: '100%', + alignItems: 'center', + flexShrink: 0, + background: 'var(--affine-background-primary-color)', + borderBottom: '1px solid var(--affine-border-color)', + selectors: { + '&[data-sidebar-floating="false"]': { + WebkitAppRegion: 'drag', + }, + }, + '@media': { + print: { + display: 'none', + }, + }, + ':has([data-popper-placement])': { + WebkitAppRegion: 'no-drag', + }, +} as ComplexStyleRule); + +export const mainHeader = style([ + header, + { + padding: '0 16px', + }, +]); + +export const sidebarHeader = style([ + header, + { + padding: '0 14px', + gap: '12px', + }, +]); + +export const mainHeaderRight = style({ + display: 'flex', + alignItems: 'center', + gap: '8px', +}); + +export const spacer = style({ + flexGrow: 1, +}); + +export const standaloneExtensionSwitcherWrapper = style({ + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + flexShrink: 0, + height: '52px', + position: 'relative', +}); 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 new file mode 100644 index 000000000..6628da77e --- /dev/null +++ b/packages/frontend/core/src/pages/workspace/detail-page/detail-page-header.tsx @@ -0,0 +1,159 @@ +import { IconButton } from '@affine/component'; +import { + appSidebarFloatingAtom, + appSidebarOpenAtom, + SidebarSwitch, +} from '@affine/component/app-sidebar'; +import type { AllWorkspace } from '@affine/core/shared'; +import { RightSidebarIcon } from '@blocksuite/icons'; +import type { Page } from '@blocksuite/store'; +import { useAtomValue, useSetAtom } from 'jotai'; + +import { SharePageButton } from '../../../components/affine/share-page-modal'; +import { BlockSuiteHeaderTitle } from '../../../components/blocksuite/block-suite-header-title'; +import { HeaderDivider } from '../../../components/pure/header'; +import { WindowsAppControls } from '../../../components/pure/header/windows-app-controls'; +import * as styles from './detail-page-header.css'; +import { ExtensionTabs } from './editor-sidebar'; +import { + editorSidebarOpenAtom, + editorSidebarToggleAtom, +} from './editor-sidebar/atoms'; + +interface PageHeaderRightProps { + showSidebarSwitch?: boolean; +} + +const ToggleSidebarButton = () => { + const toggle = useSetAtom(editorSidebarToggleAtom); + return ( + + + + ); +}; +const isWindowsDesktop = environment.isDesktop && environment.isWindows; + +const WindowsMainPageHeaderRight = ({ + showSidebarSwitch, +}: PageHeaderRightProps) => { + const editorSidebarOpen = useAtomValue(editorSidebarOpenAtom); + + if (editorSidebarOpen) { + return null; + } + + return ( + <> + +
+ {showSidebarSwitch ? : null} + +
+ + ); +}; + +const NonWindowsMainPageHeaderRight = ({ + showSidebarSwitch, +}: PageHeaderRightProps) => { + const editorSidebarOpen = useAtomValue(editorSidebarOpenAtom); + + if (editorSidebarOpen || !showSidebarSwitch) { + return null; + } + + return ( + <> + +
+ +
+ + ); +}; + +function Header({ + children, + style, + className, +}: { + children: React.ReactNode; + className?: string; + style?: React.CSSProperties; +}) { + const appSidebarFloating = useAtomValue(appSidebarFloatingAtom); + return ( +
+ {children} +
+ ); +} + +export function DetailPageHeader({ + page, + workspace, + showSidebarSwitch = true, +}: { + page: Page; + workspace: AllWorkspace; + showSidebarSwitch?: boolean; +}) { + const leftSidebarOpen = useAtomValue(appSidebarOpenAtom); + const RightHeader = isWindowsDesktop + ? WindowsMainPageHeaderRight + : NonWindowsMainPageHeaderRight; + return ( +
+ + {!leftSidebarOpen ? : null} + +
+ {page ? : null} + +
+ ); +} + +function WindowsSidebarHeader() { + return ( + <> +
+
+ + +
+
+ +
+ + ); +} + +function NonWindowsSidebarHeader() { + return ( +
+ +
+ +
+ ); +} + +export function RightSidebarHeader() { + return isWindowsDesktop ? ( + + ) : ( + + ); +} diff --git a/packages/frontend/core/src/pages/workspace/detail-page/detail-page.css.ts b/packages/frontend/core/src/pages/workspace/detail-page/detail-page.css.ts new file mode 100644 index 000000000..4c01eab62 --- /dev/null +++ b/packages/frontend/core/src/pages/workspace/detail-page/detail-page.css.ts @@ -0,0 +1,95 @@ +import { style } from '@vanilla-extract/css'; + +export const root = style({ + display: 'flex', + height: '100%', + overflow: 'hidden', + width: '100%', +}); + +export const mainContainer = style({ + display: 'flex', + flex: 1, + height: '100%', + position: 'relative', + flexDirection: 'column', + width: '100%', + selectors: { + [`${root}[data-client-border] &`]: { + borderRadius: '4px', + }, + }, +}); + +export const editorContainer = style({ + position: 'relative', + display: 'flex', + flexDirection: 'column', + flex: 1, + overflow: 'hidden', + zIndex: 0, // it will create stacking context to limit layer of child elements and be lower than after auto zIndex +}); + +export const resizeHandle = style({ + width: '1px', + position: 'relative', + backgroundColor: 'var(--affine-border-color)', + selectors: { + '&[data-collapsed=true]': { + display: 'none', + }, + [`${root}[data-client-border] &`]: { + width: '8px', + backgroundColor: 'transparent', + }, + }, +}); + +export const resizeHandleInner = style({ + height: '100%', + width: '10px', // this is the real hit box + position: 'absolute', + transform: 'translateX(-50%)', + zIndex: 10, + transition: 'all 0.2s ease-in-out', + display: 'flex', + justifyContent: 'center', + '::before': { + content: '""', + width: '0px', + height: '100%', + borderRadius: '2px', + transition: 'all 0.2s ease-in-out', + }, + selectors: { + [`${root}[data-client-border] &`]: { + transform: 'translateX(-1px)', + }, + [`:is(${resizeHandle}:hover, ${resizeHandle}[data-resize-handle-active]) &::before`]: + { + width: '2px', + backgroundColor: 'var(--affine-primary-color)', + }, + [`${resizeHandle}[data-resize-handle-active] &::before`]: { + width: '4px', + borderRadius: '4px', + }, + }, +}); + +export const sidebarContainer = style({ + transition: 'flex 0.2s ease-in-out', + display: 'flex', + flexDirection: 'column', + selectors: { + [`${resizeHandle}[data-resize-handle-active] + &`]: { + transition: 'none', + }, + [`${root}[data-disable-animation] &`]: { + transition: 'none', + }, + [`${root}[data-client-border] &`]: { + borderRadius: '4px', + }, + }, +}); diff --git a/packages/frontend/core/src/pages/workspace/detail-page/detail-page.tsx b/packages/frontend/core/src/pages/workspace/detail-page/detail-page.tsx new file mode 100644 index 000000000..a4aa1c82d --- /dev/null +++ b/packages/frontend/core/src/pages/workspace/detail-page/detail-page.tsx @@ -0,0 +1,363 @@ +import { PageDetailSkeleton } from '@affine/component/page-detail-skeleton'; +import { + createTagFilter, + useCollectionManager, +} from '@affine/component/page-list'; +import { WorkspaceSubPath } from '@affine/env/workspace'; +import { globalBlockSuiteSchema } from '@affine/workspace/manager'; +import { SyncEngineStep } from '@affine/workspace/providers'; +import { assertExists } from '@blocksuite/global/utils'; +import type { EditorContainer } from '@blocksuite/presets'; +import type { Page, Workspace } from '@blocksuite/store'; +import { useBlockSuitePageMeta } from '@toeverything/hooks/use-block-suite-page-meta'; +import { + currentPageIdAtom, + currentWorkspaceIdAtom, +} from '@toeverything/infra/atom'; +import { useAtomValue, useSetAtom } from 'jotai'; +import { + type ReactElement, + type ReactNode, + useCallback, + useEffect, + useRef, + useState, +} from 'react'; +import type { PanelOnResize } from 'react-resizable-panels'; +import { + type ImperativePanelHandle, + Panel, + PanelGroup, + PanelResizeHandle, +} from 'react-resizable-panels'; +import { type LoaderFunction, useParams } from 'react-router-dom'; +import type { Map as YMap } from 'yjs'; + +import { setPageModeAtom } from '../../../atoms'; +import { collectionsCRUDAtom } from '../../../atoms/collections'; +import { currentModeAtom } from '../../../atoms/mode'; +import { appSettingAtom } from '../../../atoms/settings'; +import { AffineErrorBoundary } from '../../../components/affine/affine-error-boundary'; +import { HubIsland } from '../../../components/affine/hub-island'; +import { GlobalPageHistoryModal } from '../../../components/affine/page-history-modal'; +import { PageDetailEditor } from '../../../components/page-detail-editor'; +import { TrashPageFooter } from '../../../components/pure/trash-page-footer'; +import { TopTip } from '../../../components/top-tip'; +import { useRegisterBlocksuiteEditorCommands } from '../../../hooks/affine/use-register-blocksuite-editor-commands'; +import { + useCurrentSyncEngine, + useCurrentSyncEngineStatus, +} from '../../../hooks/current/use-current-sync-engine'; +import { useCurrentWorkspace } from '../../../hooks/current/use-current-workspace'; +import { useNavigateHelper } from '../../../hooks/use-navigate-helper'; +import { performanceRenderLogger } from '../../../shared'; +import * as styles from './detail-page.css'; +import { DetailPageHeader, RightSidebarHeader } from './detail-page-header'; +import { + EditorSidebar, + editorSidebarOpenAtom, + editorSidebarStateAtom, + editorSidebarWidthAtom, +} from './editor-sidebar'; + +interface DetailPageLayoutProps { + main: ReactNode; + header: ReactNode; + footer: ReactNode; + sidebar: ReactNode; +} + +// disable animation to avoid UI flash +function useEnableAnimation() { + const [enable, setEnable] = useState(false); + useEffect(() => { + window.setTimeout(() => { + setEnable(true); + }, 500); + }, []); + return enable; +} + +// todo: consider move to a shared place if we also want to reuse the layout for other routes +const DetailPageLayout = ({ + main, + header, + footer, + sidebar, +}: DetailPageLayoutProps): ReactElement => { + const sidebarState = useAtomValue(editorSidebarStateAtom); + const setSidebarWidth = useSetAtom(editorSidebarWidthAtom); + const setSidebarOpen = useSetAtom(editorSidebarOpenAtom); + const { clientBorder } = useAtomValue(appSettingAtom); + + const sidebarRef = useRef(null); + + const onExpandSidebar = useCallback(() => { + setSidebarOpen(true); + }, [setSidebarOpen]); + + const onCollapseSidebar = useCallback(() => { + setSidebarOpen(false); + }, [setSidebarOpen]); + + const onResize: PanelOnResize = useCallback( + e => { + if (e.sizePixels > 0) { + setSidebarWidth(e.sizePixels); + } + }, + [setSidebarWidth] + ); + + useEffect(() => { + const panelHandle = sidebarRef.current; + if (!panelHandle) { + return; + } + + if (sidebarState.isOpen) { + panelHandle.expand(); + } else { + panelHandle.collapse(); + } + }, [sidebarState.isOpen]); + + const enableAnimation = useEnableAnimation(); + + return ( + + + {header} + {main} + {footer} + + {sidebar ? ( + <> + +
+ + + {sidebar} + + + ) : null} + + ); +}; + +const DetailPageImpl = ({ page }: { page: Page }) => { + const currentPageId = page.id; + const { openPage, jumpToSubPath } = useNavigateHelper(); + const [currentWorkspace] = useCurrentWorkspace(); + assertExists( + currentWorkspace, + 'current workspace is null when rendering detail' + ); + const blockSuiteWorkspace = currentWorkspace.blockSuiteWorkspace; + + const pageMeta = useBlockSuitePageMeta(blockSuiteWorkspace).find( + meta => meta.id === page.id + ); + + const isInTrash = pageMeta?.trash; + + const { setTemporaryFilter } = useCollectionManager(collectionsCRUDAtom); + const mode = useAtomValue(currentModeAtom); + const setPageMode = useSetAtom(setPageModeAtom); + useRegisterBlocksuiteEditorCommands(currentPageId, mode); + + const onLoad = useCallback( + (page: Page, editor: EditorContainer) => { + try { + // todo(joooye34): improve the following migration code + const surfaceBlock = page.getBlockByFlavour('affine:surface')[0]; + // hotfix for old page + if ( + surfaceBlock && + (surfaceBlock.yBlock.get('prop:elements') as YMap).get( + 'type' + ) !== '$blocksuite:internal:native$' + ) { + globalBlockSuiteSchema.upgradePage( + 0, + { + 'affine:surface': 3, + }, + page.spaceDoc + ); + } + } catch {} + setPageMode(currentPageId, mode); + const dispose = editor.slots.pageLinkClicked.on(({ pageId }) => { + return openPage(blockSuiteWorkspace.id, pageId); + }); + const disposeTagClick = editor.slots.tagClicked.on(async ({ tagId }) => { + jumpToSubPath(currentWorkspace.id, WorkspaceSubPath.ALL); + setTemporaryFilter([createTagFilter(tagId)]); + }); + return () => { + dispose.dispose(); + disposeTagClick.dispose(); + }; + }, + [ + blockSuiteWorkspace.id, + currentPageId, + currentWorkspace.id, + jumpToSubPath, + mode, + openPage, + setPageMode, + setTemporaryFilter, + ] + ); + + return ( + <> + + + + + } + main={ +
+ + +
+ } + footer={isInTrash ? : null} + sidebar={ + !isInTrash ? ( + <> + + + + ) : null + } + /> + + + ); +}; + +const useForceUpdate = () => { + const [, setCount] = useState(0); + return useCallback(() => setCount(count => count + 1), []); +}; +const useSafePage = (workspace: Workspace, pageId: string) => { + const forceUpdate = useForceUpdate(); + useEffect(() => { + const disposable = workspace.slots.pagesUpdated.on(() => { + forceUpdate(); + }); + return disposable.dispose; + }, [pageId, workspace.slots.pagesUpdated, forceUpdate]); + + return workspace.getPage(pageId); +}; + +export const DetailPage = ({ pageId }: { pageId: string }): ReactElement => { + const [currentWorkspace] = useCurrentWorkspace(); + const currentSyncEngineStatus = useCurrentSyncEngineStatus(); + const currentSyncEngine = useCurrentSyncEngine(); + + // set sync engine priority target + useEffect(() => { + currentSyncEngine?.setPriorityRule(id => id.endsWith(pageId)); + }, [pageId, currentSyncEngine, currentWorkspace]); + + const page = useSafePage(currentWorkspace?.blockSuiteWorkspace, pageId); + + const navigate = useNavigateHelper(); + + // if sync engine has been synced and the page is null, wait 1s and jump to 404 page. + useEffect(() => { + if (currentSyncEngineStatus?.step === SyncEngineStep.Synced && !page) { + const timeout = setTimeout(() => { + navigate.jumpTo404(); + }, 1000); + return () => { + clearTimeout(timeout); + }; + } + return; + }, [currentSyncEngineStatus, navigate, page]); + + if (!page) { + return ; + } + + if (page.meta.jumpOnce) { + currentWorkspace.blockSuiteWorkspace.setPageMeta(page.id, { + jumpOnce: false, + }); + } + + return ; +}; + +export const loader: LoaderFunction = async () => { + return null; +}; + +export const Component = () => { + performanceRenderLogger.info('DetailPage'); + + const setCurrentWorkspaceId = useSetAtom(currentWorkspaceIdAtom); + const setCurrentPageId = useSetAtom(currentPageIdAtom); + const params = useParams(); + + useEffect(() => { + if (params.workspaceId) { + localStorage.setItem('last_workspace_id', params.workspaceId); + setCurrentWorkspaceId(params.workspaceId); + } + if (params.pageId) { + localStorage.setItem('last_page_id', params.pageId); + setCurrentPageId(params.pageId); + } + }, [params, setCurrentPageId, setCurrentWorkspaceId]); + + const pageId = params.pageId; + + // Add a key to force rerender when page changed, to avoid error boundary persisting. + return ( + + {pageId ? : null} + + ); +}; diff --git a/packages/frontend/core/src/pages/workspace/detail-page/editor-sidebar/atoms.ts b/packages/frontend/core/src/pages/workspace/detail-page/editor-sidebar/atoms.ts new file mode 100644 index 000000000..0d0fcfb8d --- /dev/null +++ b/packages/frontend/core/src/pages/workspace/detail-page/editor-sidebar/atoms.ts @@ -0,0 +1,79 @@ +// main editor sidebar states +import { assertExists } from '@blocksuite/global/utils'; +import { atom } from 'jotai'; +import { selectAtom } from 'jotai/utils'; + +import { outlineExtension } from './extensions/outline'; +import type { EditorExtension, EditorExtensionName } from './types'; + +// the list of all possible extensions in affine. +// order matters (determines the order of the tabs) +export const extensions: EditorExtension[] = [outlineExtension]; + +export interface EditorSidebarState { + isOpen: boolean; + width: number; + activeExtension?: EditorExtension; + extensions: EditorExtension[]; +} + +const baseStateAtom = atom({ + isOpen: false, + width: 300, // todo: should be resizable + activeExtension: extensions[0], + extensions: extensions, // todo: maybe should be dynamic (by feature flag?) +}); + +export const editorSidebarStateAtom = atom(get => get(baseStateAtom)); + +const isOpenAtom = selectAtom(baseStateAtom, state => state.isOpen); +const activeExtensionAtom = selectAtom( + baseStateAtom, + state => state.activeExtension +); +const widthAtom = selectAtom(baseStateAtom, state => state.width); + +export const editorExtensionsAtom = selectAtom( + baseStateAtom, + state => state.extensions +); + +// get/set sidebar open state +export const editorSidebarOpenAtom = atom( + get => get(isOpenAtom), + (_, set, isOpen: boolean) => { + set(baseStateAtom, prev => { + return { ...prev, isOpen }; + }); + } +); + +// get/set active extension +export const editorSidebarActiveExtensionAtom = atom( + get => get(activeExtensionAtom), + (_, set, extension: EditorExtensionName) => { + set(baseStateAtom, prev => { + const extensions = prev.extensions; + const newExtension = extensions.find(e => e.name === extension); + assertExists(newExtension, `extension ${extension} not found`); + return { ...prev, activeExtension: newExtension }; + }); + } +); + +// toggle sidebar (write only) +export const editorSidebarToggleAtom = atom(null, (_, set) => { + set(baseStateAtom, prev => { + return { ...prev, isOpen: !prev.isOpen }; + }); +}); + +// get/set sidebar width +export const editorSidebarWidthAtom = atom( + get => get(widthAtom), + (_, set, width: number) => { + set(baseStateAtom, prev => { + return { ...prev, width }; + }); + } +); diff --git a/packages/frontend/core/src/pages/workspace/detail-page/editor-sidebar/editor-sidebar.css.ts b/packages/frontend/core/src/pages/workspace/detail-page/editor-sidebar/editor-sidebar.css.ts new file mode 100644 index 000000000..5c9fd6fa1 --- /dev/null +++ b/packages/frontend/core/src/pages/workspace/detail-page/editor-sidebar/editor-sidebar.css.ts @@ -0,0 +1,10 @@ +import { style } from '@vanilla-extract/css'; + +export const root = style({ + display: 'flex', + flexDirection: 'column', + flex: 1, + overflow: 'auto', + width: '100%', + minWidth: '300px', +}); diff --git a/packages/frontend/core/src/pages/workspace/detail-page/editor-sidebar/editor-sidebar.tsx b/packages/frontend/core/src/pages/workspace/detail-page/editor-sidebar/editor-sidebar.tsx new file mode 100644 index 000000000..9d0427f2c --- /dev/null +++ b/packages/frontend/core/src/pages/workspace/detail-page/editor-sidebar/editor-sidebar.tsx @@ -0,0 +1,16 @@ +import { useAtomValue } from 'jotai'; + +import { editorSidebarStateAtom } from './atoms'; +import * as styles from './editor-sidebar.css'; + +export const EditorSidebar = () => { + const sidebarState = useAtomValue(editorSidebarStateAtom); + const Component = sidebarState.activeExtension?.Component; + + // do we need this? + if (!sidebarState.isOpen) { + return null; + } + + return
{Component ? : null}
; +}; diff --git a/packages/frontend/core/src/pages/workspace/detail-page/editor-sidebar/extensions/extensions.css.ts b/packages/frontend/core/src/pages/workspace/detail-page/editor-sidebar/extensions/extensions.css.ts new file mode 100644 index 000000000..f5231a4e1 --- /dev/null +++ b/packages/frontend/core/src/pages/workspace/detail-page/editor-sidebar/extensions/extensions.css.ts @@ -0,0 +1,45 @@ +import { createVar, style } from '@vanilla-extract/css'; + +export const activeIdx = createVar(); + +export const switchRoot = style({ + vars: { + [activeIdx]: '0', + }, + display: 'flex', + alignItems: 'center', + gap: '8px', + height: '32px', + borderRadius: '12px', + padding: '4px', + position: 'relative', + background: 'var(--affine-background-secondary-color)', + + '::after': { + content: '""', + display: 'block', + width: '24px', + height: '24px', + background: 'var(--affine-background-primary-color)', + boxShadow: 'var(--affine-shadow-1)', + borderRadius: '8px', + position: 'absolute', + transform: `translateX(calc(${activeIdx} * 32px))`, + transition: 'all .15s', + }, +}); + +export const button = style({ + width: '24px', + height: '24px', + borderRadius: '8px', + color: 'var(--affine-icon-color)', + position: 'relative', + zIndex: 1, + + selectors: { + '&[data-active=true]': { + color: 'var(--affine-primary-color)', + }, + }, +}); diff --git a/packages/frontend/core/src/pages/workspace/detail-page/editor-sidebar/extensions/extensions.tsx b/packages/frontend/core/src/pages/workspace/detail-page/editor-sidebar/extensions/extensions.tsx new file mode 100644 index 000000000..01c0a0722 --- /dev/null +++ b/packages/frontend/core/src/pages/workspace/detail-page/editor-sidebar/extensions/extensions.tsx @@ -0,0 +1,37 @@ +import { IconButton } from '@affine/component'; +import { assignInlineVars } from '@vanilla-extract/dynamic'; +import { useAtom, useAtomValue } from 'jotai'; + +import { + editorExtensionsAtom, + editorSidebarActiveExtensionAtom, +} from '../atoms'; +import * as styles from './extensions.css'; + +// provide a switcher for active extensions +// will be used in global top header (MacOS) or sidebar (Windows) +export const ExtensionTabs = () => { + const exts = useAtomValue(editorExtensionsAtom); + const [selected, setSelected] = useAtom(editorSidebarActiveExtensionAtom); + const vars = assignInlineVars({ + [styles.activeIdx]: String( + exts.findIndex(ext => ext.name === selected?.name) ?? 0 + ), + }); + return ( +
+ {exts.map(extension => { + return ( + setSelected(extension.name)} + key={extension.name} + data-active={selected?.name === extension.name} + className={styles.button} + > + {extension.icon} + + ); + })} +
+ ); +}; diff --git a/packages/frontend/core/src/pages/workspace/detail-page/editor-sidebar/extensions/frame.css.ts b/packages/frontend/core/src/pages/workspace/detail-page/editor-sidebar/extensions/frame.css.ts new file mode 100644 index 000000000..0a2dbb442 --- /dev/null +++ b/packages/frontend/core/src/pages/workspace/detail-page/editor-sidebar/extensions/frame.css.ts @@ -0,0 +1,7 @@ +import { style } from '@vanilla-extract/css'; + +export const root = style({ + display: 'flex', + height: '100%', + width: '100%', +}); diff --git a/packages/frontend/core/src/pages/workspace/detail-page/editor-sidebar/extensions/frame.tsx b/packages/frontend/core/src/pages/workspace/detail-page/editor-sidebar/extensions/frame.tsx new file mode 100644 index 000000000..9ac170544 --- /dev/null +++ b/packages/frontend/core/src/pages/workspace/detail-page/editor-sidebar/extensions/frame.tsx @@ -0,0 +1,41 @@ +import { TOCNotesPanel } from '@blocksuite/blocks'; +import { assertExists } from '@blocksuite/global/utils'; +import { FrameIcon } from '@blocksuite/icons'; +import { useCallback, useRef } from 'react'; + +import { useCurrentPage } from '../../../../../hooks/current/use-current-page'; +import type { EditorExtension } from '../types'; +import * as styles from './frame.css'; + +// A wrapper for TOCNotesPanel +const EditorOutline = () => { + const tocPanelRef = useRef(null); + const currentPage = useCurrentPage(); + + const onRefChange = useCallback((container: HTMLDivElement | null) => { + if (container) { + assertExists(tocPanelRef.current, 'toc panel should be initialized'); + container.append(tocPanelRef.current); + } + }, []); + + if (!currentPage) { + return; + } + + if (!tocPanelRef.current) { + tocPanelRef.current = new TOCNotesPanel(); + } + + if (currentPage !== tocPanelRef.current?.page) { + (tocPanelRef.current as TOCNotesPanel).page = currentPage; + } + + return
; +}; + +export const frameExtension: EditorExtension = { + name: 'frame', + icon: , + Component: EditorOutline, +}; diff --git a/packages/frontend/core/src/pages/workspace/detail-page/editor-sidebar/extensions/index.tsx b/packages/frontend/core/src/pages/workspace/detail-page/editor-sidebar/extensions/index.tsx new file mode 100644 index 000000000..4509f5ccb --- /dev/null +++ b/packages/frontend/core/src/pages/workspace/detail-page/editor-sidebar/extensions/index.tsx @@ -0,0 +1 @@ +export * from './extensions'; diff --git a/packages/frontend/core/src/pages/workspace/detail-page/editor-sidebar/extensions/outline.css.ts b/packages/frontend/core/src/pages/workspace/detail-page/editor-sidebar/extensions/outline.css.ts new file mode 100644 index 000000000..0a2dbb442 --- /dev/null +++ b/packages/frontend/core/src/pages/workspace/detail-page/editor-sidebar/extensions/outline.css.ts @@ -0,0 +1,7 @@ +import { style } from '@vanilla-extract/css'; + +export const root = style({ + display: 'flex', + height: '100%', + width: '100%', +}); diff --git a/packages/frontend/core/src/pages/workspace/detail-page/editor-sidebar/extensions/outline.tsx b/packages/frontend/core/src/pages/workspace/detail-page/editor-sidebar/extensions/outline.tsx new file mode 100644 index 000000000..eaa2f3cac --- /dev/null +++ b/packages/frontend/core/src/pages/workspace/detail-page/editor-sidebar/extensions/outline.tsx @@ -0,0 +1,41 @@ +import { TOCNotesPanel } from '@blocksuite/blocks'; +import { assertExists } from '@blocksuite/global/utils'; +import { TocIcon } from '@blocksuite/icons'; +import { useCallback, useRef } from 'react'; + +import { useCurrentPage } from '../../../../../hooks/current/use-current-page'; +import type { EditorExtension } from '../types'; +import * as styles from './outline.css'; + +// A wrapper for TOCNotesPanel +const EditorOutline = () => { + const tocPanelRef = useRef(null); + const currentPage = useCurrentPage(); + + const onRefChange = useCallback((container: HTMLDivElement | null) => { + if (container) { + assertExists(tocPanelRef.current, 'toc panel should be initialized'); + container.append(tocPanelRef.current); + } + }, []); + + if (!currentPage) { + return; + } + + if (!tocPanelRef.current) { + tocPanelRef.current = new TOCNotesPanel(); + } + + if (currentPage !== tocPanelRef.current?.page) { + (tocPanelRef.current as TOCNotesPanel).page = currentPage; + } + + return
; +}; + +export const outlineExtension: EditorExtension = { + name: 'outline', + icon: , + Component: EditorOutline, +}; diff --git a/packages/frontend/core/src/pages/workspace/detail-page/editor-sidebar/index.ts b/packages/frontend/core/src/pages/workspace/detail-page/editor-sidebar/index.ts new file mode 100644 index 000000000..3ee1e3b1b --- /dev/null +++ b/packages/frontend/core/src/pages/workspace/detail-page/editor-sidebar/index.ts @@ -0,0 +1,4 @@ +export * from './atoms'; +export * from './editor-sidebar'; +export * from './extensions'; +export * from './types'; diff --git a/packages/frontend/core/src/pages/workspace/detail-page/editor-sidebar/types.ts b/packages/frontend/core/src/pages/workspace/detail-page/editor-sidebar/types.ts new file mode 100644 index 000000000..ab7ab4d04 --- /dev/null +++ b/packages/frontend/core/src/pages/workspace/detail-page/editor-sidebar/types.ts @@ -0,0 +1,7 @@ +export type EditorExtensionName = 'outline' | 'frame'; + +export interface EditorExtension { + name: EditorExtensionName; + icon: React.ReactNode; + Component: React.ComponentType; +} diff --git a/packages/frontend/core/src/pages/workspace/detail-page/index.ts b/packages/frontend/core/src/pages/workspace/detail-page/index.ts new file mode 100644 index 000000000..dbf539bf2 --- /dev/null +++ b/packages/frontend/core/src/pages/workspace/detail-page/index.ts @@ -0,0 +1 @@ +export * from './detail-page'; diff --git a/packages/frontend/core/src/components/workspace-header.css.ts b/packages/frontend/core/src/pages/workspace/trash-page.css.ts similarity index 85% rename from packages/frontend/core/src/components/workspace-header.css.ts rename to packages/frontend/core/src/pages/workspace/trash-page.css.ts index 6b483a609..ec4d58ba7 100644 --- a/packages/frontend/core/src/components/workspace-header.css.ts +++ b/packages/frontend/core/src/pages/workspace/trash-page.css.ts @@ -1,5 +1,7 @@ import { style } from '@vanilla-extract/css'; +export { root } from './all-page/all-page.css'; + export const trashTitle = style({ display: 'flex', alignItems: 'center', diff --git a/packages/frontend/core/src/pages/workspace/trash-page.tsx b/packages/frontend/core/src/pages/workspace/trash-page.tsx index 26345cc5a..82f2a87f2 100644 --- a/packages/frontend/core/src/pages/workspace/trash-page.tsx +++ b/packages/frontend/core/src/pages/workspace/trash-page.tsx @@ -3,20 +3,43 @@ import { TrashOperationCell, VirtualizedPageList, } from '@affine/component/page-list'; -import { WorkspaceSubPath } from '@affine/env/workspace'; import { useAFFiNEI18N } from '@affine/i18n/hooks'; import { assertExists } from '@blocksuite/global/utils'; +import { DeleteIcon } from '@blocksuite/icons'; import type { PageMeta } from '@blocksuite/store'; import { useBlockSuitePageMeta } from '@toeverything/hooks/use-block-suite-page-meta'; import { useCallback } from 'react'; import { usePageHelper } from '../../components/blocksuite/block-suite-page-list/utils'; -import { WorkspaceHeader } from '../../components/workspace-header'; +import { Header } from '../../components/pure/header'; +import { WindowsAppControls } from '../../components/pure/header/windows-app-controls'; import { useBlockSuiteMetaHelper } from '../../hooks/affine/use-block-suite-meta-helper'; import { useCurrentWorkspace } from '../../hooks/current/use-current-workspace'; -import * as styles from './all-page.css'; import { EmptyPageList } from './page-list-empty'; import { useFilteredPageMetas } from './pages'; +import * as styles from './trash-page.css'; + +const isWindowsDesktop = environment.isDesktop && environment.isWindows; +const TrashHeader = () => { + const t = useAFFiNEI18N(); + return ( +
+ + {t['com.affine.workspaceSubPath.trash']()} +
+ } + right={ + isWindowsDesktop ? ( +
+ +
+ ) : null + } + /> + ); +}; export const TrashPage = () => { const [currentWorkspace] = useCurrentWorkspace(); @@ -60,12 +83,7 @@ export const TrashPage = () => { ); return (
- + {filteredPageMetas.length > 0 ? ( import('./pages/workspace/all-page'), + lazy: () => import('./pages/workspace/all-page/all-page'), }, { path: 'collection/:collectionId', @@ -25,13 +25,13 @@ export const routes = [ }, { path: ':pageId', - lazy: () => import('./pages/workspace/detail-page'), + lazy: () => import('./pages/workspace/detail-page/detail-page'), }, ], }, { path: '/share/:workspaceId/:pageId', - lazy: () => import('./pages/share/detail-page'), + lazy: () => import('./pages/share/share-detail-page'), }, { path: '/404', diff --git a/packages/frontend/core/src/utils/toast.ts b/packages/frontend/core/src/utils/toast.ts index 4e951259c..b65e99d96 100644 --- a/packages/frontend/core/src/utils/toast.ts +++ b/packages/frontend/core/src/utils/toast.ts @@ -1,21 +1,16 @@ import type { ToastOptions } from '@affine/component'; import { toast as basicToast } from '@affine/component'; -import { assertEquals, assertExists } from '@blocksuite/global/utils'; -import { getCurrentStore } from '@toeverything/infra/atom'; - -import { mainContainerAtom } from '../atoms/element'; +import { assertEquals } from '@blocksuite/global/utils'; export const toast = (message: string, options?: ToastOptions) => { - const mainContainer = getCurrentStore().get(mainContainerAtom); const modal = document.querySelector( '[role=presentation]' ) as HTMLDivElement | null; - assertExists(mainContainer, 'main container should exist'); if (modal) { assertEquals(modal.constructor, HTMLDivElement, 'modal should be div'); } return basicToast(message, { - portal: modal || mainContainer || document.body, + portal: modal || document.body, ...options, }); }; diff --git a/packages/frontend/hooks/src/use-is-tiny-screen.ts b/packages/frontend/hooks/src/use-is-tiny-screen.ts index 506416a98..e87971da7 100644 --- a/packages/frontend/hooks/src/use-is-tiny-screen.ts +++ b/packages/frontend/hooks/src/use-is-tiny-screen.ts @@ -4,24 +4,22 @@ import debounce from 'lodash.debounce'; import { type RefObject, useEffect, useState } from 'react'; export function useIsTinyScreen({ - mainContainer, + container, leftStatic, leftSlot, centerDom, - rightStatic, rightSlot, }: { - mainContainer: HTMLElement | null; + container: HTMLElement | null; leftStatic: RefObject; leftSlot: RefObject[]; centerDom: RefObject; - rightStatic: RefObject; rightSlot: RefObject[]; }) { const [isTinyScreen, setIsTinyScreen] = useState(false); useEffect(() => { - if (!mainContainer) { + if (!container) { return; } const handleResize = debounce(() => { @@ -33,8 +31,6 @@ export function useIsTinyScreen({ return accWidth + (dom.current?.clientWidth || 0); }, 0); - const rightStaticWidth = rightStatic.current?.clientWidth || 0; - const rightSlotWidth = rightSlot.reduce((accWidth, dom) => { return accWidth + (dom.current?.clientWidth || 0); }, 0); @@ -46,14 +42,13 @@ export function useIsTinyScreen({ return; } - const containerRect = mainContainer.getBoundingClientRect(); + const containerRect = container.getBoundingClientRect(); const centerRect = centerDom.current.getBoundingClientRect(); if ( leftStaticWidth + leftSlotWidth + containerRect.left >= centerRect.left || - containerRect.right - centerRect.right <= - rightSlotWidth + rightStaticWidth + containerRect.right - centerRect.right <= rightSlotWidth ) { setIsTinyScreen(true); } else { @@ -67,20 +62,12 @@ export function useIsTinyScreen({ handleResize(); }); - resizeObserver.observe(mainContainer); + resizeObserver.observe(container); return () => { - resizeObserver.unobserve(mainContainer); + resizeObserver.unobserve(container); }; - }, [ - centerDom, - isTinyScreen, - leftSlot, - leftStatic, - mainContainer, - rightSlot, - rightStatic, - ]); + }, [centerDom, isTinyScreen, leftSlot, leftStatic, container, rightSlot]); return isTinyScreen; } diff --git a/packages/frontend/i18n/src/resources/en.json b/packages/frontend/i18n/src/resources/en.json index 70ede531f..32aa077c2 100644 --- a/packages/frontend/i18n/src/resources/en.json +++ b/packages/frontend/i18n/src/resources/en.json @@ -510,6 +510,7 @@ "com.affine.cmdk.affine.editor.edgeless.presentation-start": "Start Presentation", "com.affine.cmdk.affine.editor.remove-from-favourites": "Remove from Favourites", "com.affine.cmdk.affine.editor.restore-from-trash": "Restore from Trash", + "com.affine.cmdk.affine.editor.trash-footer-hint": "This page has been moved to the trash, you can either restore or permanently delete it.", "com.affine.cmdk.affine.font-style.to": "Change Font Style to", "com.affine.cmdk.affine.full-width-layout.to": "Change Full Width Layout to", "com.affine.cmdk.affine.getting-started": "Getting Started", diff --git a/tests/affine-desktop/e2e/basic.spec.ts b/tests/affine-desktop/e2e/basic.spec.ts index 5ff31caf8..1089934ac 100644 --- a/tests/affine-desktop/e2e/basic.spec.ts +++ b/tests/affine-desktop/e2e/basic.spec.ts @@ -160,9 +160,9 @@ test('affine onboarding button', async ({ page }) => { test('windows only check', async ({ page }) => { const windowOnlyUI = page.locator('[data-platform-target=win32]'); if (process.platform === 'win32') { - await expect(windowOnlyUI).toBeVisible(); + await expect(windowOnlyUI.first()).toBeVisible(); } else { - await expect(windowOnlyUI).not.toBeVisible(); + await expect(windowOnlyUI.first()).not.toBeVisible(); } });