1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-02-09 00:18:00 +01:00

Feat: clone feature toggle configuration (#201)

Create a new feature toggle by cloning the config of an
existing feature toggle.

This feature alos moves away from the input store for the
"create feature toggle form".
This commit is contained in:
Ivar Conradi Østhus 2020-01-09 22:51:05 +01:00 committed by GitHub
parent ff8ce52347
commit 19443c651f
20 changed files with 428 additions and 120 deletions

View File

@ -2,7 +2,8 @@
"presets": ["@babel/preset-env", "@babel/preset-react"], "presets": ["@babel/preset-env", "@babel/preset-react"],
"plugins": [ "plugins": [
["@babel/plugin-proposal-decorators", { "legacy": true }], ["@babel/plugin-proposal-decorators", { "legacy": true }],
"@babel/plugin-proposal-class-properties" "@babel/plugin-proposal-class-properties",
"@babel/plugin-transform-runtime"
], ],
"env": { "env": {
"test": { "test": {

View File

@ -44,8 +44,10 @@
"@babel/plugin-proposal-class-properties": "^7.5.5", "@babel/plugin-proposal-class-properties": "^7.5.5",
"@babel/plugin-proposal-decorators": "^7.6.0", "@babel/plugin-proposal-decorators": "^7.6.0",
"@babel/plugin-transform-modules-commonjs": "^7.6.0", "@babel/plugin-transform-modules-commonjs": "^7.6.0",
"@babel/plugin-transform-runtime": "^7.7.6",
"@babel/preset-env": "^7.6.2", "@babel/preset-env": "^7.6.2",
"@babel/preset-react": "^7.0.0", "@babel/preset-react": "^7.0.0",
"array-move": "^2.2.1",
"babel-eslint": "^10.0.3", "babel-eslint": "^10.0.3",
"babel-jest": "^24.9.0", "babel-jest": "^24.9.0",
"babel-loader": "^8.0.6", "babel-loader": "^8.0.6",

View File

@ -19,6 +19,7 @@ exports[`renders correctly with one feature 1`] = `
} }
> >
Another Another
</react-mdl-CardTitle> </react-mdl-CardTitle>
<react-mdl-CardText> <react-mdl-CardText>
another's description another's description
@ -56,7 +57,24 @@ exports[`renders correctly with one feature 1`] = `
Disabled Disabled
</react-mdl-Switch> </react-mdl-Switch>
</span> </span>
<div>
<a
href="/features/copy/Another"
onClick={[Function]}
title="Create new feature toggle by cloning configuration"
>
<react-mdl-Button <react-mdl-Button
style={
Object {
"flexShrink": 0,
}
}
>
Clone
</react-mdl-Button>
</a>
<react-mdl-Button
accent={true}
disabled={false} disabled={false}
onClick={[Function]} onClick={[Function]}
style={ style={
@ -64,9 +82,11 @@ exports[`renders correctly with one feature 1`] = `
"flexShrink": 0, "flexShrink": 0,
} }
} }
title="Archive feature toggle"
> >
Archive Archive
</react-mdl-Button> </react-mdl-Button>
</div>
</react-mdl-CardActions> </react-mdl-CardActions>
<hr /> <hr />
<react-mdl-Tabs <react-mdl-Tabs

View File

@ -18,9 +18,11 @@ exports[`render the create feature page 1`] = `
} }
} }
> >
Create feature toggle Create new feature toggle
</react-mdl-CardTitle> </react-mdl-CardTitle>
<form> <form
onSubmit={[MockFunction]}
>
<section <section
style={ style={
Object { Object {
@ -29,20 +31,17 @@ exports[`render the create feature page 1`] = `
} }
> >
<react-mdl-Textfield <react-mdl-Textfield
error={Object {}}
floatingLabel={true} floatingLabel={true}
label="Name" label="Name"
name="name" name="name"
onBlur={[Function]} onBlur={[Function]}
onChange={[Function]} onChange={[Function]}
required={true}
value="feature" value="feature"
/> />
<react-mdl-Textfield <react-mdl-Textfield
floatingLabel={true} floatingLabel={true}
label="Description" label="Description"
onChange={[Function]} onChange={[Function]}
required={true}
rows={1} rows={1}
style={ style={
Object { Object {
@ -59,11 +58,13 @@ exports[`render the create feature page 1`] = `
> >
Enabled Enabled
</react-mdl-Switch> </react-mdl-Switch>
<hr /> <br />
<br />
</div> </div>
<StrategiesSection <StrategiesSection
addStrategy={[MockFunction]} addStrategy={[MockFunction]}
configuredStrategies={Array []} configuredStrategies={Array []}
featureToggleName="feature"
moveStrategy={[MockFunction]} moveStrategy={[MockFunction]}
removeStrategy={[MockFunction]} removeStrategy={[MockFunction]}
updateStrategy={[MockFunction]} updateStrategy={[MockFunction]}

View File

@ -8,13 +8,14 @@ jest.mock('../strategies-section-container', () => 'StrategiesSection');
it('render the create feature page', () => { it('render the create feature page', () => {
let input = { let input = {
name: 'feature', name: 'feature',
nameError: {},
description: 'Description', description: 'Description',
enabled: false, enabled: false,
}; };
let errors = {};
const tree = shallow( const tree = shallow(
<AddFeatureComponent <AddFeatureComponent
input={input} input={input}
errors={errors}
onSubmit={jest.fn()} onSubmit={jest.fn()}
setValue={jest.fn()} setValue={jest.fn()}
addStrategy={jest.fn()} addStrategy={jest.fn()}
@ -32,10 +33,10 @@ it('render the create feature page', () => {
let input = { let input = {
name: 'feature', name: 'feature',
nameError: {},
description: 'Description', description: 'Description',
enabled: false, enabled: false,
}; };
let errors = {};
let validateName = jest.fn(); let validateName = jest.fn();
let setValue = jest.fn(); let setValue = jest.fn();
@ -56,6 +57,7 @@ let eventMock = {
const buildComponent = (setValue, validateName) => ( const buildComponent = (setValue, validateName) => (
<AddFeatureComponent <AddFeatureComponent
input={input} input={input}
errors={errors}
onSubmit={onSubmit} onSubmit={onSubmit}
setValue={setValue} setValue={setValue}
addStrategy={addStrategy} addStrategy={addStrategy}

View File

@ -8,7 +8,7 @@ jest.mock('../strategies-section-container', () => 'StrategiesSection');
it('render the create feature page', () => { it('render the create feature page', () => {
let input = { let input = {
name: 'feature', name: 'feature',
nameError: {}, errors: {},
description: 'Description', description: 'Description',
enabled: false, enabled: false,
}; };

View File

@ -5,27 +5,18 @@ import StrategiesSection from './strategies-section-container';
import { FormButtons } from './../../common'; import { FormButtons } from './../../common';
import { styles as commonStyles } from '../../common'; import { styles as commonStyles } from '../../common';
import { trim } from './util';
const trim = value => {
if (value && value.trim) {
return value.trim();
} else {
return value;
}
};
class AddFeatureComponent extends Component { class AddFeatureComponent extends Component {
// static displayName = `AddFeatureComponent-${getDisplayName(Component)}`; // static displayName = `AddFeatureComponent-${getDisplayName(Component)}`;
componentWillMount() { componentDidMount() {
// TODO unwind this stuff window.onbeforeunload = () => 'Data will be lost if you leave the page, are you sure?';
if (this.props.initCallRequired === true) {
this.props.init(this.props.input);
}
} }
render() { render() {
const { const {
input, input,
errors,
setValue, setValue,
validateName, validateName,
addStrategy, addStrategy,
@ -36,26 +27,19 @@ class AddFeatureComponent extends Component {
onCancel, onCancel,
} = this.props; } = this.props;
const {
name, // eslint-disable-line
nameError,
description,
enabled,
} = input;
const configuredStrategies = input.strategies || []; const configuredStrategies = input.strategies || [];
return ( return (
<Card shadow={0} className={commonStyles.fullwidth} style={{ overflow: 'visible' }}> <Card shadow={0} className={commonStyles.fullwidth} style={{ overflow: 'visible' }}>
<CardTitle style={{ paddingTop: '24px', wordBreak: 'break-all' }}>Create feature toggle</CardTitle> <CardTitle style={{ paddingTop: '24px', wordBreak: 'break-all' }}>Create new feature toggle</CardTitle>
<form onSubmit={onSubmit(input)}> <form onSubmit={onSubmit}>
<section style={{ padding: '16px' }}> <section style={{ padding: '16px' }}>
<Textfield <Textfield
floatingLabel floatingLabel
label="Name" label="Name"
name="name" name="name"
required value={input.name}
value={name} error={errors.name}
error={nameError}
onBlur={v => validateName(v.target.value)} onBlur={v => validateName(v.target.value)}
onChange={v => setValue('name', trim(v.target.value))} onChange={v => setValue('name', trim(v.target.value))}
/> />
@ -64,30 +48,34 @@ class AddFeatureComponent extends Component {
style={{ width: '100%' }} style={{ width: '100%' }}
rows={1} rows={1}
label="Description" label="Description"
required error={errors.description}
value={description} value={input.description}
onChange={v => setValue('description', v.target.value)} onChange={v => setValue('description', v.target.value)}
/> />
<div> <div>
<br /> <br />
<Switch <Switch
checked={enabled} checked={input.enabled}
onChange={() => { onChange={() => {
setValue('enabled', !enabled); setValue('enabled', !input.enabled);
}} }}
> >
Enabled Enabled
</Switch> </Switch>
<hr /> <br />
<br />
</div> </div>
{input.name ? (
<StrategiesSection <StrategiesSection
configuredStrategies={configuredStrategies} configuredStrategies={configuredStrategies}
featureToggleName={input.name}
addStrategy={addStrategy} addStrategy={addStrategy}
updateStrategy={updateStrategy} updateStrategy={updateStrategy}
moveStrategy={moveStrategy} moveStrategy={moveStrategy}
removeStrategy={removeStrategy} removeStrategy={removeStrategy}
/> />
) : null}
<br /> <br />
</section> </section>
@ -102,6 +90,7 @@ class AddFeatureComponent extends Component {
AddFeatureComponent.propTypes = { AddFeatureComponent.propTypes = {
input: PropTypes.object, input: PropTypes.object,
errors: PropTypes.object,
setValue: PropTypes.func.isRequired, setValue: PropTypes.func.isRequired,
addStrategy: PropTypes.func.isRequired, addStrategy: PropTypes.func.isRequired,
removeStrategy: PropTypes.func.isRequired, removeStrategy: PropTypes.func.isRequired,

View File

@ -1,78 +1,122 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import arrayMove from 'array-move';
import { createFeatureToggles, validateName } from './../../../store/feature-actions'; import { createFeatureToggles, validateName } from './../../../store/feature-actions';
import { createMapper, createActions } from './../../input-helpers';
import AddFeatureComponent from './form-add-feature-component'; import AddFeatureComponent from './form-add-feature-component';
const defaultStrategy = { name: 'default' }; const defaultStrategy = { name: 'default' };
const ID = 'add-feature-toggle'; class WrapperComponent extends Component {
const mapStateToProps = createMapper({ constructor() {
id: ID, super();
getDefault() { this.state = {
let name; featureToggle: { strategies: [], enabled: true },
try { errors: {},
[, name] = document.location.hash.match(/name=([a-z0-9-_.]+)/i); dirty: false,
} catch (e) { };
// hide error
} }
return { name };
},
});
const prepare = (methods, dispatch, ownProps) => {
methods.onSubmit = input => e => {
e.preventDefault();
input.createdAt = new Date(); setValue = (field, value) => {
const { featureToggle } = this.state;
featureToggle[field] = value;
this.setState({ featureToggle, dirty: true });
};
if (Array.isArray(input.strategies)) { validateName = async featureToggleName => {
input.strategies.forEach(s => { const { errors } = this.state;
try {
await validateName(featureToggleName);
errors.name = undefined;
} catch (err) {
errors.name = err.message;
}
this.setState({ errors });
};
addStrategy = strat => {
strat.id = Math.round(Math.random() * 10000000);
const { featureToggle } = this.state;
const strategies = featureToggle.strategies.concat(strat);
featureToggle.strategies = strategies;
this.setState({ featureToggle, dirty: true });
};
moveStrategy = (index, toIndex) => {
const { featureToggle } = this.state;
const strategies = arrayMove(featureToggle.strategies, index, toIndex);
featureToggle.strategies = strategies;
this.setState({ featureToggle, dirty: true });
};
removeStrategy = index => {
const { featureToggle } = this.state;
const strategies = featureToggle.strategies.filter((_, i) => i !== index);
featureToggle.strategies = strategies;
this.setState({ featureToggle, dirty: true });
};
updateStrategy = (index, strat) => {
const { featureToggle } = this.state;
const strategies = featureToggle.strategies.concat();
strategies[index] = strat;
featureToggle.strategies = strategies;
this.setState({ featureToggle, dirty: true });
};
onSubmit = evt => {
evt.preventDefault();
const { createFeatureToggles, history } = this.props;
const { featureToggle } = this.state;
featureToggle.createdAt = new Date();
if (Array.isArray(featureToggle.strategies)) {
featureToggle.strategies.forEach(s => {
delete s.id; delete s.id;
}); });
} else { } else {
input.strategies = [defaultStrategy]; featureToggle.strategies = [defaultStrategy];
} }
createFeatureToggles(input)(dispatch) createFeatureToggles(featureToggle).then(() => history.push(`/features/strategies/${featureToggle.name}`));
.then(() => methods.clear())
.then(() => ownProps.history.push(`/features/strategies/${input.name}`));
}; };
methods.onCancel = evt => { onCancel = evt => {
evt.preventDefault(); evt.preventDefault();
methods.clear(); this.props.history.push('/features');
ownProps.history.push('/features');
}; };
methods.addStrategy = v => { render() {
v.id = Math.round(Math.random() * 10000000); return (
methods.pushToList('strategies', v); <AddFeatureComponent
}; onSubmit={this.onSubmit}
onCancel={this.onCancel}
methods.updateStrategy = (index, n) => { addStrategy={this.addStrategy}
methods.updateInList('strategies', index, n); updateStrategy={this.updateStrategy}
}; removeStrategy={this.removeStrategy}
moveStrategy={this.moveStrategy}
methods.moveStrategy = (index, toIndex) => { validateName={this.validateName}
methods.moveItem('strategies', index, toIndex); setValue={this.setValue}
}; input={this.state.featureToggle}
errors={this.state.errors}
methods.removeStrategy = index => { />
methods.removeFromList('strategies', index); );
}; }
}
methods.validateName = featureToggleName => { WrapperComponent.propTypes = {
validateName(featureToggleName) history: PropTypes.object.isRequired,
.then(() => methods.setValue('nameError', undefined)) createFeatureToggles: PropTypes.func.isRequired,
.catch(err => methods.setValue('nameError', err.message));
};
return methods;
}; };
const actions = createActions({ id: ID, prepare });
const mapDispatchToProps = dispatch => ({
validateName: name => validateName(name)(dispatch),
createFeatureToggles: featureToggle => createFeatureToggles(featureToggle)(dispatch),
});
const FormAddContainer = connect( const FormAddContainer = connect(
mapStateToProps, () => {},
actions mapDispatchToProps
)(AddFeatureComponent); )(WrapperComponent);
export default FormAddContainer; export default FormAddContainer;

View File

@ -0,0 +1,139 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { Link } from 'react-router-dom';
import { Button, Icon, Textfield, Checkbox, Card, CardTitle, CardActions } from 'react-mdl';
import { styles as commonStyles } from '../../common';
import { trim } from './util';
class CopyFeatureComponent extends Component {
// static displayName = `AddFeatureComponent-${getDisplayName(Component)}`;
constructor() {
super();
this.state = { newToggleName: '', replaceGroupId: true };
}
componentWillMount() {
// TODO unwind this stuff
if (this.props.copyToggle) {
this.setState({ featureToggle: this.props.copyToggle });
}
}
componentDidMount() {
if (this.props.copyToggle) {
this.refs.name.inputRef.focus();
} else {
this.props.fetchFeatureToggles();
}
}
setValue = evt => {
const value = trim(evt.target.value);
this.setState({ newToggleName: value });
};
toggleReplaceGroupId = () => {
const { replaceGroupId } = !!this.state;
this.setState({ replaceGroupId });
};
onValidateName = async () => {
const { newToggleName } = this.state;
try {
await this.props.validateName(newToggleName);
this.setState({ nameError: undefined });
} catch (err) {
this.setState({ nameError: err.message });
}
};
onSubmit = evt => {
evt.preventDefault();
const { nameError, newToggleName, replaceGroupId } = this.state;
if (nameError) {
return;
}
const { copyToggle, history } = this.props;
copyToggle.name = newToggleName;
if (replaceGroupId) {
copyToggle.strategies.forEach(s => {
if (s.parameters.groupId) {
s.parameters.groupId = newToggleName;
}
});
}
this.props.createFeatureToggle(copyToggle).then(() => history.push(`/features/strategies/${copyToggle.name}`));
};
render() {
const { copyToggle } = this.props;
if (!copyToggle) return <span>Toggle not found</span>;
const { newToggleName, nameError, replaceGroupId } = this.state;
return (
<Card shadow={0} className={commonStyles.fullwidth} style={{ overflow: 'visible' }}>
<CardTitle style={{ paddingTop: '24px', wordBreak: 'break-all' }}>
Copy&nbsp;{copyToggle.name}
</CardTitle>
<form onSubmit={this.onSubmit}>
<section style={{ padding: '16px' }}>
<p>
You are about to create a new feature toggle by cloning the configuration of feature
toggle&nbsp;
<Link to={`/features/strategies/${copyToggle.name}`}>{copyToggle.name}</Link>. You must give
the new feature toggle a unique name before you can proceed.
</p>
<Textfield
floatingLabel
label="Feature toggle name"
name="name"
value={newToggleName}
error={nameError}
onBlur={this.onValidateName}
onChange={this.setValue}
ref="name"
/>
<br />
<br />
<Checkbox
checked={replaceGroupId}
label="Replace groupId"
onChange={this.toggleReplaceGroupId}
/>
<br />
</section>
<CardActions>
<Button type="submit" ripple raised primary>
<Icon name="file_copy" />
&nbsp;&nbsp;&nbsp; Copy feature toggle
</Button>
<br />
</CardActions>
</form>
</Card>
);
}
}
CopyFeatureComponent.propTypes = {
copyToggle: PropTypes.object,
history: PropTypes.object.isRequired,
createFeatureToggle: PropTypes.func.isRequired,
fetchFeatureToggles: PropTypes.func.isRequired,
validateName: PropTypes.func.isRequired,
};
export default CopyFeatureComponent;

View File

@ -0,0 +1,21 @@
import { connect } from 'react-redux';
import CopyFeatureComponent from './form-copy-feature-component';
import { createFeatureToggles, validateName, fetchFeatureToggles } from './../../../store/feature-actions';
const mapStateToProps = (state, props) => ({
history: props.history,
copyToggle: state.features.toJS().find(toggle => toggle.name === props.copyToggleName),
});
const mapDispatchToProps = dispatch => ({
validateName,
createFeatureToggle: featureToggle => createFeatureToggles(featureToggle)(dispatch),
fetchFeatureToggles: () => fetchFeatureToggles()(dispatch),
});
const FormAddContainer = connect(
mapStateToProps,
mapDispatchToProps
)(CopyFeatureComponent);
export default FormAddContainer;

View File

@ -3,8 +3,9 @@ import StrategiesSectionComponent from './strategies-section';
import { fetchStrategies } from '../../../store/strategy/actions'; import { fetchStrategies } from '../../../store/strategy/actions';
const StrategiesSection = connect( const StrategiesSection = connect(
state => ({ (state, ownProps) => ({
strategies: state.strategies.get('list').toArray(), strategies: state.strategies.get('list').toArray(),
configuredStrategies: ownProps.configuredStrategies,
}), }),
{ fetchStrategies } { fetchStrategies }
)(StrategiesSectionComponent); )(StrategiesSectionComponent);

View File

@ -0,0 +1,7 @@
export const trim = value => {
if (value && value.trim) {
return value.trim();
} else {
return value;
}
};

View File

@ -147,7 +147,7 @@ export default class ViewFeatureToggleComponent extends React.Component {
return ( return (
<Card shadow={0} className={commonStyles.fullwidth} style={{ overflow: 'visible' }}> <Card shadow={0} className={commonStyles.fullwidth} style={{ overflow: 'visible' }}>
<CardTitle style={{ wordBreak: 'break-all', paddingBottom: 0 }}>{featureToggle.name}</CardTitle> <CardTitle style={{ wordBreak: 'break-all', paddingBottom: 0 }}>{featureToggle.name} </CardTitle>
<UpdateDescriptionComponent <UpdateDescriptionComponent
isFeatureView={this.isFeatureView} isFeatureView={this.isFeatureView}
description={featureToggle.description} description={featureToggle.description}
@ -181,13 +181,23 @@ export default class ViewFeatureToggleComponent extends React.Component {
</span> </span>
{this.isFeatureView ? ( {this.isFeatureView ? (
<div>
<Link
to={`/features/copy/${featureToggle.name}`}
title="Create new feature toggle by cloning configuration"
>
<Button style={{ flexShrink: 0 }}>Clone</Button>
</Link>
<Button <Button
disabled={!hasPermission(DELETE_FEATURE)} disabled={!hasPermission(DELETE_FEATURE)}
onClick={removeToggle} onClick={removeToggle}
title="Archive feature toggle"
accent
style={{ flexShrink: 0 }} style={{ flexShrink: 0 }}
> >
Archive Archive
</Button> </Button>
</div>
) : ( ) : (
<Button <Button
disabled={!hasPermission(UPDATE_FEATURE)} disabled={!hasPermission(UPDATE_FEATURE)}

View File

@ -49,6 +49,12 @@ Array [
"path": "/features/create", "path": "/features/create",
"title": "Create", "title": "Create",
}, },
Object {
"component": [Function],
"parent": "/features",
"path": "/features/copy/:copyToggle",
"title": "Copy",
},
Object { Object {
"component": [Function], "component": [Function],
"parent": "/features", "parent": "/features",

View File

@ -1,7 +1,7 @@
import { routes, baseRoutes, getRoute } from '../routes'; import { routes, baseRoutes, getRoute } from '../routes';
test('returns all defined routes', () => { test('returns all defined routes', () => {
expect(routes.length).toEqual(13); expect(routes.length).toEqual(14);
expect(routes).toMatchSnapshot(); expect(routes).toMatchSnapshot();
}); });

View File

@ -1,4 +1,5 @@
import CreateFeatureToggle from '../../page/features/create'; import CreateFeatureToggle from '../../page/features/create';
import CopyFeatureToggle from '../../page/features/copy';
import ViewFeatureToggle from '../../page/features/show'; import ViewFeatureToggle from '../../page/features/show';
import Features from '../../page/features'; import Features from '../../page/features';
import CreateStrategies from '../../page/strategies/create'; import CreateStrategies from '../../page/strategies/create';
@ -15,6 +16,7 @@ import LogoutFeatures from '../../page/user/logout';
export const routes = [ export const routes = [
// Features // Features
{ path: '/features/create', parent: '/features', title: 'Create', component: CreateFeatureToggle }, { path: '/features/create', parent: '/features', title: 'Create', component: CreateFeatureToggle },
{ path: '/features/copy/:copyToggle', parent: '/features', title: 'Copy', component: CopyFeatureToggle },
{ path: '/features/:activeTab/:name', parent: '/features', title: ':name', component: ViewFeatureToggle }, { path: '/features/:activeTab/:name', parent: '/features', title: ':name', component: ViewFeatureToggle },
{ path: '/features', title: 'Feature Toggles', icon: 'list', component: Features }, { path: '/features', title: 'Feature Toggles', icon: 'list', component: Features },

View File

@ -0,0 +1,14 @@
import React from 'react';
import CopyFeatureToggleForm from '../../component/feature/form/form-copy-feature-container';
import PropTypes from 'prop-types';
const render = ({ history, match: { params } }) => (
<CopyFeatureToggleForm title="Copy feature toggle" history={history} copyToggleName={params.copyToggle} />
);
render.propTypes = {
history: PropTypes.object.isRequired,
match: PropTypes.object.isRequired,
};
export default render;

View File

@ -4,6 +4,7 @@ import { dispatchAndThrow } from './util';
import { MUTE_ERROR } from './error-actions'; import { MUTE_ERROR } from './error-actions';
export const ADD_FEATURE_TOGGLE = 'ADD_FEATURE_TOGGLE'; export const ADD_FEATURE_TOGGLE = 'ADD_FEATURE_TOGGLE';
export const COPY_FEATURE_TOGGLE = 'COPY_FEATURE_TOGGLE';
export const REMOVE_FEATURE_TOGGLE = 'REMOVE_FEATURE_TOGGLE'; export const REMOVE_FEATURE_TOGGLE = 'REMOVE_FEATURE_TOGGLE';
export const UPDATE_FEATURE_TOGGLE = 'UPDATE_FEATURE_TOGGLE'; export const UPDATE_FEATURE_TOGGLE = 'UPDATE_FEATURE_TOGGLE';
export const TOGGLE_FEATURE_TOGGLE = 'TOGGLE_FEATURE_TOGGLE'; export const TOGGLE_FEATURE_TOGGLE = 'TOGGLE_FEATURE_TOGGLE';

View File

@ -5,6 +5,7 @@ import {
ADD_FEATURE_TOGGLE, ADD_FEATURE_TOGGLE,
RECEIVE_FEATURE_TOGGLES, RECEIVE_FEATURE_TOGGLES,
UPDATE_FEATURE_TOGGLE, UPDATE_FEATURE_TOGGLE,
UPDATE_FEATURE_TOGGLE_STRATEGIES,
REMOVE_FEATURE_TOGGLE, REMOVE_FEATURE_TOGGLE,
TOGGLE_FEATURE_TOGGLE, TOGGLE_FEATURE_TOGGLE,
} from './feature-actions'; } from './feature-actions';
@ -28,6 +29,15 @@ const features = (state = new List([]), action) => {
return toggle; return toggle;
} }
}); });
case UPDATE_FEATURE_TOGGLE_STRATEGIES:
debug(UPDATE_FEATURE_TOGGLE_STRATEGIES, action);
return state.map(toggle => {
if (toggle.get('name') === action.featureToggle.name) {
return new $Map(action.featureToggle);
} else {
return toggle;
}
});
case UPDATE_FEATURE_TOGGLE: case UPDATE_FEATURE_TOGGLE:
debug(UPDATE_FEATURE_TOGGLE, action); debug(UPDATE_FEATURE_TOGGLE, action);
return state.map(toggle => { return state.map(toggle => {

View File

@ -137,6 +137,13 @@
dependencies: dependencies:
"@babel/types" "^7.0.0" "@babel/types" "^7.0.0"
"@babel/helper-module-imports@^7.7.4":
version "7.7.4"
resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.7.4.tgz#e5a92529f8888bf319a6376abfbd1cebc491ad91"
integrity sha512-dGcrX6K9l8258WFjyDLJwuVKxR4XZfU0/vTUgOQYWEnRD8mgr+p4d6fCUMq/ys0h4CCt/S5JhbvtyErjWouAUQ==
dependencies:
"@babel/types" "^7.7.4"
"@babel/helper-module-transforms@^7.1.0", "@babel/helper-module-transforms@^7.4.4": "@babel/helper-module-transforms@^7.1.0", "@babel/helper-module-transforms@^7.4.4":
version "7.5.5" version "7.5.5"
resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.5.5.tgz#f84ff8a09038dcbca1fd4355661a500937165b4a" resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.5.5.tgz#f84ff8a09038dcbca1fd4355661a500937165b4a"
@ -585,6 +592,16 @@
dependencies: dependencies:
"@babel/helper-plugin-utils" "^7.0.0" "@babel/helper-plugin-utils" "^7.0.0"
"@babel/plugin-transform-runtime@^7.7.6":
version "7.7.6"
resolved "https://registry.yarnpkg.com/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.7.6.tgz#4f2b548c88922fb98ec1c242afd4733ee3e12f61"
integrity sha512-tajQY+YmXR7JjTwRvwL4HePqoL3DYxpYXIHKVvrOIvJmeHe2y1w4tz5qz9ObUDC9m76rCzIMPyn4eERuwA4a4A==
dependencies:
"@babel/helper-module-imports" "^7.7.4"
"@babel/helper-plugin-utils" "^7.0.0"
resolve "^1.8.1"
semver "^5.5.1"
"@babel/plugin-transform-shorthand-properties@^7.2.0": "@babel/plugin-transform-shorthand-properties@^7.2.0":
version "7.2.0" version "7.2.0"
resolved "https://registry.yarnpkg.com/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.2.0.tgz#6333aee2f8d6ee7e28615457298934a3b46198f0" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.2.0.tgz#6333aee2f8d6ee7e28615457298934a3b46198f0"
@ -738,6 +755,15 @@
lodash "^4.17.13" lodash "^4.17.13"
to-fast-properties "^2.0.0" to-fast-properties "^2.0.0"
"@babel/types@^7.7.4":
version "7.7.4"
resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.7.4.tgz#516570d539e44ddf308c07569c258ff94fde9193"
integrity sha512-cz5Ji23KCi4T+YIE/BolWosrJuSmoZeN1EFnRtBwF+KKLi8GG/Z2c2hOJJeCXPk4mwk4QFvTmwIodJowXgttRA==
dependencies:
esutils "^2.0.2"
lodash "^4.17.13"
to-fast-properties "^2.0.0"
"@cnakazawa/watch@^1.0.3": "@cnakazawa/watch@^1.0.3":
version "1.0.3" version "1.0.3"
resolved "https://registry.yarnpkg.com/@cnakazawa/watch/-/watch-1.0.3.tgz#099139eaec7ebf07a27c1786a3ff64f39464d2ef" resolved "https://registry.yarnpkg.com/@cnakazawa/watch/-/watch-1.0.3.tgz#099139eaec7ebf07a27c1786a3ff64f39464d2ef"
@ -1407,6 +1433,11 @@ array-includes@^3.0.3:
define-properties "^1.1.2" define-properties "^1.1.2"
es-abstract "^1.7.0" es-abstract "^1.7.0"
array-move@^2.2.1:
version "2.2.1"
resolved "https://registry.yarnpkg.com/array-move/-/array-move-2.2.1.tgz#16d5b68cb949c43e8821d63e4622f3a3336f254d"
integrity sha512-qQpEHBnVT6HAFgEVUwRdHVd8TYJThrZIT5wSXpEUTPwBaYhPLclw12mEpyUvRWVdl1VwPOqnIy6LqTFN3cSeUQ==
array-union@^1.0.1: array-union@^1.0.1:
version "1.0.2" version "1.0.2"
resolved "https://registry.yarnpkg.com/array-union/-/array-union-1.0.2.tgz#9a34410e4f4e3da23dea375be5be70f24778ec39" resolved "https://registry.yarnpkg.com/array-union/-/array-union-1.0.2.tgz#9a34410e4f4e3da23dea375be5be70f24778ec39"
@ -7703,6 +7734,13 @@ resolve@^1.10.0, resolve@^1.12.0, resolve@^1.3.2:
dependencies: dependencies:
path-parse "^1.0.6" path-parse "^1.0.6"
resolve@^1.8.1:
version "1.14.2"
resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.14.2.tgz#dbf31d0fa98b1f29aa5169783b9c290cb865fea2"
integrity sha512-EjlOBLBO1kxsUxsKjLt7TAECyKW6fOh1VRkykQkKGzcBbjjPIxBqGh0jf7GJ3k/f5mxMqW3htMD3WdTUVtW8HQ==
dependencies:
path-parse "^1.0.6"
restore-cursor@^2.0.0: restore-cursor@^2.0.0:
version "2.0.0" version "2.0.0"
resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-2.0.0.tgz#9f7ee287f82fd326d4fd162923d62129eee0dfaf" resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-2.0.0.tgz#9f7ee287f82fd326d4fd162923d62129eee0dfaf"
@ -7903,7 +7941,7 @@ selfsigned@^1.10.7:
dependencies: dependencies:
node-forge "0.9.0" node-forge "0.9.0"
"semver@2 || 3 || 4 || 5", semver@^5.3.0, semver@^5.4.1, semver@^5.5.0, semver@^5.6.0, semver@^5.7.0: "semver@2 || 3 || 4 || 5", semver@^5.3.0, semver@^5.4.1, semver@^5.5.0, semver@^5.5.1, semver@^5.6.0, semver@^5.7.0:
version "5.7.1" version "5.7.1"
resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7"
integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==