Merge branch 'develop' into feature/web-plugins

This commit is contained in:
Alexey Safronov 2023-09-29 17:21:34 +04:00
commit 0d8bd872d6
34 changed files with 10140 additions and 125 deletions

View File

@ -7,7 +7,7 @@ BUILD_PATH=${BUILD_PATH:-${SRC_PATH}/publish}
BUILD_DOTNET_CORE_ARGS=${BUILD_DOTNET_CORE_ARGS:-"false"} BUILD_DOTNET_CORE_ARGS=${BUILD_DOTNET_CORE_ARGS:-"false"}
PROPERTY_BUILD=${PROPERTY_BUILD:-"all"} 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, \ 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"} 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"} 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 && \ yarn install --cwd ${SRC_PATH}/common/${ARRAY_NAME_SERVICES[$i]} --frozen-lockfile && \
mkdir -p ${BUILD_PATH}/services/${ARRAY_NAME_SERVICES[$i]}/service/ && \ 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/ 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" ]] if [[ ${DOCKER_ENTRYPOINT} != "false" ]]
then then
echo "== ADD ${DOCKER_ENTRYPOINT} to ${ARRAY_NAME_SERVICES[$i]} ==" echo "== ADD ${DOCKER_ENTRYPOINT} to ${ARRAY_NAME_SERVICES[$i]} =="

View File

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

View File

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

View File

@ -1,8 +1,8 @@
version: "3.8" version: "3.8"
services: services:
onlyoffice-dns: dnsmasq:
image: jpillora/dnsmasq image: jpillora/dnsmasq
container_name: onlyoffice-dns container_name: dnsmasq
restart: always restart: always
expose: expose:
- "5380" - "5380"

View File

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

View File

@ -53,10 +53,13 @@ public class DistributedTaskProgress : DistributedTask
[ProtoMember(3)] [ProtoMember(3)]
protected int StepCount { get; set; } protected int StepCount { get; set; }
protected CancellationToken CancellationToken { get; set; }
public virtual async Task RunJob(DistributedTask _, CancellationToken cancellationToken) public virtual async Task RunJob(DistributedTask _, CancellationToken cancellationToken)
{ {
Percentage = 0; Percentage = 0;
Status = DistributedTaskStatus.Running; Status = DistributedTaskStatus.Running;
CancellationToken = cancellationToken;
await DoJob(); await DoJob();
} }

View File

@ -47,8 +47,6 @@ public class ReassignProgressItem : DistributedTaskProgress
private bool _notify; private bool _notify;
private bool _deleteProfile; private bool _deleteProfile;
private CancellationToken _cancellationToken;
public ReassignProgressItem(IServiceScopeFactory serviceScopeFactory) public ReassignProgressItem(IServiceScopeFactory serviceScopeFactory)
{ {
_serviceScopeFactory = serviceScopeFactory; _serviceScopeFactory = serviceScopeFactory;
@ -70,13 +68,6 @@ public class ReassignProgressItem : DistributedTaskProgress
IsCompleted = false; IsCompleted = false;
} }
public override async Task RunJob(DistributedTask distributedTask, CancellationToken cancellationToken)
{
_cancellationToken = cancellationToken;
await base.RunJob(distributedTask, cancellationToken);
}
protected override async Task DoJob() protected override async Task DoJob()
{ {
await using var scope = _serviceScopeFactory.CreateAsyncScope(); await using var scope = _serviceScopeFactory.CreateAsyncScope();
@ -167,7 +158,7 @@ public class ReassignProgressItem : DistributedTaskProgress
PublishChanges(); PublishChanges();
} }
_cancellationToken.ThrowIfCancellationRequested(); CancellationToken.ThrowIfCancellationRequested();
} }
private async Task SendSuccessNotifyAsync(UserManager userManager, StudioNotifyService studioNotifyService, MessageService messageService, MessageTarget messageTarget, DisplayUserSettingsHelper displayUserSettingsHelper) private async Task SendSuccessNotifyAsync(UserManager userManager, StudioNotifyService studioNotifyService, MessageService messageService, MessageTarget messageTarget, DisplayUserSettingsHelper displayUserSettingsHelper)

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",
},
};

35
common/ASC.TelegramReports/.gitignore vendored Executable file
View File

@ -0,0 +1,35 @@
# compiled output
/dist
/node_modules
# Logs
logs
*.log
npm-debug.log*
pnpm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
# OS
.DS_Store
# Tests
/coverage
/.nyc_output
# IDEs and editors
/.idea
.project
.classpath
.c9/
*.launch
.settings/
*.sublime-workspace
# IDE - VSCode
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json

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

@ -0,0 +1,7 @@
{
"app": {
"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

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

View File

@ -0,0 +1,75 @@
{
"name": "telegram-reports",
"version": "1.0.0",
"description": "Server for telegram-reports",
"private": true,
"scripts": {
"prebuild": "rimraf dist",
"build": "rimraf dist & nest build",
"start": "rimraf dist & nest start",
"start:dev": "rimraf dist & nest start --watch",
"start:debug": "nest start --debug --watch",
"start:prod": "node dist/index",
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix"
},
"dependencies": {
"@aws-sdk/client-cloudwatch-logs": "^3.199.0",
"@nestjs/common": "^9.0.0",
"@nestjs/core": "^9.0.0",
"@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",
"winston-daily-rotate-file": "^4.5.5"
},
"devDependencies": {
"@nestjs/cli": "^9.0.0",
"@nestjs/schematics": "^9.0.0",
"@nestjs/testing": "^9.0.0",
"@types/express": "^4.17.13",
"@types/jest": "28.1.4",
"@types/multer": "^1.4.7",
"@types/node": "^16.0.0",
"@types/supertest": "^2.0.11",
"@typescript-eslint/eslint-plugin": "^5.0.0",
"@typescript-eslint/parser": "^5.0.0",
"eslint": "^8.0.1",
"eslint-config-prettier": "^8.3.0",
"eslint-plugin-prettier": "^4.0.0",
"jest": "28.1.2",
"prettier": "^2.3.2",
"source-map-support": "^0.5.20",
"supertest": "^6.1.3",
"ts-jest": "28.0.5",
"ts-loader": "^9.2.3",
"ts-node": "^10.0.0",
"tsconfig-paths": "4.0.0",
"typescript": "^4.3.5"
},
"jest": {
"moduleFileExtensions": [
"js",
"json",
"ts"
],
"rootDir": "src",
"testRegex": ".*\\.spec\\.ts$",
"transform": {
"^.+\\.(t|j)s$": "ts-jest"
},
"collectCoverageFrom": [
"**/*.(t|j)s"
],
"coverageDirectory": "../coverage",
"testEnvironment": "node"
}
}

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,
});

View File

@ -0,0 +1,4 @@
{
"extends": "./tsconfig.json",
"exclude": ["node_modules", "test", "dist", "**/*spec.ts"]
}

View File

@ -0,0 +1,22 @@
{
"compilerOptions": {
"module": "commonjs",
"declaration": true,
"removeComments": true,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"allowSyntheticDefaultImports": true,
"target": "es2017",
"sourceMap": true,
"outDir": "./dist",
"baseUrl": "./",
"incremental": true,
"skipLibCheck": true,
"strictNullChecks": false,
"noImplicitAny": false,
"strictBindCallApply": false,
"forceConsistentCasingInFileNames": false,
"noFallthroughCasesInSwitch": false,
"resolveJsonModule": true
}
}

File diff suppressed because it is too large Load Diff

View File

@ -24,6 +24,8 @@
// content are licensed under the terms of the Creative Commons Attribution-ShareAlike 4.0 // 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 // International. See the License terms at http://creativecommons.org/licenses/by-sa/4.0/legalcode
using ASC.Core;
namespace ASC.ApiCache; namespace ASC.ApiCache;
public class Startup public class Startup
@ -117,6 +119,8 @@ public class Startup
{ {
app.UseCors(CustomCorsPolicyName); app.UseCors(CustomCorsPolicyName);
} }
app.UseSynchronizationContextMiddleware();
app.UseAuthentication(); app.UseAuthentication();

View File

@ -139,6 +139,8 @@ public class Startup
{ {
app.UseCors(CustomCorsPolicyName); app.UseCors(CustomCorsPolicyName);
} }
app.UseSynchronizationContextMiddleware();
app.UseAuthentication(); app.UseAuthentication();

6
config/telegram.json Normal file
View File

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

View File

@ -88,6 +88,9 @@ export default function withFileActions(WrappedFileItem) {
e.target.classList.contains("item-file-name") || e.target.classList.contains("item-file-name") ||
e.target.classList.contains("row-content-link"); e.target.classList.contains("row-content-link");
if ((isRoomsFolder || isArchiveFolder) && isFileName && !isSelected)
setBufferSelection(item);
if ( if (
isPrivacy || isPrivacy ||
isTrashFolder || isTrashFolder ||

View File

@ -5,33 +5,12 @@ import AutoSizer from "react-virtualized-auto-sizer";
import CustomScrollbarsVirtualList from "@docspace/components/scrollbar/custom-scrollbars-virtual-list"; import CustomScrollbarsVirtualList from "@docspace/components/scrollbar/custom-scrollbars-virtual-list";
import InfiniteLoader from "react-window-infinite-loader"; import InfiniteLoader from "react-window-infinite-loader";
import User from "./User"; import User from "./User";
import { tablet, mobile, isMobile } from "@docspace/components/utils/device"; import { isMobile } from "@docspace/components/utils/device";
import throttle from "lodash/throttle";
import Loaders from "@docspace/common/components/Loaders";
const StyledMembersList = styled.div` const StyledMembersList = styled.div`
height: ${({ withBanner, isPublicRoomType }) => height: ${({ offsetTop }) => `calc(100vh - ${offsetTop})`};
isPublicRoomType
? withBanner
? "calc(100vh - 442px)"
: "calc(100vh - 286px)"
: "calc(100vh - 266px)"};
@media ${tablet} {
height: ${({ withBanner, isPublicRoomType }) =>
isPublicRoomType
? withBanner
? "calc(100vh - 362px)"
: "calc(100vh - 206px)"
: "calc(100vh - 186px)"};
}
@media ${mobile} {
height: ${({ withBanner, isPublicRoomType }) =>
isPublicRoomType
? withBanner
? "calc(100vh - 426px)"
: "calc(100vh - 270px)"
: "calc(100vh - 250px)"};
}
`; `;
const Item = memo(({ data, index, style }) => { const Item = memo(({ data, index, style }) => {
@ -53,6 +32,18 @@ const Item = memo(({ data, index, style }) => {
const user = members[index]; const user = members[index];
if (!user) {
return (
<div style={{ ...style, width: "calc(100% - 8px)", margin: "0 -16px" }}>
<Loaders.SelectorRowLoader
isMultiSelect={false}
isContainer={true}
isUser={true}
/>
</div>
);
}
return ( return (
<div key={user.id} style={{ ...style, width: "calc(100% - 8px)" }}> <div key={user.id} style={{ ...style, width: "calc(100% - 8px)" }}>
<User <User
@ -96,21 +87,34 @@ const MembersList = (props) => {
itemCount, itemCount,
onRepeatInvitation, onRepeatInvitation,
loadNextPage, loadNextPage,
isPublicRoomType,
withBanner,
} = props; } = props;
const itemsCount = members.length; const itemsCount = hasNextPage ? members.length + 1 : members.length;
const canInviteUserInRoomAbility = security?.EditAccess; const canInviteUserInRoomAbility = security?.EditAccess;
const [isNextPageLoading, setIsNextPageLoading] = useState(false); const [isNextPageLoading, setIsNextPageLoading] = useState(false);
const [isMobileView, setIsMobileView] = useState(isMobile()); const [isMobileView, setIsMobileView] = useState(isMobile());
const onResize = () => { const [offsetTop, setOffsetTop] = useState(0);
const onResize = throttle(() => {
const isMobileView = isMobile(); const isMobileView = isMobile();
setIsMobileView(isMobileView); setIsMobileView(isMobileView);
setOffset();
}, 300);
const setOffset = () => {
const rect = document
.getElementById("infoPanelMembersList")
?.getBoundingClientRect();
setOffsetTop(Math.ceil(rect?.top) + 2 + "px");
}; };
useEffect(() => {
setOffset();
}, [members]);
useEffect(() => { useEffect(() => {
window.addEventListener("resize", onResize); window.addEventListener("resize", onResize);
@ -138,10 +142,7 @@ const MembersList = (props) => {
); );
return ( return (
<StyledMembersList <StyledMembersList id="infoPanelMembersList" offsetTop={offsetTop}>
withBanner={withBanner}
isPublicRoomType={isPublicRoomType}
>
<AutoSizer> <AutoSizer>
{({ height, width }) => ( {({ height, width }) => (
<InfiniteLoader <InfiniteLoader

View File

@ -8,7 +8,7 @@ import {
RoomsType, RoomsType,
ShareAccessRights, ShareAccessRights,
} from "@docspace/common/constants"; } from "@docspace/common/constants";
import Loaders from "@docspace/common/components/Loaders";
import MembersHelper from "../../helpers/MembersHelper"; import MembersHelper from "../../helpers/MembersHelper";
import PublicRoomBlock from "./sub-components/PublicRoomBlock"; import PublicRoomBlock from "./sub-components/PublicRoomBlock";
import MembersList from "./MembersList"; import MembersList from "./MembersList";
@ -38,14 +38,16 @@ const Members = ({
setExternalLinks, setExternalLinks,
membersFilter, membersFilter,
externalLinks, externalLinks,
members,
setMembersList,
}) => { }) => {
const [isLoading, setIsLoading] = useState(false);
const membersHelper = new MembersHelper({ t }); const membersHelper = new MembersHelper({ t });
const [members, setMembers] = useState(null);
const security = selectionParentRoom ? selectionParentRoom.security : {}; const security = selectionParentRoom ? selectionParentRoom.security : {};
const fetchMembers = async (roomId, clearFilter = true) => { const fetchMembers = async (roomId, clearFilter = true) => {
if (isLoading) return;
const isPublic = selection?.roomType ?? selectionParentRoom?.roomType; const isPublic = selection?.roomType ?? selectionParentRoom?.roomType;
const requests = [getRoomMembers(roomId, clearFilter)]; const requests = [getRoomMembers(roomId, clearFilter)];
@ -53,7 +55,12 @@ const Members = ({
requests.push(getRoomLinks(roomId)); requests.push(getRoomLinks(roomId));
} }
let timerId;
if (clearFilter) timerId = setTimeout(() => setIsLoading(true), 300);
const [data, links] = await Promise.all(requests); const [data, links] = await Promise.all(requests);
clearFilter && setIsLoading(false);
clearTimeout(timerId);
links && setExternalLinks(links); links && setExternalLinks(links);
@ -131,11 +138,10 @@ const Members = ({
}; };
const updateSelectionParentRoomActionSelection = useCallback(async () => { const updateSelectionParentRoomActionSelection = useCallback(async () => {
if (!selection.isRoom) return; if (!selection.isRoom || selection.id === members?.roomId) return;
const fetchedMembers = await fetchMembers(selection.id); const fetchedMembers = await fetchMembers(selection.id);
setMembersList(fetchedMembers);
setMembers(fetchedMembers);
setSelectionParentRoom({ setSelectionParentRoom({
...selection, ...selection,
@ -159,7 +165,7 @@ const Members = ({
members: fetchedMembers, members: fetchedMembers,
}); });
setMembers(fetchedMembers); setMembersList(fetchedMembers);
}, [selectionParentRoom, selection?.id, updateRoomMembers]); }, [selectionParentRoom, selection?.id, updateRoomMembers]);
useEffect(() => { useEffect(() => {
@ -179,12 +185,6 @@ const Members = ({
.catch((err) => toastr.error(err)); .catch((err) => toastr.error(err));
}; };
if (!selectionParentRoom || !members) return null;
const [currentMember] = members.administrators.filter(
(member) => member.id === selfId
);
const loadNextPage = async () => { const loadNextPage = async () => {
const roomId = selectionParentRoom.id; const roomId = selectionParentRoom.id;
const fetchedMembers = await fetchMembers(roomId, false); const fetchedMembers = await fetchMembers(roomId, false);
@ -196,9 +196,16 @@ const Members = ({
expected: [...members.expected, ...expected], expected: [...members.expected, ...expected],
}; };
setMembers(newMembers); setMembersList(newMembers);
}; };
if (isLoading) return <Loaders.InfoPanelViewLoader view="members" />;
else if (!members) return <></>;
const [currentMember] = members.administrators.filter(
(member) => member.id === selfId
);
const { administrators, users, expected } = members; const { administrators, users, expected } = members;
const membersList = [...administrators, ...users, ...expected]; const membersList = [...administrators, ...users, ...expected];
@ -224,11 +231,11 @@ const Members = ({
changeUserType={changeUserType} changeUserType={changeUserType}
setIsScrollLocked={setIsScrollLocked} setIsScrollLocked={setIsScrollLocked}
hasNextPage={membersList.length - headersCount < membersFilter.total} hasNextPage={membersList.length - headersCount < membersFilter.total}
itemCount={membersFilter.total} itemCount={membersFilter.total + headersCount}
onRepeatInvitation={onRepeatInvitation} onRepeatInvitation={onRepeatInvitation}
isPublicRoomType={isPublicRoomType} isPublicRoomType={isPublicRoomType}
withBanner={isPublicRoomType && externalLinks.length > 0} withBanner={isPublicRoomType && externalLinks.length > 0}
setMembers={setMembers} setMembers={setMembersList}
/> />
</> </>
); );
@ -238,7 +245,6 @@ export default inject(
({ auth, filesStore, peopleStore, selectedFolderStore, publicRoomStore }) => { ({ auth, filesStore, peopleStore, selectedFolderStore, publicRoomStore }) => {
const { const {
selectionParentRoom, selectionParentRoom,
selection,
setSelectionParentRoom, setSelectionParentRoom,
setView, setView,
roomsView, roomsView,
@ -247,6 +253,8 @@ export default inject(
setUpdateRoomMembers, setUpdateRoomMembers,
setIsScrollLocked, setIsScrollLocked,
membersList,
setMembersList,
} = auth.infoPanelStore; } = auth.infoPanelStore;
const { const {
getRoomMembers, getRoomMembers,
@ -289,6 +297,8 @@ export default inject(
setExternalLinks, setExternalLinks,
membersFilter, membersFilter,
externalLinks: roomLinks, externalLinks: roomLinks,
members: membersList,
setMembersList,
}; };
} }
)( )(

View File

@ -1,17 +1,21 @@
import InfoPanelRoomEmptyScreenSvgUrl from "PUBLIC_DIR/images/empty_screen_corporate.svg?url"; import InfoPanelRoomEmptyScreenSvgUrl from "PUBLIC_DIR/images/empty_screen_corporate.svg?url";
import InfoPanelRoomEmptyScreenDarkSvgUrl from "PUBLIC_DIR/images/empty_screen_corporate_dark.svg?url"; import InfoPanelRoomEmptyScreenDarkSvgUrl from "PUBLIC_DIR/images/empty_screen_corporate_dark.svg?url";
import React from "react"; import React, { useEffect } from "react";
import { inject, observer } from "mobx-react"; import { inject, observer } from "mobx-react";
import Text from "@docspace/components/text"; import Text from "@docspace/components/text";
import { StyledNoItemContainer } from "../../styles/noItem"; import { StyledNoItemContainer } from "../../styles/noItem";
const NoRoomItem = ({ t, theme }) => { const NoRoomItem = ({ t, theme, setMembersList }) => {
const imageSrc = theme.isBase const imageSrc = theme.isBase
? InfoPanelRoomEmptyScreenSvgUrl ? InfoPanelRoomEmptyScreenSvgUrl
: InfoPanelRoomEmptyScreenDarkSvgUrl; : InfoPanelRoomEmptyScreenDarkSvgUrl;
useEffect(() => {
setMembersList(null);
}, []);
return ( return (
<StyledNoItemContainer className="info-panel_gallery-empty-screen"> <StyledNoItemContainer className="info-panel_gallery-empty-screen">
<div className="no-thumbnail-img-wrapper"> <div className="no-thumbnail-img-wrapper">
@ -27,5 +31,6 @@ const NoRoomItem = ({ t, theme }) => {
export default inject(({ auth }) => { export default inject(({ auth }) => {
return { return {
theme: auth.settingsStore.theme, theme: auth.settingsStore.theme,
setMembersList: auth.infoPanelStore.setMembersList,
}; };
})(observer(NoRoomItem)); })(observer(NoRoomItem));

View File

@ -1,24 +1,30 @@
import IntegrationSvgUrl from "PUBLIC_DIR/images/integration.svg?url";
import IntegrationDarkSvgUrl from "PUBLIC_DIR/images/integration.dark.svg?url";
import React from "react"; import React from "react";
import PropTypes from "prop-types"; import PropTypes from "prop-types";
import { withTranslation } from "react-i18next"; import { withTranslation } from "react-i18next";
import { inject, observer } from "mobx-react";
import styled from "styled-components"; import styled from "styled-components";
import { showLoader, hideLoader } from "@docspace/common/utils";
import Box from "@docspace/components/box"; import Box from "@docspace/components/box";
import Text from "@docspace/components/text"; import Text from "@docspace/components/text";
import Link from "@docspace/components/link"; import Link from "@docspace/components/link";
import Badge from "@docspace/components/badge"; import Badge from "@docspace/components/badge";
import toastr from "@docspace/components/toast/toastr"; import toastr from "@docspace/components/toast/toastr";
import Button from "@docspace/components/button"; import Button from "@docspace/components/button";
import { showLoader, hideLoader } from "@docspace/common/utils";
import ConsumerItem from "./sub-components/consumerItem";
import ConsumerModalDialog from "./sub-components/consumerModalDialog";
import { inject, observer } from "mobx-react";
import { import {
mobile, mobile,
smallTablet, smallTablet,
isSmallTablet, isSmallTablet,
} from "@docspace/components/utils/device"; } from "@docspace/components/utils/device";
import IntegrationSvgUrl from "PUBLIC_DIR/images/integration.svg?url";
import IntegrationDarkSvgUrl from "PUBLIC_DIR/images/integration.dark.svg?url"; import ConsumerItem from "./sub-components/consumerItem";
import ConsumerModalDialog from "./sub-components/consumerModalDialog";
import ThirdPartyLoader from "./sub-components/thirdPartyLoader";
const RootContainer = styled(Box)` const RootContainer = styled(Box)`
max-width: 700px; max-width: 700px;
@ -40,7 +46,6 @@ const RootContainer = styled(Box)`
display: grid; display: grid;
grid-template-columns: repeat(auto-fill, minmax(293px, 1fr)); grid-template-columns: repeat(auto-fill, minmax(293px, 1fr));
gap: 20px; gap: 20px;
margin-bottom: 20px;
} }
.consumer-item-wrapper { .consumer-item-wrapper {
@ -66,10 +71,11 @@ const RootContainer = styled(Box)`
} }
.business-plan { .business-plan {
grid-column: 1 / -1;
display: flex; display: flex;
gap: 8px; gap: 8px;
align-items: center; align-items: center;
margin-bottom: 16px; margin-bottom: -4px;
} }
`; `;
@ -218,54 +224,56 @@ class ThirdPartyServices extends React.Component {
scale={isSmallTablet()} scale={isSmallTablet()}
/> />
</Box> </Box>
<div className="consumers-list-container"> {!consumers.length ? (
{freeConsumers.map((consumer) => ( <ThirdPartyLoader />
<Box className="consumer-item-wrapper" key={consumer.name}> ) : (
<ConsumerItem <div className="consumers-list-container">
consumer={consumer} {freeConsumers.map((consumer) => (
dialogVisible={dialogVisible} <Box className="consumer-item-wrapper" key={consumer.name}>
isLoading={isLoading} <ConsumerItem
onChangeLoading={onChangeLoading} consumer={consumer}
onModalClose={onModalClose} dialogVisible={dialogVisible}
onModalOpen={onModalOpen} isLoading={isLoading}
setConsumer={setConsumer} onChangeLoading={onChangeLoading}
updateConsumerProps={updateConsumerProps} onModalClose={onModalClose}
t={t} onModalOpen={onModalOpen}
isThirdPartyAvailable={isThirdPartyAvailable} setConsumer={setConsumer}
/> updateConsumerProps={updateConsumerProps}
</Box> t={t}
))} isThirdPartyAvailable={isThirdPartyAvailable}
</div> />
{!isThirdPartyAvailable && ( </Box>
<div className="business-plan"> ))}
<Text fontSize="16px" fontWeight={700}> {!isThirdPartyAvailable && (
{t("IncludedInBusiness")} <div className="business-plan">
</Text> <Text fontSize="16px" fontWeight={700}>
<Badge {t("IncludedInBusiness")}
backgroundColor="#EDC409" </Text>
label={t("Common:Paid")} <Badge
isPaidBadge={true} backgroundColor="#EDC409"
/> label={t("Common:Paid")}
isPaidBadge={true}
/>
</div>
)}
{paidConsumers.map((consumer) => (
<Box className="consumer-item-wrapper" key={consumer.name}>
<ConsumerItem
consumer={consumer}
dialogVisible={dialogVisible}
isLoading={isLoading}
onChangeLoading={onChangeLoading}
onModalClose={onModalClose}
onModalOpen={onModalOpen}
setConsumer={setConsumer}
updateConsumerProps={updateConsumerProps}
t={t}
isThirdPartyAvailable={isThirdPartyAvailable}
/>
</Box>
))}
</div> </div>
)} )}
<div className="consumers-list-container">
{paidConsumers.map((consumer) => (
<Box className="consumer-item-wrapper" key={consumer.name}>
<ConsumerItem
consumer={consumer}
dialogVisible={dialogVisible}
isLoading={isLoading}
onChangeLoading={onChangeLoading}
onModalClose={onModalClose}
onModalOpen={onModalOpen}
setConsumer={setConsumer}
updateConsumerProps={updateConsumerProps}
t={t}
isThirdPartyAvailable={isThirdPartyAvailable}
/>
</Box>
))}
</div>
</RootContainer> </RootContainer>
{dialogVisible && ( {dialogVisible && (
<ConsumerModalDialog <ConsumerModalDialog

View File

@ -28,7 +28,7 @@ const StyledBox = styled(Box)`
css` css`
path { path {
fill: #ffffff; fill: #ffffff;
opacity: ${props.isSet ? 1 : 0.16}; opacity: 1;
} }
${props.isLinkedIn && ${props.isLinkedIn &&
css` css`
@ -45,7 +45,7 @@ const StyledBox = styled(Box)`
${(props) => ${(props) =>
!props.isThirdPartyAvailable && !props.isThirdPartyAvailable &&
!props.isSet && props.canSet &&
css` css`
path { path {
opacity: 0.5; opacity: 0.5;
@ -81,7 +81,7 @@ class ConsumerItem extends React.Component {
widthProp="100%" widthProp="100%"
> >
<StyledBox <StyledBox
isSet={isSet} canSet={consumer.canSet}
isLinkedIn={consumer.name === "linkedin"} isLinkedIn={consumer.name === "linkedin"}
isThirdPartyAvailable={isThirdPartyAvailable} isThirdPartyAvailable={isThirdPartyAvailable}
> >

View File

@ -0,0 +1,25 @@
import styled from "styled-components";
import Loaders from "@docspace/common/components/Loaders";
const StyledLoader = styled.div`
max-width: 700px;
width: 100%;
display: grid;
grid-template-columns: repeat(auto-fill, minmax(293px, 1fr));
gap: 20px;
`;
const ThirdPartyLoader = () => {
const rectangles = new Array(6).fill(0);
return (
<StyledLoader>
{rectangles.map((_) => (
<Loaders.Rectangle height="120px" borderRadius="6px" />
))}
</StyledLoader>
);
};
export default ThirdPartyLoader;

View File

@ -2480,7 +2480,7 @@ class FilesStore {
}; };
getRoomMembers = (id, clearFilter = true) => { getRoomMembers = (id, clearFilter = true) => {
let newFilter = this.membersFilter; let newFilter = clone(this.membersFilter);
if (clearFilter) { if (clearFilter) {
newFilter = this.getDefaultMembersFilter(); newFilter = this.getDefaultMembersFilter();
@ -2496,7 +2496,6 @@ class FilesStore {
}; };
return api.rooms.getRoomMembers(id, membersFilters).then((res) => { return api.rooms.getRoomMembers(id, membersFilters).then((res) => {
const newFilter = clone(this.membersFilter);
newFilter.total = res.total; newFilter.total = res.total;
this.setMembersFilter(newFilter); this.setMembersFilter(newFilter);

View File

@ -38,6 +38,7 @@ class InfoPanelStore {
filesStore = null; filesStore = null;
selectedFolderStore = null; selectedFolderStore = null;
treeFoldersStore = null; treeFoldersStore = null;
membersList = null;
constructor() { constructor() {
makeAutoObservable(this); makeAutoObservable(this);
@ -76,6 +77,7 @@ class InfoPanelStore {
this.roomsView = view; this.roomsView = view;
this.fileView = view === "info_members" ? "info_history" : view; this.fileView = view === "info_members" ? "info_history" : view;
this.isScrollLocked = false; this.isScrollLocked = false;
this.setMembersList(null);
}; };
setUpdateRoomMembers = (updateRoomMembers) => { setUpdateRoomMembers = (updateRoomMembers) => {
@ -329,6 +331,10 @@ class InfoPanelStore {
const pathname = givenPathName || window.location.pathname.toLowerCase(); const pathname = givenPathName || window.location.pathname.toLowerCase();
return pathname.indexOf("files/trash") !== -1; return pathname.indexOf("files/trash") !== -1;
}; };
setMembersList = (membersList) => {
this.membersList = membersList;
};
} }
export default InfoPanelStore; export default InfoPanelStore;

View File

@ -214,7 +214,6 @@ abstract class FileOperation<T, TId> : FileOperation where T : FileOperationData
protected ILinkDao LinkDao { get; private set; } protected ILinkDao LinkDao { get; private set; }
protected IProviderDao ProviderDao { get; private set; } protected IProviderDao ProviderDao { get; private set; }
protected ILogger Logger { get; private set; } protected ILogger Logger { get; private set; }
protected CancellationToken CancellationToken { get; private set; }
protected internal List<TId> Folders { get; private set; } protected internal List<TId> Folders { get; private set; }
protected internal List<TId> Files { get; private set; } protected internal List<TId> Files { get; private set; }
protected ExternalShareData CurrentShareData { get; private set; } protected ExternalShareData CurrentShareData { get; private set; }