Merge branch 'feature/workspaces' of github.com:ONLYOFFICE/AppServer into feature/workspaces

This commit is contained in:
TatianaLopaeva 2021-03-12 17:05:51 +03:00
commit 1675e77330
13 changed files with 356 additions and 170 deletions

View File

@ -56,69 +56,84 @@ class ModuleStore {
iconName = null,
iconUrl = null
) => {
switch (item.id) {
case "6743007c-6f95-4d20-8c88-a8601ce5e76d":
item.iconName = "CrmIcon";
item.iconUrl = "/static/images/crm.react.svg";
item.imageUrl = "/images/crm.svg";
item.helpUrl = "https://helpcenter.onlyoffice.com/userguides/crm.aspx";
break;
case "1e044602-43b5-4d79-82f3-fd6208a11960":
item.iconName = "ProjectsIcon";
item.iconUrl = "/static/images/projects.react.svg";
item.imageUrl = "/images/projects.svg";
item.helpUrl =
"https://helpcenter.onlyoffice.com/userguides/projects.aspx";
break;
case "2A923037-8B2D-487b-9A22-5AC0918ACF3F":
item.iconName = "MailIcon";
item.iconUrl = "/static/images/mail.react.svg";
item.imageUrl = "/images/mail.svg";
break;
case "32D24CB5-7ECE-4606-9C94-19216BA42086":
item.iconName = "CalendarCheckedIcon";
item.iconUrl = "/static/images/calendar.checked.react.svg";
item.imageUrl = "/images/calendar.svg";
break;
case "BF88953E-3C43-4850-A3FB-B1E43AD53A3E":
item.iconName = "ChatIcon";
item.iconUrl = "/static/images/chat.react.svg";
item.imageUrl = "/images/talk.svg";
item.isolateMode = true;
break;
default:
break;
}
const id =
item.id && typeof item.id === "string" ? item.id.toLowerCase() : null;
const actions = noAction
? null
: {
onClick: (e) => {
if (e) {
window.open(item.link, "_self");
e.preventDefault();
}
},
onBadgeClick: (e) => console.log(iconName + " Badge Clicked", e),
};
const description = noAction ? { description: item.description } : null;
return {
id: item.id,
const result = {
id,
appName: "none",
title: item.title,
link: item.link,
originUrl: item.originUrl,
helpUrl: item.helpUrl,
notifications: 0,
iconName: item.iconName || iconName || "/static/images/people.react.svg", //TODO: Change to URL
iconUrl: item.iconUrl || iconUrl,
imageUrl: item.imageUrl,
notifications: 0,
isolateMode: item.isolateMode,
isPrimary: item.isPrimary,
...description,
...actions,
};
switch (id) {
case "6743007c-6f95-4d20-8c88-a8601ce5e76d":
result.appName = "crm";
result.iconName = "CrmIcon";
result.iconUrl = "/static/images/crm.react.svg";
result.imageUrl = "/images/crm.svg";
result.helpUrl =
"https://helpcenter.onlyoffice.com/userguides/crm.aspx";
break;
case "1e044602-43b5-4d79-82f3-fd6208a11960":
result.appName = "projects";
result.iconName = "ProjectsIcon";
result.iconUrl = "/static/images/projects.react.svg";
result.imageUrl = "/images/projects.svg";
result.helpUrl =
"https://helpcenter.onlyoffice.com/userguides/projects.aspx";
break;
case "2a923037-8b2d-487b-9a22-5ac0918acf3f":
result.appName = "mail";
result.iconName = "MailIcon";
result.iconUrl = "/static/images/mail.react.svg";
result.imageUrl = "/images/mail.svg";
break;
case "32d24cb5-7ece-4606-9c94-19216ba42086":
result.appName = "calendar";
result.iconName = "CalendarCheckedIcon";
result.iconUrl = "/static/images/calendar.checked.react.svg";
result.imageUrl = "/images/calendar.svg";
break;
case "bf88953e-3c43-4850-a3fb-b1e43ad53a3e":
result.appName = "chat";
result.iconName = "ChatIcon";
result.iconUrl = "/static/images/chat.react.svg";
result.imageUrl = "/images/talk.svg";
result.isolateMode = true;
break;
case "e67be73d-f9ae-4ce1-8fec-1880cb518cb4":
result.appName = "files";
break;
case "f4d98afd-d336-4332-8778-3c6945c81ea0":
result.appName = "people";
break;
default:
result.appName = "none";
break;
}
if (!noAction) {
result.onClick = (e) => {
if (e) {
window.open(item.link, "_self");
e.preventDefault();
}
};
result.onBadgeClick = (e) => console.log(iconName + " Badge Clicked", e);
} else {
result.description = item.description;
}
return result;
};
get totalNotificationsCount() {

View File

@ -239,7 +239,7 @@ DropDownContainer.propTypes = {
DropDownContainer.defaultProps = {
directionX: "left",
directionY: "bottom",
withBackdrop: false,
withBackdrop: true,
showDisabledItems: false,
};

View File

@ -17,6 +17,33 @@ describe("<RowContainer />", () => {
expect(wrapper).toExist();
});
it("stop event on context click", () => {
const wrapper = shallow(
<RowContainer>
<span>Demo</span>
</RowContainer>
);
const event = { preventDefault: () => {} };
jest.spyOn(event, "preventDefault");
wrapper.simulate("contextmenu", event);
expect(event.preventDefault).not.toBeCalled();
});
it("renders like list", () => {
const wrapper = mount(
<RowContainer useReactWindow={false}>
<span>Demo</span>
</RowContainer>
);
expect(wrapper).toExist();
expect(wrapper.getDOMNode().style).toHaveProperty("height", "");
});
it("renders without manualHeight", () => {
const wrapper = mount(
<RowContainer>
@ -27,45 +54,6 @@ describe("<RowContainer />", () => {
expect(wrapper).toExist();
});
it("call onRowContextClick() with normal options", () => {
const options = [
{
key: "1",
label: "test",
},
];
const wrapper = mount(
<RowContainer>
<span>Demo</span>
</RowContainer>
);
const instance = wrapper.instance();
instance.onRowContextClick(options);
expect(wrapper.state("contextOptions")).toEqual(options);
});
it("call onRowContextClick() with wrong options", () => {
const options = {
key: "1",
label: "test",
};
const wrapper = mount(
<RowContainer>
<span>Demo</span>
</RowContainer>
);
const instance = wrapper.instance();
instance.onRowContextClick(options);
expect(wrapper.state("contextOptions")).toEqual([]);
});
it("componentWillUnmount() props lifecycle test", () => {
const wrapper = shallow(
<RowContainer>

View File

@ -27,7 +27,8 @@ class Row extends React.Component {
}
componentWillUnmount() {
this.container.removeEventListener("contextmenu", this.onContextMenu);
this.container &&
this.container.removeEventListener("contextmenu", this.onContextMenu);
}
onContextMenu = (e) => {
@ -141,12 +142,10 @@ Row.propTypes = {
element: PropTypes.element,
/** Accepts id */
id: PropTypes.string,
/** If true, this state is shown as a rectangle in the checkbox */
indeterminate: PropTypes.bool,
/** shouldComponentUpdate function */
needForUpdate: PropTypes.func,
/** when selecting row element. Returns data value. */
onSelect: PropTypes.func,
selectItem: PropTypes.func,
/** Accepts css style */
style: PropTypes.oneOfType([PropTypes.object, PropTypes.array]),
sectionWidth: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),

View File

@ -32,6 +32,44 @@ describe("<Row />", () => {
expect(onSelect).toHaveBeenCalled();
});
it("renders with children", () => {
const wrapper = mount(<Row {...baseProps} />);
expect(wrapper).toHaveProp("children", baseProps.children);
});
it("renders with contentElement and sectionWidth", () => {
const element = <div>content</div>;
const wrapper = mount(
<Row {...baseProps} contentElement={element} sectionWidth={600} />
);
expect(wrapper).toHaveProp("contentElement", element);
});
it("can apply contextButtonSpacerWidth", () => {
const test = "10px";
const wrapper = mount(
<Row {...baseProps} contextButtonSpacerWidth={test} />
);
expect(wrapper).toHaveProp("contextButtonSpacerWidth", test);
});
it("can apply data property", () => {
const test = { test: "test" };
const wrapper = mount(<Row {...baseProps} data={test} />);
expect(wrapper).toHaveProp("data", test);
});
it("can apply indeterminate", () => {
const test = true;
const wrapper = mount(<Row {...baseProps} indeterminate={test} />);
expect(wrapper).toHaveProp("indeterminate", test);
});
it("accepts id", () => {
const wrapper = mount(<Row {...baseProps} id="testId" />);
@ -49,4 +87,13 @@ describe("<Row />", () => {
expect(wrapper.getDOMNode().style).toHaveProperty("color", "red");
});
it("componentWillUnmount() props lifecycle test", () => {
const wrapper = shallow(<Row {...baseProps} />);
const instance = wrapper.instance();
instance.componentWillUnmount();
expect(wrapper).toExist(false);
});
});

View File

@ -17,7 +17,7 @@ const EmptyFilterContainer = ({
const subheadingText = t("EmptyFilterSubheadingText");
const descriptionText = t("EmptyFilterDescriptionText");
onResetFilter = () => {
const onResetFilter = () => {
setIsLoading(true);
const newFilter = FilesFilter.getDefault();
fetchFiles(selectedFolderId, newFilter)

View File

@ -121,7 +121,7 @@ class AddGroupsPanelComponent extends React.Component {
<StyledHeaderContent>
<IconButton
size="16"
iconName="ArrowPathIcon"
iconName="/static/images/arrow.path.react.svg"
onClick={this.onArrowClick}
color="A3A9AE"
/>

View File

@ -154,7 +154,7 @@ class AddUsersPanelComponent extends React.Component {
<StyledHeaderContent>
<IconButton
size="16"
iconName="ArrowPathIcon"
iconName="/static/images/arrow.path.react.svg"
onClick={this.onArrowClick}
color="#A3A9AE"
/>

View File

@ -37,39 +37,71 @@ export const getUserRole = (user) => {
export const getUserContactsPattern = () => {
return {
contact: [
{ type: "mail", icon: "MailIcon", link: "mailto:{0}" },
{ type: "phone", icon: "PhoneIcon", link: "tel:{0}" },
{ type: "mobphone", icon: "MobileIcon", link: "tel:{0}" },
{ type: "gmail", icon: "GmailIcon", link: "mailto:{0}" },
{ type: "skype", icon: "SkypeIcon", link: "skype:{0}?userinfo" },
{ type: "msn", icon: "WindowsMsnIcon" },
{ type: "icq", icon: "IcqIcon", link: "https://www.icq.com/people/{0}" },
{ type: "jabber", icon: "JabberIcon" },
{ type: "aim", icon: "AimIcon" },
{
type: "mail",
icon: "/static/images/mail.react.svg",
link: "mailto:{0}",
},
{
type: "phone",
icon: "/products/people/images/phone.react.svg",
link: "tel:{0}",
},
{
type: "mobphone",
icon: "/products/people/images/mobile.react.svg",
link: "tel:{0}",
},
{
type: "gmail",
icon: "/products/people/images/gmail.react.svg",
link: "mailto:{0}",
},
{
type: "skype",
icon: "/products/people/images/skype.react.svg",
link: "skype:{0}?userinfo",
},
{ type: "msn", icon: "/products/people/images/windows.msn.react.svg" },
{
type: "icq",
icon: "/products/people/images/icq.react.svg",
link: "https://www.icq.com/people/{0}",
},
{ type: "jabber", icon: "/products/people/images/jabber.react.svg" },
{ type: "aim", icon: "/products/people/images/aim.react.svg" },
],
social: [
{
type: "facebook",
icon: "ShareFacebookIcon",
icon: "/products/people/images/share.facebook.react.svg",
link: "https://facebook.com/{0}",
},
{
type: "livejournal",
icon: "LivejournalIcon",
icon: "/products/people/images/livejournal.react.svg",
link: "https://{0}.livejournal.com",
},
{ type: "myspace", icon: "MyspaceIcon", link: "https://myspace.com/{0}" },
{
type: "myspace",
icon: "/products/people/images/myspace.react.svg",
link: "https://myspace.com/{0}",
},
{
type: "twitter",
icon: "ShareTwitterIcon",
icon: "/products/people/images/share.twitter.react.svg",
link: "https://twitter.com/{0}",
},
{
type: "blogger",
icon: "BloggerIcon",
icon: "/products/people/images/blogger.react.svg",
link: "https://{0}.blogspot.com",
},
{ type: "yahoo", icon: "YahooIcon", link: "mailto:{0}@yahoo.com" },
{
type: "yahoo",
icon: "/products/people/images/yahoo.react.svg",
link: "mailto:{0}@yahoo.com",
},
],
};
};

View File

@ -1,5 +1,4 @@
import React, { useEffect } from "react";
import styled from "styled-components";
import { Router, Switch } from "react-router-dom";
import { inject, observer } from "mobx-react";
import NavMenu from "./components/NavMenu";
@ -12,9 +11,6 @@ import Layout from "./components/Layout";
import ScrollToTop from "./components/Layout/ScrollToTop";
import history from "@appserver/common/history";
import toastr from "studio/toastr";
import Loader from "@appserver/components/loader";
import Grid from "@appserver/components/grid";
import PageLayout from "@appserver/common/components/PageLayout";
import { updateTempContent } from "@appserver/common/utils";
import { Provider as MobxProvider } from "mobx-react";
import ThemeProvider from "@appserver/components/theme-provider";
@ -24,35 +20,27 @@ import config from "../package.json";
import "./custom.scss";
import { I18nextProvider } from "react-i18next";
import i18n from "./i18n";
import AppLoader from "./components/AppLoader";
import System from "./components/System";
const Payments = React.lazy(() => import("./components/pages/Payments"));
const Error404 = React.lazy(() => import("studio/Error404"));
const Error401 = React.lazy(() => import("studio/Error401"));
const Home = React.lazy(() => import("./components/pages/Home"));
const Login = React.lazy(() => import("login/app"));
const People = React.lazy(() => import("people/app"));
const Files = React.lazy(() => import("files/app"));
const About = React.lazy(() => import("./components/pages/About"));
const Settings = React.lazy(() => import("./components/pages/Settings"));
const ComingSoon = React.lazy(() => import("./components/pages/ComingSoon"));
const LoadingShell = () => (
<PageLayout>
<PageLayout.SectionBody>
<Loader className="pageLoader" type="rombs" size="40px" />
</PageLayout.SectionBody>
</PageLayout>
);
const SettingsRoute = (props) => (
<React.Suspense fallback={<LoadingShell />}>
<React.Suspense fallback={<AppLoader />}>
<ErrorBoundary>
<Settings {...props} />
</ErrorBoundary>
</React.Suspense>
);
const PaymentsRoute = (props) => (
<React.Suspense fallback={<LoadingShell />}>
<React.Suspense fallback={<AppLoader />}>
<ErrorBoundary>
<Payments {...props} />
</ErrorBoundary>
@ -60,7 +48,7 @@ const PaymentsRoute = (props) => (
);
const Error404Route = (props) => (
<React.Suspense fallback={<LoadingShell />}>
<React.Suspense fallback={<AppLoader />}>
<ErrorBoundary>
<Error404 {...props} />
</ErrorBoundary>
@ -68,14 +56,14 @@ const Error404Route = (props) => (
);
const Error401Route = (props) => (
<React.Suspense fallback={<LoadingShell />}>
<React.Suspense fallback={<AppLoader />}>
<ErrorBoundary>
<Error401 {...props} />
</ErrorBoundary>
</React.Suspense>
);
const HomeRoute = (props) => (
<React.Suspense fallback={<LoadingShell />}>
<React.Suspense fallback={<AppLoader />}>
<ErrorBoundary>
<Home {...props} />
</ErrorBoundary>
@ -83,35 +71,15 @@ const HomeRoute = (props) => (
);
const LoginRoute = (props) => (
<React.Suspense fallback={<LoadingShell />}>
<React.Suspense fallback={<AppLoader />}>
<ErrorBoundary>
<Login {...props} />
</ErrorBoundary>
</React.Suspense>
);
const PeopleRoute = (props) => {
return (
<React.Suspense fallback={<LoadingShell />}>
<ErrorBoundary>
<People {...props} />
</ErrorBoundary>
</React.Suspense>
);
};
const FilesRoute = (props) => {
return (
<React.Suspense fallback={<LoadingShell />}>
<ErrorBoundary>
<Files {...props} />
</ErrorBoundary>
</React.Suspense>
);
};
const AboutRoute = (props) => (
<React.Suspense fallback={<LoadingShell />}>
<React.Suspense fallback={<AppLoader />}>
<ErrorBoundary>
<About {...props} />
</ErrorBoundary>
@ -119,12 +87,28 @@ const AboutRoute = (props) => (
);
const ComingSoonRoute = (props) => (
<React.Suspense fallback={<LoadingShell />}>
<React.Suspense fallback={<AppLoader />}>
<ErrorBoundary>
<ComingSoon {...props} />
</ErrorBoundary>
</React.Suspense>
);
const DynamicAppRoute = ({ link, appName, ...rest }) => {
const system = {
url: `${window.location.origin}${link}remoteEntry.js`,
scope: appName,
module: "./app",
};
return (
<React.Suspense fallback={<AppLoader />}>
<ErrorBoundary>
<System system={system} {...rest} />
</ErrorBoundary>
</React.Suspense>
);
};
const Shell = ({ items = [], page = "home", ...rest }) => {
// useEffect(() => {
// //utils.removeTempContent();
@ -160,7 +144,7 @@ const Shell = ({ items = [], page = "home", ...rest }) => {
// .catch((err) => toastr.error(err.message));
// }, []);
const { isLoaded, loadBaseInfo, isThirdPartyResponse } = rest;
const { isLoaded, loadBaseInfo, isThirdPartyResponse, modules } = rest;
useEffect(() => {
try {
@ -168,7 +152,7 @@ const Shell = ({ items = [], page = "home", ...rest }) => {
} catch (err) {
toastr.error(err);
}
}, [loadBaseInfo]);
}, []);
useEffect(() => {
if (isLoaded) updateTempContent();
@ -182,6 +166,16 @@ const Shell = ({ items = [], page = "home", ...rest }) => {
const pathname = window.location.pathname.toLowerCase();
const isEditor = pathname.indexOf("doceditor") !== -1;
const dynamicRoutes = modules.map((m) => (
<PrivateRoute
key={m.id}
path={m.link}
component={DynamicAppRoute}
link={m.link}
appName={m.appName}
/>
));
return (
<Layout>
<Router history={history}>
@ -195,14 +189,6 @@ const Shell = ({ items = [], page = "home", ...rest }) => {
path={["/", "/error=:error"]}
component={HomeRoute}
/>
<PrivateRoute
path={["/products/people", "/products/people/filter"]}
component={PeopleRoute}
/>
<PrivateRoute
path={["/products/files", "/products/files/filter"]}
component={FilesRoute}
/>
<PrivateRoute path={["/about"]} component={AboutRoute} />
<PublicRoute
exact
@ -231,6 +217,7 @@ const Shell = ({ items = [], page = "home", ...rest }) => {
path="/settings"
component={SettingsRoute}
/>
{dynamicRoutes}
<PrivateRoute path="/error401" component={Error401Route} />
<PrivateRoute component={Error404Route} />
</Switch>
@ -243,7 +230,6 @@ const Shell = ({ items = [], page = "home", ...rest }) => {
const ShellWrapper = inject(({ auth }) => {
const { init, isLoaded } = auth;
const pathname = window.location.pathname.toLowerCase();
const isThirdPartyResponse = pathname.indexOf("thirdparty") !== -1;
@ -254,6 +240,7 @@ const ShellWrapper = inject(({ auth }) => {
},
isThirdPartyResponse,
isLoaded,
modules: auth.moduleStore.modules,
};
})(observer(Shell));
@ -261,7 +248,7 @@ export default () => (
<ThemeProvider theme={Base}>
<MobxProvider {...store}>
<I18nextProvider i18n={i18n}>
<ShellWrapper />
<ShellWrapper />
</I18nextProvider>
</MobxProvider>
</ThemeProvider>

View File

@ -0,0 +1,13 @@
import React from "react";
import PageLayout from "@appserver/common/components/PageLayout";
import Loader from "@appserver/components/loader";
const AppLoader = () => (
<PageLayout>
<PageLayout.SectionBody>
<Loader className="pageLoader" type="rombs" size="40px" />
</PageLayout.SectionBody>
</PageLayout>
);
export default AppLoader;

View File

@ -0,0 +1,107 @@
import React from "react";
import AppLoader from "../AppLoader";
import ErrorBoundary from "@appserver/common/components/ErrorBoundary";
import Error520 from "studio/Error520";
import Error404 from "studio/Error404";
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;
};
}
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) {
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: Uncomment if you need to remove loaded remoteEntry
// return () => {
// console.log(`Dynamic Script Removed: ${args.url}`);
// document.head.removeChild(element);
// };
}, [args.url]);
return {
ready,
failed,
};
};
const System = (props) => {
const { ready, failed } = useDynamicScript({
url: props.system && props.system.url,
id: props.system && props.system.scope,
});
if (!props.system) {
console.log(`Not system specified`);
return <Error404 />;
}
if (!ready) {
console.log(`Loading dynamic script: ${props.system.url}`);
return <AppLoader />;
}
if (failed) {
console.log(`Failed to load dynamic script: ${props.system.url}`);
return <Error520 />;
}
const Component = React.lazy(
loadComponent(props.system.scope, props.system.module)
);
return (
<React.Suspense fallback={<AppLoader />}>
<ErrorBoundary>
<Component />
</ErrorBoundary>
</React.Suspense>
);
};
export default System;

View File

@ -126,8 +126,6 @@ const config = {
filename: "remoteEntry.js",
remotes: {
studio: `studio@${homepage}/remoteEntry.js`,
people: `people@${homepage}/products/people/remoteEntry.js`,
files: `files@${homepage}/products/files/remoteEntry.js`,
login: `login@${homepage}/login/remoteEntry.js`,
},
exposes: {