mirror of
				https://github.com/Unleash/unleash.git
				synced 2025-10-27 11:02:16 +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:
		
							parent
							
								
									ff8ce52347
								
							
						
					
					
						commit
						19443c651f
					
				@ -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": {
 | 
				
			||||||
 | 
				
			|||||||
@ -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",
 | 
				
			||||||
 | 
				
			|||||||
@ -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,17 +57,36 @@ exports[`renders correctly with one feature 1`] = `
 | 
				
			|||||||
        Disabled
 | 
					        Disabled
 | 
				
			||||||
      </react-mdl-Switch>
 | 
					      </react-mdl-Switch>
 | 
				
			||||||
    </span>
 | 
					    </span>
 | 
				
			||||||
    <react-mdl-Button
 | 
					    <div>
 | 
				
			||||||
      disabled={false}
 | 
					      <a
 | 
				
			||||||
      onClick={[Function]}
 | 
					        href="/features/copy/Another"
 | 
				
			||||||
      style={
 | 
					        onClick={[Function]}
 | 
				
			||||||
        Object {
 | 
					        title="Create new feature toggle by cloning configuration"
 | 
				
			||||||
          "flexShrink": 0,
 | 
					      >
 | 
				
			||||||
 | 
					        <react-mdl-Button
 | 
				
			||||||
 | 
					          style={
 | 
				
			||||||
 | 
					            Object {
 | 
				
			||||||
 | 
					              "flexShrink": 0,
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        >
 | 
				
			||||||
 | 
					          Clone
 | 
				
			||||||
 | 
					        </react-mdl-Button>
 | 
				
			||||||
 | 
					      </a>
 | 
				
			||||||
 | 
					      <react-mdl-Button
 | 
				
			||||||
 | 
					        accent={true}
 | 
				
			||||||
 | 
					        disabled={false}
 | 
				
			||||||
 | 
					        onClick={[Function]}
 | 
				
			||||||
 | 
					        style={
 | 
				
			||||||
 | 
					          Object {
 | 
				
			||||||
 | 
					            "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
 | 
				
			||||||
 | 
				
			|||||||
@ -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]}
 | 
				
			||||||
 | 
				
			|||||||
@ -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}
 | 
				
			||||||
 | 
				
			|||||||
@ -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,
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
 | 
				
			|||||||
@ -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>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                        <StrategiesSection
 | 
					                        {input.name ? (
 | 
				
			||||||
                            configuredStrategies={configuredStrategies}
 | 
					                            <StrategiesSection
 | 
				
			||||||
                            addStrategy={addStrategy}
 | 
					                                configuredStrategies={configuredStrategies}
 | 
				
			||||||
                            updateStrategy={updateStrategy}
 | 
					                                featureToggleName={input.name}
 | 
				
			||||||
                            moveStrategy={moveStrategy}
 | 
					                                addStrategy={addStrategy}
 | 
				
			||||||
                            removeStrategy={removeStrategy}
 | 
					                                updateStrategy={updateStrategy}
 | 
				
			||||||
                        />
 | 
					                                moveStrategy={moveStrategy}
 | 
				
			||||||
 | 
					                                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,
 | 
				
			||||||
 | 
				
			|||||||
@ -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 },
 | 
				
			||||||
 | 
					            errors: {},
 | 
				
			||||||
 | 
					            dirty: false,
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    setValue = (field, value) => {
 | 
				
			||||||
 | 
					        const { featureToggle } = this.state;
 | 
				
			||||||
 | 
					        featureToggle[field] = value;
 | 
				
			||||||
 | 
					        this.setState({ featureToggle, dirty: true });
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    validateName = async featureToggleName => {
 | 
				
			||||||
 | 
					        const { errors } = this.state;
 | 
				
			||||||
        try {
 | 
					        try {
 | 
				
			||||||
            [, name] = document.location.hash.match(/name=([a-z0-9-_.]+)/i);
 | 
					            await validateName(featureToggleName);
 | 
				
			||||||
        } catch (e) {
 | 
					            errors.name = undefined;
 | 
				
			||||||
            // hide error
 | 
					        } catch (err) {
 | 
				
			||||||
 | 
					            errors.name = err.message;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        return { name };
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
});
 | 
					 | 
				
			||||||
const prepare = (methods, dispatch, ownProps) => {
 | 
					 | 
				
			||||||
    methods.onSubmit = input => e => {
 | 
					 | 
				
			||||||
        e.preventDefault();
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
        input.createdAt = new Date();
 | 
					        this.setState({ errors });
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if (Array.isArray(input.strategies)) {
 | 
					    addStrategy = strat => {
 | 
				
			||||||
            input.strategies.forEach(s => {
 | 
					        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;
 | 
				
			||||||
 | 
				
			|||||||
@ -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 {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 
 | 
				
			||||||
 | 
					                            <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" />
 | 
				
			||||||
 | 
					                                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;
 | 
				
			||||||
@ -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;
 | 
				
			||||||
@ -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);
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										7
									
								
								frontend/src/component/feature/form/util.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								frontend/src/component/feature/form/util.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,7 @@
 | 
				
			|||||||
 | 
					export const trim = value => {
 | 
				
			||||||
 | 
					    if (value && value.trim) {
 | 
				
			||||||
 | 
					        return value.trim();
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					        return value;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
@ -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 ? (
 | 
				
			||||||
                        <Button
 | 
					                        <div>
 | 
				
			||||||
                            disabled={!hasPermission(DELETE_FEATURE)}
 | 
					                            <Link
 | 
				
			||||||
                            onClick={removeToggle}
 | 
					                                to={`/features/copy/${featureToggle.name}`}
 | 
				
			||||||
                            style={{ flexShrink: 0 }}
 | 
					                                title="Create new feature toggle by cloning configuration"
 | 
				
			||||||
                        >
 | 
					                            >
 | 
				
			||||||
                            Archive
 | 
					                                <Button style={{ flexShrink: 0 }}>Clone</Button>
 | 
				
			||||||
                        </Button>
 | 
					                            </Link>
 | 
				
			||||||
 | 
					                            <Button
 | 
				
			||||||
 | 
					                                disabled={!hasPermission(DELETE_FEATURE)}
 | 
				
			||||||
 | 
					                                onClick={removeToggle}
 | 
				
			||||||
 | 
					                                title="Archive feature toggle"
 | 
				
			||||||
 | 
					                                accent
 | 
				
			||||||
 | 
					                                style={{ flexShrink: 0 }}
 | 
				
			||||||
 | 
					                            >
 | 
				
			||||||
 | 
					                                Archive
 | 
				
			||||||
 | 
					                            </Button>
 | 
				
			||||||
 | 
					                        </div>
 | 
				
			||||||
                    ) : (
 | 
					                    ) : (
 | 
				
			||||||
                        <Button
 | 
					                        <Button
 | 
				
			||||||
                            disabled={!hasPermission(UPDATE_FEATURE)}
 | 
					                            disabled={!hasPermission(UPDATE_FEATURE)}
 | 
				
			||||||
 | 
				
			|||||||
@ -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",
 | 
				
			||||||
 | 
				
			|||||||
@ -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();
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -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 },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										14
									
								
								frontend/src/page/features/copy.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								frontend/src/page/features/copy.js
									
									
									
									
									
										Normal 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;
 | 
				
			||||||
@ -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';
 | 
				
			||||||
 | 
				
			|||||||
@ -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 => {
 | 
				
			||||||
 | 
				
			|||||||
@ -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==
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
		Reference in New Issue
	
	Block a user