mirror of
https://github.com/scm-manager/scm-manager.git
synced 2025-11-11 16:05:44 +01:00
show group table in ui
This commit is contained in:
10
scm-ui/public/locales/en/groups.json
Normal file
10
scm-ui/public/locales/en/groups.json
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"group": {
|
||||||
|
"name": "Name",
|
||||||
|
"description": "Description"
|
||||||
|
},
|
||||||
|
"groups": {
|
||||||
|
"title": "Groups",
|
||||||
|
"subtitle": "Create, read, update and delete groups"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@ import { createStore, compose, applyMiddleware, combineReducers } from "redux";
|
|||||||
import { routerReducer, routerMiddleware } from "react-router-redux";
|
import { routerReducer, routerMiddleware } from "react-router-redux";
|
||||||
|
|
||||||
import users from "./users/modules/users";
|
import users from "./users/modules/users";
|
||||||
|
import groups from "./groups/modules/groups";
|
||||||
import auth from "./modules/auth";
|
import auth from "./modules/auth";
|
||||||
import pending from "./modules/pending";
|
import pending from "./modules/pending";
|
||||||
import failure from "./modules/failure";
|
import failure from "./modules/failure";
|
||||||
@@ -20,6 +21,7 @@ function createReduxStore(history: BrowserHistory) {
|
|||||||
pending,
|
pending,
|
||||||
failure,
|
failure,
|
||||||
users,
|
users,
|
||||||
|
groups,
|
||||||
auth
|
auth
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
32
scm-ui/src/groups/components/table/Details.js
Normal file
32
scm-ui/src/groups/components/table/Details.js
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
//@flow
|
||||||
|
import React from "react";
|
||||||
|
import type { Group } from "../../types/Group";
|
||||||
|
import { translate } from "react-i18next";
|
||||||
|
import { Checkbox } from "../../../components/forms";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
group: Group,
|
||||||
|
t: string => string
|
||||||
|
};
|
||||||
|
|
||||||
|
class Details extends React.Component<Props> {
|
||||||
|
render() {
|
||||||
|
const { group, t } = this.props;
|
||||||
|
return (
|
||||||
|
<table className="table">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td>{t("group.name")}</td>
|
||||||
|
<td>{group.name}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>{t("group.description")}</td>
|
||||||
|
<td>{group.description}</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default translate("groups")(Details);
|
||||||
25
scm-ui/src/groups/components/table/GroupRow.js
Normal file
25
scm-ui/src/groups/components/table/GroupRow.js
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
// @flow
|
||||||
|
import React from "react";
|
||||||
|
import { Link } from "react-router-dom";
|
||||||
|
import type { Group } from "../../types/Group";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
group: Group
|
||||||
|
};
|
||||||
|
|
||||||
|
export default class GroupRow extends React.Component<Props> {
|
||||||
|
renderLink(to: string, label: string) {
|
||||||
|
return <Link to={to}>{label}</Link>;
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { group } = this.props;
|
||||||
|
const to = `/group/${group.name}`;
|
||||||
|
return (
|
||||||
|
<tr>
|
||||||
|
<td className="is-hidden-mobile">{this.renderLink(to, group.name)}</td>
|
||||||
|
<td>{this.renderLink(to, group.description)}</td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
33
scm-ui/src/groups/components/table/GroupTable.js
Normal file
33
scm-ui/src/groups/components/table/GroupTable.js
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
// @flow
|
||||||
|
import React from "react";
|
||||||
|
import { translate } from "react-i18next";
|
||||||
|
import GroupRow from "./GroupRow";
|
||||||
|
import type { Group } from "../../types/Group";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
t: string => string,
|
||||||
|
groups: Group[]
|
||||||
|
};
|
||||||
|
|
||||||
|
class GroupTable extends React.Component<Props> {
|
||||||
|
render() {
|
||||||
|
const { groups, t } = this.props;
|
||||||
|
return (
|
||||||
|
<table className="table is-hoverable is-fullwidth">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th className="is-hidden-mobile">{t("group.name")}</th>
|
||||||
|
<th>{t("group.description")}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{groups.map((group, index) => {
|
||||||
|
return <GroupRow key={index} group={group} />;
|
||||||
|
})}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default translate("groups")(GroupTable);
|
||||||
3
scm-ui/src/groups/components/table/index.js
Normal file
3
scm-ui/src/groups/components/table/index.js
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export { default as Details } from "./Details";
|
||||||
|
export { default as GroupRow } from "./GroupRow";
|
||||||
|
export { default as GroupTable } from "./GroupTable";
|
||||||
@@ -1,24 +1,139 @@
|
|||||||
//@flow
|
//@flow
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
|
import { translate } from "react-i18next";
|
||||||
|
import type { Group } from "../types/Group.js";
|
||||||
|
import type { PagedCollection } from "../../types/Collection";
|
||||||
|
import type { History } from "history";
|
||||||
|
import { Page } from "../../components/layout";
|
||||||
|
import { GroupTable } from "./../components/table";
|
||||||
|
import Paginator from "../../components/Paginator";
|
||||||
|
|
||||||
type Props = {};
|
import {
|
||||||
|
fetchGroupsByPage,
|
||||||
|
fetchGroupsByLink,
|
||||||
|
getGroupsFromState,
|
||||||
|
isFetchGroupsPending,
|
||||||
|
getFetchGroupsFailure,
|
||||||
|
isPermittedToCreateGroups,
|
||||||
|
selectListAsCollection
|
||||||
|
} from "../modules/groups";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
groups: Group[],
|
||||||
|
loading: boolean,
|
||||||
|
error: Error,
|
||||||
|
canAddGroups: boolean,
|
||||||
|
list: PagedCollection,
|
||||||
|
page: number,
|
||||||
|
|
||||||
|
// context objects
|
||||||
|
t: string => string,
|
||||||
|
history: History,
|
||||||
|
|
||||||
|
// dispatch functions
|
||||||
|
fetchGroupsByPage: (page: number) => void,
|
||||||
|
fetchGroupsByLink: (link: string) => void
|
||||||
|
};
|
||||||
|
|
||||||
class Groups extends React.Component<Props> {
|
class Groups extends React.Component<Props> {
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
this.props.fetchGroupsByPage(this.props.page);
|
||||||
|
}
|
||||||
|
|
||||||
|
onPageChange = (link: string) => {
|
||||||
|
this.props.fetchGroupsByLink(link);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* reflect page transitions in the uri
|
||||||
|
*/
|
||||||
|
componentDidUpdate = (prevProps: Props) => {
|
||||||
|
const { page, list } = this.props;
|
||||||
|
if (list.page) {
|
||||||
|
// backend starts paging by 0
|
||||||
|
const statePage: number = list.page + 1;
|
||||||
|
if (page !== statePage) {
|
||||||
|
this.props.history.push(`/groups/${statePage}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return "groups will be displayed here";
|
const { groups, loading, error, t } = this.props;
|
||||||
|
return (
|
||||||
|
<Page
|
||||||
|
title={t("groups.title")}
|
||||||
|
subtitle={t("groups.subtitle")}
|
||||||
|
loading={loading || !groups}
|
||||||
|
error={error}
|
||||||
|
>
|
||||||
|
<GroupTable groups={groups} />
|
||||||
|
{this.renderPaginator()}
|
||||||
|
{this.renderCreateButton()}
|
||||||
|
</Page>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
renderPaginator() {
|
||||||
|
const { list } = this.props;
|
||||||
|
if (list) {
|
||||||
|
return <Paginator collection={list} onPageChange={this.onPageChange} />;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
renderCreateButton() {
|
||||||
|
/* if (this.props.canAddGroups) {
|
||||||
|
return <CreateGroupButton />;
|
||||||
|
} else {
|
||||||
|
return;
|
||||||
|
}*/
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const mapStateToProps = state => {
|
const getPageFromProps = props => {
|
||||||
return {};
|
let page = props.match.params.page;
|
||||||
|
if (page) {
|
||||||
|
page = parseInt(page, 10);
|
||||||
|
} else {
|
||||||
|
page = 1;
|
||||||
|
}
|
||||||
|
return page;
|
||||||
|
};
|
||||||
|
|
||||||
|
const mapStateToProps = (state, ownProps) => {
|
||||||
|
const groups = getGroupsFromState(state);
|
||||||
|
const loading = isFetchGroupsPending(state);
|
||||||
|
const error = getFetchGroupsFailure(state);
|
||||||
|
|
||||||
|
const page = getPageFromProps(ownProps);
|
||||||
|
const canAddGroups = isPermittedToCreateGroups(state);
|
||||||
|
const list = selectListAsCollection(state);
|
||||||
|
|
||||||
|
return {
|
||||||
|
groups,
|
||||||
|
loading,
|
||||||
|
error,
|
||||||
|
canAddGroups,
|
||||||
|
list,
|
||||||
|
page
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const mapDispatchToProps = (dispatch) => {
|
const mapDispatchToProps = (dispatch) => {
|
||||||
return {};
|
return {
|
||||||
|
fetchGroupsByPage: (page: number) => {
|
||||||
|
dispatch(fetchGroupsByPage(page));
|
||||||
|
},
|
||||||
|
fetchGroupsByLink: (link: string) => {
|
||||||
|
dispatch(fetchGroupsByLink(link));
|
||||||
|
}
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export default connect(
|
export default connect(
|
||||||
mapStateToProps,
|
mapStateToProps,
|
||||||
mapDispatchToProps
|
mapDispatchToProps
|
||||||
)(Groups);
|
)(translate("groups")(Groups));
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import * as types from "../../modules/types";
|
|||||||
import { combineReducers, Dispatch } from "redux";
|
import { combineReducers, Dispatch } from "redux";
|
||||||
import type { Action } from "../../types/Action";
|
import type { Action } from "../../types/Action";
|
||||||
import type { PagedCollection } from "../../types/Collection";
|
import type { PagedCollection } from "../../types/Collection";
|
||||||
|
import type {Groups} from "../types/Groups";
|
||||||
|
|
||||||
export const FETCH_GROUPS = "scm/groups/FETCH_GROUPS";
|
export const FETCH_GROUPS = "scm/groups/FETCH_GROUPS";
|
||||||
export const FETCH_GROUPS_PENDING = `${FETCH_GROUPS}_${types.PENDING_SUFFIX}`;
|
export const FETCH_GROUPS_PENDING = `${FETCH_GROUPS}_${types.PENDING_SUFFIX}`;
|
||||||
@@ -154,3 +155,69 @@ export default combineReducers({
|
|||||||
list: listReducer,
|
list: listReducer,
|
||||||
byNames: byNamesReducer
|
byNames: byNamesReducer
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
// selectors
|
||||||
|
|
||||||
|
const selectList = (state: Object) => {
|
||||||
|
if (state.groups && state.groups.list) {
|
||||||
|
return state.groups.list;
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
};
|
||||||
|
|
||||||
|
const selectListEntry = (state: Object): Object => {
|
||||||
|
const list = selectList(state);
|
||||||
|
if (list.entry) {
|
||||||
|
return list.entry;
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const selectListAsCollection = (state: Object): PagedCollection => {
|
||||||
|
return selectListEntry(state);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const isPermittedToCreateGroups = (state: Object): boolean => {
|
||||||
|
const permission = selectListEntry(state).groupCreatePermission;
|
||||||
|
if (permission) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function getGroupsFromState(state: Object) {
|
||||||
|
const groupNames = selectList(state).entries;
|
||||||
|
if (!groupNames) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const groupEntries: Group[] = [];
|
||||||
|
|
||||||
|
for (let groupName of groupNames) {
|
||||||
|
groupEntries.push(state.groups.byNames[groupName]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return groupEntries;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isFetchGroupsPending(state: Object) {
|
||||||
|
return isPending(state, FETCH_GROUPS);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getFetchGroupsFailure(state: Object) {
|
||||||
|
return getFailure(state, FETCH_GROUPS);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isCreateGroupPending(state: Object) {
|
||||||
|
return isPending(state, CREATE_GROUP);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getCreateGroupFailure(state: Object) {
|
||||||
|
return getFailure(state, CREATE_GROUP);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getGroupByName(state: Object, name: string) {
|
||||||
|
if (state.groups && state.groups.byNames) {
|
||||||
|
return state.groups.byNames[name];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -5,13 +5,23 @@ import fetchMock from "fetch-mock";
|
|||||||
|
|
||||||
import reducer, {
|
import reducer, {
|
||||||
fetchGroups,
|
fetchGroups,
|
||||||
|
FETCH_GROUPS,
|
||||||
FETCH_GROUPS_PENDING,
|
FETCH_GROUPS_PENDING,
|
||||||
FETCH_GROUPS_SUCCESS,
|
FETCH_GROUPS_SUCCESS,
|
||||||
FETCH_GROUPS_FAILURE,
|
FETCH_GROUPS_FAILURE,
|
||||||
fetchGroupsSuccess
|
isFetchUsersPending,
|
||||||
|
fetchGroupsSuccess,
|
||||||
|
isPermittedToCreateGroups,
|
||||||
|
getGroupsFromState,
|
||||||
|
getFetchGroupsFailure,
|
||||||
|
isFetchGroupsPending,
|
||||||
|
selectListAsCollection
|
||||||
} from "./groups"
|
} from "./groups"
|
||||||
const GROUPS_URL = "/scm/api/rest/v2/groups";
|
const GROUPS_URL = "/scm/api/rest/v2/groups";
|
||||||
|
|
||||||
|
const error = new Error("You have an error!");
|
||||||
|
|
||||||
|
|
||||||
const groupZaphod = {
|
const groupZaphod = {
|
||||||
creationDate: "2018-07-31T08:39:07.860Z",
|
creationDate: "2018-07-31T08:39:07.860Z",
|
||||||
description: "This is a group",
|
description: "This is a group",
|
||||||
@@ -182,3 +192,95 @@ describe("groups reducer", () => {
|
|||||||
expect(newState.byNames["zaphodGroup"]).toBeTruthy();
|
expect(newState.byNames["zaphodGroup"]).toBeTruthy();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
describe("selector tests", () => {
|
||||||
|
|
||||||
|
it("should return an empty object", () => {
|
||||||
|
expect(selectListAsCollection({})).toEqual({});
|
||||||
|
expect(selectListAsCollection({ groups: { a: "a" } })).toEqual({});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return a state slice collection", () => {
|
||||||
|
const collection = {
|
||||||
|
page: 3,
|
||||||
|
totalPages: 42
|
||||||
|
};
|
||||||
|
|
||||||
|
const state = {
|
||||||
|
groups: {
|
||||||
|
list: {
|
||||||
|
entry: collection
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
expect(selectListAsCollection(state)).toBe(collection);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return false", () => {
|
||||||
|
expect(isPermittedToCreateGroups({})).toBe(false);
|
||||||
|
expect(isPermittedToCreateGroups({ groups: { list: { entry: {} } } })).toBe(
|
||||||
|
false
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
isPermittedToCreateGroups({
|
||||||
|
groups: { list: { entry: { groupCreatePermission: false } } }
|
||||||
|
})
|
||||||
|
).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return true", () => {
|
||||||
|
const state = {
|
||||||
|
groups: {
|
||||||
|
list: {
|
||||||
|
entry: {
|
||||||
|
groupCreatePermission: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
expect(isPermittedToCreateGroups(state)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should get groups from state", () => {
|
||||||
|
const state = {
|
||||||
|
groups: {
|
||||||
|
list: {
|
||||||
|
entries: ["a", "b"]
|
||||||
|
},
|
||||||
|
byNames: {
|
||||||
|
a: { name: "a" },
|
||||||
|
b: { name: "b" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
expect(getGroupsFromState(state)).toEqual([{ name: "a" }, { name: "b" }]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return true, when fetch groups is pending", () => {
|
||||||
|
const state = {
|
||||||
|
pending: {
|
||||||
|
[FETCH_GROUPS]: true
|
||||||
|
}
|
||||||
|
};
|
||||||
|
expect(isFetchGroupsPending(state)).toEqual(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return false, when fetch groups is not pending", () => {
|
||||||
|
expect(isFetchGroupsPending({})).toEqual(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return error when fetch groups did fail", () => {
|
||||||
|
const state = {
|
||||||
|
failure: {
|
||||||
|
[FETCH_GROUPS]: error
|
||||||
|
}
|
||||||
|
};
|
||||||
|
expect(getFetchGroupsFailure(state)).toEqual(error);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return undefined when fetch users did not fail", () => {
|
||||||
|
expect(getFetchGroupsFailure({})).toBe(undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|||||||
17
scm-ui/src/groups/types/Group.js
Normal file
17
scm-ui/src/groups/types/Group.js
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
//@flow
|
||||||
|
import type { Links } from "../../types/hal";
|
||||||
|
import type { User } from "../../users/types/User";
|
||||||
|
|
||||||
|
export type Group = {
|
||||||
|
name: string,
|
||||||
|
creationDate: string,
|
||||||
|
description: string,
|
||||||
|
lastModified: string,
|
||||||
|
type: string,
|
||||||
|
properties: [],
|
||||||
|
members: string[],
|
||||||
|
_links: Links,
|
||||||
|
_embedded: {
|
||||||
|
members: User[]
|
||||||
|
}
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user