mirror of
https://github.com/scm-manager/scm-manager.git
synced 2025-11-08 14:35:45 +01:00
Merged 2.0.0-m3
This commit is contained in:
@@ -104,7 +104,7 @@ public final class DiffCommandBuilder
|
||||
*
|
||||
* @throws IOException
|
||||
*/
|
||||
public DiffCommandBuilder retriveContent(OutputStream outputStream) throws IOException, RevisionNotFoundException {
|
||||
public DiffCommandBuilder retrieveContent(OutputStream outputStream) throws IOException, RevisionNotFoundException {
|
||||
getDiffResult(outputStream);
|
||||
|
||||
return this;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
//@flow
|
||||
import React from "react";
|
||||
import type { Me } from "@scm-manager/ui-types";
|
||||
import {Link} from "react-router-dom";
|
||||
|
||||
type Props = {
|
||||
me?: Me
|
||||
@@ -15,7 +16,7 @@ class Footer extends React.Component<Props> {
|
||||
return (
|
||||
<footer className="footer">
|
||||
<div className="container is-centered">
|
||||
<p className="has-text-centered">{me.displayName}</p>
|
||||
<p className="has-text-centered"><Link to={"/me"}>{me.displayName}</Link></p>
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
|
||||
@@ -2,5 +2,6 @@
|
||||
|
||||
export type Me = {
|
||||
name: string,
|
||||
displayName: string
|
||||
displayName: string,
|
||||
mail: string
|
||||
};
|
||||
|
||||
@@ -38,5 +38,14 @@
|
||||
"paginator": {
|
||||
"next": "Next",
|
||||
"previous": "Previous"
|
||||
},
|
||||
"profile": {
|
||||
"actions-label": "Actions",
|
||||
"username": "Username",
|
||||
"displayName": "Display Name",
|
||||
"mail": "E-Mail",
|
||||
"change-password": "Change password",
|
||||
"error-title": "Error",
|
||||
"error-subtitle": "Cannot display profile"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,6 +29,9 @@
|
||||
"edit-user-button": {
|
||||
"label": "Edit"
|
||||
},
|
||||
"set-password-button": {
|
||||
"label": "Set password"
|
||||
},
|
||||
"user-form": {
|
||||
"submit": "Submit"
|
||||
},
|
||||
@@ -49,8 +52,11 @@
|
||||
"name-invalid": "This name is invalid",
|
||||
"displayname-invalid": "This displayname is invalid",
|
||||
"password-invalid": "Password has to be between 6 and 32 characters",
|
||||
"passwordValidation-invalid": "Passwords have to be the same",
|
||||
"validatePassword": "Please validate password here"
|
||||
"passwordValidation-invalid": "Passwords have to be identical",
|
||||
"validatePassword": "Confirm password"
|
||||
},
|
||||
"password": {
|
||||
"set-password-successful": "Password successfully set"
|
||||
},
|
||||
"help": {
|
||||
"usernameHelpText": "Unique name of the user.",
|
||||
|
||||
@@ -19,6 +19,7 @@ import SingleGroup from "../groups/containers/SingleGroup";
|
||||
import AddGroup from "../groups/containers/AddGroup";
|
||||
|
||||
import Config from "../config/containers/Config";
|
||||
import Profile from "./Profile";
|
||||
|
||||
type Props = {
|
||||
authenticated?: boolean
|
||||
@@ -106,6 +107,12 @@ class Main extends React.Component<Props> {
|
||||
component={Config}
|
||||
authenticated={authenticated}
|
||||
/>
|
||||
<ProtectedRoute
|
||||
exact
|
||||
path="/me"
|
||||
component={Profile}
|
||||
authenticated={authenticated}
|
||||
/>
|
||||
</Switch>
|
||||
</div>
|
||||
);
|
||||
|
||||
98
scm-ui/src/containers/Profile.js
Normal file
98
scm-ui/src/containers/Profile.js
Normal file
@@ -0,0 +1,98 @@
|
||||
// @flow
|
||||
|
||||
import React from "react";
|
||||
|
||||
import {
|
||||
Page,
|
||||
Navigation,
|
||||
Section,
|
||||
MailLink
|
||||
} from "../../../scm-ui-components/packages/ui-components/src/index";
|
||||
import { NavLink } from "react-router-dom";
|
||||
import { getMe } from "../modules/auth";
|
||||
import { compose } from "redux";
|
||||
import { connect } from "react-redux";
|
||||
import { translate } from "react-i18next";
|
||||
import type { Me } from "../../../scm-ui-components/packages/ui-types/src/index";
|
||||
import AvatarWrapper from "../repos/components/changesets/AvatarWrapper";
|
||||
import { ErrorPage } from "@scm-manager/ui-components";
|
||||
|
||||
type Props = {
|
||||
me: Me,
|
||||
|
||||
// Context props
|
||||
t: string => string
|
||||
};
|
||||
type State = {};
|
||||
|
||||
class Profile extends React.Component<Props, State> {
|
||||
render() {
|
||||
const { me, t } = this.props;
|
||||
|
||||
if (me) {
|
||||
return (
|
||||
<ErrorPage
|
||||
title={t("profile.error-title")}
|
||||
subtitle={t("profile.error-subtitle")}
|
||||
error={{ name: "Error", message: "'me' is undefined" }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Page title={me.displayName}>
|
||||
<div className="columns">
|
||||
<AvatarWrapper>
|
||||
<div>
|
||||
<figure className="media-left">
|
||||
<p className="image is-64x64">
|
||||
{
|
||||
// TODO: add avatar
|
||||
}
|
||||
</p>
|
||||
</figure>
|
||||
</div>
|
||||
</AvatarWrapper>
|
||||
<div className="column is-two-quarters">
|
||||
<table className="table">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>{t("profile.username")}</td>
|
||||
<td>{me.name}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{t("profile.displayName")}</td>
|
||||
<td>{me.displayName}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{t("profile.mail")}</td>
|
||||
<td>
|
||||
<MailLink address={me.mail} />
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div className="column is-one-quarter">
|
||||
<Navigation>
|
||||
<Section label={t("profile.actions-label")} />
|
||||
<NavLink to={"me/password"}>
|
||||
{t("profile.change-password")}
|
||||
</NavLink>
|
||||
</Navigation>
|
||||
</div>
|
||||
</div>
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const mapStateToProps = state => {
|
||||
return {
|
||||
me: getMe(state)
|
||||
};
|
||||
};
|
||||
|
||||
export default compose(
|
||||
translate("commons"),
|
||||
connect(mapStateToProps)
|
||||
)(Profile);
|
||||
@@ -5,7 +5,6 @@ import GroupForm from "../components/GroupForm";
|
||||
import {
|
||||
modifyGroup,
|
||||
modifyGroupReset,
|
||||
fetchGroup,
|
||||
isModifyGroupPending,
|
||||
getModifyGroupFailure
|
||||
} from "../modules/groups";
|
||||
@@ -29,13 +28,13 @@ class EditGroup extends React.Component<Props> {
|
||||
const { group, modifyGroupReset } = this.props;
|
||||
modifyGroupReset(group);
|
||||
}
|
||||
groupModified = () => () => {
|
||||
const { group, history } = this.props;
|
||||
history.push(`/group/${group.name}`);
|
||||
|
||||
groupModified = (group: Group) => () => {
|
||||
this.props.history.push(`/group/${group.name}`);
|
||||
};
|
||||
|
||||
modifyGroup = (group: Group) => {
|
||||
this.props.modifyGroup(group, this.groupModified());
|
||||
this.props.modifyGroup(group, this.groupModified(group));
|
||||
};
|
||||
|
||||
render() {
|
||||
|
||||
@@ -16,7 +16,7 @@ import type { Group } from "@scm-manager/ui-types";
|
||||
import type { History } from "history";
|
||||
import {
|
||||
deleteGroup,
|
||||
fetchGroup,
|
||||
fetchGroupByName,
|
||||
getGroupByName,
|
||||
isFetchGroupPending,
|
||||
getFetchGroupFailure,
|
||||
@@ -37,7 +37,7 @@ type Props = {
|
||||
|
||||
// dispatcher functions
|
||||
deleteGroup: (group: Group, callback?: () => void) => void,
|
||||
fetchGroup: (string, string) => void,
|
||||
fetchGroupByName: (string, string) => void,
|
||||
|
||||
// context objects
|
||||
t: string => string,
|
||||
@@ -47,7 +47,7 @@ type Props = {
|
||||
|
||||
class SingleGroup extends React.Component<Props> {
|
||||
componentDidMount() {
|
||||
this.props.fetchGroup(this.props.groupLink, this.props.name);
|
||||
this.props.fetchGroupByName(this.props.groupLink, this.props.name);
|
||||
}
|
||||
|
||||
stripEndingSlash = (url: string) => {
|
||||
@@ -147,8 +147,8 @@ const mapStateToProps = (state, ownProps) => {
|
||||
|
||||
const mapDispatchToProps = dispatch => {
|
||||
return {
|
||||
fetchGroup: (link: string, name: string) => {
|
||||
dispatch(fetchGroup(link, name));
|
||||
fetchGroupByName: (link: string, name: string) => {
|
||||
dispatch(fetchGroupByName(link, name));
|
||||
},
|
||||
deleteGroup: (group: Group, callback?: () => void) => {
|
||||
dispatch(deleteGroup(group, callback));
|
||||
|
||||
@@ -85,12 +85,20 @@ export function fetchGroupsFailure(url: string, error: Error): Action {
|
||||
}
|
||||
|
||||
//fetch group
|
||||
export function fetchGroup(link: string, name: string) {
|
||||
export function fetchGroupByLink(group: Group) {
|
||||
return fetchGroup(group._links.self.href, group.name);
|
||||
}
|
||||
|
||||
export function fetchGroupByName(link: string, name: string) {
|
||||
const groupUrl = link.endsWith("/") ? link + name : link + "/" + name;
|
||||
return fetchGroup(groupUrl, name);
|
||||
}
|
||||
|
||||
function fetchGroup(link: string, name: string) {
|
||||
return function(dispatch: any) {
|
||||
dispatch(fetchGroupPending(name));
|
||||
return apiClient
|
||||
.get(groupUrl)
|
||||
.get(link)
|
||||
.then(response => {
|
||||
return response.json();
|
||||
})
|
||||
@@ -190,6 +198,9 @@ export function modifyGroup(group: Group, callback?: () => void) {
|
||||
callback();
|
||||
}
|
||||
})
|
||||
.then(() => {
|
||||
dispatch(fetchGroupByLink(group));
|
||||
})
|
||||
.catch(cause => {
|
||||
dispatch(
|
||||
modifyGroupFailure(
|
||||
@@ -369,8 +380,6 @@ function byNamesReducer(state: any = {}, action: any = {}) {
|
||||
};
|
||||
case FETCH_GROUP_SUCCESS:
|
||||
return reducerByName(state, action.payload.name, action.payload);
|
||||
case MODIFY_GROUP_SUCCESS:
|
||||
return reducerByName(state, action.payload.name, action.payload);
|
||||
case DELETE_GROUP_SUCCESS:
|
||||
const newGroupByNames = deleteGroupInGroupsByNames(
|
||||
state,
|
||||
|
||||
@@ -15,7 +15,8 @@ import reducer, {
|
||||
getFetchGroupsFailure,
|
||||
isFetchGroupsPending,
|
||||
selectListAsCollection,
|
||||
fetchGroup,
|
||||
fetchGroupByLink,
|
||||
fetchGroupByName,
|
||||
FETCH_GROUP_PENDING,
|
||||
FETCH_GROUP_SUCCESS,
|
||||
FETCH_GROUP_FAILURE,
|
||||
@@ -46,6 +47,7 @@ import reducer, {
|
||||
getCreateGroupLink
|
||||
} from "./groups";
|
||||
const GROUPS_URL = "/api/v2/groups";
|
||||
const URL_HUMAN_GROUP = "http://localhost:8081/api/v2/groups/humanGroup";
|
||||
const URL = "/groups";
|
||||
|
||||
const error = new Error("You have an error!");
|
||||
@@ -59,13 +61,13 @@ const humanGroup = {
|
||||
members: ["userZaphod"],
|
||||
_links: {
|
||||
self: {
|
||||
href: "http://localhost:8081/api/v2/groups/humanGroup"
|
||||
href: URL_HUMAN_GROUP
|
||||
},
|
||||
delete: {
|
||||
href: "http://localhost:8081/api/v2/groups/humanGroup"
|
||||
href: URL_HUMAN_GROUP
|
||||
},
|
||||
update: {
|
||||
href: "http://localhost:8081/api/v2/groups/humanGroup"
|
||||
href: URL_HUMAN_GROUP
|
||||
}
|
||||
},
|
||||
_embedded: {
|
||||
@@ -171,11 +173,37 @@ describe("groups fetch()", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("should sucessfully fetch single group", () => {
|
||||
it("should sucessfully fetch single group by name", () => {
|
||||
fetchMock.getOnce(GROUPS_URL + "/humanGroup", humanGroup);
|
||||
|
||||
const store = mockStore({});
|
||||
return store.dispatch(fetchGroup(URL, "humanGroup")).then(() => {
|
||||
return store.dispatch(fetchGroupByName(URL, "humanGroup")).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 by name on HTTP 500", () => {
|
||||
fetchMock.getOnce(GROUPS_URL + "/humanGroup", {
|
||||
status: 500
|
||||
});
|
||||
|
||||
const store = mockStore({});
|
||||
return store.dispatch(fetchGroupByName(URL, "humanGroup")).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 sucessfully fetch single group", () => {
|
||||
fetchMock.getOnce(URL_HUMAN_GROUP, humanGroup);
|
||||
|
||||
const store = mockStore({});
|
||||
return store.dispatch(fetchGroupByLink(humanGroup)).then(() => {
|
||||
const actions = store.getActions();
|
||||
expect(actions[0].type).toEqual(FETCH_GROUP_PENDING);
|
||||
expect(actions[1].type).toEqual(FETCH_GROUP_SUCCESS);
|
||||
@@ -184,12 +212,12 @@ describe("groups fetch()", () => {
|
||||
});
|
||||
|
||||
it("should fail fetching single group on HTTP 500", () => {
|
||||
fetchMock.getOnce(GROUPS_URL + "/humanGroup", {
|
||||
fetchMock.getOnce(URL_HUMAN_GROUP, {
|
||||
status: 500
|
||||
});
|
||||
|
||||
const store = mockStore({});
|
||||
return store.dispatch(fetchGroup(URL, "humanGroup")).then(() => {
|
||||
return store.dispatch(fetchGroupByLink(humanGroup)).then(() => {
|
||||
const actions = store.getActions();
|
||||
expect(actions[0].type).toEqual(FETCH_GROUP_PENDING);
|
||||
expect(actions[1].type).toEqual(FETCH_GROUP_FAILURE);
|
||||
@@ -244,9 +272,10 @@ describe("groups fetch()", () => {
|
||||
});
|
||||
|
||||
it("should successfully modify group", () => {
|
||||
fetchMock.putOnce("http://localhost:8081/api/v2/groups/humanGroup", {
|
||||
fetchMock.putOnce(URL_HUMAN_GROUP, {
|
||||
status: 204
|
||||
});
|
||||
fetchMock.getOnce(URL_HUMAN_GROUP, humanGroup);
|
||||
|
||||
const store = mockStore({});
|
||||
|
||||
@@ -254,14 +283,16 @@ describe("groups fetch()", () => {
|
||||
const actions = store.getActions();
|
||||
expect(actions[0].type).toEqual(MODIFY_GROUP_PENDING);
|
||||
expect(actions[1].type).toEqual(MODIFY_GROUP_SUCCESS);
|
||||
expect(actions[2].type).toEqual(FETCH_GROUP_PENDING);
|
||||
expect(actions[1].payload).toEqual(humanGroup);
|
||||
});
|
||||
});
|
||||
|
||||
it("should call the callback after modifying group", () => {
|
||||
fetchMock.putOnce("http://localhost:8081/api/v2/groups/humanGroup", {
|
||||
fetchMock.putOnce(URL_HUMAN_GROUP, {
|
||||
status: 204
|
||||
});
|
||||
fetchMock.getOnce(URL_HUMAN_GROUP, humanGroup);
|
||||
|
||||
let called = false;
|
||||
const callback = () => {
|
||||
@@ -273,12 +304,13 @@ describe("groups fetch()", () => {
|
||||
const actions = store.getActions();
|
||||
expect(actions[0].type).toEqual(MODIFY_GROUP_PENDING);
|
||||
expect(actions[1].type).toEqual(MODIFY_GROUP_SUCCESS);
|
||||
expect(actions[2].type).toEqual(FETCH_GROUP_PENDING);
|
||||
expect(called).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
it("should fail modifying group on HTTP 500", () => {
|
||||
fetchMock.putOnce("http://localhost:8081/api/v2/groups/humanGroup", {
|
||||
fetchMock.putOnce(URL_HUMAN_GROUP, {
|
||||
status: 500
|
||||
});
|
||||
|
||||
@@ -293,7 +325,7 @@ describe("groups fetch()", () => {
|
||||
});
|
||||
|
||||
it("should delete successfully group humanGroup", () => {
|
||||
fetchMock.deleteOnce("http://localhost:8081/api/v2/groups/humanGroup", {
|
||||
fetchMock.deleteOnce(URL_HUMAN_GROUP, {
|
||||
status: 204
|
||||
});
|
||||
|
||||
@@ -308,7 +340,7 @@ describe("groups fetch()", () => {
|
||||
});
|
||||
|
||||
it("should call the callback, after successful delete", () => {
|
||||
fetchMock.deleteOnce("http://localhost:8081/api/v2/groups/humanGroup", {
|
||||
fetchMock.deleteOnce(URL_HUMAN_GROUP, {
|
||||
status: 204
|
||||
});
|
||||
|
||||
@@ -324,7 +356,7 @@ describe("groups fetch()", () => {
|
||||
});
|
||||
|
||||
it("should fail to delete group humanGroup", () => {
|
||||
fetchMock.deleteOnce("http://localhost:8081/api/v2/groups/humanGroup", {
|
||||
fetchMock.deleteOnce(URL_HUMAN_GROUP, {
|
||||
status: 500
|
||||
});
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// @flow
|
||||
import type { Me } from "@scm-manager/ui-components";
|
||||
import type { Me } from "@scm-manager/ui-types";
|
||||
import * as types from "./types";
|
||||
|
||||
import { apiClient, UNAUTHORIZED_ERROR } from "@scm-manager/ui-components";
|
||||
@@ -136,7 +136,11 @@ const callFetchMe = (link: string): Promise<Me> => {
|
||||
return response.json();
|
||||
})
|
||||
.then(json => {
|
||||
return { name: json.name, displayName: json.displayName };
|
||||
return {
|
||||
name: json.name,
|
||||
displayName: json.displayName,
|
||||
mail: json.mail
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -37,7 +37,11 @@ import {
|
||||
FETCH_INDEXRESOURCES_SUCCESS
|
||||
} from "./indexResource";
|
||||
|
||||
const me = { name: "tricia", displayName: "Tricia McMillian" };
|
||||
const me = {
|
||||
name: "tricia",
|
||||
displayName: "Tricia McMillian",
|
||||
mail: "trillian@heartofgold.universe"
|
||||
};
|
||||
|
||||
describe("auth reducer", () => {
|
||||
it("should set me and login on successful fetch of me", () => {
|
||||
|
||||
@@ -1,19 +1,32 @@
|
||||
//@flow
|
||||
import React from "react";
|
||||
import {deleteRepo, fetchRepo, getFetchRepoFailure, getRepository, isFetchRepoPending} from "../modules/repos";
|
||||
import {
|
||||
deleteRepo,
|
||||
fetchRepoByName,
|
||||
getFetchRepoFailure,
|
||||
getRepository,
|
||||
isFetchRepoPending
|
||||
} from "../modules/repos";
|
||||
|
||||
import {connect} from "react-redux";
|
||||
import {Route, Switch} from "react-router-dom";
|
||||
import type {Repository} from "@scm-manager/ui-types";
|
||||
import { connect } from "react-redux";
|
||||
import { Route, Switch } from "react-router-dom";
|
||||
import type { Repository } from "@scm-manager/ui-types";
|
||||
|
||||
import {ErrorPage, Loading, Navigation, NavLink, Page, Section} from "@scm-manager/ui-components";
|
||||
import {translate} from "react-i18next";
|
||||
import {
|
||||
ErrorPage,
|
||||
Loading,
|
||||
Navigation,
|
||||
NavLink,
|
||||
Page,
|
||||
Section
|
||||
} from "@scm-manager/ui-components";
|
||||
import { translate } from "react-i18next";
|
||||
import RepositoryDetails from "../components/RepositoryDetails";
|
||||
import DeleteNavAction from "../components/DeleteNavAction";
|
||||
import Edit from "../containers/Edit";
|
||||
import Permissions from "../permissions/containers/Permissions";
|
||||
|
||||
import type {History} from "history";
|
||||
import type { History } from "history";
|
||||
import EditNavLink from "../components/EditNavLink";
|
||||
|
||||
import BranchRoot from "./ChangesetsRoot";
|
||||
@@ -32,7 +45,7 @@ type Props = {
|
||||
repoLink: string,
|
||||
|
||||
// dispatch functions
|
||||
fetchRepo: (link: string, namespace: string, name: string) => void,
|
||||
fetchRepoByName: (link: string, namespace: string, name: string) => void,
|
||||
deleteRepo: (repository: Repository, () => void) => void,
|
||||
|
||||
// context props
|
||||
@@ -43,9 +56,9 @@ type Props = {
|
||||
|
||||
class RepositoryRoot extends React.Component<Props> {
|
||||
componentDidMount() {
|
||||
const { fetchRepo, namespace, name, repoLink } = this.props;
|
||||
const { fetchRepoByName, namespace, name, repoLink } = this.props;
|
||||
|
||||
fetchRepo(repoLink, namespace, name);
|
||||
fetchRepoByName(repoLink, namespace, name);
|
||||
}
|
||||
|
||||
stripEndingSlash = (url: string) => {
|
||||
@@ -209,8 +222,8 @@ const mapStateToProps = (state, ownProps) => {
|
||||
|
||||
const mapDispatchToProps = dispatch => {
|
||||
return {
|
||||
fetchRepo: (link: string, namespace: string, name: string) => {
|
||||
dispatch(fetchRepo(link, namespace, name));
|
||||
fetchRepoByName: (link: string, namespace: string, name: string) => {
|
||||
dispatch(fetchRepoByName(link, namespace, name));
|
||||
},
|
||||
deleteRepo: (repository: Repository, callback: () => void) => {
|
||||
dispatch(deleteRepo(repository, callback));
|
||||
|
||||
@@ -100,13 +100,20 @@ export function fetchReposFailure(err: Error): Action {
|
||||
}
|
||||
|
||||
// fetch repo
|
||||
export function fetchRepoByLink(repo: Repository) {
|
||||
return fetchRepo(repo._links.self.href, repo.namespace, repo.name);
|
||||
}
|
||||
|
||||
export function fetchRepo(link: string, namespace: string, name: string) {
|
||||
export function fetchRepoByName(link: string, namespace: string, name: string) {
|
||||
const repoUrl = link.endsWith("/") ? link : link + "/";
|
||||
return fetchRepo(`${repoUrl}${namespace}/${name}`, namespace, name);
|
||||
}
|
||||
|
||||
function fetchRepo(link: string, namespace: string, name: string) {
|
||||
return function(dispatch: any) {
|
||||
dispatch(fetchRepoPending(namespace, name));
|
||||
return apiClient
|
||||
.get(`${repoUrl}${namespace}/${name}`)
|
||||
.get(link)
|
||||
.then(response => response.json())
|
||||
.then(repository => {
|
||||
dispatch(fetchRepoSuccess(repository));
|
||||
@@ -214,6 +221,9 @@ export function modifyRepo(repository: Repository, callback?: () => void) {
|
||||
callback();
|
||||
}
|
||||
})
|
||||
.then(() => {
|
||||
dispatch(fetchRepoByLink(repository));
|
||||
})
|
||||
.catch(cause => {
|
||||
const error = new Error(`failed to modify repo: ${cause.message}`);
|
||||
dispatch(modifyRepoFailure(repository, error));
|
||||
@@ -356,8 +366,6 @@ export default function reducer(
|
||||
switch (action.type) {
|
||||
case FETCH_REPOS_SUCCESS:
|
||||
return normalizeByNamespaceAndName(action.payload);
|
||||
case MODIFY_REPO_SUCCESS:
|
||||
return reducerByNames(state, action.payload);
|
||||
case FETCH_REPO_SUCCESS:
|
||||
return reducerByNames(state, action.payload);
|
||||
default:
|
||||
|
||||
@@ -15,7 +15,8 @@ import reducer, {
|
||||
fetchReposByLink,
|
||||
fetchReposByPage,
|
||||
FETCH_REPO,
|
||||
fetchRepo,
|
||||
fetchRepoByLink,
|
||||
fetchRepoByName,
|
||||
FETCH_REPO_PENDING,
|
||||
FETCH_REPO_SUCCESS,
|
||||
FETCH_REPO_FAILURE,
|
||||
@@ -45,7 +46,6 @@ import reducer, {
|
||||
MODIFY_REPO,
|
||||
isModifyRepoPending,
|
||||
getModifyRepoFailure,
|
||||
modifyRepoSuccess,
|
||||
getPermissionsLink
|
||||
} from "./repos";
|
||||
import type { Repository, RepositoryCollection } from "@scm-manager/ui-types";
|
||||
@@ -323,7 +323,7 @@ describe("repos fetch", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("should successfully fetch repo slarti/fjords", () => {
|
||||
it("should successfully fetch repo slarti/fjords by name", () => {
|
||||
fetchMock.getOnce(REPOS_URL + "/slarti/fjords", slartiFjords);
|
||||
|
||||
const expectedActions = [
|
||||
@@ -343,18 +343,66 @@ describe("repos fetch", () => {
|
||||
];
|
||||
|
||||
const store = mockStore({});
|
||||
return store.dispatch(fetchRepo(URL, "slarti", "fjords")).then(() => {
|
||||
return store.dispatch(fetchRepoByName(URL, "slarti", "fjords")).then(() => {
|
||||
expect(store.getActions()).toEqual(expectedActions);
|
||||
});
|
||||
});
|
||||
|
||||
it("should dispatch FETCH_REPO_FAILURE, it the request for slarti/fjords fails", () => {
|
||||
it("should dispatch FETCH_REPO_FAILURE, if the request for slarti/fjords by name fails", () => {
|
||||
fetchMock.getOnce(REPOS_URL + "/slarti/fjords", {
|
||||
status: 500
|
||||
});
|
||||
|
||||
const store = mockStore({});
|
||||
return store.dispatch(fetchRepo(URL, "slarti", "fjords")).then(() => {
|
||||
return store.dispatch(fetchRepoByName(URL, "slarti", "fjords")).then(() => {
|
||||
const actions = store.getActions();
|
||||
expect(actions[0].type).toEqual(FETCH_REPO_PENDING);
|
||||
expect(actions[1].type).toEqual(FETCH_REPO_FAILURE);
|
||||
expect(actions[1].payload.namespace).toBe("slarti");
|
||||
expect(actions[1].payload.name).toBe("fjords");
|
||||
expect(actions[1].payload.error).toBeDefined();
|
||||
expect(actions[1].itemId).toBe("slarti/fjords");
|
||||
});
|
||||
});
|
||||
|
||||
it("should successfully fetch repo slarti/fjords", () => {
|
||||
fetchMock.getOnce(
|
||||
"http://localhost:8081/api/v2/repositories/slarti/fjords",
|
||||
slartiFjords
|
||||
);
|
||||
|
||||
const expectedActions = [
|
||||
{
|
||||
type: FETCH_REPO_PENDING,
|
||||
payload: {
|
||||
namespace: "slarti",
|
||||
name: "fjords"
|
||||
},
|
||||
itemId: "slarti/fjords"
|
||||
},
|
||||
{
|
||||
type: FETCH_REPO_SUCCESS,
|
||||
payload: slartiFjords,
|
||||
itemId: "slarti/fjords"
|
||||
}
|
||||
];
|
||||
|
||||
const store = mockStore({});
|
||||
return store.dispatch(fetchRepoByLink(slartiFjords)).then(() => {
|
||||
expect(store.getActions()).toEqual(expectedActions);
|
||||
});
|
||||
});
|
||||
|
||||
it("should dispatch FETCH_REPO_FAILURE, it the request for slarti/fjords fails", () => {
|
||||
fetchMock.getOnce(
|
||||
"http://localhost:8081/api/v2/repositories/slarti/fjords",
|
||||
{
|
||||
status: 500
|
||||
}
|
||||
);
|
||||
|
||||
const store = mockStore({});
|
||||
return store.dispatch(fetchRepoByLink(slartiFjords)).then(() => {
|
||||
const actions = store.getActions();
|
||||
expect(actions[0].type).toEqual(FETCH_REPO_PENDING);
|
||||
expect(actions[1].type).toEqual(FETCH_REPO_FAILURE);
|
||||
@@ -485,6 +533,12 @@ describe("repos fetch", () => {
|
||||
fetchMock.putOnce(slartiFjords._links.update.href, {
|
||||
status: 204
|
||||
});
|
||||
fetchMock.getOnce(
|
||||
"http://localhost:8081/api/v2/repositories/slarti/fjords",
|
||||
{
|
||||
status: 500
|
||||
}
|
||||
);
|
||||
|
||||
let editedFjords = { ...slartiFjords };
|
||||
editedFjords.description = "coast of africa";
|
||||
@@ -495,6 +549,7 @@ describe("repos fetch", () => {
|
||||
const actions = store.getActions();
|
||||
expect(actions[0].type).toEqual(MODIFY_REPO_PENDING);
|
||||
expect(actions[1].type).toEqual(MODIFY_REPO_SUCCESS);
|
||||
expect(actions[2].type).toEqual(FETCH_REPO_PENDING);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -502,6 +557,12 @@ describe("repos fetch", () => {
|
||||
fetchMock.putOnce(slartiFjords._links.update.href, {
|
||||
status: 204
|
||||
});
|
||||
fetchMock.getOnce(
|
||||
"http://localhost:8081/api/v2/repositories/slarti/fjords",
|
||||
{
|
||||
status: 500
|
||||
}
|
||||
);
|
||||
|
||||
let editedFjords = { ...slartiFjords };
|
||||
editedFjords.description = "coast of africa";
|
||||
@@ -517,6 +578,7 @@ describe("repos fetch", () => {
|
||||
const actions = store.getActions();
|
||||
expect(actions[0].type).toEqual(MODIFY_REPO_PENDING);
|
||||
expect(actions[1].type).toEqual(MODIFY_REPO_SUCCESS);
|
||||
expect(actions[2].type).toEqual(FETCH_REPO_PENDING);
|
||||
expect(called).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -574,18 +636,6 @@ describe("repos reducer", () => {
|
||||
const newState = reducer({}, fetchRepoSuccess(slartiFjords));
|
||||
expect(newState.byNames["slarti/fjords"]).toBe(slartiFjords);
|
||||
});
|
||||
|
||||
it("should update reposByNames", () => {
|
||||
const oldState = {
|
||||
byNames: {
|
||||
"slarti/fjords": slartiFjords
|
||||
}
|
||||
};
|
||||
let slartiFjordsEdited = { ...slartiFjords };
|
||||
slartiFjordsEdited.description = "I bless the rains down in Africa";
|
||||
const newState = reducer(oldState, modifyRepoSuccess(slartiFjordsEdited));
|
||||
expect(newState.byNames["slarti/fjords"]).toEqual(slartiFjordsEdited);
|
||||
});
|
||||
});
|
||||
|
||||
describe("repos selectors", () => {
|
||||
|
||||
177
scm-ui/src/users/components/SetUserPassword.js
Normal file
177
scm-ui/src/users/components/SetUserPassword.js
Normal file
@@ -0,0 +1,177 @@
|
||||
// @flow
|
||||
import React from "react";
|
||||
import type { User } from "@scm-manager/ui-types";
|
||||
import {
|
||||
InputField,
|
||||
SubmitButton,
|
||||
Notification,
|
||||
ErrorNotification
|
||||
} from "@scm-manager/ui-components";
|
||||
import * as userValidator from "./userValidation";
|
||||
import { translate } from "react-i18next";
|
||||
import { updatePassword } from "./updatePassword";
|
||||
|
||||
type Props = {
|
||||
user: User,
|
||||
t: string => string
|
||||
};
|
||||
|
||||
type State = {
|
||||
password: string,
|
||||
loading: boolean,
|
||||
passwordConfirmationError: boolean,
|
||||
validatePasswordError: boolean,
|
||||
validatePassword: string,
|
||||
error?: Error,
|
||||
passwordChanged: boolean
|
||||
};
|
||||
|
||||
class SetUserPassword extends React.Component<Props, State> {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
password: "",
|
||||
loading: false,
|
||||
passwordConfirmationError: false,
|
||||
validatePasswordError: false,
|
||||
validatePassword: "",
|
||||
passwordChanged: false
|
||||
};
|
||||
}
|
||||
|
||||
passwordIsValid = () => {
|
||||
return !(
|
||||
this.state.validatePasswordError || this.state.passwordConfirmationError
|
||||
);
|
||||
};
|
||||
|
||||
setLoadingState = () => {
|
||||
this.setState({
|
||||
...this.state,
|
||||
loading: true
|
||||
});
|
||||
};
|
||||
|
||||
setErrorState = (error: Error) => {
|
||||
this.setState({
|
||||
...this.state,
|
||||
error: error,
|
||||
loading: false
|
||||
});
|
||||
};
|
||||
|
||||
setSuccessfulState = () => {
|
||||
this.setState({
|
||||
...this.state,
|
||||
loading: false,
|
||||
passwordChanged: true,
|
||||
password: "",
|
||||
validatePassword: "",
|
||||
validatePasswordError: false,
|
||||
passwordConfirmationError: false
|
||||
});
|
||||
};
|
||||
|
||||
submit = (event: Event) => {
|
||||
event.preventDefault();
|
||||
if (this.passwordIsValid()) {
|
||||
const { user } = this.props;
|
||||
const { password } = this.state;
|
||||
this.setLoadingState();
|
||||
updatePassword(user._links.password.href, password)
|
||||
.then(result => {
|
||||
if (result.error) {
|
||||
this.setErrorState(result.error);
|
||||
} else {
|
||||
this.setSuccessfulState();
|
||||
}
|
||||
})
|
||||
.catch(err => {});
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const { t } = this.props;
|
||||
const { loading, passwordChanged, error } = this.state;
|
||||
|
||||
let message = null;
|
||||
|
||||
if (passwordChanged) {
|
||||
message = (
|
||||
<Notification
|
||||
type={"success"}
|
||||
children={t("password.set-password-successful")}
|
||||
onClose={() => this.onClose()}
|
||||
/>
|
||||
);
|
||||
} else if (error) {
|
||||
message = <ErrorNotification error={error} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<form onSubmit={this.submit}>
|
||||
{message}
|
||||
<InputField
|
||||
label={t("user.password")}
|
||||
type="password"
|
||||
onChange={this.handlePasswordChange}
|
||||
value={this.state.password ? this.state.password : ""}
|
||||
validationError={this.state.validatePasswordError}
|
||||
errorMessage={t("validation.password-invalid")}
|
||||
helpText={t("help.passwordHelpText")}
|
||||
/>
|
||||
<InputField
|
||||
label={t("validation.validatePassword")}
|
||||
type="password"
|
||||
onChange={this.handlePasswordValidationChange}
|
||||
value={this.state ? this.state.validatePassword : ""}
|
||||
validationError={this.state.passwordConfirmationError}
|
||||
errorMessage={t("validation.passwordValidation-invalid")}
|
||||
helpText={t("help.passwordConfirmHelpText")}
|
||||
/>
|
||||
<SubmitButton
|
||||
disabled={!this.passwordIsValid()}
|
||||
loading={loading}
|
||||
label={t("user-form.submit")}
|
||||
/>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
handlePasswordChange = (password: string) => {
|
||||
const validatePasswordError = !this.checkPasswords(
|
||||
password,
|
||||
this.state.validatePassword
|
||||
);
|
||||
this.setState({
|
||||
validatePasswordError: !userValidator.isPasswordValid(password),
|
||||
passwordConfirmationError: validatePasswordError,
|
||||
password: password
|
||||
});
|
||||
};
|
||||
|
||||
handlePasswordValidationChange = (validatePassword: string) => {
|
||||
const passwordConfirmed = this.checkPasswords(
|
||||
this.state.password,
|
||||
validatePassword
|
||||
);
|
||||
this.setState({
|
||||
validatePassword,
|
||||
passwordConfirmationError: !passwordConfirmed
|
||||
});
|
||||
};
|
||||
|
||||
checkPasswords = (password1: string, password2: string) => {
|
||||
return password1 === password2;
|
||||
};
|
||||
|
||||
onClose = () => {
|
||||
this.setState({
|
||||
...this.state,
|
||||
passwordChanged: false
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export default translate("users")(SetUserPassword);
|
||||
@@ -22,7 +22,7 @@ type State = {
|
||||
mailValidationError: boolean,
|
||||
nameValidationError: boolean,
|
||||
displayNameValidationError: boolean,
|
||||
passwordValidationError: boolean,
|
||||
passwordConfirmationError: boolean,
|
||||
validatePasswordError: boolean,
|
||||
validatePassword: string
|
||||
};
|
||||
@@ -44,7 +44,7 @@ class UserForm extends React.Component<Props, State> {
|
||||
mailValidationError: false,
|
||||
displayNameValidationError: false,
|
||||
nameValidationError: false,
|
||||
passwordValidationError: false,
|
||||
passwordConfirmationError: false,
|
||||
validatePasswordError: false,
|
||||
validatePassword: ""
|
||||
};
|
||||
@@ -70,7 +70,7 @@ class UserForm extends React.Component<Props, State> {
|
||||
this.state.validatePasswordError ||
|
||||
this.state.nameValidationError ||
|
||||
this.state.mailValidationError ||
|
||||
this.state.passwordValidationError ||
|
||||
this.state.passwordConfirmationError ||
|
||||
this.state.displayNameValidationError ||
|
||||
this.isFalsy(user.name) ||
|
||||
this.isFalsy(user.displayName)
|
||||
@@ -89,6 +89,7 @@ class UserForm extends React.Component<Props, State> {
|
||||
const user = this.state.user;
|
||||
|
||||
let nameField = null;
|
||||
let passwordFields = null;
|
||||
if (!this.props.user) {
|
||||
nameField = (
|
||||
<InputField
|
||||
@@ -100,6 +101,28 @@ class UserForm extends React.Component<Props, State> {
|
||||
helpText={t("help.usernameHelpText")}
|
||||
/>
|
||||
);
|
||||
passwordFields = (
|
||||
<>
|
||||
<InputField
|
||||
label={t("user.password")}
|
||||
type="password"
|
||||
onChange={this.handlePasswordChange}
|
||||
value={user ? user.password : ""}
|
||||
validationError={this.state.validatePasswordError}
|
||||
errorMessage={t("validation.password-invalid")}
|
||||
helpText={t("help.passwordHelpText")}
|
||||
/>
|
||||
<InputField
|
||||
label={t("validation.validatePassword")}
|
||||
type="password"
|
||||
onChange={this.handlePasswordValidationChange}
|
||||
value={this.state ? this.state.validatePassword : ""}
|
||||
validationError={this.state.passwordConfirmationError}
|
||||
errorMessage={t("validation.passwordValidation-invalid")}
|
||||
helpText={t("help.passwordConfirmHelpText")}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<form onSubmit={this.submit}>
|
||||
@@ -120,24 +143,7 @@ class UserForm extends React.Component<Props, State> {
|
||||
errorMessage={t("validation.mail-invalid")}
|
||||
helpText={t("help.mailHelpText")}
|
||||
/>
|
||||
<InputField
|
||||
label={t("user.password")}
|
||||
type="password"
|
||||
onChange={this.handlePasswordChange}
|
||||
value={user ? user.password : ""}
|
||||
validationError={this.state.validatePasswordError}
|
||||
errorMessage={t("validation.password-invalid")}
|
||||
helpText={t("help.passwordHelpText")}
|
||||
/>
|
||||
<InputField
|
||||
label={t("validation.validatePassword")}
|
||||
type="password"
|
||||
onChange={this.handlePasswordValidationChange}
|
||||
value={this.state ? this.state.validatePassword : ""}
|
||||
validationError={this.state.passwordValidationError}
|
||||
errorMessage={t("validation.passwordValidation-invalid")}
|
||||
helpText={t("help.passwordConfirmHelpText")}
|
||||
/>
|
||||
{passwordFields}
|
||||
<Checkbox
|
||||
label={t("user.admin")}
|
||||
onChange={this.handleAdminChange}
|
||||
@@ -189,7 +195,7 @@ class UserForm extends React.Component<Props, State> {
|
||||
);
|
||||
this.setState({
|
||||
validatePasswordError: !userValidator.isPasswordValid(password),
|
||||
passwordValidationError: validatePasswordError,
|
||||
passwordConfirmationError: validatePasswordError,
|
||||
user: { ...this.state.user, password }
|
||||
});
|
||||
};
|
||||
@@ -201,7 +207,7 @@ class UserForm extends React.Component<Props, State> {
|
||||
);
|
||||
this.setState({
|
||||
validatePassword,
|
||||
passwordValidationError: !validatePasswordError
|
||||
passwordConfirmationError: !validatePasswordError
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
28
scm-ui/src/users/components/navLinks/SetPasswordNavLink.js
Normal file
28
scm-ui/src/users/components/navLinks/SetPasswordNavLink.js
Normal file
@@ -0,0 +1,28 @@
|
||||
//@flow
|
||||
import React from "react";
|
||||
import { translate } from "react-i18next";
|
||||
import type { User } from "@scm-manager/ui-types";
|
||||
import { NavLink } from "@scm-manager/ui-components";
|
||||
|
||||
type Props = {
|
||||
t: string => string,
|
||||
user: User,
|
||||
passwordUrl: String
|
||||
};
|
||||
|
||||
class ChangePasswordNavLink extends React.Component<Props> {
|
||||
render() {
|
||||
const { t, passwordUrl } = this.props;
|
||||
|
||||
if (!this.hasPermissionToSetPassword()) {
|
||||
return null;
|
||||
}
|
||||
return <NavLink label={t("set-password-button.label")} to={passwordUrl} />;
|
||||
}
|
||||
|
||||
hasPermissionToSetPassword = () => {
|
||||
return this.props.user._links.password;
|
||||
};
|
||||
}
|
||||
|
||||
export default translate("users")(ChangePasswordNavLink);
|
||||
@@ -0,0 +1,31 @@
|
||||
import React from "react";
|
||||
import { shallow } from "enzyme";
|
||||
import "../../../tests/enzyme";
|
||||
import "../../../tests/i18n";
|
||||
import ChangePasswordNavLink from "./SetPasswordNavLink";
|
||||
|
||||
it("should render nothing, if the password link is missing", () => {
|
||||
const user = {
|
||||
_links: {}
|
||||
};
|
||||
|
||||
const navLink = shallow(
|
||||
<ChangePasswordNavLink user={user} passwordUrl="/user/password" />
|
||||
);
|
||||
expect(navLink.text()).toBe("");
|
||||
});
|
||||
|
||||
it("should render the navLink", () => {
|
||||
const user = {
|
||||
_links: {
|
||||
password: {
|
||||
href: "/password"
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const navLink = shallow(
|
||||
<ChangePasswordNavLink user={user} passwordUrl="/user/password" />
|
||||
);
|
||||
expect(navLink.text()).not.toBe("");
|
||||
});
|
||||
@@ -1,2 +1,3 @@
|
||||
export { default as DeleteUserNavLink } from "./DeleteUserNavLink";
|
||||
export { default as EditUserNavLink } from "./EditUserNavLink";
|
||||
export { default as SetPasswordNavLink } from "./SetPasswordNavLink";
|
||||
|
||||
15
scm-ui/src/users/components/updatePassword.js
Normal file
15
scm-ui/src/users/components/updatePassword.js
Normal file
@@ -0,0 +1,15 @@
|
||||
//@flow
|
||||
import { apiClient } from "@scm-manager/ui-components";
|
||||
const CONTENT_TYPE_PASSWORD_OVERWRITE =
|
||||
"application/vnd.scmm-passwordOverwrite+json;v=2";
|
||||
|
||||
export function updatePassword(url: string, password: string) {
|
||||
return apiClient
|
||||
.put(url, { newPassword: password }, CONTENT_TYPE_PASSWORD_OVERWRITE)
|
||||
.then(response => {
|
||||
return response;
|
||||
})
|
||||
.catch(err => {
|
||||
return { error: err };
|
||||
});
|
||||
}
|
||||
23
scm-ui/src/users/components/updatePassword.test.js
Normal file
23
scm-ui/src/users/components/updatePassword.test.js
Normal file
@@ -0,0 +1,23 @@
|
||||
//@flow
|
||||
import fetchMock from "fetch-mock";
|
||||
import { updatePassword } from "./updatePassword";
|
||||
|
||||
describe("get content type", () => {
|
||||
const PASSWORD_URL = "/users/testuser/password";
|
||||
const password = "testpw123";
|
||||
|
||||
afterEach(() => {
|
||||
fetchMock.reset();
|
||||
fetchMock.restore();
|
||||
});
|
||||
|
||||
it("should update password", done => {
|
||||
|
||||
fetchMock.put("/api/v2" + PASSWORD_URL, 204);
|
||||
|
||||
updatePassword(PASSWORD_URL, password).then(content => {
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -15,7 +15,7 @@ import EditUser from "./EditUser";
|
||||
import type { User } from "@scm-manager/ui-types";
|
||||
import type { History } from "history";
|
||||
import {
|
||||
fetchUser,
|
||||
fetchUserByName,
|
||||
deleteUser,
|
||||
getUserByName,
|
||||
isFetchUserPending,
|
||||
@@ -24,9 +24,14 @@ import {
|
||||
getDeleteUserFailure
|
||||
} from "../modules/users";
|
||||
|
||||
import { DeleteUserNavLink, EditUserNavLink } from "./../components/navLinks";
|
||||
import {
|
||||
DeleteUserNavLink,
|
||||
EditUserNavLink,
|
||||
SetPasswordNavLink
|
||||
} from "./../components/navLinks";
|
||||
import { translate } from "react-i18next";
|
||||
import { getUsersLink } from "../../modules/indexResource";
|
||||
import SetUserPassword from "../components/SetUserPassword";
|
||||
|
||||
type Props = {
|
||||
name: string,
|
||||
@@ -37,7 +42,7 @@ type Props = {
|
||||
|
||||
// dispatcher functions
|
||||
deleteUser: (user: User, callback?: () => void) => void,
|
||||
fetchUser: (string, string) => void,
|
||||
fetchUserByName: (string, string) => void,
|
||||
|
||||
// context objects
|
||||
t: string => string,
|
||||
@@ -47,7 +52,7 @@ type Props = {
|
||||
|
||||
class SingleUser extends React.Component<Props> {
|
||||
componentDidMount() {
|
||||
this.props.fetchUser(this.props.usersLink, this.props.name);
|
||||
this.props.fetchUserByName(this.props.usersLink, this.props.name);
|
||||
}
|
||||
|
||||
userDeleted = () => {
|
||||
@@ -97,6 +102,10 @@ class SingleUser extends React.Component<Props> {
|
||||
path={`${url}/edit`}
|
||||
component={() => <EditUser user={user} />}
|
||||
/>
|
||||
<Route
|
||||
path={`${url}/password`}
|
||||
component={() => <SetUserPassword user={user} />}
|
||||
/>
|
||||
</div>
|
||||
<div className="column">
|
||||
<Navigation>
|
||||
@@ -106,6 +115,10 @@ class SingleUser extends React.Component<Props> {
|
||||
label={t("single-user.information-label")}
|
||||
/>
|
||||
<EditUserNavLink user={user} editUrl={`${url}/edit`} />
|
||||
<SetPasswordNavLink
|
||||
user={user}
|
||||
passwordUrl={`${url}/password`}
|
||||
/>
|
||||
</Section>
|
||||
<Section label={t("single-user.actions-label")}>
|
||||
<DeleteUserNavLink user={user} deleteUser={this.deleteUser} />
|
||||
@@ -138,8 +151,8 @@ const mapStateToProps = (state, ownProps) => {
|
||||
|
||||
const mapDispatchToProps = dispatch => {
|
||||
return {
|
||||
fetchUser: (link: string, name: string) => {
|
||||
dispatch(fetchUser(link, name));
|
||||
fetchUserByName: (link: string, name: string) => {
|
||||
dispatch(fetchUserByName(link, name));
|
||||
},
|
||||
deleteUser: (user: User, callback?: () => void) => {
|
||||
dispatch(deleteUser(user, callback));
|
||||
|
||||
@@ -88,12 +88,20 @@ export function fetchUsersFailure(url: string, error: Error): Action {
|
||||
}
|
||||
|
||||
//fetch user
|
||||
export function fetchUser(link: string, name: string) {
|
||||
export function fetchUserByName(link: string, name: string) {
|
||||
const userUrl = link.endsWith("/") ? link + name : link + "/" + name;
|
||||
return fetchUser(userUrl, name);
|
||||
}
|
||||
|
||||
export function fetchUserByLink(user: User) {
|
||||
return fetchUser(user._links.self.href, user.name);
|
||||
}
|
||||
|
||||
function fetchUser(link: string, name: string) {
|
||||
return function(dispatch: any) {
|
||||
dispatch(fetchUserPending(name));
|
||||
return apiClient
|
||||
.get(userUrl)
|
||||
.get(link)
|
||||
.then(response => {
|
||||
return response.json();
|
||||
})
|
||||
@@ -196,6 +204,9 @@ export function modifyUser(user: User, callback?: () => void) {
|
||||
callback();
|
||||
}
|
||||
})
|
||||
.then(() => {
|
||||
dispatch(fetchUserByLink(user));
|
||||
})
|
||||
.catch(err => {
|
||||
dispatch(modifyUserFailure(user, err));
|
||||
});
|
||||
@@ -373,9 +384,6 @@ function byNamesReducer(state: any = {}, action: any = {}) {
|
||||
case FETCH_USER_SUCCESS:
|
||||
return reducerByName(state, action.payload.name, action.payload);
|
||||
|
||||
case MODIFY_USER_SUCCESS:
|
||||
return reducerByName(state, action.payload.name, action.payload);
|
||||
|
||||
case DELETE_USER_SUCCESS:
|
||||
const newUserByNames = deleteUserInUsersByNames(
|
||||
state,
|
||||
|
||||
@@ -20,7 +20,8 @@ import reducer, {
|
||||
FETCH_USERS_FAILURE,
|
||||
FETCH_USERS_PENDING,
|
||||
FETCH_USERS_SUCCESS,
|
||||
fetchUser,
|
||||
fetchUserByLink,
|
||||
fetchUserByName,
|
||||
fetchUserSuccess,
|
||||
getFetchUserFailure,
|
||||
fetchUsers,
|
||||
@@ -33,7 +34,6 @@ import reducer, {
|
||||
MODIFY_USER_PENDING,
|
||||
MODIFY_USER_SUCCESS,
|
||||
modifyUser,
|
||||
modifyUserSuccess,
|
||||
getUsersFromState,
|
||||
FETCH_USERS,
|
||||
getFetchUsersFailure,
|
||||
@@ -124,6 +124,7 @@ const response = {
|
||||
|
||||
const URL = "users";
|
||||
const USERS_URL = "/api/v2/users";
|
||||
const USER_ZAPHOD_URL = "http://localhost:8081/api/v2/users/zaphod";
|
||||
|
||||
const error = new Error("KAPUTT");
|
||||
|
||||
@@ -166,11 +167,37 @@ describe("users fetch()", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("should sucessfully fetch single user", () => {
|
||||
it("should sucessfully fetch single user by name", () => {
|
||||
fetchMock.getOnce(USERS_URL + "/zaphod", userZaphod);
|
||||
|
||||
const store = mockStore({});
|
||||
return store.dispatch(fetchUser(URL, "zaphod")).then(() => {
|
||||
return store.dispatch(fetchUserByName(URL, "zaphod")).then(() => {
|
||||
const actions = store.getActions();
|
||||
expect(actions[0].type).toEqual(FETCH_USER_PENDING);
|
||||
expect(actions[1].type).toEqual(FETCH_USER_SUCCESS);
|
||||
expect(actions[1].payload).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
it("should fail fetching single user by name on HTTP 500", () => {
|
||||
fetchMock.getOnce(USERS_URL + "/zaphod", {
|
||||
status: 500
|
||||
});
|
||||
|
||||
const store = mockStore({});
|
||||
return store.dispatch(fetchUserByName(URL, "zaphod")).then(() => {
|
||||
const actions = store.getActions();
|
||||
expect(actions[0].type).toEqual(FETCH_USER_PENDING);
|
||||
expect(actions[1].type).toEqual(FETCH_USER_FAILURE);
|
||||
expect(actions[1].payload).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
it("should sucessfully fetch single user", () => {
|
||||
fetchMock.getOnce(USER_ZAPHOD_URL, userZaphod);
|
||||
|
||||
const store = mockStore({});
|
||||
return store.dispatch(fetchUserByLink(userZaphod)).then(() => {
|
||||
const actions = store.getActions();
|
||||
expect(actions[0].type).toEqual(FETCH_USER_PENDING);
|
||||
expect(actions[1].type).toEqual(FETCH_USER_SUCCESS);
|
||||
@@ -179,12 +206,12 @@ describe("users fetch()", () => {
|
||||
});
|
||||
|
||||
it("should fail fetching single user on HTTP 500", () => {
|
||||
fetchMock.getOnce(USERS_URL + "/zaphod", {
|
||||
fetchMock.getOnce(USER_ZAPHOD_URL, {
|
||||
status: 500
|
||||
});
|
||||
|
||||
const store = mockStore({});
|
||||
return store.dispatch(fetchUser(URL, "zaphod")).then(() => {
|
||||
return store.dispatch(fetchUserByLink(userZaphod)).then(() => {
|
||||
const actions = store.getActions();
|
||||
expect(actions[0].type).toEqual(FETCH_USER_PENDING);
|
||||
expect(actions[1].type).toEqual(FETCH_USER_FAILURE);
|
||||
@@ -242,23 +269,26 @@ describe("users fetch()", () => {
|
||||
});
|
||||
|
||||
it("successfully update user", () => {
|
||||
fetchMock.putOnce("http://localhost:8081/api/v2/users/zaphod", {
|
||||
fetchMock.putOnce(USER_ZAPHOD_URL, {
|
||||
status: 204
|
||||
});
|
||||
fetchMock.getOnce(USER_ZAPHOD_URL, userZaphod);
|
||||
|
||||
const store = mockStore({});
|
||||
return store.dispatch(modifyUser(userZaphod)).then(() => {
|
||||
const actions = store.getActions();
|
||||
expect(actions.length).toBe(2);
|
||||
expect(actions.length).toBe(3);
|
||||
expect(actions[0].type).toEqual(MODIFY_USER_PENDING);
|
||||
expect(actions[1].type).toEqual(MODIFY_USER_SUCCESS);
|
||||
expect(actions[2].type).toEqual(FETCH_USER_PENDING);
|
||||
});
|
||||
});
|
||||
|
||||
it("should call callback, after successful modified user", () => {
|
||||
fetchMock.putOnce("http://localhost:8081/api/v2/users/zaphod", {
|
||||
fetchMock.putOnce(USER_ZAPHOD_URL, {
|
||||
status: 204
|
||||
});
|
||||
fetchMock.getOnce(USER_ZAPHOD_URL, userZaphod);
|
||||
|
||||
let called = false;
|
||||
const callMe = () => {
|
||||
@@ -415,20 +445,6 @@ describe("users reducer", () => {
|
||||
expect(newState.byNames["ford"]).toBe(userFord);
|
||||
expect(newState.list.entries).toEqual(["zaphod"]);
|
||||
});
|
||||
|
||||
it("should update state according to MODIFY_USER_SUCCESS action", () => {
|
||||
const newState = reducer(
|
||||
{
|
||||
byNames: {
|
||||
ford: {
|
||||
name: "ford"
|
||||
}
|
||||
}
|
||||
},
|
||||
modifyUserSuccess(userFord)
|
||||
);
|
||||
expect(newState.byNames["ford"]).toBe(userFord);
|
||||
});
|
||||
});
|
||||
|
||||
describe("selector tests", () => {
|
||||
|
||||
@@ -93,7 +93,7 @@ public class DiffStreamingOutput implements StreamingOutput
|
||||
public void write(OutputStream output) throws IOException {
|
||||
try
|
||||
{
|
||||
builder.retriveContent(output);
|
||||
builder.retrieveContent(output);
|
||||
}
|
||||
catch (RevisionNotFoundException ex)
|
||||
{
|
||||
|
||||
@@ -66,7 +66,7 @@ public class DiffRootResource {
|
||||
repositoryService.getDiffCommand()
|
||||
.setRevision(revision)
|
||||
.setFormat(diffFormat)
|
||||
.retriveContent(output);
|
||||
.retrieveContent(output);
|
||||
} catch (RevisionNotFoundException e) {
|
||||
throw new WebApplicationException(Response.Status.NOT_FOUND);
|
||||
}
|
||||
|
||||
@@ -92,7 +92,7 @@ public class DiffResourceTest extends RepositoryTestBase {
|
||||
public void shouldGetDiffs() throws Exception {
|
||||
when(diffCommandBuilder.setRevision(anyString())).thenReturn(diffCommandBuilder);
|
||||
when(diffCommandBuilder.setFormat(any())).thenReturn(diffCommandBuilder);
|
||||
when(diffCommandBuilder.retriveContent(any())).thenReturn(diffCommandBuilder);
|
||||
when(diffCommandBuilder.retrieveContent(any())).thenReturn(diffCommandBuilder);
|
||||
MockHttpRequest request = MockHttpRequest
|
||||
.get(DIFF_URL + "revision")
|
||||
.accept(VndMediaType.DIFF);
|
||||
@@ -124,7 +124,7 @@ public class DiffResourceTest extends RepositoryTestBase {
|
||||
public void shouldGet404OnMissingRevision() throws Exception {
|
||||
when(diffCommandBuilder.setRevision(anyString())).thenReturn(diffCommandBuilder);
|
||||
when(diffCommandBuilder.setFormat(any())).thenReturn(diffCommandBuilder);
|
||||
when(diffCommandBuilder.retriveContent(any())).thenThrow(RevisionNotFoundException.class);
|
||||
when(diffCommandBuilder.retrieveContent(any())).thenThrow(RevisionNotFoundException.class);
|
||||
|
||||
MockHttpRequest request = MockHttpRequest
|
||||
.get(DIFF_URL + "revision")
|
||||
@@ -138,7 +138,7 @@ public class DiffResourceTest extends RepositoryTestBase {
|
||||
public void shouldGet400OnCrlfInjection() throws Exception {
|
||||
when(diffCommandBuilder.setRevision(anyString())).thenReturn(diffCommandBuilder);
|
||||
when(diffCommandBuilder.setFormat(any())).thenReturn(diffCommandBuilder);
|
||||
when(diffCommandBuilder.retriveContent(any())).thenThrow(RevisionNotFoundException.class);
|
||||
when(diffCommandBuilder.retrieveContent(any())).thenThrow(RevisionNotFoundException.class);
|
||||
|
||||
MockHttpRequest request = MockHttpRequest
|
||||
.get(DIFF_URL + "ny%0D%0ASet-cookie:%20Tamper=3079675143472450634")
|
||||
@@ -152,7 +152,7 @@ public class DiffResourceTest extends RepositoryTestBase {
|
||||
public void shouldGet400OnUnknownFormat() throws Exception {
|
||||
when(diffCommandBuilder.setRevision(anyString())).thenReturn(diffCommandBuilder);
|
||||
when(diffCommandBuilder.setFormat(any())).thenReturn(diffCommandBuilder);
|
||||
when(diffCommandBuilder.retriveContent(any())).thenThrow(RevisionNotFoundException.class);
|
||||
when(diffCommandBuilder.retrieveContent(any())).thenThrow(RevisionNotFoundException.class);
|
||||
|
||||
MockHttpRequest request = MockHttpRequest
|
||||
.get(DIFF_URL + "revision?format=Unknown")
|
||||
@@ -166,7 +166,7 @@ public class DiffResourceTest extends RepositoryTestBase {
|
||||
public void shouldAcceptDiffFormats() throws Exception {
|
||||
when(diffCommandBuilder.setRevision(anyString())).thenReturn(diffCommandBuilder);
|
||||
when(diffCommandBuilder.setFormat(any())).thenReturn(diffCommandBuilder);
|
||||
when(diffCommandBuilder.retriveContent(any())).thenReturn(diffCommandBuilder);
|
||||
when(diffCommandBuilder.retrieveContent(any())).thenReturn(diffCommandBuilder);
|
||||
|
||||
Arrays.stream(DiffFormat.values()).map(DiffFormat::name).forEach(
|
||||
this::assertRequestOk
|
||||
|
||||
Reference in New Issue
Block a user