mirror of
https://github.com/scm-manager/scm-manager.git
synced 2025-11-13 08:55:44 +01:00
scm-ui: new repository layout
This commit is contained in:
137
scm-ui/ui-webapp/src/users/components/SetUserPassword.js
Normal file
137
scm-ui/ui-webapp/src/users/components/SetUserPassword.js
Normal 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);
|
||||
225
scm-ui/ui-webapp/src/users/components/UserForm.js
Normal file
225
scm-ui/ui-webapp/src/users/components/UserForm.js
Normal 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);
|
||||
@@ -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);
|
||||
@@ -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("");
|
||||
});
|
||||
@@ -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);
|
||||
@@ -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("");
|
||||
});
|
||||
@@ -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);
|
||||
@@ -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("");
|
||||
});
|
||||
3
scm-ui/ui-webapp/src/users/components/navLinks/index.js
Normal file
3
scm-ui/ui-webapp/src/users/components/navLinks/index.js
Normal file
@@ -0,0 +1,3 @@
|
||||
export { default as EditUserNavLink } from "./EditUserNavLink";
|
||||
export { default as SetPasswordNavLink } from "./SetPasswordNavLink";
|
||||
export { default as SetPermissionsNavLink } from "./SetPermissionsNavLink";
|
||||
13
scm-ui/ui-webapp/src/users/components/setPassword.js
Normal file
13
scm-ui/ui-webapp/src/users/components/setPassword.js
Normal 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;
|
||||
});
|
||||
}
|
||||
25
scm-ui/ui-webapp/src/users/components/setPassword.test.js
Normal file
25
scm-ui/ui-webapp/src/users/components/setPassword.test.js
Normal 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();
|
||||
});
|
||||
});
|
||||
});
|
||||
60
scm-ui/ui-webapp/src/users/components/table/Details.js
Normal file
60
scm-ui/ui-webapp/src/users/components/table/Details.js
Normal 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);
|
||||
43
scm-ui/ui-webapp/src/users/components/table/UserRow.js
Normal file
43
scm-ui/ui-webapp/src/users/components/table/UserRow.js
Normal 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);
|
||||
34
scm-ui/ui-webapp/src/users/components/table/UserTable.js
Normal file
34
scm-ui/ui-webapp/src/users/components/table/UserTable.js
Normal 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);
|
||||
3
scm-ui/ui-webapp/src/users/components/table/index.js
Normal file
3
scm-ui/ui-webapp/src/users/components/table/index.js
Normal file
@@ -0,0 +1,3 @@
|
||||
export { default as Details } from "./Details";
|
||||
export { default as UserRow } from "./UserRow";
|
||||
export { default as UserTable } from "./UserTable";
|
||||
17
scm-ui/ui-webapp/src/users/components/userValidation.js
Normal file
17
scm-ui/ui-webapp/src/users/components/userValidation.js
Normal 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;
|
||||
};
|
||||
40
scm-ui/ui-webapp/src/users/components/userValidation.test.js
Normal file
40
scm-ui/ui-webapp/src/users/components/userValidation.test.js
Normal 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);
|
||||
}
|
||||
});
|
||||
});
|
||||
91
scm-ui/ui-webapp/src/users/containers/CreateUser.js
Normal file
91
scm-ui/ui-webapp/src/users/containers/CreateUser.js
Normal 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));
|
||||
113
scm-ui/ui-webapp/src/users/containers/DeleteUser.js
Normal file
113
scm-ui/ui-webapp/src/users/containers/DeleteUser.js
Normal 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)));
|
||||
84
scm-ui/ui-webapp/src/users/containers/EditUser.js
Normal file
84
scm-ui/ui-webapp/src/users/containers/EditUser.js
Normal 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));
|
||||
179
scm-ui/ui-webapp/src/users/containers/SingleUser.js
Normal file
179
scm-ui/ui-webapp/src/users/containers/SingleUser.js
Normal 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));
|
||||
158
scm-ui/ui-webapp/src/users/containers/Users.js
Normal file
158
scm-ui/ui-webapp/src/users/containers/Users.js
Normal 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));
|
||||
478
scm-ui/ui-webapp/src/users/modules/users.js
Normal file
478
scm-ui/ui-webapp/src/users/modules/users.js
Normal 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);
|
||||
}
|
||||
657
scm-ui/ui-webapp/src/users/modules/users.test.js
Normal file
657
scm-ui/ui-webapp/src/users/modules/users.test.js
Normal 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);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user