Merge branch 'develop' into feature/oauth2-client

This commit is contained in:
Timofey Boyko 2023-10-02 10:05:58 +03:00
commit ad047bc372
259 changed files with 25170 additions and 16934 deletions

View File

@ -7,7 +7,7 @@ BUILD_PATH=${BUILD_PATH:-${SRC_PATH}/publish}
BUILD_DOTNET_CORE_ARGS=${BUILD_DOTNET_CORE_ARGS:-"false"}
PROPERTY_BUILD=${PROPERTY_BUILD:-"all"}
BACKEND_NODEJS_SERVICES=${BACKEND_NODEJS_SERVICES:-"ASC.Socket.IO, ASC.SsoAuth"}
BACKEND_NODEJS_SERVICES=${BACKEND_NODEJS_SERVICES:-"ASC.Socket.IO, ASC.SsoAuth, ASC.TelegramReports"}
BACKEND_DOTNETCORE_SERVICES=${BACKEND_DOTNETCORE_SERVICES:-"ASC.Files, ASC.People, ASC.Data.Backup, ASC.Files.Service, ASC.Notify, \
ASC.Studio.Notify, ASC.Web.Api, ASC.Web.Studio, ASC.Data.Backup.BackgroundTasks, ASC.ClearEvents, ASC.ApiSystem, ASC.Web.HealthChecks.UI"}
SELF_CONTAINED=${SELF_CONTAINED:-"false"}
@ -179,6 +179,12 @@ function backend-nodejs-publish {
yarn install --cwd ${SRC_PATH}/common/${ARRAY_NAME_SERVICES[$i]} --frozen-lockfile && \
mkdir -p ${BUILD_PATH}/services/${ARRAY_NAME_SERVICES[$i]}/service/ && \
cp -rfv ${SRC_PATH}/common/${ARRAY_NAME_SERVICES[$i]}/* ${BUILD_PATH}/services/${ARRAY_NAME_SERVICES[$i]}/service/
if [[ ${ARRAY_NAME_SERVICES[$i]} == "ASC.TelegramReports" ]]
then
# build before run
yarn --cwd ${BUILD_PATH}/services/ASC.TelegramReports/service/ build
fi
if [[ ${DOCKER_ENTRYPOINT} != "false" ]]
then
echo "== ADD ${DOCKER_ENTRYPOINT} to ${ARRAY_NAME_SERVICES[$i]} =="

View File

@ -101,6 +101,7 @@ done
services_name_backend_nodejs=()
services_name_backend_nodejs+=(ASC.Socket.IO)
services_name_backend_nodejs+=(ASC.SsoAuth)
services_name_backend_nodejs+=(ASC.TelegramReports)
# Publish backend services (Nodejs)
for i in ${!services_name_backend_nodejs[@]}; do

View File

@ -114,6 +114,7 @@
API_HOST=${CONTAINER_PREFIX}api
STUDIO_HOST=${CONTAINER_PREFIX}studio
SSOAUTH_HOST=${CONTAINER_PREFIX}ssoauth
TELEGRAMREPORTS_HOST=${CONTAINER_PREFIX}telegramreports
MIGRATION_RUNNER_HOST=${CONTAINER_PREFIX}migration-runner
PROXY_HOST=${CONTAINER_PREFIX}proxy
ROUTER_HOST=${CONTAINER_PREFIX}router
@ -138,6 +139,7 @@
SERVICE_API=${API_HOST}:${SERVICE_PORT}
SERVICE_STUDIO=${STUDIO_HOST}:${SERVICE_PORT}
SERVICE_SSOAUTH=${SSOAUTH_HOST}:${SERVICE_PORT}
SERVICE_TELEGRAMREPORTS=${TELEGRAMREPORTS_HOST}:${SERVICE_PORT}
SERVICE_DOCEDITOR=${DOCEDITOR_HOST}:5013
SERVICE_LOGIN=${LOGIN_HOST}:5011
SERVICE_HELTHCHECKS=${HELTHCHECKS_HOST}:${SERVICE_PORT}

View File

@ -269,6 +269,7 @@ services:
- SERVICE_API_SYSTEM=${SERVICE_API_SYSTEM}
- SERVICE_STUDIO=${SERVICE_STUDIO}
- SERVICE_SSOAUTH=${SERVICE_SSOAUTH}
- SERVICE_TELEGRAMREPORTS=${SERVICE_TELEGRAMREPORTS}
- SERVICE_DOCEDITOR=${SERVICE_DOCEDITOR}
- SERVICE_LOGIN=${SERVICE_LOGIN}
- SERVICE_HELTHCHECKS=${SERVICE_HELTHCHECKS}

View File

@ -1,10 +0,0 @@
<service>
<id>OnlyofficeWebPlugins</id>
<name>ONLYOFFICE WebPlugins Server</name>
<startmode>manual</startmode>
<executable>node</executable>
<arguments>../../common/ASC.WebPlugins/dist/src/main.js</arguments>
<log mode="none"/>
<delayedAutoStart>true</delayedAutoStart>
<onfailure action="restart" delay="5 sec" />
</service>

View File

@ -1,9 +0,0 @@
PUSHD %~dp0..
cd %~dp0../../common/ASC.WebPlugins/
call yarn install --immutable
call yarn build
POPD

View File

@ -215,7 +215,8 @@ public abstract class BaseStartup
.AddBaseDbContextPool<IntegrationEventLogContext>()
.AddBaseDbContextPool<FeedDbContext>()
.AddBaseDbContextPool<MessagesContext>()
.AddBaseDbContextPool<WebhooksDbContext>();
.AddBaseDbContextPool<WebhooksDbContext>()
.AddBaseDbContextPool<WebPluginDbContext>();
if (AddAndUseSession)
{

View File

@ -59,6 +59,7 @@ public class BaseWorkerStartup
services.AddBaseDbContextPool<FeedDbContext>();
services.AddBaseDbContextPool<MessagesContext>();
services.AddBaseDbContextPool<WebhooksDbContext>();
services.AddBaseDbContextPool<WebPluginDbContext>();
services.RegisterFeature();

View File

@ -0,0 +1,131 @@
// (c) Copyright Ascensio System SIA 2010-2022
//
// This program is a free software product.
// You can redistribute it and/or modify it under the terms
// of the GNU Affero General Public License (AGPL) version 3 as published by the Free Software
// Foundation. In accordance with Section 7(a) of the GNU AGPL 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 details, see
// the GNU AGPL at: http://www.gnu.org/licenses/agpl-3.0.html
//
// You can contact Ascensio System SIA at Lubanas st. 125a-25, Riga, Latvia, EU, LV-1021.
//
// The interactive user interfaces in modified source and object code versions of the Program must
// display Appropriate Legal Notices, as required under Section 5 of the GNU AGPL version 3.
//
// Pursuant to Section 7(b) of the License you must retain the original Product logo when
// distributing the program. Pursuant to Section 7(e) we decline to grant you any rights under
// trademark law for use of our trademarks.
//
// All the Product's GUI elements, including illustrations and icon sets, as well as technical writing
// content are licensed under the terms of the Creative Commons Attribution-ShareAlike 4.0
// International. See the License terms at http://creativecommons.org/licenses/by-sa/4.0/legalcode
namespace ASC.Core.Data;
[Scope]
public class DbWebPluginService
{
private readonly IDbContextFactory<WebPluginDbContext> _dbContextFactory;
public DbWebPluginService(IDbContextFactory<WebPluginDbContext> dbContextFactory)
{
_dbContextFactory = dbContextFactory;
}
public async Task<DbWebPlugin> SaveAsync(DbWebPlugin webPlugin)
{
ArgumentNullException.ThrowIfNull(webPlugin);
await using var dbContext = _dbContextFactory.CreateDbContext();
await dbContext.AddOrUpdateAsync(q => q.WebPlugins, webPlugin);
await dbContext.SaveChangesAsync();
return webPlugin;
}
public async Task<DbWebPlugin> GetByIdAsync(int tenantId, int id)
{
await using var dbContext = _dbContextFactory.CreateDbContext();
return await Queries.WebPluginByIdAsync(dbContext, tenantId, id);
}
public async Task<DbWebPlugin> GetByNameAsync(int tenantId, string name)
{
await using var dbContext = _dbContextFactory.CreateDbContext();
return await Queries.WebPluginByNameAsync(dbContext, tenantId, name);
}
public async Task<List<DbWebPlugin>> GetAsync(int tenantId)
{
await using var dbContext = _dbContextFactory.CreateDbContext();
return await Queries.WebPluginsAsync(dbContext, tenantId).ToListAsync();
}
public async Task UpdateAsync(int tenantId, int id, bool enabled)
{
using var dbContext = _dbContextFactory.CreateDbContext();
await Queries.UpdateWebPluginStatusAsync(dbContext, tenantId, id, enabled);
}
public async Task DeleteAsync(int tenantId, int id)
{
await using var dbContext = _dbContextFactory.CreateDbContext();
await Queries.DeleteWebPluginByIdAsync(dbContext, tenantId, id);
}
}
static file class Queries
{
public static readonly Func<WebPluginDbContext, int, IAsyncEnumerable<DbWebPlugin>>
WebPluginsAsync = EF.CompileAsyncQuery(
(WebPluginDbContext ctx, int tenantId) =>
ctx.WebPlugins
.AsNoTracking()
.Where(r => r.TenantId == tenantId));
public static readonly Func<WebPluginDbContext, int, int, Task<DbWebPlugin>>
WebPluginByIdAsync = EF.CompileAsyncQuery(
(WebPluginDbContext ctx, int tenantId, int id) =>
ctx.WebPlugins
.AsNoTracking()
.Where(r => r.TenantId == tenantId)
.Where(r => r.Id == id)
.FirstOrDefault());
public static readonly Func<WebPluginDbContext, int, string, Task<DbWebPlugin>>
WebPluginByNameAsync = EF.CompileAsyncQuery(
(WebPluginDbContext ctx, int tenantId, string name) =>
ctx.WebPlugins
.AsNoTracking()
.Where(r => r.TenantId == tenantId)
.Where(r => r.Name == name)
.FirstOrDefault());
public static readonly Func<WebPluginDbContext, int, int, bool, Task<int>>
UpdateWebPluginStatusAsync = EF.CompileAsyncQuery(
(WebPluginDbContext ctx, int tenantId, int id, bool enabled) =>
ctx.WebPlugins
.Where(r => r.TenantId == tenantId)
.Where(r => r.Id == id)
.ExecuteUpdate(toUpdate => toUpdate
.SetProperty(p => p.Enabled, enabled)
));
public static readonly Func<WebPluginDbContext, int, int, Task<int>>
DeleteWebPluginByIdAsync = EF.CompileAsyncQuery(
(WebPluginDbContext ctx, int tenantId, int id) =>
ctx.WebPlugins
.Where(r => r.TenantId == tenantId)
.Where(r => r.Id == id)
.ExecuteDelete());
}

View File

@ -0,0 +1,45 @@
// (c) Copyright Ascensio System SIA 2010-2022
//
// This program is a free software product.
// You can redistribute it and/or modify it under the terms
// of the GNU Affero General Public License (AGPL) version 3 as published by the Free Software
// Foundation. In accordance with Section 7(a) of the GNU AGPL 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 details, see
// the GNU AGPL at: http://www.gnu.org/licenses/agpl-3.0.html
//
// You can contact Ascensio System SIA at Lubanas st. 125a-25, Riga, Latvia, EU, LV-1021.
//
// The interactive user interfaces in modified source and object code versions of the Program must
// display Appropriate Legal Notices, as required under Section 5 of the GNU AGPL version 3.
//
// Pursuant to Section 7(b) of the License you must retain the original Product logo when
// distributing the program. Pursuant to Section 7(e) we decline to grant you any rights under
// trademark law for use of our trademarks.
//
// All the Product's GUI elements, including illustrations and icon sets, as well as technical writing
// content are licensed under the terms of the Creative Commons Attribution-ShareAlike 4.0
// International. See the License terms at http://creativecommons.org/licenses/by-sa/4.0/legalcode
namespace ASC.Core.Common.EF.Context;
public class WebPluginDbContext : DbContext
{
public DbSet<DbWebPlugin> WebPlugins { get; set; }
public WebPluginDbContext(DbContextOptions<WebPluginDbContext> options) : base(options)
{
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
ModelBuilderWrapper
.From(modelBuilder, Database)
.AddDbWebPlugins()
.AddDbTenant();
}
}

View File

@ -0,0 +1,212 @@
// (c) Copyright Ascensio System SIA 2010-2022
//
// This program is a free software product.
// You can redistribute it and/or modify it under the terms
// of the GNU Affero General Public License (AGPL) version 3 as published by the Free Software
// Foundation. In accordance with Section 7(a) of the GNU AGPL 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 details, see
// the GNU AGPL at: http://www.gnu.org/licenses/agpl-3.0.html
//
// You can contact Ascensio System SIA at Lubanas st. 125a-25, Riga, Latvia, EU, LV-1021.
//
// The interactive user interfaces in modified source and object code versions of the Program must
// display Appropriate Legal Notices, as required under Section 5 of the GNU AGPL version 3.
//
// Pursuant to Section 7(b) of the License you must retain the original Product logo when
// distributing the program. Pursuant to Section 7(e) we decline to grant you any rights under
// trademark law for use of our trademarks.
//
// All the Product's GUI elements, including illustrations and icon sets, as well as technical writing
// content are licensed under the terms of the Creative Commons Attribution-ShareAlike 4.0
// International. See the License terms at http://creativecommons.org/licenses/by-sa/4.0/legalcode
namespace ASC.Core.Common.EF.Model;
public class DbWebPlugin : BaseEntity
{
public int Id { get; set; }
public int TenantId { get; set; }
public string Name { get; set; }
public string Version { get; set; }
public string Description { get; set; }
public string License { get; set; }
public string Author { get; set; }
public string HomePage { get; set; }
public string PluginName { get; set; }
public string Scopes { get; set; }
public string Image { get; set; }
public Guid CreateBy { get; set; }
public DateTime CreateOn { get; set; }
public bool Enabled { get; set; }
public bool System { get; set; }
public DbTenant Tenant { get; set; }
public override object[] GetKeys()
{
return new object[] { Id };
}
}
public static class WebPluginExtension
{
public static ModelBuilderWrapper AddDbWebPlugins(this ModelBuilderWrapper modelBuilder)
{
modelBuilder.Entity<DbWebPlugin>().Navigation(e => e.Tenant).AutoInclude(false);
modelBuilder
.Add(MySqlAddDbWebPlugins, Provider.MySql)
.Add(PgSqlAddDbWebPlugins, Provider.PostgreSql);
return modelBuilder;
}
public static void MySqlAddDbWebPlugins(this ModelBuilder modelBuilder)
{
modelBuilder.Entity<DbWebPlugin>(entity =>
{
entity.HasKey(e => new { e.Id })
.HasName("PRIMARY");
entity.ToTable("webplugins")
.HasCharSet("utf8mb4");
entity.HasIndex(e => e.TenantId)
.HasDatabaseName("tenant");
entity.Property(e => e.TenantId)
.HasColumnName("tenant_id");
entity.Property(e => e.Name)
.IsRequired()
.HasColumnName("name")
.HasColumnType("varchar(255)");
entity.Property(e => e.Version)
.HasColumnName("version")
.HasColumnType("varchar(255)");
entity.Property(e => e.Description)
.HasColumnName("description")
.HasColumnType("text");
entity.Property(e => e.License)
.HasColumnName("license")
.HasColumnType("varchar(255)");
entity.Property(e => e.Author)
.HasColumnName("author")
.HasColumnType("varchar(255)");
entity.Property(e => e.HomePage)
.HasColumnName("home_page")
.HasColumnType("varchar(255)");
entity.Property(e => e.PluginName)
.IsRequired()
.HasColumnName("plugin_name")
.HasColumnType("varchar(255)");
entity.Property(e => e.Scopes)
.HasColumnName("scopes")
.HasColumnType("text");
entity.Property(e => e.Image)
.HasColumnName("image")
.HasColumnType("varchar(255)");
entity.Property(e => e.CreateBy)
.IsRequired()
.HasColumnName("create_by")
.HasColumnType("char(36)");
entity.Property(e => e.CreateOn)
.HasColumnName("create_on")
.HasColumnType("datetime");
entity.Property(e => e.Enabled)
.HasColumnName("enabled")
.HasColumnType("tinyint(1)")
.HasDefaultValueSql("'0'");
entity.Property(e => e.System)
.HasColumnName("system")
.HasColumnType("tinyint(1)")
.HasDefaultValueSql("'0'");
});
}
public static void PgSqlAddDbWebPlugins(this ModelBuilder modelBuilder)
{
modelBuilder.Entity<DbWebPlugin>(entity =>
{
entity.HasKey(e => new { e.Id })
.HasName("webplugins_pkey");
entity.ToTable("webplugins", "onlyoffice");
entity.HasIndex(e => e.TenantId)
.HasDatabaseName("tenant_webplugins");
entity.Property(e => e.TenantId)
.HasColumnName("tenant_id");
entity.Property(e => e.Name)
.IsRequired()
.HasColumnName("name")
.HasMaxLength(255);
entity.Property(e => e.Version)
.HasColumnName("version")
.HasMaxLength(255);
entity.Property(e => e.Description)
.HasColumnName("description");
entity.Property(e => e.License)
.HasColumnName("license")
.HasMaxLength(255);
entity.Property(e => e.Author)
.HasColumnName("author")
.HasMaxLength(255);
entity.Property(e => e.HomePage)
.HasColumnName("home_page")
.HasMaxLength(255);
entity.Property(e => e.PluginName)
.IsRequired()
.HasColumnName("plugin_name")
.HasMaxLength(255);
entity.Property(e => e.Scopes)
.HasColumnName("scopes");
entity.Property(e => e.Image)
.HasColumnName("image")
.HasMaxLength(255);
entity.Property(e => e.CreateBy)
.IsRequired()
.HasColumnName("create_by")
.HasMaxLength(36)
.IsFixedLength();
entity.Property(e => e.CreateOn)
.HasColumnName("create_on");
entity.Property(e => e.Enabled)
.HasColumnName("enabled");
entity.Property(e => e.System)
.HasColumnName("system");
});
}
}

View File

@ -0,0 +1,31 @@
module.exports = {
parser: "@typescript-eslint/parser",
parserOptions: {
project: "tsconfig.json",
tsconfigRootDir: __dirname,
sourceType: "module",
},
plugins: ["@typescript-eslint/eslint-plugin"],
extends: [
"plugin:@typescript-eslint/recommended",
"plugin:prettier/recommended",
],
root: true,
env: {
node: true,
jest: true,
},
ignorePatterns: [".eslintrc.js"],
"prettier/prettier": [
"error",
{
endOfLine: "auto",
},
],
rules: {
"@typescript-eslint/interface-name-prefix": "off",
"@typescript-eslint/explicit-function-return-type": "off",
"@typescript-eslint/explicit-module-boundary-types": "off",
"@typescript-eslint/no-explicit-any": "off",
},
};

View File

View File

@ -0,0 +1,14 @@
# ASC Telegram Reports
ASC.TelegramReports
### Installation and usage
```bash
$ npm install
$ npm start
```
### License
[GNU GPL V3](LICENSE)

View File

@ -1,6 +1,6 @@
{
"app": {
"port": 5014,
"port": 5016,
"appsettings": "../../../../config",
"environment": "Development"
}

View File

@ -0,0 +1,26 @@
import * as nconf from "nconf";
import * as path from "path";
import * as fs from "fs";
import * as conf from "./config.json";
nconf.argv().env().file("config", path.join(__dirname, "config.json"));
getAndSaveAppsettings();
export default nconf;
function getAndSaveAppsettings() {
var appsettings = nconf.get("app").appsettings;
if (!path.isAbsolute(appsettings)) {
appsettings = path.join(__dirname, appsettings);
}
var env = nconf.get("app").environment;
console.log('environment: ' + env);
nconf.file("appsettingsWithEnv", path.join(appsettings, 'appsettings.' + env + '.json'));
nconf.file("appsettings", path.join(appsettings, 'appsettings.json'));
nconf.file("telegramConf", path.join(appsettings, "telegram.json"));
}

View File

@ -0,0 +1,31 @@
import firebase from "firebase/compat/app";
import "firebase/compat/database";
import { NestFactory } from "@nestjs/core";
import { AppModule } from "./src/app/app.module";
import { AppService } from "./src/app/app.service";
import * as config from "./config";
const winston = require("./src/log.js");
const firebaseConfig = config.default.get("firebase");
firebase.initializeApp(firebaseConfig);
async function bootstrap() {
try {
const app = await NestFactory.create(AppModule);
const appService = app.get(AppService);
winston.info(`Start TelegramReports Service listening`);
const ref = firebase.database().ref("reports").limitToLast(1);
ref.on("child_added", (data) => {
appService.sendMessage(data.val())
});
} catch (e) {
winston.error(e);
}
}
bootstrap();

View File

@ -2,5 +2,8 @@
"$schema": "https://json.schemastore.org/nest-cli",
"collection": "@nestjs/schematics",
"sourceRoot": "src",
"entryFile": "src/main"
"entryFile": "index.js",
"compilerOptions": {
"deleteOutDir": true
}
}

View File

@ -1,7 +1,7 @@
{
"name": "plugin-system",
"name": "telegram-reports",
"version": "1.0.0",
"description": "Server for docspace-plugins",
"description": "Server for telegram-reports",
"private": true,
"scripts": {
"prebuild": "rimraf dist",
@ -9,7 +9,7 @@
"start": "rimraf dist & nest start",
"start:dev": "rimraf dist & nest start --watch",
"start:debug": "nest start --debug --watch",
"start:prod": "node dist/main",
"start:prod": "node dist/index",
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix"
},
"dependencies": {
@ -19,11 +19,13 @@
"@nestjs/platform-express": "^9.0.0",
"@nestjs/typeorm": "^9.0.0",
"date-and-time": "^2.4.1",
"firebase": "^10.4.0",
"mysql2": "^2.3.3",
"nconf": "^0.12.0",
"reflect-metadata": "^0.1.13",
"rimraf": "^3.0.2",
"rxjs": "^7.2.0",
"telegraf": "^4.13.1",
"typeorm": "^0.3.7",
"winston": "^3.8.2",
"winston-cloudwatch": "^6.1.1",

View File

@ -0,0 +1,9 @@
import { Module } from "@nestjs/common";
import { AppService } from "./app.service";
@Module({
imports: [],
controllers: [],
providers: [AppService],
})
export class AppModule { }

View File

@ -0,0 +1,43 @@
import { Injectable } from "@nestjs/common";
import { Telegraf } from "telegraf";
import * as config from "../../config";
const winston = require("../log.js");
const { botKey, chatId } = config.default.get("telegramConf");
const MAX_LENGTH = 4096; //TG Limit
@Injectable()
export class AppService {
bot = new Telegraf(botKey);
chunkMessage = (str: string, size: number): Array<string> =>
Array.from({ length: Math.ceil(str.length / size) }, (_, i) =>
str.slice(i * size, i * size + size),
);
async sendMessage(report): Promise<string> {
if (!botKey) throw new Error("Empty bot key");
if (!chatId) throw new Error("Empty chat ID");
const message = "New bug report:\n" + JSON.stringify(report);
try {
if (message.length > MAX_LENGTH) {
for (const part of this.chunkMessage(message, MAX_LENGTH)) {
await this.bot.telegram.sendMessage(chatId, part);
}
} else {
await this.bot.telegram.sendMessage(chatId, message);
}
winston.info(`Report sent successfully`, message);
return "Report sent successfully"
} catch (e) {
winston.error(e);
return e;
}
}
}

View File

@ -0,0 +1,103 @@
import * as winston from "winston";
import * as WinstonCloudWatch from "winston-cloudwatch";
import * as date from "date-and-time";
import * as os from "os";
import * as config from "../config";
import { randomUUID } from "crypto";
import "winston-daily-rotate-file";
import * as path from "path";
import * as fs from "fs";
let logpath = process.env.logpath || null;
if (logpath != null) {
if (!path.isAbsolute(logpath)) {
logpath = path.join(__dirname, "..", logpath);
}
}
const fileName = logpath
? path.join(logpath, "telegramreports.%DATE%.log")
: path.join(__dirname, "..", "..", "..", "..", "Logs", "telegramreports.%DATE%.log");
const dirName = path.dirname(fileName);
if (!fs.existsSync(dirName)) {
fs.mkdirSync(dirName);
}
const aws = config.default.get("aws").cloudWatch;
const accessKeyId = aws.accessKeyId;
const secretAccessKey = aws.secretAccessKey;
const awsRegion = aws.region;
const logGroupName = aws.logGroupName;
const logStreamName = aws.logStreamName.replace("${hostname}", os.hostname())
.replace("${applicationContext}", "TelegramReports")
.replace("${guid}", randomUUID())
.replace("${date}", date.format(new Date(), 'YYYY/MM/DDTHH.mm.ss'));
const options = {
file: {
filename: fileName,
datePattern: "MM-DD",
handleExceptions: true,
humanReadableUnhandledException: true,
zippedArchive: true,
maxSize: "50m",
maxFiles: "30d",
json: true,
},
console: {
level: "debug",
handleExceptions: true,
json: false,
colorize: true,
},
cloudWatch: {
name: 'aws',
level: "debug",
logStreamName: logStreamName,
logGroupName: logGroupName,
awsRegion: awsRegion,
jsonMessage: true,
awsOptions: {
credentials: {
accessKeyId: accessKeyId,
secretAccessKey: secretAccessKey
}
}
}
};
const transports: winston.transport[] = [
new winston.transports.Console(options.console),
new winston.transports.DailyRotateFile(options.file)
];
if (aws != null && aws.accessKeyId !== '') {
transports.push(new WinstonCloudWatch(options.cloudWatch));
}
const customFormat = winston.format(info => {
const now = new Date();
info.date = date.format(now, 'YYYY-MM-DD HH:mm:ss');
info.applicationContext = "TelegramReports";
info.level = info.level.toUpperCase();
const hostname = os.hostname();
info["instance-id"] = hostname;
return info;
})();
module.exports = winston.createLogger({
format: winston.format.combine(
customFormat,
winston.format.json()
),
transports: transports,
exitOnError: false,
});

File diff suppressed because it is too large Load Diff

View File

@ -1,31 +0,0 @@
module.exports = {
parser: '@typescript-eslint/parser',
parserOptions: {
project: 'tsconfig.json',
tsconfigRootDir : __dirname,
sourceType: 'module',
},
plugins: ['@typescript-eslint/eslint-plugin'],
extends: [
'plugin:@typescript-eslint/recommended',
'plugin:prettier/recommended',
],
root: true,
env: {
node: true,
jest: true,
},
ignorePatterns: ['.eslintrc.js'],
'prettier/prettier': [
'error',
{
'endOfLine': 'auto',
}
],
rules: {
'@typescript-eslint/interface-name-prefix': 'off',
'@typescript-eslint/explicit-function-return-type': 'off',
'@typescript-eslint/explicit-module-boundary-types': 'off',
'@typescript-eslint/no-explicit-any': 'off',
},
};

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,26 +0,0 @@
import * as nconf from "nconf";
import * as path from "path";
import * as fs from "fs";
import * as conf from "./config.json";
nconf.argv().env().file("config", path.join(__dirname, "config.json"));
getAndSaveAppsettings();
export default nconf;
function getAndSaveAppsettings() {
var appsettings = nconf.get("app").appsettings;
if (!path.isAbsolute(appsettings)) {
appsettings = path.join(__dirname, appsettings);
}
var env = nconf.get("app").environment;
console.log('environment: ' + env);
nconf.file("appsettingsWithEnv", path.join(appsettings, 'appsettings.' + env + '.json'));
nconf.file("appsettings", path.join(appsettings, 'appsettings.json'));
nconf.file("pluginsConf", path.join(appsettings, "plugins.json"));
}

View File

@ -1,7 +0,0 @@
CREATE TABLE IF NOT EXISTS `plugin` (
`id` varchar(200) NOT NULL,
`name` text NOT NULL,
`filename` text NOT NULL,
`isActive` int(11) NOT NULL DEFAULT '1',
PRIMARY KEY (`id`)
) DEFAULT CHARSET=utf8;

View File

@ -1,23 +0,0 @@
import { Module } from "@nestjs/common";
import { TypeOrmModule } from "@nestjs/typeorm";
import { PluginsModule } from "./plugins/plugins.module";
import { Plugin } from "./entities/plugin.entity";
@Module({
imports: [
TypeOrmModule.forRoot({
type: "mysql",
host: "localhost",
port: 3306,
username: "root",
password: "root",
database: "onlyoffice",
entities: [Plugin],
synchronize: true,
}),
PluginsModule,
],
})
export class AppModule {}

View File

@ -1,16 +0,0 @@
import { Entity, Column, PrimaryGeneratedColumn } from "typeorm";
@Entity()
export class Plugin {
@PrimaryGeneratedColumn()
id: number;
@Column()
name: string;
@Column()
filename: string;
@Column({ default: true })
isActive: boolean;
}

View File

@ -1,33 +0,0 @@
import { Injectable, CanActivate, ExecutionContext } from "@nestjs/common";
import { Observable } from "rxjs";
import * as config from "../../config";
const { enabled, allow } = config.default.get("plugins");
@Injectable()
export class PluginGuard implements CanActivate {
canActivate(
context: ExecutionContext
): boolean | Promise<boolean> | Observable<boolean> {
return enabled === "true";
}
}
@Injectable()
export class PluginUploadGuard implements CanActivate {
canActivate(
context: ExecutionContext
): boolean | Promise<boolean> | Observable<boolean> {
return enabled === "true" && allow.includes("upload");
}
}
@Injectable()
export class PluginDeleteGuard implements CanActivate {
canActivate(
context: ExecutionContext
): boolean | Promise<boolean> | Observable<boolean> {
return enabled === "true" && allow.includes("delete");
}
}

View File

@ -1,104 +0,0 @@
import * as winston from "winston";
import * as WinstonCloudWatch from "winston-cloudwatch";
import * as date from "date-and-time";
import * as os from "os";
import * as config from "../config";
import { randomUUID } from "crypto";
import "winston-daily-rotate-file";
import * as path from "path";
import * as fs from "fs";
let logpath = process.env.logpath || null;
if (logpath != null) {
if (!path.isAbsolute(logpath)) {
logpath = path.join(__dirname, "..", logpath);
}
}
const fileName = logpath
? path.join(logpath, "plugins.%DATE%.log")
: path.join(__dirname, "..", "..", "..", "..", "Logs", "plugins.%DATE%.log");
const dirName = path.dirname(fileName);
if (!fs.existsSync(dirName)) {
fs.mkdirSync(dirName);
}
const aws = config.default.get("aws").cloudWatch;
const accessKeyId = aws.accessKeyId;
const secretAccessKey = aws.secretAccessKey;
const awsRegion = aws.region;
const logGroupName = aws.logGroupName;
const logStreamName = aws.logStreamName.replace("${hostname}", os.hostname())
.replace("${applicationContext}", "WebPlugins")
.replace("${guid}", randomUUID())
.replace("${date}", date.format(new Date(), 'YYYY/MM/DDTHH.mm.ss'));
const options = {
file: {
filename: fileName,
datePattern: "MM-DD",
handleExceptions: true,
humanReadableUnhandledException: true,
zippedArchive: true,
maxSize: "50m",
maxFiles: "30d",
json: true,
},
console: {
level: "debug",
handleExceptions: true,
json: false,
colorize: true,
},
cloudWatch: {
name: 'aws',
level: "debug",
logStreamName: logStreamName,
logGroupName: logGroupName,
awsRegion: awsRegion,
jsonMessage: true,
awsOptions: {
credentials: {
accessKeyId: accessKeyId,
secretAccessKey: secretAccessKey
}
}
}
};
const transports: winston.transport[] = [
new winston.transports.Console(options.console),
new winston.transports.DailyRotateFile(options.file)
];
if (aws != null && aws.accessKeyId !== '')
{
transports.push(new WinstonCloudWatch(options.cloudWatch));
}
const customFormat = winston.format(info => {
const now = new Date();
info.date = date.format(now, 'YYYY-MM-DD HH:mm:ss');
info.applicationContext = "WebPlugins";
info.level = info.level.toUpperCase();
const hostname = os.hostname();
info["instance-id"] = hostname;
return info;
})();
module.exports = winston.createLogger({
format: winston.format.combine(
customFormat,
winston.format.json()
),
transports: transports,
exitOnError: false,
});

View File

@ -1,21 +0,0 @@
import { NestFactory } from "@nestjs/core";
import { AppModule } from "./app.module";
import * as config from "../config";
const winston = require("./log.js");
const port = config.default.get("app").port || 5014;
async function bootstrap() {
try {
const app = await NestFactory.create(AppModule, { cors: false });
// app.enableCors();
await app.listen(port, () => {
winston.info(`Start WebPlugins Service listening on port ${port} for http`);
});
} catch (e) {
winston.error(e);
}
}
bootstrap();

View File

@ -1,85 +0,0 @@
import {
Controller,
Param,
Body,
Get,
Post,
UploadedFiles,
UseInterceptors,
Put,
Delete,
UseGuards,
} from "@nestjs/common";
import { AnyFilesInterceptor } from "@nestjs/platform-express";
import { storage } from "src/utils";
import { Plugin } from "src/entities/plugin.entity";
import {
PluginGuard,
PluginUploadGuard,
PluginDeleteGuard,
} from "src/guards/plugin.guard";
import { PluginsService } from "./plugins.service";
import fileFilter from "src/utils/file-filter";
@Controller("/api/2.0/plugins")
@UseGuards(PluginGuard)
export class PluginsController {
constructor(private pluginsService: PluginsService) { }
@Get()
async findAll(): Promise<{ response: Plugin[] }> {
const plugins: Plugin[] = await this.pluginsService.findAll();
return { response: plugins };
}
@Put("activate/:id")
async activate(@Param("id") id: number): Promise<{ response: Plugin }> {
const plugin: Plugin = await this.pluginsService.activate(id);
return { response: plugin };
}
@Post("upload")
@UseGuards(PluginUploadGuard)
@UseInterceptors(
AnyFilesInterceptor({
storage: storage,
fileFilter: fileFilter,
})
)
async upload(
@UploadedFiles() files: Express.Multer.File[]
): Promise<{ response: Plugin | { error: string } }> {
try {
if (files[0]) {
const plugin = await this.pluginsService.upload(
files[0].originalname,
files[0].filename
);
return { response: plugin };
} else {
return {
response: { error: "Invalid file format or file already exists" },
};
}
} catch (e) {
console.log(e);
return {
response: { error: "Invalid file format or file already exists" },
};
}
}
@Delete("delete/:id")
@UseGuards(PluginDeleteGuard)
async delete(@Param("id") id: number) {
await this.pluginsService.delete(id);
}
}
export default PluginsController;

View File

@ -1,14 +0,0 @@
import { Module } from "@nestjs/common";
import { TypeOrmModule } from "@nestjs/typeorm";
import { PluginsController } from "./plugins.controller";
import { PluginsService } from "./plugins.service";
import { Plugin } from "src/entities/plugin.entity";
@Module({
imports: [TypeOrmModule.forFeature([Plugin])],
controllers: [PluginsController],
providers: [PluginsService],
})
export class PluginsModule { }

View File

@ -1,100 +0,0 @@
import { Injectable } from "@nestjs/common";
import { InjectRepository } from "@nestjs/typeorm";
import { Repository } from "typeorm";
import * as path from "path";
import * as fs from "fs";
import * as config from "../../config";
const { path: pathToPlugins } = config.default.get("pluginsConf");
import { Plugin } from "src/entities/plugin.entity";
@Injectable()
export class PluginsService {
constructor(
@InjectRepository(Plugin)
private pluginsRepository: Repository<Plugin>
) { }
findAll(): Promise<Plugin[]> {
return this.pluginsRepository.find();
}
findOne(id: number): Promise<Plugin> {
return this.pluginsRepository.findOneBy({ id });
}
async remove(id: string): Promise<void> {
await this.pluginsRepository.delete(id);
}
async activate(id: number): Promise<Plugin> {
const plugin: Plugin = await this.pluginsRepository.findOneBy({ id });
plugin.isActive = !plugin.isActive;
await this.pluginsRepository.save(plugin);
return plugin;
}
async upload(
originalname: string,
filename: string
): Promise<Plugin | { error: string }> {
const plugin: Plugin = new Plugin();
const dir = path.join(__dirname, pathToPlugins, `${filename}`);
const name = originalname.split(".")[0];
const contents = fs.readFileSync(dir, "utf8");
let isPlugin = true;
isPlugin = contents.includes(`"dependencies":{"docspace-plugin"`);
const splitName = name.split("");
splitName[0].toUpperCase();
isPlugin = contents.includes(`window.Plugins.${splitName.join("")}`);
isPlugin = contents.includes(".prototype.getPluginName=function()");
isPlugin = contents.includes(".prototype.getPluginVersion=function()");
isPlugin = contents.includes(".prototype.activate=function()");
isPlugin = contents.includes(".prototype.deactivate=function()");
plugin.name = name;
plugin.filename = filename;
plugin.isActive = true;
if (!isPlugin) {
fs.unlink(dir, (err) => {
err && console.log(err);
});
return { error: "It is not a plugin" };
}
return plugin;
}
async delete(id: number) {
const plugin: Plugin = await this.pluginsRepository.findOneBy({ id });
const fileName = plugin.filename;
const dir = path.join(__dirname, pathToPlugins, `${fileName}`);
fs.unlink(dir, (err) => {
err && console.log(err);
});
await this.pluginsRepository.delete(id);
}
}

View File

@ -1,25 +0,0 @@
import * as fs from "fs";
import * as path from "path";
import * as config from "../../config";
const { path: pathToPlugins } = config.default.get("pluginsConf");
const fileFilter = (req, file, cb) => {
const pluginsDir = path.join(__dirname, pathToPlugins);
let files = null;
let isUniqName = true;
if (fs.existsSync(pluginsDir)) {
files = fs.readdirSync(pluginsDir);
isUniqName = !files?.includes(file.originalname);
}
if (file.mimetype === "text/javascript" && isUniqName) return cb(null, true);
return cb(null, false);
};
export default fileFilter;

View File

@ -1,3 +0,0 @@
import storage from "./storage";
export { storage };

View File

@ -1,24 +0,0 @@
import { diskStorage } from "multer";
import * as path from "path";
import * as fs from "fs";
import * as config from "../../config";
const { path: pathToPlugins } = config.default.get("pluginsConf");
const storage = diskStorage({
destination: function (req, file, cb) {
const dir = path.join(__dirname, pathToPlugins);
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir);
}
cb(null, dir);
},
filename: function (req, file, cb) {
cb(null, file.originalname);
},
});
export default storage;

View File

@ -37,6 +37,8 @@ public class MigrationContext : DbContext
public DbSet<MobileAppInstall> MobileAppInstall { get; set; }
public DbSet<DbIPLookup> DbIPLookup { get; set; }
public DbSet<DbWebPlugin> WebPlugins { get; set; }
public DbSet<Regions> Regions { get; set; }
public DbSet<FireBaseUser> FireBaseUsers { get; set; }
@ -112,6 +114,7 @@ public class MigrationContext : DbContext
.AddDbTariffRow()
.AddMobileAppInstall()
.AddDbIPLookup()
.AddDbWebPlugins()
.AddRegions()
.AddFireBaseUsers()
.AddNotifyInfo()

View File

@ -409,48 +409,51 @@
}
]
},
"csp":{
"default": {
"def": ["'self'", "data:"],
"script": ["'self'", "'unsafe-inline'", "'unsafe-eval'"],
"style": ["'self'", "'unsafe-inline'"],
"img": ["'self'", "data:", "blob:"],
"frame": ["'self'"]
},
"zendesk":{
"def": ["*.zdassets.com", "*.zopim.com", "*.zendesk.com", "wss:" ],
"script": ["*.zdassets.com", "*.zopim.com","'unsafe-eval'"],
"img": ["*.zopim.io" ]
},
"firebase":{
"script": ["*.googleapis.com"],
"def": ["*.googleapis.com" ]
},
"oform":{
"img": ["*.onlyoffice.com"],
"def": ["*.onlyoffice.com" ]
}
"csp": {
"default": {
"def": ["'self'", "data:"],
"script": ["'self'", "'unsafe-inline'", "'unsafe-eval'"],
"style": ["'self'", "'unsafe-inline'"],
"img": ["'self'", "data:", "blob:"],
"frame": ["'self'"]
},
"zendesk": {
"def": ["*.zdassets.com", "*.zopim.com", "*.zendesk.com", "wss:"],
"script": ["*.zdassets.com", "*.zopim.com", "'unsafe-eval'"],
"img": ["*.zopim.io"]
},
"firebase": {
"script": ["*.googleapis.com"],
"def": ["*.googleapis.com"]
},
"oform": {
"img": ["*.onlyoffice.com"],
"def": ["*.onlyoffice.com"]
}
},
"logocolors":[
{"r":255, "g":102, "b":128},
{"r":255, "g":143, "b":64},
{"r":242, "g":210, "b":48},
{"r":97, "g":192, "b":89},
{"r":112, "g":224, "b":150},
{"r":31, "g":206, "b":203},
{"r":92, "g":195, "b":247},
{"r":97, "g":145, "b":242},
{"r":119, "g":87, "b":217},
{"r":182, "g":121, "b":242},
{"r":255, "g":127, "b":212}
"logocolors": [
{ "r": 255, "g": 102, "b": 128 },
{ "r": 255, "g": 143, "b": 64 },
{ "r": 242, "g": 210, "b": 48 },
{ "r": 97, "g": 192, "b": 89 },
{ "r": 112, "g": 224, "b": 150 },
{ "r": 31, "g": 206, "b": 203 },
{ "r": 92, "g": 195, "b": 247 },
{ "r": 97, "g": 145, "b": 242 },
{ "r": 119, "g": 87, "b": 217 },
{ "r": 182, "g": 121, "b": 242 },
{ "r": 255, "g": 127, "b": 212 }
],
"radicale": {
"admin": "",
"path": ""
},
"plugins": {
"enabled": "false",
"allow": ["upload", "delete"]
"enabled": "true",
"extension": ".zip",
"maxSize": 5242880,
"allow": [],
"assetExtensions": [".jpg", ".jpeg", ".png", ".svg"]
},
"aws": {
"cloudWatch": {

View File

@ -98,17 +98,27 @@ server {
local red = redis:new()
local redis_host = "127.0.0.1"
local redis_port = 6379
local redis_pass = ""
red:set_timeout(1000) -- 1 second
local ok, err = red:connect(redis_host, redis_port)
if not ok then
ngx.log(ngx.ERR, "failed to connect to redis: ", err)
return
end
if redis_pass ~= "" then
local res, err = red:auth(redis_pass)
if not res then
ngx.log(ngx.ERR, "failed to authenticate: ", err)
return
end
end
local csp, err = red:hget(key, "data")
if csp == ngx.null then
if csp == ngx.null or not csp then
ngx.log(ngx.ERR, "failed to get redis key: ", err)
else
ngx.header.Content_Security_Policy = csp
@ -255,11 +265,7 @@ server {
location ~* /backup {
proxy_pass http://127.0.0.1:5012;
}
location ~* /plugins {
proxy_pass http://127.0.0.1:5014;
}
location ~* /migration {
proxy_pass http://127.0.0.1:5034;
}

View File

@ -127,6 +127,23 @@
"virtualpath": "~/studio/{0}/userphotos/temp"
}
]
},
{
"name": "webplugins",
"data": "00000000-0000-0000-0000-000000000000",
"type": "disc",
"path": "$STORAGE_ROOT\\Studio\\{0}\\webplugins",
"virtualpath": "~/studio/{0}/webplugins"
},
{
"name": "systemwebplugins",
"data": "00000000-0000-0000-0000-000000000000",
"type": "disc",
"path": "$STORAGE_ROOT\\Studio\\webplugins",
"virtualpath": "~/studio/webplugins",
"appendTenantId": false,
"disableMigrate": true,
"disableEncryption": true
}
]
}

6
config/telegram.json Normal file
View File

@ -0,0 +1,6 @@
{
"telegramConf": {
"botKey": "",
"chatId": ""
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,70 @@
using System;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace ASC.Migrations.MySql.Migrations.Migrations
{
/// <inheritdoc />
public partial class MigrationContextUpgrade2 : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "webplugins",
columns: table => new
{
Id = table.Column<int>(type: "int", nullable: false)
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
tenantid = table.Column<int>(name: "tenant_id", type: "int", nullable: false),
name = table.Column<string>(type: "varchar(255)", nullable: false)
.Annotation("MySql:CharSet", "utf8mb4"),
version = table.Column<string>(type: "varchar(255)", nullable: true)
.Annotation("MySql:CharSet", "utf8mb4"),
description = table.Column<string>(type: "text", nullable: true)
.Annotation("MySql:CharSet", "utf8mb4"),
license = table.Column<string>(type: "varchar(255)", nullable: true)
.Annotation("MySql:CharSet", "utf8mb4"),
author = table.Column<string>(type: "varchar(255)", nullable: true)
.Annotation("MySql:CharSet", "utf8mb4"),
homepage = table.Column<string>(name: "home_page", type: "varchar(255)", nullable: true)
.Annotation("MySql:CharSet", "utf8mb4"),
pluginname = table.Column<string>(name: "plugin_name", type: "varchar(255)", nullable: false)
.Annotation("MySql:CharSet", "utf8mb4"),
scopes = table.Column<string>(type: "text", nullable: true)
.Annotation("MySql:CharSet", "utf8mb4"),
image = table.Column<string>(type: "varchar(255)", nullable: true)
.Annotation("MySql:CharSet", "utf8mb4"),
createby = table.Column<Guid>(name: "create_by", type: "char(36)", nullable: false, collation: "ascii_general_ci"),
createon = table.Column<DateTime>(name: "create_on", type: "datetime", nullable: false),
enabled = table.Column<bool>(type: "tinyint(1)", nullable: false, defaultValueSql: "'0'"),
system = table.Column<bool>(type: "tinyint(1)", nullable: false, defaultValueSql: "'0'")
},
constraints: table =>
{
table.PrimaryKey("PRIMARY", x => x.Id);
table.ForeignKey(
name: "FK_webplugins_tenants_tenants_tenant_id",
column: x => x.tenantid,
principalTable: "tenants_tenants",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
})
.Annotation("MySql:CharSet", "utf8mb4");
migrationBuilder.CreateIndex(
name: "tenant",
table: "webplugins",
column: "tenant_id");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "webplugins");
}
}
}

View File

@ -1521,6 +1521,48 @@ namespace ASC.Migrations.MySql.SaaS.Migrations
.Annotation("MySql:CharSet", "utf8")
.Annotation("Relational:Collation", "utf8_general_ci");
migrationBuilder.CreateTable(
name: "webplugins",
columns: table => new
{
Id = table.Column<int>(type: "int", nullable: false)
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
tenantid = table.Column<int>(name: "tenant_id", type: "int", nullable: false),
name = table.Column<string>(type: "varchar(255)", nullable: false)
.Annotation("MySql:CharSet", "utf8mb4"),
version = table.Column<string>(type: "varchar(255)", nullable: true)
.Annotation("MySql:CharSet", "utf8mb4"),
description = table.Column<string>(type: "text", nullable: true)
.Annotation("MySql:CharSet", "utf8mb4"),
license = table.Column<string>(type: "varchar(255)", nullable: true)
.Annotation("MySql:CharSet", "utf8mb4"),
author = table.Column<string>(type: "varchar(255)", nullable: true)
.Annotation("MySql:CharSet", "utf8mb4"),
homepage = table.Column<string>(name: "home_page", type: "varchar(255)", nullable: true)
.Annotation("MySql:CharSet", "utf8mb4"),
pluginname = table.Column<string>(name: "plugin_name", type: "varchar(255)", nullable: false)
.Annotation("MySql:CharSet", "utf8mb4"),
scopes = table.Column<string>(type: "text", nullable: true)
.Annotation("MySql:CharSet", "utf8mb4"),
image = table.Column<string>(type: "varchar(255)", nullable: true)
.Annotation("MySql:CharSet", "utf8mb4"),
createby = table.Column<Guid>(name: "create_by", type: "char(36)", nullable: false, collation: "ascii_general_ci"),
createon = table.Column<DateTime>(name: "create_on", type: "datetime", nullable: false),
enabled = table.Column<bool>(type: "tinyint(1)", nullable: false, defaultValueSql: "'0'"),
system = table.Column<bool>(type: "tinyint(1)", nullable: false, defaultValueSql: "'0'")
},
constraints: table =>
{
table.PrimaryKey("PRIMARY", x => x.Id);
table.ForeignKey(
name: "FK_webplugins_tenants_tenants_tenant_id",
column: x => x.tenantid,
principalTable: "tenants_tenants",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
})
.Annotation("MySql:CharSet", "utf8mb4");
migrationBuilder.InsertData(
table: "files_converts",
columns: new[] { "input", "output" },
@ -2591,6 +2633,11 @@ namespace ASC.Migrations.MySql.SaaS.Migrations
table: "short_links",
column: "short",
unique: true);
migrationBuilder.CreateIndex(
name: "tenant",
table: "webplugins",
column: "tenant_id");
}
/// <inheritdoc />
@ -2766,6 +2813,9 @@ namespace ASC.Migrations.MySql.SaaS.Migrations
migrationBuilder.DropTable(
name: "short_links");
migrationBuilder.DropTable(
name: "webplugins");
}
}
}

View File

@ -1653,6 +1653,85 @@ namespace ASC.Migrations.MySql.SaaS.Migrations
b.HasAnnotation("MySql:CharSet", "utf8");
});
modelBuilder.Entity("ASC.Core.Common.EF.Model.DbWebPlugin", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
b.Property<string>("Author")
.HasColumnType("varchar(255)")
.HasColumnName("author");
b.Property<Guid>("CreateBy")
.HasColumnType("char(36)")
.HasColumnName("create_by");
b.Property<DateTime>("CreateOn")
.HasColumnType("datetime")
.HasColumnName("create_on");
b.Property<string>("Description")
.HasColumnType("text")
.HasColumnName("description");
b.Property<bool>("Enabled")
.ValueGeneratedOnAdd()
.HasColumnType("tinyint(1)")
.HasColumnName("enabled")
.HasDefaultValueSql("'0'");
b.Property<string>("HomePage")
.HasColumnType("varchar(255)")
.HasColumnName("home_page");
b.Property<string>("Image")
.HasColumnType("varchar(255)")
.HasColumnName("image");
b.Property<string>("License")
.HasColumnType("varchar(255)")
.HasColumnName("license");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("varchar(255)")
.HasColumnName("name");
b.Property<string>("PluginName")
.IsRequired()
.HasColumnType("varchar(255)")
.HasColumnName("plugin_name");
b.Property<string>("Scopes")
.HasColumnType("text")
.HasColumnName("scopes");
b.Property<bool>("System")
.ValueGeneratedOnAdd()
.HasColumnType("tinyint(1)")
.HasColumnName("system")
.HasDefaultValueSql("'0'");
b.Property<int>("TenantId")
.HasColumnType("int")
.HasColumnName("tenant_id");
b.Property<string>("Version")
.HasColumnType("varchar(255)")
.HasColumnName("version");
b.HasKey("Id")
.HasName("PRIMARY");
b.HasIndex("TenantId")
.HasDatabaseName("tenant");
b.ToTable("webplugins", (string)null);
b.HasAnnotation("MySql:CharSet", "utf8mb4");
});
modelBuilder.Entity("ASC.Core.Common.EF.Model.DbWebstudioIndex", b =>
{
b.Property<string>("IndexName")
@ -6784,6 +6863,17 @@ namespace ASC.Migrations.MySql.SaaS.Migrations
b.Navigation("Tenant");
});
modelBuilder.Entity("ASC.Core.Common.EF.Model.DbWebPlugin", b =>
{
b.HasOne("ASC.Core.Common.EF.Model.DbTenant", "Tenant")
.WithMany()
.HasForeignKey("TenantId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Tenant");
});
modelBuilder.Entity("ASC.Core.Common.EF.Model.DbTenantPartner", b =>
{
b.HasOne("ASC.Core.Common.EF.Model.DbTenant", "Tenant")

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,64 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace ASC.Migrations.PostgreSql.Migrations
{
/// <inheritdoc />
public partial class MigrationContextUpgrade2 : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "webplugins",
schema: "onlyoffice",
columns: table => new
{
Id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
tenantid = table.Column<int>(name: "tenant_id", type: "integer", nullable: false),
name = table.Column<string>(type: "character varying(255)", maxLength: 255, nullable: false),
version = table.Column<string>(type: "character varying(255)", maxLength: 255, nullable: true),
description = table.Column<string>(type: "text", nullable: true),
license = table.Column<string>(type: "character varying(255)", maxLength: 255, nullable: true),
author = table.Column<string>(type: "character varying(255)", maxLength: 255, nullable: true),
homepage = table.Column<string>(name: "home_page", type: "character varying(255)", maxLength: 255, nullable: true),
pluginname = table.Column<string>(name: "plugin_name", type: "character varying(255)", maxLength: 255, nullable: false),
scopes = table.Column<string>(type: "text", nullable: true),
image = table.Column<string>(type: "character varying(255)", maxLength: 255, nullable: true),
createby = table.Column<Guid>(name: "create_by", type: "uuid", fixedLength: true, maxLength: 36, nullable: false),
createon = table.Column<DateTime>(name: "create_on", type: "timestamp with time zone", nullable: false),
enabled = table.Column<bool>(type: "boolean", nullable: false),
system = table.Column<bool>(type: "boolean", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("webplugins_pkey", x => x.Id);
table.ForeignKey(
name: "FK_webplugins_tenants_tenants_tenant_id",
column: x => x.tenantid,
principalSchema: "onlyoffice",
principalTable: "tenants_tenants",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "tenant_webplugins",
schema: "onlyoffice",
table: "webplugins",
column: "tenant_id");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "webplugins",
schema: "onlyoffice");
}
}
}

View File

@ -1296,6 +1296,40 @@ namespace ASC.Migrations.PostgreSql.SaaS.Migrations
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "webplugins",
schema: "onlyoffice",
columns: table => new
{
Id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
tenantid = table.Column<int>(name: "tenant_id", type: "integer", nullable: false),
name = table.Column<string>(type: "character varying(255)", maxLength: 255, nullable: false),
version = table.Column<string>(type: "character varying(255)", maxLength: 255, nullable: true),
description = table.Column<string>(type: "text", nullable: true),
license = table.Column<string>(type: "character varying(255)", maxLength: 255, nullable: true),
author = table.Column<string>(type: "character varying(255)", maxLength: 255, nullable: true),
homepage = table.Column<string>(name: "home_page", type: "character varying(255)", maxLength: 255, nullable: true),
pluginname = table.Column<string>(name: "plugin_name", type: "character varying(255)", maxLength: 255, nullable: false),
scopes = table.Column<string>(type: "text", nullable: true),
image = table.Column<string>(type: "character varying(255)", maxLength: 255, nullable: true),
createby = table.Column<Guid>(name: "create_by", type: "uuid", fixedLength: true, maxLength: 36, nullable: false),
createon = table.Column<DateTime>(name: "create_on", type: "timestamp with time zone", nullable: false),
enabled = table.Column<bool>(type: "boolean", nullable: false),
system = table.Column<bool>(type: "boolean", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("webplugins_pkey", x => x.Id);
table.ForeignKey(
name: "FK_webplugins_tenants_tenants_tenant_id",
column: x => x.tenantid,
principalSchema: "onlyoffice",
principalTable: "tenants_tenants",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.InsertData(
schema: "onlyoffice",
table: "files_converts",
@ -2420,6 +2454,12 @@ namespace ASC.Migrations.PostgreSql.SaaS.Migrations
schema: "onlyoffice",
table: "webstudio_uservisit",
column: "visitdate");
migrationBuilder.CreateIndex(
name: "tenant_webplugins",
schema: "onlyoffice",
table: "webplugins",
column: "tenant_id");
}
/// <inheritdoc />
@ -2634,6 +2674,10 @@ namespace ASC.Migrations.PostgreSql.SaaS.Migrations
migrationBuilder.DropTable(
name: "tenants_tenants",
schema: "onlyoffice");
migrationBuilder.DropTable(
name: "webplugins",
schema: "onlyoffice");
}
}
}

View File

@ -1554,6 +1554,89 @@ namespace ASC.Migrations.PostgreSql.SaaS.Migrations
b.ToTable("tenants_version", "onlyoffice");
});
modelBuilder.Entity("ASC.Core.Common.EF.Model.DbWebPlugin", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
b.Property<string>("Author")
.HasMaxLength(255)
.HasColumnType("character varying(255)")
.HasColumnName("author");
b.Property<Guid>("CreateBy")
.HasMaxLength(36)
.HasColumnType("uuid")
.HasColumnName("create_by")
.IsFixedLength();
b.Property<DateTime>("CreateOn")
.HasColumnType("timestamp with time zone")
.HasColumnName("create_on");
b.Property<string>("Description")
.HasColumnType("text")
.HasColumnName("description");
b.Property<bool>("Enabled")
.HasColumnType("boolean")
.HasColumnName("enabled");
b.Property<string>("HomePage")
.HasMaxLength(255)
.HasColumnType("character varying(255)")
.HasColumnName("home_page");
b.Property<string>("Image")
.HasMaxLength(255)
.HasColumnType("character varying(255)")
.HasColumnName("image");
b.Property<string>("License")
.HasMaxLength(255)
.HasColumnType("character varying(255)")
.HasColumnName("license");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("character varying(255)")
.HasColumnName("name");
b.Property<string>("PluginName")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("character varying(255)")
.HasColumnName("plugin_name");
b.Property<string>("Scopes")
.HasColumnType("text")
.HasColumnName("scopes");
b.Property<bool>("System")
.HasColumnType("boolean")
.HasColumnName("system");
b.Property<int>("TenantId")
.HasColumnType("integer")
.HasColumnName("tenant_id");
b.Property<string>("Version")
.HasMaxLength(255)
.HasColumnType("character varying(255)")
.HasColumnName("version");
b.HasKey("Id")
.HasName("webplugins_pkey");
b.HasIndex("TenantId")
.HasDatabaseName("tenant_webplugins");
b.ToTable("webplugins", "onlyoffice");
});
modelBuilder.Entity("ASC.Core.Common.EF.Model.DbWebstudioIndex", b =>
{
b.Property<string>("IndexName")
@ -6515,6 +6598,17 @@ namespace ASC.Migrations.PostgreSql.SaaS.Migrations
b.Navigation("Tenant");
});
modelBuilder.Entity("ASC.Core.Common.EF.Model.DbWebPlugin", b =>
{
b.HasOne("ASC.Core.Common.EF.Model.DbTenant", "Tenant")
.WithMany()
.HasForeignKey("TenantId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Tenant");
});
modelBuilder.Entity("ASC.Core.Common.EF.Model.DbWebstudioSettings", b =>
{
b.HasOne("ASC.Core.Common.EF.Model.DbTenant", "Tenant")

View File

@ -0,0 +1 @@
{}

View File

@ -0,0 +1 @@
{}

View File

@ -0,0 +1 @@
{}

View File

@ -0,0 +1 @@
{}

View File

@ -0,0 +1 @@
{}

View File

@ -0,0 +1 @@
{}

View File

@ -0,0 +1,13 @@
{
"DeletePlugin": "Delete plugin",
"DeletePluginDescription": "This plugin will no longer be available to DocSpace users. Are you sure you want to continue?",
"DeletePluginTitle": "Delete plugin?",
"Description": "You can add plugins to extend the functionality of DocSpace with extra features. Click Upload plugin and select the plugin file in the ZIP archive.",
"Metadata": "Metadata",
"NeedSettings": "Need enter settings",
"NoPlugins": "No plugins added",
"NotNeedSettings": "Not need enter settings",
"Plugins": "Plugins",
"PluginsHelp": "Plugins allow you to extend the functionality of DocSpace",
"UploadPlugin": "Upload plugin"
}

View File

@ -0,0 +1 @@
{}

View File

@ -0,0 +1 @@
{}

View File

@ -0,0 +1 @@
{}

View File

@ -0,0 +1 @@
{}

View File

@ -0,0 +1 @@
{}

View File

@ -0,0 +1 @@
{}

View File

@ -0,0 +1 @@
{}

View File

@ -0,0 +1 @@
{}

View File

@ -0,0 +1 @@
{}

View File

@ -0,0 +1 @@
{}

View File

@ -0,0 +1 @@
{}

View File

@ -0,0 +1 @@
{}

View File

@ -0,0 +1 @@
{}

View File

@ -0,0 +1 @@
{}

View File

@ -0,0 +1,13 @@
{
"DeletePlugin": "Удалить плагин",
"DeletePluginDescription": "Этот плагин больше не будет доступен пользователям DocSpace. Вы уверены что хотите продолжить?",
"DeletePluginTitle": "Удалить плагин?",
"Description": "Вы можете добавлять плагины, чтобы расширить функциональность DocSpace дополнительными функциями. Нажмите «Загрузить плагин» и выберите файл плагина в ZIP-архиве.",
"Metadata": "Метаданные",
"NeedSettings": "Нужно ввести настройки",
"NoPlugins": "Плагины не добавлены",
"NotNeedSettings": "Не нужно вводить настройки",
"Plugins": "Плагины",
"PluginsHelp": "Плагины позволяют расширить функциональность DocSpace.",
"UploadPlugin": "Загрузить плагин"
}

View File

@ -0,0 +1 @@
{}

View File

@ -0,0 +1 @@
{}

View File

@ -0,0 +1 @@
{}

View File

@ -0,0 +1 @@
{}

View File

@ -0,0 +1 @@
{}

View File

@ -0,0 +1 @@
{}

View File

@ -133,8 +133,8 @@ const ClientContent = (props) => {
return (
<>
<GlobalEvents />
<FilesPanels />
<GlobalEvents />
{!isFormGallery ? (
isFrame ? (
showMenu && <ClientArticle />
@ -154,7 +154,7 @@ const ClientContent = (props) => {
};
const Client = inject(
({ auth, clientLoadingStore, filesStore, peopleStore }) => {
({ auth, clientLoadingStore, filesStore, peopleStore, pluginStore }) => {
const {
frameConfig,
isFrame,
@ -162,6 +162,7 @@ const Client = inject(
encryptionKeys,
setEncryptionKeys,
isEncryptionSupport,
enablePlugins,
isDesktopClientInit,
setIsDesktopClientInit,
} = auth.settingsStore;
@ -170,14 +171,13 @@ const Client = inject(
const { isVisitor } = auth.userStore.user;
const {
isLoading,
setIsSectionFilterLoading,
setIsSectionHeaderLoading,
} = clientLoadingStore;
const { isLoading, setIsSectionFilterLoading, setIsSectionHeaderLoading } =
clientLoadingStore;
const withMainButton = !isVisitor;
const { isInit: isInitPlugins, initPlugins } = pluginStore;
return {
isDesktop: isDesktopClient,
isDesktopClientInit,
@ -199,6 +199,8 @@ const Client = inject(
const actions = [];
actions.push(filesStore.initFiles());
actions.push(peopleStore.init());
if (enablePlugins && !isInitPlugins) actions.push(initPlugins());
await Promise.all(actions);
},
};

View File

@ -34,13 +34,8 @@ export default function withFileActions(WrappedFileItem) {
};
onDropZoneUpload = (files, uploadToFolder) => {
const {
t,
dragging,
setDragging,
startUpload,
uploadEmptyFolders,
} = this.props;
const { t, dragging, setDragging, startUpload, uploadEmptyFolders } =
this.props;
dragging && setDragging(false);
@ -93,6 +88,9 @@ export default function withFileActions(WrappedFileItem) {
e.target.classList.contains("item-file-name") ||
e.target.classList.contains("row-content-link");
if ((isRoomsFolder || isArchiveFolder) && isFileName && !isSelected)
setBufferSelection(item);
if (
isPrivacy ||
isTrashFolder ||

View File

@ -375,6 +375,7 @@ const ShellWrapper = inject(({ auth, backup }) => {
whiteLabelLogoUrls,
standalone,
} = settingsStore;
const isBase = settingsStore.theme.isBase;
const { setPreparationPortalDialogVisible } = backup;

View File

@ -183,6 +183,7 @@ const Items = ({
activeItemId,
emptyTrashInProgress,
isCommunity,
isPaymentPageAvailable,
}) => {

View File

@ -12,6 +12,7 @@ import PersonManagerReactSvgUrl from "PUBLIC_DIR/images/person.manager.react.svg
import PersonReactSvgUrl from "PUBLIC_DIR/images/person.react.svg?url";
import PersonUserReactSvgUrl from "PUBLIC_DIR/images/person.user.react.svg?url";
import InviteAgainReactSvgUrl from "PUBLIC_DIR/images/invite.again.react.svg?url";
import PluginMoreReactSvgUrl from "PUBLIC_DIR/images/plugin.more.react.svg?url";
import React from "react";
import { isMobileOnly } from "react-device-detect";
@ -26,8 +27,6 @@ import { useNavigate, useLocation } from "react-router-dom";
import MobileView from "./MobileView";
import { Events, EmployeeType } from "@docspace/common/constants";
import { getMainButtonItems } from "SRC_DIR/helpers/plugins";
import toastr from "@docspace/components/toast/toastr";
import styled, { css } from "styled-components";
import Button from "@docspace/components/button";
@ -110,6 +109,7 @@ const ArticleMainButtonContent = (props) => {
isArchiveFolder,
enablePlugins,
mainButtonItemsList,
currentColorScheme,
@ -254,6 +254,17 @@ const ArticleMainButtonContent = (props) => {
React.useEffect(() => {
if (isRoomsFolder || isSettingsPage) return;
const pluginItems = [];
if (mainButtonItemsList && enablePlugins && !isAccountsPage) {
mainButtonItemsList.forEach((option) => {
pluginItems.push({
key: option.key,
...option.value,
});
});
}
const formActions = [
{
id: "actions_template",
@ -411,6 +422,18 @@ const ArticleMainButtonContent = (props) => {
const menuModel = [...actions];
if (pluginItems.length > 0) {
menuModel.push({
id: "actions_more-plugins",
className: "main-button_drop-down",
icon: PluginMoreReactSvgUrl,
label: t("Common:More"),
disabled: false,
key: "more-plugins",
items: pluginItems,
});
}
menuModel.push({
isSeparator: true,
key: "separator",
@ -419,19 +442,6 @@ const ArticleMainButtonContent = (props) => {
menuModel.push(...uploadActions);
setUploadActions(uploadActions);
if (enablePlugins) {
const pluginOptions = getMainButtonItems();
if (pluginOptions) {
pluginOptions.forEach((option) => {
menuModel.splice(option.value.position, 0, {
key: option.key,
...option.value,
});
});
}
}
setModel(menuModel);
setActions(actions);
}, [
@ -441,9 +451,11 @@ const ArticleMainButtonContent = (props) => {
isAccountsPage,
isSettingsPage,
enablePlugins,
mainButtonItemsList,
isRoomsFolder,
isOwner,
isAdmin,
onCreate,
onCreateRoom,
onInvite,
@ -557,6 +569,7 @@ export default inject(
treeFoldersStore,
selectedFolderStore,
clientLoadingStore,
pluginStore,
}) => {
const { showArticleLoader } = clientLoadingStore;
const { mainButtonMobileVisible } = filesStore;
@ -588,6 +601,8 @@ export default inject(
const { isAdmin, isOwner } = auth.userStore.user;
const { isGracePeriod } = auth.currentTariffStatusStore;
const { mainButtonItemsList } = pluginStore;
return {
isGracePeriod,
setInviteUsersWarningDialogVisible,
@ -613,6 +628,8 @@ export default inject(
currentFolderId,
enablePlugins,
mainButtonItemsList,
currentColorScheme,
isAdmin,

View File

@ -29,6 +29,9 @@ import {
DeleteLinkDialog,
RoomSharingDialog,
MoveToPublicRoom,
SettingsPluginDialog,
PluginDialog,
DeletePluginDialog,
} from "../dialogs";
import ConvertPasswordDialog from "../dialogs/ConvertPasswordDialog";
import ArchiveDialog from "../dialogs/ArchiveDialog";
@ -78,8 +81,11 @@ const Panels = (props) => {
embeddingPanelIsVisible,
roomSharingPanelVisible,
moveToPublicRoomVisible,
settingsPluginDialogVisible,
pluginDialogVisible,
leaveRoomDialogVisible,
changeRoomOwnerIsVisible,
deletePluginDialogVisible,
} = props;
const { t } = useTranslation(["Translations", "Common"]);
@ -89,6 +95,21 @@ const Panels = (props) => {
};
return [
settingsPluginDialogVisible && (
<SettingsPluginDialog
isVisible={settingsPluginDialogVisible}
key={"settings-plugin-dialog"}
/>
),
deletePluginDialogVisible && (
<DeletePluginDialog
isVisible={deletePluginDialogVisible}
key={"delete-plugin-dialog"}
/>
),
pluginDialogVisible && (
<PluginDialog isVisible={pluginDialogVisible} key={"plugin-dialog"} />
),
uploadPanelVisible && <UploadPanel key="upload-panel" />,
sharingPanelVisible && (
<SharingPanel
@ -179,6 +200,7 @@ export default inject(
versionHistoryStore,
backup,
createEditRoomStore,
pluginStore,
}) => {
const {
sharingPanelVisible,
@ -227,6 +249,12 @@ export default inject(
const { hotkeyPanelVisible } = auth.settingsStore;
const { confirmDialogIsLoading } = createEditRoomStore;
const {
settingsPluginDialogVisible,
deletePluginDialogVisible,
pluginDialogVisible,
} = pluginStore;
return {
preparationPortalDialogVisible,
sharingPanelVisible,
@ -265,8 +293,11 @@ export default inject(
embeddingPanelIsVisible,
roomSharingPanelVisible,
moveToPublicRoomVisible,
settingsPluginDialogVisible,
pluginDialogVisible,
leaveRoomDialogVisible,
changeRoomOwnerIsVisible,
deletePluginDialogVisible,
};
}
)(observer(Panels));

View File

@ -0,0 +1,151 @@
import React from "react";
import { inject, observer } from "mobx-react";
import { useTranslation } from "react-i18next";
import { messageActions } from "SRC_DIR/helpers/plugins/utils";
import Dialog from "./sub-components/Dialog";
const CreatePluginFile = ({
visible,
title,
startValue,
onSave,
onCancel,
onClose,
isCreateDialog,
options,
selectedOption,
onSelect,
extension,
pluginId,
pluginName,
pluginSystem,
updatePluginStatus,
setCurrentSettingsDialogPlugin,
setSettingsPluginDialogVisible,
setPluginDialogVisible,
setPluginDialogProps,
updateContextMenuItems,
updateInfoPanelItems,
updateMainButtonItems,
updateProfileMenuItems,
updateEventListenerItems,
updateFileItems,
}) => {
const { t } = useTranslation(["Translations", "Common", "Files"]);
const onSaveAction = async (e, value) => {
if (!onSave) return onCloseAction();
const message = await onSave(e, value);
messageActions(
message,
null,
pluginId,
pluginName,
pluginSystem,
setSettingsPluginDialogVisible,
setCurrentSettingsDialogPlugin,
updatePluginStatus,
null,
setPluginDialogVisible,
setPluginDialogProps,
updateContextMenuItems,
updateInfoPanelItems,
updateMainButtonItems,
updateProfileMenuItems,
updateEventListenerItems,
updateFileItems
);
onCloseAction();
};
const onCloseAction = (e) => {
onCancel && onCancel();
onClose && onClose();
};
const onSelectAction = (option) => {
if (!onSelect) return;
const message = onSelect(option);
messageActions(
message,
null,
pluginId,
pluginName,
pluginSystem,
setSettingsPluginDialogVisible,
setCurrentSettingsDialogPlugin,
updatePluginStatus,
null,
setPluginDialogVisible,
setPluginDialogProps,
updateContextMenuItems,
updateInfoPanelItems,
updateMainButtonItems,
updateProfileMenuItems,
updateEventListenerItems,
updateFileItems
);
};
return (
<Dialog
t={t}
visible={visible}
title={title}
startValue={startValue}
onSave={onSaveAction}
onCancel={onCloseAction}
onClose={onCloseAction}
isCreateDialog={isCreateDialog}
extension={extension}
options={options}
selectedOption={selectedOption}
onSelect={onSelectAction}
/>
);
};
export default inject(({ pluginStore }) => {
const {
updatePluginStatus,
setCurrentSettingsDialogPlugin,
setSettingsPluginDialogVisible,
setPluginDialogVisible,
setPluginDialogProps,
updateContextMenuItems,
updateInfoPanelItems,
updateMainButtonItems,
updateProfileMenuItems,
updateEventListenerItems,
updateFileItems,
} = pluginStore;
return {
updatePluginStatus,
setCurrentSettingsDialogPlugin,
setSettingsPluginDialogVisible,
setPluginDialogVisible,
setPluginDialogProps,
updateContextMenuItems,
updateInfoPanelItems,
updateMainButtonItems,
updateProfileMenuItems,
updateEventListenerItems,
updateFileItems,
};
})(observer(CreatePluginFile));

View File

@ -1,4 +1,6 @@
import React, { useState, useEffect, useCallback, memo } from "react";
import { useState, useEffect, useCallback, useRef, memo } from "react";
import { inject, observer } from "mobx-react";
import { FileAction } from "@docspace/common/constants";
import { Events } from "@docspace/common/constants";
@ -8,8 +10,9 @@ import RenameEvent from "./RenameEvent";
import CreateRoomEvent from "./CreateRoomEvent";
import EditRoomEvent from "./EditRoomEvent";
import ChangeUserTypeEvent from "./ChangeUserTypeEvent";
import CreatePluginFile from "./CreatePluginFileEvent";
const GlobalEvents = () => {
const GlobalEvents = ({ enablePlugins, eventListenerItemsList }) => {
const [createDialogProps, setCreateDialogProps] = useState({
visible: false,
id: null,
@ -43,6 +46,14 @@ const GlobalEvents = () => {
onClose: null,
});
const [createPluginFileDialog, setCreatePluginFileProps] = useState({
visible: false,
props: null,
onClose: null,
});
const eventHandlersList = useRef([]);
const onCreate = useCallback((e) => {
const { payload } = e;
@ -115,11 +126,29 @@ const GlobalEvents = () => {
const onChangeUserType = useCallback((e) => {
setChangeUserTypeDialogProps({
visible: true,
onClose: () =>
setChangeUserTypeDialogProps({ visible: false, onClose: null }),
onClose: () => {
setChangeUserTypeDialogProps({ visible: false, onClose: null });
},
});
}, []);
const onCreatePluginFileDialog = useCallback(
(e) => {
if (!enablePlugins) return;
const { payload } = e;
setCreatePluginFileProps({
...payload,
visible: true,
onClose: () => {
payload.onClose && payload.onClose();
setCreatePluginFileProps({ visible: false, onClose: null });
},
});
},
[enablePlugins]
);
useEffect(() => {
window.addEventListener(Events.CREATE, onCreate);
window.addEventListener(Events.RENAME, onRename);
@ -127,14 +156,57 @@ const GlobalEvents = () => {
window.addEventListener(Events.ROOM_EDIT, onEditRoom);
window.addEventListener(Events.CHANGE_USER_TYPE, onChangeUserType);
if (enablePlugins) {
window.addEventListener(
Events.CREATE_PLUGIN_FILE,
onCreatePluginFileDialog
);
if (eventListenerItemsList) {
eventListenerItemsList.forEach((item) => {
const eventHandler = (e) => {
item.eventHandler(e);
};
eventHandlersList.current.push(eventHandler);
window.addEventListener(item.eventType, eventHandler);
});
}
}
return () => {
window.removeEventListener(Events.CREATE, onCreate);
window.removeEventListener(Events.RENAME, onRename);
window.removeEventListener(Events.ROOM_CREATE, onCreateRoom);
window.removeEventListener(Events.ROOM_EDIT, onEditRoom);
window.removeEventListener(Events.CHANGE_USER_TYPE, onChangeUserType);
if (enablePlugins) {
window.removeEventListener(
Events.CREATE_PLUGIN_FILE,
onCreatePluginFileDialog
);
if (eventListenerItemsList) {
eventListenerItemsList.forEach((item, index) => {
window.removeEventListener(
item.eventType,
eventHandlersList.current[index]
);
});
}
}
};
}, [onRename, onCreate, onCreateRoom, onEditRoom, onChangeUserType]);
}, [
onRename,
onCreate,
onCreateRoom,
onEditRoom,
onChangeUserType,
onCreatePluginFileDialog,
enablePlugins,
]);
return [
createDialogProps.visible && (
@ -155,7 +227,19 @@ const GlobalEvents = () => {
{...changeUserTypeDialog}
/>
),
createPluginFileDialog.visible && (
<CreatePluginFile
key={Events.CREATE_PLUGIN_FILE}
{...createPluginFileDialog}
/>
),
];
};
export default memo(GlobalEvents);
export default inject(({ auth, pluginStore }) => {
const { enablePlugins } = auth.settingsStore;
const { eventListenerItemsList } = pluginStore;
return { enablePlugins, eventListenerItemsList };
})(observer(GlobalEvents));

View File

@ -0,0 +1,84 @@
import React from "react";
import { inject, observer } from "mobx-react";
import { useTranslation } from "react-i18next";
import ModalDialog from "@docspace/components/modal-dialog";
import Button from "@docspace/components/button";
import toastr from "@docspace/components/toast/toastr";
import ModalDialogContainer from "../ModalDialogContainer";
const DeletePluginDialog = (props) => {
const { t, ready } = useTranslation(["WebPlugins", "Common"]);
const { isVisible, onClose, onDelete } = props;
const [isRequestRunning, setIsRequestRunning] = React.useState(false);
const onDeleteClick = async () => {
try {
setIsRequestRunning(true);
await onDelete();
setIsRequestRunning(true);
onClose();
} catch (error) {
toastr.error(error);
onClose();
}
};
return (
<ModalDialogContainer
isLoading={!ready}
visible={isVisible}
onClose={onClose}
displayType="modal"
>
<ModalDialog.Header>{t("DeletePluginTitle")}</ModalDialog.Header>
<ModalDialog.Body>{t("DeletePluginDescription")}</ModalDialog.Body>
<ModalDialog.Footer>
<Button
className="delete-button"
key="DeletePortalBtn"
label={t("Common:OkButton")}
size="normal"
scale
primary={true}
isLoading={isRequestRunning}
onClick={onDeleteClick}
/>
<Button
className="cancel-button"
key="CancelDeleteBtn"
label={t("Common:CancelButton")}
size="normal"
scale
isDisabled={isRequestRunning}
onClick={onClose}
/>
</ModalDialog.Footer>
</ModalDialogContainer>
);
};
export default inject(({ pluginStore }) => {
const {
deletePluginDialogProps,
setDeletePluginDialogVisible,
setDeletePluginDialogProps,
uninstallPlugin,
} = pluginStore;
const onClose = () => {
setDeletePluginDialogVisible(false);
setDeletePluginDialogProps(null);
};
const { pluginId, pluginName, pluginSystem } = deletePluginDialogProps;
const onDelete = async () => {
await uninstallPlugin(pluginId, pluginName, pluginSystem);
};
return { onClose, onDelete };
})(observer(DeletePluginDialog));

View File

@ -0,0 +1,201 @@
import React from "react";
import { inject, observer } from "mobx-react";
import ModalDialog from "@docspace/components/modal-dialog";
import WrappedComponent from "SRC_DIR/helpers/plugins/WrappedComponent";
import { PluginComponents } from "SRC_DIR/helpers/plugins/constants";
import { messageActions } from "SRC_DIR/helpers/plugins/utils";
const PluginDialog = ({
isVisible,
dialogHeader,
dialogBody,
dialogFooter,
onClose,
onLoad,
eventListeners,
pluginId,
pluginName,
pluginSystem,
setSettingsPluginDialogVisible,
setCurrentSettingsDialogPlugin,
updatePluginStatus,
setPluginDialogVisible,
setPluginDialogProps,
updateContextMenuItems,
updateInfoPanelItems,
updateMainButtonItems,
updateProfileMenuItems,
updateEventListenerItems,
updateFileItems,
...rest
}) => {
const [dialogHeaderProps, setDialogHeaderProps] =
React.useState(dialogHeader);
const [dialogBodyProps, setDialogBodyProps] = React.useState(dialogBody);
const [dialogFooterProps, setDialogFooterProps] =
React.useState(dialogFooter);
const [modalRequestRunning, setModalRequestRunning] = React.useState(false);
const functionsRef = React.useRef([]);
const onCloseAction = async () => {
if (modalRequestRunning) return;
const message = await onClose();
messageActions(
message,
null,
pluginId,
pluginName,
pluginSystem,
setSettingsPluginDialogVisible,
setCurrentSettingsDialogPlugin,
updatePluginStatus,
null,
setPluginDialogVisible,
setPluginDialogProps,
updateContextMenuItems,
updateInfoPanelItems,
updateMainButtonItems,
updateProfileMenuItems,
updateEventListenerItems,
updateFileItems
);
};
React.useEffect(() => {
if (eventListeners) {
eventListeners.forEach((e) => {
const onAction = async (evt) => {
const message = await e.onAction(evt);
messageActions(
message,
null,
pluginId,
pluginName,
pluginSystem,
setSettingsPluginDialogVisible,
setCurrentSettingsDialogPlugin,
updatePluginStatus,
null,
setPluginDialogVisible,
setPluginDialogProps,
updateContextMenuItems,
updateInfoPanelItems,
updateMainButtonItems,
updateProfileMenuItems,
updateEventListenerItems,
updateFileItems
);
};
functionsRef.current.push(onAction);
window.addEventListener(e.name, onAction);
});
}
return () => {
if (eventListeners) {
eventListeners.forEach((e, index) => {
window.removeEventListener(e.name, functionsRef.current[index]);
});
}
};
}, []);
const onLoadAction = React.useCallback(async () => {
// if (onLoad) {
// const res = await onLoad();
// setDialogHeaderProps(res.newDialogHeader);
// setDialogBodyProps(res.newDialogBody);
// setDialogFooterProps(res.newDialogFooter);
// }
}, [onLoad]);
React.useEffect(() => {
onLoadAction();
}, [onLoadAction]);
return (
<ModalDialog visible={isVisible} onClose={onCloseAction} {...rest}>
<ModalDialog.Header>{dialogHeaderProps}</ModalDialog.Header>
<ModalDialog.Body>
<WrappedComponent
pluginId={pluginId}
pluginName={pluginName}
pluginSystem={pluginSystem}
component={{
component: PluginComponents.box,
props: dialogBodyProps,
}}
setModalRequestRunning={setModalRequestRunning}
/>
</ModalDialog.Body>
{dialogFooterProps && (
<ModalDialog.Footer>
<WrappedComponent
pluginId={pluginId}
pluginName={pluginName}
pluginSystem={pluginSystem}
component={{
component: PluginComponents.box,
props: dialogFooterProps,
}}
setModalRequestRunning={setModalRequestRunning}
/>
</ModalDialog.Footer>
)}
</ModalDialog>
);
};
export default inject(({ pluginStore }) => {
const {
pluginDialogProps,
setSettingsPluginDialogVisible,
setCurrentSettingsDialogPlugin,
updatePluginStatus,
setPluginDialogVisible,
setPluginDialogProps,
updateContextMenuItems,
updateInfoPanelItems,
updateMainButtonItems,
updateProfileMenuItems,
updateEventListenerItems,
updateFileItems,
} = pluginStore;
return {
...pluginDialogProps,
setSettingsPluginDialogVisible,
setCurrentSettingsDialogPlugin,
updatePluginStatus,
setPluginDialogVisible,
setPluginDialogProps,
updateContextMenuItems,
updateInfoPanelItems,
updateMainButtonItems,
updateProfileMenuItems,
updateEventListenerItems,
updateFileItems,
};
})(observer(PluginDialog));

View File

@ -0,0 +1,159 @@
import React from "react";
import { inject, observer } from "mobx-react";
import { useTranslation } from "react-i18next";
import ModalDialog from "@docspace/components/modal-dialog";
import { PluginComponents } from "SRC_DIR/helpers/plugins/constants";
import WrappedComponent from "SRC_DIR/helpers/plugins/WrappedComponent";
import Header from "./sub-components/Header";
import Info from "./sub-components/Info";
import Footer from "./sub-components/Footer";
import Button from "@docspace/components/button";
const SettingsPluginDialog = ({
plugin,
withDelete,
onLoad,
settings,
saveButton,
settingsPluginDialogVisible,
onClose,
onDelete,
...rest
}) => {
const { t } = useTranslation(["WebPlugins", "Common", "Files", "People"]);
const [customSettingsProps, setCustomSettingsProps] =
React.useState(settings);
const [saveButtonProps, setSaveButtonProps] = React.useState(saveButton);
const [modalRequestRunning, setModalRequestRunning] = React.useState(false);
const onLoadAction = React.useCallback(async () => {
if (!onLoad) return;
const res = await onLoad();
setCustomSettingsProps(res.settings);
if (res.saveButton)
setSaveButtonProps({
...res.saveButton,
props: { ...res.saveButton, scale: true },
});
}, [onLoad]);
React.useEffect(() => {
onLoadAction();
}, [onLoadAction]);
const onCloseAction = () => {
if (modalRequestRunning) return;
onClose();
};
const onDeleteAction = () => {
if (modalRequestRunning) return;
onClose();
onDelete();
};
return (
<ModalDialog
visible={settingsPluginDialogVisible}
displayType="aside"
onClose={onCloseAction}
withBodyScroll
withFooterBorder
>
<ModalDialog.Header>
<Header t={t} name={plugin?.name} />
</ModalDialog.Header>
<ModalDialog.Body>
<WrappedComponent
pluginId={plugin.id}
pluginName={plugin.name}
pluginSystem={plugin.system}
component={{
component: PluginComponents.box,
props: customSettingsProps,
}}
saveButton={saveButton}
setSaveButtonProps={setSaveButtonProps}
setModalRequestRunning={setModalRequestRunning}
/>
<Info t={t} plugin={plugin} withDelete={withDelete} />
{withDelete && (
<Button
label={t("DeletePlugin")}
onClick={onDeleteAction}
scale
size={"normal"}
/>
)}
</ModalDialog.Body>
<ModalDialog.Footer>
<Footer
t={t}
id={plugin?.id}
pluginName={plugin.name}
pluginSystem={plugin.system}
saveButtonProps={saveButtonProps}
setModalRequestRunning={setModalRequestRunning}
onCloseAction={onCloseAction}
modalRequestRunning={modalRequestRunning}
/>
</ModalDialog.Footer>
</ModalDialog>
);
};
export default inject(({ auth, pluginStore }) => {
const {
pluginList,
settingsPluginDialogVisible,
setSettingsPluginDialogVisible,
currentSettingsDialogPlugin,
setCurrentSettingsDialogPlugin,
setDeletePluginDialogVisible,
setDeletePluginDialogProps,
} = pluginStore;
const { pluginOptions } = auth.settingsStore;
const { pluginId, pluginSystem, pluginName } = currentSettingsDialogPlugin;
const plugin = pluginSystem
? pluginList.find((p) => p.name === pluginName)
: pluginList.find((p) => p.id === pluginId);
const withDelete = pluginOptions.includes("delete") && !pluginSystem;
const pluginSettings = plugin?.getAdminPluginSettings();
const onClose = () => {
setSettingsPluginDialogVisible(false);
setCurrentSettingsDialogPlugin(null);
};
const onDelete = () => {
setDeletePluginDialogVisible(true);
setDeletePluginDialogProps({ pluginId, pluginSystem, pluginName });
};
return {
plugin,
withDelete,
...pluginSettings,
settingsPluginDialogVisible,
onClose,
onDelete,
};
})(observer(SettingsPluginDialog));

View File

@ -0,0 +1,57 @@
import React from "react";
import styled from "styled-components";
import Button from "@docspace/components/button";
import { PluginComponent } from "SRC_DIR/helpers/plugins/WrappedComponent";
const StyledContainer = styled.div`
width: 100%;
display: flex;
align-items: space-between;
gap: 10px;
`;
const Footer = ({
t,
id,
pluginName,
pluginSystem,
saveButtonProps,
modalRequestRunning,
setModalRequestRunning,
onCloseAction,
}) => {
return (
<StyledContainer>
<PluginComponent
component={{
...saveButtonProps,
props: {
...saveButtonProps?.props,
scale: true,
isSaveButton: true,
primary: true,
size: "normal",
label: t("Common:SaveButton"),
modalRequestRunning,
setSettingsModalRequestRunning: setModalRequestRunning,
onCloseAction,
},
}}
pluginId={id}
pluginName={pluginName}
pluginSystem={pluginSystem}
/>
<Button
scale={true}
size={"normal"}
onClick={onCloseAction}
label={t("Common:CancelButton")}
/>
</StyledContainer>
);
};
export default Footer;

View File

@ -0,0 +1,25 @@
import styled from "styled-components";
import { Base } from "@docspace/components/themes";
const StyledHeader = styled.div`
display: flex;
align-items: center;
div {
color: ${(props) => props.theme.plugins.pluginName};
}
`;
StyledHeader.defaultProps = { theme: Base };
const Header = ({ t, name }) => {
return (
<StyledHeader>
{t("Common:Settings")}&nbsp;
<div>({name})</div>
</StyledHeader>
);
};
export default Header;

View File

@ -0,0 +1,216 @@
import React from "react";
import styled, { css } from "styled-components";
import { getCookie } from "@docspace/common/utils";
import { LANGUAGE } from "@docspace/common/constants";
import Text from "@docspace/components/text";
import Link from "@docspace/components/link";
import getCorrectDate from "@docspace/components/utils/getCorrectDate";
import { PluginStatus } from "SRC_DIR/helpers/plugins/constants";
import { Base } from "@docspace/components/themes";
const StyledContainer = styled.div`
width: 100%;
${(props) =>
props.withDelete &&
css`
margin-bottom: 24px;
`}
`;
const StyledSeparator = styled.div`
width: 100%;
height: 1px;
margin: 24px 0;
background-color: ${(props) => props.theme.plugins.borderColor};
`;
StyledSeparator.defaultProps = { theme: Base };
const StyledInfo = styled.div`
margin-top: 24px;
width: 100%;
height: auto;
display: grid;
grid-template-columns: max-content 1fr;
gap: 8px 24px;
`;
const Info = ({ t, plugin, withDelete }) => {
const locale = getCookie(LANGUAGE) || "en";
const uploadDate = plugin.createOn && getCorrectDate(locale, plugin.createOn);
const pluginStatus =
plugin.status === PluginStatus.active
? t("NotNeedSettings")
: t("NeedSettings");
return (
<StyledContainer withDelete={withDelete}>
<StyledSeparator />
<Text fontSize={"14px"} fontWeight={600} lineHeight={"16px"} noSelect>
{t("Metadata")}
</Text>
<StyledInfo>
{plugin.author && (
<>
<Text
fontSize={"13px"}
fontWeight={400}
lineHeight={"20px"}
noSelect
truncate
>
{t("Files:ByAuthor")}
</Text>
<Text
fontSize={"13px"}
fontWeight={600}
lineHeight={"20px"}
noSelect
>
{plugin.author}
</Text>
</>
)}
{plugin.version && (
<>
<Text
fontSize={"13px"}
fontWeight={400}
lineHeight={"20px"}
noSelect
truncate
>
{t("Common:Version")}
</Text>
<Text
fontSize={"13px"}
fontWeight={600}
lineHeight={"20px"}
noSelect
>
{plugin.version}
</Text>
</>
)}
{!plugin.system && (
<>
<Text
fontSize={"13px"}
fontWeight={400}
lineHeight={"20px"}
noSelect
truncate
>
{t("Common:Uploader")}
</Text>
<Text
fontSize={"13px"}
fontWeight={600}
lineHeight={"20px"}
noSelect
>
{plugin.createBy}
</Text>
</>
)}
{!plugin.system && uploadDate && (
<>
<Text
fontSize={"13px"}
fontWeight={400}
lineHeight={"20px"}
noSelect
truncate
>
{t("Common:UploadDate")}
</Text>
<Text
fontSize={"13px"}
fontWeight={600}
lineHeight={"20px"}
noSelect
>
{uploadDate}
</Text>
</>
)}
<Text
fontSize={"13px"}
fontWeight={400}
lineHeight={"20px"}
noSelect
truncate
>
{t("People:UserStatus")}
</Text>
<Text fontSize={"13px"} fontWeight={600} lineHeight={"20px"} noSelect>
{pluginStatus}
</Text>
{plugin.homePage && (
<>
<Text
fontSize={"13px"}
fontWeight={400}
lineHeight={"20px"}
noSelect
truncate
>
{t("Common:Homepage")}
</Text>
<Link
fontSize={"13px"}
fontWeight={600}
lineHeight={"20px"}
type={"page"}
href={plugin?.homePage}
target={"_blank"}
noSelect
isHovered
>
{plugin.homePage}
</Link>
</>
)}
{plugin.description && (
<>
<Text
fontSize={"13px"}
fontWeight={400}
lineHeight={"20px"}
noSelect
truncate
>
{t("Common:Description")}
</Text>
<Text
fontSize={"13px"}
fontWeight={600}
lineHeight={"20px"}
noSelect
>
{plugin.description}
</Text>
</>
)}
</StyledInfo>
</StyledContainer>
);
};
export default Info;

View File

@ -34,7 +34,10 @@ import UnsavedChangesDialog from "./UnsavedChangesDialog";
import DeleteLinkDialog from "./DeleteLinkDialog";
import RoomSharingDialog from "./RoomSharingDialog";
import MoveToPublicRoom from "./MoveToPublicRoom";
import SettingsPluginDialog from "./SettingsPluginDialog";
import ReportDialog from "./ReportDialog";
import PluginDialog from "./PluginDialog";
import DeletePluginDialog from "./DeletePluginDialog";
export {
EmptyTrashDialog,
@ -73,5 +76,8 @@ export {
DeleteLinkDialog,
RoomSharingDialog,
MoveToPublicRoom,
SettingsPluginDialog,
ReportDialog,
PluginDialog,
DeletePluginDialog,
};

View File

@ -1,116 +0,0 @@
import { PluginType } from "./constants";
class PluginStore {
plugins = null;
contextMenuItems = null;
mainButtonItems = null;
profileMenuItems = null;
constructor() {
this.plugins = new Map();
this.contextMenuItems = new Map();
this.mainButtonItems = new Map();
this.profileMenuItems = new Map();
}
installPlugin(id, plugin) {
this.plugins.set(+id, plugin);
if (plugin.isActive) {
this.activatePlugin(+id);
}
}
uninstallPlugin(id) {
this.deactivatePlugin(id);
this.plugins.delete(+id);
}
getInstalledPlugins() {
return this.plugins;
}
activatePlugin(id) {
this.plugins.get(+id).isActive = true;
const pluginItems = this.plugins.get(+id).activate();
if (!pluginItems) return;
Array.from(pluginItems, ([key, value]) => {
switch (key) {
case PluginType.CONTEXT_MENU:
Array.from(value, ([key, item]) =>
this.addContextMenuItem(key, item)
);
return;
case PluginType.ACTION_BUTTON:
Array.from(value, ([key, item]) => this.addMainButtonItem(key, item));
return;
case PluginType.PROFILE_MENU:
Array.from(value, ([key, item]) =>
this.addProfileMenuItem(key, item)
);
return;
}
});
}
deactivatePlugin(id) {
this.plugins.get(+id).isActive = false;
const pluginItems = this.plugins.get(+id).deactivate();
if (!pluginItems) return;
Array.from(pluginItems, ([key, value]) => {
switch (key) {
case PluginType.CONTEXT_MENU:
value.map((key) => this.deleteContextMenuItem(key));
return;
case PluginType.ACTION_BUTTON:
value.map((key) => this.deleteMainButtonItem(key));
return;
case PluginType.PROFILE_MENU:
value.map((key) => this.deleteProfileMenuItem(key));
return;
}
});
}
addContextMenuItem(key, contextMenuItem) {
this.contextMenuItems.set(key, contextMenuItem);
}
deleteContextMenuItem(key) {
this.contextMenuItems.delete(key);
}
getContextMenuItems() {
return this.contextMenuItems;
}
addMainButtonItem(key, mainButtonItem) {
this.mainButtonItems.set(key, mainButtonItem);
}
deleteMainButtonItem(key) {
this.mainButtonItems.delete(key);
}
getMainButtonItems() {
return this.mainButtonItems;
}
addProfileMenuItem(key, profileMenuItem) {
this.profileMenuItems.set(key, profileMenuItem);
}
deleteProfileMenuItem(key) {
this.profileMenuItems.delete(key);
}
getProfileMenuItems() {
return this.profileMenuItems;
}
}
export default PluginStore;

View File

@ -0,0 +1,426 @@
import React from "react";
import { inject, observer } from "mobx-react";
import RectangleLoader from "@docspace/common/components/Loaders/RectangleLoader";
import Box from "@docspace/components/box";
import Text from "@docspace/components/text";
import Checkbox from "@docspace/components/checkbox";
import TextArea from "@docspace/components/textarea";
import TextInput from "@docspace/components/text-input";
import Label from "@docspace/components/label";
import Button from "@docspace/components/button";
import ToggleButton from "@docspace/components/toggle-button";
import ComboBox from "@docspace/components/combobox";
import { PluginComponents } from "./constants";
import { messageActions } from "./utils";
const PropsContext = React.createContext({});
const ComponentPure = ({
component,
pluginId,
pluginName,
pluginSystem,
setSettingsPluginDialogVisible,
setCurrentSettingsDialogPlugin,
updatePluginStatus,
setPluginDialogVisible,
setPluginDialogProps,
updateContextMenuItems,
updateInfoPanelItems,
updateMainButtonItems,
updateProfileMenuItems,
updateEventListenerItems,
updateFileItems,
}) => {
const [elementProps, setElementProps] = React.useState(component.props);
const {
contextProps,
updatePropsContext,
isRequestRunning,
setIsRequestRunning,
setModalRequestRunning,
} = React.useContext(PropsContext);
React.useEffect(() => {
if (
!component.contextName ||
(contextProps && !contextProps[component.contextName])
)
return;
contextProps && setElementProps(contextProps[component.contextName]);
}, [contextProps && contextProps[component.contextName]]);
React.useEffect(() => {
setElementProps(component.props);
}, [component.props]);
const getElement = () => {
const componentName = component.component;
switch (componentName) {
case PluginComponents.box: {
const childrenComponents = elementProps?.children?.map(
(item, index) => (
<PluginComponent
key={`box-${index}-${item.component}`}
component={item}
pluginId={pluginId}
pluginName={pluginName}
pluginSystem={pluginSystem}
/>
)
);
return <Box {...elementProps}>{childrenComponents}</Box>;
}
case PluginComponents.text: {
return <Text {...elementProps}>{elementProps.text}</Text>;
}
case PluginComponents.label: {
return <Label {...elementProps} />;
}
case PluginComponents.checkbox: {
const onChangeAction = () => {
const message = elementProps.onChange();
messageActions(
message,
setElementProps,
pluginId,
pluginName,
pluginSystem,
setSettingsPluginDialogVisible,
setCurrentSettingsDialogPlugin,
updatePluginStatus,
updatePropsContext,
setPluginDialogVisible,
setPluginDialogProps,
updateContextMenuItems,
updateInfoPanelItems,
updateMainButtonItems,
updateProfileMenuItems,
updateEventListenerItems,
updateFileItems
);
};
return <Checkbox {...elementProps} onChange={onChangeAction} />;
}
case PluginComponents.toggleButton: {
const onChangeAction = () => {
const message = elementProps.onChange();
messageActions(
message,
setElementProps,
pluginId,
pluginName,
pluginSystem,
setSettingsPluginDialogVisible,
setCurrentSettingsDialogPlugin,
updatePluginStatus,
updatePropsContext,
setPluginDialogVisible,
setPluginDialogProps,
updateContextMenuItems,
updateInfoPanelItems,
updateMainButtonItems,
updateProfileMenuItems,
updateEventListenerItems,
updateFileItems
);
};
return <ToggleButton {...elementProps} onChange={onChangeAction} />;
}
case PluginComponents.textArea: {
const onChangeAction = (e) => {
const message = elementProps.onChange(e.target.value);
messageActions(
message,
setElementProps,
pluginId,
pluginName,
pluginSystem,
setSettingsPluginDialogVisible,
setCurrentSettingsDialogPlugin,
updatePluginStatus,
updatePropsContext,
setPluginDialogVisible,
setPluginDialogProps,
updateContextMenuItems,
updateInfoPanelItems,
updateMainButtonItems,
updateProfileMenuItems,
updateEventListenerItems,
updateFileItems
);
};
return <TextArea {...elementProps} onChange={onChangeAction} />;
}
case PluginComponents.input: {
const onChangeAction = (e) => {
const message = elementProps.onChange(e.target.value);
messageActions(
message,
setElementProps,
pluginId,
pluginName,
pluginSystem,
setSettingsPluginDialogVisible,
setCurrentSettingsDialogPlugin,
updatePluginStatus,
updatePropsContext,
setPluginDialogVisible,
setPluginDialogProps,
updateContextMenuItems,
updateInfoPanelItems,
updateMainButtonItems,
updateProfileMenuItems,
updateEventListenerItems,
updateFileItems
);
};
return <TextInput {...elementProps} onChange={onChangeAction} />;
}
case PluginComponents.button: {
const {
withLoadingAfterClick,
disableWhileRequestRunning,
isSaveButton,
modalRequestRunning,
setSettingsModalRequestRunning,
onCloseAction,
...rest
} = elementProps;
const onClickAction = async () => {
if (withLoadingAfterClick) {
setIsRequestRunning && setIsRequestRunning(true);
setModalRequestRunning && setModalRequestRunning(true);
if (isSaveButton) {
setSettingsModalRequestRunning &&
setSettingsModalRequestRunning(true);
}
}
const message = await elementProps.onClick();
messageActions(
message,
setElementProps,
pluginId,
pluginName,
pluginSystem,
setSettingsPluginDialogVisible,
setCurrentSettingsDialogPlugin,
updatePluginStatus,
updatePropsContext,
setPluginDialogVisible,
setPluginDialogProps,
updateContextMenuItems,
updateInfoPanelItems,
updateMainButtonItems,
updateProfileMenuItems,
updateEventListenerItems,
updateFileItems
);
setIsRequestRunning && setIsRequestRunning(false);
if (isSaveButton) {
setSettingsModalRequestRunning &&
setSettingsModalRequestRunning(false);
onCloseAction && onCloseAction();
}
};
const isLoading = withLoadingAfterClick
? isSaveButton
? modalRequestRunning
: isRequestRunning
? isRequestRunning
: rest.isLoading
: rest.isLoading;
const isDisabled = disableWhileRequestRunning
? isSaveButton
? modalRequestRunning
: isRequestRunning
? isRequestRunning
: rest.isDisabled
: rest.isDisabled;
return (
<Button
{...rest}
isLoading={isLoading}
isDisabled={isDisabled}
onClick={onClickAction}
/>
);
}
case PluginComponents.comboBox: {
const onSelectAction = (option) => {
const message = elementProps.onSelect(option);
messageActions(
message,
setElementProps,
pluginId,
pluginName,
pluginSystem,
setSettingsPluginDialogVisible,
setCurrentSettingsDialogPlugin,
updatePluginStatus,
updatePropsContext,
setPluginDialogVisible,
setPluginDialogProps,
updateContextMenuItems,
updateInfoPanelItems,
updateMainButtonItems,
updateProfileMenuItems,
updateEventListenerItems,
updateFileItems
);
};
return <ComboBox {...elementProps} onSelect={onSelectAction} />;
}
case PluginComponents.iFrame: {
return (
<iframe {...elementProps} style={{ minHeight: "100%" }}></iframe>
);
}
case PluginComponents.img: {
return <img {...elementProps}></img>;
}
case PluginComponents.skeleton: {
return <RectangleLoader {...elementProps} />;
}
}
};
const element = getElement();
return element;
};
export const PluginComponent = inject(({ pluginStore }) => {
const {
updatePluginStatus,
setCurrentSettingsDialogPlugin,
setSettingsPluginDialogVisible,
setPluginDialogVisible,
setPluginDialogProps,
updateContextMenuItems,
updateInfoPanelItems,
updateMainButtonItems,
updateProfileMenuItems,
updateEventListenerItems,
updateFileItems,
} = pluginStore;
return {
updatePluginStatus,
setCurrentSettingsDialogPlugin,
setSettingsPluginDialogVisible,
setPluginDialogVisible,
setPluginDialogProps,
updateContextMenuItems,
updateInfoPanelItems,
updateMainButtonItems,
updateProfileMenuItems,
updateEventListenerItems,
updateFileItems,
};
})(observer(ComponentPure));
const WrappedComponent = ({
pluginId,
pluginName,
pluginSystem,
component,
saveButton,
setSaveButtonProps,
setModalRequestRunning,
}) => {
const [contextProps, setContextProps] = React.useState({});
const [isRequestRunning, setIsRequestRunning] = React.useState(false);
const updatePropsContext = (name, props) => {
if (saveButton && name === saveButton.contextName) {
setSaveButtonProps && setSaveButtonProps((val) => ({ ...val, props }));
} else {
const newProps = { ...contextProps };
newProps[name] = props;
setContextProps(newProps);
}
};
return (
<PropsContext.Provider
value={{
contextProps,
updatePropsContext,
isRequestRunning,
setIsRequestRunning,
setModalRequestRunning,
}}
>
<PluginComponent
component={component}
pluginId={pluginId}
pluginName={pluginName}
pluginSystem={pluginSystem}
/>
</PropsContext.Provider>
);
};
export default WrappedComponent;

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