refactor(mobile): use separate explorer components for mobile (#8503)

close AF-1488

- remove dnd related logic
- separate styles
- remove empty status, always show a `New` button in each level of tree
This commit is contained in:
CatsJuice
2024-10-22 03:01:04 +00:00
parent 97ccf7f3e4
commit 21d3b5084a
41 changed files with 2814 additions and 251 deletions
@@ -64,27 +64,3 @@ export const collapseIcon = style({
},
},
});
// ------------- mobile -------------
export const mobileRoot = style([
root,
{
height: 25,
padding: '0 16px',
selectors: {
'&[data-collapsible="true"]:hover': {
backgroundColor: 'none',
},
},
},
]);
export const mobileLabel = style([
label,
{
color: cssVarV2('text/primary'),
fontSize: 20,
lineHeight: '25px',
letterSpacing: -0.45,
fontWeight: 400,
},
]);
@@ -9,7 +9,6 @@ export type CategoryDividerProps = PropsWithChildren<
label: string;
className?: string;
collapsed?: boolean;
mobile?: boolean;
setCollapsed?: (collapsed: boolean) => void;
} & {
[key: `data-${string}`]: unknown;
@@ -23,7 +22,6 @@ export const CategoryDivider = forwardRef(
children,
className,
collapsed,
mobile,
setCollapsed,
...otherProps
}: CategoryDividerProps,
@@ -33,16 +31,15 @@ export const CategoryDivider = forwardRef(
return (
<div
className={clsx(mobile ? styles.mobileRoot : styles.root, className)}
className={clsx(styles.root, className)}
ref={ref}
role="switch"
onClick={() => setCollapsed?.(!collapsed)}
data-mobile={mobile}
data-collapsed={collapsed}
data-collapsible={collapsible}
{...otherProps}
>
<div className={mobile ? styles.mobileLabel : styles.label}>
<div className={styles.label}>
{label}
{collapsible ? (
<ToggleCollapseIcon
@@ -53,11 +50,9 @@ export const CategoryDivider = forwardRef(
/>
) : null}
</div>
{mobile ? null : (
<div className={styles.actions} onClick={e => e.stopPropagation()}>
{children}
</div>
)}
<div className={styles.actions} onClick={e => e.stopPropagation()}>
{children}
</div>
</div>
);
}
@@ -9,11 +9,18 @@ import { ExplorerService } from './services/explorer';
export { ExplorerService } from './services/explorer';
export type { CollapsibleSectionName } from './types';
export { CollapsibleSection } from './views/layouts/collapsible-section';
export { ExplorerMobileContext } from './views/mobile.context';
export { ExplorerCollections } from './views/sections/collections';
export { ExplorerFavorites } from './views/sections/favorites';
export { ExplorerMigrationFavorites } from './views/sections/migration-favorites';
export { ExplorerOrganize } from './views/sections/organize';
// for mobile
export { ExplorerTreeRoot } from './views/tree';
export { ExplorerTreeContext } from './views/tree/context';
export type {
BaseExplorerTreeNodeProps,
ExplorerTreeNodeIcon,
} from './views/tree/node';
export type { NodeOperation } from './views/tree/types';
export function configureExplorerModule(framework: Framework) {
framework
@@ -13,8 +13,3 @@ export const header = style({
},
},
});
// mobile
export const mobileContent = style({
paddingTop: 8,
});
@@ -7,18 +7,11 @@ import {
type ReactNode,
type RefObject,
useCallback,
useContext,
} from 'react';
import { ExplorerService } from '../../services/explorer';
import type { CollapsibleSectionName } from '../../types';
import { ExplorerMobileContext } from '../mobile.context';
import {
content,
header,
mobileContent,
root,
} from './collapsible-section.css';
import { content, header, root } from './collapsible-section.css';
interface CollapsibleSectionProps extends PropsWithChildren {
name: CollapsibleSectionName;
@@ -50,7 +43,6 @@ export const CollapsibleSection = ({
contentClassName,
}: CollapsibleSectionProps) => {
const mobile = useContext(ExplorerMobileContext);
const section = useService(ExplorerService).sections[name];
const collapsed = useLiveData(section.collapsed$);
@@ -70,7 +62,6 @@ export const CollapsibleSection = ({
data-testid={testId}
>
<CategoryDivider
mobile={mobile}
data-testid={headerTestId}
label={title}
setCollapsed={setCollapsed}
@@ -82,7 +73,7 @@ export const CollapsibleSection = ({
</CategoryDivider>
<Collapsible.Content
data-testid="collapsible-section-content"
className={clsx(mobile ? mobileContent : content, contentClassName)}
className={clsx(content, contentClassName)}
>
{children}
</Collapsible.Content>
@@ -1,8 +0,0 @@
import { createContext } from 'react';
/**
* To enable mobile manually
* > Using `environment.isMobile` directly will affect current web entry on mobile
* > So we control it manually for now
*/
export const ExplorerMobileContext = createContext(false);
@@ -73,15 +73,8 @@ export const useExplorerCollectionNodeOperations = (
track.$.navigationPanel.collections.addDocToCollection({
control: 'button',
});
workbenchService.workbench.openDoc(newDoc.id);
onOpenCollapsed();
}, [
collectionId,
collectionService,
createPage,
onOpenCollapsed,
workbenchService.workbench,
]);
}, [collectionId, collectionService, createPage, onOpenCollapsed]);
const handleToggleFavoriteCollection = useCallback(() => {
compatibleFavoriteItemsAdapter.toggle(collectionId, 'collection');
@@ -129,9 +129,8 @@ export const useExplorerDocNodeOperations = (
await docsService.addLinkedDoc(docId, newDoc.id);
track.$.navigationPanel.docs.createDoc({ control: 'linkDoc' });
track.$.navigationPanel.docs.linkDoc({ control: 'createDoc' });
workbenchService.workbench.openDoc(newDoc.id);
options.openNodeCollapsed();
}, [createPage, docsService, docId, workbenchService.workbench, options]);
}, [createPage, docsService, docId, options]);
const handleToggleFavoriteDoc = useCallback(() => {
compatibleFavoriteItemsAdapter.toggle(docId, 'doc');
@@ -20,7 +20,6 @@ import {
type FolderNode,
OrganizeService,
} from '@affine/core/modules/organize';
import { WorkbenchService } from '@affine/core/modules/workbench';
import type { AffineDNDData } from '@affine/core/types/dnd';
import { Unreachable } from '@affine/env/constant';
import { useI18n } from '@affine/i18n';
@@ -173,7 +172,7 @@ const ExplorerFolderIcon: ExplorerTreeNodeIcon = ({
/>
);
export const ExplorerFolderNodeFolder = ({
const ExplorerFolderNodeFolder = ({
node,
onDrop,
defaultRenaming,
@@ -187,13 +186,11 @@ export const ExplorerFolderNodeFolder = ({
node: FolderNode;
} & GenericExplorerNode) => {
const t = useI18n();
const { workbenchService, workspaceService, featureFlagService } =
useServices({
WorkbenchService,
WorkspaceService,
CompatibleFavoriteItemsAdapter,
FeatureFlagService,
});
const { workspaceService, featureFlagService } = useServices({
WorkspaceService,
CompatibleFavoriteItemsAdapter,
FeatureFlagService,
});
const openDocsSelector = useSelectDoc();
const openTagsSelector = useSelectTag();
const openCollectionsSelector = useSelectCollection();
@@ -552,14 +549,13 @@ export const ExplorerFolderNodeFolder = ({
const handleNewDoc = useCallback(() => {
const newDoc = createPage();
node.createLink('doc', newDoc.id, node.indexAt('before'));
workbenchService.workbench.openDoc(newDoc.id);
track.$.navigationPanel.folders.createDoc();
track.$.navigationPanel.organize.createOrganizeItem({
type: 'link',
target: 'doc',
});
setCollapsed(false);
}, [createPage, node, workbenchService.workbench]);
}, [createPage, node]);
const handleCreateSubfolder = useCallback(() => {
const newFolderId = node.createFolder(
@@ -64,10 +64,9 @@ export const useExplorerTagNodeOperations = (
const newDoc = createPage();
tagRecord?.tag(newDoc.id);
track.$.navigationPanel.tags.createDoc();
workbenchService.workbench.openDoc(newDoc.id);
openNodeCollapsed();
}
}, [createPage, openNodeCollapsed, tagRecord, workbenchService.workbench]);
}, [createPage, openNodeCollapsed, tagRecord]);
const handleMoveToTrash = useCallback(() => {
tagService.tagList.deleteTag(tagId);
@@ -13,7 +13,6 @@ import {
FavoriteService,
isFavoriteSupportType,
} from '@affine/core/modules/favorite';
import { WorkbenchService } from '@affine/core/modules/workbench';
import type { AffineDNDData } from '@affine/core/types/dnd';
import { isNewTabTrigger } from '@affine/core/utils';
import { useI18n } from '@affine/i18n';
@@ -41,14 +40,8 @@ import {
import { RootEmpty } from './empty';
export const ExplorerFavorites = () => {
const {
favoriteService,
workspaceService,
workbenchService,
explorerService,
} = useServices({
const { favoriteService, workspaceService, explorerService } = useServices({
FavoriteService,
WorkbenchService,
WorkspaceService,
ExplorerService,
});
@@ -88,23 +81,18 @@ export const ExplorerFavorites = () => {
const handleCreateNewFavoriteDoc: MouseEventHandler = useCallback(
e => {
const newDoc = createPage();
const newDoc = createPage(
undefined,
isNewTabTrigger(e) ? 'new-tab' : true
);
favoriteService.favoriteList.add(
'doc',
newDoc.id,
favoriteService.favoriteList.indexAt('before')
);
workbenchService.workbench.openDoc(newDoc.id, {
at: isNewTabTrigger(e) ? 'new-tab' : 'active',
});
explorerSection.setCollapsed(false);
},
[
createPage,
explorerSection,
favoriteService.favoriteList,
workbenchService.workbench,
]
[createPage, explorerSection, favoriteService.favoriteList]
);
const handleOnChildrenDrop = useCallback(
@@ -179,67 +179,3 @@ export const draggedOverEffect = style({
},
},
});
// ---------- mobile ----------
export const mobileItemRoot = style([
itemRoot,
{
padding: '8px',
borderRadius: 0,
flexDirection: 'row-reverse',
gap: 12,
selectors: {
'&:hover': {
background: 'none',
},
'&:active': {
background: cssVar('hoverColor'),
},
'&[data-active="true"]': {
background: 'transparent',
},
},
':after': {
content: '',
width: `calc(100% + ${levelIndent})`,
height: 0.5,
background: cssVar('borderColor'),
bottom: 0,
position: 'absolute',
right: 0,
},
},
]);
export const mobileItemMain = style([itemMain, {}]);
export const mobileIconContainer = style([
iconContainer,
{
width: 32,
height: 32,
fontSize: 24,
},
]);
export const mobileCollapsedIconContainer = style([
collapsedIconContainer,
{
fontSize: 16,
},
]);
export const mobileItemContent = style([
itemContent,
{
fontSize: 17,
lineHeight: '22px',
letterSpacing: -0.43,
fontWeight: 400,
},
]);
export const mobileContentContainer = style([
contentContainer,
{
marginTop: 0,
},
]);
@@ -37,7 +37,6 @@ import {
useState,
} from 'react';
import { ExplorerMobileContext } from '../mobile.context';
import { ExplorerTreeContext } from './context';
import { DropEffect } from './drop-effect';
import * as styles from './node.css';
@@ -57,6 +56,38 @@ export type ExplorerTreeNodeIcon = React.ComponentType<{
collapsed?: boolean;
}>;
export interface BaseExplorerTreeNodeProps {
name?: string;
icon?: ExplorerTreeNodeIcon;
children?: React.ReactNode;
active?: boolean;
defaultRenaming?: boolean;
extractEmojiAsIcon?: boolean;
collapsed: boolean;
setCollapsed: (collapsed: boolean) => void;
renameable?: boolean;
onRename?: (newName: string) => void;
disabled?: boolean;
onClick?: () => void;
to?: To;
postfix?: React.ReactNode;
operations?: NodeOperation[];
childrenOperations?: NodeOperation[];
childrenPlaceholder?: React.ReactNode;
linkComponent?: React.ComponentType<
React.PropsWithChildren<{ to: To; className?: string }> & RefAttributes<any>
>;
[key: `data-${string}`]: any;
}
interface WebExplorerTreeNodeProps extends BaseExplorerTreeNodeProps {
canDrop?: DropTargetOptions<AffineDNDData>['canDrop'];
reorderable?: boolean;
dndData?: AffineDNDData;
onDrop?: (data: DropTargetDropEvent<AffineDNDData>) => void;
dropEffect?: ExplorerTreeNodeDropEffect;
}
export const ExplorerTreeNode = ({
children,
icon: Icon,
@@ -82,34 +113,7 @@ export const ExplorerTreeNode = ({
onDrop,
dropEffect,
...otherProps
}: {
name?: string;
icon?: ExplorerTreeNodeIcon;
children?: React.ReactNode;
active?: boolean;
reorderable?: boolean;
defaultRenaming?: boolean;
extractEmojiAsIcon?: boolean;
collapsed: boolean;
setCollapsed: (collapsed: boolean) => void;
renameable?: boolean;
onRename?: (newName: string) => void;
disabled?: boolean;
onClick?: () => void;
to?: To;
postfix?: React.ReactNode;
canDrop?: DropTargetOptions<AffineDNDData>['canDrop'];
operations?: NodeOperation[];
childrenOperations?: NodeOperation[];
childrenPlaceholder?: React.ReactNode;
linkComponent?: React.ComponentType<
React.PropsWithChildren<{ to: To; className?: string }> & RefAttributes<any>
>;
dndData?: AffineDNDData;
onDrop?: (data: DropTargetDropEvent<AffineDNDData>) => void;
dropEffect?: ExplorerTreeNodeDropEffect;
} & { [key in `data-${string}`]?: any }) => {
const mobile = useContext(ExplorerMobileContext);
}: WebExplorerTreeNodeProps) => {
const t = useI18n();
const cid = useId();
const context = useContext(ExplorerTreeContext);
@@ -141,21 +145,19 @@ export const ExplorerTreeNode = ({
AffineDNDData & { draggable: { __cid: string } }
>(
() => ({
canDrag: () => !mobile,
data: { ...dndData?.draggable, __cid: cid },
dragPreviewPosition: 'pointer-outside',
}),
[cid, dndData, mobile]
[cid, dndData]
);
const handleCanDrop = useMemo<DropTargetOptions<AffineDNDData>['canDrop']>(
() => args => {
if (mobile) return false;
if (!reorderable && args.treeInstruction?.type !== 'make-child') {
return false;
}
return (typeof canDrop === 'function' ? canDrop(args) : canDrop) ?? true;
},
[canDrop, mobile, reorderable]
[canDrop, reorderable]
);
const {
dropTargetRef,
@@ -319,7 +321,7 @@ export const ExplorerTreeNode = ({
const content = (
<div
onClick={handleClick}
className={mobile ? styles.mobileItemRoot : styles.itemRoot}
className={styles.itemRoot}
data-active={active}
data-disabled={disabled}
>
@@ -327,11 +329,7 @@ export const ExplorerTreeNode = ({
data-disabled={disabled}
onClick={handleCollapsedChange}
data-testid="explorer-collapsed-button"
className={
mobile
? styles.mobileCollapsedIconContainer
: styles.collapsedIconContainer
}
className={styles.collapsedIconContainer}
>
<ArrowDownSmallIcon
className={styles.collapsedIcon}
@@ -339,10 +337,8 @@ export const ExplorerTreeNode = ({
/>
</div>
<div className={clsx(mobile ? styles.mobileItemMain : styles.itemMain)}>
<div
className={mobile ? styles.mobileIconContainer : styles.iconContainer}
>
<div className={styles.itemMain}>
<div className={styles.iconContainer}>
{emoji ??
(Icon && (
<Icon
@@ -352,38 +348,34 @@ export const ExplorerTreeNode = ({
/>
))}
</div>
<div className={mobile ? styles.mobileItemContent : styles.itemContent}>
{name}
</div>
<div className={styles.itemContent}>{name}</div>
{postfix}
{mobile ? null : (
<div
className={styles.postfix}
onClick={e => {
// prevent jump to page
e.preventDefault();
}}
>
{inlineOperations.map(({ view }, index) => (
<Fragment key={index}>{view}</Fragment>
))}
{menuOperations.length > 0 && (
<Menu
items={menuOperations.map(({ view }, index) => (
<Fragment key={index}>{view}</Fragment>
))}
<div
className={styles.postfix}
onClick={e => {
// prevent jump to page
e.preventDefault();
}}
>
{inlineOperations.map(({ view }, index) => (
<Fragment key={index}>{view}</Fragment>
))}
{menuOperations.length > 0 && (
<Menu
items={menuOperations.map(({ view }, index) => (
<Fragment key={index}>{view}</Fragment>
))}
>
<IconButton
size="16"
data-testid="explorer-tree-node-operation-button"
style={{ marginLeft: 4 }}
>
<IconButton
size="16"
data-testid="explorer-tree-node-operation-button"
style={{ marginLeft: 4 }}
>
<MoreHorizontalIcon />
</IconButton>
</Menu>
)}
</div>
)}
<MoreHorizontalIcon />
</IconButton>
</Menu>
)}
</div>
</div>
{renameable && (
@@ -411,10 +403,7 @@ export const ExplorerTreeNode = ({
{...otherProps}
>
<div
className={clsx(
mobile ? styles.mobileContentContainer : styles.contentContainer,
styles.draggedOverEffect
)}
className={clsx(styles.contentContainer, styles.draggedOverEffect)}
data-open={!collapsed}
data-self-dragged-over={isSelfDraggedOver}
ref={dropTargetRef}