mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-13 21:05:19 +00:00
feat: replace menu with new design (#4012)
Co-authored-by: Alex Yang <himself65@outlook.com>
This commit is contained in:
@@ -1,138 +1,69 @@
|
||||
import {
|
||||
Menu,
|
||||
MenuItem,
|
||||
type MenuProps,
|
||||
MenuTrigger,
|
||||
styled,
|
||||
} from '@affine/component';
|
||||
import { LOCALES, useI18N } from '@affine/i18n';
|
||||
import { assertExists } from '@blocksuite/global/utils';
|
||||
import type { ButtonProps } from '@toeverything/components/button';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
|
||||
export const StyledListItem = styled(MenuItem)(() => ({
|
||||
height: '38px',
|
||||
textTransform: 'capitalize',
|
||||
}));
|
||||
|
||||
interface LanguageMenuContentProps {
|
||||
currentLanguage: string;
|
||||
currentLanguageIndex: number;
|
||||
}
|
||||
import { LOCALES } from '@affine/i18n';
|
||||
import { useI18N } from '@affine/i18n';
|
||||
import { Menu, MenuItem, MenuTrigger } from '@toeverything/components/menu';
|
||||
import type { ReactElement } from 'react';
|
||||
import { useCallback, useMemo, useRef } from 'react';
|
||||
|
||||
// Fixme: keyboard focus should be supported by Menu component
|
||||
const LanguageMenuContent = ({
|
||||
currentLanguage,
|
||||
currentLanguageIndex,
|
||||
}: LanguageMenuContentProps) => {
|
||||
const i18n = useI18N();
|
||||
const changeLanguage = useCallback(
|
||||
(targetLanguage: string) => {
|
||||
console.assert(
|
||||
LOCALES.some(item => item.tag === targetLanguage),
|
||||
'targetLanguage should be one of the LOCALES'
|
||||
);
|
||||
i18n.changeLanguage(targetLanguage).catch(err => {
|
||||
console.error('Failed to change language', err);
|
||||
});
|
||||
},
|
||||
[i18n]
|
||||
);
|
||||
|
||||
const [focusedOptionIndex, setFocusedOptionIndex] = useState(
|
||||
currentLanguageIndex ?? 0
|
||||
);
|
||||
|
||||
const handleKeyDown = useCallback(
|
||||
(event: KeyboardEvent) => {
|
||||
switch (event.key) {
|
||||
case 'ArrowUp':
|
||||
event.preventDefault();
|
||||
setFocusedOptionIndex(prevIndex =>
|
||||
prevIndex > 0 ? prevIndex - 1 : 0
|
||||
);
|
||||
break;
|
||||
case 'ArrowDown':
|
||||
event.preventDefault();
|
||||
setFocusedOptionIndex(prevIndex =>
|
||||
prevIndex < LOCALES.length - 1 ? prevIndex + 1 : LOCALES.length
|
||||
);
|
||||
break;
|
||||
case 'Enter':
|
||||
if (focusedOptionIndex !== -1) {
|
||||
const selectedOption = LOCALES[focusedOptionIndex];
|
||||
changeLanguage(selectedOption.tag);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
},
|
||||
[changeLanguage, focusedOptionIndex]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
document.addEventListener('keydown', handleKeyDown);
|
||||
return () => {
|
||||
document.removeEventListener('keydown', handleKeyDown);
|
||||
};
|
||||
}, [handleKeyDown]);
|
||||
|
||||
onSelect,
|
||||
}: {
|
||||
currentLanguage?: string;
|
||||
onSelect: (value: string) => void;
|
||||
}) => {
|
||||
return (
|
||||
<>
|
||||
{LOCALES.map((option, optionIndex) => {
|
||||
{LOCALES.map(option => {
|
||||
return (
|
||||
<StyledListItem
|
||||
<MenuItem
|
||||
key={option.name}
|
||||
active={option.tag === currentLanguage}
|
||||
userFocused={optionIndex == focusedOptionIndex}
|
||||
selected={currentLanguage === option.originalName}
|
||||
title={option.name}
|
||||
onClick={() => {
|
||||
changeLanguage(option.tag);
|
||||
}}
|
||||
onSelect={() => onSelect(option.tag)}
|
||||
>
|
||||
{option.originalName}
|
||||
</StyledListItem>
|
||||
</MenuItem>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
interface LanguageMenuProps extends Omit<MenuProps, 'children'> {
|
||||
triggerProps?: ButtonProps;
|
||||
}
|
||||
|
||||
export const LanguageMenu = ({
|
||||
triggerProps,
|
||||
...menuProps
|
||||
}: LanguageMenuProps) => {
|
||||
export const LanguageMenu = () => {
|
||||
const i18n = useI18N();
|
||||
|
||||
const currentLanguageIndex = LOCALES.findIndex(
|
||||
item => item.tag === i18n.language
|
||||
const ref = useRef(null);
|
||||
const currentLanguage = useMemo(
|
||||
() => LOCALES.find(item => item.tag === i18n.language),
|
||||
[i18n.language]
|
||||
);
|
||||
const onSelect = useCallback(
|
||||
(event: string) => {
|
||||
return i18n.changeLanguage(event);
|
||||
},
|
||||
[i18n]
|
||||
);
|
||||
const currentLanguage = LOCALES[currentLanguageIndex];
|
||||
assertExists(currentLanguage, 'currentLanguage should exist');
|
||||
|
||||
return (
|
||||
<Menu
|
||||
content={
|
||||
<LanguageMenuContent
|
||||
currentLanguage={currentLanguage.tag}
|
||||
currentLanguageIndex={currentLanguageIndex}
|
||||
/>
|
||||
items={
|
||||
(
|
||||
<LanguageMenuContent
|
||||
currentLanguage={currentLanguage?.originalName}
|
||||
onSelect={onSelect}
|
||||
/>
|
||||
) as ReactElement
|
||||
}
|
||||
placement="bottom-end"
|
||||
trigger="click"
|
||||
disablePortal={true}
|
||||
{...menuProps}
|
||||
portalOptions={{
|
||||
container: ref.current,
|
||||
}}
|
||||
>
|
||||
<MenuTrigger
|
||||
ref={ref}
|
||||
data-testid="language-menu-button"
|
||||
style={{ textTransform: 'capitalize' }}
|
||||
{...triggerProps}
|
||||
style={{ textTransform: 'capitalize', fontWeight: 600 }}
|
||||
block={true}
|
||||
>
|
||||
{currentLanguage.originalName}
|
||||
{currentLanguage?.originalName || ''}
|
||||
</MenuTrigger>
|
||||
</Menu>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
import { style } from '@vanilla-extract/css';
|
||||
|
||||
export const hoveredLanguageItem = style({
|
||||
background: 'var(--affine-hover-color)',
|
||||
});
|
||||
@@ -1,4 +1,3 @@
|
||||
import { Menu, MenuItem } from '@affine/component';
|
||||
import {
|
||||
InviteModal,
|
||||
type InviteModalProps,
|
||||
@@ -12,6 +11,7 @@ import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { MoreVerticalIcon } from '@blocksuite/icons';
|
||||
import { Avatar } from '@toeverything/components/avatar';
|
||||
import { Button, IconButton } from '@toeverything/components/button';
|
||||
import { Menu, MenuItem } from '@toeverything/components/menu';
|
||||
import { Tooltip } from '@toeverything/components/tooltip';
|
||||
import clsx from 'clsx';
|
||||
import { useSetAtom } from 'jotai/react';
|
||||
@@ -190,14 +190,11 @@ const MemberItem = ({
|
||||
: 'Pending'}
|
||||
</div>
|
||||
<Menu
|
||||
content={
|
||||
items={
|
||||
<MenuItem data-member-id={member.id} onClick={handleRevoke}>
|
||||
{operationButtonInfo.leaveOrRevokeText}
|
||||
</MenuItem>
|
||||
}
|
||||
placement="bottom"
|
||||
disablePortal={true}
|
||||
trigger="click"
|
||||
>
|
||||
<IconButton
|
||||
disabled={!operationButtonInfo.show}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Menu, MenuItem, MenuTrigger } from '@affine/component';
|
||||
import { Menu, MenuItem, MenuTrigger } from '@toeverything/components/menu';
|
||||
import dayjs from 'dayjs';
|
||||
import { useCallback } from 'react';
|
||||
import { useCallback, useRef } from 'react';
|
||||
|
||||
import {
|
||||
dateFormatOptions,
|
||||
@@ -23,10 +23,8 @@ const DateFormatMenuContent = ({
|
||||
return (
|
||||
<MenuItem
|
||||
key={option}
|
||||
active={currentOption === option}
|
||||
onClick={() => {
|
||||
onSelect(option);
|
||||
}}
|
||||
selected={currentOption === option}
|
||||
onSelect={() => onSelect(option)}
|
||||
>
|
||||
{dayjs(new Date()).format(option)}
|
||||
</MenuItem>
|
||||
@@ -37,6 +35,7 @@ const DateFormatMenuContent = ({
|
||||
};
|
||||
|
||||
export const DateFormatSetting = () => {
|
||||
const ref = useRef(null);
|
||||
const [appearanceSettings, setAppSettings] = useAppSetting();
|
||||
const handleSelect = useCallback(
|
||||
(option: DateFormats) => {
|
||||
@@ -47,17 +46,15 @@ export const DateFormatSetting = () => {
|
||||
|
||||
return (
|
||||
<Menu
|
||||
content={
|
||||
items={
|
||||
<DateFormatMenuContent
|
||||
onSelect={handleSelect}
|
||||
currentOption={appearanceSettings.dateFormat}
|
||||
/>
|
||||
}
|
||||
placement="bottom-end"
|
||||
trigger="click"
|
||||
disablePortal={true}
|
||||
portalOptions={{ container: ref.current }}
|
||||
>
|
||||
<MenuTrigger data-testid="date-format-menu-trigger">
|
||||
<MenuTrigger ref={ref} data-testid="date-format-menu-trigger" block>
|
||||
{dayjs(new Date()).format(appearanceSettings.dateFormat)}
|
||||
</MenuTrigger>
|
||||
</Menu>
|
||||
|
||||
@@ -113,17 +113,7 @@ export const AppearanceSettings = () => {
|
||||
desc={t['com.affine.settings.appearance.language-description']()}
|
||||
>
|
||||
<div className={settingWrapper}>
|
||||
<LanguageMenu
|
||||
triggerContainerStyle={{ width: '100%' }}
|
||||
triggerProps={{
|
||||
style: {
|
||||
width: '100%',
|
||||
justifyContent: 'space-between',
|
||||
fontWeight: 600,
|
||||
padding: '0 10px',
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<LanguageMenu />
|
||||
</div>
|
||||
</SettingRow>
|
||||
{environment.isDesktop ? (
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { FlexWrapper, Menu, MenuItem } from '@affine/component';
|
||||
import { FlexWrapper } from '@affine/component';
|
||||
import { Export, MoveToTrash } from '@affine/component/page-list';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { assertExists } from '@blocksuite/global/utils';
|
||||
@@ -12,14 +12,19 @@ import {
|
||||
PageIcon,
|
||||
} from '@blocksuite/icons';
|
||||
import type { PageMeta } from '@blocksuite/store';
|
||||
import { Divider } from '@toeverything/components/divider';
|
||||
import {
|
||||
Menu,
|
||||
MenuIcon,
|
||||
MenuItem,
|
||||
MenuSeparator,
|
||||
} from '@toeverything/components/menu';
|
||||
import {
|
||||
useBlockSuitePageMeta,
|
||||
usePageMetaHelper,
|
||||
} from '@toeverything/hooks/use-block-suite-page-meta';
|
||||
import { useBlockSuiteWorkspaceHelper } from '@toeverything/hooks/use-block-suite-workspace-helper';
|
||||
import { useAtom, useSetAtom } from 'jotai';
|
||||
import { useCallback, useState } from 'react';
|
||||
import { useCallback, useRef, useState } from 'react';
|
||||
import { applyUpdate, encodeStateAsUpdate } from 'yjs';
|
||||
|
||||
import { pageSettingFamily, setPageModeAtom } from '../../../atoms';
|
||||
@@ -34,9 +39,10 @@ type PageMenuProps = {
|
||||
rename?: () => void;
|
||||
pageId: string;
|
||||
};
|
||||
|
||||
// fixme: refactor this file
|
||||
export const PageMenu = ({ rename, pageId }: PageMenuProps) => {
|
||||
const t = useAFFiNEI18N();
|
||||
const ref = useRef(null);
|
||||
// fixme(himself65): remove these hooks ASAP
|
||||
const [workspace] = useCurrentWorkspace();
|
||||
|
||||
@@ -105,17 +111,25 @@ export const PageMenu = ({ rename, pageId }: PageMenuProps) => {
|
||||
const EditMenu = (
|
||||
<>
|
||||
<MenuItem
|
||||
icon={<EditIcon />}
|
||||
preFix={
|
||||
<MenuIcon>
|
||||
<EditIcon />
|
||||
</MenuIcon>
|
||||
}
|
||||
data-testid="editor-option-menu-rename"
|
||||
onClick={rename}
|
||||
onSelect={rename}
|
||||
style={menuItemStyle}
|
||||
>
|
||||
{t['Rename']()}
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
icon={mode === 'page' ? <EdgelessIcon /> : <PageIcon />}
|
||||
preFix={
|
||||
<MenuIcon>
|
||||
{mode === 'page' ? <EdgelessIcon /> : <PageIcon />}
|
||||
</MenuIcon>
|
||||
}
|
||||
data-testid="editor-option-menu-edgeless"
|
||||
onClick={handleSwitchMode}
|
||||
onSelect={handleSwitchMode}
|
||||
style={menuItemStyle}
|
||||
>
|
||||
{t['Convert to ']()}
|
||||
@@ -123,14 +137,16 @@ export const PageMenu = ({ rename, pageId }: PageMenuProps) => {
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
data-testid="editor-option-menu-favorite"
|
||||
onClick={handleFavorite}
|
||||
onSelect={handleFavorite}
|
||||
style={menuItemStyle}
|
||||
icon={
|
||||
favorite ? (
|
||||
<FavoritedIcon style={{ color: 'var(--affine-primary-color)' }} />
|
||||
) : (
|
||||
<FavoriteIcon />
|
||||
)
|
||||
preFix={
|
||||
<MenuIcon>
|
||||
{favorite ? (
|
||||
<FavoritedIcon style={{ color: 'var(--affine-primary-color)' }} />
|
||||
) : (
|
||||
<FavoriteIcon />
|
||||
)}
|
||||
</MenuIcon>
|
||||
}
|
||||
>
|
||||
{favorite ? t['Remove from favorites']() : t['Add to Favorites']()}
|
||||
@@ -144,28 +160,36 @@ export const PageMenu = ({ rename, pageId }: PageMenuProps) => {
|
||||
>
|
||||
{t['com.affine.header.option.add-tag']()}
|
||||
</MenuItem> */}
|
||||
<Divider size="thinner" dividerColor="var(--affine-border-color)" />
|
||||
<MenuSeparator />
|
||||
<MenuItem
|
||||
icon={<DuplicateIcon />}
|
||||
preFix={
|
||||
<MenuIcon>
|
||||
<DuplicateIcon />
|
||||
</MenuIcon>
|
||||
}
|
||||
data-testid="editor-option-menu-duplicate"
|
||||
onClick={duplicate}
|
||||
onSelect={duplicate}
|
||||
style={menuItemStyle}
|
||||
>
|
||||
{t['com.affine.header.option.duplicate']()}
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
icon={<ImportIcon />}
|
||||
preFix={
|
||||
<MenuIcon>
|
||||
<ImportIcon />
|
||||
</MenuIcon>
|
||||
}
|
||||
data-testid="editor-option-menu-import"
|
||||
onClick={importFile}
|
||||
onSelect={importFile}
|
||||
style={menuItemStyle}
|
||||
>
|
||||
{t['Import']()}
|
||||
</MenuItem>
|
||||
<Export />
|
||||
<Divider size="thinner" dividerColor="var(--affine-border-color)" />
|
||||
<MenuSeparator />
|
||||
<MoveToTrash
|
||||
data-testid="editor-option-menu-delete"
|
||||
onItemClick={() => {
|
||||
onSelect={() => {
|
||||
setOpenConfirm(true);
|
||||
}}
|
||||
/>
|
||||
@@ -176,21 +200,19 @@ export const PageMenu = ({ rename, pageId }: PageMenuProps) => {
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<FlexWrapper alignItems="center" justifyContent="center">
|
||||
<FlexWrapper alignItems="center" justifyContent="center" ref={ref}>
|
||||
<Menu
|
||||
content={EditMenu}
|
||||
placement="bottom-end"
|
||||
disablePortal={true}
|
||||
trigger="click"
|
||||
menuStyles={{
|
||||
borderRadius: '8px',
|
||||
padding: '8px',
|
||||
background: 'var(--affine-background-overlay-panel-color)',
|
||||
items={EditMenu}
|
||||
portalOptions={{
|
||||
container: ref.current,
|
||||
}}
|
||||
// menuStyles={{
|
||||
// borderRadius: '8px',
|
||||
// padding: '8px',
|
||||
// background: 'var(--affine-background-overlay-panel-color)',
|
||||
// }}
|
||||
>
|
||||
<div>
|
||||
<HeaderDropDownButton />
|
||||
</div>
|
||||
<HeaderDropDownButton />
|
||||
</Menu>
|
||||
<MoveToTrash.ConfirmModal
|
||||
open={openConfirm}
|
||||
|
||||
@@ -1,38 +1,28 @@
|
||||
import { styled } from '@affine/component';
|
||||
import { ArrowDownSmallIcon } from '@blocksuite/icons';
|
||||
import {
|
||||
IconButton,
|
||||
type IconButtonProps,
|
||||
} from '@toeverything/components/button';
|
||||
import { forwardRef } from 'react';
|
||||
|
||||
const StyledIconButtonWithAnimate = styled(IconButton)(() => {
|
||||
return {
|
||||
svg: {
|
||||
transition: 'transform 0.15s ease-in-out',
|
||||
},
|
||||
':hover': {
|
||||
svg: {
|
||||
transform: 'translateY(3px)',
|
||||
},
|
||||
backgroundColor: 'transparent !important',
|
||||
},
|
||||
};
|
||||
});
|
||||
import { headerMenuTrigger } from './styles.css';
|
||||
|
||||
// fixme(himself65): need to refactor
|
||||
export const HeaderDropDownButton = ({
|
||||
onClick,
|
||||
...props
|
||||
}: Omit<IconButtonProps, 'children'>) => {
|
||||
export const HeaderDropDownButton = forwardRef<
|
||||
HTMLButtonElement,
|
||||
Omit<IconButtonProps, 'children'>
|
||||
>((props, ref) => {
|
||||
return (
|
||||
<StyledIconButtonWithAnimate
|
||||
data-testid="header-dropDownButton"
|
||||
<IconButton
|
||||
ref={ref}
|
||||
{...props}
|
||||
onClick={e => {
|
||||
onClick?.(e);
|
||||
}}
|
||||
data-testid="header-dropDownButton"
|
||||
className={headerMenuTrigger}
|
||||
withoutHoverStyle={true}
|
||||
type="plain"
|
||||
>
|
||||
<ArrowDownSmallIcon />
|
||||
</StyledIconButtonWithAnimate>
|
||||
</IconButton>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
HeaderDropDownButton.displayName = 'HeaderDropDownButton';
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
import { globalStyle, style } from '@vanilla-extract/css';
|
||||
|
||||
export const headerMenuTrigger = style({});
|
||||
|
||||
globalStyle(`${headerMenuTrigger} svg`, {
|
||||
transition: 'transform 0.15s ease-in-out',
|
||||
});
|
||||
globalStyle(`${headerMenuTrigger}:hover svg`, {
|
||||
transform: 'translateY(3px)',
|
||||
});
|
||||
@@ -1,4 +1,3 @@
|
||||
import { Menu, MenuItem } from '@affine/component';
|
||||
import { WorkspaceList } from '@affine/component/workspace-list';
|
||||
import type {
|
||||
AffineCloudWorkspace,
|
||||
@@ -19,6 +18,7 @@ import type { DragEndEvent } from '@dnd-kit/core';
|
||||
import { Popover } from '@mui/material';
|
||||
import { IconButton } from '@toeverything/components/button';
|
||||
import { Divider } from '@toeverything/components/divider';
|
||||
import { Menu, MenuIcon, MenuItem } from '@toeverything/components/menu';
|
||||
import { useSetAtom } from 'jotai';
|
||||
// eslint-disable-next-line @typescript-eslint/no-restricted-imports
|
||||
import { signOut, useSession } from 'next-auth/react';
|
||||
@@ -35,6 +35,7 @@ import {
|
||||
StyledCreateWorkspaceCardPillContent,
|
||||
StyledCreateWorkspaceCardPillIcon,
|
||||
StyledImportWorkspaceCardPill,
|
||||
StyledItem,
|
||||
StyledModalBody,
|
||||
StyledModalContent,
|
||||
StyledModalFooterContent,
|
||||
@@ -68,13 +69,12 @@ const AccountMenu = () => {
|
||||
const setOpen = useSetAtom(openSettingModalAtom);
|
||||
return (
|
||||
<div>
|
||||
{/* <div>Unlimted</div>
|
||||
<Divider size="thinner" dividerColor="var(--affine-border-color)" />
|
||||
<MenuItem icon={<ImportIcon />} data-testid="editor-option-menu-import">
|
||||
{t['com.affine.workspace.cloud.join']()}
|
||||
</MenuItem> */}
|
||||
<MenuItem
|
||||
icon={<AccountIcon />}
|
||||
preFix={
|
||||
<MenuIcon>
|
||||
<AccountIcon />
|
||||
</MenuIcon>
|
||||
}
|
||||
data-testid="editor-option-menu-import"
|
||||
onClick={useCallback(() => {
|
||||
setOpen(prev => ({ ...prev, open: true, activeTab: 'account' }));
|
||||
@@ -84,7 +84,11 @@ const AccountMenu = () => {
|
||||
</MenuItem>
|
||||
<Divider />
|
||||
<MenuItem
|
||||
icon={<SignOutIcon />}
|
||||
preFix={
|
||||
<MenuIcon>
|
||||
<SignOutIcon />
|
||||
</MenuIcon>
|
||||
}
|
||||
data-testid="editor-option-menu-import"
|
||||
onClick={useCallback(() => {
|
||||
signOut().catch(console.error);
|
||||
@@ -188,11 +192,7 @@ export const WorkspaceListModal = ({
|
||||
{!isLoggedIn ? (
|
||||
<StyledModalHeaderContent>
|
||||
<StyledSignInCardPill>
|
||||
<MenuItem
|
||||
style={{
|
||||
height: 'auto',
|
||||
padding: '0px 12px',
|
||||
}}
|
||||
<StyledItem
|
||||
onClick={async () => {
|
||||
if (!runtimeConfig.enableCloud) {
|
||||
setDisableCloudOpen(true);
|
||||
@@ -218,7 +218,7 @@ export const WorkspaceListModal = ({
|
||||
</StyledSignInCardPillTextSecondary>
|
||||
</StyledSignInCardPillTextCotainer>
|
||||
</StyledCreateWorkspaceCardPillContent>
|
||||
</MenuItem>
|
||||
</StyledItem>
|
||||
</StyledSignInCardPill>
|
||||
<Divider style={{ margin: '12px 0px' }} />
|
||||
</StyledModalHeaderContent>
|
||||
@@ -227,12 +227,7 @@ export const WorkspaceListModal = ({
|
||||
<StyledModalHeader>
|
||||
<StyledModalTitle>{session?.user.email}</StyledModalTitle>
|
||||
<StyledOperationWrapper>
|
||||
<Menu
|
||||
placement="bottom-end"
|
||||
trigger={['click']}
|
||||
content={<AccountMenu />}
|
||||
zIndex={1000}
|
||||
>
|
||||
<Menu items={<AccountMenu />}>
|
||||
<IconButton
|
||||
data-testid="more-button"
|
||||
icon={<MoreHorizontalIcon />}
|
||||
@@ -287,14 +282,7 @@ export const WorkspaceListModal = ({
|
||||
</StyledModalContent>
|
||||
{runtimeConfig.enableSQLiteProvider && environment.isDesktop ? (
|
||||
<StyledImportWorkspaceCardPill>
|
||||
<MenuItem
|
||||
onClick={onAddWorkspace}
|
||||
data-testid="add-workspace"
|
||||
style={{
|
||||
height: 'auto',
|
||||
padding: '8px 12px',
|
||||
}}
|
||||
>
|
||||
<StyledItem onClick={onAddWorkspace} data-testid="add-workspace">
|
||||
<StyledCreateWorkspaceCardPillContent>
|
||||
<StyledCreateWorkspaceCardPillIcon>
|
||||
<ImportIcon />
|
||||
@@ -303,20 +291,13 @@ export const WorkspaceListModal = ({
|
||||
<p>{t['com.affine.workspace.local.import']()}</p>
|
||||
</div>
|
||||
</StyledCreateWorkspaceCardPillContent>
|
||||
</MenuItem>
|
||||
</StyledItem>
|
||||
</StyledImportWorkspaceCardPill>
|
||||
) : null}
|
||||
</StyledModalBody>
|
||||
<StyledModalFooterContent>
|
||||
<StyledCreateWorkspaceCardPill>
|
||||
<MenuItem
|
||||
style={{
|
||||
height: 'auto',
|
||||
padding: '8px 12px',
|
||||
}}
|
||||
onClick={onNewWorkspace}
|
||||
data-testid="new-workspace"
|
||||
>
|
||||
<StyledItem onClick={onNewWorkspace} data-testid="new-workspace">
|
||||
<StyledCreateWorkspaceCardPillContent>
|
||||
<StyledCreateWorkspaceCardPillIcon>
|
||||
<PlusIcon />
|
||||
@@ -325,7 +306,7 @@ export const WorkspaceListModal = ({
|
||||
<p>{t['New Workspace']()}</p>
|
||||
</div>
|
||||
</StyledCreateWorkspaceCardPillContent>
|
||||
</MenuItem>
|
||||
</StyledItem>
|
||||
</StyledCreateWorkspaceCardPill>
|
||||
</StyledModalFooterContent>
|
||||
</Popover>
|
||||
|
||||
@@ -241,3 +241,32 @@ export const StyledWorkspaceFlavourTitle = styled('div')(() => {
|
||||
marginBottom: '4px',
|
||||
};
|
||||
});
|
||||
export const StyledItem = styled('button')<{
|
||||
active?: boolean;
|
||||
}>(({ active = false }) => {
|
||||
return {
|
||||
height: 'auto',
|
||||
padding: '8px 12px',
|
||||
width: '100%',
|
||||
borderRadius: '5px',
|
||||
fontSize: 'var(--affine-font-sm)',
|
||||
...displayFlex('flex-start', 'center'),
|
||||
cursor: 'pointer',
|
||||
position: 'relative',
|
||||
backgroundColor: 'transparent',
|
||||
color: 'var(--affine-text-primary-color)',
|
||||
svg: {
|
||||
color: 'var(--affine-icon-color)',
|
||||
},
|
||||
|
||||
':hover': {
|
||||
backgroundColor: 'var(--affine-hover-color)',
|
||||
},
|
||||
|
||||
...(active
|
||||
? {
|
||||
backgroundColor: 'var(--affine-hover-color)',
|
||||
}
|
||||
: {}),
|
||||
};
|
||||
});
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { Menu } from '@affine/component';
|
||||
import { MenuItem } from '@affine/component/app-sidebar';
|
||||
import { MenuItem as CollectionItem } from '@affine/component/app-sidebar';
|
||||
import {
|
||||
EditCollectionModel,
|
||||
useCollectionManager,
|
||||
@@ -21,6 +20,13 @@ import type { PageMeta, Workspace } from '@blocksuite/store';
|
||||
import type { DragEndEvent } from '@dnd-kit/core';
|
||||
import { useDroppable } from '@dnd-kit/core';
|
||||
import * as Collapsible from '@radix-ui/react-collapsible';
|
||||
import { IconButton } from '@toeverything/components/button';
|
||||
import {
|
||||
Menu,
|
||||
MenuIcon,
|
||||
MenuItem,
|
||||
type MenuItemProps,
|
||||
} from '@toeverything/components/menu';
|
||||
import { useBlockSuitePageMeta } from '@toeverything/hooks/use-block-suite-page-meta';
|
||||
import type { ReactElement } from 'react';
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
@@ -60,7 +66,7 @@ const CollectionOperations = ({
|
||||
icon: ReactElement;
|
||||
name: string;
|
||||
click: () => void;
|
||||
className?: string;
|
||||
type?: MenuItemProps['type'];
|
||||
element?: undefined;
|
||||
}
|
||||
| {
|
||||
@@ -70,12 +76,20 @@ const CollectionOperations = ({
|
||||
>(
|
||||
() => [
|
||||
{
|
||||
icon: <FilterIcon />,
|
||||
icon: (
|
||||
<MenuIcon>
|
||||
<FilterIcon />
|
||||
</MenuIcon>
|
||||
),
|
||||
name: t['Edit Filter'](),
|
||||
click: showUpdateCollection,
|
||||
},
|
||||
{
|
||||
icon: <UnpinIcon />,
|
||||
icon: (
|
||||
<MenuIcon>
|
||||
<UnpinIcon />
|
||||
</MenuIcon>
|
||||
),
|
||||
name: t['Unpin'](),
|
||||
click: () => {
|
||||
return setting.updateCollection({
|
||||
@@ -88,12 +102,16 @@ const CollectionOperations = ({
|
||||
element: <div key="divider" className={styles.menuDividerStyle}></div>,
|
||||
},
|
||||
{
|
||||
icon: <DeleteIcon />,
|
||||
icon: (
|
||||
<MenuIcon>
|
||||
<DeleteIcon />
|
||||
</MenuIcon>
|
||||
),
|
||||
name: t['Delete'](),
|
||||
click: () => {
|
||||
return setting.deleteCollection(view.id);
|
||||
},
|
||||
className: styles.deleteFolder,
|
||||
type: 'danger',
|
||||
},
|
||||
],
|
||||
[setting, showUpdateCollection, t, view]
|
||||
@@ -108,8 +126,8 @@ const CollectionOperations = ({
|
||||
<MenuItem
|
||||
data-testid="collection-option"
|
||||
key={action.name}
|
||||
className={action.className}
|
||||
icon={action.icon}
|
||||
type={action.type}
|
||||
preFix={action.icon}
|
||||
onClick={action.click}
|
||||
>
|
||||
{action.name}
|
||||
@@ -181,6 +199,7 @@ const CollectionRenderer = ({
|
||||
const pagesToRender = pages.filter(
|
||||
page => filterPage(collection, page) && !page.trash
|
||||
);
|
||||
|
||||
return (
|
||||
<Collapsible.Root open={!collapsed}>
|
||||
<EditCollectionModel
|
||||
@@ -191,7 +210,7 @@ const CollectionRenderer = ({
|
||||
open={show}
|
||||
onClose={() => showUpdateCollection(false)}
|
||||
/>
|
||||
<MenuItem
|
||||
<CollectionItem
|
||||
data-testid="collection-item"
|
||||
data-type="collection-list-item"
|
||||
ref={setNodeRef}
|
||||
@@ -200,9 +219,7 @@ const CollectionRenderer = ({
|
||||
icon={<ViewLayersIcon />}
|
||||
postfix={
|
||||
<Menu
|
||||
trigger="click"
|
||||
placement="bottom-start"
|
||||
content={
|
||||
items={
|
||||
<CollectionOperations
|
||||
view={collection}
|
||||
showUpdateCollection={() => showUpdateCollection(true)}
|
||||
@@ -210,9 +227,13 @@ const CollectionRenderer = ({
|
||||
/>
|
||||
}
|
||||
>
|
||||
<div data-testid="collection-options" className={styles.more}>
|
||||
<IconButton
|
||||
data-testid="collection-options"
|
||||
type="plain"
|
||||
withoutHoverStyle
|
||||
>
|
||||
<MoreHorizontalIcon />
|
||||
</div>
|
||||
</IconButton>
|
||||
</Menu>
|
||||
}
|
||||
collapsed={pagesToRender.length > 0 ? collapsed : undefined}
|
||||
@@ -227,7 +248,7 @@ const CollectionRenderer = ({
|
||||
>
|
||||
<div>{collection.name}</div>
|
||||
</div>
|
||||
</MenuItem>
|
||||
</CollectionItem>
|
||||
<Collapsible.Content className={styles.collapsibleContent}>
|
||||
<div style={{ marginLeft: 20, marginTop: -4 }}>
|
||||
{pagesToRender.map(page => {
|
||||
@@ -260,13 +281,13 @@ export const CollectionsList = ({ workspace }: CollectionsListProps) => {
|
||||
const t = useAFFiNEI18N();
|
||||
if (pinedCollections.length === 0) {
|
||||
return (
|
||||
<MenuItem
|
||||
<CollectionItem
|
||||
data-testid="slider-bar-collection-null-description"
|
||||
icon={<InformationIcon />}
|
||||
disabled
|
||||
>
|
||||
<span>{t['Create a collection']()}</span>
|
||||
</MenuItem>
|
||||
</CollectionItem>
|
||||
);
|
||||
}
|
||||
return (
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { Menu } from '@affine/component';
|
||||
import { MenuItem } from '@affine/component/app-sidebar';
|
||||
import { MenuItem as CollectionItem } from '@affine/component/app-sidebar';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import {
|
||||
DeleteIcon,
|
||||
@@ -11,6 +10,13 @@ import {
|
||||
} from '@blocksuite/icons';
|
||||
import type { PageMeta, Workspace } from '@blocksuite/store';
|
||||
import * as Collapsible from '@radix-ui/react-collapsible';
|
||||
import { IconButton } from '@toeverything/components/button';
|
||||
import {
|
||||
Menu,
|
||||
MenuIcon,
|
||||
MenuItem,
|
||||
type MenuItemProps,
|
||||
} from '@toeverything/components/menu';
|
||||
import { useBlockSuitePageReferences } from '@toeverything/hooks/use-block-suite-page-references';
|
||||
import { useAtomValue } from 'jotai/index';
|
||||
import type { ReactElement } from 'react';
|
||||
@@ -46,7 +52,7 @@ export const PageOperations = ({
|
||||
icon: ReactElement;
|
||||
name: string;
|
||||
click: () => void;
|
||||
className?: string;
|
||||
type?: MenuItemProps['type'];
|
||||
element?: undefined;
|
||||
}
|
||||
| {
|
||||
@@ -58,7 +64,11 @@ export const PageOperations = ({
|
||||
...(inAllowList
|
||||
? [
|
||||
{
|
||||
icon: <FilterMinusIcon />,
|
||||
icon: (
|
||||
<MenuIcon>
|
||||
<FilterMinusIcon />
|
||||
</MenuIcon>
|
||||
),
|
||||
name: t['Remove special filter'](),
|
||||
click: () => removeFromAllowList(page.id),
|
||||
},
|
||||
@@ -67,7 +77,11 @@ export const PageOperations = ({
|
||||
...(!inExcludeList
|
||||
? [
|
||||
{
|
||||
icon: <FilterUndoIcon />,
|
||||
icon: (
|
||||
<MenuIcon>
|
||||
<FilterUndoIcon />
|
||||
</MenuIcon>
|
||||
),
|
||||
name: t['Exclude from filter'](),
|
||||
click: () => addToExcludeList(page.id),
|
||||
},
|
||||
@@ -77,12 +91,16 @@ export const PageOperations = ({
|
||||
element: <div key="divider" className={styles.menuDividerStyle}></div>,
|
||||
},
|
||||
{
|
||||
icon: <DeleteIcon />,
|
||||
icon: (
|
||||
<MenuIcon>
|
||||
<DeleteIcon />
|
||||
</MenuIcon>
|
||||
),
|
||||
name: t['Delete'](),
|
||||
click: () => {
|
||||
removeToTrash(page.id);
|
||||
},
|
||||
className: styles.deleteFolder,
|
||||
type: 'danger',
|
||||
},
|
||||
],
|
||||
[
|
||||
@@ -105,8 +123,8 @@ export const PageOperations = ({
|
||||
<MenuItem
|
||||
data-testid="collection-page-option"
|
||||
key={action.name}
|
||||
className={action.className}
|
||||
icon={action.icon}
|
||||
type={action.type}
|
||||
preFix={action.icon}
|
||||
onClick={action.click}
|
||||
>
|
||||
{action.name}
|
||||
@@ -133,6 +151,7 @@ export const Page = ({
|
||||
workspace: Workspace;
|
||||
allPageMeta: Record<string, PageMeta>;
|
||||
}) => {
|
||||
const ref = React.useRef(null);
|
||||
const [collapsed, setCollapsed] = React.useState(true);
|
||||
const params = useParams();
|
||||
const { jumpToPage } = useNavigateHelper();
|
||||
@@ -148,7 +167,7 @@ export const Page = ({
|
||||
const referencesToRender = references.filter(id => !allPageMeta[id]?.trash);
|
||||
return (
|
||||
<Collapsible.Root open={!collapsed}>
|
||||
<MenuItem
|
||||
<CollectionItem
|
||||
data-testid="collection-page"
|
||||
data-type="collection-list-item"
|
||||
icon={icon}
|
||||
@@ -157,31 +176,35 @@ export const Page = ({
|
||||
active={active}
|
||||
collapsed={referencesToRender.length > 0 ? collapsed : undefined}
|
||||
onCollapsedChange={setCollapsed}
|
||||
ref={ref}
|
||||
postfix={
|
||||
<Menu
|
||||
trigger="click"
|
||||
placement="bottom-start"
|
||||
content={
|
||||
<div style={{ width: 220 }}>
|
||||
<PageOperations
|
||||
inAllowList={inAllowList}
|
||||
removeFromAllowList={removeFromAllowList}
|
||||
inExcludeList={inExcludeList}
|
||||
addToExcludeList={addToExcludeList}
|
||||
page={page}
|
||||
workspace={workspace}
|
||||
/>
|
||||
</div>
|
||||
items={
|
||||
<PageOperations
|
||||
inAllowList={inAllowList}
|
||||
removeFromAllowList={removeFromAllowList}
|
||||
inExcludeList={inExcludeList}
|
||||
addToExcludeList={addToExcludeList}
|
||||
page={page}
|
||||
workspace={workspace}
|
||||
/>
|
||||
}
|
||||
portalOptions={{
|
||||
container: ref.current,
|
||||
}}
|
||||
>
|
||||
<div data-testid="collection-page-options" className={styles.more}>
|
||||
<MoreHorizontalIcon></MoreHorizontalIcon>
|
||||
</div>
|
||||
<IconButton
|
||||
data-testid="collection-page-options"
|
||||
type="plain"
|
||||
withoutHoverStyle
|
||||
>
|
||||
<MoreHorizontalIcon />
|
||||
</IconButton>
|
||||
</Menu>
|
||||
}
|
||||
>
|
||||
{page.title || t['Untitled']()}
|
||||
</MenuItem>
|
||||
</CollectionItem>
|
||||
<Collapsible.Content className={styles.collapsibleContent}>
|
||||
{referencesToRender.map(id => {
|
||||
return (
|
||||
|
||||
Reference in New Issue
Block a user