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

This commit is contained in:
Alexey Safronov 2019-07-17 16:40:48 +03:00
commit ae17daef74
14 changed files with 553 additions and 1 deletions

View File

@ -1,10 +1,13 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Drawing;
using System.Drawing.Imaging;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Net; using System.Net;
using System.Net.Mail; using System.Net.Mail;
using System.Security; using System.Security;
using System.Threading.Tasks;
using ASC.Api.Core; using ASC.Api.Core;
using ASC.Common.Web; using ASC.Common.Web;
using ASC.Core; using ASC.Core;
@ -21,6 +24,7 @@ using ASC.Web.Api.Routing;
using ASC.Web.Core; using ASC.Web.Core;
using ASC.Web.Core.PublicResources; using ASC.Web.Core.PublicResources;
using ASC.Web.Core.Users; using ASC.Web.Core.Users;
using ASC.Web.Studio.Core;
using ASC.Web.Studio.Core.Notify; using ASC.Web.Studio.Core.Notify;
using ASC.Web.Studio.UserControls.Statistics; using ASC.Web.Studio.UserControls.Statistics;
using ASC.Web.Studio.Utility; using ASC.Web.Studio.Utility;
@ -600,6 +604,100 @@ namespace ASC.Employee.Core.Controllers
return new ThumbnailsDataWrapper(user.ID); return new ThumbnailsDataWrapper(user.ID);
} }
[Create("{userid}/photo")]
public FileUploadResult UploadMemberPhoto(string userid, UploadPhotoModel model)
{
var result = new FileUploadResult();
try
{
if (model.Files.Count != 0)
{
Guid userId;
try
{
userId = new Guid(userid);
}
catch
{
userId = SecurityContext.CurrentAccount.ID;
}
SecurityContext.DemandPermissions(new UserSecurityProvider(userId), Constants.Action_EditUser);
var userPhoto = model.Files[0];
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();
CheckImgFormat(data);
if (model.Autosave)
{
if (data.Length > SetupInfo.MaxImageUploadSize)
throw new ImageSizeLimitException();
var mainPhoto = UserPhotoManager.SaveOrUpdatePhoto(userId, data);
result.Data =
new
{
main = mainPhoto,
retina = UserPhotoManager.GetRetinaPhotoURL(userId),
max = UserPhotoManager.GetMaxPhotoURL(userId),
big = UserPhotoManager.GetBigPhotoURL(userId),
medium = UserPhotoManager.GetMediumPhotoURL(userId),
small = UserPhotoManager.GetSmallPhotoURL(userId),
};
}
else
{
result.Data = UserPhotoManager.SaveTempPhoto(data, SetupInfo.MaxImageUploadSize, UserPhotoManager.OriginalFotoSize.Width, UserPhotoManager.OriginalFotoSize.Height);
}
result.Success = true;
}
else
{
result.Success = false;
result.Message = PeopleResource.ErrorEmptyUploadFileSelected;
}
}
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;
}
[Update("{userid}/photo")] [Update("{userid}/photo")]
public ThumbnailsDataWrapper UpdateMemberPhoto(string userid, UpdateMemberModel model) public ThumbnailsDataWrapper UpdateMemberPhoto(string userid, UpdateMemberModel model)
{ {
@ -1131,5 +1229,32 @@ namespace ASC.Employee.Core.Controllers
var imageByteArray = br.ReadBytes((int)response.ContentLength); var imageByteArray = br.ReadBytes((int)response.ContentLength);
UserPhotoManager.SaveOrUpdatePhoto(user.ID, imageByteArray); UserPhotoManager.SaveOrUpdatePhoto(user.ID, imageByteArray);
} }
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);
}
if (!imgFormat.Equals(ImageFormat.Png) && !imgFormat.Equals(ImageFormat.Jpeg))
{
throw new UnknownImageFormatException();
}
}
} }
} }

View File

@ -0,0 +1,18 @@
using System.Collections.Generic;
using Microsoft.AspNetCore.Http;
namespace ASC.People.Models
{
public class UploadPhotoModel
{
public List<IFormFile> Files { get; set; }
public bool Autosave { get; set; }
}
public class FileUploadResult
{
public bool Success { get; set; }
public object Data { get; set; }
public string Message { get; set; }
}
}

View File

@ -8999,6 +8999,17 @@
"dom-testing-library": "^4.0.0" "dom-testing-library": "^4.0.0"
} }
}, },
"react-toastify": {
"version": "5.3.1",
"resolved": "https://registry.npmjs.org/react-toastify/-/react-toastify-5.3.1.tgz",
"integrity": "sha512-R6WVGHQ/kMpqq4Uwsybuo95hxNfaWEN713++ob3QC9674TdBrXEBa0B+5mutLT/O7ShOvHAwfVOzuQFPGAFm5Q==",
"requires": {
"@babel/runtime": "^7.4.2",
"classnames": "^2.2.6",
"prop-types": "^15.7.2",
"react-transition-group": "^2.6.1"
}
},
"react-transition-group": { "react-transition-group": {
"version": "2.9.0", "version": "2.9.0",
"resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-2.9.0.tgz", "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-2.9.0.tgz",

View File

@ -35,6 +35,7 @@
"react-custom-scrollbars": "^4.2.1", "react-custom-scrollbars": "^4.2.1",
"react-datepicker": "^2.7.0", "react-datepicker": "^2.7.0",
"react-lifecycles-compat": "^3.0.4", "react-lifecycles-compat": "^3.0.4",
"react-toastify": "^5.3.1",
"reactstrap": "^8.0.0", "reactstrap": "^8.0.0",
"styled-components": "^4.3.2" "styled-components": "^4.3.2"
}, },

View File

@ -0,0 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6.97 12.91L1 6.94L2.94 5L6.97 9.03L13 3L14.94 4.94L6.97 12.91Z" fill="black"/>
</svg>

After

Width:  |  Height:  |  Size: 192 B

View File

@ -0,0 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M14.8768 12.6886L8.73457 2.41828C8.57934 2.15872 8.30062 2 7.99998 2C7.69934 2 7.42062 2.15869 7.26539 2.41828L1.12321 12.6886C0.963467 12.9557 0.958737 13.2889 1.11088 13.5604C1.26302 13.832 1.54841 14 1.85777 14H14.1422C14.4516 14 14.737 13.832 14.8891 13.5604C15.0413 13.2888 15.0365 12.9557 14.8768 12.6886ZM8.00457 5.55261C8.35734 5.55261 8.65582 5.75326 8.65582 6.1089C8.65582 7.1941 8.52919 8.75357 8.52919 9.83878C8.52919 10.1215 8.22166 10.24 8.00457 10.24C7.71517 10.24 7.47093 10.1215 7.47093 9.83878C7.47093 8.75357 7.34433 7.1941 7.34433 6.1089C7.34433 5.75326 7.63374 5.55261 8.00457 5.55261ZM8.01362 12.2737C7.61566 12.2737 7.31715 11.9454 7.31715 11.5715C7.31715 11.1885 7.61563 10.8693 8.01362 10.8693C8.38446 10.8693 8.70105 11.1885 8.70105 11.5715C8.70105 11.9454 8.38446 12.2737 8.01362 12.2737Z" fill="black"/>
</svg>

After

Width:  |  Height:  |  Size: 944 B

View File

@ -135,6 +135,9 @@ import OrigMenuIcon from './menu.react.svg';
import OrigNavLogoIcon from './nav.logo.react.svg'; import OrigNavLogoIcon from './nav.logo.react.svg';
import OrigNavLogoOpenedIcon from './nav.logo.opened.react.svg'; import OrigNavLogoOpenedIcon from './nav.logo.opened.react.svg';
import OrigCheckIcon from './check.react.svg';
import OrigDangerIcon from './danger.react.svg';
import OrigInfoIcon from './info.react.svg';
export const AZSortingIcon = createStyledIcon( export const AZSortingIcon = createStyledIcon(
OrigAZSortingIcon, OrigAZSortingIcon,
@ -300,6 +303,10 @@ export const ChatIcon = createStyledIcon(
OrigChatIcon, OrigChatIcon,
'ChatIcon' 'ChatIcon'
); );
export const CheckIcon = createStyledIcon(
OrigCheckIcon,
'CheckIcon'
);
export const CheckboxIcon = createStyledIcon( export const CheckboxIcon = createStyledIcon(
OrigCheckboxIcon, OrigCheckboxIcon,
'CheckboxIcon' 'CheckboxIcon'
@ -380,6 +387,10 @@ export const CrossIcon = createStyledIcon(
OrigCrossIcon, OrigCrossIcon,
'CrossIcon' 'CrossIcon'
); );
export const DangerIcon = createStyledIcon(
OrigDangerIcon,
'DangerIcon'
);
export const DocumentsIcon = createStyledIcon( export const DocumentsIcon = createStyledIcon(
OrigDocumentsIcon, OrigDocumentsIcon,
'DocumentsIcon' 'DocumentsIcon'
@ -452,6 +463,10 @@ export const ImportIcon = createStyledIcon(
OrigImportIcon, OrigImportIcon,
'ImportIcon' 'ImportIcon'
); );
export const InfoIcon = createStyledIcon(
OrigInfoIcon,
'InfoIcon'
);
export const InvitationLinkIcon = createStyledIcon( export const InvitationLinkIcon = createStyledIcon(
OrigInvitationLinkIcon, OrigInvitationLinkIcon,
'InvitationLinkIcon' 'InvitationLinkIcon'

View File

@ -0,0 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M8 15C11.866 15 15 11.866 15 8C15 4.13401 11.866 1 8 1C4.13401 1 1 4.13401 1 8C1 11.866 4.13401 15 8 15ZM7 6V4H9V6H7ZM7 12V7H9V12H7Z" fill="black"/>
</svg>

After

Width:  |  Height:  |  Size: 301 B

View File

@ -0,0 +1,154 @@
import React from 'react'
import { ToastContainer, cssTransition } from 'react-toastify'
import 'react-toastify/dist/ReactToastify.css'
import styled from 'styled-components'
import PropTypes from 'prop-types'
const Fade = cssTransition({
enter: 'fadeIn',
exit: 'fadeOut'
});
const StyledToastContainer = styled(ToastContainer)`
width: 365px !important;
.Toastify__toast--success{
background-color: #cae796;
&:hover {
background-color: #bcdf7e;
}
}
.Toastify__toast--error{
background-color: #ffbfaa;
&:hover {
background-color: #ffa98d;
}
}
.Toastify__toast--info{
background-color: #f1da92;
&:hover {
background-color: #eed27b;
}
}
.Toastify__toast--warning{
background-color: #f1ca92;
&:hover {
background-color: #eeb97b;
}
}
@-webkit-keyframes fadeout {
0% {
opacity: 1;
}
100% {
opacity: 0;
}
}
@keyframes fadeout {
0% {
opacity: 1;
}
100% {
opacity: 0;
}
}
.fadeOut {
opacity: 0;
-moz-animation: fadeout 1s linear;
-webkit-animation: fadeout 1s linear;
animation: fadeout 1s linear;
}
@-webkit-keyframes fadein {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
}
@keyframes fadein {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
}
.fadeIn {
opacity: 1;
-moz-animation: fadein 0.3s linear;
-webkit-animation: fadein 0.3s linear;
animation: fadein 0.3s linear;
}
/* .Toastily__toast or & > div (less productive) */
.Toastify__toast
{
border-radius: 3px;
-moz-border-radius: 3px;
-webkit-border-radius: 3px;
color: #000;
margin: 0 0 6px;
padding: 13px 11px 13px 11px;
min-height: 32px;
font: normal 12px 'Open Sans', sans-serif;
width: 100%;
}
/* .Toastify__toast-body or & > div > div (less productive) */
.Toastify__toast-body {
display: flex;
align-items: center;
}
svg {
width: 20px;
min-width: 20px;
height: 20px;
min-height: 20px;
}
`;
const Toast = props => {
return (
<StyledToastContainer
draggable={false}
hideProgressBar={true}
newestOnTop={true}
pauseOnFocusLoss={false}
transition={Fade}
/>
);
};
Toast.propTypes = {
autoClosed: PropTypes.bool,
text: PropTypes.string,
title: PropTypes.string,
type: PropTypes.oneOf(['success', 'error', 'warning', 'info']),
};
Toast.defaultProps = {
text: 'Demo text for example',
title: 'Demo title',
autoClosed: true,
type: 'success',
}
export default Toast;

View File

@ -0,0 +1,73 @@
import React from 'react'
import { toast } from 'react-toastify'
import styled from 'styled-components'
import { Icons } from '../icons'
const Icon = ({ type }) => (
type === "success"
? <Icons.CheckIcon color="#ffffff" isfill={true} />
: type === "error" || type === "warning"
? <Icons.DangerIcon color="#ffffff" isfill={true} />
: <Icons.InfoIcon color="#ffffff" isfill={true} />
);
const StyledDiv = styled.div`
margin-left: 15px;
`;
const ToastTitle = styled.p`
font-weight: bold;
margin: 0;
`;
const toastr = {
clear: clear,
error: error,
info: info,
success: success,
warning: warning
};
const notify = (type, text, title, autoClosed = true) => {
return toast(
<>
<div>
<Icon type={type} />
</div>
<StyledDiv>
<ToastTitle>{title}</ToastTitle>
{text}
</StyledDiv>
</>,
{
type: type,
closeOnClick: autoClosed,
closeButton: !autoClosed,
autoClose: autoClosed
}
);
};
function success(text, title, autoClosed) {
return notify('success', text, title, autoClosed);
}
function error(text, title, autoClosed) {
return notify('error', text, title, autoClosed);
}
function warning(text, title, autoClosed) {
return notify('warning', text, title, autoClosed);
}
function info(text, title, autoClosed) {
return notify('info', text, title, autoClosed);
}
function clear() {
return toast.dismiss();
}
export default toastr;

View File

@ -27,4 +27,6 @@ export { default as InputBlock } from './components/input-block'
export { default as IconButton } from './components/icon-button' export { default as IconButton } from './components/icon-button'
export { default as SearchInput } from './components/search-input' export { default as SearchInput } from './components/search-input'
export { default as Backdrop } from './components/backdrop' export { default as Backdrop } from './components/backdrop'
export { default as Scrollbar } from './components/scrollbar' export { default as Scrollbar } from './components/scrollbar'
export { default as Toast } from './components/toast'
export { default as toastr } from './components/toast/toastr'

View File

@ -0,0 +1,41 @@
import React from 'react';
import { storiesOf } from '@storybook/react';
import { Toast, toastr } from 'asc-web-components';
import Section from '../../../.storybook/decorators/section';
storiesOf('Components|Toast', module)
.addParameters({ viewport: { defaultViewport: 'responsive' } })
.addParameters({ options: { showAddonPanel: false } })
.add('all', () => {
return (
<>
<Toast/>
<Section>
<button onClick = {()=> {
toastr.success('Demo text for success Toast');
toastr.error('Demo text for error Toast');
toastr.warning('Demo text for warning Toast');
toastr.info('Demo text for info Toast');
toastr.success('Demo text for success Toast with title', 'Demo title');
toastr.error('Demo text for error Toast with title', 'Demo title');
toastr.warning('Demo text for warning Toast with title', 'Demo title');
toastr.info('Demo text for info Toast with title', 'Demo title');
toastr.success('Demo text for success manual closed Toast', null, false);
toastr.error('Demo text for error manual closed Toast', null, false);
toastr.warning('Demo text for warning manual closed Toast', null, false);
toastr.info('Demo text for info manual closed Toast', null, false);
toastr.success('Demo text for success manual closed Toast with title', 'Demo title', false);
toastr.error('Demo text for error manual closed Toast with title', 'Demo title', false);
toastr.warning('Demo text for warning manual closed Toast with title', 'Demo title', false);
toastr.info('Demo text for info manual closed Toast with title', 'Demo title', false);
}}>Show all Toastr</button>
</Section>
</>
);
});

View File

@ -0,0 +1,59 @@
# Toast
## Usage
```js
import { Toast, toastr } from 'asc-web-components';
```
#### Description
Toast allow you to add notification to your page with ease.
`<Toast />` is container for your notification. Remember to render the `<Toast />` *once* in your application tree. If you can't figure out where to put it, rendering it in the application root would be the best bet.
`toastr` is a function for showing notifications.
#### Usage
```js
<Toast />
<button onClick={() => toastr.success('Some text for toast', 'Some text for title', true)}>Click</button>
```
or
```js
<Toast>
{toastr.success('Some text for toast')}
</Toast>
```
#### Properties
| Props | Type | Required | Values | Default | Description |
| ------------------ | -------- | :------: | --------------------------- | -------------- | ----------------------------------------------------------------- |
| `type` | `oneOf` | ✅ | success, error, warning, info | - | Define color and icon of toast |
| `text` | `string` | - | - | - | Text inside a toast |
| `title` | `string` | - | - | - | Title inside a toast |
| `autoClosed` | `bool` | - | true, false | `true`|If `true`: toast disappeared after 5 seconds or after clicking on any area of toast. If `false`: included close button, toast can be closed by clicking on it.|
#### Other Options
```js
<Toast/>
// Remove all toasts in your page programmatically
<button onClick = {()=> { toastr.clear() }}>Clear</button>
```
#### Examples
```js
<Toast>
// Display a warning toast, with no title
{toastr.warning('My name is Dan. I like my cat')}
// Display a success toast, with a title
{toastr.success('Have fun storming the castle!', 'Miracle Max Says')}
// Display a error toast, with title and you should close it manually
{toastr.error('I do not think that word means what you think it means.', 'Inconceivable!', false)}
</Toast>
```

View File

@ -0,0 +1,44 @@
import React from 'react';
import { storiesOf } from '@storybook/react';
import { Toast, toastr } from 'asc-web-components';
import Readme from './README.md';
import { text, boolean, withKnobs, select } from '@storybook/addon-knobs/react';
import withReadme from 'storybook-readme/with-readme';
import Section from '../../../.storybook/decorators/section';
const type = ['success', 'error', 'warning', 'info'];
storiesOf('Components|Toast', module)
.addDecorator(withKnobs)
.addDecorator(withReadme(Readme))
.add('base', () => {
const toastType = `${select('type', type, 'success')}`;
const toastText = `${text('text', 'Demo text for Toast')}`;
const titleToast = `${text('title', 'Demo title')}`;
const autoClosed = `${boolean('autoClosed', true)}`;
return (
<>
<Toast />
<Section>
<button onClick={() => {
switch (toastType) {
case 'error':
toastr.error(toastText, titleToast, JSON.parse(autoClosed));
break;
case 'warning':
toastr.warning(toastText, titleToast, JSON.parse(autoClosed));
break;
case 'info':
toastr.info(toastText, titleToast, JSON.parse(autoClosed));
break;
default:
toastr.success(toastText, titleToast, JSON.parse(autoClosed));
break;
}
}}>
Show toast</button>
</Section>
</>
);
});