From c12040d8d0adbbb01ee57fb4171d40fc812e786f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maren=20S=C3=BCwer?= Date: Tue, 31 Jul 2018 16:48:28 +0200 Subject: [PATCH 1/3] added delete option --- scm-ui/src/groups/modules/groups.js | 90 +++++++++++++- scm-ui/src/groups/modules/groups.test.js | 144 +++++++++++++++++++---- 2 files changed, 211 insertions(+), 23 deletions(-) diff --git a/scm-ui/src/groups/modules/groups.js b/scm-ui/src/groups/modules/groups.js index a3a5aa1bdb..b91c8f2eb4 100644 --- a/scm-ui/src/groups/modules/groups.js +++ b/scm-ui/src/groups/modules/groups.js @@ -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 function extractGroupsByNames( @@ -187,6 +235,22 @@ function extractGroupsByNames( 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 newGroupsByNames = { ...state, @@ -211,7 +275,16 @@ function listReducer(state: any = {}, action: any = {}) { _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: return state; } @@ -229,6 +302,13 @@ function byNamesReducer(state: any = {}, action: any = {}) { }; case FETCH_GROUP_SUCCESS: return reducerByName(state, action.payload.name, action.payload); + case DELETE_GROUP_SUCCESS: + const newGroupByNames = deleteGroupInGroupsByNames( + state, + action.payload.name + ); + return newGroupByNames; + default: return state; } @@ -311,3 +391,11 @@ export function isFetchGroupPending(state: Object, name: string) { export function getFetchGroupFailure(state: Object, name: string) { 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); +} diff --git a/scm-ui/src/groups/modules/groups.test.js b/scm-ui/src/groups/modules/groups.test.js index cf3e7c0c95..1b7c0fec0b 100644 --- a/scm-ui/src/groups/modules/groups.test.js +++ b/scm-ui/src/groups/modules/groups.test.js @@ -30,7 +30,15 @@ import reducer, { CREATE_GROUP_FAILURE, isCreateGroupPending, CREATE_GROUP, - getCreateGroupFailure + getCreateGroupFailure, + deleteGroup, + DELETE_GROUP_PENDING, + DELETE_GROUP_SUCCESS, + DELETE_GROUP_FAILURE, + DELETE_GROUP, + deleteGroupSuccess, + isDeleteGroupPending, + getDeleteGroupFailure } from "./groups"; const GROUPS_URL = "/scm/api/rest/v2/groups"; @@ -45,13 +53,13 @@ const humanGroup = { members: ["userZaphod"], _links: { self: { - href: "http://localhost:3000/scm/api/rest/v2/groups/humanGroup" + href: "http://localhost:8081/scm/api/rest/v2/groups/humanGroup" }, delete: { - href: "http://localhost:3000/scm/api/rest/v2/groups/humanGroup" + href: "http://localhost:8081/scm/api/rest/v2/groups/humanGroup" }, update: { - href:"http://localhost:3000/scm/api/rest/v2/groups/humanGroup" + href:"http://localhost:8081/scm/api/rest/v2/groups/humanGroup" } }, _embedded: { @@ -60,7 +68,7 @@ const humanGroup = { name: "userZaphod", _links: { 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: [], _links: { self: { - href: "http://localhost:3000/scm/api/rest/v2/groups/emptyGroup" + href: "http://localhost:8081/scm/api/rest/v2/groups/emptyGroup" }, delete: { - href: "http://localhost:3000/scm/api/rest/v2/groups/emptyGroup" + href: "http://localhost:8081/scm/api/rest/v2/groups/emptyGroup" }, update: { - href:"http://localhost:3000/scm/api/rest/v2/groups/emptyGroup" + href:"http://localhost:8081/scm/api/rest/v2/groups/emptyGroup" } }, _embedded: { @@ -158,10 +166,10 @@ describe("groups fetch()", () => { }); it("should sucessfully fetch single group", () => { - fetchMock.getOnce(GROUPS_URL + "/humandGroup", humanGroup); + fetchMock.getOnce(GROUPS_URL + "/humanGroup", humanGroup); const store = mockStore({}); - return store.dispatch(fetchGroup("humandGroup")).then(() => { + return store.dispatch(fetchGroup("humanGroup")).then(() => { const actions = store.getActions(); expect(actions[0].type).toEqual(FETCH_GROUP_PENDING); expect(actions[1].type).toEqual(FETCH_GROUP_SUCCESS); @@ -170,12 +178,12 @@ describe("groups fetch()", () => { }); it("should fail fetching single group on HTTP 500", () => { - fetchMock.getOnce(GROUPS_URL + "/humandGroup", { + fetchMock.getOnce(GROUPS_URL + "/humanGroup", { status: 500 }); const store = mockStore({}); - return store.dispatch(fetchGroup("humandGroup")).then(() => { + return store.dispatch(fetchGroup("humanGroup")).then(() => { const actions = store.getActions(); expect(actions[0].type).toEqual(FETCH_GROUP_PENDING); expect(actions[1].type).toEqual(FETCH_GROUP_FAILURE); @@ -211,6 +219,53 @@ describe("groups fetch()", () => { 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", () => { @@ -283,6 +338,24 @@ describe("groups reducer", () => { 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", () => { @@ -384,30 +457,30 @@ describe("selector tests", () => { 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 = { 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", () => { - expect(isFetchGroupPending({}, "humandGroup")).toEqual(false); + it("should return false, when fetch group humanGroup is not pending", () => { + 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 = { 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", () => { - expect(getFetchGroupFailure({}, "humandGroup")).toBe(undefined); + it("should return undefined when fetch group humanGroup did not fail", () => { + expect(getFetchGroupFailure({}, "humanGroup")).toBe(undefined); }); it("should return true if create group is pending", () => { @@ -432,4 +505,31 @@ describe("selector tests", () => { 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); + }); + }); From d532c36204b7e9ee4d4062dcda4c1c8ac40f8c56 Mon Sep 17 00:00:00 2001 From: Philipp Czora Date: Tue, 31 Jul 2018 18:44:01 +0200 Subject: [PATCH 2/3] Groups can now be added via the UI --- scm-ui/public/locales/en/groups.json | 6 ++ .../components/buttons/CreateGroupButton.js | 30 ++++++++++ scm-ui/src/groups/containers/AddGroup.js | 38 ++++++++++--- scm-ui/src/groups/containers/GroupForm.js | 57 +++++++++++++++++-- scm-ui/src/groups/containers/Groups.js | 10 ++-- 5 files changed, 121 insertions(+), 20 deletions(-) create mode 100644 scm-ui/src/groups/components/buttons/CreateGroupButton.js diff --git a/scm-ui/public/locales/en/groups.json b/scm-ui/public/locales/en/groups.json index 6caf430157..247ba59f81 100644 --- a/scm-ui/public/locales/en/groups.json +++ b/scm-ui/public/locales/en/groups.json @@ -22,5 +22,11 @@ "add-group": { "title": "Create Group", "subtitle": "Create a new group" + }, + "create-group-button": { + "label": "Create group" + }, + "group-form": { + "submit": "Submit" } } diff --git a/scm-ui/src/groups/components/buttons/CreateGroupButton.js b/scm-ui/src/groups/components/buttons/CreateGroupButton.js new file mode 100644 index 0000000000..2d8a2c2762 --- /dev/null +++ b/scm-ui/src/groups/components/buttons/CreateGroupButton.js @@ -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 { + render() { + const { classes, t } = this.props; + return ( +
+ +
+ ); + } +} + +export default translate("groups")(injectSheet(styles)(CreateGroupButton)); diff --git a/scm-ui/src/groups/containers/AddGroup.js b/scm-ui/src/groups/containers/AddGroup.js index ee6166fa2e..d1fd7b0b62 100644 --- a/scm-ui/src/groups/containers/AddGroup.js +++ b/scm-ui/src/groups/containers/AddGroup.js @@ -1,24 +1,44 @@ //@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 GroupForm from './GroupForm'; +import GroupForm from "./GroupForm"; +import { connect } from "react-redux"; +import { createGroup } from "../modules/groups"; export interface Props { - t: string => string + t: string => string; } -export interface State { -} +export interface State {} class AddGroup extends React.Component { - render() { const { t } = this.props; - return
+ return ( + +
+ this.createGroup(group)} /> +
+
+ ); } + 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)); diff --git a/scm-ui/src/groups/containers/GroupForm.js b/scm-ui/src/groups/containers/GroupForm.js index f65278bcf5..4ef2b89035 100644 --- a/scm-ui/src/groups/containers/GroupForm.js +++ b/scm-ui/src/groups/containers/GroupForm.js @@ -1,23 +1,68 @@ //@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" export interface Props { + t: string => string; + submitForm: Group => void; } export interface State { + group: Group; } class GroupForm extends React.Component { + constructor(props) { + super(props); + this.state = {}; + } + onSubmit = (event: Event) => { + event.preventDefault(); + this.props.submitForm(this.state.group); + }; render() { + const { t } = this.props; return ( -
- {}} validationError={false}/> + + + + - ) + ); } + handleGroupNameChange = (name: string) => { + this.setState({ + group: { + ...this.state.group, + name + } + }); + }; + + handleDescriptionChange = (description: string) => { + this.setState({ + group: { + ...this.state.group, + description + } + }); + }; } -export default GroupForm; \ No newline at end of file +export default translate("groups")(GroupForm); diff --git a/scm-ui/src/groups/containers/Groups.js b/scm-ui/src/groups/containers/Groups.js index bf6c89efe2..7b7253bd98 100644 --- a/scm-ui/src/groups/containers/Groups.js +++ b/scm-ui/src/groups/containers/Groups.js @@ -8,6 +8,7 @@ import type { History } from "history"; import { Page } from "../../components/layout"; import { GroupTable } from "./../components/table"; import Paginator from "../../components/Paginator"; +import CreateGroupButton from "../components/buttons/CreateGroupButton"; import { fetchGroupsByPage, @@ -37,7 +38,6 @@ type Props = { }; class Groups extends React.Component { - componentDidMount() { this.props.fetchGroupsByPage(this.props.page); } @@ -69,7 +69,7 @@ class Groups extends React.Component { loading={loading || !groups} error={error} > - + {this.renderPaginator()} {this.renderCreateButton()} @@ -85,11 +85,11 @@ class Groups extends React.Component { } renderCreateButton() { - /* if (this.props.canAddGroups) { + if (this.props.canAddGroups) { return ; } else { return; - }*/ + } } } @@ -122,7 +122,7 @@ const mapStateToProps = (state, ownProps) => { }; }; -const mapDispatchToProps = (dispatch) => { +const mapDispatchToProps = dispatch => { return { fetchGroupsByPage: (page: number) => { dispatch(fetchGroupsByPage(page)); From 58fbf4ebf2417d7101e152350c008d1b9067d2ce Mon Sep 17 00:00:00 2001 From: Philipp Czora Date: Tue, 31 Jul 2018 19:05:06 +0200 Subject: [PATCH 3/3] Removed creationDate & lastModifiedDate from Group; fixed flow issuses --- scm-ui/src/groups/components/table/Details.js | 8 -------- scm-ui/src/groups/containers/AddGroup.js | 2 ++ scm-ui/src/groups/containers/GroupForm.js | 13 ++++++++++++- scm-ui/src/groups/types/Group.js | 2 -- 4 files changed, 14 insertions(+), 11 deletions(-) diff --git a/scm-ui/src/groups/components/table/Details.js b/scm-ui/src/groups/components/table/Details.js index ab8c1324ce..b3023d3fbb 100644 --- a/scm-ui/src/groups/components/table/Details.js +++ b/scm-ui/src/groups/components/table/Details.js @@ -26,14 +26,6 @@ class Details extends React.Component { {t("group.description")} {group.description} - - {t("group.creationDate")} - {new Date(group.creationDate).toString()} - - - {t("group.lastModified")} - {new Date(group.lastModified).toString()} - {t("group.type")} {group.type} diff --git a/scm-ui/src/groups/containers/AddGroup.js b/scm-ui/src/groups/containers/AddGroup.js index d1fd7b0b62..78c679eeef 100644 --- a/scm-ui/src/groups/containers/AddGroup.js +++ b/scm-ui/src/groups/containers/AddGroup.js @@ -6,9 +6,11 @@ import { translate } from "react-i18next"; import GroupForm from "./GroupForm"; import { connect } from "react-redux"; import { createGroup } from "../modules/groups"; +import type { Group } from "../types/Group"; export interface Props { t: string => string; + createGroup: Group => void; } export interface State {} diff --git a/scm-ui/src/groups/containers/GroupForm.js b/scm-ui/src/groups/containers/GroupForm.js index 4ef2b89035..0668b2bf2c 100644 --- a/scm-ui/src/groups/containers/GroupForm.js +++ b/scm-ui/src/groups/containers/GroupForm.js @@ -18,7 +18,18 @@ export interface State { class GroupForm extends React.Component { constructor(props) { super(props); - this.state = {}; + this.state = { + group: { + name: "", + description: "", + _embedded: { + members: [] + }, + _links: {}, + members: [], + type: "", + } + }; } onSubmit = (event: Event) => { event.preventDefault(); diff --git a/scm-ui/src/groups/types/Group.js b/scm-ui/src/groups/types/Group.js index b300685e84..92ed18b822 100644 --- a/scm-ui/src/groups/types/Group.js +++ b/scm-ui/src/groups/types/Group.js @@ -4,9 +4,7 @@ import type { User } from "../../users/types/User"; export type Group = { name: string, - creationDate: string, description: string, - lastModified: string, type: string, members: string[], _links: Links,