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 { createContext, type Dispatch, type SetStateAction } from 'react';
|
||||||
|
|
||||||
|
import type { DocListItemView } from './docs-view/doc-list-item';
|
||||||
import type { ExplorerPreference } from './types';
|
import type { ExplorerPreference } from './types';
|
||||||
|
|
||||||
export type DocExplorerContextType = ExplorerPreference & {
|
export type DocExplorerContextType = ExplorerPreference & {
|
||||||
view: 'list' | 'grid' | 'masonry';
|
view: DocListItemView;
|
||||||
|
setView: Dispatch<SetStateAction<DocListItemView>>;
|
||||||
groups: Array<{ key: string; items: string[] }>;
|
groups: Array<{ key: string; items: string[] }>;
|
||||||
collapsed: string[];
|
collapsed: string[];
|
||||||
selectMode?: boolean;
|
selectMode?: boolean;
|
||||||
@@ -17,6 +19,7 @@ export type DocExplorerContextType = ExplorerPreference & {
|
|||||||
|
|
||||||
export const DocExplorerContext = createContext<DocExplorerContextType>({
|
export const DocExplorerContext = createContext<DocExplorerContextType>({
|
||||||
view: 'list',
|
view: 'list',
|
||||||
|
setView: () => {},
|
||||||
groups: [],
|
groups: [],
|
||||||
collapsed: [],
|
collapsed: [],
|
||||||
selectedDocIds: [],
|
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 {
|
import type {
|
||||||
GroupByParams,
|
GroupByParams,
|
||||||
OrderByParams,
|
OrderByParams,
|
||||||
@@ -92,12 +98,14 @@ export const ExplorerDisplayMenuButton = ({
|
|||||||
style,
|
style,
|
||||||
className,
|
className,
|
||||||
preference,
|
preference,
|
||||||
|
menuProps,
|
||||||
onChange,
|
onChange,
|
||||||
}: {
|
}: {
|
||||||
style?: React.CSSProperties;
|
style?: React.CSSProperties;
|
||||||
className?: string;
|
className?: string;
|
||||||
preference: ExplorerPreference;
|
preference: ExplorerPreference;
|
||||||
onChange?: (preference: ExplorerPreference) => void;
|
onChange?: (preference: ExplorerPreference) => void;
|
||||||
|
menuProps?: Omit<MenuProps, 'items' | 'children'>;
|
||||||
}) => {
|
}) => {
|
||||||
const t = useI18n();
|
const t = useI18n();
|
||||||
return (
|
return (
|
||||||
@@ -105,6 +113,7 @@ export const ExplorerDisplayMenuButton = ({
|
|||||||
items={
|
items={
|
||||||
<ExplorerDisplayMenu preference={preference} onChange={onChange} />
|
<ExplorerDisplayMenu preference={preference} onChange={onChange} />
|
||||||
}
|
}
|
||||||
|
{...menuProps}
|
||||||
>
|
>
|
||||||
<Button
|
<Button
|
||||||
className={className}
|
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 { IconButton } from '@affine/component';
|
||||||
|
import { ExplorerNavigation } from '@affine/core/components/explorer/header/navigation';
|
||||||
import { Header } from '@affine/core/components/pure/header';
|
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 { PlusIcon } from '@blocksuite/icons/rc';
|
||||||
import clsx from 'clsx';
|
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,4 +1,3 @@
|
|||||||
import { cssVarV2 } from '@toeverything/theme/v2';
|
|
||||||
import { style } from '@vanilla-extract/css';
|
import { style } from '@vanilla-extract/css';
|
||||||
export const scrollContainer = style({
|
export const scrollContainer = style({
|
||||||
flex: 1,
|
flex: 1,
|
||||||
@@ -7,6 +6,7 @@ export const scrollContainer = style({
|
|||||||
});
|
});
|
||||||
export const headerCreateNewButton = style({
|
export const headerCreateNewButton = style({
|
||||||
transition: 'opacity 0.1s ease-in-out',
|
transition: 'opacity 0.1s ease-in-out',
|
||||||
|
marginRight: 16,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const headerCreateNewCollectionIconButton = style({
|
export const headerCreateNewCollectionIconButton = style({
|
||||||
@@ -28,17 +28,3 @@ export const body = style({
|
|||||||
height: '100%',
|
height: '100%',
|
||||||
width: '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 { type MenuProps, RadioGroup, type RadioItem } from '@affine/component';
|
||||||
import {
|
import { DocExplorerContext } from '@affine/core/components/explorer/context';
|
||||||
AllPageListOperationsMenu,
|
import { ExplorerDisplayMenuButton } from '@affine/core/components/explorer/display-menu';
|
||||||
PageDisplayMenu,
|
import { DocListViewIcon } from '@affine/core/components/explorer/docs-view/doc-list-item';
|
||||||
PageListNewPageButton,
|
import { ExplorerNavigation } from '@affine/core/components/explorer/header/navigation';
|
||||||
} from '@affine/core/components/page-list';
|
import type { ExplorerPreference } from '@affine/core/components/explorer/types';
|
||||||
import { Header } from '@affine/core/components/pure/header';
|
import { type Dispatch, type SetStateAction, useContext } from 'react';
|
||||||
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 * as styles from './all-page.css';
|
import * as styles from './all-page-header.css';
|
||||||
|
|
||||||
export const AllPageHeader = ({
|
const views = [
|
||||||
showCreateNew,
|
{
|
||||||
filters,
|
label: <DocListViewIcon view="masonry" />,
|
||||||
onChangeFilters,
|
value: 'masonry',
|
||||||
}: {
|
className: styles.viewToggleItem,
|
||||||
showCreateNew: boolean;
|
},
|
||||||
filters: Filter[];
|
{
|
||||||
onChangeFilters: (filters: Filter[]) => void;
|
label: <DocListViewIcon view="grid" />,
|
||||||
}) => {
|
value: 'grid',
|
||||||
const { workspaceService, workspaceDialogService, workbenchService } =
|
className: styles.viewToggleItem,
|
||||||
useServices({
|
},
|
||||||
WorkspaceService,
|
{
|
||||||
WorkspaceDialogService,
|
label: <DocListViewIcon view="list" />,
|
||||||
WorkbenchService,
|
value: 'list',
|
||||||
});
|
className: styles.viewToggleItem,
|
||||||
const workbench = workbenchService.workbench;
|
},
|
||||||
const workspace = workspaceService.workspace;
|
] satisfies RadioItem[];
|
||||||
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 ViewToggle = () => {
|
||||||
|
const { view, setView } = useContext(DocExplorerContext);
|
||||||
return (
|
return (
|
||||||
<Header
|
<RadioGroup
|
||||||
left={
|
itemHeight={24}
|
||||||
<AllPageListOperationsMenu
|
gap={8}
|
||||||
filterList={filters}
|
padding={0}
|
||||||
onChangeFilterList={onChangeFilters}
|
items={views}
|
||||||
propertiesMeta={workspace.docCollection.meta.properties}
|
value={view}
|
||||||
/>
|
onChange={setView}
|
||||||
}
|
className={styles.viewToggle}
|
||||||
right={
|
borderRadius={4}
|
||||||
<>
|
indicatorClassName={styles.viewToggleIndicator}
|
||||||
<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'} />}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
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';
|
import { style } from '@vanilla-extract/css';
|
||||||
export const scrollContainer = style({
|
export const scrollContainer = style({
|
||||||
flex: 1,
|
flex: 1,
|
||||||
@@ -26,4 +27,36 @@ export const body = style({
|
|||||||
flex: 1,
|
flex: 1,
|
||||||
height: '100%',
|
height: '100%',
|
||||||
width: '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 {
|
import {
|
||||||
PageListHeader,
|
DocExplorerContext,
|
||||||
useFilteredPageMetas,
|
type DocExplorerContextType,
|
||||||
VirtualizedPageList,
|
} from '@affine/core/components/explorer/context';
|
||||||
} from '@affine/core/components/page-list';
|
import {
|
||||||
import { GlobalContextService } from '@affine/core/modules/global-context';
|
DocListItem,
|
||||||
import { IntegrationService } from '@affine/core/modules/integration';
|
type DocListItemView,
|
||||||
import { WorkspacePermissionService } from '@affine/core/modules/permissions';
|
} from '@affine/core/components/explorer/docs-view/doc-list-item';
|
||||||
import { WorkspaceService } from '@affine/core/modules/workspace';
|
import type { ExplorerPreference } from '@affine/core/components/explorer/types';
|
||||||
import type { Filter } from '@affine/env/filter';
|
import { Filters } from '@affine/core/components/filter';
|
||||||
import { useI18n } from '@affine/i18n';
|
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 { 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 {
|
import {
|
||||||
useIsActiveView,
|
|
||||||
ViewBody,
|
ViewBody,
|
||||||
ViewHeader,
|
ViewHeader,
|
||||||
ViewIcon,
|
ViewIcon,
|
||||||
ViewTitle,
|
ViewTitle,
|
||||||
} from '../../../../modules/workbench';
|
} from '../../../../modules/workbench';
|
||||||
|
import { AllPage as AllPageOld } from '../all-page-old/all-page';
|
||||||
import { AllDocSidebarTabs } from '../layouts/all-doc-sidebar-tabs';
|
import { AllDocSidebarTabs } from '../layouts/all-doc-sidebar-tabs';
|
||||||
import { EmptyPageList } from '../page-list-empty';
|
|
||||||
import * as styles from './all-page.css';
|
import * as styles from './all-page.css';
|
||||||
import { FilterContainer } from './all-page-filter';
|
import { AllDocsHeader } from './all-page-header';
|
||||||
import { AllPageHeader } from './all-page-header';
|
import { MigrationAllDocsDataNotification } from './migration-data';
|
||||||
|
|
||||||
export const AllPage = () => {
|
const GroupHeader = memo(function GroupHeader({
|
||||||
const currentWorkspace = useService(WorkspaceService).workspace;
|
groupId,
|
||||||
const globalContext = useService(GlobalContextService).globalContext;
|
collapsed,
|
||||||
const permissionService = useService(WorkspacePermissionService);
|
itemCount,
|
||||||
const integrationService = useService(IntegrationService);
|
}: {
|
||||||
const pageMetas = useBlockSuiteDocMeta(currentWorkspace.docCollection);
|
groupId: string;
|
||||||
const [hideHeaderCreateNew, setHideHeaderCreateNew] = useState(true);
|
collapsed?: boolean;
|
||||||
const isAdmin = useLiveData(permissionService.permission.isAdmin$);
|
itemCount: number;
|
||||||
const isOwner = useLiveData(permissionService.permission.isOwner$);
|
}) {
|
||||||
const importing = useLiveData(integrationService.importing$);
|
const { groupBy } = useContext(DocExplorerContext);
|
||||||
|
const propertyService = useService(WorkspacePropertyService);
|
||||||
|
const allProperties = useLiveData(propertyService.sortedProperties$);
|
||||||
|
|
||||||
const [filters, setFilters] = useState<Filter[]>([]);
|
const groupType = groupBy?.type;
|
||||||
const filteredPageMetas = useFilteredPageMetas(pageMetas, {
|
const groupKey = groupBy?.key;
|
||||||
filters: filters,
|
|
||||||
});
|
|
||||||
|
|
||||||
const isActiveView = useIsActiveView();
|
const header = useMemo(() => {
|
||||||
|
if (groupType === 'property') {
|
||||||
|
const property = allProperties.find(p => p.id === groupKey);
|
||||||
|
if (!property) return null;
|
||||||
|
|
||||||
useEffect(() => {
|
const config = WorkspacePropertyTypes[property.type];
|
||||||
if (isActiveView) {
|
if (!config?.groupHeader) return null;
|
||||||
globalContext.isAllDocs.set(true);
|
return (
|
||||||
|
<config.groupHeader
|
||||||
return () => {
|
groupId={groupId}
|
||||||
globalContext.isAllDocs.set(false);
|
docCount={itemCount}
|
||||||
};
|
collapsed={!!collapsed}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return '// TODO: ' + groupType;
|
||||||
}
|
}
|
||||||
return;
|
}, [allProperties, collapsed, groupId, groupKey, groupType, itemCount]);
|
||||||
}, [globalContext, isActiveView]);
|
|
||||||
|
|
||||||
const t = useI18n();
|
if (!groupType) {
|
||||||
|
|
||||||
if (importing) {
|
|
||||||
return null;
|
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 (
|
return (
|
||||||
<>
|
<DocExplorerContext.Provider value={explorerContextValue}>
|
||||||
<ViewTitle title={t['All pages']()} />
|
<ViewTitle title={t['All pages']()} />
|
||||||
<ViewIcon icon="allDocs" />
|
<ViewIcon icon="allDocs" />
|
||||||
<ViewHeader>
|
<ViewHeader>
|
||||||
<AllPageHeader
|
<AllDocsHeader
|
||||||
showCreateNew={!hideHeaderCreateNew}
|
explorerPreference={explorerPreference}
|
||||||
filters={filters}
|
setExplorerPreference={setExplorerPreference}
|
||||||
onChangeFilters={setFilters}
|
|
||||||
/>
|
/>
|
||||||
</ViewHeader>
|
</ViewHeader>
|
||||||
<ViewBody>
|
<ViewBody>
|
||||||
<div className={styles.body}>
|
<div className={styles.body}>
|
||||||
<FilterContainer filters={filters} onChangeFilters={setFilters} />
|
<div className={styles.filterArea}>
|
||||||
{filteredPageMetas.length > 0 ? (
|
<MigrationAllDocsDataNotification />
|
||||||
<VirtualizedPageList
|
<Filters
|
||||||
disableMultiDelete={!isAdmin && !isOwner}
|
filters={explorerPreference.filters ?? []}
|
||||||
setHideHeaderCreateNewPage={setHideHeaderCreateNew}
|
onChange={handleFilterChange}
|
||||||
filters={filters}
|
|
||||||
/>
|
/>
|
||||||
) : (
|
</div>
|
||||||
<EmptyPageList type="all" heading={<PageListHeader />} />
|
<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>
|
</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>
|
</ViewBody>
|
||||||
<AllDocSidebarTabs />
|
<AllDocSidebarTabs />
|
||||||
</>
|
</DocExplorerContext.Provider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Component = () => {
|
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 { Header } from '@affine/core/components/pure/header';
|
||||||
import { WorkspaceModeFilterTab } from '@affine/core/components/pure/workspace-mode-filter-tab';
|
|
||||||
|
|
||||||
export const AllTagHeader = () => {
|
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 { IconButton } from '@affine/component';
|
||||||
|
import { ExplorerNavigation } from '@affine/core/components/explorer/header/navigation';
|
||||||
import { PageDisplayMenu } from '@affine/core/components/page-list';
|
import { PageDisplayMenu } from '@affine/core/components/page-list';
|
||||||
import { Header } from '@affine/core/components/pure/header';
|
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 { PlusIcon } from '@blocksuite/icons/rc';
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
|
|
||||||
@@ -31,7 +31,7 @@ export const CollectionDetailHeader = ({
|
|||||||
<PageDisplayMenu />
|
<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 { PageDisplayMenu } from '@affine/core/components/page-list';
|
||||||
import { Header } from '@affine/core/components/pure/header';
|
import { Header } from '@affine/core/components/pure/header';
|
||||||
import { WorkspaceModeFilterTab } from '@affine/core/components/pure/workspace-mode-filter-tab';
|
|
||||||
|
|
||||||
export const TagDetailHeader = () => {
|
export const TagDetailHeader = () => {
|
||||||
return (
|
return (
|
||||||
<Header
|
<Header
|
||||||
center={<WorkspaceModeFilterTab activeFilter={'tags'} />}
|
left={<ExplorerNavigation active={'tags'} />}
|
||||||
right={<PageDisplayMenu />}
|
right={<PageDisplayMenu />}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ export const workbenchRoutes = [
|
|||||||
lazy: () => import('./pages/workspace/all-page/all-page'),
|
lazy: () => import('./pages/workspace/all-page/all-page'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/all-new',
|
path: '/all-old',
|
||||||
lazy: () => import('./pages/workspace/all-page-new/all-page'),
|
lazy: () => import('./pages/workspace/all-page-old/all-page'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/collection',
|
path: '/collection',
|
||||||
|
|||||||
@@ -283,6 +283,13 @@ export const AFFINE_FLAGS = {
|
|||||||
configurable: false,
|
configurable: false,
|
||||||
defaultState: isCanaryBuild,
|
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 };
|
} satisfies { [key in string]: FlagInfo };
|
||||||
|
|
||||||
// oxlint-disable-next-line no-redeclare
|
// oxlint-disable-next-line no-redeclare
|
||||||
|
|||||||
Reference in New Issue
Block a user