diff --git a/ASC.Web.sln b/ASC.Web.sln index 59c50743b2..13419aedf1 100644 --- a/ASC.Web.sln +++ b/ASC.Web.sln @@ -79,7 +79,11 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ASC.Data.Encryption", "comm EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ASC.Projects", "products\ASC.Projects\Server\ASC.Projects.csproj", "{D1A33923-5680-4B86-A7DA-41E78350D997}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ASC.CRM", "products\ASC.CRM\Server\ASC.CRM.csproj", "{277F4A2C-07CC-4BC5-B4F3-9695BB2DFFB9}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ASC.CRM", "products\ASC.CRM\Server\ASC.CRM.csproj", "{277F4A2C-07CC-4BC5-B4F3-9695BB2DFFB9}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ASC.Mail", "products\ASC.Mail\Server\ASC.Mail.csproj", "{137CA67B-D0F5-4746-B8BC-1888D2859B90}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ASC.Calendar", "products\ASC.Calendar\Server\ASC.Calendar.csproj", "{F39933F8-7598-492F-9DD3-E25780D68288}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -227,6 +231,14 @@ Global {277F4A2C-07CC-4BC5-B4F3-9695BB2DFFB9}.Debug|Any CPU.Build.0 = Debug|Any CPU {277F4A2C-07CC-4BC5-B4F3-9695BB2DFFB9}.Release|Any CPU.ActiveCfg = Release|Any CPU {277F4A2C-07CC-4BC5-B4F3-9695BB2DFFB9}.Release|Any CPU.Build.0 = Release|Any CPU + {137CA67B-D0F5-4746-B8BC-1888D2859B90}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {137CA67B-D0F5-4746-B8BC-1888D2859B90}.Debug|Any CPU.Build.0 = Debug|Any CPU + {137CA67B-D0F5-4746-B8BC-1888D2859B90}.Release|Any CPU.ActiveCfg = Release|Any CPU + {137CA67B-D0F5-4746-B8BC-1888D2859B90}.Release|Any CPU.Build.0 = Release|Any CPU + {F39933F8-7598-492F-9DD3-E25780D68288}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F39933F8-7598-492F-9DD3-E25780D68288}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F39933F8-7598-492F-9DD3-E25780D68288}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F39933F8-7598-492F-9DD3-E25780D68288}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/build/install/common/publish-backend.sh b/build/install/common/publish-backend.sh index 3a0c3b87c4..93a54547d1 100644 --- a/build/install/common/publish-backend.sh +++ b/build/install/common/publish-backend.sh @@ -60,6 +60,8 @@ servers_products_name_backend=(ASC.CRM) servers_products_name_backend+=(ASC.Files) servers_products_name_backend+=(ASC.People) servers_products_name_backend+=(ASC.Projects) +servers_products_name_backend+=(ASC.Calendar) +servers_products_name_backend+=(ASC.Mail) # Publish server backend products for i in ${!servers_products_name_backend[@]}; do diff --git a/build/install/docker/.env b/build/install/docker/.env index 823635a2ee..ddf27d85e8 100644 --- a/build/install/docker/.env +++ b/build/install/docker/.env @@ -47,10 +47,12 @@ # service host # API_SYSTEM_HOST=${CONTAINER_PREFIX}api-system BACKUP_HOST=${CONTAINER_PREFIX}backup + CALENDAR_HOST=${CONTAINER_PREFIX}calendar CRM_HOST=${CONTAINER_PREFIX}crm STORAGE_ENCRYPTION_HOST=${CONTAINER_PREFIX}storage-encryption FILES_HOST=${CONTAINER_PREFIX}files FILES_SERVICES_HOST=${CONTAINER_PREFIX}files-services + MAIL_HOST=${CONTAINER_PREFIX}mail STORAGE_MIGRATION_HOST=${CONTAINER_PREFIX}storage-migration NOTIFY_HOST=${CONTAINER_PREFIX}notify PEOPLE_SERVER_HOST=${CONTAINER_PREFIX}people-server @@ -68,9 +70,11 @@ SERVICE_API_SYSTEM=${API_SYSTEM_HOST}:${SERVICE_PORT} SERVICE_BACKUP=${BACKUP_HOST}:${SERVICE_PORT} SERVICE_CRM=${CRM_HOST}:${SERVICE_PORT} + SERVICE_CALENDAR=${CALENDAR_HOST}:${SERVICE_PORT} SERVICE_STORAGE_ENCRYPTION=${STORAGE_ENCRYPTION_HOST}:${SERVICE_PORT} SERVICE_FILES=${FILES_HOST}:${SERVICE_PORT} SERVICE_FILES_SERVICES=${FILES_SERVICES_HOST}:${SERVICE_PORT} + SERVICE_MAIL=${MAIL_HOST}:${SERVICE_PORT} SERVICE_STORAGE_MIGRATION=${STORAGE_MIGRATION_HOST}:${SERVICE_PORT} SERVICE_NOTIFY=${NOTIFY_HOST}:${SERVICE_PORT} SERVICE_PEOPLE_SERVER=${PEOPLE_SERVER_HOST}:${SERVICE_PORT} diff --git a/build/install/docker/Dockerfile-app b/build/install/docker/Dockerfile-app index dd3d677de8..2ab5d1bdd3 100644 --- a/build/install/docker/Dockerfile-app +++ b/build/install/docker/Dockerfile-app @@ -45,11 +45,8 @@ RUN echo "nameserver 8.8.8.8" | tee /etc/resolv.conf > /dev/null && \ bash build-frontend.sh -sp ${SRC_PATH} && \ bash build-backend.sh -sp ${SRC_PATH} -ar "--disable-parallel" && \ bash publish-backend.sh -sp ${SRC_PATH} -bp ${BUILD_PATH} -ar "--disable-parallel" - -COPY config/mysql/conf.d/mysql.cnf /etc/mysql/conf.d/mysql.cnf -COPY config/supervisor/supervisord.conf /etc/supervisor/conf.d/supervisord.conf -RUN sed -i 's/Server=.*;Port=/Server=127.0.0.1;Port=/' /app/onlyoffice/config/appsettings.test.json +COPY config/mysql/conf.d/mysql.cnf /etc/mysql/conf.d/mysql.cnf RUN rm -rf /var/lib/apt/lists/* @@ -107,6 +104,8 @@ COPY --from=base ${SRC_PATH}/web/ASC.Web.Login/dist ${BUILD_PATH}/studio/login COPY --from=base ${SRC_PATH}/products/ASC.People/Client/dist ${BUILD_PATH}/products/ASC.People/client COPY --from=base ${SRC_PATH}/products/ASC.Projects/Client/dist ${BUILD_PATH}/products/ASC.Projects/client COPY --from=base ${SRC_PATH}/web/ASC.Web.Client/dist ${BUILD_PATH}/studio/client +COPY --from=base ${SRC_PATH}/products/ASC.Calendar/Client/dist ${BUILD_PATH}/products/ASC.Calendar/client +COPY --from=base ${SRC_PATH}/products/ASC.Mail/Client/dist ${BUILD_PATH}/products/ASC.Mail/client COPY /config/nginx/templates/upstream.conf.template /etc/nginx/templates/upstream.conf.template @@ -122,6 +121,8 @@ RUN chown nginx:nginx /etc/nginx/* -R && \ sed -i 's/localhost:5020/$service_projects_server/' /etc/nginx/conf.d/onlyoffice.conf && \ sed -i 's/localhost:5000/$service_api/' /etc/nginx/conf.d/onlyoffice.conf && \ sed -i 's/localhost:5003/$service_studio/' /etc/nginx/conf.d/onlyoffice.conf && \ + sed -i 's/localhost:5023/$service_calendar/' /etc/nginx/conf.d/onlyoffice.conf && \ + sed -i 's/localhost:5022/$service_mail/' /etc/nginx/conf.d/onlyoffice.conf && \ sed -i 's/localhost:9999/$service_urlshortener/' /etc/nginx/conf.d/onlyoffice.conf && \ sed -i 's/172.*/$document_server;/' /etc/nginx/conf.d/onlyoffice.conf && \ # configute the image nginx whith less privileged https://hub.docker.com/_/nginx @@ -146,6 +147,15 @@ COPY --from=base --chown=onlyoffice:onlyoffice ${BUILD_PATH}/services/ASC.Data.B CMD ["ASC.Data.Backup.dll", "ASC.Data.Backup", "core:products:folder=/var/www/products/", "core:products:subfolder=server"] +## ASC.Calendar ## +FROM builder AS calendar +WORKDIR ${BUILD_PATH}/products/ASC.Calendar/server/ + +COPY --chown=onlyoffice:onlyoffice docker-entrypoint.sh . +COPY --from=base --chown=onlyoffice:onlyoffice ${BUILD_PATH}/products/ASC.Calendar/server/ . + +CMD ["ASC.Calendar.dll", "ASC.Calendar"] + ## ASC.CRM ## FROM builder AS crm WORKDIR ${BUILD_PATH}/products/ASC.CRM/server/ @@ -182,6 +192,15 @@ COPY --from=base --chown=onlyoffice:onlyoffice ${BUILD_PATH}/services/ASC.Files. CMD ["ASC.Files.Service.dll", "ASC.Files.Service", "core:products:folder=/var/www/products/", "core:products:subfolder=server"] +## ASC.Mail ## +FROM builder AS mail +WORKDIR ${BUILD_PATH}/products/ASC.Mail/server/ + +COPY --chown=onlyoffice:onlyoffice docker-entrypoint.sh . +COPY --from=base --chown=onlyoffice:onlyoffice ${BUILD_PATH}/products/ASC.Mail/server/ . + +CMD ["ASC.Mail.dll", "ASC.Mail"] + ## ASC.Data.Storage.Migration ## FROM builder AS data_storage_migration WORKDIR ${BUILD_PATH}/services/storage.migration/service/ diff --git a/build/install/docker/appserver.yml b/build/install/docker/appserver.yml index 3eef1db072..4912140010 100644 --- a/build/install/docker/appserver.yml +++ b/build/install/docker/appserver.yml @@ -29,6 +29,9 @@ x-service: - people_data:/var/www/products/ASC.People/server/ - crm_data:/var/www/products/ASC.CRM/server/ - project_data:/var/www/products/ASC.Projects/server/ + - calendar_data:/var/www/products/ASC.Calendar/server/ + - mail_data:/var/www/products/ASC.Mail/server/ + services: onlyoffice-elasticsearch: @@ -95,6 +98,11 @@ services: image: "${REPO}/${STATUS}appserver-backup:${SRV_VERSION}" container_name: ${BACKUP_HOST} + onlyoffice-calendar: + <<: *x-service-base + image: "${REPO}/${STATUS}appserver-calendar:${SRV_VERSION}" + container_name: ${CALENDAR_HOST} + onlyoffice-crm: <<: *x-service-base image: "${REPO}/${STATUS}appserver-crm:${SRV_VERSION}" @@ -115,6 +123,11 @@ services: image: "${REPO}/${STATUS}appserver-files-services:${SRV_VERSION}" container_name: ${FILES_SERVICES_HOST} + onlyoffice-mail: + <<: *x-service-base + image: "${REPO}/${STATUS}appserver-mail:${SRV_VERSION}" + container_name: ${MAIL_HOST} + onlyoffice-storage-migration: <<: *x-service-base image: "${REPO}/${STATUS}appserver-storage-migration:${SRV_VERSION}" @@ -187,10 +200,12 @@ services: depends_on: - onlyoffice-api-system - onlyoffice-backup + - onlyoffice-calendar - onlyoffice-crm - onlyoffice-storage-encryption - onlyoffice-files - onlyoffice-files-services + - onlyoffice-mail - onlyoffice-storage-migration - onlyoffice-people-server - onlyoffice-projects-server @@ -204,10 +219,12 @@ services: environment: - SERVICE_API_SYSTEM=${SERVICE_API_SYSTEM} - SERVICE_BACKUP=${SERVICE_BACKUP} + - SERVICE_CALENDAR=${SERVICE_CALENDAR} - SERVICE_CRM=${SERVICE_CRM} - SERVICE_STORAGE_ENCRYPTION=${SERVICE_STORAGE_ENCRYPTION} - SERVICE_FILES=${SERVICE_FILES} - SERVICE_FILES_SERVICES=${SERVICE_FILES_SERVICES} + - SERVICE_MAIL=${SERVICE_MAIL} - SERVICE_STORAGE_MIGRATION=${SERVICE_STORAGE_MIGRATION} - SERVICE_NOTIFY=${SERVICE_NOTIFY} - SERVICE_PEOPLE_SERVER=${SERVICE_PEOPLE_SERVER} @@ -240,3 +257,5 @@ volumes: people_data: crm_data: project_data: + calendar_data: + mail_data: diff --git a/build/install/docker/build.yml b/build/install/docker/build.yml index 1e6209d635..9a43b41e51 100644 --- a/build/install/docker/build.yml +++ b/build/install/docker/build.yml @@ -15,6 +15,13 @@ services: target: backup image: "${REPO}/${STATUS}appserver-backup:${SRV_VERSION}" + onlyoffice-calendar: + build: + context: ./ + dockerfile: "${DOCKERFILE}" + target: calendar + image: "${REPO}/${STATUS}appserver-calendar:${SRV_VERSION}" + onlyoffice-crm: build: context: ./ @@ -43,6 +50,13 @@ services: target: files_services image: "${REPO}/${STATUS}appserver-files-services:${SRV_VERSION}" + onlyoffice-mail: + build: + context: ./ + dockerfile: "${DOCKERFILE}" + target: mail + image: "${REPO}/${STATUS}appserver-mail:${SRV_VERSION}" + onlyoffice-storage-migration: build: context: ./ diff --git a/build/install/docker/config/nginx/templates/upstream.conf.template b/build/install/docker/config/nginx/templates/upstream.conf.template index 675432e66c..a14517cd5a 100644 --- a/build/install/docker/config/nginx/templates/upstream.conf.template +++ b/build/install/docker/config/nginx/templates/upstream.conf.template @@ -10,6 +10,11 @@ map $SERVICE_BACKUP $service_backup { $SERVICE_BACKUP $SERVICE_BACKUP; } +map $SERVICE_CALENDAR $service_calendar { + volatile; + $SERVICE_CALENDAR $SERVICE_CALENDAR; +} + map $SERVICE_CRM $service_crm { volatile; $SERVICE_CRM $SERVICE_CRM; @@ -30,6 +35,11 @@ map $SERVICE_FILES_SERVICES $service_files_services { $SERVICE_FILES_SERVICES $SERVICE_FILES_SERVICES; } +map $SERVICE_MAIL $service_mail { + volatile; + $SERVICE_MAIL $SERVICE_MAIL; +} + map $SERVICE_STORAGE_MIGRATION $service_storage_migration { volatile; $SERVICE_STORAGE_MIGRATION $SERVICE_STORAGE_MIGRATION; diff --git a/build/install/docker/notify.yml b/build/install/docker/notify.yml index 0d813e085a..7a437cf4d3 100644 --- a/build/install/docker/notify.yml +++ b/build/install/docker/notify.yml @@ -29,6 +29,8 @@ x-service: - people_data:/var/www/products/ASC.People/server/ - crm_data:/var/www/products/ASC.CRM/server/ - project_data:/var/www/products/ASC.Projects/server/ + - calendar_data:/var/www/products/ASC.Calendar/server/ + - mail_data:/var/www/products/ASC.Mail/server/ services: onlyoffice-notify: @@ -47,3 +49,5 @@ volumes: people_data: crm_data: project_data: + calendar_data: + mail_data: diff --git a/build/run/CalendarClient.bat b/build/run/CalendarClient.bat new file mode 100644 index 0000000000..297b74d734 --- /dev/null +++ b/build/run/CalendarClient.bat @@ -0,0 +1,2 @@ +echo "RUN ASC.Web.Calendar" +call set BROWSER=none&&npm start --prefix ../../products/ASC.Calendar/Client \ No newline at end of file diff --git a/build/run/CalendarServer.bat b/build/run/CalendarServer.bat new file mode 100644 index 0000000000..1072ae298a --- /dev/null +++ b/build/run/CalendarServer.bat @@ -0,0 +1,2 @@ +echo "RUN ASC.Calendar" +call dotnet run --project ..\..\products\ASC.Calendar\Server\ASC.Calendar.csproj --no-build --$STORAGE_ROOT=..\..\..\Data --log__dir=..\..\..\Logs --log__name=calendar \ No newline at end of file diff --git a/build/run/MailClient.bat b/build/run/MailClient.bat new file mode 100644 index 0000000000..ea4da86a48 --- /dev/null +++ b/build/run/MailClient.bat @@ -0,0 +1,2 @@ +echo "RUN ASC.Web.Mail" +call set BROWSER=none&&npm start --prefix ../../products/ASC.Mail/Client \ No newline at end of file diff --git a/build/run/MailServer.bat b/build/run/MailServer.bat new file mode 100644 index 0000000000..d44b27df0e --- /dev/null +++ b/build/run/MailServer.bat @@ -0,0 +1,2 @@ +echo "RUN ASC.Mail" +call dotnet run --project ..\..\products\ASC.Mail\Server\ASC.Mail.csproj --no-build --$STORAGE_ROOT=..\..\..\Data --log__dir=..\..\..\Logs --log__name=mail \ No newline at end of file diff --git a/build/run/WebEditor.bat b/build/run/WebEditorClient.bat similarity index 100% rename from build/run/WebEditor.bat rename to build/run/WebEditorClient.bat diff --git a/build/run/WebLogin.bat b/build/run/WebLoginClient.bat similarity index 100% rename from build/run/WebLogin.bat rename to build/run/WebLoginClient.bat diff --git a/common/ASC.FederatedLogin/LoginProviders/BaseLoginProvider.cs b/common/ASC.FederatedLogin/LoginProviders/BaseLoginProvider.cs index 5eb3bcc0cb..b8a7e7cf63 100644 --- a/common/ASC.FederatedLogin/LoginProviders/BaseLoginProvider.cs +++ b/common/ASC.FederatedLogin/LoginProviders/BaseLoginProvider.cs @@ -28,6 +28,7 @@ using System; using System.Collections.Generic; using System.Threading; +using ASC.Common; using ASC.Common.Caching; using ASC.Common.Utils; using ASC.Core; @@ -160,5 +161,18 @@ namespace ASC.FederatedLogin.LoginProviders } public abstract LoginProfile GetLoginProfile(string accessToken); + } + + public class BaseLoginProviderExtension + { + public static void Register(DIHelper services) + { + services.TryAdd(); + services.TryAdd(); + services.TryAdd(); + services.TryAdd(); + services.TryAdd(); + services.TryAdd(); + } } } diff --git a/common/services/ASC.ApiSystem/ASC.ApiSystem.csproj b/common/services/ASC.ApiSystem/ASC.ApiSystem.csproj index b2b33868a8..fdf210a60c 100644 --- a/common/services/ASC.ApiSystem/ASC.ApiSystem.csproj +++ b/common/services/ASC.ApiSystem/ASC.ApiSystem.csproj @@ -14,6 +14,8 @@ + + diff --git a/common/services/ASC.ApiSystem/Program.cs b/common/services/ASC.ApiSystem/Program.cs index 8d38eb1f0a..53b4778c65 100644 --- a/common/services/ASC.ApiSystem/Program.cs +++ b/common/services/ASC.ApiSystem/Program.cs @@ -24,8 +24,11 @@ */ +using System; using System.Collections.Generic; using System.IO; +using System.Runtime.InteropServices; +using System.Threading.Tasks; using ASC.Common.Utils; @@ -39,18 +42,40 @@ namespace ASC.ApiSystem { public class Program { - public static void Main(string[] args) - { - CreateHostBuilder(args).Build().Run(); + public async static Task Main(string[] args) + { + var host = CreateHostBuilder(args).Build(); + + await host.RunAsync(); } - public static IHostBuilder CreateHostBuilder(string[] args) - { - return Host.CreateDefaultBuilder(args) + public static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .UseSystemd() + .UseWindowsService() .UseServiceProviderFactory(new AutofacServiceProviderFactory()) .ConfigureWebHostDefaults(webBuilder => { - webBuilder.UseStartup(); + var builder = webBuilder.UseStartup(); + + builder.ConfigureKestrel((hostingContext, serverOptions) => + { + var kestrelConfig = hostingContext.Configuration.GetSection("Kestrel"); + + if (!kestrelConfig.Exists()) return; + + var unixSocket = kestrelConfig.GetValue("ListenUnixSocket"); + + if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + { + if (!String.IsNullOrWhiteSpace(unixSocket)) + { + unixSocket = String.Format(unixSocket, hostingContext.HostingEnvironment.ApplicationName.Replace("ASC.", "").Replace(".", "")); + + serverOptions.ListenUnixSocket(unixSocket); + } + } + }); }) .ConfigureAppConfiguration((hostingContext, config) => { @@ -76,6 +101,5 @@ namespace ASC.ApiSystem }); }); - } } } diff --git a/common/services/ASC.Data.Backup/ASC.Data.Backup.csproj b/common/services/ASC.Data.Backup/ASC.Data.Backup.csproj index a9b9e5afa7..d76c4348fb 100644 --- a/common/services/ASC.Data.Backup/ASC.Data.Backup.csproj +++ b/common/services/ASC.Data.Backup/ASC.Data.Backup.csproj @@ -23,6 +23,8 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive + + diff --git a/common/services/ASC.Data.Backup/Program.cs b/common/services/ASC.Data.Backup/Program.cs index d420b11033..bccee04c6a 100644 --- a/common/services/ASC.Data.Backup/Program.cs +++ b/common/services/ASC.Data.Backup/Program.cs @@ -1,5 +1,7 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.IO; +using System.Runtime.InteropServices; using System.Threading.Tasks; using ASC.Common.Utils; @@ -13,14 +15,41 @@ using Microsoft.Extensions.Hosting; namespace ASC.Data.Backup { public class Program - { - public static async Task Main(string[] args) - { - await Host.CreateDefaultBuilder(args) + { + public async static Task Main(string[] args) + { + var host = CreateHostBuilder(args).Build(); + + await host.RunAsync(); + } + + public static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .UseSystemd() + .UseWindowsService() .UseServiceProviderFactory(new AutofacServiceProviderFactory()) .ConfigureWebHostDefaults(webBuilder => { - webBuilder.UseStartup(); + var builder = webBuilder.UseStartup(); + + builder.ConfigureKestrel((hostingContext, serverOptions) => + { + var kestrelConfig = hostingContext.Configuration.GetSection("Kestrel"); + + if (!kestrelConfig.Exists()) return; + + var unixSocket = kestrelConfig.GetValue("ListenUnixSocket"); + + if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + { + if (!String.IsNullOrWhiteSpace(unixSocket)) + { + unixSocket = String.Format(unixSocket, hostingContext.HostingEnvironment.ApplicationName.Replace("ASC.", "").Replace(".", "")); + + serverOptions.ListenUnixSocket(unixSocket); + } + } + }); }) .ConfigureAppConfiguration((hostContext, config) => { @@ -46,10 +75,7 @@ namespace ASC.Data.Backup {"pathToConf", path } } ); - }) - .UseConsoleLifetime() - .Build() - .RunAsync(); - } + }); + } } diff --git a/common/services/ASC.Data.Storage.Encryption/ASC.Data.Storage.Encryption.csproj b/common/services/ASC.Data.Storage.Encryption/ASC.Data.Storage.Encryption.csproj index 8ad075f7a1..cf68a5f7eb 100644 --- a/common/services/ASC.Data.Storage.Encryption/ASC.Data.Storage.Encryption.csproj +++ b/common/services/ASC.Data.Storage.Encryption/ASC.Data.Storage.Encryption.csproj @@ -22,6 +22,8 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive + + diff --git a/common/services/ASC.Data.Storage.Encryption/Program.cs b/common/services/ASC.Data.Storage.Encryption/Program.cs index 4714b8acd4..f630a09f6a 100644 --- a/common/services/ASC.Data.Storage.Encryption/Program.cs +++ b/common/services/ASC.Data.Storage.Encryption/Program.cs @@ -22,8 +22,10 @@ * 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.IO; +using System.Runtime.InteropServices; using System.Threading.Tasks; using ASC.Common.Utils; @@ -38,13 +40,40 @@ namespace ASC.Data.Storage.Encryption { public class Program { - public static async Task Main(string[] args) - { - await Host.CreateDefaultBuilder(args) + public async static Task Main(string[] args) + { + var host = CreateHostBuilder(args).Build(); + + await host.RunAsync(); + } + + public static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .UseSystemd() + .UseWindowsService() .UseServiceProviderFactory(new AutofacServiceProviderFactory()) .ConfigureWebHostDefaults(webBuilder => { - webBuilder.UseStartup(); + var builder = webBuilder.UseStartup(); + + builder.ConfigureKestrel((hostingContext, serverOptions) => + { + var kestrelConfig = hostingContext.Configuration.GetSection("Kestrel"); + + if (!kestrelConfig.Exists()) return; + + var unixSocket = kestrelConfig.GetValue("ListenUnixSocket"); + + if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + { + if (!String.IsNullOrWhiteSpace(unixSocket)) + { + unixSocket = String.Format(unixSocket, hostingContext.HostingEnvironment.ApplicationName.Replace("ASC.", "").Replace(".", "")); + + serverOptions.ListenUnixSocket(unixSocket); + } + } + }); }) .ConfigureAppConfiguration((hostContext, config) => { @@ -69,10 +98,6 @@ namespace ASC.Data.Storage.Encryption .AddJsonFile("kafka.json") .AddJsonFile($"kafka.{env}.json", true) .AddEnvironmentVariables(); - }) - .UseConsoleLifetime() - .Build() - .RunAsync(); - } + }); } } diff --git a/common/services/ASC.Data.Storage.Migration/ASC.Data.Storage.Migration.csproj b/common/services/ASC.Data.Storage.Migration/ASC.Data.Storage.Migration.csproj index 2b3b00aab4..2f88670730 100644 --- a/common/services/ASC.Data.Storage.Migration/ASC.Data.Storage.Migration.csproj +++ b/common/services/ASC.Data.Storage.Migration/ASC.Data.Storage.Migration.csproj @@ -4,6 +4,11 @@ net5.0 + + + + + diff --git a/common/services/ASC.Data.Storage.Migration/Program.cs b/common/services/ASC.Data.Storage.Migration/Program.cs index a3af304668..39e84558dc 100644 --- a/common/services/ASC.Data.Storage.Migration/Program.cs +++ b/common/services/ASC.Data.Storage.Migration/Program.cs @@ -19,9 +19,17 @@ namespace ASC.Data.Storage.Migration { public class Program { - public static async Task Main(string[] args) - { - await Host.CreateDefaultBuilder(args) + public async static Task Main(string[] args) + { + var host = CreateHostBuilder(args).Build(); + + await host.RunAsync(); + } + + public static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .UseSystemd() + .UseWindowsService() .UseServiceProviderFactory(new AutofacServiceProviderFactory()) .ConfigureAppConfiguration((hostContext, config) => { @@ -64,10 +72,6 @@ namespace ASC.Data.Storage.Migration .ConfigureContainer((context, builder) => { builder.Register(context.Configuration); - }) - .UseConsoleLifetime() - .Build() - .RunAsync(); - } + }); } } diff --git a/common/services/ASC.Notify/ASC.Notify.csproj b/common/services/ASC.Notify/ASC.Notify.csproj index 412a5e62e2..cfe2a6763e 100644 --- a/common/services/ASC.Notify/ASC.Notify.csproj +++ b/common/services/ASC.Notify/ASC.Notify.csproj @@ -13,6 +13,8 @@ + + diff --git a/common/services/ASC.Notify/Program.cs b/common/services/ASC.Notify/Program.cs index 4d51a67ecc..2f329aa41a 100644 --- a/common/services/ASC.Notify/Program.cs +++ b/common/services/ASC.Notify/Program.cs @@ -21,9 +21,17 @@ namespace ASC.Notify { public class Program { - public static async Task Main(string[] args) + public async static Task Main(string[] args) { - var host = Host.CreateDefaultBuilder(args) + var host = CreateHostBuilder(args).Build(); + + await host.RunAsync(); + } + + public static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .UseSystemd() + .UseWindowsService() .UseServiceProviderFactory(new AutofacServiceProviderFactory()) .ConfigureAppConfiguration((hostContext, config) => { @@ -73,18 +81,6 @@ namespace ASC.Notify .ConfigureContainer((context, builder) => { builder.Register(context.Configuration); - }) - .UseConsoleLifetime() - .Build(); - - using (host) - { - // Start the host - await host.StartAsync(); - - // Wait for the host to shutdown - await host.WaitForShutdownAsync(); - } - } + }); } } diff --git a/common/services/ASC.Socket.IO.Svc/ASC.Socket.IO.Svc.csproj b/common/services/ASC.Socket.IO.Svc/ASC.Socket.IO.Svc.csproj index 8237da9e8e..85a8432814 100644 --- a/common/services/ASC.Socket.IO.Svc/ASC.Socket.IO.Svc.csproj +++ b/common/services/ASC.Socket.IO.Svc/ASC.Socket.IO.Svc.csproj @@ -16,6 +16,8 @@ + + diff --git a/common/services/ASC.Socket.IO.Svc/Program.cs b/common/services/ASC.Socket.IO.Svc/Program.cs index 62cec4c062..3275a7c1d1 100644 --- a/common/services/ASC.Socket.IO.Svc/Program.cs +++ b/common/services/ASC.Socket.IO.Svc/Program.cs @@ -46,9 +46,17 @@ namespace ASC.Socket.IO.Svc { public class Program { - public static async Task Main(string[] args) - { - var host = Host.CreateDefaultBuilder(args) + public async static Task Main(string[] args) + { + var host = CreateHostBuilder(args).Build(); + + await host.RunAsync(); + } + + public static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .UseSystemd() + .UseWindowsService() .UseServiceProviderFactory(new AutofacServiceProviderFactory()) .ConfigureAppConfiguration((hostContext, config) => { @@ -90,18 +98,7 @@ namespace ASC.Socket.IO.Svc .ConfigureContainer((context, builder) => { builder.Register(context.Configuration, false, false); - }) - .UseConsoleLifetime() - .Build(); - - using (host) - { - // Start the host - await host.StartAsync(); - - // Wait for the host to shutdown - await host.WaitForShutdownAsync(); - } - } - } -} + }); + } +} + diff --git a/common/services/ASC.Studio.Notify/ASC.Studio.Notify.csproj b/common/services/ASC.Studio.Notify/ASC.Studio.Notify.csproj index 145215fa33..3cbedf75cd 100644 --- a/common/services/ASC.Studio.Notify/ASC.Studio.Notify.csproj +++ b/common/services/ASC.Studio.Notify/ASC.Studio.Notify.csproj @@ -10,6 +10,11 @@ false + + + + + diff --git a/common/services/ASC.Studio.Notify/Program.cs b/common/services/ASC.Studio.Notify/Program.cs index e8afa4aafe..803ef994fa 100644 --- a/common/services/ASC.Studio.Notify/Program.cs +++ b/common/services/ASC.Studio.Notify/Program.cs @@ -22,9 +22,17 @@ namespace ASC.Studio.Notify { public class Program { - public static async Task Main(string[] args) + public async static Task Main(string[] args) { - var host = Host.CreateDefaultBuilder(args) + var host = CreateHostBuilder(args).Build(); + + await host.RunAsync(); + } + + public static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .UseSystemd() + .UseWindowsService() .UseServiceProviderFactory(new AutofacServiceProviderFactory()) .ConfigureAppConfiguration((hostContext, config) => { @@ -67,18 +75,6 @@ namespace ASC.Studio.Notify .ConfigureContainer((context, builder) => { builder.Register(context.Configuration); - }) - .UseConsoleLifetime() - .Build(); - - using (host) - { - // Start the host - await host.StartAsync(); - - // Wait for the host to shutdown - await host.WaitForShutdownAsync(); - } - } + }); } } diff --git a/common/services/ASC.TelegramService/ASC.TelegramService.csproj b/common/services/ASC.TelegramService/ASC.TelegramService.csproj index de373e69a2..aaced03570 100644 --- a/common/services/ASC.TelegramService/ASC.TelegramService.csproj +++ b/common/services/ASC.TelegramService/ASC.TelegramService.csproj @@ -13,6 +13,8 @@ + + diff --git a/common/services/ASC.TelegramService/Program.cs b/common/services/ASC.TelegramService/Program.cs index 7824c415c9..237602576e 100644 --- a/common/services/ASC.TelegramService/Program.cs +++ b/common/services/ASC.TelegramService/Program.cs @@ -22,8 +22,10 @@ * 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.IO; +using System.Runtime.InteropServices; using System.Threading.Tasks; using ASC.Common.Utils; @@ -38,13 +40,40 @@ namespace ASC.TelegramService { public class Program { - public static async Task Main(string[] args) - { - await Host.CreateDefaultBuilder(args) + public async static Task Main(string[] args) + { + var host = CreateHostBuilder(args).Build(); + + await host.RunAsync(); + } + + public static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .UseSystemd() + .UseWindowsService() .UseServiceProviderFactory(new AutofacServiceProviderFactory()) .ConfigureWebHostDefaults(webBuilder => { - webBuilder.UseStartup(); + var builder = webBuilder.UseStartup(); + + builder.ConfigureKestrel((hostingContext, serverOptions) => + { + var kestrelConfig = hostingContext.Configuration.GetSection("Kestrel"); + + if (!kestrelConfig.Exists()) return; + + var unixSocket = kestrelConfig.GetValue("ListenUnixSocket"); + + if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + { + if (!String.IsNullOrWhiteSpace(unixSocket)) + { + unixSocket = String.Format(unixSocket, hostingContext.HostingEnvironment.ApplicationName.Replace("ASC.", "").Replace(".", "")); + + serverOptions.ListenUnixSocket(unixSocket); + } + } + }); }) .ConfigureAppConfiguration((hostContext, config) => { @@ -69,10 +98,6 @@ namespace ASC.TelegramService .AddJsonFile("kafka.json") .AddJsonFile($"kafka.{env}.json", true) .AddEnvironmentVariables(); - }) - .UseConsoleLifetime() - .Build() - .RunAsync(); - } + }); } } diff --git a/common/services/ASC.Thumbnails.Svc/ASC.Thumbnails.Svc.csproj b/common/services/ASC.Thumbnails.Svc/ASC.Thumbnails.Svc.csproj index 6a7d3d8ae2..7f0181cd2b 100644 --- a/common/services/ASC.Thumbnails.Svc/ASC.Thumbnails.Svc.csproj +++ b/common/services/ASC.Thumbnails.Svc/ASC.Thumbnails.Svc.csproj @@ -21,5 +21,10 @@ + + + + + \ No newline at end of file diff --git a/common/services/ASC.Thumbnails.Svc/Program.cs b/common/services/ASC.Thumbnails.Svc/Program.cs index 5018f6e5f5..9d5d897dc7 100644 --- a/common/services/ASC.Thumbnails.Svc/Program.cs +++ b/common/services/ASC.Thumbnails.Svc/Program.cs @@ -26,6 +26,7 @@ using System.Collections.Generic; using System.IO; +using System.Reflection; using System.Threading.Tasks; using ASC.Common; @@ -45,11 +46,19 @@ using Microsoft.Extensions.Hosting; namespace ASC.Thumbnails.Svc { public class Program - { - public static async Task Main(string[] args) - { - var host = Host.CreateDefaultBuilder(args) - .UseServiceProviderFactory(new AutofacServiceProviderFactory()) + { + public async static Task Main(string[] args) + { + var host = CreateHostBuilder(args).Build(); + + await host.RunAsync(); + } + + public static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .UseSystemd() + .UseWindowsService() + .UseServiceProviderFactory(new AutofacServiceProviderFactory()) .ConfigureAppConfiguration((hostContext, config) => { var buided = config.Build(); @@ -89,18 +98,6 @@ namespace ASC.Thumbnails.Svc .ConfigureContainer((context, builder) => { builder.Register(context.Configuration, false, false); - }) - .UseConsoleLifetime() - .Build(); - - using (host) - { - // Start the host - await host.StartAsync(); - - // Wait for the host to shutdown - await host.WaitForShutdownAsync(); - } - } + }); } } diff --git a/common/services/ASC.UrlShortener.Svc/ASC.UrlShortener.Svc.csproj b/common/services/ASC.UrlShortener.Svc/ASC.UrlShortener.Svc.csproj index cc1faf4b6a..4add2d7682 100644 --- a/common/services/ASC.UrlShortener.Svc/ASC.UrlShortener.Svc.csproj +++ b/common/services/ASC.UrlShortener.Svc/ASC.UrlShortener.Svc.csproj @@ -13,6 +13,8 @@ + + diff --git a/common/services/ASC.UrlShortener.Svc/Program.cs b/common/services/ASC.UrlShortener.Svc/Program.cs index e7c00badb8..46384b30f0 100644 --- a/common/services/ASC.UrlShortener.Svc/Program.cs +++ b/common/services/ASC.UrlShortener.Svc/Program.cs @@ -44,59 +44,60 @@ using Microsoft.Extensions.Hosting; namespace ASC.UrlShortener.Svc { public class Program - { - public static async Task Main(string[] args) - { - var host = Host.CreateDefaultBuilder(args) - .UseServiceProviderFactory(new AutofacServiceProviderFactory()) - .ConfigureAppConfiguration((hostContext, config) => - { - var buided = config.Build(); - var path = buided["pathToConf"]; - if (!Path.IsPathRooted(path)) - { - path = Path.GetFullPath(CrossPlatform.PathCombine(hostContext.HostingEnvironment.ContentRootPath, path)); - } - config.SetBasePath(path); - var env = hostContext.Configuration.GetValue("ENVIRONMENT", "Production"); - config - .AddJsonFile("appsettings.json") - .AddJsonFile($"appsettings.{env}.json", true) - .AddJsonFile($"urlshortener.{env}.json", true) - .AddJsonFile("storage.json") - .AddJsonFile("kafka.json") - .AddJsonFile($"kafka.{env}.json", true) - .AddEnvironmentVariables() - .AddCommandLine(args) - .AddInMemoryCollection(new Dictionary - { - {"pathToConf", path } - } - ); - }) - .ConfigureServices((hostContext, services) => - { - services.AddMemoryCache(); - var diHelper = new DIHelper(services); - LogNLogExtension.ConfigureLog(diHelper, "ASC.UrlShortener.Svc"); - services.AddHostedService(); - diHelper.TryAdd(); - }) - .ConfigureContainer((context, builder) => - { - builder.Register(context.Configuration, false, false); - }) - .UseConsoleLifetime() - .Build(); - - using (host) - { - // Start the host - await host.StartAsync(); - - // Wait for the host to shutdown - await host.WaitForShutdownAsync(); - } - } + { + public async static Task Main(string[] args) + { + var host = CreateHostBuilder(args).Build(); + + await host.RunAsync(); + } + + public static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .UseSystemd() + .UseWindowsService() + .UseServiceProviderFactory(new AutofacServiceProviderFactory()) + .ConfigureAppConfiguration((hostContext, config) => + { + var buided = config.Build(); + var path = buided["pathToConf"]; + + if (!Path.IsPathRooted(path)) + { + path = Path.GetFullPath(CrossPlatform.PathCombine(hostContext.HostingEnvironment.ContentRootPath, path)); + } + + config.SetBasePath(path); + + var env = hostContext.Configuration.GetValue("ENVIRONMENT", "Production"); + + config.AddJsonFile("appsettings.json") + .AddJsonFile($"appsettings.{env}.json", true) + .AddJsonFile($"urlshortener.{env}.json", true) + .AddJsonFile("storage.json") + .AddJsonFile("kafka.json") + .AddJsonFile($"kafka.{env}.json", true) + .AddEnvironmentVariables() + .AddCommandLine(args) + .AddInMemoryCollection(new Dictionary + { + {"pathToConf", path } + } + ); + }) + .ConfigureServices((hostContext, services) => + { + services.AddMemoryCache(); + + var diHelper = new DIHelper(services); + + LogNLogExtension.ConfigureLog(diHelper, "ASC.UrlShortener.Svc"); + services.AddHostedService(); + diHelper.TryAdd(); + }) + .ConfigureContainer((context, builder) => + { + builder.Register(context.Configuration, false, false); + }); } } diff --git a/config/appsettings.json b/config/appsettings.json index 867844c665..ef1f609431 100644 --- a/config/appsettings.json +++ b/config/appsettings.json @@ -4,6 +4,12 @@ "Default": "Information", "Microsoft": "Warning", "Microsoft.Hosting.Lifetime": "Information" + }, + "EventLog": { + "LogLevel": { + "Default": "Information", + "Microsoft.Hosting.Lifetime": "Information" + } } }, "AllowedHosts": "*", diff --git a/config/autofac.products.json b/config/autofac.products.json index 20f16dd4f1..a14fc90734 100644 --- a/config/autofac.products.json +++ b/config/autofac.products.json @@ -35,6 +35,24 @@ } ], "instanceScope": "InstancePerLifetimeScope" + }, + { + "type": "ASC.Mail.Configuration.ProductEntryPoint, ASC.Mail", + "services": [ + { + "type": "ASC.Web.Core.IWebItem, ASC.Web.Core" + } + ], + "instanceScope": "InstancePerLifetimeScope" + }, + { + "type": "ASC.Calendar.Configuration.ProductEntryPoint, ASC.Calendar", + "services": [ + { + "type": "ASC.Web.Core.IWebItem, ASC.Web.Core" + } + ], + "instanceScope": "InstancePerLifetimeScope" } ] } diff --git a/config/nginx/onlyoffice-calendar.conf b/config/nginx/onlyoffice-calendar.conf new file mode 100644 index 0000000000..f7b614a51f --- /dev/null +++ b/config/nginx/onlyoffice-calendar.conf @@ -0,0 +1,9 @@ +server { + listen 5017; + root /var/www/products/ASC.Calendar/client; + index index.html; + + location / { + try_files $uri /index.html =404; + } +} \ No newline at end of file diff --git a/config/nginx/onlyoffice-mail.conf b/config/nginx/onlyoffice-mail.conf new file mode 100644 index 0000000000..0514ca1fa2 --- /dev/null +++ b/config/nginx/onlyoffice-mail.conf @@ -0,0 +1,9 @@ +server { + listen 5016; + root /var/www/products/ASC.Mail/client; + index index.html; + + location / { + try_files $uri /index.html =404; + } +} \ No newline at end of file diff --git a/config/nginx/onlyoffice.conf b/config/nginx/onlyoffice.conf index ab4a0544e4..1cc2e3719b 100644 --- a/config/nginx/onlyoffice.conf +++ b/config/nginx/onlyoffice.conf @@ -155,6 +155,16 @@ server { proxy_pass http://localhost:5021; proxy_set_header X-REWRITER-URL $X_REWRITER_URL; } + + location ~* /mail { + proxy_pass http://localhost:5022; + proxy_set_header X-REWRITER-URL $X_REWRITER_URL; + } + + location ~* /calendar { + proxy_pass http://localhost:5023; + proxy_set_header X-REWRITER-URL $X_REWRITER_URL; + } } location /storage { @@ -297,6 +307,68 @@ server { try_files /$basename /index.html =404; } + location ~* (/httphandlers/filehandler.ashx|ChunkedUploader.ashx) { + proxy_pass http://localhost:5007; + proxy_set_header X-REWRITER-URL $X_REWRITER_URL; + } + } + + location ~* /mail { + #rewrite products/mail/(.*) /$1 break; + proxy_pass http://localhost:5016; + proxy_set_header X-REWRITER-URL $X_REWRITER_URL; + + location ~* /sockjs-node { + rewrite products/projects(.*)/sockjs-node/(.*) /$2/$3 break; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $remote_addr; + proxy_set_header Host $host; + + proxy_pass http://localhost:5016; + + proxy_redirect off; + + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + } + + location ~* /(manifest.json|service-worker.js|appIcon.png|bg-error.png) { + root $public_root; + try_files /$basename /index.html =404; + } + + location ~* (/httphandlers/filehandler.ashx|ChunkedUploader.ashx) { + proxy_pass http://localhost:5007; + proxy_set_header X-REWRITER-URL $X_REWRITER_URL; + } + } + + location ~* /calendar { + #rewrite products/calendar/(.*) /$1 break; + proxy_pass http://localhost:5017; + proxy_set_header X-REWRITER-URL $X_REWRITER_URL; + + location ~* /sockjs-node { + rewrite products/projects(.*)/sockjs-node/(.*) /$2/$3 break; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $remote_addr; + proxy_set_header Host $host; + + proxy_pass http://localhost:5017; + + proxy_redirect off; + + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + } + + location ~* /(manifest.json|service-worker.js|appIcon.png|bg-error.png) { + root $public_root; + try_files /$basename /index.html =404; + } + location ~* (/httphandlers/filehandler.ashx|ChunkedUploader.ashx) { proxy_pass http://localhost:5007; proxy_set_header X-REWRITER-URL $X_REWRITER_URL; diff --git a/frontend.code-workspace b/frontend.code-workspace index 6c5ebe9355..d1bb1ea531 100644 --- a/frontend.code-workspace +++ b/frontend.code-workspace @@ -28,6 +28,14 @@ "name": "🚀 @appserver/projects", "path": "products\\ASC.Projects\\Client" }, + { + "name": "🚀 @appserver/mail", + "path": "products\\ASC.Mail\\Client" + }, + { + "name": "🚀 @appserver/calendar", + "path": "products\\ASC.Calendar\\Client" + }, { "name": "🚀 @appserver/studio", "path": "web\\ASC.Web.Client" diff --git a/lerna.json b/lerna.json index f69ffe9bee..09653b99f6 100644 --- a/lerna.json +++ b/lerna.json @@ -1,13 +1,18 @@ { - "version": "0.1.6", + "version": "0.1.8", "npmClient": "yarn", "packages": [ "packages/asc-web-components", "packages/asc-web-common", "web/ASC.Web.Login", "web/ASC.Web.Client", + "web/ASC.Web.Editor", "products/ASC.People/Client", - "products/ASC.Files/Client" + "products/ASC.Files/Client", + "products/ASC.CRM/Client", + "products/ASC.Projects/Client", + "products/ASC.Mail/Client", + "products/ASC.Calendar/Client" ], "useWorkspaces": true } diff --git a/package.json b/package.json index 045cb2815d..e7f03f254d 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,9 @@ "products/ASC.People/Client", "products/ASC.Files/Client", "products/ASC.CRM/Client", - "products/ASC.Projects/Client" + "products/ASC.Projects/Client", + "products/ASC.Mail/Client", + "products/ASC.Calendar/Client" ], "scripts": { "wipe": "rimraf node_modules yarn.lock web/**/node_modules products/**/node_modules", diff --git a/packages/asc-web-common/api/files/index.js b/packages/asc-web-common/api/files/index.js index a79b50ac07..8f65a23448 100644 --- a/packages/asc-web-common/api/files/index.js +++ b/packages/asc-web-common/api/files/index.js @@ -4,12 +4,22 @@ import FilesFilter from "./filter"; import { FolderType } from "../../constants"; import find from "lodash/find"; -export function openEdit(fileId, doc) { - const params = doc ? `?doc=${doc}` : ""; +export function openEdit(fileId, version, doc) { + const params = []; // doc ? `?doc=${doc}` : ""; + + if (version) { + params.push(`version=${version}`); + } + + if (doc) { + params.push(`doc=${doc}`); + } + + const paramsString = params.length > 0 ? `?${params.join("&")}` : ""; const options = { method: "get", - url: `/files/file/${fileId}/openedit${params}`, + url: `/files/file/${fileId}/openedit${paramsString}`, }; return request(options); diff --git a/packages/asc-web-common/api/settings/index.js b/packages/asc-web-common/api/settings/index.js index 5b3ce944bc..ae37b5b5d3 100644 --- a/packages/asc-web-common/api/settings/index.js +++ b/packages/asc-web-common/api/settings/index.js @@ -236,7 +236,7 @@ export function getConsumersList() { export function getAuthProviders() { return request({ method: "get", - url: `/settings/authproviders`, + url: `/people/thirdparty/providers`, }); } diff --git a/packages/asc-web-common/package.json b/packages/asc-web-common/package.json index 173afdb3b9..6814d51dff 100644 --- a/packages/asc-web-common/package.json +++ b/packages/asc-web-common/package.json @@ -1,6 +1,6 @@ { "name": "@appserver/common", - "version": "0.0.2", + "version": "0.0.3", "private": true, "scripts": { "build": "echo 'skip it'", diff --git a/packages/asc-web-common/store/AuthStore.js b/packages/asc-web-common/store/AuthStore.js index 2058645f7c..56334a47bf 100644 --- a/packages/asc-web-common/store/AuthStore.js +++ b/packages/asc-web-common/store/AuthStore.js @@ -8,7 +8,7 @@ import UserStore from "./UserStore"; import { logout as logoutDesktop, desktopConstants } from "../desktop"; import { combineUrl, isAdmin } from "../utils"; import isEmpty from "lodash/isEmpty"; -import { AppServerConfig } from "../constants"; +import { AppServerConfig, LANGUAGE } from "../constants"; const { proxyURL } = AppServerConfig; class AuthStore { @@ -43,7 +43,14 @@ class AuthStore { return Promise.all(requests); }; - + setLanguage() { + if (this.userStore.user.cultureName) { + localStorage.getItem(LANGUAGE) !== this.userStore.user.cultureName && + localStorage.setItem(LANGUAGE, this.userStore.user.cultureName); + } else { + localStorage.setItem(LANGUAGE, this.settingsStore.culture || "en-US"); + } + } get isLoaded() { let success = false; if (this.isAuthenticated) { @@ -51,6 +58,8 @@ class AuthStore { this.userStore.isLoaded && this.moduleStore.isLoaded && this.settingsStore.isLoaded; + + success && this.setLanguage(); } else { success = this.settingsStore.isLoaded; } diff --git a/packages/asc-web-common/store/UserStore.js b/packages/asc-web-common/store/UserStore.js index 1749587346..0cfa3edc5f 100644 --- a/packages/asc-web-common/store/UserStore.js +++ b/packages/asc-web-common/store/UserStore.js @@ -1,6 +1,5 @@ import { action, makeObservable, observable } from "mobx"; import api from "../api"; -import { LANGUAGE } from "../constants"; class UserStore { user = null; @@ -21,9 +20,7 @@ class UserStore { getCurrentUser = async () => { const user = await api.people.getUser(); - user.cultureName && - localStorage.getItem(LANGUAGE) !== user.cultureName && - localStorage.setItem(LANGUAGE, user.cultureName); + this.setUser(user); }; diff --git a/packages/asc-web-components/badge/index.js b/packages/asc-web-components/badge/index.js index e5bc04118b..4c9d0162fe 100644 --- a/packages/asc-web-components/badge/index.js +++ b/packages/asc-web-components/badge/index.js @@ -11,7 +11,6 @@ const Badge = (props) => { if (!props.onClick) return; e.preventDefault(); - e.stopPropagation(); props.onClick(e); }; diff --git a/packages/asc-web-components/checkbox/styled-checkbox.js b/packages/asc-web-components/checkbox/styled-checkbox.js index 1829d70bb4..7214d065e5 100644 --- a/packages/asc-web-components/checkbox/styled-checkbox.js +++ b/packages/asc-web-components/checkbox/styled-checkbox.js @@ -15,6 +15,7 @@ const StyledLabel = styled.label` .checkbox { margin-right: 12px; + overflow: visible; } /* ${(props) => diff --git a/packages/asc-web-components/context-menu-button/index.js b/packages/asc-web-components/context-menu-button/index.js index 25814966e3..5c4c585b89 100644 --- a/packages/asc-web-components/context-menu-button/index.js +++ b/packages/asc-web-components/context-menu-button/index.js @@ -80,8 +80,8 @@ class ContextMenuButton extends React.Component { } } - onIconButtonClick = () => { - if (this.props.isDisabled) { + onIconButtonClick = (e) => { + if (this.props.isDisabled || this.props.isNew) { this.stopAction; return; } @@ -95,7 +95,7 @@ class ContextMenuButton extends React.Component { !this.props.isDisabled && this.state.isOpen && this.props.onClick && - this.props.onClick() + this.props.onClick(e) ); // eslint-disable-line react/prop-types }; @@ -124,6 +124,20 @@ class ContextMenuButton extends React.Component { return true; } + callNewMenu = (e) => { + if (this.props.isDisabled || !this.props.isNew) { + this.stopAction; + return; + } + + this.setState( + { + data: this.props.getData(), + }, + () => this.props.onClick(e) + ); + }; + render() { //console.log("ContextMenuButton render", this.props); const { @@ -148,12 +162,19 @@ class ContextMenuButton extends React.Component { style, isFill, // eslint-disable-line react/prop-types asideHeader, // eslint-disable-line react/prop-types + isNew, } = this.props; const { isOpen, displayType, offsetX, offsetY } = this.state; const iconButtonName = isOpen && iconOpenName ? iconOpenName : iconName; return ( - + ); const submenu = this.renderSubmenu(item); + const dataKeys = Object.fromEntries( + Object.entries(item).filter((el) => el[0].indexOf("data-") === 0) + ); let content = ( this.onItemClick(event, item, index)} role="menuitem" > diff --git a/packages/asc-web-components/package.json b/packages/asc-web-components/package.json index 7c62ba355d..b525a5a009 100644 --- a/packages/asc-web-components/package.json +++ b/packages/asc-web-components/package.json @@ -1,6 +1,6 @@ { "name": "@appserver/components", - "version": "0.0.2", + "version": "0.0.3", "private": true, "scripts": { "build": "echo 'skip it'", diff --git a/packages/asc-web-components/row/index.js b/packages/asc-web-components/row/index.js index e10bfe4b59..8628092e9b 100644 --- a/packages/asc-web-components/row/index.js +++ b/packages/asc-web-components/row/index.js @@ -100,6 +100,8 @@ class Row extends React.Component { className="expandButton" getData={getOptions} directionX="right" + isNew={true} + onClick={onContextMenu} /> ) : (
diff --git a/products/ASC.CRM/Client/package.json b/products/ASC.CRM/Client/package.json index cbc250bbbc..ba69f5fce3 100644 --- a/products/ASC.CRM/Client/package.json +++ b/products/ASC.CRM/Client/package.json @@ -1,6 +1,6 @@ { "name": "@appserver/crm", - "version": "0.1.6", + "version": "0.1.8", "private": "true", "homepage": "/products/crm", "title": "ONLYOFFICE", diff --git a/products/ASC.CRM/Client/src/i18n.js b/products/ASC.CRM/Client/src/i18n.js index ae1eed771a..7b2ade83d5 100644 --- a/products/ASC.CRM/Client/src/i18n.js +++ b/products/ASC.CRM/Client/src/i18n.js @@ -9,8 +9,6 @@ import { LANGUAGE } from "@appserver/common/constants"; // have a look at the Quick start guide // for passing in lng and translations on init -const languages = ["en", "ru"]; - i18n /* load translation using http -> see /public/locales (i.e. https://github.com/i18next/react-i18next/tree/master/example/react/public/locales) @@ -32,8 +30,6 @@ i18n */ .init({ lng: localStorage.getItem(LANGUAGE) || "en", - supportedLngs: languages, - whitelist: languages, fallbackLng: "en", load: "languageOnly", //debug: true, diff --git a/products/ASC.CRM/Server/ASC.CRM.csproj b/products/ASC.CRM/Server/ASC.CRM.csproj index 552230915b..482cb429ff 100644 --- a/products/ASC.CRM/Server/ASC.CRM.csproj +++ b/products/ASC.CRM/Server/ASC.CRM.csproj @@ -4,6 +4,11 @@ net5.0 + + + + + diff --git a/products/ASC.CRM/Server/Program.cs b/products/ASC.CRM/Server/Program.cs index 891db21768..40cb0088ae 100644 --- a/products/ASC.CRM/Server/Program.cs +++ b/products/ASC.CRM/Server/Program.cs @@ -1,5 +1,8 @@ +using System; using System.Collections.Generic; using System.IO; +using System.Runtime.InteropServices; +using System.Threading.Tasks; using ASC.Common.DependencyInjection; using ASC.Common.Utils; @@ -15,18 +18,40 @@ namespace ASC.CRM { public class Program { - public static void Main(string[] args) + public async static Task Main(string[] args) { - CreateHostBuilder(args).Build().Run(); + var host = CreateHostBuilder(args).Build(); + + await host.RunAsync(); } - public static IHostBuilder CreateHostBuilder(string[] args) - { - return Host.CreateDefaultBuilder(args) + public static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .UseSystemd() + .UseWindowsService() .UseServiceProviderFactory(new AutofacServiceProviderFactory()) .ConfigureWebHostDefaults(webBuilder => { - webBuilder.UseStartup(); + var builder = webBuilder.UseStartup(); + + builder.ConfigureKestrel((hostingContext, serverOptions) => + { + var kestrelConfig = hostingContext.Configuration.GetSection("Kestrel"); + + if (!kestrelConfig.Exists()) return; + + var unixSocket = kestrelConfig.GetValue("ListenUnixSocket"); + + if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + { + if (!String.IsNullOrWhiteSpace(unixSocket)) + { + unixSocket = String.Format(unixSocket, hostingContext.HostingEnvironment.ApplicationName.Replace("ASC.", "").Replace(".", "")); + + serverOptions.ListenUnixSocket(unixSocket); + } + } + }); }) .ConfigureAppConfiguration((hostingContext, config) => { @@ -54,25 +79,6 @@ namespace ASC.CRM .ConfigureContainer((context, builder) => { builder.Register(context.Configuration, true, false); - }); - - - //if (!FilesIntegration.IsRegisteredFileSecurityProvider("crm", "crm_common")) - //{ - // FilesIntegration.RegisterFileSecurityProvider("crm", "crm_common", new FileSecurityProvider()); - //} - - ////Register prodjects' calendar events - //CalendarManager.Instance.RegistryCalendarProvider(userid => - //{ - // if (WebItemSecurity.IsAvailableForUser(WebItemManager.CRMProductID, userid)) - // { - // return new List { new CRMCalendar(userid) }; - // } - // return new List(); - //}); - - - } + });//if (!FilesIntegration.IsRegisteredFileSecurityProvider("crm", "crm_common"))//{// FilesIntegration.RegisterFileSecurityProvider("crm", "crm_common", new FileSecurityProvider());//}////Register prodjects' calendar events//CalendarManager.Instance.RegistryCalendarProvider(userid =>//{// if (WebItemSecurity.IsAvailableForUser(WebItemManager.CRMProductID, userid))// {// return new List { new CRMCalendar(userid) };// }// return new List();//}); } } diff --git a/products/ASC.Calendar/Client/.babelrc b/products/ASC.Calendar/Client/.babelrc new file mode 100644 index 0000000000..b383aed85c --- /dev/null +++ b/products/ASC.Calendar/Client/.babelrc @@ -0,0 +1,7 @@ +{ + "presets": ["@babel/preset-react", "@babel/preset-env"], + "plugins": [ + "@babel/plugin-transform-runtime", + "@babel/plugin-proposal-class-properties" + ] +} diff --git a/products/ASC.Calendar/Client/.env b/products/ASC.Calendar/Client/.env new file mode 100644 index 0000000000..529d10ddce --- /dev/null +++ b/products/ASC.Calendar/Client/.env @@ -0,0 +1,3 @@ +PUBLIC_URL=/products/calendar +WDS_SOCKET_PATH=/products/calendar/sockjs-node +PORT=5017 \ No newline at end of file diff --git a/products/ASC.Calendar/Client/.gitignore b/products/ASC.Calendar/Client/.gitignore new file mode 100644 index 0000000000..d30f40ef44 --- /dev/null +++ b/products/ASC.Calendar/Client/.gitignore @@ -0,0 +1,21 @@ +# See https://help.github.com/ignore-files/ for more about ignoring files. + +# dependencies +/node_modules + +# testing +/coverage + +# production +/build + +# misc +.DS_Store +.env.local +.env.development.local +.env.test.local +.env.production.local + +npm-debug.log* +yarn-debug.log* +yarn-error.log* diff --git a/products/ASC.Calendar/Client/Dockerfile b/products/ASC.Calendar/Client/Dockerfile new file mode 100644 index 0000000000..cf973e8c13 --- /dev/null +++ b/products/ASC.Calendar/Client/Dockerfile @@ -0,0 +1,15 @@ +FROM node:12 +WORKDIR /usr/src/app + +COPY package.json ./ +COPY yarn.lock ./ + +RUN yarn install + +COPY . . + +RUN yarn build + +EXPOSE 5017 + +CMD [ "yarn", "build:start" ] diff --git a/products/ASC.Calendar/Client/analyse.js b/products/ASC.Calendar/Client/analyse.js new file mode 100644 index 0000000000..8c6c219fba --- /dev/null +++ b/products/ASC.Calendar/Client/analyse.js @@ -0,0 +1,17 @@ +// script to enable webpack-bundle-analyzer +process.env.NODE_ENV = "production"; +const webpack = require("webpack"); +const BundleAnalyzerPlugin = require("webpack-bundle-analyzer") + .BundleAnalyzerPlugin; +const webpackConfigProd = require("react-scripts/config/webpack.config")( + "production" +); + +webpackConfigProd.plugins.push(new BundleAnalyzerPlugin()); + +// actually running compilation and waiting for plugin to start explorer +webpack(webpackConfigProd, (err, stats) => { + if (err || stats.hasErrors()) { + console.error(err); + } +}); diff --git a/products/ASC.Calendar/Client/package.json b/products/ASC.Calendar/Client/package.json new file mode 100644 index 0000000000..f9749b4b3f --- /dev/null +++ b/products/ASC.Calendar/Client/package.json @@ -0,0 +1,83 @@ +{ + "name": "@appserver/calendar", + "version": "0.1.3", + "private": "true", + "homepage": "/products/calendar", + "id": "32d24cb5-7ece-4606-9c94-19216ba42086", + "title": "ONLYOFFICE", + "scripts": { + "start": "webpack-cli serve", + "start-prod": "webpack --mode production && serve dist -p 5017", + "build": "webpack --mode production", + "serve": "serve dist -p 5017", + "clean": "rm -rf dist" + }, + "devDependencies": { + "@babel/core": "^7.12.10", + "@babel/plugin-proposal-class-properties": "^7.12.1", + "@babel/plugin-proposal-export-default-from": "^7.12.1", + "@babel/plugin-transform-runtime": "^7.12.1", + "@babel/preset-env": "^7.12.7", + "@babel/preset-react": "^7.12.10", + "@svgr/webpack": "^5.5.0", + "babel-loader": "^8.2.2", + "clean-webpack-plugin": "^3.0.0", + "copy-webpack-plugin": "^7.0.0", + "css-loader": "^3.6.0", + "html-webpack-plugin": "4.5.0", + "json-loader": "^0.5.7", + "serve": "11.3.2", + "source-map-loader": "^1.1.2", + "style-loader": "1.2.1", + "webpack": "5.14.0", + "webpack-cli": "4.5.0", + "webpack-dev-server": "3.11.2", + "workbox-webpack-plugin": "^6.1.1" + }, + "dependencies": { + "@babel/runtime": "^7.12.5", + "attr-accept": "^2.2.2", + "axios": "^0.21.0", + "email-addresses": "^3.1.0", + "i18next": "^19.8.4", + "i18next-http-backend": "^1.1.0", + "mobx": "^6.1.1", + "mobx-react": "^7.1.0", + "moment": "^2.29.1", + "copy-to-clipboard": "^3.2.0", + "fast-deep-equal": "^3.1.3", + "prop-types": "^15.7.2", + "rc-tree": "^2.1.4", + "re-resizable": "^6.9.0", + "react": "^17.0.1", + "react-autosize-textarea": "^7.1.0", + "react-content-loader": "^5.1.4", + "react-custom-scrollbars": "^4.2.1", + "react-device-detect": "^1.14.0", + "react-dom": "^17.0.1", + "react-dropzone": "^11.2.4", + "react-i18next": "^11.7.3", + "react-hammerjs": "^1.0.1", + "react-onclickoutside": "^6.9.0", + "react-player": "^1.15.3", + "react-resize-detector": "^5.2.0", + "react-router": "^5.2.0", + "react-router-dom": "^5.2.0", + "react-string-format": "^0.1.0", + "react-svg": "^12.0.0", + "react-text-mask": "^5.4.3", + "react-toastify": "^6.1.0", + "react-tooltip": "^4.2.11", + "react-viewer": "^3.2.2", + "react-virtualized-auto-sizer": "^1.0.2", + "react-window": "^1.8.6", + "react-window-infinite-loader": "^1.0.5", + "resize-image": "^0.1.0", + "sass": "^1.29.0", + "sass-loader": "^10.1.0", + "sjcl": "^1.0.8", + "screenfull": "^5.1.0", + "styled-components": "^5.2.1", + "workbox-window": "^6.1.1" + } +} diff --git a/products/ASC.Calendar/Client/public/favicon.ico b/products/ASC.Calendar/Client/public/favicon.ico new file mode 100644 index 0000000000..dcddd3efdf Binary files /dev/null and b/products/ASC.Calendar/Client/public/favicon.ico differ diff --git a/products/ASC.Calendar/Client/public/images/calendar.menu.svg b/products/ASC.Calendar/Client/public/images/calendar.menu.svg new file mode 100644 index 0000000000..5a6d08cd23 --- /dev/null +++ b/products/ASC.Calendar/Client/public/images/calendar.menu.svg @@ -0,0 +1,3 @@ + + + diff --git a/products/ASC.Calendar/Client/public/images/calendar.svg b/products/ASC.Calendar/Client/public/images/calendar.svg new file mode 100644 index 0000000000..17bc3d661a --- /dev/null +++ b/products/ASC.Calendar/Client/public/images/calendar.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/products/ASC.Calendar/Client/public/index.html b/products/ASC.Calendar/Client/public/index.html new file mode 100644 index 0000000000..320f78696c --- /dev/null +++ b/products/ASC.Calendar/Client/public/index.html @@ -0,0 +1,261 @@ + + + + + + + + + + + + + + + + + + + + <%= htmlWebpackPlugin.options.title %> + + + + +
+ +
+
+
+ + + + + + + + +
+
+ + + + + + + + +
+
+
+ + + + + + + + +
+
+ +
+
+ + + + + + + + + + + +
+
+ + + + + + + + + + + +
+
+
+ + + + + diff --git a/products/ASC.Calendar/Client/public/locales/en/ComingSoon.json b/products/ASC.Calendar/Client/public/locales/en/ComingSoon.json new file mode 100644 index 0000000000..ad91fbd76f --- /dev/null +++ b/products/ASC.Calendar/Client/public/locales/en/ComingSoon.json @@ -0,0 +1,7 @@ +{ + "ComingSoon": "Coming soon", + "ViewWeb": "View web version", + "OpenApp": "Open your {{title}} app", + "LearnMore": "Learn more", + "ModuleDescription": "Schedule meetings and events" +} diff --git a/products/ASC.Calendar/Client/public/locales/ru/ComingSoon.json b/products/ASC.Calendar/Client/public/locales/ru/ComingSoon.json new file mode 100644 index 0000000000..162fa12258 --- /dev/null +++ b/products/ASC.Calendar/Client/public/locales/ru/ComingSoon.json @@ -0,0 +1,7 @@ +{ + "ComingSoon": "Скоро появится", + "ViewWeb": "Просмотреть веб-версию", + "OpenApp": "Откройте {{title}}", + "LearnMore": "Узнать больше", + "ModuleDescription": "Планируйте встречи и мероприятия" +} diff --git a/products/ASC.Calendar/Client/public/manifest.json b/products/ASC.Calendar/Client/public/manifest.json new file mode 100644 index 0000000000..7f73a7c20d --- /dev/null +++ b/products/ASC.Calendar/Client/public/manifest.json @@ -0,0 +1,15 @@ +{ + "short_name": "ASC.Calendar", + "name": "ASC.Calendar", + "icons": [ + { + "src": "favicon.ico", + "sizes": "64x64 32x32 24x24 16x16", + "type": "image/x-icon" + } + ], + "start_url": "./index.html", + "display": "standalone", + "theme_color": "#000000", + "background_color": "#ffffff" +} diff --git a/products/ASC.Calendar/Client/src/App.js b/products/ASC.Calendar/Client/src/App.js new file mode 100644 index 0000000000..4ed936eff8 --- /dev/null +++ b/products/ASC.Calendar/Client/src/App.js @@ -0,0 +1,7 @@ +//import "./wdyr"; +import React from "react"; +import Shell from "studio/shell"; + +const App = () => ; + +export default App; diff --git a/products/ASC.Calendar/Client/src/App.test.js b/products/ASC.Calendar/Client/src/App.test.js new file mode 100644 index 0000000000..3871ae868d --- /dev/null +++ b/products/ASC.Calendar/Client/src/App.test.js @@ -0,0 +1,15 @@ +import React from "react"; +import ReactDOM from "react-dom"; +import { MemoryRouter } from "react-router-dom"; +import App from "./App"; + +it("renders without crashing", async () => { + const div = document.createElement("div"); + ReactDOM.render( + + + , + div + ); + await new Promise((resolve) => setTimeout(resolve, 1000)); +}); diff --git a/products/ASC.Calendar/Client/src/Calendar.jsx b/products/ASC.Calendar/Client/src/Calendar.jsx new file mode 100644 index 0000000000..e263f51971 --- /dev/null +++ b/products/ASC.Calendar/Client/src/Calendar.jsx @@ -0,0 +1,71 @@ +import React, { useEffect } from "react"; +import { Provider as PeopleProvider, inject, observer } from "mobx-react"; +import { Switch } from "react-router-dom"; +import CalendarStore from "./store/CalendarStore"; +import ErrorBoundary from "@appserver/common/components/ErrorBoundary"; +import toastr from "studio/toastr"; +import PrivateRoute from "@appserver/common/components/PrivateRoute"; +import AppLoader from "@appserver/common/components/AppLoader"; +import { combineUrl, updateTempContent } from "@appserver/common/utils"; +import config from "../package.json"; +import "./custom.scss"; +import i18n from "./i18n"; +import { I18nextProvider } from "react-i18next"; +import Home from "./pages/Home"; +import { AppServerConfig } from "@appserver/common/constants"; + +const { proxyURL } = AppServerConfig; +const homepage = config.homepage; +const PROXY_HOMEPAGE_URL = combineUrl(proxyURL, homepage); + +const Error404 = React.lazy(() => import("studio/Error404")); + +const Error404Route = (props) => ( + }> + + + + +); + +const CalendarContent = (props) => { + const { isLoaded, loadBaseInfo } = props; + + useEffect(() => { + loadBaseInfo() + .catch((err) => toastr.error(err)) + .finally(() => { + //this.props.setIsLoaded(true); + updateTempContent(); + }); + }, []); + + useEffect(() => { + if (isLoaded) updateTempContent(); + }, [isLoaded]); + + return ( + + + + + ); +}; + +const Calendar = inject(({ auth, calendarStore }) => ({ + loadBaseInfo: async () => { + await calendarStore.init(); + auth.setProductVersion(config.version); + }, + isLoaded: auth.isLoaded && calendarStore.isLoaded, +}))(observer(CalendarContent)); + +const calendarStore = new CalendarStore(); + +export default (props) => ( + + + + + +); diff --git a/products/ASC.Calendar/Client/src/bootstrap.js b/products/ASC.Calendar/Client/src/bootstrap.js new file mode 100644 index 0000000000..784d629277 --- /dev/null +++ b/products/ASC.Calendar/Client/src/bootstrap.js @@ -0,0 +1,9 @@ +import App from "./App"; +import React from "react"; +import ReactDOM from "react-dom"; +import config from "../package.json"; +import { registerSW } from "@appserver/common/utils/sw-helper"; + +ReactDOM.render(, document.getElementById("root")); + +registerSW(config.homepage); diff --git a/products/ASC.Calendar/Client/src/custom.scss b/products/ASC.Calendar/Client/src/custom.scss new file mode 100644 index 0000000000..835034402f --- /dev/null +++ b/products/ASC.Calendar/Client/src/custom.scss @@ -0,0 +1,27 @@ +// Override default variables before the import +$font-family-base: "Open Sans", sans-serif; + +html, +body { + height: 100%; +} + +#root { + min-height: 100%; + position: relative; + + .pageLoader { + position: absolute; + left: calc(50% - 20px); + top: 35%; + } +} + +body { + margin: 0; + overflow: hidden; +} + +body.loading * { + cursor: wait !important; +} diff --git a/products/ASC.Calendar/Client/src/helpers/utils.js b/products/ASC.Calendar/Client/src/helpers/utils.js new file mode 100644 index 0000000000..5cc3febb38 --- /dev/null +++ b/products/ASC.Calendar/Client/src/helpers/utils.js @@ -0,0 +1,26 @@ +import authStore from "@appserver/common/store/AuthStore"; +//import store from "../store/store"; + +//const { getCurrentProduct } = commonStore.auth.selectors; + +export const setDocumentTitle = (subTitle = null) => { + // const state = store.getState(); + // const { auth: commonState } = state; + const { isAuthenticated, settingsStore, product: currentModule } = authStore; + const { organizationName } = settingsStore; + + let title; + if (subTitle) { + if (isAuthenticated && currentModule) { + title = subTitle + " - " + currentModule.title; + } else { + title = subTitle + " - " + organizationName; + } + } else if (currentModule && organizationName) { + title = currentModule.title + " - " + organizationName; + } else { + title = organizationName; + } + + document.title = title; +}; diff --git a/products/ASC.Calendar/Client/src/i18n.js b/products/ASC.Calendar/Client/src/i18n.js new file mode 100644 index 0000000000..ae1eed771a --- /dev/null +++ b/products/ASC.Calendar/Client/src/i18n.js @@ -0,0 +1,58 @@ +import i18n from "i18next"; +import { initReactI18next } from "react-i18next"; +import Backend from "i18next-http-backend"; +import config from "../package.json"; +import { LANGUAGE } from "@appserver/common/constants"; + +//import LanguageDetector from "i18next-browser-languagedetector"; +// not like to use this? +// have a look at the Quick start guide +// for passing in lng and translations on init + +const languages = ["en", "ru"]; + +i18n + /* + load translation using http -> see /public/locales (i.e. https://github.com/i18next/react-i18next/tree/master/example/react/public/locales) + learn more: https://github.com/i18next/i18next-http-backend + */ + .use(Backend) + /* + detect user language + learn more: https://github.com/i18next/i18next-browser-languageDetector + */ + //.use(LanguageDetector) + /* + pass the i18n instance to react-i18next. + */ + .use(initReactI18next) + /* + init i18next + for all options read: https://www.i18next.com/overview/configuration-options + */ + .init({ + lng: localStorage.getItem(LANGUAGE) || "en", + supportedLngs: languages, + whitelist: languages, + fallbackLng: "en", + load: "languageOnly", + //debug: true, + + interpolation: { + escapeValue: false, // not needed for react as it escapes by default + format: function (value, format) { + if (format === "lowercase") return value.toLowerCase(); + return value; + }, + }, + + backend: { + loadPath: `${config.homepage}/locales/{{lng}}/{{ns}}.json`, + }, + + react: { + useSuspense: false, + }, + }); + +export default i18n; diff --git a/products/ASC.Calendar/Client/src/index.js b/products/ASC.Calendar/Client/src/index.js new file mode 100644 index 0000000000..50599f7ad8 --- /dev/null +++ b/products/ASC.Calendar/Client/src/index.js @@ -0,0 +1 @@ +import("./bootstrap"); diff --git a/products/ASC.Calendar/Client/src/pages/Home/index.js b/products/ASC.Calendar/Client/src/pages/Home/index.js new file mode 100644 index 0000000000..111f615fc7 --- /dev/null +++ b/products/ASC.Calendar/Client/src/pages/Home/index.js @@ -0,0 +1,257 @@ +import React, { useEffect } from "react"; +import { ReactSVG } from "react-svg"; +import PropTypes from "prop-types"; +import { withRouter } from "react-router"; +import Text from "@appserver/components/text"; +import Link from "@appserver/components/link"; +import Badge from "@appserver/components/badge"; +import Box from "@appserver/components/box"; +import EmptyScreenContainer from "@appserver/components/empty-screen-container"; +import ExternalLinkIcon from "../../../../../../public/images/external.link.react.svg"; +import Loaders from "@appserver/common/components/Loaders"; +import toastr from "studio/toastr"; +import PageLayout from "@appserver/common/components/PageLayout"; +import { withTranslation } from "react-i18next"; +import styled from "styled-components"; +import { isMobile, isTablet, isIOS } from "react-device-detect"; + +import { setDocumentTitle } from "../../helpers/utils"; +import { inject } from "mobx-react"; +import i18n from "../../i18n"; +import { I18nextProvider } from "react-i18next"; +import { combineUrl, deleteCookie } from "@appserver/common/utils"; + +const commonStyles = ` + .link-box { + margin: 8px 0; + .view-web-link { + margin: 8px; + :focus { + outline: 0; + } + } + } +`; + +const ComingSoonPage = styled.div` + padding: ${isTablet ? "106px 0 0 0" : isMobile ? "62px 0 0 0" : "0"}; + width: ${isTablet ? "500px" : "336px"}; + margin: 0 auto; + + .module-logo-icon { + float: left; + margin-top: 8px; + margin-right: 16px; + + svg { + width: ${isTablet ? "192px" : "96px"}; + height: ${isTablet ? "192px" : "96px"}; + } + } + + .module-title { + margin-top: 14px; + margin-bottom: 14px; + } + + .module-info { + margin-bottom: 18px; + .learn-more-link { + white-space: nowrap; + } + } + + .coming-soon-badge { + margin-bottom: 26px; + } + + ${commonStyles} +`; + +const StyledDesktopContainer = styled(EmptyScreenContainer)` + img { + width: 150px; + height: 150px; + } + span:first-of-type { + font-size: 24px; + } + span { + font-size: 14px; + > p { + font-size: 14px; + .learn-more-link { + white-space: nowrap; + } + } + } + ${commonStyles} + + .view-web-link { + font-size: 14px; + } + + .coming-soon-badge > div > p { + font-size: 13px; + } +`; + +const ExternalLink = ({ label, href, onClick }) => ( + + + + {label} + + +); + +const Body = ({ modules, match, isLoaded, setCurrentProductId, t, tReady }) => { + const { error } = match.params; + const { pathname, protocol, hostname } = window.location; + const currentModule = modules.find((m) => m.link === pathname); + const { id, title, imageUrl, link, originUrl, helpUrl } = currentModule; + const url = originUrl ? originUrl : link; + const webLink = combineUrl( + protocol + "//" + hostname, + `${url}?desktop_view=true` + ); + const appLink = isIOS + ? id === "2A923037-8B2D-487b-9A22-5AC0918ACF3F" + ? "message:" + : id === "32D24CB5-7ECE-4606-9C94-19216BA42086" + ? "calshow:" + : false + : false; + + setDocumentTitle(); + + useEffect(() => { + setCurrentProductId(id); + }, [id, setCurrentProductId]); + + useEffect(() => error && toastr.error(error), [error]); + + const appButtons = ( + <> + + { + deleteCookie("desktop_view"); + window.open(webLink, "_self", "", true); + }} + /> + {appLink && ( + + )} + + ); + + const moduleDescription = ( + + {t("ModuleDescription")}{" "} + {helpUrl && false && ( + + {t("LearnMore")}... + + )} + + ); + + return !isLoaded || !tReady ? ( + <> + ) : isMobile || isTablet ? ( + + ( + + )} + src={imageUrl} + /> + + + {title} + + {moduleDescription} + {appButtons} + + + ) : ( + + ); +}; + +const ComingSoon = (props) => { + return ( + + + + + + ); +}; + +ComingSoon.propTypes = { + modules: PropTypes.array, + isLoaded: PropTypes.bool, +}; + +const ComingSoonWrapper = inject(({ auth }) => ({ + modules: auth.moduleStore.modules, + isLoaded: auth.isLoaded, + setCurrentProductId: auth.settingsStore.setCurrentProductId, +}))(withRouter(withTranslation("ComingSoon")(ComingSoon))); + +export default (props) => ( + + + +); diff --git a/products/ASC.Calendar/Client/src/store/CalendarStore.js b/products/ASC.Calendar/Client/src/store/CalendarStore.js new file mode 100644 index 0000000000..7d36a56d4c --- /dev/null +++ b/products/ASC.Calendar/Client/src/store/CalendarStore.js @@ -0,0 +1,39 @@ +import { action, makeObservable, observable } from "mobx"; +import config from "../../package.json"; +import store from "studio/store"; +const { auth: authStore } = store; + +class CalendarStore { + isLoading = false; + isLoaded = false; + isInit = false; + + constructor() { + makeObservable(this, { + isLoading: observable, + isLoaded: observable, + setIsLoading: action, + setIsLoaded: action, + init: action, + }); + } + + init = async () => { + if (this.isInit) return; + this.isInit = true; + + authStore.settingsStore.setModuleInfo(config.homepage, config.id); + + this.setIsLoaded(true); + }; + + setIsLoading = (loading) => { + this.isLoading = loading; + }; + + setIsLoaded = (isLoaded) => { + this.isLoaded = isLoaded; + }; +} + +export default CalendarStore; diff --git a/products/ASC.Calendar/Client/src/wdyr.js b/products/ASC.Calendar/Client/src/wdyr.js new file mode 100644 index 0000000000..19a8d822a5 --- /dev/null +++ b/products/ASC.Calendar/Client/src/wdyr.js @@ -0,0 +1,12 @@ +import React from "react"; + +if (process.env.NODE_ENV === "development") { + const whyDidYouRender = require("@welldone-software/why-did-you-render"); + whyDidYouRender(React, { + trackAllPureComponents: true, + collapseGroups: true, + //trackExtraHooks: [ + // [ReactRedux, 'useSelector'] + //] + }); +} diff --git a/products/ASC.Calendar/Client/webpack.config.js b/products/ASC.Calendar/Client/webpack.config.js new file mode 100644 index 0000000000..397cfb8696 --- /dev/null +++ b/products/ASC.Calendar/Client/webpack.config.js @@ -0,0 +1,198 @@ +const { CleanWebpackPlugin } = require("clean-webpack-plugin"); +const CopyPlugin = require("copy-webpack-plugin"); +const HtmlWebpackPlugin = require("html-webpack-plugin"); +const ModuleFederationPlugin = require("webpack").container + .ModuleFederationPlugin; +const TerserPlugin = require("terser-webpack-plugin"); +const { InjectManifest } = require("workbox-webpack-plugin"); +const combineUrl = require("@appserver/common/utils/combineUrl"); +const AppServerConfig = require("@appserver/common/constants/AppServerConfig"); + +const path = require("path"); +const pkg = require("./package.json"); +const deps = pkg.dependencies; +const homepage = pkg.homepage; // combineUrl(AppServerConfig.proxyURL, pkg.homepage); +const title = pkg.title; + +var config = { + mode: "development", + entry: "./src/index", + + devServer: { + publicPath: homepage, + + contentBase: [path.join(__dirname, "dist")], + contentBasePublicPath: homepage, + port: 5017, + historyApiFallback: { + // Paths with dots should still use the history fallback. + // See https://github.com/facebook/create-react-app/issues/387. + disableDotRule: true, + index: homepage, + }, + // proxy: [ + // { + // context: "/api", + // target: "http://localhost:8092", + // }, + // ], + hot: false, + hotOnly: false, + headers: { + "Access-Control-Allow-Origin": "*", + "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, PATCH, OPTIONS", + "Access-Control-Allow-Headers": + "X-Requested-With, content-type, Authorization", + }, + }, + + output: { + publicPath: "auto", + chunkFilename: "static/js/[id].[contenthash].js", + //assetModuleFilename: "static/images/[hash][ext][query]", + path: path.resolve(process.cwd(), "dist"), + filename: "static/js/[name].[contenthash].bundle.js", + }, + + resolve: { + extensions: [".jsx", ".js", ".json"], + fallback: { + crypto: false, + }, + }, + + module: { + rules: [ + { + test: /\.(png|jpe?g|gif|ico)$/i, + type: "asset/resource", + generator: { + filename: "static/images/[hash][ext][query]", + }, + }, + { + test: /\.m?js/, + type: "javascript/auto", + resolve: { + fullySpecified: false, + }, + }, + { + test: /\.react.svg$/, + use: [ + { + loader: "@svgr/webpack", + options: { + svgoConfig: { + plugins: [{ removeViewBox: false }], + }, + }, + }, + ], + }, + { test: /\.json$/, loader: "json-loader" }, + { + test: /\.css$/i, + use: ["style-loader", "css-loader"], + }, + { + test: /\.s[ac]ss$/i, + use: [ + // Creates `style` nodes from JS strings + "style-loader", + // Translates CSS into CommonJS + "css-loader", + // Compiles Sass to CSS + "sass-loader", + ], + }, + { + test: /\.(js|jsx)$/, + exclude: /node_modules/, + use: [ + { + loader: "babel-loader", + options: { + presets: ["@babel/preset-react", "@babel/preset-env"], + plugins: [ + "@babel/plugin-transform-runtime", + "@babel/plugin-proposal-class-properties", + "@babel/plugin-proposal-export-default-from", + ], + }, + }, + "source-map-loader", + ], + }, + ], + }, + + plugins: [ + new CleanWebpackPlugin(), + new ModuleFederationPlugin({ + name: "calendar", + filename: "remoteEntry.js", + remotes: { + studio: `studio@${combineUrl( + AppServerConfig.proxyURL, + "/remoteEntry.js" + )}`, + }, + exposes: { + "./app": "./src/Calendar.jsx", + }, + shared: { + ...deps, + react: { + singleton: true, + requiredVersion: deps.react, + }, + "react-dom": { + singleton: true, + requiredVersion: deps["react-dom"], + }, + }, + }), + new HtmlWebpackPlugin({ + template: "./public/index.html", + publicPath: homepage, + title: title, + base: `${homepage}/`, + }), + new CopyPlugin({ + patterns: [ + { + from: "public", + globOptions: { + dot: true, + gitignore: true, + ignore: ["**/index.html"], + }, + }, + ], + }), + ], +}; + +module.exports = (env, argv) => { + if (argv.mode === "production") { + config.mode = "production"; + config.optimization = { + splitChunks: { chunks: "all" }, + minimize: true, + minimizer: [new TerserPlugin()], + }; + config.plugins.push( + new InjectManifest({ + mode: "production", //"development", + swSrc: "@appserver/common/utils/sw-template.js", // this is your sw template file + swDest: "sw.js", // this will be created in the build step + exclude: [/\.map$/, /manifest$/, /service-worker\.js$/], + }) + ); + } else { + config.devtool = "cheap-module-source-map"; + } + + return config; +}; diff --git a/products/ASC.Calendar/Server/ASC.Calendar.csproj b/products/ASC.Calendar/Server/ASC.Calendar.csproj new file mode 100644 index 0000000000..cf1455a2e5 --- /dev/null +++ b/products/ASC.Calendar/Server/ASC.Calendar.csproj @@ -0,0 +1,47 @@ + + + + net5.0 + + + + + + + + + + + + + + + True + True + CalendarAddonResource.resx + + + + + + ResXFileCodeGenerator + CalendarAddonResource.Designer.cs + + + CalendarAddonResource.resx + + + CalendarAddonResource.resx + + + CalendarAddonResource.resx + + + CalendarAddonResource.resx + + + CalendarAddonResource.resx + + + + diff --git a/products/ASC.Calendar/Server/Classes/PathProvider.cs b/products/ASC.Calendar/Server/Classes/PathProvider.cs new file mode 100644 index 0000000000..57d9d400ab --- /dev/null +++ b/products/ASC.Calendar/Server/Classes/PathProvider.cs @@ -0,0 +1,53 @@ +/* + * + * (c) Copyright Ascensio System Limited 2010-2018 + * + * This program is freeware. You can redistribute it and/or modify it under the terms of the GNU + * General Public License (GPL) version 3 as published by the Free Software Foundation (https://www.gnu.org/copyleft/gpl.html). + * In accordance with Section 7(a) of the GNU GPL its Section 15 shall be amended to the effect that + * Ascensio System SIA expressly excludes the warranty of non-infringement of any third-party rights. + * + * THIS PROGRAM IS DISTRIBUTED WITHOUT ANY WARRANTY; WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR + * FITNESS FOR A PARTICULAR PURPOSE. For more details, see GNU GPL at https://www.gnu.org/copyleft/gpl.html + * + * You can contact Ascensio System SIA by email at sales@onlyoffice.com + * + * The interactive user interfaces in modified source and object code versions of ONLYOFFICE must display + * Appropriate Legal Notices, as required under Section 5 of the GNU GPL version 3. + * + * Pursuant to Section 7 3(b) of the GNU GPL you must retain the original ONLYOFFICE logo which contains + * relevant author attributions when distributing the software. If the display of the logo in its graphic + * form is not reasonably feasible for technical reasons, you must include the words "Powered by ONLYOFFICE" + * in every copy of the program you distribute. + * Pursuant to Section 7 3(e) we decline to grant you any rights under trademark law for use of our trademarks. + * +*/ + + +using System; + +using ASC.Common; +using ASC.Core.Common; + +namespace ASC.Web.Calendar.Classes +{ + [Scope] + public class PathProvider + { + public string BaseVirtualPath { get; private set; } + public string BaseAbsolutePath { get; private set; } + + public PathProvider(BaseCommonLinkUtility commonLinkUtility) + { + BaseVirtualPath = "~/Products/Calendar/"; + try + { + BaseAbsolutePath = commonLinkUtility.ToAbsolute(BaseVirtualPath); + } + catch (Exception) + { + BaseAbsolutePath = BaseVirtualPath; + } + } + } +} \ No newline at end of file diff --git a/products/ASC.Calendar/Server/Configuration/ProductEntryPoint.cs b/products/ASC.Calendar/Server/Configuration/ProductEntryPoint.cs new file mode 100644 index 0000000000..b58a334e05 --- /dev/null +++ b/products/ASC.Calendar/Server/Configuration/ProductEntryPoint.cs @@ -0,0 +1,112 @@ +/* + * + * (c) Copyright Ascensio System Limited 2010-2018 + * + * This program is freeware. You can redistribute it and/or modify it under the terms of the GNU + * General Public License (GPL) version 3 as published by the Free Software Foundation (https://www.gnu.org/copyleft/gpl.html). + * In accordance with Section 7(a) of the GNU GPL its Section 15 shall be amended to the effect that + * Ascensio System SIA expressly excludes the warranty of non-infringement of any third-party rights. + * + * THIS PROGRAM IS DISTRIBUTED WITHOUT ANY WARRANTY; WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR + * FITNESS FOR A PARTICULAR PURPOSE. For more details, see GNU GPL at https://www.gnu.org/copyleft/gpl.html + * + * You can contact Ascensio System SIA by email at sales@onlyoffice.com + * + * The interactive user interfaces in modified source and object code versions of ONLYOFFICE must display + * Appropriate Legal Notices, as required under Section 5 of the GNU GPL version 3. + * + * Pursuant to Section 7 3(b) of the GNU GPL you must retain the original ONLYOFFICE logo which contains + * relevant author attributions when distributing the software. If the display of the logo in its graphic + * form is not reasonably feasible for technical reasons, you must include the words "Powered by ONLYOFFICE" + * in every copy of the program you distribute. + * Pursuant to Section 7 3(e) we decline to grant you any rights under trademark law for use of our trademarks. + * +*/ + + +using System; + +using ASC.Calendar.Resources; +using ASC.Common; +using ASC.Core; +using ASC.Web.Calendar.Classes; +using ASC.Web.Core; + +namespace ASC.Calendar.Configuration +{ + [Scope] + public class ProductEntryPoint : Product + { + private ProductContext context; + + public static readonly Guid ID = WebItemManager.CalendarProductID; + + private AuthContext AuthContext { get; } + private UserManager UserManager { get; } + private PathProvider PathProvider { get; } + + public ProductEntryPoint( + AuthContext authContext, + UserManager userManager, + PathProvider pathProvider) + { + AuthContext = authContext; + UserManager = userManager; + PathProvider = pathProvider; + } + + public override Guid ProductID + { + get { return ID; } + } + + public override string Name + { + get { return CalendarAddonResource.AddonName; } + } + + public override string Description + { + get { return CalendarAddonResource.AddonDescription; } + } + + public override string StartURL + { + get { return PathProvider.BaseAbsolutePath; } + } + + public override string HelpURL + { + get { return "https://helpcenter.onlyoffice.com/userguides/calendar.aspx"; } + } + + public override string ProductClassName + { + get { return "calendar"; } + } + + public override bool Visible { get { return true; } } + + public override ProductContext Context + { + get { return context; } + } + + public override string ApiURL => "api/2.0/calendar/info.json"; + + public override void Init() + { + context = new ProductContext + { + //MasterPageFile = String.Concat(PathProvider.BaseVirtualPath, "Masters/BasicTemplate.Master"), + DisabledIconFileName = "product_disabled_logo.png", + IconFileName = "images/calendar.menu.svg", + LargeIconFileName = "images/calendar.svg", + //SubscriptionManager = new ProductSubscriptionManager(), + DefaultSortOrder = 20, + //SpaceUsageStatManager = new ProjectsSpaceUsageStatManager(), + HasComplexHierarchyOfAccessRights = true, + }; + } + } +} \ No newline at end of file diff --git a/products/ASC.Calendar/Server/Controllers/CalendarController.cs b/products/ASC.Calendar/Server/Controllers/CalendarController.cs new file mode 100644 index 0000000000..ae091c0ebe --- /dev/null +++ b/products/ASC.Calendar/Server/Controllers/CalendarController.cs @@ -0,0 +1,54 @@ +/* + * + * (c) Copyright Ascensio System Limited 2010-2018 + * + * This program is freeware. You can redistribute it and/or modify it under the terms of the GNU + * General Public License (GPL) version 3 as published by the Free Software Foundation (https://www.gnu.org/copyleft/gpl.html). + * In accordance with Section 7(a) of the GNU GPL its Section 15 shall be amended to the effect that + * Ascensio System SIA expressly excludes the warranty of non-infringement of any third-party rights. + * + * THIS PROGRAM IS DISTRIBUTED WITHOUT ANY WARRANTY; WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR + * FITNESS FOR A PARTICULAR PURPOSE. For more details, see GNU GPL at https://www.gnu.org/copyleft/gpl.html + * + * You can contact Ascensio System SIA by email at sales@onlyoffice.com + * + * The interactive user interfaces in modified source and object code versions of ONLYOFFICE must display + * Appropriate Legal Notices, as required under Section 5 of the GNU GPL version 3. + * + * Pursuant to Section 7 § 3(b) of the GNU GPL you must retain the original ONLYOFFICE logo which contains + * relevant author attributions when distributing the software. If the display of the logo in its graphic + * form is not reasonably feasible for technical reasons, you must include the words "Powered by ONLYOFFICE" + * in every copy of the program you distribute. + * Pursuant to Section 7 § 3(e) we decline to grant you any rights under trademark law for use of our trademarks. + * +*/ + +using ASC.Api.Core; +using ASC.Common; +using ASC.Calendar.Configuration; +using ASC.Web.Api.Routing; + +using Microsoft.AspNetCore.Mvc; + +namespace ASC.Calendar.Controllers +{ + [Scope] + [DefaultRoute] + [ApiController] + public class CalendarController : ControllerBase + { + private ProductEntryPoint ProductEntryPoint { get; } + + public CalendarController(ProductEntryPoint productEntryPoint) + { + ProductEntryPoint = productEntryPoint; + } + + [Read("info")] + public Module GetModule() + { + ProductEntryPoint.Init(); + return new Module(ProductEntryPoint); + } + } +} diff --git a/products/ASC.Calendar/Server/Program.cs b/products/ASC.Calendar/Server/Program.cs new file mode 100644 index 0000000000..4a73ada4f7 --- /dev/null +++ b/products/ASC.Calendar/Server/Program.cs @@ -0,0 +1,56 @@ + +using System.Collections.Generic; +using System.IO; +using System.Threading.Tasks; + +using Autofac.Extensions.DependencyInjection; + +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Hosting; + +namespace ASC.Calendar +{ + public class Program + { + public async static Task Main(string[] args) + { + var host = CreateHostBuilder(args).Build(); + + await host.RunAsync(); + } + + public static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .UseSystemd() + .UseWindowsService() + .UseServiceProviderFactory(new AutofacServiceProviderFactory()) + .ConfigureWebHostDefaults(webBuilder => + { + webBuilder.UseStartup(); + }) + .ConfigureAppConfiguration((hostingContext, config) => + { + var buided = config.Build(); + var path = buided["pathToConf"]; + if (!Path.IsPathRooted(path)) + { + path = Path.GetFullPath(Path.Combine(hostingContext.HostingEnvironment.ContentRootPath, path)); + } + + config.SetBasePath(path); + config + .AddJsonFile("appsettings.json") + .AddJsonFile($"appsettings.{hostingContext.HostingEnvironment.EnvironmentName}.json", true) + .AddJsonFile("storage.json") + .AddJsonFile("kafka.json") + .AddJsonFile($"kafka.{hostingContext.HostingEnvironment.EnvironmentName}.json", true) + .AddEnvironmentVariables() + .AddCommandLine(args) + .AddInMemoryCollection(new Dictionary + { + {"pathToConf", path} + }); + }); + } +} diff --git a/products/ASC.Calendar/Server/Properties/launchSettings.json b/products/ASC.Calendar/Server/Properties/launchSettings.json new file mode 100644 index 0000000000..484ee9d02d --- /dev/null +++ b/products/ASC.Calendar/Server/Properties/launchSettings.json @@ -0,0 +1,29 @@ +{ + "profiles": { + "Kestrel WebServer": { + "commandName": "Project", + "launchBrowser": false, + "launchUrl": "http://localhost:5023/api/2.0/calendar/info", + "environmentVariables": { + "$STORAGE_ROOT": "../../../Data", + "log__dir": "../../../Logs", + "log__name": "calendar", + "ASPNETCORE_URLS": "http://localhost:5023", + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "WSL 2 : Ubuntu 20.04": { + "commandName": "WSL2", + "launchBrowser": false, + "launchUrl": "http://localhost:5023/api/2.0/calendar/info", + "environmentVariables": { + "$STORAGE_ROOT": "../../../Data", + "log__dir": "../../../Logs", + "log__name": "calendar", + "ASPNETCORE_URLS": "http://localhost:5023", + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "distributionName": "Ubuntu-20.04" + } + } +} \ No newline at end of file diff --git a/products/ASC.Calendar/Server/Resources/CalendarAddonResource.Designer.cs b/products/ASC.Calendar/Server/Resources/CalendarAddonResource.Designer.cs new file mode 100644 index 0000000000..332067c8d4 --- /dev/null +++ b/products/ASC.Calendar/Server/Resources/CalendarAddonResource.Designer.cs @@ -0,0 +1,81 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace ASC.Calendar.Resources { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class CalendarAddonResource { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal CalendarAddonResource() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("ASC.Calendar.Resources.CalendarAddonResource", typeof(CalendarAddonResource).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized string similar to Organize your work schedule. Track tasks and milestones due dates. Get notifications about events.. + /// + internal static string AddonDescription { + get { + return ResourceManager.GetString("AddonDescription", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Calendar. + /// + internal static string AddonName { + get { + return ResourceManager.GetString("AddonName", resourceCulture); + } + } + } +} diff --git a/products/ASC.Calendar/Server/Resources/CalendarAddonResource.de.resx b/products/ASC.Calendar/Server/Resources/CalendarAddonResource.de.resx new file mode 100644 index 0000000000..7519b32402 --- /dev/null +++ b/products/ASC.Calendar/Server/Resources/CalendarAddonResource.de.resx @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=5.0.5.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=5.0.5.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Stellen Sie Ihren Arbeitsplan auf. Behalten Sie die Übersicht über die Fristen der Aufgaben und Meilensteine. Erhalten Sie Benachrichtigungen über Ereignisse. + + + Kalender + + \ No newline at end of file diff --git a/products/ASC.Calendar/Server/Resources/CalendarAddonResource.es.resx b/products/ASC.Calendar/Server/Resources/CalendarAddonResource.es.resx new file mode 100644 index 0000000000..c82c23dc99 --- /dev/null +++ b/products/ASC.Calendar/Server/Resources/CalendarAddonResource.es.resx @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=5.0.5.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=5.0.5.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Organice su calendario de trabajo. Siga tareas e hitos por sus fechas limite. Reciba notificaciones sobre los eventos. + + + Calendario + + \ No newline at end of file diff --git a/products/ASC.Calendar/Server/Resources/CalendarAddonResource.fr.resx b/products/ASC.Calendar/Server/Resources/CalendarAddonResource.fr.resx new file mode 100644 index 0000000000..7f0bc56a5b --- /dev/null +++ b/products/ASC.Calendar/Server/Resources/CalendarAddonResource.fr.resx @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=5.0.5.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=5.0.5.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Organisez votre horaire de travail. Contrôlez les dates limites des tâches et des jalons. Recevez des notifications d'événements. + + + Calendrier + + \ No newline at end of file diff --git a/products/ASC.Calendar/Server/Resources/CalendarAddonResource.it.resx b/products/ASC.Calendar/Server/Resources/CalendarAddonResource.it.resx new file mode 100644 index 0000000000..cc305d2e85 --- /dev/null +++ b/products/ASC.Calendar/Server/Resources/CalendarAddonResource.it.resx @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=5.0.5.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=5.0.5.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Organizza il tuo orario di lavoro. Controlla le attività e le milestone. Ricevi promemoria degli eventi. + + + Calendario + + \ No newline at end of file diff --git a/products/ASC.Calendar/Server/Resources/CalendarAddonResource.resx b/products/ASC.Calendar/Server/Resources/CalendarAddonResource.resx new file mode 100644 index 0000000000..3e689a75a8 --- /dev/null +++ b/products/ASC.Calendar/Server/Resources/CalendarAddonResource.resx @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=5.0.5.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=5.0.5.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Organize your work schedule. Track tasks and milestones due dates. Get notifications about events. + + + Calendar + + \ No newline at end of file diff --git a/products/ASC.Calendar/Server/Resources/CalendarAddonResource.ru.resx b/products/ASC.Calendar/Server/Resources/CalendarAddonResource.ru.resx new file mode 100644 index 0000000000..405d59269f --- /dev/null +++ b/products/ASC.Calendar/Server/Resources/CalendarAddonResource.ru.resx @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=5.0.5.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=5.0.5.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Организуйте своё рабочее расписание. Отслеживайте даты наступления задач и вех. Получайте оповещения о событиях. + + + Календарь + + \ No newline at end of file diff --git a/products/ASC.Calendar/Server/Startup.cs b/products/ASC.Calendar/Server/Startup.cs new file mode 100644 index 0000000000..7dc06000f1 --- /dev/null +++ b/products/ASC.Calendar/Server/Startup.cs @@ -0,0 +1,42 @@ + +using System.Text; + +using ASC.Api.Core; + +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; + +namespace ASC.Calendar +{ + public class Startup : BaseStartup + { + public override string[] LogParams { get => new string[] { "ASC.Calendar" }; } + + public Startup(IConfiguration configuration, IHostEnvironment hostEnvironment) + : base(configuration, hostEnvironment) + { + + } + + public override void ConfigureServices(IServiceCollection services) + { + Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); + + base.ConfigureServices(services); + } + + public override void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + app.UseCors(builder => + builder + .AllowAnyOrigin() + .AllowAnyHeader() + .AllowAnyMethod()); + + base.Configure(app, env); + } + } +} diff --git a/products/ASC.Calendar/Server/appsettings.json b/products/ASC.Calendar/Server/appsettings.json new file mode 100644 index 0000000000..9bf702e310 --- /dev/null +++ b/products/ASC.Calendar/Server/appsettings.json @@ -0,0 +1,3 @@ +{ + "pathToConf": "..\\..\\..\\config" +} diff --git a/products/ASC.Files/Client/package.json b/products/ASC.Files/Client/package.json index 9b32231b86..e0bb11fc77 100644 --- a/products/ASC.Files/Client/package.json +++ b/products/ASC.Files/Client/package.json @@ -1,6 +1,6 @@ { "name": "@appserver/files", - "version": "0.1.6", + "version": "0.1.8", "private": "true", "homepage": "/products/files", "id": "e67be73d-f9ae-4ce1-8fec-1880cb518cb4", diff --git a/products/ASC.Files/Client/public/locales/en/DeleteDialog.json b/products/ASC.Files/Client/public/locales/en/DeleteDialog.json index aaca2fbfc5..891607a095 100644 --- a/products/ASC.Files/Client/public/locales/en/DeleteDialog.json +++ b/products/ASC.Files/Client/public/locales/en/DeleteDialog.json @@ -1,16 +1,19 @@ { - "OKButton": "OK", - "CancelButton": "Cancel", - "ConfirmationTitle": "Confirmation", - "QuestionDeleteFile": "Are you sure you want to delete this file?", "FilesModule": "Files", "FoldersModule": "Folders", - "DeleteDialogQuestion": "Are you sure you want to delete these elements?", - "DeleteDialogNote": "Note: This action can not be undone.", - "QuestionDeleteFolder": "Are you sure you want to delete this folder?", - "QuestionDeleteElements": "Are you sure you want to delete these elements?", "DeleteOperation": "Deleting", "DeleteSelectedElem": "Selected elements were successfully deleted", - "DeleteFromTrash": "Selected elements were successfully deleted from trash" + "DeleteFromTrash": "Selected elements were successfully deleted from trash", + + "MoveToTrashOneFileTitle": "Move file to Trash?", + "MoveToTrashOneFolderTitle": "Move folder to Trash?", + "MoveToTrashItemsTitle": "Move items to Trash?", + + "MoveToTrashOneFileNote": "If you delete a shared file, other users won’t be able to access it.", + "MoveToTrashOneFolderNote": "If you delete a shared folder, other users won’t be able to access it.", + "MoveToTrashItemsNote": "If you delete shared items, other users won’t be able to access them.", + + "MoveToTrashButton": "Move to Trash", + "CancelButton": "Cancel" } diff --git a/products/ASC.Files/Client/public/locales/en/EmptyTrashDialog.json b/products/ASC.Files/Client/public/locales/en/EmptyTrashDialog.json index 807debcc95..40e65b3873 100644 --- a/products/ASC.Files/Client/public/locales/en/EmptyTrashDialog.json +++ b/products/ASC.Files/Client/public/locales/en/EmptyTrashDialog.json @@ -1,11 +1,10 @@ { - "OKButton": "OK", - "CancelButton": "Cancel", - "ConfirmationTitle": "Confirmation", - - "EmptyTrashDialogQuestion": "Are you sure you want to empty the recycle bin?", - "EmptyTrashDialogMessage": "Note: This action can not be undone. Note: removal from your account can not be undone.", "DeleteOperation": "Deleting", - "SuccessEmptyTrash": "Success empty recycle bin" + "SuccessEmptyTrash": "Success empty recycle bin", + "DeleteForeverTitle": "Delete forever?", + "DeleteForeverNote": "All items from Trash will be deleted forever. You won’t be able to restore them.", + + "DeleteForeverButton": "Delete forever", + "CancelButton": "Cancel" } diff --git a/products/ASC.Files/Client/public/locales/en/Home.json b/products/ASC.Files/Client/public/locales/en/Home.json index e78d7bdad7..ade6127db7 100644 --- a/products/ASC.Files/Client/public/locales/en/Home.json +++ b/products/ASC.Files/Client/public/locales/en/Home.json @@ -112,6 +112,8 @@ "AddFilter": "Add filter", "FolderCreated": "New folder {{itemTitle}} is created", "FileCreated": "New file {{itemTitle}}.{{exst}} is created", + "FileRenamed": "The document '{{oldTitle}}' is renamed to '{{newTitle}}'", + "FolderRenamed": "The folder '{{folderTitle}}' is renamed to '{{newFoldedTitle}}'", "FolderRemoved": "Folder moved to recycle bin", "FileRemoved": "File moved to recycle bin", "MoveItem": "{{title}} moved", diff --git a/products/ASC.Files/Client/public/locales/ru/DeleteDialog.json b/products/ASC.Files/Client/public/locales/ru/DeleteDialog.json index 9a8cb89c1f..fe06e7c258 100644 --- a/products/ASC.Files/Client/public/locales/ru/DeleteDialog.json +++ b/products/ASC.Files/Client/public/locales/ru/DeleteDialog.json @@ -1,17 +1,19 @@ { - "OKButton": "ОК", - "CancelButton": "Отмена", - "ConfirmationTitle": "Подтверждение удаления", - "QuestionDeleteFile": "Вы собираетесь удалить этот файл?", "FilesModule": "Файлы", "FoldersModule": "Папки", - "DeleteDialogQuestion": "Вы собираетесь удалить эти элементы?", - "DeleteDialogMessage": "Пожалуйста, обратите внимание, что если вы предоставили кому-то доступ к этим файлам/папкам, они станут недоступны. Вы уверены, что хотите продолжить?", - "DeleteDialogNote": "Замечание: Это действие не может быть отменено.", - "QuestionDeleteFolder": "Вы собираетесь удалить эту папку?", - "QuestionDeleteElements": "Вы собираетесь удалить эти элементы?", "DeleteOperation": "Удаление", "DeleteSelectedElem": "Выбранные элементы успешно удалены", - "DeleteFromTrash": "Выбранные элементы успешно удалены из корзины" + "DeleteFromTrash": "Выбранные элементы успешно удалены из корзины", + + "MoveToTrashOneFileTitle": "Переместить файл в корзину?", + "MoveToTrashOneFolderTitle": "Переместить папку в корзину?", + "MoveToTrashItemsTitle": "Переместить элементы в корзину?", + + "MoveToTrashOneFileNote": "Если вы удалите расшаренный файл, другие пользователи не смогут получить к нему доступ.", + "MoveToTrashOneFolderNote": "Если вы удалите расшаренную папку, другие пользователи не смогут получить к нему доступ.", + "MoveToTrashItemsNote": "Если вы удалите расшаренные элементы, другие пользователи не смогут получить к ним доступ.", + + "MoveToTrashButton": "Переместить в корзину", + "CancelButton": "Отмена" } diff --git a/products/ASC.Files/Client/public/locales/ru/EmptyTrashDialog.json b/products/ASC.Files/Client/public/locales/ru/EmptyTrashDialog.json index 716632bd9d..380118381f 100644 --- a/products/ASC.Files/Client/public/locales/ru/EmptyTrashDialog.json +++ b/products/ASC.Files/Client/public/locales/ru/EmptyTrashDialog.json @@ -1,10 +1,10 @@ { - "OKButton": "ОК", - "CancelButton": "Отмена", - "ConfirmationTitle": "Подтверждение удаления", - - "EmptyTrashDialogQuestion": "Вы уверены, что хотите очистить корзину?", - "EmptyTrashDialogMessage": "Замечание: Это действие не может быть отменено.", "DeleteOperation": "Удаление", - "SuccessEmptyTrash": "Корзина успешно очищена" + "SuccessEmptyTrash": "Корзина успешно очищена", + + "DeleteForeverTitle": "Удалить навсегда?", + "DeleteForeverNote": "Все объекты из корзины будут удалены навсегда. Вы не сможете их восстановить.", + + "DeleteForeverButton": "Удалить навсегда", + "CancelButton": "Отмена" } diff --git a/products/ASC.Files/Client/public/locales/ru/Home.json b/products/ASC.Files/Client/public/locales/ru/Home.json index df899fb0a7..cf1f514df9 100644 --- a/products/ASC.Files/Client/public/locales/ru/Home.json +++ b/products/ASC.Files/Client/public/locales/ru/Home.json @@ -112,6 +112,8 @@ "AddFilter": "Добавить фильтр", "FolderCreated": "Создана новая папка {{itemTitle}}", "FileCreated": "Создан новый файл {{itemTitle}}.{{exst}}", + "FileRenamed": "Документ '{{oldTitle}}' переименован в '{{newTitle}}'", + "FolderRenamed": "Папка '{{folderTitle}}' переименована в '{{newFoldedTitle}}'", "FolderRemoved": "Папка перемещена в корзину", "FileRemoved": "Файл перемещен в корзину", "MoveItem": "{{title}} перемещен", diff --git a/products/ASC.Files/Client/src/Files.jsx b/products/ASC.Files/Client/src/Files.jsx index 06376c70a6..82b3c9db14 100644 --- a/products/ASC.Files/Client/src/Files.jsx +++ b/products/ASC.Files/Client/src/Files.jsx @@ -12,9 +12,9 @@ import "./custom.scss"; import i18n from "./i18n"; import { I18nextProvider } from "react-i18next"; import { regDesktop } from "@appserver/common/desktop"; -import Home from "./components/pages/Home"; -import Settings from "./components/pages/Settings"; -import VersionHistory from "./components/pages/VersionHistory"; +import Home from "./pages/Home"; +import Settings from "./pages/Settings"; +import VersionHistory from "./pages/VersionHistory"; import ErrorBoundary from "@appserver/common/components/ErrorBoundary"; import Panels from "./components/FilesPanels"; import { AppServerConfig } from "@appserver/common/constants"; diff --git a/products/ASC.Files/Client/src/HOCs/withBadges.js b/products/ASC.Files/Client/src/HOCs/withBadges.js new file mode 100644 index 0000000000..badba784b3 --- /dev/null +++ b/products/ASC.Files/Client/src/HOCs/withBadges.js @@ -0,0 +1,269 @@ +import React from "react"; +import { inject, observer } from "mobx-react"; + +import { + ShareAccessRights, + AppServerConfig, +} from "@appserver/common/constants"; +import toastr from "studio/toastr"; +import { combineUrl } from "@appserver/common/utils"; +import { + convertFile, + getFileConversationProgress, +} from "@appserver/common/api/files"; + +import Badges from "../components/Badges"; +import config from "../../package.json"; + +export default function withBadges(WrappedComponent) { + class WithBadges extends React.Component { + constructor(props) { + super(props); + this.state = { showConvertDialog: false }; + } + onClickLock = () => { + const { item, lockFileAction } = this.props; + const { locked, id } = item; + + lockFileAction(id, !locked).catch((err) => toastr.error(err)); + }; + + onClickFavorite = () => { + const { t, item, setFavoriteAction } = this.props; + + setFavoriteAction("remove", item.id) + .then(() => toastr.success(t("RemovedFromFavorites"))) + .catch((err) => toastr.error(err)); + }; + + onShowVersionHistory = () => { + const { + homepage, + isTabletView, + item, + setIsVerHistoryPanel, + fetchFileVersions, + history, + isTrashFolder, + } = this.props; + if (isTrashFolder) return; + + if (!isTabletView) { + fetchFileVersions(item.id + ""); + setIsVerHistoryPanel(true); + } else { + history.push( + combineUrl(AppServerConfig.proxyURL, homepage, `/${item.id}/history`) + ); + } + }; + onBadgeClick = () => { + const { + item, + selectedFolderPathParts, + markAsRead, + setNewFilesPanelVisible, + setNewFilesIds, + updateRootBadge, + updateFileBadge, + } = this.props; + if (item.fileExst) { + markAsRead([], [item.id]) + .then(() => { + updateRootBadge(selectedFolderPathParts[0], 1); + updateFileBadge(item.id); + }) + .catch((err) => toastr.error(err)); + } else { + setNewFilesPanelVisible(true); + const newFolderIds = selectedFolderPathParts; + newFolderIds.push(item.id); + setNewFilesIds(newFolderIds); + } + }; + + setConvertDialogVisible = () => + this.setState({ showConvertDialog: !this.state.showConvertDialog }); + + onConvert = () => { + const { item, t, setSecondaryProgressBarData } = this.props; + setSecondaryProgressBarData({ + icon: "file", + visible: true, + percent: 0, + label: t("Convert"), + alert: false, + }); + this.setState({ showConvertDialog: false }, () => + convertFile(item.id).then((convertRes) => { + if (convertRes && convertRes[0] && convertRes[0].progress !== 100) { + this.getConvertProgress(item.id); + } + }) + ); + }; + + getConvertProgress = (fileId) => { + const { + selectedFolderId, + filter, + setIsLoading, + setSecondaryProgressBarData, + t, + clearSecondaryProgressData, + fetchFiles, + } = this.props; + getFileConversationProgress(fileId).then((res) => { + if (res && res[0] && res[0].progress !== 100) { + setSecondaryProgressBarData({ + icon: "file", + visible: true, + percent: res[0].progress, + label: t("Convert"), + alert: false, + }); + setTimeout(() => this.getConvertProgress(fileId), 1000); + } else { + if (res[0].error) { + setSecondaryProgressBarData({ + visible: true, + alert: true, + }); + toastr.error(res[0].error); + setTimeout(() => clearSecondaryProgressData(), TIMEOUT); + } else { + setSecondaryProgressBarData({ + icon: "file", + visible: true, + percent: 100, + label: t("Convert"), + alert: false, + }); + setTimeout(() => clearSecondaryProgressData(), TIMEOUT); + const newFilter = filter.clone(); + fetchFiles(selectedFolderId, newFilter) + .catch((err) => { + setSecondaryProgressBarData({ + visible: true, + alert: true, + }); + //toastr.error(err); + setTimeout(() => clearSecondaryProgressData(), TIMEOUT); + }) + .finally(() => setIsLoading(false)); + } + } + }); + }; + render() { + const { showConvertDialog } = this.state; + const { + item, + canWebEdit, + isTrashFolder, + canConvert, + onFilesClick, // from withFileAction HOC + } = this.props; + const { fileStatus, access } = item; + + const newItems = item.new || fileStatus === 2; + const showNew = !!newItems; + + const accessToEdit = + access === ShareAccessRights.FullAccess || + access === ShareAccessRights.None; // TODO: fix access type for owner (now - None) + + const badgesComponent = ( + + ); + + return ( + <> + {showConvertDialog && ( + + )} + + + ); + } + } + + return inject( + ( + { + auth, + formatsStore, + treeFoldersStore, + filesActionsStore, + versionHistoryStore, + selectedFolderStore, + dialogsStore, + filesStore, + uploadDataStore, + }, + { item } + ) => { + const { docserviceStore } = formatsStore; + const { isRecycleBinFolder, updateRootBadge } = treeFoldersStore; + const { + lockFileAction, + setFavoriteAction, + markAsRead, + } = filesActionsStore; + const { isTabletView } = auth.settingsStore; + const { setIsVerHistoryPanel, fetchFileVersions } = versionHistoryStore; + const { setNewFilesPanelVisible, setNewFilesIds } = dialogsStore; + const { updateFileBadge, filter, setIsLoading, fetchFiles } = filesStore; + const { secondaryProgressDataStore } = uploadDataStore; + const { + setSecondaryProgressBarData, + clearSecondaryProgressData, + } = secondaryProgressDataStore; + + const canWebEdit = docserviceStore.canWebEdit(item.fileExst); + const canConvert = docserviceStore.canConvert(item.fileExst); + + return { + canWebEdit, + canConvert, + isTrashFolder: isRecycleBinFolder, + lockFileAction, + setFavoriteAction, + homepage: config.homepage, + isTabletView, + setIsVerHistoryPanel, + fetchFileVersions, + selectedFolderPathParts: selectedFolderStore.pathParts, + markAsRead, + setNewFilesPanelVisible, + setNewFilesIds, + updateRootBadge, + updateFileBadge, + setSecondaryProgressBarData, + selectedFolderId: selectedFolderStore.id, + filter, + setIsLoading, + clearSecondaryProgressData, + fetchFiles, + }; + } + )(observer(WithBadges)); +} diff --git a/products/ASC.Files/Client/src/HOCs/withContent.js b/products/ASC.Files/Client/src/HOCs/withContent.js new file mode 100644 index 0000000000..f453b200a0 --- /dev/null +++ b/products/ASC.Files/Client/src/HOCs/withContent.js @@ -0,0 +1,364 @@ +import React from "react"; +import { inject, observer } from "mobx-react"; +import { Trans } from "react-i18next"; +import { isMobile } from "react-device-detect"; + +import toastr from "studio/toastr"; +import { + AppServerConfig, + FileAction, + ShareAccessRights, +} from "@appserver/common/constants"; +import { combineUrl } from "@appserver/common/utils"; + +import config from "../../package.json"; +import EditingWrapperComponent from "../components/EditingWrapperComponent"; +import { getTitleWithoutExst } from "../helpers/files-helpers"; + +export default function withContent(WrappedContent) { + class WithContent extends React.Component { + constructor(props) { + super(props); + let titleWithoutExt = getTitleWithoutExst(props.item); + + if (props.fileActionId === -1) { + titleWithoutExt = this.getDefaultName(props.fileActionExt); + } + + this.state = { + itemTitle: titleWithoutExt, + + //loading: false + }; + } + + componentDidUpdate(prevProps) { + const { fileActionId, fileActionExt } = this.props; + if (fileActionId === -1 && fileActionExt !== prevProps.fileActionExt) { + const itemTitle = this.getDefaultName(fileActionExt); + this.setState({ itemTitle }); + } + // if (fileAction) { + // if (fileActionId !== prevProps.fileActionId) { + // this.setState({ editingId: fileActionId }); + // } + // } + } + + getDefaultName = (format) => { + const { t } = this.props; + + switch (format) { + case "docx": + return t("NewDocument"); + case "xlsx": + return t("NewSpreadsheet"); + case "pptx": + return t("NewPresentation"); + default: + return t("NewFolder"); + } + }; + + completeAction = (id) => { + const { editCompleteAction, item } = this.props; + + const isCancel = + (id.currentTarget && id.currentTarget.dataset.action === "cancel") || + id.keyCode === 27; + editCompleteAction(id, item, isCancel); + }; + + updateItem = () => { + const { + t, + updateFile, + renameFolder, + item, + setIsLoading, + fileActionId, + editCompleteAction, + } = this.props; + + const { itemTitle } = this.state; + const originalTitle = getTitleWithoutExst(item); + + setIsLoading(true); + const isSameTitle = + originalTitle.trim() === itemTitle.trim() || itemTitle.trim() === ""; + if (isSameTitle) { + this.setState({ + itemTitle: originalTitle, + }); + return editCompleteAction(fileActionId, item, isSameTitle); + } + + item.fileExst || item.contentLength + ? updateFile(fileActionId, itemTitle) + .then(() => this.completeAction(fileActionId)) + .then(() => + toastr.success( + t("FileRenamed", { + oldTitle: item.title, + newTitle: itemTitle + item.fileExst, + }) + ) + ) + .catch((err) => toastr.error(err)) + .finally(() => setIsLoading(false)) + : renameFolder(fileActionId, itemTitle) + .then(() => this.completeAction(fileActionId)) + .then(() => + toastr.success( + t("FolderRenamed", { + folderTitle: item.title, + newFoldedTitle: itemTitle, + }) + ) + ) + .catch((err) => toastr.error(err)) + .finally(() => setIsLoading(false)); + }; + + cancelUpdateItem = (e) => { + const { item } = this.props; + + const originalTitle = getTitleWithoutExst(item); + this.setState({ + itemTitle: originalTitle, + }); + + return this.completeAction(e); + }; + + onClickUpdateItem = (e) => { + const { fileActionType } = this.props; + + fileActionType === FileAction.Create + ? this.createItem(e) + : this.updateItem(e); + }; + + createItem = (e) => { + const { + createFile, + item, + setIsLoading, + openDocEditor, + isPrivacy, + isDesktop, + replaceFileStream, + t, + setEncryptionAccess, + createFolder, + } = this.props; + const { itemTitle } = this.state; + + setIsLoading(true); + + const itemId = e.currentTarget.dataset.itemid; + + if (itemTitle.trim() === "") { + toastr.warning(t("CreateWithEmptyTitle")); + return this.completeAction(itemId); + } + + let tab = + !isDesktop && item.fileExst + ? window.open( + combineUrl( + AppServerConfig.proxyURL, + config.homepage, + "/products/files/doceditor" + ), + "_blank" + ) + : null; + + !item.fileExst && !item.contentLength + ? createFolder(item.parentId, itemTitle) + .then(() => this.completeAction(itemId)) + .then(() => + toastr.success( + + New folder {{ itemTitle }} is created + + ) + ) + .catch((e) => toastr.error(e)) + .finally(() => { + return setIsLoading(false); + }) + : createFile(item.parentId, `${itemTitle}.${item.fileExst}`) + .then((file) => { + if (isPrivacy) { + return setEncryptionAccess(file).then((encryptedFile) => { + if (!encryptedFile) return Promise.resolve(); + toastr.info(t("EncryptedFileSaving")); + return replaceFileStream( + file.id, + encryptedFile, + true, + false + ).then(() => + openDocEditor(file.id, file.providerKey, tab, file.webUrl) + ); + }); + } + return openDocEditor(file.id, file.providerKey, tab, file.webUrl); + }) + .then(() => this.completeAction(itemId)) + .then(() => { + const exst = item.fileExst; + return toastr.success( + + New file {{ itemTitle }}.{{ exst }} is created + + ); + }) + .catch((e) => toastr.error(e)) + .finally(() => { + return setIsLoading(false); + }); + }; + + renameTitle = (e) => { + const { t } = this.props; + + let title = e.target.value; + //const chars = '*+:"<>?|/'; TODO: think how to solve problem with interpolation escape values in i18n translate + const regexp = new RegExp('[*+:"<>?|\\\\/]', "gim"); + if (title.match(regexp)) { + toastr.warning(t("ContainsSpecCharacter")); + } + title = title.replace(regexp, "_"); + return this.setState({ itemTitle: title }); + }; + + getStatusByDate = () => { + const { culture, t, item, sectionWidth } = this.props; + const { created, updated, version, fileExst } = item; + + const title = + version > 1 + ? t("TitleModified") + : fileExst + ? t("TitleUploaded") + : t("TitleCreated"); + + const date = fileExst ? updated : created; + const dateLabel = new Date(date).toLocaleString(culture); + const mobile = (sectionWidth && sectionWidth <= 375) || isMobile; + + return mobile ? dateLabel : `${title}: ${dateLabel}`; + }; + + render() { + const { itemTitle } = this.state; + const { + item, + fileActionId, + fileActionExt, + isLoading, + viewer, + t, + isTrashFolder, + onFilesClick, + } = this.props; + const { id, fileExst, updated, createdBy, access, fileStatus } = item; + + const titleWithoutExt = getTitleWithoutExst(item); + + const isEdit = id === fileActionId && fileExst === fileActionExt; + + const updatedDate = updated && this.getStatusByDate(); + + const fileOwner = + createdBy && + ((viewer.id === createdBy.id && t("AuthorMe")) || + createdBy.displayName); + + const accessToEdit = + access === ShareAccessRights.FullAccess || // only badges? + access === ShareAccessRights.None; // TODO: fix access type for owner (now - None) + + const linkStyles = isTrashFolder //|| window.innerWidth <= 1024 + ? { noHover: true } + : { onClick: onFilesClick }; + + const newItems = item.new || fileStatus === 2; + const showNew = !!newItems; + + return isEdit ? ( + + ) : ( + + ); + } + } + + return inject( + ({ filesActionsStore, filesStore, treeFoldersStore, auth }, {}) => { + const { editCompleteAction } = filesActionsStore; + const { + setIsLoading, + openDocEditor, + updateFile, + renameFolder, + createFile, + createFolder, + isLoading, + } = filesStore; + const { isRecycleBinFolder, isPrivacyFolder } = treeFoldersStore; + + const { + type: fileActionType, + extension: fileActionExt, + id: fileActionId, + } = filesStore.fileActionStore; + const { replaceFileStream, setEncryptionAccess } = auth; + const { culture, isDesktopClient } = auth.settingsStore; + + return { + editCompleteAction, + setIsLoading, + isTrashFolder: isRecycleBinFolder, + openDocEditor, + updateFile, + renameFolder, + fileActionId, + editCompleteAction, + fileActionType, + createFile, + isPrivacy: isPrivacyFolder, + isDesktop: isDesktopClient, + replaceFileStream, + setEncryptionAccess, + createFolder, + fileActionExt, + isLoading, + culture, + homepage: config.homepage, + viewer: auth.userStore.user, + }; + } + )(observer(WithContent)); +} diff --git a/products/ASC.Files/Client/src/HOCs/withContextOptions.js b/products/ASC.Files/Client/src/HOCs/withContextOptions.js new file mode 100644 index 0000000000..8ec66f6c33 --- /dev/null +++ b/products/ASC.Files/Client/src/HOCs/withContextOptions.js @@ -0,0 +1,503 @@ +import React from "react"; +import { inject, observer } from "mobx-react"; +import copy from "copy-to-clipboard"; + +import { combineUrl } from "@appserver/common/utils"; +import { FileAction, AppServerConfig } from "@appserver/common/constants"; +import toastr from "studio/toastr"; + +import config from "../../package.json"; + +export default function withContextOptions(WrappedComponent) { + class WithContextOptions extends React.Component { + onOpenLocation = () => { + const { item, openLocationAction } = this.props; + const { id, folderId, fileExst } = item; + + const locationId = !fileExst ? id : folderId; + openLocationAction(locationId, !fileExst); + }; + + onOwnerChange = () => { + const { setChangeOwnerPanelVisible } = this.props; + setChangeOwnerPanelVisible(true); + }; + onMoveAction = () => { + const { setMoveToPanelVisible } = this.props; + setMoveToPanelVisible(true); + }; + onCopyAction = () => { + const { setCopyPanelVisible } = this.props; + setCopyPanelVisible(true); + }; + + showVersionHistory = () => { + const { + item, + isTabletView, + fetchFileVersions, + setIsVerHistoryPanel, + history, + homepage, + isTrashFolder, + } = this.props; + const { id } = item; + if (isTrashFolder) return; + + if (!isTabletView) { + fetchFileVersions(id + ""); + setIsVerHistoryPanel(true); + } else { + history.push( + combineUrl(AppServerConfig.proxyURL, homepage, `/${id}/history`) + ); + } + }; + + finalizeVersion = () => { + const { item, finalizeVersionAction } = this.props; + const { id } = item; + finalizeVersionAction(id).catch((err) => toastr.error(err)); + }; + + onClickFavorite = (e) => { + const { item, setFavoriteAction, t } = this.props; + const { id } = item; + const data = (e.currentTarget && e.currentTarget.dataset) || e; + const { action } = data; + + setFavoriteAction(action, id) + .then(() => + action === "mark" + ? toastr.success(t("MarkedAsFavorite")) + : toastr.success(t("RemovedFromFavorites")) + ) + .catch((err) => toastr.error(err)); + }; + + lockFile = () => { + const { item, lockFileAction } = this.props; + const { id, locked } = item; + lockFileAction(id, !locked).catch((err) => toastr.error(err)); + }; + + onClickLinkForPortal = () => { + const { item, homepage, t } = this.props; + const { fileExst, canOpenPlayer, webUrl } = item; + + const isFile = !!fileExst; + copy( + isFile + ? canOpenPlayer + ? `${window.location.href}&preview=${id}` + : webUrl + : `${window.location.origin + homepage}/filter?folder=${id}` + ); + + toastr.success(t("LinkCopySuccess")); + }; + + onClickLinkEdit = () => { + const { item, openDocEditor } = this.props; + const { id, providerKey } = item; + openDocEditor(id, providerKey); + }; + + onClickDownload = () => { + const { item, downloadAction, t } = this.props; + const { fileExst, contentLength, viewUrl } = item; + const isFile = !!fileExst && contentLength; + isFile + ? window.open(viewUrl, "_blank") + : downloadAction(t("ArchivingData")).catch((err) => toastr.error(err)); + }; + + onClickDownloadAs = () => { + const { setDownloadDialogVisible } = this.props; + setDownloadDialogVisible(true); + }; + + onDuplicate = () => { + const { duplicateAction, t, item } = this.props; + duplicateAction(item, t("CopyOperation")).catch((err) => + toastr.error(err) + ); + }; + + onClickRename = () => { + const { item, setAction } = this.props; + const { id, fileExst } = item; + setAction({ + type: FileAction.Rename, + extension: fileExst, + id, + }); + }; + + onChangeThirdPartyInfo = () => { + const { item, setThirdpartyInfo } = this.props; + const { providerKey } = item; + setThirdpartyInfo(providerKey); + }; + + onMediaFileClick = (fileId) => { + const { item, setMediaViewerData } = this.props; + const itemId = typeof fileId !== "object" ? fileId : item.id; + setMediaViewerData({ visible: true, id: itemId }); + }; + + onClickDelete = () => { + const { + item, + setRemoveItem, + setDeleteThirdPartyDialogVisible, + confirmDelete, + setDeleteDialogVisible, + t, + deleteFileAction, + deleteFolderAction, + isThirdPartyFolder, + } = this.props; + const { id, title, fileExst, contentLength, folderId, parentId } = item; + + if (isThirdPartyFolder) { + const splitItem = id.split("-"); + setRemoveItem({ id: splitItem[splitItem.length - 1], title }); + setDeleteThirdPartyDialogVisible(true); + return; + } + + if (confirmDelete) { + setDeleteDialogVisible(true); + } else { + const translations = { + deleteOperation: t("DeleteOperation"), + }; + + fileExst || contentLength + ? deleteFileAction(id, folderId, translations) + .then(() => toastr.success(t("FileRemoved"))) + .catch((err) => toastr.error(err)) + : deleteFolderAction(id, parentId, translations) + .then(() => toastr.success(t("FolderRemoved"))) + .catch((err) => toastr.error(err)); + } + }; + + onClickShare = () => { + const { onSelectItem, setSharingPanelVisible, item } = this.props; + onSelectItem(item); + setSharingPanelVisible(true); + }; + + getFilesContextOptions = () => { + const { item, t, isThirdPartyFolder } = this.props; + const { access, contextOptions } = item; + const isSharable = access !== 1 && access !== 0; + return contextOptions.map((option) => { + switch (option) { + case "open": + return { + key: option, + label: t("Open"), + icon: "images/catalog.folder.react.svg", + onClick: this.onOpenLocation, + disabled: false, + }; + case "show-version-history": + return { + key: option, + label: t("ShowVersionHistory"), + icon: "images/history.react.svg", + onClick: this.showVersionHistory, + disabled: false, + }; + case "finalize-version": + return { + key: option, + label: t("FinalizeVersion"), + icon: "images/history-finalized.react.svg", + onClick: this.finalizeVersion, + disabled: false, + }; + case "separator0": + case "separator1": + case "separator2": + case "separator3": + return { key: option, isSeparator: true }; + case "open-location": + return { + key: option, + label: t("OpenLocation"), + icon: "images/download-as.react.svg", + onClick: this.onOpenLocation, + disabled: false, + }; + case "mark-as-favorite": + return { + key: option, + label: t("MarkAsFavorite"), + icon: "images/favorites.react.svg", + onClick: this.onClickFavorite, + disabled: false, + "data-action": "mark", + action: "mark", + }; + case "block-unblock-version": + return { + key: option, + label: t("UnblockVersion"), + icon: "images/lock.react.svg", + onClick: this.lockFile, + disabled: false, + }; + case "sharing-settings": + return { + key: option, + label: t("SharingSettings"), + icon: "images/catalog.shared.react.svg", + onClick: this.onClickShare, + disabled: isSharable, + }; + case "send-by-email": + return { + key: option, + label: t("SendByEmail"), + icon: "/static/images/mail.react.svg", + disabled: true, + }; + case "owner-change": + return { + key: option, + label: t("ChangeOwner"), + icon: "images/catalog.user.react.svg", + onClick: this.onOwnerChange, + disabled: false, + }; + case "link-for-portal-users": + return { + key: option, + label: t("LinkForPortalUsers"), + icon: "/static/images/invitation.link.react.svg", + onClick: this.onClickLinkForPortal, + disabled: false, + }; + case "edit": + return { + key: option, + label: t("Edit"), + icon: "/static/images/access.edit.react.svg", + onClick: this.onClickLinkEdit, + disabled: false, + }; + case "preview": + return { + key: option, + label: t("Preview"), + icon: "EyeIcon", + onClick: this.onClickLinkEdit, + disabled: true, + }; + case "view": + return { + key: option, + label: t("View"), + icon: "/static/images/eye.react.svg", + onClick: this.onMediaFileClick, + disabled: false, + }; + case "download": + return { + key: option, + label: t("Download"), + icon: "images/download.react.svg", + onClick: this.onClickDownload, + disabled: false, + }; + case "download-as": + return { + key: option, + label: t("DownloadAs"), + icon: "images/download-as.react.svg", + onClick: this.onClickDownloadAs, + disabled: false, + }; + case "move-to": + return { + key: option, + label: t("MoveTo"), + icon: "images/move.react.svg", + onClick: this.onMoveAction, + disabled: false, + }; + case "restore": + return { + key: option, + label: t("Restore"), + icon: "images/move.react.svg", + onClick: this.onMoveAction, + disabled: false, + }; + case "copy-to": + return { + key: option, + label: t("Copy"), + icon: "/static/images/copy.react.svg", + onClick: this.onCopyAction, + disabled: false, + }; + case "copy": + return { + key: option, + label: t("Duplicate"), + icon: "/static/images/copy.react.svg", + onClick: this.onDuplicate, + disabled: false, + }; + case "rename": + return { + key: option, + label: t("Rename"), + icon: "images/rename.react.svg", + onClick: this.onClickRename, + disabled: false, + }; + case "change-thirdparty-info": + return { + key: option, + label: t("ThirdPartyInfo"), + icon: "/static/images/access.edit.react.svg", + onClick: this.onChangeThirdPartyInfo, + disabled: false, + }; + case "delete": + return { + key: option, + label: isThirdPartyFolder ? t("DeleteThirdParty") : t("Delete"), + icon: "/static/images/catalog.trash.react.svg", + onClick: this.onClickDelete, + disabled: false, + }; + case "remove-from-favorites": + return { + key: option, + label: t("RemoveFromFavorites"), + icon: "images/favorites.react.svg", + onClick: this.onClickFavorite, + disabled: false, + "data-action": "remove", + action: "remove", + }; + default: + break; + } + + return undefined; + }); + }; + render() { + const { actionType, actionId, actionExtension, item } = this.props; + const { id, fileExst, contextOptions } = item; + + const isEdit = + !!actionType && actionId === id && fileExst === actionExtension; + + const contextOptionsProps = + !isEdit && contextOptions && contextOptions.length > 0 + ? { + contextOptions: this.getFilesContextOptions(), + } + : {}; + + return ( + + ); + } + } + + return inject( + ( + { + filesStore, + filesActionsStore, + auth, + versionHistoryStore, + mediaViewerDataStore, + settingsStore, + selectedFolderStore, + dialogsStore, + treeFoldersStore, + }, + { item } + ) => { + const { openDocEditor, fileActionStore } = filesStore; + const { + openLocationAction, + finalizeVersionAction, + setFavoriteAction, + lockFileAction, + downloadAction, + duplicateAction, + setThirdpartyInfo, + deleteFileAction, + deleteFolderAction, + onSelectItem, + } = filesActionsStore; + const { + setChangeOwnerPanelVisible, + setMoveToPanelVisible, + setCopyPanelVisible, + setDownloadDialogVisible, + setRemoveItem, + setDeleteThirdPartyDialogVisible, + setDeleteDialogVisible, + setSharingPanelVisible, + } = dialogsStore; + const { isTabletView } = auth.settingsStore; + const { setIsVerHistoryPanel, fetchFileVersions } = versionHistoryStore; + const { setAction, type, extension, id } = fileActionStore; + const { setMediaViewerData } = mediaViewerDataStore; + const { isRootFolder } = selectedFolderStore; + const { isRecycleBinFolder } = treeFoldersStore; + + const isThirdPartyFolder = item.providerKey && isRootFolder; + + return { + openLocationAction, + setChangeOwnerPanelVisible, + setMoveToPanelVisible, + setCopyPanelVisible, + isTabletView, + setIsVerHistoryPanel, + fetchFileVersions, + homepage: config.homepage, + finalizeVersionAction, + setFavoriteAction, + lockFileAction, + openDocEditor, + downloadAction, + setDownloadDialogVisible, + duplicateAction, + setAction, + setThirdpartyInfo, + setMediaViewerData, + setRemoveItem, + setDeleteThirdPartyDialogVisible, + confirmDelete: settingsStore.confirmDelete, + setDeleteDialogVisible, + deleteFileAction, + deleteFolderAction, + isThirdPartyFolder, + onSelectItem, + setSharingPanelVisible, + actionType: type, + actionId: id, + actionExtension: extension, + isTrashFolder: isRecycleBinFolder, + }; + } + )(observer(WithContextOptions)); +} diff --git a/products/ASC.Files/Client/src/HOCs/withFileActions.js b/products/ASC.Files/Client/src/HOCs/withFileActions.js new file mode 100644 index 0000000000..08f5d8f870 --- /dev/null +++ b/products/ASC.Files/Client/src/HOCs/withFileActions.js @@ -0,0 +1,339 @@ +import React from "react"; +import { inject, observer } from "mobx-react"; +import { ReactSVG } from "react-svg"; + +import IconButton from "@appserver/components/icon-button"; +import Text from "@appserver/components/text"; + +import { EncryptedFileIcon } from "../components/Icons"; + +const svgLoader = () =>
; +export default function withFileActions(WrappedFileItem) { + class WithFileActions extends React.Component { + onContentRowSelect = (checked, file) => { + const { selectRowAction } = this.props; + if (!file) return; + selectRowAction(checked, file); + }; + + onClickShare = () => { + const { onSelectItem, setSharingPanelVisible, item } = this.props; + onSelectItem(item); + setSharingPanelVisible(true); + }; + + rowContextClick = () => { + const { onSelectItem, item } = this.props; + onSelectItem(item); + }; + + getSharedButton = (shared) => { + const { t } = this.props; + const color = shared ? "#657077" : "#a3a9ae"; + return ( + + + {t("Share")} + + ); + }; + + getItemIcon = (isEdit) => { + const { item, isPrivacy } = this.props; + const { icon, fileExst } = item; + return ( + <> + + {isPrivacy && fileExst && } + + ); + }; + + onDropZoneUpload = (files, uploadToFolder) => { + const { + selectedFolderId, + dragging, + setDragging, + startUpload, + } = this.props; + + const folderId = uploadToFolder ? uploadToFolder : selectedFolderId; + dragging && setDragging(false); + startUpload(files, folderId, t); + }; + + onDrop = (items) => { + const { item, selectedFolderId } = this.props; + const { fileExst, id } = item; + + if (!fileExst) { + this.onDropZoneUpload(items, id); + } else { + this.onDropZoneUpload(items, selectedFolderId); + } + }; + + onMouseDown = (e) => { + const { draggable, setTooltipPosition, setStartDrag } = this.props; + if (!draggable) { + return; + } + + if ( + window.innerWidth < 1025 || + e.target.tagName === "rect" || + e.target.tagName === "path" + ) { + return; + } + const mouseButton = e.which + ? e.which !== 1 + : e.button + ? e.button !== 0 + : false; + const label = e.currentTarget.getAttribute("label"); + if (mouseButton || e.currentTarget.tagName !== "DIV" || label) { + return; + } + + setTooltipPosition(e.pageX, e.pageY); + setStartDrag(true); + }; + + onFilesClick = () => { + const { + filter, + parentFolder, + setIsLoading, + fetchFiles, + isImage, + isSound, + isVideo, + canWebEdit, + item, + isTrashFolder, + openDocEditor, + expandedKeys, + addExpandedKeys, + setMediaViewerData, + } = this.props; + const { id, fileExst, viewUrl, providerKey, contentLength } = item; + + if (isTrashFolder) return; + + if (!fileExst && !contentLength) { + setIsLoading(true); + + if (!expandedKeys.includes(parentFolder + "")) { + addExpandedKeys(parentFolder + ""); + } + + fetchFiles(id, filter) + .catch((err) => { + toastr.error(err); + setIsLoading(false); + }) + .finally(() => setIsLoading(false)); + } else { + if (canWebEdit) { + return openDocEditor(id, providerKey); + } + + if (isImage || isSound || isVideo) { + setMediaViewerData({ visible: true, id }); + return; + } + + return window.open(viewUrl, "_blank"); + } + }; + + render() { + const { + item, + isRecycleBin, + draggable, + canShare, + isPrivacy, + actionType, + actionExtension, + actionId, + sectionWidth, + checked, + dragging, + isFolder, + } = this.props; + const { fileExst, access, contentLength, id, shared } = item; + + const isEdit = + !!actionType && actionId === id && fileExst === actionExtension; + + const isDragging = isFolder && access < 2 && !isRecycleBin; + + let className = isDragging ? " droppable" : ""; + if (draggable) className += " draggable not-selectable"; + + let value = fileExst || contentLength ? `file_${id}` : `folder_${id}`; + value += draggable ? "_draggable" : ""; + + const isMobile = sectionWidth < 500; + const displayShareButton = isMobile + ? "26px" + : !canShare + ? "38px" + : "96px"; + + const sharedButton = + !canShare || (isPrivacy && !fileExst) || isEdit || id <= 0 || isMobile + ? null + : this.getSharedButton(shared); + + const checkedProps = isEdit || id <= 0 ? {} : { checked }; + const element = this.getItemIcon(isEdit || id <= 0); + + return ( + + ); + } + } + + return inject( + ( + { + filesActionsStore, + dialogsStore, + treeFoldersStore, + selectedFolderStore, + filesStore, + uploadDataStore, + formatsStore, + mediaViewerDataStore, + }, + { item, t, history } + ) => { + const { selectRowAction, onSelectItem } = filesActionsStore; + const { setSharingPanelVisible } = dialogsStore; + const { + isPrivacyFolder, + isRecycleBinFolder, + expandedKeys, + addExpandedKeys, + } = treeFoldersStore; + const { id: selectedFolderId, isRootFolder } = selectedFolderStore; + const { + dragging, + setDragging, + selection, + setTooltipPosition, + setStartDrag, + fileActionStore, + canShare, + isFileSelected, + filter, + setIsLoading, + fetchFiles, + openDocEditor, + } = filesStore; + const { startUpload } = uploadDataStore; + const { type, extension, id } = fileActionStore; + const { + iconFormatsStore, + mediaViewersFormatsStore, + docserviceStore, + } = formatsStore; + const { setMediaViewerData } = mediaViewerDataStore; + + const selectedItem = selection.find( + (x) => x.id === item.id && x.fileExst === item.fileExst + ); + + const draggable = + !isRecycleBinFolder && selectedItem && selectedItem.id !== id; + + const isFolder = selectedItem + ? false + : item.fileExst //|| item.contentLength + ? false + : true; + + const isImage = iconFormatsStore.isImage(item.fileExst); + const isSound = iconFormatsStore.isSound(item.fileExst); + const isVideo = mediaViewersFormatsStore.isVideo(item.fileExst); + const canWebEdit = docserviceStore.canWebEdit(item.fileExst); + + return { + t, + item, + selectRowAction, + onSelectItem, + setSharingPanelVisible, + isPrivacy: isPrivacyFolder, + selectedFolderId, + dragging, + setDragging, + startUpload, + draggable, + setTooltipPosition, + setStartDrag, + history, + isFolder, + isRootFolder, + canShare, + actionType: type, + actionExtension: extension, + actionId: id, + checked: isFileSelected(item.id, item.parentId), + filter, + parentFolder: selectedFolderStore.parentId, + setIsLoading, + fetchFiles, + isImage, + isSound, + isVideo, + canWebEdit, + isTrashFolder: isRecycleBinFolder, + openDocEditor, + expandedKeys, + addExpandedKeys, + setMediaViewerData, + }; + } + )(observer(WithFileActions)); +} diff --git a/products/ASC.Files/Client/src/HOCs/withLoader.js b/products/ASC.Files/Client/src/HOCs/withLoader.js new file mode 100644 index 0000000000..669617e745 --- /dev/null +++ b/products/ASC.Files/Client/src/HOCs/withLoader.js @@ -0,0 +1,51 @@ +import React, { useEffect, useState } from "react"; +import { observer, inject } from "mobx-react"; +import { isMobile } from "react-device-detect"; + +import Loaders from "@appserver/common/components/Loaders"; + +let loadTimeout = null; +export default function withLoader(WrappedComponent, type) { + const withLoader = (props) => { + const { tReady, firstLoad, isLoaded, isLoading } = props; + const [inLoad, setInLoad] = useState(false); + + const cleanTimer = () => { + loadTimeout && clearTimeout(loadTimeout); + loadTimeout = null; + }; + + useEffect(() => { + if (isLoading) { + cleanTimer(); + loadTimeout = setTimeout(() => { + console.log("inLoad", true); + setInLoad(true); + }, 500); + } else { + cleanTimer(); + console.log("inLoad", false); + setInLoad(false); + } + + return () => { + cleanTimer(); + }; + }, [isLoading]); + + return firstLoad || !isLoaded || (isMobile && inLoad) || !tReady ? ( + + ) : ( + + ); + }; + + return inject(({ auth, filesStore }) => { + const { firstLoad, isLoading } = filesStore; + return { + firstLoad, + isLoaded: auth.isLoaded, + isLoading, + }; + })(observer(withLoader)); +} diff --git a/products/ASC.Files/Client/src/components/Article/Body/TreeSettings.js b/products/ASC.Files/Client/src/components/Article/Body/TreeSettings.js index 2db7a2dbcf..8a0125a8dd 100644 --- a/products/ASC.Files/Client/src/components/Article/Body/TreeSettings.js +++ b/products/ASC.Files/Client/src/components/Article/Body/TreeSettings.js @@ -81,6 +81,7 @@ const PureTreeSettings = ({ history, setIsLoading, t, + isVisitor, }) => { const { setting } = match.params; @@ -168,7 +169,7 @@ const PureTreeSettings = ({ title={t("TreeSettingsAdminSettings")} /> ) : null} - {enableThirdParty ? ( + {enableThirdParty && !isVisitor ? ( toastr.error(err)) .finally(() => { - setIsLoading(false); }); } else { - newFilter.startIndex = 0; const urlFilter = newFilter.toUrlParams(); history.push( @@ -82,7 +80,13 @@ class ArticleBodyContent extends React.Component { }; render() { - const { treeFolders, onTreeDrop, selectedTreeNode } = this.props; + const { + treeFolders, + onTreeDrop, + selectedTreeNode, + enableThirdParty, + isVisitor, + } = this.props; return isEmpty(treeFolders) ? ( @@ -96,14 +100,21 @@ class ArticleBodyContent extends React.Component { onTreeDrop={onTreeDrop} /> - + {enableThirdParty && !isVisitor && } ); } } export default inject( - ({ filesStore, treeFoldersStore, selectedFolderStore, dialogsStore }) => { + ({ + auth, + filesStore, + treeFoldersStore, + selectedFolderStore, + dialogsStore, + settingsStore, + }) => { const { fetchFiles, filter, setIsLoading } = filesStore; const { treeFolders, setSelectedNode, setTreeFolders } = treeFoldersStore; const selectedTreeNode = @@ -119,6 +130,8 @@ export default inject( treeFolders, selectedTreeNode, filter, + enableThirdParty: settingsStore.enableThirdParty, + isVisitor: auth.userStore.user.isVisitor, setIsLoading, fetchFiles, diff --git a/products/ASC.Files/Client/src/components/Badges.js b/products/ASC.Files/Client/src/components/Badges.js new file mode 100644 index 0000000000..80466b3893 --- /dev/null +++ b/products/ASC.Files/Client/src/components/Badges.js @@ -0,0 +1,123 @@ +import React from "react"; +import Badge from "@appserver/components/badge"; +import IconButton from "@appserver/components/icon-button"; +import { + StyledFavoriteIcon, + StyledFileActionsConvertEditDocIcon, + StyledFileActionsLockedIcon, +} from "./Icons"; + +const Badges = ({ + newItems, + item, + canWebEdit, + isTrashFolder, + /* canConvert, */ + accessToEdit, + showNew, + onFilesClick, + onClickLock, + onClickFavorite, + onShowVersionHistory, + onBadgeClick, + /*setConvertDialogVisible*/ +}) => { + const { id, locked, fileStatus, versionGroup, title, fileExst } = item; + + return fileExst ? ( +
+ {/* TODO: Uncomment after fix conversation {canConvert && !isTrashFolder && ( + + )} */} + {canWebEdit && !isTrashFolder && accessToEdit && ( + + )} + {locked && accessToEdit && ( + + )} + {fileStatus === 32 && !isTrashFolder && ( + + )} + {fileStatus === 1 && ( + + )} + {versionGroup > 1 && ( + + )} + {showNew && ( + + )} +
+ ) : ( + showNew && ( + + ) + ); +}; + +export default Badges; diff --git a/products/ASC.Files/Client/src/components/pages/Home/Section/Body/EditingWrapperComponent.js b/products/ASC.Files/Client/src/components/EditingWrapperComponent.js similarity index 81% rename from products/ASC.Files/Client/src/components/pages/Home/Section/Body/EditingWrapperComponent.js rename to products/ASC.Files/Client/src/components/EditingWrapperComponent.js index 20542112db..6ac5d79df7 100644 --- a/products/ASC.Files/Client/src/components/pages/Home/Section/Body/EditingWrapperComponent.js +++ b/products/ASC.Files/Client/src/components/EditingWrapperComponent.js @@ -2,6 +2,35 @@ import React, { useState } from "react"; import styled from "styled-components"; import Button from "@appserver/components/button"; import TextInput from "@appserver/components/text-input"; +import commonIconsStyles from "@appserver/components/utils/common-icons-style"; + +import CheckIcon from "../../public/images/check.react.svg"; +import CrossIcon from "../../../../../public/images/cross.react.svg"; + +const StyledCheckIcon = styled(CheckIcon)` + ${commonIconsStyles} + path { + fill: #a3a9ae; + } + :hover { + fill: #657077; + } +`; + +const StyledCrossIcon = styled(CrossIcon)` + ${commonIconsStyles} + path { + fill: #a3a9ae; + } + :hover { + fill: #657077; + } +`; + +export const okIcon = ; +export const cancelIcon = ( + +); const EditingWrapper = styled.div` width: 100%; @@ -49,8 +78,6 @@ const EditingWrapperComponent = (props) => { const { itemTitle, itemId, - okIcon, - cancelIcon, renameTitle, onClickUpdateItem, cancelUpdateItem, diff --git a/products/ASC.Files/Client/src/components/pages/Home/Section/Body/EmptyContainer/EmptyContainer.js b/products/ASC.Files/Client/src/components/EmptyContainer/EmptyContainer.js similarity index 100% rename from products/ASC.Files/Client/src/components/pages/Home/Section/Body/EmptyContainer/EmptyContainer.js rename to products/ASC.Files/Client/src/components/EmptyContainer/EmptyContainer.js diff --git a/products/ASC.Files/Client/src/components/pages/Home/Section/Body/EmptyContainer/EmptyFilterContainer.js b/products/ASC.Files/Client/src/components/EmptyContainer/EmptyFilterContainer.js similarity index 100% rename from products/ASC.Files/Client/src/components/pages/Home/Section/Body/EmptyContainer/EmptyFilterContainer.js rename to products/ASC.Files/Client/src/components/EmptyContainer/EmptyFilterContainer.js diff --git a/products/ASC.Files/Client/src/components/pages/Home/Section/Body/EmptyContainer/EmptyFolderContainer.js b/products/ASC.Files/Client/src/components/EmptyContainer/EmptyFolderContainer.js similarity index 100% rename from products/ASC.Files/Client/src/components/pages/Home/Section/Body/EmptyContainer/EmptyFolderContainer.js rename to products/ASC.Files/Client/src/components/EmptyContainer/EmptyFolderContainer.js diff --git a/products/ASC.Files/Client/src/components/pages/Home/Section/Body/EmptyContainer/RootFolderContainer.js b/products/ASC.Files/Client/src/components/EmptyContainer/RootFolderContainer.js similarity index 100% rename from products/ASC.Files/Client/src/components/pages/Home/Section/Body/EmptyContainer/RootFolderContainer.js rename to products/ASC.Files/Client/src/components/EmptyContainer/RootFolderContainer.js diff --git a/products/ASC.Files/Client/src/components/pages/Home/Section/Body/EmptyContainer/index.js b/products/ASC.Files/Client/src/components/EmptyContainer/index.js similarity index 100% rename from products/ASC.Files/Client/src/components/pages/Home/Section/Body/EmptyContainer/index.js rename to products/ASC.Files/Client/src/components/EmptyContainer/index.js diff --git a/products/ASC.Files/Client/src/components/Icons.js b/products/ASC.Files/Client/src/components/Icons.js new file mode 100644 index 0000000000..3e9538e4bf --- /dev/null +++ b/products/ASC.Files/Client/src/components/Icons.js @@ -0,0 +1,36 @@ +import styled from "styled-components"; + +import commonIconsStyles from "@appserver/components/utils/common-icons-style"; + +import FavoriteIcon from "../../public/images/favorite.react.svg"; +import FileActionsConvertEditDocIcon from "../../public/images/file.actions.convert.edit.doc.react.svg"; +import FileActionsLockedIcon from "../../public/images/file.actions.locked.react.svg"; + +export const EncryptedFileIcon = styled.div` + background: url("images/security.svg") no-repeat 0 0 / 16px 16px transparent; + height: 16px; + position: absolute; + width: 16px; + margin-top: 14px; + margin-left: ${(props) => (props.isEdit ? "40px" : "12px")}; +`; + +export const StyledFavoriteIcon = styled(FavoriteIcon)` + ${commonIconsStyles} +`; + +export const StyledFileActionsConvertEditDocIcon = styled( + FileActionsConvertEditDocIcon +)` + ${commonIconsStyles} + path { + fill: #3b72a7; + } +`; + +export const StyledFileActionsLockedIcon = styled(FileActionsLockedIcon)` + ${commonIconsStyles} + path { + fill: #3b72a7; + } +`; diff --git a/products/ASC.Files/Client/src/components/dialogs/DeleteDialog/index.js b/products/ASC.Files/Client/src/components/dialogs/DeleteDialog/index.js index 82aefd1db3..9a507b6182 100644 --- a/products/ASC.Files/Client/src/components/dialogs/DeleteDialog/index.js +++ b/products/ASC.Files/Client/src/components/dialogs/DeleteDialog/index.js @@ -85,16 +85,23 @@ class DeleteDialogComponent extends React.Component { const checkedSelections = selection.filter((x) => x.checked === true); - const questionMessage = + const title = checkedSelections.length === 1 ? checkedSelections[0].fileExst - ? t("QuestionDeleteFile") - : t("QuestionDeleteFolder") - : t("QuestionDeleteElements"); + ? t("MoveToTrashOneFileTitle") + : t("MoveToTrashOneFolderTitle") + : t("MoveToTrashItemsTitle"); + + const noteText = + checkedSelections.length === 1 + ? checkedSelections[0].fileExst + ? t("MoveToTrashOneFileNote") + : t("MoveToTrashOneFolderNote") + : t("MoveToTrashItemsNote"); const accuracy = 20; let filesHeight = 25 * filesList.length + accuracy + 8; - let foldersHeight = 25 * foldersList.length + accuracy; + let foldersHeight = 25 * foldersList.length + accuracy + 8; if (foldersList.length === 0) { foldersHeight = 0; } @@ -107,15 +114,15 @@ class DeleteDialogComponent extends React.Component { return ( - {t("ConfirmationTitle")} + {title}
- - {questionMessage} - + {noteText} {foldersList.length > 0 && ( - {t("FoldersModule")}: + + {t("FoldersModule")}: + )} {foldersList.map((item, index) => ( { return ( - {t("ConfirmationTitle")} + {t("DeleteForeverTitle")} - {t("EmptyTrashDialogQuestion")} - {t("EmptyTrashDialogMessage")} + {t("DeleteForeverNote")}