diff --git a/frontend/src/component/common/index.js b/frontend/src/component/common/index.js index c049274ec2..c2ec6611ee 100644 --- a/frontend/src/component/common/index.js +++ b/frontend/src/component/common/index.js @@ -140,8 +140,8 @@ IconLink.propTypes = { icon: PropTypes.string, }; -export const DropdownButton = ({ label, id, className }) => ( - @@ -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 }) => ( diff --git a/frontend/src/component/feature/__tests__/__snapshots__/feature-list-item-component-test.jsx.snap b/frontend/src/component/feature/__tests__/__snapshots__/feature-list-item-component-test.jsx.snap index bcf0663e02..4993962a75 100644 --- a/frontend/src/component/feature/__tests__/__snapshots__/feature-list-item-component-test.jsx.snap +++ b/frontend/src/component/feature/__tests__/__snapshots__/feature-list-item-component-test.jsx.snap @@ -51,11 +51,15 @@ exports[`renders correctly with one feature 1`] = ` 3 years ago - - another's description - + + another's description + + - - another's description - + + another's description + + Last minute By name +
@@ -241,8 +251,9 @@ exports[`renders correctly with one feature without permissions 1`] = ` > Last minute By name +
diff --git a/frontend/src/component/feature/__tests__/__snapshots__/view-component-test.jsx.snap b/frontend/src/component/feature/__tests__/__snapshots__/view-component-test.jsx.snap index 4594ca719c..3a038dcd4b 100644 --- a/frontend/src/component/feature/__tests__/__snapshots__/view-component-test.jsx.snap +++ b/frontend/src/component/feature/__tests__/__snapshots__/view-component-test.jsx.snap @@ -58,6 +58,11 @@ exports[`renders correctly with one feature 1`] = ` onChange={[Function]} value="release" /> +   + Status diff --git a/frontend/src/component/feature/__tests__/list-component-test.jsx b/frontend/src/component/feature/__tests__/list-component-test.jsx index b3fd258844..5fcf14790b 100644 --- a/frontend/src/component/feature/__tests__/list-component-test.jsx +++ b/frontend/src/component/feature/__tests__/list-component-test.jsx @@ -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 = [ { diff --git a/frontend/src/component/feature/__tests__/view-component-test.jsx b/frontend/src/component/feature/__tests__/view-component-test.jsx index bdc2c1d745..a499260757 100644 --- a/frontend/src/component/feature/__tests__/view-component-test.jsx +++ b/frontend/src/component/feature/__tests__/view-component-test.jsx @@ -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 = { diff --git a/frontend/src/component/feature/feature-list-item-component.jsx b/frontend/src/component/feature/feature-list-item-component.jsx index 55ab5475a0..916d3ec09e 100644 --- a/frontend/src/component/feature/feature-list-item-component.jsx +++ b/frontend/src/component/feature/feature-list-item-component.jsx @@ -53,7 +53,9 @@ const Feature = ({ - {description} +
+ {description} +
diff --git a/frontend/src/component/feature/form/__tests__/__snapshots__/form-add-feature-component-test.jsx.snap b/frontend/src/component/feature/form/__tests__/__snapshots__/form-add-feature-component-test.jsx.snap index 98a882c18e..81dcd7d24d 100644 --- a/frontend/src/component/feature/form/__tests__/__snapshots__/form-add-feature-component-test.jsx.snap +++ b/frontend/src/component/feature/form/__tests__/__snapshots__/form-add-feature-component-test.jsx.snap @@ -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`] = ` +
+ +
+
+ setValue('project', v.target.value)} /> +
setValue('description', v.target.value)} diff --git a/frontend/src/component/feature/form/form-add-feature-container.jsx b/frontend/src/component/feature/form/form-add-feature-container.jsx index 4e72c63778..a8618fb945 100644 --- a/frontend/src/component/feature/form/form-add-feature-container.jsx +++ b/frontend/src/component/feature/form/form-add-feature-container.jsx @@ -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; diff --git a/frontend/src/component/feature/form/project-select-component.jsx b/frontend/src/component/feature/form/project-select-component.jsx new file mode 100644 index 0000000000..0c4c540cd9 --- /dev/null +++ b/frontend/src/component/feature/form/project-select-component.jsx @@ -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 ; + } +} + +ProjectSelectComponent.propTypes = { + value: PropTypes.string, + filled: PropTypes.bool, + projects: PropTypes.array.isRequired, + fetchProjects: PropTypes.func, + onChange: PropTypes.func.isRequired, +}; + +export default ProjectSelectComponent; diff --git a/frontend/src/component/feature/form/project-select-container.jsx b/frontend/src/component/feature/form/project-select-container.jsx new file mode 100644 index 0000000000..ba148e60f7 --- /dev/null +++ b/frontend/src/component/feature/form/project-select-container.jsx @@ -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; diff --git a/frontend/src/component/feature/list-component.jsx b/frontend/src/component/feature/list-component.jsx index 41b7ae871c..3a83318c1c 100644 --- a/frontend/src/component/feature/list-component.jsx +++ b/frontend/src/component/feature/list-component.jsx @@ -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 { - + this.toggleMetrics()} style={{ width: '168px' }}> - + this.setSort(e.target.getAttribute('data-target'))} @@ -119,6 +124,7 @@ export default class FeatureListComponent extends React.Component { Metrics +
diff --git a/frontend/src/component/feature/list-container.jsx b/frontend/src/component/feature/list-container.jsx index b80db95cad..ce07edbf44 100644 --- a/frontend/src/component/feature/list-container.jsx +++ b/frontend/src/component/feature/list-container.jsx @@ -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'); diff --git a/frontend/src/component/feature/project-component.jsx b/frontend/src/component/feature/project-component.jsx new file mode 100644 index 0000000000..e4023d2a96 --- /dev/null +++ b/frontend/src/component/feature/project-component.jsx @@ -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 ( + + {item.name} + + ); +} + +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 ( + + + setProject(e.target.getAttribute('data-target'))} + style={{ width: '168px' }} + > + + {ALL_PROJECTS.name} + + {projects.map(p => projectItem(currentProjectId, p))} + + + ); +} + +ProjectComponent.propTypes = { + projects: PropTypes.array.isRequired, + fetchProjects: PropTypes.func.isRequired, + currentProjectId: PropTypes.string.isRequired, + updateCurrentProject: PropTypes.func.isRequired, +}; + +export default ProjectComponent; diff --git a/frontend/src/component/feature/project-container.jsx b/frontend/src/component/feature/project-container.jsx new file mode 100644 index 0000000000..f43350084a --- /dev/null +++ b/frontend/src/component/feature/project-container.jsx @@ -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); diff --git a/frontend/src/component/feature/view-component.jsx b/frontend/src/component/feature/view-component.jsx index dd2d86aaa8..bcf8e05cd2 100644 --- a/frontend/src/component/feature/view-component.jsx +++ b/frontend/src/component/feature/view-component.jsx @@ -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 { +   + { - expect(routes.length).toEqual(17); + expect(routes.length).toEqual(20); expect(routes).toMatchSnapshot(); }); diff --git a/frontend/src/component/menu/routes.js b/frontend/src/component/menu/routes.js index 54294137df..a7275cebf2 100644 --- a/frontend/src/component/menu/routes.js +++ b/frontend/src/component/menu/routes.js @@ -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 }, ]; diff --git a/frontend/src/component/project/create-project-container.js b/frontend/src/component/project/create-project-container.js new file mode 100644 index 0000000000..62e80ba5ff --- /dev/null +++ b/frontend/src/component/project/create-project-container.js @@ -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; diff --git a/frontend/src/component/project/edit-project-container.js b/frontend/src/component/project/edit-project-container.js new file mode 100644 index 0000000000..0b93491c3e --- /dev/null +++ b/frontend/src/component/project/edit-project-container.js @@ -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; diff --git a/frontend/src/component/project/form-project-component.jsx b/frontend/src/component/project/form-project-component.jsx new file mode 100644 index 0000000000..010364e755 --- /dev/null +++ b/frontend/src/component/project/form-project-component.jsx @@ -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 ( + + + {submitText} Project + + Projects allows you to group feature toggles together in the managemnt UI. +
+
+ this.validateId(v.target.value)} + onChange={v => this.setValue('id', trim(v.target.value))} + /> +
+ this.setValue('name', v.target.value)} + /> + this.setValue('description', v.target.value)} + /> +
+ + + +
+
+ ); + } +} + +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; diff --git a/frontend/src/component/project/list-component.jsx b/frontend/src/component/project/list-component.jsx new file mode 100644 index 0000000000..b28e24f0f9 --- /dev/null +++ b/frontend/src/component/project/list-component.jsx @@ -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 ( + + this.props.history.push('/projects/create')} + title="Add new project field" + /> + ) : ( + '' + ) + } + /> + + {projects.length > 0 ? ( + projects.map((project, i) => ( + + + + {project.name} + + + + {hasPermission(DELETE_PROJECT) ? ( + + ) : ( + '' + )} + + + )) + ) : ( + No projects defined + )} + + + ); + } +} + +export default ProjectListComponent; diff --git a/frontend/src/component/project/list-container.jsx b/frontend/src/component/project/list-container.jsx new file mode 100644 index 0000000000..7f6e26d46d --- /dev/null +++ b/frontend/src/component/project/list-container.jsx @@ -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; diff --git a/frontend/src/data/helper.js b/frontend/src/data/helper.js index 43a682cf71..666180592b 100644 --- a/frontend/src/data/helper.js +++ b/frontend/src/data/helper.js @@ -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 => { diff --git a/frontend/src/data/project-api.js b/frontend/src/data/project-api.js new file mode 100644 index 0000000000..6212b06c72 --- /dev/null +++ b/frontend/src/data/project-api.js @@ -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, +}; diff --git a/frontend/src/page/project/create.js b/frontend/src/page/project/create.js new file mode 100644 index 0000000000..7c898561d6 --- /dev/null +++ b/frontend/src/page/project/create.js @@ -0,0 +1,11 @@ +import React from 'react'; +import CreateProject from '../../component/project/create-project-container'; +import PropTypes from 'prop-types'; + +const render = ({ history }) => ; + +render.propTypes = { + history: PropTypes.object.isRequired, +}; + +export default render; diff --git a/frontend/src/page/project/edit.js b/frontend/src/page/project/edit.js new file mode 100644 index 0000000000..d66b6d3fdc --- /dev/null +++ b/frontend/src/page/project/edit.js @@ -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 }) => ( + +); + +render.propTypes = { + match: PropTypes.object.isRequired, + history: PropTypes.object.isRequired, +}; + +export default render; diff --git a/frontend/src/page/project/index.js b/frontend/src/page/project/index.js new file mode 100644 index 0000000000..504d5e2dec --- /dev/null +++ b/frontend/src/page/project/index.js @@ -0,0 +1,11 @@ +import React from 'react'; +import ProjectList from '../../component/project/list-container'; +import PropTypes from 'prop-types'; + +const render = ({ history }) => ; + +render.propTypes = { + history: PropTypes.object.isRequired, +}; + +export default render; diff --git a/frontend/src/permissions.js b/frontend/src/permissions.js index c94bced5dd..6ca5a8d84a 100644 --- a/frontend/src/permissions.js +++ b/frontend/src/permissions.js @@ -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 ( diff --git a/frontend/src/store/error-store.js b/frontend/src/store/error-store.js index 1cf06fc284..9f5dfdfd9e 100644 --- a/frontend/src/store/error-store.js +++ b/frontend/src/store/error-store.js @@ -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'); diff --git a/frontend/src/store/index.js b/frontend/src/store/index.js index d7fc5efe1a..d49f6cf395 100644 --- a/frontend/src/store/index.js +++ b/frontend/src/store/index.js @@ -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; diff --git a/frontend/src/store/loader.js b/frontend/src/store/loader.js index 2aaad20d0b..ff7e743daa 100644 --- a/frontend/src/store/loader.js +++ b/frontend/src/store/loader.js @@ -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); }; } diff --git a/frontend/src/store/project/actions.js b/frontend/src/store/project/actions.js new file mode 100644 index 0000000000..16863b3b41 --- /dev/null +++ b/frontend/src/store/project/actions.js @@ -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 }); +} diff --git a/frontend/src/store/project/index.js b/frontend/src/store/project/index.js new file mode 100644 index 0000000000..6c601cbb54 --- /dev/null +++ b/frontend/src/store/project/index.js @@ -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; diff --git a/frontend/src/store/settings/index.js b/frontend/src/store/settings/index.js index 35d1fd8c1e..9f0cf9fa62 100644 --- a/frontend/src/store/settings/index.js +++ b/frontend/src/store/settings/index.js @@ -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; } }