1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-01-25 00:07:47 +01:00

fix: UX should not eagerly store strategy updates! (#240)

Co-authored-by: Christopher Kolstad <chriswk@getunleash.ai>
This commit is contained in:
Ivar Conradi Østhus 2021-02-09 10:14:04 +01:00 committed by GitHub
parent d0a54d6859
commit 00f411d9d2
26 changed files with 450 additions and 238 deletions

View File

@ -67,7 +67,7 @@
"identity-obj-proxy": "^3.0.0",
"immutable": "^3.8.1",
"jest": "^26.6.3",
"lodash": "^4.17.15",
"lodash": "^4.17.20",
"mini-css-extract-plugin": "^0.9.0",
"node-fetch": "^2.6.1",
"node-sass": "^4.5.3",

View File

@ -8,6 +8,10 @@
width: 100%;
}
.sectionPadding {
padding: 0 16px;
}
.horisontalScroll {
overflow-x: scroll;
-webkit-overflow-scrolling: touch;

View File

@ -34,9 +34,9 @@ export const HeaderTitle = ({ title, actions, subtitle }) => (
<div
style={{
display: 'flex',
borderBottom: '1px solid #f1f1f1',
borderBottom: '1px solid #f9f9f9',
marginBottom: '10px',
padding: '16px 20px ',
padding: '16px',
}}
>
<div style={{ flex: '2' }}>

View File

@ -104,3 +104,5 @@ export const modalStyles = {
transform: 'translate(-50%, -50%)',
},
};
export const updateIndexInArray = (array, index, newValue) => array.map((v, i) => (i === index ? newValue : v));

View File

@ -28,75 +28,72 @@ exports[`render the create feature page 1`] = `
col={4}
>
<react-mdl-Textfield
className="fullwidth"
floatingLabel={true}
label="Name"
name="name"
onBlur={[Function]}
onChange={[Function]}
placeholder="Unique-name"
style={
Object {
"width": "100%",
}
}
value="feature"
/>
</react-mdl-Cell>
<react-mdl-Cell
col={2}
col={3}
>
<Connect(FeatureTypeSelectComponent)
onChange={[Function]}
/>
</react-mdl-Cell>
<react-mdl-Cell
col={2}
style={
Object {
"paddingTop": "14px",
}
}
>
<react-mdl-Switch
checked={false}
onChange={[Function]}
>
Disabled
</react-mdl-Switch>
</react-mdl-Cell>
</react-mdl-Grid>
<section
style={
Object {
"padding": "0 16px",
}
}
className="sectionPadding"
>
<Connect(ProjectSelectComponent)
onChange={[Function]}
/>
</section>
<section
style={
Object {
"padding": "0 16px",
}
}
className="sectionPadding"
>
<react-mdl-Textfield
className="fullwidth"
floatingLabel={true}
label="Description"
onChange={[Function]}
placeholder="A short description of the feature toggle"
rows={1}
style={
Object {
"width": "100%",
}
}
value="Description"
/>
</section>
<section
style={
Object {
"padding": "10px 16px",
}
}
>
<react-mdl-Switch
checked={false}
onChange={[Function]}
>
Disabled
feature toggle
</react-mdl-Switch>
</section>
<section
style={
Object {
"margin": "40px 0",
}
}
>
<Connect(StrategiesList)
editable={true}
featureToggleName="feature"
saveStrategies={[Function]}
/>
</section>
<react-mdl-CardActions>
<FormButtons
onCancel={[MockFunction]}

View File

@ -3,6 +3,7 @@ import PropTypes from 'prop-types';
import { Textfield, Switch, Card, CardTitle, CardActions, Grid, Cell } from 'react-mdl';
import FeatureTypeSelect from '../feature-type-select-container';
import ProjectSelect from '../project-select-container';
import StrategiesList from '../strategy/strategies-list-add-container';
import { FormButtons, styles as commonStyles } from '../../common';
import { trim } from '../../common/util';
@ -28,7 +29,7 @@ class AddFeatureComponent extends Component {
<Cell col={4}>
<Textfield
floatingLabel
style={{ width: '100%' }}
className={commonStyles.fullwidth}
label="Name"
placeholder="Unique-name"
name="name"
@ -38,27 +39,17 @@ class AddFeatureComponent extends Component {
onChange={v => setValue('name', trim(v.target.value))}
/>
</Cell>
<Cell col={2}>
<Cell col={3}>
<FeatureTypeSelect value={input.type} onChange={v => setValue('type', v.target.value)} />
</Cell>
<Cell col={2} style={{ paddingTop: '14px' }}>
<Switch
checked={input.enabled}
onChange={() => {
setValue('enabled', !input.enabled);
}}
>
{input.enabled ? 'Enabled' : 'Disabled'}
</Switch>
</Cell>
</Grid>
<section style={{ padding: '0 16px' }}>
<section className={commonStyles.sectionPadding}>
<ProjectSelect value={input.project} onChange={v => setValue('project', v.target.value)} />
</section>
<section style={{ padding: '0 16px' }}>
<section className={commonStyles.sectionPadding}>
<Textfield
floatingLabel
style={{ width: '100%' }}
className={commonStyles.fullwidth}
rows={1}
label="Description"
placeholder="A short description of the feature toggle"
@ -67,6 +58,24 @@ class AddFeatureComponent extends Component {
onChange={v => setValue('description', v.target.value)}
/>
</section>
<section style={{ padding: '10px 16px' }}>
<Switch
checked={input.enabled}
onChange={() => {
setValue('enabled', !input.enabled);
}}
>
{input.enabled ? 'Enabled' : 'Disabled'} feature toggle
</Switch>
</section>
<section style={{ margin: '40px 0' }}>
<StrategiesList
configuredStrategies={input.strategies}
featureToggleName={input.name}
saveStrategies={s => setValue('strategies', s)}
editable
/>
</section>
<CardActions>
<FormButtons submitText={'Create'} onCancel={onCancel} />
</CardActions>

View File

@ -61,7 +61,10 @@ class WrapperComponent extends Component {
evt.preventDefault();
const { createFeatureToggles, history } = this.props;
const { featureToggle } = this.state;
featureToggle.strategies = [defaultStrategy];
if (featureToggle.strategies < 1) {
featureToggle.strategies = [defaultStrategy];
}
createFeatureToggles(featureToggle).then(() => history.push(`/features/strategies/${featureToggle.name}`));
};
@ -78,6 +81,7 @@ class WrapperComponent extends Component {
onCancel={this.onCancel}
validateName={this.validateName}
setValue={this.setValue}
setStrategies={this.setStrategies}
input={this.state.featureToggle}
errors={this.state.errors}
/>

View File

@ -31,7 +31,7 @@ export default class InputList extends Component {
const newValues = value.split(/,\s*/).filter(a => !list.includes(a));
if (newValues.length > 0) {
const newList = list.concat(newValues).filter(a => a);
setConfig(name, newList.join(','), true);
setConfig(name, newList.join(','));
}
this.textInput.inputRef.value = '';
}
@ -40,7 +40,7 @@ export default class InputList extends Component {
onClose(index) {
const { name, list, setConfig } = this.props;
list[index] = null;
setConfig(name, list.length === 1 ? '' : list.filter(Boolean).join(','), true);
setConfig(name, list.length === 1 ? '' : list.filter(Boolean).join(','));
}
render() {

View File

@ -0,0 +1,9 @@
import React from 'react';
export default function LoadingStrategy() {
return (
<div>
<p>Loading definition...</p>
</div>
);
}

View File

@ -21,6 +21,7 @@ class AddStrategy extends React.Component {
strategies: PropTypes.array.isRequired,
addStrategy: PropTypes.func,
featureToggleName: PropTypes.string.isRequired,
disabled: PropTypes.bool,
};
addStrategy(strategyName) {
@ -45,10 +46,11 @@ class AddStrategy extends React.Component {
render() {
const menuStyle = {
maxHeight: '300px',
maxHeight: '400px',
overflowY: 'auto',
backgroundColor: 'rgb(247, 248, 255)',
};
const { disabled = false } = this.props;
return (
<div style={{ position: 'relative', width: '25px', height: '25px', display: 'inline-block' }}>
<IconButton
@ -56,6 +58,7 @@ class AddStrategy extends React.Component {
id="strategies-add"
raised
accent
disabled={disabled}
title="Add Strategy"
onClick={this.stopPropagation}
/>

View File

@ -0,0 +1,96 @@
import React from 'react';
import PropTypes from 'prop-types';
import { DndProvider } from 'react-dnd';
import { HTML5Backend } from 'react-dnd-html5-backend';
import arrayMove from 'array-move';
import ConfigureStrategy from './strategy-configure-container';
import AddStrategy from './strategies-add';
import { HeaderTitle } from '../../common';
import { updateIndexInArray } from '../../common/util';
import styles from './strategy.module.scss';
const StrategiesList = props => {
const updateStrategy = index => strategy => {
const newStrategy = { ...strategy };
const newStrategies = updateIndexInArray(props.configuredStrategies, index, newStrategy);
props.saveStrategies(newStrategies);
};
const saveStrategy = () => () => {
// not needed for create flow
};
const addStrategy = strategy => {
const strategies = [...props.configuredStrategies];
strategies.push({ ...strategy });
props.saveStrategies(strategies);
};
const moveStrategy = async (index, toIndex) => {
const strategies = arrayMove(props.configuredStrategies, index, toIndex);
await props.saveStrategies(strategies);
};
const removeStrategy = index => async () => {
props.saveStrategies(props.configuredStrategies.filter((_, i) => i !== index));
};
const { strategies, configuredStrategies, featureToggleName } = props;
const hasName = featureToggleName && featureToggleName.length > 1;
const blocks = configuredStrategies.map((strategy, i) => (
<ConfigureStrategy
index={i}
key={i}
featureToggleName={featureToggleName}
strategy={strategy}
moveStrategy={moveStrategy}
removeStrategy={removeStrategy(i)}
updateStrategy={updateStrategy(i)}
saveStrategy={saveStrategy(i)}
strategyDefinition={strategies.find(s => s.name === strategy.name)}
editable
movable
/>
));
return (
<DndProvider backend={HTML5Backend}>
<div className={styles.strategyListAdd}>
<HeaderTitle
title="Activation strategies"
actions={
<AddStrategy
strategies={strategies}
addStrategy={addStrategy}
disabled={!hasName}
featureToggleName={featureToggleName}
/>
}
/>
<div className={styles.strategyList}>
{blocks.length > 0 ? (
blocks
) : (
<p style={{ maxWidth: '800px' }}>
An activation strategy allows you to control how a feature toggle is enabled in your
applications. If you do not specify any activation strategies you will get the "default"
strategy.
</p>
)}
</div>
</div>
</DndProvider>
);
};
StrategiesList.propTypes = {
strategies: PropTypes.array.isRequired,
configuredStrategies: PropTypes.array.isRequired,
featureToggleName: PropTypes.string.isRequired,
saveStrategies: PropTypes.func,
};
export default StrategiesList;

View File

@ -0,0 +1,8 @@
import { connect } from 'react-redux';
import StrategiesList from './strategies-list-add-component';
const mapStateToProps = state => ({
strategies: state.strategies.get('list').toArray(),
});
export default connect(mapStateToProps, undefined)(StrategiesList);

View File

@ -0,0 +1,162 @@
import React, { useState, useEffect } from 'react';
import PropTypes from 'prop-types';
import { DndProvider } from 'react-dnd';
import { HTML5Backend } from 'react-dnd-html5-backend';
import { cloneDeep } from 'lodash';
import arrayMove from 'array-move';
import { Button, Icon } from 'react-mdl';
import ConfigureStrategy from './strategy-configure-container';
import AddStrategy from './strategies-add';
import { HeaderTitle } from '../../common';
import { updateIndexInArray } from '../../common/util';
import styles from './strategy.module.scss';
const cleanStrategy = strategy => ({
name: strategy.name,
parameters: cloneDeep(strategy.parameters),
constraints: cloneDeep(strategy.constraints || []),
});
const StrategiesList = props => {
const [editableStrategies, updateEditableStrategies] = useState(cloneDeep(props.configuredStrategies));
const dirty = editableStrategies.some(p => p.dirty);
useEffect(() => {
if (!dirty) {
updateEditableStrategies(cloneDeep(props.configuredStrategies));
}
}, [props.configuredStrategies]);
const updateStrategy = index => (strategy, dirty = true) => {
const newStrategy = { ...strategy, dirty };
const newStrategies = updateIndexInArray(editableStrategies, index, newStrategy);
updateEditableStrategies(newStrategies);
};
const saveStrategy = index => async () => {
const strategies = [...props.configuredStrategies];
const strategy = editableStrategies[index];
const cleanedStrategy = cleanStrategy(strategy);
if (strategy.new) {
strategies.push(cleanedStrategy);
} else {
strategies[index] = cleanedStrategy;
}
// store in server
await props.saveStrategies(strategies);
// update local state
updateStrategy(index)(cleanedStrategy, false);
};
const addStrategy = strategy => {
const strategies = [...editableStrategies];
strategies.push({ ...strategy, dirty: true, new: true });
updateEditableStrategies(strategies);
};
const moveStrategy = async (index, toIndex) => {
if (!dirty) {
// console.log(`move strategy from ${index} to ${toIndex}`);
const strategies = arrayMove(editableStrategies, index, toIndex);
await props.saveStrategies(strategies);
updateEditableStrategies(strategies);
}
};
const removeStrategy = index => async () => {
// eslint-disable-next-line no-alert
if (window.confirm('Are you sure you want to remove this activation strategy?')) {
const strategy = editableStrategies[index];
if (!strategy.new) {
await props.saveStrategies(props.configuredStrategies.filter((_, i) => i !== index));
}
updateEditableStrategies(editableStrategies.filter((_, i) => i !== index));
}
};
const clearAll = () => {
updateEditableStrategies(cloneDeep(props.configuredStrategies));
};
const saveAll = async () => {
const cleanedStrategies = editableStrategies.map(cleanStrategy);
await props.saveStrategies(cleanedStrategies);
updateEditableStrategies(cleanedStrategies);
};
const { strategies, configuredStrategies, featureToggleName, editable } = props;
if (!configuredStrategies || configuredStrategies.length === 0) {
return (
<p style={{ padding: '0 16px' }}>
<i>No activation strategies selected.</i>
</p>
);
}
const resolveStrategyDefinition = strategyName => {
if (!strategies || strategies.length === 0) {
return { name: 'Loading' };
}
return strategies.find(s => s.name === strategyName);
};
const blocks = editableStrategies.map((strategy, i) => (
<ConfigureStrategy
index={i}
key={i}
featureToggleName={featureToggleName}
strategy={strategy}
moveStrategy={moveStrategy}
removeStrategy={removeStrategy(i)}
updateStrategy={updateStrategy(i)}
saveStrategy={saveStrategy(i)}
strategyDefinition={resolveStrategyDefinition(strategy.name)}
editable={editable}
movable={!dirty}
/>
));
return (
<DndProvider backend={HTML5Backend}>
{editable && (
<HeaderTitle
title="Activation strategies"
actions={
<AddStrategy
strategies={strategies}
addStrategy={addStrategy}
featureToggleName={featureToggleName}
/>
}
/>
)}
<div className={styles.strategyList}>{blocks}</div>
<div style={{ visibility: dirty ? 'visible' : 'hidden', padding: '10px' }}>
<Button type="submit" ripple raised primary icon="add" onClick={saveAll}>
<Icon name="save" />
&nbsp;&nbsp;&nbsp; Save all
</Button>
&nbsp;
<Button accent type="cancel" onClick={clearAll}>
Clear all
</Button>
</div>
</DndProvider>
);
};
StrategiesList.propTypes = {
strategies: PropTypes.array.isRequired,
configuredStrategies: PropTypes.array.isRequired,
featureToggleName: PropTypes.string.isRequired,
saveStrategies: PropTypes.func,
editable: PropTypes.bool,
};
export default StrategiesList;

View File

@ -0,0 +1,8 @@
import { connect } from 'react-redux';
import StrategiesList from './strategies-list-component';
const mapStateToProps = state => ({
strategies: state.strategies.get('list').toArray(),
});
export default connect(mapStateToProps, undefined)(StrategiesList);

View File

@ -1,76 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import ConfigureStrategy from './strategy-configure-container';
import { DndProvider } from 'react-dnd';
import { HTML5Backend } from 'react-dnd-html5-backend';
const randomKeys = length => Array.from({ length }, () => Math.random());
class StrategiesList extends React.Component {
static propTypes = {
strategies: PropTypes.array.isRequired,
configuredStrategies: PropTypes.array.isRequired,
featureToggleName: PropTypes.string.isRequired,
updateStrategy: PropTypes.func,
removeStrategy: PropTypes.func,
moveStrategy: PropTypes.func,
editable: PropTypes.bool,
};
constructor(props) {
super();
// temporal hack, until strategies get UIDs
this.state = { keys: randomKeys(props.configuredStrategies.length) };
}
moveStrategy = async (index, toIndex) => {
await this.props.moveStrategy(index, toIndex);
this.setState({ keys: randomKeys(this.props.configuredStrategies.length) });
};
removeStrategy = async index => {
await this.props.removeStrategy(index);
this.setState({ keys: randomKeys(this.props.configuredStrategies.length) });
};
componentDidUpdate(props) {
const { keys } = this.state;
if (keys.length < props.configuredStrategies.length) {
// eslint-disable-next-line react/no-did-update-set-state
this.setState({ keys: randomKeys(props.configuredStrategies.length) });
}
}
render() {
const { strategies, configuredStrategies, updateStrategy, featureToggleName, editable } = this.props;
const { keys } = this.state;
if (!configuredStrategies || configuredStrategies.length === 0) {
return (
<p style={{ padding: '0 16px' }}>
<i>No activation strategies selected.</i>
</p>
);
}
const blocks = configuredStrategies.map((strategy, i) => (
<ConfigureStrategy
index={i}
key={`${keys[i]}}`}
featureToggleName={featureToggleName}
strategy={strategy}
moveStrategy={this.moveStrategy}
removeStrategy={this.removeStrategy.bind(this, i)}
updateStrategy={updateStrategy ? updateStrategy.bind(null, i) : null}
strategyDefinition={strategies.find(s => s.name === strategy.name)}
editable={editable}
/>
));
return (
<DndProvider backend={HTML5Backend}>
<div style={{ display: 'flex', flexWrap: 'wrap' }}>{blocks}</div>
</DndProvider>
);
}
}
export default StrategiesList;

View File

@ -8,6 +8,7 @@ import DefaultStrategy from './default-strategy';
import GeneralStrategy from './general-strategy';
import UserWithIdStrategy from './user-with-id-strategy';
import UnknownStrategy from './unknown-strategy';
import LoadingStrategy from './loading-strategy';
import styles from './strategy.module.scss';
@ -18,61 +19,35 @@ export default class StrategyConfigureComponent extends React.Component {
index: PropTypes.number.isRequired,
strategyDefinition: PropTypes.object,
updateStrategy: PropTypes.func,
saveStrategy: PropTypes.func,
removeStrategy: PropTypes.func,
moveStrategy: PropTypes.func,
isDragging: PropTypes.bool.isRequired,
hovered: PropTypes.bool,
movable: PropTypes.bool,
connectDragPreview: PropTypes.func.isRequired,
connectDragSource: PropTypes.func.isRequired,
connectDropTarget: PropTypes.func.isRequired,
editable: PropTypes.bool,
};
constructor(props) {
super();
this.state = {
constraints: props.strategy.constraints ? [...props.strategy.constraints] : [],
parameters: { ...props.strategy.parameters },
edit: false,
dirty: false,
index: props.index,
};
}
updateParameters = parameters => {
const { constraints } = this.state;
const updatedStrategy = Object.assign({}, this.props.strategy, {
parameters,
constraints,
});
const { strategy } = this.props;
const updatedStrategy = { ...strategy, parameters };
this.props.updateStrategy(updatedStrategy);
};
updateConstraints = constraints => {
this.setState({ constraints, dirty: true });
const { strategy } = this.props;
const updatedStrategy = { ...strategy, constraints };
this.props.updateStrategy(updatedStrategy);
};
updateParameter = async (field, value, forceUp = false) => {
const { parameters } = this.state;
updateParameter = async (field, value) => {
const { strategy } = this.props;
const parameters = { ...strategy.parameters };
parameters[field] = value;
if (forceUp) {
await this.updateParameters(parameters);
this.setState({ parameters, dirty: false });
} else {
this.setState({ parameters, dirty: true });
}
};
onSave = evt => {
evt.preventDefault();
const { parameters } = this.state;
this.updateParameters(parameters);
this.setState({ edit: false, dirty: false });
};
handleRemove = evt => {
evt.preventDefault();
this.props.removeStrategy();
};
resolveInputType() {
@ -81,6 +56,8 @@ export default class StrategyConfigureComponent extends React.Component {
return UnknownStrategy;
}
switch (strategyDefinition.name) {
case 'Loading':
return LoadingStrategy;
case 'default':
return DefaultStrategy;
case 'flexibleRollout':
@ -93,7 +70,6 @@ export default class StrategyConfigureComponent extends React.Component {
}
render() {
const { dirty, parameters } = this.state;
const {
isDragging,
hovered,
@ -104,11 +80,14 @@ export default class StrategyConfigureComponent extends React.Component {
strategyDefinition,
strategy,
index,
removeStrategy,
saveStrategy,
movable,
} = this.props;
const { name } = strategy;
const { name, dirty, parameters } = strategy;
const description = strategyDefinition ? strategyDefinition.description : 'Uknown';
const description = strategyDefinition ? strategyDefinition.description : 'Unknown';
const InputType = this.resolveInputType(name);
const cardClasses = [styles.card];
@ -143,7 +122,7 @@ export default class StrategyConfigureComponent extends React.Component {
editable={editable}
/>
<Button
onClick={this.onSave}
onClick={saveStrategy}
accent
raised
ripple
@ -164,17 +143,23 @@ export default class StrategyConfigureComponent extends React.Component {
</Link>
{editable && (
<IconButton
title="Remove strategy from toggle"
title="Remove this activation strategy"
name="delete"
onClick={this.handleRemove}
onClick={removeStrategy}
/>
)}
{editable &&
movable &&
connectDragSource(
<span className={styles.reorderIcon}>
<Icon name="reorder" />
</span>
)}
{editable && !movable && (
<span className={[styles.reorderIcon, styles.disabled].join(' ')}>
<Icon name="reorder" title="You can not reorder while editing." />
</span>
)}
</CardMenu>
</Card>
</div>

View File

@ -2,6 +2,7 @@ import PropTypes from 'prop-types';
export default {
strategyDefinition: PropTypes.shape({
name: PropTypes.string,
parameters: PropTypes.array,
}).isRequired,
parameters: PropTypes.object.isRequired,

View File

@ -1,7 +1,7 @@
.item {
flex: 1;
min-width: 400px;
max-width: 100%;
max-width: 537px;
margin: 5px 0 5px 5px;
position: relative;
z-index: 1;
@ -85,15 +85,25 @@
cursor: pointer;
display: inline-block;
vertical-align: bottom;
&.disabled {
cursor: default;
color: silver;
}
}
.paddingDesktop {
.strategyList {
display: flex;
flex-wrap: wrap;
padding: 10px;
}
.strategyListAdd {
background-color: #f9f9f9;
}
@media (max-width: 500px) {
.paddingDesktop {
.strategyList {
padding: 0;
}
}

View File

@ -1,9 +1,9 @@
import React from 'react';
import strategyInputProps from './strategy-input-props';
import PropTypes from 'prop-types';
import { Link } from 'react-router-dom';
export default function UknownStrategy({ strategy }) {
export default function UnknownStrategy({ strategy }) {
const { name } = strategy;
return (
<div>
@ -13,4 +13,8 @@ export default function UknownStrategy({ strategy }) {
);
}
UknownStrategy.propTypes = strategyInputProps;
UnknownStrategy.propTypes = {
strategy: PropTypes.shape({
name: PropTypes.string,
}).isRequired,
};

View File

@ -1,3 +1,27 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`render the create feature page 1`] = `""`;
exports[`render the create feature page 1`] = `
<section>
<Connect(StrategiesList)
addStrategy={[MockFunction]}
configuredStrategies={
Array [
Object {
"name": "default",
},
]
}
editable={true}
featureToggleName="some-toggle"
initCallRequired={false}
moveStrategy={[MockFunction]}
onCancel={[MockFunction]}
onSubmit={[MockFunction]}
removeStrategy={[MockFunction]}
setValue={[MockFunction]}
title="title"
updateStrategy={[MockFunction]}
validateName={[MockFunction]}
/>
</section>
`;

View File

@ -1,19 +1,14 @@
import React from 'react';
import PropTypes from 'prop-types';
import StrategiesList from '../strategy/strategies-list';
import AddStrategy from '../strategy/strategies-add';
import { HeaderTitle } from '../../common';
import styles from '../strategy/strategy.module.scss';
import StrategiesList from '../strategy/strategies-list-container';
// TODO: do we still need this wrapper?
function UpdateStrategiesComponent(props) {
const { editable, configuredStrategies, strategies } = props;
const { configuredStrategies } = props;
if (!configuredStrategies || configuredStrategies.length === 0) return null;
if (!strategies || strategies.length === 0) return null;
return (
<section className={styles.paddingDesktop}>
{editable && <HeaderTitle title="Activation strategies" actions={<AddStrategy {...props} />} />}
<section>
<StrategiesList {...props} />
</section>
);
@ -23,10 +18,6 @@ UpdateStrategiesComponent.propTypes = {
featureToggleName: PropTypes.string.isRequired,
strategies: PropTypes.array,
configuredStrategies: PropTypes.array.isRequired,
addStrategy: PropTypes.func.isRequired,
removeStrategy: PropTypes.func.isRequired,
moveStrategy: PropTypes.func.isRequired,
updateStrategy: PropTypes.func.isRequired,
editable: PropTypes.bool,
};

View File

@ -1,6 +1,5 @@
/* eslint-disable no-console */
import { connect } from 'react-redux';
import arrayMove from 'array-move';
import { requestUpdateFeatureToggleStrategies } from '../../../store/feature-toggle/actions';
import UpdateStrategiesComponent from './update-strategies-component';
@ -8,39 +7,11 @@ import UpdateStrategiesComponent from './update-strategies-component';
const mapStateToProps = (state, ownProps) => ({
featureToggleName: ownProps.featureToggle.name,
configuredStrategies: ownProps.featureToggle.strategies,
strategies: state.strategies.get('list').toArray(),
});
const mapDispatchToProps = (dispatch, ownProps) => ({
addStrategy: s => {
console.log(`add ${s}`);
saveStrategies: strategies => {
const featureToggle = ownProps.featureToggle;
const strategies = featureToggle.strategies.concat(s);
return requestUpdateFeatureToggleStrategies(featureToggle, strategies)(dispatch);
},
removeStrategy: index => {
console.log(`remove ${index}`);
const featureToggle = ownProps.featureToggle;
const strategies = featureToggle.strategies.filter((_, i) => i !== index);
return requestUpdateFeatureToggleStrategies(featureToggle, strategies)(dispatch);
},
moveStrategy: (index, toIndex) => {
// methods.moveItem('strategies', index, toIndex);
console.log(`move strategy from ${index} to ${toIndex}`);
console.log(ownProps.featureToggle);
const featureToggle = ownProps.featureToggle;
const strategies = arrayMove(featureToggle.strategies, index, toIndex);
return requestUpdateFeatureToggleStrategies(featureToggle, strategies)(dispatch);
},
updateStrategy: (index, s) => {
// methods.updateInList('strategies', index, n);
console.log(`update strtegy at index ${index} with ${JSON.stringify(s)}`);
const featureToggle = ownProps.featureToggle;
const strategies = featureToggle.strategies.concat();
strategies[index] = s;
return requestUpdateFeatureToggleStrategies(featureToggle, strategies)(dispatch);
},
});

View File

@ -13,10 +13,10 @@ exports[`renders correctly with one strategy 1`] = `
<div
style={
Object {
"borderBottom": "1px solid #f1f1f1",
"borderBottom": "1px solid #f9f9f9",
"display": "flex",
"marginBottom": "10px",
"padding": "16px 20px ",
"padding": "16px",
}
}
>
@ -104,10 +104,10 @@ exports[`renders correctly with one strategy without permissions 1`] = `
<div
style={
Object {
"borderBottom": "1px solid #f1f1f1",
"borderBottom": "1px solid #f9f9f9",
"display": "flex",
"marginBottom": "10px",
"padding": "16px 20px ",
"padding": "16px",
}
}
>

View File

@ -10,10 +10,10 @@ exports[`renders correctly with one strategy 1`] = `
<div
style={
Object {
"borderBottom": "1px solid #f1f1f1",
"borderBottom": "1px solid #f9f9f9",
"display": "flex",
"marginBottom": "10px",
"padding": "16px 20px ",
"padding": "16px",
}
}
>

View File

@ -13,10 +13,10 @@ exports[`renders a list with elements correctly 1`] = `
<div
style={
Object {
"borderBottom": "1px solid #f1f1f1",
"borderBottom": "1px solid #f9f9f9",
"display": "flex",
"marginBottom": "10px",
"padding": "16px 20px ",
"padding": "16px",
}
}
>
@ -92,10 +92,10 @@ exports[`renders an empty list correctly 1`] = `
<div
style={
Object {
"borderBottom": "1px solid #f1f1f1",
"borderBottom": "1px solid #f9f9f9",
"display": "flex",
"marginBottom": "10px",
"padding": "16px 20px ",
"padding": "16px",
}
}
>

View File

@ -6533,7 +6533,7 @@ lodash@^4.0.0, lodash@^4.15.0, lodash@^4.17.11, lodash@^4.17.13, lodash@^4.17.14
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.19.tgz#e48ddedbe30b3321783c5b4301fbd353bc1e4a4b"
integrity sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ==
lodash@^4.17.19:
lodash@^4.17.19, lodash@^4.17.20:
version "4.17.20"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.20.tgz#b44a9b6297bcb698f1c51a3545a2b3b368d59c52"
integrity sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==