From 86f9ab693dcad951271f727046214b03d91ebd69 Mon Sep 17 00:00:00 2001 From: muhamedsalih-tw <104364298+muhamedsalih-tw@users.noreply.github.com> Date: Sun, 20 Nov 2022 17:35:21 +0530 Subject: Transform Todo feature, ServiceBarTargetUrl, ServiceIcon, TeamDashboard, Slider, Loader & WorkspaceSwitchningIndicator into ts (#782) --- .../settings/settings/EditSettingsForm.tsx | 2 +- src/components/settings/team/TeamDashboard.js | 209 -------------------- src/components/settings/team/TeamDashboard.tsx | 204 +++++++++++++++++++ src/components/ui/FullscreenLoader/index.js | 52 ----- src/components/ui/FullscreenLoader/index.tsx | 54 +++++ src/components/ui/Loader.tsx | 27 ++- src/components/ui/ServiceIcon.js | 60 ------ src/components/ui/ServiceIcon.tsx | 52 +++++ src/components/ui/Slider.js | 67 ------- src/components/ui/Slider.tsx | 69 +++++++ src/components/ui/StatusBarTargetUrl.js | 36 ---- src/components/ui/StatusBarTargetUrl.tsx | 30 +++ src/components/ui/WebviewLoader/index.js | 37 ---- src/components/ui/WebviewLoader/index.tsx | 44 +++++ src/components/ui/WebviewLoader/styles.ts | 9 - src/features/todos/actions.ts | 14 +- src/features/todos/components/TodosWebview.js | 203 ------------------- src/features/todos/components/TodosWebview.tsx | 219 +++++++++++++++++++++ src/features/todos/containers/TodosScreen.js | 52 ----- src/features/todos/containers/TodosScreen.tsx | 49 +++++ src/features/utils/FeatureStore.js | 40 ---- src/features/utils/FeatureStore.ts | 48 +++++ .../components/WorkspaceSwitchingIndicator.js | 90 --------- .../components/WorkspaceSwitchingIndicator.tsx | 90 +++++++++ src/helpers/url-helpers.ts | 13 +- src/stores/FeaturesStore.ts | 17 +- 26 files changed, 894 insertions(+), 893 deletions(-) delete mode 100644 src/components/settings/team/TeamDashboard.js create mode 100644 src/components/settings/team/TeamDashboard.tsx delete mode 100644 src/components/ui/FullscreenLoader/index.js create mode 100644 src/components/ui/FullscreenLoader/index.tsx delete mode 100644 src/components/ui/ServiceIcon.js create mode 100644 src/components/ui/ServiceIcon.tsx delete mode 100644 src/components/ui/Slider.js create mode 100644 src/components/ui/Slider.tsx delete mode 100644 src/components/ui/StatusBarTargetUrl.js create mode 100644 src/components/ui/StatusBarTargetUrl.tsx delete mode 100644 src/components/ui/WebviewLoader/index.js create mode 100644 src/components/ui/WebviewLoader/index.tsx delete mode 100644 src/components/ui/WebviewLoader/styles.ts delete mode 100644 src/features/todos/components/TodosWebview.js create mode 100644 src/features/todos/components/TodosWebview.tsx delete mode 100644 src/features/todos/containers/TodosScreen.js create mode 100644 src/features/todos/containers/TodosScreen.tsx delete mode 100644 src/features/utils/FeatureStore.js create mode 100644 src/features/utils/FeatureStore.ts delete mode 100644 src/features/workspaces/components/WorkspaceSwitchingIndicator.js create mode 100644 src/features/workspaces/components/WorkspaceSwitchingIndicator.tsx diff --git a/src/components/settings/settings/EditSettingsForm.tsx b/src/components/settings/settings/EditSettingsForm.tsx index 8ccad9e49..0a05cb0e1 100644 --- a/src/components/settings/settings/EditSettingsForm.tsx +++ b/src/components/settings/settings/EditSettingsForm.tsx @@ -650,7 +650,7 @@ class EditSettingsForm extends Component { <> this.submit(e)} + onSliderChange={e => this.submit(e)} field={form.$('grayscaleServicesDim')} />
diff --git a/src/components/settings/team/TeamDashboard.js b/src/components/settings/team/TeamDashboard.js deleted file mode 100644 index 538a9a10c..000000000 --- a/src/components/settings/team/TeamDashboard.js +++ /dev/null @@ -1,209 +0,0 @@ -import { Component } from 'react'; -import PropTypes from 'prop-types'; -import { observer } from 'mobx-react'; -import { defineMessages, injectIntl } from 'react-intl'; -import ReactTooltip from 'react-tooltip'; -import injectSheet from 'react-jss'; -import classnames from 'classnames'; - -import Loader from '../../ui/Loader'; -import Button from '../../ui/button'; -import Infobox from '../../ui/Infobox'; -import { H1 } from '../../ui/headline'; -import { LIVE_FRANZ_API } from '../../../config'; - -const messages = defineMessages({ - headline: { - id: 'settings.team.headline', - defaultMessage: 'Team', - }, - contentHeadline: { - id: 'settings.team.contentHeadline', - defaultMessage: 'Franz Team Management', - }, - intro: { - id: 'settings.team.intro', - defaultMessage: - 'You are currently using Franz Servers, which is why you have access to Team Management.', - }, - copy: { - id: 'settings.team.copy', - defaultMessage: - "Franz's Team Management allows you to manage Franz Subscriptions for multiple users. Please keep in mind that having a Franz Premium subscription will give you no advantages in using Ferdium: The only reason you still have access to Team Management is so you can manage your legacy Franz Teams and so that you don't loose any functionality in managing your account.", - }, - manageButton: { - id: 'settings.team.manageAction', - defaultMessage: 'Manage your Team on meetfranz.com', - }, - teamsUnavailable: { - id: 'settings.team.teamsUnavailable', - defaultMessage: 'Teams are unavailable', - }, - teamsUnavailableInfo: { - id: 'settings.team.teamsUnavailableInfo', - defaultMessage: - 'Teams are currently only available when using the Franz Server and after paying for Franz Professional. Please change your server to https://api.franzinfra.com to use teams.', - }, - tryReloadUserInfoRequest: { - id: 'settings.team.tryReloadUserInfoRequest', - defaultMessage: 'Try reloading', - }, - userInfoRequestFailed: { - id: 'settings.team.userInfoRequestFailed', - defaultMessage: 'User Info request failed', - }, -}); - -const styles = { - cta: { - margin: [40, 'auto'], - height: 'auto', - }, - container: { - display: 'flex', - flexDirection: 'column', - height: 'auto', - - '@media(min-width: 800px)': { - flexDirection: 'row', - }, - }, - content: { - height: 'auto', - order: 1, - - '@media(min-width: 800px)': { - order: 0, - }, - }, - image: { - display: 'block', - height: 150, - order: 0, - margin: [0, 'auto', 40, 'auto'], - - '@media(min-width: 800px)': { - marginLeft: 40, - order: 1, - }, - }, - headline: { - marginBottom: 0, - }, - headlineWithSpacing: { - marginBottom: 'inherit', - }, - buttonContainer: { - display: 'flex', - height: 'auto', - }, -}; - -class TeamDashboard extends Component { - static propTypes = { - isLoading: PropTypes.bool.isRequired, - userInfoRequestFailed: PropTypes.bool.isRequired, - retryUserInfoRequest: PropTypes.func.isRequired, - openTeamManagement: PropTypes.func.isRequired, - classes: PropTypes.object.isRequired, - server: PropTypes.string.isRequired, - }; - - render() { - const { - isLoading, - userInfoRequestFailed, - retryUserInfoRequest, - openTeamManagement, - classes, - server, - } = this.props; - const { intl } = this.props; - - if (server === LIVE_FRANZ_API) { - return ( -
-
- - {intl.formatMessage(messages.headline)} - -
-
- {isLoading && } - - {!isLoading && userInfoRequestFailed && ( - - {intl.formatMessage(messages.userInfoRequestFailed)} - - )} - - {!userInfoRequestFailed && !isLoading && ( - <> -

- {intl.formatMessage(messages.contentHeadline)} -

-
-
-

{intl.formatMessage(messages.intro)}

-

{intl.formatMessage(messages.copy)}

-
- Ferdium for Teams -
-
-
- - )} -
- -
- ); - } - return ( -
-
- - {intl.formatMessage(messages.headline)} - -
-
-

- {intl.formatMessage(messages.teamsUnavailable)} -

-

- {intl.formatMessage(messages.teamsUnavailableInfo)} -

-
-
- ); - } -} - -export default injectIntl( - injectSheet(styles, { injectTheme: true })(observer(TeamDashboard)), -); diff --git a/src/components/settings/team/TeamDashboard.tsx b/src/components/settings/team/TeamDashboard.tsx new file mode 100644 index 000000000..3ef55fac6 --- /dev/null +++ b/src/components/settings/team/TeamDashboard.tsx @@ -0,0 +1,204 @@ +import { Component, ReactElement } from 'react'; +import { observer } from 'mobx-react'; +import { defineMessages, injectIntl, WrappedComponentProps } from 'react-intl'; +import ReactTooltip from 'react-tooltip'; +import withStyles, { WithStylesProps } from 'react-jss'; +import classnames from 'classnames'; +import Loader from '../../ui/Loader'; +import Button from '../../ui/button'; +import Infobox from '../../ui/Infobox'; +import { H1 } from '../../ui/headline'; +import { LIVE_FRANZ_API } from '../../../config'; + +const messages = defineMessages({ + headline: { + id: 'settings.team.headline', + defaultMessage: 'Team', + }, + contentHeadline: { + id: 'settings.team.contentHeadline', + defaultMessage: 'Franz Team Management', + }, + intro: { + id: 'settings.team.intro', + defaultMessage: + 'You are currently using Franz Servers, which is why you have access to Team Management.', + }, + copy: { + id: 'settings.team.copy', + defaultMessage: + "Franz's Team Management allows you to manage Franz Subscriptions for multiple users. Please keep in mind that having a Franz Premium subscription will give you no advantages in using Ferdium: The only reason you still have access to Team Management is so you can manage your legacy Franz Teams and so that you don't loose any functionality in managing your account.", + }, + manageButton: { + id: 'settings.team.manageAction', + defaultMessage: 'Manage your Team on meetfranz.com', + }, + teamsUnavailable: { + id: 'settings.team.teamsUnavailable', + defaultMessage: 'Teams are unavailable', + }, + teamsUnavailableInfo: { + id: 'settings.team.teamsUnavailableInfo', + defaultMessage: + 'Teams are currently only available when using the Franz Server and after paying for Franz Professional. Please change your server to https://api.franzinfra.com to use teams.', + }, + tryReloadUserInfoRequest: { + id: 'settings.team.tryReloadUserInfoRequest', + defaultMessage: 'Try reloading', + }, + userInfoRequestFailed: { + id: 'settings.team.userInfoRequestFailed', + defaultMessage: 'User Info request failed', + }, +}); + +const styles = { + cta: { + margin: [40, 'auto'], + height: 'auto', + }, + container: { + display: 'flex', + flexDirection: 'column', + height: 'auto', + + '@media(min-width: 800px)': { + flexDirection: 'row', + }, + }, + content: { + height: 'auto', + order: 1, + + '@media(min-width: 800px)': { + order: 0, + }, + }, + image: { + display: 'block', + height: 150, + order: 0, + margin: [0, 'auto', 40, 'auto'], + + '@media(min-width: 800px)': { + marginLeft: 40, + order: 1, + }, + }, + headline: { + marginBottom: 0, + }, + headlineWithSpacing: { + marginBottom: 'inherit', + }, + buttonContainer: { + display: 'flex', + height: 'auto', + }, +}; + +interface IProps extends WithStylesProps, WrappedComponentProps { + isLoading: boolean; + userInfoRequestFailed: boolean; + retryUserInfoRequest: () => void; + openTeamManagement: () => void; + server: string; +} + +@observer +class TeamDashboard extends Component { + render(): ReactElement { + const { + isLoading, + userInfoRequestFailed, + retryUserInfoRequest, + openTeamManagement, + classes, + server, + intl, + } = this.props; + + return server === LIVE_FRANZ_API ? ( +
+
+ + {intl.formatMessage(messages.headline)} + +
+
+ {isLoading && } + + {!isLoading && userInfoRequestFailed && ( + + {intl.formatMessage(messages.userInfoRequestFailed)} + + )} + + {!userInfoRequestFailed && !isLoading && ( + <> +

+ {intl.formatMessage(messages.contentHeadline)} +

+
+
+

{intl.formatMessage(messages.intro)}

+

{intl.formatMessage(messages.copy)}

+
+ Ferdium for Teams +
+
+
+ + )} +
+ +
+ ) : ( +
+
+ + {intl.formatMessage(messages.headline)} + +
+
+

+ {intl.formatMessage(messages.teamsUnavailable)} +

+

+ {intl.formatMessage(messages.teamsUnavailableInfo)} +

+
+
+ ); + } +} + +export default injectIntl( + withStyles(styles, { injectTheme: true })(TeamDashboard), +); diff --git a/src/components/ui/FullscreenLoader/index.js b/src/components/ui/FullscreenLoader/index.js deleted file mode 100644 index f8c6b92ee..000000000 --- a/src/components/ui/FullscreenLoader/index.js +++ /dev/null @@ -1,52 +0,0 @@ -import { Component } from 'react'; -import PropTypes from 'prop-types'; -import { observer } from 'mobx-react'; -import injectStyle from 'react-jss'; -import classnames from 'classnames'; - -import Loader from '../Loader'; - -import styles from './styles'; -import { H1 } from '../headline'; - -class FullscreenLoader extends Component { - static propTypes = { - className: PropTypes.string, - title: PropTypes.string, - classes: PropTypes.object.isRequired, - theme: PropTypes.object.isRequired, - spinnerColor: PropTypes.string, - children: PropTypes.node, - }; - - static defaultProps = { - className: null, - spinnerColor: null, - children: null, - title: null, - }; - - render() { - const { classes, title, children, spinnerColor, className, theme } = - this.props; - - return ( -
-
-

{title}

- - {children &&
{children}
} -
-
- ); - } -} - -export default injectStyle(styles, { injectTheme: true })( - observer(FullscreenLoader), -); diff --git a/src/components/ui/FullscreenLoader/index.tsx b/src/components/ui/FullscreenLoader/index.tsx new file mode 100644 index 000000000..002ee7932 --- /dev/null +++ b/src/components/ui/FullscreenLoader/index.tsx @@ -0,0 +1,54 @@ +import { Component, ReactElement, ReactNode } from 'react'; +import { observer } from 'mobx-react'; +import withStyles, { WithStylesProps } from 'react-jss'; +import classnames from 'classnames'; +import Loader from '../Loader'; +import styles from './styles'; +import { H1 } from '../headline'; +import { Theme } from '../../../themes'; + +interface IProps extends WithStylesProps { + className?: string; + title?: string; + theme?: Theme; + spinnerColor?: string; + loaded?: boolean; + children?: ReactNode; +} + +@observer +class FullscreenLoader extends Component { + render(): ReactElement { + const { + classes, + theme = '', + className = '', + spinnerColor = '', + children = null, + title = '', + loaded = false, + } = this.props; + + return ( +
+
+

{title}

+ + {children &&
{children}
} +
+
+ ); + } +} + +export default withStyles(styles, { injectTheme: true })(FullscreenLoader); diff --git a/src/components/ui/Loader.tsx b/src/components/ui/Loader.tsx index 5e78ed47a..ebb437d9d 100644 --- a/src/components/ui/Loader.tsx +++ b/src/components/ui/Loader.tsx @@ -1,4 +1,4 @@ -import { Component, PropsWithChildren } from 'react'; +import { Component, ReactElement, ReactNode } from 'react'; import { observer, inject } from 'mobx-react'; import Loader from 'react-loader'; @@ -9,31 +9,30 @@ interface IProps { color?: string; loaded?: boolean; stores?: FerdiumStores; + children?: ReactNode; } // Can this file be merged into the './loader/index.tsx' file? @inject('stores') @observer -class LoaderComponent extends Component> { - static defaultProps = { - loaded: false, - color: 'ACCENT', - }; +class LoaderComponent extends Component { + render(): ReactElement { + const { + loaded = false, + color = 'ACCENT', + className, + children, + } = this.props; - render() { - const { children, loaded, className } = this.props; - - const color = - this.props.color !== 'ACCENT' - ? this.props.color - : this.props.stores!.settings.app.accentColor; + const loaderColor = + color !== 'ACCENT' ? color : this.props.stores!.settings.app.accentColor; return ( diff --git a/src/components/ui/ServiceIcon.js b/src/components/ui/ServiceIcon.js deleted file mode 100644 index b05d791be..000000000 --- a/src/components/ui/ServiceIcon.js +++ /dev/null @@ -1,60 +0,0 @@ -import { Component } from 'react'; -import PropTypes from 'prop-types'; -import { observer } from 'mobx-react'; -import injectSheet from 'react-jss'; -import classnames from 'classnames'; - -import ServiceModel from '../../models/Service'; - -const styles = theme => ({ - root: { - height: 'auto', - }, - icon: { - width: theme.serviceIcon.width, - }, - isCustomIcon: { - width: theme.serviceIcon.isCustom.width, - border: theme.serviceIcon.isCustom.border, - borderRadius: theme.serviceIcon.isCustom.borderRadius, - }, - isDisabled: { - filter: 'grayscale(100%)', - opacity: '.5', - }, -}); - -// Should this file be converted into the coding style similar to './toggle/index.tsx'? -class ServiceIcon extends Component { - static propTypes = { - classes: PropTypes.object.isRequired, - service: PropTypes.instanceOf(ServiceModel).isRequired, - className: PropTypes.string, - }; - - static defaultProps = { - className: '', - }; - - render() { - const { classes, className, service } = this.props; - - return ( -
- -
- ); - } -} - -export default injectSheet(styles, { injectTheme: true })( - observer(ServiceIcon), -); diff --git a/src/components/ui/ServiceIcon.tsx b/src/components/ui/ServiceIcon.tsx new file mode 100644 index 000000000..39a32e44d --- /dev/null +++ b/src/components/ui/ServiceIcon.tsx @@ -0,0 +1,52 @@ +import { Component, ReactNode } from 'react'; +import { observer } from 'mobx-react'; +import withStyles, { WithStylesProps } from 'react-jss'; +import classnames from 'classnames'; +import ServiceModel from '../../models/Service'; + +const styles = theme => ({ + root: { + height: 'auto', + }, + icon: { + width: theme.serviceIcon.width, + }, + isCustomIcon: { + width: theme.serviceIcon.isCustom.width, + border: theme.serviceIcon.isCustom.border, + borderRadius: theme.serviceIcon.isCustom.borderRadius, + }, + isDisabled: { + filter: 'grayscale(100%)', + opacity: '.5', + }, +}); + +interface IProps extends WithStylesProps { + service: ServiceModel; + className?: string; +} + +// TODO - [TS DEBT] Should this file be converted into the coding style similar to './toggle/index.tsx'? +@observer +class ServiceIcon extends Component { + render(): ReactNode { + const { classes, className = '', service } = this.props; + + return ( +
+ +
+ ); + } +} + +export default withStyles(styles, { injectTheme: true })(ServiceIcon); diff --git a/src/components/ui/Slider.js b/src/components/ui/Slider.js deleted file mode 100644 index 90f4df1c4..000000000 --- a/src/components/ui/Slider.js +++ /dev/null @@ -1,67 +0,0 @@ -import { Component } from 'react'; -import PropTypes from 'prop-types'; -import { observer } from 'mobx-react'; -import classnames from 'classnames'; -import { Field } from 'mobx-react-form'; - -// Should this file be converted into the coding style similar to './toggle/index.tsx'? -class Slider extends Component { - static propTypes = { - field: PropTypes.instanceOf(Field).isRequired, - className: PropTypes.string, - showLabel: PropTypes.bool, - disabled: PropTypes.bool, - }; - - static defaultProps = { - className: '', - showLabel: true, - disabled: false, - }; - - onChange(e) { - const { field } = this.props; - - field.onChange(e); - } - - render() { - const { field, className, showLabel, disabled } = this.props; - - if (field.value === '' && field.default !== '') { - field.value = field.default; - } - - return ( -
-
- (!disabled ? this.onChange(e) : null)} - /> -
- - {field.error &&
{field.error}
} - {field.label && showLabel && ( - - )} -
- ); - } -} - -export default observer(Slider); diff --git a/src/components/ui/Slider.tsx b/src/components/ui/Slider.tsx new file mode 100644 index 000000000..ed9fe9073 --- /dev/null +++ b/src/components/ui/Slider.tsx @@ -0,0 +1,69 @@ +import { ChangeEvent, Component, ReactElement } from 'react'; +import { observer } from 'mobx-react'; +import classnames from 'classnames'; +import { noop } from 'lodash'; + +interface IProps { + field: any; + className?: string; + showLabel?: boolean; + disabled?: boolean; + type?: 'range' | 'number'; + onSliderChange?: (e: ChangeEvent) => void; +} + +// TODO - [TS DEBT] Should this file be converted into the coding style similar to './toggle/index.tsx'? +@observer +class Slider extends Component { + onChange(e: ChangeEvent): void { + const { field, onSliderChange = noop } = this.props; + field.onChange(e); + onSliderChange(e); + } + + render(): ReactElement { + const { + field, + className = '', + showLabel = true, + disabled = false, + type = 'range', + } = this.props; + + if (field.value === '' && field.default !== '') { + field.value = field.default; + } + + return ( +
+
+ (!disabled ? this.onChange(e) : null)} + /> +
+ + {field.error &&
{field.error}
} + {field.label && showLabel && ( + + )} +
+ ); + } +} + +export default Slider; diff --git a/src/components/ui/StatusBarTargetUrl.js b/src/components/ui/StatusBarTargetUrl.js deleted file mode 100644 index 3e0c98c5d..000000000 --- a/src/components/ui/StatusBarTargetUrl.js +++ /dev/null @@ -1,36 +0,0 @@ -import { Component } from 'react'; -import PropTypes from 'prop-types'; -import { observer } from 'mobx-react'; -import classnames from 'classnames'; - -import Appear from './effects/Appear'; - -// Should this file be converted into the coding style similar to './toggle/index.tsx'? -class StatusBarTargetUrl extends Component { - static propTypes = { - className: PropTypes.string, - text: PropTypes.string, - }; - - static defaultProps = { - className: '', - text: '', - }; - - render() { - const { className, text } = this.props; - - return ( - -
{text}
-
- ); - } -} - -export default observer(StatusBarTargetUrl); diff --git a/src/components/ui/StatusBarTargetUrl.tsx b/src/components/ui/StatusBarTargetUrl.tsx new file mode 100644 index 000000000..7b053f410 --- /dev/null +++ b/src/components/ui/StatusBarTargetUrl.tsx @@ -0,0 +1,30 @@ +import { Component } from 'react'; +import { observer } from 'mobx-react'; +import classnames from 'classnames'; +import Appear from './effects/Appear'; + +interface IProps { + className?: string; + text?: string; +} + +// TODO - [TS DEBT] Should this file be converted into the coding style similar to './toggle/index.tsx'? +@observer +class StatusBarTargetUrl extends Component { + render() { + const { className = '', text = '' } = this.props; + + return ( + +
{text}
+
+ ); + } +} + +export default StatusBarTargetUrl; diff --git a/src/components/ui/WebviewLoader/index.js b/src/components/ui/WebviewLoader/index.js deleted file mode 100644 index 20945d191..000000000 --- a/src/components/ui/WebviewLoader/index.js +++ /dev/null @@ -1,37 +0,0 @@ -import { Component } from 'react'; -import PropTypes from 'prop-types'; -import { observer } from 'mobx-react'; -import injectSheet from 'react-jss'; -import { defineMessages, injectIntl } from 'react-intl'; - -import FullscreenLoader from '../FullscreenLoader'; -import styles from './styles'; - -const messages = defineMessages({ - loading: { - id: 'service.webviewLoader.loading', - defaultMessage: 'Loading {service}', - }, -}); - -class WebviewLoader extends Component { - static propTypes = { - name: PropTypes.string.isRequired, - classes: PropTypes.object.isRequired, - }; - - render() { - const { classes, name } = this.props; - const { intl } = this.props; - return ( - - ); - } -} - -export default injectIntl( - injectSheet(styles, { injectTheme: true })(observer(WebviewLoader)), -); diff --git a/src/components/ui/WebviewLoader/index.tsx b/src/components/ui/WebviewLoader/index.tsx new file mode 100644 index 000000000..81923b6ca --- /dev/null +++ b/src/components/ui/WebviewLoader/index.tsx @@ -0,0 +1,44 @@ +import { Component, ReactElement } from 'react'; +import { observer } from 'mobx-react'; +import injectSheet, { WithStylesProps } from 'react-jss'; +import { defineMessages, injectIntl, WrappedComponentProps } from 'react-intl'; +import FullscreenLoader from '../FullscreenLoader'; + +const messages = defineMessages({ + loading: { + id: 'service.webviewLoader.loading', + defaultMessage: 'Loading {service}', + }, +}); + +const styles = theme => ({ + component: { + background: theme.colorWebviewLoaderBackground, + padding: 20, + width: 'auto', + margin: [0, 'auto'], + borderRadius: 6, + }, +}); + +interface IProps extends WithStylesProps, WrappedComponentProps { + name: string; + loaded?: boolean; +} + +class WebviewLoader extends Component { + render(): ReactElement { + const { classes, name, loaded = false, intl } = this.props; + return ( + + ); + } +} + +export default injectIntl( + injectSheet(styles, { injectTheme: true })(observer(WebviewLoader)), +); diff --git a/src/components/ui/WebviewLoader/styles.ts b/src/components/ui/WebviewLoader/styles.ts deleted file mode 100644 index dbd75db8a..000000000 --- a/src/components/ui/WebviewLoader/styles.ts +++ /dev/null @@ -1,9 +0,0 @@ -export default theme => ({ - component: { - background: theme.colorWebviewLoaderBackground, - padding: 20, - width: 'auto', - margin: [0, 'auto'], - borderRadius: 6, - }, -}); diff --git a/src/features/todos/actions.ts b/src/features/todos/actions.ts index b47a076b9..31b14d40b 100644 --- a/src/features/todos/actions.ts +++ b/src/features/todos/actions.ts @@ -1,17 +1,19 @@ +import { Webview } from 'react-electron-web-view'; import PropTypes from 'prop-types'; -import { ReactElement } from 'react'; import { createActionsFromDefinitions } from '../../actions/lib/actions'; +export interface TodoClientMessage { + action: string; + data: object; +} + interface TodoActionsType { resize: (width: number) => void; toggleTodosPanel: () => void; toggleTodosFeatureVisibility: () => void; - setTodosWebview: (webview: ReactElement) => void; + setTodosWebview: (webview: Webview) => void; handleHostMessage: (action: string, data: object) => void; - handleClientMessage: ( - channel: string, - message: { action: string; data: object }, - ) => void; + handleClientMessage: (channel: string, message: TodoClientMessage) => void; openDevTools: () => void; reload: () => void; } diff --git a/src/features/todos/components/TodosWebview.js b/src/features/todos/components/TodosWebview.js deleted file mode 100644 index 780864b91..000000000 --- a/src/features/todos/components/TodosWebview.js +++ /dev/null @@ -1,203 +0,0 @@ -import { Component } from 'react'; -import PropTypes from 'prop-types'; -import { observer } from 'mobx-react'; -import injectSheet from 'react-jss'; -import Webview from 'react-electron-web-view'; -import classnames from 'classnames'; - -import { TODOS_PARTITION_ID } from '../../../config'; - -const styles = theme => ({ - root: { - background: theme.colorBackground, - position: 'relative', - borderLeft: [1, 'solid', theme.todos.todosLayer.borderLeftColor], - zIndex: 300, - - transform: ({ isVisible, width, isTodosServiceActive }) => - `translateX(${isVisible || isTodosServiceActive ? 0 : width}px)`, - - '& webview': { - height: '100%', - }, - }, - resizeHandler: { - position: 'absolute', - left: 0, - marginLeft: -5, - width: 10, - zIndex: 400, - cursor: 'col-resize', - }, - dragIndicator: { - position: 'absolute', - left: 0, - width: 5, - zIndex: 400, - background: theme.todos.dragIndicator.background, - }, - isTodosServiceActive: { - width: 'calc(100% - 368px)', - position: 'absolute', - right: 0, - zIndex: 0, - borderLeftWidth: 0, - }, - hidden: { - borderLeftWidth: 0, - }, -}); - -class TodosWebview extends Component { - static propTypes = { - classes: PropTypes.object.isRequired, - isTodosServiceActive: PropTypes.bool.isRequired, - isVisible: PropTypes.bool.isRequired, - handleClientMessage: PropTypes.func.isRequired, - setTodosWebview: PropTypes.func.isRequired, - resize: PropTypes.func.isRequired, - width: PropTypes.number.isRequired, - minWidth: PropTypes.number.isRequired, - userAgent: PropTypes.string.isRequired, - todoUrl: PropTypes.string.isRequired, - isTodoUrlValid: PropTypes.bool.isRequired, - }; - - state = { - isDragging: false, - width: 300, - }; - - componentDidMount() { - this.setState({ - width: this.props.width, - }); - - this.node.addEventListener('mousemove', this.resizePanel.bind(this)); - this.node.addEventListener('mouseup', this.stopResize.bind(this)); - this.node.addEventListener('mouseleave', this.stopResize.bind(this)); - } - - startResize = event => { - this.setState({ - isDragging: true, - initialPos: event.clientX, - delta: 0, - }); - }; - - resizePanel(e) { - const { minWidth } = this.props; - - const { isDragging, initialPos } = this.state; - - if (isDragging && Math.abs(e.clientX - window.innerWidth) > minWidth) { - const delta = e.clientX - initialPos; - - this.setState({ - delta, - }); - } - } - - stopResize() { - const { resize, minWidth } = this.props; - - const { isDragging, delta, width } = this.state; - - if (isDragging) { - let newWidth = width + (delta < 0 ? Math.abs(delta) : -Math.abs(delta)); - - if (newWidth < minWidth) { - newWidth = minWidth; - } - - this.setState({ - isDragging: false, - delta: 0, - width: newWidth, - }); - - resize(newWidth); - } - } - - startListeningToIpcMessages() { - const { handleClientMessage } = this.props; - if (!this.webview) return; - this.webview.addEventListener('ipc-message', e => { - handleClientMessage({ channel: e.channel, message: e.args[0] }); - }); - } - - render() { - const { - classes, - isTodosServiceActive, - isVisible, - userAgent, - todoUrl, - isTodoUrlValid, - } = this.props; - - const { width, delta, isDragging } = this.state; - - let displayedWidth = isVisible ? width : 0; - if (isTodosServiceActive) { - displayedWidth = null; - } - - return ( -
this.stopResize()} - ref={node => { - this.node = node; - }} - id="todos-panel" - > -
this.startResize(e)} - /> - {isDragging && ( -
- )} - {isTodoUrlValid && ( - { - const { setTodosWebview } = this.props; - setTodosWebview(this.webview); - this.startListeningToIpcMessages(); - }} - partition={TODOS_PARTITION_ID} - preload="./features/todos/preload.js" - ref={webview => { - this.webview = webview ? webview.view : null; - }} - useragent={userAgent} - src={todoUrl} - /> - )} -
- ); - } -} - -export default injectSheet(styles, { injectTheme: true })( - observer(TodosWebview), -); diff --git a/src/features/todos/components/TodosWebview.tsx b/src/features/todos/components/TodosWebview.tsx new file mode 100644 index 000000000..3385ff74c --- /dev/null +++ b/src/features/todos/components/TodosWebview.tsx @@ -0,0 +1,219 @@ +import { Component, createRef, ReactElement, MouseEvent } from 'react'; +import { observer } from 'mobx-react'; +import withStyles, { WithStylesProps } from 'react-jss'; +import Webview from 'react-electron-web-view'; +import classnames from 'classnames'; +import { TODOS_PARTITION_ID } from '../../../config'; +import { TodoClientMessage } from '../actions'; + +const styles = theme => ({ + root: { + background: theme.colorBackground, + position: 'relative', + borderLeft: [1, 'solid', theme.todos.todosLayer.borderLeftColor], + zIndex: 300, + + transform: ({ isVisible, width, isTodosServiceActive }) => + `translateX(${isVisible || isTodosServiceActive ? 0 : width}px)`, + + '& webview': { + height: '100%', + }, + }, + resizeHandler: { + position: 'absolute', + left: 0, + marginLeft: -5, + width: 10, + zIndex: 400, + cursor: 'col-resize', + }, + dragIndicator: { + position: 'absolute', + left: 0, + width: 5, + zIndex: 400, + background: theme.todos.dragIndicator.background, + }, + isTodosServiceActive: { + width: 'calc(100% - 368px)', + position: 'absolute', + right: 0, + zIndex: 0, + borderLeftWidth: 0, + }, + hidden: { + borderLeftWidth: 0, + }, +}); + +interface IProps extends WithStylesProps { + isTodosServiceActive: boolean; + isVisible: boolean; + handleClientMessage: (channel: string, message: TodoClientMessage) => void; + setTodosWebview: (webView: Webview) => void; + resize: (newWidth: number) => void; + width: number; + minWidth: number; + userAgent: string; + todoUrl: string; + isTodoUrlValid: boolean; +} + +interface IState { + isDragging: boolean; + width: number; + initialPos: number; + delta: number; +} + +@observer +class TodosWebview extends Component { + private node = createRef(); + + private webview: Webview; + + constructor(props: IProps) { + super(props); + + this.state = { + isDragging: false, + width: 300, + initialPos: 0, + delta: 0, + }; + this.resizePanel = this.resizePanel.bind(this); + this.stopResize = this.stopResize.bind(this); + } + + componentDidMount() { + this.setState({ + width: this.props.width, + }); + + if (this.node.current) { + this.node.current.addEventListener('mousemove', this.resizePanel); + this.node.current.addEventListener('mouseup', this.stopResize); + this.node.current.addEventListener('mouseleave', this.stopResize); + } + } + + startResize(e: MouseEvent): void { + this.setState({ + isDragging: true, + initialPos: e.clientX, + delta: 0, + }); + } + + resizePanel(e: MouseEventInit): void { + const { minWidth } = this.props; + const { isDragging, initialPos } = this.state; + + if (isDragging && Math.abs(e.clientX! - window.innerWidth) > minWidth) { + const delta = e.clientX! - initialPos; + + this.setState({ + delta, + }); + } + } + + stopResize(): void { + const { resize, minWidth } = this.props; + const { isDragging, delta, width } = this.state; + + if (isDragging) { + let newWidth = width + (delta < 0 ? Math.abs(delta) : -Math.abs(delta)); + + if (newWidth < minWidth) { + newWidth = minWidth; + } + + this.setState({ + isDragging: false, + delta: 0, + width: newWidth, + }); + + resize(newWidth); + } + } + + startListeningToIpcMessages() { + if (!this.webview) { + return; + } + + const { handleClientMessage } = this.props; + this.webview.addEventListener('ipc-message', e => { + handleClientMessage(e.channel, e.args[0]); + }); + } + + render(): ReactElement { + const { + classes, + isTodosServiceActive, + isVisible, + userAgent, + todoUrl, + isTodoUrlValid, + } = this.props; + + const { width, delta, isDragging } = this.state; + let displayedWidth = isVisible ? width : 0; + if (isTodosServiceActive) { + displayedWidth = 0; + } + + return ( +
this.stopResize()} + ref={this.node} + id="todos-panel" + > +
+ {isDragging && ( +
+ )} + {isTodoUrlValid && ( + { + const { setTodosWebview } = this.props; + setTodosWebview(this.webview); + this.startListeningToIpcMessages(); + }} + partition={TODOS_PARTITION_ID} + preload="./features/todos/preload.js" + ref={webview => { + this.webview = webview ? webview.view : null; + }} + useragent={userAgent} + src={todoUrl} + /> + )} +
+ ); + } +} + +export default withStyles(styles, { injectTheme: true })(TodosWebview); diff --git a/src/features/todos/containers/TodosScreen.js b/src/features/todos/containers/TodosScreen.js deleted file mode 100644 index b97506767..000000000 --- a/src/features/todos/containers/TodosScreen.js +++ /dev/null @@ -1,52 +0,0 @@ -import { Component } from 'react'; -import { observer, inject } from 'mobx-react'; -import PropTypes from 'prop-types'; - -import FeaturesStore from '../../../stores/FeaturesStore'; -import TodosWebview from '../components/TodosWebview'; -import ErrorBoundary from '../../../components/util/ErrorBoundary'; -import { todosStore } from '..'; -import { TODOS_MIN_WIDTH } from '../../../config'; -import { todoActions } from '../actions'; -import ServicesStore from '../../../stores/ServicesStore'; - -class TodosScreen extends Component { - render() { - if ( - !todosStore || - !todosStore.isFeatureActive || - todosStore.isTodosPanelForceHidden - ) { - return null; - } - - return ( - - todoActions.setTodosWebview({ webview })} - width={todosStore.width} - minWidth={TODOS_MIN_WIDTH} - resize={width => todoActions.resize({ width })} - userAgent={todosStore.userAgent} - todoUrl={todosStore.todoUrl} - isTodoUrlValid={todosStore.isTodoUrlValid} - /> - - ); - } -} - -export default inject('stores', 'actions')(observer(TodosScreen)); - -TodosScreen.propTypes = { - stores: PropTypes.shape({ - features: PropTypes.instanceOf(FeaturesStore).isRequired, - services: PropTypes.instanceOf(ServicesStore).isRequired, - }).isRequired, -}; diff --git a/src/features/todos/containers/TodosScreen.tsx b/src/features/todos/containers/TodosScreen.tsx new file mode 100644 index 000000000..17f61bd95 --- /dev/null +++ b/src/features/todos/containers/TodosScreen.tsx @@ -0,0 +1,49 @@ +import { Component, ReactElement } from 'react'; +import { observer, inject } from 'mobx-react'; +import TodosWebview from '../components/TodosWebview'; +import ErrorBoundary from '../../../components/util/ErrorBoundary'; +import { todosStore } from '..'; +import { TODOS_MIN_WIDTH } from '../../../config'; +import { todoActions } from '../actions'; +import { RealStores } from '../../../stores'; + +interface IProps { + stores?: RealStores; +} + +@inject('stores', 'actions') +@observer +class TodosScreen extends Component { + render(): ReactElement | null { + const showTodoScreen = + !todosStore || + !todosStore.isFeatureActive || + todosStore.isTodosPanelForceHidden; + + if (showTodoScreen) { + return null; + } + + return ( + + todoActions.setTodosWebview(webview)} + width={todosStore.width} + minWidth={TODOS_MIN_WIDTH} + resize={width => todoActions.resize(width)} + userAgent={todosStore.userAgent} + todoUrl={todosStore.todoUrl} + isTodoUrlValid={todosStore.isTodoUrlValid} + /> + + ); + } +} + +export default TodosScreen; diff --git a/src/features/utils/FeatureStore.js b/src/features/utils/FeatureStore.js deleted file mode 100644 index eada332d7..000000000 --- a/src/features/utils/FeatureStore.js +++ /dev/null @@ -1,40 +0,0 @@ -export default class FeatureStore { - _actions = []; - - _reactions = []; - - stop() { - this._stopActions(); - this._stopReactions(); - } - - // ACTIONS - - _registerActions(actions) { - this._actions = actions; - this._startActions(); - } - - _startActions(actions = this._actions) { - for (const a of actions) a.start(); - } - - _stopActions(actions = this._actions) { - for (const a of actions) a.stop(); - } - - // REACTIONS - - _registerReactions(reactions) { - this._reactions = reactions; - this._startReactions(); - } - - _startReactions(reactions = this._reactions) { - for (const r of reactions) r.start(); - } - - _stopReactions(reactions = this._reactions) { - for (const r of reactions) r.stop(); - } -} diff --git a/src/features/utils/FeatureStore.ts b/src/features/utils/FeatureStore.ts new file mode 100644 index 000000000..2bdd167f3 --- /dev/null +++ b/src/features/utils/FeatureStore.ts @@ -0,0 +1,48 @@ +import Reaction from '../../stores/lib/Reaction'; + +export default class FeatureStore { + _actions: any = []; + + _reactions: Reaction[] = []; + + stop() { + this._stopActions(); + this._stopReactions(); + } + + // ACTIONS + _registerActions(actions) { + this._actions = actions; + this._startActions(); + } + + _startActions(actions = this._actions) { + for (const action of actions) { + action.start(); + } + } + + _stopActions(actions = this._actions) { + for (const action of actions) { + action.stop(); + } + } + + // REACTIONS + _registerReactions(reactions) { + this._reactions = reactions; + this._startReactions(); + } + + _startReactions(reactions = this._reactions) { + for (const reaction of reactions) { + reaction.start(); + } + } + + _stopReactions(reactions = this._reactions) { + for (const reaction of reactions) { + reaction.stop(); + } + } +} diff --git a/src/features/workspaces/components/WorkspaceSwitchingIndicator.js b/src/features/workspaces/components/WorkspaceSwitchingIndicator.js deleted file mode 100644 index ff73758c1..000000000 --- a/src/features/workspaces/components/WorkspaceSwitchingIndicator.js +++ /dev/null @@ -1,90 +0,0 @@ -import { Component } from 'react'; -import PropTypes from 'prop-types'; -import { observer } from 'mobx-react'; -import injectSheet from 'react-jss'; -import classnames from 'classnames'; -import { defineMessages, injectIntl } from 'react-intl'; - -import Loader from '../../../components/ui/loader/index'; -import { workspaceStore } from '../index'; - -const messages = defineMessages({ - switchingTo: { - id: 'workspaces.switchingIndicator.switchingTo', - defaultMessage: 'Switching to', - }, -}); - -let wrapperTransition = 'none'; - -if (window && window.matchMedia('(prefers-reduced-motion: no-preference)')) { - wrapperTransition = 'width 0.5s ease'; -} - -const styles = theme => ({ - wrapper: { - display: 'flex', - alignItems: 'flex-start', - position: 'absolute', - transition: wrapperTransition, - width: `calc(100% - ${theme.workspaces.drawer.width}px)`, - marginTop: '20px', - }, - component: { - background: 'rgba(20, 20, 20, 0.4)', - padding: '10px 20px', - display: 'flex', - width: 'auto', - height: 'auto', - margin: [0, 'auto'], - borderRadius: 6, - alignItems: 'center', - zIndex: 200, - }, - spinner: { - width: 40, - height: 40, - marginRight: 10, - }, - message: { - fontSize: 16, - whiteSpace: 'nowrap', - color: theme.colorAppLoaderSpinner, - }, -}); - -class WorkspaceSwitchingIndicator extends Component { - static propTypes = { - classes: PropTypes.object.isRequired, - theme: PropTypes.object.isRequired, - }; - - render() { - const { classes, theme } = this.props; - const { intl } = this.props; - const { isSwitchingWorkspace, nextWorkspace } = workspaceStore; - if (!isSwitchingWorkspace) return null; - const nextWorkspaceName = nextWorkspace - ? nextWorkspace.name - : 'All services'; - return ( -
-
- -

- {`${intl.formatMessage(messages.switchingTo)} ${nextWorkspaceName}`} -

-
-
- ); - } -} - -export default injectIntl( - injectSheet(styles, { injectTheme: true })( - observer(WorkspaceSwitchingIndicator), - ), -); diff --git a/src/features/workspaces/components/WorkspaceSwitchingIndicator.tsx b/src/features/workspaces/components/WorkspaceSwitchingIndicator.tsx new file mode 100644 index 000000000..c9af22c96 --- /dev/null +++ b/src/features/workspaces/components/WorkspaceSwitchingIndicator.tsx @@ -0,0 +1,90 @@ +import { Component, ReactElement } from 'react'; +import { observer } from 'mobx-react'; +import withStyles, { WithStylesProps } from 'react-jss'; +import classnames from 'classnames'; +import { defineMessages, injectIntl, WrappedComponentProps } from 'react-intl'; +import Loader from '../../../components/ui/loader/index'; +import { workspaceStore } from '../index'; +import { Theme } from '../../../themes'; + +const messages = defineMessages({ + switchingTo: { + id: 'workspaces.switchingIndicator.switchingTo', + defaultMessage: 'Switching to', + }, +}); + +const wrapperTransition = + window && window.matchMedia('(prefers-reduced-motion: no-preference)') + ? 'width 0.5s ease' + : 'none'; + +const styles = theme => ({ + wrapper: { + display: 'flex', + alignItems: 'flex-start', + position: 'absolute', + transition: wrapperTransition, + width: `calc(100% - ${theme.workspaces.drawer.width}px)`, + marginTop: '20px', + }, + component: { + background: 'rgba(20, 20, 20, 0.4)', + padding: '10px 20px', + display: 'flex', + width: 'auto', + height: 'auto', + margin: [0, 'auto'], + borderRadius: 6, + alignItems: 'center', + zIndex: 200, + }, + spinner: { + width: 40, + height: 40, + marginRight: 10, + }, + message: { + fontSize: 16, + whiteSpace: 'nowrap', + color: theme.colorAppLoaderSpinner, + }, +}); + +interface IProps extends WithStylesProps, WrappedComponentProps { + theme?: Theme; +} + +@observer +class WorkspaceSwitchingIndicator extends Component { + render(): ReactElement | null { + const { classes, intl, theme } = this.props; + const { isSwitchingWorkspace, nextWorkspace } = workspaceStore; + + if (!isSwitchingWorkspace) { + return null; + } + + const nextWorkspaceName = nextWorkspace + ? nextWorkspace.name + : 'All services'; + + return ( +
+
+ +

+ {`${intl.formatMessage(messages.switchingTo)} ${nextWorkspaceName}`} +

+
+
+ ); + } +} + +export default injectIntl( + withStyles(styles, { injectTheme: true })(WorkspaceSwitchingIndicator), +); diff --git a/src/helpers/url-helpers.ts b/src/helpers/url-helpers.ts index 69a2cc4dc..9c5cf7752 100644 --- a/src/helpers/url-helpers.ts +++ b/src/helpers/url-helpers.ts @@ -1,14 +1,12 @@ // This is taken from: https://benjamin-altpeter.de/shell-openexternal-dangers/ - import { URL } from 'url'; import { ensureDirSync, existsSync } from 'fs-extra'; import { shell } from 'electron'; - import { ALLOWED_PROTOCOLS } from '../config'; const debug = require('../preload-safe-debug')('Ferdium:Helpers:url'); -export function isValidExternalURL(url: string | URL) { +export function isValidExternalURL(url: string | URL): boolean { let parsedUrl: URL; try { parsedUrl = new URL(url.toString()); @@ -17,13 +15,12 @@ export function isValidExternalURL(url: string | URL) { } const isAllowed = ALLOWED_PROTOCOLS.includes(parsedUrl.protocol); - debug('protocol check is', isAllowed, 'for:', url); return isAllowed; } -export function fixUrl(url: string | URL) { +export function fixUrl(url: string | URL): string { return url .toString() .replaceAll('//', '/') @@ -32,11 +29,11 @@ export function fixUrl(url: string | URL) { .replaceAll('file:/', 'file://'); } -export function isValidFileUrl(path: string) { +export function isValidFileUrl(path: string): boolean { return path.startsWith('file') && existsSync(new URL(path)); } -export async function openPath(folderName: string) { +export async function openPath(folderName: string): Promise { ensureDirSync(folderName); shell.openPath(folderName); } @@ -45,7 +42,7 @@ export async function openPath(folderName: string) { export function openExternalUrl( url: string | URL, skipValidityCheck: boolean = false, -) { +): void { const fixedUrl = fixUrl(url.toString()); debug('Open url:', fixedUrl, 'with skipValidityCheck:', skipValidityCheck); if (skipValidityCheck || isValidExternalURL(fixedUrl)) { diff --git a/src/stores/FeaturesStore.ts b/src/stores/FeaturesStore.ts index 5f43ccf84..8584b6060 100644 --- a/src/stores/FeaturesStore.ts +++ b/src/stores/FeaturesStore.ts @@ -5,7 +5,6 @@ import { observable, runInAction, } from 'mobx'; - import { Stores } from '../@types/stores.types'; import { ApiInterface } from '../api'; import { Actions } from '../actions/lib/actions'; @@ -21,6 +20,14 @@ import appearance from '../features/appearance'; import TypedStore from './lib/TypedStore'; export default class FeaturesStore extends TypedStore { + @observable features = {}; + + constructor(stores: Stores, api: ApiInterface, actions: Actions) { + super(stores, api, actions); + + makeObservable(this); + } + @observable defaultFeaturesRequest = new CachedRequest( this.api.features, 'default', @@ -31,14 +38,6 @@ export default class FeaturesStore extends TypedStore { 'features', ); - @observable features = {}; - - constructor(stores: Stores, api: ApiInterface, actions: Actions) { - super(stores, api, actions); - - makeObservable(this); - } - async setup(): Promise { this.registerReactions([ this._updateFeatures, -- cgit v1.2.3-54-g00ecf