mirror of
https://github.com/scm-manager/scm-manager.git
synced 2026-03-22 03:51:36 +01:00
merge and add single group view
This commit is contained in:
@@ -1,12 +1,24 @@
|
||||
{
|
||||
"group": {
|
||||
"name": "Name",
|
||||
"description": "Description"
|
||||
"description": "Description",
|
||||
"creationDate": "Creation Date",
|
||||
"lastModified": "Last Modified",
|
||||
"type": "Type",
|
||||
"members": "Members"
|
||||
},
|
||||
"groups": {
|
||||
"title": "Groups",
|
||||
"subtitle": "Create, read, update and delete groups"
|
||||
},
|
||||
"single-group": {
|
||||
"error-title": "Error",
|
||||
"error-subtitle": "Unknown group error",
|
||||
"navigation-label": "Navigation",
|
||||
"actions-label": "Actions",
|
||||
"information-label": "Information",
|
||||
"back-label": "Back"
|
||||
},
|
||||
"add-group": {
|
||||
"title": "Create Group",
|
||||
"subtitle": "Create a new group"
|
||||
|
||||
@@ -14,7 +14,8 @@ import AddUser from "../users/containers/AddUser";
|
||||
import SingleUser from "../users/containers/SingleUser";
|
||||
|
||||
import Groups from "../groups/containers/Groups";
|
||||
import AddGroup from "../groups/containers/AddGroup"
|
||||
import SingleGroup from "../groups/containers/SingleGroup";
|
||||
import AddGroup from "../groups/containers/AddGroup";
|
||||
|
||||
type Props = {
|
||||
authenticated?: boolean
|
||||
@@ -62,6 +63,11 @@ class Main extends React.Component<Props> {
|
||||
component={Groups}
|
||||
authenticated={authenticated}
|
||||
/>
|
||||
<ProtectedRoute
|
||||
authenticated={authenticated}
|
||||
path="/group/:name"
|
||||
component={SingleGroup}
|
||||
/>
|
||||
<ProtectedRoute
|
||||
authenticated={authenticated}
|
||||
path="/groups/add"
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import React from "react";
|
||||
import type { Group } from "../../types/Group";
|
||||
import { translate } from "react-i18next";
|
||||
import GroupMember from "./GroupMember";
|
||||
|
||||
type Props = {
|
||||
group: Group,
|
||||
@@ -9,6 +10,7 @@ type Props = {
|
||||
};
|
||||
|
||||
class Details extends React.Component<Props> {
|
||||
|
||||
render() {
|
||||
const { group, t } = this.props;
|
||||
return (
|
||||
@@ -22,6 +24,28 @@ class Details extends React.Component<Props> {
|
||||
<td>{t("group.description")}</td>
|
||||
<td>{group.description}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{t("group.creationDate")}</td>
|
||||
<td>{group.creationDate}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{t("group.lastModified")}</td>
|
||||
<td>{group.lastModified}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{t("group.type")}</td>
|
||||
<td>{group.type}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{t("group.members")}</td>
|
||||
<td>
|
||||
<table><tbody>
|
||||
{group.members.map((member, index) => {
|
||||
return <GroupMember key={index} member={member} />;
|
||||
})}
|
||||
</tbody></table>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
|
||||
26
scm-ui/src/groups/components/table/GroupMember.js
Normal file
26
scm-ui/src/groups/components/table/GroupMember.js
Normal file
@@ -0,0 +1,26 @@
|
||||
// @flow
|
||||
import React from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
|
||||
type Props = {
|
||||
member: string
|
||||
};
|
||||
|
||||
export default class GroupMember extends React.Component<Props> {
|
||||
renderLink(to: string, label: string) {
|
||||
return <Link to={to}>{label}</Link>;
|
||||
}
|
||||
|
||||
render() {
|
||||
const { member } = this.props;
|
||||
const to = `/user/${member}`;
|
||||
return (
|
||||
<tr className="is-hidden-mobile">
|
||||
<td>
|
||||
{this.renderLink(to, member)}
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -18,7 +18,7 @@ export default class GroupRow extends React.Component<Props> {
|
||||
return (
|
||||
<tr>
|
||||
<td className="is-hidden-mobile">{this.renderLink(to, group.name)}</td>
|
||||
<td>{this.renderLink(to, group.description)}</td>
|
||||
<td>{group.description}</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
|
||||
123
scm-ui/src/groups/containers/SingleGroup.js
Normal file
123
scm-ui/src/groups/containers/SingleGroup.js
Normal file
@@ -0,0 +1,123 @@
|
||||
//@flow
|
||||
import React from "react";
|
||||
import { connect } from "react-redux";
|
||||
import { Page } from "../../components/layout";
|
||||
import { Route } from "react-router";
|
||||
import { Details } from "./../components/table";
|
||||
import type { Group } from "../types/Group";
|
||||
import type { History } from "history";
|
||||
import {
|
||||
fetchGroup,
|
||||
getGroupByName,
|
||||
isFetchGroupPending,
|
||||
getFetchGroupFailure,
|
||||
} from "../modules/groups";
|
||||
import Loading from "../../components/Loading";
|
||||
|
||||
import { Navigation, Section, NavLink } from "../../components/navigation";
|
||||
import ErrorPage from "../../components/ErrorPage";
|
||||
import { translate } from "react-i18next";
|
||||
|
||||
type Props = {
|
||||
name: string,
|
||||
group: Group,
|
||||
loading: boolean,
|
||||
error: Error,
|
||||
|
||||
// dispatcher functions
|
||||
fetchGroup: string => void,
|
||||
|
||||
// context objects
|
||||
t: string => string,
|
||||
match: any,
|
||||
history: History
|
||||
};
|
||||
|
||||
class SingleGroup extends React.Component<Props> {
|
||||
componentDidMount() {
|
||||
this.props.fetchGroup(this.props.name);
|
||||
}
|
||||
|
||||
stripEndingSlash = (url: string) => {
|
||||
if (url.endsWith("/")) {
|
||||
return url.substring(0, url.length - 2);
|
||||
}
|
||||
return url;
|
||||
};
|
||||
|
||||
matchedUrl = () => {
|
||||
return this.stripEndingSlash(this.props.match.url);
|
||||
};
|
||||
|
||||
render() {
|
||||
const { t, loading, error, group } = this.props;
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<ErrorPage
|
||||
title={t("single-group.error-title")}
|
||||
subtitle={t("single-group.error-subtitle")}
|
||||
error={error}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (!group || loading) {
|
||||
return <Loading />;
|
||||
}
|
||||
|
||||
const url = this.matchedUrl();
|
||||
|
||||
return (
|
||||
<Page title={group.name}>
|
||||
<div className="columns">
|
||||
<div className="column is-three-quarters">
|
||||
<Route path={url} exact component={() => <Details group={group} />} />
|
||||
</div>
|
||||
<div className="column">
|
||||
<Navigation>
|
||||
<Section label={t("single-group.navigation-label")}>
|
||||
<NavLink
|
||||
to={`${url}`}
|
||||
label={t("single-group.information-label")}
|
||||
/>
|
||||
</Section>
|
||||
<Section label={t("single-group.actions-label")}>
|
||||
<NavLink to="/groups" label={t("single-group.back-label")} />
|
||||
</Section>
|
||||
</Navigation>
|
||||
</div>
|
||||
</div>
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const mapStateToProps = (state, ownProps) => {
|
||||
const name = ownProps.match.params.name;
|
||||
const group = getGroupByName(state, name);
|
||||
const loading =
|
||||
isFetchGroupPending(state, name);
|
||||
const error =
|
||||
getFetchGroupFailure(state, name);
|
||||
|
||||
return {
|
||||
name,
|
||||
group,
|
||||
loading,
|
||||
error
|
||||
};
|
||||
};
|
||||
|
||||
const mapDispatchToProps = dispatch => {
|
||||
return {
|
||||
fetchGroup: (name: string) => {
|
||||
dispatch(fetchGroup(name));
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(translate("groups")(SingleGroup));
|
||||
@@ -37,7 +37,6 @@ const GROUPS_URL = "groups";
|
||||
const CONTENT_TYPE_GROUP = "application/vnd.scmm-group+json;v=2";
|
||||
|
||||
// fetch groups
|
||||
|
||||
export function fetchGroups() {
|
||||
return fetchGroupsByLink(GROUPS_URL);
|
||||
}
|
||||
@@ -86,6 +85,54 @@ export function fetchGroupsFailure(url: string, error: Error): Action {
|
||||
};
|
||||
}
|
||||
|
||||
//fetch group
|
||||
export function fetchGroup(name: string) {
|
||||
const groupUrl = GROUPS_URL + "/" + name;
|
||||
return function(dispatch: any) {
|
||||
dispatch(fetchGroupPending(name));
|
||||
return apiClient
|
||||
.get(groupUrl)
|
||||
.then(response => {
|
||||
return response.json();
|
||||
})
|
||||
.then(data => {
|
||||
dispatch(fetchGroupSuccess(data));
|
||||
})
|
||||
.catch(cause => {
|
||||
const error = new Error(`could not fetch group: ${cause.message}`);
|
||||
dispatch(fetchGroupFailure(name, error));
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export function fetchGroupPending(name: string): Action {
|
||||
return {
|
||||
type: FETCH_GROUP_PENDING,
|
||||
payload: name,
|
||||
itemId: name
|
||||
};
|
||||
}
|
||||
|
||||
export function fetchGroupSuccess(group: any): Action {
|
||||
return {
|
||||
type: FETCH_GROUP_SUCCESS,
|
||||
payload: group,
|
||||
itemId: group.name
|
||||
};
|
||||
}
|
||||
|
||||
export function fetchGroupFailure(name: string, error: Error): Action {
|
||||
return {
|
||||
type: FETCH_GROUP_FAILURE,
|
||||
payload: {
|
||||
name,
|
||||
error
|
||||
},
|
||||
itemId: name
|
||||
};
|
||||
}
|
||||
|
||||
//create group
|
||||
export function createGroup(group: Group) {
|
||||
return function(dispatch: Dispatch) {
|
||||
dispatch(createGroupPending());
|
||||
@@ -121,6 +168,7 @@ export function createGroupFailure(error: Error) {
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
//reducer
|
||||
function extractGroupsByNames(
|
||||
groups: Groups[],
|
||||
@@ -179,7 +227,8 @@ function byNamesReducer(state: any = {}, action: any = {}) {
|
||||
return {
|
||||
...byNames
|
||||
};
|
||||
|
||||
case FETCH_GROUP_SUCCESS:
|
||||
return reducerByName(state, action.payload.name, action.payload);
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
@@ -254,3 +303,11 @@ export function getGroupByName(state: Object, name: string) {
|
||||
return state.groups.byNames[name];
|
||||
}
|
||||
}
|
||||
|
||||
export function isFetchGroupPending(state: Object, name: string) {
|
||||
return isPending(state, FETCH_GROUP, name);
|
||||
}
|
||||
|
||||
export function getFetchGroupFailure(state: Object, name: string) {
|
||||
return getFailure(state, FETCH_GROUP, name);
|
||||
}
|
||||
|
||||
@@ -15,6 +15,15 @@ import reducer, {
|
||||
getFetchGroupsFailure,
|
||||
isFetchGroupsPending,
|
||||
selectListAsCollection,
|
||||
fetchGroup,
|
||||
FETCH_GROUP_PENDING,
|
||||
FETCH_GROUP_SUCCESS,
|
||||
FETCH_GROUP_FAILURE,
|
||||
fetchGroupSuccess,
|
||||
getFetchGroupFailure,
|
||||
FETCH_GROUP,
|
||||
isFetchGroupPending,
|
||||
getGroupByName,
|
||||
createGroup,
|
||||
CREATE_GROUP_SUCCESS,
|
||||
CREATE_GROUP_PENDING,
|
||||
@@ -27,22 +36,22 @@ const GROUPS_URL = "/scm/api/rest/v2/groups";
|
||||
|
||||
const error = new Error("You have an error!");
|
||||
|
||||
const groupZaphod = {
|
||||
const humanGroup = {
|
||||
creationDate: "2018-07-31T08:39:07.860Z",
|
||||
description: "This is a group",
|
||||
name: "zaphodGroup",
|
||||
name: "humanGroup",
|
||||
type: "xml",
|
||||
properties: {},
|
||||
members: ["userZaphod"],
|
||||
_links: {
|
||||
self: {
|
||||
href: "http://localhost:3000/scm/api/rest/v2/groups/zaphodGroup"
|
||||
href: "http://localhost:3000/scm/api/rest/v2/groups/humanGroup"
|
||||
},
|
||||
delete: {
|
||||
href: "http://localhost:3000/scm/api/rest/v2/groups/zaphodGroup"
|
||||
href: "http://localhost:3000/scm/api/rest/v2/groups/humanGroup"
|
||||
},
|
||||
update: {
|
||||
href: "http://localhost:3000/scm/api/rest/v2/groups/zaphodGroup"
|
||||
href:"http://localhost:3000/scm/api/rest/v2/groups/humanGroup"
|
||||
}
|
||||
},
|
||||
_embedded: {
|
||||
@@ -59,22 +68,22 @@ const groupZaphod = {
|
||||
}
|
||||
};
|
||||
|
||||
const groupFord = {
|
||||
const emptyGroup = {
|
||||
creationDate: "2018-07-31T08:39:07.860Z",
|
||||
description: "This is a group",
|
||||
name: "fordGroup",
|
||||
name: "emptyGroup",
|
||||
type: "xml",
|
||||
properties: {},
|
||||
members: [],
|
||||
_links: {
|
||||
self: {
|
||||
href: "http://localhost:3000/scm/api/rest/v2/groups/fordGroup"
|
||||
href: "http://localhost:3000/scm/api/rest/v2/groups/emptyGroup"
|
||||
},
|
||||
delete: {
|
||||
href: "http://localhost:3000/scm/api/rest/v2/groups/fordGroup"
|
||||
href: "http://localhost:3000/scm/api/rest/v2/groups/emptyGroup"
|
||||
},
|
||||
update: {
|
||||
href: "http://localhost:3000/scm/api/rest/v2/groups/fordGroup"
|
||||
href:"http://localhost:3000/scm/api/rest/v2/groups/emptyGroup"
|
||||
}
|
||||
},
|
||||
_embedded: {
|
||||
@@ -100,7 +109,7 @@ const responseBody = {
|
||||
}
|
||||
},
|
||||
_embedded: {
|
||||
groups: [groupZaphod, groupFord]
|
||||
groups: [humanGroup, emptyGroup]
|
||||
}
|
||||
};
|
||||
|
||||
@@ -148,13 +157,40 @@ describe("groups fetch()", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("should sucessfully fetch single group", () => {
|
||||
fetchMock.getOnce(GROUPS_URL + "/humandGroup", humanGroup);
|
||||
|
||||
const store = mockStore({});
|
||||
return store.dispatch(fetchGroup("humandGroup")).then(() => {
|
||||
const actions = store.getActions();
|
||||
expect(actions[0].type).toEqual(FETCH_GROUP_PENDING);
|
||||
expect(actions[1].type).toEqual(FETCH_GROUP_SUCCESS);
|
||||
expect(actions[1].payload).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
it("should fail fetching single group on HTTP 500", () => {
|
||||
fetchMock.getOnce(GROUPS_URL + "/humandGroup", {
|
||||
status: 500
|
||||
});
|
||||
|
||||
const store = mockStore({});
|
||||
return store.dispatch(fetchGroup("humandGroup")).then(() => {
|
||||
const actions = store.getActions();
|
||||
expect(actions[0].type).toEqual(FETCH_GROUP_PENDING);
|
||||
expect(actions[1].type).toEqual(FETCH_GROUP_FAILURE);
|
||||
expect(actions[1].payload).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it("should successfully create group", () => {
|
||||
fetchMock.postOnce(GROUPS_URL, {
|
||||
status: 201
|
||||
});
|
||||
|
||||
const store = mockStore({});
|
||||
return store.dispatch(createGroup(groupZaphod)).then(() => {
|
||||
return store.dispatch(createGroup(humanGroup)).then(() => {
|
||||
const actions = store.getActions();
|
||||
expect(actions[0].type).toEqual(CREATE_GROUP_PENDING);
|
||||
expect(actions[1].type).toEqual(CREATE_GROUP_SUCCESS);
|
||||
@@ -167,7 +203,7 @@ describe("groups fetch()", () => {
|
||||
});
|
||||
|
||||
const store = mockStore({});
|
||||
return store.dispatch(createGroup(groupZaphod)).then(() => {
|
||||
return store.dispatch(createGroup(humanGroup)).then(() => {
|
||||
const actions = store.getActions();
|
||||
expect(actions[0].type).toEqual(CREATE_GROUP_PENDING);
|
||||
expect(actions[1].type).toEqual(CREATE_GROUP_FAILURE);
|
||||
@@ -178,11 +214,13 @@ describe("groups fetch()", () => {
|
||||
});
|
||||
|
||||
describe("groups reducer", () => {
|
||||
it("should update state correctly according to FETCH_USERS_SUCCESS action", () => {
|
||||
|
||||
it("should update state correctly according to FETCH_GROUPS_SUCCESS action", () => {
|
||||
|
||||
const newState = reducer({}, fetchGroupsSuccess(responseBody));
|
||||
|
||||
expect(newState.list).toEqual({
|
||||
entries: ["zaphodGroup", "fordGroup"],
|
||||
entries: ["humanGroup", "emptyGroup"],
|
||||
entry: {
|
||||
groupCreatePermission: true,
|
||||
page: 0,
|
||||
@@ -192,8 +230,8 @@ describe("groups reducer", () => {
|
||||
});
|
||||
|
||||
expect(newState.byNames).toEqual({
|
||||
zaphodGroup: groupZaphod,
|
||||
fordGroup: groupFord
|
||||
humanGroup: humanGroup,
|
||||
emptyGroup: emptyGroup
|
||||
});
|
||||
|
||||
expect(newState.list.entry.groupCreatePermission).toBeTruthy();
|
||||
@@ -205,26 +243,46 @@ describe("groups reducer", () => {
|
||||
expect(newState.list.entry.groupCreatePermission).toBeTruthy();
|
||||
});
|
||||
|
||||
it("should not replace whole byNames map when fetching users", () => {
|
||||
it("should not replace whole byNames map when fetching groups", () => {
|
||||
const oldState = {
|
||||
byNames: {
|
||||
fordGroup: groupFord
|
||||
emptyGroup: emptyGroup
|
||||
}
|
||||
};
|
||||
|
||||
const newState = reducer(oldState, fetchGroupsSuccess(responseBody));
|
||||
expect(newState.byNames["zaphodGroup"]).toBeDefined();
|
||||
expect(newState.byNames["fordGroup"]).toBeDefined();
|
||||
expect(newState.byNames["humanGroup"]).toBeDefined();
|
||||
expect(newState.byNames["emptyGroup"]).toBeDefined();
|
||||
});
|
||||
|
||||
it("should set userCreatePermission to true if create link is present", () => {
|
||||
it("should set groupCreatePermission to true if create link is present", () => {
|
||||
const newState = reducer({}, fetchGroupsSuccess(responseBody));
|
||||
|
||||
expect(newState.list.entry.groupCreatePermission).toBeTruthy();
|
||||
expect(newState.list.entries).toEqual(["zaphodGroup", "fordGroup"]);
|
||||
expect(newState.byNames["fordGroup"]).toBeTruthy();
|
||||
expect(newState.byNames["zaphodGroup"]).toBeTruthy();
|
||||
expect(newState.list.entries).toEqual(["humanGroup", "emptyGroup"]);
|
||||
expect(newState.byNames["emptyGroup"]).toBeTruthy();
|
||||
expect(newState.byNames["humanGroup"]).toBeTruthy();
|
||||
});
|
||||
|
||||
|
||||
it("should update state according to FETCH_GROUP_SUCCESS action", () => {
|
||||
const newState = reducer({}, fetchGroupSuccess(emptyGroup));
|
||||
expect(newState.byNames["emptyGroup"]).toBe(emptyGroup);
|
||||
});
|
||||
|
||||
it("should affect groups state nor the state of other groups", () => {
|
||||
const newState = reducer(
|
||||
{
|
||||
list: {
|
||||
entries: ["humanGroup"]
|
||||
}
|
||||
},
|
||||
fetchGroupSuccess(emptyGroup)
|
||||
);
|
||||
expect(newState.byNames["emptyGroup"]).toBe(emptyGroup);
|
||||
expect(newState.list.entries).toEqual(["humanGroup"]);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe("selector tests", () => {
|
||||
@@ -311,10 +369,47 @@ describe("selector tests", () => {
|
||||
expect(getFetchGroupsFailure(state)).toEqual(error);
|
||||
});
|
||||
|
||||
it("should return undefined when fetch users did not fail", () => {
|
||||
it("should return undefined when fetch groups did not fail", () => {
|
||||
expect(getFetchGroupsFailure({})).toBe(undefined);
|
||||
});
|
||||
|
||||
it("should return group emptyGroup", () => {
|
||||
const state = {
|
||||
groups: {
|
||||
byNames: {
|
||||
emptyGroup: emptyGroup
|
||||
}
|
||||
}
|
||||
};
|
||||
expect(getGroupByName(state, "emptyGroup")).toEqual(emptyGroup);
|
||||
});
|
||||
|
||||
it("should return true, when fetch group humandGroup is pending", () => {
|
||||
const state = {
|
||||
pending: {
|
||||
[FETCH_GROUP + "/humandGroup"]: true
|
||||
}
|
||||
};
|
||||
expect(isFetchGroupPending(state, "humandGroup")).toEqual(true);
|
||||
});
|
||||
|
||||
it("should return false, when fetch group humandGroup is not pending", () => {
|
||||
expect(isFetchGroupPending({}, "humandGroup")).toEqual(false);
|
||||
});
|
||||
|
||||
it("should return error when fetch group humandGroup did fail", () => {
|
||||
const state = {
|
||||
failure: {
|
||||
[FETCH_GROUP + "/humandGroup"]: error
|
||||
}
|
||||
};
|
||||
expect(getFetchGroupFailure(state, "humandGroup")).toEqual(error);
|
||||
});
|
||||
|
||||
it("should return undefined when fetch group humandGroup did not fail", () => {
|
||||
expect(getFetchGroupFailure({}, "humandGroup")).toBe(undefined);
|
||||
});
|
||||
|
||||
it("should return true if create group is pending", () => {
|
||||
expect(isCreateGroupPending({pending: {
|
||||
[CREATE_GROUP]: true
|
||||
@@ -336,4 +431,5 @@ describe("selector tests", () => {
|
||||
it("should return undefined if creating group did not fail", () => {
|
||||
expect(getCreateGroupFailure({})).toBeUndefined()
|
||||
})
|
||||
|
||||
});
|
||||
|
||||
@@ -8,7 +8,6 @@ export type Group = {
|
||||
description: string,
|
||||
lastModified: string,
|
||||
type: string,
|
||||
properties: [],
|
||||
members: string[],
|
||||
_links: Links,
|
||||
_embedded: {
|
||||
|
||||
Reference in New Issue
Block a user