diff --git a/packages/frontend/component/src/ui/menu/desktop/sub.tsx b/packages/frontend/component/src/ui/menu/desktop/sub.tsx index 57a2e7e3c29a..bda807970b46 100644 --- a/packages/frontend/component/src/ui/menu/desktop/sub.tsx +++ b/packages/frontend/component/src/ui/menu/desktop/sub.tsx @@ -20,9 +20,9 @@ export const DesktopMenuSub = ({ } = {}, }: MenuSubProps) => { const { className, children, otherProps } = useMenuItem({ - ...triggerOptions, children: propsChildren, suffixIcon: , + ...triggerOptions, }); return ( diff --git a/packages/frontend/component/src/ui/menu/index.ts b/packages/frontend/component/src/ui/menu/index.ts index 3fa3d979b465..a3ac2f0d6f2d 100644 --- a/packages/frontend/component/src/ui/menu/index.ts +++ b/packages/frontend/component/src/ui/menu/index.ts @@ -30,3 +30,4 @@ export { }; export { Menu, MenuItem, MenuSeparator, MenuSub, MenuTrigger }; +export * from './mobile/hook'; diff --git a/packages/frontend/component/src/ui/menu/menu.types.ts b/packages/frontend/component/src/ui/menu/menu.types.ts index 06cf5403a13a..917d628c2220 100644 --- a/packages/frontend/component/src/ui/menu/menu.types.ts +++ b/packages/frontend/component/src/ui/menu/menu.types.ts @@ -36,7 +36,7 @@ export interface MenuItemProps export interface MenuSubProps { children: ReactNode; items: ReactNode; - triggerOptions?: Omit; + triggerOptions?: Omit; portalOptions?: Omit; subOptions?: Omit; subContentOptions?: Omit; diff --git a/packages/frontend/component/src/ui/menu/mobile/context.ts b/packages/frontend/component/src/ui/menu/mobile/context.ts index ec91487a5f19..fde6245de7b7 100644 --- a/packages/frontend/component/src/ui/menu/mobile/context.ts +++ b/packages/frontend/component/src/ui/menu/mobile/context.ts @@ -8,6 +8,11 @@ import { import type { MenuSubProps } from '../menu.types'; export type SubMenuContent = { + /** + * Customize submenu's title + * @default "Back" + */ + title?: string; items: ReactNode; contentOptions?: MenuSubProps['subContentOptions']; }; diff --git a/packages/frontend/component/src/ui/menu/mobile/hook.ts b/packages/frontend/component/src/ui/menu/mobile/hook.ts new file mode 100644 index 000000000000..5586bb0e32b2 --- /dev/null +++ b/packages/frontend/component/src/ui/menu/mobile/hook.ts @@ -0,0 +1,18 @@ +import { useCallback, useContext } from 'react'; + +import { MobileMenuContext } from './context'; + +export const useMobileMenuController = () => { + const context = useContext(MobileMenuContext); + + /** + * **A workaround to close mobile menu manually** + * By default, it will close automatically when `MenuItem` clicked. + * For custom menu content, you can use this method to close the menu. + */ + const close = useCallback(() => { + context.setOpen?.(false); + }, [context]); + + return { close }; +}; diff --git a/packages/frontend/component/src/ui/menu/mobile/root.tsx b/packages/frontend/component/src/ui/menu/mobile/root.tsx index bf4b33c837ee..4586d5c58424 100644 --- a/packages/frontend/component/src/ui/menu/mobile/root.tsx +++ b/packages/frontend/component/src/ui/menu/mobile/root.tsx @@ -134,9 +134,9 @@ export const MobileMenu = ({ className={styles.backButton} prefix={} onClick={() => setSubMenus(prev => prev.slice(0, index))} - prefixStyle={{ width: 20, height: 20 }} + prefixStyle={{ width: 24, height: 24 }} > - {t['com.affine.backButton']()} + {sub.title || t['com.affine.backButton']()} {sub.items} diff --git a/packages/frontend/component/src/ui/menu/mobile/sub.tsx b/packages/frontend/component/src/ui/menu/mobile/sub.tsx index cb2c773c7b32..2a0cca07ed1c 100644 --- a/packages/frontend/component/src/ui/menu/mobile/sub.tsx +++ b/packages/frontend/component/src/ui/menu/mobile/sub.tsx @@ -7,19 +7,20 @@ import { useMenuItem } from '../use-menu-item'; import { MobileMenuContext } from './context'; export const MobileMenuSub = ({ + title, children: propsChildren, items, triggerOptions, subContentOptions: contentOptions = {}, -}: MenuSubProps) => { +}: MenuSubProps & { title?: string }) => { const { className, children, otherProps: { onClick, ...otherTriggerOptions }, } = useMenuItem({ - ...triggerOptions, children: propsChildren, suffixIcon: , + ...triggerOptions, }); return ( @@ -27,6 +28,7 @@ export const MobileMenuSub = ({ onClick={onClick} items={items} subContentOptions={contentOptions} + title={title} >
{children} @@ -36,19 +38,23 @@ export const MobileMenuSub = ({ }; export const MobileMenuSubRaw = ({ + title, onClick, children, items, subContentOptions: contentOptions = {}, -}: MenuSubProps & { onClick?: (e: MouseEvent) => void }) => { +}: MenuSubProps & { + onClick?: (e: MouseEvent) => void; + title?: string; +}) => { const { setSubMenus } = useContext(MobileMenuContext); const onItemClick = useCallback( (e: MouseEvent) => { onClick?.(e); - setSubMenus(prev => [...prev, { items, contentOptions }]); + setSubMenus(prev => [...prev, { items, contentOptions, title }]); }, - [contentOptions, items, onClick, setSubMenus] + [contentOptions, items, onClick, setSubMenus, title] ); return {children}; diff --git a/packages/frontend/core/src/mobile/components/explorer/nodes/collection/dialog.tsx b/packages/frontend/core/src/mobile/components/explorer/nodes/collection/dialog.tsx new file mode 100644 index 000000000000..8c665facf7cc --- /dev/null +++ b/packages/frontend/core/src/mobile/components/explorer/nodes/collection/dialog.tsx @@ -0,0 +1,43 @@ +import { useI18n } from '@affine/i18n'; + +import { + RenameDialog, + type RenameDialogProps, + RenameSubMenu, + type RenameSubMenuProps, +} from '../../../rename'; + +export const CollectionRenameSubMenu = ({ + title, + text, + ...props +}: RenameSubMenuProps) => { + const t = useI18n(); + return ( + + ); +}; + +const CollectionDesc = () => { + const t = useI18n(); + return t['com.affine.collection.emptyCollectionDescription'](); +}; + +export const CollectionRenameDialog = ({ + title, + confirmText, + ...props +}: RenameDialogProps) => { + return ( + + ); +}; diff --git a/packages/frontend/core/src/mobile/components/explorer/nodes/collection/index.tsx b/packages/frontend/core/src/mobile/components/explorer/nodes/collection/index.tsx index 898a10d898e5..40b56feb504a 100644 --- a/packages/frontend/core/src/mobile/components/explorer/nodes/collection/index.tsx +++ b/packages/frontend/core/src/mobile/components/explorer/nodes/collection/index.tsx @@ -52,23 +52,6 @@ export const ExplorerCollectionNode = ({ const collection = useLiveData(collectionService.collection$(collectionId)); - const handleRename = useCallback( - (name: string) => { - if (collection && collection.name !== name) { - collectionService.updateCollection(collectionId, () => ({ - ...collection, - name, - })); - - track.$.navigationPanel.organize.renameOrganizeItem({ - type: 'collection', - }); - notify.success({ message: t['com.affine.toastMessage.rename']() }); - } - }, - [collection, collectionId, collectionService, t] - ); - const handleOpenCollapsed = useCallback(() => { setCollapsed(false); }, []); @@ -105,7 +88,7 @@ export const ExplorerCollectionNode = ({ return [...additionalOperations, ...collectionOperations]; } return collectionOperations; - }, [collectionOperations, additionalOperations]); + }, [additionalOperations, collectionOperations]); if (!collection) { return null; @@ -115,12 +98,10 @@ export const ExplorerCollectionNode = ({ diff --git a/packages/frontend/core/src/mobile/components/explorer/nodes/collection/operations.tsx b/packages/frontend/core/src/mobile/components/explorer/nodes/collection/operations.tsx index 730b43a5f9e1..1d803551090f 100644 --- a/packages/frontend/core/src/mobile/components/explorer/nodes/collection/operations.tsx +++ b/packages/frontend/core/src/mobile/components/explorer/nodes/collection/operations.tsx @@ -2,6 +2,7 @@ import { IconButton, MenuItem, MenuSeparator, + notify, useConfirmModal, } from '@affine/component'; import { usePageHelper } from '@affine/core/components/blocksuite/block-suite-page-list/utils'; @@ -28,6 +29,8 @@ import { } from '@toeverything/infra'; import { useCallback, useMemo } from 'react'; +import { CollectionRenameSubMenu } from './dialog'; + export const useExplorerCollectionNodeOperations = ( collectionId: string, onOpenCollapsed: () => void, @@ -113,6 +116,24 @@ export const useExplorerCollectionNodeOperations = ( onOpenEdit(); }, [onOpenEdit]); + const handleRename = useCallback( + (name: string) => { + const collection = collectionService.collection$(collectionId).value; + if (collection && collection.name !== name) { + collectionService.updateCollection(collectionId, () => ({ + ...collection, + name, + })); + + track.$.navigationPanel.organize.renameOrganizeItem({ + type: 'collection', + }); + notify.success({ message: t['com.affine.toastMessage.rename']() }); + } + }, + [collectionId, collectionService, t] + ); + return useMemo( () => ({ favorite, @@ -122,6 +143,7 @@ export const useExplorerCollectionNodeOperations = ( handleOpenInSplitView, handleShowEdit, handleToggleFavoriteCollection, + handleRename, }), [ favorite, @@ -129,6 +151,7 @@ export const useExplorerCollectionNodeOperations = ( handleDeleteCollection, handleOpenInNewTab, handleOpenInSplitView, + handleRename, handleShowEdit, handleToggleFavoriteCollection, ] @@ -154,6 +177,7 @@ export const useExplorerCollectionNodeOperationsMenu = ( handleOpenInSplitView, handleShowEdit, handleToggleFavoriteCollection, + handleRename, } = useExplorerCollectionNodeOperations( collectionId, onOpenCollapsed, @@ -177,6 +201,14 @@ export const useExplorerCollectionNodeOperationsMenu = ( ), }, + { + index: 10, + view: , + }, + { + index: 11, + view: , + }, { index: 99, view: ( @@ -256,6 +288,7 @@ export const useExplorerCollectionNodeOperationsMenu = ( handleDeleteCollection, handleOpenInNewTab, handleOpenInSplitView, + handleRename, handleShowEdit, handleToggleFavoriteCollection, t, diff --git a/packages/frontend/core/src/mobile/components/explorer/nodes/doc/dialog.tsx b/packages/frontend/core/src/mobile/components/explorer/nodes/doc/dialog.tsx new file mode 100644 index 000000000000..d4cf9ace932c --- /dev/null +++ b/packages/frontend/core/src/mobile/components/explorer/nodes/doc/dialog.tsx @@ -0,0 +1,13 @@ +import { useI18n } from '@affine/i18n'; + +import { RenameSubMenu, type RenameSubMenuProps } from '../../../rename'; + +export const DocRenameSubMenu = ({ title, text }: RenameSubMenuProps) => { + const t = useI18n(); + return ( + + ); +}; diff --git a/packages/frontend/core/src/mobile/components/explorer/nodes/doc/index.tsx b/packages/frontend/core/src/mobile/components/explorer/nodes/doc/index.tsx index 5bdcad26d76b..bb7611af27be 100644 --- a/packages/frontend/core/src/mobile/components/explorer/nodes/doc/index.tsx +++ b/packages/frontend/core/src/mobile/components/explorer/nodes/doc/index.tsx @@ -1,11 +1,9 @@ import { Loading } from '@affine/component'; -import { useAsyncCallback } from '@affine/core/components/hooks/affine-async-hooks'; import { DocDisplayMetaService } from '@affine/core/modules/doc-display-meta'; import { DocInfoService } from '@affine/core/modules/doc-info'; import { DocsSearchService } from '@affine/core/modules/docs-search'; import type { NodeOperation } from '@affine/core/modules/explorer'; import { useI18n } from '@affine/i18n'; -import track from '@affine/track'; import { DocsService, FeatureFlagService, @@ -92,14 +90,6 @@ export const ExplorerDocNode = ({ ); }, [indexerLoading]); - const handleRename = useAsyncCallback( - async (newName: string) => { - await docsService.changeDocTitle(docId, newName); - track.$.navigationPanel.organize.renameOrganizeItem({ type: 'doc' }); - }, - [docId, docsService] - ); - const docInfoModal = useService(DocInfoService).modal; const option = useMemo( () => ({ @@ -126,7 +116,6 @@ export const ExplorerDocNode = ({ ) } - onRename={handleRename} operations={finalOperations} data-testid={`explorer-doc-${docId}`} > diff --git a/packages/frontend/core/src/mobile/components/explorer/nodes/doc/operations.tsx b/packages/frontend/core/src/mobile/components/explorer/nodes/doc/operations.tsx index 0f9689778163..0b8cb02af235 100644 --- a/packages/frontend/core/src/mobile/components/explorer/nodes/doc/operations.tsx +++ b/packages/frontend/core/src/mobile/components/explorer/nodes/doc/operations.tsx @@ -33,6 +33,8 @@ import { } from '@toeverything/infra'; import { useCallback, useMemo } from 'react'; +import { DocRenameSubMenu } from './dialog'; + export const useExplorerDocNodeOperations = ( docId: string, options: { @@ -135,6 +137,14 @@ export const useExplorerDocNodeOperations = ( }); }, [docId, compatibleFavoriteItemsAdapter]); + const handleRename = useAsyncCallback( + async (newName: string) => { + await docsService.changeDocTitle(docId, newName); + track.$.navigationPanel.organize.renameOrganizeItem({ type: 'doc' }); + }, + [docId, docsService] + ); + return useMemo( () => ({ favorite, @@ -145,6 +155,7 @@ export const useExplorerDocNodeOperations = ( handleOpenInNewTab, handleMoveToTrash, handleOpenInfoModal, + handleRename, }), [ favorite, @@ -154,6 +165,7 @@ export const useExplorerDocNodeOperations = ( handleOpenInNewTab, handleOpenInSplitView, handleOpenInfoModal, + handleRename, handleToggleFavoriteDoc, ] ); @@ -177,6 +189,7 @@ export const useExplorerDocNodeOperationsMenu = ( handleOpenInNewTab, handleMoveToTrash, handleOpenInfoModal, + handleRename, } = useExplorerDocNodeOperations(docId, options); const enableMultiView = useLiveData( @@ -197,6 +210,14 @@ export const useExplorerDocNodeOperationsMenu = ( /> ), }, + { + index: 10, + view: , + }, + { + index: 11, + view: , + }, { index: 50, view: ( @@ -289,6 +310,7 @@ export const useExplorerDocNodeOperationsMenu = ( handleOpenInNewTab, handleOpenInSplitView, handleOpenInfoModal, + handleRename, handleToggleFavoriteDoc, t, ] diff --git a/packages/frontend/core/src/mobile/components/explorer/nodes/folder/dialog.tsx b/packages/frontend/core/src/mobile/components/explorer/nodes/folder/dialog.tsx new file mode 100644 index 000000000000..230c269c110b --- /dev/null +++ b/packages/frontend/core/src/mobile/components/explorer/nodes/folder/dialog.tsx @@ -0,0 +1,58 @@ +import { useI18n } from '@affine/i18n'; +import { EditIcon } from '@blocksuite/icons/rc'; + +import type { RenameDialogProps, RenameSubMenuProps } from '../../../rename'; +import { RenameDialog, RenameSubMenu } from '../../../rename'; + +export const FolderCreateTip = ({ + input, + parentName, +}: { + input?: string; + parentName?: string; +}) => { + const t = useI18n(); + const parent = parentName + ? parentName + : t['com.affine.m.explorer.folder.root'](); + + const tip = input + ? t['com.affine.m.explorer.folder.new-tip-not-empty']({ + value: input, + parent, + }) + : t['com.affine.m.explorer.folder.new-tip-empty']({ parent }); + + return tip; +}; + +export const FolderRenameSubMenu = ({ + title: propsTitle, + icon: propsIcon, + text: propsText, + ...props +}: RenameSubMenuProps) => { + const t = useI18n(); + const title = propsTitle || t['com.affine.m.explorer.folder.rename'](); + const icon = propsIcon || ; + const text = propsText || title; + + return ; +}; + +export const FolderRenameDialog = ({ + title: propsTitle, + confirmText: propsConfirmText, + ...props +}: RenameDialogProps & { + open?: boolean; + onOpenChange?: (v: boolean) => void; +}) => { + const t = useI18n(); + const title = + propsTitle || t['com.affine.m.explorer.folder.new-dialog-title'](); + const confirmText = + propsConfirmText || t['com.affine.m.explorer.folder.rename-confirm'](); + + return ; +}; diff --git a/packages/frontend/core/src/mobile/components/explorer/nodes/folder/index.tsx b/packages/frontend/core/src/mobile/components/explorer/nodes/folder/index.tsx index 2f884cb7797b..ac343beb92c7 100644 --- a/packages/frontend/core/src/mobile/components/explorer/nodes/folder/index.tsx +++ b/packages/frontend/core/src/mobile/components/explorer/nodes/folder/index.tsx @@ -47,14 +47,13 @@ import { ExplorerTreeNode } from '../../tree/node'; import { ExplorerCollectionNode } from '../collection'; import { ExplorerDocNode } from '../doc'; import { ExplorerTagNode } from '../tag'; +import { FolderCreateTip, FolderRenameSubMenu } from './dialog'; import { FavoriteFolderOperation } from './operations'; export const ExplorerFolderNode = ({ nodeId, - defaultRenaming, operations, }: { - defaultRenaming?: boolean; nodeId: string; operations?: | NodeOperation[] @@ -83,11 +82,7 @@ export const ExplorerFolderNode = ({ if (type === 'folder') { return ( - + ); } if (!data) return null; @@ -123,10 +118,8 @@ const ExplorerFolderIcon: ExplorerTreeNodeIcon = ({ const ExplorerFolderNodeFolder = ({ node, - defaultRenaming, operations: additionalOperations, }: { - defaultRenaming?: boolean; node: FolderNode; operations?: NodeOperation[]; }) => { @@ -144,7 +137,6 @@ const ExplorerFolderNodeFolder = ({ featureFlagService.flags.enable_emoji_folder_icon.$ ); const [collapsed, setCollapsed] = useState(true); - const [newFolderId, setNewFolderId] = useState(null); const { createPage } = usePageHelper( workspaceService.workspace.docCollection @@ -183,13 +175,12 @@ const ExplorerFolderNodeFolder = ({ }, [createPage, node]); const handleCreateSubfolder = useCallback(() => { - const newFolderId = node.createFolder( + node.createFolder( t['com.affine.rootAppSidebar.organize.new-folders'](), node.indexAt('before') ); track.$.navigationPanel.organize.createOrganizeItem({ type: 'folder' }); setCollapsed(false); - setNewFolderId(newFolderId); }, [node, t]); const handleAddToFolder = useCallback( @@ -237,6 +228,13 @@ const ExplorerFolderNodeFolder = ({ ] ); + const createSubTipRenderer = useCallback( + ({ input }: { input: string }) => { + return ; + }, + [name] + ); + const folderOperations = useMemo(() => { return [ { @@ -254,12 +252,30 @@ const ExplorerFolderNodeFolder = ({ ), }, + { + index: 98, + view: ( + + ), + }, + { + index: 99, + view: , + }, { index: 100, view: ( - } onClick={handleCreateSubfolder}> - {t['com.affine.rootAppSidebar.organize.folder.create-subfolder']()} - + } + /> ), }, { @@ -327,11 +343,14 @@ const ExplorerFolderNodeFolder = ({ }, ]; }, [ + createSubTipRenderer, handleAddToFolder, handleCreateSubfolder, handleDelete, handleNewDoc, - node, + handleRename, + name, + node.id, t, ]); @@ -370,7 +389,6 @@ const ExplorerFolderNodeFolder = ({ const handleCollapsedChange = useCallback((collapsed: boolean) => { if (collapsed) { - setNewFolderId(null); // reset new folder id to clear the renaming state setCollapsed(true); } else { setCollapsed(false); @@ -381,12 +399,9 @@ const ExplorerFolderNodeFolder = ({ @@ -394,7 +409,6 @@ const ExplorerFolderNodeFolder = ({ ))} diff --git a/packages/frontend/core/src/mobile/components/explorer/nodes/tag/dialog.css.ts b/packages/frontend/core/src/mobile/components/explorer/nodes/tag/dialog.css.ts new file mode 100644 index 000000000000..99ebf2c7b04c --- /dev/null +++ b/packages/frontend/core/src/mobile/components/explorer/nodes/tag/dialog.css.ts @@ -0,0 +1,53 @@ +import { cssVarV2 } from '@toeverything/theme/v2'; +import { style } from '@vanilla-extract/css'; + +export const colorDot = style({ + width: 42, + height: 42, + textAlign: 'center', + borderRadius: 8, + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + flexShrink: 0, + ':before': { + content: '""', + width: 22, + height: 22, + borderRadius: 11, + display: 'block', + background: 'currentColor', + }, +}); + +export const colorTrigger = style([ + colorDot, + { + border: `1px solid ${cssVarV2('layer/insideBorder/border')}`, + selectors: { + '&[data-active="true"]': { + borderColor: cssVarV2('input/border/active'), + }, + }, + }, +]); + +export const colorsRow = style({ + display: 'flex', + alignItems: 'center', + justifyContent: 'space-between', + padding: '0 12px', + height: 54, + + selectors: { + // TODO(@CatsJuice): this animation is conflicting with sub-menu height detection + '&[data-enable-fold]': { + height: 0, + overflow: 'hidden', + transition: 'all 0.23s ease', + }, + '&[data-enable-fold][data-active="true"]': { + height: 54, + }, + }, +}); diff --git a/packages/frontend/core/src/mobile/components/explorer/nodes/tag/dialog.tsx b/packages/frontend/core/src/mobile/components/explorer/nodes/tag/dialog.tsx new file mode 100644 index 000000000000..c0723875cb2f --- /dev/null +++ b/packages/frontend/core/src/mobile/components/explorer/nodes/tag/dialog.tsx @@ -0,0 +1,192 @@ +import { useMobileMenuController } from '@affine/component'; +import { TagService } from '@affine/core/modules/tag'; +import { useI18n } from '@affine/i18n'; +import { useLiveData, useService } from '@toeverything/infra'; +import { + createContext, + type Dispatch, + type ReactNode, + type SetStateAction, + useCallback, + useContext, + useMemo, + useState, +} from 'react'; + +import { RenameContent, RenameSubMenu } from '../../../rename'; +import { RenameDialog } from '../../../rename/dialog'; +import type { RenameContentProps } from '../../../rename/type'; +import * as styles from './dialog.css'; + +const TagColorContext = createContext<{ + colors: string[]; + color: string; + setColor: Dispatch>; + show: boolean; + setShow: Dispatch>; + enableAnimation?: boolean; +}>({ + color: '', + setColor: () => {}, + colors: [], + show: false, + setShow: () => {}, +}); + +const ColorPickerTrigger = () => { + const { color, show, setShow } = useContext(TagColorContext); + return ( +
setShow(prev => !prev)} + /> + ); +}; + +const ColorPickerSelect = () => { + const { + enableAnimation, + colors, + color: current, + setColor, + show, + } = useContext(TagColorContext); + + if (!show && !enableAnimation) return null; + + return ( +
+ {colors.map(color => ( +
setColor(color)} + key={color} + className={styles.colorDot} + style={{ color }} + /> + ))} +
+ ); +}; + +interface TagRenameContentProps extends Omit { + initialColor?: string; + onConfirm?: (name: string, color: string) => void; + enableAnimation?: boolean; +} +const TagRenameContent = ({ + initialColor, + onConfirm, + enableAnimation, + ...props +}: TagRenameContentProps) => { + const tagService = useService(TagService); + const colors = useMemo(() => { + return tagService.tagColors.map(([_, value]) => value); + }, [tagService.tagColors]); + + const [color, setColor] = useState( + initialColor || tagService.randomTagColor() + ); + const [show, setShow] = useState(false); + + const handleConfirm = useCallback( + (name: string) => { + onConfirm?.(name, color); + }, + [color, onConfirm] + ); + + return ( + + + + ); +}; + +interface TagRenameDialogProps extends TagRenameContentProps { + title?: string; + open?: boolean; + onOpenChange?: (open: boolean) => void; +} +export const TagRenameDialog = ({ + title: propsTitle, + confirmText: propsConfirmText, + open, + onOpenChange, + ...props +}: TagRenameDialogProps) => { + const t = useI18n(); + const title = propsTitle || t['com.affine.m.explorer.tag.new-dialog-title'](); + const confirmText = + propsConfirmText || t['com.affine.m.explorer.tag.rename-confirm'](); + + return ( + + + + ); +}; + +interface TagRenameSubMenuProps { + tagId?: string; + title?: string; + icon?: ReactNode; + text?: string; + onConfirm?: (name: string, color: string) => void; +} +export const TagRenameSubMenu = ({ + tagId, + title, + icon, + text, + onConfirm, +}: TagRenameSubMenuProps) => { + const t = useI18n(); + const { close } = useMobileMenuController(); + const tagService = useService(TagService); + const tagRecord = useLiveData(tagService.tagList.tagByTagId$(tagId)); + const tagName = useLiveData(tagRecord?.value$); + const tagColor = useLiveData(tagRecord?.color$); + + const handleCloseAndConfirm = useCallback( + (name: string, color: string) => { + close(); + onConfirm?.(name, color); + }, + [close, onConfirm] + ); + + return ( + + + + ); +}; diff --git a/packages/frontend/core/src/mobile/components/explorer/nodes/tag/index.tsx b/packages/frontend/core/src/mobile/components/explorer/nodes/tag/index.tsx index 2d939bf44523..8173b2644e2c 100644 --- a/packages/frontend/core/src/mobile/components/explorer/nodes/tag/index.tsx +++ b/packages/frontend/core/src/mobile/components/explorer/nodes/tag/index.tsx @@ -2,7 +2,6 @@ import type { NodeOperation } from '@affine/core/modules/explorer'; import type { Tag } from '@affine/core/modules/tag'; import { TagService } from '@affine/core/modules/tag'; import { useI18n } from '@affine/i18n'; -import track from '@affine/track'; import { GlobalContextService, useLiveData, @@ -23,10 +22,8 @@ import * as styles from './styles.css'; export const ExplorerTagNode = ({ tagId, operations: additionalOperations, - defaultRenaming, }: { tagId: string; - defaultRenaming?: boolean; operations?: NodeOperation[]; }) => { const t = useI18n(); @@ -58,18 +55,6 @@ export const ExplorerTagNode = ({ [tagColor] ); - const handleRename = useCallback( - (newName: string) => { - if (tagRecord && tagRecord.value$.value !== newName) { - tagRecord.rename(newName); - track.$.navigationPanel.organize.renameOrganizeItem({ - type: 'tag', - }); - } - }, - [tagRecord] - ); - const option = useMemo( () => ({ openNodeCollapsed: () => setCollapsed(false), @@ -94,13 +79,10 @@ export const ExplorerTagNode = ({ diff --git a/packages/frontend/core/src/mobile/components/explorer/nodes/tag/operations.tsx b/packages/frontend/core/src/mobile/components/explorer/nodes/tag/operations.tsx index 967113ad0670..b7c075aaad02 100644 --- a/packages/frontend/core/src/mobile/components/explorer/nodes/tag/operations.tsx +++ b/packages/frontend/core/src/mobile/components/explorer/nodes/tag/operations.tsx @@ -7,12 +7,7 @@ import { TagService } from '@affine/core/modules/tag'; import { WorkbenchService } from '@affine/core/modules/workbench'; import { useI18n } from '@affine/i18n'; import { track } from '@affine/track'; -import { - DeleteIcon, - OpenInNewIcon, - PlusIcon, - SplitViewIcon, -} from '@blocksuite/icons/rc'; +import { DeleteIcon, PlusIcon, SplitViewIcon } from '@blocksuite/icons/rc'; import { DocsService, FeatureFlagService, @@ -23,6 +18,8 @@ import { } from '@toeverything/infra'; import { useCallback, useMemo } from 'react'; +import { TagRenameSubMenu } from './dialog'; + export const useExplorerTagNodeOperations = ( tagId: string, { @@ -86,6 +83,37 @@ export const useExplorerTagNodeOperations = ( track.$.navigationPanel.organize.openInNewTab({ type: 'tag' }); }, [tagId, workbenchService]); + const handleRename = useCallback( + (newName: string) => { + if (tagRecord && tagRecord.value$.value !== newName) { + tagRecord.rename(newName); + track.$.navigationPanel.organize.renameOrganizeItem({ + type: 'tag', + }); + } + }, + [tagRecord] + ); + const handleChangeColor = useCallback( + (color: string) => { + if (tagRecord && tagRecord.color$.value !== color) { + tagRecord.changeColor(color); + } + }, + [tagRecord] + ); + const handleChangeNameOrColor = useCallback( + (name?: string, color?: string) => { + if (name !== undefined) { + handleRename(name); + } + if (color !== undefined) { + handleChangeColor(color); + } + }, + [handleChangeColor, handleRename] + ); + return useMemo( () => ({ favorite, @@ -94,13 +122,19 @@ export const useExplorerTagNodeOperations = ( handleOpenInSplitView, handleToggleFavoriteTag, handleOpenInNewTab, + handleRename, + handleChangeColor, + handleChangeNameOrColor, }), [ favorite, + handleChangeColor, + handleChangeNameOrColor, handleMoveToTrash, handleNewDoc, handleOpenInNewTab, handleOpenInSplitView, + handleRename, handleToggleFavoriteTag, ] ); @@ -122,7 +156,7 @@ export const useExplorerTagNodeOperationsMenu = ( handleMoveToTrash, handleOpenInSplitView, handleToggleFavoriteTag, - handleOpenInNewTab, + handleChangeNameOrColor, } = useExplorerTagNodeOperations(tagId, option); return useMemo( @@ -131,21 +165,15 @@ export const useExplorerTagNodeOperationsMenu = ( index: 0, inline: true, view: ( - + ), }, { - index: 50, + index: 10, view: ( - } onClick={handleOpenInNewTab}> - {t['com.affine.workbench.tab.page-menu-open']()} - + ), }, ...(BUILD_CONFIG.isElectron && enableMultiView @@ -196,12 +224,13 @@ export const useExplorerTagNodeOperationsMenu = ( [ enableMultiView, favorite, + handleChangeNameOrColor, handleMoveToTrash, handleNewDoc, - handleOpenInNewTab, handleOpenInSplitView, handleToggleFavoriteTag, t, + tagId, ] ); }; diff --git a/packages/frontend/core/src/mobile/components/explorer/sections/collections/index.tsx b/packages/frontend/core/src/mobile/components/explorer/sections/collections/index.tsx index d2bf33fa7461..186ed64e5256 100644 --- a/packages/frontend/core/src/mobile/components/explorer/sections/collections/index.tsx +++ b/packages/frontend/core/src/mobile/components/explorer/sections/collections/index.tsx @@ -1,4 +1,3 @@ -import { useEditCollectionName } from '@affine/core/components/page-list'; import { createEmptyCollection } from '@affine/core/components/page-list/use-collection-manager'; import { CollectionService } from '@affine/core/modules/collection'; import { ExplorerService } from '@affine/core/modules/explorer'; @@ -8,11 +7,12 @@ import { useI18n } from '@affine/i18n'; import { track } from '@affine/track'; import { useLiveData, useServices } from '@toeverything/infra'; import { nanoid } from 'nanoid'; -import { useCallback } from 'react'; +import { useCallback, useState } from 'react'; import { AddItemPlaceholder } from '../../layouts/add-item-placeholder'; import { CollapsibleSection } from '../../layouts/collapsible-section'; import { ExplorerCollectionNode } from '../../nodes/collection'; +import { CollectionRenameDialog } from '../../nodes/collection/dialog'; export const ExplorerCollections = () => { const t = useI18n(); @@ -23,31 +23,22 @@ export const ExplorerCollections = () => { }); const explorerSection = explorerService.sections.collections; const collections = useLiveData(collectionService.collections$); - const { open: openCreateCollectionModel } = useEditCollectionName({ - title: t['com.affine.editCollection.createCollection'](), - showTips: true, - }); + const [showCreateCollectionModal, setShowCreateCollectionModal] = + useState(false); - const handleCreateCollection = useCallback(() => { - openCreateCollectionModel('') - .then(name => { - const id = nanoid(); - collectionService.addCollection(createEmptyCollection(id, { name })); - track.$.navigationPanel.organize.createOrganizeItem({ - type: 'collection', - }); - workbenchService.workbench.openCollection(id); - explorerSection.setCollapsed(false); - }) - .catch(err => { - console.error(err); + const handleCreateCollection = useCallback( + (name: string) => { + setShowCreateCollectionModal(false); + const id = nanoid(); + collectionService.addCollection(createEmptyCollection(id, { name })); + track.$.navigationPanel.organize.createOrganizeItem({ + type: 'collection', }); - }, [ - collectionService, - explorerSection, - openCreateCollectionModel, - workbenchService.workbench, - ]); + workbenchService.workbench.openCollection(id); + explorerSection.setCollapsed(false); + }, + [collectionService, explorerSection, workbenchService.workbench] + ); return ( { setShowCreateCollectionModal(true)} + /> + diff --git a/packages/frontend/core/src/mobile/components/explorer/sections/organize/index.tsx b/packages/frontend/core/src/mobile/components/explorer/sections/organize/index.tsx index 601e2a6b228b..60fd4592f794 100644 --- a/packages/frontend/core/src/mobile/components/explorer/sections/organize/index.tsx +++ b/packages/frontend/core/src/mobile/components/explorer/sections/organize/index.tsx @@ -7,11 +7,12 @@ import { OrganizeService } from '@affine/core/modules/organize'; import { useI18n } from '@affine/i18n'; import track from '@affine/track'; import { useLiveData, useServices } from '@toeverything/infra'; -import { useCallback, useEffect, useState } from 'react'; +import { useCallback, useState } from 'react'; import { AddItemPlaceholder } from '../../layouts/add-item-placeholder'; import { CollapsibleSection } from '../../layouts/collapsible-section'; import { ExplorerFolderNode } from '../../nodes/folder'; +import { FolderCreateTip, FolderRenameDialog } from '../../nodes/folder/dialog'; export const ExplorerOrganize = () => { const { organizeService, explorerService } = useServices({ @@ -19,8 +20,7 @@ export const ExplorerOrganize = () => { ExplorerService, }); const explorerSection = explorerService.sections.organize; - const collapsed = useLiveData(explorerSection.collapsed$); - const [newFolderId, setNewFolderId] = useState(null); + const [openNewFolderDialog, setOpenNewFolderDialog] = useState(false); const t = useI18n(); @@ -30,20 +30,18 @@ export const ExplorerOrganize = () => { const folders = useLiveData(rootFolder.sortedChildren$); const isLoading = useLiveData(folderTree.isLoading$); - const handleCreateFolder = useCallback(() => { - const newFolderId = rootFolder.createFolder( - 'New Folder', - rootFolder.indexAt('before') - ); - track.$.navigationPanel.organize.createOrganizeItem({ type: 'folder' }); - setNewFolderId(newFolderId); - explorerSection.setCollapsed(false); - return newFolderId; - }, [explorerSection, rootFolder]); - - useEffect(() => { - if (collapsed) setNewFolderId(null); // reset new folder id to clear the renaming state - }, [collapsed]); + const handleCreateFolder = useCallback( + (name: string) => { + const newFolderId = rootFolder.createFolder( + name, + rootFolder.indexAt('before') + ); + track.$.navigationPanel.organize.createOrganizeItem({ type: 'folder' }); + explorerSection.setCollapsed(false); + return newFolderId; + }, + [explorerSection, rootFolder] + ); return ( { {/* TODO(@CatsJuice): Organize loading UI */} : null}> {folders.map(child => ( - + ))} setOpenNewFolderDialog(true)} /> + ); }; diff --git a/packages/frontend/core/src/mobile/components/explorer/sections/tags/index.tsx b/packages/frontend/core/src/mobile/components/explorer/sections/tags/index.tsx index be9a77fa9671..6c0bf1db442f 100644 --- a/packages/frontend/core/src/mobile/components/explorer/sections/tags/index.tsx +++ b/packages/frontend/core/src/mobile/components/explorer/sections/tags/index.tsx @@ -1,15 +1,23 @@ import { ExplorerService } from '@affine/core/modules/explorer'; import { ExplorerTreeRoot } from '@affine/core/modules/explorer/views/tree'; -import type { Tag } from '@affine/core/modules/tag'; import { TagService } from '@affine/core/modules/tag'; import { useI18n } from '@affine/i18n'; import { track } from '@affine/track'; import { useLiveData, useServices } from '@toeverything/infra'; -import { useCallback, useEffect, useState } from 'react'; +import { useCallback, useState } from 'react'; import { AddItemPlaceholder } from '../../layouts/add-item-placeholder'; import { CollapsibleSection } from '../../layouts/collapsible-section'; import { ExplorerTagNode } from '../../nodes/tag'; +import { TagRenameDialog } from '../../nodes/tag/dialog'; + +export const TagDesc = ({ input }: { input: string }) => { + const t = useI18n(); + + return input + ? t['com.affine.m.explorer.tag.new-tip-not-empty']({ value: input }) + : t['com.affine.m.explorer.tag.new-tip-empty'](); +}; export const ExplorerTags = () => { const { tagService, explorerService } = useServices({ @@ -17,25 +25,20 @@ export const ExplorerTags = () => { ExplorerService, }); const explorerSection = explorerService.sections.tags; - const collapsed = useLiveData(explorerSection.collapsed$); - const [createdTag, setCreatedTag] = useState(null); const tags = useLiveData(tagService.tagList.tags$); + const [showNewTagDialog, setShowNewTagDialog] = useState(false); const t = useI18n(); - const handleCreateNewFavoriteDoc = useCallback(() => { - const newTags = tagService.tagList.createTag( - t['com.affine.rootAppSidebar.tags.new-tag'](), - tagService.randomTagColor() - ); - setCreatedTag(newTags); - track.$.navigationPanel.organize.createOrganizeItem({ type: 'tag' }); - explorerSection.setCollapsed(false); - }, [explorerSection, t, tagService]); - - useEffect(() => { - if (collapsed) setCreatedTag(null); // reset created tag to clear the renaming state - }, [collapsed]); + const handleNewTag = useCallback( + (name: string, color: string) => { + setShowNewTagDialog(false); + tagService.tagList.createTag(name, color); + track.$.navigationPanel.organize.createOrganizeItem({ type: 'tag' }); + explorerSection.setCollapsed(false); + }, + [explorerSection, tagService] + ); return ( { > {tags.map(tag => ( - + ))} setShowNewTagDialog(true)} label={t[ 'com.affine.rootAppSidebar.explorer.tag-section-add-tooltip' ]()} /> + ); diff --git a/packages/frontend/core/src/mobile/components/explorer/tree/node.tsx b/packages/frontend/core/src/mobile/components/explorer/tree/node.tsx index 495eb6ad2461..b8a652c1fbb8 100644 --- a/packages/frontend/core/src/mobile/components/explorer/tree/node.tsx +++ b/packages/frontend/core/src/mobile/components/explorer/tree/node.tsx @@ -1,17 +1,10 @@ -import { MenuItem, MobileMenu } from '@affine/component'; -import { RenameModal } from '@affine/component/rename-modal'; -import { AppSidebarService } from '@affine/core/modules/app-sidebar'; -import type { - BaseExplorerTreeNodeProps, - NodeOperation, -} from '@affine/core/modules/explorer'; +import { MobileMenu } from '@affine/component'; +import type { BaseExplorerTreeNodeProps } from '@affine/core/modules/explorer'; import { ExplorerTreeContext } from '@affine/core/modules/explorer'; import { WorkbenchLink } from '@affine/core/modules/workbench'; import { extractEmojiIcon } from '@affine/core/utils'; -import { useI18n } from '@affine/i18n'; -import { ArrowDownSmallIcon, EditIcon } from '@blocksuite/icons/rc'; +import { ArrowDownSmallIcon } from '@blocksuite/icons/rc'; import * as Collapsible from '@radix-ui/react-collapsible'; -import { useLiveData, useService } from '@toeverything/infra'; import { assignInlineVars } from '@vanilla-extract/dynamic'; import { Fragment, @@ -33,9 +26,6 @@ export const ExplorerTreeNode = ({ onClick, to, active, - defaultRenaming, - renameable, - onRename, disabled, collapsed, extractEmojiAsIcon, @@ -47,18 +37,13 @@ export const ExplorerTreeNode = ({ linkComponent: LinkComponent = WorkbenchLink, ...otherProps }: ExplorerTreeNodeProps) => { - const t = useI18n(); const context = useContext(ExplorerTreeContext); const level = context?.level ?? 0; // If no onClick or to is provided, clicking on the node will toggle the collapse state const clickForCollapse = !onClick && !to && !disabled; const [childCount, setChildCount] = useState(0); - const [renaming, setRenaming] = useState(defaultRenaming); const rootRef = useRef(null); - const appSidebarService = useService(AppSidebarService).sidebar; - const sidebarWidth = useLiveData(appSidebarService.width$); - const { emoji, name } = useMemo(() => { if (!extractEmojiAsIcon || !rawName) { return { @@ -73,39 +58,13 @@ export const ExplorerTreeNode = ({ }; }, [extractEmojiAsIcon, rawName]); - const presetOperations = useMemo( - () => - ( - [ - renameable - ? { - index: 0, - view: ( - } - onClick={() => setRenaming(true)} - > - {t['com.affine.menu.rename']()} - - ), - } - : null, - ] as (NodeOperation | null)[] - ).filter((t): t is NodeOperation => t !== null), - [renameable, t] - ); - const { menuOperations } = useMemo(() => { - const sorted = [...presetOperations, ...operations].sort( - (a, b) => a.index - b.index - ); + const sorted = [...operations].sort((a, b) => a.index - b.index); return { menuOperations: sorted.filter(({ inline }) => !inline), inlineOperations: sorted.filter(({ inline }) => !!inline), }; - }, [presetOperations, operations]); + }, [operations]); const contextValue = useMemo(() => { return { @@ -127,11 +86,6 @@ export const ExplorerTreeNode = ({ [collapsed, setCollapsed] ); - const handleRename = useCallback( - (newName: string) => onRename?.(newName), - [onRename] - ); - const handleClick = useCallback( (e: React.MouseEvent) => { if (e.defaultPrevented) { @@ -193,18 +147,6 @@ export const ExplorerTreeNode = ({ data-collapsed={collapsed !== false} />
- - {renameable && ( - -
- - )}
); diff --git a/packages/frontend/core/src/mobile/components/index.ts b/packages/frontend/core/src/mobile/components/index.ts index c6d272437286..18dd7f3f6561 100644 --- a/packages/frontend/core/src/mobile/components/index.ts +++ b/packages/frontend/core/src/mobile/components/index.ts @@ -1,6 +1,7 @@ export * from './app-tabs'; export * from './doc-card'; export * from './page-header'; +export * from './rename'; export * from './search-input'; export * from './search-result'; export * from './user-plan-tag'; diff --git a/packages/frontend/core/src/mobile/components/rename/content.css.ts b/packages/frontend/core/src/mobile/components/rename/content.css.ts new file mode 100644 index 000000000000..1e7de0ff0e70 --- /dev/null +++ b/packages/frontend/core/src/mobile/components/rename/content.css.ts @@ -0,0 +1,35 @@ +import { cssVarV2 } from '@toeverything/theme/v2'; +import { style } from '@vanilla-extract/css'; + +export const inputWrapper = style({ + padding: '4px 12px', + display: 'flex', + alignItems: 'center', + gap: 10, +}); +export const input = style({ + width: '100%', + height: 42, + border: '1px solid ' + cssVarV2('input/border/active'), + borderRadius: 8, + padding: '0 4px', +}); +export const desc = style({ + padding: '11px 16px', + fontSize: 17, + fontWeight: 400, + lineHeight: '22px', + letterSpacing: -0.43, + color: cssVarV2('text/secondary'), +}); +export const doneWrapper = style({ + width: '100%', + padding: '8px 16px', +}); +export const done = style({ + width: '100%', + height: 44, + borderRadius: 8, + fontSize: 17, + fontWeight: 400, +}); diff --git a/packages/frontend/core/src/mobile/components/rename/content.tsx b/packages/frontend/core/src/mobile/components/rename/content.tsx new file mode 100644 index 000000000000..7f075e1f6d66 --- /dev/null +++ b/packages/frontend/core/src/mobile/components/rename/content.tsx @@ -0,0 +1,56 @@ +import { Button, RowInput } from '@affine/component'; +import { useI18n } from '@affine/i18n'; +import { useCallback, useState } from 'react'; + +import * as styles from './content.css'; +import type { RenameContentProps } from './type'; + +export const RenameContent = ({ + initialName = '', + inputPrefixRenderer: InputPrefixRenderer, + inputBelowRenderer: InputBelowRenderer, + descRenderer: DescRenderer, + confirmText = 'Done', + onConfirm, +}: RenameContentProps) => { + const t = useI18n(); + const [value, setValue] = useState(initialName); + + const handleDone = useCallback(() => { + onConfirm?.(value); + }, [onConfirm, value]); + + return ( +
+
+ {InputPrefixRenderer ? : null} + +
+ {} + {InputBelowRenderer ? : null} +
+ {DescRenderer ? ( + + ) : ( + t['com.affine.m.rename-to']({ name: value }) + )} +
+
+ +
+
+ ); +}; diff --git a/packages/frontend/core/src/mobile/components/rename/dialog.css.ts b/packages/frontend/core/src/mobile/components/rename/dialog.css.ts new file mode 100644 index 000000000000..fe0994c039a9 --- /dev/null +++ b/packages/frontend/core/src/mobile/components/rename/dialog.css.ts @@ -0,0 +1,17 @@ +import { cssVarV2 } from '@toeverything/theme/v2'; +import { style } from '@vanilla-extract/css'; + +export const header = style({ + display: 'flex', + justifyContent: 'space-between', + alignItems: 'center', + gap: 8, + padding: '10px 16px', +}); +export const title = style({ + fontSize: 17, + fontWeight: 600, + lineHeight: '22px', + letterSpacing: -0.43, + color: cssVarV2('text/primary'), +}); diff --git a/packages/frontend/core/src/mobile/components/rename/dialog.tsx b/packages/frontend/core/src/mobile/components/rename/dialog.tsx new file mode 100644 index 000000000000..8932aae91209 --- /dev/null +++ b/packages/frontend/core/src/mobile/components/rename/dialog.tsx @@ -0,0 +1,49 @@ +import { IconButton, Modal } from '@affine/component'; +import { CloseIcon } from '@blocksuite/icons/rc'; +import { useCallback } from 'react'; + +import { RenameContent } from './content'; +import * as styles from './dialog.css'; +import type { RenameDialogProps } from './type'; + +export const RenameDialog = ({ + open, + title, + onOpenChange, + onConfirm, + children, + ...props +}: RenameDialogProps & { + open?: boolean; + onOpenChange?: (v: boolean) => void; +}) => { + const handleRename = useCallback( + (value: string) => { + onConfirm?.(value); + onOpenChange?.(false); + }, + [onOpenChange, onConfirm] + ); + + const close = useCallback(() => { + onOpenChange?.(false); + }, [onOpenChange]); + + return ( + +
+ {title} + } onClick={close} /> +
+ {children ?? } +
+ ); +}; diff --git a/packages/frontend/core/src/mobile/components/rename/index.tsx b/packages/frontend/core/src/mobile/components/rename/index.tsx new file mode 100644 index 000000000000..bd4a345a1bb1 --- /dev/null +++ b/packages/frontend/core/src/mobile/components/rename/index.tsx @@ -0,0 +1,4 @@ +export * from './content'; +export * from './dialog'; +export * from './sub-menu'; +export * from './type'; diff --git a/packages/frontend/core/src/mobile/components/rename/sub-menu.tsx b/packages/frontend/core/src/mobile/components/rename/sub-menu.tsx new file mode 100644 index 000000000000..1a6da0c74aa8 --- /dev/null +++ b/packages/frontend/core/src/mobile/components/rename/sub-menu.tsx @@ -0,0 +1,46 @@ +import { MobileMenuSub, useMobileMenuController } from '@affine/component'; +import { useI18n } from '@affine/i18n'; +import { EditIcon } from '@blocksuite/icons/rc'; +import { useCallback } from 'react'; + +import { RenameContent } from './content'; +import type { RenameSubMenuProps } from './type'; + +export const RenameSubMenu = ({ + initialName = '', + title, + icon, + text, + children, + onConfirm, + ...props +}: RenameSubMenuProps) => { + const t = useI18n(); + const { close } = useMobileMenuController(); + + const handleRename = useCallback( + (value: string) => { + onConfirm?.(value); + close(); + }, + [close, onConfirm] + ); + + return ( + , suffixIcon: null }} + items={ + children ?? ( + + ) + } + title={title} + > + {text ?? t['com.affine.m.explorer.folder.rename']()} + + ); +}; diff --git a/packages/frontend/core/src/mobile/components/rename/type.ts b/packages/frontend/core/src/mobile/components/rename/type.ts new file mode 100644 index 000000000000..56fbe342f0e2 --- /dev/null +++ b/packages/frontend/core/src/mobile/components/rename/type.ts @@ -0,0 +1,30 @@ +import type { PropsWithChildren, ReactNode } from 'react'; + +export interface RenameBaseProps { + initialName?: string; + onConfirm?: (name: string) => void; +} + +export interface RenameContentProps extends RenameBaseProps { + confirmText?: string; + inputPrefixRenderer?: (props: { input: string }) => ReactNode; + descRenderer?: (props: { input: string }) => ReactNode; + inputBelowRenderer?: (props: { input: string }) => ReactNode; +} + +export interface RenameSubMenuProps + extends PropsWithChildren { + /** Submenu's title */ + title?: string; + /** MenuItem.icon */ + icon?: ReactNode; + /** MenuItem.text */ + text?: string; +} + +export interface RenameDialogProps + extends PropsWithChildren { + open?: boolean; + onOpenChange?: (open: boolean) => void; + title?: string; +} diff --git a/packages/frontend/core/src/modules/explorer/views/tree/node.tsx b/packages/frontend/core/src/modules/explorer/views/tree/node.tsx index 2d011fd297f5..6bb4bcba0200 100644 --- a/packages/frontend/core/src/modules/explorer/views/tree/node.tsx +++ b/packages/frontend/core/src/modules/explorer/views/tree/node.tsx @@ -61,12 +61,9 @@ export interface BaseExplorerTreeNodeProps { icon?: ExplorerTreeNodeIcon; children?: React.ReactNode; active?: boolean; - defaultRenaming?: boolean; extractEmojiAsIcon?: boolean; collapsed: boolean; setCollapsed: (collapsed: boolean) => void; - renameable?: boolean; - onRename?: (newName: string) => void; disabled?: boolean; onClick?: () => void; to?: To; @@ -81,6 +78,10 @@ export interface BaseExplorerTreeNodeProps { } interface WebExplorerTreeNodeProps extends BaseExplorerTreeNodeProps { + renameable?: boolean; + onRename?: (newName: string) => void; + defaultRenaming?: boolean; + canDrop?: DropTargetOptions['canDrop']; reorderable?: boolean; dndData?: AffineDNDData; diff --git a/packages/frontend/i18n/src/i18n-completenesses.json b/packages/frontend/i18n/src/i18n-completenesses.json index dd126c0ec5f6..e3abf71683e2 100644 --- a/packages/frontend/i18n/src/i18n-completenesses.json +++ b/packages/frontend/i18n/src/i18n-completenesses.json @@ -13,10 +13,10 @@ "ja": 100, "ko": 89, "pl": 0, - "pt-BR": 97, + "pt-BR": 96, "ru": 82, "sv-SE": 5, "ur": 3, - "zh-Hans": 99, + "zh-Hans": 98, "zh-Hant": 97 } \ No newline at end of file diff --git a/packages/frontend/i18n/src/resources/en.json b/packages/frontend/i18n/src/resources/en.json index 9337fdac1bc5..f6170340f520 100644 --- a/packages/frontend/i18n/src/resources/en.json +++ b/packages/frontend/i18n/src/resources/en.json @@ -1312,5 +1312,22 @@ "system": "System", "unnamed": "unnamed", "upgradeBrowser": "Please upgrade to the latest version of Chrome for the best experience.", - "com.affine.workspace.properties": "Workspace properties" + "com.affine.workspace.properties": "Workspace properties", + "com.affine.m.rename-to": "Rename to \"{{name}}\"", + "com.affine.m.explorer.folder.rename": "Rename", + "com.affine.m.explorer.folder.new-dialog-title": "Create Folder", + "com.affine.m.explorer.folder.root": "Organize", + "com.affine.m.explorer.folder.new-tip-empty": "Create a folder in the {{parent}}.", + "com.affine.m.explorer.folder.new-tip-not-empty": "Create \"{{value}}\" in the {{parent}}.", + "com.affine.m.explorer.folder.rename-confirm": "Done", + "com.affine.m.explorer.tag.rename": "Rename", + "com.affine.m.explorer.tag.rename-menu-title": "Rename Tag", + "com.affine.m.explorer.tag.new-dialog-title": "Create Tag", + "com.affine.m.explorer.tag.rename-confirm": "Done", + "com.affine.m.explorer.tag.new-tip-empty": "Create a tag in this workspace.", + "com.affine.m.explorer.tag.new-tip-not-empty": "Create \"{{value}}\" tag in this workspace.", + "com.affine.m.explorer.collection.rename": "Rename", + "com.affine.m.explorer.collection.rename-menu-title": "Rename Collection", + "com.affine.m.explorer.collection.new-dialog-title": "Create Collection", + "com.affine.m.explorer.doc.rename": "Rename" }