Merge branch 'master' of https://github.com/ONLYOFFICE/CommunityServer-AspNetCore
# Conflicts: # web/ASC.Web.Components/package.json
This commit is contained in:
commit
88f8818d77
@ -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: (
|
||||
<Text.Body>
|
||||
Send the password change instructions to the <a href={`mailto:${this.state.profile.email}`}>${this.state.profile.email}</a> email address
|
||||
Send the password change instructions to the <a href={`mailto:${this.state.profile.email}`}>{this.state.profile.email}</a> email address
|
||||
</Text.Body>
|
||||
),
|
||||
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}
|
||||
/>
|
||||
<AvatarEditor
|
||||
image={profile.avatarDefault ? "data:image/png;base64,"+profile.avatarDefault : null}
|
||||
visible={this.state.visibleAvatarEditor}
|
||||
onClose={this.onCloseAvatarEditor}
|
||||
onSave={this.onSaveAvatar} />
|
||||
</AvatarContainer>
|
||||
<MainFieldsContainer>
|
||||
<TextChangeField
|
||||
@ -498,6 +542,7 @@ const mapStateToProps = (state) => {
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
{
|
||||
updateProfile
|
||||
updateProfile,
|
||||
updateAvatar
|
||||
}
|
||||
)(withRouter(withTranslation()(UpdateUserForm)));
|
@ -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);
|
||||
});
|
||||
}
|
||||
};
|
||||
};
|
@ -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(
|
||||
|
@ -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([
|
||||
{
|
||||
|
@ -590,7 +590,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)
|
||||
{
|
||||
|
@ -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}");
|
||||
|
@ -8,6 +8,12 @@ namespace ASC.People.Models
|
||||
public List<IFormFile> 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
|
||||
{
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "asc-web-components",
|
||||
"version": "1.0.79",
|
||||
"version": "1.0.80",
|
||||
"description": "Ascensio System SIA component library",
|
||||
"license": "AGPL-3.0",
|
||||
"main": "dist/asc-web-components.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 });
|
||||
|
@ -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('<Avatar />', () => {
|
||||
expect(wrapper.prop('editing')).toEqual(true);
|
||||
});
|
||||
|
||||
/*
|
||||
it('not re-render test', () => {
|
||||
const wrapper = shallow(<Avatar {...baseProps} />).instance();
|
||||
|
||||
@ -119,4 +120,5 @@ describe('<Avatar />', () => {
|
||||
|
||||
expect(shouldUpdate).toBe(true);
|
||||
});
|
||||
*/
|
||||
});
|
||||
|
@ -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
|
||||
? <ImageStyled src={source} />
|
||||
: userName
|
||||
? <Initials userName={userName} size={size} />
|
||||
: <EmptyIcon size='scale' />;
|
||||
|
||||
render() {
|
||||
//console.log("Avatar render");
|
||||
const { size, source, userName, role, editing, editLabel, editAction } = this.props;
|
||||
const roleIcon = getRoleIcon(role);
|
||||
|
||||
const avatarContent = source
|
||||
? <ImageStyled src={source} />
|
||||
: userName
|
||||
? <Initials userName={userName} size={size} />
|
||||
: <EmptyIcon size='scale' />;
|
||||
|
||||
const roleIcon = getRoleIcon(role);
|
||||
|
||||
return (
|
||||
<StyledAvatar {...this.props}>
|
||||
<AvatarWrapper source={source} userName={userName}>
|
||||
{avatarContent}
|
||||
</AvatarWrapper>
|
||||
{editing && (size === 'max') &&
|
||||
<EditContainer>
|
||||
<EditLink>
|
||||
<Link
|
||||
type='action'
|
||||
title={editLabel}
|
||||
isTextOverflow={true}
|
||||
fontSize={14}
|
||||
color={whiteColor}
|
||||
onClick={editAction}
|
||||
>
|
||||
{editLabel}
|
||||
</Link>
|
||||
</EditLink>
|
||||
</EditContainer>}
|
||||
<RoleWrapper size={size}>
|
||||
{roleIcon}
|
||||
</RoleWrapper>
|
||||
</StyledAvatar>
|
||||
);
|
||||
}
|
||||
}
|
||||
return (
|
||||
<StyledAvatar {...props}>
|
||||
<AvatarWrapper source={source} userName={userName}>
|
||||
{avatarContent}
|
||||
</AvatarWrapper>
|
||||
{editing && (size === 'max') &&
|
||||
<EditContainer>
|
||||
<EditLink>
|
||||
<Link
|
||||
type='action'
|
||||
title={editLabel}
|
||||
isTextOverflow={true}
|
||||
fontSize={14}
|
||||
color={whiteColor}
|
||||
onClick={editAction}
|
||||
>
|
||||
{editLabel}
|
||||
</Link>
|
||||
</EditLink>
|
||||
</EditContainer>}
|
||||
<RoleWrapper size={size}>
|
||||
{roleIcon}
|
||||
</RoleWrapper>
|
||||
</StyledAvatar>
|
||||
);
|
||||
});
|
||||
|
||||
Avatar.propTypes = {
|
||||
size: PropTypes.oneOf(['max', 'big', 'medium', 'small']),
|
||||
|
@ -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 (
|
||||
|
@ -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() {
|
||||
|
@ -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
|
||||
}) => <a {...props} />;
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
const SimpleLink = ({ rel, isBold, fontSize, isTextOverflow, isHovered, isSemitransparent, type, color, title, containerWidth, ...props }) => <a {...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 => {
|
||||
</Text.Body>
|
||||
</StyledLink>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
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 = {
|
||||
|
@ -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 (
|
||||
<div onContextMenu={this.onRowContextClick.bind(this, options)} style={style}>
|
||||
{data[index]}
|
||||
</div>
|
||||
)
|
||||
}, areEqual);
|
||||
|
||||
render() {
|
||||
const { manualHeight, itemHeight, children } = this.props;
|
||||
|
||||
@ -48,22 +59,10 @@ class RowContainer extends React.PureComponent {
|
||||
itemData={children}
|
||||
outerElementType={CustomScrollbarsVirtualList}
|
||||
>
|
||||
{RenderRow}
|
||||
{this.renderRow}
|
||||
</List>
|
||||
);
|
||||
|
||||
|
||||
const RenderRow = ({ data, index, style }) => {
|
||||
|
||||
const options = data[index].props.contextOptions;
|
||||
|
||||
return (
|
||||
<div onContextMenu={this.onRowContextClick.bind(this, options)} style={style}>
|
||||
{data[index]}
|
||||
</div>
|
||||
)
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledRowContainer id='rowContainer' manualHeight={manualHeight}>
|
||||
<AutoSizer>
|
||||
|
@ -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 (
|
||||
<StyledRow ref={this.rowRef} {...this.props}>
|
||||
{Object.prototype.hasOwnProperty.call(this.props, 'checked') &&
|
||||
<StyledCheckbox>
|
||||
<Checkbox isChecked={checked} onChange={this.changeCheckbox} />
|
||||
</StyledCheckbox>
|
||||
return (
|
||||
<StyledRow {...props}>
|
||||
{Object.prototype.hasOwnProperty.call(props, 'checked') &&
|
||||
<StyledCheckbox>
|
||||
<Checkbox isChecked={checked} onChange={changeCheckbox} />
|
||||
</StyledCheckbox>
|
||||
}
|
||||
{Object.prototype.hasOwnProperty.call(props, 'element') &&
|
||||
<StyledElement>
|
||||
{element}
|
||||
</StyledElement>
|
||||
}
|
||||
<StyledContent>
|
||||
{children}
|
||||
</StyledContent>
|
||||
<StyledOptionButton>
|
||||
{Object.prototype.hasOwnProperty.call(props, 'contextOptions') && contextOptions.length > 0 &&
|
||||
<ContextMenuButton directionX='right' getData={getOptions} />
|
||||
}
|
||||
{Object.prototype.hasOwnProperty.call(this.props, 'element') &&
|
||||
<StyledElement>
|
||||
{element}
|
||||
</StyledElement>
|
||||
}
|
||||
<StyledContent>
|
||||
{children}
|
||||
</StyledContent>
|
||||
<StyledOptionButton>
|
||||
{Object.prototype.hasOwnProperty.call(this.props, 'contextOptions') && contextOptions.length > 0 &&
|
||||
<ContextMenuButton directionX='right' getData={this.getOptions} />
|
||||
}
|
||||
</StyledOptionButton>
|
||||
</StyledRow>
|
||||
);
|
||||
}
|
||||
}
|
||||
</StyledOptionButton>
|
||||
</StyledRow>
|
||||
);
|
||||
};
|
||||
|
||||
Row.propTypes = {
|
||||
checked: PropTypes.bool,
|
||||
|
@ -462,6 +462,10 @@ 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)
|
||||
{
|
||||
@ -501,6 +505,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)
|
||||
{
|
||||
|
Loading…
Reference in New Issue
Block a user