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,
|
PageInfoCustomPropertyMeta,
|
||||||
} from '@affine/core/modules/workspace/properties/schema';
|
} from '@affine/core/modules/workspace/properties/schema';
|
||||||
import { PagePropertyType } 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 { DebugLogger } from '@affine/debug';
|
||||||
import { generateKeyBetween } from 'fractional-indexing';
|
|
||||||
import { nanoid } from 'nanoid';
|
import { nanoid } from 'nanoid';
|
||||||
|
|
||||||
import { getDefaultIconName } from './icons-mapping';
|
import { getDefaultIconName } from './icons-mapping';
|
||||||
@@ -147,6 +147,11 @@ export class PagePropertiesManager {
|
|||||||
this.ensureRequiredProperties();
|
this.ensureRequiredProperties();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
readonly sorter = createFractionalIndexingSortableHelper<
|
||||||
|
PageInfoCustomProperty,
|
||||||
|
string | number
|
||||||
|
>(this);
|
||||||
|
|
||||||
// prevent infinite loop
|
// prevent infinite loop
|
||||||
private ensuring = false;
|
private ensuring = false;
|
||||||
ensureRequiredProperties() {
|
ensureRequiredProperties() {
|
||||||
@@ -163,6 +168,22 @@ export class PagePropertiesManager {
|
|||||||
this.ensuring = false;
|
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() {
|
get workspace() {
|
||||||
return this.adapter.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 {
|
getCustomPropertyMeta(id: string): PageInfoCustomPropertyMeta | undefined {
|
||||||
return this.metaManager.customPropertiesSchema[id];
|
return this.metaManager.customPropertiesSchema[id];
|
||||||
}
|
}
|
||||||
@@ -234,7 +245,7 @@ export class PagePropertiesManager {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const newOrder = generateKeyBetween(this.largestOrder(), null);
|
const newOrder = this.sorter.getNewItemOrder();
|
||||||
if (this.properties!.custom[id]) {
|
if (this.properties!.custom[id]) {
|
||||||
logger.warn(`custom property ${id} already exists`);
|
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) {
|
hasCustomProperty(id: string) {
|
||||||
return !!this.properties?.custom[id];
|
return !!this.properties?.custom[id];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -101,10 +101,7 @@ interface SortablePropertiesProps {
|
|||||||
|
|
||||||
const SortableProperties = ({ children }: SortablePropertiesProps) => {
|
const SortableProperties = ({ children }: SortablePropertiesProps) => {
|
||||||
const manager = useContext(managerContext);
|
const manager = useContext(managerContext);
|
||||||
const properties = useMemo(
|
const properties = useMemo(() => manager.sorter.getOrderedItems(), [manager]);
|
||||||
() => manager.getOrderedCustomProperties(),
|
|
||||||
[manager]
|
|
||||||
);
|
|
||||||
const editingItem = useAtomValue(editingPropertyAtom);
|
const editingItem = useAtomValue(editingPropertyAtom);
|
||||||
const draggable = !manager.readonly && !editingItem;
|
const draggable = !manager.readonly && !editingItem;
|
||||||
const sensors = useSensors(
|
const sensors = useSensors(
|
||||||
@@ -128,15 +125,12 @@ const SortableProperties = ({ children }: SortablePropertiesProps) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const { active, over } = event;
|
const { active, over } = event;
|
||||||
const fromIndex = properties.findIndex(p => p.id === active.id);
|
if (over) {
|
||||||
const toIndex = properties.findIndex(p => p.id === over?.id);
|
manager.sorter.move(active.id, over.id);
|
||||||
|
|
||||||
if (fromIndex !== -1 && toIndex !== -1 && fromIndex !== toIndex) {
|
|
||||||
manager.moveCustomProperty(fromIndex, toIndex);
|
|
||||||
setLocalProperties(manager.getOrderedCustomProperties());
|
|
||||||
}
|
}
|
||||||
|
setLocalProperties(manager.sorter.getOrderedItems());
|
||||||
},
|
},
|
||||||
[manager, properties, draggable]
|
[manager, draggable]
|
||||||
);
|
);
|
||||||
|
|
||||||
const filteredProperties = useMemo(
|
const filteredProperties = useMemo(
|
||||||
@@ -636,7 +630,7 @@ export const PagePropertiesTableHeader = ({
|
|||||||
onOpenChange(!open);
|
onOpenChange(!open);
|
||||||
}, [onOpenChange, open]);
|
}, [onOpenChange, open]);
|
||||||
|
|
||||||
const properties = manager.getOrderedCustomProperties();
|
const properties = manager.sorter.getOrderedItems();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={clsx(styles.tableHeader, className)} style={style}>
|
<div className={clsx(styles.tableHeader, className)} style={style}>
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
import { FavoriteTag } from '@affine/core/components/page-list';
|
import { FavoriteTag } from '@affine/core/components/page-list';
|
||||||
import { useBlockSuiteMetaHelper } from '@affine/core/hooks/affine/use-block-suite-meta-helper';
|
import { FavoriteItemsAdapter } from '@affine/core/modules/workspace';
|
||||||
import { useBlockSuiteDocMeta } from '@affine/core/hooks/use-block-suite-page-meta';
|
|
||||||
import { toast } from '@affine/core/utils';
|
import { toast } from '@affine/core/utils';
|
||||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||||
import { assertExists } from '@blocksuite/global/utils';
|
import { assertExists } from '@blocksuite/global/utils';
|
||||||
import { useService, Workspace } from '@toeverything/infra';
|
import { useLiveData, useService, Workspace } from '@toeverything/infra';
|
||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
|
|
||||||
export interface FavoriteButtonProps {
|
export interface FavoriteButtonProps {
|
||||||
@@ -16,24 +15,19 @@ export const useFavorite = (pageId: string) => {
|
|||||||
const workspace = useService(Workspace);
|
const workspace = useService(Workspace);
|
||||||
const docCollection = workspace.docCollection;
|
const docCollection = workspace.docCollection;
|
||||||
const currentPage = docCollection.getDoc(pageId);
|
const currentPage = docCollection.getDoc(pageId);
|
||||||
|
const favAdapter = useService(FavoriteItemsAdapter);
|
||||||
assertExists(currentPage);
|
assertExists(currentPage);
|
||||||
|
|
||||||
const pageMeta = useBlockSuiteDocMeta(docCollection).find(
|
const favorite = useLiveData(favAdapter.isFavorite$(pageId, 'doc'));
|
||||||
meta => meta.id === pageId
|
|
||||||
);
|
|
||||||
const favorite = pageMeta?.favorite ?? false;
|
|
||||||
|
|
||||||
const { toggleFavorite: _toggleFavorite } =
|
|
||||||
useBlockSuiteMetaHelper(docCollection);
|
|
||||||
|
|
||||||
const toggleFavorite = useCallback(() => {
|
const toggleFavorite = useCallback(() => {
|
||||||
_toggleFavorite(pageId);
|
favAdapter.toggle(pageId, 'doc');
|
||||||
toast(
|
toast(
|
||||||
favorite
|
favorite
|
||||||
? t['com.affine.toastMessage.removedFavorites']()
|
? t['com.affine.toastMessage.removedFavorites']()
|
||||||
: t['com.affine.toastMessage.addedFavorites']()
|
: t['com.affine.toastMessage.addedFavorites']()
|
||||||
);
|
);
|
||||||
}, [favorite, pageId, t, _toggleFavorite]);
|
}, [favorite, pageId, t, favAdapter]);
|
||||||
|
|
||||||
return { favorite, toggleFavorite };
|
return { favorite, toggleFavorite };
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,10 +1,8 @@
|
|||||||
import { toast } from '@affine/component';
|
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 { useTrashModalHelper } from '@affine/core/hooks/affine/use-trash-modal-helper';
|
||||||
import { useBlockSuiteDocMeta } from '@affine/core/hooks/use-block-suite-page-meta';
|
import { useBlockSuiteDocMeta } from '@affine/core/hooks/use-block-suite-page-meta';
|
||||||
import { CollectionService } from '@affine/core/modules/collection';
|
import { CollectionService } from '@affine/core/modules/collection';
|
||||||
import type { Tag } from '@affine/core/modules/tag';
|
import type { Tag } from '@affine/core/modules/tag';
|
||||||
import { Workbench } from '@affine/core/modules/workbench';
|
|
||||||
import type { Collection, Filter } from '@affine/env/filter';
|
import type { Collection, Filter } from '@affine/env/filter';
|
||||||
import { Trans } from '@affine/i18n';
|
import { Trans } from '@affine/i18n';
|
||||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||||
@@ -30,13 +28,7 @@ import {
|
|||||||
} from './page-list-header';
|
} from './page-list-header';
|
||||||
|
|
||||||
const usePageOperationsRenderer = () => {
|
const usePageOperationsRenderer = () => {
|
||||||
const currentWorkspace = useService(Workspace);
|
|
||||||
const { setTrashModal } = useTrashModalHelper(currentWorkspace.docCollection);
|
|
||||||
const { toggleFavorite, duplicate } = useBlockSuiteMetaHelper(
|
|
||||||
currentWorkspace.docCollection
|
|
||||||
);
|
|
||||||
const t = useAFFiNEI18N();
|
const t = useAFFiNEI18N();
|
||||||
const workbench = useService(Workbench);
|
|
||||||
const collectionService = useService(CollectionService);
|
const collectionService = useService(CollectionService);
|
||||||
const removeFromAllowList = useCallback(
|
const removeFromAllowList = useCallback(
|
||||||
(id: string) => {
|
(id: string) => {
|
||||||
@@ -45,57 +37,18 @@ const usePageOperationsRenderer = () => {
|
|||||||
},
|
},
|
||||||
[collectionService, t]
|
[collectionService, t]
|
||||||
);
|
);
|
||||||
|
|
||||||
const pageOperationsRenderer = useCallback(
|
const pageOperationsRenderer = useCallback(
|
||||||
(page: DocMeta, isInAllowList?: boolean) => {
|
(page: DocMeta, isInAllowList?: boolean) => {
|
||||||
const onDisablePublicSharing = () => {
|
|
||||||
toast('Successfully disabled', {
|
|
||||||
portal: document.body,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageOperationCell
|
<PageOperationCell
|
||||||
favorite={!!page.favorite}
|
page={page}
|
||||||
isPublic={!!page.isPublic}
|
|
||||||
isInAllowList={isInAllowList}
|
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)}
|
onRemoveFromAllowList={() => removeFromAllowList(page.id)}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
[
|
[removeFromAllowList]
|
||||||
currentWorkspace.id,
|
|
||||||
workbench,
|
|
||||||
duplicate,
|
|
||||||
setTrashModal,
|
|
||||||
toggleFavorite,
|
|
||||||
t,
|
|
||||||
removeFromAllowList,
|
|
||||||
]
|
|
||||||
);
|
);
|
||||||
|
|
||||||
return pageOperationsRenderer;
|
return pageOperationsRenderer;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import type { Tag } from '@affine/core/modules/tag';
|
import type { Tag } from '@affine/core/modules/tag';
|
||||||
import { TagService } 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 { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||||
import { FavoritedIcon, FavoriteIcon } from '@blocksuite/icons';
|
import { FavoritedIcon, FavoriteIcon } from '@blocksuite/icons';
|
||||||
import type { DocMeta } from '@blocksuite/store';
|
import type { DocMeta } from '@blocksuite/store';
|
||||||
@@ -154,6 +155,8 @@ export const useFavoriteGroupDefinitions = <
|
|||||||
T extends ListItem,
|
T extends ListItem,
|
||||||
>(): ItemGroupDefinition<T>[] => {
|
>(): ItemGroupDefinition<T>[] => {
|
||||||
const t = useAFFiNEI18N();
|
const t = useAFFiNEI18N();
|
||||||
|
const favAdapter = useService(FavoriteItemsAdapter);
|
||||||
|
const favourites = useLiveData(favAdapter.favorites$);
|
||||||
return useMemo(
|
return useMemo(
|
||||||
() => [
|
() => [
|
||||||
{
|
{
|
||||||
@@ -166,7 +169,7 @@ export const useFavoriteGroupDefinitions = <
|
|||||||
icon={<FavoritedIcon className={styles.favouritedIcon} />}
|
icon={<FavoritedIcon className={styles.favouritedIcon} />}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
match: item => !!(item as DocMeta).favorite,
|
match: item => favourites.some(fav => fav.id === item.id),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'notFavourited',
|
id: 'notFavourited',
|
||||||
@@ -178,10 +181,10 @@ export const useFavoriteGroupDefinitions = <
|
|||||||
icon={<FavoriteIcon className={styles.notFavouritedIcon} />}
|
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,
|
Menu,
|
||||||
MenuIcon,
|
MenuIcon,
|
||||||
MenuItem,
|
MenuItem,
|
||||||
|
toast,
|
||||||
Tooltip,
|
Tooltip,
|
||||||
} from '@affine/component';
|
} from '@affine/component';
|
||||||
import { useAppSettingHelper } from '@affine/core/hooks/affine/use-app-setting-helper';
|
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 type { Collection, DeleteCollectionInfo } from '@affine/env/filter';
|
||||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||||
import {
|
import {
|
||||||
@@ -23,6 +28,8 @@ import {
|
|||||||
ResetIcon,
|
ResetIcon,
|
||||||
SplitViewIcon,
|
SplitViewIcon,
|
||||||
} from '@blocksuite/icons';
|
} from '@blocksuite/icons';
|
||||||
|
import type { DocMeta } from '@blocksuite/store';
|
||||||
|
import { useLiveData, useService, Workspace } from '@toeverything/infra';
|
||||||
import { useCallback, useState } from 'react';
|
import { useCallback, useState } from 'react';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
@@ -37,37 +44,61 @@ import type { AllPageListConfig } from './view';
|
|||||||
import { useEditCollection, useEditCollectionName } from './view';
|
import { useEditCollection, useEditCollectionName } from './view';
|
||||||
|
|
||||||
export interface PageOperationCellProps {
|
export interface PageOperationCellProps {
|
||||||
favorite: boolean;
|
page: DocMeta;
|
||||||
isPublic: boolean;
|
|
||||||
link: string;
|
|
||||||
isInAllowList?: boolean;
|
isInAllowList?: boolean;
|
||||||
onToggleFavoritePage: () => void;
|
|
||||||
onRemoveToTrash: () => void;
|
|
||||||
onDuplicate: () => void;
|
|
||||||
onDisablePublicSharing: () => void;
|
|
||||||
onOpenInSplitView: () => void;
|
|
||||||
onRemoveFromAllowList?: () => void;
|
onRemoveFromAllowList?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const PageOperationCell = ({
|
export const PageOperationCell = ({
|
||||||
favorite,
|
|
||||||
isPublic,
|
|
||||||
isInAllowList,
|
isInAllowList,
|
||||||
link,
|
page,
|
||||||
onToggleFavoritePage,
|
|
||||||
onRemoveToTrash,
|
|
||||||
onDuplicate,
|
|
||||||
onDisablePublicSharing,
|
|
||||||
onOpenInSplitView,
|
|
||||||
onRemoveFromAllowList,
|
onRemoveFromAllowList,
|
||||||
}: PageOperationCellProps) => {
|
}: PageOperationCellProps) => {
|
||||||
const t = useAFFiNEI18N();
|
const t = useAFFiNEI18N();
|
||||||
|
const currentWorkspace = useService(Workspace);
|
||||||
const { appSettings } = useAppSettingHelper();
|
const { appSettings } = useAppSettingHelper();
|
||||||
|
const { setTrashModal } = useTrashModalHelper(currentWorkspace.docCollection);
|
||||||
const [openDisableShared, setOpenDisableShared] = useState(false);
|
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 = (
|
const OperationMenu = (
|
||||||
<>
|
<>
|
||||||
{isPublic && (
|
{page.isPublic && (
|
||||||
<DisablePublicSharing
|
<DisablePublicSharing
|
||||||
data-testid="disable-public-sharing"
|
data-testid="disable-public-sharing"
|
||||||
onSelect={() => {
|
onSelect={() => {
|
||||||
@@ -91,7 +122,7 @@ export const PageOperationCell = ({
|
|||||||
onClick={onToggleFavoritePage}
|
onClick={onToggleFavoritePage}
|
||||||
preFix={
|
preFix={
|
||||||
<MenuIcon>
|
<MenuIcon>
|
||||||
{favorite ? (
|
{favourite ? (
|
||||||
<FavoritedIcon style={{ color: 'var(--affine-primary-color)' }} />
|
<FavoritedIcon style={{ color: 'var(--affine-primary-color)' }} />
|
||||||
) : (
|
) : (
|
||||||
<FavoriteIcon />
|
<FavoriteIcon />
|
||||||
@@ -99,7 +130,7 @@ export const PageOperationCell = ({
|
|||||||
</MenuIcon>
|
</MenuIcon>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{favorite
|
{favourite
|
||||||
? t['com.affine.favoritePageOperation.remove']()
|
? t['com.affine.favoritePageOperation.remove']()
|
||||||
: t['com.affine.favoritePageOperation.add']()}
|
: t['com.affine.favoritePageOperation.add']()}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
@@ -121,7 +152,7 @@ export const PageOperationCell = ({
|
|||||||
<Link
|
<Link
|
||||||
className={styles.clearLinkStyle}
|
className={styles.clearLinkStyle}
|
||||||
onClick={stopPropagationWithoutPrevent}
|
onClick={stopPropagationWithoutPrevent}
|
||||||
to={link}
|
to={`/workspace/${currentWorkspace.id}/${page.id}`}
|
||||||
target={'_blank'}
|
target={'_blank'}
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
>
|
>
|
||||||
@@ -157,10 +188,10 @@ export const PageOperationCell = ({
|
|||||||
<ColWrapper
|
<ColWrapper
|
||||||
hideInSmallContainer
|
hideInSmallContainer
|
||||||
data-testid="page-list-item-favorite"
|
data-testid="page-list-item-favorite"
|
||||||
data-favorite={favorite ? true : undefined}
|
data-favorite={favourite ? true : undefined}
|
||||||
className={styles.favoriteCell}
|
className={styles.favoriteCell}
|
||||||
>
|
>
|
||||||
<FavoriteTag onClick={onToggleFavoritePage} active={favorite} />
|
<FavoriteTag onClick={onToggleFavoritePage} active={favourite} />
|
||||||
</ColWrapper>
|
</ColWrapper>
|
||||||
<ColWrapper alignment="start">
|
<ColWrapper alignment="start">
|
||||||
<Menu
|
<Menu
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ export const filterByFilterList = (filterList: Filter[], varMap: VariableMap) =>
|
|||||||
|
|
||||||
export type PageDataForFilter = {
|
export type PageDataForFilter = {
|
||||||
meta: DocMeta;
|
meta: DocMeta;
|
||||||
|
favorite: boolean;
|
||||||
publicMode: undefined | 'page' | 'edgeless';
|
publicMode: undefined | 'page' | 'edgeless';
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -33,13 +34,13 @@ export const filterPage = (collection: Collection, page: PageDataForFilter) => {
|
|||||||
export const filterPageByRules = (
|
export const filterPageByRules = (
|
||||||
rules: Filter[],
|
rules: Filter[],
|
||||||
allowList: string[],
|
allowList: string[],
|
||||||
{ meta, publicMode }: PageDataForFilter
|
{ meta, publicMode, favorite }: PageDataForFilter
|
||||||
) => {
|
) => {
|
||||||
if (allowList?.includes(meta.id)) {
|
if (allowList?.includes(meta.id)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return filterByFilterList(rules, {
|
return filterByFilterList(rules, {
|
||||||
'Is Favourited': !!meta.favorite,
|
'Is Favourited': !!favorite,
|
||||||
'Is Public': !!publicMode,
|
'Is Public': !!publicMode,
|
||||||
Created: meta.createDate,
|
Created: meta.createDate,
|
||||||
Updated: meta.updatedDate ?? 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 { Collection, Filter } from '@affine/env/filter';
|
||||||
import type { DocMeta } from '@blocksuite/store';
|
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 { useMemo } from 'react';
|
||||||
|
|
||||||
import { usePublicPages } from '../../hooks/affine/use-is-shared-page';
|
import { usePublicPages } from '../../hooks/affine/use-is-shared-page';
|
||||||
@@ -16,6 +17,8 @@ export const useFilteredPageMetas = (
|
|||||||
} = {}
|
} = {}
|
||||||
) => {
|
) => {
|
||||||
const { getPublicMode } = usePublicPages(workspace);
|
const { getPublicMode } = usePublicPages(workspace);
|
||||||
|
const favAdapter = useService(FavoriteItemsAdapter);
|
||||||
|
const favoriteItems = useLiveData(favAdapter.favorites$);
|
||||||
|
|
||||||
const filteredPageMetas = useMemo(
|
const filteredPageMetas = useMemo(
|
||||||
() =>
|
() =>
|
||||||
@@ -29,6 +32,7 @@ export const useFilteredPageMetas = (
|
|||||||
}
|
}
|
||||||
const pageData = {
|
const pageData = {
|
||||||
meta: pageMeta,
|
meta: pageMeta,
|
||||||
|
favorite: favoriteItems.some(fav => fav.id === pageMeta.id),
|
||||||
publicMode: getPublicMode(pageMeta.id),
|
publicMode: getPublicMode(pageMeta.id),
|
||||||
};
|
};
|
||||||
if (
|
if (
|
||||||
@@ -49,6 +53,7 @@ export const useFilteredPageMetas = (
|
|||||||
options.trash,
|
options.trash,
|
||||||
options.filters,
|
options.filters,
|
||||||
options.collection,
|
options.collection,
|
||||||
|
favoriteItems,
|
||||||
getPublicMode,
|
getPublicMode,
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
import { Menu } from '@affine/component';
|
import { Menu } from '@affine/component';
|
||||||
|
import { FavoriteItemsAdapter } from '@affine/core/modules/workspace';
|
||||||
import type { Collection } from '@affine/env/filter';
|
import type { Collection } from '@affine/env/filter';
|
||||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||||
import { FilterIcon } from '@blocksuite/icons';
|
import { FilterIcon } from '@blocksuite/icons';
|
||||||
import type { DocMeta } from '@blocksuite/store';
|
import type { DocMeta } from '@blocksuite/store';
|
||||||
|
import { useLiveData, useService } from '@toeverything/infra';
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import type { ReactNode } from 'react';
|
import type { ReactNode } from 'react';
|
||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
@@ -34,6 +36,8 @@ export const PagesMode = ({
|
|||||||
allPageListConfig: AllPageListConfig;
|
allPageListConfig: AllPageListConfig;
|
||||||
}) => {
|
}) => {
|
||||||
const t = useAFFiNEI18N();
|
const t = useAFFiNEI18N();
|
||||||
|
const favAdapter = useService(FavoriteItemsAdapter);
|
||||||
|
const favorites = useLiveData(favAdapter.favorites$);
|
||||||
const {
|
const {
|
||||||
showFilter,
|
showFilter,
|
||||||
filters,
|
filters,
|
||||||
@@ -45,6 +49,7 @@ export const PagesMode = ({
|
|||||||
allPageListConfig.allPages.map(meta => ({
|
allPageListConfig.allPages.map(meta => ({
|
||||||
meta,
|
meta,
|
||||||
publicMode: allPageListConfig.getPublicMode(meta.id),
|
publicMode: allPageListConfig.getPublicMode(meta.id),
|
||||||
|
favorite: favorites.some(f => f.id === meta.id),
|
||||||
}))
|
}))
|
||||||
);
|
);
|
||||||
const pageHeaderColsDef = usePageHeaderColsDef();
|
const pageHeaderColsDef = usePageHeaderColsDef();
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { FavoriteItemsAdapter } from '@affine/core/modules/workspace';
|
||||||
import type { Collection } from '@affine/env/filter';
|
import type { Collection } from '@affine/env/filter';
|
||||||
import { Trans } from '@affine/i18n';
|
import { Trans } from '@affine/i18n';
|
||||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||||
@@ -9,6 +10,7 @@ import {
|
|||||||
ToggleCollapseIcon,
|
ToggleCollapseIcon,
|
||||||
} from '@blocksuite/icons';
|
} from '@blocksuite/icons';
|
||||||
import type { DocMeta } from '@blocksuite/store';
|
import type { DocMeta } from '@blocksuite/store';
|
||||||
|
import { useLiveData, useService } from '@toeverything/infra';
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import type { ReactNode } from 'react';
|
import type { ReactNode } from 'react';
|
||||||
import { useCallback, useEffect, useState } from 'react';
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
@@ -42,6 +44,8 @@ export const RulesMode = ({
|
|||||||
const allowListPages: DocMeta[] = [];
|
const allowListPages: DocMeta[] = [];
|
||||||
const rulesPages: DocMeta[] = [];
|
const rulesPages: DocMeta[] = [];
|
||||||
const [showTips, setShowTips] = useState(false);
|
const [showTips, setShowTips] = useState(false);
|
||||||
|
const favAdapter = useService(FavoriteItemsAdapter);
|
||||||
|
const favorites = useLiveData(favAdapter.favorites$);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setShowTips(!localStorage.getItem('hide-rules-mode-include-page-tips'));
|
setShowTips(!localStorage.getItem('hide-rules-mode-include-page-tips'));
|
||||||
}, []);
|
}, []);
|
||||||
@@ -56,6 +60,7 @@ export const RulesMode = ({
|
|||||||
const pageData = {
|
const pageData = {
|
||||||
meta,
|
meta,
|
||||||
publicMode: allPageListConfig.getPublicMode(meta.id),
|
publicMode: allPageListConfig.getPublicMode(meta.id),
|
||||||
|
favorite: favorites.some(f => f.id === meta.id),
|
||||||
};
|
};
|
||||||
if (
|
if (
|
||||||
collection.filterList.length &&
|
collection.filterList.length &&
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
import { Button, Menu } from '@affine/component';
|
import { Button, Menu } from '@affine/component';
|
||||||
|
import { FavoriteItemsAdapter } from '@affine/core/modules/workspace';
|
||||||
import { Trans } from '@affine/i18n';
|
import { Trans } from '@affine/i18n';
|
||||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||||
import { FilterIcon } from '@blocksuite/icons';
|
import { FilterIcon } from '@blocksuite/icons';
|
||||||
import type { DocMeta } from '@blocksuite/store';
|
import type { DocMeta } from '@blocksuite/store';
|
||||||
|
import { useLiveData, useService } from '@toeverything/infra';
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import { useCallback, useState } from 'react';
|
import { useCallback, useState } from 'react';
|
||||||
|
|
||||||
@@ -35,6 +37,8 @@ export const SelectPage = ({
|
|||||||
const clearSelected = useCallback(() => {
|
const clearSelected = useCallback(() => {
|
||||||
onChange([]);
|
onChange([]);
|
||||||
}, []);
|
}, []);
|
||||||
|
const favAdapter = useService(FavoriteItemsAdapter);
|
||||||
|
const favourites = useLiveData(favAdapter.favorites$);
|
||||||
const {
|
const {
|
||||||
clickFilter,
|
clickFilter,
|
||||||
createFilter,
|
createFilter,
|
||||||
@@ -46,6 +50,7 @@ export const SelectPage = ({
|
|||||||
allPageListConfig.allPages.map(meta => ({
|
allPageListConfig.allPages.map(meta => ({
|
||||||
meta,
|
meta,
|
||||||
publicMode: allPageListConfig.getPublicMode(meta.id),
|
publicMode: allPageListConfig.getPublicMode(meta.id),
|
||||||
|
favorite: favourites.some(fav => fav.id === meta.id),
|
||||||
}))
|
}))
|
||||||
);
|
);
|
||||||
const { searchText, updateSearchText, searchedList } =
|
const { searchText, updateSearchText, searchedList } =
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
stopPropagation,
|
stopPropagation,
|
||||||
} from '@affine/core/components/page-list';
|
} from '@affine/core/components/page-list';
|
||||||
import { CollectionService } from '@affine/core/modules/collection';
|
import { CollectionService } from '@affine/core/modules/collection';
|
||||||
|
import { FavoriteItemsAdapter } from '@affine/core/modules/workspace';
|
||||||
import type { Collection, DeleteCollectionInfo } from '@affine/env/filter';
|
import type { Collection, DeleteCollectionInfo } from '@affine/env/filter';
|
||||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||||
import { MoreHorizontalIcon, ViewLayersIcon } from '@blocksuite/icons';
|
import { MoreHorizontalIcon, ViewLayersIcon } from '@blocksuite/icons';
|
||||||
@@ -40,9 +41,12 @@ const CollectionRenderer = ({
|
|||||||
const [collapsed, setCollapsed] = useState(true);
|
const [collapsed, setCollapsed] = useState(true);
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const collectionService = useService(CollectionService);
|
const collectionService = useService(CollectionService);
|
||||||
|
const favAdapter = useService(FavoriteItemsAdapter);
|
||||||
const t = useAFFiNEI18N();
|
const t = useAFFiNEI18N();
|
||||||
const dragItemId = getDropItemId('collections', collection.id);
|
const dragItemId = getDropItemId('collections', collection.id);
|
||||||
|
|
||||||
|
const favourites = useLiveData(favAdapter.favorites$);
|
||||||
|
|
||||||
const removeFromAllowList = useCallback(
|
const removeFromAllowList = useCallback(
|
||||||
(id: string) => {
|
(id: string) => {
|
||||||
collectionService.updateCollection(collection.id, () => ({
|
collectionService.updateCollection(collection.id, () => ({
|
||||||
@@ -85,6 +89,7 @@ const CollectionRenderer = ({
|
|||||||
const pageData = {
|
const pageData = {
|
||||||
meta,
|
meta,
|
||||||
publicMode: config.getPublicMode(meta.id),
|
publicMode: config.getPublicMode(meta.id),
|
||||||
|
favorite: favourites.some(fav => fav.id === meta.id),
|
||||||
};
|
};
|
||||||
return filterPage(collection, pageData);
|
return filterPage(collection, pageData);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2,13 +2,13 @@ import { toast } from '@affine/component';
|
|||||||
import { IconButton } from '@affine/component/ui/button';
|
import { IconButton } from '@affine/component/ui/button';
|
||||||
import { Menu } from '@affine/component/ui/menu';
|
import { Menu } from '@affine/component/ui/menu';
|
||||||
import { Workbench } from '@affine/core/modules/workbench';
|
import { Workbench } from '@affine/core/modules/workbench';
|
||||||
|
import { FavoriteItemsAdapter } from '@affine/core/modules/workspace';
|
||||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||||
import { MoreHorizontalIcon } from '@blocksuite/icons';
|
import { MoreHorizontalIcon } from '@blocksuite/icons';
|
||||||
import type { DocCollection } from '@blocksuite/store';
|
import type { DocCollection } from '@blocksuite/store';
|
||||||
import { useService } from '@toeverything/infra';
|
import { useService } from '@toeverything/infra';
|
||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
|
|
||||||
import { useBlockSuiteMetaHelper } from '../../../../hooks/affine/use-block-suite-meta-helper';
|
|
||||||
import { useTrashModalHelper } from '../../../../hooks/affine/use-trash-modal-helper';
|
import { useTrashModalHelper } from '../../../../hooks/affine/use-trash-modal-helper';
|
||||||
import { usePageHelper } from '../../../blocksuite/block-suite-page-list/utils';
|
import { usePageHelper } from '../../../blocksuite/block-suite-page-list/utils';
|
||||||
import { OperationItems } from './operation-item';
|
import { OperationItems } from './operation-item';
|
||||||
@@ -38,7 +38,8 @@ export const OperationMenuButton = ({ ...props }: OperationMenuButtonProps) => {
|
|||||||
const t = useAFFiNEI18N();
|
const t = useAFFiNEI18N();
|
||||||
const { createLinkedPage } = usePageHelper(docCollection);
|
const { createLinkedPage } = usePageHelper(docCollection);
|
||||||
const { setTrashModal } = useTrashModalHelper(docCollection);
|
const { setTrashModal } = useTrashModalHelper(docCollection);
|
||||||
const { removeFromFavorite } = useBlockSuiteMetaHelper(docCollection);
|
|
||||||
|
const favAdapter = useService(FavoriteItemsAdapter);
|
||||||
const workbench = useService(Workbench);
|
const workbench = useService(Workbench);
|
||||||
|
|
||||||
const handleRename = useCallback(() => {
|
const handleRename = useCallback(() => {
|
||||||
@@ -51,9 +52,9 @@ export const OperationMenuButton = ({ ...props }: OperationMenuButtonProps) => {
|
|||||||
}, [createLinkedPage, pageId, t]);
|
}, [createLinkedPage, pageId, t]);
|
||||||
|
|
||||||
const handleRemoveFromFavourites = useCallback(() => {
|
const handleRemoveFromFavourites = useCallback(() => {
|
||||||
removeFromFavorite(pageId);
|
favAdapter.remove(pageId, 'doc');
|
||||||
toast(t['com.affine.toastMessage.removedFavorites']());
|
toast(t['com.affine.toastMessage.removedFavorites']());
|
||||||
}, [pageId, removeFromFavorite, t]);
|
}, [favAdapter, pageId, t]);
|
||||||
|
|
||||||
const handleDelete = useCallback(() => {
|
const handleDelete = useCallback(() => {
|
||||||
setTrashModal({
|
setTrashModal({
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import { IconButton } from '@affine/component/ui/button';
|
import { IconButton } from '@affine/component/ui/button';
|
||||||
import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks';
|
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 { PlusIcon } from '@blocksuite/icons';
|
||||||
import type { DocCollection } from '@blocksuite/store';
|
import type { DocCollection } from '@blocksuite/store';
|
||||||
|
import { useService } from '@toeverything/infra';
|
||||||
|
|
||||||
import { usePageHelper } from '../../../blocksuite/block-suite-page-list/utils';
|
import { usePageHelper } from '../../../blocksuite/block-suite-page-list/utils';
|
||||||
|
|
||||||
@@ -16,7 +17,7 @@ export const AddFavouriteButton = ({
|
|||||||
pageId,
|
pageId,
|
||||||
}: AddFavouriteButtonProps) => {
|
}: AddFavouriteButtonProps) => {
|
||||||
const { createPage, createLinkedPage } = usePageHelper(docCollection);
|
const { createPage, createLinkedPage } = usePageHelper(docCollection);
|
||||||
const { setDocMeta } = useDocMetaHelper(docCollection);
|
const favAdapter = useService(FavoriteItemsAdapter);
|
||||||
const handleAddFavorite = useAsyncCallback(
|
const handleAddFavorite = useAsyncCallback(
|
||||||
async e => {
|
async e => {
|
||||||
if (pageId) {
|
if (pageId) {
|
||||||
@@ -26,10 +27,10 @@ export const AddFavouriteButton = ({
|
|||||||
} else {
|
} else {
|
||||||
const page = createPage();
|
const page = createPage();
|
||||||
page.load();
|
page.load();
|
||||||
setDocMeta(page.id, { favorite: true });
|
favAdapter.set(page.id, 'doc', true);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[pageId, createLinkedPage, createPage, setDocMeta]
|
[pageId, createLinkedPage, createPage, favAdapter]
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import { useBlockSuiteDocMeta } from '@affine/core/hooks/use-block-suite-page-meta';
|
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 type { DocMeta } from '@blocksuite/store';
|
||||||
import { useDroppable } from '@dnd-kit/core';
|
import { useDroppable } from '@dnd-kit/core';
|
||||||
|
import { useLiveData, useService } from '@toeverything/infra';
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
|
|
||||||
import { getDropItemId } from '../../../../hooks/affine/use-sidebar-drag';
|
import { getDropItemId } from '../../../../hooks/affine/use-sidebar-drag';
|
||||||
@@ -15,11 +17,16 @@ export const FavoriteList = ({
|
|||||||
docCollection: workspace,
|
docCollection: workspace,
|
||||||
}: FavoriteListProps) => {
|
}: FavoriteListProps) => {
|
||||||
const metas = useBlockSuiteDocMeta(workspace);
|
const metas = useBlockSuiteDocMeta(workspace);
|
||||||
|
const favAdapter = useService(FavoriteItemsAdapter);
|
||||||
const dropItemId = getDropItemId('favorites');
|
const dropItemId = getDropItemId('favorites');
|
||||||
|
|
||||||
const favoriteList = useMemo(
|
const favourites = useLiveData(
|
||||||
() => metas.filter(p => p.favorite && !p.trash),
|
favAdapter.favorites$.map(favourites =>
|
||||||
[metas]
|
favourites.filter(fav => {
|
||||||
|
const meta = metas.find(m => m.id === fav.id);
|
||||||
|
return meta && !meta.trash;
|
||||||
|
})
|
||||||
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
const metaMapping = useMemo(
|
const metaMapping = useMemo(
|
||||||
@@ -45,7 +52,7 @@ export const FavoriteList = ({
|
|||||||
ref={setNodeRef}
|
ref={setNodeRef}
|
||||||
data-over={isOver}
|
data-over={isOver}
|
||||||
>
|
>
|
||||||
{favoriteList.map((pageMeta, index) => {
|
{favourites.map((pageMeta, index) => {
|
||||||
return (
|
return (
|
||||||
<FavouritePage
|
<FavouritePage
|
||||||
key={`${pageMeta}-${index}`}
|
key={`${pageMeta}-${index}`}
|
||||||
@@ -57,7 +64,7 @@ export const FavoriteList = ({
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
{favoriteList.length === 0 && <EmptyItem />}
|
{favourites.length === 0 && <EmptyItem />}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,13 +2,13 @@ import { toast } from '@affine/component';
|
|||||||
import type { AllPageListConfig } from '@affine/core/components/page-list';
|
import type { AllPageListConfig } from '@affine/core/components/page-list';
|
||||||
import { FavoriteTag } 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 { useBlockSuiteDocMeta } from '@affine/core/hooks/use-block-suite-page-meta';
|
||||||
|
import { FavoriteItemsAdapter } from '@affine/core/modules/workspace';
|
||||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||||
import type { DocMeta } from '@blocksuite/store';
|
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 { useCallback, useMemo } from 'react';
|
||||||
|
|
||||||
import { usePageHelper } from '../../components/blocksuite/block-suite-page-list/utils';
|
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';
|
import { usePublicPages } from './use-is-shared-page';
|
||||||
|
|
||||||
export const useAllPageListConfig = () => {
|
export const useAllPageListConfig = () => {
|
||||||
@@ -21,22 +21,29 @@ export const useAllPageListConfig = () => {
|
|||||||
() => Object.fromEntries(pageMetas.map(page => [page.id, page])),
|
() => Object.fromEntries(pageMetas.map(page => [page.id, page])),
|
||||||
[pageMetas]
|
[pageMetas]
|
||||||
);
|
);
|
||||||
const { toggleFavorite } = useBlockSuiteMetaHelper(
|
const favAdapter = useService(FavoriteItemsAdapter);
|
||||||
currentWorkspace.docCollection
|
|
||||||
);
|
|
||||||
const t = useAFFiNEI18N();
|
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(
|
const onToggleFavoritePage = useCallback(
|
||||||
(page: DocMeta) => {
|
(page: DocMeta) => {
|
||||||
const status = page.favorite;
|
const status = isActive(page);
|
||||||
toggleFavorite(page.id);
|
favAdapter.toggle(page.id, 'doc');
|
||||||
toast(
|
toast(
|
||||||
status
|
status
|
||||||
? t['com.affine.toastMessage.removedFavorites']()
|
? t['com.affine.toastMessage.removedFavorites']()
|
||||||
: t['com.affine.toastMessage.addedFavorites']()
|
: t['com.affine.toastMessage.addedFavorites']()
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
[t, toggleFavorite]
|
[favAdapter, isActive, t]
|
||||||
);
|
);
|
||||||
|
|
||||||
return useMemo<AllPageListConfig>(() => {
|
return useMemo<AllPageListConfig>(() => {
|
||||||
return {
|
return {
|
||||||
allPages: pageMetas,
|
allPages: pageMetas,
|
||||||
@@ -49,7 +56,7 @@ export const useAllPageListConfig = () => {
|
|||||||
<FavoriteTag
|
<FavoriteTag
|
||||||
style={{ marginRight: 8 }}
|
style={{ marginRight: 8 }}
|
||||||
onClick={() => onToggleFavoritePage(page)}
|
onClick={() => onToggleFavoritePage(page)}
|
||||||
active={!!page.favorite}
|
active={isActive(page)}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -60,6 +67,7 @@ export const useAllPageListConfig = () => {
|
|||||||
getPublicMode,
|
getPublicMode,
|
||||||
currentWorkspace.docCollection,
|
currentWorkspace.docCollection,
|
||||||
pageMap,
|
pageMap,
|
||||||
|
isActive,
|
||||||
onToggleFavoritePage,
|
onToggleFavoritePage,
|
||||||
]);
|
]);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -19,32 +19,6 @@ export function useBlockSuiteMetaHelper(docCollection: DocCollection) {
|
|||||||
const collectionService = useService(CollectionService);
|
const collectionService = useService(CollectionService);
|
||||||
const pageRecordList = useService(PageRecordList);
|
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
|
// TODO-Doma
|
||||||
// "Remove" may cause ambiguity here. Consider renaming as "moveToTrash".
|
// "Remove" may cause ambiguity here. Consider renaming as "moveToTrash".
|
||||||
const removeToTrash = useCallback(
|
const removeToTrash = useCallback(
|
||||||
@@ -126,7 +100,6 @@ export function useBlockSuiteMetaHelper(docCollection: DocCollection) {
|
|||||||
|
|
||||||
setDocMeta(newPage.id, {
|
setDocMeta(newPage.id, {
|
||||||
tags: currentPageMeta.tags,
|
tags: currentPageMeta.tags,
|
||||||
favorite: currentPageMeta.favorite,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const lastDigitRegex = /\((\d+)\)$/;
|
const lastDigitRegex = /\((\d+)\)$/;
|
||||||
@@ -157,10 +130,6 @@ export function useBlockSuiteMetaHelper(docCollection: DocCollection) {
|
|||||||
publicPage,
|
publicPage,
|
||||||
cancelPublicPage,
|
cancelPublicPage,
|
||||||
|
|
||||||
addToFavorite,
|
|
||||||
removeFromFavorite,
|
|
||||||
toggleFavorite,
|
|
||||||
|
|
||||||
removeToTrash,
|
removeToTrash,
|
||||||
restoreFromTrash,
|
restoreFromTrash,
|
||||||
permanentlyDeletePage,
|
permanentlyDeletePage,
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { toast } from '@affine/component';
|
import { toast } from '@affine/component';
|
||||||
import { useDocMetaHelper } from '@affine/core/hooks/use-block-suite-page-meta';
|
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 { WorkspaceFlavour } from '@affine/env/workspace';
|
||||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||||
import { assertExists } from '@blocksuite/global/utils';
|
import { assertExists } from '@blocksuite/global/utils';
|
||||||
@@ -32,7 +33,9 @@ export function useRegisterBlocksuiteEditorCommands() {
|
|||||||
assertExists(currentPage);
|
assertExists(currentPage);
|
||||||
const pageMeta = getDocMeta(pageId);
|
const pageMeta = getDocMeta(pageId);
|
||||||
assertExists(pageMeta);
|
assertExists(pageMeta);
|
||||||
const favorite = pageMeta.favorite ?? false;
|
|
||||||
|
const favAdapter = useService(FavoriteItemsAdapter);
|
||||||
|
const favorite = useLiveData(favAdapter.isFavorite$(pageId, 'doc'));
|
||||||
const trash = pageMeta.trash ?? false;
|
const trash = pageMeta.trash ?? false;
|
||||||
|
|
||||||
const setPageHistoryModalState = useSetAtom(pageHistoryModalAtom);
|
const setPageHistoryModalState = useSetAtom(pageHistoryModalAtom);
|
||||||
@@ -44,7 +47,7 @@ export function useRegisterBlocksuiteEditorCommands() {
|
|||||||
}));
|
}));
|
||||||
}, [pageId, setPageHistoryModalState]);
|
}, [pageId, setPageHistoryModalState]);
|
||||||
|
|
||||||
const { toggleFavorite, restoreFromTrash, duplicate } =
|
const { restoreFromTrash, duplicate } =
|
||||||
useBlockSuiteMetaHelper(docCollection);
|
useBlockSuiteMetaHelper(docCollection);
|
||||||
const exportHandler = useExportPage(currentPage);
|
const exportHandler = useExportPage(currentPage);
|
||||||
const { setTrashModal } = useTrashModalHelper(docCollection);
|
const { setTrashModal } = useTrashModalHelper(docCollection);
|
||||||
@@ -94,7 +97,7 @@ export function useRegisterBlocksuiteEditorCommands() {
|
|||||||
? t['com.affine.favoritePageOperation.remove']()
|
? t['com.affine.favoritePageOperation.remove']()
|
||||||
: t['com.affine.favoritePageOperation.add'](),
|
: t['com.affine.favoritePageOperation.add'](),
|
||||||
run() {
|
run() {
|
||||||
toggleFavorite(pageId);
|
favAdapter.toggle(pageId, 'doc');
|
||||||
toast(
|
toast(
|
||||||
favorite
|
favorite
|
||||||
? t['com.affine.cmdk.affine.editor.remove-from-favourites']()
|
? t['com.affine.cmdk.affine.editor.remove-from-favourites']()
|
||||||
@@ -246,11 +249,11 @@ export function useRegisterBlocksuiteEditorCommands() {
|
|||||||
pageId,
|
pageId,
|
||||||
restoreFromTrash,
|
restoreFromTrash,
|
||||||
t,
|
t,
|
||||||
toggleFavorite,
|
|
||||||
trash,
|
trash,
|
||||||
isCloudWorkspace,
|
isCloudWorkspace,
|
||||||
openHistoryModal,
|
openHistoryModal,
|
||||||
duplicate,
|
duplicate,
|
||||||
page,
|
page,
|
||||||
|
favAdapter,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import { toast } from '@affine/component';
|
import { toast } from '@affine/component';
|
||||||
import type { DraggableTitleCellData } from '@affine/core/components/page-list';
|
import type { DraggableTitleCellData } from '@affine/core/components/page-list';
|
||||||
import { useDocMetaHelper } from '@affine/core/hooks/use-block-suite-page-meta';
|
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 { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||||
import type { DragEndEvent, UniqueIdentifier } from '@dnd-kit/core';
|
import type { DragEndEvent, UniqueIdentifier } from '@dnd-kit/core';
|
||||||
import { useService, Workspace } from '@toeverything/infra';
|
import { useService, Workspace } from '@toeverything/infra';
|
||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
|
|
||||||
import { useBlockSuiteMetaHelper } from './use-block-suite-meta-helper';
|
|
||||||
import { useTrashModalHelper } from './use-trash-modal-helper';
|
import { useTrashModalHelper } from './use-trash-modal-helper';
|
||||||
|
|
||||||
// Unique droppable IDs
|
// Unique droppable IDs
|
||||||
@@ -69,10 +69,10 @@ export function getDragItemId(
|
|||||||
export const useSidebarDrag = () => {
|
export const useSidebarDrag = () => {
|
||||||
const t = useAFFiNEI18N();
|
const t = useAFFiNEI18N();
|
||||||
const currentWorkspace = useService(Workspace);
|
const currentWorkspace = useService(Workspace);
|
||||||
|
const favAdapter = useService(FavoriteItemsAdapter);
|
||||||
const workspace = currentWorkspace.docCollection;
|
const workspace = currentWorkspace.docCollection;
|
||||||
const { setTrashModal } = useTrashModalHelper(workspace);
|
const { setTrashModal } = useTrashModalHelper(workspace);
|
||||||
const { addToFavorite, removeFromFavorite } =
|
|
||||||
useBlockSuiteMetaHelper(workspace);
|
|
||||||
const { getDocMeta } = useDocMetaHelper(workspace);
|
const { getDocMeta } = useDocMetaHelper(workspace);
|
||||||
|
|
||||||
const isDropArea = useCallback(
|
const isDropArea = useCallback(
|
||||||
@@ -123,7 +123,7 @@ export const useSidebarDrag = () => {
|
|||||||
const processFavouritesDrag = useCallback(
|
const processFavouritesDrag = useCallback(
|
||||||
(e: DragEndEvent) => {
|
(e: DragEndEvent) => {
|
||||||
const { pageId } = e.active.data.current as DraggableTitleCellData;
|
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(
|
const isFavouriteDrag = String(e.over?.id).startsWith(
|
||||||
DropPrefix.SidebarFavorites
|
DropPrefix.SidebarFavorites
|
||||||
);
|
);
|
||||||
@@ -131,11 +131,11 @@ export const useSidebarDrag = () => {
|
|||||||
return toast(t['com.affine.collection.addPage.alreadyExists']());
|
return toast(t['com.affine.collection.addPage.alreadyExists']());
|
||||||
}
|
}
|
||||||
processDrag(e, DropPrefix.SidebarFavorites, pageId => {
|
processDrag(e, DropPrefix.SidebarFavorites, pageId => {
|
||||||
addToFavorite(pageId);
|
favAdapter.set(pageId, 'doc', true);
|
||||||
toast(t['com.affine.cmdk.affine.editor.add-to-favourites']());
|
toast(t['com.affine.cmdk.affine.editor.add-to-favourites']());
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[getDocMeta, processDrag, addToFavorite, t]
|
[processDrag, t, favAdapter]
|
||||||
);
|
);
|
||||||
|
|
||||||
const processRemoveDrag = useCallback(
|
const processRemoveDrag = useCallback(
|
||||||
@@ -146,7 +146,7 @@ export const useSidebarDrag = () => {
|
|||||||
|
|
||||||
if (String(e.active.id).startsWith(DragPrefix.FavouriteListItem)) {
|
if (String(e.active.id).startsWith(DragPrefix.FavouriteListItem)) {
|
||||||
const pageId = e.active.data.current?.pageId;
|
const pageId = e.active.data.current?.pageId;
|
||||||
removeFromFavorite(pageId);
|
favAdapter.remove(pageId, 'doc');
|
||||||
toast(t['com.affine.cmdk.affine.editor.remove-from-favourites']());
|
toast(t['com.affine.cmdk.affine.editor.remove-from-favourites']());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -155,7 +155,7 @@ export const useSidebarDrag = () => {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
[removeFromFavorite, t]
|
[favAdapter, t]
|
||||||
);
|
);
|
||||||
|
|
||||||
return useCallback(
|
return useCallback(
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import { TagService } from './tag';
|
|||||||
import { Workbench } from './workbench';
|
import { Workbench } from './workbench';
|
||||||
import {
|
import {
|
||||||
CurrentWorkspaceService,
|
CurrentWorkspaceService,
|
||||||
|
FavoriteItemsAdapter,
|
||||||
WorkspaceLegacyProperties,
|
WorkspaceLegacyProperties,
|
||||||
WorkspacePropertiesAdapter,
|
WorkspacePropertiesAdapter,
|
||||||
} from './workspace';
|
} from './workspace';
|
||||||
@@ -30,6 +31,7 @@ export function configureBusinessServices(services: ServiceCollection) {
|
|||||||
.add(Navigator, [Workbench])
|
.add(Navigator, [Workbench])
|
||||||
.add(RightSidebar, [GlobalState])
|
.add(RightSidebar, [GlobalState])
|
||||||
.add(WorkspacePropertiesAdapter, [Workspace])
|
.add(WorkspacePropertiesAdapter, [Workspace])
|
||||||
|
.add(FavoriteItemsAdapter, [WorkspacePropertiesAdapter])
|
||||||
.add(CollectionService, [Workspace])
|
.add(CollectionService, [Workspace])
|
||||||
.add(WorkspaceLegacyProperties, [Workspace])
|
.add(WorkspaceLegacyProperties, [Workspace])
|
||||||
.add(TagService, [WorkspaceLegacyProperties, PageRecordList]);
|
.add(TagService, [WorkspaceLegacyProperties, PageRecordList]);
|
||||||
|
|||||||
@@ -1,16 +1,17 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
||||||
// the adapter is to bridge the workspace rootdoc & native js bindings
|
// the adapter is to bridge the workspace rootdoc & native js bindings
|
||||||
|
import { createFractionalIndexingSortableHelper } from '@affine/core/utils';
|
||||||
import type { Y } from '@blocksuite/store';
|
import { createYProxy, type Y } from '@blocksuite/store';
|
||||||
import { createYProxy } from '@blocksuite/store';
|
import { LiveData, type Workspace } from '@toeverything/infra';
|
||||||
import type { Workspace } from '@toeverything/infra';
|
|
||||||
import { defaultsDeep } from 'lodash-es';
|
import { defaultsDeep } from 'lodash-es';
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
|
||||||
import type {
|
import {
|
||||||
WorkspaceAffineProperties,
|
PagePropertyType,
|
||||||
WorkspaceFavoriteItem,
|
PageSystemPropertyId,
|
||||||
|
type WorkspaceAffineProperties,
|
||||||
|
type WorkspaceFavoriteItem,
|
||||||
} from './schema';
|
} from './schema';
|
||||||
import { PagePropertyType, PageSystemPropertyId } from './schema';
|
|
||||||
|
|
||||||
const AFFINE_PROPERTIES_ID = 'affine:workspace-properties';
|
const AFFINE_PROPERTIES_ID = 'affine:workspace-properties';
|
||||||
|
|
||||||
@@ -27,6 +28,7 @@ export class WorkspacePropertiesAdapter {
|
|||||||
// provides a easy-to-use interface for workspace properties
|
// provides a easy-to-use interface for workspace properties
|
||||||
public readonly proxy: WorkspaceAffineProperties;
|
public readonly proxy: WorkspaceAffineProperties;
|
||||||
public readonly properties: Y.Map<any>;
|
public readonly properties: Y.Map<any>;
|
||||||
|
public readonly properties$: LiveData<WorkspaceAffineProperties>;
|
||||||
|
|
||||||
private ensuredRoot = false;
|
private ensuredRoot = false;
|
||||||
private ensuredPages = {} as Record<string, boolean>;
|
private ensuredPages = {} as Record<string, boolean>;
|
||||||
@@ -36,9 +38,25 @@ export class WorkspacePropertiesAdapter {
|
|||||||
const rootDoc = workspace.docCollection.doc;
|
const rootDoc = workspace.docCollection.doc;
|
||||||
this.properties = rootDoc.getMap(AFFINE_PROPERTIES_ID);
|
this.properties = rootDoc.getMap(AFFINE_PROPERTIES_ID);
|
||||||
this.proxy = createYProxy(this.properties);
|
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) {
|
if (this.ensuredRoot) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -120,10 +138,6 @@ export class WorkspacePropertiesAdapter {
|
|||||||
return this.pageProperties?.[pageId] ?? null;
|
return this.pageProperties?.[pageId] ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
isFavorite(id: string, type: WorkspaceFavoriteItem['type']) {
|
|
||||||
return this.favorites?.[id]?.type === type;
|
|
||||||
}
|
|
||||||
|
|
||||||
getJournalPageDateString(id: string) {
|
getJournalPageDateString(id: string) {
|
||||||
return this.pageProperties?.[id]?.system[PageSystemPropertyId.Journal]
|
return this.pageProperties?.[id]?.system[PageSystemPropertyId.Journal]
|
||||||
?.value;
|
?.value;
|
||||||
@@ -135,3 +149,119 @@ export class WorkspacePropertiesAdapter {
|
|||||||
pageProperties!.system[PageSystemPropertyId.Journal].value = date;
|
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 ======
|
// ====== workspace properties schema ======
|
||||||
export const WorkspaceFavoriteItemSchema = z.object({
|
export const WorkspaceFavoriteItemSchema = z.object({
|
||||||
id: z.string(),
|
id: z.string(),
|
||||||
order: z.number(),
|
order: z.string(),
|
||||||
type: z.enum(['page', 'collection']),
|
type: z.enum(['doc', 'collection']),
|
||||||
|
value: z.boolean(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export type WorkspaceFavoriteItem = z.infer<typeof WorkspaceFavoriteItemSchema>;
|
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 './create-emotion-cache';
|
||||||
|
export * from './fractional-indexing';
|
||||||
export * from './intl-formatter';
|
export * from './intl-formatter';
|
||||||
export * from './mixpanel';
|
export * from './mixpanel';
|
||||||
export * from './string2color';
|
export * from './string2color';
|
||||||
|
|||||||
@@ -43,11 +43,12 @@ export const AffineOperationCell: StoryFn<PageOperationCellProps> = ({
|
|||||||
}) => <PageOperationCell {...props} />;
|
}) => <PageOperationCell {...props} />;
|
||||||
|
|
||||||
AffineOperationCell.args = {
|
AffineOperationCell.args = {
|
||||||
favorite: false,
|
page: {
|
||||||
isPublic: true,
|
id: '123',
|
||||||
onToggleFavoritePage: () => toast('Toggle favorite page'),
|
title: 'Test Page Title',
|
||||||
onDisablePublicSharing: () => toast('Disable public sharing'),
|
tags: ['tag1', 'tag2'],
|
||||||
onRemoveToTrash: () => toast('Remove to trash'),
|
createDate: new Date('2021-01-01').getTime(),
|
||||||
|
},
|
||||||
};
|
};
|
||||||
AffineOperationCell.parameters = {
|
AffineOperationCell.parameters = {
|
||||||
reactRouter: reactRouterParameters({
|
reactRouter: reactRouterParameters({
|
||||||
|
|||||||
3
tools/@types/env/__all.d.ts
vendored
3
tools/@types/env/__all.d.ts
vendored
@@ -20,6 +20,9 @@ declare global {
|
|||||||
|
|
||||||
declare module '@blocksuite/store' {
|
declare module '@blocksuite/store' {
|
||||||
interface DocMeta {
|
interface DocMeta {
|
||||||
|
/**
|
||||||
|
* @deprecated
|
||||||
|
*/
|
||||||
favorite?: boolean;
|
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
|
// 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;
|
trashRelate?: string;
|
||||||
|
|||||||
Reference in New Issue
Block a user