Merge branch 'master' of github.com:ONLYOFFICE/CommunityServer-AspNetCore

This commit is contained in:
Ilya Oleshko 2019-09-02 14:15:39 +03:00
commit 4d9aa30ab5
14 changed files with 156 additions and 90 deletions

View File

@ -20,6 +20,7 @@
"oidc-client": "^1.9.0",
"prop-types": "^15.7.2",
"react": "^16.9.0",
"react-device-detect": "^1.7.5",
"react-dom": "^16.9.0",
"react-i18next": "10.12.2",
"react-redux": "7.1.1",

View File

@ -31,6 +31,8 @@ import {
} from "../../../../../store/people/selectors";
import { isAdmin, isMe } from "../../../../../store/auth/selectors";
import { EmployeeStatus } from "../../../../../helpers/constants";
import { resendUserInvites } from "../../../../../store/services/api";
import { isMobileOnly } from "react-device-detect";
class SectionBodyContent extends React.PureComponent {
constructor(props) {
@ -51,8 +53,8 @@ class SectionBodyContent extends React.PureComponent {
window.open("mailto:" + email);
};
onSendMessageClick = () => {
toastr.success("Context action: Send message");
onSendMessageClick = mobilePhone => {
window.open(`sms:${mobilePhone}`);
};
onEditClick = user => {
@ -263,8 +265,13 @@ class SectionBodyContent extends React.PureComponent {
});
};
onInviteAgainClick = () => {
toastr.success("Context action: Invite again");
onInviteAgainClick = user => {
const { onLoading } = this.props;
onLoading(true);
resendUserInvites([user.id])
.then(() => toastr.success(<Text.Body>The email activation instructions have been sent to the <b>{user.email}</b> email address</Text.Body>))
.catch(e => toastr.error("ERROR"))
.finally(() => onLoading(false));
};
getUserContextOptions = (user, viewer) => {
let status = "";
@ -288,10 +295,11 @@ class SectionBodyContent extends React.PureComponent {
label: t("LblSendEmail"),
onClick: this.onEmailSentClick.bind(this, user.email)
},
user.mobilePhone && isMobileOnly &&
{
key: "send-message",
label: t("LblSendMessage"),
onClick: this.onSendMessageClick
onClick: this.onSendMessageClick.bind(this, user.mobilePhone)
},
{ key: "separator", isSeparator: true },
{
@ -354,7 +362,7 @@ class SectionBodyContent extends React.PureComponent {
{
key: "invite-again",
label: t("LblInviteAgain"),
onClick: this.onInviteAgainClick
onClick: this.onInviteAgainClick.bind(this, user)
},
!isSelf &&
(user.status === EmployeeStatus.Active

View File

@ -7,6 +7,7 @@ import { withTranslation } from 'react-i18next';
import { updateUserStatus, updateUserType } from '../../../../../store/people/actions';
import { EmployeeStatus, EmployeeType } from '../../../../../helpers/constants';
import { typeUser , typeGuest } from '../../../../../helpers/../helpers/customNames';
import { resendUserInvites } from '../../../../../store/services/api';
const contextOptions = ( t ) => {
return [
@ -65,6 +66,14 @@ const SectionHeaderContent = React.memo(({
toastr.success(t('SuccessChangeUserType'));
}, [selectedUserIds, updateUserType, t]);
const onSentInviteAgain = useCallback(() => {
resendUserInvites(selectedUserIds)
.then(() =>
toastr.success("The invitation was successfully sent")
)
.catch(e => toastr.error("ERROR"));
}, [selectedUserIds]);
const menuItems = [
{
label: t('LblSelect'),
@ -102,7 +111,7 @@ const SectionHeaderContent = React.memo(({
{
label: t('LblInviteAgain'),
disabled: !selection.length,
onClick: toastr.success.bind(this, "Invite again action")
onClick: onSentInviteAgain
},
{
label: t('LblSendEmail'),

View File

@ -4,14 +4,11 @@ import PropTypes from "prop-types";
import { PageLayout, Loader } from "asc-web-components";
import { ArticleHeaderContent, ArticleMainButtonContent, ArticleBodyContent } from '../../Article';
import { SectionHeaderContent, SectionBodyContent } from './Section';
import { setProfile, fetchProfile, resetProfile } from '../../../store/profile/actions';
import { fetchProfile } from '../../../store/profile/actions';
import i18n from "./i18n";
import { I18nextProvider } from "react-i18next";
class Profile extends React.Component {
constructor(props) {
super(props);
}
componentDidMount() {
const { match, fetchProfile } = this.props;
@ -66,6 +63,7 @@ Profile.propTypes = {
history: PropTypes.object.isRequired,
match: PropTypes.object.isRequired,
profile: PropTypes.object,
fetchProfile: PropTypes.func.isRequired,
isLoaded: PropTypes.bool
};
@ -76,7 +74,5 @@ function mapStateToProps(state) {
}
export default connect(mapStateToProps, {
setProfile,
fetchProfile,
resetProfile
fetchProfile
})(Profile);

View File

@ -17,7 +17,7 @@ const DepartmentField = React.memo((props) => {
hasError={hasError}
labelText={labelText}
>
{departments.map((department) => (
{departments && departments.map((department) => (
<SelectedItem
key={`department_${department.id}`}
text={department.name}

View File

@ -3,7 +3,7 @@ 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, createProfile } from '../../../../../store/profile/actions';
import { toEmployeeWrapper, getUserRole, createProfile } from '../../../../../store/profile/actions';
import { MainContainer, AvatarContainer, MainFieldsContainer } from './FormFields/Form'
import TextField from './FormFields/TextField'
import PasswordField from './FormFields/PasswordField'
@ -30,12 +30,14 @@ class CreateUserForm extends React.Component {
}
componentDidUpdate(prevProps, prevState) {
if (!profileEqual(this.props.profile, prevProps.profile)) {
if (this.props.match.params.type !== prevProps.match.params.type) {
this.setState(this.mapPropsToState(this.props));
}
}
mapPropsToState = (props) => {
const isVisitor = props.match.params.type === "guest";
return {
isLoading: false,
showPassword: false,
@ -45,10 +47,7 @@ class CreateUserForm extends React.Component {
email: false,
password: false,
},
profile: {
...{ passwordType: "link" },
...toEmployeeWrapper(props.profile)
}
profile: toEmployeeWrapper({ isVisitor: isVisitor})
};
}
@ -99,9 +98,9 @@ class CreateUserForm extends React.Component {
this.setState({isLoading: true});
this.props.createProfile(this.state.profile)
.then(() => {
.then((profile) => {
toastr.success("Success");
this.props.history.goBack();
this.props.history.push(`${this.props.settings.homepage}/view/${profile.userName}`);
})
.catch((error) => {
toastr.error(error.message)
@ -110,7 +109,7 @@ class CreateUserForm extends React.Component {
}
onCancel() {
this.props.history.goBack();
this.props.history.push(this.props.settings.homepage)
}
render() {
@ -236,7 +235,7 @@ class CreateUserForm extends React.Component {
const mapStateToProps = (state) => {
return {
profile: state.profile.targetUser
settings: state.auth.settings
}
};

View File

@ -3,7 +3,7 @@ import { withRouter } from 'react-router'
import { connect } from 'react-redux'
import { Avatar, Button, Textarea, Text, toastr, ModalDialog } from 'asc-web-components'
import { withTranslation } from 'react-i18next';
import { toEmployeeWrapper, getUserRole, profileEqual, updateProfile } from '../../../../../store/profile/actions';
import { toEmployeeWrapper, getUserRole, updateProfile } from '../../../../../store/profile/actions';
import { MainContainer, AvatarContainer, MainFieldsContainer } from './FormFields/Form'
import TextField from './FormFields/TextField'
import TextChangeField from './FormFields/TextChangeField'
@ -32,7 +32,7 @@ class UpdateUserForm extends React.Component {
}
componentDidUpdate(prevProps, prevState) {
if (!profileEqual(this.props.profile, prevProps.profile)) {
if (this.props.match.params.userId !== prevProps.match.params.userId) {
this.setState(this.mapPropsToState(this.props));
}
}
@ -47,10 +47,7 @@ class UpdateUserForm extends React.Component {
email: false,
password: false,
},
profile: {
...{ passwordType: "link" },
...toEmployeeWrapper(props.profile)
}
profile: toEmployeeWrapper(props.profile)
};
}
@ -97,9 +94,9 @@ class UpdateUserForm extends React.Component {
this.setState({isLoading: true});
this.props.updateProfile(this.state.profile)
.then(() => {
.then((profile) => {
toastr.success("Success");
this.props.history.goBack();
this.props.history.push(`${this.props.settings.homepage}/view/${profile.userName}`);
})
.catch((error) => {
toastr.error(error.message)
@ -256,7 +253,8 @@ class UpdateUserForm extends React.Component {
const mapStateToProps = (state) => {
return {
profile: state.profile.targetUser
profile: state.profile.targetUser,
settings: state.auth.settings
}
};

View File

@ -15,14 +15,17 @@ const Header = styled(Text.ContentHeader)`
`;
const SectionHeaderContent = (props) => {
const {profile, history, settings} = props;
const { profile, history, settings, match } = props;
const { type } = match.params;
const { t } = useTranslation();
const headerText = profile && profile.displayName
? profile.displayName
: profile.isVisitor
const headerText = type
? type === "guest"
? t('NewGuest')
: t('NewEmployee');
: t('NewEmployee')
: profile
? profile.displayName
: "";
const onClick = useCallback(() => {
history.push(settings.homepage)

View File

@ -4,31 +4,27 @@ import PropTypes from "prop-types";
import { PageLayout, Loader } from "asc-web-components";
import { ArticleHeaderContent, ArticleMainButtonContent, ArticleBodyContent } from '../../Article';
import { SectionHeaderContent, CreateUserForm, UpdateUserForm } from './Section';
import { setProfile, fetchProfile, resetProfile } from '../../../store/profile/actions';
import { fetchProfile } from '../../../store/profile/actions';
import i18n from "./i18n";
import { I18nextProvider } from "react-i18next";
class ProfileAction extends React.Component {
componentDidMount() {
const { match, setProfile, fetchProfile } = this.props;
const { userId, type } = match.params;
if (!userId) {
setProfile({ isVisitor: type === "guest" });
} else {
componentDidMount() {
const { match, fetchProfile } = this.props;
const { userId } = match.params;
if (userId) {
fetchProfile(userId);
}
}
componentDidUpdate(prevProps) {
const { match, setProfile, fetchProfile } = this.props;
const { userId, type } = match.params;
const { match, fetchProfile } = this.props;
const { userId } = match.params;
const prevUserId = prevProps.match.params.userId;
const prevType = prevProps.match.params.type;
if (!userId && type !== prevType) {
setProfile({ isVisitor: type === "guest" });
} else if (userId !== prevUserId) {
if (userId !== undefined && userId !== prevUserId) {
fetchProfile(userId);
}
}
@ -36,17 +32,17 @@ class ProfileAction extends React.Component {
render() {
console.log("ProfileAction render")
const { profile } = this.props;
const { profile, match } = this.props;
return (
<I18nextProvider i18n={i18n}>
{profile
{profile || match.params.type
? <PageLayout
articleHeaderContent={<ArticleHeaderContent />}
articleMainButtonContent={<ArticleMainButtonContent />}
articleBodyContent={<ArticleBodyContent />}
sectionHeaderContent={<SectionHeaderContent />}
sectionBodyContent={profile.id ? <UpdateUserForm /> : <CreateUserForm />}
sectionBodyContent={match.params.type ? <CreateUserForm /> : <UpdateUserForm />}
/>
: <PageLayout
articleHeaderContent={<ArticleHeaderContent />}
@ -62,9 +58,7 @@ class ProfileAction extends React.Component {
ProfileAction.propTypes = {
match: PropTypes.object.isRequired,
profile: PropTypes.object,
setProfile: PropTypes.func.isRequired,
fetchProfile: PropTypes.func.isRequired,
resetProfile: PropTypes.func.isRequired
fetchProfile: PropTypes.func.isRequired
};
function mapStateToProps(state) {
@ -74,7 +68,5 @@ function mapStateToProps(state) {
}
export default connect(mapStateToProps, {
setProfile,
fetchProfile,
resetProfile
fetchProfile
})(ProfileAction);

View File

@ -33,32 +33,6 @@ export function getUserRole(profile) {
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: "",
@ -68,6 +42,7 @@ export function toEmployeeWrapper(profile) {
password: "",
birthday: "",
sex: "male",
passwordType: "link",
workFrom: "",
location: "",
title: "",
@ -111,12 +86,16 @@ export function createProfile(profile) {
const {people} = getState();
const {filter} = people;
const member = employeeWrapperToMemberModel(profile);
let result;
return api.createUser(member).then(res => {
checkResponseError(res);
return Promise.resolve(dispatch(setProfile(res.data.response)));
result = res.data.response;
return dispatch(setProfile(result));
}).then(() => {
return fetchPeopleByFilter(dispatch, filter);
}).then(() => {
return Promise.resolve(result);
});
};
};
@ -126,12 +105,16 @@ export function updateProfile(profile) {
const {people} = getState();
const {filter} = people;
const member = employeeWrapperToMemberModel(profile);
let result;
return api.updateUser(member).then(res => {
checkResponseError(res);
return Promise.resolve(dispatch(setProfile(res.data.response)));
result = res.data.response;
return Promise.resolve(dispatch(setProfile(result)));
}).then(() => {
return fetchPeopleByFilter(dispatch, filter);
}).then(() => {
return Promise.resolve(result);
});
};
};

View File

@ -85,4 +85,10 @@ export function updateUserType(type, userIds) {
return IS_FAKE
? fakeApi.updateUserType(type, userIds)
: axios.put(`${API_URL}/people/type/${type}`, { userIds });
}
export function resendUserInvites(userIds) {
return IS_FAKE
? fakeApi.resendUserInvites(userIds)
: axios.put(`${API_URL}/people/invite`, { userIds });
}

View File

@ -359,3 +359,50 @@ export function updateUserType(type, userIds) {
}
]);
}
export function resendUserInvites(userIds) {
return fakeResponse([
{
id: userIds[0],
userName: "Mike.Zanyatski",
isVisitor: false,
firstName: "Mike",
lastName: "Zanyatski",
email: "my@gmail.com",
birthday: "2019-08-19T01:39:25.3240031Z",
sex: "male",
status: 1,
activationStatus: 0,
terminated: "2019-08-19T01:39:25.3240031Z",
department: "Marketing",
workFrom: "2019-08-19T01:39:25.3240031Z",
location: "Palo Alto",
notes: "Notes to worker",
displayName: null,
title: "Manager",
contacts: [
{
type: "GTalk",
value: "my@gmail.com"
}
],
groups: [
{
id: "00000000-0000-0000-0000-000000000000",
name: "Group Name",
manager: "Jake.Zazhitski"
}
],
avatarMedium: "url to medium avatar",
avatar: "url to big avatar",
isAdmin: false,
isLDAP: false,
listAdminModules: ["projects", "crm"],
isOwner: false,
cultureName: "en-EN",
isSSO: false,
avatarSmall: "url to small avatar",
profileUrl: ""
}
]);
}

View File

@ -1774,7 +1774,7 @@ asap@~2.0.3, asap@~2.0.6:
integrity sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY=
"asc-web-components@file:../../../packages/asc-web-components":
version "1.0.27"
version "1.0.29"
dependencies:
"@emotion/core" "10.0.16"
prop-types "^15.7.2"
@ -8876,6 +8876,11 @@ react-dev-utils@^9.0.3:
strip-ansi "5.2.0"
text-table "0.2.0"
react-device-detect@^1.7.5:
version "1.7.5"
resolved "https://registry.yarnpkg.com/react-device-detect/-/react-device-detect-1.7.5.tgz#04c8a6475d67b5ac4f984c8d912ec11134f5b893"
integrity sha512-ccgJuTHVCI3yfqvgU56gQDvjueFyaHVAXsa5rJuzWRasvsd0IalzfyccSECIlygjv3E+DmAGcwNYWUarUA82Fw==
react-dom@^16.9.0:
version "16.9.0"
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.9.0.tgz#5e65527a5e26f22ae3701131bcccaee9fb0d3962"

View File

@ -27,7 +27,26 @@ or
{toastr.success('Some text for toast')}
</Toast>
```
You can use simple html tags. For this action you should wrap your message by empty tags:
```js
<Toast />
<button onClick={() => toastr.success(<>You have <b>bold text</b></>)}>Click</button>
```
If your notification include only text in html tags or data in JSX tags, you can omit empty tags:
```js
<Toast />
<button onClick={() => toastr.success(<b>Bold text</b>)}>Click</button>
```
```js
import { Text } from 'asc-web-components';
<Toast />
<button onClick={() => toastr.success(<Text.Body>The email activation instructions have been sent to the <b>{user.email}</b> email address</Text.Body>))}>Click</button>
```
#### Properties