mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-08-05 19:40:30 +08:00
feat(core): init feature flag service (#7856)
This commit is contained in:
-4
@@ -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'),
|
||||
|
||||
+34
-104
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user