mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
fix: tauri folder bugs (#4589)
This commit is contained in:
parent
9d71464f1a
commit
60fc5bb2c8
@ -19,7 +19,10 @@
|
|||||||
},
|
},
|
||||||
"fs": {
|
"fs": {
|
||||||
"all": true,
|
"all": true,
|
||||||
"scope": ["$APPLOCALDATA/**", "$APPLOCALDATA/images/*"],
|
"scope": [
|
||||||
|
"$APPLOCALDATA/**",
|
||||||
|
"$APPLOCALDATA/images/*"
|
||||||
|
],
|
||||||
"readFile": true,
|
"readFile": true,
|
||||||
"writeFile": true,
|
"writeFile": true,
|
||||||
"readDir": true,
|
"readDir": true,
|
||||||
|
@ -1,64 +1,59 @@
|
|||||||
import { useAppDispatch, useAppSelector } from '$app/stores/store';
|
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 { currentUserActions } from '$app_reducers/current-user/slice';
|
||||||
import { Theme as ThemeType, ThemeMode } from '$app/stores/reducers/current-user/slice';
|
import { Theme as ThemeType, ThemeMode } from '$app/stores/reducers/current-user/slice';
|
||||||
import { createTheme } from '@mui/material/styles';
|
import { createTheme } from '@mui/material/styles';
|
||||||
import { getDesignTokens } from '$app/utils/mui';
|
import { getDesignTokens } from '$app/utils/mui';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { ThemeModePB } from '@/services/backend';
|
|
||||||
import { UserService } from '$app/application/user/user.service';
|
import { UserService } from '$app/application/user/user.service';
|
||||||
|
|
||||||
export function useUserSetting() {
|
export function useUserSetting() {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const { i18n } = useTranslation();
|
const { i18n } = useTranslation();
|
||||||
|
const {
|
||||||
const handleSystemThemeChange = useCallback(() => {
|
themeMode = ThemeMode.System,
|
||||||
const mode = window.matchMedia('(prefers-color-scheme: dark)').matches ? ThemeMode.Dark : ThemeMode.Light;
|
isDark = false,
|
||||||
|
theme: themeType = ThemeType.Default,
|
||||||
dispatch(currentUserActions.setUserSetting({ themeMode: mode }));
|
} = useAppSelector((state) => {
|
||||||
}, [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) => {
|
|
||||||
return state.currentUser.userSetting || {};
|
return state.currentUser.userSetting || {};
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const html = document.documentElement;
|
void (async () => {
|
||||||
|
const settings = await UserService.getAppearanceSetting();
|
||||||
|
|
||||||
html?.setAttribute('data-dark-mode', String(themeMode === ThemeMode.Dark));
|
if (!settings) return;
|
||||||
html?.setAttribute('data-theme', themeType);
|
dispatch(currentUserActions.setUserSetting(settings));
|
||||||
}, [themeType, themeMode]);
|
await i18n.changeLanguage(settings.language);
|
||||||
|
})();
|
||||||
|
}, [dispatch, i18n]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return () => {
|
const html = document.documentElement;
|
||||||
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
|
|
||||||
|
|
||||||
|
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);
|
mediaQuery.removeEventListener('change', handleSystemThemeChange);
|
||||||
};
|
};
|
||||||
}, [dispatch, handleSystemThemeChange]);
|
}, [dispatch, themeMode]);
|
||||||
|
|
||||||
const muiTheme = useMemo(() => createTheme(getDesignTokens(themeMode)), [themeMode]);
|
const muiTheme = useMemo(() => createTheme(getDesignTokens(isDark)), [isDark]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
muiTheme,
|
muiTheme,
|
||||||
|
@ -138,6 +138,7 @@ export interface MentionPage {
|
|||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
layout: ViewLayoutPB;
|
layout: ViewLayoutPB;
|
||||||
|
parentId: string;
|
||||||
icon?: {
|
icon?: {
|
||||||
ty: ViewIconTypePB;
|
ty: ViewIconTypePB;
|
||||||
value: string;
|
value: string;
|
||||||
|
@ -49,8 +49,7 @@ export const createOrphanPage = async (
|
|||||||
return Promise.reject(result.val);
|
return Promise.reject(result.val);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const duplicatePage = async (id: string) => {
|
export const duplicatePage = async (page: Page) => {
|
||||||
const page = await getPage(id);
|
|
||||||
const payload = ViewPB.fromObject(page);
|
const payload = ViewPB.fromObject(page);
|
||||||
|
|
||||||
const result = await FolderEventDuplicateView(payload);
|
const result = await FolderEventDuplicateView(payload);
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
<path d="M5 8C5 8.55228 4.55228 9 4 9C3.44772 9 3 8.55228 3 8C3 7.44772 3.44772 7 4 7C4.55228 7 5 7.44772 5 8Z" fill="#78797D"/>
|
<path d="M5 8C5 8.55228 4.55228 9 4 9C3.44772 9 3 8.55228 3 8C3 7.44772 3.44772 7 4 7C4.55228 7 5 7.44772 5 8Z" fill="currentColor"/>
|
||||||
<path d="M9 8C9 8.55228 8.55229 9 8 9C7.44772 9 7 8.55228 7 8C7 7.44772 7.44772 7 8 7C8.55229 7 9 7.44772 9 8Z" fill="#78797D"/>
|
<path d="M9 8C9 8.55228 8.55229 9 8 9C7.44772 9 7 8.55228 7 8C7 7.44772 7.44772 7 8 7C8.55229 7 9 7.44772 9 8Z" fill="currentColor"/>
|
||||||
<path d="M12 9C12.5523 9 13 8.55228 13 8C13 7.44772 12.5523 7 12 7C11.4477 7 11 7.44772 11 8C11 8.55228 11.4477 9 12 9Z" fill="#78797D"/>
|
<path d="M12 9C12.5523 9 13 8.55228 13 8C13 7.44772 12.5523 7 12 7C11.4477 7 11 7.44772 11 8C11 8.55228 11.4477 9 12 9Z" fill="currentColor"/>
|
||||||
</svg>
|
</svg>
|
||||||
|
Before Width: | Height: | Size: 499 B After Width: | Height: | Size: 514 B |
@ -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<Theme>;
|
|
||||||
}
|
|
||||||
|
|
||||||
function ButtonPopoverList({ popoverOrigin, isVisible, children, popoverOptions, onClose, sx }: ButtonPopoverListProps) {
|
|
||||||
const [anchorEl, setAnchorEl] = useState<HTMLDivElement>();
|
|
||||||
const open = Boolean(anchorEl);
|
|
||||||
const visible = isVisible || open;
|
|
||||||
const handleClick = (event: React.MouseEvent<HTMLDivElement>) => {
|
|
||||||
setAnchorEl(event.currentTarget);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleClose = useCallback(() => {
|
|
||||||
setAnchorEl(undefined);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{visible && <div onClick={handleClick}>{children}</div>}
|
|
||||||
<Portal>
|
|
||||||
<Popover
|
|
||||||
open={open}
|
|
||||||
{...popoverOrigin}
|
|
||||||
anchorEl={anchorEl}
|
|
||||||
onClose={() => {
|
|
||||||
handleClose();
|
|
||||||
onClose?.();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<List sx={{ ...sx }}>
|
|
||||||
{popoverOptions.map((option) => (
|
|
||||||
<MenuItem
|
|
||||||
key={option.key}
|
|
||||||
onClick={() => {
|
|
||||||
option.onClick();
|
|
||||||
handleClose();
|
|
||||||
}}
|
|
||||||
className={'flex items-center gap-1 rounded-none px-2 text-xs font-medium'}
|
|
||||||
>
|
|
||||||
<span className={'text-base'}>{option.icon}</span>
|
|
||||||
<span>{option.label}</span>
|
|
||||||
</MenuItem>
|
|
||||||
))}
|
|
||||||
</List>
|
|
||||||
</Popover>
|
|
||||||
</Portal>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default ButtonPopoverList;
|
|
@ -1,8 +1,9 @@
|
|||||||
import React from 'react';
|
import React, { useCallback } from 'react';
|
||||||
import DialogContent from '@mui/material/DialogContent';
|
import DialogContent from '@mui/material/DialogContent';
|
||||||
import { Button, DialogActions, Divider } from '@mui/material';
|
import { Button, DialogActions, Divider } from '@mui/material';
|
||||||
import Dialog from '@mui/material/Dialog';
|
import Dialog from '@mui/material/Dialog';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { Log } from '$app/utils/log';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
@ -15,9 +16,36 @@ interface Props {
|
|||||||
function DeleteConfirmDialog({ open, title, subtitle, onOk, onClose }: Props) {
|
function DeleteConfirmDialog({ open, title, subtitle, onOk, onClose }: Props) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const onDone = useCallback(async () => {
|
||||||
|
try {
|
||||||
|
await onOk();
|
||||||
|
onClose();
|
||||||
|
} catch (e) {
|
||||||
|
Log.error(e);
|
||||||
|
}
|
||||||
|
}, [onClose, onOk]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog keepMounted={false} onMouseDown={(e) => e.stopPropagation()} open={open} onClose={onClose}>
|
<Dialog
|
||||||
<DialogContent className={'flex w-[540px] flex-col items-center justify-center'}>
|
keepMounted={false}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
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}
|
||||||
|
>
|
||||||
|
<DialogContent className={'flex w-[340px] flex-col items-center justify-center gap-4'}>
|
||||||
<div className={'text-md font-medium'}>{title}</div>
|
<div className={'text-md font-medium'}>{title}</div>
|
||||||
{subtitle && <div className={'m-1 text-sm text-text-caption'}>{subtitle}</div>}
|
{subtitle && <div className={'m-1 text-sm text-text-caption'}>{subtitle}</div>}
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
@ -26,15 +54,7 @@ function DeleteConfirmDialog({ open, title, subtitle, onOk, onClose }: Props) {
|
|||||||
<Button variant={'outlined'} onClick={onClose}>
|
<Button variant={'outlined'} onClick={onClose}>
|
||||||
{t('button.cancel')}
|
{t('button.cancel')}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button variant={'contained'} onClick={onDone}>
|
||||||
variant={'contained'}
|
|
||||||
onClick={async () => {
|
|
||||||
try {
|
|
||||||
await onOk();
|
|
||||||
onClose();
|
|
||||||
} catch (e) {}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{t('button.delete')}
|
{t('button.delete')}
|
||||||
</Button>
|
</Button>
|
||||||
</DialogActions>
|
</DialogActions>
|
@ -1,10 +1,10 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useCallback, useEffect, useState } from 'react';
|
||||||
import DialogTitle from '@mui/material/DialogTitle';
|
import DialogTitle from '@mui/material/DialogTitle';
|
||||||
import DialogContent from '@mui/material/DialogContent';
|
import DialogContent from '@mui/material/DialogContent';
|
||||||
import Dialog from '@mui/material/Dialog';
|
import Dialog from '@mui/material/Dialog';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import TextField from '@mui/material/TextField';
|
import TextField from '@mui/material/TextField';
|
||||||
import { Button, DialogActions } from '@mui/material';
|
import { Button, DialogActions, Divider } from '@mui/material';
|
||||||
|
|
||||||
function RenameDialog({
|
function RenameDialog({
|
||||||
defaultValue,
|
defaultValue,
|
||||||
@ -25,20 +25,31 @@ function RenameDialog({
|
|||||||
setValue(defaultValue);
|
setValue(defaultValue);
|
||||||
setError(false);
|
setError(false);
|
||||||
}, [defaultValue]);
|
}, [defaultValue]);
|
||||||
|
|
||||||
|
const onDone = useCallback(async () => {
|
||||||
|
try {
|
||||||
|
await onOk(value);
|
||||||
|
onClose();
|
||||||
|
} catch (e) {
|
||||||
|
setError(true);
|
||||||
|
}
|
||||||
|
}, [onClose, onOk, value]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog keepMounted={false} onMouseDown={(e) => e.stopPropagation()} open={open} onClose={onClose}>
|
<Dialog keepMounted={false} onMouseDown={(e) => e.stopPropagation()} open={open} onClose={onClose}>
|
||||||
<DialogTitle>{t('menuAppHeader.renameDialog')}</DialogTitle>
|
<DialogTitle className={'pb-2'}>{t('menuAppHeader.renameDialog')}</DialogTitle>
|
||||||
<DialogContent className={'flex w-[540px]'}>
|
<DialogContent className={'flex'}>
|
||||||
<TextField
|
<TextField
|
||||||
error={error}
|
error={error}
|
||||||
autoFocus
|
autoFocus
|
||||||
spellCheck={false}
|
spellCheck={false}
|
||||||
value={value}
|
value={value}
|
||||||
|
placeholder={t('dialogCreatePageNameHint')}
|
||||||
onKeyDown={(e) => {
|
onKeyDown={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
if (e.key === 'Enter') {
|
if (e.key === 'Enter') {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
void onOk(value);
|
void onDone();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (e.key === 'Escape') {
|
if (e.key === 'Escape') {
|
||||||
@ -54,18 +65,12 @@ function RenameDialog({
|
|||||||
variant='standard'
|
variant='standard'
|
||||||
/>
|
/>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
|
<Divider className={'mb-1'} />
|
||||||
<DialogActions>
|
<DialogActions className={'mb-1 px-4'}>
|
||||||
<Button onClick={onClose}>{t('button.cancel')}</Button>
|
<Button variant={'outlined'} onClick={onClose}>
|
||||||
<Button
|
{t('button.cancel')}
|
||||||
onClick={async () => {
|
</Button>
|
||||||
try {
|
<Button variant={'contained'} onClick={onDone}>
|
||||||
await onOk(value);
|
|
||||||
} catch (e) {
|
|
||||||
setError(true);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{t('button.ok')}
|
{t('button.ok')}
|
||||||
</Button>
|
</Button>
|
||||||
</DialogActions>
|
</DialogActions>
|
@ -24,8 +24,12 @@ export function useDrag(props: Props) {
|
|||||||
setIsDraggingOver(false);
|
setIsDraggingOver(false);
|
||||||
setIsDragging(false);
|
setIsDragging(false);
|
||||||
setDropPosition(undefined);
|
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 dragId = e.dataTransfer.getData('dragId');
|
||||||
const targetRect = e.currentTarget.getBoundingClientRect();
|
const targetRect = currentTarget.getBoundingClientRect();
|
||||||
const { clientY } = e;
|
const { clientY } = e;
|
||||||
|
|
||||||
const position = calcPosition(targetRect, clientY);
|
const position = calcPosition(targetRect, clientY);
|
||||||
@ -37,8 +41,12 @@ export function useDrag(props: Props) {
|
|||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (isDragging) return;
|
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);
|
setIsDraggingOver(true);
|
||||||
const targetRect = e.currentTarget.getBoundingClientRect();
|
const targetRect = currentTarget.getBoundingClientRect();
|
||||||
const { clientY } = e;
|
const { clientY } = e;
|
||||||
const position = calcPosition(targetRect, clientY);
|
const position = calcPosition(targetRect, clientY);
|
||||||
|
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import { MenuItem, Typography } from '@mui/material';
|
import { MenuItem, Typography } from '@mui/material';
|
||||||
import { scrollIntoView } from '$app/components/_shared/keyboard_navigation/utils';
|
import { scrollIntoView } from '$app/components/_shared/keyboard_navigation/utils';
|
||||||
import { ReactEditor, useSlateStatic } from 'slate-react';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -35,7 +34,7 @@ export interface KeyboardNavigationOption<T = string> {
|
|||||||
* - onBlur: called when the keyboard navigation is blurred
|
* - onBlur: called when the keyboard navigation is blurred
|
||||||
*/
|
*/
|
||||||
export interface KeyboardNavigationProps<T> {
|
export interface KeyboardNavigationProps<T> {
|
||||||
scrollRef: React.RefObject<HTMLDivElement>;
|
scrollRef?: React.RefObject<HTMLDivElement>;
|
||||||
focusRef?: React.RefObject<HTMLElement>;
|
focusRef?: React.RefObject<HTMLElement>;
|
||||||
options: KeyboardNavigationOption<T>[];
|
options: KeyboardNavigationOption<T>[];
|
||||||
onSelected?: (optionKey: T) => void;
|
onSelected?: (optionKey: T) => void;
|
||||||
@ -68,7 +67,6 @@ function KeyboardNavigation<T>({
|
|||||||
onFocus,
|
onFocus,
|
||||||
}: KeyboardNavigationProps<T>) {
|
}: KeyboardNavigationProps<T>) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const editor = useSlateStatic();
|
|
||||||
const ref = useRef<HTMLDivElement>(null);
|
const ref = useRef<HTMLDivElement>(null);
|
||||||
const mouseY = useRef<number | null>(null);
|
const mouseY = useRef<number | null>(null);
|
||||||
const defaultKeyRef = useRef<T | undefined>(defaultFocusedKey);
|
const defaultKeyRef = useRef<T | undefined>(defaultFocusedKey);
|
||||||
@ -108,7 +106,7 @@ function KeyboardNavigation<T>({
|
|||||||
if (focusedKey === undefined) return;
|
if (focusedKey === undefined) return;
|
||||||
onSelected?.(focusedKey);
|
onSelected?.(focusedKey);
|
||||||
|
|
||||||
const scrollElement = scrollRef.current;
|
const scrollElement = scrollRef?.current;
|
||||||
|
|
||||||
if (!scrollElement) return;
|
if (!scrollElement) return;
|
||||||
|
|
||||||
@ -262,15 +260,15 @@ function KeyboardNavigation<T>({
|
|||||||
let element: HTMLElement | null | undefined = focusRef?.current;
|
let element: HTMLElement | null | undefined = focusRef?.current;
|
||||||
|
|
||||||
if (!element) {
|
if (!element) {
|
||||||
element = ReactEditor.toDOMNode(editor, editor);
|
element = document.activeElement as HTMLElement;
|
||||||
}
|
}
|
||||||
|
|
||||||
element.addEventListener('keydown', onKeyDown);
|
element?.addEventListener('keydown', onKeyDown);
|
||||||
return () => {
|
return () => {
|
||||||
element?.removeEventListener('keydown', onKeyDown);
|
element?.removeEventListener('keydown', onKeyDown);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}, [disableFocus, editor, onKeyDown, focusRef]);
|
}, [disableFocus, onKeyDown, focusRef]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
@ -13,6 +13,8 @@ import { useTranslation } from 'react-i18next';
|
|||||||
import { ErrorCode, FolderNotification } from '@/services/backend';
|
import { ErrorCode, FolderNotification } from '@/services/backend';
|
||||||
import ExpandRecordModal from '$app/components/database/components/edit_record/ExpandRecordModal';
|
import ExpandRecordModal from '$app/components/database/components/edit_record/ExpandRecordModal';
|
||||||
import { subscribeNotifications } from '$app/application/notification';
|
import { subscribeNotifications } from '$app/application/notification';
|
||||||
|
import { Page } from '$app_reducers/pages/slice';
|
||||||
|
import { getPage } from '$app/application/folder/page.service';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
selectedViewId?: string;
|
selectedViewId?: string;
|
||||||
@ -22,11 +24,12 @@ interface Props {
|
|||||||
export const Database = forwardRef<HTMLDivElement, Props>(({ selectedViewId, setSelectedViewId }, ref) => {
|
export const Database = forwardRef<HTMLDivElement, Props>(({ selectedViewId, setSelectedViewId }, ref) => {
|
||||||
const innerRef = useRef<HTMLDivElement>();
|
const innerRef = useRef<HTMLDivElement>();
|
||||||
const databaseRef = (ref ?? innerRef) as React.MutableRefObject<HTMLDivElement>;
|
const databaseRef = (ref ?? innerRef) as React.MutableRefObject<HTMLDivElement>;
|
||||||
|
|
||||||
const viewId = useViewId();
|
const viewId = useViewId();
|
||||||
|
|
||||||
|
const [page, setPage] = useState<Page | null>(null);
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [notFound, setNotFound] = useState(false);
|
const [notFound, setNotFound] = useState(false);
|
||||||
const [childViewIds, setChildViewIds] = useState<string[]>([]);
|
const [childViews, setChildViews] = useState<Page[]>([]);
|
||||||
const [editRecordRowId, setEditRecordRowId] = useState<string | null>(null);
|
const [editRecordRowId, setEditRecordRowId] = useState<string | null>(null);
|
||||||
const [openCollections, setOpenCollections] = useState<string[]>([]);
|
const [openCollections, setOpenCollections] = useState<string[]>([]);
|
||||||
|
|
||||||
@ -34,7 +37,7 @@ export const Database = forwardRef<HTMLDivElement, Props>(({ selectedViewId, set
|
|||||||
await databaseViewService
|
await databaseViewService
|
||||||
.getDatabaseViews(viewId)
|
.getDatabaseViews(viewId)
|
||||||
.then((value) => {
|
.then((value) => {
|
||||||
setChildViewIds(value.map((view) => view.id));
|
setChildViews(value);
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
if (err.code === ErrorCode.RecordNotFound) {
|
if (err.code === ErrorCode.RecordNotFound) {
|
||||||
@ -43,10 +46,39 @@ export const Database = forwardRef<HTMLDivElement, Props>(({ selectedViewId, set
|
|||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const handleGetPage = useCallback(async () => {
|
||||||
|
try {
|
||||||
|
const page = await getPage(viewId);
|
||||||
|
|
||||||
|
setPage(page);
|
||||||
|
} catch (e) {
|
||||||
|
setNotFound(true);
|
||||||
|
}
|
||||||
|
}, [viewId]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
void handleGetPage();
|
||||||
void handleResetDatabaseViews(viewId);
|
void handleResetDatabaseViews(viewId);
|
||||||
const unsubscribePromise = subscribeNotifications(
|
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) => {
|
[FolderNotification.DidUpdateChildViews]: (changeset) => {
|
||||||
if (changeset.create_child_views.length === 0 && changeset.delete_child_views.length === 0) {
|
if (changeset.create_child_views.length === 0 && changeset.delete_child_views.length === 0) {
|
||||||
return;
|
return;
|
||||||
@ -61,11 +93,35 @@ export const Database = forwardRef<HTMLDivElement, Props>(({ selectedViewId, set
|
|||||||
);
|
);
|
||||||
|
|
||||||
return () => void unsubscribePromise.then((unsubscribe) => unsubscribe());
|
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(() => {
|
const value = useMemo(() => {
|
||||||
return Math.max(0, childViewIds.indexOf(selectedViewId ?? viewId));
|
return Math.max(
|
||||||
}, [childViewIds, selectedViewId, viewId]);
|
0,
|
||||||
|
childViews.findIndex((view) => view.id === (selectedViewId ?? viewId))
|
||||||
|
);
|
||||||
|
}, [childViews, selectedViewId, viewId]);
|
||||||
|
|
||||||
const onToggleCollection = useCallback(
|
const onToggleCollection = useCallback(
|
||||||
(id: string, forceOpen?: boolean) => {
|
(id: string, forceOpen?: boolean) => {
|
||||||
@ -110,7 +166,7 @@ export const Database = forwardRef<HTMLDivElement, Props>(({ selectedViewId, set
|
|||||||
pageId={viewId}
|
pageId={viewId}
|
||||||
setSelectedViewId={setSelectedViewId}
|
setSelectedViewId={setSelectedViewId}
|
||||||
selectedViewId={selectedViewId}
|
selectedViewId={selectedViewId}
|
||||||
childViewIds={childViewIds}
|
childViews={childViews}
|
||||||
/>
|
/>
|
||||||
<SwipeableViews
|
<SwipeableViews
|
||||||
slideStyle={{
|
slideStyle={{
|
||||||
@ -120,19 +176,19 @@ export const Database = forwardRef<HTMLDivElement, Props>(({ selectedViewId, set
|
|||||||
axis={'x'}
|
axis={'x'}
|
||||||
index={value}
|
index={value}
|
||||||
>
|
>
|
||||||
{childViewIds.map((id, index) => (
|
{childViews.map((view, index) => (
|
||||||
<TabPanel className={'flex h-full w-full flex-col'} key={id} index={index} value={value}>
|
<TabPanel className={'flex h-full w-full flex-col'} key={view.id} index={index} value={value}>
|
||||||
<DatabaseLoader viewId={id}>
|
<DatabaseLoader viewId={view.id}>
|
||||||
{selectedViewId === id && (
|
{selectedViewId === view.id && (
|
||||||
<>
|
<>
|
||||||
<Portal container={databaseRef.current}>
|
<Portal container={databaseRef.current}>
|
||||||
<div className={'absolute right-16 top-0 py-1'}>
|
<div className={'absolute right-16 top-0 py-1'}>
|
||||||
<DatabaseSettings
|
<DatabaseSettings
|
||||||
onToggleCollection={(forceOpen?: boolean) => onToggleCollection(id, forceOpen)}
|
onToggleCollection={(forceOpen?: boolean) => onToggleCollection(view.id, forceOpen)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Portal>
|
</Portal>
|
||||||
<DatabaseCollection open={openCollections.includes(id)} />
|
<DatabaseCollection open={openCollections.includes(view.id)} />
|
||||||
{editRecordRowId && (
|
{editRecordRowId && (
|
||||||
<ExpandRecordModal
|
<ExpandRecordModal
|
||||||
rowId={editRecordRowId}
|
rowId={editRecordRowId}
|
||||||
|
@ -10,7 +10,7 @@ import { useViewId } from '$app/hooks';
|
|||||||
import { fieldService } from '$app/application/database';
|
import { fieldService } from '$app/application/database';
|
||||||
import { OrderObjectPositionTypePB, FieldVisibility } from '@/services/backend';
|
import { OrderObjectPositionTypePB, FieldVisibility } from '@/services/backend';
|
||||||
import { MenuItem } from '@mui/material';
|
import { MenuItem } from '@mui/material';
|
||||||
import DeleteConfirmDialog from '$app/components/_shared/delete_confirm_dialog/DeleteConfirmDialog';
|
import DeleteConfirmDialog from '$app/components/_shared/confirm_dialog/DeleteConfirmDialog';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
export enum FieldAction {
|
export enum FieldAction {
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import { FC, FunctionComponent, SVGProps, useEffect, useState } from 'react';
|
import { FC, FunctionComponent, SVGProps, useEffect, useMemo, useState } from 'react';
|
||||||
import { ViewTabs, ViewTab } from './ViewTabs';
|
import { ViewTabs, ViewTab } from './ViewTabs';
|
||||||
import { useAppSelector } from '$app/stores/store';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import AddViewBtn from '$app/components/database/components/tab_bar/AddViewBtn';
|
import AddViewBtn from '$app/components/database/components/tab_bar/AddViewBtn';
|
||||||
import { ViewLayoutPB } from '@/services/backend';
|
import { ViewLayoutPB } from '@/services/backend';
|
||||||
@ -11,7 +10,7 @@ import ViewActions from '$app/components/database/components/tab_bar/ViewActions
|
|||||||
import { Page } from '$app_reducers/pages/slice';
|
import { Page } from '$app_reducers/pages/slice';
|
||||||
|
|
||||||
export interface DatabaseTabBarProps {
|
export interface DatabaseTabBarProps {
|
||||||
childViewIds: string[];
|
childViews: Page[];
|
||||||
selectedViewId?: string;
|
selectedViewId?: string;
|
||||||
setSelectedViewId?: (viewId: string) => void;
|
setSelectedViewId?: (viewId: string) => void;
|
||||||
pageId: string;
|
pageId: string;
|
||||||
@ -26,26 +25,21 @@ const DatabaseIcons: {
|
|||||||
[ViewLayoutPB.Calendar]: GridSvg,
|
[ViewLayoutPB.Calendar]: GridSvg,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const DatabaseTabBar: FC<DatabaseTabBarProps> = ({ pageId, childViewIds, selectedViewId, setSelectedViewId }) => {
|
export const DatabaseTabBar: FC<DatabaseTabBarProps> = ({ pageId, childViews, selectedViewId, setSelectedViewId }) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [contextMenuAnchorEl, setContextMenuAnchorEl] = useState<HTMLElement | null>(null);
|
const [contextMenuAnchorEl, setContextMenuAnchorEl] = useState<HTMLElement | null>(null);
|
||||||
const [contextMenuView, setContextMenuView] = useState<Page | null>(null);
|
const [contextMenuView, setContextMenuView] = useState<Page | null>(null);
|
||||||
const open = Boolean(contextMenuAnchorEl);
|
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) => {
|
const handleChange = (_: React.SyntheticEvent, newValue: string) => {
|
||||||
setSelectedViewId?.(newValue);
|
setSelectedViewId?.(newValue);
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (selectedViewId === undefined && views.length > 0) {
|
if (selectedViewId === undefined && childViews.length > 0) {
|
||||||
setSelectedViewId?.(views[0].id);
|
setSelectedViewId?.(childViews[0].id);
|
||||||
}
|
}
|
||||||
}, [selectedViewId, setSelectedViewId, views]);
|
}, [selectedViewId, setSelectedViewId, childViews]);
|
||||||
|
|
||||||
const openMenu = (view: Page) => {
|
const openMenu = (view: Page) => {
|
||||||
return (e: React.MouseEvent<HTMLElement>) => {
|
return (e: React.MouseEvent<HTMLElement>) => {
|
||||||
@ -56,16 +50,19 @@ export const DatabaseTabBar: FC<DatabaseTabBarProps> = ({ pageId, childViewIds,
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const isSelected = useMemo(() => childViews.some((view) => view.id === selectedViewId), [childViews, selectedViewId]);
|
||||||
|
|
||||||
|
if (childViews.length === 0) return null;
|
||||||
return (
|
return (
|
||||||
<div className='-mb-px flex items-center px-16'>
|
<div className='-mb-px flex items-center px-16'>
|
||||||
<div className='flex flex-1 items-center border-b border-line-divider'>
|
<div className='flex flex-1 items-center border-b border-line-divider'>
|
||||||
<ViewTabs value={selectedViewId} onChange={handleChange}>
|
<ViewTabs value={isSelected ? selectedViewId : childViews[0].id} onChange={handleChange}>
|
||||||
{views.map((view) => {
|
{childViews.map((view) => {
|
||||||
const Icon = DatabaseIcons[view.layout];
|
const Icon = DatabaseIcons[view.layout];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ViewTab
|
<ViewTab
|
||||||
onContextMenu={openMenu(view)}
|
onContextMenuCapture={openMenu(view)}
|
||||||
onDoubleClick={openMenu(view)}
|
onDoubleClick={openMenu(view)}
|
||||||
key={view.id}
|
key={view.id}
|
||||||
icon={<Icon />}
|
icon={<Icon />}
|
||||||
@ -81,6 +78,7 @@ export const DatabaseTabBar: FC<DatabaseTabBarProps> = ({ pageId, childViewIds,
|
|||||||
</div>
|
</div>
|
||||||
{open && contextMenuView && (
|
{open && contextMenuView && (
|
||||||
<ViewActions
|
<ViewActions
|
||||||
|
pageId={pageId}
|
||||||
view={contextMenuView}
|
view={contextMenuView}
|
||||||
keepMounted={false}
|
keepMounted={false}
|
||||||
open={open}
|
open={open}
|
||||||
|
@ -4,7 +4,7 @@ import { ReactComponent as DeleteSvg } from '$app/assets/delete.svg';
|
|||||||
import { ReactComponent as EditSvg } from '$app/assets/edit.svg';
|
import { ReactComponent as EditSvg } from '$app/assets/edit.svg';
|
||||||
import { deleteView } from '$app/application/database/database_view/database_view_service';
|
import { deleteView } from '$app/application/database/database_view/database_view_service';
|
||||||
import { MenuItem, MenuProps, Menu } from '@mui/material';
|
import { MenuItem, MenuProps, Menu } from '@mui/material';
|
||||||
import RenameDialog from '$app/components/layout/nested_page/RenameDialog';
|
import RenameDialog from '$app/components/_shared/confirm_dialog/RenameDialog';
|
||||||
import { Page } from '$app_reducers/pages/slice';
|
import { Page } from '$app_reducers/pages/slice';
|
||||||
import { useAppDispatch } from '$app/stores/store';
|
import { useAppDispatch } from '$app/stores/store';
|
||||||
import { updatePageName } from '$app_reducers/pages/async_actions';
|
import { updatePageName } from '$app_reducers/pages/async_actions';
|
||||||
@ -14,7 +14,7 @@ enum ViewAction {
|
|||||||
Delete,
|
Delete,
|
||||||
}
|
}
|
||||||
|
|
||||||
function ViewActions({ view, ...props }: { view: Page } & MenuProps) {
|
function ViewActions({ view, pageId, ...props }: { pageId: string; view: Page } & MenuProps) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const viewId = view.id;
|
const viewId = view.id;
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
@ -31,6 +31,7 @@ function ViewActions({ view, ...props }: { view: Page } & MenuProps) {
|
|||||||
|
|
||||||
{
|
{
|
||||||
id: ViewAction.Delete,
|
id: ViewAction.Delete,
|
||||||
|
disabled: viewId === pageId,
|
||||||
label: t('button.delete'),
|
label: t('button.delete'),
|
||||||
icon: <DeleteSvg />,
|
icon: <DeleteSvg />,
|
||||||
action: async () => {
|
action: async () => {
|
||||||
@ -48,31 +49,33 @@ function ViewActions({ view, ...props }: { view: Page } & MenuProps) {
|
|||||||
<>
|
<>
|
||||||
<Menu keepMounted={false} disableRestoreFocus={true} {...props}>
|
<Menu keepMounted={false} disableRestoreFocus={true} {...props}>
|
||||||
{options.map((option) => (
|
{options.map((option) => (
|
||||||
<MenuItem key={option.id} onClick={option.action}>
|
<MenuItem disabled={option.disabled} key={option.id} onClick={option.action}>
|
||||||
<div className={'mr-1.5'}>{option.icon}</div>
|
<div className={'mr-1.5'}>{option.icon}</div>
|
||||||
{option.label}
|
{option.label}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
))}
|
))}
|
||||||
</Menu>
|
</Menu>
|
||||||
<RenameDialog
|
{openRenameDialog && (
|
||||||
open={openRenameDialog}
|
<RenameDialog
|
||||||
onClose={() => setOpenRenameDialog(false)}
|
open={openRenameDialog}
|
||||||
onOk={async (val) => {
|
onClose={() => setOpenRenameDialog(false)}
|
||||||
try {
|
onOk={async (val) => {
|
||||||
await dispatch(
|
try {
|
||||||
updatePageName({
|
await dispatch(
|
||||||
id: viewId,
|
updatePageName({
|
||||||
name: val,
|
id: viewId,
|
||||||
})
|
name: val,
|
||||||
);
|
})
|
||||||
setOpenRenameDialog(false);
|
);
|
||||||
props.onClose?.({}, 'backdropClick');
|
setOpenRenameDialog(false);
|
||||||
} catch (e) {
|
props.onClose?.({}, 'backdropClick');
|
||||||
// toast.error(t('error.renameView'));
|
} catch (e) {
|
||||||
}
|
// toast.error(t('error.renameView'));
|
||||||
}}
|
}
|
||||||
defaultValue={view.name}
|
}}
|
||||||
/>
|
defaultValue={view.name}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -19,7 +19,7 @@ export function DocumentHeader({ page }: DocumentHeaderProps) {
|
|||||||
|
|
||||||
if (!page) return null;
|
if (!page) return null;
|
||||||
return (
|
return (
|
||||||
<div className={'document-header px-16 pt-4'}>
|
<div className={'document-header select-none px-16 pt-4'}>
|
||||||
<ViewTitle showTitle={false} onUpdateIcon={onUpdateIcon} view={page} />
|
<ViewTitle showTitle={false} onUpdateIcon={onUpdateIcon} view={page} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -1,24 +1,13 @@
|
|||||||
import { useAppSelector } from '$app/stores/store';
|
import { useAppSelector } from '$app/stores/store';
|
||||||
import { useEffect, useState } from 'react';
|
|
||||||
import { Page } from '$app_reducers/pages/slice';
|
|
||||||
import { ViewLayoutPB } from '@/services/backend';
|
import { ViewLayoutPB } from '@/services/backend';
|
||||||
|
|
||||||
export function useLoadDatabaseList({ searchText, layout }: { searchText: string; layout: ViewLayoutPB }) {
|
export function useLoadDatabaseList({ searchText, layout }: { searchText: string; layout: ViewLayoutPB }) {
|
||||||
const [list, setList] = useState<Page[]>([]);
|
const list = useAppSelector((state) => {
|
||||||
const pages = useAppSelector((state) => state.pages.pageMap);
|
return Object.values(state.pages.pageMap).filter((page) => {
|
||||||
|
if (page.layout !== layout) return false;
|
||||||
useEffect(() => {
|
return page.name.toLowerCase().includes(searchText.toLowerCase());
|
||||||
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]);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
list,
|
list,
|
||||||
|
@ -21,7 +21,7 @@ export const Text = memo(
|
|||||||
{renderIcon()}
|
{renderIcon()}
|
||||||
<Placeholder isEmpty={isEmpty} node={node} />
|
<Placeholder isEmpty={isEmpty} node={node} />
|
||||||
|
|
||||||
<span className={'min-w-[4px]'}>{children}</span>
|
<span className={'text-content min-w-[4px]'}>{children}</span>
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
|
@ -59,7 +59,7 @@ export const InlineFormula = memo(
|
|||||||
contentEditable={false}
|
contentEditable={false}
|
||||||
onDoubleClick={handleClick}
|
onDoubleClick={handleClick}
|
||||||
onClick={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' : ''
|
selected ? 'selected' : ''
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
|
@ -6,17 +6,29 @@ import { useNavigate } from 'react-router-dom';
|
|||||||
import { pageTypeMap } from '$app_reducers/pages/slice';
|
import { pageTypeMap } from '$app_reducers/pages/slice';
|
||||||
import { getPage } from '$app/application/folder/page.service';
|
import { getPage } from '$app/application/folder/page.service';
|
||||||
import { useSelected } from 'slate-react';
|
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 }) {
|
export function MentionLeaf({ children, mention }: { mention: Mention; children: React.ReactNode }) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [page, setPage] = useState<MentionPage | null>(null);
|
const [page, setPage] = useState<MentionPage | null>(null);
|
||||||
|
const [error, setError] = useState<boolean>(false);
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const selected = useSelected();
|
const selected = useSelected();
|
||||||
const loadPage = useCallback(async () => {
|
const loadPage = useCallback(async () => {
|
||||||
|
setError(true);
|
||||||
if (!mention.page) return;
|
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]);
|
}, [mention.page]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -24,26 +36,87 @@ export function MentionLeaf({ children, mention }: { mention: Mention; children:
|
|||||||
}, [loadPage]);
|
}, [loadPage]);
|
||||||
|
|
||||||
const openPage = useCallback(() => {
|
const openPage = useCallback(() => {
|
||||||
if (!page) return;
|
if (!page) {
|
||||||
|
notify.error(t('document.mention.deletedContent'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const pageType = pageTypeMap[page.layout];
|
const pageType = pageTypeMap[page.layout];
|
||||||
|
|
||||||
navigate(`/page/${pageType}/${page.id}`);
|
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 (
|
return (
|
||||||
<span className={'relative'}>
|
<span className={'relative'}>
|
||||||
{page && (
|
<span
|
||||||
<span
|
className={'relative mx-1 inline-flex cursor-pointer items-center hover:rounded hover:bg-content-blue-100'}
|
||||||
className={'relative mx-1 inline-flex cursor-pointer items-center hover:rounded hover:bg-content-blue-100'}
|
onClick={openPage}
|
||||||
onClick={openPage}
|
style={{
|
||||||
style={{
|
backgroundColor: selected ? 'var(--content-blue-100)' : undefined,
|
||||||
backgroundColor: selected ? 'var(--content-blue-100)' : undefined,
|
}}
|
||||||
}}
|
>
|
||||||
>
|
{page && (
|
||||||
<span className={'text-sx absolute left-0.5'}>{page.icon?.value || <DocumentSvg />}</span>
|
<>
|
||||||
<span className={'ml-6 mr-0.5 underline'}>{page.name || t('document.title.placeholder')}</span>
|
<span className={'text-sx absolute left-0.5'}>{page.icon?.value || <DocumentSvg />}</span>
|
||||||
</span>
|
<span className={'ml-6 mr-0.5 underline'}>{page.name || t('document.title.placeholder')}</span>
|
||||||
)}
|
</>
|
||||||
|
)}
|
||||||
|
{error && (
|
||||||
|
<>
|
||||||
|
<span className={'text-sx absolute left-0.5'}>
|
||||||
|
<EyeClose />
|
||||||
|
</span>
|
||||||
|
<span className={'ml-6 mr-0.5 text-text-caption underline'}>{t('document.mention.deleted')}</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
|
||||||
<span className={'invisible'}>{children}</span>
|
<span className={'invisible'}>{children}</span>
|
||||||
</span>
|
</span>
|
||||||
|
@ -22,9 +22,12 @@ const Toolbar = () => {
|
|||||||
const handleOpen = useCallback(() => {
|
const handleOpen = useCallback(() => {
|
||||||
if (!node || !node.blockId) return;
|
if (!node || !node.blockId) return;
|
||||||
setOpenContextMenu(true);
|
setOpenContextMenu(true);
|
||||||
|
const path = ReactEditor.findPath(editor, node);
|
||||||
|
|
||||||
|
editor.select(path);
|
||||||
selectedBlockContext.clear();
|
selectedBlockContext.clear();
|
||||||
selectedBlockContext.add(node.blockId);
|
selectedBlockContext.add(node.blockId);
|
||||||
}, [node, selectedBlockContext]);
|
}, [editor, node, selectedBlockContext]);
|
||||||
|
|
||||||
const handleClose = useCallback(() => {
|
const handleClose = useCallback(() => {
|
||||||
setOpenContextMenu(false);
|
setOpenContextMenu(false);
|
||||||
|
@ -12,7 +12,8 @@ import KeyboardNavigation, {
|
|||||||
KeyboardNavigationOption,
|
KeyboardNavigationOption,
|
||||||
} from '$app/components/_shared/keyboard_navigation/KeyboardNavigation';
|
} from '$app/components/_shared/keyboard_navigation/KeyboardNavigation';
|
||||||
import { Color } from '$app/components/editor/components/tools/block_actions/color';
|
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 isHotkey from 'is-hotkey';
|
||||||
import { EditorNodeType } from '$app/application/document/document.types';
|
import { EditorNodeType } from '$app/application/document/document.types';
|
||||||
import { EditorSelectedBlockContext } from '$app/components/editor/stores/selected';
|
import { EditorSelectedBlockContext } from '$app/components/editor/stores/selected';
|
||||||
|
@ -25,10 +25,8 @@
|
|||||||
.block-element.block-align-center {
|
.block-element.block-align-center {
|
||||||
> div > .text-element {
|
> div > .text-element {
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -52,12 +50,35 @@ span[data-slate-placeholder="true"]:not(.inline-block-content) {
|
|||||||
::selection {
|
::selection {
|
||||||
@apply bg-content-blue-100;
|
@apply bg-content-blue-100;
|
||||||
}
|
}
|
||||||
|
.text-content {
|
||||||
|
&::selection {
|
||||||
|
@apply bg-transparent;
|
||||||
|
}
|
||||||
|
span {
|
||||||
|
&::selection {
|
||||||
|
@apply bg-content-blue-100;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
[data-dark-mode="true"] [role="textbox"]{
|
[data-dark-mode="true"] [role="textbox"]{
|
||||||
::selection {
|
::selection {
|
||||||
background-color: #1e79a2;
|
background-color: #1e79a2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.text-content {
|
||||||
|
&::selection {
|
||||||
|
@apply bg-transparent;
|
||||||
|
}
|
||||||
|
span {
|
||||||
|
&::selection {
|
||||||
|
background-color: #1e79a2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,17 +1,5 @@
|
|||||||
import { EditorMarkFormat } from '$app/application/document/document.types';
|
import { EditorMarkFormat } from '$app/application/document/document.types';
|
||||||
|
import { getModifier } from '$app/utils/get_modifier';
|
||||||
export const isMac = () => {
|
|
||||||
return navigator.userAgent.includes('Mac OS X');
|
|
||||||
};
|
|
||||||
|
|
||||||
const MODIFIERS = {
|
|
||||||
control: 'Ctrl',
|
|
||||||
meta: '⌘',
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getModifier = () => {
|
|
||||||
return isMac() ? MODIFIERS.meta : MODIFIERS.control;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Hotkeys shortcuts
|
* Hotkeys shortcuts
|
||||||
|
@ -47,6 +47,7 @@ export function useShortcuts(editor: ReactEditor) {
|
|||||||
|
|
||||||
if (format) {
|
if (format) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
if (CustomEditor.selectionIncludeRoot(editor)) return;
|
||||||
return CustomEditor.toggleMark(editor, {
|
return CustomEditor.toggleMark(editor, {
|
||||||
key: format,
|
key: format,
|
||||||
value: true,
|
value: true,
|
||||||
@ -66,7 +67,7 @@ export function useShortcuts(editor: ReactEditor) {
|
|||||||
if (isHotkey(item.hotkey, e)) {
|
if (isHotkey(item.hotkey, e)) {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
if (CustomEditor.selectionIncludeRoot(editor)) return;
|
||||||
if (item.markKey === EditorMarkFormat.Align) {
|
if (item.markKey === EditorMarkFormat.Align) {
|
||||||
CustomEditor.toggleAlign(editor, item.markValue as string);
|
CustomEditor.toggleAlign(editor, item.markValue as string);
|
||||||
return;
|
return;
|
||||||
|
@ -1,12 +1,12 @@
|
|||||||
export const FooterPanel = () => {
|
export const FooterPanel = () => {
|
||||||
return (
|
return (
|
||||||
<div className={'flex items-center justify-between px-2 py-2'}>
|
<div className={'flex h-[48px] items-center justify-between px-2 py-2'}>
|
||||||
<div className={'text-xs text-text-caption'}>
|
<div className={'text-xs text-text-caption'}>
|
||||||
© 2024 AppFlowy. <a href={'https://github.com/AppFlowy-IO/AppFlowy'}>GitHub</a>
|
© 2024 AppFlowy. <a href={'https://github.com/AppFlowy-IO/AppFlowy'}>GitHub</a>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
{/*<div>*/}
|
||||||
<button className={'h-8 w-8 rounded bg-content-blue-50 text-text-title hover:bg-content-blue-100'}>?</button>
|
{/* <button className={'h-8 w-8 rounded bg-content-blue-50 text-text-title hover:bg-content-blue-100'}>?</button>*/}
|
||||||
</div>
|
{/*</div>*/}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -2,7 +2,7 @@ import React, { ReactNode, useEffect } from 'react';
|
|||||||
import SideBar from '$app/components/layout/side_bar/SideBar';
|
import SideBar from '$app/components/layout/side_bar/SideBar';
|
||||||
import TopBar from '$app/components/layout/top_bar/TopBar';
|
import TopBar from '$app/components/layout/top_bar/TopBar';
|
||||||
import { useAppSelector } from '$app/stores/store';
|
import { useAppSelector } from '$app/stores/store';
|
||||||
import { FooterPanel } from '$app/components/layout/FooterPanel';
|
import './layout.scss';
|
||||||
|
|
||||||
function Layout({ children }: { children: ReactNode }) {
|
function Layout({ children }: { children: ReactNode }) {
|
||||||
const { isCollapsed, width } = useAppSelector((state) => state.sidebar);
|
const { isCollapsed, width } = useAppSelector((state) => state.sidebar);
|
||||||
@ -38,8 +38,6 @@ function Layout({ children }: { children: ReactNode }) {
|
|||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<FooterPanel />
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
@ -6,10 +6,11 @@ import Typography from '@mui/material/Typography';
|
|||||||
import { Page, pageTypeMap } from '$app_reducers/pages/slice';
|
import { Page, pageTypeMap } from '$app_reducers/pages/slice';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { getPageIcon } from '$app/hooks/page.hooks';
|
||||||
|
|
||||||
function Breadcrumb() {
|
function Breadcrumb() {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { pagePath, currentPage } = useLoadExpandedPages();
|
const { isTrash, pagePath, currentPage } = useLoadExpandedPages();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const parentPages = useMemo(() => pagePath.slice(1, -1).filter(Boolean) as Page[], [pagePath]);
|
const parentPages = useMemo(() => pagePath.slice(1, -1).filter(Boolean) as Page[], [pagePath]);
|
||||||
@ -22,21 +23,35 @@ function Breadcrumb() {
|
|||||||
[navigate]
|
[navigate]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (!currentPage) {
|
||||||
|
if (isTrash) {
|
||||||
|
return <Typography color='text.primary'>{t('trash.text')}</Typography>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Breadcrumbs aria-label='breadcrumb'>
|
<Breadcrumbs aria-label='breadcrumb'>
|
||||||
{parentPages?.map((page: Page) => (
|
{parentPages?.map((page: Page) => (
|
||||||
<Link
|
<Link
|
||||||
key={page.id}
|
key={page.id}
|
||||||
|
className={'flex cursor-pointer gap-1'}
|
||||||
underline='hover'
|
underline='hover'
|
||||||
color='inherit'
|
color='inherit'
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
navigateToPage(page);
|
navigateToPage(page);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
<div>{getPageIcon(page)}</div>
|
||||||
|
|
||||||
{page.name || t('document.title.placeholder')}
|
{page.name || t('document.title.placeholder')}
|
||||||
</Link>
|
</Link>
|
||||||
))}
|
))}
|
||||||
<Typography color='text.primary'>{currentPage?.name || t('menuAppHeader.defaultNewPageName')}</Typography>
|
<Typography className={'flex select-auto gap-1'} color='text.primary'>
|
||||||
|
<div>{getPageIcon(currentPage)}</div>
|
||||||
|
{currentPage?.name || t('menuAppHeader.defaultNewPageName')}
|
||||||
|
</Typography>
|
||||||
</Breadcrumbs>
|
</Breadcrumbs>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -2,11 +2,9 @@ import { useAppSelector } from '$app/stores/store';
|
|||||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
import { useParams, useLocation } from 'react-router-dom';
|
import { useParams, useLocation } from 'react-router-dom';
|
||||||
import { Page } from '$app_reducers/pages/slice';
|
import { Page } from '$app_reducers/pages/slice';
|
||||||
import { useTranslation } from 'react-i18next';
|
|
||||||
import { getPage } from '$app/application/folder/page.service';
|
import { getPage } from '$app/application/folder/page.service';
|
||||||
|
|
||||||
export function useLoadExpandedPages() {
|
export function useLoadExpandedPages() {
|
||||||
const { t } = useTranslation();
|
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const isTrash = useMemo(() => location.pathname.includes('trash'), [location.pathname]);
|
const isTrash = useMemo(() => location.pathname.includes('trash'), [location.pathname]);
|
||||||
@ -70,18 +68,9 @@ export function useLoadExpandedPages() {
|
|||||||
});
|
});
|
||||||
}, [pageMap]);
|
}, [pageMap]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (isTrash) {
|
|
||||||
setPagePath([
|
|
||||||
{
|
|
||||||
name: t('trash.text'),
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}, [isTrash, t]);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
pagePath,
|
pagePath,
|
||||||
currentPage,
|
currentPage,
|
||||||
|
isTrash,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -1,22 +1,51 @@
|
|||||||
import React from 'react';
|
import React, { useCallback, useEffect, useMemo } from 'react';
|
||||||
import { IconButton } from '@mui/material';
|
import { IconButton, Tooltip } from '@mui/material';
|
||||||
|
|
||||||
import { useAppDispatch, useAppSelector } from '$app/stores/store';
|
import { useAppDispatch, useAppSelector } from '$app/stores/store';
|
||||||
import { sidebarActions } from '$app_reducers/sidebar/slice';
|
import { sidebarActions } from '$app_reducers/sidebar/slice';
|
||||||
import { ReactComponent as LeftSvg } from '$app/assets/left.svg';
|
import { ReactComponent as ShowMenuIcon } from '$app/assets/show-menu.svg';
|
||||||
import { ReactComponent as RightSvg } from '$app/assets/right.svg';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { getModifier } from '$app/utils/get_modifier';
|
||||||
|
import isHotkey from 'is-hotkey';
|
||||||
|
|
||||||
function CollapseMenuButton() {
|
function CollapseMenuButton() {
|
||||||
const isCollapsed = useAppSelector((state) => state.sidebar.isCollapsed);
|
const isCollapsed = useAppSelector((state) => state.sidebar.isCollapsed);
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const handleClick = () => {
|
const handleClick = useCallback(() => {
|
||||||
dispatch(sidebarActions.toggleCollapse());
|
dispatch(sidebarActions.toggleCollapse());
|
||||||
};
|
}, [dispatch]);
|
||||||
|
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const title = useMemo(() => {
|
||||||
|
return (
|
||||||
|
<div className={'flex flex-col gap-1 text-xs'}>
|
||||||
|
<div>{isCollapsed ? t('sideBar.openSidebar') : t('sideBar.closeSidebar')}</div>
|
||||||
|
<div>{`${getModifier()} + \\`}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}, [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 (
|
return (
|
||||||
<IconButton size={'small'} className={'font-bold'} onClick={handleClick}>
|
<Tooltip title={title}>
|
||||||
{isCollapsed ? <RightSvg /> : <LeftSvg />}
|
<IconButton size={'small'} className={'font-bold text-text-title'} onClick={handleClick}>
|
||||||
</IconButton>
|
{isCollapsed ? <ShowMenuIcon /> : <ShowMenuIcon className={'rotate-180 transform'} />}
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -0,0 +1,12 @@
|
|||||||
|
.workspaces {
|
||||||
|
::-webkit-scrollbar {
|
||||||
|
width: 0px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.MuiPopover-root, .MuiPaper-root {
|
||||||
|
::-webkit-scrollbar {
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
}
|
||||||
|
}
|
@ -1,63 +1,64 @@
|
|||||||
import React, { useMemo } from 'react';
|
import React, { useCallback, useMemo } from 'react';
|
||||||
import ButtonPopoverList from '$app/components/_shared/button_menu/ButtonMenu';
|
|
||||||
import { IconButton } from '@mui/material';
|
|
||||||
import { ReactComponent as AddSvg } from '$app/assets/add.svg';
|
import { ReactComponent as AddSvg } from '$app/assets/add.svg';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { ReactComponent as DocumentSvg } from '$app/assets/document.svg';
|
import { ReactComponent as DocumentSvg } from '$app/assets/document.svg';
|
||||||
import { ReactComponent as GridSvg } from '$app/assets/grid.svg';
|
import { ReactComponent as GridSvg } from '$app/assets/grid.svg';
|
||||||
import { ViewLayoutPB } from '@/services/backend';
|
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 { 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(
|
const options = useMemo(
|
||||||
() => [
|
() => [
|
||||||
{
|
{
|
||||||
key: 'add-document',
|
key: 'document',
|
||||||
label: t('document.menuName'),
|
title: t('document.menuName'),
|
||||||
icon: (
|
icon: <DocumentSvg className={'h-4 w-4'} />,
|
||||||
<div className={'h-5 w-5'}>
|
|
||||||
<DocumentSvg />
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
onClick: () => {
|
|
||||||
onAddPage(ViewLayoutPB.Document);
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'add-grid',
|
key: 'grid',
|
||||||
label: t('grid.menuName'),
|
title: t('grid.menuName'),
|
||||||
icon: (
|
icon: <GridSvg className={'h-4 w-4'} />,
|
||||||
<div className={'h-5 w-5'}>
|
|
||||||
<GridSvg />
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
onClick: () => {
|
|
||||||
onAddPage(ViewLayoutPB.Grid);
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
[onAddPage, t]
|
[t]
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ButtonPopoverList
|
<OperationMenu
|
||||||
popoverOrigin={{
|
tooltip={t('menuAppHeader.addPageTooltip')}
|
||||||
anchorOrigin: {
|
isHovering={isHovering}
|
||||||
vertical: 'bottom',
|
onConfirm={onConfirm}
|
||||||
horizontal: 'left',
|
setHovering={setHovering}
|
||||||
},
|
options={options}
|
||||||
transformOrigin: {
|
|
||||||
vertical: 'top',
|
|
||||||
horizontal: 'left',
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
popoverOptions={options}
|
|
||||||
isVisible={isVisible}
|
|
||||||
>
|
>
|
||||||
<IconButton size={'small'}>
|
<AddSvg />
|
||||||
<AddSvg />
|
</OperationMenu>
|
||||||
</IconButton>
|
|
||||||
</ButtonPopoverList>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { ViewLayoutPB } from '@/services/backend';
|
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({
|
function DeleteDialog({
|
||||||
layout,
|
layout,
|
||||||
|
@ -1,25 +1,28 @@
|
|||||||
import React, { useMemo, useState } from 'react';
|
import React, { useCallback, useMemo, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
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 DetailsSvg } from '$app/assets/details.svg';
|
||||||
import { ReactComponent as EditSvg } from '$app/assets/edit.svg';
|
import { ReactComponent as EditSvg } from '$app/assets/edit.svg';
|
||||||
import { ReactComponent as CopySvg } from '$app/assets/copy.svg';
|
import { ReactComponent as CopySvg } from '$app/assets/copy.svg';
|
||||||
import { ReactComponent as TrashSvg } from '$app/assets/delete.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 { Page } from '$app_reducers/pages/slice';
|
||||||
import DeleteDialog from '$app/components/layout/nested_page/DeleteDialog';
|
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({
|
function MoreButton({
|
||||||
isVisible,
|
|
||||||
onDelete,
|
onDelete,
|
||||||
onDuplicate,
|
onDuplicate,
|
||||||
onRename,
|
onRename,
|
||||||
page,
|
page,
|
||||||
|
isHovering,
|
||||||
|
setHovering,
|
||||||
}: {
|
}: {
|
||||||
isVisible: boolean;
|
isHovering: boolean;
|
||||||
|
setHovering: (hovering: boolean) => void;
|
||||||
onDelete: () => Promise<void>;
|
onDelete: () => Promise<void>;
|
||||||
onDuplicate: () => Promise<void>;
|
onDuplicate: () => Promise<void>;
|
||||||
onRename: (newName: string) => Promise<void>;
|
onRename: (newName: string) => Promise<void>;
|
||||||
@ -29,84 +32,97 @@ function MoreButton({
|
|||||||
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
||||||
|
|
||||||
const { t } = useTranslation();
|
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(
|
const options = useMemo(
|
||||||
() => [
|
() => [
|
||||||
{
|
{
|
||||||
label: t('disclosureAction.rename'),
|
title: t('button.rename'),
|
||||||
|
icon: <EditSvg className={'h-4 w-4'} />,
|
||||||
key: 'rename',
|
key: 'rename',
|
||||||
icon: (
|
|
||||||
<div className={'h-5 w-5'}>
|
|
||||||
<EditSvg />
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
onClick: () => {
|
|
||||||
setRenameDialogOpen(true);
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: t('button.delete'),
|
|
||||||
key: 'delete',
|
key: 'delete',
|
||||||
onClick: () => {
|
title: t('button.delete'),
|
||||||
setDeleteDialogOpen(true);
|
icon: <TrashSvg className={'h-4 w-4'} />,
|
||||||
},
|
caption: 'Del',
|
||||||
icon: (
|
|
||||||
<div className={'h-5 w-5'}>
|
|
||||||
<TrashSvg />
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'duplicate',
|
key: 'duplicate',
|
||||||
label: t('button.duplicate'),
|
title: t('button.duplicate'),
|
||||||
onClick: onDuplicate,
|
icon: <CopySvg className={'h-4 w-4'} />,
|
||||||
icon: (
|
caption: `${getModifier()}+D`,
|
||||||
<div className={'h-5 w-5'}>
|
|
||||||
<CopySvg />
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
[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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<ButtonPopoverList
|
<OperationMenu
|
||||||
isVisible={isVisible}
|
tooltip={t('menuAppHeader.moreButtonToolTip')}
|
||||||
popoverOptions={options}
|
isHovering={isHovering}
|
||||||
popoverOrigin={{
|
setHovering={setHovering}
|
||||||
anchorOrigin: {
|
onConfirm={onConfirm}
|
||||||
vertical: 'bottom',
|
options={options}
|
||||||
horizontal: 'left',
|
onKeyDown={onKeyDown}
|
||||||
},
|
|
||||||
transformOrigin: {
|
|
||||||
vertical: 'top',
|
|
||||||
horizontal: 'left',
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<IconButton size={'small'}>
|
<DetailsSvg />
|
||||||
<DetailsSvg />
|
</OperationMenu>
|
||||||
</IconButton>
|
|
||||||
</ButtonPopoverList>
|
|
||||||
<RenameDialog
|
|
||||||
defaultValue={page.name}
|
|
||||||
open={renameDialogOpen}
|
|
||||||
onClose={() => setRenameDialogOpen(false)}
|
|
||||||
onOk={async (newName: string) => {
|
|
||||||
await onRename(newName);
|
|
||||||
setRenameDialogOpen(false);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<DeleteDialog
|
<DeleteDialog
|
||||||
layout={page.layout}
|
layout={page.layout}
|
||||||
open={deleteDialogOpen}
|
open={deleteDialogOpen}
|
||||||
onClose={() => setDeleteDialogOpen(false)}
|
onClose={() => {
|
||||||
onOk={async () => {
|
|
||||||
await onDelete();
|
|
||||||
setDeleteDialogOpen(false);
|
setDeleteDialogOpen(false);
|
||||||
}}
|
}}
|
||||||
|
onOk={onDelete}
|
||||||
/>
|
/>
|
||||||
|
{renameDialogOpen && (
|
||||||
|
<RenameDialog
|
||||||
|
defaultValue={page.name}
|
||||||
|
open={renameDialogOpen}
|
||||||
|
onClose={() => setRenameDialogOpen(false)}
|
||||||
|
onOk={onRename}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,12 +1,11 @@
|
|||||||
import { useCallback, useEffect, useMemo } from 'react';
|
import { useCallback, useEffect } from 'react';
|
||||||
import { Page, pagesActions, pageTypeMap } from '$app_reducers/pages/slice';
|
import { pagesActions, pageTypeMap, parserViewPBToPage } from '$app_reducers/pages/slice';
|
||||||
import { useAppDispatch, useAppSelector } from '$app/stores/store';
|
import { useAppDispatch, useAppSelector } from '$app/stores/store';
|
||||||
import { FolderNotification, ViewLayoutPB } from '@/services/backend';
|
import { FolderNotification, ViewLayoutPB } from '@/services/backend';
|
||||||
import { useNavigate, useParams } from 'react-router-dom';
|
import { useNavigate, useParams } from 'react-router-dom';
|
||||||
import { updatePageName } from '$app_reducers/pages/async_actions';
|
import { updatePageName } from '$app_reducers/pages/async_actions';
|
||||||
import { createPage, deletePage, duplicatePage, getChildPages } from '$app/application/folder/page.service';
|
import { createPage, deletePage, duplicatePage, getChildPages } from '$app/application/folder/page.service';
|
||||||
import { subscribeNotifications } from '$app/application/notification';
|
import { subscribeNotifications } from '$app/application/notification';
|
||||||
import debounce from 'lodash-es/debounce';
|
|
||||||
|
|
||||||
export function useLoadChildPages(pageId: string) {
|
export function useLoadChildPages(pageId: string) {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
@ -20,14 +19,6 @@ export function useLoadChildPages(pageId: string) {
|
|||||||
}
|
}
|
||||||
}, [dispatch, pageId, collapsed]);
|
}, [dispatch, pageId, collapsed]);
|
||||||
|
|
||||||
const onPageChanged = useMemo(() => {
|
|
||||||
return debounce((page: Page) => {
|
|
||||||
console.log('DidUpdateView');
|
|
||||||
|
|
||||||
dispatch(pagesActions.onPageChanged(page));
|
|
||||||
}, 200);
|
|
||||||
}, [dispatch]);
|
|
||||||
|
|
||||||
const loadPageChildren = useCallback(
|
const loadPageChildren = useCallback(
|
||||||
async (pageId: string) => {
|
async (pageId: string) => {
|
||||||
const childPages = await getChildPages(pageId);
|
const childPages = await getChildPages(pageId);
|
||||||
@ -49,9 +40,19 @@ export function useLoadChildPages(pageId: string) {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const unsubscribePromise = subscribeNotifications(
|
const unsubscribePromise = subscribeNotifications(
|
||||||
{
|
{
|
||||||
[FolderNotification.DidUpdateView]: (_payload) => {
|
[FolderNotification.DidUpdateView]: async (payload) => {
|
||||||
// const page = parserViewPBToPage(payload);
|
const childViews = payload.child_views;
|
||||||
// onPageChanged(page);
|
|
||||||
|
if (childViews.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatch(
|
||||||
|
pagesActions.addChildPages({
|
||||||
|
id: pageId,
|
||||||
|
childPages: childViews.map(parserViewPBToPage),
|
||||||
|
})
|
||||||
|
);
|
||||||
},
|
},
|
||||||
[FolderNotification.DidUpdateChildViews]: async (payload) => {
|
[FolderNotification.DidUpdateChildViews]: async (payload) => {
|
||||||
if (payload.delete_child_views.length === 0 && payload.create_child_views.length === 0) {
|
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());
|
return () => void unsubscribePromise.then((unsubscribe) => unsubscribe());
|
||||||
}, [pageId, onPageChanged, loadPageChildren]);
|
}, [pageId, loadPageChildren, dispatch]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
toggleCollapsed,
|
toggleCollapsed,
|
||||||
@ -79,13 +80,16 @@ export function useLoadChildPages(pageId: string) {
|
|||||||
export function usePageActions(pageId: string) {
|
export function usePageActions(pageId: string) {
|
||||||
const page = useAppSelector((state) => state.pages.pageMap[pageId]);
|
const page = useAppSelector((state) => state.pages.pageMap[pageId]);
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
const params = useParams();
|
||||||
|
const currentPageId = params.id;
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const onPageClick = useCallback(() => {
|
const onPageClick = useCallback(() => {
|
||||||
|
if (!page) return;
|
||||||
const pageType = pageTypeMap[page.layout];
|
const pageType = pageTypeMap[page.layout];
|
||||||
|
|
||||||
navigate(`/page/${pageType}/${pageId}`);
|
navigate(`/page/${pageType}/${pageId}`);
|
||||||
}, [navigate, page.layout, pageId]);
|
}, [navigate, page, pageId]);
|
||||||
|
|
||||||
const onAddPage = useCallback(
|
const onAddPage = useCallback(
|
||||||
async (layout: ViewLayoutPB) => {
|
async (layout: ViewLayoutPB) => {
|
||||||
@ -95,6 +99,18 @@ export function usePageActions(pageId: string) {
|
|||||||
parent_view_id: pageId,
|
parent_view_id: pageId,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
dispatch(
|
||||||
|
pagesActions.addPage({
|
||||||
|
page: {
|
||||||
|
id: newViewId,
|
||||||
|
parentId: pageId,
|
||||||
|
layout,
|
||||||
|
name: '',
|
||||||
|
},
|
||||||
|
isLast: true,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
dispatch(pagesActions.expandPage(pageId));
|
dispatch(pagesActions.expandPage(pageId));
|
||||||
const pageType = pageTypeMap[layout];
|
const pageType = pageTypeMap[layout];
|
||||||
|
|
||||||
@ -104,12 +120,17 @@ export function usePageActions(pageId: string) {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const onDeletePage = useCallback(async () => {
|
const onDeletePage = useCallback(async () => {
|
||||||
|
if (currentPageId === pageId) {
|
||||||
|
navigate(`/`);
|
||||||
|
}
|
||||||
|
|
||||||
await deletePage(pageId);
|
await deletePage(pageId);
|
||||||
}, [pageId]);
|
dispatch(pagesActions.deletePages([pageId]));
|
||||||
|
}, [dispatch, currentPageId, navigate, pageId]);
|
||||||
|
|
||||||
const onDuplicatePage = useCallback(async () => {
|
const onDuplicatePage = useCallback(async () => {
|
||||||
await duplicatePage(pageId);
|
await duplicatePage(page);
|
||||||
}, [pageId]);
|
}, [page]);
|
||||||
|
|
||||||
const onRenamePage = useCallback(
|
const onRenamePage = useCallback(
|
||||||
async (name: string) => {
|
async (name: string) => {
|
||||||
|
@ -4,28 +4,44 @@ import { TransitionGroup } from 'react-transition-group';
|
|||||||
import NestedPageTitle from '$app/components/layout/nested_page/NestedPageTitle';
|
import NestedPageTitle from '$app/components/layout/nested_page/NestedPageTitle';
|
||||||
import { useLoadChildPages, usePageActions } from '$app/components/layout/nested_page/NestedPage.hooks';
|
import { useLoadChildPages, usePageActions } from '$app/components/layout/nested_page/NestedPage.hooks';
|
||||||
import { useDrag } from 'src/appflowy_app/components/_shared/drag_block';
|
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 { movePageThunk } from '$app_reducers/pages/async_actions';
|
||||||
|
import { ViewLayoutPB } from '@/services/backend';
|
||||||
|
|
||||||
function NestedPage({ pageId }: { pageId: string }) {
|
function NestedPage({ pageId }: { pageId: string }) {
|
||||||
const { toggleCollapsed, collapsed, childPages } = useLoadChildPages(pageId);
|
const { toggleCollapsed, collapsed, childPages } = useLoadChildPages(pageId);
|
||||||
const { onAddPage, onPageClick, onDeletePage, onDuplicatePage, onRenamePage } = usePageActions(pageId);
|
const { onAddPage, onPageClick, onDeletePage, onDuplicatePage, onRenamePage } = usePageActions(pageId);
|
||||||
const dispatch = useAppDispatch();
|
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(() => {
|
const children = useMemo(() => {
|
||||||
|
if (disableChildren) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
return collapsed ? [] : childPages;
|
return collapsed ? [] : childPages;
|
||||||
}, [collapsed, childPages]);
|
}, [collapsed, childPages, disableChildren]);
|
||||||
|
|
||||||
const onDragFinished = useCallback(
|
const onDragFinished = useCallback(
|
||||||
(result: { dragId: string; position: 'before' | 'after' | 'inside' }) => {
|
(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(
|
void dispatch(
|
||||||
movePageThunk({
|
movePageThunk({
|
||||||
sourceId: result.dragId,
|
sourceId: dragId,
|
||||||
targetId: pageId,
|
targetId: pageId,
|
||||||
insertType: result.position,
|
insertType: position,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
[dispatch, pageId]
|
[dispatch, page?.layout, pageId]
|
||||||
);
|
);
|
||||||
|
|
||||||
const { onDrop, dropPosition, onDragOver, onDragLeave, onDragStart, onDragEnd, isDraggingOver, isDragging } = useDrag({
|
const { onDrop, dropPosition, onDragOver, onDragLeave, onDragStart, onDragEnd, isDraggingOver, isDragging } = useDrag({
|
||||||
@ -34,20 +50,20 @@ function NestedPage({ pageId }: { pageId: string }) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const className = useMemo(() => {
|
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) {
|
if (isDragging) {
|
||||||
return `${defaultClassName} opacity-40`;
|
return `${defaultClassName} opacity-40`;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isDraggingOver && dropPosition === 'inside') {
|
if (isDraggingOver && dropPosition === 'inside' && page?.layout === ViewLayoutPB.Document) {
|
||||||
if (dropPosition === 'inside') {
|
if (dropPosition === 'inside') {
|
||||||
return `${defaultClassName} bg-content-blue-100`;
|
return `${defaultClassName} bg-content-blue-100`;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
return defaultClassName;
|
return defaultClassName;
|
||||||
}
|
}
|
||||||
}, [dropPosition, isDragging, isDraggingOver]);
|
}, [dropPosition, isDragging, isDraggingOver, page?.layout]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@ -58,15 +74,17 @@ function NestedPage({ pageId }: { pageId: string }) {
|
|||||||
onDragEnd={onDragEnd}
|
onDragEnd={onDragEnd}
|
||||||
onDrop={onDrop}
|
onDrop={onDrop}
|
||||||
draggable={true}
|
draggable={true}
|
||||||
|
data-drop-enabled={page?.layout === ViewLayoutPB.Document}
|
||||||
|
data-dragging={isDragging}
|
||||||
data-page-id={pageId}
|
data-page-id={pageId}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
height: dropPosition === 'before' || dropPosition === 'after' ? '4px' : '0px',
|
height: dropPosition === 'before' || dropPosition === 'after' ? '2px' : '0px',
|
||||||
top: dropPosition === 'before' ? '-4px' : 'auto',
|
top: dropPosition === 'before' ? '-2px' : 'auto',
|
||||||
bottom: dropPosition === 'after' ? '-4px' : 'auto',
|
bottom: dropPosition === 'after' ? '-2px' : 'auto',
|
||||||
}}
|
}}
|
||||||
className={'pointer-events-none absolute left-0 z-10 w-full bg-content-blue-100'}
|
className={'pointer-events-none absolute left-0 z-10 w-full bg-content-blue-300'}
|
||||||
/>
|
/>
|
||||||
<NestedPageTitle
|
<NestedPageTitle
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
@ -1,11 +1,14 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useMemo, useState } from 'react';
|
||||||
import { ArrowRightSvg } from '$app/components/_shared/svg/ArrowRightSvg';
|
|
||||||
import { useAppSelector } from '$app/stores/store';
|
import { useAppSelector } from '$app/stores/store';
|
||||||
import AddButton from './AddButton';
|
import AddButton from './AddButton';
|
||||||
import MoreButton from './MoreButton';
|
import MoreButton from './MoreButton';
|
||||||
import { ViewLayoutPB } from '@/services/backend';
|
import { ViewLayoutPB } from '@/services/backend';
|
||||||
import { useSelectedPage } from '$app/components/layout/nested_page/NestedPage.hooks';
|
import { useSelectedPage } from '$app/components/layout/nested_page/NestedPage.hooks';
|
||||||
import { useTranslation } from 'react-i18next';
|
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({
|
function NestedPageTitle({
|
||||||
pageId,
|
pageId,
|
||||||
@ -28,51 +31,68 @@ function NestedPageTitle({
|
|||||||
}) {
|
}) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const page = useAppSelector((state) => {
|
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 [isHovering, setIsHovering] = useState(false);
|
||||||
const isSelected = useSelectedPage(pageId);
|
const isSelected = useSelectedPage(pageId);
|
||||||
|
|
||||||
|
const pageIcon = useMemo(() => (page ? getPageIcon(page) : null), [page]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`m-1 cursor-pointer rounded-lg bg-opacity-40 px-2 py-1 ${isHovering ? 'bg-fill-list-hover' : ''} ${
|
className={`my-0.5 cursor-pointer rounded-lg bg-opacity-40 p-0.5 ${isHovering ? 'bg-fill-list-hover' : ''} ${
|
||||||
isSelected ? 'bg-fill-list-hover' : ''
|
isSelected ? 'bg-fill-list-active' : ''
|
||||||
}`}
|
}`}
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
onMouseEnter={() => setIsHovering(true)}
|
onMouseMove={() => setIsHovering(true)}
|
||||||
onMouseLeave={() => setIsHovering(false)}
|
onMouseLeave={() => setIsHovering(false)}
|
||||||
>
|
>
|
||||||
<div className={'flex h-6 w-[100%] items-center justify-between'}>
|
<div className={'flex h-6 w-[100%] items-center justify-between'}>
|
||||||
<div className={'flex flex-1 items-center justify-start overflow-hidden'}>
|
<div className={'flex flex-1 items-center justify-start gap-1 overflow-hidden'}>
|
||||||
<button
|
{disableChildren ? (
|
||||||
onClick={(e) => {
|
<div className={'mx-2 h-1 w-1 rounded-full bg-text-title'} />
|
||||||
e.stopPropagation();
|
) : (
|
||||||
toggleCollapsed();
|
<IconButton
|
||||||
}}
|
size={'small'}
|
||||||
style={{
|
onClick={(e) => {
|
||||||
transform: collapsed ? 'rotate(0deg)' : 'rotate(90deg)',
|
e.stopPropagation();
|
||||||
}}
|
toggleCollapsed();
|
||||||
className={'flex h-[100%] w-8 items-center justify-center p-2'}
|
}}
|
||||||
>
|
style={{
|
||||||
<div className={'h-5 w-5'}>
|
transform: collapsed ? 'rotate(0deg)' : 'rotate(90deg)',
|
||||||
<ArrowRightSvg />
|
}}
|
||||||
</div>
|
>
|
||||||
</button>
|
<MoreIcon className={'h-4 w-4 text-text-title'} />
|
||||||
{page.icon ? <div className={'mr-1 h-5 w-5'}>{page.icon.value}</div> : null}
|
</IconButton>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{pageIcon}
|
||||||
|
|
||||||
<div className={'flex-1 overflow-hidden text-ellipsis whitespace-nowrap'}>
|
<div className={'flex-1 overflow-hidden text-ellipsis whitespace-nowrap'}>
|
||||||
{page.name || t('menuAppHeader.defaultNewPageName')}
|
{page?.name || t('menuAppHeader.defaultNewPageName')}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div onClick={(e) => e.stopPropagation()} className={'min:w-14 flex items-center justify-end'}>
|
<div onClick={(e) => e.stopPropagation()} className={'min:w-14 flex items-center justify-end px-2'}>
|
||||||
<AddButton isVisible={isHovering} onAddPage={onAddPage} />
|
{page?.layout === ViewLayoutPB.Document && (
|
||||||
<MoreButton
|
<AddButton setHovering={setIsHovering} isHovering={isHovering} onAddPage={onAddPage} />
|
||||||
page={page}
|
)}
|
||||||
isVisible={isHovering}
|
{page && (
|
||||||
onDelete={onDelete}
|
<MoreButton
|
||||||
onDuplicate={onDuplicate}
|
setHovering={setIsHovering}
|
||||||
onRename={onRename}
|
isHovering={isHovering}
|
||||||
/>
|
page={page}
|
||||||
|
onDelete={onDelete}
|
||||||
|
onDuplicate={onDuplicate}
|
||||||
|
onRename={onRename}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -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<HTMLButtonElement | null>(null);
|
||||||
|
const renderItem = useCallback((title: string, icon: ReactNode, caption?: string) => {
|
||||||
|
return (
|
||||||
|
<div className={'flex w-full items-center justify-between gap-2 px-1 font-medium'}>
|
||||||
|
{icon}
|
||||||
|
<div className={'flex-1'}>{title}</div>
|
||||||
|
<div className={'text-right text-text-caption'}>{caption || ''}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<>
|
||||||
|
<Tooltip disableInteractive={true} title={tooltip}>
|
||||||
|
<IconButton
|
||||||
|
size={'small'}
|
||||||
|
onClick={(e) => {
|
||||||
|
setAnchorEl(e.currentTarget);
|
||||||
|
}}
|
||||||
|
className={`${!isHovering ? 'invisible' : ''} text-icon-primary`}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
|
<Popover
|
||||||
|
onClose={handleClose}
|
||||||
|
open={open}
|
||||||
|
anchorEl={anchorEl}
|
||||||
|
disableRestoreFocus={true}
|
||||||
|
keepMounted={false}
|
||||||
|
PaperProps={{
|
||||||
|
className: 'py-2',
|
||||||
|
}}
|
||||||
|
anchorOrigin={{
|
||||||
|
vertical: 'bottom',
|
||||||
|
horizontal: 'left',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<KeyboardNavigation
|
||||||
|
onKeyDown={onKeyDown}
|
||||||
|
onEscape={handleClose}
|
||||||
|
options={optionList}
|
||||||
|
onConfirm={handleConfirm}
|
||||||
|
/>
|
||||||
|
</Popover>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default OperationMenu;
|
@ -10,6 +10,7 @@ function Resizer() {
|
|||||||
const startX = useRef(0);
|
const startX = useRef(0);
|
||||||
const onResize = useCallback(
|
const onResize = useCallback(
|
||||||
(e: MouseEvent) => {
|
(e: MouseEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
const diff = e.clientX - startX.current;
|
const diff = e.clientX - startX.current;
|
||||||
const newWidth = width + diff;
|
const newWidth = width + diff;
|
||||||
|
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useAppSelector } from '$app/stores/store';
|
import { useAppSelector } from '$app/stores/store';
|
||||||
import { ThemeMode } from '$app/stores/reducers/current-user/slice';
|
|
||||||
import { AppflowyLogoDark } from '$app/components/_shared/svg/AppflowyLogoDark';
|
import { AppflowyLogoDark } from '$app/components/_shared/svg/AppflowyLogoDark';
|
||||||
import { AppflowyLogoLight } from '$app/components/_shared/svg/AppflowyLogoLight';
|
import { AppflowyLogoLight } from '$app/components/_shared/svg/AppflowyLogoLight';
|
||||||
import CollapseMenuButton from '$app/components/layout/collapse_menu_button/CollapseMenuButton';
|
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() {
|
function SideBar() {
|
||||||
const { isCollapsed, width, isResizing } = useAppSelector((state) => state.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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
width: isCollapsed ? 0 : width,
|
width: isCollapsed ? 0 : width,
|
||||||
transition: isResizing ? 'none' : 'width 150ms cubic-bezier(0.4, 0, 0.2, 1)',
|
transition: isResizing ? 'none' : 'width 250ms cubic-bezier(0.4, 0, 0.2, 1)',
|
||||||
}}
|
}}
|
||||||
className={'relative h-screen overflow-hidden'}
|
className={'relative h-screen overflow-hidden'}
|
||||||
>
|
>
|
||||||
<div className={'flex h-[100vh] flex-col overflow-hidden border-r border-line-divider bg-bg-base'}>
|
<div className={'flex h-[100vh] flex-col overflow-hidden border-r border-line-divider bg-bg-base'}>
|
||||||
<div className={'flex h-[64px] justify-between px-6 py-5'}>
|
<div className={'flex h-[64px] justify-between px-4 py-5'}>
|
||||||
{isDark ? <AppflowyLogoDark /> : <AppflowyLogoLight />}
|
{isDark ? <AppflowyLogoDark /> : <AppflowyLogoLight />}
|
||||||
<CollapseMenuButton />
|
<CollapseMenuButton />
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,37 +1,46 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { useAppSelector } from '$app/stores/store';
|
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 PersonOutline from '@mui/icons-material/PersonOutline';
|
||||||
import ArrowDropDown from '@mui/icons-material/ArrowDropDown';
|
|
||||||
import UserSetting from '../user_setting/UserSetting';
|
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() {
|
function UserInfo() {
|
||||||
const currentUser = useAppSelector((state) => state.currentUser);
|
const currentUser = useAppSelector((state) => state.currentUser);
|
||||||
const [showUserSetting, setShowUserSetting] = useState(false);
|
const [showUserSetting, setShowUserSetting] = useState(false);
|
||||||
|
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div
|
<div className={'flex w-full cursor-pointer select-none items-center justify-between px-4 text-text-title'}>
|
||||||
onClick={(e) => {
|
<div className={'flex w-full flex-1 items-center gap-1'}>
|
||||||
e.stopPropagation();
|
<Avatar
|
||||||
setShowUserSetting(!showUserSetting);
|
sx={{
|
||||||
}}
|
width: 26,
|
||||||
className={'flex cursor-pointer items-center px-6 text-text-title'}
|
height: 26,
|
||||||
>
|
backgroundColor: 'var(--fill-list-active)',
|
||||||
<Avatar
|
}}
|
||||||
sx={{
|
className={'text-xs font-bold text-text-title'}
|
||||||
width: 23,
|
variant={'circular'}
|
||||||
height: 23,
|
>
|
||||||
}}
|
{currentUser.displayName ? currentUser.displayName[0] : <PersonOutline />}
|
||||||
className={'text-text-title'}
|
</Avatar>
|
||||||
variant={'rounded'}
|
<span className={'ml-2 flex-1 text-xs'}>{currentUser.displayName}</span>
|
||||||
>
|
</div>
|
||||||
<PersonOutline />
|
|
||||||
</Avatar>
|
<Tooltip disableInteractive={true} title={t('settings.menu.open')}>
|
||||||
<span className={'ml-2 text-sm'}>{currentUser.displayName}</span>
|
<IconButton
|
||||||
<button className={'ml-2 rounded hover:bg-fill-list-hover'}>
|
size={'small'}
|
||||||
<ArrowDropDown />
|
onClick={() => {
|
||||||
</button>
|
setShowUserSetting(!showUserSetting);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<SettingIcon className={'text-text-title'} />
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<UserSetting open={showUserSetting} onClose={() => setShowUserSetting(false)} />
|
<UserSetting open={showUserSetting} onClose={() => setShowUserSetting(false)} />
|
||||||
|
@ -2,8 +2,6 @@ import React from 'react';
|
|||||||
import CollapseMenuButton from '$app/components/layout/collapse_menu_button/CollapseMenuButton';
|
import CollapseMenuButton from '$app/components/layout/collapse_menu_button/CollapseMenuButton';
|
||||||
import { useAppSelector } from '$app/stores/store';
|
import { useAppSelector } from '$app/stores/store';
|
||||||
import Breadcrumb from '$app/components/layout/bread_crumb/BreadCrumb';
|
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() {
|
function TopBar() {
|
||||||
const sidebarIsCollapsed = useAppSelector((state) => state.sidebar.isCollapsed);
|
const sidebarIsCollapsed = useAppSelector((state) => state.sidebar.isCollapsed);
|
||||||
@ -19,13 +17,6 @@ function TopBar() {
|
|||||||
<div className={'flex-1'}>
|
<div className={'flex-1'}>
|
||||||
<Breadcrumb />
|
<Breadcrumb />
|
||||||
</div>
|
</div>
|
||||||
<div className={'flex items-center justify-end'}>
|
|
||||||
<div className={'mr-2'}>
|
|
||||||
<ShareButton />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<MoreButton />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -1,12 +1,11 @@
|
|||||||
import React, { useCallback, useEffect, useMemo } from 'react';
|
import React, { useCallback, useMemo } from 'react';
|
||||||
import Select from '@mui/material/Select';
|
import Select from '@mui/material/Select';
|
||||||
import { Theme, ThemeMode, UserSetting } from '$app/stores/reducers/current-user/slice';
|
import { Theme, ThemeMode, UserSetting } from '$app/stores/reducers/current-user/slice';
|
||||||
import MenuItem from '@mui/material/MenuItem';
|
import MenuItem from '@mui/material/MenuItem';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
function AppearanceSetting({
|
function AppearanceSetting({
|
||||||
theme = Theme.Default,
|
themeMode = ThemeMode.System,
|
||||||
themeMode = ThemeMode.Light,
|
|
||||||
onChange,
|
onChange,
|
||||||
}: {
|
}: {
|
||||||
theme?: Theme;
|
theme?: Theme;
|
||||||
@ -15,13 +14,6 @@ function AppearanceSetting({
|
|||||||
}) {
|
}) {
|
||||||
const { t } = useTranslation();
|
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(
|
const themeModeOptions = useMemo(
|
||||||
() => [
|
() => [
|
||||||
{
|
{
|
||||||
@ -32,6 +24,10 @@ function AppearanceSetting({
|
|||||||
value: ThemeMode.Dark,
|
value: ThemeMode.Dark,
|
||||||
content: t('settings.appearance.themeMode.dark'),
|
content: t('settings.appearance.themeMode.dark'),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
value: ThemeMode.System,
|
||||||
|
content: t('settings.appearance.themeMode.system'),
|
||||||
|
},
|
||||||
],
|
],
|
||||||
[t]
|
[t]
|
||||||
);
|
);
|
||||||
@ -63,7 +59,7 @@ function AppearanceSetting({
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{options.map((option) => (
|
{options.map((option) => (
|
||||||
<MenuItem key={option.value} value={option.value}>
|
<MenuItem key={option.value} className={'my-1 rounded-none px-2 py-1 text-xs'} value={option.value}>
|
||||||
{option.content}
|
{option.content}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
))}
|
))}
|
||||||
@ -86,6 +82,9 @@ function AppearanceSetting({
|
|||||||
onChange: (newValue) => {
|
onChange: (newValue) => {
|
||||||
onChange({
|
onChange({
|
||||||
themeMode: newValue as ThemeMode,
|
themeMode: newValue as ThemeMode,
|
||||||
|
isDark:
|
||||||
|
newValue === ThemeMode.Dark ||
|
||||||
|
(newValue === ThemeMode.System && window.matchMedia('(prefers-color-scheme: dark)').matches),
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -61,7 +61,7 @@ function LanguageSetting({
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{languages.map((option) => (
|
{languages.map((option) => (
|
||||||
<MenuItem key={option.key} value={option.key}>
|
<MenuItem key={option.key} className={'my-1 w-full rounded-none px-2 py-1 text-xs'} value={option.key}>
|
||||||
{option.title}
|
{option.title}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
))}
|
))}
|
||||||
|
@ -27,7 +27,7 @@ function UserSettingMenu({ selected, onSelect }: { onSelect: (selected: MenuItem
|
|||||||
}, [t]);
|
}, [t]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={'h-[300px] w-[200px] border-r border-solid border-r-line-border pr-4 text-sm'}>
|
<div className={'h-[300px] w-[200px] border-r border-solid border-r-line-border pr-4 text-xs'}>
|
||||||
{options.map((option) => {
|
{options.map((option) => {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@ -35,7 +35,7 @@ function UserSettingMenu({ selected, onSelect }: { onSelect: (selected: MenuItem
|
|||||||
onClick={() => {
|
onClick={() => {
|
||||||
onSelect(option.value);
|
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'
|
selected === option.value ? 'bg-fill-list-hover' : 'hover:text-content-blue-300'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
|
@ -5,10 +5,9 @@ import DialogTitle from '@mui/material/DialogTitle';
|
|||||||
import Slide, { SlideProps } from '@mui/material/Slide';
|
import Slide, { SlideProps } from '@mui/material/Slide';
|
||||||
import UserSettingMenu, { MenuItem } from './Menu';
|
import UserSettingMenu, { MenuItem } from './Menu';
|
||||||
import UserSettingPanel from './SettingPanel';
|
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 { useAppDispatch, useAppSelector } from '$app/stores/store';
|
||||||
import { currentUserActions } from '$app_reducers/current-user/slice';
|
import { currentUserActions } from '$app_reducers/current-user/slice';
|
||||||
import { ThemeModePB } from '@/services/backend';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { UserService } from '$app/application/user/user.service';
|
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';
|
const language = newSetting.language || 'en';
|
||||||
|
|
||||||
void UserService.setAppearanceSetting({
|
void UserService.setAppearanceSetting({
|
||||||
theme: newSetting.theme || Theme.Default,
|
theme: newSetting.theme,
|
||||||
theme_mode: newSetting.themeMode || ThemeModePB.Light,
|
theme_mode: newSetting.themeMode,
|
||||||
locale: {
|
locale: {
|
||||||
language_code: language.split('-')[0],
|
language_code: language.split('-')[0],
|
||||||
country_code: language.split('-')[1],
|
country_code: language.split('-')[1],
|
||||||
@ -48,7 +47,7 @@ function UserSettings({ open, onClose }: { open: boolean; onClose: () => void })
|
|||||||
keepMounted={false}
|
keepMounted={false}
|
||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
>
|
>
|
||||||
<DialogTitle>{t('settings.title')}</DialogTitle>
|
<DialogTitle className={'text-sm'}>{t('settings.title')}</DialogTitle>
|
||||||
<DialogContent className={'flex w-[540px]'}>
|
<DialogContent className={'flex w-[540px]'}>
|
||||||
<UserSettingMenu
|
<UserSettingMenu
|
||||||
onSelect={(selected) => {
|
onSelect={(selected) => {
|
||||||
|
@ -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: <SettingsIcon />,
|
|
||||||
label: t('settings.title'),
|
|
||||||
onClick: () => {
|
|
||||||
//
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'delete',
|
|
||||||
icon: <DeleteOutline />,
|
|
||||||
label: t('button.delete'),
|
|
||||||
onClick: () => onDelete(workspace.id),
|
|
||||||
},
|
|
||||||
];
|
|
||||||
}, [onDelete, t, workspace.id]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<ButtonPopoverList
|
|
||||||
isVisible={isHovered}
|
|
||||||
popoverOrigin={{
|
|
||||||
anchorOrigin: {
|
|
||||||
vertical: 'bottom',
|
|
||||||
horizontal: 'left',
|
|
||||||
},
|
|
||||||
transformOrigin: {
|
|
||||||
vertical: 'top',
|
|
||||||
horizontal: 'left',
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
popoverOptions={options}
|
|
||||||
>
|
|
||||||
<IconButton>
|
|
||||||
<MoreIcon />
|
|
||||||
</IconButton>
|
|
||||||
</ButtonPopoverList>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default MoreButton;
|
|
@ -8,7 +8,7 @@ function WorkspaceNestedPages({ workspaceId }: { workspaceId: string }) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={'h-full'}>
|
<div className={'h-full w-full overflow-x-hidden p-4 text-xs'}>
|
||||||
{pageIds?.map((pageId) => (
|
{pageIds?.map((pageId) => (
|
||||||
<NestedPage key={pageId} pageId={pageId} />
|
<NestedPage key={pageId} pageId={pageId} />
|
||||||
))}
|
))}
|
||||||
|
@ -1,35 +1,29 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import AddSvg from '$app/components/_shared/svg/AddSvg';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { ViewLayoutPB } from '@/services/backend';
|
import { useWorkspaceActions } from '$app/components/layout/workspace_manager/Workspace.hooks';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import Button from '@mui/material/Button';
|
||||||
import { createCurrentWorkspaceChildView } from '$app/application/folder/workspace.service';
|
import { ReactComponent as AddSvg } from '$app/assets/add.svg';
|
||||||
|
|
||||||
function NewPageButton({ workspaceId }: { workspaceId: string }) {
|
function NewPageButton({ workspaceId }: { workspaceId: string }) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const navigate = useNavigate();
|
const { newPage } = useWorkspaceActions(workspaceId);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={'flex h-[60px] w-full items-center border-t border-line-divider px-6 py-5'}>
|
<div className={'flex h-[60px] w-full items-center border-t border-line-divider px-5 py-5'}>
|
||||||
<button
|
<Button
|
||||||
onClick={async () => {
|
color={'inherit'}
|
||||||
const { id } = await createCurrentWorkspaceChildView({
|
onClick={newPage}
|
||||||
name: '',
|
startIcon={
|
||||||
layout: ViewLayoutPB.Document,
|
<div className={'rounded-full bg-fill-default'}>
|
||||||
parent_view_id: workspaceId,
|
<div className={'flex h-[18px] w-[18px] items-center justify-center px-0 text-lg text-content-on-fill'}>
|
||||||
});
|
<AddSvg />
|
||||||
|
</div>
|
||||||
navigate(`/page/document/${id}`);
|
|
||||||
}}
|
|
||||||
className={'flex items-center hover:text-fill-default'}
|
|
||||||
>
|
|
||||||
<div className={'mr-2 rounded-full bg-fill-default'}>
|
|
||||||
<div className={'h-[24px] w-[24px] text-content-on-fill'}>
|
|
||||||
<AddSvg />
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
}
|
||||||
|
className={'flex w-full items-center justify-start text-xs hover:bg-transparent hover:text-fill-default'}
|
||||||
|
>
|
||||||
{t('newPageText')}
|
{t('newPageText')}
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -30,14 +30,14 @@ function TrashButton() {
|
|||||||
onDragLeave={onDragLeave}
|
onDragLeave={onDragLeave}
|
||||||
data-page-id={'trash'}
|
data-page-id={'trash'}
|
||||||
onClick={navigateToTrash}
|
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' : ''
|
selected ? 'bg-fill-list-active' : ''
|
||||||
} ${isDraggingOver ? 'bg-fill-list-hover' : ''}`}
|
} ${isDraggingOver ? 'bg-fill-list-hover' : ''}`}
|
||||||
>
|
>
|
||||||
<div className='h-6 w-6'>
|
<div className='h-5 w-5'>
|
||||||
<TrashSvg />
|
<TrashSvg />
|
||||||
</div>
|
</div>
|
||||||
<span className={'ml-2'}>{t('trash.text')}</span>
|
<span>{t('trash.text')}</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -3,8 +3,10 @@ import { useAppDispatch, useAppSelector } from '$app/stores/store';
|
|||||||
import { workspaceActions, WorkspaceItem } from '$app_reducers/workspace/slice';
|
import { workspaceActions, WorkspaceItem } from '$app_reducers/workspace/slice';
|
||||||
import { Page, pagesActions, parserViewPBToPage } from '$app_reducers/pages/slice';
|
import { Page, pagesActions, parserViewPBToPage } from '$app_reducers/pages/slice';
|
||||||
import { subscribeNotifications } from '$app/application/notification';
|
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 * 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() {
|
export function useLoadWorkspaces() {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
@ -95,3 +97,21 @@ export function useLoadWorkspace(workspace: WorkspaceItem) {
|
|||||||
deleteWorkspace,
|
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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
@ -1,20 +1,65 @@
|
|||||||
import React from 'react';
|
import React, { useState } from 'react';
|
||||||
import { WorkspaceItem } from '$app_reducers/workspace/slice';
|
import { WorkspaceItem } from '$app_reducers/workspace/slice';
|
||||||
import NestedViews from '$app/components/layout/workspace_manager/NestedPages';
|
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 }) {
|
function Workspace({ workspace, opened }: { workspace: WorkspaceItem; opened: boolean }) {
|
||||||
useLoadWorkspace(workspace);
|
useLoadWorkspace(workspace);
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const { newPage } = useWorkspaceActions(workspace.id);
|
||||||
|
const [showPages, setShowPages] = useState(true);
|
||||||
|
const [showAdd, setShowAdd] = useState(false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div
|
<div
|
||||||
|
className={'w-full'}
|
||||||
style={{
|
style={{
|
||||||
height: opened ? '100%' : 0,
|
height: opened ? '100%' : 0,
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
transition: 'height 0.2s ease-in-out',
|
transition: 'height 0.2s ease-in-out',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<NestedViews workspaceId={workspace.id} />
|
<div
|
||||||
|
onClick={(e) => {
|
||||||
|
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'}
|
||||||
|
>
|
||||||
|
<Tooltip disableInteractive={true} placement={'top-start'} title={t('sideBar.clickToHidePersonal')}>
|
||||||
|
<Typography className={'rounded px-2 py-1 text-xs font-medium hover:bg-fill-list-active'}>
|
||||||
|
{t('sideBar.personal')}
|
||||||
|
</Typography>
|
||||||
|
</Tooltip>
|
||||||
|
{showAdd && (
|
||||||
|
<Tooltip disableInteractive={true} title={t('sideBar.addAPage')}>
|
||||||
|
<IconButton
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
void newPage();
|
||||||
|
}}
|
||||||
|
size={'small'}
|
||||||
|
>
|
||||||
|
<AddIcon />
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{showPages && <NestedViews workspaceId={workspace.id} />}
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
@ -8,18 +8,17 @@ function WorkspaceManager() {
|
|||||||
const { workspaces, currentWorkspace } = useLoadWorkspaces();
|
const { workspaces, currentWorkspace } = useLoadWorkspaces();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={'flex h-full flex-col justify-between'}>
|
<div className={'workspaces flex h-full select-none flex-col justify-between'}>
|
||||||
<div className={'flex w-full flex-1 flex-col overflow-y-auto overflow-x-hidden'}>
|
<div className={'mt-4 flex w-full flex-1 select-none flex-col overflow-y-auto overflow-x-hidden'}>
|
||||||
<div className={'flex-1'}>
|
<div className={'flex-1'}>
|
||||||
{workspaces.map((workspace) => (
|
{workspaces.map((workspace) => (
|
||||||
<Workspace opened={currentWorkspace?.id === workspace.id} key={workspace.id} workspace={workspace} />
|
<Workspace opened={currentWorkspace?.id === workspace.id} key={workspace.id} workspace={workspace} />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<div className={'flex w-[100%] items-center px-2'}>
|
|
||||||
<TrashButton />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div className={'flex w-[100%] items-center px-2'}>
|
||||||
|
<TrashButton />
|
||||||
|
</div>
|
||||||
{currentWorkspace && <NewPageButton workspaceId={currentWorkspace.id} />}
|
{currentWorkspace && <NewPageButton workspaceId={currentWorkspace.id} />}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -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 (
|
|
||||||
<MenuItem
|
|
||||||
onClick={() => openWorkspace()}
|
|
||||||
onMouseEnter={() => {
|
|
||||||
setIsHovered(true);
|
|
||||||
}}
|
|
||||||
onMouseLeave={() => {
|
|
||||||
setIsHovered(false);
|
|
||||||
}}
|
|
||||||
className={'hover:bg-fill-list-active'}
|
|
||||||
>
|
|
||||||
<div className={'flex w-[100%] items-center justify-between'}>
|
|
||||||
<div className={'flex-1 font-bold text-text-caption'}>{workspace.name}</div>
|
|
||||||
<div className='flex h-[23px] w-auto items-center justify-end'>
|
|
||||||
<MoreButton workspace={workspace} isHovered={isHovered} onDelete={onDelete} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</MenuItem>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default WorkspaceTitle;
|
|
@ -3,9 +3,9 @@ import { useTranslation } from 'react-i18next';
|
|||||||
import Button from '@mui/material/Button';
|
import Button from '@mui/material/Button';
|
||||||
import { DeleteOutline, RestoreOutlined } from '@mui/icons-material';
|
import { DeleteOutline, RestoreOutlined } from '@mui/icons-material';
|
||||||
import { useLoadTrash, useTrashActions } from '$app/components/trash/Trash.hooks';
|
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 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() {
|
function Trash() {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@ -26,7 +26,7 @@ function Trash() {
|
|||||||
return (
|
return (
|
||||||
<div className={'flex flex-col'}>
|
<div className={'flex flex-col'}>
|
||||||
<div className={'flex items-center justify-between'}>
|
<div className={'flex items-center justify-between'}>
|
||||||
<div className={'text-2xl font-bold'}>{t('trash.text')}</div>
|
<div className={'px-2 text-lg font-bold'}>{t('trash.text')}</div>
|
||||||
<div className={'flex items-center justify-end'}>
|
<div className={'flex items-center justify-end'}>
|
||||||
<Button color={'inherit'} onClick={() => onClickRestoreAll()}>
|
<Button color={'inherit'} onClick={() => onClickRestoreAll()}>
|
||||||
<RestoreOutlined />
|
<RestoreOutlined />
|
||||||
@ -38,13 +38,12 @@ function Trash() {
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className={'flex justify-around p-6 px-2 text-text-caption'}>
|
<div className={'flex justify-around gap-2 p-6 px-2 text-xs text-text-caption'}>
|
||||||
<div className={'w-[40%]'}>{t('trash.pageHeader.fileName')}</div>
|
<div className={'w-[40%]'}>{t('trash.pageHeader.fileName')}</div>
|
||||||
<div className={'flex-1'}>{t('trash.pageHeader.lastModified')}</div>
|
<div className={'flex-1'}>{t('trash.pageHeader.lastModified')}</div>
|
||||||
<div className={'flex-1'}>{t('trash.pageHeader.created')}</div>
|
<div className={'flex-1'}>{t('trash.pageHeader.created')}</div>
|
||||||
<div className={'w-[64px]'}></div>
|
<div className={'w-[64px]'}></div>
|
||||||
</div>
|
</div>
|
||||||
<Divider />
|
|
||||||
<List>
|
<List>
|
||||||
{trash.map((item) => (
|
{trash.map((item) => (
|
||||||
<TrashItem
|
<TrashItem
|
||||||
|
@ -34,23 +34,23 @@ function TrashItem({
|
|||||||
paddingInline: 0,
|
paddingInline: 0,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className={'flex w-[100%] items-center justify-around rounded-lg px-2 py-3 hover:bg-fill-list-hover'}>
|
<div className={'flex w-[100%] items-center justify-around gap-2 rounded-lg p-2 text-xs hover:bg-fill-list-hover'}>
|
||||||
<div className={'w-[40%] text-left'}>{item.name}</div>
|
<div className={'w-[40%] whitespace-break-spaces text-left'}>{item.name || t('document.title.placeholder')}</div>
|
||||||
<div className={'flex-1'}>{dayjs.unix(item.modifiedTime).format('MM/DD/YYYY hh:mm A')}</div>
|
<div className={'flex-1'}>{dayjs.unix(item.modifiedTime).format('MM/DD/YYYY hh:mm A')}</div>
|
||||||
<div className={'flex-1'}>{dayjs.unix(item.createTime).format('MM/DD/YYYY hh:mm A')}</div>
|
<div className={'flex-1'}>{dayjs.unix(item.createTime).format('MM/DD/YYYY hh:mm A')}</div>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
visibility: hoverId === item.id ? 'visible' : 'hidden',
|
visibility: hoverId === item.id ? 'visible' : 'hidden',
|
||||||
}}
|
}}
|
||||||
className={'w-[64px]'}
|
className={'whitespace-nowrap'}
|
||||||
>
|
>
|
||||||
<Tooltip placement={'top-start'} title={t('button.putback')}>
|
<Tooltip placement={'top-start'} title={t('button.putback')}>
|
||||||
<IconButton onClick={(_) => onPutback(item.id)} className={'mr-2'}>
|
<IconButton size={'small'} onClick={(_) => onPutback(item.id)} className={'mr-2'}>
|
||||||
<RestoreOutlined />
|
<RestoreOutlined />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Tooltip placement={'top-start'} title={t('button.delete')}>
|
<Tooltip placement={'top-start'} title={t('button.delete')}>
|
||||||
<IconButton color={'error'} onClick={(_) => onDelete([item.id])}>
|
<IconButton size={'small'} color={'error'} onClick={(_) => onDelete([item.id])}>
|
||||||
<DeleteOutline />
|
<DeleteOutline />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
@ -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 <DocumentIcon className={'h-4 w-4'} />;
|
||||||
|
case ViewLayoutPB.Grid:
|
||||||
|
return <GridIcon className={'h-4 w-4'} />;
|
||||||
|
case ViewLayoutPB.Board:
|
||||||
|
return <BoardIcon className={'h-4 w-4'} />;
|
||||||
|
case ViewLayoutPB.Calendar:
|
||||||
|
return <CalendarIcon className={'h-4 w-4'} />;
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
@ -8,6 +8,7 @@ export interface UserSetting {
|
|||||||
theme?: Theme;
|
theme?: Theme;
|
||||||
themeMode?: ThemeMode;
|
themeMode?: ThemeMode;
|
||||||
language?: string;
|
language?: string;
|
||||||
|
isDark?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum Theme {
|
export enum Theme {
|
||||||
|
@ -14,7 +14,7 @@ export const movePageThunk = createAsyncThunk(
|
|||||||
thunkAPI
|
thunkAPI
|
||||||
) => {
|
) => {
|
||||||
const { sourceId, targetId, insertType } = payload;
|
const { sourceId, targetId, insertType } = payload;
|
||||||
const { getState } = thunkAPI;
|
const { getState, dispatch } = thunkAPI;
|
||||||
const { pageMap, relationMap } = (getState() as RootState).pages;
|
const { pageMap, relationMap } = (getState() as RootState).pages;
|
||||||
const sourcePage = pageMap[sourceId];
|
const sourcePage = pageMap[sourceId];
|
||||||
const targetPage = pageMap[targetId];
|
const targetPage = pageMap[targetId];
|
||||||
@ -51,6 +51,8 @@ export const movePageThunk = createAsyncThunk(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
dispatch(pagesActions.movePage({ id: sourceId, newParentId: parentId, prevId }));
|
||||||
|
|
||||||
await movePage({
|
await movePage({
|
||||||
view_id: sourceId,
|
view_id: sourceId,
|
||||||
new_parent_id: parentId,
|
new_parent_id: parentId,
|
||||||
|
@ -89,10 +89,85 @@ export const pagesSlice = createSlice({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
removeChildPages(state, action: PayloadAction<string>) {
|
addPage(
|
||||||
const parentId = action.payload;
|
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<string[]>) {
|
||||||
|
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<string>) {
|
expandPage(state, action: PayloadAction<string>) {
|
||||||
|
@ -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;
|
||||||
|
};
|
@ -1,11 +1,8 @@
|
|||||||
import { ThemeMode } from '$app/stores/reducers/current-user/slice';
|
|
||||||
import { ThemeOptions } from '@mui/material';
|
import { ThemeOptions } from '@mui/material';
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
export const getDesignTokens = (mode: ThemeMode): ThemeOptions => {
|
export const getDesignTokens = (isDark: boolean): ThemeOptions => {
|
||||||
const isDark = mode === ThemeMode.Dark;
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
typography: {
|
typography: {
|
||||||
fontFamily: ['Poppins'].join(','),
|
fontFamily: ['Poppins'].join(','),
|
||||||
|
@ -24,10 +24,6 @@ body {
|
|||||||
width: 8px;
|
width: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.MuiPopover-root::-webkit-scrollbar {
|
|
||||||
width: 0;
|
|
||||||
height: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
:root[data-dark-mode=true] body {
|
:root[data-dark-mode=true] body {
|
||||||
|
@ -879,7 +879,9 @@
|
|||||||
"page": {
|
"page": {
|
||||||
"label": "Link to page",
|
"label": "Link to page",
|
||||||
"tooltip": "Click to open page"
|
"tooltip": "Click to open page"
|
||||||
}
|
},
|
||||||
|
"deleted": "Deleted",
|
||||||
|
"deletedContent": "This content does not exist or has been deleted"
|
||||||
},
|
},
|
||||||
"toolbar": {
|
"toolbar": {
|
||||||
"resetToDefaultFont": "Reset to default"
|
"resetToDefaultFont": "Reset to default"
|
||||||
|
Loading…
Reference in New Issue
Block a user