From 60fc5bb2c8463a5f66aaeb4c2d5d0a9e4e1211fe Mon Sep 17 00:00:00 2001 From: "Kilu.He" <108015703+qinluhe@users.noreply.github.com> Date: Thu, 8 Feb 2024 14:22:44 +0800 Subject: [PATCH] fix: tauri folder bugs (#4589) --- .../appflowy_tauri/src-tauri/tauri.conf.json | 5 +- .../src/appflowy_app/AppMain.hooks.ts | 73 +++++---- .../application/document/document.types.ts | 1 + .../application/folder/page.service.ts | 3 +- .../src/appflowy_app/assets/details.svg | 6 +- .../_shared/button_menu/ButtonMenu.tsx | 69 --------- .../DeleteConfirmDialog.tsx | 44 ++++-- .../confirm_dialog}/RenameDialog.tsx | 39 ++--- .../_shared/drag_block/drag.hooks.ts | 12 +- .../KeyboardNavigation.tsx | 12 +- .../components/database/Database.tsx | 82 +++++++++-- .../components/property/PropertyActions.tsx | 2 +- .../components/tab_bar/DatabaseTabBar.tsx | 28 ++-- .../components/tab_bar/ViewActions.tsx | 47 +++--- .../document_header/DocumentHeader.tsx | 2 +- .../blocks/database/DatabaseList.hooks.ts | 23 +-- .../editor/components/blocks/text/Text.tsx | 2 +- .../inline_formula/InlineFormula.tsx | 2 +- .../inline_nodes/mention/MentionLeaf.tsx | 105 +++++++++++-- .../block_actions/BlockActionsToolbar.tsx | 5 +- .../block_actions/BlockOperationMenu.tsx | 3 +- .../components/editor/editor.scss | 25 +++- .../editor/plugins/shortcuts/hotkey.ts | 14 +- .../plugins/shortcuts/shortcuts.hooks.ts | 3 +- .../components/layout/FooterPanel.tsx | 8 +- .../appflowy_app/components/layout/Layout.tsx | 4 +- .../layout/bread_crumb/BreadCrumb.tsx | 19 ++- .../layout/bread_crumb/Breadcrumb.hooks.ts | 13 +- .../CollapseMenuButton.tsx | 47 ++++-- .../components/layout/layout.scss | 12 ++ .../layout/nested_page/AddButton.tsx | 85 +++++------ .../layout/nested_page/DeleteDialog.tsx | 2 +- .../layout/nested_page/MoreButton.tsx | 138 ++++++++++-------- .../layout/nested_page/NestedPage.hooks.ts | 59 +++++--- .../layout/nested_page/NestedPage.tsx | 42 ++++-- .../layout/nested_page/NestedPageTitle.tsx | 84 +++++++---- .../layout/nested_page/OperationMenu.tsx | 103 +++++++++++++ .../components/layout/side_bar/Resizer.tsx | 1 + .../components/layout/side_bar/SideBar.tsx | 7 +- .../components/layout/side_bar/UserInfo.tsx | 55 ++++--- .../components/layout/top_bar/TopBar.tsx | 9 -- .../layout/user_setting/AppearanceSetting.tsx | 21 ++- .../layout/user_setting/LanguageSetting.tsx | 2 +- .../components/layout/user_setting/Menu.tsx | 4 +- .../layout/user_setting/UserSetting.tsx | 9 +- .../layout/workspace_manager/MoreButton.tsx | 64 -------- .../layout/workspace_manager/NestedPages.tsx | 2 +- .../workspace_manager/NewPageButton.tsx | 40 +++-- .../layout/workspace_manager/TrashButton.tsx | 6 +- .../workspace_manager/Workspace.hooks.ts | 22 ++- .../layout/workspace_manager/Workspace.tsx | 51 ++++++- .../workspace_manager/WorkspaceManager.tsx | 11 +- .../workspace_manager/WorkspaceTitle.tsx | 38 ----- .../appflowy_app/components/trash/Trash.tsx | 9 +- .../components/trash/TrashItem.tsx | 10 +- .../src/appflowy_app/hooks/page.hooks.tsx | 26 ++++ .../stores/reducers/current-user/slice.ts | 1 + .../stores/reducers/pages/async_actions.ts | 4 +- .../stores/reducers/pages/slice.ts | 81 +++++++++- .../src/appflowy_app/utils/get_modifier.ts | 12 ++ .../src/appflowy_app/utils/mui.ts | 5 +- .../appflowy_tauri/src/styles/template.css | 4 - frontend/resources/translations/en.json | 4 +- 63 files changed, 1050 insertions(+), 671 deletions(-) delete mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/_shared/button_menu/ButtonMenu.tsx rename frontend/appflowy_tauri/src/appflowy_app/components/_shared/{delete_confirm_dialog => confirm_dialog}/DeleteConfirmDialog.tsx (53%) rename frontend/appflowy_tauri/src/appflowy_app/components/{layout/nested_page => _shared/confirm_dialog}/RenameDialog.tsx (66%) create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/layout/layout.scss create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/layout/nested_page/OperationMenu.tsx delete mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/layout/workspace_manager/MoreButton.tsx delete mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/layout/workspace_manager/WorkspaceTitle.tsx create mode 100644 frontend/appflowy_tauri/src/appflowy_app/hooks/page.hooks.tsx create mode 100644 frontend/appflowy_tauri/src/appflowy_app/utils/get_modifier.ts diff --git a/frontend/appflowy_tauri/src-tauri/tauri.conf.json b/frontend/appflowy_tauri/src-tauri/tauri.conf.json index 7e9b0692a3..c0da4386af 100644 --- a/frontend/appflowy_tauri/src-tauri/tauri.conf.json +++ b/frontend/appflowy_tauri/src-tauri/tauri.conf.json @@ -19,7 +19,10 @@ }, "fs": { "all": true, - "scope": ["$APPLOCALDATA/**", "$APPLOCALDATA/images/*"], + "scope": [ + "$APPLOCALDATA/**", + "$APPLOCALDATA/images/*" + ], "readFile": true, "writeFile": true, "readDir": true, diff --git a/frontend/appflowy_tauri/src/appflowy_app/AppMain.hooks.ts b/frontend/appflowy_tauri/src/appflowy_app/AppMain.hooks.ts index cd61b2108d..8d4088ca25 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/AppMain.hooks.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/AppMain.hooks.ts @@ -1,64 +1,59 @@ import { useAppDispatch, useAppSelector } from '$app/stores/store'; -import { useCallback, useEffect, useMemo } from 'react'; +import { useEffect, useMemo } from 'react'; import { currentUserActions } from '$app_reducers/current-user/slice'; import { Theme as ThemeType, ThemeMode } from '$app/stores/reducers/current-user/slice'; import { createTheme } from '@mui/material/styles'; import { getDesignTokens } from '$app/utils/mui'; import { useTranslation } from 'react-i18next'; -import { ThemeModePB } from '@/services/backend'; import { UserService } from '$app/application/user/user.service'; export function useUserSetting() { const dispatch = useAppDispatch(); const { i18n } = useTranslation(); - - const handleSystemThemeChange = useCallback(() => { - const mode = window.matchMedia('(prefers-color-scheme: dark)').matches ? ThemeMode.Dark : ThemeMode.Light; - - dispatch(currentUserActions.setUserSetting({ themeMode: mode })); - }, [dispatch]); - - const loadUserSetting = useCallback(async () => { - const settings = await UserService.getAppearanceSetting(); - - if (!settings) return; - dispatch(currentUserActions.setUserSetting(settings)); - - if (settings.themeMode === ThemeModePB.System) { - const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); - - handleSystemThemeChange(); - - mediaQuery.addEventListener('change', handleSystemThemeChange); - } - - await i18n.changeLanguage(settings.language); - }, [dispatch, handleSystemThemeChange, i18n]); - - useEffect(() => { - void loadUserSetting(); - }, [loadUserSetting]); - - const { themeMode = ThemeMode.Light, theme: themeType = ThemeType.Default } = useAppSelector((state) => { + const { + themeMode = ThemeMode.System, + isDark = false, + theme: themeType = ThemeType.Default, + } = useAppSelector((state) => { return state.currentUser.userSetting || {}; }); useEffect(() => { - const html = document.documentElement; + void (async () => { + const settings = await UserService.getAppearanceSetting(); - html?.setAttribute('data-dark-mode', String(themeMode === ThemeMode.Dark)); - html?.setAttribute('data-theme', themeType); - }, [themeType, themeMode]); + if (!settings) return; + dispatch(currentUserActions.setUserSetting(settings)); + await i18n.changeLanguage(settings.language); + })(); + }, [dispatch, i18n]); useEffect(() => { - return () => { - const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); + const html = document.documentElement; + html?.setAttribute('data-dark-mode', String(isDark)); + }, [isDark]); + + useEffect(() => { + const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); + + const handleSystemThemeChange = () => { + if (themeMode !== ThemeMode.System) return; + dispatch( + currentUserActions.setUserSetting({ + isDark: mediaQuery.matches, + }) + ); + }; + + mediaQuery.addEventListener('change', handleSystemThemeChange); + + return () => { mediaQuery.removeEventListener('change', handleSystemThemeChange); }; - }, [dispatch, handleSystemThemeChange]); + }, [dispatch, themeMode]); - const muiTheme = useMemo(() => createTheme(getDesignTokens(themeMode)), [themeMode]); + const muiTheme = useMemo(() => createTheme(getDesignTokens(isDark)), [isDark]); return { muiTheme, diff --git a/frontend/appflowy_tauri/src/appflowy_app/application/document/document.types.ts b/frontend/appflowy_tauri/src/appflowy_app/application/document/document.types.ts index 4497dfff80..98b493581d 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/application/document/document.types.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/application/document/document.types.ts @@ -138,6 +138,7 @@ export interface MentionPage { id: string; name: string; layout: ViewLayoutPB; + parentId: string; icon?: { ty: ViewIconTypePB; value: string; diff --git a/frontend/appflowy_tauri/src/appflowy_app/application/folder/page.service.ts b/frontend/appflowy_tauri/src/appflowy_app/application/folder/page.service.ts index b529571c91..25aa0033a4 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/application/folder/page.service.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/application/folder/page.service.ts @@ -49,8 +49,7 @@ export const createOrphanPage = async ( return Promise.reject(result.val); }; -export const duplicatePage = async (id: string) => { - const page = await getPage(id); +export const duplicatePage = async (page: Page) => { const payload = ViewPB.fromObject(page); const result = await FolderEventDuplicateView(payload); diff --git a/frontend/appflowy_tauri/src/appflowy_app/assets/details.svg b/frontend/appflowy_tauri/src/appflowy_app/assets/details.svg index af6127ce5d..22c6830916 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/assets/details.svg +++ b/frontend/appflowy_tauri/src/appflowy_app/assets/details.svg @@ -1,5 +1,5 @@ - - - + + + diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/button_menu/ButtonMenu.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/button_menu/ButtonMenu.tsx deleted file mode 100644 index c099eb3d5d..0000000000 --- a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/button_menu/ButtonMenu.tsx +++ /dev/null @@ -1,69 +0,0 @@ -import React, { useCallback, useState } from 'react'; -import { List, MenuItem, Popover, Portal, Theme } from '@mui/material'; -import { PopoverOrigin } from '@mui/material/Popover/Popover'; -import { SxProps } from '@mui/system'; - -interface ButtonPopoverListProps { - isVisible: boolean; - children: React.ReactNode; - popoverOptions: { - key: React.Key; - icon: React.ReactNode; - label: React.ReactNode | string; - onClick: () => void; - }[]; - popoverOrigin: { - anchorOrigin: PopoverOrigin; - transformOrigin: PopoverOrigin; - }; - onClose?: () => void; - sx?: SxProps; -} - -function ButtonPopoverList({ popoverOrigin, isVisible, children, popoverOptions, onClose, sx }: ButtonPopoverListProps) { - const [anchorEl, setAnchorEl] = useState(); - const open = Boolean(anchorEl); - const visible = isVisible || open; - const handleClick = (event: React.MouseEvent) => { - setAnchorEl(event.currentTarget); - }; - - const handleClose = useCallback(() => { - setAnchorEl(undefined); - }, []); - - return ( - <> - {visible &&
{children}
} - - { - handleClose(); - onClose?.(); - }} - > - - {popoverOptions.map((option) => ( - { - option.onClick(); - handleClose(); - }} - className={'flex items-center gap-1 rounded-none px-2 text-xs font-medium'} - > - {option.icon} - {option.label} - - ))} - - - - - ); -} - -export default ButtonPopoverList; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/delete_confirm_dialog/DeleteConfirmDialog.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/confirm_dialog/DeleteConfirmDialog.tsx similarity index 53% rename from frontend/appflowy_tauri/src/appflowy_app/components/_shared/delete_confirm_dialog/DeleteConfirmDialog.tsx rename to frontend/appflowy_tauri/src/appflowy_app/components/_shared/confirm_dialog/DeleteConfirmDialog.tsx index 5723505b4c..b46ce37345 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/delete_confirm_dialog/DeleteConfirmDialog.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/confirm_dialog/DeleteConfirmDialog.tsx @@ -1,8 +1,9 @@ -import React from 'react'; +import React, { useCallback } from 'react'; import DialogContent from '@mui/material/DialogContent'; import { Button, DialogActions, Divider } from '@mui/material'; import Dialog from '@mui/material/Dialog'; import { useTranslation } from 'react-i18next'; +import { Log } from '$app/utils/log'; interface Props { open: boolean; @@ -15,9 +16,36 @@ interface Props { function DeleteConfirmDialog({ open, title, subtitle, onOk, onClose }: Props) { const { t } = useTranslation(); + const onDone = useCallback(async () => { + try { + await onOk(); + onClose(); + } catch (e) { + Log.error(e); + } + }, [onClose, onOk]); + return ( - e.stopPropagation()} open={open} onClose={onClose}> - + { + if (e.key === 'Escape') { + e.preventDefault(); + e.stopPropagation(); + onClose(); + } + + if (e.key === 'Enter') { + e.preventDefault(); + e.stopPropagation(); + void onDone(); + } + }} + onMouseDown={(e) => e.stopPropagation()} + open={open} + onClose={onClose} + > +
{title}
{subtitle &&
{subtitle}
}
@@ -26,15 +54,7 @@ function DeleteConfirmDialog({ open, title, subtitle, onOk, onClose }: Props) { - diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/layout/nested_page/RenameDialog.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/confirm_dialog/RenameDialog.tsx similarity index 66% rename from frontend/appflowy_tauri/src/appflowy_app/components/layout/nested_page/RenameDialog.tsx rename to frontend/appflowy_tauri/src/appflowy_app/components/_shared/confirm_dialog/RenameDialog.tsx index 71c00878f8..4c99d37e87 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/layout/nested_page/RenameDialog.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/confirm_dialog/RenameDialog.tsx @@ -1,10 +1,10 @@ -import React, { useEffect, useState } from 'react'; +import React, { useCallback, useEffect, useState } from 'react'; import DialogTitle from '@mui/material/DialogTitle'; import DialogContent from '@mui/material/DialogContent'; import Dialog from '@mui/material/Dialog'; import { useTranslation } from 'react-i18next'; import TextField from '@mui/material/TextField'; -import { Button, DialogActions } from '@mui/material'; +import { Button, DialogActions, Divider } from '@mui/material'; function RenameDialog({ defaultValue, @@ -25,20 +25,31 @@ function RenameDialog({ setValue(defaultValue); setError(false); }, [defaultValue]); + + const onDone = useCallback(async () => { + try { + await onOk(value); + onClose(); + } catch (e) { + setError(true); + } + }, [onClose, onOk, value]); + return ( e.stopPropagation()} open={open} onClose={onClose}> - {t('menuAppHeader.renameDialog')} - + {t('menuAppHeader.renameDialog')} + { e.stopPropagation(); if (e.key === 'Enter') { e.preventDefault(); - void onOk(value); + void onDone(); } if (e.key === 'Escape') { @@ -54,18 +65,12 @@ function RenameDialog({ variant='standard' /> - - - - + diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/drag_block/drag.hooks.ts b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/drag_block/drag.hooks.ts index b7607616de..85f0507fff 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/drag_block/drag.hooks.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/drag_block/drag.hooks.ts @@ -24,8 +24,12 @@ export function useDrag(props: Props) { setIsDraggingOver(false); setIsDragging(false); setDropPosition(undefined); + const currentTarget = e.currentTarget; + + if (currentTarget.parentElement?.closest(`[data-drop-enabled="false"]`)) return; + if (currentTarget.closest(`[data-dragging="true"]`)) return; const dragId = e.dataTransfer.getData('dragId'); - const targetRect = e.currentTarget.getBoundingClientRect(); + const targetRect = currentTarget.getBoundingClientRect(); const { clientY } = e; const position = calcPosition(targetRect, clientY); @@ -37,8 +41,12 @@ export function useDrag(props: Props) { e.stopPropagation(); e.preventDefault(); if (isDragging) return; + const currentTarget = e.currentTarget; + + if (currentTarget.parentElement?.closest(`[data-drop-enabled="false"]`)) return; + if (currentTarget.closest(`[data-dragging="true"]`)) return; setIsDraggingOver(true); - const targetRect = e.currentTarget.getBoundingClientRect(); + const targetRect = currentTarget.getBoundingClientRect(); const { clientY } = e; const position = calcPosition(targetRect, clientY); diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/keyboard_navigation/KeyboardNavigation.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/keyboard_navigation/KeyboardNavigation.tsx index df6485ab3b..b7c6db9db0 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/keyboard_navigation/KeyboardNavigation.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/keyboard_navigation/KeyboardNavigation.tsx @@ -1,7 +1,6 @@ import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { MenuItem, Typography } from '@mui/material'; import { scrollIntoView } from '$app/components/_shared/keyboard_navigation/utils'; -import { ReactEditor, useSlateStatic } from 'slate-react'; import { useTranslation } from 'react-i18next'; /** @@ -35,7 +34,7 @@ export interface KeyboardNavigationOption { * - onBlur: called when the keyboard navigation is blurred */ export interface KeyboardNavigationProps { - scrollRef: React.RefObject; + scrollRef?: React.RefObject; focusRef?: React.RefObject; options: KeyboardNavigationOption[]; onSelected?: (optionKey: T) => void; @@ -68,7 +67,6 @@ function KeyboardNavigation({ onFocus, }: KeyboardNavigationProps) { const { t } = useTranslation(); - const editor = useSlateStatic(); const ref = useRef(null); const mouseY = useRef(null); const defaultKeyRef = useRef(defaultFocusedKey); @@ -108,7 +106,7 @@ function KeyboardNavigation({ if (focusedKey === undefined) return; onSelected?.(focusedKey); - const scrollElement = scrollRef.current; + const scrollElement = scrollRef?.current; if (!scrollElement) return; @@ -262,15 +260,15 @@ function KeyboardNavigation({ let element: HTMLElement | null | undefined = focusRef?.current; if (!element) { - element = ReactEditor.toDOMNode(editor, editor); + element = document.activeElement as HTMLElement; } - element.addEventListener('keydown', onKeyDown); + element?.addEventListener('keydown', onKeyDown); return () => { element?.removeEventListener('keydown', onKeyDown); }; } - }, [disableFocus, editor, onKeyDown, focusRef]); + }, [disableFocus, onKeyDown, focusRef]); return (
(({ selectedViewId, setSelectedViewId }, ref) => { const innerRef = useRef(); const databaseRef = (ref ?? innerRef) as React.MutableRefObject; - const viewId = useViewId(); + + const [page, setPage] = useState(null); const { t } = useTranslation(); const [notFound, setNotFound] = useState(false); - const [childViewIds, setChildViewIds] = useState([]); + const [childViews, setChildViews] = useState([]); const [editRecordRowId, setEditRecordRowId] = useState(null); const [openCollections, setOpenCollections] = useState([]); @@ -34,7 +37,7 @@ export const Database = forwardRef(({ selectedViewId, set await databaseViewService .getDatabaseViews(viewId) .then((value) => { - setChildViewIds(value.map((view) => view.id)); + setChildViews(value); }) .catch((err) => { if (err.code === ErrorCode.RecordNotFound) { @@ -43,10 +46,39 @@ export const Database = forwardRef(({ selectedViewId, set }); }, []); + const handleGetPage = useCallback(async () => { + try { + const page = await getPage(viewId); + + setPage(page); + } catch (e) { + setNotFound(true); + } + }, [viewId]); + useEffect(() => { + void handleGetPage(); void handleResetDatabaseViews(viewId); const unsubscribePromise = subscribeNotifications( { + [FolderNotification.DidUpdateView]: (changeset) => { + setChildViews((prev) => { + const index = prev.findIndex((view) => view.id === changeset.id); + + if (index === -1) { + return prev; + } + + const newViews = [...prev]; + + newViews[index] = { + ...newViews[index], + name: changeset.name, + }; + + return newViews; + }); + }, [FolderNotification.DidUpdateChildViews]: (changeset) => { if (changeset.create_child_views.length === 0 && changeset.delete_child_views.length === 0) { return; @@ -61,11 +93,35 @@ export const Database = forwardRef(({ selectedViewId, set ); return () => void unsubscribePromise.then((unsubscribe) => unsubscribe()); - }, [handleResetDatabaseViews, viewId]); + }, [handleGetPage, handleResetDatabaseViews, viewId]); + + useEffect(() => { + const parentId = page?.parentId; + + if (!parentId) return; + + const unsubscribePromise = subscribeNotifications( + { + [FolderNotification.DidUpdateChildViews]: (changeset) => { + if (changeset.delete_child_views.includes(viewId)) { + setNotFound(true); + } + }, + }, + { + id: parentId, + } + ); + + return () => void unsubscribePromise.then((unsubscribe) => unsubscribe()); + }, [page, viewId]); const value = useMemo(() => { - return Math.max(0, childViewIds.indexOf(selectedViewId ?? viewId)); - }, [childViewIds, selectedViewId, viewId]); + return Math.max( + 0, + childViews.findIndex((view) => view.id === (selectedViewId ?? viewId)) + ); + }, [childViews, selectedViewId, viewId]); const onToggleCollection = useCallback( (id: string, forceOpen?: boolean) => { @@ -110,7 +166,7 @@ export const Database = forwardRef(({ selectedViewId, set pageId={viewId} setSelectedViewId={setSelectedViewId} selectedViewId={selectedViewId} - childViewIds={childViewIds} + childViews={childViews} /> (({ selectedViewId, set axis={'x'} index={value} > - {childViewIds.map((id, index) => ( - - - {selectedViewId === id && ( + {childViews.map((view, index) => ( + + + {selectedViewId === view.id && ( <>
onToggleCollection(id, forceOpen)} + onToggleCollection={(forceOpen?: boolean) => onToggleCollection(view.id, forceOpen)} />
- + {editRecordRowId && ( void; pageId: string; @@ -26,26 +25,21 @@ const DatabaseIcons: { [ViewLayoutPB.Calendar]: GridSvg, }; -export const DatabaseTabBar: FC = ({ pageId, childViewIds, selectedViewId, setSelectedViewId }) => { +export const DatabaseTabBar: FC = ({ pageId, childViews, selectedViewId, setSelectedViewId }) => { const { t } = useTranslation(); const [contextMenuAnchorEl, setContextMenuAnchorEl] = useState(null); const [contextMenuView, setContextMenuView] = useState(null); const open = Boolean(contextMenuAnchorEl); - const views = useAppSelector((state) => { - const map = state.pages.pageMap; - - return childViewIds.map((id) => map[id]).filter(Boolean); - }); const handleChange = (_: React.SyntheticEvent, newValue: string) => { setSelectedViewId?.(newValue); }; useEffect(() => { - if (selectedViewId === undefined && views.length > 0) { - setSelectedViewId?.(views[0].id); + if (selectedViewId === undefined && childViews.length > 0) { + setSelectedViewId?.(childViews[0].id); } - }, [selectedViewId, setSelectedViewId, views]); + }, [selectedViewId, setSelectedViewId, childViews]); const openMenu = (view: Page) => { return (e: React.MouseEvent) => { @@ -56,16 +50,19 @@ export const DatabaseTabBar: FC = ({ pageId, childViewIds, }; }; + const isSelected = useMemo(() => childViews.some((view) => view.id === selectedViewId), [childViews, selectedViewId]); + + if (childViews.length === 0) return null; return (
- - {views.map((view) => { + + {childViews.map((view) => { const Icon = DatabaseIcons[view.layout]; return ( } @@ -81,6 +78,7 @@ export const DatabaseTabBar: FC = ({ pageId, childViewIds,
{open && contextMenuView && ( , action: async () => { @@ -48,31 +49,33 @@ function ViewActions({ view, ...props }: { view: Page } & MenuProps) { <> {options.map((option) => ( - +
{option.icon}
{option.label}
))}
- setOpenRenameDialog(false)} - onOk={async (val) => { - try { - await dispatch( - updatePageName({ - id: viewId, - name: val, - }) - ); - setOpenRenameDialog(false); - props.onClose?.({}, 'backdropClick'); - } catch (e) { - // toast.error(t('error.renameView')); - } - }} - defaultValue={view.name} - /> + {openRenameDialog && ( + setOpenRenameDialog(false)} + onOk={async (val) => { + try { + await dispatch( + updatePageName({ + id: viewId, + name: val, + }) + ); + setOpenRenameDialog(false); + props.onClose?.({}, 'backdropClick'); + } catch (e) { + // toast.error(t('error.renameView')); + } + }} + defaultValue={view.name} + /> + )} ); } diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/document_header/DocumentHeader.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/document/document_header/DocumentHeader.tsx index 209e5fd694..a944547870 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/document/document_header/DocumentHeader.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/document_header/DocumentHeader.tsx @@ -19,7 +19,7 @@ export function DocumentHeader({ page }: DocumentHeaderProps) { if (!page) return null; return ( -
+
); diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/database/DatabaseList.hooks.ts b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/database/DatabaseList.hooks.ts index e9af150312..c4753f9124 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/database/DatabaseList.hooks.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/database/DatabaseList.hooks.ts @@ -1,24 +1,13 @@ import { useAppSelector } from '$app/stores/store'; -import { useEffect, useState } from 'react'; -import { Page } from '$app_reducers/pages/slice'; import { ViewLayoutPB } from '@/services/backend'; export function useLoadDatabaseList({ searchText, layout }: { searchText: string; layout: ViewLayoutPB }) { - const [list, setList] = useState([]); - const pages = useAppSelector((state) => state.pages.pageMap); - - useEffect(() => { - const list = Object.values(pages) - .map((page) => { - return page; - }) - .filter((page) => { - if (page.layout !== layout) return false; - return page.name.toLowerCase().includes(searchText.toLowerCase()); - }); - - setList(list); - }, [layout, pages, searchText]); + const list = useAppSelector((state) => { + return Object.values(state.pages.pageMap).filter((page) => { + if (page.layout !== layout) return false; + return page.name.toLowerCase().includes(searchText.toLowerCase()); + }); + }); return { list, diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/text/Text.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/text/Text.tsx index 4e1642a8da..bf8df3c8e7 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/text/Text.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/text/Text.tsx @@ -21,7 +21,7 @@ export const Text = memo( {renderIcon()} - {children} + {children} ); }) diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/inline_nodes/inline_formula/InlineFormula.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/inline_nodes/inline_formula/InlineFormula.tsx index fe2eba8a59..89ae42291a 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/inline_nodes/inline_formula/InlineFormula.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/inline_nodes/inline_formula/InlineFormula.tsx @@ -59,7 +59,7 @@ export const InlineFormula = memo( contentEditable={false} onDoubleClick={handleClick} onClick={handleClick} - className={`${attributes.className ?? ''} formula-inline relative rounded px-1 py-0.5 ${ + className={`${attributes.className ?? ''} formula-inline relative cursor-pointer rounded px-1 py-0.5 ${ selected ? 'selected' : '' }`} > diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/inline_nodes/mention/MentionLeaf.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/inline_nodes/mention/MentionLeaf.tsx index 4a4baebf2a..fb6f630214 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/inline_nodes/mention/MentionLeaf.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/inline_nodes/mention/MentionLeaf.tsx @@ -6,17 +6,29 @@ import { useNavigate } from 'react-router-dom'; import { pageTypeMap } from '$app_reducers/pages/slice'; import { getPage } from '$app/application/folder/page.service'; import { useSelected } from 'slate-react'; +import { ReactComponent as EyeClose } from '$app/assets/eye_close.svg'; +import { notify } from '$app/components/editor/components/tools/notify'; +import { subscribeNotifications } from '$app/application/notification'; +import { FolderNotification } from '@/services/backend'; export function MentionLeaf({ children, mention }: { mention: Mention; children: React.ReactNode }) { const { t } = useTranslation(); const [page, setPage] = useState(null); + const [error, setError] = useState(false); const navigate = useNavigate(); const selected = useSelected(); const loadPage = useCallback(async () => { + setError(true); if (!mention.page) return; - const page = await getPage(mention.page); + try { + const page = await getPage(mention.page); - setPage(page); + setPage(page); + setError(false); + } catch { + setPage(null); + setError(true); + } }, [mention.page]); useEffect(() => { @@ -24,26 +36,87 @@ export function MentionLeaf({ children, mention }: { mention: Mention; children: }, [loadPage]); const openPage = useCallback(() => { - if (!page) return; + if (!page) { + notify.error(t('document.mention.deletedContent')); + return; + } + const pageType = pageTypeMap[page.layout]; navigate(`/page/${pageType}/${page.id}`); - }, [navigate, page]); + }, [navigate, page, t]); + + useEffect(() => { + if (!page) return; + const unsubscribePromise = subscribeNotifications( + { + [FolderNotification.DidUpdateView]: (changeset) => { + setPage((prev) => { + if (!prev) { + return prev; + } + + return { + ...prev, + name: changeset.name, + }; + }); + }, + }, + { + id: page.id, + } + ); + + return () => void unsubscribePromise.then((unsubscribe) => unsubscribe()); + }, [page]); + + useEffect(() => { + const parentId = page?.parentId; + + if (!parentId) return; + + const unsubscribePromise = subscribeNotifications( + { + [FolderNotification.DidUpdateChildViews]: (changeset) => { + if (changeset.delete_child_views.includes(page.id)) { + setPage(null); + setError(true); + } + }, + }, + { + id: parentId, + } + ); + + return () => void unsubscribePromise.then((unsubscribe) => unsubscribe()); + }, [page]); return ( - {page && ( - - {page.icon?.value || } - {page.name || t('document.title.placeholder')} - - )} + + {page && ( + <> + {page.icon?.value || } + {page.name || t('document.title.placeholder')} + + )} + {error && ( + <> + + + + {t('document.mention.deleted')} + + )} + {children} diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/block_actions/BlockActionsToolbar.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/block_actions/BlockActionsToolbar.tsx index 11e41246e5..2f5f7a19d6 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/block_actions/BlockActionsToolbar.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/block_actions/BlockActionsToolbar.tsx @@ -22,9 +22,12 @@ const Toolbar = () => { const handleOpen = useCallback(() => { if (!node || !node.blockId) return; setOpenContextMenu(true); + const path = ReactEditor.findPath(editor, node); + + editor.select(path); selectedBlockContext.clear(); selectedBlockContext.add(node.blockId); - }, [node, selectedBlockContext]); + }, [editor, node, selectedBlockContext]); const handleClose = useCallback(() => { setOpenContextMenu(false); diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/block_actions/BlockOperationMenu.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/block_actions/BlockOperationMenu.tsx index 15dc3214ef..624b9ff0f1 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/block_actions/BlockOperationMenu.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/block_actions/BlockOperationMenu.tsx @@ -12,7 +12,8 @@ import KeyboardNavigation, { KeyboardNavigationOption, } from '$app/components/_shared/keyboard_navigation/KeyboardNavigation'; import { Color } from '$app/components/editor/components/tools/block_actions/color'; -import { getModifier } from '$app/components/editor/plugins/shortcuts'; +import { getModifier } from '$app/utils/get_modifier'; + import isHotkey from 'is-hotkey'; import { EditorNodeType } from '$app/application/document/document.types'; import { EditorSelectedBlockContext } from '$app/components/editor/stores/selected'; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/editor.scss b/frontend/appflowy_tauri/src/appflowy_app/components/editor/editor.scss index 6cf7ec5e9f..33e1e5fbe8 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/editor/editor.scss +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/editor.scss @@ -25,10 +25,8 @@ .block-element.block-align-center { > div > .text-element { justify-content: center; - } - } @@ -52,12 +50,35 @@ span[data-slate-placeholder="true"]:not(.inline-block-content) { ::selection { @apply bg-content-blue-100; } + .text-content { + &::selection { + @apply bg-transparent; + } + span { + &::selection { + @apply bg-content-blue-100; + } + } + } } + + [data-dark-mode="true"] [role="textbox"]{ ::selection { background-color: #1e79a2; } + + .text-content { + &::selection { + @apply bg-transparent; + } + span { + &::selection { + background-color: #1e79a2; + } + } + } } diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/plugins/shortcuts/hotkey.ts b/frontend/appflowy_tauri/src/appflowy_app/components/editor/plugins/shortcuts/hotkey.ts index 0fb85a84ce..c0a401ebfd 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/editor/plugins/shortcuts/hotkey.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/plugins/shortcuts/hotkey.ts @@ -1,17 +1,5 @@ import { EditorMarkFormat } from '$app/application/document/document.types'; - -export const isMac = () => { - return navigator.userAgent.includes('Mac OS X'); -}; - -const MODIFIERS = { - control: 'Ctrl', - meta: '⌘', -}; - -export const getModifier = () => { - return isMac() ? MODIFIERS.meta : MODIFIERS.control; -}; +import { getModifier } from '$app/utils/get_modifier'; /** * Hotkeys shortcuts diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/plugins/shortcuts/shortcuts.hooks.ts b/frontend/appflowy_tauri/src/appflowy_app/components/editor/plugins/shortcuts/shortcuts.hooks.ts index 437ec6a576..f375bd9f3c 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/editor/plugins/shortcuts/shortcuts.hooks.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/plugins/shortcuts/shortcuts.hooks.ts @@ -47,6 +47,7 @@ export function useShortcuts(editor: ReactEditor) { if (format) { e.preventDefault(); + if (CustomEditor.selectionIncludeRoot(editor)) return; return CustomEditor.toggleMark(editor, { key: format, value: true, @@ -66,7 +67,7 @@ export function useShortcuts(editor: ReactEditor) { if (isHotkey(item.hotkey, e)) { e.stopPropagation(); e.preventDefault(); - + if (CustomEditor.selectionIncludeRoot(editor)) return; if (item.markKey === EditorMarkFormat.Align) { CustomEditor.toggleAlign(editor, item.markValue as string); return; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/layout/FooterPanel.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/layout/FooterPanel.tsx index 57ed692912..4f468f5461 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/layout/FooterPanel.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/layout/FooterPanel.tsx @@ -1,12 +1,12 @@ export const FooterPanel = () => { return ( -
+
© 2024 AppFlowy. GitHub
-
- -
+ {/*
*/} + {/* */} + {/*
*/}
); }; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/layout/Layout.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/layout/Layout.tsx index a0a508a975..c5bbb17424 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/layout/Layout.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/layout/Layout.tsx @@ -2,7 +2,7 @@ import React, { ReactNode, useEffect } from 'react'; import SideBar from '$app/components/layout/side_bar/SideBar'; import TopBar from '$app/components/layout/top_bar/TopBar'; import { useAppSelector } from '$app/stores/store'; -import { FooterPanel } from '$app/components/layout/FooterPanel'; +import './layout.scss'; function Layout({ children }: { children: ReactNode }) { const { isCollapsed, width } = useAppSelector((state) => state.sidebar); @@ -38,8 +38,6 @@ function Layout({ children }: { children: ReactNode }) { > {children}
- -
diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/layout/bread_crumb/BreadCrumb.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/layout/bread_crumb/BreadCrumb.tsx index 5ef81d2e17..986ba9337d 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/layout/bread_crumb/BreadCrumb.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/layout/bread_crumb/BreadCrumb.tsx @@ -6,10 +6,11 @@ import Typography from '@mui/material/Typography'; import { Page, pageTypeMap } from '$app_reducers/pages/slice'; import { useNavigate } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; +import { getPageIcon } from '$app/hooks/page.hooks'; function Breadcrumb() { const { t } = useTranslation(); - const { pagePath, currentPage } = useLoadExpandedPages(); + const { isTrash, pagePath, currentPage } = useLoadExpandedPages(); const navigate = useNavigate(); const parentPages = useMemo(() => pagePath.slice(1, -1).filter(Boolean) as Page[], [pagePath]); @@ -22,21 +23,35 @@ function Breadcrumb() { [navigate] ); + if (!currentPage) { + if (isTrash) { + return {t('trash.text')}; + } + + return null; + } + return ( {parentPages?.map((page: Page) => ( { navigateToPage(page); }} > +
{getPageIcon(page)}
+ {page.name || t('document.title.placeholder')} ))} - {currentPage?.name || t('menuAppHeader.defaultNewPageName')} + +
{getPageIcon(currentPage)}
+ {currentPage?.name || t('menuAppHeader.defaultNewPageName')} +
); } diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/layout/bread_crumb/Breadcrumb.hooks.ts b/frontend/appflowy_tauri/src/appflowy_app/components/layout/bread_crumb/Breadcrumb.hooks.ts index 807a8f2f65..5d65e6ef08 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/layout/bread_crumb/Breadcrumb.hooks.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/components/layout/bread_crumb/Breadcrumb.hooks.ts @@ -2,11 +2,9 @@ import { useAppSelector } from '$app/stores/store'; import { useCallback, useEffect, useMemo, useState } from 'react'; import { useParams, useLocation } from 'react-router-dom'; import { Page } from '$app_reducers/pages/slice'; -import { useTranslation } from 'react-i18next'; import { getPage } from '$app/application/folder/page.service'; export function useLoadExpandedPages() { - const { t } = useTranslation(); const params = useParams(); const location = useLocation(); const isTrash = useMemo(() => location.pathname.includes('trash'), [location.pathname]); @@ -70,18 +68,9 @@ export function useLoadExpandedPages() { }); }, [pageMap]); - useEffect(() => { - if (isTrash) { - setPagePath([ - { - name: t('trash.text'), - }, - ]); - } - }, [isTrash, t]); - return { pagePath, currentPage, + isTrash, }; } diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/layout/collapse_menu_button/CollapseMenuButton.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/layout/collapse_menu_button/CollapseMenuButton.tsx index 850ac0b703..e1b6f9e414 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/layout/collapse_menu_button/CollapseMenuButton.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/layout/collapse_menu_button/CollapseMenuButton.tsx @@ -1,22 +1,51 @@ -import React from 'react'; -import { IconButton } from '@mui/material'; +import React, { useCallback, useEffect, useMemo } from 'react'; +import { IconButton, Tooltip } from '@mui/material'; import { useAppDispatch, useAppSelector } from '$app/stores/store'; import { sidebarActions } from '$app_reducers/sidebar/slice'; -import { ReactComponent as LeftSvg } from '$app/assets/left.svg'; -import { ReactComponent as RightSvg } from '$app/assets/right.svg'; +import { ReactComponent as ShowMenuIcon } from '$app/assets/show-menu.svg'; +import { useTranslation } from 'react-i18next'; +import { getModifier } from '$app/utils/get_modifier'; +import isHotkey from 'is-hotkey'; function CollapseMenuButton() { const isCollapsed = useAppSelector((state) => state.sidebar.isCollapsed); const dispatch = useAppDispatch(); - const handleClick = () => { + const handleClick = useCallback(() => { dispatch(sidebarActions.toggleCollapse()); - }; + }, [dispatch]); + + const { t } = useTranslation(); + + const title = useMemo(() => { + return ( +
+
{isCollapsed ? t('sideBar.openSidebar') : t('sideBar.closeSidebar')}
+
{`${getModifier()} + \\`}
+
+ ); + }, [isCollapsed, t]); + + useEffect(() => { + const handleKeyDown = (e: KeyboardEvent) => { + if (isHotkey('mod+\\', e)) { + e.preventDefault(); + handleClick(); + } + }; + + document.addEventListener('keydown', handleKeyDown); + return () => { + document.removeEventListener('keydown', handleKeyDown); + }; + }, [handleClick]); return ( - - {isCollapsed ? : } - + + + {isCollapsed ? : } + + ); } diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/layout/layout.scss b/frontend/appflowy_tauri/src/appflowy_app/components/layout/layout.scss new file mode 100644 index 0000000000..a708777326 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/layout/layout.scss @@ -0,0 +1,12 @@ +.workspaces { + ::-webkit-scrollbar { + width: 0px; + } +} + +.MuiPopover-root, .MuiPaper-root { + ::-webkit-scrollbar { + width: 0; + height: 0; + } +} \ No newline at end of file diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/layout/nested_page/AddButton.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/layout/nested_page/AddButton.tsx index 266775515c..1387f16f4d 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/layout/nested_page/AddButton.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/layout/nested_page/AddButton.tsx @@ -1,63 +1,64 @@ -import React, { useMemo } from 'react'; -import ButtonPopoverList from '$app/components/_shared/button_menu/ButtonMenu'; -import { IconButton } from '@mui/material'; +import React, { useCallback, useMemo } from 'react'; import { ReactComponent as AddSvg } from '$app/assets/add.svg'; import { useTranslation } from 'react-i18next'; import { ReactComponent as DocumentSvg } from '$app/assets/document.svg'; import { ReactComponent as GridSvg } from '$app/assets/grid.svg'; import { ViewLayoutPB } from '@/services/backend'; +import OperationMenu from '$app/components/layout/nested_page/OperationMenu'; -function AddButton({ isVisible, onAddPage }: { isVisible: boolean; onAddPage: (layout: ViewLayoutPB) => void }) { +function AddButton({ + isHovering, + setHovering, + onAddPage, +}: { + isHovering: boolean; + setHovering: (hovering: boolean) => void; + onAddPage: (layout: ViewLayoutPB) => void; +}) { const { t } = useTranslation(); + + const onConfirm = useCallback( + (key: string) => { + switch (key) { + case 'document': + onAddPage(ViewLayoutPB.Document); + break; + case 'grid': + onAddPage(ViewLayoutPB.Grid); + break; + default: + break; + } + }, + [onAddPage] + ); + const options = useMemo( () => [ { - key: 'add-document', - label: t('document.menuName'), - icon: ( -
- -
- ), - onClick: () => { - onAddPage(ViewLayoutPB.Document); - }, + key: 'document', + title: t('document.menuName'), + icon: , }, { - key: 'add-grid', - label: t('grid.menuName'), - icon: ( -
- -
- ), - onClick: () => { - onAddPage(ViewLayoutPB.Grid); - }, + key: 'grid', + title: t('grid.menuName'), + icon: , }, ], - [onAddPage, t] + [t] ); return ( - - - - - + + ); } diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/layout/nested_page/DeleteDialog.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/layout/nested_page/DeleteDialog.tsx index 83ed658ac1..4af8a2f2f1 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/layout/nested_page/DeleteDialog.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/layout/nested_page/DeleteDialog.tsx @@ -1,7 +1,7 @@ import React from 'react'; import { useTranslation } from 'react-i18next'; import { ViewLayoutPB } from '@/services/backend'; -import DeleteConfirmDialog from '$app/components/_shared/delete_confirm_dialog/DeleteConfirmDialog'; +import DeleteConfirmDialog from '$app/components/_shared/confirm_dialog/DeleteConfirmDialog'; function DeleteDialog({ layout, diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/layout/nested_page/MoreButton.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/layout/nested_page/MoreButton.tsx index 92bb254bd3..e0a36a5903 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/layout/nested_page/MoreButton.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/layout/nested_page/MoreButton.tsx @@ -1,25 +1,28 @@ -import React, { useMemo, useState } from 'react'; +import React, { useCallback, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { IconButton } from '@mui/material'; -import ButtonPopoverList from '$app/components/_shared/button_menu/ButtonMenu'; import { ReactComponent as DetailsSvg } from '$app/assets/details.svg'; import { ReactComponent as EditSvg } from '$app/assets/edit.svg'; import { ReactComponent as CopySvg } from '$app/assets/copy.svg'; import { ReactComponent as TrashSvg } from '$app/assets/delete.svg'; -import RenameDialog from './RenameDialog'; +import RenameDialog from '../../_shared/confirm_dialog/RenameDialog'; import { Page } from '$app_reducers/pages/slice'; import DeleteDialog from '$app/components/layout/nested_page/DeleteDialog'; +import OperationMenu from '$app/components/layout/nested_page/OperationMenu'; +import { getModifier } from '$app/utils/get_modifier'; +import isHotkey from 'is-hotkey'; function MoreButton({ - isVisible, onDelete, onDuplicate, onRename, page, + isHovering, + setHovering, }: { - isVisible: boolean; + isHovering: boolean; + setHovering: (hovering: boolean) => void; onDelete: () => Promise; onDuplicate: () => Promise; onRename: (newName: string) => Promise; @@ -29,84 +32,97 @@ function MoreButton({ const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); const { t } = useTranslation(); + + const onConfirm = useCallback( + (key: string) => { + switch (key) { + case 'rename': + setRenameDialogOpen(true); + break; + case 'delete': + setDeleteDialogOpen(true); + break; + case 'duplicate': + void onDuplicate(); + + break; + default: + break; + } + }, + [onDuplicate] + ); + const options = useMemo( () => [ { - label: t('disclosureAction.rename'), + title: t('button.rename'), + icon: , key: 'rename', - icon: ( -
- -
- ), - onClick: () => { - setRenameDialogOpen(true); - }, }, { - label: t('button.delete'), key: 'delete', - onClick: () => { - setDeleteDialogOpen(true); - }, - icon: ( -
- -
- ), + title: t('button.delete'), + icon: , + caption: 'Del', }, { key: 'duplicate', - label: t('button.duplicate'), - onClick: onDuplicate, - icon: ( -
- -
- ), + title: t('button.duplicate'), + icon: , + caption: `${getModifier()}+D`, }, ], - [onDuplicate, t] + [t] + ); + + const onKeyDown = useCallback( + (e: KeyboardEvent) => { + if (isHotkey('del', e) || isHotkey('backspace', e)) { + e.preventDefault(); + e.stopPropagation(); + onConfirm('delete'); + return; + } + + if (isHotkey('mod+d', e)) { + e.stopPropagation(); + onConfirm('duplicate'); + return; + } + }, + [onConfirm] ); return ( <> - - - - - - setRenameDialogOpen(false)} - onOk={async (newName: string) => { - await onRename(newName); - setRenameDialogOpen(false); - }} - /> + + + setDeleteDialogOpen(false)} - onOk={async () => { - await onDelete(); + onClose={() => { setDeleteDialogOpen(false); }} + onOk={onDelete} /> + {renameDialogOpen && ( + setRenameDialogOpen(false)} + onOk={onRename} + /> + )} ); } diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/layout/nested_page/NestedPage.hooks.ts b/frontend/appflowy_tauri/src/appflowy_app/components/layout/nested_page/NestedPage.hooks.ts index 8b94bd81f5..f2c2164f8c 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/layout/nested_page/NestedPage.hooks.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/components/layout/nested_page/NestedPage.hooks.ts @@ -1,12 +1,11 @@ -import { useCallback, useEffect, useMemo } from 'react'; -import { Page, pagesActions, pageTypeMap } from '$app_reducers/pages/slice'; +import { useCallback, useEffect } from 'react'; +import { pagesActions, pageTypeMap, parserViewPBToPage } from '$app_reducers/pages/slice'; import { useAppDispatch, useAppSelector } from '$app/stores/store'; import { FolderNotification, ViewLayoutPB } from '@/services/backend'; import { useNavigate, useParams } from 'react-router-dom'; import { updatePageName } from '$app_reducers/pages/async_actions'; import { createPage, deletePage, duplicatePage, getChildPages } from '$app/application/folder/page.service'; import { subscribeNotifications } from '$app/application/notification'; -import debounce from 'lodash-es/debounce'; export function useLoadChildPages(pageId: string) { const dispatch = useAppDispatch(); @@ -20,14 +19,6 @@ export function useLoadChildPages(pageId: string) { } }, [dispatch, pageId, collapsed]); - const onPageChanged = useMemo(() => { - return debounce((page: Page) => { - console.log('DidUpdateView'); - - dispatch(pagesActions.onPageChanged(page)); - }, 200); - }, [dispatch]); - const loadPageChildren = useCallback( async (pageId: string) => { const childPages = await getChildPages(pageId); @@ -49,9 +40,19 @@ export function useLoadChildPages(pageId: string) { useEffect(() => { const unsubscribePromise = subscribeNotifications( { - [FolderNotification.DidUpdateView]: (_payload) => { - // const page = parserViewPBToPage(payload); - // onPageChanged(page); + [FolderNotification.DidUpdateView]: async (payload) => { + const childViews = payload.child_views; + + if (childViews.length === 0) { + return; + } + + dispatch( + pagesActions.addChildPages({ + id: pageId, + childPages: childViews.map(parserViewPBToPage), + }) + ); }, [FolderNotification.DidUpdateChildViews]: async (payload) => { if (payload.delete_child_views.length === 0 && payload.create_child_views.length === 0) { @@ -67,7 +68,7 @@ export function useLoadChildPages(pageId: string) { ); return () => void unsubscribePromise.then((unsubscribe) => unsubscribe()); - }, [pageId, onPageChanged, loadPageChildren]); + }, [pageId, loadPageChildren, dispatch]); return { toggleCollapsed, @@ -79,13 +80,16 @@ export function useLoadChildPages(pageId: string) { export function usePageActions(pageId: string) { const page = useAppSelector((state) => state.pages.pageMap[pageId]); const dispatch = useAppDispatch(); + const params = useParams(); + const currentPageId = params.id; const navigate = useNavigate(); const onPageClick = useCallback(() => { + if (!page) return; const pageType = pageTypeMap[page.layout]; navigate(`/page/${pageType}/${pageId}`); - }, [navigate, page.layout, pageId]); + }, [navigate, page, pageId]); const onAddPage = useCallback( async (layout: ViewLayoutPB) => { @@ -95,6 +99,18 @@ export function usePageActions(pageId: string) { parent_view_id: pageId, }); + dispatch( + pagesActions.addPage({ + page: { + id: newViewId, + parentId: pageId, + layout, + name: '', + }, + isLast: true, + }) + ); + dispatch(pagesActions.expandPage(pageId)); const pageType = pageTypeMap[layout]; @@ -104,12 +120,17 @@ export function usePageActions(pageId: string) { ); const onDeletePage = useCallback(async () => { + if (currentPageId === pageId) { + navigate(`/`); + } + await deletePage(pageId); - }, [pageId]); + dispatch(pagesActions.deletePages([pageId])); + }, [dispatch, currentPageId, navigate, pageId]); const onDuplicatePage = useCallback(async () => { - await duplicatePage(pageId); - }, [pageId]); + await duplicatePage(page); + }, [page]); const onRenamePage = useCallback( async (name: string) => { diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/layout/nested_page/NestedPage.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/layout/nested_page/NestedPage.tsx index 54e283e878..cf03327302 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/layout/nested_page/NestedPage.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/layout/nested_page/NestedPage.tsx @@ -4,28 +4,44 @@ import { TransitionGroup } from 'react-transition-group'; import NestedPageTitle from '$app/components/layout/nested_page/NestedPageTitle'; import { useLoadChildPages, usePageActions } from '$app/components/layout/nested_page/NestedPage.hooks'; import { useDrag } from 'src/appflowy_app/components/_shared/drag_block'; -import { useAppDispatch } from '$app/stores/store'; +import { useAppDispatch, useAppSelector } from '$app/stores/store'; import { movePageThunk } from '$app_reducers/pages/async_actions'; +import { ViewLayoutPB } from '@/services/backend'; function NestedPage({ pageId }: { pageId: string }) { const { toggleCollapsed, collapsed, childPages } = useLoadChildPages(pageId); const { onAddPage, onPageClick, onDeletePage, onDuplicatePage, onRenamePage } = usePageActions(pageId); const dispatch = useAppDispatch(); + const page = useAppSelector((state) => state.pages.pageMap[pageId]); + const disableChildren = useAppSelector((state) => { + if (!page) return true; + const layout = state.pages.pageMap[page.parentId]?.layout; + + return !(layout === undefined || layout === ViewLayoutPB.Document); + }); const children = useMemo(() => { + if (disableChildren) { + return []; + } + return collapsed ? [] : childPages; - }, [collapsed, childPages]); + }, [collapsed, childPages, disableChildren]); const onDragFinished = useCallback( (result: { dragId: string; position: 'before' | 'after' | 'inside' }) => { + const { dragId, position } = result; + + if (dragId === pageId) return; + if (position === 'inside' && page?.layout !== ViewLayoutPB.Document) return; void dispatch( movePageThunk({ - sourceId: result.dragId, + sourceId: dragId, targetId: pageId, - insertType: result.position, + insertType: position, }) ); }, - [dispatch, pageId] + [dispatch, page?.layout, pageId] ); const { onDrop, dropPosition, onDragOver, onDragLeave, onDragStart, onDragEnd, isDraggingOver, isDragging } = useDrag({ @@ -34,20 +50,20 @@ function NestedPage({ pageId }: { pageId: string }) { }); const className = useMemo(() => { - const defaultClassName = 'relative flex-1 flex flex-col w-full'; + const defaultClassName = 'relative flex-1 select-none flex flex-col w-full'; if (isDragging) { return `${defaultClassName} opacity-40`; } - if (isDraggingOver && dropPosition === 'inside') { + if (isDraggingOver && dropPosition === 'inside' && page?.layout === ViewLayoutPB.Document) { if (dropPosition === 'inside') { return `${defaultClassName} bg-content-blue-100`; } } else { return defaultClassName; } - }, [dropPosition, isDragging, isDraggingOver]); + }, [dropPosition, isDragging, isDraggingOver, page?.layout]); return (
{ diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/layout/nested_page/NestedPageTitle.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/layout/nested_page/NestedPageTitle.tsx index 41058ee327..448fdc441a 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/layout/nested_page/NestedPageTitle.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/layout/nested_page/NestedPageTitle.tsx @@ -1,11 +1,14 @@ -import React, { useState } from 'react'; -import { ArrowRightSvg } from '$app/components/_shared/svg/ArrowRightSvg'; +import React, { useMemo, useState } from 'react'; import { useAppSelector } from '$app/stores/store'; import AddButton from './AddButton'; import MoreButton from './MoreButton'; import { ViewLayoutPB } from '@/services/backend'; import { useSelectedPage } from '$app/components/layout/nested_page/NestedPage.hooks'; import { useTranslation } from 'react-i18next'; +import { ReactComponent as MoreIcon } from '$app/assets/more.svg'; +import { IconButton } from '@mui/material'; +import { Page } from '$app_reducers/pages/slice'; +import { getPageIcon } from '$app/hooks/page.hooks'; function NestedPageTitle({ pageId, @@ -28,51 +31,68 @@ function NestedPageTitle({ }) { const { t } = useTranslation(); const page = useAppSelector((state) => { - return state.pages.pageMap[pageId]; + return state.pages.pageMap[pageId] as Page | undefined; }); + const disableChildren = useAppSelector((state) => { + if (!page) return true; + const layout = state.pages.pageMap[page.parentId]?.layout; + + return !(layout === undefined || layout === ViewLayoutPB.Document); + }); + const [isHovering, setIsHovering] = useState(false); const isSelected = useSelectedPage(pageId); + const pageIcon = useMemo(() => (page ? getPageIcon(page) : null), [page]); + return (
setIsHovering(true)} + onMouseMove={() => setIsHovering(true)} onMouseLeave={() => setIsHovering(false)} >
-
- - {page.icon ?
{page.icon.value}
: null} +
+ {disableChildren ? ( +
+ ) : ( + { + e.stopPropagation(); + toggleCollapsed(); + }} + style={{ + transform: collapsed ? 'rotate(0deg)' : 'rotate(90deg)', + }} + > + + + )} + + {pageIcon}
- {page.name || t('menuAppHeader.defaultNewPageName')} + {page?.name || t('menuAppHeader.defaultNewPageName')}
-
e.stopPropagation()} className={'min:w-14 flex items-center justify-end'}> - - +
e.stopPropagation()} className={'min:w-14 flex items-center justify-end px-2'}> + {page?.layout === ViewLayoutPB.Document && ( + + )} + {page && ( + + )}
diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/layout/nested_page/OperationMenu.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/layout/nested_page/OperationMenu.tsx new file mode 100644 index 0000000000..cef1d3307c --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/layout/nested_page/OperationMenu.tsx @@ -0,0 +1,103 @@ +import React, { ReactNode, useCallback, useMemo, useState } from 'react'; +import { IconButton } from '@mui/material'; +import Popover from '@mui/material/Popover'; +import KeyboardNavigation from '$app/components/_shared/keyboard_navigation/KeyboardNavigation'; +import Tooltip from '@mui/material/Tooltip'; + +function OperationMenu({ + options, + onConfirm, + isHovering, + setHovering, + children, + tooltip, + onKeyDown, +}: { + isHovering: boolean; + setHovering: (hovering: boolean) => void; + options: { + key: string; + title: string; + icon: React.ReactNode; + caption?: string; + }[]; + children: React.ReactNode; + onConfirm: (key: string) => void; + tooltip: string; + onKeyDown?: (e: KeyboardEvent) => void; +}) { + const [anchorEl, setAnchorEl] = useState(null); + const renderItem = useCallback((title: string, icon: ReactNode, caption?: string) => { + return ( +
+ {icon} +
{title}
+
{caption || ''}
+
+ ); + }, []); + + const handleClose = useCallback(() => { + setAnchorEl(null); + setHovering(false); + }, [setHovering]); + + const optionList = useMemo(() => { + return options.map((option) => { + return { + key: option.key, + content: renderItem(option.title, option.icon, option.caption), + }; + }); + }, [options, renderItem]); + + const open = Boolean(anchorEl); + + const handleConfirm = useCallback( + (key: string) => { + onConfirm(key); + handleClose(); + }, + [handleClose, onConfirm] + ); + + return ( + <> + + { + setAnchorEl(e.currentTarget); + }} + className={`${!isHovering ? 'invisible' : ''} text-icon-primary`} + > + {children} + + + + + + + + ); +} + +export default OperationMenu; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/layout/side_bar/Resizer.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/layout/side_bar/Resizer.tsx index 5c7830ad3a..5e284a94be 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/layout/side_bar/Resizer.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/layout/side_bar/Resizer.tsx @@ -10,6 +10,7 @@ function Resizer() { const startX = useRef(0); const onResize = useCallback( (e: MouseEvent) => { + e.preventDefault(); const diff = e.clientX - startX.current; const newWidth = width + diff; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/layout/side_bar/SideBar.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/layout/side_bar/SideBar.tsx index cfde67bde6..cee598b66d 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/layout/side_bar/SideBar.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/layout/side_bar/SideBar.tsx @@ -1,6 +1,5 @@ import React from 'react'; import { useAppSelector } from '$app/stores/store'; -import { ThemeMode } from '$app/stores/reducers/current-user/slice'; import { AppflowyLogoDark } from '$app/components/_shared/svg/AppflowyLogoDark'; import { AppflowyLogoLight } from '$app/components/_shared/svg/AppflowyLogoLight'; import CollapseMenuButton from '$app/components/layout/collapse_menu_button/CollapseMenuButton'; @@ -10,19 +9,19 @@ import WorkspaceManager from '$app/components/layout/workspace_manager/Workspace function SideBar() { const { isCollapsed, width, isResizing } = useAppSelector((state) => state.sidebar); - const isDark = useAppSelector((state) => state.currentUser?.userSetting?.themeMode === ThemeMode.Dark); + const isDark = useAppSelector((state) => state.currentUser?.userSetting?.isDark); return ( <>
-
+
{isDark ? : }
diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/layout/side_bar/UserInfo.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/layout/side_bar/UserInfo.tsx index bde742f57a..b02620e88c 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/layout/side_bar/UserInfo.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/layout/side_bar/UserInfo.tsx @@ -1,37 +1,46 @@ import React, { useState } from 'react'; import { useAppSelector } from '$app/stores/store'; -import { Avatar } from '@mui/material'; +import { Avatar, IconButton } from '@mui/material'; import PersonOutline from '@mui/icons-material/PersonOutline'; -import ArrowDropDown from '@mui/icons-material/ArrowDropDown'; import UserSetting from '../user_setting/UserSetting'; +import { ReactComponent as SettingIcon } from '$app/assets/settings.svg'; +import Tooltip from '@mui/material/Tooltip'; +import { useTranslation } from 'react-i18next'; function UserInfo() { const currentUser = useAppSelector((state) => state.currentUser); const [showUserSetting, setShowUserSetting] = useState(false); + const { t } = useTranslation(); + return ( <> -
{ - e.stopPropagation(); - setShowUserSetting(!showUserSetting); - }} - className={'flex cursor-pointer items-center px-6 text-text-title'} - > - - - - {currentUser.displayName} - +
+
+ + {currentUser.displayName ? currentUser.displayName[0] : } + + {currentUser.displayName} +
+ + + { + setShowUserSetting(!showUserSetting); + }} + > + + +
setShowUserSetting(false)} /> diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/layout/top_bar/TopBar.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/layout/top_bar/TopBar.tsx index 178c596f6b..d1b0fead63 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/layout/top_bar/TopBar.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/layout/top_bar/TopBar.tsx @@ -2,8 +2,6 @@ import React from 'react'; import CollapseMenuButton from '$app/components/layout/collapse_menu_button/CollapseMenuButton'; import { useAppSelector } from '$app/stores/store'; import Breadcrumb from '$app/components/layout/bread_crumb/BreadCrumb'; -import ShareButton from '$app/components/layout/share/Share'; -import MoreButton from '$app/components/layout/top_bar/MoreButton'; function TopBar() { const sidebarIsCollapsed = useAppSelector((state) => state.sidebar.isCollapsed); @@ -19,13 +17,6 @@ function TopBar() {
-
-
- -
- - -
); diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/layout/user_setting/AppearanceSetting.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/layout/user_setting/AppearanceSetting.tsx index e2d0367a51..e28a467526 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/layout/user_setting/AppearanceSetting.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/layout/user_setting/AppearanceSetting.tsx @@ -1,12 +1,11 @@ -import React, { useCallback, useEffect, useMemo } from 'react'; +import React, { useCallback, useMemo } from 'react'; import Select from '@mui/material/Select'; import { Theme, ThemeMode, UserSetting } from '$app/stores/reducers/current-user/slice'; import MenuItem from '@mui/material/MenuItem'; import { useTranslation } from 'react-i18next'; function AppearanceSetting({ - theme = Theme.Default, - themeMode = ThemeMode.Light, + themeMode = ThemeMode.System, onChange, }: { theme?: Theme; @@ -15,13 +14,6 @@ function AppearanceSetting({ }) { const { t } = useTranslation(); - useEffect(() => { - const html = document.documentElement; - - html?.setAttribute('data-dark-mode', String(themeMode === ThemeMode.Dark)); - html?.setAttribute('data-theme', theme); - }, [theme, themeMode]); - const themeModeOptions = useMemo( () => [ { @@ -32,6 +24,10 @@ function AppearanceSetting({ value: ThemeMode.Dark, content: t('settings.appearance.themeMode.dark'), }, + { + value: ThemeMode.System, + content: t('settings.appearance.themeMode.system'), + }, ], [t] ); @@ -63,7 +59,7 @@ function AppearanceSetting({ }} > {options.map((option) => ( - + {option.content} ))} @@ -86,6 +82,9 @@ function AppearanceSetting({ onChange: (newValue) => { onChange({ themeMode: newValue as ThemeMode, + isDark: + newValue === ThemeMode.Dark || + (newValue === ThemeMode.System && window.matchMedia('(prefers-color-scheme: dark)').matches), }); }, }, diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/layout/user_setting/LanguageSetting.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/layout/user_setting/LanguageSetting.tsx index 7ad9997567..81e7d067a4 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/layout/user_setting/LanguageSetting.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/layout/user_setting/LanguageSetting.tsx @@ -61,7 +61,7 @@ function LanguageSetting({ }} > {languages.map((option) => ( - + {option.title} ))} diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/layout/user_setting/Menu.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/layout/user_setting/Menu.tsx index 2d5bb0ff3c..9da3cb8f74 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/layout/user_setting/Menu.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/layout/user_setting/Menu.tsx @@ -27,7 +27,7 @@ function UserSettingMenu({ selected, onSelect }: { onSelect: (selected: MenuItem }, [t]); return ( -
+
{options.map((option) => { return (
{ onSelect(option.value); }} - className={`my-1 flex h-10 w-full cursor-pointer items-center justify-start rounded-md px-4 py-2 text-text-title ${ + className={`my-1 flex w-full cursor-pointer items-center justify-start rounded-md p-2 text-xs text-text-title ${ selected === option.value ? 'bg-fill-list-hover' : 'hover:text-content-blue-300' }`} > diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/layout/user_setting/UserSetting.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/layout/user_setting/UserSetting.tsx index 7fd52de56a..7cfbac4a76 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/layout/user_setting/UserSetting.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/layout/user_setting/UserSetting.tsx @@ -5,10 +5,9 @@ import DialogTitle from '@mui/material/DialogTitle'; import Slide, { SlideProps } from '@mui/material/Slide'; import UserSettingMenu, { MenuItem } from './Menu'; import UserSettingPanel from './SettingPanel'; -import { Theme, UserSetting } from '$app/stores/reducers/current-user/slice'; +import { UserSetting } from '$app/stores/reducers/current-user/slice'; import { useAppDispatch, useAppSelector } from '$app/stores/store'; import { currentUserActions } from '$app_reducers/current-user/slice'; -import { ThemeModePB } from '@/services/backend'; import { useTranslation } from 'react-i18next'; import { UserService } from '$app/application/user/user.service'; @@ -29,8 +28,8 @@ function UserSettings({ open, onClose }: { open: boolean; onClose: () => void }) const language = newSetting.language || 'en'; void UserService.setAppearanceSetting({ - theme: newSetting.theme || Theme.Default, - theme_mode: newSetting.themeMode || ThemeModePB.Light, + theme: newSetting.theme, + theme_mode: newSetting.themeMode, locale: { language_code: language.split('-')[0], country_code: language.split('-')[1], @@ -48,7 +47,7 @@ function UserSettings({ open, onClose }: { open: boolean; onClose: () => void }) keepMounted={false} onClose={onClose} > - {t('settings.title')} + {t('settings.title')} { diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/layout/workspace_manager/MoreButton.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/layout/workspace_manager/MoreButton.tsx deleted file mode 100644 index efdd8d1144..0000000000 --- a/frontend/appflowy_tauri/src/appflowy_app/components/layout/workspace_manager/MoreButton.tsx +++ /dev/null @@ -1,64 +0,0 @@ -import React, { useMemo } from 'react'; -import { WorkspaceItem } from '$app_reducers/workspace/slice'; -import { IconButton } from '@mui/material'; -import MoreIcon from '@mui/icons-material/MoreHoriz'; -import SettingsIcon from '@mui/icons-material/Settings'; -import { useTranslation } from 'react-i18next'; -import { DeleteOutline } from '@mui/icons-material'; -import ButtonPopoverList from '$app/components/_shared/button_menu/ButtonMenu'; - -function MoreButton({ - workspace, - isHovered, - onDelete, -}: { - isHovered: boolean; - workspace: WorkspaceItem; - onDelete: (id: string) => void; -}) { - const { t } = useTranslation(); - - const options = useMemo(() => { - return [ - { - key: 'settings', - icon: , - label: t('settings.title'), - onClick: () => { - // - }, - }, - { - key: 'delete', - icon: , - label: t('button.delete'), - onClick: () => onDelete(workspace.id), - }, - ]; - }, [onDelete, t, workspace.id]); - - return ( - <> - - - - - - - ); -} - -export default MoreButton; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/layout/workspace_manager/NestedPages.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/layout/workspace_manager/NestedPages.tsx index 80d61440b7..ec3335b6b3 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/layout/workspace_manager/NestedPages.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/layout/workspace_manager/NestedPages.tsx @@ -8,7 +8,7 @@ function WorkspaceNestedPages({ workspaceId }: { workspaceId: string }) { }); return ( -
+
{pageIds?.map((pageId) => ( ))} diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/layout/workspace_manager/NewPageButton.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/layout/workspace_manager/NewPageButton.tsx index f82cb2208e..537b7d2d9a 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/layout/workspace_manager/NewPageButton.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/layout/workspace_manager/NewPageButton.tsx @@ -1,35 +1,29 @@ import React from 'react'; -import AddSvg from '$app/components/_shared/svg/AddSvg'; import { useTranslation } from 'react-i18next'; -import { ViewLayoutPB } from '@/services/backend'; -import { useNavigate } from 'react-router-dom'; -import { createCurrentWorkspaceChildView } from '$app/application/folder/workspace.service'; +import { useWorkspaceActions } from '$app/components/layout/workspace_manager/Workspace.hooks'; +import Button from '@mui/material/Button'; +import { ReactComponent as AddSvg } from '$app/assets/add.svg'; function NewPageButton({ workspaceId }: { workspaceId: string }) { const { t } = useTranslation(); - const navigate = useNavigate(); + const { newPage } = useWorkspaceActions(workspaceId); return ( -
-
-
+ } + className={'flex w-full items-center justify-start text-xs hover:bg-transparent hover:text-fill-default'} + > {t('newPageText')} - +
); } diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/layout/workspace_manager/TrashButton.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/layout/workspace_manager/TrashButton.tsx index 944c23df01..e89af36869 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/layout/workspace_manager/TrashButton.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/layout/workspace_manager/TrashButton.tsx @@ -30,14 +30,14 @@ function TrashButton() { onDragLeave={onDragLeave} data-page-id={'trash'} onClick={navigateToTrash} - className={`mx-1 my-3 flex h-[32px] w-[100%] items-center rounded-lg p-2 hover:bg-fill-list-hover ${ + className={`my-3 flex h-[32px] w-[100%] cursor-pointer items-center gap-2 rounded-lg p-3.5 text-xs font-medium hover:bg-fill-list-hover ${ selected ? 'bg-fill-list-active' : '' } ${isDraggingOver ? 'bg-fill-list-hover' : ''}`} > -
+
- {t('trash.text')} + {t('trash.text')}
); } diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/layout/workspace_manager/Workspace.hooks.ts b/frontend/appflowy_tauri/src/appflowy_app/components/layout/workspace_manager/Workspace.hooks.ts index 2b5ac678e8..7d77b12d69 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/layout/workspace_manager/Workspace.hooks.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/components/layout/workspace_manager/Workspace.hooks.ts @@ -3,8 +3,10 @@ import { useAppDispatch, useAppSelector } from '$app/stores/store'; import { workspaceActions, WorkspaceItem } from '$app_reducers/workspace/slice'; import { Page, pagesActions, parserViewPBToPage } from '$app_reducers/pages/slice'; import { subscribeNotifications } from '$app/application/notification'; -import { FolderNotification } from '@/services/backend'; +import { FolderNotification, ViewLayoutPB } from '@/services/backend'; import * as workspaceService from '$app/application/folder/workspace.service'; +import { createCurrentWorkspaceChildView } from '$app/application/folder/workspace.service'; +import { useNavigate } from 'react-router-dom'; export function useLoadWorkspaces() { const dispatch = useAppDispatch(); @@ -95,3 +97,21 @@ export function useLoadWorkspace(workspace: WorkspaceItem) { deleteWorkspace, }; } + +export function useWorkspaceActions(workspaceId: string) { + const navigate = useNavigate(); + + const newPage = useCallback(async () => { + const { id } = await createCurrentWorkspaceChildView({ + name: '', + layout: ViewLayoutPB.Document, + parent_view_id: workspaceId, + }); + + navigate(`/page/document/${id}`); + }, [navigate, workspaceId]); + + return { + newPage, + }; +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/layout/workspace_manager/Workspace.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/layout/workspace_manager/Workspace.tsx index db9ce8f4b1..165a9ab1d1 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/layout/workspace_manager/Workspace.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/layout/workspace_manager/Workspace.tsx @@ -1,20 +1,65 @@ -import React from 'react'; +import React, { useState } from 'react'; import { WorkspaceItem } from '$app_reducers/workspace/slice'; import NestedViews from '$app/components/layout/workspace_manager/NestedPages'; -import { useLoadWorkspace } from '$app/components/layout/workspace_manager/Workspace.hooks'; +import { useLoadWorkspace, useWorkspaceActions } from '$app/components/layout/workspace_manager/Workspace.hooks'; +import Typography from '@mui/material/Typography'; +import { useTranslation } from 'react-i18next'; +import { ReactComponent as AddIcon } from '$app/assets/add.svg'; +import { IconButton } from '@mui/material'; +import Tooltip from '@mui/material/Tooltip'; function Workspace({ workspace, opened }: { workspace: WorkspaceItem; opened: boolean }) { useLoadWorkspace(workspace); + const { t } = useTranslation(); + const { newPage } = useWorkspaceActions(workspace.id); + const [showPages, setShowPages] = useState(true); + const [showAdd, setShowAdd] = useState(false); + return ( <>
- +
{ + e.stopPropagation(); + e.preventDefault(); + setShowPages(!showPages); + }} + onMouseEnter={() => { + setShowAdd(true); + }} + onMouseLeave={() => { + setShowAdd(false); + }} + className={'mt-2 flex h-[22px] w-full cursor-pointer select-none items-center justify-between px-4'} + > + + + {t('sideBar.personal')} + + + {showAdd && ( + + { + e.stopPropagation(); + void newPage(); + }} + size={'small'} + > + + + + )} +
+ + {showPages && }
); diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/layout/workspace_manager/WorkspaceManager.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/layout/workspace_manager/WorkspaceManager.tsx index a7a90e785f..c6404d435c 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/layout/workspace_manager/WorkspaceManager.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/layout/workspace_manager/WorkspaceManager.tsx @@ -8,18 +8,17 @@ function WorkspaceManager() { const { workspaces, currentWorkspace } = useLoadWorkspaces(); return ( -
-
+
+
{workspaces.map((workspace) => ( ))}
-
- -
- +
+ +
{currentWorkspace && }
); diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/layout/workspace_manager/WorkspaceTitle.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/layout/workspace_manager/WorkspaceTitle.tsx deleted file mode 100644 index 4c9f8988a6..0000000000 --- a/frontend/appflowy_tauri/src/appflowy_app/components/layout/workspace_manager/WorkspaceTitle.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import React, { useState } from 'react'; -import MoreButton from '$app/components/layout/workspace_manager/MoreButton'; -import MenuItem from '@mui/material/MenuItem'; -import { WorkspaceItem } from '$app_reducers/workspace/slice'; - -function WorkspaceTitle({ - workspace, - openWorkspace, - onDelete, -}: { - openWorkspace: () => void; - onDelete: (id: string) => void; - workspace: WorkspaceItem; -}) { - const [isHovered, setIsHovered] = useState(false); - - return ( - openWorkspace()} - onMouseEnter={() => { - setIsHovered(true); - }} - onMouseLeave={() => { - setIsHovered(false); - }} - className={'hover:bg-fill-list-active'} - > -
-
{workspace.name}
-
- -
-
-
- ); -} - -export default WorkspaceTitle; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/trash/Trash.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/trash/Trash.tsx index 6f0a0f94f6..40f51d1fbf 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/trash/Trash.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/trash/Trash.tsx @@ -3,9 +3,9 @@ import { useTranslation } from 'react-i18next'; import Button from '@mui/material/Button'; import { DeleteOutline, RestoreOutlined } from '@mui/icons-material'; import { useLoadTrash, useTrashActions } from '$app/components/trash/Trash.hooks'; -import { Divider, List } from '@mui/material'; +import { List } from '@mui/material'; import TrashItem from '$app/components/trash/TrashItem'; -import DeleteConfirmDialog from '$app/components/_shared/delete_confirm_dialog/DeleteConfirmDialog'; +import DeleteConfirmDialog from '$app/components/_shared/confirm_dialog/DeleteConfirmDialog'; function Trash() { const { t } = useTranslation(); @@ -26,7 +26,7 @@ function Trash() { return (
-
{t('trash.text')}
+
{t('trash.text')}
-
+
{t('trash.pageHeader.fileName')}
{t('trash.pageHeader.lastModified')}
{t('trash.pageHeader.created')}
- {trash.map((item) => ( -
-
{item.name}
+
+
{item.name || t('document.title.placeholder')}
{dayjs.unix(item.modifiedTime).format('MM/DD/YYYY hh:mm A')}
{dayjs.unix(item.createTime).format('MM/DD/YYYY hh:mm A')}
- onPutback(item.id)} className={'mr-2'}> + onPutback(item.id)} className={'mr-2'}> - onDelete([item.id])}> + onDelete([item.id])}> diff --git a/frontend/appflowy_tauri/src/appflowy_app/hooks/page.hooks.tsx b/frontend/appflowy_tauri/src/appflowy_app/hooks/page.hooks.tsx new file mode 100644 index 0000000000..49e01e75c0 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/hooks/page.hooks.tsx @@ -0,0 +1,26 @@ +import { ViewLayoutPB } from '@/services/backend'; +import React from 'react'; +import { Page } from '$app_reducers/pages/slice'; +import { ReactComponent as DocumentIcon } from '$app/assets/document.svg'; +import { ReactComponent as GridIcon } from '$app/assets/grid.svg'; +import { ReactComponent as BoardIcon } from '$app/assets/board.svg'; +import { ReactComponent as CalendarIcon } from '$app/assets/date.svg'; + +export function getPageIcon(page: Page) { + if (page.icon) { + return page.icon.value; + } + + switch (page.layout) { + case ViewLayoutPB.Document: + return ; + case ViewLayoutPB.Grid: + return ; + case ViewLayoutPB.Board: + return ; + case ViewLayoutPB.Calendar: + return ; + default: + return null; + } +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/current-user/slice.ts b/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/current-user/slice.ts index 8bb8d42b0e..13ad581175 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/current-user/slice.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/current-user/slice.ts @@ -8,6 +8,7 @@ export interface UserSetting { theme?: Theme; themeMode?: ThemeMode; language?: string; + isDark?: boolean; } export enum Theme { diff --git a/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/pages/async_actions.ts b/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/pages/async_actions.ts index 23f4d85cbc..ebb78bb7fa 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/pages/async_actions.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/pages/async_actions.ts @@ -14,7 +14,7 @@ export const movePageThunk = createAsyncThunk( thunkAPI ) => { const { sourceId, targetId, insertType } = payload; - const { getState } = thunkAPI; + const { getState, dispatch } = thunkAPI; const { pageMap, relationMap } = (getState() as RootState).pages; const sourcePage = pageMap[sourceId]; const targetPage = pageMap[targetId]; @@ -51,6 +51,8 @@ export const movePageThunk = createAsyncThunk( } } + dispatch(pagesActions.movePage({ id: sourceId, newParentId: parentId, prevId })); + await movePage({ view_id: sourceId, new_parent_id: parentId, diff --git a/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/pages/slice.ts b/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/pages/slice.ts index e5b08ae095..8d3f07507e 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/pages/slice.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/pages/slice.ts @@ -89,10 +89,85 @@ export const pagesSlice = createSlice({ } }, - removeChildPages(state, action: PayloadAction) { - const parentId = action.payload; + addPage( + state, + action: PayloadAction<{ + page: Page; + isLast?: boolean; + prevId?: string; + }> + ) { + const { page, prevId, isLast } = action.payload; - delete state.relationMap[parentId]; + state.pageMap[page.id] = page; + state.relationMap[page.id] = []; + + const parentId = page.parentId; + + if (isLast) { + state.relationMap[parentId]?.push(page.id); + } else { + const index = prevId ? state.relationMap[parentId]?.indexOf(prevId) ?? -1 : -1; + + state.relationMap[parentId]?.splice(index + 1, 0, page.id); + } + }, + + deletePages(state, action: PayloadAction) { + const ids = action.payload; + + ids.forEach((id) => { + const parentId = state.pageMap[id].parentId; + const parentChildren = state.relationMap[parentId]; + + state.relationMap[parentId] = parentChildren && parentChildren.filter((childId) => childId !== id); + delete state.relationMap[id]; + delete state.expandedIdMap[id]; + delete state.pageMap[id]; + }); + }, + + duplicatePage( + state, + action: PayloadAction<{ + id: string; + newId: string; + }> + ) { + const { id, newId } = action.payload; + const page = state.pageMap[id]; + const newPage = { ...page, id: newId }; + + state.pageMap[newPage.id] = newPage; + + const index = state.relationMap[page.parentId]?.indexOf(id); + + state.relationMap[page.parentId]?.splice(index ?? 0, 0, newId); + }, + + movePage( + state, + action: PayloadAction<{ + id: string; + newParentId: string; + prevId?: string; + }> + ) { + const { id, newParentId, prevId } = action.payload; + const parentId = state.pageMap[id].parentId; + const parentChildren = state.relationMap[parentId]; + + const index = parentChildren?.indexOf(id) ?? -1; + + if (index > -1) { + state.relationMap[parentId]?.splice(index, 1); + } + + state.pageMap[id].parentId = newParentId; + const newParentChildren = state.relationMap[newParentId] || []; + const prevIndex = prevId ? newParentChildren.indexOf(prevId) : -1; + + state.relationMap[newParentId]?.splice(prevIndex + 1, 0, id); }, expandPage(state, action: PayloadAction) { diff --git a/frontend/appflowy_tauri/src/appflowy_app/utils/get_modifier.ts b/frontend/appflowy_tauri/src/appflowy_app/utils/get_modifier.ts new file mode 100644 index 0000000000..a81e5e9093 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/utils/get_modifier.ts @@ -0,0 +1,12 @@ +export const isMac = () => { + return navigator.userAgent.includes('Mac OS X'); +}; + +const MODIFIERS = { + control: 'Ctrl', + meta: '⌘', +}; + +export const getModifier = () => { + return isMac() ? MODIFIERS.meta : MODIFIERS.control; +}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/utils/mui.ts b/frontend/appflowy_tauri/src/appflowy_app/utils/mui.ts index 2bdf102a6e..fa2520bb7a 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/utils/mui.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/utils/mui.ts @@ -1,11 +1,8 @@ -import { ThemeMode } from '$app/stores/reducers/current-user/slice'; import { ThemeOptions } from '@mui/material'; // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore -export const getDesignTokens = (mode: ThemeMode): ThemeOptions => { - const isDark = mode === ThemeMode.Dark; - +export const getDesignTokens = (isDark: boolean): ThemeOptions => { return { typography: { fontFamily: ['Poppins'].join(','), diff --git a/frontend/appflowy_tauri/src/styles/template.css b/frontend/appflowy_tauri/src/styles/template.css index e8fe35e440..f17595b74a 100644 --- a/frontend/appflowy_tauri/src/styles/template.css +++ b/frontend/appflowy_tauri/src/styles/template.css @@ -24,10 +24,6 @@ body { width: 8px; } -.MuiPopover-root::-webkit-scrollbar { - width: 0; - height: 0; -} :root[data-dark-mode=true] body { diff --git a/frontend/resources/translations/en.json b/frontend/resources/translations/en.json index 8d4f9ce4dd..93842e59c2 100644 --- a/frontend/resources/translations/en.json +++ b/frontend/resources/translations/en.json @@ -879,7 +879,9 @@ "page": { "label": "Link to page", "tooltip": "Click to open page" - } + }, + "deleted": "Deleted", + "deletedContent": "This content does not exist or has been deleted" }, "toolbar": { "resetToDefaultFont": "Reset to default"