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,