mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-12 04:18:54 +00:00
refactor(core): favorite adapter (#6285)
1. abstraction over favourites that supports different type of resources 2. sorting abstraction
This commit is contained in:
@@ -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];
|
||||
}
|
||||
|
||||
@@ -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 (
|
||||
<div className={clsx(styles.tableHeader, className)} style={style}>
|
||||
|
||||
@@ -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 };
|
||||
};
|
||||
|
||||
@@ -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 (
|
||||
<PageOperationCell
|
||||
favorite={!!page.favorite}
|
||||
isPublic={!!page.isPublic}
|
||||
page={page}
|
||||
isInAllowList={isInAllowList}
|
||||
onDisablePublicSharing={onDisablePublicSharing}
|
||||
link={`/workspace/${currentWorkspace.id}/${page.id}`}
|
||||
onOpenInSplitView={() => 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;
|
||||
};
|
||||
|
||||
|
||||
@@ -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<T>[] => {
|
||||
const t = useAFFiNEI18N();
|
||||
const favAdapter = useService(FavoriteItemsAdapter);
|
||||
const favourites = useLiveData(favAdapter.favorites$);
|
||||
return useMemo(
|
||||
() => [
|
||||
{
|
||||
@@ -166,7 +169,7 @@ export const useFavoriteGroupDefinitions = <
|
||||
icon={<FavoritedIcon className={styles.favouritedIcon} />}
|
||||
/>
|
||||
),
|
||||
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={<FavoriteIcon className={styles.notFavouritedIcon} />}
|
||||
/>
|
||||
),
|
||||
match: item => !(item as DocMeta).favorite,
|
||||
match: item => !favourites.some(fav => fav.id === item.id),
|
||||
},
|
||||
],
|
||||
[t]
|
||||
[t, favourites]
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -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 && (
|
||||
<DisablePublicSharing
|
||||
data-testid="disable-public-sharing"
|
||||
onSelect={() => {
|
||||
@@ -91,7 +122,7 @@ export const PageOperationCell = ({
|
||||
onClick={onToggleFavoritePage}
|
||||
preFix={
|
||||
<MenuIcon>
|
||||
{favorite ? (
|
||||
{favourite ? (
|
||||
<FavoritedIcon style={{ color: 'var(--affine-primary-color)' }} />
|
||||
) : (
|
||||
<FavoriteIcon />
|
||||
@@ -99,7 +130,7 @@ export const PageOperationCell = ({
|
||||
</MenuIcon>
|
||||
}
|
||||
>
|
||||
{favorite
|
||||
{favourite
|
||||
? t['com.affine.favoritePageOperation.remove']()
|
||||
: t['com.affine.favoritePageOperation.add']()}
|
||||
</MenuItem>
|
||||
@@ -121,7 +152,7 @@ export const PageOperationCell = ({
|
||||
<Link
|
||||
className={styles.clearLinkStyle}
|
||||
onClick={stopPropagationWithoutPrevent}
|
||||
to={link}
|
||||
to={`/workspace/${currentWorkspace.id}/${page.id}`}
|
||||
target={'_blank'}
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
@@ -157,10 +188,10 @@ export const PageOperationCell = ({
|
||||
<ColWrapper
|
||||
hideInSmallContainer
|
||||
data-testid="page-list-item-favorite"
|
||||
data-favorite={favorite ? true : undefined}
|
||||
data-favorite={favourite ? true : undefined}
|
||||
className={styles.favoriteCell}
|
||||
>
|
||||
<FavoriteTag onClick={onToggleFavoritePage} active={favorite} />
|
||||
<FavoriteTag onClick={onToggleFavoritePage} active={favourite} />
|
||||
</ColWrapper>
|
||||
<ColWrapper alignment="start">
|
||||
<Menu
|
||||
|
||||
@@ -21,6 +21,7 @@ export const filterByFilterList = (filterList: Filter[], varMap: VariableMap) =>
|
||||
|
||||
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,
|
||||
|
||||
@@ -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,
|
||||
]
|
||||
);
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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 &&
|
||||
|
||||
@@ -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 } =
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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 (
|
||||
<FavouritePage
|
||||
key={`${pageMeta}-${index}`}
|
||||
@@ -57,7 +64,7 @@ export const FavoriteList = ({
|
||||
/>
|
||||
);
|
||||
})}
|
||||
{favoriteList.length === 0 && <EmptyItem />}
|
||||
{favourites.length === 0 && <EmptyItem />}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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<AllPageListConfig>(() => {
|
||||
return {
|
||||
allPages: pageMetas,
|
||||
@@ -49,7 +56,7 @@ export const useAllPageListConfig = () => {
|
||||
<FavoriteTag
|
||||
style={{ marginRight: 8 }}
|
||||
onClick={() => onToggleFavoritePage(page)}
|
||||
active={!!page.favorite}
|
||||
active={isActive(page)}
|
||||
/>
|
||||
);
|
||||
},
|
||||
@@ -60,6 +67,7 @@ export const useAllPageListConfig = () => {
|
||||
getPublicMode,
|
||||
currentWorkspace.docCollection,
|
||||
pageMap,
|
||||
isActive,
|
||||
onToggleFavoritePage,
|
||||
]);
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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]);
|
||||
|
||||
@@ -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<any>;
|
||||
public readonly properties$: LiveData<WorkspaceAffineProperties>;
|
||||
|
||||
private ensuredRoot = false;
|
||||
private ensuredPages = {} as Record<string, boolean>;
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,8 +64,9 @@ export type PageInfoTagsItem = z.infer<typeof PageInfoTagsItemSchema>;
|
||||
// ====== 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<typeof WorkspaceFavoriteItemSchema>;
|
||||
|
||||
113
packages/frontend/core/src/utils/fractional-indexing.ts
Normal file
113
packages/frontend/core/src/utils/fractional-indexing.ts
Normal file
@@ -0,0 +1,113 @@
|
||||
import { generateKeyBetween } from 'fractional-indexing';
|
||||
|
||||
export interface SortableProvider<T, K extends string | number> {
|
||||
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<T, K>) {
|
||||
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,
|
||||
};
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
export * from './create-emotion-cache';
|
||||
export * from './fractional-indexing';
|
||||
export * from './intl-formatter';
|
||||
export * from './mixpanel';
|
||||
export * from './string2color';
|
||||
|
||||
Reference in New Issue
Block a user