mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-04 00:28:33 +00:00
feat(mobile): new docs list for mobile (#12329)
close AF-2514 <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **New Features** - Enhanced document explorer on mobile with live updates, responsive masonry layout, and improved empty state handling for all documents, collections, and tags. - Added customization for card height and masonry item width in document explorer views. - Extended layout components to support additional flexbox styling options for improved layout flexibility. - **Bug Fixes** - Improved flexibility in layout components by supporting additional flexbox styling options. - **Refactor** - Replaced older static document list and menu components with a unified, context-driven explorer for a more dynamic and interactive experience. - Removed obsolete CSS and component files related to the previous document list and menu implementations. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
@@ -16,6 +16,8 @@ export type WrapperProps = {
|
||||
marginLeft?: CSSProperties['marginLeft'];
|
||||
marginRight?: CSSProperties['marginRight'];
|
||||
marginBottom?: CSSProperties['marginBottom'];
|
||||
flexGrow?: CSSProperties['flexGrow'];
|
||||
flexShrink?: CSSProperties['flexShrink'];
|
||||
};
|
||||
|
||||
export type FlexWrapperProps = {
|
||||
@@ -64,6 +66,8 @@ export const Wrapper = styled('div', {
|
||||
marginLeft,
|
||||
marginRight,
|
||||
marginBottom,
|
||||
flexGrow,
|
||||
flexShrink,
|
||||
}) => {
|
||||
return {
|
||||
display,
|
||||
@@ -79,6 +83,8 @@ export const Wrapper = styled('div', {
|
||||
marginLeft,
|
||||
marginRight,
|
||||
marginBottom,
|
||||
flexGrow,
|
||||
flexShrink,
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
@@ -69,15 +69,15 @@ const GroupHeader = memo(function GroupHeader({
|
||||
return header;
|
||||
});
|
||||
|
||||
const calcCardHeightById = (id: string) => {
|
||||
const calcCardHeightById = (id: string, base = 250, scale = 10) => {
|
||||
if (!id) {
|
||||
return 250;
|
||||
return base;
|
||||
}
|
||||
const max = 5;
|
||||
const min = 1;
|
||||
const code = id.charCodeAt(0);
|
||||
const value = Math.floor((code % (max - min)) + min);
|
||||
return 250 + value * 10;
|
||||
return base + value * scale;
|
||||
};
|
||||
|
||||
export const DocListItemComponent = memo(function DocListItemComponent({
|
||||
@@ -93,9 +93,15 @@ export const DocListItemComponent = memo(function DocListItemComponent({
|
||||
export const DocsExplorer = ({
|
||||
className,
|
||||
disableMultiDelete,
|
||||
masonryItemWidthMin,
|
||||
heightBase,
|
||||
heightScale,
|
||||
}: {
|
||||
className?: string;
|
||||
disableMultiDelete?: boolean;
|
||||
masonryItemWidthMin?: number;
|
||||
heightBase?: number;
|
||||
heightScale?: number;
|
||||
}) => {
|
||||
const t = useI18n();
|
||||
const contextValue = useContext(DocExplorerContext);
|
||||
@@ -126,7 +132,7 @@ export const DocsExplorer = ({
|
||||
? 42
|
||||
: view === 'grid'
|
||||
? 280
|
||||
: calcCardHeightById(docId),
|
||||
: calcCardHeightById(docId, heightBase, heightScale),
|
||||
'data-view': view,
|
||||
className: styles.docItem,
|
||||
};
|
||||
@@ -134,7 +140,7 @@ export const DocsExplorer = ({
|
||||
} satisfies MasonryGroup;
|
||||
});
|
||||
return items;
|
||||
}, [groupBy, groups, view]);
|
||||
}, [groupBy, groups, heightBase, heightScale, view]);
|
||||
|
||||
const handleCloseFloatingToolbar = useCallback(() => {
|
||||
contextValue.selectMode$?.next(false);
|
||||
@@ -206,7 +212,7 @@ export const DocsExplorer = ({
|
||||
groupsGap={12}
|
||||
groupHeaderGapWithItems={12}
|
||||
columns={view === 'list' ? 1 : undefined}
|
||||
itemWidthMin={220}
|
||||
itemWidthMin={masonryItemWidthMin ?? 220}
|
||||
preloadHeight={100}
|
||||
itemWidth={'stretch'}
|
||||
virtualScroll
|
||||
|
||||
@@ -1,14 +1,88 @@
|
||||
import { useThemeColorV2 } from '@affine/component';
|
||||
import { useThemeColorV2, Wrapper } from '@affine/component';
|
||||
import { EmptyDocs } from '@affine/core/components/affine/empty';
|
||||
import {
|
||||
createDocExplorerContext,
|
||||
DocExplorerContext,
|
||||
} from '@affine/core/components/explorer/context';
|
||||
import { DocsExplorer } from '@affine/core/components/explorer/docs-view/docs-list';
|
||||
import { CollectionRulesService } from '@affine/core/modules/collection-rules';
|
||||
import { useLiveData, useService } from '@toeverything/infra';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { Page } from '../../components/page';
|
||||
import { AllDocList, AllDocsHeader } from '../../views';
|
||||
import { AllDocsHeader } from '../../views';
|
||||
|
||||
const AllDocs = () => {
|
||||
const [explorerContextValue] = useState(() =>
|
||||
createDocExplorerContext({
|
||||
quickFavorite: true,
|
||||
displayProperties: ['createdAt', 'updatedAt', 'tags'],
|
||||
view: 'masonry',
|
||||
showDragHandle: false,
|
||||
})
|
||||
);
|
||||
const collectionRulesService = useService(CollectionRulesService);
|
||||
const groups = useLiveData(explorerContextValue.groups$);
|
||||
const isEmpty =
|
||||
groups.length === 0 ||
|
||||
(groups.length && groups.every(group => !group.items.length));
|
||||
|
||||
useEffect(() => {
|
||||
const subscription = collectionRulesService
|
||||
.watch({
|
||||
filters: [
|
||||
{ type: 'system', key: 'trash', method: 'is', value: 'false' },
|
||||
],
|
||||
extraFilters: [
|
||||
{ type: 'system', key: 'trash', method: 'is', value: 'false' },
|
||||
{
|
||||
type: 'system',
|
||||
key: 'empty-journal',
|
||||
method: 'is',
|
||||
value: 'false',
|
||||
},
|
||||
],
|
||||
orderBy: {
|
||||
type: 'property',
|
||||
key: 'createdAt',
|
||||
desc: true,
|
||||
},
|
||||
})
|
||||
.subscribe({
|
||||
next: result => {
|
||||
explorerContextValue.groups$.next(result.groups);
|
||||
},
|
||||
error: console.error,
|
||||
});
|
||||
return () => subscription.unsubscribe();
|
||||
}, [collectionRulesService, explorerContextValue.groups$]);
|
||||
|
||||
if (isEmpty) {
|
||||
return (
|
||||
<>
|
||||
<EmptyDocs absoluteCenter />
|
||||
<Wrapper height={0} flexGrow={1} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<DocExplorerContext.Provider value={explorerContextValue}>
|
||||
<DocsExplorer
|
||||
masonryItemWidthMin={150}
|
||||
heightBase={180}
|
||||
heightScale={12}
|
||||
/>
|
||||
</DocExplorerContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const Component = () => {
|
||||
useThemeColorV2('layer/background/mobile/primary');
|
||||
|
||||
return (
|
||||
<Page header={<AllDocsHeader />} tab>
|
||||
<AllDocList />
|
||||
<AllDocs />
|
||||
</Page>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,11 +1,20 @@
|
||||
import { EmptyCollectionDetail } from '@affine/core/components/affine/empty';
|
||||
import { Wrapper } from '@affine/component';
|
||||
import {
|
||||
EmptyCollectionDetail,
|
||||
EmptyDocs,
|
||||
} from '@affine/core/components/affine/empty';
|
||||
import {
|
||||
createDocExplorerContext,
|
||||
DocExplorerContext,
|
||||
} from '@affine/core/components/explorer/context';
|
||||
import { DocsExplorer } from '@affine/core/components/explorer/docs-view/docs-list';
|
||||
import { PageHeader } from '@affine/core/mobile/components';
|
||||
import { Page } from '@affine/core/mobile/components/page';
|
||||
import type { Collection } from '@affine/core/modules/collection';
|
||||
import { ViewLayersIcon } from '@blocksuite/icons/rc';
|
||||
import { useLiveData } from '@toeverything/infra';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { AllDocList } from '../doc/list';
|
||||
import * as styles from './detail.css';
|
||||
|
||||
export const DetailHeader = ({ collection }: { collection: Collection }) => {
|
||||
@@ -20,6 +29,55 @@ export const DetailHeader = ({ collection }: { collection: Collection }) => {
|
||||
);
|
||||
};
|
||||
|
||||
const CollectionDocs = ({ collection }: { collection: Collection }) => {
|
||||
const [explorerContextValue] = useState(() =>
|
||||
createDocExplorerContext({
|
||||
quickFavorite: true,
|
||||
displayProperties: ['createdAt', 'updatedAt', 'tags'],
|
||||
view: 'masonry',
|
||||
showDragHandle: false,
|
||||
})
|
||||
);
|
||||
const groups = useLiveData(explorerContextValue.groups$);
|
||||
const isEmpty =
|
||||
groups.length === 0 ||
|
||||
(groups.length && groups.every(group => !group.items.length));
|
||||
|
||||
useEffect(() => {
|
||||
const subscription = collection.watch().subscribe({
|
||||
next: result => {
|
||||
explorerContextValue.groups$.next([
|
||||
{
|
||||
key: 'collection',
|
||||
items: result,
|
||||
},
|
||||
]);
|
||||
},
|
||||
error: console.error,
|
||||
});
|
||||
return () => subscription.unsubscribe();
|
||||
}, [collection, explorerContextValue.groups$]);
|
||||
|
||||
if (isEmpty) {
|
||||
return (
|
||||
<>
|
||||
<EmptyDocs absoluteCenter />
|
||||
<Wrapper height={0} flexGrow={1} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<DocExplorerContext.Provider value={explorerContextValue}>
|
||||
<DocsExplorer
|
||||
masonryItemWidthMin={150}
|
||||
heightBase={180}
|
||||
heightScale={12}
|
||||
/>
|
||||
</DocExplorerContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const CollectionDetail = ({
|
||||
collection,
|
||||
}: {
|
||||
@@ -38,7 +96,7 @@ export const CollectionDetail = ({
|
||||
|
||||
return (
|
||||
<Page header={<DetailHeader collection={collection} />}>
|
||||
<AllDocList collection={collection} />
|
||||
<CollectionDocs collection={collection} />
|
||||
</Page>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
export * from './list';
|
||||
export * from './masonry';
|
||||
export * from './menu';
|
||||
@@ -1,36 +0,0 @@
|
||||
import { cssVarV2 } from '@toeverything/theme/v2';
|
||||
import { globalStyle, style } from '@vanilla-extract/css';
|
||||
|
||||
export const groupTitle = style({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
padding: '0px 16px',
|
||||
width: '100%',
|
||||
});
|
||||
// to override style defined in `core`
|
||||
globalStyle(`${groupTitle} > div`, {
|
||||
marginRight: -4,
|
||||
});
|
||||
globalStyle(`${groupTitle} div[data-testid^='group-label']`, {
|
||||
fontSize: `20px !important`,
|
||||
color: `${cssVarV2('text/primary')} !important`,
|
||||
lineHeight: '25px !important',
|
||||
});
|
||||
export const groupTitleIcon = style({
|
||||
color: cssVarV2('icon/tertiary'),
|
||||
transition: 'transform 0.2s',
|
||||
selectors: {
|
||||
'[data-state="closed"] &': {
|
||||
transform: `rotate(-90deg)`,
|
||||
},
|
||||
},
|
||||
});
|
||||
export const groups = style({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: 32,
|
||||
});
|
||||
export const emptySpaceY = style({
|
||||
height: 0,
|
||||
flexGrow: 1,
|
||||
});
|
||||
@@ -1,107 +0,0 @@
|
||||
import { EmptyDocs } from '@affine/core/components/affine/empty';
|
||||
import { useBlockSuiteDocMeta } from '@affine/core/components/hooks/use-block-suite-page-meta';
|
||||
import {
|
||||
type ItemGroupProps,
|
||||
useAllDocDisplayProperties,
|
||||
} from '@affine/core/components/page-list';
|
||||
import type { Collection } from '@affine/core/modules/collection';
|
||||
import { DocsService } from '@affine/core/modules/doc';
|
||||
import type { Tag } from '@affine/core/modules/tag';
|
||||
import { WorkspaceService } from '@affine/core/modules/workspace';
|
||||
import type { DocMeta } from '@blocksuite/affine/store';
|
||||
import { ToggleDownIcon } from '@blocksuite/icons/rc';
|
||||
import * as Collapsible from '@radix-ui/react-collapsible';
|
||||
import { LiveData, useLiveData, useService } from '@toeverything/infra';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
|
||||
import * as styles from './list.css';
|
||||
import { MasonryDocs } from './masonry';
|
||||
|
||||
export const DocGroup = ({ group }: { group: ItemGroupProps<DocMeta> }) => {
|
||||
const [properties] = useAllDocDisplayProperties();
|
||||
const showTags = properties.displayProperties.tags;
|
||||
|
||||
if (group.id === 'all') {
|
||||
return <MasonryDocs items={group.items} showTags={showTags} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Collapsible.Root defaultOpen>
|
||||
<Collapsible.Trigger className={styles.groupTitle}>
|
||||
{group.label}
|
||||
<ToggleDownIcon className={styles.groupTitleIcon} />
|
||||
</Collapsible.Trigger>
|
||||
<Collapsible.Content>
|
||||
<MasonryDocs items={group.items} showTags={showTags} />
|
||||
</Collapsible.Content>
|
||||
</Collapsible.Root>
|
||||
);
|
||||
};
|
||||
|
||||
export interface AllDocListProps {
|
||||
collection?: Collection;
|
||||
tag?: Tag;
|
||||
trash?: boolean;
|
||||
}
|
||||
|
||||
export const AllDocList = ({ trash, collection, tag }: AllDocListProps) => {
|
||||
const [properties] = useAllDocDisplayProperties();
|
||||
const workspace = useService(WorkspaceService).workspace;
|
||||
const allPageMetas = useBlockSuiteDocMeta(workspace.docCollection);
|
||||
const docsService = useService(DocsService);
|
||||
|
||||
const allTrashPageIds = useLiveData(
|
||||
LiveData.from(docsService.allTrashDocIds$(), [])
|
||||
);
|
||||
|
||||
const tagPageIds = useLiveData(tag?.pageIds$);
|
||||
|
||||
const [filteredPageIds, setFilteredPageIds] = useState<string[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
const subscription = collection?.watch().subscribe(docIds => {
|
||||
setFilteredPageIds(docIds);
|
||||
});
|
||||
return () => subscription?.unsubscribe();
|
||||
}, [collection]);
|
||||
|
||||
const finalPageMetas = useMemo(() => {
|
||||
const collectionFilteredPageMetas = collection
|
||||
? allPageMetas.filter(page => filteredPageIds.includes(page.id))
|
||||
: allPageMetas;
|
||||
|
||||
const filteredPageMetas = collectionFilteredPageMetas.filter(
|
||||
page => allTrashPageIds.includes(page.id) === !!trash
|
||||
);
|
||||
|
||||
if (tag) {
|
||||
const pageIdsSet = new Set(tagPageIds);
|
||||
return filteredPageMetas.filter(page => pageIdsSet.has(page.id));
|
||||
}
|
||||
return filteredPageMetas;
|
||||
}, [
|
||||
allPageMetas,
|
||||
allTrashPageIds,
|
||||
collection,
|
||||
filteredPageIds,
|
||||
tag,
|
||||
tagPageIds,
|
||||
trash,
|
||||
]);
|
||||
|
||||
if (!finalPageMetas.length) {
|
||||
return (
|
||||
<>
|
||||
<EmptyDocs absoluteCenter tagId={tag?.id} />
|
||||
<div className={styles.emptySpaceY} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<MasonryDocs
|
||||
items={finalPageMetas}
|
||||
showTags={properties.displayProperties.tags}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -1,18 +0,0 @@
|
||||
import { style } from '@vanilla-extract/css';
|
||||
|
||||
export const paddingX = 16;
|
||||
export const columnGap = 17;
|
||||
|
||||
export const columns = style({
|
||||
padding: `16px ${paddingX}px`,
|
||||
display: 'flex',
|
||||
gap: columnGap,
|
||||
});
|
||||
|
||||
export const column = style({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: 10,
|
||||
width: 0,
|
||||
flex: 1,
|
||||
});
|
||||
@@ -1,44 +0,0 @@
|
||||
import { Masonry } from '@affine/component';
|
||||
import type { DocMeta } from '@blocksuite/affine/store';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { calcRowsById, DocCard } from '../../../components';
|
||||
|
||||
const fullStyle = {
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
};
|
||||
|
||||
export const MasonryDocs = ({
|
||||
items,
|
||||
showTags,
|
||||
}: {
|
||||
items: DocMeta[];
|
||||
showTags?: boolean;
|
||||
}) => {
|
||||
const masonryItems = useMemo(
|
||||
() =>
|
||||
items.map(item => {
|
||||
return {
|
||||
id: item.id,
|
||||
height: calcRowsById(item.id) * 18 + 95,
|
||||
children: (
|
||||
<DocCard style={fullStyle} meta={item} showTags={showTags} />
|
||||
),
|
||||
};
|
||||
}),
|
||||
[items, showTags]
|
||||
);
|
||||
return (
|
||||
<Masonry
|
||||
style={fullStyle}
|
||||
itemWidthMin={160}
|
||||
gapX={17}
|
||||
gapY={10}
|
||||
paddingX={16}
|
||||
paddingY={16}
|
||||
virtualScroll
|
||||
items={masonryItems}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -1,65 +0,0 @@
|
||||
import { bodyEmphasized, bodyRegular } from '@toeverything/theme/typography';
|
||||
import { cssVarV2 } from '@toeverything/theme/v2';
|
||||
import { style } from '@vanilla-extract/css';
|
||||
|
||||
export const root = style({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: 10,
|
||||
});
|
||||
export const head = style([
|
||||
bodyEmphasized,
|
||||
{
|
||||
padding: '10px 20px',
|
||||
color: cssVarV2('text/primary'),
|
||||
},
|
||||
]);
|
||||
|
||||
export const item = style([
|
||||
bodyRegular,
|
||||
{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
gap: 4,
|
||||
height: 34,
|
||||
padding: '0 20px',
|
||||
},
|
||||
]);
|
||||
export const itemSuffix = style({
|
||||
display: 'flex',
|
||||
gap: 8,
|
||||
alignItems: 'center',
|
||||
});
|
||||
export const itemSuffixText = style({
|
||||
color: cssVarV2('text/secondary'),
|
||||
});
|
||||
export const itemSuffixIcon = style({
|
||||
color: cssVarV2('icon/primary'),
|
||||
fontSize: 20,
|
||||
});
|
||||
export const divider = style({
|
||||
width: '100%',
|
||||
height: 16,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
':before': {
|
||||
content: "''",
|
||||
height: 0.5,
|
||||
width: '100%',
|
||||
backgroundColor: cssVarV2('layer/insideBorder/border'),
|
||||
},
|
||||
});
|
||||
export const propertiesList = style({
|
||||
padding: '4px 20px',
|
||||
display: 'flex',
|
||||
gap: 8,
|
||||
});
|
||||
export const propertyButton = style({
|
||||
opacity: 0.4,
|
||||
selectors: {
|
||||
'&[data-selected="true"]': {
|
||||
opacity: 1,
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -1,73 +0,0 @@
|
||||
import { Button, MobileMenu, MobileMenuItem } from '@affine/component';
|
||||
import {
|
||||
getGroupOptions,
|
||||
useAllDocDisplayProperties,
|
||||
} from '@affine/core/components/page-list';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
import { ArrowRightSmallIcon } from '@blocksuite/icons/rc';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import * as styles from './menu.css';
|
||||
|
||||
export const AllDocsMenu = () => {
|
||||
const t = useI18n();
|
||||
|
||||
const [properties, setProperties] = useAllDocDisplayProperties();
|
||||
const groupOptions = useMemo(() => getGroupOptions(t), [t]);
|
||||
|
||||
const activeGroup = useMemo(
|
||||
() => groupOptions.find(g => g.value === properties.groupBy),
|
||||
[groupOptions, properties.groupBy]
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={styles.root}>
|
||||
<header className={styles.head}>Display Settings</header>
|
||||
<MobileMenu
|
||||
items={
|
||||
<>
|
||||
<div className={styles.divider} />
|
||||
{groupOptions.map(group => (
|
||||
<MobileMenuItem
|
||||
onSelect={() => setProperties('groupBy', group.value)}
|
||||
selected={properties.groupBy === group.value}
|
||||
key={group.value}
|
||||
>
|
||||
{group.label}
|
||||
</MobileMenuItem>
|
||||
))}
|
||||
</>
|
||||
}
|
||||
>
|
||||
<div className={styles.item}>
|
||||
<span>{t['com.affine.page.display.grouping']()}</span>
|
||||
<div className={styles.itemSuffix}>
|
||||
<span className={styles.itemSuffixText}>{activeGroup?.label}</span>
|
||||
<ArrowRightSmallIcon className={styles.itemSuffixIcon} />
|
||||
</div>
|
||||
</div>
|
||||
</MobileMenu>
|
||||
|
||||
<div className={styles.divider} />
|
||||
|
||||
<div className={styles.item}>
|
||||
{t['com.affine.page.display.display-properties']()}
|
||||
</div>
|
||||
<div className={styles.propertiesList}>
|
||||
<Button
|
||||
size="large"
|
||||
className={styles.propertyButton}
|
||||
data-selected={properties.displayProperties.tags}
|
||||
onClick={() =>
|
||||
setProperties('displayProperties', {
|
||||
...properties.displayProperties,
|
||||
tags: !properties.displayProperties.tags,
|
||||
})
|
||||
}
|
||||
>
|
||||
Tag
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,4 +1,3 @@
|
||||
export * from './collection';
|
||||
export * from './doc/';
|
||||
export * from './header';
|
||||
export * from './tag';
|
||||
|
||||
@@ -1,13 +1,93 @@
|
||||
import { Wrapper } from '@affine/component';
|
||||
import { EmptyDocs } from '@affine/core/components/affine/empty';
|
||||
import {
|
||||
createDocExplorerContext,
|
||||
DocExplorerContext,
|
||||
} from '@affine/core/components/explorer/context';
|
||||
import { DocsExplorer } from '@affine/core/components/explorer/docs-view/docs-list';
|
||||
import { Page } from '@affine/core/mobile/components/page';
|
||||
import { CollectionRulesService } from '@affine/core/modules/collection-rules';
|
||||
import type { Tag } from '@affine/core/modules/tag';
|
||||
import { useLiveData, useService } from '@toeverything/infra';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { AllDocList } from '../doc';
|
||||
import { TagDetailHeader } from './detail-header';
|
||||
|
||||
const TagDocs = ({ tag }: { tag: Tag }) => {
|
||||
const [explorerContextValue] = useState(() =>
|
||||
createDocExplorerContext({
|
||||
quickFavorite: true,
|
||||
displayProperties: ['createdAt', 'updatedAt', 'tags'],
|
||||
view: 'masonry',
|
||||
showDragHandle: false,
|
||||
})
|
||||
);
|
||||
const collectionRulesService = useService(CollectionRulesService);
|
||||
const groups = useLiveData(explorerContextValue.groups$);
|
||||
const isEmpty =
|
||||
groups.length === 0 ||
|
||||
(groups.length && groups.every(group => !group.items.length));
|
||||
|
||||
useEffect(() => {
|
||||
const subscription = collectionRulesService
|
||||
.watch({
|
||||
filters: [
|
||||
{ type: 'system', key: 'trash', method: 'is', value: 'false' },
|
||||
{
|
||||
type: 'system',
|
||||
key: 'tags',
|
||||
method: 'include-all',
|
||||
value: tag.id,
|
||||
},
|
||||
],
|
||||
extraFilters: [
|
||||
{ type: 'system', key: 'trash', method: 'is', value: 'false' },
|
||||
{
|
||||
type: 'system',
|
||||
key: 'empty-journal',
|
||||
method: 'is',
|
||||
value: 'false',
|
||||
},
|
||||
],
|
||||
orderBy: {
|
||||
type: 'property',
|
||||
key: 'createdAt',
|
||||
desc: true,
|
||||
},
|
||||
})
|
||||
.subscribe({
|
||||
next: result => {
|
||||
explorerContextValue.groups$.next(result.groups);
|
||||
},
|
||||
error: console.error,
|
||||
});
|
||||
return () => subscription.unsubscribe();
|
||||
}, [collectionRulesService, explorerContextValue.groups$, tag.id]);
|
||||
|
||||
if (isEmpty) {
|
||||
return (
|
||||
<>
|
||||
<EmptyDocs absoluteCenter tagId={tag.id} />
|
||||
<Wrapper height={0} flexGrow={1} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<DocExplorerContext.Provider value={explorerContextValue}>
|
||||
<DocsExplorer
|
||||
masonryItemWidthMin={150}
|
||||
heightBase={180}
|
||||
heightScale={12}
|
||||
/>
|
||||
</DocExplorerContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const TagDetail = ({ tag }: { tag: Tag }) => {
|
||||
return (
|
||||
<Page header={<TagDetailHeader tag={tag} />} tab>
|
||||
<AllDocList tag={tag} />
|
||||
<TagDocs tag={tag} />
|
||||
</Page>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -13,7 +13,7 @@ test.beforeEach(async ({ page }) => {
|
||||
const docsTab = page.locator('#app-tabs').getByRole('tab', { name: 'all' });
|
||||
await expect(docsTab).toBeVisible();
|
||||
await docsTab.click();
|
||||
await page.getByTestId('doc-card').first().click();
|
||||
await page.getByTestId('doc-list-item').first().click();
|
||||
await expect(page.locator('.affine-page-viewport')).toBeVisible();
|
||||
});
|
||||
|
||||
@@ -53,7 +53,5 @@ test('can add text property', async ({ page }) => {
|
||||
).toBeVisible();
|
||||
await page.getByTestId('mobile-menu-back-button').last().click();
|
||||
|
||||
await expect(page.getByTestId('mobile-menu-back-button')).toContainText(
|
||||
'Getting Started'
|
||||
);
|
||||
await expect(page.getByTestId('mobile-menu-back-button')).toBeVisible();
|
||||
});
|
||||
|
||||
@@ -47,6 +47,6 @@ test('all tab', async ({ page }) => {
|
||||
|
||||
await docsTab.click();
|
||||
|
||||
const todayDocs = page.getByTestId('doc-card');
|
||||
const todayDocs = page.getByTestId('doc-list-item');
|
||||
expect(await todayDocs.count()).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user