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,
  },
  ...
}

```

![CleanShot 2024-08-02 at 12 26 36@2x](https://github.com/user-attachments/assets/98b1e8e7-cd8b-4309-8063-323b2f3b5a94)
This commit is contained in:
JimmFly 2024-08-13 02:26:05 +00:00
parent 6228b27271
commit 9037e6695e
No known key found for this signature in database
GPG Key ID: 126E0320FEB0D05C
8 changed files with 227 additions and 42 deletions

View File

@ -23,7 +23,7 @@
<div align="center">
<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://affine.pro/blog/">Blog</a> |
<a href="https://docs.affine.pro/docs/">Documentation</a>

View File

@ -148,7 +148,7 @@ export const emailTemplate = ({
</a>
</td>
<td style="padding: 0 10px">
<a href="https://discord.gg/Arn7TqJBvG" target="_blank"
<a href="https://discord.gg/whd5mjYqVw" target="_blank"
><img
src="https://cdn.affine.pro/mail/2023-8-9/Discord.png"
alt="AFFiNE discord link"

View File

@ -91,11 +91,14 @@ export function setupEditorFlags(docCollection: DocCollection) {
// override this flag in app settings
// TODO(@eyhn): need a better way to manage block suite flags
docCollection.awarenessStore.setFlag('enable_synced_doc_block', true);
docCollection.awarenessStore.setFlag('enable_edgeless_text', true);
docCollection.awarenessStore.setFlag('enable_color_picker', true);
docCollection.awarenessStore.setFlag('enable_ai_chat_block', true);
docCollection.awarenessStore.setFlag('enable_ai_onboarding', true);
Object.entries(blocksuiteFeatureFlags).forEach(([key, value]) => {
if (value.defaultState !== undefined) {
docCollection.awarenessStore.setFlag(
key as keyof BlockSuiteFlags,
value.defaultState
);
}
});
} catch (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,
},
};

View File

@ -21,7 +21,7 @@ export const relatedLinks = [
{
icon: <DiscordIcon />,
title: 'Discord',
link: 'https://discord.gg/Arn7TqJBvG',
link: 'https://discord.gg/whd5mjYqVw',
},
{
icon: <YouTubeIcon />,

View File

@ -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,
});

View File

@ -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}

View File

@ -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'](),

View File

@ -1247,6 +1247,7 @@
"com.affine.settings.workspace.experimental-features": "Experimental features",
"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.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-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.",