mirror of
https://github.com/toeverything/AFFiNE.git
synced 2024-12-21 10:01:43 +03:00
feat(mobile): replace search with create in app tab (#8934)
- Remove search on AppTab, replaced with `create doc` - Always show AppTab for editor page - Extract `NavigationBack` from `PageHeader`
This commit is contained in:
parent
5e92d6cfe9
commit
6a71b28a61
@ -0,0 +1,51 @@
|
|||||||
|
import { usePageHelper } from '@affine/core/components/blocksuite/block-suite-page-list/utils';
|
||||||
|
import { JournalService } from '@affine/core/modules/journal';
|
||||||
|
import { WorkbenchService } from '@affine/core/modules/workbench';
|
||||||
|
import track from '@affine/track';
|
||||||
|
import { EditIcon } from '@blocksuite/icons/rc';
|
||||||
|
import {
|
||||||
|
DocsService,
|
||||||
|
useLiveData,
|
||||||
|
useServices,
|
||||||
|
WorkspaceService,
|
||||||
|
} from '@toeverything/infra';
|
||||||
|
import { useCallback } from 'react';
|
||||||
|
|
||||||
|
import { tabItem } from './styles.css';
|
||||||
|
|
||||||
|
export const AppTabCreate = () => {
|
||||||
|
const { docsService, workbenchService, workspaceService, journalService } =
|
||||||
|
useServices({
|
||||||
|
DocsService,
|
||||||
|
WorkbenchService,
|
||||||
|
WorkspaceService,
|
||||||
|
JournalService,
|
||||||
|
});
|
||||||
|
const workbench = workbenchService.workbench;
|
||||||
|
const currentWorkspace = workspaceService.workspace;
|
||||||
|
const location = useLiveData(workbench.location$);
|
||||||
|
const pageHelper = usePageHelper(currentWorkspace.docCollection);
|
||||||
|
|
||||||
|
const maybeDocId = location.pathname.split('/')[1].split('?')[0];
|
||||||
|
const doc = useLiveData(docsService.list.doc$(maybeDocId));
|
||||||
|
const journalDate = useLiveData(journalService.journalDate$(maybeDocId));
|
||||||
|
const isActive = !!doc && !journalDate;
|
||||||
|
|
||||||
|
const createPage = useCallback(() => {
|
||||||
|
if (isActive) return;
|
||||||
|
pageHelper.createPage(undefined, true);
|
||||||
|
track.$.navigationPanel.$.createDoc();
|
||||||
|
}, [isActive, pageHelper]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={tabItem}
|
||||||
|
data-active={isActive}
|
||||||
|
role="tab"
|
||||||
|
aria-label="New Page"
|
||||||
|
onClick={createPage}
|
||||||
|
>
|
||||||
|
<EditIcon />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
@ -3,13 +3,14 @@ import {
|
|||||||
WorkbenchLink,
|
WorkbenchLink,
|
||||||
WorkbenchService,
|
WorkbenchService,
|
||||||
} from '@affine/core/modules/workbench';
|
} from '@affine/core/modules/workbench';
|
||||||
import { AllDocsIcon, MobileHomeIcon, SearchIcon } from '@blocksuite/icons/rc';
|
import { AllDocsIcon, MobileHomeIcon } from '@blocksuite/icons/rc';
|
||||||
import { useLiveData, useService } from '@toeverything/infra';
|
import { useLiveData, useService } from '@toeverything/infra';
|
||||||
import { assignInlineVars } from '@vanilla-extract/dynamic';
|
import { assignInlineVars } from '@vanilla-extract/dynamic';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import type { Location } from 'react-router-dom';
|
import type { Location } from 'react-router-dom';
|
||||||
|
|
||||||
import { VirtualKeyboardService } from '../../modules/virtual-keyboard/services/virtual-keyboard';
|
import { VirtualKeyboardService } from '../../modules/virtual-keyboard/services/virtual-keyboard';
|
||||||
|
import { AppTabCreate } from './create';
|
||||||
import { AppTabJournal } from './journal';
|
import { AppTabJournal } from './journal';
|
||||||
import * as styles from './styles.css';
|
import * as styles from './styles.css';
|
||||||
|
|
||||||
@ -48,9 +49,8 @@ const routes: Route[] = [
|
|||||||
node: <AppTabJournal />,
|
node: <AppTabJournal />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'search',
|
key: 'new',
|
||||||
to: '/search',
|
node: <AppTabCreate />,
|
||||||
Icon: SearchIcon,
|
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
export * from './app-tabs';
|
export * from './app-tabs';
|
||||||
export * from './doc-card';
|
export * from './doc-card';
|
||||||
|
export * from './navigation-back';
|
||||||
export * from './page-header';
|
export * from './page-header';
|
||||||
export * from './rename';
|
export * from './rename';
|
||||||
export * from './search-input';
|
export * from './search-input';
|
||||||
|
@ -0,0 +1,54 @@
|
|||||||
|
import {
|
||||||
|
IconButton,
|
||||||
|
type IconButtonProps,
|
||||||
|
useIsInsideModal,
|
||||||
|
} from '@affine/component';
|
||||||
|
import { ArrowLeftSmallIcon, CloseIcon } from '@blocksuite/icons/rc';
|
||||||
|
import { useService } from '@toeverything/infra';
|
||||||
|
import { useCallback, useEffect, useMemo } from 'react';
|
||||||
|
|
||||||
|
import { NavigationGestureService } from '../../modules/navigation-gesture';
|
||||||
|
|
||||||
|
export interface NavigationBackButtonProps extends IconButtonProps {
|
||||||
|
backAction?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A button to control the back behavior of the mobile app, as well as manage navigation gesture
|
||||||
|
*/
|
||||||
|
export const NavigationBackButton = ({
|
||||||
|
backAction,
|
||||||
|
style: propsStyle,
|
||||||
|
...otherProps
|
||||||
|
}: NavigationBackButtonProps) => {
|
||||||
|
const navigationGesture = useService(NavigationGestureService);
|
||||||
|
const isInsideModal = useIsInsideModal();
|
||||||
|
|
||||||
|
const handleRouteBack = useCallback(() => {
|
||||||
|
backAction ? backAction() : history.back();
|
||||||
|
}, [backAction]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isInsideModal) return;
|
||||||
|
|
||||||
|
const prev = navigationGesture.enabled$.value;
|
||||||
|
navigationGesture.setEnabled(true);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
navigationGesture.setEnabled(prev);
|
||||||
|
};
|
||||||
|
}, [isInsideModal, navigationGesture]);
|
||||||
|
|
||||||
|
const style = useMemo(() => ({ padding: 10, ...propsStyle }), [propsStyle]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<IconButton
|
||||||
|
size={24}
|
||||||
|
style={style}
|
||||||
|
onClick={handleRouteBack}
|
||||||
|
icon={isInsideModal ? <CloseIcon /> : <ArrowLeftSmallIcon />}
|
||||||
|
data-testid="page-header-back"
|
||||||
|
{...otherProps}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
@ -1,16 +1,8 @@
|
|||||||
import { IconButton, SafeArea, useIsInsideModal } from '@affine/component';
|
import { SafeArea } from '@affine/component';
|
||||||
import { ArrowLeftSmallIcon, CloseIcon } from '@blocksuite/icons/rc';
|
|
||||||
import { useService } from '@toeverything/infra';
|
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import {
|
import { forwardRef, type HtmlHTMLAttributes, type ReactNode } from 'react';
|
||||||
forwardRef,
|
|
||||||
type HtmlHTMLAttributes,
|
|
||||||
type ReactNode,
|
|
||||||
useCallback,
|
|
||||||
useEffect,
|
|
||||||
} from 'react';
|
|
||||||
|
|
||||||
import { NavigationGestureService } from '../../modules/navigation-gesture';
|
import { NavigationBackButton } from '../navigation-back';
|
||||||
import * as styles from './styles.css';
|
import * as styles from './styles.css';
|
||||||
|
|
||||||
export interface PageHeaderProps
|
export interface PageHeaderProps
|
||||||
@ -74,24 +66,6 @@ export const PageHeader = forwardRef<HTMLDivElement, PageHeaderProps>(
|
|||||||
},
|
},
|
||||||
ref
|
ref
|
||||||
) {
|
) {
|
||||||
const navigationGesture = useService(NavigationGestureService);
|
|
||||||
const isInsideModal = useIsInsideModal();
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (isInsideModal) return;
|
|
||||||
|
|
||||||
const prev = navigationGesture.enabled$.value;
|
|
||||||
navigationGesture.setEnabled(!!back);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
navigationGesture.setEnabled(prev);
|
|
||||||
};
|
|
||||||
}, [back, isInsideModal, navigationGesture]);
|
|
||||||
|
|
||||||
const handleRouteBack = useCallback(() => {
|
|
||||||
backAction ? backAction() : history.back();
|
|
||||||
}, [backAction]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<SafeArea
|
<SafeArea
|
||||||
@ -106,15 +80,7 @@ export const PageHeader = forwardRef<HTMLDivElement, PageHeaderProps>(
|
|||||||
className={clsx(styles.prefix, prefixClassName)}
|
className={clsx(styles.prefix, prefixClassName)}
|
||||||
style={prefixStyle}
|
style={prefixStyle}
|
||||||
>
|
>
|
||||||
{back ? (
|
{back ? <NavigationBackButton backAction={backAction} /> : null}
|
||||||
<IconButton
|
|
||||||
size={24}
|
|
||||||
style={{ padding: 10 }}
|
|
||||||
onClick={handleRouteBack}
|
|
||||||
icon={isInsideModal ? <CloseIcon /> : <ArrowLeftSmallIcon />}
|
|
||||||
data-testid="page-header-back"
|
|
||||||
/>
|
|
||||||
) : null}
|
|
||||||
{prefix}
|
{prefix}
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
@ -169,7 +169,7 @@ const DetailPageImpl = () => {
|
|||||||
|
|
||||||
editor.bindEditorContainer(
|
editor.bindEditorContainer(
|
||||||
editorContainer,
|
editorContainer,
|
||||||
null,
|
(editorContainer as any).docTitle, // set from proxy
|
||||||
scrollViewportRef.current
|
scrollViewportRef.current
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -272,9 +272,7 @@ const MobileDetailPage = ({
|
|||||||
) : null}
|
) : null}
|
||||||
</PageHeader>
|
</PageHeader>
|
||||||
<DetailPageImpl />
|
<DetailPageImpl />
|
||||||
{date ? (
|
<AppTabs background={cssVarV2('layer/background/primary')} />
|
||||||
<AppTabs background={cssVarV2('layer/background/primary')} />
|
|
||||||
) : null}
|
|
||||||
</DetailPageWrapper>
|
</DetailPageWrapper>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -1,10 +1,15 @@
|
|||||||
import { SafeArea, useThemeColorV2 } from '@affine/component';
|
import {
|
||||||
|
SafeArea,
|
||||||
|
startScopedViewTransition,
|
||||||
|
useThemeColorV2,
|
||||||
|
} from '@affine/component';
|
||||||
import { CollectionService } from '@affine/core/modules/collection';
|
import { CollectionService } from '@affine/core/modules/collection';
|
||||||
import {
|
import {
|
||||||
type QuickSearchItem,
|
type QuickSearchItem,
|
||||||
QuickSearchTagIcon,
|
QuickSearchTagIcon,
|
||||||
} from '@affine/core/modules/quicksearch';
|
} from '@affine/core/modules/quicksearch';
|
||||||
import { TagService } from '@affine/core/modules/tag';
|
import { TagService } from '@affine/core/modules/tag';
|
||||||
|
import { sleep } from '@blocksuite/affine/global/utils';
|
||||||
import { ViewLayersIcon } from '@blocksuite/icons/rc';
|
import { ViewLayersIcon } from '@blocksuite/icons/rc';
|
||||||
import {
|
import {
|
||||||
LiveData,
|
LiveData,
|
||||||
@ -14,7 +19,12 @@ import {
|
|||||||
} from '@toeverything/infra';
|
} from '@toeverything/infra';
|
||||||
import { useCallback, useMemo } from 'react';
|
import { useCallback, useMemo } from 'react';
|
||||||
|
|
||||||
import { AppTabs, SearchInput, SearchResLabel } from '../../components';
|
import {
|
||||||
|
NavigationBackButton,
|
||||||
|
SearchInput,
|
||||||
|
SearchResLabel,
|
||||||
|
} from '../../components';
|
||||||
|
import { searchVTScope } from '../../components/search-input/style.css';
|
||||||
import { MobileSearchService } from '../../modules/search';
|
import { MobileSearchService } from '../../modules/search';
|
||||||
import { SearchResults } from '../../views/search/search-results';
|
import { SearchResults } from '../../views/search/search-results';
|
||||||
import * as styles from '../../views/search/style.css';
|
import * as styles from '../../views/search/style.css';
|
||||||
@ -133,11 +143,20 @@ export const Component = () => {
|
|||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const transitionBack = useCallback(() => {
|
||||||
|
startScopedViewTransition(searchVTScope, async () => {
|
||||||
|
history.back();
|
||||||
|
await sleep(10);
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<SafeArea top>
|
<SafeArea top>
|
||||||
<div className={styles.searchHeader} data-testid="search-header">
|
<div className={styles.searchHeader} data-testid="search-header">
|
||||||
|
<NavigationBackButton backAction={transitionBack} />
|
||||||
<SearchInput
|
<SearchInput
|
||||||
|
className={styles.searchInput}
|
||||||
debounce={300}
|
debounce={300}
|
||||||
autoFocus={!searchInput}
|
autoFocus={!searchInput}
|
||||||
value={searchInput}
|
value={searchInput}
|
||||||
@ -147,7 +166,6 @@ export const Component = () => {
|
|||||||
</div>
|
</div>
|
||||||
</SafeArea>
|
</SafeArea>
|
||||||
{searchInput ? <WithQueryList /> : <RecentList />}
|
{searchInput ? <WithQueryList /> : <RecentList />}
|
||||||
<AppTabs />
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -4,6 +4,14 @@ import { style } from '@vanilla-extract/css';
|
|||||||
|
|
||||||
export const searchHeader = style({
|
export const searchHeader = style({
|
||||||
padding: 16,
|
padding: 16,
|
||||||
|
paddingLeft: 8,
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: 4,
|
||||||
|
});
|
||||||
|
export const searchInput = style({
|
||||||
|
width: 0,
|
||||||
|
flex: 1,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const resTitle = style([
|
export const resTitle = style([
|
||||||
|
@ -229,7 +229,15 @@ export class Editor extends Entity {
|
|||||||
const title = docTitle?.querySelector<
|
const title = docTitle?.querySelector<
|
||||||
HTMLElement & { inlineEditor: InlineEditor | null }
|
HTMLElement & { inlineEditor: InlineEditor | null }
|
||||||
>('rich-text');
|
>('rich-text');
|
||||||
title?.inlineEditor?.focusEnd();
|
// Only focus on the title when it's empty on mobile edition.
|
||||||
|
if (BUILD_CONFIG.isMobileEdition) {
|
||||||
|
const titleText = this.doc.title$.value;
|
||||||
|
if (!titleText?.length) {
|
||||||
|
title?.inlineEditor?.focusEnd();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
title?.inlineEditor?.focusEnd();
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
const selection = editorContainer.host?.std.selection;
|
const selection = editorContainer.host?.std.selection;
|
||||||
|
|
||||||
|
@ -13,7 +13,8 @@ test('app tabs is visible', async ({ page }) => {
|
|||||||
|
|
||||||
await expect(tabs.getByRole('tab', { name: 'home' })).toBeVisible();
|
await expect(tabs.getByRole('tab', { name: 'home' })).toBeVisible();
|
||||||
await expect(tabs.getByRole('tab', { name: 'all' })).toBeVisible();
|
await expect(tabs.getByRole('tab', { name: 'all' })).toBeVisible();
|
||||||
await expect(tabs.getByRole('tab', { name: 'search' })).toBeVisible();
|
await expect(tabs.getByRole('tab', { name: 'journal' })).toBeVisible();
|
||||||
|
await expect(tabs.getByRole('tab', { name: 'new' })).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('recent docs', async ({ page }) => {
|
test('recent docs', async ({ page }) => {
|
||||||
@ -49,15 +50,3 @@ test('all tab', async ({ page }) => {
|
|||||||
const todayDocs = page.getByTestId('doc-card');
|
const todayDocs = page.getByTestId('doc-card');
|
||||||
expect(await todayDocs.count()).toBeGreaterThan(0);
|
expect(await todayDocs.count()).toBeGreaterThan(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('search tab', async ({ page }) => {
|
|
||||||
const searchTab = page
|
|
||||||
.locator('#app-tabs')
|
|
||||||
.getByRole('tab', { name: 'search' });
|
|
||||||
await expect(searchTab).toBeVisible();
|
|
||||||
|
|
||||||
await searchTab.click();
|
|
||||||
|
|
||||||
const searchInput = page.getByTestId('search-header').getByRole('textbox');
|
|
||||||
await expect(searchInput).toBeVisible();
|
|
||||||
});
|
|
||||||
|
Loading…
Reference in New Issue
Block a user