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

This commit is contained in:
Alexey Safronov 2019-09-25 09:13:11 +03:00
commit 396b08527f
24 changed files with 827 additions and 197 deletions

View File

@ -52,12 +52,24 @@ namespace ASC.Api.Core.Auth
case ConfirmType.EmailChange:
checkKeyResult = EmailValidationKeyProvider.ValidateEmailKey(_email + _type + SecurityContext.CurrentAccount.ID, key, validInterval);
break;
case ConfirmType.PasswordChange:
var userHash = Request.TryGetValue("p", out var p) && p == "1";
var hash = string.Empty;
if (userHash)
{
var tenantId = CoreContext.TenantManager.GetCurrentTenant().TenantId;
hash = CoreContext.Authentication.GetUserPasswordHash(tenantId, CoreContext.UserManager.GetUserByEmail(tenantId, _email).ID);
}
checkKeyResult = EmailValidationKeyProvider.ValidateEmailKey(_email + _type + (string.IsNullOrEmpty(hash) ? string.Empty : Hasher.Base64Hash(hash)), key, validInterval);
break;
default:
checkKeyResult = EmailValidationKeyProvider.ValidateEmailKey(_email + _type, key, validInterval);
break;
}
var claims = new List<Claim>()
var claims = new List<Claim>()
{
new Claim(ClaimTypes.Role, _type.ToString())
};

View File

@ -102,7 +102,8 @@ namespace ASC.Core.Data
var groupQuery = new SqlQuery("core_usergroup cug")
.Select("cug.userid")
.Where(Exp.EqColumns("cug.tenant", "u.tenant"))
.Where(Exp.EqColumns("u.id", "cug.userid"));
.Where(Exp.EqColumns("u.id", "cug.userid"))
.Where("cug.removed", false);
foreach (var groups in includeGroups)
{

View File

@ -102,7 +102,7 @@ namespace ASC.Resource.Manager
using var resXResourceWriter = new ResXResourceWriter(zipFileName);
foreach (var word in toAdd.Where(r => r != null && !string.IsNullOrEmpty(r.ValueTo)).OrderBy(x => x.Title))
foreach (var word in toAdd.Where(r => r != null && (!string.IsNullOrEmpty(r.ValueTo) || language == "Neutral")).OrderBy(x => x.Title))
{
resXResourceWriter.AddResource(word.Title, word.ValueTo);
}

View File

@ -4,7 +4,7 @@ import { connect } from 'react-redux'
import { Avatar, Button, Textarea, toastr, AvatarEditor } from 'asc-web-components'
import { withTranslation } from 'react-i18next';
import { toEmployeeWrapper, getUserRole, getUserContactsPattern, getUserContacts, mapGroupsToGroupSelectorOptions, mapGroupSelectorOptionsToGroups, filterGroupSelectorOptions } from "../../../../../store/people/selectors";
import { createProfile, updateAvatar } from '../../../../../store/profile/actions';
import { createProfile, loadAvatar } from '../../../../../store/profile/actions';
import { MainContainer, AvatarContainer, MainFieldsContainer } from './FormFields/Form'
import TextField from './FormFields/TextField'
import PasswordField from './FormFields/PasswordField'
@ -414,6 +414,6 @@ export default connect(
mapStateToProps,
{
createProfile,
updateAvatar
loadAvatar
}
)(withRouter(withTranslation()(CreateUserForm)));

View File

@ -4,7 +4,7 @@ import { connect } from 'react-redux'
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, updateAvatar } from '../../../../../store/profile/actions';
import { updateProfile, loadAvatar, createThumbnailsAvatar, deleteAvatar } from '../../../../../store/profile/actions';
import { sendInstructionsToChangePassword, sendInstructionsToChangeEmail } from "../../../../../store/services/api";
import { MainContainer, AvatarContainer, MainFieldsContainer } from './FormFields/Form'
import TextField from './FormFields/TextField'
@ -46,14 +46,14 @@ class UpdateUserForm extends React.Component {
this.openAvatarEditor = this.openAvatarEditor.bind(this);
this.onSaveAvatar = this.onSaveAvatar.bind(this);
this.onCloseAvatarEditor = this.onCloseAvatarEditor.bind(this);
this.onLoadFileAvatar = this.onLoadFileAvatar.bind(this);
this.onShowGroupSelector = this.onShowGroupSelector.bind(this);
this.onCloseGroupSelector = this.onCloseGroupSelector.bind(this);
this.onSearchGroups = this.onSearchGroups.bind(this);
this.onSelectGroups = this.onSelectGroups.bind(this);
this.onRemoveGroup = this.onRemoveGroup.bind(this);
}
componentDidUpdate(prevProps, prevState) {
@ -87,6 +87,12 @@ class UpdateUserForm extends React.Component {
allOptions: allOptions,
options: [...allOptions],
selected: selected
},
avatar: {
tmpFile:"",
image: profile.avatarDefault ? "data:image/png;base64," + profile.avatarDefault : null,
defaultWidth: 0,
defaultHeight: 0
}
};
@ -295,27 +301,86 @@ class UpdateUserForm extends React.Component {
}
openAvatarEditor(){
let avatarDefault = this.state.profile.avatarDefault ? "data:image/png;base64," + this.state.profile.avatarDefault : null;
let _this = this;
if(avatarDefault !== null){
let img = new Image();
img.onload = function () {
_this.setState({
avatar:{
defaultWidth: img.width,
defaultHeight: img.height
}
})
};
img.src = avatarDefault;
}
this.setState({
visibleAvatarEditor: true,
});
}
onSaveAvatar(result) {
this.props.updateAvatar(this.state.profile.id, result)
onLoadFileAvatar(file) {
let data = new FormData();
let _this = this;
data.append("file", file);
data.append("Autosave", false);
this.props.loadAvatar(this.state.profile.id, data)
.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);
var img = new Image();
img.onload = function () {
var stateCopy = Object.assign({}, _this.state);
stateCopy.avatar = {
tmpFile: result.data.response.data,
image: result.data.response.data,
defaultWidth: img.width,
defaultHeight: img.height
}
_this.setState(stateCopy);
};
img.src = result.data.response.data;
})
.catch((error) => {
toastr.error(error.message);
});
}
onSaveAvatar(isUpdate, result) {
if(isUpdate){
this.props.createThumbnailsAvatar(this.state.profile.id, {
x: Math.round(result.x*this.state.avatar.defaultWidth - result.width/2),
y: Math.round(result.y*this.state.avatar.defaultHeight - result.height/2),
width: result.width,
height: result.height,
tmpFile: this.state.avatar.tmpFile
})
.then((result) => {
if(result.status === 200){
let stateCopy = Object.assign({}, this.state);
stateCopy.visibleAvatarEditor = false;
stateCopy.avatar.tmpFile = '';
stateCopy.profile.avatarMax = result.data.response.max + '?_='+Math.floor(Math.random() * Math.floor(10000));
toastr.success("Success");
this.setState(stateCopy);
}
})
.catch((error) => {
toastr.error(error.message);
});
}else{
this.props.deleteAvatar(this.state.profile.id)
.then((result) => {
if(result.status === 200){
let stateCopy = Object.assign({}, this.state);
stateCopy.visibleAvatarEditor = false;
stateCopy.profile.avatarMax = result.data.response.big;
toastr.success("Success");
this.setState(stateCopy);
}
})
.catch((error) => {
toastr.error(error.message);
});
}
}
onCloseAvatarEditor() {
this.setState({
visibleAvatarEditor: false,
@ -375,11 +440,13 @@ class UpdateUserForm extends React.Component {
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} />
<AvatarEditor
image={this.state.avatar.image}
visible={this.state.visibleAvatarEditor}
onClose={this.onCloseAvatarEditor}
onSave={this.onSaveAvatar}
onLoadFile={this.onLoadFileAvatar}
chooseFileLabel="Drop files here, or click to select files"/>
</AvatarContainer>
<MainFieldsContainer>
<TextChangeField
@ -555,6 +622,8 @@ export default connect(
mapStateToProps,
{
updateProfile,
updateAvatar
loadAvatar,
deleteAvatar,
createThumbnailsAvatar
}
)(withRouter(withTranslation()(UpdateUserForm)));

View File

@ -91,26 +91,36 @@ export function updateProfile(profile) {
});
};
};
export function updateAvatar(profileId, images) {
export function loadAvatar(profileId, data) {
return (dispatch, getState) => {
if (images.croppedImage) {
return api.updateAvatar(
profileId,
{
autosave: true,
base64CroppedImage: images.croppedImage.split(',')[1],
base64DefaultImage: images.defaultImage.split(',')[1]
}
).then(res => {
return api.loadAvatar(
profileId,
data
).then(res => {
checkResponseError(res);
return Promise.resolve(res);
});
};
};
export function createThumbnailsAvatar(profileId, data) {
return (dispatch, getState) => {
return api.createThumbnailsAvatar(
profileId,
data
).then(res => {
checkResponseError(res);
return Promise.resolve(res);
});
};
};
export function deleteAvatar(profileId) {
return (dispatch, getState) => {
return api.deleteAvatar(profileId)
.then(res => {
checkResponseError(res);
return Promise.resolve(res);
});
} else {
return api.deleteAvatar(profileId).then(res => {
checkResponseError(res);
return Promise.resolve(res);
});
}
};
};

View File

@ -73,10 +73,15 @@ export function updateUser(data) {
? fakeApi.updateUser()
: axios.put(`${API_URL}/people/${data.id}`, data);
}
export function updateAvatar(profileId, data) {
export function loadAvatar(profileId, data) {
return IS_FAKE
? fakeApi.updateAvatar()
: axios.post(`${API_URL}/people/${profileId}/photo/cropped`, data);
? fakeApi.loadAvatar()
: axios.post(`${API_URL}/people/${profileId}/photo`, data);
}
export function createThumbnailsAvatar(profileId, data) {
return IS_FAKE
? fakeApi.createThumbnailsAvatar()
: axios.post(`${API_URL}/people/${profileId}/photo/thumbnails.json`, data);
}
export function deleteAvatar(profileId) {

View File

@ -277,7 +277,10 @@ export function updateUser(data) {
return fakeResponse(data);
}
export function updateAvatar(data) {
export function loadAvatar(data) {
return fakeResponse(data);
}
export function createThumbnailsAvatar(data) {
return fakeResponse(data);
}
export function deleteAvatar(data) {

View File

@ -588,7 +588,7 @@ namespace ASC.Employee.Core.Controllers
return new ThumbnailsDataWrapper(Tenant, user.ID);
}
public FormFile Base64ToImage(string base64String, string fileName)
{
byte[] imageBytes = Convert.FromBase64String(base64String);
@ -617,7 +617,7 @@ namespace ASC.Employee.Core.Controllers
SecurityContext.DemandPermissions(Tenant, new UserSecurityProvider(userId), Constants.Action_EditUser);
var userPhoto = Base64ToImage(model.base64CroppedImage, "userPhoto_"+ userId.ToString());
var userPhoto = Base64ToImage(model.base64CroppedImage, "userPhoto_" + userId.ToString());
var defaultUserPhoto = Base64ToImage(model.base64DefaultImage, "defaultPhoto" + userId.ToString());
if (userPhoto.Length > SetupInfo.MaxImageUploadSize)
@ -693,9 +693,11 @@ namespace ASC.Employee.Core.Controllers
return result;
}
[Create("{userid}/photo")]
public FileUploadResult UploadMemberPhoto(string userid, UploadPhotoModel model)
public FileUploadResult UploadMemberPhoto(string userid, IFormCollection model)
{
var result = new FileUploadResult();
bool autosave = Boolean.Parse(model["Autosave"]);
try
{
if (model.Files.Count != 0)
@ -730,7 +732,7 @@ namespace ASC.Employee.Core.Controllers
CheckImgFormat(data);
if (model.Autosave)
if (autosave)
{
if (data.Length > SetupInfo.MaxImageUploadSize)
throw new ImageSizeLimitException();
@ -867,7 +869,7 @@ namespace ASC.Employee.Core.Controllers
}
[Update("{userid}/password")]
[Authorize(AuthenticationSchemes = "confirm", Roles = "EmailChange,Administrators")]
[Authorize(AuthenticationSchemes = "confirm", Roles = "PasswordChange,EmailChange,Administrators")]
public EmployeeWraperFull ChangeUserPassword(Guid userid, MemberModel memberModel)
{
ApiContext.AuthByClaim();

View File

@ -1,7 +1,124 @@
import React from 'react';
import React, { useState } from "react";
import { withRouter } from "react-router";
import { connect } from 'react-redux';
import { withTranslation } from 'react-i18next';
import styled from "styled-components";
import { Button, TextInput, PageLayout, Text } from "asc-web-components";
//import { useTranslation } from "react-i18next";
import { welcomePageTitle } from "../../../../helpers/customNames";
//import { login } from '../../../../../src/store/auth/actions';
const changePhoneForm = (props) => {
return (<span>{props.location.pathname}</span>);
const BodyStyle = styled.div`
margin: 70px auto 0 auto;
max-width: 432px;
.edit-header {
.header-logo {
max-width: 216px;
max-height: 35px;
}
.header-title {
word-wrap: break-word;
margin: 8px 0;
text-align: left;
font-size: 24px;
color: #116d9d;
}
}
.edit-text {
margin-bottom: 18px;
}
.edit-input {
margin-bottom: 24px;
}
`;
const PhoneForm = props => {
const { t, currentPhone } = props;
const [phone, setPhone] = useState(currentPhone);
const [isLoading, setIsLoading] = useState(false);
const subTitleTranslation = `Enter mobile phone number`;
const infoTranslation = `Your current mobile phone number`;
const subInfoTranslation = `The two-factor authentication is enabled to provide additional portal security.
Enter your mobile phone number to continue work on the portal.
Mobile phone number must be entered using an international format with country code.`;
const phonePlaceholder = `Phone`;
const buttonTranslation = `Enter number`;
const onSubmit = () => {
console.log("onSubmit CHANGE");
};
const onKeyPress = target => {
if (target.code === "Enter") onSubmit();
};
const simplePhoneMask = new Array(15).fill(/\d/);
return (
<BodyStyle>
<div className="edit-header">
<img className="header-logo" src="images/dark_general.png" alt="Logo" />
<div className="header-title">
{t("CustomWelcomePageTitle", { welcomePageTitle })}
</div>
</div>
<Text.Body className="edit-text" isBold fontSize={14}>{subTitleTranslation}</Text.Body>
<Text.Body fontSize={13}>{infoTranslation}: <b>+{currentPhone}</b></Text.Body>
<Text.Body className="edit-text" fontSize={13}>{subInfoTranslation}</Text.Body>
<TextInput
id="phone"
name="phone"
type="text"
size="huge"
scale={true}
isAutoFocussed={true}
tabIndex={1}
autocomple="off"
placeholder={phonePlaceholder}
onChange={event => {
setPhone(event.target.value);
onKeyPress(event.target);
}}
value={phone}
hasError={false}
isDisabled={isLoading}
onKeyDown={event => onKeyPress(event.target)}
guide={false}
mask={simplePhoneMask}
className="edit-input"
/>
<Button
primary
size="big"
tabIndex={3}
label={
isLoading ? t("LoadingProcessing") : buttonTranslation
}
isDisabled={isLoading}
isLoading={isLoading}
onClick={onSubmit}
/>
</BodyStyle>
);
}
export default changePhoneForm;
const ChangePhoneForm = props => {
return <PageLayout sectionBodyContent={<PhoneForm {...props} />} />;
};
function mapStateToProps(state) {
return {
isLoaded: state.auth.isLoaded,
currentPhone: state.auth.user.mobilePhone
};
}
export default connect(mapStateToProps)(withRouter(withTranslation()(ChangePhoneForm)));

View File

@ -1809,7 +1809,9 @@ asap@~2.0.6:
rc-tree "^2.1.2"
react-autosize-textarea "^7.0.0"
react-avatar-edit "^0.8.3"
react-avatar-editor "^11.0.7"
react-custom-scrollbars "^4.2.1"
react-dropzone "^10.1.8"
react-text-mask "^5.4.3"
react-toastify "^5.3.2"
react-virtualized-auto-sizer "^1.0.2"
@ -1897,6 +1899,13 @@ atob@^2.1.1:
resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9"
integrity sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==
attr-accept@^1.1.3:
version "1.1.3"
resolved "https://registry.yarnpkg.com/attr-accept/-/attr-accept-1.1.3.tgz#48230c79f93790ef2775fcec4f0db0f5db41ca52"
integrity sha512-iT40nudw8zmCweivz6j58g+RT33I4KbaIvRUhjNmDwO2WmsQUxFEZZYZ5w3vXe5x5MX9D7mfvA/XaLOZYFR9EQ==
dependencies:
core-js "^2.5.0"
autoprefixer@^9.6.1:
version "9.6.1"
resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-9.6.1.tgz#51967a02d2d2300bb01866c1611ec8348d355a47"
@ -2961,7 +2970,7 @@ core-js@3.1.4:
resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.1.4.tgz#3a2837fc48e582e1ae25907afcd6cf03b0cc7a07"
integrity sha512-YNZN8lt82XIMLnLirj9MhKDFZHalwzzrL9YLt6eb0T5D0EDl4IQ90IGkua8mHbnxNrkj1d8hbdizMc0Qmg1WnQ==
core-js@^2.4.0, core-js@^2.6.4:
core-js@^2.4.0, core-js@^2.5.0, core-js@^2.6.4:
version "2.6.9"
resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.6.9.tgz#6b4b214620c834152e179323727fc19741b084f2"
integrity sha512-HOpZf6eXmnl7la+cUdMnLvUxKNqLUzJvgIziQ0DiF3JwSImNphIqdGqzj6hIKyX04MmV0poclQ7+wjWvxQyR2A==
@ -4313,6 +4322,13 @@ file-loader@3.0.1:
loader-utils "^1.0.2"
schema-utils "^1.0.0"
file-selector@^0.1.11:
version "0.1.12"
resolved "https://registry.yarnpkg.com/file-selector/-/file-selector-0.1.12.tgz#fe726547be219a787a9dcc640575a04a032b1fd0"
integrity sha512-Kx7RTzxyQipHuiqyZGf+Nz4vY9R1XGxuQl/hLoJwq+J4avk/9wxxgZyHKtbyIPJmbD4A66DWGYfyykWNpcYutQ==
dependencies:
tslib "^1.9.0"
filesize@3.6.1:
version "3.6.1"
resolved "https://registry.yarnpkg.com/filesize/-/filesize-3.6.1.tgz#090bb3ee01b6f801a8a8be99d31710b3422bb317"
@ -8736,6 +8752,13 @@ react-avatar-edit@^0.8.3:
dependencies:
konva "2.5.1"
react-avatar-editor@^11.0.7:
version "11.0.7"
resolved "https://registry.yarnpkg.com/react-avatar-editor/-/react-avatar-editor-11.0.7.tgz#021053cfeaa138407b79279ee5a0384f273f0c54"
integrity sha512-GbNYBd1/L1QyuU9VRvOW0hSkW1R0XSneOWZFgqI5phQf6dX+dF/G3/AjiJ0hv3JWh2irMQ7DL0oYDKzwtTnNBQ==
dependencies:
prop-types "^15.5.8"
react-custom-scrollbars@^4.2.1:
version "4.2.1"
resolved "https://registry.yarnpkg.com/react-custom-scrollbars/-/react-custom-scrollbars-4.2.1.tgz#830fd9502927e97e8a78c2086813899b2a8b66db"
@ -8786,6 +8809,15 @@ react-dom@^16.9.0:
prop-types "^15.6.2"
scheduler "^0.15.0"
react-dropzone@^10.1.8:
version "10.1.9"
resolved "https://registry.yarnpkg.com/react-dropzone/-/react-dropzone-10.1.9.tgz#8093ecd7d2dc4002280eb2dac1d5fa4216c800ee"
integrity sha512-7iqALZ0mzk+4g/AsYxEy3QyWPMTVQYKQVkYUe9zIbH18u+pi7EBDg010KEwfIX6jeTDH2qP0E6/eUnXvBYrovA==
dependencies:
attr-accept "^1.1.3"
file-selector "^0.1.11"
prop-types "^15.7.2"
react-error-overlay@^6.0.1:
version "6.0.1"
resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-6.0.1.tgz#b8d3cf9bb991c02883225c48044cb3ee20413e0f"

View File

@ -1,6 +1,6 @@
{
"name": "asc-web-components",
"version": "1.0.94",
"version": "1.0.95",
"description": "Ascensio System SIA component library",
"license": "AGPL-3.0",
"main": "dist/asc-web-components.js",
@ -103,7 +103,9 @@
"rc-tree": "^2.1.2",
"react-autosize-textarea": "^7.0.0",
"react-avatar-edit": "^0.8.3",
"react-avatar-editor": "^11.0.7",
"react-custom-scrollbars": "^4.2.1",
"react-dropzone": "^10.1.8",
"react-lifecycles-compat": "^3.0.4",
"react-text-mask": "^5.4.3",
"react-toastify": "^5.3.2",

View File

@ -28,7 +28,6 @@ Required to display user avatar editor on page.
| `headerLabel` | `string` | - | | `Edit Photo` | |
| `saveButtonLabel` | `string` | - | | `Save` | |
| `cancelButtonLabel` | `string` | - | | `Cancel` | |
| `maxSizeErrorLabel` | `string` | - | | `File is too big` | |
| `maxSize` | `number` | - | | `1` | Max size of image |
| `onSave` | `function` | - | | | |
| `onClose` | `function` | - | | | |

View File

@ -20,12 +20,22 @@ class AvatarEditorStory extends React.Component {
this.openEditor = this.openEditor.bind(this);
this.onClose = this.onClose.bind(this);
this.onSave = this.onSave.bind(this);
this.onLoadFile = this.onLoadFile.bind(this)
this.onImageChange = this.onImageChange.bind(this)
}
onSave(result){
action('onSave')(result);
onImageChange(img){
action('onLoadFile');
this.setState({
userImage: img
})
}
onLoadFile(file){
action('onLoadFile')(file);
}
onSave(isUpdate, data){
action('onSave')(isUpdate, data);
this.setState({
userImage: result.croppedImage,
isOpen: false
})
}
@ -54,7 +64,8 @@ class AvatarEditorStory extends React.Component {
visible={this.state.isOpen}
onClose={this.onClose}
onSave={this.onSave}
/>
onImageChange={this.onImageChange}
/>
</div>
)
}

View File

@ -1,145 +1,68 @@
import React, { memo } from 'react'
import styled, { css } from 'styled-components'
import PropTypes from 'prop-types'
import ModalDialog from '../modal-dialog'
import Button from '../button'
import { Text } from '../text'
import Avatar from 'react-avatar-edit'
import { default as ASCAvatar } from '../avatar/index'
const StyledASCAvatar = styled(ASCAvatar)`
display: inline-block;
vertical-align: bottom;
`;
const StyledAvatarContainer = styled.div`
text-align: center;
div:first-child {
margin: 0 auto;
}
`;
class AvatarEditorBody extends React.Component {
constructor(props) {
super(props);
this.state = {
croppedImage: null,
src: this.props.image,
hasMaxSizeError: false
}
this.onCrop = this.onCrop.bind(this)
this.onClose = this.onClose.bind(this)
this.onBeforeFileLoad = this.onBeforeFileLoad.bind(this)
this.onFileLoad = this.onFileLoad.bind(this)
}
onClose() {
this.props.onCloseEditor();
this.setState({ croppedImage: null })
}
onCrop(croppedImage) {
this.props.onCropImage(croppedImage);
this.setState({ croppedImage })
}
onBeforeFileLoad(elem) {
if (elem.target.files[0].size > this.props.maxSize * 1000000) {
this.setState({
hasMaxSizeError: true
});
elem.target.value = "";
}else if(this.state.hasMaxSizeError){
this.setState({
hasMaxSizeError: false
});
}
}
onFileLoad(file){
let reader = new FileReader();
let _this = this;
reader.onloadend = () => {
_this.props.onFileLoad(reader.result);
};
reader.readAsDataURL(file)
}
render() {
return (
<StyledAvatarContainer>
<Avatar
width={400}
height={295}
imageWidth={400}
cropRadius={50}
onCrop={this.onCrop}
onClose={this.onClose}
onBeforeFileLoad={this.onBeforeFileLoad}
onFileLoad={this.onFileLoad}
label={this.props.label}
src={this.state.src}
/>
{this.state.croppedImage && (
<div>
<StyledASCAvatar
size='max'
role='user'
source={this.state.croppedImage}
editing={false}
/>
<StyledASCAvatar
size='big'
role='user'
source={this.state.croppedImage}
editing={false}
/>
</div>
)
}
{
this.state.hasMaxSizeError &&
<Text.Body as='span' color="#ED7309" isBold={true}>
{this.props.maxSizeErrorLabel}
</Text.Body>
}
</StyledAvatarContainer>
);
}
}
import AvatarEditorBody from './sub-components/avatar-editor-body'
class AvatarEditor extends React.Component {
constructor(props) {
super(props);
this.state = {
defaultImage: null,
croppedImage: null,
visible: props.value
};
isContainsFile: !!this.props.image,
visible: props.visible,
x: 0,
y: 0,
width: 0,
height:0
}
this.onClose = this.onClose.bind(this);
this.onCropImage = this.onCropImage.bind(this);
this.onCloseEditor = this.onCloseEditor.bind(this);
this.onFileLoad = this.onFileLoad.bind(this);
this.onSaveButtonClick = this.onSaveButtonClick.bind(this);
this.onImageChange = this.onImageChange.bind(this);
this.onLoadFileError = this.onLoadFileError.bind(this);
this.onLoadFile = this.onLoadFile.bind(this);
this.onPositionChange = this.onPositionChange.bind(this);
this.onDeleteImage = this.onDeleteImage.bind(this)
}
onFileLoad(file){
this.setState({ defaultImage: file });
onImageChange(file){
if(typeof this.props.onImageChange === 'function') this.props.onImageChange(file);
}
onDeleteImage(){
this.setState({
isContainsFile: false
})
if(typeof this.props.onDeleteImage === 'function') this.props.onDeleteImage();
}
onPositionChange(data){
this.setState(data);
}
onLoadFileError(error) {
switch (error) {
case 0:
break;
case 1:
break;
case 2:
break;
}
}
onLoadFile(file) {
if(typeof this.props.onLoadFile === 'function') this.props.onLoadFile(file);
this.setState({ isContainsFile: true });
}
onSaveButtonClick() {
this.props.onSave({
defaultImage: this.state.defaultImage ? this.state.defaultImage : this.props.image,
croppedImage: this.state.croppedImage
});
this.setState({ visible: false });
}
onCloseEditor() {
this.setState({
croppedImage: null
});
}
onCropImage(result) {
this.setState({
croppedImage: result
this.props.onSave(this.state.isContainsFile, {
x: this.state.x,
y: this.state.y,
width: this.state.width,
height: this.state.height
});
}
@ -160,13 +83,15 @@ class AvatarEditor extends React.Component {
headerContent={this.props.headerLabel}
bodyContent={
<AvatarEditorBody
maxSize={this.props.maxSize}
onImageChange={this.onImageChange}
onPositionChange={this.onPositionChange}
onLoadFileError={this.onLoadFileError}
onLoadFile={this.onLoadFile}
deleteImage={this.onDeleteImage}
maxSize={this.props.maxSize * 1000000} // megabytes to bytes
accept={this.props.accept}
image={this.props.image}
onCropImage={this.onCropImage}
onCloseEditor={this.onCloseEditor}
label={this.props.chooseFileLabel}
maxSizeErrorLabel={this.props.maxSizeErrorLabel}
onFileLoad={this.onFileLoad}
chooseFileLabel={this.props.chooseFileLabel}
/>
}
footerContent={[
@ -198,15 +123,17 @@ AvatarEditor.propTypes = {
image: PropTypes.string,
cancelButtonLabel: PropTypes.string,
maxSize: PropTypes.number,
accept: PropTypes.arrayOf(PropTypes.string),
onSave: PropTypes.func,
onClose: PropTypes.func
onClose: PropTypes.func,
onDeleteImage: PropTypes.func,
onLoadFile: PropTypes.func,
onImageChange: PropTypes.func,
};
AvatarEditor.defaultProps = {
visible: false,
maxSize: 1, //1MB
chooseFileLabel: 'Choose a file',
headerLabel: 'Edit Photo',
saveButtonLabel: 'Save',
cancelButtonLabel: 'Cancel',

View File

@ -0,0 +1,303 @@
import React from 'react'
import styled from 'styled-components'
import Dropzone from 'react-dropzone'
import ReactAvatarEditor from 'react-avatar-editor'
import PropTypes from 'prop-types'
import { default as ASCAvatar } from '../../avatar/index'
import accepts from 'attr-accept'
import { tablet } from '../../../utils/device';
const CloseButton = styled.a`
cursor: pointer;
position: absolute;
right: 33px;
top: 4px;
width: 16px;
height: 16px;
&:before, &:after {
position: absolute;
left: 8px;
content: ' ';
height: 16px;
width: 1px;
background-color: #D8D8D8;
}
&:before {
transform: rotate(45deg);
}
&:after {
transform: rotate(-45deg);
}
@media ${tablet} {
right: calc(50% - 147px);
}
`;
const DropZoneContainer = styled.div`
display: block;
width: 100%;
height: 300px;
border: 1px dashed #ccc;
text-align: center;
padding: 10em 0;
margin: 0 auto;
p{
margin: 0;
cursor: default
}
`;
const StyledAvatarContainer = styled.div`
text-align: center;
.custom-range{
width: 300px;
}
.avatar-container{
display: inline-block;
vertical-align: top;
}
.editor-container{
display: inline-block;
width: calc(100% - 170px);
position: relative;
@media ${tablet} {
width: 100%
}
}
`;
const StyledASCAvatar = styled(ASCAvatar)`
display: block;
@media ${tablet} {
display: none
}
`;
class AvatarEditorBody extends React.Component {
constructor(props) {
super(props);
this.state = {
image: this.props.image ? this.props.image : "",
scale: 1,
croppedImage: ''
}
this.setEditorRef = React.createRef();
this.handleScale = this.handleScale.bind(this);
this.onWheel = this.onWheel.bind(this);
this.onTouchStart = this.onTouchStart.bind(this);
this.onTouchMove = this.onTouchMove.bind(this);
this.onTouchEnd = this.onTouchEnd.bind(this);
this.distance = this.distance.bind(this);
this.onImageChange = this.onImageChange.bind(this);
this.onImageReady = this.onImageReady.bind(this);
this.deleteImage = this.deleteImage.bind(this);
this.onDropAccepted = this.onDropAccepted.bind(this);
this.onPositionChange = this.onPositionChange.bind(this);
}
onPositionChange(position) {
this.props.onPositionChange({
x: position.x,
y: position.y,
width: this.setEditorRef.current.getImage().width,
height: this.setEditorRef.current.getImage().height
});
}
onDropRejected(rejectedFiles) {
if (!accepts(rejectedFiles[0], this.props.accept)) {
this.props.onLoadFileError(0);
return;
} else if (rejectedFiles[0].size > this.props.maxSize) {
this.props.onLoadFileError(1);
return;
}
this.props.onLoadFileError(2);
}
onDropAccepted(acceptedFiles) {
this.setState({
image: acceptedFiles[0]
});
this.props.onLoadFile(acceptedFiles[0]);
}
deleteImage() {
this.setState({
image: '',
croppedImage: ''
});
this.props.deleteImage();
}
onImageChange() {
this.setState({
croppedImage: this.setEditorRef.current.getImage().toDataURL()
});
this.props.onImageChange(this.setEditorRef.current.getImage().toDataURL());
}
dist = 0
scaling = false
curr_scale = 1.0
scale_factor = 1.0
distance(p1, p2) {
return (Math.sqrt(Math.pow((p1.clientX - p2.clientX), 2) + Math.pow((p1.clientY - p2.clientY), 2)));
}
onTouchStart(evt) {
evt.preventDefault();
var tt = evt.targetTouches;
if (tt.length >= 2) {
this.dist = this.distance(tt[0], tt[1]);
this.scaling = true;
} else {
this.scaling = false;
}
}
onTouchMove(evt) {
evt.preventDefault();
var tt = evt.targetTouches;
if (this.scaling) {
this.curr_scale = this.distance(tt[0], tt[1]) / this.dist * this.scale_factor;
let scale = (Math.round(this.curr_scale) * 10) / 10;
this.setState({
scale: scale < 1 ? 1 : scale > 5 ? 5 : scale
});
}
}
onTouchEnd(evt) {
var tt = evt.targetTouches;
if (tt.length < 2) {
this.scaling = false;
if (this.curr_scale < 1) {
this.scale_factor = 1;
} else {
if (this.curr_scale > 5) {
this.scale_factor = 5;
} else {
this.scale_factor = this.curr_scale;
}
}
} else {
this.scaling = true;
}
}
onWheel(e) {
e = e || window.event;
const delta = e.deltaY || e.detail || e.wheelDelta;
let scale = delta > 0 && this.state.scale === 1 ? 1 : this.state.scale - (delta / 100) * 0.1;
scale = Math.round(scale * 10) / 10;
this.setState({
scale: scale < 1 ? 1 : scale > 5 ? 5 : scale
});
}
handleScale = e => {
const scale = parseFloat(e.target.value);
this.setState({ scale })
};
onImageReady() {
this.setState({
croppedImage: this.setEditorRef.current.getImage().toDataURL()
});
this.props.onPositionChange({
x: 0.5,
y: 0.5,
width: this.setEditorRef.current.getImage().width,
height: this.setEditorRef.current.getImage().height
});
}
render() {
return (
<div
onWheel={this.onWheel}
onTouchStart={this.onTouchStart}
onTouchMove={this.onTouchMove}
onTouchEnd={this.onTouchEnd}
>
{this.state.image === '' ?
<Dropzone
onDropAccepted={this.onDropAccepted}
onDropRejected={this.onDropRejected}
maxSize={this.props.maxSize}>
{({ getRootProps, getInputProps }) => (
<DropZoneContainer
{...getRootProps()}
>
<input {...getInputProps()} />
<p>{this.props.chooseFileLabel}</p>
</DropZoneContainer>
)}
</Dropzone> :
<StyledAvatarContainer>
<div className='editor-container'>
<ReactAvatarEditor
ref={this.setEditorRef}
width={250}
borderRadius={200}
scale={this.state.scale}
height={250}
className="react-avatar-editor"
image={this.state.image}
color={[0, 0, 0, .5]}
onImageChange={this.onImageChange}
onPositionChange={this.onPositionChange}
onImageReady={this.onImageReady}
/>
<input
id='scale'
type='range'
className='custom-range'
onChange={this.handleScale}
min={this.state.allowZoomOut ? '0.1' : '1'}
max='5'
step='0.01'
value={this.state.scale}
/>
<CloseButton onClick={this.deleteImage}></CloseButton>
</div>
<div className='avatar-container'>
<StyledASCAvatar
size='max'
role='user'
source={this.state.croppedImage}
editing={false}
/>
<StyledASCAvatar
size='big'
role='user'
source={this.state.croppedImage}
editing={false}
/>
</div>
</StyledAvatarContainer>
}
</div>
);
}
}
AvatarEditorBody.propTypes = {
onImageChange: PropTypes.func,
onPositionChange: PropTypes.func,
onLoadFileError: PropTypes.func,
onLoadFile: PropTypes.func,
deleteImage: PropTypes.func,
maxSize: PropTypes.number,
image: PropTypes.string,
accept: PropTypes.arrayOf(PropTypes.string),
chooseFileLabel: PropTypes.string,
};
AvatarEditorBody.defaultProps = {
accept: ['image/png', 'image/jpeg'],
maxSize: Number.MAX_SAFE_INTEGER,
chooseFileLabel: "Drop files here, or click to select files"
};
export default AvatarEditorBody;

View File

@ -2615,6 +2615,13 @@ atob@^2.1.1:
resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9"
integrity sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==
attr-accept@^1.1.3:
version "1.1.3"
resolved "https://registry.yarnpkg.com/attr-accept/-/attr-accept-1.1.3.tgz#48230c79f93790ef2775fcec4f0db0f5db41ca52"
integrity sha512-iT40nudw8zmCweivz6j58g+RT33I4KbaIvRUhjNmDwO2WmsQUxFEZZYZ5w3vXe5x5MX9D7mfvA/XaLOZYFR9EQ==
dependencies:
core-js "^2.5.0"
autoprefixer@^9.4.9:
version "9.6.1"
resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-9.6.1.tgz#51967a02d2d2300bb01866c1611ec8348d355a47"
@ -5321,6 +5328,13 @@ file-loader@^3.0.1:
loader-utils "^1.0.2"
schema-utils "^1.0.0"
file-selector@^0.1.11:
version "0.1.12"
resolved "https://registry.yarnpkg.com/file-selector/-/file-selector-0.1.12.tgz#fe726547be219a787a9dcc640575a04a032b1fd0"
integrity sha512-Kx7RTzxyQipHuiqyZGf+Nz4vY9R1XGxuQl/hLoJwq+J4avk/9wxxgZyHKtbyIPJmbD4A66DWGYfyykWNpcYutQ==
dependencies:
tslib "^1.9.0"
file-system-cache@^1.0.5:
version "1.0.5"
resolved "https://registry.yarnpkg.com/file-system-cache/-/file-system-cache-1.0.5.tgz#84259b36a2bbb8d3d6eb1021d3132ffe64cfff4f"
@ -9520,6 +9534,13 @@ react-avatar-edit@^0.8.3:
dependencies:
konva "2.5.1"
react-avatar-editor@^11.0.7:
version "11.0.7"
resolved "https://registry.yarnpkg.com/react-avatar-editor/-/react-avatar-editor-11.0.7.tgz#021053cfeaa138407b79279ee5a0384f273f0c54"
integrity sha512-GbNYBd1/L1QyuU9VRvOW0hSkW1R0XSneOWZFgqI5phQf6dX+dF/G3/AjiJ0hv3JWh2irMQ7DL0oYDKzwtTnNBQ==
dependencies:
prop-types "^15.5.8"
react-clientside-effect@^1.2.0:
version "1.2.2"
resolved "https://registry.yarnpkg.com/react-clientside-effect/-/react-clientside-effect-1.2.2.tgz#6212fb0e07b204e714581dd51992603d1accc837"
@ -9610,6 +9631,15 @@ react-draggable@^3.1.1:
classnames "^2.2.5"
prop-types "^15.6.0"
react-dropzone@^10.1.8:
version "10.1.8"
resolved "https://registry.yarnpkg.com/react-dropzone/-/react-dropzone-10.1.8.tgz#348895a3ee9efe7c0f6a2f19642f04704c170757"
integrity sha512-Lm6+TxIDf/my4i3VdYmufRcrJ4SUbSTJP3HB49V2+HNjZwLI4NKVkaNRHwwSm9CEuzMP+6SW7pT1txc1uBPfDg==
dependencies:
attr-accept "^1.1.3"
file-selector "^0.1.11"
prop-types "^15.7.2"
react-element-to-jsx-string@^14.0.2:
version "14.0.3"
resolved "https://registry.yarnpkg.com/react-element-to-jsx-string/-/react-element-to-jsx-string-14.0.3.tgz#64f50fdbf6ba154d6439da3d7307f79069b94d58"

View File

@ -904,6 +904,9 @@ ONLYOFFICE-Team</value>
<data name="subject_change_tfa" xml:space="preserve">
<value>$ {LetterLogoText}. Anfrage zur Änderung der Sicherungscodes</value>
</data>
<data name="subject_confirm_owner_change" xml:space="preserve">
<value>${LetterLogoText}. Änderung des Portalsbesitzer</value>
</data>
<data name="subject_dns_change" xml:space="preserve">
<value>${LetterLogoText}. Änderung der Portaladresse</value>
</data>

View File

@ -1621,6 +1621,9 @@ El enlace es válido solo durante 7 días.</value>
<data name="subject_change_tfa" xml:space="preserve">
<value>${LetterLogoText}. Solicitud de cambio de códigos de respaldo</value>
</data>
<data name="subject_confirm_owner_change" xml:space="preserve">
<value>${LetterLogoText}. Cambio de dueño de portal</value>
</data>
<data name="subject_dns_change" xml:space="preserve">
<value>${LetterLogoText}. Cambio de dirección de portal</value>
</data>

View File

@ -996,6 +996,9 @@ Meilleures salutations,
<data name="subject_change_tfa" xml:space="preserve">
<value>${LetterLogoText}. Demande de changement des codes de sauvegarde</value>
</data>
<data name="subject_confirm_owner_change" xml:space="preserve">
<value>${LetterLogoText}. Changement du propriétaire du portail </value>
</data>
<data name="subject_dns_change" xml:space="preserve">
<value>${LetterLogoText}. Changement de l'adresse du portail</value>
</data>

View File

@ -351,6 +351,35 @@ Vai su Impostazioni &gt;&gt; Comuni &gt;&gt; Personalizzazione.</value>
<data name="pattern_enterprise_admin_customize_portal_v10_item_regional_hdr" xml:space="preserve">
<value>Suggerimento #2: Regola le impostazioni regionali</value>
</data>
<data name="pattern_enterprise_admin_without_activity_v10" xml:space="preserve">
<value>Salve, $UserName!
Ci auguriamo che ti piaccia usare ONLYOFFICE. Come sta procedendo con la versione di prova? Gradiremmo molto il tuo feedback!
Per favore, non esitare a contattarci a "support@onlyoffice.com":"mailto: support@onlyoffice.com" tutte le volte che avete domande o idee, suggerimenti.</value>
</data>
<data name="pattern_enterprise_guest_activation_v10" xml:space="preserve">
<value>Salve, $UserName!
$__AuthorName ti ha invitato come ospite su "${__VirtualRootPath}":"${__VirtualRootPath}". Accetta l'invito cliccando il link:
$GreenButton
Il link è valido solo per 7 giorni.
Otterrai più consigli su come usare il tuo web-office. Puoi cancellare le sottoscrizioni dalla pagina del tuo profilo in ogni momento e riabilitarle.</value>
</data>
<data name="pattern_enterprise_whitelabel_guest_activation_v10" xml:space="preserve">
<value>Salve, $UserName!
$__AuthorName ti ha invitato come ospite su "${__VirtualRootPath}":"${__VirtualRootPath}". Accetta l'invito cliccando il link:
$GreenButton
Il link è valido solo per 7 giorni.
Otterrai più consigli su come usare il tuo web-office. Puoi cancellare le sottoscrizioni dalla pagina del tuo profilo in ogni momento e riabilitarle.</value>
</data>
<data name="pattern_for_admin_notify" xml:space="preserve">
<value>h1.Messaggio dal portale "${__VirtualRootPath}":"${__VirtualRootPath}"
@ -919,6 +948,9 @@ Team di supporto ONLYOFFICE®
<data name="pattern_saas_admin_user_docs_tips_v10_item_review_hdr" xml:space="preserve">
<value>Revisiona, commenta e discuti</value>
</data>
<data name="pattern_saas_admin_user_docs_tips_v10_item_share" xml:space="preserve">
<value>Condividi documenti a singoli o gruppi di utenti. Scegli il livello di accesso: sola lettura, commento, compilazione modulo, revisione o accesso completo.</value>
</data>
<data name="pattern_saas_admin_user_docs_tips_v10_item_share_hdr" xml:space="preserve">
<value>Condividi documenti</value>
</data>
@ -960,6 +992,17 @@ il team ONLYOFFICE
<data name="pattern_saas_admin_welcome_v10_item_usertrack" xml:space="preserve">
<value>Traccia Accesso utenti e altre azioni.</value>
</data>
<data name="pattern_saas_admin_without_activity_v10" xml:space="preserve">
<value>Salve, $UserName!
Ci auguriamo che ti piaccia usare ONLYOFFICE. Come sta procedendo con la versione di prova? Gradiremmo molto il tuo feedback!
Per favore, non esitare a contattarci a "support@onlyoffice.com":"mailto: support@onlyoffice.com" tutte le volte che avete domande o idee, suggerimenti.
Distinti saluti,
Squadra ONLYOFFICE
"www.onlyoffice.com":"http://onlyoffice.com/"</value>
</data>
<data name="pattern_saas_guest_activation_v10" xml:space="preserve">
<value>Salve!
@ -971,6 +1014,25 @@ Ti invieremo anche suggerimenti utili, ultime notizie su ONLYOFFICE e offerte sp
Cordialmente,
il team di ONLYOFFICE
"www.onlyoffice.com":"http://onlyoffice.com/"</value>
</data>
<data name="pattern_saas_guest_welcome_v10" xml:space="preserve">
<value>Salve!
Il tuo profilo ospite è stato aggiunto con successo a "${__VirtualRootPath}":"${__VirtualRootPath}". Adesso puoi:
# Modificare il tuo "profilo":"$MyStaffLink".
# Visualizza e commenta il contenuto disponibile nella "Community":"${__VirtualRootPath}/products/community/" e "Progetti":"${__VirtualRootPath}/products/projects/".
# Aggiungi e scarica i file disponibili per te in "Documenti":"${__VirtualRootPath}/products/files/".
# Organizza il tuo piano con il "Calendario":"${__VirtualRootPath}/addons/calendar/" incorporato.
# Usa la Use "Chat":"${__VirtualRootPath}/addons/talk/" per scambiare messaggi istantanei.
$GreenButton
Se hai bisogno di aiuto, consulta il nostro "Centro assistenza":"${__HelpLink}" o chiedi alla nostra "squadra di supporto":"https://support.onlyoffice.com".
Cordialmente,
la squadra ONLYOFFICE
"www.onlyoffice.com":"http://onlyoffice.com/"</value>
</data>
<data name="pattern_saas_user_activation_v10" xml:space="preserve">
@ -1080,6 +1142,9 @@ il team di ONLYOFFICE</value>
<data name="subject_change_tfa" xml:space="preserve">
<value>${LetterLogoText}. Richiesta di modifica dei codici di backup</value>
</data>
<data name="subject_confirm_owner_change" xml:space="preserve">
<value>${LetterLogoText}. Cambio del proprietario del portale</value>
</data>
<data name="subject_dns_change" xml:space="preserve">
<value>${LetterLogoText}. Cambio dell'indirizzo del portale.</value>
</data>
@ -1089,6 +1154,9 @@ il team di ONLYOFFICE</value>
<data name="subject_enterprise_admin_customize_portal_v10" xml:space="preserve">
<value>Personalizza ONLYOFFICE</value>
</data>
<data name="subject_enterprise_admin_invite_teammates_v10" xml:space="preserve">
<value>Invita i tuoi compagni di squadra</value>
</data>
<data name="subject_enterprise_admin_payment_warning_before7_v10" xml:space="preserve">
<value>Notifica di rinnovo ONLYOFFICE®</value>
</data>
@ -1110,9 +1178,15 @@ il team di ONLYOFFICE</value>
<data name="subject_enterprise_admin_without_activity_v10" xml:space="preserve">
<value>Attenzionato dal team ONLYOFFICE </value>
</data>
<data name="subject_enterprise_guest_activation_v10" xml:space="preserve">
<value>Unirsi su ${__VirtualRootPath}</value>
</data>
<data name="subject_enterprise_guest_welcome_v10" xml:space="preserve">
<value>Benvenuti sul vostro web-office</value>
</data>
<data name="subject_enterprise_user_activation_v10" xml:space="preserve">
<value>Unirsi su ${__VirtualRootPath}</value>
</data>
<data name="subject_enterprise_user_welcome_v10" xml:space="preserve">
<value>Benvenuti sul vostro web-office</value>
</data>
@ -1131,9 +1205,15 @@ il team di ONLYOFFICE</value>
<data name="subject_enterprise_whitelabel_admin_welcome_v10" xml:space="preserve">
<value>Rendi il tuo ufficio online più sicuro</value>
</data>
<data name="subject_enterprise_whitelabel_guest_activation_v10" xml:space="preserve">
<value>Unirsi su ${__VirtualRootPath}</value>
</data>
<data name="subject_enterprise_whitelabel_guest_welcome_v10" xml:space="preserve">
<value>Benvenuti sul vostro web-office</value>
</data>
<data name="subject_enterprise_whitelabel_user_activation_v10" xml:space="preserve">
<value>Unirsi su ${__VirtualRootPath}</value>
</data>
<data name="subject_enterprise_whitelabel_user_welcome_v10" xml:space="preserve">
<value>Benvenuti sul vostro web-office</value>
</data>
@ -1260,6 +1340,9 @@ il team di ONLYOFFICE</value>
<data name="subject_saas_admin_trial_warning_after5_v10" xml:space="preserve">
<value>Estendi il periodo di prova ONLYOFFICE e ottieni sconti</value>
</data>
<data name="subject_saas_admin_trial_warning_before5_v10" xml:space="preserve">
<value>Usa questo sconto prima che il periodo di prova finisca</value>
</data>
<data name="subject_saas_admin_user_apps_tips_v10" xml:space="preserve">
<value>Ottieni apps di ONLYOFFICE gratis</value>
</data>
@ -1275,9 +1358,15 @@ il team di ONLYOFFICE</value>
<data name="subject_saas_admin_without_activity_v10" xml:space="preserve">
<value>Attenzionato dal team ONLYOFFICE </value>
</data>
<data name="subject_saas_guest_activation_v10" xml:space="preserve">
<value>Unirsi su ${__VirtualRootPath}</value>
</data>
<data name="subject_saas_guest_welcome_v10" xml:space="preserve">
<value>Benvenuto nel tuo ONLYOFFICE!</value>
</data>
<data name="subject_saas_user_activation_v10" xml:space="preserve">
<value>Unirsi su ${__VirtualRootPath}</value>
</data>
<data name="subject_saas_user_welcome_v10" xml:space="preserve">
<value>Benvenuto nel tuo ONLYOFFICE!</value>
</data>

View File

@ -1182,6 +1182,9 @@ h3.Email
h3.Request content
$Body</value>
</data>
<data name="pattern_restore_completed" xml:space="preserve">
<value />
</data>
<data name="pattern_restore_started" xml:space="preserve">
<value>h1.Portal restoration started
@ -1634,6 +1637,9 @@ The link is only valid for 7 days.</value>
<data name="subject_change_tfa" xml:space="preserve">
<value>${LetterLogoText}. Backup codes change request</value>
</data>
<data name="subject_confirm_owner_change" xml:space="preserve">
<value>${LetterLogoText}. Change of portal owner</value>
</data>
<data name="subject_dns_change" xml:space="preserve">
<value>${LetterLogoText}. Change of portal address</value>
</data>

View File

@ -1624,6 +1624,9 @@ $GreenButton
<data name="subject_change_tfa" xml:space="preserve">
<value>${LetterLogoText}. Запрос на изменение резервных кодов</value>
</data>
<data name="subject_confirm_owner_change" xml:space="preserve">
<value>${LetterLogoText}. Смена владельца портала</value>
</data>
<data name="subject_dns_change" xml:space="preserve">
<value>${LetterLogoText}. Изменение адреса портала</value>
</data>

View File

@ -1,5 +1,5 @@
<patterns>
<formatter type="ASC.Notify.Patterns.NVelocityPatternFormatter, ASC.Common" />
<formatter type="ASC.Notify.Patterns.NVelocityPatternFormatter, ASC.Core.Common" />
<!-- self_profile_updated -->
<pattern id="self_profile_updated" sender="email.sender">
@ -161,7 +161,7 @@ $activity.Key
<!-- owner_confirm_change -->
<pattern id="owner_confirm_change">
<subject resource="subject_confirm_owner_change|ASC.Web.Core.PublicResources.WebstudioNotifyPatternResource,ASC.Web.Core" />
<subject resource="|subject_confirm_owner_change|ASC.Web.Core.PublicResources.WebstudioNotifyPatternResource,ASC.Web.Core" />
<body styler="ASC.Notify.Textile.TextileStyler,ASC.Notify.Textile" resource="|pattern_confirm_owner_change|ASC.Web.Core.PublicResources.WebstudioNotifyPatternResource,ASC.Web.Core" />
</pattern>