Skip to content

Commit

Permalink
feat: standalone mode (#79)
Browse files Browse the repository at this point in the history
* feat: standalone mode
  • Loading branch information
zovomat authored May 16, 2022
1 parent 0844366 commit b904928
Show file tree
Hide file tree
Showing 22 changed files with 143 additions and 89 deletions.
1 change: 0 additions & 1 deletion .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ module.exports = {
},
globals: {
BASE_PATH: 'readonly',
__SHELL_ENV__: 'readonly',
__CARBONIO_DEV__: 'readonly',
PACKAGE_NAME: 'readonly',
PACKAGE_VERSION: 'readonly',
Expand Down
3 changes: 1 addition & 2 deletions carbonio.webpack.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,7 @@ module.exports = (conf, pkg, options, mode) => {
inject: true,
template: path.resolve(process.cwd(), 'src', 'index.template.html'),
chunks: ['index'],
BASE_PATH: baseStaticPath,
SHELL_ENV: root
BASE_PATH: baseStaticPath
}),
new HtmlWebpackPlugin({
inject: false,
Expand Down
6 changes: 2 additions & 4 deletions src/boot/app/load-app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ function loadAppModule(appPkg: CarbonioModule, store: Store<any>): Promise<Carbo
_resolve(appPkg);
}
};
const reject: (e: Error) => void = (e) => {
const reject: (e: unknown) => void = (e) => {
if (!resolved) {
resolved = true;
_reject(e);
Expand Down Expand Up @@ -98,10 +98,8 @@ function loadAppModule(appPkg: CarbonioModule, store: Store<any>): Promise<Carbo
script.setAttribute('src', `${appPkg.js_entrypoint}`);
document.body.appendChild(script);
_scripts[`${appPkg.name}-loader-${(_scriptId += 1)}`] = script;
} catch (err) {
} catch (err: unknown) {
console.error(err);
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
reject(err);
}
});
Expand Down
2 changes: 1 addition & 1 deletion src/boot/app/load-apps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export function loadApps(storeFactory: StoreFactory, apps: Array<CarbonioModule>
injectSharedLibraries();
const appsToLoad = filter(apps, (app) => {
if (app.name === SHELL_APP_ID) return false;
if (app.attrKey && getUserSetting('attrs', app.attrKey) !== 'TRUE') return false;
if (app.attrKey && getUserSetting('attrs', app.attrKey) === 'FALSE') return false;
return true;
});
console.log(
Expand Down
19 changes: 16 additions & 3 deletions src/boot/bootstrapper-router.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,16 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/

import React, { FC, useContext } from 'react';
import { BrowserRouter, useHistory } from 'react-router-dom';
import React, { FC, useContext, useEffect } from 'react';
import { BrowserRouter, Route, Switch, useHistory, useParams } from 'react-router-dom';
import { SnackbarManagerContext, ModalManagerContext } from '@zextras/carbonio-design-system';
import AppLoaderMounter from './app/app-loader-mounter';
import { useBridge } from '../store/context-bridge';
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
import ShellView from '../shell/shell-view';
import { BASENAME } from '../constants';
import { useAppStore } from '../store/app';

const ContextBridge: FC = () => {
const history = useHistory();
Expand All @@ -30,12 +31,24 @@ const ContextBridge: FC = () => {
return null;
};

const StandaloneListener: FC = () => {
const { route } = useParams<{ route?: string }>();
useEffect(() => {
if (route) useAppStore.setState({ standalone: route });
}, [route]);
return null;
};

const BootstrapperRouter: FC = () => (
<BrowserRouter basename={BASENAME}>
<Switch>
<Route path={'/:route'}>
<StandaloneListener />
</Route>
</Switch>
<ContextBridge />
<AppLoaderMounter />
<ShellView />
</BrowserRouter>
);

export default BootstrapperRouter;
2 changes: 1 addition & 1 deletion src/boot/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import StoreFactory from '../redux/store-factory';
import { getInfo } from '../network/get-info';

export const init = (_i18nFactory: I18nFactory, _storeFactory: StoreFactory): void => {
getInfo().then(() => {
getInfo().finally(() => {
_i18nFactory.setLocale(
(
(useAccountStore.getState().settings?.prefs?.zimbraPrefLocale as string) ??
Expand Down
7 changes: 6 additions & 1 deletion src/constants/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,12 @@ export const darkReaderDynamicThemeFixes: DynamicThemeFix = {
disableStyleSheetsProxy: false
};

export const BASENAME = `/carbonio/`;
const base = '/carbonio/';

const standaloneBase = `${base}standalone`;

export const IS_STANDALONE = window.location.pathname.startsWith(standaloneBase);
export const BASENAME = IS_STANDALONE ? standaloneBase : base;
export const EMAIL_VALIDATION_REGEX =
// eslint-disable-next-line @typescript-eslint/no-unused-vars, max-len, no-control-regex
/(?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*|"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\[(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?|[a-z0-9-]*[a-z0-9]:(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21-\x5a\x53-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])+)\])/;
Expand Down
31 changes: 16 additions & 15 deletions src/index.template.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,24 +4,25 @@
SPDX-License-Identifier: AGPL-3.0-only
-->

<!DOCTYPE html>
<!DOCTYPE html>
<html lang="">

<head>
<title>Carbonio Client</title>
<link rel="icon" type="image/svg+xml" href="<%- htmlWebpackPlugin.options.BASE_PATH %>favicon.svg">
<link rel="alternate icon" href="<%- htmlWebpackPlugin.options.BASE_PATH %>favicon.png">
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="minimum-scale=1.0, initial-scale=1.0, width=device-width, shrink-to-fit=no">
<base href="/<%- htmlWebpackPlugin.options.SHELL_ENV %>/" />
<meta http-equiv="Cache-Control" content="no-cache" />
<meta http-equiv="Pragma" content="no-cache" />
<meta http-equiv="Expires" content="0" />
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto"></head>
<title>Carbonio Client</title>
<link rel="icon" type="image/svg+xml" href="<%- htmlWebpackPlugin.options.BASE_PATH %>favicon.svg">
<link rel="alternate icon" href="<%- htmlWebpackPlugin.options.BASE_PATH %>favicon.png">
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="minimum-scale=1.0, initial-scale=1.0, width=device-width, shrink-to-fit=no">
<base href="/carbonio/" />
<meta http-equiv="Cache-Control" content="no-cache" />
<meta http-equiv="Pragma" content="no-cache" />
<meta http-equiv="Expires" content="0" />
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto">
</head>

<body>
<script>
window.__SHELL_ENV__ = "<%- htmlWebpackPlugin.options.SHELL_ENV %>"
</script>
<div id="app"></div>
</body>

</html>
1 change: 0 additions & 1 deletion src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@ window.addEventListener('contextmenu', (ev) => {
// @ts-ignore works as intended, but it's tampering with the window
window.__CARBONIO_DEV__ = !!new URL(window.location).searchParams.get('dev');
const Bootstrapper = lazy(() => import('./boot/bootstrapper'));

if (module.hot) module.hot.accept();
render(
<Suspense fallback={<LoadingView />}>
Expand Down
19 changes: 13 additions & 6 deletions src/network/fetch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,10 @@ import { Account, ErrorSoapResponse, SoapContext, SoapResponse } from '../../typ
import { userAgent } from './user-agent';
import { report } from '../reporting';
import { useAccountStore } from '../store/account';
import { SHELL_APP_ID } from '../constants';
import { IS_STANDALONE, SHELL_APP_ID } from '../constants';
import { useNetworkStore } from '../store/network';
import { handleSync } from '../store/network/utils';
import { useAppStore } from '../store/app';

export const noOp = (): void => {
// eslint-disable-next-line @typescript-eslint/no-use-before-define
Expand Down Expand Up @@ -98,12 +99,18 @@ const handleResponse = <R>(api: string, res: SoapResponse<R>): R => {
(code) => code === (<ErrorSoapResponse>res).Body.Fault.Detail?.Error?.Code
)
) {
goToLogin();
if (IS_STANDALONE) {
useAccountStore.setState({ authenticated: false });
} else {
goToLogin();
}
}
throw new Error(
`${(<ErrorSoapResponse>res).Body.Fault.Detail?.Error?.Detail}: ${
(<ErrorSoapResponse>res).Body.Fault.Reason?.Text
}`
console.error(
new Error(
`${(<ErrorSoapResponse>res).Body.Fault.Detail?.Error?.Detail}: ${
(<ErrorSoapResponse>res).Body.Fault.Reason?.Text
}`
)
);
}
if (res.Header?.context) {
Expand Down
38 changes: 20 additions & 18 deletions src/network/get-info.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,36 +28,38 @@ const parsePollingInterval = (settings: AccountSettings): number => {
}
return pollingValue * 1000;
};

export const getInfo = (): Promise<void> =>
getSoapFetch(SHELL_APP_ID)<{ _jsns: string; rights: string }, GetInfoResponse>('GetInfo', {
_jsns: 'urn:zimbraAccount',
rights: 'sendAs,sendAsDistList,viewFreeBusy,sendOnBehalfOf,sendOnBehalfOfDistList'
})
.then((res: any): void => {
fetch('/static/iris/components.json')
.then((r) => r.json())
.then(({ components }: { components: Array<CarbonioModule> }) => {
useAppStore.getState().setters.addApps(
filter(components, ({ type }) => {
if (type === 'shell' || type === 'carbonio') return true;
return false;
})
);
})
.then(() =>
getSoapFetch(SHELL_APP_ID)<{ _jsns: string; rights: string }, GetInfoResponse>('GetInfo', {
_jsns: 'urn:zimbraAccount',
rights: 'sendAs,sendAsDistList,viewFreeBusy,sendOnBehalfOf,sendOnBehalfOfDistList'
})
)
.then((res: GetInfoResponse): void => {
if (res) {
const { account, settings, version } = normalizeAccount(res);
useNetworkStore.setState({
pollingInterval: parsePollingInterval(settings)
});
useAccountStore.setState({
authenticated: true,
account,
settings,
zimbraVersion: version
});
}
})
.then(() => fetch('/static/iris/components.json'))
.then((r: any) => r.json())
.then(({ components }: { components: Array<CarbonioModule> }) => {
useAppStore.getState().setters.addApps(
filter(components, ({ type }) => {
if (type === 'shell' || type === 'carbonio') return true;
return false;
})
);
})
.catch((err: unknown) => {
console.log('there was an error checking user data');
.catch((err: Error) => {
console.error(err);
goToLogin();
});
27 changes: 5 additions & 22 deletions src/shell/shell-primary-bar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,7 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/

import {
Container,
IconButton,
Row,
Tooltip,
Text,
Padding,
Icon
} from '@zextras/carbonio-design-system';
import { Container, IconButton, Row, Tooltip } from '@zextras/carbonio-design-system';
import { map, isEmpty, trim, filter, sortBy } from 'lodash';
import React, { useContext, FC, useState, useEffect, useMemo } from 'react';
import styled from 'styled-components';
Expand All @@ -26,18 +18,7 @@ import { AppRoute, PrimaryAccessoryView, PrimaryBarView } from '../../types';
import BadgeWrap from './badge-wrap';
import AppContextProvider from '../boot/app/app-context-provider';
import { checkRoute } from '../utility-bar/utils';

const PrimaryContainer = styled(Container)<{ active: boolean }>`
background: ${({ theme, active }): string => theme.palette[active ? 'gray4' : 'gray6'].regular};
cursor: pointer;
transition: background 0.2s ease-out;
&:hover {
background: ${({ theme, active }): string => theme.palette[active ? 'gray4' : 'gray6'].hover};
}
&:focus {
background: ${({ theme, active }): string => theme.palette[active ? 'gray4' : 'gray6'].focus};
}
`;
import { IS_STANDALONE } from '../constants';

const ContainerWithDivider = styled(Container)`
border-right: 1px solid ${({ theme }): string => theme.palette.gray3.regular};
Expand Down Expand Up @@ -107,7 +88,6 @@ const PrimaryBarAccessoryElement: FC<PrimaryBarAccessoryItemProps> = ({ view })

const ShellPrimaryBar: FC<{ activeRoute: AppRoute }> = ({ activeRoute }) => {
const primaryBarViews = useAppStore((s) => s.views.primaryBar);

const [routes, setRoutes] = useState<Record<string, string>>({});
const history = useHistory();

Expand Down Expand Up @@ -137,6 +117,9 @@ const ShellPrimaryBar: FC<{ activeRoute: AppRoute }> = ({ activeRoute }) => {
),
[activeRoute, primaryBarAccessoryViews]
);
if (IS_STANDALONE && activeRoute?.standalone?.hidePrimaryBar) {
return null;
}
return (
<ContainerWithDivider
width={49}
Expand Down
31 changes: 23 additions & 8 deletions src/shell/shell-view.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,11 @@ import ShellHeader from './shell-header';
import ShellNavigationBar from './shell-navigation-bar';
import AppBoardWindow from './boards/app-board-window';
import { ThemeCallbacksContext } from '../boot/theme-provider';
import { useUserSettings } from '../store/account';
import { useAccountStore, useUserSettings } from '../store/account';
import { ShellUtilityBar, ShellUtilityPanel } from '../utility-bar';
import { useCurrentRoute } from '../history/hooks';
import { IS_STANDALONE } from '../constants';
import { goToLogin } from '../network/go-to-login';

const Background = styled.div`
background: ${({ theme }) => theme.palette.gray6.regular};
Expand All @@ -43,20 +45,33 @@ function DarkReaderListener() {
return null;
}

const useLoginRedirection = (activeRoute) => {
const auth = useAccountStore((s) => s.authenticated);
console.log({ auth, activeRoute });
useEffect(() => {
if (IS_STANDALONE && !auth && activeRoute && !activeRoute.standalone?.allowUnauthenticated) {
goToLogin();
}
}, [activeRoute, auth]);
};

export function Shell() {
const [mobileNavOpen, setMobileNavOpen] = useState(false);
const activeRoute = useCurrentRoute();
useLoginRedirection(activeRoute);
return (
<Background>
<DarkReaderListener />
{/* <MainAppRerouter /> */}
<ShellHeader
activeRoute={activeRoute}
mobileNavIsOpen={mobileNavOpen}
onMobileMenuClick={() => setMobileNavOpen(!mobileNavOpen)}
>
<ShellUtilityBar />
</ShellHeader>
{!(IS_STANDALONE && activeRoute?.standalone?.hideShellHeader) && (
<ShellHeader
activeRoute={activeRoute}
mobileNavIsOpen={mobileNavOpen}
onMobileMenuClick={() => setMobileNavOpen(!mobileNavOpen)}
>
<ShellUtilityBar />
</ShellHeader>
)}
<Row crossAlignment="unset" style={{ position: 'relative', flexGrow: '1' }}>
<ShellNavigationBar activeRoute={activeRoute} mobileNavIsOpen={mobileNavOpen} />
<AppViewContainer activeRoute={activeRoute} />
Expand Down
1 change: 1 addition & 0 deletions src/store/account/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { AccountState } from '../../../types';
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
export const useAccountStore = create<AccountState>(() => ({
authenticated: false,
account: undefined,
version: '',
settings: {
Expand Down
Loading

0 comments on commit b904928

Please sign in to comment.