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:
@@ -0,0 +1 @@
|
|||||||
|
export const cacheKey = 'activeAppTabId';
|
||||||
@@ -8,8 +8,8 @@ import track from '@affine/track';
|
|||||||
import { EditIcon } from '@blocksuite/icons/rc';
|
import { EditIcon } from '@blocksuite/icons/rc';
|
||||||
import { useLiveData, useService } from '@toeverything/infra';
|
import { useLiveData, useService } from '@toeverything/infra';
|
||||||
|
|
||||||
import type { AppTabCustomFCProps } from './data';
|
|
||||||
import { TabItem } from './tab-item';
|
import { TabItem } from './tab-item';
|
||||||
|
import type { AppTabCustomFCProps } from './type';
|
||||||
|
|
||||||
export const AppTabCreate = ({ tab }: AppTabCustomFCProps) => {
|
export const AppTabCreate = ({ tab }: AppTabCustomFCProps) => {
|
||||||
const workbench = useService(WorkbenchService).workbench;
|
const workbench = useService(WorkbenchService).workbench;
|
||||||
|
|||||||
@@ -1,28 +1,8 @@
|
|||||||
import { AllDocsIcon, HomeIcon } from '@blocksuite/icons/rc';
|
import { AllDocsIcon, HomeIcon } from '@blocksuite/icons/rc';
|
||||||
import type { Framework } from '@toeverything/infra';
|
|
||||||
|
|
||||||
import { AppTabCreate } from './create';
|
import { AppTabCreate } from './create';
|
||||||
import { AppTabJournal } from './journal';
|
import { AppTabJournal } from './journal';
|
||||||
|
import type { Tab } from './type';
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const tabs: Tab[] = [
|
export const tabs: Tab[] = [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,14 +1,20 @@
|
|||||||
import { SafeArea } from '@affine/component';
|
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 { useLiveData, useService } from '@toeverything/infra';
|
||||||
import { assignInlineVars } from '@vanilla-extract/dynamic';
|
import { assignInlineVars } from '@vanilla-extract/dynamic';
|
||||||
import React from 'react';
|
import React, { useEffect } from 'react';
|
||||||
import { createPortal } from 'react-dom';
|
import { createPortal } from 'react-dom';
|
||||||
|
|
||||||
import { VirtualKeyboardService } from '../../modules/virtual-keyboard/services/virtual-keyboard';
|
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 * as styles from './styles.css';
|
||||||
import { TabItem } from './tab-item';
|
import { TabItem } from './tab-item';
|
||||||
|
import type { AppTabLink } from './type';
|
||||||
|
|
||||||
export const AppTabs = ({
|
export const AppTabs = ({
|
||||||
background,
|
background,
|
||||||
@@ -19,6 +25,16 @@ export const AppTabs = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const virtualKeyboardService = useService(VirtualKeyboardService);
|
const virtualKeyboardService = useService(VirtualKeyboardService);
|
||||||
const virtualKeyboardVisible = useLiveData(virtualKeyboardService.visible$);
|
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 = (
|
const tab = (
|
||||||
<SafeArea
|
<SafeArea
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ import { TodayIcon } from '@blocksuite/icons/rc';
|
|||||||
import { useLiveData, useService } from '@toeverything/infra';
|
import { useLiveData, useService } from '@toeverything/infra';
|
||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
|
|
||||||
import type { AppTabCustomFCProps } from './data';
|
|
||||||
import { TabItem } from './tab-item';
|
import { TabItem } from './tab-item';
|
||||||
|
import type { AppTabCustomFCProps } from './type';
|
||||||
|
|
||||||
export const AppTabJournal = ({ tab }: AppTabCustomFCProps) => {
|
export const AppTabJournal = ({ tab }: AppTabCustomFCProps) => {
|
||||||
const workbench = useService(WorkbenchService).workbench;
|
const workbench = useService(WorkbenchService).workbench;
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { GlobalCacheService } from '@affine/core/modules/storage';
|
import { GlobalCacheService } from '@affine/core/modules/storage';
|
||||||
import { LiveData, useLiveData, useService } from '@toeverything/infra';
|
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';
|
import { tabItem } from './styles.css';
|
||||||
|
|
||||||
export interface TabItemProps extends PropsWithChildren {
|
export interface TabItemProps extends PropsWithChildren {
|
||||||
@@ -10,8 +11,6 @@ export interface TabItemProps extends PropsWithChildren {
|
|||||||
onClick?: (isActive: boolean) => void;
|
onClick?: (isActive: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const cacheKey = 'activeAppTabId';
|
|
||||||
let isInitialized = false;
|
|
||||||
export const TabItem = ({ id, label, children, onClick }: TabItemProps) => {
|
export const TabItem = ({ id, label, children, onClick }: TabItemProps) => {
|
||||||
const globalCache = useService(GlobalCacheService).globalCache;
|
const globalCache = useService(GlobalCacheService).globalCache;
|
||||||
const activeTabId$ = useMemo(
|
const activeTabId$ = useMemo(
|
||||||
@@ -27,14 +26,6 @@ export const TabItem = ({ id, label, children, onClick }: TabItemProps) => {
|
|||||||
onClick?.(isActive);
|
onClick?.(isActive);
|
||||||
}, [globalCache, id, isActive, onClick]);
|
}, [globalCache, id, isActive, onClick]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (isInitialized) return;
|
|
||||||
isInitialized = true;
|
|
||||||
if (BUILD_CONFIG.isIOS || BUILD_CONFIG.isAndroid) {
|
|
||||||
globalCache.set(cacheKey, 'home');
|
|
||||||
}
|
|
||||||
}, [globalCache]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<li
|
<li
|
||||||
className={tabItem}
|
className={tabItem}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { IconButton, notify } from '@affine/component';
|
import { IconButton, notify, toast, useConfirmModal } from '@affine/component';
|
||||||
import {
|
import {
|
||||||
MenuSeparator,
|
MenuSeparator,
|
||||||
MenuSub,
|
MenuSub,
|
||||||
@@ -6,7 +6,7 @@ import {
|
|||||||
MobileMenuItem,
|
MobileMenuItem,
|
||||||
} from '@affine/component/ui/menu';
|
} from '@affine/component/ui/menu';
|
||||||
import { useFavorite } from '@affine/core/blocksuite/block-suite-header/favorite';
|
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 { IsFavoriteIcon } from '@affine/core/components/pure/icons';
|
||||||
import { DocInfoSheet } from '@affine/core/mobile/components';
|
import { DocInfoSheet } from '@affine/core/mobile/components';
|
||||||
import { MobileTocMenu } from '@affine/core/mobile/components/toc-menu';
|
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 { useI18n } from '@affine/i18n';
|
||||||
import { track } from '@affine/track';
|
import { track } from '@affine/track';
|
||||||
import {
|
import {
|
||||||
|
DeleteIcon,
|
||||||
EdgelessIcon,
|
EdgelessIcon,
|
||||||
InformationIcon,
|
InformationIcon,
|
||||||
MoreHorizontalIcon,
|
MoreHorizontalIcon,
|
||||||
@@ -34,7 +35,8 @@ import * as styles from './page-header-more-button.css';
|
|||||||
export const PageHeaderMenuButton = () => {
|
export const PageHeaderMenuButton = () => {
|
||||||
const t = useI18n();
|
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 canEdit = useGuard('Doc_Update', docId);
|
||||||
|
|
||||||
const editorService = useService(EditorService);
|
const editorService = useService(EditorService);
|
||||||
@@ -50,6 +52,7 @@ export const PageHeaderMenuButton = () => {
|
|||||||
const title = useLiveData(editorService.editor.doc.title$);
|
const title = useLiveData(editorService.editor.doc.title$);
|
||||||
|
|
||||||
const { favorite, toggleFavorite } = useFavorite(docId);
|
const { favorite, toggleFavorite } = useFavorite(docId);
|
||||||
|
const { openConfirmModal } = useConfirmModal();
|
||||||
|
|
||||||
const handleSwitchMode = useCallback(() => {
|
const handleSwitchMode = useCallback(() => {
|
||||||
const mode = primaryMode === 'page' ? 'edgeless' : 'page';
|
const mode = primaryMode === 'page' ? 'edgeless' : 'page';
|
||||||
@@ -88,6 +91,32 @@ export const PageHeaderMenuButton = () => {
|
|||||||
toggleFavorite();
|
toggleFavorite();
|
||||||
}, [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 = (
|
const EditMenu = (
|
||||||
<>
|
<>
|
||||||
<EditorModeSwitch />
|
<EditorModeSwitch />
|
||||||
@@ -135,6 +164,18 @@ export const PageHeaderMenuButton = () => {
|
|||||||
</MobileMenuItem>
|
</MobileMenuItem>
|
||||||
</MobileMenu>
|
</MobileMenu>
|
||||||
<JournalConflictsMenuItem />
|
<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) {
|
if (isInTrash) {
|
||||||
|
|||||||
Reference in New Issue
Block a user