mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-13 12:55:00 +00:00
feat(core): add configuration for experimental features (#7699)
close AF-1218 AF-1219
Added configuration for experimental features
Example:
```
const blocksuiteFeatureFlags = {
...
enable_expand_database_block: {
displayName: 'Enable Expand Database Block',
description: 'Allows expanding database blocks for better view and management.',
feedbackType: 'discord',
displayChannel: ['stable', 'beta', 'canary', 'internal'],
restrictedPlatform: 'client'
},
enable_ai_onboarding: {
displayName: 'AI Onboarding',
description: 'Enables AI onboarding.',
displayChannel: [],
defaultState: true,
},
...
}
```

This commit is contained in:
@@ -21,7 +21,7 @@ export const relatedLinks = [
|
||||
{
|
||||
icon: <DiscordIcon />,
|
||||
title: 'Discord',
|
||||
link: 'https://discord.gg/Arn7TqJBvG',
|
||||
link: 'https://discord.gg/whd5mjYqVw',
|
||||
},
|
||||
{
|
||||
icon: <YouTubeIcon />,
|
||||
|
||||
@@ -54,6 +54,7 @@ export const switchRow = style({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
width: '100%',
|
||||
});
|
||||
export const switchDisabled = style({
|
||||
opacity: 0.5,
|
||||
@@ -64,3 +65,34 @@ export const subHeader = style({
|
||||
color: cssVar('textSecondaryColor'),
|
||||
marginBottom: 8,
|
||||
});
|
||||
|
||||
export const rowContainer = style({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
gap: 10,
|
||||
});
|
||||
export const description = style({
|
||||
color: cssVar('textSecondaryColor'),
|
||||
fontSize: cssVar('fontXs'),
|
||||
// 2 lines
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
display: '-webkit-box',
|
||||
WebkitLineClamp: 2,
|
||||
WebkitBoxOrient: 'vertical',
|
||||
width: '100%',
|
||||
});
|
||||
export const feedback = style({
|
||||
width: '100%',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
fontSize: cssVar('fontXs'),
|
||||
color: cssVar('textSecondaryColor'),
|
||||
gap: 8,
|
||||
});
|
||||
|
||||
export const arrowRightIcon = style({
|
||||
marginLeft: 'auto',
|
||||
marginRight: 0,
|
||||
});
|
||||
|
||||
@@ -1,8 +1,19 @@
|
||||
import { Button, Checkbox, Loading, Switch } from '@affine/component';
|
||||
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 {
|
||||
ArrowRightSmallIcon,
|
||||
DiscordIcon,
|
||||
EmailIcon,
|
||||
GithubIcon,
|
||||
} from '@blocksuite/icons/rc';
|
||||
import {
|
||||
affineFeatureFlags,
|
||||
blocksuiteFeatureFlags,
|
||||
type FeedbackType,
|
||||
} from '@toeverything/infra';
|
||||
import { useAtom } from 'jotai';
|
||||
import { atomWithStorage } from 'jotai/utils';
|
||||
import { Suspense, useCallback, useState } from 'react';
|
||||
@@ -75,28 +86,75 @@ const ExperimentalFeaturesPrompt = ({
|
||||
);
|
||||
};
|
||||
|
||||
const FeedbackIcon = ({ type }: { type: FeedbackType }) => {
|
||||
switch (type) {
|
||||
case 'discord':
|
||||
return <DiscordIcon fontSize={16} />;
|
||||
case 'email':
|
||||
return <EmailIcon fontSize={16} />;
|
||||
case 'github':
|
||||
return <GithubIcon fontSize={16} />;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const feedbackLink: Record<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;
|
||||
|
||||
return (
|
||||
<div className={styles.switchRow}>
|
||||
{title}
|
||||
<Switch
|
||||
checked={checked}
|
||||
onChange={onChange}
|
||||
className={isMutating ? styles.switchDisabled : ''}
|
||||
data-testid={testId}
|
||||
/>
|
||||
<div className={styles.rowContainer}>
|
||||
<div className={styles.switchRow}>
|
||||
{title}
|
||||
<Switch
|
||||
checked={checked}
|
||||
onChange={onChange}
|
||||
className={isMutating ? styles.switchDisabled : ''}
|
||||
data-testid={testId}
|
||||
/>
|
||||
</div>
|
||||
{!!description && (
|
||||
<Tooltip content={description}>
|
||||
<div className={styles.description}>{description}</div>
|
||||
</Tooltip>
|
||||
)}
|
||||
{!!feedbackType && (
|
||||
<a
|
||||
className={styles.feedback}
|
||||
href={link}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
<FeedbackIcon type={feedbackType} />
|
||||
<span>Discussion about this feature</span>
|
||||
<ArrowRightSmallIcon
|
||||
fontSize={20}
|
||||
className={styles.arrowRightIcon}
|
||||
/>
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -110,28 +168,24 @@ const SplitViewSettingRow = () => {
|
||||
},
|
||||
[updateSettings]
|
||||
);
|
||||
const multiViewFlagConfig = affineFeatureFlags['enableMultiView'];
|
||||
const shouldShow = multiViewFlagConfig?.precondition?.();
|
||||
|
||||
if (!environment.isDesktop) {
|
||||
return null; // only enable on desktop
|
||||
if (!multiViewFlagConfig || !shouldShow) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<ExperimentalFeaturesItem
|
||||
title="Split View"
|
||||
title={multiViewFlagConfig.displayName}
|
||||
description={multiViewFlagConfig.description}
|
||||
feedbackType={multiViewFlagConfig.feedbackType}
|
||||
checked={appSettings.enableMultiView}
|
||||
onChange={onToggle}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
// feature flag -> display name
|
||||
const blocksuiteFeatureFlags: Partial<Record<keyof BlockSuiteFlags, string>> = {
|
||||
enable_expand_database_block: 'Enable Expand Database Block',
|
||||
enable_database_attachment_note: 'Enable Database Attachment Note',
|
||||
enable_database_statistics: 'Enable Database Block Statistics',
|
||||
enable_block_query: 'Enable Todo Block Query',
|
||||
};
|
||||
|
||||
const BlocksuiteFeatureFlagSettings = () => {
|
||||
const { appSettings, updateSettings } = useAppSettingHelper();
|
||||
const toggleSetting = useCallback(
|
||||
@@ -148,16 +202,25 @@ const BlocksuiteFeatureFlagSettings = () => {
|
||||
|
||||
return (
|
||||
<>
|
||||
{Object.entries(blocksuiteFeatureFlags).map(([flag, displayName]) => (
|
||||
<ExperimentalFeaturesItem
|
||||
key={flag}
|
||||
title={'Block Suite: ' + displayName}
|
||||
checked={!!appSettings.editorFlags?.[flag as EditorFlag]}
|
||||
onChange={checked =>
|
||||
toggleSetting(flag as keyof BlockSuiteFlags, checked)
|
||||
}
|
||||
/>
|
||||
))}
|
||||
{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)
|
||||
}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -171,6 +234,9 @@ const ExperimentalFeaturesMain = () => {
|
||||
title={t[
|
||||
'com.affine.settings.workspace.experimental-features.header.plugins'
|
||||
]()}
|
||||
subtitle={t[
|
||||
'com.affine.settings.workspace.experimental-features.header.subtitle'
|
||||
]()}
|
||||
/>
|
||||
<div
|
||||
className={styles.settingsContainer}
|
||||
|
||||
@@ -40,9 +40,6 @@ export const useGeneralSettingList = (): GeneralSettingList => {
|
||||
const hasPaymentFeature = useLiveData(
|
||||
serverConfigService.serverConfig.features$.map(f => f?.payment)
|
||||
);
|
||||
const isEarlyAccess = useLiveData(
|
||||
userFeatureService.userFeature.isEarlyAccess$
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
userFeatureService.userFeature.revalidate();
|
||||
@@ -86,7 +83,7 @@ export const useGeneralSettingList = (): GeneralSettingList => {
|
||||
}
|
||||
}
|
||||
|
||||
if (isEarlyAccess || runtimeConfig.enableExperimentalFeature) {
|
||||
if (runtimeConfig.enableExperimentalFeature) {
|
||||
settings.push({
|
||||
key: 'experimental-features',
|
||||
title: t['com.affine.settings.workspace.experimental-features'](),
|
||||
|
||||
Reference in New Issue
Block a user