mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-09-23 20:18:42 +08:00
feat(core): new favorite (#7590)
This commit is contained in:
Vendored
+7
@@ -31,6 +31,13 @@ export const runtimeFlagsSchema = z.object({
|
|||||||
enableExperimentalFeature: z.boolean(),
|
enableExperimentalFeature: z.boolean(),
|
||||||
enableInfoModal: z.boolean(),
|
enableInfoModal: z.boolean(),
|
||||||
enableOrganize: 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>;
|
export type RuntimeConfig = z.infer<typeof runtimeFlagsSchema>;
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ export * from './sync';
|
|||||||
export * from './utils';
|
export * from './utils';
|
||||||
|
|
||||||
import type { Framework } from './framework';
|
import type { Framework } from './framework';
|
||||||
import { configureDBModule } from './modules/db';
|
import { configureWorkspaceDBModule } from './modules/db';
|
||||||
import { configureDocModule } from './modules/doc';
|
import { configureDocModule } from './modules/doc';
|
||||||
import { configureGlobalContextModule } from './modules/global-context';
|
import { configureGlobalContextModule } from './modules/global-context';
|
||||||
import { configureLifecycleModule } from './modules/lifecycle';
|
import { configureLifecycleModule } from './modules/lifecycle';
|
||||||
@@ -31,7 +31,7 @@ import {
|
|||||||
export function configureInfraModules(framework: Framework) {
|
export function configureInfraModules(framework: Framework) {
|
||||||
configureWorkspaceModule(framework);
|
configureWorkspaceModule(framework);
|
||||||
configureDocModule(framework);
|
configureDocModule(framework);
|
||||||
configureDBModule(framework);
|
configureWorkspaceDBModule(framework);
|
||||||
configureGlobalStorageModule(framework);
|
configureGlobalStorageModule(framework);
|
||||||
configureGlobalContextModule(framework);
|
configureGlobalContextModule(framework);
|
||||||
configureLifecycleModule(framework);
|
configureLifecycleModule(framework);
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
import type { Framework } from '../../framework';
|
import type { Framework } from '../../framework';
|
||||||
import { WorkspaceScope, WorkspaceService } from '../workspace';
|
import { WorkspaceScope, WorkspaceService } from '../workspace';
|
||||||
import { DBService } from './services/db';
|
import { WorkspaceDBService } from './services/db';
|
||||||
|
|
||||||
export { AFFiNE_DB_SCHEMA } from './schema';
|
export { AFFiNE_WORKSPACE_DB_SCHEMA } from './schema';
|
||||||
export { DBService } from './services/db';
|
export { WorkspaceDBService } from './services/db';
|
||||||
|
|
||||||
export function configureDBModule(framework: Framework) {
|
export function configureWorkspaceDBModule(framework: Framework) {
|
||||||
framework.scope(WorkspaceScope).service(DBService, [WorkspaceService]);
|
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';
|
import { type DBSchemaBuilder, f } from '../../../orm';
|
||||||
|
|
||||||
export const AFFiNE_DB_SCHEMA = {
|
export const AFFiNE_WORKSPACE_DB_SCHEMA = {
|
||||||
folders: {
|
folders: {
|
||||||
id: f.string().primaryKey().optional().default(nanoid),
|
id: f.string().primaryKey().optional().default(nanoid),
|
||||||
parentId: f.string().optional(),
|
parentId: f.string().optional(),
|
||||||
@@ -11,4 +11,13 @@ export const AFFiNE_DB_SCHEMA = {
|
|||||||
index: f.string(),
|
index: f.string(),
|
||||||
},
|
},
|
||||||
} as const satisfies DBSchemaBuilder;
|
} 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 { Service } from '../../../framework';
|
||||||
import { createORMClient, type TableMap, YjsDBAdapter } from '../../../orm';
|
import { createORMClient, type TableMap, YjsDBAdapter } from '../../../orm';
|
||||||
|
import { ObjectPool } from '../../../utils';
|
||||||
import type { WorkspaceService } from '../../workspace';
|
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 {
|
const WorkspaceDBClient = createORMClient(AFFiNE_WORKSPACE_DB_SCHEMA);
|
||||||
db: TableMap<AFFiNE_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) {
|
constructor(private readonly workspaceService: WorkspaceService) {
|
||||||
super();
|
super();
|
||||||
const Client = createORMClient(AFFiNE_DB_SCHEMA);
|
this.db = new WorkspaceDBClient(
|
||||||
this.db = new Client(
|
new YjsDBAdapter(AFFiNE_WORKSPACE_DB_SCHEMA, {
|
||||||
new YjsDBAdapter(AFFiNE_DB_SCHEMA, {
|
|
||||||
getDoc: guid => {
|
getDoc: guid => {
|
||||||
const ydoc = new YDoc({
|
const ydoc = new YDoc({
|
||||||
// guid format: db${workspaceId}${guid}
|
// 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) {
|
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 { cssVar } from '@toeverything/theme';
|
||||||
import { style } from '@vanilla-extract/css';
|
import { style } from '@vanilla-extract/css';
|
||||||
|
|
||||||
export const root = style({
|
export const root = style({
|
||||||
fontSize: cssVar('fontXs'),
|
fontSize: cssVar('fontXs'),
|
||||||
minHeight: '16px',
|
minHeight: '16px',
|
||||||
@@ -10,6 +11,7 @@ export const root = style({
|
|||||||
justifyContent: 'space-between',
|
justifyContent: 'space-between',
|
||||||
marginBottom: '4px',
|
marginBottom: '4px',
|
||||||
padding: '0 8px',
|
padding: '0 8px',
|
||||||
|
gap: '8px',
|
||||||
selectors: {
|
selectors: {
|
||||||
'&:not(:first-of-type)': {
|
'&:not(:first-of-type)': {
|
||||||
marginTop: '16px',
|
marginTop: '16px',
|
||||||
@@ -18,4 +20,5 @@ export const root = style({
|
|||||||
});
|
});
|
||||||
export const label = style({
|
export const label = style({
|
||||||
color: cssVar('black30'),
|
color: cssVar('black30'),
|
||||||
|
flex: '1',
|
||||||
});
|
});
|
||||||
|
|||||||
+2
-2
@@ -1,5 +1,5 @@
|
|||||||
import { FavoriteTag } from '@affine/core/components/page-list';
|
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 { toast } from '@affine/core/utils';
|
||||||
import { useI18n } from '@affine/i18n';
|
import { useI18n } from '@affine/i18n';
|
||||||
import { useLiveData, useService } from '@toeverything/infra';
|
import { useLiveData, useService } from '@toeverything/infra';
|
||||||
@@ -11,7 +11,7 @@ export interface FavoriteButtonProps {
|
|||||||
|
|
||||||
export const useFavorite = (pageId: string) => {
|
export const useFavorite = (pageId: string) => {
|
||||||
const t = useI18n();
|
const t = useI18n();
|
||||||
const favAdapter = useService(FavoriteItemsAdapter);
|
const favAdapter = useService(CompatibleFavoriteItemsAdapter);
|
||||||
|
|
||||||
const favorite = useLiveData(favAdapter.isFavorite$(pageId, 'doc'));
|
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 type { Tag } from '@affine/core/modules/tag';
|
||||||
import { TagService } from '@affine/core/modules/tag';
|
import { TagService } from '@affine/core/modules/tag';
|
||||||
import { useI18n } from '@affine/i18n';
|
import { useI18n } from '@affine/i18n';
|
||||||
@@ -131,9 +131,9 @@ export const useTagGroupDefinitions = (): ItemGroupDefinition<ListItem>[] => {
|
|||||||
const sortedTagsLiveData$ = useMemo(
|
const sortedTagsLiveData$ = useMemo(
|
||||||
() =>
|
() =>
|
||||||
LiveData.computed(get =>
|
LiveData.computed(get =>
|
||||||
get(tagList.tags$).sort((a, b) =>
|
get(tagList.tags$)
|
||||||
get(a.value$).localeCompare(get(b.value$))
|
.slice()
|
||||||
)
|
.sort((a, b) => get(a.value$).localeCompare(get(b.value$)))
|
||||||
),
|
),
|
||||||
[tagList.tags$]
|
[tagList.tags$]
|
||||||
);
|
);
|
||||||
@@ -174,7 +174,7 @@ export const useFavoriteGroupDefinitions = <
|
|||||||
T extends ListItem,
|
T extends ListItem,
|
||||||
>(): ItemGroupDefinition<T>[] => {
|
>(): ItemGroupDefinition<T>[] => {
|
||||||
const t = useI18n();
|
const t = useI18n();
|
||||||
const favAdapter = useService(FavoriteItemsAdapter);
|
const favAdapter = useService(CompatibleFavoriteItemsAdapter);
|
||||||
const favourites = useLiveData(favAdapter.favorites$);
|
const favourites = useLiveData(favAdapter.favorites$);
|
||||||
return useMemo(
|
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 collectionItemRoot } from './collections/collection-list-item.css';
|
||||||
import { root as pageItemRoot } from './docs/page-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 listRootContainer = createContainer('list-root-container');
|
||||||
export const pageListScrollContainer = style({
|
export const pageListScrollContainer = style({
|
||||||
width: '100%',
|
width: '100%',
|
||||||
@@ -49,7 +50,7 @@ export const favoriteCell = style({
|
|||||||
flexShrink: 0,
|
flexShrink: 0,
|
||||||
opacity: 0,
|
opacity: 0,
|
||||||
selectors: {
|
selectors: {
|
||||||
[`&[data-favorite], ${pageItemRoot}:hover &, ${collectionItemRoot}:hover &`]:
|
[`&[data-favorite], ${pageItemRoot}:hover &, ${collectionItemRoot}:hover &, ${tagItemRoot}:hover &`]:
|
||||||
{
|
{
|
||||||
opacity: 1,
|
opacity: 1,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -10,7 +10,8 @@ import {
|
|||||||
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 { 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 { 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 { WorkbenchService } from '@affine/core/modules/workbench';
|
||||||
import { mixpanel } from '@affine/core/utils';
|
import { mixpanel } from '@affine/core/utils';
|
||||||
import type { Collection, DeleteCollectionInfo } from '@affine/env/filter';
|
import type { Collection, DeleteCollectionInfo } from '@affine/env/filter';
|
||||||
@@ -32,7 +33,12 @@ import {
|
|||||||
SplitViewIcon,
|
SplitViewIcon,
|
||||||
} from '@blocksuite/icons/rc';
|
} from '@blocksuite/icons/rc';
|
||||||
import type { DocMeta } from '@blocksuite/store';
|
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 { useCallback, useState } from 'react';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
@@ -63,7 +69,7 @@ export const PageOperationCell = ({
|
|||||||
const { appSettings } = useAppSettingHelper();
|
const { appSettings } = useAppSettingHelper();
|
||||||
const { setTrashModal } = useTrashModalHelper(currentWorkspace.docCollection);
|
const { setTrashModal } = useTrashModalHelper(currentWorkspace.docCollection);
|
||||||
const [openDisableShared, setOpenDisableShared] = useState(false);
|
const [openDisableShared, setOpenDisableShared] = useState(false);
|
||||||
const favAdapter = useService(FavoriteItemsAdapter);
|
const favAdapter = useService(CompatibleFavoriteItemsAdapter);
|
||||||
const favourite = useLiveData(favAdapter.isFavorite$(page.id, 'doc'));
|
const favourite = useLiveData(favAdapter.isFavorite$(page.id, 'doc'));
|
||||||
const workbench = useService(WorkbenchService).workbench;
|
const workbench = useService(WorkbenchService).workbench;
|
||||||
const { duplicate } = useBlockSuiteMetaHelper(currentWorkspace.docCollection);
|
const { duplicate } = useBlockSuiteMetaHelper(currentWorkspace.docCollection);
|
||||||
@@ -215,14 +221,16 @@ export const PageOperationCell = ({
|
|||||||
);
|
);
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ColWrapper
|
{runtimeConfig.enableNewFavorite && (
|
||||||
hideInSmallContainer
|
<ColWrapper
|
||||||
data-testid="page-list-item-favorite"
|
hideInSmallContainer
|
||||||
data-favorite={favourite ? true : undefined}
|
data-testid="page-list-item-favorite"
|
||||||
className={styles.favoriteCell}
|
data-favorite={favourite ? true : undefined}
|
||||||
>
|
className={styles.favoriteCell}
|
||||||
<FavoriteTag onClick={onToggleFavoritePage} active={favourite} />
|
>
|
||||||
</ColWrapper>
|
<FavoriteTag onClick={onToggleFavoritePage} active={favourite} />
|
||||||
|
</ColWrapper>
|
||||||
|
)}
|
||||||
<ColWrapper alignment="start">
|
<ColWrapper alignment="start">
|
||||||
<Menu
|
<Menu
|
||||||
items={OperationMenu}
|
items={OperationMenu}
|
||||||
@@ -319,7 +327,7 @@ export const CollectionOperationCell = ({
|
|||||||
}: CollectionOperationCellProps) => {
|
}: CollectionOperationCellProps) => {
|
||||||
const t = useI18n();
|
const t = useI18n();
|
||||||
|
|
||||||
const favAdapter = useService(FavoriteItemsAdapter);
|
const favAdapter = useService(CompatibleFavoriteItemsAdapter);
|
||||||
const docCollection = useService(WorkspaceService).workspace.docCollection;
|
const docCollection = useService(WorkspaceService).workspace.docCollection;
|
||||||
const { createPage } = usePageHelper(docCollection);
|
const { createPage } = usePageHelper(docCollection);
|
||||||
const { openConfirmModal } = useConfirmModal();
|
const { openConfirmModal } = useConfirmModal();
|
||||||
@@ -482,12 +490,31 @@ export const TagOperationCell = ({
|
|||||||
}: TagOperationCellProps) => {
|
}: TagOperationCellProps) => {
|
||||||
const t = useI18n();
|
const t = useI18n();
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
|
const { favoriteService } = useServices({
|
||||||
|
FavoriteService,
|
||||||
|
});
|
||||||
|
const favourite = useLiveData(
|
||||||
|
favoriteService.favoriteList.isFavorite$('tag', tag.id)
|
||||||
|
);
|
||||||
|
|
||||||
const handleDelete = useCallback(() => {
|
const handleDelete = useCallback(() => {
|
||||||
onTagDelete([tag.id]);
|
onTagDelete([tag.id]);
|
||||||
}, [onTagDelete, tag.id]);
|
}, [onTagDelete, tag.id]);
|
||||||
|
|
||||||
|
const onToggleFavoriteCollection = useCallback(() => {
|
||||||
|
favoriteService.favoriteList.toggle('tag', tag.id);
|
||||||
|
}, [favoriteService, tag.id]);
|
||||||
return (
|
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 className={styles.editTagWrapper} data-show={open}>
|
||||||
<div style={{ width: '100%' }}>
|
<div style={{ width: '100%' }}>
|
||||||
<CreateOrEditTag open={open} onOpenChange={setOpen} tagMeta={tag} />
|
<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 { ShareDocsService } from '@affine/core/modules/share-doc';
|
||||||
import type { Collection, Filter } from '@affine/env/filter';
|
import type { Collection, Filter } from '@affine/env/filter';
|
||||||
import { PublicPageMode } from '@affine/graphql';
|
import { PublicPageMode } from '@affine/graphql';
|
||||||
@@ -36,7 +36,7 @@ export const useFilteredPageMetas = (
|
|||||||
shareDocsService.shareDocs?.revalidate();
|
shareDocsService.shareDocs?.revalidate();
|
||||||
}, [shareDocsService]);
|
}, [shareDocsService]);
|
||||||
|
|
||||||
const favAdapter = useService(FavoriteItemsAdapter);
|
const favAdapter = useService(CompatibleFavoriteItemsAdapter);
|
||||||
const favoriteItems = useLiveData(favAdapter.favorites$);
|
const favoriteItems = useLiveData(favAdapter.favorites$);
|
||||||
|
|
||||||
const filteredPageMetas = useMemo(
|
const filteredPageMetas = useMemo(
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import type { MenuItemProps } from '@affine/component';
|
|||||||
import { Menu, MenuIcon, MenuItem } from '@affine/component';
|
import { Menu, MenuIcon, MenuItem } 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 { useDeleteCollectionInfo } from '@affine/core/hooks/affine/use-delete-collection-info';
|
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 { WorkbenchService } from '@affine/core/modules/workbench';
|
||||||
import type { Collection } from '@affine/env/filter';
|
import type { Collection } from '@affine/env/filter';
|
||||||
import { useI18n } from '@affine/i18n';
|
import { useI18n } from '@affine/i18n';
|
||||||
@@ -79,7 +79,7 @@ export const CollectionOperations = ({
|
|||||||
workbench.openCollection(collection.id, { at: 'tail' });
|
workbench.openCollection(collection.id, { at: 'tail' });
|
||||||
}, [collection.id, workbench]);
|
}, [collection.id, workbench]);
|
||||||
|
|
||||||
const favAdapter = useService(FavoriteItemsAdapter);
|
const favAdapter = useService(CompatibleFavoriteItemsAdapter);
|
||||||
|
|
||||||
const onToggleFavoritePage = useCallback(() => {
|
const onToggleFavoritePage = useCallback(() => {
|
||||||
favAdapter.toggle(collection.id, 'collection');
|
favAdapter.toggle(collection.id, 'collection');
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Tooltip } from '@affine/component';
|
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 type { Collection } from '@affine/env/filter';
|
||||||
import { Trans, useI18n } from '@affine/i18n';
|
import { Trans, useI18n } from '@affine/i18n';
|
||||||
import {
|
import {
|
||||||
@@ -42,7 +42,7 @@ export const RulesMode = ({
|
|||||||
const [showPreview, setShowPreview] = useState(true);
|
const [showPreview, setShowPreview] = useState(true);
|
||||||
const allowListPages: DocMeta[] = [];
|
const allowListPages: DocMeta[] = [];
|
||||||
const rulesPages: DocMeta[] = [];
|
const rulesPages: DocMeta[] = [];
|
||||||
const favAdapter = useService(FavoriteItemsAdapter);
|
const favAdapter = useService(CompatibleFavoriteItemsAdapter);
|
||||||
const favorites = useLiveData(favAdapter.favorites$);
|
const favorites = useLiveData(favAdapter.favorites$);
|
||||||
allPageListConfig.allPages.forEach(meta => {
|
allPageListConfig.allPages.forEach(meta => {
|
||||||
if (meta.trash) {
|
if (meta.trash) {
|
||||||
|
|||||||
+6
-6
@@ -1,6 +1,6 @@
|
|||||||
import { Menu, toast } from '@affine/component';
|
import { Menu, toast } from '@affine/component';
|
||||||
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/properties';
|
import { CompatibleFavoriteItemsAdapter } from '@affine/core/modules/properties';
|
||||||
import { ShareDocsService } from '@affine/core/modules/share-doc';
|
import { ShareDocsService } from '@affine/core/modules/share-doc';
|
||||||
import { PublicPageMode } from '@affine/graphql';
|
import { PublicPageMode } from '@affine/graphql';
|
||||||
import { Trans, useI18n } from '@affine/i18n';
|
import { Trans, useI18n } from '@affine/i18n';
|
||||||
@@ -60,20 +60,20 @@ export const SelectPage = ({
|
|||||||
}, [onChange]);
|
}, [onChange]);
|
||||||
const {
|
const {
|
||||||
workspaceService,
|
workspaceService,
|
||||||
favoriteItemsAdapter,
|
compatibleFavoriteItemsAdapter,
|
||||||
shareDocsService,
|
shareDocsService,
|
||||||
docsService,
|
docsService,
|
||||||
} = useServices({
|
} = useServices({
|
||||||
DocsService,
|
DocsService,
|
||||||
ShareDocsService,
|
ShareDocsService,
|
||||||
WorkspaceService,
|
WorkspaceService,
|
||||||
FavoriteItemsAdapter,
|
CompatibleFavoriteItemsAdapter,
|
||||||
});
|
});
|
||||||
const shareDocs = useLiveData(shareDocsService.shareDocs?.list$);
|
const shareDocs = useLiveData(shareDocsService.shareDocs?.list$);
|
||||||
const workspace = workspaceService.workspace;
|
const workspace = workspaceService.workspace;
|
||||||
const docCollection = workspace.docCollection;
|
const docCollection = workspace.docCollection;
|
||||||
const pageMetas = useBlockSuiteDocMeta(docCollection);
|
const pageMetas = useBlockSuiteDocMeta(docCollection);
|
||||||
const favourites = useLiveData(favoriteItemsAdapter.favorites$);
|
const favourites = useLiveData(compatibleFavoriteItemsAdapter.favorites$);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
shareDocsService.shareDocs?.revalidate();
|
shareDocsService.shareDocs?.revalidate();
|
||||||
@@ -108,14 +108,14 @@ export const SelectPage = ({
|
|||||||
const onToggleFavoritePage = useCallback(
|
const onToggleFavoritePage = useCallback(
|
||||||
(page: DocMeta) => {
|
(page: DocMeta) => {
|
||||||
const status = isFavorite(page);
|
const status = isFavorite(page);
|
||||||
favoriteItemsAdapter.toggle(page.id, 'doc');
|
compatibleFavoriteItemsAdapter.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']()
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
[favoriteItemsAdapter, isFavorite, t]
|
[compatibleFavoriteItemsAdapter, isFavorite, t]
|
||||||
);
|
);
|
||||||
|
|
||||||
const pageHeaderColsDef = usePageHeaderColsDef();
|
const pageHeaderColsDef = usePageHeaderColsDef();
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks';
|
|||||||
import {
|
import {
|
||||||
ExplorerCollections,
|
ExplorerCollections,
|
||||||
ExplorerFavorites,
|
ExplorerFavorites,
|
||||||
|
ExplorerMigrationFavorites,
|
||||||
|
ExplorerOldFavorites,
|
||||||
ExplorerOrganize,
|
ExplorerOrganize,
|
||||||
} from '@affine/core/modules/explorer';
|
} from '@affine/core/modules/explorer';
|
||||||
import { ExplorerTags } from '@affine/core/modules/explorer/views/sections/tags';
|
import { ExplorerTags } from '@affine/core/modules/explorer/views/sections/tags';
|
||||||
@@ -161,8 +163,10 @@ export const RootAppSidebar = memo(
|
|||||||
</MenuItem>
|
</MenuItem>
|
||||||
</SidebarContainer>
|
</SidebarContainer>
|
||||||
<SidebarScrollableContainer>
|
<SidebarScrollableContainer>
|
||||||
|
{runtimeConfig.enableNewFavorite && <ExplorerFavorites />}
|
||||||
{runtimeConfig.enableOrganize && <ExplorerOrganize />}
|
{runtimeConfig.enableOrganize && <ExplorerOrganize />}
|
||||||
<ExplorerFavorites />
|
{runtimeConfig.enableNewFavorite && <ExplorerMigrationFavorites />}
|
||||||
|
{runtimeConfig.enableOldFavorite && <ExplorerOldFavorites />}
|
||||||
<ExplorerCollections />
|
<ExplorerCollections />
|
||||||
<ExplorerTags />
|
<ExplorerTags />
|
||||||
<CategoryDivider label={t['com.affine.rootAppSidebar.others']()} />
|
<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 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/properties';
|
import { CompatibleFavoriteItemsAdapter } from '@affine/core/modules/properties';
|
||||||
import { ShareDocsService } from '@affine/core/modules/share-doc';
|
import { ShareDocsService } from '@affine/core/modules/share-doc';
|
||||||
import { PublicPageMode } from '@affine/graphql';
|
import { PublicPageMode } from '@affine/graphql';
|
||||||
import { useI18n } from '@affine/i18n';
|
import { useI18n } from '@affine/i18n';
|
||||||
@@ -32,7 +32,7 @@ export const useAllPageListConfig = () => {
|
|||||||
() => Object.fromEntries(pageMetas.map(page => [page.id, page])),
|
() => Object.fromEntries(pageMetas.map(page => [page.id, page])),
|
||||||
[pageMetas]
|
[pageMetas]
|
||||||
);
|
);
|
||||||
const favAdapter = useService(FavoriteItemsAdapter);
|
const favAdapter = useService(CompatibleFavoriteItemsAdapter);
|
||||||
const t = useI18n();
|
const t = useI18n();
|
||||||
const favoriteItems = useLiveData(favAdapter.favorites$);
|
const favoriteItems = useLiveData(favAdapter.favorites$);
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import {
|
|||||||
PreconditionStrategy,
|
PreconditionStrategy,
|
||||||
registerAffineCommand,
|
registerAffineCommand,
|
||||||
} from '@affine/core/commands';
|
} 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 { TelemetryWorkspaceContextService } from '@affine/core/modules/telemetry/services/telemetry';
|
||||||
import { mixpanel } from '@affine/core/utils';
|
import { mixpanel } from '@affine/core/utils';
|
||||||
import { WorkspaceFlavour } from '@affine/env/workspace';
|
import { WorkspaceFlavour } from '@affine/env/workspace';
|
||||||
@@ -32,7 +32,7 @@ export function useRegisterBlocksuiteEditorCommands() {
|
|||||||
const workspace = useService(WorkspaceService).workspace;
|
const workspace = useService(WorkspaceService).workspace;
|
||||||
const docCollection = workspace.docCollection;
|
const docCollection = workspace.docCollection;
|
||||||
|
|
||||||
const favAdapter = useService(FavoriteItemsAdapter);
|
const favAdapter = useService(CompatibleFavoriteItemsAdapter);
|
||||||
const favorite = useLiveData(favAdapter.isFavorite$(docId, 'doc'));
|
const favorite = useLiveData(favAdapter.isFavorite$(docId, 'doc'));
|
||||||
const trash = useLiveData(doc.trash$);
|
const trash = useLiveData(doc.trash$);
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import { DebugLogger } from '@affine/debug';
|
import { DebugLogger } from '@affine/debug';
|
||||||
import type { Job, JobQueue, WorkspaceService } from '@toeverything/infra';
|
import type { Job, JobQueue, WorkspaceService } from '@toeverything/infra';
|
||||||
import {
|
import {
|
||||||
DBService,
|
|
||||||
Entity,
|
Entity,
|
||||||
IndexedDBIndexStorage,
|
IndexedDBIndexStorage,
|
||||||
IndexedDBJobQueue,
|
IndexedDBJobQueue,
|
||||||
JobRunner,
|
JobRunner,
|
||||||
LiveData,
|
LiveData,
|
||||||
|
WorkspaceDBService,
|
||||||
} from '@toeverything/infra';
|
} from '@toeverything/infra';
|
||||||
import { map } from 'rxjs';
|
import { map } from 'rxjs';
|
||||||
|
|
||||||
@@ -69,7 +69,7 @@ export class DocsIndexer extends Entity {
|
|||||||
|
|
||||||
setupListener() {
|
setupListener() {
|
||||||
this.workspaceEngine.doc.storage.eventBus.on(event => {
|
this.workspaceEngine.doc.storage.eventBus.on(event => {
|
||||||
if (DBService.isDBDocId(event.docId)) {
|
if (WorkspaceDBService.isDBDocId(event.docId)) {
|
||||||
// skip db doc
|
// skip db doc
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
export { ExplorerCollections } from './views/sections/collections';
|
export { ExplorerCollections } from './views/sections/collections';
|
||||||
export { ExplorerFavorites } from './views/sections/favorites';
|
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';
|
export { ExplorerOrganize } from './views/sections/organize';
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import {
|
|||||||
useEditCollection,
|
useEditCollection,
|
||||||
} 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/properties';
|
import { CompatibleFavoriteItemsAdapter } from '@affine/core/modules/properties';
|
||||||
import { ShareDocsService } from '@affine/core/modules/share-doc';
|
import { ShareDocsService } from '@affine/core/modules/share-doc';
|
||||||
import type { AffineDNDData } from '@affine/core/types/dnd';
|
import type { AffineDNDData } from '@affine/core/types/dnd';
|
||||||
import type { Collection } from '@affine/env/filter';
|
import type { Collection } from '@affine/env/filter';
|
||||||
@@ -225,12 +225,12 @@ const ExplorerCollectionNodeChildren = ({
|
|||||||
const t = useI18n();
|
const t = useI18n();
|
||||||
const {
|
const {
|
||||||
docsService,
|
docsService,
|
||||||
favoriteItemsAdapter,
|
compatibleFavoriteItemsAdapter,
|
||||||
shareDocsService,
|
shareDocsService,
|
||||||
collectionService,
|
collectionService,
|
||||||
} = useServices({
|
} = useServices({
|
||||||
DocsService,
|
DocsService,
|
||||||
FavoriteItemsAdapter,
|
CompatibleFavoriteItemsAdapter,
|
||||||
ShareDocsService,
|
ShareDocsService,
|
||||||
CollectionService,
|
CollectionService,
|
||||||
});
|
});
|
||||||
@@ -251,7 +251,7 @@ const ExplorerCollectionNodeChildren = ({
|
|||||||
[docsService]
|
[docsService]
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
const favourites = useLiveData(favoriteItemsAdapter.favorites$);
|
const favourites = useLiveData(compatibleFavoriteItemsAdapter.favorites$);
|
||||||
const allowList = useMemo(
|
const allowList = useMemo(
|
||||||
() => new Set(collection.allowList),
|
() => new Set(collection.allowList),
|
||||||
[collection.allowList]
|
[collection.allowList]
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import {
|
|||||||
import { useAppSettingHelper } from '@affine/core/hooks/affine/use-app-setting-helper';
|
import { useAppSettingHelper } from '@affine/core/hooks/affine/use-app-setting-helper';
|
||||||
import { useDeleteCollectionInfo } from '@affine/core/hooks/affine/use-delete-collection-info';
|
import { useDeleteCollectionInfo } from '@affine/core/hooks/affine/use-delete-collection-info';
|
||||||
import { CollectionService } from '@affine/core/modules/collection';
|
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 { WorkbenchService } from '@affine/core/modules/workbench';
|
||||||
import { useI18n } from '@affine/i18n';
|
import { useI18n } from '@affine/i18n';
|
||||||
import {
|
import {
|
||||||
@@ -35,19 +35,20 @@ export const useExplorerCollectionNodeOperations = (
|
|||||||
workbenchService,
|
workbenchService,
|
||||||
docsService,
|
docsService,
|
||||||
collectionService,
|
collectionService,
|
||||||
favoriteItemsAdapter,
|
compatibleFavoriteItemsAdapter,
|
||||||
} = useServices({
|
} = useServices({
|
||||||
DocsService,
|
DocsService,
|
||||||
WorkbenchService,
|
WorkbenchService,
|
||||||
CollectionService,
|
CollectionService,
|
||||||
FavoriteItemsAdapter,
|
CompatibleFavoriteItemsAdapter,
|
||||||
});
|
});
|
||||||
const deleteInfo = useDeleteCollectionInfo();
|
const deleteInfo = useDeleteCollectionInfo();
|
||||||
|
|
||||||
const favorite = useLiveData(
|
const favorite = useLiveData(
|
||||||
useMemo(
|
useMemo(
|
||||||
() => favoriteItemsAdapter.isFavorite$(collectionId, 'collection'),
|
() =>
|
||||||
[collectionId, favoriteItemsAdapter]
|
compatibleFavoriteItemsAdapter.isFavorite$(collectionId, 'collection'),
|
||||||
|
[collectionId, compatibleFavoriteItemsAdapter]
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
const { openConfirmModal } = useConfirmModal();
|
const { openConfirmModal } = useConfirmModal();
|
||||||
@@ -66,8 +67,8 @@ export const useExplorerCollectionNodeOperations = (
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
const handleToggleFavoritePage = useCallback(() => {
|
const handleToggleFavoritePage = useCallback(() => {
|
||||||
favoriteItemsAdapter.toggle(collectionId, 'collection');
|
compatibleFavoriteItemsAdapter.toggle(collectionId, 'collection');
|
||||||
}, [favoriteItemsAdapter, collectionId]);
|
}, [compatibleFavoriteItemsAdapter, collectionId]);
|
||||||
|
|
||||||
const handleAddDocToCollection = useCallback(() => {
|
const handleAddDocToCollection = useCallback(() => {
|
||||||
openConfirmModal({
|
openConfirmModal({
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import {
|
|||||||
} 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 { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks';
|
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 { WorkbenchService } from '@affine/core/modules/workbench';
|
||||||
import { useI18n } from '@affine/i18n';
|
import { useI18n } from '@affine/i18n';
|
||||||
import {
|
import {
|
||||||
@@ -32,20 +32,20 @@ export const useExplorerDocNodeOperations = (
|
|||||||
): NodeOperation[] => {
|
): NodeOperation[] => {
|
||||||
const t = useI18n();
|
const t = useI18n();
|
||||||
const { appSettings } = useAppSettingHelper();
|
const { appSettings } = useAppSettingHelper();
|
||||||
const { workbenchService, docsService, favoriteItemsAdapter } = useServices({
|
const { workbenchService, docsService, compatibleFavoriteItemsAdapter } =
|
||||||
DocsService,
|
useServices({
|
||||||
WorkbenchService,
|
DocsService,
|
||||||
FavoriteItemsAdapter,
|
WorkbenchService,
|
||||||
});
|
CompatibleFavoriteItemsAdapter,
|
||||||
|
});
|
||||||
const { openConfirmModal } = useConfirmModal();
|
const { openConfirmModal } = useConfirmModal();
|
||||||
|
|
||||||
const docRecord = useLiveData(docsService.list.doc$(docId));
|
const docRecord = useLiveData(docsService.list.doc$(docId));
|
||||||
|
|
||||||
const favorite = useLiveData(
|
const favorite = useLiveData(
|
||||||
useMemo(
|
useMemo(() => {
|
||||||
() => favoriteItemsAdapter.isFavorite$(docId, 'doc'),
|
return compatibleFavoriteItemsAdapter.isFavorite$(docId, 'doc');
|
||||||
[docId, favoriteItemsAdapter]
|
}, [docId, compatibleFavoriteItemsAdapter])
|
||||||
)
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleMoveToTrash = useCallback(() => {
|
const handleMoveToTrash = useCallback(() => {
|
||||||
@@ -84,8 +84,8 @@ export const useExplorerDocNodeOperations = (
|
|||||||
}, [docId, options, docsService, workbenchService.workbench]);
|
}, [docId, options, docsService, workbenchService.workbench]);
|
||||||
|
|
||||||
const handleToggleFavoriteDoc = useCallback(() => {
|
const handleToggleFavoriteDoc = useCallback(() => {
|
||||||
favoriteItemsAdapter.toggle(docId, 'doc');
|
compatibleFavoriteItemsAdapter.toggle(docId, 'doc');
|
||||||
}, [favoriteItemsAdapter, docId]);
|
}, [docId, compatibleFavoriteItemsAdapter]);
|
||||||
|
|
||||||
return useMemo(
|
return useMemo(
|
||||||
() => [
|
() => [
|
||||||
|
|||||||
@@ -6,10 +6,17 @@ import {
|
|||||||
toast,
|
toast,
|
||||||
} 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 { FavoriteService } from '@affine/core/modules/favorite';
|
||||||
import { TagService } from '@affine/core/modules/tag';
|
import { TagService } from '@affine/core/modules/tag';
|
||||||
import { WorkbenchService } from '@affine/core/modules/workbench';
|
import { WorkbenchService } from '@affine/core/modules/workbench';
|
||||||
import { useI18n } from '@affine/i18n';
|
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 { DocsService, useLiveData, useServices } from '@toeverything/infra';
|
||||||
import { useCallback, useMemo } from 'react';
|
import { useCallback, useMemo } from 'react';
|
||||||
|
|
||||||
@@ -25,12 +32,17 @@ export const useExplorerTagNodeOperations = (
|
|||||||
): NodeOperation[] => {
|
): NodeOperation[] => {
|
||||||
const t = useI18n();
|
const t = useI18n();
|
||||||
const { appSettings } = useAppSettingHelper();
|
const { appSettings } = useAppSettingHelper();
|
||||||
const { docsService, workbenchService, tagService } = useServices({
|
const { docsService, workbenchService, tagService, favoriteService } =
|
||||||
WorkbenchService,
|
useServices({
|
||||||
TagService,
|
WorkbenchService,
|
||||||
DocsService,
|
TagService,
|
||||||
});
|
DocsService,
|
||||||
|
FavoriteService,
|
||||||
|
});
|
||||||
|
|
||||||
|
const favorite = useLiveData(
|
||||||
|
favoriteService.favoriteList.favorite$('tag', tagId)
|
||||||
|
);
|
||||||
const tagRecord = useLiveData(tagService.tagList.tagByTagId$(tagId));
|
const tagRecord = useLiveData(tagService.tagList.tagByTagId$(tagId));
|
||||||
|
|
||||||
const handleNewDoc = useCallback(() => {
|
const handleNewDoc = useCallback(() => {
|
||||||
@@ -53,6 +65,10 @@ export const useExplorerTagNodeOperations = (
|
|||||||
});
|
});
|
||||||
}, [tagId, workbenchService]);
|
}, [tagId, workbenchService]);
|
||||||
|
|
||||||
|
const handleToggleFavoriteTag = useCallback(() => {
|
||||||
|
favoriteService.favoriteList.toggle('tag', tagId);
|
||||||
|
}, [favoriteService, tagId]);
|
||||||
|
|
||||||
return useMemo(
|
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,
|
index: 9999,
|
||||||
view: <MenuSeparator key="menu-separator" />,
|
view: <MenuSeparator key="menu-separator" />,
|
||||||
@@ -106,9 +149,11 @@ export const useExplorerTagNodeOperations = (
|
|||||||
],
|
],
|
||||||
[
|
[
|
||||||
appSettings.enableMultiView,
|
appSettings.enableMultiView,
|
||||||
|
favorite,
|
||||||
handleMoveToTrash,
|
handleMoveToTrash,
|
||||||
handleNewDoc,
|
handleNewDoc,
|
||||||
handleOpenInSplitView,
|
handleOpenInSplitView,
|
||||||
|
handleToggleFavoriteTag,
|
||||||
t,
|
t,
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -39,10 +39,10 @@ export const RootEmpty = ({
|
|||||||
<FolderIcon className={styles.icon} />
|
<FolderIcon className={styles.icon} />
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
data-testid="slider-bar-organize-empty-message"
|
data-testid="slider-bar-favorites-empty-message"
|
||||||
className={styles.message}
|
className={styles.message}
|
||||||
>
|
>
|
||||||
{t['com.affine.rootAppSidebar.organize.empty']()}
|
{t['com.affine.rootAppSidebar.favorites.empty']()}
|
||||||
</div>
|
</div>
|
||||||
{dropEffect && draggedOverDraggable && (
|
{dropEffect && draggedOverDraggable && (
|
||||||
<DropEffect
|
<DropEffect
|
||||||
|
|||||||
@@ -10,7 +10,11 @@ import {
|
|||||||
type ExplorerTreeNodeDropEffect,
|
type ExplorerTreeNodeDropEffect,
|
||||||
ExplorerTreeRoot,
|
ExplorerTreeRoot,
|
||||||
} from '@affine/core/modules/explorer/views/tree';
|
} 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 { WorkbenchService } from '@affine/core/modules/workbench';
|
||||||
import type { AffineDNDData } from '@affine/core/types/dnd';
|
import type { AffineDNDData } from '@affine/core/types/dnd';
|
||||||
import { useI18n } from '@affine/i18n';
|
import { useI18n } from '@affine/i18n';
|
||||||
@@ -20,55 +24,42 @@ import { useCallback, useMemo } from 'react';
|
|||||||
|
|
||||||
import { ExplorerCollectionNode } from '../../nodes/collection';
|
import { ExplorerCollectionNode } from '../../nodes/collection';
|
||||||
import { ExplorerDocNode } from '../../nodes/doc';
|
import { ExplorerDocNode } from '../../nodes/doc';
|
||||||
|
import { ExplorerFolderNode } from '../../nodes/folder';
|
||||||
|
import { ExplorerTagNode } from '../../nodes/tag';
|
||||||
import { RootEmpty } from './empty';
|
import { RootEmpty } from './empty';
|
||||||
import * as styles from './styles.css';
|
import * as styles from './styles.css';
|
||||||
|
|
||||||
export const ExplorerFavorites = () => {
|
export const ExplorerFavorites = () => {
|
||||||
const { favoriteItemsAdapter, docsService, workbenchService } = useServices({
|
const { favoriteService, docsService, workbenchService } = useServices({
|
||||||
FavoriteItemsAdapter,
|
FavoriteService,
|
||||||
DocsService,
|
DocsService,
|
||||||
WorkbenchService,
|
WorkbenchService,
|
||||||
});
|
});
|
||||||
|
|
||||||
const docs = useLiveData(docsService.list.docs$);
|
const favorites = useLiveData(favoriteService.favoriteList.sortedList$);
|
||||||
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 t = useI18n();
|
||||||
|
|
||||||
const handleDrop = useCallback(
|
const handleDrop = useCallback(
|
||||||
(data: DropTargetDropEvent<AffineDNDData>) => {
|
(data: DropTargetDropEvent<AffineDNDData>) => {
|
||||||
if (
|
if (
|
||||||
data.source.data.entity?.type === 'doc' ||
|
data.source.data.entity?.type &&
|
||||||
data.source.data.entity?.type === 'collection'
|
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.id,
|
||||||
data.source.data.entity?.type,
|
favoriteService.favoriteList.indexAt('before')
|
||||||
true
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[favoriteItemsAdapter]
|
[favoriteService]
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleDropEffect = useCallback<ExplorerTreeNodeDropEffect>(data => {
|
const handleDropEffect = useCallback<ExplorerTreeNodeDropEffect>(data => {
|
||||||
if (
|
if (
|
||||||
data.source.data.entity?.type === 'doc' ||
|
data.source.data.entity?.type &&
|
||||||
data.source.data.entity?.type === 'collection'
|
isFavoriteSupportType(data.source.data.entity.type)
|
||||||
) {
|
) {
|
||||||
return 'link';
|
return 'link';
|
||||||
}
|
}
|
||||||
@@ -77,23 +68,26 @@ export const ExplorerFavorites = () => {
|
|||||||
|
|
||||||
const handleCanDrop = useMemo<DropTargetOptions<AffineDNDData>['canDrop']>(
|
const handleCanDrop = useMemo<DropTargetOptions<AffineDNDData>['canDrop']>(
|
||||||
() => data => {
|
() => data => {
|
||||||
return (
|
return data.source.data.entity?.type
|
||||||
data.source.data.entity?.type === 'doc' ||
|
? isFavoriteSupportType(data.source.data.entity.type)
|
||||||
data.source.data.entity?.type === 'collection'
|
: false;
|
||||||
);
|
|
||||||
},
|
},
|
||||||
[]
|
[]
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleCreateNewFavoriteDoc = useCallback(() => {
|
const handleCreateNewFavoriteDoc = useCallback(() => {
|
||||||
const newDoc = docsService.createDoc();
|
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);
|
workbenchService.workbench.openDoc(newDoc.id);
|
||||||
}, [docsService, favoriteItemsAdapter, workbenchService]);
|
}, [docsService, favoriteService, workbenchService]);
|
||||||
|
|
||||||
const handleOnChildrenDrop = useCallback(
|
const handleOnChildrenDrop = useCallback(
|
||||||
(
|
(
|
||||||
favorite: { id: string; type: 'doc' | 'collection' },
|
favorite: { id: string; type: FavoriteSupportType },
|
||||||
data: DropTargetDropEvent<AffineDNDData>
|
data: DropTargetDropEvent<AffineDNDData>
|
||||||
) => {
|
) => {
|
||||||
if (
|
if (
|
||||||
@@ -101,42 +95,41 @@ export const ExplorerFavorites = () => {
|
|||||||
data.treeInstruction?.type === 'reorder-below'
|
data.treeInstruction?.type === 'reorder-below'
|
||||||
) {
|
) {
|
||||||
if (
|
if (
|
||||||
data.source.data.from?.at === 'explorer:favorite:items' &&
|
data.source.data.from?.at === 'explorer:favorite:list' &&
|
||||||
(data.source.data.entity?.type === 'doc' ||
|
data.source.data.entity?.type &&
|
||||||
data.source.data.entity?.type === 'collection')
|
isFavoriteSupportType(data.source.data.entity.type)
|
||||||
) {
|
) {
|
||||||
// is reordering
|
// is reordering
|
||||||
favoriteItemsAdapter.sorter.moveTo(
|
favoriteService.favoriteList.reorder(
|
||||||
FavoriteItemsAdapter.getFavItemKey(
|
data.source.data.entity.type,
|
||||||
data.source.data.entity.id,
|
data.source.data.entity.id,
|
||||||
data.source.data.entity.type
|
favoriteService.favoriteList.indexAt(
|
||||||
),
|
data.treeInstruction?.type === 'reorder-above'
|
||||||
FavoriteItemsAdapter.getFavItemKey(favorite.id, favorite.type),
|
? 'before'
|
||||||
data.treeInstruction?.type === 'reorder-above' ? 'before' : 'after'
|
: 'after',
|
||||||
|
favorite
|
||||||
|
)
|
||||||
);
|
);
|
||||||
} else if (
|
} else if (
|
||||||
data.source.data.entity?.type === 'doc' ||
|
data.source.data.entity?.type &&
|
||||||
data.source.data.entity?.type === 'collection'
|
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.id,
|
||||||
data.source.data.entity?.type,
|
favoriteService.favoriteList.indexAt(
|
||||||
true
|
data.treeInstruction?.type === 'reorder-above'
|
||||||
);
|
? 'before'
|
||||||
favoriteItemsAdapter.sorter.moveTo(
|
: 'after',
|
||||||
FavoriteItemsAdapter.getFavItemKey(
|
favorite
|
||||||
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
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
return; // not supported
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[favoriteItemsAdapter]
|
[favoriteService]
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleChildrenDropEffect = useCallback<ExplorerTreeNodeDropEffect>(
|
const handleChildrenDropEffect = useCallback<ExplorerTreeNodeDropEffect>(
|
||||||
@@ -146,14 +139,14 @@ export const ExplorerFavorites = () => {
|
|||||||
data.treeInstruction?.type === 'reorder-below'
|
data.treeInstruction?.type === 'reorder-below'
|
||||||
) {
|
) {
|
||||||
if (
|
if (
|
||||||
data.source.data.from?.at === 'explorer:favorite:items' &&
|
data.source.data.from?.at === 'explorer:favorite:list' &&
|
||||||
(data.source.data.entity?.type === 'doc' ||
|
data.source.data.entity?.type &&
|
||||||
data.source.data.entity?.type === 'collection')
|
isFavoriteSupportType(data.source.data.entity.type)
|
||||||
) {
|
) {
|
||||||
return 'move';
|
return 'move';
|
||||||
} else if (
|
} else if (
|
||||||
data.source.data.entity?.type === 'doc' ||
|
data.source.data.entity?.type &&
|
||||||
data.source.data.entity?.type === 'collection'
|
isFavoriteSupportType(data.source.data.entity.type)
|
||||||
) {
|
) {
|
||||||
return 'link';
|
return 'link';
|
||||||
}
|
}
|
||||||
@@ -167,8 +160,9 @@ export const ExplorerFavorites = () => {
|
|||||||
DropTargetOptions<AffineDNDData>['canDrop']
|
DropTargetOptions<AffineDNDData>['canDrop']
|
||||||
>(
|
>(
|
||||||
() => args =>
|
() => args =>
|
||||||
args.source.data.entity?.type === 'doc' ||
|
args.source.data.entity?.type
|
||||||
args.source.data.entity?.type === 'collection',
|
? isFavoriteSupportType(args.source.data.entity.type)
|
||||||
|
: false,
|
||||||
[]
|
[]
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -236,7 +230,7 @@ export const ExplorerFavorites = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const childLocation = {
|
const childLocation = {
|
||||||
at: 'explorer:favorite:items' as const,
|
at: 'explorer:favorite:list' as const,
|
||||||
};
|
};
|
||||||
const ExplorerFavoriteNode = ({
|
const ExplorerFavoriteNode = ({
|
||||||
favorite,
|
favorite,
|
||||||
@@ -246,13 +240,13 @@ const ExplorerFavoriteNode = ({
|
|||||||
}: {
|
}: {
|
||||||
favorite: {
|
favorite: {
|
||||||
id: string;
|
id: string;
|
||||||
type: 'collection' | 'doc';
|
type: FavoriteSupportType;
|
||||||
};
|
};
|
||||||
canDrop?: DropTargetOptions<AffineDNDData>['canDrop'];
|
canDrop?: DropTargetOptions<AffineDNDData>['canDrop'];
|
||||||
onDrop: (
|
onDrop: (
|
||||||
favorite: {
|
favorite: {
|
||||||
id: string;
|
id: string;
|
||||||
type: 'collection' | 'doc';
|
type: FavoriteSupportType;
|
||||||
},
|
},
|
||||||
data: DropTargetDropEvent<AffineDNDData>
|
data: DropTargetDropEvent<AffineDNDData>
|
||||||
) => void;
|
) => void;
|
||||||
@@ -273,6 +267,24 @@ const ExplorerFavoriteNode = ({
|
|||||||
dropEffect={dropEffect}
|
dropEffect={dropEffect}
|
||||||
canDrop={canDrop}
|
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
|
<ExplorerCollectionNode
|
||||||
key={favorite.id}
|
key={favorite.id}
|
||||||
|
|||||||
+149
@@ -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}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
+25
@@ -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}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
+15
@@ -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',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -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
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -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 { configureCollectionModule } from './collection';
|
||||||
import { configureDocLinksModule } from './doc-link';
|
import { configureDocLinksModule } from './doc-link';
|
||||||
import { configureDocsSearchModule } from './docs-search';
|
import { configureDocsSearchModule } from './docs-search';
|
||||||
|
import { configureFavoriteModule } from './favorite';
|
||||||
import { configureFindInPageModule } from './find-in-page';
|
import { configureFindInPageModule } from './find-in-page';
|
||||||
import { configureNavigationModule } from './navigation';
|
import { configureNavigationModule } from './navigation';
|
||||||
import { configureOrganizeModule } from './organize';
|
import { configureOrganizeModule } from './organize';
|
||||||
@@ -35,4 +36,5 @@ export function configureCommonModules(framework: Framework) {
|
|||||||
configureDocsSearchModule(framework);
|
configureDocsSearchModule(framework);
|
||||||
configureDocLinksModule(framework);
|
configureDocLinksModule(framework);
|
||||||
configureOrganizeModule(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 { FolderNode } from './entities/folder-node';
|
||||||
import { FolderTree } from './entities/folder-tree';
|
import { FolderTree } from './entities/folder-tree';
|
||||||
@@ -14,5 +18,5 @@ export function configureOrganizeModule(framework: Framework) {
|
|||||||
.service(OrganizeService)
|
.service(OrganizeService)
|
||||||
.entity(FolderTree, [FolderStore])
|
.entity(FolderTree, [FolderStore])
|
||||||
.entity(FolderNode, [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';
|
import { Store } from '@toeverything/infra';
|
||||||
|
|
||||||
export class FolderStore extends Store {
|
export class FolderStore extends Store {
|
||||||
constructor(private readonly dbService: DBService) {
|
constructor(private readonly dbService: WorkspaceDBService) {
|
||||||
super();
|
super();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
export {
|
export {
|
||||||
|
CompatibleFavoriteItemsAdapter,
|
||||||
FavoriteItemsAdapter,
|
FavoriteItemsAdapter,
|
||||||
WorkspacePropertiesAdapter,
|
WorkspacePropertiesAdapter,
|
||||||
} from './services/adapter';
|
} from './services/adapter';
|
||||||
@@ -10,7 +11,9 @@ import {
|
|||||||
WorkspaceService,
|
WorkspaceService,
|
||||||
} from '@toeverything/infra';
|
} from '@toeverything/infra';
|
||||||
|
|
||||||
|
import { FavoriteService } from '../favorite';
|
||||||
import {
|
import {
|
||||||
|
CompatibleFavoriteItemsAdapter,
|
||||||
FavoriteItemsAdapter,
|
FavoriteItemsAdapter,
|
||||||
WorkspacePropertiesAdapter,
|
WorkspacePropertiesAdapter,
|
||||||
} from './services/adapter';
|
} from './services/adapter';
|
||||||
@@ -21,5 +24,9 @@ export function configureWorkspacePropertiesModule(framework: Framework) {
|
|||||||
.scope(WorkspaceScope)
|
.scope(WorkspaceScope)
|
||||||
.service(WorkspaceLegacyProperties, [WorkspaceService])
|
.service(WorkspaceLegacyProperties, [WorkspaceService])
|
||||||
.service(WorkspacePropertiesAdapter, [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 { defaultsDeep } from 'lodash-es';
|
||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
|
|
||||||
|
import type { FavoriteService } from '../../favorite';
|
||||||
import {
|
import {
|
||||||
PagePropertyType,
|
PagePropertyType,
|
||||||
PageSystemPropertyId,
|
PageSystemPropertyId,
|
||||||
@@ -130,6 +131,9 @@ export class WorkspacePropertiesAdapter extends Service {
|
|||||||
return this.proxy.schema;
|
return this.proxy.schema;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated
|
||||||
|
*/
|
||||||
get favorites() {
|
get favorites() {
|
||||||
return this.proxy.favorites;
|
return this.proxy.favorites;
|
||||||
}
|
}
|
||||||
@@ -154,8 +158,18 @@ export class WorkspacePropertiesAdapter extends Service {
|
|||||||
const pageProperties = this.pageProperties?.[id];
|
const pageProperties = this.pageProperties?.[id];
|
||||||
pageProperties!.system[PageSystemPropertyId.Journal].value = date;
|
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 {
|
export class FavoriteItemsAdapter extends Service {
|
||||||
constructor(private readonly adapter: WorkspacePropertiesAdapter) {
|
constructor(private readonly adapter: WorkspacePropertiesAdapter) {
|
||||||
super();
|
super();
|
||||||
@@ -285,4 +299,70 @@ export class FavoriteItemsAdapter extends Service {
|
|||||||
existing.value = false;
|
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;
|
collectionId: string;
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
at: 'explorer:favorite:items';
|
at: 'explorer:favorite:list';
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
at: 'explorer:old-favorite:list';
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
at: 'explorer:migration-data:list';
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
at: 'all-docs:list';
|
at: 'all-docs:list';
|
||||||
@@ -64,6 +70,12 @@ export interface AffineDNDData extends DNDData {
|
|||||||
| {
|
| {
|
||||||
at: 'explorer:organize:folder';
|
at: 'explorer:organize:folder';
|
||||||
}
|
}
|
||||||
|
| {
|
||||||
|
at: 'explorer:favorite:root';
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
at: 'explorer:old-favorite:root';
|
||||||
|
}
|
||||||
| {
|
| {
|
||||||
at: 'explorer:doc';
|
at: 'explorer:doc';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1115,6 +1115,15 @@
|
|||||||
"com.affine.resetSyncStatus.description": "This operation may fix some synchronization issues.",
|
"com.affine.resetSyncStatus.description": "This operation may fix some synchronization issues.",
|
||||||
"com.affine.rootAppSidebar.collections": "Collections",
|
"com.affine.rootAppSidebar.collections": "Collections",
|
||||||
"com.affine.rootAppSidebar.favorites": "Favourites",
|
"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": "Organize",
|
||||||
"com.affine.rootAppSidebar.organize.empty": "No Folders",
|
"com.affine.rootAppSidebar.organize.empty": "No Folders",
|
||||||
"com.affine.rootAppSidebar.organize.empty-folder": "Empty Folder",
|
"com.affine.rootAppSidebar.organize.empty-folder": "Empty Folder",
|
||||||
|
|||||||
@@ -178,11 +178,11 @@ test('items in favourites can be reordered by dragging', async ({ page }) => {
|
|||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
page.getByTestId('explorer-favorites').locator('[draggable]').first()
|
page.getByTestId('explorer-favorites').locator('[draggable]').first()
|
||||||
).toHaveText(title0);
|
).toHaveText('test collection');
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
page.getByTestId('explorer-favorites').locator('[draggable]').last()
|
page.getByTestId('explorer-favorites').locator('[draggable]').last()
|
||||||
).toHaveText('test collection');
|
).toHaveText(title0);
|
||||||
|
|
||||||
// drag the first item to the last
|
// drag the first item to the last
|
||||||
const firstItem = page
|
const firstItem = page
|
||||||
@@ -206,5 +206,5 @@ test('items in favourites can be reordered by dragging', async ({ page }) => {
|
|||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
page.getByTestId('explorer-favorites').locator('[draggable]').last()
|
page.getByTestId('explorer-favorites').locator('[draggable]').last()
|
||||||
).toHaveText(title0);
|
).toHaveText('test collection');
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -22,6 +22,8 @@ export function getRuntimeConfig(buildFlags: BuildFlags): RuntimeConfig {
|
|||||||
allowLocalWorkspace:
|
allowLocalWorkspace:
|
||||||
buildFlags.distribution === 'desktop' ? true : false,
|
buildFlags.distribution === 'desktop' ? true : false,
|
||||||
enableOrganize: false,
|
enableOrganize: false,
|
||||||
|
enableNewFavorite: false,
|
||||||
|
enableOldFavorite: true,
|
||||||
|
|
||||||
// TODO(@forehalo): enable in next release and canary
|
// TODO(@forehalo): enable in next release and canary
|
||||||
enableInfoModal: false,
|
enableInfoModal: false,
|
||||||
@@ -56,6 +58,8 @@ export function getRuntimeConfig(buildFlags: BuildFlags): RuntimeConfig {
|
|||||||
changelogUrl: 'https://github.com/toeverything/AFFiNE/releases',
|
changelogUrl: 'https://github.com/toeverything/AFFiNE/releases',
|
||||||
enableInfoModal: true,
|
enableInfoModal: true,
|
||||||
enableOrganize: true,
|
enableOrganize: true,
|
||||||
|
enableNewFavorite: true,
|
||||||
|
enableOldFavorite: true,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user