Skip to content
This repository was archived by the owner on Jul 9, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
101243c
initial front end feature flag implementation
pavolumMsft Oct 6, 2020
912d101
Merging latest
pavolumMsft Oct 13, 2020
a1d73da
Initial server implementation
pavolumMsft Oct 16, 2020
a788f58
Converting state to object over array
pavolumMsft Oct 16, 2020
239a45b
Cleaning up dev test changes
pavolumMsft Oct 16, 2020
19a1561
created HOC to abstract feature flag checking for UI features. Change…
pavolumMsft Oct 19, 2020
f65cf8a
Added feature flag value population for hidden feature flags via env …
pavolumMsft Oct 20, 2020
7e24fce
Making PR changes: variable and file name changes, general clean up, …
pavolumMsft Oct 20, 2020
8a4684e
adding format message for feature flag strings
pavolumMsft Oct 20, 2020
bb97679
moving ComposerFeature HOC to client workspace so it is implicitly aw…
pavolumMsft Oct 20, 2020
ecf4f25
changed default feature flags to function for format message, created…
pavolumMsft Oct 21, 2020
76008bf
merging latest
pavolumMsft Oct 21, 2020
08da677
adding selector for feature flag filtered template state, created fea…
pavolumMsft Oct 22, 2020
f1ec8c8
removing unneeded div
pavolumMsft Oct 23, 2020
7f1b773
Adding util functions and reusing hook in HOC
pavolumMsft Oct 23, 2020
45ae235
Updating UI for electron and web view of feature flag toggle per desi…
pavolumMsft Oct 23, 2020
5c4373a
Added feature flag documentation
pavolumMsft Oct 23, 2020
3bf1f67
Merging latest
pavolumMsft Oct 23, 2020
be7db2a
removing unused imports and variables
pavolumMsft Oct 23, 2020
1101fe5
Updated unit tests, added build locale
pavolumMsft Oct 24, 2020
7f5511f
Merge branch 'main' into pavolum/featureFlags
cwhitten Oct 24, 2020
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ describe('<AppSettings /> & <ElectronSettings />', () => {
// there are 2 onboarding texts
getAllByText('Onboarding');
getByText('Property editor preferences');
expect(() => getByText('Application Updates')).toThrow();
getByText('Application Updates');
});

it('should render the electron settings section', () => {
Expand All @@ -47,7 +47,6 @@ describe('<AppSettings /> & <ElectronSettings />', () => {
appLocale: 'en-US',
});
});
getByText('Application Updates');
getByText('Auto update');
getByText('Early adopters');
});
Expand Down
4 changes: 0 additions & 4 deletions Composer/packages/client/config/webpack.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,6 @@ const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPl
const getClientEnvironment = require('./env');
const paths = require('./paths');

new webpack.DefinePlugin({
'process.env.COMPOSER_ENABLE_FORMS': JSON.stringify(process.env.COMPOSER_ENABLE_FORMS),
});

// Source maps are resource heavy and can cause out of memory issue for large source files.
const shouldUseSourceMap = process.env.GENERATE_SOURCEMAP !== 'false';

Expand Down
5 changes: 5 additions & 0 deletions Composer/packages/client/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,15 @@ initializeIcons(undefined, { disableWarnings: true });

export const App: React.FC = () => {
const { appLocale } = useRecoilValue(userSettingsState);
const { fetchFeatureFlags } = useRecoilValue(dispatcherState);
useEffect(() => {
loadLocale(appLocale);
}, [appLocale]);

useEffect(() => {
fetchFeatureFlags();
}, []);

const { fetchExtensions } = useRecoilValue(dispatcherState);

useEffect(() => {
Expand Down
16 changes: 16 additions & 0 deletions Composer/packages/client/src/components/ComposerFeature.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
import { FeatureFlagKey } from '@bfc/shared';
import React, { Fragment } from 'react';

import { useFeatureFlag } from '../utils/hooks';

type ComposerFeatureProps = {
featureFlagKey: FeatureFlagKey;
};

export const ComposerFeature: React.FC<ComposerFeatureProps> = (props) => {
const { featureFlagKey } = props;
const featureIsEnabled = useFeatureFlag(featureFlagKey);
return <Fragment>{featureIsEnabled ? props.children : null}</Fragment>;
};
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import { Sticky, StickyPositionType } from 'office-ui-fabric-react/lib/Sticky';
import { ProjectTemplate } from '@bfc/shared';
import { DialogWrapper, DialogTypes } from '@bfc/ui-shared';
import { NeutralColors } from '@uifabric/fluent-theme';
import { RouteComponentProps } from '@reach/router';

import { DialogCreationCopy, EmptyBotTemplateId, QnABotTemplateId } from '../../constants';

Expand Down Expand Up @@ -103,14 +104,18 @@ const optionKeys = {
};

// -------------------- CreateOptions -------------------- //
type CreateOptionsProps = {
templates: ProjectTemplate[];
onDismiss: () => void;
onNext: (data: string) => void;
} & RouteComponentProps<{}>;

export function CreateOptions(props) {
export function CreateOptions(props: CreateOptionsProps) {
const [option, setOption] = useState(optionKeys.createFromScratch);
const [disabled, setDisabled] = useState(true);
const { templates, onDismiss, onNext } = props;
const [currentTemplate, setCurrentTemplate] = useState('');
const [emptyBotKey, setEmptyBotKey] = useState('');

const selection = useMemo(() => {
return new Selection({
onSelectionChanged: () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,11 @@ import { CreationFlowStatus } from '../../constants';
import {
dispatcherState,
creationFlowStatusState,
templateProjectsState,
storagesState,
focusedStorageFolderState,
currentProjectIdState,
userSettingsState,
filteredTemplatesSelector,
} from '../../recoilModel';
import Home from '../../pages/home/Home';
import { useProjectIdCache } from '../../utils/hooks';
Expand Down Expand Up @@ -48,9 +48,9 @@ const CreationFlow: React.FC<CreationFlowProps> = () => {
fetchProjectById,
} = useRecoilValue(dispatcherState);

const templateProjects = useRecoilValue(filteredTemplatesSelector);
const creationFlowStatus = useRecoilValue(creationFlowStatusState);
const projectId = useRecoilValue(currentProjectIdState);
const templateProjects = useRecoilValue(templateProjectsState);
const storages = useRecoilValue(storagesState);
const focusedStorageFolder = useRecoilValue(focusedStorageFolderState);
const { appLocale } = useRecoilValue(userSettingsState);
Expand Down Expand Up @@ -152,7 +152,7 @@ const CreationFlow: React.FC<CreationFlowProps> = () => {
}
};

const handleCreateNext = async (data) => {
const handleCreateNext = async (data: string) => {
setCreationFlowStatus(CreationFlowStatus.NEW_FROM_TEMPLATE);
navigate(`./create/${data}`);
};
Expand Down
14 changes: 4 additions & 10 deletions Composer/packages/client/src/pages/home/Home.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,8 @@ import { navigate } from '@reach/router';
import { useRecoilValue } from 'recoil';

import { CreationFlowStatus } from '../../constants';
import { dispatcherState, botDisplayNameState } from '../../recoilModel';
import {
recentProjectsState,
templateProjectsState,
templateIdState,
currentProjectIdState,
} from '../../recoilModel/atoms/appState';
import { dispatcherState, botDisplayNameState, filteredTemplatesSelector } from '../../recoilModel';
import { recentProjectsState, templateIdState, currentProjectIdState } from '../../recoilModel/atoms/appState';
import { Toolbar, IToolbarItem } from '../../components/Toolbar';

import * as home from './styles';
Expand Down Expand Up @@ -61,14 +56,14 @@ const tutorials = [
];

const Home: React.FC<RouteComponentProps> = () => {
const templateProjects = useRecoilValue(templateProjectsState);
const projectId = useRecoilValue(currentProjectIdState);
const botName = useRecoilValue(botDisplayNameState(projectId));
const recentProjects = useRecoilValue(recentProjectsState);
const templateId = useRecoilValue(templateIdState);
const { openProject, setCreationFlowStatus, onboardingAddCoachMarkRef, saveTemplateId } = useRecoilValue(
dispatcherState
);
const filteredTemplates = useRecoilValue(filteredTemplatesSelector);

const onItemChosen = async (item) => {
if (item && item.path) {
Expand Down Expand Up @@ -135,7 +130,6 @@ const Home: React.FC<RouteComponentProps> = () => {
disabled: botName ? false : true,
},
];

return (
<div css={home.outline}>
<Toolbar toolbarItems={toolbarItems} />
Expand Down Expand Up @@ -242,7 +236,7 @@ const Home: React.FC<RouteComponentProps> = () => {
"These examples bring together all of the best practices and supporting components we've identified through building of conversational experiences."
)}
</p>
<ExampleList examples={templateProjects} onClick={onClickTemplate} />
<ExampleList examples={filteredTemplates} onClick={onClickTemplate} />
</div>
</div>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { container, section } from './styles';
import { SettingToggle } from './SettingToggle';
import { SettingDropdown } from './SettingDropdown';
import * as images from './images';
import { PreviewFeatureToggle } from './PreviewFeatureToggle';

const ElectronSettings = lazy(() =>
import('./electronSettings').then((module) => ({ default: module.ElectronSettings }))
Expand All @@ -30,7 +31,6 @@ const AppSettings: React.FC<RouteComponentProps> = () => {
const { onboardingSetComplete, updateUserSettings } = useRecoilValue(dispatcherState);
const userSettings = useRecoilValue(userSettingsState);
const { complete } = useRecoilValue(onboardingState);

const onOnboardingChange = useCallback(
(checked: boolean) => {
// on means its not complete
Expand Down Expand Up @@ -155,7 +155,11 @@ const AppSettings: React.FC<RouteComponentProps> = () => {
onChange={onLocaleChange}
/>
</section>
<Suspense fallback={<div />}>{renderElectronSettings && <ElectronSettings />}</Suspense>
<section css={section}>
<h2>{formatMessage('Application Updates')}</h2>
<Suspense fallback={<div />}>{renderElectronSettings && <ElectronSettings />}</Suspense>
<PreviewFeatureToggle />
</section>
</div>
);
};
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

/** @jsx jsx */
import { jsx } from '@emotion/core';
import React from 'react';
import { Checkbox } from 'office-ui-fabric-react/lib/Checkbox';
import { FeatureFlagKey } from '@bfc/shared';

import * as styles from './styles';

type FeatureFlagCheckBoxProps = {
featureFlagKey: FeatureFlagKey;
featureFlagName: string;
description: string;
enabled: boolean;
toggleFeatureFlag: (FeatureFlagKey: FeatureFlagKey, enabled: boolean) => void;
};

const renderLabel = (featureName: string, description: string) => () => (
<span>
<span css={styles.featureFlagTitle}>{`${featureName}.`}</span>
{` ${description}`}
</span>
);

export const FeatureFlagCheckBox: React.FC<FeatureFlagCheckBoxProps> = (props) => {
return (
<Checkbox
checked={props.enabled}
css={styles.featureFlagContainer}
onChange={(e: any, checked?: boolean) => {
if (checked !== undefined) {
props.toggleFeatureFlag(props.featureFlagKey, checked);
}
}}
onRenderLabel={renderLabel(props.featureFlagName, props.description)}
/>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

/** @jsx jsx */
import { jsx } from '@emotion/core';
import { Fragment, useState } from 'react';
import formatMessage from 'format-message';
import { FeatureFlag, FeatureFlagKey } from '@bfc/shared';
import { useRecoilValue } from 'recoil';

import { dispatcherState, featureFlagsState } from '../../../recoilModel';

import { featureFlagGroupContainer } from './styles';
import { SettingToggle } from './SettingToggle';
import * as images from './images';
import { FeatureFlagCheckBox } from './FeatureFlagCheckBox';

export const PreviewFeatureToggle: React.FC = () => {
const featureFlags = useRecoilValue(featureFlagsState);
const { toggleFeatureFlag } = useRecoilValue(dispatcherState);
const [featureFlagVisible, showFeatureFlag] = useState(false);

const renderFeatureFlagOptions = () => {
const result: React.ReactNode[] = [];
Object.keys(featureFlags).forEach((key: string) => {
const featureFlag: FeatureFlag = featureFlags[key];
if (!featureFlag.isHidden) {
result.push(
<FeatureFlagCheckBox
key={key}
description={featureFlag.description}
enabled={featureFlag.enabled}
featureFlagKey={key as FeatureFlagKey}
featureFlagName={featureFlag.displayName}
toggleFeatureFlag={toggleFeatureFlag}
/>
);
}
});
return <div css={featureFlagGroupContainer}>{result}</div>;
};

return (
<Fragment>
<SettingToggle
hideToggle
checked={featureFlagVisible}
description={formatMessage(
'Try new features in preview and help us make Composer better. You can turn them on or off at any time.'
)}
image={images.previewFeatures}
title={formatMessage('Preview features')}
onToggle={(checked: boolean) => {
showFeatureFlag(checked);
}}
/>
{renderFeatureFlagOptions()}
</Fragment>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,11 @@ interface ISettingToggleProps {
image: string;
onToggle: (checked: boolean) => void;
title: string;
hideToggle?: boolean;
}

const SettingToggle: React.FC<ISettingToggleProps> = (props) => {
const { id, title, description, image, checked, onToggle } = props;
const { id, title, description, image, checked, onToggle, hideToggle } = props;
const uniqueId = useId(kebabCase(title));

return (
Expand All @@ -36,7 +37,7 @@ const SettingToggle: React.FC<ISettingToggleProps> = (props) => {
</Label>
<p css={styles.settingsDescription}>{description}</p>
</div>
<div>
{!hideToggle && (
<Toggle
checked={!!checked}
data-testid={id}
Expand All @@ -45,7 +46,7 @@ const SettingToggle: React.FC<ISettingToggleProps> = (props) => {
onChange={(_e, checked) => onToggle(!!checked)}
onText={formatMessage('On')}
/>
</div>
)}
</div>
);
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,11 @@ import formatMessage from 'format-message';
import { Link } from 'office-ui-fabric-react/lib/Link';
import { RouteComponentProps } from '@reach/router';
import { useRecoilValue } from 'recoil';
import { Fragment } from 'react';

import { userSettingsState, dispatcherState } from '../../../recoilModel';

import { link, section } from './styles';
import { link } from './styles';
import { SettingToggle } from './SettingToggle';
import * as images from './images';

Expand All @@ -23,8 +24,7 @@ export const ElectronSettings: React.FC<RouteComponentProps> = () => {
};

return (
<section css={section}>
<h2>{formatMessage('Application Updates')}</h2>
<Fragment>
<SettingToggle
checked={userSettings.appUpdater.autoDownload}
description={formatMessage('Check for updates and install them automatically.')}
Expand Down Expand Up @@ -55,6 +55,6 @@ export const ElectronSettings: React.FC<RouteComponentProps> = () => {
title={formatMessage('Early adopters')}
onToggle={onAppUpdatesChange('useNightly')}
/>
</section>
</Fragment>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,6 @@ import wordWrap from './word-wrap.svg';
import autoUpdate from './auto-update.svg';
import earlyAdopters from './early-adopters.svg';
import language from './language.svg';
import previewFeatures from './preview-features.svg';

export { minimap, onboarding, lineNumbers, wordWrap, autoUpdate, earlyAdopters, language };
export { minimap, onboarding, lineNumbers, wordWrap, autoUpdate, earlyAdopters, language, previewFeatures };
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading