From 0504d0b0ff9c1186196000408de7896b03c5e3e6 Mon Sep 17 00:00:00 2001 From: EYHN Date: Wed, 14 Aug 2024 10:35:21 +0000 Subject: [PATCH] feat(core): init feature flag service (#7856) --- packages/common/infra/src/atom/settings.ts | 123 ---------------- packages/common/infra/src/index.ts | 3 + .../src/modules/feature-flag/constant.ts | 88 +++++++++++ .../modules/feature-flag/entities/flags.ts | 62 ++++++++ .../infra/src/modules/feature-flag/index.ts | 13 ++ .../feature-flag/services/feature-flag.ts | 21 +++ .../infra/src/modules/feature-flag/types.ts | 17 +++ .../src/modules/workspace/events/index.ts | 5 + .../infra/src/modules/workspace/index.ts | 2 +- .../src/modules/workspace/services/repo.ts | 4 +- .../experimental-features/index.css.ts | 4 - .../experimental-features/index.tsx | 138 +++++------------- .../components/page-list/operation-cell.tsx | 24 ++- .../page-list/view/collection-operations.tsx | 28 +++- .../views/nodes/collection/operations.tsx | 18 ++- .../explorer/views/nodes/doc/operations.tsx | 33 +++-- .../explorer/views/nodes/tag/operations.tsx | 36 +++-- .../modules/workbench/view/workbench-link.tsx | 21 ++- tests/affine-desktop/e2e/tabs.spec.ts | 33 +---- 19 files changed, 361 insertions(+), 312 deletions(-) create mode 100644 packages/common/infra/src/modules/feature-flag/constant.ts create mode 100644 packages/common/infra/src/modules/feature-flag/entities/flags.ts create mode 100644 packages/common/infra/src/modules/feature-flag/index.ts create mode 100644 packages/common/infra/src/modules/feature-flag/services/feature-flag.ts create mode 100644 packages/common/infra/src/modules/feature-flag/types.ts diff --git a/packages/common/infra/src/atom/settings.ts b/packages/common/infra/src/atom/settings.ts index 3430ffed70..2427705f2f 100644 --- a/packages/common/infra/src/atom/settings.ts +++ b/packages/common/infra/src/atom/settings.ts @@ -1,12 +1,9 @@ import { DebugLogger } from '@affine/debug'; import { setupGlobal } from '@affine/env/global'; -import type { DocCollection } from '@blocksuite/store'; import { atom } from 'jotai'; import { atomWithStorage } from 'jotai/utils'; import { atomEffect } from 'jotai-effect'; -import { getCurrentStore } from './root-store'; - setupGlobal(); const logger = new DebugLogger('affine:settings'); @@ -31,9 +28,7 @@ export type AppSetting = { enableNoisyBackground: boolean; autoCheckUpdate: boolean; autoDownloadUpdate: boolean; - enableMultiView: boolean; enableTelemetry: boolean; - editorFlags: Partial>; }; export const windowFrameStyleOptions: AppSetting['windowFrameStyle'][] = [ 'frameless', @@ -73,40 +68,8 @@ const appSettingBaseAtom = atomWithStorage('affine-settings', { autoCheckUpdate: true, autoDownloadUpdate: true, enableTelemetry: true, - enableMultiView: false, - editorFlags: {}, }); -export function setupEditorFlags(docCollection: DocCollection) { - const store = getCurrentStore(); - const syncEditorFlags = () => { - try { - const editorFlags = getCurrentStore().get(appSettingBaseAtom).editorFlags; - Object.entries(editorFlags ?? {}).forEach(([key, value]) => { - docCollection.awarenessStore.setFlag( - key as keyof BlockSuiteFlags, - value - ); - }); - - // override this flag in app settings - // TODO(@eyhn): need a better way to manage block suite flags - Object.entries(blocksuiteFeatureFlags).forEach(([key, value]) => { - if (value.defaultState !== undefined) { - docCollection.awarenessStore.setFlag( - key as keyof BlockSuiteFlags, - value.defaultState - ); - } - }); - } catch (err) { - logger.error('syncEditorFlags', err); - } - }; - store.sub(appSettingBaseAtom, syncEditorFlags); - syncEditorFlags(); -} - type SetStateAction = Value | ((prev: Value) => Value); // todo(@pengx17): use global state instead @@ -143,89 +106,3 @@ export const appSettingAtom = atom< }); } ); - -export type BuildChannel = 'stable' | 'beta' | 'canary' | 'internal'; - -export type FeedbackType = 'discord' | 'email' | 'github'; - -export type PreconditionType = () => boolean | undefined; - -export type Flag = Partial<{ - [key in K]: { - displayName: string; - description?: string; - precondition?: PreconditionType; - defaultState?: boolean; // default to open and not controlled by user - feedbackType?: FeedbackType; - }; -}>; - -const isNotStableBuild: PreconditionType = () => { - return runtimeConfig.appBuildType !== 'stable'; -}; -const isDesktopEnvironment: PreconditionType = () => environment.isDesktop; -const neverShow: PreconditionType = () => false; - -export const blocksuiteFeatureFlags: Flag = { - enable_database_attachment_note: { - displayName: 'Database Attachment Note', - description: 'Allows adding notes to database attachments.', - precondition: isNotStableBuild, - }, - enable_database_statistics: { - displayName: 'Database Block Statistics', - description: 'Shows statistics for database blocks.', - precondition: isNotStableBuild, - }, - enable_block_query: { - displayName: 'Todo Block Query', - description: 'Enables querying of todo blocks.', - precondition: isNotStableBuild, - }, - enable_synced_doc_block: { - displayName: 'Synced Doc Block', - description: 'Enables syncing of doc blocks.', - precondition: neverShow, - defaultState: true, - }, - enable_edgeless_text: { - displayName: 'Edgeless Text', - description: 'Enables edgeless text blocks.', - precondition: neverShow, - defaultState: true, - }, - enable_color_picker: { - displayName: 'Color Picker', - description: 'Enables color picker blocks.', - precondition: neverShow, - defaultState: true, - }, - enable_ai_chat_block: { - displayName: 'AI Chat Block', - description: 'Enables AI chat blocks.', - precondition: neverShow, - defaultState: true, - }, - enable_ai_onboarding: { - displayName: 'AI Onboarding', - description: 'Enables AI onboarding.', - precondition: neverShow, - defaultState: true, - }, - enable_expand_database_block: { - displayName: 'Expand Database Block', - description: 'Enables expanding of database blocks.', - precondition: neverShow, - defaultState: true, - }, -}; - -export const affineFeatureFlags: Flag = { - enableMultiView: { - displayName: 'Split View', - description: - 'The Split View feature in AFFiNE allows users to divide their workspace into multiple sections, enabling simultaneous viewing and editing of different documents.The Split View feature in AFFiNE allows users to divide their workspace into multiple sections, enabling simultaneous viewing and editing of different documents.', - feedbackType: 'discord', - precondition: isDesktopEnvironment, - }, -}; diff --git a/packages/common/infra/src/index.ts b/packages/common/infra/src/index.ts index 77c4fb4739..67e67b3a91 100644 --- a/packages/common/infra/src/index.ts +++ b/packages/common/infra/src/index.ts @@ -6,6 +6,7 @@ export * from './initialization'; export * from './livedata'; export * from './modules/db'; export * from './modules/doc'; +export * from './modules/feature-flag'; export * from './modules/global-context'; export * from './modules/lifecycle'; export * from './modules/storage'; @@ -17,6 +18,7 @@ export * from './utils'; import type { Framework } from './framework'; import { configureWorkspaceDBModule } from './modules/db'; import { configureDocModule } from './modules/doc'; +import { configureFeatureFlagModule } from './modules/feature-flag'; import { configureGlobalContextModule } from './modules/global-context'; import { configureLifecycleModule } from './modules/lifecycle'; import { @@ -35,6 +37,7 @@ export function configureInfraModules(framework: Framework) { configureGlobalStorageModule(framework); configureGlobalContextModule(framework); configureLifecycleModule(framework); + configureFeatureFlagModule(framework); } export function configureTestingInfraModules(framework: Framework) { diff --git a/packages/common/infra/src/modules/feature-flag/constant.ts b/packages/common/infra/src/modules/feature-flag/constant.ts new file mode 100644 index 0000000000..9d4ea4a4f1 --- /dev/null +++ b/packages/common/infra/src/modules/feature-flag/constant.ts @@ -0,0 +1,88 @@ +import type { FlagInfo } from './types'; + +const isNotStableBuild = runtimeConfig.appBuildType !== 'stable'; +const isDesktopEnvironment = environment.isDesktop; +const isCanaryBuild = runtimeConfig.appBuildType === 'canary'; + +export const AFFINE_FLAGS = { + enable_database_attachment_note: { + category: 'blocksuite', + bsFlag: 'enable_database_attachment_note', + displayName: 'Database Attachment Note', + description: 'Allows adding notes to database attachments.', + configurable: isNotStableBuild, + }, + enable_database_statistics: { + category: 'blocksuite', + bsFlag: 'enable_database_statistics', + displayName: 'Database Block Statistics', + description: 'Shows statistics for database blocks.', + configurable: isNotStableBuild, + }, + enable_block_query: { + category: 'blocksuite', + bsFlag: 'enable_block_query', + displayName: 'Todo Block Query', + description: 'Enables querying of todo blocks.', + configurable: isNotStableBuild, + }, + enable_synced_doc_block: { + category: 'blocksuite', + bsFlag: 'enable_synced_doc_block', + displayName: 'Synced Doc Block', + description: 'Enables syncing of doc blocks.', + configurable: false, + defaultState: true, + }, + enable_edgeless_text: { + category: 'blocksuite', + bsFlag: 'enable_edgeless_text', + displayName: 'Edgeless Text', + description: 'Enables edgeless text blocks.', + configurable: false, + defaultState: true, + }, + enable_color_picker: { + category: 'blocksuite', + bsFlag: 'enable_color_picker', + displayName: 'Color Picker', + description: 'Enables color picker blocks.', + configurable: false, + defaultState: true, + }, + enable_ai_chat_block: { + category: 'blocksuite', + bsFlag: 'enable_ai_chat_block', + displayName: 'AI Chat Block', + description: 'Enables AI chat blocks.', + configurable: false, + defaultState: true, + }, + enable_ai_onboarding: { + category: 'blocksuite', + bsFlag: 'enable_ai_onboarding', + displayName: 'AI Onboarding', + description: 'Enables AI onboarding.', + configurable: false, + defaultState: true, + }, + enable_expand_database_block: { + category: 'blocksuite', + bsFlag: 'enable_expand_database_block', + displayName: 'Expand Database Block', + description: 'Enables expanding of database blocks.', + configurable: false, + defaultState: true, + }, + enable_multi_view: { + category: 'affine', + displayName: 'Split View', + description: + 'The Split View feature in AFFiNE allows users to divide their workspace into multiple sections, enabling simultaneous viewing and editing of different documents.The Split View feature in AFFiNE allows users to divide their workspace into multiple sections, enabling simultaneous viewing and editing of different documents.', + feedbackType: 'discord', + configurable: isDesktopEnvironment, + defaultState: isCanaryBuild, + }, +} satisfies { [key in string]: FlagInfo }; + +export type AFFINE_FLAGS = typeof AFFINE_FLAGS; diff --git a/packages/common/infra/src/modules/feature-flag/entities/flags.ts b/packages/common/infra/src/modules/feature-flag/entities/flags.ts new file mode 100644 index 0000000000..340e68dbf8 --- /dev/null +++ b/packages/common/infra/src/modules/feature-flag/entities/flags.ts @@ -0,0 +1,62 @@ +import { NEVER } from 'rxjs'; + +import { Entity } from '../../../framework'; +import { LiveData } from '../../../livedata'; +import type { GlobalStateService } from '../../storage'; +import { AFFINE_FLAGS } from '../constant'; +import type { FlagInfo } from '../types'; + +const FLAG_PREFIX = 'affine-flag:'; + +export type Flag = { + readonly value: F['defaultState'] extends boolean + ? boolean + : boolean | undefined; + set: (value: boolean) => void; + // eslint-disable-next-line rxjs/finnish + $: F['defaultState'] extends boolean + ? LiveData + : LiveData | LiveData; +} & F; + +export class Flags extends Entity { + private readonly globalState = this.globalStateService.globalState; + + constructor(private readonly globalStateService: GlobalStateService) { + super(); + + Object.entries(AFFINE_FLAGS).forEach(([flagKey, flag]) => { + const configurable = flag.configurable ?? true; + const defaultState = + 'defaultState' in flag ? flag.defaultState : undefined; + const item = { + ...flag, + value: configurable + ? (this.globalState.get(FLAG_PREFIX + flagKey) ?? + defaultState) + : defaultState, + set: (value: boolean) => { + if (!configurable) { + return; + } + this.globalState.set(FLAG_PREFIX + flagKey, value); + }, + $: configurable + ? LiveData.from( + this.globalState.watch(FLAG_PREFIX + flagKey), + undefined + ).map(value => value ?? defaultState) + : LiveData.from(NEVER, defaultState), + } as Flag; + Object.defineProperty(this, flagKey, { + get: () => { + return item; + }, + }); + }); + } +} + +export type FlagsExt = Flags & { + [K in keyof AFFINE_FLAGS]: Flag; +}; diff --git a/packages/common/infra/src/modules/feature-flag/index.ts b/packages/common/infra/src/modules/feature-flag/index.ts new file mode 100644 index 0000000000..e52725bf5e --- /dev/null +++ b/packages/common/infra/src/modules/feature-flag/index.ts @@ -0,0 +1,13 @@ +import type { Framework } from '../../framework'; +import { GlobalStateService } from '../storage'; +import { Flags } from './entities/flags'; +import { FeatureFlagService } from './services/feature-flag'; + +export { AFFINE_FLAGS } from './constant'; +export type { Flag } from './entities/flags'; +export { FeatureFlagService } from './services/feature-flag'; +export type { FlagInfo } from './types'; + +export function configureFeatureFlagModule(framework: Framework) { + framework.service(FeatureFlagService).entity(Flags, [GlobalStateService]); +} diff --git a/packages/common/infra/src/modules/feature-flag/services/feature-flag.ts b/packages/common/infra/src/modules/feature-flag/services/feature-flag.ts new file mode 100644 index 0000000000..f5b356a6e1 --- /dev/null +++ b/packages/common/infra/src/modules/feature-flag/services/feature-flag.ts @@ -0,0 +1,21 @@ +import { OnEvent, Service } from '../../../framework'; +import type { Workspace } from '../../workspace'; +import { WorkspaceInitialized } from '../../workspace/events'; +import { AFFINE_FLAGS } from '../constant'; +import { Flags, type FlagsExt } from '../entities/flags'; + +@OnEvent(WorkspaceInitialized, e => e.setupBlocksuiteEditorFlags) +export class FeatureFlagService extends Service { + flags = this.framework.createEntity(Flags) as FlagsExt; + + setupBlocksuiteEditorFlags(workspace: Workspace) { + for (const [key, flag] of Object.entries(AFFINE_FLAGS)) { + if (flag.category === 'blocksuite') { + const value = this.flags[key as keyof AFFINE_FLAGS].value; + if (value !== undefined) { + workspace.docCollection.awarenessStore.setFlag(flag.bsFlag, value); + } + } + } + } +} diff --git a/packages/common/infra/src/modules/feature-flag/types.ts b/packages/common/infra/src/modules/feature-flag/types.ts new file mode 100644 index 0000000000..897df817ec --- /dev/null +++ b/packages/common/infra/src/modules/feature-flag/types.ts @@ -0,0 +1,17 @@ +type FeedbackType = 'discord' | 'email' | 'github'; + +export type FlagInfo = { + displayName: string; + description?: string; + configurable?: boolean; + defaultState?: boolean; // default to open and not controlled by user + feedbackType?: FeedbackType; +} & ( + | { + category: 'affine'; + } + | { + category: 'blocksuite'; + bsFlag: keyof BlockSuiteFlags; + } +); diff --git a/packages/common/infra/src/modules/workspace/events/index.ts b/packages/common/infra/src/modules/workspace/events/index.ts index 2cd9c2f230..9e0c68c01d 100644 --- a/packages/common/infra/src/modules/workspace/events/index.ts +++ b/packages/common/infra/src/modules/workspace/events/index.ts @@ -1,6 +1,11 @@ import { createEvent } from '../../../framework'; import type { WorkspaceEngine } from '../entities/engine'; +import type { Workspace } from '../entities/workspace'; export const WorkspaceEngineBeforeStart = createEvent( 'WorkspaceEngineBeforeStart' ); + +export const WorkspaceInitialized = createEvent( + 'WorkspaceInitialized' +); diff --git a/packages/common/infra/src/modules/workspace/index.ts b/packages/common/infra/src/modules/workspace/index.ts index 7487b80271..d6f59a682e 100644 --- a/packages/common/infra/src/modules/workspace/index.ts +++ b/packages/common/infra/src/modules/workspace/index.ts @@ -1,6 +1,6 @@ export type { WorkspaceProfileInfo } from './entities/profile'; export { Workspace } from './entities/workspace'; -export { WorkspaceEngineBeforeStart } from './events'; +export { WorkspaceEngineBeforeStart, WorkspaceInitialized } from './events'; export { globalBlockSuiteSchema } from './global-schema'; export type { WorkspaceMetadata } from './metadata'; export type { WorkspaceOpenOptions } from './open-options'; diff --git a/packages/common/infra/src/modules/workspace/services/repo.ts b/packages/common/infra/src/modules/workspace/services/repo.ts index d5acc577bf..d8d4cb8461 100644 --- a/packages/common/infra/src/modules/workspace/services/repo.ts +++ b/packages/common/infra/src/modules/workspace/services/repo.ts @@ -1,10 +1,10 @@ import { DebugLogger } from '@affine/debug'; -import { setupEditorFlags } from '../../../atom'; import { fixWorkspaceVersion } from '../../../blocksuite'; import { Service } from '../../../framework'; import { ObjectPool } from '../../../utils'; import type { Workspace } from '../entities/workspace'; +import { WorkspaceInitialized } from '../events'; import type { WorkspaceOpenOptions } from '../open-options'; import type { WorkspaceFlavourProvider } from '../providers/flavour'; import { WorkspaceScope } from '../scopes/workspace'; @@ -103,7 +103,7 @@ export class WorkspaceRepositoryService extends Service { // apply compatibility fix fixWorkspaceVersion(workspace.docCollection.doc); - setupEditorFlags(workspace.docCollection); + this.framework.emitEvent(WorkspaceInitialized, workspace); this.profileRepo .getProfile(openOptions.metadata) diff --git a/packages/frontend/core/src/components/affine/setting-modal/general-setting/experimental-features/index.css.ts b/packages/frontend/core/src/components/affine/setting-modal/general-setting/experimental-features/index.css.ts index 052c2e45f2..c82c9f2c78 100644 --- a/packages/frontend/core/src/components/affine/setting-modal/general-setting/experimental-features/index.css.ts +++ b/packages/frontend/core/src/components/affine/setting-modal/general-setting/experimental-features/index.css.ts @@ -56,10 +56,6 @@ export const switchRow = style({ justifyContent: 'space-between', width: '100%', }); -export const switchDisabled = style({ - opacity: 0.5, - pointerEvents: 'none', -}); export const subHeader = style({ fontWeight: '600', color: cssVar('textSecondaryColor'), diff --git a/packages/frontend/core/src/components/affine/setting-modal/general-setting/experimental-features/index.tsx b/packages/frontend/core/src/components/affine/setting-modal/general-setting/experimental-features/index.tsx index 0038f97645..df0d983c34 100644 --- a/packages/frontend/core/src/components/affine/setting-modal/general-setting/experimental-features/index.tsx +++ b/packages/frontend/core/src/components/affine/setting-modal/general-setting/experimental-features/index.tsx @@ -1,6 +1,5 @@ import { Button, Checkbox, Loading, Switch, Tooltip } from '@affine/component'; import { SettingHeader } from '@affine/component/setting-components'; -import { useAppSettingHelper } from '@affine/core/hooks/affine/use-app-setting-helper'; import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks'; import { useI18n } from '@affine/i18n'; import { @@ -10,9 +9,11 @@ import { GithubIcon, } from '@blocksuite/icons/rc'; import { - affineFeatureFlags, - blocksuiteFeatureFlags, - type FeedbackType, + AFFINE_FLAGS, + FeatureFlagService, + type Flag, + useLiveData, + useServices, } from '@toeverything/infra'; import { useAtom } from 'jotai'; import { atomWithStorage } from 'jotai/utils'; @@ -86,7 +87,7 @@ const ExperimentalFeaturesPrompt = ({ ); }; -const FeedbackIcon = ({ type }: { type: FeedbackType }) => { +const FeedbackIcon = ({ type }: { type: Flag['feedbackType'] }) => { switch (type) { case 'discord': return ; @@ -99,55 +100,45 @@ const FeedbackIcon = ({ type }: { type: FeedbackType }) => { } }; -const feedbackLink: Record = { +const feedbackLink: Record, string> = { discord: 'https://discord.gg/whd5mjYqVw', email: 'mailto:support@toeverything.info', github: 'https://github.com/toeverything/AFFiNE/issues', }; -const ExperimentalFeaturesItem = ({ - title, - description, - feedbackType, - isMutating, - checked, - onChange, - testId, -}: { - title: React.ReactNode; - description?: React.ReactNode; - feedbackType?: FeedbackType; - isMutating?: boolean; - checked: boolean; - onChange: (checked: boolean) => void; - testId?: string; -}) => { - const link = feedbackType ? feedbackLink[feedbackType] : undefined; +const ExperimentalFeaturesItem = ({ flag }: { flag: Flag }) => { + const value = useLiveData(flag.$); + const onChange = useCallback( + (checked: boolean) => { + flag.set(checked); + }, + [flag] + ); + const link = flag.feedbackType ? feedbackLink[flag.feedbackType] : undefined; + + if (flag.configurable === false) { + return null; + } return (
- {title} - + {flag.displayName} +
- {!!description && ( - -
{description}
+ {!!flag.description && ( + +
{flag.description}
)} - {!!feedbackType && ( + {!!flag.feedbackType && ( - + Discussion about this feature { - const { appSettings, updateSettings } = useAppSettingHelper(); - - const onToggle = useCallback( - (checked: boolean) => { - updateSettings('enableMultiView', checked); - }, - [updateSettings] - ); - const multiViewFlagConfig = affineFeatureFlags['enableMultiView']; - const shouldShow = multiViewFlagConfig?.precondition?.(); - - if (!multiViewFlagConfig || !shouldShow) { - return null; - } - - return ( - - ); -}; - -const BlocksuiteFeatureFlagSettings = () => { - const { appSettings, updateSettings } = useAppSettingHelper(); - const toggleSetting = useCallback( - (flag: keyof BlockSuiteFlags, checked: boolean) => { - updateSettings('editorFlags', { - ...appSettings.editorFlags, - [flag]: checked, - }); - }, - [appSettings.editorFlags, updateSettings] - ); - - type EditorFlag = keyof typeof appSettings.editorFlags; - - return ( - <> - {Object.entries(blocksuiteFeatureFlags).map(([key, value]) => { - const hidden = value.precondition && !value.precondition(); - - if (hidden) { - return null; - } - return ( - - toggleSetting(key as keyof BlockSuiteFlags, checked) - } - /> - ); - })} - - ); -}; - const ExperimentalFeaturesMain = () => { const t = useI18n(); + const { featureFlagService } = useServices({ FeatureFlagService }); return ( <> @@ -242,8 +168,12 @@ const ExperimentalFeaturesMain = () => { className={styles.settingsContainer} data-testid="experimental-settings" > - - + {Object.keys(AFFINE_FLAGS).map(key => ( + + ))}
); 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 29612031e4..6ebc97f1f7 100644 --- a/packages/frontend/core/src/components/page-list/operation-cell.tsx +++ b/packages/frontend/core/src/components/page-list/operation-cell.tsx @@ -6,7 +6,6 @@ import { toast, useConfirmModal, } from '@affine/component'; -import { useAppSettingHelper } from '@affine/core/hooks/affine/use-app-setting-helper'; import { useBlockSuiteMetaHelper } from '@affine/core/hooks/affine/use-block-suite-meta-helper'; import { useTrashModalHelper } from '@affine/core/hooks/affine/use-trash-modal-helper'; import { useCatchEventCallback } from '@affine/core/hooks/use-catch-event-hook'; @@ -34,6 +33,7 @@ import { } from '@blocksuite/icons/rc'; import type { DocMeta } from '@blocksuite/store'; import { + FeatureFlagService, useLiveData, useService, useServices, @@ -67,13 +67,25 @@ export const PageOperationCell = ({ onRemoveFromAllowList, }: PageOperationCellProps) => { const t = useI18n(); - const currentWorkspace = useService(WorkspaceService).workspace; - const { appSettings } = useAppSettingHelper(); + const { + featureFlagService, + workspaceService, + compatibleFavoriteItemsAdapter: favAdapter, + workbenchService, + } = useServices({ + FeatureFlagService, + WorkspaceService, + CompatibleFavoriteItemsAdapter, + WorkbenchService, + }); + const enableSplitView = useLiveData( + featureFlagService.flags.enable_multi_view.$ + ); + const currentWorkspace = workspaceService.workspace; const { setTrashModal } = useTrashModalHelper(currentWorkspace.docCollection); const [openDisableShared, setOpenDisableShared] = useState(false); - const favAdapter = useService(CompatibleFavoriteItemsAdapter); const favourite = useLiveData(favAdapter.isFavorite$(page.id, 'doc')); - const workbench = useService(WorkbenchService).workbench; + const workbench = workbenchService.workbench; const { duplicate } = useBlockSuiteMetaHelper(currentWorkspace.docCollection); const blocksuiteDoc = currentWorkspace.docCollection.getDoc(page.id); @@ -201,7 +213,7 @@ export const PageOperationCell = ({ {t['com.affine.workbench.tab.page-menu-open']()} - {environment.isDesktop && appSettings.enableMultiView ? ( + {environment.isDesktop && enableSplitView ? ( void; onAddDocToCollection?: () => void; }>) => { + const { + collectionService: service, + workbenchService, + featureFlagService, + } = useServices({ + CollectionService, + WorkbenchService, + FeatureFlagService, + }); const deleteInfo = useDeleteCollectionInfo(); - const { appSettings } = useAppSettingHelper(); - const service = useService(CollectionService); - const workbench = useService(WorkbenchService).workbench; + const workbench = workbenchService.workbench; const { open: openEditCollectionModal, node: editModal } = useEditCollection(); const t = useI18n(); @@ -48,6 +59,9 @@ export const CollectionOperations = ({ useEditCollectionName({ title: t['com.affine.editCollection.renameCollection'](), }); + const enableMultiView = useLiveData( + featureFlagService.flags.enable_multi_view.$ + ); const showEditName = useCallback(() => { // use openRenameModal if it is in the sidebar collection list @@ -167,7 +181,7 @@ export const CollectionOperations = ({ name: t['com.affine.workbench.tab.page-menu-open'](), click: openCollectionNewTab, }, - ...(appSettings.enableMultiView + ...(enableMultiView && environment.isDesktop ? [ { icon: ( @@ -197,6 +211,7 @@ export const CollectionOperations = ({ }, ], [ + enableMultiView, t, showEditName, showEdit, @@ -204,7 +219,6 @@ export const CollectionOperations = ({ favorite, onToggleFavoritePage, openCollectionNewTab, - appSettings.enableMultiView, openCollectionSplitView, service, deleteInfo, 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 9a43303954..e26bdf0be2 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 @@ -5,7 +5,6 @@ import { MenuSeparator, useConfirmModal, } 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 { track } from '@affine/core/mixpanel'; import { CollectionService } from '@affine/core/modules/collection'; @@ -21,7 +20,12 @@ import { PlusIcon, SplitViewIcon, } from '@blocksuite/icons/rc'; -import { DocsService, useLiveData, useServices } from '@toeverything/infra'; +import { + DocsService, + FeatureFlagService, + useLiveData, + useServices, +} from '@toeverything/infra'; import { useCallback, useMemo } from 'react'; import type { NodeOperation } from '../../tree/types'; @@ -32,20 +36,24 @@ export const useExplorerCollectionNodeOperations = ( onOpenEdit: () => void ): NodeOperation[] => { const t = useI18n(); - const { appSettings } = useAppSettingHelper(); const { workbenchService, docsService, collectionService, compatibleFavoriteItemsAdapter, + featureFlagService, } = useServices({ DocsService, WorkbenchService, CollectionService, CompatibleFavoriteItemsAdapter, + FeatureFlagService, }); const deleteInfo = useDeleteCollectionInfo(); + const enableMultiView = useLiveData( + featureFlagService.flags.enable_multi_view.$ + ); const favorite = useLiveData( useMemo( () => @@ -200,7 +208,7 @@ export const useExplorerCollectionNodeOperations = ( ), }, - ...(appSettings.enableMultiView + ...(environment.isDesktop && enableMultiView ? [ { index: 99, @@ -241,7 +249,7 @@ export const useExplorerCollectionNodeOperations = ( }, ], [ - appSettings.enableMultiView, + enableMultiView, favorite, handleAddDocToCollection, handleDeleteCollection, 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 bd37f560f7..8023529ab6 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 @@ -6,7 +6,6 @@ import { toast, useConfirmModal, } from '@affine/component'; -import { useAppSettingHelper } from '@affine/core/hooks/affine/use-app-setting-helper'; import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks'; import { track } from '@affine/core/mixpanel'; import { CompatibleFavoriteItemsAdapter } from '@affine/core/modules/properties'; @@ -22,7 +21,12 @@ import { PlusIcon, SplitViewIcon, } from '@blocksuite/icons/rc'; -import { DocsService, useLiveData, useServices } from '@toeverything/infra'; +import { + DocsService, + FeatureFlagService, + useLiveData, + useServices, +} from '@toeverything/infra'; import { useCallback, useMemo } from 'react'; import type { NodeOperation } from '../../tree/types'; @@ -35,13 +39,20 @@ export const useExplorerDocNodeOperations = ( } ): NodeOperation[] => { const t = useI18n(); - const { appSettings } = useAppSettingHelper(); - const { workbenchService, docsService, compatibleFavoriteItemsAdapter } = - useServices({ - DocsService, - WorkbenchService, - CompatibleFavoriteItemsAdapter, - }); + const { + workbenchService, + docsService, + compatibleFavoriteItemsAdapter, + featureFlagService, + } = useServices({ + DocsService, + WorkbenchService, + CompatibleFavoriteItemsAdapter, + FeatureFlagService, + }); + const enableMultiView = useLiveData( + featureFlagService.flags.enable_multi_view.$ + ); const { openConfirmModal } = useConfirmModal(); const docRecord = useLiveData(docsService.list.doc$(docId)); @@ -179,7 +190,7 @@ export const useExplorerDocNodeOperations = ( ), }, - ...(appSettings.enableMultiView + ...(enableMultiView && environment.isDesktop ? [ { index: 100, @@ -243,7 +254,7 @@ export const useExplorerDocNodeOperations = ( }, ], [ - appSettings.enableMultiView, + enableMultiView, favorite, handleAddLinkedPage, handleMoveToTrash, 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 154785e510..9e4a941523 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 @@ -5,7 +5,6 @@ import { MenuSeparator, toast, } from '@affine/component'; -import { useAppSettingHelper } from '@affine/core/hooks/affine/use-app-setting-helper'; import { track } from '@affine/core/mixpanel'; import { FavoriteService } from '@affine/core/modules/favorite'; import { TagService } from '@affine/core/modules/tag'; @@ -19,7 +18,12 @@ import { PlusIcon, SplitViewIcon, } from '@blocksuite/icons/rc'; -import { DocsService, useLiveData, useServices } from '@toeverything/infra'; +import { + DocsService, + FeatureFlagService, + useLiveData, + useServices, +} from '@toeverything/infra'; import { useCallback, useMemo } from 'react'; import type { NodeOperation } from '../../tree/types'; @@ -33,19 +37,27 @@ export const useExplorerTagNodeOperations = ( } ): NodeOperation[] => { const t = useI18n(); - const { appSettings } = useAppSettingHelper(); - const { docsService, workbenchService, tagService, favoriteService } = - useServices({ - WorkbenchService, - TagService, - DocsService, - FavoriteService, - }); + const { + docsService, + workbenchService, + tagService, + favoriteService, + featureFlagService, + } = useServices({ + WorkbenchService, + TagService, + DocsService, + FavoriteService, + FeatureFlagService, + }); const favorite = useLiveData( favoriteService.favoriteList.favorite$('tag', tagId) ); const tagRecord = useLiveData(tagService.tagList.tagByTagId$(tagId)); + const enableMultiView = useLiveData( + featureFlagService.flags.enable_multi_view.$ + ); const handleNewDoc = useCallback(() => { if (tagRecord) { @@ -114,7 +126,7 @@ export const useExplorerTagNodeOperations = ( ), }, - ...(appSettings.enableMultiView + ...(enableMultiView && environment.isDesktop ? [ { index: 100, @@ -178,7 +190,7 @@ export const useExplorerTagNodeOperations = ( }, ], [ - appSettings.enableMultiView, + enableMultiView, favorite, handleMoveToTrash, handleNewDoc, diff --git a/packages/frontend/core/src/modules/workbench/view/workbench-link.tsx b/packages/frontend/core/src/modules/workbench/view/workbench-link.tsx index 0287a56cda..2d8e521734 100644 --- a/packages/frontend/core/src/modules/workbench/view/workbench-link.tsx +++ b/packages/frontend/core/src/modules/workbench/view/workbench-link.tsx @@ -1,7 +1,10 @@ -import { useAppSettingHelper } from '@affine/core/hooks/affine/use-app-setting-helper'; import { useCatchEventCallback } from '@affine/core/hooks/use-catch-event-hook'; import { isNewTabTrigger } from '@affine/core/utils'; -import { useLiveData, useService } from '@toeverything/infra'; +import { + FeatureFlagService, + useLiveData, + useServices, +} from '@toeverything/infra'; import { type To } from 'history'; import { forwardRef, type MouseEvent } from 'react'; @@ -16,8 +19,14 @@ export const WorkbenchLink = forwardRef< } & React.HTMLProps > >(function WorkbenchLink({ to, onClick, ...other }, ref) { - const workbench = useService(WorkbenchService).workbench; - const { appSettings } = useAppSettingHelper(); + const { featureFlagService, workbenchService } = useServices({ + FeatureFlagService, + WorkbenchService, + }); + const enableMultiView = useLiveData( + featureFlagService.flags.enable_multi_view.$ + ); + const workbench = workbenchService.workbench; const basename = useLiveData(workbench.basename$); const link = basename + @@ -30,7 +39,7 @@ export const WorkbenchLink = forwardRef< } const at = (() => { if (isNewTabTrigger(event)) { - return event.altKey && appSettings.enableMultiView + return event.altKey && enableMultiView && environment.isDesktop ? 'tail' : 'new-tab'; } @@ -39,7 +48,7 @@ export const WorkbenchLink = forwardRef< workbench.open(to, { at }); event.preventDefault(); }, - [appSettings.enableMultiView, onClick, to, workbench] + [enableMultiView, onClick, to, workbench] ); // eslint suspicious runtime error diff --git a/tests/affine-desktop/e2e/tabs.spec.ts b/tests/affine-desktop/e2e/tabs.spec.ts index e1a1c7a479..37d03406d4 100644 --- a/tests/affine-desktop/e2e/tabs.spec.ts +++ b/tests/affine-desktop/e2e/tabs.spec.ts @@ -132,35 +132,7 @@ test('tab title will change when navigating', async ({ page }) => { } }); -// temporary way to enable split view -async function enableSplitView(page: Page) { - await page.evaluate(() => { - const settingKey = 'affine-settings'; - window.localStorage.setItem( - settingKey, - JSON.stringify({ - clientBorder: false, - fullWidthLayout: false, - windowFrameStyle: 'frameless', - fontStyle: 'Serif', - dateFormat: 'MM/dd/YYYY', - startWeekOnMonday: false, - enableBlurBackground: true, - enableNoisyBackground: true, - autoCheckUpdate: true, - autoDownloadUpdate: true, - enableMultiView: true, - editorFlags: {}, - }) - ); - }); - await page.reload({ - timeout: 30000, - }); -} - test('open new tab via cmd+click page link', async ({ page }) => { - await enableSplitView(page); await clickNewPageButton(page); await page.waitForTimeout(500); await page.keyboard.press('Enter'); @@ -177,7 +149,6 @@ test('open new tab via cmd+click page link', async ({ page }) => { }); test('open split view', async ({ page }) => { - await enableSplitView(page); await clickNewPageButton(page); await page.waitForTimeout(500); await page.keyboard.press('Enter'); @@ -229,10 +200,10 @@ test('reorder tabs', async ({ page }) => { await createLinkedPage(page, titles[0]); await createLinkedPage(page, titles[1]); await page.locator(`.affine-reference-title:has-text("${titles[0]}")`).click({ - modifiers: ['ControlOrMeta', 'Alt'], + modifiers: ['ControlOrMeta'], }); await page.locator(`.affine-reference-title:has-text("${titles[1]}")`).click({ - modifiers: ['ControlOrMeta', 'Alt'], + modifiers: ['ControlOrMeta'], }); await expectTabTitle(page, 0, 'Untitled');