diff --git a/packages/common/env/src/global.ts b/packages/common/env/src/global.ts index 5c4fb81996..ffd3df7be1 100644 --- a/packages/common/env/src/global.ts +++ b/packages/common/env/src/global.ts @@ -31,6 +31,13 @@ export const runtimeFlagsSchema = z.object({ enableExperimentalFeature: z.boolean(), enableInfoModal: z.boolean(), enableOrganize: z.boolean(), + // show the new favorite, which exclusive to each user + enableNewFavorite: z.boolean(), + // show the old favorite + enableOldFavorite: z.boolean(), + // before 0.16, enableNewFavorite = false and enableOldFavorite = true + // after 0.16, enableNewFavorite = true and enableOldFavorite = false + // for debug purpose, we can enable both }); export type RuntimeConfig = z.infer; diff --git a/packages/common/infra/src/index.ts b/packages/common/infra/src/index.ts index 6124ef192b..77c4fb4739 100644 --- a/packages/common/infra/src/index.ts +++ b/packages/common/infra/src/index.ts @@ -15,7 +15,7 @@ export * from './sync'; export * from './utils'; import type { Framework } from './framework'; -import { configureDBModule } from './modules/db'; +import { configureWorkspaceDBModule } from './modules/db'; import { configureDocModule } from './modules/doc'; import { configureGlobalContextModule } from './modules/global-context'; import { configureLifecycleModule } from './modules/lifecycle'; @@ -31,7 +31,7 @@ import { export function configureInfraModules(framework: Framework) { configureWorkspaceModule(framework); configureDocModule(framework); - configureDBModule(framework); + configureWorkspaceDBModule(framework); configureGlobalStorageModule(framework); configureGlobalContextModule(framework); configureLifecycleModule(framework); diff --git a/packages/common/infra/src/modules/db/index.ts b/packages/common/infra/src/modules/db/index.ts index d36b09b0ea..7d9399a671 100644 --- a/packages/common/infra/src/modules/db/index.ts +++ b/packages/common/infra/src/modules/db/index.ts @@ -1,10 +1,12 @@ import type { Framework } from '../../framework'; import { WorkspaceScope, WorkspaceService } from '../workspace'; -import { DBService } from './services/db'; +import { WorkspaceDBService } from './services/db'; -export { AFFiNE_DB_SCHEMA } from './schema'; -export { DBService } from './services/db'; +export { AFFiNE_WORKSPACE_DB_SCHEMA } from './schema'; +export { WorkspaceDBService } from './services/db'; -export function configureDBModule(framework: Framework) { - framework.scope(WorkspaceScope).service(DBService, [WorkspaceService]); +export function configureWorkspaceDBModule(framework: Framework) { + framework + .scope(WorkspaceScope) + .service(WorkspaceDBService, [WorkspaceService]); } diff --git a/packages/common/infra/src/modules/db/schema/index.ts b/packages/common/infra/src/modules/db/schema/index.ts index e394f67c75..d56a956d85 100644 --- a/packages/common/infra/src/modules/db/schema/index.ts +++ b/packages/common/infra/src/modules/db/schema/index.ts @@ -1 +1 @@ -export { AFFiNE_DB_SCHEMA } from './schema'; +export { AFFiNE_WORKSPACE_DB_SCHEMA } from './schema'; diff --git a/packages/common/infra/src/modules/db/schema/schema.ts b/packages/common/infra/src/modules/db/schema/schema.ts index 6075ea0afc..ff481d8802 100644 --- a/packages/common/infra/src/modules/db/schema/schema.ts +++ b/packages/common/infra/src/modules/db/schema/schema.ts @@ -2,7 +2,7 @@ import { nanoid } from 'nanoid'; import { type DBSchemaBuilder, f } from '../../../orm'; -export const AFFiNE_DB_SCHEMA = { +export const AFFiNE_WORKSPACE_DB_SCHEMA = { folders: { id: f.string().primaryKey().optional().default(nanoid), parentId: f.string().optional(), @@ -11,4 +11,13 @@ export const AFFiNE_DB_SCHEMA = { index: f.string(), }, } as const satisfies DBSchemaBuilder; -export type AFFiNE_DB_SCHEMA = typeof AFFiNE_DB_SCHEMA; +export type AFFiNE_WORKSPACE_DB_SCHEMA = typeof AFFiNE_WORKSPACE_DB_SCHEMA; + +export const AFFiNE_WORKSPACE_USERDATA_DB_SCHEMA = { + favorite: { + key: f.string().primaryKey(), + index: f.string(), + }, +} as const satisfies DBSchemaBuilder; +export type AFFiNE_WORKSPACE_USERDATA_DB_SCHEMA = + typeof AFFiNE_WORKSPACE_USERDATA_DB_SCHEMA; diff --git a/packages/common/infra/src/modules/db/services/db.ts b/packages/common/infra/src/modules/db/services/db.ts index 5c05bc0a9c..eecdfe336f 100644 --- a/packages/common/infra/src/modules/db/services/db.ts +++ b/packages/common/infra/src/modules/db/services/db.ts @@ -2,17 +2,29 @@ import { Doc as YDoc } from 'yjs'; import { Service } from '../../../framework'; import { createORMClient, type TableMap, YjsDBAdapter } from '../../../orm'; +import { ObjectPool } from '../../../utils'; import type { WorkspaceService } from '../../workspace'; -import { AFFiNE_DB_SCHEMA } from '../schema'; +import { AFFiNE_WORKSPACE_DB_SCHEMA } from '../schema'; +import { AFFiNE_WORKSPACE_USERDATA_DB_SCHEMA } from '../schema/schema'; -export class DBService extends Service { - db: TableMap; +const WorkspaceDBClient = createORMClient(AFFiNE_WORKSPACE_DB_SCHEMA); +const WorkspaceUserdataDBClient = createORMClient( + AFFiNE_WORKSPACE_USERDATA_DB_SCHEMA +); +type WorkspaceUserdataDBClient = InstanceType; + +export class WorkspaceDBService extends Service { + db: TableMap; + userdataDBPool = new ObjectPool({ + onDangling() { + return false; // never release + }, + }); constructor(private readonly workspaceService: WorkspaceService) { super(); - const Client = createORMClient(AFFiNE_DB_SCHEMA); - this.db = new Client( - new YjsDBAdapter(AFFiNE_DB_SCHEMA, { + this.db = new WorkspaceDBClient( + new YjsDBAdapter(AFFiNE_WORKSPACE_DB_SCHEMA, { getDoc: guid => { const ydoc = new YDoc({ // guid format: db${workspaceId}${guid} @@ -26,7 +38,33 @@ export class DBService extends Service { ); } + // eslint-disable-next-line @typescript-eslint/ban-types + userdataDB(userId: (string & {}) | '__local__') { + // __local__ for local workspace + const userdataDb = this.userdataDBPool.get(userId); + if (userdataDb) { + return userdataDb.obj; + } + + const newDB = new WorkspaceUserdataDBClient( + new YjsDBAdapter(AFFiNE_WORKSPACE_USERDATA_DB_SCHEMA, { + getDoc: guid => { + const ydoc = new YDoc({ + // guid format: userdata${userId}${workspaceId}${guid} + guid: `userdata$${userId}$${this.workspaceService.workspace.id}$${guid}`, + }); + this.workspaceService.workspace.engine.doc.addDoc(ydoc, false); + this.workspaceService.workspace.engine.doc.setPriority(ydoc.guid, 50); + return ydoc; + }, + }) + ); + + this.userdataDBPool.put(userId, newDB); + return newDB; + } + static isDBDocId(docId: string) { - return docId.startsWith('db$'); + return docId.startsWith('db$') || docId.startsWith('userdata$'); } } diff --git a/packages/common/infra/src/orm/index.ts b/packages/common/infra/src/orm/index.ts index 4b0e041376..01143d4466 100644 --- a/packages/common/infra/src/orm/index.ts +++ b/packages/common/infra/src/orm/index.ts @@ -1 +1,2 @@ -export * from './core'; +export type { DBSchemaBuilder, FieldSchemaBuilder, TableMap } from './core'; +export { createORMClient, f, YjsDBAdapter } from './core'; diff --git a/packages/frontend/core/src/components/app-sidebar/category-divider/index.css.ts b/packages/frontend/core/src/components/app-sidebar/category-divider/index.css.ts index accfeaf3e8..1894851362 100644 --- a/packages/frontend/core/src/components/app-sidebar/category-divider/index.css.ts +++ b/packages/frontend/core/src/components/app-sidebar/category-divider/index.css.ts @@ -1,5 +1,6 @@ import { cssVar } from '@toeverything/theme'; import { style } from '@vanilla-extract/css'; + export const root = style({ fontSize: cssVar('fontXs'), minHeight: '16px', @@ -10,6 +11,7 @@ export const root = style({ justifyContent: 'space-between', marginBottom: '4px', padding: '0 8px', + gap: '8px', selectors: { '&:not(:first-of-type)': { marginTop: '16px', @@ -18,4 +20,5 @@ export const root = style({ }); export const label = style({ color: cssVar('black30'), + flex: '1', }); diff --git a/packages/frontend/core/src/components/blocksuite/block-suite-header/favorite/index.tsx b/packages/frontend/core/src/components/blocksuite/block-suite-header/favorite/index.tsx index 7f9e72b89a..748406fc17 100644 --- a/packages/frontend/core/src/components/blocksuite/block-suite-header/favorite/index.tsx +++ b/packages/frontend/core/src/components/blocksuite/block-suite-header/favorite/index.tsx @@ -1,5 +1,5 @@ import { FavoriteTag } from '@affine/core/components/page-list'; -import { FavoriteItemsAdapter } from '@affine/core/modules/properties'; +import { CompatibleFavoriteItemsAdapter } from '@affine/core/modules/properties'; import { toast } from '@affine/core/utils'; import { useI18n } from '@affine/i18n'; import { useLiveData, useService } from '@toeverything/infra'; @@ -11,7 +11,7 @@ export interface FavoriteButtonProps { export const useFavorite = (pageId: string) => { const t = useI18n(); - const favAdapter = useService(FavoriteItemsAdapter); + const favAdapter = useService(CompatibleFavoriteItemsAdapter); const favorite = useLiveData(favAdapter.isFavorite$(pageId, 'doc')); diff --git a/packages/frontend/core/src/components/page-list/group-definitions.tsx b/packages/frontend/core/src/components/page-list/group-definitions.tsx index 520cb12af8..6135972fa0 100644 --- a/packages/frontend/core/src/components/page-list/group-definitions.tsx +++ b/packages/frontend/core/src/components/page-list/group-definitions.tsx @@ -1,4 +1,4 @@ -import { FavoriteItemsAdapter } from '@affine/core/modules/properties'; +import { CompatibleFavoriteItemsAdapter } from '@affine/core/modules/properties'; import type { Tag } from '@affine/core/modules/tag'; import { TagService } from '@affine/core/modules/tag'; import { useI18n } from '@affine/i18n'; @@ -131,9 +131,9 @@ export const useTagGroupDefinitions = (): ItemGroupDefinition[] => { const sortedTagsLiveData$ = useMemo( () => LiveData.computed(get => - get(tagList.tags$).sort((a, b) => - get(a.value$).localeCompare(get(b.value$)) - ) + get(tagList.tags$) + .slice() + .sort((a, b) => get(a.value$).localeCompare(get(b.value$))) ), [tagList.tags$] ); @@ -174,7 +174,7 @@ export const useFavoriteGroupDefinitions = < T extends ListItem, >(): ItemGroupDefinition[] => { const t = useI18n(); - const favAdapter = useService(FavoriteItemsAdapter); + const favAdapter = useService(CompatibleFavoriteItemsAdapter); const favourites = useLiveData(favAdapter.favorites$); return useMemo( () => [ diff --git a/packages/frontend/core/src/components/page-list/list.css.ts b/packages/frontend/core/src/components/page-list/list.css.ts index 688a9a8adf..3bfbfd4569 100644 --- a/packages/frontend/core/src/components/page-list/list.css.ts +++ b/packages/frontend/core/src/components/page-list/list.css.ts @@ -3,6 +3,7 @@ import { createContainer, style } from '@vanilla-extract/css'; import { root as collectionItemRoot } from './collections/collection-list-item.css'; import { root as pageItemRoot } from './docs/page-list-item.css'; +import { root as tagItemRoot } from './tags/tag-list-item.css'; export const listRootContainer = createContainer('list-root-container'); export const pageListScrollContainer = style({ width: '100%', @@ -49,7 +50,7 @@ export const favoriteCell = style({ flexShrink: 0, opacity: 0, selectors: { - [`&[data-favorite], ${pageItemRoot}:hover &, ${collectionItemRoot}:hover &`]: + [`&[data-favorite], ${pageItemRoot}:hover &, ${collectionItemRoot}:hover &, ${tagItemRoot}:hover &`]: { opacity: 1, }, diff --git a/packages/frontend/core/src/components/page-list/operation-cell.tsx b/packages/frontend/core/src/components/page-list/operation-cell.tsx index ac937897ef..eb1185f081 100644 --- a/packages/frontend/core/src/components/page-list/operation-cell.tsx +++ b/packages/frontend/core/src/components/page-list/operation-cell.tsx @@ -10,7 +10,8 @@ import { import { useAppSettingHelper } from '@affine/core/hooks/affine/use-app-setting-helper'; import { useBlockSuiteMetaHelper } from '@affine/core/hooks/affine/use-block-suite-meta-helper'; import { useTrashModalHelper } from '@affine/core/hooks/affine/use-trash-modal-helper'; -import { FavoriteItemsAdapter } from '@affine/core/modules/properties'; +import { FavoriteService } from '@affine/core/modules/favorite'; +import { CompatibleFavoriteItemsAdapter } from '@affine/core/modules/properties'; import { WorkbenchService } from '@affine/core/modules/workbench'; import { mixpanel } from '@affine/core/utils'; import type { Collection, DeleteCollectionInfo } from '@affine/env/filter'; @@ -32,7 +33,12 @@ import { SplitViewIcon, } from '@blocksuite/icons/rc'; import type { DocMeta } from '@blocksuite/store'; -import { useLiveData, useService, WorkspaceService } from '@toeverything/infra'; +import { + useLiveData, + useService, + useServices, + WorkspaceService, +} from '@toeverything/infra'; import { useCallback, useState } from 'react'; import { Link } from 'react-router-dom'; @@ -63,7 +69,7 @@ export const PageOperationCell = ({ const { appSettings } = useAppSettingHelper(); const { setTrashModal } = useTrashModalHelper(currentWorkspace.docCollection); const [openDisableShared, setOpenDisableShared] = useState(false); - const favAdapter = useService(FavoriteItemsAdapter); + const favAdapter = useService(CompatibleFavoriteItemsAdapter); const favourite = useLiveData(favAdapter.isFavorite$(page.id, 'doc')); const workbench = useService(WorkbenchService).workbench; const { duplicate } = useBlockSuiteMetaHelper(currentWorkspace.docCollection); @@ -215,14 +221,16 @@ export const PageOperationCell = ({ ); return ( <> - - - + {runtimeConfig.enableNewFavorite && ( + + + + )} { const t = useI18n(); - const favAdapter = useService(FavoriteItemsAdapter); + const favAdapter = useService(CompatibleFavoriteItemsAdapter); const docCollection = useService(WorkspaceService).workspace.docCollection; const { createPage } = usePageHelper(docCollection); const { openConfirmModal } = useConfirmModal(); @@ -482,12 +490,31 @@ export const TagOperationCell = ({ }: TagOperationCellProps) => { const t = useI18n(); const [open, setOpen] = useState(false); + const { favoriteService } = useServices({ + FavoriteService, + }); + const favourite = useLiveData( + favoriteService.favoriteList.isFavorite$('tag', tag.id) + ); const handleDelete = useCallback(() => { onTagDelete([tag.id]); }, [onTagDelete, tag.id]); + + const onToggleFavoriteCollection = useCallback(() => { + favoriteService.favoriteList.toggle('tag', tag.id); + }, [favoriteService, tag.id]); return ( <> + + + +
diff --git a/packages/frontend/core/src/components/page-list/use-filtered-page-metas.tsx b/packages/frontend/core/src/components/page-list/use-filtered-page-metas.tsx index d1665950bf..5274f4098a 100644 --- a/packages/frontend/core/src/components/page-list/use-filtered-page-metas.tsx +++ b/packages/frontend/core/src/components/page-list/use-filtered-page-metas.tsx @@ -1,4 +1,4 @@ -import { FavoriteItemsAdapter } from '@affine/core/modules/properties'; +import { CompatibleFavoriteItemsAdapter } from '@affine/core/modules/properties'; import { ShareDocsService } from '@affine/core/modules/share-doc'; import type { Collection, Filter } from '@affine/env/filter'; import { PublicPageMode } from '@affine/graphql'; @@ -36,7 +36,7 @@ export const useFilteredPageMetas = ( shareDocsService.shareDocs?.revalidate(); }, [shareDocsService]); - const favAdapter = useService(FavoriteItemsAdapter); + const favAdapter = useService(CompatibleFavoriteItemsAdapter); const favoriteItems = useLiveData(favAdapter.favorites$); const filteredPageMetas = useMemo( diff --git a/packages/frontend/core/src/components/page-list/view/collection-operations.tsx b/packages/frontend/core/src/components/page-list/view/collection-operations.tsx index 5a6707ab73..5e844ffca7 100644 --- a/packages/frontend/core/src/components/page-list/view/collection-operations.tsx +++ b/packages/frontend/core/src/components/page-list/view/collection-operations.tsx @@ -2,7 +2,7 @@ import type { MenuItemProps } from '@affine/component'; import { Menu, MenuIcon, MenuItem } from '@affine/component'; import { useAppSettingHelper } from '@affine/core/hooks/affine/use-app-setting-helper'; import { useDeleteCollectionInfo } from '@affine/core/hooks/affine/use-delete-collection-info'; -import { FavoriteItemsAdapter } from '@affine/core/modules/properties'; +import { CompatibleFavoriteItemsAdapter } from '@affine/core/modules/properties'; import { WorkbenchService } from '@affine/core/modules/workbench'; import type { Collection } from '@affine/env/filter'; import { useI18n } from '@affine/i18n'; @@ -79,7 +79,7 @@ export const CollectionOperations = ({ workbench.openCollection(collection.id, { at: 'tail' }); }, [collection.id, workbench]); - const favAdapter = useService(FavoriteItemsAdapter); + const favAdapter = useService(CompatibleFavoriteItemsAdapter); const onToggleFavoritePage = useCallback(() => { favAdapter.toggle(collection.id, 'collection'); diff --git a/packages/frontend/core/src/components/page-list/view/edit-collection/rules-mode.tsx b/packages/frontend/core/src/components/page-list/view/edit-collection/rules-mode.tsx index 1803c1305e..3b0198f828 100644 --- a/packages/frontend/core/src/components/page-list/view/edit-collection/rules-mode.tsx +++ b/packages/frontend/core/src/components/page-list/view/edit-collection/rules-mode.tsx @@ -1,5 +1,5 @@ import { Tooltip } from '@affine/component'; -import { FavoriteItemsAdapter } from '@affine/core/modules/properties'; +import { CompatibleFavoriteItemsAdapter } from '@affine/core/modules/properties'; import type { Collection } from '@affine/env/filter'; import { Trans, useI18n } from '@affine/i18n'; import { @@ -42,7 +42,7 @@ export const RulesMode = ({ const [showPreview, setShowPreview] = useState(true); const allowListPages: DocMeta[] = []; const rulesPages: DocMeta[] = []; - const favAdapter = useService(FavoriteItemsAdapter); + const favAdapter = useService(CompatibleFavoriteItemsAdapter); const favorites = useLiveData(favAdapter.favorites$); allPageListConfig.allPages.forEach(meta => { if (meta.trash) { diff --git a/packages/frontend/core/src/components/page-list/view/edit-collection/select-page.tsx b/packages/frontend/core/src/components/page-list/view/edit-collection/select-page.tsx index 209bf2a793..62a9d9a68c 100644 --- a/packages/frontend/core/src/components/page-list/view/edit-collection/select-page.tsx +++ b/packages/frontend/core/src/components/page-list/view/edit-collection/select-page.tsx @@ -1,6 +1,6 @@ import { Menu, toast } from '@affine/component'; import { useBlockSuiteDocMeta } from '@affine/core/hooks/use-block-suite-page-meta'; -import { FavoriteItemsAdapter } from '@affine/core/modules/properties'; +import { CompatibleFavoriteItemsAdapter } from '@affine/core/modules/properties'; import { ShareDocsService } from '@affine/core/modules/share-doc'; import { PublicPageMode } from '@affine/graphql'; import { Trans, useI18n } from '@affine/i18n'; @@ -60,20 +60,20 @@ export const SelectPage = ({ }, [onChange]); const { workspaceService, - favoriteItemsAdapter, + compatibleFavoriteItemsAdapter, shareDocsService, docsService, } = useServices({ DocsService, ShareDocsService, WorkspaceService, - FavoriteItemsAdapter, + CompatibleFavoriteItemsAdapter, }); const shareDocs = useLiveData(shareDocsService.shareDocs?.list$); const workspace = workspaceService.workspace; const docCollection = workspace.docCollection; const pageMetas = useBlockSuiteDocMeta(docCollection); - const favourites = useLiveData(favoriteItemsAdapter.favorites$); + const favourites = useLiveData(compatibleFavoriteItemsAdapter.favorites$); useEffect(() => { shareDocsService.shareDocs?.revalidate(); @@ -108,14 +108,14 @@ export const SelectPage = ({ const onToggleFavoritePage = useCallback( (page: DocMeta) => { const status = isFavorite(page); - favoriteItemsAdapter.toggle(page.id, 'doc'); + compatibleFavoriteItemsAdapter.toggle(page.id, 'doc'); toast( status ? t['com.affine.toastMessage.removedFavorites']() : t['com.affine.toastMessage.addedFavorites']() ); }, - [favoriteItemsAdapter, isFavorite, t] + [compatibleFavoriteItemsAdapter, isFavorite, t] ); const pageHeaderColsDef = usePageHeaderColsDef(); diff --git a/packages/frontend/core/src/components/root-app-sidebar/index.tsx b/packages/frontend/core/src/components/root-app-sidebar/index.tsx index cefff5a7b8..fa0b501bc7 100644 --- a/packages/frontend/core/src/components/root-app-sidebar/index.tsx +++ b/packages/frontend/core/src/components/root-app-sidebar/index.tsx @@ -2,6 +2,8 @@ import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks'; import { ExplorerCollections, ExplorerFavorites, + ExplorerMigrationFavorites, + ExplorerOldFavorites, ExplorerOrganize, } from '@affine/core/modules/explorer'; import { ExplorerTags } from '@affine/core/modules/explorer/views/sections/tags'; @@ -161,8 +163,10 @@ export const RootAppSidebar = memo( + {runtimeConfig.enableNewFavorite && } {runtimeConfig.enableOrganize && } - + {runtimeConfig.enableNewFavorite && } + {runtimeConfig.enableOldFavorite && } diff --git a/packages/frontend/core/src/hooks/affine/use-all-page-list-config.tsx b/packages/frontend/core/src/hooks/affine/use-all-page-list-config.tsx index ca648cfd5d..27e4961008 100644 --- a/packages/frontend/core/src/hooks/affine/use-all-page-list-config.tsx +++ b/packages/frontend/core/src/hooks/affine/use-all-page-list-config.tsx @@ -2,7 +2,7 @@ import { toast } from '@affine/component'; import type { AllPageListConfig } from '@affine/core/components/page-list'; import { FavoriteTag } from '@affine/core/components/page-list'; import { useBlockSuiteDocMeta } from '@affine/core/hooks/use-block-suite-page-meta'; -import { FavoriteItemsAdapter } from '@affine/core/modules/properties'; +import { CompatibleFavoriteItemsAdapter } from '@affine/core/modules/properties'; import { ShareDocsService } from '@affine/core/modules/share-doc'; import { PublicPageMode } from '@affine/graphql'; import { useI18n } from '@affine/i18n'; @@ -32,7 +32,7 @@ export const useAllPageListConfig = () => { () => Object.fromEntries(pageMetas.map(page => [page.id, page])), [pageMetas] ); - const favAdapter = useService(FavoriteItemsAdapter); + const favAdapter = useService(CompatibleFavoriteItemsAdapter); const t = useI18n(); const favoriteItems = useLiveData(favAdapter.favorites$); diff --git a/packages/frontend/core/src/hooks/affine/use-register-blocksuite-editor-commands.tsx b/packages/frontend/core/src/hooks/affine/use-register-blocksuite-editor-commands.tsx index 7774a11b73..da2ca067c3 100644 --- a/packages/frontend/core/src/hooks/affine/use-register-blocksuite-editor-commands.tsx +++ b/packages/frontend/core/src/hooks/affine/use-register-blocksuite-editor-commands.tsx @@ -4,7 +4,7 @@ import { PreconditionStrategy, registerAffineCommand, } from '@affine/core/commands'; -import { FavoriteItemsAdapter } from '@affine/core/modules/properties'; +import { CompatibleFavoriteItemsAdapter } from '@affine/core/modules/properties'; import { TelemetryWorkspaceContextService } from '@affine/core/modules/telemetry/services/telemetry'; import { mixpanel } from '@affine/core/utils'; import { WorkspaceFlavour } from '@affine/env/workspace'; @@ -32,7 +32,7 @@ export function useRegisterBlocksuiteEditorCommands() { const workspace = useService(WorkspaceService).workspace; const docCollection = workspace.docCollection; - const favAdapter = useService(FavoriteItemsAdapter); + const favAdapter = useService(CompatibleFavoriteItemsAdapter); const favorite = useLiveData(favAdapter.isFavorite$(docId, 'doc')); const trash = useLiveData(doc.trash$); diff --git a/packages/frontend/core/src/modules/docs-search/entities/docs-indexer.ts b/packages/frontend/core/src/modules/docs-search/entities/docs-indexer.ts index c15ef38c26..5f88fae1c0 100644 --- a/packages/frontend/core/src/modules/docs-search/entities/docs-indexer.ts +++ b/packages/frontend/core/src/modules/docs-search/entities/docs-indexer.ts @@ -1,12 +1,12 @@ import { DebugLogger } from '@affine/debug'; import type { Job, JobQueue, WorkspaceService } from '@toeverything/infra'; import { - DBService, Entity, IndexedDBIndexStorage, IndexedDBJobQueue, JobRunner, LiveData, + WorkspaceDBService, } from '@toeverything/infra'; import { map } from 'rxjs'; @@ -69,7 +69,7 @@ export class DocsIndexer extends Entity { setupListener() { this.workspaceEngine.doc.storage.eventBus.on(event => { - if (DBService.isDBDocId(event.docId)) { + if (WorkspaceDBService.isDBDocId(event.docId)) { // skip db doc return; } diff --git a/packages/frontend/core/src/modules/explorer/index.ts b/packages/frontend/core/src/modules/explorer/index.ts index 9373ab8cd0..0fc8071e95 100644 --- a/packages/frontend/core/src/modules/explorer/index.ts +++ b/packages/frontend/core/src/modules/explorer/index.ts @@ -1,3 +1,5 @@ export { ExplorerCollections } from './views/sections/collections'; export { ExplorerFavorites } from './views/sections/favorites'; +export { ExplorerMigrationFavorites } from './views/sections/migration-favorites'; +export { ExplorerOldFavorites } from './views/sections/old-favorites'; export { ExplorerOrganize } from './views/sections/organize'; diff --git a/packages/frontend/core/src/modules/explorer/views/nodes/collection/index.tsx b/packages/frontend/core/src/modules/explorer/views/nodes/collection/index.tsx index 6c3b528a13..7f72c3ad94 100644 --- a/packages/frontend/core/src/modules/explorer/views/nodes/collection/index.tsx +++ b/packages/frontend/core/src/modules/explorer/views/nodes/collection/index.tsx @@ -11,7 +11,7 @@ import { useEditCollection, } from '@affine/core/components/page-list'; import { CollectionService } from '@affine/core/modules/collection'; -import { FavoriteItemsAdapter } from '@affine/core/modules/properties'; +import { CompatibleFavoriteItemsAdapter } from '@affine/core/modules/properties'; import { ShareDocsService } from '@affine/core/modules/share-doc'; import type { AffineDNDData } from '@affine/core/types/dnd'; import type { Collection } from '@affine/env/filter'; @@ -225,12 +225,12 @@ const ExplorerCollectionNodeChildren = ({ const t = useI18n(); const { docsService, - favoriteItemsAdapter, + compatibleFavoriteItemsAdapter, shareDocsService, collectionService, } = useServices({ DocsService, - FavoriteItemsAdapter, + CompatibleFavoriteItemsAdapter, ShareDocsService, CollectionService, }); @@ -251,7 +251,7 @@ const ExplorerCollectionNodeChildren = ({ [docsService] ) ); - const favourites = useLiveData(favoriteItemsAdapter.favorites$); + const favourites = useLiveData(compatibleFavoriteItemsAdapter.favorites$); const allowList = useMemo( () => new Set(collection.allowList), [collection.allowList] diff --git a/packages/frontend/core/src/modules/explorer/views/nodes/collection/operations.tsx b/packages/frontend/core/src/modules/explorer/views/nodes/collection/operations.tsx index 2a7da69ce1..2862904dda 100644 --- a/packages/frontend/core/src/modules/explorer/views/nodes/collection/operations.tsx +++ b/packages/frontend/core/src/modules/explorer/views/nodes/collection/operations.tsx @@ -8,7 +8,7 @@ import { import { useAppSettingHelper } from '@affine/core/hooks/affine/use-app-setting-helper'; import { useDeleteCollectionInfo } from '@affine/core/hooks/affine/use-delete-collection-info'; import { CollectionService } from '@affine/core/modules/collection'; -import { FavoriteItemsAdapter } from '@affine/core/modules/properties'; +import { CompatibleFavoriteItemsAdapter } from '@affine/core/modules/properties'; import { WorkbenchService } from '@affine/core/modules/workbench'; import { useI18n } from '@affine/i18n'; import { @@ -35,19 +35,20 @@ export const useExplorerCollectionNodeOperations = ( workbenchService, docsService, collectionService, - favoriteItemsAdapter, + compatibleFavoriteItemsAdapter, } = useServices({ DocsService, WorkbenchService, CollectionService, - FavoriteItemsAdapter, + CompatibleFavoriteItemsAdapter, }); const deleteInfo = useDeleteCollectionInfo(); const favorite = useLiveData( useMemo( - () => favoriteItemsAdapter.isFavorite$(collectionId, 'collection'), - [collectionId, favoriteItemsAdapter] + () => + compatibleFavoriteItemsAdapter.isFavorite$(collectionId, 'collection'), + [collectionId, compatibleFavoriteItemsAdapter] ) ); const { openConfirmModal } = useConfirmModal(); @@ -66,8 +67,8 @@ export const useExplorerCollectionNodeOperations = ( ]); const handleToggleFavoritePage = useCallback(() => { - favoriteItemsAdapter.toggle(collectionId, 'collection'); - }, [favoriteItemsAdapter, collectionId]); + compatibleFavoriteItemsAdapter.toggle(collectionId, 'collection'); + }, [compatibleFavoriteItemsAdapter, collectionId]); const handleAddDocToCollection = useCallback(() => { openConfirmModal({ diff --git a/packages/frontend/core/src/modules/explorer/views/nodes/doc/operations.tsx b/packages/frontend/core/src/modules/explorer/views/nodes/doc/operations.tsx index 2430ebd8be..bec89e178d 100644 --- a/packages/frontend/core/src/modules/explorer/views/nodes/doc/operations.tsx +++ b/packages/frontend/core/src/modules/explorer/views/nodes/doc/operations.tsx @@ -7,7 +7,7 @@ import { } from '@affine/component'; import { useAppSettingHelper } from '@affine/core/hooks/affine/use-app-setting-helper'; import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks'; -import { FavoriteItemsAdapter } from '@affine/core/modules/properties'; +import { CompatibleFavoriteItemsAdapter } from '@affine/core/modules/properties'; import { WorkbenchService } from '@affine/core/modules/workbench'; import { useI18n } from '@affine/i18n'; import { @@ -32,20 +32,20 @@ export const useExplorerDocNodeOperations = ( ): NodeOperation[] => { const t = useI18n(); const { appSettings } = useAppSettingHelper(); - const { workbenchService, docsService, favoriteItemsAdapter } = useServices({ - DocsService, - WorkbenchService, - FavoriteItemsAdapter, - }); + const { workbenchService, docsService, compatibleFavoriteItemsAdapter } = + useServices({ + DocsService, + WorkbenchService, + CompatibleFavoriteItemsAdapter, + }); const { openConfirmModal } = useConfirmModal(); const docRecord = useLiveData(docsService.list.doc$(docId)); const favorite = useLiveData( - useMemo( - () => favoriteItemsAdapter.isFavorite$(docId, 'doc'), - [docId, favoriteItemsAdapter] - ) + useMemo(() => { + return compatibleFavoriteItemsAdapter.isFavorite$(docId, 'doc'); + }, [docId, compatibleFavoriteItemsAdapter]) ); const handleMoveToTrash = useCallback(() => { @@ -84,8 +84,8 @@ export const useExplorerDocNodeOperations = ( }, [docId, options, docsService, workbenchService.workbench]); const handleToggleFavoriteDoc = useCallback(() => { - favoriteItemsAdapter.toggle(docId, 'doc'); - }, [favoriteItemsAdapter, docId]); + compatibleFavoriteItemsAdapter.toggle(docId, 'doc'); + }, [docId, compatibleFavoriteItemsAdapter]); return useMemo( () => [ diff --git a/packages/frontend/core/src/modules/explorer/views/nodes/tag/operations.tsx b/packages/frontend/core/src/modules/explorer/views/nodes/tag/operations.tsx index d7d429cbd0..c255b4ad27 100644 --- a/packages/frontend/core/src/modules/explorer/views/nodes/tag/operations.tsx +++ b/packages/frontend/core/src/modules/explorer/views/nodes/tag/operations.tsx @@ -6,10 +6,17 @@ import { toast, } from '@affine/component'; import { useAppSettingHelper } from '@affine/core/hooks/affine/use-app-setting-helper'; +import { FavoriteService } from '@affine/core/modules/favorite'; import { TagService } from '@affine/core/modules/tag'; import { WorkbenchService } from '@affine/core/modules/workbench'; import { useI18n } from '@affine/i18n'; -import { DeleteIcon, PlusIcon, SplitViewIcon } from '@blocksuite/icons/rc'; +import { + DeleteIcon, + FavoritedIcon, + FavoriteIcon, + PlusIcon, + SplitViewIcon, +} from '@blocksuite/icons/rc'; import { DocsService, useLiveData, useServices } from '@toeverything/infra'; import { useCallback, useMemo } from 'react'; @@ -25,12 +32,17 @@ export const useExplorerTagNodeOperations = ( ): NodeOperation[] => { const t = useI18n(); const { appSettings } = useAppSettingHelper(); - const { docsService, workbenchService, tagService } = useServices({ - WorkbenchService, - TagService, - DocsService, - }); + const { docsService, workbenchService, tagService, favoriteService } = + useServices({ + WorkbenchService, + TagService, + DocsService, + FavoriteService, + }); + const favorite = useLiveData( + favoriteService.favoriteList.favorite$('tag', tagId) + ); const tagRecord = useLiveData(tagService.tagList.tagByTagId$(tagId)); const handleNewDoc = useCallback(() => { @@ -53,6 +65,10 @@ export const useExplorerTagNodeOperations = ( }); }, [tagId, workbenchService]); + const handleToggleFavoriteTag = useCallback(() => { + favoriteService.favoriteList.toggle('tag', tagId); + }, [favoriteService, tagId]); + return useMemo( () => [ { @@ -83,6 +99,33 @@ export const useExplorerTagNodeOperations = ( }, ] : []), + ...(runtimeConfig.enableNewFavorite + ? [ + { + index: 199, + view: ( + + {favorite ? ( + + ) : ( + + )} + + } + onClick={handleToggleFavoriteTag} + > + {favorite + ? t['com.affine.favoritePageOperation.remove']() + : t['com.affine.favoritePageOperation.add']()} + + ), + }, + ] + : []), { index: 9999, view: , @@ -106,9 +149,11 @@ export const useExplorerTagNodeOperations = ( ], [ appSettings.enableMultiView, + favorite, handleMoveToTrash, handleNewDoc, handleOpenInSplitView, + handleToggleFavoriteTag, t, ] ); diff --git a/packages/frontend/core/src/modules/explorer/views/sections/favorites/empty.tsx b/packages/frontend/core/src/modules/explorer/views/sections/favorites/empty.tsx index 9dc9b8caf9..ca3f483d90 100644 --- a/packages/frontend/core/src/modules/explorer/views/sections/favorites/empty.tsx +++ b/packages/frontend/core/src/modules/explorer/views/sections/favorites/empty.tsx @@ -39,10 +39,10 @@ export const RootEmpty = ({
- {t['com.affine.rootAppSidebar.organize.empty']()} + {t['com.affine.rootAppSidebar.favorites.empty']()}
{dropEffect && draggedOverDraggable && ( { - const { favoriteItemsAdapter, docsService, workbenchService } = useServices({ - FavoriteItemsAdapter, + const { favoriteService, docsService, workbenchService } = useServices({ + FavoriteService, DocsService, WorkbenchService, }); - const docs = useLiveData(docsService.list.docs$); - const trashDocs = useLiveData(docsService.list.trashDocs$); - - const favorites = useLiveData( - favoriteItemsAdapter.orderedFavorites$.map(favs => { - return favs.filter(fav => { - if (fav.type === 'doc') { - return ( - docs.some(doc => doc.id === fav.id) && - !trashDocs.some(doc => doc.id === fav.id) - ); - } - return true; - }); - }) - ); + const favorites = useLiveData(favoriteService.favoriteList.sortedList$); const t = useI18n(); const handleDrop = useCallback( (data: DropTargetDropEvent) => { if ( - data.source.data.entity?.type === 'doc' || - data.source.data.entity?.type === 'collection' + data.source.data.entity?.type && + isFavoriteSupportType(data.source.data.entity.type) ) { - favoriteItemsAdapter.set( + favoriteService.favoriteList.add( + data.source.data.entity.type, data.source.data.entity.id, - data.source.data.entity?.type, - true + favoriteService.favoriteList.indexAt('before') ); } }, - [favoriteItemsAdapter] + [favoriteService] ); const handleDropEffect = useCallback(data => { if ( - data.source.data.entity?.type === 'doc' || - data.source.data.entity?.type === 'collection' + data.source.data.entity?.type && + isFavoriteSupportType(data.source.data.entity.type) ) { return 'link'; } @@ -77,23 +68,26 @@ export const ExplorerFavorites = () => { const handleCanDrop = useMemo['canDrop']>( () => data => { - return ( - data.source.data.entity?.type === 'doc' || - data.source.data.entity?.type === 'collection' - ); + return data.source.data.entity?.type + ? isFavoriteSupportType(data.source.data.entity.type) + : false; }, [] ); const handleCreateNewFavoriteDoc = useCallback(() => { const newDoc = docsService.createDoc(); - favoriteItemsAdapter.set(newDoc.id, 'doc', true); + favoriteService.favoriteList.add( + 'doc', + newDoc.id, + favoriteService.favoriteList.indexAt('before') + ); workbenchService.workbench.openDoc(newDoc.id); - }, [docsService, favoriteItemsAdapter, workbenchService]); + }, [docsService, favoriteService, workbenchService]); const handleOnChildrenDrop = useCallback( ( - favorite: { id: string; type: 'doc' | 'collection' }, + favorite: { id: string; type: FavoriteSupportType }, data: DropTargetDropEvent ) => { if ( @@ -101,42 +95,41 @@ export const ExplorerFavorites = () => { data.treeInstruction?.type === 'reorder-below' ) { if ( - data.source.data.from?.at === 'explorer:favorite:items' && - (data.source.data.entity?.type === 'doc' || - data.source.data.entity?.type === 'collection') + data.source.data.from?.at === 'explorer:favorite:list' && + data.source.data.entity?.type && + isFavoriteSupportType(data.source.data.entity.type) ) { // is reordering - favoriteItemsAdapter.sorter.moveTo( - FavoriteItemsAdapter.getFavItemKey( - data.source.data.entity.id, - data.source.data.entity.type - ), - FavoriteItemsAdapter.getFavItemKey(favorite.id, favorite.type), - data.treeInstruction?.type === 'reorder-above' ? 'before' : 'after' + favoriteService.favoriteList.reorder( + data.source.data.entity.type, + data.source.data.entity.id, + favoriteService.favoriteList.indexAt( + data.treeInstruction?.type === 'reorder-above' + ? 'before' + : 'after', + favorite + ) ); } else if ( - data.source.data.entity?.type === 'doc' || - data.source.data.entity?.type === 'collection' + data.source.data.entity?.type && + isFavoriteSupportType(data.source.data.entity.type) ) { - favoriteItemsAdapter.set( + favoriteService.favoriteList.add( + data.source.data.entity.type, data.source.data.entity.id, - data.source.data.entity?.type, - true - ); - favoriteItemsAdapter.sorter.moveTo( - FavoriteItemsAdapter.getFavItemKey( - data.source.data.entity.id, - data.source.data.entity.type - ), - FavoriteItemsAdapter.getFavItemKey(favorite.id, favorite.type), - data.treeInstruction?.type === 'reorder-above' ? 'before' : 'after' + favoriteService.favoriteList.indexAt( + data.treeInstruction?.type === 'reorder-above' + ? 'before' + : 'after', + favorite + ) ); + } else { + return; // not supported } - } else { - return; // not supported } }, - [favoriteItemsAdapter] + [favoriteService] ); const handleChildrenDropEffect = useCallback( @@ -146,14 +139,14 @@ export const ExplorerFavorites = () => { data.treeInstruction?.type === 'reorder-below' ) { if ( - data.source.data.from?.at === 'explorer:favorite:items' && - (data.source.data.entity?.type === 'doc' || - data.source.data.entity?.type === 'collection') + data.source.data.from?.at === 'explorer:favorite:list' && + data.source.data.entity?.type && + isFavoriteSupportType(data.source.data.entity.type) ) { return 'move'; } else if ( - data.source.data.entity?.type === 'doc' || - data.source.data.entity?.type === 'collection' + data.source.data.entity?.type && + isFavoriteSupportType(data.source.data.entity.type) ) { return 'link'; } @@ -167,8 +160,9 @@ export const ExplorerFavorites = () => { DropTargetOptions['canDrop'] >( () => args => - args.source.data.entity?.type === 'doc' || - args.source.data.entity?.type === 'collection', + args.source.data.entity?.type + ? isFavoriteSupportType(args.source.data.entity.type) + : false, [] ); @@ -236,7 +230,7 @@ export const ExplorerFavorites = () => { }; const childLocation = { - at: 'explorer:favorite:items' as const, + at: 'explorer:favorite:list' as const, }; const ExplorerFavoriteNode = ({ favorite, @@ -246,13 +240,13 @@ const ExplorerFavoriteNode = ({ }: { favorite: { id: string; - type: 'collection' | 'doc'; + type: FavoriteSupportType; }; canDrop?: DropTargetOptions['canDrop']; onDrop: ( favorite: { id: string; - type: 'collection' | 'doc'; + type: FavoriteSupportType; }, data: DropTargetDropEvent ) => void; @@ -273,6 +267,24 @@ const ExplorerFavoriteNode = ({ dropEffect={dropEffect} canDrop={canDrop} /> + ) : favorite.type === 'tag' ? ( + + ) : favorite.type === 'folder' ? ( + ) : ( { + 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: ( + , + }} + /> + ), + 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: , + type: 'default', + onClick: () => { + requestAnimationFrame(() => { + handleClickClear(); + }); + }, + }, + }); + }, [handleClickClear, openConfirmModal, t]); + + if (favorites.length === 0) { + return null; + } + + return ( +
+ + + + + + + + + + {favorites.map((favorite, i) => ( + + ))} + +
+ ); +}; + +const childLocation = { + at: 'explorer:migration-data:list' as const, +}; +const ExplorerMigrationFavoriteNode = ({ + favorite, +}: { + favorite: { + id: string; + type: 'collection' | 'doc'; + }; +}) => { + return favorite.type === 'doc' ? ( + + ) : ( + + ); +}; diff --git a/packages/frontend/core/src/modules/explorer/views/sections/migration-favorites/styles.css.ts b/packages/frontend/core/src/modules/explorer/views/sections/migration-favorites/styles.css.ts new file mode 100644 index 0000000000..3da2d9e8e7 --- /dev/null +++ b/packages/frontend/core/src/modules/explorer/views/sections/migration-favorites/styles.css.ts @@ -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', +}); diff --git a/packages/frontend/core/src/modules/explorer/views/sections/old-favorites/empty.css.ts b/packages/frontend/core/src/modules/explorer/views/sections/old-favorites/empty.css.ts new file mode 100644 index 0000000000..c9fb078fe3 --- /dev/null +++ b/packages/frontend/core/src/modules/explorer/views/sections/old-favorites/empty.css.ts @@ -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'), +}); diff --git a/packages/frontend/core/src/modules/explorer/views/sections/old-favorites/empty.tsx b/packages/frontend/core/src/modules/explorer/views/sections/old-favorites/empty.tsx new file mode 100644 index 0000000000..6892f529da --- /dev/null +++ b/packages/frontend/core/src/modules/explorer/views/sections/old-favorites/empty.tsx @@ -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) => void; + canDrop?: DropTargetOptions['canDrop']; + dropEffect?: ExplorerTreeNodeDropEffect; +}) => { + const t = useI18n(); + + const { dropTargetRef, draggedOverDraggable, draggedOverPosition } = + useDropTarget( + () => ({ + data: { + at: 'explorer:old-favorite:root', + }, + onDrop: onDrop, + canDrop: canDrop, + }), + [onDrop, canDrop] + ); + + return ( +
+
+ +
+
+ {t['com.affine.rootAppSidebar.favorites.empty']()} +
+ {dropEffect && draggedOverDraggable && ( + + )} +
+ ); +}; diff --git a/packages/frontend/core/src/modules/explorer/views/sections/old-favorites/index.tsx b/packages/frontend/core/src/modules/explorer/views/sections/old-favorites/index.tsx new file mode 100644 index 0000000000..4f8d60b96b --- /dev/null +++ b/packages/frontend/core/src/modules/explorer/views/sections/old-favorites/index.tsx @@ -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) => { + 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(data => { + if ( + data.source.data.entity?.type === 'doc' || + data.source.data.entity?.type === 'collection' + ) { + return 'link'; + } + return; + }, []); + + const handleCanDrop = useMemo['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 + ) => { + 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( + 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['canDrop'] + >( + () => args => + args.source.data.entity?.type === 'doc' || + args.source.data.entity?.type === 'collection', + [] + ); + + return ( +
+ + + + + + + } + > + {favorites.map((favorite, i) => ( + + ))} + +
+ ); +}; + +const childLocation = { + at: 'explorer:old-favorite:list' as const, +}; +const ExplorerFavoriteNode = ({ + favorite, + onDrop, + canDrop, + dropEffect, +}: { + favorite: { + id: string; + type: 'collection' | 'doc'; + }; + canDrop?: DropTargetOptions['canDrop']; + onDrop: ( + favorite: { + id: string; + type: 'collection' | 'doc'; + }, + data: DropTargetDropEvent + ) => void; + dropEffect: ExplorerTreeNodeDropEffect; +}) => { + const handleOnChildrenDrop = useCallback( + (data: DropTargetDropEvent) => { + onDrop(favorite, data); + }, + [favorite, onDrop] + ); + return favorite.type === 'doc' ? ( + + ) : ( + + ); +}; diff --git a/packages/frontend/core/src/modules/explorer/views/sections/old-favorites/styles.css.ts b/packages/frontend/core/src/modules/explorer/views/sections/old-favorites/styles.css.ts new file mode 100644 index 0000000000..574b2653a0 --- /dev/null +++ b/packages/frontend/core/src/modules/explorer/views/sections/old-favorites/styles.css.ts @@ -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', + }, + }, +}); diff --git a/packages/frontend/core/src/modules/favorite/constant.ts b/packages/frontend/core/src/modules/favorite/constant.ts new file mode 100644 index 0000000000..bc88a915e1 --- /dev/null +++ b/packages/frontend/core/src/modules/favorite/constant.ts @@ -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); diff --git a/packages/frontend/core/src/modules/favorite/entities/favorite-list.ts b/packages/frontend/core/src/modules/favorite/entities/favorite-list.ts new file mode 100644 index 0000000000..306239eec7 --- /dev/null +++ b/packages/frontend/core/src/modules/favorite/entities/favorite-list.ts @@ -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 + ); + } + } + } +} diff --git a/packages/frontend/core/src/modules/favorite/index.ts b/packages/frontend/core/src/modules/favorite/index.ts new file mode 100644 index 0000000000..983860ae26 --- /dev/null +++ b/packages/frontend/core/src/modules/favorite/index.ts @@ -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]); +} diff --git a/packages/frontend/core/src/modules/favorite/services/favorite.ts b/packages/frontend/core/src/modules/favorite/services/favorite.ts new file mode 100644 index 0000000000..d2c4fa9b73 --- /dev/null +++ b/packages/frontend/core/src/modules/favorite/services/favorite.ts @@ -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); +} diff --git a/packages/frontend/core/src/modules/favorite/stores/favorite.ts b/packages/frontend/core/src/modules/favorite/stores/favorite.ts new file mode 100644 index 0000000000..2177589db8 --- /dev/null +++ b/packages/frontend/core/src/modules/favorite/stores/favorite.ts @@ -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( + 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}`; + } +} diff --git a/packages/frontend/core/src/modules/index.ts b/packages/frontend/core/src/modules/index.ts index 5a56c2464f..66b4936eb5 100644 --- a/packages/frontend/core/src/modules/index.ts +++ b/packages/frontend/core/src/modules/index.ts @@ -5,6 +5,7 @@ import { configureCloudModule } from './cloud'; import { configureCollectionModule } from './collection'; import { configureDocLinksModule } from './doc-link'; import { configureDocsSearchModule } from './docs-search'; +import { configureFavoriteModule } from './favorite'; import { configureFindInPageModule } from './find-in-page'; import { configureNavigationModule } from './navigation'; import { configureOrganizeModule } from './organize'; @@ -35,4 +36,5 @@ export function configureCommonModules(framework: Framework) { configureDocsSearchModule(framework); configureDocLinksModule(framework); configureOrganizeModule(framework); + configureFavoriteModule(framework); } diff --git a/packages/frontend/core/src/modules/organize/index.ts b/packages/frontend/core/src/modules/organize/index.ts index ebab9aa565..89388b1f17 100644 --- a/packages/frontend/core/src/modules/organize/index.ts +++ b/packages/frontend/core/src/modules/organize/index.ts @@ -1,4 +1,8 @@ -import { DBService, type Framework, WorkspaceScope } from '@toeverything/infra'; +import { + type Framework, + WorkspaceDBService, + WorkspaceScope, +} from '@toeverything/infra'; import { FolderNode } from './entities/folder-node'; import { FolderTree } from './entities/folder-tree'; @@ -14,5 +18,5 @@ export function configureOrganizeModule(framework: Framework) { .service(OrganizeService) .entity(FolderTree, [FolderStore]) .entity(FolderNode, [FolderStore]) - .store(FolderStore, [DBService]); + .store(FolderStore, [WorkspaceDBService]); } diff --git a/packages/frontend/core/src/modules/organize/stores/folder.ts b/packages/frontend/core/src/modules/organize/stores/folder.ts index be4d927cd0..6ff08fae93 100644 --- a/packages/frontend/core/src/modules/organize/stores/folder.ts +++ b/packages/frontend/core/src/modules/organize/stores/folder.ts @@ -1,8 +1,8 @@ -import type { DBService } from '@toeverything/infra'; +import type { WorkspaceDBService } from '@toeverything/infra'; import { Store } from '@toeverything/infra'; export class FolderStore extends Store { - constructor(private readonly dbService: DBService) { + constructor(private readonly dbService: WorkspaceDBService) { super(); } diff --git a/packages/frontend/core/src/modules/properties/index.ts b/packages/frontend/core/src/modules/properties/index.ts index ba052ccd86..b0afd58211 100644 --- a/packages/frontend/core/src/modules/properties/index.ts +++ b/packages/frontend/core/src/modules/properties/index.ts @@ -1,4 +1,5 @@ export { + CompatibleFavoriteItemsAdapter, FavoriteItemsAdapter, WorkspacePropertiesAdapter, } from './services/adapter'; @@ -10,7 +11,9 @@ import { WorkspaceService, } from '@toeverything/infra'; +import { FavoriteService } from '../favorite'; import { + CompatibleFavoriteItemsAdapter, FavoriteItemsAdapter, WorkspacePropertiesAdapter, } from './services/adapter'; @@ -21,5 +24,9 @@ export function configureWorkspacePropertiesModule(framework: Framework) { .scope(WorkspaceScope) .service(WorkspaceLegacyProperties, [WorkspaceService]) .service(WorkspacePropertiesAdapter, [WorkspaceService]) - .service(FavoriteItemsAdapter, [WorkspacePropertiesAdapter]); + .service(FavoriteItemsAdapter, [WorkspacePropertiesAdapter]) + .service(CompatibleFavoriteItemsAdapter, [ + FavoriteItemsAdapter, + FavoriteService, + ]); } diff --git a/packages/frontend/core/src/modules/properties/services/adapter.ts b/packages/frontend/core/src/modules/properties/services/adapter.ts index b241b666d6..3674800057 100644 --- a/packages/frontend/core/src/modules/properties/services/adapter.ts +++ b/packages/frontend/core/src/modules/properties/services/adapter.ts @@ -7,6 +7,7 @@ import { LiveData, Service } from '@toeverything/infra'; import { defaultsDeep } from 'lodash-es'; import { Observable } from 'rxjs'; +import type { FavoriteService } from '../../favorite'; import { PagePropertyType, PageSystemPropertyId, @@ -130,6 +131,9 @@ export class WorkspacePropertiesAdapter extends Service { return this.proxy.schema; } + /** + * @deprecated + */ get favorites() { return this.proxy.favorites; } @@ -154,8 +158,18 @@ export class WorkspacePropertiesAdapter extends Service { const pageProperties = this.pageProperties?.[id]; pageProperties!.system[PageSystemPropertyId.Journal].value = date; } + + /** + * After the user completes the migration, call this function to clear the favorite data + */ + cleanupFavorites() { + this.proxy.favorites = {}; + } } +/** + * @deprecated use CompatibleFavoriteItemsAdapter + */ export class FavoriteItemsAdapter extends Service { constructor(private readonly adapter: WorkspacePropertiesAdapter) { super(); @@ -285,4 +299,70 @@ export class FavoriteItemsAdapter extends Service { existing.value = false; } } + + clearAll() { + this.adapter.cleanupFavorites(); + } +} + +/** + * A service written for compatibility,with the same API as FavoriteItemsAdapter. + * When `runtimeConfig.enableNewFavorite` is false, it operates FavoriteItemsAdapter, + * and when it is true, it operates FavoriteService. + */ +export class CompatibleFavoriteItemsAdapter extends Service { + constructor( + private readonly favoriteItemsAdapter: FavoriteItemsAdapter, + private readonly favoriteService: FavoriteService + ) { + super(); + } + + toggle(id: string, type: WorkspaceFavoriteItem['type']) { + if (runtimeConfig.enableNewFavorite) { + this.favoriteService.favoriteList.toggle(type, id); + } else { + this.favoriteItemsAdapter.toggle(id, type); + } + } + + isFavorite$(id: string, type: WorkspaceFavoriteItem['type']) { + if (runtimeConfig.enableNewFavorite) { + return this.favoriteService.favoriteList.isFavorite$(type, id); + } else { + return this.favoriteItemsAdapter.isFavorite$(id, type); + } + } + + isFavorite(id: string, type: WorkspaceFavoriteItem['type']) { + if (runtimeConfig.enableNewFavorite) { + return this.favoriteService.favoriteList.isFavorite$(type, id).value; + } else { + return this.favoriteItemsAdapter.isFavorite(id, type); + } + } + + get favorites$() { + if (runtimeConfig.enableNewFavorite) { + return this.favoriteService.favoriteList.list$.map< + { + id: string; + order: string; + type: 'doc' | 'collection'; + value: boolean; + }[] + >(v => + v + .filter(i => i.type === 'doc' || i.type === 'collection') // only support doc and collection + .map(i => ({ + id: i.id, + order: '', + type: i.type as 'doc' | 'collection', + value: true, + })) + ); + } else { + return this.favoriteItemsAdapter.favorites$; + } + } } diff --git a/packages/frontend/core/src/types/dnd.ts b/packages/frontend/core/src/types/dnd.ts index 878127d189..7da3e9eb4c 100644 --- a/packages/frontend/core/src/types/dnd.ts +++ b/packages/frontend/core/src/types/dnd.ts @@ -36,7 +36,13 @@ export interface AffineDNDData extends DNDData { collectionId: string; } | { - at: 'explorer:favorite:items'; + at: 'explorer:favorite:list'; + } + | { + at: 'explorer:old-favorite:list'; + } + | { + at: 'explorer:migration-data:list'; } | { at: 'all-docs:list'; @@ -64,6 +70,12 @@ export interface AffineDNDData extends DNDData { | { at: 'explorer:organize:folder'; } + | { + at: 'explorer:favorite:root'; + } + | { + at: 'explorer:old-favorite:root'; + } | { at: 'explorer:doc'; } diff --git a/packages/frontend/i18n/src/resources/en.json b/packages/frontend/i18n/src/resources/en.json index 64d43b1e41..840bd39ea5 100644 --- a/packages/frontend/i18n/src/resources/en.json +++ b/packages/frontend/i18n/src/resources/en.json @@ -1115,6 +1115,15 @@ "com.affine.resetSyncStatus.description": "This operation may fix some synchronization issues.", "com.affine.rootAppSidebar.collections": "Collections", "com.affine.rootAppSidebar.favorites": "Favourites", + "com.affine.rootAppSidebar.migration-data": "Migration data", + "com.affine.rootAppSidebar.migration-data.help": "Migration your favorites data", + "com.affine.rootAppSidebar.migration-data.help.description": "This is the old Favourites data. Previously, Favourites were shared during collaboration. We have redesigned the Favourites feature, which requires you to take certain actions. You can drag this content or directly hide and delete this part of the data.", + "com.affine.rootAppSidebar.migration-data.help.clean-all": "Clear all migration data", + "com.affine.rootAppSidebar.migration-data.help.confirm": "OK", + "com.affine.rootAppSidebar.migration-data.clean-all": "Clean all migration data", + "com.affine.rootAppSidebar.migration-data.clean-all.description": "This will delete all Migration data, don't worry, it won't delete any data entities, it will just clean up the content on the sidebar. Once the content is cleared, the sidebar will no longer display this section until the next occurrence of conflicting data.", + "com.affine.rootAppSidebar.migration-data.clean-all.confirm": "OK", + "com.affine.rootAppSidebar.migration-data.clean-all.cancel": "Cancel", "com.affine.rootAppSidebar.organize": "Organize", "com.affine.rootAppSidebar.organize.empty": "No Folders", "com.affine.rootAppSidebar.organize.empty-folder": "Empty Folder", diff --git a/tests/affine-local/e2e/drag-page.spec.ts b/tests/affine-local/e2e/drag-page.spec.ts index 44ffa1cebf..c2920f43df 100644 --- a/tests/affine-local/e2e/drag-page.spec.ts +++ b/tests/affine-local/e2e/drag-page.spec.ts @@ -178,11 +178,11 @@ test('items in favourites can be reordered by dragging', async ({ page }) => { await expect( page.getByTestId('explorer-favorites').locator('[draggable]').first() - ).toHaveText(title0); + ).toHaveText('test collection'); await expect( page.getByTestId('explorer-favorites').locator('[draggable]').last() - ).toHaveText('test collection'); + ).toHaveText(title0); // drag the first item to the last const firstItem = page @@ -206,5 +206,5 @@ test('items in favourites can be reordered by dragging', async ({ page }) => { await expect( page.getByTestId('explorer-favorites').locator('[draggable]').last() - ).toHaveText(title0); + ).toHaveText('test collection'); }); diff --git a/tools/cli/src/webpack/runtime-config.ts b/tools/cli/src/webpack/runtime-config.ts index ab69e84214..823a487ffd 100644 --- a/tools/cli/src/webpack/runtime-config.ts +++ b/tools/cli/src/webpack/runtime-config.ts @@ -22,6 +22,8 @@ export function getRuntimeConfig(buildFlags: BuildFlags): RuntimeConfig { allowLocalWorkspace: buildFlags.distribution === 'desktop' ? true : false, enableOrganize: false, + enableNewFavorite: false, + enableOldFavorite: true, // TODO(@forehalo): enable in next release and canary enableInfoModal: false, @@ -56,6 +58,8 @@ export function getRuntimeConfig(buildFlags: BuildFlags): RuntimeConfig { changelogUrl: 'https://github.com/toeverything/AFFiNE/releases', enableInfoModal: true, enableOrganize: true, + enableNewFavorite: true, + enableOldFavorite: true, }; }, };