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 {