Merge branch 'release/rc-v1.2.0' of https://github.com/ONLYOFFICE/DocSpace into release/rc-v1.2.0

This commit is contained in:
Timofey Boyko 2023-01-17 17:42:25 +03:00
commit 13dd1bdd62
25 changed files with 354 additions and 247 deletions

View File

@ -79,6 +79,7 @@ services_name_backend+=(ASC.Web.Studio)
services_name_backend+=(ASC.Data.Backup.BackgroundTasks)
services_name_backend+=(ASC.ClearEvents)
services_name_backend+=(ASC.ApiSystem)
services_name_backend+=(ASC.Web.HealthChecks.UI)
# Publish backend services
for i in ${!services_name_backend[@]}; do

View File

@ -68,6 +68,7 @@
PROXY_HOST=${CONTAINER_PREFIX}proxy
DOCEDITOR_HOST=${CONTAINER_PREFIX}doceditor
LOGIN_HOST=${CONTAINER_PREFIX}login
HELTHCHECKS_HOST=${CONTAINER_PREFIX}healthchecks
# proxy upstream environment #
SERVICE_API_SYSTEM=${API_SYSTEM_HOST}:${SERVICE_PORT}

View File

@ -296,6 +296,16 @@ COPY --from=base --chown=onlyoffice:onlyoffice ${BUILD_PATH}/services/ASC.Web.St
CMD ["ASC.Web.Studio.dll", "ASC.Web.Studio"]
## ASC.Web.HealthChecks.UI ##
FROM dotnetrun AS healthchecks
WORKDIR ${BUILD_PATH}/services/ASC.Web.HealthChecks.UI/service
COPY --chown=onlyoffice:onlyoffice docker-healthchecks-entrypoint.sh ./docker-healthchecks-entrypoint.sh
COPY --from=base --chown=onlyoffice:onlyoffice ${BUILD_PATH}/services/ASC.Web.HealthChecks.UI/service/ .
ENTRYPOINT ["./docker-healthchecks-entrypoint.sh"]
CMD ["ASC.Web.HealthChecks.UI.dll", "ASC.Web.HealthChecks.UI"]
## ASC.Migration.Runner ##
FROM $DOTNET_RUN AS onlyoffice-migration-runner
ARG BUILD_PATH

View File

@ -133,3 +133,10 @@ services:
dockerfile: "${DOCKERFILE}"
target: onlyoffice-migration-runner
image: "${REPO}/${DOCKER_IMAGE_PREFIX}-migration-runner:${DOCKER_TAG}"
onlyoffice-healthchecks:
build:
context: ./
dockerfile: "${DOCKERFILE}"
target: healthchecks
image: "${REPO}/${DOCKER_IMAGE_PREFIX}-healthchecks:${DOCKER_TAG}"

View File

@ -0,0 +1,46 @@
#!/bin/bash
# read parameters
if [ -n "$1" ]; then
RUN_DLL="${1}";
shift
fi
if [ -n "$1" ]; then
NAME_SERVICE="${1}";
shift
fi
echo "Executing -- ${NAME_SERVICE}"
PRODUCT=${PRODUCT:-"onlyoffice"}
CONTAINER_PREFIX=${PRODUCT}-
SERVICE_PORT=${SERVICE_PORT:-"5050"}
SHEME=${SHEME:-"http"}
URLS=${URLS:-"${SHEME}://0.0.0.0:${SERVICE_PORT}"}
PATH_TO_CONF=${PATH_TO_CONF:-"/var/www/services/ASC.Web.HealthChecks.UI/service"}
API_SYSTEM_HOST=${API_SYSTEM_HOST:-"${CONTAINER_PREFIX}api-system:${SERVICE_PORT}"}
BACKUP_HOST=${BACKUP_HOST:-"${CONTAINER_PREFIX}backup:${SERVICE_PORT}"}
BACKUP_BACKGRUOND_TASKS_HOST=${BACKUP_BACKGRUOND_TASKS_HOST:-"${CONTAINER_PREFIX}backup-background-tasks:${SERVICE_PORT}"}
CLEAR_EVENTS_HOST=${CLEAR_EVENTS_HOST:-"${CONTAINER_PREFIX}clear-events:${SERVICE_PORT}"}
FILES_HOST=${FILES_HOST:-"${CONTAINER_PREFIX}files:${SERVICE_PORT}"}
FILES_SERVICES_HOST=${FILES_SERVICES_HOST:-"${CONTAINER_PREFIX}files-services:${SERVICE_PORT}"}
NOTIFY_HOST=${NOTIFY_HOST:-"${CONTAINER_PREFIX}notify:${SERVICE_PORT}"}
PEOPLE_SERVER_HOST=${PEOPLE_SERVER_HOST:-"${CONTAINER_PREFIX}people-server:${SERVICE_PORT}"}
STUDIO_NOTIFY_HOST=${STUDIO_NOTIFY_HOST:-"${CONTAINER_PREFIX}studio-notify:${SERVICE_PORT}"}
API_HOST=${API_HOST:-"${CONTAINER_PREFIX}api:${SERVICE_PORT}"}
STUDIO_HOST=${STUDIO_HOST:-"${CONTAINER_PREFIX}studio:${SERVICE_PORT}"}
sed -i "s!localhost:5010!${API_SYSTEM_HOST}!g" ${PATH_TO_CONF}/appsettings.json
sed -i "s!localhost:5012!${BACKUP_HOST}!g" ${PATH_TO_CONF}/appsettings.json
sed -i "s!localhost:5032!${BACKUP_BACKGRUOND_TASKS_HOST}!g" ${PATH_TO_CONF}/appsettings.json
sed -i "s!localhost:5027!${CLEAR_EVENTS_HOST}!g" ${PATH_TO_CONF}/appsettings.json
sed -i "s!localhost:5007!${FILES_HOST}!g" ${PATH_TO_CONF}/appsettings.json
sed -i "s!localhost:5009!${FILES_SERVICES_HOST}!g" ${PATH_TO_CONF}/appsettings.json
sed -i "s!localhost:5005!${NOTIFY_HOST}!g" ${PATH_TO_CONF}/appsettings.json
sed -i "s!localhost:5004!${PEOPLE_SERVER_HOST}!g" ${PATH_TO_CONF}/appsettings.json
sed -i "s!localhost:5006!${API_HOST}!g" ${PATH_TO_CONF}/appsettings.json
sed -i "s!localhost:5000!${STUDIO_NOTIFY_HOST}!g" ${PATH_TO_CONF}/appsettings.json
sed -i "s!localhost:5003!${STUDIO_HOST}!g" ${PATH_TO_CONF}/appsettings.json
dotnet ${RUN_DLL} --urls=${URLS}

View File

@ -0,0 +1,29 @@
version: "3.8"
x-service:
&x-service-base
container_name: base
restart: always
expose:
- ${SERVICE_PORT}
volumes:
#- /app/onlyoffice/CommunityServer/data:/app/onlyoffice/data
- app_data:/app/onlyoffice/data
- files_data:/var/www/products/ASC.Files/server/
- people_data:/var/www/products/ASC.People/server/
services:
onlyoffice-health-checks-ui:
<<: *x-service-base
image: "${REPO}/${DOCKER_IMAGE_PREFIX}-healthchecks:${DOCKER_TAG}"
container_name: ${HELTHCHECKS_HOST}
networks:
default:
external:
name: ${NETWORK_NAME}
volumes:
app_data:
files_data:
people_data:

View File

@ -42,8 +42,10 @@ public class MigrationCreator
private string _pathToSave;
private string _userName;
private string _mail;
private string _toRegion;
private int _tenant;
private string _toRegion;
private string _toAlias;
private string _fromAlias;
private int _fromTenantId;
private readonly object _locker = new object();
private readonly int _limit = 1000;
private readonly List<ModuleName> _namesModules = new List<ModuleName>()
@ -55,6 +57,13 @@ public class MigrationCreator
ModuleName.WebStudio
};
private readonly List<ModuleName> _namesModulesForAlreadyExistPortal = new List<ModuleName>()
{
ModuleName.Core,
ModuleName.Files,
ModuleName.Files2,
};
public string NewAlias { get; private set; }
public MigrationCreator(
@ -73,9 +82,9 @@ public class MigrationCreator
_moduleProvider = moduleProvider;
}
public string Create(int tenant, string userName, string mail, string toRegion)
public string Create(string fromAlias, string userName, string mail, string toRegion, string toAlias)
{
Init(tenant, userName, mail, toRegion);
Init(fromAlias, userName, mail, toRegion, toAlias);
var id = GetUserId();
var fileName = _userName + ".tar.gz";
@ -88,49 +97,92 @@ public class MigrationCreator
return fileName;
}
private void Init(int tenant, string userName, string mail, string toRegion)
private void Init(string fromAlias, string userName, string mail, string toRegion, string toAlias)
{
_modules = _moduleProvider.AllModules.Where(m => _namesModules.Contains(m.ModuleName)).ToList();
_pathToSave = "";
_toRegion = toRegion;
_userName = userName;
_mail = mail;
_tenant = tenant;
_fromAlias = fromAlias;
_toAlias = toAlias;
using var dbContextTenant = _dbFactory.CreateDbContext<TenantDbContext>();
var tenant = dbContextTenant.Tenants.SingleOrDefault(q => q.Alias == _fromAlias);
if (tenant == null)
{
throw new ArgumentException("tenant was not found");
}
_fromTenantId = tenant.Id;
_modules = string.IsNullOrEmpty(_toAlias)
? _moduleProvider.AllModules.Where(m => _namesModules.Contains(m.ModuleName)).ToList()
: _moduleProvider.AllModules.Where(m => _namesModulesForAlreadyExistPortal.Contains(m.ModuleName)).ToList();
}
private Guid GetUserId()
{
try
{
var userDbContext = _dbFactory.CreateDbContext<UserDbContext>();
using var userDbContext = _dbFactory.CreateDbContext<UserDbContext>();
User user = null;
if (string.IsNullOrEmpty(_userName) || string.IsNullOrEmpty(_mail))
{
if (string.IsNullOrEmpty(_userName))
{
user = userDbContext.Users.FirstOrDefault(q => q.Tenant == _tenant && q.Status == EmployeeStatus.Active && q.Email == _mail);
user = userDbContext.Users.FirstOrDefault(q => q.Tenant == _fromTenantId && q.Status == EmployeeStatus.Active && q.Email == _mail);
_userName = user.UserName;
}
else
{
user = userDbContext.Users.FirstOrDefault(q => q.Tenant == _tenant && q.Status == EmployeeStatus.Active && q.UserName == _userName);
user = userDbContext.Users.FirstOrDefault(q => q.Tenant == _fromTenantId && q.Status == EmployeeStatus.Active && q.UserName == _userName);
}
}
else
{
user = userDbContext.Users.FirstOrDefault(q => q.Tenant == _tenant && q.Status == EmployeeStatus.Active && q.UserName == _userName && q.Email == _mail);
user = userDbContext.Users.FirstOrDefault(q => q.Tenant == _fromTenantId && q.Status == EmployeeStatus.Active && q.UserName == _userName && q.Email == _mail);
}
if (!string.IsNullOrEmpty(_toAlias))
{
using var dbContextTenant = _dbFactory.CreateDbContext<TenantDbContext>(_toRegion);
using var userDbContextToregion = _dbFactory.CreateDbContext<UserDbContext>(_toRegion);
var tenant = dbContextTenant.Tenants.SingleOrDefault(t => t.Alias == _toAlias);
if (tenant == null)
{
throw new ArgumentException("tenant was not found");
}
else
{
if (userDbContextToregion.Users.Any(q => q.Tenant == tenant.Id && q.UserName == _userName || q.Email == _mail))
{
throw new ArgumentException("username already exist in the portal");
}
}
}
return user.Id;
}
catch (ArgumentException e)
{
throw e;
}
catch (Exception)
{
throw new Exception("username was not found");
throw new ArgumentException("username was not found");
}
}
private void DoMigrationDb(Guid id, IDataWriteOperator writer)
{
{
if (!string.IsNullOrEmpty(_toAlias))
{
using (var connection = _dbFactory.OpenConnection())
{
var tenantsModule = _moduleProvider.AllModules.Single(q => q.ModuleName == ModuleName.Tenants);
var coreUserTable = tenantsModule.Tables.Single(q => q.Name == "core_user");
ArhiveTable(coreUserTable, writer, tenantsModule, connection, id);
}
}
foreach (var module in _modules)
{
var tablesToProcess = module.Tables.Where(t => t.InsertMethod != InsertMethod.None).ToList();
@ -143,55 +195,63 @@ public class MigrationCreator
{
continue;
}
Console.WriteLine($"backup table {table.Name}");
using (var data = new DataTable(table.Name))
{
ActionInvoker.Try(
state =>
{
data.Clear();
int counts;
var offset = 0;
do
{
var t = (TableInfo)state;
var dataAdapter = _dbFactory.CreateDataAdapter();
dataAdapter.SelectCommand = module.CreateSelectCommand(connection.Fix(), _tenant, t, _limit, offset, id).WithTimeout(600);
counts = ((DbDataAdapter)dataAdapter).Fill(data);
offset += _limit;
} while (counts == _limit);
},
table,
maxAttempts: 5,
onFailure: error => { throw ThrowHelper.CantBackupTable(table.Name, error); });
foreach (var col in data.Columns.Cast<DataColumn>().Where(col => col.DataType == typeof(DateTime)))
{
col.DateTimeMode = DataSetDateTime.Unspecified;
}
module.PrepareData(data);
if (data.TableName == "tenants_tenants")
{
ChangeAlias(data);
ChangeName(data);
}
using (var file = _tempStream.Create())
{
data.WriteXml(file, XmlWriteMode.WriteSchema);
data.Clear();
writer.WriteEntry(KeyHelper.GetTableZipKey(module, data.TableName), file);
}
}
ArhiveTable(table, writer, module, connection, id);
}
}
}
}
private void ArhiveTable(TableInfo table, IDataWriteOperator writer, IModuleSpecifics module, DbConnection connection, Guid id)
{
Console.WriteLine($"backup table {table.Name}");
using (var data = new DataTable(table.Name))
{
ActionInvoker.Try(
state =>
{
data.Clear();
int counts;
var offset = 0;
do
{
var t = (TableInfo)state;
var dataAdapter = _dbFactory.CreateDataAdapter();
dataAdapter.SelectCommand = module.CreateSelectCommand(connection.Fix(), _fromTenantId, t, _limit, offset, id).WithTimeout(600);
counts = ((DbDataAdapter)dataAdapter).Fill(data);
offset += _limit;
} while (counts == _limit);
},
table,
maxAttempts: 5,
onFailure: error => { throw ThrowHelper.CantBackupTable(table.Name, error); });
foreach (var col in data.Columns.Cast<DataColumn>().Where(col => col.DataType == typeof(DateTime)))
{
col.DateTimeMode = DataSetDateTime.Unspecified;
}
module.PrepareData(data);
if (data.TableName == "tenants_tenants")
{
ChangeAlias(data);
ChangeName(data);
}
WriteEnrty(data, writer, module);
}
}
private void WriteEnrty(DataTable data, IDataWriteOperator writer, IModuleSpecifics module)
{
using (var file = _tempStream.Create())
{
data.WriteXml(file, XmlWriteMode.WriteSchema);
data.Clear();
writer.WriteEntry(KeyHelper.GetTableZipKey(module, data.TableName), file);
}
}
private void ChangeAlias(DataTable data)
@ -255,8 +315,8 @@ public class MigrationCreator
private void ChangeName(DataTable data)
{
data.Rows[0]["name"] = "";
}
}
private void DoMigrationStorage(Guid id, IDataWriteOperator writer)
{
Console.WriteLine($"start backup storage");
@ -266,7 +326,7 @@ public class MigrationCreator
Console.WriteLine($"start backup fileGroup: {group.Key}");
foreach (var file in group)
{
var storage = _storageFactory.GetStorage(_tenant, group.Key);
var storage = _storageFactory.GetStorage(_fromTenantId, group.Key);
var file1 = file;
ActionInvoker.Try(state =>
{
@ -307,9 +367,9 @@ public class MigrationCreator
var module = _storageFactoryConfig.GetModuleList().Where(m => m == "files").Single();
var store = _storageFactory.GetStorage(_tenant, module);
var store = _storageFactory.GetStorage(_fromTenantId, module);
var dbFiles = filesDbContext.Files.Where(q => q.CreateBy == id && q.TenantId == _tenant).ToList();
var dbFiles = filesDbContext.Files.Where(q => q.CreateBy == id && q.TenantId == _fromTenantId).ToList();
var tasks = new List<Task>(20);
foreach (var dbFile in dbFiles)
@ -331,7 +391,7 @@ public class MigrationCreator
private async Task FindFiles(List<BackupFileInfo> list, IDataStore store, DbFile dbFile, string module)
{
var files = await store.ListFilesRelativeAsync(string.Empty, $"\\{GetUniqFileDirectory(dbFile.Id)}", "*.*", true)
.Select(path => new BackupFileInfo(string.Empty, module, $"{GetUniqFileDirectory(dbFile.Id)}\\{path}", _tenant))
.Select(path => new BackupFileInfo(string.Empty, module, $"{GetUniqFileDirectory(dbFile.Id)}\\{path}", _fromTenantId))
.ToListAsync();
lock (_locker)

View File

@ -61,13 +61,29 @@ public class MigrationRunner
_logger = logger;
}
public async Task Run(string backupFile, string region)
public async Task Run(string backupFile, string region, string fromAlias, string toAlias)
{
_region = region;
_modules = _moduleProvider.AllModules.Where(m => _namesModules.Contains(m.ModuleName)).ToList();
_backupFile = backupFile;
var columnMapper = new ColumnMapper();
if (!string.IsNullOrEmpty(toAlias))
{
using var dbContextTenant = _dbFactory.CreateDbContext<TenantDbContext>();
var fromTenant = dbContextTenant.Tenants.SingleOrDefault(q => q.Alias == fromAlias);
using var dbContextToTenant = _dbFactory.CreateDbContext<TenantDbContext>(region);
var toTenant = dbContextToTenant.Tenants.SingleOrDefault(q => q.Alias == toAlias);
toTenant.Status = TenantStatus.Restoring;
toTenant.StatusChanged = DateTime.UtcNow;
dbContextTenant.Tenants.Update(toTenant);
dbContextToTenant.SaveChanges();
columnMapper.SetMapping("tenants_tenants", "id", fromTenant.Id, toTenant.Id);
columnMapper.Commit();
}
using (var dataReader = new ZipReadOperator(_backupFile))
{
foreach (var module in _modules)

View File

@ -37,8 +37,7 @@ var param = Parser.Default.ParseArguments<Options>(args).Value;
{
FromRegion = "personal",
ToRegion = "personal",
Tenant = 1,
UserName = "administrator"
FromAlias = "localhost"
};*/
var builder = WebApplication.CreateBuilder(options);
@ -94,11 +93,12 @@ if(string.IsNullOrEmpty(param.UserName) && string.IsNullOrEmpty(param.Mail))
var app = builder.Build();
Console.WriteLine("backup start");
var migrationCreator = app.Services.GetService<MigrationCreator>();
var fileName = migrationCreator.Create(param.Tenant, param.UserName, param.Mail, param.ToRegion);
var fileName = migrationCreator.Create(param.FromAlias, param.UserName, param.Mail, param.ToRegion, param.ToAlias);
Console.WriteLine("backup was success");
Console.WriteLine("restore start");
var migrationRunner = app.Services.GetService<MigrationRunner>();
await migrationRunner.Run(fileName, param.ToRegion);
await migrationRunner.Run(fileName, param.ToRegion, param.FromAlias, param.ToAlias);
Console.WriteLine("restore was success");
Directory.GetFiles(AppContext.BaseDirectory).Where(f => f.Equals(fileName)).ToList().ForEach(File.Delete);
@ -109,12 +109,14 @@ if (Directory.Exists(AppContext.BaseDirectory + "\\temp"))
}
Console.WriteLine("migration was success");
Console.WriteLine($"new alias is - {migrationCreator.NewAlias}");
if (!string.IsNullOrEmpty(migrationCreator.NewAlias))
{
Console.WriteLine($"new alias is - {migrationCreator.NewAlias}");
}
public sealed class Options
{
[Option('t', "tenant", Required = true)]
public int Tenant { get; set; }
[Option('a', "FromAlias", Required = true)]
public string FromAlias { get; set; }
[Option('u', "username", Required = false, HelpText = "enter username or mail for find user")]
public string UserName { get; set; }
@ -122,9 +124,12 @@ public sealed class Options
[Option('m', "mail", Required = false, HelpText = "enter username or mail for find user")]
public string Mail { get; set; }
[Option("toregion", Required = true)]
[Option('t' ,"toregion", Required = true)]
public string ToRegion { get; set; }
[Option("fromregion", Required = false, Default = "personal")]
[Option('f' ,"fromregion", Required = false, Default = "personal")]
public string FromRegion { get; set; }
[Option("ToAlias", Required = false, HelpText = "if you wish migration to already exist portal, enter the alias")]
public string ToAlias { get; set; }
}

View File

@ -42,7 +42,7 @@
"property": [
{
"name": "$STORAGE_ROOT",
"value": "..\\Data\\"
"value": "..\\..\\..\\Data\\"
}
]
}
@ -90,7 +90,7 @@
"property": [
{
"name": "$STORAGE_ROOT",
"value": "..\\Data\\"
"value": "..\\..\\..\\Data\\"
}
]
}

View File

@ -1,120 +0,0 @@
{
"DbProviderFactories": {
"mysql": {
"name": "MySQL Data Provider",
"invariant": "MySql.Data.MySqlClient",
"description": ".Net Framework Data Provider for MySQL",
"type": "MySql.Data.MySqlClient.MySqlClientFactory, MySql.Data"
}
},
"core": {
"machinekey": "1123askdasjklasbnd",
"payment": {
"delay": 10,
"region": "test",
"test": true,
"url": "",
"key": "",
"secret": ""
}
},
"regions": {
"personal": {
"ConnectionStrings": {
"default": {
"name": "default",
"connectionString": "Server=localhost;Database=onlyoffice;User ID=dev;Password=dev;Pooling=true;Character Set=utf8;AutoEnlist=false;SSL Mode=none;AllowPublicKeyRetrieval=True;ConnectionReset=false",
"providerName": "MySql.Data.MySqlClient"
}
},
"storage": {
"appender": [
{
"name": "generic",
"append": "~/"
}
],
"handler": [
{
"name": "disc",
"type": "ASC.Data.Storage.DiscStorage.DiscDataStore, ASC.Data.Storage",
"property": [
{
"name": "$STORAGE_ROOT",
"value": "..\\Data\\"
}
]
}
],
"module": [
{
"name": "files",
"data": "e67be73d-f9ae-4ce1-8fec-1880cb518cb4",
"type": "disc",
"path": "$STORAGE_ROOT\\Products\\Files",
"expires": "0:16:0",
"domain": [
{
"name": "files_temp",
"visible": false,
"data": "00000000-0000-0000-0000-000000000000",
"path": "$STORAGE_ROOT\\Products\\Files\\{0}\\temp",
"virtualpath": "~/products/community/modules/wiki/data/filestemp",
"expires": "0:10:0"
}
]
}
]
}
},
"docSpace": {
"ConnectionStrings": {
"default": {
"name": "default",
"connectionString": "Server=localhost;Database=onlyoffice1;User ID=dev;Password=dev;Pooling=true;Character Set=utf8;AutoEnlist=false;SSL Mode=none;AllowPublicKeyRetrieval=True;ConnectionReset=false",
"providerName": "MySql.Data.MySqlClient"
}
},
"storage": {
"appender": [
{
"name": "generic",
"append": "~/"
}
],
"handler": [
{
"name": "disc",
"type": "ASC.Data.Storage.DiscStorage.DiscDataStore, ASC.Data.Storage",
"property": [
{
"name": "$STORAGE_ROOT",
"value": "..\\Data\\"
}
]
}
],
"module": [
{
"name": "files",
"data": "e67be73d-f9ae-4ce1-8fec-1880cb518cb4",
"type": "disc",
"path": "$STORAGE_ROOT\\Products\\Files1",
"expires": "0:16:0",
"domain": [
{
"name": "files_temp",
"visible": false,
"data": "00000000-0000-0000-0000-000000000000",
"path": "$STORAGE_ROOT\\Products\\Files1\\{0}\\temp",
"virtualpath": "~/products/community/modules/wiki/data/filestemp",
"expires": "0:10:0"
}
]
}
]
}
}
}
}

View File

@ -1,5 +1,4 @@
{
"DeleteGroupUsersSuccessMessage": "Benutzer wurden erfolgreich gelöscht.",
"DeleteUsers": "Benutzer löschen",
"DeleteUsersMessage": "Die ausgewählten deaktivierten Benutzer werden aus dem Portal gelöscht. Persönliche Dokumente dieser Benutzer, die für andere verfügbar sind, werden auch gelöscht. Um dies zu vermeiden, müssen Sie den Datenübertragungsprozess starten."
"DeleteUsers": "Benutzer löschen"
}

View File

@ -1,5 +1,5 @@
{
"DeleteGroupUsersSuccessMessage": "Users have been successfully deleted.",
"DeleteUsers": "Delete users",
"DeleteUsersMessage": "The selected disabled users will be deleted from the DocSpace. Personal documents of these users which are available to others will be deleted. To avoid this, you need to start the data transfer process before deleting."
"DeleteUsersMessage": "The selected disabled users will be deleted from the DocSpace. Personal documents of these users which are available to others will be deleted."
}

View File

@ -1,5 +1,4 @@
{
"DeleteGroupUsersSuccessMessage": "Les utilisateurs ont été supprimés avec succès.",
"DeleteUsers": "Supprimer les utilisateurs",
"DeleteUsersMessage": "Les utilisateurs désactivés sélectionnés seront supprimés du portail. Les documents personnels de ces utilisateurs qui sont partagés avec d'autres seront supprimés. Pour éviter cela, vous devez démarrer le processus de transfert de données avant la suppression."
"DeleteUsers": "Supprimer les utilisateurs"
}

View File

@ -1,5 +1,4 @@
{
"DeleteGroupUsersSuccessMessage": "Gli utenti sono stati eliminati con successo.",
"DeleteUsers": "Eliminare utenti",
"DeleteUsersMessage": "Gli utenti disabilitati selezionati verranno eliminati dal portale. I documenti personali di questi utenti disponibili per gli altri verranno eliminati. Per evitarlo, è necessario avviare il processo di trasferimento dei dati prima di eliminarli."
"DeleteUsers": "Eliminare utenti"
}

View File

@ -1,5 +1,4 @@
{
"DeleteGroupUsersSuccessMessage": "ユーザーが正常に削除されました。",
"DeleteUsers": "ユーザーを削除する",
"DeleteUsersMessage": "選択した無効のユーザーは、ポータルサイトから削除されます。これらのユーザーの個人文書で、他の人が利用できるものも削除されます。これを避けるには、削除する前にデータ転送プロセスを開始する必要があります。"
"DeleteUsers": "ユーザーを削除する"
}

View File

@ -1,5 +1,5 @@
{
"DeleteGroupUsersSuccessMessage": "Пользователи были успешно удалены",
"DeleteUsers": "Удалить пользователей",
"DeleteUsersMessage": "Выбранные заблокированные пользователи будут удалены с портала. Будут удалены личные документы этих пользователей, доступные для других. Чтобы избежать этого, нужно перед удалением запустить процесс передачи данных."
"DeleteUsersMessage": "Выбранные заблокированные пользователи будут удалены с портала. Будут удалены личные документы этих пользователей, доступные для других."
}

View File

@ -1,5 +1,4 @@
{
"DeleteGroupUsersSuccessMessage": "用户已成功删除。",
"DeleteUsers": "删除用户",
"DeleteUsersMessage": "选择的禁用用户将从门户网站中删除。这些用户对其他人可用的个人文档将被删除。为了避免这种情况,您必须在删除前启动数据传输过程。"
"DeleteUsers": "删除用户"
}

View File

@ -26,6 +26,7 @@ const EmptyFolderContainer = ({
tReady,
isLoadedFetchFiles,
viewAs,
setIsLoadedEmptyPage,
}) => {
const onBackToParentFolder = () => {
setIsLoading(true);
@ -35,10 +36,21 @@ const EmptyFolderContainer = ({
: fetchFiles(parentId).finally(() => setIsLoading(false));
};
React.useEffect(() => {
if (isLoadedFetchFiles && tReady) {
setIsLoadedEmptyPage(true);
} else {
setIsLoadedEmptyPage(false);
}
}, [isLoadedFetchFiles, tReady]);
React.useEffect(() => {
setIsEmptyPage(true);
return () => setIsEmptyPage(false);
return () => {
setIsEmptyPage(false);
setIsLoadedEmptyPage(false);
};
}, []);
const onInviteUsersClick = () => {
@ -161,6 +173,7 @@ export default inject(
setIsEmptyPage,
isLoadedFetchFiles,
viewAs,
setIsLoadedEmptyPage,
} = filesStore;
const {
navigationPath,
@ -198,6 +211,7 @@ export default inject(
folderId,
isLoadedFetchFiles,
viewAs,
setIsLoadedEmptyPage,
};
}
)(withTranslation(["Files", "Translations"])(observer(EmptyFolderContainer)));

View File

@ -48,6 +48,7 @@ const RootFolderContainer = (props) => {
setIsEmptyPage,
isVisitor,
sectionWidth,
setIsLoadedEmptyPage,
} = props;
const personalDescription = t("EmptyFolderDecription");
@ -87,8 +88,13 @@ const RootFolderContainer = (props) => {
return () => {
setIsEmptyPage(false);
setIsLoadedEmptyPage(false);
};
}, [isEmptyPage, setIsEmptyPage, rootFolderType]);
}, []);
React.useEffect(() => {
setIsLoadedEmptyPage(!isLoading);
}, [isLoading]);
const onGoToPersonal = () => {
const newFilter = filter.clone();
@ -313,31 +319,17 @@ const RootFolderContainer = (props) => {
const headerText = isPrivacyFolder ? privateRoomHeader : title;
const emptyFolderProps = getEmptyFolderProps();
React.useEffect(() => {
let timeout;
if (isLoading) {
setShowLoader(isLoading);
} else {
timeout = setTimeout(() => setShowLoader(isLoading), 300);
}
return () => clearTimeout(timeout);
}, [isLoading]);
if (isLoading) {
return <Loaders.EmptyContainerLoader viewAs={viewAs} />;
}
return (
<>
{showLoader ? (
<Loaders.EmptyContainerLoader viewAs={viewAs} />
) : (
<EmptyContainer
headerText={headerText}
isEmptyPage={isEmptyPage}
sectionWidth={sectionWidth}
{...emptyFolderProps}
/>
)}
</>
<EmptyContainer
headerText={headerText}
isEmptyPage={isEmptyPage}
sectionWidth={sectionWidth}
{...emptyFolderProps}
/>
);
};
@ -362,6 +354,7 @@ export default inject(
setAlreadyFetchingRooms,
isEmptyPage,
setIsEmptyPage,
setIsLoadedEmptyPage,
} = filesStore;
const { title, rootFolderType } = selectedFolderStore;
const { isPrivacyFolder, myFolderId } = treeFoldersStore;
@ -387,6 +380,7 @@ export default inject(
setAlreadyFetchingRooms,
isEmptyPage,
setIsEmptyPage,
setIsLoadedEmptyPage,
};
}
)(withTranslation(["Files"])(observer(RootFolderContainer)));

View File

@ -169,10 +169,23 @@ const SectionFilterContent = ({
searchTitleOpenLocation,
isLoadedLocationFiles,
setIsLoadedSearchFiles,
isLoadedEmptyPage,
isEmptyPage,
clearSearch,
setClearSearch,
}) => {
const [selectedFilterValues, setSelectedFilterValues] = React.useState(null);
const [isLoadedFilter, setIsLoadedFilter] = React.useState(false);
React.useEffect(() => {
if (isEmptyPage) {
setIsLoadedFilter(isLoadedEmptyPage);
}
if (!isEmptyPage && !isLoadedEmptyPage) {
setIsLoadedFilter(true);
}
}, [isLoadedEmptyPage, isEmptyPage]);
React.useEffect(() => {
if (!(searchTitleOpenLocation && isLoadedLocationFiles)) return;
@ -1300,6 +1313,10 @@ const SectionFilterContent = ({
}
};
if (!isLoadedFilter) {
return <Loaders.Filter />;
}
return (
<FilterInput
t={t}
@ -1353,6 +1370,8 @@ export default inject(
thirdPartyStore,
clearSearch,
setClearSearch,
isLoadedEmptyPage,
isEmptyPage,
} = filesStore;
const { providers } = thirdPartyStore;
@ -1411,6 +1430,9 @@ export default inject(
isLoadedLocationFiles,
setIsLoadedSearchFiles,
isLoadedEmptyPage,
isEmptyPage,
clearSearch,
setClearSearch,
};

View File

@ -492,6 +492,7 @@ class PureHome extends React.Component {
frameConfig,
withPaging,
isEmptyPage,
isLoadedEmptyPage,
} = this.props;
if (window.parent && !frameConfig) {
@ -538,7 +539,7 @@ class PureHome extends React.Component {
</Section.SectionHeader>
)}
{!isEmptyPage && !isErrorRoomNotAvailable && (
{!isLoadedEmptyPage && !isErrorRoomNotAvailable && (
<Section.SectionFilter>
{isFrame ? (
showFilter && <SectionFilterContent />
@ -548,6 +549,12 @@ class PureHome extends React.Component {
</Section.SectionFilter>
)}
{isLoadedEmptyPage && (
<Section.SectionFilter>
<div style={{ height: "32px" }} />
</Section.SectionFilter>
)}
<Section.SectionBody>
<Consumer>
{(context) => (
@ -622,7 +629,7 @@ export default inject(
refreshFiles,
setViewAs,
isEmptyPage,
isLoadedEmptyPage,
disableDrag,
isErrorRoomNotAvailable,
} = filesStore;
@ -773,6 +780,7 @@ export default inject(
setViewAs,
withPaging,
isEmptyPage,
isLoadedEmptyPage,
};
}
)(withRouter(observer(Home)));

View File

@ -113,6 +113,8 @@ class FilesStore {
clearSearch = false;
isLoadedEmptyPage = false;
constructor(
authStore,
selectedFolderStore,
@ -135,7 +137,6 @@ class FilesStore {
this.roomsController = new AbortController();
this.filesController = new AbortController();
const { socketHelper, withPaging } = authStore.settingsStore;
socketHelper.on("s:modify-folder", async (opt) => {
@ -438,6 +439,10 @@ class FilesStore {
this.isEmptyPage = isEmptyPage;
};
setIsLoadedEmptyPage = (isLoadedEmptyPage) => {
this.isLoadedEmptyPage = isLoadedEmptyPage;
};
get tooltipOptions() {
if (!this.dragging) return null;
@ -2958,22 +2963,26 @@ class FilesStore {
(f) => f.id === item.id && f.isFolder === item.isFolder
);
if (fileIndex === -1) {
this.setSelection([item, ...this.selection]);
this.setSelection([...this.selection, item]);
} else {
this.deselectFile(item);
}
};
withShiftSelect = (item) => {
const caretStart = this.hotkeyCaretStart
? this.hotkeyCaretStart
: this.filesList[0];
const caret = this.hotkeyCaret ? this.hotkeyCaret : caretStart;
if (!caret || !caretStart) return;
const startCaretIndex = this.filesList.findIndex(
(f) =>
f.id === this.hotkeyCaretStart.id &&
f.isFolder === this.hotkeyCaretStart.isFolder
(f) => f.id === caretStart.id && f.isFolder === caretStart.isFolder
);
const caretIndex = this.filesList.findIndex(
(f) =>
f.id === this.hotkeyCaret.id && f.isFolder === this.hotkeyCaret.isFolder
(f) => f.id === caret.id && f.isFolder === caret.isFolder
);
const itemIndex = this.filesList.findIndex(
@ -2994,6 +3003,11 @@ class FilesStore {
);
if (selectionIndex === -1) {
newSelection.push(filesItem);
} else {
newSelection = newSelection.filter(
(_, fIndex) => selectionIndex !== fIndex
);
newSelection.push(filesItem);
}
if (isMoveDown) {
@ -3003,13 +3017,18 @@ class FilesStore {
}
}
const lastSelection = this.selection[this.selection.length - 1];
const indexOfLast = this.filesList.findIndex(
(f) => f.id === lastSelection.id && f.isFolder === lastSelection.isFolder
);
newSelection = newSelection.filter((f) => {
const listIndex = this.filesList.findIndex(
(x) => x.id === f.id && x.isFolder === f.isFolder
);
if (isMoveDown) {
const isSelect = itemIndex > startCaretIndex;
const isSelect = listIndex < indexOfLast;
if (isSelect) return true;
if (listIndex >= startCaretIndex) {
@ -3018,7 +3037,7 @@ class FilesStore {
return listIndex >= itemIndex;
}
} else {
const isSelect = itemIndex < startCaretIndex;
const isSelect = listIndex > indexOfLast;
if (isSelect) return true;
if (listIndex <= startCaretIndex) {