mirror of
				https://github.com/Unleash/unleash.git
				synced 2025-10-27 11:02:16 +01:00 
			
		
		
		
	Merge pull request #227 from Unleash/feat/projects
This commit is contained in:
		
						commit
						df95f803c6
					
				| @ -1,7 +1,7 @@ | ||||
| { | ||||
|   "name": "unleash-frontend", | ||||
|   "description": "unleash your features", | ||||
|   "version": "3.6.4", | ||||
|   "version": "3.6.5", | ||||
|   "keywords": [ | ||||
|     "unleash", | ||||
|     "feature toggle", | ||||
|  | ||||
| @ -140,8 +140,8 @@ IconLink.propTypes = { | ||||
|     icon: PropTypes.string, | ||||
| }; | ||||
| 
 | ||||
| export const DropdownButton = ({ label, id, className }) => ( | ||||
|     <Button id={id} className={className || styles.dropdownButton}> | ||||
| export const DropdownButton = ({ label, id, className, title }) => ( | ||||
|     <Button id={id} className={[className, styles.dropdownButton].join(' ')} title={title}> | ||||
|         {label} | ||||
|         <Icon name="arrow_drop_down" className="mdl-color-text--grey-600" /> | ||||
|     </Button> | ||||
| @ -149,6 +149,7 @@ export const DropdownButton = ({ label, id, className }) => ( | ||||
| DropdownButton.propTypes = { | ||||
|     label: PropTypes.string, | ||||
|     id: PropTypes.string, | ||||
|     title: PropTypes.string, | ||||
| }; | ||||
| 
 | ||||
| export const MenuItemWithIcon = ({ icon, label, disabled, ...menuItemProps }) => ( | ||||
|  | ||||
| @ -51,11 +51,15 @@ exports[`renders correctly with one feature 1`] = ` | ||||
|           3 years ago | ||||
|         </time> | ||||
|       </small> | ||||
|       <span | ||||
|         className="mdl-list__item-sub-title truncate" | ||||
|       <div | ||||
|         className="mdl-list__item-sub-title" | ||||
|       > | ||||
|         another's description | ||||
|       </span> | ||||
|         <span | ||||
|           className="truncate" | ||||
|         > | ||||
|           another's description | ||||
|         </span> | ||||
|       </div> | ||||
|     </a> | ||||
|   </span> | ||||
|   <span | ||||
| @ -115,11 +119,15 @@ exports[`renders correctly with one feature without permission 1`] = ` | ||||
|           3 years ago | ||||
|         </time> | ||||
|       </small> | ||||
|       <span | ||||
|         className="mdl-list__item-sub-title truncate" | ||||
|       <div | ||||
|         className="mdl-list__item-sub-title" | ||||
|       > | ||||
|         another's description | ||||
|       </span> | ||||
|         <span | ||||
|           className="truncate" | ||||
|         > | ||||
|           another's description | ||||
|         </span> | ||||
|       </div> | ||||
|     </a> | ||||
|   </span> | ||||
|   <span | ||||
|  | ||||
| @ -55,8 +55,9 @@ exports[`renders correctly with one feature 1`] = ` | ||||
|   > | ||||
|     <react-mdl-CardActions> | ||||
|       <react-mdl-Button | ||||
|         className="dropdownButton" | ||||
|         className=" dropdownButton" | ||||
|         id="metric" | ||||
|         title="Metric interval" | ||||
|       > | ||||
|         Last minute | ||||
|         <react-mdl-Icon | ||||
| @ -114,8 +115,9 @@ exports[`renders correctly with one feature 1`] = ` | ||||
|         </react-mdl-MenuItem> | ||||
|       </react-mdl-Menu> | ||||
|       <react-mdl-Button | ||||
|         className="dropdownButton" | ||||
|         className=" dropdownButton" | ||||
|         id="sorting" | ||||
|         title="Sort by" | ||||
|       > | ||||
|         By name | ||||
|         <react-mdl-Icon | ||||
| @ -175,6 +177,14 @@ exports[`renders correctly with one feature 1`] = ` | ||||
|           Metrics | ||||
|         </react-mdl-MenuItem> | ||||
|       </react-mdl-Menu> | ||||
|       <Project | ||||
|         settings={ | ||||
|           Object { | ||||
|             "sort": "name", | ||||
|           } | ||||
|         } | ||||
|         updateSetting={[MockFunction]} | ||||
|       /> | ||||
|     </react-mdl-CardActions> | ||||
|     <hr /> | ||||
|     <react-mdl-List> | ||||
| @ -241,8 +251,9 @@ exports[`renders correctly with one feature without permissions 1`] = ` | ||||
|   > | ||||
|     <react-mdl-CardActions> | ||||
|       <react-mdl-Button | ||||
|         className="dropdownButton" | ||||
|         className=" dropdownButton" | ||||
|         id="metric" | ||||
|         title="Metric interval" | ||||
|       > | ||||
|         Last minute | ||||
|         <react-mdl-Icon | ||||
| @ -300,8 +311,9 @@ exports[`renders correctly with one feature without permissions 1`] = ` | ||||
|         </react-mdl-MenuItem> | ||||
|       </react-mdl-Menu> | ||||
|       <react-mdl-Button | ||||
|         className="dropdownButton" | ||||
|         className=" dropdownButton" | ||||
|         id="sorting" | ||||
|         title="Sort by" | ||||
|       > | ||||
|         By name | ||||
|         <react-mdl-Icon | ||||
| @ -361,6 +373,14 @@ exports[`renders correctly with one feature without permissions 1`] = ` | ||||
|           Metrics | ||||
|         </react-mdl-MenuItem> | ||||
|       </react-mdl-Menu> | ||||
|       <Project | ||||
|         settings={ | ||||
|           Object { | ||||
|             "sort": "name", | ||||
|           } | ||||
|         } | ||||
|         updateSetting={[MockFunction]} | ||||
|       /> | ||||
|     </react-mdl-CardActions> | ||||
|     <hr /> | ||||
|     <react-mdl-List> | ||||
|  | ||||
| @ -58,6 +58,11 @@ exports[`renders correctly with one feature 1`] = ` | ||||
|       onChange={[Function]} | ||||
|       value="release" | ||||
|     /> | ||||
|       | ||||
|     <ProjectSelect | ||||
|       filled={true} | ||||
|       onChange={[Function]} | ||||
|     /> | ||||
|   </react-mdl-CardText> | ||||
|   <react-mdl-CardActions | ||||
|     border={true} | ||||
| @ -92,7 +97,7 @@ exports[`renders correctly with one feature 1`] = ` | ||||
|     <div> | ||||
|       <span> | ||||
|         <react-mdl-Button | ||||
|           className="mdl-button" | ||||
|           className="mdl-button dropdownButton" | ||||
|           id="update_status" | ||||
|         > | ||||
|           Status | ||||
|  | ||||
| @ -11,6 +11,8 @@ jest.mock('../feature-list-item-component', () => ({ | ||||
|     default: 'Feature', | ||||
| })); | ||||
| 
 | ||||
| jest.mock('../project-container', () => 'Project'); | ||||
| 
 | ||||
| test('renders correctly with one feature', () => { | ||||
|     const features = [ | ||||
|         { | ||||
|  | ||||
| @ -11,6 +11,7 @@ jest.mock('../form/form-update-feature-container', () => ({ | ||||
|     default: 'UpdateFeatureToggleComponent', | ||||
| })); | ||||
| jest.mock('../form/feature-type-select-container', () => 'FeatureTypeSelect'); | ||||
| jest.mock('../form/project-select-container', () => 'ProjectSelect'); | ||||
| 
 | ||||
| test('renders correctly with one feature', () => { | ||||
|     const feature = { | ||||
|  | ||||
| @ -53,7 +53,9 @@ const Feature = ({ | ||||
|                     <small className="mdl-color-text--blue-grey-300"> | ||||
|                         <TimeAgo date={createdAt} live={false} /> | ||||
|                     </small> | ||||
|                     <span className={['mdl-list__item-sub-title', commonStyles.truncate].join(' ')}>{description}</span> | ||||
|                     <div className="mdl-list__item-sub-title"> | ||||
|                         <span className={commonStyles.truncate}>{description}</span> | ||||
|                     </div> | ||||
|                 </Link> | ||||
|             </span> | ||||
|             <span className={[styles.listItemStrategies, commonStyles.hideLt920].join(' ')}> | ||||
|  | ||||
| @ -33,6 +33,7 @@ exports[`render the create feature page 1`] = ` | ||||
|           name="name" | ||||
|           onBlur={[Function]} | ||||
|           onChange={[Function]} | ||||
|           placeholder="Unique-name" | ||||
|           style={ | ||||
|             Object { | ||||
|               "width": "100%", | ||||
| @ -64,6 +65,17 @@ exports[`render the create feature page 1`] = ` | ||||
|         </react-mdl-Switch> | ||||
|       </react-mdl-Cell> | ||||
|     </react-mdl-Grid> | ||||
|     <section | ||||
|       style={ | ||||
|         Object { | ||||
|           "padding": "0 16px", | ||||
|         } | ||||
|       } | ||||
|     > | ||||
|       <Connect(ProjectSelectComponent) | ||||
|         onChange={[Function]} | ||||
|       /> | ||||
|     </section> | ||||
|     <section | ||||
|       style={ | ||||
|         Object { | ||||
| @ -75,6 +87,7 @@ exports[`render the create feature page 1`] = ` | ||||
|         floatingLabel={true} | ||||
|         label="Description" | ||||
|         onChange={[Function]} | ||||
|         placeholder="A short description of the feature toggle" | ||||
|         rows={1} | ||||
|         style={ | ||||
|           Object { | ||||
|  | ||||
| @ -3,6 +3,7 @@ import PropTypes from 'prop-types'; | ||||
| import { Textfield, Switch, Card, CardTitle, CardActions, Grid, Cell } from 'react-mdl'; | ||||
| import StrategiesSection from './strategies-section-container'; | ||||
| import FeatureTypeSelect from './feature-type-select-container'; | ||||
| import ProjectSelect from './project-select-container'; | ||||
| 
 | ||||
| import { FormButtons } from './../../common'; | ||||
| import { styles as commonStyles } from '../../common'; | ||||
| @ -44,6 +45,7 @@ class AddFeatureComponent extends Component { | ||||
|                                 floatingLabel | ||||
|                                 style={{ width: '100%' }} | ||||
|                                 label="Name" | ||||
|                                 placeholder="Unique-name" | ||||
|                                 name="name" | ||||
|                                 value={input.name} | ||||
|                                 error={errors.name} | ||||
| @ -65,12 +67,16 @@ class AddFeatureComponent extends Component { | ||||
|                             </Switch> | ||||
|                         </Cell> | ||||
|                     </Grid> | ||||
|                     <section style={{ padding: '0 16px' }}> | ||||
|                         <ProjectSelect value={input.project} onChange={v => setValue('project', v.target.value)} /> | ||||
|                     </section> | ||||
|                     <section style={{ padding: '0 16px' }}> | ||||
|                         <Textfield | ||||
|                             floatingLabel | ||||
|                             style={{ width: '100%' }} | ||||
|                             rows={1} | ||||
|                             label="Description" | ||||
|                             placeholder="A short description of the feature toggle" | ||||
|                             error={errors.description} | ||||
|                             value={input.description} | ||||
|                             onChange={v => setValue('description', v.target.value)} | ||||
|  | ||||
| @ -13,7 +13,14 @@ class WrapperComponent extends Component { | ||||
|         super(props); | ||||
|         const name = loadNameFromHash(); | ||||
|         this.state = { | ||||
|             featureToggle: { name, description: '', type: 'release', strategies: [], enabled: true }, | ||||
|             featureToggle: { | ||||
|                 name, | ||||
|                 description: '', | ||||
|                 type: 'release', | ||||
|                 strategies: [], | ||||
|                 enabled: true, | ||||
|                 project: props.currentProjectId, | ||||
|             }, | ||||
|             errors: {}, | ||||
|             dirty: false, | ||||
|         }; | ||||
| @ -109,6 +116,7 @@ class WrapperComponent extends Component { | ||||
| WrapperComponent.propTypes = { | ||||
|     history: PropTypes.object.isRequired, | ||||
|     createFeatureToggles: PropTypes.func.isRequired, | ||||
|     currentProjectId: PropTypes.string.isRequired, | ||||
| }; | ||||
| 
 | ||||
| const mapDispatchToProps = dispatch => ({ | ||||
| @ -116,6 +124,13 @@ const mapDispatchToProps = dispatch => ({ | ||||
|     createFeatureToggles: featureToggle => createFeatureToggles(featureToggle)(dispatch), | ||||
| }); | ||||
| 
 | ||||
| const FormAddContainer = connect(() => ({}), mapDispatchToProps)(WrapperComponent); | ||||
| const mapStateToProps = state => { | ||||
|     const settings = state.settings.toJS().feature || {}; | ||||
|     const currentProjectId = settings.currentProjectId || 'default'; | ||||
| 
 | ||||
|     return { currentProjectId }; | ||||
| }; | ||||
| 
 | ||||
| const FormAddContainer = connect(mapStateToProps, mapDispatchToProps)(WrapperComponent); | ||||
| 
 | ||||
| export default FormAddContainer; | ||||
|  | ||||
| @ -0,0 +1,38 @@ | ||||
| import React, { Component } from 'react'; | ||||
| import PropTypes from 'prop-types'; | ||||
| import MySelect from '../../common/select'; | ||||
| 
 | ||||
| class ProjectSelectComponent extends Component { | ||||
|     componentDidMount() { | ||||
|         const { fetchProjects, projects } = this.props; | ||||
|         if (projects[0].inital && fetchProjects) { | ||||
|             this.props.fetchProjects(); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     render() { | ||||
|         const { value, projects, onChange, filled } = this.props; | ||||
| 
 | ||||
|         if (!projects || projects.length === 1) { | ||||
|             return null; | ||||
|         } | ||||
| 
 | ||||
|         const options = projects.map(t => ({ key: t.id, label: t.name, title: t.description })); | ||||
| 
 | ||||
|         if (!options.find(o => o.key === value)) { | ||||
|             options.push({ key: value, label: value }); | ||||
|         } | ||||
| 
 | ||||
|         return <MySelect label="Project" options={options} value={value} onChange={onChange} filled={filled} />; | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| ProjectSelectComponent.propTypes = { | ||||
|     value: PropTypes.string, | ||||
|     filled: PropTypes.bool, | ||||
|     projects: PropTypes.array.isRequired, | ||||
|     fetchProjects: PropTypes.func, | ||||
|     onChange: PropTypes.func.isRequired, | ||||
| }; | ||||
| 
 | ||||
| export default ProjectSelectComponent; | ||||
| @ -0,0 +1,15 @@ | ||||
| import { connect } from 'react-redux'; | ||||
| import ProjectSelectComponent from './project-select-component'; | ||||
| import { fetchProjects } from './../../../store/project/actions'; | ||||
| 
 | ||||
| const mapStateToProps = state => { | ||||
|     const projects = state.projects.toJS(); | ||||
| 
 | ||||
|     return { | ||||
|         projects, | ||||
|     }; | ||||
| }; | ||||
| 
 | ||||
| const ProjectContainer = connect(mapStateToProps, { fetchProjects })(ProjectSelectComponent); | ||||
| 
 | ||||
| export default ProjectContainer; | ||||
| @ -7,6 +7,7 @@ import Feature from './feature-list-item-component'; | ||||
| import { MenuItemWithIcon, DropdownButton, styles as commonStyles } from '../common'; | ||||
| import SearchField from '../common/search-field'; | ||||
| import { CREATE_FEATURE } from '../../permissions'; | ||||
| import ProjectMenu from './project-container'; | ||||
| 
 | ||||
| export default class FeatureListComponent extends React.Component { | ||||
|     static propTypes = { | ||||
| @ -76,7 +77,11 @@ export default class FeatureListComponent extends React.Component { | ||||
|                 </div> | ||||
|                 <Card shadow={0} className={commonStyles.fullwidth} style={{ overflow: 'visible' }}> | ||||
|                     <CardActions> | ||||
|                         <DropdownButton id="metric" label={`Last ${settings.showLastHour ? 'hour' : 'minute'}`} /> | ||||
|                         <DropdownButton | ||||
|                             id="metric" | ||||
|                             label={`Last ${settings.showLastHour ? 'hour' : 'minute'}`} | ||||
|                             title="Metric interval" | ||||
|                         /> | ||||
|                         <Menu target="metric" onClick={() => this.toggleMetrics()} style={{ width: '168px' }}> | ||||
|                             <MenuItemWithIcon | ||||
|                                 icon="hourglass_empty" | ||||
| @ -91,7 +96,7 @@ export default class FeatureListComponent extends React.Component { | ||||
|                                 label="Last hour" | ||||
|                             /> | ||||
|                         </Menu> | ||||
|                         <DropdownButton id="sorting" label={`By ${settings.sort}`} /> | ||||
|                         <DropdownButton id="sorting" label={`By ${settings.sort}`} title="Sort by" /> | ||||
|                         <Menu | ||||
|                             target="sorting" | ||||
|                             onClick={e => this.setSort(e.target.getAttribute('data-target'))} | ||||
| @ -119,6 +124,7 @@ export default class FeatureListComponent extends React.Component { | ||||
|                                 Metrics | ||||
|                             </MenuItem> | ||||
|                         </Menu> | ||||
|                         <ProjectMenu settings={this.props.settings} updateSetting={this.props.updateSetting} /> | ||||
|                     </CardActions> | ||||
|                     <hr /> | ||||
|                     <List> | ||||
|  | ||||
| @ -9,6 +9,11 @@ export const mapStateToPropsConfigurable = isFeature => state => { | ||||
|     const featureMetrics = state.featureMetrics.toJS(); | ||||
|     const settings = state.settings.toJS().feature || {}; | ||||
|     let features = isFeature ? state.features.toJS() : state.archive.get('list').toArray(); | ||||
| 
 | ||||
|     if (settings.currentProjectId) { | ||||
|         features = features.filter(f => f.project === settings.currentProjectId); | ||||
|     } | ||||
| 
 | ||||
|     if (settings.filter) { | ||||
|         try { | ||||
|             const regex = new RegExp(settings.filter, 'i'); | ||||
|  | ||||
							
								
								
									
										66
									
								
								frontend/src/component/feature/project-component.jsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										66
									
								
								frontend/src/component/feature/project-component.jsx
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,66 @@ | ||||
| import React, { useEffect } from 'react'; | ||||
| import { Menu, MenuItem } from 'react-mdl'; | ||||
| import { DropdownButton } from '../common'; | ||||
| import PropTypes from 'prop-types'; | ||||
| 
 | ||||
| const ALL_PROJECTS = { id: undefined, name: '> All projects' }; | ||||
| 
 | ||||
| function projectItem(selectedId, item) { | ||||
|     return ( | ||||
|         <MenuItem disabled={selectedId === item.id} data-target={item.id} key={item.id}> | ||||
|             {item.name} | ||||
|         </MenuItem> | ||||
|     ); | ||||
| } | ||||
| 
 | ||||
| function ProjectComponent({ fetchProjects, projects, currentProjectId, updateCurrentProject }) { | ||||
|     useEffect(() => { | ||||
|         if (projects[0].inital) { | ||||
|             fetchProjects(); | ||||
|         } | ||||
|     }); | ||||
| 
 | ||||
|     function setProject(v) { | ||||
|         const id = typeof v === 'string' ? v.trim() : ''; | ||||
|         updateCurrentProject(id); | ||||
|     } | ||||
| 
 | ||||
|     if (!projects || projects.length === 1) { | ||||
|         return null; | ||||
|     } | ||||
| 
 | ||||
|     // TODO fixme | ||||
|     let curentProject = projects.find(i => i.id === currentProjectId); | ||||
|     if (!curentProject) { | ||||
|         curentProject = ALL_PROJECTS; | ||||
|     } | ||||
|     return ( | ||||
|         <React.Fragment> | ||||
|             <DropdownButton | ||||
|                 className="mdl-color--amber-50" | ||||
|                 id="project" | ||||
|                 label={`${curentProject.name}`} | ||||
|                 title="Select project" | ||||
|             /> | ||||
|             <Menu | ||||
|                 target="project" | ||||
|                 onClick={e => setProject(e.target.getAttribute('data-target'))} | ||||
|                 style={{ width: '168px' }} | ||||
|             > | ||||
|                 <MenuItem disabled={curentProject === ALL_PROJECTS} data-target={ALL_PROJECTS.id}> | ||||
|                     {ALL_PROJECTS.name} | ||||
|                 </MenuItem> | ||||
|                 {projects.map(p => projectItem(currentProjectId, p))} | ||||
|             </Menu> | ||||
|         </React.Fragment> | ||||
|     ); | ||||
| } | ||||
| 
 | ||||
| ProjectComponent.propTypes = { | ||||
|     projects: PropTypes.array.isRequired, | ||||
|     fetchProjects: PropTypes.func.isRequired, | ||||
|     currentProjectId: PropTypes.string.isRequired, | ||||
|     updateCurrentProject: PropTypes.func.isRequired, | ||||
| }; | ||||
| 
 | ||||
| export default ProjectComponent; | ||||
							
								
								
									
										11
									
								
								frontend/src/component/feature/project-container.jsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								frontend/src/component/feature/project-container.jsx
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,11 @@ | ||||
| import { connect } from 'react-redux'; | ||||
| import Component from './project-component'; | ||||
| import { fetchProjects } from './../../store/project/actions'; | ||||
| 
 | ||||
| const mapStateToProps = (state, ownProps) => ({ | ||||
|     projects: state.projects.toJS(), | ||||
|     currentProjectId: ownProps.settings.currentProjectId || '*', | ||||
|     updateCurrentProject: id => ownProps.updateSetting('currentProjectId', id), | ||||
| }); | ||||
| 
 | ||||
| export default connect(mapStateToProps, { fetchProjects })(Component); | ||||
| @ -9,6 +9,7 @@ import EditFeatureToggle from './form/form-update-feature-container'; | ||||
| import EditVariants from './variant/update-variant-container'; | ||||
| import ViewFeatureToggle from './form/form-view-feature-container'; | ||||
| import FeatureTypeSelect from './form/feature-type-select-container'; | ||||
| import ProjectSelect from './form/project-select-container'; | ||||
| import UpdateDescriptionComponent from './form/update-description-component'; | ||||
| import { styles as commonStyles } from '../common'; | ||||
| import { CREATE_FEATURE, DELETE_FEATURE, UPDATE_FEATURE } from '../../permissions'; | ||||
| @ -162,6 +163,19 @@ export default class ViewFeatureToggleComponent extends React.Component { | ||||
|             this.props.editFeatureToggle(feature); | ||||
|         }; | ||||
| 
 | ||||
|         const updateProject = evt => { | ||||
|             evt.preventDefault(); | ||||
|             const project = evt.target.value; | ||||
|             let feature = { ...featureToggle, project }; | ||||
|             if (Array.isArray(feature.strategies)) { | ||||
|                 feature.strategies.forEach(s => { | ||||
|                     delete s.id; | ||||
|                 }); | ||||
|             } | ||||
| 
 | ||||
|             this.props.editFeatureToggle(feature); | ||||
|         }; | ||||
| 
 | ||||
|         const updateStale = stale => { | ||||
|             this.props.setStale(stale, featureToggleName); | ||||
|         }; | ||||
| @ -187,6 +201,8 @@ export default class ViewFeatureToggleComponent extends React.Component { | ||||
|                 </CardText> | ||||
|                 <CardText style={{ paddingTop: 0 }}> | ||||
|                     <FeatureTypeSelect value={featureToggle.type} onChange={updateType} filled /> | ||||
|                       | ||||
|                     <ProjectSelect value={featureToggle.project} onChange={updateProject} filled /> | ||||
|                 </CardText> | ||||
| 
 | ||||
|                 <CardActions | ||||
|  | ||||
| @ -140,6 +140,25 @@ Array [ | ||||
|     "path": "/context", | ||||
|     "title": "Context Fields", | ||||
|   }, | ||||
|   Object { | ||||
|     "component": [Function], | ||||
|     "parent": "/projects", | ||||
|     "path": "/projects/create", | ||||
|     "title": "Create", | ||||
|   }, | ||||
|   Object { | ||||
|     "component": [Function], | ||||
|     "parent": "/projects", | ||||
|     "path": "/projects/edit/:id", | ||||
|     "title": ":id", | ||||
|   }, | ||||
|   Object { | ||||
|     "component": [Function], | ||||
|     "hidden": true, | ||||
|     "icon": "folder_open", | ||||
|     "path": "/projects", | ||||
|     "title": "Projects", | ||||
|   }, | ||||
|   Object { | ||||
|     "component": [Function], | ||||
|     "icon": "exit_to_app", | ||||
|  | ||||
| @ -1,7 +1,7 @@ | ||||
| import { routes, baseRoutes, getRoute } from '../routes'; | ||||
| 
 | ||||
| test('returns all defined routes', () => { | ||||
|     expect(routes.length).toEqual(17); | ||||
|     expect(routes.length).toEqual(20); | ||||
|     expect(routes).toMatchSnapshot(); | ||||
| }); | ||||
| 
 | ||||
|  | ||||
| @ -15,6 +15,9 @@ import ContextFields from '../../page/context'; | ||||
| import CreateContextField from '../../page/context/create'; | ||||
| import EditContextField from '../../page/context/edit'; | ||||
| import LogoutFeatures from '../../page/user/logout'; | ||||
| import ListProjects from '../../page/project'; | ||||
| import CreateProject from '../../page/project/create'; | ||||
| import EditProject from '../../page/project/edit'; | ||||
| 
 | ||||
| export const routes = [ | ||||
|     // Features
 | ||||
| @ -50,6 +53,11 @@ export const routes = [ | ||||
|     { path: '/context/edit/:name', parent: '/context', title: ':name', component: EditContextField }, | ||||
|     { path: '/context', title: 'Context Fields', icon: 'apps', component: ContextFields, hidden: true }, | ||||
| 
 | ||||
|     // Project
 | ||||
|     { path: '/projects/create', parent: '/projects', title: 'Create', component: CreateProject }, | ||||
|     { path: '/projects/edit/:id', parent: '/projects', title: ':id', component: EditProject }, | ||||
|     { path: '/projects', title: 'Projects', icon: 'folder_open', component: ListProjects, hidden: true }, | ||||
| 
 | ||||
|     { path: '/logout', title: 'Sign out', icon: 'exit_to_app', component: LogoutFeatures }, | ||||
| ]; | ||||
| 
 | ||||
|  | ||||
							
								
								
									
										17
									
								
								frontend/src/component/project/create-project-container.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								frontend/src/component/project/create-project-container.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,17 @@ | ||||
| import { connect } from 'react-redux'; | ||||
| import ProjectComponent from './form-project-component'; | ||||
| import { createProject, validateId } from './../../store/project/actions'; | ||||
| 
 | ||||
| const mapStateToProps = () => ({ | ||||
|     project: { id: '', name: '', description: '' }, | ||||
| }); | ||||
| 
 | ||||
| const mapDispatchToProps = dispatch => ({ | ||||
|     validateId, | ||||
|     submit: project => createProject(project)(dispatch), | ||||
|     editMode: false, | ||||
| }); | ||||
| 
 | ||||
| const FormAddContainer = connect(mapStateToProps, mapDispatchToProps)(ProjectComponent); | ||||
| 
 | ||||
| export default FormAddContainer; | ||||
							
								
								
									
										23
									
								
								frontend/src/component/project/edit-project-container.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								frontend/src/component/project/edit-project-container.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,23 @@ | ||||
| import { connect } from 'react-redux'; | ||||
| import Component from './form-project-component'; | ||||
| import { updateProject, validateId } from './../../store/project/actions'; | ||||
| 
 | ||||
| const mapStateToProps = (state, props) => { | ||||
|     const projectBase = { id: '', name: '', description: '' }; | ||||
|     const realProject = state.projects.toJS().find(n => n.id === props.projectId); | ||||
|     const project = Object.assign(projectBase, realProject); | ||||
| 
 | ||||
|     return { | ||||
|         project, | ||||
|     }; | ||||
| }; | ||||
| 
 | ||||
| const mapDispatchToProps = dispatch => ({ | ||||
|     validateId, | ||||
|     submit: project => updateProject(project)(dispatch), | ||||
|     editMode: true, | ||||
| }); | ||||
| 
 | ||||
| const FormAddContainer = connect(mapStateToProps, mapDispatchToProps)(Component); | ||||
| 
 | ||||
| export default FormAddContainer; | ||||
							
								
								
									
										120
									
								
								frontend/src/component/project/form-project-component.jsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										120
									
								
								frontend/src/component/project/form-project-component.jsx
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,120 @@ | ||||
| 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 AddContextComponent extends Component { | ||||
|     constructor(props) { | ||||
|         super(props); | ||||
|         this.state = { | ||||
|             project: props.project, | ||||
|             errors: {}, | ||||
|             currentLegalValue: '', | ||||
|             dirty: false, | ||||
|         }; | ||||
|     } | ||||
| 
 | ||||
|     static getDerivedStateFromProps(props, state) { | ||||
|         if (!state.project.id && props.project.id) { | ||||
|             return { project: props.project }; | ||||
|         } else { | ||||
|             return null; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     setValue = (field, value) => { | ||||
|         const { project } = this.state; | ||||
|         project[field] = value; | ||||
|         this.setState({ project, dirty: true }); | ||||
|     }; | ||||
| 
 | ||||
|     validateId = async id => { | ||||
|         const { errors } = this.state; | ||||
|         const { validateId } = this.props; | ||||
|         try { | ||||
|             await validateId(id); | ||||
|             errors.id = undefined; | ||||
|         } catch (err) { | ||||
|             errors.id = err.message; | ||||
|         } | ||||
| 
 | ||||
|         this.setState({ errors }); | ||||
|     }; | ||||
| 
 | ||||
|     onCancel = evt => { | ||||
|         evt.preventDefault(); | ||||
|         this.props.history.push('/projects'); | ||||
|     }; | ||||
| 
 | ||||
|     onSubmit = async evt => { | ||||
|         evt.preventDefault(); | ||||
|         const { project } = this.state; | ||||
|         await this.props.submit(project); | ||||
|         this.props.history.push('/projects'); | ||||
|     }; | ||||
| 
 | ||||
|     render() { | ||||
|         const { project, 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} Project | ||||
|                 </CardTitle> | ||||
|                 <CardText>Projects allows you to group feature toggles together in the managemnt UI.</CardText> | ||||
|                 <form onSubmit={this.onSubmit}> | ||||
|                     <section style={{ padding: '16px' }}> | ||||
|                         <Textfield | ||||
|                             floatingLabel | ||||
|                             label="Project Id" | ||||
|                             name="id" | ||||
|                             placeholder="A-unique-key" | ||||
|                             value={project.id} | ||||
|                             error={errors.id} | ||||
|                             disabled={editMode} | ||||
|                             onBlur={v => this.validateId(v.target.value)} | ||||
|                             onChange={v => this.setValue('id', trim(v.target.value))} | ||||
|                         /> | ||||
|                         <br /> | ||||
|                         <Textfield | ||||
|                             floatingLabel | ||||
|                             label="Name" | ||||
|                             name="name" | ||||
|                             placeholder="Project name" | ||||
|                             value={project.name} | ||||
|                             error={errors.name} | ||||
|                             onChange={v => this.setValue('name', v.target.value)} | ||||
|                         /> | ||||
|                         <Textfield | ||||
|                             floatingLabel | ||||
|                             style={{ width: '100%' }} | ||||
|                             placeholder="A short description" | ||||
|                             rows={1} | ||||
|                             label="Description" | ||||
|                             error={errors.description} | ||||
|                             value={project.description} | ||||
|                             onChange={v => this.setValue('description', v.target.value)} | ||||
|                         /> | ||||
|                     </section> | ||||
|                     <CardActions> | ||||
|                         <FormButtons submitText={submitText} onCancel={this.onCancel} /> | ||||
|                     </CardActions> | ||||
|                 </form> | ||||
|             </Card> | ||||
|         ); | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| AddContextComponent.propTypes = { | ||||
|     project: PropTypes.object.isRequired, | ||||
|     validateId: PropTypes.func.isRequired, | ||||
|     submit: PropTypes.func.isRequired, | ||||
|     history: PropTypes.object.isRequired, | ||||
|     editMode: PropTypes.bool.isRequired, | ||||
| }; | ||||
| 
 | ||||
| export default AddContextComponent; | ||||
							
								
								
									
										80
									
								
								frontend/src/component/project/list-component.jsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										80
									
								
								frontend/src/component/project/list-component.jsx
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,80 @@ | ||||
| import React, { Component } from 'react'; | ||||
| import PropTypes from 'prop-types'; | ||||
| import { Link } from 'react-router-dom'; | ||||
| 
 | ||||
| import { List, ListItem, ListItemAction, ListItemContent, IconButton, Card } from 'react-mdl'; | ||||
| import { HeaderTitle, styles as commonStyles } from '../common'; | ||||
| import { CREATE_PROJECT, DELETE_PROJECT } from '../../permissions'; | ||||
| 
 | ||||
| class ProjectListComponent extends Component { | ||||
|     static propTypes = { | ||||
|         projects: PropTypes.array.isRequired, | ||||
|         fetchProjects: PropTypes.func.isRequired, | ||||
|         removeProject: PropTypes.func.isRequired, | ||||
|         history: PropTypes.object.isRequired, | ||||
|         hasPermission: PropTypes.func.isRequired, | ||||
|     }; | ||||
| 
 | ||||
|     componentDidMount() { | ||||
|         this.props.fetchProjects(); | ||||
|     } | ||||
| 
 | ||||
|     removeProject = (project, evt) => { | ||||
|         evt.preventDefault(); | ||||
|         this.props.removeProject(project); | ||||
|     }; | ||||
| 
 | ||||
|     render() { | ||||
|         const { projects, hasPermission } = this.props; | ||||
| 
 | ||||
|         return ( | ||||
|             <Card shadow={0} className={commonStyles.fullwidth} style={{ overflow: 'visible' }}> | ||||
|                 <HeaderTitle | ||||
|                     title="Projects (beta)" | ||||
|                     actions={ | ||||
|                         hasPermission(CREATE_PROJECT) ? ( | ||||
|                             <IconButton | ||||
|                                 raised | ||||
|                                 colored | ||||
|                                 accent | ||||
|                                 name="add" | ||||
|                                 onClick={() => this.props.history.push('/projects/create')} | ||||
|                                 title="Add new project field" | ||||
|                             /> | ||||
|                         ) : ( | ||||
|                             '' | ||||
|                         ) | ||||
|                     } | ||||
|                 /> | ||||
|                 <List> | ||||
|                     {projects.length > 0 ? ( | ||||
|                         projects.map((project, i) => ( | ||||
|                             <ListItem key={i} twoLine> | ||||
|                                 <ListItemContent icon="folder_open" subtitle={project.description}> | ||||
|                                     <Link to={`/projects/edit/${project.id}`}> | ||||
|                                         <strong>{project.name}</strong> | ||||
|                                     </Link> | ||||
|                                 </ListItemContent> | ||||
|                                 <ListItemAction> | ||||
|                                     {hasPermission(DELETE_PROJECT) ? ( | ||||
|                                         <IconButton | ||||
|                                             name="delete" | ||||
|                                             title="Remove project" | ||||
|                                             onClick={this.removeProject.bind(this, project)} | ||||
|                                         /> | ||||
|                                     ) : ( | ||||
|                                         '' | ||||
|                                     )} | ||||
|                                 </ListItemAction> | ||||
|                             </ListItem> | ||||
|                         )) | ||||
|                     ) : ( | ||||
|                         <ListItem>No projects defined</ListItem> | ||||
|                     )} | ||||
|                 </List> | ||||
|             </Card> | ||||
|         ); | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| export default ProjectListComponent; | ||||
							
								
								
									
										27
									
								
								frontend/src/component/project/list-container.jsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								frontend/src/component/project/list-container.jsx
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,27 @@ | ||||
| import { connect } from 'react-redux'; | ||||
| import Component from './list-component.jsx'; | ||||
| import { fetchProjects, removeProject } from './../../store/project/actions'; | ||||
| import { hasPermission } from '../../permissions'; | ||||
| 
 | ||||
| const mapStateToProps = state => { | ||||
|     const projects = state.projects.toJS(); | ||||
| 
 | ||||
|     return { | ||||
|         projects, | ||||
|         hasPermission: hasPermission.bind(null, state.user.get('profile')), | ||||
|     }; | ||||
| }; | ||||
| 
 | ||||
| const mapDispatchToProps = dispatch => ({ | ||||
|     removeProject: project => { | ||||
|         // eslint-disable-next-line no-alert | ||||
|         if (window.confirm('Are you sure you want to remove this project?')) { | ||||
|             removeProject(project)(dispatch); | ||||
|         } | ||||
|     }, | ||||
|     fetchProjects: () => fetchProjects()(dispatch), | ||||
| }); | ||||
| 
 | ||||
| const ProjectListContainer = connect(mapStateToProps, mapDispatchToProps)(Component); | ||||
| 
 | ||||
| export default ProjectListContainer; | ||||
| @ -33,6 +33,14 @@ export class ForbiddenError extends Error { | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| export class NotFoundError extends Error { | ||||
|     constructor(statusCode) { | ||||
|         super('The requested resource could not be found but may be available in the future'); | ||||
|         this.name = 'NotFoundError'; | ||||
|         this.statusCode = statusCode; | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| export function throwIfNotSuccess(response) { | ||||
|     if (!response.ok) { | ||||
|         if (response.status === 401) { | ||||
| @ -43,6 +51,10 @@ export function throwIfNotSuccess(response) { | ||||
|             return new Promise((resolve, reject) => { | ||||
|                 response.json().then(body => reject(new ForbiddenError(response.status, body))); | ||||
|             }); | ||||
|         } else if (response.status === 404) { | ||||
|             return new Promise((resolve, reject) => { | ||||
|                 reject(new NotFoundError(response.status)); | ||||
|             }); | ||||
|         } else if (response.status > 399 && response.status < 499) { | ||||
|             return new Promise((resolve, reject) => { | ||||
|                 response.json().then(body => { | ||||
|  | ||||
							
								
								
									
										52
									
								
								frontend/src/data/project-api.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										52
									
								
								frontend/src/data/project-api.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,52 @@ | ||||
| import { throwIfNotSuccess, headers } from './helper'; | ||||
| 
 | ||||
| const URI = 'api/admin/projects'; | ||||
| 
 | ||||
| function fetchAll() { | ||||
|     return fetch(URI, { credentials: 'include' }) | ||||
|         .then(throwIfNotSuccess) | ||||
|         .then(response => response.json()); | ||||
| } | ||||
| 
 | ||||
| function create(project) { | ||||
|     return fetch(URI, { | ||||
|         method: 'POST', | ||||
|         headers, | ||||
|         body: JSON.stringify(project), | ||||
|         credentials: 'include', | ||||
|     }).then(throwIfNotSuccess); | ||||
| } | ||||
| 
 | ||||
| function update(project) { | ||||
|     return fetch(`${URI}/${project.id}`, { | ||||
|         method: 'PUT', | ||||
|         headers, | ||||
|         body: JSON.stringify(project), | ||||
|         credentials: 'include', | ||||
|     }).then(throwIfNotSuccess); | ||||
| } | ||||
| 
 | ||||
| function remove(project) { | ||||
|     return fetch(`${URI}/${project.id}`, { | ||||
|         method: 'DELETE', | ||||
|         headers, | ||||
|         credentials: 'include', | ||||
|     }).then(throwIfNotSuccess); | ||||
| } | ||||
| 
 | ||||
| function validate(id) { | ||||
|     return fetch(`${URI}/validate`, { | ||||
|         method: 'POST', | ||||
|         headers, | ||||
|         credentials: 'include', | ||||
|         body: JSON.stringify(id), | ||||
|     }).then(throwIfNotSuccess); | ||||
| } | ||||
| 
 | ||||
| export default { | ||||
|     fetchAll, | ||||
|     create, | ||||
|     update, | ||||
|     remove, | ||||
|     validate, | ||||
| }; | ||||
							
								
								
									
										11
									
								
								frontend/src/page/project/create.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								frontend/src/page/project/create.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,11 @@ | ||||
| import React from 'react'; | ||||
| import CreateProject from '../../component/project/create-project-container'; | ||||
| import PropTypes from 'prop-types'; | ||||
| 
 | ||||
| const render = ({ history }) => <CreateProject title="Create Project" history={history} />; | ||||
| 
 | ||||
| render.propTypes = { | ||||
|     history: PropTypes.object.isRequired, | ||||
| }; | ||||
| 
 | ||||
| export default render; | ||||
							
								
								
									
										14
									
								
								frontend/src/page/project/edit.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								frontend/src/page/project/edit.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,14 @@ | ||||
| import React from 'react'; | ||||
| import EditProject from '../../component/project/edit-project-container'; | ||||
| import PropTypes from 'prop-types'; | ||||
| 
 | ||||
| const render = ({ match: { params }, history }) => ( | ||||
|     <EditProject projectId={params.id} title="Edit project" history={history} /> | ||||
| ); | ||||
| 
 | ||||
| render.propTypes = { | ||||
|     match: PropTypes.object.isRequired, | ||||
|     history: PropTypes.object.isRequired, | ||||
| }; | ||||
| 
 | ||||
| export default render; | ||||
							
								
								
									
										11
									
								
								frontend/src/page/project/index.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								frontend/src/page/project/index.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,11 @@ | ||||
| import React from 'react'; | ||||
| import ProjectList from '../../component/project/list-container'; | ||||
| import PropTypes from 'prop-types'; | ||||
| 
 | ||||
| const render = ({ history }) => <ProjectList history={history} />; | ||||
| 
 | ||||
| render.propTypes = { | ||||
|     history: PropTypes.object.isRequired, | ||||
| }; | ||||
| 
 | ||||
| export default render; | ||||
| @ -9,6 +9,9 @@ export const UPDATE_APPLICATION = 'UPDATE_APPLICATION'; | ||||
| export const CREATE_CONTEXT_FIELD = 'CREATE_CONTEXT_FIELD'; | ||||
| export const UPDATE_CONTEXT_FIELD = 'UPDATE_CONTEXT_FIELD'; | ||||
| export const DELETE_CONTEXT_FIELD = 'DELETE_CONTEXT_FIELD'; | ||||
| export const CREATE_PROJECT = 'CREATE_PROJECT'; | ||||
| export const UPDATE_PROJECT = 'UPDATE_PROJECT'; | ||||
| export const DELETE_PROJECT = 'DELETE_PROJECT'; | ||||
| 
 | ||||
| export function hasPermission(user, permission) { | ||||
|     return ( | ||||
|  | ||||
| @ -13,6 +13,8 @@ import { ERROR_UPDATING_STRATEGY, ERROR_CREATING_STRATEGY, ERROR_RECEIVE_STRATEG | ||||
| 
 | ||||
| import { ERROR_ADD_CONTEXT_FIELD, ERROR_UPDATE_CONTEXT_FIELD } from './context/actions'; | ||||
| 
 | ||||
| import { ERROR_REMOVING_PROJECT, ERROR_ADD_PROJECT, ERROR_UPDATE_PROJECT } from './project/actions'; | ||||
| 
 | ||||
| import { UPDATE_APPLICATION_FIELD } from './application/actions'; | ||||
| 
 | ||||
| import { FORBIDDEN } from './util'; | ||||
| @ -44,6 +46,9 @@ const strategies = (state = getInitState(), action) => { | ||||
|         case ERROR_RECEIVE_STRATEGIES: | ||||
|         case ERROR_ADD_CONTEXT_FIELD: | ||||
|         case ERROR_UPDATE_CONTEXT_FIELD: | ||||
|         case ERROR_REMOVING_PROJECT: | ||||
|         case ERROR_UPDATE_PROJECT: | ||||
|         case ERROR_ADD_PROJECT: | ||||
|             return addErrorIfNotAlreadyInList(state, action.error.message); | ||||
|         case FORBIDDEN: | ||||
|             return addErrorIfNotAlreadyInList(state, action.error.message || '403 Forbidden'); | ||||
|  | ||||
| @ -13,6 +13,7 @@ import user from './user'; | ||||
| import applications from './application'; | ||||
| import uiConfig from './ui-config'; | ||||
| import context from './context'; | ||||
| import projects from './project'; | ||||
| 
 | ||||
| const unleashStore = combineReducers({ | ||||
|     features, | ||||
| @ -29,6 +30,7 @@ const unleashStore = combineReducers({ | ||||
|     applications, | ||||
|     uiConfig, | ||||
|     context, | ||||
|     projects, | ||||
| }); | ||||
| 
 | ||||
| export default unleashStore; | ||||
|  | ||||
| @ -1,11 +1,13 @@ | ||||
| import { fetchUIConfig } from './ui-config/actions'; | ||||
| import { fetchContext } from './context/actions'; | ||||
| import { fetchFeatureTypes } from './feature-type/actions'; | ||||
| import { fetchProjects } from './project/actions'; | ||||
| 
 | ||||
| export function loadInitalData() { | ||||
|     return dispatch => { | ||||
|         fetchUIConfig()(dispatch); | ||||
|         fetchContext()(dispatch); | ||||
|         fetchFeatureTypes()(dispatch); | ||||
|         fetchProjects()(dispatch); | ||||
|     }; | ||||
| } | ||||
|  | ||||
							
								
								
									
										57
									
								
								frontend/src/store/project/actions.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										57
									
								
								frontend/src/store/project/actions.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,57 @@ | ||||
| import api from '../../data/project-api'; | ||||
| import { dispatchAndThrow } from '../util'; | ||||
| 
 | ||||
| export const RECEIVE_PROJECT = 'RECEIVE_PROJECT'; | ||||
| export const ERROR_RECEIVE_PROJECT = 'ERROR_RECEIVE_PROJECT'; | ||||
| export const REMOVE_PROJECT = 'REMOVE_PROJECT'; | ||||
| export const ERROR_REMOVING_PROJECT = 'ERROR_REMOVING_PROJECT'; | ||||
| export const ADD_PROJECT = 'ADD_PROJECT'; | ||||
| export const ERROR_ADD_PROJECT = 'ERROR_ADD_PROJECT'; | ||||
| export const UPDATE_PROJECT = 'UPDATE_PROJECT'; | ||||
| export const ERROR_UPDATE_PROJECT = 'ERROR_UPDATE_PROJECT'; | ||||
| 
 | ||||
| const addProject = project => ({ type: ADD_PROJECT, project }); | ||||
| const upProject = project => ({ type: UPDATE_PROJECT, project }); | ||||
| const delProject = project => ({ type: REMOVE_PROJECT, project }); | ||||
| 
 | ||||
| export function fetchProjects() { | ||||
|     return () => {}; | ||||
|     /* | ||||
|     const receiveProjects = value => ({ type: RECEIVE_PROJECT, value }); | ||||
|     return dispatch => | ||||
|         api | ||||
|             .fetchAll() | ||||
|             .then(json => { | ||||
|                 dispatch(receiveProjects(json.projects)); | ||||
|             }) | ||||
|             .catch(dispatchAndThrow(dispatch, ERROR_RECEIVE_PROJECT)); | ||||
|     */ | ||||
| } | ||||
| 
 | ||||
| export function removeProject(project) { | ||||
|     return dispatch => | ||||
|         api | ||||
|             .remove(project) | ||||
|             .then(() => dispatch(delProject(project))) | ||||
|             .catch(dispatchAndThrow(dispatch, ERROR_REMOVING_PROJECT)); | ||||
| } | ||||
| 
 | ||||
| export function createProject(project) { | ||||
|     return dispatch => | ||||
|         api | ||||
|             .create(project) | ||||
|             .then(() => dispatch(addProject(project))) | ||||
|             .catch(dispatchAndThrow(dispatch, ERROR_ADD_PROJECT)); | ||||
| } | ||||
| 
 | ||||
| export function updateProject(project) { | ||||
|     return dispatch => | ||||
|         api | ||||
|             .update(project) | ||||
|             .then(() => dispatch(upProject(project))) | ||||
|             .catch(dispatchAndThrow(dispatch, ERROR_UPDATE_PROJECT)); | ||||
| } | ||||
| 
 | ||||
| export function validateId(id) { | ||||
|     return api.validate({ id }); | ||||
| } | ||||
							
								
								
									
										33
									
								
								frontend/src/store/project/index.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								frontend/src/store/project/index.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,33 @@ | ||||
| import { List } from 'immutable'; | ||||
| import { RECEIVE_PROJECT, REMOVE_PROJECT, ADD_PROJECT, UPDATE_PROJECT } from './actions'; | ||||
| import { USER_LOGOUT, USER_LOGIN } from '../user/actions'; | ||||
| 
 | ||||
| const DEFAULT_PROJECTS = [{ id: 'default', name: 'Default', inital: true }]; | ||||
| 
 | ||||
| function getInitState() { | ||||
|     return new List(DEFAULT_PROJECTS); | ||||
| } | ||||
| 
 | ||||
| const strategies = (state = getInitState(), action) => { | ||||
|     switch (action.type) { | ||||
|         case RECEIVE_PROJECT: | ||||
|             return new List(action.value); | ||||
|         case REMOVE_PROJECT: { | ||||
|             const index = state.findIndex(item => item.id === action.project.id); | ||||
|             return state.remove(index); | ||||
|         } | ||||
|         case ADD_PROJECT: | ||||
|             return state.push(action.project); | ||||
|         case UPDATE_PROJECT: { | ||||
|             const index = state.findIndex(item => item.id === action.project.id); | ||||
|             return state.set(index, action.project); | ||||
|         } | ||||
|         case USER_LOGOUT: | ||||
|         case USER_LOGIN: | ||||
|             return getInitState(); | ||||
|         default: | ||||
|             return state; | ||||
|     } | ||||
| }; | ||||
| 
 | ||||
| export default strategies; | ||||
| @ -1,4 +1,4 @@ | ||||
| import { fromJS, Map as $Map } from 'immutable'; | ||||
| import { fromJS } from 'immutable'; | ||||
| import { UPDATE_SETTING } from './actions'; | ||||
| import { USER_LOGOUT, USER_LOGIN } from '../user/actions'; | ||||
| 
 | ||||
| @ -6,12 +6,16 @@ import { USER_LOGOUT, USER_LOGIN } from '../user/actions'; | ||||
| const localStorage = window.localStorage || {}; | ||||
| const SETTINGS = 'settings'; | ||||
| 
 | ||||
| const DEFAULT = fromJS({ | ||||
|     feature: { currentProjectId: 'default' }, | ||||
| }); | ||||
| 
 | ||||
| function getInitState() { | ||||
|     try { | ||||
|         const state = JSON.parse(localStorage.getItem(SETTINGS)); | ||||
|         return state ? fromJS(state) : new $Map(); | ||||
|         return state ? DEFAULT.merge(state) : DEFAULT; | ||||
|     } catch (e) { | ||||
|         return new $Map(); | ||||
|         return DEFAULT; | ||||
|     } | ||||
| } | ||||
| 
 | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user