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
8 changed files with 227 additions and 42 deletions

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