scm-ui: new repository layout

This commit is contained in:
Sebastian Sdorra
2019-10-07 10:57:09 +02:00
parent 09c7def874
commit c05798e254
417 changed files with 3620 additions and 52971 deletions

View File

@@ -0,0 +1,137 @@
// @flow
import React from "react";
import type { User } from "@scm-manager/ui-types";
import {
SubmitButton,
Notification,
ErrorNotification,
PasswordConfirmation
} from "@scm-manager/ui-components";
import { translate } from "react-i18next";
import { setPassword } from "./setPassword";
type Props = {
user: User,
t: string => string
};
type State = {
password: string,
loading: boolean,
error?: Error,
passwordChanged: boolean,
passwordValid: 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,
passwordValid: false
};
}
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: ""
});
};
submit = (event: Event) => {
event.preventDefault();
if (this.state.password) {
const { user } = this.props;
const { password } = this.state;
this.setLoadingState();
setPassword(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("singleUserPassword.setPasswordSuccessful")}
onClose={() => this.onClose()}
/>
);
} else if (error) {
message = <ErrorNotification error={error} />;
}
return (
<form onSubmit={this.submit}>
{message}
<PasswordConfirmation
passwordChanged={this.passwordChanged}
key={this.state.passwordChanged ? "changed" : "unchanged"}
/>
<div className="columns">
<div className="column">
<SubmitButton
disabled={!this.state.passwordValid}
loading={loading}
label={t("singleUserPassword.button")}
/>
</div>
</div>
</form>
);
}
passwordChanged = (password: string, passwordValid: boolean) => {
this.setState({
...this.state,
password,
passwordValid: !!password && passwordValid
});
};
onClose = () => {
this.setState({
...this.state,
passwordChanged: false
});
};
}
export default translate("users")(SetUserPassword);

View File

@@ -0,0 +1,225 @@
// @flow
import React from "react";
import { translate } from "react-i18next";
import type { User } from "@scm-manager/ui-types";
import {
Subtitle,
Checkbox,
InputField,
PasswordConfirmation,
SubmitButton,
validation as validator
} from "@scm-manager/ui-components";
import * as userValidator from "./userValidation";
type Props = {
submitForm: User => void,
user?: User,
loading?: boolean,
t: string => string
};
type State = {
user: User,
mailValidationError: boolean,
nameValidationError: boolean,
displayNameValidationError: boolean,
passwordValid: boolean
};
class UserForm extends React.Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = {
user: {
name: "",
displayName: "",
mail: "",
password: "",
active: true,
_links: {}
},
mailValidationError: false,
displayNameValidationError: false,
nameValidationError: false,
passwordValid: false
};
}
componentDidMount() {
const { user } = this.props;
if (user) {
this.setState({ user: { ...user } });
}
}
isFalsy(value) {
if (!value) {
return true;
}
return false;
}
createUserComponentsAreInvalid = () => {
const user = this.state.user;
if (!this.props.user) {
return (
this.state.nameValidationError ||
this.isFalsy(user.name) ||
!this.state.passwordValid
);
} else {
return false;
}
};
editUserComponentsAreUnchanged = () => {
const user = this.state.user;
if (this.props.user) {
return (
this.props.user.displayName === user.displayName &&
this.props.user.mail === user.mail &&
this.props.user.active === user.active
);
} else {
return false;
}
};
isValid = () => {
const user = this.state.user;
return !(
this.createUserComponentsAreInvalid() ||
this.editUserComponentsAreUnchanged() ||
this.state.mailValidationError ||
this.state.displayNameValidationError ||
this.isFalsy(user.displayName) ||
this.isFalsy(user.mail)
);
};
submit = (event: Event) => {
event.preventDefault();
if (this.isValid()) {
this.props.submitForm(this.state.user);
}
};
render() {
const { loading, t } = this.props;
const user = this.state.user;
let nameField = null;
let passwordChangeField = null;
let subtitle = null;
if (!this.props.user) {
// create new user
nameField = (
<div className="column is-half">
<InputField
label={t("user.name")}
onChange={this.handleUsernameChange}
value={user ? user.name : ""}
validationError={this.state.nameValidationError}
errorMessage={t("validation.name-invalid")}
helpText={t("help.usernameHelpText")}
/>
</div>
);
passwordChangeField = (
<PasswordConfirmation passwordChanged={this.handlePasswordChange} />
);
} else {
// edit existing user
subtitle = <Subtitle subtitle={t("userForm.subtitle")} />;
}
return (
<>
{subtitle}
<form onSubmit={this.submit}>
<div className="columns is-multiline">
{nameField}
<div className="column is-half">
<InputField
label={t("user.displayName")}
onChange={this.handleDisplayNameChange}
value={user ? user.displayName : ""}
validationError={this.state.displayNameValidationError}
errorMessage={t("validation.displayname-invalid")}
helpText={t("help.displayNameHelpText")}
/>
</div>
<div className="column is-half">
<InputField
label={t("user.mail")}
onChange={this.handleEmailChange}
value={user ? user.mail : ""}
validationError={this.state.mailValidationError}
errorMessage={t("validation.mail-invalid")}
helpText={t("help.mailHelpText")}
/>
</div>
</div>
{passwordChangeField}
<div className="columns">
<div className="column">
<Checkbox
label={t("user.active")}
onChange={this.handleActiveChange}
checked={user ? user.active : false}
helpText={t("help.activeHelpText")}
/>
</div>
</div>
<div className="columns">
<div className="column">
<SubmitButton
disabled={!this.isValid()}
loading={loading}
label={t("userForm.button")}
/>
</div>
</div>
</form>
</>
);
}
handleUsernameChange = (name: string) => {
this.setState({
nameValidationError: !validator.isNameValid(name),
user: { ...this.state.user, name }
});
};
handleDisplayNameChange = (displayName: string) => {
this.setState({
displayNameValidationError: !userValidator.isDisplayNameValid(
displayName
),
user: { ...this.state.user, displayName }
});
};
handleEmailChange = (mail: string) => {
this.setState({
mailValidationError: !validator.isMailValid(mail),
user: { ...this.state.user, mail }
});
};
handlePasswordChange = (password: string, passwordValid: boolean) => {
this.setState({
user: { ...this.state.user, password },
passwordValid: !this.isFalsy(password) && passwordValid
});
};
handleActiveChange = (active: boolean) => {
this.setState({ user: { ...this.state.user, active } });
};
}
export default translate("users")(UserForm);

View File

@@ -0,0 +1,28 @@
//@flow
import React from "react";
import type { User } from "@scm-manager/ui-types";
import { NavLink } from "@scm-manager/ui-components";
import { translate } from "react-i18next";
type Props = {
user: User,
editUrl: String,
t: string => string
};
class EditUserNavLink extends React.Component<Props> {
isEditable = () => {
return this.props.user._links.update;
};
render() {
const { t, editUrl } = this.props;
if (!this.isEditable()) {
return null;
}
return <NavLink to={editUrl} label={t("singleUser.menu.generalNavLink")} />;
}
}
export default translate("users")(EditUserNavLink);

View File

@@ -0,0 +1,27 @@
import React from "react";
import { shallow } from "enzyme";
import "../../../tests/enzyme";
import "../../../tests/i18n";
import EditUserNavLink from "./EditUserNavLink";
it("should render nothing, if the edit link is missing", () => {
const user = {
_links: {}
};
const navLink = shallow(<EditUserNavLink user={user} editUrl='/user/edit'/>);
expect(navLink.text()).toBe("");
});
it("should render the navLink", () => {
const user = {
_links: {
update: {
href: "/users"
}
}
};
const navLink = shallow(<EditUserNavLink user={user} editUrl='/user/edit'/>);
expect(navLink.text()).not.toBe("");
});

View 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 to={passwordUrl} label={t("singleUser.menu.setPasswordNavLink")} />;
}
hasPermissionToSetPassword = () => {
return this.props.user._links.password;
};
}
export default translate("users")(ChangePasswordNavLink);

View File

@@ -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("");
});

View 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,
permissionsUrl: String
};
class ChangePermissionNavLink extends React.Component<Props> {
render() {
const { t, permissionsUrl } = this.props;
if (!this.hasPermissionToSetPermission()) {
return null;
}
return <NavLink to={permissionsUrl} label={t("singleUser.menu.setPermissionsNavLink")} />;
}
hasPermissionToSetPermission = () => {
return this.props.user._links.permissions;
};
}
export default translate("users")(ChangePermissionNavLink);

View File

@@ -0,0 +1,31 @@
import React from "react";
import { shallow } from "enzyme";
import "../../../tests/enzyme";
import "../../../tests/i18n";
import SetPermissionsNavLink from "./SetPermissionsNavLink";
it("should render nothing, if the permissions link is missing", () => {
const user = {
_links: {}
};
const navLink = shallow(
<SetPermissionsNavLink user={user} permissionsUrl="/user/permissions" />
);
expect(navLink.text()).toBe("");
});
it("should render the navLink", () => {
const user = {
_links: {
permissions: {
href: "/permissions"
}
}
};
const navLink = shallow(
<SetPermissionsNavLink user={user} permissionsUrl="/user/permissions" />
);
expect(navLink.text()).not.toBe("");
});

View File

@@ -0,0 +1,3 @@
export { default as EditUserNavLink } from "./EditUserNavLink";
export { default as SetPasswordNavLink } from "./SetPasswordNavLink";
export { default as SetPermissionsNavLink } from "./SetPermissionsNavLink";

View File

@@ -0,0 +1,13 @@
//@flow
import { apiClient } from "@scm-manager/ui-components";
export const CONTENT_TYPE_PASSWORD_OVERWRITE =
"application/vnd.scmm-passwordOverwrite+json;v=2";
export function setPassword(url: string, password: string) {
return apiClient
.put(url, { newPassword: password }, CONTENT_TYPE_PASSWORD_OVERWRITE)
.then(response => {
return response;
});
}

View File

@@ -0,0 +1,25 @@
//@flow
import fetchMock from "fetch-mock";
import { CONTENT_TYPE_PASSWORD_OVERWRITE, setPassword } from "./setPassword";
describe("password change", () => {
const SET_PASSWORD_URL = "/users/testuser/password";
const newPassword = "testpw123";
afterEach(() => {
fetchMock.reset();
fetchMock.restore();
});
it("should set password", done => {
fetchMock.put("/api/v2" + SET_PASSWORD_URL, 204, {
headers: {
"content-type": CONTENT_TYPE_PASSWORD_OVERWRITE
}
});
setPassword(SET_PASSWORD_URL, newPassword).then(content => {
done();
});
});
});

View File

@@ -0,0 +1,60 @@
//@flow
import React from "react";
import type { User } from "@scm-manager/ui-types";
import { translate } from "react-i18next";
import { Checkbox, MailLink, DateFromNow } from "@scm-manager/ui-components";
type Props = {
user: User,
t: string => string
};
class Details extends React.Component<Props> {
render() {
const { user, t } = this.props;
return (
<table className="table">
<tbody>
<tr>
<th>{t("user.name")}</th>
<td>{user.name}</td>
</tr>
<tr>
<th>{t("user.displayName")}</th>
<td>{user.displayName}</td>
</tr>
<tr>
<th>{t("user.mail")}</th>
<td>
<MailLink address={user.mail} />
</td>
</tr>
<tr>
<th>{t("user.active")}</th>
<td>
<Checkbox checked={user.active} />
</td>
</tr>
<tr>
<th>{t("user.type")}</th>
<td>{user.type}</td>
</tr>
<tr>
<th>{t("user.creationDate")}</th>
<td>
<DateFromNow date={user.creationDate} />
</td>
</tr>
<tr>
<th>{t("user.lastModified")}</th>
<td>
<DateFromNow date={user.lastModified} />
</td>
</tr>
</tbody>
</table>
);
}
}
export default translate("users")(Details);

View File

@@ -0,0 +1,43 @@
// @flow
import React from "react";
import { translate } from "react-i18next";
import { Link } from "react-router-dom";
import type { User } from "@scm-manager/ui-types";
import { Icon } from "@scm-manager/ui-components";
type Props = {
user: User,
// context props
t: string => string
};
class UserRow extends React.Component<Props> {
renderLink(to: string, label: string) {
return <Link to={to}>{label}</Link>;
}
render() {
const { user, t } = this.props;
const to = `/user/${user.name}`;
const iconType = user.active ? (
<Icon title={t("user.active")} name="user" />
) : (
<Icon title={t("user.inactive")} name="user-slash" />
);
return (
<tr className={user.active ? "border-is-green" : "border-is-yellow"}>
<td>{iconType} {this.renderLink(to, user.name)}</td>
<td className="is-hidden-mobile">
{this.renderLink(to, user.displayName)}
</td>
<td>
<a href={`mailto:${user.mail}`}>{user.mail}</a>
</td>
</tr>
);
}
}
export default translate("users")(UserRow);

View File

@@ -0,0 +1,34 @@
// @flow
import React from "react";
import { translate } from "react-i18next";
import UserRow from "./UserRow";
import type { User } from "@scm-manager/ui-types";
type Props = {
t: string => string,
users: User[]
};
class UserTable extends React.Component<Props> {
render() {
const { users, t } = this.props;
return (
<table className="card-table table is-hoverable is-fullwidth">
<thead>
<tr>
<th className="is-hidden-mobile">{t("user.name")}</th>
<th>{t("user.displayName")}</th>
<th>{t("user.mail")}</th>
</tr>
</thead>
<tbody>
{users.map((user, index) => {
return <UserRow key={index} user={user} />;
})}
</tbody>
</table>
);
}
}
export default translate("users")(UserTable);

View File

@@ -0,0 +1,3 @@
export { default as Details } from "./Details";
export { default as UserRow } from "./UserRow";
export { default as UserTable } from "./UserTable";

View File

@@ -0,0 +1,17 @@
// @flow
import { validation } from "@scm-manager/ui-components";
const { isNameValid, isMailValid, isPathValid } = validation;
export { isNameValid, isMailValid, isPathValid };
export const isDisplayNameValid = (displayName: string) => {
if (displayName) {
return true;
}
return false;
};
export const isPasswordValid = (password: string) => {
return password.length >= 6 && password.length < 32;
};

View File

@@ -0,0 +1,40 @@
// @flow
import * as validator from "./userValidation";
describe("test displayName validation", () => {
it("should return false", () => {
expect(validator.isDisplayNameValid("")).toBe(false);
});
it("should return true", () => {
// valid names taken from ValidationUtilTest.java
const validNames = [
"Arthur Dent",
"Tricia.McMillan@hitchhiker.com",
"Ford Prefect (ford.prefect@hitchhiker.com)",
"Zaphod Beeblebrox <zaphod.beeblebrox@hitchhiker.com>",
"Marvin, der depressive Roboter"
];
for (let name of validNames) {
expect(validator.isDisplayNameValid(name)).toBe(true);
}
});
});
describe("test password validation", () => {
it("should return false", () => {
// invalid taken from ValidationUtilTest.java
const invalid = ["", "abc", "aaabbbcccdddeeefffggghhhiiijjjkkk"];
for (let password of invalid) {
expect(validator.isPasswordValid(password)).toBe(false);
}
});
it("should return true", () => {
// valid taken from ValidationUtilTest.java
const valid = ["secret123", "mySuperSecretPassword"];
for (let password of valid) {
expect(validator.isPasswordValid(password)).toBe(true);
}
});
});

View File

@@ -0,0 +1,91 @@
//@flow
import React from "react";
import { connect } from "react-redux";
import UserForm from "../components/UserForm";
import type { User } from "@scm-manager/ui-types";
import type { History } from "history";
import {
createUser,
createUserReset,
isCreateUserPending,
getCreateUserFailure
} from "../modules/users";
import { Page } from "@scm-manager/ui-components";
import { translate } from "react-i18next";
import { getUsersLink } from "../../modules/indexResource";
type Props = {
loading?: boolean,
error?: Error,
usersLink: string,
// dispatcher functions
addUser: (link: string, user: User, callback?: () => void) => void,
resetForm: () => void,
// context objects
t: string => string,
history: History
};
class CreateUser extends React.Component<Props> {
componentDidMount() {
this.props.resetForm();
}
userCreated = (user: User) => {
const { history } = this.props;
history.push("/user/" + user.name);
};
createUser = (user: User) => {
this.props.addUser(this.props.usersLink, user, () =>
this.userCreated(user)
);
};
render() {
const { t, loading, error } = this.props;
return (
<Page
title={t("createUser.title")}
subtitle={t("createUser.subtitle")}
error={error}
showContentOnError={true}
>
<UserForm
submitForm={user => this.createUser(user)}
loading={loading}
/>
</Page>
);
}
}
const mapDispatchToProps = dispatch => {
return {
addUser: (link: string, user: User, callback?: () => void) => {
dispatch(createUser(link, user, callback));
},
resetForm: () => {
dispatch(createUserReset());
}
};
};
const mapStateToProps = (state, ownProps) => {
const loading = isCreateUserPending(state);
const error = getCreateUserFailure(state);
const usersLink = getUsersLink(state);
return {
usersLink,
loading,
error
};
};
export default connect(
mapStateToProps,
mapDispatchToProps
)(translate("users")(CreateUser));

View File

@@ -0,0 +1,113 @@
// @flow
import React from "react";
import { translate } from "react-i18next";
import type { User } from "@scm-manager/ui-types";
import {
Subtitle,
DeleteButton,
confirmAlert,
ErrorNotification
} from "@scm-manager/ui-components";
import {
deleteUser,
getDeleteUserFailure,
isDeleteUserPending
} from "../modules/users";
import { connect } from "react-redux";
import { withRouter } from "react-router-dom";
import type { History } from "history";
type Props = {
loading: boolean,
error: Error,
user: User,
confirmDialog?: boolean,
deleteUser: (user: User, callback?: () => void) => void,
// context props
history: History,
t: string => string
};
class DeleteUser extends React.Component<Props> {
static defaultProps = {
confirmDialog: true
};
userDeleted = () => {
this.props.history.push("/users/");
};
deleteUser = () => {
this.props.deleteUser(this.props.user, this.userDeleted);
};
confirmDelete = () => {
const { t } = this.props;
confirmAlert({
title: t("deleteUser.confirmAlert.title"),
message: t("deleteUser.confirmAlert.message"),
buttons: [
{
label: t("deleteUser.confirmAlert.submit"),
onClick: () => this.deleteUser()
},
{
label: t("deleteUser.confirmAlert.cancel"),
onClick: () => null
}
]
});
};
isDeletable = () => {
return this.props.user._links.delete;
};
render() {
const { loading, error, confirmDialog, t } = this.props;
const action = confirmDialog ? this.confirmDelete : this.deleteUser;
if (!this.isDeletable()) {
return null;
}
return (
<>
<Subtitle subtitle={t("deleteUser.subtitle")} />
<ErrorNotification error={error} />
<div className="columns">
<div className="column">
<DeleteButton
label={t("deleteUser.button")}
action={action}
loading={loading}
/>
</div>
</div>
</>
);
}
}
const mapStateToProps = (state, ownProps) => {
const loading = isDeleteUserPending(state, ownProps.user.name);
const error = getDeleteUserFailure(state, ownProps.user.name);
return {
loading,
error
};
};
const mapDispatchToProps = dispatch => {
return {
deleteUser: (user: User, callback?: () => void) => {
dispatch(deleteUser(user, callback));
}
};
};
export default connect(
mapStateToProps,
mapDispatchToProps
)(withRouter(translate("users")(DeleteUser)));

View File

@@ -0,0 +1,84 @@
//@flow
import React from "react";
import { connect } from "react-redux";
import { withRouter } from "react-router-dom";
import UserForm from "../components/UserForm";
import DeleteUser from "./DeleteUser";
import type { User } from "@scm-manager/ui-types";
import {
modifyUser,
isModifyUserPending,
getModifyUserFailure,
modifyUserReset
} from "../modules/users";
import type { History } from "history";
import { ErrorNotification } from "@scm-manager/ui-components";
type Props = {
loading: boolean,
error: Error,
// dispatch functions
modifyUser: (user: User, callback?: () => void) => void,
modifyUserReset: User => void,
// context objects
user: User,
history: History
};
class EditUser extends React.Component<Props> {
componentDidMount() {
const { modifyUserReset, user } = this.props;
modifyUserReset(user);
}
userModified = (user: User) => () => {
this.props.history.push(`/user/${user.name}`);
};
modifyUser = (user: User) => {
this.props.modifyUser(user, this.userModified(user));
};
render() {
const { user, loading, error } = this.props;
return (
<div>
<ErrorNotification error={error} />
<UserForm
submitForm={user => this.modifyUser(user)}
user={user}
loading={loading}
/>
<hr />
<DeleteUser user={user} />
</div>
);
}
}
const mapStateToProps = (state, ownProps) => {
const loading = isModifyUserPending(state, ownProps.user.name);
const error = getModifyUserFailure(state, ownProps.user.name);
return {
loading,
error
};
};
const mapDispatchToProps = dispatch => {
return {
modifyUser: (user: User, callback?: () => void) => {
dispatch(modifyUser(user, callback));
},
modifyUserReset: (user: User) => {
dispatch(modifyUserReset(user));
}
};
};
export default connect(
mapStateToProps,
mapDispatchToProps
)(withRouter(EditUser));

View File

@@ -0,0 +1,179 @@
//@flow
import React from "react";
import { connect } from "react-redux";
import {
Page,
Loading,
Navigation,
SubNavigation,
Section,
NavLink,
ErrorPage
} from "@scm-manager/ui-components";
import { Route } from "react-router";
import { Details } from "./../components/table";
import EditUser from "./EditUser";
import type { User } from "@scm-manager/ui-types";
import type { History } from "history";
import {
fetchUserByName,
getUserByName,
isFetchUserPending,
getFetchUserFailure
} from "../modules/users";
import { EditUserNavLink, SetPasswordNavLink, SetPermissionsNavLink } from "./../components/navLinks";
import { translate } from "react-i18next";
import { getUsersLink } from "../../modules/indexResource";
import SetUserPassword from "../components/SetUserPassword";
import SetPermissions from "../../permissions/components/SetPermissions";
import {ExtensionPoint} from "@scm-manager/ui-extensions";
type Props = {
name: string,
user: User,
loading: boolean,
error: Error,
usersLink: string,
// dispatcher function
fetchUserByName: (string, string) => void,
// context objects
t: string => string,
match: any,
history: History
};
class SingleUser extends React.Component<Props> {
componentDidMount() {
this.props.fetchUserByName(this.props.usersLink, 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, user } = this.props;
if (error) {
return (
<ErrorPage
title={t("singleUser.errorTitle")}
subtitle={t("singleUser.errorSubtitle")}
error={error}
/>
);
}
if (!user || loading) {
return <Loading />;
}
const url = this.matchedUrl();
const extensionProps = {
user,
url
};
return (
<Page title={user.displayName}>
<div className="columns">
<div className="column is-three-quarters">
<Route path={url} exact component={() => <Details user={user} />} />
<Route
path={`${url}/settings/general`}
component={() => <EditUser user={user} />}
/>
<Route
path={`${url}/settings/password`}
component={() => <SetUserPassword user={user} />}
/>
<Route
path={`${url}/settings/permissions`}
component={() => (
<SetPermissions
selectedPermissionsLink={user._links.permissions}
/>
)}
/>
<ExtensionPoint
name="user.route"
props={extensionProps}
renderAll={true}
/>
</div>
<div className="column">
<Navigation>
<Section label={t("singleUser.menu.navigationLabel")}>
<NavLink
to={`${url}`}
icon="fas fa-info-circle"
label={t("singleUser.menu.informationNavLink")}
/>
<SubNavigation
to={`${url}/settings/general`}
label={t("singleUser.menu.settingsNavLink")}
>
<EditUserNavLink
user={user}
editUrl={`${url}/settings/general`}
/>
<SetPasswordNavLink
user={user}
passwordUrl={`${url}/settings/password`}
/>
<SetPermissionsNavLink
user={user}
permissionsUrl={`${url}/settings/permissions`}
/>
<ExtensionPoint
name="user.setting"
props={extensionProps}
renderAll={true}
/>
</SubNavigation>
</Section>
</Navigation>
</div>
</div>
</Page>
);
}
}
const mapStateToProps = (state, ownProps) => {
const name = ownProps.match.params.name;
const user = getUserByName(state, name);
const loading = isFetchUserPending(state, name);
const error = getFetchUserFailure(state, name);
const usersLink = getUsersLink(state);
return {
usersLink,
name,
user,
loading,
error
};
};
const mapDispatchToProps = dispatch => {
return {
fetchUserByName: (link: string, name: string) => {
dispatch(fetchUserByName(link, name));
}
};
};
export default connect(
mapStateToProps,
mapDispatchToProps
)(translate("users")(SingleUser));

View File

@@ -0,0 +1,158 @@
// @flow
import React from "react";
import { connect } from "react-redux";
import { translate } from "react-i18next";
import type { History } from "history";
import type { User, PagedCollection } from "@scm-manager/ui-types";
import {
fetchUsersByPage,
getUsersFromState,
selectListAsCollection,
isPermittedToCreateUsers,
isFetchUsersPending,
getFetchUsersFailure
} from "../modules/users";
import {
Page,
PageActions,
OverviewPageActions,
Notification,
LinkPaginator,
urls,
CreateButton
} from "@scm-manager/ui-components";
import { UserTable } from "./../components/table";
import { getUsersLink } from "../../modules/indexResource";
type Props = {
users: User[],
loading: boolean,
error: Error,
canAddUsers: boolean,
list: PagedCollection,
page: number,
usersLink: string,
// context objects
t: string => string,
history: History,
location: any,
// dispatch functions
fetchUsersByPage: (link: string, page: number, filter?: string) => void
};
class Users extends React.Component<Props> {
componentDidMount() {
const { fetchUsersByPage, usersLink, page, location } = this.props;
fetchUsersByPage(
usersLink,
page,
urls.getQueryStringFromLocation(location)
);
}
componentDidUpdate = (prevProps: Props) => {
const {
loading,
list,
page,
usersLink,
location,
fetchUsersByPage
} = this.props;
if (list && page && !loading) {
const statePage: number = list.page + 1;
if (page !== statePage || prevProps.location.search !== location.search) {
fetchUsersByPage(
usersLink,
page,
urls.getQueryStringFromLocation(location)
);
}
}
};
render() {
const { users, loading, error, canAddUsers, t } = this.props;
return (
<Page
title={t("users.title")}
subtitle={t("users.subtitle")}
loading={loading || !users}
error={error}
>
{this.renderUserTable()}
{this.renderCreateButton()}
<PageActions>
<OverviewPageActions
showCreateButton={canAddUsers}
link="users"
label={t("users.createButton")}
/>
</PageActions>
</Page>
);
}
renderUserTable() {
const { users, list, page, location, t } = this.props;
if (users && users.length > 0) {
return (
<>
<UserTable users={users} />
<LinkPaginator
collection={list}
page={page}
filter={urls.getQueryStringFromLocation(location)}
/>
</>
);
}
return <Notification type="info">{t("users.noUsers")}</Notification>;
}
renderCreateButton() {
const { canAddUsers, t } = this.props;
if (canAddUsers) {
return (
<CreateButton label={t("users.createButton")} link="/users/create" />
);
}
return null;
}
}
const mapStateToProps = (state, ownProps) => {
const { match } = ownProps;
const users = getUsersFromState(state);
const loading = isFetchUsersPending(state);
const error = getFetchUsersFailure(state);
const page = urls.getPageFromMatch(match);
const canAddUsers = isPermittedToCreateUsers(state);
const list = selectListAsCollection(state);
const usersLink = getUsersLink(state);
return {
users,
loading,
error,
canAddUsers,
list,
page,
usersLink
};
};
const mapDispatchToProps = dispatch => {
return {
fetchUsersByPage: (link: string, page: number, filter?: string) => {
dispatch(fetchUsersByPage(link, page, filter));
}
};
};
export default connect(
mapStateToProps,
mapDispatchToProps
)(translate("users")(Users));

View File

@@ -0,0 +1,478 @@
// @flow
import { apiClient } from "@scm-manager/ui-components";
import { isPending } from "../../modules/pending";
import { getFailure } from "../../modules/failure";
import * as types from "../../modules/types";
import { combineReducers, Dispatch } from "redux";
import type { User, Action, PagedCollection } from "@scm-manager/ui-types";
export const FETCH_USERS = "scm/users/FETCH_USERS";
export const FETCH_USERS_PENDING = `${FETCH_USERS}_${types.PENDING_SUFFIX}`;
export const FETCH_USERS_SUCCESS = `${FETCH_USERS}_${types.SUCCESS_SUFFIX}`;
export const FETCH_USERS_FAILURE = `${FETCH_USERS}_${types.FAILURE_SUFFIX}`;
export const FETCH_USER = "scm/users/FETCH_USER";
export const FETCH_USER_PENDING = `${FETCH_USER}_${types.PENDING_SUFFIX}`;
export const FETCH_USER_SUCCESS = `${FETCH_USER}_${types.SUCCESS_SUFFIX}`;
export const FETCH_USER_FAILURE = `${FETCH_USER}_${types.FAILURE_SUFFIX}`;
export const CREATE_USER = "scm/users/CREATE_USER";
export const CREATE_USER_PENDING = `${CREATE_USER}_${types.PENDING_SUFFIX}`;
export const CREATE_USER_SUCCESS = `${CREATE_USER}_${types.SUCCESS_SUFFIX}`;
export const CREATE_USER_FAILURE = `${CREATE_USER}_${types.FAILURE_SUFFIX}`;
export const CREATE_USER_RESET = `${CREATE_USER}_${types.RESET_SUFFIX}`;
export const MODIFY_USER = "scm/users/MODIFY_USER";
export const MODIFY_USER_PENDING = `${MODIFY_USER}_${types.PENDING_SUFFIX}`;
export const MODIFY_USER_SUCCESS = `${MODIFY_USER}_${types.SUCCESS_SUFFIX}`;
export const MODIFY_USER_FAILURE = `${MODIFY_USER}_${types.FAILURE_SUFFIX}`;
export const MODIFY_USER_RESET = `${MODIFY_USER}_${types.RESET_SUFFIX}`;
export const DELETE_USER = "scm/users/DELETE_USER";
export const DELETE_USER_PENDING = `${DELETE_USER}_${types.PENDING_SUFFIX}`;
export const DELETE_USER_SUCCESS = `${DELETE_USER}_${types.SUCCESS_SUFFIX}`;
export const DELETE_USER_FAILURE = `${DELETE_USER}_${types.FAILURE_SUFFIX}`;
const CONTENT_TYPE_USER = "application/vnd.scmm-user+json;v=2";
// TODO i18n for error messages
// fetch users
export function fetchUsers(link: string) {
return fetchUsersByLink(link);
}
export function fetchUsersByPage(link: string, page: number, filter?: string) {
// backend start counting by 0
if (filter) {
return fetchUsersByLink(
`${link}?page=${page - 1}&q=${decodeURIComponent(filter)}`
);
}
return fetchUsersByLink(`${link}?page=${page - 1}`);
}
export function fetchUsersByLink(link: string) {
return function(dispatch: any) {
dispatch(fetchUsersPending());
return apiClient
.get(link)
.then(response => response.json())
.then(data => {
dispatch(fetchUsersSuccess(data));
})
.catch(error => {
dispatch(fetchUsersFailure(link, error));
});
};
}
export function fetchUsersPending(): Action {
return {
type: FETCH_USERS_PENDING
};
}
export function fetchUsersSuccess(users: any): Action {
return {
type: FETCH_USERS_SUCCESS,
payload: users
};
}
export function fetchUsersFailure(url: string, error: Error): Action {
return {
type: FETCH_USERS_FAILURE,
payload: {
error,
url
}
};
}
//fetch user
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(link)
.then(response => {
return response.json();
})
.then(data => {
dispatch(fetchUserSuccess(data));
})
.catch(error => {
dispatch(fetchUserFailure(name, error));
});
};
}
export function fetchUserPending(name: string): Action {
return {
type: FETCH_USER_PENDING,
payload: name,
itemId: name
};
}
export function fetchUserSuccess(user: any): Action {
return {
type: FETCH_USER_SUCCESS,
payload: user,
itemId: user.name
};
}
export function fetchUserFailure(name: string, error: Error): Action {
return {
type: FETCH_USER_FAILURE,
payload: {
name,
error
},
itemId: name
};
}
//create user
export function createUser(link: string, user: User, callback?: () => void) {
return function(dispatch: Dispatch) {
dispatch(createUserPending(user));
return apiClient
.post(link, user, CONTENT_TYPE_USER)
.then(() => {
dispatch(createUserSuccess());
if (callback) {
callback();
}
})
.catch(error => dispatch(createUserFailure(error)));
};
}
export function createUserPending(user: User): Action {
return {
type: CREATE_USER_PENDING,
user
};
}
export function createUserSuccess(): Action {
return {
type: CREATE_USER_SUCCESS
};
}
export function createUserFailure(error: Error): Action {
return {
type: CREATE_USER_FAILURE,
payload: error
};
}
export function createUserReset() {
return {
type: CREATE_USER_RESET
};
}
//modify user
export function modifyUser(user: User, callback?: () => void) {
return function(dispatch: Dispatch) {
dispatch(modifyUserPending(user));
return apiClient
.put(user._links.update.href, user, CONTENT_TYPE_USER)
.then(() => {
dispatch(modifyUserSuccess(user));
if (callback) {
callback();
}
})
.then(() => {
dispatch(fetchUserByLink(user));
})
.catch(err => {
dispatch(modifyUserFailure(user, err));
});
};
}
export function modifyUserPending(user: User): Action {
return {
type: MODIFY_USER_PENDING,
payload: user,
itemId: user.name
};
}
export function modifyUserSuccess(user: User): Action {
return {
type: MODIFY_USER_SUCCESS,
payload: user,
itemId: user.name
};
}
export function modifyUserFailure(user: User, error: Error): Action {
return {
type: MODIFY_USER_FAILURE,
payload: {
error,
user
},
itemId: user.name
};
}
export function modifyUserReset(user: User): Action {
return {
type: MODIFY_USER_RESET,
itemId: user.name
};
}
//delete user
export function deleteUser(user: User, callback?: () => void) {
return function(dispatch: any) {
dispatch(deleteUserPending(user));
return apiClient
.delete(user._links.delete.href)
.then(() => {
dispatch(deleteUserSuccess(user));
if (callback) {
callback();
}
})
.catch(error => {
dispatch(deleteUserFailure(user, error));
});
};
}
export function deleteUserPending(user: User): Action {
return {
type: DELETE_USER_PENDING,
payload: user,
itemId: user.name
};
}
export function deleteUserSuccess(user: User): Action {
return {
type: DELETE_USER_SUCCESS,
payload: user,
itemId: user.name
};
}
export function deleteUserFailure(user: User, error: Error): Action {
return {
type: DELETE_USER_FAILURE,
payload: {
error,
user
},
itemId: user.name
};
}
function extractUsersByNames(
users: User[],
userNames: string[],
oldUsersByNames: Object
) {
const usersByNames = {};
for (let user of users) {
usersByNames[user.name] = user;
}
for (let userName in oldUsersByNames) {
usersByNames[userName] = oldUsersByNames[userName];
}
return usersByNames;
}
function deleteUserInUsersByNames(users: {}, userName: string) {
let newUsers = {};
for (let username in users) {
if (username !== userName) newUsers[username] = users[username];
}
return newUsers;
}
function deleteUserInEntries(users: [], userName: string) {
let newUsers = [];
for (let user of users) {
if (user !== userName) newUsers.push(user);
}
return newUsers;
}
const reducerByName = (state: any, username: string, newUserState: any) => {
return {
...state,
[username]: newUserState
};
};
function listReducer(state: any = {}, action: any = {}) {
switch (action.type) {
case FETCH_USERS_SUCCESS:
const users = action.payload._embedded.users;
const userNames = users.map(user => user.name);
return {
...state,
entries: userNames,
entry: {
userCreatePermission: !!action.payload._links.create,
page: action.payload.page,
pageTotal: action.payload.pageTotal,
_links: action.payload._links
}
};
// Delete single user actions
case DELETE_USER_SUCCESS:
const newUserEntries = deleteUserInEntries(
state.entries,
action.payload.name
);
return {
...state,
entries: newUserEntries
};
default:
return state;
}
}
function byNamesReducer(state: any = {}, action: any = {}) {
switch (action.type) {
// Fetch all users actions
case FETCH_USERS_SUCCESS:
const users = action.payload._embedded.users;
const userNames = users.map(user => user.name);
const byNames = extractUsersByNames(users, userNames, state.byNames);
return {
...byNames
};
// Fetch single user actions
case FETCH_USER_SUCCESS:
return reducerByName(state, action.payload.name, action.payload);
case DELETE_USER_SUCCESS:
return deleteUserInUsersByNames(
state,
action.payload.name
);
default:
return state;
}
}
export default combineReducers({
list: listReducer,
byNames: byNamesReducer
});
// selectors
const selectList = (state: Object) => {
if (state.users && state.users.list) {
return state.users.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 isPermittedToCreateUsers = (state: Object): boolean => {
return !!selectListEntry(state).userCreatePermission;
};
export function getUsersFromState(state: Object) {
const userNames = selectList(state).entries;
if (!userNames) {
return null;
}
const userEntries: User[] = [];
for (let userName of userNames) {
userEntries.push(state.users.byNames[userName]);
}
return userEntries;
}
export function isFetchUsersPending(state: Object) {
return isPending(state, FETCH_USERS);
}
export function getFetchUsersFailure(state: Object) {
return getFailure(state, FETCH_USERS);
}
export function isCreateUserPending(state: Object) {
return isPending(state, CREATE_USER);
}
export function getCreateUserFailure(state: Object) {
return getFailure(state, CREATE_USER);
}
export function getUserByName(state: Object, name: string) {
if (state.users && state.users.byNames) {
return state.users.byNames[name];
}
}
export function isFetchUserPending(state: Object, name: string) {
return isPending(state, FETCH_USER, name);
}
export function getFetchUserFailure(state: Object, name: string) {
return getFailure(state, FETCH_USER, name);
}
export function isModifyUserPending(state: Object, name: string) {
return isPending(state, MODIFY_USER, name);
}
export function getModifyUserFailure(state: Object, name: string) {
return getFailure(state, MODIFY_USER, name);
}
export function isDeleteUserPending(state: Object, name: string) {
return isPending(state, DELETE_USER, name);
}
export function getDeleteUserFailure(state: Object, name: string) {
return getFailure(state, DELETE_USER, name);
}

View File

@@ -0,0 +1,657 @@
//@flow
import configureMockStore from "redux-mock-store";
import thunk from "redux-thunk";
import fetchMock from "fetch-mock";
import reducer, {
FETCH_USERS,
FETCH_USERS_PENDING,
FETCH_USERS_SUCCESS,
FETCH_USERS_FAILURE,
FETCH_USER,
FETCH_USER_PENDING,
FETCH_USER_SUCCESS,
FETCH_USER_FAILURE,
CREATE_USER,
CREATE_USER_PENDING,
CREATE_USER_SUCCESS,
CREATE_USER_FAILURE,
MODIFY_USER,
MODIFY_USER_PENDING,
MODIFY_USER_SUCCESS,
MODIFY_USER_FAILURE,
DELETE_USER,
DELETE_USER_PENDING,
DELETE_USER_SUCCESS,
DELETE_USER_FAILURE,
fetchUsers,
getFetchUsersFailure,
getUsersFromState,
isFetchUsersPending,
fetchUsersSuccess,
fetchUserByLink,
fetchUserByName,
fetchUserSuccess,
isFetchUserPending,
getFetchUserFailure,
createUser,
isCreateUserPending,
getCreateUserFailure,
getUserByName,
modifyUser,
isModifyUserPending,
getModifyUserFailure,
deleteUser,
isDeleteUserPending,
deleteUserSuccess,
getDeleteUserFailure,
selectListAsCollection,
isPermittedToCreateUsers
} from "./users";
const userZaphod = {
active: true,
admin: true,
creationDate: "2018-07-11T12:23:49.027Z",
displayName: "Z. Beeblebrox",
mail: "president@heartofgold.universe",
name: "zaphod",
password: "",
type: "xml",
properties: {},
_links: {
self: {
href: "http://localhost:8081/api/v2/users/zaphod"
},
delete: {
href: "http://localhost:8081/api/v2/users/zaphod"
},
update: {
href: "http://localhost:8081/api/v2/users/zaphod"
}
}
};
const userFord = {
active: true,
admin: false,
creationDate: "2018-07-06T13:21:18.459Z",
displayName: "F. Prefect",
mail: "ford@prefect.universe",
name: "ford",
password: "",
type: "xml",
properties: {},
_links: {
self: {
href: "http://localhost:8081/api/v2/users/ford"
},
delete: {
href: "http://localhost:8081/api/v2/users/ford"
},
update: {
href: "http://localhost:8081/api/v2/users/ford"
}
}
};
const responseBody = {
page: 0,
pageTotal: 1,
_links: {
self: {
href: "http://localhost:3000/api/v2/users/?page=0&pageSize=10"
},
first: {
href: "http://localhost:3000/api/v2/users/?page=0&pageSize=10"
},
last: {
href: "http://localhost:3000/api/v2/users/?page=0&pageSize=10"
},
create: {
href: "http://localhost:3000/api/v2/users/"
}
},
_embedded: {
users: [userZaphod, userFord]
}
};
const response = {
headers: { "content-type": "application/json" },
responseBody
};
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");
describe("users fetch()", () => {
const mockStore = configureMockStore([thunk]);
afterEach(() => {
fetchMock.reset();
fetchMock.restore();
});
it("should successfully fetch users", () => {
fetchMock.getOnce(USERS_URL, response);
const expectedActions = [
{ type: FETCH_USERS_PENDING },
{
type: FETCH_USERS_SUCCESS,
payload: response
}
];
const store = mockStore({});
return store.dispatch(fetchUsers(URL)).then(() => {
expect(store.getActions()).toEqual(expectedActions);
});
});
it("should fail getting users on HTTP 500", () => {
fetchMock.getOnce(USERS_URL, {
status: 500
});
const store = mockStore({});
return store.dispatch(fetchUsers(URL)).then(() => {
const actions = store.getActions();
expect(actions[0].type).toEqual(FETCH_USERS_PENDING);
expect(actions[1].type).toEqual(FETCH_USERS_FAILURE);
expect(actions[1].payload).toBeDefined();
});
});
it("should sucessfully fetch single user by name", () => {
fetchMock.getOnce(USERS_URL + "/zaphod", userZaphod);
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_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);
expect(actions[1].payload).toBeDefined();
});
});
it("should fail fetching single user on HTTP 500", () => {
fetchMock.getOnce(USER_ZAPHOD_URL, {
status: 500
});
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_FAILURE);
expect(actions[1].payload).toBeDefined();
});
});
it("should add a user successfully", () => {
// unmatched
fetchMock.postOnce(USERS_URL, {
status: 204
});
// after create, the users are fetched again
fetchMock.getOnce(USERS_URL, response);
const store = mockStore({});
return store.dispatch(createUser(URL, userZaphod)).then(() => {
const actions = store.getActions();
expect(actions[0].type).toEqual(CREATE_USER_PENDING);
expect(actions[1].type).toEqual(CREATE_USER_SUCCESS);
});
});
it("should fail adding a user on HTTP 500", () => {
fetchMock.postOnce(USERS_URL, {
status: 500
});
const store = mockStore({});
return store.dispatch(createUser(URL, userZaphod)).then(() => {
const actions = store.getActions();
expect(actions[0].type).toEqual(CREATE_USER_PENDING);
expect(actions[1].type).toEqual(CREATE_USER_FAILURE);
expect(actions[1].payload).toBeDefined();
});
});
it("should call the callback after user successfully created", () => {
// unmatched
fetchMock.postOnce(USERS_URL, {
status: 204
});
let callMe = "not yet";
const callback = () => {
callMe = "yeah";
};
const store = mockStore({});
return store.dispatch(createUser(URL, userZaphod, callback)).then(() => {
expect(callMe).toBe("yeah");
});
});
it("successfully update user", () => {
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(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(USER_ZAPHOD_URL, {
status: 204
});
fetchMock.getOnce(USER_ZAPHOD_URL, userZaphod);
let called = false;
const callMe = () => {
called = true;
};
const store = mockStore({});
return store.dispatch(modifyUser(userZaphod, callMe)).then(() => {
expect(called).toBeTruthy();
});
});
it("should fail updating user on HTTP 500", () => {
fetchMock.putOnce(USER_ZAPHOD_URL, {
status: 500
});
const store = mockStore({});
return store.dispatch(modifyUser(userZaphod)).then(() => {
const actions = store.getActions();
expect(actions[0].type).toEqual(MODIFY_USER_PENDING);
expect(actions[1].type).toEqual(MODIFY_USER_FAILURE);
expect(actions[1].payload).toBeDefined();
});
});
it("should delete successfully user zaphod", () => {
fetchMock.deleteOnce(USER_ZAPHOD_URL, {
status: 204
});
const store = mockStore({});
return store.dispatch(deleteUser(userZaphod)).then(() => {
const actions = store.getActions();
expect(actions.length).toBe(2);
expect(actions[0].type).toEqual(DELETE_USER_PENDING);
expect(actions[0].payload).toBe(userZaphod);
expect(actions[1].type).toEqual(DELETE_USER_SUCCESS);
});
});
it("should call the callback, after successful delete", () => {
fetchMock.deleteOnce(USER_ZAPHOD_URL, {
status: 204
});
let called = false;
const callMe = () => {
called = true;
};
const store = mockStore({});
return store.dispatch(deleteUser(userZaphod, callMe)).then(() => {
expect(called).toBeTruthy();
});
});
it("should fail to delete user zaphod", () => {
fetchMock.deleteOnce(USER_ZAPHOD_URL, {
status: 500
});
const store = mockStore({});
return store.dispatch(deleteUser(userZaphod)).then(() => {
const actions = store.getActions();
expect(actions[0].type).toEqual(DELETE_USER_PENDING);
expect(actions[0].payload).toBe(userZaphod);
expect(actions[1].type).toEqual(DELETE_USER_FAILURE);
expect(actions[1].payload).toBeDefined();
});
});
});
describe("users reducer", () => {
it("should update state correctly according to FETCH_USERS_SUCCESS action", () => {
const newState = reducer({}, fetchUsersSuccess(responseBody));
expect(newState.list).toEqual({
entries: ["zaphod", "ford"],
entry: {
userCreatePermission: true,
page: 0,
pageTotal: 1,
_links: responseBody._links
}
});
expect(newState.byNames).toEqual({
zaphod: userZaphod,
ford: userFord
});
expect(newState.list.entry.userCreatePermission).toBeTruthy();
});
it("should set userCreatePermission to true if update link is present", () => {
const newState = reducer({}, fetchUsersSuccess(responseBody));
expect(newState.list.entry.userCreatePermission).toBeTruthy();
});
it("should not replace whole byNames map when fetching users", () => {
const oldState = {
byNames: {
ford: userFord
}
};
const newState = reducer(oldState, fetchUsersSuccess(responseBody));
expect(newState.byNames["zaphod"]).toBeDefined();
expect(newState.byNames["ford"]).toBeDefined();
});
it("should remove user from state when delete succeeds", () => {
const state = {
list: {
entries: ["ford", "zaphod"]
},
byNames: {
zaphod: userZaphod,
ford: userFord
}
};
const newState = reducer(state, deleteUserSuccess(userFord));
expect(newState.byNames["zaphod"]).toBeDefined();
expect(newState.byNames["ford"]).toBeFalsy();
expect(newState.list.entries).toEqual(["zaphod"]);
});
it("should set userCreatePermission to true if create link is present", () => {
const newState = reducer({}, fetchUsersSuccess(responseBody));
expect(newState.list.entry.userCreatePermission).toBeTruthy();
expect(newState.list.entries).toEqual(["zaphod", "ford"]);
expect(newState.byNames["ford"]).toBeTruthy();
expect(newState.byNames["zaphod"]).toBeTruthy();
});
it("should update state according to FETCH_USER_SUCCESS action", () => {
const newState = reducer({}, fetchUserSuccess(userFord));
expect(newState.byNames["ford"]).toBe(userFord);
});
it("should affect users state nor the state of other users", () => {
const newState = reducer(
{
list: {
entries: ["zaphod"]
}
},
fetchUserSuccess(userFord)
);
expect(newState.byNames["ford"]).toBe(userFord);
expect(newState.list.entries).toEqual(["zaphod"]);
});
});
describe("selector tests", () => {
it("should return an empty object", () => {
expect(selectListAsCollection({})).toEqual({});
expect(selectListAsCollection({ users: { a: "a" } })).toEqual({});
});
it("should return a state slice collection", () => {
const collection = {
page: 3,
totalPages: 42
};
const state = {
users: {
list: {
entry: collection
}
}
};
expect(selectListAsCollection(state)).toBe(collection);
});
it("should return false", () => {
expect(isPermittedToCreateUsers({})).toBe(false);
expect(isPermittedToCreateUsers({ users: { list: { entry: {} } } })).toBe(
false
);
expect(
isPermittedToCreateUsers({
users: { list: { entry: { userCreatePermission: false } } }
})
).toBe(false);
});
it("should return true", () => {
const state = {
users: {
list: {
entry: {
userCreatePermission: true
}
}
}
};
expect(isPermittedToCreateUsers(state)).toBe(true);
});
it("should get users from state", () => {
const state = {
users: {
list: {
entries: ["a", "b"]
},
byNames: {
a: { name: "a" },
b: { name: "b" }
}
}
};
expect(getUsersFromState(state)).toEqual([{ name: "a" }, { name: "b" }]);
});
it("should return true, when fetch users is pending", () => {
const state = {
pending: {
[FETCH_USERS]: true
}
};
expect(isFetchUsersPending(state)).toEqual(true);
});
it("should return false, when fetch users is not pending", () => {
expect(isFetchUsersPending({})).toEqual(false);
});
it("should return error when fetch users did fail", () => {
const state = {
failure: {
[FETCH_USERS]: error
}
};
expect(getFetchUsersFailure(state)).toEqual(error);
});
it("should return undefined when fetch users did not fail", () => {
expect(getFetchUsersFailure({})).toBe(undefined);
});
it("should return true if create user is pending", () => {
const state = {
pending: {
[CREATE_USER]: true
}
};
expect(isCreateUserPending(state)).toBe(true);
});
it("should return false if create user is not pending", () => {
const state = {
pending: {
[CREATE_USER]: false
}
};
expect(isCreateUserPending(state)).toBe(false);
});
it("should return error when create user did fail", () => {
const state = {
failure: {
[CREATE_USER]: error
}
};
expect(getCreateUserFailure(state)).toEqual(error);
});
it("should return undefined when create user did not fail", () => {
expect(getCreateUserFailure({})).toBe(undefined);
});
it("should return user ford", () => {
const state = {
users: {
byNames: {
ford: userFord
}
}
};
expect(getUserByName(state, "ford")).toEqual(userFord);
});
it("should return true, when fetch user zaphod is pending", () => {
const state = {
pending: {
[FETCH_USER + "/zaphod"]: true
}
};
expect(isFetchUserPending(state, "zaphod")).toEqual(true);
});
it("should return false, when fetch user zaphod is not pending", () => {
expect(isFetchUserPending({}, "zaphod")).toEqual(false);
});
it("should return error when fetch user zaphod did fail", () => {
const state = {
failure: {
[FETCH_USER + "/zaphod"]: error
}
};
expect(getFetchUserFailure(state, "zaphod")).toEqual(error);
});
it("should return undefined when fetch user zaphod did not fail", () => {
expect(getFetchUserFailure({}, "zaphod")).toBe(undefined);
});
it("should return true, when modify user ford is pending", () => {
const state = {
pending: {
[MODIFY_USER + "/ford"]: true
}
};
expect(isModifyUserPending(state, "ford")).toEqual(true);
});
it("should return false, when modify user ford is not pending", () => {
expect(isModifyUserPending({}, "ford")).toEqual(false);
});
it("should return error when modify user ford did fail", () => {
const state = {
failure: {
[MODIFY_USER + "/ford"]: error
}
};
expect(getModifyUserFailure(state, "ford")).toEqual(error);
});
it("should return undefined when modify user ford did not fail", () => {
expect(getModifyUserFailure({}, "ford")).toBe(undefined);
});
it("should return true, when delete user zaphod is pending", () => {
const state = {
pending: {
[DELETE_USER + "/zaphod"]: true
}
};
expect(isDeleteUserPending(state, "zaphod")).toEqual(true);
});
it("should return false, when delete user zaphod is not pending", () => {
expect(isDeleteUserPending({}, "zaphod")).toEqual(false);
});
it("should return error when delete user zaphod did fail", () => {
const state = {
failure: {
[DELETE_USER + "/zaphod"]: error
}
};
expect(getDeleteUserFailure(state, "zaphod")).toEqual(error);
});
it("should return undefined when delete user zaphod did not fail", () => {
expect(getDeleteUserFailure({}, "zaphod")).toBe(undefined);
});
});