Merge branch 'feature/virtual-rooms-1.2' into hotfix/campaigns-banner
This commit is contained in:
commit
7abe990d96
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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; }
|
||||
|
@ -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>();
|
||||
}
|
||||
}
|
||||
}
|
@ -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>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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();
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
{
|
||||
|
@ -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");
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,5 @@
|
||||
{
|
||||
"kafka": {
|
||||
"BootstrapServers": "localhost:9092"
|
||||
}
|
||||
}
|
@ -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;
|
||||
|
@ -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",
|
||||
|
@ -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 = {
|
||||
|
@ -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,
|
||||
|
@ -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;
|
@ -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;
|
@ -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);
|
@ -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);
|
@ -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);
|
@ -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);
|
||||
|
@ -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);
|
@ -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);
|
@ -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);
|
@ -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);
|
||||
|
@ -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;
|
@ -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;
|
@ -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;
|
@ -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;
|
||||
|
@ -341,6 +341,7 @@ const ErrorContainer = (props) => {
|
||||
{headerText}
|
||||
</Headline>
|
||||
)}
|
||||
{children}
|
||||
{bodyText && <Text id="text">{bodyText}</Text>}
|
||||
{buttonText && buttonUrl && (
|
||||
<div id="button-container">
|
||||
|
@ -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;
|
@ -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 };
|
@ -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;
|
@ -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,
|
||||
};
|
||||
|
@ -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));
|
||||
|
@ -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,
|
||||
});
|
||||
|
@ -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) => {
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
|
@ -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>
|
||||
|
@ -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 };
|
||||
|
@ -39,4 +39,4 @@ Heading.defaultProps = {
|
||||
className: "",
|
||||
};
|
||||
|
||||
export default Heading;
|
||||
export default React.memo(Heading);
|
||||
|
@ -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,
|
||||
|
@ -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;
|
||||
|
@ -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%;
|
||||
`}
|
||||
|
@ -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 = {
|
||||
|
@ -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;
|
||||
}
|
||||
`}
|
||||
}
|
||||
`;
|
||||
|
@ -66,4 +66,4 @@ Text.defaultProps = {
|
||||
noSelect: false,
|
||||
};
|
||||
|
||||
export default Text;
|
||||
export default React.memo(Text);
|
||||
|
@ -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",
|
||||
|
@ -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: {
|
||||
|
@ -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ı",
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
@ -13,7 +13,6 @@
|
||||
"CopyItem": "<strong>{{title}}</strong> е копиран",
|
||||
"CopyItems": "<strong>{{qty}}</strong> елемента са копирани",
|
||||
"Document": "Документ",
|
||||
"Duplicate": "Суздай копие",
|
||||
"EmptyFile": "Изпразни файл",
|
||||
"EmptyFilterDescriptionText": "Нито един файл или папка не пасват на този филтър. Изпробвайте друг или изчистете филтъра, за да видите всички файлове. ",
|
||||
"EmptyFilterSubheadingText": "Няма файлове, които да бъдат показани за този филтър тук",
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
@ -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í",
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
@ -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",
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
@ -13,7 +13,6 @@
|
||||
"CopyItem": "Το <strong>{{{title}}</strong> αντιγράφηκε",
|
||||
"CopyItems": "<strong> {{qty}} </strong> στοιχεία αντιγράφηκαν",
|
||||
"Document": "Έγγραφο",
|
||||
"Duplicate": "Δημιουργήστε ένα αντίγραφο",
|
||||
"EmptyFile": "Κενό αρχείο",
|
||||
"EmptyFilterDescriptionText": "Με αυτό το φίλτρο δεν ταιριάζει κανένα αρχείο ή φάκελος. Δοκιμάστε ένα διαφορετικό ή καταργήστε το φίλτρο για να δείτε όλα τα αρχεία.",
|
||||
"EmptyFilterSubheadingText": "Δεν υπάρχουν αρχεία για εμφάνιση για αυτό το φίλτρο",
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
@ -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",
|
||||
|
@ -1,3 +1,3 @@
|
||||
{
|
||||
"MoveToFolderMessage": "You can't move the folder to its subfolder"
|
||||
}
|
||||
{
|
||||
"MoveToFolderMessage": "You can't move the folder to its subfolder"
|
||||
}
|
||||
|
@ -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."
|
||||
}
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
@ -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í",
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
@ -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",
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
@ -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",
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
@ -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",
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
@ -13,7 +13,6 @@
|
||||
"CopyItem": "<strong>{{title}}</strong>のコピー",
|
||||
"CopyItems": "<strong>{{qty}}</strong>要素のコピー",
|
||||
"Document": "ドキュメント",
|
||||
"Duplicate": "コピーの作成",
|
||||
"EmptyFile": "空きファイル",
|
||||
"EmptyFilterDescriptionText": "このフィルターに一致するファイルやフォルダーはありません。別のフィルタを試すか、フィルタを解除してすべてのファイルを表示してください。 ",
|
||||
"EmptyFilterSubheadingText": "このフィルターで表示されるファイルはありません。",
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
@ -13,7 +13,6 @@
|
||||
"CopyItem": "<strong>{{title}}</strong> 복사 완료",
|
||||
"CopyItems": "<strong>{{qty}}</strong> 요소 복사 완료",
|
||||
"Document": "문서",
|
||||
"Duplicate": "복사본 생성",
|
||||
"EmptyFile": "빈 파일",
|
||||
"EmptyFilterDescriptionText": "이 필터에 해당하는 파일이나 폴더가 없습니다. 다르게 시도하거나 필터를 삭제하여 모든 파일을 확인하세요.",
|
||||
"EmptyFilterSubheadingText": "이 필터에 대해 표시할 파일이 없습니다",
|
||||
@ -77,4 +76,4 @@
|
||||
"UploadToFolder": "폴더에 업로드",
|
||||
"ViewList": "목록",
|
||||
"ViewTiles": "제목"
|
||||
}
|
||||
}
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
@ -13,7 +13,6 @@
|
||||
"CopyItem": "<strong>{{title}}</strong> ຄັດລອກ",
|
||||
"CopyItems": "<strong>{{qty}}</strong> ອົງປະກອບທີ່ຖືກຄັດລອກ",
|
||||
"Document": "ເອກະສານ",
|
||||
"Duplicate": "ສ້າງສຳເນົາ",
|
||||
"EmptyFile": "ເອກະສານຫວ່າງເປົ່າ",
|
||||
"EmptyFilterDescriptionText": "ບໍ່ມີໄຟລ໌ ຫລື ແຟ້ມໃດທີ່ກົງກັບຕົວກອງນີ້. ລອງໃຊ້ຕົວກັ່ນຕອງອື່ນ ຫຼື ລຶບລ້າງຂໍ້ມູນເພື່ອເບິ່ງເອກະສານທັງໝົດ.",
|
||||
"EmptyFilterSubheadingText": "ບໍ່ມີເອກະສານທີ່ສະແດງສຳລັບຕົວກອງຢູ່ທີ່ນີ້",
|
||||
@ -77,4 +76,4 @@
|
||||
"UploadToFolder": "ອັບໂຫລດໄປຍັງໂຟຣເດີ",
|
||||
"ViewList": "ລາຍການ",
|
||||
"ViewTiles": "ຫົວຂໍ້"
|
||||
}
|
||||
}
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
@ -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",
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
@ -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",
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
@ -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",
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
@ -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",
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
@ -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 ",
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
@ -13,7 +13,6 @@
|
||||
"CopyItem": "<strong>{{title}}</strong> скопирован",
|
||||
"CopyItems": "Скопировано элементов: <strong>{{qty}}</strong>",
|
||||
"Document": "Документ",
|
||||
"Duplicate": "Создать копию",
|
||||
"EmptyFile": "Пустой файл",
|
||||
"EmptyFilterDescriptionText": "В этом разделе нет файлов или папок, соответствующих фильтру. Пожалуйста, выберите другие параметры или очистите фильтр, чтобы показать все файлы в этом разделе. Вы можете также поискать нужный файл в других разделах.",
|
||||
"EmptyFilterSubheadingText": "Здесь нет файлов, соответствующих этому фильтру",
|
||||
|
@ -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": "Обновлять версию файла для существующего файла с таким же именем. В противном случае будет создаваться копия файла."
|
||||
}
|
||||
|
@ -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": "Яндекс.Диск"
|
||||
}
|
||||
}
|
||||
|
@ -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",
|
||||
|
@ -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
Loading…
Reference in New Issue
Block a user