diff --git a/packages/frontend/core/src/components/affine/page-properties/page-properties-manager.ts b/packages/frontend/core/src/components/affine/page-properties/page-properties-manager.ts index 060468261b..53791aaf38 100644 --- a/packages/frontend/core/src/components/affine/page-properties/page-properties-manager.ts +++ b/packages/frontend/core/src/components/affine/page-properties/page-properties-manager.ts @@ -5,8 +5,8 @@ import type { PageInfoCustomPropertyMeta, } from '@affine/core/modules/workspace/properties/schema'; import { PagePropertyType } from '@affine/core/modules/workspace/properties/schema'; +import { createFractionalIndexingSortableHelper } from '@affine/core/utils'; import { DebugLogger } from '@affine/debug'; -import { generateKeyBetween } from 'fractional-indexing'; import { nanoid } from 'nanoid'; import { getDefaultIconName } from './icons-mapping'; @@ -147,6 +147,11 @@ export class PagePropertiesManager { this.ensureRequiredProperties(); } + readonly sorter = createFractionalIndexingSortableHelper< + PageInfoCustomProperty, + string | number + >(this); + // prevent infinite loop private ensuring = false; ensureRequiredProperties() { @@ -163,6 +168,22 @@ export class PagePropertiesManager { this.ensuring = false; } + getItems() { + return Object.values(this.getCustomProperties()); + } + + getItemOrder(item: PageInfoCustomProperty): string { + return item.order; + } + + setItemOrder(item: PageInfoCustomProperty, order: string) { + item.order = order; + } + + getItemId(item: PageInfoCustomProperty) { + return item.id; + } + get workspace() { return this.adapter.workspace; } @@ -204,16 +225,6 @@ export class PagePropertiesManager { : {}; } - getOrderedCustomProperties() { - return Object.values(this.getCustomProperties()).sort((a, b) => - a.order > b.order ? 1 : a.order < b.order ? -1 : 0 - ); - } - - largestOrder() { - return this.getOrderedCustomProperties().at(-1)?.order ?? null; - } - getCustomPropertyMeta(id: string): PageInfoCustomPropertyMeta | undefined { return this.metaManager.customPropertiesSchema[id]; } @@ -234,7 +245,7 @@ export class PagePropertiesManager { return; } - const newOrder = generateKeyBetween(this.largestOrder(), null); + const newOrder = this.sorter.getNewItemOrder(); if (this.properties!.custom[id]) { logger.warn(`custom property ${id} already exists`); } @@ -247,21 +258,6 @@ export class PagePropertiesManager { }; } - moveCustomProperty(from: number, to: number) { - this.ensureRequiredProperties(); - // move from -> to means change from's order to a new order between to and to -1/+1 - const properties = this.getOrderedCustomProperties(); - const fromProperty = properties[from]; - const toProperty = properties[to]; - const toNextProperty = properties[from < to ? to + 1 : to - 1]; - const args: [string?, string?] = - from < to - ? [toProperty.order, toNextProperty?.order ?? null] - : [toNextProperty?.order ?? null, toProperty.order]; - const newOrder = generateKeyBetween(...args); - this.properties!.custom[fromProperty.id].order = newOrder; - } - hasCustomProperty(id: string) { return !!this.properties?.custom[id]; } diff --git a/packages/frontend/core/src/components/affine/page-properties/table.tsx b/packages/frontend/core/src/components/affine/page-properties/table.tsx index 4776c63d68..068bc5e67f 100644 --- a/packages/frontend/core/src/components/affine/page-properties/table.tsx +++ b/packages/frontend/core/src/components/affine/page-properties/table.tsx @@ -101,10 +101,7 @@ interface SortablePropertiesProps { const SortableProperties = ({ children }: SortablePropertiesProps) => { const manager = useContext(managerContext); - const properties = useMemo( - () => manager.getOrderedCustomProperties(), - [manager] - ); + const properties = useMemo(() => manager.sorter.getOrderedItems(), [manager]); const editingItem = useAtomValue(editingPropertyAtom); const draggable = !manager.readonly && !editingItem; const sensors = useSensors( @@ -128,15 +125,12 @@ const SortableProperties = ({ children }: SortablePropertiesProps) => { return; } const { active, over } = event; - const fromIndex = properties.findIndex(p => p.id === active.id); - const toIndex = properties.findIndex(p => p.id === over?.id); - - if (fromIndex !== -1 && toIndex !== -1 && fromIndex !== toIndex) { - manager.moveCustomProperty(fromIndex, toIndex); - setLocalProperties(manager.getOrderedCustomProperties()); + if (over) { + manager.sorter.move(active.id, over.id); } + setLocalProperties(manager.sorter.getOrderedItems()); }, - [manager, properties, draggable] + [manager, draggable] ); const filteredProperties = useMemo( @@ -636,7 +630,7 @@ export const PagePropertiesTableHeader = ({ onOpenChange(!open); }, [onOpenChange, open]); - const properties = manager.getOrderedCustomProperties(); + const properties = manager.sorter.getOrderedItems(); return (
diff --git a/packages/frontend/core/src/components/blocksuite/block-suite-header/favorite/index.tsx b/packages/frontend/core/src/components/blocksuite/block-suite-header/favorite/index.tsx index 409cd33287..c5d83200a8 100644 --- a/packages/frontend/core/src/components/blocksuite/block-suite-header/favorite/index.tsx +++ b/packages/frontend/core/src/components/blocksuite/block-suite-header/favorite/index.tsx @@ -1,10 +1,9 @@ import { FavoriteTag } from '@affine/core/components/page-list'; -import { useBlockSuiteMetaHelper } from '@affine/core/hooks/affine/use-block-suite-meta-helper'; -import { useBlockSuiteDocMeta } from '@affine/core/hooks/use-block-suite-page-meta'; +import { FavoriteItemsAdapter } from '@affine/core/modules/workspace'; import { toast } from '@affine/core/utils'; import { useAFFiNEI18N } from '@affine/i18n/hooks'; import { assertExists } from '@blocksuite/global/utils'; -import { useService, Workspace } from '@toeverything/infra'; +import { useLiveData, useService, Workspace } from '@toeverything/infra'; import { useCallback } from 'react'; export interface FavoriteButtonProps { @@ -16,24 +15,19 @@ export const useFavorite = (pageId: string) => { const workspace = useService(Workspace); const docCollection = workspace.docCollection; const currentPage = docCollection.getDoc(pageId); + const favAdapter = useService(FavoriteItemsAdapter); assertExists(currentPage); - const pageMeta = useBlockSuiteDocMeta(docCollection).find( - meta => meta.id === pageId - ); - const favorite = pageMeta?.favorite ?? false; - - const { toggleFavorite: _toggleFavorite } = - useBlockSuiteMetaHelper(docCollection); + const favorite = useLiveData(favAdapter.isFavorite$(pageId, 'doc')); const toggleFavorite = useCallback(() => { - _toggleFavorite(pageId); + favAdapter.toggle(pageId, 'doc'); toast( favorite ? t['com.affine.toastMessage.removedFavorites']() : t['com.affine.toastMessage.addedFavorites']() ); - }, [favorite, pageId, t, _toggleFavorite]); + }, [favorite, pageId, t, favAdapter]); return { favorite, toggleFavorite }; }; diff --git a/packages/frontend/core/src/components/page-list/docs/virtualized-page-list.tsx b/packages/frontend/core/src/components/page-list/docs/virtualized-page-list.tsx index 9d7b96572d..9265e0a37a 100644 --- a/packages/frontend/core/src/components/page-list/docs/virtualized-page-list.tsx +++ b/packages/frontend/core/src/components/page-list/docs/virtualized-page-list.tsx @@ -1,10 +1,8 @@ import { toast } from '@affine/component'; -import { useBlockSuiteMetaHelper } from '@affine/core/hooks/affine/use-block-suite-meta-helper'; import { useTrashModalHelper } from '@affine/core/hooks/affine/use-trash-modal-helper'; import { useBlockSuiteDocMeta } from '@affine/core/hooks/use-block-suite-page-meta'; import { CollectionService } from '@affine/core/modules/collection'; import type { Tag } from '@affine/core/modules/tag'; -import { Workbench } from '@affine/core/modules/workbench'; import type { Collection, Filter } from '@affine/env/filter'; import { Trans } from '@affine/i18n'; import { useAFFiNEI18N } from '@affine/i18n/hooks'; @@ -30,13 +28,7 @@ import { } from './page-list-header'; const usePageOperationsRenderer = () => { - const currentWorkspace = useService(Workspace); - const { setTrashModal } = useTrashModalHelper(currentWorkspace.docCollection); - const { toggleFavorite, duplicate } = useBlockSuiteMetaHelper( - currentWorkspace.docCollection - ); const t = useAFFiNEI18N(); - const workbench = useService(Workbench); const collectionService = useService(CollectionService); const removeFromAllowList = useCallback( (id: string) => { @@ -45,57 +37,18 @@ const usePageOperationsRenderer = () => { }, [collectionService, t] ); - const pageOperationsRenderer = useCallback( (page: DocMeta, isInAllowList?: boolean) => { - const onDisablePublicSharing = () => { - toast('Successfully disabled', { - portal: document.body, - }); - }; - return ( workbench.openPage(page.id, { at: 'tail' })} - onDuplicate={() => { - duplicate(page.id, false); - }} - onRemoveToTrash={() => - setTrashModal({ - open: true, - pageIds: [page.id], - pageTitles: [page.title], - }) - } - onToggleFavoritePage={() => { - const status = page.favorite; - toggleFavorite(page.id); - toast( - status - ? t['com.affine.toastMessage.removedFavorites']() - : t['com.affine.toastMessage.addedFavorites']() - ); - }} onRemoveFromAllowList={() => removeFromAllowList(page.id)} /> ); }, - [ - currentWorkspace.id, - workbench, - duplicate, - setTrashModal, - toggleFavorite, - t, - removeFromAllowList, - ] + [removeFromAllowList] ); - return pageOperationsRenderer; }; diff --git a/packages/frontend/core/src/components/page-list/group-definitions.tsx b/packages/frontend/core/src/components/page-list/group-definitions.tsx index 4d67d4bd39..0483580436 100644 --- a/packages/frontend/core/src/components/page-list/group-definitions.tsx +++ b/packages/frontend/core/src/components/page-list/group-definitions.tsx @@ -1,5 +1,6 @@ import type { Tag } from '@affine/core/modules/tag'; import { TagService } from '@affine/core/modules/tag'; +import { FavoriteItemsAdapter } from '@affine/core/modules/workspace'; import { useAFFiNEI18N } from '@affine/i18n/hooks'; import { FavoritedIcon, FavoriteIcon } from '@blocksuite/icons'; import type { DocMeta } from '@blocksuite/store'; @@ -154,6 +155,8 @@ export const useFavoriteGroupDefinitions = < T extends ListItem, >(): ItemGroupDefinition[] => { const t = useAFFiNEI18N(); + const favAdapter = useService(FavoriteItemsAdapter); + const favourites = useLiveData(favAdapter.favorites$); return useMemo( () => [ { @@ -166,7 +169,7 @@ export const useFavoriteGroupDefinitions = < icon={} /> ), - match: item => !!(item as DocMeta).favorite, + match: item => favourites.some(fav => fav.id === item.id), }, { id: 'notFavourited', @@ -178,10 +181,10 @@ export const useFavoriteGroupDefinitions = < icon={} /> ), - match: item => !(item as DocMeta).favorite, + match: item => !favourites.some(fav => fav.id === item.id), }, ], - [t] + [t, favourites] ); }; diff --git a/packages/frontend/core/src/components/page-list/operation-cell.tsx b/packages/frontend/core/src/components/page-list/operation-cell.tsx index 88028f8a46..acc18c6037 100644 --- a/packages/frontend/core/src/components/page-list/operation-cell.tsx +++ b/packages/frontend/core/src/components/page-list/operation-cell.tsx @@ -4,9 +4,14 @@ import { Menu, MenuIcon, MenuItem, + toast, Tooltip, } from '@affine/component'; import { useAppSettingHelper } from '@affine/core/hooks/affine/use-app-setting-helper'; +import { useBlockSuiteMetaHelper } from '@affine/core/hooks/affine/use-block-suite-meta-helper'; +import { useTrashModalHelper } from '@affine/core/hooks/affine/use-trash-modal-helper'; +import { Workbench } from '@affine/core/modules/workbench'; +import { FavoriteItemsAdapter } from '@affine/core/modules/workspace'; import type { Collection, DeleteCollectionInfo } from '@affine/env/filter'; import { useAFFiNEI18N } from '@affine/i18n/hooks'; import { @@ -23,6 +28,8 @@ import { ResetIcon, SplitViewIcon, } from '@blocksuite/icons'; +import type { DocMeta } from '@blocksuite/store'; +import { useLiveData, useService, Workspace } from '@toeverything/infra'; import { useCallback, useState } from 'react'; import { Link } from 'react-router-dom'; @@ -37,37 +44,61 @@ import type { AllPageListConfig } from './view'; import { useEditCollection, useEditCollectionName } from './view'; export interface PageOperationCellProps { - favorite: boolean; - isPublic: boolean; - link: string; + page: DocMeta; isInAllowList?: boolean; - onToggleFavoritePage: () => void; - onRemoveToTrash: () => void; - onDuplicate: () => void; - onDisablePublicSharing: () => void; - onOpenInSplitView: () => void; onRemoveFromAllowList?: () => void; } export const PageOperationCell = ({ - favorite, - isPublic, isInAllowList, - link, - onToggleFavoritePage, - onRemoveToTrash, - onDuplicate, - onDisablePublicSharing, - onOpenInSplitView, + page, onRemoveFromAllowList, }: PageOperationCellProps) => { const t = useAFFiNEI18N(); + const currentWorkspace = useService(Workspace); const { appSettings } = useAppSettingHelper(); + const { setTrashModal } = useTrashModalHelper(currentWorkspace.docCollection); const [openDisableShared, setOpenDisableShared] = useState(false); + const favAdapter = useService(FavoriteItemsAdapter); + const favourite = useLiveData(favAdapter.isFavorite$(page.id, 'doc')); + const workbench = useService(Workbench); + const { duplicate } = useBlockSuiteMetaHelper(currentWorkspace.docCollection); + + const onDisablePublicSharing = useCallback(() => { + toast('Successfully disabled', { + portal: document.body, + }); + }, []); + + const onRemoveToTrash = useCallback(() => { + setTrashModal({ + open: true, + pageIds: [page.id], + pageTitles: [page.title], + }); + }, [page.id, page.title, setTrashModal]); + + const onOpenInSplitView = useCallback(() => { + workbench.openPage(page.id, { at: 'tail' }); + }, [page.id, workbench]); + + const onToggleFavoritePage = useCallback(() => { + const status = favAdapter.isFavorite(page.id, 'doc'); + favAdapter.toggle(page.id, 'doc'); + toast( + status + ? t['com.affine.toastMessage.removedFavorites']() + : t['com.affine.toastMessage.addedFavorites']() + ); + }, [page.id, favAdapter, t]); + + const onDuplicate = useCallback(() => { + duplicate(page.id, false); + }, [duplicate, page.id]); const OperationMenu = ( <> - {isPublic && ( + {page.isPublic && ( { @@ -91,7 +122,7 @@ export const PageOperationCell = ({ onClick={onToggleFavoritePage} preFix={ - {favorite ? ( + {favourite ? ( ) : ( @@ -99,7 +130,7 @@ export const PageOperationCell = ({ } > - {favorite + {favourite ? t['com.affine.favoritePageOperation.remove']() : t['com.affine.favoritePageOperation.add']()} @@ -121,7 +152,7 @@ export const PageOperationCell = ({ @@ -157,10 +188,10 @@ export const PageOperationCell = ({ - + export type PageDataForFilter = { meta: DocMeta; + favorite: boolean; publicMode: undefined | 'page' | 'edgeless'; }; @@ -33,13 +34,13 @@ export const filterPage = (collection: Collection, page: PageDataForFilter) => { export const filterPageByRules = ( rules: Filter[], allowList: string[], - { meta, publicMode }: PageDataForFilter + { meta, publicMode, favorite }: PageDataForFilter ) => { if (allowList?.includes(meta.id)) { return true; } return filterByFilterList(rules, { - 'Is Favourited': !!meta.favorite, + 'Is Favourited': !!favorite, 'Is Public': !!publicMode, Created: meta.createDate, Updated: meta.updatedDate ?? meta.createDate, diff --git a/packages/frontend/core/src/components/page-list/use-filtered-page-metas.tsx b/packages/frontend/core/src/components/page-list/use-filtered-page-metas.tsx index 92346d0d60..c190d857ce 100644 --- a/packages/frontend/core/src/components/page-list/use-filtered-page-metas.tsx +++ b/packages/frontend/core/src/components/page-list/use-filtered-page-metas.tsx @@ -1,6 +1,7 @@ +import { FavoriteItemsAdapter } from '@affine/core/modules/workspace'; import type { Collection, Filter } from '@affine/env/filter'; import type { DocMeta } from '@blocksuite/store'; -import type { Workspace } from '@toeverything/infra'; +import { useLiveData, useService, type Workspace } from '@toeverything/infra'; import { useMemo } from 'react'; import { usePublicPages } from '../../hooks/affine/use-is-shared-page'; @@ -16,6 +17,8 @@ export const useFilteredPageMetas = ( } = {} ) => { const { getPublicMode } = usePublicPages(workspace); + const favAdapter = useService(FavoriteItemsAdapter); + const favoriteItems = useLiveData(favAdapter.favorites$); const filteredPageMetas = useMemo( () => @@ -29,6 +32,7 @@ export const useFilteredPageMetas = ( } const pageData = { meta: pageMeta, + favorite: favoriteItems.some(fav => fav.id === pageMeta.id), publicMode: getPublicMode(pageMeta.id), }; if ( @@ -49,6 +53,7 @@ export const useFilteredPageMetas = ( options.trash, options.filters, options.collection, + favoriteItems, getPublicMode, ] ); diff --git a/packages/frontend/core/src/components/page-list/view/edit-collection/pages-mode.tsx b/packages/frontend/core/src/components/page-list/view/edit-collection/pages-mode.tsx index a4a2b1e64a..c26cb6a2c1 100644 --- a/packages/frontend/core/src/components/page-list/view/edit-collection/pages-mode.tsx +++ b/packages/frontend/core/src/components/page-list/view/edit-collection/pages-mode.tsx @@ -1,8 +1,10 @@ import { Menu } from '@affine/component'; +import { FavoriteItemsAdapter } from '@affine/core/modules/workspace'; import type { Collection } from '@affine/env/filter'; import { useAFFiNEI18N } from '@affine/i18n/hooks'; import { FilterIcon } from '@blocksuite/icons'; import type { DocMeta } from '@blocksuite/store'; +import { useLiveData, useService } from '@toeverything/infra'; import clsx from 'clsx'; import type { ReactNode } from 'react'; import { useCallback } from 'react'; @@ -34,6 +36,8 @@ export const PagesMode = ({ allPageListConfig: AllPageListConfig; }) => { const t = useAFFiNEI18N(); + const favAdapter = useService(FavoriteItemsAdapter); + const favorites = useLiveData(favAdapter.favorites$); const { showFilter, filters, @@ -45,6 +49,7 @@ export const PagesMode = ({ allPageListConfig.allPages.map(meta => ({ meta, publicMode: allPageListConfig.getPublicMode(meta.id), + favorite: favorites.some(f => f.id === meta.id), })) ); const pageHeaderColsDef = usePageHeaderColsDef(); diff --git a/packages/frontend/core/src/components/page-list/view/edit-collection/rules-mode.tsx b/packages/frontend/core/src/components/page-list/view/edit-collection/rules-mode.tsx index d27f127ede..5a868bba86 100644 --- a/packages/frontend/core/src/components/page-list/view/edit-collection/rules-mode.tsx +++ b/packages/frontend/core/src/components/page-list/view/edit-collection/rules-mode.tsx @@ -1,3 +1,4 @@ +import { FavoriteItemsAdapter } from '@affine/core/modules/workspace'; import type { Collection } from '@affine/env/filter'; import { Trans } from '@affine/i18n'; import { useAFFiNEI18N } from '@affine/i18n/hooks'; @@ -9,6 +10,7 @@ import { ToggleCollapseIcon, } from '@blocksuite/icons'; import type { DocMeta } from '@blocksuite/store'; +import { useLiveData, useService } from '@toeverything/infra'; import clsx from 'clsx'; import type { ReactNode } from 'react'; import { useCallback, useEffect, useState } from 'react'; @@ -42,6 +44,8 @@ export const RulesMode = ({ const allowListPages: DocMeta[] = []; const rulesPages: DocMeta[] = []; const [showTips, setShowTips] = useState(false); + const favAdapter = useService(FavoriteItemsAdapter); + const favorites = useLiveData(favAdapter.favorites$); useEffect(() => { setShowTips(!localStorage.getItem('hide-rules-mode-include-page-tips')); }, []); @@ -56,6 +60,7 @@ export const RulesMode = ({ const pageData = { meta, publicMode: allPageListConfig.getPublicMode(meta.id), + favorite: favorites.some(f => f.id === meta.id), }; if ( collection.filterList.length && diff --git a/packages/frontend/core/src/components/page-list/view/edit-collection/select-page.tsx b/packages/frontend/core/src/components/page-list/view/edit-collection/select-page.tsx index 404a95e726..19ea9bdf62 100644 --- a/packages/frontend/core/src/components/page-list/view/edit-collection/select-page.tsx +++ b/packages/frontend/core/src/components/page-list/view/edit-collection/select-page.tsx @@ -1,8 +1,10 @@ import { Button, Menu } from '@affine/component'; +import { FavoriteItemsAdapter } from '@affine/core/modules/workspace'; import { Trans } from '@affine/i18n'; import { useAFFiNEI18N } from '@affine/i18n/hooks'; import { FilterIcon } from '@blocksuite/icons'; import type { DocMeta } from '@blocksuite/store'; +import { useLiveData, useService } from '@toeverything/infra'; import clsx from 'clsx'; import { useCallback, useState } from 'react'; @@ -35,6 +37,8 @@ export const SelectPage = ({ const clearSelected = useCallback(() => { onChange([]); }, []); + const favAdapter = useService(FavoriteItemsAdapter); + const favourites = useLiveData(favAdapter.favorites$); const { clickFilter, createFilter, @@ -46,6 +50,7 @@ export const SelectPage = ({ allPageListConfig.allPages.map(meta => ({ meta, publicMode: allPageListConfig.getPublicMode(meta.id), + favorite: favourites.some(fav => fav.id === meta.id), })) ); const { searchText, updateSearchText, searchedList } = diff --git a/packages/frontend/core/src/components/pure/workspace-slider-bar/collections/collections-list.tsx b/packages/frontend/core/src/components/pure/workspace-slider-bar/collections/collections-list.tsx index c72b18d243..6aa9aa8de5 100644 --- a/packages/frontend/core/src/components/pure/workspace-slider-bar/collections/collections-list.tsx +++ b/packages/frontend/core/src/components/pure/workspace-slider-bar/collections/collections-list.tsx @@ -7,6 +7,7 @@ import { stopPropagation, } from '@affine/core/components/page-list'; import { CollectionService } from '@affine/core/modules/collection'; +import { FavoriteItemsAdapter } from '@affine/core/modules/workspace'; import type { Collection, DeleteCollectionInfo } from '@affine/env/filter'; import { useAFFiNEI18N } from '@affine/i18n/hooks'; import { MoreHorizontalIcon, ViewLayersIcon } from '@blocksuite/icons'; @@ -40,9 +41,12 @@ const CollectionRenderer = ({ const [collapsed, setCollapsed] = useState(true); const [open, setOpen] = useState(false); const collectionService = useService(CollectionService); + const favAdapter = useService(FavoriteItemsAdapter); const t = useAFFiNEI18N(); const dragItemId = getDropItemId('collections', collection.id); + const favourites = useLiveData(favAdapter.favorites$); + const removeFromAllowList = useCallback( (id: string) => { collectionService.updateCollection(collection.id, () => ({ @@ -85,6 +89,7 @@ const CollectionRenderer = ({ const pageData = { meta, publicMode: config.getPublicMode(meta.id), + favorite: favourites.some(fav => fav.id === meta.id), }; return filterPage(collection, pageData); }); diff --git a/packages/frontend/core/src/components/pure/workspace-slider-bar/components/operation-menu-button.tsx b/packages/frontend/core/src/components/pure/workspace-slider-bar/components/operation-menu-button.tsx index 8bc31429fd..2e6b0613dd 100644 --- a/packages/frontend/core/src/components/pure/workspace-slider-bar/components/operation-menu-button.tsx +++ b/packages/frontend/core/src/components/pure/workspace-slider-bar/components/operation-menu-button.tsx @@ -2,13 +2,13 @@ import { toast } from '@affine/component'; import { IconButton } from '@affine/component/ui/button'; import { Menu } from '@affine/component/ui/menu'; import { Workbench } from '@affine/core/modules/workbench'; +import { FavoriteItemsAdapter } from '@affine/core/modules/workspace'; import { useAFFiNEI18N } from '@affine/i18n/hooks'; import { MoreHorizontalIcon } from '@blocksuite/icons'; import type { DocCollection } from '@blocksuite/store'; import { useService } from '@toeverything/infra'; import { useCallback } from 'react'; -import { useBlockSuiteMetaHelper } from '../../../../hooks/affine/use-block-suite-meta-helper'; import { useTrashModalHelper } from '../../../../hooks/affine/use-trash-modal-helper'; import { usePageHelper } from '../../../blocksuite/block-suite-page-list/utils'; import { OperationItems } from './operation-item'; @@ -38,7 +38,8 @@ export const OperationMenuButton = ({ ...props }: OperationMenuButtonProps) => { const t = useAFFiNEI18N(); const { createLinkedPage } = usePageHelper(docCollection); const { setTrashModal } = useTrashModalHelper(docCollection); - const { removeFromFavorite } = useBlockSuiteMetaHelper(docCollection); + + const favAdapter = useService(FavoriteItemsAdapter); const workbench = useService(Workbench); const handleRename = useCallback(() => { @@ -51,9 +52,9 @@ export const OperationMenuButton = ({ ...props }: OperationMenuButtonProps) => { }, [createLinkedPage, pageId, t]); const handleRemoveFromFavourites = useCallback(() => { - removeFromFavorite(pageId); + favAdapter.remove(pageId, 'doc'); toast(t['com.affine.toastMessage.removedFavorites']()); - }, [pageId, removeFromFavorite, t]); + }, [favAdapter, pageId, t]); const handleDelete = useCallback(() => { setTrashModal({ diff --git a/packages/frontend/core/src/components/pure/workspace-slider-bar/favorite/add-favourite-button.tsx b/packages/frontend/core/src/components/pure/workspace-slider-bar/favorite/add-favourite-button.tsx index 3bc67ca8b1..a9e8f35d3a 100644 --- a/packages/frontend/core/src/components/pure/workspace-slider-bar/favorite/add-favourite-button.tsx +++ b/packages/frontend/core/src/components/pure/workspace-slider-bar/favorite/add-favourite-button.tsx @@ -1,8 +1,9 @@ import { IconButton } from '@affine/component/ui/button'; import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks'; -import { useDocMetaHelper } from '@affine/core/hooks/use-block-suite-page-meta'; +import { FavoriteItemsAdapter } from '@affine/core/modules/workspace'; import { PlusIcon } from '@blocksuite/icons'; import type { DocCollection } from '@blocksuite/store'; +import { useService } from '@toeverything/infra'; import { usePageHelper } from '../../../blocksuite/block-suite-page-list/utils'; @@ -16,7 +17,7 @@ export const AddFavouriteButton = ({ pageId, }: AddFavouriteButtonProps) => { const { createPage, createLinkedPage } = usePageHelper(docCollection); - const { setDocMeta } = useDocMetaHelper(docCollection); + const favAdapter = useService(FavoriteItemsAdapter); const handleAddFavorite = useAsyncCallback( async e => { if (pageId) { @@ -26,10 +27,10 @@ export const AddFavouriteButton = ({ } else { const page = createPage(); page.load(); - setDocMeta(page.id, { favorite: true }); + favAdapter.set(page.id, 'doc', true); } }, - [pageId, createLinkedPage, createPage, setDocMeta] + [pageId, createLinkedPage, createPage, favAdapter] ); return ( diff --git a/packages/frontend/core/src/components/pure/workspace-slider-bar/favorite/favorite-list.tsx b/packages/frontend/core/src/components/pure/workspace-slider-bar/favorite/favorite-list.tsx index b2ed986642..26706ee5a6 100644 --- a/packages/frontend/core/src/components/pure/workspace-slider-bar/favorite/favorite-list.tsx +++ b/packages/frontend/core/src/components/pure/workspace-slider-bar/favorite/favorite-list.tsx @@ -1,6 +1,8 @@ import { useBlockSuiteDocMeta } from '@affine/core/hooks/use-block-suite-page-meta'; +import { FavoriteItemsAdapter } from '@affine/core/modules/workspace'; import type { DocMeta } from '@blocksuite/store'; import { useDroppable } from '@dnd-kit/core'; +import { useLiveData, useService } from '@toeverything/infra'; import { useMemo } from 'react'; import { getDropItemId } from '../../../../hooks/affine/use-sidebar-drag'; @@ -15,11 +17,16 @@ export const FavoriteList = ({ docCollection: workspace, }: FavoriteListProps) => { const metas = useBlockSuiteDocMeta(workspace); + const favAdapter = useService(FavoriteItemsAdapter); const dropItemId = getDropItemId('favorites'); - const favoriteList = useMemo( - () => metas.filter(p => p.favorite && !p.trash), - [metas] + const favourites = useLiveData( + favAdapter.favorites$.map(favourites => + favourites.filter(fav => { + const meta = metas.find(m => m.id === fav.id); + return meta && !meta.trash; + }) + ) ); const metaMapping = useMemo( @@ -45,7 +52,7 @@ export const FavoriteList = ({ ref={setNodeRef} data-over={isOver} > - {favoriteList.map((pageMeta, index) => { + {favourites.map((pageMeta, index) => { return ( ); })} - {favoriteList.length === 0 && } + {favourites.length === 0 && }
); }; diff --git a/packages/frontend/core/src/hooks/affine/use-all-page-list-config.tsx b/packages/frontend/core/src/hooks/affine/use-all-page-list-config.tsx index 821717f3d1..99d8643437 100644 --- a/packages/frontend/core/src/hooks/affine/use-all-page-list-config.tsx +++ b/packages/frontend/core/src/hooks/affine/use-all-page-list-config.tsx @@ -2,13 +2,13 @@ import { toast } from '@affine/component'; import type { AllPageListConfig } from '@affine/core/components/page-list'; import { FavoriteTag } from '@affine/core/components/page-list'; import { useBlockSuiteDocMeta } from '@affine/core/hooks/use-block-suite-page-meta'; +import { FavoriteItemsAdapter } from '@affine/core/modules/workspace'; import { useAFFiNEI18N } from '@affine/i18n/hooks'; import type { DocMeta } from '@blocksuite/store'; -import { useService, Workspace } from '@toeverything/infra'; +import { useLiveData, useService, Workspace } from '@toeverything/infra'; import { useCallback, useMemo } from 'react'; import { usePageHelper } from '../../components/blocksuite/block-suite-page-list/utils'; -import { useBlockSuiteMetaHelper } from './use-block-suite-meta-helper'; import { usePublicPages } from './use-is-shared-page'; export const useAllPageListConfig = () => { @@ -21,22 +21,29 @@ export const useAllPageListConfig = () => { () => Object.fromEntries(pageMetas.map(page => [page.id, page])), [pageMetas] ); - const { toggleFavorite } = useBlockSuiteMetaHelper( - currentWorkspace.docCollection - ); + const favAdapter = useService(FavoriteItemsAdapter); const t = useAFFiNEI18N(); + const favoriteItems = useLiveData(favAdapter.favorites$); + + const isActive = useCallback( + (page: DocMeta) => { + return favoriteItems.some(fav => fav.id === page.id); + }, + [favoriteItems] + ); const onToggleFavoritePage = useCallback( (page: DocMeta) => { - const status = page.favorite; - toggleFavorite(page.id); + const status = isActive(page); + favAdapter.toggle(page.id, 'doc'); toast( status ? t['com.affine.toastMessage.removedFavorites']() : t['com.affine.toastMessage.addedFavorites']() ); }, - [t, toggleFavorite] + [favAdapter, isActive, t] ); + return useMemo(() => { return { allPages: pageMetas, @@ -49,7 +56,7 @@ export const useAllPageListConfig = () => { onToggleFavoritePage(page)} - active={!!page.favorite} + active={isActive(page)} /> ); }, @@ -60,6 +67,7 @@ export const useAllPageListConfig = () => { getPublicMode, currentWorkspace.docCollection, pageMap, + isActive, onToggleFavoritePage, ]); }; diff --git a/packages/frontend/core/src/hooks/affine/use-block-suite-meta-helper.ts b/packages/frontend/core/src/hooks/affine/use-block-suite-meta-helper.ts index cc48ca589b..273249acc0 100644 --- a/packages/frontend/core/src/hooks/affine/use-block-suite-meta-helper.ts +++ b/packages/frontend/core/src/hooks/affine/use-block-suite-meta-helper.ts @@ -19,32 +19,6 @@ export function useBlockSuiteMetaHelper(docCollection: DocCollection) { const collectionService = useService(CollectionService); const pageRecordList = useService(PageRecordList); - const addToFavorite = useCallback( - (pageId: string) => { - setDocMeta(pageId, { - favorite: true, - }); - }, - [setDocMeta] - ); - const removeFromFavorite = useCallback( - (pageId: string) => { - setDocMeta(pageId, { - favorite: false, - }); - }, - [setDocMeta] - ); - const toggleFavorite = useCallback( - (pageId: string) => { - const { favorite } = getDocMeta(pageId) ?? {}; - setDocMeta(pageId, { - favorite: !favorite, - }); - }, - [getDocMeta, setDocMeta] - ); - // TODO-Doma // "Remove" may cause ambiguity here. Consider renaming as "moveToTrash". const removeToTrash = useCallback( @@ -126,7 +100,6 @@ export function useBlockSuiteMetaHelper(docCollection: DocCollection) { setDocMeta(newPage.id, { tags: currentPageMeta.tags, - favorite: currentPageMeta.favorite, }); const lastDigitRegex = /\((\d+)\)$/; @@ -157,10 +130,6 @@ export function useBlockSuiteMetaHelper(docCollection: DocCollection) { publicPage, cancelPublicPage, - addToFavorite, - removeFromFavorite, - toggleFavorite, - removeToTrash, restoreFromTrash, permanentlyDeletePage, diff --git a/packages/frontend/core/src/hooks/affine/use-register-blocksuite-editor-commands.tsx b/packages/frontend/core/src/hooks/affine/use-register-blocksuite-editor-commands.tsx index 8a800e9626..d3220ba1de 100644 --- a/packages/frontend/core/src/hooks/affine/use-register-blocksuite-editor-commands.tsx +++ b/packages/frontend/core/src/hooks/affine/use-register-blocksuite-editor-commands.tsx @@ -1,5 +1,6 @@ import { toast } from '@affine/component'; import { useDocMetaHelper } from '@affine/core/hooks/use-block-suite-page-meta'; +import { FavoriteItemsAdapter } from '@affine/core/modules/workspace'; import { WorkspaceFlavour } from '@affine/env/workspace'; import { useAFFiNEI18N } from '@affine/i18n/hooks'; import { assertExists } from '@blocksuite/global/utils'; @@ -32,7 +33,9 @@ export function useRegisterBlocksuiteEditorCommands() { assertExists(currentPage); const pageMeta = getDocMeta(pageId); assertExists(pageMeta); - const favorite = pageMeta.favorite ?? false; + + const favAdapter = useService(FavoriteItemsAdapter); + const favorite = useLiveData(favAdapter.isFavorite$(pageId, 'doc')); const trash = pageMeta.trash ?? false; const setPageHistoryModalState = useSetAtom(pageHistoryModalAtom); @@ -44,7 +47,7 @@ export function useRegisterBlocksuiteEditorCommands() { })); }, [pageId, setPageHistoryModalState]); - const { toggleFavorite, restoreFromTrash, duplicate } = + const { restoreFromTrash, duplicate } = useBlockSuiteMetaHelper(docCollection); const exportHandler = useExportPage(currentPage); const { setTrashModal } = useTrashModalHelper(docCollection); @@ -94,7 +97,7 @@ export function useRegisterBlocksuiteEditorCommands() { ? t['com.affine.favoritePageOperation.remove']() : t['com.affine.favoritePageOperation.add'](), run() { - toggleFavorite(pageId); + favAdapter.toggle(pageId, 'doc'); toast( favorite ? t['com.affine.cmdk.affine.editor.remove-from-favourites']() @@ -246,11 +249,11 @@ export function useRegisterBlocksuiteEditorCommands() { pageId, restoreFromTrash, t, - toggleFavorite, trash, isCloudWorkspace, openHistoryModal, duplicate, page, + favAdapter, ]); } diff --git a/packages/frontend/core/src/hooks/affine/use-sidebar-drag.ts b/packages/frontend/core/src/hooks/affine/use-sidebar-drag.ts index 900d4c3f72..654898e804 100644 --- a/packages/frontend/core/src/hooks/affine/use-sidebar-drag.ts +++ b/packages/frontend/core/src/hooks/affine/use-sidebar-drag.ts @@ -1,12 +1,12 @@ import { toast } from '@affine/component'; import type { DraggableTitleCellData } from '@affine/core/components/page-list'; import { useDocMetaHelper } from '@affine/core/hooks/use-block-suite-page-meta'; +import { FavoriteItemsAdapter } from '@affine/core/modules/workspace'; import { useAFFiNEI18N } from '@affine/i18n/hooks'; import type { DragEndEvent, UniqueIdentifier } from '@dnd-kit/core'; import { useService, Workspace } from '@toeverything/infra'; import { useCallback } from 'react'; -import { useBlockSuiteMetaHelper } from './use-block-suite-meta-helper'; import { useTrashModalHelper } from './use-trash-modal-helper'; // Unique droppable IDs @@ -69,10 +69,10 @@ export function getDragItemId( export const useSidebarDrag = () => { const t = useAFFiNEI18N(); const currentWorkspace = useService(Workspace); + const favAdapter = useService(FavoriteItemsAdapter); const workspace = currentWorkspace.docCollection; const { setTrashModal } = useTrashModalHelper(workspace); - const { addToFavorite, removeFromFavorite } = - useBlockSuiteMetaHelper(workspace); + const { getDocMeta } = useDocMetaHelper(workspace); const isDropArea = useCallback( @@ -123,7 +123,7 @@ export const useSidebarDrag = () => { const processFavouritesDrag = useCallback( (e: DragEndEvent) => { const { pageId } = e.active.data.current as DraggableTitleCellData; - const isFavourited = getDocMeta(pageId)?.favorite; + const isFavourited = favAdapter.isFavorite(pageId, 'doc'); const isFavouriteDrag = String(e.over?.id).startsWith( DropPrefix.SidebarFavorites ); @@ -131,11 +131,11 @@ export const useSidebarDrag = () => { return toast(t['com.affine.collection.addPage.alreadyExists']()); } processDrag(e, DropPrefix.SidebarFavorites, pageId => { - addToFavorite(pageId); + favAdapter.set(pageId, 'doc', true); toast(t['com.affine.cmdk.affine.editor.add-to-favourites']()); }); }, - [getDocMeta, processDrag, addToFavorite, t] + [processDrag, t, favAdapter] ); const processRemoveDrag = useCallback( @@ -146,7 +146,7 @@ export const useSidebarDrag = () => { if (String(e.active.id).startsWith(DragPrefix.FavouriteListItem)) { const pageId = e.active.data.current?.pageId; - removeFromFavorite(pageId); + favAdapter.remove(pageId, 'doc'); toast(t['com.affine.cmdk.affine.editor.remove-from-favourites']()); return; } @@ -155,7 +155,7 @@ export const useSidebarDrag = () => { } }, - [removeFromFavorite, t] + [favAdapter, t] ); return useCallback( diff --git a/packages/frontend/core/src/modules/services.ts b/packages/frontend/core/src/modules/services.ts index 0c31441797..849f19c04f 100644 --- a/packages/frontend/core/src/modules/services.ts +++ b/packages/frontend/core/src/modules/services.ts @@ -18,6 +18,7 @@ import { TagService } from './tag'; import { Workbench } from './workbench'; import { CurrentWorkspaceService, + FavoriteItemsAdapter, WorkspaceLegacyProperties, WorkspacePropertiesAdapter, } from './workspace'; @@ -30,6 +31,7 @@ export function configureBusinessServices(services: ServiceCollection) { .add(Navigator, [Workbench]) .add(RightSidebar, [GlobalState]) .add(WorkspacePropertiesAdapter, [Workspace]) + .add(FavoriteItemsAdapter, [WorkspacePropertiesAdapter]) .add(CollectionService, [Workspace]) .add(WorkspaceLegacyProperties, [Workspace]) .add(TagService, [WorkspaceLegacyProperties, PageRecordList]); diff --git a/packages/frontend/core/src/modules/workspace/properties/adapter.ts b/packages/frontend/core/src/modules/workspace/properties/adapter.ts index 00bcc043fb..9cd65f9170 100644 --- a/packages/frontend/core/src/modules/workspace/properties/adapter.ts +++ b/packages/frontend/core/src/modules/workspace/properties/adapter.ts @@ -1,16 +1,17 @@ /* eslint-disable @typescript-eslint/no-non-null-assertion */ // the adapter is to bridge the workspace rootdoc & native js bindings - -import type { Y } from '@blocksuite/store'; -import { createYProxy } from '@blocksuite/store'; -import type { Workspace } from '@toeverything/infra'; +import { createFractionalIndexingSortableHelper } from '@affine/core/utils'; +import { createYProxy, type Y } from '@blocksuite/store'; +import { LiveData, type Workspace } from '@toeverything/infra'; import { defaultsDeep } from 'lodash-es'; +import { Observable } from 'rxjs'; -import type { - WorkspaceAffineProperties, - WorkspaceFavoriteItem, +import { + PagePropertyType, + PageSystemPropertyId, + type WorkspaceAffineProperties, + type WorkspaceFavoriteItem, } from './schema'; -import { PagePropertyType, PageSystemPropertyId } from './schema'; const AFFINE_PROPERTIES_ID = 'affine:workspace-properties'; @@ -27,6 +28,7 @@ export class WorkspacePropertiesAdapter { // provides a easy-to-use interface for workspace properties public readonly proxy: WorkspaceAffineProperties; public readonly properties: Y.Map; + public readonly properties$: LiveData; private ensuredRoot = false; private ensuredPages = {} as Record; @@ -36,9 +38,25 @@ export class WorkspacePropertiesAdapter { const rootDoc = workspace.docCollection.doc; this.properties = rootDoc.getMap(AFFINE_PROPERTIES_ID); this.proxy = createYProxy(this.properties); + + this.properties$ = LiveData.from( + new Observable(observer => { + const update = () => { + requestAnimationFrame(() => { + observer.next(new Proxy(this.proxy, {})); + }); + }; + update(); + this.properties.observeDeep(update); + return () => { + this.properties.unobserveDeep(update); + }; + }), + this.proxy + ); } - private ensureRootProperties() { + public ensureRootProperties() { if (this.ensuredRoot) { return; } @@ -120,10 +138,6 @@ export class WorkspacePropertiesAdapter { return this.pageProperties?.[pageId] ?? null; } - isFavorite(id: string, type: WorkspaceFavoriteItem['type']) { - return this.favorites?.[id]?.type === type; - } - getJournalPageDateString(id: string) { return this.pageProperties?.[id]?.system[PageSystemPropertyId.Journal] ?.value; @@ -135,3 +149,119 @@ export class WorkspacePropertiesAdapter { pageProperties!.system[PageSystemPropertyId.Journal].value = date; } } + +export class FavoriteItemsAdapter { + constructor(private readonly adapter: WorkspacePropertiesAdapter) { + this.migrateFavorites(); + } + + readonly sorter = createFractionalIndexingSortableHelper< + WorkspaceFavoriteItem, + string + >(this); + + static getFavItemKey(id: string, type: WorkspaceFavoriteItem['type']) { + return `${type}:${id}`; + } + + favorites$ = this.adapter.properties$.map(() => + this.getItems().filter(i => i.value) + ); + + getItems() { + return Object.values(this.adapter.favorites ?? {}); + } + + get favorites() { + return this.adapter.favorites; + } + + get workspace() { + return this.adapter.workspace; + } + + getItemId(item: WorkspaceFavoriteItem) { + return item.id; + } + + getItemOrder(item: WorkspaceFavoriteItem) { + return item.order; + } + + setItemOrder(item: WorkspaceFavoriteItem, order: string) { + item.order = order; + } + + // read from the workspace meta and migrate to the properties + private migrateFavorites() { + // only migrate if favorites is empty + if (Object.keys(this.favorites ?? {}).length > 0) { + return; + } + + // old favorited pages + const oldFavorites = this.workspace.docCollection.meta.docMetas + .filter(meta => meta.favorite) + .map(meta => meta.id); + + this.adapter.transact(() => { + for (const id of oldFavorites) { + this.set(id, 'doc', true); + } + }); + } + + isFavorite(id: string, type: WorkspaceFavoriteItem['type']) { + const existing = this.getFavoriteItem(id, type); + return existing?.value ?? false; + } + + isFavorite$(id: string, type: WorkspaceFavoriteItem['type']) { + return this.favorites$.map(() => { + return this.isFavorite(id, type); + }); + } + + private getFavoriteItem(id: string, type: WorkspaceFavoriteItem['type']) { + return this.favorites?.[FavoriteItemsAdapter.getFavItemKey(id, type)]; + } + + // add or set a new fav item to the list. note the id added with prefix + set( + id: string, + type: WorkspaceFavoriteItem['type'], + value: boolean, + order?: string + ) { + this.adapter.ensureRootProperties(); + if (!this.favorites) { + throw new Error('Favorites is not initialized'); + } + const existing = this.getFavoriteItem(id, type); + if (!existing) { + this.favorites[FavoriteItemsAdapter.getFavItemKey(id, type)] = { + id, + type, + value: true, + order: order ?? this.sorter.getNewItemOrder(), + }; + } else { + Object.assign(existing, { + value, + order: order ?? existing.order, + }); + } + } + + toggle(id: string, type: WorkspaceFavoriteItem['type']) { + this.set(id, type, !this.isFavorite(id, type)); + } + + remove(id: string, type: WorkspaceFavoriteItem['type']) { + this.adapter.ensureRootProperties(); + const existing = this.getFavoriteItem(id, type); + if (existing) { + existing.value = false; + } + } +} diff --git a/packages/frontend/core/src/modules/workspace/properties/schema.ts b/packages/frontend/core/src/modules/workspace/properties/schema.ts index f07671b859..95e306fbe9 100644 --- a/packages/frontend/core/src/modules/workspace/properties/schema.ts +++ b/packages/frontend/core/src/modules/workspace/properties/schema.ts @@ -64,8 +64,9 @@ export type PageInfoTagsItem = z.infer; // ====== workspace properties schema ====== export const WorkspaceFavoriteItemSchema = z.object({ id: z.string(), - order: z.number(), - type: z.enum(['page', 'collection']), + order: z.string(), + type: z.enum(['doc', 'collection']), + value: z.boolean(), }); export type WorkspaceFavoriteItem = z.infer; diff --git a/packages/frontend/core/src/utils/fractional-indexing.ts b/packages/frontend/core/src/utils/fractional-indexing.ts new file mode 100644 index 0000000000..30b5a5aabe --- /dev/null +++ b/packages/frontend/core/src/utils/fractional-indexing.ts @@ -0,0 +1,113 @@ +import { generateKeyBetween } from 'fractional-indexing'; + +export interface SortableProvider { + getItems(): T[]; + getItemId(item: T): K; + getItemOrder(item: T): string; + setItemOrder(item: T, order: string): void; +} + +// Using fractional-indexing managing orders of items in a list +export function createFractionalIndexingSortableHelper< + T, + K extends string | number, +>(provider: SortableProvider) { + function getOrderedItems() { + return provider.getItems().sort((a, b) => { + const oa = provider.getItemOrder(a); + const ob = provider.getItemOrder(b); + return oa > ob ? 1 : oa < ob ? -1 : 0; + }); + } + + function getLargestOrder() { + const lastItem = getOrderedItems().at(-1); + return lastItem ? provider.getItemOrder(lastItem) : null; + } + + function getSmallestOrder() { + const firstItem = getOrderedItems().at(0); + return firstItem ? provider.getItemOrder(firstItem) : null; + } + + /** + * Get a new order at the end of the list + */ + function getNewItemOrder() { + return generateKeyBetween(getLargestOrder(), null); + } + + /** + * Move item from one position to another + * + * in the most common sorting case, moving over will visually place the dragging item to the target position + * the original item in the target position will either move up or down, depending on the direction of the drag + * + * @param fromId + * @param toId + */ + function move(fromId: K, toId: K) { + const items = getOrderedItems(); + const from = items.findIndex(i => provider.getItemId(i) === fromId); + const to = items.findIndex(i => provider.getItemId(i) === toId); + const fromItem = items[from]; + const toItem = items[to]; + const toNextItem = items[from < to ? to + 1 : to - 1]; + const toOrder = toItem ? provider.getItemOrder(toItem) : null; + const toNextOrder = toNextItem ? provider.getItemOrder(toNextItem) : null; + const args: [string | null, string | null] = + from < to ? [toOrder, toNextOrder] : [toNextOrder, toOrder]; + provider.setItemOrder(fromItem, generateKeyBetween(...args)); + } + + /** + * Cases example: + * Imagine we have the following items, | a | b | c | + * 1. insertBefore('b', undefined). before is not provided, which means insert b after c + * | a | c | + * ▴ + * b + * result: | a | c | b | + * + * 2. insertBefore('b', 'a'). insert b before a + * | a | c | + * ▴ + * b + * + * result: | b | a | c | + */ + function insertBefore( + id: string | number, + beforeId: string | number | undefined + ) { + const items = getOrderedItems(); + // assert id is in the list + const item = items.find(i => provider.getItemId(i) === id); + if (!item) return; + + const beforeItemIndex = items.findIndex( + i => provider.getItemId(i) === beforeId + ); + const beforeItem = beforeItemIndex !== -1 ? items[beforeItemIndex] : null; + const beforeItemPrev = beforeItem ? items[beforeItemIndex - 1] : null; + + const beforeOrder = beforeItem ? provider.getItemOrder(beforeItem) : null; + const beforePrevOrder = beforeItemPrev + ? provider.getItemOrder(beforeItemPrev) + : null; + + provider.setItemOrder( + item, + generateKeyBetween(beforePrevOrder, beforeOrder) + ); + } + + return { + getOrderedItems, + getLargestOrder, + getSmallestOrder, + getNewItemOrder, + move, + insertBefore, + }; +} diff --git a/packages/frontend/core/src/utils/index.ts b/packages/frontend/core/src/utils/index.ts index a2defbb180..90e1777b92 100644 --- a/packages/frontend/core/src/utils/index.ts +++ b/packages/frontend/core/src/utils/index.ts @@ -1,4 +1,5 @@ export * from './create-emotion-cache'; +export * from './fractional-indexing'; export * from './intl-formatter'; export * from './mixpanel'; export * from './string2color'; diff --git a/tests/storybook/src/stories/page-list.stories.tsx b/tests/storybook/src/stories/page-list.stories.tsx index 9a1b540352..20f91b3e58 100644 --- a/tests/storybook/src/stories/page-list.stories.tsx +++ b/tests/storybook/src/stories/page-list.stories.tsx @@ -43,11 +43,12 @@ export const AffineOperationCell: StoryFn = ({ }) => ; AffineOperationCell.args = { - favorite: false, - isPublic: true, - onToggleFavoritePage: () => toast('Toggle favorite page'), - onDisablePublicSharing: () => toast('Disable public sharing'), - onRemoveToTrash: () => toast('Remove to trash'), + page: { + id: '123', + title: 'Test Page Title', + tags: ['tag1', 'tag2'], + createDate: new Date('2021-01-01').getTime(), + }, }; AffineOperationCell.parameters = { reactRouter: reactRouterParameters({ diff --git a/tools/@types/env/__all.d.ts b/tools/@types/env/__all.d.ts index 1e92c49305..224bc064e5 100644 --- a/tools/@types/env/__all.d.ts +++ b/tools/@types/env/__all.d.ts @@ -20,6 +20,9 @@ declare global { declare module '@blocksuite/store' { interface DocMeta { + /** + * @deprecated + */ favorite?: boolean; // If a page remove to trash, and it is a subpage, it will remove from its parent `subpageIds`, 'trashRelate' is use for save it parent trashRelate?: string;