diff --git a/apps/web/public/imgs/no-result.svg b/apps/web/public/imgs/no-result.svg new file mode 100644 index 0000000000..16b2bfc1f8 --- /dev/null +++ b/apps/web/public/imgs/no-result.svg @@ -0,0 +1,68 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/web/src/components/__tests__/PinBoard.spec.tsx b/apps/web/src/components/__tests__/PinBoard.spec.tsx new file mode 100644 index 0000000000..c216ff346a --- /dev/null +++ b/apps/web/src/components/__tests__/PinBoard.spec.tsx @@ -0,0 +1,229 @@ +/** + * @vitest-environment happy-dom + */ +import 'fake-indexeddb/auto'; + +import type { PageMeta } from '@blocksuite/store'; +import matchers from '@testing-library/jest-dom/matchers'; +import type { RenderResult } from '@testing-library/react'; +import { render, renderHook } from '@testing-library/react'; +import { createStore, getDefaultStore, Provider } from 'jotai'; +import type { FC, PropsWithChildren } from 'react'; +import { beforeEach, describe, expect, test } from 'vitest'; + +import { workspacesAtom } from '../../atoms'; +import { + currentWorkspaceAtom, + useCurrentWorkspace, +} from '../../hooks/current/use-current-workspace'; +import { useWorkspacesHelper } from '../../hooks/use-workspaces'; +import { ThemeProvider } from '../../providers/ThemeProvider'; +import type { BlockSuiteWorkspace } from '../../shared'; +import type { PivotsProps } from '../pure/workspace-slider-bar/Pivots'; +import Pivots from '../pure/workspace-slider-bar/Pivots'; + +expect.extend(matchers); + +let store = getDefaultStore(); +beforeEach(async () => { + store = createStore(); + await store.get(workspacesAtom); +}); + +const ProviderWrapper: FC = ({ children }) => { + return {children}; +}; + +const initPinBoard = async () => { + // create one workspace with 2 root pages and 2 pivot pages + // - hasPivotPage + // - pivot1 + // - pivot2 + // - noPivotPage + + const mutationHook = renderHook(() => useWorkspacesHelper(), { + wrapper: ProviderWrapper, + }); + const rootPageIds = ['hasPivotPage', 'noPivotPage']; + const pivotPageIds = ['pivot1', 'pivot2']; + const id = await mutationHook.result.current.createLocalWorkspace('test0'); + await store.get(workspacesAtom); + mutationHook.rerender(); + + await store.get(currentWorkspaceAtom); + const currentWorkspaceHook = renderHook(() => useCurrentWorkspace(), { + wrapper: ProviderWrapper, + }); + currentWorkspaceHook.result.current[1](id); + const currentWorkspace = await store.get(currentWorkspaceAtom); + const blockSuiteWorkspace = + currentWorkspace?.blockSuiteWorkspace as BlockSuiteWorkspace; + + rootPageIds.forEach(rootPageId => { + mutationHook.result.current.createWorkspacePage(id, rootPageId); + blockSuiteWorkspace.meta.setPageMeta(rootPageId, { + isPivots: true, + subpageIds: rootPageId === rootPageIds[0] ? pivotPageIds : [], + }); + }); + pivotPageIds.forEach(pivotId => { + mutationHook.result.current.createWorkspacePage(id, pivotId); + blockSuiteWorkspace.meta.setPageMeta(pivotId, { + title: pivotId, + }); + }); + + const App = (props: PivotsProps) => { + return ( + + + + + + ); + }; + + const app = render( + {}} + /> + ); + + return { + rootPageIds, + pivotPageIds, + app, + blockSuiteWorkspace, + }; +}; +const openOperationMenu = async (app: RenderResult, pageId: string) => { + const rootPivot = await app.findByTestId(`pivot-${pageId}`); + const operationBtn = (await rootPivot.querySelector( + '[data-testid="pivot-operation-button"]' + )) as HTMLElement; + await operationBtn.click(); + const menu = await app.findByTestId('pivot-operation-menu'); + expect(menu).toBeInTheDocument(); +}; +describe('PinBoard', () => { + test('add pivot', async () => { + const { app, blockSuiteWorkspace, rootPageIds, pivotPageIds } = + await initPinBoard(); + const [hasPivotPageId] = rootPageIds; + await openOperationMenu(app, hasPivotPageId); + + const addBtn = await app.findByTestId('pivot-operation-add'); + await addBtn.click(); + + const metas = blockSuiteWorkspace.meta.pageMetas ?? []; + const rootPageMeta = blockSuiteWorkspace.meta.getPageMeta(hasPivotPageId); + const addedPageMeta = metas.find( + meta => !pivotPageIds.includes(meta.id) && !rootPageIds.includes(meta.id) + ) as PageMeta; + + // Page meta have been added + expect(blockSuiteWorkspace.meta.pageMetas.length).toBe(5); + // New page meta is added in initial page meta + + expect(rootPageMeta?.subpageIds.includes(addedPageMeta.id)).toBe(true); + app.unmount(); + }); + + test('delete pivot', async () => { + const { + app, + blockSuiteWorkspace, + rootPageIds: [hasPivotPageId], + } = await initPinBoard(); + await openOperationMenu(app, hasPivotPageId); + + const deleteBtn = await app.findByTestId('pivot-operation-move-to-trash'); + await deleteBtn.click(); + + const confirmBtn = await app.findByTestId('move-to-trash-confirm'); + expect(confirmBtn).toBeInTheDocument(); + await confirmBtn.click(); + + // Every page should be tagged as trash + expect(blockSuiteWorkspace.meta.pageMetas.filter(m => m.trash).length).toBe( + 3 + ); + app.unmount(); + }); + + test('rename pivot', async () => { + const { + app, + rootPageIds: [hasPivotPageId], + } = await initPinBoard(); + await openOperationMenu(app, hasPivotPageId); + + const renameBtn = await app.findByTestId('pivot-operation-rename'); + await renameBtn.click(); + + const input = await app.findByTestId(`pivot-input-${hasPivotPageId}`); + expect(input).toBeInTheDocument(); + + // TODO: Fix this test + // fireEvent.change(input, { target: { value: 'tteesstt' } }); + // + // expect( + // blockSuiteWorkspace.meta.getPageMeta(rootPageId)?.name + // ).toBe('tteesstt'); + app.unmount(); + }); + + test('move pivot', async () => { + const { + app, + blockSuiteWorkspace, + rootPageIds: [hasPivotPageId], + pivotPageIds: [pivotId1, pivotId2], + } = await initPinBoard(); + await openOperationMenu(app, pivotId1); + + const moveToBtn = await app.findByTestId('pivot-operation-move-to'); + await moveToBtn.click(); + + const pivotsMenu = await app.findByTestId('pivots-menu'); + expect(pivotsMenu).toBeInTheDocument(); + + await ( + pivotsMenu.querySelector( + `[data-testid="pivot-${pivotId2}"]` + ) as HTMLElement + ).click(); + + const rootPageMeta = blockSuiteWorkspace.meta.getPageMeta(hasPivotPageId); + + expect(rootPageMeta?.subpageIds.includes(pivotId1)).toBe(false); + expect(rootPageMeta?.subpageIds.includes(pivotId2)).toBe(true); + app.unmount(); + }); + + test('remove from pivots', async () => { + const { + app, + blockSuiteWorkspace, + rootPageIds: [hasPivotPageId], + pivotPageIds: [pivotId1], + } = await initPinBoard(); + await openOperationMenu(app, pivotId1); + + const moveToBtn = await app.findByTestId('pivot-operation-move-to'); + await moveToBtn.click(); + + const removeFromPivotsBtn = await app.findByTestId( + 'remove-from-pivots-button' + ); + removeFromPivotsBtn.click(); + + const hasPivotsPageMeta = + blockSuiteWorkspace.meta.getPageMeta(hasPivotPageId); + + expect(hasPivotsPageMeta?.subpageIds.length).toBe(1); + expect(hasPivotsPageMeta?.subpageIds.includes(pivotId1)).toBe(false); + }); +}); diff --git a/apps/web/src/components/affine/operation-menu-items/CopyLink.tsx b/apps/web/src/components/affine/operation-menu-items/CopyLink.tsx index b8008bb58f..260bbf1319 100644 --- a/apps/web/src/components/affine/operation-menu-items/CopyLink.tsx +++ b/apps/web/src/components/affine/operation-menu-items/CopyLink.tsx @@ -1,22 +1,30 @@ import { MenuItem } from '@affine/component'; import { useTranslation } from '@affine/i18n'; import { CopyIcon } from '@blocksuite/icons'; -// import { useRouter } from "next/router"; -// import { useCallback } from "react"; -// -// import { toast } from "../../../utils"; +import { useCallback } from 'react'; -export const CopyLink = () => { +// +import { toast } from '../../../utils'; +import type { CommonMenuItemProps } from './types'; + +export const CopyLink = ({ onItemClick, onSelect }: CommonMenuItemProps) => { const { t } = useTranslation(); - // const router = useRouter(); - // const copyUrl = useCallback(() => { - // const workspaceId = router.query.workspaceId; - // navigator.clipboard.writeText(window.location.href); - // toast(t("Copied link to clipboard")); - // }, [router.query.workspaceId, t]); + + const copyUrl = useCallback(() => { + navigator.clipboard.writeText(window.location.href); + toast(t('Copied link to clipboard')); + }, [t]); + return ( <> - {}} icon={} disabled={true}> + { + copyUrl(); + onItemClick?.(); + onSelect?.(); + }} + icon={} + > {t('Copy Link')} diff --git a/apps/web/src/components/affine/operation-menu-items/Export.tsx b/apps/web/src/components/affine/operation-menu-items/Export.tsx index 6c2a876717..45d0ced54c 100644 --- a/apps/web/src/components/affine/operation-menu-items/Export.tsx +++ b/apps/web/src/components/affine/operation-menu-items/Export.tsx @@ -7,7 +7,12 @@ import { ExportToMarkdownIcon, } from '@blocksuite/icons'; -export const Export = () => { +import type { CommonMenuItemProps } from './types'; + +export const Export = ({ + onSelect, + onItemClick, +}: CommonMenuItemProps<{ type: 'markdown' | 'html' }>) => { const { t } = useTranslation(); return ( @@ -21,6 +26,7 @@ export const Export = () => { onClick={() => { // @ts-expect-error globalThis.currentEditor.contentParser.onExportHtml(); + onSelect?.({ type: 'html' }); }} icon={} > @@ -30,6 +36,7 @@ export const Export = () => { onClick={() => { // @ts-expect-error globalThis.currentEditor.contentParser.onExportMarkdown(); + onSelect?.({ type: 'markdown' }); }} icon={} > @@ -41,7 +48,10 @@ export const Export = () => { } endIcon={} - onClick={e => e.stopPropagation()} + onClick={e => { + e.stopPropagation(); + onItemClick?.(); + }} > {t('Export')} diff --git a/apps/web/src/components/affine/operation-menu-items/MoveTo.tsx b/apps/web/src/components/affine/operation-menu-items/MoveTo.tsx index deba4def80..5e31978c17 100644 --- a/apps/web/src/components/affine/operation-menu-items/MoveTo.tsx +++ b/apps/web/src/components/affine/operation-menu-items/MoveTo.tsx @@ -6,16 +6,24 @@ import { useRef, useState } from 'react'; import type { BlockSuiteWorkspace } from '../../../shared'; import { PivotsMenu } from '../pivots'; +import type { CommonMenuItemProps } from './types'; + +export type MoveToProps = CommonMenuItemProps<{ + dragId: string; + dropId: string; +}> & { + metas: PageMeta[]; + currentMeta: PageMeta; + blockSuiteWorkspace: BlockSuiteWorkspace; +}; export const MoveTo = ({ metas, currentMeta, blockSuiteWorkspace, -}: { - metas: PageMeta[]; - currentMeta: PageMeta; - blockSuiteWorkspace: BlockSuiteWorkspace; -}) => { + onSelect, + onItemClick, +}: MoveToProps) => { const { t } = useTranslation(); const ref = useRef(null); const [anchorEl, setAnchorEl] = useState(null); @@ -27,9 +35,11 @@ export const MoveTo = ({ onClick={e => { e.stopPropagation(); setAnchorEl(ref.current); + onItemClick?.(); }} icon={} endIcon={} + data-testid="move-to-menu-item" > {t('Move to')} @@ -40,6 +50,7 @@ export const MoveTo = ({ metas={metas.filter(meta => !meta.trash)} currentMeta={currentMeta} blockSuiteWorkspace={blockSuiteWorkspace} + onPivotClick={onSelect} /> ); diff --git a/apps/web/src/components/affine/operation-menu-items/MoveToTrash.tsx b/apps/web/src/components/affine/operation-menu-items/MoveToTrash.tsx index 3ea05448fc..0fd5f3828c 100644 --- a/apps/web/src/components/affine/operation-menu-items/MoveToTrash.tsx +++ b/apps/web/src/components/affine/operation-menu-items/MoveToTrash.tsx @@ -1,60 +1,53 @@ +import type { ConfirmProps } from '@affine/component'; import { Confirm, MenuItem } from '@affine/component'; import { useTranslation } from '@affine/i18n'; import { DeleteTemporarilyIcon } from '@blocksuite/icons'; import type { PageMeta } from '@blocksuite/store'; -import { useState } from 'react'; -import { usePageMetaHelper } from '../../../hooks/use-page-meta'; -import type { BlockSuiteWorkspace } from '../../../shared'; -import { toast } from '../../../utils'; +import type { CommonMenuItemProps } from './types'; export const MoveToTrash = ({ - currentMeta, - blockSuiteWorkspace, + onSelect, + onItemClick, testId, -}: { - currentMeta: PageMeta; - blockSuiteWorkspace: BlockSuiteWorkspace; - testId?: string; -}) => { +}: CommonMenuItemProps) => { const { t } = useTranslation(); - const { setPageMeta } = usePageMetaHelper(blockSuiteWorkspace); - const [open, setOpen] = useState(false); return ( <> { - setOpen(true); + onItemClick?.(); + onSelect?.(); }} icon={} > {t('Move to Trash')} - { - toast(t('Moved to Trash')); - setOpen(false); - setPageMeta(currentMeta.id, { - trash: true, - trashDate: +new Date(), - }); - }} - onClose={() => { - setOpen(false); - }} - onCancel={() => { - setOpen(false); - }} - /> ); }; + +const ConfirmModal = ({ + meta, + ...confirmModalProps +}: { + meta: PageMeta; +} & ConfirmProps) => { + const { t } = useTranslation(); + + return ( + + ); +}; + +MoveToTrash.ConfirmModal = ConfirmModal; diff --git a/apps/web/src/components/affine/operation-menu-items/types.ts b/apps/web/src/components/affine/operation-menu-items/types.ts new file mode 100644 index 0000000000..79cf3c7a74 --- /dev/null +++ b/apps/web/src/components/affine/operation-menu-items/types.ts @@ -0,0 +1,7 @@ +export type CommonMenuItemProps = { + // onItemClick is triggered when the item is clicked, sometimes after item click, it still has some internal logic to run(like popover a new menu), so we need to have a separate callback for that + onItemClick?: () => void; + // onSelect is triggered when the item is selected, it's the final callback for the item click + onSelect?: (params?: SelectParams) => void; + testId?: string; +}; diff --git a/apps/web/src/components/affine/pivots/OperationButton.tsx b/apps/web/src/components/affine/pivots/OperationButton.tsx deleted file mode 100644 index e949aafeb8..0000000000 --- a/apps/web/src/components/affine/pivots/OperationButton.tsx +++ /dev/null @@ -1,103 +0,0 @@ -import { MuiClickAwayListener } from '@affine/component'; -import { MoreVerticalIcon } from '@blocksuite/icons'; -import type { PageMeta } from '@blocksuite/store'; -import { useTheme } from '@mui/material'; -import { useMemo, useState } from 'react'; - -import type { BlockSuiteWorkspace } from '../../../shared'; -import { OperationMenu } from './OperationMenu'; -import { PivotsMenu } from './PivotsMenu/PivotsMenu'; -import { StyledOperationButton } from './styles'; - -export type OperationButtonProps = { - onAdd: () => void; - onDelete: () => void; - metas: PageMeta[]; - currentMeta: PageMeta; - blockSuiteWorkspace: BlockSuiteWorkspace; - isHover: boolean; - onMenuClose?: () => void; -}; -export const OperationButton = ({ - onAdd, - onDelete, - metas, - currentMeta, - blockSuiteWorkspace, - isHover, - onMenuClose, -}: OperationButtonProps) => { - const { - zIndex: { modal: modalIndex }, - } = useTheme(); - - const [anchorEl, setAnchorEl] = useState(null); - const [operationOpen, setOperationOpen] = useState(false); - const [pivotsMenuOpen, setPivotsMenuOpen] = useState(false); - - const menuIndex = useMemo(() => modalIndex + 1, [modalIndex]); - - return ( - { - setOperationOpen(false); - setPivotsMenuOpen(false); - }} - > -
{ - e.stopPropagation(); - }} - onMouseLeave={() => { - setOperationOpen(false); - setPivotsMenuOpen(false); - }} - > - setAnchorEl(ref)} - size="small" - onClick={() => { - setOperationOpen(!operationOpen); - }} - visible={isHover} - > - - - { - switch (type) { - case 'add': - onAdd(); - break; - case 'move': - setPivotsMenuOpen(true); - break; - case 'delete': - onDelete(); - break; - } - setOperationOpen(false); - onMenuClose?.(); - }} - currentMeta={currentMeta} - blockSuiteWorkspace={blockSuiteWorkspace} - /> - - -
-
- ); -}; diff --git a/apps/web/src/components/affine/pivots/OperationMenu.tsx b/apps/web/src/components/affine/pivots/OperationMenu.tsx deleted file mode 100644 index 137d75fedf..0000000000 --- a/apps/web/src/components/affine/pivots/OperationMenu.tsx +++ /dev/null @@ -1,74 +0,0 @@ -import type { PureMenuProps } from '@affine/component'; -import { MenuItem, PureMenu } from '@affine/component'; -import { useTranslation } from '@affine/i18n'; -import { MoveToIcon, PenIcon, PlusIcon } from '@blocksuite/icons'; -import type { PageMeta } from '@blocksuite/store'; -import type { ReactElement } from 'react'; - -import type { BlockSuiteWorkspace } from '../../../shared'; -import { CopyLink, MoveToTrash } from '../operation-menu-items'; - -export type OperationMenuProps = { - onSelect: (type: OperationMenuItems['type']) => void; - blockSuiteWorkspace: BlockSuiteWorkspace; - currentMeta: PageMeta; -} & PureMenuProps; - -export type OperationMenuItems = { - label: string; - icon: ReactElement; - type: 'add' | 'move' | 'rename' | 'delete' | 'copy'; - disabled?: boolean; -}; - -const menuItems: OperationMenuItems[] = [ - { - label: 'Add a subpage inside', - icon: , - type: 'add', - }, - { - label: 'Move to', - icon: , - type: 'move', - }, - { - label: 'Rename', - icon: , - type: 'rename', - disabled: true, - }, -]; - -export const OperationMenu = ({ - onSelect, - blockSuiteWorkspace, - currentMeta, - ...menuProps -}: OperationMenuProps) => { - const { t } = useTranslation(); - - return ( - - {menuItems.map((item, index) => { - return ( - { - onSelect(item.type); - }} - icon={item.icon} - disabled={!!item.disabled} - > - {t(item.label)} - - ); - })} - - - - ); -}; diff --git a/apps/web/src/components/affine/pivots/PivotsMenu/Pivots.tsx b/apps/web/src/components/affine/pivots/PivotsMenu/Pivots.tsx index 9ac93ba7ab..f6e6b9ceab 100644 --- a/apps/web/src/components/affine/pivots/PivotsMenu/Pivots.tsx +++ b/apps/web/src/components/affine/pivots/PivotsMenu/Pivots.tsx @@ -1,3 +1,4 @@ +import type { TreeViewProps } from '@affine/component'; import { MuiCollapse, TreeView } from '@affine/component'; import { useTranslation } from '@affine/i18n'; import { ArrowDownSmallIcon, PivotsIcon } from '@blocksuite/icons'; @@ -5,48 +6,32 @@ import type { MouseEvent } from 'react'; import { useCallback, useMemo, useState } from 'react'; import { usePageMetaHelper } from '../../../../hooks/use-page-meta'; -import { usePivotData } from '../hooks/usePivotData'; -import { usePivotHandler } from '../hooks/usePivotHandler'; -import { PivotRender } from '../PivotRender'; +import { toast } from '../../../../utils'; import { StyledCollapsedButton, StyledPivot } from '../styles'; +import type { NodeRenderProps } from '../types'; import EmptyItem from './EmptyItem'; import type { PivotsMenuProps } from './PivotsMenu'; +export type PivotsProps = { + data: TreeViewProps['data']; +} & Pick; export const Pivots = ({ - metas, + data, blockSuiteWorkspace, currentMeta, -}: Pick) => { +}: PivotsProps) => { const { t } = useTranslation(); const { setPageMeta } = usePageMetaHelper(blockSuiteWorkspace); const [showPivot, setShowPivot] = useState(true); - const { handleDrop } = usePivotHandler({ - blockSuiteWorkspace, - metas, - }); - const { data } = usePivotData({ - metas, - pivotRender: PivotRender, - blockSuiteWorkspace, - onClick: (e, node) => { - handleDrop(currentMeta.id, node.id, { - bottomLine: false, - topLine: false, - internal: true, - }); - }, - }); - - const isPivotEmpty = useMemo( - () => metas.filter(meta => !meta.trash).length === 0, - [metas] - ); - + const isPivotEmpty = useMemo(() => data.length === 0, [data]); return ( <> { setPageMeta(currentMeta.id, { isPivots: true }); + toast(`Moved "${currentMeta.title}" to Pivots`); }} > void; } & PureMenuProps; export const PivotsMenu = ({ @@ -28,6 +34,7 @@ export const PivotsMenu = ({ currentMeta, blockSuiteWorkspace, showRemovePivots = false, + onPivotClick, ...pureMenuProps }: PivotsMenuProps) => { const { t } = useTranslation(); @@ -39,8 +46,42 @@ export const PivotsMenu = ({ meta => !meta.trash && meta.title.includes(query) ); + const { handleDrop } = usePivotHandler({ + blockSuiteWorkspace, + metas, + }); + + const handleClick = useCallback( + (dropId: string) => { + const targetTitle = metas.find(m => m.id === dropId)?.title; + + handleDrop(currentMeta.id, dropId, { + bottomLine: false, + topLine: false, + internal: true, + }); + onPivotClick?.({ dragId: currentMeta.id, dropId }); + toast(`Moved "${currentMeta.title}" to "${targetTitle}"`); + }, + [currentMeta.id, currentMeta.title, handleDrop, metas, onPivotClick] + ); + + const { data } = usePivotData({ + metas, + pivotRender: PivotRender, + blockSuiteWorkspace, + onClick: (e, node) => { + handleClick(node.id); + }, + }); + return ( - + {isSearching && ( - <> - - {searchResult.length - ? t('Find results', { number: searchResult.length }) - : t('Find 0 result')} - - {searchResult.map(meta => { - return {meta.title}; - })} - + )} - {!isSearching && ( <> Suggested @@ -84,6 +116,7 @@ export const PivotsMenu = ({ {showRemovePivots && ( { setPageMeta(currentMeta.id, { isPivots: false }); const parentMeta = metas.find(m => diff --git a/apps/web/src/components/affine/pivots/PivotsMenu/SearchContent.tsx b/apps/web/src/components/affine/pivots/PivotsMenu/SearchContent.tsx new file mode 100644 index 0000000000..924dc67275 --- /dev/null +++ b/apps/web/src/components/affine/pivots/PivotsMenu/SearchContent.tsx @@ -0,0 +1,63 @@ +import { FlexWrapper } from '@affine/component'; +import { useTranslation } from '@affine/i18n'; +import { EdgelessIcon, PageIcon } from '@blocksuite/icons'; +import type { PageMeta } from '@blocksuite/store'; +import { useAtomValue } from 'jotai'; +import Image from 'next/legacy/image'; +import React from 'react'; + +import { workspacePreferredModeAtom } from '../../../../atoms'; +import { StyledMenuSubTitle, StyledPivot } from '../styles'; + +export const SearchContent = ({ + results, + onClick, +}: { + results: PageMeta[]; + onClick?: (dropId: string) => void; +}) => { + const { t } = useTranslation(); + const record = useAtomValue(workspacePreferredModeAtom); + + if (results.length) { + return ( + <> + + {t('Find results', { number: results.length })} + + {results.map(meta => { + return ( + { + onClick?.(meta.id); + }} + data-testid="pivot-search-result" + > + {record[meta.id] === 'edgeless' ? : } + {meta.title} + + ); + })} + + ); + } + + return ( + <> + {t('Find 0 result')} + + no result + + + ); +}; diff --git a/apps/web/src/components/affine/pivots/index.ts b/apps/web/src/components/affine/pivots/index.ts index eb887647ac..4f5691a9ed 100644 --- a/apps/web/src/components/affine/pivots/index.ts +++ b/apps/web/src/components/affine/pivots/index.ts @@ -1,5 +1,5 @@ export * from './hooks/usePivotData'; export * from './hooks/usePivotHandler'; -export * from './PivotRender'; +export * from './pivot-render/PivotRender'; export * from './PivotsMenu/PivotsMenu'; export * from './types'; diff --git a/apps/web/src/components/affine/pivots/pivot-render/OperationButton.tsx b/apps/web/src/components/affine/pivots/pivot-render/OperationButton.tsx new file mode 100644 index 0000000000..d013ffb0b1 --- /dev/null +++ b/apps/web/src/components/affine/pivots/pivot-render/OperationButton.tsx @@ -0,0 +1,161 @@ +import { MenuItem, MuiClickAwayListener, PureMenu } from '@affine/component'; +import { useTranslation } from '@affine/i18n'; +import { + MoreVerticalIcon, + MoveToIcon, + PenIcon, + PlusIcon, +} from '@blocksuite/icons'; +import type { PageMeta } from '@blocksuite/store'; +import { useTheme } from '@mui/material'; +import { useMemo, useState } from 'react'; + +import { usePageMetaHelper } from '../../../../hooks/use-page-meta'; +import type { BlockSuiteWorkspace } from '../../../../shared'; +import { toast } from '../../../../utils'; +import { CopyLink, MoveToTrash } from '../../operation-menu-items'; +import { PivotsMenu } from '../PivotsMenu/PivotsMenu'; +import { StyledOperationButton } from '../styles'; + +export type OperationButtonProps = { + onAdd: () => void; + onDelete: () => void; + metas: PageMeta[]; + currentMeta: PageMeta; + blockSuiteWorkspace: BlockSuiteWorkspace; + isHover: boolean; + onRename?: () => void; + onMenuClose?: () => void; +}; +export const OperationButton = ({ + onAdd, + onDelete, + metas, + currentMeta, + blockSuiteWorkspace, + isHover, + onMenuClose, + onRename, +}: OperationButtonProps) => { + const { + zIndex: { modal: modalIndex }, + } = useTheme(); + const { t } = useTranslation(); + + const [anchorEl, setAnchorEl] = useState(null); + const [operationMenuOpen, setOperationMenuOpen] = useState(false); + const [pivotsMenuOpen, setPivotsMenuOpen] = useState(false); + const [confirmModalOpen, setConfirmModalOpen] = useState(false); + const menuIndex = useMemo(() => modalIndex + 1, [modalIndex]); + const { setPageMeta } = usePageMetaHelper(blockSuiteWorkspace); + + return ( + { + setOperationMenuOpen(false); + setPivotsMenuOpen(false); + }} + > +
{ + e.stopPropagation(); + }} + onMouseLeave={() => { + setOperationMenuOpen(false); + setPivotsMenuOpen(false); + }} + > + setAnchorEl(ref)} + size="small" + onClick={() => { + setOperationMenuOpen(!operationMenuOpen); + }} + visible={isHover} + > + + + + + { + onAdd(); + setOperationMenuOpen(false); + onMenuClose?.(); + }} + icon={} + > + {t('Add a subpage inside')} + + { + setOperationMenuOpen(false); + setPivotsMenuOpen(true); + }} + icon={} + > + {t('Move to')} + + { + onRename?.(); + setOperationMenuOpen(false); + onMenuClose?.(); + }} + icon={} + > + {t('Rename')} + + { + setOperationMenuOpen(false); + setConfirmModalOpen(true); + onMenuClose?.(); + }} + /> + + + + + { + toast(t('Moved to Trash')); + setPageMeta(currentMeta.id, { + trash: true, + trashDate: +new Date(), + }); + onDelete(); + }} + onCancel={() => { + setConfirmModalOpen(false); + }} + confirmButtonTestId="move-to-trash-confirm" + cancelButtonTestId="move-to-trash-cancel" + /> +
+
+ ); +}; diff --git a/apps/web/src/components/affine/pivots/PivotRender.tsx b/apps/web/src/components/affine/pivots/pivot-render/PivotRender.tsx similarity index 59% rename from apps/web/src/components/affine/pivots/PivotRender.tsx rename to apps/web/src/components/affine/pivots/pivot-render/PivotRender.tsx index 8073ccdd03..2714fa2446 100644 --- a/apps/web/src/components/affine/pivots/PivotRender.tsx +++ b/apps/web/src/components/affine/pivots/pivot-render/PivotRender.tsx @@ -1,12 +1,14 @@ +import { Input } from '@affine/component'; import { ArrowDownSmallIcon, EdgelessIcon, PageIcon } from '@blocksuite/icons'; import { useAtomValue } from 'jotai'; import { useRouter } from 'next/router'; import { useState } from 'react'; -import { workspacePreferredModeAtom } from '../../../atoms'; +import { workspacePreferredModeAtom } from '../../../../atoms'; +import { usePageMetaHelper } from '../../../../hooks/use-page-meta'; +import { StyledCollapsedButton, StyledPivot } from '../styles'; +import type { TreeNode } from '../types'; import { OperationButton } from './OperationButton'; -import { StyledCollapsedButton, StyledPivot } from './styles'; -import type { TreeNode } from './types'; export const PivotRender: TreeNode['render'] = ( node, @@ -21,14 +23,18 @@ export const PivotRender: TreeNode['render'] = ( blockSuiteWorkspace, } = renderProps!; const record = useAtomValue(workspacePreferredModeAtom); + const { setPageTitle } = usePageMetaHelper(blockSuiteWorkspace); + const router = useRouter(); const [isHover, setIsHover] = useState(false); + const [showRename, setShowRename] = useState(false); const active = router.query.pageId === node.id; return ( { onClick?.(e, node); }} @@ -48,7 +54,25 @@ export const PivotRender: TreeNode['render'] = (
{record[node.id] === 'edgeless' ? : } - {currentMeta?.title || 'Untitled'} + {showRename ? ( + e.stopPropagation()} + height={32} + onBlur={() => { + setShowRename(false); + }} + onChange={value => { + // FIXME: setPageTitle would make input blur, and can't input the Chinese character + setPageTitle(node.id, value); + }} + /> + ) : ( + {currentMeta?.title || 'Untitled'} + )} + {showOperationButton && ( setIsHover(false)} + onRename={() => { + setShowRename(true); + setIsHover(false); + }} /> )}
diff --git a/apps/web/src/components/affine/pivots/styles.ts b/apps/web/src/components/affine/pivots/styles.ts index c29eaca885..caf9af3e6e 100644 --- a/apps/web/src/components/affine/pivots/styles.ts +++ b/apps/web/src/components/affine/pivots/styles.ts @@ -67,13 +67,15 @@ export const StyledPivot = styled('div')<{ }; }); -export const StyledOperationButton = styled(IconButton)<{ visible: boolean }>( - ({ visible }) => { - return { - visibility: visible ? 'visible' : 'hidden', - }; - } -); +export const StyledOperationButton = styled(IconButton, { + shouldForwardProp: prop => { + return !['visible'].includes(prop as string); + }, +})<{ visible: boolean }>(({ visible }) => { + return { + visibility: visible ? 'visible' : 'hidden', + }; +}); export const StyledSearchContainer = styled('div')(({ theme }) => { return { diff --git a/apps/web/src/components/blocksuite/header/header-right-items/EditorOptionMenu.tsx b/apps/web/src/components/blocksuite/header/header-right-items/EditorOptionMenu.tsx index 9f94b1682f..8b8a335639 100644 --- a/apps/web/src/components/blocksuite/header/header-right-items/EditorOptionMenu.tsx +++ b/apps/web/src/components/blocksuite/header/header-right-items/EditorOptionMenu.tsx @@ -11,6 +11,7 @@ import { import { assertExists } from '@blocksuite/store'; import { useTheme } from '@mui/material'; import { useAtom } from 'jotai'; +import { useState } from 'react'; import { workspacePreferredModeAtom } from '../../../../atoms'; import { useCurrentPageId } from '../../../../hooks/current/use-current-page-id'; @@ -45,6 +46,7 @@ export const EditorOptionMenu = () => { assertExists(pageMeta); const { favorite } = pageMeta; const { setPageMeta } = usePageMetaHelper(blockSuiteWorkspace); + const [openConfirm, setOpenConfirm] = useState(false); const EditMenu = ( <> @@ -87,8 +89,9 @@ export const EditorOptionMenu = () => { /> { + setOpenConfirm(true); + }} /> ); @@ -107,6 +110,20 @@ export const EditorOptionMenu = () => { + { + toast(t('Moved to Trash')); + setPageMeta(pageMeta.id, { + trash: true, + trashDate: +new Date(), + }); + }} + onCancel={() => { + setOpenConfirm(false); + }} + /> ); diff --git a/apps/web/src/components/pure/create-workspace-modal/index.tsx b/apps/web/src/components/pure/create-workspace-modal/index.tsx index 15d2eb6844..811275207e 100644 --- a/apps/web/src/components/pure/create-workspace-modal/index.tsx +++ b/apps/web/src/components/pure/create-workspace-modal/index.tsx @@ -1,7 +1,11 @@ -import { styled } from '@affine/component'; -import { Modal, ModalCloseButton, ModalWrapper } from '@affine/component'; -import { Button } from '@affine/component'; -import { Input } from '@affine/component'; +import { + Button, + Input, + Modal, + ModalCloseButton, + ModalWrapper, + styled, +} from '@affine/component'; import { useTranslation } from '@affine/i18n'; import type { KeyboardEvent } from 'react'; import { useCallback, useRef, useState } from 'react'; @@ -33,62 +37,60 @@ export const CreateWorkspaceModal = ({ ); const { t } = useTranslation(); return ( -
- - -
- { - onClose(); - }} - /> -
- - {t('New Workspace')} -

{t('Workspace description')}

- { - if (ref) { - setTimeout(() => ref.focus(), 0); - } - }} - data-testid="create-workspace-input" - onKeyDown={handleKeyDown} - placeholder={t('Set a Workspace name')} - maxLength={15} - minLength={0} - onChange={value => { - setWorkspaceName(value); - }} - onCompositionStart={() => { - isComposition.current = true; - }} - onCompositionEnd={() => { - isComposition.current = false; - }} - /> - -
-
-
-
+ + +
+ { + onClose(); + }} + /> +
+ + {t('New Workspace')} +

{t('Workspace description')}

+ { + if (ref) { + setTimeout(() => ref.focus(), 0); + } + }} + data-testid="create-workspace-input" + onKeyDown={handleKeyDown} + placeholder={t('Set a Workspace name')} + maxLength={15} + minLength={0} + onChange={value => { + setWorkspaceName(value); + }} + onCompositionStart={() => { + isComposition.current = true; + }} + onCompositionEnd={() => { + isComposition.current = false; + }} + /> + +
+
+
); }; diff --git a/apps/web/src/components/pure/quick-search-modal/NoResultSVG.tsx b/apps/web/src/components/pure/quick-search-modal/NoResultSVG.tsx deleted file mode 100644 index b25bb01ad6..0000000000 --- a/apps/web/src/components/pure/quick-search-modal/NoResultSVG.tsx +++ /dev/null @@ -1,74 +0,0 @@ -import { memo } from 'react'; - -export const NoResultSVG = memo(function NoResultSVG() { - return ( - - - - - - - - - - - - - - - - - - - - - - ); -}); diff --git a/apps/web/src/components/pure/quick-search-modal/PublishedResults.tsx b/apps/web/src/components/pure/quick-search-modal/PublishedResults.tsx index 5537e41706..c8bbc799d6 100644 --- a/apps/web/src/components/pure/quick-search-modal/PublishedResults.tsx +++ b/apps/web/src/components/pure/quick-search-modal/PublishedResults.tsx @@ -1,13 +1,13 @@ import { useTranslation } from '@affine/i18n'; import { EdgelessIcon, PageIcon } from '@blocksuite/icons'; import { Command } from 'cmdk'; +import Image from 'next/legacy/image'; import { useRouter } from 'next/router'; -import type React from 'react'; +import type { FC } from 'react'; import { useEffect, useMemo, useState } from 'react'; import { usePageMeta } from '../../../hooks/use-page-meta'; import type { BlockSuiteWorkspace } from '../../../shared'; -import { NoResultSVG } from './NoResultSVG'; import { StyledListItem, StyledNotFound } from './style'; export type PublishedResultsProps = { @@ -18,7 +18,7 @@ export type PublishedResultsProps = { blockSuiteWorkspace: BlockSuiteWorkspace; }; -export const PublishedResults: React.FC = ({ +export const PublishedResults: FC = ({ query, loading, onClose, @@ -86,7 +86,12 @@ export const PublishedResults: React.FC = ({ ) : ( {t('Find 0 result')} - + no result ) ) : ( diff --git a/apps/web/src/components/pure/quick-search-modal/Results.tsx b/apps/web/src/components/pure/quick-search-modal/Results.tsx index 53072314b8..22b0032bd1 100644 --- a/apps/web/src/components/pure/quick-search-modal/Results.tsx +++ b/apps/web/src/components/pure/quick-search-modal/Results.tsx @@ -3,9 +3,9 @@ import { useTranslation } from '@affine/i18n'; import { EdgelessIcon, PageIcon } from '@blocksuite/icons'; import { assertExists } from '@blocksuite/store'; import { Command } from 'cmdk'; +import Image from 'next/legacy/image'; import type { NextRouter } from 'next/router'; -import type { Dispatch, SetStateAction } from 'react'; -import type React from 'react'; +import type { Dispatch, FC, SetStateAction } from 'react'; import { useEffect } from 'react'; import { useRecentlyViewed } from '../../../hooks/affine/use-recent-views'; @@ -14,7 +14,6 @@ import { usePageMeta } from '../../../hooks/use-page-meta'; import { useRouterHelper } from '../../../hooks/use-router-helper'; import type { BlockSuiteWorkspace } from '../../../shared'; import { useSwitchToConfig } from './config'; -import { NoResultSVG } from './NoResultSVG'; import { StyledListItem, StyledNotFound } from './style'; export type ResultsProps = { @@ -24,7 +23,7 @@ export type ResultsProps = { setShowCreatePage: Dispatch>; router: NextRouter; }; -export const Results: React.FC = ({ +export const Results: FC = ({ query, blockSuiteWorkspace, setShowCreatePage, @@ -114,7 +113,12 @@ export const Results: React.FC = ({ return ( {t('Find 0 result')} - + no result ); } diff --git a/apps/web/src/components/pure/quick-search-modal/style.ts b/apps/web/src/components/pure/quick-search-modal/style.ts index 6255acea62..fe2e2ea28f 100644 --- a/apps/web/src/components/pure/quick-search-modal/style.ts +++ b/apps/web/src/components/pure/quick-search-modal/style.ts @@ -63,9 +63,8 @@ export const StyledNotFound = styled('div')(({ theme }) => { height: '36px', }, - '>svg': { + img: { marginTop: '10px', - height: '200px', }, }; }); @@ -162,7 +161,7 @@ export const StyledModalFooterContent = styled('button')(({ theme }) => { }, }; }); -export const StyledListItem = styled('button')(({ theme }) => { +export const StyledListItem = styled('button')(() => { return { // width: '612px', height: '32px', diff --git a/apps/web/src/components/pure/workspace-slider-bar/Pivots.tsx b/apps/web/src/components/pure/workspace-slider-bar/Pivots.tsx index 145d0e18e9..8bc2225015 100644 --- a/apps/web/src/components/pure/workspace-slider-bar/Pivots.tsx +++ b/apps/web/src/components/pure/workspace-slider-bar/Pivots.tsx @@ -5,7 +5,7 @@ import type { PageMeta } from '@blocksuite/store'; import type { MouseEvent } from 'react'; import { useCallback, useMemo, useState } from 'react'; -import type { AllWorkspace } from '../../../shared'; +import type { BlockSuiteWorkspace } from '../../../shared'; import type { TreeNode } from '../../affine/pivots'; import { PivotRender, @@ -16,11 +16,11 @@ import EmptyItem from './favorite/empty-item'; import { StyledCollapseButton, StyledListItem } from './shared-styles'; export const PivotInternal = ({ - currentWorkspace, + blockSuiteWorkspace, openPage, allMetas, }: { - currentWorkspace: AllWorkspace; + blockSuiteWorkspace: BlockSuiteWorkspace; openPage: (pageId: string) => void; allMetas: PageMeta[]; }) => { @@ -40,13 +40,13 @@ export const PivotInternal = ({ const { data } = usePivotData({ metas: allMetas.filter(meta => !meta.trash), pivotRender: PivotRender, - blockSuiteWorkspace: currentWorkspace.blockSuiteWorkspace, + blockSuiteWorkspace: blockSuiteWorkspace, onClick: handlePivotClick, showOperationButton: true, }); const { handleAdd, handleDelete, handleDrop } = usePivotHandler({ - blockSuiteWorkspace: currentWorkspace.blockSuiteWorkspace, + blockSuiteWorkspace: blockSuiteWorkspace, metas: allMetas, onAdd, @@ -63,58 +63,52 @@ export const PivotInternal = ({ ); }; -export const Pivots = ({ - currentWorkspace, - openPage, - allMetas, -}: { - currentWorkspace: AllWorkspace; +export type PivotsProps = { + blockSuiteWorkspace: BlockSuiteWorkspace; openPage: (pageId: string) => void; allMetas: PageMeta[]; -}) => { +}; + +export const Pivots = ({ + blockSuiteWorkspace, + openPage, + allMetas, +}: PivotsProps) => { const { t } = useTranslation(); const [showPivot, setShowPivot] = useState(true); - + const metas = useMemo(() => allMetas.filter(meta => !meta.trash), [allMetas]); const isPivotEmpty = useMemo( - () => allMetas.filter(meta => !meta.trash).length === 0, - [allMetas] + () => metas.filter(meta => meta.isPivots === true).length === 0, + [metas] ); return ( - <> - - { - setShowPivot(!showPivot); - }, [showPivot])} - collapse={showPivot} - > +
+ { + setShowPivot(!showPivot); + }, [showPivot])} + > + {t('Pivots')} - + {isPivotEmpty ? ( ) : ( )} - +
); }; export default Pivots; diff --git a/apps/web/src/components/pure/workspace-slider-bar/index.tsx b/apps/web/src/components/pure/workspace-slider-bar/index.tsx index 2b99c7f366..b1430c317f 100644 --- a/apps/web/src/components/pure/workspace-slider-bar/index.tsx +++ b/apps/web/src/components/pure/workspace-slider-bar/index.tsx @@ -9,7 +9,7 @@ import { } from '@blocksuite/icons'; import type { Page, PageMeta } from '@blocksuite/store'; import type React from 'react'; -import { useCallback, useEffect } from 'react'; +import { useCallback, useEffect, useState } from 'react'; import { useSidebarFloating, @@ -27,6 +27,7 @@ import { StyledListItem } from './shared-styles'; import { StyledLink, StyledNewPageButton, + StyledScrollWrapper, StyledSidebarSwitchWrapper, StyledSliderBar, StyledSliderBarInnerWrapper, @@ -71,9 +72,10 @@ export const WorkSpaceSliderBar: React.FC = ({ onOpenWorkspaceListModal, }) => { const currentWorkspaceId = currentWorkspace?.id || null; + const blockSuiteWorkspace = currentWorkspace?.blockSuiteWorkspace; const { t } = useTranslation(); const [sidebarOpen, setSidebarOpen] = useSidebarStatus(); - const pageMeta = usePageMeta(currentWorkspace?.blockSuiteWorkspace ?? null); + const pageMeta = usePageMeta(blockSuiteWorkspace ?? null); const onClickNewPage = useCallback(async () => { const page = await createPage(); openPage(page.id); @@ -81,6 +83,7 @@ export const WorkSpaceSliderBar: React.FC = ({ const floatingSlider = useSidebarFloating(); const [sliderWidth] = useSidebarWidth(); const [isResizing] = useSidebarResizing(); + const [isScrollAtTop, setIsScrollAtTop] = useState(true); const show = isPublicWorkspace ? false : sidebarOpen; const actualWidth = floatingSlider ? 'calc(10vw + 400px)' : sliderWidth; useEffect(() => { @@ -169,20 +172,29 @@ export const WorkSpaceSliderBar: React.FC = ({
- - {config.enableSubpage && !!currentWorkspace && ( - { + (e.target as HTMLDivElement).scrollTop === 0 + ? setIsScrollAtTop(true) + : setIsScrollAtTop(false); + }} + > + - )} + {!!blockSuiteWorkspace && ( + + )} + ( }; } ); +export const StyledSliderResizer = styled('div')<{ isResizing: boolean }>( + () => { + return { + position: 'absolute', + top: 0, + right: 0, + bottom: 0, + width: '12px', + transform: 'translateX(50%)', + cursor: 'col-resize', + zIndex: 1, + userSelect: 'none', + ':hover > *': { + background: 'rgba(0, 0, 0, 0.1)', + }, + }; + } +); +export const StyledSliderResizerInner = styled('div')<{ isResizing: boolean }>( + ({ isResizing }) => { + return { + transition: 'background .15s .1s', + position: 'absolute', + top: 0, + right: '50%', + bottom: 0, + transform: 'translateX(0.5px)', + width: '2px', + background: isResizing ? 'rgba(0, 0, 0, 0.1)' : 'transparent', + }; + } +); + +export const StyledScrollWrapper = styled('div')<{ + showTopBorder: boolean; +}>(({ showTopBorder, theme }) => { + return { + maxHeight: '360px', + overflowY: 'auto', + borderTop: '1px solid', + borderColor: showTopBorder ? theme.colors.borderColor : 'transparent', + }; +}); diff --git a/packages/component/src/ui/confirm/Confirm.tsx b/packages/component/src/ui/confirm/Confirm.tsx index 986ada55c0..553d0b18b3 100644 --- a/packages/component/src/ui/confirm/Confirm.tsx +++ b/packages/component/src/ui/confirm/Confirm.tsx @@ -10,6 +10,7 @@ import { StyledModalWrapper, StyledRowButtonWrapper, } from './styles'; + export type ConfirmProps = { title?: string; content?: string; @@ -20,6 +21,8 @@ export type ConfirmProps = { buttonDirection?: 'row' | 'column'; onConfirm?: () => void; onCancel?: () => void; + cancelButtonTestId?: string; + confirmButtonTestId?: string; } & Omit; export const Confirm = ({ @@ -32,10 +35,12 @@ export const Confirm = ({ buttonDirection = 'row', cancelText = 'Cancel', open, + cancelButtonTestId = '', + confirmButtonTestId = '', }: ConfirmProps) => { const { t } = useTranslation(); return ( - + { @@ -53,6 +58,7 @@ export const Confirm = ({ onCancel?.(); }} style={{ marginRight: '24px' }} + data-testid={cancelButtonTestId} > {cancelText === 'Cancel' ? t('Cancel') : cancelText} @@ -63,6 +69,7 @@ export const Confirm = ({ onClick={() => { onConfirm?.(); }} + data-testid={confirmButtonTestId} > {confirmText} @@ -77,6 +84,7 @@ export const Confirm = ({ onConfirm?.(); }} style={{ width: '284px', height: '38px', textAlign: 'center' }} + data-testid={confirmButtonTestId} > {confirmText} @@ -92,6 +100,7 @@ export const Confirm = ({ height: '38px', textAlign: 'center', }} + data-testid={cancelButtonTestId} > {cancelText === 'Cancel' ? t('Cancel') : cancelText} diff --git a/packages/component/src/ui/toast/toast.ts b/packages/component/src/ui/toast/toast.ts index ff8646cd5a..9f42cffc4b 100644 --- a/packages/component/src/ui/toast/toast.ts +++ b/packages/component/src/ui/toast/toast.ts @@ -103,9 +103,12 @@ export const toast = ( easing: 'cubic-bezier(0.21, 1.02, 0.73, 1)', fill: 'forwards' as const, }; // satisfies KeyframeAnimationOptions; - element.animate(fadeIn, options); + // FIXME: Vitest not support element.animate, + // can try it in `apps/web/src/components/__tests__/PinBoard.spec.tsx` `delete pivot` + typeof element.animate === 'function' && element.animate(fadeIn, options); setTimeout(async () => { + if (typeof element.animate !== 'function') return; const fadeOut = fadeIn.reverse(); const animation = element.animate(fadeOut, options); await animation.finished; diff --git a/tests/parallels/pin-board.spec.ts b/tests/parallels/pin-board.spec.ts new file mode 100644 index 0000000000..7eaf41f33c --- /dev/null +++ b/tests/parallels/pin-board.spec.ts @@ -0,0 +1,85 @@ +import type { Page } from '@playwright/test'; +import { expect } from '@playwright/test'; + +import { openHomePage } from '../libs/load-page'; +import { clickPageMoreActions, newPage } from '../libs/page-logic'; +import { test } from '../libs/playwright'; + +async function createRootPage(page: Page, title: string) { + await newPage(page); + await page.focus('.affine-default-page-block-title'); + await page.type('.affine-default-page-block-title', title, { + delay: 100, + }); + await clickPageMoreActions(page); + await page.getByTestId('move-to-menu-item').click(); + await page.getByTestId('root-pivot-button-in-pivots-menu').click(); +} +async function createPivotPage(page: Page, title: string, parentTitle: string) { + await newPage(page); + await page.focus('.affine-default-page-block-title'); + await page.type('.affine-default-page-block-title', title, { + delay: 100, + }); + await clickPageMoreActions(page); + await page.getByTestId('move-to-menu-item').click(); + await page.getByTestId('pivots-menu').getByText(parentTitle).click(); +} +export async function initPinBoard(page: Page) { + await openHomePage(page); + await createRootPage(page, 'parent1'); + await createRootPage(page, 'parent2'); + await createPivotPage(page, 'child1', 'parent1'); + await createPivotPage(page, 'child2', 'parent1'); +} +test.describe('PinBoard interaction', () => { + test('Add pivot', async ({ page }) => { + await initPinBoard(page); + }); + test('Remove pivots', async ({ page }) => { + await initPinBoard(page); + + const child2Meta = await page.evaluate(() => { + return globalThis.currentWorkspace.blockSuiteWorkspace.meta.pageMetas.find( + m => m.title === 'child2' + ); + }); + + const child2 = await page + .getByTestId('sidebar-pivots-container') + .getByTestId(`pivot-${child2Meta?.id}`); + await child2.hover(); + await child2.getByTestId('pivot-operation-button').click(); + await page.getByTestId('pivot-operation-move-to').click(); + await page.getByTestId('remove-from-pivots-button').click(); + await page.waitForTimeout(1000); + expect( + await page.locator(`[data-testid="pivot-${child2Meta.id}"]`).count() + ).toEqual(0); + }); + + test('search pivot', async ({ page }) => { + await initPinBoard(page); + + const child2Meta = await page.evaluate(() => { + return globalThis.currentWorkspace.blockSuiteWorkspace.meta.pageMetas.find( + m => m.title === 'child2' + ); + }); + + const child2 = await page + .getByTestId('sidebar-pivots-container') + .getByTestId(`pivot-${child2Meta?.id}`); + await child2.hover(); + await child2.getByTestId('pivot-operation-button').click(); + await page.getByTestId('pivot-operation-move-to').click(); + + await page.fill('[data-testid="pivots-menu-search"]', '111'); + expect(await page.locator('[alt="no result"]').count()).toEqual(1); + + await page.fill('[data-testid="pivots-menu-search"]', 'parent2'); + expect( + await page.locator('[data-testid="pivot-search-result"]').count() + ).toEqual(1); + }); +});