From 68b57cdee73d4be3ddaa4f01271ab038d52334f2 Mon Sep 17 00:00:00 2001 From: EYHN Date: Thu, 15 May 2025 15:36:42 +0900 Subject: [PATCH] feat(core): move collapsed groups state to masonry --- .../component/src/ui/masonry/masonry.tsx | 100 ++++++++++++------ .../frontend/component/src/ui/masonry/type.ts | 12 +-- .../component/src/ui/masonry/utils.ts | 20 ++-- .../core/src/components/explorer/context.ts | 2 - .../explorer/docs-view/group-header.tsx | 21 ++-- .../core/src/components/explorer/types.ts | 1 + .../workspace-property-types/checkbox.tsx | 9 +- .../created-updated-by.tsx | 9 +- .../workspace-property-types/date.tsx | 14 ++- .../doc-primary-mode.tsx | 9 +- .../workspace-property-types/journal.tsx | 14 ++- .../workspace-property-types/tags.tsx | 11 +- .../workspace-property-types/text.tsx | 14 ++- .../pages/workspace/all-page/all-page.tsx | 43 +++++--- .../doc-created-by-updated-by-sync.ts | 3 +- 15 files changed, 191 insertions(+), 91 deletions(-) diff --git a/packages/frontend/component/src/ui/masonry/masonry.tsx b/packages/frontend/component/src/ui/masonry/masonry.tsx index a99e054e7e..ffdc1d6157 100644 --- a/packages/frontend/component/src/ui/masonry/masonry.tsx +++ b/packages/frontend/component/src/ui/masonry/masonry.tsx @@ -31,6 +31,23 @@ import { export interface MasonryProps extends React.HTMLAttributes { items: MasonryItem[] | MasonryGroup[]; + itemComponent: React.ComponentType<{ + groupId: string; + itemId: string; + }>; + groupComponent?: React.ComponentType<{ + groupId: string; + itemCount: number; + collapsed: boolean; + onCollapse: (collapsed: boolean) => void; + }>; + + groupHeight?: number | ((group: MasonryGroup) => number); + itemHeight: number | ((item: MasonryItem) => number); + + groupClassName?: string; + itemClassName?: string; + gapX?: number; gapY?: number; paddingX?: MasonryPX; @@ -39,8 +56,7 @@ export interface MasonryProps extends React.HTMLAttributes { groupsGap?: number; groupHeaderGapWithItems?: number; stickyGroupHeader?: boolean; - collapsedGroups?: string[]; - onGroupCollapse?: (groupId: string, collapsed: boolean) => void; + /** * Specify the width of the item. * - `number`: The width of the item in pixels. @@ -61,6 +77,9 @@ export interface MasonryProps extends React.HTMLAttributes { columns?: number; resizeDebounce?: number; preloadHeight?: number; + + itemSelected?: string[]; + onItemSelectedChanged?: (selected: string[]) => void; } export const Masonry = ({ @@ -77,11 +96,15 @@ export const Masonry = ({ groupsGap = 0, groupHeaderGapWithItems = 0, stickyGroupHeader = true, - collapsedGroups, columns, preloadHeight = 50, resizeDebounce = 20, - onGroupCollapse, + groupComponent: GroupComponent, + itemComponent: ItemComponent, + groupClassName, + itemClassName, + itemHeight, + groupHeight, ...props }: MasonryProps) => { const rootRef = useRef(null); @@ -99,9 +122,17 @@ export const Masonry = ({ undefined ); const [totalWidth, setTotalWidth] = useState(0); + + const [collapsedGroups, setCollapsedGroups] = useState([]); + const onGroupCollapse = useCallback((groupId: string, collapsed: boolean) => { + setCollapsedGroups(prev => + collapsed ? [...prev, groupId] : prev.filter(id => id !== groupId) + ); + }, []); + const stickyGroupCollapsed = !!( collapsedGroups && - stickyGroupId && + stickyGroupId !== undefined && collapsedGroups.includes(stickyGroupId) ); @@ -114,7 +145,7 @@ export const Masonry = ({ }, [items]); const stickyGroup = useMemo(() => { - if (!stickyGroupId) return undefined; + if (stickyGroupId === undefined) return undefined; return groups.find(group => group.id === stickyGroupId); }, [groups, stickyGroupId]); @@ -164,6 +195,8 @@ export const Masonry = ({ groupsGap, groupHeaderGapWithItems, collapsedGroups: collapsedGroups ?? [], + groupHeight: groupHeight ?? 0, + itemHeight, }); setLayoutMap(layout); setHeight(height); @@ -180,8 +213,10 @@ export const Masonry = ({ gapX, gapY, groupHeaderGapWithItems, + groupHeight, groups, groupsGap, + itemHeight, itemWidth, itemWidthMin, paddingX, @@ -233,13 +268,7 @@ export const Masonry = ({ > {groups.map(group => { // sleep is not calculated, do not render - const { - id: groupId, - items, - className, - Component, - ...groupProps - } = group; + const { id: groupId, items, ...groupProps } = group; const collapsed = collapsedGroups && collapsedGroups.includes(groupId); @@ -248,14 +277,16 @@ export const Masonry = ({ {/* group header */} {virtualScroll && !activeMap.get(group.id) ? null : ( onGroupCollapse?.(groupId, !collapsed)} - Component={Component} + onCollapse={collapsed => + onGroupCollapse?.(groupId, collapsed) + } + Component={GroupComponent} itemCount={items.length} collapsed={!!collapsed} groupId={groupId} @@ -265,19 +296,20 @@ export const Masonry = ({ {/* group items */} {collapsed ? null - : items.map(({ id: itemId, Component, ...item }) => { + : items.map(item => { + const itemId = item.id; const mixId = groupId ? `${groupId}:${itemId}` : itemId; if (virtualScroll && !activeMap.get(mixId)) return null; return ( ); })} @@ -289,24 +321,24 @@ export const Masonry = ({ {stickyGroup ? (
- onGroupCollapse?.(stickyGroup.id, !stickyGroupCollapsed) - } > - {stickyGroup.Component ? ( - { + onGroupCollapse(stickyGroup.id, collapsed); + }} /> - ) : ( - stickyGroup.children )}
) : null} @@ -330,11 +362,12 @@ const MasonryGroupHeader = memo(function MasonryGroupHeader({ groupId, itemCount, collapsed, - height, paddingX, + onCollapse, ...props }: Omit & { - Component?: MasonryGroup['Component']; + Component?: MasonryProps['groupComponent']; + onCollapse: (collapsed: boolean) => void; groupId: string; itemCount: number; collapsed: boolean; @@ -347,16 +380,16 @@ const MasonryGroupHeader = memo(function MasonryGroupHeader({ groupId={groupId} itemCount={itemCount} collapsed={collapsed} + onCollapse={onCollapse} /> ); } return children; - }, [Component, children, collapsed, groupId, itemCount]); + }, [Component, children, collapsed, groupId, itemCount, onCollapse]); return ( { if (Component) { diff --git a/packages/frontend/component/src/ui/masonry/type.ts b/packages/frontend/component/src/ui/masonry/type.ts index c10d281be8..54d9e1f7b7 100644 --- a/packages/frontend/component/src/ui/masonry/type.ts +++ b/packages/frontend/component/src/ui/masonry/type.ts @@ -1,18 +1,10 @@ -export interface MasonryItem extends React.HTMLAttributes { +export interface MasonryItem { id: string; - height: number; - Component?: React.ComponentType<{ groupId: string; itemId: string }>; } -export interface MasonryGroup extends React.HTMLAttributes { +export interface MasonryGroup { id: string; - height: number; items: MasonryItem[]; - Component?: React.ComponentType<{ - groupId: string; - collapsed?: boolean; - itemCount: number; - }>; } export interface MasonryItemXYWH { diff --git a/packages/frontend/component/src/ui/masonry/utils.ts b/packages/frontend/component/src/ui/masonry/utils.ts index 13da3b036d..4107843890 100644 --- a/packages/frontend/component/src/ui/masonry/utils.ts +++ b/packages/frontend/component/src/ui/masonry/utils.ts @@ -61,6 +61,8 @@ export const calcLayout = ( groupsGap: number; groupHeaderGapWithItems: number; collapsedGroups: string[]; + groupHeight: number | ((group: MasonryGroup) => number); + itemHeight: number | ((item: MasonryItem) => number); } ) => { const { @@ -74,6 +76,8 @@ export const calcLayout = ( groupsGap, groupHeaderGapWithItems, collapsedGroups, + groupHeight, + itemHeight, } = options; const paddingX = calcPX(_paddingX, totalWidth); @@ -92,7 +96,7 @@ export const calcLayout = ( x: 0, y: finalHeight, w: totalWidth, - h: group.height, + h: typeof groupHeight === 'function' ? groupHeight(group) : groupHeight, }; layout.set(group.id, groupHeaderLayout); @@ -110,19 +114,21 @@ export const calcLayout = ( const hasGap = heightStack[minHeightIndex] ? gapY : 0; const x = minHeightIndex * (width + gapX) + paddingX; const y = finalHeight + minHeight + hasGap; + const height = + typeof itemHeight === 'function' ? itemHeight(item) : itemHeight; - heightStack[minHeightIndex] += item.height + hasGap; + heightStack[minHeightIndex] += height + hasGap; layout.set(itemId, { type: 'item', x, y, w: width, - h: item.height, + h: height, }); }); - const groupHeight = Math.max(...heightStack) + paddingY; - finalHeight += groupHeight; + const height = Math.max(...heightStack) + paddingY; + finalHeight += height; }); return { layout, height: finalHeight }; @@ -167,9 +173,9 @@ export const calcSticky = (options: { return xywh.y < scrollY && (!next || next[1].y > scrollY); }); - return stickyGroupEntry + return stickyGroupEntry !== undefined ? stickyGroupEntry[0] : groupEntries.length > 0 ? groupEntries[0][0] - : ''; + : undefined; }; diff --git a/packages/frontend/core/src/components/explorer/context.ts b/packages/frontend/core/src/components/explorer/context.ts index 8ab7a2a057..c7ac18be8f 100644 --- a/packages/frontend/core/src/components/explorer/context.ts +++ b/packages/frontend/core/src/components/explorer/context.ts @@ -7,7 +7,6 @@ import type { ExplorerPreference } from './types'; export type DocExplorerContextType = { view$: LiveData; groups$: LiveData>; - collapsedGroups$: LiveData; selectMode$?: LiveData; selectedDocIds$: LiveData; prevCheckAnchorId$?: LiveData; @@ -25,7 +24,6 @@ export const createDocExplorerContext = () => ({ view$: new LiveData('list'), groups$: new LiveData>([]), - collapsedGroups$: new LiveData([]), selectMode$: new LiveData(false), selectedDocIds$: new LiveData([]), prevCheckAnchorId$: new LiveData(null), diff --git a/packages/frontend/core/src/components/explorer/docs-view/group-header.tsx b/packages/frontend/core/src/components/explorer/docs-view/group-header.tsx index 77962003eb..e5ac086b18 100644 --- a/packages/frontend/core/src/components/explorer/docs-view/group-header.tsx +++ b/packages/frontend/core/src/components/explorer/docs-view/group-header.tsx @@ -16,16 +16,19 @@ import * as styles from './group-header.css'; export const DocGroupHeader = ({ className, groupId, + collapsed, + onCollapse, ...props }: HTMLAttributes & { groupId: string; + collapsed: boolean; + onCollapse: (collapsed: boolean) => void; }) => { const t = useI18n(); const contextValue = useContext(DocExplorerContext); const groups = useLiveData(contextValue.groups$); const selectedDocIds = useLiveData(contextValue.selectedDocIds$); - const collapsedGroups = useLiveData(contextValue.collapsedGroups$); const selectMode = useLiveData(contextValue.selectMode$); const group = groups.find(g => g.key === groupId); @@ -34,13 +37,8 @@ export const DocGroupHeader = ({ ); const handleToggleCollapse = useCallback(() => { - const prev = contextValue.collapsedGroups$.value; - contextValue.collapsedGroups$.next( - prev.includes(groupId) - ? prev.filter(id => id !== groupId) - : [...prev, groupId] - ); - }, [groupId, contextValue]); + onCollapse(!collapsed); + }, [collapsed, onCollapse]); const handleSelectAll = useCallback(() => { const prev = contextValue.selectedDocIds$.value; @@ -64,10 +62,7 @@ export const DocGroupHeader = ({ ).length; return ( -
+
{selectMode ? (
@@ -105,6 +100,8 @@ export const PlainTextDocGroupHeader = ({ ...props }: HTMLAttributes & { groupId: string; + collapsed: boolean; + onCollapse: (collapsed: boolean) => void; docCount: number; icon?: ReactNode; }) => { diff --git a/packages/frontend/core/src/components/explorer/types.ts b/packages/frontend/core/src/components/explorer/types.ts index c8609eba18..05ff05ed85 100644 --- a/packages/frontend/core/src/components/explorer/types.ts +++ b/packages/frontend/core/src/components/explorer/types.ts @@ -30,4 +30,5 @@ export interface GroupHeaderProps { groupId: string; docCount: number; collapsed: boolean; + onCollapse: (collapsed: boolean) => void; } diff --git a/packages/frontend/core/src/components/workspace-property-types/checkbox.tsx b/packages/frontend/core/src/components/workspace-property-types/checkbox.tsx index 78b1c82e16..dc5fc8c06a 100644 --- a/packages/frontend/core/src/components/workspace-property-types/checkbox.tsx +++ b/packages/frontend/core/src/components/workspace-property-types/checkbox.tsx @@ -100,10 +100,17 @@ export const CheckboxDocListProperty = ({ export const CheckboxGroupHeader = ({ groupId, docCount, + collapsed, + onCollapse, }: GroupHeaderProps) => { const text = groupId === 'true' ? 'Checked' : 'Unchecked'; return ( - + {text} ); diff --git a/packages/frontend/core/src/components/workspace-property-types/created-updated-by.tsx b/packages/frontend/core/src/components/workspace-property-types/created-updated-by.tsx index 8794796d6b..20800c6038 100644 --- a/packages/frontend/core/src/components/workspace-property-types/created-updated-by.tsx +++ b/packages/frontend/core/src/components/workspace-property-types/created-updated-by.tsx @@ -166,11 +166,18 @@ export const UpdatedByDocListInlineProperty = ({ export const ModifiedByGroupHeader = ({ groupId, docCount, + collapsed, + onCollapse, }: GroupHeaderProps) => { const userId = groupId; return ( - +
diff --git a/packages/frontend/core/src/components/workspace-property-types/date.tsx b/packages/frontend/core/src/components/workspace-property-types/date.tsx index ec45a299bb..84ea13896d 100644 --- a/packages/frontend/core/src/components/workspace-property-types/date.tsx +++ b/packages/frontend/core/src/components/workspace-property-types/date.tsx @@ -243,11 +243,21 @@ export const UpdatedDateDocListProperty = ({ doc }: DocListPropertyProps) => { ); }; -export const DateGroupHeader = ({ groupId, docCount }: GroupHeaderProps) => { +export const DateGroupHeader = ({ + groupId, + docCount, + collapsed, + onCollapse, +}: GroupHeaderProps) => { const date = groupId || 'No Date'; return ( - + {date} ); diff --git a/packages/frontend/core/src/components/workspace-property-types/doc-primary-mode.tsx b/packages/frontend/core/src/components/workspace-property-types/doc-primary-mode.tsx index 6bba38df11..fee5720de4 100644 --- a/packages/frontend/core/src/components/workspace-property-types/doc-primary-mode.tsx +++ b/packages/frontend/core/src/components/workspace-property-types/doc-primary-mode.tsx @@ -137,6 +137,8 @@ export const DocPrimaryModeDocListProperty = ({ export const DocPrimaryModeGroupHeader = ({ groupId, docCount, + collapsed, + onCollapse, }: GroupHeaderProps) => { const t = useI18n(); const text = @@ -147,7 +149,12 @@ export const DocPrimaryModeGroupHeader = ({ : 'Default'; return ( - + {text} ); diff --git a/packages/frontend/core/src/components/workspace-property-types/journal.tsx b/packages/frontend/core/src/components/workspace-property-types/journal.tsx index 2112d52440..8b7584488e 100644 --- a/packages/frontend/core/src/components/workspace-property-types/journal.tsx +++ b/packages/frontend/core/src/components/workspace-property-types/journal.tsx @@ -236,10 +236,20 @@ export const JournalDocListProperty = ({ doc }: DocListPropertyProps) => { ); }; -export const JournalGroupHeader = ({ groupId, docCount }: GroupHeaderProps) => { +export const JournalGroupHeader = ({ + groupId, + docCount, + collapsed, + onCollapse, +}: GroupHeaderProps) => { const text = groupId === 'true' ? 'Journal' : 'Not Journal'; return ( - + {text} ); diff --git a/packages/frontend/core/src/components/workspace-property-types/tags.tsx b/packages/frontend/core/src/components/workspace-property-types/tags.tsx index 4db7d6e101..0c70c6ad13 100644 --- a/packages/frontend/core/src/components/workspace-property-types/tags.tsx +++ b/packages/frontend/core/src/components/workspace-property-types/tags.tsx @@ -202,7 +202,12 @@ export const TagsDocListProperty = ({ doc }: DocListPropertyProps) => { ); }; -export const TagsGroupHeader = ({ groupId, docCount }: GroupHeaderProps) => { +export const TagsGroupHeader = ({ + groupId, + docCount, + collapsed, + onCollapse, +}: GroupHeaderProps) => { const t = useI18n(); const tagService = useService(TagService); const tag = useLiveData(tagService.tagList.tagByTagId$(groupId)); @@ -212,6 +217,8 @@ export const TagsGroupHeader = ({ groupId, docCount }: GroupHeaderProps) => { { } > diff --git a/packages/frontend/core/src/components/workspace-property-types/text.tsx b/packages/frontend/core/src/components/workspace-property-types/text.tsx index 364e3a474c..7435a03fe4 100644 --- a/packages/frontend/core/src/components/workspace-property-types/text.tsx +++ b/packages/frontend/core/src/components/workspace-property-types/text.tsx @@ -260,10 +260,20 @@ export const TextDocListProperty = ({ value }: { value: string }) => { return }>{value}; }; -export const TextGroupHeader = ({ groupId, docCount }: GroupHeaderProps) => { +export const TextGroupHeader = ({ + groupId, + docCount, + collapsed, + onCollapse, +}: GroupHeaderProps) => { const text = groupId || 'No Text'; return ( - + {text} ); diff --git a/packages/frontend/core/src/desktop/pages/workspace/all-page/all-page.tsx b/packages/frontend/core/src/desktop/pages/workspace/all-page/all-page.tsx index 5be0ad42ed..8067f942ed 100644 --- a/packages/frontend/core/src/desktop/pages/workspace/all-page/all-page.tsx +++ b/packages/frontend/core/src/desktop/pages/workspace/all-page/all-page.tsx @@ -50,10 +50,12 @@ import { PinnedCollections } from './pinned-collections'; const GroupHeader = memo(function GroupHeader({ groupId, collapsed, + onCollapse, itemCount, }: { groupId: string; collapsed?: boolean; + onCollapse: (collapsed: boolean) => void; itemCount: number; }) { const contextValue = useContext(DocExplorerContext); @@ -76,12 +78,21 @@ const GroupHeader = memo(function GroupHeader({ groupId={groupId} docCount={itemCount} collapsed={!!collapsed} + onCollapse={onCollapse} /> ); } else { return '// TODO: ' + groupType; } - }, [allProperties, collapsed, groupId, groupKey, groupType, itemCount]); + }, [ + allProperties, + collapsed, + groupId, + groupKey, + groupType, + itemCount, + onCollapse, + ]); if (!groupType) { return null; @@ -166,7 +177,6 @@ export const AllPage = () => { const orderBy = useLiveData(explorerContextValue.orderBy$); const groups = useLiveData(explorerContextValue.groups$); const selectedDocIds = useLiveData(explorerContextValue.selectedDocIds$); - const collapsedGroups = useLiveData(explorerContextValue.collapsedGroups$); const selectMode = useLiveData(explorerContextValue.selectMode$); const { openPromptModal } = usePromptModal(); @@ -175,27 +185,15 @@ export const AllPage = () => { 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]); + }, [groups]); const collectionRulesService = useService(CollectionRulesService); useEffect(() => { @@ -443,11 +441,24 @@ export const AllPage = () => { preloadHeight={100} itemWidth={'stretch'} virtualScroll - collapsedGroups={collapsedGroups} + groupComponent={GroupHeader} + itemComponent={DocListItemComponent} + groupClassName={styles.groupHeader} + itemClassName={styles.docItem} paddingX={useCallback( (w: number) => (w > 500 ? 24 : w > 393 ? 20 : 16), [] )} + groupHeight={groupBy ? 24 : 0} + itemHeight={useMemo( + () => + view === 'list' + ? 42 + : view === 'grid' + ? 280 + : item => calcCardHeightById(item.id), + [view] + )} />
diff --git a/packages/frontend/core/src/modules/cloud/services/doc-created-by-updated-by-sync.ts b/packages/frontend/core/src/modules/cloud/services/doc-created-by-updated-by-sync.ts index 302008eeed..9ccc8c4edd 100644 --- a/packages/frontend/core/src/modules/cloud/services/doc-created-by-updated-by-sync.ts +++ b/packages/frontend/core/src/modules/cloud/services/doc-created-by-updated-by-sync.ts @@ -136,7 +136,8 @@ export class DocCreatedByUpdatedBySyncService extends Service { workspaceRootDocSynced && isOwnerOrAdmin && missingCreatedBy && - !markedSynced + !markedSynced && + this.workspaceService.workspace.flavour !== 'local' ) ), false