mirror of
https://github.com/Unleash/unleash.git
synced 2025-04-15 01:16:22 +02:00
Add tag feature
- CRUD for tag-types - CD for tags - tagging for features - display tags on feature-toggle
This commit is contained in:
parent
d8c531b7de
commit
2cabe7f297
1
frontend/.nvmrc
Normal file
1
frontend/.nvmrc
Normal file
@ -0,0 +1 @@
|
|||||||
|
12
|
@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
|
|||||||
The latest version of this document is always available in
|
The latest version of this document is always available in
|
||||||
[releases][releases-url].
|
[releases][releases-url].
|
||||||
|
|
||||||
|
# 3.9.0
|
||||||
|
- feat: Tags for feature toggles (#232)
|
||||||
|
- feat: Tag-types (#232)
|
||||||
|
|
||||||
# 3.8.4
|
# 3.8.4
|
||||||
- fix: update canisue-lite
|
- fix: update canisue-lite
|
||||||
- fix: move all api calls to store folders
|
- fix: move all api calls to store folders
|
||||||
|
@ -22,7 +22,7 @@ const Select = ({ name, value, label, options, style, onChange, disabled = false
|
|||||||
</option>
|
</option>
|
||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
<label className="mdl-textfield__label" htmlFor="textfield-conextName">
|
<label className="mdl-textfield__label" htmlFor="textfield-contextName">
|
||||||
{label}
|
{label}
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
@ -82,3 +82,25 @@ export function loadNameFromHash() {
|
|||||||
}
|
}
|
||||||
return field;
|
return field;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const modalStyles = {
|
||||||
|
overlay: {
|
||||||
|
position: 'absolute',
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
bottom: 0,
|
||||||
|
backgroundColor: 'rgba(0, 0, 0, 0.25)',
|
||||||
|
zIndex: 5,
|
||||||
|
},
|
||||||
|
content: {
|
||||||
|
width: '500px',
|
||||||
|
maxWidth: '90%',
|
||||||
|
margin: '0',
|
||||||
|
top: '50%',
|
||||||
|
left: '50%',
|
||||||
|
right: 'auto',
|
||||||
|
bottom: 'auto',
|
||||||
|
transform: 'translate(-50%, -50%)',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
101
frontend/src/component/feature/add-tag-dialog-component.jsx
Normal file
101
frontend/src/component/feature/add-tag-dialog-component.jsx
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
import React, { Component } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import Modal from 'react-modal';
|
||||||
|
import { Button, Textfield, DialogActions } from 'react-mdl';
|
||||||
|
import { FormButtons } from '../common';
|
||||||
|
import TagTypeSelect from '../feature/tag-type-select-container';
|
||||||
|
import { trim, modalStyles } from '../common/util';
|
||||||
|
|
||||||
|
class AddTagDialogComponent extends Component {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
openDialog: false,
|
||||||
|
errors: {},
|
||||||
|
currentLegalValue: '',
|
||||||
|
dirty: false,
|
||||||
|
featureToggleName: props.featureToggleName,
|
||||||
|
tag: props.tag,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
handleOpenDialog() {
|
||||||
|
this.setState({ openDialog: true });
|
||||||
|
}
|
||||||
|
handleCancel() {
|
||||||
|
this.setState({
|
||||||
|
openDialog: false,
|
||||||
|
tag: { type: 'simple', value: '' },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
setValue = (field, value) => {
|
||||||
|
const { tag } = this.state;
|
||||||
|
tag[field] = trim(value);
|
||||||
|
this.setState({ tag, dirty: true });
|
||||||
|
};
|
||||||
|
|
||||||
|
onCancel = evt => {
|
||||||
|
evt.preventDefault();
|
||||||
|
this.setState({ openDialog: false, tag: { type: 'simple', value: '' } });
|
||||||
|
};
|
||||||
|
onSubmit = async evt => {
|
||||||
|
evt.preventDefault();
|
||||||
|
const { tag } = this.state;
|
||||||
|
if (!tag.type) {
|
||||||
|
tag.type = 'simple';
|
||||||
|
}
|
||||||
|
await this.props.submit(this.props.featureToggleName, tag);
|
||||||
|
this.setState({ openDialog: false, tag: { type: 'simple', value: '' } });
|
||||||
|
};
|
||||||
|
render() {
|
||||||
|
const { tag, errors, openDialog } = this.state;
|
||||||
|
const submitText = 'Tag feature';
|
||||||
|
return (
|
||||||
|
<React.Fragment>
|
||||||
|
<Button colored onClick={this.handleOpenDialog.bind(this)} ripple>
|
||||||
|
Add tag
|
||||||
|
</Button>
|
||||||
|
<Modal
|
||||||
|
isOpen={openDialog}
|
||||||
|
contentLabel="Add tags to feature toggle"
|
||||||
|
style={modalStyles}
|
||||||
|
onRequestClose={this.onCancel}
|
||||||
|
>
|
||||||
|
<h3>{submitText}</h3>
|
||||||
|
<p>Tags allows you to group features together</p>
|
||||||
|
<form onSubmit={this.onSubmit}>
|
||||||
|
<section>
|
||||||
|
<TagTypeSelect
|
||||||
|
name="type"
|
||||||
|
value={tag.type}
|
||||||
|
onChange={v => this.setValue('type', v.target.value)}
|
||||||
|
/>
|
||||||
|
<br />
|
||||||
|
<Textfield
|
||||||
|
floatingLabel
|
||||||
|
label="Value"
|
||||||
|
name="value"
|
||||||
|
placeholder="Your tag"
|
||||||
|
value={tag.value}
|
||||||
|
error={errors.value}
|
||||||
|
onChange={v => this.setValue('value', v.target.value)}
|
||||||
|
/>
|
||||||
|
</section>
|
||||||
|
<DialogActions>
|
||||||
|
<FormButtons submitText={submitText} onCancel={this.onCancel} />
|
||||||
|
</DialogActions>
|
||||||
|
</form>
|
||||||
|
</Modal>
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
AddTagDialogComponent.propTypes = {
|
||||||
|
featureToggleName: PropTypes.string.isRequired,
|
||||||
|
tag: PropTypes.object.isRequired,
|
||||||
|
submit: PropTypes.func.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AddTagDialogComponent;
|
16
frontend/src/component/feature/add-tag-dialog-container.js
Normal file
16
frontend/src/component/feature/add-tag-dialog-container.js
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import { connect } from 'react-redux';
|
||||||
|
import AddTagDialogComponent from './add-tag-dialog-component';
|
||||||
|
|
||||||
|
import { tagFeature } from '../../store/feature-tags/actions';
|
||||||
|
|
||||||
|
const mapStateToProps = () => ({
|
||||||
|
tag: { type: 'simple', value: '' },
|
||||||
|
});
|
||||||
|
|
||||||
|
const mapDispatchToProps = dispatch => ({
|
||||||
|
submit: (featureToggleName, tag) => tagFeature(featureToggleName, tag)(dispatch),
|
||||||
|
});
|
||||||
|
|
||||||
|
const AddTagDialogContainer = connect(mapStateToProps, mapDispatchToProps)(AddTagDialogComponent);
|
||||||
|
|
||||||
|
export default AddTagDialogContainer;
|
48
frontend/src/component/feature/feature-tag-component.jsx
Normal file
48
frontend/src/component/feature/feature-tag-component.jsx
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { Chip, ChipContact, Icon } from 'react-mdl';
|
||||||
|
function FeatureTagComponent({ tags, tagTypes, featureToggleName, untagFeature }) {
|
||||||
|
const onUntagFeature = tag => {
|
||||||
|
// eslint-disable-next-line no-alert
|
||||||
|
if (window.confirm('Are you sure you want to remove this tag')) {
|
||||||
|
untagFeature(featureToggleName, tag);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const tagIcon = typeName => {
|
||||||
|
let tagType = tagTypes.find(type => type.name === typeName);
|
||||||
|
if (tagType && tagType.icon) {
|
||||||
|
return <Icon name={tagType.icon} />;
|
||||||
|
} else {
|
||||||
|
return <span>{typeName[0].toUpperCase()}</span>;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderTag = t => (
|
||||||
|
<Chip
|
||||||
|
onClose={() => onUntagFeature(t)}
|
||||||
|
title={t.value}
|
||||||
|
key={`${t.type}:${t.value}`}
|
||||||
|
style={{ marginRight: '3px', fontSize: '0.8em' }}
|
||||||
|
>
|
||||||
|
<ChipContact className="mdl-color--grey-500">{tagIcon(t.type)}</ChipContact>
|
||||||
|
{t.value}
|
||||||
|
</Chip>
|
||||||
|
);
|
||||||
|
|
||||||
|
return tags && tags.length > 0 ? (
|
||||||
|
<div>
|
||||||
|
<p style={{ marginBottom: 0 }}>Tags</p>
|
||||||
|
{tags.map(renderTag)}
|
||||||
|
</div>
|
||||||
|
) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
FeatureTagComponent.propTypes = {
|
||||||
|
tags: PropTypes.array,
|
||||||
|
tagTypes: PropTypes.array,
|
||||||
|
featureToggleName: PropTypes.string.isRequired,
|
||||||
|
untagFeature: PropTypes.func,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default FeatureTagComponent;
|
29
frontend/src/component/feature/tag-type-select-component.jsx
Normal file
29
frontend/src/component/feature/tag-type-select-component.jsx
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
import React, { Component } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import MySelect from '../common/select';
|
||||||
|
|
||||||
|
class TagTypeSelectComponent extends Component {
|
||||||
|
componentDidMount() {
|
||||||
|
const { fetchTagTypes } = this.props;
|
||||||
|
if (fetchTagTypes) {
|
||||||
|
this.props.fetchTagTypes();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { value, types, onChange, filled } = this.props;
|
||||||
|
const options = types.map(t => ({ key: t.name, label: t.name, title: t.name }));
|
||||||
|
|
||||||
|
return <MySelect label="Tag type" options={options} value={value} onChange={onChange} filled={filled} />;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TagTypeSelectComponent.propTypes = {
|
||||||
|
value: PropTypes.string,
|
||||||
|
filled: PropTypes.bool,
|
||||||
|
types: PropTypes.array.isRequired,
|
||||||
|
fetchTagTypes: PropTypes.func,
|
||||||
|
onChange: PropTypes.func.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TagTypeSelectComponent;
|
11
frontend/src/component/feature/tag-type-select-container.jsx
Normal file
11
frontend/src/component/feature/tag-type-select-container.jsx
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import { connect } from 'react-redux';
|
||||||
|
import TagTypeSelectComponent from './tag-type-select-component';
|
||||||
|
import { fetchTagTypes } from '../../store/tag-type/actions';
|
||||||
|
|
||||||
|
const mapStateToProps = state => ({
|
||||||
|
types: state.tagTypes.toJS(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const FormAddContainer = connect(mapStateToProps, { fetchTagTypes })(TagTypeSelectComponent);
|
||||||
|
|
||||||
|
export default FormAddContainer;
|
@ -4,34 +4,12 @@ import Modal from 'react-modal';
|
|||||||
import { Button, Textfield, DialogActions, Grid, Cell, Icon, Switch } from 'react-mdl';
|
import { Button, Textfield, DialogActions, Grid, Cell, Icon, Switch } from 'react-mdl';
|
||||||
import styles from './variant.module.scss';
|
import styles from './variant.module.scss';
|
||||||
import MySelect from '../../common/select';
|
import MySelect from '../../common/select';
|
||||||
import { trim } from '../../common/util';
|
import { trim, modalStyles } from '../../common/util';
|
||||||
import { weightTypes } from './enums';
|
import { weightTypes } from './enums';
|
||||||
import OverrideConfig from './override-config';
|
import OverrideConfig from './override-config';
|
||||||
|
|
||||||
Modal.setAppElement('#app');
|
Modal.setAppElement('#app');
|
||||||
|
|
||||||
const customStyles = {
|
|
||||||
overlay: {
|
|
||||||
position: 'absolute',
|
|
||||||
top: 0,
|
|
||||||
left: 0,
|
|
||||||
right: 0,
|
|
||||||
bottom: 0,
|
|
||||||
backgroundColor: 'rgba(0, 0, 0, 0.25)',
|
|
||||||
zIndex: 5,
|
|
||||||
},
|
|
||||||
content: {
|
|
||||||
width: '500px',
|
|
||||||
maxWidth: '90%',
|
|
||||||
margin: '0',
|
|
||||||
top: '50%',
|
|
||||||
left: '50%',
|
|
||||||
right: 'auto',
|
|
||||||
bottom: 'auto',
|
|
||||||
transform: 'translate(-50%, -50%)',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const payloadOptions = [
|
const payloadOptions = [
|
||||||
{ key: 'string', label: 'string' },
|
{ key: 'string', label: 'string' },
|
||||||
{ key: 'json', label: 'json' },
|
{ key: 'json', label: 'json' },
|
||||||
@ -172,7 +150,7 @@ function AddVariant({ showDialog, closeDialog, save, validateName, editVariant,
|
|||||||
const isFixWeight = data.weightType === weightTypes.FIX;
|
const isFixWeight = data.weightType === weightTypes.FIX;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal isOpen={showDialog} contentLabel="Example Modal" style={customStyles} onRequestClose={onCancel}>
|
<Modal isOpen={showDialog} contentLabel="Example Modal" style={modalStyles} onRequestClose={onCancel}>
|
||||||
<h3>{title}</h3>
|
<h3>{title}</h3>
|
||||||
|
|
||||||
<form onSubmit={submit}>
|
<form onSubmit={submit}>
|
||||||
|
@ -63,6 +63,11 @@ exports[`renders correctly with one feature 1`] = `
|
|||||||
filled={true}
|
filled={true}
|
||||||
onChange={[Function]}
|
onChange={[Function]}
|
||||||
/>
|
/>
|
||||||
|
<FeatureTagComponent
|
||||||
|
featureToggleName="Another"
|
||||||
|
tags={Array []}
|
||||||
|
untagFeature={[MockFunction]}
|
||||||
|
/>
|
||||||
</react-mdl-CardText>
|
</react-mdl-CardText>
|
||||||
<react-mdl-CardActions
|
<react-mdl-CardActions
|
||||||
border={true}
|
border={true}
|
||||||
@ -95,6 +100,9 @@ exports[`renders correctly with one feature 1`] = `
|
|||||||
</react-mdl-Switch>
|
</react-mdl-Switch>
|
||||||
</span>
|
</span>
|
||||||
<div>
|
<div>
|
||||||
|
<AddTagDialog
|
||||||
|
featureToggleName="Another"
|
||||||
|
/>
|
||||||
<span>
|
<span>
|
||||||
<react-mdl-Button
|
<react-mdl-Button
|
||||||
className="mdl-button dropdownButton"
|
className="mdl-button dropdownButton"
|
||||||
|
@ -12,7 +12,9 @@ jest.mock('../update-strategies-container', () => ({
|
|||||||
}));
|
}));
|
||||||
jest.mock('../../feature-type-select-container', () => 'FeatureTypeSelect');
|
jest.mock('../../feature-type-select-container', () => 'FeatureTypeSelect');
|
||||||
jest.mock('../../project-select-container', () => 'ProjectSelect');
|
jest.mock('../../project-select-container', () => 'ProjectSelect');
|
||||||
|
jest.mock('../../tag-type-select-container', () => 'TagTypeSelect');
|
||||||
|
jest.mock('../../feature-tag-component', () => 'FeatureTagComponent');
|
||||||
|
jest.mock('../../add-tag-dialog-container', () => 'AddTagDialog');
|
||||||
test('renders correctly with one feature', () => {
|
test('renders correctly with one feature', () => {
|
||||||
const feature = {
|
const feature = {
|
||||||
name: 'Another',
|
name: 'Another',
|
||||||
@ -39,7 +41,10 @@ test('renders correctly with one feature', () => {
|
|||||||
featureToggle={feature}
|
featureToggle={feature}
|
||||||
fetchFeatureToggles={jest.fn()}
|
fetchFeatureToggles={jest.fn()}
|
||||||
history={{}}
|
history={{}}
|
||||||
|
featureTags={[]}
|
||||||
hasPermission={permission => [DELETE_FEATURE, UPDATE_FEATURE].indexOf(permission) !== -1}
|
hasPermission={permission => [DELETE_FEATURE, UPDATE_FEATURE].indexOf(permission) !== -1}
|
||||||
|
fetchTags={jest.fn()}
|
||||||
|
untagFeature={jest.fn()}
|
||||||
/>
|
/>
|
||||||
</MemoryRouter>
|
</MemoryRouter>
|
||||||
);
|
);
|
||||||
|
@ -13,7 +13,9 @@ import UpdateDescriptionComponent from './update-description-component';
|
|||||||
import { styles as commonStyles } from '../../common';
|
import { styles as commonStyles } from '../../common';
|
||||||
import { CREATE_FEATURE, DELETE_FEATURE, UPDATE_FEATURE } from '../../../permissions';
|
import { CREATE_FEATURE, DELETE_FEATURE, UPDATE_FEATURE } from '../../../permissions';
|
||||||
import StatusComponent from '../status-component';
|
import StatusComponent from '../status-component';
|
||||||
|
import FeatureTagComponent from '../feature-tag-component';
|
||||||
import StatusUpdateComponent from './status-update-component';
|
import StatusUpdateComponent from './status-update-component';
|
||||||
|
import AddTagDialog from '../add-tag-dialog-container';
|
||||||
|
|
||||||
const TABS = {
|
const TABS = {
|
||||||
strategies: 0,
|
strategies: 0,
|
||||||
@ -44,6 +46,10 @@ export default class ViewFeatureToggleComponent extends React.Component {
|
|||||||
featureToggle: PropTypes.object,
|
featureToggle: PropTypes.object,
|
||||||
history: PropTypes.object.isRequired,
|
history: PropTypes.object.isRequired,
|
||||||
hasPermission: PropTypes.func.isRequired,
|
hasPermission: PropTypes.func.isRequired,
|
||||||
|
fetchTags: PropTypes.func,
|
||||||
|
untagFeature: PropTypes.func,
|
||||||
|
featureTags: PropTypes.array,
|
||||||
|
tagTypes: PropTypes.array,
|
||||||
};
|
};
|
||||||
|
|
||||||
// eslint-disable-next-line camelcase
|
// eslint-disable-next-line camelcase
|
||||||
@ -55,6 +61,7 @@ export default class ViewFeatureToggleComponent extends React.Component {
|
|||||||
this.props.fetchArchive();
|
this.props.fetchArchive();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
this.props.fetchTags(this.props.featureToggleName);
|
||||||
}
|
}
|
||||||
|
|
||||||
getTabContent(activeTab) {
|
getTabContent(activeTab) {
|
||||||
@ -105,11 +112,13 @@ export default class ViewFeatureToggleComponent extends React.Component {
|
|||||||
features,
|
features,
|
||||||
activeTab,
|
activeTab,
|
||||||
revive,
|
revive,
|
||||||
// setValue,
|
|
||||||
featureToggleName,
|
featureToggleName,
|
||||||
toggleFeature,
|
toggleFeature,
|
||||||
removeFeatureToggle,
|
removeFeatureToggle,
|
||||||
hasPermission,
|
hasPermission,
|
||||||
|
featureTags,
|
||||||
|
tagTypes,
|
||||||
|
untagFeature,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
if (!featureToggle) {
|
if (!featureToggle) {
|
||||||
@ -214,6 +223,12 @@ export default class ViewFeatureToggleComponent extends React.Component {
|
|||||||
<FeatureTypeSelect value={featureToggle.type} onChange={updateType} filled />
|
<FeatureTypeSelect value={featureToggle.type} onChange={updateType} filled />
|
||||||
|
|
||||||
<ProjectSelect value={featureToggle.project} onChange={updateProject} filled />
|
<ProjectSelect value={featureToggle.project} onChange={updateProject} filled />
|
||||||
|
<FeatureTagComponent
|
||||||
|
featureToggleName={featureToggle.name}
|
||||||
|
tags={featureTags}
|
||||||
|
tagTypes={tagTypes}
|
||||||
|
untagFeature={untagFeature}
|
||||||
|
/>
|
||||||
</CardText>
|
</CardText>
|
||||||
|
|
||||||
<CardActions
|
<CardActions
|
||||||
@ -245,6 +260,7 @@ export default class ViewFeatureToggleComponent extends React.Component {
|
|||||||
|
|
||||||
{this.isFeatureView ? (
|
{this.isFeatureView ? (
|
||||||
<div>
|
<div>
|
||||||
|
<AddTagDialog featureToggleName={featureToggle.name} />
|
||||||
<StatusUpdateComponent stale={featureToggle.stale} updateStale={updateStale} />
|
<StatusUpdateComponent stale={featureToggle.stale} updateStale={updateStale} />
|
||||||
<Link
|
<Link
|
||||||
to={`/features/copy/${featureToggle.name}`}
|
to={`/features/copy/${featureToggle.name}`}
|
||||||
|
@ -11,11 +11,14 @@ import {
|
|||||||
|
|
||||||
import ViewToggleComponent from './view-component';
|
import ViewToggleComponent from './view-component';
|
||||||
import { hasPermission } from '../../../permissions';
|
import { hasPermission } from '../../../permissions';
|
||||||
|
import { fetchTags, tagFeature, untagFeature } from '../../../store/feature-tags/actions';
|
||||||
|
|
||||||
export default connect(
|
export default connect(
|
||||||
(state, props) => ({
|
(state, props) => ({
|
||||||
features: state.features.toJS(),
|
features: state.features.toJS(),
|
||||||
featureToggle: state.features.toJS().find(toggle => toggle.name === props.featureToggleName),
|
featureToggle: state.features.toJS().find(toggle => toggle.name === props.featureToggleName),
|
||||||
|
featureTags: state.featureTags.toJS(),
|
||||||
|
tagTypes: state.tagTypes.toJS(),
|
||||||
activeTab: props.activeTab,
|
activeTab: props.activeTab,
|
||||||
hasPermission: hasPermission.bind(null, state.user.get('profile')),
|
hasPermission: hasPermission.bind(null, state.user.get('profile')),
|
||||||
}),
|
}),
|
||||||
@ -26,5 +29,8 @@ export default connect(
|
|||||||
setStale,
|
setStale,
|
||||||
removeFeatureToggle,
|
removeFeatureToggle,
|
||||||
editFeatureToggle,
|
editFeatureToggle,
|
||||||
|
tagFeature,
|
||||||
|
untagFeature,
|
||||||
|
fetchTags,
|
||||||
}
|
}
|
||||||
)(ViewToggleComponent);
|
)(ViewToggleComponent);
|
||||||
|
@ -93,6 +93,19 @@ exports[`should render DrawerMenu 1`] = `
|
|||||||
|
|
||||||
Applications
|
Applications
|
||||||
</a>
|
</a>
|
||||||
|
<a
|
||||||
|
aria-current={null}
|
||||||
|
className="navigationLink mdl-color-text--grey-900"
|
||||||
|
href="/tag-types"
|
||||||
|
onClick={[Function]}
|
||||||
|
>
|
||||||
|
<react-mdl-Icon
|
||||||
|
className="navigationIcon"
|
||||||
|
name="label"
|
||||||
|
/>
|
||||||
|
|
||||||
|
Tag types
|
||||||
|
</a>
|
||||||
<a
|
<a
|
||||||
aria-current={null}
|
aria-current={null}
|
||||||
className="navigationLink mdl-color-text--grey-900"
|
className="navigationLink mdl-color-text--grey-900"
|
||||||
@ -208,6 +221,19 @@ exports[`should render DrawerMenu with "features" selected 1`] = `
|
|||||||
|
|
||||||
Applications
|
Applications
|
||||||
</a>
|
</a>
|
||||||
|
<a
|
||||||
|
aria-current={null}
|
||||||
|
className="navigationLink mdl-color-text--grey-900"
|
||||||
|
href="/tag-types"
|
||||||
|
onClick={[Function]}
|
||||||
|
>
|
||||||
|
<react-mdl-Icon
|
||||||
|
className="navigationIcon"
|
||||||
|
name="label"
|
||||||
|
/>
|
||||||
|
|
||||||
|
Tag types
|
||||||
|
</a>
|
||||||
<a
|
<a
|
||||||
aria-current={null}
|
aria-current={null}
|
||||||
className="navigationLink mdl-color-text--grey-900"
|
className="navigationLink mdl-color-text--grey-900"
|
||||||
|
@ -43,6 +43,13 @@ exports[`should render DrawerMenu 1`] = `
|
|||||||
>
|
>
|
||||||
Applications
|
Applications
|
||||||
</a>
|
</a>
|
||||||
|
<a
|
||||||
|
aria-current={null}
|
||||||
|
href="/tag-types"
|
||||||
|
onClick={[Function]}
|
||||||
|
>
|
||||||
|
Tag types
|
||||||
|
</a>
|
||||||
<a
|
<a
|
||||||
aria-current={null}
|
aria-current={null}
|
||||||
href="/logout"
|
href="/logout"
|
||||||
@ -147,6 +154,13 @@ exports[`should render DrawerMenu with "features" selected 1`] = `
|
|||||||
>
|
>
|
||||||
Applications
|
Applications
|
||||||
</a>
|
</a>
|
||||||
|
<a
|
||||||
|
aria-current={null}
|
||||||
|
href="/tag-types"
|
||||||
|
onClick={[Function]}
|
||||||
|
>
|
||||||
|
Tag types
|
||||||
|
</a>
|
||||||
<a
|
<a
|
||||||
aria-current={null}
|
aria-current={null}
|
||||||
href="/logout"
|
href="/logout"
|
||||||
|
@ -32,6 +32,12 @@ Array [
|
|||||||
"path": "/applications",
|
"path": "/applications",
|
||||||
"title": "Applications",
|
"title": "Applications",
|
||||||
},
|
},
|
||||||
|
Object {
|
||||||
|
"component": [Function],
|
||||||
|
"icon": "label",
|
||||||
|
"path": "/tag-types",
|
||||||
|
"title": "Tag types",
|
||||||
|
},
|
||||||
Object {
|
Object {
|
||||||
"component": [Function],
|
"component": [Function],
|
||||||
"icon": "exit_to_app",
|
"icon": "exit_to_app",
|
||||||
@ -159,6 +165,37 @@ Array [
|
|||||||
"path": "/projects",
|
"path": "/projects",
|
||||||
"title": "Projects",
|
"title": "Projects",
|
||||||
},
|
},
|
||||||
|
Object {
|
||||||
|
"component": [Function],
|
||||||
|
"parent": "/tag-types",
|
||||||
|
"path": "/tag-types/create",
|
||||||
|
"title": "Create",
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"component": [Function],
|
||||||
|
"parent": "/tag-types",
|
||||||
|
"path": "/tag-types/edit/:name",
|
||||||
|
"title": ":name",
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"component": [Function],
|
||||||
|
"icon": "label",
|
||||||
|
"path": "/tag-types",
|
||||||
|
"title": "Tag types",
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"component": [Function],
|
||||||
|
"parent": "/tags",
|
||||||
|
"path": "/tags/create",
|
||||||
|
"title": "Create",
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"component": [Function],
|
||||||
|
"hidden": true,
|
||||||
|
"icon": "label",
|
||||||
|
"path": "/tags",
|
||||||
|
"title": "Tags",
|
||||||
|
},
|
||||||
Object {
|
Object {
|
||||||
"component": [Function],
|
"component": [Function],
|
||||||
"icon": "exit_to_app",
|
"icon": "exit_to_app",
|
||||||
|
@ -1,12 +1,12 @@
|
|||||||
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(20);
|
expect(routes.length).toEqual(25);
|
||||||
expect(routes).toMatchSnapshot();
|
expect(routes).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('returns all baseRoutes', () => {
|
test('returns all baseRoutes', () => {
|
||||||
expect(baseRoutes.length).toEqual(6);
|
expect(baseRoutes.length).toEqual(7);
|
||||||
expect(baseRoutes).toMatchSnapshot();
|
expect(baseRoutes).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -18,6 +18,11 @@ import LogoutFeatures from '../../page/user/logout';
|
|||||||
import ListProjects from '../../page/project';
|
import ListProjects from '../../page/project';
|
||||||
import CreateProject from '../../page/project/create';
|
import CreateProject from '../../page/project/create';
|
||||||
import EditProject from '../../page/project/edit';
|
import EditProject from '../../page/project/edit';
|
||||||
|
import ListTagTypes from '../../page/tag-types';
|
||||||
|
import CreateTagType from '../../page/tag-types/create';
|
||||||
|
import EditTagType from '../../page/tag-types/edit';
|
||||||
|
import ListTags from '../../page/tags';
|
||||||
|
import CreateTag from '../../page/tags/create';
|
||||||
|
|
||||||
export const routes = [
|
export const routes = [
|
||||||
// Features
|
// Features
|
||||||
@ -58,6 +63,13 @@ export const routes = [
|
|||||||
{ path: '/projects/edit/:id', parent: '/projects', title: ':id', component: EditProject },
|
{ path: '/projects/edit/:id', parent: '/projects', title: ':id', component: EditProject },
|
||||||
{ path: '/projects', title: 'Projects', icon: 'folder_open', component: ListProjects, hidden: true },
|
{ path: '/projects', title: 'Projects', icon: 'folder_open', component: ListProjects, hidden: true },
|
||||||
|
|
||||||
|
{ path: '/tag-types/create', parent: '/tag-types', title: 'Create', component: CreateTagType },
|
||||||
|
{ path: '/tag-types/edit/:name', parent: '/tag-types', title: ':name', component: EditTagType },
|
||||||
|
{ path: '/tag-types', title: 'Tag types', icon: 'label', component: ListTagTypes },
|
||||||
|
|
||||||
|
{ path: '/tags/create', parent: '/tags', title: 'Create', component: CreateTag },
|
||||||
|
{ path: '/tags', title: 'Tags', icon: 'label', component: ListTags, hidden: true },
|
||||||
|
|
||||||
{ path: '/logout', title: 'Sign out', icon: 'exit_to_app', component: LogoutFeatures },
|
{ path: '/logout', title: 'Sign out', icon: 'exit_to_app', component: LogoutFeatures },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
@ -0,0 +1,191 @@
|
|||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`it supports editMode 1`] = `
|
||||||
|
<react-mdl-Card
|
||||||
|
className="fullWidth"
|
||||||
|
shadow={0}
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"overflow": "visible",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<react-mdl-CardTitle
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"paddingBottom": "0",
|
||||||
|
"paddingTop": "24px",
|
||||||
|
"wordBreak": "break-all",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Update
|
||||||
|
Tag type
|
||||||
|
</react-mdl-CardTitle>
|
||||||
|
<react-mdl-CardText>
|
||||||
|
Tag types allows you to group tags together in the management UI
|
||||||
|
</react-mdl-CardText>
|
||||||
|
<form
|
||||||
|
onSubmit={[Function]}
|
||||||
|
>
|
||||||
|
<section
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"padding": "16px",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<p
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"color": "red",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<react-mdl-Textfield
|
||||||
|
disabled={true}
|
||||||
|
floatingLabel={true}
|
||||||
|
label="Name"
|
||||||
|
name="name"
|
||||||
|
onBlur={[Function]}
|
||||||
|
onChange={[Function]}
|
||||||
|
placeholder="url-friendly-unique-name"
|
||||||
|
value=""
|
||||||
|
/>
|
||||||
|
<react-mdl-Textfield
|
||||||
|
floatingLabel={true}
|
||||||
|
label="Description"
|
||||||
|
name="description"
|
||||||
|
onChange={[Function]}
|
||||||
|
placeholder="Some short helpful descriptive text"
|
||||||
|
rows={1}
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"width": "100%",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
value=""
|
||||||
|
/>
|
||||||
|
</section>
|
||||||
|
<react-mdl-CardActions>
|
||||||
|
<div>
|
||||||
|
<react-mdl-Button
|
||||||
|
icon="add"
|
||||||
|
primary={true}
|
||||||
|
raised={true}
|
||||||
|
ripple={true}
|
||||||
|
type="submit"
|
||||||
|
>
|
||||||
|
<react-mdl-Icon
|
||||||
|
name="add"
|
||||||
|
/>
|
||||||
|
|
||||||
|
Update
|
||||||
|
</react-mdl-Button>
|
||||||
|
|
||||||
|
<react-mdl-Button
|
||||||
|
onClick={[Function]}
|
||||||
|
type="cancel"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</react-mdl-Button>
|
||||||
|
</div>
|
||||||
|
</react-mdl-CardActions>
|
||||||
|
</form>
|
||||||
|
</react-mdl-Card>
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`renders correctly for creating 1`] = `
|
||||||
|
<react-mdl-Card
|
||||||
|
className="fullWidth"
|
||||||
|
shadow={0}
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"overflow": "visible",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<react-mdl-CardTitle
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"paddingBottom": "0",
|
||||||
|
"paddingTop": "24px",
|
||||||
|
"wordBreak": "break-all",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Create
|
||||||
|
Tag type
|
||||||
|
</react-mdl-CardTitle>
|
||||||
|
<react-mdl-CardText>
|
||||||
|
Tag types allows you to group tags together in the management UI
|
||||||
|
</react-mdl-CardText>
|
||||||
|
<form
|
||||||
|
onSubmit={[Function]}
|
||||||
|
>
|
||||||
|
<section
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"padding": "16px",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<p
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"color": "red",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<react-mdl-Textfield
|
||||||
|
disabled={false}
|
||||||
|
floatingLabel={true}
|
||||||
|
label="Name"
|
||||||
|
name="name"
|
||||||
|
onBlur={[Function]}
|
||||||
|
onChange={[Function]}
|
||||||
|
placeholder="url-friendly-unique-name"
|
||||||
|
value=""
|
||||||
|
/>
|
||||||
|
<react-mdl-Textfield
|
||||||
|
floatingLabel={true}
|
||||||
|
label="Description"
|
||||||
|
name="description"
|
||||||
|
onChange={[Function]}
|
||||||
|
placeholder="Some short helpful descriptive text"
|
||||||
|
rows={1}
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"width": "100%",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
value=""
|
||||||
|
/>
|
||||||
|
</section>
|
||||||
|
<react-mdl-CardActions>
|
||||||
|
<div>
|
||||||
|
<react-mdl-Button
|
||||||
|
icon="add"
|
||||||
|
primary={true}
|
||||||
|
raised={true}
|
||||||
|
ripple={true}
|
||||||
|
type="submit"
|
||||||
|
>
|
||||||
|
<react-mdl-Icon
|
||||||
|
name="add"
|
||||||
|
/>
|
||||||
|
|
||||||
|
Create
|
||||||
|
</react-mdl-Button>
|
||||||
|
|
||||||
|
<react-mdl-Button
|
||||||
|
onClick={[Function]}
|
||||||
|
type="cancel"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</react-mdl-Button>
|
||||||
|
</div>
|
||||||
|
</react-mdl-CardActions>
|
||||||
|
</form>
|
||||||
|
</react-mdl-Card>
|
||||||
|
`;
|
@ -0,0 +1,141 @@
|
|||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`renders a list with elements correctly 1`] = `
|
||||||
|
<react-mdl-Card
|
||||||
|
className="fullwidth"
|
||||||
|
shadow={0}
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"overflow": "visible",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"borderBottom": "1px solid #f1f1f1",
|
||||||
|
"display": "flex",
|
||||||
|
"marginBottom": "10px",
|
||||||
|
"padding": "16px 20px ",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"flex": "2",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<h6
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"margin": 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Tag Types
|
||||||
|
</h6>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"flex": "1",
|
||||||
|
"textAlign": "right",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<react-mdl-IconButton
|
||||||
|
name="add"
|
||||||
|
onClick={[Function]}
|
||||||
|
raised={true}
|
||||||
|
title="Add new tag type"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<react-mdl-List>
|
||||||
|
<react-mdl-ListItem
|
||||||
|
twoLine={true}
|
||||||
|
>
|
||||||
|
<react-mdl-ListItemContent
|
||||||
|
icon="label"
|
||||||
|
subtitle="Some simple description"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
href="/tag-types/edit/simple"
|
||||||
|
onClick={[Function]}
|
||||||
|
>
|
||||||
|
<strong>
|
||||||
|
simple
|
||||||
|
</strong>
|
||||||
|
</a>
|
||||||
|
</react-mdl-ListItemContent>
|
||||||
|
<react-mdl-IconButton
|
||||||
|
name="delete"
|
||||||
|
onClick={[Function]}
|
||||||
|
/>
|
||||||
|
</react-mdl-ListItem>
|
||||||
|
</react-mdl-List>
|
||||||
|
</react-mdl-Card>
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`renders an empty list correctly 1`] = `
|
||||||
|
<react-mdl-Card
|
||||||
|
className="fullwidth"
|
||||||
|
shadow={0}
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"overflow": "visible",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"borderBottom": "1px solid #f1f1f1",
|
||||||
|
"display": "flex",
|
||||||
|
"marginBottom": "10px",
|
||||||
|
"padding": "16px 20px ",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"flex": "2",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<h6
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"margin": 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Tag Types
|
||||||
|
</h6>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"flex": "1",
|
||||||
|
"textAlign": "right",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<react-mdl-IconButton
|
||||||
|
name="add"
|
||||||
|
onClick={[Function]}
|
||||||
|
raised={true}
|
||||||
|
title="Add new tag type"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<react-mdl-List>
|
||||||
|
<react-mdl-ListItem>
|
||||||
|
No entries
|
||||||
|
</react-mdl-ListItem>
|
||||||
|
</react-mdl-List>
|
||||||
|
</react-mdl-Card>
|
||||||
|
`;
|
@ -0,0 +1,42 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import TagTypes from '../form-tag-type-component';
|
||||||
|
import renderer from 'react-test-renderer';
|
||||||
|
|
||||||
|
jest.mock('react-mdl');
|
||||||
|
|
||||||
|
test('renders correctly for creating', () => {
|
||||||
|
const tree = renderer
|
||||||
|
.create(
|
||||||
|
<TagTypes
|
||||||
|
history={{}}
|
||||||
|
title="Add tag type"
|
||||||
|
createTagType={jest.fn()}
|
||||||
|
validateName={() => Promise.resolve(true)}
|
||||||
|
hasPermission={() => true}
|
||||||
|
tagType={{ name: '', description: '', icon: '' }}
|
||||||
|
editMode={false}
|
||||||
|
submit={jest.fn()}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
.toJSON();
|
||||||
|
expect(tree).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('it supports editMode', () => {
|
||||||
|
const tree = renderer
|
||||||
|
.create(
|
||||||
|
<TagTypes
|
||||||
|
history={{}}
|
||||||
|
title="Add tag type"
|
||||||
|
createTagType={jest.fn()}
|
||||||
|
validateName={() => Promise.resolve(true)}
|
||||||
|
hasPermission={() => true}
|
||||||
|
tagType={{ name: '', description: '', icon: '' }}
|
||||||
|
editMode
|
||||||
|
submit={jest.fn()}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
.toJSON();
|
||||||
|
expect(tree).toMatchSnapshot();
|
||||||
|
});
|
@ -0,0 +1,35 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import TagTypesList from '../list-component';
|
||||||
|
import renderer from 'react-test-renderer';
|
||||||
|
import { MemoryRouter } from 'react-router-dom';
|
||||||
|
|
||||||
|
jest.mock('react-mdl');
|
||||||
|
|
||||||
|
test('renders an empty list correctly', () => {
|
||||||
|
const tree = renderer.create(
|
||||||
|
<TagTypesList
|
||||||
|
tagTypes={[]}
|
||||||
|
fetchTagTypes={jest.fn()}
|
||||||
|
removeTagType={jest.fn()}
|
||||||
|
history={{}}
|
||||||
|
hasPermission={() => true}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
expect(tree).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('renders a list with elements correctly', () => {
|
||||||
|
const tree = renderer.create(
|
||||||
|
<MemoryRouter>
|
||||||
|
<TagTypesList
|
||||||
|
tagTypes={[{ name: 'simple', description: 'Some simple description', icon: '#' }]}
|
||||||
|
fetchTagTypes={jest.fn()}
|
||||||
|
removeTagType={jest.fn()}
|
||||||
|
history={{}}
|
||||||
|
hasPermission={() => true}
|
||||||
|
/>
|
||||||
|
</MemoryRouter>
|
||||||
|
);
|
||||||
|
expect(tree).toMatchSnapshot();
|
||||||
|
});
|
@ -0,0 +1,17 @@
|
|||||||
|
import { connect } from 'react-redux';
|
||||||
|
import TagTypeComponent from './form-tag-type-component';
|
||||||
|
import { createTagType, validateName } from '../../store/tag-type/actions';
|
||||||
|
|
||||||
|
const mapStateToProps = () => ({
|
||||||
|
tagType: { name: '', description: '', icon: '' },
|
||||||
|
editMode: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const mapDispatchToProps = dispatch => ({
|
||||||
|
validateName: name => validateName(name),
|
||||||
|
submit: tagType => createTagType(tagType)(dispatch),
|
||||||
|
});
|
||||||
|
|
||||||
|
const FormAddContainer = connect(mapStateToProps, mapDispatchToProps)(TagTypeComponent);
|
||||||
|
|
||||||
|
export default FormAddContainer;
|
23
frontend/src/component/tag-types/edit-tag-type-container.js
Normal file
23
frontend/src/component/tag-types/edit-tag-type-container.js
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import { connect } from 'react-redux';
|
||||||
|
import Component from './form-tag-type-component';
|
||||||
|
import { updateTagType } from '../../store/tag-type/actions';
|
||||||
|
|
||||||
|
const mapStateToProps = (state, props) => {
|
||||||
|
const tagTypeBase = { name: '', description: '', icon: '' };
|
||||||
|
const realTagType = state.tagTypes.toJS().find(n => n.name === props.tagTypeName);
|
||||||
|
const tagType = Object.assign(tagTypeBase, realTagType);
|
||||||
|
|
||||||
|
return {
|
||||||
|
tagType,
|
||||||
|
editMode: true,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const mapDispatchToProps = dispatch => ({
|
||||||
|
validateName: () => {},
|
||||||
|
submit: tagType => updateTagType(tagType)(dispatch),
|
||||||
|
});
|
||||||
|
|
||||||
|
const FormAddContainer = connect(mapStateToProps, mapDispatchToProps)(Component);
|
||||||
|
|
||||||
|
export default FormAddContainer;
|
119
frontend/src/component/tag-types/form-tag-type-component.js
Normal file
119
frontend/src/component/tag-types/form-tag-type-component.js
Normal file
@ -0,0 +1,119 @@
|
|||||||
|
import React, { Component } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { Textfield, Card, CardTitle, CardText, CardActions } from 'react-mdl';
|
||||||
|
import { FormButtons, styles as commonStyles } from '../common';
|
||||||
|
import { trim } from '../common/util';
|
||||||
|
class AddTagTypeComponent extends Component {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
tagType: props.tagType,
|
||||||
|
errors: {},
|
||||||
|
dirty: false,
|
||||||
|
currentLegalValue: '',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
static getDerivedStateFromProps(props, state) {
|
||||||
|
if (!state.tagType && props.tagType.name) {
|
||||||
|
return { tagType: props.tagType };
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onValidateName = async evt => {
|
||||||
|
evt.preventDefault();
|
||||||
|
const name = evt.target.value;
|
||||||
|
const { errors } = this.state;
|
||||||
|
const { validateName } = this.props;
|
||||||
|
try {
|
||||||
|
await validateName(name);
|
||||||
|
errors.name = undefined;
|
||||||
|
} catch (err) {
|
||||||
|
errors.name = err.message;
|
||||||
|
}
|
||||||
|
this.setState({ errors });
|
||||||
|
};
|
||||||
|
|
||||||
|
setValue = (field, value) => {
|
||||||
|
const { tagType } = this.state;
|
||||||
|
tagType[field] = trim(value);
|
||||||
|
this.setState({ tagType, dirty: true });
|
||||||
|
};
|
||||||
|
|
||||||
|
onCancel = evt => {
|
||||||
|
evt.preventDefault();
|
||||||
|
this.props.history.push('/tag-types');
|
||||||
|
};
|
||||||
|
|
||||||
|
onSubmit = async evt => {
|
||||||
|
evt.preventDefault();
|
||||||
|
const { tagType } = this.state;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.props.submit(tagType);
|
||||||
|
this.props.history.push('/tag-types');
|
||||||
|
} catch (e) {
|
||||||
|
this.setState({
|
||||||
|
errors: {
|
||||||
|
general: e.message,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { tagType, errors } = this.state;
|
||||||
|
const { editMode } = this.props;
|
||||||
|
const submitText = editMode ? 'Update' : 'Create';
|
||||||
|
return (
|
||||||
|
<Card shadow={0} className={commonStyles.fullWidth} style={{ overflow: 'visible' }}>
|
||||||
|
<CardTitle style={{ paddingTop: '24px', paddingBottom: '0', wordBreak: 'break-all' }}>
|
||||||
|
{submitText} Tag type
|
||||||
|
</CardTitle>
|
||||||
|
<CardText>Tag types allows you to group tags together in the management UI</CardText>
|
||||||
|
<form onSubmit={this.onSubmit}>
|
||||||
|
<section style={{ padding: '16px' }}>
|
||||||
|
<p style={{ color: 'red' }}>{errors.general}</p>
|
||||||
|
<Textfield
|
||||||
|
floatingLabel
|
||||||
|
label="Name"
|
||||||
|
name="name"
|
||||||
|
placeholder="url-friendly-unique-name"
|
||||||
|
value={tagType.name}
|
||||||
|
error={errors.name}
|
||||||
|
disabled={editMode}
|
||||||
|
onBlur={this.onValidateName}
|
||||||
|
onChange={v => this.setValue('name', v.target.value)}
|
||||||
|
/>
|
||||||
|
<Textfield
|
||||||
|
floatingLabel
|
||||||
|
style={{ width: '100%' }}
|
||||||
|
label="Description"
|
||||||
|
name="description"
|
||||||
|
placeholder="Some short helpful descriptive text"
|
||||||
|
rows={1}
|
||||||
|
error={errors.description}
|
||||||
|
value={tagType.description}
|
||||||
|
onChange={v => this.setValue('description', v.target.value)}
|
||||||
|
/>
|
||||||
|
</section>
|
||||||
|
<CardActions>
|
||||||
|
<FormButtons submitText={submitText} onCancel={this.onCancel} />
|
||||||
|
</CardActions>
|
||||||
|
</form>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
AddTagTypeComponent.propTypes = {
|
||||||
|
tagType: PropTypes.object.isRequired,
|
||||||
|
validateName: PropTypes.func.isRequired,
|
||||||
|
submit: PropTypes.func.isRequired,
|
||||||
|
history: PropTypes.object.isRequired,
|
||||||
|
editMode: PropTypes.bool.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AddTagTypeComponent;
|
71
frontend/src/component/tag-types/list-component.jsx
Normal file
71
frontend/src/component/tag-types/list-component.jsx
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
import React, { Component } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
|
import { List, ListItem, ListItemContent, Card, IconButton } from 'react-mdl';
|
||||||
|
import { HeaderTitle, styles as commonStyles } from '../common';
|
||||||
|
import { CREATE_TAG_TYPE, DELETE_TAG_TYPE } from '../../permissions';
|
||||||
|
|
||||||
|
class TagTypesListComponent extends Component {
|
||||||
|
static propTypes = {
|
||||||
|
tagTypes: PropTypes.array.isRequired,
|
||||||
|
fetchTagTypes: PropTypes.func.isRequired,
|
||||||
|
removeTagType: PropTypes.func.isRequired,
|
||||||
|
history: PropTypes.object.isRequired,
|
||||||
|
hasPermission: PropTypes.func.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
this.props.fetchTagTypes();
|
||||||
|
}
|
||||||
|
|
||||||
|
removeTagType = (tagType, evt) => {
|
||||||
|
evt.preventDefault();
|
||||||
|
this.props.removeTagType(tagType);
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { tagTypes, hasPermission } = this.props;
|
||||||
|
return (
|
||||||
|
<Card shadow={0} className={commonStyles.fullwidth} style={{ overflow: 'visible' }}>
|
||||||
|
<HeaderTitle
|
||||||
|
title="Tag Types"
|
||||||
|
actions={
|
||||||
|
hasPermission(CREATE_TAG_TYPE) ? (
|
||||||
|
<IconButton
|
||||||
|
raised
|
||||||
|
name="add"
|
||||||
|
onClick={() => this.props.history.push('/tag-types/create')}
|
||||||
|
title="Add new tag type"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
''
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<List>
|
||||||
|
{tagTypes.length > 0 ? (
|
||||||
|
tagTypes.map((tagType, i) => (
|
||||||
|
<ListItem key={i} twoLine>
|
||||||
|
<ListItemContent icon="label" subtitle={tagType.description}>
|
||||||
|
<Link to={`/tag-types/edit/${tagType.name}`}>
|
||||||
|
<strong>{tagType.name}</strong>
|
||||||
|
</Link>
|
||||||
|
</ListItemContent>
|
||||||
|
{hasPermission(DELETE_TAG_TYPE) ? (
|
||||||
|
<IconButton name="delete" onClick={this.removeTagType.bind(this, tagType.name)} />
|
||||||
|
) : (
|
||||||
|
''
|
||||||
|
)}
|
||||||
|
</ListItem>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<ListItem>No entries</ListItem>
|
||||||
|
)}
|
||||||
|
</List>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default TagTypesListComponent;
|
26
frontend/src/component/tag-types/list-container.jsx
Normal file
26
frontend/src/component/tag-types/list-container.jsx
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import { connect } from 'react-redux';
|
||||||
|
import TagTypesListComponent from './list-component.jsx';
|
||||||
|
import { fetchTagTypes, removeTagType } from '../../store/tag-type/actions';
|
||||||
|
import { hasPermission } from '../../permissions';
|
||||||
|
|
||||||
|
const mapStateToProps = state => {
|
||||||
|
const list = state.tagTypes.toJS();
|
||||||
|
return {
|
||||||
|
tagTypes: list,
|
||||||
|
hasPermission: hasPermission.bind(null, state.user.get('profile')),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const mapDispatchToProps = dispatch => ({
|
||||||
|
removeTagType: tagtype => {
|
||||||
|
// eslint-disable-next-line no-alert
|
||||||
|
if (window.confirm('Are you sure you want to remove this tag type?')) {
|
||||||
|
removeTagType(tagtype)(dispatch);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
fetchTagTypes: () => fetchTagTypes()(dispatch),
|
||||||
|
});
|
||||||
|
|
||||||
|
const TagTypesListContainer = connect(mapStateToProps, mapDispatchToProps)(TagTypesListComponent);
|
||||||
|
|
||||||
|
export default TagTypesListContainer;
|
15
frontend/src/component/tags/create-tag-container.js
Normal file
15
frontend/src/component/tags/create-tag-container.js
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import { connect } from 'react-redux';
|
||||||
|
import TagComponent from './form-tag-component';
|
||||||
|
import { create } from '../../store/tag/actions';
|
||||||
|
|
||||||
|
const mapStateToProps = () => ({
|
||||||
|
tag: { type: '', value: '' },
|
||||||
|
});
|
||||||
|
|
||||||
|
const mapDispatchToProps = dispatch => ({
|
||||||
|
submit: tag => create(tag)(dispatch),
|
||||||
|
});
|
||||||
|
|
||||||
|
const FormAddContainer = connect(mapStateToProps, mapDispatchToProps)(TagComponent);
|
||||||
|
|
||||||
|
export default FormAddContainer;
|
96
frontend/src/component/tags/form-tag-component.js
Normal file
96
frontend/src/component/tags/form-tag-component.js
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
import React, { Component } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { Textfield, Card, CardTitle, CardActions } from 'react-mdl';
|
||||||
|
import { FormButtons, styles as commonStyles } from '../common';
|
||||||
|
import TagTypeSelect from '../feature/tag-type-select-container';
|
||||||
|
import { trim } from '../common/util';
|
||||||
|
class AddTagComponent extends Component {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
tag: props.tag,
|
||||||
|
errors: {},
|
||||||
|
dirty: false,
|
||||||
|
currentLegalValue: '',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
static getDerivedStateFromProps(props, state) {
|
||||||
|
if (!state.tag.id && props.tag.id) {
|
||||||
|
return { tag: props.tag };
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setValue = (field, value) => {
|
||||||
|
const { tag } = this.state;
|
||||||
|
tag[field] = value;
|
||||||
|
this.setState({ tag, dirty: true });
|
||||||
|
};
|
||||||
|
|
||||||
|
onCancel = evt => {
|
||||||
|
evt.preventDefault();
|
||||||
|
this.props.history.push('/tags');
|
||||||
|
};
|
||||||
|
|
||||||
|
onSubmit = async evt => {
|
||||||
|
evt.preventDefault();
|
||||||
|
const { tag } = this.state;
|
||||||
|
if (!tag.type || tag.type === '') {
|
||||||
|
tag.type = 'simple';
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await this.props.submit(tag);
|
||||||
|
this.props.history.push('/tags');
|
||||||
|
} catch (e) {
|
||||||
|
this.setState({
|
||||||
|
errors: {
|
||||||
|
general: e.message,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { tag, errors } = this.state;
|
||||||
|
const submitText = 'Create';
|
||||||
|
return (
|
||||||
|
<Card shadow={0} className={commonStyles.fullWidth} style={{ overflow: 'visible' }}>
|
||||||
|
<CardTitle style={{ paddingTop: '24px', paddingBottom: '0', wordBreak: 'break-all' }}>
|
||||||
|
{submitText} Tag
|
||||||
|
</CardTitle>
|
||||||
|
<form onSubmit={this.onSubmit}>
|
||||||
|
<section style={{ padding: '16px' }}>
|
||||||
|
<p style={{ color: 'red' }}>{errors.general}</p>
|
||||||
|
<TagTypeSelect
|
||||||
|
name="type"
|
||||||
|
value={tag.type}
|
||||||
|
onChange={v => this.setValue('type', v.target.value)}
|
||||||
|
/>
|
||||||
|
<Textfield
|
||||||
|
floatingLabel
|
||||||
|
label="Value"
|
||||||
|
name="value"
|
||||||
|
placeholder="Your tag"
|
||||||
|
value={tag.value}
|
||||||
|
error={errors.value}
|
||||||
|
onChange={v => this.setValue('value', trim(v.target.value))}
|
||||||
|
/>
|
||||||
|
</section>
|
||||||
|
<CardActions>
|
||||||
|
<FormButtons submitText={submitText} onCancel={this.onCancel} />
|
||||||
|
</CardActions>
|
||||||
|
</form>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
AddTagComponent.propTypes = {
|
||||||
|
tag: PropTypes.object.isRequired,
|
||||||
|
submit: PropTypes.func.isRequired,
|
||||||
|
history: PropTypes.object.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AddTagComponent;
|
71
frontend/src/component/tags/list-component.jsx
Normal file
71
frontend/src/component/tags/list-component.jsx
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
import React, { Component } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
import { List, ListItem, ListItemContent, Card, IconButton } from 'react-mdl';
|
||||||
|
import { HeaderTitle, styles as commonStyles } from '../common';
|
||||||
|
import { CREATE_TAG, DELETE_TAG } from '../../permissions';
|
||||||
|
|
||||||
|
class TagsListComponent extends Component {
|
||||||
|
static propTypes = {
|
||||||
|
tags: PropTypes.array.isRequired,
|
||||||
|
fetchTags: PropTypes.func.isRequired,
|
||||||
|
removeTag: PropTypes.func.isRequired,
|
||||||
|
history: PropTypes.object.isRequired,
|
||||||
|
hasPermission: PropTypes.func.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
this.props.fetchTags();
|
||||||
|
}
|
||||||
|
|
||||||
|
removeTag = (tag, evt) => {
|
||||||
|
evt.preventDefault();
|
||||||
|
this.props.removeTag(tag);
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { tags, hasPermission } = this.props;
|
||||||
|
return (
|
||||||
|
<Card shadow={0} className={commonStyles.fullwidth} style={{ overflow: 'visible' }}>
|
||||||
|
<HeaderTitle
|
||||||
|
title="Tags"
|
||||||
|
actions={
|
||||||
|
hasPermission(CREATE_TAG) ? (
|
||||||
|
<IconButton
|
||||||
|
raised
|
||||||
|
name="add"
|
||||||
|
onClick={() => this.props.history.push('/tags/create')}
|
||||||
|
title="Add new tag"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
''
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<List>
|
||||||
|
{tags.length > 0 ? (
|
||||||
|
tags.map((tag, i) => (
|
||||||
|
<ListItem key={i} twoLine>
|
||||||
|
<ListItemContent icon="label" subtitle={tag.type}>
|
||||||
|
<strong>{tag.value}</strong>
|
||||||
|
</ListItemContent>
|
||||||
|
{hasPermission(DELETE_TAG) ? (
|
||||||
|
<IconButton
|
||||||
|
name="delete"
|
||||||
|
onClick={this.removeTag.bind(this, { type: tag.type, value: tag.value })}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
''
|
||||||
|
)}
|
||||||
|
</ListItem>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<ListItem>No entries</ListItem>
|
||||||
|
)}
|
||||||
|
</List>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default TagsListComponent;
|
26
frontend/src/component/tags/list-container.jsx
Normal file
26
frontend/src/component/tags/list-container.jsx
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import { connect } from 'react-redux';
|
||||||
|
import TagsListComponent from './list-component.jsx';
|
||||||
|
import { fetchTags, removeTag } from '../../store/tag/actions';
|
||||||
|
import { hasPermission } from '../../permissions';
|
||||||
|
|
||||||
|
const mapStateToProps = state => {
|
||||||
|
const list = state.tags.toJS();
|
||||||
|
return {
|
||||||
|
tags: list,
|
||||||
|
hasPermission: hasPermission.bind(null, state.user.get('profile')),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const mapDispatchToProps = dispatch => ({
|
||||||
|
removeTag: tag => {
|
||||||
|
// eslint-disable-next-line no-alert
|
||||||
|
if (window.confirm('Are you sure you want to remove this tag?')) {
|
||||||
|
removeTag(tag)(dispatch);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
fetchTags: () => fetchTags()(dispatch),
|
||||||
|
});
|
||||||
|
|
||||||
|
const TagsListContainer = connect(mapStateToProps, mapDispatchToProps)(TagsListComponent);
|
||||||
|
|
||||||
|
export default TagsListContainer;
|
11
frontend/src/page/tag-types/create.js
Normal file
11
frontend/src/page/tag-types/create.js
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import AddTagType from '../../component/tag-types/create-tag-type-container';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
const render = ({ history }) => <AddTagType title="Add tag type" history={history} />;
|
||||||
|
|
||||||
|
render.propTypes = {
|
||||||
|
history: PropTypes.object.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default render;
|
13
frontend/src/page/tag-types/edit.js
Normal file
13
frontend/src/page/tag-types/edit.js
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import EditTagType from './../../component/tag-types/edit-tag-type-container';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
const render = ({ match: { params }, history }) => (
|
||||||
|
<EditTagType tagTypeName={params.name} title="Edit Tag type" history={history} />
|
||||||
|
);
|
||||||
|
render.propTypes = {
|
||||||
|
match: PropTypes.object.isRequired,
|
||||||
|
history: PropTypes.object.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default render;
|
11
frontend/src/page/tag-types/index.js
Normal file
11
frontend/src/page/tag-types/index.js
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import TagTypes from '../../component/tag-types/list-container';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
const render = ({ history }) => <TagTypes history={history} />;
|
||||||
|
|
||||||
|
render.propTypes = {
|
||||||
|
history: PropTypes.object.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default render;
|
11
frontend/src/page/tags/create.js
Normal file
11
frontend/src/page/tags/create.js
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import AddTag from '../../component/tags/create-tag-container';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
const render = ({ history }) => <AddTag title="Add Tag" history={history} />;
|
||||||
|
|
||||||
|
render.propTypes = {
|
||||||
|
history: PropTypes.object.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default render;
|
11
frontend/src/page/tags/index.js
Normal file
11
frontend/src/page/tags/index.js
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import Tags from '../../component/tags/list-container';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
const render = ({ history }) => <Tags history={history} />;
|
||||||
|
|
||||||
|
render.propTypes = {
|
||||||
|
history: PropTypes.object.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default render;
|
@ -12,6 +12,11 @@ export const DELETE_CONTEXT_FIELD = 'DELETE_CONTEXT_FIELD';
|
|||||||
export const CREATE_PROJECT = 'CREATE_PROJECT';
|
export const CREATE_PROJECT = 'CREATE_PROJECT';
|
||||||
export const UPDATE_PROJECT = 'UPDATE_PROJECT';
|
export const UPDATE_PROJECT = 'UPDATE_PROJECT';
|
||||||
export const DELETE_PROJECT = 'DELETE_PROJECT';
|
export const DELETE_PROJECT = 'DELETE_PROJECT';
|
||||||
|
export const CREATE_TAG_TYPE = 'CREATE_TAG_TYPE';
|
||||||
|
export const DELETE_TAG_TYPE = 'DELETE_TAG_TYPE';
|
||||||
|
export const UPDATE_TAG_TYPE = 'UPDATE_TAG_TYPE';
|
||||||
|
export const CREATE_TAG = 'CREATE_TAG';
|
||||||
|
export const DELETE_TAG = 'DELETE_TAG';
|
||||||
|
|
||||||
export function hasPermission(user, permission) {
|
export function hasPermission(user, permission) {
|
||||||
return (
|
return (
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
const defaultErrorMessage = 'Unexptected exception when talking to unleash-api';
|
const defaultErrorMessage = 'Unexpected exception when talking to unleash-api';
|
||||||
|
|
||||||
function extractJoiMsg(body) {
|
function extractJoiMsg(body) {
|
||||||
return body.details.length > 0 ? body.details[0].message : defaultErrorMessage;
|
return body.details.length > 0 ? body.details[0].message : defaultErrorMessage;
|
||||||
|
50
frontend/src/store/feature-tags/actions.js
Normal file
50
frontend/src/store/feature-tags/actions.js
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
import api from './api';
|
||||||
|
import { dispatchAndThrow } from '../util';
|
||||||
|
|
||||||
|
export const TAG_FEATURE_TOGGLE = 'TAG_FEATURE_TOGGLE';
|
||||||
|
export const UNTAG_FEATURE_TOGGLE = 'UNTAG_FEATURE_TOGGLE';
|
||||||
|
export const START_TAG_FEATURE_TOGGLE = 'START_TAG_FEATURE_TOGGLE';
|
||||||
|
export const START_UNTAG_FEATURE_TOGGLE = 'START_UNTAG_FEATURE_TOGGLE';
|
||||||
|
export const ERROR_TAG_FEATURE_TOGGLE = 'ERROR_TAG_FEATURE_TOGGLE';
|
||||||
|
export const ERROR_UNTAG_FEATURE_TOGGLE = 'ERROR_UNTAG_FEATURE_TOGGLE';
|
||||||
|
export const START_FETCH_FEATURE_TOGGLE_TAGS = 'START_FETCH_FEATURE_TOGGLE_TAGS';
|
||||||
|
export const RECEIVE_FEATURE_TOGGLE_TAGS = 'RECEIVE_FEATURE_TOGGLE_TAGS';
|
||||||
|
export const ERROR_FETCH_FEATURE_TOGGLE_TAGS = 'ERROR_FETCH_FEATURE_TOGGLE_TAGS';
|
||||||
|
|
||||||
|
function receiveFeatureToggleTags(json) {
|
||||||
|
return {
|
||||||
|
type: RECEIVE_FEATURE_TOGGLE_TAGS,
|
||||||
|
value: json,
|
||||||
|
receivedAt: Date.now(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function tagFeature(featureToggle, tag) {
|
||||||
|
return dispatch => {
|
||||||
|
dispatch({ type: START_TAG_FEATURE_TOGGLE });
|
||||||
|
return api
|
||||||
|
.tagFeature(featureToggle, tag)
|
||||||
|
.then(json => dispatch({ type: TAG_FEATURE_TOGGLE, featureToggle, tag: json }))
|
||||||
|
.catch(dispatchAndThrow(dispatch, ERROR_TAG_FEATURE_TOGGLE));
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function untagFeature(featureToggle, tag) {
|
||||||
|
return dispatch => {
|
||||||
|
dispatch({ type: START_UNTAG_FEATURE_TOGGLE });
|
||||||
|
return api
|
||||||
|
.untagFeature(featureToggle, tag)
|
||||||
|
.then(() => dispatch({ type: UNTAG_FEATURE_TOGGLE, featureToggle, tag }))
|
||||||
|
.catch(dispatchAndThrow(dispatch, ERROR_UNTAG_FEATURE_TOGGLE));
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fetchTags(featureToggle) {
|
||||||
|
return dispatch => {
|
||||||
|
dispatch({ type: START_FETCH_FEATURE_TOGGLE_TAGS });
|
||||||
|
return api
|
||||||
|
.fetchFeatureToggleTags(featureToggle)
|
||||||
|
.then(json => dispatch(receiveFeatureToggleTags(json)))
|
||||||
|
.catch(dispatchAndThrow(dispatch, ERROR_FETCH_FEATURE_TOGGLE_TAGS));
|
||||||
|
};
|
||||||
|
}
|
38
frontend/src/store/feature-tags/api.js
Normal file
38
frontend/src/store/feature-tags/api.js
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
import { throwIfNotSuccess, headers } from '../api-helper';
|
||||||
|
|
||||||
|
const URI = 'api/admin/features';
|
||||||
|
|
||||||
|
function tagFeature(featureToggle, tag) {
|
||||||
|
return fetch(`${URI}/${featureToggle}/tags`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers,
|
||||||
|
credentials: 'include',
|
||||||
|
body: JSON.stringify(tag),
|
||||||
|
})
|
||||||
|
.then(throwIfNotSuccess)
|
||||||
|
.then(response => response.json());
|
||||||
|
}
|
||||||
|
|
||||||
|
function untagFeature(featureToggle, tag) {
|
||||||
|
return fetch(`${URI}/${featureToggle}/tags/${tag.type}/${tag.value}`, {
|
||||||
|
method: 'DELETE',
|
||||||
|
headers,
|
||||||
|
credentials: 'include',
|
||||||
|
}).then(throwIfNotSuccess);
|
||||||
|
}
|
||||||
|
|
||||||
|
function fetchFeatureToggleTags(featureToggle) {
|
||||||
|
return fetch(`${URI}/${featureToggle}/tags`, {
|
||||||
|
method: 'GET',
|
||||||
|
headers,
|
||||||
|
credentials: 'include',
|
||||||
|
})
|
||||||
|
.then(throwIfNotSuccess)
|
||||||
|
.then(response => response.json());
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
tagFeature,
|
||||||
|
untagFeature,
|
||||||
|
fetchFeatureToggleTags,
|
||||||
|
};
|
25
frontend/src/store/feature-tags/index.js
Normal file
25
frontend/src/store/feature-tags/index.js
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import { List, Map as $MAP } from 'immutable';
|
||||||
|
import { RECEIVE_FEATURE_TOGGLE_TAGS, TAG_FEATURE_TOGGLE, UNTAG_FEATURE_TOGGLE } from './actions';
|
||||||
|
|
||||||
|
function getInitState() {
|
||||||
|
return new List();
|
||||||
|
}
|
||||||
|
|
||||||
|
const featureTags = (state = getInitState(), action) => {
|
||||||
|
switch (action.type) {
|
||||||
|
case RECEIVE_FEATURE_TOGGLE_TAGS:
|
||||||
|
if (action.value) {
|
||||||
|
return new List(action.value.tags);
|
||||||
|
} else {
|
||||||
|
return getInitState();
|
||||||
|
}
|
||||||
|
case TAG_FEATURE_TOGGLE:
|
||||||
|
return state.push(new $MAP(action.tag));
|
||||||
|
case UNTAG_FEATURE_TOGGLE:
|
||||||
|
return state.remove(state.indexOf(t => t.id === action.value.tagId));
|
||||||
|
default:
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default featureTags;
|
@ -2,6 +2,9 @@ import { combineReducers } from 'redux';
|
|||||||
import features from './feature-toggle';
|
import features from './feature-toggle';
|
||||||
import featureTypes from './feature-type';
|
import featureTypes from './feature-type';
|
||||||
import featureMetrics from './feature-metrics';
|
import featureMetrics from './feature-metrics';
|
||||||
|
import featureTags from './feature-tags';
|
||||||
|
import tagTypes from './tag-type';
|
||||||
|
import tags from './tag';
|
||||||
import strategies from './strategy';
|
import strategies from './strategy';
|
||||||
import history from './history'; // eslint-disable-line
|
import history from './history'; // eslint-disable-line
|
||||||
import archive from './archive';
|
import archive from './archive';
|
||||||
@ -18,6 +21,9 @@ const unleashStore = combineReducers({
|
|||||||
featureTypes,
|
featureTypes,
|
||||||
featureMetrics,
|
featureMetrics,
|
||||||
strategies,
|
strategies,
|
||||||
|
tagTypes,
|
||||||
|
tags,
|
||||||
|
featureTags,
|
||||||
history,
|
history,
|
||||||
archive,
|
archive,
|
||||||
error,
|
error,
|
||||||
|
@ -3,6 +3,7 @@ import { fetchContext } from './context/actions';
|
|||||||
import { fetchFeatureTypes } from './feature-type/actions';
|
import { fetchFeatureTypes } from './feature-type/actions';
|
||||||
import { fetchProjects } from './project/actions';
|
import { fetchProjects } from './project/actions';
|
||||||
import { fetchStrategies } from './strategy/actions';
|
import { fetchStrategies } from './strategy/actions';
|
||||||
|
import { fetchTagTypes } from './tag-type/actions';
|
||||||
|
|
||||||
export function loadInitalData() {
|
export function loadInitalData() {
|
||||||
return dispatch => {
|
return dispatch => {
|
||||||
@ -11,5 +12,6 @@ export function loadInitalData() {
|
|||||||
fetchFeatureTypes()(dispatch);
|
fetchFeatureTypes()(dispatch);
|
||||||
fetchProjects()(dispatch);
|
fetchProjects()(dispatch);
|
||||||
fetchStrategies()(dispatch);
|
fetchStrategies()(dispatch);
|
||||||
|
fetchTagTypes()(dispatch);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
67
frontend/src/store/tag-type/actions.js
Normal file
67
frontend/src/store/tag-type/actions.js
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
import api from './api';
|
||||||
|
import { dispatchAndThrow } from '../util';
|
||||||
|
|
||||||
|
export const START_FETCH_TAG_TYPES = 'START_FETCH_TAG_TYPES';
|
||||||
|
export const RECEIVE_TAG_TYPES = 'RECEIVE_TAG_TYPES';
|
||||||
|
export const ERROR_FETCH_TAG_TYPES = 'ERROR_FETCH_TAG_TYPES';
|
||||||
|
export const START_CREATE_TAG_TYPE = 'START_CREATE_TAG_TYPE';
|
||||||
|
export const ADD_TAG_TYPE = 'ADD_TAG_TYPE';
|
||||||
|
export const ERROR_CREATE_TAG_TYPE = 'ERROR_CREATE_TAG_TYPE';
|
||||||
|
export const START_DELETE_TAG_TYPE = 'START_DELETE_TAG_TYPE';
|
||||||
|
export const DELETE_TAG_TYPE = 'DELETE_TAG_TYPE';
|
||||||
|
export const ERROR_DELETE_TAG_TYPE = 'ERROR_DELETE_TAG_TYPE';
|
||||||
|
export const START_UPDATE_TAG_TYPE = 'START_UPDATE_TAG_TYPE';
|
||||||
|
export const UPDATE_TAG_TYPE = 'UPDATE_TAG_TYPE';
|
||||||
|
export const ERROR_UPDATE_TAG_TYPE = 'ERROR_UPDATE_TAG_TYPE';
|
||||||
|
|
||||||
|
function receiveTagTypes(json) {
|
||||||
|
return {
|
||||||
|
type: RECEIVE_TAG_TYPES,
|
||||||
|
value: json,
|
||||||
|
receivedAt: Date.now(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fetchTagTypes() {
|
||||||
|
return dispatch => {
|
||||||
|
dispatch({ type: START_FETCH_TAG_TYPES });
|
||||||
|
return api
|
||||||
|
.fetchTagTypes()
|
||||||
|
.then(json => dispatch(receiveTagTypes(json)))
|
||||||
|
.catch(dispatchAndThrow(dispatch, ERROR_FETCH_TAG_TYPES));
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createTagType({ name, description, icon }) {
|
||||||
|
return dispatch => {
|
||||||
|
dispatch({ type: START_CREATE_TAG_TYPE });
|
||||||
|
return api
|
||||||
|
.create({ name, description, icon })
|
||||||
|
.then(() => dispatch({ type: ADD_TAG_TYPE, tagType: { name, description, icon } }))
|
||||||
|
.catch(dispatchAndThrow(dispatch, ERROR_CREATE_TAG_TYPE));
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function updateTagType({ name, description, icon }) {
|
||||||
|
return dispatch => {
|
||||||
|
dispatch({ type: START_UPDATE_TAG_TYPE });
|
||||||
|
return api
|
||||||
|
.update({ name, description, icon })
|
||||||
|
.then(() => dispatch({ type: UPDATE_TAG_TYPE, tagType: { name, description, icon } }))
|
||||||
|
.catch(dispatchAndThrow(dispatch, ERROR_UPDATE_TAG_TYPE));
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function removeTagType(name) {
|
||||||
|
return dispatch => {
|
||||||
|
dispatch({ type: START_DELETE_TAG_TYPE });
|
||||||
|
return api
|
||||||
|
.deleteTagType(name)
|
||||||
|
.then(() => dispatch({ type: DELETE_TAG_TYPE, tagType: { name } }))
|
||||||
|
.catch(dispatchAndThrow(dispatch, ERROR_DELETE_TAG_TYPE));
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function validateName(name) {
|
||||||
|
return api.validateTagType({ name });
|
||||||
|
}
|
60
frontend/src/store/tag-type/api.js
Normal file
60
frontend/src/store/tag-type/api.js
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
import { throwIfNotSuccess, headers } from '../api-helper';
|
||||||
|
|
||||||
|
const URI = 'api/admin/tag-types';
|
||||||
|
|
||||||
|
function fetchTagTypes() {
|
||||||
|
return fetch(URI, { credentials: 'include' })
|
||||||
|
.then(throwIfNotSuccess)
|
||||||
|
.then(response => response.json());
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateTagType(tagType) {
|
||||||
|
return fetch(`${URI}/validate`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers,
|
||||||
|
credentials: 'include',
|
||||||
|
body: JSON.stringify(tagType),
|
||||||
|
}).then(throwIfNotSuccess);
|
||||||
|
}
|
||||||
|
|
||||||
|
function create(tagType) {
|
||||||
|
return validateTagType(tagType)
|
||||||
|
.then(() =>
|
||||||
|
fetch(URI, {
|
||||||
|
method: 'POST',
|
||||||
|
headers,
|
||||||
|
credentials: 'include',
|
||||||
|
body: JSON.stringify(tagType),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.then(throwIfNotSuccess);
|
||||||
|
}
|
||||||
|
|
||||||
|
function update(tagType) {
|
||||||
|
return validateTagType(tagType)
|
||||||
|
.then(() =>
|
||||||
|
fetch(`${URI}/${tagType.name}`, {
|
||||||
|
method: 'PUT',
|
||||||
|
headers,
|
||||||
|
credentials: 'include',
|
||||||
|
body: JSON.stringify(tagType),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.then(throwIfNotSuccess);
|
||||||
|
}
|
||||||
|
|
||||||
|
function deleteTagType(tagTypeName) {
|
||||||
|
return fetch(`${URI}/${tagTypeName}`, {
|
||||||
|
method: 'DELETE',
|
||||||
|
headers,
|
||||||
|
credentials: 'include',
|
||||||
|
}).then(throwIfNotSuccess);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
fetchTagTypes,
|
||||||
|
create,
|
||||||
|
update,
|
||||||
|
deleteTagType,
|
||||||
|
validateTagType,
|
||||||
|
};
|
33
frontend/src/store/tag-type/index.js
Normal file
33
frontend/src/store/tag-type/index.js
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
import { List, Map as $Map } from 'immutable';
|
||||||
|
const debug = require('debug')('unleash:tag-type-store');
|
||||||
|
|
||||||
|
import { RECEIVE_TAG_TYPES, ADD_TAG_TYPE, DELETE_TAG_TYPE, UPDATE_TAG_TYPE, ERROR_FETCH_TAG_TYPES } from './actions';
|
||||||
|
|
||||||
|
const tagTypes = (state = new List([]), action) => {
|
||||||
|
switch (action.type) {
|
||||||
|
case ADD_TAG_TYPE:
|
||||||
|
debug('Add tagtype');
|
||||||
|
return state.push(new $Map(action.tagType));
|
||||||
|
case DELETE_TAG_TYPE:
|
||||||
|
debug('Delete tagtype');
|
||||||
|
return state.filter(tagtype => tagtype.get('name') !== action.tagType.name);
|
||||||
|
case RECEIVE_TAG_TYPES:
|
||||||
|
debug('Receive tag types', action);
|
||||||
|
return new List(action.value.tagTypes.map($Map));
|
||||||
|
case ERROR_FETCH_TAG_TYPES:
|
||||||
|
debug('Error receiving tag types', action);
|
||||||
|
return state;
|
||||||
|
case UPDATE_TAG_TYPE:
|
||||||
|
return state.map(tagtype => {
|
||||||
|
if (tagtype.get('name') === action.tagType.name) {
|
||||||
|
return new $Map(action.tagType);
|
||||||
|
} else {
|
||||||
|
return tagtype;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
default:
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default tagTypes;
|
50
frontend/src/store/tag/actions.js
Normal file
50
frontend/src/store/tag/actions.js
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
import api from './api';
|
||||||
|
import { dispatchAndThrow } from '../util';
|
||||||
|
|
||||||
|
export const START_FETCH_TAGS = 'START_FETCH_TAGS';
|
||||||
|
export const RECEIVE_TAGS = 'RECEIVE_TAGS';
|
||||||
|
export const ERROR_FETCH_TAGS = 'ERROR_FETCH_TAGS';
|
||||||
|
export const START_CREATE_TAG = 'START_CREATE_TAG';
|
||||||
|
export const ADD_TAG = 'ADD_TAG';
|
||||||
|
export const ERROR_CREATE_TAG = 'ERROR_CREATE_TAG';
|
||||||
|
export const START_DELETE_TAG = 'START_DELETE_TAG';
|
||||||
|
export const DELETE_TAG = 'DELETE_TAG';
|
||||||
|
export const ERROR_DELETE_TAG = 'ERROR_DELETE_TAG';
|
||||||
|
|
||||||
|
function receiveTags(json) {
|
||||||
|
return {
|
||||||
|
type: RECEIVE_TAGS,
|
||||||
|
value: json,
|
||||||
|
receivedAt: Date.now(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fetchTags() {
|
||||||
|
return dispatch => {
|
||||||
|
dispatch({ type: START_FETCH_TAGS });
|
||||||
|
return api
|
||||||
|
.fetchTags()
|
||||||
|
.then(json => dispatch(receiveTags(json)))
|
||||||
|
.catch(dispatchAndThrow(dispatch, ERROR_FETCH_TAGS));
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function create({ type, value }) {
|
||||||
|
return dispatch => {
|
||||||
|
dispatch({ type: START_CREATE_TAG });
|
||||||
|
return api
|
||||||
|
.create({ type, value })
|
||||||
|
.then(() => dispatch({ type: ADD_TAG, tag: { type, value } }))
|
||||||
|
.catch(dispatchAndThrow(dispatch, ERROR_CREATE_TAG));
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function removeTag(tag) {
|
||||||
|
return dispatch => {
|
||||||
|
dispatch({ type: START_DELETE_TAG });
|
||||||
|
return api
|
||||||
|
.deleteTag(tag)
|
||||||
|
.then(() => dispatch({ type: DELETE_TAG, tag }))
|
||||||
|
.catch(dispatchAndThrow(dispatch, ERROR_DELETE_TAG));
|
||||||
|
};
|
||||||
|
}
|
32
frontend/src/store/tag/api.js
Normal file
32
frontend/src/store/tag/api.js
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import { throwIfNotSuccess, headers } from '../api-helper';
|
||||||
|
|
||||||
|
const URI = 'api/admin/tags';
|
||||||
|
|
||||||
|
function fetchTags() {
|
||||||
|
return fetch(URI, { credentials: 'include' })
|
||||||
|
.then(throwIfNotSuccess)
|
||||||
|
.then(response => response.json());
|
||||||
|
}
|
||||||
|
|
||||||
|
function create(tag) {
|
||||||
|
return fetch(URI, {
|
||||||
|
method: 'POST',
|
||||||
|
headers,
|
||||||
|
credentials: 'include',
|
||||||
|
body: JSON.stringify(tag),
|
||||||
|
}).then(throwIfNotSuccess);
|
||||||
|
}
|
||||||
|
|
||||||
|
function deleteTag(tag) {
|
||||||
|
return fetch(`${URI}/${tag.type}/${tag.value}`, {
|
||||||
|
method: 'DELETE',
|
||||||
|
headers,
|
||||||
|
credentials: 'include',
|
||||||
|
}).then(throwIfNotSuccess);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
fetchTags,
|
||||||
|
deleteTag,
|
||||||
|
create,
|
||||||
|
};
|
24
frontend/src/store/tag/index.js
Normal file
24
frontend/src/store/tag/index.js
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import { List, Map as $Map } from 'immutable';
|
||||||
|
import { ADD_TAG, DELETE_TAG, ERROR_FETCH_TAGS, RECEIVE_TAGS } from './actions';
|
||||||
|
const debug = require('debug')('unleash:tag-store');
|
||||||
|
|
||||||
|
const tags = (state = new List([]), action) => {
|
||||||
|
switch (action.type) {
|
||||||
|
case ADD_TAG:
|
||||||
|
debug('Add tag');
|
||||||
|
return state.push(new $Map(action.tag));
|
||||||
|
case DELETE_TAG:
|
||||||
|
debug('Delete tag');
|
||||||
|
return state.filter(tag => tag.get('type') !== action.tag.type && tag.get('value') !== action.tag.value);
|
||||||
|
case RECEIVE_TAGS:
|
||||||
|
debug('Receive tags', action);
|
||||||
|
return new List(action.value.tags.map($Map));
|
||||||
|
case ERROR_FETCH_TAGS:
|
||||||
|
debug('Error receiving tag types', action);
|
||||||
|
return state;
|
||||||
|
default:
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default tags;
|
@ -123,5 +123,6 @@ module.exports = {
|
|||||||
},
|
},
|
||||||
port: process.env.PORT || 3000,
|
port: process.env.PORT || 3000,
|
||||||
host: '0.0.0.0',
|
host: '0.0.0.0',
|
||||||
|
disableHostCheck: true,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
Loading…
Reference in New Issue
Block a user