mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-24 18:02:47 +08:00
feat(core): new favorite (#7590)
This commit is contained in:
7
packages/common/env/src/global.ts
vendored
7
packages/common/env/src/global.ts
vendored
@@ -31,6 +31,13 @@ export const runtimeFlagsSchema = z.object({
|
||||
enableExperimentalFeature: z.boolean(),
|
||||
enableInfoModal: z.boolean(),
|
||||
enableOrganize: z.boolean(),
|
||||
// show the new favorite, which exclusive to each user
|
||||
enableNewFavorite: z.boolean(),
|
||||
// show the old favorite
|
||||
enableOldFavorite: z.boolean(),
|
||||
// before 0.16, enableNewFavorite = false and enableOldFavorite = true
|
||||
// after 0.16, enableNewFavorite = true and enableOldFavorite = false
|
||||
// for debug purpose, we can enable both
|
||||
});
|
||||
|
||||
export type RuntimeConfig = z.infer<typeof runtimeFlagsSchema>;
|
||||
|
||||
@@ -15,7 +15,7 @@ export * from './sync';
|
||||
export * from './utils';
|
||||
|
||||
import type { Framework } from './framework';
|
||||
import { configureDBModule } from './modules/db';
|
||||
import { configureWorkspaceDBModule } from './modules/db';
|
||||
import { configureDocModule } from './modules/doc';
|
||||
import { configureGlobalContextModule } from './modules/global-context';
|
||||
import { configureLifecycleModule } from './modules/lifecycle';
|
||||
@@ -31,7 +31,7 @@ import {
|
||||
export function configureInfraModules(framework: Framework) {
|
||||
configureWorkspaceModule(framework);
|
||||
configureDocModule(framework);
|
||||
configureDBModule(framework);
|
||||
configureWorkspaceDBModule(framework);
|
||||
configureGlobalStorageModule(framework);
|
||||
configureGlobalContextModule(framework);
|
||||
configureLifecycleModule(framework);
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import type { Framework } from '../../framework';
|
||||
import { WorkspaceScope, WorkspaceService } from '../workspace';
|
||||
import { DBService } from './services/db';
|
||||
import { WorkspaceDBService } from './services/db';
|
||||
|
||||
export { AFFiNE_DB_SCHEMA } from './schema';
|
||||
export { DBService } from './services/db';
|
||||
export { AFFiNE_WORKSPACE_DB_SCHEMA } from './schema';
|
||||
export { WorkspaceDBService } from './services/db';
|
||||
|
||||
export function configureDBModule(framework: Framework) {
|
||||
framework.scope(WorkspaceScope).service(DBService, [WorkspaceService]);
|
||||
export function configureWorkspaceDBModule(framework: Framework) {
|
||||
framework
|
||||
.scope(WorkspaceScope)
|
||||
.service(WorkspaceDBService, [WorkspaceService]);
|
||||
}
|
||||
|
||||
@@ -1 +1 @@
|
||||
export { AFFiNE_DB_SCHEMA } from './schema';
|
||||
export { AFFiNE_WORKSPACE_DB_SCHEMA } from './schema';
|
||||
|
||||
@@ -2,7 +2,7 @@ import { nanoid } from 'nanoid';
|
||||
|
||||
import { type DBSchemaBuilder, f } from '../../../orm';
|
||||
|
||||
export const AFFiNE_DB_SCHEMA = {
|
||||
export const AFFiNE_WORKSPACE_DB_SCHEMA = {
|
||||
folders: {
|
||||
id: f.string().primaryKey().optional().default(nanoid),
|
||||
parentId: f.string().optional(),
|
||||
@@ -11,4 +11,13 @@ export const AFFiNE_DB_SCHEMA = {
|
||||
index: f.string(),
|
||||
},
|
||||
} as const satisfies DBSchemaBuilder;
|
||||
export type AFFiNE_DB_SCHEMA = typeof AFFiNE_DB_SCHEMA;
|
||||
export type AFFiNE_WORKSPACE_DB_SCHEMA = typeof AFFiNE_WORKSPACE_DB_SCHEMA;
|
||||
|
||||
export const AFFiNE_WORKSPACE_USERDATA_DB_SCHEMA = {
|
||||
favorite: {
|
||||
key: f.string().primaryKey(),
|
||||
index: f.string(),
|
||||
},
|
||||
} as const satisfies DBSchemaBuilder;
|
||||
export type AFFiNE_WORKSPACE_USERDATA_DB_SCHEMA =
|
||||
typeof AFFiNE_WORKSPACE_USERDATA_DB_SCHEMA;
|
||||
|
||||
@@ -2,17 +2,29 @@ import { Doc as YDoc } from 'yjs';
|
||||
|
||||
import { Service } from '../../../framework';
|
||||
import { createORMClient, type TableMap, YjsDBAdapter } from '../../../orm';
|
||||
import { ObjectPool } from '../../../utils';
|
||||
import type { WorkspaceService } from '../../workspace';
|
||||
import { AFFiNE_DB_SCHEMA } from '../schema';
|
||||
import { AFFiNE_WORKSPACE_DB_SCHEMA } from '../schema';
|
||||
import { AFFiNE_WORKSPACE_USERDATA_DB_SCHEMA } from '../schema/schema';
|
||||
|
||||
export class DBService extends Service {
|
||||
db: TableMap<AFFiNE_DB_SCHEMA>;
|
||||
const WorkspaceDBClient = createORMClient(AFFiNE_WORKSPACE_DB_SCHEMA);
|
||||
const WorkspaceUserdataDBClient = createORMClient(
|
||||
AFFiNE_WORKSPACE_USERDATA_DB_SCHEMA
|
||||
);
|
||||
type WorkspaceUserdataDBClient = InstanceType<typeof WorkspaceUserdataDBClient>;
|
||||
|
||||
export class WorkspaceDBService extends Service {
|
||||
db: TableMap<AFFiNE_WORKSPACE_DB_SCHEMA>;
|
||||
userdataDBPool = new ObjectPool<string, WorkspaceUserdataDBClient>({
|
||||
onDangling() {
|
||||
return false; // never release
|
||||
},
|
||||
});
|
||||
|
||||
constructor(private readonly workspaceService: WorkspaceService) {
|
||||
super();
|
||||
const Client = createORMClient(AFFiNE_DB_SCHEMA);
|
||||
this.db = new Client(
|
||||
new YjsDBAdapter(AFFiNE_DB_SCHEMA, {
|
||||
this.db = new WorkspaceDBClient(
|
||||
new YjsDBAdapter(AFFiNE_WORKSPACE_DB_SCHEMA, {
|
||||
getDoc: guid => {
|
||||
const ydoc = new YDoc({
|
||||
// guid format: db${workspaceId}${guid}
|
||||
@@ -26,7 +38,33 @@ export class DBService extends Service {
|
||||
);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||
userdataDB(userId: (string & {}) | '__local__') {
|
||||
// __local__ for local workspace
|
||||
const userdataDb = this.userdataDBPool.get(userId);
|
||||
if (userdataDb) {
|
||||
return userdataDb.obj;
|
||||
}
|
||||
|
||||
const newDB = new WorkspaceUserdataDBClient(
|
||||
new YjsDBAdapter(AFFiNE_WORKSPACE_USERDATA_DB_SCHEMA, {
|
||||
getDoc: guid => {
|
||||
const ydoc = new YDoc({
|
||||
// guid format: userdata${userId}${workspaceId}${guid}
|
||||
guid: `userdata$${userId}$${this.workspaceService.workspace.id}$${guid}`,
|
||||
});
|
||||
this.workspaceService.workspace.engine.doc.addDoc(ydoc, false);
|
||||
this.workspaceService.workspace.engine.doc.setPriority(ydoc.guid, 50);
|
||||
return ydoc;
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
this.userdataDBPool.put(userId, newDB);
|
||||
return newDB;
|
||||
}
|
||||
|
||||
static isDBDocId(docId: string) {
|
||||
return docId.startsWith('db$');
|
||||
return docId.startsWith('db$') || docId.startsWith('userdata$');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
export * from './core';
|
||||
export type { DBSchemaBuilder, FieldSchemaBuilder, TableMap } from './core';
|
||||
export { createORMClient, f, YjsDBAdapter } from './core';
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { cssVar } from '@toeverything/theme';
|
||||
import { style } from '@vanilla-extract/css';
|
||||
|
||||
export const root = style({
|
||||
fontSize: cssVar('fontXs'),
|
||||
minHeight: '16px',
|
||||
@@ -10,6 +11,7 @@ export const root = style({
|
||||
justifyContent: 'space-between',
|
||||
marginBottom: '4px',
|
||||
padding: '0 8px',
|
||||
gap: '8px',
|
||||
selectors: {
|
||||
'&:not(:first-of-type)': {
|
||||
marginTop: '16px',
|
||||
@@ -18,4 +20,5 @@ export const root = style({
|
||||
});
|
||||
export const label = style({
|
||||
color: cssVar('black30'),
|
||||
flex: '1',
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { FavoriteTag } from '@affine/core/components/page-list';
|
||||
import { FavoriteItemsAdapter } from '@affine/core/modules/properties';
|
||||
import { CompatibleFavoriteItemsAdapter } from '@affine/core/modules/properties';
|
||||
import { toast } from '@affine/core/utils';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
import { useLiveData, useService } from '@toeverything/infra';
|
||||
@@ -11,7 +11,7 @@ export interface FavoriteButtonProps {
|
||||
|
||||
export const useFavorite = (pageId: string) => {
|
||||
const t = useI18n();
|
||||
const favAdapter = useService(FavoriteItemsAdapter);
|
||||
const favAdapter = useService(CompatibleFavoriteItemsAdapter);
|
||||
|
||||
const favorite = useLiveData(favAdapter.isFavorite$(pageId, 'doc'));
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { FavoriteItemsAdapter } from '@affine/core/modules/properties';
|
||||
import { CompatibleFavoriteItemsAdapter } from '@affine/core/modules/properties';
|
||||
import type { Tag } from '@affine/core/modules/tag';
|
||||
import { TagService } from '@affine/core/modules/tag';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
@@ -131,9 +131,9 @@ export const useTagGroupDefinitions = (): ItemGroupDefinition<ListItem>[] => {
|
||||
const sortedTagsLiveData$ = useMemo(
|
||||
() =>
|
||||
LiveData.computed(get =>
|
||||
get(tagList.tags$).sort((a, b) =>
|
||||
get(a.value$).localeCompare(get(b.value$))
|
||||
)
|
||||
get(tagList.tags$)
|
||||
.slice()
|
||||
.sort((a, b) => get(a.value$).localeCompare(get(b.value$)))
|
||||
),
|
||||
[tagList.tags$]
|
||||
);
|
||||
@@ -174,7 +174,7 @@ export const useFavoriteGroupDefinitions = <
|
||||
T extends ListItem,
|
||||
>(): ItemGroupDefinition<T>[] => {
|
||||
const t = useI18n();
|
||||
const favAdapter = useService(FavoriteItemsAdapter);
|
||||
const favAdapter = useService(CompatibleFavoriteItemsAdapter);
|
||||
const favourites = useLiveData(favAdapter.favorites$);
|
||||
return useMemo(
|
||||
() => [
|
||||
|
||||
@@ -3,6 +3,7 @@ import { createContainer, style } from '@vanilla-extract/css';
|
||||
|
||||
import { root as collectionItemRoot } from './collections/collection-list-item.css';
|
||||
import { root as pageItemRoot } from './docs/page-list-item.css';
|
||||
import { root as tagItemRoot } from './tags/tag-list-item.css';
|
||||
export const listRootContainer = createContainer('list-root-container');
|
||||
export const pageListScrollContainer = style({
|
||||
width: '100%',
|
||||
@@ -49,7 +50,7 @@ export const favoriteCell = style({
|
||||
flexShrink: 0,
|
||||
opacity: 0,
|
||||
selectors: {
|
||||
[`&[data-favorite], ${pageItemRoot}:hover &, ${collectionItemRoot}:hover &`]:
|
||||
[`&[data-favorite], ${pageItemRoot}:hover &, ${collectionItemRoot}:hover &, ${tagItemRoot}:hover &`]:
|
||||
{
|
||||
opacity: 1,
|
||||
},
|
||||
|
||||
@@ -10,7 +10,8 @@ import {
|
||||
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 { FavoriteItemsAdapter } from '@affine/core/modules/properties';
|
||||
import { FavoriteService } from '@affine/core/modules/favorite';
|
||||
import { CompatibleFavoriteItemsAdapter } from '@affine/core/modules/properties';
|
||||
import { WorkbenchService } from '@affine/core/modules/workbench';
|
||||
import { mixpanel } from '@affine/core/utils';
|
||||
import type { Collection, DeleteCollectionInfo } from '@affine/env/filter';
|
||||
@@ -32,7 +33,12 @@ import {
|
||||
SplitViewIcon,
|
||||
} from '@blocksuite/icons/rc';
|
||||
import type { DocMeta } from '@blocksuite/store';
|
||||
import { useLiveData, useService, WorkspaceService } from '@toeverything/infra';
|
||||
import {
|
||||
useLiveData,
|
||||
useService,
|
||||
useServices,
|
||||
WorkspaceService,
|
||||
} from '@toeverything/infra';
|
||||
import { useCallback, useState } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
@@ -63,7 +69,7 @@ export const PageOperationCell = ({
|
||||
const { appSettings } = useAppSettingHelper();
|
||||
const { setTrashModal } = useTrashModalHelper(currentWorkspace.docCollection);
|
||||
const [openDisableShared, setOpenDisableShared] = useState(false);
|
||||
const favAdapter = useService(FavoriteItemsAdapter);
|
||||
const favAdapter = useService(CompatibleFavoriteItemsAdapter);
|
||||
const favourite = useLiveData(favAdapter.isFavorite$(page.id, 'doc'));
|
||||
const workbench = useService(WorkbenchService).workbench;
|
||||
const { duplicate } = useBlockSuiteMetaHelper(currentWorkspace.docCollection);
|
||||
@@ -215,14 +221,16 @@ export const PageOperationCell = ({
|
||||
);
|
||||
return (
|
||||
<>
|
||||
<ColWrapper
|
||||
hideInSmallContainer
|
||||
data-testid="page-list-item-favorite"
|
||||
data-favorite={favourite ? true : undefined}
|
||||
className={styles.favoriteCell}
|
||||
>
|
||||
<FavoriteTag onClick={onToggleFavoritePage} active={favourite} />
|
||||
</ColWrapper>
|
||||
{runtimeConfig.enableNewFavorite && (
|
||||
<ColWrapper
|
||||
hideInSmallContainer
|
||||
data-testid="page-list-item-favorite"
|
||||
data-favorite={favourite ? true : undefined}
|
||||
className={styles.favoriteCell}
|
||||
>
|
||||
<FavoriteTag onClick={onToggleFavoritePage} active={favourite} />
|
||||
</ColWrapper>
|
||||
)}
|
||||
<ColWrapper alignment="start">
|
||||
<Menu
|
||||
items={OperationMenu}
|
||||
@@ -319,7 +327,7 @@ export const CollectionOperationCell = ({
|
||||
}: CollectionOperationCellProps) => {
|
||||
const t = useI18n();
|
||||
|
||||
const favAdapter = useService(FavoriteItemsAdapter);
|
||||
const favAdapter = useService(CompatibleFavoriteItemsAdapter);
|
||||
const docCollection = useService(WorkspaceService).workspace.docCollection;
|
||||
const { createPage } = usePageHelper(docCollection);
|
||||
const { openConfirmModal } = useConfirmModal();
|
||||
@@ -482,12 +490,31 @@ export const TagOperationCell = ({
|
||||
}: TagOperationCellProps) => {
|
||||
const t = useI18n();
|
||||
const [open, setOpen] = useState(false);
|
||||
const { favoriteService } = useServices({
|
||||
FavoriteService,
|
||||
});
|
||||
const favourite = useLiveData(
|
||||
favoriteService.favoriteList.isFavorite$('tag', tag.id)
|
||||
);
|
||||
|
||||
const handleDelete = useCallback(() => {
|
||||
onTagDelete([tag.id]);
|
||||
}, [onTagDelete, tag.id]);
|
||||
|
||||
const onToggleFavoriteCollection = useCallback(() => {
|
||||
favoriteService.favoriteList.toggle('tag', tag.id);
|
||||
}, [favoriteService, tag.id]);
|
||||
return (
|
||||
<>
|
||||
<ColWrapper
|
||||
hideInSmallContainer
|
||||
data-testid="page-list-item-favorite"
|
||||
data-favorite={favourite ? true : undefined}
|
||||
className={styles.favoriteCell}
|
||||
>
|
||||
<FavoriteTag onClick={onToggleFavoriteCollection} active={favourite} />
|
||||
</ColWrapper>
|
||||
|
||||
<div className={styles.editTagWrapper} data-show={open}>
|
||||
<div style={{ width: '100%' }}>
|
||||
<CreateOrEditTag open={open} onOpenChange={setOpen} tagMeta={tag} />
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { FavoriteItemsAdapter } from '@affine/core/modules/properties';
|
||||
import { CompatibleFavoriteItemsAdapter } from '@affine/core/modules/properties';
|
||||
import { ShareDocsService } from '@affine/core/modules/share-doc';
|
||||
import type { Collection, Filter } from '@affine/env/filter';
|
||||
import { PublicPageMode } from '@affine/graphql';
|
||||
@@ -36,7 +36,7 @@ export const useFilteredPageMetas = (
|
||||
shareDocsService.shareDocs?.revalidate();
|
||||
}, [shareDocsService]);
|
||||
|
||||
const favAdapter = useService(FavoriteItemsAdapter);
|
||||
const favAdapter = useService(CompatibleFavoriteItemsAdapter);
|
||||
const favoriteItems = useLiveData(favAdapter.favorites$);
|
||||
|
||||
const filteredPageMetas = useMemo(
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { MenuItemProps } from '@affine/component';
|
||||
import { Menu, MenuIcon, MenuItem } from '@affine/component';
|
||||
import { useAppSettingHelper } from '@affine/core/hooks/affine/use-app-setting-helper';
|
||||
import { useDeleteCollectionInfo } from '@affine/core/hooks/affine/use-delete-collection-info';
|
||||
import { FavoriteItemsAdapter } from '@affine/core/modules/properties';
|
||||
import { CompatibleFavoriteItemsAdapter } from '@affine/core/modules/properties';
|
||||
import { WorkbenchService } from '@affine/core/modules/workbench';
|
||||
import type { Collection } from '@affine/env/filter';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
@@ -79,7 +79,7 @@ export const CollectionOperations = ({
|
||||
workbench.openCollection(collection.id, { at: 'tail' });
|
||||
}, [collection.id, workbench]);
|
||||
|
||||
const favAdapter = useService(FavoriteItemsAdapter);
|
||||
const favAdapter = useService(CompatibleFavoriteItemsAdapter);
|
||||
|
||||
const onToggleFavoritePage = useCallback(() => {
|
||||
favAdapter.toggle(collection.id, 'collection');
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Tooltip } from '@affine/component';
|
||||
import { FavoriteItemsAdapter } from '@affine/core/modules/properties';
|
||||
import { CompatibleFavoriteItemsAdapter } from '@affine/core/modules/properties';
|
||||
import type { Collection } from '@affine/env/filter';
|
||||
import { Trans, useI18n } from '@affine/i18n';
|
||||
import {
|
||||
@@ -42,7 +42,7 @@ export const RulesMode = ({
|
||||
const [showPreview, setShowPreview] = useState(true);
|
||||
const allowListPages: DocMeta[] = [];
|
||||
const rulesPages: DocMeta[] = [];
|
||||
const favAdapter = useService(FavoriteItemsAdapter);
|
||||
const favAdapter = useService(CompatibleFavoriteItemsAdapter);
|
||||
const favorites = useLiveData(favAdapter.favorites$);
|
||||
allPageListConfig.allPages.forEach(meta => {
|
||||
if (meta.trash) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Menu, toast } from '@affine/component';
|
||||
import { useBlockSuiteDocMeta } from '@affine/core/hooks/use-block-suite-page-meta';
|
||||
import { FavoriteItemsAdapter } from '@affine/core/modules/properties';
|
||||
import { CompatibleFavoriteItemsAdapter } from '@affine/core/modules/properties';
|
||||
import { ShareDocsService } from '@affine/core/modules/share-doc';
|
||||
import { PublicPageMode } from '@affine/graphql';
|
||||
import { Trans, useI18n } from '@affine/i18n';
|
||||
@@ -60,20 +60,20 @@ export const SelectPage = ({
|
||||
}, [onChange]);
|
||||
const {
|
||||
workspaceService,
|
||||
favoriteItemsAdapter,
|
||||
compatibleFavoriteItemsAdapter,
|
||||
shareDocsService,
|
||||
docsService,
|
||||
} = useServices({
|
||||
DocsService,
|
||||
ShareDocsService,
|
||||
WorkspaceService,
|
||||
FavoriteItemsAdapter,
|
||||
CompatibleFavoriteItemsAdapter,
|
||||
});
|
||||
const shareDocs = useLiveData(shareDocsService.shareDocs?.list$);
|
||||
const workspace = workspaceService.workspace;
|
||||
const docCollection = workspace.docCollection;
|
||||
const pageMetas = useBlockSuiteDocMeta(docCollection);
|
||||
const favourites = useLiveData(favoriteItemsAdapter.favorites$);
|
||||
const favourites = useLiveData(compatibleFavoriteItemsAdapter.favorites$);
|
||||
|
||||
useEffect(() => {
|
||||
shareDocsService.shareDocs?.revalidate();
|
||||
@@ -108,14 +108,14 @@ export const SelectPage = ({
|
||||
const onToggleFavoritePage = useCallback(
|
||||
(page: DocMeta) => {
|
||||
const status = isFavorite(page);
|
||||
favoriteItemsAdapter.toggle(page.id, 'doc');
|
||||
compatibleFavoriteItemsAdapter.toggle(page.id, 'doc');
|
||||
toast(
|
||||
status
|
||||
? t['com.affine.toastMessage.removedFavorites']()
|
||||
: t['com.affine.toastMessage.addedFavorites']()
|
||||
);
|
||||
},
|
||||
[favoriteItemsAdapter, isFavorite, t]
|
||||
[compatibleFavoriteItemsAdapter, isFavorite, t]
|
||||
);
|
||||
|
||||
const pageHeaderColsDef = usePageHeaderColsDef();
|
||||
|
||||
@@ -2,6 +2,8 @@ import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks';
|
||||
import {
|
||||
ExplorerCollections,
|
||||
ExplorerFavorites,
|
||||
ExplorerMigrationFavorites,
|
||||
ExplorerOldFavorites,
|
||||
ExplorerOrganize,
|
||||
} from '@affine/core/modules/explorer';
|
||||
import { ExplorerTags } from '@affine/core/modules/explorer/views/sections/tags';
|
||||
@@ -161,8 +163,10 @@ export const RootAppSidebar = memo(
|
||||
</MenuItem>
|
||||
</SidebarContainer>
|
||||
<SidebarScrollableContainer>
|
||||
{runtimeConfig.enableNewFavorite && <ExplorerFavorites />}
|
||||
{runtimeConfig.enableOrganize && <ExplorerOrganize />}
|
||||
<ExplorerFavorites />
|
||||
{runtimeConfig.enableNewFavorite && <ExplorerMigrationFavorites />}
|
||||
{runtimeConfig.enableOldFavorite && <ExplorerOldFavorites />}
|
||||
<ExplorerCollections />
|
||||
<ExplorerTags />
|
||||
<CategoryDivider label={t['com.affine.rootAppSidebar.others']()} />
|
||||
|
||||
@@ -2,7 +2,7 @@ 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/properties';
|
||||
import { CompatibleFavoriteItemsAdapter } from '@affine/core/modules/properties';
|
||||
import { ShareDocsService } from '@affine/core/modules/share-doc';
|
||||
import { PublicPageMode } from '@affine/graphql';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
@@ -32,7 +32,7 @@ export const useAllPageListConfig = () => {
|
||||
() => Object.fromEntries(pageMetas.map(page => [page.id, page])),
|
||||
[pageMetas]
|
||||
);
|
||||
const favAdapter = useService(FavoriteItemsAdapter);
|
||||
const favAdapter = useService(CompatibleFavoriteItemsAdapter);
|
||||
const t = useI18n();
|
||||
const favoriteItems = useLiveData(favAdapter.favorites$);
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import {
|
||||
PreconditionStrategy,
|
||||
registerAffineCommand,
|
||||
} from '@affine/core/commands';
|
||||
import { FavoriteItemsAdapter } from '@affine/core/modules/properties';
|
||||
import { CompatibleFavoriteItemsAdapter } from '@affine/core/modules/properties';
|
||||
import { TelemetryWorkspaceContextService } from '@affine/core/modules/telemetry/services/telemetry';
|
||||
import { mixpanel } from '@affine/core/utils';
|
||||
import { WorkspaceFlavour } from '@affine/env/workspace';
|
||||
@@ -32,7 +32,7 @@ export function useRegisterBlocksuiteEditorCommands() {
|
||||
const workspace = useService(WorkspaceService).workspace;
|
||||
const docCollection = workspace.docCollection;
|
||||
|
||||
const favAdapter = useService(FavoriteItemsAdapter);
|
||||
const favAdapter = useService(CompatibleFavoriteItemsAdapter);
|
||||
const favorite = useLiveData(favAdapter.isFavorite$(docId, 'doc'));
|
||||
const trash = useLiveData(doc.trash$);
|
||||
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { DebugLogger } from '@affine/debug';
|
||||
import type { Job, JobQueue, WorkspaceService } from '@toeverything/infra';
|
||||
import {
|
||||
DBService,
|
||||
Entity,
|
||||
IndexedDBIndexStorage,
|
||||
IndexedDBJobQueue,
|
||||
JobRunner,
|
||||
LiveData,
|
||||
WorkspaceDBService,
|
||||
} from '@toeverything/infra';
|
||||
import { map } from 'rxjs';
|
||||
|
||||
@@ -69,7 +69,7 @@ export class DocsIndexer extends Entity {
|
||||
|
||||
setupListener() {
|
||||
this.workspaceEngine.doc.storage.eventBus.on(event => {
|
||||
if (DBService.isDBDocId(event.docId)) {
|
||||
if (WorkspaceDBService.isDBDocId(event.docId)) {
|
||||
// skip db doc
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
export { ExplorerCollections } from './views/sections/collections';
|
||||
export { ExplorerFavorites } from './views/sections/favorites';
|
||||
export { ExplorerMigrationFavorites } from './views/sections/migration-favorites';
|
||||
export { ExplorerOldFavorites } from './views/sections/old-favorites';
|
||||
export { ExplorerOrganize } from './views/sections/organize';
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
useEditCollection,
|
||||
} from '@affine/core/components/page-list';
|
||||
import { CollectionService } from '@affine/core/modules/collection';
|
||||
import { FavoriteItemsAdapter } from '@affine/core/modules/properties';
|
||||
import { CompatibleFavoriteItemsAdapter } from '@affine/core/modules/properties';
|
||||
import { ShareDocsService } from '@affine/core/modules/share-doc';
|
||||
import type { AffineDNDData } from '@affine/core/types/dnd';
|
||||
import type { Collection } from '@affine/env/filter';
|
||||
@@ -225,12 +225,12 @@ const ExplorerCollectionNodeChildren = ({
|
||||
const t = useI18n();
|
||||
const {
|
||||
docsService,
|
||||
favoriteItemsAdapter,
|
||||
compatibleFavoriteItemsAdapter,
|
||||
shareDocsService,
|
||||
collectionService,
|
||||
} = useServices({
|
||||
DocsService,
|
||||
FavoriteItemsAdapter,
|
||||
CompatibleFavoriteItemsAdapter,
|
||||
ShareDocsService,
|
||||
CollectionService,
|
||||
});
|
||||
@@ -251,7 +251,7 @@ const ExplorerCollectionNodeChildren = ({
|
||||
[docsService]
|
||||
)
|
||||
);
|
||||
const favourites = useLiveData(favoriteItemsAdapter.favorites$);
|
||||
const favourites = useLiveData(compatibleFavoriteItemsAdapter.favorites$);
|
||||
const allowList = useMemo(
|
||||
() => new Set(collection.allowList),
|
||||
[collection.allowList]
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
import { useAppSettingHelper } from '@affine/core/hooks/affine/use-app-setting-helper';
|
||||
import { useDeleteCollectionInfo } from '@affine/core/hooks/affine/use-delete-collection-info';
|
||||
import { CollectionService } from '@affine/core/modules/collection';
|
||||
import { FavoriteItemsAdapter } from '@affine/core/modules/properties';
|
||||
import { CompatibleFavoriteItemsAdapter } from '@affine/core/modules/properties';
|
||||
import { WorkbenchService } from '@affine/core/modules/workbench';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
import {
|
||||
@@ -35,19 +35,20 @@ export const useExplorerCollectionNodeOperations = (
|
||||
workbenchService,
|
||||
docsService,
|
||||
collectionService,
|
||||
favoriteItemsAdapter,
|
||||
compatibleFavoriteItemsAdapter,
|
||||
} = useServices({
|
||||
DocsService,
|
||||
WorkbenchService,
|
||||
CollectionService,
|
||||
FavoriteItemsAdapter,
|
||||
CompatibleFavoriteItemsAdapter,
|
||||
});
|
||||
const deleteInfo = useDeleteCollectionInfo();
|
||||
|
||||
const favorite = useLiveData(
|
||||
useMemo(
|
||||
() => favoriteItemsAdapter.isFavorite$(collectionId, 'collection'),
|
||||
[collectionId, favoriteItemsAdapter]
|
||||
() =>
|
||||
compatibleFavoriteItemsAdapter.isFavorite$(collectionId, 'collection'),
|
||||
[collectionId, compatibleFavoriteItemsAdapter]
|
||||
)
|
||||
);
|
||||
const { openConfirmModal } = useConfirmModal();
|
||||
@@ -66,8 +67,8 @@ export const useExplorerCollectionNodeOperations = (
|
||||
]);
|
||||
|
||||
const handleToggleFavoritePage = useCallback(() => {
|
||||
favoriteItemsAdapter.toggle(collectionId, 'collection');
|
||||
}, [favoriteItemsAdapter, collectionId]);
|
||||
compatibleFavoriteItemsAdapter.toggle(collectionId, 'collection');
|
||||
}, [compatibleFavoriteItemsAdapter, collectionId]);
|
||||
|
||||
const handleAddDocToCollection = useCallback(() => {
|
||||
openConfirmModal({
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
} from '@affine/component';
|
||||
import { useAppSettingHelper } from '@affine/core/hooks/affine/use-app-setting-helper';
|
||||
import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks';
|
||||
import { FavoriteItemsAdapter } from '@affine/core/modules/properties';
|
||||
import { CompatibleFavoriteItemsAdapter } from '@affine/core/modules/properties';
|
||||
import { WorkbenchService } from '@affine/core/modules/workbench';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
import {
|
||||
@@ -32,20 +32,20 @@ export const useExplorerDocNodeOperations = (
|
||||
): NodeOperation[] => {
|
||||
const t = useI18n();
|
||||
const { appSettings } = useAppSettingHelper();
|
||||
const { workbenchService, docsService, favoriteItemsAdapter } = useServices({
|
||||
DocsService,
|
||||
WorkbenchService,
|
||||
FavoriteItemsAdapter,
|
||||
});
|
||||
const { workbenchService, docsService, compatibleFavoriteItemsAdapter } =
|
||||
useServices({
|
||||
DocsService,
|
||||
WorkbenchService,
|
||||
CompatibleFavoriteItemsAdapter,
|
||||
});
|
||||
const { openConfirmModal } = useConfirmModal();
|
||||
|
||||
const docRecord = useLiveData(docsService.list.doc$(docId));
|
||||
|
||||
const favorite = useLiveData(
|
||||
useMemo(
|
||||
() => favoriteItemsAdapter.isFavorite$(docId, 'doc'),
|
||||
[docId, favoriteItemsAdapter]
|
||||
)
|
||||
useMemo(() => {
|
||||
return compatibleFavoriteItemsAdapter.isFavorite$(docId, 'doc');
|
||||
}, [docId, compatibleFavoriteItemsAdapter])
|
||||
);
|
||||
|
||||
const handleMoveToTrash = useCallback(() => {
|
||||
@@ -84,8 +84,8 @@ export const useExplorerDocNodeOperations = (
|
||||
}, [docId, options, docsService, workbenchService.workbench]);
|
||||
|
||||
const handleToggleFavoriteDoc = useCallback(() => {
|
||||
favoriteItemsAdapter.toggle(docId, 'doc');
|
||||
}, [favoriteItemsAdapter, docId]);
|
||||
compatibleFavoriteItemsAdapter.toggle(docId, 'doc');
|
||||
}, [docId, compatibleFavoriteItemsAdapter]);
|
||||
|
||||
return useMemo(
|
||||
() => [
|
||||
|
||||
@@ -6,10 +6,17 @@ import {
|
||||
toast,
|
||||
} from '@affine/component';
|
||||
import { useAppSettingHelper } from '@affine/core/hooks/affine/use-app-setting-helper';
|
||||
import { FavoriteService } from '@affine/core/modules/favorite';
|
||||
import { TagService } from '@affine/core/modules/tag';
|
||||
import { WorkbenchService } from '@affine/core/modules/workbench';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
import { DeleteIcon, PlusIcon, SplitViewIcon } from '@blocksuite/icons/rc';
|
||||
import {
|
||||
DeleteIcon,
|
||||
FavoritedIcon,
|
||||
FavoriteIcon,
|
||||
PlusIcon,
|
||||
SplitViewIcon,
|
||||
} from '@blocksuite/icons/rc';
|
||||
import { DocsService, useLiveData, useServices } from '@toeverything/infra';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
|
||||
@@ -25,12 +32,17 @@ export const useExplorerTagNodeOperations = (
|
||||
): NodeOperation[] => {
|
||||
const t = useI18n();
|
||||
const { appSettings } = useAppSettingHelper();
|
||||
const { docsService, workbenchService, tagService } = useServices({
|
||||
WorkbenchService,
|
||||
TagService,
|
||||
DocsService,
|
||||
});
|
||||
const { docsService, workbenchService, tagService, favoriteService } =
|
||||
useServices({
|
||||
WorkbenchService,
|
||||
TagService,
|
||||
DocsService,
|
||||
FavoriteService,
|
||||
});
|
||||
|
||||
const favorite = useLiveData(
|
||||
favoriteService.favoriteList.favorite$('tag', tagId)
|
||||
);
|
||||
const tagRecord = useLiveData(tagService.tagList.tagByTagId$(tagId));
|
||||
|
||||
const handleNewDoc = useCallback(() => {
|
||||
@@ -53,6 +65,10 @@ export const useExplorerTagNodeOperations = (
|
||||
});
|
||||
}, [tagId, workbenchService]);
|
||||
|
||||
const handleToggleFavoriteTag = useCallback(() => {
|
||||
favoriteService.favoriteList.toggle('tag', tagId);
|
||||
}, [favoriteService, tagId]);
|
||||
|
||||
return useMemo(
|
||||
() => [
|
||||
{
|
||||
@@ -83,6 +99,33 @@ export const useExplorerTagNodeOperations = (
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(runtimeConfig.enableNewFavorite
|
||||
? [
|
||||
{
|
||||
index: 199,
|
||||
view: (
|
||||
<MenuItem
|
||||
preFix={
|
||||
<MenuIcon>
|
||||
{favorite ? (
|
||||
<FavoritedIcon
|
||||
style={{ color: 'var(--affine-primary-color)' }}
|
||||
/>
|
||||
) : (
|
||||
<FavoriteIcon />
|
||||
)}
|
||||
</MenuIcon>
|
||||
}
|
||||
onClick={handleToggleFavoriteTag}
|
||||
>
|
||||
{favorite
|
||||
? t['com.affine.favoritePageOperation.remove']()
|
||||
: t['com.affine.favoritePageOperation.add']()}
|
||||
</MenuItem>
|
||||
),
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
index: 9999,
|
||||
view: <MenuSeparator key="menu-separator" />,
|
||||
@@ -106,9 +149,11 @@ export const useExplorerTagNodeOperations = (
|
||||
],
|
||||
[
|
||||
appSettings.enableMultiView,
|
||||
favorite,
|
||||
handleMoveToTrash,
|
||||
handleNewDoc,
|
||||
handleOpenInSplitView,
|
||||
handleToggleFavoriteTag,
|
||||
t,
|
||||
]
|
||||
);
|
||||
|
||||
@@ -39,10 +39,10 @@ export const RootEmpty = ({
|
||||
<FolderIcon className={styles.icon} />
|
||||
</div>
|
||||
<div
|
||||
data-testid="slider-bar-organize-empty-message"
|
||||
data-testid="slider-bar-favorites-empty-message"
|
||||
className={styles.message}
|
||||
>
|
||||
{t['com.affine.rootAppSidebar.organize.empty']()}
|
||||
{t['com.affine.rootAppSidebar.favorites.empty']()}
|
||||
</div>
|
||||
{dropEffect && draggedOverDraggable && (
|
||||
<DropEffect
|
||||
|
||||
@@ -10,7 +10,11 @@ import {
|
||||
type ExplorerTreeNodeDropEffect,
|
||||
ExplorerTreeRoot,
|
||||
} from '@affine/core/modules/explorer/views/tree';
|
||||
import { FavoriteItemsAdapter } from '@affine/core/modules/properties';
|
||||
import type { FavoriteSupportType } from '@affine/core/modules/favorite';
|
||||
import {
|
||||
FavoriteService,
|
||||
isFavoriteSupportType,
|
||||
} from '@affine/core/modules/favorite';
|
||||
import { WorkbenchService } from '@affine/core/modules/workbench';
|
||||
import type { AffineDNDData } from '@affine/core/types/dnd';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
@@ -20,55 +24,42 @@ import { useCallback, useMemo } from 'react';
|
||||
|
||||
import { ExplorerCollectionNode } from '../../nodes/collection';
|
||||
import { ExplorerDocNode } from '../../nodes/doc';
|
||||
import { ExplorerFolderNode } from '../../nodes/folder';
|
||||
import { ExplorerTagNode } from '../../nodes/tag';
|
||||
import { RootEmpty } from './empty';
|
||||
import * as styles from './styles.css';
|
||||
|
||||
export const ExplorerFavorites = () => {
|
||||
const { favoriteItemsAdapter, docsService, workbenchService } = useServices({
|
||||
FavoriteItemsAdapter,
|
||||
const { favoriteService, docsService, workbenchService } = useServices({
|
||||
FavoriteService,
|
||||
DocsService,
|
||||
WorkbenchService,
|
||||
});
|
||||
|
||||
const docs = useLiveData(docsService.list.docs$);
|
||||
const trashDocs = useLiveData(docsService.list.trashDocs$);
|
||||
|
||||
const favorites = useLiveData(
|
||||
favoriteItemsAdapter.orderedFavorites$.map(favs => {
|
||||
return favs.filter(fav => {
|
||||
if (fav.type === 'doc') {
|
||||
return (
|
||||
docs.some(doc => doc.id === fav.id) &&
|
||||
!trashDocs.some(doc => doc.id === fav.id)
|
||||
);
|
||||
}
|
||||
return true;
|
||||
});
|
||||
})
|
||||
);
|
||||
const favorites = useLiveData(favoriteService.favoriteList.sortedList$);
|
||||
|
||||
const t = useI18n();
|
||||
|
||||
const handleDrop = useCallback(
|
||||
(data: DropTargetDropEvent<AffineDNDData>) => {
|
||||
if (
|
||||
data.source.data.entity?.type === 'doc' ||
|
||||
data.source.data.entity?.type === 'collection'
|
||||
data.source.data.entity?.type &&
|
||||
isFavoriteSupportType(data.source.data.entity.type)
|
||||
) {
|
||||
favoriteItemsAdapter.set(
|
||||
favoriteService.favoriteList.add(
|
||||
data.source.data.entity.type,
|
||||
data.source.data.entity.id,
|
||||
data.source.data.entity?.type,
|
||||
true
|
||||
favoriteService.favoriteList.indexAt('before')
|
||||
);
|
||||
}
|
||||
},
|
||||
[favoriteItemsAdapter]
|
||||
[favoriteService]
|
||||
);
|
||||
|
||||
const handleDropEffect = useCallback<ExplorerTreeNodeDropEffect>(data => {
|
||||
if (
|
||||
data.source.data.entity?.type === 'doc' ||
|
||||
data.source.data.entity?.type === 'collection'
|
||||
data.source.data.entity?.type &&
|
||||
isFavoriteSupportType(data.source.data.entity.type)
|
||||
) {
|
||||
return 'link';
|
||||
}
|
||||
@@ -77,23 +68,26 @@ export const ExplorerFavorites = () => {
|
||||
|
||||
const handleCanDrop = useMemo<DropTargetOptions<AffineDNDData>['canDrop']>(
|
||||
() => data => {
|
||||
return (
|
||||
data.source.data.entity?.type === 'doc' ||
|
||||
data.source.data.entity?.type === 'collection'
|
||||
);
|
||||
return data.source.data.entity?.type
|
||||
? isFavoriteSupportType(data.source.data.entity.type)
|
||||
: false;
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const handleCreateNewFavoriteDoc = useCallback(() => {
|
||||
const newDoc = docsService.createDoc();
|
||||
favoriteItemsAdapter.set(newDoc.id, 'doc', true);
|
||||
favoriteService.favoriteList.add(
|
||||
'doc',
|
||||
newDoc.id,
|
||||
favoriteService.favoriteList.indexAt('before')
|
||||
);
|
||||
workbenchService.workbench.openDoc(newDoc.id);
|
||||
}, [docsService, favoriteItemsAdapter, workbenchService]);
|
||||
}, [docsService, favoriteService, workbenchService]);
|
||||
|
||||
const handleOnChildrenDrop = useCallback(
|
||||
(
|
||||
favorite: { id: string; type: 'doc' | 'collection' },
|
||||
favorite: { id: string; type: FavoriteSupportType },
|
||||
data: DropTargetDropEvent<AffineDNDData>
|
||||
) => {
|
||||
if (
|
||||
@@ -101,42 +95,41 @@ export const ExplorerFavorites = () => {
|
||||
data.treeInstruction?.type === 'reorder-below'
|
||||
) {
|
||||
if (
|
||||
data.source.data.from?.at === 'explorer:favorite:items' &&
|
||||
(data.source.data.entity?.type === 'doc' ||
|
||||
data.source.data.entity?.type === 'collection')
|
||||
data.source.data.from?.at === 'explorer:favorite:list' &&
|
||||
data.source.data.entity?.type &&
|
||||
isFavoriteSupportType(data.source.data.entity.type)
|
||||
) {
|
||||
// is reordering
|
||||
favoriteItemsAdapter.sorter.moveTo(
|
||||
FavoriteItemsAdapter.getFavItemKey(
|
||||
data.source.data.entity.id,
|
||||
data.source.data.entity.type
|
||||
),
|
||||
FavoriteItemsAdapter.getFavItemKey(favorite.id, favorite.type),
|
||||
data.treeInstruction?.type === 'reorder-above' ? 'before' : 'after'
|
||||
favoriteService.favoriteList.reorder(
|
||||
data.source.data.entity.type,
|
||||
data.source.data.entity.id,
|
||||
favoriteService.favoriteList.indexAt(
|
||||
data.treeInstruction?.type === 'reorder-above'
|
||||
? 'before'
|
||||
: 'after',
|
||||
favorite
|
||||
)
|
||||
);
|
||||
} else if (
|
||||
data.source.data.entity?.type === 'doc' ||
|
||||
data.source.data.entity?.type === 'collection'
|
||||
data.source.data.entity?.type &&
|
||||
isFavoriteSupportType(data.source.data.entity.type)
|
||||
) {
|
||||
favoriteItemsAdapter.set(
|
||||
favoriteService.favoriteList.add(
|
||||
data.source.data.entity.type,
|
||||
data.source.data.entity.id,
|
||||
data.source.data.entity?.type,
|
||||
true
|
||||
);
|
||||
favoriteItemsAdapter.sorter.moveTo(
|
||||
FavoriteItemsAdapter.getFavItemKey(
|
||||
data.source.data.entity.id,
|
||||
data.source.data.entity.type
|
||||
),
|
||||
FavoriteItemsAdapter.getFavItemKey(favorite.id, favorite.type),
|
||||
data.treeInstruction?.type === 'reorder-above' ? 'before' : 'after'
|
||||
favoriteService.favoriteList.indexAt(
|
||||
data.treeInstruction?.type === 'reorder-above'
|
||||
? 'before'
|
||||
: 'after',
|
||||
favorite
|
||||
)
|
||||
);
|
||||
} else {
|
||||
return; // not supported
|
||||
}
|
||||
} else {
|
||||
return; // not supported
|
||||
}
|
||||
},
|
||||
[favoriteItemsAdapter]
|
||||
[favoriteService]
|
||||
);
|
||||
|
||||
const handleChildrenDropEffect = useCallback<ExplorerTreeNodeDropEffect>(
|
||||
@@ -146,14 +139,14 @@ export const ExplorerFavorites = () => {
|
||||
data.treeInstruction?.type === 'reorder-below'
|
||||
) {
|
||||
if (
|
||||
data.source.data.from?.at === 'explorer:favorite:items' &&
|
||||
(data.source.data.entity?.type === 'doc' ||
|
||||
data.source.data.entity?.type === 'collection')
|
||||
data.source.data.from?.at === 'explorer:favorite:list' &&
|
||||
data.source.data.entity?.type &&
|
||||
isFavoriteSupportType(data.source.data.entity.type)
|
||||
) {
|
||||
return 'move';
|
||||
} else if (
|
||||
data.source.data.entity?.type === 'doc' ||
|
||||
data.source.data.entity?.type === 'collection'
|
||||
data.source.data.entity?.type &&
|
||||
isFavoriteSupportType(data.source.data.entity.type)
|
||||
) {
|
||||
return 'link';
|
||||
}
|
||||
@@ -167,8 +160,9 @@ export const ExplorerFavorites = () => {
|
||||
DropTargetOptions<AffineDNDData>['canDrop']
|
||||
>(
|
||||
() => args =>
|
||||
args.source.data.entity?.type === 'doc' ||
|
||||
args.source.data.entity?.type === 'collection',
|
||||
args.source.data.entity?.type
|
||||
? isFavoriteSupportType(args.source.data.entity.type)
|
||||
: false,
|
||||
[]
|
||||
);
|
||||
|
||||
@@ -236,7 +230,7 @@ export const ExplorerFavorites = () => {
|
||||
};
|
||||
|
||||
const childLocation = {
|
||||
at: 'explorer:favorite:items' as const,
|
||||
at: 'explorer:favorite:list' as const,
|
||||
};
|
||||
const ExplorerFavoriteNode = ({
|
||||
favorite,
|
||||
@@ -246,13 +240,13 @@ const ExplorerFavoriteNode = ({
|
||||
}: {
|
||||
favorite: {
|
||||
id: string;
|
||||
type: 'collection' | 'doc';
|
||||
type: FavoriteSupportType;
|
||||
};
|
||||
canDrop?: DropTargetOptions<AffineDNDData>['canDrop'];
|
||||
onDrop: (
|
||||
favorite: {
|
||||
id: string;
|
||||
type: 'collection' | 'doc';
|
||||
type: FavoriteSupportType;
|
||||
},
|
||||
data: DropTargetDropEvent<AffineDNDData>
|
||||
) => void;
|
||||
@@ -273,6 +267,24 @@ const ExplorerFavoriteNode = ({
|
||||
dropEffect={dropEffect}
|
||||
canDrop={canDrop}
|
||||
/>
|
||||
) : favorite.type === 'tag' ? (
|
||||
<ExplorerTagNode
|
||||
key={favorite.id}
|
||||
tagId={favorite.id}
|
||||
location={childLocation}
|
||||
onDrop={handleOnChildrenDrop}
|
||||
dropEffect={dropEffect}
|
||||
canDrop={canDrop}
|
||||
/>
|
||||
) : favorite.type === 'folder' ? (
|
||||
<ExplorerFolderNode
|
||||
key={favorite.id}
|
||||
nodeId={favorite.id}
|
||||
location={childLocation}
|
||||
onDrop={handleOnChildrenDrop}
|
||||
dropEffect={dropEffect}
|
||||
canDrop={canDrop}
|
||||
/>
|
||||
) : (
|
||||
<ExplorerCollectionNode
|
||||
key={favorite.id}
|
||||
|
||||
@@ -0,0 +1,149 @@
|
||||
import { IconButton, useConfirmModal } from '@affine/component';
|
||||
import { CategoryDivider } from '@affine/core/components/app-sidebar';
|
||||
import { ExplorerTreeRoot } from '@affine/core/modules/explorer/views/tree';
|
||||
import { FavoriteItemsAdapter } from '@affine/core/modules/properties';
|
||||
import { Trans, useI18n } from '@affine/i18n';
|
||||
import { BroomIcon, HelpIcon } from '@blocksuite/icons/rc';
|
||||
import { DocsService, useLiveData, useServices } from '@toeverything/infra';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { ExplorerCollectionNode } from '../../nodes/collection';
|
||||
import { ExplorerDocNode } from '../../nodes/doc';
|
||||
import * as styles from './styles.css';
|
||||
|
||||
export const ExplorerMigrationFavorites = () => {
|
||||
const t = useI18n();
|
||||
|
||||
const { favoriteItemsAdapter, docsService } = useServices({
|
||||
FavoriteItemsAdapter,
|
||||
DocsService,
|
||||
});
|
||||
|
||||
const docs = useLiveData(docsService.list.docs$);
|
||||
const trashDocs = useLiveData(docsService.list.trashDocs$);
|
||||
const { openConfirmModal } = useConfirmModal();
|
||||
|
||||
const favorites = useLiveData(
|
||||
favoriteItemsAdapter.orderedFavorites$.map(favs => {
|
||||
return favs.filter(fav => {
|
||||
if (fav.type === 'doc') {
|
||||
return (
|
||||
docs.some(doc => doc.id === fav.id) &&
|
||||
!trashDocs.some(doc => doc.id === fav.id)
|
||||
);
|
||||
}
|
||||
return true;
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
const handleClickClear = useCallback(() => {
|
||||
openConfirmModal({
|
||||
title: t['com.affine.rootAppSidebar.migration-data.clean-all'](),
|
||||
description: (
|
||||
<Trans
|
||||
i18nKey="com.affine.rootAppSidebar.migration-data.clean-all.description"
|
||||
components={{
|
||||
b: <b className={styles.descriptionHighlight} />,
|
||||
}}
|
||||
/>
|
||||
),
|
||||
confirmText:
|
||||
t['com.affine.rootAppSidebar.migration-data.clean-all.confirm'](),
|
||||
confirmButtonOptions: {
|
||||
type: 'primary',
|
||||
},
|
||||
cancelText:
|
||||
t['com.affine.rootAppSidebar.migration-data.clean-all.cancel'](),
|
||||
onConfirm() {
|
||||
favoriteItemsAdapter.clearAll();
|
||||
},
|
||||
});
|
||||
}, [favoriteItemsAdapter, openConfirmModal, t]);
|
||||
|
||||
const handleClickHelp = useCallback(() => {
|
||||
openConfirmModal({
|
||||
title: t['com.affine.rootAppSidebar.migration-data.help'](),
|
||||
description:
|
||||
t['com.affine.rootAppSidebar.migration-data.help.description'](),
|
||||
confirmText: t['com.affine.rootAppSidebar.migration-data.help.confirm'](),
|
||||
confirmButtonOptions: {
|
||||
type: 'primary',
|
||||
},
|
||||
cancelText:
|
||||
t['com.affine.rootAppSidebar.migration-data.help.clean-all'](),
|
||||
cancelButtonOptions: {
|
||||
icon: <BroomIcon />,
|
||||
type: 'default',
|
||||
onClick: () => {
|
||||
requestAnimationFrame(() => {
|
||||
handleClickClear();
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
}, [handleClickClear, openConfirmModal, t]);
|
||||
|
||||
if (favorites.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<CategoryDivider label={t['com.affine.rootAppSidebar.migration-data']()}>
|
||||
<IconButton
|
||||
data-testid="explorer-bar-favorite-migration-clear-button"
|
||||
onClick={handleClickClear}
|
||||
size="small"
|
||||
>
|
||||
<BroomIcon />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
data-testid="explorer-bar-favorite-migration-help-button"
|
||||
size="small"
|
||||
onClick={handleClickHelp}
|
||||
>
|
||||
<HelpIcon />
|
||||
</IconButton>
|
||||
</CategoryDivider>
|
||||
<ExplorerTreeRoot>
|
||||
{favorites.map((favorite, i) => (
|
||||
<ExplorerMigrationFavoriteNode
|
||||
key={favorite.id + ':' + i}
|
||||
favorite={favorite}
|
||||
/>
|
||||
))}
|
||||
</ExplorerTreeRoot>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const childLocation = {
|
||||
at: 'explorer:migration-data:list' as const,
|
||||
};
|
||||
const ExplorerMigrationFavoriteNode = ({
|
||||
favorite,
|
||||
}: {
|
||||
favorite: {
|
||||
id: string;
|
||||
type: 'collection' | 'doc';
|
||||
};
|
||||
}) => {
|
||||
return favorite.type === 'doc' ? (
|
||||
<ExplorerDocNode
|
||||
key={favorite.id}
|
||||
docId={favorite.id}
|
||||
location={childLocation}
|
||||
reorderable={false}
|
||||
canDrop={false}
|
||||
/>
|
||||
) : (
|
||||
<ExplorerCollectionNode
|
||||
key={favorite.id}
|
||||
collectionId={favorite.id}
|
||||
location={childLocation}
|
||||
reorderable={false}
|
||||
canDrop={false}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,25 @@
|
||||
import { cssVar } from '@toeverything/theme';
|
||||
import { style } from '@vanilla-extract/css';
|
||||
|
||||
export const container = style({
|
||||
marginTop: '16px',
|
||||
position: 'relative',
|
||||
selectors: {
|
||||
'&:after': {
|
||||
display: 'block',
|
||||
content: '""',
|
||||
position: 'absolute',
|
||||
left: '-8px',
|
||||
top: '0',
|
||||
width: '6px',
|
||||
height: '100%',
|
||||
background:
|
||||
'repeating-linear-gradient(30deg, #F5CC47, #F5CC47 8px, #000000 8px, #000000 14px)',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const descriptionHighlight = style({
|
||||
color: cssVar('--affine-warning-color'),
|
||||
fontWeight: 'normal',
|
||||
});
|
||||
@@ -0,0 +1,36 @@
|
||||
import { cssVar } from '@toeverything/theme';
|
||||
import { style } from '@vanilla-extract/css';
|
||||
|
||||
export const content = style({
|
||||
position: 'relative',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
gap: 6,
|
||||
padding: '9px 20px 25px 21px',
|
||||
});
|
||||
export const iconWrapper = style({
|
||||
width: 36,
|
||||
height: 36,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
borderRadius: '50%',
|
||||
backgroundColor: cssVar('hoverColor'),
|
||||
});
|
||||
export const icon = style({
|
||||
fontSize: 20,
|
||||
color: cssVar('iconSecondary'),
|
||||
});
|
||||
export const message = style({
|
||||
fontSize: cssVar('fontSm'),
|
||||
textAlign: 'center',
|
||||
color: cssVar('black30'),
|
||||
userSelect: 'none',
|
||||
});
|
||||
|
||||
export const newButton = style({
|
||||
padding: '0 8px',
|
||||
height: '28px',
|
||||
fontSize: cssVar('fontXs'),
|
||||
});
|
||||
@@ -0,0 +1,61 @@
|
||||
import {
|
||||
type DropTargetDropEvent,
|
||||
type DropTargetOptions,
|
||||
useDropTarget,
|
||||
} from '@affine/component';
|
||||
import type { AffineDNDData } from '@affine/core/types/dnd';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
import { FolderIcon } from '@blocksuite/icons/rc';
|
||||
|
||||
import { DropEffect, type ExplorerTreeNodeDropEffect } from '../../tree';
|
||||
import * as styles from './empty.css';
|
||||
|
||||
export const RootEmpty = ({
|
||||
onDrop,
|
||||
canDrop,
|
||||
dropEffect,
|
||||
}: {
|
||||
onDrop?: (data: DropTargetDropEvent<AffineDNDData>) => void;
|
||||
canDrop?: DropTargetOptions<AffineDNDData>['canDrop'];
|
||||
dropEffect?: ExplorerTreeNodeDropEffect;
|
||||
}) => {
|
||||
const t = useI18n();
|
||||
|
||||
const { dropTargetRef, draggedOverDraggable, draggedOverPosition } =
|
||||
useDropTarget<AffineDNDData>(
|
||||
() => ({
|
||||
data: {
|
||||
at: 'explorer:old-favorite:root',
|
||||
},
|
||||
onDrop: onDrop,
|
||||
canDrop: canDrop,
|
||||
}),
|
||||
[onDrop, canDrop]
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={styles.content} ref={dropTargetRef}>
|
||||
<div className={styles.iconWrapper}>
|
||||
<FolderIcon className={styles.icon} />
|
||||
</div>
|
||||
<div
|
||||
data-testid="slider-bar-favorites-empty-message"
|
||||
className={styles.message}
|
||||
>
|
||||
{t['com.affine.rootAppSidebar.favorites.empty']()}
|
||||
</div>
|
||||
{dropEffect && draggedOverDraggable && (
|
||||
<DropEffect
|
||||
position={{
|
||||
x: draggedOverPosition.relativeX,
|
||||
y: draggedOverPosition.relativeY,
|
||||
}}
|
||||
dropEffect={dropEffect({
|
||||
source: draggedOverDraggable,
|
||||
treeInstruction: null,
|
||||
})}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,265 @@
|
||||
import {
|
||||
type DropTargetDropEvent,
|
||||
type DropTargetOptions,
|
||||
IconButton,
|
||||
} from '@affine/component';
|
||||
import { CategoryDivider } from '@affine/core/components/app-sidebar';
|
||||
import {
|
||||
type ExplorerTreeNodeDropEffect,
|
||||
ExplorerTreeRoot,
|
||||
} from '@affine/core/modules/explorer/views/tree';
|
||||
import { FavoriteItemsAdapter } from '@affine/core/modules/properties';
|
||||
import { WorkbenchService } from '@affine/core/modules/workbench';
|
||||
import type { AffineDNDData } from '@affine/core/types/dnd';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
import { PlusIcon } from '@blocksuite/icons/rc';
|
||||
import { DocsService, useLiveData, useServices } from '@toeverything/infra';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
|
||||
import { ExplorerCollectionNode } from '../../nodes/collection';
|
||||
import { ExplorerDocNode } from '../../nodes/doc';
|
||||
import { RootEmpty } from './empty';
|
||||
import * as styles from './styles.css';
|
||||
|
||||
/**
|
||||
* @deprecated remove this after 0.17 released
|
||||
*/
|
||||
export const ExplorerOldFavorites = () => {
|
||||
const { favoriteItemsAdapter, docsService, workbenchService } = useServices({
|
||||
FavoriteItemsAdapter,
|
||||
DocsService,
|
||||
WorkbenchService,
|
||||
});
|
||||
|
||||
const docs = useLiveData(docsService.list.docs$);
|
||||
const trashDocs = useLiveData(docsService.list.trashDocs$);
|
||||
|
||||
const favorites = useLiveData(
|
||||
favoriteItemsAdapter.orderedFavorites$.map(favs => {
|
||||
return favs.filter(fav => {
|
||||
if (fav.type === 'doc') {
|
||||
return (
|
||||
docs.some(doc => doc.id === fav.id) &&
|
||||
!trashDocs.some(doc => doc.id === fav.id)
|
||||
);
|
||||
}
|
||||
return true;
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
const t = useI18n();
|
||||
|
||||
const handleDrop = useCallback(
|
||||
(data: DropTargetDropEvent<AffineDNDData>) => {
|
||||
if (
|
||||
data.source.data.entity?.type === 'doc' ||
|
||||
data.source.data.entity?.type === 'collection'
|
||||
) {
|
||||
favoriteItemsAdapter.set(
|
||||
data.source.data.entity.id,
|
||||
data.source.data.entity.type,
|
||||
true
|
||||
);
|
||||
}
|
||||
},
|
||||
[favoriteItemsAdapter]
|
||||
);
|
||||
|
||||
const handleDropEffect = useCallback<ExplorerTreeNodeDropEffect>(data => {
|
||||
if (
|
||||
data.source.data.entity?.type === 'doc' ||
|
||||
data.source.data.entity?.type === 'collection'
|
||||
) {
|
||||
return 'link';
|
||||
}
|
||||
return;
|
||||
}, []);
|
||||
|
||||
const handleCanDrop = useMemo<DropTargetOptions<AffineDNDData>['canDrop']>(
|
||||
() => data => {
|
||||
return (
|
||||
data.source.data.entity?.type === 'doc' ||
|
||||
data.source.data.entity?.type === 'collection'
|
||||
);
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const handleCreateNewFavoriteDoc = useCallback(() => {
|
||||
const newDoc = docsService.createDoc();
|
||||
favoriteItemsAdapter.set(newDoc.id, 'doc', true);
|
||||
workbenchService.workbench.openDoc(newDoc.id);
|
||||
}, [docsService, favoriteItemsAdapter, workbenchService]);
|
||||
|
||||
const handleOnChildrenDrop = useCallback(
|
||||
(
|
||||
favorite: { id: string; type: 'doc' | 'collection' },
|
||||
data: DropTargetDropEvent<AffineDNDData>
|
||||
) => {
|
||||
if (
|
||||
data.treeInstruction?.type === 'reorder-above' ||
|
||||
data.treeInstruction?.type === 'reorder-below'
|
||||
) {
|
||||
if (
|
||||
data.source.data.from?.at === 'explorer:old-favorite:list' &&
|
||||
(data.source.data.entity?.type === 'doc' ||
|
||||
data.source.data.entity?.type === 'collection')
|
||||
) {
|
||||
// is reordering
|
||||
favoriteItemsAdapter.sorter.moveTo(
|
||||
FavoriteItemsAdapter.getFavItemKey(
|
||||
data.source.data.entity.id,
|
||||
data.source.data.entity.type
|
||||
),
|
||||
FavoriteItemsAdapter.getFavItemKey(favorite.id, favorite.type),
|
||||
data.treeInstruction?.type === 'reorder-above' ? 'before' : 'after'
|
||||
);
|
||||
} else if (
|
||||
data.source.data.entity?.type === 'doc' ||
|
||||
data.source.data.entity?.type === 'collection'
|
||||
) {
|
||||
favoriteItemsAdapter.set(
|
||||
data.source.data.entity.id,
|
||||
data.source.data.entity?.type,
|
||||
true
|
||||
);
|
||||
favoriteItemsAdapter.sorter.moveTo(
|
||||
FavoriteItemsAdapter.getFavItemKey(
|
||||
data.source.data.entity.id,
|
||||
data.source.data.entity.type
|
||||
),
|
||||
FavoriteItemsAdapter.getFavItemKey(favorite.id, favorite.type),
|
||||
data.treeInstruction?.type === 'reorder-above' ? 'before' : 'after'
|
||||
);
|
||||
}
|
||||
} else {
|
||||
return; // not supported
|
||||
}
|
||||
},
|
||||
[favoriteItemsAdapter]
|
||||
);
|
||||
|
||||
const handleChildrenDropEffect = useCallback<ExplorerTreeNodeDropEffect>(
|
||||
data => {
|
||||
if (
|
||||
data.treeInstruction?.type === 'reorder-above' ||
|
||||
data.treeInstruction?.type === 'reorder-below'
|
||||
) {
|
||||
if (
|
||||
data.source.data.from?.at === 'explorer:old-favorite:list' &&
|
||||
(data.source.data.entity?.type === 'doc' ||
|
||||
data.source.data.entity?.type === 'collection')
|
||||
) {
|
||||
return 'move';
|
||||
} else if (
|
||||
data.source.data.entity?.type === 'doc' ||
|
||||
data.source.data.entity?.type === 'collection'
|
||||
) {
|
||||
return 'link';
|
||||
}
|
||||
}
|
||||
return; // not supported
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const handleChildrenCanDrop = useMemo<
|
||||
DropTargetOptions<AffineDNDData>['canDrop']
|
||||
>(
|
||||
() => args =>
|
||||
args.source.data.entity?.type === 'doc' ||
|
||||
args.source.data.entity?.type === 'collection',
|
||||
[]
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<CategoryDivider
|
||||
className={styles.draggedOverHighlight}
|
||||
label={
|
||||
runtimeConfig.enableNewFavorite
|
||||
? `${t['com.affine.rootAppSidebar.favorites']()} (OLD)`
|
||||
: t['com.affine.rootAppSidebar.favorites']()
|
||||
}
|
||||
>
|
||||
<IconButton
|
||||
data-testid="explorer-bar-add-old-favorite-button"
|
||||
onClick={handleCreateNewFavoriteDoc}
|
||||
size="small"
|
||||
>
|
||||
<PlusIcon />
|
||||
</IconButton>
|
||||
</CategoryDivider>
|
||||
<ExplorerTreeRoot
|
||||
placeholder={
|
||||
<RootEmpty
|
||||
onDrop={handleDrop}
|
||||
canDrop={handleCanDrop}
|
||||
dropEffect={handleDropEffect}
|
||||
/>
|
||||
}
|
||||
>
|
||||
{favorites.map((favorite, i) => (
|
||||
<ExplorerFavoriteNode
|
||||
key={favorite.id + ':' + i}
|
||||
favorite={favorite}
|
||||
onDrop={handleOnChildrenDrop}
|
||||
dropEffect={handleChildrenDropEffect}
|
||||
canDrop={handleChildrenCanDrop}
|
||||
/>
|
||||
))}
|
||||
</ExplorerTreeRoot>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const childLocation = {
|
||||
at: 'explorer:old-favorite:list' as const,
|
||||
};
|
||||
const ExplorerFavoriteNode = ({
|
||||
favorite,
|
||||
onDrop,
|
||||
canDrop,
|
||||
dropEffect,
|
||||
}: {
|
||||
favorite: {
|
||||
id: string;
|
||||
type: 'collection' | 'doc';
|
||||
};
|
||||
canDrop?: DropTargetOptions<AffineDNDData>['canDrop'];
|
||||
onDrop: (
|
||||
favorite: {
|
||||
id: string;
|
||||
type: 'collection' | 'doc';
|
||||
},
|
||||
data: DropTargetDropEvent<AffineDNDData>
|
||||
) => void;
|
||||
dropEffect: ExplorerTreeNodeDropEffect;
|
||||
}) => {
|
||||
const handleOnChildrenDrop = useCallback(
|
||||
(data: DropTargetDropEvent<AffineDNDData>) => {
|
||||
onDrop(favorite, data);
|
||||
},
|
||||
[favorite, onDrop]
|
||||
);
|
||||
return favorite.type === 'doc' ? (
|
||||
<ExplorerDocNode
|
||||
key={favorite.id}
|
||||
docId={favorite.id}
|
||||
location={childLocation}
|
||||
onDrop={handleOnChildrenDrop}
|
||||
dropEffect={dropEffect}
|
||||
canDrop={canDrop}
|
||||
/>
|
||||
) : (
|
||||
<ExplorerCollectionNode
|
||||
key={favorite.id}
|
||||
collectionId={favorite.id}
|
||||
location={childLocation}
|
||||
onDrop={handleOnChildrenDrop}
|
||||
dropEffect={dropEffect}
|
||||
canDrop={canDrop}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,15 @@
|
||||
import { cssVar } from '@toeverything/theme';
|
||||
import { style } from '@vanilla-extract/css';
|
||||
|
||||
export const container = style({
|
||||
marginTop: '16px',
|
||||
});
|
||||
|
||||
export const draggedOverHighlight = style({
|
||||
selectors: {
|
||||
'&[data-dragged-over="true"]': {
|
||||
background: cssVar('--affine-hover-color'),
|
||||
borderRadius: '4px',
|
||||
},
|
||||
},
|
||||
});
|
||||
11
packages/frontend/core/src/modules/favorite/constant.ts
Normal file
11
packages/frontend/core/src/modules/favorite/constant.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
export const FavoriteSupportType = [
|
||||
'collection',
|
||||
'doc',
|
||||
'tag',
|
||||
'folder',
|
||||
] as const;
|
||||
export type FavoriteSupportType = 'collection' | 'doc' | 'tag' | 'folder';
|
||||
export const isFavoriteSupportType = (
|
||||
type: string
|
||||
): type is FavoriteSupportType =>
|
||||
FavoriteSupportType.includes(type as FavoriteSupportType);
|
||||
@@ -0,0 +1,97 @@
|
||||
import { generateFractionalIndexingKeyBetween } from '@affine/core/utils/fractional-indexing';
|
||||
import { Entity } from '@toeverything/infra';
|
||||
|
||||
import type { FavoriteSupportType } from '../constant';
|
||||
import type { FavoriteRecord, FavoriteStore } from '../stores/favorite';
|
||||
|
||||
export class FavoriteList extends Entity {
|
||||
list$ = this.store.watchFavorites();
|
||||
sortedList$ = this.list$.map(v =>
|
||||
v.sort((a, b) => (a.index > b.index ? 1 : -1))
|
||||
);
|
||||
|
||||
constructor(private readonly store: FavoriteStore) {
|
||||
super();
|
||||
}
|
||||
|
||||
/**
|
||||
* get favorite record by type and id
|
||||
*/
|
||||
favorite$(type: FavoriteSupportType, id: string) {
|
||||
return this.store.watchFavorite(type, id);
|
||||
}
|
||||
|
||||
isFavorite$(type: FavoriteSupportType, id: string) {
|
||||
return this.favorite$(type, id).map(v => !!v);
|
||||
}
|
||||
|
||||
add(
|
||||
type: FavoriteSupportType,
|
||||
id: string,
|
||||
index: string = this.indexAt('before')
|
||||
) {
|
||||
return this.store.addFavorite(type, id, index);
|
||||
}
|
||||
|
||||
toggle(
|
||||
type: FavoriteSupportType,
|
||||
id: string,
|
||||
index: string = this.indexAt('before')
|
||||
) {
|
||||
if (this.favorite$(type, id).value) {
|
||||
return this.remove(type, id);
|
||||
} else {
|
||||
return this.add(type, id, index);
|
||||
}
|
||||
}
|
||||
|
||||
remove(type: FavoriteSupportType, id: string) {
|
||||
return this.store.removeFavorite(type, id);
|
||||
}
|
||||
|
||||
reorder(type: FavoriteSupportType, id: string, index: string) {
|
||||
return this.store.reorderFavorite(type, id, index);
|
||||
}
|
||||
|
||||
indexAt(
|
||||
at: 'before' | 'after',
|
||||
targetRecord?: {
|
||||
type: FavoriteSupportType;
|
||||
id: string;
|
||||
}
|
||||
) {
|
||||
if (!targetRecord) {
|
||||
if (at === 'before') {
|
||||
const first = this.sortedList$.value.at(0);
|
||||
return generateFractionalIndexingKeyBetween(null, first?.index || null);
|
||||
} else {
|
||||
const last = this.sortedList$.value.at(-1);
|
||||
return generateFractionalIndexingKeyBetween(last?.index || null, null);
|
||||
}
|
||||
} else {
|
||||
const sortedChildren = this.sortedList$.value;
|
||||
const targetIndex = sortedChildren.findIndex(
|
||||
node => node.id === targetRecord.id && node.type === targetRecord.type
|
||||
);
|
||||
if (targetIndex === -1) {
|
||||
throw new Error('Target favorite record not found');
|
||||
}
|
||||
const target = sortedChildren[targetIndex];
|
||||
const before: FavoriteRecord | null =
|
||||
sortedChildren[targetIndex - 1] || null;
|
||||
const after: FavoriteRecord | null =
|
||||
sortedChildren[targetIndex + 1] || null;
|
||||
if (at === 'before') {
|
||||
return generateFractionalIndexingKeyBetween(
|
||||
before?.index || null,
|
||||
target.index
|
||||
);
|
||||
} else {
|
||||
return generateFractionalIndexingKeyBetween(
|
||||
target.index,
|
||||
after?.index || null
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
23
packages/frontend/core/src/modules/favorite/index.ts
Normal file
23
packages/frontend/core/src/modules/favorite/index.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import {
|
||||
type Framework,
|
||||
WorkspaceDBService,
|
||||
WorkspaceScope,
|
||||
WorkspaceService,
|
||||
} from '@toeverything/infra';
|
||||
|
||||
import { AuthService } from '../cloud';
|
||||
import { FavoriteList } from './entities/favorite-list';
|
||||
import { FavoriteService } from './services/favorite';
|
||||
import { FavoriteStore } from './stores/favorite';
|
||||
|
||||
export { FavoriteSupportType, isFavoriteSupportType } from './constant';
|
||||
export type { FavoriteList } from './entities/favorite-list';
|
||||
export { FavoriteService } from './services/favorite';
|
||||
|
||||
export function configureFavoriteModule(framework: Framework) {
|
||||
framework
|
||||
.scope(WorkspaceScope)
|
||||
.service(FavoriteService)
|
||||
.entity(FavoriteList, [FavoriteStore])
|
||||
.store(FavoriteStore, [AuthService, WorkspaceDBService, WorkspaceService]);
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import { Service } from '@toeverything/infra';
|
||||
|
||||
import { FavoriteList } from '../entities/favorite-list';
|
||||
|
||||
export class FavoriteService extends Service {
|
||||
favoriteList = this.framework.createEntity(FavoriteList);
|
||||
}
|
||||
122
packages/frontend/core/src/modules/favorite/stores/favorite.ts
Normal file
122
packages/frontend/core/src/modules/favorite/stores/favorite.ts
Normal file
@@ -0,0 +1,122 @@
|
||||
import { WorkspaceFlavour } from '@affine/env/workspace';
|
||||
import type { WorkspaceDBService, WorkspaceService } from '@toeverything/infra';
|
||||
import { LiveData, Store } from '@toeverything/infra';
|
||||
import { map } from 'rxjs';
|
||||
|
||||
import type { AuthService } from '../../cloud';
|
||||
import type { FavoriteSupportType } from '../constant';
|
||||
import { isFavoriteSupportType } from '../constant';
|
||||
|
||||
export interface FavoriteRecord {
|
||||
type: FavoriteSupportType;
|
||||
id: string;
|
||||
index: string;
|
||||
}
|
||||
|
||||
export class FavoriteStore extends Store {
|
||||
constructor(
|
||||
private readonly authService: AuthService,
|
||||
private readonly workspaceDBService: WorkspaceDBService,
|
||||
private readonly workspaceService: WorkspaceService
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
private get userdataDB$() {
|
||||
return this.authService.session.account$.map(account => {
|
||||
// if is local workspace or no account, use __local__ userdata
|
||||
// sometimes we may have cloud workspace but no account for a short time, we also use __local__ userdata
|
||||
if (
|
||||
this.workspaceService.workspace.meta.flavour ===
|
||||
WorkspaceFlavour.LOCAL ||
|
||||
!account
|
||||
) {
|
||||
return this.workspaceDBService.userdataDB('__local__');
|
||||
}
|
||||
return this.workspaceDBService.userdataDB(account.id);
|
||||
});
|
||||
}
|
||||
|
||||
watchFavorites() {
|
||||
return this.userdataDB$
|
||||
.map(db => LiveData.from(db.favorite.find$(), []))
|
||||
.flat()
|
||||
.map(raw => {
|
||||
return raw
|
||||
.map(data => this.toRecord(data))
|
||||
.filter((record): record is FavoriteRecord => !!record);
|
||||
});
|
||||
}
|
||||
|
||||
addFavorite(
|
||||
type: FavoriteSupportType,
|
||||
id: string,
|
||||
index: string
|
||||
): FavoriteRecord {
|
||||
const db = this.userdataDB$.value;
|
||||
const raw = db.favorite.create({
|
||||
key: this.encodeKey(type, id),
|
||||
index,
|
||||
});
|
||||
return this.toRecord(raw) as FavoriteRecord;
|
||||
}
|
||||
|
||||
reorderFavorite(type: FavoriteSupportType, id: string, index: string) {
|
||||
const db = this.userdataDB$.value;
|
||||
db.favorite.update(this.encodeKey(type, id), { index });
|
||||
}
|
||||
|
||||
removeFavorite(type: FavoriteSupportType, id: string) {
|
||||
const db = this.userdataDB$.value;
|
||||
db.favorite.delete(this.encodeKey(type, id));
|
||||
}
|
||||
|
||||
watchFavorite(type: FavoriteSupportType, id: string) {
|
||||
const db = this.userdataDB$.value;
|
||||
return LiveData.from<FavoriteRecord | undefined>(
|
||||
db.favorite
|
||||
.get$(this.encodeKey(type, id))
|
||||
.pipe(map(data => (data ? this.toRecord(data) : undefined))),
|
||||
null as any
|
||||
);
|
||||
}
|
||||
|
||||
private toRecord(data: {
|
||||
key: string;
|
||||
index: string;
|
||||
}): FavoriteRecord | undefined {
|
||||
const key = this.parseKey(data.key);
|
||||
if (!key) {
|
||||
return undefined;
|
||||
}
|
||||
return {
|
||||
type: key.type,
|
||||
id: key.id,
|
||||
index: data.index,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* parse favorite key
|
||||
* key format: ${type}:${id}
|
||||
* type: collection | doc | tag
|
||||
* @returns null if key is invalid
|
||||
*/
|
||||
private parseKey(key: string): {
|
||||
type: FavoriteSupportType;
|
||||
id: string;
|
||||
} | null {
|
||||
const [type, id] = key.split(':');
|
||||
if (!type || !id) {
|
||||
return null;
|
||||
}
|
||||
if (!isFavoriteSupportType(type)) {
|
||||
return null;
|
||||
}
|
||||
return { type: type as FavoriteSupportType, id };
|
||||
}
|
||||
|
||||
private encodeKey(type: FavoriteSupportType, id: string) {
|
||||
return `${type}:${id}`;
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import { configureCloudModule } from './cloud';
|
||||
import { configureCollectionModule } from './collection';
|
||||
import { configureDocLinksModule } from './doc-link';
|
||||
import { configureDocsSearchModule } from './docs-search';
|
||||
import { configureFavoriteModule } from './favorite';
|
||||
import { configureFindInPageModule } from './find-in-page';
|
||||
import { configureNavigationModule } from './navigation';
|
||||
import { configureOrganizeModule } from './organize';
|
||||
@@ -35,4 +36,5 @@ export function configureCommonModules(framework: Framework) {
|
||||
configureDocsSearchModule(framework);
|
||||
configureDocLinksModule(framework);
|
||||
configureOrganizeModule(framework);
|
||||
configureFavoriteModule(framework);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
import { DBService, type Framework, WorkspaceScope } from '@toeverything/infra';
|
||||
import {
|
||||
type Framework,
|
||||
WorkspaceDBService,
|
||||
WorkspaceScope,
|
||||
} from '@toeverything/infra';
|
||||
|
||||
import { FolderNode } from './entities/folder-node';
|
||||
import { FolderTree } from './entities/folder-tree';
|
||||
@@ -14,5 +18,5 @@ export function configureOrganizeModule(framework: Framework) {
|
||||
.service(OrganizeService)
|
||||
.entity(FolderTree, [FolderStore])
|
||||
.entity(FolderNode, [FolderStore])
|
||||
.store(FolderStore, [DBService]);
|
||||
.store(FolderStore, [WorkspaceDBService]);
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import type { DBService } from '@toeverything/infra';
|
||||
import type { WorkspaceDBService } from '@toeverything/infra';
|
||||
import { Store } from '@toeverything/infra';
|
||||
|
||||
export class FolderStore extends Store {
|
||||
constructor(private readonly dbService: DBService) {
|
||||
constructor(private readonly dbService: WorkspaceDBService) {
|
||||
super();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
export {
|
||||
CompatibleFavoriteItemsAdapter,
|
||||
FavoriteItemsAdapter,
|
||||
WorkspacePropertiesAdapter,
|
||||
} from './services/adapter';
|
||||
@@ -10,7 +11,9 @@ import {
|
||||
WorkspaceService,
|
||||
} from '@toeverything/infra';
|
||||
|
||||
import { FavoriteService } from '../favorite';
|
||||
import {
|
||||
CompatibleFavoriteItemsAdapter,
|
||||
FavoriteItemsAdapter,
|
||||
WorkspacePropertiesAdapter,
|
||||
} from './services/adapter';
|
||||
@@ -21,5 +24,9 @@ export function configureWorkspacePropertiesModule(framework: Framework) {
|
||||
.scope(WorkspaceScope)
|
||||
.service(WorkspaceLegacyProperties, [WorkspaceService])
|
||||
.service(WorkspacePropertiesAdapter, [WorkspaceService])
|
||||
.service(FavoriteItemsAdapter, [WorkspacePropertiesAdapter]);
|
||||
.service(FavoriteItemsAdapter, [WorkspacePropertiesAdapter])
|
||||
.service(CompatibleFavoriteItemsAdapter, [
|
||||
FavoriteItemsAdapter,
|
||||
FavoriteService,
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import { LiveData, Service } from '@toeverything/infra';
|
||||
import { defaultsDeep } from 'lodash-es';
|
||||
import { Observable } from 'rxjs';
|
||||
|
||||
import type { FavoriteService } from '../../favorite';
|
||||
import {
|
||||
PagePropertyType,
|
||||
PageSystemPropertyId,
|
||||
@@ -130,6 +131,9 @@ export class WorkspacePropertiesAdapter extends Service {
|
||||
return this.proxy.schema;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
get favorites() {
|
||||
return this.proxy.favorites;
|
||||
}
|
||||
@@ -154,8 +158,18 @@ export class WorkspacePropertiesAdapter extends Service {
|
||||
const pageProperties = this.pageProperties?.[id];
|
||||
pageProperties!.system[PageSystemPropertyId.Journal].value = date;
|
||||
}
|
||||
|
||||
/**
|
||||
* After the user completes the migration, call this function to clear the favorite data
|
||||
*/
|
||||
cleanupFavorites() {
|
||||
this.proxy.favorites = {};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated use CompatibleFavoriteItemsAdapter
|
||||
*/
|
||||
export class FavoriteItemsAdapter extends Service {
|
||||
constructor(private readonly adapter: WorkspacePropertiesAdapter) {
|
||||
super();
|
||||
@@ -285,4 +299,70 @@ export class FavoriteItemsAdapter extends Service {
|
||||
existing.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
clearAll() {
|
||||
this.adapter.cleanupFavorites();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A service written for compatibility,with the same API as FavoriteItemsAdapter.
|
||||
* When `runtimeConfig.enableNewFavorite` is false, it operates FavoriteItemsAdapter,
|
||||
* and when it is true, it operates FavoriteService.
|
||||
*/
|
||||
export class CompatibleFavoriteItemsAdapter extends Service {
|
||||
constructor(
|
||||
private readonly favoriteItemsAdapter: FavoriteItemsAdapter,
|
||||
private readonly favoriteService: FavoriteService
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
toggle(id: string, type: WorkspaceFavoriteItem['type']) {
|
||||
if (runtimeConfig.enableNewFavorite) {
|
||||
this.favoriteService.favoriteList.toggle(type, id);
|
||||
} else {
|
||||
this.favoriteItemsAdapter.toggle(id, type);
|
||||
}
|
||||
}
|
||||
|
||||
isFavorite$(id: string, type: WorkspaceFavoriteItem['type']) {
|
||||
if (runtimeConfig.enableNewFavorite) {
|
||||
return this.favoriteService.favoriteList.isFavorite$(type, id);
|
||||
} else {
|
||||
return this.favoriteItemsAdapter.isFavorite$(id, type);
|
||||
}
|
||||
}
|
||||
|
||||
isFavorite(id: string, type: WorkspaceFavoriteItem['type']) {
|
||||
if (runtimeConfig.enableNewFavorite) {
|
||||
return this.favoriteService.favoriteList.isFavorite$(type, id).value;
|
||||
} else {
|
||||
return this.favoriteItemsAdapter.isFavorite(id, type);
|
||||
}
|
||||
}
|
||||
|
||||
get favorites$() {
|
||||
if (runtimeConfig.enableNewFavorite) {
|
||||
return this.favoriteService.favoriteList.list$.map<
|
||||
{
|
||||
id: string;
|
||||
order: string;
|
||||
type: 'doc' | 'collection';
|
||||
value: boolean;
|
||||
}[]
|
||||
>(v =>
|
||||
v
|
||||
.filter(i => i.type === 'doc' || i.type === 'collection') // only support doc and collection
|
||||
.map(i => ({
|
||||
id: i.id,
|
||||
order: '',
|
||||
type: i.type as 'doc' | 'collection',
|
||||
value: true,
|
||||
}))
|
||||
);
|
||||
} else {
|
||||
return this.favoriteItemsAdapter.favorites$;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,7 +36,13 @@ export interface AffineDNDData extends DNDData {
|
||||
collectionId: string;
|
||||
}
|
||||
| {
|
||||
at: 'explorer:favorite:items';
|
||||
at: 'explorer:favorite:list';
|
||||
}
|
||||
| {
|
||||
at: 'explorer:old-favorite:list';
|
||||
}
|
||||
| {
|
||||
at: 'explorer:migration-data:list';
|
||||
}
|
||||
| {
|
||||
at: 'all-docs:list';
|
||||
@@ -64,6 +70,12 @@ export interface AffineDNDData extends DNDData {
|
||||
| {
|
||||
at: 'explorer:organize:folder';
|
||||
}
|
||||
| {
|
||||
at: 'explorer:favorite:root';
|
||||
}
|
||||
| {
|
||||
at: 'explorer:old-favorite:root';
|
||||
}
|
||||
| {
|
||||
at: 'explorer:doc';
|
||||
}
|
||||
|
||||
@@ -1115,6 +1115,15 @@
|
||||
"com.affine.resetSyncStatus.description": "This operation may fix some synchronization issues.",
|
||||
"com.affine.rootAppSidebar.collections": "Collections",
|
||||
"com.affine.rootAppSidebar.favorites": "Favourites",
|
||||
"com.affine.rootAppSidebar.migration-data": "Migration data",
|
||||
"com.affine.rootAppSidebar.migration-data.help": "Migration your favorites data",
|
||||
"com.affine.rootAppSidebar.migration-data.help.description": "This is the old Favourites data. Previously, Favourites were shared during collaboration. We have redesigned the Favourites feature, which requires you to take certain actions. You can drag this content or directly hide and delete this part of the data.",
|
||||
"com.affine.rootAppSidebar.migration-data.help.clean-all": "Clear all migration data",
|
||||
"com.affine.rootAppSidebar.migration-data.help.confirm": "OK",
|
||||
"com.affine.rootAppSidebar.migration-data.clean-all": "Clean all migration data",
|
||||
"com.affine.rootAppSidebar.migration-data.clean-all.description": "This will delete all Migration data, don't worry, <b>it won't delete any data entities</b>, it will just clean up the content on the sidebar. Once the content is cleared, the sidebar will no longer display this section until the next occurrence of conflicting data.",
|
||||
"com.affine.rootAppSidebar.migration-data.clean-all.confirm": "OK",
|
||||
"com.affine.rootAppSidebar.migration-data.clean-all.cancel": "Cancel",
|
||||
"com.affine.rootAppSidebar.organize": "Organize",
|
||||
"com.affine.rootAppSidebar.organize.empty": "No Folders",
|
||||
"com.affine.rootAppSidebar.organize.empty-folder": "Empty Folder",
|
||||
|
||||
Reference in New Issue
Block a user