Merged heads

This commit is contained in:
Philipp Czora
2018-08-01 09:52:49 +02:00
9 changed files with 344 additions and 66 deletions

View File

@@ -22,5 +22,11 @@
"add-group": { "add-group": {
"title": "Create Group", "title": "Create Group",
"subtitle": "Create a new group" "subtitle": "Create a new group"
},
"create-group-button": {
"label": "Create group"
},
"group-form": {
"submit": "Submit"
} }
} }

View File

@@ -0,0 +1,30 @@
//@flow
import React from "react";
import injectSheet from "react-jss";
import { translate } from "react-i18next";
import { AddButton } from "../../../components/buttons";
import classNames from "classnames";
const styles = {
spacing: {
margin: "1em 0 0 1em"
}
};
type Props = {
t: string => string,
classes: any
};
class CreateGroupButton extends React.Component<Props> {
render() {
const { classes, t } = this.props;
return (
<div className={classNames("is-pulled-right", classes.spacing)}>
<AddButton label={t("create-group-button.label")} link="/groups/add" />
</div>
);
}
}
export default translate("groups")(injectSheet(styles)(CreateGroupButton));

View File

@@ -26,14 +26,6 @@ class Details extends React.Component<Props> {
<td>{t("group.description")}</td> <td>{t("group.description")}</td>
<td>{group.description}</td> <td>{group.description}</td>
</tr> </tr>
<tr>
<td>{t("group.creationDate")}</td>
<td>{new Date(group.creationDate).toString()}</td>
</tr>
<tr>
<td>{t("group.lastModified")}</td>
<td>{new Date(group.lastModified).toString()}</td>
</tr>
<tr> <tr>
<td>{t("group.type")}</td> <td>{t("group.type")}</td>
<td>{group.type}</td> <td>{group.type}</td>

View File

@@ -1,25 +1,46 @@
//@flow //@flow
import React from 'react'; import React from "react";
import Page from "../../components/layout/Page" import Page from "../../components/layout/Page";
import { translate } from "react-i18next"; import { translate } from "react-i18next";
import GroupForm from './GroupForm'; import GroupForm from "./GroupForm";
import type { Group } from "../types/Group" import { connect } from "react-redux";
import { createGroup } from "../modules/groups";
import type { Group } from "../types/Group";
export interface Props { export interface Props {
t: string => string t: string => string;
createGroup: Group => void;
} }
export interface State { export interface State {}
}
class AddGroup extends React.Component<Props, State> { class AddGroup extends React.Component<Props, State> {
render() { render() {
const { t } = this.props; const { t } = this.props;
return <Page title={t("add-group.title")} subtitle={t("add-group.subtitle")}><div><GroupForm submitForm={(group: Group)=>{}}/></div></Page> return (
<Page title={t("add-group.title")} subtitle={t("add-group.subtitle")}>
<div>
<GroupForm submitForm={group => this.createGroup(group)} />
</div>
</Page>
);
} }
createGroup = (group: Group) => {
this.props.createGroup(group);
};
} }
export default translate("groups")(AddGroup); const mapDispatchToProps = dispatch => {
return {
createGroup: (group: Group) => dispatch(createGroup(group))
};
};
const mapStateToProps = state => {};
export default connect(
mapStateToProps,
mapDispatchToProps
)(translate("groups")(AddGroup));

View File

@@ -1,19 +1,40 @@
//@flow //@flow
import React from 'react'; import React from "react";
import InputField from "../../components/forms/InputField";
import { SubmitButton } from "../../components/buttons";
import { translate } from "react-i18next";
import type { Group } from "../types/Group";
import InputField from "../../components/forms/InputField"
import SubmitButton from "../../components/buttons/SubmitButton"
import type { Group } from "../types/Group"
export interface Props { export interface Props {
loading?: boolean, t: string => string;
submitForm: Group => void submitForm: Group => void;
} }
export interface State { export interface State {
group: Group group: Group;
} }
class GroupForm extends React.Component<Props, State> { class GroupForm extends React.Component<Props, State> {
constructor(props) {
super(props);
this.state = {
group: {
name: "",
description: "",
_embedded: {
members: []
},
_links: {},
members: [],
type: "",
}
};
}
onSubmit = (event: Event) => {
event.preventDefault();
this.props.submitForm(this.state.group);
};
isValid = () => { isValid = () => {
return true; return true;
@@ -25,21 +46,43 @@ class GroupForm extends React.Component<Props, State> {
} }
render() { render() {
const { loading } = this.props; const { t } = this.props;
return ( return (
// TODO: i18n <form onSubmit={this.onSubmit}>
<form onSubmit={this.submit}> <InputField
<InputField label="Name" errorMessage="Invalid group name" onChange={()=>{}} validationError={false}/> label={t("group.name")}
<InputField label="Description" errorMessage="Invalid group description" onChange={()=>{}} validationError={false} /> errorMessage=""
<SubmitButton onChange={this.handleGroupNameChange}
disabled={!this.isValid()} validationError={false}
loading={loading}
label="Submit"
/> />
<InputField
label={t("group.description")}
errorMessage=""
onChange={this.handleDescriptionChange}
validationError={false}
/>
<SubmitButton label={t("group-form.submit")} />
</form> </form>
) );
} }
handleGroupNameChange = (name: string) => {
this.setState({
group: {
...this.state.group,
name
}
});
};
handleDescriptionChange = (description: string) => {
this.setState({
group: {
...this.state.group,
description
}
});
};
} }
export default GroupForm; export default translate("groups")(GroupForm);

View File

@@ -8,6 +8,7 @@ import type { History } from "history";
import { Page } from "../../components/layout"; import { Page } from "../../components/layout";
import { GroupTable } from "./../components/table"; import { GroupTable } from "./../components/table";
import Paginator from "../../components/Paginator"; import Paginator from "../../components/Paginator";
import CreateGroupButton from "../components/buttons/CreateGroupButton";
import { import {
fetchGroupsByPage, fetchGroupsByPage,
@@ -37,7 +38,6 @@ type Props = {
}; };
class Groups extends React.Component<Props> { class Groups extends React.Component<Props> {
componentDidMount() { componentDidMount() {
this.props.fetchGroupsByPage(this.props.page); this.props.fetchGroupsByPage(this.props.page);
} }
@@ -69,7 +69,7 @@ class Groups extends React.Component<Props> {
loading={loading || !groups} loading={loading || !groups}
error={error} error={error}
> >
<GroupTable groups={groups} /> <GroupTable groups={groups} />
{this.renderPaginator()} {this.renderPaginator()}
{this.renderCreateButton()} {this.renderCreateButton()}
</Page> </Page>
@@ -85,11 +85,11 @@ class Groups extends React.Component<Props> {
} }
renderCreateButton() { renderCreateButton() {
/* if (this.props.canAddGroups) { if (this.props.canAddGroups) {
return <CreateGroupButton />; return <CreateGroupButton />;
} else { } else {
return; return;
}*/ }
} }
} }
@@ -122,7 +122,7 @@ const mapStateToProps = (state, ownProps) => {
}; };
}; };
const mapDispatchToProps = (dispatch) => { const mapDispatchToProps = dispatch => {
return { return {
fetchGroupsByPage: (page: number) => { fetchGroupsByPage: (page: number) => {
dispatch(fetchGroupsByPage(page)); dispatch(fetchGroupsByPage(page));

View File

@@ -168,6 +168,54 @@ export function createGroupFailure(error: Error) {
}; };
} }
//delete group
export function deleteGroup(group: Group, callback?: () => void) {
return function(dispatch: any) {
dispatch(deleteGroupPending(group));
return apiClient
.delete(group._links.delete.href)
.then(() => {
dispatch(deleteGroupSuccess(group));
if (callback) {
callback();
}
})
.catch(cause => {
const error = new Error(
`could not delete group ${group.name}: ${cause.message}`
);
dispatch(deleteGroupFailure(group, error));
});
};
}
export function deleteGroupPending(group: Group): Action {
return {
type: DELETE_GROUP_PENDING,
payload: group,
itemId: group.name
};
}
export function deleteGroupSuccess(group: Group): Action {
return {
type: DELETE_GROUP_SUCCESS,
payload: group,
itemId: group.name
};
}
export function deleteGroupFailure(group: Group, error: Error): Action {
return {
type: DELETE_GROUP_FAILURE,
payload: {
error,
group
},
itemId: group.name
};
}
//reducer //reducer
function extractGroupsByNames( function extractGroupsByNames(
@@ -187,6 +235,22 @@ function extractGroupsByNames(
return groupsByNames; return groupsByNames;
} }
function deleteGroupInGroupsByNames(groups: {}, groupName: string) {
let newGroups = {};
for (let groupname in groups) {
if (groupname !== groupName) newGroups[groupname] = groups[groupname];
}
return newGroups;
}
function deleteGroupInEntries(groups: [], groupName: string) {
let newGroups = [];
for (let group of groups) {
if (group !== groupName) newGroups.push(group);
}
return newGroups;
}
const reducerByName = (state: any, groupname: string, newGroupState: any) => { const reducerByName = (state: any, groupname: string, newGroupState: any) => {
const newGroupsByNames = { const newGroupsByNames = {
...state, ...state,
@@ -211,7 +275,16 @@ function listReducer(state: any = {}, action: any = {}) {
_links: action.payload._links _links: action.payload._links
} }
}; };
// Delete single group actions
case DELETE_GROUP_SUCCESS:
const newGroupEntries = deleteGroupInEntries(
state.entries,
action.payload.name
);
return {
...state,
entries: newGroupEntries
};
default: default:
return state; return state;
} }
@@ -229,6 +302,13 @@ function byNamesReducer(state: any = {}, action: any = {}) {
}; };
case FETCH_GROUP_SUCCESS: case FETCH_GROUP_SUCCESS:
return reducerByName(state, action.payload.name, action.payload); return reducerByName(state, action.payload.name, action.payload);
case DELETE_GROUP_SUCCESS:
const newGroupByNames = deleteGroupInGroupsByNames(
state,
action.payload.name
);
return newGroupByNames;
default: default:
return state; return state;
} }
@@ -311,3 +391,11 @@ export function isFetchGroupPending(state: Object, name: string) {
export function getFetchGroupFailure(state: Object, name: string) { export function getFetchGroupFailure(state: Object, name: string) {
return getFailure(state, FETCH_GROUP, name); return getFailure(state, FETCH_GROUP, name);
} }
export function isDeleteGroupPending(state: Object, name: string) {
return isPending(state, DELETE_GROUP, name);
}
export function getDeleteGroupFailure(state: Object, name: string) {
return getFailure(state, DELETE_GROUP, name);
}

View File

@@ -30,7 +30,15 @@ import reducer, {
CREATE_GROUP_FAILURE, CREATE_GROUP_FAILURE,
isCreateGroupPending, isCreateGroupPending,
CREATE_GROUP, CREATE_GROUP,
getCreateGroupFailure getCreateGroupFailure,
deleteGroup,
DELETE_GROUP_PENDING,
DELETE_GROUP_SUCCESS,
DELETE_GROUP_FAILURE,
DELETE_GROUP,
deleteGroupSuccess,
isDeleteGroupPending,
getDeleteGroupFailure
} from "./groups"; } from "./groups";
const GROUPS_URL = "/scm/api/rest/v2/groups"; const GROUPS_URL = "/scm/api/rest/v2/groups";
@@ -45,13 +53,13 @@ const humanGroup = {
members: ["userZaphod"], members: ["userZaphod"],
_links: { _links: {
self: { self: {
href: "http://localhost:3000/scm/api/rest/v2/groups/humanGroup" href: "http://localhost:8081/scm/api/rest/v2/groups/humanGroup"
}, },
delete: { delete: {
href: "http://localhost:3000/scm/api/rest/v2/groups/humanGroup" href: "http://localhost:8081/scm/api/rest/v2/groups/humanGroup"
}, },
update: { update: {
href:"http://localhost:3000/scm/api/rest/v2/groups/humanGroup" href:"http://localhost:8081/scm/api/rest/v2/groups/humanGroup"
} }
}, },
_embedded: { _embedded: {
@@ -60,7 +68,7 @@ const humanGroup = {
name: "userZaphod", name: "userZaphod",
_links: { _links: {
self: { self: {
href: "http://localhost:3000/scm/api/rest/v2/users/userZaphod" href: "http://localhost:8081/scm/api/rest/v2/users/userZaphod"
} }
} }
} }
@@ -77,13 +85,13 @@ const emptyGroup = {
members: [], members: [],
_links: { _links: {
self: { self: {
href: "http://localhost:3000/scm/api/rest/v2/groups/emptyGroup" href: "http://localhost:8081/scm/api/rest/v2/groups/emptyGroup"
}, },
delete: { delete: {
href: "http://localhost:3000/scm/api/rest/v2/groups/emptyGroup" href: "http://localhost:8081/scm/api/rest/v2/groups/emptyGroup"
}, },
update: { update: {
href:"http://localhost:3000/scm/api/rest/v2/groups/emptyGroup" href:"http://localhost:8081/scm/api/rest/v2/groups/emptyGroup"
} }
}, },
_embedded: { _embedded: {
@@ -158,10 +166,10 @@ describe("groups fetch()", () => {
}); });
it("should sucessfully fetch single group", () => { it("should sucessfully fetch single group", () => {
fetchMock.getOnce(GROUPS_URL + "/humandGroup", humanGroup); fetchMock.getOnce(GROUPS_URL + "/humanGroup", humanGroup);
const store = mockStore({}); const store = mockStore({});
return store.dispatch(fetchGroup("humandGroup")).then(() => { return store.dispatch(fetchGroup("humanGroup")).then(() => {
const actions = store.getActions(); const actions = store.getActions();
expect(actions[0].type).toEqual(FETCH_GROUP_PENDING); expect(actions[0].type).toEqual(FETCH_GROUP_PENDING);
expect(actions[1].type).toEqual(FETCH_GROUP_SUCCESS); expect(actions[1].type).toEqual(FETCH_GROUP_SUCCESS);
@@ -170,12 +178,12 @@ describe("groups fetch()", () => {
}); });
it("should fail fetching single group on HTTP 500", () => { it("should fail fetching single group on HTTP 500", () => {
fetchMock.getOnce(GROUPS_URL + "/humandGroup", { fetchMock.getOnce(GROUPS_URL + "/humanGroup", {
status: 500 status: 500
}); });
const store = mockStore({}); const store = mockStore({});
return store.dispatch(fetchGroup("humandGroup")).then(() => { return store.dispatch(fetchGroup("humanGroup")).then(() => {
const actions = store.getActions(); const actions = store.getActions();
expect(actions[0].type).toEqual(FETCH_GROUP_PENDING); expect(actions[0].type).toEqual(FETCH_GROUP_PENDING);
expect(actions[1].type).toEqual(FETCH_GROUP_FAILURE); expect(actions[1].type).toEqual(FETCH_GROUP_FAILURE);
@@ -211,6 +219,53 @@ describe("groups fetch()", () => {
expect(actions[1].payload instanceof Error).toBeTruthy(); expect(actions[1].payload instanceof Error).toBeTruthy();
}); });
}); });
it("should delete successfully group humanGroup", () => {
fetchMock.deleteOnce("http://localhost:8081/scm/api/rest/v2/groups/humanGroup", {
status: 204
});
const store = mockStore({});
return store.dispatch(deleteGroup(humanGroup)).then(() => {
const actions = store.getActions();
expect(actions.length).toBe(2);
expect(actions[0].type).toEqual(DELETE_GROUP_PENDING);
expect(actions[0].payload).toBe(humanGroup);
expect(actions[1].type).toEqual(DELETE_GROUP_SUCCESS);
});
});
it("should call the callback, after successful delete", () => {
fetchMock.deleteOnce("http://localhost:8081/scm/api/rest/v2/groups/humanGroup", {
status: 204
});
let called = false;
const callMe = () => {
called = true;
};
const store = mockStore({});
return store.dispatch(deleteGroup(humanGroup, callMe)).then(() => {
expect(called).toBeTruthy();
});
});
it("should fail to delete group humanGroup", () => {
fetchMock.deleteOnce("http://localhost:8081/scm/api/rest/v2/groups/humanGroup", {
status: 500
});
const store = mockStore({});
return store.dispatch(deleteGroup(humanGroup)).then(() => {
const actions = store.getActions();
expect(actions[0].type).toEqual(DELETE_GROUP_PENDING);
expect(actions[0].payload).toBe(humanGroup);
expect(actions[1].type).toEqual(DELETE_GROUP_FAILURE);
expect(actions[1].payload).toBeDefined();
});
});
}); });
describe("groups reducer", () => { describe("groups reducer", () => {
@@ -283,6 +338,24 @@ describe("groups reducer", () => {
expect(newState.list.entries).toEqual(["humanGroup"]); expect(newState.list.entries).toEqual(["humanGroup"]);
}); });
it("should remove group from state when delete succeeds", () => {
const state = {
list: {
entries: ["humanGroup", "emptyGroup"]
},
byNames: {
humanGroup: humanGroup,
emptyGroup: emptyGroup
}
};
const newState = reducer(state, deleteGroupSuccess(emptyGroup));
expect(newState.byNames["humanGroup"]).toBeDefined();
expect(newState.byNames["emptyGroup"]).toBeFalsy();
expect(newState.list.entries).toEqual(["humanGroup"]);
});
}); });
describe("selector tests", () => { describe("selector tests", () => {
@@ -384,30 +457,30 @@ describe("selector tests", () => {
expect(getGroupByName(state, "emptyGroup")).toEqual(emptyGroup); expect(getGroupByName(state, "emptyGroup")).toEqual(emptyGroup);
}); });
it("should return true, when fetch group humandGroup is pending", () => { it("should return true, when fetch group humanGroup is pending", () => {
const state = { const state = {
pending: { pending: {
[FETCH_GROUP + "/humandGroup"]: true [FETCH_GROUP + "/humanGroup"]: true
} }
}; };
expect(isFetchGroupPending(state, "humandGroup")).toEqual(true); expect(isFetchGroupPending(state, "humanGroup")).toEqual(true);
}); });
it("should return false, when fetch group humandGroup is not pending", () => { it("should return false, when fetch group humanGroup is not pending", () => {
expect(isFetchGroupPending({}, "humandGroup")).toEqual(false); expect(isFetchGroupPending({}, "humanGroup")).toEqual(false);
}); });
it("should return error when fetch group humandGroup did fail", () => { it("should return error when fetch group humanGroup did fail", () => {
const state = { const state = {
failure: { failure: {
[FETCH_GROUP + "/humandGroup"]: error [FETCH_GROUP + "/humanGroup"]: error
} }
}; };
expect(getFetchGroupFailure(state, "humandGroup")).toEqual(error); expect(getFetchGroupFailure(state, "humanGroup")).toEqual(error);
}); });
it("should return undefined when fetch group humandGroup did not fail", () => { it("should return undefined when fetch group humanGroup did not fail", () => {
expect(getFetchGroupFailure({}, "humandGroup")).toBe(undefined); expect(getFetchGroupFailure({}, "humanGroup")).toBe(undefined);
}); });
it("should return true if create group is pending", () => { it("should return true if create group is pending", () => {
@@ -432,4 +505,31 @@ describe("selector tests", () => {
expect(getCreateGroupFailure({})).toBeUndefined() expect(getCreateGroupFailure({})).toBeUndefined()
}) })
it("should return true, when delete group humanGroup is pending", () => {
const state = {
pending: {
[DELETE_GROUP + "/humanGroup"]: true
}
};
expect(isDeleteGroupPending(state, "humanGroup")).toEqual(true);
});
it("should return false, when delete group humanGroup is not pending", () => {
expect(isDeleteGroupPending({}, "humanGroup")).toEqual(false);
});
it("should return error when delete group humanGroup did fail", () => {
const state = {
failure: {
[DELETE_GROUP + "/humanGroup"]: error
}
};
expect(getDeleteGroupFailure(state, "humanGroup")).toEqual(error);
});
it("should return undefined when delete group humanGroup did not fail", () => {
expect(getDeleteGroupFailure({}, "humanGroup")).toBe(undefined);
});
}); });

View File

@@ -4,9 +4,7 @@ import type { User } from "../../users/types/User";
export type Group = { export type Group = {
name: string, name: string,
creationDate: string,
description: string, description: string,
lastModified: string,
type: string, type: string,
members: string[], members: string[],
_links: Links, _links: Links,