mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-17 01:56:27 +08:00
feat(core): new all docs header (#12182)
<!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **New Features** - Introduced a new grouped document explorer with a masonry layout, advanced filtering, and multi-select capabilities. - Added a floating toolbar for batch actions, such as deleting multiple documents. - Implemented a feature flag to enable the new "All Docs" page, allowing gradual rollout. - Added a new navigation component with links for Docs, Collections, and Tags. - Introduced a display menu button supporting additional menu properties. - **Enhancements** - Redesigned navigation headers across document, collection, and tag pages for improved consistency and usability. - Added new display and view toggling options for document lists. - Improved styling and responsiveness for explorer and header components. - Updated context to support dynamic view switching in the document explorer. - **Bug Fixes** - Updated context and prop types to enhance state management and user interactions. - **Refactor** - Replaced legacy page list views with a modern, grouped explorer interface. - Simplified and modularized header components and styles for better maintainability. - Removed workspace-specific controls from document headers to focus on explorer functionality. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
@@ -1,9 +1,11 @@
|
||||
import { createContext, type Dispatch, type SetStateAction } from 'react';
|
||||
|
||||
import type { DocListItemView } from './docs-view/doc-list-item';
|
||||
import type { ExplorerPreference } from './types';
|
||||
|
||||
export type DocExplorerContextType = ExplorerPreference & {
|
||||
view: 'list' | 'grid' | 'masonry';
|
||||
view: DocListItemView;
|
||||
setView: Dispatch<SetStateAction<DocListItemView>>;
|
||||
groups: Array<{ key: string; items: string[] }>;
|
||||
collapsed: string[];
|
||||
selectMode?: boolean;
|
||||
@@ -17,6 +19,7 @@ export type DocExplorerContextType = ExplorerPreference & {
|
||||
|
||||
export const DocExplorerContext = createContext<DocExplorerContextType>({
|
||||
view: 'list',
|
||||
setView: () => {},
|
||||
groups: [],
|
||||
collapsed: [],
|
||||
selectedDocIds: [],
|
||||
|
||||
@@ -1,4 +1,10 @@
|
||||
import { Button, Divider, Menu, MenuSub } from '@affine/component';
|
||||
import {
|
||||
Button,
|
||||
Divider,
|
||||
Menu,
|
||||
type MenuProps,
|
||||
MenuSub,
|
||||
} from '@affine/component';
|
||||
import type {
|
||||
GroupByParams,
|
||||
OrderByParams,
|
||||
@@ -92,12 +98,14 @@ export const ExplorerDisplayMenuButton = ({
|
||||
style,
|
||||
className,
|
||||
preference,
|
||||
menuProps,
|
||||
onChange,
|
||||
}: {
|
||||
style?: React.CSSProperties;
|
||||
className?: string;
|
||||
preference: ExplorerPreference;
|
||||
onChange?: (preference: ExplorerPreference) => void;
|
||||
menuProps?: Omit<MenuProps, 'items' | 'children'>;
|
||||
}) => {
|
||||
const t = useI18n();
|
||||
return (
|
||||
@@ -105,6 +113,7 @@ export const ExplorerDisplayMenuButton = ({
|
||||
items={
|
||||
<ExplorerDisplayMenu preference={preference} onChange={onChange} />
|
||||
}
|
||||
{...menuProps}
|
||||
>
|
||||
<Button
|
||||
className={className}
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
import { cssVarV2 } from '@toeverything/theme/v2';
|
||||
import { style } from '@vanilla-extract/css';
|
||||
|
||||
export const container = style({
|
||||
display: 'flex',
|
||||
gap: 12,
|
||||
alignItems: 'center',
|
||||
fontSize: 18,
|
||||
lineHeight: '26px',
|
||||
fontWeight: 600,
|
||||
});
|
||||
|
||||
export const item = style({
|
||||
color: cssVarV2.text.secondary,
|
||||
selectors: {
|
||||
'&[data-active="true"]': {
|
||||
color: cssVarV2.text.primary,
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,47 @@
|
||||
import { WorkbenchLink } from '@affine/core/modules/workbench';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
|
||||
import * as styles from './navigation.css';
|
||||
|
||||
const items = [
|
||||
{
|
||||
value: 'docs',
|
||||
label: 'com.affine.docs.header',
|
||||
testId: 'workspace-docs-button',
|
||||
to: '/all',
|
||||
},
|
||||
{
|
||||
value: 'collections',
|
||||
label: 'com.affine.collections.header',
|
||||
testId: 'workspace-collections-button',
|
||||
to: '/collection',
|
||||
},
|
||||
{
|
||||
value: 'tags',
|
||||
label: 'Tags',
|
||||
testId: 'workspace-tags-button',
|
||||
to: '/tag',
|
||||
},
|
||||
] as const;
|
||||
|
||||
type NavigationKey = (typeof items)[number]['value'];
|
||||
|
||||
export const ExplorerNavigation = ({ active }: { active: NavigationKey }) => {
|
||||
const t = useI18n();
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
{items.map(item => (
|
||||
<WorkbenchLink
|
||||
key={item.value}
|
||||
data-testid={item.testId}
|
||||
data-active={active === item.value}
|
||||
to={item.to}
|
||||
className={styles.item}
|
||||
>
|
||||
{t[item.label]()}
|
||||
</WorkbenchLink>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,6 +1,6 @@
|
||||
import { IconButton } from '@affine/component';
|
||||
import { ExplorerNavigation } from '@affine/core/components/explorer/header/navigation';
|
||||
import { Header } from '@affine/core/components/pure/header';
|
||||
import { WorkspaceModeFilterTab } from '@affine/core/components/pure/workspace-mode-filter-tab';
|
||||
import { PlusIcon } from '@blocksuite/icons/rc';
|
||||
import clsx from 'clsx';
|
||||
|
||||
@@ -26,7 +26,7 @@ export const AllCollectionHeader = ({
|
||||
)}
|
||||
/>
|
||||
}
|
||||
center={<WorkspaceModeFilterTab activeFilter={'collections'} />}
|
||||
left={<ExplorerNavigation active={'collections'} />}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,360 +0,0 @@
|
||||
import {
|
||||
Masonry,
|
||||
type MasonryGroup,
|
||||
RadioGroup,
|
||||
useConfirmModal,
|
||||
} from '@affine/component';
|
||||
import {
|
||||
DocExplorerContext,
|
||||
type DocExplorerContextType,
|
||||
} from '@affine/core/components/explorer/context';
|
||||
import { ExplorerDisplayMenuButton } from '@affine/core/components/explorer/display-menu';
|
||||
import {
|
||||
DocListItem,
|
||||
type DocListItemView,
|
||||
} from '@affine/core/components/explorer/docs-view/doc-list-item';
|
||||
import type { ExplorerPreference } from '@affine/core/components/explorer/types';
|
||||
import { Filters } from '@affine/core/components/filter';
|
||||
import { ListFloatingToolbar } from '@affine/core/components/page-list/components/list-floating-toolbar';
|
||||
import { WorkspacePropertyTypes } from '@affine/core/components/workspace-property-types';
|
||||
import { CollectionRulesService } from '@affine/core/modules/collection-rules';
|
||||
import type { FilterParams } from '@affine/core/modules/collection-rules/types';
|
||||
import { DocsService } from '@affine/core/modules/doc';
|
||||
import { WorkspacePropertyService } from '@affine/core/modules/workspace-property';
|
||||
import { Trans, useI18n } from '@affine/i18n';
|
||||
import { useLiveData, useService } from '@toeverything/infra';
|
||||
import { cssVarV2 } from '@toeverything/theme/v2';
|
||||
import {
|
||||
memo,
|
||||
useCallback,
|
||||
useContext,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useState,
|
||||
} from 'react';
|
||||
|
||||
import {
|
||||
ViewBody,
|
||||
ViewHeader,
|
||||
ViewIcon,
|
||||
ViewTitle,
|
||||
} from '../../../../modules/workbench';
|
||||
import { AllDocSidebarTabs } from '../layouts/all-doc-sidebar-tabs';
|
||||
import * as styles from './all-page.css';
|
||||
import { MigrationAllDocsDataNotification } from './migration-data';
|
||||
|
||||
const GroupHeader = memo(function GroupHeader({
|
||||
groupId,
|
||||
collapsed,
|
||||
itemCount,
|
||||
}: {
|
||||
groupId: string;
|
||||
collapsed?: boolean;
|
||||
itemCount: number;
|
||||
}) {
|
||||
const { groupBy } = useContext(DocExplorerContext);
|
||||
const propertyService = useService(WorkspacePropertyService);
|
||||
const allProperties = useLiveData(propertyService.sortedProperties$);
|
||||
|
||||
const groupType = groupBy?.type;
|
||||
const groupKey = groupBy?.key;
|
||||
|
||||
const header = useMemo(() => {
|
||||
if (groupType === 'property') {
|
||||
const property = allProperties.find(p => p.id === groupKey);
|
||||
if (!property) return null;
|
||||
|
||||
const config = WorkspacePropertyTypes[property.type];
|
||||
if (!config?.groupHeader) return null;
|
||||
return (
|
||||
<config.groupHeader
|
||||
groupId={groupId}
|
||||
docCount={itemCount}
|
||||
collapsed={!!collapsed}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
return '// TODO: ' + groupType;
|
||||
}
|
||||
}, [allProperties, collapsed, groupId, groupKey, groupType, itemCount]);
|
||||
|
||||
if (!groupType) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return header;
|
||||
});
|
||||
|
||||
const calcCardHeightById = (id: string) => {
|
||||
const max = 5;
|
||||
const min = 1;
|
||||
const code = id.charCodeAt(0);
|
||||
const value = Math.floor((code % (max - min)) + min);
|
||||
return 250 + value * 10;
|
||||
};
|
||||
|
||||
const DocListItemComponent = memo(function DocListItemComponent({
|
||||
itemId,
|
||||
groupId,
|
||||
}: {
|
||||
groupId: string;
|
||||
itemId: string;
|
||||
}) {
|
||||
return <DocListItem docId={itemId} groupId={groupId} />;
|
||||
});
|
||||
|
||||
export const AllPage = () => {
|
||||
const t = useI18n();
|
||||
const docsService = useService(DocsService);
|
||||
const [view, setView] = useState<DocListItemView>('masonry');
|
||||
const [collapsedGroups, setCollapsedGroups] = useState<string[]>([]);
|
||||
const [selectMode, setSelectMode] = useState(false);
|
||||
const [selectedDocIds, setSelectedDocIds] = useState<string[]>([]);
|
||||
const [prevCheckAnchorId, setPrevCheckAnchorId] = useState<string | null>(
|
||||
null
|
||||
);
|
||||
|
||||
const [explorerPreference, setExplorerPreference] =
|
||||
useState<ExplorerPreference>({
|
||||
filters: [
|
||||
{
|
||||
type: 'system',
|
||||
key: 'trash',
|
||||
value: 'false',
|
||||
method: 'is',
|
||||
},
|
||||
],
|
||||
displayProperties: [],
|
||||
showDocIcon: true,
|
||||
showDocPreview: true,
|
||||
});
|
||||
|
||||
const [groups, setGroups] = useState<any>([]);
|
||||
|
||||
const { openConfirmModal } = useConfirmModal();
|
||||
|
||||
const masonryItems = useMemo(() => {
|
||||
const items = groups.map((group: any) => {
|
||||
return {
|
||||
id: group.key,
|
||||
Component: groups.length > 1 ? GroupHeader : undefined,
|
||||
height: groups.length > 1 ? 24 : 0,
|
||||
className: styles.groupHeader,
|
||||
items: group.items.map((docId: string) => {
|
||||
return {
|
||||
id: docId,
|
||||
Component: DocListItemComponent,
|
||||
height:
|
||||
view === 'list'
|
||||
? 42
|
||||
: view === 'grid'
|
||||
? 280
|
||||
: calcCardHeightById(docId),
|
||||
'data-view': view,
|
||||
className: styles.docItem,
|
||||
};
|
||||
}),
|
||||
} satisfies MasonryGroup;
|
||||
});
|
||||
return items;
|
||||
}, [groups, view]);
|
||||
|
||||
const collectionRulesService = useService(CollectionRulesService);
|
||||
useEffect(() => {
|
||||
const subscription = collectionRulesService
|
||||
.watch(
|
||||
explorerPreference.filters ?? [],
|
||||
explorerPreference.groupBy,
|
||||
explorerPreference.orderBy
|
||||
)
|
||||
.subscribe({
|
||||
next: result => {
|
||||
setGroups(result.groups);
|
||||
},
|
||||
error: error => {
|
||||
console.error(error);
|
||||
},
|
||||
});
|
||||
return () => {
|
||||
subscription.unsubscribe();
|
||||
};
|
||||
}, [
|
||||
collectionRulesService,
|
||||
explorerPreference.filters,
|
||||
explorerPreference.groupBy,
|
||||
explorerPreference.orderBy,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
const onKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Escape') {
|
||||
setSelectMode(false);
|
||||
setSelectedDocIds([]);
|
||||
setPrevCheckAnchorId(null);
|
||||
}
|
||||
};
|
||||
document.addEventListener('keydown', onKeyDown);
|
||||
return () => {
|
||||
document.removeEventListener('keydown', onKeyDown);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const handleFilterChange = useCallback((filters: FilterParams[]) => {
|
||||
setExplorerPreference(prev => ({
|
||||
...prev,
|
||||
filters,
|
||||
}));
|
||||
}, []);
|
||||
|
||||
const toggleGroupCollapse = useCallback((groupId: string) => {
|
||||
setCollapsedGroups(prev => {
|
||||
return prev.includes(groupId)
|
||||
? prev.filter(id => id !== groupId)
|
||||
: [...prev, groupId];
|
||||
});
|
||||
}, []);
|
||||
const toggleDocSelect = useCallback((docId: string) => {
|
||||
setSelectMode(true);
|
||||
setSelectedDocIds(prev => {
|
||||
return prev.includes(docId)
|
||||
? prev.filter(id => id !== docId)
|
||||
: [...prev, docId];
|
||||
});
|
||||
}, []);
|
||||
const onSelect = useCallback(
|
||||
(...args: Parameters<typeof setSelectedDocIds>) => {
|
||||
setSelectMode(true);
|
||||
setSelectedDocIds(...args);
|
||||
},
|
||||
[]
|
||||
);
|
||||
const handleCloseFloatingToolbar = useCallback(() => {
|
||||
setSelectMode(false);
|
||||
setSelectedDocIds([]);
|
||||
}, []);
|
||||
const handleMultiDelete = useCallback(() => {
|
||||
if (selectedDocIds.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
openConfirmModal({
|
||||
title: t['com.affine.moveToTrash.confirmModal.title.multiple']({
|
||||
number: selectedDocIds.length.toString(),
|
||||
}),
|
||||
description: t[
|
||||
'com.affine.moveToTrash.confirmModal.description.multiple'
|
||||
]({
|
||||
number: selectedDocIds.length.toString(),
|
||||
}),
|
||||
cancelText: t['com.affine.confirmModal.button.cancel'](),
|
||||
confirmText: t.Delete(),
|
||||
confirmButtonOptions: {
|
||||
variant: 'error',
|
||||
},
|
||||
onConfirm: () => {
|
||||
for (const docId of selectedDocIds) {
|
||||
const doc = docsService.list.doc$(docId).value;
|
||||
doc?.moveToTrash();
|
||||
}
|
||||
},
|
||||
});
|
||||
}, [docsService.list, openConfirmModal, selectedDocIds, t]);
|
||||
|
||||
const explorerContextValue = useMemo(
|
||||
() =>
|
||||
({
|
||||
...explorerPreference,
|
||||
view,
|
||||
groups,
|
||||
collapsed: collapsedGroups,
|
||||
selectedDocIds,
|
||||
selectMode,
|
||||
prevCheckAnchorId,
|
||||
setPrevCheckAnchorId,
|
||||
onToggleCollapse: toggleGroupCollapse,
|
||||
onToggleSelect: toggleDocSelect,
|
||||
onSelect,
|
||||
}) satisfies DocExplorerContextType,
|
||||
[
|
||||
collapsedGroups,
|
||||
explorerPreference,
|
||||
groups,
|
||||
onSelect,
|
||||
prevCheckAnchorId,
|
||||
selectMode,
|
||||
selectedDocIds,
|
||||
toggleDocSelect,
|
||||
toggleGroupCollapse,
|
||||
view,
|
||||
]
|
||||
);
|
||||
|
||||
return (
|
||||
<DocExplorerContext.Provider value={explorerContextValue}>
|
||||
<ViewTitle title={t['All pages']()} />
|
||||
<ViewIcon icon="allDocs" />
|
||||
<ViewHeader></ViewHeader>
|
||||
<ViewBody>
|
||||
<div className={styles.body}>
|
||||
<MigrationAllDocsDataNotification />
|
||||
<div>
|
||||
<RadioGroup
|
||||
items={['masonry', 'grid', 'list']}
|
||||
value={view}
|
||||
onChange={setView}
|
||||
width={240}
|
||||
/>
|
||||
<Filters
|
||||
filters={explorerPreference.filters ?? []}
|
||||
onChange={handleFilterChange}
|
||||
/>
|
||||
<ExplorerDisplayMenuButton
|
||||
preference={explorerPreference}
|
||||
onChange={setExplorerPreference}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.scrollArea}>
|
||||
<Masonry
|
||||
items={masonryItems}
|
||||
gapY={12}
|
||||
gapX={12}
|
||||
groupsGap={12}
|
||||
groupHeaderGapWithItems={12}
|
||||
columns={view === 'list' ? 1 : undefined}
|
||||
itemWidthMin={220}
|
||||
preloadHeight={100}
|
||||
itemWidth={'stretch'}
|
||||
virtualScroll
|
||||
collapsedGroups={collapsedGroups}
|
||||
paddingX={useCallback(
|
||||
(w: number) => (w > 500 ? 24 : w > 393 ? 20 : 16),
|
||||
[]
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<ListFloatingToolbar
|
||||
open={selectMode}
|
||||
onDelete={handleMultiDelete}
|
||||
onClose={handleCloseFloatingToolbar}
|
||||
content={
|
||||
<Trans
|
||||
i18nKey="com.affine.page.toolbar.selected"
|
||||
count={selectedDocIds.length}
|
||||
>
|
||||
<div style={{ color: cssVarV2.text.secondary }}>
|
||||
{{ count: selectedDocIds.length } as any}
|
||||
</div>
|
||||
selected
|
||||
</Trans>
|
||||
}
|
||||
/>
|
||||
</ViewBody>
|
||||
<AllDocSidebarTabs />
|
||||
</DocExplorerContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const Component = () => {
|
||||
return <AllPage />;
|
||||
};
|
||||
@@ -0,0 +1,103 @@
|
||||
import { usePageHelper } from '@affine/core/blocksuite/block-suite-page-list/utils';
|
||||
import { ExplorerNavigation } from '@affine/core/components/explorer/header/navigation';
|
||||
import {
|
||||
AllPageListOperationsMenu,
|
||||
PageDisplayMenu,
|
||||
PageListNewPageButton,
|
||||
} from '@affine/core/components/page-list';
|
||||
import { Header } from '@affine/core/components/pure/header';
|
||||
import { WorkspaceDialogService } from '@affine/core/modules/dialogs';
|
||||
import { WorkbenchService } from '@affine/core/modules/workbench';
|
||||
import { WorkspaceService } from '@affine/core/modules/workspace';
|
||||
import { inferOpenMode } from '@affine/core/utils';
|
||||
import type { Filter } from '@affine/env/filter';
|
||||
import { track } from '@affine/track';
|
||||
import { PlusIcon } from '@blocksuite/icons/rc';
|
||||
import { useServices } from '@toeverything/infra';
|
||||
import clsx from 'clsx';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import * as styles from './all-page.css';
|
||||
|
||||
export const AllPageHeader = ({
|
||||
showCreateNew,
|
||||
filters,
|
||||
onChangeFilters,
|
||||
}: {
|
||||
showCreateNew: boolean;
|
||||
filters: Filter[];
|
||||
onChangeFilters: (filters: Filter[]) => void;
|
||||
}) => {
|
||||
const { workspaceService, workspaceDialogService, workbenchService } =
|
||||
useServices({
|
||||
WorkspaceService,
|
||||
WorkspaceDialogService,
|
||||
WorkbenchService,
|
||||
});
|
||||
const workbench = workbenchService.workbench;
|
||||
const workspace = workspaceService.workspace;
|
||||
const { createEdgeless, createPage } = usePageHelper(workspace.docCollection);
|
||||
|
||||
const handleOpenDocs = useCallback(
|
||||
(result: {
|
||||
docIds: string[];
|
||||
entryId?: string;
|
||||
isWorkspaceFile?: boolean;
|
||||
}) => {
|
||||
const { docIds, entryId, isWorkspaceFile } = result;
|
||||
// If the imported file is a workspace file, open the entry page.
|
||||
if (isWorkspaceFile && entryId) {
|
||||
workbench.openDoc(entryId);
|
||||
} else if (!docIds.length) {
|
||||
return;
|
||||
}
|
||||
// Open all the docs when there are multiple docs imported.
|
||||
if (docIds.length > 1) {
|
||||
workbench.openAll();
|
||||
} else {
|
||||
// Otherwise, open the only doc.
|
||||
workbench.openDoc(docIds[0]);
|
||||
}
|
||||
},
|
||||
[workbench]
|
||||
);
|
||||
|
||||
const onImportFile = useCallback(() => {
|
||||
track.$.header.importModal.open();
|
||||
workspaceDialogService.open('import', undefined, payload => {
|
||||
if (!payload) {
|
||||
return;
|
||||
}
|
||||
handleOpenDocs(payload);
|
||||
});
|
||||
}, [workspaceDialogService, handleOpenDocs]);
|
||||
|
||||
return (
|
||||
<Header
|
||||
left={<ExplorerNavigation active={'docs'} />}
|
||||
right={
|
||||
<>
|
||||
<PageListNewPageButton
|
||||
size="small"
|
||||
className={clsx(
|
||||
styles.headerCreateNewButton,
|
||||
!showCreateNew && styles.headerCreateNewButtonHidden
|
||||
)}
|
||||
onCreateEdgeless={e => createEdgeless({ at: inferOpenMode(e) })}
|
||||
onCreatePage={e => createPage('page', { at: inferOpenMode(e) })}
|
||||
onCreateDoc={e => createPage(undefined, { at: inferOpenMode(e) })}
|
||||
onImportFile={onImportFile}
|
||||
>
|
||||
<PlusIcon />
|
||||
</PageListNewPageButton>
|
||||
<AllPageListOperationsMenu
|
||||
filterList={filters}
|
||||
onChangeFilterList={onChangeFilters}
|
||||
propertiesMeta={workspace.docCollection.meta.properties}
|
||||
/>
|
||||
<PageDisplayMenu />
|
||||
</>
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
+1
-15
@@ -1,4 +1,3 @@
|
||||
import { cssVarV2 } from '@toeverything/theme/v2';
|
||||
import { style } from '@vanilla-extract/css';
|
||||
export const scrollContainer = style({
|
||||
flex: 1,
|
||||
@@ -7,6 +6,7 @@ export const scrollContainer = style({
|
||||
});
|
||||
export const headerCreateNewButton = style({
|
||||
transition: 'opacity 0.1s ease-in-out',
|
||||
marginRight: 16,
|
||||
});
|
||||
|
||||
export const headerCreateNewCollectionIconButton = style({
|
||||
@@ -28,17 +28,3 @@ export const body = style({
|
||||
height: '100%',
|
||||
width: '100%',
|
||||
});
|
||||
|
||||
export const scrollArea = style({
|
||||
height: 0,
|
||||
flex: 1,
|
||||
});
|
||||
|
||||
// group
|
||||
export const groupHeader = style({
|
||||
background: cssVarV2.layer.background.primary,
|
||||
});
|
||||
|
||||
export const docItem = style({
|
||||
transition: 'width 0.2s ease-in-out',
|
||||
});
|
||||
@@ -0,0 +1,96 @@
|
||||
import { useBlockSuiteDocMeta } from '@affine/core/components/hooks/use-block-suite-page-meta';
|
||||
import {
|
||||
PageListHeader,
|
||||
useFilteredPageMetas,
|
||||
VirtualizedPageList,
|
||||
} from '@affine/core/components/page-list';
|
||||
import { GlobalContextService } from '@affine/core/modules/global-context';
|
||||
import { IntegrationService } from '@affine/core/modules/integration';
|
||||
import { WorkspacePermissionService } from '@affine/core/modules/permissions';
|
||||
import { WorkspaceService } from '@affine/core/modules/workspace';
|
||||
import type { Filter } from '@affine/env/filter';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
import { useLiveData, useService } from '@toeverything/infra';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import {
|
||||
useIsActiveView,
|
||||
ViewBody,
|
||||
ViewHeader,
|
||||
ViewIcon,
|
||||
ViewTitle,
|
||||
} from '../../../../modules/workbench';
|
||||
import { AllDocSidebarTabs } from '../layouts/all-doc-sidebar-tabs';
|
||||
import { EmptyPageList } from '../page-list-empty';
|
||||
import * as styles from './all-page.css';
|
||||
import { FilterContainer } from './all-page-filter';
|
||||
import { AllPageHeader } from './all-page-header';
|
||||
|
||||
export const AllPage = () => {
|
||||
const currentWorkspace = useService(WorkspaceService).workspace;
|
||||
const globalContext = useService(GlobalContextService).globalContext;
|
||||
const permissionService = useService(WorkspacePermissionService);
|
||||
const integrationService = useService(IntegrationService);
|
||||
const pageMetas = useBlockSuiteDocMeta(currentWorkspace.docCollection);
|
||||
const [hideHeaderCreateNew, setHideHeaderCreateNew] = useState(true);
|
||||
const isAdmin = useLiveData(permissionService.permission.isAdmin$);
|
||||
const isOwner = useLiveData(permissionService.permission.isOwner$);
|
||||
const importing = useLiveData(integrationService.importing$);
|
||||
|
||||
const [filters, setFilters] = useState<Filter[]>([]);
|
||||
const filteredPageMetas = useFilteredPageMetas(pageMetas, {
|
||||
filters: filters,
|
||||
});
|
||||
|
||||
const isActiveView = useIsActiveView();
|
||||
|
||||
useEffect(() => {
|
||||
if (isActiveView) {
|
||||
globalContext.isAllDocs.set(true);
|
||||
|
||||
return () => {
|
||||
globalContext.isAllDocs.set(false);
|
||||
};
|
||||
}
|
||||
return;
|
||||
}, [globalContext, isActiveView]);
|
||||
|
||||
const t = useI18n();
|
||||
|
||||
if (importing) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<ViewTitle title={t['All pages']()} />
|
||||
<ViewIcon icon="allDocs" />
|
||||
<ViewHeader>
|
||||
<AllPageHeader
|
||||
showCreateNew={!hideHeaderCreateNew}
|
||||
filters={filters}
|
||||
onChangeFilters={setFilters}
|
||||
/>
|
||||
</ViewHeader>
|
||||
<ViewBody>
|
||||
<div className={styles.body}>
|
||||
<FilterContainer filters={filters} onChangeFilters={setFilters} />
|
||||
{filteredPageMetas.length > 0 ? (
|
||||
<VirtualizedPageList
|
||||
disableMultiDelete={!isAdmin && !isOwner}
|
||||
setHideHeaderCreateNewPage={setHideHeaderCreateNew}
|
||||
filters={filters}
|
||||
/>
|
||||
) : (
|
||||
<EmptyPageList type="all" heading={<PageListHeader />} />
|
||||
)}
|
||||
</div>
|
||||
</ViewBody>
|
||||
<AllDocSidebarTabs />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const Component = () => {
|
||||
return <AllPage />;
|
||||
};
|
||||
@@ -0,0 +1,35 @@
|
||||
import { cssVarV2 } from '@toeverything/theme/v2';
|
||||
import { style } from '@vanilla-extract/css';
|
||||
|
||||
export const header = style({
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
});
|
||||
|
||||
export const actions = style({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 16,
|
||||
});
|
||||
|
||||
export const viewToggle = style({
|
||||
backgroundColor: 'transparent',
|
||||
});
|
||||
export const viewToggleItem = style({
|
||||
padding: 0,
|
||||
fontSize: 16,
|
||||
width: 24,
|
||||
color: cssVarV2.icon.primary,
|
||||
selectors: {
|
||||
'&[data-state=checked]': {
|
||||
color: cssVarV2.icon.primary,
|
||||
},
|
||||
},
|
||||
});
|
||||
export const viewToggleIndicator = style({
|
||||
backgroundColor: cssVarV2.layer.background.hoverOverlay,
|
||||
boxShadow: 'none',
|
||||
});
|
||||
@@ -1,105 +1,75 @@
|
||||
import { usePageHelper } from '@affine/core/blocksuite/block-suite-page-list/utils';
|
||||
import {
|
||||
AllPageListOperationsMenu,
|
||||
PageDisplayMenu,
|
||||
PageListNewPageButton,
|
||||
} from '@affine/core/components/page-list';
|
||||
import { Header } from '@affine/core/components/pure/header';
|
||||
import { WorkspaceModeFilterTab } from '@affine/core/components/pure/workspace-mode-filter-tab';
|
||||
import { WorkspaceDialogService } from '@affine/core/modules/dialogs';
|
||||
import { WorkbenchService } from '@affine/core/modules/workbench';
|
||||
import { WorkspaceService } from '@affine/core/modules/workspace';
|
||||
import { inferOpenMode } from '@affine/core/utils';
|
||||
import type { Filter } from '@affine/env/filter';
|
||||
import { track } from '@affine/track';
|
||||
import { PlusIcon } from '@blocksuite/icons/rc';
|
||||
import { useServices } from '@toeverything/infra';
|
||||
import clsx from 'clsx';
|
||||
import { useCallback } from 'react';
|
||||
import { type MenuProps, RadioGroup, type RadioItem } from '@affine/component';
|
||||
import { DocExplorerContext } from '@affine/core/components/explorer/context';
|
||||
import { ExplorerDisplayMenuButton } from '@affine/core/components/explorer/display-menu';
|
||||
import { DocListViewIcon } from '@affine/core/components/explorer/docs-view/doc-list-item';
|
||||
import { ExplorerNavigation } from '@affine/core/components/explorer/header/navigation';
|
||||
import type { ExplorerPreference } from '@affine/core/components/explorer/types';
|
||||
import { type Dispatch, type SetStateAction, useContext } from 'react';
|
||||
|
||||
import * as styles from './all-page.css';
|
||||
import * as styles from './all-page-header.css';
|
||||
|
||||
export const AllPageHeader = ({
|
||||
showCreateNew,
|
||||
filters,
|
||||
onChangeFilters,
|
||||
}: {
|
||||
showCreateNew: boolean;
|
||||
filters: Filter[];
|
||||
onChangeFilters: (filters: Filter[]) => void;
|
||||
}) => {
|
||||
const { workspaceService, workspaceDialogService, workbenchService } =
|
||||
useServices({
|
||||
WorkspaceService,
|
||||
WorkspaceDialogService,
|
||||
WorkbenchService,
|
||||
});
|
||||
const workbench = workbenchService.workbench;
|
||||
const workspace = workspaceService.workspace;
|
||||
const { createEdgeless, createPage } = usePageHelper(workspace.docCollection);
|
||||
|
||||
const handleOpenDocs = useCallback(
|
||||
(result: {
|
||||
docIds: string[];
|
||||
entryId?: string;
|
||||
isWorkspaceFile?: boolean;
|
||||
}) => {
|
||||
const { docIds, entryId, isWorkspaceFile } = result;
|
||||
// If the imported file is a workspace file, open the entry page.
|
||||
if (isWorkspaceFile && entryId) {
|
||||
workbench.openDoc(entryId);
|
||||
} else if (!docIds.length) {
|
||||
return;
|
||||
}
|
||||
// Open all the docs when there are multiple docs imported.
|
||||
if (docIds.length > 1) {
|
||||
workbench.openAll();
|
||||
} else {
|
||||
// Otherwise, open the only doc.
|
||||
workbench.openDoc(docIds[0]);
|
||||
}
|
||||
},
|
||||
[workbench]
|
||||
);
|
||||
|
||||
const onImportFile = useCallback(() => {
|
||||
track.$.header.importModal.open();
|
||||
workspaceDialogService.open('import', undefined, payload => {
|
||||
if (!payload) {
|
||||
return;
|
||||
}
|
||||
handleOpenDocs(payload);
|
||||
});
|
||||
}, [workspaceDialogService, handleOpenDocs]);
|
||||
const views = [
|
||||
{
|
||||
label: <DocListViewIcon view="masonry" />,
|
||||
value: 'masonry',
|
||||
className: styles.viewToggleItem,
|
||||
},
|
||||
{
|
||||
label: <DocListViewIcon view="grid" />,
|
||||
value: 'grid',
|
||||
className: styles.viewToggleItem,
|
||||
},
|
||||
{
|
||||
label: <DocListViewIcon view="list" />,
|
||||
value: 'list',
|
||||
className: styles.viewToggleItem,
|
||||
},
|
||||
] satisfies RadioItem[];
|
||||
|
||||
const ViewToggle = () => {
|
||||
const { view, setView } = useContext(DocExplorerContext);
|
||||
return (
|
||||
<Header
|
||||
left={
|
||||
<AllPageListOperationsMenu
|
||||
filterList={filters}
|
||||
onChangeFilterList={onChangeFilters}
|
||||
propertiesMeta={workspace.docCollection.meta.properties}
|
||||
/>
|
||||
}
|
||||
right={
|
||||
<>
|
||||
<PageListNewPageButton
|
||||
size="small"
|
||||
className={clsx(
|
||||
styles.headerCreateNewButton,
|
||||
!showCreateNew && styles.headerCreateNewButtonHidden
|
||||
)}
|
||||
onCreateEdgeless={e => createEdgeless({ at: inferOpenMode(e) })}
|
||||
onCreatePage={e => createPage('page', { at: inferOpenMode(e) })}
|
||||
onCreateDoc={e => createPage(undefined, { at: inferOpenMode(e) })}
|
||||
onImportFile={onImportFile}
|
||||
>
|
||||
<PlusIcon />
|
||||
</PageListNewPageButton>
|
||||
<PageDisplayMenu />
|
||||
</>
|
||||
}
|
||||
center={<WorkspaceModeFilterTab activeFilter={'docs'} />}
|
||||
<RadioGroup
|
||||
itemHeight={24}
|
||||
gap={8}
|
||||
padding={0}
|
||||
items={views}
|
||||
value={view}
|
||||
onChange={setView}
|
||||
className={styles.viewToggle}
|
||||
borderRadius={4}
|
||||
indicatorClassName={styles.viewToggleIndicator}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const menuProps: Partial<MenuProps> = {
|
||||
contentOptions: {
|
||||
side: 'bottom',
|
||||
align: 'end',
|
||||
alignOffset: 0,
|
||||
sideOffset: 8,
|
||||
},
|
||||
};
|
||||
export const AllDocsHeader = ({
|
||||
explorerPreference,
|
||||
setExplorerPreference,
|
||||
}: {
|
||||
explorerPreference: ExplorerPreference;
|
||||
setExplorerPreference: Dispatch<SetStateAction<ExplorerPreference>>;
|
||||
}) => {
|
||||
return (
|
||||
<div className={styles.header}>
|
||||
<ExplorerNavigation active="docs" />
|
||||
|
||||
<div className={styles.actions}>
|
||||
<ViewToggle />
|
||||
<ExplorerDisplayMenuButton
|
||||
preference={explorerPreference}
|
||||
onChange={setExplorerPreference}
|
||||
menuProps={menuProps}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { cssVarV2 } from '@toeverything/theme/v2';
|
||||
import { style } from '@vanilla-extract/css';
|
||||
export const scrollContainer = style({
|
||||
flex: 1,
|
||||
@@ -26,4 +27,36 @@ export const body = style({
|
||||
flex: 1,
|
||||
height: '100%',
|
||||
width: '100%',
|
||||
containerName: 'docs-body',
|
||||
containerType: 'size',
|
||||
});
|
||||
|
||||
export const scrollArea = style({
|
||||
height: 0,
|
||||
flex: 1,
|
||||
});
|
||||
|
||||
// group
|
||||
export const groupHeader = style({
|
||||
background: cssVarV2.layer.background.primary,
|
||||
});
|
||||
|
||||
export const docItem = style({
|
||||
transition: 'width 0.2s ease-in-out',
|
||||
});
|
||||
|
||||
export const filterArea = style({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: 8,
|
||||
padding: '0 24px',
|
||||
paddingTop: '24px',
|
||||
'@container': {
|
||||
'docs-body (width <= 500px)': {
|
||||
padding: '0 20px',
|
||||
},
|
||||
'docs-body (width <= 393px)': {
|
||||
padding: '0 16px',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,96 +1,358 @@
|
||||
import { useBlockSuiteDocMeta } from '@affine/core/components/hooks/use-block-suite-page-meta';
|
||||
import { Masonry, type MasonryGroup, useConfirmModal } from '@affine/component';
|
||||
import {
|
||||
PageListHeader,
|
||||
useFilteredPageMetas,
|
||||
VirtualizedPageList,
|
||||
} from '@affine/core/components/page-list';
|
||||
import { GlobalContextService } from '@affine/core/modules/global-context';
|
||||
import { IntegrationService } from '@affine/core/modules/integration';
|
||||
import { WorkspacePermissionService } from '@affine/core/modules/permissions';
|
||||
import { WorkspaceService } from '@affine/core/modules/workspace';
|
||||
import type { Filter } from '@affine/env/filter';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
DocExplorerContext,
|
||||
type DocExplorerContextType,
|
||||
} from '@affine/core/components/explorer/context';
|
||||
import {
|
||||
DocListItem,
|
||||
type DocListItemView,
|
||||
} from '@affine/core/components/explorer/docs-view/doc-list-item';
|
||||
import type { ExplorerPreference } from '@affine/core/components/explorer/types';
|
||||
import { Filters } from '@affine/core/components/filter';
|
||||
import { ListFloatingToolbar } from '@affine/core/components/page-list/components/list-floating-toolbar';
|
||||
import { WorkspacePropertyTypes } from '@affine/core/components/workspace-property-types';
|
||||
import { CollectionRulesService } from '@affine/core/modules/collection-rules';
|
||||
import type { FilterParams } from '@affine/core/modules/collection-rules/types';
|
||||
import { DocsService } from '@affine/core/modules/doc';
|
||||
import { FeatureFlagService } from '@affine/core/modules/feature-flag';
|
||||
import { WorkspacePropertyService } from '@affine/core/modules/workspace-property';
|
||||
import { Trans, useI18n } from '@affine/i18n';
|
||||
import { useLiveData, useService } from '@toeverything/infra';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { cssVarV2 } from '@toeverything/theme/v2';
|
||||
import {
|
||||
memo,
|
||||
useCallback,
|
||||
useContext,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useState,
|
||||
} from 'react';
|
||||
|
||||
import {
|
||||
useIsActiveView,
|
||||
ViewBody,
|
||||
ViewHeader,
|
||||
ViewIcon,
|
||||
ViewTitle,
|
||||
} from '../../../../modules/workbench';
|
||||
import { AllPage as AllPageOld } from '../all-page-old/all-page';
|
||||
import { AllDocSidebarTabs } from '../layouts/all-doc-sidebar-tabs';
|
||||
import { EmptyPageList } from '../page-list-empty';
|
||||
import * as styles from './all-page.css';
|
||||
import { FilterContainer } from './all-page-filter';
|
||||
import { AllPageHeader } from './all-page-header';
|
||||
import { AllDocsHeader } from './all-page-header';
|
||||
import { MigrationAllDocsDataNotification } from './migration-data';
|
||||
|
||||
export const AllPage = () => {
|
||||
const currentWorkspace = useService(WorkspaceService).workspace;
|
||||
const globalContext = useService(GlobalContextService).globalContext;
|
||||
const permissionService = useService(WorkspacePermissionService);
|
||||
const integrationService = useService(IntegrationService);
|
||||
const pageMetas = useBlockSuiteDocMeta(currentWorkspace.docCollection);
|
||||
const [hideHeaderCreateNew, setHideHeaderCreateNew] = useState(true);
|
||||
const isAdmin = useLiveData(permissionService.permission.isAdmin$);
|
||||
const isOwner = useLiveData(permissionService.permission.isOwner$);
|
||||
const importing = useLiveData(integrationService.importing$);
|
||||
const GroupHeader = memo(function GroupHeader({
|
||||
groupId,
|
||||
collapsed,
|
||||
itemCount,
|
||||
}: {
|
||||
groupId: string;
|
||||
collapsed?: boolean;
|
||||
itemCount: number;
|
||||
}) {
|
||||
const { groupBy } = useContext(DocExplorerContext);
|
||||
const propertyService = useService(WorkspacePropertyService);
|
||||
const allProperties = useLiveData(propertyService.sortedProperties$);
|
||||
|
||||
const [filters, setFilters] = useState<Filter[]>([]);
|
||||
const filteredPageMetas = useFilteredPageMetas(pageMetas, {
|
||||
filters: filters,
|
||||
});
|
||||
const groupType = groupBy?.type;
|
||||
const groupKey = groupBy?.key;
|
||||
|
||||
const isActiveView = useIsActiveView();
|
||||
const header = useMemo(() => {
|
||||
if (groupType === 'property') {
|
||||
const property = allProperties.find(p => p.id === groupKey);
|
||||
if (!property) return null;
|
||||
|
||||
useEffect(() => {
|
||||
if (isActiveView) {
|
||||
globalContext.isAllDocs.set(true);
|
||||
|
||||
return () => {
|
||||
globalContext.isAllDocs.set(false);
|
||||
};
|
||||
const config = WorkspacePropertyTypes[property.type];
|
||||
if (!config?.groupHeader) return null;
|
||||
return (
|
||||
<config.groupHeader
|
||||
groupId={groupId}
|
||||
docCount={itemCount}
|
||||
collapsed={!!collapsed}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
return '// TODO: ' + groupType;
|
||||
}
|
||||
return;
|
||||
}, [globalContext, isActiveView]);
|
||||
}, [allProperties, collapsed, groupId, groupKey, groupType, itemCount]);
|
||||
|
||||
const t = useI18n();
|
||||
|
||||
if (importing) {
|
||||
if (!groupType) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return header;
|
||||
});
|
||||
|
||||
const calcCardHeightById = (id: string) => {
|
||||
const max = 5;
|
||||
const min = 1;
|
||||
const code = id.charCodeAt(0);
|
||||
const value = Math.floor((code % (max - min)) + min);
|
||||
return 250 + value * 10;
|
||||
};
|
||||
|
||||
const DocListItemComponent = memo(function DocListItemComponent({
|
||||
itemId,
|
||||
groupId,
|
||||
}: {
|
||||
groupId: string;
|
||||
itemId: string;
|
||||
}) {
|
||||
return <DocListItem docId={itemId} groupId={groupId} />;
|
||||
});
|
||||
|
||||
export const AllPage = () => {
|
||||
const t = useI18n();
|
||||
const docsService = useService(DocsService);
|
||||
const [view, setView] = useState<DocListItemView>('masonry');
|
||||
const [collapsedGroups, setCollapsedGroups] = useState<string[]>([]);
|
||||
const [selectMode, setSelectMode] = useState(false);
|
||||
const [selectedDocIds, setSelectedDocIds] = useState<string[]>([]);
|
||||
const [prevCheckAnchorId, setPrevCheckAnchorId] = useState<string | null>(
|
||||
null
|
||||
);
|
||||
|
||||
const [explorerPreference, setExplorerPreference] =
|
||||
useState<ExplorerPreference>({
|
||||
filters: [
|
||||
{
|
||||
type: 'system',
|
||||
key: 'trash',
|
||||
value: 'false',
|
||||
method: 'is',
|
||||
},
|
||||
],
|
||||
displayProperties: [],
|
||||
showDocIcon: true,
|
||||
showDocPreview: true,
|
||||
});
|
||||
|
||||
const [groups, setGroups] = useState<any>([]);
|
||||
|
||||
const { openConfirmModal } = useConfirmModal();
|
||||
|
||||
const masonryItems = useMemo(() => {
|
||||
const items = groups.map((group: any) => {
|
||||
return {
|
||||
id: group.key,
|
||||
Component: groups.length > 1 ? GroupHeader : undefined,
|
||||
height: groups.length > 1 ? 24 : 0,
|
||||
className: styles.groupHeader,
|
||||
items: group.items.map((docId: string) => {
|
||||
return {
|
||||
id: docId,
|
||||
Component: DocListItemComponent,
|
||||
height:
|
||||
view === 'list'
|
||||
? 42
|
||||
: view === 'grid'
|
||||
? 280
|
||||
: calcCardHeightById(docId),
|
||||
'data-view': view,
|
||||
className: styles.docItem,
|
||||
};
|
||||
}),
|
||||
} satisfies MasonryGroup;
|
||||
});
|
||||
return items;
|
||||
}, [groups, view]);
|
||||
|
||||
const collectionRulesService = useService(CollectionRulesService);
|
||||
useEffect(() => {
|
||||
const subscription = collectionRulesService
|
||||
.watch(
|
||||
explorerPreference.filters ?? [],
|
||||
explorerPreference.groupBy,
|
||||
explorerPreference.orderBy
|
||||
)
|
||||
.subscribe({
|
||||
next: result => {
|
||||
setGroups(result.groups);
|
||||
},
|
||||
error: error => {
|
||||
console.error(error);
|
||||
},
|
||||
});
|
||||
return () => {
|
||||
subscription.unsubscribe();
|
||||
};
|
||||
}, [
|
||||
collectionRulesService,
|
||||
explorerPreference.filters,
|
||||
explorerPreference.groupBy,
|
||||
explorerPreference.orderBy,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
const onKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Escape') {
|
||||
setSelectMode(false);
|
||||
setSelectedDocIds([]);
|
||||
setPrevCheckAnchorId(null);
|
||||
}
|
||||
};
|
||||
document.addEventListener('keydown', onKeyDown);
|
||||
return () => {
|
||||
document.removeEventListener('keydown', onKeyDown);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const handleFilterChange = useCallback((filters: FilterParams[]) => {
|
||||
setExplorerPreference(prev => ({
|
||||
...prev,
|
||||
filters,
|
||||
}));
|
||||
}, []);
|
||||
|
||||
const toggleGroupCollapse = useCallback((groupId: string) => {
|
||||
setCollapsedGroups(prev => {
|
||||
return prev.includes(groupId)
|
||||
? prev.filter(id => id !== groupId)
|
||||
: [...prev, groupId];
|
||||
});
|
||||
}, []);
|
||||
const toggleDocSelect = useCallback((docId: string) => {
|
||||
setSelectMode(true);
|
||||
setSelectedDocIds(prev => {
|
||||
return prev.includes(docId)
|
||||
? prev.filter(id => id !== docId)
|
||||
: [...prev, docId];
|
||||
});
|
||||
}, []);
|
||||
const onSelect = useCallback(
|
||||
(...args: Parameters<typeof setSelectedDocIds>) => {
|
||||
setSelectMode(true);
|
||||
setSelectedDocIds(...args);
|
||||
},
|
||||
[]
|
||||
);
|
||||
const handleCloseFloatingToolbar = useCallback(() => {
|
||||
setSelectMode(false);
|
||||
setSelectedDocIds([]);
|
||||
}, []);
|
||||
const handleMultiDelete = useCallback(() => {
|
||||
if (selectedDocIds.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
openConfirmModal({
|
||||
title: t['com.affine.moveToTrash.confirmModal.title.multiple']({
|
||||
number: selectedDocIds.length.toString(),
|
||||
}),
|
||||
description: t[
|
||||
'com.affine.moveToTrash.confirmModal.description.multiple'
|
||||
]({
|
||||
number: selectedDocIds.length.toString(),
|
||||
}),
|
||||
cancelText: t['com.affine.confirmModal.button.cancel'](),
|
||||
confirmText: t.Delete(),
|
||||
confirmButtonOptions: {
|
||||
variant: 'error',
|
||||
},
|
||||
onConfirm: () => {
|
||||
for (const docId of selectedDocIds) {
|
||||
const doc = docsService.list.doc$(docId).value;
|
||||
doc?.moveToTrash();
|
||||
}
|
||||
},
|
||||
});
|
||||
}, [docsService.list, openConfirmModal, selectedDocIds, t]);
|
||||
|
||||
const explorerContextValue = useMemo(
|
||||
() =>
|
||||
({
|
||||
...explorerPreference,
|
||||
view,
|
||||
setView,
|
||||
groups,
|
||||
collapsed: collapsedGroups,
|
||||
selectedDocIds,
|
||||
selectMode,
|
||||
prevCheckAnchorId,
|
||||
setPrevCheckAnchorId,
|
||||
onToggleCollapse: toggleGroupCollapse,
|
||||
onToggleSelect: toggleDocSelect,
|
||||
onSelect,
|
||||
}) satisfies DocExplorerContextType,
|
||||
[
|
||||
collapsedGroups,
|
||||
explorerPreference,
|
||||
groups,
|
||||
onSelect,
|
||||
prevCheckAnchorId,
|
||||
selectMode,
|
||||
selectedDocIds,
|
||||
toggleDocSelect,
|
||||
toggleGroupCollapse,
|
||||
view,
|
||||
]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<DocExplorerContext.Provider value={explorerContextValue}>
|
||||
<ViewTitle title={t['All pages']()} />
|
||||
<ViewIcon icon="allDocs" />
|
||||
<ViewHeader>
|
||||
<AllPageHeader
|
||||
showCreateNew={!hideHeaderCreateNew}
|
||||
filters={filters}
|
||||
onChangeFilters={setFilters}
|
||||
<AllDocsHeader
|
||||
explorerPreference={explorerPreference}
|
||||
setExplorerPreference={setExplorerPreference}
|
||||
/>
|
||||
</ViewHeader>
|
||||
<ViewBody>
|
||||
<div className={styles.body}>
|
||||
<FilterContainer filters={filters} onChangeFilters={setFilters} />
|
||||
{filteredPageMetas.length > 0 ? (
|
||||
<VirtualizedPageList
|
||||
disableMultiDelete={!isAdmin && !isOwner}
|
||||
setHideHeaderCreateNewPage={setHideHeaderCreateNew}
|
||||
filters={filters}
|
||||
<div className={styles.filterArea}>
|
||||
<MigrationAllDocsDataNotification />
|
||||
<Filters
|
||||
filters={explorerPreference.filters ?? []}
|
||||
onChange={handleFilterChange}
|
||||
/>
|
||||
) : (
|
||||
<EmptyPageList type="all" heading={<PageListHeader />} />
|
||||
)}
|
||||
</div>
|
||||
<div className={styles.scrollArea}>
|
||||
<Masonry
|
||||
items={masonryItems}
|
||||
gapY={12}
|
||||
gapX={12}
|
||||
groupsGap={12}
|
||||
groupHeaderGapWithItems={12}
|
||||
columns={view === 'list' ? 1 : undefined}
|
||||
itemWidthMin={220}
|
||||
preloadHeight={100}
|
||||
itemWidth={'stretch'}
|
||||
virtualScroll
|
||||
collapsedGroups={collapsedGroups}
|
||||
paddingX={useCallback(
|
||||
(w: number) => (w > 500 ? 24 : w > 393 ? 20 : 16),
|
||||
[]
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<ListFloatingToolbar
|
||||
open={selectMode}
|
||||
onDelete={handleMultiDelete}
|
||||
onClose={handleCloseFloatingToolbar}
|
||||
content={
|
||||
<Trans
|
||||
i18nKey="com.affine.page.toolbar.selected"
|
||||
count={selectedDocIds.length}
|
||||
>
|
||||
<div style={{ color: cssVarV2.text.secondary }}>
|
||||
{{ count: selectedDocIds.length } as any}
|
||||
</div>
|
||||
selected
|
||||
</Trans>
|
||||
}
|
||||
/>
|
||||
</ViewBody>
|
||||
<AllDocSidebarTabs />
|
||||
</>
|
||||
</DocExplorerContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const Component = () => {
|
||||
return <AllPage />;
|
||||
const featureFlagService = useService(FeatureFlagService);
|
||||
const enableNewAllDocsPage = useLiveData(
|
||||
featureFlagService.flags.enable_new_all_docs_page.$
|
||||
);
|
||||
|
||||
return enableNewAllDocsPage ? <AllPage /> : <AllPageOld />;
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { ExplorerNavigation } from '@affine/core/components/explorer/header/navigation';
|
||||
import { Header } from '@affine/core/components/pure/header';
|
||||
import { WorkspaceModeFilterTab } from '@affine/core/components/pure/workspace-mode-filter-tab';
|
||||
|
||||
export const AllTagHeader = () => {
|
||||
return <Header center={<WorkspaceModeFilterTab activeFilter={'tags'} />} />;
|
||||
return <Header left={<ExplorerNavigation active={'tags'} />} />;
|
||||
};
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { IconButton } from '@affine/component';
|
||||
import { ExplorerNavigation } from '@affine/core/components/explorer/header/navigation';
|
||||
import { PageDisplayMenu } from '@affine/core/components/page-list';
|
||||
import { Header } from '@affine/core/components/pure/header';
|
||||
import { WorkspaceModeFilterTab } from '@affine/core/components/pure/workspace-mode-filter-tab';
|
||||
import { PlusIcon } from '@blocksuite/icons/rc';
|
||||
import clsx from 'clsx';
|
||||
|
||||
@@ -31,7 +31,7 @@ export const CollectionDetailHeader = ({
|
||||
<PageDisplayMenu />
|
||||
</>
|
||||
}
|
||||
center={<WorkspaceModeFilterTab activeFilter={'collections'} />}
|
||||
left={<ExplorerNavigation active="collections" />}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { ExplorerNavigation } from '@affine/core/components/explorer/header/navigation';
|
||||
import { PageDisplayMenu } from '@affine/core/components/page-list';
|
||||
import { Header } from '@affine/core/components/pure/header';
|
||||
import { WorkspaceModeFilterTab } from '@affine/core/components/pure/workspace-mode-filter-tab';
|
||||
|
||||
export const TagDetailHeader = () => {
|
||||
return (
|
||||
<Header
|
||||
center={<WorkspaceModeFilterTab activeFilter={'tags'} />}
|
||||
left={<ExplorerNavigation active={'tags'} />}
|
||||
right={<PageDisplayMenu />}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -6,8 +6,8 @@ export const workbenchRoutes = [
|
||||
lazy: () => import('./pages/workspace/all-page/all-page'),
|
||||
},
|
||||
{
|
||||
path: '/all-new',
|
||||
lazy: () => import('./pages/workspace/all-page-new/all-page'),
|
||||
path: '/all-old',
|
||||
lazy: () => import('./pages/workspace/all-page-old/all-page'),
|
||||
},
|
||||
{
|
||||
path: '/collection',
|
||||
|
||||
@@ -283,6 +283,13 @@ 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,
|
||||
},
|
||||
} satisfies { [key in string]: FlagInfo };
|
||||
|
||||
// oxlint-disable-next-line no-redeclare
|
||||
|
||||
Reference in New Issue
Block a user