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
index 8bc0fb945f..7de9d9909e 100644
--- 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
@@ -1,10 +1,11 @@
import React from 'react'
import { withRouter } from 'react-router'
import { connect } from 'react-redux'
-import { Avatar, Button, Textarea, Text, toastr, ModalDialog, TextInput } from 'asc-web-components'
+import { Avatar, Button, Textarea, Text, toastr, ModalDialog, TextInput, AvatarEditor } from 'asc-web-components'
import { withTranslation } from 'react-i18next';
import { toEmployeeWrapper, getUserRole, getUserContactsPattern, getUserContacts, mapGroupsToGroupSelectorOptions, mapGroupSelectorOptionsToGroups, filterGroupSelectorOptions } from "../../../../../store/people/selectors";
-import { updateProfile } from '../../../../../store/profile/actions';
+import { updateProfile, updateAvatar } from '../../../../../store/profile/actions';
+import { sendInstructionsToChangePassword } from "../../../../../store/services/api";
import { MainContainer, AvatarContainer, MainFieldsContainer } from './FormFields/Form'
import TextField from './FormFields/TextField'
import TextChangeField from './FormFields/TextChangeField'
@@ -42,6 +43,12 @@ class UpdateUserForm extends React.Component {
this.onContactsItemTypeChange = this.onContactsItemTypeChange.bind(this);
this.onContactsItemTextChange = this.onContactsItemTextChange.bind(this);
+ this.openAvatarEditor = this.openAvatarEditor.bind(this);
+ this.onSaveAvatar = this.onSaveAvatar.bind(this);
+ this.onCloseAvatarEditor = this.onCloseAvatarEditor.bind(this);
+
+
+
this.onShowGroupSelector = this.onShowGroupSelector.bind(this);
this.onCloseGroupSelector = this.onCloseGroupSelector.bind(this);
this.onSearchGroups = this.onSearchGroups.bind(this);
@@ -67,6 +74,7 @@ class UpdateUserForm extends React.Component {
lastName: false,
},
profile: profile,
+ visibleAvatarEditor: false,
dialog: {
visible: false,
header: "",
@@ -189,7 +197,7 @@ class UpdateUserForm extends React.Component {
header: "Change password",
body: (
- Send the password change instructions to the ${this.state.profile.email} email address
+ Send the password change instructions to the {this.state.profile.email} email address
),
buttons: [
@@ -206,8 +214,10 @@ class UpdateUserForm extends React.Component {
}
onSendPasswordChangeInstructions() {
- toastr.success("Context action: Change password");
- this.onDialogClose();
+ sendInstructionsToChangePassword(this.state.profile.email)
+ .then((res) => toastr.success(res.data.response))
+ .catch((error) => toastr.error(error.message))
+ .finally(this.onDialogClose);
}
onPhoneChange() {
@@ -272,6 +282,34 @@ class UpdateUserForm extends React.Component {
this.setState(stateCopy);
}
+ openAvatarEditor(){
+ this.setState({
+ visibleAvatarEditor: true,
+ });
+ }
+ onSaveAvatar(result) {
+ this.props.updateAvatar(this.state.profile.id, result)
+ .then((result) => {
+ let stateCopy = Object.assign({}, this.state);
+ stateCopy.visibleAvatarEditor = false;
+ if(result.data.response.success){
+ stateCopy.profile.avatarMax = result.data.response.data.max;
+ }else{
+ stateCopy.profile.avatarMax = result.data.response.max && result.data.response.max;
+ }
+ toastr.success("Success");
+ this.setState(stateCopy);
+ })
+ .catch((error) => {
+ toastr.error(error.message);
+ });
+ }
+ onCloseAvatarEditor() {
+ this.setState({
+ visibleAvatarEditor: false,
+ });
+ }
+
onShowGroupSelector() {
var stateCopy = Object.assign({}, this.state);
stateCopy.selector.visible = true;
@@ -323,7 +361,13 @@ class UpdateUserForm extends React.Component {
userName={profile.displayName}
editing={true}
editLabel={t("EditPhoto")}
+ editAction={this.openAvatarEditor}
/>
+
{
export default connect(
mapStateToProps,
{
- updateProfile
+ updateProfile,
+ updateAvatar
}
)(withRouter(withTranslation()(UpdateUserForm)));
\ No newline at end of file
diff --git a/products/ASC.People/Client/src/store/profile/actions.js b/products/ASC.People/Client/src/store/profile/actions.js
index 47b9376025..e4a775f042 100644
--- a/products/ASC.People/Client/src/store/profile/actions.js
+++ b/products/ASC.People/Client/src/store/profile/actions.js
@@ -90,4 +90,26 @@ export function updateProfile(profile) {
return Promise.resolve(result);
});
};
+};
+export function updateAvatar(profileId, images) {
+ return (dispatch, getState) => {
+ if (images.croppedImage) {
+ return api.updateAvatar(
+ profileId,
+ {
+ autosave: true,
+ base64CroppedImage: images.croppedImage.split(',')[1],
+ base64DefaultImage: images.defaultImage.split(',')[1]
+ }
+ ).then(res => {
+ checkResponseError(res);
+ return Promise.resolve(res);
+ });
+ } else {
+ return api.deleteAvatar(profileId).then(res => {
+ checkResponseError(res);
+ return Promise.resolve(res);
+ });
+ }
+ };
};
\ No newline at end of file
diff --git a/products/ASC.People/Client/src/store/services/api.js b/products/ASC.People/Client/src/store/services/api.js
index f5c7f45fc1..f58327ad68 100644
--- a/products/ASC.People/Client/src/store/services/api.js
+++ b/products/ASC.People/Client/src/store/services/api.js
@@ -68,6 +68,17 @@ export function updateUser(data) {
? fakeApi.updateUser()
: axios.put(`${API_URL}/people/${data.id}`, data);
}
+export function updateAvatar(profileId, data) {
+ return IS_FAKE
+ ? fakeApi.updateAvatar()
+ : axios.post(`${API_URL}/people/${profileId}/photo/cropped`, data);
+}
+export function deleteAvatar(profileId) {
+
+ return IS_FAKE
+ ? fakeApi.deleteAvatar()
+ : axios.delete(`${API_URL}/people/${profileId}/photo`, profileId);
+}
export function getInitInfo() {
return axios.all([getUser(), getModulesList(), getSettings(), getPortalPasswordSettings()]).then(
diff --git a/products/ASC.People/Client/src/store/services/fakeApi.js b/products/ASC.People/Client/src/store/services/fakeApi.js
index 1941330483..06f6db9624 100644
--- a/products/ASC.People/Client/src/store/services/fakeApi.js
+++ b/products/ASC.People/Client/src/store/services/fakeApi.js
@@ -277,6 +277,13 @@ export function updateUser(data) {
return fakeResponse(data);
}
+export function updateAvatar(data) {
+ return fakeResponse(data);
+}
+export function deleteAvatar(data) {
+ return fakeResponse(data);
+}
+
export function updateUserStatus(status, userIds) {
return fakeResponse([
{
diff --git a/products/ASC.People/Server/Controllers/PeopleController.cs b/products/ASC.People/Server/Controllers/PeopleController.cs
index bd11c4f444..b3f838fce2 100644
--- a/products/ASC.People/Server/Controllers/PeopleController.cs
+++ b/products/ASC.People/Server/Controllers/PeopleController.cs
@@ -1,7 +1,5 @@
using System;
using System.Collections.Generic;
-using System.Drawing;
-using System.Drawing.Imaging;
using System.IO;
using System.Linq;
using System.Net;
@@ -590,7 +588,110 @@ namespace ASC.Employee.Core.Controllers
return new ThumbnailsDataWrapper(Tenant, user.ID);
}
+
+ public FormFile Base64ToImage(string base64String, string fileName)
+ {
+ byte[] imageBytes = Convert.FromBase64String(base64String);
+ MemoryStream ms = new MemoryStream(imageBytes, 0, imageBytes.Length);
+ ms.Write(imageBytes, 0, imageBytes.Length);
+ return new FormFile(ms, 0, ms.Length, fileName, fileName);
+ }
+
+ [Create("{userid}/photo/cropped")]
+ public FileUploadResult UploadCroppedMemberPhoto(string userid, UploadCroppedPhotoModel model)
+ {
+ var result = new FileUploadResult();
+
+ try
+ {
+ Guid userId;
+ try
+ {
+ userId = new Guid(userid);
+ }
+ catch
+ {
+ userId = SecurityContext.CurrentAccount.ID;
+ }
+
+ SecurityContext.DemandPermissions(Tenant, new UserSecurityProvider(userId), Constants.Action_EditUser);
+
+ var userPhoto = Base64ToImage(model.base64CroppedImage, "userPhoto_"+ userId.ToString());
+ var defaultUserPhoto = Base64ToImage(model.base64DefaultImage, "defaultPhoto" + userId.ToString());
+
+ if (userPhoto.Length > SetupInfo.MaxImageUploadSize)
+ {
+ result.Success = false;
+ result.Message = FileSizeComment.FileImageSizeExceptionString;
+ return result;
+ }
+
+ var data = new byte[userPhoto.Length];
+ using var inputStream = userPhoto.OpenReadStream();
+
+ var br = new BinaryReader(inputStream);
+ br.Read(data, 0, (int)userPhoto.Length);
+ br.Close();
+
+ var defaultData = new byte[defaultUserPhoto.Length];
+ using var defaultInputStream = defaultUserPhoto.OpenReadStream();
+
+ var defaultBr = new BinaryReader(defaultInputStream);
+ defaultBr.Read(defaultData, 0, (int)defaultUserPhoto.Length);
+ defaultBr.Close();
+
+ //CheckImgFormat(data);
+
+ if (model.Autosave)
+ {
+ if (data.Length > SetupInfo.MaxImageUploadSize)
+ throw new ImageSizeLimitException();
+
+ var mainPhoto = UserPhotoManager.SaveOrUpdateCroppedPhoto(Tenant, userId, data, defaultData);
+
+ result.Data =
+ new
+ {
+ main = mainPhoto,
+ retina = UserPhotoManager.GetRetinaPhotoURL(Tenant.TenantId, userId),
+ max = UserPhotoManager.GetMaxPhotoURL(Tenant.TenantId, userId),
+ big = UserPhotoManager.GetBigPhotoURL(Tenant.TenantId, userId),
+ medium = UserPhotoManager.GetMediumPhotoURL(Tenant.TenantId, userId),
+ small = UserPhotoManager.GetSmallPhotoURL(Tenant.TenantId, userId),
+ };
+ }
+ else
+ {
+ result.Data = UserPhotoManager.SaveTempPhoto(Tenant.TenantId, data, SetupInfo.MaxImageUploadSize, UserPhotoManager.OriginalFotoSize.Width, UserPhotoManager.OriginalFotoSize.Height);
+ }
+
+ result.Success = true;
+
+ }
+ catch (UnknownImageFormatException)
+ {
+ result.Success = false;
+ result.Message = PeopleResource.ErrorUnknownFileImageType;
+ }
+ catch (ImageWeightLimitException)
+ {
+ result.Success = false;
+ result.Message = PeopleResource.ErrorImageWeightLimit;
+ }
+ catch (ImageSizeLimitException)
+ {
+ result.Success = false;
+ result.Message = PeopleResource.ErrorImageSizetLimit;
+ }
+ catch (Exception ex)
+ {
+ result.Success = false;
+ result.Message = ex.Message.HtmlEncode();
+ }
+
+ return result;
+ }
[Create("{userid}/photo")]
public FileUploadResult UploadMemberPhoto(string userid, UploadPhotoModel model)
{
@@ -627,7 +728,7 @@ namespace ASC.Employee.Core.Controllers
br.Read(data, 0, (int)userPhoto.Length);
br.Close();
- CheckImgFormat(data);
+ //CheckImgFormat(data);
if (model.Autosave)
{
@@ -1231,29 +1332,30 @@ namespace ASC.Employee.Core.Controllers
UserPhotoManager.SaveOrUpdatePhoto(Tenant, user.ID, imageByteArray);
}
- private static void CheckImgFormat(byte[] data)
- {
- ImageFormat imgFormat;
+ //not working under unix
+ //private static void CheckImgFormat(byte[] data)
+ //{
+ // ImageFormat imgFormat;
- try
- {
- using var stream = new MemoryStream(data);
- using var img = new Bitmap(stream);
- imgFormat = img.RawFormat;
- }
- catch (OutOfMemoryException)
- {
- throw new ImageSizeLimitException();
- }
- catch (ArgumentException error)
- {
- throw new UnknownImageFormatException(error);
- }
+ // try
+ // {
+ // using var stream = new MemoryStream(data);
+ // using var img = new Bitmap(stream);
+ // imgFormat = img.RawFormat;
+ // }
+ // catch (OutOfMemoryException)
+ // {
+ // throw new ImageSizeLimitException();
+ // }
+ // catch (ArgumentException error)
+ // {
+ // throw new UnknownImageFormatException(error);
+ // }
- if (!imgFormat.Equals(ImageFormat.Png) && !imgFormat.Equals(ImageFormat.Jpeg))
- {
- throw new UnknownImageFormatException();
- }
- }
+ // if (!imgFormat.Equals(ImageFormat.Png) && !imgFormat.Equals(ImageFormat.Jpeg))
+ // {
+ // throw new UnknownImageFormatException();
+ // }
+ //}
}
}
diff --git a/products/ASC.People/Server/Models/EmployeeWraperFull.cs b/products/ASC.People/Server/Models/EmployeeWraperFull.cs
index f0976859b4..9e24f16bab 100644
--- a/products/ASC.People/Server/Models/EmployeeWraperFull.cs
+++ b/products/ASC.People/Server/Models/EmployeeWraperFull.cs
@@ -86,6 +86,9 @@ namespace ASC.Web.Api.Models
[DataMember(Order = 20)]
public string AvatarMax { get; set; }
+ [DataMember(Order = 20)]
+ public string AvatarDefault { get; set; }
+
[DataMember(Order = 20)]
public string AvatarMedium { get; set; }
@@ -185,6 +188,11 @@ namespace ASC.Web.Api.Models
var userInfoLM = userInfo.LastModified.GetHashCode();
+ if (context.Check("avatarDefault"))
+ {
+ AvatarDefault = Convert.ToBase64String(CoreContext.UserManager.GetUserPhoto(context.Tenant.TenantId, userInfo.ID));
+ }
+
if (context.Check("avatarMax"))
{
AvatarMax = UserPhotoManager.GetMaxPhotoURL(context.Tenant.TenantId, userInfo.ID, out var isdef) + (isdef ? "" : $"?_={userInfoLM}");
diff --git a/products/ASC.People/Server/Models/UploadPhotoModel.cs b/products/ASC.People/Server/Models/UploadPhotoModel.cs
index 2f1661bddc..e26ea7b92e 100644
--- a/products/ASC.People/Server/Models/UploadPhotoModel.cs
+++ b/products/ASC.People/Server/Models/UploadPhotoModel.cs
@@ -8,6 +8,12 @@ namespace ASC.People.Models
public List Files { get; set; }
public bool Autosave { get; set; }
}
+ public class UploadCroppedPhotoModel
+ {
+ public string base64CroppedImage { get; set; }
+ public string base64DefaultImage { get; set; }
+ public bool Autosave { get; set; }
+ }
public class FileUploadResult
{
diff --git a/web/ASC.Web.Components/package.json b/web/ASC.Web.Components/package.json
index 513bfe405a..30d63ea20e 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.78",
+ "version": "1.0.80",
"description": "Ascensio System SIA component library",
"license": "AGPL-3.0",
"main": "dist/asc-web-components.js",
diff --git a/web/ASC.Web.Components/src/components/avatar-editor/index.js b/web/ASC.Web.Components/src/components/avatar-editor/index.js
index bdf29bd897..02b1fc2dc8 100644
--- a/web/ASC.Web.Components/src/components/avatar-editor/index.js
+++ b/web/ASC.Web.Components/src/components/avatar-editor/index.js
@@ -127,7 +127,7 @@ class AvatarEditor extends React.Component {
}
onSaveButtonClick() {
this.props.onSave({
- defaultImage: this.state.defaultImage,
+ defaultImage: this.state.defaultImage ? this.state.defaultImage : this.props.image,
croppedImage: this.state.croppedImage
});
this.setState({ visible: false });
diff --git a/web/ASC.Web.Components/src/components/avatar/avatar.test.js b/web/ASC.Web.Components/src/components/avatar/avatar.test.js
index 3ec4ac0b37..a91d430ee3 100644
--- a/web/ASC.Web.Components/src/components/avatar/avatar.test.js
+++ b/web/ASC.Web.Components/src/components/avatar/avatar.test.js
@@ -1,5 +1,5 @@
import React from 'react';
-import { mount, shallow } from 'enzyme';
+import { mount } from 'enzyme';
import Avatar from '.';
const baseProps = {
@@ -96,6 +96,7 @@ describe('', () => {
expect(wrapper.prop('editing')).toEqual(true);
});
+ /*
it('not re-render test', () => {
const wrapper = shallow().instance();
@@ -119,4 +120,5 @@ describe('', () => {
expect(shouldUpdate).toBe(true);
});
+ */
});
diff --git a/web/ASC.Web.Components/src/components/avatar/index.js b/web/ASC.Web.Components/src/components/avatar/index.js
index 168c08c669..37a3198f4c 100644
--- a/web/ASC.Web.Components/src/components/avatar/index.js
+++ b/web/ASC.Web.Components/src/components/avatar/index.js
@@ -1,9 +1,8 @@
-import React from 'react'
+import React, { memo } from 'react'
import styled, { css } from 'styled-components'
import PropTypes from 'prop-types'
import { Icons } from '../icons'
import Link from '../link'
-import isEqual from "lodash/isEqual";
const whiteColor = '#FFFFFF';
const avatarBackground = '#ECEEF1';
@@ -159,51 +158,44 @@ Initials.propTypes = {
};
// eslint-disable-next-line react/display-name
-class Avatar extends React.Component {
+const Avatar = memo(props => {
+ //console.log("Avatar render");
+ const { size, source, userName, role, editing, editLabel, editAction } = props;
- shouldComponentUpdate(nextProps, nextState) {
- return !isEqual(this.props, nextProps) || !isEqual(this.state, nextState);
- }
+ const avatarContent = source
+ ?
+ : userName
+ ?
+ : ;
- render() {
- //console.log("Avatar render");
- const { size, source, userName, role, editing, editLabel, editAction } = this.props;
+ const roleIcon = getRoleIcon(role);
- const avatarContent = source
- ?
- : userName
- ?
- : ;
-
- const roleIcon = getRoleIcon(role);
-
- return (
-
-
- {avatarContent}
-
- {editing && (size === 'max') &&
-
-
-
- {editLabel}
-
-
- }
-
- {roleIcon}
-
-
- );
- }
-}
+ return (
+
+
+ {avatarContent}
+
+ {editing && (size === 'max') &&
+
+
+
+ {editLabel}
+
+
+ }
+
+ {roleIcon}
+
+
+ );
+});
Avatar.propTypes = {
size: PropTypes.oneOf(['max', 'big', 'medium', 'small']),
diff --git a/web/ASC.Web.Components/src/components/context-menu-button/index.js b/web/ASC.Web.Components/src/components/context-menu-button/index.js
index 87a987d9cf..670dea8503 100644
--- a/web/ASC.Web.Components/src/components/context-menu-button/index.js
+++ b/web/ASC.Web.Components/src/components/context-menu-button/index.js
@@ -12,7 +12,7 @@ const StyledOuther = styled.div`
cursor: pointer;
`;
-class ContextMenuButton extends React.PureComponent {
+class ContextMenuButton extends React.Component {
constructor(props) {
super(props);
@@ -70,6 +70,13 @@ class ContextMenuButton extends React.PureComponent {
this.toggle(!this.state.isOpen);
}
+ shouldComponentUpdate(nextProps, nextState) {
+ if (this.props.opened === nextProps.opened && this.state.isOpen === nextState.isOpen) {
+ return false;
+ }
+ return true;
+ }
+
render() {
//console.log("ContextMenuButton render");
return (
diff --git a/web/ASC.Web.Components/src/components/input-block/index.js b/web/ASC.Web.Components/src/components/input-block/index.js
index 6136949fab..58df043bce 100644
--- a/web/ASC.Web.Components/src/components/input-block/index.js
+++ b/web/ASC.Web.Components/src/components/input-block/index.js
@@ -50,10 +50,10 @@ class InputBlock extends React.Component {
}
onIconClick(e) {
- this.props.onIconClick(e);
+ if(typeof this.props.onIconClick === "function") this.props.onIconClick(e);
}
onChange(e) {
- this.props.onChange(e);
+ if(typeof this.props.onChange === "function") this.props.onChange(e);
}
render() {
diff --git a/web/ASC.Web.Components/src/components/link/index.js b/web/ASC.Web.Components/src/components/link/index.js
index a8417d585c..d4a1ca1af1 100644
--- a/web/ASC.Web.Components/src/components/link/index.js
+++ b/web/ASC.Web.Components/src/components/link/index.js
@@ -1,21 +1,23 @@
-import React from "react";
+import React, { memo } from "react";
import styled, { css } from "styled-components";
import PropTypes from "prop-types";
import { Text } from "../text";
-const SimpleLink = ({
- rel,
- isBold,
- fontSize,
- isTextOverflow,
- isHovered,
- isSemitransparent,
- type,
- color,
- title,
- containerWidth,
- ...props
-}) => ;
+// eslint-disable-next-line no-unused-vars
+const SimpleLink = ({ rel, isBold, fontSize, isTextOverflow, isHovered, isSemitransparent, type, color, title, containerWidth, ...props }) => ;
+
+SimpleLink.propTypes = {
+ color: PropTypes.string,
+ fontSize: PropTypes.number,
+ isBold: PropTypes.bool,
+ isHovered: PropTypes.bool,
+ isSemitransparent: PropTypes.bool,
+ isTextOverflow: PropTypes.bool,
+ rel: PropTypes.string,
+ title: PropTypes.string,
+ type: PropTypes.oneOf(["action", "page"]),
+ containerWidth: PropTypes.string
+};
const colorCss = css`
@@ -45,7 +47,8 @@ const StyledLink = styled(SimpleLink)`
${props => props.isHovered && hoveredCss}
`;
-const Link = props => {
+// eslint-disable-next-line react/display-name
+const Link = memo(props => {
const {
isBold,
title,
@@ -70,7 +73,7 @@ const Link = props => {
);
-};
+});
Link.propTypes = {
color: PropTypes.string,
@@ -86,6 +89,7 @@ Link.propTypes = {
target: PropTypes.oneOf(["_blank", "_self", "_parent", "_top"]),
title: PropTypes.string,
type: PropTypes.oneOf(["action", "page"]),
+ children: PropTypes.string
};
Link.defaultProps = {
diff --git a/web/ASC.Web.Components/src/components/row-container/index.js b/web/ASC.Web.Components/src/components/row-container/index.js
index 9ba4a6d40b..7d3d92515f 100644
--- a/web/ASC.Web.Components/src/components/row-container/index.js
+++ b/web/ASC.Web.Components/src/components/row-container/index.js
@@ -1,8 +1,9 @@
-import React from 'react';
+/* eslint-disable react/display-name */
+import React, { memo } from 'react';
import styled from 'styled-components';
import PropTypes from 'prop-types';
import CustomScrollbarsVirtualList from '../scrollbar/custom-scrollbars-virtual-list';
-import { FixedSizeList as List } from 'react-window';
+import { FixedSizeList as List, areEqual } from 'react-window';
import AutoSizer from 'react-virtualized-auto-sizer';
import ContextMenu from '../context-menu';
@@ -35,6 +36,16 @@ class RowContainer extends React.PureComponent {
window.removeEventListener('contextmenu', this.onRowContextClick);
}
+ renderRow = memo(({ data, index, style }) => {
+ const options = data[index].props.contextOptions;
+
+ return (
+
+ {data[index]}
+
+ )
+ }, areEqual);
+
render() {
const { manualHeight, itemHeight, children } = this.props;
@@ -48,22 +59,10 @@ class RowContainer extends React.PureComponent {
itemData={children}
outerElementType={CustomScrollbarsVirtualList}
>
- {RenderRow}
+ {this.renderRow}
);
-
- const RenderRow = ({ data, index, style }) => {
-
- const options = data[index].props.contextOptions;
-
- return (
-
- {data[index]}
-
- )
- };
-
return (
diff --git a/web/ASC.Web.Components/src/components/row/index.js b/web/ASC.Web.Components/src/components/row/index.js
index ee525c2958..d32ab9c993 100644
--- a/web/ASC.Web.Components/src/components/row/index.js
+++ b/web/ASC.Web.Components/src/components/row/index.js
@@ -1,4 +1,4 @@
-import React from 'react'
+import React, { memo } from 'react'
import styled from 'styled-components'
import PropTypes from 'prop-types'
@@ -40,6 +40,7 @@ const StyledElement = styled.div`
flex: 0 0 auto;
display: flex;
margin-right: 8px;
+ margin-left: 2px;
user-select: none;
`;
@@ -50,54 +51,39 @@ const StyledOptionButton = styled.div`
margin-right: 16px;
`;
-class Row extends React.PureComponent {
- constructor(props) {
- super(props);
- this.state = {
- checked: this.props.checked
- }
- }
-
- changeCheckbox = (e) => {
- this.props.onSelect && this.props.onSelect(e.target.checked, this.props.data);
+// eslint-disable-next-line react/display-name
+const Row = props => {
+ const changeCheckbox = (e) => {
+ props.onSelect && props.onSelect(e.target.checked, props.data);
};
- getOptions = () => this.props.contextOptions;
+ const getOptions = () => props.contextOptions;
+ //console.log("Row render");
+ const { checked, element, children, contextOptions } = props;
- componentDidUpdate(prevProps) {
- if (this.props.checked !== prevProps.checked) {
- this.setState({ checked: this.props.checked });
- }
- }
-
- render() {
- //console.log("Row render");
- const { checked, element, children, contextOptions } = this.props;
-
- return (
-
- {Object.prototype.hasOwnProperty.call(this.props, 'checked') &&
-
-
-
+ return (
+
+ {Object.prototype.hasOwnProperty.call(props, 'checked') &&
+
+
+
+ }
+ {Object.prototype.hasOwnProperty.call(props, 'element') &&
+
+ {element}
+
+ }
+
+ {children}
+
+
+ {Object.prototype.hasOwnProperty.call(props, 'contextOptions') && contextOptions.length > 0 &&
+
}
- {Object.prototype.hasOwnProperty.call(this.props, 'element') &&
-
- {element}
-
- }
-
- {children}
-
-
- {Object.prototype.hasOwnProperty.call(this.props, 'contextOptions') && contextOptions.length > 0 &&
-
- }
-
-
- );
- }
-}
+
+
+ );
+};
Row.propTypes = {
checked: PropTypes.bool,
diff --git a/web/ASC.Web.Components/src/components/textarea/index.js b/web/ASC.Web.Components/src/components/textarea/index.js
index 72ba09021c..226c110034 100644
--- a/web/ASC.Web.Components/src/components/textarea/index.js
+++ b/web/ASC.Web.Components/src/components/textarea/index.js
@@ -5,7 +5,7 @@ import PropTypes from 'prop-types';
import commonInputStyle from '../text-input/common-input-styles';
import TextareaAutosize from 'react-autosize-textarea';
-const ClearScrollbar = ({isDisabled, ...props}) =>
+const ClearScrollbar = ({ isDisabled, ...props }) =>
const StyledScrollbar = styled(ClearScrollbar)`
${commonInputStyle};
:focus-within {
@@ -23,7 +23,7 @@ const StyledScrollbar = styled(ClearScrollbar)`
}
`;
-const ClearTextareaAutosize = ({isDisabled, ...props}) =>
+const ClearTextareaAutosize = ({ isDisabled, ...props }) =>
const StyledTextarea = styled(ClearTextareaAutosize)`
${commonInputStyle};
width: 100%;
@@ -49,9 +49,10 @@ const StyledTextarea = styled(ClearTextareaAutosize)`
class Textarea extends React.PureComponent {
render() {
- // console.log('Textarea render');
+ console.log('Textarea render');
return (
@@ -65,7 +66,7 @@ class Textarea extends React.PureComponent {
isDisabled={this.props.isDisabled}
disabled={this.props.isDisabled}
readOnly={this.props.isReadOnly}
- defaultValue={this.props.value}
+ value={this.props.value}
/>
)
@@ -81,7 +82,8 @@ Textarea.propTypes = {
onChange: PropTypes.func,
placeholder: PropTypes.string,
tabIndex: PropTypes.number,
- value: PropTypes.string
+ value: PropTypes.string,
+ className: PropTypes.string
}
Textarea.defaultProps = {
@@ -90,6 +92,7 @@ Textarea.defaultProps = {
placeholder: '',
value: '',
tabIndex: -1,
+ className: ''
}
export default Textarea;
diff --git a/web/ASC.Web.Core/Users/UserPhotoManager.cs b/web/ASC.Web.Core/Users/UserPhotoManager.cs
index 64b27047ea..3d01f58e05 100644
--- a/web/ASC.Web.Core/Users/UserPhotoManager.cs
+++ b/web/ASC.Web.Core/Users/UserPhotoManager.cs
@@ -118,11 +118,12 @@ namespace ASC.Web.Core.Users
try
{
- Photofiles.TryGetValue(data.Size, out var dict);
- dict?.TryRemove(userId, out _);
- //var storage = GetDataStore();
- //storage.DeleteFiles("", data.UserID + "*.*", false);
- //SetCacheLoadedForTenant(false);
+ foreach(var s in (CacheSize[])Enum.GetValues(typeof(CacheSize)))
+ {
+ Photofiles.TryGetValue(s, out var dict);
+ dict?.TryRemove(userId, out _);
+ }
+ SetCacheLoadedForTenant(false, data.TenantId);
}
catch { }
}, CacheNotifyAction.Remove);
@@ -356,9 +357,9 @@ namespace ASC.Web.Core.Users
return TenantDiskCache.Contains(tenantId);
}
- private static bool SetCacheLoadedForTenant(bool isLoaded)
+ private static bool SetCacheLoadedForTenant(bool isLoaded, int tenantId)
{
- return isLoaded ? TenantDiskCache.Add(TenantProvider.CurrentTenantID) : TenantDiskCache.Remove(TenantProvider.CurrentTenantID);
+ return isLoaded ? TenantDiskCache.Add(tenantId) : TenantDiskCache.Remove(tenantId);
}
@@ -426,7 +427,7 @@ namespace ASC.Web.Core.Users
}
}
}
- SetCacheLoadedForTenant(true);
+ SetCacheLoadedForTenant(true, tenantId);
}
catch (Exception err)
{
@@ -462,9 +463,16 @@ namespace ASC.Web.Core.Users
{
return SaveOrUpdatePhoto(tenant, userID, data, -1, OriginalFotoSize, true, out _);
}
+ public static string SaveOrUpdateCroppedPhoto(Tenant tenant, Guid userID, byte[] data, byte[] defaultData)
+ {
+ return SaveOrUpdateCroppedPhoto(tenant, userID, data, defaultData, -1, OriginalFotoSize, true, out _);
+ }
public static void RemovePhoto(Tenant tenant, Guid idUser)
{
+ var storage = GetDataStore(tenant.TenantId);
+ storage.DeleteFiles("", idUser + "*.*", false);
+
CoreContext.UserManager.SaveUserPhoto(tenant, idUser, null);
ClearCache(idUser);
}
@@ -501,6 +509,49 @@ namespace ASC.Web.Core.Users
}
return photoUrl;
}
+ private static string SaveOrUpdateCroppedPhoto(Tenant tenant, Guid userID, byte[] data, byte[] defaultData, long maxFileSize, Size size, bool saveInCoreContext, out string fileName)
+ {
+ data = TryParseImage(data, maxFileSize, size, out var imgFormat, out var width, out var height);
+
+ var widening = CommonPhotoManager.GetImgFormatName(imgFormat);
+ fileName = string.Format("{0}_orig_{1}-{2}.{3}", userID, width, height, widening);
+
+ if (saveInCoreContext)
+ {
+ CoreContext.UserManager.SaveUserPhoto(tenant, userID, defaultData);
+
+ var max = Math.Max(Math.Max(width, height), SmallFotoSize.Width);
+ var min = Math.Max(Math.Min(width, height), SmallFotoSize.Width);
+
+ var pos = (max - min) / 2;
+
+ var settings = new UserPhotoThumbnailSettings(
+ width >= height ? new Point(pos, 0) : new Point(0, pos),
+ new Size(min, min));
+
+ settings.SaveForUser(userID);
+
+ ClearCache(userID);
+ }
+
+ var store = GetDataStore(tenant.TenantId);
+
+ var photoUrl = GetDefaultPhotoAbsoluteWebPath();
+ if (data != null && data.Length > 0)
+ {
+ using (var stream = new MemoryStream(data))
+ {
+ photoUrl = store.Save(fileName, stream).ToString();
+ }
+ //Queue resizing
+ SizePhoto(tenant.TenantId, userID, data, -1, SmallFotoSize, true);
+ SizePhoto(tenant.TenantId, userID, data, -1, MediumFotoSize, true);
+ SizePhoto(tenant.TenantId, userID, data, -1, BigFotoSize, true);
+ SizePhoto(tenant.TenantId, userID, data, -1, MaxFotoSize, true);
+ SizePhoto(tenant.TenantId, userID, data, -1, RetinaFotoSize, true);
+ }
+ return photoUrl;
+ }
private static void SetUserPhotoThumbnailSettings(Guid userId, int width, int height)
{
diff --git a/web/ASC.Web.Core/protos/UserPhotoManagerCacheItem.proto b/web/ASC.Web.Core/protos/UserPhotoManagerCacheItem.proto
index 7292497f01..c3391acf93 100644
--- a/web/ASC.Web.Core/protos/UserPhotoManagerCacheItem.proto
+++ b/web/ASC.Web.Core/protos/UserPhotoManagerCacheItem.proto
@@ -8,6 +8,8 @@ message UserPhotoManagerCacheItem {
CacheSize Size = 2;
string FileName = 3;
+
+ int32 TenantId = 4;
}
enum CacheSize {