- );
-};
-
-SectionBodyContent.propTypes = {
- profile: PropTypes.object,
- userType: PropTypes.oneOf(["user", "guest"])
-};
-
-SectionBodyContent.defaultProps = {
- profile: null,
- userType: "user"
-}
-
-export default SectionBodyContent;
\ No newline at end of file
diff --git a/products/ASC.People/Client/src/components/pages/ProfileAction/Section/Body/updateUserForm.js b/products/ASC.People/Client/src/components/pages/ProfileAction/Section/Body/updateUserForm.js
new file mode 100644
index 0000000000..5a91147cf9
--- /dev/null
+++ b/products/ASC.People/Client/src/components/pages/ProfileAction/Section/Body/updateUserForm.js
@@ -0,0 +1,233 @@
+import React from 'react'
+import { withRouter } from 'react-router'
+import { connect } from 'react-redux'
+import { Avatar, Button, Textarea, Text, toastr } from 'asc-web-components'
+import { withTranslation } from 'react-i18next';
+import { toEmployeeWrapper, getUserRole, profileEqual, updateProfile } from '../../../../../store/profile/actions';
+import { MainContainer, AvatarContainer, MainFieldsContainer, TextField, PasswordField, DateField, RadioField, DepartmentField } from './userFormFields'
+
+class UpdateUserForm extends React.Component {
+
+ constructor(props) {
+ super(props);
+
+ this.state = this.mapPropsToState(props);
+
+ this.validate = this.validate.bind(this);
+ this.handleSubmit = this.handleSubmit.bind(this);
+ this.onTextChange = this.onTextChange.bind(this);
+ this.onBirthdayDateChange = this.onBirthdayDateChange.bind(this);
+ this.onWorkFromDateChange = this.onWorkFromDateChange.bind(this);
+ this.onGroupClose = this.onGroupClose.bind(this);
+ this.onCancel = this.onCancel.bind(this);
+ }
+
+ componentDidUpdate(prevProps, prevState) {
+ if (!profileEqual(this.props.profile, prevProps.profile)) {
+ this.setState(this.mapPropsToState(this.props));
+ }
+ }
+
+ mapPropsToState = (props) => {
+ return {
+ isLoading: false,
+ errors: {
+ firstName: false,
+ lastName: false,
+ email: false,
+ password: false,
+ },
+ profile: {
+ ...{ passwordType: "link" },
+ ...toEmployeeWrapper(props.profile)
+ }
+ };
+ }
+
+ onTextChange(event) {
+ var stateCopy = Object.assign({}, this.state);
+ stateCopy.profile[event.target.name] = event.target.value;
+ this.setState(stateCopy)
+ }
+
+ onBirthdayDateChange(value) {
+ var stateCopy = Object.assign({}, this.state);
+ stateCopy.profile.birthday = value ? value.toJSON() : null;
+ this.setState(stateCopy)
+ }
+
+ onWorkFromDateChange(value) {
+ var stateCopy = Object.assign({}, this.state);
+ stateCopy.profile.workFrom = value ? value.toJSON() : null;
+ this.setState(stateCopy)
+ }
+
+ onGroupClose(id) {
+ var stateCopy = Object.assign({}, this.state);
+ stateCopy.profile.groups = this.state.groups.filter((group) => group.id !== id);
+ this.setState(stateCopy)
+ }
+
+ validate() {
+ const errors = {
+ firstName: !this.state.profile.firstName,
+ lastName: !this.state.profile.lastName,
+ email: !this.state.profile.email,
+ password: this.state.profile.passwordType === "temp" && !this.state.profile.password
+ };
+ const hasError = errors.firstName || errors.lastName || errors.email || errors.password;
+ this.setState({errors: errors});
+ return !hasError;
+ }
+
+ handleSubmit() {
+ if(!this.validate())
+ return false;
+
+ this.setState({isLoading: true});
+
+ this.props.updateProfile(this.state.profile)
+ .then(() => {
+ toastr.success("Success");
+ this.props.history.goBack();
+ })
+ .catch((error) => {
+ toastr.error(error.message)
+ this.setState({isLoading: false})
+ });
+ }
+
+ onCancel() {
+ this.props.history.goBack();
+ }
+
+ render() {
+ return (
+ <>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {this.props.t("Comments")}
+
+
+
+
+
+ >
+ );
+ };
+}
+
+const mapStateToProps = (state) => {
+ return {
+ profile: state.profile.targetUser
+ }
+};
+
+export default connect(
+ mapStateToProps,
+ {
+ updateProfile
+ }
+)(withRouter(withTranslation()(UpdateUserForm)));
\ No newline at end of file
diff --git a/products/ASC.People/Client/src/components/pages/ProfileAction/Section/Body/userFormFields.js b/products/ASC.People/Client/src/components/pages/ProfileAction/Section/Body/userFormFields.js
new file mode 100644
index 0000000000..60ce8a5f6b
--- /dev/null
+++ b/products/ASC.People/Client/src/components/pages/ProfileAction/Section/Body/userFormFields.js
@@ -0,0 +1,79 @@
+import React from 'react'
+import styled from 'styled-components';
+import { device, FieldContainer, TextInput, DateInput, RadioButtonGroup, SelectedItem } from 'asc-web-components'
+
+const MainContainer = styled.div`
+ display: flex;
+ flex-direction: row;
+
+ @media ${device.tablet} {
+ flex-direction: column;
+ }
+`;
+
+const AvatarContainer = styled.div`
+ margin: 0 32px 32px 0;
+ width: 160px;
+`;
+
+const MainFieldsContainer = styled.div`
+ flex-grow: 1;
+`;
+
+const TextField = React.memo((props) => {
+ const {isRequired, hasError, labelText, inputName, inputValue, isDisabled, onChange} = props;
+ return (
+
+
+
+ );
+});
+
+const PasswordField = React.memo((props) => {
+ const {isRequired, hasError, labelText, radioName, radioValue, radioOptions, radioIsDisabled, radioOnChange, inputName, inputValue, inputIsDisabled, inputOnChange} = props;
+ return (
+
+
+
+
+ );
+});
+
+const DateField = React.memo((props) => {
+ const {isRequired, hasError, labelText, inputName, inputValue, inputIsDisabled, inputOnChange} = props;
+ return (
+
+
+
+ );
+});
+
+const RadioField = React.memo((props) => {
+ const {isRequired, hasError, labelText, radioName, radioValue, radioOptions, radioIsDisabled, radioOnChange} = props;
+ return (
+
+
+
+ );
+});
+
+const DepartmentField = React.memo((props) => {
+ const {isRequired, hasError, labelText, departments, onClose} = props;
+ return (
+ departments && departments.length
+ ?
+ {departments.map((department, index) => (
+ { onClose(department.id) }}
+ isInline={true}
+ style={{ marginRight: "8px", marginBottom: "8px" }}
+ />
+ ))}
+
+ : ""
+ );
+});
+
+export { MainContainer, AvatarContainer, MainFieldsContainer, TextField, PasswordField, DateField, RadioField, DepartmentField }
\ No newline at end of file
diff --git a/products/ASC.People/Client/src/components/pages/ProfileAction/Section/Header/index.js b/products/ASC.People/Client/src/components/pages/ProfileAction/Section/Header/index.js
index faa48da08c..0e49cc2ba9 100644
--- a/products/ASC.People/Client/src/components/pages/ProfileAction/Section/Header/index.js
+++ b/products/ASC.People/Client/src/components/pages/ProfileAction/Section/Header/index.js
@@ -1,50 +1,44 @@
-import React from 'react';
+import React, { useCallback } from 'react';
+import styled from 'styled-components';
import { connect } from 'react-redux';
import { withRouter } from "react-router";
-import PropTypes from "prop-types";
import { IconButton, Text } from 'asc-web-components';
import { useTranslation } from 'react-i18next';
-const wrapperStyle = {
- display: "flex",
- alignItems: "center"
-};
+const Wrapper = styled.div`
+ display: flex;
+ align-Items: center;
+`;
-const textStyle = {
- marginLeft: "16px"
-};
+const Header = styled(Text.ContentHeader)`
+ margin-left: 16px;
+`;
const SectionHeaderContent = (props) => {
- const {profile, history, userType, settings} = props;
+ const {profile, history, settings} = props;
const { t } = useTranslation();
const headerText = profile && profile.displayName
? profile.displayName
- : userType === "user"
- ? t('NewEmployee')
- : t('NewGuest');
+ : profile.isVisitor
+ ? t('NewGuest')
+ : t('NewEmployee');
+
+ const onClick = useCallback(() => {
+ history.push(settings.homepage)
+ }, [history, settings]);
return (
-
- history.push(settings.homepage)}/>
- {headerText}
-
+
+
+
+
);
};
-SectionHeaderContent.propTypes = {
- profile: PropTypes.object,
- history: PropTypes.object.isRequired,
- userType: PropTypes.oneOf(["user", "guest"])
-};
-
-SectionHeaderContent.defaultProps = {
- profile: null,
- userType: "user"
-};
-
function mapStateToProps(state) {
return {
+ profile: state.profile.targetUser,
settings: state.auth.settings
};
};
diff --git a/products/ASC.People/Client/src/components/pages/ProfileAction/Section/index.js b/products/ASC.People/Client/src/components/pages/ProfileAction/Section/index.js
index 0acbfe9dd2..e1f05c7713 100644
--- a/products/ASC.People/Client/src/components/pages/ProfileAction/Section/index.js
+++ b/products/ASC.People/Client/src/components/pages/ProfileAction/Section/index.js
@@ -1,2 +1,3 @@
-export { default as SectionHeaderContent } from './Header';
-export { default as SectionBodyContent } from './Body';
\ No newline at end of file
+export { default as SectionHeaderContent } from './Header';
+export { default as CreateUserForm } from './Body/createUserForm';
+export { default as UpdateUserForm } from './Body/updateUserForm';
\ No newline at end of file
diff --git a/products/ASC.People/Client/src/components/pages/ProfileAction/index.js b/products/ASC.People/Client/src/components/pages/ProfileAction/index.js
index 75e07a7096..73e87f50e3 100644
--- a/products/ASC.People/Client/src/components/pages/ProfileAction/index.js
+++ b/products/ASC.People/Client/src/components/pages/ProfileAction/index.js
@@ -3,24 +3,19 @@ import { connect } from "react-redux";
import PropTypes from "prop-types";
import { PageLayout, Loader } from "asc-web-components";
import { ArticleHeaderContent, ArticleMainButtonContent, ArticleBodyContent } from '../../Article';
-import { SectionHeaderContent, SectionBodyContent } from './Section';
+import { SectionHeaderContent, CreateUserForm, UpdateUserForm } from './Section';
import { setProfile, fetchProfile, resetProfile } from '../../../store/profile/actions';
import i18n from "./i18n";
import { I18nextProvider } from "react-i18next";
class ProfileAction extends React.Component {
- constructor(props) {
- super(props);
- }
-
componentDidMount() {
const { match, setProfile, fetchProfile } = this.props;
const { userId, type } = match.params;
if (!userId) {
setProfile({ isVisitor: type === "guest" });
- }
- else {
+ } else {
fetchProfile(userId);
}
}
@@ -33,37 +28,31 @@ class ProfileAction extends React.Component {
if (!userId && type !== prevType) {
setProfile({ isVisitor: type === "guest" });
- }
- else if (userId !== prevUserId) {
+ } else if (userId !== prevUserId) {
fetchProfile(userId);
}
}
- componentWillUnmount() {
- this.props.resetProfile();
- }
-
render() {
console.log("ProfileAction render")
- const { profile, match } = this.props;
- const { type } = match.params;
+ const { profile } = this.props;
return (
{profile
- ? }
- articleMainButtonContent={}
- articleBodyContent={}
- sectionHeaderContent={}
- sectionBodyContent={}
- />
- : }
- articleMainButtonContent={}
- articleBodyContent={}
- sectionBodyContent={}
+ ? }
+ articleMainButtonContent={}
+ articleBodyContent={}
+ sectionHeaderContent={}
+ sectionBodyContent={profile.id ? : }
+ />
+ : }
+ articleMainButtonContent={}
+ articleBodyContent={}
+ sectionBodyContent={}
/>}
);
@@ -73,8 +62,9 @@ class ProfileAction extends React.Component {
ProfileAction.propTypes = {
match: PropTypes.object.isRequired,
profile: PropTypes.object,
+ setProfile: PropTypes.func.isRequired,
fetchProfile: PropTypes.func.isRequired,
- setProfile: PropTypes.func.isRequired
+ resetProfile: PropTypes.func.isRequired
};
function mapStateToProps(state) {
diff --git a/products/ASC.People/Client/src/components/pages/ProfileAction/locales/en/translation.json b/products/ASC.People/Client/src/components/pages/ProfileAction/locales/en/translation.json
index 7e6d0efcb6..1190276c9d 100644
--- a/products/ASC.People/Client/src/components/pages/ProfileAction/locales/en/translation.json
+++ b/products/ASC.People/Client/src/components/pages/ProfileAction/locales/en/translation.json
@@ -18,5 +18,9 @@
"SexFemale": "Female",
"RequiredField": "Required field",
"NewEmployee": "New employee",
- "NewGuest": "New guest"
+ "NewGuest": "New guest",
+
+ "EmployedSinceDate": "EmployedSinceDate",
+ "Position": "Position",
+ "Departments": "Departments"
}
\ No newline at end of file
diff --git a/products/ASC.People/Client/src/store/people/actions.js b/products/ASC.People/Client/src/store/people/actions.js
index 966c10477a..d0ed18a329 100644
--- a/products/ASC.People/Client/src/store/people/actions.js
+++ b/products/ASC.People/Client/src/store/people/actions.js
@@ -94,6 +94,16 @@ export function fetchPeople(filter) {
});
};
}
+
+export function fetchPeopleByFilter(dispatch, filter) {
+ let filterData = (filter && filter.clone()) || Filter.getDefault();
+ return api.getUserList(filterData).then(res => {
+ filterData.total = res.data.total;
+ dispatch(setFilter(filterData));
+ return dispatch(setUsers(res.data.response));
+ });
+}
+
export async function fetchPeopleAsync(dispatch, filter = null) {
let filterData = (filter && filter.clone()) || Filter.getDefault();
diff --git a/products/ASC.People/Client/src/store/profile/actions.js b/products/ASC.People/Client/src/store/profile/actions.js
index 4a30c3f97f..b469851c53 100644
--- a/products/ASC.People/Client/src/store/profile/actions.js
+++ b/products/ASC.People/Client/src/store/profile/actions.js
@@ -1,7 +1,7 @@
import * as api from "../../store/services/api";
import { isMe } from '../auth/selectors';
import { getUserByUserName } from '../people/selectors';
-import { fetchPeopleAsync } from "../people/actions";
+import { fetchPeopleByFilter } from "../people/actions";
export const SET_PROFILE = 'SET_PROFILE';
export const CLEAN_PROFILE = 'CLEAN_PROFILE';
@@ -19,59 +19,119 @@ export function resetProfile() {
};
};
-function checkResponseError(res) {
- if(res && res.data && res.data.error){
+export function checkResponseError(res) {
+ if (res && res.data && res.data.error) {
console.error(res.data.error);
throw new Error(res.data.error.message);
}
}
-export function fetchProfile(userName) {
- return async (dispatch, getState) => {
- try {
- const { auth, people } = getState();
-
- if (isMe(auth.user, userName)) {
- dispatch(setProfile(auth.user));
- } else {
- const user = getUserByUserName(people.users, userName);
- if (!user) {
- const res = await api.getUser(userName);
- dispatch(setProfile(res.data.response))
- }
- else
- dispatch(setProfile(user));
- }
- } catch (error) {
- console.error(error);
- }
- };
+export function getUserRole(profile) {
+ if(profile.isOwner) return "owner";
+ if(profile.isAdmin) return "admin";
+ if(profile.isVisitor) return "guest";
+ return "user";
};
+export function profileEqual(profileA, profileB) {
+ const keys = Object.keys(profileA);
+
+ for (let i = 0; i < keys.length; i++) {
+ let key = keys[i];
+
+ if (key === "groups") {
+ if (profileA[key].length !== profileB[key].length)
+ return false;
+
+ const groupsA = profileA[key].map(group => group.id);
+ const groupsB = profileA[key].map(group => group.id);
+
+ for (let j = 0; j < groupsA.length; j++) {
+ if (!groupsB.includes(groupsA[j]))
+ return false;
+ }
+ }
+
+ if(profileA[key] !== profileB[key])
+ return false;
+ }
+
+ return true;
+}
+
+export function toEmployeeWrapper(profile) {
+ const emptyData = {
+ id: "",
+ firstName: "",
+ lastName: "",
+ email: "",
+ password: "",
+ birthday: "",
+ sex: "male",
+ workFrom: "",
+ location: "",
+ title: "",
+ groups: [],
+ notes: ""
+ };
+
+ return { ...emptyData, ...profile };
+}
+
+export function employeeWrapperToMemberModel(profile) {
+ const comment = profile.notes;
+ const department = profile.groups ? profile.groups.map(group => group.id) : [];
+ const worksFrom = profile.workFrom;
+
+ return {...profile, comment, department, worksFrom};
+}
+
+export function fetchProfile(userName) {
+ return (dispatch, getState) => {
+ const { auth, people } = getState();
+
+ if (isMe(auth.user, userName)) {
+ dispatch(setProfile(auth.user));
+ } else {
+ const user = getUserByUserName(people.users, userName);
+ if (!user) {
+ api.getUser(userName).then(res => {
+ checkResponseError(res);
+ dispatch(setProfile(res.data.response));
+ });
+ } else {
+ dispatch(setProfile(user));
+ }
+ }
+ };
+}
+
export function createProfile(profile) {
- return async (dispatch, getState) => {
+ return (dispatch, getState) => {
const {people} = getState();
const {filter} = people;
+ const member = employeeWrapperToMemberModel(profile);
- const res = await api.createUser(profile);
-
- checkResponseError(res);
-
- dispatch(setProfile(res.data.response))
- await fetchPeopleAsync(dispatch, filter);
+ return api.createUser(member).then(res => {
+ checkResponseError(res);
+ return Promise.resolve(dispatch(setProfile(res.data.response)));
+ }).then(() => {
+ return fetchPeopleByFilter(dispatch, filter);
+ });
};
};
export function updateProfile(profile) {
- return async (dispatch, getState) => {
+ return (dispatch, getState) => {
const {people} = getState();
const {filter} = people;
+ const member = employeeWrapperToMemberModel(profile);
- const res = await api.updateUser(profile);
-
- checkResponseError(res);
-
- dispatch(setProfile(res.data.response))
- await fetchPeopleAsync(dispatch, filter);
+ return api.updateUser(member).then(res => {
+ checkResponseError(res);
+ return Promise.resolve(dispatch(setProfile(res.data.response)));
+ }).then(() => {
+ return fetchPeopleByFilter(dispatch, filter);
+ });
};
};
\ No newline at end of file
diff --git a/web/ASC.Web.Components/package.json b/web/ASC.Web.Components/package.json
index eb29848138..e7d2548ac0 100644
--- a/web/ASC.Web.Components/package.json
+++ b/web/ASC.Web.Components/package.json
@@ -1,6 +1,6 @@
{
"name": "asc-web-components",
- "version": "1.0.15",
+ "version": "1.0.16",
"description": "Ascensio System SIA component library",
"license": "AGPL-3.0",
"main": "dist/asc-web-components.cjs.js",
diff --git a/web/ASC.Web.Components/src/components/field-container/index.js b/web/ASC.Web.Components/src/components/field-container/index.js
new file mode 100644
index 0000000000..a601237a26
--- /dev/null
+++ b/web/ASC.Web.Components/src/components/field-container/index.js
@@ -0,0 +1,57 @@
+import React from 'react'
+import styled from 'styled-components';
+import device from '../device'
+import Label from '../label'
+
+const Container = styled.div`
+ display: flex;
+ flex-direction: row;
+ margin: 0 0 16px 0;
+
+ .field-label {
+ line-height: 32px;
+ margin: 0;
+ width: 110px;
+ }
+
+ .field-input {
+ width: 320px;
+ }
+
+ .radio-group {
+ line-height: 32px;
+ display: flex;
+
+ label:not(:first-child) {
+ margin-left: 33px;
+ }
+ }
+
+ @media ${device.tablet} {
+ flex-direction: column;
+ align-items: start;
+
+ .field-label {
+ line-height: unset;
+ margin: 0 0 4px 0;
+ width: auto;
+ flex-grow: 1;
+ }
+ }
+`;
+
+const Body = styled.div`
+ flex-grow: 1;
+`;
+
+const FieldContainer = React.memo((props) => {
+ const {isRequired, hasError, labelText, className, children} = props;
+ return (
+
+
+ {children}
+
+ );
+});
+
+export default FieldContainer
\ No newline at end of file
diff --git a/web/ASC.Web.Components/src/index.js b/web/ASC.Web.Components/src/index.js
index 7169901e94..e80eea73e4 100644
--- a/web/ASC.Web.Components/src/index.js
+++ b/web/ASC.Web.Components/src/index.js
@@ -50,4 +50,5 @@ export { default as EmptyScreenContainer} from './components/empty-screen-contai
export { default as CustomScrollbarsVirtualList } from './components/scrollbar/custom-scrollbars-virtual-list'
export { default as RowContent } from './components/row-content'
export { default as NewCalendar } from './components/calendar-new'
-export { default as AdvancedSelector } from './components/advanced-selector'
\ No newline at end of file
+export { default as AdvancedSelector } from './components/advanced-selector'
+export { default as FieldContainer } from './components/field-container'
\ No newline at end of file
diff --git a/web/ASC.Web.Storybook/stories/field-container/base/README.md b/web/ASC.Web.Storybook/stories/field-container/base/README.md
new file mode 100644
index 0000000000..631f610b3b
--- /dev/null
+++ b/web/ASC.Web.Storybook/stories/field-container/base/README.md
@@ -0,0 +1,29 @@
+# FieldContainer
+
+## Usage
+
+```js
+import { FieldContainer } from 'asc-web-components';
+```
+
+###