From 137555821f172e4eadc7cf099d4270ae8fc1374e Mon Sep 17 00:00:00 2001 From: Markus Hatvan Date: Thu, 14 Oct 2021 23:32:05 +0200 Subject: chore: convert components to tsx (#2071) --- src/components/ui/FAB.js | 65 ------------ src/components/ui/FAB.tsx | 51 +++++++++ src/components/ui/ImageUpload.js | 110 ------------------- src/components/ui/ImageUpload.tsx | 108 +++++++++++++++++++ src/components/ui/Loader.js | 56 ---------- src/components/ui/Loader.tsx | 46 ++++++++ src/components/ui/Radio.js | 78 -------------- src/components/ui/Radio.tsx | 77 ++++++++++++++ src/components/ui/SearchInput.js | 112 -------------------- src/components/ui/SearchInput.tsx | 116 +++++++++++++++++++++ src/features/basicAuth/index.js | 34 ------ src/features/basicAuth/index.ts | 34 ++++++ src/features/nightlyBuilds/index.js | 41 -------- src/features/nightlyBuilds/index.ts | 41 ++++++++ src/features/todos/preload.js | 59 ----------- src/features/todos/preload.ts | 59 +++++++++++ .../workspaces/components/WorkspaceItem.js | 39 ------- .../workspaces/components/WorkspaceItem.tsx | 45 ++++++++ .../components/WorkspaceServiceListItem.js | 75 ------------- .../components/WorkspaceServiceListItem.tsx | 66 ++++++++++++ .../workspaces/containers/EditWorkspaceScreen.js | 59 ----------- .../workspaces/containers/EditWorkspaceScreen.tsx | 61 +++++++++++ .../workspaces/containers/WorkspacesScreen.js | 41 -------- .../workspaces/containers/WorkspacesScreen.tsx | 41 ++++++++ src/index.ts | 2 +- src/stores.types.ts | 9 +- 26 files changed, 753 insertions(+), 772 deletions(-) delete mode 100644 src/components/ui/FAB.js create mode 100644 src/components/ui/FAB.tsx delete mode 100644 src/components/ui/ImageUpload.js create mode 100644 src/components/ui/ImageUpload.tsx delete mode 100644 src/components/ui/Loader.js create mode 100644 src/components/ui/Loader.tsx delete mode 100644 src/components/ui/Radio.js create mode 100644 src/components/ui/Radio.tsx delete mode 100644 src/components/ui/SearchInput.js create mode 100644 src/components/ui/SearchInput.tsx delete mode 100644 src/features/basicAuth/index.js create mode 100644 src/features/basicAuth/index.ts delete mode 100644 src/features/nightlyBuilds/index.js create mode 100644 src/features/nightlyBuilds/index.ts delete mode 100644 src/features/todos/preload.js create mode 100644 src/features/todos/preload.ts delete mode 100644 src/features/workspaces/components/WorkspaceItem.js create mode 100644 src/features/workspaces/components/WorkspaceItem.tsx delete mode 100644 src/features/workspaces/components/WorkspaceServiceListItem.js create mode 100644 src/features/workspaces/components/WorkspaceServiceListItem.tsx delete mode 100644 src/features/workspaces/containers/EditWorkspaceScreen.js create mode 100644 src/features/workspaces/containers/EditWorkspaceScreen.tsx delete mode 100644 src/features/workspaces/containers/WorkspacesScreen.js create mode 100644 src/features/workspaces/containers/WorkspacesScreen.tsx (limited to 'src') diff --git a/src/components/ui/FAB.js b/src/components/ui/FAB.js deleted file mode 100644 index 55fe97e82..000000000 --- a/src/components/ui/FAB.js +++ /dev/null @@ -1,65 +0,0 @@ -/** - * Floating Action Button (FAB) - */ -import { Component } from 'react'; -import PropTypes from 'prop-types'; -import { observer } from 'mobx-react'; -import classnames from 'classnames'; - -import { oneOrManyChildElements } from '../../prop-types'; - -@observer -class Button extends Component { - static propTypes = { - className: PropTypes.string, - disabled: PropTypes.bool, - onClick: PropTypes.func, - type: PropTypes.string, - children: oneOrManyChildElements.isRequired, - htmlForm: PropTypes.string, - }; - - static defaultProps = { - className: null, - disabled: false, - onClick: () => {}, - type: 'button', - htmlForm: '', - }; - - element = null; - - render() { - const { className, disabled, onClick, type, children, htmlForm } = - this.props; - - const buttonProps = { - className: classnames({ - ferdi__fab: true, - [`${className}`]: className, - }), - type, - }; - - if (disabled) { - buttonProps.disabled = true; - } - - if (onClick) { - buttonProps.onClick = onClick; - } - - if (htmlForm) { - buttonProps.form = htmlForm; - } - - return ( - // disabling rule as button has type defined in `buttonProps` - - ); - } -} - -export default Button; diff --git a/src/components/ui/FAB.tsx b/src/components/ui/FAB.tsx new file mode 100644 index 000000000..583c9d556 --- /dev/null +++ b/src/components/ui/FAB.tsx @@ -0,0 +1,51 @@ +/** + * Floating Action Button (FAB) + */ +import { Component, ReactChildren } from 'react'; +import { observer } from 'mobx-react'; +import classnames from 'classnames'; + +type Props = { + className: string; + disabled: boolean; + onClick: () => void; + type: string; + children: ReactChildren; + htmlForm: string; +}; + +@observer +class Button extends Component { + static defaultProps = { + disabled: false, + onClick: () => {}, + type: 'button', + htmlForm: '', + }; + + element = null; + + render() { + const { className, disabled, onClick, type, children, htmlForm } = + this.props; + + const buttonProps = { + className: classnames({ + ferdi__fab: true, + [`${className}`]: className, + }), + type, + disabled, + onClick, + form: htmlForm, + }; + + return ( + + ); + } +} + +export default Button; diff --git a/src/components/ui/ImageUpload.js b/src/components/ui/ImageUpload.js deleted file mode 100644 index c51d39a9b..000000000 --- a/src/components/ui/ImageUpload.js +++ /dev/null @@ -1,110 +0,0 @@ -import { Component, Fragment } from 'react'; -import PropTypes from 'prop-types'; -import { observer } from 'mobx-react'; -import { Field } from 'mobx-react-form'; -import classnames from 'classnames'; -import Dropzone from 'react-dropzone'; -import { isWindows } from '../../environment'; - -@observer -class ImageUpload extends Component { - static propTypes = { - field: PropTypes.instanceOf(Field).isRequired, - className: PropTypes.string, - multiple: PropTypes.bool, - textDelete: PropTypes.string.isRequired, - textUpload: PropTypes.string.isRequired, - }; - - static defaultProps = { - className: null, - multiple: false, - }; - - state = { - path: null, - }; - - dropzoneRef = null; - - onDrop(acceptedFiles) { - const { field } = this.props; - - for (const file of acceptedFiles) { - const imgPath = isWindows ? file.path.replace(/\\/g, '/') : file.path; - this.setState({ - path: imgPath, - }); - - this.props.field.onDrop(file); - } - - field.set(''); - } - - render() { - const { field, className, multiple, textDelete, textUpload } = this.props; - - const cssClasses = classnames({ - 'image-upload__dropzone': true, - [`${className}`]: className, - }); - - return ( -
- -
- {(field.value && field.value !== 'delete') || this.state.path ? ( - <> -
-
- -
-
- - ) : ( - { - this.dropzoneRef = node; - }} - onDrop={this.onDrop.bind(this)} - multiple={multiple} - accept="image/jpeg, image/png, image/svg+xml" - > - {({ getRootProps, getInputProps }) => ( -
- -

{textUpload}

- -
- )} -
- )} -
-
- ); - } -} - -export default ImageUpload; diff --git a/src/components/ui/ImageUpload.tsx b/src/components/ui/ImageUpload.tsx new file mode 100644 index 000000000..4b25be502 --- /dev/null +++ b/src/components/ui/ImageUpload.tsx @@ -0,0 +1,108 @@ +import { Component } from 'react'; +import { observer } from 'mobx-react'; +import { Field } from 'mobx-react-form'; +import classnames from 'classnames'; +import Dropzone, { DropzoneRef } from 'react-dropzone'; +import { isWindows } from '../../environment'; + +type Props = { + field: typeof Field; + className: string; + multiple: boolean; + textDelete: string; + textUpload: string; +}; + +@observer +class ImageUpload extends Component { + static defaultProps = { + multiple: false, + }; + + state = { + path: null, + }; + + dropzoneRef: DropzoneRef | null = null; + + onDrop(acceptedFiles) { + const { field } = this.props; + + for (const file of acceptedFiles) { + const imgPath = isWindows ? file.path.replace(/\\/g, '/') : file.path; + this.setState({ + path: imgPath, + }); + + this.props.field.onDrop(file); + } + + field.set(''); + } + + render() { + const { field, className, multiple, textDelete, textUpload } = this.props; + + const cssClasses = classnames({ + 'image-upload__dropzone': true, + [`${className}`]: className, + }); + + return ( +
+ +
+ {(field.value && field.value !== 'delete') || this.state.path ? ( + <> +
+
+ +
+
+ + ) : ( + { + this.dropzoneRef = node; + }} + onDrop={this.onDrop.bind(this)} + multiple={multiple} + accept="image/jpeg, image/png, image/svg+xml" + > + {({ getRootProps, getInputProps }) => ( +
+ +

{textUpload}

+ +
+ )} +
+ )} +
+
+ ); + } +} + +export default ImageUpload; diff --git a/src/components/ui/Loader.js b/src/components/ui/Loader.js deleted file mode 100644 index 71c6b9552..000000000 --- a/src/components/ui/Loader.js +++ /dev/null @@ -1,56 +0,0 @@ -import { Component } from 'react'; -import { observer, inject } from 'mobx-react'; -import PropTypes from 'prop-types'; -import Loader from 'react-loader'; - -import { oneOrManyChildElements } from '../../prop-types'; - -@inject('stores') -@observer -class LoaderComponent extends Component { - static propTypes = { - children: oneOrManyChildElements, - loaded: PropTypes.bool, - className: PropTypes.string, - color: PropTypes.string, - stores: PropTypes.shape({ - settings: PropTypes.shape({ - app: PropTypes.shape({ - accentColor: PropTypes.string.isRequired, - }).isRequired, - }).isRequired, - }).isRequired, - }; - - static defaultProps = { - children: null, - loaded: false, - className: '', - color: 'ACCENT', - }; - - render() { - const { children, loaded, className } = this.props; - - const color = - this.props.color !== 'ACCENT' - ? this.props.color - : this.props.stores.settings.app.accentColor; - - return ( - - {children} - - ); - } -} - -export default LoaderComponent; diff --git a/src/components/ui/Loader.tsx b/src/components/ui/Loader.tsx new file mode 100644 index 000000000..1173c11e7 --- /dev/null +++ b/src/components/ui/Loader.tsx @@ -0,0 +1,46 @@ +import { Component, ReactChildren } from 'react'; +import { observer, inject } from 'mobx-react'; +import Loader from 'react-loader'; + +import { FerdiStores } from '../../stores.types'; + +type Props = { + children: ReactChildren; + loaded: boolean; + className: string; + color: string; + stores: FerdiStores; +}; + +@inject('stores') +@observer +class LoaderComponent extends Component { + static defaultProps = { + loaded: false, + color: 'ACCENT', + }; + + render() { + const { children, loaded, className } = this.props; + + const color = + this.props.color !== 'ACCENT' + ? this.props.color + : this.props.stores.settings.app.accentColor; + + return ( + + {children} + + ); + } +} + +export default LoaderComponent; diff --git a/src/components/ui/Radio.js b/src/components/ui/Radio.js deleted file mode 100644 index 5354dbfe1..000000000 --- a/src/components/ui/Radio.js +++ /dev/null @@ -1,78 +0,0 @@ -import { Component } from 'react'; -import PropTypes from 'prop-types'; -import { observer } from 'mobx-react'; -import { Field } from 'mobx-react-form'; -import classnames from 'classnames'; - -@observer -class Radio extends Component { - static propTypes = { - field: PropTypes.instanceOf(Field).isRequired, - className: PropTypes.string, - focus: PropTypes.bool, - showLabel: PropTypes.bool, - }; - - static defaultProps = { - className: null, - focus: false, - showLabel: true, - }; - - inputElement = null; - - componentDidMount() { - if (this.props.focus) { - this.focus(); - } - } - - focus() { - this.inputElement.focus(); - } - - render() { - const { field, className, showLabel } = this.props; - - return ( -
- {field.label && showLabel && ( - - )} -
- {field.options.map(type => ( - - ))} -
- {field.error &&
{field.error}
} -
- ); - } -} - -export default Radio; diff --git a/src/components/ui/Radio.tsx b/src/components/ui/Radio.tsx new file mode 100644 index 000000000..594ea70e4 --- /dev/null +++ b/src/components/ui/Radio.tsx @@ -0,0 +1,77 @@ +import { Component } from 'react'; +import { observer } from 'mobx-react'; +import { Field } from 'mobx-react-form'; +import classnames from 'classnames'; + +type Props = { + field: typeof Field; + className: string; + focus: boolean; + showLabel: boolean; +}; + +@observer +class Radio extends Component { + static defaultProps = { + focus: false, + showLabel: true, + }; + + inputElement = null; + + componentDidMount() { + if (this.props.focus) { + this.focus(); + } + } + + focus() { + // @ts-expect-error Object is possibly 'null'. + this.inputElement.focus(); + } + + render() { + const { field, className, showLabel } = this.props; + + return ( +
+ {field.label && showLabel && ( + + )} +
+ {field.options.map(type => ( + + ))} +
+ {field.error &&
{field.error}
} +
+ ); + } +} + +export default Radio; diff --git a/src/components/ui/SearchInput.js b/src/components/ui/SearchInput.js deleted file mode 100644 index 2e8793a2b..000000000 --- a/src/components/ui/SearchInput.js +++ /dev/null @@ -1,112 +0,0 @@ -import { Component } from 'react'; -import PropTypes from 'prop-types'; -import { observer } from 'mobx-react'; -import classnames from 'classnames'; -import { debounce } from 'lodash'; - -@observer -class SearchInput extends Component { - static propTypes = { - value: PropTypes.string, - placeholder: PropTypes.string, - className: PropTypes.string, - onChange: PropTypes.func, - onReset: PropTypes.func, - name: PropTypes.string, - throttle: PropTypes.bool, - throttleDelay: PropTypes.number, - autoFocus: PropTypes.bool, - }; - - static defaultProps = { - value: '', - placeholder: '', - className: '', - name: 'searchInput', - throttle: false, - throttleDelay: 250, - onChange: () => null, - onReset: () => null, - autoFocus: false, - }; - - input = null; - - constructor(props) { - super(props); - - this.state = { - value: props.value, - }; - - this.throttledOnChange = debounce( - this.throttledOnChange, - this.props.throttleDelay, - ); - } - - componentDidMount() { - const { autoFocus } = this.props; - - if (autoFocus) { - this.input.focus(); - } - } - - onChange(e) { - const { throttle, onChange } = this.props; - const { value } = e.target; - this.setState({ value }); - - if (throttle) { - e.persist(); - this.throttledOnChange(value); - } else { - onChange(value); - } - } - - throttledOnChange(e) { - const { onChange } = this.props; - - onChange(e); - } - - reset() { - const { onReset } = this.props; - this.setState({ value: '' }); - - onReset(); - } - - render() { - const { className, name, placeholder } = this.props; - const { value } = this.state; - - return ( -
- - {value.length > 0 && ( - this.reset()} - /> - )} -
- ); - } -} - -export default SearchInput; diff --git a/src/components/ui/SearchInput.tsx b/src/components/ui/SearchInput.tsx new file mode 100644 index 000000000..af55b0e11 --- /dev/null +++ b/src/components/ui/SearchInput.tsx @@ -0,0 +1,116 @@ +import { ChangeEvent, Component } from 'react'; +import { observer } from 'mobx-react'; +import classnames from 'classnames'; +import { debounce } from 'lodash'; + +type Props = { + value: string; + placeholder: string; + className: string; + onChange: (e: ChangeEvent) => void; + onReset: () => void; + name: string; + throttle: boolean; + throttleDelay: number; + autoFocus: boolean; +}; + +@observer +class SearchInput extends Component { + static defaultProps = { + value: '', + placeholder: '', + className: '', + name: 'searchInput', + throttle: false, + throttleDelay: 250, + onChange: () => null, + onReset: () => null, + autoFocus: false, + }; + + input = null; + + constructor(props: Props) { + super(props); + + this.state = { + value: props.value, + }; + + this.throttledOnChange = debounce( + this.throttledOnChange, + this.props.throttleDelay, + ); + } + + componentDidMount() { + const { autoFocus } = this.props; + + if (autoFocus) { + // @ts-expect-error Object is possibly 'null'. + this.input.focus(); + } + } + + onChange(e: ChangeEvent) { + const { throttle, onChange } = this.props; + const { value } = e.target; + this.setState({ value }); + + if (throttle) { + e.persist(); + // @ts-expect-error Argument of type 'string' is not assignable to parameter of type 'ChangeEvent'. + this.throttledOnChange(value); + } else { + // @ts-expect-error Argument of type 'string' is not assignable to parameter of type 'ChangeEvent'. + onChange(value); + } + } + + throttledOnChange(e: ChangeEvent) { + const { onChange } = this.props; + + onChange(e); + } + + reset() { + const { onReset } = this.props; + this.setState({ value: '' }); + + onReset(); + } + + render() { + const { className, name, placeholder } = this.props; + // @ts-expect-error Property 'value' does not exist on type 'Readonly<{}>'. + const { value } = this.state; + + return ( +
+ + {value.length > 0 && ( + this.reset()} + /> + )} +
+ ); + } +} + +export default SearchInput; diff --git a/src/features/basicAuth/index.js b/src/features/basicAuth/index.js deleted file mode 100644 index e43d51d15..000000000 --- a/src/features/basicAuth/index.js +++ /dev/null @@ -1,34 +0,0 @@ -import { ipcRenderer } from 'electron'; - -import BasicAuthComponent from './Component'; - -import { state as ModalState } from './store'; - -const debug = require('debug')('Ferdi:feature:basicAuth'); - -const state = ModalState; - -export default function initialize() { - debug('Initialize basicAuth feature'); - - window.ferdi.features.basicAuth = { - state, - }; - - ipcRenderer.on('feature:basic-auth-request', (e, data) => { - debug(e, data); - // state.serviceId = data.serviceId; - state.authInfo = data.authInfo; - state.isModalVisible = true; - }); -} - -export function mainIpcHandler(mainWindow, authInfo) { - debug('Sending basic auth call', authInfo); - - mainWindow.webContents.send('feature:basic-auth-request', { - authInfo, - }); -} - -export const Component = BasicAuthComponent; diff --git a/src/features/basicAuth/index.ts b/src/features/basicAuth/index.ts new file mode 100644 index 000000000..149ab6c19 --- /dev/null +++ b/src/features/basicAuth/index.ts @@ -0,0 +1,34 @@ +import { AuthInfo, BrowserWindow, ipcRenderer } from 'electron'; + +import BasicAuthComponent from './Component'; + +import { state as ModalState } from './store'; + +const debug = require('debug')('Ferdi:feature:basicAuth'); + +const state = ModalState; + +export default function initialize() { + debug('Initialize basicAuth feature'); + + window['ferdi'].features.basicAuth = { + state, + }; + + ipcRenderer.on('feature:basic-auth-request', (e, data) => { + debug(e, data); + // state.serviceId = data.serviceId; + state.authInfo = data.authInfo; + state.isModalVisible = true; + }); +} + +export function mainIpcHandler(mainWindow: BrowserWindow, authInfo: AuthInfo) { + debug('Sending basic auth call', authInfo); + + mainWindow.webContents.send('feature:basic-auth-request', { + authInfo, + }); +} + +export const Component = BasicAuthComponent; diff --git a/src/features/nightlyBuilds/index.js b/src/features/nightlyBuilds/index.js deleted file mode 100644 index 89bcb5cb3..000000000 --- a/src/features/nightlyBuilds/index.js +++ /dev/null @@ -1,41 +0,0 @@ -import { state as ModalState } from './store'; - -export { default as Component } from './Component'; - -const debug = require('debug')('Ferdi:feature:nightlyBuilds'); - -const state = ModalState; - -export default function initialize() { - debug('Initialize nightlyBuilds feature'); - - function showModal() { - state.isModalVisible = true; - } - - function toggleFeature() { - if (window.ferdi.stores.settings.app.nightly) { - window.ferdi.actions.settings.update({ - type: 'app', - data: { - nightly: false, - }, - }); - window.ferdi.actions.user.update({ - userData: { - nightly: false, - }, - }); - } else { - // We need to close the settings, otherwise the modal will be drawn under the settings window - window.ferdi.actions.ui.closeSettings(); - showModal(); - } - } - - window.ferdi.features.nightlyBuilds = { - state, - showModal, - toggleFeature, - }; -} diff --git a/src/features/nightlyBuilds/index.ts b/src/features/nightlyBuilds/index.ts new file mode 100644 index 000000000..14afbaf1b --- /dev/null +++ b/src/features/nightlyBuilds/index.ts @@ -0,0 +1,41 @@ +import { state as ModalState } from './store'; + +export { default as Component } from './Component'; + +const debug = require('debug')('Ferdi:feature:nightlyBuilds'); + +const state = ModalState; + +export default function initialize() { + debug('Initialize nightlyBuilds feature'); + + function showModal() { + state.isModalVisible = true; + } + + function toggleFeature() { + if (window['ferdi'].stores.settings.app.nightly) { + window['ferdi'].actions.settings.update({ + type: 'app', + data: { + nightly: false, + }, + }); + window['ferdi'].actions.user.update({ + userData: { + nightly: false, + }, + }); + } else { + // We need to close the settings, otherwise the modal will be drawn under the settings window + window['ferdi'].actions.ui.closeSettings(); + showModal(); + } + } + + window['ferdi'].features.nightlyBuilds = { + state, + showModal, + toggleFeature, + }; +} diff --git a/src/features/todos/preload.js b/src/features/todos/preload.js deleted file mode 100644 index 3b86ddbc5..000000000 --- a/src/features/todos/preload.js +++ /dev/null @@ -1,59 +0,0 @@ -import { ipcRenderer } from 'electron'; -import { IPC } from './constants'; - -const debug = require('debug')('Ferdi:feature:todos:preload'); - -debug('Preloading Todos Webview'); - -let hostMessageListener = ({ action }) => { - switch (action) { - case 'todos:initialize-as-service': - ipcRenderer.sendToHost('hello'); - break; - default: - } -}; - -window.ferdi = { - onInitialize(ipcHostMessageListener) { - hostMessageListener = ipcHostMessageListener; - ipcRenderer.sendToHost(IPC.TODOS_CLIENT_CHANNEL, { - action: 'todos:initialized', - }); - }, - sendToHost(message) { - ipcRenderer.sendToHost(IPC.TODOS_CLIENT_CHANNEL, message); - }, -}; - -ipcRenderer.on(IPC.TODOS_HOST_CHANNEL, (event, message) => { - debug('Received host message', event, message); - hostMessageListener(message); -}); - -if (window.location.href === 'https://app.franztodos.com/login/') { - // Insert info element informing about Franz accounts - const infoElement = document.createElement('p'); - infoElement.textContent = `You are using Franz's official Todo Service. -This service will only work with accounts registered with Franz - no Ferdi accounts will work here! -If you do not have a Franz account you can change the Todo service by going into Ferdi's settings and changing the "Todo server". -You can choose any service as this Todo server, e.g. Todoist or Apple Notes.`; - - // Franz Todos uses React. Because of this we can't directly insert the element into the page - // but we have to wait for React to finish rendering the login page - let numChecks = 0; - const waitForReact = setInterval(() => { - const textElement = document.querySelector('p'); - if (textElement) { - clearInterval(waitForReact); - textElement.parentElement?.insertBefore(infoElement, textElement); - } else { - numChecks += 1; - - // Stop after ~10 seconds. We are probably not on the login page - if (numChecks > 1000) { - clearInterval(waitForReact); - } - } - }, 10); -} diff --git a/src/features/todos/preload.ts b/src/features/todos/preload.ts new file mode 100644 index 000000000..31d473051 --- /dev/null +++ b/src/features/todos/preload.ts @@ -0,0 +1,59 @@ +import { ipcRenderer } from 'electron'; +import { IPC } from './constants'; + +const debug = require('debug')('Ferdi:feature:todos:preload'); + +debug('Preloading Todos Webview'); + +let hostMessageListener = ({ action }) => { + switch (action) { + case 'todos:initialize-as-service': + ipcRenderer.sendToHost('hello'); + break; + default: + } +}; + +window['ferdi'] = { + onInitialize(ipcHostMessageListener) { + hostMessageListener = ipcHostMessageListener; + ipcRenderer.sendToHost(IPC.TODOS_CLIENT_CHANNEL, { + action: 'todos:initialized', + }); + }, + sendToHost(message) { + ipcRenderer.sendToHost(IPC.TODOS_CLIENT_CHANNEL, message); + }, +}; + +ipcRenderer.on(IPC.TODOS_HOST_CHANNEL, (event, message) => { + debug('Received host message', event, message); + hostMessageListener(message); +}); + +if (window.location.href === 'https://app.franztodos.com/login/') { + // Insert info element informing about Franz accounts + const infoElement = document.createElement('p'); + infoElement.textContent = `You are using Franz's official Todo Service. +This service will only work with accounts registered with Franz - no Ferdi accounts will work here! +If you do not have a Franz account you can change the Todo service by going into Ferdi's settings and changing the "Todo server". +You can choose any service as this Todo server, e.g. Todoist or Apple Notes.`; + + // Franz Todos uses React. Because of this we can't directly insert the element into the page + // but we have to wait for React to finish rendering the login page + let numChecks = 0; + const waitForReact = setInterval(() => { + const textElement = document.querySelector('p'); + if (textElement) { + clearInterval(waitForReact); + textElement.parentElement?.insertBefore(infoElement, textElement); + } else { + numChecks += 1; + + // Stop after ~10 seconds. We are probably not on the login page + if (numChecks > 1000) { + clearInterval(waitForReact); + } + } + }, 10); +} diff --git a/src/features/workspaces/components/WorkspaceItem.js b/src/features/workspaces/components/WorkspaceItem.js deleted file mode 100644 index ff3f69dd9..000000000 --- a/src/features/workspaces/components/WorkspaceItem.js +++ /dev/null @@ -1,39 +0,0 @@ -import { Component } from 'react'; -import PropTypes from 'prop-types'; -import { observer } from 'mobx-react'; -import injectSheet from 'react-jss'; - -import Workspace from '../models/Workspace'; - -const styles = theme => ({ - row: { - height: theme.workspaces.settings.listItems.height, - borderBottom: `1px solid ${theme.workspaces.settings.listItems.borderColor}`, - '&:hover': { - background: theme.workspaces.settings.listItems.hoverBgColor, - }, - }, - columnName: {}, -}); - -@injectSheet(styles) -@observer -class WorkspaceItem extends Component { - static propTypes = { - classes: PropTypes.object.isRequired, - workspace: PropTypes.instanceOf(Workspace).isRequired, - onItemClick: PropTypes.func.isRequired, - }; - - render() { - const { classes, workspace, onItemClick } = this.props; - - return ( - - onItemClick(workspace)}>{workspace.name} - - ); - } -} - -export default WorkspaceItem; diff --git a/src/features/workspaces/components/WorkspaceItem.tsx b/src/features/workspaces/components/WorkspaceItem.tsx new file mode 100644 index 000000000..6fb02d2f5 --- /dev/null +++ b/src/features/workspaces/components/WorkspaceItem.tsx @@ -0,0 +1,45 @@ +import { Component } from 'react'; +import PropTypes from 'prop-types'; +import { observer } from 'mobx-react'; +import injectSheet from 'react-jss'; + +import Workspace from '../models/Workspace'; + +const styles = theme => ({ + row: { + height: theme.workspaces.settings.listItems.height, + borderBottom: `1px solid ${theme.workspaces.settings.listItems.borderColor}`, + '&:hover': { + background: theme.workspaces.settings.listItems.hoverBgColor, + }, + }, + columnName: {}, +}); + +type Props = { + classes: any; + workspace: any; + onItemClick: (workspace) => void; +}; + +@injectSheet(styles) +@observer +class WorkspaceItem extends Component { + static propTypes = { + classes: PropTypes.object.isRequired, + workspace: PropTypes.instanceOf(Workspace).isRequired, + onItemClick: PropTypes.func.isRequired, + }; + + render() { + const { classes, workspace, onItemClick } = this.props; + + return ( + + onItemClick(workspace)}>{workspace.name} + + ); + } +} + +export default WorkspaceItem; diff --git a/src/features/workspaces/components/WorkspaceServiceListItem.js b/src/features/workspaces/components/WorkspaceServiceListItem.js deleted file mode 100644 index c06f3c61c..000000000 --- a/src/features/workspaces/components/WorkspaceServiceListItem.js +++ /dev/null @@ -1,75 +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 { Toggle } from '@meetfranz/forms'; - -import Service from '../../../models/Service'; -import ServiceIcon from '../../../components/ui/ServiceIcon'; - -const styles = (theme) => ({ - listItem: { - height: theme.workspaces.settings.listItems.height, - borderBottom: `1px solid ${theme.workspaces.settings.listItems.borderColor}`, - display: 'flex', - alignItems: 'center', - }, - serviceIcon: { - padding: theme.workspaces.settings.listItems.padding, - }, - toggle: { - height: 'auto', - margin: 0, - }, - label: { - padding: theme.workspaces.settings.listItems.padding, - flexGrow: 1, - }, - disabledLabel: { - color: theme.workspaces.settings.listItems.disabled.color, - }, -}); - -@injectSheet(styles) @observer -class WorkspaceServiceListItem extends Component { - static propTypes = { - classes: PropTypes.object.isRequired, - isInWorkspace: PropTypes.bool.isRequired, - onToggle: PropTypes.func.isRequired, - service: PropTypes.instanceOf(Service).isRequired, - }; - - render() { - const { - classes, - isInWorkspace, - onToggle, - service, - } = this.props; - - return ( -
- - - {service.name} - - -
- ); - } -} - -export default WorkspaceServiceListItem; diff --git a/src/features/workspaces/components/WorkspaceServiceListItem.tsx b/src/features/workspaces/components/WorkspaceServiceListItem.tsx new file mode 100644 index 000000000..b6faaf4ce --- /dev/null +++ b/src/features/workspaces/components/WorkspaceServiceListItem.tsx @@ -0,0 +1,66 @@ +import { Component } from 'react'; +import { observer } from 'mobx-react'; +import injectSheet from 'react-jss'; +import classnames from 'classnames'; +import { Toggle } from '@meetfranz/forms'; + +import ServiceIcon from '../../../components/ui/ServiceIcon'; + +const styles = theme => ({ + listItem: { + height: theme.workspaces.settings.listItems.height, + borderBottom: `1px solid ${theme.workspaces.settings.listItems.borderColor}`, + display: 'flex', + alignItems: 'center', + }, + serviceIcon: { + padding: theme.workspaces.settings.listItems.padding, + }, + toggle: { + height: 'auto', + margin: 0, + }, + label: { + padding: theme.workspaces.settings.listItems.padding, + flexGrow: 1, + }, + disabledLabel: { + color: theme.workspaces.settings.listItems.disabled.color, + }, +}); + +type Props = { + classes: any; + isInWorkspace: boolean; + onToggle: () => void; + service: any; +}; + +@injectSheet(styles) +@observer +class WorkspaceServiceListItem extends Component { + render() { + const { classes, isInWorkspace, onToggle, service } = this.props; + + return ( +
+ + + {service.name} + + +
+ ); + } +} + +export default WorkspaceServiceListItem; diff --git a/src/features/workspaces/containers/EditWorkspaceScreen.js b/src/features/workspaces/containers/EditWorkspaceScreen.js deleted file mode 100644 index bd9e235e3..000000000 --- a/src/features/workspaces/containers/EditWorkspaceScreen.js +++ /dev/null @@ -1,59 +0,0 @@ -import { Component } from 'react'; -import { inject, observer } from 'mobx-react'; -import PropTypes from 'prop-types'; - -import ErrorBoundary from '../../../components/util/ErrorBoundary'; -import EditWorkspaceForm from '../components/EditWorkspaceForm'; -import ServicesStore from '../../../stores/ServicesStore'; -import Workspace from '../models/Workspace'; -import { workspaceStore } from '../index'; -import { deleteWorkspaceRequest, updateWorkspaceRequest } from '../api'; -import WorkspacesStore from '../store'; - -@inject('stores', 'actions') @observer -class EditWorkspaceScreen extends Component { - static propTypes = { - actions: PropTypes.shape({ - workspaces: PropTypes.instanceOf(WorkspacesStore), - }).isRequired, - stores: PropTypes.shape({ - services: PropTypes.instanceOf(ServicesStore).isRequired, - }).isRequired, - }; - - onDelete = () => { - const { workspaceBeingEdited } = workspaceStore; - const { actions } = this.props; - if (!workspaceBeingEdited) return null; - actions.workspaces.delete({ workspace: workspaceBeingEdited }); - }; - - onSave = (values) => { - const { workspaceBeingEdited } = workspaceStore; - const { actions } = this.props; - const workspace = new Workspace( - ({ saving: true, ...workspaceBeingEdited, ...values }), - ); - actions.workspaces.update({ workspace }); - }; - - render() { - const { workspaceBeingEdited } = workspaceStore; - const { stores } = this.props; - if (!workspaceBeingEdited) return null; - return ( - - - - ); - } -} - -export default EditWorkspaceScreen; diff --git a/src/features/workspaces/containers/EditWorkspaceScreen.tsx b/src/features/workspaces/containers/EditWorkspaceScreen.tsx new file mode 100644 index 000000000..8e8f8179d --- /dev/null +++ b/src/features/workspaces/containers/EditWorkspaceScreen.tsx @@ -0,0 +1,61 @@ +import { Component } from 'react'; +import { inject, observer } from 'mobx-react'; + +import ErrorBoundary from '../../../components/util/ErrorBoundary'; +import EditWorkspaceForm from '../components/EditWorkspaceForm'; +import Workspace from '../models/Workspace'; +import { workspaceStore } from '../index'; +import { deleteWorkspaceRequest, updateWorkspaceRequest } from '../api'; +import { ServicesStore, WorkspacesStore } from '../../../stores.types'; + +type Props = { + actions: { + workspaces: WorkspacesStore; + }; + stores: { + services: ServicesStore; + }; +}; + +@inject('stores', 'actions') +@observer +class EditWorkspaceScreen extends Component { + // @ts-expect-error Not all code paths return a value. + onDelete = () => { + const { workspaceBeingEdited } = workspaceStore; + const { actions } = this.props; + if (!workspaceBeingEdited) return null; + actions.workspaces.delete({ workspace: workspaceBeingEdited }); + }; + + onSave = values => { + const { workspaceBeingEdited } = workspaceStore; + const { actions } = this.props; + const workspace = new Workspace({ + saving: true, + ...workspaceBeingEdited, + ...values, + }); + actions.workspaces.update({ workspace }); + }; + + render() { + const { workspaceBeingEdited } = workspaceStore; + const { stores } = this.props; + if (!workspaceBeingEdited) return null; + return ( + + + + ); + } +} + +export default EditWorkspaceScreen; diff --git a/src/features/workspaces/containers/WorkspacesScreen.js b/src/features/workspaces/containers/WorkspacesScreen.js deleted file mode 100644 index 2ba3784cb..000000000 --- a/src/features/workspaces/containers/WorkspacesScreen.js +++ /dev/null @@ -1,41 +0,0 @@ -import { Component } from 'react'; -import { inject, observer } from 'mobx-react'; -import PropTypes from 'prop-types'; -import WorkspacesDashboard from '../components/WorkspacesDashboard'; -import ErrorBoundary from '../../../components/util/ErrorBoundary'; -import { workspaceStore } from '../index'; -import { - createWorkspaceRequest, - deleteWorkspaceRequest, - getUserWorkspacesRequest, - updateWorkspaceRequest, -} from '../api'; -import WorkspacesStore from '../store'; - -@inject('stores', 'actions') @observer -class WorkspacesScreen extends Component { - static propTypes = { - actions: PropTypes.shape({ - workspaces: PropTypes.instanceOf(WorkspacesStore), - }).isRequired, - }; - - render() { - const { actions } = this.props; - return ( - - actions.workspaces.create(data)} - onWorkspaceClick={(w) => actions.workspaces.edit({ workspace: w })} - /> - - ); - } -} - -export default WorkspacesScreen; diff --git a/src/features/workspaces/containers/WorkspacesScreen.tsx b/src/features/workspaces/containers/WorkspacesScreen.tsx new file mode 100644 index 000000000..a07e92439 --- /dev/null +++ b/src/features/workspaces/containers/WorkspacesScreen.tsx @@ -0,0 +1,41 @@ +import { Component } from 'react'; +import { inject, observer } from 'mobx-react'; +import WorkspacesDashboard from '../components/WorkspacesDashboard'; +import ErrorBoundary from '../../../components/util/ErrorBoundary'; +import { workspaceStore } from '../index'; +import { + createWorkspaceRequest, + deleteWorkspaceRequest, + getUserWorkspacesRequest, + updateWorkspaceRequest, +} from '../api'; +import { WorkspacesStore } from '../../../stores.types'; + +type Props = { + actions: { + workspaces: WorkspacesStore; + }; +}; + +@inject('stores', 'actions') +@observer +class WorkspacesScreen extends Component { + render() { + const { actions } = this.props; + return ( + + actions.workspaces.create(data)} + onWorkspaceClick={w => actions.workspaces.edit({ workspace: w })} + /> + + ); + } +} + +export default WorkspacesScreen; diff --git a/src/index.ts b/src/index.ts index 248c6d110..b4a754289 100644 --- a/src/index.ts +++ b/src/index.ts @@ -499,7 +499,7 @@ app.on('login', (event, _webContents, _request, authInfo, callback) => { if (!authInfo.isProxy && authInfo.scheme === 'basic') { debug('basic auth handler', authInfo); - basicAuthHandler(mainWindow, authInfo); + basicAuthHandler(mainWindow!, authInfo); } }); diff --git a/src/stores.types.ts b/src/stores.types.ts index dd879179d..e5978a392 100644 --- a/src/stores.types.ts +++ b/src/stores.types.ts @@ -182,7 +182,7 @@ interface RouterStore { replace: () => void; } -interface ServicesStore { +export interface ServicesStore { actions: Actions; api: Api; clearCacheRequest: () => void; @@ -334,8 +334,13 @@ interface UserStore { team: () => void; } -interface WorkspacesStore { +export interface WorkspacesStore { activeWorkspace: () => void; + delete: ({ workspace }) => void; + update: ({ workspace }) => void; + create: ({ workspace }) => void; + edit: ({ workspace }) => void; + saving: boolean; filterServicesByActiveWorkspace: () => void; isFeatureActive: () => void; isFeatureEnabled: () => void; -- cgit v1.2.3-54-g00ecf