feat(core): init feature flag service (#7856)

This commit is contained in:
EYHN
2024-08-14 10:35:21 +00:00
parent 339c39c1ec
commit 0504d0b0ff
19 changed files with 361 additions and 312 deletions
@@ -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'),
@@ -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 <DiscordIcon fontSize={16} />;
@@ -99,55 +100,45 @@ const FeedbackIcon = ({ type }: { type: FeedbackType }) => {
}
};
const feedbackLink: Record<FeedbackType, string> = {
const feedbackLink: Record<NonNullable<Flag['feedbackType']>, 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 (
<div className={styles.rowContainer}>
<div className={styles.switchRow}>
{title}
<Switch
checked={checked}
onChange={onChange}
className={isMutating ? styles.switchDisabled : ''}
data-testid={testId}
/>
{flag.displayName}
<Switch checked={value} onChange={onChange} />
</div>
{!!description && (
<Tooltip content={description}>
<div className={styles.description}>{description}</div>
{!!flag.description && (
<Tooltip content={flag.description}>
<div className={styles.description}>{flag.description}</div>
</Tooltip>
)}
{!!feedbackType && (
{!!flag.feedbackType && (
<a
className={styles.feedback}
href={link}
target="_blank"
rel="noreferrer"
>
<FeedbackIcon type={feedbackType} />
<FeedbackIcon type={flag.feedbackType} />
<span>Discussion about this feature</span>
<ArrowRightSmallIcon
fontSize={20}
@@ -159,74 +150,9 @@ const ExperimentalFeaturesItem = ({
);
};
const SplitViewSettingRow = () => {
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 (
<ExperimentalFeaturesItem
title={multiViewFlagConfig.displayName}
description={multiViewFlagConfig.description}
feedbackType={multiViewFlagConfig.feedbackType}
checked={appSettings.enableMultiView}
onChange={onToggle}
/>
);
};
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 (
<ExperimentalFeaturesItem
key={key}
title={'Block Suite: ' + value.displayName}
description={value.description}
feedbackType={value.feedbackType}
checked={!!appSettings.editorFlags?.[key as EditorFlag]}
onChange={checked =>
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"
>
<SplitViewSettingRow />
<BlocksuiteFeatureFlagSettings />
{Object.keys(AFFINE_FLAGS).map(key => (
<ExperimentalFeaturesItem
key={key}
flag={featureFlagService.flags[key as keyof AFFINE_FLAGS]}
/>
))}
</div>
</>
);
@@ -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']()}
</MenuItem>
{environment.isDesktop && appSettings.enableMultiView ? (
{environment.isDesktop && enableSplitView ? (
<MenuItem
onClick={onOpenInSplitView}
preFix={
@@ -1,6 +1,5 @@
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 { CompatibleFavoriteItemsAdapter } from '@affine/core/modules/properties';
import { WorkbenchService } from '@affine/core/modules/workbench';
@@ -16,7 +15,12 @@ import {
PlusIcon,
SplitViewIcon,
} from '@blocksuite/icons/rc';
import { useLiveData, useService } from '@toeverything/infra';
import {
FeatureFlagService,
useLiveData,
useService,
useServices,
} from '@toeverything/infra';
import type { PropsWithChildren, ReactElement } from 'react';
import { useCallback, useMemo } from 'react';
@@ -37,10 +41,17 @@ export const CollectionOperations = ({
openRenameModal?: () => 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,
@@ -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 = (
</MenuItem>
),
},
...(appSettings.enableMultiView
...(environment.isDesktop && enableMultiView
? [
{
index: 99,
@@ -241,7 +249,7 @@ export const useExplorerCollectionNodeOperations = (
},
],
[
appSettings.enableMultiView,
enableMultiView,
favorite,
handleAddDocToCollection,
handleDeleteCollection,
@@ -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 = (
</MenuItem>
),
},
...(appSettings.enableMultiView
...(enableMultiView && environment.isDesktop
? [
{
index: 100,
@@ -243,7 +254,7 @@ export const useExplorerDocNodeOperations = (
},
],
[
appSettings.enableMultiView,
enableMultiView,
favorite,
handleAddLinkedPage,
handleMoveToTrash,
@@ -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 = (
</MenuItem>
),
},
...(appSettings.enableMultiView
...(enableMultiView && environment.isDesktop
? [
{
index: 100,
@@ -178,7 +190,7 @@ export const useExplorerTagNodeOperations = (
},
],
[
appSettings.enableMultiView,
enableMultiView,
favorite,
handleMoveToTrash,
handleNewDoc,
@@ -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<HTMLAnchorElement>
>
>(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