Merge branch 'develop' into feature/inifinite-scroll

This commit is contained in:
Nikita Gopienko 2022-07-22 12:18:47 +03:00
commit af377fae99
60 changed files with 5892 additions and 4480 deletions

View File

@ -36,7 +36,6 @@ powershell -Command "(gc build\deploy\nginx\onlyoffice.conf) -replace '#', '' |
xcopy config\nginx\sites-enabled\* build\deploy\nginx\sites-enabled\ /E /R /Y
REM fix paths
powershell -Command "(gc build\deploy\nginx\sites-enabled\onlyoffice-editor.conf) -replace 'ROOTPATH', '%~dp0deploy\products\ASC.Files\editor' -replace '\\', '/' | Out-File -encoding ASCII build\deploy\nginx\sites-enabled\onlyoffice-editor.conf"
powershell -Command "(gc build\deploy\nginx\sites-enabled\onlyoffice-files.conf) -replace 'ROOTPATH', '%~dp0deploy\products\ASC.Files\client' -replace '\\', '/' | Out-File -encoding ASCII build\deploy\nginx\sites-enabled\onlyoffice-files.conf"
powershell -Command "(gc build\deploy\nginx\sites-enabled\onlyoffice-login.conf) -replace 'ROOTPATH', '%~dp0deploy\studio\login' -replace '\\', '/' | Out-File -encoding ASCII build\deploy\nginx\sites-enabled\onlyoffice-login.conf"
powershell -Command "(gc build\deploy\nginx\sites-enabled\onlyoffice-people.conf) -replace 'ROOTPATH', '%~dp0deploy\products\ASC.People\client' -replace '\\', '/' | Out-File -encoding ASCII build\deploy\nginx\sites-enabled\onlyoffice-people.conf"

View File

@ -33,16 +33,16 @@ public static partial class CommonLogger
[LoggerMessage(Level = LogLevel.Error)]
public static partial void ErrorWithException(this ILogger logger, string message, Exception exception);
[LoggerMessage(Level = LogLevel.Error)]
[LoggerMessage(Level = LogLevel.Error, Message = "{message}")]
public static partial void Error(this ILogger logger, string message);
[LoggerMessage(Level = LogLevel.Debug)]
[LoggerMessage(Level = LogLevel.Debug, Message = "{message}")]
public static partial void Debug(this ILogger logger, string message);
[LoggerMessage(Level = LogLevel.Information)]
[LoggerMessage(Level = LogLevel.Information, Message = "{message}")]
public static partial void Information(this ILogger logger, string message);
[LoggerMessage(Level = LogLevel.Warning)]
[LoggerMessage(Level = LogLevel.Warning, Message = "{message}")]
public static partial void Warning(this ILogger logger, string message);
[LoggerMessage(Level = LogLevel.Warning)]

View File

@ -1,9 +0,0 @@
server {
listen 5013;
root /var/www/products/ASC.Files/editor;
index index.html;
location / {
try_files $uri /index.html =404;
}
}

View File

@ -248,12 +248,13 @@ server {
}
location ~* /files/doceditor {
#rewrite products/files/doceditor/(.*) /$1 break;
proxy_pass http://localhost:5013;
proxy_redirect off;
proxy_set_header Host $host;
proxy_set_header Host $this_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header X-REWRITER-URL $X_REWRITER_URL;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;

View File

@ -1,9 +0,0 @@
server {
listen 5013;
root "ROOTPATH";
index index.html;
location / {
try_files $uri /index.html =404;
}
}

View File

@ -1,107 +1,15 @@
import axios from "axios";
import { AppServerConfig } from "../constants";
import { combineUrl } from "../utils";
import AxiosClient from "../utils/axiosClient";
const { proxyURL, apiPrefixURL, apiTimeout } = AppServerConfig;
const origin = window.location.origin;
const client = new AxiosClient();
const apiBaseURL = combineUrl(origin, proxyURL, apiPrefixURL);
const loginURL = combineUrl(proxyURL, "/login");
const paymentsURL = combineUrl(proxyURL, "/payments");
window.AppServer = {
...window.AppServer,
origin,
proxyURL,
apiPrefixURL,
apiBaseURL,
apiTimeout,
paymentsURL,
export const initSSR = (headers) => {
client.initSSR(headers);
};
/**
* @description axios instance for ajax requests
*/
const client = axios.create({
baseURL: apiBaseURL,
responseType: "json",
timeout: apiTimeout, // default is `0` (no timeout)
});
export function setWithCredentialsStatus(state) {
client.defaults.withCredentials = state;
}
export function setClientBasePath(path) {
if (!path) return;
client.defaults.baseURL = path;
}
const getResponseError = (res) => {
if (!res) return;
if (res.data && res.data.error) {
return res.data.error.message;
}
if (res.isAxiosError && res.message) {
//console.error(res.message);
return res.message;
}
export const request = (options) => {
return client.request(options);
};
/**
* @description wrapper for making ajax requests
* @param {object} object with method,url,data etc.
*/
export const request = function (options) {
const onSuccess = function (response) {
const error = getResponseError(response);
if (error) throw new Error(error);
if (!response || !response.data || response.isAxiosError) return null;
if (response.data.hasOwnProperty("total"))
return { total: +response.data.total, items: response.data.response };
if (response.request.responseType === "text") return response.data;
return response.data.response;
};
const onError = function (error) {
//console.error("Request Failed:", error);
const errorText = error.response
? getResponseError(error.response)
: error.message;
switch (error.response?.status) {
case 401:
if (options.skipUnauthorized) return Promise.resolve();
if (options.skipLogout) return Promise.reject(errorText || error);
request({
method: "post",
url: "/authentication/logout",
}).then(() => {
setWithCredentialsStatus(false);
window.location.href = window?.AppServer?.personal ? "/" : loginURL;
});
break;
case 402:
if (!window.location.pathname.includes("payments")) {
window.location.href = paymentsURL;
}
break;
default:
break;
}
return Promise.reject(errorText || error);
};
return client(options).then(onSuccess).catch(onError);
export const setWithCredentialsStatus = (state) => {
return client.setWithCredentialsStatus(state);
};

View File

@ -192,6 +192,7 @@ const Selector = (props) => {
return;
}
if (newGroupList.length) {
newGroupList[0].selectedCount = isChecked
? newGroupList[0].selectedCount - 1
: newGroupList[0].selectedCount + 1;
@ -207,6 +208,7 @@ const Selector = (props) => {
: newGroupList[groupIndex].selectedCount + 1;
}
});
}
});
setSelectedOptionList(newSelected);

View File

@ -27,7 +27,7 @@ const StyledFilterInput = styled.div`
.filter-input_selected-row {
width: 100%;
height: 32px;
min-height: 32px;
display: flex;
flex-direction: row;

View File

@ -19,6 +19,7 @@ const StyledSelectedItem = styled.div`
padding: 6px 8px;
margin-right: 4px;
margin-bottom: 4px;
background: #eceef1;

View File

@ -104,6 +104,7 @@ module.exports = {
"react-device-detect": {
singleton: true,
requiredVersion: compDeps["react-device-detect"],
// eager: true,
},
"react-dropzone": {
singleton: true,

View File

@ -0,0 +1 @@
export { default as fonts } from "./opensansoffline.scss";

View File

@ -0,0 +1,140 @@
import axios from "axios";
import { AppServerConfig } from "../constants";
import combineUrl from "./combineUrl";
const { proxyURL, apiPrefixURL, apiTimeout } = AppServerConfig;
class AxiosClient {
constructor() {
if (typeof window !== "undefined") this.initCSR();
}
initCSR = () => {
this.isSSR = false;
const origin = window.location.origin;
const apiBaseURL = combineUrl(origin, proxyURL, apiPrefixURL);
const paymentsURL = combineUrl(proxyURL, "/payments");
this.paymentsURL = paymentsURL;
window.AppServer = {
...window.AppServer,
origin,
proxyURL,
apiPrefixURL,
apiBaseURL,
apiTimeout,
paymentsURL,
};
this.client = axios.create({
baseURL: apiBaseURL,
responseType: "json",
timeout: apiTimeout, // default is `0` (no timeout)
});
};
initSSR = (headers) => {
this.isSSR = true;
const xRewriterUrl = headers["x-rewriter-url"];
const apiBaseURL = combineUrl(xRewriterUrl, proxyURL, apiPrefixURL);
this.client = axios.create({
baseURL: apiBaseURL,
responseType: "json",
timeout: apiTimeout,
headers: headers,
});
};
setWithCredentialsStatus = (state) => {
this.client.defaults.withCredentials = state;
};
setClientBasePath = (path) => {
if (!path) return;
this.client.defaults.baseURL = path;
};
getResponseError = (res) => {
if (!res) return;
if (res.data && res.data.error) {
return res.data.error.message;
}
if (res.isAxiosError && res.message) {
//console.error(res.message);
return res.message;
}
};
request = (options) => {
const onSuccess = (response) => {
const error = this.getResponseError(response);
if (error) throw new Error(error);
if (!response || !response.data || response.isAxiosError) return null;
if (response.data.hasOwnProperty("total"))
return { total: +response.data.total, items: response.data.response };
if (response.request.responseType === "text") return response.data;
return response.data.response;
};
const onError = (error) => {
//console.error("Request Failed:", error);
let errorText = error.response
? this.getResponseError(error.response)
: error.message;
if (error?.response?.status === 401 && this.isSSR) errorText = 401;
const loginURL = combineUrl(proxyURL, "/login");
if (!this.isSSR) {
switch (error.response?.status) {
case 401:
if (options.skipUnauthorized) return Promise.resolve();
if (options.skipLogout) return Promise.reject(errorText || error);
this.request({
method: "post",
url: "/authentication/logout",
}).then(() => {
this.setWithCredentialsStatus(false);
window.location.href = window?.AppServer?.personal
? "/"
: loginURL;
});
break;
case 402:
if (!window.location.pathname.includes("payments")) {
window.location.href = this.paymentsURL;
}
break;
default:
break;
}
return Promise.reject(errorText || error);
} else {
switch (error.response?.status) {
case 401:
return Promise.resolve();
default:
break;
}
return Promise.reject(errorText || error);
}
};
return this.client(options).then(onSuccess).catch(onError);
};
}
export default AxiosClient;

View File

@ -1,7 +1,6 @@
import { LANGUAGE } from "../constants";
import sjcl from "sjcl";
import { isMobile } from "react-device-detect";
import history from "../history";
import TopLoaderService from "@appserver/components/top-loading-indicator";
import { Encoder } from "./encoder";
@ -145,20 +144,6 @@ export function showLoader() {
export { withLayoutSize } from "./withLayoutSize";
export function tryRedirectTo(page) {
if (
window.location.pathname === page ||
window.location.pathname.indexOf(page) !== -1
) {
return false;
}
//TODO: check if we already on default page
//window.location.replace(page);
history.push(page);
return true;
}
export function isMe(user, userName) {
return (
user && user.id && (userName === "@self" || user.userName === userName)

View File

@ -0,0 +1,14 @@
import history from "../history";
export default function (page) {
if (
window.location.pathname === page ||
window.location.pathname.indexOf(page) !== -1
) {
return false;
}
//TODO: check if we already on default page
window.location.replace(page);
history.push(page); // SSR crash
return true;
}

View File

@ -7,7 +7,9 @@ let percentage = 0;
let increasePercentage = 0.75;
let moreIncreasePercentage = 3;
let elem = document.getElementById("ipl-progress-indicator");
let elem =
typeof document !== "undefined" &&
document.getElementById("ipl-progress-indicator");
const startInterval = () => {
if (timerId) return;

View File

@ -82,7 +82,7 @@
"style-loader": "3.2.1",
"terser-webpack-plugin": "^5.2.4",
"webpack": "5.52.1",
"webpack-cli": "4.9.0",
"webpack-cli": "4.10.0",
"webpack-dev-server": "4.3.1"
},
"id": "e67be73d-f9ae-4ce1-8fec-1880cb518cb4",

View File

@ -6,6 +6,7 @@ import throttle from "lodash/throttle";
import SelectFileDialogAsideView from "./AsideView";
import utils from "@appserver/components/utils";
import { FilterType } from "@appserver/common/constants";
import isEqual from "lodash/isEqual";
import SelectionPanel from "../SelectionPanel/SelectionPanelBody";
import toastr from "studio/toastr";
@ -131,6 +132,13 @@ class SelectFileDialog extends React.Component {
resultingFolderTree: tree,
});
}
componentDidUpdate(prevProps) {
if (!isEqual(prevProps, this.props)) {
this.setFilter();
}
}
componentWillUnmount() {
const {
setFolderId,

View File

@ -199,6 +199,7 @@ class SelectFolderDialog extends React.Component {
isRecycleBin,
currentFolderId,
selectionFiles,
selectionButtonPrimary,
} = this.props;
const {
displayType,

View File

@ -445,7 +445,7 @@ class FilesTableHeader extends React.Component {
const sortOrder = isRooms ? roomsFilter.sortOrder : filter.sortOrder;
// TODO: make some better
let needReset = false;
let needReset = this.props.isRooms !== this.state.isRooms;
let currentColumnStorageName = columnStorageName;
let currentColumnInfoPanelStorageName = columnInfoPanelStorageName;

@ -1 +1 @@
Subproject commit 022000362f747b9492183e77245f93bf6295b387
Subproject commit 2bb4d878b42e8c648dd44fed7d445163772551fa

View File

@ -80,7 +80,7 @@
"style-loader": "3.2.1",
"terser-webpack-plugin": "^5.2.4",
"webpack": "5.52.1",
"webpack-cli": "4.9.0",
"webpack-cli": "4.10.0",
"webpack-dev-server": "4.3.1"
},
"id": "f4d98afd-d336-4332-8778-3c6945c81ea0",

View File

@ -75,7 +75,7 @@
"style-loader": "3.2.1",
"terser-webpack-plugin": "^5.2.4",
"webpack": "5.52.1",
"webpack-cli": "4.9.0",
"webpack-cli": "4.10.0",
"webpack-dev-server": "4.3.1"
},
"title": "ONLYOFFICE"

View File

@ -3,7 +3,8 @@ import { withRouter } from "react-router";
import PropTypes from "prop-types";
import Loader from "@appserver/components/loader";
import Section from "@appserver/common/components/Section";
import { combineUrl, tryRedirectTo } from "@appserver/common/utils";
import { combineUrl } from "@appserver/common/utils";
import tryRedirectTo from "@appserver/common/utils/tryRedirectTo";
import { inject, observer } from "mobx-react";
import { AppServerConfig } from "@appserver/common/constants";

View File

@ -4,7 +4,8 @@ import PropTypes from "prop-types";
import { inject, observer } from "mobx-react";
import Loader from "@appserver/components/loader";
import Section from "@appserver/common/components/Section";
import { combineUrl, tryRedirectTo } from "@appserver/common/utils";
import { combineUrl } from "@appserver/common/utils";
import tryRedirectTo from "@appserver/common/utils/tryRedirectTo";
import { AppServerConfig } from "@appserver/common/constants";
class ChangeEmail extends React.PureComponent {

View File

@ -10,7 +10,8 @@ import { inject, observer } from "mobx-react";
import { StyledPage, StyledBody, StyledHeader } from "./StyledConfirm";
import withLoader from "../withLoader";
import { getPasswordErrorMessage } from "../../../../helpers/utils";
import { createPasswordHash, tryRedirectTo } from "@appserver/common/utils";
import { createPasswordHash } from "@appserver/common/utils";
import tryRedirectTo from "@appserver/common/utils/tryRedirectTo";
import toastr from "@appserver/components/toast/toastr";
const ChangePasswordForm = (props) => {

View File

@ -5,7 +5,7 @@ import { useTranslation } from "react-i18next";
import Text from "@appserver/components/text";
import toastr from "studio/toastr";
import Section from "@appserver/common/components/Section";
import { tryRedirectTo } from "@appserver/common/utils";
import tryRedirectTo from "@appserver/common/utils/tryRedirectTo";
import { setDocumentTitle } from "../../../helpers/utils";
import { inject, observer } from "mobx-react";
import { HomeIllustration, ModuleTile, HomeContainer } from "./sub-components";

View File

@ -1,7 +1,19 @@
{
"presets": ["@babel/preset-react", "@babel/preset-env"],
"plugins": [
"@babel/plugin-transform-runtime",
"@babel/plugin-proposal-class-properties"
]
"presets": [
[
"@babel/preset-env",
{
"bugfixes": true
}
],
"@babel/preset-react"
],
"plugins": [
"@loadable/babel-plugin",
"babel-plugin-styled-components",
["@babel/plugin-proposal-class-properties", { "loose": false }],
"@babel/plugin-proposal-export-default-from"
],
"ignore": ["node_modules", "build"],
"sourceType": "unambiguous"
}

View File

@ -0,0 +1,11 @@
module.exports = {
presets: ["@babel/preset-env", "@babel/preset-react"],
plugins: [
"@loadable/babel-plugin",
"babel-plugin-styled-components",
["@babel/plugin-proposal-class-properties", { loose: false }],
"@babel/plugin-proposal-export-default-from",
],
//ignore: ["node_modules", "build"],
//sourceType: "unambiguous",
};

View File

@ -4,45 +4,79 @@
"private": true,
"homepage": "/products/files/doceditor",
"scripts": {
"build": "webpack --mode production",
"build:personal": "webpack --mode production --env personal=true",
"build:test": "webpack --env minimize=false --mode production",
"build:test.translation": "webpack --env minimize=false hideText=true --mode production",
"build:test.translation:personal": "webpack --env minimize=false hideText=true personal=true --mode production",
"build": "yarn clean && yarn build:client && yarn build:server",
"build:personal": "yarn clean && yarn build:client-personal && yarn build:server-personal",
"build:server": "webpack --mode production --config webpack/webpack.server.js",
"build:client": "webpack --mode production --config webpack/webpack.client.js",
"build:dev-server": "webpack --config webpack/webpack.server.js",
"build:dev-client": "webpack --config webpack/webpack.client.js",
"build:server-personal": "webpack --mode production --env personal=true --config webpack/webpack.server.js",
"build:client-personal": "webpack --mode production --env personal=true --config webpack/webpack.client.js",
"build:dev-server-personal": "webpack --env personal=true --config webpack/webpack.server.js",
"build:dev-client-personal": "webpack --env personal=true --config webpack/webpack.client.js",
"clean": "shx rm -rf dist",
"deploy": "shx --silent mkdir -p ../../build/deploy/products/ASC.Files/editor && shx cp -r dist/* ../../build/deploy/products/ASC.Files/editor",
"serve": "serve dist -p 5013",
"start": "webpack-cli serve",
"start:personal": "webpack-cli serve --env personal=true",
"start-prod": "webpack --mode production && serve dist -p 5013"
"start": "yarn clean && npm-run-all --parallel start:*",
"start-personal": "yarn clean && npm-run-all --parallel start-personal:* ",
"start:client": "webpack --config webpack/webpack.client.js --watch --no-cache",
"start:server": "webpack --config webpack/webpack.server.js --watch --no-cache",
"start:common": "yarn build:dev-client && yarn build:dev-server && nodemon --watch dist/server.js dist/server.js",
"start-personal:client": "webpack --env personal=true --config webpack/webpack.client.js --watch --no-cache",
"start-personal:server": "webpack --env personal=true --config webpack/webpack.server.js --watch --no-cache",
"start-personal:common": "yarn build:dev-client-personal && yarn build:dev-server-personal && nodemon --watch dist/server.js dist/server.js",
"deploy": "shx --silent mkdir -p ../../build/deploy/products/ASC.Files/editor && shx cp -r dist/* ../../build/deploy/products/ASC.Files/editor"
},
"devDependencies": {
"@babel/core": "^7.15.5",
"@babel/plugin-proposal-class-properties": "^7.14.5",
"@babel/plugin-proposal-export-default-from": "^7.14.5",
"@babel/plugin-transform-runtime": "^7.15.0",
"@babel/preset-env": "^7.15.6",
"@babel/preset-react": "^7.14.5",
"@svgr/webpack": "^5.5.0",
"@babel/core": "^7.18.6",
"@babel/plugin-proposal-class-properties": "^7.13.0",
"@babel/plugin-proposal-export-default-from": "^7.17.12",
"@babel/plugin-transform-runtime": "^7.18.6",
"@babel/preset-env": "^7.14.4",
"@babel/preset-react": "^7.13.13",
"@babel/register": "^7.18.6",
"@loadable/babel-plugin": "^5.13.2",
"@loadable/component": "^5.15.2",
"@loadable/server": "^5.15.2",
"@loadable/webpack-plugin": "^5.15.0",
"@svgr/webpack": "^6.2.1",
"babel-loader": "^8.2.2",
"babel-plugin-styled-components": "^1.12.0",
"clean-webpack-plugin": "^4.0.0",
"copy-webpack-plugin": "^9.0.1",
"css-loader": "^6.2.0",
"copy-webpack-plugin": "^11.0.0",
"cross-env": "^7.0.3",
"css-loader": "^6.7.1",
"dotenv": "^16.0.1",
"external-remotes-plugin": "^1.0.0",
"file-loader": "^6.2.0",
"html-webpack-plugin": "5.3.2",
"json-loader": "^0.5.7",
"sass": "^1.39.2",
"sass-loader": "^12.1.0",
"serve": "12.0.1",
"shx": "^0.3.3",
"source-map-loader": "^3.0.0",
"style-loader": "3.2.1",
"terser-webpack-plugin": "^5.2.4",
"nodemon": "^2.0.7",
"npm-run-all": "^4.1.5",
"sass": "^1.53.0",
"sass-loader": "^13.0.2",
"shx": "^0.3.4",
"source-map-loader": "^4.0.0",
"style-loader": "^3.3.1",
"styled-components": "^5.3.5",
"terser-webpack-plugin": "^5.1.2",
"webpack": "5.52.1",
"webpack-cli": "4.9.0",
"webpack-dev-server": "4.3.1"
"webpack-cli": "4.10.0",
"webpack-dev-server": "4.3.1",
"webpack-hot-middleware": "^2.25.0",
"webpack-manifest-plugin": "^5.0.0",
"webpack-merge": "^5.7.3",
"webpack-node-externals": "^3.0.0",
"ws": "^8.8.0"
},
"id": "e67be73d-f9ae-4ce1-8fec-1880cb518cb4",
"title": "ONLYOFFICE"
"title": "ONLYOFFICE",
"socketPath": "/products/files/doceditor/ws",
"dependencies": {
"compression": "^1.7.4",
"express": "^4.18.1",
"i18next-express-middleware": "^2.0.0",
"i18next-fs-backend": "^1.1.4",
"morgan": "^1.10.0",
"winston": "^3.8.1",
"winston-daily-rotate-file": "^4.7.1"
}
}

View File

@ -1,361 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta
name="viewport"
content="width=device-width, initial-scale=1, shrink-to-fit=no, user-scalable=no, viewport-fit=cover"
/>
<meta name="theme-color" content="#000000" />
<!--
manifest.json provides metadata used when your web app is added to the
homescreen on Android. See https://developers.google.com/web/fundamentals/engage-and-retain/web-app-manifest/
-->
<link id="favicon" rel="shortcut icon" sizes="any" href="/favicon.ico" />
<link rel="manifest" href="/manifest.json" />
<!-- Tell the browser it's a PWA -->
<!-- <meta name="mobile-web-app-capable" content="yes" /> -->
<!-- Tell iOS it's a PWA -->
<!-- <meta name="apple-mobile-web-app-capable" content="yes" /> -->
<link rel="apple-touch-icon" href="/appIcon-180.png" />
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<style type="text/css">
.loadmask {
left: 0;
top: 0;
position: absolute;
height: 100%;
width: 100%;
overflow: hidden;
border: none;
background-color: #f4f4f4;
z-index: 1001;
}
.loader-page {
width: 100%;
height: 170px;
bottom: 42%;
position: absolute;
text-align: center;
line-height: 10px;
margin-bottom: 20px;
}
.loader-page-romb {
width: 40px;
display: inline-block;
}
.romb {
width: 40px;
height: 40px;
position: absolute;
background: red;
border-radius: 6px;
-webkit-transform: rotate(135deg) skew(20deg, 20deg);
-moz-transform: rotate(135deg) skew(20deg, 20deg);
-ms-transform: rotate(135deg) skew(20deg, 20deg);
-o-transform: rotate(135deg) skew(20deg, 20deg);
-webkit-animation: movedown 3s infinite ease;
-moz-animation: movedown 3s infinite ease;
-ms-animation: movedown 3s infinite ease;
-o-animation: movedown 3s infinite ease;
animation: movedown 3s infinite ease;
}
#blue {
z-index: 3;
background: #55bce6;
-webkit-animation-name: blue;
-moz-animation-name: blue;
-ms-animation-name: blue;
-o-animation-name: blue;
animation-name: blue;
}
#red {
z-index: 1;
background: #de7a59;
-webkit-animation-name: red;
-moz-animation-name: red;
-ms-animation-name: red;
-o-animation-name: red;
animation-name: red;
}
#green {
z-index: 2;
background: #a1cb5c;
-webkit-animation-name: green;
-moz-animation-name: green;
-ms-animation-name: green;
-o-animation-name: green;
animation-name: green;
}
@-webkit-keyframes red {
0% {
top: 120px;
background: #de7a59;
}
10% {
top: 120px;
background: #f2cbbf;
}
14% {
background: #f4f4f4;
top: 120px;
}
15% {
background: #f4f4f4;
top: 0;
}
20% {
background: #e6e4e4;
}
30% {
background: #d2d2d2;
}
40% {
top: 120px;
}
100% {
top: 120px;
background: #de7a59;
}
}
@keyframesred {
0% {
top: 120px;
background: #de7a59;
}
10% {
top: 120px;
background: #f2cbbf;
}
14% {
background: #f4f4f4;
top: 120px;
}
15% {
background: #f4f4f4;
top: 0;
}
20% {
background: #e6e4e4;
}
30% {
background: #d2d2d2;
}
40% {
top: 120px;
}
100% {
top: 120px;
background: #de7a59;
}
}
@-webkit-keyframes green {
0% {
top: 110px;
background: #a1cb5c;
opacity: 1;
}
10% {
top: 110px;
background: #cbe0ac;
opacity: 1;
}
14% {
background: #f4f4f4;
top: 110px;
opacity: 1;
}
15% {
background: #f4f4f4;
top: 0;
opacity: 1;
}
20% {
background: #f4f4f4;
top: 0;
opacity: 0;
}
25% {
background: #efefef;
top: 0;
opacity: 1;
}
30% {
background: #e6e4e4;
}
70% {
top: 110px;
}
100% {
top: 110px;
background: #a1cb5c;
}
}
@keyframes green {
0% {
top: 110px;
background: #a1cb5c;
opacity: 1;
}
10% {
top: 110px;
background: #cbe0ac;
opacity: 1;
}
14% {
background: #f4f4f4;
top: 110px;
opacity: 1;
}
15% {
background: #f4f4f4;
top: 0;
opacity: 1;
}
20% {
background: #f4f4f4;
top: 0;
opacity: 0;
}
25% {
background: #efefef;
top: 0;
opacity: 1;
}
30% {
background: #e6e4e4;
}
70% {
top: 110px;
}
100% {
top: 110px;
background: #a1cb5c;
}
}
@-webkit-keyframes blue {
0% {
top: 100px;
background: #55bce6;
opacity: 1;
}
10% {
top: 100px;
background: #bfe8f8;
opacity: 1;
}
14% {
background: #f4f4f4;
top: 100px;
opacity: 1;
}
15% {
background: #f4f4f4;
top: 0;
opacity: 1;
}
20% {
background: #f4f4f4;
top: 0;
opacity: 0;
}
25% {
background: #f4f4f4;
top: 0;
opacity: 0;
}
45% {
background: #efefef;
top: 0;
opacity: 0.2;
}
100% {
top: 100px;
background: #55bce6;
}
}
@keyframes blue {
0% {
top: 100px;
background: #55bce6;
opacity: 1;
}
10% {
top: 100px;
background: #bfe8f8;
opacity: 1;
}
14% {
background: #f4f4f4;
top: 100px;
opacity: 1;
}
15% {
background: #f4f4f4;
top: 0;
opacity: 1;
}
20% {
background: #f4f4f4;
top: 0;
opacity: 0;
}
25% {
background: #fff;
top: 0;
opacity: 0;
}
45% {
background: #efefef;
top: 0;
opacity: 0.2;
}
100% {
top: 100px;
background: #55bce6;
}
}
</style>
<title><%= htmlWebpackPlugin.options.title %></title>
</head>
<body>
<noscript> You need to enable JavaScript to run this app. </noscript>
<div id="loader" class="loadmask">
<div class="loader-page">
<div class="loader-page-romb">
<div class="romb" id="blue"></div>
<div class="romb" id="green"></div>
<div class="romb" id="red"></div>
</div>
</div>
</div>
<div id="root"></div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
<script>
const tempElm = document.getElementById("loader");
tempElm.style.backgroundColor =
localStorage.theme === "Dark" ? "#333333" : "#f4f4f4";
console.log("It's Editor INIT");
</script>
</body>
</html>

View File

@ -1,19 +0,0 @@
//import "@appserver/common/utils/wdyr";
import React from "react";
import Editor from "./Editor";
import ErrorBoundary from "@appserver/common/components/ErrorBoundary";
import "@appserver/common/custom.scss";
import { AppServerConfig } from "@appserver/common/constants";
import { combineUrl } from "@appserver/common/utils";
const App = () => {
const onError = () =>
window.open(combineUrl(AppServerConfig.proxyURL, "/login"), "_self");
return (
<ErrorBoundary onError={onError}>
<Editor />
</ErrorBoundary>
);
};
export default App;

View File

@ -1,983 +0,0 @@
import React, { useEffect, useState } from "react";
import Toast from "@appserver/components/toast";
import toastr from "studio/toastr";
import { toast } from "react-toastify";
import { Trans } from "react-i18next";
import Box from "@appserver/components/box";
import { regDesktop } from "@appserver/common/desktop";
import {
combineUrl,
getObjectByLocation,
loadScript,
isRetina,
getCookie,
setCookie,
assign,
} from "@appserver/common/utils";
import {
getDocServiceUrl,
openEdit,
setEncryptionKeys,
getEncryptionAccess,
getFileInfo,
updateFile,
removeFromFavorite,
markAsFavorite,
getPresignedUri,
convertFile,
checkFillFormDraft,
getEditHistory,
getEditDiff,
restoreDocumentsVersion,
getSettingsFiles,
} from "@appserver/common/api/files";
import FilesFilter from "@appserver/common/api/files/filter";
import throttle from "lodash/throttle";
import { isIOS, deviceType } from "react-device-detect";
import { homepage } from "../package.json";
import { AppServerConfig } from "@appserver/common/constants";
import SharingDialog from "files/SharingDialog";
import { getDefaultFileName, SaveAs } from "files/utils";
import SelectFileDialog from "files/SelectFileDialog";
import SelectFolderDialog from "files/SelectFolderDialog";
import PreparationPortalDialog from "studio/PreparationPortalDialog";
import { StyledSelectFolder } from "./StyledEditor";
import i18n from "./i18n";
import Text from "@appserver/components/text";
import TextInput from "@appserver/components/text-input";
import Checkbox from "@appserver/components/checkbox";
import { isMobile } from "react-device-detect";
import store from "studio/store";
import ThemeProvider from "@appserver/components/theme-provider";
const { auth: authStore } = store;
const theme = store.auth.settingsStore.theme;
let documentIsReady = false;
const text = "word";
const spreadSheet = "cell";
const presentation = "slide";
const insertImageAction = "imageFileType";
const mailMergeAction = "mailMergeFileType";
const compareFilesAction = "documentsFileType";
let docTitle = null;
let actionLink;
let docSaved = null;
let docEditor;
let fileInfo;
let successAuth;
let isSharingAccess;
let user = null;
let personal = IS_PERSONAL || null;
let config;
let url = window.location.href;
const filesUrl = url.substring(0, url.indexOf("/doceditor"));
//const doc = url.indexOf("doc=") !== -1 ? url.split("doc=")[1] : null;
toast.configure();
const Editor = () => {
const urlParams = getObjectByLocation(window.location);
const decodedId = urlParams
? urlParams.fileId || urlParams.fileid || null
: null;
let fileId =
typeof decodedId === "string" ? encodeURIComponent(decodedId) : decodedId;
let version = urlParams ? urlParams.version || null : null;
const doc = urlParams ? urlParams.doc || null : null;
const isDesktop = window["AscDesktopEditor"] !== undefined;
const view = url.indexOf("action=view") !== -1;
const [isLoading, setIsLoading] = useState(true);
const [isAuthenticated, setIsAuthenticated] = useState(true);
const [titleSelectorFolder, setTitleSelectorFolder] = useState("");
const [extension, setExtension] = useState();
const [urlSelectorFolder, setUrlSelectorFolder] = useState("");
const [openNewTab, setNewOpenTab] = useState(false);
const [typeInsertImageAction, setTypeInsertImageAction] = useState();
const throttledChangeTitle = throttle(() => changeTitle(), 500);
const [settings, setSettings] = useState(null);
const [
preparationPortalDialogVisible,
setPreparationPortalDialogVisible,
] = useState(false);
let filesSettings;
useEffect(() => {
const tempElm = document.getElementById("loader");
tempElm.style.backgroundColor = theme.backgroundColor;
}, []);
useEffect(() => {
if (isRetina() && getCookie("is_retina") == null) {
setCookie("is_retina", true, { path: "/" });
}
init();
}, []);
const canConvert = (extension) => {
const array = filesSettings?.extsMustConvert || [];
const result = array.findIndex((item) => item === extension);
return result === -1 ? false : true;
};
const loadUsersRightsList = () => {
SharingDialog.getSharingSettings(fileId).then((sharingSettings) => {
docEditor.setSharingSettings({
sharingSettings,
});
});
};
const insertImage = (link) => {
const token = link.token;
docEditor.insertImage({
...typeInsertImageAction,
fileType: link.filetype,
...(token && { token }),
url: link.url,
});
};
const mailMerge = (link) => {
const token = link.token;
docEditor.setMailMergeRecipients({
fileType: link.filetype,
...(token && { token }),
url: link.url,
});
};
const compareFiles = (link) => {
const token = link.token;
docEditor.setRevisedFile({
fileType: link.filetype,
...(token && { token }),
url: link.url,
});
};
const updateFavorite = (favorite) => {
docEditor.setFavorite(favorite);
};
const initDesktop = (cfg) => {
const encryptionKeys = cfg?.editorConfig?.encryptionKeys;
regDesktop(
user,
!!encryptionKeys,
encryptionKeys,
(keys) => {
setEncryptionKeys(keys);
},
true,
(callback) => {
getEncryptionAccess(fileId)
.then((keys) => {
var data = {
keys,
};
callback(data);
})
.catch((error) => {
console.log(error);
toastr.error(
typeof error === "string" ? error : error.message,
null,
0,
true
);
});
},
i18n.t
);
};
const convertDocumentUrl = async () => {
const convert = await convertFile(fileId, null, true);
return convert && convert[0]?.result;
};
const init = async () => {
try {
if (!fileId) return;
console.log(
`Editor componentDidMount fileId=${fileId}, version=${version}, doc=${doc}`
);
if (isIPad()) {
const vh = window.innerHeight * 0.01;
document.documentElement.style.setProperty("--vh", `${vh}px`);
}
//showLoader();
const docApiUrl = await getDocServiceUrl();
try {
await authStore.init(true);
user = authStore.userStore.user;
if (user) {
filesSettings = await getSettingsFiles();
setSettings(filesSettings);
}
personal = authStore.settingsStore.personal;
successAuth = !!user;
const { socketHelper } = authStore.settingsStore;
socketHelper.emit({
command: "subscribe",
data: "backup-restore",
});
socketHelper.on("restore-backup", () => {
setPreparationPortalDialogVisible(true);
});
} catch (e) {
successAuth = false;
}
if (!doc && !successAuth) {
localStorage.setItem("redirectPath", window.location.href);
window.open(
combineUrl(
AppServerConfig.proxyURL,
personal ? "/sign-in" : "/login"
),
"_self",
"",
true
);
return;
}
if (successAuth) {
try {
fileInfo = await getFileInfo(fileId);
if (url.indexOf("#message/") > -1) {
if (canConvert(fileInfo.fileExst)) {
const result = await convertDocumentUrl();
const splitUrl = url.split("#message/");
if (result) {
const newUrl = `${result.webUrl}#message/${splitUrl[1]}`;
history.pushState({}, null, newUrl);
fileInfo = result;
url = newUrl;
fileId = result.id;
version = result.version;
}
}
}
} catch (err) {
console.error(err);
}
setIsAuthenticated(successAuth);
}
config = await openEdit(fileId, version, doc, view);
if (
!view &&
fileInfo &&
fileInfo.canWebRestrictedEditing &&
fileInfo.canFillForms &&
!fileInfo.canEdit
) {
try {
const formUrl = await checkFillFormDraft(fileId);
history.pushState({}, null, formUrl);
document.location.reload();
} catch (err) {
console.error(err);
}
}
actionLink = config?.editorConfig?.actionLink;
if (isDesktop) {
initDesktop(config);
}
isSharingAccess = fileInfo && fileInfo.canShare;
if (view) {
config.editorConfig.mode = "view";
}
setIsLoading(false);
loadScript(docApiUrl, "scripDocServiceAddress", () => onLoad());
} catch (error) {
console.log(error);
toastr.error(
typeof error === "string" ? error : error.message,
null,
0,
true
);
}
};
const isIPad = () => {
return isIOS && deviceType === "tablet";
};
const setFavicon = (documentType) => {
const favicon = document.getElementById("favicon");
if (!favicon) return;
let icon = null;
switch (documentType) {
case text:
icon = "text.ico";
break;
case presentation:
icon = "presentation.ico";
break;
case spreadSheet:
icon = "spreadsheet.ico";
break;
default:
break;
}
if (icon) favicon.href = `${homepage}/images/${icon}`;
};
const changeTitle = () => {
docSaved ? setDocumentTitle(docTitle) : setDocumentTitle(`*${docTitle}`);
};
const setDocumentTitle = (subTitle = null) => {
//const { isAuthenticated, settingsStore, product: currentModule } = auth;
//const { organizationName } = settingsStore;
const organizationName = "ONLYOFFICE"; //TODO: Replace to API variant
const moduleTitle = "Documents"; //TODO: Replace to API variant
let title;
if (subTitle) {
if (isAuthenticated && moduleTitle) {
title = subTitle + " - " + moduleTitle;
} else {
title = subTitle + " - " + organizationName;
}
} else if (moduleTitle && organizationName) {
title = moduleTitle + " - " + organizationName;
} else {
title = organizationName;
}
document.title = title;
};
const onLoad = () => {
try {
if (!window.DocsAPI) throw new Error("DocsAPI is not defined");
console.log("Editor config: ", config);
docTitle = config.document.title;
setFavicon(config.documentType);
setDocumentTitle(docTitle);
if (isMobile) {
config.type = "mobile";
}
let goBack;
if (fileInfo) {
const filterObj = FilesFilter.getDefault();
filterObj.folder = fileInfo.folderId;
const urlFilter = filterObj.toUrlParams();
goBack = {
blank: true,
requestClose: false,
text: i18n.t("FileLocation"),
url: `${combineUrl(filesUrl, `/filter?${urlFilter}`)}`,
};
}
config.editorConfig.customization = {
...config.editorConfig.customization,
goback: goBack,
};
if (personal && !fileInfo) {
//TODO: add conditions for SaaS
config.document.info.favorite = null;
}
if (url.indexOf("anchor") !== -1) {
const splitUrl = url.split("anchor=");
const decodeURI = decodeURIComponent(splitUrl[1]);
const obj = JSON.parse(decodeURI);
config.editorConfig.actionLink = {
action: obj.action,
};
}
if (successAuth) {
const documentType = config.documentType;
const fileExt =
documentType === text
? "docx"
: documentType === presentation
? "pptx"
: "xlsx";
const defaultFileName = getDefaultFileName(fileExt);
if (!user.isVisitor)
config.editorConfig.createUrl = combineUrl(
window.location.origin,
AppServerConfig.proxyURL,
"products/files/",
`/httphandlers/filehandler.ashx?action=create&doctype=text&title=${encodeURIComponent(
defaultFileName
)}`
);
}
let onRequestSharingSettings,
onRequestRename,
onRequestSaveAs,
onRequestInsertImage,
onRequestMailMergeRecipients,
onRequestCompareFile,
onRequestRestore;
if (isSharingAccess) {
onRequestSharingSettings = onSDKRequestSharingSettings;
}
if (fileInfo && fileInfo.canEdit && !fileInfo.providerKey) {
onRequestRename = onSDKRequestRename;
}
if (successAuth) {
onRequestSaveAs = onSDKRequestSaveAs;
onRequestInsertImage = onSDKRequestInsertImage;
onRequestMailMergeRecipients = onSDKRequestMailMergeRecipients;
onRequestCompareFile = onSDKRequestCompareFile;
}
if (!!config.document.permissions.changeHistory) {
onRequestRestore = onSDKRequestRestore;
}
const events = {
events: {
onAppReady: onSDKAppReady,
onDocumentStateChange: onDocumentStateChange,
onMetaChange: onMetaChange,
onDocumentReady: onDocumentReady,
onInfo: onSDKInfo,
onWarning: onSDKWarning,
onError: onSDKError,
onRequestSharingSettings,
onRequestRename,
onMakeActionLink: onMakeActionLink,
onRequestInsertImage,
onRequestSaveAs,
onRequestMailMergeRecipients,
onRequestCompareFile,
onRequestEditRights: onSDKRequestEditRights,
onRequestHistory: onSDKRequestHistory,
onRequestHistoryClose: onSDKRequestHistoryClose,
onRequestHistoryData: onSDKRequestHistoryData,
onRequestRestore,
},
};
const newConfig = Object.assign(config, events);
docEditor = window.DocsAPI.DocEditor("editor", newConfig);
assign(window, ["ASC", "Files", "Editor", "docEditor"], docEditor); //Do not remove: it's for Back button on Mobile App
} catch (error) {
console.log(error);
toastr.error(error.message, null, 0, true);
}
};
const onSDKRequestHistoryData = async (event) => {
const version = event.data;
try {
const versionDifference = await getEditDiff(fileId, version, doc);
const changesUrl = versionDifference.changesUrl;
const previous = versionDifference.previous;
const token = versionDifference.token;
docEditor.setHistoryData({
...(changesUrl && { changesUrl }),
key: versionDifference.key,
fileType: versionDifference.fileType,
...(previous && {
previous: {
fileType: previous.fileType,
key: previous.key,
url: previous.url,
},
}),
...(token && { token }),
url: versionDifference.url,
version,
});
} catch (e) {
docEditor.setHistoryData({
error: `${e}`, //TODO: maybe need to display something else.
version,
});
}
};
const onSDKRequestHistoryClose = () => {
document.location.reload();
};
const getDocumentHistory = (fileHistory, historyLength) => {
let result = [];
for (let i = 0; i < historyLength; i++) {
const changes = fileHistory[i].changes;
const serverVersion = fileHistory[i].serverVersion;
const version = fileHistory[i].version;
const versionGroup = fileHistory[i].versionGroup;
let obj = {
...(changes.length !== 0 && { changes }),
created: `${new Date(fileHistory[i].created).toLocaleString(
config.editorConfig.lang
)}`,
...(serverVersion && { serverVersion }),
key: fileHistory[i].key,
user: {
id: fileHistory[i].user.id,
name: fileHistory[i].user.name,
},
version,
versionGroup,
};
result.push(obj);
}
return result;
};
const getCurrentDocumentVersion = (fileHistory, historyLength) => {
return url.indexOf("&version=") !== -1
? +url.split("&version=")[1]
: fileHistory[historyLength - 1].version;
};
const onSDKRequestHistory = async () => {
try {
const fileHistory = await getEditHistory(fileId, doc);
const historyLength = fileHistory.length;
docEditor.refreshHistory({
currentVersion: getCurrentDocumentVersion(fileHistory, historyLength),
history: getDocumentHistory(fileHistory, historyLength),
});
} catch (e) {
docEditor.refreshHistory({
error: `${e}`, //TODO: maybe need to display something else.
});
}
};
const onSDKRequestRestore = async (event) => {
const restoreVersion = event.data.version;
try {
const updateVersions = await restoreDocumentsVersion(
fileId,
restoreVersion,
doc
);
const historyLength = updateVersions.length;
docEditor.refreshHistory({
currentVersion: getCurrentDocumentVersion(
updateVersions,
historyLength
),
history: getDocumentHistory(updateVersions, historyLength),
});
} catch (e) {
docEditor.refreshHistory({
error: `${e}`, //TODO: maybe need to display something else.
});
}
};
const onSDKAppReady = () => {
console.log("ONLYOFFICE Document Editor is ready");
const tempElm = document.getElementById("loader");
if (tempElm) {
tempElm.outerHTML = "";
}
const index = url.indexOf("#message/");
if (index > -1) {
const splitUrl = url.split("#message/");
if (splitUrl.length === 2) {
let raw = splitUrl[1]?.trim();
const message = decodeURIComponent(raw).replace(/\+/g, " ");
docEditor.showMessage(message);
history.pushState({}, null, url.substring(0, index));
}
} else {
if (config?.Error) docEditor.showMessage(config.Error);
}
};
const onSDKInfo = (event) => {
console.log(
"ONLYOFFICE Document Editor is opened in mode " + event.data.mode
);
};
const onSDKRequestEditRights = async () => {
console.log("ONLYOFFICE Document Editor requests editing rights");
const index = url.indexOf("&action=view");
if (index) {
let convertUrl = url.substring(0, index);
if (canConvert(fileInfo.fileExst)) {
const newUrl = await convertDocumentUrl();
if (newUrl) {
convertUrl = newUrl.webUrl;
}
}
history.pushState({}, null, convertUrl);
document.location.reload();
}
};
const [isVisible, setIsVisible] = useState(false);
const [isFileDialogVisible, setIsFileDialogVisible] = useState(false);
const [isFolderDialogVisible, setIsFolderDialogVisible] = useState(false);
const [filesType, setFilesType] = useState("");
const onSDKRequestSharingSettings = () => {
setIsVisible(true);
};
const onSDKRequestRename = (event) => {
const title = event.data;
updateFile(fileId, title);
};
const onMakeActionLink = (event) => {
var ACTION_DATA = event.data;
const link = generateLink(ACTION_DATA);
const urlFormation = !actionLink ? url : url.split("&anchor=")[0];
const linkFormation = `${urlFormation}&anchor=${link}`;
docEditor.setActionLink(linkFormation);
};
const generateLink = (actionData) => {
return encodeURIComponent(JSON.stringify(actionData));
};
const onCancel = () => {
setIsVisible(false);
};
const onSDKWarning = (event) => {
console.log(
"ONLYOFFICE Document Editor reports a warning: code " +
event.data.warningCode +
", description " +
event.data.warningDescription
);
};
const onSDKError = (event) => {
console.log(
"ONLYOFFICE Document Editor reports an error: code " +
event.data.errorCode +
", description " +
event.data.errorDescription
);
};
const onDocumentStateChange = (event) => {
if (!documentIsReady) return;
docSaved = !event.data;
throttledChangeTitle();
};
const onDocumentReady = () => {
documentIsReady = true;
if (isSharingAccess) {
loadUsersRightsList();
}
};
const onMetaChange = (event) => {
const newTitle = event.data.title;
const favorite = event.data.favorite;
if (newTitle && newTitle !== docTitle) {
setDocumentTitle(newTitle);
docTitle = newTitle;
}
if (!newTitle) {
const onlyNumbers = new RegExp("^[0-9]+$");
const isFileWithoutProvider = onlyNumbers.test(fileId);
const convertFileId = isFileWithoutProvider
? +fileId
: decodeURIComponent(fileId);
favorite
? markAsFavorite([convertFileId])
.then(() => updateFavorite(favorite))
.catch((error) => console.log("error", error))
: removeFromFavorite([convertFileId])
.then(() => updateFavorite(favorite))
.catch((error) => console.log("error", error));
}
};
const onSDKRequestInsertImage = (event) => {
setTypeInsertImageAction(event.data);
setFilesType(insertImageAction);
setIsFileDialogVisible(true);
};
const onSDKRequestMailMergeRecipients = () => {
setFilesType(mailMergeAction);
setIsFileDialogVisible(true);
};
const onSDKRequestCompareFile = () => {
setFilesType(compareFilesAction);
setIsFileDialogVisible(true);
};
const onSelectFile = async (file) => {
try {
const link = await getPresignedUri(file.id);
if (filesType === insertImageAction) insertImage(link);
if (filesType === mailMergeAction) mailMerge(link);
if (filesType === compareFilesAction) compareFiles(link);
} catch (e) {
console.error(e);
}
};
const onCloseFileDialog = () => {
setIsFileDialogVisible(false);
};
const onSDKRequestSaveAs = (event) => {
setTitleSelectorFolder(event.data.title);
setUrlSelectorFolder(event.data.url);
setExtension(event.data.title.split(".").pop());
setIsFolderDialogVisible(true);
};
const onCloseFolderDialog = () => {
setIsFolderDialogVisible(false);
setNewOpenTab(false);
};
const getSavingInfo = async (title, folderId) => {
const savingInfo = await SaveAs(
title,
urlSelectorFolder,
folderId,
openNewTab
);
if (savingInfo) {
const convertedInfo = savingInfo.split(": ").pop();
docEditor.showMessage(convertedInfo);
}
};
const onClickSaveSelectFolder = (e, folderId) => {
const currentExst = titleSelectorFolder.split(".").pop();
const title =
currentExst !== extension
? titleSelectorFolder.concat(`.${extension}`)
: titleSelectorFolder;
if (openNewTab) {
SaveAs(title, urlSelectorFolder, folderId, openNewTab);
} else {
getSavingInfo(title, folderId);
}
};
const onChangeInput = (e) => {
setTitleSelectorFolder(e.target.value);
};
const onClickCheckbox = () => {
setNewOpenTab(!openNewTab);
};
const getFileTypeTranslation = () => {
switch (filesType) {
case mailMergeAction:
return i18n.t("MailMergeFileType");
case insertImageAction:
return i18n.t("ImageFileType");
case compareFilesAction:
return i18n.t("DocumentsFileType");
}
};
const selectFilesListTitle = () => {
return (
<>
{filesType === mailMergeAction ? (
getFileTypeTranslation()
) : (
<Trans i18n={i18n} i18nKey="SelectFilesType" ns="Editor">
Select files of type: {{ fileType: getFileTypeTranslation() }}
</Trans>
)}
</>
);
};
const insertImageActionProps = {
isImageOnly: true,
};
const mailMergeActionProps = {
isTablesOnly: true,
searchParam: ".xlsx",
};
const compareFilesActionProps = {
isDocumentsOnly: true,
};
const fileTypeDetection = () => {
if (filesType === insertImageAction) {
return insertImageActionProps;
}
if (filesType === mailMergeAction) {
return mailMergeActionProps;
}
if (filesType === compareFilesAction) {
return compareFilesActionProps;
}
};
return (
<ThemeProvider theme={theme}>
<Box
widthProp="100vw"
heightProp={isIPad() ? "calc(var(--vh, 1vh) * 100)" : "100vh"}
>
<Toast />
{!isLoading ? (
<>
<div id="editor"></div>
{isSharingAccess && isVisible && (
<SharingDialog
settings={settings} //TODO: Maybe init filesSettings in editor?
isVisible={isVisible}
sharingObject={fileInfo}
onCancel={onCancel}
onSuccess={loadUsersRightsList}
/>
)}
{isFileDialogVisible && (
<SelectFileDialog
settings={settings} //TODO: Maybe init filesSettings in editor?
resetTreeFolders
onSelectFile={onSelectFile}
isPanelVisible={isFileDialogVisible}
onClose={onCloseFileDialog}
foldersType="exceptPrivacyTrashFolders"
{...fileTypeDetection()}
filesListTitle={selectFilesListTitle()}
/>
)}
<SelectFolderDialog
folderId={fileInfo?.folderId}
isPanelVisible={isFolderDialogVisible}
onClose={onCloseFolderDialog}
foldersType="exceptSortedByTags"
onSave={onClickSaveSelectFolder}
isDisableButton={!titleSelectorFolder.trim()}
header={
<StyledSelectFolder>
<Text className="editor-select-folder_text" fontWeight={600}>
{i18n.t("FileName")}
</Text>
<TextInput
className="editor-select-folder_text-input"
scale
onChange={onChangeInput}
value={titleSelectorFolder}
/>
</StyledSelectFolder>
}
{...(extension !== "fb2" && {
footer: (
<StyledSelectFolder>
<Checkbox
className="editor-select-folder_checkbox"
label={i18n.t("OpenSavedDocument")}
onChange={onClickCheckbox}
isChecked={openNewTab}
/>
</StyledSelectFolder>
),
})}
/>
{preparationPortalDialogVisible && (
<PreparationPortalDialog
visible={preparationPortalDialogVisible}
/>
)}
</>
) : (
<></>
)}
</Box>
</ThemeProvider>
);
};
export default Editor;

View File

@ -1,8 +0,0 @@
import React from "react";
import ReactDOM from "react-dom";
import App from "./App";
import { registerSW } from "@appserver/common/sw/helper";
ReactDOM.render(<App />, document.getElementById("root"));
registerSW();

View File

@ -0,0 +1,75 @@
import React, { useEffect } from "react";
import Editor from "./components/Editor.js";
import { useSSR } from "react-i18next";
import useMfScripts from "./helpers/useMfScripts";
import {
combineUrl,
isRetina,
getCookie,
setCookie,
} from "@appserver/common/utils";
import { AppServerConfig } from "@appserver/common/constants";
import initDesktop from "./helpers/initDesktop";
import ErrorBoundary from "./components/ErrorBoundary";
import store from "studio/store";
import i18n from "./i18n";
import { I18nextProvider } from "react-i18next";
import { fonts } from "@appserver/common/fonts.js";
import GlobalStyle from "./components/GlobalStyle.js";
import { inject, observer, Provider as MobxProvider } from "mobx-react";
import ThemeProvider from "@appserver/components/theme-provider";
const isDesktopEditor = window["AscDesktopEditor"] !== undefined;
const ThemeProviderWrapper = inject(({ auth }) => {
const { settingsStore } = auth;
return { theme: settingsStore.theme };
})(observer(ThemeProvider));
const App = ({ initialLanguage, initialI18nStoreASC, ...rest }) => {
const [isInitialized, isErrorLoading] = useMfScripts();
useSSR(initialI18nStoreASC, initialLanguage);
useEffect(() => {
const tempElm = document.getElementById("loader");
if (tempElm && !rest.error && !rest.needLoader && rest.docApiUrl) {
tempElm.outerHTML = "";
}
if (isRetina() && getCookie("is_retina") == null) {
setCookie("is_retina", true, { path: "/" });
}
}, []);
const onError = () => {
console.log("Error");
window.open(
combineUrl(
AppServerConfig.proxyURL,
rest.personal ? "sign-in" : "/login"
),
"_self"
);
};
return (
<ErrorBoundary onError={onError}>
<MobxProvider {...store}>
<I18nextProvider i18n={i18n}>
<ThemeProviderWrapper>
<GlobalStyle fonts={fonts} />
<Editor
mfReady={isInitialized}
mfFailed={isErrorLoading}
isDesktopEditor={isDesktopEditor}
initDesktop={initDesktop}
{...rest}
/>
</ThemeProviderWrapper>
</I18nextProvider>
</MobxProvider>
</ErrorBoundary>
);
};
export default App;

View File

@ -0,0 +1,54 @@
import React from "react";
import { hydrate } from "react-dom";
import { registerSW } from "@appserver/common/sw/helper";
import App from "./App.js";
import pkg from "../../package.json";
import { initI18n } from "./helpers/utils.js";
const propsObj = window.__ASC_INITIAL_EDITOR_STATE__;
const initialI18nStoreASC = window.initialI18nStoreASC;
const initialLanguage = window.initialLanguage;
initI18n(initialI18nStoreASC);
hydrate(
<App
initialLanguage={initialLanguage}
initialI18nStoreASC={initialI18nStoreASC}
{...propsObj}
/>,
document.getElementById("root")
);
if (IS_DEVELOPMENT) {
const port = PORT || 5013;
const socketPath = pkg.socketPath;
const ws = new WebSocket(`ws://localhost:${port}${socketPath}`);
let isErrorConnection = false;
ws.onopen = (event) => {
console.log("[editor-dev] Socket is connected. Live reload enabled");
};
ws.onmessage = function (event) {
if (event.data === "reload") {
console.log("[editor-dev] App updated. Reloading...");
location.reload();
}
};
ws.onclose = function (event) {
console.log("[editor-dev] Socket is disconnected! Reloading...");
setTimeout(() => {
!isErrorConnection && location.reload();
}, 1500);
};
ws.onerror = (event) => {
isErrorConnection = true;
console.log("[editor-dev] Socket connect error!");
};
}
registerSW();

View File

@ -0,0 +1,116 @@
import React from "react";
// import AppLoader from "@appserver/common/components/AppLoader";
// import ErrorBoundary from "@appserver/common/components/ErrorBoundary";
// import Error520 from "studio/Error520";
// import Error404 from "studio/Error404";
export function loadComponent(scope, module) {
return async () => {
// Initializes the share scope. This fills it with known provided modules from this build and all remotes
await __webpack_init_sharing__("default");
const container = window[scope]; // or get the container somewhere else
// Initialize the container, it may provide shared modules
await container.init(__webpack_share_scopes__.default);
const factory = await window[scope].get(module);
const Module = factory();
return Module;
};
}
export const useDynamicScript = (args) => {
const [ready, setReady] = React.useState(false);
const [failed, setFailed] = React.useState(false);
React.useEffect(() => {
if (!args.url) {
return;
}
const exists = document.getElementById(args.id);
if (exists || args?.isInit) {
setReady(true);
setFailed(false);
return;
}
const element = document.createElement("script");
element.id = args.id;
element.src = args.url;
element.type = "text/javascript";
element.async = true;
setReady(false);
setFailed(false);
element.onload = () => {
console.log(`Dynamic Script Loaded: ${args.url}`);
setReady(true);
};
element.onerror = () => {
console.error(`Dynamic Script Error: ${args.url}`);
setReady(false);
setFailed(true);
};
document.head.appendChild(element);
//TODO: Comment if you don't want to remove loaded remoteEntry
// return () => {
// console.log(`Dynamic Script Removed: ${args.url}`);
// document.head.removeChild(element);
// };
}, [args.url]);
return {
ready,
failed,
};
};
const DynamicComponent = ({ system, needProxy, ...rest }) => {
const [isInitialized, setIsInitialized] = React.useState(false);
const [LoadedComponent, setLoadedComponent] = React.useState();
const { ready, failed } = useDynamicScript({
url: system && system.url,
id: system && system.scope,
isInit: isInitialized,
});
if (!system) {
console.log(`Not system specified`);
throw Error("Not system specified");
}
if (!ready) {
console.log(`Loading dynamic script: ${system.url}`);
return <div className={rest.className} />;
}
if (failed) {
console.log(`Failed to load dynamic script: ${system.url}`);
throw Error("failed");
}
if (ready && !isInitialized) {
setIsInitialized(true);
const Component = React.lazy(loadComponent(system.scope, system.module));
setLoadedComponent(Component);
}
const Component = React.lazy(loadComponent(system.scope, system.module));
return (
<React.Suspense fallback={<div />}>
{needProxy
? LoadedComponent && <LoadedComponent {...rest} />
: Component && <Component {...rest} />}
</React.Suspense>
);
};
export default DynamicComponent;

View File

@ -0,0 +1,576 @@
import React, { useState, useEffect } from "react";
import { isMobile, isIOS, deviceType } from "react-device-detect";
import FilesFilter from "@appserver/common/api/files/filter";
import combineUrl from "@appserver/common/utils/combineUrl";
import { AppServerConfig } from "@appserver/common/constants";
import throttle from "lodash/throttle";
import Toast from "@appserver/components/toast";
import { toast } from "react-toastify";
import {
restoreDocumentsVersion,
markAsFavorite,
removeFromFavorite,
getEditDiff,
getEditHistory,
updateFile,
checkFillFormDraft,
} from "@appserver/common/api/files";
import { EditorWrapper } from "../components/StyledEditor";
import { useTranslation } from "react-i18next";
import withDialogs from "../helpers/withDialogs";
import { canConvert, convertDocumentUrl } from "../helpers/utils";
import { assign } from "@appserver/common/utils";
toast.configure();
const onSDKInfo = (event) => {
console.log(
"ONLYOFFICE Document Editor is opened in mode " + event.data.mode
);
};
const onSDKWarning = (event) => {
console.log(
"ONLYOFFICE Document Editor reports a warning: code " +
event.data.warningCode +
", description " +
event.data.warningDescription
);
};
const onSDKError = (event) => {
console.log(
"ONLYOFFICE Document Editor reports an error: code " +
event.data.errorCode +
", description " +
event.data.errorDescription
);
};
const text = "text";
const presentation = "presentation";
let documentIsReady = false;
let docSaved = null;
let docTitle = null;
let docEditor;
function Editor({
config,
personal,
successAuth,
isSharingAccess,
user,
doc,
actionLink,
error,
sharingDialog,
onSDKRequestSharingSettings,
loadUsersRightsList,
isVisible,
selectFileDialog,
onSDKRequestInsertImage,
onSDKRequestMailMergeRecipients,
onSDKRequestCompareFile,
selectFolderDialog,
onSDKRequestSaveAs,
isFileDialogVisible,
isFolderDialogVisible,
isDesktopEditor,
initDesktop,
view,
preparationPortalDialog,
mfReady,
...rest
}) {
const [fileInfo, setFileInfo] = useState(rest.fileInfo);
const [url, setUrl] = useState(rest.url);
const [fileId, setFileId] = useState(rest.fileId);
const [version, setVersion] = useState(rest.version);
const { t } = useTranslation(["Editor", "Common"]);
useEffect(() => {
if (error && mfReady) {
if (error?.unAuthorized && error?.redirectPath) {
localStorage.setItem("redirectPath", window.location.href);
window.location.href = error?.redirectPath;
}
const errorText = typeof error === "string" ? error : error.errorMessage;
window.toastr.error(errorText);
}
}, [mfReady, error]);
useEffect(() => {
if (config) {
document.getElementById("scripDocServiceAddress").onload = onLoad();
setDocumentTitle(config?.document?.title);
if (isIOS && deviceType === "tablet") {
const vh = window.innerHeight * 0.01;
document.documentElement.style.setProperty("--vh", `${vh}px`);
}
if (
!view &&
fileInfo &&
fileInfo.canWebRestrictedEditing &&
fileInfo.canFillForms &&
!fileInfo.canEdit
) {
try {
initForm();
} catch (err) {
console.error(err);
}
}
if (view) {
config.editorConfig.mode = "view";
}
}
}, []);
useEffect(() => {
if (config) {
if (isDesktopEditor) {
initDesktop(config, user, fileId, t);
}
}
}, [isDesktopEditor]);
useEffect(() => {
try {
const url = window.location.href;
if (
successAuth &&
url.indexOf("#message/") > -1 &&
fileInfo &&
fileInfo?.fileExst &&
canConvert(fileInfo.fileExst)
) {
showDocEditorMessage(url);
}
} catch (err) {
console.error(err);
}
}, [url, fileInfo?.fileExst]);
const initForm = async () => {
const formUrl = await checkFillFormDraft(fileId);
history.pushState({}, null, formUrl);
document.location.reload();
};
const showDocEditorMessage = async (url) => {
const result = await convertDocumentUrl();
const splitUrl = url.split("#message/");
if (result) {
const newUrl = `${result.webUrl}#message/${splitUrl[1]}`;
history.pushState({}, null, newUrl);
setFileInfo(result);
setUrl(newUrl);
setFileId(fileId);
setVersion(version);
}
};
const getDefaultFileName = (format) => {
switch (format) {
case "docx":
return t("NewDocument");
case "xlsx":
return t("NewSpreadsheet");
case "pptx":
return t("NewPresentation");
case "docxf":
return t("NewMasterForm");
default:
return t("NewFolder");
}
};
const throttledChangeTitle = throttle(() => changeTitle(), 500);
const onSDKRequestHistoryClose = () => {
document.location.reload();
};
const onSDKRequestEditRights = async () => {
console.log("ONLYOFFICE Document Editor requests editing rights");
const index = url.indexOf("&action=view");
if (index) {
let convertUrl = url.substring(0, index);
if (canConvert(fileInfo.fileExst)) {
const newUrl = await convertDocumentUrl();
if (newUrl) {
convertUrl = newUrl.webUrl;
}
}
history.pushState({}, null, convertUrl);
document.location.reload();
}
};
const onMakeActionLink = (event) => {
const actionData = event.data;
const link = generateLink(actionData);
const urlFormation = !actionLink ? url : url.split("&anchor=")[0];
const linkFormation = `${urlFormation}&anchor=${link}`;
docEditor.setActionLink(linkFormation);
};
const generateLink = (actionData) => {
return encodeURIComponent(JSON.stringify(actionData));
};
const onSDKRequestRename = (event) => {
const title = event.data;
updateFile(fileInfo.id, title);
};
const onSDKRequestRestore = async (event) => {
const restoreVersion = event.data.version;
try {
const updateVersions = await restoreDocumentsVersion(
fileId,
restoreVersion,
doc
);
const historyLength = updateVersions.length;
docEditor.refreshHistory({
currentVersion: getCurrentDocumentVersion(
updateVersions,
historyLength
),
history: getDocumentHistory(updateVersions, historyLength),
});
} catch (e) {
docEditor.refreshHistory({
error: `${e}`, //TODO: maybe need to display something else.
});
}
};
const getDocumentHistory = (fileHistory, historyLength) => {
let result = [];
for (let i = 0; i < historyLength; i++) {
const changes = fileHistory[i].changes;
const serverVersion = fileHistory[i].serverVersion;
const version = fileHistory[i].version;
const versionGroup = fileHistory[i].versionGroup;
let obj = {
...(changes.length !== 0 && { changes }),
created: `${new Date(fileHistory[i].created).toLocaleString(
config.editorConfig.lang
)}`,
...(serverVersion && { serverVersion }),
key: fileHistory[i].key,
user: {
id: fileHistory[i].user.id,
name: fileHistory[i].user.name,
},
version,
versionGroup,
};
result.push(obj);
}
return result;
};
const getCurrentDocumentVersion = (fileHistory, historyLength) => {
return url.indexOf("&version=") !== -1
? +url.split("&version=")[1]
: fileHistory[historyLength - 1].version;
};
const onSDKRequestHistory = async () => {
try {
const fileHistory = await getEditHistory(fileId, doc);
const historyLength = fileHistory.length;
docEditor.refreshHistory({
currentVersion: getCurrentDocumentVersion(fileHistory, historyLength),
history: getDocumentHistory(fileHistory, historyLength),
});
} catch (e) {
docEditor.refreshHistory({
error: `${e}`, //TODO: maybe need to display something else.
});
}
};
const onSDKRequestHistoryData = async (event) => {
const version = event.data;
try {
const versionDifference = await getEditDiff(fileId, version, doc);
const changesUrl = versionDifference.changesUrl;
const previous = versionDifference.previous;
const token = versionDifference.token;
docEditor.setHistoryData({
...(changesUrl && { changesUrl }),
key: versionDifference.key,
fileType: versionDifference.fileType,
...(previous && {
previous: {
fileType: previous.fileType,
key: previous.key,
url: previous.url,
},
}),
...(token && { token }),
url: versionDifference.url,
version,
});
} catch (e) {
docEditor.setHistoryData({
error: `${e}`, //TODO: maybe need to display something else.
version,
});
}
};
const onDocumentReady = () => {
documentIsReady = true;
if (isSharingAccess) {
loadUsersRightsList(docEditor);
}
};
const updateFavorite = (favorite) => {
docEditor.setFavorite(favorite);
};
const onMetaChange = (event) => {
const newTitle = event.data.title;
const favorite = event.data.favorite;
if (newTitle && newTitle !== docTitle) {
setDocumentTitle(newTitle);
docTitle = newTitle;
}
if (!newTitle) {
const onlyNumbers = new RegExp("^[0-9]+$");
const isFileWithoutProvider = onlyNumbers.test(fileId);
const convertFileId = isFileWithoutProvider ? +fileId : fileId;
favorite
? markAsFavorite([convertFileId])
.then(() => updateFavorite(favorite))
.catch((error) => console.log("error", error))
: removeFromFavorite([convertFileId])
.then(() => updateFavorite(favorite))
.catch((error) => console.log("error", error));
}
};
const setDocumentTitle = (subTitle = null) => {
//const { isAuthenticated, settingsStore, product: currentModule } = auth;
//const { organizationName } = settingsStore;
const organizationName = "ONLYOFFICE"; //TODO: Replace to API variant
const moduleTitle = "Documents"; //TODO: Replace to API variant
let title;
if (subTitle) {
if (successAuth && moduleTitle) {
title = subTitle + " - " + moduleTitle;
} else {
title = subTitle + " - " + organizationName;
}
} else if (moduleTitle && organizationName) {
title = moduleTitle + " - " + organizationName;
} else {
title = organizationName;
}
document.title = title;
};
const changeTitle = () => {
docSaved ? setDocumentTitle(docTitle) : setDocumentTitle(`*${docTitle}`);
};
const onDocumentStateChange = (event) => {
if (!documentIsReady) return;
docSaved = !event.data;
throttledChangeTitle();
}; //+++
const onSDKAppReady = () => {
console.log("ONLYOFFICE Document Editor is ready");
const url = window.location.href;
const index = url.indexOf("#message/");
if (index > -1) {
const splitUrl = url.split("#message/");
if (splitUrl.length === 2) {
const message = decodeURIComponent(raw).replace(/\+/g, " ");
docEditor.showMessage(message);
history.pushState({}, null, url.substring(0, index));
} else {
if (config?.Error) docEditor.showMessage(config.Error);
}
}
};
const onLoad = () => {
try {
if (!window.DocsAPI) throw new Error("DocsAPI is not defined");
if (isMobile) {
config.type = "mobile";
}
let goBack;
const url = window.location.href;
if (fileInfo) {
const filterObj = FilesFilter.getDefault();
filterObj.folder = fileInfo.folderId;
const urlFilter = filterObj.toUrlParams();
const filesUrl = url.substring(0, url.indexOf("/doceditor"));
goBack = {
blank: true,
requestClose: false,
text: t("FileLocation"),
url: `${combineUrl(filesUrl, `/filter?${urlFilter}`)}`,
};
}
config.editorConfig.customization = {
...config.editorConfig.customization,
goback: goBack,
};
if (personal && !fileInfo) {
//TODO: add conditions for SaaS
config.document.info.favorite = null;
}
if (url.indexOf("anchor") !== -1) {
const splitUrl = url.split("anchor=");
const decodeURI = decodeURIComponent(splitUrl[1]);
const obj = JSON.parse(decodeURI);
config.editorConfig.actionLink = {
action: obj.action,
};
}
if (successAuth) {
const documentType = config.documentType;
const fileExt =
documentType === text
? "docx"
: documentType === presentation
? "pptx"
: "xlsx";
const defaultFileName = getDefaultFileName(fileExt);
if (!user.isVisitor)
config.editorConfig.createUrl = combineUrl(
window.location.origin,
AppServerConfig.proxyURL,
"products/files/",
`/httphandlers/filehandler.ashx?action=create&doctype=text&title=${encodeURIComponent(
defaultFileName
)}`
);
}
let onRequestSharingSettings,
onRequestRename,
onRequestSaveAs,
onRequestInsertImage,
onRequestMailMergeRecipients,
onRequestCompareFile,
onRequestRestore;
if (isSharingAccess) {
onRequestSharingSettings = onSDKRequestSharingSettings;
}
if (fileInfo && fileInfo.canEdit) {
onRequestRename = onSDKRequestRename;
}
if (successAuth) {
onRequestSaveAs = onSDKRequestSaveAs;
onRequestInsertImage = onSDKRequestInsertImage;
onRequestMailMergeRecipients = onSDKRequestMailMergeRecipients;
onRequestCompareFile = onSDKRequestCompareFile;
}
if (!!config.document.permissions.changeHistory) {
onRequestRestore = onSDKRequestRestore;
}
const events = {
events: {
onAppReady: onSDKAppReady,
onDocumentStateChange: onDocumentStateChange,
onMetaChange: onMetaChange,
onDocumentReady: onDocumentReady,
onInfo: onSDKInfo,
onWarning: onSDKWarning,
onError: onSDKError,
onRequestSharingSettings,
onRequestRename,
onMakeActionLink: onMakeActionLink,
onRequestInsertImage,
onRequestSaveAs,
onRequestMailMergeRecipients,
onRequestCompareFile,
onRequestEditRights: onSDKRequestEditRights,
onRequestHistory: onSDKRequestHistory,
onRequestHistoryClose: onSDKRequestHistoryClose,
onRequestHistoryData: onSDKRequestHistoryData,
onRequestRestore,
},
};
const newConfig = Object.assign(config, events);
docEditor = window.docEditor = window.DocsAPI.DocEditor(
"editor",
newConfig
);
assign(window, ["ASC", "Files", "Editor", "docEditor"], docEditor); //Do not remove: it's for Back button on Mobile App
} catch (error) {
window.toastr.error(error.message, null, 0, true);
}
};
return (
<EditorWrapper isVisibleSharingDialog={isVisible}>
<div id="editor"></div>
{sharingDialog}
{selectFileDialog}
{selectFolderDialog}
{preparationPortalDialog}
<Toast />
</EditorWrapper>
);
}
export default withDialogs(Editor);

View File

@ -0,0 +1,47 @@
import React from "react";
import PropTypes from "prop-types";
import ErrorContainer from "@appserver/common/components/ErrorContainer";
import { useTranslation } from "react-i18next";
const Error520 = ({ match }) => {
const { t } = useTranslation(["Common"]);
const { error } = (match && match.params) || {};
return (
<ErrorContainer headerText={t("SomethingWentWrong")} bodyText={error} />
);
};
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false };
}
// eslint-disable-next-line no-unused-vars
static getDerivedStateFromError(error) {
// Update state so the next render will show the fallback UI.
return { hasError: true };
}
componentDidCatch(error, errorInfo) {
// You can also log the error to an error reporting service
console.error(error, errorInfo);
this.props.onError && this.props.onError();
}
render() {
if (this.state.hasError) {
// You can render any custom fallback UI
return <Error520 />;
}
return this.props.children;
}
}
ErrorBoundary.propTypes = {
children: PropTypes.any,
onError: PropTypes.func,
};
export default ErrorBoundary;

View File

@ -0,0 +1,30 @@
import { createGlobalStyle } from "styled-components";
const GlobalStyle = createGlobalStyle`
html,
body {
height: 100%;
${(props) => props?.fonts}
font-family: 'Open Sans', sans-serif, Arial;
font-size: 13px;
}
#root {
min-height: 100%;
.pageLoader {
position: fixed;
left: calc(50% - 20px);
top: 35%;
}
}
body {
margin: 0;
}
body.loading * {
cursor: wait !important;
}
`;
export default GlobalStyle;

View File

@ -20,4 +20,12 @@ const StyledSelectFile = styled.div`
}
`;
export { StyledSelectFolder, StyledSelectFile };
const EditorWrapper = styled.div`
height: 100vh;
.dynamic-sharing-dialog {
${(props) => !props.isVisibleSharingDialog && "display: none"}
}
`;
export { StyledSelectFolder, StyledSelectFile, EditorWrapper };

View File

@ -0,0 +1,4 @@
export const FILES_SCOPE = "files";
export const FILES_REMOTE_ENTRY_URL = "/products/files/remoteEntry.js";
export const STUDIO_SCOPE = "studio";
export const STUDIO_REMOTE_ENTRY_URL = "/remoteEntry.js";

View File

@ -0,0 +1,39 @@
import {
setEncryptionKeys,
getEncryptionAccess,
} from "@appserver/common/api/files";
import { regDesktop } from "@appserver/common/desktop";
const initDesktop = (cfg, user, fileId, t) => {
const encryptionKeys = cfg?.editorConfig?.encryptionKeys;
regDesktop(
user,
!!encryptionKeys,
encryptionKeys,
(keys) => {
setEncryptionKeys(keys);
},
true,
(callback) => {
getEncryptionAccess(fileId)
.then((keys) => {
var data = {
keys,
};
callback(data);
})
.catch((error) => {
window?.toastr.error(
typeof error === "string" ? error : error.message,
null,
0,
true
);
});
},
t
);
};
export default initDesktop;

View File

@ -0,0 +1,55 @@
import React from "react";
import {
loadComponent,
useDynamicScript,
} from "../components/DynamicComponent";
import {
STUDIO_SCOPE,
FILES_SCOPE,
STUDIO_REMOTE_ENTRY_URL,
FILES_REMOTE_ENTRY_URL,
} from "../helpers/constants";
function useMfScripts() {
const [isInitialized, setIsInitialized] = React.useState(false);
const [isError, setIsError] = React.useState(false);
const { ready: filesReady, failed: filesFailed } = useDynamicScript({
id: FILES_SCOPE,
url: FILES_REMOTE_ENTRY_URL,
});
const { ready: studioReady, failed: studioFailed } = useDynamicScript({
id: STUDIO_SCOPE,
url: STUDIO_REMOTE_ENTRY_URL,
});
React.useEffect(() => {
if (filesReady && studioReady) {
initMfScripts();
}
if (studioFailed || filesFailed) {
setIsError(true);
setIsInitialized(false);
}
}, [filesReady, studioReady]);
const initMfScripts = async () => {
const SharingDialog = await loadComponent(FILES_SCOPE, "./SharingDialog")();
const toastr = await loadComponent(STUDIO_SCOPE, "./toastr")();
const filesUtils = await loadComponent(FILES_SCOPE, "./utils")();
const authStore = await loadComponent(STUDIO_SCOPE, "./store")();
window.toastr = toastr.default;
window.filesUtils = filesUtils;
window.SharingDialog = SharingDialog.default;
window.authStore = authStore.default;
setIsInitialized(true);
setIsError(false);
};
return [isInitialized, isError];
}
export default useMfScripts;

View File

@ -0,0 +1,39 @@
import { convertFile } from "@appserver/common/api/files";
import pkg from "../../../package.json";
export const canConvert = (extension, filesSettings) => {
const array = filesSettings?.extsMustConvert || [];
const result = array.findIndex((item) => item === extension);
return result === -1 ? false : true;
};
export const convertDocumentUrl = async () => {
const convert = await convertFile(fileId, null, true);
return convert && convert[0]?.result;
};
export const initI18n = (initialI18nStoreASC) => {
if (!initialI18nStoreASC || window.i18n) return;
const { homepage } = pkg;
window.i18n = {};
window.i18n.inLoad = [];
window.i18n.loaded = {};
for (let lng in initialI18nStoreASC) {
for (let ns in initialI18nStoreASC[lng]) {
if (ns === "Common") {
window.i18n.loaded[`/static/locales/${lng}/${ns}.json`] = {
namespaces: ns,
data: initialI18nStoreASC[lng][ns],
};
} else {
window.i18n.loaded[`${homepage}/locales/${lng}/${ns}.json`] = {
namespaces: ns,
data: initialI18nStoreASC[lng][ns],
};
}
}
}
};

View File

@ -0,0 +1,344 @@
import React, { useState, useEffect } from "react";
import DynamicComponent from "../components/DynamicComponent";
import { getPresignedUri } from "@appserver/common/api/files";
import {
FILES_REMOTE_ENTRY_URL,
FILES_SCOPE,
STUDIO_SCOPE,
STUDIO_REMOTE_ENTRY_URL,
} from "./constants";
import Text from "@appserver/components/text";
import TextInput from "@appserver/components/text-input";
import Checkbox from "@appserver/components/checkbox";
import { StyledSelectFolder } from "../components/StyledEditor";
import { useTranslation } from "react-i18next";
const insertImageAction = "imageFileType";
const mailMergeAction = "mailMergeFileType";
const compareFilesAction = "documentsFileType";
const withDialogs = (WrappedComponent) => {
return (props) => {
const [isVisible, setIsVisible] = useState(false);
const [filesType, setFilesType] = useState("");
const [isFileDialogVisible, setIsFileDialogVisible] = useState(false);
const [typeInsertImageAction, setTypeInsertImageAction] = useState();
const [isFolderDialogVisible, setIsFolderDialogVisible] = useState(false);
const [titleSelectorFolder, setTitleSelectorFolder] = useState("");
const [urlSelectorFolder, setUrlSelectorFolder] = useState("");
const [extension, setExtension] = useState();
const [openNewTab, setNewOpenTab] = useState(false);
const [
preparationPortalDialogVisible,
setPreparationPortalDialogVisible,
] = useState(false);
const { t } = useTranslation(["Editor", "Common"]);
const { fileInfo, fileId, mfReady } = props;
useEffect(() => {
if (window.authStore) {
initSocketHelper();
}
}, [mfReady]);
const initSocketHelper = async () => {
await window.authStore.auth.init(true);
const { socketHelper } = window.authStore.auth.settingsStore;
socketHelper.emit({
command: "subscribe",
data: "backup-restore",
});
socketHelper.on("restore-backup", () => {
setPreparationPortalDialogVisible(true);
});
};
const onSDKRequestSharingSettings = () => {
setIsVisible(true);
};
const onCancel = () => {
setIsVisible(false);
};
const loadUsersRightsList = () => {
window.SharingDialog.getSharingSettings(fileId).then(
(sharingSettings) => {
window.docEditor.setSharingSettings({
sharingSettings,
});
}
);
};
const onCloseFileDialog = () => {
setIsFileDialogVisible(false);
};
const onSDKRequestCompareFile = () => {
setFilesType(compareFilesAction);
setIsFileDialogVisible(true);
};
const onSDKRequestMailMergeRecipients = () => {
setFilesType(mailMergeAction);
setIsFileDialogVisible(true);
};
const onSDKRequestInsertImage = (event) => {
setTypeInsertImageAction(event.data);
setFilesType(insertImageAction);
setIsFileDialogVisible(true);
};
const insertImage = (link) => {
const token = link.token;
window.docEditor.insertImage({
...typeInsertImageAction,
fileType: link.filetype,
...(token && { token }),
url: link.url,
});
};
const mailMerge = (link) => {
const token = link.token;
window.docEditor.setMailMergeRecipients({
fileType: link.filetype,
...(token && { token }),
url: link.url,
});
};
const compareFiles = (link) => {
const token = link.token;
window.docEditor.setRevisedFile({
fileType: link.filetype,
...(token && { token }),
url: link.url,
});
};
const insertImageActionProps = {
isImageOnly: true,
};
const mailMergeActionProps = {
isTablesOnly: true,
searchParam: ".xlsx",
};
const compareFilesActionProps = {
isDocumentsOnly: true,
};
const fileTypeDetection = () => {
if (filesType === insertImageAction) {
return insertImageActionProps;
}
if (filesType === mailMergeAction) {
return mailMergeActionProps;
}
if (filesType === compareFilesAction) {
return compareFilesActionProps;
}
};
const onSelectFile = async (file) => {
try {
const link = await getPresignedUri(file.id);
if (filesType === insertImageAction) insertImage(link);
if (filesType === mailMergeAction) mailMerge(link);
if (filesType === compareFilesAction) compareFiles(link);
} catch (e) {
console.error(e);
}
};
const getFileTypeTranslation = () => {
switch (filesType) {
case mailMergeAction:
return t("MailMergeFileType");
case insertImageAction:
return t("ImageFileType");
case compareFilesAction:
return t("DocumentsFileType");
}
};
const selectFilesListTitle = () => {
const type = getFileTypeTranslation();
return filesType === mailMergeAction
? type
: t("SelectFilesType", { fileType: type });
};
const onSDKRequestSaveAs = (event) => {
setTitleSelectorFolder(event.data.title);
setUrlSelectorFolder(event.data.url);
setExtension(event.data.title.split(".").pop());
setIsFolderDialogVisible(true);
};
const onCloseFolderDialog = () => {
setIsFolderDialogVisible(false);
setNewOpenTab(false);
};
const getSavingInfo = async (title, folderId) => {
const savingInfo = await window.filesUtils.SaveAs(
title,
urlSelectorFolder,
folderId,
openNewTab
);
if (savingInfo) {
const convertedInfo = savingInfo.split(": ").pop();
docEditor.showMessage(convertedInfo);
}
};
const onClickSaveSelectFolder = (e, folderId) => {
const currentExst = titleSelectorFolder.split(".").pop();
const title =
currentExst !== extension
? titleSelectorFolder.concat(`.${extension}`)
: titleSelectorFolder;
if (openNewTab) {
window.filesUtils.SaveAs(
title,
urlSelectorFolder,
folderId,
openNewTab
);
} else {
getSavingInfo(title, folderId);
}
};
const onClickCheckbox = () => {
setNewOpenTab(!openNewTab);
};
const onChangeInput = (e) => {
setTitleSelectorFolder(e.target.value);
};
const sharingDialog = mfReady && (
<DynamicComponent
className="dynamic-sharing-dialog"
system={{
scope: FILES_SCOPE,
url: FILES_REMOTE_ENTRY_URL,
module: "./SharingDialog",
name: "SharingDialog",
}}
isVisible={isVisible}
sharingObject={fileInfo}
onCancel={onCancel}
onSuccess={loadUsersRightsList}
settings={props.filesSettings}
/>
);
const selectFileDialog = mfReady && props.successAuth && (
<DynamicComponent
system={{
scope: FILES_SCOPE,
url: FILES_REMOTE_ENTRY_URL,
module: "./SelectFileDialog",
}}
resetTreeFolders
foldersType="exceptPrivacyTrashFolders"
isPanelVisible={isFileDialogVisible}
onSelectFile={onSelectFile}
onClose={onCloseFileDialog}
{...fileTypeDetection()}
filesListTitle={selectFilesListTitle()}
settings={props.filesSettings}
/>
);
const selectFolderDialog = mfReady && props.successAuth && (
<DynamicComponent
system={{
scope: FILES_SCOPE,
url: FILES_REMOTE_ENTRY_URL,
module: "./SelectFolderDialog",
}}
needProxy
folderId={fileInfo?.folderId}
isPanelVisible={isFolderDialogVisible}
onClose={onCloseFolderDialog}
foldersType="exceptSortedByTags"
onSave={onClickSaveSelectFolder}
isDisableButton={!titleSelectorFolder.trim()}
header={
<StyledSelectFolder>
<Text className="editor-select-folder_text">{t("FileName")}</Text>
<TextInput
className="editor-select-folder_text-input"
scale
onChange={onChangeInput}
value={titleSelectorFolder}
/>
</StyledSelectFolder>
}
{...(extension !== "fb2" && {
footer: (
<StyledSelectFolder>
<Checkbox
className="editor-select-folder_checkbox"
label={t("OpenSavedDocument")}
onChange={onClickCheckbox}
isChecked={openNewTab}
/>
</StyledSelectFolder>
),
})}
/>
);
const preparationPortalDialog = mfReady && (
<DynamicComponent
system={{
scope: STUDIO_SCOPE,
url: STUDIO_REMOTE_ENTRY_URL,
module: "./PreparationPortalDialog",
}}
visible={preparationPortalDialogVisible}
/>
);
return (
<WrappedComponent
{...props}
sharingDialog={sharingDialog}
onSDKRequestSharingSettings={onSDKRequestSharingSettings}
loadUsersRightsList={loadUsersRightsList}
isVisible={isVisible}
selectFileDialog={selectFileDialog}
onSDKRequestInsertImage={onSDKRequestInsertImage}
onSDKRequestMailMergeRecipients={onSDKRequestMailMergeRecipients}
onSDKRequestCompareFile={onSDKRequestCompareFile}
isFileDialogVisible={isFileDialogVisible}
selectFolderDialog={selectFolderDialog}
onSDKRequestSaveAs={onSDKRequestSaveAs}
isFolderDialogVisible={isFolderDialogVisible}
preparationPortalDialog={preparationPortalDialog}
/>
);
};
};
export default withDialogs;

View File

@ -1,7 +1,7 @@
import i18n from "i18next";
import { initReactI18next } from "react-i18next";
import Backend from "@appserver/common/utils/i18next-http-backend";
import config from "../package.json";
import config from "../../package.json";
import { LANGUAGE } from "@appserver/common/constants";
import { loadLanguagePath } from "@appserver/common/utils";

View File

@ -1,24 +0,0 @@
// Override default variables before the import
//$font-family-base: "Open Sans", sans-serif;
html,
body {
height: 100%;
}
#root {
min-height: 100%;
.pageLoader {
position: fixed;
left: calc(50% - 20px);
top: 35%;
}
}
body {
margin: 0;
}
body.loading * {
cursor: wait !important;
}

View File

@ -0,0 +1,182 @@
import express from "express";
import template from "./lib/template";
import devMiddleware from "./lib/middleware/devMiddleware";
import i18nextMiddleware from "i18next-express-middleware";
import i18next from "i18next";
import Backend from "i18next-fs-backend";
import path from "path";
import compression from "compression";
import ws from "./lib/websocket";
import fs from "fs";
import logger from "morgan";
import winston from "./lib/logger.js";
import { getAssets, initDocEditor } from "./lib/helpers";
import GlobalStyle from "../client/components/GlobalStyle.js";
import { I18nextProvider } from "react-i18next";
import React from "react";
import { renderToString } from "react-dom/server";
import Editor from "../client/components/Editor.js";
import { ServerStyleSheet } from "styled-components";
const sheet = new ServerStyleSheet();
const fallbackLng = "en";
const port = PORT || 5013;
const renderApp = (i18n, initialEditorState) => {
return renderToString(
sheet.collectStyles(
<I18nextProvider i18n={i18n}>
<GlobalStyle />
<Editor {...initialEditorState} />
</I18nextProvider>
)
);
};
winston.stream = {
write: (message) => winston.info(message),
};
const loadPath = (lng, ns) => {
let resourcePath = path.resolve(
path.join(__dirname, "client", `locales/${lng}/${ns}.json`)
);
if (ns === "Common")
resourcePath = path.resolve(
path.join(__dirname, `../../../public/locales/${lng}/${ns}.json`)
);
return resourcePath;
};
const app = express();
i18next.use(Backend).init({
backend: {
loadPath: loadPath,
allowMultiLoading: true,
crossDomain: false,
},
fallbackLng: fallbackLng,
load: "currentOnly",
saveMissing: true,
ns: ["Editor", "Common"],
defaultNS: "Editor",
interpolation: {
escapeValue: false,
format: function (value, format) {
if (format === "lowercase") return value.toLowerCase();
return value;
},
},
});
app.use(i18nextMiddleware.handle(i18next));
app.use(compression());
app.use(
"/products/files/doceditor/",
express.static(path.resolve(path.join(__dirname, "client")))
);
app.use(logger("dev", { stream: winston.stream }));
if (IS_DEVELOPMENT) {
app.use(devMiddleware);
app.get("/products/files/doceditor", async (req, res) => {
const { i18n, initialEditorState, assets } = req;
const userLng = initialEditorState?.user?.cultureName || "en";
await i18next.changeLanguage(userLng);
const initialI18nStoreASC = i18n.services.resourceStore.data;
if (initialEditorState?.error) {
winston.error(initialEditorState.error.errorMessage);
}
const appComponent = renderApp(i18n, initialEditorState);
const styleTags = sheet.getStyleTags();
const htmlString = template(
initialEditorState,
appComponent,
styleTags,
initialI18nStoreASC,
userLng,
assets
);
res.send(htmlString);
});
const server = app.listen(port, () => {
winston.info(`Server is listening on port ${port}`);
});
const wss = ws(server);
const manifestFile = path.resolve(
path.join(__dirname, "client/manifest.json")
);
let fsWait = false;
let waitTimeout = null;
fs.watch(manifestFile, (event, filename) => {
if (filename && event === "change") {
if (fsWait) return;
fsWait = true;
waitTimeout = setTimeout(() => {
fsWait = false;
clearTimeout(waitTimeout);
wss.broadcast("reload");
}, 100);
}
});
} else {
let assets;
try {
assets = getAssets();
} catch (e) {
winston.error(e.message);
}
app.get("/products/files/doceditor", async (req, res) => {
const { i18n } = req;
let initialEditorState;
try {
initialEditorState = await initDocEditor(req);
} catch (e) {
winston.error(e.message);
}
const userLng = initialEditorState?.user?.cultureName || "en";
await i18next.changeLanguage(userLng);
const initialI18nStoreASC = i18n.services.resourceStore.data;
if (initialEditorState?.error) {
winston.error(initialEditorState.error.errorMessage);
}
const appComponent = renderApp(i18n, initialEditorState);
const styleTags = sheet.getStyleTags();
const htmlString = template(
initialEditorState,
appComponent,
styleTags,
initialI18nStoreASC,
userLng,
assets
);
res.send(htmlString);
});
app.listen(port, () => {
winston.info(`Server is listening on port ${port}`);
});
}

View File

@ -0,0 +1,141 @@
import path from "path";
import fs from "fs";
import { initSSR } from "@appserver/common/api/client";
import { getUser } from "@appserver/common/api/people";
import { getSettings } from "@appserver/common/api/settings";
import combineUrl from "@appserver/common/utils/combineUrl";
import { AppServerConfig } from "@appserver/common/constants";
import {
getDocServiceUrl,
getFileInfo,
openEdit,
getSettingsFiles,
} from "@appserver/common/api/files";
import pkg from "../../../../package.json";
export const getFavicon = (documentType) => {
const { homepage } = pkg;
let icon = null;
switch (documentType) {
case "word":
icon = "text.ico";
break;
case "slide":
icon = "presentation.ico";
break;
case "cell":
icon = "spreadsheet.ico";
break;
default:
break;
}
const favicon = icon ? `${homepage}/images/${icon}` : "/favicon.ico";
return favicon;
};
export const initDocEditor = async (req) => {
if (!req) return false;
let personal = IS_PERSONAL || null;
const { headers, url, query } = req;
const { version, desktop: isDesktop } = query;
let error = null;
initSSR(headers);
try {
const decodedId = query.fileId || query.fileid || null;
const fileId =
typeof decodedId === "string" ? encodeURIComponent(decodedId) : decodedId;
if (!fileId) {
return {
props: {
needLoader: true,
},
};
}
const doc = query?.doc || null;
const view = url.indexOf("action=view") !== -1;
const fileVersion = version || null;
const [user, settings, filesSettings] = await Promise.all([
getUser(),
getSettings(),
getSettingsFiles(),
]);
const successAuth = !!user;
personal = settings?.personal;
if (!successAuth && !doc) {
error = {
unAuthorized: true,
redirectPath: combineUrl(
AppServerConfig.proxyURL,
personal ? "/sign-in" : "/login"
),
};
return { error };
}
let [config, docApiUrl, fileInfo] = await Promise.all([
openEdit(fileId, fileVersion, doc, view),
getDocServiceUrl(),
getFileInfo(fileId),
]);
const isSharingAccess = fileInfo && fileInfo.canShare;
if (view) {
config.editorConfig.mode = "view";
}
const actionLink = config?.editorConfig?.actionLink || null;
return {
fileInfo,
docApiUrl,
config,
personal,
successAuth,
user,
error,
actionLink,
isSharingAccess,
url,
doc,
fileId,
view,
filesSettings,
};
} catch (err) {
error = { errorMessage: typeof err === "string" ? err : err.message };
return { error };
}
};
export const getAssets = () => {
const manifest = fs.readFileSync(
path.join(__dirname, "client/manifest.json"),
"utf-8"
);
const assets = JSON.parse(manifest);
return assets;
};
export const getScripts = (assets) => {
const regTest = /static\/js\/.*/;
const keys = [];
for (let key in assets) {
if (assets.hasOwnProperty(key) && regTest.test(key)) {
keys.push(key);
}
}
return keys;
};

View File

@ -0,0 +1,67 @@
import winston from "winston";
import "winston-daily-rotate-file";
import path from "path";
import fs from "fs";
let logpath = process.env.logpath || null;
if (logpath != null) {
if (!path.isAbsolute(logpath)) {
logpath = path.join(__dirname, "..", logpath);
}
}
const fileName = !IS_DEVELOPMENT
? path.join(__dirname, "..", "..", "..", "Logs", "editor.%DATE%.log")
: logpath
? path.join(logpath, "editor.%DATE%.log")
: path.join(
__dirname,
"..",
"..",
"..",
"..",
"..",
"Logs",
"editor.%DATE%.log"
);
const dirName = path.dirname(fileName);
if (!fs.existsSync(dirName)) {
fs.mkdirSync(dirName);
}
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,
},
};
const transports = [
new winston.transports.Console(options.console),
new winston.transports.DailyRotateFile(options.file),
];
export default new winston.createLogger({
format: winston.format.combine(
winston.format.timestamp({
format: "YYYY-MM-DD HH:mm:ss",
}),
winston.format.json()
),
transports: transports,
exitOnError: false,
});

View File

@ -0,0 +1,17 @@
import winston from "../logger.js";
import { getAssets, initDocEditor } from "../helpers";
winston.stream = {
write: (message) => winston.info(message),
};
export default async (req, res, next) => {
try {
const assets = await getAssets();
req.initialEditorState = await initDocEditor(req);
req.assets = assets;
} catch (e) {
winston.error(e.message);
}
next();
};

View File

@ -0,0 +1,391 @@
import { getFavicon, getScripts } from "./helpers";
import pkg from "../../../package.json";
export default function template(
initialEditorState = {},
appComponent = "",
styleTags,
initialI18nStoreASC,
initialLanguage,
assets
) {
const { title } = pkg;
const { docApiUrl, error } = initialEditorState;
const faviconHref = getFavicon(initialEditorState?.config?.documentType);
let clientScripts =
assets && assets.hasOwnProperty("client.js")
? `<script defer="defer" src='${assets["client.js"]}'></script>`
: "";
const editorApiScript =
error || !docApiUrl
? ""
: `<script type='text/javascript' id='scripDocServiceAddress' src="${docApiUrl}" async></script>`;
if (!IS_DEVELOPMENT) {
const productionBundleKeys = getScripts(assets);
productionBundleKeys.map((key) => {
clientScripts =
clientScripts + `<script defer="defer" src='${assets[key]}'></script>`;
});
}
const scripts = `
<script id="__ASC_INITIAL_EDITOR_STATE__">
window.__ASC_INITIAL_EDITOR_STATE__ = ${JSON.stringify(
initialEditorState
)}
</script>
<script id="__ASC_INITIAL_EDITOR_I18N__">
window.initialI18nStoreASC = ${JSON.stringify(initialI18nStoreASC)}
window.initialLanguage = '${initialLanguage}'
</script>
${clientScripts}
<script>
const tempElm = document.getElementById("loader");
tempElm.style.backgroundColor =
localStorage.theme === "Dark" ? "#333333" : "#f4f4f4";
console.log("It's Editor INIT");
</script>
${editorApiScript}
`;
// <script defer="defer" src='${assets["runtime.js"]}'></script>
// <script defer="defer" src='${assets["vendor.js"]}'></script>
const page = `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title> ${title} </title>
<meta charset="utf-8" />
<meta
name="viewport"
content="width=device-width, initial-scale=1, shrink-to-fit=no, user-scalable=no, viewport-fit=cover"
/>
<meta name="theme-color" content="#000000" />
<link id="favicon" rel="shortcut icon" href=${faviconHref} type="image/x-icon"/>
<link rel="manifest" href="/manifest.json" />
<meta name="mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<link rel="apple-touch-icon" href="/appIcon.png" />
${styleTags}
<style type="text/css">
.loadmask {
left: 0;
top: 0;
position: absolute;
height: 100%;
width: 100%;
overflow: hidden;
border: none;
background-color: #f4f4f4;
z-index: 1001;
}
.loader-page {
width: 100%;
height: 170px;
bottom: 42%;
position: absolute;
text-align: center;
line-height: 10px;
margin-bottom: 20px;
}
.loader-page-romb {
width: 40px;
display: inline-block;
}
.romb {
width: 40px;
height: 40px;
position: absolute;
background: red;
border-radius: 6px;
-webkit-transform: rotate(135deg) skew(20deg, 20deg);
-moz-transform: rotate(135deg) skew(20deg, 20deg);
-ms-transform: rotate(135deg) skew(20deg, 20deg);
-o-transform: rotate(135deg) skew(20deg, 20deg);
-webkit-animation: movedown 3s infinite ease;
-moz-animation: movedown 3s infinite ease;
-ms-animation: movedown 3s infinite ease;
-o-animation: movedown 3s infinite ease;
animation: movedown 3s infinite ease;
}
#blue {
z-index: 3;
background: #55bce6;
-webkit-animation-name: blue;
-moz-animation-name: blue;
-ms-animation-name: blue;
-o-animation-name: blue;
animation-name: blue;
}
#red {
z-index: 1;
background: #de7a59;
-webkit-animation-name: red;
-moz-animation-name: red;
-ms-animation-name: red;
-o-animation-name: red;
animation-name: red;
}
#green {
z-index: 2;
background: #a1cb5c;
-webkit-animation-name: green;
-moz-animation-name: green;
-ms-animation-name: green;
-o-animation-name: green;
animation-name: green;
}
@-webkit-keyframes red {
0% {
top: 120px;
background: #de7a59;
}
10% {
top: 120px;
background: #f2cbbf;
}
14% {
background: #f4f4f4;
top: 120px;
}
15% {
background: #f4f4f4;
top: 0;
}
20% {
background: #e6e4e4;
}
30% {
background: #d2d2d2;
}
40% {
top: 120px;
}
100% {
top: 120px;
background: #de7a59;
}
}
@keyframesred {
0% {
top: 120px;
background: #de7a59;
}
10% {
top: 120px;
background: #f2cbbf;
}
14% {
background: #f4f4f4;
top: 120px;
}
15% {
background: #f4f4f4;
top: 0;
}
20% {
background: #e6e4e4;
}
30% {
background: #d2d2d2;
}
40% {
top: 120px;
}
100% {
top: 120px;
background: #de7a59;
}
}
@-webkit-keyframes green {
0% {
top: 110px;
background: #a1cb5c;
opacity: 1;
}
10% {
top: 110px;
background: #cbe0ac;
opacity: 1;
}
14% {
background: #f4f4f4;
top: 110px;
opacity: 1;
}
15% {
background: #f4f4f4;
top: 0;
opacity: 1;
}
20% {
background: #f4f4f4;
top: 0;
opacity: 0;
}
25% {
background: #efefef;
top: 0;
opacity: 1;
}
30% {
background: #e6e4e4;
}
70% {
top: 110px;
}
100% {
top: 110px;
background: #a1cb5c;
}
}
@keyframes green {
0% {
top: 110px;
background: #a1cb5c;
opacity: 1;
}
10% {
top: 110px;
background: #cbe0ac;
opacity: 1;
}
14% {
background: #f4f4f4;
top: 110px;
opacity: 1;
}
15% {
background: #f4f4f4;
top: 0;
opacity: 1;
}
20% {
background: #f4f4f4;
top: 0;
opacity: 0;
}
25% {
background: #efefef;
top: 0;
opacity: 1;
}
30% {
background: #e6e4e4;
}
70% {
top: 110px;
}
100% {
top: 110px;
background: #a1cb5c;
}
}
@-webkit-keyframes blue {
0% {
top: 100px;
background: #55bce6;
opacity: 1;
}
10% {
top: 100px;
background: #bfe8f8;
opacity: 1;
}
14% {
background: #f4f4f4;
top: 100px;
opacity: 1;
}
15% {
background: #f4f4f4;
top: 0;
opacity: 1;
}
20% {
background: #f4f4f4;
top: 0;
opacity: 0;
}
25% {
background: #f4f4f4;
top: 0;
opacity: 0;
}
45% {
background: #efefef;
top: 0;
opacity: 0.2;
}
100% {
top: 100px;
background: #55bce6;
}
}
@keyframes blue {
0% {
top: 100px;
background: #55bce6;
opacity: 1;
}
10% {
top: 100px;
background: #bfe8f8;
opacity: 1;
}
14% {
background: #f4f4f4;
top: 100px;
opacity: 1;
}
15% {
background: #f4f4f4;
top: 0;
opacity: 1;
}
20% {
background: #f4f4f4;
top: 0;
opacity: 0;
}
25% {
background: #fff;
top: 0;
opacity: 0;
}
45% {
background: #efefef;
top: 0;
opacity: 0.2;
}
100% {
top: 100px;
background: #55bce6;
}
}
</style>
</head>
<body>
<div id="loader" class="loadmask">
<div class="loader-page">
<div class="loader-page-romb">
<div class="romb" id="blue"></div>
<div class="romb" id="green"></div>
<div class="romb" id="red"></div>
</div>
</div>
</div>
<div id="root">${appComponent}</div>
<noscript> You need to enable JavaScript to run this app. </noscript>
${scripts}
</body>
</html>
`;
return page;
}

View File

@ -0,0 +1,30 @@
const WebSocket = require("ws");
const pkg = require("../../../package.json");
const { socketPath } = pkg;
module.exports = (expressServer) => {
const wss = new WebSocket.Server({
noServer: true,
path: socketPath,
});
expressServer.on("upgrade", (request, socket, head) => {
wss.handleUpgrade(request, socket, head, (websocket) => {
wss.emit("connection", websocket, request);
});
});
wss.on("connection", function connection(ws) {
ws.on("message", function (message) {
wss.broadcast(message);
});
});
wss.broadcast = function broadcast(msg) {
wss.clients.forEach(function each(client) {
client.send(msg);
});
};
return wss;
};

View File

@ -1,217 +0,0 @@
const { CleanWebpackPlugin } = require("clean-webpack-plugin");
const CopyPlugin = require("copy-webpack-plugin");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const ModuleFederationPlugin = require("webpack").container
.ModuleFederationPlugin;
const ExternalTemplateRemotesPlugin = require("external-remotes-plugin");
const TerserPlugin = require("terser-webpack-plugin");
const DefinePlugin = require("webpack").DefinePlugin;
const combineUrl = require("@appserver/common/utils/combineUrl");
const minifyJson = require("@appserver/common/utils/minifyJson");
const AppServerConfig = require("@appserver/common/constants/AppServerConfig");
const sharedDeps = require("@appserver/common/constants/sharedDependencies");
const { proxyURL } = AppServerConfig;
const path = require("path");
const pkg = require("./package.json");
const deps = pkg.dependencies || {};
const homepage = pkg.homepage; // combineUrl(AppServerConfig.proxyURL, pkg.homepage);
const title = pkg.title;
const config = {
entry: "./src/index",
target: "web",
mode: "development",
devServer: {
devMiddleware: {
publicPath: homepage,
},
static: {
directory: path.join(__dirname, "dist"),
publicPath: homepage,
},
port: 5013,
historyApiFallback: {
// Paths with dots should still use the history fallback.
// See https://github.com/facebook/create-react-app/issues/387.
disableDotRule: true,
index: homepage,
},
hot: false,
headers: {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, PATCH, OPTIONS",
"Access-Control-Allow-Headers":
"X-Requested-With, content-type, Authorization",
},
},
resolve: {
extensions: [".jsx", ".js", ".json"],
fallback: {
crypto: false,
},
},
output: {
publicPath: "auto",
chunkFilename: "static/js/[id].[contenthash].js",
//assetModuleFilename: "static/images/[hash][ext][query]",
path: path.resolve(process.cwd(), "dist"),
filename: "static/js/[name].[contenthash].bundle.js",
},
performance: {
maxEntrypointSize: 512000,
maxAssetSize: 512000,
},
module: {
rules: [
{
test: /\.(png|jpe?g|gif|ico)$/i,
type: "asset/resource",
generator: {
filename: "static/images/[hash][ext][query]",
},
},
{
test: /\.m?js/,
type: "javascript/auto",
resolve: {
fullySpecified: false,
},
},
{
test: /\.react.svg$/,
use: [
{
loader: "@svgr/webpack",
options: {
svgoConfig: {
plugins: [{ removeViewBox: false }],
},
},
},
],
},
{ test: /\.json$/, loader: "json-loader" },
{
test: /\.css$/i,
use: ["style-loader", "css-loader"],
},
{
test: /\.s[ac]ss$/i,
use: [
// Creates `style` nodes from JS strings
"style-loader",
// Translates CSS into CommonJS
{
loader: "css-loader",
options: {
url: {
filter: (url, resourcePath) => {
// resourcePath - path to css file
// Don't handle `/static` urls
if (url.startsWith("/static") || url.startsWith("data:")) {
return false;
}
return true;
},
},
},
},
// Compiles Sass to CSS
"sass-loader",
],
},
{
test: /\.(js|jsx)$/,
exclude: /node_modules/,
use: [
{
loader: "babel-loader",
options: {
presets: ["@babel/preset-react", "@babel/preset-env"],
plugins: [
"@babel/plugin-transform-runtime",
"@babel/plugin-proposal-class-properties",
"@babel/plugin-proposal-export-default-from",
],
},
},
"source-map-loader",
],
},
],
},
plugins: [
new CleanWebpackPlugin(),
new ModuleFederationPlugin({
name: "editor",
filename: "remoteEntry.js",
remotes: {
studio: `studio@${combineUrl(proxyURL, "/remoteEntry.js")}`,
files: `files@${combineUrl(
proxyURL,
"/products/files/remoteEntry.js"
)}`,
},
exposes: {
"./app": "./src/Editor.jsx",
},
shared: {
...deps,
...sharedDeps,
},
}),
new ExternalTemplateRemotesPlugin(),
new HtmlWebpackPlugin({
template: "./public/index.html",
publicPath: homepage,
title: title,
base: `${homepage}/`,
}),
new CopyPlugin({
patterns: [
{
context: path.resolve(__dirname, "public"),
from: "images/**/*.*",
},
{
context: path.resolve(__dirname, "public"),
from: "locales/**/*.json",
transform: minifyJson,
},
],
}),
],
};
module.exports = (env, argv) => {
if (argv.mode === "production") {
config.mode = "production";
config.optimization = {
splitChunks: { chunks: "all" },
minimize: !env.minimize,
minimizer: [new TerserPlugin()],
};
} else {
config.devtool = "cheap-module-source-map";
}
config.plugins.push(
new DefinePlugin({
IS_PERSONAL: env.personal || false,
})
);
return config;
};

View File

@ -0,0 +1,45 @@
const scriptExtensions = /\.(tsx|ts|js|jsx|mjs)$/;
const imageExtensions = /\.(bmp|gif|jpg|jpeg|png)$/;
const fontsExtension = /\.(eot|otf|ttf|woff|woff2)$/;
module.exports = {
resolve: {
extensions: [".js", ".jsx", ".json", ".ts", ".tsx"],
fallback: {
crypto: false,
},
},
module: {
rules: [
{
test: scriptExtensions,
exclude: /node_modules/,
use: [
{
loader: "babel-loader",
options: {
presets: ["@babel/preset-react", "@babel/preset-env"],
plugins: [
"@babel/plugin-transform-runtime",
"@babel/plugin-proposal-class-properties",
"@babel/plugin-proposal-export-default-from",
],
},
},
],
},
{
test: fontsExtension,
type: "asset",
},
{
test: /\.svg/,
type: "asset/inline",
},
{
test: imageExtensions,
type: "asset/resource",
},
],
},
};

View File

@ -0,0 +1,132 @@
const { merge } = require("webpack-merge");
const path = require("path");
const ModuleFederationPlugin = require("webpack").container
.ModuleFederationPlugin;
const DefinePlugin = require("webpack").DefinePlugin;
const { WebpackManifestPlugin } = require("webpack-manifest-plugin");
const { CleanWebpackPlugin } = require("clean-webpack-plugin");
const ExternalTemplateRemotesPlugin = require("external-remotes-plugin");
const CopyPlugin = require("copy-webpack-plugin");
const TerserPlugin = require("terser-webpack-plugin");
const combineUrl = require("@appserver/common/utils/combineUrl");
const minifyJson = require("@appserver/common/utils/minifyJson");
const AppServerConfig = require("@appserver/common/constants/AppServerConfig");
const { proxyURL } = AppServerConfig;
const sharedDeps = require("@appserver/common/constants/sharedDependencies");
const baseConfig = require("./webpack.base.js");
for (let dep in sharedDeps) {
sharedDeps[dep].eager = true;
}
const clientConfig = {
target: "web",
// mode: "development",
entry: {
client: ["./src/client/index.js"],
},
devtool: "inline-cheap-module-source-map",
output: {
path: path.resolve(process.cwd(), "dist/client"),
filename: "static/js/[name].[contenthash].bundle.js",
publicPath: "/products/files/doceditor/",
chunkFilename: "static/js/[id].[contenthash].js",
},
performance: {
maxEntrypointSize: 512000,
maxAssetSize: 512000,
},
module: {
rules: [
{
test: /\.s[ac]ss$/i,
use: [
// Creates `style` nodes from JS strings
"style-loader",
// Translates CSS into CommonJS
{
loader: "css-loader",
options: {
url: {
filter: (url, resourcePath) => {
// resourcePath - path to css file
// Don't handle `/static` urls
if (url.startsWith("/static") || url.startsWith("data:")) {
return false;
}
return true;
},
},
},
},
// Compiles Sass to CSS
"sass-loader",
],
},
],
},
plugins: [
new CleanWebpackPlugin(),
new ModuleFederationPlugin({
name: "editor",
filename: "remoteEntry.js",
remotes: {
studio: `studio@${combineUrl(proxyURL, "/remoteEntry.js")}`,
files: `files@${combineUrl(
proxyURL,
"/products/files/remoteEntry.js"
)}`,
},
exposes: {
"./app": "./src/client/index.js",
},
shared: { ...sharedDeps },
}),
new ExternalTemplateRemotesPlugin(),
new CopyPlugin({
patterns: [
{
context: path.resolve(process.cwd(), "public"),
from: "images/**/*.*",
},
{
context: path.resolve(process.cwd(), "public"),
from: "locales/**/*.json",
transform: minifyJson,
},
],
}),
new WebpackManifestPlugin(),
],
};
module.exports = (env, argv) => {
if (argv.mode === "production") {
clientConfig.mode = "production";
clientConfig.optimization = {
splitChunks: { chunks: "all" },
minimize: !env.minimize,
minimizer: [new TerserPlugin()],
};
} else {
clientConfig.mode = "development";
clientConfig.devtool = "cheap-module-source-map";
}
clientConfig.plugins = [
...clientConfig.plugins,
new DefinePlugin({
IS_DEVELOPMENT: argv.mode !== "production",
PORT: process.env.PORT || 5013,
IS_PERSONAL: env.personal || false,
}),
];
return merge(baseConfig, clientConfig);
};

View File

@ -0,0 +1,44 @@
const { merge } = require("webpack-merge");
const baseConfig = require("./webpack.base.js");
const webpackNodeExternals = require("webpack-node-externals");
const path = require("path");
const DefinePlugin = require("webpack").DefinePlugin;
const TerserPlugin = require("terser-webpack-plugin");
const serverConfig = {
target: "node",
//mode: "development",
name: "server",
entry: {
server: "./src/server/index.js",
},
output: {
path: path.resolve(process.cwd(), "dist/"),
filename: "[name].js",
libraryTarget: "commonjs2",
chunkFilename: "chunks/[name].js",
},
externals: [webpackNodeExternals(), { express: "express" }],
};
module.exports = (env, argv) => {
if (argv.mode === "production") {
serverConfig.mode = "production";
serverConfig.optimization = {
minimize: !env.minimize,
minimizer: [new TerserPlugin()],
};
} else {
serverConfig.mode = "development";
}
serverConfig.plugins = [
new DefinePlugin({
IS_DEVELOPMENT: argv.mode !== "production",
PORT: process.env.PORT || 5013,
IS_PERSONAL: env.personal || false,
}),
];
return merge(baseConfig, serverConfig);
};

View File

@ -70,7 +70,7 @@
"style-loader": "3.2.1",
"terser-webpack-plugin": "^5.2.4",
"webpack": "5.52.1",
"webpack-cli": "4.9.0",
"webpack-cli": "4.10.0",
"webpack-dev-server": "4.3.1"
},
"title": "ONLYOFFICE"

5809
yarn.lock

File diff suppressed because it is too large Load Diff