feat(mobile): add delete operation for detail page menu (#12900)

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

- **New Features**
- Added a "Move to Trash" option in the page header menu, allowing users
to move documents to trash with confirmation and permission checks.

- **Refactor**
- Centralized and reorganized tab-related type definitions for improved
maintainability.
  - Updated tab components to use shared constants and types.

- **Style**
- Updated menu item styling for the new trash action to indicate a
destructive operation.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
Cats Juice 2025-06-23 18:33:24 +08:00 committed by GitHub
parent 934e377054
commit 24b205ae83
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 90 additions and 40 deletions

View File

@ -0,0 +1 @@
export const cacheKey = 'activeAppTabId';

View File

@ -8,8 +8,8 @@ import track from '@affine/track';
import { EditIcon } from '@blocksuite/icons/rc';
import { useLiveData, useService } from '@toeverything/infra';
import type { AppTabCustomFCProps } from './data';
import { TabItem } from './tab-item';
import type { AppTabCustomFCProps } from './type';
export const AppTabCreate = ({ tab }: AppTabCustomFCProps) => {
const workbench = useService(WorkbenchService).workbench;

View File

@ -1,28 +1,8 @@
import { AllDocsIcon, HomeIcon } from '@blocksuite/icons/rc';
import type { Framework } from '@toeverything/infra';
import { AppTabCreate } from './create';
import { AppTabJournal } from './journal';
interface AppTabBase {
key: string;
onClick?: (framework: Framework, isActive: boolean) => void;
}
export interface AppTabLink extends AppTabBase {
Icon: React.FC;
to: string;
LinkComponent?: React.FC;
}
export interface AppTabCustom extends AppTabBase {
custom: (props: AppTabCustomFCProps) => React.ReactNode;
}
export type Tab = AppTabLink | AppTabCustom;
export interface AppTabCustomFCProps {
tab: Tab;
}
import type { Tab } from './type';
export const tabs: Tab[] = [
{

View File

@ -1,14 +1,20 @@
import { SafeArea } from '@affine/component';
import { WorkbenchLink } from '@affine/core/modules/workbench';
import { GlobalCacheService } from '@affine/core/modules/storage';
import {
WorkbenchLink,
WorkbenchService,
} from '@affine/core/modules/workbench';
import { useLiveData, useService } from '@toeverything/infra';
import { assignInlineVars } from '@vanilla-extract/dynamic';
import React from 'react';
import React, { useEffect } from 'react';
import { createPortal } from 'react-dom';
import { VirtualKeyboardService } from '../../modules/virtual-keyboard/services/virtual-keyboard';
import { type AppTabLink, tabs } from './data';
import { cacheKey } from './constants';
import { tabs } from './data';
import * as styles from './styles.css';
import { TabItem } from './tab-item';
import type { AppTabLink } from './type';
export const AppTabs = ({
background,
@ -19,6 +25,16 @@ export const AppTabs = ({
}) => {
const virtualKeyboardService = useService(VirtualKeyboardService);
const virtualKeyboardVisible = useLiveData(virtualKeyboardService.visible$);
const workbench = useService(WorkbenchService).workbench;
const location = useLiveData(workbench.location$);
const globalCache = useService(GlobalCacheService).globalCache;
// always set the active tab to home when the location is changed to home
useEffect(() => {
if (location.pathname === '/home') {
globalCache.set(cacheKey, 'home');
}
}, [globalCache, location.pathname]);
const tab = (
<SafeArea

View File

@ -5,8 +5,8 @@ import { TodayIcon } from '@blocksuite/icons/rc';
import { useLiveData, useService } from '@toeverything/infra';
import { useCallback } from 'react';
import type { AppTabCustomFCProps } from './data';
import { TabItem } from './tab-item';
import type { AppTabCustomFCProps } from './type';
export const AppTabJournal = ({ tab }: AppTabCustomFCProps) => {
const workbench = useService(WorkbenchService).workbench;

View File

@ -1,7 +1,8 @@
import { GlobalCacheService } from '@affine/core/modules/storage';
import { LiveData, useLiveData, useService } from '@toeverything/infra';
import { type PropsWithChildren, useCallback, useEffect, useMemo } from 'react';
import { type PropsWithChildren, useCallback, useMemo } from 'react';
import { cacheKey } from './constants';
import { tabItem } from './styles.css';
export interface TabItemProps extends PropsWithChildren {
@ -10,8 +11,6 @@ export interface TabItemProps extends PropsWithChildren {
onClick?: (isActive: boolean) => void;
}
const cacheKey = 'activeAppTabId';
let isInitialized = false;
export const TabItem = ({ id, label, children, onClick }: TabItemProps) => {
const globalCache = useService(GlobalCacheService).globalCache;
const activeTabId$ = useMemo(
@ -27,14 +26,6 @@ export const TabItem = ({ id, label, children, onClick }: TabItemProps) => {
onClick?.(isActive);
}, [globalCache, id, isActive, onClick]);
useEffect(() => {
if (isInitialized) return;
isInitialized = true;
if (BUILD_CONFIG.isIOS || BUILD_CONFIG.isAndroid) {
globalCache.set(cacheKey, 'home');
}
}, [globalCache]);
return (
<li
className={tabItem}

View File

@ -0,0 +1,21 @@
import type { Framework } from '@toeverything/infra';
interface AppTabBase {
key: string;
onClick?: (framework: Framework, isActive: boolean) => void;
}
export interface AppTabLink extends AppTabBase {
Icon: React.FC;
to: string;
LinkComponent?: React.FC;
}
export interface AppTabCustom extends AppTabBase {
custom: (props: AppTabCustomFCProps) => React.ReactNode;
}
export type Tab = AppTabLink | AppTabCustom;
export interface AppTabCustomFCProps {
tab: Tab;
}

View File

@ -1,4 +1,4 @@
import { IconButton, notify } from '@affine/component';
import { IconButton, notify, toast, useConfirmModal } from '@affine/component';
import {
MenuSeparator,
MenuSub,
@ -6,7 +6,7 @@ import {
MobileMenuItem,
} from '@affine/component/ui/menu';
import { useFavorite } from '@affine/core/blocksuite/block-suite-header/favorite';
import { useGuard } from '@affine/core/components/guard';
import { Guard, useGuard } from '@affine/core/components/guard';
import { IsFavoriteIcon } from '@affine/core/components/pure/icons';
import { DocInfoSheet } from '@affine/core/mobile/components';
import { MobileTocMenu } from '@affine/core/mobile/components/toc-menu';
@ -17,6 +17,7 @@ import { preventDefault } from '@affine/core/utils';
import { useI18n } from '@affine/i18n';
import { track } from '@affine/track';
import {
DeleteIcon,
EdgelessIcon,
InformationIcon,
MoreHorizontalIcon,
@ -34,7 +35,8 @@ import * as styles from './page-header-more-button.css';
export const PageHeaderMenuButton = () => {
const t = useI18n();
const docId = useService(DocService).doc.id;
const doc = useService(DocService).doc;
const docId = doc?.id;
const canEdit = useGuard('Doc_Update', docId);
const editorService = useService(EditorService);
@ -50,6 +52,7 @@ export const PageHeaderMenuButton = () => {
const title = useLiveData(editorService.editor.doc.title$);
const { favorite, toggleFavorite } = useFavorite(docId);
const { openConfirmModal } = useConfirmModal();
const handleSwitchMode = useCallback(() => {
const mode = primaryMode === 'page' ? 'edgeless' : 'page';
@ -88,6 +91,32 @@ export const PageHeaderMenuButton = () => {
toggleFavorite();
}, [toggleFavorite]);
const handleMoveToTrash = useCallback(() => {
if (!doc) {
return;
}
openConfirmModal({
title: t['com.affine.moveToTrash.title'](),
description: t['com.affine.moveToTrash.confirmModal.description']({
title: doc.title$.value,
}),
confirmText: t['com.affine.moveToTrash.confirmModal.confirm'](),
cancelText: t['com.affine.moveToTrash.confirmModal.cancel'](),
confirmButtonOptions: {
variant: 'error',
},
onConfirm() {
doc.moveToTrash();
track.$.navigationPanel.docs.deleteDoc({
control: 'button',
});
toast(t['com.affine.toastMessage.movedTrash']());
// navigate back
history.back();
},
});
}, [doc, openConfirmModal, t]);
const EditMenu = (
<>
<EditorModeSwitch />
@ -135,6 +164,18 @@ export const PageHeaderMenuButton = () => {
</MobileMenuItem>
</MobileMenu>
<JournalConflictsMenuItem />
<Guard docId={docId} permission="Doc_Trash">
{canMoveToTrash => (
<MobileMenuItem
prefixIcon={<DeleteIcon />}
type="danger"
disabled={!canMoveToTrash}
onSelect={handleMoveToTrash}
>
{t['com.affine.moveToTrash.title']()}
</MobileMenuItem>
)}
</Guard>
</>
);
if (isInTrash) {