mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-04 08:38:34 +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:
@@ -23,7 +23,7 @@
|
|||||||
|
|
||||||
<div align="center">
|
<div align="center">
|
||||||
<a href="https://affine.pro">Home Page</a> |
|
<a href="https://affine.pro">Home Page</a> |
|
||||||
<a href="https://discord.com/invite/yz6tGVsf5p">Discord</a> |
|
<a href="https://discord.gg/whd5mjYqVw">Discord</a> |
|
||||||
<a href="https://app.affine.pro">Live Demo</a> |
|
<a href="https://app.affine.pro">Live Demo</a> |
|
||||||
<a href="https://affine.pro/blog/">Blog</a> |
|
<a href="https://affine.pro/blog/">Blog</a> |
|
||||||
<a href="https://docs.affine.pro/docs/">Documentation</a>
|
<a href="https://docs.affine.pro/docs/">Documentation</a>
|
||||||
|
|||||||
@@ -148,7 +148,7 @@ export const emailTemplate = ({
|
|||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td style="padding: 0 10px">
|
<td style="padding: 0 10px">
|
||||||
<a href="https://discord.gg/Arn7TqJBvG" target="_blank"
|
<a href="https://discord.gg/whd5mjYqVw" target="_blank"
|
||||||
><img
|
><img
|
||||||
src="https://cdn.affine.pro/mail/2023-8-9/Discord.png"
|
src="https://cdn.affine.pro/mail/2023-8-9/Discord.png"
|
||||||
alt="AFFiNE discord link"
|
alt="AFFiNE discord link"
|
||||||
|
|||||||
@@ -91,11 +91,14 @@ export function setupEditorFlags(docCollection: DocCollection) {
|
|||||||
|
|
||||||
// override this flag in app settings
|
// override this flag in app settings
|
||||||
// TODO(@eyhn): need a better way to manage block suite flags
|
// TODO(@eyhn): need a better way to manage block suite flags
|
||||||
docCollection.awarenessStore.setFlag('enable_synced_doc_block', true);
|
Object.entries(blocksuiteFeatureFlags).forEach(([key, value]) => {
|
||||||
docCollection.awarenessStore.setFlag('enable_edgeless_text', true);
|
if (value.defaultState !== undefined) {
|
||||||
docCollection.awarenessStore.setFlag('enable_color_picker', true);
|
docCollection.awarenessStore.setFlag(
|
||||||
docCollection.awarenessStore.setFlag('enable_ai_chat_block', true);
|
key as keyof BlockSuiteFlags,
|
||||||
docCollection.awarenessStore.setFlag('enable_ai_onboarding', true);
|
value.defaultState
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.error('syncEditorFlags', err);
|
logger.error('syncEditorFlags', err);
|
||||||
}
|
}
|
||||||
@@ -140,3 +143,89 @@ 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<K extends string> = 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<keyof BlockSuiteFlags> = {
|
||||||
|
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<keyof AppSetting> = {
|
||||||
|
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,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ export const relatedLinks = [
|
|||||||
{
|
{
|
||||||
icon: <DiscordIcon />,
|
icon: <DiscordIcon />,
|
||||||
title: 'Discord',
|
title: 'Discord',
|
||||||
link: 'https://discord.gg/Arn7TqJBvG',
|
link: 'https://discord.gg/whd5mjYqVw',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: <YouTubeIcon />,
|
icon: <YouTubeIcon />,
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ export const switchRow = style({
|
|||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'space-between',
|
justifyContent: 'space-between',
|
||||||
|
width: '100%',
|
||||||
});
|
});
|
||||||
export const switchDisabled = style({
|
export const switchDisabled = style({
|
||||||
opacity: 0.5,
|
opacity: 0.5,
|
||||||
@@ -64,3 +65,34 @@ export const subHeader = style({
|
|||||||
color: cssVar('textSecondaryColor'),
|
color: cssVar('textSecondaryColor'),
|
||||||
marginBottom: 8,
|
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 { SettingHeader } from '@affine/component/setting-components';
|
||||||
import { useAppSettingHelper } from '@affine/core/hooks/affine/use-app-setting-helper';
|
import { useAppSettingHelper } from '@affine/core/hooks/affine/use-app-setting-helper';
|
||||||
import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks';
|
import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks';
|
||||||
import { useI18n } from '@affine/i18n';
|
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 { useAtom } from 'jotai';
|
||||||
import { atomWithStorage } from 'jotai/utils';
|
import { atomWithStorage } from 'jotai/utils';
|
||||||
import { Suspense, useCallback, useState } from 'react';
|
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 = ({
|
const ExperimentalFeaturesItem = ({
|
||||||
title,
|
title,
|
||||||
|
description,
|
||||||
|
feedbackType,
|
||||||
isMutating,
|
isMutating,
|
||||||
checked,
|
checked,
|
||||||
onChange,
|
onChange,
|
||||||
testId,
|
testId,
|
||||||
}: {
|
}: {
|
||||||
title: React.ReactNode;
|
title: React.ReactNode;
|
||||||
|
description?: React.ReactNode;
|
||||||
|
feedbackType?: FeedbackType;
|
||||||
isMutating?: boolean;
|
isMutating?: boolean;
|
||||||
checked: boolean;
|
checked: boolean;
|
||||||
onChange: (checked: boolean) => void;
|
onChange: (checked: boolean) => void;
|
||||||
testId?: string;
|
testId?: string;
|
||||||
}) => {
|
}) => {
|
||||||
|
const link = feedbackType ? feedbackLink[feedbackType] : undefined;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.switchRow}>
|
<div className={styles.rowContainer}>
|
||||||
{title}
|
<div className={styles.switchRow}>
|
||||||
<Switch
|
{title}
|
||||||
checked={checked}
|
<Switch
|
||||||
onChange={onChange}
|
checked={checked}
|
||||||
className={isMutating ? styles.switchDisabled : ''}
|
onChange={onChange}
|
||||||
data-testid={testId}
|
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>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -110,28 +168,24 @@ const SplitViewSettingRow = () => {
|
|||||||
},
|
},
|
||||||
[updateSettings]
|
[updateSettings]
|
||||||
);
|
);
|
||||||
|
const multiViewFlagConfig = affineFeatureFlags['enableMultiView'];
|
||||||
|
const shouldShow = multiViewFlagConfig?.precondition?.();
|
||||||
|
|
||||||
if (!environment.isDesktop) {
|
if (!multiViewFlagConfig || !shouldShow) {
|
||||||
return null; // only enable on desktop
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ExperimentalFeaturesItem
|
<ExperimentalFeaturesItem
|
||||||
title="Split View"
|
title={multiViewFlagConfig.displayName}
|
||||||
|
description={multiViewFlagConfig.description}
|
||||||
|
feedbackType={multiViewFlagConfig.feedbackType}
|
||||||
checked={appSettings.enableMultiView}
|
checked={appSettings.enableMultiView}
|
||||||
onChange={onToggle}
|
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 BlocksuiteFeatureFlagSettings = () => {
|
||||||
const { appSettings, updateSettings } = useAppSettingHelper();
|
const { appSettings, updateSettings } = useAppSettingHelper();
|
||||||
const toggleSetting = useCallback(
|
const toggleSetting = useCallback(
|
||||||
@@ -148,16 +202,25 @@ const BlocksuiteFeatureFlagSettings = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{Object.entries(blocksuiteFeatureFlags).map(([flag, displayName]) => (
|
{Object.entries(blocksuiteFeatureFlags).map(([key, value]) => {
|
||||||
<ExperimentalFeaturesItem
|
const hidden = value.precondition && !value.precondition();
|
||||||
key={flag}
|
|
||||||
title={'Block Suite: ' + displayName}
|
if (hidden) {
|
||||||
checked={!!appSettings.editorFlags?.[flag as EditorFlag]}
|
return null;
|
||||||
onChange={checked =>
|
}
|
||||||
toggleSetting(flag as keyof BlockSuiteFlags, checked)
|
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[
|
title={t[
|
||||||
'com.affine.settings.workspace.experimental-features.header.plugins'
|
'com.affine.settings.workspace.experimental-features.header.plugins'
|
||||||
]()}
|
]()}
|
||||||
|
subtitle={t[
|
||||||
|
'com.affine.settings.workspace.experimental-features.header.subtitle'
|
||||||
|
]()}
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
className={styles.settingsContainer}
|
className={styles.settingsContainer}
|
||||||
|
|||||||
@@ -40,9 +40,6 @@ export const useGeneralSettingList = (): GeneralSettingList => {
|
|||||||
const hasPaymentFeature = useLiveData(
|
const hasPaymentFeature = useLiveData(
|
||||||
serverConfigService.serverConfig.features$.map(f => f?.payment)
|
serverConfigService.serverConfig.features$.map(f => f?.payment)
|
||||||
);
|
);
|
||||||
const isEarlyAccess = useLiveData(
|
|
||||||
userFeatureService.userFeature.isEarlyAccess$
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
userFeatureService.userFeature.revalidate();
|
userFeatureService.userFeature.revalidate();
|
||||||
@@ -86,7 +83,7 @@ export const useGeneralSettingList = (): GeneralSettingList => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isEarlyAccess || runtimeConfig.enableExperimentalFeature) {
|
if (runtimeConfig.enableExperimentalFeature) {
|
||||||
settings.push({
|
settings.push({
|
||||||
key: 'experimental-features',
|
key: 'experimental-features',
|
||||||
title: t['com.affine.settings.workspace.experimental-features'](),
|
title: t['com.affine.settings.workspace.experimental-features'](),
|
||||||
|
|||||||
@@ -1247,6 +1247,7 @@
|
|||||||
"com.affine.settings.workspace.experimental-features": "Experimental features",
|
"com.affine.settings.workspace.experimental-features": "Experimental features",
|
||||||
"com.affine.settings.workspace.experimental-features.get-started": "Get started",
|
"com.affine.settings.workspace.experimental-features.get-started": "Get started",
|
||||||
"com.affine.settings.workspace.experimental-features.header.plugins": "Experimental features",
|
"com.affine.settings.workspace.experimental-features.header.plugins": "Experimental features",
|
||||||
|
"com.affine.settings.workspace.experimental-features.header.subtitle": "You can customize your workspace here.",
|
||||||
"com.affine.settings.workspace.experimental-features.prompt-disclaimer": "I am aware of the risks, and I am willing to continue to use it.",
|
"com.affine.settings.workspace.experimental-features.prompt-disclaimer": "I am aware of the risks, and I am willing to continue to use it.",
|
||||||
"com.affine.settings.workspace.experimental-features.prompt-header": "Do you want to use the plugin system that is in an experimental stage?",
|
"com.affine.settings.workspace.experimental-features.prompt-header": "Do you want to use the plugin system that is in an experimental stage?",
|
||||||
"com.affine.settings.workspace.experimental-features.prompt-warning": "You are about to enable an experimental feature. This feature is still in development and may contain errors or behave unpredictably. Please proceed with caution and at your own risk.",
|
"com.affine.settings.workspace.experimental-features.prompt-warning": "You are about to enable an experimental feature. This feature is still in development and may contain errors or behave unpredictably. Please proceed with caution and at your own risk.",
|
||||||
|
|||||||
Reference in New Issue
Block a user