1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-01-25 00:07:47 +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"],
"plugins": [
["@babel/plugin-proposal-decorators", { "legacy": true }],
"@babel/plugin-proposal-class-properties"
"@babel/plugin-proposal-class-properties",
"@babel/plugin-transform-runtime"
],
"env": {
"test": {

View File

@ -44,8 +44,10 @@
"@babel/plugin-proposal-class-properties": "^7.5.5",
"@babel/plugin-proposal-decorators": "^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-react": "^7.0.0",
"array-move": "^2.2.1",
"babel-eslint": "^10.0.3",
"babel-jest": "^24.9.0",
"babel-loader": "^8.0.6",

View File

@ -19,6 +19,7 @@ exports[`renders correctly with one feature 1`] = `
}
>
Another
</react-mdl-CardTitle>
<react-mdl-CardText>
another's description
@ -56,17 +57,36 @@ exports[`renders correctly with one feature 1`] = `
Disabled
</react-mdl-Switch>
</span>
<react-mdl-Button
disabled={false}
onClick={[Function]}
style={
Object {
"flexShrink": 0,
<div>
<a
href="/features/copy/Another"
onClick={[Function]}
title="Create new feature toggle by cloning configuration"
>
<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,
}
}
}
>
Archive
</react-mdl-Button>
title="Archive feature toggle"
>
Archive
</react-mdl-Button>
</div>
</react-mdl-CardActions>
<hr />
<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>
<form>
<form
onSubmit={[MockFunction]}
>
<section
style={
Object {
@ -29,20 +31,17 @@ exports[`render the create feature page 1`] = `
}
>
<react-mdl-Textfield
error={Object {}}
floatingLabel={true}
label="Name"
name="name"
onBlur={[Function]}
onChange={[Function]}
required={true}
value="feature"
/>
<react-mdl-Textfield
floatingLabel={true}
label="Description"
onChange={[Function]}
required={true}
rows={1}
style={
Object {
@ -59,11 +58,13 @@ exports[`render the create feature page 1`] = `
>
Enabled
</react-mdl-Switch>
<hr />
<br />
<br />
</div>
<StrategiesSection
addStrategy={[MockFunction]}
configuredStrategies={Array []}
featureToggleName="feature"
moveStrategy={[MockFunction]}
removeStrategy={[MockFunction]}
updateStrategy={[MockFunction]}

View File

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

View File

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

View File

@ -5,27 +5,18 @@ import StrategiesSection from './strategies-section-container';
import { FormButtons } from './../../common';
import { styles as commonStyles } from '../../common';
const trim = value => {
if (value && value.trim) {
return value.trim();
} else {
return value;
}
};
import { trim } from './util';
class AddFeatureComponent extends Component {
// static displayName = `AddFeatureComponent-${getDisplayName(Component)}`;
componentWillMount() {
// TODO unwind this stuff
if (this.props.initCallRequired === true) {
this.props.init(this.props.input);
}
componentDidMount() {
window.onbeforeunload = () => 'Data will be lost if you leave the page, are you sure?';
}
render() {
const {
input,
errors,
setValue,
validateName,
addStrategy,
@ -36,26 +27,19 @@ class AddFeatureComponent extends Component {
onCancel,
} = this.props;
const {
name, // eslint-disable-line
nameError,
description,
enabled,
} = input;
const configuredStrategies = input.strategies || [];
return (
<Card shadow={0} className={commonStyles.fullwidth} style={{ overflow: 'visible' }}>
<CardTitle style={{ paddingTop: '24px', wordBreak: 'break-all' }}>Create feature toggle</CardTitle>
<form onSubmit={onSubmit(input)}>
<CardTitle style={{ paddingTop: '24px', wordBreak: 'break-all' }}>Create new feature toggle</CardTitle>
<form onSubmit={onSubmit}>
<section style={{ padding: '16px' }}>
<Textfield
floatingLabel
label="Name"
name="name"
required
value={name}
error={nameError}
value={input.name}
error={errors.name}
onBlur={v => validateName(v.target.value)}
onChange={v => setValue('name', trim(v.target.value))}
/>
@ -64,30 +48,34 @@ class AddFeatureComponent extends Component {
style={{ width: '100%' }}
rows={1}
label="Description"
required
value={description}
error={errors.description}
value={input.description}
onChange={v => setValue('description', v.target.value)}
/>
<div>
<br />
<Switch
checked={enabled}
checked={input.enabled}
onChange={() => {
setValue('enabled', !enabled);
setValue('enabled', !input.enabled);
}}
>
Enabled
</Switch>
<hr />
<br />
<br />
</div>
<StrategiesSection
configuredStrategies={configuredStrategies}
addStrategy={addStrategy}
updateStrategy={updateStrategy}
moveStrategy={moveStrategy}
removeStrategy={removeStrategy}
/>
{input.name ? (
<StrategiesSection
configuredStrategies={configuredStrategies}
featureToggleName={input.name}
addStrategy={addStrategy}
updateStrategy={updateStrategy}
moveStrategy={moveStrategy}
removeStrategy={removeStrategy}
/>
) : null}
<br />
</section>
@ -102,6 +90,7 @@ class AddFeatureComponent extends Component {
AddFeatureComponent.propTypes = {
input: PropTypes.object,
errors: PropTypes.object,
setValue: PropTypes.func.isRequired,
addStrategy: 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 arrayMove from 'array-move';
import { createFeatureToggles, validateName } from './../../../store/feature-actions';
import { createMapper, createActions } from './../../input-helpers';
import AddFeatureComponent from './form-add-feature-component';
const defaultStrategy = { name: 'default' };
const ID = 'add-feature-toggle';
const mapStateToProps = createMapper({
id: ID,
getDefault() {
let name;
class WrapperComponent extends Component {
constructor() {
super();
this.state = {
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 {
[, name] = document.location.hash.match(/name=([a-z0-9-_.]+)/i);
} catch (e) {
// hide error
await validateName(featureToggleName);
errors.name = undefined;
} 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)) {
input.strategies.forEach(s => {
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;
});
} else {
input.strategies = [defaultStrategy];
featureToggle.strategies = [defaultStrategy];
}
createFeatureToggles(input)(dispatch)
.then(() => methods.clear())
.then(() => ownProps.history.push(`/features/strategies/${input.name}`));
createFeatureToggles(featureToggle).then(() => history.push(`/features/strategies/${featureToggle.name}`));
};
methods.onCancel = evt => {
onCancel = evt => {
evt.preventDefault();
methods.clear();
ownProps.history.push('/features');
this.props.history.push('/features');
};
methods.addStrategy = v => {
v.id = Math.round(Math.random() * 10000000);
methods.pushToList('strategies', v);
};
methods.updateStrategy = (index, n) => {
methods.updateInList('strategies', index, n);
};
methods.moveStrategy = (index, toIndex) => {
methods.moveItem('strategies', index, toIndex);
};
methods.removeStrategy = index => {
methods.removeFromList('strategies', index);
};
methods.validateName = featureToggleName => {
validateName(featureToggleName)
.then(() => methods.setValue('nameError', undefined))
.catch(err => methods.setValue('nameError', err.message));
};
return methods;
render() {
return (
<AddFeatureComponent
onSubmit={this.onSubmit}
onCancel={this.onCancel}
addStrategy={this.addStrategy}
updateStrategy={this.updateStrategy}
removeStrategy={this.removeStrategy}
moveStrategy={this.moveStrategy}
validateName={this.validateName}
setValue={this.setValue}
input={this.state.featureToggle}
errors={this.state.errors}
/>
);
}
}
WrapperComponent.propTypes = {
history: PropTypes.object.isRequired,
createFeatureToggles: PropTypes.func.isRequired,
};
const actions = createActions({ id: ID, prepare });
const mapDispatchToProps = dispatch => ({
validateName: name => validateName(name)(dispatch),
createFeatureToggles: featureToggle => createFeatureToggles(featureToggle)(dispatch),
});
const FormAddContainer = connect(
mapStateToProps,
actions
)(AddFeatureComponent);
() => {},
mapDispatchToProps
)(WrapperComponent);
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';
const StrategiesSection = connect(
state => ({
(state, ownProps) => ({
strategies: state.strategies.get('list').toArray(),
configuredStrategies: ownProps.configuredStrategies,
}),
{ fetchStrategies }
)(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 (
<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
isFeatureView={this.isFeatureView}
description={featureToggle.description}
@ -181,13 +181,23 @@ export default class ViewFeatureToggleComponent extends React.Component {
</span>
{this.isFeatureView ? (
<Button
disabled={!hasPermission(DELETE_FEATURE)}
onClick={removeToggle}
style={{ flexShrink: 0 }}
>
Archive
</Button>
<div>
<Link
to={`/features/copy/${featureToggle.name}`}
title="Create new feature toggle by cloning configuration"
>
<Button style={{ flexShrink: 0 }}>Clone</Button>
</Link>
<Button
disabled={!hasPermission(DELETE_FEATURE)}
onClick={removeToggle}
title="Archive feature toggle"
accent
style={{ flexShrink: 0 }}
>
Archive
</Button>
</div>
) : (
<Button
disabled={!hasPermission(UPDATE_FEATURE)}

View File

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

View File

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

View File

@ -1,4 +1,5 @@
import CreateFeatureToggle from '../../page/features/create';
import CopyFeatureToggle from '../../page/features/copy';
import ViewFeatureToggle from '../../page/features/show';
import Features from '../../page/features';
import CreateStrategies from '../../page/strategies/create';
@ -15,6 +16,7 @@ import LogoutFeatures from '../../page/user/logout';
export const routes = [
// Features
{ 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', 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';
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 UPDATE_FEATURE_TOGGLE = 'UPDATE_FEATURE_TOGGLE';
export const TOGGLE_FEATURE_TOGGLE = 'TOGGLE_FEATURE_TOGGLE';

View File

@ -5,6 +5,7 @@ import {
ADD_FEATURE_TOGGLE,
RECEIVE_FEATURE_TOGGLES,
UPDATE_FEATURE_TOGGLE,
UPDATE_FEATURE_TOGGLE_STRATEGIES,
REMOVE_FEATURE_TOGGLE,
TOGGLE_FEATURE_TOGGLE,
} from './feature-actions';
@ -28,6 +29,15 @@ const features = (state = new List([]), action) => {
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:
debug(UPDATE_FEATURE_TOGGLE, action);
return state.map(toggle => {

View File

@ -137,6 +137,13 @@
dependencies:
"@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":
version "7.5.5"
resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.5.5.tgz#f84ff8a09038dcbca1fd4355661a500937165b4a"
@ -585,6 +592,16 @@
dependencies:
"@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":
version "7.2.0"
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"
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":
version "1.0.3"
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"
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:
version "1.0.2"
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:
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:
version "2.0.0"
resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-2.0.0.tgz#9f7ee287f82fd326d4fd162923d62129eee0dfaf"
@ -7903,7 +7941,7 @@ selfsigned@^1.10.7:
dependencies:
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"
resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7"
integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==