Merge branch 'feature/virtual-rooms-1.2' into hotfix/campaigns-banner

This commit is contained in:
Dmitry Sychugov 2022-03-28 12:36:11 +05:00
commit 7abe990d96
255 changed files with 8793 additions and 1404 deletions

View File

@ -1,4 +1,6 @@
using System.Net;
using System;
using System.Linq;
using System.Net;
using ASC.Common;
using ASC.Common.Logging;
@ -15,6 +17,8 @@ namespace ASC.Api.Core.Middleware
public class TenantStatusFilter : IResourceFilter
{
private readonly ILog log;
private readonly string[] passthroughtRequestEndings = new[] { "preparation-portal", "getrestoreprogress", "settings", "settings.json" }; //TODO add or update when "preparation-portal" will be done
public TenantStatusFilter(IOptionsMonitor<ILog> options, TenantManager tenantManager)
{
@ -44,6 +48,17 @@ namespace ASC.Api.Core.Middleware
log.WarnFormat("Tenant {0} is not removed or suspended", tenant.TenantId);
return;
}
if (tenant.Status == TenantStatus.Transfering || tenant.Status == TenantStatus.Restoring)
{
if (passthroughtRequestEndings.Any(path => context.HttpContext.Request.Path.ToString().EndsWith(path, StringComparison.InvariantCultureIgnoreCase)))
{
return;
}
context.Result = new StatusCodeResult((int)HttpStatusCode.Forbidden);
log.WarnFormat("Tenant {0} is {1}", tenant.TenantId, tenant.Status);
return;
}
}
}
}

View File

@ -178,7 +178,7 @@ namespace ASC.Common.Threading
}
public void QueueTask(Action<DistributedTask, CancellationToken> action, DistributedTask distributedTask = null)
{
{
if (distributedTask == null)
{
distributedTask = new DistributedTask();
@ -329,7 +329,10 @@ namespace ASC.Common.Threading
if (distributedTask != null)
{
distributedTask.Status = DistributedTaskStatus.Completed;
distributedTask.Exception = task.Exception;
if (task.Exception != null)
{
distributedTask.Exception = task.Exception;
}
if (task.IsFaulted)
{
distributedTask.Status = DistributedTaskStatus.Failted;

View File

@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using ASC.Api.Utils;
@ -14,7 +15,6 @@ using ASC.MessagingSystem;
using ASC.Notify.Cron;
using ASC.Web.Core.PublicResources;
using ASC.Web.Studio.Core;
using ASC.Web.Studio.Core.Backup;
using ASC.Web.Studio.Utility;
namespace ASC.Data.Backup
@ -31,8 +31,11 @@ namespace ASC.Data.Backup
private UserManager UserManager { get; }
private TenantExtra TenantExtra { get; }
private ConsumerFactory ConsumerFactory { get; }
private BackupFileUploadHandler BackupFileUploadHandler { get; }
private BackupService BackupService { get; }
private TempPath TempPath { get; }
private const string BackupTempFolder = "backup";
private const string BackupFileName = "backup.tmp";
#region backup
@ -47,7 +50,7 @@ namespace ASC.Data.Backup
UserManager userManager,
TenantExtra tenantExtra,
ConsumerFactory consumerFactory,
BackupFileUploadHandler backupFileUploadHandler)
TempPath tempPath)
{
TenantManager = tenantManager;
MessageService = messageService;
@ -58,8 +61,8 @@ namespace ASC.Data.Backup
UserManager = userManager;
TenantExtra = tenantExtra;
ConsumerFactory = consumerFactory;
BackupFileUploadHandler = backupFileUploadHandler;
BackupService = backupService;
TempPath = tempPath;
}
public void StartBackup(BackupStorageType storageType, Dictionary<string, string> storageParams, bool backupMail)
@ -263,7 +266,7 @@ namespace ASC.Data.Backup
if (restoreRequest.StorageType == BackupStorageType.Local && !CoreBaseSettings.Standalone)
{
restoreRequest.FilePathOrId = BackupFileUploadHandler.GetFilePath();
restoreRequest.FilePathOrId = GetTmpFilePath();
}
}
@ -280,7 +283,7 @@ namespace ASC.Data.Backup
return result;
}
private void DemandPermissionsRestore()
public void DemandPermissionsRestore()
{
PermissionContext.DemandPermissions(SecutiryConstants.EditPortalSettings);
@ -289,6 +292,15 @@ namespace ASC.Data.Backup
throw new BillingException(Resource.ErrorNotAllowedOption, "Restore");
}
public void DemandPermissionsAutoBackup()
{
PermissionContext.DemandPermissions(SecutiryConstants.EditPortalSettings);
if (!SetupInfo.IsVisibleSettings("AutoBackup") ||
(!CoreBaseSettings.Standalone && !TenantManager.GetTenantQuota(TenantManager.GetCurrentTenant().TenantId).AutoBackup))
throw new BillingException(Resource.ErrorNotAllowedOption, "AutoBackup");
}
#endregion
#region transfer
@ -342,6 +354,18 @@ namespace ASC.Data.Backup
return TenantManager.GetCurrentTenant().TenantId;
}
public string GetTmpFilePath()
{
var folder = Path.Combine(TempPath.GetTempPath(), BackupTempFolder, TenantManager.GetCurrentTenant().TenantId.ToString());
if (!Directory.Exists(folder))
{
Directory.CreateDirectory(folder);
}
return Path.Combine(folder, BackupFileName);
}
public class Schedule
{
public BackupStorageType StorageType { get; set; }

View File

@ -17,55 +17,52 @@
using System;
using System.IO;
using System.Text.Json;
using System.Threading.Tasks;
using ASC.Common;
using ASC.Core;
using ASC.Web.Core.Utility;
using ASC.Web.Studio.Core;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
namespace ASC.Web.Studio.Core.Backup
namespace ASC.Data.Backup
{
[Scope]
public class BackupFileUploadHandler
{
private const long MaxBackupFileSize = 1024L * 1024L * 1024L;
private const string BackupTempFolder = "backup";
private const string BackupFileName = "backup.tmp";
private PermissionContext PermissionContext { get; }
private TempPath TempPath { get; }
private TenantManager TenantManager { get; }
public BackupFileUploadHandler(
PermissionContext permissionContext,
TempPath tempPath,
TenantManager tenantManager)
public BackupFileUploadHandler(RequestDelegate next)
{
PermissionContext = permissionContext;
TempPath = tempPath;
TenantManager = tenantManager;
}
public string ProcessUpload(IFormFile file)
public async Task Invoke(HttpContext context,
PermissionContext permissionContext,
BackupAjaxHandler backupAjaxHandler)
{
if (file == null)
FileUploadResult result = null;
try
{
return "No files.";
if (context.Request.Form.Files.Count == 0)
{
result = Error("No files.");
}
if (!PermissionContext.CheckPermissions(SecutiryConstants.EditPortalSettings))
if (!permissionContext.CheckPermissions(SecutiryConstants.EditPortalSettings))
{
return "Access denied.";
result = Error("Access denied.");
}
var file = context.Request.Form.Files[0];
if (file.Length <= 0 || file.Length > MaxBackupFileSize)
{
return $"File size must be greater than 0 and less than {MaxBackupFileSize} bytes";
result = Error($"File size must be greater than 0 and less than {MaxBackupFileSize} bytes");
}
try
{
var filePath = GetFilePath();
var filePath = backupAjaxHandler.GetTmpFilePath();
if (File.Exists(filePath))
{
@ -74,27 +71,42 @@ namespace ASC.Web.Studio.Core.Backup
using (var fileStream = File.Create(filePath))
{
file.CopyTo(fileStream);
await file.CopyToAsync(fileStream);
}
return string.Empty;
result = Success();
}
catch (Exception error)
{
return error.Message;
result = Error(error.Message);
}
await context.Response.WriteAsync(JsonSerializer.Serialize(result));
}
internal string GetFilePath()
private FileUploadResult Success()
{
var folder = Path.Combine(TempPath.GetTempPath(), BackupTempFolder, TenantManager.GetCurrentTenant().TenantId.ToString());
if (!Directory.Exists(folder))
return new FileUploadResult
{
Directory.CreateDirectory(folder);
}
Success = true
};
}
return Path.Combine(folder, BackupFileName);
private FileUploadResult Error(string messageFormat, params object[] args)
{
return new FileUploadResult
{
Success = false,
Message = string.Format(messageFormat, args)
};
}
}
public static class BackupFileUploadHandlerExtensions
{
public static IApplicationBuilder UseBackupFileUploadHandler(this IApplicationBuilder builder)
{
return builder.UseMiddleware<BackupFileUploadHandler>();
}
}
}

View File

@ -1,247 +1,255 @@
/*
*
* (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.
*
*/
using System;
using System.Collections.Generic;
using System.Linq;
using ASC.Common;
using ASC.Core;
using ASC.Core.Tenants;
using ASC.Core.Users;
using ASC.Data.Backup.Core;
using ASC.Notify.Model;
using ASC.Notify.Patterns;
using ASC.Notify.Recipients;
using ASC.Web.Core.Users;
using ASC.Web.Core.WhiteLabel;
using ASC.Web.Studio.Core.Notify;
using ASC.Web.Studio.Utility;
using Microsoft.Extensions.DependencyInjection;
namespace ASC.Data.Backup
{
[Singletone(Additional = typeof(NotifyHelperExtension))]
public class NotifyHelper
{
private IServiceProvider ServiceProvider { get; }
public NotifyHelper(IServiceProvider serviceProvider)
{
ServiceProvider = serviceProvider;
}
public void SendAboutTransferStart(Tenant tenant, string targetRegion, bool notifyUsers)
{
MigrationNotify(tenant, Actions.MigrationPortalStart, targetRegion, string.Empty, notifyUsers);
}
public void SendAboutTransferComplete(Tenant tenant, string targetRegion, string targetAddress, bool notifyOnlyOwner, int toTenantId)
{
MigrationNotify(tenant, Actions.MigrationPortalSuccessV115, targetRegion, targetAddress, !notifyOnlyOwner, toTenantId);
}
public void SendAboutTransferError(Tenant tenant, string targetRegion, string resultAddress, bool notifyOnlyOwner)
{
MigrationNotify(tenant, !string.IsNullOrEmpty(targetRegion) ? Actions.MigrationPortalError : Actions.MigrationPortalServerFailure, targetRegion, resultAddress, !notifyOnlyOwner);
}
public void SendAboutBackupCompleted(Guid userId)
{
using var scope = ServiceProvider.CreateScope();
var scopeClass = scope.ServiceProvider.GetService<NotifyHelperScope>();
var (userManager, studioNotifyHelper, studioNotifySource, displayUserSettingsHelper, _) = scopeClass;
var client = WorkContext.NotifyContext.NotifyService.RegisterClient(studioNotifySource, scope);
client.SendNoticeToAsync(
Actions.BackupCreated,
new[] { studioNotifyHelper.ToRecipient(userId) },
new[] { StudioNotifyService.EMailSenderName },
new TagValue(Tags.OwnerName, userManager.GetUsers(userId).DisplayUserName(displayUserSettingsHelper)));
}
public void SendAboutRestoreStarted(Tenant tenant, bool notifyAllUsers)
{
using var scope = ServiceProvider.CreateScope();
var scopeClass = scope.ServiceProvider.GetService<NotifyHelperScope>();
var (userManager, studioNotifyHelper, studioNotifySource, displayUserSettingsHelper, _) = scopeClass;
var client = WorkContext.NotifyContext.NotifyService.RegisterClient(studioNotifySource, scope);
var owner = userManager.GetUsers(tenant.OwnerId);
var users =
notifyAllUsers
? studioNotifyHelper.RecipientFromEmail(userManager.GetUsers(EmployeeStatus.Active).Where(r => r.ActivationStatus == EmployeeActivationStatus.Activated).Select(u => u.Email).ToList(), false)
: owner.ActivationStatus == EmployeeActivationStatus.Activated ? studioNotifyHelper.RecipientFromEmail(owner.Email, false) : new IDirectRecipient[0];
client.SendNoticeToAsync(
Actions.RestoreStarted,
users,
new[] { StudioNotifyService.EMailSenderName });
}
public void SendAboutRestoreCompleted(Tenant tenant, bool notifyAllUsers)
{
using var scope = ServiceProvider.CreateScope();
var scopeClass = scope.ServiceProvider.GetService<NotifyHelperScope>();
var tenantManager = scope.ServiceProvider.GetService<TenantManager>();
var commonLinkUtility = scope.ServiceProvider.GetService<CommonLinkUtility>();
var (userManager, studioNotifyHelper, studioNotifySource, displayUserSettingsHelper, authManager) = scopeClass;
var client = WorkContext.NotifyContext.NotifyService.RegisterClient(studioNotifySource, scope);
var users = notifyAllUsers
? userManager.GetUsers(EmployeeStatus.Active)
: new[] { userManager.GetUsers(tenantManager.GetCurrentTenant().OwnerId) };
foreach (var user in users)
{
var hash = authManager.GetUserPasswordStamp(user.ID).ToString("s");
var confirmationUrl = commonLinkUtility.GetConfirmationUrl(user.Email, ConfirmType.PasswordChange, hash);
Func<string> greenButtonText = () => BackupResource.ButtonSetPassword;
client.SendNoticeToAsync(
Actions.RestoreCompletedV115,
new IRecipient[] { user },
new[] { StudioNotifyService.EMailSenderName },
null,
TagValues.GreenButton(greenButtonText, confirmationUrl));
}
}
private void MigrationNotify(Tenant tenant, INotifyAction action, string region, string url, bool notify, int? toTenantId = null)
{
using var scope = ServiceProvider.CreateScope();
var scopeClass = scope.ServiceProvider.GetService<NotifyHelperScope>();
var (userManager, studioNotifyHelper, studioNotifySource, _, authManager) = scopeClass;
var client = WorkContext.NotifyContext.NotifyService.RegisterClient(studioNotifySource, scope);
var commonLinkUtility = scope.ServiceProvider.GetService<CommonLinkUtility>();
var users = userManager.GetUsers()
.Where(u => notify ? u.ActivationStatus.HasFlag(EmployeeActivationStatus.Activated) : u.IsOwner(tenant))
.ToArray();
if (users.Length > 0)
{
var args = CreateArgs(scope, region, url);
if (action == Actions.MigrationPortalSuccessV115)
{
foreach (var user in users)
{
var currentArgs = new List<ITagValue>(args);
var newTenantId = toTenantId.HasValue ? toTenantId.Value : tenant.TenantId;
var hash = authManager.GetUserPasswordStamp(user.ID).ToString("s");
var confirmationUrl = url + "/" + commonLinkUtility.GetConfirmationUrlRelative(newTenantId, user.Email, ConfirmType.PasswordChange, hash);
Func<string> greenButtonText = () => BackupResource.ButtonSetPassword;
currentArgs.Add(TagValues.GreenButton(greenButtonText, confirmationUrl));
client.SendNoticeToAsync(
action,
null,
new IRecipient[] { user },
new[] { StudioNotifyService.EMailSenderName },
currentArgs.ToArray());
}
}
else
{
client.SendNoticeToAsync(
action,
null,
users.Select(u => studioNotifyHelper.ToRecipient(u.ID)).ToArray(),
new[] { StudioNotifyService.EMailSenderName },
args.ToArray());
}
}
}
private List<ITagValue> CreateArgs(IServiceScope scope,string region, string url)
{
var args = new List<ITagValue>()
{
new TagValue(Tags.RegionName, TransferResourceHelper.GetRegionDescription(region)),
new TagValue(Tags.PortalUrl, url)
};
if (!string.IsNullOrEmpty(url))
{
args.Add(new TagValue(CommonTags.VirtualRootPath, url));
args.Add(new TagValue(CommonTags.ProfileUrl, url + scope.ServiceProvider.GetService<CommonLinkUtility>().GetMyStaff()));
args.Add(new TagValue(CommonTags.LetterLogo, scope.ServiceProvider.GetService<TenantLogoManager>().GetLogoDark(true)));
}
return args;
}
}
[Scope]
public class NotifyHelperScope
{
private AuthManager AuthManager { get; }
private UserManager UserManager { get; }
private StudioNotifyHelper StudioNotifyHelper { get; }
private StudioNotifySource StudioNotifySource { get; }
private DisplayUserSettingsHelper DisplayUserSettingsHelper { get; }
public NotifyHelperScope(
UserManager userManager,
StudioNotifyHelper studioNotifyHelper,
StudioNotifySource studioNotifySource,
DisplayUserSettingsHelper displayUserSettingsHelper,
AuthManager authManager)
{
UserManager = userManager;
StudioNotifyHelper = studioNotifyHelper;
StudioNotifySource = studioNotifySource;
DisplayUserSettingsHelper = displayUserSettingsHelper;
AuthManager = authManager;
}
public void Deconstruct(
out UserManager userManager,
out StudioNotifyHelper studioNotifyHelper,
out StudioNotifySource studioNotifySource,
out DisplayUserSettingsHelper displayUserSettingsHelper,
out AuthManager authManager
)
{
userManager = UserManager;
studioNotifyHelper = StudioNotifyHelper;
studioNotifySource = StudioNotifySource;
displayUserSettingsHelper = DisplayUserSettingsHelper;
authManager = AuthManager;
}
}
public static class NotifyHelperExtension
{
public static void Register(DIHelper services)
{
services.TryAdd<NotifyHelperScope>();
}
}
}
/*
*
* (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.
*
*/
using System;
using System.Collections.Generic;
using System.Linq;
using ASC.Common;
using ASC.Core;
using ASC.Core.Tenants;
using ASC.Core.Users;
using ASC.Data.Backup.Core;
using ASC.Notify.Model;
using ASC.Notify.Patterns;
using ASC.Notify.Recipients;
using ASC.Web.Core.Users;
using ASC.Web.Core.WhiteLabel;
using ASC.Web.Studio.Core.Notify;
using ASC.Web.Studio.Utility;
using Microsoft.Extensions.DependencyInjection;
namespace ASC.Data.Backup
{
[Singletone(Additional = typeof(NotifyHelperExtension))]
public class NotifyHelper
{
private IServiceProvider ServiceProvider { get; }
public NotifyHelper(IServiceProvider serviceProvider)
{
ServiceProvider = serviceProvider;
}
public void SendAboutTransferStart(Tenant tenant, string targetRegion, bool notifyUsers)
{
MigrationNotify(tenant, Actions.MigrationPortalStart, targetRegion, string.Empty, notifyUsers);
}
public void SendAboutTransferComplete(Tenant tenant, string targetRegion, string targetAddress, bool notifyOnlyOwner, int toTenantId)
{
MigrationNotify(tenant, Actions.MigrationPortalSuccessV115, targetRegion, targetAddress, !notifyOnlyOwner, toTenantId);
}
public void SendAboutTransferError(Tenant tenant, string targetRegion, string resultAddress, bool notifyOnlyOwner)
{
MigrationNotify(tenant, !string.IsNullOrEmpty(targetRegion) ? Actions.MigrationPortalError : Actions.MigrationPortalServerFailure, targetRegion, resultAddress, !notifyOnlyOwner);
}
public void SendAboutBackupCompleted(int tenantId, Guid userId)
{
using var scope = ServiceProvider.CreateScope();
var scopeClass = scope.ServiceProvider.GetService<NotifyHelperScope>();
var (userManager, studioNotifyHelper, studioNotifySource, displayUserSettingsHelper, tenantManager, _) = scopeClass;
tenantManager.SetCurrentTenant(tenantId);
var client = WorkContext.NotifyContext.NotifyService.RegisterClient(studioNotifySource, scope);
client.SendNoticeToAsync(
Actions.BackupCreated,
new[] { studioNotifyHelper.ToRecipient(userId) },
new[] { StudioNotifyService.EMailSenderName },
new TagValue(Tags.OwnerName, userManager.GetUsers(userId).DisplayUserName(displayUserSettingsHelper)));
}
public void SendAboutRestoreStarted(Tenant tenant, bool notifyAllUsers)
{
using var scope = ServiceProvider.CreateScope();
var scopeClass = scope.ServiceProvider.GetService<NotifyHelperScope>();
var (userManager, studioNotifyHelper, studioNotifySource, displayUserSettingsHelper, tenantManager, _) = scopeClass;
tenantManager.SetCurrentTenant(tenant.TenantId);
var client = WorkContext.NotifyContext.NotifyService.RegisterClient(studioNotifySource, scope);
var owner = userManager.GetUsers(tenant.OwnerId);
var users =
notifyAllUsers
? studioNotifyHelper.RecipientFromEmail(userManager.GetUsers(EmployeeStatus.Active).Where(r => r.ActivationStatus == EmployeeActivationStatus.Activated).Select(u => u.Email).ToList(), false)
: owner.ActivationStatus == EmployeeActivationStatus.Activated ? studioNotifyHelper.RecipientFromEmail(owner.Email, false) : new IDirectRecipient[0];
client.SendNoticeToAsync(
Actions.RestoreStarted,
users,
new[] { StudioNotifyService.EMailSenderName });
}
public void SendAboutRestoreCompleted(Tenant tenant, bool notifyAllUsers)
{
using var scope = ServiceProvider.CreateScope();
var scopeClass = scope.ServiceProvider.GetService<NotifyHelperScope>();
var tenantManager = scope.ServiceProvider.GetService<TenantManager>();
var commonLinkUtility = scope.ServiceProvider.GetService<CommonLinkUtility>();
var (userManager, _, studioNotifySource, _, _, authManager) = scopeClass;
var client = WorkContext.NotifyContext.NotifyService.RegisterClient(studioNotifySource, scope);
var users = notifyAllUsers
? userManager.GetUsers(EmployeeStatus.Active)
: new[] { userManager.GetUsers(tenantManager.GetCurrentTenant().OwnerId) };
foreach (var user in users)
{
var hash = authManager.GetUserPasswordStamp(user.ID).ToString("s");
var confirmationUrl = commonLinkUtility.GetConfirmationUrl(user.Email, ConfirmType.PasswordChange, hash);
Func<string> greenButtonText = () => BackupResource.ButtonSetPassword;
client.SendNoticeToAsync(
Actions.RestoreCompletedV115,
new IRecipient[] { user },
new[] { StudioNotifyService.EMailSenderName },
null,
TagValues.GreenButton(greenButtonText, confirmationUrl));
}
}
private void MigrationNotify(Tenant tenant, INotifyAction action, string region, string url, bool notify, int? toTenantId = null)
{
using var scope = ServiceProvider.CreateScope();
var scopeClass = scope.ServiceProvider.GetService<NotifyHelperScope>();
var (userManager, studioNotifyHelper, studioNotifySource, _, _, authManager) = scopeClass;
var client = WorkContext.NotifyContext.NotifyService.RegisterClient(studioNotifySource, scope);
var commonLinkUtility = scope.ServiceProvider.GetService<CommonLinkUtility>();
var users = userManager.GetUsers()
.Where(u => notify ? u.ActivationStatus.HasFlag(EmployeeActivationStatus.Activated) : u.IsOwner(tenant))
.ToArray();
if (users.Length > 0)
{
var args = CreateArgs(scope, region, url);
if (action == Actions.MigrationPortalSuccessV115)
{
foreach (var user in users)
{
var currentArgs = new List<ITagValue>(args);
var newTenantId = toTenantId.HasValue ? toTenantId.Value : tenant.TenantId;
var hash = authManager.GetUserPasswordStamp(user.ID).ToString("s");
var confirmationUrl = url + "/" + commonLinkUtility.GetConfirmationUrlRelative(newTenantId, user.Email, ConfirmType.PasswordChange, hash);
Func<string> greenButtonText = () => BackupResource.ButtonSetPassword;
currentArgs.Add(TagValues.GreenButton(greenButtonText, confirmationUrl));
client.SendNoticeToAsync(
action,
null,
new IRecipient[] { user },
new[] { StudioNotifyService.EMailSenderName },
currentArgs.ToArray());
}
}
else
{
client.SendNoticeToAsync(
action,
null,
users.Select(u => studioNotifyHelper.ToRecipient(u.ID)).ToArray(),
new[] { StudioNotifyService.EMailSenderName },
args.ToArray());
}
}
}
private List<ITagValue> CreateArgs(IServiceScope scope,string region, string url)
{
var args = new List<ITagValue>()
{
new TagValue(Tags.RegionName, TransferResourceHelper.GetRegionDescription(region)),
new TagValue(Tags.PortalUrl, url)
};
if (!string.IsNullOrEmpty(url))
{
args.Add(new TagValue(CommonTags.VirtualRootPath, url));
args.Add(new TagValue(CommonTags.ProfileUrl, url + scope.ServiceProvider.GetService<CommonLinkUtility>().GetMyStaff()));
args.Add(new TagValue(CommonTags.LetterLogo, scope.ServiceProvider.GetService<TenantLogoManager>().GetLogoDark(true)));
}
return args;
}
}
[Scope]
public class NotifyHelperScope
{
private AuthManager AuthManager { get; }
private UserManager UserManager { get; }
private StudioNotifyHelper StudioNotifyHelper { get; }
private StudioNotifySource StudioNotifySource { get; }
private DisplayUserSettingsHelper DisplayUserSettingsHelper { get; }
private TenantManager TenantManager { get; }
public NotifyHelperScope(
UserManager userManager,
StudioNotifyHelper studioNotifyHelper,
StudioNotifySource studioNotifySource,
DisplayUserSettingsHelper displayUserSettingsHelper,
TenantManager tenantManager,
AuthManager authManager)
{
UserManager = userManager;
StudioNotifyHelper = studioNotifyHelper;
StudioNotifySource = studioNotifySource;
DisplayUserSettingsHelper = displayUserSettingsHelper;
TenantManager = tenantManager;
AuthManager = authManager;
}
public void Deconstruct(
out UserManager userManager,
out StudioNotifyHelper studioNotifyHelper,
out StudioNotifySource studioNotifySource,
out DisplayUserSettingsHelper displayUserSettingsHelper,
out TenantManager tenantManager,
out AuthManager authManager)
{
userManager = UserManager;
studioNotifyHelper = StudioNotifyHelper;
studioNotifySource = StudioNotifySource;
displayUserSettingsHelper = DisplayUserSettingsHelper;
tenantManager = TenantManager;
authManager = AuthManager;
}
}
public static class NotifyHelperExtension
{
public static void Register(DIHelper services)
{
services.TryAdd<NotifyHelperScope>();
}
}
}

View File

@ -17,13 +17,13 @@ namespace ASC.Data.Backup.EF.Context
public BackupsContext(DbContextOptions<BackupsContext> options)
: base(options)
{
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
ModelBuilderWrapper
.From(modelBuilder, Provider)
.AddDbTenant();
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
ModelBuilderWrapper
.From(modelBuilder, Provider)
.AddDbTenant();
}
}

View File

@ -122,7 +122,7 @@ namespace ASC.Data.Backup.Services
{
lock (SynchRoot)
{
var item = ProgressQueue.GetTasks<BackupProgressItem>().FirstOrDefault(t => t.TenantId == request.TenantId);
var item = ProgressQueue.GetTasks<BackupProgressItem>().FirstOrDefault(t => t.TenantId == request.TenantId && t.BackupProgressItemEnum == BackupProgressItemEnum.Backup);
if (item != null && item.IsCompleted)
{
ProgressQueue.RemoveTask(item.Id);
@ -144,7 +144,7 @@ namespace ASC.Data.Backup.Services
{
lock (SynchRoot)
{
var item = ProgressQueue.GetTasks<BackupProgressItem>().FirstOrDefault(t => t.TenantId == schedule.TenantId);
var item = ProgressQueue.GetTasks<BackupProgressItem>().FirstOrDefault(t => t.TenantId == schedule.TenantId && t.BackupProgressItemEnum == BackupProgressItemEnum.Backup);
if (item != null && item.IsCompleted)
{
ProgressQueue.RemoveTask(item.Id);
@ -162,7 +162,7 @@ namespace ASC.Data.Backup.Services
{
lock (SynchRoot)
{
return ToBackupProgress(ProgressQueue.GetTasks<BackupProgressItem>().FirstOrDefault(t => t.TenantId == tenantId));
return ToBackupProgress(ProgressQueue.GetTasks<BackupProgressItem>().FirstOrDefault(t => t.TenantId == tenantId && t.BackupProgressItemEnum == BackupProgressItemEnum.Backup));
}
}
@ -170,7 +170,7 @@ namespace ASC.Data.Backup.Services
{
lock (SynchRoot)
{
return ToBackupProgress(ProgressQueue.GetTasks<TransferProgressItem>().FirstOrDefault(t => t.TenantId == tenantId));
return ToBackupProgress(ProgressQueue.GetTasks<TransferProgressItem>().FirstOrDefault(t => t.TenantId == tenantId && t.BackupProgressItemEnum == BackupProgressItemEnum.Transfer));
}
}
@ -178,7 +178,7 @@ namespace ASC.Data.Backup.Services
{
lock (SynchRoot)
{
return ToBackupProgress(ProgressQueue.GetTasks<RestoreProgressItem>().FirstOrDefault(t => t.TenantId == tenantId));
return ToBackupProgress(ProgressQueue.GetTasks<RestoreProgressItem>().FirstOrDefault(t => t.TenantId == tenantId && t.BackupProgressItemEnum == BackupProgressItemEnum.Restore));
}
}
@ -262,16 +262,9 @@ namespace ASC.Data.Backup.Services
BackupProgressEnum = progressItem.BackupProgressItemEnum.Convert()
};
if (progressItem is BackupProgressItem backupProgressItem && backupProgressItem.Link != null)
if ((progressItem.BackupProgressItemEnum == BackupProgressItemEnum.Backup || progressItem.BackupProgressItemEnum == BackupProgressItemEnum.Transfer) && progressItem.Link != null)
{
progress.Link = backupProgressItem.Link;
}
else
{
if (progressItem is TransferProgressItem transferProgressItem && transferProgressItem.Link != null)
{
progress.Link = transferProgressItem.Link;
}
progress.Link = progressItem.Link;
}
return progress;
@ -312,22 +305,48 @@ namespace ASC.Data.Backup.Services
public abstract class BaseBackupProgressItem : DistributedTaskProgress
{
private int? tenantId;
private int? _tenantId;
public int TenantId
{
get
{
return tenantId ?? GetProperty<int>(nameof(tenantId));
return _tenantId ?? GetProperty<int>(nameof(_tenantId));
}
set
{
tenantId = value;
SetProperty(nameof(tenantId), value);
_tenantId = value;
SetProperty(nameof(_tenantId), value);
}
}
private string _link;
public string Link
{
get
{
return _link ?? GetProperty<string>(nameof(_link));
}
set
{
_link = value;
SetProperty(nameof(_link), value);
}
}
private BackupProgressItemEnum? backupProgressItemEnum;
public BackupProgressItemEnum BackupProgressItemEnum
{
get
{
return backupProgressItemEnum ?? GetProperty<BackupProgressItemEnum>(nameof(backupProgressItemEnum));
}
protected set
{
backupProgressItemEnum = value;
SetProperty(nameof(backupProgressItemEnum), value);
}
}
public abstract BackupProgressItemEnum BackupProgressItemEnum { get; }
public abstract object Clone();
protected ILog Log { get; set; }
@ -350,15 +369,12 @@ namespace ASC.Data.Backup.Services
{
}
public override BackupProgressItemEnum BackupProgressItemEnum { get => BackupProgressItemEnum.Backup; }
private bool IsScheduled { get; set; }
private Guid UserId { get; set; }
private BackupStorageType StorageType { get; set; }
private string StorageBasePath { get; set; }
public bool BackupMail { get; set; }
public Dictionary<string, string> StorageParams { get; set; }
public string Link { get; private set; }
public string TempFolder { get; set; }
private string CurrentRegion { get; set; }
private Dictionary<string, string> ConfigPaths { get; set; }
@ -376,7 +392,8 @@ namespace ASC.Data.Backup.Services
TempFolder = tempFolder;
Limit = limit;
CurrentRegion = currentRegion;
ConfigPaths = configPaths;
ConfigPaths = configPaths;
BackupProgressItemEnum = BackupProgressItemEnum.Backup;
}
public void Init(StartBackupRequest request, bool isScheduled, string tempFolder, int limit, string currentRegion, Dictionary<string, string> configPaths)
@ -456,7 +473,7 @@ namespace ASC.Data.Backup.Services
if (UserId != Guid.Empty && !IsScheduled)
{
notifyHelper.SendAboutBackupCompleted(UserId);
notifyHelper.SendAboutBackupCompleted(TenantId, UserId);
}
IsCompleted = true;
@ -506,7 +523,6 @@ namespace ASC.Data.Backup.Services
{
}
public override BackupProgressItemEnum BackupProgressItemEnum { get => BackupProgressItemEnum.Restore; }
public BackupStorageType StorageType { get; set; }
public string StoragePath { get; set; }
public bool Notify { get; set; }
@ -525,7 +541,9 @@ namespace ASC.Data.Backup.Services
TempFolder = tempFolder;
UpgradesPath = upgradesPath;
CurrentRegion = currentRegion;
ConfigPaths = configPaths;
ConfigPaths = configPaths;
BackupProgressItemEnum = BackupProgressItemEnum.Restore;
StorageParams = request.StorageParams;
}
protected override void DoJob()
@ -539,7 +557,10 @@ namespace ASC.Data.Backup.Services
{
tenant = tenantManager.GetTenant(TenantId);
tenantManager.SetCurrentTenant(tenant);
notifyHelper.SendAboutRestoreStarted(tenant, Notify);
notifyHelper.SendAboutRestoreStarted(tenant, Notify);
tenant.SetStatus(TenantStatus.Restoring);
tenantManager.SaveTenant(tenant);
var storage = backupStorageFactory.GetBackupStorage(StorageType, TenantId, StorageParams);
storage.Download(StoragePath, tempFile);
@ -555,9 +576,6 @@ namespace ASC.Data.Backup.Services
Percentage = 10;
tenant.SetStatus(TenantStatus.Restoring);
tenantManager.SaveTenant(tenant);
var columnMapper = new ColumnMapper();
columnMapper.SetMapping("tenants_tenants", "alias", tenant.TenantAlias, Guid.Parse(Id).ToString("N"));
columnMapper.Commit();
@ -627,7 +645,8 @@ namespace ASC.Data.Backup.Services
}
}
finally
{
{
IsCompleted = true;
try
{
PublishChanges();
@ -659,12 +678,9 @@ namespace ASC.Data.Backup.Services
{
}
public override BackupProgressItemEnum BackupProgressItemEnum { get => BackupProgressItemEnum.Transfer; }
public string TargetRegion { get; set; }
public bool TransferMail { get; set; }
public bool Notify { get; set; }
public string Link { get; set; }
public string TempFolder { get; set; }
public Dictionary<string, string> ConfigPaths { get; set; }
public string CurrentRegion { get; set; }
@ -687,8 +703,8 @@ namespace ASC.Data.Backup.Services
TempFolder = tempFolder;
ConfigPaths = configPaths;
CurrentRegion = currentRegion;
Limit = limit;
Limit = limit;
BackupProgressItemEnum = BackupProgressItemEnum.Restore;
}
protected override void DoJob()
@ -729,7 +745,8 @@ namespace ASC.Data.Backup.Services
notifyHelper.SendAboutTransferError(tenant, TargetRegion, Link, !Notify);
}
finally
{
{
IsCompleted = true;
try
{
PublishChanges();

View File

@ -54,33 +54,40 @@ namespace ASC.Data.Backup.Storage
{
using var stream = File.OpenRead(localPath);
var storagePath = Path.GetFileName(localPath);
Store.SaveAsync(Domain, storagePath, stream, ACL.Private).Wait();
Store.SaveAsync(Domain, storagePath, stream, ACL.Private).Wait();
return storagePath;
}
public void Download(string storagePath, string targetLocalPath)
{
using var source = Store.GetReadStreamAsync(Domain, storagePath).Result;
using var source = Store.GetReadStreamAsync(Domain, storagePath).Result;
using var destination = File.OpenWrite(targetLocalPath);
source.CopyTo(destination);
}
public void Delete(string storagePath)
{
if (Store.IsFileAsync(Domain, storagePath).Result)
if (Store.IsFileAsync(Domain, storagePath).Result)
{
Store.DeleteAsync(Domain, storagePath).Wait();
Store.DeleteAsync(Domain, storagePath).Wait();
}
}
public bool IsExists(string storagePath)
{
return Store.IsFileAsync(Domain, storagePath).Result;
{
if (Store != null)
{
return Store.IsFileAsync(Domain, storagePath).Result;
}
else
{
return false;
}
}
public string GetPublicLink(string storagePath)
{
return Store.GetInternalUriAsync(Domain, storagePath, TimeSpan.FromDays(1), null).Result.AbsoluteUri;
return Store.GetInternalUriAsync(Domain, storagePath, TimeSpan.FromDays(1), null).Result.AbsoluteUri;
}
}
}

View File

@ -62,6 +62,7 @@ namespace ASC.Data.Backup.Tasks.Data
IdColumn = idColumn;
IdType = idType;
TenantColumn = tenantColumn;
UserIDColumns = new string[0];
DateColumns = new Dictionary<string, bool>();
InsertMethod = InsertMethod.Insert;
}

View File

@ -136,6 +136,7 @@ namespace ASC.Data.Storage.Configuration
private SettingsManager SettingsManager { get; }
private IHttpContextAccessor HttpContextAccessor { get; }
private ConsumerFactory ConsumerFactory { get; }
private IServiceProvider ServiceProvider { get; }
public StorageSettingsHelper(
BaseStorageSettingsListener baseStorageSettingsListener,
@ -145,7 +146,8 @@ namespace ASC.Data.Storage.Configuration
IOptionsMonitor<ILog> options,
TenantManager tenantManager,
SettingsManager settingsManager,
ConsumerFactory consumerFactory)
ConsumerFactory consumerFactory,
IServiceProvider serviceProvider)
{
baseStorageSettingsListener.Subscribe();
StorageFactoryConfig = storageFactoryConfig;
@ -155,6 +157,7 @@ namespace ASC.Data.Storage.Configuration
TenantManager = tenantManager;
SettingsManager = settingsManager;
ConsumerFactory = consumerFactory;
ServiceProvider = serviceProvider;
}
public StorageSettingsHelper(
BaseStorageSettingsListener baseStorageSettingsListener,
@ -165,8 +168,9 @@ namespace ASC.Data.Storage.Configuration
TenantManager tenantManager,
SettingsManager settingsManager,
IHttpContextAccessor httpContextAccessor,
ConsumerFactory consumerFactory)
: this(baseStorageSettingsListener, storageFactoryConfig, pathUtils, cache, options, tenantManager, settingsManager, consumerFactory)
ConsumerFactory consumerFactory,
IServiceProvider serviceProvider)
: this(baseStorageSettingsListener, storageFactoryConfig, pathUtils, cache, options, tenantManager, settingsManager, consumerFactory, serviceProvider)
{
HttpContextAccessor = httpContextAccessor;
}
@ -219,8 +223,7 @@ namespace ASC.Data.Storage.Configuration
if (DataStoreConsumer(baseStorageSettings).HandlerType == null) return null;
return dataStore = ((IDataStore)
Activator.CreateInstance(DataStoreConsumer(baseStorageSettings).HandlerType, TenantManager, PathUtils, HttpContextAccessor, Options))
return dataStore = ((IDataStore)ServiceProvider.GetService(DataStoreConsumer(baseStorageSettings).HandlerType))
.Configure(TenantManager.GetCurrentTenant().TenantId.ToString(), null, null, DataStoreConsumer(baseStorageSettings));
}
}

View File

@ -220,6 +220,12 @@ namespace ASC.Data.Storage.DiscStorage
}
#region chunking
public override Task<string> InitiateChunkedUploadAsync(string domain, string path)
{
var target = GetTarget(domain, path);
CreateDirectory(target);
return Task.FromResult(target);
}
public override async Task<string> UploadChunkAsync(string domain, string path, string uploadId, Stream stream, long defaultChunkSize, int chunkNumber, long chunkLength)
{

View File

@ -88,6 +88,12 @@
logger.info(`refresh folder ${folderId} in room ${room}`);
socket.to(room).emit("refresh-folder", folderId);
});
socket.on("restore-backup", () => {
const room = getRoom("backup-restore");
logger.info(`restore backup in room ${room}`);
socket.to(room).emit("restore-backup");
});
});
function startEdit({ fileId, room } = {}) {
@ -108,7 +114,7 @@
logger.info(`create new file ${fileId} in room ${room}`);
modifyFolder(room, "create", fileId, "file", data);
}
function deleteFile({ fileId, room } = {}) {
logger.info(`delete file ${fileId} in room ${room}`);
modifyFolder(room, "delete", fileId, "file");

View File

@ -250,4 +250,42 @@ public class BackupController
return _backupHandler.GetTmpFolder();
}
///<visible>false</visible>
[Read("enablerestore")]
public bool EnableRestore()
{
try
{
if (_coreBaseSettings.Standalone)
{
_tenantExtra.DemandControlPanelPermission();
}
_backupHandler.DemandPermissionsRestore();
return true;
}
catch
{
return false;
}
}
///<visible>false</visible>
[Read("enableAutoBackup")]
public bool EnableAutoBackup()
{
try
{
if (_coreBaseSettings.Standalone)
{
_tenantExtra.DemandControlPanelPermission();
}
_backupHandler.DemandPermissionsAutoBackup();
return true;
}
catch
{
return false;
}
}
}

View File

@ -60,4 +60,16 @@ public class Startup : BaseStartup
services.AddHostedService<BackupListenerService>();
services.AddHostedService<BackupWorkerService>();
}
public override void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
base.Configure(app, env);
app.MapWhen(
context => context.Request.Path.ToString().EndsWith("backupFileUpload.ashx"),
appBranch =>
{
appBranch.UseBackupFileUploadHandler();
});
}
}

View File

@ -1,4 +1,5 @@
{
"kafka": {
"BootstrapServers": "localhost:9092"
}
}

View File

@ -201,6 +201,11 @@ server {
proxy_set_header X-REWRITER-URL $X_REWRITER_URL;
}
location /backupFileUpload.ashx {
proxy_pass http://localhost:5012;
proxy_set_header X-REWRITER-URL $X_REWRITER_URL;
}
location /products {
location ~* /people {
#rewrite products/people/(.*) /$1 break;

View File

@ -55,6 +55,109 @@ export function getInvitationLinks() {
);
}
export function startBackup(storageType, storageParams, backupMail = false) {
const options = {
method: "post",
url: `/portal/startbackup`,
data: {
storageType,
storageParams: storageParams,
backupMail,
},
};
return request(options);
}
export function getBackupProgress() {
const options = {
method: "get",
url: "/portal/getbackupprogress",
};
return request(options);
}
export function deleteBackupSchedule() {
const options = {
method: "delete",
url: "/portal/deletebackupschedule",
};
return request(options);
}
export function getBackupSchedule() {
const options = {
method: "get",
url: "/portal/getbackupschedule",
};
return request(options);
}
export function createBackupSchedule(
storageType,
storageParams,
backupsStored,
Period,
Hour,
Day = null,
backupMail = false
) {
const cronParams = {
Period: Period,
Hour: Hour,
Day: Day,
};
const options = {
method: "post",
url: "/portal/createbackupschedule",
data: {
storageType,
storageParams,
backupsStored,
cronParams: cronParams,
backupMail,
},
};
return request(options);
}
export function deleteBackupHistory() {
return request({ method: "delete", url: "/portal/deletebackuphistory" });
}
export function deleteBackup(id) {
return request({ method: "delete", url: `/portal/deletebackup/${id}` });
}
export function getBackupHistory() {
return request({ method: "get", url: "/portal/getbackuphistory" });
}
export function startRestore(backupId, storageType, storageParams, notify) {
return request({
method: "post",
url: `/portal/startrestore`,
data: {
backupId,
storageType,
storageParams: storageParams,
notify,
},
});
}
export function getRestoreProgress() {
return request({ method: "get", url: "/portal/getrestoreprogress" });
}
export function enableRestore() {
return request({ method: "get", url: "/portal/enablerestore" });
}
export function enableAutoBackup() {
return request({ method: "get", url: "/portal/enableAutoBackup" });
}
export function setPortalRename(alias) {
return request({
method: "put",

View File

@ -417,6 +417,13 @@ export function getCommonThirdPartyList() {
};
return request(options);
}
export function getBackupStorage() {
const options = {
method: "get",
url: "/settings/storage/backup",
};
return request(options);
}
export function getBuildVersion() {
const options = {

View File

@ -1,11 +1,55 @@
import React from "react";
import PropTypes from "prop-types";
import styled, { css } from "styled-components";
import { isMobileOnly } from "react-device-detect";
import { mobile } from "@appserver/components/utils/device";
import { Base } from "@appserver/components/themes";
import Selector from "./sub-components/Selector";
import Backdrop from "@appserver/components/backdrop";
import Aside from "@appserver/components/aside";
const sizes = ["compact", "full"];
const mobileView = css`
top: 64px;
width: 100vw !important;
height: calc(100vh - 64px) !important;
`;
const StyledBlock = styled.div`
position: fixed;
top: 0;
right: 0;
width: 480px;
max-width: 100vw;
height: 100vh;
z-index: 400;
display: flex;
flex-direction: column;
background: ${(props) => props.theme.filterInput.filter.background};
@media ${mobile} {
${mobileView}
}
${isMobileOnly && mobileView}
.people-selector {
height: 100%;
width: 100%;
.selector-wrapper,
.column-options {
width: 100%;
}
}
`;
StyledBlock.defaultProps = { theme: Base };
class AdvancedSelector extends React.Component {
constructor(props) {
@ -21,34 +65,30 @@ class AdvancedSelector extends React.Component {
};
render() {
const {
isOpen,
id,
className,
style,
withoutAside,
isDefaultDisplayDropDown,
smallSectionWidth,
} = this.props;
const { isOpen, id, className, style, withoutAside } = this.props;
return (
<div id={id} className={className} style={style}>
{withoutAside ? (
<Selector {...this.props} />
) : (
<>
<Backdrop
onClick={this.onClose}
visible={isOpen}
zIndex={310}
isAside={true}
/>
<Aside visible={isOpen} scale={false} className="aside-container">
<>
{isOpen && (
<div id={id} className={className} style={style}>
{withoutAside ? (
<Selector {...this.props} />
</Aside>
</>
) : (
<>
<Backdrop
onClick={this.onClose}
visible={isOpen}
zIndex={310}
isAside={true}
/>
<StyledBlock>
<Selector {...this.props} />
</StyledBlock>
</>
)}
</div>
)}
</div>
</>
);
}
}
@ -67,8 +107,6 @@ AdvancedSelector.propTypes = {
selectAllLabel: PropTypes.string,
buttonLabel: PropTypes.string,
size: PropTypes.oneOf(sizes),
maxHeight: PropTypes.number,
isMultiSelect: PropTypes.bool,

View File

@ -1,26 +0,0 @@
import React from "react";
import PropTypes from "prop-types";
import StyledBody from "./StyledBody";
class Body extends React.Component {
constructor(props) {
super(props);
}
render() {
const { children, className, style } = this.props;
return (
<StyledBody className={className} style={style}>
{children}
</StyledBody>
);
}
}
Body.propTypes = {
children: PropTypes.any,
className: PropTypes.string,
style: PropTypes.object,
};
export default Body;

View File

@ -1,27 +0,0 @@
import React from "react";
import PropTypes from "prop-types";
import StyledColumn from "./StyledColumn";
class Column extends React.Component {
constructor(props) {
super(props);
}
render() {
const { children, className, style, size } = this.props;
return (
<StyledColumn className={className} style={style} size={size}>
{children}
</StyledColumn>
);
}
}
Column.propTypes = {
children: PropTypes.any,
className: PropTypes.string,
style: PropTypes.object,
size: PropTypes.oneOf(["compact", "full"]),
};
export default Column;

View File

@ -0,0 +1,62 @@
import React from "react";
import Avatar from "@appserver/components/avatar";
import Text from "@appserver/components/text";
import Checkbox from "@appserver/components/checkbox";
const Group = ({ data, style, index }) => {
const { groupList, isMultiSelect, onGroupClick } = data;
const { label, avatarUrl, total, selectedCount } = groupList[index];
const isIndeterminate = selectedCount > 0 && selectedCount !== total;
const isChecked = total !== 0 && total === selectedCount;
let groupLabel = label;
if (isMultiSelect && selectedCount > 0) {
groupLabel = `${label} (${selectedCount})`;
}
const onGroupClickAction = React.useCallback(() => {
onGroupClick && onGroupClick(index);
}, []);
return (
<div
style={style}
className="row-option"
name={`selector-row-option-${index}`}
onClick={onGroupClickAction}
>
<div className="option-info">
<Avatar
className="option-avatar"
role="user"
size="min"
source={avatarUrl}
userName={label}
/>
<Text
className="option-text option-text__group"
truncate={true}
noSelect={true}
fontSize="14px"
>
{groupLabel}
</Text>
</div>
{isMultiSelect && (
<Checkbox
value={`${index}`}
isChecked={isChecked}
isIndeterminate={isIndeterminate}
className="option-checkbox"
/>
)}
</div>
);
};
export default React.memo(Group);

View File

@ -0,0 +1,59 @@
import React from "react";
import Avatar from "@appserver/components/avatar";
import Text from "@appserver/components/text";
import Checkbox from "@appserver/components/checkbox";
const GroupHeader = ({
avatarUrl,
label,
selectedCount,
isMultiSelect,
onSelectAll,
isIndeterminate,
isChecked,
...rest
}) => {
const [groupLabel, setGroupLabel] = React.useState(label);
React.useEffect(() => {
if (isMultiSelect) {
selectedCount > 0
? setGroupLabel(`${label} (${selectedCount})`)
: setGroupLabel(`${label}`);
}
}, [selectedCount, isMultiSelect, label]);
return (
<>
<div className="row-option row-header">
<div className="option-info">
<Avatar
className="option-avatar"
role="user"
size="min"
source={avatarUrl}
userName={label}
/>
<Text
className="option-text option-text__header"
truncate={true}
noSelect={true}
fontSize="14px"
>
{groupLabel}
</Text>
</div>
{isMultiSelect && (
<Checkbox
isIndeterminate={isIndeterminate}
isChecked={isChecked}
onChange={onSelectAll}
className="option-checkbox"
/>
)}
</div>
</>
);
};
export default React.memo(GroupHeader);

View File

@ -0,0 +1,28 @@
import React from "react";
import { FixedSizeList as List } from "react-window";
import AutoSizer from "react-virtualized-auto-sizer";
import CustomScrollbarsVirtualList from "@appserver/components/scrollbar/custom-scrollbars-virtual-list";
import Group from "./Group";
const GroupList = ({ groupList, onGroupClick, isMultiSelect }) => {
return (
<AutoSizer>
{({ width, height }) => (
<List
className="options-list"
height={height - 8}
width={width + 8}
itemCount={groupList.length}
itemData={{ groupList, onGroupClick, isMultiSelect }}
itemSize={48}
outerElementType={CustomScrollbarsVirtualList}
>
{Group}
</List>
)}
</AutoSizer>
);
};
export default React.memo(GroupList);

View File

@ -1,27 +1,23 @@
import React from "react";
import PropTypes from "prop-types";
import StyledHeader from "./StyledHeader";
class Header extends React.Component {
constructor(props) {
super(props);
}
import Heading from "@appserver/components/heading";
import IconButton from "@appserver/components/icon-button";
render() {
const { children, className, style } = this.props;
return (
<StyledHeader className={className} style={style}>
{children}
</StyledHeader>
);
}
}
Header.propTypes = {
children: PropTypes.any,
className: PropTypes.string,
style: PropTypes.object,
displayType: PropTypes.oneOf(["dropdown", "aside"]),
const Header = ({ headerLabel, onArrowClickAction }) => {
return (
<div className="header">
<IconButton
iconName="/static/images/arrow.path.react.svg"
size="17"
isFill={true}
className="arrow-button"
onClick={onArrowClickAction}
/>
<Heading size="medium" truncate={true}>
{headerLabel.replace("()", "")}
</Heading>
</div>
);
};
export default Header;
export default React.memo(Header);

View File

@ -0,0 +1,107 @@
import React from "react";
import Avatar from "@appserver/components/avatar";
import Text from "@appserver/components/text";
import Checkbox from "@appserver/components/checkbox";
import Loader from "@appserver/components/loader";
const Option = ({
style,
isMultiSelect,
index,
isChecked,
avatarUrl,
label,
keyProp,
onOptionChange,
onLinkClick,
isLoader,
loadingLabel,
}) => {
const onOptionChangeAction = React.useCallback(() => {
onOptionChange && onOptionChange(index, isChecked);
}, [onOptionChange, index, isChecked]);
const onLinkClickAction = React.useCallback(() => {
onLinkClick && onLinkClick(index);
}, [onLinkClick, index]);
return isLoader ? (
<div style={style} className="row-option">
<div key="loader">
<Loader
type="oval"
size="16px"
style={{
display: "inline",
marginRight: "10px",
}}
/>
<Text as="span" noSelect={true}>
{loadingLabel}
</Text>
</div>
</div>
) : isMultiSelect ? (
<div
style={style}
className="row-option"
value={`${index}`}
name={`selector-row-option-${index}`}
onClick={onOptionChangeAction}
>
<div className="option-info">
<Avatar
className="option-avatar"
role="user"
size="min"
source={avatarUrl}
userName={label}
/>
<Text
className="option-text"
truncate={true}
noSelect={true}
fontSize="14px"
>
{label}
</Text>
</div>
<Checkbox
id={keyProp}
value={`${index}`}
isChecked={isChecked}
className="option-checkbox"
/>
</div>
) : (
<div
key={keyProp}
style={style}
className="row-option"
data-index={index}
name={`selector-row-option-${index}`}
onClick={onLinkClickAction}
>
<div className="option-info">
<Avatar
className="option-avatar"
role="user"
size="min"
source={avatarUrl}
userName={label}
/>
<Text
className="option-text"
truncate={true}
noSelect={true}
fontSize="14px"
>
{label}
</Text>
</div>
</div>
);
};
export default React.memo(Option);

View File

@ -0,0 +1,86 @@
import React from "react";
import { FixedSizeList as List } from "react-window";
import InfiniteLoader from "react-window-infinite-loader";
import AutoSizer from "react-virtualized-auto-sizer";
import CustomScrollbarsVirtualList from "@appserver/components/scrollbar/custom-scrollbars-virtual-list";
import Option from "./Option";
const OptionList = ({
listOptionsRef,
loadingLabel,
options,
isOptionChecked,
isMultiSelect,
onOptionChange,
onLinkClick,
isItemLoaded,
itemCount,
loadMoreItems,
}) => {
const renderOption = React.useCallback(
({ index, style }) => {
const isLoaded = isItemLoaded(index);
if (!isLoaded) {
return <Option isLoader={true} loadingLabel={loadingLabel} />;
}
const option = options[index];
const isChecked = isOptionChecked(option);
return (
<Option
index={index}
style={style}
{...option}
isChecked={isChecked}
onOptionChange={onOptionChange}
onLinkClick={onLinkClick}
isMultiSelect={isMultiSelect}
/>
);
},
[
options,
loadingLabel,
isMultiSelect,
isItemLoaded,
isOptionChecked,
onOptionChange,
onLinkClick,
]
);
return (
<AutoSizer>
{({ width, height }) => (
<InfiniteLoader
ref={listOptionsRef}
isItemLoaded={isItemLoaded}
itemCount={itemCount}
loadMoreItems={loadMoreItems}
>
{({ onItemsRendered, ref }) => (
<List
className="options-list"
height={height - 73}
itemCount={itemCount}
itemSize={48}
onItemsRendered={onItemsRendered}
ref={ref}
width={width + 8}
outerElementType={CustomScrollbarsVirtualList}
>
{renderOption}
</List>
)}
</InfiniteLoader>
)}
</AutoSizer>
);
};
export default React.memo(OptionList);

View File

@ -0,0 +1,29 @@
import React from "react";
import SearchInput from "@appserver/components/search-input";
const Search = ({
isDisabled,
searchPlaceHolderLabel,
searchValue,
onSearchChange,
onSearchReset,
}) => {
return (
<div className="header-options">
<SearchInput
className="options_searcher"
isDisabled={isDisabled}
size="base"
scale={true}
isNeedFilter={false}
placeholder={searchPlaceHolderLabel}
value={searchValue}
onChange={onSearchChange}
onClearSearch={onSearchReset}
/>
</div>
);
};
export default React.memo(Search);

View File

@ -1,23 +1,17 @@
import React, { useRef, useState, useEffect, useCallback } from "react";
import PropTypes from "prop-types";
import Column from "./Column";
import Footer from "./Footer";
import Header from "./Header";
import Body from "./Body";
import { FixedSizeList as List } from "react-window";
import InfiniteLoader from "react-window-infinite-loader";
import AutoSizer from "react-virtualized-auto-sizer";
import ReactTooltip from "react-tooltip";
import Avatar from "@appserver/components/avatar";
import Checkbox from "@appserver/components/checkbox";
import SearchInput from "@appserver/components/search-input";
import Loader from "@appserver/components/loader";
import Header from "./Header";
import Search from "./Search";
import GroupList from "./GroupList";
import GroupHeader from "./GroupHeader";
import OptionList from "./OptionList";
import Option from "./Option";
import Footer from "./Footer";
import Text from "@appserver/components/text";
import Tooltip from "@appserver/components/tooltip";
import Heading from "@appserver/components/heading";
import IconButton from "@appserver/components/icon-button";
import CustomScrollbarsVirtualList from "@appserver/components/scrollbar/custom-scrollbars-virtual-list";
import StyledSelector from "./StyledSelector";
@ -34,7 +28,7 @@ const convertGroup = (group) => {
key: group.key,
label: `${group.label} (${group.total})`,
total: group.total,
selected: 0,
selectedCount: 0,
};
};
@ -46,7 +40,6 @@ const getCurrentGroup = (items) => {
const Selector = (props) => {
const {
groups,
selectButtonLabel,
isDisabled,
isMultiSelect,
hasNextPage,
@ -55,22 +48,20 @@ const Selector = (props) => {
loadNextPage,
selectedOptions,
selectedGroups,
groupsHeaderLabel,
searchPlaceHolderLabel,
emptySearchOptionsLabel,
emptyOptionsLabel,
loadingLabel,
selectAllLabel,
onSelect,
getOptionTooltipContent,
onSearchChanged,
onGroupChanged,
size,
allowGroupSelection,
embeddedComponent,
showCounter,
onArrowClick,
headerLabel,
total,
} = props;
const listOptionsRef = useRef(null);
@ -81,15 +72,20 @@ const Selector = (props) => {
resetCache();
}, [searchValue, currentGroup, hasNextPage]);
const resetCache = useCallback(() => {
if (listOptionsRef && listOptionsRef.current) {
listOptionsRef.current.resetloadMoreItemsCache(true);
}
}, [listOptionsRef]);
const [selectedOptionList, setSelectedOptionList] = useState(
selectedOptions || []
);
const [selectedGroupList, setSelectedGroupList] = useState(
selectedGroups || []
);
const [searchValue, setSearchValue] = useState("");
const [groupList, setGroupList] = useState([]);
const [currentGroup, setCurrentGroup] = useState(
getCurrentGroup(convertGroups(groups))
);
@ -97,8 +93,64 @@ const Selector = (props) => {
const [groupHeader, setGroupHeader] = useState(null);
useEffect(() => {
if (groups.length === 1) setGroupHeader(groups[0]);
}, [groups]);
if (groups.length === 0) return;
const newGroupList = [...groups];
if (
groups.length === 1 &&
selectedOptions &&
selectedOptions.length === 0
) {
return setGroupHeader(newGroupList[0]);
}
if (selectedOptions && selectedOptions.length === 0) {
return setGroupList(newGroupList);
}
if (selectedOptions) {
newGroupList[0].selectedCount = selectedOptions.length;
if (groups.length === 1) return setGroupHeader(newGroupList[0]);
selectedOptions.forEach((option) => {
option.groups.forEach((group) => {
const groupIndex = newGroupList.findIndex(
(newGroup) => group === newGroup.id
);
if (groupIndex) {
newGroupList[groupIndex].selectedCount++;
}
});
});
}
if (groups.length === 1) return setGroupHeader(newGroupList[0]);
setGroupList(newGroupList);
}, [groups, selectedOptions]);
useEffect(() => {
if (total) {
setGroupHeader({ ...groupHeader, total: total });
const newGroupList = groupList;
newGroupList.find((group) => group.key === groupHeader.key).total = total;
setGroupList(newGroupList);
}
}, [total]);
const onSearchChange = useCallback(
(value) => {
setSearchValue(value);
onSearchChanged && onSearchChanged(value);
},
[onSearchChanged]
);
const onSearchReset = useCallback(() => {
onSearchChanged && onSearchChange("");
}, [onSearchChanged]);
// Every row is loaded except for our loading indicator row.
const isItemLoaded = useCallback(
@ -109,118 +161,65 @@ const Selector = (props) => {
);
const onOptionChange = useCallback(
(index, isChecked) => {
const option = options[index];
const newSelected = !isChecked
? [option, ...selectedOptionList]
: selectedOptionList.filter((el) => el.key !== option.key);
setSelectedOptionList(newSelected);
(idx, isChecked) => {
const indexList = Array.isArray(idx) ? idx : [idx];
if (!option.groups) return;
let newSelected = selectedOptionList;
let newGroupList = groupList;
let newGroupHeader = { ...groupHeader };
const newSelectedGroups = [];
const removedSelectedGroups = [];
indexList.forEach((index) => {
newGroupHeader.selectedCount = isChecked
? newGroupHeader.selectedCount - 1
: newGroupHeader.selectedCount + 1;
if (isChecked) {
option.groups.forEach((g) => {
let index = selectedGroupList.findIndex((sg) => sg.key === g);
if (index > -1) {
// exists
const selectedGroup = selectedGroupList[index];
const newSelected = selectedGroup.selected + 1;
newSelectedGroups.push(
Object.assign({}, selectedGroup, {
selected: newSelected,
})
);
} else {
index = groups.findIndex((sg) => sg.key === g);
if (index < 0) return;
const notSelectedGroup = convertGroup(groups[index]);
newSelectedGroups.push(
Object.assign({}, notSelectedGroup, {
selected: 1,
})
);
}
});
} else {
option.groups.forEach((g) => {
let index = selectedGroupList.findIndex((sg) => sg.key === g);
if (index > -1) {
// exists
const selectedGroup = selectedGroupList[index];
const newSelected = selectedGroup.selected - 1;
if (newSelected > 0) {
newSelectedGroups.push(
Object.assign({}, selectedGroup, {
selected: newSelected,
})
);
} else {
removedSelectedGroups.push(
Object.assign({}, selectedGroup, {
selected: newSelected,
})
);
}
}
});
}
const option = options[index];
selectedGroupList.forEach((g) => {
const indexNew = newSelectedGroups.findIndex((sg) => sg.key === g.key);
newSelected = !isChecked
? [option, ...newSelected]
: newSelected.filter((el) => el.key !== option.key);
if (indexNew === -1) {
const indexRemoved = removedSelectedGroups.findIndex(
(sg) => sg.key === g.key
if (!option.groups) {
setSelectedOptionList(newSelected);
setGroupHeader(newGroupHeader);
return;
}
newGroupList[0].selectedCount = isChecked
? newGroupList[0].selectedCount - 1
: newGroupList[0].selectedCount + 1;
option.groups.forEach((group) => {
const groupIndex = newGroupList.findIndex(
(item) => item.key === group
);
if (indexRemoved === -1) {
newSelectedGroups.push(g);
if (groupIndex > 0) {
newGroupList[groupIndex].selectedCount = isChecked
? newGroupList[groupIndex].selectedCount - 1
: newGroupList[groupIndex].selectedCount + 1;
}
}
});
});
setSelectedGroupList(newSelectedGroups);
setSelectedOptionList(newSelected);
setGroupList(newGroupList);
setGroupHeader(newGroupHeader);
},
[options, selectedOptionList, groups, selectedGroupList]
[options, groupList, selectedOptionList, groupHeader]
);
const resetCache = useCallback(() => {
if (listOptionsRef && listOptionsRef.current) {
listOptionsRef.current.resetloadMoreItemsCache(true);
}
}, [listOptionsRef]);
const onSearchChange = useCallback((value) => {
setSearchValue(value);
onSearchChanged && onSearchChanged(value);
});
const onSearchReset = useCallback(() => {
onSearchChanged && onSearchChange("");
});
const isOptionChecked = useCallback(
(option) => {
const checked =
selectedOptionList.findIndex((el) => el.key === option.key) > -1 ||
(option.groups &&
option.groups.filter((gKey) => {
const selectedGroup = selectedGroupList.find(
(sg) => sg.key === gKey
);
const checked = selectedOptionList.find(
(item) => item.key === option.key
);
if (!selectedGroup) return false;
return selectedGroup.total === selectedGroup.selected;
}).length > 0);
return checked;
return !!checked;
},
[selectedOptionList, selectedGroupList]
[selectedOptionList]
);
const onSelectOptions = (items) => {
onSelect && onSelect(items);
};
@ -240,136 +239,7 @@ const Selector = (props) => {
[options]
);
const renderOptionItem = useCallback(
(index, style, option, isChecked, tooltipProps) => {
return isMultiSelect ? (
<div
style={style}
className="row-option"
value={`${index}`}
name={`selector-row-option-${index}`}
onClick={() => onOptionChange(index, isChecked)}
{...tooltipProps}
>
<div className="option-info">
<Avatar
className="option-avatar"
role="user"
size="min"
source={option.avatarUrl}
userName={option.label}
/>
<Text
className="option-text"
truncate={true}
noSelect={true}
fontSize="14px"
>
{option.label}
</Text>
</div>
<Checkbox
id={option.key}
value={`${index}`}
isChecked={isChecked}
className="option-checkbox"
/>
</div>
) : (
<div
key={option.key}
style={style}
className="row-option"
data-index={index}
name={`selector-row-option-${index}`}
onClick={() => onLinkClick(index)}
{...tooltipProps}
>
<div className="option-info">
{" "}
<Avatar
className="option-avatar"
role="user"
size="min"
source={option.avatarUrl}
userName={option.label}
/>
<Text
className="option-text"
truncate={true}
noSelect={true}
fontSize="14px"
>
{option.label}
</Text>
</div>
</div>
);
},
[isMultiSelect, onOptionChange, onLinkClick]
);
const renderOptionLoader = useCallback(
(style) => {
return (
<div style={style} className="row-option">
<div key="loader">
<Loader
type="oval"
size="16px"
style={{
display: "inline",
marginRight: "10px",
}}
/>
<Text as="span" noSelect={true}>
{loadingLabel}
</Text>
</div>
</div>
);
},
[loadingLabel]
);
// Render an item or a loading indicator.
// eslint-disable-next-line react/prop-types
const renderOption = useCallback(
({ index, style }) => {
const isLoaded = isItemLoaded(index);
if (!isLoaded) {
return renderOptionLoader(style);
}
const option = options[index];
const isChecked = isOptionChecked(option);
let tooltipProps = {};
ReactTooltip.rebuild();
return renderOptionItem(index, style, option, isChecked, tooltipProps);
},
[
isItemLoaded,
renderOptionLoader,
renderOptionItem,
loadingLabel,
options,
isOptionChecked,
isMultiSelect,
onOptionChange,
onLinkClick,
getOptionTooltipContent,
]
);
const hasSelected = useCallback(() => {
return selectedOptionList.length > 0 || selectedGroupList.length > 0;
}, [selectedOptionList, selectedGroupList]);
// If there are more items to be loaded then add an extra row to hold a loading indicator.
const itemCount = hasNextPage ? options.length + 1 : options.length;
// Only load 1 page of items at a time.
// Pass an empty callback to InfiniteLoader in case it asks us to load more than once.
@ -388,153 +258,36 @@ const Selector = (props) => {
[isNextPageLoading, searchValue, currentGroup, options]
);
const getGroupSelectedOptions = useCallback(
(group) => {
const selectedGroup = selectedOptionList.filter(
(o) => o.groups && o.groups.indexOf(group) > -1
);
const onSelectAll = useCallback(() => {
const currentSelectedOption = [];
selectedOptionList.forEach((selectedOption) => {
options.forEach((option, idx) => {
if (option.key === selectedOption.key) currentSelectedOption.push(idx);
});
});
if (group === "all") {
selectedGroup.push(...selectedOptionList);
}
if (currentSelectedOption.length > 0) {
return onOptionChange(currentSelectedOption, true);
}
return selectedGroup;
},
[selectedOptionList]
);
onOptionChange(
options.map((item, index) => index),
false
);
}, [onOptionChange, selectedOptionList, options]);
const onGroupClick = useCallback(
(index) => {
const group = groups[index];
const group = groupList[index];
setGroupHeader({ ...group });
onGroupChanged && onGroupChanged(group);
setCurrentGroup(group);
},
[groups, onGroupChanged]
[groupList, onGroupChanged]
);
const renderGroup = useCallback(
({ index, style }) => {
const group = groups[index];
const selectedOption = getGroupSelectedOptions(group.id);
const isIndeterminate = selectedOption.length > 0;
let label = group.label;
if (isMultiSelect && selectedOption.length > 0) {
label = `${group.label} (${selectedOption.length})`;
}
return (
<div
style={style}
className="row-option"
name={`selector-row-option-${index}`}
onClick={() => onGroupClick(index)}
>
<div className="option-info">
<Avatar
className="option-avatar"
role="user"
size="min"
source={group.avatarUrl}
userName={group.label}
/>
<Text
className="option-text option-text__group"
truncate={true}
noSelect={true}
fontSize="14px"
>
{label}
</Text>
</div>
{isMultiSelect && (
<Checkbox
value={`${index}`}
isIndeterminate={isIndeterminate}
className="option-checkbox"
/>
)}
</div>
);
},
[
isMultiSelect,
groups,
currentGroup,
selectedGroupList,
selectedOptionList,
getGroupSelectedOptions,
]
);
const renderGroupsList = useCallback(() => {
if (groups.length === 0) return renderOptionLoader();
return (
<AutoSizer>
{({ width, height }) => (
<List
className="options_list"
height={height - 8}
width={width + 8}
itemCount={groups.length}
itemSize={48}
outerElementType={CustomScrollbarsVirtualList}
>
{renderGroup}
</List>
)}
</AutoSizer>
);
}, [isMultiSelect, groups, selectedOptionList, getGroupSelectedOptions]);
const renderGroupHeader = useCallback(() => {
const selectedOption = getGroupSelectedOptions(groupHeader.id);
const isIndeterminate = selectedOption.length > 0;
let label = groupHeader.label;
if (isMultiSelect && selectedOption.length > 0) {
label = `${groupHeader.label} (${selectedOption.length})`;
}
return (
<>
<div className="row-option row-header">
<div className="option-info">
<Avatar
className="option-avatar"
role="user"
size="min"
source={groupHeader.avatarUrl}
userName={groupHeader.label}
/>
<Text
className="option-text option-text__header"
truncate={true}
noSelect={true}
fontSize="14px"
>
{label}
</Text>
</div>
{isMultiSelect && (
<Checkbox
isIndeterminate={isIndeterminate}
className="option-checkbox"
/>
)}
</div>
<div className="option-separator"></div>
</>
);
}, [isMultiSelect, groupHeader, selectedOptionList, getGroupSelectedOptions]);
const onArrowClickAction = useCallback(() => {
if (groupHeader && groups.length !== 1) {
setGroupHeader(null);
@ -543,50 +296,68 @@ const Selector = (props) => {
setCurrentGroup([]);
return;
}
onArrowClick && onArrowClick();
}, [groups, groupHeader, onArrowClick, onGroupChanged]);
}, [groups, groupHeader && groupHeader.label, onArrowClick, onGroupChanged]);
const renderGroupsList = useCallback(() => {
if (groupList.length === 0) {
return <Option isLoader={true} loadingLabel={loadingLabel} />;
}
return (
<GroupList
groupList={groupList}
isMultiSelect={isMultiSelect}
onGroupClick={onGroupClick}
/>
);
}, [isMultiSelect, groupList, onGroupClick, loadingLabel]);
const itemCount = hasNextPage ? options.length + 1 : options.length;
const hasSelected = selectedOptionList.length > 0;
return (
<StyledSelector
options={options}
groups={groups}
isMultiSelect={isMultiSelect}
allowGroupSelection={allowGroupSelection}
hasSelected={hasSelected()}
hasSelected={hasSelected}
className="selector-wrapper"
>
<div className="header">
<IconButton
iconName="/static/images/arrow.path.react.svg"
size="17"
isFill={true}
className="arrow-button"
onClick={onArrowClickAction}
<Header
headerLabel={headerLabel}
onArrowClickAction={onArrowClickAction}
/>
<div style={{ height: "100%" }} className="column-options" size={size}>
<Search
isDisabled={isDisabled}
placeholder={searchPlaceHolderLabel}
value={searchValue}
onChange={onSearchChange}
onClearSearch={onSearchReset}
/>
<Heading size="medium" truncate={true}>
{headerLabel.replace("()", "")}
</Heading>
</div>
<Column className="column-options" size={size}>
<Header className="header-options">
<SearchInput
className="options_searcher"
isDisabled={isDisabled}
size="base"
scale={true}
isNeedFilter={false}
placeholder={searchPlaceHolderLabel}
value={searchValue}
onChange={onSearchChange}
onClearSearch={onSearchReset}
/>
</Header>
<Body className="body-options">
<div style={{ width: "100%", height: "100%" }} className="body-options">
{!groupHeader && !searchValue && groups ? (
renderGroupsList()
) : (
<>
{!searchValue && renderGroupHeader()}
{!searchValue && (
<>
<GroupHeader
{...groupHeader}
onSelectAll={onSelectAll}
isMultiSelect={isMultiSelect}
isIndeterminate={
groupHeader.selectedCount > 0 &&
groupHeader.selectedCount !== groupHeader.total
}
isChecked={
groupHeader.total !== 0 &&
groupHeader.total === groupHeader.selectedCount
}
/>
<div className="option-separator"></div>
</>
)}
{!hasNextPage && itemCount === 0 ? (
<div className="row-option">
<Text>
@ -594,31 +365,18 @@ const Selector = (props) => {
</Text>
</div>
) : (
<AutoSizer>
{({ width, height }) => (
<InfiniteLoader
ref={listOptionsRef}
isItemLoaded={isItemLoaded}
itemCount={itemCount}
loadMoreItems={loadMoreItems}
>
{({ onItemsRendered, ref }) => (
<List
className="options_list"
height={height - 25}
itemCount={itemCount}
itemSize={48}
onItemsRendered={onItemsRendered}
ref={ref}
width={width + 8}
outerElementType={CustomScrollbarsVirtualList}
>
{renderOption}
</List>
)}
</InfiniteLoader>
)}
</AutoSizer>
<OptionList
listOptionsRef={listOptionsRef}
loadingLabel={loadingLabel}
options={options}
itemCount={itemCount}
isMultiSelect={isMultiSelect}
onOptionChange={onOptionChange}
onLinkClick={onLinkClick}
isItemLoaded={isItemLoaded}
isOptionChecked={isOptionChecked}
loadMoreItems={loadMoreItems}
/>
)}
</>
)}
@ -630,14 +388,14 @@ const Selector = (props) => {
getContent={getOptionTooltipContent}
/>
)}
</Body>
</Column>
</div>
</div>
<Footer
className="footer"
selectButtonLabel={headerLabel}
showCounter={showCounter}
isDisabled={isDisabled}
isVisible={isMultiSelect && hasSelected()}
isVisible={isMultiSelect && hasSelected}
onClick={onAddClick}
embeddedComponent={embeddedComponent}
selectedLength={selectedOptionList.length}
@ -666,8 +424,6 @@ Selector.propTypes = {
emptyOptionsLabel: PropTypes.string,
loadingLabel: PropTypes.string,
size: PropTypes.oneOf(["compact", "full"]),
selectedOptions: PropTypes.array,
selectedGroups: PropTypes.array,
@ -679,8 +435,4 @@ Selector.propTypes = {
embeddedComponent: PropTypes.any,
};
Selector.defaultProps = {
size: "full",
};
export default Selector;
export default React.memo(Selector);

View File

@ -1,15 +0,0 @@
import React from "react";
import styled from "styled-components";
/* eslint-disable no-unused-vars */
/* eslint-disable react/prop-types */
const Container = ({ ...props }) => <div {...props} />;
/* eslint-enable react/prop-types */
/* eslint-enable no-unused-vars */
const StyledBody = styled(Container)`
width: 100%;
height: 100%;
`;
export default StyledBody;

View File

@ -1,15 +0,0 @@
import React from "react";
import styled, { css } from "styled-components";
/* eslint-disable no-unused-vars */
/* eslint-disable react/prop-types */
const Container = ({ ...props }) => <div {...props} />;
/* eslint-enable react/prop-types */
/* eslint-enable no-unused-vars */
const StyledColumn = styled(Container)`
width: 320px;
height: 100%;
`;
export default StyledColumn;

View File

@ -1,14 +0,0 @@
import React from "react";
import styled from "styled-components";
/* eslint-disable no-unused-vars */
/* eslint-disable react/prop-types */
const Container = ({ ...props }) => <div {...props} />;
/* eslint-enable react/prop-types */
/* eslint-enable no-unused-vars */
const StyledHeader = styled(Container)`
/*height: 64px;*/
`;
export default StyledHeader;

View File

@ -1,22 +1,14 @@
import React from "react";
import styled, { css } from "styled-components";
import Base from "@appserver/components/themes/base";
/* eslint-disable no-unused-vars */
/* eslint-disable react/prop-types */
const Container = ({
options,
groups,
isMultiSelect,
allowGroupSelection,
hasSelected,
...props
}) => <div {...props} />;
/* eslint-enable react/prop-types */
/* eslint-enable no-unused-vars */
const StyledSelector = styled(Container)`
const StyledSelector = styled.div`
display: grid;
height: 100%;
@ -112,6 +104,12 @@ const StyledSelector = styled(Container)`
grid-area: body-options;
margin-top: 8px;
.options-list {
div:nth-child(3) {
right: 10px !important;
}
}
.row-option {
box-sizing: border-box;
height: 48px;

View File

@ -341,6 +341,7 @@ const ErrorContainer = (props) => {
{headerText}
</Headline>
)}
{children}
{bodyText && <Text id="text">{bodyText}</Text>}
{buttonText && buttonUrl && (
<div id="button-container">

View File

@ -0,0 +1,93 @@
import React from "react";
import PropTypes from "prop-types";
import { StyledRow } from "./StyledListLoader";
import RectangleLoader from "../RectangleLoader";
const ListItemLoader = ({ id, className, style, isRectangle, ...rest }) => {
const {
title,
borderRadius,
backgroundColor,
foregroundColor,
backgroundOpacity,
foregroundOpacity,
speed,
animate,
} = rest;
return (
<StyledRow id={id} className={className} style={style}>
{isRectangle && (
<RectangleLoader
title={title}
width="16"
height="16"
borderRadius={borderRadius}
backgroundColor={backgroundColor}
foregroundColor={foregroundColor}
backgroundOpacity={backgroundOpacity}
foregroundOpacity={foregroundOpacity}
speed={speed}
animate={animate}
className="list-loader_rectangle"
/>
)}
<RectangleLoader
className="list-loader_rectangle-content"
title={title}
width="100%"
height="100%"
borderRadius={borderRadius}
backgroundColor={backgroundColor}
foregroundColor={foregroundColor}
backgroundOpacity={backgroundOpacity}
foregroundOpacity={foregroundOpacity}
speed={speed}
animate={animate}
/>
<RectangleLoader
className="list-loader_rectangle-row"
title={title}
height="16px"
borderRadius={borderRadius}
backgroundColor={backgroundColor}
foregroundColor={foregroundColor}
backgroundOpacity={backgroundOpacity}
foregroundOpacity={foregroundOpacity}
speed={speed}
animate={animate}
/>
<RectangleLoader
title={title}
width="16"
height="16"
borderRadius={borderRadius}
backgroundColor={backgroundColor}
foregroundColor={foregroundColor}
backgroundOpacity={backgroundOpacity}
foregroundOpacity={foregroundOpacity}
speed={speed}
animate={animate}
/>
</StyledRow>
);
};
ListItemLoader.propTypes = {
id: PropTypes.string,
className: PropTypes.string,
style: PropTypes.object,
isRectangle: PropTypes.bool,
};
ListItemLoader.defaultProps = {
id: undefined,
className: undefined,
style: undefined,
isRectangle: true,
};
export default ListItemLoader;

View File

@ -0,0 +1,33 @@
import styled from "styled-components";
import { desktop } from "@appserver/components/utils/device";
const StyledList = styled.div`
padding: 0 16px;
`;
const StyledRow = styled.div`
width: 100%;
display: grid;
grid-template-columns: 16px 32px 1fr 16px;
grid-template-rows: 1fr;
grid-column-gap: 8px;
margin-bottom: 16px;
justify-items: center;
align-items: center;
.list-loader_rectangle {
padding-right: 4px;
}
.list-loader_rectangle-content {
width: 32px;
height: 32px;
}
.list-loader_rectangle-row {
margin-right: auto;
max-width: 167px;
}
`;
export { StyledRow, StyledList };

View File

@ -0,0 +1,21 @@
import ListItemLoader from "./ListItemLoader";
import React from "react";
import PropTypes from "prop-types";
import { StyledList } from "./StyledListLoader";
const ListLoader = ({ count, ...props }) => {
const items = [];
for (var i = 0; i < count; i++) {
items.push(<ListItemLoader key={`list_loader_${i}`} {...props} />);
}
return <StyledList>{items}</StyledList>;
};
ListLoader.propTypes = {
count: PropTypes.number,
};
ListLoader.defaultProps = {
count: 25,
};
export default ListLoader;

View File

@ -20,6 +20,7 @@ import Tile from "./TileLoader";
import Tiles from "./TilesLoader";
import DialogLoader from "./DialogLoader";
import DialogAsideLoader from "./DialogAsideLoader";
import ListLoader from "./ListLoader";
export default {
Rectangle,
@ -44,4 +45,5 @@ export default {
ArticleButton,
ArticleFolder,
ArticleGroup,
ListLoader,
};

View File

@ -9,7 +9,7 @@ import AppLoader from "../AppLoader";
import { inject, observer } from "mobx-react";
import { isMe } from "../../utils";
import combineUrl from "../../utils/combineUrl";
import { AppServerConfig } from "../../constants";
import { AppServerConfig, TenantStatus } from "../../constants";
const PrivateRoute = ({ component: Component, ...rest }) => {
const {
@ -26,8 +26,9 @@ const PrivateRoute = ({ component: Component, ...rest }) => {
wizardCompleted,
personal,
location,
tenantStatus,
} = rest;
const isPortal = window.location.pathname === "/preparation-portal";
const { params, path } = computedMatch;
const { userId } = params;
@ -63,6 +64,25 @@ const PrivateRoute = ({ component: Component, ...rest }) => {
);
}
if (
isLoaded &&
isAuthenticated &&
tenantStatus === TenantStatus.PortalRestore &&
!isPortal
) {
return (
<Redirect
to={{
pathname: combineUrl(
AppServerConfig.proxyURL,
"/preparation-portal"
),
state: { from: props.location },
}}
/>
);
}
if (!isLoaded) {
return <AppLoader />;
}
@ -160,7 +180,12 @@ export default inject(({ auth }) => {
} = auth;
const { user } = userStore;
const { modules } = moduleStore;
const { setModuleInfo, wizardCompleted, personal } = settingsStore;
const {
setModuleInfo,
wizardCompleted,
personal,
tenantStatus,
} = settingsStore;
return {
modules,
@ -170,7 +195,7 @@ export default inject(({ auth }) => {
isLoaded,
setModuleInfo,
wizardCompleted,
tenantStatus,
personal,
};
})(observer(PrivateRoute));

View File

@ -142,6 +142,34 @@ export const LoaderStyle = {
animate: true,
};
/**
* Enum for third-party storages.
* @readonly
*/
export const ThirdPartyStorages = Object.freeze({
GoogleId: "googlecloud",
RackspaceId: "rackspace",
SelectelId: "selectel",
AmazonId: "s3",
});
/**
* Enum for backup types.
* @readonly
*/
export const BackupStorageType = Object.freeze({
DocumentModuleType: 0,
ResourcesModuleType: 1,
LocalFileModuleType: 3,
TemporaryModuleType: 4,
StorageModuleType: 5,
});
export const AutoBackupPeriod = Object.freeze({
EveryDayType: 0,
EveryWeekType: 1,
EveryMonthType: 2,
});
import config from "./AppServerConfig";
export const AppServerConfig = config;
@ -173,3 +201,11 @@ export const FileStatus = Object.freeze({
IsTemplate: 64,
IsFillFormDraft: 128,
});
/**
* Enum for tenant status.
* @readonly
*/
export const TenantStatus = Object.freeze({
PortalRestore: 4,
});

View File

@ -9,7 +9,7 @@ import TfaStore from "./TfaStore";
import { logout as logoutDesktop, desktopConstants } from "../desktop";
import { combineUrl, isAdmin } from "../utils";
import isEmpty from "lodash/isEmpty";
import { AppServerConfig, LANGUAGE } from "../constants";
import { AppServerConfig, LANGUAGE, TenantStatus } from "../constants";
const { proxyURL } = AppServerConfig;
class AuthStore {
@ -39,19 +39,23 @@ class AuthStore {
this.skipModules = skipModules;
await this.userStore.init();
try {
await this.userStore.init();
} catch (e) {
console.error(e);
}
const requests = [];
requests.push(this.settingsStore.init());
if (this.isAuthenticated && !skipModules) {
requests.push(this.moduleStore.init());
this.userStore.user && requests.push(this.moduleStore.init());
}
return Promise.all(requests);
};
setLanguage() {
if (this.userStore.user.cultureName) {
if (this.userStore.user?.cultureName) {
localStorage.getItem(LANGUAGE) !== this.userStore.user.cultureName &&
localStorage.setItem(LANGUAGE, this.userStore.user.cultureName);
} else {
@ -61,9 +65,12 @@ class AuthStore {
get isLoaded() {
let success = false;
if (this.isAuthenticated) {
success = this.userStore.isLoaded && this.settingsStore.isLoaded;
success =
(this.userStore.isLoaded && this.settingsStore.isLoaded) ||
this.settingsStore.tenantStatus === TenantStatus.PortalRestore;
if (!this.skipModules) success = success && this.moduleStore.isLoaded;
if (!this.skipModules && this.userStore.user)
success = success && this.moduleStore.isLoaded;
success && this.setLanguage();
} else {
@ -241,7 +248,10 @@ class AuthStore {
};
get isAuthenticated() {
return this.userStore.isAuthenticated;
return (
this.userStore.isAuthenticated ||
this.settingsStore.tenantStatus === TenantStatus.PortalRestore
);
}
getEncryptionAccess = (fileId) => {

View File

@ -1,6 +1,6 @@
import { makeAutoObservable } from "mobx";
import api from "../api";
import { ARTICLE_PINNED_KEY, LANGUAGE } from "../constants";
import { ARTICLE_PINNED_KEY, LANGUAGE, TenantStatus } from "../constants";
import { combineUrl } from "../utils";
import FirebaseHelper from "../utils/firebase";
import { AppServerConfig } from "../constants";
@ -80,6 +80,8 @@ class SettingsStore {
showText = false;
articleOpen = false;
folderPath = [];
hashSettings = null;
title = "";
ownerId = null;
@ -110,10 +112,15 @@ class SettingsStore {
userFormValidation = /^[\p{L}\p{M}'\-]+$/gu;
folderFormValidation = new RegExp('[*+:"<>?|\\\\/]', "gim");
tenantStatus = null;
constructor() {
makeAutoObservable(this);
}
setTenantStatus = (tenantStatus) => {
this.tenantStatus = tenantStatus;
};
get urlAuthKeys() {
const splitted = this.culture.split("-");
const lang = splitted.length > 0 ? splitted[0] : "en";
@ -131,6 +138,12 @@ class SettingsStore {
return `https://helpcenter.onlyoffice.com/${lang}/administration/configuration.aspx#CustomizingPortal_block`;
}
get helpUrlCreatingBackup() {
const splitted = this.culture.split("-");
const lang = splitted.length > 0 ? splitted[0] : "en";
return `https://helpcenter.onlyoffice.com/${lang}/administration/configuration.aspx#CreatingBackup_block`;
}
setValue = (key, value) => {
this[key] = value;
};
@ -187,6 +200,10 @@ class SettingsStore {
return newSettings;
};
getFolderPath = async (id) => {
this.folderPath = await api.files.getFolderPath(id);
};
getCurrentCustomSchema = async (id) => {
this.customNames = await api.settings.getCurrentCustomSchema(id);
};
@ -198,15 +215,24 @@ class SettingsStore {
getPortalSettings = async () => {
const origSettings = await this.getSettings();
if (origSettings.nameSchemaId) {
if (
origSettings.nameSchemaId &&
this.tenantStatus !== TenantStatus.PortalRestore
) {
this.getCurrentCustomSchema(origSettings.nameSchemaId);
}
};
init = async () => {
this.setIsLoading(true);
const requests = [];
await Promise.all([this.getPortalSettings(), this.getBuildVersionInfo()]);
requests.push(this.getPortalSettings());
this.tenantStatus !== TenantStatus.PortalRestore &&
requests.push(this.getBuildVersionInfo());
await Promise.all(requests);
this.setIsLoading(false);
this.setIsLoaded(true);

View File

@ -37,7 +37,11 @@ class SocketIOHelper {
if (!client.connected) {
client.on("connect", () => {
room ? client.to(room).emit(command, data) : client.emit(command, data);
if (room !== null) {
client.to(room).emit(command, data);
} else {
client.emit(command, data);
}
});
} else {
room ? client.to(room).emit(command, data) : client.emit(command, data);
@ -46,6 +50,7 @@ class SocketIOHelper {
on = (eventName, callback) => {
if (!this.isEnabled) return;
if (!client.connected) {
client.on("connect", () => {
client.on(eventName, callback);

View File

@ -50,6 +50,7 @@ class Checkbox extends React.Component {
if (this.props.isIndeterminate !== prevProps.isIndeterminate) {
this.ref.current.indeterminate = this.props.isIndeterminate;
}
if (this.props.isChecked !== prevProps.isChecked) {
this.setState({ checked: this.props.isChecked });
}
@ -73,6 +74,7 @@ class Checkbox extends React.Component {
value,
title,
truncate,
name,
} = this.props;
return (
@ -85,6 +87,7 @@ class Checkbox extends React.Component {
title={title}
>
<HiddenInput
name={name}
type="checkbox"
checked={this.state.checked}
isDisabled={isDisabled}
@ -141,4 +144,4 @@ Checkbox.defaultProps = {
truncate: false,
};
export default Checkbox;
export default React.memo(Checkbox);

View File

@ -73,26 +73,6 @@ class FileInput extends Component {
...rest
} = this.props;
let iconSize = 0;
switch (size) {
case "base":
iconSize = 15;
break;
case "middle":
iconSize = 15;
break;
case "big":
iconSize = 16;
break;
case "huge":
iconSize = 16;
break;
case "large":
iconSize = 16;
break;
}
return (
<StyledFileInput
size={size}
@ -127,7 +107,6 @@ class FileInput extends Component {
className="icon-button"
iconName={"/static/images/catalog.folder.react.svg"}
// color={"#A3A9AE"}
size={iconSize}
isDisabled={isDisabled}
/>
</div>

View File

@ -7,6 +7,8 @@ const paddingRightStyle = (props) =>
const widthIconStyle = (props) => props.theme.fileInput.icon.width[props.size];
const heightIconStyle = (props) =>
props.theme.fileInput.icon.height[props.size];
const widthIconButtonStyle = (props) =>
props.theme.fileInput.iconButton.width[props.size];
const StyledFileInput = styled.div`
display: flex;
@ -31,6 +33,7 @@ const StyledFileInput = styled.div`
padding-right: 40px;
padding-right: ${(props) => paddingRightStyle(props)};
cursor: ${(props) => (props.isDisabled ? "default" : "pointer")};
margin: 0;
}
:hover {
@ -79,6 +82,7 @@ const StyledFileInput = styled.div`
.icon-button {
cursor: ${(props) => (props.isDisabled ? "default" : "pointer")};
width: ${(props) => widthIconButtonStyle(props)};
}
`;
StyledFileInput.defaultProps = { theme: Base };

View File

@ -39,4 +39,4 @@ Heading.defaultProps = {
className: "",
};
export default Heading;
export default React.memo(Heading);

View File

@ -60,6 +60,7 @@ class HelpButton extends React.Component {
getContent,
className,
dataTip,
tooltipMaxWidth,
style,
size,
} = this.props;
@ -92,6 +93,7 @@ class HelpButton extends React.Component {
afterShow={this.afterShow}
afterHide={this.afterHide}
getContent={getContent}
maxWidth={tooltipMaxWidth}
/>
) : (
<Tooltip
@ -123,7 +125,7 @@ HelpButton.propTypes = {
offsetLeft: PropTypes.number,
offsetTop: PropTypes.number,
offsetBottom: PropTypes.number,
tooltipMaxWidth: PropTypes.number,
tooltipMaxWidth: PropTypes.string,
tooltipId: PropTypes.string,
place: PropTypes.string,
iconName: PropTypes.string,

View File

@ -110,8 +110,9 @@ class ModalDialog extends React.Component {
children,
isLoading,
contentPaddingBottom,
removeScroll,
withoutBodyScroll,
modalLoaderBodyHeight,
withoutCloseButton,
theme,
width,
} = this.props;
@ -166,10 +167,12 @@ class ModalDialog extends React.Component {
<Heading className="heading" size="medium" truncate={true}>
{header ? header.props.children : null}
</Heading>
<CloseButton
className="modal-dialog-button_close"
onClick={onClose}
></CloseButton>
{!withoutCloseButton && (
<CloseButton
className="modal-dialog-button_close"
onClick={onClose}
></CloseButton>
)}
</StyledHeader>
<BodyBox paddingProp={modalBodyPadding}>
{body ? body.props.children : null}
@ -194,12 +197,12 @@ class ModalDialog extends React.Component {
zIndex={zIndex}
contentPaddingBottom={contentPaddingBottom}
className="modal-dialog-aside not-selectable"
withoutBodyScroll={removeScroll}
withoutBodyScroll={withoutBodyScroll}
>
<Content
contentHeight={contentHeight}
contentWidth={contentWidth}
removeScroll={removeScroll}
withoutBodyScroll={withoutBodyScroll}
displayType={this.state.displayType}
>
{isLoading ? (
@ -215,7 +218,7 @@ class ModalDialog extends React.Component {
<BodyBox
className="modal-dialog-aside-body"
paddingProp={asideBodyPadding}
removeScroll={removeScroll}
withoutBodyScroll={withoutBodyScroll}
>
{body ? body.props.children : null}
</BodyBox>
@ -247,6 +250,8 @@ ModalDialog.propTypes = {
/** Will be triggered when a close button is clicked */
onClose: PropTypes.func,
onResize: PropTypes.func,
/**Display close button or not */
withoutCloseButton: PropTypes.bool,
/** CSS z-index */
zIndex: PropTypes.number,
/** CSS padding props for body section */
@ -255,7 +260,7 @@ ModalDialog.propTypes = {
contentHeight: PropTypes.string,
contentWidth: PropTypes.string,
isLoading: PropTypes.bool,
removeScroll: PropTypes.bool,
withoutBodyScroll: PropTypes.bool,
className: PropTypes.string,
id: PropTypes.string,
style: PropTypes.oneOfType([PropTypes.object, PropTypes.array]),
@ -270,6 +275,8 @@ ModalDialog.defaultProps = {
asideBodyPadding: "16px 0",
modalBodyPadding: "12px 0",
contentWidth: "100%",
withoutCloseButton: false,
withoutBodyScroll: false,
};
ModalDialog.Header = Header;

View File

@ -47,7 +47,7 @@ const Content = styled.div`
}
${(props) =>
props.removeScroll &&
props.withoutBodyScroll &&
css`
overflow: hidden;
`}
@ -85,7 +85,7 @@ const BodyBox = styled(Box)`
position: relative;
${(props) =>
props.removeScroll &&
props.withoutBodyScroll &&
css`
height: 100%;
`}

View File

@ -43,16 +43,11 @@ class SaveCancelButtons extends React.Component {
reminderTest,
saveButtonLabel,
cancelButtonLabel,
hasChanged,
hasScroll,
className,
id,
} = this.props;
// TODO: hasChanged не нужен, тк есть showReminder?
const isDisabled = hasChanged !== undefined ? !hasChanged : false;
return (
<StyledSaveCancelButtons
className={className}
@ -65,17 +60,19 @@ class SaveCancelButtons extends React.Component {
<Button
className="save-button"
size="normal"
isDisabled={isDisabled}
isDisabled={!showReminder}
primary
onClick={onSaveClick}
label={saveButtonLabel}
minwidth={displaySettings && "auto"}
/>
<Button
className="cancel-button"
size="normal"
isDisabled={isDisabled}
isDisabled={!showReminder}
onClick={onCancelClick}
label={cancelButtonLabel}
minwidth={displaySettings && "auto"}
/>
</div>
{showReminder && (
@ -103,9 +100,10 @@ SaveCancelButtons.propTypes = {
onCancelClick: PropTypes.func,
/** Show message about unsaved changes (Only shown on desktops) */
showReminder: PropTypes.bool,
/** Tells when the button should present a disabled state */
displaySettings: PropTypes.bool,
hasChanged: PropTypes.bool,
hasScroll: PropTypes.bool,
minwidth: PropTypes.string,
};
SaveCancelButtons.defaultProps = {

View File

@ -1,16 +1,29 @@
import styled, { css } from "styled-components";
import Base from "../themes/base";
import { tablet } from "../utils/device";
import { isMobile, isTablet } from "react-device-detect";
import { isMobileOnly, isTablet } from "react-device-detect";
const displaySettings = css`
position: relative;
position: absolute;
display: block;
flex-direction: column-reverse;
align-items: flex-start;
border-top: ${(props) =>
props.hasScroll && !props.showReminder ? "1px solid #ECEEF1" : "none"};
${(props) =>
!props.hasScroll &&
!isMobileOnly &&
css`
padding-left: 24px;
`}
${(props) =>
props.hasScroll &&
css`
bottom: auto;
`}
.buttons-flex {
display: flex;
width: 100%;
@ -28,11 +41,15 @@ const displaySettings = css`
.unsaved-changes {
position: absolute;
padding-top: 16px;
padding-bottom: 16px;
font-size: 12px;
font-weight: 600;
width: calc(100% - 32px);
bottom: 56px;
background-color: white;
background-color: ${(props) =>
props.hasScroll
? props.theme.mainButtonMobile.buttonWrapper.background
: "none"};
}
${(props) =>
@ -56,6 +73,7 @@ const tabletButtons = css`
justify-content: flex-start;
align-items: center;
padding: 0;
border-top: none;
.buttons-flex {
width: auto;
@ -73,7 +91,7 @@ const tabletButtons = css`
margin-left: 8px;
margin-bottom: 0;
position: static;
padding-top: 0px;
padding: 0;
}
`;
@ -85,12 +103,9 @@ const StyledSaveCancelButtons = styled.div`
align-items: center;
bottom: ${(props) => props.theme.saveCancelButtons.bottom};
width: ${(props) => props.theme.saveCancelButtons.width};
left: ${(props) =>
props.displaySettings ? "auto" : props.theme.saveCancelButtons.left};
left: ${(props) => props.theme.saveCancelButtons.left};
padding: ${(props) =>
props.displaySettings
? "16px 16px 0px 16px"
: props.theme.saveCancelButtons.padding};
props.displaySettings ? "16px" : props.theme.saveCancelButtons.padding};
.save-button {
margin-right: ${(props) => props.theme.saveCancelButtons.marginRight};
@ -109,13 +124,6 @@ const StyledSaveCancelButtons = styled.div`
`}
}
@media (orientation: landscape) and (min-width: 600px) {
${isMobile &&
css`
padding-left: 16px;
`}
}
@media ${tablet} {
${(props) =>
!props.displaySettings &&
@ -134,8 +142,6 @@ const StyledSaveCancelButtons = styled.div`
props.displaySettings &&
!isTablet &&
`
${tabletButtons}
.save-button, .cancel-button {
font-size: 13px;
line-height: 20px;
@ -143,9 +149,6 @@ const StyledSaveCancelButtons = styled.div`
padding-bottom: 5px;
}
.unsaved-changes {
padding-bottom: 0;
}
`}
}
`;

View File

@ -66,4 +66,4 @@ Text.defaultProps = {
noSelect: false,
};
export default Text;
export default React.memo(Text);

View File

@ -706,6 +706,16 @@ const Base = {
large: "43px",
},
},
iconButton: {
width: {
base: "15px",
middle: "15px",
big: "16px",
huge: "16px",
large: "16px",
},
},
},
passwordInput: {
@ -1948,6 +1958,11 @@ const Base = {
expanderColor: "dimgray",
downloadAppList: {
color: "#83888d",
winHoverColor: "#3785D3",
macHoverColor: black,
linuxHoverColor: "#FFB800",
androidHoverColor: "#9BD71C",
iosHoverColor: black,
},
thirdPartyList: {
color: "#818b91",

View File

@ -705,6 +705,15 @@ const Dark = {
large: "43px",
},
},
iconButton: {
width: {
base: "15px",
middle: "15px",
big: "16px",
huge: "16px",
large: "16px",
},
},
},
passwordInput: {
@ -1100,7 +1109,7 @@ const Dark = {
slider: {
width: "100%",
margin: "8px 0",
margin: "24px 0",
backgroundColor: "transparent",
runnableTrack: {
@ -1952,6 +1961,11 @@ const Dark = {
downloadAppList: {
color: "#C4C4C4",
winHoverColor: "#3785D3",
macHoverColor: white,
linuxHoverColor: "#FFB800",
androidHoverColor: "#9BD71C",
iosHoverColor: white,
},
thirdPartyList: {

View File

@ -13,7 +13,6 @@
"CopyItem": "<strong>{{title}}</strong> köçürüldü",
"CopyItems": "<strong>{{qty}}</strong> elementlər köçürüldü",
"Document": "Sənəd",
"Duplicate": "Sürətini yaratmaq",
"EmptyFile": "Boş fayl",
"EmptyFilterDescriptionText": "Heç bir fayl və ya qovluq bu süzgəcə uyğun deyil. Xahiş edirik digər süzgəc parametrindən istifadə edin və ya bütün faylların göstərilməsi üçün süzgəci sıfırlayın.",
"EmptyFilterSubheadingText": "Bu süzgəc üçün heç bir fayl tapılmadı",

View File

@ -3,7 +3,6 @@
"ArchivingData": "Məlumatların arxivləşdirilməsi",
"ConnectingAccount": "Akkauntun qoşulması",
"Copy": "Köçür",
"CopyOperation": "Köçürülür",
"CreateMasterFormFromFile": "Fayldan forma şablonunu yaradın",
"DeleteFromTrash": "Seçilmiş elementlər zibil qutusundan silindi",
"DeleteOperation": "Silinir",
@ -32,7 +31,6 @@
"NewFormFile": "Faylından forma",
"OwnerChange": "Sahibi dəyiş",
"Presentations": "Təqdimatlar",
"Restore": "Bərpa et",
"Spreadsheets": "Cədvəllər",
"SubNewForm": "Blankı",
"SubNewFormFile": "Mətn faylından",
@ -46,4 +44,4 @@
"TypeTitleSkyDrive": "OneDrive",
"TypeTitleWebDav": "WebDAV",
"TypeTitleYandex": "Yandex.Disk"
}
}

View File

@ -13,7 +13,6 @@
"CopyItem": "<strong>{{title}}</strong> е копиран",
"CopyItems": "<strong>{{qty}}</strong> елемента са копирани",
"Document": "Документ",
"Duplicate": "Суздай копие",
"EmptyFile": "Изпразни файл",
"EmptyFilterDescriptionText": "Нито един файл или папка не пасват на този филтър. Изпробвайте друг или изчистете филтъра, за да видите всички файлове. ",
"EmptyFilterSubheadingText": "Няма файлове, които да бъдат показани за този филтър тук",

View File

@ -3,7 +3,6 @@
"ArchivingData": "Архивиране на данни",
"ConnectingAccount": "Свързване на профил",
"Copy": "Копирай",
"CopyOperation": "Копиране",
"CreateMasterFormFromFile": "Създай шаблон на формуляр от файл",
"DeleteFromTrash": "Избраните елементи бяха успешно изтрити от Кошчето",
"DeleteOperation": "Изтриване",
@ -33,7 +32,6 @@
"NewFormFile": "Шаблон от файл",
"OwnerChange": "Смени собственик",
"Presentations": "Презентации",
"Restore": "Възстанови",
"Spreadsheets": "Таблици",
"SubNewForm": "Празен",
"SubNewFormFile": "От текстов файл",
@ -47,4 +45,4 @@
"TypeTitleSkyDrive": "OneDrive",
"TypeTitleWebDav": "WebDAV",
"TypeTitleYandex": "Yandex.Disk"
}
}

View File

@ -13,7 +13,6 @@
"CopyItem": "<strong>{{title}}</strong> zkopírováno",
"CopyItems": "Prvky <strong>{{qty}}</strong> zkopírovany",
"Document": "Dokument",
"Duplicate": "Vytvořit kopii",
"EmptyFile": "Prázdný soubor",
"EmptyFilterDescriptionText": "Tomuto filtru neodpovídají žádné soubory ani složky. Vyzkoušejte jiný nebo vymažte filtr pro zobrazení všech souborů.",
"EmptyFilterSubheadingText": "Pro tento filtr nejsou žádné soubory k zobrazení",

View File

@ -3,7 +3,6 @@
"ArchivingData": "Archivace dat",
"ConnectingAccount": "Připojovací účet",
"Copy": "Zkopírovat",
"CopyOperation": "Kopírování",
"CreateMasterFormFromFile": "Vytvorit sablonu formulare ze souboru",
"DeleteFromTrash": "Vybrané prvky byly úspěšně odstraněny z koše",
"DeleteOperation": "Mazání",
@ -32,7 +31,6 @@
"NewFormFile": "Formulare ze souboru",
"OwnerChange": "Změnit vlastníka",
"Presentations": "Prezentace",
"Restore": "Obnovit",
"Spreadsheets": "Tabulky",
"SubNewForm": "Prazdny",
"SubNewFormFile": "Z textoveho souboru",
@ -46,4 +44,4 @@
"TypeTitleSkyDrive": "OneDrive",
"TypeTitleWebDav": "WebDAV",
"TypeTitleYandex": "Yandex.Disk"
}
}

View File

@ -13,7 +13,6 @@
"CopyItem": "<strong>{{title}}</strong> kopiert",
"CopyItems": "Elemente kopiert: <strong>{{qty}}</strong>",
"Document": "Dokument",
"Duplicate": "Kopie erstellen",
"EmptyFile": "Leere Datei",
"EmptyFilterDescriptionText": "Keine Dateien oder Ordner entsprechen diesem Filter. Versuchen Sie einen anderen Filter oder entfernen Sie diesen, um alle Dateien zu sehen.",
"EmptyFilterSubheadingText": "Hier gibt es keine Dateien, die diesem Filter entsprechen",

View File

@ -4,7 +4,6 @@
"ButtonShareAccess": "Freigabeeinstellungen",
"ConnectingAccount": "Konto wird verbunden",
"Copy": "Kopieren",
"CopyOperation": "Kopieren",
"CreateMasterFormFromFile": "Formularvorlage aus Datei erstellen",
"DeleteFromTrash": "Die ausgewählten Elemente wurden aus dem Papierkorb gelöscht",
"DeleteOperation": "Löschen",
@ -39,7 +38,6 @@
"NewFormFile": "Formular aus Textdatei",
"OwnerChange": "Besitzer ändern",
"Presentations": "Präsentationen",
"Restore": "Wiederherstellen",
"Spreadsheets": "Tabellenkalkulationen",
"SubNewForm": "Leer",
"SubNewFormFile": "Aus Textdatei",
@ -55,4 +53,4 @@
"TypeTitleSkyDrive": "OneDrive",
"TypeTitleWebDav": "WebDAV",
"TypeTitleYandex": "Yandex.Disk"
}
}

View File

@ -13,7 +13,6 @@
"CopyItem": "Το <strong>{{{title}}</strong> αντιγράφηκε",
"CopyItems": "<strong> {{qty}} </strong> στοιχεία αντιγράφηκαν",
"Document": "Έγγραφο",
"Duplicate": "Δημιουργήστε ένα αντίγραφο",
"EmptyFile": "Κενό αρχείο",
"EmptyFilterDescriptionText": "Με αυτό το φίλτρο δεν ταιριάζει κανένα αρχείο ή φάκελος. Δοκιμάστε ένα διαφορετικό ή καταργήστε το φίλτρο για να δείτε όλα τα αρχεία.",
"EmptyFilterSubheadingText": "Δεν υπάρχουν αρχεία για εμφάνιση για αυτό το φίλτρο",

View File

@ -3,7 +3,6 @@
"ArchivingData": "Αρχειοθέτηση δεδομένων",
"ConnectingAccount": "Σύνδεση λογαριασμού",
"Copy": "Αντιγραφή",
"CopyOperation": "Αντιγραφή",
"CreateMasterFormFromFile": "Δημιουργία προτύπου φόρμας από αρχείο",
"DeleteFromTrash": "Τα επιλεγμένα στοιχεία διαγράφηκαν επιτυχώς από τον Κάδο απορριμμάτων",
"DeleteOperation": "Διαγραφή",
@ -32,7 +31,6 @@
"NewFormFile": "Φόρμας από αρχείο",
"OwnerChange": "Αλλαγή κατόχου",
"Presentations": "Παρουσιάσεις",
"Restore": "Επαναφορά",
"Spreadsheets": "Υπολογιστικά φύλλα",
"SubNewForm": "Κενό",
"SubNewFormFile": "Από αρχείο κειμένου",
@ -46,4 +44,4 @@
"TypeTitleSkyDrive": "OneDrive",
"TypeTitleWebDav": "WebDAV",
"TypeTitleYandex": "Yandex.Disk"
}
}

View File

@ -13,7 +13,6 @@
"CopyItem": "<strong>{{title}}</strong> copied",
"CopyItems": "<strong>{{qty}}</strong> elements copied",
"Document": "Document",
"Duplicate": "Create a copy",
"EmptyFile": "Empty file",
"EmptyFilterDescriptionText": "No files or folders match this filter. Try a different one or clear filter to view all files. ",
"EmptyFilterSubheadingText": "No files to be displayed for this filter here",

View File

@ -1,3 +1,3 @@
{
"MoveToFolderMessage": "You can't move the folder to its subfolder"
}
{
"MoveToFolderMessage": "You can't move the folder to its subfolder"
}

View File

@ -1,21 +1,21 @@
{
"CommonSettings": "Common settings",
"ConnectAccounts": "Connect Accounts",
"ConnectAccountsSubTitle": "No connected accounts",
"ConnectAdminDescription": "For successful connection, enter the necessary data on <1>this page</1>.",
"ConnectDescription": "You can connect the following accounts to the ONLYOFFICE Documents. The documents from these accounts will be available for editing in 'My Documents' section. ",
"ConnectedCloud": "Connected cloud",
"ConnectMakeShared": "Make shared and put into the 'Common' folder",
"ConnextOtherAccount": "Other account",
"DisplayFavorites": "Display Favorites",
"DisplayNotification": "Display notification when moving items to Trash",
"DisplayRecent": "Display Recent",
"DisplayTemplates": "Display Templates",
"IntermediateVersion": "Keep all saved intermediate versions",
"KeepIntermediateVersion": "Keep intermediate versions when editing",
"OriginalCopy": "Save the file copy in the original format as well",
"StoringFileVersion": "Storing file versions",
"ThirdPartyBtn": "Allow users to connect third-party storages",
"ThirdPartySettings": "Connected clouds",
"UpdateOrCreate": "Update the file version for the existing file with the same name. Otherwise, a copy of the file will be created."
}
{
"CommonSettings": "Common settings",
"ConnectAccounts": "Connect Accounts",
"ConnectAccountsSubTitle": "No connected accounts",
"ConnectAdminDescription": "For successful connection, enter the necessary data on <1>this page</1>.",
"ConnectDescription": "You can connect the following accounts to the ONLYOFFICE Documents. The documents from these accounts will be available for editing in 'My Documents' section. ",
"ConnectedCloud": "Connected cloud",
"ConnectMakeShared": "Make shared and put into the 'Common' folder",
"ConnextOtherAccount": "Other account",
"DisplayFavorites": "Display Favorites",
"DisplayNotification": "Display notification when moving items to Trash",
"DisplayRecent": "Display Recent",
"DisplayTemplates": "Display Templates",
"IntermediateVersion": "Keep all saved intermediate versions",
"KeepIntermediateVersion": "Keep intermediate versions when editing",
"OriginalCopy": "Save the file copy in the original format as well",
"StoringFileVersion": "Storing file versions",
"ThirdPartyBtn": "Allow users to connect third-party storages",
"ThirdPartySettings": "Connected clouds",
"UpdateOrCreate": "Update the file version for the existing file with the same name. Otherwise, a copy of the file will be created."
}

View File

@ -4,7 +4,6 @@
"ButtonShareAccess": "Sharing Settings",
"ConnectingAccount": "Connecting account",
"Copy": "Copy",
"CopyOperation": "Copying",
"CreateMasterFormFromFile": "Create Form Template from file",
"DeleteFromTrash": "Selected elements were successfully deleted from Trash",
"DeleteOperation": "Deleting",
@ -39,7 +38,6 @@
"NewFormFile": "Form from text file",
"OwnerChange": "Change owner",
"Presentations": "Presentations",
"Restore": "Restore",
"Spreadsheets": "Spreadsheets",
"SubNewForm": "Blank",
"SubNewFormFile": "From text file",
@ -55,4 +53,4 @@
"TypeTitleSkyDrive": "OneDrive",
"TypeTitleWebDav": "WebDAV",
"TypeTitleYandex": "Yandex.Disk"
}
}

View File

@ -13,7 +13,6 @@
"CopyItem": "<strong>{{title}}</strong> copiado",
"CopyItems": "<strong>{{qty}}</strong> elementos copiados",
"Document": "Documento",
"Duplicate": "Crear una copia",
"EmptyFile": "Archivo vacío",
"EmptyFilterDescriptionText": "Ningún archivo o carpeta coincide con este filtro. Pruebe con otro o borre el filtro para ver todos los archivos. ",
"EmptyFilterSubheadingText": "No hay archivos que se muestren para este filtro aquí",

View File

@ -3,7 +3,6 @@
"ArchivingData": "Archivando datos",
"ConnectingAccount": "Conectando cuenta",
"Copy": "Copiar",
"CopyOperation": "Copiando",
"CreateMasterFormFromFile": "Crear plantilla de formulario desde archivo",
"DeleteFromTrash": "Los elementos seleccionados se han eliminado correctamente de la papelera",
"DeleteOperation": "Eliminando",
@ -32,7 +31,6 @@
"NewFormFile": "Formulario desde archivo de texto",
"OwnerChange": "Cambiar propietario",
"Presentations": "Presentaciones",
"Restore": "Restaurar",
"Spreadsheets": "Hojas de cálculo",
"SubNewForm": "En blanco",
"SubNewFormFile": "Desde archivo de texto",
@ -46,4 +44,4 @@
"TypeTitleSkyDrive": "OneDrive",
"TypeTitleWebDav": "WebDAV",
"TypeTitleYandex": "Yandex.Disk"
}
}

View File

@ -13,7 +13,6 @@
"CopyItem": "<strong> {{title}} </strong> kopioitu",
"CopyItems": "<strong> {{qty}} </strong> elementtiä kopioitu",
"Document": "Asiakirja",
"Duplicate": "Luo kopio",
"EmptyFile": "Tyhjä tiedosto",
"EmptyFilterDescriptionText": "Yksikään tiedosto tai kansio ei vastaa tätä suodatinta. Kokeile toista suodatinta tai poista suodatin nähdäksesi kaikki tiedostot.",
"EmptyFilterSubheadingText": "Tässä suodattimessa ei ole näytettäviä tiedostoja",

View File

@ -3,7 +3,6 @@
"ArchivingData": "Tietojen arkistointi",
"ConnectingAccount": "Yhdistetään tili",
"Copy": "Kopio",
"CopyOperation": "Kopioidaan",
"CreateMasterFormFromFile": "Luo lomakemalli tiedostosta",
"DeleteFromTrash": "Valitut elementit poistettiin roskakorista",
"DeleteOperation": "Poistetaan",
@ -32,7 +31,6 @@
"NewFormFile": "Lomake tekstitiedostosta",
"OwnerChange": "Vaihda omistaja",
"Presentations": "Esitykset",
"Restore": "Palatua",
"Spreadsheets": "Laskentataulukot",
"SubNewForm": "Tyhjä",
"SubNewFormFile": "Luo tekstitiedostosta",
@ -46,4 +44,4 @@
"TypeTitleSkyDrive": "OneDrive",
"TypeTitleWebDav": "WebDAV",
"TypeTitleYandex": "Yandex.Disk"
}
}

View File

@ -13,7 +13,6 @@
"CopyItem": "<strong>{{title}}</strong> copié",
"CopyItems": "<strong>{{qty}}</strong> éléments copiés",
"Document": "Document",
"Duplicate": "Créer une copie",
"EmptyFile": "Fichier vide",
"EmptyFilterDescriptionText": "Aucun fichier ou dossier ne correspond à ce filtre. Essayez un autre filtre ou effacez le filtre pour afficher tous les fichiers. ",
"EmptyFilterSubheadingText": "Aucun fichier à afficher pour ce filtre ici",

View File

@ -4,7 +4,6 @@
"ButtonShareAccess": "Paramètres de partage",
"ConnectingAccount": "Connecter un compte",
"Copy": "Copier",
"CopyOperation": "copier",
"CreateMasterFormFromFile": "Créer un modèle de formulaire à partir d'un fichier",
"DeleteFromTrash": "Les éléments sélectionnés ont été supprimés avec succès de la corbeille",
"DeleteOperation": "Suppression",
@ -39,7 +38,6 @@
"NewFormFile": "Formulaire à partir d'un fichier texte",
"OwnerChange": "Changer le propriétaire",
"Presentations": "Présentations",
"Restore": "Restaurer",
"Spreadsheets": "Feuilles de calcul",
"SubNewForm": "Blanc",
"SubNewFormFile": "Depuis un fichier texte",
@ -55,4 +53,4 @@
"TypeTitleSkyDrive": "OneDrive",
"TypeTitleWebDav": "WebDAV",
"TypeTitleYandex": "Yandex.Disk"
}
}

View File

@ -13,7 +13,6 @@
"CopyItem": "<strong>{{title}}</strong> copiato",
"CopyItems": "<strong>{{qty}}</strong> elementi copiati",
"Document": "Documento",
"Duplicate": "Crea una copia",
"EmptyFile": "File vuoto",
"EmptyFilterDescriptionText": "Nessun file o cartella corrisponde a questo filtro. Provane uno diverso o cancella il filtro per visualizzare tutti i file.",
"EmptyFilterSubheadingText": "Nessun file da visualizzare per con l'uso di filtro qui",

View File

@ -4,7 +4,6 @@
"ButtonShareAccess": "Impostazioni di condivisione",
"ConnectingAccount": "Collegamento dell'account",
"Copy": "Copiare",
"CopyOperation": "Sta copiando",
"CreateMasterFormFromFile": "Crear plantilla de formulario desde archivo",
"DeleteFromTrash": "Gli elementi selezionati sono stati eliminati dal Cestino con successo",
"DeleteOperation": "Sta eliminando",
@ -39,7 +38,6 @@
"NewFormFile": "Formulario desde archivo de texto",
"OwnerChange": "Cambiare proprietario",
"Presentations": "Presentazioni",
"Restore": "Ripristinare",
"Spreadsheets": "Fogli di calcolo",
"SubNewForm": "En blanco",
"SubNewFormFile": "Desde archivo de texto",
@ -55,4 +53,4 @@
"TypeTitleSkyDrive": "OneDrive",
"TypeTitleWebDav": "WebDAV",
"TypeTitleYandex": "Yandex.Disk"
}
}

View File

@ -13,7 +13,6 @@
"CopyItem": "<strong>{{title}}</strong>のコピー",
"CopyItems": "<strong>{{qty}}</strong>要素のコピー",
"Document": "ドキュメント",
"Duplicate": "コピーの作成",
"EmptyFile": "空きファイル",
"EmptyFilterDescriptionText": "このフィルターに一致するファイルやフォルダーはありません。別のフィルタを試すか、フィルタを解除してすべてのファイルを表示してください。 ",
"EmptyFilterSubheadingText": "このフィルターで表示されるファイルはありません。",

View File

@ -3,7 +3,6 @@
"ArchivingData": "データのアーカイブ",
"ConnectingAccount": "接続アカウント",
"Copy": "コピー",
"CopyOperation": "コピー中",
"CreateMasterFormFromFile": "ファイルからフォームテンプレートを作成する",
"DeleteFromTrash": "選択された要素がゴミ箱から削除されました。",
"DeleteOperation": "削除中",
@ -32,7 +31,6 @@
"NewFormFile": "テキストファイルからのフォーム",
"OwnerChange": "オーナー変更",
"Presentations": "プレゼンテーション",
"Restore": "元に戻す",
"Spreadsheets": "スプレッドシート",
"SubNewForm": "空白",
"SubNewFormFile": "テキスト ファイルから作成",
@ -46,4 +44,4 @@
"TypeTitleSkyDrive": "OneDrive",
"TypeTitleWebDav": "WebDAV",
"TypeTitleYandex": "Yandex.Disk"
}
}

View File

@ -13,7 +13,6 @@
"CopyItem": "<strong>{{title}}</strong> 복사 완료",
"CopyItems": "<strong>{{qty}}</strong> 요소 복사 완료",
"Document": "문서",
"Duplicate": "복사본 생성",
"EmptyFile": "빈 파일",
"EmptyFilterDescriptionText": "이 필터에 해당하는 파일이나 폴더가 없습니다. 다르게 시도하거나 필터를 삭제하여 모든 파일을 확인하세요.",
"EmptyFilterSubheadingText": "이 필터에 대해 표시할 파일이 없습니다",
@ -77,4 +76,4 @@
"UploadToFolder": "폴더에 업로드",
"ViewList": "목록",
"ViewTiles": "제목"
}
}

View File

@ -3,7 +3,6 @@
"ArchivingData": "데이터 아카이브 중",
"ConnectingAccount": "계정 연결 중",
"Copy": "복사",
"CopyOperation": "복사 중",
"CreateMasterFormFromFile": "파일에서 양식 템플릿 만들기",
"DeleteFromTrash": "선택 요소가 휴지통에서 성공적으로 삭제되었습니다",
"DeleteOperation": "삭제 중",
@ -32,7 +31,6 @@
"NewFormFile": "파일에서 양식 템플릿",
"OwnerChange": "소유자 변경",
"Presentations": "프레젠테이션",
"Restore": "복원",
"Spreadsheets": "스프레드시트",
"SubNewForm": "공백",
"SubNewFormFile": "기존 텍스트 파일에서",
@ -46,4 +44,4 @@
"TypeTitleSkyDrive": "OneDrive",
"TypeTitleWebDav": "WebDAV",
"TypeTitleYandex": "Yandex.Disk"
}
}

View File

@ -13,7 +13,6 @@
"CopyItem": "<strong>{{title}}</strong> ຄັດລອກ",
"CopyItems": "<strong>{{qty}}</strong> ອົງປະກອບທີ່ຖືກຄັດລອກ",
"Document": "ເອກະສານ",
"Duplicate": "ສ້າງສຳເນົາ",
"EmptyFile": "ເອກະສານຫວ່າງເປົ່າ",
"EmptyFilterDescriptionText": "ບໍ່ມີໄຟລ໌ ຫລື ແຟ້ມໃດທີ່ກົງກັບຕົວກອງນີ້. ລອງໃຊ້ຕົວກັ່ນຕອງອື່ນ ຫຼື ລຶບລ້າງຂໍ້ມູນເພື່ອເບິ່ງເອກະສານທັງໝົດ.",
"EmptyFilterSubheadingText": "ບໍ່ມີເອກະສານທີ່ສະແດງສຳລັບຕົວກອງຢູ່ທີ່ນີ້",
@ -77,4 +76,4 @@
"UploadToFolder": "ອັບໂຫລດໄປຍັງໂຟຣເດີ",
"ViewList": "ລາຍການ",
"ViewTiles": "ຫົວຂໍ້"
}
}

View File

@ -3,7 +3,6 @@
"ArchivingData": "ການເກັບກໍາຂໍ້ມູນ",
"ConnectingAccount": "ການເຊື່ອມໂຍງບັນຊີ",
"Copy": "ສໍາເນົາ",
"CopyOperation": "ການສໍາເນົາ",
"CreateMasterFormFromFile": "ສ້າງໄຟລ໌ແບບຟອມ",
"DeleteFromTrash": "ຂໍ້ມູນທີ່ເລືອກໄດ້ຖືກລຶບອອກຈາກຂີ້ເຫຍື້ອຢ່າງສໍາເລັດຜົນ",
"DeleteOperation": "ການລຶບ",
@ -32,7 +31,6 @@
"NewFormFile": "ໄຟລ໌ແບບຟອມ",
"OwnerChange": "ປ່ຽນເຈົ້າຂອງ",
"Presentations": "ບົດສະເຫນີ",
"Restore": "ກູ້ຄືນ",
"Spreadsheets": "Spreadsheets",
"SubNewForm": "ຟອມເປົ່າ",
"SubNewFormFile": "ຟອມທີ່ມີຂໍ້ມູນ",
@ -46,4 +44,4 @@
"TypeTitleSkyDrive": "OneDrive",
"TypeTitleWebDav": "WebDAV",
"TypeTitleYandex": "Yandex.Disk"
}
}

View File

@ -13,7 +13,6 @@
"CopyItem": "<strong>{{title}}</strong> nokopēts",
"CopyItems": "<strong>{{qty}}</strong> elementi nokopēti",
"Document": "Dokuments",
"Duplicate": "Izveidojiet kopiju",
"EmptyFile": "Tukšs fails",
"EmptyFilterDescriptionText": "Šim filtram neatbilst neviens fails vai mape. Lai skatītu visus failus, izmēģiniet citu filtru vai notīriet filtru. ",
"EmptyFilterSubheadingText": "Netiek parādīti faili šim filtram šeit",

View File

@ -3,7 +3,6 @@
"ArchivingData": "Datu arhivēšana",
"ConnectingAccount": "Savieno kontu",
"Copy": "Kopēt",
"CopyOperation": "Kopē",
"CreateMasterFormFromFile": "Izveidojiet veidlapas veidni no faila",
"DeleteFromTrash": "Atlasītie elementi tika veiksmīgi izdzēsti no atkritnes",
"DeleteOperation": "Dzēš",
@ -32,7 +31,6 @@
"NewFormFile": "Veidlapas no faila",
"OwnerChange": "Mainīt īpašnieku",
"Presentations": "Prezentācijas",
"Restore": "Atjaunot",
"Spreadsheets": "Izklājlapas",
"SubNewForm": "Tukša",
"SubNewFormFile": "No teksta faila",
@ -46,4 +44,4 @@
"TypeTitleSkyDrive": "OneDrive",
"TypeTitleWebDav": "WebDAV",
"TypeTitleYandex": "Yandex.Disk"
}
}

View File

@ -13,7 +13,6 @@
"CopyItem": "<strong>{{title}}</strong> gekopieerd",
"CopyItems": "<strong>{{qty}}</strong> elementen gekopieerd",
"Document": "Document",
"Duplicate": "Maak een kopie",
"EmptyFile": "Leeg bestand",
"EmptyFilterDescriptionText": "Geen bestanden of mappen komen overeen met dit filter. Probeer een andere of verwijder de filter om alle bestanden te zien. ",
"EmptyFilterSubheadingText": "Geen bestanden die hier voor dit filter moeten worden weergegeven",

View File

@ -3,7 +3,6 @@
"ArchivingData": "Gegevens archiveren",
"ConnectingAccount": "Account koppelen",
"Copy": "Kopieer",
"CopyOperation": "Kopiëren",
"CreateMasterFormFromFile": "Maak Formulier sjabloon van bestand",
"DeleteFromTrash": "Geselecteerde elementen werden met succes uit de Prullenmand verwijderd",
"DeleteOperation": "Verwijderen",
@ -33,7 +32,6 @@
"NewFormFile": "Formulier van еekstbestand",
"OwnerChange": "Wijzig eigenaar",
"Presentations": "Presentaties",
"Restore": "Herstel",
"Spreadsheets": "Spreadsheets",
"SubNewForm": "Blanco",
"SubNewFormFile": "Vanuit een tekstbestand",
@ -47,4 +45,4 @@
"TypeTitleSkyDrive": "OneDrive",
"TypeTitleWebDav": "WebDAV",
"TypeTitleYandex": "Yandex.Disk"
}
}

View File

@ -13,7 +13,6 @@
"CopyItem": "Skopiowano <strong>{{title}}</strong>",
"CopyItems": "Skopiowano <strong>{{qty}}</strong> element(y/ów)",
"Document": "Dokument",
"Duplicate": "Utwórz kopię",
"EmptyFile": "Pusty plik",
"EmptyFilterDescriptionText": "Żaden plik ani folder nie pasują do wybranego filtra. Wypróbuj inny lub usuń go, aby zobaczyć wszystkie pliki. ",
"EmptyFilterSubheadingText": "Brak plików do wyświetlenia dla danego filtra",

View File

@ -3,7 +3,6 @@
"ArchivingData": "Archiwizacja danych",
"ConnectingAccount": "Podłączanie konta",
"Copy": "Skopiuj",
"CopyOperation": "Kopiowanie",
"CreateMasterFormFromFile": "Utwórz szablon formularza z pliku",
"DeleteFromTrash": "Wybrane elementy zostały pomyślnie usunięte z kosza",
"DeleteOperation": "Usuwanie",
@ -32,7 +31,6 @@
"NewFormFile": "Formularz z pliku",
"OwnerChange": "Zmień właściciela",
"Presentations": "Prezentacje",
"Restore": "Odzyskaj",
"Spreadsheets": "Arkusze kalkulacyjne",
"SubNewForm": "Pusty",
"SubNewFormFile": "Z pliku tekstowego",
@ -46,4 +44,4 @@
"TypeTitleSkyDrive": "OneDrive",
"TypeTitleWebDav": "WebDAV",
"TypeTitleYandex": "Yandex.Disk"
}
}

View File

@ -13,7 +13,6 @@
"CopyItem": "<strong>{{title}}</strong> copiado",
"CopyItems": "<strong>{{qty}}</strong> elementos copiados",
"Document": "Documento",
"Duplicate": "Criar uma cópia",
"EmptyFile": "Arquivo vazio",
"EmptyFilterDescriptionText": "Nenhum arquivo ou pasta corresponde a este filtro. Tente um filtro diferente ou transparente para visualizar todos os arquivos. ",
"EmptyFilterSubheadingText": "Não há arquivos a serem exibidos para este filtro aqui",

View File

@ -4,7 +4,6 @@
"ButtonShareAccess": "Configurações de compartilhamento",
"ConnectingAccount": "Conectando conta",
"Copy": "Copiar",
"CopyOperation": "Copiando",
"CreateMasterFormFromFile": "Criar modelo de formulário do arquivo",
"DeleteFromTrash": "Os elementos selecionados foram excluídos com sucesso do Lixo",
"DeleteOperation": "Excluindo",
@ -39,7 +38,6 @@
"NewFormFile": "Formulário do arquivo",
"OwnerChange": "Alterar proprietário",
"Presentations": "Apresentações ",
"Restore": "Restaurar",
"Spreadsheets": "Planilhas",
"SubNewForm": "Branco",
"SubNewFormFile": "De um arquivo de texto",
@ -55,4 +53,4 @@
"TypeTitleSkyDrive": "OneDrive",
"TypeTitleWebDav": "WebDAV",
"TypeTitleYandex": "Yandex.Disk"
}
}

View File

@ -13,7 +13,6 @@
"CopyItem": "<strong>{{title}}</strong> copiado",
"CopyItems": "<strong>{{qty}}</strong> elementos copiados",
"Document": "Documento",
"Duplicate": "Criar uma cópia",
"EmptyFile": "Ficheiro vazio",
"EmptyFilterDescriptionText": "Nenhum ficheiro ou pasta corresponde a este filtro. Tente um filtro diferente ou limpe-o para ver todos os ficheiros. ",
"EmptyFilterSubheadingText": "Nenhum ficheiro a ser exibido para este filtro aqui",
@ -77,4 +76,4 @@
"UploadToFolder": "Carregar para a pasta",
"ViewList": "Lista",
"ViewTiles": "Títulos"
}
}

View File

@ -3,7 +3,6 @@
"ArchivingData": "Arquivar dados",
"ConnectingAccount": "A ligar conta",
"Copy": "Copiar",
"CopyOperation": "A copiar",
"CreateMasterFormFromFile": "Create Master Form from file",
"DeleteFromTrash": "Os elementos selecionados foram eliminados com êxito do Lixo",
"DeleteOperation": "A eliminar",
@ -32,7 +31,6 @@
"NewFormFile": "Master form from file",
"OwnerChange": "Alterar dono",
"Presentations": "Apresentações ",
"Restore": "Restaurar",
"Spreadsheets": "Folhas de cálculo",
"SubNewForm": "From blank",
"SubNewFormFile": "From an existing text file",
@ -46,4 +44,4 @@
"TypeTitleSkyDrive": "OneDrive",
"TypeTitleWebDav": "WebDAV",
"TypeTitleYandex": "Yandex.Disk"
}
}

View File

@ -13,7 +13,6 @@
"CopyItem": "<strong>{{title}}</strong> copiat",
"CopyItems": "<strong>{{qty}}</strong> elemente copiate",
"Document": "Document",
"Duplicate": "Crează o copie",
"EmptyFile": "Fișier gol",
"EmptyFilterDescriptionText": "Niciun fișier sau dosar nu corespunde setărilor filtru. Aplicați un alt filtru sau îl eliminați ca să afișați toate fișiere.",
"EmptyFilterSubheadingText": "Niciun fișier corespunzător de afișat ",

View File

@ -3,7 +3,6 @@
"ArchivingData": "Se arhiveaza datele",
"ConnectingAccount": "Conectare cont",
"Copy": "Copiere",
"CopyOperation": "Copiază",
"CreateMasterFormFromFile": "Crearea unui șablon formă din fișier",
"DeleteFromTrash": "Elementele selectate au fost șterse cu succes din Coșul de gunoi",
"DeleteOperation": "Ștergere în curs de desfășurare",
@ -32,7 +31,6 @@
"NewFormFile": "Forma din fișier text",
"OwnerChange": "Schimbare proprietar",
"Presentations": "Prezentări",
"Restore": "Restabilire",
"Spreadsheets": "Foi de calcul",
"SubNewForm": "Necompletat",
"SubNewFormFile": "Din fișier text",
@ -46,4 +44,4 @@
"TypeTitleSkyDrive": "OneDrive",
"TypeTitleWebDav": "WebDAV",
"TypeTitleYandex": "Yandex.Disk"
}
}

View File

@ -13,7 +13,6 @@
"CopyItem": "<strong>{{title}}</strong> скопирован",
"CopyItems": "Скопировано элементов: <strong>{{qty}}</strong>",
"Document": "Документ",
"Duplicate": "Создать копию",
"EmptyFile": "Пустой файл",
"EmptyFilterDescriptionText": "В этом разделе нет файлов или папок, соответствующих фильтру. Пожалуйста, выберите другие параметры или очистите фильтр, чтобы показать все файлы в этом разделе. Вы можете также поискать нужный файл в других разделах.",
"EmptyFilterSubheadingText": "Здесь нет файлов, соответствующих этому фильтру",

View File

@ -1,21 +1,21 @@
{
"CommonSettings": "Общие настройки",
"ConnectAccounts": "Подключенные облака",
"ConnectAccountsSubTitle": "Вы еще не подключили сторонние облачные сервисы",
"ConnectAdminDescription": "Для успешного подключения введите нужные данные на <1>этой странице</1>.",
"ConnectDescription": "Вы можете подключить следующие сервисы к вашему аккаунту ONLYOFFICE. Они будут отображаться в папке 'Мои документы' и Вы сможете редактировать и сохранять все свои документы в едином рабочем пространстве. ",
"ConnectedCloud": "Подключить облако",
"ConnectMakeShared": "Сделать доступным и поместить в папку 'Общие'",
"ConnextOtherAccount": "Другой аккаунт",
"DisplayFavorites": "Показывать избранное",
"DisplayNotification": "Показывать оповещение при перемещении элемента в корзину",
"DisplayRecent": "Показывать последние",
"DisplayTemplates": "Показывать шаблоны",
"IntermediateVersion": "Хранить все сохраненные промежуточные версии",
"KeepIntermediateVersion": "Хранить промежуточные версии при редактировании",
"OriginalCopy": "Сохранять также копию файла в исходном формате",
"StoringFileVersion": "Хранение версий файлов",
"ThirdPartyBtn": "Разрешить пользователям подключать сторонние хранилища",
"ThirdPartySettings": "Подключенные облака",
"UpdateOrCreate": "Обновлять версию файла для существующего файла с таким же именем. В противном случае будет создаваться копия файла."
}
{
"CommonSettings": "Общие настройки",
"ConnectAccounts": "Подключенные облака",
"ConnectAccountsSubTitle": "Вы еще не подключили сторонние облачные сервисы",
"ConnectAdminDescription": "Для успешного подключения введите нужные данные на <1>этой странице</1>.",
"ConnectDescription": "Вы можете подключить следующие сервисы к вашему аккаунту ONLYOFFICE. Они будут отображаться в папке 'Мои документы' и Вы сможете редактировать и сохранять все свои документы в едином рабочем пространстве. ",
"ConnectedCloud": "Подключить облако",
"ConnectMakeShared": "Сделать доступным и поместить в папку 'Общие'",
"ConnextOtherAccount": "Другой аккаунт",
"DisplayFavorites": "Показывать избранное",
"DisplayNotification": "Показывать оповещение при перемещении элемента в корзину",
"DisplayRecent": "Показывать последние",
"DisplayTemplates": "Показывать шаблоны",
"IntermediateVersion": "Хранить все сохраненные промежуточные версии",
"KeepIntermediateVersion": "Хранить промежуточные версии при редактировании",
"OriginalCopy": "Сохранять также копию файла в исходном формате",
"StoringFileVersion": "Хранение версий файлов",
"ThirdPartyBtn": "Разрешить пользователям подключать сторонние хранилища",
"ThirdPartySettings": "Подключенные облака",
"UpdateOrCreate": "Обновлять версию файла для существующего файла с таким же именем. В противном случае будет создаваться копия файла."
}

View File

@ -4,7 +4,6 @@
"ButtonShareAccess": "Настройка доступа",
"ConnectingAccount": "Подключение аккаунта",
"Copy": "Копировать",
"CopyOperation": "Копирование",
"CreateMasterFormFromFile": "Создать шаблон формы из файла",
"DeleteFromTrash": "Выбранные элементы успешно удалены из корзины",
"DeleteOperation": "Удаление",
@ -39,7 +38,6 @@
"NewFormFile": "Форма из текстового файла",
"OwnerChange": "Сменить владельца",
"Presentations": "Презентации",
"Restore": "Восстановить",
"Spreadsheets": "Таблицы",
"SubNewForm": "Пустая",
"SubNewFormFile": " Из текстового файла",
@ -55,4 +53,4 @@
"TypeTitleSkyDrive": "OneDrive",
"TypeTitleWebDav": "WebDAV",
"TypeTitleYandex": "Яндекс.Диск"
}
}

View File

@ -13,7 +13,6 @@
"CopyItem": "<strong>{{title}}</strong> skopírované",
"CopyItems": "<strong>{{qty}}</strong> prvkov bolo skopírovaných",
"Document": "Dokument",
"Duplicate": "Vytvoriť kópiu",
"EmptyFile": "Prázdny súbor",
"EmptyFilterDescriptionText": "Tomuto filtru nevyhovujú žiadne súbory ani priečinky. Skúste použiť iný alebo vyčistite filter a zobrazte všetky súbory. ",
"EmptyFilterSubheadingText": "Pre tento filter sa tu nezobrazujú žiadne súbory",

View File

@ -4,7 +4,6 @@
"ButtonShareAccess": "Nastavenie zdieľania",
"ConnectingAccount": "Pripojenie účtu",
"Copy": "Kopírovať",
"CopyOperation": "Kopírovanie",
"CreateMasterFormFromFile": "Vytvoriť šablónu formulára zo súboru",
"DeleteFromTrash": "Vybraté prvky boli úspešne odstránené z koša",
"DeleteOperation": "Odstraňuje sa",
@ -33,7 +32,6 @@
"NewFormFile": "Šablóna zo textového súboru",
"OwnerChange": "Zmeniť vlastníka",
"Presentations": "Prezentácie",
"Restore": "Obnoviť",
"Spreadsheets": "Tabuľky",
"SubNewForm": "Prázdny",
"SubNewFormFile": "Z textového súboru",
@ -49,4 +47,4 @@
"TypeTitleSkyDrive": "OneDrive",
"TypeTitleWebDav": "WebDAV",
"TypeTitleYandex": "Yandex.Disk"
}
}

Some files were not shown because too many files have changed in this diff Show More