mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-26 02:35:58 +08:00
feat(core): enable new all docs by default (#12404)
<!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **Refactor** - Simplified the user interface by always displaying the new All Pages view, removing the feature flag and old page version. - Updated selection interactions to use shift+click on document items instead of checkboxes. - Centralized drag-and-drop functionality in document list items and simplified drag handle behavior. - Generalized new page button component to accept standard HTML attributes. - Changed test ID attributes on new page buttons and list headers to use standard `data-testid`. - **Bug Fixes** - Added stable test identifiers to new page buttons, document list items, menu items, and operation buttons for improved test reliability. - Enabled external drag-and-drop support on the trash button. - **Tests** - Streamlined and updated end-to-end tests to match the new selection flow and UI changes, removing outdated or redundant test cases. - Simplified utility functions and wait conditions in test helpers for better accuracy and maintainability. - Updated selectors in tests to reflect new document item identifiers and centralized page element retrieval using utility functions. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
@@ -15,6 +15,8 @@ const DefaultDisplayPreference: ExplorerDisplayPreference = {
|
||||
showDocIcon: true,
|
||||
showDocPreview: true,
|
||||
quickFavorite: true,
|
||||
showDragHandle: true,
|
||||
showMoreOperation: true,
|
||||
};
|
||||
|
||||
export type DocExplorerContextType = {
|
||||
@@ -85,5 +87,11 @@ export const createDocExplorerContext = (
|
||||
quickTab$: displayPreference$.selector(
|
||||
displayPreference => displayPreference.quickTab
|
||||
),
|
||||
showMoreOperation$: displayPreference$.selector(
|
||||
displayPreference => displayPreference.showMoreOperation
|
||||
),
|
||||
showDragHandle$: displayPreference$.selector(
|
||||
displayPreference => displayPreference.showDragHandle
|
||||
),
|
||||
} satisfies DocExplorerContextType;
|
||||
};
|
||||
|
||||
@@ -8,7 +8,6 @@ import { DocsService } from '@affine/core/modules/doc';
|
||||
import { DocDisplayMetaService } from '@affine/core/modules/doc-display-meta';
|
||||
import { WorkbenchLink } from '@affine/core/modules/workbench';
|
||||
import type { AffineDNDData } from '@affine/core/types/dnd';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
import {
|
||||
AutoTidyUpIcon,
|
||||
PropertyIcon,
|
||||
@@ -146,20 +145,45 @@ export const DocListItem = ({ ...props }: DocListItemProps) => {
|
||||
[contextValue, handleMultiSelect, prevCheckAnchorId, props, selectMode]
|
||||
);
|
||||
|
||||
const { dragRef, CustomDragPreview } = useDraggable<AffineDNDData>(
|
||||
() => ({
|
||||
canDrag: true,
|
||||
data: {
|
||||
entity: {
|
||||
type: 'doc',
|
||||
id: props.docId as string,
|
||||
},
|
||||
from: {
|
||||
at: 'all-docs:list',
|
||||
},
|
||||
},
|
||||
}),
|
||||
[props.docId]
|
||||
);
|
||||
|
||||
return (
|
||||
<WorkbenchLink
|
||||
draggable={false}
|
||||
to={`/${props.docId}`}
|
||||
onClick={handleClick}
|
||||
data-selected={selectedDocIds.includes(props.docId)}
|
||||
className={styles.root}
|
||||
>
|
||||
{view === 'list' ? (
|
||||
<ListViewDoc {...props} />
|
||||
) : (
|
||||
<CardViewDoc {...props} />
|
||||
)}
|
||||
</WorkbenchLink>
|
||||
<>
|
||||
<WorkbenchLink
|
||||
ref={dragRef}
|
||||
draggable={false}
|
||||
to={`/${props.docId}`}
|
||||
onClick={handleClick}
|
||||
data-selected={selectedDocIds.includes(props.docId)}
|
||||
className={styles.root}
|
||||
data-testid={`doc-list-item`}
|
||||
data-doc-id={props.docId}
|
||||
>
|
||||
{view === 'list' ? (
|
||||
<ListViewDoc {...props} />
|
||||
) : (
|
||||
<CardViewDoc {...props} />
|
||||
)}
|
||||
</WorkbenchLink>
|
||||
<CustomDragPreview>
|
||||
<RawDocIcon id={props.docId} />
|
||||
<RawDocTitle id={props.docId} />
|
||||
</CustomDragPreview>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -172,10 +196,9 @@ const RawDocIcon = memo(function RawDocIcon({
|
||||
return <Icon {...props} />;
|
||||
});
|
||||
const RawDocTitle = memo(function RawDocTitle({ id }: { id: string }) {
|
||||
const i18n = useI18n();
|
||||
const docDisplayMetaService = useService(DocDisplayMetaService);
|
||||
const title = useLiveData(docDisplayMetaService.title$(id));
|
||||
return i18n.t(title);
|
||||
return title;
|
||||
});
|
||||
const RawDocPreview = memo(function RawDocPreview({
|
||||
id,
|
||||
@@ -188,47 +211,20 @@ const RawDocPreview = memo(function RawDocPreview({
|
||||
});
|
||||
const DragHandle = memo(function DragHandle({
|
||||
id,
|
||||
preview,
|
||||
...props
|
||||
}: HTMLProps<HTMLDivElement> & { preview?: ReactNode }) {
|
||||
}: HTMLProps<HTMLDivElement>) {
|
||||
const contextValue = useContext(DocExplorerContext);
|
||||
const selectMode = useLiveData(contextValue.selectMode$);
|
||||
const showDragHandle = useLiveData(contextValue.showDragHandle$);
|
||||
|
||||
const { dragRef, CustomDragPreview } = useDraggable<AffineDNDData>(
|
||||
() => ({
|
||||
canDrag: true,
|
||||
data: {
|
||||
entity: {
|
||||
type: 'doc',
|
||||
id: id as string,
|
||||
},
|
||||
from: {
|
||||
at: 'all-docs:list',
|
||||
},
|
||||
},
|
||||
}),
|
||||
[id]
|
||||
);
|
||||
|
||||
if (selectMode || !id || !showDragHandle) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div ref={dragRef} {...props}>
|
||||
<DragHandleIcon />
|
||||
</div>
|
||||
<CustomDragPreview>
|
||||
{preview ?? (
|
||||
<>
|
||||
<RawDocIcon id={id} />
|
||||
<RawDocTitle id={id} />
|
||||
</>
|
||||
)}
|
||||
</CustomDragPreview>
|
||||
</>
|
||||
<div {...props}>
|
||||
<DragHandleIcon />
|
||||
</div>
|
||||
);
|
||||
});
|
||||
const Select = memo(function Select({
|
||||
@@ -248,7 +244,11 @@ const Select = memo(function Select({
|
||||
}
|
||||
|
||||
return (
|
||||
<div data-select-mode={selectMode} {...props}>
|
||||
<div
|
||||
data-select-mode={selectMode}
|
||||
data-testid={`doc-list-item-select`}
|
||||
{...props}
|
||||
>
|
||||
<Checkbox
|
||||
checked={selectedDocIds.includes(id)}
|
||||
onChange={handleSelectChange}
|
||||
@@ -323,7 +323,11 @@ export const ListViewDoc = ({ docId }: DocListItemProps) => {
|
||||
<Select id={docId} className={styles.listSelect} />
|
||||
<DocIcon id={docId} className={styles.listIcon} />
|
||||
<div className={styles.listBrief}>
|
||||
<DocTitle id={docId} className={styles.listTitle} />
|
||||
<DocTitle
|
||||
id={docId}
|
||||
className={styles.listTitle}
|
||||
data-testid="doc-list-item-title"
|
||||
/>
|
||||
<DocPreview
|
||||
id={docId}
|
||||
className={styles.listPreview}
|
||||
@@ -370,7 +374,11 @@ export const CardViewDoc = ({ docId }: DocListItemProps) => {
|
||||
<li className={styles.cardViewRoot}>
|
||||
<header className={styles.cardViewHeader}>
|
||||
<DocIcon id={docId} className={styles.cardViewIcon} />
|
||||
<DocTitle id={docId} className={styles.cardViewTitle} />
|
||||
<DocTitle
|
||||
id={docId}
|
||||
className={styles.cardViewTitle}
|
||||
data-testid="doc-list-item-title"
|
||||
/>
|
||||
{quickActions.map(action => {
|
||||
return <action.Component size="16" key={action.key} doc={doc} />;
|
||||
})}
|
||||
|
||||
@@ -47,6 +47,7 @@ const ToggleFavorite = ({ docId }: DocOperationProps) => {
|
||||
<MenuItem
|
||||
prefixIcon={<IsFavoriteIcon favorite={favourite} />}
|
||||
onClick={toggleFavorite}
|
||||
data-testid="doc-list-operation-favorite"
|
||||
>
|
||||
{favourite
|
||||
? t['com.affine.favoritePageOperation.remove']()
|
||||
@@ -107,7 +108,7 @@ const SplitView = ({ docId }: DocOperationProps) => {
|
||||
|
||||
return (
|
||||
<MenuItem onClick={onOpenInSplitView} prefixIcon={<SplitViewIcon />}>
|
||||
{t['com.affine.workbench.tab.page-menu-open']()}
|
||||
{t['com.affine.workbench.split-view.page-menu-open']()}
|
||||
</MenuItem>
|
||||
);
|
||||
};
|
||||
@@ -165,7 +166,11 @@ const MoveToTrash = ({ docId }: DocOperationProps) => {
|
||||
}, [doc, openConfirmModal, t]);
|
||||
|
||||
return (
|
||||
<MenuItem prefixIcon={<DeleteIcon />} onClick={onMoveToTrash}>
|
||||
<MenuItem
|
||||
prefixIcon={<DeleteIcon />}
|
||||
data-testid="doc-list-operation-trash"
|
||||
onClick={onMoveToTrash}
|
||||
>
|
||||
{t['com.affine.moveToTrash.title']()}
|
||||
</MenuItem>
|
||||
);
|
||||
@@ -187,10 +192,22 @@ export const MoreMenuContent = (props: DocOperationProps) => {
|
||||
export const MoreMenu = ({
|
||||
docId,
|
||||
children,
|
||||
contentOptions,
|
||||
...menuProps
|
||||
}: Omit<MenuProps, 'items'> & { docId: string }) => {
|
||||
return (
|
||||
<Menu items={<MoreMenuContent docId={docId} />} {...menuProps}>
|
||||
<Menu
|
||||
items={<MoreMenuContent docId={docId} />}
|
||||
contentOptions={{
|
||||
...contentOptions,
|
||||
onClick: e => {
|
||||
// prevent external click events from being triggered
|
||||
e.stopPropagation();
|
||||
contentOptions?.onClick?.(e);
|
||||
},
|
||||
}}
|
||||
{...menuProps}
|
||||
>
|
||||
{children}
|
||||
</Menu>
|
||||
);
|
||||
@@ -213,7 +230,11 @@ export const MoreMenuButton = ({
|
||||
|
||||
return (
|
||||
<MoreMenu docId={docId} {...menuProps}>
|
||||
<IconButton icon={<MoreVerticalIcon />} {...iconProps} />
|
||||
<IconButton
|
||||
data-testid="doc-list-operation-button"
|
||||
icon={<MoreVerticalIcon />}
|
||||
{...iconProps}
|
||||
/>
|
||||
</MoreMenu>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -47,6 +47,7 @@ export const QuickFavorite = memo(function QuickFavorite({
|
||||
<IconButton
|
||||
icon={<IsFavoriteIcon favorite={favourite} />}
|
||||
onClick={toggleFavorite}
|
||||
data-testid="doc-list-operation-favorite"
|
||||
{...iconButtonProps}
|
||||
/>
|
||||
);
|
||||
@@ -157,6 +158,7 @@ export const QuickDelete = memo(function QuickDelete({
|
||||
onClick={onMoveToTrash}
|
||||
icon={<DeleteIcon />}
|
||||
variant="danger"
|
||||
data-testid="doc-list-operation-trash"
|
||||
{...iconButtonProps}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -92,7 +92,7 @@ export const PageListHeader = () => {
|
||||
<div className={styles.docListHeaderTitle}>{title}</div>
|
||||
<PageListNewPageButton
|
||||
size="small"
|
||||
testId="new-page-button-trigger"
|
||||
data-testid="new-page-button-trigger"
|
||||
onCreateEdgeless={e => createEdgeless({ at: inferOpenMode(e) })}
|
||||
onCreatePage={e =>
|
||||
createPage('page' as DocMode, { at: inferOpenMode(e) })
|
||||
@@ -194,7 +194,7 @@ export const CollectionPageListHeader = ({
|
||||
<Button onClick={handleEdit}>{t['Edit']()}</Button>
|
||||
<PageListNewPageButton
|
||||
size="small"
|
||||
testId="new-page-button-trigger"
|
||||
data-testid="new-page-button-trigger"
|
||||
onCreateDoc={onCreateDoc}
|
||||
onCreateEdgeless={onCreateEdgeless}
|
||||
onCreatePage={onCreatePage}
|
||||
|
||||
@@ -7,22 +7,22 @@ export const PageListNewPageButton = ({
|
||||
className,
|
||||
children,
|
||||
size,
|
||||
testId,
|
||||
onCreateDoc,
|
||||
onCreatePage,
|
||||
onCreateEdgeless,
|
||||
onImportFile,
|
||||
...props
|
||||
}: PropsWithChildren<{
|
||||
className?: string;
|
||||
size?: 'small' | 'default';
|
||||
testId?: string;
|
||||
onCreateDoc: (e?: MouseEvent) => void;
|
||||
onCreatePage: (e?: MouseEvent) => void;
|
||||
onCreateEdgeless: (e?: MouseEvent) => void;
|
||||
onImportFile?: (e?: MouseEvent) => void;
|
||||
}>) => {
|
||||
}> &
|
||||
React.HTMLAttributes<HTMLDivElement>) => {
|
||||
return (
|
||||
<div className={className} data-testid={testId}>
|
||||
<div className={className} {...props}>
|
||||
<NewPageButton
|
||||
size={size}
|
||||
importFile={onImportFile}
|
||||
|
||||
@@ -68,6 +68,7 @@ export const TrashButton = () => {
|
||||
}
|
||||
}
|
||||
},
|
||||
allowExternal: true,
|
||||
}),
|
||||
[docsService.list, guardService, openConfirmModal, t]
|
||||
);
|
||||
|
||||
@@ -104,6 +104,7 @@ export const AllDocsHeader = ({
|
||||
onCreatePage={e => createPage('page', { at: inferOpenMode(e) })}
|
||||
onCreateDoc={e => createPage(undefined, { at: inferOpenMode(e) })}
|
||||
onImportFile={onImportFile}
|
||||
data-testid="new-page-button-trigger"
|
||||
>
|
||||
<span className={styles.newPageButtonLabel}>{t['New Page']()}</span>
|
||||
</PageListNewPageButton>
|
||||
|
||||
@@ -12,7 +12,6 @@ import {
|
||||
} from '@affine/core/modules/collection';
|
||||
import { CollectionRulesService } from '@affine/core/modules/collection-rules';
|
||||
import type { FilterParams } from '@affine/core/modules/collection-rules/types';
|
||||
import { FeatureFlagService } from '@affine/core/modules/feature-flag';
|
||||
import { WorkspaceLocalState } from '@affine/core/modules/workspace';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
import { useLiveData, useService } from '@toeverything/infra';
|
||||
@@ -24,7 +23,6 @@ import {
|
||||
ViewIcon,
|
||||
ViewTitle,
|
||||
} from '../../../../modules/workbench';
|
||||
import { AllPage as AllPageOld } from '../all-page-old/all-page';
|
||||
import { AllDocSidebarTabs } from '../layouts/all-doc-sidebar-tabs';
|
||||
import * as styles from './all-page.css';
|
||||
import { AllDocsHeader } from './all-page-header';
|
||||
@@ -350,10 +348,5 @@ export const AllPage = () => {
|
||||
};
|
||||
|
||||
export const Component = () => {
|
||||
const featureFlagService = useService(FeatureFlagService);
|
||||
const enableNewAllDocsPage = useLiveData(
|
||||
featureFlagService.flags.enable_new_all_docs_page.$
|
||||
);
|
||||
|
||||
return enableNewAllDocsPage ? <AllPage /> : <AllPageOld />;
|
||||
return <AllPage />;
|
||||
};
|
||||
|
||||
@@ -100,7 +100,7 @@ export const CollectionListHeader = ({
|
||||
<Button onClick={handleEdit}>{t['Edit']()}</Button>
|
||||
<PageListNewPageButton
|
||||
size="small"
|
||||
testId="new-page-button-trigger"
|
||||
data-testid="new-page-button-trigger"
|
||||
onCreateDoc={onCreateDoc}
|
||||
onCreateEdgeless={onCreateEdgeless}
|
||||
onCreatePage={onCreatePage}
|
||||
|
||||
@@ -311,13 +311,6 @@ export const AFFINE_FLAGS = {
|
||||
configurable: false,
|
||||
defaultState: isCanaryBuild,
|
||||
},
|
||||
enable_new_all_docs_page: {
|
||||
category: 'affine',
|
||||
displayName: 'Enable New All Docs Page',
|
||||
description: 'Use new all docs page',
|
||||
configurable: isCanaryBuild,
|
||||
defaultState: false,
|
||||
},
|
||||
enable_cloud_indexer: {
|
||||
category: 'affine',
|
||||
displayName: 'Enable Cloud Indexer',
|
||||
|
||||
Reference in New Issue
Block a user