From eb7b2481f631cec5953265eef4ebc3f2fa7e496a Mon Sep 17 00:00:00 2001 From: muhamedsalih-tw <104364298+muhamedsalih-tw@users.noreply.github.com> Date: Wed, 16 Nov 2022 23:30:39 +0530 Subject: Transform JSX components to TSX (#755) * color picker types * Import * SetupAssistant * Services & appear * ServiceWebView * SettingsLayout * ImportantScreen * WorkspaceDrawer * SetupAssistant * chore: update vscode settings * chore: removed stale Import screen component & its tree --- .vscode/extensions.json | 35 ++- .vscode/settings.json | 51 +++- src/@types/legacy-types.ts | 5 + src/components/auth/Import.js | 168 ---------- src/components/auth/SetupAssistant.jsx | 336 -------------------- src/components/auth/SetupAssistant.tsx | 337 +++++++++++++++++++++ src/components/services/content/ServiceWebview.jsx | 132 -------- src/components/services/content/ServiceWebview.tsx | 140 +++++++++ src/components/services/content/Services.jsx | 165 ---------- src/components/services/content/Services.tsx | 162 ++++++++++ src/components/settings/SettingsLayout.jsx | 80 ----- src/components/settings/SettingsLayout.tsx | 77 +++++ .../settings/settings/EditSettingsForm.tsx | 25 +- src/components/ui/ColorPickerInput.tsx | 102 ------- src/components/ui/colorPickerInput/index.tsx | 88 ++++++ src/components/ui/effects/Appear.tsx | 21 +- src/containers/auth/ImportScreen.tsx | 25 -- src/containers/auth/SetupAssistantScreen.tsx | 133 ++++---- src/features/workspaces/actions.ts | 1 + .../workspaces/components/WorkspaceDrawer.jsx | 184 ----------- .../workspaces/components/WorkspaceDrawer.tsx | 186 ++++++++++++ src/i18n/locales/en-US.json | 4 - src/models/Recipe.ts | 1 + src/routes.tsx | 5 - 24 files changed, 1175 insertions(+), 1288 deletions(-) create mode 100644 src/@types/legacy-types.ts delete mode 100644 src/components/auth/Import.js delete mode 100644 src/components/auth/SetupAssistant.jsx create mode 100644 src/components/auth/SetupAssistant.tsx delete mode 100644 src/components/services/content/ServiceWebview.jsx create mode 100644 src/components/services/content/ServiceWebview.tsx delete mode 100644 src/components/services/content/Services.jsx create mode 100644 src/components/services/content/Services.tsx delete mode 100644 src/components/settings/SettingsLayout.jsx create mode 100644 src/components/settings/SettingsLayout.tsx delete mode 100644 src/components/ui/ColorPickerInput.tsx create mode 100644 src/components/ui/colorPickerInput/index.tsx delete mode 100644 src/containers/auth/ImportScreen.tsx delete mode 100644 src/features/workspaces/components/WorkspaceDrawer.jsx create mode 100644 src/features/workspaces/components/WorkspaceDrawer.tsx diff --git a/.vscode/extensions.json b/.vscode/extensions.json index 98529ee1f..9f26c98d1 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -1,8 +1,31 @@ { - "recommendations": [ - "esbenp.prettier-vscode", - "dbaeumer.vscode-eslint", - "codezombiech.gitignore", - "EditorConfig.EditorConfig" - ] + "recommendations": [ + "esbenp.prettier-vscode", + "dbaeumer.vscode-eslint", + "codezombiech.gitignore", + "editorconfig.editorconfig", + "steoates.autoimport", + "formulahendry.auto-rename-tag", + "streetsidesoftware.code-spell-checker", + "naumovs.color-highlight", + "mkhl.direnv", + "ms-azuretools.vscode-docker", + "usernamehw.errorlens", + "dsznajder.es7-react-js-snippets", + "mhutchie.git-graph", + "vincaslt.highlight-matching-tag", + "eamodio.gitlens", + "jbockle.jbockle-format-files", + "wix.vscode-import-cost", + "visualstudioexptteam.intellicode-api-usage-examples", + "visualstudioexptteam.vscodeintellicode", + "orta.vscode-jest", + "pkief.material-icon-theme", + "techer.open-in-browser", + "christian-kohler.path-intellisense", + "ofhumanbondage.react-proptypes-intellisense", + "jingkaizhao.vscode-redux-devtools", + "planbcoding.vscode-react-refactor", + "redhat.vscode-yaml" + ] } diff --git a/.vscode/settings.json b/.vscode/settings.json index 21bfab5db..815aa0cfb 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -3,5 +3,54 @@ "js/ts.implicitProjectConfig.experimentalDecorators": true, "yaml.schemas": { "https://json.schemastore.org/github-issue-config.json": ".github/ISSUE_TEMPLATE/config.yml" - } + }, + + // "editor.fontFamily": "Fira Code", + // "editor.fontLigatures": true, + "editor.detectIndentation": false, + "editor.bracketPairColorization.enabled": true, + "editor.bracketPairColorization.independentColorPoolPerBracketType": true, + "editor.guides.bracketPairs": "active", + "editor.codeActionsOnSave": { + "source.fixAll": true, + "source.fixAll.eslint": true + }, + + // "explorer.confirmDelete": false, + // "explorer.confirmDragAndDrop": false, + + "eslint.enable": true, + "eslint.runtime": "node", + "eslint.format.enable": true, + "eslint.alwaysShowStatus": true, + "eslint.workingDirectories": [ { "mode": "auto" } ], + // "eslint.packageManager": "npm", + "eslint.validate": ["javascript","javascriptreact","typescript","typescriptreact"], + "editor.defaultFormatter": "dbaeumer.vscode-eslint", + "[javascript]": { "editor.defaultFormatter": "dbaeumer.vscode-eslint" }, + "[javascriptreact]": { "editor.defaultFormatter": "dbaeumer.vscode-eslint" }, + "[typescript]": { "editor.defaultFormatter": "dbaeumer.vscode-eslint" }, + "[typescriptreact]": { "editor.defaultFormatter": "dbaeumer.vscode-eslint" }, + "[json]": { "editor.defaultFormatter": "esbenp.prettier-vscode" }, + "[jsonc]": { "editor.defaultFormatter": "esbenp.prettier-vscode" }, + + + // "workbench.colorTheme": "Visual Studio Dark", + // "workbench.iconTheme": "material-icon-theme", + + "terminal.integrated.sendKeybindingsToShell": true, + // "terminal.integrated.copyOnSelection": true, + "terminal.integrated.defaultProfile.osx": "zsh", + "terminal.integrated.cursorBlinking": true, + "terminal.integrated.cursorStyle": "block", + "terminal.integrated.shellIntegration.enabled":true, + + "git.mergeEditor": false, + "git.enableSmartCommit": true, + "diffEditor.ignoreTrimWhitespace": false, + + // "formatFiles.runOrganizeImports": true, + + "javascript.preferences.importModuleSpecifier": "relative", + "typescript.preferences.importModuleSpecifier": "relative", } diff --git a/src/@types/legacy-types.ts b/src/@types/legacy-types.ts new file mode 100644 index 000000000..c17fdfe82 --- /dev/null +++ b/src/@types/legacy-types.ts @@ -0,0 +1,5 @@ +export interface ILegacyServices { + [key: string]: { + [key: string]: string | boolean; + }; +} diff --git a/src/components/auth/Import.js b/src/components/auth/Import.js deleted file mode 100644 index b897116e2..000000000 --- a/src/components/auth/Import.js +++ /dev/null @@ -1,168 +0,0 @@ -import { Component } from 'react'; -import PropTypes from 'prop-types'; -import { observer, PropTypes as MobxPropTypes } from 'mobx-react'; -import { defineMessages, injectIntl } from 'react-intl'; -import { Link } from 'react-router-dom'; -import classnames from 'classnames'; -import Form from '../../lib/Form'; -import Toggle from '../ui/toggle'; -import Button from '../ui/button'; -import { H1 } from '../ui/headline'; - -const messages = defineMessages({ - headline: { - id: 'import.headline', - defaultMessage: 'Import your Ferdium 4 services', - }, - notSupportedHeadline: { - id: 'import.notSupportedHeadline', - defaultMessage: 'Services not yet supported in Ferdium 5', - }, - submitButtonLabel: { - id: 'import.submit.label', - defaultMessage: 'Import {count} services', - }, - skipButtonLabel: { - id: 'import.skip.label', - defaultMessage: 'I want to add services manually', - }, -}); - -class Import extends Component { - static propTypes = { - services: MobxPropTypes.arrayOrObservableArray.isRequired, - onSubmit: PropTypes.func.isRequired, - isSubmitting: PropTypes.bool.isRequired, - inviteRoute: PropTypes.string.isRequired, - }; - - componentDidMount() { - const config = { - fields: { - import: [ - ...this.props.services - .filter(s => s.recipe) - .map(s => ({ - fields: { - add: { - default: true, - options: s, - }, - }, - })), - ], - }, - }; - - this.form = new Form(config, this.props.intl); - } - - submit(e) { - const { services } = this.props; - e.preventDefault(); - this.form.submit({ - onSuccess: form => { - const servicesImport = form - .values() - .import.map( - (value, i) => !value.add || services.filter(s => s.recipe)[i], - ) - .filter(s => typeof s !== 'boolean'); - - this.props.onSubmit({ services: servicesImport }); - }, - onError: () => {}, - }); - } - - render() { - const { intl } = this.props; - const { services, isSubmitting, inviteRoute } = this.props; - - const availableServices = services.filter(s => s.recipe); - const unavailableServices = services.filter(s => !s.recipe); - - return ( -
-
-
this.submit(e)} - > - -

{intl.formatMessage(messages.headline)}

- - - {this.form.$('import').map((service, i) => ( - - - - - - ))} - -
- - - - - {availableServices[i].name !== '' - ? availableServices[i].name - : availableServices[i].recipe.name} -
- {unavailableServices.length > 0 && ( -
- - {intl.formatMessage(messages.notSupportedHeadline)} - -

- {services - .filter(s => !s.recipe) - .map((service, i) => ( - - {service.name !== '' ? service.name : service.service} - {unavailableServices.length > i + 1 ? ', ' : ''} - - ))} -

-
- )} - - {isSubmitting ? ( -
-
- ); - } -} - -export default injectIntl(observer(Import)); diff --git a/src/components/auth/SetupAssistant.jsx b/src/components/auth/SetupAssistant.jsx deleted file mode 100644 index 8d15e36d1..000000000 --- a/src/components/auth/SetupAssistant.jsx +++ /dev/null @@ -1,336 +0,0 @@ -import { Component } from 'react'; -import PropTypes from 'prop-types'; -import { observer } from 'mobx-react'; -import { defineMessages, injectIntl } from 'react-intl'; -import injectSheet from 'react-jss'; -import classnames from 'classnames'; - -import Input from '../ui/input/index'; -import Button from '../ui/button'; -import Badge from '../ui/badge'; -import Modal from '../ui/Modal'; -import Infobox from '../ui/Infobox'; -import Appear from '../ui/effects/Appear'; -import globalMessages from '../../i18n/globalMessages'; - -import { CDN_URL } from '../../config'; -import { H1, H2 } from '../ui/headline'; - -const SLACK_ID = 'slack'; - -const messages = defineMessages({ - headline: { - id: 'setupAssistant.headline', - defaultMessage: "Let's get started", - }, - subHeadline: { - id: 'setupAssistant.subheadline', - defaultMessage: - 'Choose from our most used services and get back on top of your messaging now.', - }, - submitButtonLabel: { - id: 'setupAssistant.submit.label', - defaultMessage: "Let's go", - }, - skipButtonLabel: { - id: 'setupAssistant.skip.label', - defaultMessage: 'Skip', - }, - inviteSuccessInfo: { - id: 'invite.successInfo', - defaultMessage: 'Invitations sent successfully', - }, -}); - -let transition = 'none'; - -if (window && window.matchMedia('(prefers-reduced-motion: no-preference)')) { - transition = 'all 0.25s'; -} - -const styles = theme => ({ - root: { - width: '500px !important', - textAlign: 'center', - padding: 20, - - '& h1': {}, - }, - servicesGrid: { - display: 'flex', - flexWrap: 'wrap', - justifyContent: 'space-between', - }, - serviceContainer: { - background: theme.colorBackground, - position: 'relative', - width: '32%', - display: 'flex', - alignItems: 'center', - flexDirection: 'column', - justifyContent: 'center', - padding: 20, - borderRadius: theme.borderRadius, - marginBottom: 10, - opacity: 0.5, - transition, - border: [3, 'solid', 'transparent'], - - '& h2': { - margin: [10, 0, 0], - color: theme.colorText, - }, - - '&:hover': { - border: [3, 'solid', theme.brandPrimary], - '& $serviceIcon': {}, - }, - }, - selected: { - border: [3, 'solid', theme.brandPrimary], - background: `${theme.brandPrimary}47`, - opacity: 1, - }, - serviceIcon: { - width: 50, - transition, - }, - - slackModalContent: { - textAlign: 'center', - - '& img': { - width: 50, - marginBottom: 20, - }, - }, - modalActionContainer: { - display: 'flex', - flexDirection: 'column', - justifyContent: 'center', - alignItems: 'center', - }, - ctaCancel: { - background: 'none !important', - }, - slackBadge: { - position: 'absolute', - bottom: 4, - height: 'auto', - padding: '0px 4px', - borderRadius: theme.borderRadiusSmall, - margin: 0, - display: 'flex', - overflow: 'hidden', - }, - clearSlackWorkspace: { - background: theme.inputPrefixColor, - marginLeft: 5, - height: '100%', - color: theme.colorText, - display: 'inline-flex', - justifyContent: 'center', - alignItems: 'center', - marginRight: -4, - padding: [0, 5], - }, -}); - -class SetupAssistant extends Component { - static propTypes = { - classes: PropTypes.object.isRequired, - onSubmit: PropTypes.func.isRequired, - isInviteSuccessful: PropTypes.bool, - services: PropTypes.object.isRequired, - isSettingUpServices: PropTypes.bool.isRequired, - }; - - static defaultProps = { - isInviteSuccessful: false, - }; - - constructor() { - super(); - - this.state = { - services: [], - isSlackModalOpen: false, - slackWorkspace: '', - }; - } - - slackWorkspaceHandler() { - const { slackWorkspace = '', services } = this.state; - - const sanitizedWorkspace = slackWorkspace - .trim() - .replace(/^https?:\/\//, ''); - - if (sanitizedWorkspace) { - const index = services.findIndex(s => s.id === SLACK_ID); - - if (index === -1) { - const newServices = services; - newServices.push({ id: SLACK_ID, team: sanitizedWorkspace }); - this.setState({ services: newServices }); - } - } - - this.setState({ - isSlackModalOpen: false, - slackWorkspace: sanitizedWorkspace, - }); - } - - render() { - const { intl } = this.props; - const { - classes, - isInviteSuccessful, - onSubmit, - services, - isSettingUpServices, - } = this.props; - const { - isSlackModalOpen, - slackWorkspace, - services: addedServices, - } = this.state; - - return ( -
- {this.state.showSuccessInfo && isInviteSuccessful && ( - - - {intl.formatMessage(messages.inviteSuccessInfo)} - - - )} - - -

{intl.formatMessage(messages.headline)}

-

{intl.formatMessage(messages.subHeadline)}

-
- {Object.keys(services).map(id => { - const service = services[id]; - return ( - - - )} - - ); - })} -
- this.setState({ isSlackModalOpen: false })} - > -
- -

Create your first Slack workspace

-
{ - e.preventDefault(); - this.slackWorkspaceHandler(); - }} - > - - this.setState({ slackWorkspace: e.target.value }) - } - value={slackWorkspace} - /> -
-
-
-
-
-
- ); - } -} - -export default injectIntl( - injectSheet(styles, { injectTheme: true })(observer(SetupAssistant)), -); diff --git a/src/components/auth/SetupAssistant.tsx b/src/components/auth/SetupAssistant.tsx new file mode 100644 index 000000000..c5fb79919 --- /dev/null +++ b/src/components/auth/SetupAssistant.tsx @@ -0,0 +1,337 @@ +import { Component } from 'react'; +import { observer } from 'mobx-react'; +import { defineMessages, injectIntl, WrappedComponentProps } from 'react-intl'; +import withStyles, { WithStylesProps } from 'react-jss'; +import classnames from 'classnames'; +import Input from '../ui/input/index'; +import Button from '../ui/button'; +import Badge from '../ui/badge'; +import Modal from '../ui/Modal'; +import Infobox from '../ui/Infobox'; +import Appear from '../ui/effects/Appear'; +import globalMessages from '../../i18n/globalMessages'; +import { CDN_URL } from '../../config'; +import { H1, H2 } from '../ui/headline'; + +const SLACK_ID = 'slack'; + +const messages = defineMessages({ + headline: { + id: 'setupAssistant.headline', + defaultMessage: "Let's get started", + }, + subHeadline: { + id: 'setupAssistant.subheadline', + defaultMessage: + 'Choose from our most used services and get back on top of your messaging now.', + }, + submitButtonLabel: { + id: 'setupAssistant.submit.label', + defaultMessage: "Let's go", + }, + skipButtonLabel: { + id: 'setupAssistant.skip.label', + defaultMessage: 'Skip', + }, + inviteSuccessInfo: { + id: 'invite.successInfo', + defaultMessage: 'Invitations sent successfully', + }, +}); + +const transition = + window && window.matchMedia('(prefers-reduced-motion: no-preference)') + ? 'all 0.25s' + : 'none'; + +const styles = theme => ({ + root: { + width: '500px !important', + textAlign: 'center', + padding: 20, + + '& h1': {}, + }, + servicesGrid: { + display: 'flex', + flexWrap: 'wrap', + justifyContent: 'space-between', + }, + serviceContainer: { + background: theme.colorBackground, + position: 'relative', + width: '32%', + display: 'flex', + alignItems: 'center', + flexDirection: 'column', + justifyContent: 'center', + padding: 20, + borderRadius: theme.borderRadius, + marginBottom: 10, + opacity: 0.5, + transition, + border: [3, 'solid', 'transparent'], + + '& h2': { + margin: [10, 0, 0], + color: theme.colorText, + }, + + '&:hover': { + border: [3, 'solid', theme.brandPrimary], + '& $serviceIcon': {}, + }, + }, + selected: { + border: [3, 'solid', theme.brandPrimary], + background: `${theme.brandPrimary}47`, + opacity: 1, + }, + serviceIcon: { + width: 50, + transition, + }, + + slackModalContent: { + textAlign: 'center', + + '& img': { + width: 50, + marginBottom: 20, + }, + }, + modalActionContainer: { + display: 'flex', + flexDirection: 'column', + justifyContent: 'center', + alignItems: 'center', + }, + ctaCancel: { + background: 'none !important', + }, + slackBadge: { + position: 'absolute', + bottom: 4, + height: 'auto', + padding: '0px 4px', + borderRadius: theme.borderRadiusSmall, + margin: 0, + display: 'flex', + overflow: 'hidden', + }, + clearSlackWorkspace: { + background: theme.inputPrefixColor, + marginLeft: 5, + height: '100%', + color: theme.colorText, + display: 'inline-flex', + justifyContent: 'center', + alignItems: 'center', + marginRight: -4, + padding: [0, 5], + }, +}); + +interface IProps extends WithStylesProps, WrappedComponentProps { + onSubmit: (...args: any[]) => void; + isInviteSuccessful?: boolean; + services: any; // TODO - [TS DEBT] check legacy services type + isSettingUpServices: boolean; +} + +interface IState { + services: any[]; + isSlackModalOpen: boolean; + slackWorkspace: string; + showSuccessInfo: boolean; +} + +@observer +class SetupAssistant extends Component { + constructor(props: IProps) { + super(props); + + this.state = { + services: [], + isSlackModalOpen: false, + showSuccessInfo: false, + slackWorkspace: '', + }; + } + + slackWorkspaceHandler(): void { + const { slackWorkspace = '', services } = this.state; + const sanitizedWorkspace = slackWorkspace + .trim() + .replace(/^https?:\/\//, ''); + + if (sanitizedWorkspace) { + const index = services.findIndex(s => s.id === SLACK_ID); + if (index === -1) { + const newServices = services; + newServices.push({ id: SLACK_ID, team: sanitizedWorkspace }); + this.setState({ services: newServices }); + } + } + + this.setState({ + isSlackModalOpen: false, + slackWorkspace: sanitizedWorkspace, + }); + } + + render() { + const { + classes, + isInviteSuccessful = false, + onSubmit, + services, + isSettingUpServices, + intl, + } = this.props; + const { + isSlackModalOpen, + slackWorkspace, + services: addedServices, + } = this.state; + + return ( +
+ {this.state.showSuccessInfo && isInviteSuccessful && ( + + + {intl.formatMessage(messages.inviteSuccessInfo)} + + + )} + + +

{intl.formatMessage(messages.headline)}

+

{intl.formatMessage(messages.subHeadline)}

+
+ {Object.keys(services).map(id => { + const service = services[id]; + return ( + + + )} + + ); + })} +
+ this.setState({ isSlackModalOpen: false })} + > +
+ +

Create your first Slack workspace

+
{ + e.preventDefault(); + this.slackWorkspaceHandler(); + }} + > + + this.setState({ slackWorkspace: e.target.value }) + } + value={slackWorkspace} + /> +
+
+
+
+
+
+ ); + } +} + +export default injectIntl( + withStyles(styles, { injectTheme: true })(SetupAssistant), +); diff --git a/src/components/services/content/ServiceWebview.jsx b/src/components/services/content/ServiceWebview.jsx deleted file mode 100644 index 835c5125e..000000000 --- a/src/components/services/content/ServiceWebview.jsx +++ /dev/null @@ -1,132 +0,0 @@ -import { Component } from 'react'; -import PropTypes from 'prop-types'; -import { observer } from 'mobx-react'; -import { action, makeObservable, observable, reaction } from 'mobx'; -import ElectronWebView from 'react-electron-web-view'; -import { join } from 'path'; - -import ServiceModel from '../../../models/Service'; - -const debug = require('../../../preload-safe-debug')('Ferdium:Services'); - -class ServiceWebview extends Component { - static propTypes = { - service: PropTypes.instanceOf(ServiceModel).isRequired, - setWebviewReference: PropTypes.func.isRequired, - detachService: PropTypes.func.isRequired, - isSpellcheckerEnabled: PropTypes.bool.isRequired, - }; - - @observable webview = null; - - constructor(props) { - super(props); - - makeObservable(this); - - reaction( - () => this.webview, - () => { - if (this.webview && this.webview.view) { - this.webview.view.addEventListener('console-message', e => { - debug('Service logged a message:', e.message); - }); - this.webview.view.addEventListener('did-navigate', () => { - if (this.props.service._webview) { - document.title = `Ferdium - ${this.props.service.name} ${ - this.props.service.dialogTitle - ? ` - ${this.props.service.dialogTitle}` - : '' - } ${`- ${this.props.service._webview.getTitle()}`}`; - } - }); - } - }, - ); - } - - componentWillUnmount() { - const { service, detachService } = this.props; - detachService({ service }); - } - - refocusWebview = () => { - const { webview } = this; - debug('Refocus Webview is called', this.props.service); - if (!webview) return; - if (this.props.service.isActive) { - webview.view.blur(); - webview.view.focus(); - window.setTimeout(() => { - document.title = `Ferdium - ${this.props.service.name} ${ - this.props.service.dialogTitle - ? ` - ${this.props.service.dialogTitle}` - : '' - } ${`- ${this.props.service._webview.getTitle()}`}`; - }, 100); - } else { - debug('Refocus not required - Not active service'); - } - }; - - @action _setWebview(webview) { - this.webview = webview; - } - - render() { - const { service, setWebviewReference, isSpellcheckerEnabled } = this.props; - - const preloadScript = join( - __dirname, - '..', - '..', - '..', - 'webview', - 'recipe.js', - ); - - return ( - { - this._setWebview(webview); - if (webview && webview.view) { - webview.view.addEventListener( - 'did-stop-loading', - this.refocusWebview, - ); - } - }} - autosize - src={service.url} - preload={preloadScript} - partition={service.partition} - onDidAttach={() => { - // Force the event handler to run in a new task. - // This resolves a race condition when the `did-attach` is called, - // but the webview is not attached to the DOM yet: - // https://github.com/electron/electron/issues/31918 - // This prevents us from immediately attaching listeners such as `did-stop-load`: - // https://github.com/ferdium/ferdium-app/issues/157 - setTimeout(() => { - setWebviewReference({ - serviceId: service.id, - webview: this.webview.view, - }); - }, 0); - }} - onUpdateTargetUrl={this.updateTargetUrl} - useragent={service.userAgent} - disablewebsecurity={ - service.recipe.disablewebsecurity ? true : undefined - } - allowpopups - nodeintegration - webpreferences={`spellcheck=${ - isSpellcheckerEnabled ? 1 : 0 - }, contextIsolation=1`} - /> - ); - } -} - -export default observer(ServiceWebview); diff --git a/src/components/services/content/ServiceWebview.tsx b/src/components/services/content/ServiceWebview.tsx new file mode 100644 index 000000000..ac8d1ea66 --- /dev/null +++ b/src/components/services/content/ServiceWebview.tsx @@ -0,0 +1,140 @@ +import { Component, ReactElement } from 'react'; +import { observer } from 'mobx-react'; +import { action, makeObservable, observable, reaction } from 'mobx'; +import ElectronWebView from 'react-electron-web-view'; +import { join } from 'path'; +import ServiceModel from '../../../models/Service'; + +const debug = require('../../../preload-safe-debug')('Ferdium:Services'); + +interface IProps { + service: ServiceModel; + setWebviewReference: (options: { + serviceId: string; + webview: ElectronWebView | null; + }) => void; + detachService: (options: { service: ServiceModel }) => void; + isSpellcheckerEnabled: boolean; +} + +@observer +class ServiceWebview extends Component { + @observable webview: ElectronWebView | null = null; + + constructor(props: IProps) { + super(props); + + this.refocusWebview = this.refocusWebview.bind(this); + this._setWebview = this._setWebview.bind(this); + + makeObservable(this); + + reaction( + () => this.webview, + () => { + if (this.webview && this.webview.view) { + this.webview.view.addEventListener('console-message', e => { + debug('Service logged a message:', e.message); + }); + this.webview.view.addEventListener('did-navigate', () => { + if (this.props.service._webview) { + document.title = `Ferdium - ${this.props.service.name} ${ + this.props.service.dialogTitle + ? ` - ${this.props.service.dialogTitle}` + : '' + } ${`- ${this.props.service._webview.getTitle()}`}`; + } + }); + } + }, + ); + } + + componentWillUnmount(): void { + const { service, detachService } = this.props; + detachService({ service }); + } + + refocusWebview(): void { + const { webview } = this; + debug('Refocus Webview is called', this.props.service); + if (!webview) { + return; + } + + if (this.props.service.isActive) { + webview.view.blur(); + webview.view.focus(); + window.setTimeout(() => { + document.title = `Ferdium - ${this.props.service.name} ${ + this.props.service.dialogTitle + ? ` - ${this.props.service.dialogTitle}` + : '' + } ${`- ${this.props.service._webview.getTitle()}`}`; + }, 100); + } else { + debug('Refocus not required - Not active service'); + } + } + + @action _setWebview(webview): void { + this.webview = webview; + } + + render(): ReactElement { + const { service, setWebviewReference, isSpellcheckerEnabled } = this.props; + + const preloadScript = join( + __dirname, + '..', + '..', + '..', + 'webview', + 'recipe.js', + ); + + return ( + { + this._setWebview(webview); + if (webview && webview.view) { + webview.view.addEventListener( + 'did-stop-loading', + this.refocusWebview, + ); + } + }} + autosize + src={service.url} + preload={preloadScript} + partition={service.partition} + onDidAttach={() => { + // Force the event handler to run in a new task. + // This resolves a race condition when the `did-attach` is called, + // but the webview is not attached to the DOM yet: + // https://github.com/electron/electron/issues/31918 + // This prevents us from immediately attaching listeners such as `did-stop-load`: + // https://github.com/ferdium/ferdium-app/issues/157 + setTimeout(() => { + setWebviewReference({ + serviceId: service.id, + webview: this.webview.view, + }); + }, 0); + }} + // onUpdateTargetUrl={this.updateTargetUrl} // TODO - [TS DEBT] need to check where its from + useragent={service.userAgent} + disablewebsecurity={ + service.recipe.disablewebsecurity ? true : undefined + } + allowpopups + nodeintegration + webpreferences={`spellcheck=${ + isSpellcheckerEnabled ? 1 : 0 + }, contextIsolation=1`} + /> + ); + } +} + +export default ServiceWebview; diff --git a/src/components/services/content/Services.jsx b/src/components/services/content/Services.jsx deleted file mode 100644 index da700b5b1..000000000 --- a/src/components/services/content/Services.jsx +++ /dev/null @@ -1,165 +0,0 @@ -import { Component } from 'react'; -import PropTypes from 'prop-types'; -import { observer, PropTypes as MobxPropTypes, inject } from 'mobx-react'; -import { Link } from 'react-router-dom'; -import { defineMessages, injectIntl } from 'react-intl'; -import Confetti from 'react-confetti'; -import ms from 'ms'; -import injectSheet from 'react-jss'; - -import ServiceView from './ServiceView'; -import Appear from '../../ui/effects/Appear'; - -const messages = defineMessages({ - getStarted: { - id: 'services.getStarted', - defaultMessage: 'Get started', - }, - login: { - id: 'services.login', - defaultMessage: 'Please login to use Ferdium.', - }, - serverless: { - id: 'services.serverless', - defaultMessage: 'Use Ferdium without an Account', - }, - serverInfo: { - id: 'services.serverInfo', - defaultMessage: - 'Optionally, you can change your Ferdium server by clicking the cog in the bottom left corner. If you are switching over (from one of the hosted servers) to using Ferdium without an account, please be informed that you can export your data from that server and subsequently import it using the Help menu to resurrect all your workspaces and configured services!', - }, -}); - -const styles = { - confettiContainer: { - position: 'absolute', - width: '100%', - zIndex: 9999, - pointerEvents: 'none', - }, -}; - -class Services extends Component { - static propTypes = { - services: MobxPropTypes.arrayOrObservableArray, - setWebviewReference: PropTypes.func.isRequired, - detachService: PropTypes.func.isRequired, - handleIPCMessage: PropTypes.func.isRequired, - openWindow: PropTypes.func.isRequired, - reload: PropTypes.func.isRequired, - openSettings: PropTypes.func.isRequired, - update: PropTypes.func.isRequired, - userHasCompletedSignup: PropTypes.bool.isRequired, - // eslint-disable-next-line react/forbid-prop-types - classes: PropTypes.object.isRequired, - isSpellcheckerEnabled: PropTypes.bool.isRequired, - }; - - static defaultProps = { - services: [], - }; - - _confettiTimeout = null; - - constructor() { - super(); - - this.state = { - showConfetti: true, - }; - } - - componentDidMount() { - this._confettiTimeout = window.setTimeout(() => { - this.setState({ - showConfetti: false, - }); - }, ms('8s')); - } - - componentWillUnmount() { - if (this._confettiTimeout) { - clearTimeout(this._confettiTimeout); - } - } - - render() { - const { - services, - handleIPCMessage, - setWebviewReference, - detachService, - openWindow, - reload, - openSettings, - update, - userHasCompletedSignup, - classes, - isSpellcheckerEnabled, - } = this.props; - - const { showConfetti } = this.state; - - const { intl } = this.props; - - return ( -
- {userHasCompletedSignup && ( -
- -
- )} - {services.length === 0 && ( - -
- Logo - - - {intl.formatMessage(messages.getStarted)} - - -
-
- )} - {services - .filter(service => !service.isTodosService) - .map(service => ( - reload({ serviceId: service.id })} - edit={() => openSettings({ path: `services/edit/${service.id}` })} - enable={() => - update({ - serviceId: service.id, - serviceData: { - isEnabled: true, - }, - redirect: false, - }) - } - isSpellcheckerEnabled={isSpellcheckerEnabled} - /> - ))} -
- ); - } -} - -export default injectIntl( - injectSheet(styles, { injectTheme: true })( - inject('actions')(observer(Services)), - ), -); diff --git a/src/components/services/content/Services.tsx b/src/components/services/content/Services.tsx new file mode 100644 index 000000000..53cddd907 --- /dev/null +++ b/src/components/services/content/Services.tsx @@ -0,0 +1,162 @@ +import { Component, ReactElement } from 'react'; +import { observer } from 'mobx-react'; +import { Link } from 'react-router-dom'; +import { defineMessages, injectIntl, WrappedComponentProps } from 'react-intl'; +import Confetti from 'react-confetti'; +import ms from 'ms'; +import withStyles, { WithStylesProps } from 'react-jss'; +import ServiceView from './ServiceView'; +import Appear from '../../ui/effects/Appear'; +import Service from '../../../models/Service'; + +const messages = defineMessages({ + getStarted: { + id: 'services.getStarted', + defaultMessage: 'Get started', + }, + login: { + id: 'services.login', + defaultMessage: 'Please login to use Ferdium.', + }, + serverless: { + id: 'services.serverless', + defaultMessage: 'Use Ferdium without an Account', + }, + serverInfo: { + id: 'services.serverInfo', + defaultMessage: + 'Optionally, you can change your Ferdium server by clicking the cog in the bottom left corner. If you are switching over (from one of the hosted servers) to using Ferdium without an account, please be informed that you can export your data from that server and subsequently import it using the Help menu to resurrect all your workspaces and configured services!', + }, +}); + +const styles = { + confettiContainer: { + position: 'absolute', + width: '100%', + zIndex: 9999, + pointerEvents: 'none', + }, +}; + +interface IProps extends WrappedComponentProps, WithStylesProps { + services?: Service[]; + setWebviewReference: () => void; + detachService: () => void; + handleIPCMessage: () => void; + openWindow: () => void; + reload: (options: { serviceId: string }) => void; + openSettings: (options: { path: string }) => void; + update: (options: { + serviceId: string; + serviceData: { isEnabled: boolean }; + redirect: boolean; + }) => void; + userHasCompletedSignup: boolean; + isSpellcheckerEnabled: boolean; +} + +interface IState { + showConfetti: boolean; +} + +@observer +class Services extends Component { + _confettiTimeout: number | null = null; + + constructor(props: IProps) { + super(props); + + this.state = { + showConfetti: true, + }; + } + + componentDidMount(): void { + this._confettiTimeout = window.setTimeout(() => { + this.setState({ + showConfetti: false, + }); + }, ms('8s')); + } + + componentWillUnmount(): void { + if (this._confettiTimeout) { + clearTimeout(this._confettiTimeout); + } + } + + render(): ReactElement { + const { + services = [], + handleIPCMessage, + setWebviewReference, + detachService, + openWindow, + reload, + openSettings, + update, + userHasCompletedSignup, + classes, + isSpellcheckerEnabled, + intl, + } = this.props; + + const { showConfetti } = this.state; + + return ( +
+ {userHasCompletedSignup && ( +
+ +
+ )} + {services.length === 0 && ( + +
+ Logo + + + {intl.formatMessage(messages.getStarted)} + + +
+
+ )} + {services + .filter(service => !service.isTodosService) + .map(service => ( + reload({ serviceId: service.id })} + edit={() => openSettings({ path: `services/edit/${service.id}` })} + enable={() => + update({ + serviceId: service.id, + serviceData: { + isEnabled: true, + }, + redirect: false, + }) + } + isSpellcheckerEnabled={isSpellcheckerEnabled} + /> + ))} +
+ ); + } +} + +export default injectIntl(withStyles(styles, { injectTheme: true })(Services)); diff --git a/src/components/settings/SettingsLayout.jsx b/src/components/settings/SettingsLayout.jsx deleted file mode 100644 index 989c428f2..000000000 --- a/src/components/settings/SettingsLayout.jsx +++ /dev/null @@ -1,80 +0,0 @@ -import { Component } from 'react'; -import PropTypes from 'prop-types'; -import { observer } from 'mobx-react'; -import { defineMessages, injectIntl } from 'react-intl'; - -import { mdiClose } from '@mdi/js'; -import { Outlet } from 'react-router-dom'; -import ErrorBoundary from '../util/ErrorBoundary'; -import Appear from '../ui/effects/Appear'; -import Icon from '../ui/icon'; -import { isEscKeyPress } from '../../jsUtils'; - -const messages = defineMessages({ - closeSettings: { - id: 'settings.app.closeSettings', - defaultMessage: 'Close settings', - }, -}); - -class SettingsLayout extends Component { - static propTypes = { - navigation: PropTypes.element.isRequired, - closeSettings: PropTypes.func.isRequired, - }; - - componentDidMount() { - document.addEventListener('keydown', this.handleKeyDown.bind(this), false); - } - - componentWillUnmount() { - document.removeEventListener( - 'keydown', - // eslint-disable-next-line unicorn/no-invalid-remove-event-listener - this.handleKeyDown.bind(this), - false, - ); - } - - handleKeyDown(e) { - if (isEscKeyPress(e.keyCode)) { - this.props.closeSettings(); - } - } - - render() { - const { navigation, closeSettings } = this.props; - - const { intl } = this.props; - - return ( - -
- - -
- - -
- ); - } -} - -export default injectIntl(observer(SettingsLayout)); diff --git a/src/components/settings/SettingsLayout.tsx b/src/components/settings/SettingsLayout.tsx new file mode 100644 index 000000000..3b706571e --- /dev/null +++ b/src/components/settings/SettingsLayout.tsx @@ -0,0 +1,77 @@ +import { Component, ReactElement } from 'react'; +import { observer } from 'mobx-react'; +import { defineMessages, injectIntl, WrappedComponentProps } from 'react-intl'; +import { mdiClose } from '@mdi/js'; +import { Outlet } from 'react-router-dom'; +import ErrorBoundary from '../util/ErrorBoundary'; +import Appear from '../ui/effects/Appear'; +import Icon from '../ui/icon'; +import { isEscKeyPress } from '../../jsUtils'; + +const messages = defineMessages({ + closeSettings: { + id: 'settings.app.closeSettings', + defaultMessage: 'Close settings', + }, +}); + +interface IProps extends WrappedComponentProps { + navigation: ReactElement; + closeSettings: () => void; +} + +@observer +class SettingsLayout extends Component { + constructor(props: IProps) { + super(props); + + this.handleKeyDown = this.handleKeyDown.bind(this); + } + + componentDidMount(): void { + document.addEventListener('keydown', this.handleKeyDown, false); + } + + componentWillUnmount(): void { + document.removeEventListener('keydown', this.handleKeyDown, false); + } + + handleKeyDown(e: KeyboardEvent): void { + if (isEscKeyPress(e.keyCode)) { + this.props.closeSettings(); + } + } + + render(): ReactElement { + const { navigation, closeSettings, intl } = this.props; + + return ( + +
+ + +
+ + +
+ ); + } +} + +export default injectIntl(SettingsLayout); diff --git a/src/components/settings/settings/EditSettingsForm.tsx b/src/components/settings/settings/EditSettingsForm.tsx index e796a48ec..8ccad9e49 100644 --- a/src/components/settings/settings/EditSettingsForm.tsx +++ b/src/components/settings/settings/EditSettingsForm.tsx @@ -1,5 +1,5 @@ import { systemPreferences } from '@electron/remote'; -import { Component } from 'react'; +import { Component, ReactElement } from 'react'; import { observer } from 'mobx-react'; import prettyBytes from 'pretty-bytes'; import { defineMessages, injectIntl, WrappedComponentProps } from 'react-intl'; @@ -9,7 +9,7 @@ import Button from '../../ui/button'; import Toggle from '../../ui/toggle'; import Select from '../../ui/Select'; import Input from '../../ui/input/index'; -import ColorPickerInput from '../../ui/ColorPickerInput'; +import ColorPickerInput from '../../ui/colorPickerInput'; import Infobox from '../../ui/Infobox'; import { H1, H2, H3, H5 } from '../../ui/headline'; import { @@ -258,14 +258,14 @@ const messages = defineMessages({ }, }); -const Hr = () => ( +const Hr = (): ReactElement => (
); -const HrSections = () => ( +const HrSections = (): ReactElement => (
{ this.setState({ clearCacheButtonClicked: true }); }; - submit(e) { - e.preventDefault(); + submit(e): void { + if (e) { + e.preventDefault(); + } + this.props.form.submit({ onSuccess: form => { const values = form.values(); @@ -344,7 +347,7 @@ class EditSettingsForm extends Component { }); } - render() { + render(): ReactElement { const { checkForUpdates, installUpdate, @@ -742,8 +745,8 @@ class EditSettingsForm extends Component { {intl.formatMessage(messages.overallTheme)}
this.submit(e)} - field={form.$('accentColor')} + {...form.$('accentColor').bind()} + onColorChange={this.submit.bind(this)} className="color-picker-input" />
@@ -752,8 +755,8 @@ class EditSettingsForm extends Component { {intl.formatMessage(messages.progressbarTheme)}
this.submit(e)} - field={form.$('progressbarAccentColor')} + {...form.$('progressbarAccentColor').bind()} + onColorChange={this.submit.bind(this)} className="color-picker-input" />
diff --git a/src/components/ui/ColorPickerInput.tsx b/src/components/ui/ColorPickerInput.tsx deleted file mode 100644 index da1fffb71..000000000 --- a/src/components/ui/ColorPickerInput.tsx +++ /dev/null @@ -1,102 +0,0 @@ -import { - ChangeEvent, - ChangeEventHandler, - Component, - createRef, - RefObject, -} from 'react'; -import { observer } from 'mobx-react'; -import classnames from 'classnames'; -import { SliderPicker } from 'react-color'; -import { noop } from 'lodash'; -import { Field } from '../../@types/mobx-form.types'; - -interface IProps { - field: Field; - className?: string; - focus?: boolean; - onChange: ChangeEventHandler; -} - -// TODO - [TS DEBT] check if field can be spread instead of having it single field attribute in interface -@observer -class ColorPickerInput extends Component { - private inputElement: RefObject = - createRef(); - - componentDidMount() { - const { focus = false } = this.props; - if (focus) { - this.focus(); - } - } - - onChange(e: ChangeEvent) { - const { field, onChange = noop } = this.props; - - onChange(e); - if (field.onChange) { - field.onChange(e); - } - } - - focus() { - if (this.inputElement && this.inputElement.current) { - this.inputElement.current.focus(); - } - } - - handleChangeComplete = (color: { hex: string }) => { - const { field } = this.props; - field.value = color.hex; - }; - - render() { - const { field, className = null } = this.props; - - let { type } = field; - type = 'text'; - - return ( -
- -
- this.onChange(e)} - onBlur={field.onBlur} - onFocus={field.onFocus} - ref={this.inputElement} - disabled={field.disabled} - /> -
-
- ); - } -} - -export default ColorPickerInput; diff --git a/src/components/ui/colorPickerInput/index.tsx b/src/components/ui/colorPickerInput/index.tsx new file mode 100644 index 000000000..9bab6efec --- /dev/null +++ b/src/components/ui/colorPickerInput/index.tsx @@ -0,0 +1,88 @@ +import { + Component, + createRef, + InputHTMLAttributes, + ReactElement, + RefObject, +} from 'react'; +import { observer } from 'mobx-react'; +import classnames from 'classnames'; +import { SliderPicker } from 'react-color'; +import { noop } from 'lodash'; +import { FormFields } from '../../../@types/mobx-form.types'; + +interface IProps extends InputHTMLAttributes, FormFields { + className?: string; + focus?: boolean; + onColorChange?: () => void; + error: string; +} + +@observer +class ColorPickerInput extends Component { + private inputElement: RefObject = + createRef(); + + componentDidMount(): void { + const { focus = false } = this.props; + if (focus && this.inputElement && this.inputElement.current) { + this.inputElement.current.focus(); + } + } + + onChange({ hex }: { hex: string }): void { + const { onColorChange = noop, onChange = noop } = this.props; + onColorChange(); + onChange(hex); + } + + render(): ReactElement { + const { + id, + name, + value = '', + placeholder = '', + disabled = false, + className = null, + type = 'text', + error = '', + onChange = noop, + } = this.props; + + return ( +
+ +
+ +
+
+ ); + } +} + +export default ColorPickerInput; diff --git a/src/components/ui/effects/Appear.tsx b/src/components/ui/effects/Appear.tsx index bf097b6a6..2076f6ba6 100644 --- a/src/components/ui/effects/Appear.tsx +++ b/src/components/ui/effects/Appear.tsx @@ -5,11 +5,22 @@ interface IProps { children: ReactNode; transitionName?: string; className?: string; + transitionAppear?: boolean; + transitionLeave?: boolean; + transitionAppearTimeout?: number; + transitionEnterTimeout?: number; + transitionLeaveTimeout?: number; } + const Appear = ({ children, transitionName = 'fadeIn', className = '', + transitionAppear = true, + transitionLeave = true, + transitionAppearTimeout = 1500, + transitionEnterTimeout = 1500, + transitionLeaveTimeout = 1500, }: IProps): ReactElement | null => { const [mounted, setMounted] = useState(false); @@ -24,11 +35,11 @@ const Appear = ({ return ( {children} diff --git a/src/containers/auth/ImportScreen.tsx b/src/containers/auth/ImportScreen.tsx deleted file mode 100644 index 91e985ad5..000000000 --- a/src/containers/auth/ImportScreen.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import { Component, ReactElement } from 'react'; -import { inject, observer } from 'mobx-react'; -import { StoresProps } from '../../@types/ferdium-components.types'; -import Import from '../../components/auth/Import'; - -class ImportScreen extends Component { - render(): ReactElement { - const { actions, stores } = this.props; - - if (stores.user.isImportLegacyServicesCompleted) { - stores.router.push(stores.user.inviteRoute); - } - - return ( - - ); - } -} - -export default inject('stores', 'actions')(observer(ImportScreen)); diff --git a/src/containers/auth/SetupAssistantScreen.tsx b/src/containers/auth/SetupAssistantScreen.tsx index 661d688aa..0d4c3feec 100644 --- a/src/containers/auth/SetupAssistantScreen.tsx +++ b/src/containers/auth/SetupAssistantScreen.tsx @@ -1,88 +1,93 @@ -/* eslint-disable no-await-in-loop */ import { Component, ReactElement } from 'react'; import { inject, observer } from 'mobx-react'; - import { StoresProps } from '../../@types/ferdium-components.types'; import sleep from '../../helpers/async-helpers'; import SetupAssistant from '../../components/auth/SetupAssistant'; +import { ILegacyServices } from '../../@types/legacy-types'; + +interface IProps extends StoresProps {} + +interface IState { + isSettingUpServices: boolean; +} + +@inject('stores', 'actions') +@observer +class SetupAssistantScreen extends Component { + services: ILegacyServices; + + constructor(props: IProps) { + super(props); -class SetupAssistantScreen extends Component { - state = { - isSettingUpServices: false, - }; + this.state = { + isSettingUpServices: false, + }; - // TODO: Why are these hardcoded here? Do they need to conform to specific services in the packaged recipes? If so, its more important to fix this - services = { - whatsapp: { - name: 'WhatsApp', - hasTeamId: false, - }, - messenger: { - name: 'Messenger', - hasTeamId: false, - }, - gmail: { - name: 'Gmail', - hasTeamId: false, - }, - skype: { - name: 'Skype', - hasTeamId: false, - }, - telegram: { - name: 'Telegram', - hasTeamId: false, - }, - instagram: { - name: 'Instagram', - hasTeamId: false, - }, - slack: { - name: 'Slack', - hasTeamId: true, - }, - hangouts: { - name: 'Hangouts', - hasTeamId: false, - }, - linkedin: { - name: 'LinkedIn', - hasTeamId: false, - }, - }; + // TODO: Why are these hardcoded here? Do they need to conform to specific services in the packaged recipes? If so, its more important to fix this + this.services = { + whatsapp: { + name: 'WhatsApp', + hasTeamId: false, + }, + messenger: { + name: 'Messenger', + hasTeamId: false, + }, + gmail: { + name: 'Gmail', + hasTeamId: false, + }, + skype: { + name: 'Skype', + hasTeamId: false, + }, + telegram: { + name: 'Telegram', + hasTeamId: false, + }, + instagram: { + name: 'Instagram', + hasTeamId: false, + }, + slack: { + name: 'Slack', + hasTeamId: true, + }, + hangouts: { + name: 'Hangouts', + hasTeamId: false, + }, + linkedin: { + name: 'LinkedIn', + hasTeamId: false, + }, + }; + } async setupServices(serviceConfig: any): Promise { - const { - stores: { services, router }, - } = this.props; + const { services, router } = this.props.stores; - this.setState({ - isSettingUpServices: true, - }); + this.setState({ isSettingUpServices: true }); // The store requests are not build for parallel requests so we need to finish one request after another for (const config of serviceConfig) { - const serviceData = { - name: this.services[config.id].name, - team: config.team, - }; - + // eslint-disable-next-line no-await-in-loop await services._createService({ recipeId: config.id, - serviceData, + serviceData: { + name: this.services[config.id].name, + team: config.team, + }, redirect: false, skipCleanup: true, }); + // eslint-disable-next-line no-await-in-loop await sleep(100); } - this.setState({ - isSettingUpServices: false, - }); - + this.setState({ isSettingUpServices: false }); await sleep(100); - router.push('/'); } @@ -91,11 +96,11 @@ class SetupAssistantScreen extends Component { this.setupServices(config)} services={this.services} - embed={false} + // embed={false} // TODO - [TS DEBT][PROP NOT USED IN COMPONENT] check legacy services type isSettingUpServices={this.state.isSettingUpServices} /> ); } } -export default inject('stores', 'actions')(observer(SetupAssistantScreen)); +export default SetupAssistantScreen; diff --git a/src/features/workspaces/actions.ts b/src/features/workspaces/actions.ts index 4c8b74450..b32bd7c86 100644 --- a/src/features/workspaces/actions.ts +++ b/src/features/workspaces/actions.ts @@ -7,6 +7,7 @@ export interface WorkspaceActions { toggleWorkspaceDrawer: () => void; deactivate: () => void; activate: (options: any) => void; + edit: ({ workspace }: { workspace: Workspace }) => void; } export default createActionsFromDefinitions( diff --git a/src/features/workspaces/components/WorkspaceDrawer.jsx b/src/features/workspaces/components/WorkspaceDrawer.jsx deleted file mode 100644 index b0b0e639a..000000000 --- a/src/features/workspaces/components/WorkspaceDrawer.jsx +++ /dev/null @@ -1,184 +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 ReactTooltip from 'react-tooltip'; - -import { mdiPlusBox, mdiCog } from '@mdi/js'; - -import { H1 } from '../../../components/ui/headline'; -import Icon from '../../../components/ui/icon'; -import WorkspaceDrawerItem from './WorkspaceDrawerItem'; -import workspaceActions from '../actions'; -import { workspaceStore } from '../index'; -import { getUserWorkspacesRequest } from '../api'; - -const messages = defineMessages({ - headline: { - id: 'workspaceDrawer.headline', - defaultMessage: 'Workspaces', - }, - allServices: { - id: 'workspaceDrawer.allServices', - defaultMessage: 'All services', - }, - workspacesSettingsTooltip: { - id: 'workspaceDrawer.workspacesSettingsTooltip', - defaultMessage: 'Edit workspaces settings', - }, - workspaceFeatureInfo: { - id: 'workspaceDrawer.workspaceFeatureInfo', - defaultMessage: - '

Ferdium Workspaces let you focus on what’s important right now. Set up different sets of services and easily switch between them at any time.

You decide which services you need when and where, so we can help you stay on top of your game - or easily switch off from work whenever you want.

', - }, - addNewWorkspaceLabel: { - id: 'workspaceDrawer.addNewWorkspaceLabel', - defaultMessage: 'Add new workspace', - }, -}); - -const styles = theme => ({ - drawer: { - background: theme.workspaces.drawer.background, - width: `${theme.workspaces.drawer.width}px`, - display: 'flex', - flexDirection: 'column', - }, - headline: { - fontSize: '24px', - marginTop: '38px', - marginBottom: '25px', - marginLeft: theme.workspaces.drawer.padding, - }, - workspacesSettingsButton: { - float: 'right', - marginRight: theme.workspaces.drawer.padding, - marginTop: '2px', - }, - workspacesSettingsButtonIcon: { - fill: theme.workspaces.drawer.buttons.color, - '&:hover': { - fill: theme.workspaces.drawer.buttons.hoverColor, - }, - }, - workspaces: { - height: 'auto', - overflowY: 'auto', - }, - addNewWorkspaceLabel: { - height: 'auto', - color: theme.workspaces.drawer.buttons.color, - margin: [40, 0], - textAlign: 'center', - '& > svg': { - fill: theme.workspaces.drawer.buttons.color, - }, - '& > span': { - fontSize: '13px', - marginLeft: 10, - position: 'relative', - top: -3, - }, - '&:hover': { - color: theme.workspaces.drawer.buttons.hoverColor, - '& > svg': { - fill: theme.workspaces.drawer.buttons.hoverColor, - }, - }, - }, -}); - -class WorkspaceDrawer extends Component { - static propTypes = { - classes: PropTypes.object.isRequired, - getServicesForWorkspace: PropTypes.func.isRequired, - }; - - componentDidMount() { - ReactTooltip.rebuild(); - try { - getUserWorkspacesRequest.execute(); - } catch (error) { - console.log(error); - } - } - - render() { - const { classes, getServicesForWorkspace } = this.props; - const { intl } = this.props; - const { activeWorkspace, isSwitchingWorkspace, nextWorkspace, workspaces } = - workspaceStore; - const actualWorkspace = isSwitchingWorkspace - ? nextWorkspace - : activeWorkspace; - return ( -
-

- {intl.formatMessage(messages.headline)} - { - workspaceActions.openWorkspaceSettings(); - }} - data-tip={`${intl.formatMessage( - messages.workspacesSettingsTooltip, - )}`} - > - - -

-
- { - workspaceActions.deactivate(); - workspaceActions.toggleWorkspaceDrawer(); - }} - services={getServicesForWorkspace(null)} - isActive={actualWorkspace == null} - shortcutIndex={0} - /> - {workspaces.map((workspace, index) => ( - { - if (actualWorkspace === workspace) return; - workspaceActions.activate({ workspace }); - workspaceActions.toggleWorkspaceDrawer(); - }} - onContextMenuEditClick={() => - workspaceActions.edit({ workspace }) - } - services={getServicesForWorkspace(workspace)} - shortcutIndex={index + 1} - /> - ))} -
{ - workspaceActions.openWorkspaceSettings(); - }} - > - - {intl.formatMessage(messages.addNewWorkspaceLabel)} -
-
- -
- ); - } -} - -export default injectIntl( - injectSheet(styles, { injectTheme: true })(observer(WorkspaceDrawer)), -); diff --git a/src/features/workspaces/components/WorkspaceDrawer.tsx b/src/features/workspaces/components/WorkspaceDrawer.tsx new file mode 100644 index 000000000..bdbebdb0a --- /dev/null +++ b/src/features/workspaces/components/WorkspaceDrawer.tsx @@ -0,0 +1,186 @@ +import { Component, ReactElement } from 'react'; +import { observer } from 'mobx-react'; +import withStyles, { WithStylesProps } from 'react-jss'; +import { defineMessages, injectIntl, WrappedComponentProps } from 'react-intl'; +import ReactTooltip from 'react-tooltip'; +import { mdiPlusBox, mdiCog } from '@mdi/js'; +import { noop } from 'lodash'; +import { H1 } from '../../../components/ui/headline'; +import Icon from '../../../components/ui/icon'; +import WorkspaceDrawerItem from './WorkspaceDrawerItem'; +import workspaceActions from '../actions'; +import { workspaceStore } from '../index'; +import { getUserWorkspacesRequest } from '../api'; +import Service from '../../../models/Service'; +import Workspace from '../models/Workspace'; + +const messages = defineMessages({ + headline: { + id: 'workspaceDrawer.headline', + defaultMessage: 'Workspaces', + }, + allServices: { + id: 'workspaceDrawer.allServices', + defaultMessage: 'All services', + }, + workspacesSettingsTooltip: { + id: 'workspaceDrawer.workspacesSettingsTooltip', + defaultMessage: 'Edit workspaces settings', + }, + workspaceFeatureInfo: { + id: 'workspaceDrawer.workspaceFeatureInfo', + defaultMessage: + '

Ferdium Workspaces let you focus on what’s important right now. Set up different sets of services and easily switch between them at any time.

You decide which services you need when and where, so we can help you stay on top of your game - or easily switch off from work whenever you want.

', + }, + addNewWorkspaceLabel: { + id: 'workspaceDrawer.addNewWorkspaceLabel', + defaultMessage: 'Add new workspace', + }, +}); + +const styles = theme => ({ + drawer: { + background: theme.workspaces.drawer.background, + width: `${theme.workspaces.drawer.width}px`, + display: 'flex', + flexDirection: 'column', + }, + headline: { + fontSize: '24px', + marginTop: '38px', + marginBottom: '25px', + marginLeft: theme.workspaces.drawer.padding, + }, + workspacesSettingsButton: { + float: 'right', + marginRight: theme.workspaces.drawer.padding, + marginTop: '2px', + }, + workspacesSettingsButtonIcon: { + fill: theme.workspaces.drawer.buttons.color, + '&:hover': { + fill: theme.workspaces.drawer.buttons.hoverColor, + }, + }, + workspaces: { + height: 'auto', + overflowY: 'auto', + }, + addNewWorkspaceLabel: { + height: 'auto', + color: theme.workspaces.drawer.buttons.color, + margin: [40, 0], + textAlign: 'center', + '& > svg': { + fill: theme.workspaces.drawer.buttons.color, + }, + '& > span': { + fontSize: '13px', + marginLeft: 10, + position: 'relative', + top: -3, + }, + '&:hover': { + color: theme.workspaces.drawer.buttons.hoverColor, + '& > svg': { + fill: theme.workspaces.drawer.buttons.hoverColor, + }, + }, + }, +}); + +interface IProps extends WithStylesProps, WrappedComponentProps { + getServicesForWorkspace: (workspace: Workspace | null) => Service[]; +} + +@observer +class WorkspaceDrawer extends Component { + componentDidMount(): void { + try { + ReactTooltip.rebuild(); + getUserWorkspacesRequest.execute(); + } catch (error) { + console.log(error); + } + } + + render(): ReactElement { + const { classes, getServicesForWorkspace } = this.props; + const { intl } = this.props; + const { activeWorkspace, isSwitchingWorkspace, nextWorkspace, workspaces } = + workspaceStore; + const actualWorkspace = isSwitchingWorkspace + ? nextWorkspace + : activeWorkspace; + return ( +
+

+ {intl.formatMessage(messages.headline)} + { + workspaceActions.openWorkspaceSettings(); + }} + data-tip={`${intl.formatMessage( + messages.workspacesSettingsTooltip, + )}`} + > + + +

+
+ { + workspaceActions.deactivate(); + workspaceActions.toggleWorkspaceDrawer(); + }} + services={getServicesForWorkspace(null)} + isActive={actualWorkspace == null} + shortcutIndex={0} + /> + {workspaces.map((workspace, index) => ( + { + if (actualWorkspace === workspace) return; + workspaceActions.activate({ workspace }); + workspaceActions.toggleWorkspaceDrawer(); + }} + onContextMenuEditClick={() => + workspaceActions.edit({ workspace }) + } + services={getServicesForWorkspace(workspace)} + shortcutIndex={index + 1} + /> + ))} +
{ + workspaceActions.openWorkspaceSettings(); + }} + onKeyDown={noop} + > + + {intl.formatMessage(messages.addNewWorkspaceLabel)} +
+
+ +
+ ); + } +} + +export default injectIntl( + withStyles(styles, { injectTheme: true })(WorkspaceDrawer), +); diff --git a/src/i18n/locales/en-US.json b/src/i18n/locales/en-US.json index 10c92216d..31b22847c 100644 --- a/src/i18n/locales/en-US.json +++ b/src/i18n/locales/en-US.json @@ -38,10 +38,6 @@ "global.userAgentHelp": "Use 'https://whatmyuseragent.com/' (to discover) or 'https://developers.whatismybrowser.com/useragents/explore/' (to choose) your desired user agent and copy-paste it here.", "global.userAgentPref": "User Agent", "global.yes": "Yes", - "import.headline": "Import your Ferdium 4 services", - "import.notSupportedHeadline": "Services not yet supported in Ferdium 5", - "import.skip.label": "I want to add services manually", - "import.submit.label": "Import {count} services", "infobar.authRequestFailed": "There were errors while trying to perform an authenticated request. Please try logging out and back in if this error persists.", "infobar.buttonChangelog": "What is new?", "infobar.buttonInstallUpdate": "Restart & install update", diff --git a/src/models/Recipe.ts b/src/models/Recipe.ts index 8b4c7a8ba..9a28a59ac 100644 --- a/src/models/Recipe.ts +++ b/src/models/Recipe.ts @@ -60,6 +60,7 @@ export interface IRecipe { author?: string[]; hasDarkMode?: boolean; validateUrl?: (url: string) => boolean; + icons?: any; } export default class Recipe implements IRecipe { diff --git a/src/routes.tsx b/src/routes.tsx index 696d7762f..c0b637c3c 100644 --- a/src/routes.tsx +++ b/src/routes.tsx @@ -26,7 +26,6 @@ import AuthReleaseNotesScreen from './containers/auth/AuthReleaseNotesScreen'; import PasswordScreen from './containers/auth/PasswordScreen'; import ChangeServerScreen from './containers/auth/ChangeServerScreen'; import SignupScreen from './containers/auth/SignupScreen'; -import ImportScreen from './containers/auth/ImportScreen'; import SetupAssistantScreen from './containers/auth/SetupAssistantScreen'; import InviteScreen from './containers/auth/InviteScreen'; import AuthLayoutContainer from './containers/auth/AuthLayoutContainer'; @@ -80,10 +79,6 @@ class FerdiumRoutes extends Component { path="/auth/signup/form" element={} /> - } - /> } -- cgit v1.2.3-70-g09d2