create own endpoints for user conversion and apply changed workflow to user form in ui

This commit is contained in:
Eduard Heimbuch
2020-10-22 13:15:54 +02:00
parent 69950f63b3
commit 5b10dc57ac
12 changed files with 296 additions and 67 deletions

View File

@@ -0,0 +1,130 @@
/*
* MIT License
*
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
import React, { FC, useState } from "react";
import {
Button,
Modal,
PasswordConfirmation,
SubmitButton,
ErrorNotification,
Level
} from "@scm-manager/ui-components";
import { useTranslation } from "react-i18next";
import { Link, User } from "@scm-manager/ui-types";
import { convertToExternal, convertToInternal } from "./convertUser";
import styled from "styled-components";
const ExternalDescription = styled.div`
display: flex;
align-items: center;
font-size: 1.25rem;
font-weight: 400;
`;
type Props = {
user: User;
fetchUser: (user: User) => void;
};
const UserConverter: FC<Props> = ({ user, fetchUser }) => {
const [t] = useTranslation("users");
const [showPasswordModal, setShowPasswordModal] = useState(false);
const [password, setPassword] = useState("");
const [passwordValid, setPasswordValid] = useState(false);
const [error, setError] = useState<Error | undefined>();
const toInternal = () => {
convertToInternal((user._links.convertToInternal as Link).href, password)
.then(() => fetchUser(user))
.then(() => setShowPasswordModal(false))
.catch(setError);
};
const toExternal = () => {
convertToExternal((user._links.convertToExternal as Link).href)
.then(() => fetchUser(user))
.catch(setError);
};
const changePassword = (password: string, valid: boolean) => {
setPassword(password);
setPasswordValid(valid);
};
const getUserExternalDescription = () => {
if (user.external) {
return t("userForm.userIsExternal");
} else {
return t("userForm.userIsInternal");
}
};
const getConvertButton = () => {
if (user.external) {
return (
<Button
label={t("userForm.button.convertToInternal")}
action={() => setShowPasswordModal(true)}
icon="exchange-alt"
className="is-pulled-right"
/>
);
} else {
return <Button label={t("userForm.button.convertToExternal")} action={() => toExternal()} icon="exchange-alt" />;
}
};
const passwordChangeField = <PasswordConfirmation passwordChanged={changePassword} />;
const passwordModal = (
<Modal
body={passwordChangeField}
closeFunction={() => setShowPasswordModal(false)}
active={showPasswordModal}
title={t("userForm.modal.passwordRequired")}
footer={
<SubmitButton
action={() => password && passwordValid && toInternal()}
disabled={!passwordValid}
scrollToTop={false}
label={t("userForm.modal.convertToInternal")}
/>
}
/>
);
return (
<div>
{showPasswordModal && passwordModal}
{error && <ErrorNotification error={error} />}
<div className="columns is-multiline">
<ExternalDescription className="column is-half">{getUserExternalDescription()}</ExternalDescription>
<div className="column is-half">
<Level right={getConvertButton()} />
</div>
</div>
</div>
);
};
export default UserConverter;

View File

@@ -29,14 +29,12 @@ import {
ErrorNotification,
InputField,
Level,
Modal,
PasswordConfirmation,
SubmitButton,
Subtitle,
validation as validator
} from "@scm-manager/ui-components";
import * as userValidator from "./userValidation";
import { setPassword } from "./setPassword";
type Props = WithTranslation & {
submitForm: (p: User) => void;
@@ -50,7 +48,6 @@ type State = {
nameValidationError: boolean;
displayNameValidationError: boolean;
passwordValid: boolean;
showPasswordModal: boolean;
error?: Error;
};
@@ -71,8 +68,7 @@ class UserForm extends React.Component<Props, State> {
mailValidationError: false,
displayNameValidationError: false,
nameValidationError: false,
passwordValid: false,
showPasswordModal: false
passwordValid: false
};
}
@@ -119,43 +115,22 @@ class UserForm extends React.Component<Props, State> {
this.state.mailValidationError ||
this.state.displayNameValidationError ||
this.state.nameValidationError ||
!user.displayName ||
(this.props.user?.external && !user.external && !user.password)
!user.displayName
);
};
submit = (event: Event) => {
const { user, passwordValid } = this.state;
event.preventDefault();
if (!this.isInvalid()) {
this.props.submitForm(this.state.user);
if (user.password && passwordValid && user._links?.password) {
setPassword((user._links.password as Link).href, user.password).catch(error => this.setState({ error }));
}
}
};
render() {
const { loading, t } = this.props;
const { user, showPasswordModal, passwordValid, error } = this.state;
const { user, error } = this.state;
const passwordChangeField = <PasswordConfirmation passwordChanged={this.handlePasswordChange} />;
const passwordModal = (
<Modal
body={passwordChangeField}
closeFunction={() => this.setState({ user: { ...user, external: true } }, () => this.showPasswordModal(false))}
active={showPasswordModal}
title={t("userForm.modal.passwordRequired")}
footer={
<SubmitButton
action={() => !!user.password && passwordValid && this.showPasswordModal(false)}
disabled={!this.state.passwordValid}
scrollToTop={false}
label={t("userForm.modal.setPassword")}
/>
}
/>
);
let nameField = null;
let subtitle = null;
if (!this.props.user) {
@@ -180,7 +155,6 @@ class UserForm extends React.Component<Props, State> {
return (
<>
{subtitle}
{showPasswordModal && passwordModal}
<form onSubmit={this.submit}>
<div className="columns is-multiline">
{nameField}
@@ -204,14 +178,6 @@ class UserForm extends React.Component<Props, State> {
helpText={t("help.mailHelpText")}
/>
</div>
<div className="column is-full">
<Checkbox
label={t("user.externalFlag")}
onChange={this.handleExternalFlagChange}
checked={!!user?.external}
helpText={t("help.externalFlagHelpText")}
/>
</div>
</div>
{!user.external && (
<>
@@ -229,16 +195,12 @@ class UserForm extends React.Component<Props, State> {
</>
)}
{error && <ErrorNotification error={error} />}
<Level right={<SubmitButton disabled={this.isInvalid()} loading={loading} label={t("userForm.button")} />} />
<Level right={<SubmitButton disabled={this.isInvalid()} loading={loading} label={t("userForm.button.submit")} />} />
</form>
</>
);
}
showPasswordModal = (showPasswordModal: boolean) => {
this.setState({ showPasswordModal });
};
handleUsernameChange = (name: string) => {
this.setState({
nameValidationError: !validator.isNameValid(name),
@@ -287,19 +249,6 @@ class UserForm extends React.Component<Props, State> {
}
});
};
handleExternalFlagChange = (external: boolean) => {
this.setState(
{
user: {
...this.state.user,
external
}
},
//Only show password modal if edit mode and external flag was changed to internal and password was not already set
() => !external && this.props.user?.external && !this.state.user.password && this.showPasswordModal(true)
);
};
}
export default withTranslation("users")(UserForm);

View File

@@ -0,0 +1,46 @@
/*
* MIT License
*
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
import { apiClient } from "@scm-manager/ui-components";
import { CONTENT_TYPE_USER } from "../modules/users";
export function convertToInternal(url: string, newPassword: string) {
return apiClient
.put(
url,
{
newPassword
},
CONTENT_TYPE_USER
)
.then(response => {
return response;
});
}
export function convertToExternal(url: string) {
return apiClient.put(url, {}, CONTENT_TYPE_USER).then(response => {
return response;
});
}

View File

@@ -43,7 +43,7 @@ class ChangePasswordNavLink extends React.Component<Props> {
hasPermissionToSetPassword = () => {
const { user } = this.props;
return !user.external && user._links.password;
return user._links.password;
};
}

View File

@@ -27,10 +27,17 @@ import { withRouter } from "react-router-dom";
import UserForm from "../components/UserForm";
import DeleteUser from "./DeleteUser";
import { User } from "@scm-manager/ui-types";
import { getModifyUserFailure, isModifyUserPending, modifyUser, modifyUserReset } from "../modules/users";
import {
fetchUserByLink,
getModifyUserFailure,
isModifyUserPending,
modifyUser,
modifyUserReset
} from "../modules/users";
import { History } from "history";
import { ErrorNotification } from "@scm-manager/ui-components";
import { compose } from "redux";
import UserConverter from "../components/UserConverter";
type Props = {
loading: boolean;
@@ -39,6 +46,7 @@ type Props = {
// dispatch functions
modifyUser: (user: User, callback?: () => void) => void;
modifyUserReset: (p: User) => void;
fetchUser: (user: User) => void;
// context objects
user: User;
@@ -64,7 +72,9 @@ class EditUser extends React.Component<Props> {
return (
<div>
<ErrorNotification error={error} />
<UserForm submitForm={user => this.modifyUser(user)} user={user} loading={loading} />
<UserForm submitForm={this.modifyUser} user={user} loading={loading} />
<hr />
<UserConverter user={user} fetchUser={this.props.fetchUser} />
<DeleteUser user={user} />
</div>
);
@@ -87,6 +97,9 @@ const mapDispatchToProps = (dispatch: any) => {
},
modifyUserReset: (user: User) => {
dispatch(modifyUserReset(user));
},
fetchUser: (user: User) => {
dispatch(fetchUserByLink(user));
}
};
};

View File

@@ -56,7 +56,7 @@ 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";
export const CONTENT_TYPE_USER = "application/vnd.scmm-user+json;v=2";
// TODO i18n for error messages