Merge branch 'feature/files' into feature/media-viewer

This commit is contained in:
NikolayRechkin 2020-05-05 18:53:33 +03:00
commit 3afb210225
92 changed files with 2558 additions and 1118 deletions

View File

@ -0,0 +1,51 @@
/*
*
* (c) Copyright Ascensio System Limited 2010-2020
*
* This program is freeware. You can redistribute it and/or modify it under the terms of the GNU
* General Public License (GPL) version 3 as published by the Free Software Foundation (https://www.gnu.org/copyleft/gpl.html).
* In accordance with Section 7(a) of the GNU GPL its Section 15 shall be amended to the effect that
* Ascensio System SIA expressly excludes the warranty of non-infringement of any third-party rights.
*
* THIS PROGRAM IS DISTRIBUTED WITHOUT ANY WARRANTY; WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR
* FITNESS FOR A PARTICULAR PURPOSE. For more details, see GNU GPL at https://www.gnu.org/copyleft/gpl.html
*
* You can contact Ascensio System SIA by email at sales@onlyoffice.com
*
* The interactive user interfaces in modified source and object code versions of ONLYOFFICE must display
* Appropriate Legal Notices, as required under Section 5 of the GNU GPL version 3.
*
* Pursuant to Section 7 § 3(b) of the GNU GPL you must retain the original ONLYOFFICE logo which contains
* relevant author attributions when distributing the software. If the display of the logo in its graphic
* form is not reasonably feasible for technical reasons, you must include the words "Powered by ONLYOFFICE"
* in every copy of the program you distribute.
* Pursuant to Section 7 § 3(e) we decline to grant you any rights under trademark law for use of our trademarks.
*
*/
namespace ASC.Api.Utils
{
public static class Update
{
public static T IfNotEquals<T>(T current, T @new)
{
if (!Equals(current, @new))
{
return @new;
}
return current;
}
public static T IfNotEmptyAndNotEquals<T>(T current, T @new)
{
if (Equals(@new, default(T))) return current;
if (!Equals(current, @new))
{
return @new;
}
return current;
}
}
}

View File

@ -38,6 +38,7 @@
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="JWT" Version="6.1.4" />
<PackageReference Include="log4net" Version="2.0.8" />
<PackageReference Include="Microsoft.AspNetCore.Http" Version="2.2.2" />
<PackageReference Include="Microsoft.AspNetCore.Http.Extensions" Version="2.2.0" />

View File

@ -27,11 +27,12 @@
using System;
using System.IO;
using System.Threading.Tasks;
using ASC.Data.Storage;
public static class StreamExtension
{
private const int BufferSize = 2048; //NOTE: set to 2048 to fit in minimum tcp window
public const int BufferSize = 2048; //NOTE: set to 2048 to fit in minimum tcp window
public static Stream GetBuffered(this Stream srcStream)
{

View File

@ -0,0 +1,104 @@
/*
*
* (c) Copyright Ascensio System Limited 2010-2018
*
* This program is freeware. You can redistribute it and/or modify it under the terms of the GNU
* General Public License (GPL) version 3 as published by the Free Software Foundation (https://www.gnu.org/copyleft/gpl.html).
* In accordance with Section 7(a) of the GNU GPL its Section 15 shall be amended to the effect that
* Ascensio System SIA expressly excludes the warranty of non-infringement of any third-party rights.
*
* THIS PROGRAM IS DISTRIBUTED WITHOUT ANY WARRANTY; WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR
* FITNESS FOR A PARTICULAR PURPOSE. For more details, see GNU GPL at https://www.gnu.org/copyleft/gpl.html
*
* You can contact Ascensio System SIA by email at sales@onlyoffice.com
*
* The interactive user interfaces in modified source and object code versions of ONLYOFFICE must display
* Appropriate Legal Notices, as required under Section 5 of the GNU GPL version 3.
*
* Pursuant to Section 7 § 3(b) of the GNU GPL you must retain the original ONLYOFFICE logo which contains
* relevant author attributions when distributing the software. If the display of the logo in its graphic
* form is not reasonably feasible for technical reasons, you must include the words "Powered by ONLYOFFICE"
* in every copy of the program you distribute.
* Pursuant to Section 7 § 3(e) we decline to grant you any rights under trademark law for use of our trademarks.
*
*/
using System;
using JWT;
using JWT.Algorithms;
using JWT.Serializers;
using Newtonsoft.Json;
using Newtonsoft.Json.Serialization;
using Formatting = Newtonsoft.Json.Formatting;
namespace ASC.Web.Core.Files
{
public class JsonWebToken
{
public static string Encode(object payload, string key)
{
var (serializer, algorithm, urlEncoder) = GetSettings();
var encoder = new JwtEncoder(algorithm, serializer, urlEncoder);
return encoder.Encode(payload, key);
}
public static string Decode(string token, string key, bool verify = true, bool baseSerializer = false)
{
var (serializer, algorithm, urlEncoder) = GetSettings(baseSerializer);
var provider = new UtcDateTimeProvider();
IJwtValidator validator = new JwtValidator(serializer, provider);
var decoder = new JwtDecoder(serializer, validator, urlEncoder, algorithm);
return decoder.Decode(token, key, verify);
}
private static (IJsonSerializer, IJwtAlgorithm, IBase64UrlEncoder) GetSettings(bool baseSerializer = false)
{
return (baseSerializer ? (IJsonSerializer)new JsonNetSerializer() : new JwtSerializer(), new HMACSHA256Algorithm(), new JwtBase64UrlEncoder());
}
}
public class JwtSerializer : IJsonSerializer
{
private class CamelCaseExceptDictionaryKeysResolver : CamelCasePropertyNamesContractResolver
{
protected override JsonDictionaryContract CreateDictionaryContract(Type objectType)
{
var contract = base.CreateDictionaryContract(objectType);
contract.DictionaryKeyResolver = propertyName => propertyName;
return contract;
}
}
public string Serialize(object obj)
{
var settings = new JsonSerializerSettings
{
ContractResolver = new CamelCaseExceptDictionaryKeysResolver(),
NullValueHandling = NullValueHandling.Ignore,
};
return JsonConvert.SerializeObject(obj, Formatting.Indented, settings);
}
public T Deserialize<T>(string json)
{
var settings = new JsonSerializerSettings
{
ContractResolver = new CamelCaseExceptDictionaryKeysResolver(),
NullValueHandling = NullValueHandling.Ignore,
};
return JsonConvert.DeserializeObject<T>(json, settings);
}
}
}

View File

@ -370,19 +370,13 @@ namespace ASC.Core.Common.Configuration
}
}
public class ConsumerFactory
public class ConsumerFactory : IDisposable
{
public ILifetimeScope Builder { get; set; }
public ConsumerFactory(IContainer builder)
{
Builder = builder;
}
public ConsumerFactory(ILifetimeScope builder)
{
Builder = builder;
Builder = builder.BeginLifetimeScope();
}
public Consumer GetByKey(string key)
@ -418,7 +412,12 @@ namespace ASC.Core.Common.Configuration
public IEnumerable<T> GetAll<T>() where T : Consumer, new()
{
return Builder.Resolve<IEnumerable<T>>();
}
}
public void Dispose()
{
Builder.Dispose();
}
}
public static class ConsumerFactoryExtension

View File

@ -19,7 +19,6 @@
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="JWT" Version="1.3.4" />
<PackageReference Include="Microsoft.AspNetCore.Hosting.Abstractions" Version="2.2.0" />
</ItemGroup>

View File

@ -72,7 +72,7 @@ namespace ASC.FederatedLogin.LoginProviders
{
public static DIHelper AddBoxLoginProviderService(this DIHelper services)
{
services.TryAddScoped<BoxLoginProvider>();
//services.TryAddScoped<BoxLoginProvider>();
return services
.AddConsumerFactoryService()
.AddKafkaService()

View File

@ -132,7 +132,7 @@ namespace ASC.FederatedLogin.LoginProviders
{
public static DIHelper AddDocuSignLoginProviderService(this DIHelper services)
{
services.TryAddScoped<DocuSignLoginProvider>();
//services.TryAddScoped<DocuSignLoginProvider>();
return services
.AddConsumerFactoryService()
.AddKafkaService()

View File

@ -185,7 +185,7 @@ namespace ASC.FederatedLogin.LoginProviders
{
public static DIHelper AddGoogleLoginProviderService(this DIHelper services)
{
services.TryAddScoped<GoogleLoginProvider>();
//services.TryAddScoped<GoogleLoginProvider>();
return services
.AddConsumerFactoryService()
.AddKafkaService()

View File

@ -40,8 +40,7 @@ using ASC.Core.Common.Configuration;
using ASC.FederatedLogin.Helpers;
using ASC.FederatedLogin.Profile;
using ASC.Security.Cryptography;
using JWT;
using ASC.Web.Core.Files;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.WebUtilities;
@ -212,7 +211,7 @@ namespace ASC.FederatedLogin.LoginProviders
public override LoginProfile GetLoginProfile(string accessToken)
{
var tokenPayloadString = JsonWebToken.Decode(accessToken, string.Empty, false);
var tokenPayloadString = JsonWebToken.Decode(accessToken, string.Empty, false, true);
var tokenPayload = JObject.Parse(tokenPayloadString);
if (tokenPayload == null)
{

View File

@ -75,7 +75,7 @@ namespace ASC.FederatedLogin.LoginProviders
{
public static DIHelper AddOneDriveLoginProviderService(this DIHelper services)
{
services.TryAddScoped<OneDriveLoginProvider>();
//services.TryAddScoped<OneDriveLoginProvider>();
return services
.AddConsumerFactoryService()
.AddKafkaService()

View File

@ -26,6 +26,7 @@
using System;
using ASC.Core;
using ASC.Core.Common.EF;
using ASC.Core.Common.EF.Context;
@ -37,10 +38,10 @@ namespace ASC.VoipService.Dao
protected VoipDbContext VoipDbContext { get; set; }
protected AbstractDao(DbContextManager<VoipDbContext> dbOptions, int tenantID)
protected AbstractDao(DbContextManager<VoipDbContext> dbOptions, TenantManager tenantManager)
{
VoipDbContext = dbOptions.Get(dbid);
TenantID = tenantID;
TenantID = tenantManager.GetCurrentTenant().TenantId;
}
protected int TenantID

View File

@ -29,6 +29,7 @@ using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using ASC.Common;
using ASC.Common.Caching;
using ASC.Core;
using ASC.Core.Common;
@ -65,7 +66,7 @@ namespace ASC.VoipService.Dao
public VoipDaoCache VoipDaoCache { get; }
public CachedVoipDao(
int tenantID,
TenantManager tenantManager,
DbContextManager<VoipDbContext> dbOptions,
AuthContext authContext,
TenantUtil tenantUtil,
@ -73,7 +74,7 @@ namespace ASC.VoipService.Dao
BaseCommonLinkUtility baseCommonLinkUtility,
ConsumerFactory consumerFactory,
VoipDaoCache voipDaoCache)
: base(tenantID, dbOptions, authContext, tenantUtil, securityContext, baseCommonLinkUtility, consumerFactory)
: base(tenantManager, dbOptions, authContext, tenantUtil, securityContext, baseCommonLinkUtility, consumerFactory)
{
cache = voipDaoCache.Cache;
VoipDaoCache = voipDaoCache;
@ -108,5 +109,22 @@ namespace ASC.VoipService.Dao
{
return "voip" + tenant.ToString(CultureInfo.InvariantCulture);
}
}
public static class VoipDaoExtention
{
public static DIHelper AddVoipDaoService(this DIHelper services)
{
services.TryAddScoped<VoipDao, CachedVoipDao>();
services.TryAddSingleton<VoipDaoCache>();
return services
.AddDbContextManagerService<VoipDbContext>()
.AddAuthContextService()
.AddTenantUtilService()
.AddSecurityContextService()
.AddBaseCommonLinkUtilityService()
.AddConsumerFactoryService();
}
}
}

View File

@ -42,14 +42,14 @@ namespace ASC.VoipService.Dao
public class VoipDao : AbstractDao
{
public VoipDao(
int tenantID,
TenantManager tenantManager,
DbContextManager<VoipDbContext> dbOptions,
AuthContext authContext,
TenantUtil tenantUtil,
SecurityContext securityContext,
BaseCommonLinkUtility baseCommonLinkUtility,
ConsumerFactory consumerFactory)
: base(dbOptions, tenantID)
: base(dbOptions, tenantManager)
{
AuthContext = authContext;
TenantUtil = tenantUtil;

View File

@ -43,6 +43,45 @@
"sign": ""
}
},
"files": {
"thirdparty": {
"enable" : ["box", "dropboxv2", "docusign", "google", "onedrive", "sharepoint", "nextcloud", "owncloud", "webdav", "kdrive", "yandex"]
},
"docservice": {
"coauthor-docs" : [ ".pptx", ".ppsx", ".xlsx", ".csv", ".docx", ".txt" ],
"commented-docs" : [ ".docx", ".xlsx", ".pptx" ],
"convert-docs" : [ ".pptm",".ppt",".ppsm",".pps",".potx",".potm",".pot",".odp",".fodp",".otp",".xlsm",".xls",".xltx",".xltm",".xlt",".ods",".fods",".ots",".docm",".doc",".dotx",".dotm",".dot",".odt",".fodt",".ott",".rtf" ],
"edited-docs" : [ ".pptx",".pptm",".ppt",".ppsx",".ppsm",".pps",".potx",".potm",".pot",".odp",".fodp",".otp",".xlsx",".xlsm",".xls",".xltx",".xltm",".xlt",".ods",".fods",".ots",".csv",".docx",".docm",".doc",".dotx",".dotm",".dot",".odt",".fodt",".ott",".txt",".rtf",".mht",".html",".htm" ],
"encrypted-docs" : [ ".docx",".xlsx",".pptx" ],
"formfilling-docs" : [ ".docx" ],
"reviewed-docs" : [ ".docx" ],
"viewed-docs" : [ ".pptx",".pptm",".ppt",".ppsx",".ppsm",".pps",".potx",".potm",".pot",".odp",".fodp",".otp",".gslides",".xlsx",".xlsm",".xls",".xltx",".xltm",".xlt",".ods",".fods",".ots",".gsheet",".csv",".docx",".docm",".doc",".dotx",".dotm",".dot",".odt",".fodt",".ott",".gdoc",".txt",".rtf",".mht",".html",".htm",".epub",".pdf",".djvu",".xps" ],
"secret" :
{
"value": "",
"header": "",
},
"url":
{
"public": "http://192.168.3.142/",
"internal": "",
"portal": ""
}
},
"ffmpeg" :
{
"value": "",
"exts": [ "avi", "mpeg", "mpg", "wmv" ]
},
"uploader":
{
"chunk-size": 10485760,
"url": "products/files/"
},
"viewed-images": [".bmp",".gif",".jpeg",".jpg",".png",".ico",".tif",".tiff",".webp"],
"viewed-media": [".aac",".flac",".m4a",".mp3",".oga",".ogg",".wav",".f4v",".m4v",".mov",".mp4",".ogv",".webm"],
"index": [".pptx",".pptm",".ppt",".ppsx",".ppsm",".pps",".potx",".potm",".pot",".odp",".fodp",".otp",".gslides",".xlsx",".xlsm",".xls",".xltx",".xltm",".xlt",".ods",".fods",".ots",".gsheet",".csv",".docx",".docm",".doc",".dotx",".dotm",".dot",".odt",".fodt",".ott",".gdoc",".txt",".rtf",".mht",".html",".htm",".epub",".pdf",".djvu",".xps"],
},
"web": {
"api": "api/2.0",
"alias": {

View File

@ -23,5 +23,18 @@
"notify": {
"postman": "services"
}
},
"files": {
"docservice": {
"secret" : {
"value": "SQyTqextlJFq",
"header": "AuthorizationJwt"
},
"url": {
"public" : "https://dotnet.onlyoffice.com:8093",
"internal" : "",
"portal" : "",
}
}
}
}
}

View File

@ -141,6 +141,11 @@ server {
root $public_root;
try_files /$basename /index.html =404;
}
location ~* (/httphandlers/filehandler.ashx|ChunkedUploader.ashx) {
proxy_pass http://localhost:5007;
proxy_set_header X-REWRITER-URL $X_REWRITER_URL;
}
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

View File

@ -0,0 +1,3 @@
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
<path fill="#83888d" d="M10 10v-1.5c0.459-0.185 0.815-0.541 0.996-0.988l0.004-0.012v-3c0-1-1-2.5-2-2.5h-2c-1 0-2 1.5-2 2.5v3c0.185 0.459 0.541 0.815 0.988 0.996l0.012 0.004v1.5s-4 2-4 3v1h12v-1c0-1-4-3-4-3z"></path>
</svg>

After

Width:  |  Height:  |  Size: 321 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

View File

Before

Width:  |  Height:  |  Size: 478 B

After

Width:  |  Height:  |  Size: 478 B

View File

Before

Width:  |  Height:  |  Size: 532 B

After

Width:  |  Height:  |  Size: 532 B

View File

Before

Width:  |  Height:  |  Size: 547 B

After

Width:  |  Height:  |  Size: 547 B

View File

Before

Width:  |  Height:  |  Size: 686 B

After

Width:  |  Height:  |  Size: 686 B

View File

@ -19,6 +19,7 @@
<link rel="apple-touch-icon" href="icon.png">
<link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico">
<link href='https://fonts.googleapis.com/css?family=Open+Sans:300,300i,400,400i,600,600i,700,700i' rel='stylesheet' type='text/css'></link>
<script type="text/javascript" src="https://dotnet.onlyoffice.com/ds-vpath/web-apps/apps/api/documents/api.js?ver=10.0.1.875"></script>
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.

View File

@ -3,6 +3,7 @@ import { connect } from "react-redux";
import { Router, Switch, Redirect } from "react-router-dom";
import { Loader } from "asc-web-components";
import Home from "./components/pages/Home";
import DocEditor from "./components/pages/DocEditor";
import { history, PrivateRoute, PublicRoute, Login, Error404, StudioLayout, Offline } from "asc-web-common";
const App = ({ settings }) => {
@ -17,6 +18,7 @@ const App = ({ settings }) => {
<Switch>
<Redirect exact from="/" to={`${homepage}`} />
<PrivateRoute exact path={[homepage, `${homepage}/filter`]} component={Home} />
<PrivateRoute exact path={`${homepage}/doceditor`} component={DocEditor} />
<PublicRoute exact path={["/login","/login/error=:error", "/login/confirmed-email=:confirmedEmail"]} component={Login} />
<PrivateRoute component={Error404} />
</Switch>

View File

@ -58,10 +58,11 @@ class TreeFolders extends React.Component {
onSelect = data => {
if (this.props.selectedKeys[0] !== data[0]) {
this.props.onLoading(true);
const newFilter = this.props.filter.clone();
fetchFiles(data[0], newFilter, store.dispatch).catch(err =>
toastr.error("Something went wrong", err)
);
).finally(() => this.props.onLoading(false));
}
//this.props.selectFolder(data && data.length === 1 && data[0] !== "root" ? data[0] : null);
@ -171,7 +172,7 @@ class TreeFolders extends React.Component {
};
componentDidUpdate(prevProps) {
const { expandedKeys, data } = this.props;
const { expandedKeys, data, onLoading } = this.props;
if (this.state.expandedKeys.length !== expandedKeys.length) {
this.setState({ expandedKeys });
}

View File

@ -2,37 +2,231 @@ import React from 'react';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import { withRouter } from 'react-router';
import { MainButton, DropDownItem, toastr, utils } from "asc-web-components";
import { withTranslation, I18nextProvider } from "react-i18next";
import {
MainButton,
DropDownItem,
toastr
} from "asc-web-components";
import { withTranslation, I18nextProvider } from 'react-i18next';
import { setAction } from '../../../store/files/actions';
import { isCanCreate } from '../../../store/files/selectors';
import i18n from '../i18n';
import { utils, constants } from 'asc-web-common';
setAction,
fetchFiles,
setTreeFolders,
} from "../../../store/files/actions";
import { isCanCreate, loopTreeFolders } from "../../../store/files/selectors";
import store from "../../../store/store";
import i18n from "../i18n";
import { utils as commonUtils, constants, api } from "asc-web-common";
const { changeLanguage } = utils;
const { changeLanguage } = commonUtils;
const { FileAction } = constants;
class PureArticleMainButtonContent extends React.Component {
state = {
files: [],
uploadedFiles: 0,
totalSize: 0,
percent: 0
};
onCreate = (format) => {
this.props.setAction(
{
type: FileAction.Create,
extension: format,
id: -1
this.props.setAction({
type: FileAction.Create,
extension: format,
id: -1,
});
};
onUploadFileClick = () => this.inputFilesElement.click();
onUploadFolderClick = () => this.inputFolderElement.click();
updateFiles = () => {
const { onLoading, filter, currentFolderId, treeFolders, setTreeFolders } = this.props;
onLoading(true);
const newFilter = filter.clone();
fetchFiles(currentFolderId, newFilter, store.dispatch, treeFolders)
.then((data) => {
const path = data.selectedFolder.pathParts;
const newTreeFolders = treeFolders;
const folders = data.selectedFolder.folders;
const foldersCount = data.selectedFolder.foldersCount;
loopTreeFolders(path, newTreeFolders, folders, foldersCount);
setTreeFolders(newTreeFolders);
})
.catch((err) => toastr.error(err))
.finally(() => {
onLoading(false);
});
};
sendChunk = (files, location, requestsDataArray, isLatestFile, indexOfFile) => {
const sendRequestFunc = (index) => {
let newState = {};
api.files
.uploadFile(location, requestsDataArray[index])
.then((res) => {
let newPercent = this.state.percent;
const percent = (newPercent +=
(files[indexOfFile].size / this.state.totalSize) * 100);
if (res.data.data && res.data.data.uploaded) {
files[indexOfFile].uploaded = true;
newState = { files, percent };
}
if (index + 1 !== requestsDataArray.length) {
sendRequestFunc(index + 1);
} else if (isLatestFile) {
this.updateFiles();
newState = Object.assign({}, newState, {
uploadedFiles: this.state.uploadedFiles + 1,
});
return;
} else {
newState = Object.assign({}, newState, {
uploadedFiles: this.state.uploadedFiles + 1,
});
this.startSessionFunc(indexOfFile + 1);
}
})
.catch((err) => toastr.error(err))
.finally(() => {
if (
newState.hasOwnProperty("files") ||
newState.hasOwnProperty("percent") ||
newState.hasOwnProperty("uploadedFiles")
) {
let progressVisible = true;
let uploadedFiles = newState.uploadedFiles;
let percent = newState.percent;
if (newState.uploadedFiles === files.length) {
percent = 100;
newState.percent = 0;
newState.uploadedFiles = 0;
progressVisible = false;
}
this.setState(newState, () => {
this.props.setProgressValue(percent);
this.props.setProgressLabel(
this.props.t("UploadingLabel", {
file: uploadedFiles,
totalFiles: files.length,
})
);
if (!progressVisible) {
this.props.setProgressVisible(false);
}
});
}
});
};
sendRequestFunc(0);
};
startSessionFunc = indexOfFile => {
const { files } = this.state;
const { currentFolderId } = this.props;
const file = files[indexOfFile];
const isLatestFile = indexOfFile === files.length - 1;
const fileName = file.name;
const fileSize = file.size;
const relativePath = file.webkitRelativePath
? file.webkitRelativePath.slice(0, -file.name.length)
: "";
let location;
const requestsDataArray = [];
const chunkSize = 1024 * 1023; //~0.999mb
const chunks = Math.ceil(file.size / chunkSize, chunkSize);
let chunk = 0;
api.files
.startUploadSession(currentFolderId, fileName, fileSize, relativePath)
.then((res) => {
location = res.data.location;
while (chunk < chunks) {
const offset = chunk * chunkSize;
//console.log("current chunk..", chunk);
//console.log("file blob from offset...", offset);
//console.log(file.slice(offset, offset + chunkSize));
const formData = new FormData();
formData.append("file", file.slice(offset, offset + chunkSize));
requestsDataArray.push(formData);
chunk++;
}
})
.then(() =>
this.sendChunk(
files,
location,
requestsDataArray,
isLatestFile,
indexOfFile
)
);
};
onFileChange = (e) => {
const { t, setProgressVisible, setProgressLabel } = this.props;
const files = e.target.files;
//console.log("files", files);
const newFiles = [];
if(files) {
let total = 0;
for (let item of files) {
if (item.size !== 0) {
newFiles.push(item);
total += item.size;
} else {
toastr.error(t("ErrorUploadMessage"));
}
}
if (newFiles.length > 0) {
this.setState({ files: newFiles, totalSize: total }, () => {
setProgressVisible(true);
setProgressLabel(
this.props.t("UploadingLabel", {
file: 0,
totalFiles: newFiles.length,
})
);
this.startSessionFunc(0);
});
}
}
};
onInputClick = e => {
e.target.value = null;
}
shouldComponentUpdate(nextProps, nextState) {
return nextProps.isCanCreate !== this.props.isCanCreate;
const { files, uploadedFiles, totalSize, percent } = this.state;
if (nextProps.isCanCreate !== this.props.isCanCreate) {
return true;
}
if (!utils.array.isArrayEqual(nextState.files, files)) {
return true;
}
if (nextState.uploadedFiles !== uploadedFiles) {
return true;
}
if (nextState.totalSize !== totalSize) {
return true;
}
if (nextState.percent !== percent) {
return true;
}
return false;
}
render() {
console.log("Files ArticleMainButtonContent render");
//console.log("Files ArticleMainButtonContent render");
const { t, isCanCreate } = this.props;
return (
@ -64,14 +258,39 @@ class PureArticleMainButtonContent extends React.Component {
<DropDownItem isSeparator />
<DropDownItem
icon="ActionsUploadIcon"
label={t('Upload')}
onClick={() => toastr.info("Upload click")}
disabled
label={t("UploadFiles")}
onClick={this.onUploadFileClick}
/>
<DropDownItem
icon="ActionsUploadIcon"
label={t("UploadFolder")}
onClick={this.onUploadFolderClick}
/>
<input
id="customFile"
className="custom-file-input"
multiple
type="file"
onChange={this.onFileChange}
onClick={this.onInputClick}
ref={(input) => (this.inputFilesElement = input)}
style={{ display: "none" }}
/>
<input
id="customFile"
className="custom-file-input"
webkitdirectory=""
mozdirectory=""
type="file"
onChange={this.onFileChange}
onClick={this.onInputClick}
ref={(input) => (this.inputFolderElement = input)}
style={{ display: "none" }}
/>
</MainButton>
);
};
};
}
}
const ArticleMainButtonContentContainer = withTranslation()(PureArticleMainButtonContent);
@ -86,11 +305,18 @@ ArticleMainButtonContent.propTypes = {
};
const mapStateToProps = (state) => {
const { selectedFolder, filter, treeFolders } = state.files;
const { settings, user } = state.auth;
return {
settings: state.auth.settings,
isCanCreate: isCanCreate(state.files.selectedFolder, state.auth.user)
}
}
settings,
isCanCreate: isCanCreate(selectedFolder, user),
currentFolderId: selectedFolder.id,
filter,
treeFolders,
};
};
export default connect(mapStateToProps, { setAction })(withRouter(ArticleMainButtonContent));
export default connect(mapStateToProps, { setAction, setTreeFolders })(
withRouter(ArticleMainButtonContent)
);

View File

@ -4,5 +4,8 @@
"NewSpreadSheet": "New Spreadsheet",
"NewPresentation": "New Presentation",
"NewFolder": "New Folder",
"Upload": "Upload"
"UploadFiles": "Upload files",
"UploadFolder": "Upload folder",
"ErrorUploadMessage": "You cannot upload a folder or an empty file",
"UploadingLabel": "Uploading files: {{file}} of {{totalFiles}}"
}

View File

@ -4,5 +4,8 @@
"NewSpreadsheet": "Новая таблица",
"NewPresentation": "Новая презентация",
"NewFolder": "Новая папка",
"Upload": "Загрузить"
"UploadFiles": "Загрузить файлы",
"UploadFolder": "Загрузить папку",
"ErrorUploadMessage": "Нельзя загрузить папку или пустой файл",
"UploadingLabel": "Загружено файлов: {{file}} из {{totalFiles}}"
}

View File

@ -71,9 +71,9 @@ class DeleteDialogComponent extends React.Component {
let i = 0;
while (selection.length !== i) {
if (selection[i].fileExst && selection[i].checked) {
fileIds.push(selection[i].id.toString());
fileIds.push(selection[i].id);
} else if (selection[i].checked) {
folderIds.push(selection[i].id.toString());
folderIds.push(selection[i].id);
}
i++;
}

View File

@ -4,12 +4,14 @@ import { toastr, ModalDialog, Button, Text } from "asc-web-components";
import { withTranslation } from "react-i18next";
import i18n from "./i18n";
import { api, utils } from "asc-web-common";
import { fetchFiles } from "../../../store/files/actions";
import store from "../../../store/store";
const { files } = api;
const { changeLanguage } = utils;
const EmptyTrashDialogComponent = props => {
const { onClose, visible, t } = props;
const { onClose, visible, t, filter, currentFolderId } = props;
const [isLoading, setIsLoading] = useState(false);
changeLanguage(i18n);
@ -21,6 +23,7 @@ const EmptyTrashDialogComponent = props => {
files
.emptyTrash()
.then(res => {
fetchFiles(currentFolderId, filter, store.dispatch)
toastr.success(successMessage);
}) //toastr.success("It was successfully deleted 24 from 24"); + progressBar
.catch(err => toastr.error(err))
@ -28,7 +31,7 @@ const EmptyTrashDialogComponent = props => {
setIsLoading(false);
onClose();
});
}, [onClose]);
}, [onClose, filter, currentFolderId]);
return (
<ModalDialogContainer>

View File

@ -0,0 +1,54 @@
import i18n from "i18next";
import Backend from "i18next-xhr-backend";
import config from "../../../../package.json";
import { constants } from 'asc-web-common';
const { LANGUAGE } = constants;
const newInstance = i18n.createInstance();
if (process.env.NODE_ENV === "production") {
newInstance
.use(Backend)
.init({
lng: localStorage.getItem(LANGUAGE) || 'en',
fallbackLng: "en",
interpolation: {
escapeValue: false, // not needed for react as it escapes by default
},
react: {
useSuspense: true
},
backend: {
loadPath: `${config.homepage}/locales/DocEditor/{{lng}}/{{ns}}.json`
}
});
} else if (process.env.NODE_ENV === "development") {
const resources = {
en: {
translation: require("./locales/en/translation.json")
},
ru: {
translation: require("./locales/ru/translation.json")
}
};
newInstance.init({
resources: resources,
lng: localStorage.getItem(LANGUAGE) || 'en',
fallbackLng: "en",
debug: true,
interpolation: {
escapeValue: false, // not needed for react as it escapes by default
},
react: {
useSuspense: false
}
});
}
export default newInstance;

View File

@ -0,0 +1,74 @@
import React from "react";
import { connect } from "react-redux";
import PropTypes from "prop-types";
import { withRouter } from "react-router";
import { RequestLoader } from "asc-web-components";
import { utils, api } from "asc-web-common";
import { withTranslation, I18nextProvider } from 'react-i18next';
import i18n from "./i18n";
const { changeLanguage, getObjectByLocation } = utils;
const { files } = api;
class PureEditor extends React.Component {
constructor(props) {
super(props);
this.state = {
isLoading: false
};
}
onLoading = status => {
this.setState({ isLoading: status });
};
render() {
const { isLoading } = this.state;
const { t } = this.props;
const urlParams = getObjectByLocation(window.location);
const fileId = urlParams.fileId || null;
files.openEdit(fileId)
.then(config => window.DocsAPI.DocEditor("editor", config));
return (
<>
<RequestLoader
visible={isLoading}
zIndex={256}
loaderSize='16px'
loaderColor={"#999"}
label={`${t('LoadingProcessing')} ${t('LoadingDescription')}`}
fontSize='12px'
fontColor={"#999"}
/>
<div id="editor"></div>
</>
);
}
}
const EditorContainer = withTranslation()(PureEditor);
const DocEditor = (props) => {
changeLanguage(i18n);
return (<I18nextProvider i18n={i18n}><EditorContainer {...props} /></I18nextProvider>);
}
DocEditor.propTypes = {
files: PropTypes.array,
history: PropTypes.object,
isLoaded: PropTypes.bool
};
function mapStateToProps(state) {
return {
files: state.files.files,
folders: state.files.folders,
isLoaded: state.auth.isLoaded
};
}
export default connect(mapStateToProps)(withRouter(DocEditor));

View File

@ -0,0 +1,72 @@
{
"NewDocument": "New document",
"NewSpreadsheet": "New spreadsheet",
"NewPresentation": "New presentation",
"NewFolder": "New folder",
"UploadToFolder": "Upload to folder",
"SharingSettings": "Sharing settings",
"LinkForPortalUsers": "Link for portal users",
"MoveTo": "Move to",
"Copy": "Copy",
"Download": "Download",
"Rename": "Rename",
"Delete": "Delete",
"Type": "Type",
"Author": "Author",
"Search": "Search",
"Folders": "Folders",
"Document": "Document",
"Documents": "Documents",
"Spreadsheet": "Spreadsheet",
"Presentation": "Presentation",
"Presentations": "Presentations",
"Spreadsheets": "Spreadsheets",
"Images": "Images",
"Media": "Media",
"Archives": "Archives",
"AllFiles": "All files",
"NoSubfolders": "No subfolders",
"LoadingProcessing": "Loading...",
"LoadingDescription": "Please wait...",
"ByLastModifiedDate": "Last modified date",
"ByCreationDate": "Creation date",
"ByTitle": "Title",
"ByType": "Type",
"BySize": "Size",
"ByAuthor": "Author",
"DirectionAscLabel":"A-Z",
"DirectionDescLabel":"Z-A",
"CountPerPage": "{{count}} per page",
"PageOfTotalPage": "{{page}} of {{totalPage}}",
"PreviousPage": "Previous",
"NextPage": "Next",
"DefaultOptionLabel": "Me",
"LblSelect": "Select",
"AuthorMe": "Me",
"TitleCreated": "Created",
"TitleUploaded": "Uploaded",
"TitleModified": "Updated",
"TitleRemoved": "Removed",
"TitleSubfolders": "Flds",
"TitleDocuments": "Dcs",
"Share": "Share",
"DownloadAs": "Download as",
"More": "More",
"CloseButton": "Close",
"All": "All",
"Files": "Files",
"EmptyRecycleBin": "Empty Recycle Bin",
"Folder": "Folder",
"ClearButton": "Reset filter",
"SubheadingEmptyText": "No files to be displayed in this section",
"MyEmptyContainerDescription": "The documents and image files you create or upload to the portal are kept here in 'My Documents' section. You can open and edit them using the ONLYOFFICE™ portal editor, share them with friends or colleagues, organize into folders. Drag-and-drop the files from your computer here to upload them to your portal even more easily.",
"SharedEmptyContainerDescription": "The 'Shared with Me' section is used to show the files which your friends or colleagues gave you access to. In case you haven't seen the latest changes in the documents they are marked 'new'. You can remove the files from the list clicking the appropriate button.",
"CommonEmptyContainerDescription": "The 'Common Documents' section shows all the documents shared by portal administrator for common access. Only portal administrator can create folders in this section, but with the access granted the portal users can also upload their files here. Drag-and-drop the files from your computer here to upload them to your portal even more easily.",
"TrashEmptyContainerDescription": "The 'Recycle Bin' section is where all the deleted files are moved. You can either restore them in case they are deleted by mistake or delete them permanently. Please note, that when you delete the files from the 'Recycle Bin' they cannot be restored any longer.",
"GoToMyButton": "Go to My Documents",
"BackToParentFolderButton" : "Back to parent folder",
"EmptyFolderHeader": "No files in this folder",
"EmptyFilterSubheadingText": "No files to be displayed for this filter here",
"EmptyFilterDescriptionText": "No files or folders matching your filter can be displayed in this section. Please select other filter options or clear filter to view all the files in this section. You can also look for the file you need in other sections.",
"Filter": "Filter"
}

View File

@ -0,0 +1,72 @@
{
"NewDocument": "Новый документ",
"NewSpreadsheet": "Новая таблица",
"NewPresentation": "Новая презентация",
"NewFolder": "Новая папка",
"UploadToFolder": "Загрузить в папку",
"SharingSettings": "Настройки доступа",
"LinkForPortalUsers": "Ссылка для пользователей портала",
"MoveTo": "Переместить",
"Copy": "Копировать",
"Download": "Скачать",
"Rename": "Переименовать",
"Delete": "Удалить",
"Type": "Тип",
"Author": "Автор",
"Search": "Поиск",
"Folders": "Папки",
"Document": "Документ",
"Documents": "Документы",
"Spreadsheet": "Таблица",
"Presentation": "Презентация",
"Presentations": "Презентации",
"Spreadsheets": "Таблицы",
"Images": "Изображения",
"Media": "Медиа",
"Archives": "Архивы",
"AllFiles": "Все файлы",
"NoSubfolders": "Без подпапок",
"LoadingProcessing": "Загрузка...",
"LoadingDescription": "Пожалуйста, подождите...",
"ByLastModifiedDate": "Дата последнего изменения",
"ByCreationDate": "Дата создания",
"ByTitle": "Название",
"ByType": "Тип",
"BySize": "Размер",
"ByAuthor": "Автор",
"DirectionAscLabel":"А-Я",
"DirectionDescLabel":"Я-А",
"CountPerPage": "{{count}} на странице",
"PageOfTotalPage": "{{page}} из {{totalPage}}",
"PreviousPage": "Предыдущая",
"NextPage": "Следующая",
"DefaultOptionLabel": "Я",
"LblSelect": "Выберите",
"AuthorMe": "Я",
"TitleCreated": "Создана",
"TitleUploaded": "Загружен",
"TitleModified": "Обновлён",
"TitleRemoved": "Удалён",
"TitleSubfolders": "Flds",
"TitleDocuments": "Dcs",
"Share": "Общий доступ",
"DownloadAs": "Скачать как",
"More": "Больше",
"CloseButton": "Закрыть",
"All": "Все",
"Files": "Файлы",
"EmptyRecycleBin": "Очистить корзину",
"Folder": "Папка",
"ClearButton": "Сбросить фильтр",
"SubheadingEmptyText": "Нет файлов для отображения в этом разделе",
"MyEmptyContainerDescription": "Документы и файлы изображений, которые вы создаете или загружаете на портал, хранятся здесь, в разделе «Мои документы». Вы можете открывать и редактировать их с помощью редактора портала ONLYOFFICE ™, делиться ими с друзьями или коллегами, организовывать в папки. Перетащите файлы со своего компьютера сюда, чтобы загрузить их на свой портал еще проще.",
"SharedEmptyContainerDescription": "Раздел «Доступно для меня» используется для отображения файлов, к которым ваши друзья или коллеги предоставили вам доступ. Если вы не видели последние изменения в документах, они помечаются как «новые». Вы можете удалить файлы из списка, нажав соответствующую кнопку.",
"CommonEmptyContainerDescription": "В разделе «Общие документы» отображаются все документы, которыми администратор портала предоставил общий доступ. Только администраторы портала могут создавать папки в этом разделе, но с предоставленным доступом пользователи портала также могут загружать свои файлы здесь. Перетащите файлы со своего компьютера сюда, чтобы загрузить их на свой портал еще проще.",
"TrashEmptyContainerDescription": "В разделе «Корзина» находятся все удаленные файлы. Вы можете восстановить их, если они были удалены по ошибке, или удалить их навсегда. Обратите внимание, что когда вы удаляете файлы из корзины, они больше не могут быть восстановлены.",
"GoToMyButton": "Перейти к моим документам",
"BackToParentFolderButton" : "Вернуться в папку на уровень выше",
"EmptyFolderHeader": "В этой папке нет файлов",
"EmptyFilterSubheadingText": "Здесь нет файлов, соответствующих этому фильтру",
"EmptyFilterDescriptionText": "В этом разделе нет файлов или папок, соответствующих фильтру. Пожалуйста, выберите другие параметры или очистите фильтр, чтобы показать все файлы в этом разделе. Вы можете также поискать нужный файл в других разделах.",
"Filter": "Фильтр"
}

View File

@ -0,0 +1,75 @@
import React, { memo } from "react";
import styled from "styled-components";
import { TextInput, Button } from "asc-web-components";
const EditingWrapper = styled.div`
width: 100%;
display: inline-flex;
align-items: center;
@media (max-width: 1024px) {
height: 56px;
}
.edit-text {
height: 30px;
font-size: 15px;
outline: 0 !important;
font-weight: bold;
margin: 0;
font-family: 'Open Sans',sans-serif,Arial;
text-align: left;
color: #333333;
}
.edit-button {
margin-left: 8px;
height: 30px;
}
.edit-ok-icon {
margin-top: -6px;
width: 16px;
height: 16px;
}
.edit-cancel-icon {
margin-top: -6px;
width: 14px;
height: 14px;
}
`;
const EditingWrapperComponent = props => {
const { isLoading, itemTitle, okIcon, cancelIcon, renameTitle, onKeyUpUpdateItem, onClickUpdateItem, cancelUpdateItem } = props;
return(
<EditingWrapper>
<TextInput
className='edit-text'
name='title'
scale={true}
value={itemTitle}
tabIndex={1}
isAutoFocussed={true}
onChange={renameTitle}
onKeyUp={onKeyUpUpdateItem}
isDisabled={isLoading}
/>
<Button
className='edit-button'
size='medium'
isDisabled={isLoading}
onClick={onClickUpdateItem}
icon={okIcon}
/>
<Button
className='edit-button'
size='medium'
isDisabled={isLoading}
onClick={cancelUpdateItem}
icon={cancelIcon}
/>
</EditingWrapper>
)
}
export default memo(EditingWrapperComponent);

View File

@ -1,120 +1,54 @@
import React from "react";
import styled from "styled-components";
import { constants } from 'asc-web-common';
import { EmptyScreenContainer, Link } from "asc-web-components";
import { fetchFiles } from "../../../../../store/files/actions";
import store from "../../../../../store/store";
import { string } from "prop-types";
const { FileAction } = constants;
import { EmptyScreenContainer } from "asc-web-components";
const EmptyFolderWrapper = styled.div`
.empty-folder_container {
max-width: 550px;
}
.ec-image {
padding-right: 16px;
margin: 0 0 0 auto;
}
.empty-folder_link {
margin-right: 8px;
}
.empty-folder_link {
margin-right: 8px;
}
.empty-folder_container-links {
display: flex;
margin: 12px 0;
}
.empty-folder_container-links {
display: flex;
margin: 12px 0;
}
.empty-folder_container_up-image {
margin-right: 8px;
cursor: pointer;
}
.empty-folder_container_up-image {
margin-right: 8px;
cursor: pointer;
}
.empty-folder_container_plus-image {
margin: -8px 8px 0 0;
.empty-folder_container_plus-image {
margin: -8px 8px 0 0;
}
.empty-folder_container-icon {
margin-right: 4px;
}
}
`;
const EmptyFolderContainer = props => {
const linkStyles = {
isHovered: true,
type: "action",
fontSize: "14px",
className: "empty-folder_link",
display: "flex"
};
const onCreate = (format) => {
props.setAction(
{
type: FileAction.Create,
extension: format,
id: -1
});
}
const onBackToParentFolder = () => {
const newFilter = props.filter.clone();
fetchFiles(props.parentId, newFilter, store.dispatch);
};
const EmptyFoldersContainer = props => {
const imageAlt = "Empty folder image";
return (
<EmptyFolderWrapper>
<EmptyScreenContainer
className="empty-folder_container"
imageSrc="images/empty_screen.png"
imageAlt="Empty folder image"
headerText="В этой папке нет файлов"
buttons={
<>
<div className="empty-folder_container-links">
<Link
className="empty-folder_container_plus-image"
color="#83888d"
fontSize="26px"
fontWeight="800"
noHover
onClick={() => console.log("Create document click")}
>
+
</Link>
<Link onClick={onCreate.bind(this, 'docx')} {...linkStyles}>
Документ,
</Link>
<Link onClick={onCreate.bind(this, 'xlsx')} {...linkStyles}>
Таблица,
</Link>
<Link onClick={onCreate.bind(this, 'pptx')} {...linkStyles}>
Презентация
</Link>
</div>
<div className="empty-folder_container-links">
<Link
className="empty-folder_container_plus-image"
color="#83888d"
fontSize="26px"
fontWeight="800"
onClick={onCreate.bind(this, null)}
noHover
>
+
</Link>
<Link {...linkStyles} onClick={onCreate.bind(this, null)}>
Папка
</Link>
</div>
<div className="empty-folder_container-links">
<img
className="empty-folder_container_up-image"
src="images/up.svg"
onClick={onBackToParentFolder}
/>
<Link onClick={onBackToParentFolder} {...linkStyles}>
Вернутся в папку на уровень выше
</Link>
</div>
</>
}
imageSrc={props.imageSrc}
imageAlt={imageAlt}
headerText={props.headerText}
subheadingText={props.subheadingText}
descriptionText={props.descriptionText}
buttons={props.buttons}
/>
</EmptyFolderWrapper>
);
};
export default EmptyFolderContainer;
export default EmptyFoldersContainer;

View File

@ -4,11 +4,12 @@ import { connect } from "react-redux";
import { withRouter } from "react-router";
import { withTranslation } from "react-i18next";
import styled from "styled-components";
import { RowContent, Link, Text, Icons, Badge, TextInput, Button, toastr } from "asc-web-components";
import { RowContent, Link, Text, Icons, Badge, toastr } from "asc-web-components";
import { constants } from 'asc-web-common';
import { createFile, createFolder, renameFolder, updateFile, setFilter, fetchFiles } from '../../../../../store/files/actions';
import { canWebEdit, canConvert, getTitleWithoutExst } from '../../../../../store/files/selectors';
import store from "../../../../../store/store";
import EditingWrapperComponent from "./EditingWrapperComponent";
const { FileAction } = constants;
@ -24,38 +25,38 @@ class FilesRowContent extends React.PureComponent {
this.state = {
itemTitle: titleWithoutExt,
editingId: props.fileAction.id,
loading: false
editingId: props.fileAction.id
//loading: false
};
}
completeAction = () => {
this.setState({ loading: false }, () =>
this.props.onEditComplete());
//this.setState({ loading: false }, () =>)
this.props.onEditComplete();
}
updateItem = () => {
const { fileAction, updateFile, renameFolder, item } = this.props;
const { fileAction, updateFile, renameFolder, item, onLoading } = this.props;
const { itemTitle } = this.state;
const originalTitle = getTitleWithoutExst(item);
this.setState({ loading: true });
onLoading(true);
if (originalTitle === itemTitle)
return this.completeAction();
item.fileExst
? updateFile(fileAction.id, itemTitle)
.then(() => this.completeAction())
.then(() => this.completeAction()).finally(() => onLoading(false))
: renameFolder(fileAction.id, itemTitle)
.then(() => this.completeAction());
.then(() => this.completeAction()).finally(() => onLoading(false));
};
createItem = () => {
const { createFile, createFolder, item } = this.props;
const { itemTitle } = this.state;
this.setState({ loading: true });
//this.setState({ loading: true });
if (itemTitle.trim() === '')
return this.completeAction();
@ -81,7 +82,7 @@ class FilesRowContent extends React.PureComponent {
}
cancelUpdateItem = () => {
this.setState({ loading: false });
//this.setState({ loading: false });
this.completeAction();
}
@ -160,8 +161,8 @@ class FilesRowContent extends React.PureComponent {
};
render() {
const { t, item, fileAction } = this.props;
const { itemTitle, editingId, loading } = this.state;
const { t, item, fileAction, isLoading, isTrashFolder } = this.props;
const { itemTitle, editingId/*, loading*/ } = this.state;
const {
contentLength,
updated,
@ -195,42 +196,6 @@ class FilesRowContent extends React.PureComponent {
}
`;
const EditingWrapper = styled.div`
width: 100%;
display: inline-flex;
align-items: center;
@media (max-width: 1024px) {
height: 56px;
}
.edit-text {
height: 30px;
font-size: 15px;
outline: 0 !important;
font-weight: bold;
margin: 0;
font-family: 'Open Sans',sans-serif,Arial;
text-align: left;
color: #333333;
}
.edit-button {
margin-left: 8px;
height: 30px;
}
.edit-ok-icon {
margin-top: -6px;
width: 16px;
height: 16px;
}
.edit-cancel-icon {
margin-top: -6px;
width: 14px;
height: 14px;
}
`;
const titleWithoutExt = getTitleWithoutExst(item);
const fileOwner = createdBy && ((this.props.viewer.id === createdBy.id && t("AuthorMe")) || createdBy.displayName);
const updatedDate = updated && this.getStatusByDate();
@ -252,35 +217,19 @@ class FilesRowContent extends React.PureComponent {
/>;
const isEdit = (id === editingId) && (fileExst === fileAction.extension);
const linkStyles = isTrashFolder ? { noHover: true } : { onClick: this.onFilesClick };
return isEdit
? (<EditingWrapper>
<TextInput
className='edit-text'
name='title'
scale={true}
value={itemTitle}
tabIndex={1}
isAutoFocussed={true}
onChange={this.renameTitle}
onKeyUp={this.onKeyUpUpdateItem}
isDisabled={loading}
? <EditingWrapperComponent
isLoading={isLoading}
itemTitle={itemTitle}
okIcon={okIcon}
cancelIcon={cancelIcon}
renameTitle={this.renameTitle}
onKeyUpUpdateItem={this.onKeyUpUpdateItem}
onClickUpdateItem={this.onClickUpdateItem}
cancelUpdateItem={this.cancelUpdateItem}
/>
<Button
className='edit-button'
size='medium'
isDisabled={loading}
onClick={this.onClickUpdateItem}
icon={okIcon}
/>
<Button
className='edit-button'
size='medium'
isDisabled={loading}
onClick={this.cancelUpdateItem}
icon={cancelIcon}
/>
</EditingWrapper>)
: (
<SimpleFilesRowContent
sideColor="#333"
@ -292,10 +241,10 @@ class FilesRowContent extends React.PureComponent {
type='page'
title={titleWithoutExt}
fontWeight="bold"
onClick={this.onFilesClick}
fontSize='15px'
{...linkStyles}
color="#333"
isTextOverflow={true}
isTextOverflow
>
{titleWithoutExt}
</Link>
@ -421,10 +370,14 @@ class FilesRowContent extends React.PureComponent {
};
function mapStateToProps(state) {
const { filter, fileAction, selectedFolder, treeFolders } = state.files;
const indexOfTrash = 3;
return {
filter: state.files.filter,
fileAction: state.files.fileAction,
parentFolder: state.files.selectedFolder.id
filter,
fileAction,
parentFolder: selectedFolder.id,
isTrashFolder: treeFolders[indexOfTrash].id === selectedFolder.id
}
}

View File

@ -6,10 +6,11 @@ import { withTranslation } from "react-i18next";
import isEqual from "lodash/isEqual";
import styled from "styled-components";
import {
Icons,
IconButton,
Row,
RowContainer,
toastr
toastr,
Link
} from "asc-web-components";
import EmptyFolderContainer from "./EmptyFolderContainer";
import FilesRowContent from "./FilesRowContent";
@ -25,14 +26,16 @@ import {
setAction,
setTreeFolders
} from '../../../../../store/files/actions';
import { isFileSelected, getFileIcon, getFolderIcon } from '../../../../../store/files/selectors';
import { isFileSelected, getFileIcon, getFolderIcon, getFolderType, loopTreeFolders } from '../../../../../store/files/selectors';
import store from "../../../../../store/store";
//import { getFilterByLocation } from "../../../../../helpers/converters";
//import config from "../../../../../../package.json";
//const { FilesFilter } = api;
const { FilesFilter } = api;
const { FileAction } = constants;
const linkStyles = { isHovered: true, type: "action", fontSize: "14px", className: "empty-folder_link", display: "flex" };
class SectionBodyContent extends React.PureComponent {
constructor(props) {
super(props);
@ -84,11 +87,12 @@ class SectionBodyContent extends React.PureComponent {
if (fileAction.type === FileAction.Create || fileAction.type === FileAction.Rename) {
onLoading(true);
fetchFiles(folderId, filter, store.dispatch).then(data => {
const newItem = item.id === -1 ? null : item;
if (!item.fileExst) {
const path = data.selectedFolder.pathParts;
const newTreeFolders = treeFolders;
const folders = data.selectedFolder.folders;
this.loop(path, newTreeFolders, item.parentId, folders);
loopTreeFolders(path, newTreeFolders, folders, null, newItem);
setTreeFolders(newTreeFolders);
}
}).finally(() => onLoading(false))
@ -108,76 +112,97 @@ class SectionBodyContent extends React.PureComponent {
}
onDeleteFile = (fileId, currentFolderId) => {
const { deleteFile, filter } = this.props;
const { deleteFile, filter, onLoading } = this.props;
onLoading(true);
deleteFile(fileId)
.catch(err => toastr.error(err))
.then(() => fetchFiles(currentFolderId, filter, store.dispatch))
.then(() => toastr.success(`File moved to recycle bin`));
.then(() => toastr.success(`File moved to recycle bin`))
.finally(() => onLoading(false));
}
loop = (path, item, folderId, folders, foldersCount) => {
const newPath = path;
while (path.length !== 0) {
const newItems = item.find(x => x.id === path[0]);
newPath.shift();
if (path.length === 0) {
const currentItem = item.find(x => x.id === folderId);
currentItem.folders = folders;
currentItem.foldersCount = foldersCount;
return;
}
this.loop(newPath, newItems.folders, folderId, folders, foldersCount);
}
};
onDeleteFolder = (folderId, currentFolderId) => {
const { deleteFolder, filter, treeFolders, setTreeFolders, onLoading } = this.props;
const { deleteFolder, filter, treeFolders, setTreeFolders, onLoading, currentFolderType } = this.props;
onLoading(true);
deleteFolder(folderId, currentFolderId)
.catch(err => toastr.error(err))
.then(() =>
fetchFiles(currentFolderId, filter, store.dispatch).then(data => {
const path = data.selectedFolder.pathParts;
const newTreeFolders = treeFolders;
const folders = data.selectedFolder.folders;
const foldersCount = data.selectedFolder.foldersCount;
this.loop(path, newTreeFolders, currentFolderId, folders, foldersCount);
setTreeFolders(newTreeFolders);
if(currentFolderType !== "Trash") {
const path = data.selectedFolder.pathParts;
const newTreeFolders = treeFolders;
const folders = data.selectedFolder.folders;
const foldersCount = data.selectedFolder.foldersCount;
loopTreeFolders(path, newTreeFolders, folders, foldersCount);
setTreeFolders(newTreeFolders);
}
})
)
.then(() => toastr.success(`Folder moved to recycle bin`))
.finally(() => onLoading(false));
}
onClickLinkForPortal = (folderId) => {
return fetchFolder(folderId, store.dispatch);
onClickShare = item => {
return false;
}
onClickLinkForPortal = item => {
return fetchFolder(item.folderId, store.dispatch);
}
onClickDownload = item => {
return window.open(item.viewUrl, "_blank");
}
onClickLinkEdit = item => {
return window.open(`./doceditor?fileId=${item.id}`, "_blank");
}
getFilesContextOptions = (item, viewer) => {
return [
const isFile = !!item.fileExst;
const menu = [
{
key: "sharing-settings",
label: "Sharing settings",
onClick: () => { },
onClick: this.onClickShare.bind(this, item),
disabled: true
},
isFile
? {
key: "send-by-email",
label: "Send by e-mail",
onClick: () => { },
disabled: true
}
: null,
{
key: "link-for-portal-users",
label: "Link for portal users",
onClick: this.onClickLinkForPortal.bind(this, item.folderId),
onClick: this.onClickLinkForPortal.bind(this, item),
disabled: true
},
{
key: "sep",
isSeparator: true
},
{
key: "download",
label: "Download",
onClick: () => { },
disabled: true
},
isFile
? {
key: "edit",
label: "Edit",
onClick: this.onClickLinkEdit.bind(this, item),
disabled: false
}
: null,
isFile
? {
key: "download",
label: "Download",
onClick: this.onClickDownload.bind(this, item),
disabled: false
}
: null,
{
key: "rename",
label: "Rename",
@ -190,7 +215,9 @@ class SectionBodyContent extends React.PureComponent {
onClick: this.onClickDelete.bind(this, item),
disabled: false
},
]
];
return menu;
};
needForUpdate = (currentProps, nextProps) => {
@ -222,6 +249,7 @@ class SectionBodyContent extends React.PureComponent {
const icon = extension
? getFileIcon(extension, 24)
: getFolderIcon(item.providerKey, 24);
const loader = <div style={{width: '24px'}}></div>
return <ReactSVG
beforeInjection={svg => {
@ -229,17 +257,270 @@ class SectionBodyContent extends React.PureComponent {
isEdit && svg.setAttribute('style', 'margin-left: 24px');
}}
src={icon}
loading={() => loader}
/>;
};
onCreate = (format) => {
this.props.setAction({
type: FileAction.Create,
extension: format,
id: -1,
});
};
onResetFilter = () => {
const { selectedFolderId, onLoading } = this.props;
onLoading(true);
const newFilter = FilesFilter.getDefault();
fetchFiles(selectedFolderId, newFilter, store.dispatch).catch(err =>
toastr.error(err)
).finally(() => onLoading(false));
}
onGoToMyDocuments = () => {
const { filter, myDocumentsId, onLoading } = this.props;
const newFilter = filter.clone();
onLoading(true);
fetchFiles(myDocumentsId, newFilter, store.dispatch).finally(() =>
onLoading(false)
);
};
onBackToParentFolder = () => {
const { filter, parentId, onLoading } = this.props;
const newFilter = filter.clone();
onLoading(true);
fetchFiles(parentId, newFilter, store.dispatch).finally(() =>
onLoading(false)
);
};
renderEmptyRootFolderContainer = () => {
const { currentFolderType, title, t } = this.props;
const subheadingText = t("SubheadingEmptyText");
const myDescription = t("MyEmptyContainerDescription");
const shareDescription = t("SharedEmptyContainerDescription");
const commonDescription = t("CommonEmptyContainerDescription");
const trashDescription = t("TrashEmptyContainerDescription");
const commonButtons = (
<>
<div className="empty-folder_container-links">
<Link
className="empty-folder_container_plus-image"
color="#83888d"
fontSize="26px"
fontWeight="800"
noHover
onClick={this.onCreate.bind(this, "docx")}
>
+
</Link>
<Link onClick={this.onCreate.bind(this, "docx")} {...linkStyles}>
{t("Document")},
</Link>
<Link onClick={this.onCreate.bind(this, "xlsx")} {...linkStyles}>
{t("Spreadsheet")},
</Link>
<Link onClick={this.onCreate.bind(this, "pptx")} {...linkStyles}>
{t("Presentation")}
</Link>
</div>
<div className="empty-folder_container-links">
<Link
className="empty-folder_container_plus-image"
color="#83888d"
fontSize="26px"
fontWeight="800"
onClick={this.onCreate.bind(this, null)}
noHover
>
+
</Link>
<Link {...linkStyles} onClick={this.onCreate.bind(this, null)}>
{t("Folder")}
</Link>
</div>
</>
);
const trashButtons = (
<div className="empty-folder_container-links">
<img
className="empty-folder_container_up-image"
src="images/empty_screen_people.svg"
alt=""
onClick={this.onGoToMyDocuments}
/>
<Link onClick={this.onGoToMyDocuments} {...linkStyles}>
{t("GoToMyButton")}
</Link>
</div>
);
switch (currentFolderType) {
case "My":
return (
<EmptyFolderContainer
headerText={title}
subheadingText={subheadingText}
descriptionText={myDescription}
imageSrc="images/empty_screen.png"
buttons={commonButtons}
/>
);
case "Share":
return (
<EmptyFolderContainer
headerText={title}
subheadingText={subheadingText}
descriptionText={shareDescription}
imageSrc="images/empty_screen_forme.png"
/>
);
case "Common":
return (
<EmptyFolderContainer
headerText={title}
subheadingText={subheadingText}
descriptionText={commonDescription}
imageSrc="images/empty_screen_corporate.png"
buttons={commonButtons}
/>
);
case "Trash":
return (
<EmptyFolderContainer
headerText={title}
subheadingText={subheadingText}
descriptionText={trashDescription}
imageSrc="images/empty_screen_trash.png"
buttons={trashButtons}
/>
);
default:
return;
}
};
renderEmptyFolderContainer = () => {
const { t } = this.props;
const buttons = (
<>
<div className="empty-folder_container-links">
<Link
className="empty-folder_container_plus-image"
color="#83888d"
fontSize="26px"
fontWeight="800"
noHover
onClick={() => console.log("Create document click")}
>
+
</Link>
<Link onClick={this.onCreate.bind(this, "docx")} {...linkStyles}>
{t("Document")},
</Link>
<Link onClick={this.onCreate.bind(this, "xlsx")} {...linkStyles}>
{t("Spreadsheet")},
</Link>
<Link onClick={this.onCreate.bind(this, "pptx")} {...linkStyles}>
{t("Presentation")}
</Link>
</div>
<div className="empty-folder_container-links">
<Link
className="empty-folder_container_plus-image"
color="#83888d"
fontSize="26px"
fontWeight="800"
onClick={this.onCreate.bind(this, null)}
noHover
>
+
</Link>
<Link {...linkStyles} onClick={this.onCreate.bind(this, null)}>
{t("Folder")}
</Link>
</div>
<div className="empty-folder_container-links">
<img
className="empty-folder_container_up-image"
src="images/up.svg"
onClick={this.onBackToParentFolder}
alt=""
/>
<Link onClick={this.onBackToParentFolder} {...linkStyles}>
{t("BackToParentFolderButton")}
</Link>
</div>
</>
);
return (
<EmptyFolderContainer
headerText={t("EmptyFolderHeader")}
imageSrc="images/empty_screen.png"
buttons={buttons}
/>
);
};
renderEmptyFilterContainer = () => {
const { t } = this.props;
const subheadingText = t("EmptyFilterSubheadingText");
const descriptionText = t("EmptyFilterDescriptionText");
const buttons = (
<div className="empty-folder_container-links">
<IconButton
className="empty-folder_container-icon"
size="12"
onClick={this.onResetFilter}
iconName="CrossIcon"
isFill
color="A3A9AE"
/>
<Link onClick={this.onResetFilter} {...linkStyles}>
{this.props.t("ClearButton")}
</Link>
</div>
);
return (
<EmptyFolderContainer
headerText={t("Filter")}
subheadingText={subheadingText}
descriptionText={descriptionText}
imageSrc="images/empty_screen_filter.png"
buttons={buttons}
/>
)
}
render() {
const { files, folders, viewer, parentId, folderId, settings, selection, fileAction, onLoading, filter } = this.props;
const {
files,
folders,
viewer,
parentId,
folderId,
settings,
selection,
fileAction,
onLoading,
isLoading,
currentFolderCount,
} = this.props;
const { editingId } = this.state;
let items = [...folders, ...files];
const SimpleFilesRow = styled(Row)`
${props => !props.contextOptions && `
${(props) =>
!props.contextOptions &&
`
& > div:last-child {
width: 0px;
}
@ -249,43 +530,63 @@ class SectionBodyContent extends React.PureComponent {
if (fileAction && fileAction.type === FileAction.Create) {
items.unshift({
id: -1,
title: '',
title: "",
parentId: folderId,
fileExst: fileAction.extension
})
fileExst: fileAction.extension,
});
}
return items.length > 0 ? (
<RowContainer useReactWindow={false}>
{items.map(item => {
const isEdit = fileAction.type && (editingId === item.id || item.id === -1) && (item.fileExst === fileAction.extension);
const contextOptions = this.getFilesContextOptions(item, viewer).filter(o => o);
const contextOptionsProps = !contextOptions.length || isEdit
? {}
: { contextOptions };
const checked = isFileSelected(selection, item.id, item.parentId);
const checkedProps = /* isAdmin(viewer) */ isEdit ? {} : { checked };
const element = this.getItemIcon(item, isEdit);
return (
<SimpleFilesRow
key={item.id}
data={item}
element={element}
onSelect={this.onContentRowSelect}
editing={editingId}
{...checkedProps}
{...contextOptionsProps}
needForUpdate={this.needForUpdate}
>
<FilesRowContent item={item} viewer={viewer} culture={settings.culture} onEditComplete={this.onEditComplete.bind(this, item)} onLoading={onLoading} />
</SimpleFilesRow>
);
})}
</RowContainer>
) : parentId !== 0 ? (
<EmptyFolderContainer parentId={parentId} filter={filter} setAction={this.props.setAction} />
) : <p>RootFolderContainer</p>;
return !fileAction.id && currentFolderCount === 0 ? (
parentId === 0 ? (
this.renderEmptyRootFolderContainer()
) : (
this.renderEmptyFolderContainer()
)
) : !fileAction.id && items.length === 0 ? (
this.renderEmptyFilterContainer()
) : (
<RowContainer useReactWindow={false}>
{items.map((item) => {
const isEdit =
fileAction.type &&
(editingId === item.id || item.id === -1) &&
item.fileExst === fileAction.extension;
const contextOptions = this.getFilesContextOptions(
item,
viewer
).filter((o) => o);
const contextOptionsProps =
!contextOptions.length || isEdit ? {} : { contextOptions };
const checked = isFileSelected(selection, item.id, item.parentId);
const checkedProps = /* isAdmin(viewer) */ isEdit ? {} : { checked };
const element = this.getItemIcon(item, isEdit);
return (
<SimpleFilesRow
key={item.id}
data={item}
element={element}
onSelect={this.onContentRowSelect}
editing={editingId}
{...checkedProps}
{...contextOptionsProps}
needForUpdate={this.needForUpdate}
>
<FilesRowContent
item={item}
viewer={viewer}
culture={settings.culture}
onEditComplete={this.onEditComplete.bind(this, item)}
onLoading={onLoading}
isLoading={isLoading}
/>
</SimpleFilesRow>
);
})}
</RowContainer>
);
}
}
@ -294,6 +595,13 @@ SectionBodyContent.defaultProps = {
};
const mapStateToProps = state => {
const { selectedFolder, treeFolders } = state.files;
const { id, title, foldersCount, filesCount } = selectedFolder;
const currentFolderType = getFolderType(id, treeFolders);
const myFolderIndex = 0;
const currentFolderCount = filesCount + foldersCount;
return {
fileAction: state.files.fileAction,
files: state.files.files,
@ -305,7 +613,12 @@ const mapStateToProps = state => {
selection: state.files.selection,
settings: state.auth.settings,
viewer: state.auth.user,
treeFolders: state.files.treeFolders
treeFolders: state.files.treeFolders,
currentFolderType,
title,
myDocumentsId: treeFolders[myFolderIndex].id,
currentFolderCount,
selectedFolderId: id
};
};

View File

@ -17,7 +17,8 @@ import { EmptyTrashDialog, DeleteDialog } from "../../../../dialogs";
import { SharingPanel } from "../../../../panels";
import {
isCanBeDeleted,
getAccessOption
getAccessOption,
checkFolderType
} from "../../../../../store/files/selectors";
const { isAdmin } = store.auth.selectors;
@ -233,7 +234,11 @@ class SectionHeaderContent extends React.Component {
};
onBackToParentFolder = () => {
fetchFiles(this.props.parentId, this.props.filter, filesStore.dispatch);
const { onLoading, parentId, filter } = this.props;
onLoading(true);
fetchFiles(parentId, filter, filesStore.dispatch).finally(() =>
onLoading(false)
);
};
@ -255,7 +260,8 @@ class SectionHeaderContent extends React.Component {
onCheck,
title,
accessOptions,
shareDataItems
shareDataItems,
currentFolderId
} = this.props;
const {
showDeleteDialog,
@ -416,6 +422,7 @@ class SectionHeaderContent extends React.Component {
{showEmptyTrashDialog && (
<EmptyTrashDialog
currentFolderId={currentFolderId}
visible={showEmptyTrashDialog}
onClose={this.onEmptyTrashAction}
/>
@ -448,14 +455,15 @@ const mapStateToProps = state => {
return {
folder: parentId !== 0,
isAdmin: isAdmin(user),
isRecycleBinFolder: treeFolders[indexOfTrash].id === id,
isRecycleBinFolder: checkFolderType(id, indexOfTrash, treeFolders),
parentId,
selection,
title,
filter,
deleteDialogVisible: isCanBeDeleted(selectedFolder, user),
accessOptions: getAccessOption(selection),
shareDataItems
shareDataItems,
currentFolderId: id
};
};

View File

@ -119,7 +119,7 @@ const SectionPagingContent = ({
const selectedPageItem = pageItems.find(x => x.key === filter.page) || emptyPageSelection;
const selectedCountItem = countItems.find(x => x.key === filter.pageCount) || emptyCountSelection;
console.log("SectionPagingContent render", filter);
//console.log("SectionPagingContent render", filter);
return filter.total < filter.pageCount ? (
<></>

View File

@ -2,7 +2,7 @@ import React from "react";
import { connect } from "react-redux";
import PropTypes from "prop-types";
import { withRouter } from "react-router";
import { RequestLoader } from "asc-web-components";
import { RequestLoader, Checkbox } from "asc-web-components";
import { PageLayout, utils } from "asc-web-common";
import { withTranslation, I18nextProvider } from 'react-i18next';
import i18n from "./i18n";
@ -29,7 +29,14 @@ class PureHome extends React.Component {
isHeaderVisible: false,
isHeaderIndeterminate: false,
isHeaderChecked: false,
isLoading: false
isLoading: false,
showProgressBar: false,
progressBarValue: 0,
progressBarLabel: "",
overwriteSetting: false,
uploadOriginalFormatSetting: false,
hideWindowSetting: false
};
}
@ -93,15 +100,53 @@ class PureHome extends React.Component {
this.setState({ isLoading: status });
};
setProgressVisible = visible => {
if(visible) {this.setState({ showProgressBar: visible })}
else { setTimeout(() => this.setState({ showProgressBar: visible, progressBarValue: 0 }), 10000)};
};
setProgressValue = value => this.setState({ progressBarValue: value });
setProgressLabel = label => this.setState({ progressBarLabel: label });
onChangeOverwrite = () => this.setState({overwriteSetting: !this.state.overwriteSetting})
onChangeOriginalFormat = () => this.setState({uploadOriginalFormatSetting: !this.state.uploadOriginalFormatSetting})
onChangeWindowVisible = () => this.setState({hideWindowSetting: !this.state.hideWindowSetting})
render() {
const {
isHeaderVisible,
isHeaderIndeterminate,
isHeaderChecked,
selected,
isLoading
isLoading,
showProgressBar,
progressBarValue,
progressBarLabel,
overwriteSetting,
uploadOriginalFormatSetting,
hideWindowSetting
} = this.state;
const { t } = this.props;
const progressBarContent = (
<div>
<Checkbox
onChange={this.onChangeOverwrite}
isChecked={overwriteSetting}
label={t("OverwriteSetting")}
/>
<Checkbox
onChange={this.onChangeOriginalFormat}
isChecked={uploadOriginalFormatSetting}
label={t("UploadOriginalFormatSetting")}
/>
<Checkbox
onChange={this.onChangeWindowVisible}
isChecked={hideWindowSetting}
label={t("HideWindowSetting")}
/>
</div>
);
return (
<>
<RequestLoader
@ -116,15 +161,19 @@ class PureHome extends React.Component {
<PageLayout
withBodyScroll
withBodyAutoFocus
//showProgressBar
//progressBarMaxValue
//progressBarValue
//progressBarDropDownContent
//progressBarLabel={`Uploading files: ${progressBarValue} of ${progressBarMaxValue}`}
showProgressBar={showProgressBar}
progressBarValue={progressBarValue}
progressBarDropDownContent={progressBarContent}
progressBarLabel={progressBarLabel}
articleHeaderContent={<ArticleHeaderContent />}
articleMainButtonContent={<ArticleMainButtonContent/>}
articleMainButtonContent={
<ArticleMainButtonContent
onLoading={this.onLoading}
setProgressVisible={this.setProgressVisible}
setProgressValue={this.setProgressValue}
setProgressContent={this.setProgressContent}
setProgressLabel={this.setProgressLabel}
/>}
articleBodyContent={<ArticleBodyContent onLoading={this.onLoading} isLoading={isLoading} />}
sectionHeaderContent={
<SectionHeaderContent
@ -141,6 +190,7 @@ class PureHome extends React.Component {
sectionBodyContent={
<SectionBodyContent
selected={selected}
isLoading={isLoading}
onLoading={this.onLoading}
onChange={this.onRowChange}
/>

View File

@ -15,7 +15,10 @@
"Author": "Author",
"Search": "Search",
"Folders": "Folders",
"Document": "Document",
"Documents": "Documents",
"Spreadsheet": "Spreadsheet",
"Presentation": "Presentation",
"Presentations": "Presentations",
"Spreadsheets": "Spreadsheets",
"Images": "Images",
@ -52,5 +55,21 @@
"CloseButton": "Close",
"All": "All",
"Files": "Files",
"EmptyRecycleBin": "Empty Recycle Bin"
"EmptyRecycleBin": "Empty Recycle Bin",
"Folder": "Folder",
"ClearButton": "Reset filter",
"SubheadingEmptyText": "No files to be displayed in this section",
"MyEmptyContainerDescription": "The documents and image files you create or upload to the portal are kept here in 'My Documents' section. You can open and edit them using the ONLYOFFICE™ portal editor, share them with friends or colleagues, organize into folders. Drag-and-drop the files from your computer here to upload them to your portal even more easily.",
"SharedEmptyContainerDescription": "The 'Shared with Me' section is used to show the files which your friends or colleagues gave you access to. In case you haven't seen the latest changes in the documents they are marked 'new'. You can remove the files from the list clicking the appropriate button.",
"CommonEmptyContainerDescription": "The 'Common Documents' section shows all the documents shared by portal administrator for common access. Only portal administrator can create folders in this section, but with the access granted the portal users can also upload their files here. Drag-and-drop the files from your computer here to upload them to your portal even more easily.",
"TrashEmptyContainerDescription": "The 'Recycle Bin' section is where all the deleted files are moved. You can either restore them in case they are deleted by mistake or delete them permanently. Please note, that when you delete the files from the 'Recycle Bin' they cannot be restored any longer.",
"GoToMyButton": "Go to My Documents",
"BackToParentFolderButton" : "Back to parent folder",
"EmptyFolderHeader": "No files in this folder",
"EmptyFilterSubheadingText": "No files to be displayed for this filter here",
"EmptyFilterDescriptionText": "No files or folders matching your filter can be displayed in this section. Please select other filter options or clear filter to view all the files in this section. You can also look for the file you need in other sections.",
"Filter": "Filter",
"OverwriteSetting": "Overwrite existing file with the same name",
"UploadOriginalFormatSetting": "Upload the documents in original format as well",
"HideWindowSetting": "Show this window minimized"
}

View File

@ -15,7 +15,10 @@
"Author": "Автор",
"Search": "Поиск",
"Folders": "Папки",
"Document": "Документ",
"Documents": "Документы",
"Spreadsheet": "Таблица",
"Presentation": "Презентация",
"Presentations": "Презентации",
"Spreadsheets": "Таблицы",
"Images": "Изображения",
@ -52,5 +55,21 @@
"CloseButton": "Закрыть",
"All": "Все",
"Files": "Файлы",
"EmptyRecycleBin": "Очистить корзину"
"EmptyRecycleBin": "Очистить корзину",
"Folder": "Папка",
"ClearButton": "Сбросить фильтр",
"SubheadingEmptyText": "Нет файлов для отображения в этом разделе",
"MyEmptyContainerDescription": "Документы и файлы изображений, которые вы создаете или загружаете на портал, хранятся здесь, в разделе «Мои документы». Вы можете открывать и редактировать их с помощью редактора портала ONLYOFFICE ™, делиться ими с друзьями или коллегами, организовывать в папки. Перетащите файлы со своего компьютера сюда, чтобы загрузить их на свой портал еще проще.",
"SharedEmptyContainerDescription": "Раздел «Доступно для меня» используется для отображения файлов, к которым ваши друзья или коллеги предоставили вам доступ. Если вы не видели последние изменения в документах, они помечаются как «новые». Вы можете удалить файлы из списка, нажав соответствующую кнопку.",
"CommonEmptyContainerDescription": "В разделе «Общие документы» отображаются все документы, которыми администратор портала предоставил общий доступ. Только администраторы портала могут создавать папки в этом разделе, но с предоставленным доступом пользователи портала также могут загружать свои файлы здесь. Перетащите файлы со своего компьютера сюда, чтобы загрузить их на свой портал еще проще.",
"TrashEmptyContainerDescription": "В разделе «Корзина» находятся все удаленные файлы. Вы можете восстановить их, если они были удалены по ошибке, или удалить их навсегда. Обратите внимание, что когда вы удаляете файлы из корзины, они больше не могут быть восстановлены.",
"GoToMyButton": "Перейти к моим документам",
"BackToParentFolderButton" : "Вернуться в папку на уровень выше",
"EmptyFolderHeader": "В этой папке нет файлов",
"EmptyFilterSubheadingText": "Здесь нет файлов, соответствующих этому фильтру",
"EmptyFilterDescriptionText": "В этом разделе нет файлов или папок, соответствующих фильтру. Пожалуйста, выберите другие параметры или очистите фильтр, чтобы показать все файлы в этом разделе. Вы можете также поискать нужный файл в других разделах.",
"Filter": "Фильтр",
"OverwriteSetting": "Перезаписывать существующий файл с таким же именем",
"UploadOriginalFormatSetting": "Сохранять также копию файла в исходном формате",
"HideWindowSetting": "Показывать это окно минимизированным"
}

View File

@ -18,7 +18,7 @@ import {
import { connect } from "react-redux";
import { withRouter } from "react-router";
import { withTranslation } from "react-i18next";
import { utils as commonUtils, constants } from "asc-web-common";
import { utils as commonUtils, constants, api } from "asc-web-common";
import i18n from "./i18n";
import {
getShareUsersAndGroups,
@ -27,6 +27,7 @@ import {
setSharedFiles,
setShareData,
} from "../../../store/files/actions";
import { checkFolderType } from "../../../store/files/selectors";
import {
StyledSharingPanel,
StyledContent,
@ -39,6 +40,7 @@ import SharingRow from "./SharingRow";
const { changeLanguage } = commonUtils;
const { ShareAccessRights } = constants;
const { files } = api;
class SharingPanelComponent extends React.Component {
constructor(props) {
@ -60,7 +62,7 @@ class SharingPanelComponent extends React.Component {
rights: "ReadOnly",
accessNumber: ShareAccessRights.ReadOnly,
},
shareLink: ""
shareLink: "",
};
this.ref = React.createRef();
@ -74,7 +76,7 @@ class SharingPanelComponent extends React.Component {
this.setState({ showActionPanel: !this.state.showActionPanel });
};
onKeyClick = () => console.log("onKeyClick");
//onKeyClick = () => console.log("onKeyClick");
onSaveClick = () => {
toastr.success("onSaveClick");
@ -272,14 +274,18 @@ class SharingPanelComponent extends React.Component {
};
getItemAccess = (item) => {
const fullAccessRights = {
icon: "AccessEditIcon",
rights: "FullAccess",
accessNumber: ShareAccessRights.FullAccess,
isOwner: item.isOwner,
};
if (item.sharedTo.shareLink) {
return fullAccessRights;
}
switch (item.access) {
case 1:
return {
icon: "AccessEditIcon",
rights: "FullAccess",
accessNumber: ShareAccessRights.FullAccess,
isOwner: item.isOwner,
};
return fullAccessRights;
case 2:
return {
icon: "EyeIcon",
@ -320,54 +326,75 @@ class SharingPanelComponent extends React.Component {
}
};
getShareDataItems = (res, shareDataItems, foldersIds, filesIds) => {
const listOwners = [];
getShareDataItems = (items, shareDataItems, foldersIds, filesIds) => {
const storeShareData = [];
const arrayItems = [];
for (let item of items) {
const rights = this.getItemAccess(item);
for (let array of res) {
let foldersIndex = 0;
let filesIndex = 0;
let shareIndex = 0;
if (foldersIds.length > foldersIndex) {
storeShareData.push({
folderId: foldersIds[foldersIndex],
shareDataItems: [],
});
foldersIndex++;
} else {
storeShareData.push({
fileId: filesIds[filesIndex],
shareDataItems: [],
});
filesIndex++;
if (rights) {
item.sharedTo = { ...item.sharedTo, ...{ rights } };
arrayItems.push(item.sharedTo);
}
}
if (foldersIds) {
storeShareData.push({
folderId: foldersIds[0],
shareDataItems: arrayItems,
});
} else {
storeShareData.push({ fileId: filesIds[0], shareDataItems: arrayItems });
}
this.props.setShareData([...storeShareData, ...this.props.shareData]);
let arrayItems = [];
let listOfArrays = [...shareDataItems.slice(0), ...[arrayItems.slice(0)]];
listOfArrays = listOfArrays.filter((x) => x.length !== 0);
let allItems = [];
for (let array of listOfArrays) {
for (let item of array) {
const rights = this.getItemAccess(item);
allItems.push(item);
}
}
if (rights) {
item.sharedTo = { ...item.sharedTo, ...{ rights } };
item.sharedTo.shareLink
? shareDataItems.unshift(item.sharedTo)
: shareDataItems.push(item.sharedTo);
arrayItems.push(item.sharedTo);
allItems = this.removeDuplicateShareData(allItems);
allItems = JSON.parse(JSON.stringify(allItems));
let stash = 0;
for (let item of allItems) {
let length = listOfArrays.length;
if (!item.shareLink) {
while (length !== 0) {
if (listOfArrays[length - 1].length !== 0) {
stash = listOfArrays[length - 1].find((x) => x.id === item.id);
if (stash === this.props.isMyId) {
const adminRights = {
icon: "AccessEditIcon",
rights: "FullAccess",
accessNumber: ShareAccessRights.FullAccess,
isOwner: item.isOwner,
};
item.rights = adminRights;
} else if (
!stash ||
item.rights.rights !== stash.rights.rights ||
item.rights.isOwner !== stash.rights.isOwner
) {
const variesRights = {
icon: "CatalogQuestionIcon",
rights: "Varies",
isOwner: false,
};
item.rights = variesRights;
}
}
length--;
}
}
storeShareData[shareIndex].shareDataItems = arrayItems;
arrayItems = [];
shareIndex++;
}
for (let item of shareDataItems) {
if (item.rights && item.rights.isOwner) {
listOwners.push(item.id);
}
}
this.props.setShareData([...storeShareData, ...this.props.shareData]);
return [shareDataItems, listOwners];
this.setShareDataItemsFunction(allItems);
};
removeDuplicateShareData = (shareDataItems) => {
@ -379,104 +406,99 @@ class SharingPanelComponent extends React.Component {
});
};
setDuplicateItemsRights = (shareDataItems, rights) => {
const array = shareDataItems.slice(0);
setShareDataItemsFunction = (shareDataItems) => {
shareDataItems = shareDataItems.filter(
(x) => x !== undefined && x.length !== 0
);
let i = 0;
while (array.length !== 0) {
const item = array[i];
array.splice(i, 1);
const duplicateItem = array.find((x) => x.id === item.id);
if (duplicateItem) {
if (item.rights.rights !== duplicateItem.rights.rights) {
const shareIndex = shareDataItems.findIndex(
(x) => x.id === duplicateItem.id
);
shareDataItems[shareIndex].rights = rights;
}
}
}
return shareDataItems;
const clearShareData = JSON.parse(JSON.stringify(shareDataItems));
this.props.setShareDataItems(shareDataItems.slice(0));
this.setState({ baseShareData: clearShareData });
};
setOwnersRights = (listOwners, shareDataItems, rights) => {
if (listOwners.length > 1) {
while (listOwners.length !== 0) {
const index = shareDataItems.findIndex((x) => x.id === listOwners[0]);
shareDataItems[index].rights = rights;
listOwners.splice(0, 1);
}
}
return shareDataItems;
};
getShareData() {
getData = () => {
const { selection, shareData } = this.props;
const foldersIds = [];
const filesIds = [];
let shareDataItems = [];
let listOwners = [];
const shareDataItems = [];
const folderId = [];
const fileId = [];
for (let item of selection) {
if (item.fileExst) {
const itemShareData = shareData.find((x) => x.fileId === item.id);
if (itemShareData) {
for (let item of itemShareData.shareDataItems) {
shareDataItems.push(item);
}
shareDataItems.push(itemShareData.shareDataItems);
} else {
filesIds.push(item.id);
fileId.push(item.id);
}
} else {
const itemShareData = shareData.find((x) => x.folderId === item.id);
if (itemShareData) {
for (let item of itemShareData.shareDataItems) {
shareDataItems.push(item);
}
shareDataItems.push(itemShareData.shareDataItems);
} else {
foldersIds.push(item.id);
folderId.push(item.id);
}
}
}
getShareUsersAndGroups(foldersIds, filesIds)
.then((res) => {
//console.log("Response", res);
return [shareDataItems, folderId, fileId];
};
const shareDataResult = this.getShareDataItems(
res,
shareDataItems,
foldersIds,
filesIds
);
shareDataItems = shareDataResult[0];
listOwners = shareDataResult[1];
})
.then(() => {
const rights = {
icon: "CatalogQuestionIcon",
rights: "Varies",
isOwner: false,
};
getShareData() {
const returnValue = this.getData();
let shareDataItems = returnValue[0];
const folderId = returnValue[1];
const fileId = returnValue[2];
shareDataItems = shareDataItems.filter(
(x) => x !== undefined && x.length !== 0
);
shareDataItems = this.setDuplicateItemsRights(shareDataItems, rights);
shareDataItems = this.removeDuplicateShareData(shareDataItems);
shareDataItems = this.setOwnersRights(
listOwners,
shareDataItems,
rights
);
if (this.props.isRecycleBinFolder) {
return;
}
const clearShareData = JSON.parse(JSON.stringify(shareDataItems));
this.props.setShareDataItems(shareDataItems.slice(0));
this.setState({ baseShareData: clearShareData });
});
if (folderId.length !== 0) {
files
.getShareFolders(folderId)
.then((res) => {
shareDataItems = this.getShareDataItems(
res,
shareDataItems,
folderId,
null
);
})
.catch((err) => {
const newShareDataItem = [
{ folderId: folderId[0], shareDataItems: [] },
];
this.props.setShareData([
...newShareDataItem,
...this.props.shareData,
]);
console.log("getShareFolders", err);
return;
});
} else if (fileId.length !== 0) {
files
.getShareFiles(fileId)
.then((res) => {
shareDataItems = this.getShareDataItems(
res,
shareDataItems,
null,
fileId
);
})
.catch((err) => {
const newShareDataItem = [{ fileId: fileId[0], shareDataItems: [] }];
this.props.setShareData([
...newShareDataItem,
...this.props.shareData,
]);
console.log("getShareFiles", err);
return;
});
} else {
shareDataItems = this.getShareDataItems([], shareDataItems, null, fileId);
}
}
onClose = () => {
@ -485,8 +507,11 @@ class SharingPanelComponent extends React.Component {
this.props.onClose();
};
onShowEmbeddingPanel = link =>
this.setState({ showEmbeddingPanel: !this.state.showEmbeddingPanel, shareLink: link });
onShowEmbeddingPanel = (link) =>
this.setState({
showEmbeddingPanel: !this.state.showEmbeddingPanel,
shareLink: link,
});
onShowGroupsPanel = () =>
this.setState({ showAddGroupsPanel: !this.state.showAddGroupsPanel });
@ -515,7 +540,7 @@ class SharingPanelComponent extends React.Component {
render() {
//console.log("Sharing panel render");
const { visible, t, accessOptions, isMe, selection } = this.props;
const { visible, t, accessOptions, isMyId, selection } = this.props;
const {
showActionPanel,
isNotifyUsers,
@ -525,7 +550,7 @@ class SharingPanelComponent extends React.Component {
showAddGroupsPanel,
showEmbeddingPanel,
accessRight,
shareLink
shareLink,
} = this.state;
const zIndex = 310;
@ -636,11 +661,11 @@ class SharingPanelComponent extends React.Component {
</DropDown>
</div>
<IconButton
{/*<IconButton
size="16"
iconName="KeyIcon"
onClick={this.onKeyClick}
/>
/>*/}
</div>
</StyledSharingHeaderContent>
<StyledSharingBody>
@ -651,7 +676,7 @@ class SharingPanelComponent extends React.Component {
selection={selection}
item={item}
index={index}
isMe={isMe}
isMyId={isMyId}
accessOptions={accessOptions}
onFullAccessClick={this.onFullAccessItemClick}
onReadOnlyClick={this.onReadOnlyItemClick}
@ -723,7 +748,7 @@ class SharingPanelComponent extends React.Component {
SharingPanelComponent.propTypes = {
onClose: PropTypes.func,
visible: PropTypes.bool
visible: PropTypes.bool,
};
const SharingPanelContainerTranslated = withTranslation()(
@ -735,9 +760,26 @@ const SharingPanel = (props) => (
);
const mapStateToProps = (state) => {
const { shareDataItems, shareData, selection } = state.files;
const {
shareDataItems,
shareData,
selection,
selectedFolder,
treeFolders,
} = state.files;
const indexOfTrash = 3;
return { shareDataItems, shareData, selection, isMe: state.auth.user.id };
return {
shareDataItems,
shareData,
selection,
isMyId: state.auth.user.id,
isRecycleBinFolder: checkFolderType(
selectedFolder.id,
indexOfTrash,
treeFolders
),
};
};
export default connect(mapStateToProps, { setShareDataItems, setShareData })(

View File

@ -17,7 +17,7 @@ const SharingRow = (props) => {
selection,
item,
index,
isMe,
isMyId,
accessOptions,
onFullAccessClick,
onReadOnlyClick,
@ -28,17 +28,69 @@ const SharingRow = (props) => {
onRemoveUserClick,
onShowEmbeddingPanel,
} = props;
const linkVisible = selection.length === 1 && item.shareLink;
const onCopyInternalLink = (link) => {
const internalLink = link.split("&");
const onCopyInternalLink = () => {
const internalLink = item.shareLink.split("&");
copy(`${internalLink[0]}&${internalLink[1]}`);
toastr.success(t("LinkCopySuccess"));
};
const embeddedComponentRender = (accessOptions, item) => (
const advancedOptionsRender = () => (
<>
{accessOptions.includes("FullAccess") && (
<DropDownItem
label="Full access"
icon="AccessEditIcon"
onClick={() => onFullAccessClick(item)}
/>
)}
{accessOptions.includes("ReadOnly") && (
<DropDownItem
label="Read only"
icon="EyeIcon"
onClick={() => onReadOnlyClick(item)}
/>
)}
{accessOptions.includes("Review") && (
<DropDownItem
label="Review"
icon="AccessReviewIcon"
onClick={() => onReviewClick(item)}
/>
)}
{accessOptions.includes("Comment") && (
<DropDownItem
label="Comment"
icon="AccessCommentIcon"
onClick={() => onCommentClick(item)}
/>
)}
{accessOptions.includes("FormFilling") && (
<DropDownItem
label="Form filling"
icon="AccessFormIcon"
onClick={() => onFormFillingClick(item)}
/>
)}
{accessOptions.includes("DenyAccess") && (
<DropDownItem
label="Deny access"
icon="AccessNoneIcon"
onClick={() => onDenyAccessClick(item)}
/>
)}
</>
);
const embeddedComponentRender = () => (
<ComboBox
advancedOptions={advancedOptionsRender(accessOptions, item)}
advancedOptions={advancedOptionsRender()}
options={[]}
selectedOption={{ key: 0 }}
size="content"
@ -54,57 +106,6 @@ const SharingRow = (props) => {
</ComboBox>
);
const advancedOptionsRender = (accessOptions, item) => (
<>
{accessOptions.includes("FullAccess") && (
<DropDownItem
label="Full access"
icon="AccessEditIcon"
onClick={onFullAccessClick.bind(this, item)}
/>
)}
{accessOptions.includes("ReadOnly") && (
<DropDownItem
label="Read only"
icon="EyeIcon"
onClick={onReadOnlyClick.bind(this, item)}
/>
)}
{accessOptions.includes("Review") && (
<DropDownItem
label="Review"
icon="AccessReviewIcon"
onClick={onReviewClick.bind(this, item)}
/>
)}
{accessOptions.includes("Comment") && (
<DropDownItem
label="Comment"
icon="AccessCommentIcon"
onClick={onCommentClick.bind(this, item)}
/>
)}
{accessOptions.includes("FormFilling") && (
<DropDownItem
label="Form filling"
icon="AccessFormIcon"
onClick={onFormFillingClick.bind(this, item)}
/>
)}
{accessOptions.includes("DenyAccess") && (
<DropDownItem
label="Deny access"
icon="AccessNoneIcon"
onClick={onDenyAccessClick.bind(this, item)}
/>
)}
</>
);
const options = [
{
key: 1,
@ -153,8 +154,8 @@ const SharingRow = (props) => {
const internalLinkData = [
{
key: "linkItem",
label: "Copy internal link",
onClick: onCopyInternalLink.bind(this, item.shareLink),
label: t("CopyInternalLink"),
onClick: onCopyInternalLink,
},
];
@ -199,44 +200,56 @@ const SharingRow = (props) => {
},
];
const linksFunction = (linkText, data) => (
<Row
key={`${linkText}-key_${index}`}
//element={embeddedComponentRender(accessOptions, item)}
element={
<Icons.AccessEditIcon
size="medium"
className="sharing_panel-owner-icon"
/>
}
contextButtonSpacerWidth="0px"
>
<>
<LinkWithDropdown
className="sharing_panel-link"
color="black"
dropdownType="alwaysDashed"
data={data}
>
{t(linkText)}
</LinkWithDropdown>
{/*
<ComboBox
className="sharing_panel-link-combo-box"
options={options}
isDisabled={false}
selectedOption={options[0]}
dropDownMaxHeight={200}
noBorder={false}
scaled={false}
scaledOptions
size="content"
onSelect={(option) => console.log("selected", option)}
/>
*/
}
</>
</Row>
);
//console.log("SharingRow render");
return (
<>
{linkVisible && (
<Row
key={`external-link-key_${index}`}
element={embeddedComponentRender(accessOptions, item)}
contextButtonSpacerWidth="0px"
>
<>
<LinkWithDropdown
className="sharing_panel-link"
color="black"
dropdownType="alwaysDashed"
data={externalLinkData}
>
{t("ExternalLink")}
</LinkWithDropdown>
<ComboBox
className="sharing_panel-link-combo-box"
options={options}
isDisabled={false}
selectedOption={options[0]}
dropDownMaxHeight={200}
noBorder={false}
scaled={false}
scaledOptions={true}
size="content"
onSelect={(option) => console.log("selected", option)}
/>
</>
</Row>
)}
{(linkVisible || !item.shareLink) && (
{linkVisible && linksFunction("ExternalLink", externalLinkData)}
{linkVisible && linksFunction("InternalLink", internalLinkData)}
{!item.shareLink && (
<Row
key={`internal-link-key_${index}`}
element={
item.rights.isOwner || item.id === isMe ? (
item.rights.isOwner || item.id === isMyId ? (
<Icons.AccessEditIcon
size="medium"
className="sharing_panel-owner-icon"
@ -248,27 +261,15 @@ const SharingRow = (props) => {
contextButtonSpacerWidth="0px"
>
<>
{item.shareLink ? (
<LinkWithDropdown
className="sharing_panel-link"
color="black"
dropdownType="alwaysDashed"
data={internalLinkData}
>
{t("InternalLink")}
</LinkWithDropdown>
) : (
!item.shareLink && (
<Text truncate className="sharing_panel-text">
{item.label
? item.label
: item.name
? item.name
: item.displayName}
</Text>
)
{!item.shareLink && (
<Text truncate className="sharing_panel-text">
{item.label
? item.label
: item.name
? item.name
: item.displayName}
</Text>
)}
{item.rights.isOwner ? (
<Text
className="sharing_panel-remove-icon"
@ -276,31 +277,18 @@ const SharingRow = (props) => {
>
{t("Owner")}
</Text>
) : item.id === isMe ? (
) : item.id === isMyId ? (
<Text
className="sharing_panel-remove-icon"
//color="#A3A9AE"
>
{t("AccessRightsFullAccess")}
</Text>
) : item.shareLink ? (
<ComboBox
className="sharing_panel-link-combo-box"
options={options}
isDisabled={false}
selectedOption={options[0]}
dropDownMaxHeight={200}
noBorder={false}
scaled={false}
scaledOptions={true}
size="content"
onSelect={(option) => console.log("selected", option)}
/>
) : (
!item.shareLink && (
<IconButton
iconName="RemoveIcon"
onClick={onRemoveUserClick.bind(this, item)}
onClick={() => onRemoveUserClick(item)}
className="sharing_panel-remove-icon"
/>
)
@ -312,4 +300,4 @@ const SharingRow = (props) => {
);
};
export default React.memo(SharingRow);
export default SharingRow;

View File

@ -10,6 +10,7 @@
"Notify users": "Notify users",
"CopyExternalLink": "Copy external link",
"CopyInternalLink": "Copy internal link",
"ShareVia": "Share via",
"Embedding": "Embedding",
"ExternalLink": "External link",

View File

@ -10,6 +10,7 @@
"Notify users": "Уведомить пользователей",
"CopyExternalLink": "Скопировать внешнюю ссылку",
"CopyInternalLink": "Скопировать внутреннюю ссылку",
"ShareVia": "Отправить по",
"Embedding": "Встраивание",
"ExternalLink": "Внешняя ссылка",

View File

@ -36,7 +36,7 @@ if (token) {
}
}
else {
return Promise.resolve(FilesFilter.getDefault());
return Promise.resolve();
}
})
.then(filter => {
@ -64,6 +64,8 @@ if (token) {
}
})
.then(data => {
if (!data) return Promise.resolve();
if (data instanceof FilesFilter) return Promise.resolve(data);
const { filter, itemId, type } = data;
const newFilter = filter ? filter.clone() : FilesFilter.getDefault();
@ -78,6 +80,8 @@ if (token) {
})
.catch(err => Promise.resolve(FilesFilter.getDefault()))
.then(data => {
if (!data) return Promise.resolve();
if (data instanceof FilesFilter) return Promise.resolve(data);
const result = data[0];
const filter = data[1];
@ -91,6 +95,8 @@ if (token) {
return Promise.resolve(filter);
})
.then(filter => {
if (!filter) return Promise.resolve();
const folderId = filter.folder;
return fetchFiles(folderId, filter, store.dispatch);
})

View File

@ -322,20 +322,6 @@ export function deleteFolder(folderId, deleteAfter, immediately) {
}
}
export function getShareUsersAndGroups(foldersIds, filesIds) {
const getFoldersRequests = foldersIds.map(id =>
files.getShareFolders(id).catch(() => {
return [];
})
);
const getFilesRequests = filesIds.map(id =>
files.getShareFiles(id).catch(() => [])
);
const requests = [...getFoldersRequests, ...getFilesRequests];
return axios.all(requests).then(res => res);
}
export function setSharedFolders(folderIds, shareTo, access, notify, sharingMessage) {
const requests = folderIds.map((id) =>
files.setShareFolder(id, shareTo, access, notify, sharingMessage)

View File

@ -158,6 +158,75 @@ export const getTreeFolders = (pathParts, filterData) => {
return treeFolders;
};
const renameTreeFolder = (folders, newItems, currentFolder) => {
const newItem = folders.find(x => x.id === currentFolder.id);
const oldItemIndex = newItems.folders.findIndex(x => x.id === currentFolder.id);
newItem.folders = newItems.folders[oldItemIndex].folders;
newItems.folders[oldItemIndex] = newItem;
return;
}
const removeTreeFolder = (folders, newItems, foldersCount) => {
for (let folder of newItems.folders) {
let currentFolder;
if(folders) {
currentFolder = folders.find((x) => x.id === folder.id);
}
if (!currentFolder) {
const arrayFolders = newItems.folders.filter(x => x.id !== folder.id);
newItems.folders = arrayFolders;
newItems.foldersCount = foldersCount;
return;
}
}
}
const addTreeFolder = (folders, newItems, foldersCount) => {
let array;
const newItemFolders = newItems.folders ? newItems.folders : [];
for (let folder of folders) {
let currentFolder;
if(newItemFolders) {
currentFolder = newItemFolders.find((x) => x.id === folder.id);
}
if (folders.length < 1 || !currentFolder) {
array = [...newItemFolders, ...[folder]].sort((prev, next) =>
prev.title.toLowerCase() < next.title.toLowerCase() ? -1 : 1
);
newItems.folders = array;
newItems.foldersCount = foldersCount;
return;
}
}
}
export const loopTreeFolders = (path, item, folders, foldersCount, currentFolder) => {
const newPath = path;
while (path.length !== 0) {
const newItems = item.find((x) => x.id === path[0]);
if(!newItems) { return; }
newPath.shift();
if (path.length === 0) {
let foldersLength = newItems.folders ? newItems.folders.length : 0;
//new Date(prev.updated) > new Date(next.updated) ? -1 : 1;
if (folders.length > foldersLength) {
addTreeFolder(folders, newItems, foldersCount);
} else if (folders.length < foldersLength) {
removeTreeFolder(folders, newItems, foldersCount);
} else if (folders.length > 0 && newItems.length > 0) {
renameTreeFolder(folders, newItems, currentFolder);
} else {
return;
}
return;
}
loopTreeFolders(newPath, newItems.folders, folders, foldersCount, currentFolder);
}
}
export const isCanCreate = (selectedFolder, user) => {
if (!selectedFolder || !selectedFolder.id) return;
@ -318,4 +387,21 @@ export const getFileIcon = (extension, size = 32) => {
default:
return `${folderPath}/file.svg`;
}
}
export const checkFolderType = (id, index, treeFolders) => {
return treeFolders[index].id === id;
}
export const getFolderType = (id, treeFolders) => {
const indexOfMy = 0;
const indexOfShare = 1;
const indexOfCommon = 2;
const indexOfTrash = 3;
if(checkFolderType(id, indexOfMy, treeFolders)) { return "My"; }
else if(checkFolderType(id, indexOfShare, treeFolders)) { return "Share"; }
else if(checkFolderType(id, indexOfCommon, treeFolders)) { return "Common"; }
else if(checkFolderType(id, indexOfTrash, treeFolders)) { return "Trash"; }
}

View File

@ -546,15 +546,15 @@ namespace ASC.Api.Documents
/// ]]>
/// </returns>
[Create("{folderId}/upload/create_session", DisableFormat = true)]
public object CreateUploadSession(string folderId, string fileName, long fileSize, string relativePath, bool encrypted)
public object CreateUploadSession(string folderId, SessionModel sessionModel)
{
return FilesControllerHelperString.CreateUploadSession(folderId, fileName, fileSize, relativePath, encrypted);
return FilesControllerHelperString.CreateUploadSession(folderId, sessionModel.FileName, sessionModel.FileSize, sessionModel.RelativePath, sessionModel.Encrypted);
}
[Create("{folderId:int}/upload/create_session")]
public object CreateUploadSession(int folderId, string fileName, long fileSize, string relativePath, bool encrypted)
public object CreateUploadSession(int folderId, SessionModel sessionModel)
{
return FilesControllerHelperInt.CreateUploadSession(folderId, fileName, fileSize, relativePath, encrypted);
return FilesControllerHelperInt.CreateUploadSession(folderId, sessionModel.FileName, sessionModel.FileSize, sessionModel.RelativePath, sessionModel.Encrypted);
}
/// <summary>

View File

@ -630,7 +630,11 @@ namespace ASC.Files.Core.Data
if (fileId == default) return;
using var tx = FilesDbContext.Database.BeginTransaction();
var fromFolders = Query(FilesDbContext.Files).Where(r => r.Id == fileId).GroupBy(r => r.Id).SelectMany(r => r.Select(a => a.FolderId)).Distinct().ToList();
var fromFolders = Query(FilesDbContext.Files)
.Where(r => r.Id == fileId)
.Select(a => a.FolderId)
.Distinct()
.ToList();
var toDeleteFiles = Query(FilesDbContext.Files).Where(r => r.Id == fileId);
FilesDbContext.RemoveRange(toDeleteFiles);

View File

@ -46,6 +46,7 @@ using ASC.Web.Studio.Core;
using ASC.Web.Studio.UserControls.Statistics;
using ASC.Web.Studio.Utility;
using Microsoft.EntityFrameworkCore.Storage;
using Microsoft.Extensions.DependencyInjection;
namespace ASC.Files.Core.Data
@ -285,6 +286,11 @@ namespace ASC.Files.Core.Data
}
public int SaveFolder(Folder<int> folder)
{
return SaveFolder(folder, null);
}
public int SaveFolder(Folder<int> folder, IDbContextTransaction transaction)
{
if (folder == null) throw new ArgumentNullException("folder");
@ -298,72 +304,75 @@ namespace ASC.Files.Core.Data
var isnew = false;
using (var tx = FilesDbContext.Database.BeginTransaction())
var tx = transaction ?? FilesDbContext.Database.BeginTransaction();
if (folder.ID != default && IsExist(folder.ID))
{
if (folder.ID != default && IsExist(folder.ID))
{
var toUpdate = Query(FilesDbContext.Folders)
.Where(r => r.Id == folder.ID)
.FirstOrDefault();
var toUpdate = Query(FilesDbContext.Folders)
.Where(r => r.Id == folder.ID)
.FirstOrDefault();
toUpdate.Title = folder.Title;
toUpdate.CreateBy = folder.CreateBy;
toUpdate.ModifiedOn = TenantUtil.DateTimeToUtc(folder.ModifiedOn);
toUpdate.ModifiedBy = folder.ModifiedBy;
toUpdate.Title = folder.Title;
toUpdate.CreateBy = folder.CreateBy;
toUpdate.ModifiedOn = TenantUtil.DateTimeToUtc(folder.ModifiedOn);
toUpdate.ModifiedBy = folder.ModifiedBy;
FilesDbContext.SaveChanges();
}
else
FilesDbContext.SaveChanges();
}
else
{
isnew = true;
var newFolder = new DbFolder
{
isnew = true;
var newFolder = new DbFolder
Id = 0,
ParentId = folder.ParentFolderID,
Title = folder.Title,
CreateOn = TenantUtil.DateTimeToUtc(folder.CreateOn),
CreateBy = folder.CreateBy,
ModifiedOn = TenantUtil.DateTimeToUtc(folder.ModifiedOn),
ModifiedBy = folder.ModifiedBy,
FolderType = folder.FolderType,
TenantId = TenantID
};
newFolder = FilesDbContext.Folders.Add(newFolder).Entity;
FilesDbContext.SaveChanges();
folder.ID = newFolder.Id;
//itself link
var newTree = new DbFolderTree
{
FolderId = folder.ID,
ParentId = folder.ID,
Level = 0
};
FilesDbContext.Tree.Add(newTree);
FilesDbContext.SaveChanges();
//full path to root
var oldTree = FilesDbContext.Tree
.Where(r => r.FolderId == (int)folder.ParentFolderID);
foreach (var o in oldTree)
{
var treeToAdd = new DbFolderTree
{
Id = 0,
ParentId = folder.ParentFolderID,
Title = folder.Title,
CreateOn = TenantUtil.DateTimeToUtc(folder.CreateOn),
CreateBy = folder.CreateBy,
ModifiedOn = TenantUtil.DateTimeToUtc(folder.ModifiedOn),
ModifiedBy = folder.ModifiedBy,
FolderType = folder.FolderType,
TenantId = TenantID
FolderId = (int)folder.ID,
ParentId = o.ParentId,
Level = o.Level + 1
};
newFolder = FilesDbContext.Folders.Add(newFolder).Entity;
FilesDbContext.SaveChanges();
folder.ID = newFolder.Id;
//itself link
var newTree = new DbFolderTree
{
FolderId = folder.ID,
ParentId = folder.ID,
Level = 0
};
FilesDbContext.Tree.Add(newTree);
FilesDbContext.SaveChanges();
//full path to root
var oldTree = FilesDbContext.Tree
.Where(r => r.FolderId == (int)folder.ParentFolderID);
foreach (var o in oldTree)
{
var treeToAdd = new DbFolderTree
{
FolderId = (int)folder.ID,
ParentId = o.ParentId,
Level = o.Level + 1
};
FilesDbContext.Tree.Add(treeToAdd);
}
FilesDbContext.SaveChanges();
FilesDbContext.Tree.Add(treeToAdd);
}
FilesDbContext.SaveChanges();
}
if (transaction == null)
{
tx.Commit();
tx.Dispose();
}
if (isnew)
@ -406,7 +415,7 @@ namespace ASC.Files.Core.Data
var treeToDelete = FilesDbContext.Tree.Where(r => subfolders.Any(a => r.FolderId == a));
FilesDbContext.Tree.RemoveRange(treeToDelete);
var subfoldersStrings = subfolders.Select(r => r.ToString()).ToList();
var linkToDelete = Query(FilesDbContext.TagLink)
.Where(r => subfoldersStrings.Any(a => r.EntryId == a))
@ -835,7 +844,7 @@ namespace ASC.Files.Core.Data
}
using var tx = FilesDbContext.Database.BeginTransaction();//NOTE: Maybe we shouldn't start transaction here at all
newFolderId = SaveFolder(folder); //Save using our db manager
newFolderId = SaveFolder(folder, tx); //Save using our db manager
var newBunch = new DbFilesBunchObjects
{
@ -905,7 +914,7 @@ namespace ASC.Files.Core.Data
break;
}
using var tx = FilesDbContext.Database.BeginTransaction(); //NOTE: Maybe we shouldn't start transaction here at all
newFolderId = SaveFolder(folder); //Save using our db manager
newFolderId = SaveFolder(folder, tx); //Save using our db manager
var toInsert = new DbFilesBunchObjects
{
LeftNode = newFolderId.ToString(),

View File

@ -416,7 +416,7 @@ namespace ASC.Files.Core.Data
public IEnumerable<Tag> GetNewTags(Guid subject, Folder<T> parentFolder, bool deepSearch)
{
if (parentFolder == null || parentFolder.ID.Equals(default))
if (parentFolder == null || parentFolder.ID.Equals(default(T)))
throw new ArgumentException("folderId");
var result = new List<Tag>();

View File

@ -215,9 +215,15 @@ namespace ASC.Files.Core
}
public object NativeAccessor { get; set; }
public FilesLinkUtility FilesLinkUtility { get; }
public FileUtility FileUtility { get; }
public FileConverter FileConverter { get; }
[NonSerialized]
private readonly FilesLinkUtility FilesLinkUtility;
[NonSerialized]
private readonly FileUtility FileUtility;
[NonSerialized]
private readonly FileConverter FileConverter;
private T _folderIdDisplay;
@ -235,10 +241,8 @@ namespace ASC.Files.Core
public static string Serialize(File<T> file)
{
using (var ms = new FileEntrySerializer().ToXml(file))
{
return Encoding.UTF8.GetString(ms.ToArray());
}
using var ms = new FileEntrySerializer().ToXml(file);
return Encoding.UTF8.GetString(ms.ToArray());
}
}
}

View File

@ -32,6 +32,8 @@ using ASC.Web.Files.Classes;
namespace ASC.Files.Core
{
[DataContract(Name = "entry", Namespace = "")]
[Serializable]
public abstract class FileEntry : ICloneable
{
public FileEntry(Global global)
@ -107,7 +109,8 @@ namespace ASC.Files.Core
public FileEntryType FileEntryType;
public Global Global { get; }
[NonSerialized]
protected Global Global;
private string _modifiedByString;
private string _createByString;

View File

@ -474,7 +474,7 @@ namespace ASC.Web.Files.Services.WCFService
ErrorIf(file == null, FilesCommonResource.ErrorMassage_FileNotFound);
ErrorIf(!FileSecurity.CanRead(file), FilesCommonResource.ErrorMassage_SecurityException_ReadFile);
var parent = folderDao.GetFolder(parentId == null || parentId.Equals(default) ? file.FolderID : parentId);
var parent = folderDao.GetFolder(parentId == null || parentId.Equals(default(T)) ? file.FolderID : parentId);
ErrorIf(parent == null, FilesCommonResource.ErrorMassage_FolderNotFound);
ErrorIf(parent.RootFolderType == FolderType.TRASH, FilesCommonResource.ErrorMassage_ViewTrashItem);

View File

@ -73,8 +73,7 @@ namespace ASC.Web.Files.Helpers
public TokenHelper TokenHelper { get; }
public AuthContext AuthContext { get; }
public ConsumerFactory ConsumerFactory { get; }
public DocuSignLoginProvider DocuSignLoginProvider { get; }
public ConsumerFactory ConsumerFactory { get; }
public DocuSignToken(
TokenHelper tokenHelper,
@ -154,7 +153,6 @@ namespace ASC.Web.Files.Helpers
public static int MaxEmailLength = 10000;
public DocuSignToken DocuSignToken { get; }
public DocuSignLoginProvider DocuSignLoginProvider { get; }
public FileSecurity FileSecurity { get; }
public IDaoFactory DaoFactory { get; }
public BaseCommonLinkUtility BaseCommonLinkUtility { get; }
@ -170,7 +168,6 @@ namespace ASC.Web.Files.Helpers
public DocuSignHelper(
DocuSignToken docuSignToken,
DocuSignLoginProvider docuSignLoginProvider,
FileSecurity fileSecurity,
IDaoFactory daoFactory,
IOptionsMonitor<ILog> options,
@ -186,7 +183,6 @@ namespace ASC.Web.Files.Helpers
ConsumerFactory consumerFactory)
{
DocuSignToken = docuSignToken;
DocuSignLoginProvider = docuSignLoginProvider;
FileSecurity = fileSecurity;
DaoFactory = daoFactory;
BaseCommonLinkUtility = baseCommonLinkUtility;
@ -319,7 +315,7 @@ namespace ASC.Web.Files.Helpers
// new RecipientEvent {RecipientEventStatusCode = "AuthenticationFailed"},
// new RecipientEvent {RecipientEventStatusCode = "AutoResponded"},
// },
Url = BaseCommonLinkUtility.GetFullAbsolutePath(DocuSignHandler.Path(FilesLinkUtility) + "?" + FilesLinkUtility.Action + "=webhook"),
Url = BaseCommonLinkUtility.GetFullAbsolutePath(DocuSignHandlerService.Path(FilesLinkUtility) + "?" + FilesLinkUtility.Action + "=webhook"),
};
Log.Debug("DocuSign hook url: " + eventNotification.Url);
@ -371,7 +367,7 @@ namespace ASC.Web.Files.Helpers
var envelopeId = envelopeSummary.EnvelopeId;
var url = envelopesApi.CreateSenderView(accountId, envelopeId, new ReturnUrlRequest
{
ReturnUrl = BaseCommonLinkUtility.GetFullAbsolutePath(DocuSignHandler.Path(FilesLinkUtility) + "?" + FilesLinkUtility.Action + "=redirect")
ReturnUrl = BaseCommonLinkUtility.GetFullAbsolutePath(DocuSignHandlerService.Path(FilesLinkUtility) + "?" + FilesLinkUtility.Action + "=redirect")
});
Log.Debug("DocuSign senderView: " + url.Url);

View File

@ -251,13 +251,13 @@ namespace ASC.Files.Helpers
if (FilesLinkUtility.IsLocalFileUploader)
{
var session = FileUploader.InitiateUpload(file.FolderID.ToString(), (file.ID ?? default).ToString(), file.Title, file.ContentLength, encrypted);
var session = FileUploader.InitiateUpload(file.FolderID, (file.ID ?? default), file.Title, file.ContentLength, encrypted);
var response = ChunkedUploadSessionHelper.ToResponseObject(session, true);
var responseObject = ChunkedUploadSessionHelper.ToResponseObject(session, true);
return new
{
success = true,
data = response
data = responseObject
};
}
@ -279,11 +279,10 @@ namespace ASC.Files.Helpers
ServicePointManager.ServerCertificateValidationCallback += (s, ce, ca, p) => true;
}
using (var response = request.GetResponse())
using (var responseStream = response.GetResponseStream())
{
return JObject.Parse(new StreamReader(responseStream).ReadToEnd()); //result is json string
}
using var response = request.GetResponse();
using var responseStream = response.GetResponseStream();
using var streamReader = new StreamReader(responseStream);
return JObject.Parse(streamReader.ReadToEnd()); //result is json string
}
public FileWrapper<T> CreateTextFile(T folderId, string title, string content)

View File

@ -64,7 +64,7 @@ namespace ASC.Web.Files.Helpers
public IEnumerable<string> ThirdPartyProviders
{
get { return (Configuration["files:thirdparty:enable"] ?? "").Split(new char[] { '|', ',' }, StringSplitOptions.RemoveEmptyEntries); }
get { return (Configuration.GetSection("files:thirdparty:enable").Get<string[]>() ?? new string[] { }).ToList(); }
}
public bool SupportInclusion

View File

@ -45,16 +45,37 @@ using ASC.Web.Files.Helpers;
using ASC.Web.Files.Utils;
using ASC.Web.Studio.Core;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Newtonsoft.Json;
namespace ASC.Web.Files.HttpHandlers
{
public class ChunkedUploaderHandler //: AbstractHttpAsyncHandler
public class ChunkedUploaderHandler
{
public RequestDelegate Next { get; }
public IServiceProvider ServiceProvider { get; }
public ChunkedUploaderHandler(RequestDelegate next, IServiceProvider serviceProvider)
{
Next = next;
ServiceProvider = serviceProvider;
}
public async Task Invoke(HttpContext context)
{
using var scope = ServiceProvider.CreateScope();
var chunkedUploaderHandlerService = scope.ServiceProvider.GetService<ChunkedUploaderHandlerService>();
await chunkedUploaderHandlerService.Invoke(context);
await Next.Invoke(context);
}
}
public class ChunkedUploaderHandlerService
{
public TenantManager TenantManager { get; }
public FileUploader FileUploader { get; }
public FilesMessageService FilesMessageService { get; }
@ -67,8 +88,7 @@ namespace ASC.Web.Files.HttpHandlers
public ChunkedUploadSessionHelper ChunkedUploadSessionHelper { get; }
public ILog Logger { get; }
public ChunkedUploaderHandler(
RequestDelegate next,
public ChunkedUploaderHandlerService(
IOptionsMonitor<ILog> optionsMonitor,
TenantManager tenantManager,
FileUploader fileUploader,
@ -81,7 +101,6 @@ namespace ASC.Web.Files.HttpHandlers
ChunkedUploadSessionHolder chunkedUploadSessionHolder,
ChunkedUploadSessionHelper chunkedUploadSessionHelper)
{
Next = next;
TenantManager = tenantManager;
FileUploader = fileUploader;
FilesMessageService = filesMessageService;
@ -96,69 +115,80 @@ namespace ASC.Web.Files.HttpHandlers
}
public async Task Invoke(HttpContext context)
{
var uploadSession = ChunkedUploadSessionHolder.GetSession(context.Request.Query["uid"]);
if (uploadSession as ChunkedUploadSession<int> != null)
{
await Invoke<int>(context);
return;
}
await Invoke<string>(context);
}
public async Task Invoke<T>(HttpContext context)
{
try
{
var request = new ChunkedRequestHelper(context.Request);
var request = new ChunkedRequestHelper<T>(context.Request);
if (!TryAuthorize(request))
{
WriteError(context, "Can't authorize given initiate session request or session with specified upload id already expired");
await WriteError(context, "Can't authorize given initiate session request or session with specified upload id already expired");
return;
}
if (TenantManager.GetCurrentTenant().Status != TenantStatus.Active)
{
WriteError(context, "Can't perform upload for deleted or transfering portals");
await WriteError(context, "Can't perform upload for deleted or transfering portals");
return;
}
switch (request.Type(InstanceCrypto))
{
case ChunkedRequestType.Abort:
FileUploader.AbortUpload<string>(request.UploadId);
WriteSuccess(context, null);
FileUploader.AbortUpload<T>(request.UploadId);
await WriteSuccess(context, null);
return;
case ChunkedRequestType.Initiate:
var createdSession = FileUploader.InitiateUpload(request.FolderId, request.FileId, request.FileName, request.FileSize, request.Encrypted);
WriteSuccess(context, ChunkedUploadSessionHelper.ToResponseObject(createdSession, true));
await WriteSuccess(context, ChunkedUploadSessionHelper.ToResponseObject(createdSession, true));
return;
case ChunkedRequestType.Upload:
var resumedSession = FileUploader.UploadChunk<string>(request.UploadId, request.ChunkStream, request.ChunkSize);
var resumedSession = FileUploader.UploadChunk<T>(request.UploadId, request.ChunkStream, request.ChunkSize);
if (resumedSession.BytesUploaded == resumedSession.BytesTotal)
{
WriteSuccess(context, ToResponseObject(resumedSession.File), (int)HttpStatusCode.Created);
await WriteSuccess(context, ToResponseObject(resumedSession.File), (int)HttpStatusCode.Created);
FilesMessageService.Send(resumedSession.File, MessageAction.FileUploaded, resumedSession.File.Title);
}
else
{
WriteSuccess(context, ChunkedUploadSessionHelper.ToResponseObject(resumedSession));
await WriteSuccess(context, ChunkedUploadSessionHelper.ToResponseObject(resumedSession));
}
return;
default:
WriteError(context, "Unknown request type.");
await WriteError(context, "Unknown request type.");
return;
}
}
catch (FileNotFoundException error)
{
Logger.Error(error);
WriteError(context, FilesCommonResource.ErrorMassage_FileNotFound);
await WriteError(context, FilesCommonResource.ErrorMassage_FileNotFound);
}
catch (Exception error)
{
Logger.Error(error);
WriteError(context, error.Message);
await WriteError(context, error.Message);
}
await Next.Invoke(context);
}
private bool TryAuthorize(ChunkedRequestHelper request)
private bool TryAuthorize<T>(ChunkedRequestHelper<T> request)
{
if (request.Type(InstanceCrypto) == ChunkedRequestType.Initiate)
{
@ -172,7 +202,7 @@ namespace ASC.Web.Files.HttpHandlers
if (!string.IsNullOrEmpty(request.UploadId))
{
var uploadSession = ChunkedUploadSessionHolder.GetSession<string>(request.UploadId);
var uploadSession = ChunkedUploadSessionHolder.GetSession(request.UploadId);
if (uploadSession != null)
{
TenantManager.SetCurrentTenant(uploadSession.TenantId);
@ -187,21 +217,21 @@ namespace ASC.Web.Files.HttpHandlers
return false;
}
private static void WriteError(HttpContext context, string message)
private static async Task WriteError(HttpContext context, string message)
{
WriteResponse(context, false, null, message, (int)HttpStatusCode.OK);
await WriteResponse(context, false, null, message, (int)HttpStatusCode.OK);
}
private static void WriteSuccess(HttpContext context, object data, int statusCode = (int)HttpStatusCode.OK)
private static async Task WriteSuccess(HttpContext context, object data, int statusCode = (int)HttpStatusCode.OK)
{
WriteResponse(context, true, data, string.Empty, statusCode);
await WriteResponse(context, true, data, string.Empty, statusCode);
}
private static void WriteResponse(HttpContext context, bool success, object data, string message, int statusCode)
private static async Task WriteResponse(HttpContext context, bool success, object data, string message, int statusCode)
{
context.Response.StatusCode = statusCode;
context.Response.WriteAsync(JsonConvert.SerializeObject(new { success, data, message })).Wait();
context.Response.ContentType = "application/json";
await context.Response.WriteAsync(JsonConvert.SerializeObject(new { success, data, message }));
}
private static object ToResponseObject<T>(File<T> file)
@ -216,151 +246,151 @@ namespace ASC.Web.Files.HttpHandlers
uploaded = true
};
}
}
private enum ChunkedRequestType
public enum ChunkedRequestType
{
None,
Initiate,
Abort,
Upload
}
[DebuggerDisplay("{Type} ({UploadId})")]
public class ChunkedRequestHelper<T>
{
private readonly HttpRequest _request;
private IFormFile _file;
private int? _tenantId;
private long? _fileContentLength;
private Guid? _authKey;
private CultureInfo _cultureInfo;
public ChunkedRequestType Type(InstanceCrypto instanceCrypto)
{
None,
Initiate,
Abort,
Upload
if (_request.Query["initiate"] == "true" && IsAuthDataSet(instanceCrypto) && IsFileDataSet())
return ChunkedRequestType.Initiate;
if (_request.Query["abort"] == "true" && !string.IsNullOrEmpty(UploadId))
return ChunkedRequestType.Abort;
return !string.IsNullOrEmpty(UploadId)
? ChunkedRequestType.Upload
: ChunkedRequestType.None;
}
[DebuggerDisplay("{Type} ({UploadId})")]
private class ChunkedRequestHelper
public string UploadId
{
private readonly HttpRequest _request;
private IFormFile _file;
private int? _tenantId;
private long? _fileContentLength;
private Guid? _authKey;
private CultureInfo _cultureInfo;
get { return _request.Query["uid"]; }
}
public ChunkedRequestType Type(InstanceCrypto instanceCrypto)
public int TenantId
{
get
{
if (_request.Query["initiate"] == "true" && IsAuthDataSet(instanceCrypto) && IsFileDataSet())
return ChunkedRequestType.Initiate;
if (_request.Query["abort"] == "true" && !string.IsNullOrEmpty(UploadId))
return ChunkedRequestType.Abort;
return !string.IsNullOrEmpty(UploadId)
? ChunkedRequestType.Upload
: ChunkedRequestType.None;
}
public string UploadId
{
get { return _request.Query["uid"]; }
}
public int TenantId
{
get
if (!_tenantId.HasValue)
{
if (!_tenantId.HasValue)
{
if (int.TryParse(_request.Query["tid"], out var v))
_tenantId = v;
else
_tenantId = -1;
}
return _tenantId.Value;
if (int.TryParse(_request.Query["tid"], out var v))
_tenantId = v;
else
_tenantId = -1;
}
return _tenantId.Value;
}
}
public Guid AuthKey(InstanceCrypto instanceCrypto)
public Guid AuthKey(InstanceCrypto instanceCrypto)
{
if (!_authKey.HasValue)
{
if (!_authKey.HasValue)
_authKey = !string.IsNullOrEmpty(_request.Query["userid"])
? new Guid(instanceCrypto.Decrypt(_request.Query["userid"]))
: Guid.Empty;
}
return _authKey.Value;
}
public T FolderId
{
get { return (T)Convert.ChangeType(_request.Query[FilesLinkUtility.FolderId], typeof(T)); }
}
public T FileId
{
get { return (T)Convert.ChangeType(_request.Query[FilesLinkUtility.FileId], typeof(T)); }
}
public string FileName
{
get { return _request.Query[FilesLinkUtility.FileTitle]; }
}
public long FileSize
{
get
{
if (!_fileContentLength.HasValue)
{
_authKey = !string.IsNullOrEmpty(_request.Query["userid"])
? new Guid(instanceCrypto.Decrypt(_request.Query["userid"]))
: Guid.Empty;
long.TryParse(_request.Query["fileSize"], out var v);
_fileContentLength = v;
}
return _authKey.Value;
return _fileContentLength.Value;
}
}
public string FolderId
public long ChunkSize
{
get { return File.Length; }
}
public Stream ChunkStream
{
get { return File.OpenReadStream(); }
}
public CultureInfo CultureInfo(SetupInfo setupInfo)
{
if (_cultureInfo != null)
return _cultureInfo;
var culture = _request.Query["culture"];
if (string.IsNullOrEmpty(culture)) culture = "en-US";
return _cultureInfo = setupInfo.EnabledCulturesPersonal.Find(c => string.Equals(c.Name, culture, StringComparison.InvariantCultureIgnoreCase));
}
public bool Encrypted
{
get { return _request.Query["encrypted"] == "true"; }
}
private IFormFile File
{
get
{
get { return _request.Query[FilesLinkUtility.FolderId]; }
if (_file != null)
return _file;
if (_request.Form.Files.Count > 0)
return _file = _request.Form.Files[0];
throw new Exception("HttpRequest.Files is empty");
}
}
public string FileId
{
get { return _request.Query[FilesLinkUtility.FileId]; }
}
public ChunkedRequestHelper(HttpRequest request)
{
_request = request ?? throw new ArgumentNullException("request");
}
public string FileName
{
get { return _request.Query[FilesLinkUtility.FileTitle]; }
}
private bool IsAuthDataSet(InstanceCrypto instanceCrypto)
{
return TenantId > -1 && AuthKey(instanceCrypto) != Guid.Empty;
}
public long FileSize
{
get
{
if (!_fileContentLength.HasValue)
{
long.TryParse(_request.Query["fileSize"], out var v);
_fileContentLength = v;
}
return _fileContentLength.Value;
}
}
public long ChunkSize
{
get { return File.Length; }
}
public Stream ChunkStream
{
get { return File.OpenReadStream(); }
}
public CultureInfo CultureInfo(SetupInfo setupInfo)
{
if (_cultureInfo != null)
return _cultureInfo;
var culture = _request.Query["culture"];
if (string.IsNullOrEmpty(culture)) culture = "en-US";
return _cultureInfo = setupInfo.EnabledCulturesPersonal.Find(c => string.Equals(c.Name, culture, StringComparison.InvariantCultureIgnoreCase));
}
public bool Encrypted
{
get { return _request.Query["encrypted"] == "true"; }
}
private IFormFile File
{
get
{
if (_file != null)
return _file;
if (_request.Form.Files.Count > 0)
return _file = _request.Form.Files[0];
throw new Exception("HttpRequest.Files is empty");
}
}
public ChunkedRequestHelper(HttpRequest request)
{
_request = request ?? throw new ArgumentNullException("request");
}
private bool IsAuthDataSet(InstanceCrypto instanceCrypto)
{
return TenantId > -1 && AuthKey(instanceCrypto) != Guid.Empty;
}
private bool IsFileDataSet()
{
return !string.IsNullOrEmpty(FileName) && !string.IsNullOrEmpty(FolderId);
}
private bool IsFileDataSet()
{
return !string.IsNullOrEmpty(FileName) && !FolderId.Equals(default(T));
}
}
@ -368,7 +398,7 @@ namespace ASC.Web.Files.HttpHandlers
{
public static DIHelper AddChunkedUploaderHandlerService(this DIHelper services)
{
services.TryAddScoped<ChunkedUploaderHandler>();
services.TryAddScoped<ChunkedUploaderHandlerService>();
return services
.AddTenantManagerService()
.AddFileUploaderService()
@ -381,5 +411,10 @@ namespace ASC.Web.Files.HttpHandlers
.AddChunkedUploadSessionHolderService()
.AddChunkedUploadSessionHelperService();
}
public static IApplicationBuilder UseChunkedUploaderHandler(this IApplicationBuilder builder)
{
return builder.UseMiddleware<ChunkedUploaderHandler>();
}
}
}

View File

@ -34,10 +34,12 @@ using System.Threading;
using System.Threading.Tasks;
using System.Web;
using ASC.Common;
using ASC.Common.Logging;
using ASC.Common.Web;
using ASC.Core;
using ASC.Files.Core;
using ASC.Files.Core.Data;
using ASC.Files.Core.Security;
using ASC.Files.Resources;
using ASC.MessagingSystem;
@ -56,27 +58,45 @@ using ASC.Web.Studio.Utility;
using JWT;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Newtonsoft.Json.Linq;
using FileShare = ASC.Files.Core.Security.FileShare;
using MimeMapping = ASC.Common.Web.MimeMapping;
using SecurityContext = ASC.Core.SecurityContext;
namespace ASC.Web.Files
{
public class FileHandler //: AbstractHttpAsyncHandler
public class FileHandler
{
public RequestDelegate Next { get; }
public IServiceProvider ServiceProvider { get; }
public FileHandler(RequestDelegate next, IServiceProvider serviceProvider)
{
Next = next;
ServiceProvider = serviceProvider;
}
public async Task Invoke(HttpContext context)
{
using var scope = ServiceProvider.CreateScope();
var fileHandlerService = scope.ServiceProvider.GetService<FileHandlerService>();
await fileHandlerService.Invoke(context);
await Next.Invoke(context);
}
}
public class FileHandlerService
{
public string FileHandlerPath
{
get { return FilesLinkUtility.FileHandlerPath; }
}
public RequestDelegate Next { get; }
public FilesLinkUtility FilesLinkUtility { get; }
public TenantExtra TenantExtra { get; }
public AuthContext AuthContext { get; }
@ -103,8 +123,7 @@ namespace ASC.Web.Files
public CookiesManager CookiesManager { get; }
public TenantStatisticsProvider TenantStatisticsProvider { get; }
public FileHandler(
RequestDelegate next,
public FileHandlerService(
FilesLinkUtility filesLinkUtility,
TenantExtra tenantExtra,
CookiesManager cookiesManager,
@ -130,7 +149,6 @@ namespace ASC.Web.Files
FFmpegService fFmpegService,
IServiceProvider serviceProvider)
{
Next = next;
FilesLinkUtility = filesLinkUtility;
TenantExtra = tenantExtra;
AuthContext = authContext;
@ -172,31 +190,31 @@ namespace ASC.Web.Files
{
case "view":
case "download":
DownloadFile(context);
await DownloadFile(context);
break;
case "bulk":
BulkDownloadFile(context);
await BulkDownloadFile(context);
break;
case "stream":
StreamFile(context);
await StreamFile(context);
break;
case "empty":
EmptyFile(context);
await EmptyFile(context);
break;
case "tmp":
TempFile(context);
await TempFile(context);
break;
case "create":
CreateFile(context);
await CreateFile(context);
break;
case "redirect":
Redirect(context);
break;
case "diff":
DifferenceFile(context);
await DifferenceFile(context);
break;
case "track":
TrackFile(context);
await TrackFile(context);
break;
default:
throw new HttpException((int)HttpStatusCode.BadRequest, FilesCommonResource.ErrorMassage_BadRequest);
@ -207,11 +225,9 @@ namespace ASC.Web.Files
{
throw new HttpException((int)HttpStatusCode.InternalServerError, FilesCommonResource.ErrorMassage_BadRequest, e);
}
await Next.Invoke(context);
}
private void BulkDownloadFile(HttpContext context)
private async Task BulkDownloadFile(HttpContext context)
{
if (!SecurityContext.AuthenticateMe(CookiesManager.GetCookies(CookiesType.AuthKey)))
{
@ -250,10 +266,10 @@ namespace ASC.Web.Files
readStream.Seek(offset, SeekOrigin.Begin);
}
SendStreamByChunks(context, length, FileConstant.DownloadTitle + ".zip", readStream, ref flushed);
flushed = await SendStreamByChunksAsync(context, length, FileConstant.DownloadTitle + ".zip", readStream, flushed);
}
context.Response.Body.Flush();
await context.Response.Body.FlushAsync();
//context.Response.SuppressContent = true;
//context.ApplicationInstance.CompleteRequest();
}
@ -264,21 +280,21 @@ namespace ASC.Web.Files
}
}
private void DownloadFile(HttpContext context)
private async Task DownloadFile(HttpContext context)
{
var q = context.Request.Query[FilesLinkUtility.FileId];
if (int.TryParse(q, out var id))
{
DownloadFile(context, id);
await DownloadFile(context, id);
}
else
{
DownloadFile(context, q);
await DownloadFile(context, q);
}
}
private void DownloadFile<T>(HttpContext context, T id)
private async Task DownloadFile<T>(HttpContext context, T id)
{
var flushed = false;
try
@ -415,7 +431,7 @@ namespace ASC.Web.Files
}
}
SendStreamByChunks(context, length, title, fileStream, ref flushed);
flushed = await SendStreamByChunksAsync(context, length, title, fileStream, flushed);
}
else
{
@ -436,7 +452,7 @@ namespace ASC.Web.Files
fileStream.Seek(offset, SeekOrigin.Begin);
}
SendStreamByChunks(context, length, title, fileStream, ref flushed);
flushed = await SendStreamByChunksAsync(context, length, title, fileStream, flushed);
}
}
catch (ThreadAbortException tae)
@ -453,13 +469,13 @@ namespace ASC.Web.Files
if (fileStream != null)
{
fileStream.Close();
fileStream.Dispose();
await fileStream.DisposeAsync();
}
}
try
{
context.Response.Body.Flush();
await context.Response.Body.FlushAsync();
//context.Response.SuppressContent = true;
//context.ApplicationInstance.CompleteRequest();
flushed = true;
@ -487,7 +503,7 @@ namespace ASC.Web.Files
if (!flushed && !context.RequestAborted.IsCancellationRequested)
{
context.Response.StatusCode = 400;
context.Response.WriteAsync(HttpUtility.HtmlEncode(ex.Message)).Wait();
await context.Response.WriteAsync(HttpUtility.HtmlEncode(ex.Message));
}
}
}
@ -525,7 +541,7 @@ namespace ASC.Web.Files
return length;
}
private void SendStreamByChunks(HttpContext context, long toRead, string title, Stream fileStream, ref bool flushed)
private async Task<bool> SendStreamByChunksAsync(HttpContext context, long toRead, string title, Stream fileStream, bool flushed)
{
//context.Response.Buffer = false;
context.Response.Headers.Add("Connection", "Keep-Alive");
@ -541,8 +557,8 @@ namespace ASC.Web.Files
if (!context.RequestAborted.IsCancellationRequested)
{
context.Response.Body.Write(buffer, 0, length);
context.Response.Body.Flush();
await context.Response.Body.WriteAsync(buffer, 0, length);
await context.Response.Body.FlushAsync();
flushed = true;
toRead -= length;
}
@ -552,23 +568,25 @@ namespace ASC.Web.Files
Logger.Warn(string.Format("IsClientConnected is false. Why? Download file {0} Connection is lost. ", title));
}
}
return flushed;
}
private void StreamFile(HttpContext context)
private async Task StreamFile(HttpContext context)
{
var q = context.Request.Query[FilesLinkUtility.FileId];
if (int.TryParse(q, out var id))
{
StreamFile(context, id);
await StreamFile(context, id);
}
else
{
StreamFile(context, q);
await StreamFile(context, q);
}
}
private void StreamFile<T>(HttpContext context, T id)
private async Task StreamFile<T>(HttpContext context, T id)
{
try
{
@ -593,7 +611,7 @@ namespace ASC.Web.Files
Logger.Error($"{FilesLinkUtility.AuthKey} {validateResult}: {context.Request.Url()}", exc);
context.Response.StatusCode = (int)HttpStatusCode.Forbidden;
context.Response.WriteAsync(FilesCommonResource.ErrorMassage_SecurityException).Wait();
await context.Response.WriteAsync(FilesCommonResource.ErrorMassage_SecurityException);
return;
}
@ -606,9 +624,8 @@ namespace ASC.Web.Files
{
throw new Exception("Invalid header " + header);
}
header = header.Substring("Bearer ".Length);
JsonWebToken.JsonSerializer = new DocumentService.JwtSerializer();
header = header.Substring("Bearer ".Length);
var stringPayload = JsonWebToken.Decode(header, FileUtility.SignatureSecret);
@ -636,7 +653,7 @@ namespace ASC.Web.Files
{
Logger.Error("Download stream header " + context.Request.Url(), ex);
context.Response.StatusCode = (int)HttpStatusCode.Forbidden;
context.Response.WriteAsync(FilesCommonResource.ErrorMassage_SecurityException).Wait();
await context.Response.WriteAsync(FilesCommonResource.ErrorMassage_SecurityException);
return;
}
}
@ -664,7 +681,7 @@ namespace ASC.Web.Files
if (!string.IsNullOrEmpty(file.Error))
{
context.Response.WriteAsync(file.Error).Wait();
await context.Response.WriteAsync(file.Error);
context.Response.StatusCode = (int)HttpStatusCode.BadRequest;
return;
}
@ -672,26 +689,24 @@ namespace ASC.Web.Files
context.Response.Headers.Add("Content-Disposition", ContentDispositionUtil.GetHeaderValue(file.Title));
context.Response.ContentType = MimeMapping.GetMimeMapping(file.Title);
using (var stream = fileDao.GetFileStream(file))
{
context.Response.Headers.Add("Content-Length",
stream.CanSeek
? stream.Length.ToString(CultureInfo.InvariantCulture)
: file.ContentLength.ToString(CultureInfo.InvariantCulture));
stream.StreamCopyTo(context.Response.Body);
}
using var stream = fileDao.GetFileStream(file);
context.Response.Headers.Add("Content-Length",
stream.CanSeek
? stream.Length.ToString(CultureInfo.InvariantCulture)
: file.ContentLength.ToString(CultureInfo.InvariantCulture));
await stream.CopyToAsync(context.Response.Body, StreamExtension.BufferSize);
}
catch (Exception ex)
{
Logger.Error("Error for: " + context.Request.Url(), ex);
context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
context.Response.WriteAsync(ex.Message).Wait();
await context.Response.WriteAsync(ex.Message);
return;
}
try
{
context.Response.Body.Flush();
await context.Response.Body.FlushAsync();
//context.Response.SuppressContent = true;
//context.ApplicationInstance.CompleteRequest();
}
@ -701,7 +716,7 @@ namespace ASC.Web.Files
}
}
private void EmptyFile(HttpContext context)
private async Task EmptyFile(HttpContext context)
{
try
{
@ -715,9 +730,8 @@ namespace ASC.Web.Files
{
throw new Exception("Invalid header " + header);
}
header = header.Substring("Bearer ".Length);
JsonWebToken.JsonSerializer = new DocumentService.JwtSerializer();
header = header.Substring("Bearer ".Length);
var stringPayload = JsonWebToken.Decode(header, FileUtility.SignatureSecret);
@ -745,7 +759,7 @@ namespace ASC.Web.Files
{
Logger.Error("Download stream header " + context.Request.Url(), ex);
context.Response.StatusCode = (int)HttpStatusCode.Forbidden;
context.Response.WriteAsync(FilesCommonResource.ErrorMassage_SecurityException).Wait();
await context.Response.WriteAsync(FilesCommonResource.ErrorMassage_SecurityException);
return;
}
}
@ -761,7 +775,7 @@ namespace ASC.Web.Files
if (!storeTemplate.IsFile("", path))
{
context.Response.StatusCode = (int)HttpStatusCode.NotFound;
context.Response.WriteAsync(FilesCommonResource.ErrorMassage_FileNotFound).Wait();
await context.Response.WriteAsync(FilesCommonResource.ErrorMassage_FileNotFound);
return;
}
@ -779,13 +793,13 @@ namespace ASC.Web.Files
{
Logger.Error("Error for: " + context.Request.Url(), ex);
context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
context.Response.WriteAsync(ex.Message).Wait();
await context.Response.WriteAsync(ex.Message);
return;
}
try
{
context.Response.Body.Flush();
await context.Response.Body.FlushAsync();
//context.Response.SuppressContent = true;
//context.ApplicationInstance.CompleteRequest();
}
@ -795,7 +809,7 @@ namespace ASC.Web.Files
}
}
private void TempFile(HttpContext context)
private async Task TempFile(HttpContext context)
{
var fileName = context.Request.Query[FilesLinkUtility.FileTitle];
var auth = context.Request.Query[FilesLinkUtility.AuthKey].FirstOrDefault();
@ -808,7 +822,7 @@ namespace ASC.Web.Files
Logger.Error($"{FilesLinkUtility.AuthKey} {validateResult}: {context.Request.Url()}", exc);
context.Response.StatusCode = (int)HttpStatusCode.Forbidden;
context.Response.WriteAsync(FilesCommonResource.ErrorMassage_SecurityException);
await context.Response.WriteAsync(FilesCommonResource.ErrorMassage_SecurityException);
return;
}
@ -823,21 +837,21 @@ namespace ASC.Web.Files
if (!store.IsFile(FileConstant.StorageDomainTmp, path))
{
context.Response.StatusCode = (int)HttpStatusCode.NotFound;
context.Response.WriteAsync(FilesCommonResource.ErrorMassage_FileNotFound).Wait();
await context.Response.WriteAsync(FilesCommonResource.ErrorMassage_FileNotFound);
return;
}
using (var readStream = store.GetReadStream(FileConstant.StorageDomainTmp, path))
{
context.Response.Headers.Add("Content-Length", readStream.Length.ToString(CultureInfo.InvariantCulture));
readStream.StreamCopyTo(context.Response.Body);
await readStream.CopyToAsync(context.Response.Body, StreamExtension.BufferSize);
}
store.Delete(FileConstant.StorageDomainTmp, path);
try
{
context.Response.Body.Flush();
await context.Response.Body.FlushAsync();
//context.Response.SuppressContent = true;
//context.ApplicationInstance.CompleteRequest();
}
@ -847,21 +861,21 @@ namespace ASC.Web.Files
}
}
private void DifferenceFile(HttpContext context)
private async Task DifferenceFile(HttpContext context)
{
var q = context.Request.Query[FilesLinkUtility.FileId];
if (int.TryParse(q, out var id))
{
DifferenceFile(context, id);
await DifferenceFile(context, id);
}
else
{
DifferenceFile(context, q);
await DifferenceFile(context, q);
}
}
private void DifferenceFile<T>(HttpContext context, T id)
private async Task DifferenceFile<T>(HttpContext context, T id)
{
try
{
@ -881,7 +895,7 @@ namespace ASC.Web.Files
Logger.Error($"{FilesLinkUtility.AuthKey} {validateResult}: {context.Request.Url()}", exc);
context.Response.StatusCode = (int)HttpStatusCode.Forbidden;
context.Response.WriteAsync(FilesCommonResource.ErrorMassage_SecurityException).Wait();
await context.Response.WriteAsync(FilesCommonResource.ErrorMassage_SecurityException);
return;
}
}
@ -910,7 +924,7 @@ namespace ASC.Web.Files
if (!string.IsNullOrEmpty(file.Error))
{
context.Response.WriteAsync(file.Error).Wait();
await context.Response.WriteAsync(file.Error);
context.Response.StatusCode = (int)HttpStatusCode.BadRequest;
return;
}
@ -921,20 +935,20 @@ namespace ASC.Web.Files
using (var stream = fileDao.GetDifferenceStream(file))
{
context.Response.Headers.Add("Content-Length", stream.Length.ToString(CultureInfo.InvariantCulture));
stream.StreamCopyTo(context.Response.Body);
await stream.CopyToAsync(context.Response.Body, StreamExtension.BufferSize);
}
}
catch (Exception ex)
{
context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
context.Response.WriteAsync(ex.Message).Wait();
await context.Response.WriteAsync(ex.Message);
Logger.Error("Error for: " + context.Request.Url(), ex);
return;
}
try
{
context.Response.Body.Flush();
await context.Response.Body.FlushAsync();
//context.Response.SuppressContent = true;
//context.ApplicationInstance.CompleteRequest();
}
@ -949,20 +963,20 @@ namespace ASC.Web.Files
return file.ID + ":" + file.Version + ":" + file.Title.GetHashCode() + ":" + file.ContentLength;
}
private void CreateFile(HttpContext context)
private async Task CreateFile(HttpContext context)
{
var folderId = context.Request.Query[FilesLinkUtility.FolderId].FirstOrDefault();
if (string.IsNullOrEmpty(folderId))
{
CreateFile(context, GlobalFolderHelper.FolderMy);
await CreateFile(context, GlobalFolderHelper.FolderMy);
}
else
{
CreateFile(context, folderId);
await CreateFile(context, folderId);
}
}
private void CreateFile<T>(HttpContext context, T folderId)
private async Task CreateFile<T>(HttpContext context, T folderId)
{
var responseMessage = context.Request.Query["response"] == "message";
Folder<T> folder;
@ -993,7 +1007,7 @@ namespace ASC.Web.Files
Logger.Error(ex);
if (responseMessage)
{
context.Response.WriteAsync("error: " + ex.Message).Wait();
await context.Response.WriteAsync("error: " + ex.Message);
return;
}
context.Response.Redirect(PathProvider.StartURL + "#error/" + HttpUtility.UrlEncode(ex.Message), true);
@ -1004,7 +1018,7 @@ namespace ASC.Web.Files
if (responseMessage)
{
context.Response.WriteAsync("ok: " + string.Format(FilesCommonResource.MessageFileCreated, folder.Title)).Wait();
await context.Response.WriteAsync("ok: " + string.Format(FilesCommonResource.MessageFileCreated, folder.Title));
return;
}
@ -1134,7 +1148,7 @@ namespace ASC.Web.Files
context.Response.Redirect(urlRedirect);
}
private void TrackFile(HttpContext context)
private async Task TrackFile(HttpContext context)
{
var auth = context.Request.Query[FilesLinkUtility.AuthKey].FirstOrDefault();
var fileId = context.Request.Query[FilesLinkUtility.FileId].FirstOrDefault();
@ -1177,7 +1191,6 @@ namespace ASC.Web.Files
if (!string.IsNullOrEmpty(FileUtility.SignatureSecret))
{
JsonWebToken.JsonSerializer = new DocumentService.JwtSerializer();
if (!string.IsNullOrEmpty(fileData.Token))
{
try
@ -1240,7 +1253,43 @@ namespace ASC.Web.Files
}
result ??= new DocumentServiceTracker.TrackResponse();
context.Response.WriteAsync(DocumentServiceTracker.TrackResponse.Serialize(result)).Wait();
await context.Response.WriteAsync(DocumentServiceTracker.TrackResponse.Serialize(result));
}
}
public static class FileHandlerExtensions
{
public static DIHelper AddFileHandlerService(this DIHelper services)
{
services.TryAddScoped<FileHandlerService>();
return services
.AddFilesLinkUtilityService()
.AddTenantExtraService()
.AddCookiesManagerService()
.AddAuthContextService()
.AddSecurityContextService()
.AddGlobalStoreService()
.AddDaoFactoryService()
.AddFileSecurityService()
.AddFileMarkerService()
.AddSetupInfo()
.AddFileUtilityService()
.AddGlobalService()
.AddEmailValidationKeyProviderService()
.AddCoreBaseSettingsService()
.AddGlobalFolderHelperService()
.AddPathProviderService()
.AddUserManagerService()
.AddDocumentServiceTrackerHelperService()
.AddFilesMessageService()
.AddFileConverterService()
.AddFileShareLinkService()
.AddFFmpegServiceService();
}
public static IApplicationBuilder UseFileHandler(this IApplicationBuilder builder)
{
return builder.UseMiddleware<FileHandler>();
}
}
}

View File

@ -30,6 +30,7 @@ using System.Threading;
using System.Threading.Tasks;
using System.Web;
using ASC.Common;
using ASC.Common.Logging;
using ASC.Core;
using ASC.Core.Common;
@ -37,37 +38,54 @@ using ASC.Files.Resources;
using ASC.Web.Files.ThirdPartyApp;
using ASC.Web.Studio.Utility;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
namespace ASC.Web.Files.HttpHandlers
{
public class ThirdPartyAppHandler //: IHttpHandler
public class ThirdPartyAppHandler
{
public RequestDelegate Next { get; }
public IServiceProvider ServiceProvider { get; }
public ThirdPartyAppHandler(RequestDelegate next, IServiceProvider serviceProvider)
{
Next = next;
ServiceProvider = serviceProvider;
}
public async Task Invoke(HttpContext context)
{
using var scope = ServiceProvider.CreateScope();
var thirdPartyAppHandlerService = scope.ServiceProvider.GetService<ThirdPartyAppHandlerService>();
thirdPartyAppHandlerService.Invoke(context);
await Next.Invoke(context);
}
}
public class ThirdPartyAppHandlerService
{
public AuthContext AuthContext { get; }
public BaseCommonLinkUtility BaseCommonLinkUtility { get; }
public CommonLinkUtility CommonLinkUtility { get; }
private ILog Log { get; set; }
public string HandlerPath { get; set; }
public ThirdPartyAppHandler(
RequestDelegate next,
public ThirdPartyAppHandlerService(
IOptionsMonitor<ILog> optionsMonitor,
AuthContext authContext,
BaseCommonLinkUtility baseCommonLinkUtility,
CommonLinkUtility commonLinkUtility)
{
Next = next;
AuthContext = authContext;
BaseCommonLinkUtility = baseCommonLinkUtility;
CommonLinkUtility = commonLinkUtility;
Log = optionsMonitor.CurrentValue;
HandlerPath = baseCommonLinkUtility.ToAbsolute("~/thirdpartyapp");
}
public async Task Invoke(HttpContext context)
public void Invoke(HttpContext context)
{
Log.Debug("ThirdPartyApp: handler request - " + context.Request.Url());
@ -80,13 +98,11 @@ namespace ASC.Web.Files.HttpHandlers
if (app.Request(context))
{
await Next.Invoke(context);
return;
}
}
catch (ThreadAbortException)
{
await Next.Invoke(context);
//Thats is responce ending
return;
}
@ -111,7 +127,23 @@ namespace ASC.Web.Files.HttpHandlers
redirectUrl += HttpUtility.UrlEncode(message);
}
context.Response.Redirect(redirectUrl, true);
await Next.Invoke(context);
}
}
public static class ThirdPartyAppHandlerExtention
{
public static DIHelper AddThirdPartyAppHandlerService(this DIHelper services)
{
services.TryAddScoped<ThirdPartyAppHandlerService>();
return services
.AddCommonLinkUtilityService()
.AddBaseCommonLinkUtilityService()
.AddAuthContextService();
}
public static IApplicationBuilder UseThirdPartyAppHandler(this IApplicationBuilder builder)
{
return builder.UseMiddleware<ThirdPartyAppHandler>();
}
}
}

View File

@ -41,13 +41,35 @@ using ASC.Web.Files.Classes;
using ASC.Web.Files.Helpers;
using ASC.Web.Files.Services.NotifyService;
using ASC.Web.Studio.Utility;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
namespace ASC.Web.Files.HttpHandlers
{
public class DocuSignHandler
public class DocuSignHandler
{
public RequestDelegate Next { get; }
public IServiceProvider ServiceProvider { get; }
public DocuSignHandler(RequestDelegate next, IServiceProvider serviceProvider)
{
Next = next;
ServiceProvider = serviceProvider;
}
public async Task Invoke(HttpContext context)
{
using var scope = ServiceProvider.CreateScope();
var docuSignHandlerService = scope.ServiceProvider.GetService<DocuSignHandlerService>();
await docuSignHandlerService.Invoke(context);
await Next.Invoke(context);
}
}
public class DocuSignHandlerService
{
public static string Path(FilesLinkUtility filesLinkUtility)
{
@ -55,23 +77,18 @@ namespace ASC.Web.Files.HttpHandlers
}
private ILog Log { get; set; }
public RequestDelegate Next { get; }
public TenantExtra TenantExtra { get; }
public DocuSignHelper DocuSignHelper { get; }
public SecurityContext SecurityContext { get; }
public NotifyClient NotifyClient { get; }
public DocuSignHandler(
RequestDelegate next,
public DocuSignHandlerService(
IOptionsMonitor<ILog> optionsMonitor,
TenantExtra tenantExtra,
DocuSignHelper docuSignHelper,
SecurityContext securityContext,
NotifyClient notifyClient)
{
Next = next;
TenantExtra = tenantExtra;
DocuSignHelper = docuSignHelper;
SecurityContext = securityContext;
@ -84,7 +101,7 @@ namespace ASC.Web.Files.HttpHandlers
if (TenantExtra.IsNotPaid())
{
context.Response.StatusCode = (int)HttpStatusCode.PaymentRequired;
context.Response.WriteAsync("Payment Required.").Wait();
await context.Response.WriteAsync("Payment Required.");
return;
}
@ -106,8 +123,6 @@ namespace ASC.Web.Files.HttpHandlers
{
throw new HttpException((int)HttpStatusCode.InternalServerError, FilesCommonResource.ErrorMassage_BadRequest, e);
}
await Next.Invoke(context);
}
private void Redirect(HttpContext context)
@ -241,13 +256,18 @@ namespace ASC.Web.Files.HttpHandlers
{
public static DIHelper AddDocuSignHandlerService(this DIHelper services)
{
services.TryAddScoped<DocuSignHandler>();
services.TryAddScoped<DocuSignHandlerService>();
return services
.AddFilesLinkUtilityService()
.AddTenantExtraService()
.AddDocuSignHelperService()
.AddSecurityContextService()
.AddNotifyClientService();
}
public static IApplicationBuilder UseDocuSignHandler(this IApplicationBuilder builder)
{
return builder.UseMiddleware<DocuSignHandler>();
}
}
}

View File

@ -0,0 +1,10 @@
namespace ASC.Files.Model
{
public class SessionModel
{
public string FileName { get; set; }
public long FileSize { get; set; }
public string RelativePath { get; set; }
public bool Encrypted { get; set; }
}
}

View File

@ -43,8 +43,6 @@ using ASC.Web.Files.Classes;
using ASC.Web.Files.Utils;
using ASC.Web.Studio.Core;
using JWT;
using FileShare = ASC.Files.Core.Security.FileShare;
namespace ASC.Web.Files.Services.DocumentService
@ -293,8 +291,7 @@ namespace ASC.Web.Files.Services.DocumentService
{
if (string.IsNullOrEmpty(FileUtility.SignatureSecret)) return null;
JsonWebToken.JsonSerializer = new Web.Core.Files.DocumentService.JwtSerializer();
return JsonWebToken.Encode(payload, FileUtility.SignatureSecret, JwtHashAlgorithm.HS256);
return JsonWebToken.Encode(payload, FileUtility.SignatureSecret);
}

View File

@ -6,6 +6,7 @@ using System.Linq;
using System.Text;
using System.Threading.Tasks;
using ASC.Common;
using ASC.Common.Logging;
using ASC.Core;
@ -53,7 +54,7 @@ namespace ASC.Web.Files.Services.FFmpegService
public FFmpegService(IOptionsMonitor<ILog> optionsMonitor, IConfiguration configuration)
{
logger = optionsMonitor.CurrentValue;
FFmpegPath = configuration["files:ffmpeg"];
FFmpegPath = configuration["files:ffmpeg:value"];
FFmpegArgs = configuration["files:ffmpeg:args"] ?? "-i - -preset ultrafast -movflags frag_keyframe+empty_moov -f {0} -";
var exts = configuration["files:ffmpeg:exts"];
@ -169,4 +170,12 @@ namespace ASC.Web.Files.Services.FFmpegService
}
}
}
public static class FFmpegServiceExtensions
{
public static DIHelper AddFFmpegServiceService(this DIHelper services)
{
services.TryAddSingleton<FFmpegService>();
return services;
}
}
}

View File

@ -8,6 +8,8 @@ using ASC.Api.Documents;
using ASC.Common;
using ASC.Common.DependencyInjection;
using ASC.Common.Logging;
using ASC.Web.Files;
using ASC.Web.Files.HttpHandlers;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authorization;
@ -81,7 +83,11 @@ namespace ASC.Files
diHelper
.AddDocumentsControllerService()
.AddEncryptionControllerService();
.AddEncryptionControllerService()
.AddFileHandlerService()
.AddChunkedUploaderHandlerService()
.AddThirdPartyAppHandlerService()
.AddDocuSignHandlerService();
services.AddAutofac(Configuration, HostEnvironment.ContentRootPath);
}
@ -120,6 +126,34 @@ namespace ASC.Files
endpoints.MapCustom();
});
app.MapWhen(
context => context.Request.Path.ToString().EndsWith("httphandlers/filehandler.ashx"),
appBranch =>
{
appBranch.UseFileHandler();
});
app.MapWhen(
context => context.Request.Path.ToString().EndsWith("ChunkedUploader.ashx"),
appBranch =>
{
appBranch.UseChunkedUploaderHandler();
});
app.MapWhen(
context => context.Request.Path.ToString().EndsWith("ThirdPartyAppHandler.ashx"),
appBranch =>
{
appBranch.UseThirdPartyAppHandler();
});
app.MapWhen(
context => context.Request.Path.ToString().EndsWith("DocuSignHandler.ashx"),
appBranch =>
{
appBranch.UseDocuSignHandler();
});
app.UseStaticFiles();
}
}

View File

@ -109,7 +109,7 @@ namespace ASC.Web.Files.ThirdPartyApp
public SetupInfo SetupInfo { get; }
public TokenHelper TokenHelper { get; }
public DocumentServiceConnector DocumentServiceConnector { get; }
public ThirdPartyAppHandler ThirdPartyAppHandler { get; }
public ThirdPartyAppHandlerService ThirdPartyAppHandlerService { get; }
public IServiceProvider ServiceProvider { get; }
public ILog Logger { get; }
@ -137,7 +137,7 @@ namespace ASC.Web.Files.ThirdPartyApp
SetupInfo setupInfo,
TokenHelper tokenHelper,
DocumentServiceConnector documentServiceConnector,
ThirdPartyAppHandler thirdPartyAppHandler,
ThirdPartyAppHandlerService thirdPartyAppHandlerService,
IServiceProvider serviceProvider,
TenantManager tenantManager,
CoreBaseSettings coreBaseSettings,
@ -165,7 +165,7 @@ namespace ASC.Web.Files.ThirdPartyApp
SetupInfo = setupInfo;
TokenHelper = tokenHelper;
DocumentServiceConnector = documentServiceConnector;
ThirdPartyAppHandler = thirdPartyAppHandler;
ThirdPartyAppHandlerService = thirdPartyAppHandlerService;
ServiceProvider = serviceProvider;
Logger = option.CurrentValue;
}
@ -251,7 +251,7 @@ namespace ASC.Web.Files.ThirdPartyApp
Logger.Debug("BoxApp: get file stream url " + fileId);
var uriBuilder = new UriBuilder(BaseCommonLinkUtility.GetFullAbsolutePath(ThirdPartyAppHandler.HandlerPath));
var uriBuilder = new UriBuilder(BaseCommonLinkUtility.GetFullAbsolutePath(ThirdPartyAppHandlerService.HandlerPath));
if (uriBuilder.Uri.IsLoopback)
{
uriBuilder.Host = Dns.GetHostName();
@ -476,7 +476,7 @@ namespace ASC.Web.Files.ThirdPartyApp
using (var stream = new ResponseStream(request.GetResponse()))
{
stream.StreamCopyTo(context.Response.Body);
stream.CopyTo(context.Response.Body, StreamExtension.BufferSize);
}
}
catch (Exception ex)

View File

@ -116,7 +116,7 @@ namespace ASC.Web.Files.ThirdPartyApp
public GoogleLoginProvider GoogleLoginProvider { get; }
public TokenHelper TokenHelper { get; }
public DocumentServiceConnector DocumentServiceConnector { get; }
public ThirdPartyAppHandler ThirdPartyAppHandler { get; }
public ThirdPartyAppHandlerService ThirdPartyAppHandlerService { get; }
public IServiceProvider ServiceProvider { get; }
public GoogleDriveApp()
@ -147,7 +147,7 @@ namespace ASC.Web.Files.ThirdPartyApp
GoogleLoginProvider googleLoginProvider,
TokenHelper tokenHelper,
DocumentServiceConnector documentServiceConnector,
ThirdPartyAppHandler thirdPartyAppHandler,
ThirdPartyAppHandlerService thirdPartyAppHandlerService,
IServiceProvider serviceProvider,
TenantManager tenantManager,
CoreBaseSettings coreBaseSettings,
@ -180,7 +180,7 @@ namespace ASC.Web.Files.ThirdPartyApp
GoogleLoginProvider = googleLoginProvider;
TokenHelper = tokenHelper;
DocumentServiceConnector = documentServiceConnector;
ThirdPartyAppHandler = thirdPartyAppHandler;
ThirdPartyAppHandlerService = thirdPartyAppHandlerService;
ServiceProvider = serviceProvider;
}
@ -257,7 +257,7 @@ namespace ASC.Web.Files.ThirdPartyApp
{
Logger.Debug("GoogleDriveApp: get file stream url " + fileId);
var uriBuilder = new UriBuilder(BaseCommonLinkUtility.GetFullAbsolutePath(ThirdPartyAppHandler.HandlerPath));
var uriBuilder = new UriBuilder(BaseCommonLinkUtility.GetFullAbsolutePath(ThirdPartyAppHandlerService.HandlerPath));
if (uriBuilder.Uri.IsLoopback)
{
uriBuilder.Host = Dns.GetHostName();
@ -535,7 +535,7 @@ namespace ASC.Web.Files.ThirdPartyApp
using (var response = request.GetResponse())
using (var stream = new ResponseStream(response))
{
stream.StreamCopyTo(context.Response.Body);
stream.CopyTo(context.Response.Body, StreamExtension.BufferSize);
var contentLength = jsonFile.Value<string>("size");
Logger.Debug("GoogleDriveApp: get file stream contentLength - " + contentLength);

View File

@ -75,7 +75,11 @@ namespace ASC.Web.Files.Utils
public ChunkedUploadSession<T> GetSession<T>(string sessionId)
{
return (ChunkedUploadSession<T>)CommonSessionHolder(false).Get(sessionId);
return (ChunkedUploadSession<T>)GetSession(sessionId);
}
public CommonChunkedUploadSession GetSession(string sessionId)
{
return CommonSessionHolder(false).Get(sessionId);
}
public ChunkedUploadSession<T> CreateUploadSession<T>(File<T> file, long contentLength)

View File

@ -406,11 +406,11 @@ namespace ASC.Web.Files.Utils
cacheFolderId = GlobalFolder.GetFolderShare<T>(DaoFactory);
}
if (rootFolderId != null)
if (!rootFolderId.Equals(default(T)))
{
parentFolders.Add(folderDao.GetFolder(rootFolderId));
}
if (cacheFolderId != null)
if (!cacheFolderId.Equals(default(T)))
{
RemoveFromCahce(cacheFolderId, userID);
}

View File

@ -1,6 +1,6 @@
{
"name": "asc-web-common",
"version": "1.0.133",
"version": "1.0.138",
"description": "Ascensio System SIA common components and solutions library",
"license": "AGPL-3.0",
"files": [

View File

@ -3,6 +3,15 @@ import axios from "axios";
import FilesFilter from "./filter";
import * as fakeFiles from "./fake";
export function openEdit(fileId) {
const options = {
method: "get",
url: `/files/file/${fileId}/openedit`
};
return request(options);
}
export function getFolderInfo(folderId) {
const options = {
method: "get",
@ -305,3 +314,12 @@ export function setShareFiles(fileId, shareTo, access, notify, sharingMessage) {
const data = { share, notify, sharingMessage };
return request({ method: "put", url: `/files/file/${fileId}/share`, data });
}
export function startUploadSession(folderId, fileName, fileSize, relativePath) {
const data = { fileName, fileSize, relativePath };
return request({ method: "post", url: `/files/${folderId}/upload/create_session.json`, data });
}
export function uploadFile(url, data) {
return axios.post(url, data);
}

View File

@ -1,6 +1,6 @@
import React from "react";
import PropTypes from "prop-types";
import { Backdrop } from "asc-web-components";
import { Backdrop, ProgressBar } from "asc-web-components";
import { withTranslation } from 'react-i18next';
import i18n from './i18n';
import { ARTICLE_PINNED_KEY } from "../../constants";
@ -148,7 +148,7 @@ class PageLayoutComponent extends React.PureComponent {
};
render() {
const { showProgressBar, progressBarMaxValue, progressBarValue, progressBarDropDownContent, withBodyScroll, withBodyAutoFocus, progressBarLabel } = this.props;
const { showProgressBar, progressBarValue, progressBarDropDownContent, withBodyScroll, withBodyAutoFocus, progressBarLabel } = this.props;
return (
<>
{this.state.isBackdropAvailable && (
@ -196,24 +196,29 @@ class PageLayoutComponent extends React.PureComponent {
<SectionFilter className="section-header_filter">{this.state.sectionFilterContent}</SectionFilter>
)}
{this.state.isSectionBodyAvailable && (
<SectionBody
showProgressBar={showProgressBar}
progressBarMaxValue={progressBarMaxValue}
progressBarValue={progressBarValue}
progressBarLabel={progressBarLabel}
progressBarDropDownContent={progressBarDropDownContent}
withScroll={withBodyScroll}
autoFocus={withBodyAutoFocus}
pinned={this.state.isArticlePinned}
>
{this.state.isSectionFilterAvailable && (
<SectionFilter className="section-body_filter">{this.state.sectionFilterContent}</SectionFilter>
)}
{this.state.sectionBodyContent}
{this.state.isSectionPagingAvailable && (
<SectionPaging>{this.state.sectionPagingContent}</SectionPaging>
<>
<SectionBody
withScroll={withBodyScroll}
autoFocus={withBodyAutoFocus}
pinned={this.state.isArticlePinned}
>
{this.state.isSectionFilterAvailable && (
<SectionFilter className="section-body_filter">{this.state.sectionFilterContent}</SectionFilter>
)}
{this.state.sectionBodyContent}
{this.state.isSectionPagingAvailable && (
<SectionPaging>{this.state.sectionPagingContent}</SectionPaging>
)}
</SectionBody>
{showProgressBar && (
<ProgressBar
className="layout-progress-bar"
label={progressBarLabel}
percent={progressBarValue}
dropDownContent={progressBarDropDownContent}
/>
)}
</SectionBody>
</>
)}
{this.state.isArticleAvailable && (
@ -275,7 +280,6 @@ PageLayoutComponent.propTypes = {
t: PropTypes.func,
showProgressBar: PropTypes.bool,
progressBarMaxValue: PropTypes.number,
progressBarValue: PropTypes.number,
progressBarDropDownContent: PropTypes.any,
progressBarLabel: PropTypes.string

View File

@ -1,7 +1,7 @@
import React from "react";
import PropTypes from "prop-types";
import styled from "styled-components";
import { utils, Scrollbar, ProgressBar } from "asc-web-components";
import { utils, Scrollbar } from "asc-web-components";
const { tablet } = utils.device;
const StyledSectionBody = styled.div`
@ -28,11 +28,6 @@ const StyledSectionWrapper = styled.div`
display: flex;
flex-direction: column;
min-height: 100%;
.layout-progress-bar {
flex: 0 0 auto;
margin-right: -16px;
}
`;
const StyledSpacer = styled.div`
@ -49,7 +44,6 @@ class SectionBody extends React.Component {
super(props);
this.focusRef = React.createRef();
this.ref = React.createRef();
}
componentDidMount() {
@ -60,17 +54,15 @@ class SectionBody extends React.Component {
render() {
//console.log("PageLayout SectionBody render");
const { children, withScroll, autoFocus, pinned, showProgressBar, progressBarMaxValue, progressBarValue, progressBarDropDownContent, progressBarLabel } = this.props;
const { children, withScroll, autoFocus, pinned } = this.props;
const focusProps = autoFocus ? {
ref: this.focusRef,
tabIndex: 1
} : {};
const width = this.ref.current && this.ref.current.offsetWidth;
return (
<StyledSectionBody ref={this.ref} withScroll={withScroll}>
<StyledSectionBody withScroll={withScroll}>
{withScroll ? (
<Scrollbar stype="mediumBlack">
<StyledSectionWrapper>
@ -78,16 +70,6 @@ class SectionBody extends React.Component {
{children}
<StyledSpacer pinned={pinned}/>
</div>
{showProgressBar && (
<ProgressBar
className="layout-progress-bar"
label={progressBarLabel}
value={progressBarValue}
width={width}
maxValue={progressBarMaxValue}
dropDownContent={progressBarDropDownContent}
/>
)}
</StyledSectionWrapper>
</Scrollbar>
) : (
@ -107,11 +89,6 @@ SectionBody.propTypes = {
withScroll: PropTypes.bool,
autoFocus: PropTypes.bool,
pinned: PropTypes.bool,
showProgressBar: PropTypes.bool,
progressBarMaxValue: PropTypes.number,
progressBarValue: PropTypes.number,
progressBarLabel: PropTypes.string,
progressBarDropDownContent: PropTypes.any,
children: PropTypes.oneOfType([
PropTypes.arrayOf(PropTypes.node),
PropTypes.node,

View File

@ -9,6 +9,11 @@ const StyledSection = styled.section`
display: flex;
flex-direction: column;
.layout-progress-bar {
position: sticky;
margin-left: -16px;
}
.section-header_filter {
display: block;
}

View File

@ -4,7 +4,7 @@ import PropTypes from 'prop-types'
import CustomScrollbarsVirtualList from '../scrollbar/custom-scrollbars-virtual-list'
import DropDownItem from '../drop-down-item'
import Backdrop from '../backdrop'
import { FixedSizeList } from "react-window"
import { VariableSizeList } from "react-window"
import onClickOutside from "react-onclickoutside";
const StyledDropdown = styled.div`
@ -43,12 +43,14 @@ const StyledDropdown = styled.div`
// eslint-disable-next-line react/display-name, react/prop-types
const Row = memo(({ data, index, style }) => {
const option = data[index];
const separator = option.props.isSeparator ? { width: `calc(100% - 32px)`, height: `1px` } : {}
const newStyle = {...style, ...separator};
return (
<DropDownItem
// eslint-disable-next-line react/prop-types
{...option.props}
style={style} />
style={newStyle} />
);
});
@ -115,12 +117,21 @@ class DropDown extends React.PureComponent {
});
}
getItemHeight = (item) => {
const isTablet = window.innerWidth < 1024; //TODO: Make some better
if (item && item.props.isSeparator)
return isTablet ? 16 : 12;
return isTablet ? 36 : 32;
}
render() {
const { maxHeight, children } = this.props;
const { directionX, directionY, width } = this.state;
const isTablet = window.innerWidth < 1024; //TODO: Make some better
const itemHeight = isTablet ? 36 : 32;
const fullHeight = children && children.length * itemHeight;
const rowHeights = React.Children.map(children, child => this.getItemHeight(child));
const getItemSize = index => rowHeights[index];
const fullHeight = children && rowHeights.reduce((a, b) => a + b, 0);
const calculatedHeight = ((fullHeight > 0) && (fullHeight < maxHeight)) ? fullHeight : maxHeight;
const dropDownMaxHeightProp = maxHeight ? { height: calculatedHeight + 'px' } : {};
//console.log("DropDown render", this.props);
@ -133,16 +144,16 @@ class DropDown extends React.PureComponent {
{...dropDownMaxHeightProp}
>
{maxHeight
? <FixedSizeList
? <VariableSizeList
height={calculatedHeight}
width={width}
itemSize={itemHeight}
itemSize={getItemSize}
itemCount={children.length}
itemData={children}
outerElementType={CustomScrollbarsVirtualList}
>
{Row}
</FixedSizeList>
</VariableSizeList>
: children}
</StyledDropdown>
);

View File

@ -13,6 +13,7 @@ import { EmptyScreenContainer } from "asc-web-components";
imageSrc="empty_screen_filter.png"
imageAlt="Empty Screen Filter image"
headerText="No results matching your search could be found"
subheading="No files to be displayed in this section"
descriptionText="No results matching your search could be found"
buttons={<a href="/">Go to home</a>}
/>
@ -26,6 +27,7 @@ import { EmptyScreenContainer } from "asc-web-components";
| `className` | `string` | - | - | - | Accepts class |
| `descriptionText` | `string` | - | - | - | Description text |
| `headerText` | `string` | - | - | - | Header text |
| `subheadingText` | `string` | - | - | - | Subheading text |
| `id` | `string` | - | - | - | Accepts id |
| `imageAlt` | `string` | - | - | - | Alternative image text |
| `imageSrc` | `string` | - | - | - | Image url source |

View File

@ -19,6 +19,10 @@ storiesOf("Components| EmptyScreenContainer", module)
"headerText",
"No results matching your search could be found"
)}
subheadingText={text(
"subheaderText",
"No files to be displayed in this section"
)}
descriptionText={text(
"descriptionText",
"No people matching your filter can be displayed in this section. Please select other filter options or clear filter to view all the people in this section."

View File

@ -10,6 +10,7 @@ const EmptyContentBody = styled.div`
display: grid;
grid-template-areas:
"img header"
"img subheading"
"img desc"
"img button";
min-width: 320px;
@ -23,6 +24,11 @@ const EmptyContentBody = styled.div`
.ec-header {
grid-area: header;
}
.ec-subheading {
grid-area: subheading;
padding: 8px 0;
}
.ec-desc {
grid-area: desc;
@ -44,6 +50,10 @@ const EmptyContentBody = styled.div`
font-size: 3.5vw;
}
.ec-subheading {
font-size: 2.9vw;
}
.ec-desc {
font-size: 2.4vw;
}
@ -58,6 +68,10 @@ const EmptyContentBody = styled.div`
font-size: 4.75vw;
}
.ec-subheading {
font-size: 4.15vw;
}
.ec-desc {
font-size: 3.7vw;
}
@ -73,7 +87,7 @@ const EmptyContentImage = styled.img.attrs(props => ({
`;
const EmptyScreenContainer = props => {
const { imageSrc, imageAlt, headerText, descriptionText, buttons } = props;
const { imageSrc, imageAlt, headerText, subheadingText, descriptionText, buttons } = props;
return (
<EmptyContentBody {...props}>
@ -84,6 +98,10 @@ const EmptyScreenContainer = props => {
<Text as="span" color="#333333" fontSize='24px' className="ec-header">{headerText}</Text>
)}
{subheadingText && (
<Text as="span" color="#737373" fontSize='18px' className="ec-subheading">{subheadingText}</Text>
)}
{descriptionText && (
<Text as="span" color="#737373" fontSize='14px' className="ec-desc">{descriptionText}</Text>
)}
@ -102,6 +120,7 @@ EmptyScreenContainer.propTypes = {
imageSrc: PropTypes.string,
imageAlt: PropTypes.string,
headerText: PropTypes.string,
subheadingText: PropTypes.string,
descriptionText: PropTypes.string,
buttons: PropTypes.any,
className: PropTypes.string,

View File

@ -16,7 +16,6 @@ import { ProgressBar } from "asc-web-components";
| Props | Type | Required | Values | Default | Description |
| :---------------: | :------: | :------: | :----: | :-----: | ---------------------- |
| `value` | `number` | ✅ | - | - | Progress value. |
| `percent` | `number` | ✅ | - | - | Progress value. |
| `label` | `string` | - | - | - | Text in progress-bar. |
| `maxValue` | `number` | - | - | 100 | Max value of progress. |
| `dropDownContent` | `any` | - | - | - | Drop-down content. |

View File

@ -17,14 +17,14 @@ const StyledProgressBar = styled.div`
}
.progress-bar_percent {
width: ${props => props.percent}%;
width: ${props => props.uploadedPercent}%;
float: left;
overflow: hidden;
max-height: 22px;
min-height: 22px;
}
.progress-bar_field {
width: ${props => props.percent2}%;
width: ${props => props.remainPercent}%;
float: left;
overflow: hidden;
max-height: 22px;
@ -32,6 +32,7 @@ const StyledProgressBar = styled.div`
}
.progress-bar_percent {
transition: width .6s ease;
background: linear-gradient(90deg, #20d21f 75%, #b9d21f 100%);
}
@ -56,8 +57,7 @@ const StyledProgressBar = styled.div`
`;
const ProgressBar = props => {
const { value, maxValue, label, dropDownContent, ...rest } = props;
const percent = value > maxValue ? 100 : (value / maxValue) * 100;
const { percent, label, dropDownContent, ...rest } = props;
const ref = React.createRef();
const [isOpen, setIsOpen] = useState(false);
@ -69,7 +69,7 @@ const ProgressBar = props => {
//console.log("ProgressBar render");
return (
<StyledProgressBar ref={ref} {...rest} percent={percent} percent2={100 - percent} >
<StyledProgressBar ref={ref} {...rest} uploadedPercent={percent} remainPercent={100 - percent} >
<Link
className="progress-bar_full-text"
color="#333"
@ -101,14 +101,9 @@ const ProgressBar = props => {
};
ProgressBar.propTypes = {
value: PropTypes.number,
maxValue: PropTypes.number,
percent: PropTypes.number.isRequired,
label: PropTypes.string,
dropDownContent: PropTypes.any,
};
ProgressBar.defaultProps = {
maxValue: 100
};
export default ProgressBar;

View File

@ -12,8 +12,7 @@ storiesOf("Components|ProgressBar", module)
<ProgressBar
style={{marginTop: 16}}
label={text("label", "Uploading files: 20 of 100")}
value={number("value", 20)}
maxValue={number("maxValue", 100)}
percent={number("value", 20)}
dropDownContent={text("dropDownContent", "You content here")}
/>
));

View File

@ -38,9 +38,10 @@ namespace ASC.Web.Core.Files
// to RFC 2231 (with clarifications from RFC 5987)
if (fileName.Any(c => (int)c > 127))
{
//.netcore
var str = withoutBase
? "{0}; filename*=UTF-8''{2}"
: "{0}; filename=\"{1}\"; filename*=UTF-8''{2}";
: "{0}; filename=\"{2}\"; filename*=UTF-8''{2}";
return string.Format(str,
inline ? "inline" : "attachment",

View File

@ -35,13 +35,12 @@ using System.Runtime.Serialization;
using System.Security.Cryptography;
using System.Text;
using System.Text.RegularExpressions;
using ASC.Common.Web;
using ASC.Core;
using JWT;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Newtonsoft.Json.Serialization;
using Formatting = Newtonsoft.Json.Formatting;
namespace ASC.Web.Core.Files
{
@ -145,12 +144,11 @@ namespace ASC.Web.Core.Files
{
{ "payload", body }
};
JsonWebToken.JsonSerializer = new JwtSerializer();
var token = JsonWebToken.Encode(payload, signatureSecret, JwtHashAlgorithm.HS256);
var token = JsonWebToken.Encode(payload, signatureSecret);
//todo: remove old scheme
request.Headers.Add(fileUtility.SignatureHeader, "Bearer " + token);
token = JsonWebToken.Encode(body, signatureSecret, JwtHashAlgorithm.HS256);
token = JsonWebToken.Encode(body, signatureSecret);
body.Token = token;
}
@ -252,15 +250,15 @@ namespace ASC.Web.Core.Files
if (!string.IsNullOrEmpty(signatureSecret))
{
var payload = new Dictionary<string, object>
{
{ "payload", body }
};
JsonWebToken.JsonSerializer = new JwtSerializer();
var token = JsonWebToken.Encode(payload, signatureSecret, JwtHashAlgorithm.HS256);
{
{ "payload", body }
};
var token = JsonWebToken.Encode(payload, signatureSecret);
//todo: remove old scheme
request.Headers.Add(fileUtility.SignatureHeader, "Bearer " + token);
token = JsonWebToken.Encode(body, signatureSecret, JwtHashAlgorithm.HS256);
token = JsonWebToken.Encode(body, signatureSecret);
body.Token = token;
}
@ -336,12 +334,11 @@ namespace ASC.Web.Core.Files
{ "payload", body }
};
JsonWebToken.JsonSerializer = new JwtSerializer();
var token = JsonWebToken.Encode(payload, signatureSecret, JwtHashAlgorithm.HS256);
var token = JsonWebToken.Encode(payload, signatureSecret);
//todo: remove old scheme
request.Headers.Add(fileUtility.SignatureHeader, "Bearer " + token);
token = JsonWebToken.Encode(body, signatureSecret, JwtHashAlgorithm.HS256);
token = JsonWebToken.Encode(body, signatureSecret);
body.Token = token;
}
@ -625,43 +622,5 @@ namespace ASC.Web.Core.Files
return resultPercent;
}
public class JwtSerializer : IJsonSerializer
{
private class CamelCaseExceptDictionaryKeysResolver : CamelCasePropertyNamesContractResolver
{
protected override JsonDictionaryContract CreateDictionaryContract(Type objectType)
{
var contract = base.CreateDictionaryContract(objectType);
contract.DictionaryKeyResolver = propertyName => propertyName;
return contract;
}
}
public string Serialize(object obj)
{
var settings = new JsonSerializerSettings
{
ContractResolver = new CamelCaseExceptDictionaryKeysResolver(),
NullValueHandling = NullValueHandling.Ignore,
};
return JsonConvert.SerializeObject(obj, Formatting.Indented, settings);
}
public T Deserialize<T>(string json)
{
var settings = new JsonSerializerSettings
{
ContractResolver = new CamelCaseExceptDictionaryKeysResolver(),
NullValueHandling = NullValueHandling.Ignore,
};
return JsonConvert.DeserializeObject<T>(json, settings);
}
}
}
}

View File

@ -213,17 +213,17 @@ namespace ASC.Web.Core.Files
}
}
private List<string> extsWebPreviewed { get => (Configuration["files.docservice.viewed-docs"] ?? "").Split(new char[] { '|', ',' }, StringSplitOptions.RemoveEmptyEntries).ToList(); }
private List<string> extsWebEdited { get => (Configuration["files.docservice.edited-docs"] ?? "").Split(new char[] { '|', ',' }, StringSplitOptions.RemoveEmptyEntries).ToList(); }
private List<string> extsWebReviewed { get => (Configuration["files.docservice.reviewed-docs"] ?? "").Split(new char[] { '|', ',' }, StringSplitOptions.RemoveEmptyEntries).ToList(); }
private List<string> extsWebRestrictedEditing { get => (Configuration["files.docservice.formfilling-docs"] ?? "").Split(new char[] { '|', ',' }, StringSplitOptions.RemoveEmptyEntries).ToList(); }
private List<string> extsWebCommented { get => (Configuration["files.docservice.commented-docs"] ?? "").Split(new char[] { '|', ',' }, StringSplitOptions.RemoveEmptyEntries).ToList(); }
private List<string> extsMustConvert { get => (Configuration["files.docservice.convert-docs"] ?? "").Split(new char[] { '|', ',' }, StringSplitOptions.RemoveEmptyEntries).ToList(); }
private List<string> extsIndexing { get => (Configuration["files.index.formats"] ?? "").Split(new[] { '|', ',' }, StringSplitOptions.RemoveEmptyEntries).ToList(); }
private List<string> extsWebPreviewed { get => (Configuration.GetSection("files:docservice:viewed-docs").Get<string[]>() ?? new string[] { }).ToList(); }
private List<string> extsWebEdited { get => (Configuration.GetSection("files:docservice:edited-docs").Get<string[]>() ?? new string[] { }).ToList(); }
private List<string> extsWebReviewed { get => (Configuration.GetSection("files:docservice:reviewed-docs").Get<string[]>() ?? new string[] { }).ToList(); }
private List<string> extsWebRestrictedEditing { get => (Configuration.GetSection("files:docservice:formfilling-docs").Get<string[]>() ?? new string[] { }).ToList(); }
private List<string> extsWebCommented { get => (Configuration.GetSection("files:docservice:commented-docs").Get<string[]>() ?? new string[] { }).ToList(); }
private List<string> extsMustConvert { get => (Configuration.GetSection("files:docservice:convert-docs").Get<string[]>() ?? new string[] { }).ToList(); }
private List<string> extsIndexing { get => (Configuration.GetSection("files:index").Get<string[]>() ?? new string[] { }).ToList(); }
public List<string> ExtsImagePreviewed { get => (Configuration["files.viewed-images"] ?? "").Split(new char[] { '|', ',' }, StringSplitOptions.RemoveEmptyEntries).ToList(); }
public List<string> ExtsImagePreviewed { get => (Configuration.GetSection("files:viewed-images").Get<string[]>() ?? new string[] { }).ToList(); }
public List<string> ExtsMediaPreviewed { get => (Configuration["files.viewed-media"] ?? "").Split(new char[] { '|', ',' }, StringSplitOptions.RemoveEmptyEntries).ToList(); }
public List<string> ExtsMediaPreviewed { get => (Configuration.GetSection("files:viewed-media").Get<string[]>() ?? new string[] { }).ToList(); }
public List<string> ExtsWebPreviewed
{
@ -235,7 +235,7 @@ namespace ASC.Web.Core.Files
get { return string.IsNullOrEmpty(FilesLinkUtility.DocServiceApiUrl) ? new List<string>() : extsWebEdited; }
}
public List<string> ExtsWebEncrypt { get => (Configuration["files.docservice.encrypted-docs"] ?? "").Split(new char[] { '|', ',' }, StringSplitOptions.RemoveEmptyEntries).ToList(); }
public List<string> ExtsWebEncrypt { get => (Configuration.GetSection("files:docservice:encrypted-docs").Get<string[]>() ?? new string[] { }).ToList(); }
public List<string> ExtsWebReviewed
{
@ -257,7 +257,7 @@ namespace ASC.Web.Core.Files
get { return string.IsNullOrEmpty(FilesLinkUtility.DocServiceConverterUrl) ? new List<string>() : extsMustConvert; }
}
public List<string> ExtsCoAuthoring { get => (Configuration["files.docservice.coauthor-docs"] ?? "").Split(new char[] { '|', ',' }, StringSplitOptions.RemoveEmptyEntries).ToList(); }
public List<string> ExtsCoAuthoring { get => (Configuration.GetSection("files:docservice:coauthor-docs").Get<string[]>() ?? new string[] { }).ToList(); }
public IConfiguration Configuration { get; }
public FilesLinkUtility FilesLinkUtility { get; }
@ -330,9 +330,9 @@ namespace ASC.Web.Core.Files
{
get => new Dictionary<FileType, string>
{
{ FileType.Document, Configuration["files.docservice.internal-doc"] ?? ".docx" },
{ FileType.Spreadsheet, Configuration["files.docservice.internal-xls"] ?? ".xlsx" },
{ FileType.Presentation, Configuration["files.docservice.internal-ppt"] ?? ".pptx" }
{ FileType.Document, Configuration["files:docservice:internal-doc"] ?? ".docx" },
{ FileType.Spreadsheet, Configuration["files:docservice:internal-xls"] ?? ".xlsx" },
{ FileType.Presentation, Configuration["files:docservice:internal-ppt"] ?? ".pptx" }
};
}
@ -350,7 +350,7 @@ namespace ASC.Web.Core.Files
private string GetSignatureSecret()
{
var result = Configuration["files.docservice.secret"] ?? "";
var result = Configuration["files:docservice:secret:value"] ?? "";
var regex = new Regex(@"^\s+$");
@ -362,7 +362,7 @@ namespace ASC.Web.Core.Files
private string GetSignatureHeader()
{
var result = (Configuration["files.docservice.secret.header"] ?? "").Trim();
var result = (Configuration["files:docservice:secret:header"] ?? "").Trim();
if (string.IsNullOrEmpty(result))
result = "Authorization";
return result;

View File

@ -65,7 +65,7 @@ namespace ASC.Web.Core.Files
CoreSettings = coreSettings;
Configuration = configuration;
InstanceCrypto = instanceCrypto;
FilesUploaderURL = Configuration["files.uploader.url"] ?? "~";
FilesUploaderURL = Configuration["files:uploader:url"] ?? "~";
}
public string FilesBaseAbsolutePath
@ -409,7 +409,7 @@ namespace ASC.Web.Core.Files
}
if (string.IsNullOrEmpty(value))
{
value = Configuration["files.docservice.url." + (appSettingsKey ?? key)];
value = Configuration["files:docservice:url:" + (appSettingsKey ?? key)];
}
return value;
}

View File

@ -389,6 +389,8 @@ namespace ASC.Web.Core.Sms
set { }
}
public VoipDao VoipDao { get; }
public override bool Enable()
{
return
@ -425,6 +427,7 @@ namespace ASC.Web.Core.Sms
}
public TwilioProvider(
VoipDao voipDao,
TenantManager tenantManager,
CoreBaseSettings coreBaseSettings,
CoreSettings coreSettings,
@ -434,6 +437,7 @@ namespace ASC.Web.Core.Sms
string name, int order, Dictionary<string, string> props, Dictionary<string, string> additional = null)
: base(tenantManager, coreBaseSettings, coreSettings, configuration, cache, options, name, order, props, additional)
{
VoipDao = voipDao;
}
@ -456,12 +460,11 @@ namespace ASC.Web.Core.Sms
var provider = new VoipService.Twilio.TwilioProvider(Key, Secret, authContext, tenantUtil, securityContext, baseCommonLinkUtility);
var dao = new CachedVoipDao(tenantManager.GetCurrentTenant().TenantId, dbOptions, authContext, tenantUtil, securityContext, baseCommonLinkUtility, ConsumerFactory, voipDaoCache);
var numbers = dao.GetNumbers();
var numbers = VoipDao.GetNumbers();
foreach (var number in numbers)
{
provider.DisablePhone(number);
dao.DeleteNumber(number.Id);
VoipDao.DeleteNumber(number.Id);
}
}
}
@ -473,6 +476,7 @@ namespace ASC.Web.Core.Sms
}
public TwilioSaaSProvider(
VoipDao voipDao,
TenantManager tenantManager,
CoreBaseSettings coreBaseSettings,
CoreSettings coreSettings,
@ -480,8 +484,21 @@ namespace ASC.Web.Core.Sms
ICacheNotify<ConsumerCacheItem> cache,
IOptionsMonitor<ILog> options,
string name, int order, Dictionary<string, string> additional = null)
: base(tenantManager, coreBaseSettings, coreSettings, configuration, cache, options, name, order, null, additional)
: base(voipDao, tenantManager, coreBaseSettings, coreSettings, configuration, cache, options, name, order, null, additional)
{
}
}
public static class TwilioProviderExtention
{
public static DIHelper AddTwilioProviderService(this DIHelper services)
{
services.TryAddScoped<TwilioProvider>();
services.TryAddScoped<TwilioSaaSProvider>();
return services.AddVoipDaoService()
.AddTenantManagerService()
.AddCoreBaseSettingsService()
.AddCoreSettingsService();
}
}
}